Skip to content

Commit 3aa6577

Browse files
committed
add tcrracer
1 parent cdeded1 commit 3aa6577

1 file changed

Lines changed: 173 additions & 0 deletions

File tree

tcrracer/tcrracer.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import colorama
5+
import os
6+
import random
7+
import signal
8+
import string
9+
import subprocess
10+
import sys
11+
import tempfile
12+
import threading
13+
import time
14+
import token
15+
import tokenize
16+
17+
from colorama import Fore, Style
18+
from difflib import SequenceMatcher
19+
from pathlib import Path
20+
21+
location = Path(sys.argv[0]).parent
22+
content = location / ".." / "content"
23+
24+
default_patterns = {
25+
"cpp": ["*.cc", "*.C", "*.cpp", "*.cxx", "*.c++"],
26+
"py": ["*.py", "*.py2", "*.py3", "*.cpy"],
27+
"java": ["*.java"],
28+
}
29+
30+
colorama.init()
31+
clear = "\033[K"
32+
33+
def interrupt_handler(sig, frame):
34+
print(f"\n{clear}{Fore.RED}aborted{Style.RESET_ALL}")
35+
os._exit(1)
36+
37+
signal.signal(signal.SIGINT, interrupt_handler)
38+
39+
parser = argparse.ArgumentParser(description="TCR-Racer is a game to train your TCR typing ability.")
40+
parser.add_argument("-p", dest="penalty", default="60", type=int, help='The penalty for "submitting" wrong code (in seconds)')
41+
parser.add_argument("--patterns", default=None, nargs="+", help=f"Patterns to check which files in '{content}' should be considered")
42+
for name, pattern in default_patterns.items():
43+
pattern_text = ",".join(pattern)
44+
parser.add_argument(f"--{name}", action="store_true", help=f"add {name} pattern ({pattern_text})")
45+
parser.add_argument("--debug", action="store_true", help="Show debug information")
46+
args = parser.parse_args()
47+
48+
selected_default_patterns = []
49+
for name, pattern in default_patterns.items():
50+
if getattr(args, name):
51+
selected_default_patterns += pattern
52+
if selected_default_patterns:
53+
args.patterns = (args.patterns or []) + selected_default_patterns
54+
55+
def matches_pattern(f):
56+
if args.patterns is None:
57+
return True
58+
return any(f.match(p) for p in args.patterns)
59+
60+
files = [f for f in content.rglob("*") if f.is_file() and f.suffix not in [".tex", ".sty"]]
61+
filtered = [f for f in files if matches_pattern(f)]
62+
print(f"Found {len(filtered)} matching files.")
63+
if not filtered:
64+
sys.exit(1)
65+
selected = random.choice(filtered)
66+
67+
language = "unknown"
68+
for name, pattern in default_patterns.items():
69+
if any(selected.match(p) for p in pattern):
70+
language = name
71+
if language in ["cpp", "py", "java"]:
72+
print(f"Language is: {language}, comments are ignored")
73+
else:
74+
print(f"{Fore.YELLOW}WARNING:{Style.RESET_ALL} Language is unknown, you must type comments")
75+
76+
def normalize(file):
77+
if language in ["cpp", "java"]:
78+
# call c preprocessor to remove macros, comments and stuff
79+
with open(file) as f:
80+
text = subprocess.check_output(["cpp", "-dD", "-P", "-fpreprocessed"], stdin=f, encoding="utf-8")
81+
# remove whitespaces
82+
for whitespace in string.whitespace:
83+
text = text.replace(whitespace, "")
84+
return text
85+
elif language == "py":
86+
previous = token.INDENT
87+
cleaned = []
88+
with open(file) as f:
89+
for type, value, _, _, _ in tokenize.generate_tokens(f.readline):
90+
# remove comment and multiline comment
91+
if type == token.COMMENT:
92+
continue
93+
elif type == token.STRING and previous == token.INDENT:
94+
continue
95+
# remove whitespaces
96+
cleaned.append(value.strip())
97+
previous = type
98+
return "".join(cleaned)
99+
else:
100+
text = file.read_text()
101+
# remove whitespaces
102+
for whitespace in string.whitespace:
103+
text = text.replace(whitespace, "")
104+
return text
105+
106+
expected = normalize(selected)
107+
108+
tmpfile = (Path(tempfile.gettempdir()) / "tcrracer").with_suffix(selected.suffix)
109+
tmpfile.write_text("")
110+
111+
print("You have to type to: ", Fore.YELLOW, tmpfile, Style.RESET_ALL, sep="")
112+
#print("Press Enter to start",end="")
113+
input("Press Enter to start")
114+
print()
115+
print("You have to type: ", Fore.YELLOW, selected.relative_to(content), Style.RESET_ALL, sep="")
116+
if args.debug:
117+
print("Expected:", f"{Fore.YELLOW}{expected}{Style.RESET_ALL}")
118+
print('Press enter to "submit" (or ctrl+c to abort)')
119+
print()
120+
121+
accepted = False
122+
tries = 0
123+
start = time.perf_counter()
124+
125+
def edit_distance(a, b):
126+
n, m = len(a), len(b)
127+
dp = [[0] * (m + 1) for _ in range(n + 1)]
128+
for i in range(n + 1):
129+
dp[i][0] = i
130+
for j in range(m + 1):
131+
dp[0][j] = j
132+
for i in range(1, n + 1):
133+
for j in range(1, m + 1):
134+
if a[i - 1] == b[j - 1]:
135+
dp[i][j] = dp[i - 1][j - 1]
136+
else:
137+
dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
138+
return dp[n][m]
139+
140+
def submit():
141+
global accepted
142+
global tries
143+
while not accepted:
144+
debug = input()
145+
got = normalize(tmpfile)
146+
equal = got == expected
147+
verdict = f"{Fore.GREEN}AC{Style.RESET_ALL}" if equal else f"{Fore.RED}WA{Style.RESET_ALL}"
148+
message = [f"{clear}submitted:", verdict]
149+
if args.debug:
150+
message.append(f"(distance: {edit_distance(got, expected)})")
151+
if debug == "ac": equal = True
152+
if debug == "wa": equal = False
153+
print(*message)
154+
if equal:
155+
accepted = True
156+
else:
157+
tries += 1
158+
159+
threading.Thread(target=submit, daemon=True).start()
160+
161+
elapsed = 0
162+
penalty = 0
163+
while not accepted:
164+
elapsed = time.perf_counter() - start
165+
penalty = args.penalty * tries
166+
print(f"time: {elapsed + penalty:.2f}s (try: {tries+1})", end="\r", flush=True)
167+
time.sleep(0.075)
168+
print()
169+
print(f"time: {elapsed + penalty:.2f}s (tries: {tries+1})")
170+
print(f"speed: ~{len(expected) / elapsed:.2f} chars per second", end="")
171+
if penalty:
172+
print(f", ~{len(expected) / (elapsed + penalty):.2f} with penalty", end="")
173+
print()

0 commit comments

Comments
 (0)