First slice: classes, combat, questing, Deadmines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
71dd99795a
17 changed files with 4522 additions and 0 deletions
260
tests/test_game.py
Normal file
260
tests/test_game.py
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue