wog/tests/test_game.py
rope 71dd99795a First slice: classes, combat, questing, Deadmines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 22:52:42 +01:00

260 lines
12 KiB
Python

import random
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import combat # noqa: E402
import content # noqa: E402
import game # noqa: E402
def world_with(*players, now=1000.0, dev=True):
world = game.new_world(now)
world["config"]["dev"] = dev
for uid, cls in players:
world["players"][uid] = game.new_player(uid, uid.title(), cls, now)
return world
def solo_fight(cls, level, mob, seed=1, watch=None, manual=()):
player = game.new_player("p", "P", cls, 0)
game.dev_level({"config": {}}, player, level, 0)
player["manual"] = list(manual)
fight = combat.new_fight("f", seed, 0, game.player_unit(player, 0, False),
[combat.mob_unit("e0", mob, level)], "hunt")
return player, fight
class Content(unittest.TestCase):
def test_every_spec_has_four_known_abilities(self):
for spec in content.SPECS.values():
self.assertEqual(len(spec["abilities"]), 4)
self.assertEqual(sorted(spec["abilities"]), sorted(spec["priority"]))
for ab in spec["abilities"]:
self.assertIn(ab, content.ABILITIES)
def test_places_link_both_ways_and_hunts_exist(self):
for pid, place in content.PLACES.items():
for other in place["links"]:
self.assertIn(pid, content.PLACES[other]["links"], f"{pid} -> {other}")
for hunt, pulls in place.get("hunts", {}).items():
self.assertIn(hunt, content.MOBS)
for pull in pulls:
for mob in pull:
self.assertIn(mob, content.MOBS)
def test_quests_point_at_real_things(self):
for qid, quest in content.QUESTS.items():
self.assertIn(quest["giver"], content.PLACES)
for req in quest.get("req", []):
self.assertIn(req, content.QUESTS)
for objective in quest["obj"]:
if "kill" in objective:
self.assertIn(objective["kill"], content.MOBS)
for mob in objective.get("from", []):
self.assertIn(mob, content.MOBS)
for reward in quest.get("choice", []):
self.assertIsNotNone(game.item(reward), reward)
def test_loot_tables_resolve(self):
for items in list(content.BOSS_LOOT.values()) + [content.TRASH_LOOT]:
for item_id in items:
self.assertIsNotNone(game.item(item_id), item_id)
class Items(unittest.TestCase):
def test_generated_green_has_name_and_stats(self):
green = game.item("g/chest/mail/bear/14")
self.assertEqual(green["q"], "uncommon")
self.assertIn("of the Bear", green["name"])
self.assertGreater(green["sta"], green["power"])
self.assertGreater(green["armor"], 0)
def test_armour_rules(self):
mage = game.new_player("m", "M", "mage", 0)
self.assertIsNotNone(game.can_wear(mage, game.item("g/chest/mail/bear/2")))
self.assertIsNone(game.can_wear(mage, game.item("g/chest/cloth/eagle/2")))
warrior = game.new_player("w", "W", "warrior", 0)
self.assertIsNone(game.can_wear(warrior, game.item("g/chest/cloth/eagle/2")))
self.assertIn("level", game.can_wear(warrior, game.item("g/chest/mail/bear/14")))
class Combat(unittest.TestCase):
def test_every_class_beats_an_even_mob_alone(self):
for cls in content.CLASSES:
_, fight = solo_fight(cls, 10, "riverpaw_gnoll")
combat.advance(fight, 300)
self.assertEqual(fight["state"], "won", cls)
def test_same_seed_same_fight(self):
a = solo_fight("mage", 8, "kobold_miner", seed=7)[1]
b = solo_fight("mage", 8, "kobold_miner", seed=7)[1]
combat.advance(a, 300)
combat.advance(b, 300)
self.assertEqual(a["t"], b["t"])
self.assertEqual(a["units"]["p"]["hp"], b["units"]["p"]["hp"])
def test_manual_ability_waits_for_the_player_while_watched(self):
_, fight = solo_fight("warrior", 10, "riverpaw_gnoll", manual=["sunder"])
combat.advance(fight, 6, watch={"p": 5})
self.assertNotIn("sunder", fight["units"]["p"]["cds"])
combat.press(fight, "p", "sunder", 6)
combat.advance(fight, 7, watch={"p": 6})
self.assertIn("sunder", fight["units"]["p"]["cds"])
def test_manual_ability_falls_back_to_auto_when_nobody_watches(self):
_, fight = solo_fight("warrior", 10, "riverpaw_gnoll", manual=["sunder"])
combat.advance(fight, 6)
self.assertIn("sunder", fight["units"]["p"]["cds"])
def test_pressed_cooldown_is_shorter_than_auto(self):
_, fight = solo_fight("shaman", 10, "riverpaw_gnoll", manual=["stormstrike"])
combat.press(fight, "p", "stormstrike", 0)
combat.advance(fight, 0.25, watch={"p": 0})
pressed = fight["units"]["p"]["cds"]["stormstrike"]
_, auto = solo_fight("shaman", 10, "riverpaw_gnoll")
combat.advance(auto, 3)
used = auto["units"]["p"]["cds"]["stormstrike"]
self.assertAlmostEqual(pressed - 0.25, content.ABILITIES["stormstrike"]["cd"], places=2)
self.assertGreater(used - pressed, content.ABILITIES["stormstrike"]["cd"] * 0.4)
def test_interrupt_stops_a_cast(self):
player = game.new_player("p", "P", "rogue", 0)
game.dev_level({"config": {}}, player, 16, 0)
player["manual"] = ["kick"]
fight = combat.new_fight("f", 3, 0, game.player_unit(player, 0, False),
[combat.mob_unit("e0", "defias_pillager", 16)], "hunt")
for step in range(40):
combat.advance(fight, (step + 1) * combat.STEP, watch={"p": step * combat.STEP})
if fight["units"]["e0"]["cast"]:
break
self.assertTrue(fight["units"]["e0"]["cast"], "the pillager should start a Fireball")
combat.press(fight, "p", "kick", fight["t"], "e0")
combat.advance(fight, fight["t"] + 0.5, watch={"p": fight["t"]})
self.assertIsNone(fight["units"]["e0"]["cast"])
self.assertTrue(any("interrupts" in line["text"] for line in fight["log"]))
def test_mob_turns_on_the_healer_and_taunt_takes_it_back(self):
tank = game.new_player("tank", "Tank", "warrior", 0)
healer = game.new_player("heal", "Heal", "priest", 0)
for p in (tank, healer):
game.dev_level({"config": {}}, p, 18, 0)
party = game.player_unit(tank, 0, True) + game.player_unit(healer, 0, True)
fight = combat.new_fight("f", 5, 0, party, [combat.mob_unit("e0", "defias_overseer", 18)], "dungeon")
mob = fight["units"]["e0"]
mob["threat"]["heal"] = 10000
combat.advance(fight, 0.5)
self.assertEqual(mob["target"], "heal")
combat.advance(fight, 4)
self.assertEqual(mob["target"], "tank")
self.assertTrue(any("Taunt" in line["text"] for line in fight["log"]))
def test_cc_breaks_on_damage_and_bosses_are_immune(self):
player = game.new_player("p", "P", "mage", 0)
game.dev_level({"config": {}}, player, 18, 0)
fight = combat.new_fight("f", 2, 0, game.player_unit(player, 0, True),
[combat.mob_unit("e0", "defias_miner", 18), combat.mob_unit("e1", "rhahkzor", 19)],
"dungeon")
rng = random.Random(1)
combat._apply(fight, fight["units"]["p"], "polymorph", "e1", 0, rng)
self.assertEqual(fight["units"]["e1"]["cc"], 0)
combat._apply(fight, fight["units"]["p"], "polymorph", "e0", 0, rng)
self.assertGreater(fight["units"]["e0"]["cc"], 0)
combat._damage(fight, fight["units"]["p"], fight["units"]["e0"], 10, "frost", 0.5, rng)
self.assertEqual(fight["units"]["e0"]["cc"], 0)
def test_boss_phase_summons_adds(self):
player = game.new_player("p", "P", "warrior", 0)
game.dev_level({"config": {}}, player, 18, 0)
fight = combat.new_fight("f", 2, 0, game.player_unit(player, 0, True),
[combat.mob_unit("e0", "edwin_vancleef", 21)], "dungeon")
boss = fight["units"]["e0"]
boss["hp"] = boss["max_hp"] * 0.49
combat.advance(fight, 0.25)
self.assertEqual(sum(1 for u in fight["units"].values() if u.get("mob") == "defias_blackguard"), 2)
class World(unittest.TestCase):
def test_hunting_earns_xp_and_quest_progress_while_away(self):
world = world_with(("fred", "warrior"))
fred = world["players"]["fred"]
self.assertIsNone(game.accept(fred, "kobold_camp_cleanup"))
fred["place"] = "northshire_valley"
self.assertIsNone(game.start_hunt(world, fred, "kobold_vermin", 1000))
game.process(world, 1000 + 20 * 60, random.Random(1))
self.assertGreater(fred["level"], 1)
self.assertEqual(fred["quests"]["kobold_camp_cleanup"], [10])
def test_hunting_stops_after_the_offline_cap(self):
world = world_with(("fred", "warrior"))
fred = world["players"]["fred"]
fred["place"] = "northshire_valley"
game.start_hunt(world, fred, "kobold_vermin", 1000)
game.process(world, 1000 + game.OFFLINE_CAP + 3600, random.Random(1))
self.assertIsNone(fred["activity"])
self.assertIsNone(fred["fight"])
def test_turn_in_needs_the_right_place_and_a_reward_pick(self):
world = world_with(("fred", "warrior"))
fred = world["players"]["fred"]
game.accept(fred, "kobold_camp_cleanup")
fred["quests"]["kobold_camp_cleanup"] = [10]
fred["place"] = "goldshire"
self.assertIn("Turn it in", game.turn_in(world, fred, "kobold_camp_cleanup", 2, 0))
fred["place"] = "northshire_abbey"
self.assertEqual(game.turn_in(world, fred, "kobold_camp_cleanup", None, 0), "Pick a reward")
self.assertIsNone(game.turn_in(world, fred, "kobold_camp_cleanup", 2, 0))
self.assertIn("w/feet/mail/4", fred["bags"])
self.assertIn("investigate_echo_ridge", game.available_quests(fred))
def test_travel_takes_the_shortest_road(self):
minutes, path = game.route("northshire_abbey", "moonbrook")
self.assertEqual(path, ["northshire_abbey", "goldshire", "sentinel_hill", "moonbrook"])
self.assertEqual(minutes, 14)
world = world_with(("fred", "warrior"))
fred = world["players"]["fred"]
self.assertIsNone(game.travel(world, fred, "goldshire", 1000))
game.process(world, 1000 + 4 * 60 - 1, random.Random(1))
self.assertEqual(fred["place"], "northshire_abbey")
game.process(world, 1000 + 4 * 60, random.Random(1))
self.assertEqual(fred["place"], "goldshire")
self.assertIsNone(fred["activity"])
def test_a_group_clears_the_deadmines(self):
world = world_with(*[(uid, cls) for uid, cls in
(("t", "warrior"), ("h", "priest"), ("m", "mage"), ("r", "rogue"), ("x", "hunter"))])
for uid, player in world["players"].items():
game.dev_level(world, player, 20, 1000)
player["place"] = "moonbrook"
leader = world["players"]["t"]
game.create_group(world, leader)
gid = leader["group"]
for uid in "hmrx":
self.assertIsNone(game.join_group(world, world["players"][uid], gid))
self.assertIsNone(game.enter_dungeon(world, leader, 1000))
game.process(world, 1000 + 3 * 3600, random.Random(4))
dungeon = world["groups"][gid]["dungeon"]
self.assertEqual(dungeon["state"], "done", dungeon)
loot = sum(len(p["bags"]) for p in world["players"].values())
self.assertGreater(loot, 0)
def test_dungeon_needs_everyone_at_the_door(self):
world = world_with(("a", "warrior"), ("b", "priest"), dev=False)
a, b = world["players"]["a"], world["players"]["b"]
a["place"] = "moonbrook"
game.create_group(world, a)
self.assertIn("at least 2", game.enter_dungeon(world, a, 0))
game.join_group(world, b, a["group"])
self.assertIn("Waiting on B", game.enter_dungeon(world, a, 0))
b["place"] = "moonbrook"
self.assertIsNone(game.enter_dungeon(world, a, 0))
self.assertEqual(b["activity"]["kind"], "dungeon")
game.leave_group(world, b, 10)
self.assertIsNone(b["activity"])
self.assertEqual(b["place"], "moonbrook")
if __name__ == "__main__":
unittest.main()