cli-arcade 2026.1.2__tar.gz → 2026.2.0__tar.gz
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_arcade-2026.1.2 → cli_arcade-2026.2.0}/MANIFEST.in +0 -1
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/PKG-INFO +38 -16
- cli_arcade-2026.2.0/PYPI_README.md +65 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/README.md +15 -21
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/cli.py +108 -143
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/cli_arcade.egg-info/SOURCES.txt +6 -1
- cli_arcade-2026.2.0/game_classes/__pycache__/game_base.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/__pycache__/menu.cpython-313.pyc +0 -0
- cli_arcade-2026.2.0/game_classes/__pycache__/ptk.cpython-313.pyc +0 -0
- cli_arcade-2026.2.0/game_classes/__pycache__/ptk_curses.cpython-313.pyc +0 -0
- cli_arcade-2026.2.0/game_classes/__pycache__/ptk_game_base.cpython-313.pyc +0 -0
- cli_arcade-2026.2.0/game_classes/__pycache__/ptk_menu.cpython-313.pyc +0 -0
- cli_arcade-2026.2.0/game_classes/__pycache__/ptk_tools.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/__pycache__/tools.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/game_base.py +5 -5
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/menu.py +4 -4
- cli_arcade-2026.2.0/game_classes/ptk.py +319 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/tools.py +27 -25
- cli_arcade-2026.2.0/games/byte_bouncer/__pycache__/game.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/byte_bouncer/game.py +45 -21
- cli_arcade-2026.2.0/games/star_ship/__pycache__/game.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/star_ship/game.py +64 -24
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/terminal_tumble/__pycache__/game.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/terminal_tumble/game.py +41 -38
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/setup.cfg +2 -3
- cli_arcade-2026.1.2/CHANGELOG.md +0 -18
- cli_arcade-2026.1.2/PYPI_README.md +0 -43
- cli_arcade-2026.1.2/game_classes/__pycache__/game_base.cpython-313.pyc +0 -0
- cli_arcade-2026.1.2/games/byte_bouncer/__pycache__/game.cpython-313.pyc +0 -0
- cli_arcade-2026.1.2/games/star_ship/__pycache__/game.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/LICENSE +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/__init__.py +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/__pycache__/__init__.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/__pycache__/highscores.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/game_classes/highscores.py +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/__init__.py +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/byte_bouncer/__init__.py +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/byte_bouncer/__pycache__/byte_bouncer.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/byte_bouncer/__pycache__/highscores.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/star_ship/__init__.py +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/star_ship/__pycache__/highscores.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/star_ship/__pycache__/nibbles.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/star_ship/__pycache__/snek.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/star_ship/__pycache__/star_ship.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/terminal_tumble/__init__.py +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/terminal_tumble/__pycache__/highscores.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/games/terminal_tumble/__pycache__/terminal_tumble.cpython-313.pyc +0 -0
- {cli_arcade-2026.1.2 → cli_arcade-2026.2.0}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli-arcade
|
|
3
|
-
Version: 2026.
|
|
3
|
+
Version: 2026.2.0
|
|
4
4
|
Summary: Collection of terminal CLI games
|
|
5
5
|
Home-page: https://github.com/Bro-Code-Technologies/cli-arcade/tree/main/windows
|
|
6
6
|
Author: Bro Code Technologies LLC
|
|
@@ -10,7 +10,7 @@ Project-URL: Source, https://github.com/Bro-Code-Technologies/cli-arcade/tree/ma
|
|
|
10
10
|
Project-URL: Issues, https://github.com/Bro-Code-Technologies/cli-arcade/tree/main/windows
|
|
11
11
|
Project-URL: Documentation, https://github.com/Bro-Code-Technologies/cli-arcade/tree/main/windows
|
|
12
12
|
Project-URL: Package, https://pypi.org/project/cli-arcade/
|
|
13
|
-
Keywords: cli,terminal,arcade,games
|
|
13
|
+
Keywords: cli,terminal,arcade,games
|
|
14
14
|
Classifier: Development Status :: 4 - Beta
|
|
15
15
|
Classifier: Environment :: Console
|
|
16
16
|
Classifier: Intended Audience :: End Users/Desktop
|
|
@@ -28,17 +28,17 @@ Requires-Python: >=3.8
|
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
Requires-Dist: appdirs
|
|
31
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: prompt_toolkit
|
|
32
32
|
Dynamic: license-file
|
|
33
33
|
|
|
34
34
|
# CLI Arcade
|
|
35
35
|
|
|
36
36
|
Collection of small terminal games bundled with a single CLI launcher.
|
|
37
37
|
|
|
38
|
-
Requirements
|
|
38
|
+
## Requirements
|
|
39
39
|
- Python 3.8+
|
|
40
40
|
|
|
41
|
-
Quick start
|
|
41
|
+
## Quick start
|
|
42
42
|
```powershell
|
|
43
43
|
# list installed console aliases and available games
|
|
44
44
|
clia list
|
|
@@ -54,23 +54,45 @@ clia run "Byte Bouncer"
|
|
|
54
54
|
clia reset 0
|
|
55
55
|
clia reset "Byte Bouncer"
|
|
56
56
|
clia reset -y # skip confirmation
|
|
57
|
-
|
|
58
|
-
# check for updates and optionally update
|
|
59
|
-
clia update
|
|
60
|
-
clia update --check # only check for updates
|
|
61
|
-
clia update -y # skip confirmation
|
|
62
57
|
```
|
|
63
58
|
|
|
64
|
-
Commands
|
|
65
|
-
- `clia` — interactive
|
|
59
|
+
## Commands
|
|
60
|
+
- `clia` — interactive terminal menu
|
|
66
61
|
- `clia list` — print available games and zero-based indices
|
|
67
62
|
- `clia run <index|name>` — run a game directly (index is zero-based)
|
|
68
63
|
- `clia reset [<index|name>] [-y]` — delete highscores for a game or all games
|
|
69
|
-
- `clia update [--check] [-y]` — check for updates and optionally update
|
|
70
64
|
- Aliases available: `cli-arcade`
|
|
71
65
|
|
|
72
|
-
License
|
|
66
|
+
## License
|
|
73
67
|
- MIT
|
|
74
68
|
|
|
75
|
-
Changelog
|
|
76
|
-
|
|
69
|
+
# Changelog
|
|
70
|
+
|
|
71
|
+
All notable changes to this project will be documented in this file.
|
|
72
|
+
|
|
73
|
+
## 2026.0.0
|
|
74
|
+
- Initial release as CLI Game.
|
|
75
|
+
|
|
76
|
+
## 2026.1.0
|
|
77
|
+
- Project renamed to CLI Arcade.
|
|
78
|
+
- Packaging metadata updated for PyPI.
|
|
79
|
+
- Documentation refresh.
|
|
80
|
+
|
|
81
|
+
### 2026.1.1
|
|
82
|
+
- Updated TITLE ASCII art.
|
|
83
|
+
- Added `clia update` command to check for and install updates from PyPI.
|
|
84
|
+
- (YANKED)
|
|
85
|
+
|
|
86
|
+
### 2026.1.2 (YANKED)
|
|
87
|
+
- Version bump for testing the update mechanism.
|
|
88
|
+
|
|
89
|
+
### 2026.1.3
|
|
90
|
+
- Removed `clia update` command.
|
|
91
|
+
|
|
92
|
+
### 2026.2.0
|
|
93
|
+
- Refactoring using `prompt_toolkit` replacing `windows-curses` for better cross-platform compatibility.
|
|
94
|
+
- Restore/clear alternate screen on run/exit (no UI artifacts).
|
|
95
|
+
- Centralize terminal-size checks in CLI; games expose `MIN_COLS`/`MIN_ROWS`.
|
|
96
|
+
- Byte Bouncer & Star Ship: floor + off-screen right wall; left-column bug fixed.
|
|
97
|
+
- `ptk`: default-attribute/color emission fix; add helper to exit alt screen.
|
|
98
|
+
- Fixed 180 movement glitch in Star Ship.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# CLI Arcade
|
|
2
|
+
|
|
3
|
+
Collection of small terminal games bundled with a single CLI launcher.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
- Python 3.8+
|
|
7
|
+
|
|
8
|
+
## Quick start
|
|
9
|
+
```powershell
|
|
10
|
+
# list installed console aliases and available games
|
|
11
|
+
clia list
|
|
12
|
+
|
|
13
|
+
# run interactive menu
|
|
14
|
+
clia
|
|
15
|
+
|
|
16
|
+
# run a game by zero-based index or name
|
|
17
|
+
clia run 0
|
|
18
|
+
clia run "Byte Bouncer"
|
|
19
|
+
|
|
20
|
+
# reset highscores for one game or all
|
|
21
|
+
clia reset 0
|
|
22
|
+
clia reset "Byte Bouncer"
|
|
23
|
+
clia reset -y # skip confirmation
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
- `clia` — interactive terminal menu
|
|
28
|
+
- `clia list` — print available games and zero-based indices
|
|
29
|
+
- `clia run <index|name>` — run a game directly (index is zero-based)
|
|
30
|
+
- `clia reset [<index|name>] [-y]` — delete highscores for a game or all games
|
|
31
|
+
- Aliases available: `cli-arcade`
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
- MIT
|
|
35
|
+
|
|
36
|
+
# Changelog
|
|
37
|
+
|
|
38
|
+
All notable changes to this project will be documented in this file.
|
|
39
|
+
|
|
40
|
+
## 2026.0.0
|
|
41
|
+
- Initial release as CLI Game.
|
|
42
|
+
|
|
43
|
+
## 2026.1.0
|
|
44
|
+
- Project renamed to CLI Arcade.
|
|
45
|
+
- Packaging metadata updated for PyPI.
|
|
46
|
+
- Documentation refresh.
|
|
47
|
+
|
|
48
|
+
### 2026.1.1
|
|
49
|
+
- Updated TITLE ASCII art.
|
|
50
|
+
- Added `clia update` command to check for and install updates from PyPI.
|
|
51
|
+
- (YANKED)
|
|
52
|
+
|
|
53
|
+
### 2026.1.2 (YANKED)
|
|
54
|
+
- Version bump for testing the update mechanism.
|
|
55
|
+
|
|
56
|
+
### 2026.1.3
|
|
57
|
+
- Removed `clia update` command.
|
|
58
|
+
|
|
59
|
+
### 2026.2.0
|
|
60
|
+
- Refactoring using `prompt_toolkit` replacing `windows-curses` for better cross-platform compatibility.
|
|
61
|
+
- Restore/clear alternate screen on run/exit (no UI artifacts).
|
|
62
|
+
- Centralize terminal-size checks in CLI; games expose `MIN_COLS`/`MIN_ROWS`.
|
|
63
|
+
- Byte Bouncer & Star Ship: floor + off-screen right wall; left-column bug fixed.
|
|
64
|
+
- `ptk`: default-attribute/color emission fix; add helper to exit alt screen.
|
|
65
|
+
- Fixed 180 movement glitch in Star Ship.
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Collection of small terminal games bundled with a single CLI launcher.
|
|
4
4
|
|
|
5
|
-
Requirements
|
|
5
|
+
## Requirements
|
|
6
6
|
- Python 3.8+
|
|
7
|
-
-
|
|
7
|
+
- prompt_toolkit (installed via requirements)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
pip install windows-curses
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Quick start (developer)
|
|
9
|
+
## Quick start (developer)
|
|
14
10
|
|
|
15
11
|
```powershell
|
|
16
12
|
# install editable (recommended during development)
|
|
@@ -45,35 +41,33 @@ You can always run the CLI directly without installing:
|
|
|
45
41
|
python -m cli
|
|
46
42
|
```
|
|
47
43
|
|
|
48
|
-
Commands
|
|
49
|
-
- `clia` — interactive
|
|
44
|
+
### Commands
|
|
45
|
+
- `clia` — interactive terminal menu
|
|
50
46
|
- `clia list` — print available games and zero-based indices
|
|
51
47
|
- `clia run <index|name>` — run a game directly (index is zero-based)
|
|
52
48
|
- `clia reset [<index|name>] [-y]` — delete highscores for a game or all games
|
|
53
|
-
- `clia update [--check] [-y]` — check for updates and optionally update
|
|
54
49
|
- Aliases available: `cli-arcade`
|
|
55
50
|
|
|
56
|
-
Highscores storage and migration
|
|
51
|
+
### Highscores storage and migration
|
|
57
52
|
- Highscores are now stored in a user-writable application data directory. Typical locations:
|
|
58
53
|
- Windows (appdirs): `%LOCALAPPDATA%\cli-arcade\games\<game>\highscores.json`
|
|
59
54
|
- Fallback (no appdirs): `%USERPROFILE%\.cli-arcade\games\<game>\highscores.json`
|
|
60
55
|
- On first run the CLI attempts to migrate any legacy `games/<game>/highscores.json` found in the project into the user data directory.
|
|
61
56
|
|
|
62
|
-
Packaging & publishing (brief)
|
|
63
|
-
|
|
57
|
+
### Packaging & publishing (brief)
|
|
64
58
|
- `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.
|
|
65
59
|
- Update `setup.cfg` version.
|
|
66
|
-
- Update `
|
|
60
|
+
- Update `PYPI_README.md` Changelog for new version.
|
|
67
61
|
- Build: `py -m build` (requires `build` package).
|
|
68
62
|
- Upload: `twine upload dist/*` (requires `twine`).
|
|
69
63
|
- The package exposes several console script aliases (see `setup.cfg` -> `options.entry_points.console_scripts`).
|
|
70
64
|
|
|
71
|
-
Notes & Troubleshooting
|
|
72
|
-
-
|
|
65
|
+
### Notes & Troubleshooting
|
|
66
|
+
- No platform-specific terminal dependencies are required.
|
|
73
67
|
- 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.
|
|
74
68
|
- 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.
|
|
75
69
|
|
|
76
|
-
Terminal recommendations (Windows)
|
|
70
|
+
### Terminal recommendations (Windows)
|
|
77
71
|
- Recommended: use Windows Terminal or the VS Code integrated terminal for the best UTF-8 + glyph support.
|
|
78
72
|
- Install Windows Terminal via Microsoft Store or `winget`:
|
|
79
73
|
|
|
@@ -97,12 +91,12 @@ $env:PYTHONIOENCODING = 'utf-8'
|
|
|
97
91
|
|
|
98
92
|
- 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.
|
|
99
93
|
|
|
100
|
-
Contributing
|
|
94
|
+
## Contributing
|
|
101
95
|
- Add a new game by creating a subdirectory under `games/` with a `game.py` file that exports `main(stdscr)`.
|
|
102
96
|
- Keep changes minimal and run `clia` locally to verify.
|
|
103
97
|
|
|
104
|
-
License
|
|
98
|
+
## License
|
|
105
99
|
- MIT
|
|
106
100
|
|
|
107
|
-
Changelog
|
|
108
|
-
- See
|
|
101
|
+
# Changelog
|
|
102
|
+
- See PYPI_README.md
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
from game_classes import ptk
|
|
2
|
+
from game_classes.tools import verify_terminal_size
|
|
2
3
|
import os
|
|
3
4
|
import importlib.util
|
|
4
5
|
import argparse
|
|
5
6
|
import glob
|
|
6
7
|
import sys
|
|
7
|
-
import json
|
|
8
8
|
import re
|
|
9
|
-
import
|
|
10
|
-
import urllib.request
|
|
9
|
+
import time
|
|
11
10
|
|
|
12
11
|
# helper: recognize Enter from multiple terminals/keypads
|
|
13
12
|
def is_enter_key(ch):
|
|
14
13
|
try:
|
|
15
|
-
enter_vals = {10, 13, getattr(
|
|
14
|
+
enter_vals = {10, 13, getattr(ptk, 'KEY_ENTER', -1), 343, 459}
|
|
16
15
|
except Exception:
|
|
17
16
|
enter_vals = {10, 13}
|
|
18
17
|
return ch in enter_vals
|
|
@@ -52,13 +51,38 @@ def _discover_games():
|
|
|
52
51
|
file_to_check = os.path.join(dirpath, pyfiles[0])
|
|
53
52
|
if not file_to_check:
|
|
54
53
|
continue
|
|
54
|
+
# try to read declared minimum terminal size from the game file
|
|
55
|
+
min_cols = None
|
|
56
|
+
min_rows = None
|
|
57
|
+
try:
|
|
58
|
+
with open(file_to_check, 'r', encoding='utf-8') as fh:
|
|
59
|
+
src = fh.read()
|
|
60
|
+
m = re.search(r'MIN_TERMINAL\s*=\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)', src)
|
|
61
|
+
if m:
|
|
62
|
+
min_cols = int(m.group(1))
|
|
63
|
+
min_rows = int(m.group(2))
|
|
64
|
+
else:
|
|
65
|
+
m1 = re.search(r'MIN_COLS\s*=\s*(\d+)', src)
|
|
66
|
+
m2 = re.search(r'MIN_ROWS\s*=\s*(\d+)', src)
|
|
67
|
+
if m1:
|
|
68
|
+
min_cols = int(m1.group(1))
|
|
69
|
+
if m2:
|
|
70
|
+
min_rows = int(m2.group(1))
|
|
71
|
+
except Exception:
|
|
72
|
+
min_cols = None
|
|
73
|
+
min_rows = None
|
|
55
74
|
# use directory name as the display name
|
|
56
75
|
name = entry.replace('_', ' ').title()
|
|
57
76
|
rel = os.path.relpath(file_to_check, base).replace('\\', '/')
|
|
58
77
|
games.append((name, rel))
|
|
78
|
+
try:
|
|
79
|
+
GAME_MINS[rel] = (min_cols, min_rows)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
59
82
|
return games
|
|
60
83
|
|
|
61
84
|
|
|
85
|
+
GAME_MINS = {}
|
|
62
86
|
GAMES = _discover_games()
|
|
63
87
|
|
|
64
88
|
|
|
@@ -78,29 +102,29 @@ def _read_console_aliases():
|
|
|
78
102
|
|
|
79
103
|
|
|
80
104
|
def _menu(stdscr):
|
|
81
|
-
|
|
105
|
+
ptk.curs_set(0)
|
|
82
106
|
stdscr.nodelay(False)
|
|
83
107
|
# ensure a cyan color pair is available for the title
|
|
84
|
-
if
|
|
108
|
+
if ptk.has_colors():
|
|
85
109
|
try:
|
|
86
110
|
# init colors
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
ptk.start_color()
|
|
112
|
+
ptk.use_default_colors()
|
|
89
113
|
# try to normalize key colors (0..1000 scale). Must run before init_pair.
|
|
90
|
-
if
|
|
114
|
+
if ptk.can_change_color() and ptk.COLORS >= 8:
|
|
91
115
|
try:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
ptk.init_color(ptk.COLOR_MAGENTA, 1000, 0, 1000)
|
|
117
|
+
ptk.init_color(ptk.COLOR_YELLOW, 1000, 1000, 0)
|
|
118
|
+
ptk.init_color(ptk.COLOR_WHITE, 1000, 1000, 1000)
|
|
119
|
+
ptk.init_color(ptk.COLOR_CYAN, 0, 1000, 1000)
|
|
120
|
+
ptk.init_color(ptk.COLOR_BLUE, 0, 0, 1000)
|
|
121
|
+
ptk.init_color(ptk.COLOR_GREEN, 0, 800, 0)
|
|
122
|
+
ptk.init_color(ptk.COLOR_RED, 1000, 0, 0)
|
|
123
|
+
ptk.init_color(ptk.COLOR_BLACK, 0, 0, 0)
|
|
100
124
|
except Exception:
|
|
101
125
|
pass
|
|
102
126
|
for i in range(1,8):
|
|
103
|
-
|
|
127
|
+
ptk.init_pair(i, i, -1)
|
|
104
128
|
except Exception:
|
|
105
129
|
pass
|
|
106
130
|
sel = 0
|
|
@@ -110,13 +134,13 @@ def _menu(stdscr):
|
|
|
110
134
|
h, w = stdscr.getmaxyx()
|
|
111
135
|
title_h = len(TITLE)
|
|
112
136
|
title_start = 0
|
|
113
|
-
colors = [
|
|
137
|
+
colors = [ptk.COLOR_MAGENTA, ptk.COLOR_MAGENTA, ptk.COLOR_CYAN, ptk.COLOR_CYAN, ptk.COLOR_GREEN, ptk.COLOR_GREEN]
|
|
114
138
|
for i, line in enumerate(TITLE):
|
|
115
139
|
try:
|
|
116
|
-
stdscr.addstr(title_start + i, 0, line,
|
|
140
|
+
stdscr.addstr(title_start + i, 0, line, ptk.color_pair(colors[i]))
|
|
117
141
|
except Exception:
|
|
118
142
|
pass
|
|
119
|
-
stdscr.addstr(title_h + 1, 2, "Use Up/Down, PageUp/PageDown, Enter to start, ESC to quit",
|
|
143
|
+
stdscr.addstr(title_h + 1, 2, "Use Up/Down, PageUp/PageDown, Enter to start, ESC to quit", ptk.color_pair(ptk.COLOR_WHITE))
|
|
120
144
|
start_y = title_h + 3
|
|
121
145
|
# number of lines available for the game list
|
|
122
146
|
avail = max(1, h - start_y - 2)
|
|
@@ -130,9 +154,9 @@ def _menu(stdscr):
|
|
|
130
154
|
for vis_i in range(min(avail, total)):
|
|
131
155
|
i = top + vis_i
|
|
132
156
|
name = GAMES[i][0]
|
|
133
|
-
attr =
|
|
157
|
+
attr = ptk.A_REVERSE if i == sel else ptk.A_NORMAL
|
|
134
158
|
try:
|
|
135
|
-
stdscr.addstr(start_y + vis_i, 2, name[:w-4],
|
|
159
|
+
stdscr.addstr(start_y + vis_i, 2, name[:w-4], ptk.color_pair(ptk.COLOR_CYAN) | attr)
|
|
136
160
|
except Exception:
|
|
137
161
|
pass
|
|
138
162
|
# optional scrollbar indicator when list is long
|
|
@@ -159,15 +183,34 @@ def _menu(stdscr):
|
|
|
159
183
|
stdscr.refresh()
|
|
160
184
|
|
|
161
185
|
ch = stdscr.getch()
|
|
162
|
-
if ch ==
|
|
186
|
+
if ch == ptk.KEY_UP:
|
|
163
187
|
sel = max(0, sel - 1)
|
|
164
|
-
elif ch ==
|
|
188
|
+
elif ch == ptk.KEY_DOWN:
|
|
165
189
|
sel = min(len(GAMES) - 1, sel + 1)
|
|
166
|
-
elif ch ==
|
|
190
|
+
elif ch == ptk.KEY_PPAGE: # Page Up
|
|
167
191
|
sel = max(0, sel - avail)
|
|
168
|
-
elif ch ==
|
|
192
|
+
elif ch == ptk.KEY_NPAGE: # Page Down
|
|
169
193
|
sel = min(len(GAMES) - 1, sel + avail)
|
|
170
194
|
elif is_enter_key(ch):
|
|
195
|
+
# Before leaving the menu, validate the selected game's minimum
|
|
196
|
+
# terminal size (if declared). If it's too small, show an
|
|
197
|
+
# on-screen message and keep the menu running.
|
|
198
|
+
try:
|
|
199
|
+
rel = GAMES[sel][1]
|
|
200
|
+
mins = GAME_MINS.get(rel)
|
|
201
|
+
if mins:
|
|
202
|
+
min_cols, min_rows = mins
|
|
203
|
+
if min_cols and min_rows and (w < int(min_cols) or h < int(min_rows)):
|
|
204
|
+
try:
|
|
205
|
+
msg = f"Terminal too small: {w}x{h}, need {min_cols}x{min_rows}. Resize to start."
|
|
206
|
+
stdscr.addstr(max(0, h-1), 2, msg[:max(0, w-4)], ptk.color_pair(ptk.COLOR_RED))
|
|
207
|
+
stdscr.refresh()
|
|
208
|
+
time.sleep(1.5)
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
continue
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
171
214
|
return sel
|
|
172
215
|
elif ch == 27:
|
|
173
216
|
return None
|
|
@@ -178,7 +221,7 @@ def _menu(stdscr):
|
|
|
178
221
|
top = sel - avail + 1
|
|
179
222
|
|
|
180
223
|
|
|
181
|
-
def _run_game_by_index(choice):
|
|
224
|
+
def _run_game_by_index(choice, from_menu=False):
|
|
182
225
|
"""Load and run the game given by numeric index in GAMES."""
|
|
183
226
|
name, relpath = GAMES[choice]
|
|
184
227
|
base = os.path.dirname(__file__)
|
|
@@ -211,7 +254,29 @@ def _run_game_by_index(choice):
|
|
|
211
254
|
pass
|
|
212
255
|
if hasattr(mod, 'main'):
|
|
213
256
|
try:
|
|
214
|
-
|
|
257
|
+
# If the module exposes minimum terminal requirements, verify
|
|
258
|
+
# them before entering the alternate screen so messages are visible
|
|
259
|
+
try:
|
|
260
|
+
min_cols = getattr(mod, 'MIN_COLS', None)
|
|
261
|
+
min_rows = getattr(mod, 'MIN_ROWS', None)
|
|
262
|
+
if min_cols is None and hasattr(mod, 'MIN_TERMINAL'):
|
|
263
|
+
t = getattr(mod, 'MIN_TERMINAL')
|
|
264
|
+
if isinstance(t, (tuple, list)) and len(t) >= 2:
|
|
265
|
+
min_cols, min_rows = t[0], t[1]
|
|
266
|
+
if min_cols is not None and min_rows is not None:
|
|
267
|
+
verify_terminal_size(name, int(min_cols), int(min_rows))
|
|
268
|
+
except SystemExit:
|
|
269
|
+
return
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
ptk.wrapper(mod.main)
|
|
274
|
+
# If launched via `clia run`, exit the process after the game ends.
|
|
275
|
+
if not from_menu:
|
|
276
|
+
try:
|
|
277
|
+
sys.exit(0)
|
|
278
|
+
except SystemExit:
|
|
279
|
+
raise
|
|
215
280
|
except Exception as e:
|
|
216
281
|
print(f" [ERROR] Error running game {name}: {e}")
|
|
217
282
|
else:
|
|
@@ -304,43 +369,6 @@ def _read_version_from_setupcfg():
|
|
|
304
369
|
except Exception:
|
|
305
370
|
return '0.0.0'
|
|
306
371
|
|
|
307
|
-
|
|
308
|
-
def _version_key(ver):
|
|
309
|
-
parts = []
|
|
310
|
-
for chunk in re.split(r'(\d+)', ver):
|
|
311
|
-
if not chunk:
|
|
312
|
-
continue
|
|
313
|
-
if chunk.isdigit():
|
|
314
|
-
parts.append((0, int(chunk)))
|
|
315
|
-
else:
|
|
316
|
-
parts.append((1, chunk.lower()))
|
|
317
|
-
return parts
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def _fetch_latest_version(package):
|
|
321
|
-
url = f"https://pypi.org/pypi/{package}/json"
|
|
322
|
-
try:
|
|
323
|
-
with urllib.request.urlopen(url, timeout=5) as resp:
|
|
324
|
-
data = json.load(resp)
|
|
325
|
-
return data.get('info', {}).get('version')
|
|
326
|
-
except Exception as e:
|
|
327
|
-
print(f" [ERROR] Failed to check latest version: {e}")
|
|
328
|
-
return None
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def _is_update_available(current, latest):
|
|
332
|
-
if not latest:
|
|
333
|
-
return False
|
|
334
|
-
try:
|
|
335
|
-
return _version_key(latest) > _version_key(current)
|
|
336
|
-
except Exception:
|
|
337
|
-
return latest != current
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def _run_pip_update(package):
|
|
341
|
-
cmd = [sys.executable, '-m', 'pip', 'install', '--upgrade', package]
|
|
342
|
-
return subprocess.call(cmd)
|
|
343
|
-
|
|
344
372
|
def main():
|
|
345
373
|
# support simple CLI subcommands (e.g. `clia list`)
|
|
346
374
|
# build epilog with examples and any console script aliases from setup.cfg
|
|
@@ -350,7 +378,6 @@ def main():
|
|
|
350
378
|
f' %(prog)s list [-h]',
|
|
351
379
|
f' %(prog)s run [-h] [0, "Byte Bouncer"]',
|
|
352
380
|
f' %(prog)s reset [-h] [0, "Byte Bouncer"] [-y]',
|
|
353
|
-
f' %(prog)s update [--check] [-y]',
|
|
354
381
|
]
|
|
355
382
|
aliases = _read_console_aliases()
|
|
356
383
|
if aliases:
|
|
@@ -390,15 +417,6 @@ def main():
|
|
|
390
417
|
)
|
|
391
418
|
resetp.add_argument('game', nargs='?', help='Optional game name or zero-based index (omit to reset all)')
|
|
392
419
|
resetp.add_argument('-y', '--yes', action='store_true', help='Do not prompt; proceed with deletion')
|
|
393
|
-
updatep = sub.add_parser(
|
|
394
|
-
'update',
|
|
395
|
-
help='Update cli-arcade package from PyPI',
|
|
396
|
-
description='Check PyPI for a newer version and update if available.',
|
|
397
|
-
epilog='Examples:\n %(prog)s\n %(prog)s --check\n %(prog)s -y\n',
|
|
398
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
399
|
-
)
|
|
400
|
-
updatep.add_argument('-c', '--check', action='store_true', help='Only check for updates (do not install)')
|
|
401
|
-
updatep.add_argument('-y', '--yes', action='store_true', help='Do not prompt; proceed with update')
|
|
402
420
|
args, _rest = parser.parse_known_args()
|
|
403
421
|
|
|
404
422
|
if args.cmd == 'list':
|
|
@@ -440,7 +458,7 @@ def main():
|
|
|
440
458
|
return
|
|
441
459
|
# run the selected game (skip menu)
|
|
442
460
|
try:
|
|
443
|
-
_run_game_by_index(choice)
|
|
461
|
+
_run_game_by_index(choice, from_menu=False)
|
|
444
462
|
except Exception as e:
|
|
445
463
|
print(f" [ERROR] Error running game: {e}")
|
|
446
464
|
return
|
|
@@ -472,74 +490,21 @@ def main():
|
|
|
472
490
|
_reset_game_by_index(choice, yes=yes)
|
|
473
491
|
return
|
|
474
492
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if not latest:
|
|
482
|
-
print(' [INFO] Unable to determine latest version.')
|
|
483
|
-
return
|
|
484
|
-
if _is_update_available(current, latest):
|
|
485
|
-
print(f" [INFO] Update available: {current} -> {latest}")
|
|
486
|
-
if args.check:
|
|
487
|
-
return
|
|
488
|
-
if not getattr(args, 'yes', False):
|
|
489
|
-
ans = input(' [ACTION] Update now? [y/N]: ')
|
|
490
|
-
if not ans.lower().startswith('y'):
|
|
491
|
-
print(' [CANCELED]')
|
|
492
|
-
return
|
|
493
|
-
code = _run_pip_update(package)
|
|
494
|
-
if code == 0:
|
|
495
|
-
print(f" [OK] Updated {package}.")
|
|
496
|
-
else:
|
|
497
|
-
print(f" [ERROR] Update failed with exit code {code}.")
|
|
493
|
+
# Interactive menu loop: verify terminal size before showing menu each time,
|
|
494
|
+
# run the selected game, then return to the menu when the game exits.
|
|
495
|
+
while True:
|
|
496
|
+
try:
|
|
497
|
+
verify_terminal_size('CLI Arcade', 70, 20)
|
|
498
|
+
except SystemExit:
|
|
498
499
|
return
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
choice = curses.wrapper(_menu)
|
|
504
|
-
if choice is None:
|
|
505
|
-
return
|
|
506
|
-
name, relpath = GAMES[choice]
|
|
507
|
-
base = os.path.dirname(__file__)
|
|
508
|
-
path = os.path.join(base, relpath)
|
|
509
|
-
if not os.path.exists(path):
|
|
510
|
-
print(f" [INFO] Game file not found: {path}")
|
|
511
|
-
return
|
|
512
|
-
# Ensure the game's directory is on sys.path so local imports (like `highscores`) resolve
|
|
513
|
-
game_dir = os.path.dirname(path)
|
|
514
|
-
spec = importlib.util.spec_from_file_location(f"cli_game_{choice}", path)
|
|
515
|
-
mod = importlib.util.module_from_spec(spec)
|
|
516
|
-
inserted = []
|
|
517
|
-
try:
|
|
518
|
-
proj_root = os.path.dirname(__file__)
|
|
519
|
-
if game_dir and game_dir not in sys.path:
|
|
520
|
-
sys.path.insert(0, game_dir)
|
|
521
|
-
inserted.append(game_dir)
|
|
522
|
-
if proj_root and proj_root not in sys.path:
|
|
523
|
-
sys.path.insert(0, proj_root)
|
|
524
|
-
inserted.append(proj_root)
|
|
525
|
-
spec.loader.exec_module(mod)
|
|
526
|
-
except Exception as e:
|
|
527
|
-
print(f" [INFO] Failed to load game {name}: {e}")
|
|
528
|
-
return
|
|
529
|
-
finally:
|
|
530
|
-
for p in inserted:
|
|
531
|
-
try:
|
|
532
|
-
sys.path.remove(p)
|
|
533
|
-
except Exception:
|
|
534
|
-
pass
|
|
535
|
-
# call the game's main function if present
|
|
536
|
-
if hasattr(mod, 'main'):
|
|
500
|
+
choice = ptk.wrapper(_menu)
|
|
501
|
+
if choice is None:
|
|
502
|
+
break
|
|
503
|
+
# Run the selected game (this returns when the game exits, e.g. ESC)
|
|
537
504
|
try:
|
|
538
|
-
|
|
505
|
+
_run_game_by_index(choice, from_menu=True)
|
|
539
506
|
except Exception as e:
|
|
540
|
-
print(f" [ERROR] Error running game
|
|
541
|
-
else:
|
|
542
|
-
print(f" [INFO] Game {name} has no main(stdscr) entry point.")
|
|
507
|
+
print(f" [ERROR] Error running game: {e}")
|
|
543
508
|
|
|
544
509
|
|
|
545
510
|
if __name__ == '__main__':
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
CHANGELOG.md
|
|
2
1
|
LICENSE
|
|
3
2
|
MANIFEST.in
|
|
4
3
|
PYPI_README.md
|
|
@@ -10,11 +9,17 @@ game_classes/__init__.py
|
|
|
10
9
|
game_classes/game_base.py
|
|
11
10
|
game_classes/highscores.py
|
|
12
11
|
game_classes/menu.py
|
|
12
|
+
game_classes/ptk.py
|
|
13
13
|
game_classes/tools.py
|
|
14
14
|
game_classes/__pycache__/__init__.cpython-313.pyc
|
|
15
15
|
game_classes/__pycache__/game_base.cpython-313.pyc
|
|
16
16
|
game_classes/__pycache__/highscores.cpython-313.pyc
|
|
17
17
|
game_classes/__pycache__/menu.cpython-313.pyc
|
|
18
|
+
game_classes/__pycache__/ptk.cpython-313.pyc
|
|
19
|
+
game_classes/__pycache__/ptk_curses.cpython-313.pyc
|
|
20
|
+
game_classes/__pycache__/ptk_game_base.cpython-313.pyc
|
|
21
|
+
game_classes/__pycache__/ptk_menu.cpython-313.pyc
|
|
22
|
+
game_classes/__pycache__/ptk_tools.cpython-313.pyc
|
|
18
23
|
game_classes/__pycache__/tools.cpython-313.pyc
|
|
19
24
|
games/__init__.py
|
|
20
25
|
games/byte_bouncer/__init__.py
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|