commit
95d3f05079
@ -0,0 +1,38 @@ |
|||||||
|
# Tetris Game in Go |
||||||
|
|
||||||
|
A simple Tetris game implementation using Go and the Ebiten game engine. |
||||||
|
|
||||||
|
## Requirements |
||||||
|
|
||||||
|
- Go 1.16 or later |
||||||
|
- Ebiten v2 |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
1. Clone the repository |
||||||
|
2. Install dependencies: |
||||||
|
```bash |
||||||
|
go mod tidy |
||||||
|
``` |
||||||
|
|
||||||
|
## Running the Game |
||||||
|
|
||||||
|
```bash |
||||||
|
go run . |
||||||
|
``` |
||||||
|
|
||||||
|
## Controls |
||||||
|
|
||||||
|
- Left Arrow: Move piece left |
||||||
|
- Right Arrow: Move piece right |
||||||
|
- Down Arrow: Move piece down faster |
||||||
|
- Up Arrow: Rotate piece |
||||||
|
- Space: Drop piece instantly |
||||||
|
- ESC: Quit game |
||||||
|
|
||||||
|
## Features |
||||||
|
|
||||||
|
- Classic Tetris gameplay |
||||||
|
- Score tracking |
||||||
|
- Next piece preview |
||||||
|
- Level system |
@ -0,0 +1,190 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"image/color" |
||||||
|
) |
||||||
|
|
||||||
|
// Block represents a single block in the game
|
||||||
|
type Block struct { |
||||||
|
X, Y int |
||||||
|
Type BlockType |
||||||
|
} |
||||||
|
|
||||||
|
// BlockType represents different types of tetrominos
|
||||||
|
type BlockType int |
||||||
|
|
||||||
|
const ( |
||||||
|
IBlock BlockType = iota |
||||||
|
JBlock |
||||||
|
LBlock |
||||||
|
OBlock |
||||||
|
SBlock |
||||||
|
TBlock |
||||||
|
ZBlock |
||||||
|
) |
||||||
|
|
||||||
|
// Tetromino represents a complete tetris piece
|
||||||
|
type Tetromino struct { |
||||||
|
Blocks []Block |
||||||
|
BlockType BlockType |
||||||
|
X, Y int |
||||||
|
} |
||||||
|
|
||||||
|
// Colors for different block types
|
||||||
|
var BlockColors = map[BlockType]color.Color{ |
||||||
|
IBlock: color.RGBA{0, 255, 255, 255}, // Cyan
|
||||||
|
JBlock: color.RGBA{0, 0, 255, 255}, // Blue
|
||||||
|
LBlock: color.RGBA{255, 165, 0, 255}, // Orange
|
||||||
|
OBlock: color.RGBA{255, 255, 0, 255}, // Yellow
|
||||||
|
SBlock: color.RGBA{0, 255, 0, 255}, // Green
|
||||||
|
TBlock: color.RGBA{128, 0, 128, 255}, // Purple
|
||||||
|
ZBlock: color.RGBA{255, 0, 0, 255}, // Red
|
||||||
|
} |
||||||
|
|
||||||
|
// TetrominoShapes defines the shape of each tetromino type
|
||||||
|
var TetrominoShapes = map[BlockType][][]bool{ |
||||||
|
IBlock: { |
||||||
|
{false, false, false, false}, |
||||||
|
{false, false, false, false}, |
||||||
|
{true, true, true, true}, |
||||||
|
{false, false, false, false}, |
||||||
|
}, |
||||||
|
JBlock: { |
||||||
|
{true, false, false}, |
||||||
|
{true, true, true}, |
||||||
|
{false, false, false}, |
||||||
|
}, |
||||||
|
LBlock: { |
||||||
|
{false, false, true}, |
||||||
|
{true, true, true}, |
||||||
|
{false, false, false}, |
||||||
|
}, |
||||||
|
OBlock: { |
||||||
|
{true, true}, |
||||||
|
{true, true}, |
||||||
|
}, |
||||||
|
SBlock: { |
||||||
|
{false, true, true}, |
||||||
|
{true, true, false}, |
||||||
|
{false, false, false}, |
||||||
|
}, |
||||||
|
TBlock: { |
||||||
|
{false, true, false}, |
||||||
|
{true, true, true}, |
||||||
|
{false, false, false}, |
||||||
|
}, |
||||||
|
ZBlock: { |
||||||
|
{true, true, false}, |
||||||
|
{false, true, true}, |
||||||
|
{false, false, false}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
// Rotation offsets for I piece wall kicks
|
||||||
|
var IKicks = [][2]int{ |
||||||
|
{0, 0}, |
||||||
|
{-2, 0}, |
||||||
|
{1, 0}, |
||||||
|
{-2, -1}, |
||||||
|
{1, 2}, |
||||||
|
} |
||||||
|
|
||||||
|
// Rotation offsets for other pieces wall kicks
|
||||||
|
var NormalKicks = [][2]int{ |
||||||
|
{0, 0}, |
||||||
|
{-1, 0}, |
||||||
|
{1, 0}, |
||||||
|
{0, -1}, |
||||||
|
} |
||||||
|
|
||||||
|
// NewTetromino creates a new tetromino of the specified type
|
||||||
|
func NewTetromino(blockType BlockType, x, y int) *Tetromino { |
||||||
|
shape := TetrominoShapes[blockType] |
||||||
|
blocks := make([]Block, 0) |
||||||
|
|
||||||
|
for i := 0; i < len(shape); i++ { |
||||||
|
for j := 0; j < len(shape[i]); j++ { |
||||||
|
if shape[i][j] { |
||||||
|
blocks = append(blocks, Block{ |
||||||
|
X: j, |
||||||
|
Y: i, |
||||||
|
Type: blockType, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Adjust initial position for I piece
|
||||||
|
if blockType == IBlock { |
||||||
|
y-- // Start one row higher
|
||||||
|
} |
||||||
|
|
||||||
|
return &Tetromino{ |
||||||
|
Blocks: blocks, |
||||||
|
BlockType: blockType, |
||||||
|
X: x, |
||||||
|
Y: y, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Move moves the tetromino by the specified delta
|
||||||
|
func (t *Tetromino) Move(dx, dy int) { |
||||||
|
t.X += dx |
||||||
|
t.Y += dy |
||||||
|
} |
||||||
|
|
||||||
|
// Rotate rotates the tetromino clockwise
|
||||||
|
func (t *Tetromino) Rotate() { |
||||||
|
if t.BlockType == OBlock { |
||||||
|
return // O block doesn't need rotation
|
||||||
|
} |
||||||
|
|
||||||
|
// Get the size of the shape matrix
|
||||||
|
size := 3 |
||||||
|
if t.BlockType == IBlock { |
||||||
|
size = 4 |
||||||
|
} |
||||||
|
|
||||||
|
// Create a temporary matrix for rotation
|
||||||
|
matrix := make([][]bool, size) |
||||||
|
for i := range matrix { |
||||||
|
matrix[i] = make([]bool, size) |
||||||
|
} |
||||||
|
|
||||||
|
// Fill the matrix with current block positions
|
||||||
|
for _, block := range t.Blocks { |
||||||
|
if block.Y >= 0 && block.Y < size && block.X >= 0 && block.X < size { |
||||||
|
matrix[block.Y][block.X] = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Create a new rotated shape
|
||||||
|
newBlocks := make([]Block, 0) |
||||||
|
|
||||||
|
// Perform rotation
|
||||||
|
for y := 0; y < size; y++ { |
||||||
|
for x := 0; x < size; x++ { |
||||||
|
if matrix[y][x] { |
||||||
|
// Rotate coordinates 90 degrees clockwise
|
||||||
|
newX := size - 1 - y |
||||||
|
newY := x |
||||||
|
newBlocks = append(newBlocks, Block{ |
||||||
|
X: newX, |
||||||
|
Y: newY, |
||||||
|
Type: t.BlockType, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
t.Blocks = newBlocks |
||||||
|
} |
||||||
|
|
||||||
|
// GetAbsolutePositions returns the absolute positions of all blocks in the tetromino
|
||||||
|
func (t *Tetromino) GetAbsolutePositions() [][2]int { |
||||||
|
positions := make([][2]int, len(t.Blocks)) |
||||||
|
for i, block := range t.Blocks { |
||||||
|
positions[i] = [2]int{t.X + block.X, t.Y + block.Y} |
||||||
|
} |
||||||
|
return positions |
||||||
|
} |
@ -0,0 +1,468 @@ |
|||||||
|
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 |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
module tetris |
||||||
|
|
||||||
|
go 1.24.2 |
||||||
|
|
||||||
|
require github.com/hajimehoshi/ebiten/v2 v2.8.8 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect |
||||||
|
github.com/ebitengine/hideconsole v1.0.0 // indirect |
||||||
|
github.com/ebitengine/purego v0.8.0 // indirect |
||||||
|
github.com/jezek/xgb v1.1.1 // indirect |
||||||
|
golang.org/x/sync v0.8.0 // indirect |
||||||
|
golang.org/x/sys v0.25.0 // indirect |
||||||
|
) |
@ -0,0 +1,16 @@ |
|||||||
|
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM= |
||||||
|
github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY= |
||||||
|
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= |
||||||
|
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= |
||||||
|
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= |
||||||
|
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= |
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.8.8 h1:xyMxOAn52T1tQ+j3vdieZ7auDBOXmvjUprSrxaIbsi8= |
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.8.8/go.mod h1:durJ05+OYnio9b8q0sEtOgaNeBEQG7Yr7lRviAciYbs= |
||||||
|
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= |
||||||
|
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= |
||||||
|
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= |
||||||
|
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= |
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= |
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= |
||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= |
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
@ -0,0 +1,21 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"log" |
||||||
|
"math/rand" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2" |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
rand.Seed(time.Now().UnixNano()) |
||||||
|
|
||||||
|
ebiten.SetWindowSize(ScreenWidth, ScreenHeight) |
||||||
|
ebiten.SetWindowTitle("Tetris") |
||||||
|
|
||||||
|
game := NewGame() |
||||||
|
if err := ebiten.RunGame(game); err != nil { |
||||||
|
log.Fatal(err) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue