11# ...existing code...
2- """
3- Word Scramble — interactive CLI game
2+ """Word Scramble — interactive CLI game.
3+
44Features:
55 - Difficulty levels (easy / medium / hard)
66 - Multiple rounds per game based on difficulty
1212
1313import json
1414import random
15+ import sys
1516import time
1617from pathlib import Path
17- from datetime import datetime
18+ from typing import Dict , List , Optional , Sequence , Union
1819
1920# Files (stored in the same folder)
2021DATA_DIR = Path (__file__ ).parent
6869]
6970
7071# Optional categories to make the game more interesting — each category is a small subset
72+ # small sets to keep comprehensions readable and within line length
73+ PROGRAMMING_SET = {
74+ "python" ,
75+ "function" ,
76+ "variable" ,
77+ "module" ,
78+ "algorithm" ,
79+ "debug" ,
80+ "compile" ,
81+ "object" ,
82+ "inheritance" ,
83+ "interface" ,
84+ "repository" ,
85+ "commit" ,
86+ "lambda" ,
87+ "generator" ,
88+ "concurrency" ,
89+ "thread" ,
90+ "process" ,
91+ }
92+
7193CATEGORIES = {
7294 "general" : WORD_LIST ,
73- "programming" : [
95+ "programming" : [w for w in WORD_LIST if w in PROGRAMMING_SET ],
96+ "networking" : [
7497 w
7598 for w in WORD_LIST
76- if w
77- in (
78- "python" ,
79- "function" ,
80- "variable" ,
81- "module" ,
82- "algorithm" ,
83- "debug" ,
84- "compile" ,
85- "object" ,
86- "inheritance" ,
87- "interface" ,
88- "repository" ,
89- "commit" ,
90- "lambda" ,
91- "generator" ,
92- "concurrency" ,
93- "thread" ,
94- "process" ,
95- )
96- ],
97- "networking" : [
98- w for w in WORD_LIST if w in ("network" , "protocol" , "bandwidth" , "packet" )
99+ if w in ("network" , "protocol" , "bandwidth" , "packet" )
99100 ],
100101}
101102
164165}
165166
166167
167- def load_high_scores ():
168- """Load high scores from disk; return list of dicts."""
168+ def load_high_scores () -> List [Dict [str , Union [str , int ]]]:
169+ """Load high scores from disk; return list of dicts.
170+
171+ Each entry is expected to contain at least 'name' and 'score'.
172+ """
169173 if HIGH_SCORES_FILE .exists ():
170174 try :
171175 with HIGH_SCORES_FILE .open ("r" , encoding = "utf-8" ) as fh :
172176 return json .load (fh )
173- except Exception :
177+ except (json .JSONDecodeError , OSError ):
178+ # Corrupted file or IO issue — return empty scores
174179 return []
175180 return []
176181
177182
178- def save_high_scores (scores ) :
183+ def save_high_scores (scores : Sequence [ Dict [ str , Union [ str , int ]]]) -> None :
179184 """Save high scores to disk (keep only top 10)."""
180- scores = sorted (scores , key = lambda s : s ["score" ], reverse = True )[:10 ]
185+ # Use descriptive name for sorting key
186+ top_scores : List [Dict [str , Union [str , int ]]] = sorted (
187+ scores , key = lambda entry : entry ["score" ], reverse = True
188+ )[:10 ]
181189 with HIGH_SCORES_FILE .open ("w" , encoding = "utf-8" ) as fh :
182- json .dump (scores , fh , indent = 2 )
190+ json .dump (top_scores , fh , indent = 2 )
183191
184192
185- def load_stats ():
193+ def load_stats () -> Dict [ str , int ] :
186194 """Load simple player stats (games_played, best_streak)."""
187195 if STATS_FILE .exists ():
188196 try :
189197 with STATS_FILE .open ("r" , encoding = "utf-8" ) as fh :
190198 return json .load (fh )
191- except Exception :
199+ except ( json . JSONDecodeError , OSError ) :
192200 return {"games_played" : 0 , "best_streak" : 0 }
193201 return {"games_played" : 0 , "best_streak" : 0 }
194202
195203
196- def save_stats (stats ) :
204+ def save_stats (stats : Dict [ str , int ]) -> None :
197205 try :
198206 with STATS_FILE .open ("w" , encoding = "utf-8" ) as fh :
199207 json .dump (stats , fh , indent = 2 )
200- except Exception :
201- pass
208+ except OSError as exc : # disk write / permissions issue
209+ print ( f"Failed to save stats: { exc } " , file = sys . stderr )
202210
203211
204- def show_high_scores ():
212+ def show_high_scores () -> None :
205213 scores = load_high_scores ()
206214 if not scores :
207215 print ("\n No high scores yet. Be the first!\n " )
@@ -213,7 +221,7 @@ def show_high_scores():
213221 print ()
214222
215223
216- def scramble_word (word ) :
224+ def scramble_word (word : str ) -> str :
217225 """Return a scrambled version of word that's not identical to original if possible."""
218226 if len (set (word )) == 1 :
219227 # all same letter (rare) — return as-is
@@ -229,12 +237,12 @@ def scramble_word(word):
229237 return scrambled # fallback
230238
231239
232- def color (text , code ) :
240+ def color (text : str , code : str ) -> str :
233241 """Lightweight colored output using ANSI codes. Works on modern terminals."""
234242 return f"\x1b [{ code } m{ text } \x1b [0m"
235243
236244
237- def compute_score (word , elapsed , difficulty ) :
245+ def compute_score (word : str , elapsed : float , difficulty : str ) -> int :
238246 """Compute score for a correct guess."""
239247 base = len (word ) * 10
240248 mult = DIFFICULTIES [difficulty ]["multiplier" ]
@@ -244,8 +252,12 @@ def compute_score(word, elapsed, difficulty):
244252 return int (base * mult + time_bonus )
245253
246254
247- def play_round (word , difficulty ):
248- """Play one scramble round. Returns points earned (0 if failed/skip)."""
255+ def play_round (word : str , difficulty : str ) -> Union [int , tuple ]:
256+ """Play one scramble round. Returns points earned (0 if failed/skip).
257+
258+ - Returns an int for points.
259+ - Returns ("quit", 0) if the player chose to quit.
260+ """
249261 max_time = DIFFICULTIES [difficulty ]["max_time" ]
250262 scrambled = scramble_word (word )
251263 hint_mask = ["_" ] * len (word )
@@ -254,7 +266,8 @@ def play_round(word, difficulty):
254266
255267 print ("\n Scrambled:" , " " .join (scrambled ))
256268 print (
257- f"(Type your guess, or 'hint', 'skip', 'quit'. Time suggested: { max_time } s)\n "
269+ "(Type your guess, or 'hint', 'skip', 'quit'."
270+ f" Time suggested: { max_time } s)\n "
258271 )
259272
260273 start = time .perf_counter ()
@@ -282,61 +295,52 @@ def play_round(word, difficulty):
282295 # check answer
283296 if guess == word :
284297 points = compute_score (word , elapsed , difficulty )
285- print (
286- color (
287- f"Correct! +{ points } pts (time: { int (elapsed )} s, attempts: { attempts } )" ,
288- "32" ,
289- )
290- )
298+ print (color (f"Correct! +{ points } pts (time: { int (elapsed )} s, attempts: { attempts } )" , '32' ))
291299 return points
292300 else :
293301 # small helpful feedback
294302 same_positions = sum (1 for a , b in zip (guess , word ) if a == b )
295- print (
296- f"Not quite. { same_positions } letter(s) in the correct position. Try again."
297- )
303+ print (f"Not quite. { same_positions } letter(s) in the correct position. Try again." )
298304
299305
300- def pick_word_by_difficulty_and_category (difficulty , category = None ):
306+ def pick_word_by_difficulty_and_category (
307+ difficulty : str , category : Optional [str ] = None
308+ ) -> str :
301309 """Pick a word influenced by difficulty and optional category (longer words for harder)."""
302310 if category and category in CATEGORIES :
303311 pool = CATEGORIES [category ]
312+ elif difficulty == "easy" :
313+ pool = [w for w in WORD_LIST if 4 <= len (w ) <= 6 ]
314+ elif difficulty == "medium" :
315+ pool = [w for w in WORD_LIST if 5 <= len (w ) <= 8 ]
304316 else :
305- if difficulty == "easy" :
306- pool = [w for w in WORD_LIST if 4 <= len (w ) <= 6 ]
307- elif difficulty == "medium" :
308- pool = [w for w in WORD_LIST if 5 <= len (w ) <= 8 ]
309- else :
310- pool = [w for w in WORD_LIST if len (w ) >= 6 ]
317+ pool = [w for w in WORD_LIST if len (w ) >= 6 ]
311318 if not pool :
312319 pool = WORD_LIST
313320 return random .choice (pool )
314321
315322
316- def celebratory_art ():
323+ def celebratory_art () -> None :
317324 art = [
318325 "\n ★ Congratulations! ★\n " ,
319- "\n (\_/)" ,
326+ "\n (\\ _/)" ,
320327 "\n ( •_•)" ,
321328 "\n / >💥 You did it!\n " ,
322329 ]
323330 print (color ("\n " .join (art ), "35" ))
324331
325332
326- def play_game ():
327- print (color ("WELCOME TO WORD SCRAMBLE" , "36" ))
328- print (color ("------------------------" , "36" ))
333+ def play_game () -> None :
334+ print (color ("WELCOME TO WORD SCRAMBLE" , '36' ))
335+ print (color ("------------------------" , '36' ))
329336 # choose difficulty
330337 while True :
331- diff = (
332- input ("Choose difficulty (easy / medium / hard) [medium]: " ).strip ().lower ()
333- or "medium"
334- )
338+ diff = input ("Choose difficulty (easy / medium / hard) [medium]: " ).strip ().lower () or "medium"
335339 if diff in DIFFICULTIES :
336340 break
337341 print ("Invalid choice." )
338342 # choose optional category
339- print ("Available categories:" , ", " .join (CATEGORIES .keys ()))
343+ print ("Available categories:" , ', ' .join (CATEGORIES .keys ()))
340344 cat = input ("Pick category (or press Enter for mixed): " ).strip ().lower () or None
341345 if cat and cat not in CATEGORIES :
342346 print ("Unknown category, using mixed words." )
@@ -364,12 +368,7 @@ def play_game():
364368 # combo bonus for streaks: +5% per consecutive correct (rounded)
365369 combo_bonus = int (points * (0.05 * (streak - 1 ))) if streak > 1 else 0
366370 if combo_bonus :
367- print (
368- color (
369- f"Combo! +{ combo_bonus } bonus points for a streak of { streak } " ,
370- "33" ,
371- )
372- )
371+ print (color (f"Combo! +{ combo_bonus } bonus points for a streak of { streak } " , '33' ))
373372 points += combo_bonus
374373 total_score += points
375374 celebratory_art ()
@@ -383,7 +382,7 @@ def play_game():
383382 save_stats (stats )
384383
385384
386- def main_menu ():
385+ def main_menu () -> None :
387386 while True :
388387 print ("Word Scramble — Main Menu" )
389388 print ("1) Play" )
@@ -398,9 +397,7 @@ def main_menu():
398397 elif choice == "3" :
399398 print ("\n Instructions:" )
400399 print ("- Guess the original word from the scrambled letters." )
401- print (
402- "- Commands during a round: 'hint' (reveals one letter), 'skip', 'quit'."
403- )
400+ print ("- Commands during a round: 'hint' (reveals one letter), 'skip', 'quit'." )
404401 print ("- Faster correct answers score more points." )
405402 print ("- High scores are saved locally in high_scores.json.\n " )
406403 elif choice == "4" :
0 commit comments