Skip to content

Release v0.2.0 - #15

Merged
NoHudd merged 136 commits into
mainfrom
dev
Sep 30, 2026
Merged

NoHudd merged 136 commits into
mainfrom
dev

Conversation

@NoHudd

@NoHudd NoHudd commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Release v0.2.0 to main. Full notes: the v0.2.0 section of CHANGELOG.md.

Highlights:

  • Capture-the-flag progression (13 main + 5 secret flags; /boot opens at 11), grep and kill, hints
  • Keys earned from flags; hidden doors
  • Main menu: SETTINGS (arrow-driven), LOAD GAME save picker with one slot per run (up to 9), Save & main menu / Main menu without saving, a menu command, game over r/m/q
  • Ctrl+Q mid-fight no longer says "command not found"
  • Fresh README screenshots

Players get a game-only zip from the v0.2.0 release (.gitattributes export-ignores tests/, sim/, CI and dev tooling).

🤖 Generated with Claude Code

NoHudd and others added 30 commits September 8, 2026 07:39
A typo like `take segfault_sheld` used to dead-end at "Cannot find
segfault_sheld in this directory." with no clue whether the mechanic or
the spelling failed. Now every not-found path (take, cat, examine, talk,
drop, equip, cd) appends one line: the close match via difflib, or the
list of what is actually here. cd hints only discovered exits so hidden
rooms stay hidden.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZzvukbvLcLhqtaJMd6KxS
Esc was bound straight to `quit`. At tutorial start there is no
"progress", so quit skipped the confirm screen and the game closed the
instant a player hit Esc to abandon a typo. Now Esc with text in the
prompt clears the prompt, like a shell; Esc on an empty prompt still
goes through the existing quit flow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZzvukbvLcLhqtaJMd6KxS
Remove three pieces of code that could never run:

