parent
95d3f05079
commit
22864ffcab
@ -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 |
@ -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 |
||||
} |
@ -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
|
||||
} |
@ -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…
Reference in new issue