Guildhall alpha: co-op idle game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
9058039eb7
14 changed files with 2815 additions and 0 deletions
254
tests/test_game.py
Normal file
254
tests/test_game.py
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue