package tetris import "math/rand/v2" const ( boardRows = 20 boardCols = 10 ) // cell represents a single board cell. Zero value is empty. type cell struct { filled bool piece pieceType // which piece type filled this cell (for color) } // gameState holds all mutable state for a Tetris game. type gameState struct { board [boardRows][boardCols]cell current pieceType currentRot int currentRow int currentCol int next pieceType score int level int lines int gameOver bool } // newGame creates a new game state, optionally starting at a given level. func newGame(startLevel int) *gameState { g := &gameState{ level: startLevel, next: pieceType(rand.IntN(numPieceTypes)), } g.spawnPiece() return g } // spawnPiece pulls the next piece and generates a new next. func (g *gameState) spawnPiece() { g.current = g.next g.next = pieceType(rand.IntN(numPieceTypes)) g.currentRot = 0 g.currentRow = 0 g.currentCol = spawnCol(g.current, 0) if !g.canPlace(g.current, g.currentRot, g.currentRow, g.currentCol) { g.gameOver = true } } // canPlace checks whether the piece fits at the given position. func (g *gameState) canPlace(pt pieceType, rot, row, col int) bool { shape := pieces[pt][rot] for _, off := range shape { r, c := row+off[0], col+off[1] if r < 0 || r >= boardRows || c < 0 || c >= boardCols { return false } if g.board[r][c].filled { return false } } return true } // moveLeft moves the current piece left if possible. func (g *gameState) moveLeft() bool { if g.canPlace(g.current, g.currentRot, g.currentRow, g.currentCol-1) { g.currentCol-- return true } return false } // moveRight moves the current piece right if possible. func (g *gameState) moveRight() bool { if g.canPlace(g.current, g.currentRot, g.currentRow, g.currentCol+1) { g.currentCol++ return true } return false } // moveDown moves the current piece down one row. Returns false if it cannot. func (g *gameState) moveDown() bool { if g.canPlace(g.current, g.currentRot, g.currentRow+1, g.currentCol) { g.currentRow++ return true } return false } // rotate rotates the current piece clockwise with wall kick attempts. func (g *gameState) rotate() bool { newRot := (g.currentRot + 1) % 4 // Try in-place first. if g.canPlace(g.current, newRot, g.currentRow, g.currentCol) { g.currentRot = newRot return true } // Wall kick: try +-1 column offset. for _, offset := range []int{-1, 1} { if g.canPlace(g.current, newRot, g.currentRow, g.currentCol+offset) { g.currentRot = newRot g.currentCol += offset return true } } // I piece: try +-2. if g.current == pieceI { for _, offset := range []int{-2, 2} { if g.canPlace(g.current, newRot, g.currentRow, g.currentCol+offset) { g.currentRot = newRot g.currentCol += offset return true } } } return false } // ghostRow returns the row where the piece would land. func (g *gameState) ghostRow() int { row := g.currentRow for g.canPlace(g.current, g.currentRot, row+1, g.currentCol) { row++ } return row } // hardDrop drops the piece to the bottom and returns the number of rows dropped. func (g *gameState) hardDrop() int { ghost := g.ghostRow() dropped := ghost - g.currentRow g.currentRow = ghost return dropped } // lockPiece writes the current piece into the board. func (g *gameState) lockPiece() { shape := pieces[g.current][g.currentRot] for _, off := range shape { r, c := g.currentRow+off[0], g.currentCol+off[1] if r >= 0 && r < boardRows && c >= 0 && c < boardCols { g.board[r][c] = cell{filled: true, piece: g.current} } } } // clearLines removes completed rows and returns how many were cleared. func (g *gameState) clearLines() int { cleared := 0 for r := boardRows - 1; r >= 0; r-- { full := true for c := range boardCols { if !g.board[r][c].filled { full = false break } } if full { cleared++ // Shift everything above down. for rr := r; rr > 0; rr-- { g.board[rr] = g.board[rr-1] } g.board[0] = [boardCols]cell{} r++ // re-check this row since we shifted } } return cleared } // NES-style scoring multipliers per lines cleared. var lineScoreMultipliers = [5]int{0, 40, 100, 300, 1200} // addScore updates score, lines, and level after clearing rows. func (g *gameState) addScore(linesCleared int) { if linesCleared > 0 && linesCleared <= 4 { g.score += lineScoreMultipliers[linesCleared] * (g.level + 1) } g.lines += linesCleared // Level up every 10 lines. newLevel := g.lines / 10 if newLevel > g.level { g.level = newLevel } } // afterLock locks the piece, clears lines, scores, and spawns the next piece. // Returns the number of lines cleared. func (g *gameState) afterLock() int { g.lockPiece() cleared := g.clearLines() g.addScore(cleared) g.spawnPiece() return cleared } // tickInterval returns the gravity interval in milliseconds for the current level. func tickInterval(level int) int { return max(800-level*60, 100) }