From 9f54a97cc62938588fdc987cbaa62d59e9d640fb Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Sat, 27 Jun 2026 07:39:25 -0700 Subject: [PATCH] fix(understone): rebalance early game and add inn turn refresh The opening hours were a gold/HP death spiral: a fresh hero spent more gold healing a fight than the kill paid, mid-tier foes out-damaged a starting HP bar, the map gave no read on where harder foes spawned, and a spent day left the player idle until the UTC rollover. This eases the on-ramp across both shipped worlds. Economy - Healer drops from 2 to 1 gold per HP, so topping up no longer outruns income. - Starting purse 20 -> 37: enough to buy the cheapest armor and one potion up front, a one-point DEF bump plus a heal cushion the player chooses to spend. Combat - Tier 2-4 common foes lose 1 ATK each, trimming the burst that could halve or end a fresh hero in a single bout. Rares and the boss are untouched. Wayfinding - The road glyph changes from "=" to a shaded path that reads as one continuous road in every orientation; "=" only looked right horizontally and broke into stacked dashes on vertical runs. Cinder's basalt path gets the same treatment with a crosshatch glyph free in its palette. Rest - Sleeping at the inn now rolls a spent adventurer into a fresh day's turns (it already fully heals). The top-up fires only at zero turns, so it never banks past the daily cap. Verified: full suite green (+2 rest tests, ruff/mypy clean); the greedy balance bot still clears the world 11/12 seeds at an unchanged pace on both packs. --- examples/door-game/tests/test_game.py | 29 +++++++++++++++++++ examples/door-game/understone/game.py | 26 ++++++++++++----- .../understone/world/data/monsters.json | 12 ++++---- .../understone/world/data/terrain.json | 2 +- .../understone/world/data/world.json | 4 +-- .../world/packs/cinder-wastes/monsters.json | 12 ++++---- .../world/packs/cinder-wastes/terrain.json | 2 +- .../world/packs/cinder-wastes/world.json | 4 +-- 8 files changed, 66 insertions(+), 25 deletions(-) diff --git a/examples/door-game/tests/test_game.py b/examples/door-game/tests/test_game.py index a76fa53b..51d0957e 100644 --- a/examples/door-game/tests/test_game.py +++ b/examples/door-game/tests/test_game.py @@ -161,6 +161,35 @@ def test_rest_heals_and_charges(tmp_path: Path, clock: object) -> None: assert "full health" in out.lower() +def test_rest_when_spent_restores_a_fresh_days_turns(tmp_path: Path, clock: object) -> None: + """Sleeping at the inn with no turns left rolls into a fresh day's allowance.""" + game = _game(tmp_path, clock) + game.join("Brandr") + player = game.players["Brandr"] + daily = game.world.settings.daily_turns + player.turns_left = 0 # spent for the day + player.hp = 5 + game.move("Brandr", "", "west", 2) # step into the inn + out = game.action("Brandr", "rest", "", "") + assert player.turns_left == daily # a fresh day's turns restored + assert player.hp == player.max_hp # and fully mended + assert f"/{daily} ]" in out # footer reflects the refreshed budget + + +def test_rest_with_turns_in_hand_never_inflates_the_budget(tmp_path: Path, clock: object) -> None: + """Resting mid-day mends but adds no turns — the top-up only fires at zero.""" + game = _game(tmp_path, clock) + game.join("Brandr") + player = game.players["Brandr"] + daily = game.world.settings.daily_turns + player.turns_left = daily - 3 # turns still in hand + player.hp = 5 + game.move("Brandr", "", "west", 2) # step into the inn + game.action("Brandr", "rest", "", "") + assert player.turns_left == daily - 3 # unchanged: no farming past the cap + assert player.hp == player.max_hp # but the heal still lands + + def test_fight_spends_a_turn_and_credits(tmp_path: Path, clock: object) -> None: game = _game(tmp_path, clock) game.join("Brandr") diff --git a/examples/door-game/understone/game.py b/examples/door-game/understone/game.py index c5627598..d8cf5e77 100644 --- a/examples/door-game/understone/game.py +++ b/examples/door-game/understone/game.py @@ -1015,16 +1015,28 @@ class Game: return self._overworld_frame(player, lines=["You step back out into the open air."]) def _rest(self, player: Player) -> str: + # Settle any pending day rollover first, so a rest taken as the first act + # of a new day is the ordinary refresh, not the spent-turns top-up below. + self._ensure_day(player) cost = self.world.settings.rest_cost - if leveling.rest(player, cost): - # A night's rest is a private errand, not Herald news; persist only. - self._persist(player) + if not leveling.rest(player, cost): return self._location_menu( - player, lines=[f"You sleep deeply and wake at full health. (-{cost} gold)"] + player, lines=[f"You can't afford the {cost}-gold bed. (You have {player.gold}.)"] ) - return self._location_menu( - player, lines=[f"You can't afford the {cost}-gold bed. (You have {player.gold}.)"] - ) + # A night at the inn always mends. Once the day's turns are spent it also + # rolls the sleeper into a fresh day's allowance, so a spent adventurer can + # press on rather than idling until the dawn rollover. The top-up only + # fires at zero, so it never banks turns past the daily cap. + line = f"You sleep deeply and wake at full health. (-{cost} gold)" + if player.turns_left <= 0: + player.turns_left = self.world.settings.daily_turns + line = ( + f"You sleep through to a new dawn, waking at full health " + f"and ready to venture out anew. (-{cost} gold)" + ) + # A night's rest is a private errand, not Herald news; persist only. + self._persist(player) + return self._location_menu(player, lines=[line]) # -- the Vault: bank gold at the inn (safe from ambush) -------------- diff --git a/examples/door-game/understone/world/data/monsters.json b/examples/door-game/understone/world/data/monsters.json index e49d98b6..0dc4eac6 100644 --- a/examples/door-game/understone/world/data/monsters.json +++ b/examples/door-game/understone/world/data/monsters.json @@ -21,7 +21,7 @@ "tier": 2, "name": "Goblin", "hp": 12, - "atk": 5, + "atk": 4, "def": 1, "xp": 18, "gold": 7 @@ -30,7 +30,7 @@ "tier": 2, "name": "Bandit Scout", "hp": 14, - "atk": 6, + "atk": 5, "def": 1, "xp": 20, "gold": 9 @@ -50,7 +50,7 @@ "tier": 3, "name": "Forest Wolf", "hp": 20, - "atk": 8, + "atk": 7, "def": 2, "xp": 35, "gold": 14 @@ -59,7 +59,7 @@ "tier": 3, "name": "Bog Stalker", "hp": 22, - "atk": 9, + "atk": 8, "def": 2, "xp": 38, "gold": 16 @@ -79,7 +79,7 @@ "tier": 4, "name": "Cave Troll", "hp": 38, - "atk": 12, + "atk": 11, "def": 4, "xp": 70, "gold": 30 @@ -88,7 +88,7 @@ "tier": 4, "name": "Barrow Wight", "hp": 35, - "atk": 13, + "atk": 12, "def": 4, "xp": 65, "gold": 28 diff --git a/examples/door-game/understone/world/data/terrain.json b/examples/door-game/understone/world/data/terrain.json index 13b7c2a4..a1173f54 100644 --- a/examples/door-game/understone/world/data/terrain.json +++ b/examples/door-game/understone/world/data/terrain.json @@ -22,7 +22,7 @@ }, "=": { "key": "road", - "glyph": "=", + "glyph": "▒", "walkable": true, "encounter_rate": 0.02, "color": "road" diff --git a/examples/door-game/understone/world/data/world.json b/examples/door-game/understone/world/data/world.json index 0c24ff9b..114a9251 100644 --- a/examples/door-game/understone/world/data/world.json +++ b/examples/door-game/understone/world/data/world.json @@ -100,8 +100,8 @@ "settings": { "daily_turns": 10, "rest_cost": 15, - "heal_cost_per_hp": 2, - "starting_gold": 20, + "heal_cost_per_hp": 1, + "starting_gold": 37, "starting_weapon": "rusty_dagger", "starting_armor": "cloth_tunic", "start_hp": 20, diff --git a/examples/door-game/understone/world/packs/cinder-wastes/monsters.json b/examples/door-game/understone/world/packs/cinder-wastes/monsters.json index 62318f8b..d3ccd244 100644 --- a/examples/door-game/understone/world/packs/cinder-wastes/monsters.json +++ b/examples/door-game/understone/world/packs/cinder-wastes/monsters.json @@ -21,7 +21,7 @@ "tier": 2, "name": "Ember Imp", "hp": 12, - "atk": 5, + "atk": 4, "def": 1, "xp": 18, "gold": 7 @@ -30,7 +30,7 @@ "tier": 2, "name": "Slag Scuttler", "hp": 14, - "atk": 6, + "atk": 5, "def": 1, "xp": 20, "gold": 9 @@ -50,7 +50,7 @@ "tier": 3, "name": "Magma Hound", "hp": 20, - "atk": 8, + "atk": 7, "def": 2, "xp": 35, "gold": 14 @@ -59,7 +59,7 @@ "tier": 3, "name": "Obsidian Lurker", "hp": 22, - "atk": 9, + "atk": 8, "def": 2, "xp": 38, "gold": 16 @@ -79,7 +79,7 @@ "tier": 4, "name": "Basalt Golem", "hp": 38, - "atk": 12, + "atk": 11, "def": 4, "xp": 70, "gold": 30 @@ -88,7 +88,7 @@ "tier": 4, "name": "Ashen Wraith", "hp": 35, - "atk": 13, + "atk": 12, "def": 4, "xp": 65, "gold": 28 diff --git a/examples/door-game/understone/world/packs/cinder-wastes/terrain.json b/examples/door-game/understone/world/packs/cinder-wastes/terrain.json index ffadd9ab..e329c330 100644 --- a/examples/door-game/understone/world/packs/cinder-wastes/terrain.json +++ b/examples/door-game/understone/world/packs/cinder-wastes/terrain.json @@ -22,7 +22,7 @@ }, "=": { "key": "basalt", - "glyph": "=", + "glyph": "▩", "walkable": true, "encounter_rate": 0.02, "color": "road" diff --git a/examples/door-game/understone/world/packs/cinder-wastes/world.json b/examples/door-game/understone/world/packs/cinder-wastes/world.json index 22b7af0f..73c6fef3 100644 --- a/examples/door-game/understone/world/packs/cinder-wastes/world.json +++ b/examples/door-game/understone/world/packs/cinder-wastes/world.json @@ -100,8 +100,8 @@ "settings": { "daily_turns": 10, "rest_cost": 15, - "heal_cost_per_hp": 2, - "starting_gold": 20, + "heal_cost_per_hp": 1, + "starting_gold": 37, "starting_weapon": "charred_shiv", "starting_armor": "scorched_rags", "start_hp": 20,