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,380 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Claude Invaders - Play while Claude Code runs!"""
|
|
3
|
+
import curses
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
6
|
+
from notify import init_notify_colors, check_task_done, draw_notification
|
|
7
|
+
|
|
8
|
+
ALIEN_FRAMES = [
|
|
9
|
+
["/o\\", "\\o/"], # Type 0 (top rows) - red
|
|
10
|
+
["<o>", ">o<"], # Type 1 (mid rows) - yellow
|
|
11
|
+
["{o}", "}o{"], # Type 2 (bottom rows) - green
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
PLAYER_SHIP = "▄█▄"
|
|
15
|
+
PLAYER_W = 3
|
|
16
|
+
|
|
17
|
+
COLS = 9
|
|
18
|
+
ROWS = 4
|
|
19
|
+
ALIEN_W = 4 # width per alien slot
|
|
20
|
+
ALIEN_SPACING_Y = 2
|
|
21
|
+
|
|
22
|
+
FRAME_TIME = 0.05
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(stdscr):
|
|
26
|
+
curses.curs_set(0)
|
|
27
|
+
stdscr.nodelay(True)
|
|
28
|
+
curses.start_color()
|
|
29
|
+
curses.use_default_colors()
|
|
30
|
+
curses.init_pair(1, curses.COLOR_CYAN, -1) # player
|
|
31
|
+
curses.init_pair(2, curses.COLOR_RED, -1) # alien type 0
|
|
32
|
+
curses.init_pair(3, curses.COLOR_YELLOW, -1) # alien type 1
|
|
33
|
+
curses.init_pair(4, curses.COLOR_GREEN, -1) # alien type 2
|
|
34
|
+
curses.init_pair(5, curses.COLOR_WHITE, -1) # bullets/text
|
|
35
|
+
curses.init_pair(6, curses.COLOR_MAGENTA, -1) # shields
|
|
36
|
+
curses.init_pair(7, curses.COLOR_RED, -1) # enemy bullets
|
|
37
|
+
init_notify_colors()
|
|
38
|
+
|
|
39
|
+
def play_game():
|
|
40
|
+
height, width = stdscr.getmaxyx()
|
|
41
|
+
|
|
42
|
+
player_x = width // 2
|
|
43
|
+
player_y = height - 3
|
|
44
|
+
score = 0
|
|
45
|
+
lives = 3
|
|
46
|
+
wave = 1
|
|
47
|
+
game_over = False
|
|
48
|
+
paused = False
|
|
49
|
+
notify_timer = 0
|
|
50
|
+
invincible = 0 # frames of invincibility after hit
|
|
51
|
+
|
|
52
|
+
bullets = [] # [y, x, dy] player bullets go up
|
|
53
|
+
enemy_bullets = [] # [y, x]
|
|
54
|
+
|
|
55
|
+
# Aliens grid
|
|
56
|
+
aliens = [] # list of {r, c, alive, type}
|
|
57
|
+
alien_base_x = 0
|
|
58
|
+
alien_base_y = 0
|
|
59
|
+
alien_dir = 1 # 1=right, -1=left
|
|
60
|
+
alien_move_timer = 0
|
|
61
|
+
alien_move_interval = 12 # frames between moves
|
|
62
|
+
alien_frame = 0
|
|
63
|
+
alien_shoot_timer = 0
|
|
64
|
+
|
|
65
|
+
# Shields
|
|
66
|
+
shields = [] # list of (y, x) positions
|
|
67
|
+
|
|
68
|
+
def init_wave():
|
|
69
|
+
nonlocal aliens, alien_base_x, alien_base_y, alien_dir
|
|
70
|
+
nonlocal alien_move_interval, alien_move_timer, shields
|
|
71
|
+
aliens = []
|
|
72
|
+
for r in range(ROWS):
|
|
73
|
+
for c in range(COLS):
|
|
74
|
+
atype = 0 if r == 0 else (1 if r <= 2 else 2)
|
|
75
|
+
aliens.append({"r": r, "c": c, "alive": True, "type": atype})
|
|
76
|
+
|
|
77
|
+
grid_w = COLS * ALIEN_W
|
|
78
|
+
alien_base_x = (width - grid_w) // 2
|
|
79
|
+
alien_base_y = 4
|
|
80
|
+
alien_dir = 1
|
|
81
|
+
alien_move_interval = max(4, 14 - wave * 2)
|
|
82
|
+
alien_move_timer = 0
|
|
83
|
+
|
|
84
|
+
# Shields
|
|
85
|
+
shields.clear()
|
|
86
|
+
shield_y = player_y - 5
|
|
87
|
+
num_shields = min(4, (width - 10) // 15)
|
|
88
|
+
spacing = width // (num_shields + 1)
|
|
89
|
+
for s in range(num_shields):
|
|
90
|
+
sx = spacing * (s + 1) - 3
|
|
91
|
+
for dy in range(2):
|
|
92
|
+
for dx in range(5):
|
|
93
|
+
shields.append((shield_y + dy, sx + dx))
|
|
94
|
+
|
|
95
|
+
init_wave()
|
|
96
|
+
|
|
97
|
+
frame = 0
|
|
98
|
+
|
|
99
|
+
while True:
|
|
100
|
+
frame_start = time.time()
|
|
101
|
+
|
|
102
|
+
# Input
|
|
103
|
+
key = stdscr.getch()
|
|
104
|
+
while True:
|
|
105
|
+
k2 = stdscr.getch()
|
|
106
|
+
if k2 == -1:
|
|
107
|
+
break
|
|
108
|
+
key = k2
|
|
109
|
+
|
|
110
|
+
if key in (ord('q'), ord('Q')):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
if key in (ord('p'), ord('P')) and not game_over:
|
|
114
|
+
paused = not paused
|
|
115
|
+
|
|
116
|
+
if paused:
|
|
117
|
+
pause_msg = "PAUSED - P to resume"
|
|
118
|
+
try:
|
|
119
|
+
stdscr.addstr(height // 2, max(0, (width - len(pause_msg)) // 2),
|
|
120
|
+
pause_msg, curses.color_pair(5) | curses.A_BOLD)
|
|
121
|
+
except curses.error:
|
|
122
|
+
pass
|
|
123
|
+
stdscr.refresh()
|
|
124
|
+
time.sleep(0.05)
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
if game_over:
|
|
128
|
+
if key in (ord(' '), curses.KEY_ENTER, 10, 13):
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
stdscr.erase()
|
|
132
|
+
msgs = ["GAME OVER!", f"Score: {score} Wave: {wave}",
|
|
133
|
+
"SPACE to restart Q to quit"]
|
|
134
|
+
cy = height // 2 - 1
|
|
135
|
+
for i, msg in enumerate(msgs):
|
|
136
|
+
attr = curses.color_pair(2) | curses.A_BOLD if i == 0 else curses.color_pair(5)
|
|
137
|
+
try:
|
|
138
|
+
stdscr.addstr(cy + i, max(0, (width - len(msg)) // 2), msg, attr)
|
|
139
|
+
except curses.error:
|
|
140
|
+
pass
|
|
141
|
+
stdscr.refresh()
|
|
142
|
+
time.sleep(0.05)
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Player movement
|
|
146
|
+
if key == curses.KEY_LEFT:
|
|
147
|
+
player_x = max(2, player_x - 2)
|
|
148
|
+
elif key == curses.KEY_RIGHT:
|
|
149
|
+
player_x = min(width - 3, player_x + 2)
|
|
150
|
+
elif key == ord(' '):
|
|
151
|
+
if len(bullets) < 3:
|
|
152
|
+
bullets.append([player_y - 1, player_x, -1])
|
|
153
|
+
|
|
154
|
+
if check_task_done():
|
|
155
|
+
notify_timer = 80
|
|
156
|
+
|
|
157
|
+
# Update bullets
|
|
158
|
+
new_bullets = []
|
|
159
|
+
for b in bullets:
|
|
160
|
+
b[0] += b[2]
|
|
161
|
+
if 0 <= b[0] < height:
|
|
162
|
+
new_bullets.append(b)
|
|
163
|
+
bullets = new_bullets
|
|
164
|
+
|
|
165
|
+
new_eb = []
|
|
166
|
+
for b in enemy_bullets:
|
|
167
|
+
b[0] += 1
|
|
168
|
+
if b[0] < height:
|
|
169
|
+
new_eb.append(b)
|
|
170
|
+
enemy_bullets = new_eb
|
|
171
|
+
|
|
172
|
+
# Alien movement
|
|
173
|
+
alien_move_timer += 1
|
|
174
|
+
alive_count = sum(1 for a in aliens if a["alive"])
|
|
175
|
+
|
|
176
|
+
# Dynamic speed
|
|
177
|
+
if alive_count > 0:
|
|
178
|
+
speed_factor = max(1, alive_count)
|
|
179
|
+
current_interval = max(2, int(alien_move_interval * speed_factor / (ROWS * COLS)))
|
|
180
|
+
else:
|
|
181
|
+
current_interval = alien_move_interval
|
|
182
|
+
|
|
183
|
+
if alien_move_timer >= current_interval:
|
|
184
|
+
alien_move_timer = 0
|
|
185
|
+
alien_frame = 1 - alien_frame
|
|
186
|
+
|
|
187
|
+
# Check if we need to descend
|
|
188
|
+
need_descend = False
|
|
189
|
+
for a in aliens:
|
|
190
|
+
if not a["alive"]:
|
|
191
|
+
continue
|
|
192
|
+
ax = alien_base_x + a["c"] * ALIEN_W
|
|
193
|
+
if alien_dir > 0 and ax + ALIEN_W >= width - 2:
|
|
194
|
+
need_descend = True
|
|
195
|
+
break
|
|
196
|
+
if alien_dir < 0 and ax <= 2:
|
|
197
|
+
need_descend = True
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
if need_descend:
|
|
201
|
+
alien_dir = -alien_dir
|
|
202
|
+
alien_base_y += 1
|
|
203
|
+
else:
|
|
204
|
+
alien_base_x += alien_dir * 2
|
|
205
|
+
|
|
206
|
+
# Alien shooting
|
|
207
|
+
alien_shoot_timer += 1
|
|
208
|
+
shoot_interval = max(8, 25 - wave * 3)
|
|
209
|
+
if alien_shoot_timer >= shoot_interval and alive_count > 0:
|
|
210
|
+
alien_shoot_timer = 0
|
|
211
|
+
# Pick a random alive alien from the bottom of each column
|
|
212
|
+
bottom_aliens = {}
|
|
213
|
+
for a in aliens:
|
|
214
|
+
if a["alive"]:
|
|
215
|
+
c = a["c"]
|
|
216
|
+
if c not in bottom_aliens or a["r"] > bottom_aliens[c]["r"]:
|
|
217
|
+
bottom_aliens[c] = a
|
|
218
|
+
if bottom_aliens:
|
|
219
|
+
shooter = random.choice(list(bottom_aliens.values()))
|
|
220
|
+
ay = alien_base_y + shooter["r"] * ALIEN_SPACING_Y + 1
|
|
221
|
+
ax = alien_base_x + shooter["c"] * ALIEN_W + 1
|
|
222
|
+
enemy_bullets.append([ay, ax])
|
|
223
|
+
|
|
224
|
+
# Collision: player bullets vs aliens
|
|
225
|
+
for b in bullets[:]:
|
|
226
|
+
hit = False
|
|
227
|
+
for a in aliens:
|
|
228
|
+
if not a["alive"]:
|
|
229
|
+
continue
|
|
230
|
+
ay = alien_base_y + a["r"] * ALIEN_SPACING_Y
|
|
231
|
+
ax = alien_base_x + a["c"] * ALIEN_W
|
|
232
|
+
if ay <= b[0] <= ay + 1 and ax <= b[1] <= ax + 2:
|
|
233
|
+
a["alive"] = False
|
|
234
|
+
hit = True
|
|
235
|
+
points = [30, 20, 10][a["type"]]
|
|
236
|
+
score += points * wave
|
|
237
|
+
break
|
|
238
|
+
if hit and b in bullets:
|
|
239
|
+
bullets.remove(b)
|
|
240
|
+
|
|
241
|
+
# Collision: player bullets vs shields
|
|
242
|
+
for b in bullets[:]:
|
|
243
|
+
pos = (b[0], b[1])
|
|
244
|
+
if pos in shields:
|
|
245
|
+
shields.remove(pos)
|
|
246
|
+
if b in bullets:
|
|
247
|
+
bullets.remove(b)
|
|
248
|
+
|
|
249
|
+
# Collision: enemy bullets vs shields
|
|
250
|
+
for b in enemy_bullets[:]:
|
|
251
|
+
pos = (b[0], b[1])
|
|
252
|
+
if pos in shields:
|
|
253
|
+
shields.remove(pos)
|
|
254
|
+
if b in enemy_bullets:
|
|
255
|
+
enemy_bullets.remove(b)
|
|
256
|
+
|
|
257
|
+
# Collision: enemy bullets vs player
|
|
258
|
+
if invincible > 0:
|
|
259
|
+
invincible -= 1
|
|
260
|
+
else:
|
|
261
|
+
for b in enemy_bullets[:]:
|
|
262
|
+
if b[0] >= player_y - 1 and b[0] <= player_y:
|
|
263
|
+
if abs(b[1] - player_x) <= 1:
|
|
264
|
+
lives -= 1
|
|
265
|
+
invincible = 40
|
|
266
|
+
if b in enemy_bullets:
|
|
267
|
+
enemy_bullets.remove(b)
|
|
268
|
+
if lives <= 0:
|
|
269
|
+
game_over = True
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
# Aliens reach player
|
|
273
|
+
for a in aliens:
|
|
274
|
+
if a["alive"]:
|
|
275
|
+
ay = alien_base_y + a["r"] * ALIEN_SPACING_Y
|
|
276
|
+
if ay >= player_y - 2:
|
|
277
|
+
game_over = True
|
|
278
|
+
break
|
|
279
|
+
|
|
280
|
+
# Wave cleared
|
|
281
|
+
if alive_count == 0:
|
|
282
|
+
wave += 1
|
|
283
|
+
bullets.clear()
|
|
284
|
+
enemy_bullets.clear()
|
|
285
|
+
init_wave()
|
|
286
|
+
|
|
287
|
+
# Draw
|
|
288
|
+
stdscr.erase()
|
|
289
|
+
|
|
290
|
+
# HUD
|
|
291
|
+
hud = f"SCORE: {score:06d} LIVES: {'@ ' * lives} WAVE: {wave}"
|
|
292
|
+
try:
|
|
293
|
+
stdscr.addstr(0, max(0, (width - len(hud)) // 2), hud,
|
|
294
|
+
curses.color_pair(5) | curses.A_BOLD)
|
|
295
|
+
except curses.error:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
title = "CLAUDE INVADERS"
|
|
299
|
+
try:
|
|
300
|
+
stdscr.addstr(1, max(0, (width - len(title)) // 2), title,
|
|
301
|
+
curses.color_pair(1) | curses.A_BOLD)
|
|
302
|
+
except curses.error:
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
# Shields
|
|
306
|
+
for sy, sx in shields:
|
|
307
|
+
if 0 <= sy < height and 0 <= sx < width:
|
|
308
|
+
try:
|
|
309
|
+
stdscr.addstr(sy, sx, "#", curses.color_pair(6))
|
|
310
|
+
except curses.error:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
# Aliens
|
|
314
|
+
for a in aliens:
|
|
315
|
+
if not a["alive"]:
|
|
316
|
+
continue
|
|
317
|
+
ay = alien_base_y + a["r"] * ALIEN_SPACING_Y
|
|
318
|
+
ax = alien_base_x + a["c"] * ALIEN_W
|
|
319
|
+
sprite = ALIEN_FRAMES[a["type"]][alien_frame]
|
|
320
|
+
color = curses.color_pair(2 + a["type"])
|
|
321
|
+
if 0 <= ay < height and 0 <= ax < width - len(sprite):
|
|
322
|
+
try:
|
|
323
|
+
stdscr.addstr(ay, ax, sprite, color | curses.A_BOLD)
|
|
324
|
+
except curses.error:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
# Player
|
|
328
|
+
show_player = True
|
|
329
|
+
if invincible > 0 and frame % 4 < 2:
|
|
330
|
+
show_player = False
|
|
331
|
+
if show_player:
|
|
332
|
+
try:
|
|
333
|
+
stdscr.addstr(player_y, player_x - 1, PLAYER_SHIP,
|
|
334
|
+
curses.color_pair(1) | curses.A_BOLD)
|
|
335
|
+
except curses.error:
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
# Player bullets
|
|
339
|
+
for b in bullets:
|
|
340
|
+
if 0 <= b[0] < height and 0 <= b[1] < width:
|
|
341
|
+
try:
|
|
342
|
+
stdscr.addstr(b[0], b[1], "|", curses.color_pair(5) | curses.A_BOLD)
|
|
343
|
+
except curses.error:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# Enemy bullets
|
|
347
|
+
for b in enemy_bullets:
|
|
348
|
+
if 0 <= b[0] < height and 0 <= b[1] < width:
|
|
349
|
+
try:
|
|
350
|
+
stdscr.addstr(b[0], b[1], "!", curses.color_pair(7) | curses.A_BOLD)
|
|
351
|
+
except curses.error:
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
# Controls
|
|
355
|
+
ctrl = "<- -> Move SPACE Shoot P Pause Q Quit"
|
|
356
|
+
try:
|
|
357
|
+
stdscr.addstr(height - 1, max(0, (width - len(ctrl)) // 2), ctrl,
|
|
358
|
+
curses.color_pair(5) | curses.A_DIM)
|
|
359
|
+
except curses.error:
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
notify_timer = draw_notification(stdscr, height, width, notify_timer)
|
|
363
|
+
stdscr.refresh()
|
|
364
|
+
frame += 1
|
|
365
|
+
|
|
366
|
+
elapsed = time.time() - frame_start
|
|
367
|
+
if elapsed < FRAME_TIME:
|
|
368
|
+
time.sleep(FRAME_TIME - elapsed)
|
|
369
|
+
|
|
370
|
+
while True:
|
|
371
|
+
result = play_game()
|
|
372
|
+
if not result:
|
|
373
|
+
break
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def run():
|
|
377
|
+
curses.wrapper(main)
|
|
378
|
+
|
|
379
|
+
if __name__ == "__main__":
|
|
380
|
+
run()
|
package/games/launch.sh
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Claude Games Arcade — smart launcher
|
|
3
|
+
# Auto-detects terminal environment and opens games in a split pane or new tab
|
|
4
|
+
|
|
5
|
+
GAMES_DIR="$HOME/.claude/games"
|
|
6
|
+
SCRIPT="$GAMES_DIR/menu.py"
|
|
7
|
+
GAME_ARG="${1:-}"
|
|
8
|
+
|
|
9
|
+
# ── tmux (works even when invoked outside a tmux pane) ────────────────────────
|
|
10
|
+
if [ -n "$TMUX" ] || tmux has-session 2>/dev/null; then
|
|
11
|
+
if [ -n "$GAME_ARG" ]; then
|
|
12
|
+
tmux split-window -d -v -l 20 "python3 '$SCRIPT' '$GAME_ARG'; read -p 'Press enter to close...'"
|
|
13
|
+
else
|
|
14
|
+
tmux split-window -d -v -l 20 "python3 '$SCRIPT'; read -p 'Press enter to close...'"
|
|
15
|
+
fi
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# ── iTerm2 ────────────────────────────────────────────────────────────────────
|
|
20
|
+
if [ -n "$ITERM_SESSION_ID" ]; then
|
|
21
|
+
if [ -n "$GAME_ARG" ]; then
|
|
22
|
+
osascript -e "tell application \"iTerm2\" to tell current session of current window to split horizontally with default profile command \"python3 '$SCRIPT' '$GAME_ARG'\""
|
|
23
|
+
else
|
|
24
|
+
osascript -e "tell application \"iTerm2\" to tell current session of current window to split horizontally with default profile command \"python3 '$SCRIPT'\""
|
|
25
|
+
fi
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Build the shell command for Terminal.app
|
|
30
|
+
if [ -n "$GAME_ARG" ]; then
|
|
31
|
+
SHELL_CMD="python3 '${SCRIPT}' '${GAME_ARG}'; exit"
|
|
32
|
+
else
|
|
33
|
+
SHELL_CMD="python3 '${SCRIPT}'; exit"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# ── VS Code integrated terminal ───────────────────────────────────────────────
|
|
37
|
+
if [ "$TERM_PROGRAM" = "vscode" ]; then
|
|
38
|
+
osascript <<APPLESCRIPT
|
|
39
|
+
tell application "Terminal"
|
|
40
|
+
activate
|
|
41
|
+
do script "${SHELL_CMD}"
|
|
42
|
+
end tell
|
|
43
|
+
APPLESCRIPT
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# ── Terminal.app (default) ────────────────────────────────────────────────────
|
|
48
|
+
osascript <<APPLESCRIPT
|
|
49
|
+
tell application "Terminal"
|
|
50
|
+
activate
|
|
51
|
+
do script "${SHELL_CMD}"
|
|
52
|
+
end tell
|
|
53
|
+
APPLESCRIPT
|