-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtttb.cpp
More file actions
3339 lines (2901 loc) · 132 KB
/
tttb.cpp
File metadata and controls
3339 lines (2901 loc) · 132 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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* C++ Version of Type Through the Bible
By Kenneth Burchfiel
Released under the MIT License
Link to project's GitHub page:
https://github.com/kburchfiel/cpp_tttb
Note: As with my other GitHub projects, I chose not to use
generative AI tools when creating Type Through the Bible.
I wanted to learn how to
perform these tasks in C++ (or in a C++ library) rather than
simply learn how to get an AI tool to create them.
This code also makes extensive use of the following
open-source libraries:
1. Vincent La's CSV parser (https://github.com/vincentlaucsb/csv-parser)
2. CPP-Terminal (https://github.com/jupyter-xeus/cpp-terminal)
In addition, this program uses the Catholic Public Domain Version
of the Bible that Ronald L. Conte put together. This Bible can be
found at https://sacredbible.org/catholic/ I last updated my local
copy of this Bible (whose text does get updated periodically)
around June 10, 2025.
Some relevant notes:
1. I found that, when I tried to mix std::cin and Term::cin,
certain user prompts would work fine the first time around but
then fail again. I think that this may be because the cpp-terminal
library changes default cin settings; for reference,
see https://github.com/jupyter-xeus/cpp-terminal/issues/320 .
To avoid this issue, I simply replaced all std::cin and std::cout
calls with Term::cin and Term::cout, respectively.
However, I then found that Term::cin calls didn't always work
within OSX following typing tests; therefore, I ended up updating
the game so that all input was entered within raw_mode.
2. It also appears that the cpp-terminal library requires
std::endl or std::flush at the end of Term::cout statements rather than '\n';
otherwise, output may not appear.
3. References to 'PPP3' refer to the 3rd Edition of
// Programming: Principles and Practice Using C++
// by Bjarne Stroustrup.
This game is dedicated to my wife, Allie. I am very grateful
for her patience and understanding as I worked to put it
together! My multiplayer gameplay sessions with her (yes, she
was kind enough to play it with me) also helped me refine the
code and improve the OSX release.
Blessed Carlo Acutis, pray for us!
*/
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <map>
#include <memory>
#include <numeric>
#include <random>
#include <utility>
#include <vector>
#include "cpp-terminal/color.hpp"
#include "cpp-terminal/cursor.hpp"
#include "cpp-terminal/exception.hpp"
#include "cpp-terminal/input.hpp"
#include "cpp-terminal/iostream.hpp"
#include "cpp-terminal/key.hpp"
#include "cpp-terminal/options.hpp"
#include "cpp-terminal/screen.hpp"
#include "cpp-terminal/terminal.hpp"
#include "cpp-terminal/tty.hpp"
#include "cpp-terminal/version.hpp"
#include "csv.hpp"
using namespace csv;
// Storing codes that correspond to 16 different background
// colors within the ANSI escape code system:
// This code is based on the hello world example at
// https://github.com/jupyter-xeus/cpp-terminal and on the
// ANSI escape sequence color reference at
// https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit .
std::vector<std::string> background_color_codes{/*Bright Red*/ "101",
/*Bright Green*/ "102",
/*Bright Blue*/ "104",
/*Bright Yellow*/ "103",
/*Bright Magenta*/ "105",
/*Bright Cyan*/ "106",
/*Bright Black*/ "100",
/*Bright White*/ "107",
/*Red*/ "41",
/*Green*/ "42",
/*Blue*/ "44",
/*Yellow*/ "43",
/*Magenta*/ "45",
/*Cyan*/ "46",
/*Black*/ "40",
/*White*/ "47"};
// Specifying prefixes and suffixes that will precede and proceed
// these background color codes, respectively:
std::string background_color_prefix = "\033[37;"; //
// The ANSI escape code for white foreground text.
std::string background_color_suffix = "m \033[0m";
// Four spaces
// (which will give the background color space to appear)
// followed by an ANSI escape code that restores default color
// settings)
std::string all_verses_typed_message = "All verses have already \
been typed at least once! try another choice (such as i for a \
non-marathon-mode session or s for a marathon-mode session).";
std::string verses_file_path = "../Files/CPDB_for_TTTB.csv";
std::string autosaved_verses_file_path = "../Files/\
autosaved_CPDB_for_TTTB.csv";
// Defining colors that represent correct and incorrect output:
// (This code is based on the Hello World example for
// cpp-terminal at https://github.com/jupyter-xeus/cpp-terminal .)
// A full list of color names can be found at
// https://jupyter-xeus.github.io/cpp-terminal/cpp-terminal_Manual.pdf .
std::string correct_output_color_code =
Term::color_fg(Term::Color::Name::Green);
std::string incorrect_output_color_code =
Term::color_fg(Term::Color::Name::Magenta); // Switched from Red to Magenta
// to make the game more accessible to colorblind players.
// (Special thanks to David Nichols for his excellent
// colorblindness reference at
// https://davidmathlogic.com/colorblind -- and for suggesting
// magenta as an alternative to green.)
std::string default_output_color_code =
Term::color_fg(Term::Color::Name::Default);
std::string print_color_code = default_output_color_code; // This
// variable will get updated within typing tests to reflect either
// correct or incorrect output.
/* Defining a struct that can represent each row
of CPDB_for_TTTB.csv (the .csv file containing all
Bible verses:) */
struct Verse_Row {
int verse_id;
std::string ot_nt;
std::string book;
int book_num;
std::string chapter_num; // This is actually a string, since
// some values are 'P' (for 'prologue').
int verse_num;
std::string verse_code;
std::string verse;
int characters;
int tests; // Will store how many times the user has typed
// a given verse.
double best_wpm;
};
// Defining a struct that can store relevant test result data:
// (Since this data will be used to populate all rows within
// the test results CSV file, it will need to include all
// of the rows within that file as well. However, not all of
// the attributes stored here will actually end up within
// that file.)
struct Test_Result_Row {
long test_number; // Stores the total number of tests the player
// has completed so far.
long session_number; // Stores the total number of
// sessions the player has completed so far (e.g. the
// total number of times the player has launched a
// single-player game).
int within_session_test_number; // Stores the total number of
// tests completed by the player during the current session.
long unix_test_start_time;
std::string local_test_start_time;
long unix_test_end_time;
std::string local_test_end_time;
int verse_id;
std::string verse_code;
std::string verse; // In rare occasions, corrections might be
// made to a given verse. (For instance, I replaced double
// spaces in the original CPDB text with single spaces.)
// Therefore, it will be helpful to keep a record of the verse
// that the user actually typed, even though this will make
// the test results file considerably larger.
int characters; // Similarly, this length could potentially
// change between tests (e.g. if extra spaces get removed from
// a verse). Although this value can be determined from the
// verse itself, including it here won't take up too much
// extra space.
double wpm;
double test_seconds;
double error_rate;
double error_and_backspace_rate;
// This struct will be expanded to include accuracy data,
// the time the test was started and finished, and possibly other
// items also.
int marathon_mode; // Tracks whether or not marathon mode was
// active during a test.
// Entries for player info and other tags that the player
// can use to store custom information (e.g. about
// what keyboard is being used, how much sleep he/she got
// the night before, etc.)
// These will be initialized as empty strings because, as
// optional fields, there may not be any data entered for
// them during the typing test.
std::string player = "";
std::string mode = ""; // Can be either SP for single player
// or MP for multiplayer.
std::string tag_1 = "";
std::string tag_2 = "";
std::string tag_3 = "";
std::string notes = "";
};
// Defining a struct that can store word-level WPM information:
// (This struct will be used to keep track of WPM results for
// individual words within tests.)
struct Word_Result_Row {
long test_number; // Storing this value within
// Word_Result_Row will make it easier to link these results
// to their respective tests. (Indeed, this value will allow
// us to avoid having to store player names, tags, etc.
// within word results rows, as we can retrieve those by merging
// their respective test result columns into our word results
// table using test_number as a key.
long unix_test_start_time; // Added as a fallback merge
// key in case, somehow,
// word-result and test-result test numbers get unsynced.
// (It's possible, but high unlikely, that two test results
// will have the same test start time; thus, the test_number
// value should still be the best merge key.
std::string word = "";
// Initializing certain variables as -1 or -1.0 will make it
// easier to identify cases in which they weren't updated.
// I could try this approach with other structs as well.
long last_character_index = -1;
// The starting character could also be stored, but since this
// value will be used as a map key, it would be redundant to
// include it here also.
long word_length = -1; // an int would likely work fine here.
double wpm = -1.0;
double test_seconds = -1.0;
double error_rate = -1.0;
double error_and_backspace_rate = -1.0;
};
// Defining a set of configuration settings that can be updated
// by the user during the game:
struct Game_Config {
std::string player = "";
std::string tag_1 = "";
std::string tag_2 = "";
std::string tag_3 = "";
std::string notes = "";
std::string mode = "";
};
// Defining a struct that will allow me to keep track of
// the amount of time, in microseconds, the game needs to process
// individual keypresses:
// (Note: I've since commented this code out, as it was
// meant for testing/debugging purposes.)
// struct Keypress_Processing_Time_Row
// {
// long test_number;
// int keypress_number;
// long processing_time_in_microseconds;
// };
std::string cooked_input_within_raw_mode(
std::string prompt = "",
bool multiline = false) { /* This function aims to provide 'cooked-like'
input within a raw mode cpp-terminal session. It
does so by allowing users to add to a string until
they press enter. If the input may be more than one
line long, set multiline to true. The function will
then clear the console and display only the prompt,
thus making multi-line input easier to handle.*/
int starting_result_row = 2; // Defining the row at which
// the player's input should be typed. (Will only be used
// for multiline results, as otherwise, it would likely
// overwrite existing information on the screen.)
std::string user_string;
// Defining a string that will allow the cursor to get moved
// directly below the prompt:
std::string cursor_reposition_code =
Term::cursor_move(starting_result_row, 1);
if (multiline == true) {
int prompt_length = prompt.size();
// Determining on which row to begin printing the user's
// input: (See similar code within run_test() for more
// details.)
Term::Screen term_size{Term::screen_size()};
starting_result_row = (prompt_length - 1) / (term_size.columns()) + 2;
// Clearing the console and displaying the prompt:
// (Note that, unlike within run_test(), clear() isn't
// called here, as the user might want to be able to scroll
// up to see previous input.
Term::cout << Term::clear_screen() << Term::cursor_move(1, 1) << prompt
<< std::endl;
// Calling cursor_move to determine the cursor reposition
// code that will bring the cursor right under the verse
// after each keypress:
cursor_reposition_code = Term::cursor_move(starting_result_row, 1);
}
else
{ // Printing the prompt, then adding two newlines (including that
// printed by std::endl) to give
// the player more space to enter his/her response:
Term::cout << prompt << "\n" << std::endl;
// If multiline isn't active, the cursor reposition code
// will be set as "\r", the carriage return call, which
// will move the cursor back to the start of the line.
cursor_reposition_code = "\r";
}
std::string keyname;
while (keyname != "Enter") // May also need to add 'Return'
// or 'Ctrl+J' here also for OSX compatibility)
{
Term::Event event = Term::read_event();
switch (event.type()) {
case Term::Event::Type::Key: {
Term::Key key(event);
std::string char_to_add = "";
keyname = key.name();
if (keyname == "Space") {
char_to_add = " ";
} else if (keyname == "Backspace") // We'll need to remove the
// last character from our string.
{
user_string = user_string.substr(0, user_string.length() - 1);
} else if (keyname == "Enter") {
break;
} else {
char_to_add = keyname;
}
user_string += char_to_add;
// See run_test documentation for more information about
// "\033[J". This code repositions the cursor (either
// to the beginning of the current line if multiline
// is false or to the beginning of the line below the
// prompt if multiline is true); clears out everything
// past this point; then shows what the user has entered
// so far.
Term::cout << cursor_reposition_code << "\033[J" << user_string
<< std::flush; // Using std::flush rather
// than std::endl so that we can begin our new
// entry from the same line (which will be useful in
// non-multiline mode).
}
default: {
break;
}
}
}
// For debugging:
// Term::cout << "The string you entered was:\n"
// << user_string << std::endl;
return std::move(user_string);
}
std::string get_single_keypress(std::vector<std::string> valid_keypresses = {})
/* This function retrieves a single keypress, then responds
immediately to it.
The valid_keypresses argument allows the caller to specify
which keypresses should be considered valid entries. If
the default option is kept, any keypress will be considered
valid. */
{
std::string single_key;
bool valid_keypress_entered = false;
while (valid_keypress_entered == false) {
Term::Event event = Term::read_event();
switch (event.type()) {
case Term::Event::Type::Key: {
Term::Key key(event);
single_key = key.name();
if (valid_keypresses.size() > 0) {
for (std::string valid_keypress : valid_keypresses) {
if (single_key == valid_keypress) {
valid_keypress_entered = true;
}
}
if (valid_keypress_entered == false) {
Term::cout << single_key << " isn't a valid entry. Please \
try again." << std::endl;
}
} else // In this case, we'll assume the keypress
// to be valid.
{
valid_keypress_entered = true;
}
}
default: {
break;
}
}
}
return single_key;
}
int select_verse_id(const std::vector<Verse_Row> &vrv,
const int &last_verse_offset = 0) {
/* This function allows the player to select a certain verse ID to
type. It also checks for errors in order to (hopefully!) prevent
the game from crashing.
last_verse_offset was added in to allow the last valid verse ID
to be reduced within multiplayer games in which more than one
verse is typed. Without this offset, a multiplayer game could end
up attempting to access verses beyond the final verse, which would
likely result in a crash.
Note: I had originally set the value typed by the user to an
integer, then checked to see whether cin was valid; however,
this created a strange (to me) bug in which the function would
work fine the first time, but not during subsequent uses.
I did include cin.clear() and cin.ignore() calls, but to no avail.
(This was
most likely due to the use of std::cout following Term::cout, which
may have caused the former not to work correctly.)
Therefore, I decided to read in a string, then use a try/except
block to handle it. This approach is working much better,
thankfully!*/
std::string id_response_as_str = "";
while (true) {
Term::cout << "Type in the ID of the verse that you would \
like to type, then hit Enter. This ID can be found in the first \
column of the CPDB_for_TTTB.csv file. To exit out of this option, \
type -1 followed by Enter."
<< std::endl;
// Checking for a valid response:
id_response_as_str = cooked_input_within_raw_mode();
try {
int id_response_as_int = std::stoi(id_response_as_str);
if (id_response_as_int == -1 || id_response_as_int == -2)
// Added in -2 to support random-verse selection within
// multiplayer games
{
// Term::cout << "Never mind, then!" << std::endl;
return id_response_as_int;
}
else if ((id_response_as_int >= 1) &&
(id_response_as_int <= (vrv.size() - last_verse_offset)))
// Subtracting 1 from id_response_as_int will get us the index
// position of that verse (as the index of each verse is one less
// than its ID).
{
return (id_response_as_int - 1);
} else {
Term::cout << "\nThat is not a valid ID. \
Please try again." << std::endl;
// return -1;
}
}
catch (...) {
Term::cout << "Your input was invalid. \
Please try again."
<< std::endl;
// return -1;
}
}
}
std::map<long, Word_Result_Row> gen_word_result_map(const std::string &verse)
/*This function will go through each character within the verse
passed to it in order to identify all words within the verse;
their starting and ending characters; and their lengths. This
information will then be stored within a map of Word_Result_Row objects
that the typing test code can access in order to calculate word-level
WPM data. */
{
// Initializing several variables here so that they can get
// utilized within the following loops:
int first_character_index = -1;
std::string newword = "";
std::map<long, Word_Result_Row> word_map;
// Checking for the first character within the verse that starts
// a word:
for (int i = 0; i < verse.size(); i++) {
if (isalnum(verse[i]) != 0)
first_character_index = i;
newword = verse[i];
break;
}
// Term::cout << "Current values of first_character_index \
// and newword: " << first_character_index << " " << newword << std::endl;
// Now that we know where the first character that starts a word
// is located,
// we can continue to retrieve the other characters (starting
// from the character following this first character).
for (int i = first_character_index + 1; i < verse.size(); i++)
{
/*If a character is alphanumeric, and the one prior to it
was not, we'll consider this to be the start of a new word.
Note that isalnum returns either a 0 or non-0 number
(see https://en.cppreference.com/w/cpp/string/byte/isalnum;
in my case, it was 8), which is why I'm checking for
0 and non-0 in my if statement. This code could likely
be simplified, however.*/
if ((isalnum(verse[i]) != 0) && (isalnum(verse[i - 1]) == 0)) {
first_character_index = i;
newword = verse[i]; // I had previously tried to
// create a Word_Result_Row class here and assign verse[i] to
// its .word attribute, but this failed to work correctly.
// Term::cout << "Starting character info: "
// << i << " " << verse[i] << std::endl;
}
else if (((isalnum(verse[i]) == 0) && (isalnum(verse[i - 1]) != 0)) ||
((isalnum(verse[i]) != 0) && (i == (verse.size() - 1))))
/*In this case, either verse[i-1] marks the end of a word,
or the final character of the verse is part of a word.
For both of these situations, we should go ahead and add
this word to a new Word_Result_Row object, then add that object
to our word map (with the initial character as the key). */
{
int last_character_index = i - 1;
// Changing this value to i if the second condition
// (e.g. the final word within the verse is a letter) is true:
if ((isalnum(verse[i]) != 0) && (i == (verse.size() - 1))) {
last_character_index = i;
// In this case, we'll need to add this final character
// to newword also.
newword += verse[i];
}
// Creating a new Word_Result_Row object that can store
// various attributes about this word:
// (Other attributes of this row will get filled in
// within run_test().)
Word_Result_Row wr;
wr.word = newword;
wr.word_length = newword.size();
wr.last_character_index = last_character_index;
word_map[first_character_index] = wr;
// Term::cout << "Ending character info: " << i << " "
// << verse[i-1] << std::endl;
}
else if (isalnum(verse[i]) != 0)
// In this case, we're in the middle of constructing a word,
// so we'll go ahead and add this alphanumeric character
// to that word.
{
newword += verse[i];
}
}
// Printing out all initial characters, ending characters,
// and words within the map for debugging purposes:
// for (auto const& [starting_character, word_result] : word_map)
// {Term::cout << word_result.word << ": characters " <<
// starting_character << " to " <<
// word_result.last_character_index
// << " (" << word_result.word_length << " characters long)"
// << std::endl;}
// Moving word_map in order to avoid an unnecessary copy:
return std::move(word_map);
};
bool run_test(Verse_Row &verse_row, std::vector<Test_Result_Row> &trrv,
std::vector<Word_Result_Row> &wrrv, const bool &marathon_mode,
const std::string &player, const std::string &mode,
const std::string &tag_1, const std::string &tag_2,
const std::string &tag_3, const std::string ¬es,
long &test_number, long &session_number,
int &within_session_test_number, const bool allow_quitting,
// std::vector<Keypress_Processing_Time_Row> &kptrv,
const std::string &within_test_update_message,
const std::string &post_entry_message)
/* This function allows the player to complete a single typing test.
It then updates the verse_row, trrv, and wrrv vectors with the results
of that test.
*/
{
/* Some of the following code was based on the documentation at
https://github.com/jupyter-xeus/cpp-terminal/blob/
master/examples/keys.cpp .*/
// int keypress_counter = 0; // Currently, this value is only
// being used to keep track of keypress processing times.
// Creating a Keypress Processing Time Row vector that will
// store results while a test is in progress:
// (Once the test is completed, additional information will
// get added to these tests, and they will then get stored
// within kptrv. This approach will allow the main
// keypress processing time vector to remain unmodified
// until the test has been complete. (Otherwise, we might end
// up assigning incorrect test result names to keypresses
// in cases when players quit tests early.)
// std::vector<Keypress_Processing_Time_Row> local_kptrv;
// Calling gen_word_result_map() to create a map that contains
// information about each word within the verse:
// (This will prove useful when calculating word-level WPM data.)
std::map<long, Word_Result_Row> word_map =
gen_word_result_map(verse_row.verse);
// Initializing several variables that will be used later in
// this function:
// Initializing counters that will keep track of the number of
// errors the user made:
// (One of these counters won't count backspaces as errors;
// the other will.)
long error_counter = 0;
long backspace_counter = 0;
long word_error_counter = 0;
long word_backspace_counter = 0;
// Initializing a string to which characters typed by the user
// will be added: (This will allow us to print the entire portion
// of the verse that the user has typed so far, rather than just
// the most recent character.)
std::string user_string = "";
bool exit_test = false;
bool completed_test = true;
int last_character_index = -1;
// For debugging:
std::string last_character_index_as_string = "";
// std::string word_timing_note = ""; // for debugging
int latest_first_character_index = -1;
int first_character_index = -1;
long word_length = 0;
auto word_start_time = std::chrono::high_resolution_clock::now();
Term::Cursor cursor{Term::cursor_position()};
Term::Screen term_size{Term::screen_size()};
// Clearing the console and displaying the verse to type:
// In order to make the transition from the following dialog
// to the actual test less jarring, I chose to place the verse
// at the top of the screen, exactly where it will appear
// during the test itself. I also placed the post-entry message
// at the same place that it will appear when a marathon mode
// is active.
// Note: clear_screen() hides content within the current
// window by scrolling down until the window is blank;
// clear() removes content further up in the terminal that is
// now out of view. Thus, printing clear_screen(), followed by
// clear() and a command to move the cursor to the top left,
// prevents the terminal from filling up with previous entries
// that no longer
// need to be saved in memory.
// (Note that, without the
// inclusion of clear(), a new screen would be stored in
// memory for all, or almost all *keypresses* during typing
// tests--which I imagine could cause all sorts of
// memory- or performance-related issues, at least on lower-powered devices.
// I found clear() within the cpp-terminal documentation;
// you can search for it by looking for the text [3J (which
// is part of a particular 'Erase in Display' ANSI erase
// sequence; see https://en.wikipedia.org/wiki/ANSI_escape_code#SGR
// and Goran's AskUbuntu response at
// https://askubuntu.com/a/473770/1685413 .
Term::cout << Term::clear_screen() << Term::terminal.clear()
<< Term::cursor_move(1, 1) << verse_row.verse << "\n"
<< post_entry_message << std::endl;
// Determining where to position the cursor after each keypress:
// The best option here will be to position it at the leftmost
// column just under the verse. That way, we won't need to
// resend the verse to the terminal before each keypress,
// but we'll also be able to easily account for multiple-line
// verses, backspaces, and other special cases.
// To figure out which row belongs just under the verse, we
// can divide the length of the verse minus one by
// term_size.columns(), then add 2 to the result. (This value
// should always be an integer, since we're dividing one
// integer by another; thus, the remainder of this division
// operation will be dropped by default. Subtracting 1 from
// the length of the verse will prevent an extra row from
// getting added if the length of the verse is an exact multiple
// of the width of the terminal.)
int starting_result_row =
(verse_row.characters - 1) / (term_size.columns()) + 2;
// Calling cursor_move to determine the cursor reposition
// code that will bring the cursor right under the verse
// after each keypress:
std::string cursor_reposition_code =
Term::cursor_move(starting_result_row, 1);
/*
Note that, for cursor_move to have an effect, we need to pass
its output (a string) to Term::cout. Also note that using
std::cout in place of Term::cout may not work.
The kilo.cpp example (available at
https://github.com/jupyter-xeus/cpp-terminal/blob/master/examples/kilo.cpp
and the cursor.cpp source code at
https://github.com/jupyter-xeus/cpp-terminal/blob/master/cpp-terminal/cursor.cpp
helped me recognize all this.)
I've also found that ending Term::cout lines with "\n" may
not work; instead, it may be necessary to use std::endl or std::flush.
*/
if (marathon_mode == false) // The following prompt should be skipped
// within marathon mode, thus allowing users to go directly
// into the typing test.
{
Term::cout << "\n"
<< within_test_update_message << "\nYour \
next verse to type ("
<< verse_row.verse_code << ") is shown above. This verse is "
<< verse_row.characters << " characters long.\nPress \
the space bar to begin the typing test and 'e' to cancel it."
<< std::endl;
// The following while statement allows the user to begin the
// test immediately after pressing *only* the space bar.
// It also lets the player cancel the test by
// pressing 'e.'
// The verse will still be present at the
// top left of the terminal so that
// the user can reference it when beginning the test.
// the top left so that its location won't change between this
// section of the script and the following while loop.)
std::string player_response = get_single_keypress({"Space", "e"});
if (player_response == "e") {
exit_test = true;
completed_test = false;
}
}
// Determining the start time of the test in system clock form:
// (Note: Linux allowed the start_time created by
// std::chrono::high_resolution_clock::now() to get converted
// to unix_test_start_time, but Windows didn't--hence my addition
// of code to derive unix_test_start_time from
// system_clock instead.
// This code was based on
// https://en.cppreference.com/w/cpp/chrono/system_clock/to_time_t.html
// and https://en.cppreference.com/w/cpp/chrono/system_clock/now.html .
std::time_t unix_test_start_time =
std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
// Starting our timing clock:
// (This code was based on p. 1010 of The C++ Programming Language,
// 4th edition.)
auto start_time = std::chrono::high_resolution_clock::now();
/* Checking to see whether the first character begins
one of the words in word_map: (This will generally, but not
always be the case.)
If so, we'll need to begin word-timing-related
processes immediately.
*/
if (word_map.contains(0)) {
// Updating latest_first_character_index:
latest_first_character_index = 0;
last_character_index = word_map[0].last_character_index;
word_start_time = start_time;
word_length = word_map[0].word_length;
last_character_index_as_string = std::to_string(last_character_index);
// word_error_counter and word_backspace_counter
// were already initialized to 0 earlier within
// this function, so we don't
// need to re-initialize them as 0 here.
// word_timing_note = "the first letter within the verse \
// began a word. its corresponding ending character index \
// is: "+last_character_index_as_string;
}
// Users may wish to exit a given test before completing it,
// so we should accommodate that choice.
// However, in that case, this boolean will be set to 'false' so that
// we don't mistake the exited test for a completed one.
while ((user_string != verse_row.verse) && (exit_test == false)) {
Term::Event event = Term::read_event();
switch (event.type()) {
case Term::Event::Type::Key: {
Term::Key key(event);
// keypress_counter++;
// Creating a timer following this keypress:
// (This will be useful for timing how long it took the user
// to type each individual word.)
auto keypress_time = std::chrono::high_resolution_clock::now();
std::string char_to_add = ""; // This default setting will
// be used in certain special keypress cases (including Backspace)
std::string keyname = key.name();
if (keyname == "Space") {
char_to_add = " ";
} else if ((keyname == "Ctrl+C") && (allow_quitting == true)) {
exit_test = true;
completed_test = false;
}
else if (keyname == "Backspace") // We'll need to remove the
// last character from our string.
{
user_string = user_string.substr(0, user_string.length() - 1);
backspace_counter++; // Since the user pressed backspace,
// we'll need to increment this counter by 1 so that this
// keypress can get reflected within our error_and_backspace_counter
// value.
word_backspace_counter++;
}
// For documentation on substr, see
// https://cppscripts.com/string-slicing-cpp
// Defining key combinations that will allow entire
// words to be deleted:
else if ((keyname == "Alt+Del") || (keyname == "Del") ||
(keyname == "Alt+Backspace"))
// Alt+Del, Del, and Alt+Backspace were added in for
// Linux, OSX, and Windows, respectively; thankfully,
// they don't seem to conflict with one another.
// I found these key names by experimenting with the
// executable version of the keys.cpp example within
// the cpp-terminal library (available at
// https://github.com/jupyter-xeus/cpp-terminal/blob/master/examples/keys.cpp
// ). Note that Alt+Del is generated in Linux by pressing Alt+Backspace
// (at least when using Linux Mint on my Gigabyte Aorus laptop); Del is
// generated in OSX by pressing Function + Delete; and Alt+Backspace is
// generated in Windows by pressing Alt+Backspace. I would have liked to
// set Ctrl + Backspace as an option, but this key combination simply
// produced 'Backspace' for me on Windows and Linux. Ctrl + Backspace
// (which seemed to be interpreted as just Backspace by the cpp-terminal
// library). The following code will remove all characters from the end of
// the string up to (but not including) the most recent space. However, if
// the last character in the string happens to be a space as well, the
// loop will skip over it and search instead for the second-to-last space.
// (That way, repeated Alt+Del entries can successfully remove multiple
// words.) Note: the cpp-terminal code interpreted the combination of the
// Alt and Backspace keys, at least on Linux, as Alt + Delete. Note: Alt +
// Delete was interpreted as a regular Backspace entry on the Mac on which
// I tested TTTB; therefore, I added Del (which could be produced via Fn +
// Delete on that Mac) as an option.)
{
backspace_counter++;
word_backspace_counter++;
for (int j = user_string.length() - 1; j >= 0; --j) {
if ((isspace(user_string[j])) && (j != (user_string.length() - 1)))
// if (isspace(user_string[j]))
/* Truncating the response so that it only extends up to
the latest space (thus removing all of the characters
proceeding that space): */
{
user_string = user_string.substr(0, j + 1);
break;
}
if (j == 0) // In this case, we made it to
// the beginning of the string without finding
// any space. Therefore, we'll go ahead and
// delete the entire sequence that the player
// has typed so far. (This inclusion allows
// Alt + Delete to still work when the player
// hasn't yet finished his/her first word.)
{
user_string = "";
break;
}
}
}
else {
char_to_add = keyname;
}
user_string += char_to_add;
/* Determining how to color the output: (If the output is correct
so far, it will be colored green; if there is a mistake, it will
instead be colored red.*/
if (user_string == verse_row.verse.substr(0, user_string.length()))
// Checking to see whether we should start or end our word timer:
{ // Checking to see whether we should begin timing a new word:
// This new word's index position will be one greater than
// the current index position--but, conveniently, user_string.length()
// allows us to access that position. And we'll want to
// start the timing right before the first letter of the word so that
// the time that it takes that letter to get typed can also get
// included in the overall timing for the word.
// The code also checks to see whether the length of the user's entry
// is greater than the most recent first_character_index so that
// the user can't 'get out of' or restart the current word timer by
// going back to the start of the current word or a previous word.
if ((word_map.contains(user_string.length())) &&
(long(user_string.length()) > latest_first_character_index))
// Note that string.length() needs to be cast to a long or int
// in order to get compared with latest_first_character_index.
{ // Updating latest_first_character_index:
latest_first_character_index = user_string.length();
last_character_index =
word_map[latest_first_character_index].last_character_index;
word_start_time = keypress_time;
// Resetting word-specific error and backspace counters:
word_error_counter = 0;
word_backspace_counter = 0;
word_length = word_map[latest_first_character_index].word_length;
last_character_index_as_string = std::to_string(last_character_index);
// word_timing_note = "the next letter will start a word. \
// its corresponding ending character index \
// is: "+last_character_index_as_string;
}
if (user_string.length() - 1 ==
last_character_index) { // In this case, we've made it to the end of
// the word whose
// starting character we approached earlier. Thus, we can
// stop our word timer and create a new Word_Result_Row object
// with our output.
auto word_end_time = keypress_time;
auto word_seconds =
std::chrono::duration<double>(word_end_time - word_start_time)
.count();
double word_wpm = (word_length / word_seconds) * 12;
int word_error_and_backspace_counter =
word_error_counter + word_backspace_counter;
double word_error_rate =
(word_error_counter / static_cast<double>(word_length));
double word_error_and_backspace_rate =
(word_error_and_backspace_counter /
static_cast<double>(word_length));
// word_timing_note = "the end of the word has been \
// reached. Ending character: "+last_character_index_as_string + " \
// word length: "+std::to_string(word_length) + " word \
// duration: "+ std::to_string(word_seconds) + " WPM: " + std::to_string(
// word_wpm);
// Note that, once we arrive at the next word's starting
// character, certain variables (like last_character_index)
// will get replaced with new versions.
// Storing these details within word_map: (We can use
// latest_first_character_index as a key here.)
// Other elements of each word_row object that
// don't need to be computed here (such as
// test numbers)
// will get added in after the test completes,
// thus reducing the amount of
// processing time needed to keep track of
// word results within the race.
word_map[latest_first_character_index].wpm = word_wpm;
word_map[latest_first_character_index].test_seconds = word_seconds;
word_map[latest_first_character_index].error_rate = word_error_rate;
word_map[latest_first_character_index].error_and_backspace_rate =
word_error_and_backspace_rate;
}
print_color_code = correct_output_color_code;
} else // The user made a mistake, as signified by the fact that
// this string doesn't match the initial section of the verse that
// has the same length.
{
print_color_code = incorrect_output_color_code;
if ((keyname != "Backspace") && (keyname != "Alt+Del")) {
// In this case, we'll increment our main error
// counter, as the user did not press Backspace.
// (This code may need to be updated to also address
// the use of Ctrl+Backspace, though on my Linux system,
// that combination also produces the name 'Backspace.')
error_counter++;
word_error_counter++;