4_2-4_3c++练手小项目

5 阅读24分钟
# C++ 控制台魔塔 — 项目说明文档

> 本文档面向:**代码讲解、课程答辩、技术博客** 可直接节选使用。  
> 项目路径示例:`4_2`;主程序为控制台字符界面,无第三方图形库。

---

## 一、项目简介

本项目是一个运行在 **Windows 控制台** 下的 **魔塔类 RPG**:玩家在多层地图中移动(WASD),与怪物战斗、收集钥匙开门、拾取药水与装备、在商店消费金币,并通过楼梯在楼层间穿梭。地图与逻辑用 **C++** 实现,通过 **光标定位 + 重复绘制** 模拟「刷新画面」。

**核心特点:**

- 多楼层地图(默认 5 层),数据与规则分离。
- 面向对象的怪物体系(基类 + 多种子类,`onDeath` 掉落金币/经验)。
- 钥匙 /  / 道具 / 商店 / NPC(占位对话)等完整交互骨架。
- UTF-8 中文图例与状态栏,针对控制台**中英文字符显示宽度**做了对齐处理。

---

## 二、技术栈与环境

| 项目 | 说明 |
|------|------|
| 语言 | C++(建议 C++11 及以上) |
| 平台 | **Windows**(使用 `windows.h`、`conio.h`) |
| 构建 | 本地 `g++` / Visual Studio 等,需**多文件一起编译链接** |
| 依赖 | 无第三方库 |

---

## 三、仓库结构(主工程)

4_2/ ├── main.cpp # 程序入口:初始化控制台、创建 Game、run ├── config.h # 地图尺寸、楼层数、Tile 枚举(格子类型编号) ├── Game.h / Game.cpp # 游戏主循环、绘制、输入、战斗、道具、门、商店、楼梯逻辑 ├── Map.h / Map.cpp # 多层地图数据 defaultMap、getTile / setTile 等 ├── Player.h / Player.cpp # 勇者属性、位置、楼层、钥匙、金币、经验 ├── Moster.h / Moster.cpp # 怪物基类与各子类(文件名拼写为 Moster) ├── Utils.h / Utils.cpp # 控制台光标、延时、清屏等 ├── README.md # 本说明文档 ├── 4_2_1.cpp # 可选:早期/实验代码,与主工程独立 └── .vscode/ # VS Code 任务、工作区(可选)

---

## 四、编译与运行

**必须**将下列源文件一并编译并链接(只编译 `main.cpp` 会报链接错误):

`main.cpp``Game.cpp``Map.cpp``Player.cpp``Utils.cpp``Moster.cpp`

**示例(MinGW / g++):**

