Repository navigation
Expand file tree
/
Copy pathrenderer.cpp
More file actions
181 lines (163 loc) · 6.64 KB
/
Copy pathrenderer.cpp
File metadata and controls
181 lines (163 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// AnsiRenderer 实现:纯 ANSI 转义序列,零外部依赖(不引入 ncurses)。
// 布局:上方为棋盘(蛇 + 果实),最下方一行显示“反转后的日志字符串”。
//
// 反转规则(按“令牌”反转,见验收测试 3):{…} / […] / (…) 括号组作为
// 不可拆分的整体,其余字符(如 →↓↑←)逐个作为令牌,令牌序列整体倒序:
// {2×2}[2×2](1×1)→↓←↑{W} → {W}↑←↓→(1×1)[2×2]{2×2}
// 日志行由 main 经 IRenderer::set_status_line / draw_static 传入——render
// 模块不依赖 log 模块(见 DESIGN.md 的模块约束)。
#include "render/renderer.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <deque>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace snake::render {
namespace {
// 返回以 c 开头的 UTF-8 码点的字节数(ASCII 1 字节,× 2 字节,箭头 3 字节)。
// Args:
// c: 码点首字节。
// Returns: 1~4。
std::size_t Utf8SeqLen(char c) {
const unsigned char u = static_cast<unsigned char>(c);
if ((u & 0xE0) == 0xC0) return 2;
if ((u & 0xF0) == 0xE0) return 3;
if ((u & 0xF8) == 0xF0) return 4;
return 1;
}
// 把日志行按“令牌”反转:括号组({…} […] (…))整体视为一个令牌,
// 其余字符按码点逐个成令牌,令牌序列倒序拼接。
// Args:
// s: 单行日志文本(UTF-8)。
// Returns: 令牌倒序后的新字符串。
std::string ReverseLogLine(std::string_view s) {
std::vector<std::string_view> tokens;
tokens.reserve(s.size() / 3 + 1);
for (std::size_t i = 0; i < s.size();) {
std::size_t len;
if (s[i] == '{' || s[i] == '[' || s[i] == '(') {
const char close = (s[i] == '{') ? '}' : (s[i] == '[') ? ']' : ')';
const std::size_t end = s.find(close, i + 1);
len = (end == std::string_view::npos) ? s.size() - i : end - i + 1;
} else {
len = Utf8SeqLen(s[i]);
}
tokens.push_back(s.substr(i, len));
i += len;
}
std::string out;
out.reserve(s.size());
for (auto it = tokens.rbegin(); it != tokens.rend(); ++it) {
out += *it;
}
return out;
}
// 棋盘一格的字符画(每格固定 2 个 ASCII 字符宽,保证对齐)。
constexpr std::string_view kHead = "@@"; // 蛇头
constexpr std::string_view kBody = "##"; // 蛇身
constexpr std::string_view kFruit = "**"; // 果实
constexpr std::string_view kEmpty = " "; // 空格
// 生成上/下边框线:"+----+",宽度 = 2×棋盘列数(每格 2 字符)。
// Args:
// width: 棋盘列数。
// Returns: 边框线文本(不含换行)。
std::string BuildBorder(std::int32_t width) {
std::string line = "+";
line.append(static_cast<std::size_t>(width) * 2, '-');
line += "+";
return line;
}
// 生成棋盘帧文本(含清屏 + 光标归位前缀与边框,需求 3:可视化墙)。
// Args:
// width/height: 棋盘尺寸(墙即边界,碰撞判定仍基于网格边界)。
// body: 蛇身(front 为头,可为空)。
// fruit: 果实位置(可能不存在)。
// Returns: 可直接写出的“清屏 + 边框 + 棋盘”文本(不含状态行)。
std::string BuildBoardText(std::int32_t width, std::int32_t height,
const std::deque<core::Position>& body,
std::optional<core::Position> fruit) {
std::string out;
out += "\x1b[2J\x1b[H"; // 清屏 + 光标归位到左上角
if (body.empty()) return out;
const std::string border = BuildBorder(width);
const core::Position head = body.front();
out += border;
out += "\r\n";
for (std::int32_t y = 0; y < height; ++y) {
out += "|";
for (std::int32_t x = 0; x < width; ++x) {
const core::Position p{x, y};
if (p == head) {
out += kHead;
} else if (std::find(body.begin(), body.end(), p) != body.end()) {
out += kBody;
} else if (fruit && *fruit == p) {
out += kFruit;
} else {
out += kEmpty;
}
}
out += "|\r\n";
}
out += border;
out += "\r\n";
return out;
}
} // namespace
class AnsiRenderer final : public IRenderer {
public:
// 每帧重绘:清屏 + 棋盘 + 最下方反转后的日志行。
void draw(const core::GameState& state) override {
std::optional<core::Position> fruit;
if (state.fruit().exists()) fruit = state.fruit().position();
Write(BuildBoardText(state.width(), state.height(),
state.snake().body(), fruit) +
ReverseLogLine(status_line_) + "\r\n");
}
// 局末画面:棋盘 + 胜负行(WIN/LOST + 得分)+ 反转后的完整日志行。
void draw_game_over(const core::GameState& state) override {
std::optional<core::Position> fruit;
if (state.fruit().exists()) fruit = state.fruit().position();
std::string out = BuildBoardText(state.width(), state.height(),
state.snake().body(), fruit);
out += state.phase() == core::Phase::Won ? "*** WIN! Score: "
: "*** LOST! Score: ";
out += std::to_string(state.score());
out += "\r\n";
out += ReverseLogLine(status_line_);
out += "\r\n";
Write(out);
}
// 首次进入或终端尺寸变化时全量重绘:清屏归位即可(draw 每帧都会重绘)。
void invalidate() noexcept override {
constexpr char kClear[] = "\x1b[2J\x1b[H";
std::fwrite(kClear, 1, sizeof(kClear) - 1, stdout);
std::fflush(stdout);
}
void set_status_line(std::string_view line) override {
status_line_.assign(line);
}
// 静态画面(回放/调试用):给定棋盘与蛇/果实数据直接渲染一帧。
void draw_static(std::int32_t width, std::int32_t height,
const std::deque<core::Position>& body,
std::optional<core::Position> fruit,
std::string_view status_line) override {
Write(BuildBoardText(width, height, body, fruit) +
ReverseLogLine(status_line) + "\r\n");
}
private:
// 把一帧文本整体写入 stdout 并刷新(单次 write 避免半帧闪烁)。
static void Write(std::string_view s) {
std::fwrite(s.data(), 1, s.size(), stdout);
std::fflush(stdout);
}
std::string status_line_;
};
IRenderer* make_renderer() {
static AnsiRenderer instance;
return &instance;
}
} // namespace snake::render