-
Notifications
You must be signed in to change notification settings - Fork 0
/
Type_The_Bible_v4.py
1251 lines (1080 loc) · 56.2 KB
/
Type_The_Bible_v4.py
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
# %% [markdown]
# # Type Through The Bible
#
# By Kenneth Burchfiel
#
# Code is released under the MIT license; Bible verses are from the Web English Bible (Catholic Edition)* and are in the public domain.
#
# \* Genesis was not found within the original WEB Catholic Edition folder, so I copied in files from another Web English Bible translation instead. I imagine, but am not certain, that these files are the same as the actual Catholic Edition Genesis files.
# %% [markdown]
# ## More documentation to come!
# %% [markdown]
# Next steps: (Not necessarily in order of importance)
#
# * Improve chart formatting (e.g. add titles, legend names, etc.)
# * Add in more documentation
# * Revise verse numbering for chapters that have lots of verses grouped together. (You can use the PDF version of the WEB as a guide for this)
# %%
import pandas as pd
pd.set_option('display.max_columns', 1000)
import time
import plotly.express as px
from getch import getch # Installed this library using pip install py-getch, not
# pip install getch. See https://github.com/joeyespo/py-getch
import numpy as np
from datetime import datetime, date, timezone # Based on
# https://docs.python.org/3/library/datetime.html
from colorama import just_fix_windows_console, Fore, Back, Style
# From https://github.com/tartley/colorama/blob/master/demos/demo01.py
just_fix_windows_console() # From https://github.com/tartley/colorama/blob/master/demos/demo01.py
# %%
extra_analyses = False
# %% [markdown]
# Checking whether the program is currently running on a Jupyter notebook:
#
# (The program normally uses getch() to begin typing tests; however, I wasn't able to enter input after getch() got called within a Jupyter notebook and thus couldn't begin a typing test in that situation. Therefore, the program will use input() instead of getch() to start tests when running within a notebook.)
# %%
# The following method of determining whether the code is running
# within a Jupyter notebook is based on Gustavo Bezerra's response
# at https://stackoverflow.com/a/39662359/13097194 . I found that
# just calling get_ipython() was sufficient, at least on Windows and within
# Visual Studio Code; his answer is more complex.
try:
get_ipython()
run_on_notebook = True
except:
run_on_notebook = False
# print(run_on_notebook)
# %%
df_Bible = pd.read_csv('WEB_Catholic_Version_for_game_updated.csv')
df_Bible
# %%
df_results = pd.read_csv('results.csv', index_col='Test_Number')
df_results
# %%
# If you ever need to drop a particular result,
# you can do so as follows:
# df_results.drop(17, inplace = True)
# df_results.to_csv('results.csv') # We want to preserve the index so as not
# to lose our 'Test_Number' values
# df_results
# %%
# Creating an RNG seed:
# In order to make the RNG values a bit more random, the following code will
# derive the RNG seed from the decimal component of the current timestamp.
# This seed will change 1 million times each second.
# Using the decimal component of time.time() to select an RNG seed:
current_time = time.time()
decimal_component = current_time - int(current_time) # This
# line retrieves the decimal component of current_time. int() is used instead
# of np.round() so that the code won't ever round current_time up prior
# to the subtraction operation, which would return a different value.
# I don't think that converting current_time to an integer (e.g. via
# np.int64(current_time)) is necessary, as int() appears to handle at least
# some integers larger than 32 bits in size just fine.
decimal_component
random_seed = round(decimal_component * 1000000)
decimal_component, random_seed
# %%
rng = np.random.default_rng(random_seed) # Based on
# https://numpy.org/doc/stable/reference/random/index.html?highlight=random#module-numpy.random
# %%
df_Bible
# %% [markdown]
# [This fantastic answer](https://stackoverflow.com/a/23294659/13097194) by Kevin at Stack Overflow proved helpful in implementing user validation code within this program.
# %%
def select_verse():
print("Select a verse to type! Enter 0 to receive a random verse\n\
or enter a verse number (see 'Verse_Order column of\n\
the WEB_Catholic_Version.csv spreadsheet for a list of numbers to enter\n\
to select a specific verse.\n\
You can also enter -2 to receive a random verse that you haven't yet typed\n\
or -3 to choose the first Bible verse that hasn't yet been typed.")
while True:
try:
response = int(input())
except:
print("Please enter an integer corresponding to a particular Bible \
verse or 0 for a randomly selected verse.")
continue # Allows the user to retry entering a number
if response == 0:
return rng.integers(1, 35380) # Selects any verse within the Bible.
# there are 35,379 verses present, so we'll pass 1 (the first verse)
# and 35,380 (1 more than the last verse, as rng.integers won't
# include the final number within the range) to rng.integers().
# The next two elif statements will require us to determine which
# verses haven't yet been typed. We can do so by filtering df_Bible
# to include only untyped verses.
elif response == -2:
verses_not_yet_typed = list(
df_Bible.query("Typed == 0")['Verse_Order'].copy())
if len(verses_not_yet_typed) == 0:
print("Congratulations! You have typed all verses from \
the Bible, so there are no new verses to type! Try selecting another option \
instead.")
continue
print(f"{len(verses_not_yet_typed)} verses have not yet \
been typed.")
return rng.choice(verses_not_yet_typed) # Chooses one of these
# untyped verses at random
elif response == -3:
verses_not_yet_typed = list(
df_Bible.query("Typed == 0")['Verse_Order'].copy())
if len(verses_not_yet_typed) == 0:
print("Congratulations! You have typed all verses from \
the Bible, so there are no new verses to type! Try selecting another option \
instead.")
continue
print(f"{len(verses_not_yet_typed)} verses have not yet \
been typed.")
verses_not_yet_typed.sort() # Probably not necessary, as df_Bible
# is already sorted from the first to the last verse.
return verses_not_yet_typed[0]
else:
if ((response >= 1)
& (response <= 35379)): # Making sure that the response is
# an integer between 1 and 35,379 (inclusive) so that it
# matches one of the Bible verse numbers present:
return response
else: # Will be called if a non-integer number was passed
# or if the integer didn't correspond to a Bible verse
# number.
print("Please enter an integer between 1 and 35,379.") # Since
# we're still within a While loop, the user will be returned
# to the initial try/except block.
# %%
def run_typing_test(verse_number, results_table, test_type = 'v2'):
'''This function calculates how quickly the user types the characters
passed to the Bible verse represented by verse_number, then saves those
results to the DataFrame passed to results_table.'''
# Retrieving the verse to be typed:
# The index begins at 0 whereas the list of verse numbers begins at 1,
# so we'll need to subtract 1 from verse_number in order to obtain
# the verse's index.
verse = df_Bible.iloc[verse_number-1]['Verse']
book = df_Bible.iloc[verse_number-1]['Book_Name']
chapter = df_Bible.iloc[verse_number-1]['Chapter_Name']
verse_number_within_chapter = df_Bible.iloc[verse_number-1]['Verse_#']
verse_number_within_Bible = df_Bible.iloc[
verse_number-1]['Verse_Order']
# I moved these introductory comments out of the following while loop
# in order to simplify the dialogue presented to users during retest
# attempts.
print("Welcome to the typing test! Note that you can exit a test in \
progress by typing 'exit' and then hitting Enter.")
print(f"\nYour verse to type is {book} \
{chapter}:{verse_number_within_chapter} (verse {verse_number_within_Bible} \
within the Bible .csv file).\n")
if run_on_notebook == False:
print("Press any key to begin typing!")
else:
print("Press Enter to begin the test!")
complete_flag = 0
while complete_flag == 0:
print(f"Here is the verse:\n\n{verse}")
if run_on_notebook == False: # In this case, we can use getch()
# to begin the test.
# time.sleep(3) # I realized that players could actually begin typing
# during this sleep period, thus allowing them to complete the test
# faster than intended. Therefore, I'm now having the test start
# after the player hits a character of his/her choice. getch()
# accomplishes this task well.
# A simpler approach would be to add in an additional input block
# and have the player begin after he/she presses Enter, but that would
# cause the player's right hand to leave the default home row position,
# which could end up slowing him/her down. getch() allows any character
# to be pressed (such as the space bar) and thus avoids this issue.
start_character = getch() # See https://github.com/joeyespo/py-getch
else: # When running the program within a Jupyter notebook, I wasn't
# able to enter input after getch() was called, so I created
# an alternative start method below that simply uses input().
input()
print("Start!")
if test_type == 'v1':
# This is a simple typing test setup that receives input from
# the user when 'Enter' is pressed, then checks that input
# against the verse. Because it doesn't check the response
# for accuracy as the player types, the player might not realize
# a character was mistyped until the very end, which can get
# frustrating. Therefore, I've now added in a new version
# of the test (called 'v2') that can be used instead.
local_start_time = datetime.now().isoformat()
utc_start_time = datetime.now(timezone.utc).isoformat()
typing_start_time = time.time()
verse_response = input()
# The following code will execute once the player finishes typing and
# hits Enter. (Having the program evaluate the player's entry only after
# 'Enter' is pressed isn't the best option, as the time required to
# hit Enter will reduce the player's reported WPM. Version v2,
# shown below, stops the test right when the final correct
# character is typed, which will make the final WPM slightly faster.
typing_end_time = time.time()
typing_time = typing_end_time - typing_start_time
elif test_type == 'v2':
# This version of the test checks the player's input after
# each character is typed. If the player's input is correct
# so far, the text will be highlighted green; otherwise,
# it will be highlighted red. (This allows the player to be
# notified of an error without the need for a line break
# in the console, which could prove distracting.)
# This function has been tested on Windows, but not yet
# on Mac or Linux. The use of the Colorama library should
# make it cross-platform, however.
verse_response = '' # This string will store the player's
# response.
verse_response_with_newlines = ''
local_start_time = datetime.now().isoformat()
utc_start_time = datetime.now(timezone.utc).isoformat()
typing_start_time = time.time()
while True: # This while loop allows the player to enter
# multiple characters.
# to allow the player to enter
character = getch() # getch() allows each character to be
# checked, making it easier to identify mistyped words.
if character == b'\x08':
# This will return True if the user hits backspace.
# In this case, we'll want to remove the latest character
# from verse_response in order to keep that value
# in sync with what the player sees on the screen.
# Calling print(character) after
# hitting backspace revealed that b'\x08' was the code
# associated with the backspace key.
verse_response = verse_response[:-1] # Trims the last
# value off verse_response.
verse_response_with_newlines = verse_response_with_newlines[:-1]
elif character == b'\r': # Returns True if the user hits Enter.
verse_response_with_newlines = '' # Resets the view to avoid a glitch
# that can occur when more than one line is present in the output.
print('\r') # Moves the cursor one line down so that the player
# doesn't overwrite his/her previous output in the process
# of writing the new line
elif character == b'`':
print(Style.RESET_ALL) # Resets the color of the text.
# See https://pypi.org/project/colorama/
break
else:
# The following line adds the latest character typed
# to verse_response.
verse_response += character.decode('ascii')
verse_response_with_newlines += character.decode('ascii')
# See https://stackoverflow.com/questions/17615414/how-to-convert-binary-string-to-normal-string-in-python3
# Determining which color to use for the text:
if verse[0:len(verse_response)] == verse_response:
text_color = Fore.GREEN
else:
text_color = Fore.RED
# Printing the player's response so far: (Note that
# verse_response gets printed instead of the last character.)
# The addition of 'end = "\r", which comes from Sencer H at
# https://stackoverflow.com/a/69030559/13097194,
# allows characters to get
# displayed immediately
# after one another rather than on separate lines. It also
# prevents a new line from appearing each time the user
# types a character.
print(f"{text_color}{verse_response_with_newlines.ljust(100)}", end = "\r")
# . I had also added in flush = True, but this didn't appear
# to affect the output.
# .ljust(100) pads the string with ASCII spaces on the right
# (see https://docs.python.org/3/library/stdtypes.html#str.ljust).
# I added this in so that, if the user needed to hit backspace,
# the deleted characters would no longer appear within the string.
# For the use of Colorama to produce red and green text, see
# https://pypi.org/project/colorama/
# and https://stackoverflow.com/a/3332860/13097194
if verse_response == verse: # Note that, unlike with version
# v1, the player does not need to hit 'Enter' in order
# to end the typing test after writing a completed
# verse. This should speed up his/her WPM as a result.
typing_end_time = time.time()
typing_time = typing_end_time - typing_start_time
print("\nSuccess!")
print(Style.RESET_ALL)
break
if len(verse_response_with_newlines) == 100:
verse_response_with_newlines = '' # Resets the view to avoid a glitch
# that can occur when more than one line is present in the output.
print('\r') # Moves the cursor one line down so that the player
# doesn't overwrite his/her previous output in the process
# of writing the new line
if verse_response == verse:
print(f"Well done! You typed the verse correctly.")
complete_flag = 1 # Setting this flag to 1 allows the player to exit
# out of the while statement.
elif (verse_response.lower() == 'exit') or ('`' in verse_response):
print("Exiting typing test.")
return results_table # Exits the function without saving the
# current test to results_table or df_Bible. This function has
# been updated to work with both versions of the typing
# test.
else:
print("Sorry, that wasn't the correct input.")
# Identifying incorrectly typed words:
verse_words = verse.split(' ')
verse_response_words = verse_response.split(' ')[0:len(verse_words)]
# I added in the [0:len(verse_words)] filter so that the following
# for loop would not attempt to access more words that were
# present in the original verse (which would cause the game
# to crash with an IndexError).
for i in range(len(verse_response_words)):
if verse_response_words[i] != verse_words[i]:
print(f"Word number {i} ('{verse_words[i]}') \
was typed '{verse_response_words[i]}'.")
# If the response has more or fewer words than the original
# verse, some correctly typed words might appear within
# this list also.
print("Try again!") # complete_flag will still be 0 in this case,
# so the while loop will continue back to the beginning.
# Calculating typing statistics and storing them within a single-row
# DataFrame:
cps = len(verse) / typing_time # Calculating characters per second
wpm = cps * 12 # Multiplying by 60 to convert from characters to minutes,
# then dividing by 5 to convert from characters to words.
wpm
print(f"Your CPS and WPM were {round(cps, 3)} and {round(wpm, 3)}, \
respectively.")
# Creating a single-row DataFrame that stores the player's results:
df_latest_result = pd.DataFrame(index = [
len(results_table)+1], data = {'Unix_Start_Time':typing_start_time,
'Local_Start_Time':local_start_time,
'UTC_Start_Time':utc_start_time,
'Characters':len(verse),
'Seconds':typing_time,
'CPS': cps,
'WPM':wpm,
'Book': book,
'Chapter': chapter,
'Verse #': verse_number_within_chapter,
'Verse':verse,
'Verse_Order':verse_number_within_Bible})
df_latest_result.index.name = 'Test_Number'
df_latest_result
# Adding this new row to results_table:
results_table = pd.concat([results_table, df_latest_result])\
# Note: I could also have used df.at or df.iloc to add a new row
# to df_latest_result, but I chose a pd.concat() setup in order to ensure
# that the latest result would never overwrite an earlier result.
# Updating df_Bible to store the player's results: (This will allow the
# player to track how much of the Bible he/she has typed so far)
df_Bible.at[verse_number-1, 'Typed'] = 1 # Denotes that this verse
# has now ben typed
df_Bible.at[verse_number-1, 'Tests'] += 1 # Keeps track of how
# many times this verse has been typed
fastest_wpm = df_Bible.at[verse_number-1, 'Fastest_WPM']
if ((pd.isna(fastest_wpm) == True) | (wpm > fastest_wpm)):
# In these cases, we should replace the pre-existing Fastest_WPM value
# with the WPM the player just achieved.
# I found that 5 > np.NaN returned False, so if I only checked for
# wpm > fastest_wpm, blank fastest_wpm values would never get overwritten.
# Therefore, I chose to also check for NaN values
# in the above if statement.
df_Bible.at[verse_number-1, 'Fastest_WPM'] = wpm
return results_table
# %% [markdown]
# Alternative version of run_typing_test that supports only version v2:
# %%
# def run_typing_test(verse_number, results_table, test_type = 'v2'):
# '''This function calculates how quickly the user types the characters
# passed to the Bible verse represented by verse_number, then saves those
# results to the DataFrame passed to results_table.'''
# # Retrieving the verse to be typed:
# # The index begins at 0 whereas the list of verse numbers begins at 1,
# # so we'll need to subtract 1 from verse_number in order to obtain
# # the verse's index.
# verse = df_Bible.iloc[verse_number-1]['Verse']
# book = df_Bible.iloc[verse_number-1]['Book_Name']
# chapter = df_Bible.iloc[verse_number-1]['Chapter_Name']
# verse_number_within_chapter = df_Bible.iloc[verse_number-1]['Verse_#']
# verse_number_within_Bible = df_Bible.iloc[
# verse_number-1]['Verse_Order']
# print("Welcome to the typing test! Note that you can exit a test in \
# progress by typing 'exit' and then hitting Enter.")
# print(f"\nYour verse to type is {book} \
# {chapter}:{verse_number_within_chapter} (verse {verse_number_within_Bible} \
# within the Bible .csv file).\n")
# print("Press any key to begin typing!")
# print(f"Here is the verse:\n\n{verse}")
# # time.sleep(3) # I realized that players could actually begin typing
# # during this sleep period, thus allowing them to complete the test
# # faster than intended. Therefore, I'm now having the test start
# # after the player hits a character of his/her choice. getch()
# # accomplishes this task well.
# # A simpler approach would be to add in an additional input block
# # and have the player begin after he/she presses Enter, but that would
# # cause the player's right hand to leave the default home row position,
# # which could end up slowing him/her down. getch() allows any character
# # to be pressed (such as the space bar) and thus avoids this issue.
# start_character = getch() # See https://github.com/joeyespo/py-getch
# print("Start!")
# # This version of the test checks the player's input after
# # each character is typed. If the player's input is correct
# # so far, the text will be highlighted green; otherwise,
# # it will be highlighted red. (This allows the player to be
# # notified of an error without the need for a line break
# # in the console, which could prove distracting.)
# # This function has been tested on Windows, but not yet
# # on Mac or Linux. The use of the Colorama library should
# # make it cross-platform, however.
# verse_response = '' # This string will store the player's
# # response.
# verse_response_with_newlines = ''
# local_start_time = datetime.now().isoformat()
# utc_start_time = datetime.now(timezone.utc).isoformat()
# typing_start_time = time.time()
# while True: # This while loop allows the player to enter
# # multiple characters.
# # to allow the player to enter
# character = getch() # getch() allows each character to be
# # checked, making it easier to identify mistyped words.
# if character == b'\x08':
# # This will return True if the user hits backspace.
# # In this case, we'll want to remove the latest character
# # from verse_response in order to keep that value
# # in sync with what the player sees on the screen.
# # Calling print(character) after
# # hitting backspace revealed that b'\x08' was the code
# # associated with the backspace key.
# verse_response = verse_response[:-1] # Trims the last
# # value off verse_response.
# verse_response_with_newlines = verse_response_with_newlines[:-1]
# elif (character == b'\r'):
# # Returns True if the user hits Enter.
# verse_response_with_newlines = '' # Resets the view to avoid a glitch
# # that can occur when more than one line is present in the output.
# print('\r') # Moves the cursor one line down so that the player
# # doesn't overwrite his/her previous output in the process
# # of writing the new line
# elif character == b'`':
# print(Style.RESET_ALL) # Resets the color of the text.
# # See https://pypi.org/project/colorama/
# print("Exiting typing test.")
# return results_table # Exits the function without saving the
# # current test to results_table or df_Bible. This function has
# # been updated to work with both versions of the typing
# # test.
# else:
# # The following line adds the latest character typed
# # to verse_response.
# verse_response += character.decode('ascii')
# verse_response_with_newlines += character.decode('ascii')
# # See https://stackoverflow.com/questions/17615414/how-to-convert-binary-string-to-normal-string-in-python3
# # Determining which color to use for the text:
# if verse[0:len(verse_response)] == verse_response:
# text_color = Fore.GREEN
# else:
# text_color = Fore.RED
# # Printing the player's response so far: (Note that
# # verse_response gets printed instead of the last character.
# # The addition of 'end = "\r", flush = True' to the print()
# # call allows characters to get displayed immediately
# # after one another rather than on separate lines.
# print(f"{text_color}{verse_response_with_newlines}", end = "\r", flush = True)
# # The 'end' and 'flush' arguments are
# # Based on https://stackoverflow.com/a/69030559/13097194
# # For the use of Colorama to produce red and green text, see
# # https://pypi.org/project/colorama/
# # and https://stackoverflow.com/a/3332860/13097194
# if verse_response == verse: # Note that, unlike with version
# # v1, the player does not need to hit 'Enter' in order
# # to end the typing test after writing a completed
# # verse. This should speed up his/her WPM as a result.
# typing_end_time = time.time()
# typing_time = typing_end_time - typing_start_time
# print("\nSuccess!")
# print(Style.RESET_ALL)
# # Calculating typing statistics and storing them within a single-row
# # DataFrame:
# cps = len(verse) / typing_time # Calculating characters per second
# wpm = cps * 12 # Multiplying by 60 to convert from characters to minutes,
# # then dividing by 5 to convert from characters to words.
# wpm
# print(f"Your CPS and WPM were {round(cps, 3)} and {round(wpm, 3)}, \
# respectively.")
# # Creating a single-row DataFrame that stores the player's results:
# df_latest_result = pd.DataFrame(index = [
# len(results_table)+1], data = {'Unix_Start_Time':typing_start_time,
# 'Local_Start_Time':local_start_time,
# 'UTC_Start_Time':utc_start_time,
# 'Characters':len(verse),
# 'Seconds':typing_time,
# 'CPS': cps,
# 'WPM':wpm,
# 'Book': book,
# 'Chapter': chapter,
# 'Verse #': verse_number_within_chapter,
# 'Verse':verse,
# 'Verse_Order':verse_number_within_Bible})
# df_latest_result.index.name = 'Test_Number'
# df_latest_result
# # Adding this new row to results_table:
# results_table = pd.concat([results_table, df_latest_result])\
# # Note: I could also have used df.at or df.iloc to add a new row
# # to df_latest_result, but I chose a pd.concat() setup in order to ensure
# # that the latest result would never overwrite an earlier result.
# # Updating df_Bible to store the player's results: (This will allow the
# # player to track how much of the Bible he/she has typed so far)
# df_Bible.at[verse_number-1, 'Typed'] = 1 # Denotes that this verse
# # has now ben typed
# df_Bible.at[verse_number-1, 'Tests'] += 1 # Keeps track of how
# # many times this verse has been typed
# fastest_wpm = df_Bible.at[verse_number-1, 'Fastest_WPM']
# if ((pd.isna(fastest_wpm) == True) | (wpm > fastest_wpm)):
# # In these cases, we should replace the pre-existing Fastest_WPM value
# # with the WPM the player just achieved.
# # I found that 5 > np.NaN returned False, so if I only checked for
# # wpm > fastest_wpm, blank fastest_wpm values would never get overwritten.
# # Therefore, I chose to also check for NaN values
# # in the above if statement.
# df_Bible.at[verse_number-1, 'Fastest_WPM'] = wpm
# return results_table
# if len(verse_response_with_newlines) == 100:
# verse_response_with_newlines = '' # Resets the view to avoid a glitch
# # that can occur when more than one line is present in the output.
# print('\r') # Moves the cursor one line down so that the player
# # doesn't overwrite his/her previous output in the process
# # of writing the new line
# %%
# run_typing_test(1, results_table=df_results)
# %%
def select_subsequent_verse(previous_verse_number):
'''This function allows the player to specify which verse to
type next, or, alternatively, to exit the game.'''
print("Press 0 to retry the verse you just typed; \
1 to type the next verse; 2 to type the next verse that hasn't yet been typed; \
3 to select a different verse; \
or -1 to save your results and exit.")
while True:
try:
response = int(input())
except: # The user didn't enter a number.
print("Please enter a number.")
continue
if response == 0:
return previous_verse_number
elif response == 1:
if previous_verse_number == 35379: # The verse order value
# corresponding to the final verse of Revelation
print("You just typed the last verse in the Bible, so \
there's no next verse to type! Please enter an option other than 1.\n")
continue
else:
return previous_verse_number + 1
elif response == 2:
# In this case, we'll retrieve a list of verses that haven't
# yet been typed; filter that list to include only verses
# greater than previous_verse_number; and then select
# the first verse within that list (i.e. the next
# untyped verse).
verses_not_yet_typed = list(df_Bible.query(
"Typed == 0")['Verse_Order'].copy())
if len(verses_not_yet_typed) == 0:
print("Congratulations! You have typed all verses from \
the Bible, so there are no new verses to type! Try selecting another option \
instead.")
continue
print(f"{len(verses_not_yet_typed)} verses have not yet \
been typed.")
verses_not_yet_typed.sort()
next_untyped_verses = [verse for verse in verses_not_yet_typed
if verse > previous_verse_number]
return next_untyped_verses[0]
elif response == 3:
return select_verse()
elif response == -1:
return response
else: # A number other than -1, 0, 1, 2, or 3 was passed.
print("Please enter either -1, 0, 1, 2, or 3.\n")
# %%
def calculate_current_day_results(df):
''' This function reports the number of characters, total verses, and
unique verses that the player has typed so far today.'''
df_current_day_results = df[pd.to_datetime(
df['Local_Start_Time']).dt.date == datetime.today().date()].copy()
if len(df_current_day_results) == 0:
result_string = "You haven't typed any Bible verses yet today."
else:
characters_typed_today = df_current_day_results['Characters'].sum()
total_verses_typed_today = len(df_current_day_results)
# Allowing for both singular and plural versions of 'verse' to
# be displayed:
if total_verses_typed_today == 1:
total_verses_string = 'verse'
else:
total_verses_string = 'verses'
unique_verses_typed_today = len(df_current_day_results[
'Verse_Order'].unique())
if unique_verses_typed_today == 1:
unique_verses_string = 'verse'
else:
unique_verses_string = 'verses'
average_wpm_today = round(df_current_day_results['WPM'].mean(), 3)
median_wpm_today = round(df_current_day_results['WPM'].median(), 3)
result_string = f"So far today, you have typed \
{characters_typed_today} characters from {total_verses_typed_today} Bible \
{total_verses_string} (including {unique_verses_typed_today} unique \
{unique_verses_string}). Your mean and median WPM today are \
{average_wpm_today} and {median_wpm_today}, respectively."
return result_string
# %%
# %%
def run_game(results_table):
'''This function runs Type Through the Bible by
calling various other functions. It allows users to select
verses to type, then runs typing tests and stores the results in
the DataFrame passed to results_table.'''
print("Welcome to Type Through the Bible!")
# The game will now share the player's progress for the current day:
print(calculate_current_day_results(results_table))
print("To switch to a simpler typing test method that doesn't \
check your input as you type, enter v1. Otherwise, to stick with the \
recommended version, press Enter.")
response = input()
if (response == 'v1') or (run_on_notebook == True): # Version 2 likely
# won't work within Jupyter notebooks,
# so the version will always be kept as v1 for notebook users.
typing_test_version = 'v1'
else:
typing_test_version = 'v2'
verse_number = select_verse()
while True: # Allows the game to continue until the user exits
results_table = run_typing_test(verse_number=verse_number,
results_table=results_table, test_type = typing_test_version)
# The game will next share an updated progress report:
print(calculate_current_day_results(results_table))
# The player will now be prompted to select a new verse number
# (or to save and quit). This verse_number, provided it is not -1,
# will then be passed back to run_typing_test().
verse_number = select_subsequent_verse(
previous_verse_number=verse_number)
if verse_number == -1: # In this case, the game will quit and the
# user's new test results will be saved to results_table.
return results_table
# %%
df_results = run_game(results_table = df_results)
# %%
# Updating certain df_Bible columns to reflect new results:
# %%
df_Bible['Characters_Typed'] = df_Bible['Characters'] * df_Bible['Typed']
df_Bible['Total_Characters_Typed'] = df_Bible['Characters'] * df_Bible['Tests']
df_Bible
# %%
characters_typed_sum = df_Bible['Characters_Typed'].sum()
proportion_of_Bible_typed = characters_typed_sum / df_Bible['Characters'].sum()
print(f"You have typed {characters_typed_sum} characters so far, which represents \
{round(100*proportion_of_Bible_typed, 4)}% of the Bible.")
# %% [markdown]
# # Adding in additional values and statistics to df_results:
#
# (The following cell was derived from [this script](https://github.com/kburchfiel/typeracer_data_analyzer/blob/master/typeracer_data_analyzer_v2.ipynb) that I wrote.)
#
# These statistics will get recreated whenever the script is run; this approach allows for the results to be revised as needed (e.g. if certain rows are removed from the dataset).
# %%
df_results['Last 10 Avg'] = df_results['WPM'].rolling(10).mean()
df_results['Last 100 Avg'] = df_results['WPM'].rolling(100).mean()
df_results['Last 1000 Avg'] = df_results['WPM'].rolling(1000).mean()
df_results['Local_Year'] = pd.to_datetime(df_results['Local_Start_Time']).dt.year
df_results['Local_Month'] = pd.to_datetime(df_results['Local_Start_Time']).dt.month
df_results['Local_Date'] = pd.to_datetime(df_results['Local_Start_Time']).dt.date
df_results['Local_Hour'] = pd.to_datetime(df_results['Local_Start_Time']).dt.hour
df_results['Count'] = 1 # Useful for pivot tables that analyze test counts
# by book, month, etc.
# The following line uses a list comprehension to generate a cumulative average
# of all WPM scores up until the current race. .iloc searches from 0 to i+1 for
# each row so that that row is included in the calculation.
df_results['cumulative_avg'] = [round(np.mean(df_results.iloc[0:i+1]['WPM']),
3) for i in range(len(df_results))]
df_results
# %%
print("Saving results:")
# %%
def attempt_save(df, filename, index):
'''This function attempts to save the DataFrame passed to df to the file
specified by filename. It allows players to retry the save operation
if it wasn't initially successful (e.g. because the file was open at
the time), thus preventing them from losing their latest progress.
The index parameter determines whether or not the DataFrame's index
will be included in the .csv export. Set to True for results.csv
but False for Web_Catholic_Version_for_game_updated.csv.'''
while True:
try:
df.to_csv(filename, index = index)
return
except:
print("File could not be saved, likely because it is currently open. \
Try closing the file and trying again. Press Enter to retry.")
input()
# %%
attempt_save(df_results, 'results.csv', index = True)
# %%
attempt_save(df_Bible, 'WEB_Catholic_Version_for_game_updated.csv', index = False)
# %%
print("Successfully saved updated copies of the Results and Bible .csv files.")
# %% [markdown]
# # Visualizing the player's progress in typing the entire Bible:
# %%
analysis_start_time = time.time() # Allows us to determine how long the
# analyses took
print("Updating analyses:")
# %%
df_Bible['Count'] = 1
# %% [markdown]
# ### Creating a tree map within Plotly that visualizes the player's progress in typing the entire Bible:
# %%
# This code is based on https://plotly.com/python/treemaps/
# It's pretty amazing that such a complex visualization can be created using
# just one line of code. Thanks Plotly!
fig_tree_map_books_chapters_verses = px.treemap(
df_Bible, path = ['Book_Name', 'Chapter_Name', 'Verse_#'],
values = 'Characters', color = 'Typed')
# fig_verses_typed
# %%
fig_tree_map_books_chapters_verses.write_html(
'Analyses/tree_map_books_chapters_verses.html')
# %%
# # A similar chart that doesn't use the Typed column for color coding:
# (This chart, unlike fig_verses_typed above, won't change unless edits are
# made to the code itself, so it can be
# commented out after being run once.)
# fig_Bible_verses.write_html('Bible_tree_map.html')
# fig_Bible_verses = px.treemap(df_Bible, path = ['Book_Name',
# 'Chapter_Name', 'Verse_#'], values = 'Characters')
# fig_Bible_verses
# %%
df_Bible
# %%
# This variant of the treemap shows chapters and verses rather than books,
# chapters, and verses.
if (run_on_notebook == True) & (extra_analyses == True):
fig_tree_map_chapters_verses = px.treemap(df_Bible, path = [
'Book_and_Chapter', 'Verse_#'], values = 'Characters', color = 'Typed')
fig_tree_map_chapters_verses.write_html(
'Analyses/tree_map_chapters_verses.html')
fig_tree_map_chapters_verses.write_image(
'Analyses/tree_map_chapters_verses.png', width = 7680, height = 4320)
# %%
# This variant of the treemap shows each verse as its own box, which results in
# a very busy graph that takes a while to load within a web browser
# (if it even loads at all).
if (run_on_notebook == True) & (extra_analyses == True):
fig_tree_map_verses = px.treemap(df_Bible, path = ['Verse_Order'],
values = 'Characters', color = 'Typed')
fig_tree_map_verses.write_html('Analyses/tree_map_verses.html')
fig_tree_map_verses.write_image('Analyses/tree_map_verses_8K.png',
width = 7680, height = 4320)
fig_tree_map_verses.write_image('Analyses/tree_map_verses_16K.png',
width = 15360, height = 8640)
# fig_tree_map_verses.write_image('Analyses/tree_map_verses.png', width = 30720,
# height = 17280) # Didn't end up rendering successfully, probably
# because the dimensions were absurdly large!
# %% [markdown]
# ### Creating a bar chart that shows the proportion of each book that has been typed so far:
# %%
df_characters_typed_by_book = df_Bible.pivot_table(index = ['Book_Order',
'Book_Name'], values = ['Characters', 'Characters_Typed'],
aggfunc = 'sum').reset_index()
# Adding 'Book_Order' as the first index value allows for the pivot tables
# and bars to be ordered by that value.
df_characters_typed_by_book['proportion_typed'] = df_characters_typed_by_book[
'Characters_Typed'] / df_characters_typed_by_book['Characters']
df_characters_typed_by_book.to_csv(
'Analyses/characters_typed_by_book.csv')
df_characters_typed_by_book
# %%
fig_proportion_of_each_book_typed = px.bar(df_characters_typed_by_book,
x = 'Book_Name', y = 'proportion_typed')
fig_proportion_of_each_book_typed.update_yaxes(range = [0, 1]) # Setting
# the maximum y value as 1 better demonstrates how much of the Bible
# has been typed so far
fig_proportion_of_each_book_typed.write_html(
'Analyses/proportion_of_each_book_typed.html')
fig_proportion_of_each_book_typed.write_image(
'Analyses/proportion_of_each_book_typed.png',
width = 1920, height = 1080, engine = 'kaleido', scale = 2)
fig_proportion_of_each_book_typed
# %% [markdown]
# ### Creating a chart that compares the number of characters in each book with the number that have been typed:
#
# This provides a clearer view of the player's progress in typing the Bible, as each bar's height is based on the number of characters. (In contrast, bars for fully typed small books will be just as high in fig_proportion_of_each_book_typed as those for fully typed large books.)
# %%
fig_characters_typed_in_each_book = px.bar(df_characters_typed_by_book,
x = 'Book_Name', y = ['Characters', 'Characters_Typed'], barmode = 'overlay')
fig_characters_typed_in_each_book.write_html(
'Analyses/characters_typed_by_book.html')
fig_characters_typed_in_each_book.write_image(
'Analyses/characters_typed_by_book.png',
width = 1920, height = 1080, engine = 'kaleido', scale = 2)
fig_characters_typed_in_each_book
# %% [markdown]
# ## Creating charts that show both book- and chapter-level data:
# %%
df_characters_typed_by_book_and_chapter = df_Bible.pivot_table(index = [
'Book_Order', 'Book_Name', 'Book_and_Chapter'], values = [
'Characters', 'Characters_Typed'], aggfunc = 'sum').reset_index()
df_characters_typed_by_book_and_chapter[
'proportion_typed'] = df_characters_typed_by_book_and_chapter[
'Characters_Typed'] / df_characters_typed_by_book_and_chapter['Characters']
df_characters_typed_by_book_and_chapter.to_csv(
'Analyses/characters_typed_by_book_and_chapter.csv')
df_characters_typed_by_book_and_chapter
# %% [markdown]
# The following chart shows both books (as bars) and chapters (as sections of these bars). These sections are also color coded by the proportion of each chapter that has been typed.
# %%
fig_characters_typed_in_each_book_and_chapter = px.bar(
df_characters_typed_by_book_and_chapter, x = 'Book_Name', y = [
'Characters'], color = 'proportion_typed')
fig_characters_typed_in_each_book_and_chapter.write_html(
'Analyses/characters_typed_by_book_and_chapter.html')
fig_characters_typed_in_each_book_and_chapter.write_image(
'Analyses/characters_typed_by_book_and_chapter.png',
width = 1920, height = 1080, engine = 'kaleido', scale = 2)
fig_characters_typed_in_each_book_and_chapter
# %% [markdown]
# ## Creating similar charts at the chapter level:
#
# These proved difficult to interpret due to the narrowness of the bars, so I'm commenting this code out for now.
# %%
# fig_proportion_of_each_chapter_typed = px.bar(df_characters_typed_by_chapter,
# x = 'Book_and_Chapter', y = 'proportion_typed')
# fig_proportion_of_each_chapter_typed.update_yaxes(range = [0, 1]) # Setting
# # the maximum y value as 1 better demonstrates how much of the Bible
# # has been typed so far
# fig_proportion_of_each_chapter_typed.write_html(
# 'Analyses/proportion_of_each_chapter_typed.html')
# fig_proportion_of_each_chapter_typed
# fig_characters_typed_in_each_chapter = px.bar(df_characters_typed_by_chapter,
# x = 'Book_and_Chapter', y = ['Characters', 'Characters_Typed'],
# barmode = 'overlay')
# fig_characters_typed_in_each_chapter.write_html(
# 'Analyses/characters_typed_by_chapter.html')
# fig_characters_typed_in_each_chapter
# %% [markdown]
# ## Calculating the dates with the most characters and verses typed:
# %%
df_top_dates_by_characters = df_results.pivot_table(
index = 'Local_Date', values = 'Characters', aggfunc = 'sum').reset_index(
).sort_values('Characters', ascending = False).head(50)
df_top_dates_by_characters['Rank'] = df_top_dates_by_characters[
'Characters'].rank(ascending = False, method = 'min').astype('int')
# Creating a column that shows both the rank and date: (This also prevents
# Plotly from converting the x axis to a date range, which would interfere
# with the order of the chart items)
df_top_dates_by_characters['Rank and Date'] = '#'+df_top_dates_by_characters[
'Rank'].astype('str') + ': ' + df_top_dates_by_characters[
'Local_Date'].astype('str')
df_top_dates_by_characters.reset_index(drop=True,inplace=True)
# %%
fig_top_dates_by_characters = px.bar(df_top_dates_by_characters,
x = 'Rank and Date', y = 'Characters')
fig_top_dates_by_characters.update_xaxes(tickangle = 90)
fig_top_dates_by_characters.write_html('Analyses/top_dates_by_characters.html')
fig_top_dates_by_characters.write_image(
'Analyses/top_dates_by_characters.png',
width = 1920, height = 1080, engine = 'kaleido', scale = 2)
fig_top_dates_by_characters