tetris v1
This commit is contained in:
parent
3a96f71760
commit
eb1f14d19c
501
dstetris/raw/index.html
Normal file
501
dstetris/raw/index.html
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 200px;
|
||||||
|
background: #222;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-canvas, .hold-canvas {
|
||||||
|
border: 1px solid #444;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
color: #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.disconnected {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Hold</h3>
|
||||||
|
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Next</h3>
|
||||||
|
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="score-info">
|
||||||
|
<div>Score<br><span id="score">0</span></div>
|
||||||
|
<div>Level<br><span id="level">1</span></div>
|
||||||
|
<div>Lines<br><span id="lines">0</span></div>
|
||||||
|
<div>Goal<br><span id="goal">5</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<div>Status: <span id="connectionStatus" class="connection-status">Connecting...</span></div>
|
||||||
|
<div>Game: <span id="gameStatus">Not Started</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-game">
|
||||||
|
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="startGame()">Start Game</button>
|
||||||
|
<button onclick="location.reload()">Refresh</button>
|
||||||
|
|
||||||
|
<h3>Controls</h3>
|
||||||
|
<p>← → Move</p>
|
||||||
|
<p>↓ Soft Drop</p>
|
||||||
|
<p>Space Hard Drop</p>
|
||||||
|
<p>Z Rotate Left</p>
|
||||||
|
<p>X Rotate Right</p>
|
||||||
|
<p>C Hold</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameOver" class="game-over" style="display: none;">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
|
<p>Level Reached: <span id="finalLevel">1</span></p>
|
||||||
|
<p>Lines Cleared: <span id="finalLines">0</span></p>
|
||||||
|
<button onclick="startGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('nextCanvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const holdCanvas = document.getElementById('holdCanvas');
|
||||||
|
const holdCtx = holdCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const CELL_SIZE = 30;
|
||||||
|
const COLORS = [
|
||||||
|
'#000000', // Empty
|
||||||
|
'#00f5ff', // I - Cyan
|
||||||
|
'#ffff00', // O - Yellow
|
||||||
|
'#800080', // T - Purple
|
||||||
|
'#00ff00', // S - Green
|
||||||
|
'#ff0000', // Z - Red
|
||||||
|
'#0000ff', // J - Blue
|
||||||
|
'#ffa500' // L - Orange
|
||||||
|
];
|
||||||
|
|
||||||
|
let gameState = null;
|
||||||
|
let eventSource = null;
|
||||||
|
|
||||||
|
// Tetrimino shapes for rendering (only first rotation needed for display)
|
||||||
|
const shapes = {
|
||||||
|
1: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I
|
||||||
|
2: [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // O
|
||||||
|
3: [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // T
|
||||||
|
4: [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], // S
|
||||||
|
5: [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // Z
|
||||||
|
6: [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // J
|
||||||
|
7: [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]] // L
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current piece shapes with all rotations
|
||||||
|
const currentPieceShapes = {
|
||||||
|
1: [ // I piece
|
||||||
|
[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,1,0],[0,0,1,0],[0,0,1,0],[0,0,1,0]],
|
||||||
|
[[0,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,0,0],[0,1,0,0],[0,1,0,0]]
|
||||||
|
],
|
||||||
|
2: [ // O piece
|
||||||
|
[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
3: [ // T piece
|
||||||
|
[[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[1,1,1,0],[0,1,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
4: [ // S piece
|
||||||
|
[[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,1,0],[0,0,1,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[0,1,1,0],[1,1,0,0],[0,0,0,0]],
|
||||||
|
[[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
5: [ // Z piece
|
||||||
|
[[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,1,0],[0,1,1,0],[0,1,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[1,1,0,0],[0,1,1,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[1,1,0,0],[1,0,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
6: [ // J piece
|
||||||
|
[[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,1,0],[0,1,0,0],[0,1,0,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[1,1,1,0],[0,0,1,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,0,0],[1,1,0,0],[0,0,0,0]]
|
||||||
|
],
|
||||||
|
7: [ // L piece
|
||||||
|
[[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]],
|
||||||
|
[[0,1,0,0],[0,1,0,0],[0,1,1,0],[0,0,0,0]],
|
||||||
|
[[0,0,0,0],[1,1,1,0],[1,0,0,0],[0,0,0,0]],
|
||||||
|
[[1,1,0,0],[0,1,0,0],[0,1,0,0],[0,0,0,0]]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectEventSource() {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource = new EventSource('/events');
|
||||||
|
|
||||||
|
eventSource.onopen = function() {
|
||||||
|
updateConnectionStatus('Connected', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onmessage = function(event) {
|
||||||
|
gameState = JSON.parse(event.data);
|
||||||
|
render();
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function(event) {
|
||||||
|
console.log('EventSource failed:', event);
|
||||||
|
updateConnectionStatus('Disconnected', false);
|
||||||
|
setTimeout(connectEventSource, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus(status, connected) {
|
||||||
|
const statusElement = document.getElementById('connectionStatus');
|
||||||
|
statusElement.textContent = status;
|
||||||
|
statusElement.className = connected ? 'connection-status' : 'connection-status disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAction(action, direction = '') {
|
||||||
|
const url = '/action?action=' + action + (direction ? '&direction=' + direction : '');
|
||||||
|
fetch(url, { method: 'POST' }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
document.getElementById('gameOver').style.display = 'none';
|
||||||
|
sendAction('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
// Clear main canvas
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw board (only visible part: rows 0-19, but our array is 0-39)
|
||||||
|
for (let y = 0; y < 20; y++) {
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
const cellValue = gameState.board[y][x];
|
||||||
|
if (cellValue > 0) {
|
||||||
|
ctx.fillStyle = COLORS[cellValue];
|
||||||
|
ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current piece
|
||||||
|
if (gameState.current_piece && gameState.game_running) {
|
||||||
|
const piece = gameState.current_piece;
|
||||||
|
const shape = currentPieceShapes[piece.type][piece.rotation];
|
||||||
|
if (shape) {
|
||||||
|
ctx.fillStyle = COLORS[piece.type];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
const boardX = piece.x + px;
|
||||||
|
const boardY = piece.y + py;
|
||||||
|
if (boardY < 20) { // Only draw visible part
|
||||||
|
ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#666';
|
||||||
|
ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let x = 0; x <= 10; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * CELL_SIZE, 0);
|
||||||
|
ctx.lineTo(x * CELL_SIZE, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= 20; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * CELL_SIZE);
|
||||||
|
ctx.lineTo(canvas.width, y * CELL_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNextPieces();
|
||||||
|
renderHoldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNextPieces() {
|
||||||
|
nextCtx.fillStyle = '#111';
|
||||||
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.next_pieces) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) {
|
||||||
|
const pieceType = gameState.next_pieces[i];
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
nextCtx.fillStyle = COLORS[pieceType];
|
||||||
|
const offsetY = i * 50 + 5;
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
nextCtx.strokeStyle = '#666';
|
||||||
|
nextCtx.strokeRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHoldPiece() {
|
||||||
|
holdCtx.fillStyle = '#111';
|
||||||
|
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.hold_piece) return;
|
||||||
|
|
||||||
|
const pieceType = gameState.hold_piece;
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
holdCtx.fillStyle = gameState.can_hold ? COLORS[pieceType] : '#666';
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
holdCtx.strokeStyle = '#444';
|
||||||
|
holdCtx.strokeRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
document.getElementById('score').textContent = gameState.score.toLocaleString();
|
||||||
|
document.getElementById('level').textContent = gameState.level;
|
||||||
|
document.getElementById('lines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level;
|
||||||
|
|
||||||
|
// Update game status
|
||||||
|
let gameStatus = 'Not Started';
|
||||||
|
if (gameState.game_over) {
|
||||||
|
gameStatus = 'Game Over';
|
||||||
|
} else if (gameState.game_running) {
|
||||||
|
gameStatus = 'Playing';
|
||||||
|
}
|
||||||
|
document.getElementById('gameStatus').textContent = gameStatus;
|
||||||
|
|
||||||
|
if (gameState.game_over) {
|
||||||
|
document.getElementById('finalScore').textContent = gameState.score.toLocaleString();
|
||||||
|
document.getElementById('finalLevel').textContent = gameState.level;
|
||||||
|
document.getElementById('finalLines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (!gameState || !gameState.game_running) return;
|
||||||
|
|
||||||
|
switch(event.code) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'right');
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'down');
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hard_drop');
|
||||||
|
break;
|
||||||
|
case 'KeyZ':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'ccw');
|
||||||
|
break;
|
||||||
|
case 'KeyX':
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'cw');
|
||||||
|
break;
|
||||||
|
case 'KeyC':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hold');
|
||||||
|
break;
|
||||||
|
case 'KeyR':
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent context menu on right click
|
||||||
|
document.addEventListener('contextmenu', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize connection when page loads
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
connectEventSource();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle page visibility changes
|
||||||
|
document.addEventListener('visibilitychange', function() {
|
||||||
|
if (document.hidden) {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connectEventSource();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
464
dstetris/raw/main.go
Normal file
464
dstetris/raw/main.go
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle using current time
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, _ := json.Marshal(g.state)
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
default:
|
||||||
|
close(client)
|
||||||
|
delete(g.clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Serve static files (index.html)
|
||||||
|
http.Handle("/", http.FileServer(http.Dir("./")))
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :8080")
|
||||||
|
fmt.Println("Open http://localhost:8080 in your browser")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
client := make(chan string)
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
53
dstetris/specs.txt
Normal file
53
dstetris/specs.txt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
Here's the simplified technical information from the Tetris guidelines:
|
||||||
|
|
||||||
|
**Playfield & Display**
|
||||||
|
- 10×40 grid (rows above 20 are hidden, appearing as 10×20)
|
||||||
|
- Ghost piece enabled by default
|
||||||
|
- 1-6 next pieces shown (6 recommended)
|
||||||
|
|
||||||
|
**Tetrimino Colors**
|
||||||
|
- I: Cyan
|
||||||
|
- O: Yellow
|
||||||
|
- T: Purple
|
||||||
|
- S: Green
|
||||||
|
- Z: Red
|
||||||
|
- J: Blue
|
||||||
|
- L: Orange
|
||||||
|
|
||||||
|
**Spawn Rules**
|
||||||
|
- I and O spawn in middle columns
|
||||||
|
- J, L, T, S, Z spawn in left-middle columns
|
||||||
|
- All spawn horizontally at row 21 (I) or 21/22 (others)
|
||||||
|
- Drop one space immediately if path is clear
|
||||||
|
|
||||||
|
**Controls (Standard)**
|
||||||
|
- Hard drop: Up arrow/Space
|
||||||
|
- Soft drop: Down arrow
|
||||||
|
- Move: Left/Right arrows
|
||||||
|
- Rotate clockwise: X
|
||||||
|
- Rotate counterclockwise: Z/Ctrl
|
||||||
|
- Hold: Shift/C
|
||||||
|
|
||||||
|
**Core Mechanics**
|
||||||
|
- Super Rotation System (SRS)
|
||||||
|
- 7-bag random generator
|
||||||
|
- Hold piece function (default enabled)
|
||||||
|
- Half-second lock delay
|
||||||
|
- 15 moves/rotations before lock (multiplayer/arcade)
|
||||||
|
|
||||||
|
**Scoring & Progression**
|
||||||
|
- Level up by clearing lines or T-spins
|
||||||
|
- Variable goal: 5× level number lines needed
|
||||||
|
- Line values: Single=1, Double=3, Triple=5, Tetris=8
|
||||||
|
- T-spin recognition (3-corner rule)
|
||||||
|
- Back-to-back bonus chains
|
||||||
|
|
||||||
|
**Game Modes**
|
||||||
|
- Marathon: 15 levels
|
||||||
|
- Sprint: 40 lines
|
||||||
|
- Ultra: 2-3 minutes timed
|
||||||
|
|
||||||
|
**Top Out Conditions**
|
||||||
|
- Block out: piece spawns overlapping existing blocks
|
||||||
|
- Lock out: piece locks completely above visible area
|
||||||
|
|
||||||
841
dstetris/temp/tetris_main (1).go
Normal file
841
dstetris/temp/tetris_main (1).go
Normal file
@ -0,0 +1,841 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, _ := json.Marshal(g.state)
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
default:
|
||||||
|
close(client)
|
||||||
|
delete(g.clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", serveHTML)
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
client := make(chan string)
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveHTML(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
fmt.Fprint(w, htmlContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 200px;
|
||||||
|
background: #222;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-canvas, .hold-canvas {
|
||||||
|
border: 1px solid #444;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Hold</h3>
|
||||||
|
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Next</h3>
|
||||||
|
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="score-info">
|
||||||
|
<div>Score<br><span id="score">0</span></div>
|
||||||
|
<div>Level<br><span id="level">1</span></div>
|
||||||
|
<div>Lines<br><span id="lines">0</span></div>
|
||||||
|
<div>Goal<br><span id="goal">5</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-game">
|
||||||
|
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="startGame()">Start Game</button>
|
||||||
|
|
||||||
|
<h3>Controls</h3>
|
||||||
|
<p>← → Move</p>
|
||||||
|
<p>↓ Soft Drop</p>
|
||||||
|
<p>Space Hard Drop</p>
|
||||||
|
<p>Z Rotate Left</p>
|
||||||
|
<p>X Rotate Right</p>
|
||||||
|
<p>C Hold</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameOver" class="game-over" style="display: none;">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
|
<button onclick="startGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('nextCanvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const holdCanvas = document.getElementById('holdCanvas');
|
||||||
|
const holdCtx = holdCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const CELL_SIZE = 30;
|
||||||
|
const COLORS = [
|
||||||
|
'#000000', // Empty
|
||||||
|
'#00f5ff', // I - Cyan
|
||||||
|
'#ffff00', // O - Yellow
|
||||||
|
'#800080', // T - Purple
|
||||||
|
'#00ff00', // S - Green
|
||||||
|
'#ff0000', // Z - Red
|
||||||
|
'#0000ff', // J - Blue
|
||||||
|
'#ffa500' // L - Orange
|
||||||
|
];
|
||||||
|
|
||||||
|
let gameState = null;
|
||||||
|
let eventSource = null;
|
||||||
|
|
||||||
|
// Tetrimino shapes for rendering
|
||||||
|
const shapes = {
|
||||||
|
1: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I
|
||||||
|
2: [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // O
|
||||||
|
3: [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // T
|
||||||
|
4: [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], // S
|
||||||
|
5: [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // Z
|
||||||
|
6: [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // J
|
||||||
|
7: [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]] // L
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectEventSource() {
|
||||||
|
eventSource = new EventSource('/events');
|
||||||
|
eventSource.onmessage = function(event) {
|
||||||
|
gameState = JSON.parse(event.data);
|
||||||
|
render();
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function(event) {
|
||||||
|
console.log('EventSource failed:', event);
|
||||||
|
setTimeout(connectEventSource, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAction(action, direction = '') {
|
||||||
|
const url = '/action?action=' + action + (direction ? '&direction=' + direction : '');
|
||||||
|
fetch(url, { method: 'POST' }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
document.getElementById('gameOver').style.display = 'none';
|
||||||
|
sendAction('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
// Clear main canvas
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw board (only visible part: rows 0-19, but our array is 0-39)
|
||||||
|
for (let y = 0; y < 20; y++) {
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
const cellValue = gameState.board[y][x];
|
||||||
|
if (cellValue > 0) {
|
||||||
|
ctx.fillStyle = COLORS[cellValue];
|
||||||
|
ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current piece
|
||||||
|
if (gameState.current_piece && gameState.game_running) {
|
||||||
|
const piece = gameState.current_piece;
|
||||||
|
const shape = shapes[piece.type];
|
||||||
|
if (shape) {
|
||||||
|
ctx.fillStyle = COLORS[piece.type];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHoldPiece() {
|
||||||
|
holdCtx.fillStyle = '#111';
|
||||||
|
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.hold_piece) return;
|
||||||
|
|
||||||
|
const pieceType = gameState.hold_piece;
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
holdCtx.fillStyle = COLORS[pieceType];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
document.getElementById('score').textContent = gameState.score;
|
||||||
|
document.getElementById('level').textContent = gameState.level;
|
||||||
|
document.getElementById('lines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level;
|
||||||
|
|
||||||
|
if (gameState.game_over) {
|
||||||
|
document.getElementById('finalScore').textContent = gameState.score;
|
||||||
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (!gameState || !gameState.game_running) return;
|
||||||
|
|
||||||
|
switch(event.code) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'right');
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'down');
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hard_drop');
|
||||||
|
break;
|
||||||
|
case 'KeyZ':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'ccw');
|
||||||
|
break;
|
||||||
|
case 'KeyX':
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'cw');
|
||||||
|
break;
|
||||||
|
case 'KeyC':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hold');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
connectEventSource();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
if (shape[py][px]) {
|
||||||
|
const boardX = piece.x + px;
|
||||||
|
const boardY = piece.y + py;
|
||||||
|
if (boardY < 20) { // Only draw visible part
|
||||||
|
ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#666';
|
||||||
|
ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
for (let x = 0; x <= 10; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * CELL_SIZE, 0);
|
||||||
|
ctx.lineTo(x * CELL_SIZE, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= 20; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * CELL_SIZE);
|
||||||
|
ctx.lineTo(canvas.width, y * CELL_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNextPieces();
|
||||||
|
renderHoldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNextPieces() {
|
||||||
|
nextCtx.fillStyle = '#111';
|
||||||
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.next_pieces) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) {
|
||||||
|
const pieceType = gameState.next_pieces[i];
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
nextCtx.fillStyle = COLORS[pieceType];
|
||||||
|
const offsetY = i * 50 + 5;
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
842
dstetris/temp/tetris_main.go
Normal file
842
dstetris/temp/tetris_main.go
Normal file
@ -0,0 +1,842 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, _ := json.Marshal(g.state)
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
default:
|
||||||
|
close(client)
|
||||||
|
delete(g.clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", serveHTML)
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :8080")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
client := make(chan string)
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveHTML(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
fmt.Fprint(w, htmlContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 200px;
|
||||||
|
background: #222;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-canvas, .hold-canvas {
|
||||||
|
border: 1px solid #444;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Hold</h3>
|
||||||
|
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Next</h3>
|
||||||
|
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="score-info">
|
||||||
|
<div>Score<br><span id="score">0</span></div>
|
||||||
|
<div>Level<br><span id="level">1</span></div>
|
||||||
|
<div>Lines<br><span id="lines">0</span></div>
|
||||||
|
<div>Goal<br><span id="goal">5</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-game">
|
||||||
|
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="startGame()">Start Game</button>
|
||||||
|
|
||||||
|
<h3>Controls</h3>
|
||||||
|
<p>← → Move</p>
|
||||||
|
<p>↓ Soft Drop</p>
|
||||||
|
<p>Space Hard Drop</p>
|
||||||
|
<p>Z Rotate Left</p>
|
||||||
|
<p>X Rotate Right</p>
|
||||||
|
<p>C Hold</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameOver" class="game-over" style="display: none;">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
|
<button onclick="startGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('nextCanvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const holdCanvas = document.getElementById('holdCanvas');
|
||||||
|
const holdCtx = holdCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const CELL_SIZE = 30;
|
||||||
|
const COLORS = [
|
||||||
|
'#000000', // Empty
|
||||||
|
'#00f5ff', // I - Cyan
|
||||||
|
'#ffff00', // O - Yellow
|
||||||
|
'#800080', // T - Purple
|
||||||
|
'#00ff00', // S - Green
|
||||||
|
'#ff0000', // Z - Red
|
||||||
|
'#0000ff', // J - Blue
|
||||||
|
'#ffa500' // L - Orange
|
||||||
|
];
|
||||||
|
|
||||||
|
let gameState = null;
|
||||||
|
let eventSource = null;
|
||||||
|
|
||||||
|
// Tetrimino shapes for rendering
|
||||||
|
const shapes = {
|
||||||
|
1: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I
|
||||||
|
2: [[0,1,1,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // O
|
||||||
|
3: [[0,1,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // T
|
||||||
|
4: [[0,1,1,0],[1,1,0,0],[0,0,0,0],[0,0,0,0]], // S
|
||||||
|
5: [[1,1,0,0],[0,1,1,0],[0,0,0,0],[0,0,0,0]], // Z
|
||||||
|
6: [[1,0,0,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]], // J
|
||||||
|
7: [[0,0,1,0],[1,1,1,0],[0,0,0,0],[0,0,0,0]] // L
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectEventSource() {
|
||||||
|
eventSource = new EventSource('/events');
|
||||||
|
eventSource.onmessage = function(event) {
|
||||||
|
gameState = JSON.parse(event.data);
|
||||||
|
render();
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function(event) {
|
||||||
|
console.log('EventSource failed:', event);
|
||||||
|
setTimeout(connectEventSource, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAction(action, direction = '') {
|
||||||
|
const url = '/action?action=' + action + (direction ? '&direction=' + direction : '');
|
||||||
|
fetch(url, { method: 'POST' }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
document.getElementById('gameOver').style.display = 'none';
|
||||||
|
sendAction('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
// Clear main canvas
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw board (only visible part: rows 0-19, but our array is 0-39)
|
||||||
|
for (let y = 0; y < 20; y++) {
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
const cellValue = gameState.board[y][x];
|
||||||
|
if (cellValue > 0) {
|
||||||
|
ctx.fillStyle = COLORS[cellValue];
|
||||||
|
ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current piece
|
||||||
|
if (gameState.current_piece && gameState.game_running) {
|
||||||
|
const piece = gameState.current_piece;
|
||||||
|
const shape = shapes[piece.type];
|
||||||
|
if (shape) {
|
||||||
|
nextCtx.fillStyle = COLORS[pieceType];
|
||||||
|
const offsetY = i * 50 + 5;
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHoldPiece() {
|
||||||
|
holdCtx.fillStyle = '#111';
|
||||||
|
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.hold_piece) return;
|
||||||
|
|
||||||
|
const pieceType = gameState.hold_piece;
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
holdCtx.fillStyle = COLORS[pieceType];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
document.getElementById('score').textContent = gameState.score;
|
||||||
|
document.getElementById('level').textContent = gameState.level;
|
||||||
|
document.getElementById('lines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level;
|
||||||
|
|
||||||
|
if (gameState.game_over) {
|
||||||
|
document.getElementById('finalScore').textContent = gameState.score;
|
||||||
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
document.addEventListener('keydown', function(event) {
|
||||||
|
if (!gameState || !gameState.game_running) return;
|
||||||
|
|
||||||
|
switch(event.code) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'right');
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'down');
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hard_drop');
|
||||||
|
break;
|
||||||
|
case 'KeyZ':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'ccw');
|
||||||
|
break;
|
||||||
|
case 'KeyX':
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'cw');
|
||||||
|
break;
|
||||||
|
case 'KeyC':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hold');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
connectEventSource();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
ctx.fillStyle = COLORS[piece.type];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
const boardX = piece.x + px;
|
||||||
|
const boardY = piece.y + py;
|
||||||
|
if (boardY < 20) { // Only draw visible part
|
||||||
|
ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#666';
|
||||||
|
ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
for (let x = 0; x <= 10; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * CELL_SIZE, 0);
|
||||||
|
ctx.lineTo(x * CELL_SIZE, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= 20; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * CELL_SIZE);
|
||||||
|
ctx.lineTo(canvas.width, y * CELL_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNextPieces();
|
||||||
|
renderHoldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNextPieces() {
|
||||||
|
nextCtx.fillStyle = '#111';
|
||||||
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.next_pieces) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) {
|
||||||
|
const pieceType = gameState.next_pieces[i];
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
66
dstetris/v1/frontend/index.html
Normal file
66
dstetris/v1/frontend/index.html
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tetris</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="main.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-container">
|
||||||
|
<div class="info-panel">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Hold</h3>
|
||||||
|
<canvas id="holdCanvas" class="hold-canvas" width="80" height="80"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>Next</h3>
|
||||||
|
<canvas id="nextCanvas" class="next-canvas" width="80" height="320"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-section">
|
||||||
|
<div class="score-info">
|
||||||
|
<div>Score<br><span id="score">0</span></div>
|
||||||
|
<div>Level<br><span id="level">1</span></div>
|
||||||
|
<div>Lines<br><span id="lines">0</span></div>
|
||||||
|
<div>Goal<br><span id="goal">5</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status">
|
||||||
|
<div>Status: <span id="connectionStatus" class="connection-status">Connecting...</span></div>
|
||||||
|
<div>Game: <span id="gameStatus">Not Started</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-game">
|
||||||
|
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="startGame()">Start Game</button>
|
||||||
|
<button onclick="location.reload()">Refresh</button>
|
||||||
|
|
||||||
|
<h3>Controls</h3>
|
||||||
|
<p>← → Move</p>
|
||||||
|
<p>↓ Soft Drop</p>
|
||||||
|
<p>Space Hard Drop</p>
|
||||||
|
<p>Z Rotate Left</p>
|
||||||
|
<p>X Rotate Right</p>
|
||||||
|
<p>C Hold</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gameOver" class="game-over" style="display: none;">
|
||||||
|
<h2>Game Over</h2>
|
||||||
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
|
<p>Level Reached: <span id="finalLevel">1</span></p>
|
||||||
|
<p>Lines Cleared: <span id="finalLines">0</span></p>
|
||||||
|
<button onclick="startGame()">Play Again</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
316
dstetris/v1/frontend/main.js
Normal file
316
dstetris/v1/frontend/main.js
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
const canvas = document.getElementById('gameCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const nextCanvas = document.getElementById('nextCanvas');
|
||||||
|
const nextCtx = nextCanvas.getContext('2d');
|
||||||
|
const holdCanvas = document.getElementById('holdCanvas');
|
||||||
|
const holdCtx = holdCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const CELL_SIZE = 30;
|
||||||
|
const COLORS = [
|
||||||
|
'#000000', // Empty
|
||||||
|
'#00f5ff', // I - Cyan
|
||||||
|
'#ffff00', // O - Yellow
|
||||||
|
'#800080', // T - Purple
|
||||||
|
'#00ff00', // S - Green
|
||||||
|
'#ff0000', // Z - Red
|
||||||
|
'#0000ff', // J - Blue
|
||||||
|
'#ffa500' // L - Orange
|
||||||
|
];
|
||||||
|
|
||||||
|
let gameState = null;
|
||||||
|
let eventSource = null;
|
||||||
|
|
||||||
|
// Tetrimino shapes for rendering (only first rotation needed for display)
|
||||||
|
const shapes = {
|
||||||
|
1: [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], // I
|
||||||
|
2: [[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // O
|
||||||
|
3: [[0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // T
|
||||||
|
4: [[0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // S
|
||||||
|
5: [[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // Z
|
||||||
|
6: [[1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]], // J
|
||||||
|
7: [[0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]] // L
|
||||||
|
};
|
||||||
|
|
||||||
|
// Current piece shapes with all rotations
|
||||||
|
const currentPieceShapes = {
|
||||||
|
1: [ // I piece
|
||||||
|
[[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0], [0, 0, 1, 0]],
|
||||||
|
[[0, 0, 0, 0], [0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0]]
|
||||||
|
],
|
||||||
|
2: [ // O piece
|
||||||
|
[[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 1, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
3: [ // T piece
|
||||||
|
[[0, 1, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [1, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
4: [ // S piece
|
||||||
|
[[0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [0, 1, 1, 0], [1, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[1, 0, 0, 0], [1, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
5: [ // Z piece
|
||||||
|
[[1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 1, 0], [0, 1, 1, 0], [0, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [1, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
6: [ // J piece
|
||||||
|
[[1, 0, 0, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 1, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [1, 1, 1, 0], [0, 0, 1, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 0, 0], [1, 1, 0, 0], [0, 0, 0, 0]]
|
||||||
|
],
|
||||||
|
7: [ // L piece
|
||||||
|
[[0, 0, 1, 0], [1, 1, 1, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 1, 0, 0], [0, 1, 0, 0], [0, 1, 1, 0], [0, 0, 0, 0]],
|
||||||
|
[[0, 0, 0, 0], [1, 1, 1, 0], [1, 0, 0, 0], [0, 0, 0, 0]],
|
||||||
|
[[1, 1, 0, 0], [0, 1, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function connectEventSource() {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
eventSource = new EventSource('/events');
|
||||||
|
|
||||||
|
eventSource.onopen = function () {
|
||||||
|
updateConnectionStatus('Connected', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onmessage = function (event) {
|
||||||
|
gameState = JSON.parse(event.data);
|
||||||
|
render();
|
||||||
|
updateUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function (event) {
|
||||||
|
console.log('EventSource failed:', event);
|
||||||
|
updateConnectionStatus('Disconnected', false);
|
||||||
|
setTimeout(connectEventSource, 2000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnectionStatus(status, connected) {
|
||||||
|
const statusElement = document.getElementById('connectionStatus');
|
||||||
|
statusElement.textContent = status;
|
||||||
|
statusElement.className = connected ? 'connection-status' : 'connection-status disconnected';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAction(action, direction = '') {
|
||||||
|
const url = '/action?action=' + action + (direction ? '&direction=' + direction : '');
|
||||||
|
fetch(url, { method: 'POST' }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startGame() {
|
||||||
|
document.getElementById('gameOver').style.display = 'none';
|
||||||
|
sendAction('start');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
// Clear main canvas
|
||||||
|
ctx.fillStyle = '#111';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw board (only visible part: rows 0-19, but our array is 0-39)
|
||||||
|
for (let y = 0; y < 20; y++) {
|
||||||
|
for (let x = 0; x < 10; x++) {
|
||||||
|
const cellValue = gameState.board[y][x];
|
||||||
|
if (cellValue > 0) {
|
||||||
|
ctx.fillStyle = COLORS[cellValue];
|
||||||
|
ctx.fillRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.strokeRect(x * CELL_SIZE, (19 - y) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current piece
|
||||||
|
if (gameState.current_piece && gameState.game_running) {
|
||||||
|
const piece = gameState.current_piece;
|
||||||
|
const shape = currentPieceShapes[piece.type][piece.rotation];
|
||||||
|
if (shape) {
|
||||||
|
ctx.fillStyle = COLORS[piece.type];
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
const boardX = piece.x + px;
|
||||||
|
const boardY = piece.y + py;
|
||||||
|
if (boardY < 20) { // Only draw visible part
|
||||||
|
ctx.fillRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
ctx.strokeStyle = '#666';
|
||||||
|
ctx.strokeRect(boardX * CELL_SIZE, (19 - boardY) * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
ctx.strokeStyle = '#333';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let x = 0; x <= 10; x++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x * CELL_SIZE, 0);
|
||||||
|
ctx.lineTo(x * CELL_SIZE, canvas.height);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
for (let y = 0; y <= 20; y++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y * CELL_SIZE);
|
||||||
|
ctx.lineTo(canvas.width, y * CELL_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNextPieces();
|
||||||
|
renderHoldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNextPieces() {
|
||||||
|
nextCtx.fillStyle = '#111';
|
||||||
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.next_pieces) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.min(6, gameState.next_pieces.length); i++) {
|
||||||
|
const pieceType = gameState.next_pieces[i];
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
nextCtx.fillStyle = COLORS[pieceType];
|
||||||
|
const offsetY = i * 50 + 5;
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
nextCtx.fillRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
nextCtx.strokeStyle = '#666';
|
||||||
|
nextCtx.strokeRect(px * 15 + 10, py * 15 + offsetY, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHoldPiece() {
|
||||||
|
holdCtx.fillStyle = '#111';
|
||||||
|
holdCtx.fillRect(0, 0, holdCanvas.width, holdCanvas.height);
|
||||||
|
|
||||||
|
if (!gameState || !gameState.hold_piece) return;
|
||||||
|
|
||||||
|
const pieceType = gameState.hold_piece;
|
||||||
|
const shape = shapes[pieceType];
|
||||||
|
if (shape) {
|
||||||
|
holdCtx.fillStyle = gameState.can_hold ? COLORS[pieceType] : '#666';
|
||||||
|
for (let py = 0; py < 4; py++) {
|
||||||
|
for (let px = 0; px < 4; px++) {
|
||||||
|
if (shape[py][px]) {
|
||||||
|
holdCtx.fillRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
holdCtx.strokeStyle = '#444';
|
||||||
|
holdCtx.strokeRect(px * 15 + 10, py * 15 + 10, 15, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
if (!gameState) return;
|
||||||
|
|
||||||
|
document.getElementById('score').textContent = gameState.score.toLocaleString();
|
||||||
|
document.getElementById('level').textContent = gameState.level;
|
||||||
|
document.getElementById('lines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('goal').textContent = gameState.level * gameState.lines_for_level;
|
||||||
|
|
||||||
|
// Update game status
|
||||||
|
let gameStatus = 'Not Started';
|
||||||
|
if (gameState.game_over) {
|
||||||
|
gameStatus = 'Game Over';
|
||||||
|
} else if (gameState.game_running) {
|
||||||
|
gameStatus = 'Playing';
|
||||||
|
}
|
||||||
|
document.getElementById('gameStatus').textContent = gameStatus;
|
||||||
|
|
||||||
|
if (gameState.game_over) {
|
||||||
|
document.getElementById('finalScore').textContent = gameState.score.toLocaleString();
|
||||||
|
document.getElementById('finalLevel').textContent = gameState.level;
|
||||||
|
document.getElementById('finalLines').textContent = gameState.lines_cleared;
|
||||||
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
document.addEventListener('keydown', function (event) {
|
||||||
|
if (!gameState || !gameState.game_running) return;
|
||||||
|
|
||||||
|
switch (event.code) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'left');
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'right');
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('move', 'down');
|
||||||
|
break;
|
||||||
|
case 'Space':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hard_drop');
|
||||||
|
break;
|
||||||
|
case 'KeyZ':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'ccw');
|
||||||
|
break;
|
||||||
|
case 'KeyX':
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('rotate', 'cw');
|
||||||
|
break;
|
||||||
|
case 'KeyC':
|
||||||
|
event.preventDefault();
|
||||||
|
sendAction('hold');
|
||||||
|
break;
|
||||||
|
case 'KeyR':
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent context menu on right click
|
||||||
|
document.addEventListener('contextmenu', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize connection when page loads
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
connectEventSource();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle page visibility changes
|
||||||
|
document.addEventListener('visibilitychange', function () {
|
||||||
|
if (document.hidden) {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connectEventSource();
|
||||||
|
}
|
||||||
|
});
|
||||||
119
dstetris/v1/frontend/style.css
Normal file
119
dstetris/v1/frontend/style.css
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-panel {
|
||||||
|
width: 200px;
|
||||||
|
background: #222;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-canvas,
|
||||||
|
.hold-canvas {
|
||||||
|
border: 1px solid #444;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls h3 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-over {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-info div {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
color: #0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status.disconnected {
|
||||||
|
color: #f00;
|
||||||
|
}
|
||||||
3
dstetris/v1/go.mod
Normal file
3
dstetris/v1/go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module tetris
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
501
dstetris/v1/main.go
Normal file
501
dstetris/v1/main.go
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Teris rules https://tetris.fandom.com/wiki/Tetris_Guideline
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle using current time
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, err := json.Marshal(g.state)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error marshaling game state: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
// Non-blocking broadcast to all clients
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
// Message sent successfully
|
||||||
|
default:
|
||||||
|
// Channel full or client blocked, skip this client
|
||||||
|
log.Printf("Dropping message for slow client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
//go:embed frontend/*
|
||||||
|
var frontendFS embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Serve static files (index.html)
|
||||||
|
// http.Handle("/", http.FileServer(http.Dir("./frontend")))
|
||||||
|
|
||||||
|
// Create a sub-filesystem to serve files from the 'frontend' directory.
|
||||||
|
fs, err := fs.Sub(frontendFS, "frontend")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve static files from the embedded filesystem
|
||||||
|
http.Handle("/", http.FileServer(http.FS(fs)))
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :4444")
|
||||||
|
fmt.Println("Open http://localhost:4444 in your browser")
|
||||||
|
log.Fatal(http.ListenAndServe(":4444", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Critical SSE headers for nginx compatibility
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := make(chan string, 10) // Buffered channel
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
|
// Heartbeat to keep connection alive
|
||||||
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
flusher.Flush()
|
||||||
|
case <-ticker.C:
|
||||||
|
// Send comment as heartbeat
|
||||||
|
fmt.Fprintf(w, ": heartbeat\n\n")
|
||||||
|
flusher.Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
BIN
dstetris/v1/tetris
Executable file
BIN
dstetris/v1/tetris
Executable file
Binary file not shown.
486
dstetris/v1/tetris_backend (1).go.txt
Normal file
486
dstetris/v1/tetris_backend (1).go.txt
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino types
|
||||||
|
const (
|
||||||
|
EMPTY = iota
|
||||||
|
I_PIECE
|
||||||
|
O_PIECE
|
||||||
|
T_PIECE
|
||||||
|
S_PIECE
|
||||||
|
Z_PIECE
|
||||||
|
J_PIECE
|
||||||
|
L_PIECE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tetrimino shapes (4 rotations each)
|
||||||
|
var tetriminoShapes = map[int][4][4][4]int{
|
||||||
|
I_PIECE: {
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}},
|
||||||
|
},
|
||||||
|
O_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
T_PIECE: {
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
S_PIECE: {
|
||||||
|
{{0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
Z_PIECE: {
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {1, 1, 0, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
J_PIECE: {
|
||||||
|
{{1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
L_PIECE: {
|
||||||
|
{{0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}},
|
||||||
|
{{0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
{{1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tetrimino struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
Rotation int `json:"rotation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameState struct {
|
||||||
|
Board [40][10]int `json:"board"`
|
||||||
|
CurrentPiece *Tetrimino `json:"current_piece"`
|
||||||
|
NextPieces []int `json:"next_pieces"`
|
||||||
|
HoldPiece *int `json:"hold_piece"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
LinesCleared int `json:"lines_cleared"`
|
||||||
|
GameRunning bool `json:"game_running"`
|
||||||
|
GameOver bool `json:"game_over"`
|
||||||
|
CanHold bool `json:"can_hold"`
|
||||||
|
LinesForLevel int `json:"lines_for_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
state *GameState
|
||||||
|
bag []int
|
||||||
|
bagIndex int
|
||||||
|
clients map[chan string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame() *Game {
|
||||||
|
g := &Game{
|
||||||
|
state: &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: false,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
},
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillBag() {
|
||||||
|
g.bag = []int{I_PIECE, O_PIECE, T_PIECE, S_PIECE, Z_PIECE, J_PIECE, L_PIECE}
|
||||||
|
// Simple shuffle using current time
|
||||||
|
for i := len(g.bag) - 1; i > 0; i-- {
|
||||||
|
j := int(time.Now().UnixNano()) % (i + 1)
|
||||||
|
g.bag[i], g.bag[j] = g.bag[j], g.bag[i]
|
||||||
|
}
|
||||||
|
g.bagIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) getNextPiece() int {
|
||||||
|
if g.bagIndex >= len(g.bag) {
|
||||||
|
g.fillBag()
|
||||||
|
}
|
||||||
|
piece := g.bag[g.bagIndex]
|
||||||
|
g.bagIndex++
|
||||||
|
return piece
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) fillNextPieces() {
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
g.state.NextPieces[i] = g.getNextPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) spawnPiece() {
|
||||||
|
pieceType := g.state.NextPieces[0]
|
||||||
|
// Shift next pieces
|
||||||
|
copy(g.state.NextPieces[:5], g.state.NextPieces[1:])
|
||||||
|
g.state.NextPieces[5] = g.getNextPiece()
|
||||||
|
|
||||||
|
// Spawn position based on guidelines
|
||||||
|
x := 3 // left-middle for most pieces
|
||||||
|
if pieceType == I_PIECE || pieceType == O_PIECE {
|
||||||
|
x = 4 // middle for I and O
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: pieceType,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop one space if possible
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for game over
|
||||||
|
if !g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.GameOver = true
|
||||||
|
g.state.GameRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CanHold = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) canMove(x, y, rotation int) bool {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := x + px
|
||||||
|
boardY := y + py
|
||||||
|
if boardX < 0 || boardX >= 10 || boardY < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if boardY < 40 && g.state.Board[boardY][boardX] != EMPTY {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) lockPiece() {
|
||||||
|
shape := tetriminoShapes[g.state.CurrentPiece.Type][g.state.CurrentPiece.Rotation]
|
||||||
|
for py := 0; py < 4; py++ {
|
||||||
|
for px := 0; px < 4; px++ {
|
||||||
|
if shape[py][px] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
boardX := g.state.CurrentPiece.X + px
|
||||||
|
boardY := g.state.CurrentPiece.Y + py
|
||||||
|
if boardY < 40 {
|
||||||
|
g.state.Board[boardY][boardX] = g.state.CurrentPiece.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.clearLines()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) clearLines() {
|
||||||
|
linesCleared := 0
|
||||||
|
for y := 0; y < 40; y++ {
|
||||||
|
full := true
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
if g.state.Board[y][x] == EMPTY {
|
||||||
|
full = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if full {
|
||||||
|
// Remove line
|
||||||
|
for moveY := y; moveY < 39; moveY++ {
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[moveY][x] = g.state.Board[moveY+1][x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear top line
|
||||||
|
for x := 0; x < 10; x++ {
|
||||||
|
g.state.Board[39][x] = EMPTY
|
||||||
|
}
|
||||||
|
linesCleared++
|
||||||
|
y-- // Check same line again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if linesCleared > 0 {
|
||||||
|
g.state.LinesCleared += linesCleared
|
||||||
|
g.updateScore(linesCleared)
|
||||||
|
g.updateLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateScore(lines int) {
|
||||||
|
baseScore := map[int]int{1: 100, 2: 300, 3: 500, 4: 800}
|
||||||
|
if score, ok := baseScore[lines]; ok {
|
||||||
|
g.state.Score += score * g.state.Level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateLevel() {
|
||||||
|
requiredLines := g.state.Level * g.state.LinesForLevel
|
||||||
|
if g.state.LinesCleared >= requiredLines {
|
||||||
|
g.state.Level++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) move(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newX := g.state.CurrentPiece.X
|
||||||
|
newY := g.state.CurrentPiece.Y
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case "left":
|
||||||
|
newX--
|
||||||
|
case "right":
|
||||||
|
newX++
|
||||||
|
case "down":
|
||||||
|
newY--
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(newX, newY, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.X = newX
|
||||||
|
g.state.CurrentPiece.Y = newY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) rotate(direction string) {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newRotation := g.state.CurrentPiece.Rotation
|
||||||
|
if direction == "cw" {
|
||||||
|
newRotation = (newRotation + 1) % 4
|
||||||
|
} else {
|
||||||
|
newRotation = (newRotation + 3) % 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y, newRotation) {
|
||||||
|
g.state.CurrentPiece.Rotation = newRotation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hardDrop() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) hold() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil || !g.state.CanHold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.state.HoldPiece == nil {
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
g.spawnPiece()
|
||||||
|
} else {
|
||||||
|
temp := *g.state.HoldPiece
|
||||||
|
g.state.HoldPiece = &g.state.CurrentPiece.Type
|
||||||
|
|
||||||
|
x := 3
|
||||||
|
if temp == I_PIECE || temp == O_PIECE {
|
||||||
|
x = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
g.state.CurrentPiece = &Tetrimino{
|
||||||
|
Type: temp,
|
||||||
|
X: x,
|
||||||
|
Y: 21,
|
||||||
|
Rotation: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.state.CanHold = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) startGame() {
|
||||||
|
g.state = &GameState{
|
||||||
|
NextPieces: make([]int, 6),
|
||||||
|
Level: 1,
|
||||||
|
GameRunning: true,
|
||||||
|
GameOver: false,
|
||||||
|
CanHold: true,
|
||||||
|
LinesForLevel: 5,
|
||||||
|
}
|
||||||
|
g.fillBag()
|
||||||
|
g.fillNextPieces()
|
||||||
|
g.spawnPiece()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) tick() {
|
||||||
|
if !g.state.GameRunning || g.state.CurrentPiece == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.canMove(g.state.CurrentPiece.X, g.state.CurrentPiece.Y-1, g.state.CurrentPiece.Rotation) {
|
||||||
|
g.state.CurrentPiece.Y--
|
||||||
|
} else {
|
||||||
|
g.lockPiece()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) broadcast() {
|
||||||
|
data, err := json.Marshal(g.state)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error marshaling game state: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message := fmt.Sprintf("data: %s\n\n", string(data))
|
||||||
|
|
||||||
|
// Non-blocking broadcast to all clients
|
||||||
|
for client := range g.clients {
|
||||||
|
select {
|
||||||
|
case client <- message:
|
||||||
|
// Message sent successfully
|
||||||
|
default:
|
||||||
|
// Channel full or client blocked, skip this client
|
||||||
|
log.Printf("Dropping message for slow client")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var game = NewGame()
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Serve static files (index.html)
|
||||||
|
http.Handle("/", http.FileServer(http.Dir("./")))
|
||||||
|
|
||||||
|
// API endpoints
|
||||||
|
http.HandleFunc("/events", handleSSE)
|
||||||
|
http.HandleFunc("/action", handleAction)
|
||||||
|
|
||||||
|
// Game loop
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if game.state.GameRunning {
|
||||||
|
game.tick()
|
||||||
|
game.broadcast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fmt.Println("Server starting on :8080")
|
||||||
|
fmt.Println("Open http://localhost:8080 in your browser")
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Critical SSE headers for nginx compatibility
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("X-Accel-Buffering", "no") // Disable nginx buffering
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := make(chan string, 10) // Buffered channel
|
||||||
|
game.clients[client] = true
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
data, _ := json.Marshal(game.state)
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
||||||
|
flusher.Flush()
|
||||||
|
|
||||||
|
// Heartbeat to keep connection alive
|
||||||
|
ticker := time.NewTicker(15 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message := <-client:
|
||||||
|
fmt.Fprint(w, message)
|
||||||
|
flusher.Flush()
|
||||||
|
case <-ticker.C:
|
||||||
|
// Send comment as heartbeat
|
||||||
|
fmt.Fprintf(w, ": heartbeat\n\n")
|
||||||
|
flusher.Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
delete(game.clients, client)
|
||||||
|
close(client)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
direction := r.URL.Query().Get("direction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "move":
|
||||||
|
game.move(direction)
|
||||||
|
case "rotate":
|
||||||
|
game.rotate(direction)
|
||||||
|
case "hard_drop":
|
||||||
|
game.hardDrop()
|
||||||
|
case "hold":
|
||||||
|
game.hold()
|
||||||
|
case "start":
|
||||||
|
game.startGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
game.broadcast()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
@ -8,13 +8,84 @@ Who knows what this is? Follow up with very short discussian
|
|||||||
|
|
||||||
Starting with Web-Development many years ago, I came up with the following formular, I call it the Drake Formular for Webdevelopment
|
Starting with Web-Development many years ago, I came up with the following formular, I call it the Drake Formular for Webdevelopment
|
||||||
|
|
||||||
It like Drake's but I run it in reverse, it is a big Multiplication of technologies choices. Leading to billions of possible combinations of web applications
|
Insert Drake's formular of web technologies here.
|
||||||
|
|
||||||
No each living web application out there is a little star, showing a viable combination of web technologies.
|
It like Drake's but I run it in reverse, it is a big Multiplication of technologies choices. Leading to billions of possible combinations of web applications. With literally billions of combinations it is quite hard to choose one.
|
||||||
|
|
||||||
One can see this formular as the basis for a large n-dimensional vector space and just like in the real space where we have galaxies and clusters of galaxies, we also have clusters in this space. There is for example the cluster of react and supporting react technologies or there is an angular or vue cluster.
|
Now, each living web application out there is a little star, showing a viable combination of web technologies.
|
||||||
|
|
||||||
|
One can also see this formular as the basis for a large n-dimensional vector space and just like in the real space where we have galaxies and clusters of galaxies, we also have clusters in this space. There is for example the cluster of react and supporting react technologies or there is an angular cluster or vue cluster. I, for example, live in the line-of-business apps with vue neigborhood.
|
||||||
|
|
||||||
And we a basically following our personal journey through this space, gravitating to one cluster at on point in time or gravitating to another cluster later in time.
|
And we a basically following our personal journey through this space, gravitating to one cluster at on point in time or gravitating to another cluster later in time.
|
||||||
|
|
||||||
Today I want to visit with you a already quite old cluster, it is called hypermedia, and all web apps were living in this cluster a few decades ago. This cluster developed a new rather large bulge called htmx and very recently a still very small blob called Datastar and this is where our journey takes us today.
|
Today I want to visit with you a already quite old cluster, it is called hypermedia, and all web apps were living in this cluster a few decades ago. This cluster developed a new rather large bulge called htmx and very recently a still tiny blob called Datastar and this is where our journey takes us today.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
some preparations:
|
||||||
|
- to use Datastar you need to control the backend
|
||||||
|
- learn your prefered templating system
|
||||||
|
- be ready for a major paradigm shift (similar to the one with HTMX)
|
||||||
|
|
||||||
|
Insert Delaney's quote: Datastar is a backend agnostic backend framework with a 10 Kb shim
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Quick Excourse, my motivation: the minimal viable web app:
|
||||||
|
- I believe that the Complexity Budget is a real thing
|
||||||
|
- I believe that constraints can induce creativity: Creativity Paradox
|
||||||
|
- I believe that Simplicity is the ultimate form of perfection
|
||||||
|
|
||||||
|
My self imposed constraints are:
|
||||||
|
- No build step for the frontend part (no npm, no vite.js, no special tooling)
|
||||||
|
- Very few 3rd party libraries (sqlite-driver, datastar-sdk, datastar)
|
||||||
|
- Write the app with a reasonably minimal number of code lines
|
||||||
|
- Reduce the dependancies to a minimum.
|
||||||
|
|
||||||
|
Minimal is also about everything you need to build a complete web app. In this sense the mvwa establishes a baseline of everything you need to build (a great) web application.
|
||||||
|
|
||||||
|
part 1: Databases
|
||||||
|
part 2: The No-ORM ORM
|
||||||
|
part 3: Go-Programming language
|
||||||
|
part 4: Datastar
|
||||||
|
part 5: Modern CSS
|
||||||
|
part 6: Web Components
|
||||||
|
part 7: VPS and Deployment
|
||||||
|
|
||||||
|
To be clear: is true that Datastar is a minimalistic "framework" for web development, but it is by no means limited to minimalistic web apps.
|
||||||
|
Delaneys claim to be able to recreate any web app with datastar, faster with less code.
|
||||||
|
|
||||||
|
The longer you can stay in "declaritive space" and the shorter time you spend in "imparitive space" the faster you can develop you application.
|
||||||
|
|
||||||
|
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The core of Datastar
|
||||||
|
|
||||||
|
DS stays true to the web specifications, background the web can be a place for long-living apps/sites. A thirty year-old website can still be used.
|
||||||
|
|
||||||
|
DS uses the custom data-[what-ever] attributes. add info about browsers forgiving behavior of invalid attributes.
|
||||||
|
DS uses SSEs part of the HTTP-spec
|
||||||
|
DS works well with Web Components another web standard.
|
||||||
|
|
||||||
|
It uses Signals (based on Alien Signal) - I showed a minimalist implementation of signals in my last talk. Signals may become a web standard too.
|
||||||
|
It uses a morphing algorithm derived from ideomorph created by the team around HTMX, but interestingly not used my HTMX.
|
||||||
|
It can work with JSON or HTML API responses, but
|
||||||
|
it shines when using SSE.
|
||||||
|
|
||||||
|
In short it is quite simple actually, but a bit paradigm shift in web development
|
||||||
|
|
||||||
|
JSON responses update or create signals
|
||||||
|
HTML responses morph into the DOM based on id attibutes
|
||||||
|
SSEs can contain both JSON and HTML responses
|
||||||
|
|
||||||
|
The team has created an ADR, that show how to develop a backend, there are dozents of backend sdks available already
|
||||||
|
|
||||||
|
Lets revisit Delaney's quote: Datastar is a backend agnostic backend framework with a 10 Kb shim
|
||||||
|
|
||||||
|
insert a quick intro to SSEs and a quick compare to websockets
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user