# 纸牌上的坦克大战

基于 C++17 + SDL2 的 2D 坦克对战游戏，支持三种难度、实时战斗与胜负判定。

## 功能特性

- **3 档难度**：简单（HP 5 / ATK 1）、中等（HP 10 / ATK 2）、困难（HP 15 / ATK 3）
- **实时坦克对战**：WASD 移动 + 空格开火，敌人 AI 随机移动与射击
- **战场交互**：撞子弹扣血、踩残骸回血 + 提升攻击力、墙壁阻挡
- **胜负判定**：消灭全部 10 辆敌方坦克获胜，HP 归零失败
- **背景音乐**：循环 BGM 播放（静音自动降级）
- **跨平台**：Linux / Windows / macOS，CMake 一键构建

---

## 开发环境

| 依赖 | 版本要求 |
|------|----------|
| C++ | 17+ |
| CMake | 3.10+ |
| SDL2 | 2.x |
| SDL2_image | 2.x |
| SDL2_mixer | 2.x |
| SDL2_ttf | 2.x |
| 中文字体 | wqy-microhei 或 noto-cjk（用于界面文本渲染） |

### Linux

```bash
# Ubuntu / Debian
sudo apt install build-essential cmake \
    libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \
    fonts-wqy-microhei

# Fedora
sudo dnf install gcc-c++ cmake \
    SDL2-devel SDL2_image-devel SDL2_mixer-devel SDL2_ttf-devel \
    wqy-microhei-fonts

# 构建
cmake -B build
cmake --build build
./build/tank_battles_on_the_scrap_paper
```

### macOS

```bash
brew install cmake sdl2 sdl2_image sdl2_mixer sdl2_ttf
cmake -B build
cmake --build build
./build/tank_battles_on_the_scrap_paper
```

### Windows

```powershell
# 1. 安装 CMake（https://cmake.org/download）
# 2. 安装 vcpkg 并安装 SDL2 依赖：
vcpkg install sdl2 sdl2-image sdl2-mixer sdl2-ttf

# 构建
cmake -B build -DCMAKE_TOOLCHAIN_FILE=<vcpkg-root>/scripts/buildsystems/vcpkg.cmake
cmake --build build
.\build\Release\tank_battles_on_the_scrap_paper.exe
```

---

## 操作指南

| 按键 | 功能 |
|------|------|
| W / ↑ | 向上移动 |
| S / ↓ | 向下移动 |
| A / ← | 向左移动 |
| D / → | 向右移动 |
| 空格 | 发射子弹 |
| ESC | 返回主菜单 |

鼠标左键点击按钮进行菜单导航。

---

## 玩法说明

1. 主菜单选择**开始游戏**（使用当前难度设置）或**难度设置**
2. 操控红色坦克在 20×20 地图中移动、射击敌方蓝色坦克
3. 踩到敌方坦克残骸（`is_taken`）可 +1 HP（上限 5）并 +1 攻击力
4. 撞到敌方子弹扣 1 HP
5. 击败全部 10 辆敌方坦克即**胜利**；玩家 HP 归零则**失败**
6. 子弹射到墙壁消失；敌方子弹射到玩家扣 HP 并消失

---

## 项目结构

```
tank_battles_on_the_scrap_paper/
├── CMakeLists.txt                  # CMake 构建配置
├── README.md
├── tank_battles_on_the_scrap_paper/
│   ├── main.cpp                    # 入口：创建 Game 实例，调用 run()
│   ├── data_config.h               # 全局常量（网格大小、数量上限、枚举）
│   ├── images/                     # 58 张 PNG/JPG 素材
│   ├── music/                      # 背景音乐 bgm.wav
│   └── src/
│       ├── Game.h / Game.cpp       # 状态机主循环 + 游戏逻辑
│       ├── AssetManager.h / .cpp   # 纹理 / 字体 / BGM 加载与缓存
│       ├── Renderer.h / .cpp       # SDL 渲染封装
│       ├── Map.h / .cpp            # 地图数据 + 碰撞检测
│       ├── Tank.h / .cpp           # 坦克实体
│       ├── Bullet.h / .cpp         # 子弹实体
│       └── UI.h                    # 界面按钮区域常量
└── .gitignore
```

`images/` 与 `music/` 在 CMake 构建时自动拷贝到 `build/` 目录，游戏通过 `SDL_GetBasePath()` 自动定位可执行文件所在目录加载资源。

---

## 核心模块

### `Game` — 状态机主循环

| 职责 | 说明 |
|------|------|
| 场景管理 | `enum class Scene { MENU, LEVEL, GAME, WIN, LOSE, QUIT }` 驱动主循环，彻底消除旧版递归函数跳转导致的栈溢出隐患 |
| 事件分发 | `handleEvents()` 统一处理 SDL 事件（退出 / ESC / 鼠标点击），根据当前场景路由到不同按钮命中检测 |
| 更新循环 | `updateGame()` 每帧执行：玩家输入 → 玩家移动 / 射击 → 敌人 AI 移动 / 射击 → 子弹移动与碰撞 → 地图同步 |
| 渲染调度 | `renderScene()` 根据场景调用对应渲染函数 |

