# 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」的情况。
五、整体架构(怎么串起来)
main:调用Utils::hideCursor(),构造Game,执行game.run()。Game::run():死循环 ——draw()→input()→update()。draw():光标回到(0,0),打印状态栏与当前楼层地图;地图上每个格子根据Map::getTile的编号显示一个字符;下方输出图例与操作说明。input():_getch()读取一个按键,写入成员变量lastKey。update():根据lastKey(WASD)计算目标坐标,读取目标格类型,依次判断:墙、怪物、道具、门、NPC、商店、楼梯、普通道路等,并调用fight、pickItem、tryOpenDoor、runShop或换层。
一句话: Map 存「世界长什么样」,Player 存「人状态如何」,Game 存「规则与流程」,怪物行为通过 Monster 多态扩展。
六、核心模块说明
1. config.h
MAP_W、MAP_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。 - 属性:生命、攻击、防御、金币、经验;棕 / 银 / 红钥匙数量。
setPos、setFloor:楼梯传送与换层落点均通过它们完成。
4. Monster(Moster.*)
- 抽象基类:
virtual void onDeath(Player&) = 0,子类实现掉落;virtual析构保证多态删除安全。 - 子类示例:绿/红/黑史莱姆、蝙蝠、骷髅、未知怪等,数值与掉落各不相同。
5. Utils
gotoxy:SetConsoleCursorPosition,用于从屏幕左上角重绘。hideCursor:隐藏控制台光标,避免闪烁干扰。sleep:封装Sleep(毫秒)。
6. Game
fight:按攻防差计算每回合伤害与回合数,预判总受伤;打不过则不入战;战胜后清怪格、onDeath、加经验等。pickItem:各类药水、钥匙、宝石、剑等效果,switch分支维护,拾取后该格改为道路。tryOpenDoor:门类型与钥匙类型对应,消耗钥匙并开门。runShop:站在商店三格之一进入子循环,数字键购买或离开。update楼梯分支:根据当前楼层 + 目标格坐标决定上楼/下楼及落点(项目迭代中已按关卡设计调整,例如第四层左下角上第五层、第五层左下角回第四层等)。
七、游戏机制摘要
移动与碰撞
- 仅 W / A / S / D(大小写均可)有效;目标格为墙则不移动。
- 勇者显示为「勇」,覆盖当前格子的地图显示。
战斗(简化回合)
- 若对怪物每回合伤害 ≤ 0,提示「打不动」。
- 否则用血量与每回合伤害推算回合数,再估算勇者受到的总伤害(多回合时按怪物每回合反击累计);若预计会阵亡,则不进入战斗并提示。
- 胜利:扣血、移除怪物图块、执行
onDeath、增加经验。
道具与装备(以代码为准)
- 红/蓝宝石、红/蓝药、宝剑等均在
pickItem中有对应效果。 - 未知钥匙等可能按棕钥匙数量处理,并有人机可读提示。
商店
- 走进「店 / 商 / 铺」任一格进入商店菜单;商品价格与属性加成见
runShop源码中的常量与选项。
图例(draw 中)
- 图例分为多列(怪物 / 钥匙与门 / 药水与宝石与道具 / NPC 等),使用 UTF-8 显示宽度计算做列对齐,便于后期增删条目。
八、设计上的取舍
- 控制台绘图:不引入图形库,依赖
gotoxy整屏刷新,实现简单、依赖少,但可移植性限于 Windows 控制台 API。 - 怪物数据:地图上用
Tile区分种类,战斗时创建栈上怪物对象并传入fight;若需「每只怪独立血量」,可改为monsterMap指向堆对象并序列化状态。 - 楼梯规则:集中在
Game::update的STAIRS分支,楼层多时代码量会变大;后续可抽成表驱动(楼层、入口坐标、目标楼层、落点)。
九、扩展与维护建议
- 新怪物:在
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 << "请输入 1、2、3 或 4。" << 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;
}