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 |
- Classic Tetris gameplay |
||||||
- Ebiten v2 |
- 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 |
```bash |
||||||
|
# Run the game |
||||||
go run . |
go run . |
||||||
|
|
||||||
|
# Build executable |
||||||
|
go build -o tetris |
||||||
|
./tetris |
||||||
``` |
``` |
||||||
|
|
||||||
## Controls |
## Dependencies |
||||||
|
|
||||||
- Left Arrow: Move piece left |
- [Ebitengine](https://ebitengine.org/) v2.8.8 - 2D game engine for Go |
||||||
- Right Arrow: Move piece right |
|
||||||
- Down Arrow: Move piece down faster |
|
||||||
- Up Arrow: Rotate piece |
|
||||||
- Space: Drop piece instantly |
|
||||||
- ESC: Quit game |
|
||||||
|
|
||||||
## Features |
## License |
||||||
|
|
||||||
- Classic Tetris gameplay |
This project is open source and available under the MIT License. |
||||||
- Score tracking |
|
||||||
- Next piece preview |
|
||||||
- Level system |
|
@ -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