Spaces:
Runtime error
Runtime error
| """Tests for the Phase-2 RPG-depth systems (rpg_plan.md): | |
| leveling 1-20, monster tiers, loot/reforge economy, crits.""" | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) | |
| from rune_goblin import story, world # noqa: E402 | |
| def _cum_xp(target_level: int) -> int: | |
| total, lvl = 0, 1 | |
| while lvl < target_level: | |
| total += story.xp_to_next(lvl) | |
| lvl += 1 | |
| return total | |
| def test_xp_curve_landmarks(): | |
| assert story.MAX_LEVEL == 20 | |
| assert _cum_xp(5) == 64 | |
| assert _cum_xp(8) == 154 | |
| assert _cum_xp(10) == 236 # tier-2 evolution lands here | |
| assert _cum_xp(16) == 638 # king-eligible | |
| assert _cum_xp(20) == 1088 | |
| def test_level_rewards_cover_every_level(): | |
| # Every level 2..20 grants at least +1 max HP plus its milestone. | |
| for lvl in range(2, 21): | |
| assert lvl in story.LEVEL_REWARDS | |
| assert story.LEVEL_REWARDS[lvl].get("max_hp", 0) >= 1 | |
| assert story.LEVEL_REWARDS[10]["evolve_tier"] == 2 | |
| assert story.LEVEL_REWARDS[8]["crit"] == 10 | |
| def test_apply_xp_reaches_level_10(): | |
| level, xp, rewards = story.apply_xp(1, 0, 236) | |
| assert level == 10 | |
| assert xp == 0 | |
| assert len(rewards) == 9 # levels 2..10 | |
| def test_tier_kill_xp_and_respawn_halving(): | |
| assert story.xp_for_kill("minion", 1) == 4 | |
| assert story.xp_for_kill("standard", 2) == 10 | |
| assert story.xp_for_kill("elite", 5) == 34 | |
| assert story.xp_for_kill("standard", 2, respawned=True) == 5 # halved | |
| def test_tier_stat_formulas(): | |
| assert world._tier_hp("minion", 1) == 5 | |
| assert world._tier_hp("standard", 2) == 12 | |
| assert world._tier_hp("elite", 5) == 37 | |
| assert world._tier_dmg("minion", 6) == 1 | |
| assert world._tier_dmg("standard", 4) == 3 | |
| assert world._tier_dmg("elite", 6) == 5 | |
| def test_monster_tiers_applied_in_world(): | |
| cav = world.AREAS["caverns"] | |
| enemies = [e for e in cav.entities if e.type == "enemy"] | |
| assert any(e.tier == "elite" for e in enemies) | |
| assert any(e.tier == "minion" and e.unique is False for e in enemies) | |
| # every combat entity is tier-stamped with a melee value and display level | |
| for e in enemies: | |
| assert e.tier in ("minion", "standard", "elite") | |
| assert e.dmg >= 1 | |
| assert e.level >= 1 | |
| def test_tier_levels_track_dr_and_tier(): | |
| # higher DR and tougher tiers read as higher display levels | |
| assert world._tier_level("minion", 1) == 1 # floored at 1 | |
| assert world._tier_level("standard", 2) == 4 | |
| assert world._tier_level("elite", 5) == 12 | |
| assert world._tier_level("boss", 6) == 20 # final boss sits at the level cap | |
| # the arena boss carries the level the client renders above its head | |
| boss = next(e for e in world.AREAS["arena"].entities if e.type == "boss") | |
| assert boss.level == 20 | |
| def test_reforge_clamps_and_gates(): | |
| base = {"weapon_inventory": ["bell_staff"], "weapon_tiers": {}, | |
| "items": {"rune_grit": 99, "warped_cog": 99}, "gold": 999, "level": 16} | |
| cur = 0 | |
| for expected in (1, 2, 3): | |
| res = world.resolve_shop(base, "market_merchant", "reforge:bell_staff") | |
| act = [a for a in res["world_actions"] if a["type"] == "reforge_weapon"] | |
| assert act and act[0]["tier"] == expected | |
| cur = expected | |
| base["weapon_tiers"]["bell_staff"] = cur | |
| # maxed out | |
| res = world.resolve_shop(base, "market_merchant", "reforge:bell_staff") | |
| assert res.get("denied") and not res["world_actions"] | |
| def test_reforge_level_gate(): | |
| low = {"weapon_inventory": ["bell_staff"], "weapon_tiers": {"bell_staff": 2}, | |
| "items": {"warped_cog": 9}, "gold": 99, "level": 5} | |
| res = world.resolve_shop(low, "market_merchant", "reforge:bell_staff") | |
| assert res.get("denied") # +3 needs level 12 | |
| def test_crit_chance_gating_and_cap(): | |
| assert world._crit_chance({"level": 1, "crit": 10}, []) == 0 # locked < L8 | |
| assert world._crit_chance({"level": 8, "crit": 10}, []) == 10 | |
| assert world._crit_chance({"level": 20, "crit": 100}, []) == 40 # capped | |
| masterful = {"level": 8, "crit": 10, "rune_mastery": {"bell": 5}} | |
| assert world._crit_chance(masterful, ["bell"]) == 20 # +10 mastered rune | |
| def test_trinket_roll_shape(): | |
| rare = story.roll_trinket("rare", "Clock Sewer", 1234) | |
| assert rare["id"] == "trinket_1234" | |
| assert len(rare["stats"]) == 1 | |
| epic = story.roll_trinket("epic", "Bone Market", 5678) | |
| assert len(epic["stats"]) == 2 | |
| assert all(k in story.TRINKET_STATS for k in epic["stats"]) | |
| def test_taunt_fallback_table(): | |
| assert story.taunt_fallback("boss", "enrage") | |
| assert story.taunt_fallback("charge", "windup") | |
| # unknown archetype falls back to the brute line for that event | |
| assert story.taunt_fallback("nonsense", "spotted") == story.TAUNT_FALLBACK[("brute", "spotted")] | |
| def test_drop_table_respawn_no_rares(): | |
| import random | |
| acts = world._roll_drops({"tier": "elite", "id": "x", "name": "Foe", "respawned": True}, | |
| random.Random(1), area="Clock Sewer") | |
| kinds = {a["type"] for a in acts} | |
| assert "add_trinket" not in kinds # respawn farming never yields rares | |
| assert "add_gold" in kinds | |