wack-a-wombat 1.0.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.
- wack_a_wombat-1.0.0/LICENSE +21 -0
- wack_a_wombat-1.0.0/PKG-INFO +155 -0
- wack_a_wombat-1.0.0/README.md +136 -0
- wack_a_wombat-1.0.0/pyproject.toml +33 -0
- wack_a_wombat-1.0.0/setup.cfg +4 -0
- wack_a_wombat-1.0.0/tests/test_art.py +111 -0
- wack_a_wombat-1.0.0/tests/test_engine.py +305 -0
- wack_a_wombat-1.0.0/tests/test_model.py +295 -0
- wack_a_wombat-1.0.0/tests/test_persistence.py +220 -0
- wack_a_wombat-1.0.0/tests/test_render.py +131 -0
- wack_a_wombat-1.0.0/tests/test_rules.py +119 -0
- wack_a_wombat-1.0.0/tests/test_scenes.py +445 -0
- wack_a_wombat-1.0.0/tests/test_smoke_pty.py +86 -0
- wack_a_wombat-1.0.0/wack_a_wombat/__init__.py +3 -0
- wack_a_wombat-1.0.0/wack_a_wombat/__main__.py +4 -0
- wack_a_wombat-1.0.0/wack_a_wombat/app.py +181 -0
- wack_a_wombat-1.0.0/wack_a_wombat/art/__init__.py +1 -0
- wack_a_wombat-1.0.0/wack_a_wombat/art/banners.py +48 -0
- wack_a_wombat-1.0.0/wack_a_wombat/art/blockfont.py +76 -0
- wack_a_wombat-1.0.0/wack_a_wombat/art/skin.py +32 -0
- wack_a_wombat-1.0.0/wack_a_wombat/art/sprite.py +82 -0
- wack_a_wombat-1.0.0/wack_a_wombat/art/wombats.py +230 -0
- wack_a_wombat-1.0.0/wack_a_wombat/cli.py +83 -0
- wack_a_wombat-1.0.0/wack_a_wombat/engine/__init__.py +1 -0
- wack_a_wombat-1.0.0/wack_a_wombat/engine/canvas.py +147 -0
- wack_a_wombat-1.0.0/wack_a_wombat/engine/fx.py +144 -0
- wack_a_wombat-1.0.0/wack_a_wombat/engine/input.py +69 -0
- wack_a_wombat-1.0.0/wack_a_wombat/engine/palette.py +79 -0
- wack_a_wombat-1.0.0/wack_a_wombat/engine/scene.py +115 -0
- wack_a_wombat-1.0.0/wack_a_wombat/engine/tween.py +97 -0
- wack_a_wombat-1.0.0/wack_a_wombat/game/__init__.py +1 -0
- wack_a_wombat-1.0.0/wack_a_wombat/game/config.py +71 -0
- wack_a_wombat-1.0.0/wack_a_wombat/game/critters.py +49 -0
- wack_a_wombat-1.0.0/wack_a_wombat/game/director.py +64 -0
- wack_a_wombat-1.0.0/wack_a_wombat/game/model.py +244 -0
- wack_a_wombat-1.0.0/wack_a_wombat/game/scoring.py +36 -0
- wack_a_wombat-1.0.0/wack_a_wombat/game/stats.py +47 -0
- wack_a_wombat-1.0.0/wack_a_wombat/persistence/__init__.py +1 -0
- wack_a_wombat-1.0.0/wack_a_wombat/persistence/highscores.py +121 -0
- wack_a_wombat-1.0.0/wack_a_wombat/persistence/settings.py +50 -0
- wack_a_wombat-1.0.0/wack_a_wombat/persistence/store.py +70 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/__init__.py +1 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/attract.py +79 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/board_view.py +151 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/game.py +223 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/game_over.py +165 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/help.py +73 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/highscores.py +81 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/name_entry.py +66 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/pause.py +56 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/title.py +169 -0
- wack_a_wombat-1.0.0/wack_a_wombat/scenes/ui.py +53 -0
- wack_a_wombat-1.0.0/wack_a_wombat.egg-info/PKG-INFO +155 -0
- wack_a_wombat-1.0.0/wack_a_wombat.egg-info/SOURCES.txt +56 -0
- wack_a_wombat-1.0.0/wack_a_wombat.egg-info/dependency_links.txt +1 -0
- wack_a_wombat-1.0.0/wack_a_wombat.egg-info/entry_points.txt +2 -0
- wack_a_wombat-1.0.0/wack_a_wombat.egg-info/requires.txt +3 -0
- wack_a_wombat-1.0.0/wack_a_wombat.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The wack-a-wombat authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wack-a-wombat
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A terminal whack-a-mole starring wombats. WASD to move, SPACE to whack.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: game,tui,curses,terminal,wombat,whack-a-mole,arcade
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: Environment :: Console :: Curses
|
|
9
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Games/Entertainment :: Arcade
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: windows-curses; sys_platform == "win32"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Wack-a-Wombat
|
|
21
|
+
|
|
22
|
+
A terminal whack-a-mole with 100% more wombat. Pure Python 3.9+, no
|
|
23
|
+
dependencies (just the standard library's `curses`; on Windows it pulls in
|
|
24
|
+
`windows-curses`).
|
|
25
|
+
|
|
26
|
+
Play it right now, nothing to install:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
uvx wack-a-wombat
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
or install the `wack-a-wombat` command:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pipx install wack-a-wombat # or: pip install --user wack-a-wombat
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
From a checkout: `python3 -m wack_a_wombat`.
|
|
39
|
+
|
|
40
|
+
## How to play
|
|
41
|
+
|
|
42
|
+
Wombats pop out of their burrows. Move the mallet onto one and whack it before it
|
|
43
|
+
ducks back down. One round on the clock; score as much as you can.
|
|
44
|
+
|
|
45
|
+
| Key | Action |
|
|
46
|
+
| --------------------------- | ------------------------------ |
|
|
47
|
+
| `W A S D` / arrows / `HJKL` | move the mallet |
|
|
48
|
+
| `SPACE` / `ENTER` | whack |
|
|
49
|
+
| `P` / `ESC` | pause (resume, end round, menu)|
|
|
50
|
+
| `M` | toggle the terminal bell |
|
|
51
|
+
| `?` | how to play (title screen) |
|
|
52
|
+
|
|
53
|
+
### Critters
|
|
54
|
+
|
|
55
|
+
| Sprite | What to do | Points |
|
|
56
|
+
| ------------ | ------------------------------ | ------------------------------- |
|
|
57
|
+
| Wombat | whack it | 10 x multiplier (+ speed bonus) |
|
|
58
|
+
| Golden wombat| whack it, fast - it's brief | 50 x multiplier (+ speed bonus) |
|
|
59
|
+
| Sleeping | **leave it alone** | -25 and your streak is gone |
|
|
60
|
+
|
|
61
|
+
- **Streak**: every 5 consecutive hits raise your multiplier, up to x4. A whiff, a
|
|
62
|
+
bonked sleeper or an escaped wombat resets it.
|
|
63
|
+
- **Speed bonus**: up to +10 for hitting a wombat the instant it appears.
|
|
64
|
+
- **Danger**: the final 5 seconds - wombats stay up for less time and come faster.
|
|
65
|
+
|
|
66
|
+
### Modes
|
|
67
|
+
|
|
68
|
+
| Mode | Grid | Round | Feel |
|
|
69
|
+
| -------- | ---- | ----- | ------------------------------------ |
|
|
70
|
+
| Backyard | 3x3 | 30s | one wombat at a time, gentle ramp |
|
|
71
|
+
| Outback | 4x4 | 45s | up to two at once, more sleepers |
|
|
72
|
+
| Frenzy | 4x4 | 30s | up to three at once, very short gaps |
|
|
73
|
+
|
|
74
|
+
Each mode has its own top-10 leaderboard with name, score, hits, accuracy, best
|
|
75
|
+
streak, fastest whack and date.
|
|
76
|
+
|
|
77
|
+
### Wombat skins
|
|
78
|
+
|
|
79
|
+
Three hand-drawn wombats, all with the signature big flat nose, small round ears
|
|
80
|
+
and loaf-shaped body. Switch with `WOMBAT < >` on the title screen or `--skin`.
|
|
81
|
+
|
|
82
|
+
| Skin | Style |
|
|
83
|
+
| ---------- | ------------------------------------------------- |
|
|
84
|
+
| `classic` | nose-first: a giant filled nose pad on a flat head |
|
|
85
|
+
| `arcade` | bursting out of a grassy mound, paws on the rim |
|
|
86
|
+
| `lineart` | 90s ASCII-forum line art |
|
|
87
|
+
|
|
88
|
+
The art came from a panel of ASCII artists each drawing in a different style,
|
|
89
|
+
scored by judges on wombat-likeness, how readable each state is mid-game, charm
|
|
90
|
+
and the title showpiece; `classic` won and was refined from the judges' notes.
|
|
91
|
+
|
|
92
|
+
## Command line
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
wack-a-wombat [--mode backyard|outback|frenzy] [--seed N] [--no-color] [--fps N]
|
|
96
|
+
[--sound | --no-sound] [--skin NAME] [--data-dir DIR]
|
|
97
|
+
[--list-modes] [--list-skins] [--reset-scores] [--version]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Settings and high scores are JSON files in `~/.config/wack-a-wombat/` (or
|
|
101
|
+
`$XDG_CONFIG_HOME/wack-a-wombat/`, or `$WACK_A_WOMBAT_HOME`). Writes are atomic;
|
|
102
|
+
a corrupt file is moved aside rather than overwritten.
|
|
103
|
+
|
|
104
|
+
`--seed N` makes every round reproducible: the first round after launch always
|
|
105
|
+
gets the same wombats, as does the second, and so on, however long you idle on
|
|
106
|
+
the title screen.
|
|
107
|
+
|
|
108
|
+
Backyard fits a classic 80x24 terminal (the HUD squeezes to two lines below 28
|
|
109
|
+
rows); the 4x4 modes need at least 31 rows, 35 for the full HUD. Anything smaller
|
|
110
|
+
gets a resize prompt and the round pauses until there's room.
|
|
111
|
+
|
|
112
|
+
## Architecture
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
wack_a_wombat/
|
|
116
|
+
cli.py argparse entry point
|
|
117
|
+
app.py curses bootstrap, frame loop, scene stack, shared services
|
|
118
|
+
engine/ reusable TUI bits - no game knowledge
|
|
119
|
+
input.py key -> Action mapping (wasd / arrows / hjkl)
|
|
120
|
+
palette.py named attrs; 256 -> 8 colours -> mono degradation
|
|
121
|
+
canvas.py clipped, exception-free drawing over a curses window
|
|
122
|
+
scene.py Scene + SceneStack with transparent overlays
|
|
123
|
+
tween.py clamp/lerp/easing, Timer, Blink, RollingNumber
|
|
124
|
+
fx.py score popups, labels, screen shake, confetti
|
|
125
|
+
game/ pure game logic - no curses, deterministic given a seed
|
|
126
|
+
config.py Mode definitions (grid, timing, difficulty curve)
|
|
127
|
+
critters.py CritterKind, Phase, Critter
|
|
128
|
+
director.py difficulty ramp: up-time, gaps, kind mix, simultaneous cap
|
|
129
|
+
scoring.py points, multipliers, speed bonus
|
|
130
|
+
stats.py per-round statistics
|
|
131
|
+
model.py Round: the state machine, emits events
|
|
132
|
+
art/ Sprite type, wombat skins, block-letter banners, block font
|
|
133
|
+
persistence/ atomic JSON store, per-mode leaderboard, settings
|
|
134
|
+
scenes/ title (+attract demo), game, pause, game over, name entry,
|
|
135
|
+
high scores, help; board_view draws and colours the grid,
|
|
136
|
+
ui draws the shadowed banners
|
|
137
|
+
tests/ unit tests + headless end-to-end via a fake curses window
|
|
138
|
+
tools/preview.py render every screen at any terminal size, as text or colour HTML
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The `Round` in `game/model.py` is the heart: feed it `update(dt)`, `move()` and
|
|
142
|
+
`whack()`, drain its events, and it never touches the screen. Scenes translate
|
|
143
|
+
events into effects. That split is what makes the game fully testable without a
|
|
144
|
+
terminal - `tests/test_scenes.py` plays whole rounds through the real scenes into
|
|
145
|
+
a fake window.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
python3 -m unittest discover -s tests -v # everything, incl. a ~10s pty smoke test
|
|
151
|
+
WOMBAT_SKIP_PTY=1 python3 -m unittest discover -s tests
|
|
152
|
+
python3 tools/preview.py 40 100 # eyeball every screen as text
|
|
153
|
+
python3 tools/preview.py --html shots/all.html --skin arcade # in colour
|
|
154
|
+
python3 -m wack_a_wombat --seed 1 # deterministic wombats
|
|
155
|
+
```
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Wack-a-Wombat
|
|
2
|
+
|
|
3
|
+
A terminal whack-a-mole with 100% more wombat. Pure Python 3.9+, no
|
|
4
|
+
dependencies (just the standard library's `curses`; on Windows it pulls in
|
|
5
|
+
`windows-curses`).
|
|
6
|
+
|
|
7
|
+
Play it right now, nothing to install:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
uvx wack-a-wombat
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or install the `wack-a-wombat` command:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pipx install wack-a-wombat # or: pip install --user wack-a-wombat
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
From a checkout: `python3 -m wack_a_wombat`.
|
|
20
|
+
|
|
21
|
+
## How to play
|
|
22
|
+
|
|
23
|
+
Wombats pop out of their burrows. Move the mallet onto one and whack it before it
|
|
24
|
+
ducks back down. One round on the clock; score as much as you can.
|
|
25
|
+
|
|
26
|
+
| Key | Action |
|
|
27
|
+
| --------------------------- | ------------------------------ |
|
|
28
|
+
| `W A S D` / arrows / `HJKL` | move the mallet |
|
|
29
|
+
| `SPACE` / `ENTER` | whack |
|
|
30
|
+
| `P` / `ESC` | pause (resume, end round, menu)|
|
|
31
|
+
| `M` | toggle the terminal bell |
|
|
32
|
+
| `?` | how to play (title screen) |
|
|
33
|
+
|
|
34
|
+
### Critters
|
|
35
|
+
|
|
36
|
+
| Sprite | What to do | Points |
|
|
37
|
+
| ------------ | ------------------------------ | ------------------------------- |
|
|
38
|
+
| Wombat | whack it | 10 x multiplier (+ speed bonus) |
|
|
39
|
+
| Golden wombat| whack it, fast - it's brief | 50 x multiplier (+ speed bonus) |
|
|
40
|
+
| Sleeping | **leave it alone** | -25 and your streak is gone |
|
|
41
|
+
|
|
42
|
+
- **Streak**: every 5 consecutive hits raise your multiplier, up to x4. A whiff, a
|
|
43
|
+
bonked sleeper or an escaped wombat resets it.
|
|
44
|
+
- **Speed bonus**: up to +10 for hitting a wombat the instant it appears.
|
|
45
|
+
- **Danger**: the final 5 seconds - wombats stay up for less time and come faster.
|
|
46
|
+
|
|
47
|
+
### Modes
|
|
48
|
+
|
|
49
|
+
| Mode | Grid | Round | Feel |
|
|
50
|
+
| -------- | ---- | ----- | ------------------------------------ |
|
|
51
|
+
| Backyard | 3x3 | 30s | one wombat at a time, gentle ramp |
|
|
52
|
+
| Outback | 4x4 | 45s | up to two at once, more sleepers |
|
|
53
|
+
| Frenzy | 4x4 | 30s | up to three at once, very short gaps |
|
|
54
|
+
|
|
55
|
+
Each mode has its own top-10 leaderboard with name, score, hits, accuracy, best
|
|
56
|
+
streak, fastest whack and date.
|
|
57
|
+
|
|
58
|
+
### Wombat skins
|
|
59
|
+
|
|
60
|
+
Three hand-drawn wombats, all with the signature big flat nose, small round ears
|
|
61
|
+
and loaf-shaped body. Switch with `WOMBAT < >` on the title screen or `--skin`.
|
|
62
|
+
|
|
63
|
+
| Skin | Style |
|
|
64
|
+
| ---------- | ------------------------------------------------- |
|
|
65
|
+
| `classic` | nose-first: a giant filled nose pad on a flat head |
|
|
66
|
+
| `arcade` | bursting out of a grassy mound, paws on the rim |
|
|
67
|
+
| `lineart` | 90s ASCII-forum line art |
|
|
68
|
+
|
|
69
|
+
The art came from a panel of ASCII artists each drawing in a different style,
|
|
70
|
+
scored by judges on wombat-likeness, how readable each state is mid-game, charm
|
|
71
|
+
and the title showpiece; `classic` won and was refined from the judges' notes.
|
|
72
|
+
|
|
73
|
+
## Command line
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
wack-a-wombat [--mode backyard|outback|frenzy] [--seed N] [--no-color] [--fps N]
|
|
77
|
+
[--sound | --no-sound] [--skin NAME] [--data-dir DIR]
|
|
78
|
+
[--list-modes] [--list-skins] [--reset-scores] [--version]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Settings and high scores are JSON files in `~/.config/wack-a-wombat/` (or
|
|
82
|
+
`$XDG_CONFIG_HOME/wack-a-wombat/`, or `$WACK_A_WOMBAT_HOME`). Writes are atomic;
|
|
83
|
+
a corrupt file is moved aside rather than overwritten.
|
|
84
|
+
|
|
85
|
+
`--seed N` makes every round reproducible: the first round after launch always
|
|
86
|
+
gets the same wombats, as does the second, and so on, however long you idle on
|
|
87
|
+
the title screen.
|
|
88
|
+
|
|
89
|
+
Backyard fits a classic 80x24 terminal (the HUD squeezes to two lines below 28
|
|
90
|
+
rows); the 4x4 modes need at least 31 rows, 35 for the full HUD. Anything smaller
|
|
91
|
+
gets a resize prompt and the round pauses until there's room.
|
|
92
|
+
|
|
93
|
+
## Architecture
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
wack_a_wombat/
|
|
97
|
+
cli.py argparse entry point
|
|
98
|
+
app.py curses bootstrap, frame loop, scene stack, shared services
|
|
99
|
+
engine/ reusable TUI bits - no game knowledge
|
|
100
|
+
input.py key -> Action mapping (wasd / arrows / hjkl)
|
|
101
|
+
palette.py named attrs; 256 -> 8 colours -> mono degradation
|
|
102
|
+
canvas.py clipped, exception-free drawing over a curses window
|
|
103
|
+
scene.py Scene + SceneStack with transparent overlays
|
|
104
|
+
tween.py clamp/lerp/easing, Timer, Blink, RollingNumber
|
|
105
|
+
fx.py score popups, labels, screen shake, confetti
|
|
106
|
+
game/ pure game logic - no curses, deterministic given a seed
|
|
107
|
+
config.py Mode definitions (grid, timing, difficulty curve)
|
|
108
|
+
critters.py CritterKind, Phase, Critter
|
|
109
|
+
director.py difficulty ramp: up-time, gaps, kind mix, simultaneous cap
|
|
110
|
+
scoring.py points, multipliers, speed bonus
|
|
111
|
+
stats.py per-round statistics
|
|
112
|
+
model.py Round: the state machine, emits events
|
|
113
|
+
art/ Sprite type, wombat skins, block-letter banners, block font
|
|
114
|
+
persistence/ atomic JSON store, per-mode leaderboard, settings
|
|
115
|
+
scenes/ title (+attract demo), game, pause, game over, name entry,
|
|
116
|
+
high scores, help; board_view draws and colours the grid,
|
|
117
|
+
ui draws the shadowed banners
|
|
118
|
+
tests/ unit tests + headless end-to-end via a fake curses window
|
|
119
|
+
tools/preview.py render every screen at any terminal size, as text or colour HTML
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The `Round` in `game/model.py` is the heart: feed it `update(dt)`, `move()` and
|
|
123
|
+
`whack()`, drain its events, and it never touches the screen. Scenes translate
|
|
124
|
+
events into effects. That split is what makes the game fully testable without a
|
|
125
|
+
terminal - `tests/test_scenes.py` plays whole rounds through the real scenes into
|
|
126
|
+
a fake window.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
python3 -m unittest discover -s tests -v # everything, incl. a ~10s pty smoke test
|
|
132
|
+
WOMBAT_SKIP_PTY=1 python3 -m unittest discover -s tests
|
|
133
|
+
python3 tools/preview.py 40 100 # eyeball every screen as text
|
|
134
|
+
python3 tools/preview.py --html shots/all.html --skin arcade # in colour
|
|
135
|
+
python3 -m wack_a_wombat --seed 1 # deterministic wombats
|
|
136
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wack-a-wombat"
|
|
7
|
+
dynamic = ["version"] # from wack_a_wombat/__init__.py
|
|
8
|
+
description = "A terminal whack-a-mole starring wombats. WASD to move, SPACE to whack."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["game", "tui", "curses", "terminal", "wombat", "whack-a-mole", "arcade"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Environment :: Console :: Curses",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Topic :: Games/Entertainment :: Arcade",
|
|
22
|
+
]
|
|
23
|
+
# curses ships with Python everywhere except Windows
|
|
24
|
+
dependencies = ["windows-curses; sys_platform == 'win32'"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
wack-a-wombat = "wack_a_wombat.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.dynamic]
|
|
30
|
+
version = { attr = "wack_a_wombat.__version__" }
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
include = ["wack_a_wombat*"]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from wack_a_wombat.art import banners, blockfont
|
|
4
|
+
from wack_a_wombat.art.skin import CELL_SPRITE_NAMES
|
|
5
|
+
from wack_a_wombat.art.sprite import CELL_H, CELL_W, TITLE_MAX_H, TITLE_MAX_W, Sprite, is_ascii_row
|
|
6
|
+
from wack_a_wombat.art.wombats import DEFAULT_SKIN, SKINS, get_skin, next_skin, skin_keys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SpriteType(unittest.TestCase):
|
|
10
|
+
def test_make_pads_and_anchors_bottom(self):
|
|
11
|
+
s = Sprite.make(["ab", "c"], width=4, height=3)
|
|
12
|
+
self.assertEqual(s.rows, (" ", "ab ", "c "))
|
|
13
|
+
self.assertEqual((s.h, s.w), (3, 4))
|
|
14
|
+
|
|
15
|
+
def test_make_anchor_top(self):
|
|
16
|
+
s = Sprite.make(["ab"], width=2, height=2, anchor="top")
|
|
17
|
+
self.assertEqual(s.rows, ("ab", " "))
|
|
18
|
+
|
|
19
|
+
def test_make_rejects_non_ascii_and_overflow(self):
|
|
20
|
+
with self.assertRaises(ValueError):
|
|
21
|
+
Sprite.make(["café"])
|
|
22
|
+
with self.assertRaises(ValueError):
|
|
23
|
+
Sprite.make(["toolong"], width=3)
|
|
24
|
+
with self.assertRaises(ValueError):
|
|
25
|
+
Sprite.make(["a", "b", "c"], height=2)
|
|
26
|
+
with self.assertRaises(ValueError):
|
|
27
|
+
Sprite.make(["tab\there"])
|
|
28
|
+
|
|
29
|
+
def test_shift_and_hide(self):
|
|
30
|
+
s = Sprite.make(["ab", "cd", "ef"])
|
|
31
|
+
self.assertEqual(s.shifted_down(1).rows, (" ", "ab", "cd"))
|
|
32
|
+
self.assertEqual(s.shifted_down(0), s)
|
|
33
|
+
self.assertEqual(s.shifted_down(9).rows, (" ", " ", " "))
|
|
34
|
+
self.assertEqual(Sprite.make(["zZa"]).hide("zZ").rows, (" a",))
|
|
35
|
+
self.assertEqual(Sprite.make(["ab"]).translate(str.upper).rows, ("AB",))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Skins(unittest.TestCase):
|
|
39
|
+
def test_registry(self):
|
|
40
|
+
self.assertIn(DEFAULT_SKIN, SKINS)
|
|
41
|
+
self.assertIs(get_skin("does-not-exist"), SKINS[DEFAULT_SKIN])
|
|
42
|
+
keys = skin_keys()
|
|
43
|
+
self.assertEqual(next_skin(keys[-1]), keys[0])
|
|
44
|
+
self.assertEqual(next_skin("bogus"), keys[1 % len(keys)])
|
|
45
|
+
|
|
46
|
+
def test_every_cell_sprite_is_exactly_cell_sized_ascii(self):
|
|
47
|
+
for skin in SKINS.values():
|
|
48
|
+
for name in CELL_SPRITE_NAMES:
|
|
49
|
+
sprite = getattr(skin, name)
|
|
50
|
+
self.assertEqual(sprite.h, CELL_H, f"{skin.key}.{name}")
|
|
51
|
+
for row in sprite.rows:
|
|
52
|
+
self.assertEqual(len(row), CELL_W, f"{skin.key}.{name}: {row!r}")
|
|
53
|
+
self.assertTrue(is_ascii_row(row), f"{skin.key}.{name}: {row!r}")
|
|
54
|
+
self.assertLessEqual(skin.title.h, TITLE_MAX_H)
|
|
55
|
+
self.assertLessEqual(skin.title.w, TITLE_MAX_W)
|
|
56
|
+
self.assertTrue(all(is_ascii_row(r) for r in skin.title.rows))
|
|
57
|
+
|
|
58
|
+
def test_states_differ_and_burrow_is_empty_on_top(self):
|
|
59
|
+
for skin in SKINS.values():
|
|
60
|
+
self.assertNotEqual(skin.up.rows, skin.sleeping.rows, skin.key)
|
|
61
|
+
self.assertNotEqual(skin.up.rows, skin.golden.rows, skin.key)
|
|
62
|
+
self.assertNotEqual(skin.up.rows, skin.bonked.rows, skin.key)
|
|
63
|
+
self.assertNotEqual(skin.up.rows, skin.peek.rows, skin.key)
|
|
64
|
+
self.assertNotEqual(skin.up.rows, skin.burrow.rows, skin.key)
|
|
65
|
+
# a sleeping wombat needs z's, a burrow has nothing above the rim
|
|
66
|
+
self.assertTrue(any(ch in "zZ" for row in skin.sleeping.rows for ch in row), skin.key)
|
|
67
|
+
self.assertEqual(skin.burrow.rows[0].strip(), "", skin.key)
|
|
68
|
+
|
|
69
|
+
def test_cell_sprites_share_a_rim_with_the_burrow(self):
|
|
70
|
+
"""The bottom row of every wombat state should mostly coincide with the burrow's,
|
|
71
|
+
so states swap in place without the hole jumping around."""
|
|
72
|
+
for skin in SKINS.values():
|
|
73
|
+
rim = skin.burrow.rows[-1]
|
|
74
|
+
for name in ("peek", "up", "bonked", "sleeping", "golden"):
|
|
75
|
+
row = getattr(skin, name).rows[-1]
|
|
76
|
+
shared = sum(1 for a, b in zip(rim, row) if a == b and a != " ")
|
|
77
|
+
self.assertGreaterEqual(shared, CELL_W // 3, f"{skin.key}.{name} rim: {row!r} vs {rim!r}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Banners(unittest.TestCase):
|
|
81
|
+
def test_banners_are_ascii_and_fit_80_columns(self):
|
|
82
|
+
for name in ("TITLE", "GAME_OVER", "HIGH_SCORES", "PAUSED"):
|
|
83
|
+
banner = getattr(banners, name)
|
|
84
|
+
self.assertIsInstance(banner, Sprite)
|
|
85
|
+
self.assertGreater(banner.h, 0)
|
|
86
|
+
self.assertLessEqual(banner.w, 78, name)
|
|
87
|
+
self.assertTrue(all(is_ascii_row(r) for r in banner.rows), name)
|
|
88
|
+
self.assertTrue(all(len(r) == banner.w for r in banner.rows), name)
|
|
89
|
+
self.assertLessEqual(banners.PAUSED.w, 44)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class BlockFont(unittest.TestCase):
|
|
93
|
+
def test_all_glyphs_are_five_rows_and_rectangular(self):
|
|
94
|
+
for ch, rows in blockfont.GLYPHS.items():
|
|
95
|
+
self.assertEqual(len(rows), blockfont.GLYPH_H, ch)
|
|
96
|
+
self.assertEqual(len({len(r) for r in rows}), 1, ch)
|
|
97
|
+
self.assertTrue(all(set(r) <= {"#", " "} for r in rows), ch)
|
|
98
|
+
|
|
99
|
+
def test_render_covers_what_the_game_prints(self):
|
|
100
|
+
for text in ("3", "2", "1", "GO!", "TIME'S UP!", "ROUND OVER", "0123456789"):
|
|
101
|
+
rows = blockfont.render(text)
|
|
102
|
+
self.assertEqual(len(rows), blockfont.GLYPH_H)
|
|
103
|
+
self.assertEqual(len({len(r) for r in rows}), 1)
|
|
104
|
+
self.assertEqual(len(rows[0]), blockfont.width(text))
|
|
105
|
+
self.assertEqual(blockfont.width(""), 0)
|
|
106
|
+
self.assertEqual(blockfont.glyph("~"), blockfont.GLYPHS["?"])
|
|
107
|
+
self.assertEqual(blockfont.render("a"), blockfont.render("A"))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
unittest.main()
|