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
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Claude Games 🎮
|
|
2
|
+
|
|
3
|
+
Terminal arcade games to play while [Claude Code](https://claude.ai/claude-code) works on your tasks.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g claude-games
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
In Claude Code, type:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
/games
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or launch a specific game:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
/games tetris
|
|
23
|
+
/games pacman
|
|
24
|
+
/games snake
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Games
|
|
28
|
+
|
|
29
|
+
| Game | Description |
|
|
30
|
+
|------|-------------|
|
|
31
|
+
| Crab Runner | Chrome dino-style endless runner |
|
|
32
|
+
| 2048 | Slide and merge tiles |
|
|
33
|
+
| Pac-Man | Classic dot-eating maze game |
|
|
34
|
+
| ASCII Patrol | Side-scrolling space shooter |
|
|
35
|
+
| Hogwarts Duels | Harry Potter spell-casting adventure |
|
|
36
|
+
| Pokemon Battle | Choose your starter and battle to become Champion |
|
|
37
|
+
| Tetris | Classic block-stacking puzzler |
|
|
38
|
+
| Snake | Eat food, grow long, don't bite yourself |
|
|
39
|
+
| Wordle | Guess the 5-letter word in 6 tries |
|
|
40
|
+
| Space Invaders | Defend Earth from alien waves |
|
|
41
|
+
| Hangman | Guess the programming word |
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Python 3 (for the game engines)
|
|
46
|
+
- A terminal that supports curses (most do)
|
|
47
|
+
- Claude Code
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
`npm install -g` copies game files to `~/.claude/games/` and registers a `/games` slash command in `~/.claude/commands/`. The games run in a tmux split pane, iTerm2 split, or new Terminal window depending on your environment.
|
package/command.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Play terminal games while Claude works (crab-runner, 2048, pacman, ascii-patrol, hogwarts-duels, pokemon-battle, tetris, snake, wordle, invaders, hangman)
|
|
3
|
+
argument-hint: [game-name]
|
|
4
|
+
allowed-tools: [Bash]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Launch the Claude Games Arcade immediately with a SINGLE background bash command — do not wait, do not use multiple steps.
|
|
8
|
+
|
|
9
|
+
If `$ARGUMENTS` is provided, pass it as the game name argument. Otherwise launch the game menu.
|
|
10
|
+
|
|
11
|
+
Run this ONE command with run_in_background=true:
|
|
12
|
+
```bash
|
|
13
|
+
bash "$HOME/.claude/games/launch.sh" "$ARGUMENTS"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
After triggering, immediately respond with exactly one short sentence:
|
|
17
|
+
- If no argument: tell the user the games arcade is opening in a new pane/tab and they can pick a game.
|
|
18
|
+
- If argument provided: tell them the game is launching.
|
|
19
|
+
Add: "Controls shown in-game. Press Q to quit anytime." Nothing else. Do NOT wait for the background task to complete.
|
package/games/2048.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""2048 – Slide and merge tiles to reach 2048!"""
|
|
3
|
+
import curses
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
def main(stdscr):
|
|
7
|
+
curses.curs_set(0)
|
|
8
|
+
curses.start_color()
|
|
9
|
+
curses.use_default_colors()
|
|
10
|
+
curses.init_pair(1, curses.COLOR_WHITE, -1)
|
|
11
|
+
curses.init_pair(2, curses.COLOR_YELLOW, -1)
|
|
12
|
+
curses.init_pair(3, curses.COLOR_GREEN, -1)
|
|
13
|
+
curses.init_pair(4, curses.COLOR_CYAN, -1)
|
|
14
|
+
curses.init_pair(5, curses.COLOR_BLUE, -1)
|
|
15
|
+
curses.init_pair(6, curses.COLOR_MAGENTA, -1)
|
|
16
|
+
curses.init_pair(7, curses.COLOR_RED, -1)
|
|
17
|
+
|
|
18
|
+
TILE_COLOR = {
|
|
19
|
+
0: curses.color_pair(1) | curses.A_DIM,
|
|
20
|
+
2: curses.color_pair(2),
|
|
21
|
+
4: curses.color_pair(2) | curses.A_BOLD,
|
|
22
|
+
8: curses.color_pair(3),
|
|
23
|
+
16: curses.color_pair(3) | curses.A_BOLD,
|
|
24
|
+
32: curses.color_pair(4),
|
|
25
|
+
64: curses.color_pair(4) | curses.A_BOLD,
|
|
26
|
+
128: curses.color_pair(5) | curses.A_BOLD,
|
|
27
|
+
256: curses.color_pair(6),
|
|
28
|
+
512: curses.color_pair(6) | curses.A_BOLD,
|
|
29
|
+
1024: curses.color_pair(7),
|
|
30
|
+
2048: curses.color_pair(7) | curses.A_BOLD,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def tile_color(v):
|
|
34
|
+
return TILE_COLOR.get(v, curses.color_pair(7) | curses.A_BOLD)
|
|
35
|
+
|
|
36
|
+
def new_board():
|
|
37
|
+
b = [[0]*4 for _ in range(4)]
|
|
38
|
+
spawn(b); spawn(b)
|
|
39
|
+
return b
|
|
40
|
+
|
|
41
|
+
def spawn(b):
|
|
42
|
+
empty = [(r, c) for r in range(4) for c in range(4) if b[r][c] == 0]
|
|
43
|
+
if empty:
|
|
44
|
+
r, c = random.choice(empty)
|
|
45
|
+
b[r][c] = 4 if random.random() < 0.1 else 2
|
|
46
|
+
|
|
47
|
+
def slide(row):
|
|
48
|
+
tiles = [x for x in row if x]
|
|
49
|
+
merged, pts = [], 0
|
|
50
|
+
i = 0
|
|
51
|
+
while i < len(tiles):
|
|
52
|
+
if i + 1 < len(tiles) and tiles[i] == tiles[i+1]:
|
|
53
|
+
val = tiles[i] * 2
|
|
54
|
+
merged.append(val)
|
|
55
|
+
pts += val
|
|
56
|
+
i += 2
|
|
57
|
+
else:
|
|
58
|
+
merged.append(tiles[i])
|
|
59
|
+
i += 1
|
|
60
|
+
merged += [0] * (4 - len(merged))
|
|
61
|
+
return merged, pts
|
|
62
|
+
|
|
63
|
+
def do_move(b, d):
|
|
64
|
+
nb = [r[:] for r in b]
|
|
65
|
+
pts = 0
|
|
66
|
+
moved = False
|
|
67
|
+
if d == 'L':
|
|
68
|
+
for r in range(4):
|
|
69
|
+
nr, s = slide(nb[r])
|
|
70
|
+
if nr != nb[r]: moved = True
|
|
71
|
+
nb[r] = nr; pts += s
|
|
72
|
+
elif d == 'R':
|
|
73
|
+
for r in range(4):
|
|
74
|
+
rev, s = slide(nb[r][::-1])
|
|
75
|
+
nr = rev[::-1]
|
|
76
|
+
if nr != nb[r]: moved = True
|
|
77
|
+
nb[r] = nr; pts += s
|
|
78
|
+
elif d == 'U':
|
|
79
|
+
for c in range(4):
|
|
80
|
+
col = [nb[r][c] for r in range(4)]
|
|
81
|
+
nc, s = slide(col)
|
|
82
|
+
for r in range(4):
|
|
83
|
+
if nb[r][c] != nc[r]: moved = True
|
|
84
|
+
nb[r][c] = nc[r]
|
|
85
|
+
pts += s
|
|
86
|
+
elif d == 'D':
|
|
87
|
+
for c in range(4):
|
|
88
|
+
col = [nb[r][c] for r in range(4)][::-1]
|
|
89
|
+
ncr, s = slide(col)
|
|
90
|
+
nc = ncr[::-1]
|
|
91
|
+
for r in range(4):
|
|
92
|
+
if nb[r][c] != nc[r]: moved = True
|
|
93
|
+
nb[r][c] = nc[r]
|
|
94
|
+
pts += s
|
|
95
|
+
return nb, pts, moved
|
|
96
|
+
|
|
97
|
+
def is_over(b):
|
|
98
|
+
for r in range(4):
|
|
99
|
+
for c in range(4):
|
|
100
|
+
if b[r][c] == 0: return False
|
|
101
|
+
if c < 3 and b[r][c] == b[r][c+1]: return False
|
|
102
|
+
if r < 3 and b[r][c] == b[r+1][c]: return False
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def has_won(b):
|
|
106
|
+
return any(b[r][c] >= 2048 for r in range(4) for c in range(4))
|
|
107
|
+
|
|
108
|
+
CW, CH = 7, 3 # cell width / height
|
|
109
|
+
|
|
110
|
+
def draw(b, score, best, over, won):
|
|
111
|
+
H, W = stdscr.getmaxyx()
|
|
112
|
+
stdscr.erase()
|
|
113
|
+
|
|
114
|
+
# ── Title ──────────────────────────────────────────────
|
|
115
|
+
title = " ╔══╗ ╔═══╗ ╔══╗ ╔═══╗ "
|
|
116
|
+
sub = " ║ ║ ║ ╠══╣ ║ ║ "
|
|
117
|
+
sub2 = " ╚══╝ ╚═══╝ ║ ║ ╚═══╝ "
|
|
118
|
+
big = " 2 0 4 8 "
|
|
119
|
+
try:
|
|
120
|
+
stdscr.addstr(0, max(0, (W - len(big)) // 2), big,
|
|
121
|
+
curses.color_pair(7) | curses.A_BOLD)
|
|
122
|
+
except curses.error:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
sc_str = f"Score: {score:7d} Best: {best:7d}"
|
|
126
|
+
try:
|
|
127
|
+
stdscr.addstr(1, max(0, (W - len(sc_str)) // 2), sc_str,
|
|
128
|
+
curses.color_pair(2))
|
|
129
|
+
except curses.error:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
# ── Grid ───────────────────────────────────────────────
|
|
133
|
+
bw = 4 * CW + 5
|
|
134
|
+
sx = max(0, (W - bw) // 2)
|
|
135
|
+
sy = 3
|
|
136
|
+
|
|
137
|
+
# Horizontal lines
|
|
138
|
+
for row in range(5):
|
|
139
|
+
y = sy + row * (CH + 1)
|
|
140
|
+
l = "┌" if row == 0 else ("└" if row == 4 else "├")
|
|
141
|
+
r = "┐" if row == 0 else ("┘" if row == 4 else "┤")
|
|
142
|
+
m = "┬" if row == 0 else ("┴" if row == 4 else "┼")
|
|
143
|
+
seg = "─" * CW
|
|
144
|
+
line = l + (seg + m) * 3 + seg + r
|
|
145
|
+
try:
|
|
146
|
+
stdscr.addstr(y, sx, line, curses.color_pair(1) | curses.A_DIM)
|
|
147
|
+
except curses.error:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
# Cell content + vertical separators
|
|
151
|
+
for row in range(4):
|
|
152
|
+
for line in range(CH):
|
|
153
|
+
y = sy + row * (CH + 1) + 1 + line
|
|
154
|
+
try:
|
|
155
|
+
stdscr.addstr(y, sx, "│", curses.color_pair(1) | curses.A_DIM)
|
|
156
|
+
except curses.error:
|
|
157
|
+
pass
|
|
158
|
+
for col in range(4):
|
|
159
|
+
x = sx + 1 + col * (CW + 1)
|
|
160
|
+
val = b[row][col]
|
|
161
|
+
color = tile_color(val)
|
|
162
|
+
if line == CH // 2:
|
|
163
|
+
text = (str(val) if val else "·").center(CW)
|
|
164
|
+
else:
|
|
165
|
+
text = " " * CW
|
|
166
|
+
try:
|
|
167
|
+
stdscr.addstr(y, x, text, color)
|
|
168
|
+
stdscr.addstr(y, x + CW, "│", curses.color_pair(1) | curses.A_DIM)
|
|
169
|
+
except curses.error:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
# ── Status bar ─────────────────────────────────────────
|
|
173
|
+
bot = sy + 4 * (CH + 1) + 1
|
|
174
|
+
if over:
|
|
175
|
+
msg = "GAME OVER! R = restart Q = quit"
|
|
176
|
+
try:
|
|
177
|
+
stdscr.addstr(bot, max(0, (W - len(msg)) // 2), msg,
|
|
178
|
+
curses.color_pair(7) | curses.A_BOLD)
|
|
179
|
+
except curses.error:
|
|
180
|
+
pass
|
|
181
|
+
elif won:
|
|
182
|
+
msg = "YOU WIN! 🎉 Keep going or R = restart Q = quit"
|
|
183
|
+
try:
|
|
184
|
+
stdscr.addstr(bot, max(0, (W - len(msg)) // 2), msg,
|
|
185
|
+
curses.color_pair(3) | curses.A_BOLD)
|
|
186
|
+
except curses.error:
|
|
187
|
+
pass
|
|
188
|
+
else:
|
|
189
|
+
hint = "← → ↑ ↓ slide R restart Q quit"
|
|
190
|
+
try:
|
|
191
|
+
stdscr.addstr(bot, max(0, (W - len(hint)) // 2), hint,
|
|
192
|
+
curses.color_pair(1) | curses.A_DIM)
|
|
193
|
+
except curses.error:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
stdscr.refresh()
|
|
197
|
+
|
|
198
|
+
# ── Game loop ──────────────────────────────────────────────
|
|
199
|
+
board = new_board()
|
|
200
|
+
score = 0
|
|
201
|
+
best = 0
|
|
202
|
+
over = False
|
|
203
|
+
won = False
|
|
204
|
+
keep_going = False
|
|
205
|
+
|
|
206
|
+
while True:
|
|
207
|
+
draw(board, score, best, over, won and not keep_going)
|
|
208
|
+
key = stdscr.getch()
|
|
209
|
+
|
|
210
|
+
if key in (ord('q'), ord('Q')):
|
|
211
|
+
break
|
|
212
|
+
if key in (ord('r'), ord('R')):
|
|
213
|
+
board = new_board(); score = 0
|
|
214
|
+
over = False; won = False; keep_going = False
|
|
215
|
+
continue
|
|
216
|
+
if key in (ord('c'), ord('C')) and won:
|
|
217
|
+
keep_going = True
|
|
218
|
+
if over:
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
d = None
|
|
222
|
+
if key == curses.KEY_LEFT: d = 'L'
|
|
223
|
+
elif key == curses.KEY_RIGHT: d = 'R'
|
|
224
|
+
elif key == curses.KEY_UP: d = 'U'
|
|
225
|
+
elif key == curses.KEY_DOWN: d = 'D'
|
|
226
|
+
|
|
227
|
+
if d:
|
|
228
|
+
nb, pts, moved = do_move(board, d)
|
|
229
|
+
if moved:
|
|
230
|
+
board = nb
|
|
231
|
+
score += pts
|
|
232
|
+
if score > best: best = score
|
|
233
|
+
spawn(board)
|
|
234
|
+
if not keep_going and has_won(board): won = True
|
|
235
|
+
if is_over(board): over = True
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
curses.wrapper(main)
|
package/games/dino.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Claude Crab Runner - 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
|
+
def main(stdscr):
|
|
9
|
+
curses.curs_set(0)
|
|
10
|
+
stdscr.nodelay(True)
|
|
11
|
+
curses.start_color()
|
|
12
|
+
# Try to set a terracotta/orange color for the crab
|
|
13
|
+
if curses.can_change_color() and curses.COLORS >= 256:
|
|
14
|
+
curses.init_color(16, 800, 480, 360) # terracotta
|
|
15
|
+
curses.init_pair(1, 16, curses.COLOR_BLACK)
|
|
16
|
+
else:
|
|
17
|
+
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # crab (fallback)
|
|
18
|
+
curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK) # cactus
|
|
19
|
+
curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # score
|
|
20
|
+
curses.init_pair(4, curses.COLOR_CYAN, curses.COLOR_BLACK) # clouds
|
|
21
|
+
init_notify_colors()
|
|
22
|
+
|
|
23
|
+
height, width = stdscr.getmaxyx()
|
|
24
|
+
ground_y = height - 4
|
|
25
|
+
crab_x = 8
|
|
26
|
+
|
|
27
|
+
# Game state
|
|
28
|
+
crab_y = ground_y
|
|
29
|
+
vel_y = 0.0
|
|
30
|
+
is_jumping = False
|
|
31
|
+
is_ducking = False
|
|
32
|
+
duck_frames = 0
|
|
33
|
+
score = 0
|
|
34
|
+
high_score = 0
|
|
35
|
+
game_speed = 1.5
|
|
36
|
+
game_over = False
|
|
37
|
+
started = False
|
|
38
|
+
paused = False
|
|
39
|
+
frame = 0
|
|
40
|
+
notify_timer = 0
|
|
41
|
+
scroll_acc = 0.0
|
|
42
|
+
|
|
43
|
+
obstacles = []
|
|
44
|
+
clouds = [[width - 10, ground_y - 6], [width - 30, ground_y - 8]]
|
|
45
|
+
particles = []
|
|
46
|
+
|
|
47
|
+
# Claude crab sprites – pixel-art matching claudecode-color.png icon
|
|
48
|
+
# Wide body, two square eye-holes, 4 stubby legs
|
|
49
|
+
CRAB_RUN1 = [
|
|
50
|
+
"▄▄▄▄▄▄▄▄",
|
|
51
|
+
"█ ▄ ▄ █",
|
|
52
|
+
"█ ▀ ▀ █",
|
|
53
|
+
"████████",
|
|
54
|
+
"▌▌ ▐▐",
|
|
55
|
+
]
|
|
56
|
+
CRAB_RUN2 = [
|
|
57
|
+
"▄▄▄▄▄▄▄▄",
|
|
58
|
+
"█ ▄ ▄ █",
|
|
59
|
+
"█ ▀ ▀ █",
|
|
60
|
+
"████████",
|
|
61
|
+
" ▌▌ ▐▐ ",
|
|
62
|
+
]
|
|
63
|
+
CRAB_DUCK = [
|
|
64
|
+
"▄▄▄▄▄▄▄▄",
|
|
65
|
+
"█ ▄ ▄ █",
|
|
66
|
+
"█ ▀ ▀ █",
|
|
67
|
+
"████████",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
CACTUS_SMALL = [" ▐█▌ ", " ███ ", "─███─"]
|
|
71
|
+
CACTUS_TALL = [" ▐█▌ ", " ███ ", " ███ ", "─███─"]
|
|
72
|
+
CACTUS_DOUBLE = [" ▐█▌▐█▌", " ███████", " ███████", "─███████"]
|
|
73
|
+
|
|
74
|
+
CLOUD = "☁ ☁ ☁"
|
|
75
|
+
|
|
76
|
+
obstacle_types = [CACTUS_SMALL, CACTUS_TALL, CACTUS_DOUBLE]
|
|
77
|
+
obs_heights = [3, 4, 3]
|
|
78
|
+
obs_widths = [5, 5, 8]
|
|
79
|
+
|
|
80
|
+
next_obs_dist = random.randint(40, 70)
|
|
81
|
+
obs_timer = 0
|
|
82
|
+
|
|
83
|
+
FRAME_TIME = 0.05 # 20 fps
|
|
84
|
+
GRAVITY = 0.7
|
|
85
|
+
JUMP_VEL = -5.5
|
|
86
|
+
SPRITE_H = 5 # rows tall
|
|
87
|
+
SPRITE_H_DUCK = 4 # rows tall when ducking
|
|
88
|
+
|
|
89
|
+
def draw_crab(y, x, f, ducking):
|
|
90
|
+
sprite = CRAB_DUCK if ducking else (CRAB_RUN1 if f % 2 == 0 else CRAB_RUN2)
|
|
91
|
+
sh = len(sprite)
|
|
92
|
+
for i, line in enumerate(sprite):
|
|
93
|
+
row = y - sh + i + 1
|
|
94
|
+
if 0 <= row < height and 0 <= x < width - len(line):
|
|
95
|
+
try:
|
|
96
|
+
stdscr.addstr(row, x, line, curses.color_pair(1) | curses.A_BOLD)
|
|
97
|
+
except curses.error:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
def draw_obstacle(obs):
|
|
101
|
+
ox, otype = obs
|
|
102
|
+
sprite = obstacle_types[otype]
|
|
103
|
+
oh = obs_heights[otype]
|
|
104
|
+
for i, line in enumerate(sprite):
|
|
105
|
+
row = ground_y - oh + i + 1
|
|
106
|
+
if 0 <= row < height and 0 <= ox < width - len(line):
|
|
107
|
+
try:
|
|
108
|
+
stdscr.addstr(row, ox, line, curses.color_pair(2) | curses.A_BOLD)
|
|
109
|
+
except curses.error:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
def check_collision(cy, cx, obs_list):
|
|
113
|
+
sh = SPRITE_H_DUCK if is_ducking else SPRITE_H
|
|
114
|
+
c_top = cy - (sh - 1)
|
|
115
|
+
c_bot = cy
|
|
116
|
+
c_left = cx + 1
|
|
117
|
+
c_right = cx + 6
|
|
118
|
+
for obs in obs_list:
|
|
119
|
+
ox, otype = obs
|
|
120
|
+
oh = obs_heights[otype]
|
|
121
|
+
ow = obs_widths[otype]
|
|
122
|
+
o_top = ground_y - oh + 1
|
|
123
|
+
o_bot = ground_y
|
|
124
|
+
o_left = ox
|
|
125
|
+
o_right = ox + ow - 1
|
|
126
|
+
if (c_right >= o_left + 1 and c_left <= o_right - 1 and
|
|
127
|
+
c_bot >= o_top and c_top <= o_bot):
|
|
128
|
+
return True
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
def draw_title_screen():
|
|
132
|
+
stdscr.erase()
|
|
133
|
+
try:
|
|
134
|
+
stdscr.addstr(ground_y + 1, 0, "─" * (width - 1))
|
|
135
|
+
except curses.error:
|
|
136
|
+
pass
|
|
137
|
+
title = "🦀 CLAUDE CRAB RUNNER 🦀"
|
|
138
|
+
subtitle = "SPACE / ↑ to start & jump ↓ to duck Q to quit"
|
|
139
|
+
try:
|
|
140
|
+
stdscr.addstr(height // 2 - 3, max(0, (width - len(title)) // 2), title,
|
|
141
|
+
curses.color_pair(1) | curses.A_BOLD)
|
|
142
|
+
stdscr.addstr(height // 2 - 1, max(0, (width - len(subtitle)) // 2), subtitle,
|
|
143
|
+
curses.color_pair(3))
|
|
144
|
+
except curses.error:
|
|
145
|
+
pass
|
|
146
|
+
if game_over:
|
|
147
|
+
msgs = [
|
|
148
|
+
("GAME OVER", curses.color_pair(2) | curses.A_BOLD),
|
|
149
|
+
(f"High Score: {high_score}", curses.color_pair(3)),
|
|
150
|
+
("Press SPACE to restart", curses.color_pair(4)),
|
|
151
|
+
]
|
|
152
|
+
for offset, (msg, attr) in enumerate(msgs, 1):
|
|
153
|
+
try:
|
|
154
|
+
stdscr.addstr(height // 2 + offset, max(0, (width - len(msg)) // 2), msg, attr)
|
|
155
|
+
except curses.error:
|
|
156
|
+
pass
|
|
157
|
+
draw_crab(ground_y, crab_x, 0, False)
|
|
158
|
+
stdscr.refresh()
|
|
159
|
+
|
|
160
|
+
while True:
|
|
161
|
+
frame_start = time.time()
|
|
162
|
+
|
|
163
|
+
# --- Input: drain buffer, keep last key ---
|
|
164
|
+
key = stdscr.getch()
|
|
165
|
+
k2 = key
|
|
166
|
+
while k2 != -1:
|
|
167
|
+
k2 = stdscr.getch()
|
|
168
|
+
if k2 != -1:
|
|
169
|
+
key = k2
|
|
170
|
+
|
|
171
|
+
if key in (ord('q'), ord('Q')):
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
if key in (ord('p'), ord('P')) and started and not game_over:
|
|
175
|
+
paused = not paused
|
|
176
|
+
|
|
177
|
+
if paused:
|
|
178
|
+
pause_msg = "PAUSED - P to resume"
|
|
179
|
+
try:
|
|
180
|
+
stdscr.addstr(height // 2, max(0, (width - len(pause_msg)) // 2),
|
|
181
|
+
pause_msg, curses.color_pair(3) | curses.A_BOLD)
|
|
182
|
+
except curses.error:
|
|
183
|
+
pass
|
|
184
|
+
stdscr.refresh()
|
|
185
|
+
time.sleep(FRAME_TIME)
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
if key in (ord(' '), curses.KEY_UP):
|
|
189
|
+
if not started:
|
|
190
|
+
started = True
|
|
191
|
+
game_over = False
|
|
192
|
+
elif game_over:
|
|
193
|
+
crab_y = ground_y
|
|
194
|
+
vel_y = 0.0
|
|
195
|
+
is_jumping = False
|
|
196
|
+
is_ducking = False
|
|
197
|
+
duck_frames = 0
|
|
198
|
+
obstacles.clear()
|
|
199
|
+
obs_timer = 0
|
|
200
|
+
next_obs_dist = random.randint(40, 70)
|
|
201
|
+
score = 0
|
|
202
|
+
game_speed = 1.5
|
|
203
|
+
game_over = False
|
|
204
|
+
frame = 0
|
|
205
|
+
scroll_acc = 0.0
|
|
206
|
+
elif not is_jumping:
|
|
207
|
+
vel_y = JUMP_VEL
|
|
208
|
+
is_jumping = True
|
|
209
|
+
particles.append([crab_x + 3, crab_y, 4])
|
|
210
|
+
|
|
211
|
+
# Duck: extend timer on each DOWN keypress; auto-expires
|
|
212
|
+
if key == curses.KEY_DOWN:
|
|
213
|
+
duck_frames = max(duck_frames, 8)
|
|
214
|
+
if duck_frames > 0:
|
|
215
|
+
is_ducking = True
|
|
216
|
+
duck_frames -= 1
|
|
217
|
+
else:
|
|
218
|
+
is_ducking = False
|
|
219
|
+
|
|
220
|
+
if check_task_done():
|
|
221
|
+
notify_timer = 80 # 4 seconds at 20fps
|
|
222
|
+
|
|
223
|
+
if not started or game_over:
|
|
224
|
+
draw_title_screen()
|
|
225
|
+
time.sleep(FRAME_TIME)
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
# --- Physics ---
|
|
229
|
+
vel_y += GRAVITY
|
|
230
|
+
crab_y += vel_y
|
|
231
|
+
if crab_y >= ground_y:
|
|
232
|
+
crab_y = ground_y
|
|
233
|
+
vel_y = 0.0
|
|
234
|
+
is_jumping = False
|
|
235
|
+
|
|
236
|
+
# --- Scroll ---
|
|
237
|
+
scroll_acc += game_speed
|
|
238
|
+
scroll = int(scroll_acc)
|
|
239
|
+
scroll_acc -= scroll
|
|
240
|
+
|
|
241
|
+
obs_timer += scroll
|
|
242
|
+
for obs in obstacles:
|
|
243
|
+
obs[0] -= scroll
|
|
244
|
+
obstacles = [o for o in obstacles if o[0] > -10]
|
|
245
|
+
|
|
246
|
+
if obs_timer >= next_obs_dist:
|
|
247
|
+
obs_timer = 0
|
|
248
|
+
next_obs_dist = random.randint(35, 60)
|
|
249
|
+
obstacles.append([width - 2, random.randint(0, 2)])
|
|
250
|
+
|
|
251
|
+
for cloud in clouds:
|
|
252
|
+
if frame % 2 == 0:
|
|
253
|
+
cloud[0] -= max(1, scroll // 2)
|
|
254
|
+
if cloud[0] < -len(CLOUD):
|
|
255
|
+
cloud[0] = width + random.randint(5, 20)
|
|
256
|
+
cloud[1] = ground_y - random.randint(5, 10)
|
|
257
|
+
|
|
258
|
+
for p in particles:
|
|
259
|
+
p[0] += random.randint(-1, 1)
|
|
260
|
+
p[1] += 1
|
|
261
|
+
p[2] -= 1
|
|
262
|
+
particles[:] = [p for p in particles if p[2] > 0]
|
|
263
|
+
|
|
264
|
+
score += 1
|
|
265
|
+
if score > high_score:
|
|
266
|
+
high_score = score
|
|
267
|
+
game_speed = min(4.0, 1.5 + score / 800)
|
|
268
|
+
|
|
269
|
+
if check_collision(int(crab_y), crab_x, obstacles):
|
|
270
|
+
game_over = True
|
|
271
|
+
|
|
272
|
+
# --- Draw ---
|
|
273
|
+
stdscr.erase()
|
|
274
|
+
|
|
275
|
+
for cloud in clouds:
|
|
276
|
+
if 0 <= cloud[1] < height and 0 <= cloud[0] < width - len(CLOUD):
|
|
277
|
+
try:
|
|
278
|
+
stdscr.addstr(cloud[1], cloud[0], CLOUD, curses.color_pair(4))
|
|
279
|
+
except curses.error:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
stdscr.addstr(ground_y + 1, 0, "─" * (width - 1))
|
|
284
|
+
dot_offset = (frame * scroll) % 6
|
|
285
|
+
ground_detail = "".join("·" if (i + dot_offset) % 6 == 0 else " " for i in range(width - 1))
|
|
286
|
+
stdscr.addstr(ground_y + 2, 0, ground_detail)
|
|
287
|
+
except curses.error:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
for p in particles:
|
|
291
|
+
if 0 <= p[1] < height and 0 <= p[0] < width:
|
|
292
|
+
try:
|
|
293
|
+
stdscr.addstr(p[1], p[0], "·", curses.color_pair(3))
|
|
294
|
+
except curses.error:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
for obs in obstacles:
|
|
298
|
+
draw_obstacle(obs)
|
|
299
|
+
|
|
300
|
+
draw_crab(int(crab_y), crab_x, frame, is_ducking)
|
|
301
|
+
|
|
302
|
+
score_str = f"SCORE: {score:05d} HI: {high_score:05d}"
|
|
303
|
+
controls = "↑/SPACE jump ↓ duck P pause Q quit"
|
|
304
|
+
try:
|
|
305
|
+
stdscr.addstr(0, max(0, (width - len(score_str)) // 2), score_str,
|
|
306
|
+
curses.color_pair(3) | curses.A_BOLD)
|
|
307
|
+
stdscr.addstr(0, 1, controls, curses.color_pair(4))
|
|
308
|
+
except curses.error:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
notify_timer = draw_notification(stdscr, height, width, notify_timer)
|
|
312
|
+
stdscr.refresh()
|
|
313
|
+
frame += 1
|
|
314
|
+
|
|
315
|
+
elapsed = time.time() - frame_start
|
|
316
|
+
sleep_t = FRAME_TIME - elapsed
|
|
317
|
+
if sleep_t > 0:
|
|
318
|
+
time.sleep(sleep_t)
|
|
319
|
+
|
|
320
|
+
def run():
|
|
321
|
+
curses.wrapper(main)
|
|
322
|
+
|
|
323
|
+
if __name__ == "__main__":
|
|
324
|
+
run()
|