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.
- cli.py +110 -67
- {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/METADATA +41 -10
- cli_arcade-2026.2.1.dist-info/RECORD +50 -0
- game_classes/__pycache__/__init__.cpython-312.pyc +0 -0
- game_classes/__pycache__/game_base.cpython-312.pyc +0 -0
- game_classes/__pycache__/game_base.cpython-313.pyc +0 -0
- game_classes/__pycache__/highscores.cpython-312.pyc +0 -0
- game_classes/__pycache__/menu.cpython-312.pyc +0 -0
- game_classes/__pycache__/menu.cpython-313.pyc +0 -0
- game_classes/__pycache__/ptk.cpython-312.pyc +0 -0
- game_classes/__pycache__/ptk.cpython-313.pyc +0 -0
- game_classes/__pycache__/ptk_curses.cpython-313.pyc +0 -0
- game_classes/__pycache__/ptk_game_base.cpython-313.pyc +0 -0
- game_classes/__pycache__/ptk_menu.cpython-313.pyc +0 -0
- game_classes/__pycache__/ptk_tools.cpython-313.pyc +0 -0
- game_classes/__pycache__/tools.cpython-312.pyc +0 -0
- game_classes/__pycache__/tools.cpython-313.pyc +0 -0
- game_classes/game_base.py +5 -5
- game_classes/menu.py +4 -4
- game_classes/ptk.py +435 -0
- game_classes/tools.py +27 -25
- games/byte_bouncer/__pycache__/game.cpython-312.pyc +0 -0
- games/byte_bouncer/__pycache__/game.cpython-313.pyc +0 -0
- games/byte_bouncer/game.py +45 -21
- games/star_ship/__pycache__/game.cpython-312.pyc +0 -0
- games/star_ship/__pycache__/game.cpython-313.pyc +0 -0
- games/star_ship/game.py +64 -24
- games/terminal_tumble/__pycache__/game.cpython-312.pyc +0 -0
- games/terminal_tumble/__pycache__/game.cpython-313.pyc +0 -0
- games/terminal_tumble/game.py +41 -38
- cli_arcade-2026.1.3.dist-info/RECORD +0 -35
- {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/WHEEL +0 -0
- {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/entry_points.txt +0 -0
- {cli_arcade-2026.1.3.dist-info → cli_arcade-2026.2.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
47
|
+
if ptk.can_change_color() and ptk.COLORS >= 8:
|
|
46
48
|
try:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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(' ',
|
|
63
|
+
stdscr.bkgd(' ', ptk.color_pair(ptk.COLOR_WHITE))
|
|
62
64
|
except Exception:
|
|
63
65
|
try:
|
|
64
|
-
stdscr.bkgd(' ',
|
|
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(
|
|
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
|
|
Binary file
|
|
Binary file
|
games/byte_bouncer/game.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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,
|
|
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,
|
|
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}',
|
|
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}',
|
|
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"]):,}',
|
|
58
|
-
self.stdscr.addstr(info_y + 6, info_x, f'Level: {int(self.scores["level"]):,}',
|
|
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 =
|
|
79
|
+
attr = ptk.color_pair(ptk.COLOR_MAGENTA) | ptk.A_BOLD
|
|
75
80
|
else:
|
|
76
|
-
attr =
|
|
77
|
-
self.stdscr.addch(int(b['y']),
|
|
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
|
|
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 -
|
|
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 -
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
ptk.wrapper(main)
|
|
204
228
|
except KeyboardInterrupt:
|
|
205
229
|
try:
|
|
206
|
-
|
|
230
|
+
ptk.endwin()
|
|
207
231
|
except Exception:
|
|
208
232
|
pass
|
|
Binary file
|
|
Binary file
|