Repository navigation
Expand file tree
/
Copy pathlog_entry.cpp
More file actions
262 lines (236 loc) · 9.57 KB
/
Copy pathlog_entry.cpp
File metadata and controls
262 lines (236 loc) · 9.57 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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// LogEntry 的序列化 / 反序列化实现。
//
// 坐标约定(与 DESIGN.md §3 的实例一致):
// 日志中的坐标形如 [i×j] / (i×j),按“行 × 列”书写且以 1 为基准,
// 与 core::Position{x=列, y=行} 对应,即 token(i×j) ⇔ Position{x:j, y:i}。
// 该坐标域与 GameState 内部坐标域之间的换算由上层集成负责,
// 本模块只保证 serialize/deserialize 在此坐标域内往返一致。
#include "log/log_entry.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
namespace snake::log {
namespace {
// 方向按枚举序对应的 UTF-8 字形:Right →、Down ↓、Up ↑、Left ←(各 3 字节)。
constexpr std::array<std::string_view, 4> kArrowGlyphs = {
std::string_view("\xE2\x86\x92", 3), // →
std::string_view("\xE2\x86\x93", 3), // ↓
std::string_view("\xE2\x86\x91", 3), // ↑
std::string_view("\xE2\x86\x90", 3), // ←
};
// UTF-8 乘号 ×(2 字节)。
constexpr std::string_view kTimes("\xC3\x97", 2);
// 从 s[pos] 起消费一个 ASCII 字符 c。
bool ConsumeAscii(std::string_view s, std::size_t& pos, char c) {
if (pos < s.size() && s[pos] == c) {
++pos;
return true;
}
return false;
}
// 从 s[pos] 起解析一个正整数并以 delim 结尾;成功时 pos 越过 delim。
// Args:
// s: 被解析的整行文本。
// pos: 输入输出,当前扫描位置。
// delim: 数字段后的定界符('×' 等 UTF-8 片段或 ASCII 字符)。
// value: 输出解析出的整数值。
// Returns: 数字缺失、溢出或定界符缺失时返回 false。
bool ParseIntUntil(std::string_view s, std::size_t& pos,
std::string_view delim, std::int32_t& value) {
if (pos >= s.size() || s[pos] < '0' || s[pos] > '9') return false;
std::int64_t acc = 0;
while (pos < s.size() && s[pos] >= '0' && s[pos] <= '9') {
acc = acc * 10 + (s[pos] - '0');
if (acc > std::numeric_limits<std::int32_t>::max()) return false;
++pos;
}
if (!s.substr(pos).starts_with(delim)) return false;
pos += delim.size();
value = static_cast<std::int32_t>(acc);
return true;
}
// 从 s[pos] 起尝试解析一个方向箭头字形;成功时不推进 pos。
// Returns: 命中返回对应 Direction,否则返回 nullopt。
std::optional<core::Direction> ArrowAt(std::string_view s, std::size_t pos) {
for (std::size_t i = 0; i < kArrowGlyphs.size(); ++i) {
if (s.substr(pos, 3) == kArrowGlyphs[i]) {
return static_cast<core::Direction>(i);
}
}
return std::nullopt;
}
// 从 s[pos] 起解析果实坐标 "(i×j)";成功时 pos 越过 ')'。
// Returns: 结构非法时返回 false。
bool ParseFruitToken(std::string_view s, std::size_t& pos, core::Position& out) {
if (!ConsumeAscii(s, pos, '(')) return false;
std::int32_t row = 0, col = 0;
if (!ParseIntUntil(s, pos, kTimes, row)) return false;
if (!ParseIntUntil(s, pos, ")", col)) return false;
out = core::Position{col, row};
return true;
}
// 判断坐标(1 基,行×列语义已折叠进 Position)是否落在棋盘内。
bool InBoard(const core::Position& p, std::int32_t width, std::int32_t height) {
return p.x >= 1 && p.x <= width && p.y >= 1 && p.y <= height;
}
// 向 out 追加 "row×col"(两个坐标均按日志的行×列顺序输出)。
void AppendCoord(std::string& out, std::int32_t row, std::int32_t col) {
out += std::to_string(row);
out += kTimes;
out += std::to_string(col);
}
// 向 out 追加果实段 "(i×j)"。
void AppendFruit(std::string& out, const core::Position& p) {
out.push_back('(');
AppendCoord(out, p.y, p.x);
out.push_back(')');
}
} // namespace
std::string_view to_string(ParseError e) noexcept {
switch (e) {
case ParseError::Empty: return "empty log line";
case ParseError::BadBoardSize: return "missing/invalid {n×m} board size";
case ParseError::BadHead: return "missing/out-of-range head [i×j]";
case ParseError::BadDirection: return "non-arrow character in move stream";
case ParseError::BadFruitList: return "malformed fruit segment (i×j)";
case ParseError::BadOutcome: return "missing/invalid outcome {W}/{L}";
case ParseError::TrailingGarbage: return "content after final '}'";
}
return "unknown parse error";
}
std::string LogEntry::serialize() const {
std::string out;
// {n×m}
out.push_back('{');
out += std::to_string(board_width);
out += kTimes;
out += std::to_string(board_height);
out.push_back('}');
// [i×j] 初始蛇头(行×列)
out.push_back('[');
AppendCoord(out, initial_head.y, initial_head.x);
out.push_back(']');
// 首个果实 (i×j) 紧跟蛇头;果实列表恒非空(每局开始时必有首个果实)。
if (!fruits.empty()) AppendFruit(out, fruits.front());
// →↓↑← 方向流(每 tick 一个字符,紧接首果之后)
for (core::Direction d : moves) {
out += kArrowGlyphs[static_cast<std::size_t>(d)];
}
// 后续果实 (i×j)(吃果后新放置的果实)跟在方向流之后
for (std::size_t i = 1; i < fruits.size(); ++i) {
AppendFruit(out, fruits[i]);
}
// {W}/{L} 结局
out.push_back('{');
out.push_back(outcome == Outcome::Won ? 'W' : 'L');
out.push_back('}');
return out;
}
std::expected<LogEntry, ParseError> LogEntry::deserialize(std::string_view line) noexcept {
if (line.empty()) return std::unexpected(ParseError::Empty);
LogEntry entry;
std::size_t pos = 0;
// {n×m}:n 列 × m 行,十进制正整数。
if (!ConsumeAscii(line, pos, '{')) {
return std::unexpected(ParseError::BadBoardSize);
}
std::int32_t width = 0, height = 0;
if (!ParseIntUntil(line, pos, kTimes, width) || width <= 0) {
return std::unexpected(ParseError::BadBoardSize);
}
if (!ParseIntUntil(line, pos, "}", height) || height <= 0) {
return std::unexpected(ParseError::BadBoardSize);
}
entry.board_width = width;
entry.board_height = height;
// [i×j] 初始蛇头(i=行, j=列),越界即违例。
if (!ConsumeAscii(line, pos, '[')) {
return std::unexpected(ParseError::BadHead);
}
std::int32_t row = 0, col = 0;
if (!ParseIntUntil(line, pos, kTimes, row) ||
!ParseIntUntil(line, pos, "]", col)) {
return std::unexpected(ParseError::BadHead);
}
entry.initial_head = core::Position{col, row};
if (!InBoard(entry.initial_head, width, height)) {
return std::unexpected(ParseError::BadHead);
}
// 首个果实紧跟蛇头;缺失或越界即违例。
core::Position fruit;
if (!ParseFruitToken(line, pos, fruit) || !InBoard(fruit, width, height)) {
return std::unexpected(ParseError::BadFruitList);
}
entry.fruits.push_back(fruit);
// 方向流与后续果实(吃果后新放置的果实)可交错出现,直到结局段 '{'。
while (pos < line.size() && line[pos] != '{') {
if (std::optional<core::Direction> d = ArrowAt(line, pos)) {
entry.moves.push_back(*d);
pos += 3; // 跳过 3 字节箭头字形
continue;
}
if (line[pos] == '(') {
core::Position next;
if (!ParseFruitToken(line, pos, next) || !InBoard(next, width, height)) {
return std::unexpected(ParseError::BadFruitList);
}
entry.fruits.push_back(next);
continue;
}
return std::unexpected(ParseError::BadDirection);
}
// 可选结局段 {W}/{L}:缺失时 outcome 保持默认值 Lost。
if (pos < line.size()) {
if (!ConsumeAscii(line, pos, '{')) {
return std::unexpected(ParseError::BadOutcome);
}
if (pos >= line.size()) return std::unexpected(ParseError::BadOutcome);
const char c = line[pos];
if (c != 'W' && c != 'L') return std::unexpected(ParseError::BadOutcome);
entry.outcome = (c == 'W') ? Outcome::Won : Outcome::Lost;
++pos;
if (!ConsumeAscii(line, pos, '}')) {
return std::unexpected(ParseError::BadOutcome);
}
if (pos != line.size()) return std::unexpected(ParseError::TrailingGarbage);
}
return entry;
}
void LogEntry::append_move(core::Direction d) {
moves.push_back(d);
}
void LogEntry::append_fruit(core::Position p) {
fruits.push_back(p);
}
std::expected<std::vector<LogEntry>, LogTextParseError>
parse_log(std::string_view text) noexcept {
std::vector<LogEntry> entries;
std::size_t line_index = 0; // 0 基行号
std::size_t start = 0;
while (start <= text.size()) {
const std::size_t nl = text.find('\n', start);
const std::size_t end = (nl == std::string_view::npos) ? text.size() : nl;
std::string_view line = text.substr(start, end - start);
if (!line.empty() && line.back() == '\r') line.remove_suffix(1);
const bool blank = std::all_of(line.begin(), line.end(), [](char c) {
return c == ' ' || c == '\t' || c == '\r';
});
if (!blank) {
if (auto r = LogEntry::deserialize(line); r.has_value()) {
entries.push_back(std::move(*r));
} else {
return std::unexpected(LogTextParseError{line_index, r.error()});
}
}
if (nl == std::string_view::npos) break;
start = nl + 1;
++line_index;
}
return entries;
}
} // namespace snake::log