Skip to content

Commit d4bc1b0

Browse files
Add Word Scramble interactive CLI game
This commit introduces a new interactive CLI game called Word Scramble. It features difficulty levels, multiple rounds, a hint system, scoring with time bonuses, and persistent high scores.
1 parent e2a78d4 commit d4bc1b0

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# ...existing code...
2+
"""
3+
Word Scramble — interactive CLI game
4+
Features:
5+
- Difficulty levels (easy / medium / hard)
6+
- Multiple rounds per game based on difficulty
7+
- Hint system (reveal a letter)
8+
- Scoring with time bonus
9+
- Persistent top-10 high scores in high_scores.json
10+
- Beginner-friendly, well-commented, no external dependencies
11+
"""
12+
13+
import json
14+
import random
15+
import time
16+
from pathlib import Path
17+
from datetime import datetime
18+
19+
# Files (stored in the same folder)
20+
DATA_DIR = Path(__file__).parent
21+
HIGH_SCORES_FILE = DATA_DIR / "high_scores.json"
22+
STATS_FILE = DATA_DIR / "stats.json"
23+
24+
# Simple embedded word list (can be extended)
25+
WORD_LIST = [
26+
"planet", "python", "scramble", "library", "function", "variable",
27+
"keyboard", "monitor", "developer", "network", "package", "module",
28+
"algorithm", "debug", "database", "container", "virtual", "iterate",
29+
"compile", "syntax", "object", "inheritance", "interface", "performance",
30+
"encryption", "protocol", "bandwidth", "repository", "branch", "commit",
31+
"feature", "engine", "cursor", "iterator", "lambda", "generator",
32+
"concurrency", "thread", "process", "serializer", "message", "payload"
33+
]
34+
35+
# Optional categories to make the game more interesting — each category is a small subset
36+
CATEGORIES = {
37+
"general": WORD_LIST,
38+
"programming": [w for w in WORD_LIST if w in (
39+
"python","function","variable","module","algorithm","debug","compile",
40+
"object","inheritance","interface","repository","commit","lambda","generator",
41+
"concurrency","thread","process"
42+
)],
43+
"networking": [w for w in WORD_LIST if w in (
44+
"network","protocol","bandwidth","packet" )],
45+
}
46+
47+
# New extended categories
48+
ANIMALS = [
49+
"elephant", "tiger", "penguin", "giraffe", "dolphin", "kangaroo", "alligator",
50+
"cheetah", "hedgehog", "raccoon", "squirrel", "porcupine", "butterfly", "octopus",
51+
"hummingbird", "flamingo"
52+
]
53+
54+
MOVIES = [
55+
"inception", "gladiator", "titanic", "avatar", "interstellar", "matrix", "casablanca",
56+
"goodfellas", "amadeus", "psycho", "rocky", "alien", "jaws"
57+
]
58+
59+
FOODS = [
60+
"pizza", "sushi", "taco", "lasagna", "pancake", "risotto", "casserole", "burger",
61+
"cupcake", "avocado", "blueberry", "spaghetti", "chocolate"
62+
]
63+
64+
# merge into categories dict
65+
CATEGORIES["animals"] = ANIMALS
66+
CATEGORIES["movies"] = MOVIES
67+
CATEGORIES["foods"] = FOODS
68+
69+
# Configuration by difficulty
70+
DIFFICULTIES = {
71+
"easy": {"rounds": 8, "removal_hint": 0, "multiplier": 1.0, "max_time": 30},
72+
"medium": {"rounds": 6, "removal_hint": 1, "multiplier": 1.5, "max_time": 25},
73+
"hard": {"rounds": 4, "removal_hint": 2, "multiplier": 2.0, "max_time": 20},
74+
}
75+
76+
77+
def load_high_scores():
78+
"""Load high scores from disk; return list of dicts."""
79+
if HIGH_SCORES_FILE.exists():
80+
try:
81+
with HIGH_SCORES_FILE.open("r", encoding="utf-8") as fh:
82+
return json.load(fh)
83+
except Exception:
84+
return []
85+
return []
86+
87+
88+
def save_high_scores(scores):
89+
"""Save high scores to disk (keep only top 10)."""
90+
scores = sorted(scores, key=lambda s: s["score"], reverse=True)[:10]
91+
with HIGH_SCORES_FILE.open("w", encoding="utf-8") as fh:
92+
json.dump(scores, fh, indent=2)
93+
94+
95+
def load_stats():
96+
"""Load simple player stats (games_played, best_streak)."""
97+
if STATS_FILE.exists():
98+
try:
99+
with STATS_FILE.open("r", encoding="utf-8") as fh:
100+
return json.load(fh)
101+
except Exception:
102+
return {"games_played": 0, "best_streak": 0}
103+
return {"games_played": 0, "best_streak": 0}
104+
105+
106+
def save_stats(stats):
107+
try:
108+
with STATS_FILE.open("w", encoding="utf-8") as fh:
109+
json.dump(stats, fh, indent=2)
110+
except Exception:
111+
pass
112+
113+
114+
def show_high_scores():
115+
scores = load_high_scores()
116+
if not scores:
117+
print("\nNo high scores yet. Be the first!\n")
118+
return
119+
print("\nTop scores:")
120+
for i, s in enumerate(scores, 1):
121+
when = s.get("date", "")
122+
print(f"{i}. {s['name']}{s['score']} pts ({when})")
123+
print()
124+
125+
126+
def scramble_word(word):
127+
"""Return a scrambled version of word that's not identical to original if possible."""
128+
if len(set(word)) == 1:
129+
# all same letter (rare) — return as-is
130+
return word
131+
letters = list(word)
132+
scrambled = word
133+
# try multiple times to avoid returning the same word
134+
for _ in range(10):
135+
random.shuffle(letters)
136+
scrambled = "".join(letters)
137+
if scrambled != word:
138+
return scrambled
139+
return scrambled # fallback
140+
141+
142+
def color(text, code):
143+
"""Lightweight colored output using ANSI codes. Works on modern terminals."""
144+
return f"\x1b[{code}m{text}\x1b[0m"
145+
146+
147+
def compute_score(word, elapsed, difficulty):
148+
"""Compute score for a correct guess."""
149+
base = len(word) * 10
150+
mult = DIFFICULTIES[difficulty]["multiplier"]
151+
max_time = DIFFICULTIES[difficulty]["max_time"]
152+
# time bonus scales with how quickly the player answers (0-20)
153+
time_bonus = max(0, int((max_time - min(elapsed, max_time)) / max_time * 20))
154+
return int(base * mult + time_bonus)
155+
156+
157+
def play_round(word, difficulty):
158+
"""Play one scramble round. Returns points earned (0 if failed/skip)."""
159+
max_time = DIFFICULTIES[difficulty]["max_time"]
160+
scrambled = scramble_word(word)
161+
hint_mask = ["_"] * len(word)
162+
revealed_indices = set()
163+
attempts = 0
164+
165+
print("\nScrambled:", " ".join(scrambled))
166+
print(f"(Type your guess, or 'hint', 'skip', 'quit'. Time suggested: {max_time}s)\n")
167+
168+
start = time.perf_counter()
169+
while True:
170+
attempts += 1
171+
guess = input("Your guess > ").strip().lower()
172+
elapsed = time.perf_counter() - start
173+
174+
if guess == "quit":
175+
return "quit", 0
176+
if guess == "skip":
177+
print(f"Skipped. Answer was: {word}")
178+
return 0
179+
if guess == "hint":
180+
# reveal one unrevealed letter in correct position
181+
unrevealed = [i for i in range(len(word)) if i not in revealed_indices]
182+
if not unrevealed:
183+
print("All letters already revealed.")
184+
else:
185+
i = random.choice(unrevealed)
186+
revealed_indices.add(i)
187+
hint_mask[i] = word[i]
188+
print("Hint:", " ".join(hint_mask))
189+
continue
190+
# check answer
191+
if guess == word:
192+
points = compute_score(word, elapsed, difficulty)
193+
print(color(f"Correct! +{points} pts (time: {int(elapsed)}s, attempts: {attempts})", '32'))
194+
return points
195+
else:
196+
# small helpful feedback
197+
same_positions = sum(1 for a, b in zip(guess, word) if a == b)
198+
print(f"Not quite. {same_positions} letter(s) in the correct position. Try again.")
199+
200+
201+
def pick_word_by_difficulty_and_category(difficulty, category=None):
202+
"""Pick a word influenced by difficulty and optional category (longer words for harder)."""
203+
if category and category in CATEGORIES:
204+
pool = CATEGORIES[category]
205+
else:
206+
if difficulty == "easy":
207+
pool = [w for w in WORD_LIST if 4 <= len(w) <= 6]
208+
elif difficulty == "medium":
209+
pool = [w for w in WORD_LIST if 5 <= len(w) <= 8]
210+
else:
211+
pool = [w for w in WORD_LIST if len(w) >= 6]
212+
if not pool:
213+
pool = WORD_LIST
214+
return random.choice(pool)
215+
216+
217+
def celebratory_art():
218+
art = [
219+
"\n ★ Congratulations! ★\n",
220+
"\n (\_/)",
221+
"\n ( •_•)",
222+
"\n / >💥 You did it!\n"
223+
]
224+
print(color('\n'.join(art), '35'))
225+
226+
227+
def play_game():
228+
print(color("WELCOME TO WORD SCRAMBLE", '36'))
229+
print(color("------------------------", '36'))
230+
# choose difficulty
231+
while True:
232+
diff = input("Choose difficulty (easy / medium / hard) [medium]: ").strip().lower() or "medium"
233+
if diff in DIFFICULTIES:
234+
break
235+
print("Invalid choice.")
236+
# choose optional category
237+
print("Available categories:", ', '.join(CATEGORIES.keys()))
238+
cat = input("Pick category (or press Enter for mixed): ").strip().lower() or None
239+
if cat and cat not in CATEGORIES:
240+
print("Unknown category, using mixed words.")
241+
cat = None
242+
243+
rounds = DIFFICULTIES[diff]["rounds"]
244+
print(f"Starting {rounds} rounds on {diff} difficulty. Good luck!\n")
245+
246+
total_score = 0
247+
streak = 0
248+
best_streak = 0
249+
stats = load_stats()
250+
251+
for r in range(1, rounds + 1):
252+
print(f"=== Round {r}/{rounds} ===")
253+
word = pick_word_by_difficulty_and_category(diff, cat)
254+
result = play_round(word, diff)
255+
if isinstance(result, tuple) and result[0] == "quit":
256+
print("Quitting game early.")
257+
break
258+
points = result
259+
if points > 0:
260+
streak += 1
261+
best_streak = max(best_streak, streak)
262+
# combo bonus for streaks: +5% per consecutive correct (rounded)
263+
combo_bonus = int(points * (0.05 * (streak - 1))) if streak > 1 else 0
264+
if combo_bonus:
265+
print(color(f"Combo! +{combo_bonus} bonus points for a streak of {streak}", '33'))
266+
points += combo_bonus
267+
total_score += points
268+
celebratory_art()
269+
else:
270+
streak = 0
271+
272+
print("\nGame over. Total score:", total_score)
273+
# update stats
274+
stats["games_played"] = stats.get("games_played", 0) + 1
275+
stats["best_streak"] = max(stats.get("best_streak", 0), best_streak)
276+
save_stats(stats)
277+
278+
279+
def main_menu():
280+
while True:
281+
print("Word Scramble — Main Menu")
282+
print("1) Play")
283+
print("2) Show High Scores")
284+
print("3) About / Instructions")
285+
print("4) Quit")
286+
choice = input("Select [1-4] > ").strip()
287+
if choice == "1":
288+
play_game()
289+
elif choice == "2":
290+
show_high_scores()
291+
elif choice == "3":
292+
print("\nInstructions:")
293+
print("- Guess the original word from the scrambled letters.")
294+
print("- Commands during a round: 'hint' (reveals one letter), 'skip', 'quit'.")
295+
print("- Faster correct answers score more points.")
296+
print("- High scores are saved locally in high_scores.json.\n")
297+
elif choice == "4":
298+
print("Goodbye.")
299+
break
300+
else:
301+
print("Invalid option. Try again.")
302+
303+
304+
if __name__ == "__main__":
305+
try:
306+
main_menu()
307+
except KeyboardInterrupt:
308+
print("\nInterrupted. Bye.")

0 commit comments

Comments
 (0)