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 = ` Tetris

Hold

Next

Score
0
Level
1
Lines
0
Goal
5

Controls

← → Move

↓ Soft Drop

Space Hard Drop

Z Rotate Left

X Rotate Right

C Hold

` 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) {