生成可运行的版本

main v1.0
huyinsong 1 week ago
parent 95d3f05079
commit 22864ffcab
  1. 173
      .github/workflows/build.yml
  2. 87
      .gitignore
  3. 223
      Makefile
  4. 77
      README.md
  5. 468
      game.go
  6. 0
      go.mod
  7. 0
      go.sum
  8. 440
      internal/game/game.go
  9. 88
      internal/tetromino/tetromino.go
  10. 34
      internal/types/types.go
  11. 9
      main.go
  12. 151
      patch/0001-fix-IBlock-disappear-issue.patch
  13. 17
      pkg/config/config.go

@ -0,0 +1,173 @@
name: Build and Release
on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
jobs:
build:
name: Build for ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
binary_suffix: ""
archive_type: tar.gz
- os: windows-latest
goos: windows
goarch: amd64
binary_suffix: ".exe"
archive_type: zip
- os: macos-latest
goos: darwin
goarch: amd64
binary_suffix: ""
archive_type: tar.gz
- os: macos-latest
goos: darwin
goarch: arm64
binary_suffix: ""
archive_type: tar.gz
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install dependencies (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libc6-dev libglu1-mesa-dev libgl1-mesa-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev libasound2-dev pkg-config
- name: Download dependencies
run: go mod download
- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
mkdir -p build
go build -ldflags "-s -w" -o build/tetris${{ matrix.binary_suffix }} .
- name: Create archive (tar.gz)
if: matrix.archive_type == 'tar.gz'
run: |
cd build
tar -czf tetris-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz tetris${{ matrix.binary_suffix }}
- name: Create archive (zip)
if: matrix.archive_type == 'zip'
run: |
cd build
powershell Compress-Archive -Path tetris${{ matrix.binary_suffix }} -DestinationPath tetris-${{ matrix.goos }}-${{ matrix.goarch }}.zip
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: tetris-${{ matrix.goos }}-${{ matrix.goarch }}
path: build/tetris-${{ matrix.goos }}-${{ matrix.goarch }}.*
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libc6-dev libglu1-mesa-dev libgl1-mesa-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev libxxf86vm-dev libasound2-dev pkg-config
- name: Download dependencies
run: go mod download
- name: Run tests
run: go test -v ./...
- name: Run go vet
run: go vet ./...
- name: Check formatting
run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "The following files are not formatted:"
gofmt -s -l .
exit 1
fi
release:
name: Create Release
needs: [build, test]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Tetris ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Linux Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./tetris-linux-amd64/tetris-linux-amd64.tar.gz
asset_name: tetris-linux-amd64.tar.gz
asset_content_type: application/gzip
- name: Upload Windows Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./tetris-windows-amd64/tetris-windows-amd64.zip
asset_name: tetris-windows-amd64.zip
asset_content_type: application/zip
- name: Upload macOS Intel Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./tetris-darwin-amd64/tetris-darwin-amd64.tar.gz
asset_name: tetris-darwin-amd64.tar.gz
asset_content_type: application/gzip
- name: Upload macOS ARM Release Asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./tetris-darwin-arm64/tetris-darwin-arm64.tar.gz
asset_name: tetris-darwin-arm64.tar.gz
asset_content_type: application/gzip

87
.gitignore vendored

@ -0,0 +1,87 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
tetris
tetris.exe
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# IDE and Editor files
.vscode/
.idea/
*.swp
*.swo
*~
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
# Linux
*~
# Temporary files
*.tmp
*.temp
*.log
# Build artifacts
build/
dist/
bin/
# Environment variables
.env
.env.local
.env.production
# Debug files
debug
*.pprof
# Air live reload
tmp/
# Go mod cache
go.sum.bak
# Crash logs
crash.log
# Runtime data
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# Logs
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Game specific
saves/
screenshots/

@ -0,0 +1,223 @@
# Tetris Game Makefile
# Supports cross-compilation for Windows, macOS, and Linux
# Variables
BINARY_NAME=tetris
MAIN_PATH=.
BUILD_DIR=build
VERSION?=dev
LDFLAGS=-ldflags "-s -w"
# Platform specific settings
WINDOWS_BINARY=$(BINARY_NAME).exe
MACOS_BINARY=$(BINARY_NAME)_macos
LINUX_BINARY=$(BINARY_NAME)_linux
# Default target
.PHONY: help
help: ## Show this help message
@echo "Available targets:"
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
# Build targets
.PHONY: build
build: ## Build for current platform
@echo "Building $(BINARY_NAME) for current platform..."
go build $(LDFLAGS) -o $(BINARY_NAME) $(MAIN_PATH)
@echo "Build complete: $(BINARY_NAME)"
.PHONY: build-windows
build-windows: ## Build for Windows (64-bit) - requires Windows or cross-compilation setup
@echo "Building $(BINARY_NAME) for Windows..."
@echo "Note: Cross-compilation for Ebitengine requires proper CGO setup"
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(WINDOWS_BINARY) $(MAIN_PATH)
@echo "Build complete: $(BUILD_DIR)/$(WINDOWS_BINARY)"
.PHONY: build-windows-native
build-windows-native: ## Build for Windows using native Go (may have limitations)
@echo "Building $(BINARY_NAME) for Windows (native Go)..."
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -tags ebitengine -o $(BUILD_DIR)/$(WINDOWS_BINARY) $(MAIN_PATH)
@echo "Build complete: $(BUILD_DIR)/$(WINDOWS_BINARY)"
.PHONY: build-macos
build-macos: ## Build for macOS (Intel) - requires macOS or cross-compilation setup
@echo "Building $(BINARY_NAME) for macOS (Intel)..."
@echo "Note: Cross-compilation for Ebitengine requires proper CGO setup"
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(MACOS_BINARY)_amd64 $(MAIN_PATH)
@echo "Build complete: $(BUILD_DIR)/$(MACOS_BINARY)_amd64"
.PHONY: build-macos-arm
build-macos-arm: ## Build for macOS (Apple Silicon) - requires macOS or cross-compilation setup
@echo "Building $(BINARY_NAME) for macOS (Apple Silicon)..."
@echo "Note: Cross-compilation for Ebitengine requires proper CGO setup"
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(MACOS_BINARY)_arm64 $(MAIN_PATH)
@echo "Build complete: $(BUILD_DIR)/$(MACOS_BINARY)_arm64"
.PHONY: build-macos-current
build-macos-current: ## Build for current macOS architecture
@echo "Building $(BINARY_NAME) for current macOS architecture..."
@mkdir -p $(BUILD_DIR)
go build $(LDFLAGS) -o $(BUILD_DIR)/$(MACOS_BINARY)_$$(go env GOARCH) $(MAIN_PATH)
@echo "Build complete: $(BUILD_DIR)/$(MACOS_BINARY)_$$(go env GOARCH)"
.PHONY: build-linux
build-linux: ## Build for Linux (64-bit) - requires Linux or cross-compilation setup
@echo "Building $(BINARY_NAME) for Linux..."
@echo "Note: Cross-compilation for Ebitengine requires proper CGO setup"
@mkdir -p $(BUILD_DIR)
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(LINUX_BINARY) $(MAIN_PATH)
@echo "Build complete: $(BUILD_DIR)/$(LINUX_BINARY)"
.PHONY: build-current-platform
build-current-platform: ## Build for current platform (recommended)
@echo "Building $(BINARY_NAME) for current platform: $$(go env GOOS)/$$(go env GOARCH)"
@mkdir -p $(BUILD_DIR)
go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)_$$(go env GOOS)_$$(go env GOARCH) $(MAIN_PATH)
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)_$$(go env GOOS)_$$(go env GOARCH)"
.PHONY: build-all
build-all: ## Build for all platforms (requires proper cross-compilation setup)
@echo "Attempting to build for all platforms..."
@echo "Note: This may fail without proper cross-compilation environment"
$(MAKE) build-current-platform
-$(MAKE) build-windows-native
@echo "Build summary:"
@ls -la $(BUILD_DIR)/ 2>/dev/null || echo "No builds completed successfully"
# Development targets
.PHONY: run
run: ## Run the game
@echo "Starting Tetris game..."
go run $(MAIN_PATH)
.PHONY: dev
dev: clean build run ## Clean, build and run for development
.PHONY: install
install: ## Install the game to GOPATH/bin
@echo "Installing $(BINARY_NAME)..."
go install $(LDFLAGS) $(MAIN_PATH)
@echo "Installation complete!"
# Quality assurance targets
.PHONY: test
test: ## Run tests
@echo "Running tests..."
go test -v ./...
.PHONY: test-coverage
test-coverage: ## Run tests with coverage
@echo "Running tests with coverage..."
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
.PHONY: fmt
fmt: ## Format code
@echo "Formatting code..."
go fmt ./...
.PHONY: vet
vet: ## Run go vet
@echo "Running go vet..."
go vet ./...
.PHONY: lint
lint: ## Run golangci-lint (requires golangci-lint to be installed)
@echo "Running golangci-lint..."
golangci-lint run
.PHONY: check
check: fmt vet test ## Run format, vet, and tests
# Dependency management
.PHONY: deps
deps: ## Download dependencies
@echo "Downloading dependencies..."
go mod download
.PHONY: deps-update
deps-update: ## Update dependencies
@echo "Updating dependencies..."
go mod tidy
go get -u ./...
go mod tidy
.PHONY: deps-vendor
deps-vendor: ## Vendor dependencies
@echo "Vendoring dependencies..."
go mod vendor
# Cleanup targets
.PHONY: clean
clean: ## Clean build artifacts
@echo "Cleaning build artifacts..."
@rm -f $(BINARY_NAME)
@rm -rf $(BUILD_DIR)
@rm -f coverage.out coverage.html
@echo "Clean complete!"
.PHONY: clean-all
clean-all: clean ## Clean everything including vendor
@echo "Cleaning vendor directory..."
@rm -rf vendor/
# Release targets
.PHONY: release
release: clean build-current-platform ## Create release build for current platform
@echo "Creating release package for current platform..."
@mkdir -p $(BUILD_DIR)/releases
@CURRENT_OS=$$(go env GOOS); \
CURRENT_ARCH=$$(go env GOARCH); \
if [ "$$CURRENT_OS" = "windows" ]; then \
cd $(BUILD_DIR) && zip releases/$(BINARY_NAME)-$$CURRENT_OS-$$CURRENT_ARCH.zip $(BINARY_NAME)_$$CURRENT_OS_$$CURRENT_ARCH; \
else \
cd $(BUILD_DIR) && tar -czf releases/$(BINARY_NAME)-$$CURRENT_OS-$$CURRENT_ARCH.tar.gz $(BINARY_NAME)_$$CURRENT_OS_$$CURRENT_ARCH; \
fi
@echo "Release package created in $(BUILD_DIR)/releases/"
@ls -la $(BUILD_DIR)/releases/
# Docker targets (optional)
.PHONY: docker-build
docker-build: ## Build Docker image
@echo "Building Docker image..."
docker build -t $(BINARY_NAME):$(VERSION) .
.PHONY: docker-run
docker-run: ## Run in Docker container
@echo "Running in Docker container..."
docker run --rm -it $(BINARY_NAME):$(VERSION)
# Info targets
.PHONY: info
info: ## Show build information
@echo "=== Build Information ==="
@echo "Binary name: $(BINARY_NAME)"
@echo "Version: $(VERSION)"
@echo "Go version: $$(go version)"
@echo "Build directory: $(BUILD_DIR)"
@echo "Current platform: $$(go env GOOS)/$$(go env GOARCH)"
@echo "CGO enabled: $$(go env CGO_ENABLED)"
.PHONY: env
env: ## Show Go environment
@echo "=== Go Environment ==="
@go env
.PHONY: cross-compile-info
cross-compile-info: ## Show cross-compilation information
@echo "=== Cross-Compilation Information ==="
@echo "Ebitengine requires CGO for cross-compilation."
@echo "To build for other platforms, you need:"
@echo "1. C compiler for target platform"
@echo "2. Platform-specific libraries"
@echo "3. Proper CGO_ENABLED=1 setting"
@echo ""
@echo "Recommended approach:"
@echo "- Build on the target platform directly"
@echo "- Use GitHub Actions or CI/CD for multi-platform builds"
@echo "- Use Docker with multi-stage builds"

