Repository navigation
Releases: VirInvictus/Hearthfall
Release list
v0.28.0
v0.28.0 (2026-09-15)
The final blitz release: the game becomes losable, and the skin becomes
playable. The 2026-09-13 final audit's verdict was that the engineering
contracts are pristine and the play experience is the finding: under the
shipped skin, the game could neither be played nor lost. This release fixes
exactly that, gives the Ring an honest opening, lands the corpus campaign's
first batch, and pays the audit's comment-truth debts.
The terminal loop is gone, in the one lane the audit said it should be.
run_until_interrupted stops on a shortfall forecast before resolving, and
with the shipped all-zero standing orders and a stub editor it stopped
forever: no turn resolved, no deaths occurred, and GAME_OVER was unreachable
through the skin. Two things land together. The standing-orders editor is
real: five labour lines against the adults the clan actually has, the
rationing policy, and the forecast rendered live underneath, which is the
engine's own turn.forecast (produced, demand, net, idle hands, the
shortfall in red). The same forecast now sits in the rail in play, so the
allocation is decided against numbers instead of against a guess. And the
stop has a way through: "accept the shortfall and resolve anyway" resolves
one season on the standing orders, deaths and all, recorded exactly as a
background season would be (turn.resolve_and_record is the driver's
per-season ritual, named and shared). A winter the orders cannot fix no
longer ends the run by hanging it. The pilot tests lose an all-zero run end
to end; the skin can lose now.
The Ring waits to be earned. It shipped gating on households >= 3, true
from turn zero, at weight 100 against everyone else's 1: it fired as the
first event of nearly every run while its own comments claimed the
conditions kept it locked. The gate is year >= 2 and people >= 10 (the
founding clan is eight), the comments tell the truth, and corpus tests pin
year-one immunity and the growth floor structurally. Measured honestly on
the way: a household-count gate is unreachable inside a twenty-season run
because hearths barely split, so the gate prices growth in people.
Also in the skin: --seed 0 is a seed now, not a typo for random; Save and
Load while a question is up dismiss the question instead of swapping state
out from under the answer; saves are a stamped envelope, so a save from
another format of the game is refused at load instead of unpickling and
dying seasons later on a slot it never had; --replay --seed N plays one
run headless and prints the annotated chronicle, season by season,
allocation, forecast, events, and answers (the standing gate's reading, as
a command instead of an edited test); the chronicle pane renders event
bodies at last; and the number keys reach options four through nine.
The corpus grew from ninety to a hundred and two. The batch includes a
second take-in route (a man with a sled, distinct from the woman at the
treeline) repairing the doctrine payoff chain that the Ring fix squeezed,
and two gate widenings measured against the reachability harness:
debts.another_at_the_edge's food > 18 never once coincided with a
taken-in stranger across fourteen hundred ladder runs (it reads > 12
now), and the buried-enough event sat one grave above every run's ceiling
(it keys on two graves now). The liveness guard holds over all 102.
The honesty pass. The director's comment claimed an enforcement backed by
nothing ("the fact MUST have existed for at least a few turns") and a
random draw that did not exist ("50% of the time" was deterministic
turn-parity); it now describes the real mechanism (the massing announcement
is what makes the raid learnable, and the low-slack hold is the calendar,
not a coin), and the never-used rng parameter is gone from
Director.evaluate. The band-economy numbers that lived inline in
agents.py moved to balance.py, which is where its own header contract says
they belong. The three unexplained type: ignores are resolved the house
way (runtime imports and parameterised factories; chronicle's cycle broken
with a lazy annotation). turn.py's mid-file imports are hoisted and the
driver sits above the helpers banner. The rest of the audit's comment-truth
set is swept, and the audit's rendered-prose recasts are done: no em-dashes
or spaced-hyphen surrogates left in the docs.
GitHub: the pushed v0.27.0 tag was rewritten (annotated force-push,
Brandon-approved) to carry both entries that release shipped, verified with
git cat-file tag before pushing, and the v0.27.0 Release, the repo's
first, was created from the corrected tag. Topics stand at exactly the
20-topic cap, the description carries the deterministic hook, and the wiki
is off. CI gained workflow_dispatch and a 10-minute timeout; *.pkl is
ignored. The audit's removal candidates were asked (check_emergence,
Intent.target, and the rest) and the answer did not come back, so nothing
was deleted: each is marked in code, and the gate stays open in the
roadmap.
12 new tests; suite at 427; pyright strict zero. The textual floor is
raised to the tested 8.2, lock re-run in this commit.
v0.27.0
v0.27.0 (2026-09-13)
The audit release: the skin answers its events again. The 2026-09-12
six-lens audit found one HIGH and a handful of MEDIUMs; this release
repairs all of them and files the economy campaign the arc was waiting on.
The HIGH was a soft-lock. run_until_interrupted returns EVENT when the
world asks a question, and since the v0.13.0 skin rebuild nothing
answered: no modal rendered state.pending, nothing called
apply_choice, and the next run re-interrupted forever. The Phase 0
roadmap box still claimed an event modal; the claim was stale, not the
test. The modal is back (EventChoiceScreen): the engine-formed question
with every option and endorsement, answered by number key or click, the
answer appended to the chronicle, and every interrupt now says why the run
handed control back, including the director's, which used to stop the run
in silence. Escape steps back over the modal to read the chronicle; the
question is waiting when you come back. The skin has tests now, driven
through Textual's own pilot: a real fired event renders, choosing lands,
the run advances.
Two determinism repairs ride with it. The forecast divided the store with
share_out while resolution divides with _divide, so the two disagreed
exactly when a hoarding hearth took its share first: at five food under an
even split the forecast promised three deaths and resolution buried two.
Forecast uses _divide now, and the parity suite gained the case its
zero-resentment fixtures could never see. And the three household
tie-breaks (new hands, new children, deaths) broke ties on id(h), a
memory address that survives neither a restart nor a save; they break on
the stable household id now, and test_a_save_replays_exactly pins the
property end to end: pickle a run mid-stream, revive it, and the seasons
after the load must match the uninterrupted twin.
Save/load grew up. Saves went to savegame.pkl in whatever directory the
game was launched from; they now live at one fixed path
(~/.local/state/hearthfall/savegame.pkl), written through a temp file
and a rename so a crash cannot tear them. Loading refuses a missing or
corrupt save with a line in the chronicle instead of a traceback, and
rebuilds the pane from the loaded chronicle instead of showing the old
run's seasons above the new ones. The architecture guard pins the
engine's I/O boundary now: no open(), no storage-flavored imports, the
two content loaders named as the only exceptions.
And the campaign is filed. The economy re-tune boxes exist as real
checkboxes under SP 8 (coordinated levers, 50-seed gate-read measurement,
targets to Brandon before any ship verdict), and Box 1 is measured: the
per-season food-flow baseline ledger, recorded in the roadmap with its
reading: the game is a sawtooth that winters reset, and the campaign is
pricing a winter/yield pair, not a raid or rot problem.
Also: the standing-orders gap the audit caught in the README is now stated
in the README (the palette runs on engine defaults; the editor is not
built), five jobs instead of four, ninety corpus entries instead of
eighty-seven, the works in "The turn", the phantom data/ tree corrected
in spec and README, and run_tests.sh runs through uv because the
skin's new tests need Textual. 12 new tests; suite at 415; pyright
strict zero.
Retrospective, recorded 2026-09-13: SP 4 and SP 5, shipped unrecorded
Sub-project 4 (neighbours and the director) and sub-project 5 (the ring)
shipped on 2026-08-31, between v0.13.0's TUI rebuild and the v0.13.1
hygiene release, in commits 2521313 and 61ae8ed: no version bump, no
patchnotes entry, no tag. The 2026-09-12 audit's docs lens found the gap;
this entry is the record, retroactive, and changes nothing about code,
versions, or tags.
SP 4, neighbours and the director. engine/agents.py put neighbours
on the map as agents with state, needs, and intents (weather and wildlife
are typed in the module but were not populated): bands hold grain and
mood, form raid intents when miserable, and the honesty guarantee is
enforced in the shape of the thing: an intent requires a learnable fact,
so the massing was always knowable before it arrived. engine/director.py
paces and nothing else: it chooses which
already-justified intent surfaces now and when to break standing orders,
never inventing a threat (spec §9.10), and every interrupt lands
traceable in the chronicle.
SP 5, the ring. The named cast arrived, drawn from households exactly
as spec §5 requires: people with traits, ambitions, relationships,
ageing, and death; advisors whose agendas are wrong in characteristic
ways; council decisions rendered inline in the chronicle;
engine/tiers.py for emergence conditions and the named moment; and
corpus entries keyed on ring composition and household standing.