cli-arcade 2026.0.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.
Files changed (35) hide show
  1. cli.py +468 -0
  2. cli_arcade-2026.0.0.dist-info/METADATA +136 -0
  3. cli_arcade-2026.0.0.dist-info/RECORD +35 -0
  4. cli_arcade-2026.0.0.dist-info/WHEEL +5 -0
  5. cli_arcade-2026.0.0.dist-info/entry_points.txt +4 -0
  6. cli_arcade-2026.0.0.dist-info/licenses/LICENSE +9 -0
  7. cli_arcade-2026.0.0.dist-info/top_level.txt +3 -0
  8. game_classes/__init__.py +1 -0
  9. game_classes/__pycache__/__init__.cpython-313.pyc +0 -0
  10. game_classes/__pycache__/game_base.cpython-313.pyc +0 -0
  11. game_classes/__pycache__/highscores.cpython-313.pyc +0 -0
  12. game_classes/__pycache__/menu.cpython-313.pyc +0 -0
  13. game_classes/__pycache__/tools.cpython-313.pyc +0 -0
  14. game_classes/game_base.py +121 -0
  15. game_classes/highscores.py +108 -0
  16. game_classes/menu.py +68 -0
  17. game_classes/tools.py +155 -0
  18. games/__init__.py +1 -0
  19. games/byte_bouncer/__init__.py +1 -0
  20. games/byte_bouncer/__pycache__/byte_bouncer.cpython-313.pyc +0 -0
  21. games/byte_bouncer/__pycache__/game.cpython-313.pyc +0 -0
  22. games/byte_bouncer/__pycache__/highscores.cpython-313.pyc +0 -0
  23. games/byte_bouncer/game.py +208 -0
  24. games/star_ship/__init__.py +1 -0
  25. games/star_ship/__pycache__/game.cpython-313.pyc +0 -0
  26. games/star_ship/__pycache__/highscores.cpython-313.pyc +0 -0
  27. games/star_ship/__pycache__/nibbles.cpython-313.pyc +0 -0
  28. games/star_ship/__pycache__/snek.cpython-313.pyc +0 -0
  29. games/star_ship/__pycache__/star_ship.cpython-313.pyc +0 -0
  30. games/star_ship/game.py +243 -0
  31. games/terminal_tumble/__init__.py +1 -0
  32. games/terminal_tumble/__pycache__/game.cpython-313.pyc +0 -0
  33. games/terminal_tumble/__pycache__/highscores.cpython-313.pyc +0 -0
  34. games/terminal_tumble/__pycache__/terminal_tumble.cpython-313.pyc +0 -0
  35. games/terminal_tumble/game.py +380 -0
