“结构决定行为” —— 好的项目结构让开发更高效
game_2048/
├── .git/ # Git 版本控制
├── .venv/ # 虚拟环境(自动生成)
├── docs/ # 文档目录
│ ├── TUTORIAL.md # 教程总目录
│ ├── ARCHITECTURE.md # 架构文档
│ ├── 01_overview.md # 第 1 章
│ ├── 02_structure.md # 第 2 章(本章)
│ └── ...
├── game_2048/ # 主包目录
│ ├── __init__.py # 包标识
│ ├── app.py # 应用入口
│ ├── config.py # 配置常量
│ ├── game.py # 游戏逻辑
│ ├── models.py # 数据模型
│ ├── ui.py # 界面组件
│ └── utils.py # 工具函数
├── .gitignore # Git 忽略规则
├── .python-version # Python 版本
├── main.py # 备用入口
├── pyproject.toml # 项目配置
├── README.md # 项目说明
└── uv.lock # 依赖锁定
| 文件/目录 | 作用 | 是否手写 |
|---|---|---|
game_2048/ |
主代码包 | ✅ |
pyproject.toml |
项目配置 | ✅ |
README.md |
项目说明 | ✅ |
.gitignore |
Git 忽略 | ✅ |
.venv/ |
虚拟环境 | ❌ 自动生成 |
uv.lock |
依赖锁定 | ❌ uv 生成 |
想象一下,如果 500 行代码都在 game.py 里:
# ❌ 单文件地狱:game.py (500 行)
# 配置常量
GRID_SIZE = 4
...
# 数据类
class Tile:
...
class Grid:
...
# 游戏逻辑
class Game:
...
# UI 组件
class TileWidget:
...
class GridWidget:
...
# 应用入口
class GameApp:
...
# 工具函数
def print_grid():
...
问题:
✅ 模块化后:
config.py → 配置常量(30 行)
models.py → 数据模型(120 行)
game.py → 游戏逻辑(200 行)
ui.py → 界面组件(180 行)
app.py → 应用入口(100 行)
utils.py → 工具函数(80 行)
好处:
__init__.py - 包标识"""Game 2048 - A terminal-based 2048 game built with Textual."""
__version__ = "0.1.0"
作用:
为什么需要它?
# 没有 __init__.py
python -c "import game_2048"
# → ModuleNotFoundError
# 有 __init__.py
python -c "import game_2048"
# → 成功导入
config.py - 配置中心"""Game configuration and constants."""
# 网格大小
GRID_SIZE: int = 4
# 初始方块数
INITIAL_TILES: int = 2
# 新方块概率
NEW_TILE_PROBABILITIES: dict[int, float] = {
2: 0.9, # 90% 生成 2
4: 0.1, # 10% 生成 4
}
# 方向常量
DIRECTION_UP: str = "up"
DIRECTION_DOWN: str = "down"
DIRECTION_LEFT: str = "left"
DIRECTION_RIGHT: str = "right"
设计思想:
使用示例:
# game.py
from .config import GRID_SIZE
for i in range(GRID_SIZE): # 清晰!
...
# 而不是
for i in range(4): # 4 是什么?
...
models.py - 数据模型"""Data models for the 2048 game."""
from dataclasses import dataclass
@dataclass
class Tile:
"""一个方块。"""
value: int
row: int
col: int
merged: bool = False
new: bool = False
class Grid:
"""4x4 游戏棋盘。"""
def __init__(self):
self.cells: list[list[int]] = [...]
def __getitem__(self, position) -> int:
...
def spawn_tile(self) -> Optional[tuple[int, int]]:
...
核心职责:
为什么单独成模块?
game.py - 游戏逻辑"""Core game logic for 2048."""
from enum import Enum
class GameState(Enum):
PLAYING = auto()
WON = auto()
GAME_OVER = auto()
class Game:
"""游戏控制器。"""
def __init__(self):
self.grid = Grid()
self.score = 0
self.state = GameState.PLAYING
def move(self, direction: str) -> bool:
"""移动方块。"""
...
def _slide_and_merge(self, line, ascending):
"""核心算法:滑动合并。"""
...
核心职责:
关键特点:
ui.py - 界面组件"""Textual TUI components for the 2048 game."""
from textual.app import ComposeResult
from textual.widgets import Static, Label
class TileWidget(Static):
"""单个方块组件。"""
DEFAULT_CSS = """
TileWidget {
background: $surface;
border: solid $primary;
}
"""
def render(self) -> str:
return str(self.value)
class GridWidget(Static):
"""4x4 棋盘组件。"""
def compose(self) -> ComposeResult:
for row in range(GRID_SIZE):
yield Horizontal(...)
核心职责:
设计原则:
app.py - 应用入口"""Main application entry point."""
from textual.app import App
from textual.binding import Binding
class Game2048App(App):
"""主应用。"""
TITLE = "2048"
BINDINGS = [
Binding("up", "move_up", "Up"),
Binding("r", "restart", "Restart"),
Binding("q", "quit", "Quit"),
]
def action_move_up(self):
self.game.move("up")
self.refresh_display()
def run_game():
app = Game2048App()
app.run()
核心职责:
为什么单独成模块?
utils.py - 工具函数"""Utility functions for the 2048 game."""
def get_tile_style(value: int) -> dict[str, str]:
"""获取方块的样式信息。"""
styles = {
2: {"bg": "gray60", "fg": "gray10"},
4: {"bg": "gray55", "fg": "gray10"},
...
}
return styles.get(value, styles[2])
def format_number(value: int) -> str:
"""格式化大数字。"""
if value >= 1000:
return f"{value / 1000:.1f}k"
return str(value)
核心职责:
设计原则:
[project]
name = "game-2048"
version = "0.1.0"
description = "🎮 A terminal-based 2048 game built with Textual"
requires-python = ">=3.14"
dependencies = [
"rich>=14.3.3",
"textual>=8.0.2",
]
[project.scripts]
game-2048 = "game_2048.app:run_game"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
| 字段 | 含义 | 示例值 |
|---|---|---|
name |
项目名称 | "game-2048" |
version |
版本号(语义化版本) | "0.1.0" |
description |
项目描述 | "A terminal game..." |
requires-python |
Python 版本要求 | ">=3.14" |
dependencies |
运行时依赖 | ["textual>=8.0.2"] |
project.scripts |
命令行入口 | "game-2048 = ..." |
[project.scripts]
game-2048 = "game_2048.app:run_game"
含义:
game-2048game_2048.app 模块中的 run_game 函数等价于:
# 运行 game-2048 命令时
from game_2048.app import run_game
run_game()
app.py
│
↓
ui.py
│
↓
game.py
│
↓
models.py
│
┌────────────┴────────────┐
↓ ↓
config.py utils.py
# ✅ 正确:上层依赖下层
# app.py
from .game import Game
from .ui import GameScreen
# game.py
from .models import Grid
# models.py
from .config import GRID_SIZE
# ❌ 错误:下层依赖上层
# models.py 不应该导入 Game
# config.py 不应该导入任何模块
# ❌ 错误:循环依赖
# game.py
from .ui import GameScreen # Game 依赖 UI
# ui.py
from .game import Game # UI 依赖 Game
# 结果:导入错误!
# ✅ 正确:单向依赖
# game.py - 不导入 UI
# ui.py - 导入 Game
cd ~/Work/open_learn/python/game_2048
# 查看目录结构
tree -L 2 -I '.venv|__pycache__'
# 统计每个模块的代码行数
wc -l game_2048/*.py
# 编辑 config.py
nano game_2048/config.py
# 修改:
GRID_SIZE = 5 # 改成 5x5
NEW_TILE_PROBABILITIES = {2: 0.7, 4: 0.3} # 增加 4 的概率
# 运行游戏观察变化
uv run game-2048
# 在 game.py 的 move() 方法中添加
print(f"Moving {direction}, score: {self.score}")
# 运行游戏,观察输出
uv run game-2048
# 创建动画模块
touch game_2048/animations.py
# 添加基础内容
cat > game_2048/animations.py << 'EOF'
"""Animation utilities for 2048."""
def animate_slide():
"""TODO: 实现滑动动画。"""
pass
EOF
__init__.py 可以是空的吗?A: 可以!空文件也能让 Python 识别为包。但建议添加文档字符串和版本号。
A: 对于小项目可能显得多,但这是专业项目的标准结构。随着项目变大,优势会体现。
A: 问自己:
models.pygame.pyui.pyutils.py下一章: 第 3 章 数据模型设计
🐧 好的结构是成功的一半!