Compare commits

..

3 Commits
v1.0.2 ... main

Author SHA1 Message Date
huyinsong 1e046f2c6a docs: 更新README.md - 添加双语支持和完善项目文档 6 days ago
huyinsong 0e6338a383 添加退出说明 6 days ago
huyinsong 9a8acbe126 添加退出机制,修复游戏结束重新开始按键绑定问题,修复游戏结束时显示文字的问题 6 days ago
  1. 261
      README.md
  2. 185
      internal/game/game.go
  3. 3
      internal/i18n/i18n.go

@ -1,80 +1,237 @@
# Tetris Game
# Tetris Game | 俄罗斯方块
A classic Tetris game implemented in Go using the Ebitengine game engine.
一个使用 Go 语言和 Ebitengine 游戏引擎实现的经典俄罗斯方块游戏,支持中英文双语界面。
## Features
A classic Tetris game implemented in Go using the Ebitengine game engine with bilingual support (Chinese/English).
- Classic Tetris gameplay
- Ghost piece preview
- Next piece preview
- Line clearing with scoring
- Progressive difficulty levels
- Smooth controls with wall kicks
## ✨ 特性 | Features
## Project Structure
### 🎮 游戏特性 | Game Features
- ✅ 经典俄罗斯方块玩法 | Classic Tetris gameplay
- 👻 半透明幽灵方块预览 | Transparent ghost piece preview
- 🔮 下一个方块预览 | Next piece preview
- 💥 消除行数计分系统 | Line clearing with scoring system
- 📈 渐进式难度等级 | Progressive difficulty levels
- 🎯 平滑控制与踢墙系统 | Smooth controls with wall kicks
- ⚡ 硬降落特效 | Hard drop visual effects
- 🎨 彩色方块显示 | Colorful block rendering
### 🌍 多语言支持 | Multi-language Support
- 🇺🇸 **English** - 完整英文界面 | Full English interface
- 🇨🇳 **中文** - 完整中文界面 | Full Chinese interface
- 🔄 实时语言切换 | Real-time language switching (Press **L** key)
### 🖥 平台支持 | Platform Support
- 🪟 **Windows** (64-bit) - 包含安装包和启动脚本
- 🍎 **macOS** (10.15+) - 原生 .app 应用包
- 🐧 **Linux** (64-bit) - 支持 X11/Wayland 显示系统
## 🎯 游戏控制 | Game Controls
| 键位 | 功能 | Key | Function |
|------|------|-----|----------|
| ← → | 左右移动方块 | ← → | Move piece horizontally |
| ↑ | 顺时针旋转方块 | ↑ | Rotate piece clockwise |
| ↓ | 软降落 (加速下降) | ↓ | Soft drop (faster falling) |
| 空格 | 硬降落 (瞬间落底) | Space | Hard drop (instant drop) |
| L | 切换中英文界面 | L | Switch language |
| R | 重新开始游戏 | R | Restart game (when game over) |
| Esc | 退出游戏 | Esc | Exit game |
## 🏗 项目结构 | Project Structure
```
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
├── main.go # 应用程序入口 | Application entry point
├── go.mod # Go 模块定义 | Go module definition
├── go.sum # Go 模块校验和 | Go module checksums
├── Makefile # 构建系统 | Build system
├── BUILD.md # 构建说明文档 | Build documentation
├── build.sh # 构建脚本 | Build script
├── assets/ # 游戏资源文件 | Game assets
│ ├── fonts/ # 字体文件 | Font files
│ │ └── HYSongYunLangHeiW-1.ttf # 中文字体
│ └── icons/ # 图标文件 | Icon files
├── internal/ # 内部包 | Internal packages
│ ├── game/ # 核心游戏逻辑 | Core game logic
│ │ └── game.go # 游戏状态和渲染 | Game state and rendering
│ ├── tetromino/ # 俄罗斯方块逻辑 | Tetromino logic
│ │ └── tetromino.go # 方块形状、旋转、移动 | Piece shapes, rotation, movement
│ ├── types/ # 共享类型定义 | Shared type definitions
│ │ └── types.go # 方块类型和颜色 | Block types and colors
│ ├── font/ # 字体渲染系统 | Font rendering system
│ │ └── font.go # 字体渲染器 | Font renderer
│ └── i18n/ # 国际化支持 | Internationalization
│ └── i18n.go # 多语言文本 | Multi-language text
├── pkg/ # 公共包 | Public packages
│ └── config/ # 配置常量 | Configuration constants
│ └── config.go # 游戏配置和常量 | Game configuration
└── dist/ # 分发文件 | Distribution files (generated)
├── TetrisGame-Windows-v1.0.zip # Windows 分发包
├── TetrisGame-macOS-v1.0.dmg # macOS 分发包
└── TetrisGame-Linux-v1.0.tar.gz # Linux 分发包
```
## Architecture
## 🏛 架构设计 | Architecture
项目遵循 Go 语言最佳实践,采用清晰的包结构设计:
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)
- **`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
- **`internal/game`**: 核心游戏机制 | Core game mechanics (board state, input handling, rendering)
- **`internal/font`**: 字体渲染系统 | Font rendering system for multi-language support
- **`internal/i18n`**: 国际化和本地化支持 | Internationalization and localization support
## 🚀 快速开始 | Quick Start
### 开发环境运行 | Development
```bash
# 克隆项目 | Clone the repository
git clone <repository-url>
cd tetris
# 下载依赖 | Download dependencies
go mod download
# 运行游戏 | Run the game
go run .
# 测试字体渲染 | Test font rendering
go run . --test-font
```
### 构建分发版本 | Build Distribution
```bash
# 构建所有平台版本 | Build for all platforms
make all
# 构建特定平台 | Build for specific platform
make windows # Windows 版本
make macos # macOS 版本
make linux # Linux 版本
## Controls
# 查看构建帮助 | View build help
make help
- **Left/Right Arrow**: Move piece horizontally
- **Up Arrow**: Rotate piece clockwise
- **Down Arrow**: Soft drop (faster falling)
- **Space**: Hard drop (instant drop)
- **L**: Switch language (English ⇄ 中文)
- **R** (when game over): Restart game
# 清理构建文件 | Clean build files
make clean
```
构建完成后,分发文件将保存在 `dist/` 目录中。
After building, distribution files will be saved in the `dist/` directory.
## 中文支持 / Chinese Support
## 📦 系统要求 | System Requirements
游戏支持中文界面!启动游戏后按 **L 键** 切换到中文模式。
### 开发环境 | Development Environment
- **Go**: 1.19 或更高版本 | 1.19 or higher
- **操作系统**: Windows 10+, macOS 10.15+, Linux (64-bit)
The game supports Chinese interface! Press **L key** after starting the game to switch to Chinese mode.
### 运行环境 | Runtime Environment
- **Windows**: Windows 10/11 (64-bit)
- **macOS**: macOS 10.15 Catalina 或更高版本 | or higher
- **Linux**: 64-bit Linux 发行版,支持 X11 或 Wayland | distributions with X11/Wayland support
## 🛠 主要依赖 | Dependencies
```go
require (
github.com/hajimehoshi/ebiten/v2 v2.8.8 // 2D 游戏引擎 | 2D game engine
golang.org/x/image v0.20.0 // 图像处理库 | Image processing
)
```
参见 [中文使用指南](HOW_TO_USE_CHINESE.md) 了解详细说明。
- **[Ebitengine](https://ebitengine.org/)**: 现代 Go 语言 2D 游戏引擎 | Modern 2D game engine for Go
- **golang.org/x/image**: Go 官方图像处理扩展库 | Official Go image processing extensions
See [Chinese Usage Guide](HOW_TO_USE_CHINESE.md) for detailed instructions.
## 🎨 特色功能 | Special Features
## Building and Running
### 🌟 视觉效果 | Visual Effects
- **幽灵方块**: 半透明预览方块将要落下的位置 | Transparent preview of where piece will land
- **硬降落特效**: 硬降落时的闪烁动画效果 | Flashing animation during hard drop
- **网格线**: 清晰的游戏区域网格显示 | Clear game area grid display
- **渐变背景**: 美观的背景色彩搭配 | Beautiful background color scheme
### 💯 计分系统 | Scoring System
- **基础分数**: 根据消除行数计算 | Based on lines cleared
- **等级系统**: 随着分数增加难度递增 | Progressive difficulty with score
- **实时显示**: 分数和等级实时更新 | Real-time score and level updates
### 🎯 游戏机制 | Game Mechanics
- **7种标准方块**: I、O、T、S、Z、J、L 形方块 | Standard 7 tetromino pieces
- **踢墙算法**: 智能旋转碰撞检测 | Smart rotation collision detection
- **行消除**: 满行自动消除和分数计算 | Automatic line clearing and scoring
- **渐进速度**: 等级越高下降速度越快 | Progressive drop speed increase
## 🎮 游戏截图 | Screenshots
*(游戏支持实时中英文切换,界面简洁美观)*
*(The game supports real-time Chinese/English switching with a clean and beautiful interface)*
## 🔧 故障排除 | Troubleshooting
### 常见问题 | Common Issues
**中文字体显示异常 | Chinese Font Display Issues**
```bash
# Run the game
go run .
# 检查字体文件是否存在 | Check if font file exists
ls -la assets/fonts/HYSongYunLangHeiW-1.ttf
# Build executable
go build -o tetris
./tetris
# 重新运行字体测试 | Re-run font test
go run . --test-font
```
## Dependencies
**构建失败 | Build Failures**
```bash
# 检查 Go 环境 | Check Go environment
go version
# 清理并重新构建 | Clean and rebuild
make clean && make all
# 检查依赖 | Check dependencies
go mod verify
```
**运行时错误 | Runtime Errors**
- 确保在支持的操作系统上运行 | Ensure running on supported OS
- 检查显卡驱动是否支持 OpenGL | Check if graphics drivers support OpenGL
- 确保有足够的系统资源 | Ensure sufficient system resources
## 🤝 贡献指南 | Contributing
欢迎提交 Issue 和 Pull Request!
Welcome to submit Issues and Pull Requests!
1. Fork 本项目 | Fork the project
2. 创建特性分支 | Create a feature branch
3. 提交更改 | Commit your changes
4. 推送到分支 | Push to the branch
5. 创建 Pull Request | Create a Pull Request
## 📄 许可证 | License
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 📞 联系方式 | Contact
如有问题或建议,请通过以下方式联系:
For questions or suggestions, please contact via:
- [Ebitengine](https://ebitengine.org/) v2.8.8 - 2D game engine for Go
- 📧 提交 Issue | Submit an Issue
- 🐙 GitHub Discussions
- 📝 Pull Request
## License
---
This project is open source and available under the MIT License.
**享受游戏!| Enjoy the game!** 🎮✨

@ -4,6 +4,7 @@ import (
"fmt"
"image/color"
"math/rand"
"os"
"time"
"tetris/internal/font"
@ -19,27 +20,33 @@ import (
// 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
fontRenderer *font.FontRenderer
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
lastMoveLeft time.Time
lastMoveRight time.Time
fontRenderer *font.FontRenderer
hardDropping bool
hardDropCounter int
}
// 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(),
fontRenderer: font.NewFontRenderer(),
board: make([][]types.BlockType, config.BoardHeight),
dropInterval: config.InitialDropInterval,
lastMoveDown: time.Now(),
lastRotate: time.Now(),
lastMoveLeft: time.Now(),
lastMoveRight: time.Now(),
fontRenderer: font.NewFontRenderer(),
}
for i := range g.board {
@ -53,7 +60,7 @@ func New() *Game {
// Update handles game logic updates
func (g *Game) Update() error {
if g.gameOver {
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
*g = *New()
return nil
}
@ -114,10 +121,14 @@ func (g *Game) Draw(screen *ebiten.Image) {
}
}
// Draw actual piece
// Draw actual piece with special effect during hard drop
for _, pos := range g.currentPiece.GetAbsolutePositions() {
if pos[1] >= 0 {
g.drawBlock(screen, pos[0], pos[1], g.currentPiece.BlockType)
if g.hardDropping {
g.drawHardDropBlock(screen, pos[0], pos[1], g.currentPiece.BlockType)
} else {
g.drawBlock(screen, pos[0], pos[1], g.currentPiece.BlockType)
}
}
}
}
@ -151,8 +162,8 @@ func (g *Game) Draw(screen *ebiten.Image) {
if g.gameOver {
text := i18n.GetText()
g.fontRenderer.DrawText(screen, text.GameOver, float64(config.PreviewX-config.BlockSize), float64(config.ScreenHeight/2), color.RGBA{255, 255, 255, 255})
g.fontRenderer.DrawText(screen, text.Restart, float64(config.PreviewX-config.BlockSize), float64(config.ScreenHeight/2+20), color.RGBA{255, 255, 255, 255})
g.fontRenderer.DrawText(screen, text.GameOver, float64(config.ScreenWidth/2-20), float64(config.ScreenHeight/2-100), color.RGBA{255, 255, 255, 255})
g.fontRenderer.DrawText(screen, text.Restart, float64(config.ScreenWidth/2-20), float64(config.ScreenHeight/2-80), color.RGBA{255, 0, 0, 255})
}
}
@ -168,8 +179,8 @@ func (g *Game) handleInput() {
i18n.SwitchLanguage()
}
// Restart game when game over
if g.gameOver && inpututil.IsKeyJustPressed(ebiten.KeyR) {
// Restart game anytime
if inpututil.IsKeyJustPressed(ebiten.KeyR) {
g.restart()
return
}
@ -179,12 +190,37 @@ func (g *Game) handleInput() {
return
}
// Move left/right
if inpututil.IsKeyJustPressed(ebiten.KeyLeft) {
g.tryMove(-1, 0)
// Hard drop
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
g.hardDrop()
}
if inpututil.IsKeyJustPressed(ebiten.KeyRight) {
g.tryMove(1, 0)
// Skip movement inputs during hard drop animation
if g.hardDropping {
return
}
// Move left/right - support continuous movement when holding keys
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
// First press moves immediately, then add delay for continuous movement
if inpututil.IsKeyJustPressed(ebiten.KeyLeft) {
g.tryMove(-1, 0)
g.lastMoveLeft = time.Now()
} else if time.Since(g.lastMoveLeft) > time.Millisecond*120 {
g.tryMove(-1, 0)
g.lastMoveLeft = time.Now()
}
}
if ebiten.IsKeyPressed(ebiten.KeyRight) {
// First press moves immediately, then add delay for continuous movement
if inpututil.IsKeyJustPressed(ebiten.KeyRight) {
g.tryMove(1, 0)
g.lastMoveRight = time.Now()
} else if time.Since(g.lastMoveRight) > time.Millisecond*120 {
g.tryMove(1, 0)
g.lastMoveRight = time.Now()
}
}
// Rotate
@ -202,20 +238,32 @@ func (g *Game) handleInput() {
g.lastMoveDown = time.Now()
}
}
// Hard drop
if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
g.hardDrop()
// Exit game
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
os.Exit(0)
}
}
// updateDropCounter handles automatic piece dropping
// updateDropCounter handles automatic piece dropping and hard drop animation
func (g *Game) updateDropCounter() {
g.dropCounter++
if g.dropCounter >= g.dropInterval {
g.dropCounter = 0
if !g.tryMove(0, 1) {
g.lockPiece()
if g.hardDropping {
// Hard drop animation - move faster
g.hardDropCounter++
if g.hardDropCounter >= 2 { // Move every 2 frames for visible animation
g.hardDropCounter = 0
if !g.tryMove(0, 1) {
g.hardDropping = false
g.lockPiece()
}
}
} else {
// Normal drop
g.dropCounter++
if g.dropCounter >= g.dropInterval {
g.dropCounter = 0
if !g.tryMove(0, 1) {
g.lockPiece()
}
}
}
}
@ -275,24 +323,14 @@ func (g *Game) tryRotate() bool {
return false
}
// hardDrop drops the piece instantly to the bottom
// hardDrop starts the hard drop animation
func (g *Game) hardDrop() {
if g.currentPiece == nil {
if g.currentPiece == nil || g.hardDropping {
return
}
dropDistance := 0
for {
if !g.tryMove(0, 1) {
break
}
dropDistance++
}
// Award extra points for hard drop
g.score += dropDistance * 2
g.lockPiece()
g.hardDropping = true
g.hardDropCounter = 0
}
// isColliding checks if the current piece is colliding with the board or boundaries
@ -454,6 +492,40 @@ func (g *Game) drawBlock(screen *ebiten.Image, x, y int, blockType types.BlockTy
borderColor, false)
}
// drawHardDropBlock draws a block with special hard drop effect
func (g *Game) drawHardDropBlock(screen *ebiten.Image, x, y int, blockType types.BlockType) {
// Get original color and make it brighter for hard drop effect
originalColor := types.BlockColors[blockType].(color.RGBA)
hardDropColor := color.RGBA{
min(255, originalColor.R+50),
min(255, originalColor.G+50),
min(255, originalColor.B+50),
255,
}
vector.DrawFilledRect(screen,
float32(x*config.BlockSize),
float32(y*config.BlockSize),
float32(config.BlockSize-1),
float32(config.BlockSize-1),
hardDropColor, false)
// Add bright white border for hard drop effect
borderColor := color.RGBA{255, 255, 255, 200}
vector.DrawFilledRect(screen,
float32(x*config.BlockSize),
float32(y*config.BlockSize),
float32(config.BlockSize-1),
3,
borderColor, false)
vector.DrawFilledRect(screen,
float32(x*config.BlockSize),
float32(y*config.BlockSize),
3,
float32(config.BlockSize-1),
borderColor, false)
}
// max returns the maximum of two integers
func max(a, b int) int {
if a > b {
@ -462,6 +534,14 @@ func max(a, b int) int {
return b
}
// min returns the minimum of two uint8 values
func min(a, b uint8) uint8 {
if a < b {
return a
}
return b
}
// drawControls renders the control instructions on the right side of the screen
func (g *Game) drawControls(screen *ebiten.Image) {
// Starting position for controls display
@ -480,6 +560,7 @@ func (g *Game) drawControls(screen *ebiten.Image) {
text.Rotate,
text.SoftDrop,
text.HardDrop,
text.PressE,
"",
text.Language,
text.PressL,

@ -22,6 +22,7 @@ type Localization struct {
Restart string
Language string
PressL string
PressE string
}
// currentLanguage holds the current language setting
@ -42,6 +43,7 @@ var localizations = map[Language]Localization{
Restart: "Press R to restart",
Language: "Language:",
PressL: "Press L to switch",
PressE: "Press Esc to exit",
},
Chinese: {
Next: "下一个:",
@ -56,6 +58,7 @@ var localizations = map[Language]Localization{
Restart: "按 R 重新开始",
Language: "语言:",
PressL: "按 L 切换",
PressE: "按 Esc 退出",
},
}

Loading…
Cancel
Save