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.
@@ -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)