Custom Roguelike - 9/19/26
Published 2026-09-21
9/19/26 — Balance Ledger and Class Tuning, Map-Gen Fixes, and the Town/Quest/Blade Redesign (Phases 1-2)
Why the bot runs took WSL down, and the results ledger
Running the sharded bot at full size disconnected the session: the whole WSL VM restarted (exit 137, fresh boot in dmesg). Reproduced it with 8 processes and 40 runs per row and added a per-process memory check to find the cause: each process’s memory grew from ~150MB past 1.5GB, within a run and across the runs a shard does. Cause: the HUD systems the bot runs call DrawBatch::submit, which pushes into bracket-terminal’s global COMMAND_BUFFER - only the real renderer ever drains it, so headless it grew forever. Fixed by calling clear_command_buffer() every bot iteration; peak memory is now ~13MB per process, and 500 runs take about 2 minutes. SIM_MAX_RSS_MB (default 1500) stays as a safety net that aborts a run instead of letting one shard eat the machine. (Side effect worth knowing: any batch of results from the leaking builds that reported many “crashed” rows was the guard/leak, not the bot.)
Every report run now appends to two files: target/sim_log.txt (a timestamped line per shard start/finish and per job, plus memory/load snapshots - survives a crash, not committed) and balance_tests/results.jsonl (committed): one JSON line per run with commit, dirty flag, scenario, every SIM_* setting, per-row counts, the win-rate totals and the raw per-run outcome codes. No JSON crate was added; the lines are written by hand. New knobs: SIM_ROUTES (default ChestWeapon only - what an informed player does and what the target bands measure) and SIM_SNEAK (off by default).
Stalled runs: mostly map bugs, plus a bot fix
About 3% of runs stalled for the full 25000 actions (~50s each). The new stall log showed why: the ground weapon sitting on an impassable tile, an Amulet inside a Wall, and Exit/Amulet tiles with no path from the start. The bot now skips optional stops it can’t reach or has spent 600 moves on, and ends a run early as a new Stuck outcome (its own report column, code S) when the exit itself can’t be reached. Result at 100 runs per class: 0 timeouts, 0 crashes, 4 stuck, 5 weapon-in-wall anomalies. Logged as backlog item 16 (a real map-gen bug, not fixed - bot-only by agreement).
Compare tool, and the agreed balance goal
balance_tests/compare.py compares two ledger runs (--list to see them, or two run numbers, default the last two): each win rate with a 95% interval, and “real?” YES only when the change clears the noise. Agreed target with the user: 70-85% win rate for every class, on the ChestWeapon route (an informed player takes the chest and the fortress weapon; later quests will gate the weapon behind kills/an item plus talking to the quest giver in the shop, so that logic stays as is). Allowed levers: class numbers, starting kits, enemy numbers. Once the numbers are there the user plays it themselves before any quest work starts.
The bot now uses the Hunter’s and Amazon’s out-of-combat kit
Their kits were being ignored by the bot (Hunter: Shoot + 2 Freeze Traps, Amazon: Trap), so those classes looked weaker than they are. The bot now fires Shoot at any enemy in view within 8 tiles and drops a Trap/Freeze Trap when one is within 4 (SIM_USE_KIT=0 turns it off). At 200 runs per class: Amazon 42% -> 52%, Hunter 41% -> 42% (noise), others unchanged - so the kit helps Amazon and barely moves Hunter.
Class stat tuning: health is the lever
Sweeps in balance_tests/test.ron (hp_a … hp_e; a class’s win rate only depends on its own stats, so one scenario tunes several classes at once). Start (before): Barbarian 81, Mage 64, Rogue 24, Amazon 52, Hunter 42. Each extra HP is worth roughly +10-18 points on the low classes (15 HP Barbarian is already ~82; Mage 12 -> 13 HP went 72 -> 90). Landed on hp_e, verified at 500 runs per class: Barbarian 84, Mage 80, Rogue 79, Amazon 82, Hunter 80, 81% overall. Applied to classes.ron: Mage 10 -> 12 HP and 0 -> 5 evasion, Rogue 10 -> 13 HP, Amazon 12 -> 13 HP, Hunter 12 -> 14 HP. Barbarian unchanged. Enemies and kits untouched. Not yet checked: that a human playing these classes feels the same (the user plays it next), and defense/evasion sweeps (defense_1, evasion_up) are in test.ron but were not needed.
Backlog additions
Item 17: an enemy debuff tracker (icons like the player’s buff tracker, instead of text with the turns left). Item 18: move the whole balance simulation out of screens/battle.rs into its own test-only module tree - it only ended up there because the Arena bot started there and needs battle.rs’s private items; to be done after the tuning.
Enemy debuff tracker (item 17)
New branch enemy-debuff-tracker. An enemy’s debuffs now draw as icons under its name with the turns left as a red number (like the player’s buff column), replacing the red “garrote (3t)” text; Stun had no display before. ActiveStatus::Stun gained a label (its technique’s name, “Stun”/“Feint”) so the icon can be looked up - Dot already carried its (lowercased) name, so there is a new case-insensitive glyph lookup. Layout is enemy_debuff_slots, checked with a throwaway geometry test (removed after): it caught a real bug on its first run - converting the number’s pixel position to a HUD column with plain integer division rounds down and can start the number up to ~11px left of the icon’s right edge, under the icon’s opaque glyph; fixed with a ceiling. The player’s own buff numbers use the same floor conversion and may have the same small issue (not changed). Not verified live: needs a screenshot of a battle with a Dot and a Stun active. Item 17 moved to Done; the module-move item is now 17.
Balance simulation moved into src/balance_sim/ (item 18)
New branch move-balance-sim. The whole simulation left screens/battle.rs (3545 -> 1905 lines): balance_sim/{mod,dungeon_bot,runner,ledger,arena_bot,overrides}.rs, compiled only for tests; test_overrides.rs moved in as overrides.rs and the crate::test_overrides:: paths in the game’s test-only hooks were updated. It was in battle.rs only because the Arena bot started there; the battle methods it calls were already pub(crate), so nothing in the game needed to change. The sharded runner finds its own test by module_path!(), so it still works from the new location (balance_sim::runner::class_survivability_report). Doc references (CLAUDE.md, test.ron, comments) updated. Verified by running the identical 2500 real-value runs on the commit before the move (in a throwaway worktree) and after: 81% overall both times (2023 vs 2017 wins) and every class within noise. (A first attempt at the “before” run reported 100% crashes - not a code problem: the worktree had no target/ folder for the shards’ result files. Worth knowing if the sim is ever run from a fresh checkout: it writes to target/ relative to the crate folder.) Note the 78% “after tuning” baseline in the ledger was a low draw; repeated runs at those settings land at 78-81%, so the true rate is about 80%. The compare.py “real?” flag is a 95% test, so across six comparisons one false YES is expected now and then - trust it more for bigger, repeated differences.
Battle log overflow, and the compiler warnings
New branch battle-log-overflow. Checked both candidates from the backlog: the row count was already fine (log trimmed to 4 messages, 4 text rows), the width was the bug - messages up to 46 characters in a box about 26 wide ran past the right border. New wrap_log_rows word-wraps each message to the box interior, indents continuation rows, and keeps the most recent rows that fit, dropping whole older messages rather than cutting one in half. A throwaway test on the real long messages caught a bug on its first run (a single very long word split into continuation rows that were 2 characters too wide once indented, because the width limit was computed once per word instead of per row) - fixed, test removed. The 26-character interior comes from the panel’s text inset, not a measurement, so it needs a screenshot of a long message. Also made the 4 build warnings go away: deleted map_builder/empty.rs (EmptyArchitect, a tutorial leftover nothing built), removed PanelBox‘s never-read width/height fields, allowed the intentionally-unused Forest/Sewer panel theme variants (they map to real bands in ui_panels.png), and scoped the test-only mut in Templates::load. Dev, test and release builds now have zero warnings.
Map-generation bugs (item 16) - fixed
New branch fix-map-gen-bugs. Measured before touching anything, with a throwaway test over 6000 seeded MapBuilder::new floors: 3% had a serious problem (walled-in guards 92, amulet/exit walled in 33 or unreachable 17, weapon walled in 30 or unreachable 17, player start walled in 13, no weapon/chest placed 11/9, 6 generation panics). Root cause in map_builder/prefab.rs: prefabs are stamped as rectangles of walls onto an existing map with no check afterwards - the chest room could overwrite the fortress’s weapon spot, a rectangle could cover the amulet (the placement check passed if ANY one tile was far enough), and a wall ring could cut a corridor. The fix is a verified build: layout_is_sound checks everything the level depends on, each stamp is snapshotted and undone/re-rolled if it fails, a placement can no longer cover the start or amulet, find_most_distant returns the start instead of panicking on a boxed-in start, and MapBuilder::new rebuilds the floor (up to 30 tries) until it has a usable start/amulet and both prefabs. 20,000 seeded floors after: 0 problems; map generation costs about 0.65ms per floor. The same test is removed (per the project rule) - it would make a good permanent regression test if you want one kept. Bot, 2500 real-value runs: 0 stuck, 0 crashed, 0 timed out, no anomalies in the sim log; win rate 82% overall, classes 81-84% (that ledger line is from a tree with only this change uncommitted, so it says dirty). Two new backlog items from the user: 16 the Debug class turning into any class on a key press with 5 of every ability, 17 building ability icons with the Pixellab API.
Debug class: switch class with a key press
New branch debug-class-switch. In a run that started as Debug, B/R/A/H/W(Mage - M is the Item Menu)/D(back to Debug) turn the player into that class in place, with 5 of every ability/technique it can carry; floor, position, weapons and potions stay, no turn used. A new DebugClassSwitch marker (added to the player in spawn_player when the class is Debug) is what enables it, so it keeps working after the first switch instead of ending when the class changes. The old class’s ability/technique items (carried, class-restricted, not weapons) are removed first; switching back to Debug first gave 6 of each cheat item (its cheats are already “abilities”, and I was also adding its kit on top) - the throwaway test caught that, fixed. The test ran the real input schedule (the runtime-only component-access failure class in CLAUDE.md), then was removed. Keys skip anything bound to movement. Not tried on screen or in the Battle Arena (it shares the input system, so it should work there too).
Merged to master
The user finished the in-game checks on all six stacked branches (debuff tracker, battle log wrapping, map-generation fix, class stat changes, Debug class switch, Stun/Feint/damage-over-time, Battle Arena and menus all fine) and added 3 Dungeon Maps to the Debug class’s starting kit. Final checks on the code as merged: cargo build (dev and release) and cargo check --tests with zero warnings, cargo test passing, and a 1000-run real-value bot report: 81% overall (Barbarian 90, Mage 80, Rogue 76, Amazon 80, Hunter 81), 0 stuck, 0 crashed, 0 timed out. Master was a direct ancestor of the top branch, so it was a plain fast-forward - nothing needed rebasing. Merged locally only (not pushed).
Backlog: new potions, quests and the gold economy
From the user: item 17 (Speed Potion +50% Speed for 20 attacks, Defense Potion +50% Defense for 20 attacks against the player) and item 18 (talk through quests first, then shop items and prices - Dungeon Crawl gives far more gold than there is to spend: a chest is 30-50 gold, a kill 5, and the shop stocks 41 gold worth). The Battle Arena stays exactly as it is until its bot is ready (noted under item 1). Checked the bot for the user’s question about new items: it only buys Healing Potions and has rules only for the items it already knows, so every new item needs a bot rule; and the sim’s battles ignore Speed (simplified turn order, no ATB gauge), so a Speed Potion is invisible to it until Speed is modeled.
Town, quests and the Blade ladder - design captured (no code yet)
The user described a large redesign: a fixed town map (fountain, blacksmith, shop, a house per townsperson) where each run starts and returns after every dungeon; townsfolk quests (kill X -> potions, kill X + prefab item -> weapon upgrade from the Blacksmith, find a special item -> potions/Map from a townswoman, find a scroll -> up to 5 abilities from the Elder, find an item -> better shop stock/prices from the Shop Keeper), one quest at a time, proximity shows the quest; the fortress weapon pickup goes away and chests hold only a Map plus 1-2 potions; the player starts with no money; the finish of the 3rd dungeon level is the Blade of Death, recorded in persistent data; 5 Blades unlock the hardest dungeon; the final boss unlocks Endless; and a new menu: Title -> Main Game / Battle Arena / Options, with Main Game showing Dungeon Crawl, 1..5 Blades and Endless as they unlock. The Battle Arena and its shop stay exactly as they are. Everything is written down in docs/quests_town_redesign.md: the requests as given, what the code does today (checked), my assumptions, 20 numbered open questions, the art list, and a 7-phase draft plan (persistence + menu flow, town map and loop, townsfolk and quests, items and economy, tiers/Blade/final boss/Endless, balance-sim catch-up, art). Key fact found: the weapon is currently mandatory for balance (the informed route wins ~80%, without the weapon ~0-7%), so moving it to a quest reward re-derives every class’s balance. Waiting on the user’s answers before any code.
Answers recorded in the design doc so far: ladder = Crawl + 1-5 Blades + Endless (5 Blades dungeon = final boss, difficulty levels = Crawl and 1-4 Blades); one dungeon = one floor with a town between each; chests are the only gold source (no kill gold) and hold a Map, 1-2 potions and gold; town buildings are exteriors only; every class starts with a tier-1 weapon and the Blacksmith quest upgrades it; Blades are per save file for now (maybe per class or class unlocks later); death ends the run and returns to the title; quest items spawn only while their quest is active; quests span trips and can be abandoned in town; floor 3’s existing boss guards the Blade; the Elder’s scroll gives up to 5 uses of one ability usable in or out of combat; town = fixed Shop Keeper and Blacksmith plus 1-2 random of Elder/townswoman/townsman. The doc also has default answers for the remaining details and a 7-phase build order (progress + menu, town + loop, townsfolk + quests, items + economy, tiers + Blade + final boss, balance sim, art), each on its own branch merged into an integration branch town-quests.
Added to the design doc’s art phase (and to backlog item 16): re-look at all the existing animations, since Pixellab has a new animation generator - class idle/walk and battle sheets, enemy sheets and the shopkeeper - as a review pass first, ideally before the townsfolk animations are integrated so every character shares one style.
Clarified with the user: the Elder’s scroll is not a new dual-use ability - it grants 5 copies of ONE ability the player could already win from a battle (a technique or an out-of-combat item, whichever is picked), so it can reuse grant_random_battle_loot‘s class pool and needs no Ability Bar / Battle Bar redesign. Removed that risk from the design doc.
Town/quest redesign, Phase 1: progress save and the new menu flow
Branches: integration branch town-quests (off master) and town-phase1-progress-menu (off it). New src/progress.rs: Progress { blades, endless_unlocked } in saves/progress.ron (loaded fresh like Stats, #[serde(default)] so a hand-edited or older file still loads, a missing/corrupt file means a fresh player), DungeonTier (Crawl, Blades(1..5), Endless) and available_tiers() - Dungeon Crawl always, “N Blades” for N up to the Blades owned (nothing else shown before the first Blade), Endless once unlocked. Menu: Title -> Main Game / Battle Arena / Options (Options returns to this menu); Main Game -> new TurnState::DifficultySelect listing the unlocked dungeons -> class select. Battle Arena is untouched (menu -> class select as before). Class select now has an Esc that goes back to the right screen (it had none). The pick is a DungeonTier resource inserted by start_game; nothing reads it yet (the tier’s enemy stats, the Blade reward, Endless itself and Progress::save are Phase 5). Checked with a throwaway test (unlock rules for 0/2/5 Blades, more Blades than exist, hand-edited partial/empty/garbage files, RON round trip) - removed; zero warnings in dev/test builds; the game launches with no panic. Held-Enter hand-off is guarded on the new screen the same way class select does it. Not verified live: the screens themselves - needs the user to click through (Progress(blades: 2) in saves/progress.ron shows the unlocks).
Phase 1 screenshot fix (from the user’s first look): the difficulty screen’s first row (HUD row 18) printed straight over its header, which is on the 32px-row big-text console (bottom edge y=224, about HUD row 19) - rows now start at 26 - and the header reads “Choose Your Adventure” instead of “Choose Your Dungeon”. Debug functionality re-checked in the code: the hidden D key on class select (both Main Game and Arena branches, and the theme picker), start_game("Debug"), the DebugClassSwitch marker and the class-switch keys in the dungeon are all untouched.
Town redesign, Phase 2: the town and the town/dungeon loop
Branch town-phase2-town-loop (off town-quests). The user asked for the town to be much bigger than the shop (at least about 40x40), built from a template, with a proper tile set and good-looking buildings: a well at the centre, an anvil for the blacksmith, a shop - and reminded me a tile set is 16 cells unless a second set is used. So: map_builder/town.rs holds a 60x40 template (legend at the top; every tile’s variant is set explicitly, no random variants) with buildings assembled from roof/wall/window/door pieces, a plaza and well, roads, gardens, trees, the shop with its counter, the blacksmith’s spot and anvil, and three house spots; TownTheme (delegates to Forest for now); MapBuilder::new_town. State::build_shop_room now takes a ShopLayout (ArenaShop or Town) so the Arena’s shop is untouched; enter_town builds the town; start_game starts every non-Debug run there (Debug still starts on floor 1); dungeon_shop_transition now leads to the town instead of the small shop; advance_level skips the level increment when leaving the run’s first town (in_start_town). The town keeps the old shop’s stock (2 items, bought with kill gold - the economy rework is Phase 4). Throwaway test (removed): a run starts in the town with 0 enemies and the stock on the counter, walking to the exit enters level 0, the second town leads to level 1, Debug skips the town, and the bot completes runs as all five classes; the bot already walks to the counter to shop, so the balance report is unchanged (84% overall vs 81%, 0 stuck/crashed). Also wrote the town tile set spec into the design doc (two 16-cell blocks: terrain, and buildings/props; the engine support for a second block is not built yet). Placeholder art: buildings/roofs/well show Forest cells. Not verified live - needs the user to walk around it.
The user has the townsfolk animations ready: per townsperson Idle Stance, Quest Available and Quest Complete, each facing North/South/East/West (12 per person, 60 for the five). Recorded in the design doc (art table + Phase 3): the quest state picks the animation, the facing follows the player when near. Also wrote the draft tile-set prompts for the town (two 16-cell blocks: terrain, buildings; plus a props list) into the doc - roofs come as upper/lower rows per colour, windows/doors/anvil/well are transparent prop sprites over a wall/floor tile.
Town tiles integrated; townsfolk and class animations received
The user walked the town (the size is right) and delivered both tile blocks and five townsfolk zips. Tiles: extended resources/map_tiles.png to 25 rows (block A terrain rows 17-20, block B buildings rows 21-24; rows 0-16 pixel-identical), added MapTheme::extra_tile_row so a theme can own a second block (Floor/Wall variants 8+ pick from it; components/tiles.rs), gave TownTheme both blocks plus the archway as the Exit and the bench as the Counter, and moved the town template to the real cells (roofs split into an upper and lower row, a plank apron in front of the shop counter, tilled soil, bushes/stumps/boulders). Block A came out as specified; block B’s generator ignored the numbering for three cells (dark slate roof in slot 9, stone archway in 15, bench in 16), so its cells were reordered into the right wall/floor slots (documented in the design doc). A throwaway test confirmed every one of the town’s tiles resolves to a real cell in rows 17-24 (22 distinct cells) and the exit/counter cells are right; the game launches without a panic; zero warnings. Props (house doors/windows, anvil, well, signs) do not exist yet - stand-ins for now (anvil = boulder, plaza centre = the pond). Townsfolk: five people (Elder, Blacksmith, Young Child, Townswoman, Townsman - no Shop Keeper set), 3 animations x 4 facings x 9 frames each, 44x44 canvases (Elder 40x40); analysis and open questions are in the design doc. Class animations the user will supply: Barbarian Walk and Death, Rogue Walk, Hunter Attack, Amazon everything - recorded in the design doc’s art phase as the replacement list. (The code/atlas commit for this work went in before its docs because of a script slip; the docs followed in their own commit.)
Answers from the user: the Shop Keeper is the Arena’s and keeps his existing idle-selling animation (no quest animations); the Young Child has no quest (a random townsperson to talk to - the no-quest people will need a few flavour lines); everyone faces South by default. Drafted “Block C - Village details” in the design doc: 16 impassable tile cells with their ground baked in (house door, shuttered window, flower-box window, dark slate lower roof, well, anvil, forge front, shop sign wall, blacksmith sign wall, barrels, crates, hand cart, lantern post, signpost, striped awning, dark slate roof with chimney) plus optional walkable extras; a third tile block will map Wall variants 16-31 to it, built when the art arrives.
Town polish: Block C, a redesigned layout, and the wall-shade fix
The user’s screenshot: the town looked sparse and less polished than the rest of the game. Findings: big empty plaza, flat grass, doorstep tiles floating alone, dark squares around bushes/stumps/boulders, no doors or windows. Cause of the squares: not the atlas (the grass baked into those cells matched the plain grass to within 1 colour unit) but WALL_TEXTURE_SHADE (0.72) darkening every wall-type tile, grass included - now MapTheme::wall_texture_shade(), 1.0 for the town. Block C (village details, 16 impassable hero cells with their ground baked in) arrived and matched its numbering exactly; integrated as a third tile block (atlas rows 25-28, MapTheme::props_tile_row, Wall variants 16-31); the lantern/signpost backgrounds were recoloured to the plain grass. Layout redesign: written with a small generator (kept out of the repo) and checked for reachability before going into town.rs: main street + two cross streets (cobblestone), a compact plaza with the well, the shop with awning/sign/counter and a market square, the blacksmith with slate roof/chimney/forge/anvil, six houses (doors, shuttered and flower-box windows, alternating red/blue roofs), fenced plots and flower beds, lanterns, signposts, a gate with low stone walls and deterministic clover/pebble grass variety. New dev tool tools/town_preview.py renders the whole town from town.rs and the atlas without launching the game, which is how the layout was iterated. Throwaway test (removed): all 2400 tiles resolve into rows 17-28 (41 distinct cells), the exit is still the archway, the town loop and the bot’s runs still work (4 of 5 classes won, Rogue died on floor 1, no stuck/crashed). Zero warnings; the game launches. Not verified on screen - needs the user’s eyes again.
Town: props-on-walls bug, more varied buildings, and the townsfolk are in
The user’s next screenshots: (1) grass patches sitting on the lower houses’ walls around lanterns and signs - a generator bug of mine (the lanterns, signposts and a flower bed were placed after the houses without checking the tiles were free, and those cells carry grass baked in, so they overwrote wall tiles); now every decoration only goes onto open grass and the generator verifies no prop ends up inside a building. (2) “The buildings need some love”: the houses now vary in width (5-8) and height (2 or 3 wall rows), alternate red and blue roofs, some have a striped porch awning over the door, the blacksmith is a compact stone smithy with a chimney, forge and sign and a small yard with one anvil and two ash tiles instead of a field of identical ash piles. The rest needs new art - drafted “Block D - building pieces” prompts in the design doc (roof ends, chimneys on red/blue roofs, corner posts, stone foundation, plain plaster, two more house colours). (3) “Add in the other townsfolk”: built. The five townspeople from the zips are on the shopkeeper’s sheet (shopkeeper_idle.png, 128px cells x 9 columns, now 64 rows: rows 0-3 his, rows 4-63 the townsfolk - 5 people x 3 animations x 4 facings x 9 frames, documented in the glyph-map doc), reusing the existing Shopkeeper renderer path (no new consoles or fonts). New Townsperson component, TownsfolkKind/TownsfolkAnimation, idle_frames_for_townsperson, spawn_townsperson; MapBuilder::town_townsfolk_spots reads the template’s K/H markers; enter_town spawns the Blacksmith at his anvil and the Elder, Townswoman, Townsman and Young Child on the doorsteps (facing South). They are solid: systems/movement.rs treats a townsperson like a wall (new #[read_component(Townsperson)] - the runtime-only legion pitfall - covered by running the real schedule in a throwaway test). Throwaway test (removed): 5 townsfolk spawn with the right frame ranges, none of the 60 x 9 sheet cells is empty, the player walking at the blacksmith stops adjacent and never on his tile, the Shopkeeper is still there, and the bot still completes runs (Barbarian and Mage won, Hunter died on floor 1). Zero warnings, the game launches. Not verified on screen. Left for the quest work: random 1-2 of Elder/Townswoman/Townsman (plus the child), the quest-available/complete animations, facing the player, proximity and the quest panel.
Town: blacksmith above his anvil, shopkeeper beside the goods
From the user’s screenshots: (1) the blacksmith should stand above the anvil when facing south - he was below it, with the anvil behind him; now K is directly above A (the anvil tile is one south of him). (2) The shopkeeper stood directly behind the potions, where the player can’t reach him; he needs to be beside them so the player can stand next to him to accept and turn in quests. The counter is now 4 tiles with the shopkeeper’s spot (S) just past its right end, level with the counter and reachable from the plank apron in front of him; the 2 stock items sit at the counter’s right end so he is right beside the last one (the player stands in front of the potions to buy, in front of him to talk). He is solid in the town: new Obstacle marker (replaces the townsperson-specific check in systems/movement.rs; townsfolk carry it too), added to the shopkeeper only for the town layout - the Arena’s shopkeeper (behind an impassable counter) is unchanged. Throwaway test through the real schedule (removed): the shopkeeper is on the counter’s row, the player walking at him stops adjacent and never on his tile, the tile below the blacksmith is impassable (the anvil), and the bot still completes runs. The user said the rest of the buildings look fine. Zero warnings; the game launches.
Town: full-length counter again, and the blacksmith’s smithy reworked
The user preferred the full-length counter with the shopkeeper behind it, plus a wider “close enough to accept a quest” check for him (recorded as a requirement for the proximity panel: normal radius for townsfolk, larger for the shopkeeper). So: the counter is 6 tiles again with the shopkeeper in the pocket behind its RIGHT side and the two potions at its LEFT end (beside him, not directly in front of him); the town shopkeeper no longer needs the Obstacle marker (removed - it stays on the townsfolk). The smithy: the forge front is now directly above the blacksmith, a plaster door at the right end of the front wall (with a doorstep), the hanging anvil sign was removed from the wall and a signpost stands on the ground to the left of the building. Throwaway test (removed): 6 counter tiles, the shopkeeper on the row behind them, the stock starting at the counter’s left end with the shopkeeper further right, every counter has walkable apron in front, and the bot still completes runs. Zero warnings; the game launches.