#include "game.h"
#include "str.h"
#include <clocale>
#include <cctype>
#include <cstring>
#include <ctime>
#include <vector>
#include <stack>
#include <iostream>
#include <fstream>
using namespace std;
class Article {
vector<String> text;
vector<int> widths;
public:
void read_stream(const string &filename, ifstream &ifs) {
ifs.open(filename, ios::in);
if (!ifs) {
cerr << "error: cannot open file '" << filename << "'. "
"does this file exist?\n";
exit(1);
}
}
void read(const string &filename, int begin = 1, int end = 0x7fffffff) {
ifstream ifs;
read_stream(filename, ifs);
String line;
text.clear();
for (int i = 1; getline(ifs, line); ++i) {
if (i < begin) continue;
else if (i > end) break;
text.push_back(line);
int w = 0;
for (size_t i = 0; i < line.size(); ++i) {
if (line[i].is_ascii())
w += 1;
else
w += 2;
}
if (line.size() == 0)
w = 1;
widths.push_back(w);
}
ifs.close();
}
void rand(const string &filename, int rand_cnt = 10) {
ifstream ifs;
read_stream(filename, ifs);
Char ch;
vector<Char> str;
while (ifs >> ch) {
if (!ch.is_ascii()) {
str.push_back(ch);
}
}
int cnt = 0, n = str.size();
rand_cnt = std::min(n, rand_cnt);
for (int i = 0; i < rand_cnt; ++i) {
int j = i + std::rand() % (n-i);
std::swap(str[i], str[j]);
}
vector<Char> slice = vector<Char>(str.begin(), str.begin() + rand_cnt);
text.clear();
text.push_back(String(slice));
widths.push_back(rand_cnt * 2);
ifs.close();
}
int size() const {
return text.size();
}
const String &get(int line) const {
return text[line];
}
Char get(size_t line, size_t col) const {
if (line < text.size() && col < text[line].size())
return text[line][col];
return Char();
}
int width(int line) {
return widths[line];
}
bool is_end(int line, int col) const {
return line == size() - 1 && col == get(line).size();
}
};
enum Status {
C1,
C2,
E1,
E2,
E3,
E4,
CR
};
enum Ctrl {NONE, CONTINUE, BREAK, RETURN};
class Game {
const char *filename;
int begin, end, rand_cnt;
bool need_restart;
int line, col, x;
int loaded_cnt;
struct { int correct = 0, error = 0, backspace = 0; } statistics;
stack<Status> undo;
Article article;
public:
Game(const char *filename, int begin = 1, int end = 0x7fffffff, int rand_cnt = -1):
filename(filename), begin(begin), end(end), rand_cnt(rand_cnt)
{
do {
restart();
} while (need_restart);
}
private:
void restart() {
this->line = 0;
this->col = 0;
this->x = 0;
this->loaded_cnt = 0;
this->need_restart = false;
this->statistics = { 0, 0, 0 };
setlocale(LC_ALL, "");
if (rand_cnt == -1) {
article.read(filename, begin, end);
} else {
article.rand(filename, rand_cnt);
}
print();
play();
}
void print() {
screen_clear();
get_ttysize();
cout << STYLE_DIM;
if (loaded_cnt < article.size()) {
int h = height(loaded_cnt);
while (loaded_cnt < article.size() && h < tty.ws_row) {
cout << article.get(loaded_cnt++) << '\n';
h += height(loaded_cnt);
}
cursor_up(h - height(loaded_cnt));
}
cout << STYLE_RESET;
}
void play() {
toggle_flush();
int c = getchar();
long begin = time_ms();
control(c);
if (!need_restart) {
summary(begin);
}
toggle_flush();
}
static int width(const Char &c) {
return c.is_ascii() ? 1 : 2;
}
int width(int line) {
return article.width(line);
}
int height(int line) {
return width(line) / tty.ws_col + 1;
}
void update() {
if (loaded_cnt < line + 4 && loaded_cnt < article.size()) {
get_ttysize();
cout << STYLE_DIM;
int k = 0;
while (k < 3) cursor_down(height(line+(k++)));
const int h = height(loaded_cnt);
cout << article.get(loaded_cnt++) << endl;
cursor_up(h);
while (k) cursor_up(height(line+(--k)));
cout << STYLE_RESET;
}
}
void summary(long begin)
{
int s = time_ms() - begin;
int mm = s / 60000;
int ss = s / 1000;
int ms = s % 1000;
int typed = statistics.correct + statistics.error;
get_ttysize();
int sum = 0;
while (line < article.size() && sum < tty.ws_row) {
int h = height(line++);
cursor_down(h);
sum += h;
}
int count = 0;
int len = 0;
const int recent_count = 20;
float recent_speed[recent_count];
float avg = 0.0f;
fstream fs("typing.log", ios::in);
if (fs) {
fs >> count;
len = count < recent_count ? count : recent_count;
for (int i = 0; i < len; ++i) {
fs >> recent_speed[i];
}
fs.close();
fs.open("typing.log", ios::out | ios::trunc);
} else {
fs.close();
fs.open("typing.log", ios::out);
}
float accuracy = 100.0 * statistics.correct / typed;
float speed = 600.0 * statistics.correct * accuracy / s;
fs << count + 1 << '\n';
for (int i = 0; i < len; ++i) {
avg += recent_speed[i];
if (i > 0 || len < recent_count) {
fs << recent_speed[i] << '\n';
}
}
avg = (avg + speed) / (len + 1);
fs << speed << endl;
fs.close();
++count;
printf(
"\r--------------------\n"
"lesson: %d\n"
"time: %dm %ds %dms\n"
"speed: %.2fkpm\n"
"avg_speed: %.2fkpm\n"
"accuracy: %.2f%%\n"
"typed: %d\n"
"correct: %d\n"
"backspace: %d\n",
count,
mm, ss, ms,
speed,
avg,
accuracy,
typed,
statistics.correct,
statistics.backspace
);
}
void control(int key) {
string buf;
Ctrl ctrl = NONE;
while (key != 4) {
if (key == 127) {
ctrl = onBackspace();
} else if (key == 21) {
need_restart = true;
return;
} else if (key == '\n') {
ctrl = onReturn();
} else {
Char answer = article.get(line, col);
if (answer) {
ctrl = onInput(key, buf, answer);
} else if (article.is_end(line, col)) {
ctrl = BREAK;
}
}
if (ctrl == CONTINUE) continue;
else if (ctrl == BREAK) break;
else if (ctrl == RETURN) return;
key = getchar();
}
}
Ctrl onBackspace() {
get_ttysize();
if (!undo.empty()) {
++statistics.backspace;
enum Status stat = undo.top(); undo.pop();
switch (stat) {
case C1: remove(true, 1); break;
case C2: remove(true, 2); break;
case E1: remove(false, 1); break;
case E2: case E4: remove(false, 2); break;
case E3: remove(false, 2, true); break;
case CR:
col = article.get(--line).size();
x = article.width(line);
cursor_up(1);
cursor_right(x % tty.ws_col);
break;
default: break;
}
}
return NONE;
}
Ctrl onReturn() {
if (!article.get(line, col)) {
undo.push(CR);
++statistics.correct;
++line;
col = 0;
x = 0;
putchar('\n');
update();
if (line == article.size())
return BREAK;
}
return NONE;
}
Ctrl onInput(char key, string &buf, const Char &answer) {
buf += key;
istringstream is(buf);
Char c;
if (is >> c) {
buf = "";
if (c == answer) {
cout << c;
add_correct(width(c));
} else {
cout << COLOR(FG_RED) << c << STYLE_RESET;
add_error(width(c), width(answer));
}
x += width(c);
++col;
}
return NONE;
}
void add_correct(int w) {
if (w == 1)
undo.push(C1);
else
undo.push(C2);
++statistics.correct;
}
void add_error(int w, int w_ans) {
if (w == 1) {
if (w_ans == 1) {
undo.push(E1);
} else {
undo.push(E4);
cout << ' ';
}
} else {
if (w_ans == 1) {
undo.push(E3);
++col;
} else {
undo.push(E2);
}
}
++statistics.error;
}
void remove(bool correct, int w, bool additional=false) {
if (correct)
--statistics.correct;
else
--statistics.error;
--col;
x -= w;
cursor_left(w);
cout << STYLE_DIM;
if (additional) {
cout << article.get(line, col-1) << article.get(line, col);
--col;
} else {
cout << article.get(line, col);
}
cout << STYLE_RESET;
cursor_left(w);
}
};
int main(int argc, char *argv[])
{
if (argc == 1) {
cout << "Typing practice. Choose your favorite article!\n"
"usage: typing [filename] [begin line] [end line]\n";
return 0;
}
srand(time(NULL));
if (argc == 2) {
Game game(argv[1]);
} else if (argc == 3) {
if (strcmp(argv[2], "-r") == 0) {
Game game(argv[1], 1, 0x7fffffff, 10);
} else {
Game game(argv[1], atoi(argv[2]));
}
} else if (argc == 4) {
if (strcmp(argv[2], "-r") == 0) {
Game game(argv[1], 1, 0x7fffffff, atoi(argv[3]));
} else {
Game game(argv[1], atoi(argv[2]), atoi(argv[3]));
}
}
return 0;
}