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,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()
@@ -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