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