Compare commits

..

10 Commits

Author SHA1 Message Date
Aidan Haas
023960695f
Fix end screen rolling off terminal 2023-10-25 18:25:56 -04:00
Aidan Haas
790a373637
Fixed 'stats' page crashing when compiled from source 2023-10-25 12:29:40 -04:00
Aidan Haas
00d9685b19
Update README.md 2023-01-08 21:15:00 -05:00
Aidan Haas
8a15966e65
Update README.md 2023-01-08 14:38:54 -05:00
Aidan Haas
ee19435715
Delete db 2023-01-05 12:07:29 -05:00
Aidan Haas
3d0a76afbf
Delete run 2023-01-05 12:07:16 -05:00
Aidan Haas
6ce73ab765
Milestone 2! Polished code, add settings menu 2023-01-05 12:04:51 -05:00
Aidan Haas
b737ac8375
Fix stats for timed tests 2023-01-04 13:33:51 -05:00
Aidan Haas
2489d55f0a
Add timed mode (unpolished) 2023-01-03 21:22:25 -05:00
Aidan Haas
bf32b63e0e
Add more stats, getting ready for release 2 2022-12-28 15:42:38 -05:00
8 changed files with 360 additions and 164 deletions

View File

@ -1,4 +1,4 @@
# Typing-Test-C (WIP)
# Typing-Test-C
<p align="center">
@ -33,6 +33,7 @@
* Download source files
* Install ncurses (If not already installed)
* On Debian Systems: ```sudo apt install libncurses5-dev libncursesw5-dev```
* Compile Using ```gcc typing_test.c utilities.c -lncurses```
* OR: Run ```make``` using Makefile
@ -55,19 +56,23 @@ Find it [here](https://github.com/ahaas25/Typing-Test-C/releases/tag/milestone)
Second release goal:
The second release goal for this project is to have a much more refined project. I hope to have all modes implemented, the UI to automatically wrap the console size, and user keystroke analysis if time permits. I'm hoping to have this release out by early January. (It's finals season so I won't have much time to work on this project until late December)
Second release goal reached! Due to time constraints, not all features were implemented in this release.
Find it [here](https://github.com/ahaas25/Typing-Test-C/releases/tag/milestone2)
Current status of project:
Code:
* Reading of words.txt *implemented*
* Random word picking *implemented*
* Different test lengths *implemented*
* Timed Tests to be implemented
* Timed Tests *implemented*
* Test results screen *implemeted*
* Saving user stats to be implemented
* Per-key stats to be implemented
* Saving user stats *implemented*
* ~~Per-key stats~~ May be added in a feature release
UI:
* Main menu *implemented*
* Typing prompt *in progress*
* Stats menu to be implemented
* Settings menu to be implemented
* Typing prompt *implemented*
* Stats menu *implemented*
* Settings menu *implemented*
* Themes

2
db
View File

@ -1,2 +0,0 @@
gcc typing_test.c utilities.c -lncurses -g
gdb a.out

View File

@ -4,6 +4,7 @@
#include <string.h>
#include <ncurses.h>
#include <sys/time.h>
#include <time.h>
#include "utilities.h"
#include "typing_test.h"
@ -31,15 +32,14 @@ void debug_words() {
}
void debug_stats() {
int i;
FILE *stats_file;
Stat_struct x;
char buf[256], temp[256];
int temp_stat;
stats_file = fopen("stats", "r");
load_stats(stats_file, &x);
for (int i = 0; i < 9; i++) {
for (i = 0; i < 9; i++) {
printf("%d\n", x.data[i]);
}
@ -47,5 +47,6 @@ void debug_stats() {
int main() {
debug_stats();
return 0;
}

2
run
View File

@ -1,2 +0,0 @@
gcc typing_test.c -lncurses -g
./a.out

View File

@ -38,6 +38,20 @@ void print_centered_text_menu(WINDOW *win, int row, int target, char str[][MAX_S
}
}
/* Highlights text if cursor points to the col */
void print_centered_text_menu_single(WINDOW *win, int row, int target, char str[]) {
int text_length = strlen(str), cursor;
cursor = (win->_maxx - text_length) / 2;
move(row, cursor);
if (target == row) {
attron(A_STANDOUT);
}
printw(str);
attroff(A_STANDOUT);
}
/* Prints the typing prompt onto the terminal.
Automatically centers, wraps, and scrolls through text */
void print_typing_prompt(WINDOW *win, Word_array *prompt, char *prompt_string,
@ -71,41 +85,49 @@ void print_typing_prompt(WINDOW *win, Word_array *prompt, char *prompt_string,
}
void typing_ui(WINDOW *win, int level, int mode, Word_array *word_array, Stat_struct *stats) {
int run = 1, ch, i, new_test = 1, user_input_length, start_timer;
double test_time, wpm, accuracy;
int run = 1, ch, i, new_test = 1, user_input_length, start_timer, line = 4, end_test, row;
int timed_reserved_accuracy, timed_reserved_characters;
double test_time, wpm, wpm_raw, accuracy, elapsed_time;
long long time_start, time_stop, misc;
char str[1024], temp[24], *user_input = NULL;
char *prompt_string = NULL; /* Full prompt string */
Word_array *prompt = NULL;
Word *prompt_lines; /* Array of lines for displaying */
struct timeval timer_start, timer_stop;
struct timeval timer_start, timer_stop, test_end;
while (run) {
if (new_test == 1) {
if (new_test == 1 || new_test == 2) {
/* Reset Vars */
clear();
accuracy = 0;
curs_set(1);
if (new_test == 1) {
timed_reserved_accuracy = 0;
timed_reserved_characters = 0;
}
/* Reset str */
str[0] = '\0';
if (mode == 0) {
strcat(str, "Timed Test - ");
strcat(str, TIMED_MODES_STRING[level]);
print_centered_text(stdscr, 0, str);
} else {
strcat(str, "Word Test - ");
strcat(str, WORD_MODES_STRING[level]);
print_centered_text(stdscr, 0, str);
}
if (prompt == NULL) {
prompt = malloc(sizeof(Word_array));
} else {
clear_word_array(prompt);
}
/* Only works for words mode for now, no timed */
generate_words(WORD_MODES[level], word_array, prompt);
/* Draw UI */
if (mode == 0) {
strcat(str, "Timed Test - ");
strcat(str, TIMED_MODES_STRING[level]);
print_centered_text(stdscr, 0, str);
end_test = 0;
generate_words(WORD_MODES[2], word_array, prompt);
} else {
strcat(str, "Word Test - ");
strcat(str, WORD_MODES_STRING[level]);
print_centered_text(stdscr, 0, str);
generate_words(WORD_MODES[level], word_array, prompt);
}
if (user_input != NULL) {
free(user_input);
@ -115,8 +137,6 @@ void typing_ui(WINDOW *win, int level, int mode, Word_array *word_array, Stat_st
user_input[0] = '\0';
user_input_length = 0;
new_test = 0;
prompt_string = malloc(prompt->num_characters); /* For easier parsing of prompt */
prompt_string[0] = '\0';
@ -131,10 +151,21 @@ void typing_ui(WINDOW *win, int level, int mode, Word_array *word_array, Stat_st
printf("%s", user_input);
print_typing_prompt(win, prompt, prompt_string, user_input);
start_timer = 1; /* Flag timer as ready to be started */
if (new_test == 1) {
start_timer = 1; /* Flag timer as ready to be started */
timed_reserved_accuracy = 0;
timed_reserved_characters = 0;
} else {
start_timer = 0;
}
new_test = 0;
}
ch = getch();
if (isalnum(ch) || ch == ' ' || ch == '\'') { /* If typing prompt character */
if (start_timer) {
gettimeofday(&timer_start, NULL);
@ -167,109 +198,191 @@ void typing_ui(WINDOW *win, int level, int mode, Word_array *word_array, Stat_st
}
user_input_length = strlen(user_input);
if (user_input_length == strlen(prompt_string) && user_input[user_input_length - 1] == prompt_string[user_input_length - 1]) {
gettimeofday(&timer_stop, NULL);
/* Convert time stamps to miliseconds */
long long time_start = timer_start.tv_sec * 1000LL + timer_start.tv_usec / 1000;
long long time_stop = timer_stop.tv_sec * 1000LL + timer_stop.tv_usec / 1000;
/* Calculate accuracy */
accuracy = (accuracy / user_input_length) * 100;
/* If timed test, see if test is still going */
/* Test if last keystroke is ignored */
if (mode == 0) {
gettimeofday(&test_end, NULL);
/* Subtract starting time from ending time to get elapsed time */
test_time = time_stop - time_start;
test_time /= 1000; /* Convert time into seconds */
time_start = timer_start.tv_sec * 1000LL + timer_start.tv_usec / 1000;
misc = test_end.tv_sec * 1000LL + test_end.tv_usec / 1000;
/* WPM = (Total Chars / 5) /(Total Mins) */
wpm = ((double)user_input_length / 5) / (test_time / 60);
elapsed_time = misc - time_start; /* Calculates elapsed time of test in seconds */
elapsed_time /= 1000;
clear();
/* Checks if test is over */
if (elapsed_time >= TIMED_MODES[level]) {
end_test = 1;
/* Ignores last keystroke */
user_input_length--;
accuracy--;
/* Print end screen */
print_centered_text(stdscr, 0, str); /* Print test type */
curs_set(0);
print_centered_text(win, 2, "Test Complete!");
/* Note, console resizing support is planned. */
/* Generate WPM String */
str[0] = '\0';
append_line("WPM: ", str);
gcvt(wpm, 5, temp);
append_line(temp, str);
print_centered_text(win, 4, str);
/* Generate Accuracy String */
str[0] = '\0';
append_line("Accuracy: ", str);
gcvt(accuracy, 5, temp);
append_line(temp, str);
append_line("\%%", str);
print_centered_text(win, 5, str);
/* Generate Time String */
str[0] = '\0';
append_line("Time: ", str);
gcvt(test_time, 5, temp);
append_line(temp, str);
append_line("s", str);
print_centered_text(win, 6, str);
/* Generate Characters String */
str[0] = '\0';
append_line("Characters: ", str);
sprintf(temp, "%d", user_input_length);
append_line(temp, str);
print_centered_text(win, 7, str);
print_centered_text(win, 19, "Press tab to start a new test, or any key to return to main menu");
stats->data[TESTS_COMPLETE]++;
switch (level) {
case 0:
if (stats->data[W_5] < (int)wpm) {
stats->data[W_5] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
}
break;
case 1:
if (stats->data[W_10] < (int)wpm) {
stats->data[W_10] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
}
break;
case 2:
if (stats->data[W_25] < (int)wpm) {
stats->data[W_25] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
}
break;
case 3:
if (stats->data[W_50] < (int)wpm) {
stats->data[W_50] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
}
break;
case 4:
if (stats->data[W_100] < (int)wpm) {
stats->data[W_100] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
}
break;
}
/* Wait on user input */
ch = getch();
if (ch != ' ') {
run = 0;
clear();
} else {
new_test = 1;
row = 4;
print_centered_text(win, row++, "Time's up!");
print_centered_text(win, row++, "Press Enter to See Results");
print_centered_text(win, row++, "Or Tab to start a new test");
while (1) {
ch = getch();
if (ch == '\n') {
break;
} else if (ch == 27) { /* If esc, end and do not save results */
run = 0;
end_test = 0;
break;
} else if (ch == ' ') {
new_test = 1;
end_test = 0;
break;
}
}
}
elapsed_time = 0;
}
/* End of test */
if ((user_input_length == strlen(prompt_string) && user_input[user_input_length - 1] == prompt_string[user_input_length - 1]) || (mode == 0 && end_test)) {
if (mode == 0 && !end_test) {
/* If timed test is not over yet */
timed_reserved_accuracy += accuracy;
timed_reserved_characters += user_input_length;
new_test = 2;
} else {
/* If end of test screen should be shown */
if (mode == 0) {
/* If finished test was a timed test */
accuracy += timed_reserved_accuracy;
user_input_length += timed_reserved_characters;
test_time = TIMED_MODES[level];
} else {
/* If finished test was a word test */
/* Convert time stamps to miliseconds */
gettimeofday(&timer_stop, NULL);
time_start = timer_start.tv_sec * 1000LL + timer_start.tv_usec / 1000;
time_stop = timer_stop.tv_sec * 1000LL + timer_stop.tv_usec / 1000;
/* Subtract starting time from ending time to get elapsed time */
test_time = time_stop - time_start;
test_time /= 1000; /* Convert time into seconds */
}
/* WPM = (Total Chars / 5) /(Total Mins) */
wpm = (accuracy / 5) / (test_time / 60);
wpm_raw = ((double)user_input_length / 5) / (test_time / 60);
/* Update stats */
stats->data[TESTS_COMPLETE]++;
stats->data[CHARS_TYPED] += user_input_length;
stats->data[CHARS_CORRECT] += accuracy;
stats->data[TIME_TYPED] += (test_time * 10);
/* Multiply by 10 to more precision */
/* Calculate accuracy */
accuracy = (accuracy / user_input_length) * 100;
clear();
/* Print end screen */
line = 4;
print_centered_text(stdscr, 0, str); /* Print test type */
curs_set(0);
print_centered_text(win, 2, "Test Complete!");
/* Note, console resizing support is planned. */
/* Generate WPM String */
str[0] = '\0';
append_line("WPM: ", str);
gcvt(wpm, 5, temp);
append_line(temp, str);
print_centered_text(win, line++, str);
str[0] = '\0';
append_line("Raw WPM: ", str);
gcvt(wpm_raw, 5, temp);
append_line(temp, str);
print_centered_text(win, line++, str);
/* Generate Accuracy String */
str[0] = '\0';
append_line("Accuracy: ", str);
gcvt(accuracy, 5, temp);
append_line(temp, str);
append_line("\%%", str);
print_centered_text(win, line++, str);
/* Generate Time String */
str[0] = '\0';
append_line("Time: ", str);
gcvt(test_time, 5, temp);
append_line(temp, str);
append_line("s", str);
print_centered_text(win, line++, str);
/* Generate Characters String */
str[0] = '\0';
append_line("Characters: ", str);
sprintf(temp, "%d", user_input_length);
append_line(temp, str);
print_centered_text(win, line++, str);
print_centered_text(win, 19, "Press tab to start a new test, or any key to return to main menu");
/* Stats are only for word mode */
if (mode == 1) {
switch (level) {
case 0:
if (stats->data[W_5] < (int)wpm) {
stats->data[W_5] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
update_max_wpm(stats);
}
break;
case 1:
if (stats->data[W_10] < (int)wpm) {
stats->data[W_10] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
update_max_wpm(stats);
}
break;
case 2:
if (stats->data[W_25] < (int)wpm) {
stats->data[W_25] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
update_max_wpm(stats);
}
break;
case 3:
if (stats->data[W_50] < (int)wpm) {
stats->data[W_50] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
update_max_wpm(stats);
}
break;
case 4:
if (stats->data[W_100] < (int)wpm) {
stats->data[W_100] = (int)wpm;
print_centered_text(win, 17, "New High Score!");
update_max_wpm(stats);
}
break;
}
}
/* Wait on user input */
ch = getch();
if (ch != ' ') {
run = 0;
clear();
} else {
new_test = 1;
}
}
} else {
/* Prints typing prompt after all input is done processing */
print_typing_prompt(win, prompt, prompt_string, user_input);
@ -282,25 +395,70 @@ void typing_ui(WINDOW *win, int level, int mode, Word_array *word_array, Stat_st
/* Draws settings UI to console */
/* Placeholder, will work on next */
void settings_ui(WINDOW *win) {
char ch;
clear();
print_centered_text(win, 0, "Settings");
void settings_ui(WINDOW *win, Stat_struct *stats) {
FILE *stats_file;
int selection = 13, row = 0, count = 2, run = 1;
int selections[] = { 8, 11, 13 };
int ch;
print_centered_text(win, 3, "Controls");
print_centered_text(win, 4, "Tab - Reset Test");
print_centered_text(win, 5, "Esc - End Test");
while (run) {
clear();
row = 0;
print_centered_text(win, row, "Settings");
print_centered_text(win, 7, "Themes");
print_centered_text(win, 8, "Default");
row += 3;
print_centered_text(win, row++, "Controls");
print_centered_text(win, row++, "Tab - Reset Test");
print_centered_text(win, row, "Esc - End Test");
print_centered_text(win, 10, "Statistics");
print_centered_text(win, 11, "Reset Stats");
row += 2;
print_centered_text(win, 13, "Return to Menu");
refresh();
print_centered_text(win, row++, "Themes");
print_centered_text_menu_single(win, row, selection, "Default");
row += 2;
print_centered_text(win, row++, "Statistics");
print_centered_text_menu_single(win, row, selection, "Reset Stats");
row += 2;
print_centered_text_menu_single(win, row, selection, "Return to Menu");
refresh();
ch = getch();
switch (ch) {
case KEY_UP:
if (count == 0) {
count = 2;
} else {
count--;
}
break;
case KEY_DOWN:
if (count < 2) {
count++;
} else {
count = 0;
}
break;
case '\n':
if (count == 2) { /* Exit menu */
run = 0;
} else if (count == 1) { /* Clear stats */
stats_file = fopen(STATS_FILEPATH, "w");
create_stats_file(stats_file);
stats_file = fopen(STATS_FILEPATH, "r");
load_stats(stats_file, stats);
}
}
selection = selections[count];
move(0, 0);
printw("The count is: %d", count);
refresh();
}
ch = getchar();
clear();
}
@ -308,6 +466,7 @@ void settings_ui(WINDOW *win) {
/* Placeholder, will work on next */
void stat_ui(WINDOW *win, Stat_struct *stats) {
int row = 0;
double temp;
char ch, temp_str[MAX_STRING], temp_num[MAX_STRING];
clear();
@ -317,72 +476,91 @@ void stat_ui(WINDOW *win, Stat_struct *stats) {
row += 3;
append_line("Best WPM: ", temp_str);
gcvt(stats->data[BEST_WPM], 5, temp_num);
gcvt((double)stats->data[BEST_WPM], 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("Average WPM: ", temp_str);
/* Calculate here */
temp = (((double)stats->data[CHARS_CORRECT]) / 5) / (((double)stats->data[TIME_TYPED]) / 600);
gcvt(temp, 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("Average Accuracy: ", temp_str);
temp = ((double)stats->data[CHARS_CORRECT] / (double)stats->data[CHARS_TYPED]) * 100;
gcvt(temp, 5, temp_num);
append_line(temp_num, temp_str);
append_line("\%%", temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("Tests Completed: ", temp_str);
gcvt(stats->data[TESTS_COMPLETE], 5, temp_num);
gcvt((double)stats->data[TESTS_COMPLETE], 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("Keys Typed: ", temp_str);
gcvt((double)stats->data[CHARS_TYPED], 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("Time Spent Typing: ", temp_str);
gcvt(stats->data[TIME_TYPED], 5, temp_num);
gcvt((double)stats->data[TIME_TYPED] / 600, 5, temp_num);
append_line(temp_num, temp_str);
append_line(" min", temp_str);
print_centered_text(win, row, temp_str);
row += 2;
temp_str[0] = '\0';
append_line("5 Word Test: ", temp_str);
gcvt(stats->data[W_5], 5, temp_num);
gcvt((double)stats->data[W_5], 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("10 Word Test: ", temp_str);
gcvt(stats->data[W_10], 5, temp_num);
gcvt((double)stats->data[W_10], 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("25 Word Test: ", temp_str);
gcvt(stats->data[W_25], 5, temp_num);
gcvt((double)stats->data[W_25], 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("50 Word Test: ", temp_str);
gcvt(stats->data[W_50], 5, temp_num);
gcvt((double)stats->data[W_50], 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
temp_str[0] = '\0';
append_line("100 Word Test: ", temp_str);
gcvt(stats->data[W_100], 5, temp_num);
gcvt((double)stats->data[W_100], 5, temp_num);
append_line(temp_num, temp_str);
print_centered_text(win, row, temp_str);
row++;
row += 2;
temp_str[0] = '\0';
print_centered_text(win, 15, "Return to Menu");
print_centered_text(win, row, "Return to Menu");
refresh();
ch = getchar();
clear();
}
@ -401,8 +579,8 @@ int main() {
noecho();
print_centered_text(stdscr, 4, "Loading...");
words_file = fopen("words.txt", "r");
stats_file = fopen("stats", "r");
words_file = fopen(WORDS_FILEPATH, "r");
stats_file = fopen(STATS_FILEPATH, "r");
if (has_colors() == FALSE) {
print_centered_text(stdscr, 4, "Your terminal does not support color");
@ -432,9 +610,9 @@ int main() {
/* Creates a new stats file if one is not detected */
if (stats_file == NULL) {
stats_file = fopen("stats", "w");
stats_file = fopen(STATS_FILEPATH, "w");
create_stats_file(stats_file);
stats_file = fopen("stats", "r");
stats_file = fopen(STATS_FILEPATH, "r");
}
/* Load stats from stats file */
@ -499,7 +677,7 @@ int main() {
stat_ui(stdscr, &stats);
} else if (cursor_x == 2 && cursor_y == 2) {
/* Settings */
settings_ui(stdscr);
settings_ui(stdscr, &stats);
} else if (cursor_y == 1 || cursor_y == 0) {
typing_ui(stdscr, cursor_x, cursor_y, word_array, &stats);
clear();
@ -514,7 +692,7 @@ int main() {
/* Saves stats and closes file */
/* Open stats first */
stats_file = fopen("stats", "w");
stats_file = fopen(STATS_FILEPATH, "w");
save_stats(stats_file, &stats);
return 0;

View File

@ -14,6 +14,8 @@
#define NUM_MISC 3
#define SUCCESS 1
#define FAILURE 0
#define WORDS_FILEPATH "words.txt"
#define STATS_FILEPATH "stats"
char TIMED_MODES_STRING[5][MAX_STRING] = { "5s", "10s", "25s", "30s", "60s" };
int TIMED_MODES[5] = {5, 10, 25, 30, 60};

View File

@ -107,12 +107,25 @@ int save_stats(FILE *stats_file, Stat_struct *stats) {
int i;
for (i = 0; i < NUM_STATS; i++) {
fprintf(stats_file, "%f\n", stats->data[i]);
fprintf(stats_file, "%d\n", stats->data[i]);
}
fclose(stats_file);
return SUCCESS;
}
/* Finds highest WPM score */
int update_max_wpm(Stat_struct *stats) {
int i, max = stats->data[W_5];
for (i = W_5; i <= W_100; i++) {
if (max < stats->data[i]) {
max = stats->data[i];
}
}
stats->data[BEST_WPM] = max;
return SUCCESS;
}
/* Appends first parameter to the end of end of the second parameter
(Does not change size of the target, assumes there is space) */

View File

@ -37,7 +37,7 @@ typedef struct {
} Word_array;
typedef struct {
double data[NUM_STATS];
int data[NUM_STATS];
} Stat_struct;
void clear_word_array(Word_array *array);
@ -46,4 +46,5 @@ int parse_words_file(FILE *words_file, Word_array *words);
int create_stats_file(FILE *stats_file);
int load_stats(FILE *stats_file, Stat_struct *stats);
int save_stats(FILE *stats_file, Stat_struct *stats);
void append_line(char *source, char *target);
void append_line(char *source, char *target);
int update_max_wpm(Stat_struct *stats);