import random import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import game # noqa: E402 T0 = 1_800_000_000.0 def world_with(*players): world = game.new_world(T0) for uid, cls in players: world["players"][uid] = game.new_player(uid, uid.title(), cls, T0) return world class XpTest(unittest.TestCase): def test_curve_matches_runescape(self): self.assertEqual(game.XP_TABLE[2], 83) self.assertEqual(game.XP_TABLE[99], 13034431) self.assertEqual(game.level_for(0), 1) self.assertEqual(game.level_for(83), 2) self.assertEqual(game.level_for(10**9), 99) class ActionTest(unittest.TestCase): def test_gathering_runs_while_away(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"] self.assertIsNone(game.start_action(world, p, "mine_stone", T0)) summary = game.process_player(world, p, T0 + 200, random.Random(1)) self.assertEqual(summary["done"], 10) self.assertEqual(p["bank"]["stone"], 10) self.assertEqual(p["xp"]["mining"], 50) self.assertEqual(p["action"]["start"], T0 + 200) def test_class_speed_and_lucky_yield(self): world = world_with(("pia", "pioneer")) p = world["players"]["pia"] game.start_action(world, p, "mine_stone", T0) summary = game.process_player(world, p, T0 + 1600, random.Random(3)) self.assertEqual(summary["done"], 100) # 16s instead of 20s self.assertGreater(p["bank"]["stone"], 100) self.assertEqual(p["bank"]["stone"], 100 + summary["lucky"]) def test_offline_cap(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"] game.start_action(world, p, "cut_logs", T0) summary = game.process_player(world, p, T0 + 3 * game.DAY, random.Random(1)) self.assertEqual(summary["done"], game.OFFLINE_CAP // 20) self.assertEqual(p["action"]["start"], T0 + 3 * game.DAY) def test_crafting_stops_when_inputs_run_out(self): world = world_with(("ann", "warrior")) p = world["players"]["ann"] p["bank"] = {"copper_ore": 3, "logs": 10} game.start_action(world, p, "smith_copper_bar", T0) summary = game.process_player(world, p, T0 + 1000, random.Random(1)) self.assertEqual(p["bank"]["copper_bar"], 3) self.assertNotIn("copper_ore", p["bank"]) self.assertIsNone(p["action"]) self.assertEqual(summary["stopped"], "Out of copper ore") def test_level_and_city_requirements(self): world = world_with(("ann", "warrior")) p = world["players"]["ann"] self.assertEqual(game.start_action(world, p, "mine_iron", T0), "Needs level 15") p["xp"]["smithing"] = game.XP_TABLE[30] p["bank"] = {"iron_bar": 10} self.assertEqual(game.start_action(world, p, "smith_iron_sword", T0), "Needs Smithy level 1") self.assertEqual(game.start_action(world, p, "recon_roads", T0), "Only scouts can do that") def test_full_bank_overflows_to_city(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"] p["bank"] = {"stone": game.BASE_STACK - 2} game.start_action(world, p, "mine_stone", T0) summary = game.process_player(world, p, T0 + 100, random.Random(1)) self.assertEqual(p["bank"]["stone"], game.BASE_STACK) self.assertEqual(summary["overflow"], {"stone": 3}) self.assertEqual(world["city"]["stockpile"]["stone"], 3) self.assertEqual(p["favour"], 3) def test_full_bank_can_stop_instead(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"] p["overflow"] = "stop" p["bank"] = {f"x{i}": 1 for i in range(game.BASE_SLOTS)} game.start_action(world, p, "mine_stone", T0) summary = game.process_player(world, p, T0 + 100, random.Random(1)) self.assertEqual(summary["stopped"], "Bank full") self.assertNotIn("stone", p["bank"]) def test_combat_pays_and_loses(self): world = world_with(("wil", "warrior")) p = world["players"]["wil"] game.start_action(world, p, "fight_wolves", T0) summary = game.process_player(world, p, T0 + 48 * 100, random.Random(5)) self.assertEqual(summary["done"], 100) self.assertEqual(summary["wins"] + summary["losses"], 100) self.assertEqual(p["gold"], summary["wins"] * 2) class GiftTest(unittest.TestCase): def test_give_moves_items_within_bank_room(self): world = world_with(("fred", "pioneer"), ("kay", "artisan")) fred, kay = world["players"]["fred"], world["players"]["kay"] fred["bank"] = {"copper_ore": 300} self.assertIsNone(game.give(world, fred, kay, "copper_ore", 200, T0)) self.assertEqual(fred["bank"], {"copper_ore": 100}) self.assertEqual(kay["bank"], {"copper_ore": 200}) self.assertEqual(world["ledger"][-1]["text"], "Fred gave 200 copper ore to Kay") kay["bank"] = {f"x{i}": 1 for i in range(game.BASE_SLOTS)} self.assertEqual(game.give(world, fred, kay, "copper_ore", 1, T0), "Kay only has room for 0") self.assertEqual(game.give(world, fred, fred, "copper_ore", 1, T0), "You can't give to yourself") def test_amount_pluralises(self): self.assertEqual(game.amount("copper_sword", 179), "179 copper swords") self.assertEqual(game.amount("copper_sword", 1), "1 copper sword") self.assertEqual(game.amount("stone", 1200), "1,200 stone") class CityTest(unittest.TestCase): def test_build_needs_stockpile_then_takes_time(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"] self.assertIn("short", game.commission(world, p, "walls", T0)) world["city"]["stockpile"].update({"stone": 1200, "bricks": 500}) world["city"]["treasury"] = 300 self.assertIsNone(game.commission(world, p, "walls", T0)) self.assertEqual(world["city"]["treasury"], 0) self.assertNotIn("stone", world["city"]["stockpile"]) game.tick(world, T0 + 3 * game.HOUR, random.Random(1)) self.assertEqual(world["city"]["buildings"]["walls"]["level"], 1) self.assertEqual(game.defence(world)["parts"]["walls"], 20) def test_people_eat_and_go_hungry(self): world = world_with() city = world["city"] city["stockpile"] = {"bread": 5} game.tick(world, T0 + 2 * game.DAY, random.Random(1)) self.assertTrue(city["starving"]) self.assertNotIn("bread", city["stockpile"]) self.assertLess(city["population"], 20) def test_fed_city_grows_to_housing_and_pays_tax(self): world = world_with() city = world["city"] city["stockpile"] = {"bread": 1000} world["next_event_at"] = None game.tick(world, T0 + 10 * game.DAY, random.Random(1)) self.assertFalse(city["starving"]) self.assertEqual(city["population"], 25) self.assertGreater(city["treasury"], 100) def test_speed_scales_time(self): world = game.new_world(T0, speed=60) world["players"]["fred"] = game.new_player("fred", "Fred", "warrior", T0) p = world["players"]["fred"] game.start_action(world, p, "mine_stone", T0) game.process_player(world, p, T0 + 10, random.Random(1)) self.assertEqual(p["bank"]["stone"], 30) class EventTest(unittest.TestCase): def announce(self, world): world["next_event_at"] = T0 game.tick(world, T0, random.Random(2)) return game.upcoming_raid(world) def test_first_event_is_a_raid_with_vague_intel(self): world = world_with() raid = self.announce(world) self.assertIsNotNone(raid) view = game.event_view(world, raid, T0) self.assertEqual(view["intel"], 0) self.assertNotIn("strength", view) self.assertNotIn("group", view) self.assertLessEqual(view["earliest"], raid["fire_at"]) self.assertGreaterEqual(view["latest"], raid["fire_at"]) def test_spymaster_and_recon_sharpen_intel(self): world = world_with(("sam", "scout")) raid = self.announce(world) world["city"]["buildings"]["spymaster"]["level"] = 1 self.assertIn("size", game.event_view(world, raid, T0)) p = world["players"]["sam"] p["xp"]["reconnaissance"] = game.XP_TABLE[20] game.start_action(world, p, "recon_camps", T0) game.process_player(world, p, T0 + 8 * 72, random.Random(1)) view = game.event_view(world, raid, T0) self.assertEqual(view["intel"], 3) self.assertEqual(view["exact"], raid["fire_at"]) self.assertIn("strength", view) self.assertTrue(view["camp"]) def test_victory_rewards_garrison(self): world = world_with(("wil", "warrior")) raid = self.announce(world) p = world["players"]["wil"] p["xp"]["combat"] = game.XP_TABLE[60] p["garrison"] = True game.tick(world, raid["fire_at"], random.Random(1)) self.assertEqual(raid["status"], "done") self.assertTrue(raid["report"]["title"].startswith("Victory")) self.assertEqual(world["city"]["repelled"], 1) self.assertEqual(p["favour"], 25) self.assertFalse(p["garrison"]) def test_defeat_culls_and_loots_but_spares_banks(self): world = world_with(("fred", "warrior")) p = world["players"]["fred"] p["bank"] = {"stone": 500} city = world["city"] city["stockpile"] = {"bread": 200, "stone": 1000} city["treasury"] = 500 city["buildings"]["walls"]["level"] = 1 raid = self.announce(world) raid["strength"] = 500 game.tick(world, raid["fire_at"], random.Random(1)) self.assertTrue(raid["report"]["title"].startswith("Defeat")) self.assertLess(city["population"], 20) self.assertLess(city["stockpile"]["stone"], 1000) self.assertLess(city["treasury"], 500) self.assertTrue(city["buildings"]["walls"]["damaged"]) self.assertEqual(city["buildings"]["walls"]["level"], 1) self.assertEqual(p["bank"], {"stone": 500}) self.assertEqual(game.defence(world)["parts"]["walls"], 0) def test_repair_is_cheaper_than_building(self): cost = game.repair_cost("walls", 2) full = game.build_cost("walls", 2) for item, qty in cost.items(): self.assertLess(qty, full[item]) def test_sabotage_needs_a_located_camp(self): world = world_with(("wil", "warrior")) p = world["players"]["wil"] p["xp"]["combat"] = game.XP_TABLE[40] raid = self.announce(world) self.assertEqual(game.start_action(world, p, "sabotage", T0), "No bandit camp has been located") raid["recon"] = 3 * game.RECON_PER_INTEL self.assertIsNone(game.start_action(world, p, "sabotage", T0)) game.process_player(world, p, T0 + 96 * 20, random.Random(1)) self.assertGreater(raid["sabotage"], 0) self.assertLessEqual(raid["sabotage"], 0.3) if __name__ == "__main__": unittest.main()