claude-games 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/command.md +19 -0
- package/games/2048.py +239 -0
- package/games/dino.py +324 -0
- package/games/hangman.py +288 -0
- package/games/hogwarts.py +595 -0
- package/games/invaders.py +380 -0
- package/games/launch.sh +53 -0
- package/games/menu.py +276 -0
- package/games/notify.py +47 -0
- package/games/pacman.py +382 -0
- package/games/patrol.py +348 -0
- package/games/pokemon.py +658 -0
- package/games/snake.py +257 -0
- package/games/tetris.py +347 -0
- package/games/wordle.py +285 -0
- package/install.js +70 -0
- package/package.json +20 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Hogwarts Duel – A Harry Potter interactive spell-casting adventure!"""
|
|
3
|
+
import curses
|
|
4
|
+
import time
|
|
5
|
+
import random
|
|
6
|
+
import textwrap
|
|
7
|
+
|
|
8
|
+
FRAME_TIME = 0.05
|
|
9
|
+
|
|
10
|
+
# ── ASCII Art ──────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
HOGWARTS_ART = [
|
|
13
|
+
" _____",
|
|
14
|
+
" / \\",
|
|
15
|
+
" _____________/ ___ \\____________",
|
|
16
|
+
" | _____ _ | | | | _____ _|",
|
|
17
|
+
" | |_____| |_| | |___| | |_____| |_|",
|
|
18
|
+
" | ___________ ___________ ___|",
|
|
19
|
+
" | | | | | | | | |",
|
|
20
|
+
" | | H | O | | G | W | | |",
|
|
21
|
+
" | |_____|_____| |_____|_____| | |",
|
|
22
|
+
" | _____ ________________________|",
|
|
23
|
+
" | | \\ / \\ / \\ |",
|
|
24
|
+
" | | V \\ / | |",
|
|
25
|
+
" | | HOGWARTS \\ / | |",
|
|
26
|
+
" |__|________________V_________|___|",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
HARRY_ART = [
|
|
30
|
+
" _ ",
|
|
31
|
+
" /O\\ ",
|
|
32
|
+
" |~| ",
|
|
33
|
+
" /|_|\\ ",
|
|
34
|
+
" / |_| \\ ",
|
|
35
|
+
" | | ",
|
|
36
|
+
" _| |_ ",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
VOLDEMORT_ART = [
|
|
40
|
+
" _ ",
|
|
41
|
+
" /X\\ ",
|
|
42
|
+
" |=| ",
|
|
43
|
+
" /|_|\\ ",
|
|
44
|
+
" / |_| \\ ",
|
|
45
|
+
" | | ",
|
|
46
|
+
" _| |_ ",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
SNAPE_ART = [
|
|
50
|
+
" _ ",
|
|
51
|
+
" /S\\ ",
|
|
52
|
+
" |_| ",
|
|
53
|
+
" /| |\\ ",
|
|
54
|
+
" | | ",
|
|
55
|
+
" _| |_ ",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
DEMENTOR_ART = [
|
|
59
|
+
" ~~~~~ ",
|
|
60
|
+
" ( ☠ ☠ ) ",
|
|
61
|
+
" ~~~~~ ",
|
|
62
|
+
" /|||||\\ ",
|
|
63
|
+
" ||||| ",
|
|
64
|
+
" ~|||||~ ",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
PATRONUS_ART = [
|
|
68
|
+
" * * * ",
|
|
69
|
+
" * \\___/ * ",
|
|
70
|
+
"* ( ) *",
|
|
71
|
+
" * /___\\ * ",
|
|
72
|
+
" * * * ",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
# ── Spells ─────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
SPELLS = [
|
|
78
|
+
{
|
|
79
|
+
"name": "Expelliarmus",
|
|
80
|
+
"latin": "expelliarmus",
|
|
81
|
+
"damage": 25,
|
|
82
|
+
"effect": "disarm",
|
|
83
|
+
"desc": "Disarming Charm – knocks the wand from your opponent",
|
|
84
|
+
"color": curses.COLOR_RED,
|
|
85
|
+
"key": "1",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "Stupefy",
|
|
89
|
+
"latin": "stupefy",
|
|
90
|
+
"damage": 35,
|
|
91
|
+
"effect": "stun",
|
|
92
|
+
"desc": "Stunning Spell – temporarily stuns your opponent",
|
|
93
|
+
"color": curses.COLOR_RED,
|
|
94
|
+
"key": "2",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "Protego",
|
|
98
|
+
"latin": "protego",
|
|
99
|
+
"damage": 0,
|
|
100
|
+
"effect": "shield",
|
|
101
|
+
"desc": "Shield Charm – blocks the next incoming spell",
|
|
102
|
+
"color": curses.COLOR_CYAN,
|
|
103
|
+
"key": "3",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "Lumos",
|
|
107
|
+
"latin": "lumos",
|
|
108
|
+
"damage": 10,
|
|
109
|
+
"effect": "reveal",
|
|
110
|
+
"desc": "Illumination Charm – reveals weakness, deals minor damage",
|
|
111
|
+
"color": curses.COLOR_YELLOW,
|
|
112
|
+
"key": "4",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "Expecto Patronum",
|
|
116
|
+
"latin": "expecto patronum",
|
|
117
|
+
"damage": 60,
|
|
118
|
+
"effect": "patronus",
|
|
119
|
+
"desc": "Patronus Charm – devastating against dark creatures",
|
|
120
|
+
"color": curses.COLOR_WHITE,
|
|
121
|
+
"key": "5",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"name": "Accio",
|
|
125
|
+
"latin": "accio",
|
|
126
|
+
"damage": 15,
|
|
127
|
+
"effect": "summon",
|
|
128
|
+
"desc": "Summoning Charm – pulls objects and disorients foes",
|
|
129
|
+
"color": curses.COLOR_GREEN,
|
|
130
|
+
"key": "6",
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
# ── Encounters ─────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
ENCOUNTERS = [
|
|
137
|
+
{
|
|
138
|
+
"name": "Draco Malfoy",
|
|
139
|
+
"art": SNAPE_ART,
|
|
140
|
+
"hp": 80,
|
|
141
|
+
"color": curses.COLOR_WHITE,
|
|
142
|
+
"spells": ["Expelliarmus", "Stupefy"],
|
|
143
|
+
"intro": "Draco Malfoy blocks your path with a sneer. \"Not so brave now, are you?\"",
|
|
144
|
+
"defeat_msg": "Malfoy stumbles back. \"This isn't over, Potter!\"",
|
|
145
|
+
"weakness": None,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "Dementor",
|
|
149
|
+
"art": DEMENTOR_ART,
|
|
150
|
+
"hp": 100,
|
|
151
|
+
"color": curses.COLOR_BLUE,
|
|
152
|
+
"spells": ["Stupefy", "Stupefy"],
|
|
153
|
+
"intro": "A Dementor glides towards you, draining all warmth. Think of your happiest memory…",
|
|
154
|
+
"defeat_msg": "The Dementor dissolves into shadow and retreats!",
|
|
155
|
+
"weakness": "patronus",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "Lord Voldemort",
|
|
159
|
+
"art": VOLDEMORT_ART,
|
|
160
|
+
"hp": 150,
|
|
161
|
+
"color": curses.COLOR_RED,
|
|
162
|
+
"spells": ["Stupefy", "Expelliarmus", "Stupefy"],
|
|
163
|
+
"intro": "He-Who-Must-Not-Be-Named materialises before you. \"Harry Potter… the Boy Who Lived.\"",
|
|
164
|
+
"defeat_msg": "Voldemort howls as his power shatters. The Wizarding World is saved!",
|
|
165
|
+
"weakness": None,
|
|
166
|
+
},
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
# ── Helpers ────────────────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
def box(stdscr, y, x, h, w, title="", color=None):
|
|
172
|
+
attr = color or curses.color_pair(1)
|
|
173
|
+
H, W = stdscr.getmaxyx()
|
|
174
|
+
try:
|
|
175
|
+
stdscr.addstr(y, x, "╔" + "═" * (w - 2) + "╗", attr)
|
|
176
|
+
stdscr.addstr(y + h - 1, x, "╚" + "═" * (w - 2) + "╝", attr)
|
|
177
|
+
for i in range(1, h - 1):
|
|
178
|
+
stdscr.addstr(y + i, x, "║", attr)
|
|
179
|
+
stdscr.addstr(y + i, x + w - 1, "║", attr)
|
|
180
|
+
if title:
|
|
181
|
+
tx = x + max(1, (w - len(title) - 2) // 2)
|
|
182
|
+
stdscr.addstr(y, tx, f" {title} ", attr | curses.A_BOLD)
|
|
183
|
+
except curses.error:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def hp_bar(stdscr, y, x, label, current, maximum, color, width=20):
|
|
188
|
+
filled = max(0, int(width * current / maximum))
|
|
189
|
+
bar = "█" * filled + "░" * (width - filled)
|
|
190
|
+
pct = f"{current}/{maximum}"
|
|
191
|
+
try:
|
|
192
|
+
stdscr.addstr(y, x, f"{label:<12}", curses.color_pair(1))
|
|
193
|
+
stdscr.addstr(y, x + 12, "[", curses.color_pair(1))
|
|
194
|
+
stdscr.addstr(y, x + 13, bar, curses.color_pair(color) | curses.A_BOLD)
|
|
195
|
+
stdscr.addstr(y, x + 13 + width, f"] {pct}", curses.color_pair(1))
|
|
196
|
+
except curses.error:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def typewriter(stdscr, y, x, text, color, delay=0.018, width=60):
|
|
201
|
+
"""Print text character by character for dramatic effect."""
|
|
202
|
+
H, W = stdscr.getmaxyx()
|
|
203
|
+
lines = textwrap.wrap(text, width)
|
|
204
|
+
for li, line in enumerate(lines):
|
|
205
|
+
for ci, ch in enumerate(line):
|
|
206
|
+
try:
|
|
207
|
+
stdscr.addstr(y + li, x + ci, ch, color)
|
|
208
|
+
except curses.error:
|
|
209
|
+
pass
|
|
210
|
+
stdscr.refresh()
|
|
211
|
+
time.sleep(delay)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def show_art(stdscr, art, cy, cx, color):
|
|
215
|
+
for i, line in enumerate(art):
|
|
216
|
+
try:
|
|
217
|
+
stdscr.addstr(cy + i, cx, line, color)
|
|
218
|
+
except curses.error:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def wait_key(stdscr, prompt="Press any key to continue…"):
|
|
223
|
+
H, W = stdscr.getmaxyx()
|
|
224
|
+
try:
|
|
225
|
+
stdscr.addstr(H - 2, max(0, (W - len(prompt)) // 2), prompt,
|
|
226
|
+
curses.color_pair(3) | curses.A_DIM)
|
|
227
|
+
except curses.error:
|
|
228
|
+
pass
|
|
229
|
+
stdscr.refresh()
|
|
230
|
+
stdscr.nodelay(False)
|
|
231
|
+
stdscr.getch()
|
|
232
|
+
stdscr.nodelay(True)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ── Scenes ─────────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
def scene_title(stdscr):
|
|
238
|
+
H, W = stdscr.getmaxyx()
|
|
239
|
+
stdscr.erase()
|
|
240
|
+
|
|
241
|
+
title_lines = [
|
|
242
|
+
" ╦ ╦╔═╗╔═╗╦ ╦╦ ╦╔═╗╦═╗╔╦╗╔═╗",
|
|
243
|
+
" ╠═╣║ ║║ ╦║║║╠═╣╠═╣╠╦╝ ║ ╚═╗",
|
|
244
|
+
" ╩ ╩╚═╝╚═╝╚╩╝╩ ╩╩ ╩╩╚═ ╩ ╚═╝",
|
|
245
|
+
" ✦ D U E L S ✦ ",
|
|
246
|
+
]
|
|
247
|
+
sy = max(2, (H - len(HOGWARTS_ART) - len(title_lines) - 4) // 2)
|
|
248
|
+
for i, line in enumerate(title_lines):
|
|
249
|
+
try:
|
|
250
|
+
stdscr.addstr(sy + i, max(0, (W - len(line)) // 2),
|
|
251
|
+
line, curses.color_pair(4) | curses.A_BOLD)
|
|
252
|
+
except curses.error:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
art_y = sy + len(title_lines) + 1
|
|
256
|
+
art_x = max(0, (W - len(HOGWARTS_ART[0])) // 2)
|
|
257
|
+
for i, line in enumerate(HOGWARTS_ART):
|
|
258
|
+
try:
|
|
259
|
+
stdscr.addstr(art_y + i, art_x, line, curses.color_pair(3) | curses.A_DIM)
|
|
260
|
+
except curses.error:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
sub = "An Interactive Hogwarts Spell-Casting Adventure"
|
|
264
|
+
try:
|
|
265
|
+
stdscr.addstr(art_y + len(HOGWARTS_ART) + 1,
|
|
266
|
+
max(0, (W - len(sub)) // 2), sub, curses.color_pair(2))
|
|
267
|
+
except curses.error:
|
|
268
|
+
pass
|
|
269
|
+
start = "Press SPACE to begin your journey · Q to quit"
|
|
270
|
+
try:
|
|
271
|
+
stdscr.addstr(art_y + len(HOGWARTS_ART) + 3,
|
|
272
|
+
max(0, (W - len(start)) // 2), start, curses.color_pair(1) | curses.A_DIM)
|
|
273
|
+
except curses.error:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
stdscr.refresh()
|
|
277
|
+
stdscr.nodelay(False)
|
|
278
|
+
while True:
|
|
279
|
+
k = stdscr.getch()
|
|
280
|
+
if k in (ord(' '), ord('\n')): return True
|
|
281
|
+
if k in (ord('q'), ord('Q')): return False
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def scene_story(stdscr, lines, title=""):
|
|
285
|
+
H, W = stdscr.getmaxyx()
|
|
286
|
+
stdscr.erase()
|
|
287
|
+
if title:
|
|
288
|
+
try:
|
|
289
|
+
stdscr.addstr(1, max(0, (W - len(title)) // 2),
|
|
290
|
+
f"── {title} ──", curses.color_pair(4) | curses.A_BOLD)
|
|
291
|
+
except curses.error:
|
|
292
|
+
pass
|
|
293
|
+
for i, line in enumerate(lines):
|
|
294
|
+
try:
|
|
295
|
+
stdscr.addstr(3 + i, max(4, (W - 70) // 2), line,
|
|
296
|
+
curses.color_pair(2))
|
|
297
|
+
except curses.error:
|
|
298
|
+
pass
|
|
299
|
+
wait_key(stdscr)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def duel(stdscr, encounter, player_hp, player_max):
|
|
303
|
+
"""Run a duel. Returns remaining player_hp (0 if defeated)."""
|
|
304
|
+
H, W = stdscr.getmaxyx()
|
|
305
|
+
stdscr.nodelay(True)
|
|
306
|
+
|
|
307
|
+
enemy = dict(encounter)
|
|
308
|
+
enemy['hp'] = encounter['hp']
|
|
309
|
+
enemy['max'] = encounter['hp']
|
|
310
|
+
enemy['shield'] = False
|
|
311
|
+
enemy['spell_i'] = 0
|
|
312
|
+
|
|
313
|
+
p_shield = False
|
|
314
|
+
message = encounter['intro']
|
|
315
|
+
msg_color = curses.color_pair(2)
|
|
316
|
+
spell_result = ""
|
|
317
|
+
frame = 0
|
|
318
|
+
|
|
319
|
+
while True:
|
|
320
|
+
H, W = stdscr.getmaxyx()
|
|
321
|
+
stdscr.erase()
|
|
322
|
+
|
|
323
|
+
# ── Layout ────────────────────────────────────────────
|
|
324
|
+
cx = max(2, (W - 80) // 2)
|
|
325
|
+
|
|
326
|
+
# Title bar
|
|
327
|
+
title = f" ✦ DUEL: Harry Potter vs {enemy['name']} ✦ "
|
|
328
|
+
try:
|
|
329
|
+
stdscr.addstr(0, max(0, (W - len(title)) // 2),
|
|
330
|
+
title, curses.color_pair(4) | curses.A_BOLD)
|
|
331
|
+
except curses.error:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
# HP bars
|
|
335
|
+
hp_bar(stdscr, 2, cx, "Harry Potter",
|
|
336
|
+
max(0, player_hp), player_max, curses.COLOR_GREEN)
|
|
337
|
+
hp_bar(stdscr, 3, cx, enemy['name'],
|
|
338
|
+
max(0, enemy['hp']), enemy['max'], enemy['color'])
|
|
339
|
+
|
|
340
|
+
# Combatant art
|
|
341
|
+
art_y = 5
|
|
342
|
+
show_art(stdscr, HARRY_ART, art_y, cx,
|
|
343
|
+
curses.color_pair(1) | curses.A_BOLD)
|
|
344
|
+
enemy_art_x = min(W - 15, cx + 40)
|
|
345
|
+
show_art(stdscr, enemy['art'], art_y, enemy_art_x,
|
|
346
|
+
curses.color_pair(enemy['color']) | curses.A_BOLD)
|
|
347
|
+
|
|
348
|
+
if p_shield:
|
|
349
|
+
try:
|
|
350
|
+
stdscr.addstr(art_y + 3, cx + 1, "[ SHIELD ]",
|
|
351
|
+
curses.color_pair(6) | curses.A_BOLD)
|
|
352
|
+
except curses.error:
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
# Message area
|
|
356
|
+
msg_y = art_y + max(len(HARRY_ART), len(enemy['art'])) + 1
|
|
357
|
+
box(stdscr, msg_y, cx, 5, min(76, W - 4),
|
|
358
|
+
title="", color=curses.color_pair(3) | curses.A_DIM)
|
|
359
|
+
for li, mline in enumerate(textwrap.wrap(message, min(70, W - 8))[:3]):
|
|
360
|
+
try:
|
|
361
|
+
stdscr.addstr(msg_y + 1 + li, cx + 2, mline, msg_color)
|
|
362
|
+
except curses.error:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
# Spell menu
|
|
366
|
+
spell_y = msg_y + 6
|
|
367
|
+
try:
|
|
368
|
+
stdscr.addstr(spell_y, cx, "Choose your spell:", curses.color_pair(1) | curses.A_BOLD)
|
|
369
|
+
except curses.error:
|
|
370
|
+
pass
|
|
371
|
+
for si, sp in enumerate(SPELLS):
|
|
372
|
+
col = curses.color_pair(sp['color'] if sp['color'] <= 7 else 1)
|
|
373
|
+
label = f" [{sp['key']}] {sp['name']:<18} {sp['desc'][:40]}"
|
|
374
|
+
try:
|
|
375
|
+
stdscr.addstr(spell_y + 1 + si, cx, label, col)
|
|
376
|
+
except curses.error:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
ctrl = "Q = quit duel"
|
|
380
|
+
try:
|
|
381
|
+
stdscr.addstr(H - 2, max(0, (W - len(ctrl)) // 2), ctrl,
|
|
382
|
+
curses.color_pair(1) | curses.A_DIM)
|
|
383
|
+
except curses.error:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
stdscr.refresh()
|
|
387
|
+
|
|
388
|
+
# Wait for key
|
|
389
|
+
stdscr.nodelay(False)
|
|
390
|
+
key = stdscr.getch()
|
|
391
|
+
stdscr.nodelay(True)
|
|
392
|
+
|
|
393
|
+
if key in (ord('q'), ord('Q')):
|
|
394
|
+
return player_hp
|
|
395
|
+
|
|
396
|
+
chosen = None
|
|
397
|
+
for sp in SPELLS:
|
|
398
|
+
if key == ord(sp['key']):
|
|
399
|
+
chosen = sp
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
if chosen is None:
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
# ── Player attacks ────────────────────────────────────
|
|
406
|
+
dmg = chosen['damage']
|
|
407
|
+
effect = chosen['effect']
|
|
408
|
+
|
|
409
|
+
if effect == 'shield':
|
|
410
|
+
p_shield = True
|
|
411
|
+
message = "✨ Protego! A shimmering shield surrounds you."
|
|
412
|
+
msg_color = curses.color_pair(6)
|
|
413
|
+
else:
|
|
414
|
+
if effect == 'patronus' and enemy.get('weakness') == 'patronus':
|
|
415
|
+
dmg = int(dmg * 2.5)
|
|
416
|
+
spell_result = " ★ SUPER EFFECTIVE ★"
|
|
417
|
+
else:
|
|
418
|
+
spell_result = ""
|
|
419
|
+
|
|
420
|
+
if enemy['shield']:
|
|
421
|
+
message = f"💥 {chosen['name']}! — But {enemy['name']}'s shield absorbs it!{spell_result}"
|
|
422
|
+
enemy['shield'] = False
|
|
423
|
+
msg_color = curses.color_pair(3)
|
|
424
|
+
else:
|
|
425
|
+
enemy['hp'] -= dmg
|
|
426
|
+
message = (f"✨ {chosen['name'].upper()}! {enemy['name']} takes {dmg} damage!{spell_result}")
|
|
427
|
+
msg_color = curses.color_pair(4) | curses.A_BOLD
|
|
428
|
+
|
|
429
|
+
if enemy['hp'] <= 0:
|
|
430
|
+
# Victory
|
|
431
|
+
stdscr.erase()
|
|
432
|
+
show_art(stdscr, HARRY_ART, 4, cx, curses.color_pair(1) | curses.A_BOLD)
|
|
433
|
+
try:
|
|
434
|
+
stdscr.addstr(2, max(0, (W - 20) // 2), " ✦ VICTORY! ✦ ",
|
|
435
|
+
curses.color_pair(9) | curses.A_BOLD)
|
|
436
|
+
stdscr.addstr(12, cx, encounter['defeat_msg'],
|
|
437
|
+
curses.color_pair(2))
|
|
438
|
+
except curses.error:
|
|
439
|
+
pass
|
|
440
|
+
wait_key(stdscr)
|
|
441
|
+
return player_hp
|
|
442
|
+
|
|
443
|
+
# ── Enemy attacks ─────────────────────────────────────
|
|
444
|
+
e_spell_name = enemy['spells'][enemy['spell_i'] % len(enemy['spells'])]
|
|
445
|
+
enemy['spell_i'] += 1
|
|
446
|
+
e_dmg = random.randint(15, 30)
|
|
447
|
+
|
|
448
|
+
# 30% chance enemy shields
|
|
449
|
+
if random.random() < 0.3:
|
|
450
|
+
enemy['shield'] = True
|
|
451
|
+
message += f"\n{enemy['name']} raises a shield!"
|
|
452
|
+
else:
|
|
453
|
+
if p_shield:
|
|
454
|
+
message += f"\n🛡 {e_spell_name}! — Your shield blocks it!"
|
|
455
|
+
p_shield = False
|
|
456
|
+
else:
|
|
457
|
+
player_hp -= e_dmg
|
|
458
|
+
message += f"\n💥 {enemy['name']} casts {e_spell_name}! You take {e_dmg} damage!"
|
|
459
|
+
msg_color = curses.color_pair(7) | curses.A_BOLD
|
|
460
|
+
|
|
461
|
+
if player_hp <= 0:
|
|
462
|
+
stdscr.erase()
|
|
463
|
+
try:
|
|
464
|
+
stdscr.addstr(H // 2, max(0, (W - 30) // 2),
|
|
465
|
+
" You have been defeated… ",
|
|
466
|
+
curses.color_pair(7) | curses.A_BOLD)
|
|
467
|
+
stdscr.addstr(H // 2 + 2, max(0, (W - 36) // 2),
|
|
468
|
+
"\"There are things much worse than death…\"",
|
|
469
|
+
curses.color_pair(3))
|
|
470
|
+
except curses.error:
|
|
471
|
+
pass
|
|
472
|
+
wait_key(stdscr)
|
|
473
|
+
return 0
|
|
474
|
+
|
|
475
|
+
frame += 1
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def main(stdscr):
|
|
479
|
+
curses.curs_set(0)
|
|
480
|
+
curses.start_color()
|
|
481
|
+
curses.use_default_colors()
|
|
482
|
+
curses.init_pair(1, curses.COLOR_WHITE, -1)
|
|
483
|
+
curses.init_pair(2, curses.COLOR_GREEN, -1)
|
|
484
|
+
curses.init_pair(3, curses.COLOR_CYAN, -1)
|
|
485
|
+
curses.init_pair(4, curses.COLOR_YELLOW, -1)
|
|
486
|
+
curses.init_pair(5, curses.COLOR_BLUE, -1)
|
|
487
|
+
curses.init_pair(6, curses.COLOR_CYAN, -1)
|
|
488
|
+
curses.init_pair(7, curses.COLOR_RED, -1)
|
|
489
|
+
curses.init_pair(8, curses.COLOR_MAGENTA, -1)
|
|
490
|
+
curses.init_pair(9, curses.COLOR_GREEN, -1)
|
|
491
|
+
|
|
492
|
+
while True:
|
|
493
|
+
if not scene_title(stdscr):
|
|
494
|
+
break
|
|
495
|
+
|
|
496
|
+
player_hp = 150
|
|
497
|
+
player_max = 150
|
|
498
|
+
|
|
499
|
+
# ── Chapter 1 ─────────────────────────────────────────
|
|
500
|
+
scene_story(stdscr, [
|
|
501
|
+
"Year 5 at Hogwarts. Dolores Umbridge has banned defensive magic.",
|
|
502
|
+
"You and your friends meet secretly in the Room of Requirement",
|
|
503
|
+
"to form Dumbledore's Army.",
|
|
504
|
+
"",
|
|
505
|
+
"But not everyone is pleased. Word has reached the wrong ears…",
|
|
506
|
+
], title="Chapter I: Dumbledore's Army")
|
|
507
|
+
|
|
508
|
+
player_hp = duel(stdscr, ENCOUNTERS[0], player_hp, player_max)
|
|
509
|
+
if player_hp <= 0:
|
|
510
|
+
scene_story(stdscr, [
|
|
511
|
+
"Defeated in the corridor, you wake in the hospital wing.",
|
|
512
|
+
"Madam Pomfrey tends your wounds. You must train harder.",
|
|
513
|
+
"",
|
|
514
|
+
"\"It is our choices that show what we truly are,\"",
|
|
515
|
+
"Dumbledore reminds you from his portrait.",
|
|
516
|
+
], title="Defeated")
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
player_hp = min(player_max, player_hp + 30)
|
|
520
|
+
|
|
521
|
+
# ── Chapter 2 ─────────────────────────────────────────
|
|
522
|
+
scene_story(stdscr, [
|
|
523
|
+
"The Ministry has fallen. Dementors now roam freely,",
|
|
524
|
+
"sent by Voldemort to spread terror across Britain.",
|
|
525
|
+
"",
|
|
526
|
+
"While crossing the bridge to Hogsmeade, a pair of Dementors",
|
|
527
|
+
"descend from the mist, their rattling breath freezing the air.",
|
|
528
|
+
"",
|
|
529
|
+
"Remember: the Patronus Charm is the only true defence.",
|
|
530
|
+
"Think of your happiest memory…",
|
|
531
|
+
], title="Chapter II: The Dementor's Kiss")
|
|
532
|
+
|
|
533
|
+
player_hp = duel(stdscr, ENCOUNTERS[1], player_hp, player_max)
|
|
534
|
+
if player_hp <= 0:
|
|
535
|
+
scene_story(stdscr, [
|
|
536
|
+
"The Dementor's cold overwhelms you.",
|
|
537
|
+
"You collapse on the bridge, memories fading…",
|
|
538
|
+
"Hermione's Patronus drives it away in time.",
|
|
539
|
+
"",
|
|
540
|
+
"You live to fight another day.",
|
|
541
|
+
], title="Defeated")
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
player_hp = min(player_max, player_hp + 40)
|
|
545
|
+
|
|
546
|
+
# ── Chapter 3 ─────────────────────────────────────────
|
|
547
|
+
scene_story(stdscr, [
|
|
548
|
+
"The Battle of Hogwarts. The Great Hall burns.",
|
|
549
|
+
"Friends have fallen. The Forbidden Forest grows silent.",
|
|
550
|
+
"",
|
|
551
|
+
"Voldemort himself stands before you in the ruined courtyard.",
|
|
552
|
+
"This is the moment everything has led to.",
|
|
553
|
+
"",
|
|
554
|
+
"\"Neither can live while the other survives.\"",
|
|
555
|
+
"",
|
|
556
|
+
"This is it, Harry. Make every spell count.",
|
|
557
|
+
], title="Chapter III: The Final Duel")
|
|
558
|
+
|
|
559
|
+
player_hp = duel(stdscr, ENCOUNTERS[2], player_hp, player_max)
|
|
560
|
+
if player_hp <= 0:
|
|
561
|
+
scene_story(stdscr, [
|
|
562
|
+
"Voldemort stands triumphant.",
|
|
563
|
+
"But this is not the end of the story…",
|
|
564
|
+
"",
|
|
565
|
+
"Press SPACE to try again from the beginning.",
|
|
566
|
+
], title="Defeated")
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
# ── Victory ───────────────────────────────────────────
|
|
570
|
+
H, W = stdscr.getmaxyx()
|
|
571
|
+
stdscr.erase()
|
|
572
|
+
art_x = max(0, (W - len(PATRONUS_ART[0])) // 2)
|
|
573
|
+
show_art(stdscr, PATRONUS_ART, H // 2 - 8, art_x,
|
|
574
|
+
curses.color_pair(4) | curses.A_BOLD)
|
|
575
|
+
|
|
576
|
+
ending = [
|
|
577
|
+
" Voldemort is vanquished. The Wizarding World is free. ",
|
|
578
|
+
"",
|
|
579
|
+
" The elder wand snaps. The Deathly Hallows are at rest. ",
|
|
580
|
+
"",
|
|
581
|
+
" Mischief managed. ✦",
|
|
582
|
+
]
|
|
583
|
+
for i, line in enumerate(ending):
|
|
584
|
+
try:
|
|
585
|
+
stdscr.addstr(H // 2 - 2 + i, max(0, (W - len(line)) // 2),
|
|
586
|
+
line, curses.color_pair(4) | curses.A_BOLD)
|
|
587
|
+
except curses.error:
|
|
588
|
+
pass
|
|
589
|
+
|
|
590
|
+
wait_key(stdscr, "Press any key to return to the menu…")
|
|
591
|
+
break
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
if __name__ == "__main__":
|
|
595
|
+
curses.wrapper(main)
|