package main import ( "fmt" "image/color" "math/rand" "time" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" ) const ( // Size constants BlockSize = 30 BoardWidth = 10 BoardHeight = 20 ScreenWidth = BlockSize * (BoardWidth + 6) // Extra space for next piece preview ScreenHeight = BlockSize * BoardHeight PreviewX = BlockSize * (BoardWidth + 1) PreviewY = BlockSize * 2 // Game constants InitialDropInterval = 60 MinDropInterval = 5 ) // Game represents the main game state type Game struct { board [][]BlockType currentPiece *Tetromino nextPiece *Tetromino dropCounter int dropInterval int score int level int gameOver bool lastMoveDown time.Time lastRotate time.Time } // NewGame creates a new game instance func NewGame() *Game { g := &Game{ board: make([][]BlockType, BoardHeight), dropInterval: InitialDropInterval, lastMoveDown: time.Now(), lastRotate: time.Now(), } for i := range g.board { g.board[i] = make([]BlockType, BoardWidth) } g.spawnNewPiece() return g } // Update handles game logic updates func (g *Game) Update() error { if g.gameOver { if inpututil.IsKeyJustPressed(ebiten.KeySpace) { *g = *NewGame() return nil } return nil } g.handleInput() g.updateDropCounter() return nil } // Draw renders the game state func (g *Game) Draw(screen *ebiten.Image) { // Draw board background ebitenutil.DrawRect(screen, 0, 0, float64(BoardWidth*BlockSize), float64(BoardHeight*BlockSize), color.RGBA{40, 40, 40, 255}) // Draw preview area background ebitenutil.DrawRect(screen, float64(PreviewX-BlockSize), 0, float64(6*BlockSize), float64(ScreenHeight), color.RGBA{30, 30, 30, 255}) // Draw grid lines gridColor := color.RGBA{60, 60, 60, 255} for x := 0; x <= BoardWidth; x++ { ebitenutil.DrawLine(screen, float64(x*BlockSize), 0, float64(x*BlockSize), float64(BoardHeight*BlockSize), gridColor) } for y := 0; y <= BoardHeight; y++ { ebitenutil.DrawLine(screen, 0, float64(y*BlockSize), float64(BoardWidth*BlockSize), float64(y*BlockSize), gridColor) } // Draw placed blocks for y := 0; y < BoardHeight; y++ { for x := 0; x < BoardWidth; x++ { if g.board[y][x] != 0 { g.drawBlock(screen, x, y, g.board[y][x]) } } } // Draw current piece and its ghost if g.currentPiece != nil { // Draw ghost piece ghostPiece := *g.currentPiece for { ghostPiece.Move(0, 1) if g.wouldCollide(&ghostPiece) { ghostPiece.Move(0, -1) break } } for _, pos := range ghostPiece.GetAbsolutePositions() { if pos[1] >= 0 { ghostColor := color.RGBA{128, 128, 128, 128} ebitenutil.DrawRect(screen, float64(pos[0]*BlockSize), float64(pos[1]*BlockSize), float64(BlockSize-1), float64(BlockSize-1), ghostColor) } } // Draw actual piece for _, pos := range g.currentPiece.GetAbsolutePositions() { if pos[1] >= 0 { g.drawBlock(screen, pos[0], pos[1], g.currentPiece.BlockType) } } } // Draw next piece preview if g.nextPiece != nil { previewLabel := "NEXT:" ebitenutil.DebugPrintAt(screen, previewLabel, PreviewX-BlockSize, BlockSize) for _, block := range g.nextPiece.Blocks { x := PreviewX + block.X*BlockSize y := PreviewY + block.Y*BlockSize ebitenutil.DrawRect(screen, float64(x), float64(y), float64(BlockSize-1), float64(BlockSize-1), BlockColors[g.nextPiece.BlockType]) } } // Draw score and level with larger font scoreStr := fmt.Sprintf("Score: %d", g.score) levelStr := fmt.Sprintf("Level: %d", g.level) ebitenutil.DebugPrintAt(screen, scoreStr, PreviewX-BlockSize, ScreenHeight-4*BlockSize) ebitenutil.DebugPrintAt(screen, levelStr, PreviewX-BlockSize, ScreenHeight-3*BlockSize) if g.gameOver { gameOverStr := "Game Over!" restartStr := "Press SPACE to restart" ebitenutil.DebugPrintAt(screen, gameOverStr, PreviewX-BlockSize, ScreenHeight/2) ebitenutil.DebugPrintAt(screen, restartStr, PreviewX-BlockSize, ScreenHeight/2+20) } } // Layout implements ebiten.Game interface func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return ScreenWidth, ScreenHeight } // handleInput processes player input func (g *Game) handleInput() { // Move left/right if inpututil.IsKeyJustPressed(ebiten.KeyLeft) { g.tryMove(-1, 0) } if inpututil.IsKeyJustPressed(ebiten.KeyRight) { g.tryMove(1, 0) } // Rotate if inpututil.IsKeyJustPressed(ebiten.KeyUp) { if time.Since(g.lastRotate) > time.Millisecond*100 { g.tryRotate() g.lastRotate = time.Now() } } // Soft drop if ebiten.IsKeyPressed(ebiten.KeyDown) { if time.Since(g.lastMoveDown) > time.Millisecond*50 { g.tryMove(0, 1) g.lastMoveDown = time.Now() } } // Hard drop if inpututil.IsKeyJustPressed(ebiten.KeySpace) { g.hardDrop() } } // updateDropCounter handles automatic piece dropping func (g *Game) updateDropCounter() { if g.currentPiece == nil { return } g.dropCounter++ if g.dropCounter >= g.dropInterval { g.dropCounter = 0 // Try to move down g.currentPiece.Move(0, 1) if g.isColliding() { // Move back up and lock the piece g.currentPiece.Move(0, -1) g.lockPiece() if !g.gameOver { g.clearLines() g.spawnNewPiece() } } } } // tryMove attempts to move the current piece func (g *Game) tryMove(dx, dy int) bool { if g.currentPiece == nil { return false } g.currentPiece.Move(dx, dy) if g.isColliding() { g.currentPiece.Move(-dx, -dy) return false } return true } // tryRotate attempts to rotate the current piece func (g *Game) tryRotate() bool { if g.currentPiece == nil { return false } g.currentPiece.Rotate() if g.isColliding() { // Try wall kicks kicks := NormalKicks if g.currentPiece.BlockType == IBlock { kicks = IKicks } success := false for _, kick := range kicks { g.currentPiece.Move(kick[0], kick[1]) if !g.isColliding() { success = true break } g.currentPiece.Move(-kick[0], -kick[1]) } if !success { // Rotate back to original position g.currentPiece.Rotate() g.currentPiece.Rotate() g.currentPiece.Rotate() return false } } return true } // hardDrop drops the piece to the bottom instantly func (g *Game) hardDrop() { if g.currentPiece == nil { return } // Move down until collision dropDistance := 0 for { g.currentPiece.Move(0, 1) if g.isColliding() { g.currentPiece.Move(0, -1) break } dropDistance++ } // Add bonus points for hard drop g.score += dropDistance * 2 g.lockPiece() if !g.gameOver { g.clearLines() g.spawnNewPiece() } } // isColliding checks if the current piece collides with the board or boundaries func (g *Game) isColliding() bool { if g.currentPiece == nil { return false } for _, pos := range g.currentPiece.GetAbsolutePositions() { x, y := pos[0], pos[1] // Check boundaries if x < 0 || x >= BoardWidth || y >= BoardHeight { return true } // Check collision with other pieces if y >= 0 && x >= 0 && x < BoardWidth && y < BoardHeight { if g.board[y][x] != 0 { return true } } } return false } // lockPiece fixes the current piece to the board func (g *Game) lockPiece() { if g.currentPiece == nil { return } positions := g.currentPiece.GetAbsolutePositions() for _, pos := range positions { x, y := pos[0], pos[1] if y < 0 { g.gameOver = true return } if x >= 0 && x < BoardWidth && y >= 0 && y < BoardHeight { g.board[y][x] = g.currentPiece.BlockType } } } // clearLines removes completed lines and updates the score func (g *Game) clearLines() { linesCleared := 0 for y := BoardHeight - 1; y >= 0; y-- { if g.isLineFull(y) { g.removeLine(y) linesCleared++ y++ // Check the same line again after shifting } } // Update score and level if linesCleared > 0 { // 计分规则:一次消除的行数越多,得分越高 baseScore := 0 switch linesCleared { case 1: baseScore = 100 case 2: baseScore = 300 case 3: baseScore = 500 case 4: baseScore = 800 } // 根据当前等级增加得分 levelMultiplier := g.level + 1 g.score += baseScore * levelMultiplier // 更新等级 g.level = g.score / 1000 // 随等级提高,方块下落速度加快 g.dropInterval = max(MinDropInterval, InitialDropInterval-g.level*5) } } // isLineFull checks if a line is completely filled func (g *Game) isLineFull(y int) bool { for x := 0; x < BoardWidth; x++ { if g.board[y][x] == 0 { return false } } return true } // removeLine removes a line and shifts everything above down func (g *Game) removeLine(y int) { for i := y; i > 0; i-- { copy(g.board[i], g.board[i-1]) } for x := 0; x < BoardWidth; x++ { g.board[0][x] = 0 } } // spawnNewPiece creates a new piece at the top of the board func (g *Game) spawnNewPiece() { if g.nextPiece == nil { g.nextPiece = NewTetromino(BlockType(rand.Intn(7)), 0, 0) } g.currentPiece = g.nextPiece g.currentPiece.X = BoardWidth/2 - 2 if g.currentPiece.BlockType != IBlock { g.currentPiece.X++ // Adjust for non-I pieces } g.currentPiece.Y = 0 g.nextPiece = NewTetromino(BlockType(rand.Intn(7)), 0, 0) if g.isColliding() { g.gameOver = true } } // wouldCollide checks if a piece would collide in its current position func (g *Game) wouldCollide(piece *Tetromino) bool { for _, pos := range piece.GetAbsolutePositions() { x, y := pos[0], pos[1] if x < 0 || x >= BoardWidth || y >= BoardHeight { return true } if y >= 0 && y < BoardHeight && x >= 0 && x < BoardWidth && g.board[y][x] != 0 { return true } } return false } // drawBlock draws a single block at the specified position func (g *Game) drawBlock(screen *ebiten.Image, x, y int, blockType BlockType) { if y < 0 { return } // Draw block background ebitenutil.DrawRect(screen, float64(x*BlockSize), float64(y*BlockSize), float64(BlockSize-1), float64(BlockSize-1), BlockColors[blockType]) // Draw highlight (3D effect) highlightColor := color.RGBA{255, 255, 255, 64} ebitenutil.DrawLine(screen, float64(x*BlockSize), float64(y*BlockSize), float64(x*BlockSize+BlockSize-1), float64(y*BlockSize), highlightColor) ebitenutil.DrawLine(screen, float64(x*BlockSize), float64(y*BlockSize), float64(x*BlockSize), float64(y*BlockSize+BlockSize-1), highlightColor) } func max(a, b int) int { if a > b { return a } return b }