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.
This commit is contained in:
Patrick Buckley
2026-06-27 07:39:25 -07:00
parent 0e1972e6aa
commit 9f54a97cc6
8 changed files with 66 additions and 25 deletions
+29
View File
@@ -161,6 +161,35 @@ def test_rest_heals_and_charges(tmp_path: Path, clock: object) -> None:
assert "full health" in out.lower() 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: def test_fight_spends_a_turn_and_credits(tmp_path: Path, clock: object) -> None:
game = _game(tmp_path, clock) game = _game(tmp_path, clock)
game.join("Brandr") game.join("Brandr")
+19 -7
View File
@@ -1015,16 +1015,28 @@ class Game:
return self._overworld_frame(player, lines=["You step back out into the open air."]) return self._overworld_frame(player, lines=["You step back out into the open air."])
def _rest(self, player: Player) -> str: 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 cost = self.world.settings.rest_cost
if leveling.rest(player, cost): if not leveling.rest(player, cost):
# A night's rest is a private errand, not Herald news; persist only.
self._persist(player)
return self._location_menu( 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( # A night at the inn always mends. Once the day's turns are spent it also
player, lines=[f"You can't afford the {cost}-gold bed. (You have {player.gold}.)"] # 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) -------------- # -- the Vault: bank gold at the inn (safe from ambush) --------------
@@ -21,7 +21,7 @@
"tier": 2, "tier": 2,
"name": "Goblin", "name": "Goblin",
"hp": 12, "hp": 12,
"atk": 5, "atk": 4,
"def": 1, "def": 1,
"xp": 18, "xp": 18,
"gold": 7 "gold": 7
@@ -30,7 +30,7 @@
"tier": 2, "tier": 2,
"name": "Bandit Scout", "name": "Bandit Scout",
"hp": 14, "hp": 14,
"atk": 6, "atk": 5,
"def": 1, "def": 1,
"xp": 20, "xp": 20,
"gold": 9 "gold": 9
@@ -50,7 +50,7 @@
"tier": 3, "tier": 3,
"name": "Forest Wolf", "name": "Forest Wolf",
"hp": 20, "hp": 20,
"atk": 8, "atk": 7,
"def": 2, "def": 2,
"xp": 35, "xp": 35,
"gold": 14 "gold": 14
@@ -59,7 +59,7 @@
"tier": 3, "tier": 3,
"name": "Bog Stalker", "name": "Bog Stalker",
"hp": 22, "hp": 22,
"atk": 9, "atk": 8,
"def": 2, "def": 2,
"xp": 38, "xp": 38,
"gold": 16 "gold": 16
@@ -79,7 +79,7 @@
"tier": 4, "tier": 4,
"name": "Cave Troll", "name": "Cave Troll",
"hp": 38, "hp": 38,
"atk": 12, "atk": 11,
"def": 4, "def": 4,
"xp": 70, "xp": 70,
"gold": 30 "gold": 30
@@ -88,7 +88,7 @@
"tier": 4, "tier": 4,
"name": "Barrow Wight", "name": "Barrow Wight",
"hp": 35, "hp": 35,
"atk": 13, "atk": 12,
"def": 4, "def": 4,
"xp": 65, "xp": 65,
"gold": 28 "gold": 28
@@ -22,7 +22,7 @@
}, },
"=": { "=": {
"key": "road", "key": "road",
"glyph": "=", "glyph": "",
"walkable": true, "walkable": true,
"encounter_rate": 0.02, "encounter_rate": 0.02,
"color": "road" "color": "road"
@@ -100,8 +100,8 @@
"settings": { "settings": {
"daily_turns": 10, "daily_turns": 10,
"rest_cost": 15, "rest_cost": 15,
"heal_cost_per_hp": 2, "heal_cost_per_hp": 1,
"starting_gold": 20, "starting_gold": 37,
"starting_weapon": "rusty_dagger", "starting_weapon": "rusty_dagger",
"starting_armor": "cloth_tunic", "starting_armor": "cloth_tunic",
"start_hp": 20, "start_hp": 20,
@@ -21,7 +21,7 @@
"tier": 2, "tier": 2,
"name": "Ember Imp", "name": "Ember Imp",
"hp": 12, "hp": 12,
"atk": 5, "atk": 4,
"def": 1, "def": 1,
"xp": 18, "xp": 18,
"gold": 7 "gold": 7
@@ -30,7 +30,7 @@
"tier": 2, "tier": 2,
"name": "Slag Scuttler", "name": "Slag Scuttler",
"hp": 14, "hp": 14,
"atk": 6, "atk": 5,
"def": 1, "def": 1,
"xp": 20, "xp": 20,
"gold": 9 "gold": 9
@@ -50,7 +50,7 @@
"tier": 3, "tier": 3,
"name": "Magma Hound", "name": "Magma Hound",
"hp": 20, "hp": 20,
"atk": 8, "atk": 7,
"def": 2, "def": 2,
"xp": 35, "xp": 35,
"gold": 14 "gold": 14
@@ -59,7 +59,7 @@
"tier": 3, "tier": 3,
"name": "Obsidian Lurker", "name": "Obsidian Lurker",
"hp": 22, "hp": 22,
"atk": 9, "atk": 8,
"def": 2, "def": 2,
"xp": 38, "xp": 38,
"gold": 16 "gold": 16
@@ -79,7 +79,7 @@
"tier": 4, "tier": 4,
"name": "Basalt Golem", "name": "Basalt Golem",
"hp": 38, "hp": 38,
"atk": 12, "atk": 11,
"def": 4, "def": 4,
"xp": 70, "xp": 70,
"gold": 30 "gold": 30
@@ -88,7 +88,7 @@
"tier": 4, "tier": 4,
"name": "Ashen Wraith", "name": "Ashen Wraith",
"hp": 35, "hp": 35,
"atk": 13, "atk": 12,
"def": 4, "def": 4,
"xp": 65, "xp": 65,
"gold": 28 "gold": 28
@@ -22,7 +22,7 @@
}, },
"=": { "=": {
"key": "basalt", "key": "basalt",
"glyph": "=", "glyph": "",
"walkable": true, "walkable": true,
"encounter_rate": 0.02, "encounter_rate": 0.02,
"color": "road" "color": "road"
@@ -100,8 +100,8 @@
"settings": { "settings": {
"daily_turns": 10, "daily_turns": 10,
"rest_cost": 15, "rest_cost": 15,
"heal_cost_per_hp": 2, "heal_cost_per_hp": 1,
"starting_gold": 20, "starting_gold": 37,
"starting_weapon": "charred_shiv", "starting_weapon": "charred_shiv",
"starting_armor": "scorched_rags", "starting_armor": "scorched_rags",
"start_hp": 20, "start_hp": 20,