```bash
g++ -Wall -Wextra -g3 main.cpp Game.cpp Map.cpp Player.cpp Utils.cpp Moster.cpp -o game_build.exe

在工程目录下运行:

.\game_build.exe

若使用 VS Code 任务将可执行文件输出到子目录(如 output/main.exe),请在同一相对路径下运行,否则会出现「找不到 exe」的情况。


五、整体架构(怎么串起来)

  1. main:调用 Utils::hideCursor(),构造 Game,执行 game.run()
  2. Game::run():死循环 —— draw()input()update()
  3. draw():光标回到 (0,0),打印状态栏与当前楼层地图;地图上每个格子根据 Map::getTile 的编号显示一个字符;下方输出图例与操作说明。
  4. input()_getch() 读取一个按键,写入成员变量 lastKey
  5. update():根据 lastKey(WASD)计算目标坐标,读取目标格类型,依次判断:墙、怪物、道具、门、NPC、商店、楼梯、普通道路等,并调用 fightpickItemtryOpenDoorrunShop 或换层。

一句话: Map 存「世界长什么样」,Player 存「人状态如何」,Game 存「规则与流程」,怪物行为通过 Monster 多态扩展。


六、核心模块说明

1. config.h

  • MAP_WMAP_H:地图宽高(默认 13×13,外圈多为墙)。
  • MAX_FLOORS:最大楼层数(默认 5)。
  • enum Tile:将墙、路、各类怪物、钥匙、门、楼梯、商店格等映射为 整数,与 Map.cpp 中数组数据一致。

2. Map

  • 成员 tileMap[MAX_FLOORS][MAP_H][MAP_W]:每层每个格子的图块编号。
  • 另有 monsterMap(怪物指针层),便于扩展;当前逻辑大量依赖 tile 类型与 fight 内临时构造的怪物对象。
  • loadDefaultMap():将静态表 defaultMap 拷贝进 tileMap,即关卡设计入口

3. Player

  • 坐标 (x, y)、当前楼层索引 currentFloor
  • 属性:生命、攻击、防御、金币、经验;棕 / 银 / 红钥匙数量。
  • setPossetFloor:楼梯传送与换层落点均通过它们完成。

4. MonsterMoster.*

  • 抽象基类:virtual void onDeath(Player&) = 0,子类实现掉落;virtual 析构保证多态删除安全。
  • 子类示例:绿/红/黑史莱姆、蝙蝠、骷髅、未知怪等,数值与掉落各不相同。

5. Utils

  • gotoxySetConsoleCursorPosition,用于从屏幕左上角重绘。
  • hideCursor:隐藏控制台光标,避免闪烁干扰。
  • sleep:封装 Sleep(毫秒)。

6. Game

  • fight:按攻防差计算每回合伤害与回合数,预判总受伤;打不过则不入战;战胜后清怪格、onDeath、加经验等。
  • pickItem:各类药水、钥匙、宝石、剑等效果,switch 分支维护,拾取后该格改为道路。
  • tryOpenDoor:门类型与钥匙类型对应,消耗钥匙并开门。
  • runShop:站在商店三格之一进入子循环,数字键购买或离开。
  • update 楼梯分支:根据当前楼层 + 目标格坐标决定上楼/下楼及落点(项目迭代中已按关卡设计调整,例如第四层左下角上第五层、第五层左下角回第四层等)。

七、游戏机制摘要

移动与碰撞

  • W / A / S / D(大小写均可)有效;目标格为墙则不移动。
  • 勇者显示为「勇」,覆盖当前格子的地图显示。

战斗(简化回合)

  • 若对怪物每回合伤害 ≤ 0,提示「打不动」。
  • 否则用血量与每回合伤害推算回合数,再估算勇者受到的总伤害(多回合时按怪物每回合反击累计);若预计会阵亡,则不进入战斗并提示。
  • 胜利:扣血、移除怪物图块、执行 onDeath、增加经验。

道具与装备(以代码为准)

  • 红/蓝宝石、红/蓝药、宝剑等均在 pickItem 中有对应效果。
  • 未知钥匙等可能按棕钥匙数量处理,并有人机可读提示。

商店

  • 走进「店 / 商 / 铺」任一格进入商店菜单;商品价格与属性加成见 runShop 源码中的常量与选项。

图例(draw 中)

  • 图例分为多列(怪物 / 钥匙与门 / 药水与宝石与道具 / NPC 等),使用 UTF-8 显示宽度计算做列对齐,便于后期增删条目。

八、设计上的取舍

  1. 控制台绘图:不引入图形库,依赖 gotoxy 整屏刷新,实现简单、依赖少,但可移植性限于 Windows 控制台 API
  2. 怪物数据:地图上用 Tile 区分种类,战斗时创建栈上怪物对象并传入 fight;若需「每只怪独立血量」,可改为 monsterMap 指向堆对象并序列化状态。
  3. 楼梯规则:集中在 Game::updateSTAIRS 分支,楼层多时代码量会变大;后续可抽成表驱动(楼层、入口坐标、目标楼层、落点)。

九、扩展与维护建议

  • 新怪物:在 config.h 增加 Tile 枚举 → Map 摆怪 → draw 里增加显示字符 → update 里加入 fight 分支与 Moster 子类。
  • 新楼层:增大 MAX_FLOORS(若需要)→ Map::defaultMap 增加一层 → STAIRS 分支增加连通关系。
  • 通关:可在特定楼梯或事件里设置 isRunning = false 或进入结局画面(当前以项目内实际代码为准)。

十、版权与声明

本项目为学习/课程用途;魔塔玩法常见于经典游戏,实现与数值均为独立编写或自行设计,引用本文档时请根据实际课程要求标注作者与日期。


文档生成自项目源码结构,修改游戏逻辑后请同步更新「编译命令」「楼层与楼梯」等章节。

config.h

//全局配置文件,存放数据啥的
#ifndef CONFIG_H
#define CONFIG_H

#include <iostream>
#include <vector>
using namespace std;

//地图大小:13×13,最外一圈为墙
const int MAP_W      = 13;
const int MAP_H      = 13;
const int MAX_FLOORS = 5;

//地图元素
enum Tile {
    ROAD            = 0,  //路
    WALL            = 1,  //墙
    HERO            = 2,  //英雄
    SLIME_GREEN     = 3,  // 绿色史莱姆
    BAT             = 4,  //蝙蝠
    KEY_BROWN       = 5,  //棕钥匙
    DOOR_BROWN      = 6,  //棕门
    STAIRS          = 7,  //楼梯
    POTION_GREEN    = 8,  //绿色药水

    SLIME_RED       = 9,  //红色史莱姆
    KEY_SILVER      = 10, //银钥匙
    KEY_RED         = 11, //红色钥匙
    DOOR_SILVER     = 12, //银门
    DOOR_RED        = 13, //红色门
    MONSTER_UNKNOWN = 14, //未知怪物
    ITEM_UNKNOWN    = 15, //未知物品
    GEM_RED         = 16, //红色宝石
    GEM_BLUE        = 17, //蓝色宝石
    POTION_RED      = 18, //红色药水

    POTION_BLUE     = 19, //蓝色药水
    DOOR_GREEN      = 20, //绿色门

    NPC_SAGE        = 21, //非战斗人物:智者
    NPC_CHILD       = 22, //童
    NPC_ELDER       = 23, //民/长者

    SWORD_ATK       = 24, //加攻击的宝剑

    SHOP_LEFT       = 25, //1×3商店左格
    SHOP_MID        = 26, //商店中格
    SHOP_RIGHT      = 27, //商店右格

    SKELETON        = 28, //骷髅兵
    DOOR_UNKNOWN    = 29, //未知门
    KEY_UNKNOWN     = 30, //未知钥匙
    SLIME_BLACK     = 31  //黑色史莱姆
};

#endif

Map.h

#ifndef GAME_MAP_HEADER_H
#define GAME_MAP_HEADER_H

#include "config.h"
#include "Moster.h"

class Map {
private:
    //地图数据
    int tileMap[MAX_FLOORS][MAP_H][MAP_W];

    //怪物层
    Monster* monsterMap[MAX_FLOORS][MAP_H][MAP_W];

public:
    //构造
    Map();

    //加载默认地图
    void loadDefaultMap();

    //获取格子
    int getTile(int floor, int x, int y) const;

    //设置格子
    void setTile(int floor, int x, int y, int tile);

    //怪物操作
    Monster* getMonster(int floor, int x, int y) const;
    void setMonster(int floor, int x, int y, Monster* monster);

    //清空怪物
    void clearMonster(int floor, int x, int y);
};

#endif // GAME_MAP_HEADER_H

Map.cpp

//地图
#include "Map.h"

//构造
//初始化地图数据
Map::Map() {
    for (int floor = 0; floor < MAX_FLOORS; floor++) {
        for (int y = 0; y < MAP_H; y++) {
            for (int x = 0; x < MAP_W; x++) {
                tileMap[floor][y][x] = ROAD;
                monsterMap[floor][y][x] = nullptr;
            }
        }
    }
    loadDefaultMap();
}

//获取格子
int Map::getTile(int floor, int x, int y) const {
    return tileMap[floor][y][x];
}

//设置格子
void Map::setTile(int floor, int x, int y, int tile) {
    tileMap[floor][y][x] = tile;
}

//获取怪物
Monster* Map::getMonster(int floor, int x, int y) const {
    return monsterMap[floor][y][x];
}

//设置怪物
void Map::setMonster(int floor, int x, int y, Monster* monster) {
    monsterMap[floor][y][x] = monster;
}

//清空怪物
void Map::clearMonster(int floor, int x, int y) {
    monsterMap[floor][y][x] = nullptr;
}

void Map::loadDefaultMap() {
    static const int defaultMap[MAX_FLOORS][MAP_H][MAP_W] = {
        //第1层
        {
            {1,1,1,1,1,1,1,1,1,1,1,1,1},
            {1,7,0,5,3,9,3,0,0,0,0,0,1},
            {1,1,1,1,1,1,1,1,1,1,1,0,1},
            {1,18,0,28,6,0,1,18,5,18,1,0,1},
            {1,5,28,16,1,0,1,18,5,18,1,0,1},
            {1,1,6,1,1,0,1,1,1,14,1,0,1},
            {1,5,28,0,1,0,6,14,3,4,1,0,1},
            {1,17,0,10,1,0,1,1,1,1,1,0,1},
            {1,1,6,1,1,0,0,0,0,0,0,0,1},
            {1,0,28,0,1,1,13,1,1,1,6,1,1},
            {1,18,19,5,1,11,0,0,1,5,14,10,1},
            {1,18,15,5,1,0,7,0,1,5,5,5,1},
            {1,1,1,1,1,1,1,1,1,1,1,1,1}
        },
        //第2层
        {
            {1,1,1,1,1,1,1,1,1,1,1,1,1},
            {1,7,1,0,14,0,1,16,17,5,11,1,1},
            {1,0,1,17,1,19,1,16,17,5,10,1,1},
            {1,0,1,5,1,5,1,16,17,5,14,1,1},
            {1,0,1,5,1,5,1,1,1,1,6,1,1},
            {1,0,1,0,1,0,0,0,6,0,0,1,1},
            {1,0,1,6,1,1,6,1,1,6,1,1,1},
            {1,0,20,0,0,0,0,1,0,14,0,1,1},
            {1,0,1,6,1,1,12,1,14,1,20,1,1},
            {1,0,1,5,1,19,18,1,0,1,0,1,1},
            {1,0,1,5,1,19,18,1,0,1,0,1,1},
            {1,7,1,16,1,19,18,1,21,1,22,1,1},
            {1,1,1,1,1,1,1,1,1,1,1,1,1}
        },
        //第3层
        {
            {1,1,1,1,1,1,1,1,1,1,1,1,1},
            {1,24,9,5,1,25,26,27,1,1,1,1,1},
            {1,9,5,0,1,0,0,0,1,0,4,0,1},
            {1,5,28,0,1,1,6,1,1,0,1,0,1},
            {1,1,6,1,1,0,28,0,1,5,1,9,1},
            {1,0,0,0,1,1,1,0,1,5,1,4,1},
            {1,3,1,0,4,9,4,0,1,5,1,9,1},
            {1,3,1,1,1,1,1,0,0,0,1,0,1},
            {1,0,0,0,0,0,1,1,6,1,1,0,1},
            {1,1,1,1,1,4,1,9,0,9,1,0,1},
            {1,1,0,0,0,0,1,17,4,5,1,0,1},
            {1,7,0,1,1,1,1,16,19,5,1,7,1},
            {1,1,1,1,1,1,1,1,1,1,1,1,1}
        },
        //第4层
        {
            {1,1,1,1,1,1,1,1,1,1,1,1,1},
            {1,0,31,0,1,0,21,0,1,0,31,0,1},
            {1,6,1,6,1,0,0,0,1,6,1,6,1},
            {1,0,1,0,1,1,29,1,1,0,1,0,1},
            {1,0,1,28,1,4,14,4,1,28,1,0,1},
            {1,4,1,18,1,17,4,17,1,18,1,4,1},
            {1,4,1,18,1,1,13,1,1,18,1,4,1},
            {1,9,1,0,1,14,14,14,1,0,1,9,1},
            {1,0,1,0,1,16,14,16,1,0,1,0,1},
            {1,0,1,0,1,1,12,1,1,0,1,0,1},
            {1,0,1,0,1,5,0,5,1,0,1,0,1},
            {1,7,1,0,31,0,0,0,31,0,1,7,1},
            {1,1,1,1,1,1,1,1,1,1,1,1,1}
        },
        //第5层
        {
            {1,1,1,1,1,1,1,1,1,1,1,1,1},
            {1,30,1,18,1,19,14,0,0,14,5,10,1},
            {1,0,1,16,1,14,0,0,0,0,14,5,1},
            {1,4,1,0,1,14,0,1,1,6,1,1,1},
            {1,0,6,14,1,15,14,1,0,14,14,21,1},
            {1,4,1,0,1,1,1,1,1,0,1,14,1},
            {1,16,1,0,0,0,4,28,0,0,0,0,1},
            {1,17,1,1,14,1,1,1,1,0,1,0,1},
            {1,0,21,1,14,1,0,0,0,14,14,0,1},
            {1,1,1,1,4,1,6,1,12,1,6,1,1},
            {1,0,0,1,0,1,4,1,17,6,0,1,1},
            {1,7,0,4,0,0,0,1,5,1,7,1,1},
            {1,1,1,1,1,1,1,1,1,1,1,1,1}
        }
    };
    for (int f = 0; f < MAX_FLOORS; f++) {
        for (int y = 0; y < MAP_H; y++) {
            for (int x = 0; x < MAP_W; x++) {
                tileMap[f][y][x] = defaultMap[f][y][x];
            }
        }
    }
}

Player.h

#ifndef PLAYER_H
#define PLAYER_H

#include "config.h"

//勇者的所有的信息
class Player{
    private:
        int x, y;           //玩家坐标
        int currentFloor;   //玩家当前所在楼层
        int hp, maxHp;      //玩家属性
        int atk, def;       //攻击力和防御力
        int gold;           //玩家持有的金币数量
        int exp;            // 经验值

        //三把钥匙
        int key_brown;      // 棕钥匙
        int key_silver;     // 银钥匙
        int key_red;        // 红钥匙
    
    public:
        //构造函数
        Player();

        //玩家移动函数
        void move(int dx, int dy);
        //设置玩家位置
        void setPos(int x, int y);
        void setFloor(int f);

        //获取当前玩家位置和楼层
        int getX() const {return x;}
        int getY() const {return y;}
        int getCurrentFloor() const {return currentFloor;}

        //玩家血量
        int getHp() const {return hp;}
        void addHp(int val);
        void setHp(int val);
        void takeDamage(int val);
        bool isDead() const;

        //防御相关
        int getAtk() const {return atk;}
        int getDef() const {return def;}
        void addAtk(int val);
        void addDef(int val);

        //三种钥匙
        int getKeyBrown() const {return key_brown;}
        int getKeySilver() const {return key_silver;}
        int getKeyRed() const {return key_red;}

        void addKeyBrown(int val) {key_brown += val;}
        void addKeySilver(int val) {key_silver += val;}
        void addKeyRed(int val) {key_red += val;}

        bool useKeyBrown();
        bool useKeySilver();
        bool useKeyRed();

        //金币相关
        int getGold() const {return gold;}
        void addGold(int val) {gold += val;}
        void spendGold(int val) {gold -= val;}

        int getExp() const { return exp; }
        void addExp(int val) { exp += val; }
};

#endif

Player.cpp

//勇者
#include "Player.h"

Player::Player()
    : x(6), y(10), currentFloor(0), hp(1000), maxHp(1000), 
    atk(10), def(10), gold(0), exp(0),
    key_brown(0), key_silver(1), key_red(1) {}

//勇者移动
void Player::move(int dx, int dy) {
    x += dx;
    y += dy;
}

//设置位置
void Player::setPos(int nx, int ny) {
    x = nx;
    y = ny;
}

//设置楼层
void Player::setFloor(int f) {
    currentFloor = f;
}

//增加生命值
void Player::addHp(int val) {
    hp += val;
    if (hp > maxHp) {
        maxHp = hp;
    }
}

//设置生命值
void Player::setHp(int val) {
    hp = val;
    if (hp > maxHp) {
        maxHp = hp;
    }
}

//受到伤害
void Player::takeDamage(int val) {
    hp -= val;
    if (hp < 0) {
        hp = 0;
    }
}

//判断是否死亡
bool Player::isDead() const {
    return hp <= 0;
}

//增加攻击力
void Player::addAtk(int val) {
    atk += val;
}

//增加防御力
void Player::addDef(int val) {
    def += val;
}

//使用棕色钥匙
bool Player::useKeyBrown() {
    if (key_brown > 0) {
        key_brown--;
        return true;
    }
    return false;
}

//使用银钥匙
bool Player::useKeySilver() {
    if (key_silver > 0) {
        key_silver--;
        return true;
    }
    return false;
}

//使用红色钥匙
bool Player::useKeyRed() {
    if (key_red > 0) {
        key_red--;
        return true;
    }
    return false;
}

Moster.h

#ifndef MONSTER_H
#define MONSTER_H

#include "config.h"
#include <string>
#include "Player.h"

//怪物基类
class Monster {
    protected:
        std::string name;    //怪物名称
        int hp;              //怪物血量
        int atk;             //怪物攻击力
        int def;             //怪物防御力
        int gold;            //击败怪物后获得的金币
        int exp;             //击败怪物后获得的经验值

    public:
       //构造函数
       Monster(std::string n, int h, int a, int d, int g,int e)
        : name(n), hp(h), atk(a), def(d), gold(g), exp(e) {}
        
        //虚析构函数
        virtual ~Monster() = default;

        //虚函数(die)
        //怪物死亡时调用
        //用于处理怪物死亡后的逻辑,如奖励玩家金币、经验等
        virtual void onDeath(Player& player) = 0;

        //获取属性
        std::string getName() const { return name; }
        int getHp() const { return hp; }
        int getAtk() const { return atk; }
        int getDef() const { return def; }
        int getGold() const { return gold; }
        int getExp()   const { return exp; }

        //受到伤害
        void takeDamage(int val) {
            hp -= val;
            if (hp < 0) hp = 0;
        }

        //判断是否死亡
        bool isDead() const { return hp <= 0; }
};

//绿色史莱姆
class GreenSlime : public Monster {
    public:
        GreenSlime() : Monster("绿色史莱姆", 50, 20, 1, 1 ,1) {}
        void onDeath(Player& player) override ;
};

//红色史莱姆
class RedSlime : public Monster {
    public:
        RedSlime() : Monster("红色史莱姆", 70, 15, 2, 2 ,2) {}
        void onDeath(Player& player) override ;
};

//黑色史莱姆
class BlackSlime : public Monster {
    public:
        BlackSlime() : Monster("黑色史莱姆", 200, 35, 10, 5, 5) {}
        void onDeath(Player& player) override;
};

//小蝙蝠
class Bat : public Monster {
    public:
        Bat() : Monster("小蝙蝠", 100, 20, 5, 3 ,3) {}
        void onDeath(Player& player) override ;
};

//骷髅兵
class Skeleton : public Monster {
    public:
        Skeleton() : Monster("骷髅兵", 110, 25, 5, 5 ,4) {}
        void onDeath(Player& player) override ;
};

// 地图上的「未知怪」
class MysteryMonster : public Monster {
    public:
        MysteryMonster() : Monster("未知怪物", 80, 22, 6, 8, 6) {}
        void onDeath(Player& player) override;
};

//占位怪
class Placeholder : public Monster {
    public:
        Placeholder() : Monster("占位怪", 999, 999, 999, 999, 999) {}
        void onDeath(Player&) override {}
};
#endif

Moster.cpp

//怪物
#include "Moster.h"

//绿色史莱姆死亡效果
void GreenSlime::onDeath(Player& player) {
    player.addGold(gold);
}

// 红色史莱姆
void RedSlime::onDeath(Player& player) {
    player.addGold(gold);
}

void BlackSlime::onDeath(Player& player) {
    player.addGold(gold);
}

// 小蝙蝠
void Bat::onDeath(Player& player) {
    player.addGold(gold);
}

//骷髅兵
void Skeleton::onDeath(Player& player) {
    player.addGold(gold);
}

void MysteryMonster::onDeath(Player& player) {
    player.addGold(gold);
}

Game.h

#ifndef GAME_H
#define GAME_H

#include "Player.h"
#include "Map.h"

// 游戏总控类
class Game {
private:
    Player player;   //玩家
    Map map;         //地图
    bool isRunning;  //游戏是否运行

    //核心流程
    void draw();     //绘制画面
    void input();    //按键输入
    void update();   //逻辑更新

    char lastKey;    //记忆玩家点下的按键

    //交互
    void fight(int mx, int my, Monster* monster);   //战斗
    bool tryOpenDoor(int tile);                     //开门
    void pickItem(int tile, int x, int y);          //捡道具
    void runShop();                                 //商店菜单

public:
    Game();
    void run(); // 启动游戏
};

#endif

Game.cpp

//游戏核心逻辑
#include "Game.h"
#include "Moster.h"
#include "Utils.h"
#include <algorithm>
#include <conio.h>
#include <iostream>
#include <string>

namespace {

//控制台常见等宽假设:ASCII=1列,UTF-8中日韩等多数字符按2列
int utf8_displayCols(const std::string& s) {
    int cols = 0;
    size_t i = 0;
    while (i < s.size()) {
        unsigned char c = static_cast<unsigned char>(s[i]);
        if (c < 0x80u) {
            cols += 1;
            i += 1;
        } else if ((c & 0xE0u) == 0xC0u && i + 1 < s.size()) {
            cols += 2;
            i += 2;
        } else if ((c & 0xF0u) == 0xE0u && i + 2 < s.size()) {
            cols += 2;
            i += 3;
        } else if ((c & 0xF8u) == 0xF0u && i + 3 < s.size()) {
            cols += 2;
            i += 4;
        } else {
            cols += 1;
            i += 1;
        }
    }
    return cols;
}

std::string padRightToCols(const std::string& s, int targetCols) {
    std::string out = s;
    int w = utf8_displayCols(s);
    while (w < targetCols) {
        out.push_back(' ');
        ++w;
    }
    return out;
}

void printLegendLine4(const std::string& a, const std::string& b, const std::string& c, const std::string& d,
                        int w1, int w2, int w3) {
    std::cout << padRightToCols(a, w1) << padRightToCols(b, w2) << padRightToCols(c, w3) << d << std::endl;
}

} // namespace

Game::Game() : isRunning(true), lastKey(0) {}

void Game::draw() {
    Utils::gotoxy(0, 0);
    int f = player.getCurrentFloor();
    //
    std::cout << "C++魔塔 楼层:" << (f + 1) << "/" << MAX_FLOORS
              << " HP:" << player.getHp() << " 攻:" << player.getAtk() << " 防:" << player.getDef()
              << " 金:" << player.getGold() << " 经验:" << player.getExp()
              << " 钥匙[棕" << player.getKeyBrown() << "][银" << player.getKeySilver() << "][红" << player.getKeyRed() << "]"
              << std::string(64, ' ') << std::endl;

    //画地图
    for (int y = 0; y < MAP_H; y++) {
        for (int x = 0; x < MAP_W; x++) {
            if (x == player.getX() && y == player.getY()) {
                std::cout << "勇";
            } else {
                int tile = map.getTile(f, x, y);
                switch (tile) {
                    case ROAD:
                        std::cout << "  ";
                        break;
                    case WALL:
                        std::cout << "■ ";
                        break;
                    case HERO:
                        std::cout << "  ";
                        break;
                    case SLIME_GREEN:
                        std::cout << "绿";
                        break;
                    case SLIME_RED:
                        std::cout << "赤";
                        break;
                    case SLIME_BLACK:
                        std::cout << "黑";
                        break;
                    case BAT:
                        std::cout << "蝠";
                        break;
                    case KEY_BROWN:
                        std::cout << "棕";
                        break;
                    case KEY_SILVER:
                        std::cout << "银";
                        break;
                    case KEY_RED:
                        std::cout << "绯";
                        break;
                    case DOOR_BROWN:
                        std::cout << "木";
                        break;
                    case DOOR_SILVER:
                        std::cout << "铁";
                        break;
                    case DOOR_RED:
                        std::cout << "禁";
                        break;
                    case STAIRS:
                        std::cout << "梯";
                        break;
                    case POTION_GREEN:
                        std::cout << "瓶";
                        break;
                    case POTION_RED:
                        std::cout << "朱";
                        break;
                    case MONSTER_UNKNOWN:
                        std::cout << "异";
                        break;
                    case ITEM_UNKNOWN:
                        std::cout << "谜";
                        break;
                    case GEM_RED:
                        std::cout << "红";
                        break;
                    case GEM_BLUE:
                        std::cout << "蓝";
                        break;
                    case POTION_BLUE:
                        std::cout << "青";
                        break;
                    case DOOR_GREEN:
                        std::cout << "藤";
                        break;
                    case NPC_SAGE:
                        std::cout << "师";
                        break;
                    case NPC_CHILD:
                        std::cout << "童";
                        break;
                    case NPC_ELDER:
                        std::cout << "翁";
                        break;
                    case SWORD_ATK:
                        std::cout << "剑";
                        break;
                    case SHOP_LEFT:
                        std::cout << "店";
                        break;
                    case SHOP_MID:
                        std::cout << "商";
                        break;
                    case SHOP_RIGHT:
                        std::cout << "铺";
                        break;
                    case SKELETON:
                        std::cout << "骷";
                        break;
                    case DOOR_UNKNOWN:
                        std::cout << "秘";
                        break;
                    case KEY_UNKNOWN:
                        std::cout << "匙";
                        break;
                    default:
                        std::cout << "?";
                        break;
                }
            }
        }
        std::cout << std::endl;
    }

    // 图例:四列——①怪物类 ②钥匙与门(含未知)③药水/宝石/未知道具 ④NPC;按列对齐便于增删
    static const char* const legendRows[][4] = {
        // 地图基础(单独一行,与下列「四类」并列可读)
        {"■ :墙", " 梯:楼梯", " 勇:勇者(你)", ""},
        {"绿:绿色史莱姆 ", "棕:棕钥匙 ", "瓶:绿药水 ", "师:智者"},
        {"赤:红色史莱姆 ", "银:银钥匙 ", "朱:红药水 ", "童:孩童"},
        {"黑:黑色史莱姆 ", "绯:红钥匙 ", "青:蓝药水 ", "翁:村民"},
        {"蝠:蝙蝠 ", "匙:未知钥匙 ", "红:红宝石 ", " "},
        {"骷:骷髅兵 ", "木:棕门 ", "蓝:蓝宝石 ", " "},
        {"异:未知怪 ", "铁:银门 ", "剑:宝剑 ", " "},
        {"占:占位怪 ", "禁:红门 ", "谜:未知道具 ", " "},
        {" ", "藤:绿门", " ", " "},
        {" ", "秘:未知门", " ", " "},
    };

    const int legendN = static_cast<int>(sizeof(legendRows) / sizeof(legendRows[0]));
    int col1w = 0;
    int col2w = 0;
    int col3w = 0;
    for (int i = 0; i < legendN; ++i) {
        col1w = std::max(col1w, utf8_displayCols(legendRows[i][0]));
        col2w = std::max(col2w, utf8_displayCols(legendRows[i][1]));
        col3w = std::max(col3w, utf8_displayCols(legendRows[i][2]));
    }
    std::cout << "----------------------- 图例 -----------------------" << std::endl;
    for (int i = 0; i < legendN; ++i) {
        printLegendLine4(legendRows[i][0], legendRows[i][1], legendRows[i][2], legendRows[i][3], col1w, col2w, col3w);
    }
    std::cout << "店·商·铺:商店(走进任一格,按 1-4 选购)" << std::endl;
    std::cout << "操作:W A S D 移动" << std::endl;
}

//按键输入
void Game::input() {
    lastKey = static_cast<char>(_getch());
}

//战斗
void Game::fight(int mx, int my, Monster* monster) {
    int dmgToHero = (monster->getAtk() > player.getDef()) ? (monster->getAtk() - player.getDef()) : 0;
    int dmgToMon = (player.getAtk() > monster->getDef()) ? (player.getAtk() - monster->getDef()) : 0;

    if (dmgToMon <= 0) {
        std::cout << "打不动啊!" << std::endl;
        Utils::sleep(1000);
        return;
    }

    int turns = (monster->getHp() + dmgToMon - 1) / dmgToMon;
    //一击秒杀无伤;多回合时按「每回合怪都反击」计总伤,与相比多算最后一击前的反击
    int totalDmg = (turns > 1) ? (turns * dmgToHero) : 0;

    if (player.getHp() <= totalDmg) {
        std::cout << "打不过!预计受到 " << totalDmg << " 点伤害!" << std::endl;
        Utils::sleep(1000);
        return;
    }

    player.takeDamage(totalDmg);
    map.setTile(player.getCurrentFloor(), mx, my, ROAD);
    monster->onDeath(player);
    player.addExp(monster->getExp());
}

//尝试打开门
bool Game::tryOpenDoor(int tile) {
    if (tile == DOOR_BROWN || tile == DOOR_UNKNOWN) {
        return player.useKeyBrown();
    }
    if (tile == DOOR_SILVER) {
        return player.useKeySilver();
    }
    if (tile == DOOR_RED) {
        return player.useKeyRed();
    }
    return false;
}

//拾取道具
void Game::pickItem(int tile, int x, int y) {
    int fl = player.getCurrentFloor();
    switch (tile) {
        case KEY_BROWN:
            player.addKeyBrown(1);
            break;
        case KEY_SILVER:
            player.addKeySilver(1);
            break;
        case KEY_RED:
            player.addKeyRed(1);
            break;
        case KEY_UNKNOWN:
            std::cout << "拾起未知钥匙,获得棕钥匙1把。" << std::endl;
            Utils::sleep(400);
            player.addKeyBrown(1);
            break;
        case POTION_GREEN:
            player.addHp(200);
            break;
        case POTION_RED:
            std::cout << "喝下红色药水,生命+200。" << std::endl;
            Utils::sleep(350);
            player.addHp(200);
            break;
        case POTION_BLUE:
            std::cout << "喝下蓝色药水,生命+500。" << std::endl;
            Utils::sleep(350);
            player.addHp(500);
            break;
        case SWORD_ATK:
            std::cout << "获得宝剑!攻击力+10。" << std::endl;
            Utils::sleep(350);
            player.addAtk(10);
            break;
        case GEM_RED:
            std::cout << "拾起红宝石,攻击力+3。" << std::endl;
            Utils::sleep(350);
            player.addAtk(3);
            break;
        case GEM_BLUE:
            std::cout << "拾起蓝宝石,防御力+3。" << std::endl;
            Utils::sleep(350);
            player.addDef(3);
            break;
        case ITEM_UNKNOWN:
            std::cout << "拾起不明道具……(效果待定)" << std::endl;
            Utils::sleep(400);
            break;
        default:
            return;
    }
    map.setTile(fl, x, y, ROAD);
}

//更新游戏状态
void Game::update() {
    char key = lastKey;
    int f = player.getCurrentFloor();
    int nx = player.getX();
    int ny = player.getY();

    if (key == 'w' || key == 'W') {
        ny--;
    } else if (key == 's' || key == 'S') {
        ny++;
    } else if (key == 'a' || key == 'A') {
        nx--;
    } else if (key == 'd' || key == 'D') {
        nx++;
    } else {
        return;
    }

    if (nx < 0 || nx >= MAP_W || ny < 0 || ny >= MAP_H) {
        return;
    }

    int nextTile = map.getTile(f, nx, ny);

    if (nextTile == WALL) {
        return;
    }

    if (nextTile == SLIME_GREEN || nextTile == SLIME_RED || nextTile == SLIME_BLACK || nextTile == BAT
        || nextTile == MONSTER_UNKNOWN || nextTile == SKELETON) {
        GreenSlime gs;
        RedSlime rs;
        BlackSlime bs;
        Bat bat;
        MysteryMonster my;
        Skeleton sk;
        Monster* m = nullptr;
        if (nextTile == SLIME_GREEN) {
            m = &gs;
        } else if (nextTile == SLIME_RED) {
            m = &rs;
        } else if (nextTile == SLIME_BLACK) {
            m = &bs;
        } else if (nextTile == BAT) {
            m = &bat;
        } else if (nextTile == SKELETON) {
            m = &sk;
        } else {
            m = &my;
        }
        fight(nx, ny, m);
        return;
    }

    if (nextTile == KEY_BROWN || nextTile == KEY_SILVER || nextTile == KEY_RED
        || nextTile == KEY_UNKNOWN
        || nextTile == POTION_GREEN || nextTile == POTION_RED || nextTile == POTION_BLUE
        || nextTile == GEM_RED || nextTile == GEM_BLUE || nextTile == ITEM_UNKNOWN
        || nextTile == SWORD_ATK) {
        pickItem(nextTile, nx, ny);
        player.setPos(nx, ny);
        return;
    }

    if (nextTile == DOOR_GREEN) {
        std::cout << "绿门……(效果待定,暂时打开)" << std::endl;
        Utils::sleep(450);
        map.setTile(f, nx, ny, ROAD);
        player.setPos(nx, ny);
        return;
    }

    if (nextTile == DOOR_BROWN || nextTile == DOOR_SILVER || nextTile == DOOR_RED
        || nextTile == DOOR_UNKNOWN) {
        if (tryOpenDoor(nextTile)) {
            map.setTile(f, nx, ny, ROAD);
            player.setPos(nx, ny);
        } else {
            std::cout << "需要对应颜色的钥匙!" << std::endl;
            Utils::sleep(500);
        }
        return;
    }

    if (nextTile == NPC_SAGE) {
        std::cout << "智者:……(对话效果待定)" << std::endl;
        Utils::sleep(450);
        player.setPos(nx, ny);
        return;
    }
    if (nextTile == NPC_CHILD) {
        std::cout << "孩童:……(对话效果待定)" << std::endl;
        Utils::sleep(450);
        player.setPos(nx, ny);
        return;
    }
    if (nextTile == NPC_ELDER) {
        std::cout << "村民:……(对话效果待定)" << std::endl;
        Utils::sleep(450);
        player.setPos(nx, ny);
        return;
    }

    if (nextTile == SHOP_LEFT || nextTile == SHOP_MID || nextTile == SHOP_RIGHT) {
        player.setPos(nx, ny);
        runShop();
        return;
    }

    if (nextTile == STAIRS) {
        //第1层(1,1)楼梯上 → 第2层,落在左上角楼梯(1,1)正下方可走格(1,2);
        if (nx == 1 && ny == 1 && f == 0) {
            player.setFloor(1);
            player.setPos(1, 2);
            draw();
            std::cout << "到达第2层!" << std::endl;
            Utils::sleep(500);
            return;
        }
        //(6,11):第1层仅为地图上的「楼梯」格,走过去不换层
        if (nx == 6 && ny == 11 && f == 0) {
            player.setPos(nx, ny);
            return;
        }
        //第2层(1,1)楼梯下 → 回到第1层
        if (nx == 1 && ny == 1 && f == 1) {
            player.setFloor(0);
            player.setPos(1, 1);
            draw();
            std::cout << "回到第1层!" << std::endl;
            Utils::sleep(500);
            return;
        }
        //第2层(1,11)楼梯上 → 第3层,落在可走格(5,10),第三关地图该处为路
        if (nx == 1 && ny == 11 && f == 1) {
            player.setFloor(2);
            player.setPos(5, 10);
            draw();
            std::cout << "到达第3层!" << std::endl;
            Utils::sleep(500);
            return;
        }
        //第3层(1,11)楼梯下 → 第2层
        if (nx == 1 && ny == 11 && f == 2) {
            player.setFloor(1);
            player.setPos(1, 10);
            draw();
            std::cout << "回到第2层!" << std::endl;
            Utils::sleep(500);
            return;
        }
        //第3层(11,11)楼梯下 → 第4层,落在右下角楼梯(11,11)正上一格(11,10)
        if (nx == 11 && ny == 11 && f == 2) {
            player.setFloor(3);
            player.setPos(11, 10);
            draw();
            std::cout << "到达第4层!" << std::endl;
            Utils::sleep(500);
            return;
        }
        //第4层(1,11)楼梯 → 第5层,落在左下角楼梯上一格(1,10)
        if (nx == 1 && ny == 11 && f == 3) {
            player.setFloor(4);
            player.setPos(1, 10);
            draw();
            std::cout << "到达第5层!" << std::endl;
            Utils::sleep(500);
            return;
        }
        //第4层(11,11)楼梯 → 第3层,与从3楼来4层时(11,10)落地相对
        if (nx == 11 && ny == 11 && f == 3) {
            player.setFloor(2);
            player.setPos(11, 10);
            draw();
            std::cout << "回到第3层!" << std::endl;
            Utils::sleep(500);
            return;
        }
        //第5层(1,11)楼梯下 → 第4层
        if (nx == 1 && ny == 11 && f == 4) {
            player.setFloor(3);
            player.setPos(1, 10);
            draw();
            std::cout << "回到第4层!" << std::endl;
            Utils::sleep(500);
            return;
        }
        //第5层(10,11)楼梯:暂未开放
        if (nx == 10 && ny == 11 && f == 4) {
            std::cout << "此处通道尚未开放,游戏正在开发中……" << std::endl;
            Utils::sleep(500);
            return;
        }
        //第2层(6,10):第2↔3、第3↔2、第4↔3、第5↔4
        if (nx == 6 && ny == 10) {
            if (f == 1) {
                player.setFloor(2);
                player.setPos(5, 10);
                draw();
                std::cout << "到达第3层!" << std::endl;
                Utils::sleep(500);
            } else if (f == 2) {
                player.setFloor(1);
                player.setPos(1, 10);
                draw();
                std::cout << "回到第2层!" << std::endl;
                Utils::sleep(500);
            } else if (f == 3) {
                player.setFloor(2);
                player.setPos(5, 10);
                draw();
                std::cout << "回到第3层!" << std::endl;
                Utils::sleep(500);
            } else if (f == 4) {
                player.setFloor(3);
                player.setPos(6, 10);
                draw();
                std::cout << "回到第4层!" << std::endl;
                Utils::sleep(500);
            }
            return;
        }
        if (nx == 10 && ny == 10) {
            //第2层(10,10):第2↔3、第3↔2、第4↔5、第5↔4
            if (f == 1) {
                player.setFloor(2);
                player.setPos(11, 10);
                draw();
                std::cout << "到达第3层!" << std::endl;
                Utils::sleep(500);
            } else if (f == 2) {
                player.setFloor(1);
                player.setPos(1, 10);
                draw();
                std::cout << "回到第2层!" << std::endl;
                Utils::sleep(500);
            } else if (f == 3) {
                player.setFloor(4);
                player.setPos(1, 10);
                draw();
                std::cout << "到达第5层!" << std::endl;
                Utils::sleep(500);
            } else if (f == 4) {
                player.setFloor(3);
                player.setPos(11, 10);
                draw();
                std::cout << "回到第4层!" << std::endl;
                Utils::sleep(500);
            }
        }
        return;
    }

    player.setPos(nx, ny);
}

void Game::runShop() {
    const int shopPrice = 25;
    for (;;) {
        draw();
        std::cout << "\n============= 商店 =============\n";
        std::cout << "当前金币:" << player.getGold() << "\n";
        std::cout << "1. 生命 +800(花费 " << shopPrice << " 金币)\n";
        std::cout << "2. 攻击 +4(花费 " << shopPrice << " 金币)\n";
        std::cout << "3. 防御 +4(花费 " << shopPrice << " 金币)\n";
        std::cout << "4. 离开商店(不花费金币)\n";
        std::cout << "请按数字键 1-4";
        std::cout.flush();
        char c = static_cast<char>(_getch());
        std::cout << c << std::endl;
        if (c == '1') {
            if (player.getGold() < shopPrice) {
                std::cout << "金币不足!购买需要 " << shopPrice << " 金币。" << std::endl;
                Utils::sleep(450);
                continue;
            }
            player.spendGold(shopPrice);
            player.addHp(800);
            std::cout << "生命 +800!已扣除 " << shopPrice << " 金币。" << std::endl;
        } else if (c == '2') {
            if (player.getGold() < shopPrice) {
                std::cout << "金币不足!购买需要 " << shopPrice << " 金币。" << std::endl;
                Utils::sleep(450);
                continue;
            }
            player.spendGold(shopPrice);
            player.addAtk(4);
            std::cout << "攻击 +4!已扣除 " << shopPrice << " 金币。" << std::endl;
        } else if (c == '3') {
            if (player.getGold() < shopPrice) {
                std::cout << "金币不足!购买需要 " << shopPrice << " 金币。" << std::endl;
                Utils::sleep(450);
                continue;
            }
            player.spendGold(shopPrice);
            player.addDef(4);
            std::cout << "防御 +4!已扣除 " << shopPrice << " 金币。" << std::endl;
        } else if (c == '4') {
            std::cout << "下次再来。" << std::endl;
            Utils::sleep(350);
            return;
        } else {
            std::cout << "请输入 1234" << std::endl;
            Utils::sleep(400);
            continue;
        }
        Utils::sleep(450);
    }
}

void Game::run() {
    while (isRunning) {
        draw();
        input();
        update();
    }
}

Utils.h

#ifndef UTILS_H
#define UTILS_H


//工具函数:控制台光标、睡眠、清屏、隐藏光标
namespace Utils {

    //移动光标到(x,y)
    void gotoxy(int x, int y);

    //隐藏光标
    void hideCursor();

    //延时ms毫秒
    void sleep(int ms);

    //清屏
    void clearScreen();
}

#endif

Utils.cpp

//工具函数
#include "Utils.h"
#include <windows.h>
#include <conio.h>

namespace Utils {

    //移动光标
    void gotoxy(int x, int y) {
        COORD pos = { (SHORT)x, (SHORT)y };
        //给控制台的屏幕缓冲区设置光标当前位置
        SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
    }

    //隐藏光标
    //windows控制台里面的写法
    void hideCursor() {
        //CONSOLE_CURSOR_INFO 一个结构体,用于存储光标的信息
        CONSOLE_CURSOR_INFO info = { 1, FALSE };
        //1.表示光标填充的位数 //.FALSE表示不显示光标

        //设置指定控制台屏幕缓冲区当前的光标外观,控制台光标标准输出设置
        SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info);
    }

    //延时ms毫秒
    void sleep(int ms) {
        Sleep(ms);
    }

    //清屏
    void clearScreen() {
        system("cls");
    }
}

main.cpp

//程序入口

#include "Game.h"
#include "Utils.h"

int main() {
    //初始化控制台
    //隐藏光标
    Utils::hideCursor();

    std::cout << "\n";
    Utils::sleep(450);

    //创建游戏对象
    Game game;

    //启动游戏
    game.run();

    return 0;
}