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_sit_in_zones_and_hunts_exist(self): hunts = set() for pid, place in content.PLACES.items(): self.assertIn(place["zone"], content.ZONES) for hunt, pulls in place.get("hunts", {}).items(): self.assertNotIn((place["zone"], hunt), hunts, "hunts must be unique within a zone") hunts.add((place["zone"], hunt)) self.assertIn(hunt, content.MOBS) for pull in pulls: for mob in pull: self.assertIn(mob, content.MOBS) def test_roads_join_zones_through_real_mobs(self): for road in content.ROADS.values(): for zone in road["zones"]: self.assertIn(zone, content.ZONES) self.assertIn(road["rare"], content.MOBS) for stop in road["stops"]: for pull in stop["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")) self.assertIsNone(game.start_hunt(world, fred, "kobold_vermin", 1000)) self.assertEqual(fred["place"], "northshire_valley") 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"] 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"] = "sentinel_hill" self.assertIn("Turn it in", game.turn_in(world, fred, "kobold_camp_cleanup", 2, 0)) fred["place"] = "fargodeep" # anywhere in Elwynn will do 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_a_journey_is_a_fight_at_every_stop(self): world = world_with(("fred", "warrior")) fred = world["players"]["fred"] game.dev_level(world, fred, 14, 1000) self.assertIsNone(game.accept(fred, "riverpaw_gnoll_bounty")) fred["done"].append("riverpaw_gnoll_bounty") fred["quests"].pop("riverpaw_gnoll_bounty") self.assertIsNone(game.accept(fred, "report_to_gryan")) self.assertIsNone(game.journey(world, fred, "westfall", 1000)) self.assertIn("road", game.start_hunt(world, fred, "kobold_vermin", 1000)) fights = 0 for step in range(2000): game.process(world, 1000 + step, random.Random(step)) if fred["fight"]: fights += step == 0 or world["fights"][fred["fight"]]["t"] <= 1 if not fred["activity"]: break self.assertEqual(fred["place"], "sentinel_hill") self.assertEqual(fred["zones"], ["elwynn", "westfall"]) self.assertEqual(fred["quests"]["report_to_gryan"], [1]) self.assertGreaterEqual(fights, 3) def test_dying_on_the_road_sends_you_back(self): world = world_with(("fred", "mage")) fred = world["players"]["fred"] self.assertIsNone(game.journey(world, fred, "westfall", 1000)) game.process(world, 1000 + 600, random.Random(1)) self.assertIsNone(fred["activity"]) self.assertEqual(game.zone_of(fred["place"]), "elwynn") self.assertEqual(fred["zones"], ["elwynn"]) def test_flying_needs_the_zone_reached_and_the_fare(self): world = world_with(("fred", "warrior")) fred = world["players"]["fred"] self.assertIn("on foot", game.fly(world, fred, "westfall", 0)) fred["zones"].append("westfall") self.assertIn("money", game.fly(world, fred, "westfall", 0)) fred["copper"] = 100 self.assertIsNone(game.fly(world, fred, "westfall", 0)) self.assertEqual(fred["place"], "sentinel_hill") self.assertEqual(fred["copper"], 100 - content.FLIGHT_COST) def test_old_worlds_migrate(self): world = world_with(("fred", "warrior")) world["version"] = 1 fred = world["players"]["fred"] del fred["zones"] fred["activity"] = {"kind": "travel", "to": "sentinel_hill", "end": 0} fred["quests"]["report_to_goldshire"] = [0] game.migrate(world) self.assertEqual((fred["place"], fred["activity"], fred["zones"]), ("sentinel_hill", None, ["westfall"])) self.assertEqual(fred["quests"]["report_to_goldshire"], []) 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) world["players"]["t"]["place"] = "sentinel_hill" 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_the_meeting_stone_summons_the_group(self): world = world_with(("a", "warrior"), ("b", "priest"), dev=False) a, b = world["players"]["a"], world["players"]["b"] game.create_group(world, a) self.assertIn("no dungeon", game.enter_dungeon(world, a, 0)) a["place"] = "sentinel_hill" self.assertIn("at least 2", game.enter_dungeon(world, a, 0)) game.join_group(world, b, a["group"]) game.journey(world, b, "westfall", 0) self.assertIn("Waiting on B", game.enter_dungeon(world, a, 0)) game.stop(world, b, 0) self.assertIsNone(game.enter_dungeon(world, a, 0)) self.assertEqual((b["activity"]["kind"], b["place"]), ("dungeon", "moonbrook")) self.assertIn("westfall", b["zones"]) game.leave_group(world, b, 10) self.assertIsNone(b["activity"]) self.assertEqual(b["place"], "moonbrook") if __name__ == "__main__": unittest.main()