**场景跳转流程**：

```
MENU ──开始游戏──→ GAME ──胜利──→ WIN ──返回──→ MENU
  │                  │                    │
  ├──难度设置──→ LEVEL │                    │
  │    │               └──失败──→ LOSE ──返回──┘
  │    └──返回──→ MENU
  └──退出游戏──→ 退出
```

### `Map` — 地图与碰撞

| 方法 | 职责 |
|------|------|
| `reset()` | 初始化 20×20 网格：四周为 `WALL`，内部为 `BLANK` |
| `get(x, y)` | 返回指定坐标的 `CELL_Type` 枚举值 |
| `set(x, y, type)` | 写入单元，每帧同步坦克位置 |
| `peek(x, y, dir)` | 查看某方向前方一格的内容，供坦克和子弹碰撞检测 |

### `Tank` — 坦克实体

| 属性 | 说明 |
|------|------|
| `x, y` | 网格坐标 |
| `hp` | 生命值（玩家默认 5，敌人由难度决定） |
| `attack` | 攻击力（决定子弹伤害） |
| `dir` | 方向（`UP/DOWN/LEFT/RIGHT`） |
| `isTaken` | 仅敌方：残骸是否已被玩家拾取 |

方向向量 `dx(dir)` / `dy(dir)` 统一处理四方向位移，消除旧版中 4 份复制粘贴的 switch 逻辑。

### `Bullet` — 子弹实体

| 方法 | 职责 |
|------|------|
| `fire(x, y, dir, dmg)` | 初始化子弹（位置、方向、伤害值） |
| `peekAt(map)` | 检测前方一格的地图单元类型 |
| `step()` | 向前移动一格 |
| `isLive` | 是否存活（碰墙/命中后置 `false`） |

玩家子弹命中 `BLUE_TANK` 时遍历敌人数组扣血；敌方子弹命中 `RED_TANK` 时直接扣玩家 HP。子弹碰 `WALL` 立即消失。

### `AssetManager` — 资源管理

- **纹理缓存**：`std::unordered_map<string, SDL_Texture*>` 按路径缓存，避免重复加载
- **字体加载**：`TTF_OpenFont()` 带多路径回退（wqy-microhei → wqy-zenhei → noto-cjk）
- **BGM 管理**：`Mix_LoadMUS()` + `Mix_PlayMusic(-1)` 循环播放，失败静默降级
- **生命周期**：析构时自动释放全部纹理、字体、音频资源

### `Renderer` — 渲染封装

| 方法 | 职责 |
|------|------|
| `clear()` | 填充黑色背景 |
| `present()` | 提交帧缓冲区到屏幕 |
| `drawTexture()` | 渲染纹理到指定矩形区域 |
| `drawImage()` | 加载图片并渲染（通过 AssetManager 缓存） |
| `drawText()` | TTF 文字渲染（UTF-8 编码，`SDL_RenderCopy` 绘制） |
| `drawInt()` | 整数值渲染（`std::to_string` 后调用 `drawText`） |

### `UI` — 界面常量

| 常量 | 用途 |
|------|------|
| `BTN_START` | 开始游戏按钮区域 {50, 250, 150, 50} |
| `BTN_LEVEL` | 难度设置按钮区域 {50, 330, 150, 50} |
| `BTN_QUIT` | 退出游戏按钮区域 {50, 410, 150, 50} |
| `BTN_EASY/MEDIUM/HARD` | 难度选择按钮区域 |
| `BTN_BACK` | 返回按钮区域 {30, 550, 150, 50} |
| `hit(rect, mx, my)` | 鼠标命中检测辅助函数 |

---

## 计时机制

游戏使用 `SDL_GetTicks()` 替代跨平台不一致的 `clock()`，各逻辑模块以毫秒为间隔独立刷新：

| 模块 | 间隔 | 说明 |
|------|------|------|
| 玩家移动 | 100 ms | 按住方向键连续移动 |
| 玩家射击 | 500 ms | 按住空格连续发射 |
| 敌人移动 | 1000 ms | 随机方向移动 |
| 敌人射击 | 3000 ms | 每个存活敌人发射一发 |
| 玩家子弹 | 100 ms | 逐格前进 |
| 敌方子弹 | 500 ms | 逐格前进 |

---

## 版本日志

| 版本 | 变更 |
|------|------|
| v19 | 全面重构：1468 行单文件拆分为 8 个模块，状态机替代递归，全局变量清零，方向向量消除 4x 复制粘贴，修复 EnemiesMove 循环变量遮蔽 bug |
| v18 | 从 Windows/EasyX 迁移到 SDL2，支持跨平台；修复启动黑屏（`SDL_GetBasePath` 自动定位资源目录） |
| v17 | 修复返回菜单后 `InitMap()` 未调用导致地图脏数据 |
| v16 | 添加全局 BGM 背景音乐循环播放 |