cli.py ADDED
@@ -0,0 +1,468 @@
1
+ import curses
2
+ import os
3
+ import importlib.util
4
+ import argparse
5
+ import glob
6
+ import sys
7
+
8
+ # helper: recognize Enter from multiple terminals/keypads
9
+ def is_enter_key(ch):
10
+ try:
11
+ enter_vals = {10, 13, getattr(curses, 'KEY_ENTER', -1), 343, 459}
12
+ except Exception:
13
+ enter_vals = {10, 13}
14
+ return ch in enter_vals
15
+
16
+ TITLE = [
17
+ ' ________ ____ _________ __ ______________ ',
18
+ r' / ____/ / / _/ / ____/ | / |/ / ____/ ___/ ',
19
+ r' / / / / / / / / __/ /| | / /|_/ / __/ \__ \ ',
20
+ r' / /___/ /____/ / / /_/ / ___ |/ / / / /___ ___/ / ',
21
+ r' \____/_____/___/ \____/_/ |_/_/ /_/_____//____/ '
22
+ ]
23
+
24
+ def _discover_games():
25
+ base = os.path.dirname(__file__)
26
+ games = []
27
+ # only scan the 'games' subdirectory
28
+ games_dir = os.path.join(base, 'games')
29
+ if not os.path.isdir(games_dir):
30
+ return []
31
+ scan_roots = [games_dir]
32
+
33
+ for root in scan_roots:
34
+ for entry in sorted(os.listdir(root)):
35
+ dirpath = os.path.join(root, entry)
36
+ if not os.path.isdir(dirpath):
37
+ continue
38
+ if entry.startswith('__'):
39
+ continue
40
+ # prefer standardized <dir>/game.py
41
+ candidate = os.path.join(dirpath, "game.py")
42
+ file_to_check = None
43
+ if os.path.exists(candidate):
44
+ file_to_check = candidate
45
+ else:
46
+ pyfiles = [p for p in os.listdir(dirpath) if p.endswith('.py') and not p.startswith('_')]
47
+ if pyfiles:
48
+ file_to_check = os.path.join(dirpath, pyfiles[0])
49
+ if not file_to_check:
50
+ continue
51
+ # use directory name as the display name
52
+ name = entry.replace('_', ' ').title()
53
+ rel = os.path.relpath(file_to_check, base).replace('\\', '/')
54
+ games.append((name, rel))
55
+ return games
56
+
57
+
58
+ GAMES = _discover_games()
59
+
60
+
61
+ def _read_console_aliases():
62
+ """Read console_scripts names from installed package metadata."""
63
+ try:
64
+ from importlib import metadata as importlib_metadata
65
+ except Exception:
66
+ return []
67
+
68
+ try:
69
+ dist = importlib_metadata.distribution('cli-games')
70
+ aliases = [ep.name for ep in dist.entry_points if ep.group == 'console_scripts']
71
+ return sorted(set(aliases))
72
+ except Exception:
73
+ return []
74
+
75
+
76
+ def _menu(stdscr):
77
+ curses.curs_set(0)
78
+ stdscr.nodelay(False)
79
+ # ensure a cyan color pair is available for the title
80
+ if curses.has_colors():
81
+ try:
82
+ # init colors
83
+ curses.start_color()
84
+ curses.use_default_colors()
85
+ # try to normalize key colors (0..1000 scale). Must run before init_pair.
86
+ if curses.can_change_color() and curses.COLORS >= 8:
87
+ try:
88
+ curses.init_color(curses.COLOR_MAGENTA, 1000, 0, 1000)
89
+ curses.init_color(curses.COLOR_YELLOW, 1000, 1000, 0)
90
+ curses.init_color(curses.COLOR_WHITE, 1000, 1000, 1000)
91
+ curses.init_color(curses.COLOR_CYAN, 0, 1000, 1000)
92
+ curses.init_color(curses.COLOR_BLUE, 0, 0, 1000)
93
+ curses.init_color(curses.COLOR_GREEN, 0, 800, 0)
94
+ curses.init_color(curses.COLOR_RED, 1000, 0, 0)
95
+ curses.init_color(curses.COLOR_BLACK, 0, 0, 0)
96
+ except Exception:
97
+ pass
98
+ for i in range(1,8):
99
+ curses.init_pair(i, i, -1)
100
+ except Exception:
101
+ pass
102
+ sel = 0
103
+ top = 0
104
+ while True:
105
+ stdscr.clear()
106
+ h, w = stdscr.getmaxyx()
107
+ title_h = len(TITLE)
108
+ title_start = 0
109
+ colors = [curses.COLOR_MAGENTA, curses.COLOR_MAGENTA, curses.COLOR_CYAN, curses.COLOR_CYAN, curses.COLOR_GREEN, curses.COLOR_GREEN]
110
+ for i, line in enumerate(TITLE):
111
+ try:
112
+ stdscr.addstr(title_start + i, 0, line, curses.color_pair(colors[i]))
113
+ except Exception:
114
+ pass
115
+ stdscr.addstr(title_h + 1, 2, "Use Up/Down, PageUp/PageDown, Enter to start, ESC to quit", curses.color_pair(curses.COLOR_WHITE))
116
+ start_y = title_h + 3
117
+ # number of lines available for the game list
118
+ avail = max(1, h - start_y - 2)
119
+ total = len(GAMES)
120
+ # clamp top so it stays within valid range
121
+ if top < 0:
122
+ top = 0
123
+ if top > max(0, total - avail):
124
+ top = max(0, total - avail)
125
+
126
+ for vis_i in range(min(avail, total)):
127
+ i = top + vis_i
128
+ name = GAMES[i][0]
129
+ attr = curses.A_REVERSE if i == sel else curses.A_NORMAL
130
+ try:
131
+ stdscr.addstr(start_y + vis_i, 2, name[:w-4], curses.color_pair(curses.COLOR_CYAN) | attr)
132
+ except Exception:
133
+ pass
134
+ # optional scrollbar indicator when list is long
135
+ if total > avail:
136
+ try:
137
+ # try to use glyphs from game_classes.tools when available
138
+ try:
139
+ from game_classes.tools import glyph
140
+ vbar = glyph('VBAR')
141
+ block = glyph('THUMB')
142
+ except Exception:
143
+ vbar = '|'
144
+ block = '#'
145
+ bar_y = start_y
146
+ bar_h = avail
147
+ thumb_h = max(1, int(bar_h * (avail / total)))
148
+ thumb_pos = int((bar_h - thumb_h) * (top / max(1, total - avail)))
149
+ for by in range(bar_h):
150
+ stdscr.addstr(bar_y + by, w - 2, vbar)
151
+ for by in range(thumb_h):
152
+ stdscr.addstr(bar_y + thumb_pos + by, w - 2, block)
153
+ except Exception:
154
+ pass
155
+ stdscr.refresh()
156
+
157
+ ch = stdscr.getch()
158
+ if ch == curses.KEY_UP:
159
+ sel = max(0, sel - 1)
160
+ elif ch == curses.KEY_DOWN:
161
+ sel = min(len(GAMES) - 1, sel + 1)
162
+ elif ch == curses.KEY_PPAGE: # Page Up
163
+ sel = max(0, sel - avail)
164
+ elif ch == curses.KEY_NPAGE: # Page Down
165
+ sel = min(len(GAMES) - 1, sel + avail)
166
+ elif is_enter_key(ch):
167
+ return sel
168
+ elif ch == 27:
169
+ return None
170
+ # adjust top to keep the selected item visible
171
+ if sel < top:
172
+ top = sel
173
+ elif sel >= top + avail:
174
+ top = sel - avail + 1
175
+
176
+
177
+ def _run_game_by_index(choice):
178
+ """Load and run the game given by numeric index in GAMES."""
179
+ name, relpath = GAMES[choice]
180
+ base = os.path.dirname(__file__)
181
+ path = os.path.join(base, relpath)
182
+ if not os.path.exists(path):
183
+ print(f" [INFO] Game file not found: {path}")
184
+ return
185
+ game_dir = os.path.dirname(path)
186
+ spec = importlib.util.spec_from_file_location(f"cli_game_{choice}", path)
187
+ mod = importlib.util.module_from_spec(spec)
188
+ inserted = []
189
+ try:
190
+ # ensure both the game's directory and project root are on sys.path
191
+ proj_root = os.path.dirname(__file__)
192
+ if game_dir and game_dir not in sys.path:
193
+ sys.path.insert(0, game_dir)
194
+ inserted.append(game_dir)
195
+ if proj_root and proj_root not in sys.path:
196
+ sys.path.insert(0, proj_root)
197
+ inserted.append(proj_root)
198
+ spec.loader.exec_module(mod)
199
+ except Exception as e:
200
+ print(f" [ERROR] Failed to load game {name}: {e}")
201
+ return
202
+ finally:
203
+ for p in inserted:
204
+ try:
205
+ sys.path.remove(p)
206
+ except Exception:
207
+ pass
208
+ if hasattr(mod, 'main'):
209
+ try:
210
+ curses.wrapper(mod.main)
211
+ except Exception as e:
212
+ print(f" [ERROR] Error running game {name}: {e}")
213
+ else:
214
+ print(f" [INFO] Game {name} has no main(stdscr) entry point.")
215
+
216
+
217
+ def _reset_game_by_index(choice, yes=False):
218
+ name, relpath = GAMES[choice]
219
+ base = os.path.dirname(__file__)
220
+ game_dir = os.path.dirname(os.path.join(base, relpath))
221
+ # find highscores files (common pattern in project) and user-data highscores
222
+ files = glob.glob(os.path.join(game_dir, 'highscores*.json'))
223
+ # try user-data location via HighScores
224
+ try:
225
+ from game_classes.highscores import HighScores
226
+ slug = os.path.basename(game_dir)
227
+ hs = HighScores(slug)
228
+ user_path = hs._path()
229
+ if user_path and os.path.exists(user_path):
230
+ files.append(user_path)
231
+ except Exception:
232
+ # if import fails or path not available, ignore
233
+ pass
234
+ if not files:
235
+ print(f" [INFO] No highscore files found for '{name}' ({game_dir}).")
236
+ return
237
+ # dedupe and present
238
+ files = sorted(set(files))
239
+ print(f" [INFO] Found {len(files)} highscore file(s) for '{name}':")
240
+ for f in files:
241
+ print(f' [{choice}] {f}')
242
+ if not yes:
243
+ ans = input(f" [ACTION] Delete these files for '{name}'? [y/N]: ")
244
+ if not ans.lower().startswith('y'):
245
+ print(' [CANCELED]')
246
+ return
247
+ for f in files:
248
+ try:
249
+ os.remove(f)
250
+ print(f" [DELETED] {f}")
251
+ except Exception as e:
252
+ print(f" [ERROR] Failed to delete {f}: {e}")
253
+
254
+
255
+ def _reset_all_games(yes=False):
256
+ base = os.path.dirname(__file__)
257
+ all_files = []
258
+ for name, rel in GAMES:
259
+ game_dir = os.path.dirname(os.path.join(base, rel))
260
+ all_files.extend(glob.glob(os.path.join(game_dir, 'highscores*.json')))
261
+ # include user-data highscores when present
262
+ try:
263
+ from game_classes.highscores import HighScores
264
+ slug = os.path.basename(game_dir)
265
+ hs = HighScores(slug)
266
+ user_path = hs._path()
267
+ if user_path and os.path.exists(user_path):
268
+ all_files.append(user_path)
269
+ except Exception:
270
+ pass
271
+ if not all_files:
272
+ print(' [INFO] No highscore files found for any game.')
273
+ return
274
+ # dedupe list before showing
275
+ all_files = sorted(set(all_files))
276
+ print(f' [INFO] Found {len(all_files)} highscore file(s):')
277
+ for i, f in enumerate(all_files):
278
+ print(f' [{i}] {f}')
279
+ if not yes:
280
+ ans = input(' [ACTION] Delete all these highscore files? [y/N]: ')
281
+ if not ans.lower().startswith('y'):
282
+ print(' [CANCELED]')
283
+ return
284
+ for f in all_files:
285
+ try:
286
+ os.remove(f)
287
+ print(f" [DELETED] {f}")
288
+ except Exception as e:
289
+ print(f" [ERROR] Failed to delete {f}: {e}")
290
+
291
+ # CLI version: read from setup.cfg to keep a single source of truth
292
+ def _read_version_from_setupcfg():
293
+ try:
294
+ from importlib import metadata as importlib_metadata
295
+ except Exception:
296
+ return '0.0.0'
297
+
298
+ try:
299
+ return importlib_metadata.version('cli-games')
300
+ except Exception:
301
+ return '0.0.0'
302
+
303
+ def main():
304
+ # support simple CLI subcommands (e.g. `clig list`)
305
+ # build epilog with examples and any console script aliases from setup.cfg
306
+ epilog_lines = [
307
+ 'Examples:',
308
+ f' %(prog)s',
309
+ f' %(prog)s list [-h]',
310
+ f' %(prog)s run [-h] [0, "Byte Bouncer"]',
311
+ f' %(prog)s reset [-h] [0, "Byte Bouncer"] [-y]',
312
+ ]
313
+ aliases = _read_console_aliases()
314
+ if aliases:
315
+ epilog_lines.append('\nAliases: ' + ', '.join(aliases))
316
+ epilog = '\n'.join(epilog_lines) + '\n'
317
+
318
+ parser = argparse.ArgumentParser(
319
+ prog=os.path.basename(sys.argv[0]) or 'games',
320
+ description='Run the CLI Games menu or subcommands.',
321
+ epilog=epilog,
322
+ formatter_class=argparse.RawDescriptionHelpFormatter,
323
+ )
324
+ # standard --version support
325
+ parser.add_argument('-v', '--version', action='version', version=f"%(prog)s {_read_version_from_setupcfg()}")
326
+ sub = parser.add_subparsers(dest='cmd')
327
+ sub.add_parser(
328
+ 'list',
329
+ help='List available games',
330
+ description='List all available games with their zero-based indices.',
331
+ epilog='Example:\n %(prog)s\n',
332
+ formatter_class=argparse.RawDescriptionHelpFormatter,
333
+ )
334
+ runp = sub.add_parser(
335
+ 'run',
336
+ help='Run a game by name or zero-based index',
337
+ description='Run a game directly without the menu.',
338
+ epilog='Examples:\n %(prog)s 0\n %(prog)s "Byte Bouncer"\n',
339
+ formatter_class=argparse.RawDescriptionHelpFormatter,
340
+ )
341
+ runp.add_argument('game', help='Game name or zero-based index')
342
+ resetp = sub.add_parser(
343
+ 'reset',
344
+ help='Reset highscores (delete highscore files)',
345
+ description='Delete highscores for one game or all games. Use with care.',
346
+ epilog='Examples:\n %(prog)s\n %(prog)s -y\n %(prog)s 0\n %(prog)s "Byte Bouncer" -y\n',
347
+ formatter_class=argparse.RawDescriptionHelpFormatter,
348
+ )
349
+ resetp.add_argument('game', nargs='?', help='Optional game name or zero-based index (omit to reset all)')
350
+ resetp.add_argument('-y', '--yes', action='store_true', help='Do not prompt; proceed with deletion')
351
+ args, _rest = parser.parse_known_args()
352
+
353
+ if args.cmd == 'list':
354
+ base = os.path.dirname(__file__)
355
+ for i, (name, rel) in enumerate(GAMES):
356
+ print(f" [{i}] {name}")
357
+ return
358
+
359
+ if args.cmd == 'run':
360
+ token = args.game
361
+ choice = None
362
+ # try integer (zero-based index)
363
+ try:
364
+ idx = int(token)
365
+ if 0 <= idx < len(GAMES):
366
+ choice = idx
367
+ else:
368
+ print(f" [INFO] Index out of range: {idx}")
369
+ for i, (name, rel) in enumerate(GAMES):
370
+ print(f" [{i}] {name}")
371
+ return
372
+ except Exception:
373
+ # match by exact name (case-insensitive)
374
+ lowered = token.lower()
375
+ for i, (name, _) in enumerate(GAMES):
376
+ if name.lower() == lowered:
377
+ choice = i
378
+ break
379
+ # fallback: substring match
380
+ if choice is None:
381
+ for i, (name, _) in enumerate(GAMES):
382
+ if lowered in name.lower():
383
+ choice = i
384
+ break
385
+ if choice is None:
386
+ print(f" [INFO] Game not found: {token}")
387
+ for i, (name, rel) in enumerate(GAMES):
388
+ print(f" [{i}] {name}")
389
+ return
390
+ # run the selected game (skip menu)
391
+ try:
392
+ _run_game_by_index(choice)
393
+ except Exception as e:
394
+ print(f" [ERROR] Error running game: {e}")
395
+ return
396
+
397
+ if args.cmd == 'reset':
398
+ token = args.game
399
+ yes = getattr(args, 'yes', False)
400
+ if token is None:
401
+ _reset_all_games(yes=yes)
402
+ return
403
+ # resolve token to index similar to 'run'
404
+ choice = None
405
+ try:
406
+ idx = int(token)
407
+ if 0 <= idx < len(GAMES):
408
+ choice = idx
409
+ else:
410
+ print(f" [INFO] Index out of range: {idx}")
411
+ return
412
+ except Exception:
413
+ lowered = token.lower()
414
+ for i, (name, _) in enumerate(GAMES):
415
+ if name.lower() == lowered:
416
+ choice = i
417
+ break
418
+ if choice is None:
419
+ print(f" [INFO] Game not found: {token}")
420
+ return
421
+ _reset_game_by_index(choice, yes=yes)
422
+ return
423
+
424
+ # run the menu under curses, then launch the chosen game's main()
425
+ choice = curses.wrapper(_menu)
426
+ if choice is None:
427
+ return
428
+ name, relpath = GAMES[choice]
429
+ base = os.path.dirname(__file__)
430
+ path = os.path.join(base, relpath)
431
+ if not os.path.exists(path):
432
+ print(f" [INFO] Game file not found: {path}")
433
+ return
434
+ # Ensure the game's directory is on sys.path so local imports (like `highscores`) resolve
435
+ game_dir = os.path.dirname(path)
436
+ spec = importlib.util.spec_from_file_location(f"cli_game_{choice}", path)
437
+ mod = importlib.util.module_from_spec(spec)
438
+ inserted = []
439
+ try:
440
+ proj_root = os.path.dirname(__file__)
441
+ if game_dir and game_dir not in sys.path:
442
+ sys.path.insert(0, game_dir)
443
+ inserted.append(game_dir)
444
+ if proj_root and proj_root not in sys.path:
445
+ sys.path.insert(0, proj_root)
446
+ inserted.append(proj_root)
447
+ spec.loader.exec_module(mod)
448
+ except Exception as e:
449
+ print(f" [INFO] Failed to load game {name}: {e}")
450
+ return
451
+ finally:
452
+ for p in inserted:
453
+ try:
454
+ sys.path.remove(p)
455
+ except Exception:
456
+ pass
457
+ # call the game's main function if present
458
+ if hasattr(mod, 'main'):
459
+ try:
460
+ curses.wrapper(mod.main)
461
+ except Exception as e:
462
+ print(f" [ERROR] Error running game {name}: {e}")
463
+ else:
464
+ print(f" [INFO] Game {name} has no main(stdscr) entry point.")
465
+
466
+
467
+ if __name__ == '__main__':
468
+ main()
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-arcade
3
+ Version: 2026.0.0
4
+ Summary: Collection of terminal CLI games
5
+ Home-page: https://github.com/Bro-Code-Technologies/cli_games/tree/main/windows
6
+ Author: Bro Code Technologies LLC
7
+ Author-email: info@brocodetechnologies.com
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/Bro-Code-Technologies/cli_games/tree/main/windows
10
+ Project-URL: Issues, https://github.com/Bro-Code-Technologies/cli_games/tree/main/windows
11
+ Project-URL: Documentation, https://github.com/Bro-Code-Technologies/cli_games/tree/main/windows
12
+ Project-URL: Package, https://pypi.org/project/cli-games/
13
+ Keywords: cli,terminal,games,curses
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Topic :: Games/Entertainment
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: appdirs
31
+ Requires-Dist: windows-curses; sys_platform == "win32"
32
+ Dynamic: license-file
33
+
34
+ # CLI Games
35
+
36
+ Collection of small terminal games bundled with a single CLI launcher.
37
+
38
+ Requirements
39
+ - Python 3.8+
40
+ - On Windows: install `windows-curses` for curses support:
41
+
42
+ ```powershell
43
+ pip install windows-curses
44
+ ```
45
+
46
+ Quick start (developer)
47
+
48
+ ```powershell
49
+ # install editable (recommended during development)
50
+ pip install -e .
51
+
52
+ # list installed console aliases and available games
53
+ clig list
54
+
55
+ # run interactive menu
56
+ clig
57
+
58
+ # run a game by zero-based index or name
59
+ clig run 0
60
+ clig run "Byte Bouncer"
61
+
62
+ # reset highscores for one game or all
63
+ clig reset 0
64
+ clig reset "Byte Bouncer"
65
+ clig reset -y # skip confirmation
66
+ ```
67
+
68
+ If the `clig` command is not found after installation, the installer likely wrote scripts to your user Scripts directory. On Windows add this to your PATH (PowerShell):
69
+
70
+ ```powershell
71
+ $env:Path += ";$env:APPDATA\Python\Python<version>\Scripts"
72
+ # or permanently via System settings: add %APPDATA%\Python\Python<version>\Scripts to your user PATH
73
+ ```
74
+
75
+ You can always run the CLI directly without installing:
76
+
77
+ ```powershell
78
+ python -m cli
79
+ ```
80
+
81
+ Commands
82
+ - `clig` — interactive curses menu
83
+ - `clig list` — print available games and zero-based indices
84
+ - `clig run <index|name>` — run a game directly (index is zero-based)
85
+ - `clig reset [<index|name>] [-y]` — delete highscores for a game or all games
86
+ - Aliases available: `cli-games`, `cli-game`
87
+
88
+ Highscores storage and migration
89
+ - Highscores are now stored in a user-writable application data directory. Typical locations:
90
+ - Windows (appdirs): `%LOCALAPPDATA%\cli-games\games\<game>\highscores.json`
91
+ - Fallback (no appdirs): `%USERPROFILE%\.cli-games\games\<game>\highscores.json`
92
+ - On first run the CLI attempts to migrate any legacy `games/<game>/highscores.json` found in the project into the user data directory.
93
+
94
+ Packaging & publishing (brief)
95
+
96
+ - `setup.cfg` now declares `packages = find:` and `include_package_data = true` so `game_classes/` and `games/` are included in sdist/wheels. Remember to add a `MANIFEST.in` if you need additional files in source distributions.
97
+ - Update `setup.cfg` version.
98
+ - Build: `python -m build` (requires `build` package).
99
+ - Upload: `twine upload dist/*` (requires `twine`).
100
+ - The package exposes several console script aliases (see `setup.cfg` -> `options.entry_points.console_scripts`).
101
+
102
+ Notes & Troubleshooting
103
+ - On Windows, `curses` requires `windows-curses`.
104
+ - The CLI requires a minimum terminal size; if the menu exits with an error, try enlarging your terminal or run `python -m cli` in a larger window.
105
+ - Games should live in their own subdirectory (`games/<slug>/game.py`) and export a `main(stdscr)` entry point. The CLI uses the directory name (slug) as the display title.
106
+
107
+ Terminal recommendations (Windows)
108
+ - Recommended: use Windows Terminal or the VS Code integrated terminal for the best UTF-8 + glyph support.
109
+ - Install Windows Terminal via Microsoft Store or `winget`:
110
+
111
+ ```powershell
112
+ winget install --id Microsoft.WindowsTerminal -e
113
+ ```
114
+
115
+ - PowerShell (external) often uses OEM code page 437 and may not render some Unicode glyphs. If you prefer the external shell to render glyphs, either use Windows Terminal or make these changes:
116
+ - Change the console font to a glyph-capable font (Cascadia Code PL, Fira Code, DejaVu Sans Mono, or modern Consolas) via the console Properties → Font.
117
+ - Ensure UTF-8 is enabled for the session (temporary):
118
+
119
+ ```powershell
120
+ chcp 65001
121
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
122
+ $env:PYTHONIOENCODING = 'utf-8'
123
+ ```
124
+
125
+ - To make the change persistent, add the above lines to your PowerShell profile (see `code $profile`), or prefer PowerShell Core (`pwsh`) which typically handles UTF-8 better.
126
+
127
+ - If you cannot enable UTF-8 in your external terminal, the CLI will automatically fall back to safe ASCII glyphs for problematic terminals. To force ASCII output regardless of terminal detection, set the environment variable `CLI_GAMES_FORCE_ASCII=1` before running the CLI.
128
+
129
+ - Advanced: enable system-wide UTF-8 (Region → Administrative → Change system locale → check “Beta: Use Unicode UTF-8 for worldwide language support”) and restart. This affects other apps and requires caution.
130
+
131
+ Contributing
132
+ - Add a new game by creating a subdirectory under `games/` with a `game.py` file that exports `main(stdscr)`.
133
+ - Keep changes minimal and run `clig` locally to verify.
134
+
135
+ License
136
+ - MIT
@@ -0,0 +1,35 @@
1
+ cli.py,sha256=Kc8jgyneJYYjLedgvniskd2_0mXyBg1R6-TLslJgcgE,17458
2
+ cli_arcade-2026.0.0.dist-info/licenses/LICENSE,sha256=1PLSNFyGPi9COkVEeNYcUTuRLqGxzMLLTah05tByD4s,1099
3
+ game_classes/__init__.py,sha256=A0o4vBUktOiONToIJb0hACalszBnFS9ls9WXPn3-KH0,28
4
+ game_classes/game_base.py,sha256=q779LzYsK8rO0rWrj2iB5407wuLiZvuFiRBx6qQEfPQ,3505
5
+ game_classes/highscores.py,sha256=uzK2NJU-hnmMcDT7TEWhkikKn4rnqiJMln8szOqqPX4,4060
6
+ game_classes/menu.py,sha256=6frg15Ibx_H89g-KmHv53d6uWSUsxluhXMGvn4Nj7xY,2795
7
+ game_classes/tools.py,sha256=TKkJ0bv2sbs_smjrXLCJZd9IIcZe_f96ms0uSBYA6gc,4972
8
+ game_classes/__pycache__/__init__.cpython-313.pyc,sha256=zSl9ESMUQn5Vo_XXAA8DD7lpFHOtOwBruGvCOJ5qeuo,192
9
+ game_classes/__pycache__/game_base.cpython-313.pyc,sha256=p6NUAgjNMXwPoNkQq01tUZm_ZYX18jaIMRZOm35vKsk,6962
10
+ game_classes/__pycache__/highscores.cpython-313.pyc,sha256=diYyL8QT5LpS_68hz5s_kfHbgaqihyOygTABlkzg2MI,7067
11
+ game_classes/__pycache__/menu.cpython-313.pyc,sha256=wseCBPJ5XKtVI8scyJrBZxAW2avD2JF3J0IpRUYzsR8,4459
12
+ game_classes/__pycache__/tools.cpython-313.pyc,sha256=OrKBCCTPdimXHQZ7hXRCkB_0IcCQiG9POLdnnKgKjl8,7331
13
+ games/__init__.py,sha256=Rg46lnLj_sezpnXaQTS1NXX5OaRhR_EWpNTyYUxycj4,21
14
+ games/byte_bouncer/__init__.py,sha256=8hxIHRyUPB-X6vJXh1432ytKjOcfIzCbdpO5mmWA7iQ,33
15
+ games/byte_bouncer/game.py,sha256=9SFBre-QvjTa5maKP-XjI-Fh-1dNgxeYxxtglrccnA4,8105
16
+ games/byte_bouncer/__pycache__/byte_bouncer.cpython-313.pyc,sha256=JSTB6-_KCukAAVaZz9TpJ3V4gif_M3n_L5X1KOviSJ8,24605
17
+ games/byte_bouncer/__pycache__/game.cpython-313.pyc,sha256=Z5xiPsrisDqe8Mx6FQjW3eHNiXMXuIY5mJlrVmNUfec,13017
18
+ games/byte_bouncer/__pycache__/highscores.cpython-313.pyc,sha256=g3FozKpWGDVsTVETNpb6lRHcKA3ZIsQio8Kh7KCGVCI,3125
19
+ games/star_ship/__init__.py,sha256=X8AdhoccmNVu6yl7MalsxIsMxuEsddRdlvEQmhZMD0A,30
20
+ games/star_ship/game.py,sha256=Mph_USmrroynsli43vD3luadYAOiPXmbs0JMOCDGxMk,9545
21
+ games/star_ship/__pycache__/game.cpython-313.pyc,sha256=TnsJQS7zrwB0zG7CZ7VlTPE_qq063cZdZcL0Z66fu08,14623
22
+ games/star_ship/__pycache__/highscores.cpython-313.pyc,sha256=nHbEOTWeX0U9D47K2EP5S5uDPI0WW52Eg7h_NesG1OU,3148
23
+ games/star_ship/__pycache__/nibbles.cpython-313.pyc,sha256=Yv2P-GwEc30zA24aO7k0hkk1AOja6K85AFvmjLIffqQ,20510
24
+ games/star_ship/__pycache__/snek.cpython-313.pyc,sha256=3Mwyy6vjGFRmNyAWSqV5JRilLOxiCSndBmwua_Y7VTs,13313
25
+ games/star_ship/__pycache__/star_ship.cpython-313.pyc,sha256=9gJt10MTzVTSwsc4I16TDZTaAXclfhfGTzgfYgZNOKo,28021
26
+ games/terminal_tumble/__init__.py,sha256=axTd7hCgnLRNU8QS73NnQY3q5xjIb3ZYQORJ9Lz8l_E,36
27
+ games/terminal_tumble/game.py,sha256=p4lrFHBoHZJLpsUr0jXmR9FI8OTZmwfGIS1Z29MoYzU,15699
28
+ games/terminal_tumble/__pycache__/game.cpython-313.pyc,sha256=-xFF1wgupqiSoHz3QZwMNHxhAIV_UyIp0YifRdg8ijA,24104
29
+ games/terminal_tumble/__pycache__/highscores.cpython-313.pyc,sha256=UPnU9Zq88q2TZq1qXyo8e2Y_PdOgwvu5mncIkunFah8,3154
30
+ games/terminal_tumble/__pycache__/terminal_tumble.cpython-313.pyc,sha256=7oLpmmkYjpLYRC0bjRoEkHbIUNq7FZVJToGoja94m0o,38016
31
+ cli_arcade-2026.0.0.dist-info/METADATA,sha256=YdvaQUC9u0xS_teAd3dVSJTn0-hrxVNFE3gzh_erD2Y,5952
32
+ cli_arcade-2026.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
33
+ cli_arcade-2026.0.0.dist-info/entry_points.txt,sha256=dEd6b2AwMr2o-sSOzi2aAfIy0ViqcnAkuHShiOMVr4o,75
34
+ cli_arcade-2026.0.0.dist-info/top_level.txt,sha256=gTfYlz7gHYgPY0ugfJivukCOkNzmOisNR6VuJjAXPF4,23
35
+ cli_arcade-2026.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ cli-game = cli:main
3
+ cli-games = cli:main
4
+ clig = cli:main
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bro Code Technologies LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ cli
2
+ game_classes
3
+ games
@@ -0,0 +1 @@
1
+ """game_classes package"""