You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tetris/game.go

469 lines
10 KiB

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
}