cli-arcade 2026.1.2__py3-none-any.whl → 2026.2.0__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.
game_classes/ptk.py ADDED
@@ -0,0 +1,319 @@
1
+ import os
2
+ import sys
3
+ import shutil
4
+ import threading
5
+ import queue
6
+ import time
7
+
8
+ from prompt_toolkit.input import create_input
9
+ from prompt_toolkit.keys import Keys
10
+
11
+ # basic color constants (match curses style usage)
12
+ COLOR_BLACK = 0
13
+ COLOR_RED = 1
14
+ COLOR_GREEN = 2
15
+ COLOR_YELLOW = 3
16
+ COLOR_BLUE = 4
17
+ COLOR_MAGENTA = 5
18
+ COLOR_CYAN = 6
19
+ COLOR_WHITE = 7
20
+
21
+ A_NORMAL = 0
22
+ A_BOLD = 1 << 0
23
+ A_DIM = 1 << 1
24
+ A_REVERSE = 1 << 2
25
+
26
+ KEY_LEFT = 260
27
+ KEY_RIGHT = 261
28
+ KEY_UP = 259
29
+ KEY_DOWN = 258
30
+ KEY_PPAGE = 339
31
+ KEY_NPAGE = 338
32
+ KEY_BACKSPACE = 263
33
+ KEY_ENTER = 10
34
+
35
+ _ANSI_COLORS = {
36
+ COLOR_BLACK: 30,
37
+ COLOR_RED: 31,
38
+ COLOR_GREEN: 32,
39
+ COLOR_YELLOW: 33,
40
+ COLOR_BLUE: 34,
41
+ COLOR_MAGENTA: 35,
42
+ COLOR_CYAN: 36,
43
+ COLOR_WHITE: 37,
44
+ }
45
+
46
+
47
+ def color_pair(color):
48
+ return int(color) << 8
49
+
50
+
51
+ def curs_set(_):
52
+ return None
53
+
54
+
55
+ def has_colors():
56
+ return True
57
+
58
+
59
+ def start_color():
60
+ return None
61
+
62
+
63
+ def use_default_colors():
64
+ return None
65
+
66
+
67
+ def can_change_color():
68
+ return False
69
+
70
+
71
+ def init_color(*_args, **_kwargs):
72
+ return None
73
+
74
+
75
+ def init_pair(*_args, **_kwargs):
76
+ return None
77
+
78
+
79
+ def _decode_attr(attr):
80
+ color = (attr >> 8) & 0xFF
81
+ bold = bool(attr & A_BOLD)
82
+ dim = bool(attr & A_DIM)
83
+ reverse = bool(attr & A_REVERSE)
84
+ return color, bold, dim, reverse
85
+
86
+
87
+ class _Screen:
88
+ def __init__(self):
89
+ _enable_vt_mode()
90
+ self._queue = queue.Queue()
91
+ self._stop = threading.Event()
92
+ self._use_msvcrt = os.name == "nt"
93
+ self._input = None
94
+ self._thread = None
95
+ if not self._use_msvcrt:
96
+ self._input = create_input()
97
+ self._thread = threading.Thread(target=self._reader, daemon=True)
98
+ self._thread.start()
99
+ self._timeout = 0.0
100
+ self._buffer = []
101
+ self._attrs = []
102
+ self._rows = 24
103
+ self._cols = 80
104
+ self._refresh_size()
105
+ self.clear()
106
+ try:
107
+ sys.stdout.write("\x1b[?1049h\x1b[?25l")
108
+ sys.stdout.flush()
109
+ except Exception:
110
+ pass
111
+
112
+ def _refresh_size(self):
113
+ try:
114
+ size = shutil.get_terminal_size()
115
+ self._cols = size.columns
116
+ self._rows = size.lines
117
+ except Exception:
118
+ self._cols = 80
119
+ self._rows = 24
120
+
121
+ def _reader(self):
122
+ if not self._input:
123
+ return
124
+ with self._input:
125
+ while not self._stop.is_set():
126
+ try:
127
+ for key in self._input.read_keys():
128
+ self._queue.put(key)
129
+ except Exception:
130
+ time.sleep(0.01)
131
+
132
+ def stop(self):
133
+ self._stop.set()
134
+ try:
135
+ if self._input:
136
+ self._input.close()
137
+ except Exception:
138
+ pass
139
+
140
+ def nodelay(self, _flag=True):
141
+ return None
142
+
143
+ def timeout(self, ms):
144
+ try:
145
+ self._timeout = max(0.0, float(ms) / 1000.0)
146
+ except Exception:
147
+ self._timeout = 0.0
148
+
149
+ def keypad(self, _flag=True):
150
+ return None
151
+
152
+ def getmaxyx(self):
153
+ self._refresh_size()
154
+ return self._rows, self._cols
155
+
156
+ def clear(self):
157
+ self._refresh_size()
158
+ self._buffer = [[" " for _ in range(self._cols)] for _ in range(self._rows)]
159
+ self._attrs = [[0 for _ in range(self._cols)] for _ in range(self._rows)]
160
+
161
+ def bkgd(self, _ch, _attr=0):
162
+ return None
163
+
164
+ def addstr(self, y, x, text, attr=0):
165
+ if text is None:
166
+ return
167
+ try:
168
+ s = str(text)
169
+ except Exception:
170
+ return
171
+ if y < 0 or y >= self._rows:
172
+ return
173
+ for i, ch in enumerate(s):
174
+ px = x + i
175
+ if 0 <= px < self._cols:
176
+ self._buffer[y][px] = ch
177
+ self._attrs[y][px] = attr
178
+
179
+ def addch(self, y, x, ch, attr=0):
180
+ if y < 0 or y >= self._rows or x < 0 or x >= self._cols:
181
+ return
182
+ try:
183
+ c = chr(ch) if isinstance(ch, int) else str(ch)
184
+ except Exception:
185
+ return
186
+ if not c:
187
+ return
188
+ self._buffer[y][x] = c[0]
189
+ self._attrs[y][x] = attr
190
+
191
+ def refresh(self):
192
+ out_lines = []
193
+ for y in range(self._rows):
194
+ line = []
195
+ prev_attr = None
196
+ for x in range(self._cols):
197
+ attr = self._attrs[y][x]
198
+ if attr != prev_attr:
199
+ color, bold, dim, reverse = _decode_attr(attr)
200
+ seq = "\x1b[0m"
201
+ if bold:
202
+ seq += "\x1b[1m"
203
+ if dim:
204
+ seq += "\x1b[2m"
205
+ if reverse:
206
+ seq += "\x1b[7m"
207
+ if attr != 0 and color in _ANSI_COLORS:
208
+ seq += f"\x1b[{_ANSI_COLORS[color]}m"
209
+ line.append(seq)
210
+ prev_attr = attr
211
+ line.append(self._buffer[y][x])
212
+ line.append("\x1b[0m")
213
+ out_lines.append("".join(line))
214
+ sys.stdout.write("\x1b[H" + "\n".join(out_lines))
215
+ sys.stdout.flush()
216
+
217
+ def getch(self):
218
+ if self._use_msvcrt:
219
+ return _getch_msvcrt(self._timeout)
220
+ try:
221
+ key = self._queue.get(timeout=self._timeout)
222
+ except Exception:
223
+ return -1
224
+ return _map_keypress(key)
225
+
226
+
227
+ def _map_keypress(keypress):
228
+ key = keypress.key
229
+ if key == Keys.Left:
230
+ return KEY_LEFT
231
+ if key == Keys.Right:
232
+ return KEY_RIGHT
233
+ if key == Keys.Up:
234
+ return KEY_UP
235
+ if key == Keys.Down:
236
+ return KEY_DOWN
237
+ if key == Keys.PageUp:
238
+ return KEY_PPAGE
239
+ if key == Keys.PageDown:
240
+ return KEY_NPAGE
241
+ if key in (Keys.Backspace, Keys.ControlH):
242
+ return KEY_BACKSPACE
243
+ if key in (Keys.Enter, Keys.ControlM):
244
+ return 10
245
+ if isinstance(key, str) and len(key) == 1:
246
+ return ord(key)
247
+ return -1
248
+
249
+
250
+ def _getch_msvcrt(timeout):
251
+ try:
252
+ import msvcrt
253
+ except Exception:
254
+ time.sleep(timeout)
255
+ return -1
256
+ end = time.time() + timeout
257
+ while True:
258
+ if msvcrt.kbhit():
259
+ ch = msvcrt.getwch()
260
+ if ch in ("\x00", "\xe0"):
261
+ ch2 = msvcrt.getwch()
262
+ return {
263
+ "K": KEY_LEFT,
264
+ "M": KEY_RIGHT,
265
+ "H": KEY_UP,
266
+ "P": KEY_DOWN,
267
+ "I": KEY_PPAGE,
268
+ "Q": KEY_NPAGE,
269
+ }.get(ch2, -1)
270
+ if ch == "\r":
271
+ return 10
272
+ if ch == "\x08":
273
+ return KEY_BACKSPACE
274
+ return ord(ch)
275
+ if timeout <= 0:
276
+ return -1
277
+ if time.time() >= end:
278
+ return -1
279
+ time.sleep(0.01)
280
+
281
+
282
+ def _enable_vt_mode():
283
+ if os.name != "nt":
284
+ return
285
+ try:
286
+ import ctypes
287
+ kernel32 = ctypes.windll.kernel32
288
+ handle = kernel32.GetStdHandle(-11)
289
+ mode = ctypes.c_uint()
290
+ if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
291
+ kernel32.SetConsoleMode(handle, mode.value | 0x0004)
292
+ except Exception:
293
+ pass
294
+
295
+
296
+ def exit_alternate_screen():
297
+ """Exit the alternate screen buffer and restore the cursor/attributes.
298
+
299
+ Safe to call from anywhere; used when printing messages that must
300
+ appear in the main terminal/scrollback.
301
+ """
302
+ try:
303
+ sys.stdout.write("\x1b[0m\x1b[?25h\x1b[?1049l")
304
+ sys.stdout.flush()
305
+ except Exception:
306
+ pass
307
+
308
+
309
+ def wrapper(func):
310
+ stdscr = _Screen()
311
+ try:
312
+ return func(stdscr)
313
+ finally:
314
+ stdscr.stop()
315
+ try:
316
+ sys.stdout.write("\x1b[0m\x1b[2J\x1b[3J\x1b[H\x1b[?25h\x1b[?1049l")
317
+ sys.stdout.flush()
318
+ except Exception:
319
+ 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