cli-arcade 2026.1.3__py3-none-any.whl → 2026.2.1__py3-none-any.whl

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.
Files changed (35) hide show
  1. cli.py +110 -67
  2. {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/METADATA +41 -10
  3. cli_arcade-2026.2.1.dist-info/RECORD +50 -0
  4. game_classes/__pycache__/__init__.cpython-312.pyc +0 -0
  5. game_classes/__pycache__/game_base.cpython-312.pyc +0 -0
  6. game_classes/__pycache__/game_base.cpython-313.pyc +0 -0
  7. game_classes/__pycache__/highscores.cpython-312.pyc +0 -0
  8. game_classes/__pycache__/menu.cpython-312.pyc +0 -0
  9. game_classes/__pycache__/menu.cpython-313.pyc +0 -0
  10. game_classes/__pycache__/ptk.cpython-312.pyc +0 -0
  11. game_classes/__pycache__/ptk.cpython-313.pyc +0 -0
  12. game_classes/__pycache__/ptk_curses.cpython-313.pyc +0 -0
  13. game_classes/__pycache__/ptk_game_base.cpython-313.pyc +0 -0
  14. game_classes/__pycache__/ptk_menu.cpython-313.pyc +0 -0
  15. game_classes/__pycache__/ptk_tools.cpython-313.pyc +0 -0
  16. game_classes/__pycache__/tools.cpython-312.pyc +0 -0
  17. game_classes/__pycache__/tools.cpython-313.pyc +0 -0
  18. game_classes/game_base.py +5 -5
  19. game_classes/menu.py +4 -4
  20. game_classes/ptk.py +435 -0
  21. game_classes/tools.py +27 -25
  22. games/byte_bouncer/__pycache__/game.cpython-312.pyc +0 -0
  23. games/byte_bouncer/__pycache__/game.cpython-313.pyc +0 -0
  24. games/byte_bouncer/game.py +45 -21
  25. games/star_ship/__pycache__/game.cpython-312.pyc +0 -0
  26. games/star_ship/__pycache__/game.cpython-313.pyc +0 -0
  27. games/star_ship/game.py +64 -24
  28. games/terminal_tumble/__pycache__/game.cpython-312.pyc +0 -0
  29. games/terminal_tumble/__pycache__/game.cpython-313.pyc +0 -0
  30. games/terminal_tumble/game.py +41 -38
  31. cli_arcade-2026.1.3.dist-info/RECORD +0 -35
  32. {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/WHEEL +0 -0
  33. {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/entry_points.txt +0 -0
  34. {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/licenses/LICENSE +0 -0
  35. {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/top_level.txt +0 -0
game_classes/ptk.py ADDED
@@ -0,0 +1,435 @@
1
+ import os
2
+ import sys
3
+ import shutil
4
+ import threading
5
+ import queue
6
+ import time
7
+
8
+ _HAS_PROMPT_TOOLKIT = True
9
+ try:
10
+ from prompt_toolkit.input import create_input
11
+ from prompt_toolkit.keys import Keys
12
+ except Exception:
13
+ create_input = None
14
+ Keys = None
15
+ _HAS_PROMPT_TOOLKIT = False
16
+ import select
17
+
18
+ _HAS_TERMIOS = True
19
+ try:
20
+ import termios
21
+ import tty
22
+ except Exception:
23
+ termios = None
24
+ tty = None
25
+ _HAS_TERMIOS = False
26
+
27
+ # basic color constants (match curses style usage)
28
+ COLOR_BLACK = 0
29
+ COLOR_RED = 1
30
+ COLOR_GREEN = 2
31
+ COLOR_YELLOW = 3
32
+ COLOR_BLUE = 4
33
+ COLOR_MAGENTA = 5
34
+ COLOR_CYAN = 6
35
+ COLOR_WHITE = 7
36
+
37
+ A_NORMAL = 0
38
+ A_BOLD = 1 << 0
39
+ A_DIM = 1 << 1
40
+ A_REVERSE = 1 << 2
41
+
42
+ KEY_LEFT = 260
43
+ KEY_RIGHT = 261
44
+ KEY_UP = 259
45
+ KEY_DOWN = 258
46
+ KEY_PPAGE = 339
47
+ KEY_NPAGE = 338
48
+ KEY_BACKSPACE = 263
49
+ KEY_ENTER = 10
50
+
51
+ _ANSI_COLORS = {
52
+ COLOR_BLACK: 30,
53
+ COLOR_RED: 31,
54
+ COLOR_GREEN: 32,
55
+ COLOR_YELLOW: 33,
56
+ COLOR_BLUE: 34,
57
+ COLOR_MAGENTA: 35,
58
+ COLOR_CYAN: 36,
59
+ COLOR_WHITE: 37,
60
+ }
61
+
62
+
63
+ def color_pair(color):
64
+ return int(color) << 8
65
+
66
+
67
+ def curs_set(_):
68
+ return None
69
+
70
+
71
+ def has_colors():
72
+ return True
73
+
74
+
75
+ def start_color():
76
+ return None
77
+
78
+
79
+ def use_default_colors():
80
+ return None
81
+
82
+
83
+ def can_change_color():
84
+ return False
85
+
86
+
87
+ def init_color(*_args, **_kwargs):
88
+ return None
89
+
90
+
91
+ def init_pair(*_args, **_kwargs):
92
+ return None
93
+
94
+
95
+ def _decode_attr(attr):
96
+ color = (attr >> 8) & 0xFF
97
+ bold = bool(attr & A_BOLD)
98
+ dim = bool(attr & A_DIM)
99
+ reverse = bool(attr & A_REVERSE)
100
+ return color, bold, dim, reverse
101
+
102
+
103
+ class _Screen:
104
+ def __init__(self):
105
+ _enable_vt_mode()
106
+ self._queue = queue.Queue()
107
+ self._stop = threading.Event()
108
+ self._use_msvcrt = os.name == "nt"
109
+ self._input = None
110
+ self._thread = None
111
+ self._posix_fd = None
112
+ self._orig_term_attrs = None
113
+ if not self._use_msvcrt:
114
+ # Prefer a simple termios-based reader on POSIX for raw key capture
115
+ if _HAS_TERMIOS:
116
+ try:
117
+ self._posix_fd = sys.stdin.fileno()
118
+ self._orig_term_attrs = termios.tcgetattr(self._posix_fd)
119
+ tty.setcbreak(self._posix_fd)
120
+ self._thread = threading.Thread(target=self._posix_reader, daemon=True)
121
+ self._thread.start()
122
+ except Exception:
123
+ # fall back to prompt_toolkit if termios fails
124
+ self._posix_fd = None
125
+ self._orig_term_attrs = None
126
+ if self._posix_fd is None and _HAS_PROMPT_TOOLKIT and create_input is not None:
127
+ try:
128
+ self._input = create_input()
129
+ self._thread = threading.Thread(target=self._reader, daemon=True)
130
+ self._thread.start()
131
+ except Exception as e:
132
+ self._input = None
133
+ sys.stderr.write(f"[ptk] create_input failed: {e}\n")
134
+ self._timeout = 0.0
135
+ self._buffer = []
136
+ self._attrs = []
137
+ self._rows = 24
138
+ self._cols = 80
139
+ self._refresh_size()
140
+ self.clear()
141
+ try:
142
+ sys.stdout.write("\x1b[?1049h\x1b[?25l")
143
+ sys.stdout.flush()
144
+ except Exception:
145
+ pass
146
+
147
+ def _refresh_size(self):
148
+ try:
149
+ size = shutil.get_terminal_size()
150
+ self._cols = size.columns
151
+ self._rows = size.lines
152
+ except Exception:
153
+ self._cols = 80
154
+ self._rows = 24
155
+
156
+ def _reader(self):
157
+ if not self._input:
158
+ return
159
+ with self._input:
160
+ while not self._stop.is_set():
161
+ try:
162
+ for key in self._input.read_keys():
163
+ self._queue.put(key)
164
+ except Exception as e:
165
+ # log and continue — don't let the thread die silently
166
+ sys.stderr.write(f"[ptk] reader exception: {e}\n")
167
+ time.sleep(0.01)
168
+
169
+ def _posix_reader(self):
170
+ if not _HAS_TERMIOS:
171
+ return
172
+ fd = self._posix_fd
173
+ buf = b""
174
+ while not self._stop.is_set():
175
+ try:
176
+ r, _, _ = select.select([fd], [], [], 0.1)
177
+ if not r:
178
+ continue
179
+ chunk = os.read(fd, 32)
180
+ if not chunk:
181
+ continue
182
+ buf += chunk
183
+ # process buffer for known sequences
184
+ while buf:
185
+ # single-byte control checks
186
+ if buf.startswith(b"\x1b"):
187
+ # escape sequences: try to consume common sequences
188
+ if buf.startswith(b"\x1b[A"):
189
+ self._queue.put(KEY_UP)
190
+ buf = buf[3:]
191
+ continue
192
+ if buf.startswith(b"\x1b[B"):
193
+ self._queue.put(KEY_DOWN)
194
+ buf = buf[3:]
195
+ continue
196
+ if buf.startswith(b"\x1b[C"):
197
+ self._queue.put(KEY_RIGHT)
198
+ buf = buf[3:]
199
+ continue
200
+ if buf.startswith(b"\x1b[D"):
201
+ self._queue.put(KEY_LEFT)
202
+ buf = buf[3:]
203
+ continue
204
+ # PageUp/PageDown common sequences
205
+ if buf.startswith(b"\x1b[5~"):
206
+ self._queue.put(KEY_PPAGE)
207
+ buf = buf[4:]
208
+ continue
209
+ if buf.startswith(b"\x1b[6~"):
210
+ self._queue.put(KEY_NPAGE)
211
+ buf = buf[4:]
212
+ continue
213
+ # unknown escape: drop single ESC
214
+ self._queue.put(27)
215
+ buf = buf[1:]
216
+ continue
217
+ # newline / carriage return
218
+ if buf[0] in (10, 13):
219
+ self._queue.put(10)
220
+ buf = buf[1:]
221
+ continue
222
+ # backspace (DEL or BS)
223
+ if buf[0] in (8, 127):
224
+ self._queue.put(KEY_BACKSPACE)
225
+ buf = buf[1:]
226
+ continue
227
+ # regular printable character
228
+ ch = buf[0]
229
+ if 32 <= ch <= 126:
230
+ self._queue.put(ch)
231
+ buf = buf[1:]
232
+ continue
233
+ # unhandled byte: drop
234
+ buf = buf[1:]
235
+ except Exception as e:
236
+ sys.stderr.write(f"[ptk] posix_reader exception: {e}\n")
237
+ time.sleep(0.01)
238
+
239
+ def stop(self):
240
+ self._stop.set()
241
+ try:
242
+ if self._input:
243
+ self._input.close()
244
+ except Exception:
245
+ pass
246
+ # restore termios attrs if we changed them
247
+ try:
248
+ if self._orig_term_attrs is not None and _HAS_TERMIOS:
249
+ termios.tcsetattr(self._posix_fd, termios.TCSANOW, self._orig_term_attrs)
250
+ except Exception:
251
+ pass
252
+
253
+ def nodelay(self, _flag=True):
254
+ return None
255
+
256
+ def timeout(self, ms):
257
+ try:
258
+ self._timeout = max(0.0, float(ms) / 1000.0)
259
+ except Exception:
260
+ self._timeout = 0.0
261
+
262
+ def keypad(self, _flag=True):
263
+ return None
264
+
265
+ def getmaxyx(self):
266
+ self._refresh_size()
267
+ return self._rows, self._cols
268
+
269
+ def clear(self):
270
+ self._refresh_size()
271
+ self._buffer = [[" " for _ in range(self._cols)] for _ in range(self._rows)]
272
+ self._attrs = [[0 for _ in range(self._cols)] for _ in range(self._rows)]
273
+
274
+ def bkgd(self, _ch, _attr=0):
275
+ return None
276
+
277
+ def addstr(self, y, x, text, attr=0):
278
+ if text is None:
279
+ return
280
+ try:
281
+ s = str(text)
282
+ except Exception:
283
+ return
284
+ if y < 0 or y >= self._rows:
285
+ return
286
+ for i, ch in enumerate(s):
287
+ px = x + i
288
+ if 0 <= px < self._cols:
289
+ self._buffer[y][px] = ch
290
+ self._attrs[y][px] = attr
291
+
292
+ def addch(self, y, x, ch, attr=0):
293
+ if y < 0 or y >= self._rows or x < 0 or x >= self._cols:
294
+ return
295
+ try:
296
+ c = chr(ch) if isinstance(ch, int) else str(ch)
297
+ except Exception:
298
+ return
299
+ if not c:
300
+ return
301
+ self._buffer[y][x] = c[0]
302
+ self._attrs[y][x] = attr
303
+
304
+ def refresh(self):
305
+ out_lines = []
306
+ for y in range(self._rows):
307
+ line = []
308
+ prev_attr = None
309
+ for x in range(self._cols):
310
+ attr = self._attrs[y][x]
311
+ if attr != prev_attr:
312
+ color, bold, dim, reverse = _decode_attr(attr)
313
+ seq = "\x1b[0m"
314
+ if bold:
315
+ seq += "\x1b[1m"
316
+ if dim:
317
+ seq += "\x1b[2m"
318
+ if reverse:
319
+ seq += "\x1b[7m"
320
+ if attr != 0 and color in _ANSI_COLORS:
321
+ seq += f"\x1b[{_ANSI_COLORS[color]}m"
322
+ line.append(seq)
323
+ prev_attr = attr
324
+ line.append(self._buffer[y][x])
325
+ line.append("\x1b[0m")
326
+ out_lines.append("".join(line))
327
+ sys.stdout.write("\x1b[H" + "\n".join(out_lines))
328
+ sys.stdout.flush()
329
+
330
+ def getch(self):
331
+ if self._use_msvcrt:
332
+ return _getch_msvcrt(self._timeout)
333
+ try:
334
+ key = self._queue.get(timeout=self._timeout)
335
+ except Exception:
336
+ return -1
337
+ # key may be an int from posix reader or a keypress object from prompt_toolkit
338
+ if isinstance(key, int):
339
+ return key
340
+ return _map_keypress(key)
341
+
342
+
343
+ def _map_keypress(keypress):
344
+ key = keypress.key
345
+ if key == Keys.Left:
346
+ return KEY_LEFT
347
+ if key == Keys.Right:
348
+ return KEY_RIGHT
349
+ if key == Keys.Up:
350
+ return KEY_UP
351
+ if key == Keys.Down:
352
+ return KEY_DOWN
353
+ if key == Keys.PageUp:
354
+ return KEY_PPAGE
355
+ if key == Keys.PageDown:
356
+ return KEY_NPAGE
357
+ if key in (Keys.Backspace, Keys.ControlH):
358
+ return KEY_BACKSPACE
359
+ if key in (Keys.Enter, Keys.ControlM):
360
+ return 10
361
+ if isinstance(key, str) and len(key) == 1:
362
+ return ord(key)
363
+ return -1
364
+
365
+
366
+ def _getch_msvcrt(timeout):
367
+ try:
368
+ import msvcrt
369
+ except Exception:
370
+ time.sleep(timeout)
371
+ return -1
372
+ end = time.time() + timeout
373
+ while True:
374
+ if msvcrt.kbhit():
375
+ ch = msvcrt.getwch()
376
+ if ch in ("\x00", "\xe0"):
377
+ ch2 = msvcrt.getwch()
378
+ return {
379
+ "K": KEY_LEFT,
380
+ "M": KEY_RIGHT,
381
+ "H": KEY_UP,
382
+ "P": KEY_DOWN,
383
+ "I": KEY_PPAGE,
384
+ "Q": KEY_NPAGE,
385
+ }.get(ch2, -1)
386
+ if ch == "\r":
387
+ return 10
388
+ if ch == "\x08":
389
+ return KEY_BACKSPACE
390
+ return ord(ch)
391
+ if timeout <= 0:
392
+ return -1
393
+ if time.time() >= end:
394
+ return -1
395
+ time.sleep(0.01)
396
+
397
+
398
+ def _enable_vt_mode():
399
+ if os.name != "nt":
400
+ return
401
+ try:
402
+ import ctypes
403
+ kernel32 = ctypes.windll.kernel32
404
+ handle = kernel32.GetStdHandle(-11)
405
+ mode = ctypes.c_uint()
406
+ if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
407
+ kernel32.SetConsoleMode(handle, mode.value | 0x0004)
408
+ except Exception:
409
+ pass
410
+
411
+
412
+ def exit_alternate_screen():
413
+ """Exit the alternate screen buffer and restore the cursor/attributes.
414
+
415
+ Safe to call from anywhere; used when printing messages that must
416
+ appear in the main terminal/scrollback.
417
+ """
418
+ try:
419
+ sys.stdout.write("\x1b[0m\x1b[?25h\x1b[?1049l")
420
+ sys.stdout.flush()
421
+ except Exception:
422
+ pass
423
+
424
+
425
+ def wrapper(func):
426
+ stdscr = _Screen()
427
+ try:
428
+ return func(stdscr)
429
+ finally:
430
+ stdscr.stop()
431
+ try:
432
+ sys.stdout.write("\x1b[0m\x1b[2J\x1b[3J\x1b[H\x1b[?25h\x1b[?1049l")
433
+ sys.stdout.flush()
434
+ except Exception:
435
+ pass
game_classes/tools.py CHANGED
@@ -1,4 +1,4 @@
1
- import curses
1
+ from game_classes import ptk
2
2
  import os
3
3
  import shutil
4
4
 
@@ -12,12 +12,14 @@ def verify_terminal_size(game_name, min_cols=70, min_rows=20):
12
12
  except Exception:
13
13
  cols, rows = 0, 0
14
14
  if cols < min_cols or rows < min_rows:
15
- print(f" [ACTION] Terminal size is too small to run {game_name}.")
16
- if cols < min_cols:
17
- print(f" [ACTION] Actual Colums: {cols} Required Colums: {min_cols}")
18
- if rows < min_rows:
19
- print(f" [ACTION] Actual Rows: {rows} Required Rows: {min_rows}")
20
- raise SystemExit(1)
15
+ # Print a clear message and exit; callers should check sizes before
16
+ # entering the alternate screen when possible.
17
+ print(f" [ACTION] Terminal size is too small to run {game_name}.")
18
+ if cols < min_cols:
19
+ print(f" [ACTION] Actual Colums: {cols} Required Colums: {min_cols}")
20
+ if rows < min_rows:
21
+ print(f" [ACTION] Actual Rows: {rows} Required Rows: {min_rows}")
22
+ raise SystemExit(1)
21
23
 
22
24
  def get_terminal_size(stdscr):
23
25
  try:
@@ -29,39 +31,39 @@ def get_terminal_size(stdscr):
29
31
  cols, rows = 24, 80
30
32
  return max(20, cols - 2), max(6, rows - 1)
31
33
 
32
- def init_curses(stdscr):
33
- curses.curs_set(0)
34
+ def init_ptk(stdscr):
35
+ ptk.curs_set(0)
34
36
  stdscr.nodelay(True)
35
37
  stdscr.timeout(50)
36
- # enable keypad mode so special keys (e.g. numpad Enter) map to curses.KEY_ENTER
38
+ # enable keypad mode so special keys (e.g. numpad Enter) map to ptk.KEY_ENTER
37
39
  try:
38
40
  stdscr.keypad(True)
39
41
  except Exception:
40
42
  pass
41
43
  # init colors
42
- curses.start_color()
43
- curses.use_default_colors()
44
+ ptk.start_color()
45
+ ptk.use_default_colors()
44
46
  # try to normalize key colors (0..1000 scale). Must run before init_pair.
45
- if curses.can_change_color() and curses.COLORS >= 8:
47
+ if ptk.can_change_color() and ptk.COLORS >= 8:
46
48
  try:
47
- curses.init_color(curses.COLOR_MAGENTA, 1000, 0, 1000)
48
- curses.init_color(curses.COLOR_YELLOW, 1000, 1000, 0)
49
- curses.init_color(curses.COLOR_WHITE, 1000, 1000, 1000)
50
- curses.init_color(curses.COLOR_CYAN, 0, 1000, 1000)
51
- curses.init_color(curses.COLOR_BLUE, 0, 0, 1000)
52
- curses.init_color(curses.COLOR_GREEN, 0, 800, 0)
53
- curses.init_color(curses.COLOR_RED, 1000, 0, 0)
54
- curses.init_color(curses.COLOR_BLACK, 0, 0, 0)
49
+ ptk.init_color(ptk.COLOR_MAGENTA, 1000, 0, 1000)
50
+ ptk.init_color(ptk.COLOR_YELLOW, 1000, 1000, 0)
51
+ ptk.init_color(ptk.COLOR_WHITE, 1000, 1000, 1000)
52
+ ptk.init_color(ptk.COLOR_CYAN, 0, 1000, 1000)
53
+ ptk.init_color(ptk.COLOR_BLUE, 0, 0, 1000)
54
+ ptk.init_color(ptk.COLOR_GREEN, 0, 800, 0)
55
+ ptk.init_color(ptk.COLOR_RED, 1000, 0, 0)
56
+ ptk.init_color(ptk.COLOR_BLACK, 0, 0, 0)
55
57
  except Exception:
56
58
  pass
57
59
  for i in range(1,8):
58
- curses.init_pair(i, i, -1)
60
+ ptk.init_pair(i, i, -1)
59
61
  # set an explicit default attribute so unstyled text uses white
60
62
  try:
61
- stdscr.bkgd(' ', curses.color_pair(curses.COLOR_WHITE))
63
+ stdscr.bkgd(' ', ptk.color_pair(ptk.COLOR_WHITE))
62
64
  except Exception:
63
65
  try:
64
- stdscr.bkgd(' ', curses.color_pair(7))
66
+ stdscr.bkgd(' ', ptk.color_pair(7))
65
67
  except Exception:
66
68
  pass
67
69
 
@@ -70,7 +72,7 @@ def clamp(v, a, b):
70
72
 
71
73
  def is_enter_key(ch):
72
74
  try:
73
- enter_vals = {10, 13, getattr(curses, 'KEY_ENTER', -1), 343, 459}
75
+ enter_vals = {10, 13, getattr(ptk, 'KEY_ENTER', -1), 343, 459}
74
76
  except Exception:
75
77
  enter_vals = {10, 13}
76
78
  return ch in enter_vals
@@ -1,4 +1,4 @@
1
- import curses
1
+ from game_classes import ptk
2
2
  import os
3
3
  import random
4
4
  import sys
@@ -14,7 +14,7 @@ except Exception:
14
14
  from game_classes.highscores import HighScores
15
15
  from game_classes.game_base import GameBase
16
16
  from game_classes.menu import Menu
17
- from game_classes.tools import glyph, verify_terminal_size, init_curses, clamp
17
+ from game_classes.tools import glyph, verify_terminal_size, init_ptk, clamp
18
18
 
19
19
  TITLE = [
20
20
  ' ____ _ _ ____ ____ ____ __ _ _ __ _ ___ ____ ____ ',
@@ -23,6 +23,10 @@ TITLE = [
23
23
  r' (____/(__/ (__) (____) (____/ \__/ \____/\_)__) \___)(____)(__\_) '
24
24
  ]
25
25
 
26
+ # minimum terminal size required to run this game (cols, rows)
27
+ MIN_COLS = 70
28
+ MIN_ROWS = 20
29
+
26
30
  class Game(GameBase):
27
31
  def __init__(self, stdscr, player_name='Player'):
28
32
  self.title = TITLE
@@ -30,8 +34,9 @@ class Game(GameBase):
30
34
  'score': {'player': 'Player', 'value': 0},
31
35
  'level': {'player': 'Player', 'value': 1},
32
36
  })
33
- super().__init__(stdscr, player_name, 0.12, curses.COLOR_GREEN)
37
+ super().__init__(stdscr, player_name, 0.12, ptk.COLOR_GREEN)
34
38
  self.init_scores([['score', 0], ['level', 1]])
39
+ self.width += 1
35
40
 
36
41
  # game state
37
42
  self.count = 0
@@ -49,13 +54,13 @@ class Game(GameBase):
49
54
  # draw high scores below title
50
55
  new_score = ' ***NEW High Score!' if self.new_highs.get('score', False) else ''
51
56
  new_level = ' ***NEW High Level!' if self.new_highs.get('level', False) else ''
52
- self.stdscr.addstr(info_y + 1 , info_x, f'High Score: {int(self.high_scores["score"]["value"]):,} ({self.high_scores["score"]["player"]}){new_score}', curses.color_pair(curses.COLOR_GREEN))
53
- self.stdscr.addstr(info_y + 2 , info_x, f'High Level: {int(self.high_scores["level"]["value"]):,} ({self.high_scores["level"]["player"]}){new_level}', curses.color_pair(curses.COLOR_BLUE))
57
+ self.stdscr.addstr(info_y + 1 , info_x, f'High Score: {int(self.high_scores["score"]["value"]):,} ({self.high_scores["score"]["player"]}){new_score}', ptk.color_pair(ptk.COLOR_GREEN))
58
+ self.stdscr.addstr(info_y + 2 , info_x, f'High Level: {int(self.high_scores["level"]["value"]):,} ({self.high_scores["level"]["player"]}){new_level}', ptk.color_pair(ptk.COLOR_BLUE))
54
59
 
55
60
  # draw game info below title
56
61
  self.stdscr.addstr(info_y + 4, info_x, f'Player: {self.player_name}')
57
- self.stdscr.addstr(info_y + 5, info_x, f'Score: {int(self.scores["score"]):,}', curses.color_pair(curses.COLOR_GREEN))
58
- self.stdscr.addstr(info_y + 6, info_x, f'Level: {int(self.scores["level"]):,}', curses.color_pair(curses.COLOR_BLUE))
62
+ self.stdscr.addstr(info_y + 5, info_x, f'Score: {int(self.scores["score"]):,}', ptk.color_pair(ptk.COLOR_GREEN))
63
+ self.stdscr.addstr(info_y + 6, info_x, f'Level: {int(self.scores["level"]):,}', ptk.color_pair(ptk.COLOR_BLUE))
59
64
 
60
65
  self.stdscr.addstr(info_y + 8 , info_x, '← | a : Left')
61
66
  self.stdscr.addstr(info_y + 9 , info_x, '→ | d : Right')
@@ -71,19 +76,39 @@ class Game(GameBase):
71
76
  for idx, b in enumerate(self.balls):
72
77
  try:
73
78
  if idx == 0:
74
- attr = curses.color_pair(curses.COLOR_MAGENTA) | curses.A_BOLD
79
+ attr = ptk.color_pair(ptk.COLOR_MAGENTA) | ptk.A_BOLD
75
80
  else:
76
- attr = curses.color_pair(curses.COLOR_YELLOW) | curses.A_BOLD
77
- self.stdscr.addch(int(b['y']), 1 + int(b['x']), glyph('CIRCLE_FILLED', 'O'), attr)
81
+ attr = ptk.color_pair(ptk.COLOR_YELLOW) | ptk.A_BOLD
82
+ self.stdscr.addch(int(b['y']), int(b['x']), glyph('CIRCLE_FILLED', 'O'), attr)
78
83
  except Exception:
79
84
  pass
80
85
  except Exception:
81
86
  pass
82
87
  # draw paddle
88
+ # draw a green floor along the bottom using the BLOCK glyph, then
89
+ # draw a green right wall. Paddle is drawn on top of the floor.
90
+ try:
91
+ block = glyph('BLOCK')
92
+ except Exception:
93
+ block = '#'
94
+ # floor: across playable width
95
+ for fx in range(0, self.width):
96
+ try:
97
+ self.stdscr.addch(self.height, fx, block, ptk.color_pair(ptk.COLOR_BLUE))
98
+ except Exception:
99
+ pass
100
+ # right wall: draw from top down to the floor at the rightmost column
101
+ right_col = self.width
102
+ for wy in range(0, self.height + 1):
103
+ try:
104
+ self.stdscr.addch(wy, right_col, block, ptk.color_pair(ptk.COLOR_BLUE))
105
+ except Exception:
106
+ pass
107
+
83
108
  for i in range(self.paddle_w):
84
109
  x = clamp(self.paddle_x + i, 0, self.width - 1)
85
110
  try:
86
- self.stdscr.addch(self.height, x + 1, '=', curses.color_pair(curses.COLOR_GREEN) | curses.A_BOLD)
111
+ self.stdscr.addch(self.height - 1, x, '=', ptk.color_pair(ptk.COLOR_GREEN) | ptk.A_BOLD)
87
112
  except Exception:
88
113
  pass
89
114
 
@@ -96,17 +121,17 @@ class Game(GameBase):
96
121
  if b['x'] < 0:
97
122
  b['x'] = 0
98
123
  b['vx'] *= -1
99
- elif b['x'] >= self.width:
100
- b['x'] = self.width - 1
124
+ elif b['x'] >= self.width - 1:
125
+ b['x'] = self.width - 2
101
126
  b['vx'] *= -1
102
127
  if b['y'] < 0:
103
128
  b['y'] = 0
104
129
  b['vy'] *= -1
105
130
  # bottom: check paddle
106
- if b['y'] >= self.height:
131
+ if b['y'] >= self.height - 1:
107
132
  if self.paddle_x <= b['x'] < self.paddle_x + self.paddle_w:
108
133
  # bounce
109
- b['y'] = self.height - 1
134
+ b['y'] = self.height - 2
110
135
  b['vy'] *= -1
111
136
  # normalize horizontal velocity to magnitude 1
112
137
  if b['vx'] < 0:
@@ -181,14 +206,13 @@ class Game(GameBase):
181
206
  pass
182
207
 
183
208
  def movement(self, ch):
184
- if ch in (curses.KEY_LEFT, ord('a')):
209
+ if ch in (ptk.KEY_LEFT, ord('a')):
185
210
  self.paddle_x = int(clamp(self.paddle_x - 2, 0, self.width - self.paddle_w))
186
- elif ch in (curses.KEY_RIGHT, ord('d')):
211
+ elif ch in (ptk.KEY_RIGHT, ord('d')):
187
212
  self.paddle_x = int(clamp(self.paddle_x + 2, 0, self.width - self.paddle_w))
188
213
 
189
214
  def main(stdscr):
190
- verify_terminal_size('Byte Bouncer')
191
- init_curses(stdscr)
215
+ init_ptk(stdscr)
192
216
  while True:
193
217
  game = Game(stdscr)
194
218
  menu = Menu(game)
@@ -200,9 +224,9 @@ def main(stdscr):
200
224
 
201
225
  if __name__ == '__main__':
202
226
  try:
203
- curses.wrapper(main)
227
+ ptk.wrapper(main)
204
228
  except KeyboardInterrupt:
205
229
  try:
206
- curses.endwin()
230
+ ptk.endwin()
207
231
  except Exception:
208
232
  pass