- `_get_discoverable_hidden_rooms` / `_get_hidden_room_hint` had zero
  callers and had drifted out of sync with the YAML they duplicated:
  they described `opt_mage_tower`/`srv_warrior_tomb` as restricted to
  "mages"/"fighters" (pre-dating the Guardian/Weaver/Shaman rename) and
  called three `hidden: false` rooms hidden. Wired up today they would
  have misled the player. The data they hardcoded already lives in
  data/rooms/*.yml.

- `self.commands` / `self.commands_with_args` were assigned empty in
  __init__ and never populated, so the `elif cmd in self.commands`
  branch in handle_command was unreachable. Phase 3 finished moving
  every verb to `command_registry`; the legacy path was left behind.

Also replace the stale mirror_sector xfail. It claimed the room was
"genuinely unreachable content", but all three of its premises were
false: /proc/self is a path-tree child of /proc (proc_secrets), the
Process Scheduler NPC there grants `sudo_quest_active`, and
`_may_discover` gates `ls -a` on exactly that flag. The bug was in the
test's `_reachable`, which BFS-walks `exits` only — but `cd` does path
arithmetic over the room tree and is not restricted to exits.

Two replacement tests model real navigation instead, and both were
mutation-checked to confirm they fail when the invariant breaks:
reparenting mirror_sector strands it, and dropping the NPC's story_flag
seals it.

Fix two pre-existing quality-gate failures surfaced along the way:
E501 in test_not_found_hints.py and a `str = None` annotation on
_show_error. `make check` now exits 0 (401 passed, 0 xfailed).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpGLtNtiShivwXKf555goM
game_world.py mixed one-shot world generation with the runtime state hit
on every command. The generation half (place_items, place_starter_items
and ~800 lines of _place_* / rarity / zone helpers) only writes
item_locations and item_spawn_counts, so it moves to src/item_placer.py
as ItemPlacer(world) with no back-edge into the runtime.

Seeded placement output is byte-identical before and after for all
three classes plus the unknown-class fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_show_error was the most common private reach-back from src/commands into
CommandHandler (~25 call sites), yet it only writes to the output sink and
logs. It now lives on GameOutput, which every command already has as
ctx.output, so the commands no longer need the handler for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
event_bus and state_manager were process-wide singletons, so any handler
a run left subscribed kept reacting to the next run's events (the
recurring "fight each enemy twice / double loot" leaks), and two engines
could never coexist in one process.

ImprovedGameEngine now creates its bus and StateManager and hands them
down: GameWorld, CommandHandler (commands reach it as ctx.bus),
CombatSession, and the UI via a new UIProtocol.attach_bus(). The module
globals are deleted rather than kept as defaults, so a missed reference
fails at import instead of silently talking to a bus nobody hears.
GameSession exposes .bus; tests use it, and the conftest fixtures that
reset the shared bus between tests are gone.

Verified with the full suite plus a live Textual smoke under Pilot
(menu -> difficulty -> class -> tutorial -> play -> combat by key press).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CommandHandler was the shared junk drawer: commands reached back into
16 of its private helpers, so the src/commands split was structural
only. The helpers now live in five collaborators the handler builds and
exposes to commands:

- ItemResolver (ctx.resolver): shortcut/partial/name lookups, player keys
- ItemEffects (ctx.effects): key/weapon/lore/consumable/upgrade/spell
  use, execute_effect, story flags
- LootService (ctx.loot): enemy drops, gear loot tables, award-once guard
- TutorialCoach (ctx.tutorial): gated hints, current step, tutorial
  reactions to combat events
- GameFlow (ctx.flow): game-over screen, victory/endings, quit confirm

No command touches a ctx._private name any more. The handler keeps
dispatch, event wiring, combat start/end and room presentation
(1,375 -> ~530 lines). Moved method bodies were diffed against the
originals; the only differences are the renames. Also drops the unused
_get_room_status_indicator and adds tests for the death -> game-over
flow, which nothing covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GameWorld.get_enemy used to model_dump the typed Enemy template back into
a dict, so combat, loot, the view builder and the sim read it through
~40 .get("key", default) calls where a typo fails silently. It now
returns a deep model_copy (scaling and a fight both mutate it, and the
shared template must never leak), and every consumer uses attributes.

difficulty.scale_enemy now scales models; before, it returned anything
that was not a dict unchanged, which would have silently switched
difficulty scaling off.

Behavior is unchanged: every enemy YAML sets experience and
description, so no model default changes a value, and seeded
sim.playtest output is identical across all modes and classes. Two
deliberate deletions, both no-ops at runtime:
- the 3x boss-XP branch in combat and sim keyed on boss_room/boss_enemy,
  fields no enemy has ever had, so it never fired
- the unused build_enemy_view / EnemyView

The one visible change: an enemy whose YAML has an empty `dialogue:`
no longer prints "Name: " with nothing after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NPCs were the last content type still handed to the runtime as dicts
(load_npc_data dumped the validated models straight back). They now stay
NPC models end to end; the talk command, auto-dialogue, look and room
views read attributes. on_talk becomes a declared field — five NPCs use
it — so reading it no longer depends on an undeclared extra key.

Verified by talking to all 11 NPCs headlessly: every name and on_talk
message still renders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… saves

Items were the last content type dumped back to dicts at the world
boundary, and the inventory stored those dicts, so ~130 reads across
commands, effects, combat, the placer, the view builder and the sim used
.get("key", default). Now:

- world.get_item and a single data_loader.load_item return private deep
  copies of the typed template (load_item replaces the hand-rolled
  load_weapon_data/load_consumable_data YAML readers; the weapon one
  handed out a shared cached dict)
- Player.inventory holds Item models; to_dict/from_dict convert at the
  save boundary with model_dump(exclude_unset=True), so the save JSON is
  unchanged and existing saves load
- the Item schema declares every field the code reads, with the same
  defaults the .get calls used; keys no code reads stay undeclared extras

Behavior is unchanged, verified three ways: a seeded transcript that runs
every verb on all 41 items for each class (plus combat item use and the
saved inventory JSON) is byte-identical before and after; seeded
sim.playtest and new-run placement output are identical; and a save
written by the old code loads into typed items.

Also removes ItemEffects.equip_weapon: nothing called it, and it passed
Player.equip_weapon an argument it does not take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starts typing src/ in place instead of waiting on a rewrite: difficulty,
events, game_output, game_states, rarity, rng, room_paths and
state_manager now pass mypy --strict and are listed in pyproject, so CI
keeps them typed. Changes are annotations only (plus bool() around enum
comparisons whose __eq__ returns Any).

Config notes, because the obvious setup silently checks nothing:
- explicit_package_bases names src files src.<module>, matching imports
  (src/ has no __init__.py)
- a module-specific override sets follow_imports=normal for the typed
  modules; without it the src.* skip rule turns their imports of each
  other (GameState, EventBus) into Any and every call type-checks
- utils.* is followed silently so debug_log's new signature is seen

Verified by injecting a bad set_state/emit_event call: mypy now flags
both, where before it reported success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
engine.validate proved every id exists, not that content says what the
author meant. Two new checks run in validate (and so in CI):

- find_lock_problems: every locked room names a key, that item is a key,
  the key's unlocks lists the room, and no key claims a room guarded by a
  different key or by nothing
- find_unread_fields: the schema now declares every content field the
  game reads, so any undeclared field is authored intent the game ignores
  (the "phantom reward" bug class). Known unimplemented fields are listed
  in UNIMPLEMENTED_FIELDS; any other undeclared field fails, and so does
  an allowlist entry no content uses any more

To make "undeclared" mean "unread", Room gains enemy_tier, enemy_count
and discovery_requirement, and Ability/Attack gain healing and
enemy_damage_reduction; code already read all five through getattr or
raw dicts.

Tests pin each rule against deliberately broken content, the CLI wiring,
and art coverage: every enemy, NPC and class has a sprite and every room
zone a backdrop, so the scene never falls back to placeholders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The TUI kept the player, inventory, room and combat views as plain
dicts rebuilt from event payloads, and the panels read them with
.get("key", default). Event payloads stay to_dict() dicts (engine
emitters and tests are untouched); the UI now rebuilds typed views at
the boundary with new from_dict constructors and passes those to the
stats panel, inventory panel and scene view. view_models and the two
sidebar panels join the strict-mypy set.

One aliasing dependency made explicit: the scene view used to zero
enemy_health on the combat dict the UI also held, so the UI saw the
defeat by accident. Views are frozen now, so both sides replace() it.

Verified with a scripted Pilot session (title, pickers, play commands,
combat turns, finale, death and game over): all 26 exported
screenshots are identical before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
textual_ui.py still rendered the title screen and main menu, paced the
victory finale, built the combat panel text and the game-over card
itself. Each now lives in its own module the app delegates to:

- title_menu.TitleMenu owns the intro/menu state (typewriter, highlight,
  selection) and key handling; the engine's _display_title_screen hook
  stays on the app as a one-line delegate
- endings.FinaleReveal owns the reveal queue and timers; build_recap and
  game_over_text are plain functions
- combat_log.render_combat_output / hotkey_display are pure functions
  over the typed views, so they join the strict-mypy set with endings

Code and text were moved by extraction, not retyped: every string
constant in the moved methods is byte-identical, and the scripted Pilot
session's 26 screenshots match the previous commit. New tests drive the
menu keys, the typewriter skip, and the finale beats/skip/reduce-motion
paths the screenshot run does not reach.

These stay content in the main output panel rather than becoming
Textual Screens; switching to Screens changes what the player sees and
wants a hands-on play-check first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
QuitConfirmScreen defined its own _render(), which silently overrode
Textual's Widget._render() -- the method that returns what the widget
draws. Ours returned None, so any full repaint of the screen raised
"'NoneType' object has no attribute 'render_strips'" and killed the app.
It showed up after fleeing the tutorial fight, but any repaint could
trigger it. Renamed to _draw().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re Echo

Playtest feedback on the tutorial:

- Equipping the first weapon opened a fight with no instructions. The
  step-4 hint was written before the fight started and the battle panel
  replaced it; it now arrives after, inside the combat log. The battle
  panel's opening view (attack list + selection-mode help) also never
  showed, because it required an empty log and the "combat initiated"
  line is logged immediately; it now shows until the first blow.
- Fleeing the tutorial fight removed the enemy until the player left and
  re-entered the room, while Echo kept saying "press 1" -- a dead end.
  During the tutorial the scripted enemy now stays put and Echo explains
  that flee works and `attack` restarts the fight (new step4_fled hint);
  stray commands at that point repeat it.
- After `take`, Echo spoke before the room re-list; the list now comes
  first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nput

Tutorial hints scrolled through the output with everything else, where
the next room listing or the battle panel could push them out of sight
before the player read them. TutorialCoach now emits a TUTORIAL_HINT
event instead of writing to the output: the TUI shows it in a new
EchoPanel ("ECHO · step N of 6") pinned above the input that stays put,
survives combat, and hides when the tutorial ends; the closing summary
still lands in the output as a record. The headless UI appends the text
to its log, so tests and scripted runs see the same order as before.

Step 1 and the skip summary were shown before start_game(), whose
GAME_STARTED reset wiped them; they now follow it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the screen holds a single prompt (name entry, the tutorial offer),
the text sat in the top-left corner: the centering rule never applied
because the text widget only sized to its content. Selection mode now
centers a bordered card (84 columns, green border, centered text) in the
output panel.

The prompts themselves are reflowed for it: the name prompt loses its
hard line breaks at ~90 columns and ends on a clear question with a
"type it below" hint; the tutorial offer shows its two answers as
styled choices ("yes — show me around", "skip — I know my way").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts at

Playtest: Echo said "See those directories?" in /home — but /home has no
subdirectories, so `ls` there never shows a Directories section and the
player had nothing to look at; they just typed the example. The tutorial
also never covered ps, which in /mnt reveals a hidden chamber.

The second half now teaches where you are before telling you where to go
(9 steps, was 6):

  5. after the fight: ps        -- what's running; odd ones hide secrets
  6. after ps: pwd              -- "you're in /home"
  7. /home is a dead end: cd .. -- go up a level
  8. somewhere new: ls
  9. ls lists directories: cd into one (example = a real listed name)

When step 9 fires, the TUI flashes the Directories section of that ls
output three times and leaves it highlighted (steady with reduce-motion).
The hint carries a "highlight" field; hints can now use {location} and
{example_dir}. Progression moved into TutorialCoach (after_ls, after_ps,
after_pwd, after_move) so commands just report what happened. If the
player wanders or skips ahead, each step adapts: pwd somewhere that has
directories skips "cd ..", and ls in a dead end repeats the way up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
452 -> 354 tests, same coverage of real behaviour:

- One test per table instead of one per row: legacy nav aliases and path
  arithmetic (test_nav), and the per-hint "has text" / "renders without
  placeholders" checks. Failures now list every bad entry at once.
- Dropped low-value tests: GameOutput.error (a three-line method), two
  of three session-isolation tests (kept the leaked-session scenario, the
  real bug), two of three fake-timer finale tests (kept skip).
- Tutorial tests assert which step fired and the tutorial state, not
  Echo's text, so copy edits stop breaking them. The headless UI now
  records each hint (s.ui.hints) for that. The prescribed-commands test
  follows the current tutorial path (ps, pwd, cd .., ls, cd bin).

Checked the rewritten tests still bite: reintroducing the flee dead end
or the take/Echo ordering bug each fails its test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ommand names

Playtest screenshot: after "skip", the tutorial offer stayed on screen
with the quick-reference summary glued underneath, and the summary read
"type use  instead".

- start_game() never cleared the output; the next Echo line used to
  replace it, which stopped once hints moved to the Echo panel. It now
  clears the panel and opens on the room (name + description); the skip
  summary follows after a blank line.
- "[item]"-style placeholders are markup tags to the renderer, which
  silently drops them -- in four tutorial lines, the help screen, eight
  commands' usage errors, the combat hint screen and the enemy-blocking
  message. They are now "<item>" etc. ps shows kernel threads the Unix
  way, in brackets, so those are escaped ("\[kthreadd]") -- they had been
  invisible. `man` printed "[dim]Try: man ls[/dim]" literally (markup in
  a plain Text); it is dimmed now.
- A command word at a name prompt ("quit", "ls", "help", "yes", ...) is
  refused and asked again, instead of becoming the player's name.

Tests: the start clears a stale prompt; no tutorial line or command
output loses a bracketed word; command words are refused. The old
combat-hint test that pinned the broken "use [item]" now checks
"use <item>".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tutorial never had the player read a file, so the checkpoint loop
(cat a lore file -> memory restored -> autosave) went untaught. Two new
steps after the fight: ls -a reveals bash_profile (item hidden: true now
actually hides files from plain ls — it was declared in content but
unread), cat fires the identity memory restore, and Echo names it as the
checkpoint you resume from on death. ps/pwd/nav follow as before;
11 steps total.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skip sets tutorial_state.completed but never equipped_weapon, and the
equip hook only checked the latter: a skipping player's first equip
still spawned the tutorial enemy and opened combat — with the step4
explanation suppressed, since hints no-op once completed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two game-over screen bugs from the architecture review (F4a, F4b), both
from the UI guessing what a GAME_OVER meant:

- On death the room scene reappeared for ~2.6s before the drain. The UI
  hears COMBAT_ENDED before the engine sets GAME_OVER, so the
  is_in_game_over() guard never fired, and the GAME_OVER reset called
  end_battle() again. The UI now reads the event's own data: defeat
  keeps the battle on screen and starts play_death immediately.
- F5 and the post-win "new game"/"restore" choices showed the GAME OVER
  card 2.6s later over whatever came next. GAME_OVER now carries
  reason "defeat" or "restart"; only a defeat shows the card, and a
  restart returns to the title screen.
- The death card said "Unknown Sysadmin": the reset cleared the player
  view it reads. The name is kept for the card.

Tests drive the real engine and TUI together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The event bus logs and swallows a listener's exception, and ViewBuilder
returns a placeholder view when a build fails. Both are right for the
game (a panel degrades instead of crashing) and wrong for tests, where a
crashed handler left a log line and a green test.

EventBus takes strict=True (class default strict_by_default) and
ViewBuilder has raise_errors; tests/conftest.py turns both on for every
test. The game keeps the forgiving defaults.

The full suite still passes strict, so no crash was hiding today;
test_strict_mode pins that the switches stay wired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loading a save quietly changed the run (review findings F1, F2):

- Player.to_dict never wrote equipped_armor or status_effects, so a
  load unequipped your armor (mitigation 0) and dropped active effects.
  Armor is re-equipped on load so mitigation is recomputed from the
  item rather than trusted from the file.
- The run's difficulty was not saved, and mounting the UI reset it from
  user settings, so a Hard run reloaded as Medium. The save now records
  it and both load paths restore it; the UI no longer touches it.

Save format is v4. v3 saves still load: medium difficulty, no armor,
no effects.

Also: everything the game opens (content, saves, logs, settings) is
relative to the working directory, and some of it at import time.
Launched from another directory the game could not find its content and
left empty data/ and saves/ folders behind. main.py now moves to the
repo before importing the game.

test_every_player_field_survives_a_round_trip walks every Player
attribute rather than listing them, so the next field added without
save support fails there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hint-text conflict: kept the new ls -a / cat checkpoint steps, adopted
dev's 'use <item>' spelling (bracketed words now survive markup).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(tutorial): teach ls -a and cat via the memory checkpoint
fix(tutorial): don't ambush skipping players with the scripted fight
Choosing "r" after a win restored the save but rebuilt the command
handler without its event subscriptions (review finding F3): walking
into a room with an enemy started no fight, NPCs said nothing after
combat and tutorial combat hints stopped. It also never told the UI the
run had started (no GAME_STARTED / ROOM_ENTERED) and left the state
alone.

Both load paths now go through _enter_loaded_run(), so neither can skip
a step. Its failure fallback called ui.display_message, which the
Textual UI did not implement (UIProtocol declares it); it does now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NoHudd and others added 29 commits September 30, 2026 13:06
…ins it

- After a revive: "That Legacy Backup is spent and gone from your
  inventory", then how many are left or that the next fall ends the run.
  The three revive sites share Player.revive_text.
- `use legacy_backup` in a fight gives the same "works automatically"
  answer as outside one, instead of "cannot be used in combat". Other
  non-combat items now say "try it after the battle".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tab after cd suggested nothing: it read the room as a dict, the typed
Room model raised, and the error was swallowed. It now asks
GameWorld.visible_exits (also used by cd's "did you mean"), so a hidden
room stays out until ls -a finds it and a locked door until its key
reveals it. Doors complete as their real path (/usr, not the old
/usr/lib/arcane alias), which retires the room-alias plumbing the
suggester alone used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In /bin it printed "Found: /dev/null_void … cd dev_null_void" before the
Sudo Privileges Badge had revealed the door, pointing new players at a
directory they could neither see nor enter. /dev now exists for find only
once its door is visible, from any room (like a real absolute path), and
the hint is the real path: cd /dev. REFERENCE.md no longer lists find as
a way to reveal a hidden directory (it never was one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- data/hidden_rooms_guide.txt: no code loads it, and it was stale (it
  still called /etc a hidden room reached by ls -a).
- data/difficulty.proposed.yaml: sim/tune.py's scratch proposal for a
  human to review; now gitignored so a tuning run can't commit it.
- data_loader.load_abilities_data / get_abilities_for_class: never called.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- health_packet keeps its heal in combat_effects.player_heal, so the
  inventory view's healing was always empty. ViewBuilder._heal_info reads
  heals in combat's order; heal-over-time shows its real total and turns
  (Stable Cache: +39 HP over 3 turns). New view field healTurns.
- AttackView gains kind (attacks.yml type); the fight menu shows
  ⚔ / ✨ / 🌿 instead of the same bullet on every row.
- CHANGELOG entries for this batch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ave on

The class sprite appeared only in battle, and equipped gear only as
"⚡EQUIPPED" tags in a long inventory. The new panel shows the sprite
(idle bob, off with reduce motion) beside the equipped weapon (+DMG) and
armor (-% dmg taken); an empty slot says "type equip <name>". It
redraws with every Stats/Inventory refresh.

Sidebar layout: the inventory now takes the leftover height and scrolls,
so Stats and Character are never clipped on a long inventory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filling the leftover height (b13d08a) kept the Character panel whole but
left a huge empty inventory box early in a run. The inventory now sizes to
its items up to 35% of the sidebar and scrolls past that; the sidebar
itself scrolls on a very short terminal instead of clipping a panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Playtest: a player cleared the map and reached the boss at 26/160 HP with
no way to recover. A capture now refills HP ("✚ +N HP — the recovered
memory has you fully restored"), so the flag hunt is the way back to
health.

The sim models it too. Its deaths all came from attrition across fights,
so it now wins ~100% in every mode (was 94/80/65% avg); any partial
refill does the same. difficulty.yaml carries a dated note that its
measured rates are stale; re-tuning is a separate decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saves from before the keys chain still had chmod_key lying in /var, so
capturing the /var flag later handed out a second one. Loading now clears
flag-granted keys from floors (GameWorld.flag_granted_keys, which the
placer also uses).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typing equip, take, cd, cat… in a fight printed "bash: …: command not
found", which reads like a typo to someone new to the shell. They now
get "<cmd> works once the fight is over. Right now: press 1-9 to attack,
0 to flee, or type use <item>."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…balance

Adds the player-visible changes since v0.1-playtest that weren't listed
(item icons, ✓ marks, room art, F12 logs, loot and save fixes, tutorial
and launcher fixes), today's capture refill, stray-key and mid-fight
command messages, and replaces the old win-rate targets, which the
capture refill made stale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d scrolling"

This reverts commit 16b5961: the inventory fills the leftover sidebar
height again, as the user prefers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sprite grows from 18px to 26px (13 rows). Beside it, a narrow sidebar
cut weapon names short ("Privile…"), so the panel now picks its layout
by width: gear beside the sprite when it has 26 columns, below it
otherwise, with names whole either way. Re-lays out on resize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/legacy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… when full

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…un cleared

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ooser

Ctrl+Q / Ctrl+C / quit mid-fight used to print 'command not found'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A valid-JSON but malformed run file (or a failed old-save migration) is
skipped instead of crashing LOAD GAME and NEW GAME; a run that lists but
fails to load re-opens the picker; saved class/difficulty text is escaped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared mgr/clock fixtures in conftest (the isolated save_manager instead of
per-file managers patched into two modules), no real sleep in the picker
Pilot test, duplicate tests folded (v6 envelope + round trip into
test_saves, save_cap into the one-file test, main-menu-after-death, the
double migration check), palette tests parametrized, a combat fixture,
local imports hoisted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hape)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All five retaken at a full-screen terminal size (240x64, ~1.84:1), the
explore shot through the real start flow, with Fira Code embedded so
GitHub renders the logo and box art like a terminal. New paths under
assets/screenshots/ (sidesteps GitHub's image cache); an HTML table with
equal image widths replaces the markdown table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CHANGELOG dated; README points players at the Releases zip; REFERENCE loses
its Development section; .gitattributes export-ignores dev tooling so
release/source zips contain only the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NoHudd
NoHudd merged commit 41c9223 into main Sep 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant