Commit Graph
212 Commits
Author SHA1 Message Date
Dash 2896b54a69 docs: update the changelog
Lists the changes since 1.1.0.
2026-10-17 19:59:24 +02:00
Dash 9ceda9b873 ui/curses: wait for keys instead of polling
The main loop slept 100 ms whenever no key was pending, so keys took
up to a tick to register and every screen woke ten times a second.
Each view now declares how often it needs idle(): the game view keeps
its 100 ms tick, while menus wait for a key. getch() blocks for that
long, so keys register at once, and over 15 s in the roster menu the
process goes from about 300 wake-ups to 6.
2026-10-17 19:57:28 +02:00
Dash c9bce751ac build: ignore coverage data
Coverage runs leave a .coverage file in the working tree.
2026-10-17 19:54:09 +02:00
Dash 9689372e25 tests: run in parallel with pytest-xdist
pytest now runs with -n auto; pass -n0 to run tests one by one. The
pty harness starts pqcli through subprocess instead of pty.fork(),
which is unsafe in the threaded xdist workers, and the child claims
the pty as its controlling terminal so ^C still raises SIGINT.
2026-10-17 19:53:53 +02:00
Dash d1d7c2d9df tests: play shorter games when comparing
The upstream games ran 10000 tasks and the .pqw comparisons 3000 or
2000 more. Differences so far showed within the first few tasks, so
3000, 1000 and 500 keep the tests sharp at a fraction of the time.
Two .pqw save points that tested nothing new are dropped.
2026-10-17 19:53:51 +02:00
Dash c169102c32 pqcli: reject --load-save 0 and negative numbers
`if args.load_save:` treated 0 as unset, so the menu opened instead,
and a negative number indexed from the end of the roster, so -1
played the last character. Both now list the characters and exit
like any other number out of range.
2026-10-17 19:51:19 +02:00
Dash 60d2c54a88 tests: drive the curses UI in a pseudo-terminal
tests/pty_app.py starts pqcli with pty.fork at a fixed size, with
XDG_CONFIG_HOME in a temporary directory, and feeds its output to a
pyte screen. Tests send keys and poll the screen until it matches,
counting a match once the output pauses or the match has held for a
moment, so they never read a half-drawn frame and need no fixed
sleeps.

The tests cover the roster menu, creating a character (names, races,
classes, rolling and unrolling stats, and every way back), the game
view's panes, focus moves and page scrolling, deleting, importing and
exporting .pqw files, resizing the terminal, the cheat keys, Ctrl+C
and --load-save. They are skipped on Windows.

pyte does not draw the DEC line drawing characters, so the app runs
with NCURSES_NO_UTF8_ACS=1 to draw its boxes with Unicode lines.
2026-10-17 19:46:06 +02:00
Dash f8c370cc2e tests: cover the basic UI
Drive the line-based UI in-process: input() answers come from a list
and the output is captured. The tests walk the main menu through
creating, playing, querying, deleting, importing and exporting
characters, plus bad menu input and the Ctrl+C handler.

The play loop runs on a fake clock that advances a minute per call and
stops when time.sleep has been called a set number of times, so it
levels up and saves on schedule without waiting.

The test helpers move to tests/helpers.py, which now also holds the
input() feeder.
2026-10-17 19:46:05 +02:00
Dash 083dbe01c2 tests: cover the roster and the command line
The roster tests pin down the .old backup rotation and the five minute
periodic save, with a fake clock in place of datetime.

The command line tests call main() in-process with the save and data
paths in a temporary directory, so coverage counts them. They cover
listing, loading, importing and exporting saves, dumping and loading
word lists, and the first-run character creation. The curses UI is
swapped for a stub there so a stray code path fails instead of taking
over the terminal.

--load-save 0 falls through to the menu and --load-save -1 plays the
last character instead of being rejected; both are marked xfail.

A shared fixture restores pytest's SIGINT handler after the UIs
install their own.
2026-10-17 19:46:05 +02:00
Dash 92d4466f6d build: add pyte and measure subprocess coverage
pyte is a VT100 emulator; the curses tests will run pqcli in a
pseudo-terminal and read the rendered screen through it.

Coverage now also measures the pqcli processes the tests start, so the
curses UI and the existing CLI subprocess tests count towards the
report.
2026-10-17 19:46:04 +02:00
Dash dde49cf2dd build: run hooks and tests in GitHub Actions
The lint job runs the prek hooks (ruff and ty) on all files. The test
job runs pytest on Python 3.10 to 3.14 with node available, after
fetching the web version's code for the upstream comparison; the
download is cached, and if it fails those tests skip themselves.
2026-10-17 19:16:02 +02:00
Dash 8d523a4af8 build: update locked packages
xdg-base-dirs 6.0.3 and windows-curses 2.4.2. The development tools
and the ruff hook were already at their latest versions.
2026-10-17 19:15:33 +02:00
Dash 2fef41ca5e pqw: restore state through public methods
The importer assigned private attributes of QuestBook, Inventory and
SpellBook. Each now has a restore() method that sets its state
wholesale, and the importer uses those.
2026-10-17 19:14:46 +02:00
Dash 7573807808 build: drop the unused urwid dependencies
Nothing imports urwid or urwid-readline since the interfaces moved to
plain curses, yet every install pulled both in.
2026-10-17 19:13:32 +02:00
Dash 4dd2cdf1ad misc: release 1.1.0 1.1.0 2026-10-17 18:56:55 +02:00
Dash 56a954e6c5 build: drop pre-commit from runtime dependencies
The Docker commit added pre-commit to [project] dependencies, so every
install of pqcli pulled it in. It is a development tool, and the hooks
run through prek from the dev group now.
2026-10-17 18:56:54 +02:00
Dash cee0626827 docs: add a changelog
Lists the unreleased changes since 1.0.4, grouped the way the TRX
changelog groups them.
2026-10-17 18:56:54 +02:00
Dash 804b0d8b38 ui: import and export .pqw files from the menus
The basic interface gets two main menu entries that ask for a path.
The curses roster gets [I] Import and [E] Export, with a new text input
view for the path, prefilled with NAME.pqw on export, and a message
view for the outcome and any warnings. The README explains how to move
characters to and from the web version.

Resolves #21
2026-10-17 15:56:03 +02:00
Dash 612b223a59 pqcli: add --import-save and --export-save
--import-save FILE adds the character in a .pqw file to the roster;
it can be given more than once, and if any file fails, nothing is
imported. --export-save NUM FILE writes a character out. Export never
overwrites an existing file. The file handling lives in pqw.read_file
and pqw.write_file, so the interfaces can share it.
2026-10-17 15:56:02 +02:00
Dash 0beb550548 pqw: import and export web saves
A .pqw file is what Progress Quest web's roster page downloads: base64
of one character's JSON. pqcli.pqw converts between that and Player,
keeping the generator state, so a character carries on exactly as it
would have where it came from. Saves from before 2022, which were not
UTF-8, are read as Latin-1, and upstream's two spell renames are merged
the way its loader does. Online realm fields are dropped with a
warning.

The upstream tests check both directions against the real JS: a save
made by upstream, at a task boundary or mid-task, is imported and
played on for 3000 tasks next to upstream; then the same the other way.
2026-10-17 15:56:02 +02:00
Dash fced929d77 lingo: add to_arabic, the inverse of to_roman
Ported from upstream's toArabic, including the T, MT, A and MA
numerals. Web saves store spell levels as roman numerals, so the .pqw
importer needs it.
2026-10-17 15:56:01 +02:00
Dash ed5c5cbdc3 tests: type queued tasks in the upstream bridge
Player.queue holds BaseTask instances, so the bridge's queue encoder
takes BaseTask too. ty flagged this once it covered the tests.
2026-10-17 15:56:00 +02:00
Dash fd4a7d151d misc: replace Any with precise types
Curses screens, windows and pads are typed as curses.window, and the
widgets that may get no parent window take curses.window | None.

- EventHandler is generic over a ParamSpec, so each handler declares
  the arguments it passes, and MenuView is generic over the item its
  on_confirm hands out. Choice callbacks take no arguments.
