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()