Skip to content

Commit db1c84f

Browse files
Merge pull request steam-bell-92#1337 from mrudulaa11/feature/emoji-memory-streak-timer
feat: add streak system, timer-based input validation, and unit tests for Emoji Memory Game (steam-bell-92#1229)
2 parents 4436b43 + 73b6d8c commit db1c84f

2 files changed

Lines changed: 331 additions & 32 deletions

File tree

Lines changed: 178 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,151 @@
11
import random
22
import time
33
import os
4+
import sys
5+
import threading
46

5-
def main():
6-
global HIGH_SCORE_FILE, choice, display_time, emojis, high_score, i, level, play_again, replay, score, sequence, user_input, user_input_raw
7-
emojis = ["🍎", "🚗", "⚽", "🐍", "🎧", "🔥", "🌈", "🚀"]
8-
HIGH_SCORE_FILE = "highscore.txt"
7+
EMOJIS = ["🍎", "🚗", "⚽", "🐍", "🎧", "🔥", "🌈", "🚀"]
8+
HIGH_SCORE_FILE = "highscore.txt"
9+
10+
DIFFICULTY_TIME_LIMITS = {
11+
"1": 5, # Easy - memorise time
12+
"2": 4, # Medium - memorise time
13+
"3": 2, # Hard - memorise time
14+
}
15+
16+
DIFFICULTY_INPUT_LIMITS = {
17+
"1": 10, # Easy - input time
18+
"2": 8, # Medium - input time
19+
"3": 5, # Hard - input time
20+
}
21+
22+
23+
24+
# Core, testable logic (no I/O, no global state)
25+
26+
def get_time_limit(choice):
27+
"""Return the memorise display time (seconds) for a difficulty choice."""
28+
return DIFFICULTY_TIME_LIMITS.get(choice, 4)
29+
30+
31+
def get_input_limit(choice):
32+
"""Return the allowed input time (seconds) for a difficulty choice."""
33+
return DIFFICULTY_INPUT_LIMITS.get(choice, 8)
34+
35+
36+
def generate_sequence(emojis, level):
37+
"""Generate a random emoji sequence for the given level.
38+
39+
Sequence length grows with level (level + 2), matching the
40+
original game's difficulty curve.
41+
"""
42+
if level < 1:
43+
raise ValueError("level must be >= 1")
44+
return random.choices(emojis, k=level + 2)
45+
46+
47+
def parse_input(raw_input):
48+
"""Turn raw user text into a list of emoji tokens."""
49+
return [token.strip() for token in raw_input.split() if token.strip()]
50+
51+
52+
def validate_input(user_sequence, correct_sequence):
53+
"""Return True if the user's sequence exactly matches the correct one."""
54+
return user_sequence == correct_sequence
55+
56+
57+
def is_timeout(elapsed_seconds, time_limit):
58+
"""Return True if elapsed_seconds exceeds the allowed time_limit."""
59+
return elapsed_seconds > time_limit
60+
61+
62+
def update_streak(is_correct, current_streak):
63+
"""Increment streak on a correct round, reset to 0 on an incorrect one."""
64+
return current_streak + 1 if is_correct else 0
65+
66+
67+
def format_countdown_message(remaining_seconds):
68+
"""Return the display string for a given number of seconds left."""
69+
if remaining_seconds <= 0:
70+
return "⏱️ Time's up!"
71+
return f"⏱️ {remaining_seconds}s left..."
72+
73+
74+
def calculate_score(level, streak):
75+
"""Calculate score for a correctly-answered round.
976
10-
# Load or initialize high score ONCE at the start of the program
77+
Base score scales with level; a streak bonus rewards consecutive
78+
correct answers.
79+
"""
80+
base = level * 10
81+
streak_bonus = streak * 2
82+
return base + streak_bonus
83+
84+
85+
86+
# I/O helpers
87+
88+
def load_high_score():
1189
if not os.path.exists(HIGH_SCORE_FILE):
1290
try:
1391
with open(HIGH_SCORE_FILE, "w", encoding="utf-8") as file:
1492
file.write("0")
1593
except OSError as e:
1694
print(f"⚠️ Warning: Could not initialize high score file: {e}")
1795

18-
high_score = 0
1996
try:
2097
with open(HIGH_SCORE_FILE, "r", encoding="utf-8") as file:
21-
high_score = int(file.read().strip() or "0")
98+
return int(file.read().strip() or "0")
2299
except (FileNotFoundError, ValueError, OSError) as e:
23100
print(f"⚠️ Warning: Could not read high score file: {e}")
24-
high_score = 0
101+
return 0
102+
103+
104+
def save_high_score(score):
105+
try:
106+
with open(HIGH_SCORE_FILE, "w", encoding="utf-8") as file:
107+
file.write(str(score))
108+
except OSError as e:
109+
print(f"⚠️ Could not save high score: {e}")
110+
111+
112+
def clear_screen():
113+
os.system('cls' if os.name == 'nt' else 'clear')
114+
115+
116+
def run_countdown(duration, stop_event):
117+
"""Update a single countdown line once per second while waiting for input.
118+
119+
Runs in a background thread. Instead of printing a new line each
120+
tick, it:
121+
1. Saves the cursor's current position (which is on the input
122+
line, wherever the user has typed so far).
123+
2. Moves the cursor up 2 lines to the dedicated countdown line.
124+
3. Clears that line and writes the new remaining-time message.
125+
4. Restores the cursor back to the input line, so the user's
126+
typing is completely undisturbed.
127+
128+
stop_event is set as soon as the user submits their answer, so the
129+
countdown stops immediately rather than continuing to update.
130+
"""
131+
remaining = duration
132+
while remaining > 0:
133+
if stop_event.wait(timeout=1):
134+
return # user already answered, stop counting
135+
remaining -= 1
136+
sys.stdout.write("\0337") # save cursor position
137+
sys.stdout.write("\033[2A") # move up 2 lines -> countdown line
138+
sys.stdout.write("\r\033[2K") # jump to col 0 and clear the line
139+
sys.stdout.write(format_countdown_message(remaining))
140+
sys.stdout.write("\0338") # restore cursor -> back to input line
141+
sys.stdout.flush()
142+
143+
144+
# Game loop
145+
146+
def main():
147+
high_score = load_high_score()
25148

26-
# Main Game Loop
27149
while True:
28150
print("\n🎮 Welcome to Emoji Memory Game!")
29151
print(f"🏅 High Score: {high_score}\n")
@@ -35,52 +157,79 @@ def main():
35157

36158
while True:
37159
choice = input("Enter choice (1/2/3): ").strip()
38-
if choice in ['1', '2', '3']:
160+
if choice in DIFFICULTY_TIME_LIMITS:
39161
break
40162
print("⚠️ Invalid input. Enter 1, 2, or 3.")
41163

42-
display_time = 4
43-
if choice == "1":
44-
display_time = 5
45-
elif choice == "3":
46-
display_time = 2
164+
display_time = get_time_limit(choice)
165+
input_time = get_input_limit(choice)
47166

48167
print("\n⏳ Get Ready!")
49168
for i in range(3, 0, -1):
50169
print(i)
51170
time.sleep(1)
52-
os.system('cls' if os.name == 'nt' else 'clear')
171+
clear_screen()
53172

54173
score = 0
55174
level = 1
175+
streak = 0
56176

57177
# Gameplay Loop
58178
while True:
59-
sequence = random.choices(emojis, k=level + 2)
179+
sequence = generate_sequence(EMOJIS, level)
60180

61181
print("🧠 MEMORIZE THESE EMOJIS:")
62182
print(" ".join(sequence))
63183

64184
time.sleep(display_time)
65-
os.system('cls' if os.name == 'nt' else 'clear')
185+
clear_screen()
186+
187+
# Line 1: countdown line (updated in place by run_countdown)
188+
# Line 2: static prompt text
189+
# Line 3: "> " where the user actually types
190+
print(format_countdown_message(input_time))
191+
print("Type the emojis in order (separated by spaces):")
192+
sys.stdout.write("> ")
193+
sys.stdout.flush()
66194

67-
user_input_raw = input("Type the emojis in order (separated by spaces):\n> ").strip()
68-
user_input = [emoji.strip() for emoji in user_input_raw.split()]
195+
stop_event = threading.Event()
196+
timer_thread = threading.Thread(
197+
target=run_countdown, args=(input_time, stop_event), daemon=True
198+
)
199+
timer_thread.start()
200+
201+
start_time = time.time()
202+
user_input_raw = input().strip()
203+
elapsed = time.time() - start_time
204+
205+
stop_event.set()
206+
timer_thread.join(timeout=0.2)
207+
208+
user_input = parse_input(user_input_raw)
209+
timed_out = is_timeout(elapsed, input_time)
69210

70211
if not user_input:
71212
print("⚠️ Empty input detected!")
72213
print("❌ Wrong!")
73214
print("Correct sequence was:", " ".join(sequence))
74215
break
75216

76-
if user_input == sequence:
77-
score += level * 10
217+
correct = validate_input(user_input, sequence) and not timed_out
218+
219+
streak = update_streak(correct, streak)
220+
221+
if correct:
222+
score += calculate_score(level, streak)
78223
print("✅ Correct!")
79224
level += 1
80-
print(f"🏆 Score: {score}\n")
225+
print(f"🏆 Score: {score}")
226+
print(f"🔥 Streak: {streak}\n")
81227
else:
228+
if timed_out:
229+
print("⏱️ Time's up!")
82230
print("❌ Wrong!")
83231
print("Correct sequence was:", " ".join(sequence))
232+
print(f"🔥 Streak: {streak}")
84233
break
85234

86235
# Game Over Processing
@@ -89,16 +238,12 @@ def main():
89238

90239
if score > high_score:
91240
print("🔥 NEW HIGH SCORE!")
92-
high_score = score # Update local memory variable
93-
try:
94-
with open(HIGH_SCORE_FILE, "w", encoding="utf-8") as file:
95-
file.write(str(score))
96-
except OSError as e:
97-
print(f"⚠️ Could not save high score: {e}")
241+
high_score = score
242+
save_high_score(score)
98243
else:
99244
print(f"🏅 High Score remains: {high_score}")
100245

101-
# Replay Loop Fix
246+
# Replay Loop
102247
play_again = False
103248
while True:
104249
replay = input("\nPlay again? (y/n): ").strip().lower()
@@ -112,7 +257,8 @@ def main():
112257

113258
if not play_again:
114259
print("👋 Thanks for playing!")
115-
break # Safely breaks the main outer loop and exits the game
260+
break
261+
116262

117263
if __name__ == '__main__':
118-
main()
264+
main()

0 commit comments

Comments
 (0)