- Scrollable is generic over its items, which DataTable and ListBox
  fix to tuple[str, str] and str.
- random.choice() and choice_low() are generic over the sequence, so
  the casts at their call sites are gone. BasicUserInterface.menu()
  and util.first() are generic too.
- Signal callbacks and their payloads are Callable[..., object] and
  object, since emit() only forwards them.
- JSON values in game_data are object and narrowed by isinstance,
  and data files are Traversable.
- Signal handlers take an int and a FrameType | None, and
  list_players() takes a TextIO.
- The focus_standout() context managers return
  Generator[None, None, None].

Any remains in three places: Player.__setstate__, whose pickled
__dict__ holds values of every type; Focusable.__init__, a mixin that
passes *args and **kwargs through to the next class; and the values
load_game_data() collects, whose element type depends on the field
each one fills.
2026-10-17 15:55:59 +02:00
Dash 00794aca37 build: type-check with ty in a pre-commit hook
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.
2026-10-17 15:55:58 +02:00
Dash 9155ef418b misc: fix all ty diagnostics
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.
2026-10-17 15:55:58 +02:00
Dash 0c1d5185a4 tests: compare pqcli with upstream in node
`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.
2026-10-17 15:55:57 +02:00
Dash 9ad3bee59b mechanic: start acts as their loading task does
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.
2026-10-17 15:55:56 +02:00
Dash 0a4358e2b8 misc: drop typing as T, use builtin generics
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.
2026-10-17 15:55:55 +02:00
Dash ada8d5b6ed build: switch hooks to prek, check 50/72
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`.
2026-10-17 15:55:55 +02:00
Dash 4d620bc384 build: replace black and isort with ruff
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.
2026-10-17 15:55:51 +02:00
Dash 9c44d69633 random: port Alea, one generator per character
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.
2026-10-17 15:55:50 +02:00
Dash f1cdff8140 mechanic: fill the plot bar during the Prologue 2026-10-17 15:55:49 +02:00
Dash c25f9dc057 lingo: extend roman numerals past 4000 2026-10-17 15:55:49 +02:00
Dash 48f48aec58 mechanic: keep at most 100 quests in the log 2026-10-17 15:55:48 +02:00
Dash fa31045052 lingo: use "an" before words starting with Ü 2026-10-17 15:55:47 +02:00
Dash 0f1100d4be mechanic: sync impressive guys and cinematics 2026-10-17 15:55:46 +02:00
Dash 9942091af7 mechanic: cap new item wins on huge inventories 2026-10-17 15:55:46 +02:00
Dash 17e1b91a18 mechanic: clump stat gains on prime stats only 2026-10-17 15:55:45 +02:00
Dash ab179c1eb0 mechanic: shorten quests as upstream does 2026-10-17 15:55:44 +02:00
Dash bec6171483 mechanic: use upstream's exponential level curve 2026-10-17 15:55:43 +02:00
Dash f03394f680 tests: add a recipe to re-record parity traces 2026-10-17 15:55:43 +02:00
Dash b69231f321 docs: update install and contributing for uv 2026-10-17 15:55:42 +02:00
Dash 1a6cd720ac build: move dev dependencies to dependency-groups 2026-10-17 15:55:41 +02:00
Dash c0c6c8aeba mechanic: move word lists to data files
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
2026-10-17 15:55:41 +02:00
Dash 5337d94f82 build: add justfile 2026-10-17 15:55:29 +02:00
Dash da60c6f792 build: remove poetry.lock 2026-10-17 15:55:28 +02:00
Jakub StastkaandJakub Stastka 696fad78b3 build: add Docker builds
Co-authored-by: Jakub Stastka <jakub@stastka.org>
2026-04-19 09:58:04 +09:00
Dash 7426924170 misc: release 1.0.4 1.0.4 2025-12-11 14:59:18 +01:00
Dash 9609e9a3f2 misc: fix style issues 2025-12-11 14:58:55 +01:00
Dash 7e52a74065 misc: update author info 2025-12-11 14:58:55 +01:00