ty joins the dev dependency group, locked at 0.0.89, and a local
hook runs `uv run ty check` over the whole project whenever Python
files change. [tool.ty.src] limits the check to pqcli/ and tests/,
so agent worktrees under .claude/ are not checked as duplicate
copies of the project.
ty reported 20 diagnostics, and 0.0.89 adds a few more; this clears
them all without suppressions.
- The quest monster is held in a local before being stored on the
quest book, so its name no longer reads from an Optional.
- PgUp/PgDn narrow the focused window with isinstance against a
runtime-checkable PageScrollable protocol instead of hasattr.
- The ^t cheat used to monkeypatch SignalMixin.emit. SignalMixin now
has a muted() context manager that emit() checks, and it restores
emission even if a tick raises.
- GameView keeps its children as GameWindow, a protocol declaring
stop() and sync(), so the cheat's resync type-checks.
- curses.ascii is imported explicitly where it is used.
- ProgressBarWindow is now Focusable itself, instead of calling
Focusable.focus_standout() on a class that is not Focusable. Its
four subclasses drop the duplicate Focusable base; the MRO and
initialisation order are unchanged.
- MainMenu narrows the Optional pad before colouring the logo.
- The tests that record tasks assert player.task is set.
- Context managers are annotated as Generator rather than Iterator,
which ty flags as deprecated for @contextmanager.
- The redundant cast() around getch() is gone.
- The frozen GameData test assigns through setattr(), since the
direct assignment is an error by design.
Bug fixes:
- ListBox.select(None) crashed on `None < 0`; it now clears the
selection, as DataTable.select(None) already does.
- ListBox.get() caught ValueError, which indexing never raises; it
now catches IndexError, so an out-of-range index returns None as
intended.
`just test` now fetches Progress Quest web's game logic from the public
site into $XDG_CACHE_HOME/pqcli/pq-web, pinned by SHA-256 so a change on
the site fails loudly. Nothing from upstream is committed.
tests/upstream/harness.js runs that code headless. test_upstream.py
checks the word lists, MonsterTask and the other generators from 200
generator states, and three whole games of 10000 tasks each, comparing
all state including the generator after every task. Without node or the
fetched files, these tests are skipped.
Upstream calls CompleteAct() when it dequeues the plot task, so the act
number, the plot bar and the act rewards change as "Loading Act N"
begins. pqcli did all that when the task ended. The random sequence is
the same either way, so the parity traces are unchanged; only the 1-2
seconds of the loading task differ.
Python 3.10 is the minimum, so annotations use `X | None`, `list[X]`
and friends, enforced by ruff's pyupgrade rules. The remaining typing
names are imported directly, with abstract types coming from
collections.abc. first() is now generic over the item and default.
prek replaces pre-commit as the hook runner and reads the same
config. A new commit-msg hook in .prek/ rejects subjects over 50
characters, subjects without a module prefix, and body lines over
72 characters. Install the hooks with `uv run prek install`, and run
them with `just lint`.
ruff format keeps black's style at the same line length, and ruff
check covers import sorting plus pycodestyle and pyflakes. Three
comments and a test regex are wrapped to fit 79 columns.
pqcli used Python's global Mersenne Twister, seeded once from the clock.
Upstream uses Johannes Baagøe's Alea, with Random(n) = uint32() % n, and
keeps the generator state with each character. The port matches the JS
output bit for bit, including UTF-16 seeding of non-BMP characters.
Each Player now owns an Alea that is pickled with the save, and
Simulation.tick draws from it. Saves without one get a fresh generator.
As upstream does, a new character's game continues from the state the
kept stat roll was made from.
The parity traces are re-recorded, since every random number changes.
The word lists (monsters, spells, equipment, races, classes, titles and
so on) now live in pqcli/data/*.json instead of pqcli/config.py. A
GameData object loaded at startup is passed to the simulation and UIs.
Users can replace any list by putting NAME.json in
$XDG_CONFIG_HOME/pqcli/data/ or a directory given with --data-dir.
--dump-data DIR copies the built-in files out for editing. Malformed
files are reported and pqcli exits before starting the UI.
Combat task descriptions come from the new combat_verbs list, with
{target} standing for the enemy. The built-in list holds only "Executing
{target}", and a single verb consumes no random roll, so stock gameplay
is unchanged. Recorded traces in tests/fixtures pin this down.
Monster, Race and Class stay in pqcli.config so existing saves still
load.
Resolves#3