@ -1,38 +1,69 @@
# Tetris Game in Go
# Tetris Game
A simple Tetris game implementation using Go and the Ebiten game engine.
A classic Tetris game implemented in Go using the Ebitengine game engine.
## Requirements
## Features
- Go 1.16 or later
- Ebiten v2
- Classic Tetris gameplay
- Ghost piece preview
- Next piece preview
- Line clearing with scoring
- Progressive difficulty levels
- Smooth controls with wall kicks
## Installation
## Project Structure
1. Clone the repository
2. Install dependencies:
```bash
go mod tidy
```
tetris/
├── main.go # Application entry point
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── assets/ # Game assets (future use)
├── internal/ # Internal packages
│ ├── game/ # Core game logic
│ │ └── game.go # Game state and rendering
│ ├── tetromino/ # Tetromino (falling pieces) logic
│ │ └── tetromino.go # Piece shapes, rotation, movement
│ └── types/ # Shared type definitions
│ └── types.go # Block types and colors
└── pkg/ # Public packages
└── config/ # Configuration constants
└── config.go # Game configuration and constants
```
## Architecture
The project follows Go best practices with a clean package structure:
- **`main.go`**: Entry point that initializes the game
- **`pkg/config`**: Contains all game configuration constants
- **`internal/types`**: Defines core data types and enums
- **`internal/tetromino`**: Handles tetromino piece logic (shapes, rotation, movement)
- **`internal/game`**: Core game mechanics (board state, input handling, rendering)
## Running the Game
## Controls
- **Left/Right Arrow**: Move piece horizontally
- **Up Arrow**: Rotate piece clockwise
- **Down Arrow**: Soft drop (faster falling)
- **Space**: Hard drop (instant drop)
- **Space** (when game over): Restart game
## Building and Running
```bash
# Run the game
go run .
# Build executable
go build -o tetris
./tetris
```
## Controls
## Dependencies
- 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
- [Ebitengine](https://ebitengine.org/) v2.8.8 - 2D game engine for Go
## Features
## License
- Classic Tetris gameplay
- Score tracking
- Next piece preview
- Level system
This project is open source and available under the MIT License.

@ -1,468 +0,0 @@
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,440 @@
package game
import (
"fmt"
"image/color"
"math/rand"
"time"
"tetris/internal/tetromino"
"tetris/internal/types"
"tetris/pkg/config"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
// Game represents the main game state
type Game struct {
board [][]types.BlockType
currentPiece *tetromino.Tetromino
nextPiece *tetromino.Tetromino
dropCounter int
dropInterval int
score int
level int
gameOver bool
lastMoveDown time.Time
lastRotate time.Time
}
// New creates a new game instance
func New() *Game {
g := &Game{
board: make([][]types.BlockType, config.BoardHeight),
dropInterval: config.InitialDropInterval,
lastMoveDown: time.Now(),
lastRotate: time.Now(),
}
for i := range g.board {
g.board[i] = make([]types.BlockType, config.BoardWidth)
}
g.spawnNewPiece()
return g
}
// Update handles game logic updates
func (g *Game) Update() error {
if g.gameOver {
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
*g = *New()
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(config.BoardWidth*config.BlockSize), float64(config.BoardHeight*config.BlockSize), color.RGBA{40, 40, 40, 255})
// Draw preview area background
ebitenutil.DrawRect(screen, float64(config.PreviewX-config.BlockSize), 0, float64(6*config.BlockSize), float64(config.ScreenHeight), color.RGBA{30, 30, 30, 255})
// Draw grid lines
gridColor := color.RGBA{60, 60, 60, 255}
for x := 0; x <= config.BoardWidth; x++ {
ebitenutil.DrawLine(screen, float64(x*config.BlockSize), 0, float64(x*config.BlockSize), float64(config.BoardHeight*config.BlockSize), gridColor)
}
for y := 0; y <= config.BoardHeight; y++ {
ebitenutil.DrawLine(screen, 0, float64(y*config.BlockSize), float64(config.BoardWidth*config.BlockSize), float64(y*config.BlockSize), gridColor)
}
// Draw placed blocks
for y := range config.BoardHeight {
for x := range config.BoardWidth {
if g.board[y][x] != config.EmptyBlock {
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]*config.BlockSize),
float64(pos[1]*config.BlockSize),
float64(config.BlockSize-1),
float64(config.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, config.PreviewX-config.BlockSize, config.BlockSize)
for _, block := range g.nextPiece.Blocks {
x := config.PreviewX + block.X*config.BlockSize
y := config.PreviewY + block.Y*config.BlockSize
ebitenutil.DrawRect(screen,
float64(x),
float64(y),
float64(config.BlockSize-1),
float64(config.BlockSize-1),
types.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, config.PreviewX-config.BlockSize, config.ScreenHeight-4*config.BlockSize)
ebitenutil.DebugPrintAt(screen, levelStr, config.PreviewX-config.BlockSize, config.ScreenHeight-3*config.BlockSize)
if g.gameOver {
gameOverStr := "Game Over!"
restartStr := "Press SPACE to restart"
ebitenutil.DebugPrintAt(screen, gameOverStr, config.PreviewX-config.BlockSize, config.ScreenHeight/2)
ebitenutil.DebugPrintAt(screen, restartStr, config.PreviewX-config.BlockSize, config.ScreenHeight/2+20)
}
}
// Layout implements ebiten.Game interface
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return config.ScreenWidth, config.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() {
g.dropCounter++
if g.dropCounter >= g.dropInterval {
g.dropCounter = 0
if !g.tryMove(0, 1) {
g.lockPiece()
}
}
}
// tryMove attempts to move the current piece and returns success
func (g *Game) tryMove(dx, dy int) bool {
if g.currentPiece == nil {
return false
}
originalX, originalY := g.currentPiece.X, g.currentPiece.Y
g.currentPiece.Move(dx, dy)
if g.isColliding() {
g.currentPiece.X, g.currentPiece.Y = originalX, originalY
return false
}
return true
}
// tryRotate attempts to rotate the current piece with wall kicks
func (g *Game) tryRotate() bool {
if g.currentPiece == nil {
return false
}
// Store original state
originalBlocks := make([]types.Block, len(g.currentPiece.Blocks))
copy(originalBlocks, g.currentPiece.Blocks)
// Try rotation
g.currentPiece.Rotate()
// Try wall kicks
var kicks [][2]int
if g.currentPiece.BlockType == types.IBlock {
kicks = tetromino.IKicks
} else {
kicks = tetromino.NormalKicks
}
for _, kick := range kicks {
originalX, originalY := g.currentPiece.X, g.currentPiece.Y
g.currentPiece.Move(kick[0], kick[1])
if !g.isColliding() {
return true
}
// Reset position for next kick attempt
g.currentPiece.X, g.currentPiece.Y = originalX, originalY
}
// All kicks failed, revert rotation
g.currentPiece.Blocks = originalBlocks
return false
}
// hardDrop drops the piece instantly to the bottom
func (g *Game) hardDrop() {
if g.currentPiece == nil {
return
}
dropDistance := 0
for {
if !g.tryMove(0, 1) {
break
}
dropDistance++
}
// Award extra points for hard drop
g.score += dropDistance * 2
g.lockPiece()
}
// isColliding checks if the current piece is colliding 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 >= config.BoardWidth || y >= config.BoardHeight {
return true
}
// Check collision with placed blocks (but allow pieces above the visible board)
if y >= 0 && g.board[y][x] != config.EmptyBlock {
return true
}
}
return false
}
// lockPiece places the current piece on the board and spawns a new one
func (g *Game) lockPiece() {
if g.currentPiece == nil {
return
}
// Place the piece on the board
for _, pos := range g.currentPiece.GetAbsolutePositions() {
x, y := pos[0], pos[1]
if y >= 0 && y < config.BoardHeight && x >= 0 && x < config.BoardWidth {
g.board[y][x] = g.currentPiece.BlockType
}
}
// Clear completed lines
linesCleared := g.clearLines()
// Update score and level
g.updateScore(linesCleared)
// Spawn new piece
g.spawnNewPiece()
// Check for game over
if g.isColliding() {
g.gameOver = true
}
}
// clearLines removes completed lines and returns the number cleared
func (g *Game) clearLines() int {
linesCleared := 0
for y := config.BoardHeight - 1; y >= 0; y-- {
if g.isLineFull(y) {
g.removeLine(y)
linesCleared++
y++ // Check the same line again since everything moved down
}
}
return linesCleared
}
// updateScore updates the score and level based on lines cleared
func (g *Game) updateScore(linesCleared int) {
points := []int{0, 40, 100, 300, 1200} // Points for 0, 1, 2, 3, 4 lines
if linesCleared <= 4 {
g.score += points[linesCleared] * (g.level + 1)
}
// Increase level every 10 lines
newLevel := g.score / 1000
if newLevel > g.level {
g.level = newLevel
g.dropInterval = max(config.MinDropInterval, config.InitialDropInterval-g.level*3)
}
}
// isLineFull checks if a line is completely filled
func (g *Game) isLineFull(y int) bool {
for x := 0; x < config.BoardWidth; x++ {
if g.board[y][x] == config.EmptyBlock {
return false
}
}
return true
}
// removeLine removes a line and shifts everything above down
func (g *Game) removeLine(lineY int) {
for y := lineY; y > 0; y-- {
copy(g.board[y], g.board[y-1])
}
// Clear the top line
for x := 0; x < config.BoardWidth; x++ {
g.board[0][x] = config.EmptyBlock
}
}
// spawnNewPiece creates a new falling piece
func (g *Game) spawnNewPiece() {
if g.nextPiece == nil {
g.nextPiece = tetromino.New(types.BlockType(rand.Intn(7)+1), config.BoardWidth/2-1, 0)
}
g.currentPiece = g.nextPiece
g.nextPiece = tetromino.New(types.BlockType(rand.Intn(7)+1), config.BoardWidth/2-1, 0)
g.dropCounter = 0
}
// wouldCollide checks if a piece would collide at its current position
func (g *Game) wouldCollide(piece *tetromino.Tetromino) bool {
for _, pos := range piece.GetAbsolutePositions() {
x, y := pos[0], pos[1]
// Check boundaries
if x < 0 || x >= config.BoardWidth || y >= config.BoardHeight {
return true
}
// Check collision with placed blocks
if y >= 0 && g.board[y][x] != config.EmptyBlock {
return true
}
}
return false
}
// drawBlock draws a single block at the specified position
func (g *Game) drawBlock(screen *ebiten.Image, x, y int, blockType types.BlockType) {
ebitenutil.DrawRect(screen,
float64(x*config.BlockSize),
float64(y*config.BlockSize),
float64(config.BlockSize-1),
float64(config.BlockSize-1),
types.BlockColors[blockType])
// Add a subtle border for 3D effect
borderColor := color.RGBA{255, 255, 255, 50}
ebitenutil.DrawRect(screen,
float64(x*config.BlockSize),
float64(y*config.BlockSize),
float64(config.BlockSize-1),
2,
borderColor)
ebitenutil.DrawRect(screen,
float64(x*config.BlockSize),
float64(y*config.BlockSize),
2,
float64(config.BlockSize-1),
borderColor)
}
// max returns the maximum of two integers
func max(a, b int) int {
if a > b {
return a
}
return b
}

@ -1,79 +1,40 @@
package main
package tetromino
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
}
import "tetris/internal/types"
// TetrominoShapes defines the shape of each tetromino type
var TetrominoShapes = map[BlockType][][]bool{
IBlock: {
var TetrominoShapes = map[types.BlockType][][]bool{
types.IBlock: {
{false, false, false, false},
{false, false, false, false},
{true, true, true, true},
{false, false, false, false},
},
JBlock: {
types.JBlock: {
{true, false, false},
{true, true, true},
{false, false, false},
},
LBlock: {
types.LBlock: {
{false, false, true},
{true, true, true},
{false, false, false},
},
OBlock: {
types.OBlock: {
{true, true},
{true, true},
},
SBlock: {
types.SBlock: {
{false, true, true},
{true, true, false},
{false, false, false},
},
TBlock: {
types.TBlock: {
{false, true, false},
{true, true, true},
{false, false, false},
},
ZBlock: {
types.ZBlock: {
{true, true, false},
{false, true, true},
{false, false, false},
@ -97,15 +58,22 @@ var NormalKicks = [][2]int{
{0, -1},
}
// NewTetromino creates a new tetromino of the specified type
func NewTetromino(blockType BlockType, x, y int) *Tetromino {
// Tetromino represents a complete tetris piece
type Tetromino struct {
Blocks []types.Block
BlockType types.BlockType
X, Y int
}
// New creates a new tetromino of the specified type
func New(blockType types.BlockType, x, y int) *Tetromino {
shape := TetrominoShapes[blockType]
blocks := make([]Block, 0)
blocks := make([]types.Block, 0)
for i := 0; i < len(shape); i++ {
for j := 0; j < len(shape[i]); j++ {
for i := range shape {
for j := range shape[i] {
if shape[i][j] {
blocks = append(blocks, Block{
blocks = append(blocks, types.Block{
X: j,
Y: i,
Type: blockType,
@ -115,7 +83,7 @@ func NewTetromino(blockType BlockType, x, y int) *Tetromino {
}
// Adjust initial position for I piece
if blockType == IBlock {
if blockType == types.IBlock {
y-- // Start one row higher
}
@ -135,13 +103,13 @@ func (t *Tetromino) Move(dx, dy int) {
// Rotate rotates the tetromino clockwise
func (t *Tetromino) Rotate() {
if t.BlockType == OBlock {
if t.BlockType == types.OBlock {
return // O block doesn't need rotation
}
// Get the size of the shape matrix
size := 3
if t.BlockType == IBlock {
if t.BlockType == types.IBlock {
size = 4
}
@ -159,7 +127,7 @@ func (t *Tetromino) Rotate() {
}
// Create a new rotated shape
newBlocks := make([]Block, 0)
newBlocks := make([]types.Block, 0)
// Perform rotation
for y := 0; y < size; y++ {
@ -168,7 +136,7 @@ func (t *Tetromino) Rotate() {
// Rotate coordinates 90 degrees clockwise
newX := size - 1 - y
newY := x
newBlocks = append(newBlocks, Block{
newBlocks = append(newBlocks, types.Block{
X: newX,
Y: newY,
Type: t.BlockType,

@ -0,0 +1,34 @@
package types
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 + 1
JBlock
LBlock
OBlock
SBlock
TBlock
ZBlock
)
// Colors for different block types
var BlockColors = map[BlockType]color.Color{
0: color.RGBA{0, 0, 0, 0}, // EmptyBlock
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
}

@ -5,17 +5,20 @@ import (
"math/rand"
"time"
"tetris/internal/game"
"tetris/pkg/config"
"github.com/hajimehoshi/ebiten/v2"
)
func main() {
rand.Seed(time.Now().UnixNano())
ebiten.SetWindowSize(ScreenWidth, ScreenHeight)
ebiten.SetWindowSize(config.ScreenWidth, config.ScreenHeight)
ebiten.SetWindowTitle("Tetris")
game := NewGame()
if err := ebiten.RunGame(game); err != nil {
g := game.New()
if err := ebiten.RunGame(g); err != nil {
log.Fatal(err)
}
}

@ -0,0 +1,151 @@
From ffe9242a73abfb3e8ee9b6effd57e64155717bfb Mon Sep 17 00:00:00 2001
From: yinqiang <zhuyinqiang@foxmail.com>
Date: Sat, 7 Jun 2025 22:50:07 +0800
Subject: [PATCH] fix IBlock disappear issue
---
block.go | 21 +++++++++++----------
game.go | 26 +++++++++++++++-----------
2 files changed, 26 insertions(+), 21 deletions(-)
diff --git a/block.go b/block.go
index ad276ea..0b3aaee 100644
--- a/block.go
+++ b/block.go
@@ -14,7 +14,7 @@ type Block struct {
type BlockType int
const (
- IBlock BlockType = iota
+ IBlock BlockType = iota + 1
JBlock
LBlock
OBlock
@@ -32,13 +32,14 @@ type Tetromino struct {
// 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
+ EmptyBlock: color.RGBA{0, 0, 0, 0}, // Black
+ 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
@@ -102,8 +103,8 @@ 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++ {
+ for i := range shape {
+ for j := range shape[i] {
if shape[i][j] {
blocks = append(blocks, Block{
X: j,
diff --git a/game.go b/game.go
index aa9896f..2aa98e9 100644
--- a/game.go
+++ b/game.go
@@ -24,6 +24,7 @@ const (
// Game constants
InitialDropInterval = 60
MinDropInterval = 5
+ EmptyBlock = 0
)
// Game represents the main game state
@@ -90,9 +91,9 @@ func (g *Game) Draw(screen *ebiten.Image) {
}
// Draw placed blocks
- for y := 0; y < BoardHeight; y++ {
- for x := 0; x < BoardWidth; x++ {
- if g.board[y][x] != 0 {
+ for y := range BoardHeight {
+ for x := range BoardWidth {
+ if g.board[y][x] != EmptyBlock {
g.drawBlock(screen, x, y, g.board[y][x])
}
}
@@ -311,7 +312,7 @@ func (g *Game) isColliding() bool {
}
// Check collision with other pieces
if y >= 0 && x >= 0 && x < BoardWidth && y < BoardHeight {
- if g.board[y][x] != 0 {
+ if g.board[y][x] != EmptyBlock {
return true
}
}
@@ -324,6 +325,9 @@ func (g *Game) lockPiece() {
if g.currentPiece == nil {
return
}
+ // if g.currentPiece.BlockType == IBlock {
+ // g.currentPiece.BlockType = IBlock
+ // }
positions := g.currentPiece.GetAbsolutePositions()
for _, pos := range positions {
@@ -378,8 +382,8 @@ func (g *Game) clearLines() {
// 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 {
+ for x := range BoardWidth {
+ if g.board[y][x] == EmptyBlock {
return false
}
}
@@ -391,15 +395,15 @@ 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
+ for x := range BoardWidth {
+ g.board[0][x] = EmptyBlock
}
}
// 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.nextPiece = NewTetromino(BlockType(rand.Intn(7)+1), 0, 0)
}
g.currentPiece = g.nextPiece
@@ -409,7 +413,7 @@ func (g *Game) spawnNewPiece() {
}
g.currentPiece.Y = 0
- g.nextPiece = NewTetromino(BlockType(rand.Intn(7)), 0, 0)
+ g.nextPiece = NewTetromino(BlockType(rand.Intn(7)+1), 0, 0)
if g.isColliding() {
g.gameOver = true
@@ -423,7 +427,7 @@ func (g *Game) wouldCollide(piece *Tetromino) bool {
if x < 0 || x >= BoardWidth || y >= BoardHeight {
return true
}
- if y >= 0 && y < BoardHeight && x >= 0 && x < BoardWidth && g.board[y][x] != 0 {
+ if y >= 0 && y < BoardHeight && x >= 0 && x < BoardWidth && g.board[y][x] != EmptyBlock {
return true
}
}
--
2.39.5 (Apple Git-154)

@ -0,0 +1,17 @@
package config
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
EmptyBlock = 0
)
Loading…
Cancel
Save