Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
zmx0142857
GitHub Repository: zmx0142857/mini-games
Path: blob/master/c/typing/typing.cpp
363 views
1
/* bug:
2
* 1. 窗口过小,容不下一段文字时文字不显示
3
* 2. 窗口由大变小时光标位置不对
4
*/
5
#include "game.h"
6
#include "str.h" // String
7
#include <clocale> // setlocale()
8
#include <cctype> // isspace()
9
#include <cstring> // strcmp()
10
#include <ctime> // rand()
11
#include <vector>
12
#include <stack>
13
#include <iostream>
14
#include <fstream>
15
16
using namespace std;
17
18
//ofstream logger;
19
20
class Article {
21
22
vector<String> text;
23
vector<int> widths;
24
25
public:
26
void read_stream(const string &filename, ifstream &ifs) {
27
ifs.open(filename, ios::in);
28
if (!ifs) {
29
cerr << "error: cannot open file '" << filename << "'. "
30
"does this file exist?\n";
31
exit(1);
32
}
33
}
34
35
// 载入
36
void read(const string &filename, int begin = 1, int end = 0x7fffffff) {
37
ifstream ifs;
38
read_stream(filename, ifs);
39
String line;
40
text.clear();
41
for (int i = 1; getline(ifs, line); ++i) {
42
if (i < begin) continue;
43
else if (i > end) break;
44
text.push_back(line);
45
// 计算一行文字的宽度并缓存
46
int w = 0;
47
for (size_t i = 0; i < line.size(); ++i) {
48
if (line[i].is_ascii())
49
w += 1;
50
else
51
w += 2;
52
}
53
if (line.size() == 0)
54
w = 1; // 空行也有一格的高度
55
widths.push_back(w);
56
}
57
ifs.close();
58
}
59
60
// 随机文本
61
void rand(const string &filename, int rand_cnt = 10) {
62
ifstream ifs;
63
read_stream(filename, ifs);
64
Char ch;
65
vector<Char> str;
66
while (ifs >> ch) {
67
if (!ch.is_ascii()) {
68
str.push_back(ch);
69
}
70
}
71
72
// 均匀随机 k 排列: shuffle
73
int cnt = 0, n = str.size();
74
rand_cnt = std::min(n, rand_cnt);
75
for (int i = 0; i < rand_cnt; ++i) {
76
int j = i + std::rand() % (n-i);
77
std::swap(str[i], str[j]);
78
}
79
vector<Char> slice = vector<Char>(str.begin(), str.begin() + rand_cnt);
80
text.clear();
81
text.push_back(String(slice));
82
widths.push_back(rand_cnt * 2);
83
ifs.close();
84
}
85
86
int size() const {
87
return text.size();
88
}
89
90
const String &get(int line) const {
91
return text[line];
92
}
93
94
// 第 line 行第 col 字
95
Char get(size_t line, size_t col) const {
96
if (line < text.size() && col < text[line].size())
97
return text[line][col];
98
return Char();
99
}
100
101
int width(int line) {
102
return widths[line];
103
}
104
105
bool is_end(int line, int col) const {
106
return line == size() - 1 && col == get(line).size();
107
}
108
109
};
110
111
enum Status {
112
C1, // 英文正确
113
C2, // 中文正确
114
E1, // 英文错误
115
E2, // 中文错误
116
E3, // 误在英文处输入中文
117
E4, // 误在中文处输入英文
118
CR // 回车
119
};
120
enum Ctrl {NONE, CONTINUE, BREAK, RETURN};
121
122
class Game {
123
const char *filename;
124
int begin, end, rand_cnt;
125
bool need_restart;
126
int line, col, x;
127
int loaded_cnt;
128
struct { int correct = 0, error = 0, backspace = 0; } statistics;
129
stack<Status> undo;
130
Article article;
131
132
public:
133
Game(const char *filename, int begin = 1, int end = 0x7fffffff, int rand_cnt = -1):
134
filename(filename), begin(begin), end(end), rand_cnt(rand_cnt)
135
{
136
do {
137
restart();
138
} while (need_restart);
139
}
140
141
private:
142
void restart() {
143
this->line = 0;
144
this->col = 0;
145
this->x = 0;
146
this->loaded_cnt = 0;
147
this->need_restart = false;
148
this->statistics = { 0, 0, 0 };
149
150
setlocale(LC_ALL, ""); // 使用系统 locale
151
if (rand_cnt == -1) {
152
article.read(filename, begin, end);
153
} else {
154
article.rand(filename, rand_cnt);
155
}
156
157
print();
158
play();
159
}
160
161
// 在画面范围内打印
162
void print() {
163
screen_clear();
164
get_ttysize();
165
cout << STYLE_DIM;
166
if (loaded_cnt < article.size()) {
167
int h = height(loaded_cnt);
168
while (loaded_cnt < article.size() && h < tty.ws_row) {
169
cout << article.get(loaded_cnt++) << '\n';
170
h += height(loaded_cnt);
171
}
172
cursor_up(h - height(loaded_cnt));
173
}
174
cout << STYLE_RESET;
175
}
176
177
void play() {
178
toggle_flush();
179
int c = getchar();
180
long begin = time_ms();
181
control(c);
182
if (!need_restart) {
183
summary(begin);
184
}
185
toggle_flush();
186
}
187
188
static int width(const Char &c) {
189
return c.is_ascii() ? 1 : 2;
190
}
191
192
int width(int line) {
193
return article.width(line);
194
}
195
196
int height(int line) {
197
return width(line) / tty.ws_col + 1; // 记得刷新终端尺寸
198
}
199
200
void update() {
201
// 用户已打完的行数距离已打印行数小于 4 时更新一行
202
if (loaded_cnt < line + 4 && loaded_cnt < article.size()) {
203
get_ttysize();
204
cout << STYLE_DIM;
205
// 光标下移三行
206
int k = 0;
207
while (k < 3) cursor_down(height(line+(k++)));
208
// 打印新的一行
209
const int h = height(loaded_cnt);
210
cout << article.get(loaded_cnt++) << endl;
211
// 光标归位
212
cursor_up(h);
213
while (k) cursor_up(height(line+(--k)));
214
cout << STYLE_RESET;
215
}
216
}
217
218
void summary(long begin)
219
{
220
int s = time_ms() - begin;
221
int mm = s / 60000;
222
int ss = s / 1000;
223
int ms = s % 1000;
224
int typed = statistics.correct + statistics.error;
225
226
// 滚动到页尾
227
get_ttysize();
228
int sum = 0;
229
while (line < article.size() && sum < tty.ws_row) {
230
int h = height(line++);
231
cursor_down(h);
232
sum += h;
233
}
234
235
int count = 0;
236
int len = 0;
237
const int recent_count = 20;
238
float recent_speed[recent_count];
239
float avg = 0.0f;
240
fstream fs("typing.log", ios::in);
241
if (fs) {
242
// 存在则读取, 然后清空文件重新打开
243
fs >> count;
244
len = count < recent_count ? count : recent_count;
245
for (int i = 0; i < len; ++i) {
246
fs >> recent_speed[i];
247
}
248
fs.close();
249
fs.open("typing.log", ios::out | ios::trunc);
250
} else {
251
// 不存在则创建
252
fs.close();
253
fs.open("typing.log", ios::out);
254
}
255
256
float accuracy = 100.0 * statistics.correct / typed;
257
float speed = 600.0 * statistics.correct * accuracy / s;
258
fs << count + 1 << '\n';
259
for (int i = 0; i < len; ++i) {
260
avg += recent_speed[i];
261
// 最多只保留最近 recent_count 条速度记录, 然后取平均
262
if (i > 0 || len < recent_count) {
263
fs << recent_speed[i] << '\n';
264
}
265
}
266
avg = (avg + speed) / (len + 1);
267
fs << speed << endl;
268
fs.close();
269
++count;
270
271
printf(
272
"\r--------------------\n"
273
"lesson: %d\n"
274
"time: %dm %ds %dms\n"
275
"speed: %.2fkpm\n"
276
"avg_speed: %.2fkpm\n"
277
"accuracy: %.2f%%\n"
278
"typed: %d\n"
279
"correct: %d\n"
280
"backspace: %d\n",
281
count,
282
mm, ss, ms,
283
speed,
284
avg,
285
accuracy,
286
typed,
287
statistics.correct,
288
statistics.backspace
289
);
290
}
291
292
void control(int key) {
293
string buf;
294
Ctrl ctrl = NONE;
295
// ctrl-d to exit
296
while (key != 4) {
297
if (key == 127) {
298
ctrl = onBackspace();
299
} else if (key == 21) { // ctrl-u
300
need_restart = true;
301
return;
302
} else if (key == '\n') {
303
ctrl = onReturn();
304
} else {
305
Char answer = article.get(line, col);
306
if (answer) {
307
ctrl = onInput(key, buf, answer);
308
} else if (article.is_end(line, col)) {
309
// 跟打结束, 按任意键退出
310
ctrl = BREAK;
311
}
312
// else
313
// cout << "ignore: " << key << endl;
314
}
315
316
if (ctrl == CONTINUE) continue;
317
else if (ctrl == BREAK) break;
318
else if (ctrl == RETURN) return;
319
key = getchar();
320
}
321
}
322
323
Ctrl onBackspace() {
324
get_ttysize();
325
if (!undo.empty()) {
326
++statistics.backspace;
327
enum Status stat = undo.top(); undo.pop();
328
switch (stat) {
329
case C1: remove(true, 1); break;
330
case C2: remove(true, 2); break;
331
case E1: remove(false, 1); break;
332
case E2: case E4: remove(false, 2); break;
333
case E3: remove(false, 2, true); break;
334
case CR:
335
col = article.get(--line).size();
336
x = article.width(line);
337
cursor_up(1);
338
cursor_right(x % tty.ws_col);
339
break;
340
default: break;
341
}
342
}
343
return NONE;
344
}
345
346
Ctrl onReturn() {
347
if (!article.get(line, col)) {
348
undo.push(CR);
349
++statistics.correct;
350
++line;
351
col = 0;
352
x = 0;
353
putchar('\n');
354
update();
355
if (line == article.size())
356
return BREAK;
357
}
358
return NONE;
359
}
360
361
Ctrl onInput(char key, string &buf, const Char &answer) {
362
buf += key;
363
istringstream is(buf);
364
Char c;
365
if (is >> c) {
366
buf = "";
367
if (c == answer) {
368
cout << c;
369
add_correct(width(c));
370
} else {
371
cout << COLOR(FG_RED) << c << STYLE_RESET;
372
// cout << answer.ord;
373
add_error(width(c), width(answer));
374
}
375
x += width(c);
376
++col;
377
}
378
return NONE;
379
}
380
381
void add_correct(int w) {
382
if (w == 1)
383
undo.push(C1); // 英文正确
384
else
385
undo.push(C2); // 中文正确
386
++statistics.correct;
387
}
388
389
void add_error(int w, int w_ans) {
390
if (w == 1) {
391
if (w_ans == 1) {
392
undo.push(E1); // 英文错误
393
} else {
394
undo.push(E4); // 误在中文处输入英文
395
cout << ' '; //! 补齐中文宽度
396
}
397
} else {
398
if (w_ans == 1) {
399
undo.push(E3); // 误在英文处输入中文
400
++col; //! 额外多加1
401
} else {
402
undo.push(E2); // 中文错误
403
}
404
}
405
++statistics.error;
406
}
407
408
void remove(bool correct, int w, bool additional=false) {
409
if (correct)
410
--statistics.correct;
411
else
412
--statistics.error;
413
--col;
414
x -= w;
415
cursor_left(w);
416
cout << STYLE_DIM;
417
if (additional) {
418
cout << article.get(line, col-1) << article.get(line, col);
419
--col; //! 额外多减1
420
} else {
421
cout << article.get(line, col);
422
}
423
cout << STYLE_RESET;
424
cursor_left(w);
425
}
426
427
};
428
429
int main(int argc, char *argv[])
430
{
431
if (argc == 1) {
432
cout << "Typing practice. Choose your favorite article!\n"
433
"usage: typing [filename] [begin line] [end line]\n";
434
return 0;
435
}
436
//logger.open("typing.log", ios::out);
437
438
srand(time(NULL));
439
if (argc == 2) {
440
Game game(argv[1]);
441
} else if (argc == 3) {
442
if (strcmp(argv[2], "-r") == 0) {
443
Game game(argv[1], 1, 0x7fffffff, 10);
444
} else {
445
Game game(argv[1], atoi(argv[2]));
446
}
447
} else if (argc == 4) {
448
if (strcmp(argv[2], "-r") == 0) {
449
Game game(argv[1], 1, 0x7fffffff, atoi(argv[3]));
450
} else {
451
Game game(argv[1], atoi(argv[2]), atoi(argv[3]));
452
}
453
}
454
return 0;
455
}
456
457
458