2906 lines
154 KiB
Python
2906 lines
154 KiB
Python
import json
|
||
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
|
||
# A test-only class with no bonuses, so tests of the rules aren't skewed by a trade.
|
||
game.CLASSES.setdefault("commoner", {"name": "Commoner", "icon": "", "fast": [], "craft": [],
|
||
"bonus": "", "masterwork": "", "signature": "", "glyph": "farmer", "tint": ""})
|
||
|
||
|
||
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", "commoner"))
|
||
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_make_one_then_back_to_work(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
p["bank"] = {"copper_bar": 10, "logs": 10}
|
||
game.start_action(world, p, "mine_stone", T0)
|
||
game.process_player(world, p, T0 + 100, random.Random(1))
|
||
p["xp"]["smithing"] = game.XP_TABLE[5]
|
||
self.assertIsNone(game.start_action(world, p, "smith_copper_pickaxe", T0 + 100, once=True))
|
||
summary = game.process_player(world, p, T0 + 100 + 30 + 40, random.Random(1))
|
||
self.assertEqual(p["bank"]["copper_pickaxe"], 1)
|
||
self.assertEqual(summary["resumed"], "mine_stone")
|
||
self.assertEqual(p["action"]["id"], "mine_stone")
|
||
self.assertEqual(p["bank"]["stone"], 5 + 2) # mining carried on from when the pickaxe was done
|
||
|
||
def test_make_one_while_idle_just_stops(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
game.start_action(world, p, "cut_logs", T0, once=True)
|
||
game.process_player(world, p, T0 + 500, random.Random(1))
|
||
self.assertEqual(p["bank"], {"logs": 1})
|
||
self.assertIsNone(p["action"])
|
||
self.assertIsNone(p["stopped"])
|
||
|
||
def test_class_speed_and_lucky_yield(self):
|
||
world = world_with(("pia", "miner"))
|
||
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", "commoner"))
|
||
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", "commoner"))
|
||
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_crafting_stops_as_the_last_one_finishes(self):
|
||
world = world_with(("ann", "commoner"))
|
||
p = world["players"]["ann"]
|
||
p["bank"] = {"copper_ore": 1, "logs": 10}
|
||
game.start_action(world, p, "smith_copper_bar", T0)
|
||
duration = game.action_time(world, p, game.ACTION_BY_ID["smith_copper_bar"])
|
||
summary = game.process_player(world, p, T0 + duration + 0.1, random.Random(1))
|
||
self.assertIsNone(p["action"])
|
||
self.assertEqual(summary["stopped"], "Out of copper ore")
|
||
|
||
def test_level_and_city_requirements(self):
|
||
world = world_with(("ann", "commoner"))
|
||
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.assertIsNone(game.can_start(world, p, game.ACTION_BY_ID["recon_roads"])) # anyone can scout
|
||
|
||
def test_full_bank_overflows_to_city(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
p["bank"] = {"stone": game.BASE_STACK * game.BASE_SLOTS - 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 * game.BASE_SLOTS)
|
||
self.assertEqual(summary["overflow"], {"stone": 3})
|
||
self.assertEqual(world["city"]["stockpile"]["stone"], 3)
|
||
self.assertEqual(p["favour"], 1.5) # half favour for overflow
|
||
self.assertEqual(p["donated"], 3)
|
||
|
||
def test_full_bank_sells_what_the_city_cannot_use(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
p["bank"] = {"copper_ore": game.BASE_STACK * game.BASE_SLOTS - 1}
|
||
game.start_action(world, p, "mine_copper", T0)
|
||
summary = game.process_player(world, p, T0 + 100, random.Random(1))
|
||
self.assertEqual(summary["sold"], {"copper_ore": 3})
|
||
self.assertEqual(p["gold"], 3 * game.ITEMS["copper_ore"]["value"])
|
||
self.assertNotIn("copper_ore", world["city"]["stockpile"])
|
||
self.assertEqual(p["favour"], 0)
|
||
|
||
def test_only_useful_things_can_be_donated(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
p["bank"] = {"copper_ore": 10, "bricks": 10, "bread": 5, "iron_helmet": 1, "steel_sword": 1}
|
||
self.assertEqual(game.donate(world, p, "copper_ore", 10, T0), "The city has no use for that")
|
||
self.assertEqual(game.donate(world, p, "iron_helmet", 1, T0), "The city has no use for that")
|
||
for item, qty in (("bricks", 10), ("bread", 5), ("steel_sword", 1)):
|
||
self.assertIsNone(game.donate(world, p, item, qty, T0))
|
||
self.assertIn("Walls", game.city_uses("bricks"))
|
||
self.assertEqual(game.city_uses("bread"), ["the granary"])
|
||
|
||
def test_a_full_stack_spills_into_another_slot(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
p["bank"] = {"coal": game.BASE_STACK, "logs": 10}
|
||
self.assertEqual(game.slots_used(world, p), 2)
|
||
self.assertEqual(game.bank_room(world, p, "coal"), (game.BASE_SLOTS - 2) * game.BASE_STACK)
|
||
self.assertEqual(game.bank_room(world, p, "logs"), game.BASE_STACK - 10 + (game.BASE_SLOTS - 2) * game.BASE_STACK)
|
||
p["bank"]["coal"] = 2500
|
||
self.assertEqual(game.slots_used(world, p), 4) # 1000 + 1000 + 500, plus the logs
|
||
self.assertEqual(game.bank_room(world, p, "coal"), 500 + (game.BASE_SLOTS - 4) * game.BASE_STACK)
|
||
|
||
def test_full_bank_can_stop_instead(self):
|
||
world = world_with(("fred", "commoner"))
|
||
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", "commoner"))
|
||
p = world["players"]["wil"]
|
||
p["bank"] = {"bread": 500}
|
||
p["equip"].update(weapon="copper_sword", helmet="copper_helmet", body="copper_body", legs="copper_legs")
|
||
game.start_action(world, p, "fight_wolves", T0)
|
||
summary = game.process_player(world, p, T0 + 3600, random.Random(5))
|
||
self.assertGreater(summary["done"], 30) # a fight lasts as long as it takes
|
||
self.assertEqual(summary["wins"] + summary["losses"], summary["done"])
|
||
low, high = game.ACTION_BY_ID["fight_wolves"]["gold"]
|
||
self.assertTrue(summary["wins"] * low <= p["gold"] <= summary["wins"] * high)
|
||
|
||
|
||
class GiftTest(unittest.TestCase):
|
||
def test_give_moves_items_within_bank_room(self):
|
||
world = world_with(("fred", "miner"), ("kay", "miner"))
|
||
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_fund_from_bank_and_stores_then_builds(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
city = world["city"]
|
||
p["bank"] = {"stone": 1500}
|
||
city["stockpile"] = {"bricks": 600, "provisions": 90}
|
||
city["treasury"] = 300
|
||
self.assertIsNone(game.fund(world, p, "walls", "stone", "all", "bank", T0))
|
||
self.assertEqual(p["bank"], {"stone": 500}) # only what the fund needed
|
||
self.assertEqual(p["favour"], 1000)
|
||
self.assertIsNone(game.fund(world, p, "walls", "bricks", 500, "city", T0))
|
||
self.assertEqual(city["stockpile"], {"bricks": 350, "provisions": 90})
|
||
self.assertIsNone(game.fund(world, p, "walls", "provisions", "all", "city", T0))
|
||
self.assertEqual(city["stockpile"], {"bricks": 350})
|
||
self.assertIsNone(city["construction"])
|
||
self.assertEqual(game.fund(world, p, "walls", "stone", 1, "bank", T0), "The fund doesn't need that")
|
||
self.assertIsNone(game.fund(world, p, "walls", "gold", "all", "city", T0))
|
||
self.assertEqual(city["construction"]["building"], "walls")
|
||
self.assertEqual(city["buildings"]["walls"]["fund"], {})
|
||
self.assertEqual(city["construction"]["needed"], game.LABOUR_PER_LEVEL)
|
||
city["construction"]["labour"] = game.LABOUR_PER_LEVEL
|
||
game.tick(world, T0 + 1, random.Random(1))
|
||
self.assertEqual(city["buildings"]["walls"]["level"], 1)
|
||
self.assertEqual(game.defence(world)["parts"]["walls"], 20)
|
||
|
||
def test_funded_buildings_queue_behind_the_builders(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
city = world["city"]
|
||
for key in ("walls", "houses"):
|
||
city["buildings"][key]["fund"] = {k: v for k, v in game.build_cost(key, 1).items() if k != "gold"}
|
||
city["treasury"] += game.build_cost(key, 1)["gold"]
|
||
self.assertIsNone(game.fund(world, p, key, "gold", "all", "city", T0))
|
||
self.assertEqual(city["construction"]["building"], "walls")
|
||
self.assertEqual(city["queue"], ["houses"])
|
||
city["construction"]["labour"] = city["construction"]["needed"]
|
||
game.tick(world, T0 + 1, random.Random(1))
|
||
self.assertEqual(city["construction"]["building"], "houses")
|
||
self.assertEqual(city["queue"], [])
|
||
|
||
def test_raids_never_touch_funds(self):
|
||
world = world_with(("fred", "commoner"))
|
||
city = world["city"]
|
||
city["buildings"]["walls"]["fund"] = {"stone": 1000}
|
||
city["stockpile"] = {"stone": 1000}
|
||
world["next_event_at"] = T0
|
||
game.tick(world, T0, random.Random(2))
|
||
raid = game.upcoming_raid(world)
|
||
raid["strength"] = 500
|
||
game.tick(world, raid["fire_at"], random.Random(1))
|
||
self.assertLess(city["stockpile"]["stone"], 1000)
|
||
self.assertEqual(city["buildings"]["walls"]["fund"], {"stone": 1000})
|
||
|
||
def test_tier_caps_levels_and_promotes_for_good(self):
|
||
world = world_with(("fred", "commoner"))
|
||
city = world["city"]
|
||
city["buildings"]["walls"]["level"] = 1
|
||
self.assertIsNone(game.fund_target(world, "walls")) # hamlet: level 1 only
|
||
self.assertIsNotNone(game.fund_target(world, "houses")) # houses go to 2
|
||
slots = game.bank_slots(world, world["players"]["fred"])
|
||
city["population"] = 41
|
||
world["next_event_at"] = None
|
||
game.tick(world, T0 + 1, random.Random(1))
|
||
self.assertEqual(city["tier"], 1)
|
||
self.assertIsNotNone(game.fund_target(world, "walls"))
|
||
self.assertEqual(game.bank_slots(world, world["players"]["fred"]), slots + game.TIER_SLOTS)
|
||
city["population"] = 10
|
||
game.tick(world, T0 + 2, random.Random(1))
|
||
self.assertEqual(city["tier"], 1)
|
||
|
||
def test_damage_turns_the_fund_towards_repairs_and_keeps_the_rest(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
city = world["city"]
|
||
city["buildings"]["walls"].update(level=1, damaged=True, fund={"stone": 2000})
|
||
repair = game.repair_cost("walls", 1)
|
||
self.assertEqual(game.fund_target(world, "walls"), repair)
|
||
for item, qty in repair.items():
|
||
if item == "gold":
|
||
city["treasury"] = qty
|
||
game.fund(world, p, "walls", "gold", "all", "city", T0)
|
||
elif item != "stone":
|
||
city["stockpile"][item] = qty
|
||
game.fund(world, p, "walls", item, "all", "city", T0)
|
||
self.assertTrue(city["construction"]["repair"])
|
||
self.assertEqual(city["buildings"]["walls"]["fund"], {"stone": 2000 - repair["stone"]})
|
||
|
||
def test_old_worlds_migrate(self):
|
||
world = game.new_world(T0)
|
||
world["version"] = 1
|
||
for building in world["city"]["buildings"].values():
|
||
del building["fund"]
|
||
del world["city"]["queue"], world["city"]["tier"]
|
||
game.migrate(world)
|
||
self.assertEqual(world["version"], game.WORLD_VERSION)
|
||
self.assertEqual(world["city"]["queue"], [])
|
||
self.assertEqual(world["city"]["buildings"]["walls"]["fund"], {})
|
||
|
||
def test_v3_worlds_sell_useless_stock_into_the_treasury(self):
|
||
world = world_with()
|
||
world["version"] = 3
|
||
world["city"]["stockpile"] = {"copper_ore": 8, "raw_meat": 1, "bricks": 500, "bread": 30}
|
||
world["city"]["treasury"] = 10
|
||
game.migrate(world)
|
||
self.assertEqual(world["city"]["stockpile"], {"bricks": 500, "bread": 30})
|
||
self.assertEqual(world["city"]["treasury"], 10 + 8 * 2 + 1 * 2)
|
||
self.assertEqual(world["ledger"][-1]["text"],
|
||
"The council sold 8 copper ore and 1 raw meat from the stockpile for 18 gold")
|
||
|
||
def test_population_view_explains_growth(self):
|
||
world = world_with()
|
||
view = game.population_view(world)
|
||
self.assertEqual(view["tier_name"], "Hamlet")
|
||
self.assertEqual(view["next_tier"], {"name": "Village", "pop": 40})
|
||
self.assertGreater(view["growth"], 0)
|
||
self.assertEqual(view["food_days"], 4.0) # 40 bread for 20 people eating 0.5 a day
|
||
|
||
def test_people_eat_and_go_hungry(self):
|
||
world = world_with()
|
||
city = world["city"]
|
||
city["stockpile"] = {"bread": 5}
|
||
city["population"] = 25.0 # a full town, so nobody new arrives
|
||
world["next_fortune_at"] = None # no lucky wagon of bread
|
||
game.tick(world, T0 + 4 * game.DAY, random.Random(1))
|
||
self.assertTrue(city["starving"])
|
||
self.assertNotIn("bread", city["stockpile"])
|
||
self.assertLess(city["population"], 25)
|
||
|
||
def test_empty_homes_draw_settlers(self):
|
||
world = world_with()
|
||
city = world["city"]
|
||
city["population"] = 22.0
|
||
city["buildings"]["houses"]["level"] = 2
|
||
city["stockpile"] = {"bread": 10_000}
|
||
self.assertAlmostEqual(game.growth_per_day(world), 0.5 * 43 + 0.05 * 22)
|
||
for _hour in range(24):
|
||
game._city_hour(world, T0)
|
||
self.assertGreater(city["population"], 38)
|
||
# Slower as the homes fill, but never down to nothing.
|
||
self.assertLess(game.growth_per_day(world), 0.5 * 43)
|
||
city["population"] = 64.0
|
||
self.assertAlmostEqual(game.growth_per_day(world), 0.5 + 3.2)
|
||
|
||
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", "commoner", 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_recon_sharpens_intel_as_the_raid_draws_near(self):
|
||
world = world_with(("sam", "ranger"))
|
||
raid = self.announce(world)
|
||
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 + 4 * game.HOUR, random.Random(1))
|
||
self.assertGreaterEqual(raid["recon"], game.intel_costs(raid)[-1])
|
||
# Plenty of scouting, but a day or more out only the first level is knowable.
|
||
view = game.event_view(world, raid, T0 + 4 * game.HOUR)
|
||
self.assertEqual(view["intel"], 1)
|
||
self.assertIn("size", view)
|
||
self.assertNotIn("strength", view)
|
||
self.assertEqual(view["recon_next"], 0)
|
||
late = raid["fire_at"] - 25 * game.HOUR
|
||
self.assertEqual(game.intel_level(world, raid, late), 2)
|
||
view = game.event_view(world, raid, raid["fire_at"] - game.HOUR)
|
||
self.assertEqual(view["intel"], 3)
|
||
self.assertEqual(view["exact"], raid["fire_at"])
|
||
self.assertTrue(view["camp"])
|
||
|
||
def test_scouting_stops_counting_once_the_threat_is_fully_scouted(self):
|
||
world = world_with(("sam", "commoner"))
|
||
raid = self.announce(world)
|
||
full = game.intel_costs(raid)[-1]
|
||
raid["recon"] = full - 1
|
||
world["city"]["recon_boost"] = 30
|
||
p = world["players"]["sam"]
|
||
game.start_action(world, p, "recon_roads", T0)
|
||
game.process_player(world, p, T0 + 60 * 3 + 1, random.Random(1))
|
||
self.assertEqual(raid["recon"], full)
|
||
self.assertEqual(world["city"]["recon_boost"], 29) # leads only spent while there's something to learn
|
||
self.assertIsNotNone(p["action"]) # still trains the skill
|
||
self.assertEqual(game.event_view(world, raid, T0)["recon"], full)
|
||
|
||
def test_threats_land_every_two_days_and_the_camp_opens_a_day_out(self):
|
||
world = world_with(("sam", "ranger"))
|
||
raid = self.announce(world)
|
||
self.assertLessEqual(abs(raid["fire_at"] - (T0 + 48 * game.HOUR)), 2 * game.HOUR)
|
||
raid["recon"] = game.intel_costs(raid)[-1]
|
||
self.assertEqual(game.intel_level(world, raid, raid["fire_at"] - 24 * game.HOUR - 1), 2)
|
||
self.assertEqual(game.intel_level(world, raid, raid["fire_at"] - 24 * game.HOUR), 3)
|
||
raid["strength"] = 0
|
||
game.tick(world, raid["fire_at"], random.Random(3))
|
||
following = game.upcoming_raid(world)
|
||
self.assertIsNotNone(following) # rumoured the moment the last one lands
|
||
self.assertEqual(following["slot"], raid["slot"] + 48 * game.HOUR)
|
||
|
||
def test_v26_worlds_get_livelier_tracks(self):
|
||
world = world_with()
|
||
world["version"] = 26
|
||
world["next_fortune_at"] = T0 + 70 * game.HOUR
|
||
world["next_event_at"] = T0 + 20 * game.HOUR
|
||
game.migrate(world, T0)
|
||
self.assertEqual(world["next_fortune_at"], T0 + 20 * game.HOUR)
|
||
self.assertEqual(world["next_event_at"], T0)
|
||
|
||
def test_only_paid_guards_stand_and_hold_kit(self):
|
||
world = world_with(("sam", "commoner"))
|
||
city = world["city"]
|
||
city["population"] = 100
|
||
city["buildings"]["barracks"]["level"] = 2
|
||
city["stockpile"] = {"copper_sword": 100}
|
||
self.assertEqual(game.guard_posts(world), 50)
|
||
self.assertEqual(game.defence(world)["parts"]["weapons"], 0)
|
||
p = world["players"]["sam"]
|
||
p["gold"] = 1000
|
||
self.assertIsNone(game.pay_wages(world, p, 600, "bank", T0))
|
||
self.assertEqual(game.guards_on_duty(world), 10)
|
||
self.assertEqual(p["favour"], 600)
|
||
parts = game.defence(world)["parts"]
|
||
self.assertEqual(parts["weapons"], round(0.35 * 10 * 4, 1))
|
||
self.assertEqual(parts["militia"], round(0.3 * 2 * 100 * 10 / 50, 1))
|
||
city["treasury"] = 10_000
|
||
self.assertIsNone(game.pay_wages(world, p, "all", "city", T0))
|
||
self.assertEqual(city["wages"], 50 * game.GUARD_WAGE) # never more than every post
|
||
self.assertEqual(game.pay_wages(world, p, 1, "bank", T0), "Every post is paid for")
|
||
event = {"strength": 1, "sabotage": 0, "group": "the Red Hand"}
|
||
game._resolve_raid(world, event, T0, random.Random(1))
|
||
self.assertEqual(event["report"]["guards"], 50)
|
||
self.assertEqual(city["wages"], 0)
|
||
|
||
def test_threat_size_is_against_standing_defence(self):
|
||
self.assertEqual(game.threat_size({"standing": 100}, 110), 0)
|
||
self.assertEqual(game.threat_size({"standing": 100}, 260), 3)
|
||
self.assertEqual(game.threat_size({"strength": 20}, 20), 0) # rumoured before v29
|
||
|
||
def test_v28_worlds_pay_the_coming_threats_guards(self):
|
||
world = world_with()
|
||
world["version"] = 28
|
||
game.migrate(world, T0)
|
||
self.assertEqual(game.guards_on_duty(world), game.guard_posts(world))
|
||
|
||
def test_bigger_bands_take_more_scouting(self):
|
||
small = {"kind": "raid", "strength": 20}
|
||
huge = {"kind": "raid", "strength": 150}
|
||
dragon = {"kind": "dragon", "strength": 20}
|
||
self.assertEqual(game.intel_costs(small), [15, 45, 90])
|
||
self.assertEqual(game.intel_costs(huge), [30, 90, 180])
|
||
self.assertEqual(game.intel_costs(dragon), [8, 22, 45])
|
||
|
||
def test_the_spymasters_guild_boosts_and_gathers_recon(self):
|
||
world = world_with(("sam", "commoner"))
|
||
raid = self.announce(world)
|
||
action, p = game.ACTION_BY_ID["recon_roads"], world["players"]["sam"]
|
||
self.assertEqual(game.recon_gain(world, p, action), 1)
|
||
world["city"]["buildings"]["spymaster"]["level"] = 2
|
||
self.assertEqual(game.recon_gain(world, p, action), 1.5)
|
||
game.tick(world, T0 + 2 * game.HOUR, random.Random(2))
|
||
self.assertAlmostEqual(raid["recon"], 2.0)
|
||
|
||
def test_victory_rewards_garrison(self):
|
||
world = world_with(("wil", "commoner"))
|
||
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", "commoner"))
|
||
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", "commoner"))
|
||
p = world["players"]["wil"]
|
||
p["xp"]["combat"] = game.XP_TABLE[40]
|
||
p["hp"] = game.max_hp(p)
|
||
raid = self.announce(world)
|
||
self.assertEqual(game.start_action(world, p, "sabotage", T0), "No bandit camp has been located")
|
||
raid["recon"] = game.intel_costs(raid)[-1]
|
||
now = raid["fire_at"] - game.HOUR
|
||
world["city"]["last_tick"] = p["last_active"] = now
|
||
self.assertIsNone(game.start_action(world, p, "sabotage", now))
|
||
game.process_player(world, p, now + 96 * 20, random.Random(1))
|
||
self.assertGreater(raid["sabotage"], 0)
|
||
self.assertLessEqual(raid["sabotage"], 0.3)
|
||
|
||
|
||
|
||
class CombatTest(unittest.TestCase):
|
||
def fighter(self, level=10, **equip):
|
||
world = world_with(("wil", "commoner"))
|
||
p = world["players"]["wil"]
|
||
p["xp"]["combat"] = game.XP_TABLE[level]
|
||
p["hp"] = game.max_hp(p)
|
||
p["equip"].update(equip)
|
||
return world, p
|
||
|
||
def test_fights_hurt_and_food_heals(self):
|
||
world, p = self.fighter()
|
||
p["bank"] = {"bread": 100}
|
||
game.start_action(world, p, "fight_scouts", T0)
|
||
summary = game.process_player(world, p, T0 + 60 * 60, random.Random(4))
|
||
self.assertGreater(summary["damage"], 0)
|
||
self.assertGreater(summary["eaten"].get("bread", 0), 0)
|
||
self.assertEqual(p["bank"].get("bread", 0), 100 - summary["eaten"]["bread"])
|
||
self.assertGreaterEqual(p["hp"], 1)
|
||
|
||
def test_three_knockouts_in_a_row_stop_you_and_nothing_is_lost(self):
|
||
world, p = self.fighter(level=10, weapon="copper_sword")
|
||
p["bank"] = {"stone": 10}
|
||
game.start_action(world, p, "fight_scouts", T0)
|
||
summary = game.process_player(world, p, T0 + 6 * 3600, random.Random(1))
|
||
self.assertIsNone(p["action"])
|
||
self.assertEqual(summary["stopped"], "Knocked out 3 times in a row")
|
||
self.assertEqual(summary["losses"], 3)
|
||
self.assertEqual(p["equip"]["weapon"], "copper_sword")
|
||
self.assertEqual(p["bank"]["stone"], 10)
|
||
self.assertAlmostEqual(p["hp"], game.max_hp(p) * game.KNOCKED_OUT_HP)
|
||
self.assertIsNone(game.start_action(world, p, "fight_scouts", T0 + 6 * 3600)) # you can always go again
|
||
|
||
def test_a_win_resets_the_knockouts(self):
|
||
world, p = self.fighter(level=1, weapon="copper_sword")
|
||
game.start_action(world, p, "fight_wolves", T0)
|
||
summary = game.process_player(world, p, T0 + 3600, random.Random(1))
|
||
self.assertGreater(summary["losses"], 2) # knocked out now and then, never three in a row
|
||
self.assertEqual(p["action"]["id"], "fight_wolves")
|
||
self.assertGreater(summary["wins"], 20)
|
||
|
||
def test_food_shortens_the_rest_between_fights(self):
|
||
world, p = self.fighter(level=30)
|
||
p["eat_below"] = 1.0 # eat whenever a bite is quicker
|
||
top = game.max_hp(p)
|
||
p["bank"] = {}
|
||
resting, eaten = game.rest_plan(world, p, top / 2)
|
||
self.assertEqual(eaten, {})
|
||
self.assertAlmostEqual(resting, top / 2 / (top * game.REST_RATE))
|
||
p["bank"] = {"bread": 100}
|
||
bread, eaten = game.rest_plan(world, p, top / 2)
|
||
self.assertLess(bread, resting)
|
||
self.assertEqual(eaten, {"bread": 10}) # 80 HP back in 8 HP bites
|
||
p["bank"] = {"cooked_salmon": 100}
|
||
salmon, eaten = game.rest_plan(world, p, top / 2)
|
||
self.assertLess(salmon, bread) # better food, shorter rest
|
||
self.assertEqual(eaten, {"cooked_salmon": 3})
|
||
self.assertEqual(game.rest_plan(world, p, top)[0], game.RESPAWN) # at full: a breather
|
||
p["xp"]["combat"] = game.XP_TABLE[99]
|
||
p["bank"] = {"bread": 100}
|
||
self.assertEqual(game.rest_plan(world, p, 100)[1], {}) # a bite of bread is no faster than resting
|
||
|
||
def test_you_choose_how_hurt_you_eat_at(self):
|
||
world, p = self.fighter(level=30)
|
||
top = game.max_hp(p)
|
||
p["bank"] = {"cooked_salmon": 100}
|
||
self.assertEqual(p["eat_below"], 0.5)
|
||
self.assertEqual(game.rest_plan(world, p, top * 0.95)[1], {}) # a scratch: just rest
|
||
_seconds, eaten = game.rest_plan(world, p, top * 0.2)
|
||
self.assertEqual(eaten, {"cooked_salmon": 2}) # back over half, then rest
|
||
self.assertIsNone(game.set_eat_below(p, 0))
|
||
self.assertEqual(game.rest_plan(world, p, top * 0.2)[1], {}) # never
|
||
self.assertIsNone(game.set_eat_below(p, 1.0))
|
||
self.assertEqual(game.rest_plan(world, p, top * 0.2)[1], {"cooked_salmon": 5}) # all the way
|
||
self.assertEqual(game.set_eat_below(p, 0.3), "Pick one of the choices")
|
||
|
||
def test_food_is_eaten_resting_not_fighting(self):
|
||
world, p = self.fighter(level=15, weapon="iron_sword", helmet="iron_helmet", body="iron_body", legs="iron_legs")
|
||
p["bank"] = {"cooked_trout": 100}
|
||
p["hp"] = 10
|
||
fight = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_clay_golems"], random.Random(3))
|
||
mine = game.member_of(fight, p["id"])
|
||
self.assertGreater(sum(mine["eaten"].values()), 0)
|
||
self.assertGreater(fight["rest"], game.RESPAWN)
|
||
self.assertEqual(mine["hp_in"], game.max_hp(p)) # every fight starts rested
|
||
self.assertTrue(all(e[0] >= fight["rest"] for e in fight["events"]))
|
||
self.assertFalse(any(e[3] == "eat" for e in fight["events"]))
|
||
|
||
def test_rest_heals(self):
|
||
world, p = self.fighter()
|
||
p["hp"] = 1
|
||
game.regen(world, p, T0 + 4 * 3600)
|
||
self.assertEqual(p["hp"], game.max_hp(p))
|
||
|
||
def test_armour_and_levels_cut_damage(self):
|
||
world, bare = self.fighter(level=10)
|
||
_, armoured = self.fighter(level=10, helmet="iron_helmet", body="iron_body", legs="iron_legs")
|
||
_, veteran = self.fighter(level=60)
|
||
scouts = game.ACTION_BY_ID["fight_scouts"]
|
||
hurt = lambda p: game.fight_odds(p, scouts)["damage"]
|
||
self.assertLess(hurt(armoured), hurt(bare) * 0.7)
|
||
self.assertLess(hurt(veteran), hurt(bare))
|
||
self.assertGreater(game.fight_odds(veteran, scouts)["hit"], game.fight_odds(bare, scouts)["hit"])
|
||
|
||
def test_a_fight_is_played_out_slice_by_slice(self):
|
||
world, p = self.fighter(level=20, weapon="iron_sword", helmet="iron_helmet", body="iron_body", legs="iron_legs")
|
||
p["bank"] = {"bread": 50}
|
||
game.start_action(world, p, "fight_scouts", T0)
|
||
game.process_player(world, p, T0 + 1, random.Random(2))
|
||
fight = p["action"]["fight"] # simulated, waiting to finish, for the page to replay
|
||
self.assertEqual(fight["result"], "won")
|
||
kinds = {"hit", "crit", "tick", "miss", "block", "absorb", "heal", "mend", "cast", "use", "stop", "cc", "free",
|
||
"die", "enrage", "wind", "down", "left"}
|
||
self.assertTrue(all(e[3] in kinds for e in fight["events"]))
|
||
self.assertTrue(all(e[1] in (p["id"], None) or e[1].startswith("e") for e in fight["events"]))
|
||
dead = [e[2] for e in fight["events"] if e[3] == "die"]
|
||
self.assertEqual(sorted(dead), sorted(f"e{i}" for i in range(len(fight["pack"])))) # the last of them finished it
|
||
self.assertAlmostEqual(fight["duration"], fight["end"], places=1) # rewards land on the kill
|
||
summary = game.process_player(world, p, T0 + fight["duration"] + 0.01, random.Random(2))
|
||
self.assertEqual(summary["wins"], 1)
|
||
self.assertAlmostEqual(p["hp"], game.member_of(fight, p["id"])["hp"], delta=0.1)
|
||
self.assertAlmostEqual(p["action"]["start"], T0 + fight["duration"]) # straight on to the next rest
|
||
|
||
def test_outclassing_an_enemy_kills_faster(self):
|
||
kills = {}
|
||
for level in (20, 90):
|
||
world, p = self.fighter(level=level, weapon="rune_sword")
|
||
game.start_action(world, p, "fight_wolves", T0)
|
||
kills[level] = game.process_player(world, p, T0 + 600, random.Random(1))["wins"]
|
||
self.assertGreater(kills[90], kills[20] * 1.3)
|
||
|
||
def test_each_fight_starts_with_every_swing_timer_empty(self):
|
||
world, p = self.fighter(level=60, weapon="rune_sword")
|
||
game.start_action(world, p, "fight_wolves", T0)
|
||
game.process_player(world, p, T0 + 0.1, random.Random(1))
|
||
first = p["action"]["fight"]
|
||
game.process_player(world, p, T0 + first["duration"] + 0.01, random.Random(1))
|
||
self.assertAlmostEqual(p["action"]["start"], T0 + first["duration"]) # the rest starts at the kill
|
||
second = p["action"]["fight"]
|
||
swing = next(e[0] for e in second["events"] if e[1] == p["id"] and e[6] in game.DAMAGE_TYPES)
|
||
self.assertAlmostEqual(swing, second["rest"] + 2.5) # a full swing from scratch, to the slice
|
||
|
||
def test_preferred_food_is_eaten_first(self):
|
||
world, p = self.fighter()
|
||
p["bank"] = {"bread": 5, "cooked_salmon": 5}
|
||
self.assertEqual(game.edible(p), ["bread", "cooked_salmon"])
|
||
p["food"] = "cooked_salmon"
|
||
self.assertEqual(game.edible(p), ["cooked_salmon", "bread"])
|
||
|
||
|
||
class AbilityTest(unittest.TestCase):
|
||
def fighter(self, cls, level, tier=1, **equip):
|
||
world = world_with(("ash", cls))
|
||
p = world["players"]["ash"]
|
||
p["xp"]["combat"] = game.XP_TABLE[level]
|
||
p["equip"].update(equip)
|
||
p["hp"] = game.max_hp(p)
|
||
return world, p
|
||
|
||
def test_the_weapon_gives_four_abilities_and_the_trade_one(self):
|
||
known = lambda style, cls, level, shield=False: game.combat.known_abilities(style, cls, level, shield)
|
||
self.assertEqual(set(known("hammer", "miner", 1)), {"crushing_blow", "find_the_seam"})
|
||
self.assertEqual(set(known("hammer", "miner", 45)), {"crushing_blow", "concuss", "earthshaker", "unstoppable",
|
||
"find_the_seam"})
|
||
self.assertEqual(set(known("bow", "miner", 15)), {"aimed_shot", "pinning_shot", "find_the_seam"}) # any weapon
|
||
self.assertEqual(set(known("sword", "builder", 45)), {"challenge", "sweeping_cut", "barricade"}) # no shield
|
||
self.assertEqual(set(known("sword", "builder", 45, True)), {"shield_bash", "challenge", "sweeping_cut",
|
||
"shield_wall", "barricade"})
|
||
self.assertEqual(known("staff", "homesteader", 30), ["mend", "ward", "hearty_meal", "bolt"]) # auto's order
|
||
self.assertEqual(game.combat.role("sword", True), "tank")
|
||
self.assertEqual(game.combat.role("sword", False), "damage")
|
||
self.assertEqual(game.combat.role("staff", False), "healer")
|
||
|
||
def test_abilities_are_used_in_the_fight(self):
|
||
world, p = self.fighter("ranger", 30, weapon="willow_bow")
|
||
fight = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_clay_golems"], random.Random(1))
|
||
used = {e[6] for e in fight["events"] if e[1] == p["id"] and e[6] in game.combat.ABILITIES}
|
||
self.assertIn("aimed_shot", used)
|
||
casts = [e for e in fight["events"] if e[1] == p["id"] and e[3] == "cast"]
|
||
self.assertTrue(casts)
|
||
|
||
def test_casters_get_stopped(self):
|
||
world, p = self.fighter("ranger", 30, weapon="willow_bow")
|
||
harpies = game.ACTION_BY_ID["fight_harpies"]
|
||
stops = 0
|
||
for seed in range(20):
|
||
fight = game.simulate_fight(world, p, harpies, random.Random(seed))
|
||
stops += sum(1 for e in fight["events"] if e[3] == "stop" and e[6] == "hex_bolt")
|
||
self.assertGreater(stops, 0) # Pinning Shot stops a windcaster's hex
|
||
|
||
def test_a_health_potion_once_a_fight_when_low(self):
|
||
world, p = self.fighter("miner", 10, weapon="copper_warhammer")
|
||
p["bank"] = {"health_potion_1": 5, "health_potion_2": 5}
|
||
self.assertEqual(game.health_potion(p), "health_potion_2")
|
||
game.start_action(world, p, "fight_scouts", T0)
|
||
summary = game.process_player(world, p, T0 + 3600, random.Random(1))
|
||
drunk = summary["used"].get("health_potion_2", 0)
|
||
self.assertGreater(drunk, 0)
|
||
self.assertLessEqual(drunk, summary["done"])
|
||
self.assertEqual(p["bank"].get("health_potion_2", 0), 5 - drunk)
|
||
fight = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_scouts"], random.Random(3))
|
||
self.assertLessEqual(sum(1 for e in fight["events"] if e[3] == "heal" and e[6] == "potion"), 1)
|
||
|
||
def test_health_potions_are_brewed(self):
|
||
brew = game.ACTION_BY_ID["brew_health_potion_3"]
|
||
self.assertEqual((brew["skill"], brew["level"]), ("herblore", 30))
|
||
self.assertEqual(brew["outputs"], {"health_potion_3": 1})
|
||
self.assertIn("tarromin", brew["inputs"])
|
||
self.assertIn("cooked_salmon", brew["inputs"])
|
||
self.assertEqual(game.ITEMS["health_potion_3"]["drink"], game.HEALTH_HEAL[2])
|
||
self.assertNotIn("health_potion_3", game.edible({"bank": {"health_potion_3": 1}, "food": None}))
|
||
|
||
def test_volley_needs_no_arrows(self):
|
||
world, p = self.fighter("ranger", 45, weapon="maple_bow")
|
||
p["bank"] = {}
|
||
fight = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_goblins"], random.Random(2))
|
||
self.assertTrue(any(e[1] == p["id"] and e[6] == "volley" for e in fight["events"]))
|
||
|
||
def test_a_party_rests_as_long_as_its_slowest(self):
|
||
world = world_with(("wil", "builder"), ("kay", "builder"))
|
||
wil, kay = world["players"]["wil"], world["players"]["kay"]
|
||
for p in (wil, kay):
|
||
p["xp"]["combat"] = game.XP_TABLE[20]
|
||
wil["hp"], kay["hp"] = game.max_hp(wil), 10
|
||
fight = game.draw_fight(world, [wil, kay], game.ACTION_BY_ID["fight_scouts"], random.Random(1))
|
||
self.assertAlmostEqual(fight["rest"], game.rest_plan(world, kay)[0])
|
||
|
||
def test_fights_in_progress_are_drawn_again_after_the_update(self):
|
||
world = world_with(("wil", "builder"))
|
||
wil = world["players"]["wil"]
|
||
game.start_action(world, wil, "fight_wolves", T0)
|
||
wil["action"]["fight"] = {"old": True}
|
||
world["parties"]["1"] = {"id": "1", "action": "fight_wolves", "members": [], "start": T0, "fight": {"old": True}}
|
||
world["version"] = 25
|
||
game.migrate(world, T0)
|
||
self.assertNotIn("fight", wil["action"])
|
||
self.assertIsNone(world["parties"]["1"]["fight"])
|
||
|
||
|
||
class ActivePlayTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("ash", "miner"))
|
||
self.p = self.world["players"]["ash"]
|
||
self.p["xp"]["combat"] = game.XP_TABLE[30]
|
||
self.p["equip"].update(weapon="steel_warhammer", helmet="steel_helmet", body="steel_body", legs="steel_legs")
|
||
self.p["hp"] = game.max_hp(self.p)
|
||
self.p["last_active"] = T0
|
||
game.start_action(self.world, self.p, "fight_stone_guardians", T0)
|
||
game.process_player(self.world, self.p, T0, random.Random(3)) # two stone guardians
|
||
self.fight = self.p["action"]["fight"]
|
||
|
||
def at(self, t):
|
||
"""The real moment `t` seconds into the fight."""
|
||
return T0 + (self.fight["rest"] + t) / self.fight["pace"]
|
||
|
||
def mine(self, label, kinds=("cast", "use", "hit", "crit")):
|
||
return [e for e in self.fight["events"] if e[1] == "ash" and e[6] == label and e[3] in kinds]
|
||
|
||
def test_switching_and_pressing(self):
|
||
self.assertEqual(game.press(self.world, self.p, "crushing_blow", self.at(5)), "That one's on auto")
|
||
self.assertIsNone(game.set_manual(self.world, self.p, "crushing_blow", True, T0)) # yours from the start
|
||
self.assertEqual(self.p["manual"], ["crushing_blow"])
|
||
watch = (self.fight["rest"] + game.member_of(self.fight, "ash")["watch"]) / self.fight["pace"]
|
||
self.assertTrue(all(e[0] >= watch for e in self.mine("crushing_blow", ("cast",)))) # back on auto once you look away
|
||
before = [e for e in self.fight["events"] if e[0] < self.at(5) - T0]
|
||
self.assertIsNone(game.press(self.world, self.p, "crushing_blow", self.at(5)))
|
||
self.assertEqual([e for e in self.fight["events"] if e[0] < self.at(5) - T0], before) # the past stands
|
||
casts = [e[0] for e in self.mine("crushing_blow", ("cast",)) if e[0] >= self.at(5) - T0]
|
||
self.assertTrue(casts)
|
||
self.assertLessEqual(casts[0], self.at(5 + combat_queue()) - T0) # as soon as it's ready
|
||
|
||
def test_while_watching_auto_leaves_your_abilities_alone(self):
|
||
game.set_manual(self.world, self.p, "crushing_blow", True, self.at(1))
|
||
watch = game.member_of(self.fight, "ash")["watch"]
|
||
uses = [e[0] for e in self.mine("crushing_blow", ("cast",))]
|
||
self.assertTrue(all(u >= (self.fight["rest"] + watch) / self.fight["pace"] for u in uses if u > self.at(1) - T0))
|
||
# Look again and again: still nothing on auto until you look away.
|
||
for t in (5, 10, 15):
|
||
game.watch_fight(self.world, self.p, self.at(t))
|
||
self.assertGreater(game.member_of(self.fight, "ash")["watch"], 25)
|
||
|
||
def test_tapping_an_enemy_turns_you_on_it(self):
|
||
self.assertIsNone(game.set_fight_target(self.world, self.p, "e1", self.at(2)))
|
||
swings = [e for e in self.fight["events"] if e[1] == "ash" and e[3] in ("hit", "crit", "miss")
|
||
and e[6] in game.DAMAGE_TYPES and e[0] > self.at(2.5) - T0]
|
||
dead = {e[2]: e[0] for e in self.fight["events"] if e[3] == "die"}
|
||
self.assertTrue(all(e[2] == "e1" for e in swings if e[0] < dead.get("e1", 1e9)))
|
||
|
||
def test_a_press_lands_on_the_enemy_it_names(self):
|
||
game.set_manual(self.world, self.p, "concuss", True, T0)
|
||
self.p["xp"]["combat"] = game.XP_TABLE[30]
|
||
self.assertIsNone(game.press(self.world, self.p, "concuss", self.at(3), "e1")) # not the one you're hitting
|
||
used = [e for e in self.fight["events"] if e[1] == "ash" and e[6] == "concuss" and e[0] >= self.at(3) - T0]
|
||
self.assertEqual(used[0][2], "e1")
|
||
|
||
def test_no_presses_while_resting_or_out_of_a_fight(self):
|
||
game.set_manual(self.world, self.p, "crushing_blow", True, T0)
|
||
self.assertEqual(game.press(self.world, self.p, "crushing_blow", T0), "Still resting")
|
||
later = T0 + self.fight["duration"] + 100
|
||
self.p["action"] = None
|
||
self.assertEqual(game.press(self.world, self.p, "crushing_blow", later), "You're not fighting")
|
||
|
||
def test_old_worlds_get_manual(self):
|
||
del self.p["manual"]
|
||
self.world["version"] = 27
|
||
game.migrate(self.world, T0)
|
||
self.assertEqual(self.p["manual"], [])
|
||
|
||
|
||
def combat_queue():
|
||
return game.combat.QUEUE + game.combat.GCD
|
||
|
||
|
||
class DungeonTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("wil", "builder"), ("kay", "homesteader"), ("ann", "miner"))
|
||
gear = {"builder": dict(weapon="rune_sword", shield="rune_shield", helmet="rune_helmet", body="rune_body",
|
||
legs="rune_legs"),
|
||
"homesteader": dict(weapon="magic_staff", helmet="rune_helmet", body="rune_body", legs="rune_legs"),
|
||
"miner": dict(weapon="rune_warhammer", helmet="rune_helmet", body="rune_body", legs="rune_legs")}
|
||
for p in self.world["players"].values():
|
||
p["xp"]["combat"] = game.XP_TABLE[60] # well past it: these are about the run, not the fights
|
||
p["equip"].update(gear[p["cls"]])
|
||
p["hp"] = game.max_hp(p)
|
||
p["last_active"] = T0
|
||
self.wil, self.kay, self.ann = (self.world["players"][uid] for uid in ("wil", "kay", "ann"))
|
||
|
||
def online(self, now, players=None):
|
||
for p in players or self.world["players"].values():
|
||
p["last_active"] = now
|
||
game.catch_up(self.world, now, random.Random(1))
|
||
|
||
def party(self):
|
||
return game.party_of(self.world, self.wil)
|
||
|
||
def test_a_run_goes_pull_by_pull_and_pays_on_the_way_out(self):
|
||
self.assertIsNone(game.start_action(self.world, self.wil, "dungeon_watchtower", T0))
|
||
self.online(T0 + 600)
|
||
self.assertNotIn("fight", self.wil["action"]) # alone, it waits
|
||
self.assertIsNone(game.join_fight(self.world, self.kay, "wil", T0 + 600))
|
||
self.assertIsNone(game.start_run(self.world, self.wil, T0 + 600))
|
||
names = []
|
||
now = T0 + 600
|
||
while game.party_of(self.world, self.wil):
|
||
fight = self.party()["fight"] or {}
|
||
if fight.get("stage"):
|
||
names.append(fight["enemy"])
|
||
now += 2
|
||
self.online(now)
|
||
self.assertLess(now, T0 + 6 * 3600)
|
||
pulls = game.ACTION_BY_ID["dungeon_watchtower"]["pulls"]
|
||
self.assertEqual(list(dict.fromkeys(names)), pulls)
|
||
for p in (self.wil, self.kay):
|
||
self.assertIsNone(p["action"])
|
||
self.assertEqual(p["stopped"], "Cleared Watchtower Depths")
|
||
self.assertGreaterEqual(p["gold"], game.DUNGEONS["dungeon_watchtower"]["bonus_gold"])
|
||
self.assertIn("cleared Watchtower Depths", self.world["ledger"][-1]["text"])
|
||
|
||
def test_a_group_waits_for_its_leader_and_holds_four(self):
|
||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||
self.assertIsNone(game.join_fight(self.world, self.kay, "wil", T0))
|
||
self.online(T0 + 600)
|
||
self.assertIsNone(self.party()["fight"]) # two in: still waiting for more
|
||
self.assertFalse(game.run_of(self.world, self.kay)["started"])
|
||
self.assertIsNone(game.join_fight(self.world, self.ann, "wil", T0 + 600))
|
||
extra = game.new_player("bo", "Bo", "ranger", T0)
|
||
extra["xp"]["combat"] = game.XP_TABLE[60]
|
||
self.world["players"]["bo"] = extra
|
||
self.assertIsNone(game.join_fight(self.world, extra, "wil", T0 + 600))
|
||
self.assertEqual(len(self.party()["members"]), 4)
|
||
self.assertFalse(game.joinable(self.world, self.wil))
|
||
self.assertEqual(game.start_run(self.world, self.kay, T0 + 601), "Only the leader starts")
|
||
self.assertIsNone(game.start_run(self.world, self.wil, T0 + 601))
|
||
self.online(T0 + 602)
|
||
self.assertEqual(len(self.party()["fight"]["members"]), 4)
|
||
game.leave_party(self.world, extra, self.party()["id"], T0 + 603)
|
||
extra["action"] = None
|
||
self.assertFalse(game.joinable(self.world, self.wil))
|
||
self.assertEqual(game.join_fight(self.world, extra, "wil", T0 + 604), "The run has started")
|
||
|
||
def test_no_run_alone(self):
|
||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||
self.assertEqual(game.start_run(self.world, self.wil, T0), "Not in a dungeon")
|
||
|
||
def test_a_run_waits_for_whoever_looked_away(self):
|
||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||
game.join_fight(self.world, self.kay, "wil", T0)
|
||
game.start_run(self.world, self.wil, T0)
|
||
self.online(T0 + 300, [self.wil]) # kay went to make tea
|
||
self.online(T0 + 1200, [self.wil])
|
||
self.assertIsNone(self.party()["fight"])
|
||
self.assertEqual(game.run_of(self.world, self.wil)["away"], ["Kay"])
|
||
self.online(T0 + 1210)
|
||
self.assertIsNotNone(self.party()["fight"])
|
||
|
||
def test_three_wipes_on_a_pull_end_the_run(self):
|
||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||
game.join_fight(self.world, self.kay, "wil", T0)
|
||
for p in (self.wil, self.kay):
|
||
p["equip"] = {slot: None for slot in p["equip"]}
|
||
p["xp"]["combat"] = game.XP_TABLE[20]
|
||
self.party()["run"] = {"pull": 7, "wipes": 0} # the ogre, naked
|
||
now = T0
|
||
while game.party_of(self.world, self.wil):
|
||
now += 30
|
||
self.online(now)
|
||
self.assertLess(now, T0 + 3 * 3600)
|
||
self.assertEqual(self.wil["stopped"], "Wiped 3 times at Watchtower ogre")
|
||
|
||
def test_the_leader_can_pull_early(self):
|
||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||
game.join_fight(self.world, self.kay, "wil", T0)
|
||
game.start_run(self.world, self.wil, T0)
|
||
self.kay["hp"] = 1
|
||
self.online(T0 + 0.5)
|
||
fight = self.party()["fight"]
|
||
self.assertGreater(fight["rest"], 10)
|
||
self.assertEqual(game.pull_now(self.world, self.kay, T0 + 3), "Only the leader pulls")
|
||
self.assertIsNone(game.pull_now(self.world, self.wil, T0 + 3))
|
||
self.assertLessEqual(fight["rest"], 3)
|
||
self.assertLess(game.member_of(fight, "kay")["hp_in"], game.max_hp(self.kay) / 2)
|
||
|
||
def test_a_pull_early_heals_each_at_their_own_pace(self):
|
||
game.start_action(self.world, self.wil, "dungeon_watchtower", T0)
|
||
game.join_fight(self.world, self.kay, "wil", T0)
|
||
game.start_run(self.world, self.wil, T0)
|
||
self.wil["hp"], self.kay["hp"] = game.max_hp(self.wil) - 5, 1
|
||
self.online(T0 + 0.5)
|
||
fight = self.party()["fight"]
|
||
wil = game.member_of(fight, "wil")
|
||
self.assertLess(wil["rest"], fight["rest"]) # done resting first, waiting on Kay
|
||
self.assertIsNone(game.pull_now(self.world, self.wil, T0 + wil["rest"] / fight["pace"] + 1))
|
||
self.assertEqual(wil["hp_in"], game.max_hp(self.wil))
|
||
self.assertLess(game.member_of(fight, "kay")["hp_in"], game.max_hp(self.kay))
|
||
|
||
def test_adds_join_halfway_and_never_count_if_they_dont(self):
|
||
foes = game.DUNGEONS["dungeon_watchtower"]["pulls"][7][1]
|
||
self.assertTrue(foes[1]["dormant"] and foes[2]["dormant"])
|
||
players = [self.wil, self.kay]
|
||
fight = game.draw_pull(self.world, players, game.ACTION_BY_ID["dungeon_watchtower"], {"pull": 7, "wipes": 0},
|
||
random.Random(2), T0)
|
||
joined = [e for e in fight["events"] if e[3] == "join"]
|
||
yell = next(e for e in fight["events"] if e[3] == "yell")
|
||
self.assertEqual(yell[6], "Crush them!")
|
||
self.assertEqual({e[1] for e in joined}, {"e1", "e2"})
|
||
self.assertGreaterEqual(joined[0][0], yell[0])
|
||
|
||
|
||
class LootTest(unittest.TestCase):
|
||
def smith(self, level=30):
|
||
world = world_with(("sam", "miner"))
|
||
p = world["players"]["sam"]
|
||
p["xp"]["smithing"] = game.XP_TABLE[level]
|
||
return world, p
|
||
|
||
def test_every_dungeon_has_relic_patterns_and_a_unique(self):
|
||
for key in game.DUNGEONS:
|
||
loot = game.ACTION_BY_ID[key]["loot"]
|
||
self.assertEqual(len(loot["patterns"]), 7)
|
||
self.assertTrue(game.ITEMS[loot["unique"]]["mods"])
|
||
for pattern in loot["patterns"]:
|
||
made = game.ITEMS[game.ITEMS[pattern]["pattern"]]
|
||
self.assertEqual(made["req"], ["combat", game.DUNGEONS[key]["level"]])
|
||
base = game.ITEMS[made["base"]]
|
||
for stat in ("power", "armour"):
|
||
if base.get(stat):
|
||
self.assertGreater(made[stat], base[stat])
|
||
|
||
def test_bosses_drop_relics_and_patterns(self):
|
||
ogre = game.DUNGEONS["dungeon_watchtower"]["pulls"][7][1]
|
||
fight = {"pack": [dict(ogre[0])], "kills": ["e0"]}
|
||
found = {}
|
||
for seed in range(200):
|
||
for item, qty in game.pack_loot(game.ACTION_BY_ID["dungeon_watchtower"], fight, random.Random(seed))[1].items():
|
||
found[item] = found.get(item, 0) + qty
|
||
self.assertEqual(found["watchtower_relic"], 400) # two, every time
|
||
patterns = sum(qty for item, qty in found.items() if item.startswith("pattern_"))
|
||
self.assertTrue(40 < patterns < 100) # about 35%
|
||
self.assertGreater(found.get("ogres_knuckle", 0), 0)
|
||
|
||
def test_learn_a_pattern_and_reforge(self):
|
||
world, p = self.smith()
|
||
reforge = game.ACTION_BY_ID["reforge_ogre_forged_warhammer"]
|
||
p["bank"] = {"iron_warhammer": 1, "watchtower_relic": 3}
|
||
self.assertEqual(game.can_start(world, p, reforge), "Learn its pattern first")
|
||
self.assertEqual(game.learn_pattern(p, "pattern_ogre_forged_warhammer"), "You don't have it")
|
||
p["bank"]["pattern_ogre_forged_warhammer"] = 1
|
||
p["xp"]["smithing"] = game.XP_TABLE[20]
|
||
self.assertEqual(game.learn_pattern(p, "pattern_ogre_forged_warhammer"), "Needs smithing level 29")
|
||
p["xp"]["smithing"] = game.XP_TABLE[30]
|
||
self.assertIsNone(game.learn_pattern(p, "pattern_ogre_forged_warhammer"))
|
||
self.assertNotIn("pattern_ogre_forged_warhammer", p["bank"])
|
||
self.assertEqual(game.learn_pattern(p, "pattern_ogre_forged_warhammer"), "You don't have it")
|
||
world["city"]["buildings"]["smithy"]["level"] = 5
|
||
self.assertIsNone(game.start_action(world, p, "reforge_ogre_forged_warhammer", T0, once=True))
|
||
game.process_player(world, p, T0 + 3600, random.Random(1))
|
||
self.assertEqual(p["bank"].get("ogre_forged_warhammer"), 1)
|
||
self.assertNotIn("watchtower_relic", p["bank"])
|
||
|
||
def ranger(self, *uniques):
|
||
world = world_with(("ash", "ranger"))
|
||
p = world["players"]["ash"]
|
||
p["xp"]["combat"] = game.XP_TABLE[85]
|
||
p["equip"].update(weapon="magic_bow", helmet="mammoth_leather_cap", body="mammoth_leather_body",
|
||
legs="mammoth_leather_legs")
|
||
for unique in uniques:
|
||
p["equip"][game.ITEMS[unique]["slot"]] = unique
|
||
p["hp"] = game.max_hp(p)
|
||
p["bank"] = {"arrows": 1000}
|
||
return world, p
|
||
|
||
def test_a_unique_changes_an_ability(self):
|
||
world, p = self.ranger()
|
||
self.assertEqual(game.fighter(p, game.NO_BLESSING, {})["mods"], {})
|
||
plain = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_scouts"], random.Random(4))
|
||
world, p = self.ranger("wyrmscale_quiver")
|
||
mods = game.fighter(p, game.NO_BLESSING, {})["mods"]
|
||
self.assertEqual(mods["volley"], {"c": 1.4})
|
||
quivered = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_scouts"], random.Random(4))
|
||
aims = lambda f: [e[0] for e in f["events"] if e[1] == "ash" and e[6] == "aimed_shot" and e[3] in ("cast", "hit", "crit")]
|
||
cast, landed = aims(quivered)[:2]
|
||
self.assertAlmostEqual(landed - cast, 1.5, places=1) # 2s of aiming, a quarter shorter
|
||
|
||
def test_a_pendant_mends_a_second_ally(self):
|
||
world = world_with(("kay", "homesteader"), ("wil", "builder"), ("bo", "miner"))
|
||
players = list(world["players"].values())
|
||
for p in players:
|
||
p["xp"]["combat"] = game.XP_TABLE[40]
|
||
p["hp"] = game.max_hp(p)
|
||
kay = world["players"]["kay"]
|
||
kay["equip"].update(weapon="willow_staff", amulet="barrow_kings_pendant")
|
||
ogre = game.ACTION_BY_ID["fight_ruins"]
|
||
heals = 0
|
||
for seed in range(5):
|
||
fight = game.draw_fight(world, players, ogre, random.Random(seed))
|
||
mends = [e for e in fight["events"] if e[1] == "kay" and e[3] == "heal" and e[6] == "mend"]
|
||
heals += sum(1 for a, b in zip(mends, mends[1:]) if a[0] == b[0] and a[2] != b[2])
|
||
self.assertGreater(heals, 0) # one cast, two people healed
|
||
|
||
def test_old_worlds_get_patterns(self):
|
||
world, p = self.smith()
|
||
del p["patterns"]
|
||
world["version"] = 29
|
||
game.migrate(world, T0)
|
||
self.assertEqual(p["patterns"], [])
|
||
|
||
def test_achievements_read_what_you_have(self):
|
||
world, p = self.smith()
|
||
card = lambda name: next(c for s in game.achievements(p) for c in s["cards"] if c["name"] == name)
|
||
done = lambda name: [e["name"] for e in card(name)["entries"] if e["done"]]
|
||
self.assertEqual(len(card("Watchtower Depths")["entries"]), 7)
|
||
self.assertEqual(done("Watchtower Depths"), [])
|
||
p["patterns"].append("pattern_ogre_forged_warhammer")
|
||
self.assertEqual(done("Watchtower Depths"), ["Ogre-forged warhammer"])
|
||
# A unique stays found after it's gone.
|
||
game.bank_add(world, p, "ogres_knuckle", 1, T0, game.empty_summary())
|
||
game.bank_take(p, "ogres_knuckle", 1)
|
||
self.assertEqual(done("Uniques"), ["Ogre's knuckle"])
|
||
self.assertEqual(done("Masters"), [])
|
||
p["xp"]["mining"] = game.XP_TABLE[50]
|
||
self.assertEqual(done("Masters"), ["Master miner"])
|
||
|
||
def test_old_worlds_find_the_uniques_they_hold(self):
|
||
world, p = self.smith()
|
||
del p["found"]
|
||
p["equip"]["ring"] = "ogres_knuckle"
|
||
world["version"] = 30
|
||
game.migrate(world, T0)
|
||
self.assertEqual(p["found"], ["ogres_knuckle"])
|
||
|
||
|
||
class NewSkillsTest(unittest.TestCase):
|
||
def test_tools_speed_up_gathering(self):
|
||
world = world_with(("pia", "commoner"))
|
||
p = world["players"]["pia"]
|
||
plain = game.action_time(world, p, game.ACTION_BY_ID["mine_stone"])
|
||
p["equip"]["tool_mining"] = "iron_pickaxe"
|
||
self.assertAlmostEqual(game.action_time(world, p, game.ACTION_BY_ID["mine_stone"]), plain * 0.7)
|
||
self.assertEqual(game.action_time(world, p, game.ACTION_BY_ID["cut_logs"]), plain)
|
||
|
||
def test_fish_to_plate(self):
|
||
world = world_with(("sam", "ranger"))
|
||
p = world["players"]["sam"]
|
||
game.start_action(world, p, "fish_shrimp", T0)
|
||
game.process_player(world, p, T0 + 200, random.Random(1))
|
||
self.assertEqual(p["bank"]["raw_shrimp"], 10) # 20s with the scout bonus
|
||
p["bank"]["logs"] = 10
|
||
game.start_action(world, p, "cook_shrimp", T0 + 200)
|
||
summary = game.process_player(world, p, T0 + 1000, random.Random(1))
|
||
self.assertEqual(p["bank"]["cooked_shrimp"], 10 + summary["lucky"]) # rangers cook their catch, sometimes double
|
||
self.assertIn("cooked_shrimp", game.FOOD_ORDER)
|
||
|
||
def test_hides_to_armour(self):
|
||
world = world_with(("kay", "miner"))
|
||
p = world["players"]["kay"]
|
||
p["bank"] = {"hide": 4}
|
||
game.start_action(world, p, "tan_leather", T0)
|
||
game.process_player(world, p, T0 + 1000, random.Random(1))
|
||
self.assertGreaterEqual(p["bank"]["leather"], 4)
|
||
p["xp"]["leatherworking"] = game.XP_TABLE[12]
|
||
self.assertIsNone(game.start_action(world, p, "lw_body", T0 + 1000))
|
||
|
||
def test_v2_worlds_gain_hp_and_slots(self):
|
||
world = world_with(("fred", "miner"))
|
||
p = world["players"]["fred"]
|
||
world["version"] = 2
|
||
for key in ("hp", "hp_at", "food"):
|
||
del p[key]
|
||
p["equip"] = {"weapon": "copper_sword", "shield": None}
|
||
del p["xp"]["fishing"], p["xp"]["leatherworking"]
|
||
game.migrate(world)
|
||
self.assertEqual(world["version"], game.WORLD_VERSION)
|
||
self.assertEqual(p["hp"], game.max_hp(p))
|
||
self.assertEqual(p["equip"]["weapon"], "copper_sword")
|
||
self.assertIsNone(p["equip"]["tool_fishing"])
|
||
self.assertEqual(p["xp"]["fishing"], 0)
|
||
|
||
|
||
|
||
class FavourTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("fred", "miner"))
|
||
self.p = self.world["players"]["fred"]
|
||
self.p["favour"] = 5000
|
||
|
||
def test_master_builder_doubles_labour_and_xp_for_a_while(self):
|
||
world, p = self.world, self.p
|
||
self.assertEqual(game.spend_favour(world, p, "builders", T0), "Nothing is being built")
|
||
world["city"]["construction"] = {"building": "walls", "repair": False, "start": T0, "labour": 0.0, "needed": 360}
|
||
self.assertEqual(game.favours(world, T0)["builders"]["cost"], 150)
|
||
self.assertIsNone(game.spend_favour(world, p, "builders", T0))
|
||
self.assertEqual(game.favours(world, T0)["builders"]["cost"], 225)
|
||
self.assertEqual(world["city"]["construction"]["labour"], 0) # nothing is built for you
|
||
game.start_action(world, p, "labour", T0)
|
||
summary = game.process_player(world, p, T0 + 30 * 60, random.Random(1)) # 30s an action
|
||
self.assertEqual(summary["done"], 60)
|
||
self.assertEqual(summary["labour"], 60 + 50) # the first 50 labour counted double
|
||
self.assertEqual(world["city"]["site_boost"], 0)
|
||
self.assertEqual(summary["xp"]["construction"], 10 * 60 + 10 * 50)
|
||
|
||
def test_master_builder_price_resets_on_a_new_site(self):
|
||
world, p = self.world, self.p
|
||
city = world["city"]
|
||
city["construction"] = {"building": "walls", "repair": False, "start": T0, "labour": 359.0, "needed": 360}
|
||
game.spend_favour(world, p, "builders", T0)
|
||
city["buildings"]["houses"]["fund"] = {k: v for k, v in game.build_cost("houses", 1).items()}
|
||
city["queue"] = ["houses"]
|
||
city["construction"]["labour"] = 360
|
||
game.tick(world, T0 + 1, random.Random(1))
|
||
self.assertEqual(city["construction"]["building"], "houses")
|
||
self.assertEqual(game.favours(world, T0)["builders"]["cost"], 150)
|
||
self.assertEqual(city["site_boost"], 50) # unused boost carries over
|
||
|
||
def test_informant_doubles_a_scouts_recon_for_a_while(self):
|
||
world, p = self.world, self.p
|
||
self.assertEqual(game.spend_favour(world, p, "informant", T0), "No news to dig into")
|
||
world["next_event_at"] = T0
|
||
game.tick(world, T0, random.Random(2))
|
||
event = game.upcoming_raid(world)
|
||
world["players"]["sam"] = scout = game.new_player("sam", "Sam", "commoner", T0)
|
||
self.assertIsNone(game.spend_favour(world, p, "informant", T0))
|
||
self.assertEqual(event["recon"], 0) # leads, not intel
|
||
self.assertEqual(game.favours(world, T0)["informant"]["cost"], 375)
|
||
game.start_action(world, scout, "recon_roads", T0)
|
||
game.process_player(world, scout, T0 + 60 * 10 + 1, random.Random(1)) # ten scouting runs
|
||
self.assertEqual(event["recon"], 20)
|
||
self.assertEqual(scout["xp"]["reconnaissance"], 240)
|
||
self.assertEqual(world["city"]["recon_boost"], 20)
|
||
event["recon"] = game.intel_costs(event)[-1]
|
||
self.assertEqual(game.spend_favour(world, p, "informant", T0), "Nothing more to learn")
|
||
|
||
def test_drilling_takes_as_long_as_the_scouting(self):
|
||
world, p = self.world, self.p
|
||
self.assertEqual(game.start_action(world, p, "drill_militia", T0), "No raid to prepare for")
|
||
world["next_event_at"] = T0
|
||
game.tick(world, T0, random.Random(2))
|
||
raid = game.upcoming_raid(world)
|
||
world["city"]["population"] = 20.0
|
||
needed = game.drill_needed(raid)
|
||
self.assertEqual(needed, game.intel_costs(raid)[-1])
|
||
before = game.defence(world)["total"]
|
||
self.assertIsNone(game.start_action(world, p, "drill_militia", T0))
|
||
game.process_player(world, p, T0 + 60 * 10 + 1, random.Random(1))
|
||
self.assertEqual(raid["drilled"], 10)
|
||
self.assertAlmostEqual(game.defence(world)["parts"]["drilled"], round(20 * 10 / needed, 1))
|
||
self.assertAlmostEqual(game.defence(world)["total"], before + 20 * 10 / needed, places=0)
|
||
self.assertEqual(p["xp"]["combat"], 120)
|
||
self.assertEqual(p["hp"], 44) # drilling is not a fight
|
||
game.process_player(world, p, T0 + 60 * (needed + 5), random.Random(1))
|
||
self.assertEqual(game.defence(world)["parts"]["drilled"], 20.0)
|
||
self.assertIsNone(p["action"]) # stops once the militia is fully drilled
|
||
game.tick(world, raid["fire_at"], random.Random(1))
|
||
self.assertEqual(game.defence(world)["parts"]["drilled"], 0)
|
||
|
||
def test_seasoned_fighters_drill_a_little_faster(self):
|
||
self.assertEqual(game.drill_per_action(self.p), 1.0)
|
||
self.p["xp"]["combat"] = game.XP_TABLE[40]
|
||
self.assertEqual(game.drill_per_action(self.p), 1.5)
|
||
|
||
def test_drill_sergeant_doubles_drilling_for_a_while(self):
|
||
world, p = self.world, self.p
|
||
self.assertEqual(game.spend_favour(world, p, "sergeant", T0), "No raid to prepare for")
|
||
world["next_event_at"] = T0
|
||
game.tick(world, T0, random.Random(2))
|
||
self.assertIsNone(game.spend_favour(world, p, "sergeant", T0))
|
||
self.assertEqual(game.defence(world)["parts"]["drilled"], 0) # a sergeant drills nobody by himself
|
||
self.assertEqual(game.favours(world, T0)["sergeant"]["cost"], 225)
|
||
game.start_action(world, p, "drill_militia", T0)
|
||
game.process_player(world, p, T0 + 60 * 4 + 1, random.Random(1))
|
||
self.assertEqual(game.upcoming_raid(world)["drilled"], 8)
|
||
self.assertEqual(p["xp"]["combat"], 96)
|
||
self.assertEqual(world["city"]["drill_boost"], 26)
|
||
self.assertEqual(p["favour"], 5000 - 150)
|
||
|
||
def test_not_enough_favour(self):
|
||
self.p["favour"] = 10
|
||
self.world["city"]["construction"] = {"building": "walls", "repair": False, "start": T0, "labour": 0.0, "needed": 360}
|
||
self.assertEqual(game.spend_favour(self.world, self.p, "builders", T0), "Needs 150 favour")
|
||
|
||
|
||
|
||
class TierTest(unittest.TestCase):
|
||
def test_steel_needs_coal_and_the_smithy(self):
|
||
world = world_with(("kay", "miner"))
|
||
p = world["players"]["kay"]
|
||
p["xp"]["smithing"] = game.XP_TABLE[30]
|
||
p["bank"] = {"iron_ore": 5, "coal": 10, "willow_logs": 5}
|
||
self.assertEqual(game.start_action(world, p, "smith_steel_bar", T0), "Needs Smithy level 2")
|
||
world["city"]["buildings"]["smithy"]["level"] = 2
|
||
self.assertIsNone(game.start_action(world, p, "smith_steel_bar", T0))
|
||
game.process_player(world, p, T0 + 1000, random.Random(1))
|
||
self.assertGreaterEqual(p["bank"]["steel_bar"], 5)
|
||
self.assertNotIn("iron_ore", p["bank"])
|
||
|
||
def test_every_tier_has_full_kit(self):
|
||
for key, *_ in game.METALS:
|
||
for piece in ("sword", "warhammer", "helmet", "legs", "body", "pickaxe", "axe", "hoe", "fishing_rod", "hunting_knife"):
|
||
self.assertIn(f"{key}_{piece}", game.ITEMS)
|
||
self.assertIn(f"smith_{key}_{piece}", game.ACTION_BY_ID)
|
||
self.assertIn(f"make_{key}_shield", game.ACTION_BY_ID) # builders make shields
|
||
for *_, prefix, _label in game.LEATHERS:
|
||
for piece in ("cap", "legs", "body"):
|
||
self.assertIn(f"{prefix}_{piece}", game.ITEMS)
|
||
for *_, prefix, _label in game.CLOTHS:
|
||
for piece in ("hood", "legs", "body"):
|
||
self.assertIn(f"{prefix}_{piece}", game.ITEMS)
|
||
self.assertIn(f"sew_{prefix}_{piece}", game.ACTION_BY_ID)
|
||
self.assertEqual(game.ITEMS["rune_body"]["plural"], "Rune cuirasses")
|
||
|
||
def test_gear_needs_levels_to_wear(self):
|
||
world = world_with(("wil", "commoner"))
|
||
p = world["players"]["wil"]
|
||
self.assertEqual(game.equip_problem(p, "rune_sword"), "Needs combat level 75")
|
||
self.assertEqual(game.equip_problem(p, "steel_pickaxe"), "Needs mining level 30")
|
||
self.assertIsNone(game.equip_problem(p, "copper_sword"))
|
||
p["xp"]["combat"] = game.XP_TABLE[75]
|
||
self.assertIsNone(game.equip_problem(p, "rune_sword"))
|
||
|
||
def test_militia_takes_the_best_weapons_first(self):
|
||
world = world_with()
|
||
world["city"]["stockpile"] = {"copper_sword": 50, "rune_sword": 2, "iron_shield": 3}
|
||
world["city"]["wages"] = 10_000
|
||
parts = game.defence(world)["parts"]
|
||
armed = int(world["city"]["population"] // 2) # 10
|
||
self.assertEqual(parts["weapons"], round(0.35 * (2 * 35 + (armed - 2) * 4), 1))
|
||
self.assertEqual(parts["shields"], 0.5 * 3 * 4)
|
||
|
||
def test_higher_building_levels_need_higher_tiers(self):
|
||
self.assertNotIn("steel_bar", game.build_cost("walls", 2))
|
||
self.assertEqual(game.build_cost("walls", 3)["steel_bar"], 60)
|
||
self.assertEqual(game.build_cost("walls", 5)["mithril_brackets"], 60)
|
||
self.assertEqual(game.build_cost("walls", 5)["steel_bar"], 180)
|
||
|
||
def test_dragons_drop_their_areas_reagents(self):
|
||
world = world_with(("wil", "commoner"), ("kay", "commoner"))
|
||
for p in world["players"].values():
|
||
p["xp"]["combat"] = game.XP_TABLE[80]
|
||
p["equip"].update(weapon="rune_sword", shield="rune_shield", helmet="rune_helmet", body="rune_body",
|
||
legs="rune_legs")
|
||
p["hp"] = game.max_hp(p)
|
||
p["bank"] = {"cooked_swordfish": 200}
|
||
p["last_active"] = T0 + 3600 * 2
|
||
p = world["players"]["wil"]
|
||
game.start_action(world, p, "fight_green_dragon", T0) # a group fight
|
||
game.join_fight(world, world["players"]["kay"], "wil", T0)
|
||
game.catch_up(world, T0 + 3600 * 2, random.Random(3))
|
||
self.assertGreater(sum(p["bank"].get(game.reagent(weight, 3), 0) for weight in game.WEIGHTS), 30)
|
||
|
||
|
||
|
||
class PrayerTest(unittest.TestCase):
|
||
def priest(self, level=1, combat=10):
|
||
world = world_with(("pat", "commoner"))
|
||
p = world["players"]["pat"]
|
||
p["xp"]["prayer"] = game.XP_TABLE[level]
|
||
p["xp"]["combat"] = game.XP_TABLE[combat]
|
||
p["hp"] = game.max_hp(p)
|
||
return world, p
|
||
|
||
def test_burying_bones_gives_points_and_xp(self):
|
||
world, p = self.priest()
|
||
p["bank"] = {"bones": 5}
|
||
game.start_action(world, p, "bury_bones", T0)
|
||
game.process_player(world, p, T0 + 100, random.Random(1))
|
||
self.assertEqual(p["pp"], 10)
|
||
self.assertEqual(p["xp"]["prayer"], game.XP_TABLE[1] + 20)
|
||
world["city"]["buildings"]["temple"]["level"] = 2
|
||
self.assertEqual(game.bone_points(world, game.ACTION_BY_ID["bury_bones"]), 4)
|
||
|
||
def test_choosing_prayers(self):
|
||
world, p = self.priest(level=10)
|
||
self.assertEqual(game.set_prayers(p, ["steel_skin"]), "Steel Skin needs prayer level 20")
|
||
self.assertEqual(game.set_prayers(p, ["thick_skin", "burst_of_strength", "rapid_heal"]), "At most 2 prayers at once")
|
||
self.assertIsNone(game.set_prayers(p, ["thick_skin", "burst_of_strength"]))
|
||
|
||
def test_prayers_cost_points_cut_damage_and_train_prayer(self):
|
||
world, p = self.priest(level=10)
|
||
scouts = game.ACTION_BY_ID["fight_scouts"]
|
||
plain = game.fight_odds(p, scouts)["damage"]
|
||
game.set_prayers(p, ["thick_skin", "burst_of_strength"])
|
||
p["pp"] = 4
|
||
blessed = game.blessing(p)
|
||
self.assertEqual(blessed["cost"], 2)
|
||
self.assertLess(game.fight_odds(p, scouts)["damage"], plain * 0.9)
|
||
p["bank"] = {"bread": 200}
|
||
game.start_action(world, p, "fight_scouts", T0)
|
||
summary = game.process_player(world, p, T0 + 600, random.Random(1))
|
||
self.assertEqual(p["pp"], 0)
|
||
self.assertEqual(summary["pp_spent"], 4)
|
||
self.assertTrue(summary["prayer_out"])
|
||
self.assertEqual(p["xp"]["prayer"], game.XP_TABLE[10] + 4 * game.PRAYER_XP_PER_POINT)
|
||
|
||
def test_second_wind_saves_one_knockout(self):
|
||
world, p = self.priest(level=40, combat=10)
|
||
game.set_prayers(p, ["second_wind"])
|
||
p["pp"] = 1000
|
||
game.start_action(world, p, "fight_scouts", T0)
|
||
summary = game.process_player(world, p, T0 + 6 * 3600, random.Random(1))
|
||
self.assertEqual(summary["second_wind"], 1)
|
||
self.assertEqual(summary["stopped"], "Knocked out 3 times in a row")
|
||
|
||
def test_rapid_heal_mends_during_fights(self):
|
||
world, p = self.priest(level=10, combat=10)
|
||
scouts = game.ACTION_BY_ID["fight_scouts"]
|
||
p["pp"] = 1000
|
||
p["equip"].update(weapon="copper_sword", helmet="copper_helmet", body="copper_body", legs="copper_legs")
|
||
plain = game.simulate_fight(world, p, scouts, random.Random(2))
|
||
game.set_prayers(p, ["rapid_heal"])
|
||
mended = game.simulate_fight(world, p, scouts, random.Random(2))
|
||
self.assertEqual([e[:4] for e in plain["events"] if e[1] == p["id"]][:5],
|
||
[e[:4] for e in mended["events"] if e[1] == p["id"]][:5]) # the same fight
|
||
self.assertGreater(game.member_of(mended, p["id"])["hp"], game.member_of(plain, p["id"])["hp"])
|
||
self.assertLess(game.fight_odds(p, scouts)["damage"], game.fight_odds({**p, "prayers": []}, scouts)["damage"])
|
||
|
||
def test_offerings_ward_the_city_until_the_raid(self):
|
||
world, p = self.priest(level=15)
|
||
world["city"]["buildings"]["temple"]["level"] = 1
|
||
p["bank"] = {"big_bones": 10}
|
||
game.start_action(world, p, "offer_big_bones", T0)
|
||
game.process_player(world, p, T0 + 1000, random.Random(1))
|
||
self.assertEqual(world["city"]["devotion"], 10 * 6 * 1.25)
|
||
self.assertEqual(game.defence(world)["parts"]["ward"], min(75 / 4, 15))
|
||
world["next_event_at"] = T0
|
||
game.tick(world, T0 + 1000, random.Random(2))
|
||
game.tick(world, game.upcoming_raid(world)["fire_at"], random.Random(1))
|
||
self.assertEqual(world["city"]["devotion"], 0)
|
||
|
||
def test_offering_needs_a_temple(self):
|
||
world, p = self.priest()
|
||
p["bank"] = {"bones": 1}
|
||
self.assertEqual(game.start_action(world, p, "offer_bones", T0), "Needs Temple level 1")
|
||
|
||
def test_v4_worlds_get_prayer(self):
|
||
world = world_with(("fred", "miner"))
|
||
world["version"] = 4
|
||
p = world["players"]["fred"]
|
||
del p["pp"], p["prayers"], p["xp"]["prayer"]
|
||
del world["city"]["buildings"]["temple"]
|
||
game.migrate(world, T0)
|
||
self.assertEqual((p["pp"], p["prayers"], p["xp"]["prayer"]), (0, [], 0))
|
||
self.assertEqual(world["city"]["buildings"]["temple"]["level"], 0)
|
||
|
||
|
||
|
||
class HerbloreTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("hal", "commoner"))
|
||
self.p = self.world["players"]["hal"]
|
||
|
||
def test_herbs_to_potion(self):
|
||
world, p = self.world, self.p
|
||
game.start_action(world, p, "grow_guam_leaf", T0)
|
||
game.process_player(world, p, T0 + 450, random.Random(1))
|
||
self.assertEqual(p["bank"]["guam_leaf"], 10)
|
||
p["bank"]["logs"] = 6 # one a potion, one to burn under the still
|
||
self.assertIsNone(game.start_action(world, p, "brew_woodcutting_potion_1", T0 + 450)) # tier I needs no Herblore
|
||
game.process_player(world, p, T0 + 1000, random.Random(1))
|
||
self.assertEqual(p["bank"]["woodcutting_potion_1"], 3)
|
||
self.assertEqual(game.start_action(world, p, "brew_woodcutting_potion_2", T0 + 1000), "Needs level 15")
|
||
|
||
def test_potion_tiers_climb_the_ladders(self):
|
||
mining = [potion for potion in game.POTIONS if potion[0].startswith("mining_potion")]
|
||
self.assertEqual([(herb, second) for _id, _name, _level, herb, second, *_rest in mining[:2]],
|
||
[("guam_leaf", "copper_ore"), ("marrentill", "iron_ore")])
|
||
self.assertEqual([p[2] for p in mining], game.TIER_LEVELS)
|
||
self.assertEqual([p[6]["speed"] for p in mining], game.SPEEDS)
|
||
self.assertEqual(game.ITEMS["mining_potion_6"]["name"], "Mining potion VI")
|
||
self.assertFalse(any("heal" in p[6] or "double" in p[6] for p in game.POTIONS))
|
||
|
||
def test_skill_potion_speeds_up_and_uses_charges(self):
|
||
world, p = self.world, self.p
|
||
plain = game.action_time(world, p, game.ACTION_BY_ID["cut_logs"])
|
||
p["bank"] = {"woodcutting_potion_3": 1}
|
||
self.assertIsNone(game.use_potion(p, "woodcutting_potion_3"))
|
||
self.assertEqual(p["potions"]["woodcutting"], {"item": "woodcutting_potion_3", "charges": game.POTION_CHARGES})
|
||
self.assertAlmostEqual(game.action_time(world, p, game.ACTION_BY_ID["cut_logs"]), plain * 0.8)
|
||
self.assertEqual(game.action_time(world, p, game.ACTION_BY_ID["mine_stone"]), plain)
|
||
game.start_action(world, p, "cut_logs", T0)
|
||
summary = game.process_player(world, p, T0 + 16 * 60, random.Random(1))
|
||
self.assertEqual(summary["potion_out"], ["woodcutting_potion_3"])
|
||
self.assertNotIn("woodcutting", p["potions"])
|
||
|
||
def test_empty_potion_is_replaced_from_the_bank(self):
|
||
world, p = self.world, self.p
|
||
p["bank"] = {"hunting_potion_1": 2}
|
||
game.use_potion(p, "hunting_potion_1")
|
||
p["potions"]["hunting"]["charges"] = 1
|
||
game.start_action(world, p, "hunt_birds", T0)
|
||
summary = game.process_player(world, p, T0 + 25, random.Random(1))
|
||
self.assertEqual(summary["potions_drunk"], {"hunting_potion_1": 1})
|
||
self.assertEqual(p["potions"]["hunting"]["charges"], game.POTION_CHARGES)
|
||
self.assertNotIn("hunting_potion_1", p["bank"])
|
||
|
||
def test_keep_chance_saves_materials(self):
|
||
world, p = self.world, self.p
|
||
p["bank"] = {"copper_ore": 100, "logs": 100, "smithing_potion_2": 1}
|
||
game.use_potion(p, "smithing_potion_2")
|
||
p["potions"]["smithing"]["charges"] = 1000
|
||
game.start_action(world, p, "smith_copper_bar", T0)
|
||
summary = game.process_player(world, p, T0 + 20 * 100, random.Random(2))
|
||
self.assertGreater(summary["kept"], 5)
|
||
self.assertEqual(p["bank"]["copper_ore"], 100 - (100 - summary["kept"]))
|
||
|
||
def test_combat_potions_add_power_and_cut_damage(self):
|
||
world, p = self.world, self.p
|
||
p["xp"]["combat"] = game.XP_TABLE[20]
|
||
p["hp"] = game.max_hp(p)
|
||
p["bank"] = {"defence_potion_6": 1, "bread": 500}
|
||
game.use_potion(p, "defence_potion_6")
|
||
game.start_action(world, p, "fight_scouts", T0)
|
||
scouts = game.ACTION_BY_ID["fight_scouts"]
|
||
per_second = lambda odds: odds["damage"] / odds["kill_time"] # less eating also shortens the fight
|
||
brewed = per_second(game.fight_odds(p, scouts))
|
||
active = p["potions"].pop("combat")
|
||
self.assertAlmostEqual(brewed, per_second(game.fight_odds(p, scouts)) * 0.70, delta=0.02)
|
||
p["potions"]["combat"] = active
|
||
summary = game.process_player(world, p, T0 + 75 * 10, random.Random(1))
|
||
self.assertEqual(p["potions"]["combat"]["charges"], game.POTION_CHARGES - summary["done"])
|
||
|
||
def test_old_potions_become_their_tiers(self):
|
||
world, p = self.world, self.p
|
||
world["version"] = 10
|
||
p["bank"] = {"lumberjacks_tonic": 4, "woodcutting_potion_3": 1, "regeneration_potion": 2}
|
||
p["potions"] = {"mining": {"item": "prospectors_draught", "charges": 12}}
|
||
p["action"] = {"id": "brew_anglers_brew", "start": T0, "resume": "brew_smiths_focus"}
|
||
world["city"]["stockpile"]["super_strength"] = 1
|
||
game.migrate(world, T0)
|
||
self.assertEqual(p["bank"], {"woodcutting_potion_3": 5, "defence_potion_3": 2})
|
||
self.assertEqual(p["potions"]["mining"], {"item": "mining_potion_2", "charges": 12})
|
||
self.assertEqual((p["action"]["id"], p["action"]["resume"]), ("brew_fishing_potion_3", "brew_smithing_potion_2"))
|
||
self.assertEqual(world["city"]["stockpile"]["strength_potion_4"], 1)
|
||
for action in (p["action"]["id"], p["action"]["resume"]):
|
||
self.assertIn(action, game.ACTION_BY_ID)
|
||
|
||
def test_v5_worlds_get_herblore(self):
|
||
world = world_with(("fred", "miner"))
|
||
world["version"] = 5
|
||
p = world["players"]["fred"]
|
||
del p["potions"], p["xp"]["herblore"]
|
||
game.migrate(world, T0)
|
||
self.assertEqual((p["potions"], p["xp"]["herblore"]), ({}, 0))
|
||
|
||
|
||
|
||
class ConstructionTest(unittest.TestCase):
|
||
def site(self):
|
||
world = world_with(("bob", "miner"), ("ann", "commoner"))
|
||
world["city"]["construction"] = {"building": "walls", "repair": False, "start": T0, "labour": 0.0, "needed": 360}
|
||
world["next_event_at"] = None
|
||
return world
|
||
|
||
def test_players_build_with_their_hands(self):
|
||
world = self.site()
|
||
p = world["players"]["ann"]
|
||
game.start_action(world, p, "labour", T0)
|
||
summary = game.process_player(world, p, T0 + 30 * 100, random.Random(1))
|
||
self.assertEqual(summary["labour"], 100)
|
||
self.assertEqual(world["city"]["construction"]["labour"], 100)
|
||
self.assertGreater(p["xp"]["construction"], 0)
|
||
|
||
def test_finishing_the_site_raises_the_building_and_stops_the_work(self):
|
||
world = self.site()
|
||
world["city"]["construction"]["labour"] = 355
|
||
p = world["players"]["ann"]
|
||
game.start_action(world, p, "labour", T0)
|
||
summary = game.process_player(world, p, T0 + 30 * 10, random.Random(1))
|
||
self.assertEqual(world["city"]["buildings"]["walls"]["level"], 1)
|
||
self.assertIsNone(world["city"]["construction"])
|
||
self.assertEqual(summary["stopped"], "Nothing is being built")
|
||
|
||
def test_townsfolk_trickle_and_levels_help(self):
|
||
world = self.site()
|
||
world["city"]["population"] = 25.0 # houses full, so the workforce stays put
|
||
game.tick(world, T0 + 10 * game.HOUR, random.Random(1))
|
||
self.assertAlmostEqual(world["city"]["construction"]["labour"], 10 * 25 * game.TOWNSFOLK_LABOUR, delta=5)
|
||
p = world["players"]["bob"]
|
||
self.assertEqual(game.labour_per_action(p), 1)
|
||
p["xp"]["construction"] = game.XP_TABLE[40]
|
||
self.assertEqual(game.labour_per_action(p), 3)
|
||
plain = game.action_time(world, p, game.ACTION_BY_ID["labour"])
|
||
p["equip"]["tool_construction"] = "iron_hammer"
|
||
self.assertAlmostEqual(game.action_time(world, p, game.ACTION_BY_ID["labour"]), plain * 0.7)
|
||
|
||
def test_no_site_no_work(self):
|
||
world = world_with(("ann", "commoner"))
|
||
self.assertEqual(game.start_action(world, world["players"]["ann"], "labour", T0), "Nothing is being built")
|
||
|
||
def test_planks_and_hammers_exist_for_every_tier(self):
|
||
for plank, _name in game.PLANKS:
|
||
self.assertIn(f"saw_{plank}", game.ACTION_BY_ID)
|
||
for key, *_ in game.METALS:
|
||
self.assertIn(f"smith_{key}_hammer", game.ACTION_BY_ID)
|
||
self.assertIn("oak_plank", game.build_cost("houses", 3))
|
||
|
||
def test_v6_timer_becomes_labour(self):
|
||
world = world_with(("fred", "miner"))
|
||
world["version"] = 6
|
||
p = world["players"]["fred"]
|
||
del p["xp"]["construction"], p["equip"]["tool_construction"]
|
||
world["city"]["construction"] = {"building": "houses", "repair": False, "start": T0, "done_at": T0 + 3 * 3600}
|
||
game.migrate(world, T0 + 3600)
|
||
work = world["city"]["construction"]
|
||
self.assertEqual(work["needed"], game.LABOUR_PER_LEVEL)
|
||
self.assertAlmostEqual(work["labour"], game.LABOUR_PER_LEVEL / 3)
|
||
self.assertNotIn("done_at", work)
|
||
self.assertIsNone(p["equip"]["tool_construction"])
|
||
|
||
|
||
|
||
class HappeningsTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("ann", "commoner"), ("sam", "ranger"))
|
||
self.world["next_event_at"] = None
|
||
for track in ("minor", "hardship", "fortune"):
|
||
self.world[f"next_{track}_at"] = None
|
||
self.city = self.world["city"]
|
||
self.rng = random.Random(4)
|
||
|
||
def test_weather_rolls_and_seasons_turn(self):
|
||
seen = set()
|
||
for hour in range(1, 24 * 14):
|
||
game.tick(self.world, T0 + hour * game.HOUR, self.rng)
|
||
seen.add(self.city["weather"]["kind"])
|
||
self.assertGreaterEqual(len(seen), 3)
|
||
self.assertEqual(game.season(self.world, T0)["name"], "Spring")
|
||
self.assertEqual(game.season(self.world, T0 + 8 * game.DAY)["name"], "Summer")
|
||
self.assertEqual(game.season(self.world, T0 + 22 * game.DAY)["name"], "Winter")
|
||
|
||
def test_weather_changes_on_time_and_always_to_something_new(self):
|
||
weather = self.city["weather"]
|
||
weather.update(kind="rain", next="clear", until=T0 + 10 * 60)
|
||
self.city["last_tick"] = T0
|
||
game.tick(self.world, T0 + 11 * 60, self.rng) # well inside the hour
|
||
self.assertEqual(weather["kind"], "clear")
|
||
for _ in range(50):
|
||
spell = weather["until"]
|
||
game.tick(self.world, spell, self.rng)
|
||
self.assertNotEqual(weather["next"], weather["kind"])
|
||
self.assertGreater(weather["until"], spell)
|
||
|
||
def test_days_count_midnights_not_24_hour_stretches(self):
|
||
from datetime import datetime
|
||
late = datetime(2026, 10, 18, 23, 0).timestamp()
|
||
world = game.new_world(late)
|
||
self.assertEqual(game.calendar_day(world, late + 1800), 0) # 23:30 the same evening
|
||
self.assertEqual(game.calendar_day(world, late + 2 * 3600), 1) # 01:00 the next morning
|
||
self.assertEqual(game.season(world, late + 2 * 3600)["day"], 2)
|
||
|
||
def test_weather_changes_skill_speed(self):
|
||
p = self.world["players"]["ann"]
|
||
farm = game.ACTION_BY_ID["farm_wheat"]
|
||
plain = game.action_time(self.world, p, farm)
|
||
self.city["weather"]["kind"] = "heat"
|
||
self.assertAlmostEqual(game.action_time(self.world, p, farm), plain * 0.85)
|
||
self.assertAlmostEqual(game.city_rate(self.world, "food"), 1.15)
|
||
|
||
def test_sickness_makes_the_city_eat_more(self):
|
||
game._hardship(self.world, T0, self.rng, "sickness")
|
||
self.assertAlmostEqual(game.city_rate(self.world, "food"), 2.0)
|
||
self.assertEqual(self.world["events"][-1]["report"]["icon"], "🤒")
|
||
|
||
def test_locusts_and_rot_take_from_stores_and_buildings_protect(self):
|
||
self.city["stockpile"] = {"bread": 100, "wheat": 100, "logs": 100, "stone": 100}
|
||
game._hardship(self.world, T0, self.rng, "locusts")
|
||
self.assertEqual(self.city["stockpile"]["bread"], 60)
|
||
self.assertEqual(self.city["stockpile"]["stone"], 100)
|
||
self.city["buildings"]["lumber_shed"]["level"] = 2
|
||
game._hardship(self.world, T0, self.rng, "rot")
|
||
self.assertEqual(self.city["stockpile"]["logs"], 85)
|
||
|
||
def test_storm_damages_a_building(self):
|
||
self.city["buildings"]["walls"]["level"] = 1
|
||
game._hardship(self.world, T0, self.rng, "storm")
|
||
self.assertTrue(self.city["buildings"]["walls"]["damaged"])
|
||
|
||
def test_fire_is_put_out_by_water_barrels(self):
|
||
self.city["stockpile"] = {"water_barrel": 6, "logs": 100}
|
||
game._hardship(self.world, T0, self.rng, "fire")
|
||
self.assertEqual(self.city["stockpile"], {"water_barrel": 1, "logs": 100})
|
||
self.assertIn("the fire brigade", game.city_uses("water_barrel"))
|
||
|
||
def test_winter_burns_firewood_or_the_city_goes_cold(self):
|
||
game._hardship(self.world, T0, self.rng, "winter")
|
||
self.city["stockpile"] = {"logs": 3}
|
||
game.tick(self.world, T0 + 4 * game.HOUR, self.rng)
|
||
self.assertNotIn("logs", self.city["stockpile"])
|
||
self.assertTrue(self.city["cold"])
|
||
|
||
def test_wolves_slow_farming_until_fighters_clear_them(self):
|
||
game._minor_threat(self.world, T0, self.rng, "wolves")
|
||
p = self.world["players"]["ann"]
|
||
self.assertEqual(game.skill_mods(self.world, game.ACTION_BY_ID["farm_wheat"])["speed"], 2.0)
|
||
p["xp"]["combat"] = game.XP_TABLE[30]
|
||
p["hp"] = game.max_hp(p)
|
||
self.assertIsNone(game.start_action(self.world, p, "clear_wolves", T0))
|
||
summary = game.process_player(self.world, p, T0 + 3600, random.Random(1))
|
||
self.assertIsNone(game.active_problem(self.world, "wolves"))
|
||
self.assertEqual(summary["stopped"], "That's been dealt with")
|
||
|
||
def test_collapse_blocks_ore_until_planks_shore_it_up(self):
|
||
game._minor_threat(self.world, T0, self.rng, "collapse")
|
||
p = self.world["players"]["ann"]
|
||
self.assertIn("collapsed", game.start_action(self.world, p, "mine_copper", T0))
|
||
self.assertIsNone(game.start_action(self.world, p, "mine_stone", T0))
|
||
p["bank"] = {"plank": 50}
|
||
self.assertIsNone(game.aid_problem(self.world, p, "collapse", "plank", "bank", T0))
|
||
self.assertEqual(p["bank"], {"plank": 20})
|
||
self.assertIsNone(game.active_problem(self.world, "collapse"))
|
||
|
||
def test_flood_recedes_on_its_own(self):
|
||
game._minor_threat(self.world, T0, self.rng, "flood")
|
||
game.tick(self.world, T0 + 37 * game.HOUR, self.rng)
|
||
self.assertIsNone(game.active_problem(self.world, "flood"))
|
||
|
||
def test_goblins_steal_every_hour(self):
|
||
self.city["stockpile"] = {"stone": 1000}
|
||
game._minor_threat(self.world, T0, self.rng, "goblins")
|
||
game.tick(self.world, T0 + 5 * game.HOUR, self.rng)
|
||
self.assertLess(self.city["stockpile"]["stone"], 1000)
|
||
|
||
def test_fortunes(self):
|
||
game._fortune(self.world, T0, self.rng, "harvest")
|
||
self.assertEqual(game.skill_mods(self.world, game.ACTION_BY_ID["farm_wheat"])["yield"], 2)
|
||
game._fortune(self.world, T0, self.rng, "master")
|
||
self.assertTrue(any(e["kind"] == "master" for e in self.city["effects"]))
|
||
before = self.city["treasury"]
|
||
game._fortune(self.world, T0, self.rng, "noble")
|
||
self.assertGreater(self.city["treasury"], before)
|
||
game._fortune(self.world, T0, self.rng, "pilgrims")
|
||
self.assertEqual(self.world["players"]["ann"]["bank"]["bones"], 10)
|
||
game.tick(self.world, T0 + 13 * game.HOUR, self.rng)
|
||
self.assertFalse(any(e["kind"] == "harvest" for e in self.city["effects"]))
|
||
|
||
def test_new_happenings(self):
|
||
game._fortune(self.world, T0, self.rng, "masons")
|
||
self.assertEqual(game.skill_mods(self.world, game.ACTION_BY_ID["clear_rockslide"])["speed"], 0.75)
|
||
self.city["treasury"] = 1000
|
||
game._hardship(self.world, T0, self.rng, "taxman")
|
||
self.assertEqual(self.city["treasury"], 850)
|
||
game._minor_threat(self.world, T0, self.rng, "smugglers")
|
||
game.tick(self.world, T0 + 2 * game.HOUR + 1, self.rng)
|
||
self.assertLess(self.city["treasury"], 850)
|
||
game._minor_threat(self.world, T0, self.rng, "bridge")
|
||
p = self.world["players"]["ann"]
|
||
hunt = next(a for a in game.ACTIONS if a["skill"] == "hunting" and not a.get("special"))
|
||
self.assertTrue(game.can_start(self.world, p, hunt))
|
||
|
||
def test_festival_boosts_favour(self):
|
||
game._fortune(self.world, T0, self.rng, "festival")
|
||
p = self.world["players"]["ann"]
|
||
p["bank"] = {"stone": 100}
|
||
game.donate(self.world, p, "stone", 100, T0)
|
||
self.assertEqual(p["favour"], 150)
|
||
|
||
def test_mercenaries_join_the_next_defence(self):
|
||
self.world["next_event_at"] = T0
|
||
game.tick(self.world, T0, self.rng)
|
||
game._fortune(self.world, T0, self.rng, "mercenaries")
|
||
p = self.world["players"]["ann"]
|
||
self.city["treasury"] = 5000
|
||
before = game.defence(self.world)["total"]
|
||
self.assertIsNone(game.hire_mercenaries(self.world, p, T0))
|
||
self.assertEqual(game.defence(self.world)["total"], before + 25)
|
||
|
||
def test_dragons_burn_when_they_win(self):
|
||
self.city["buildings"]["houses"]["level"] = 1
|
||
self.city["stockpile"] = {"logs": 500, "bread": 200}
|
||
self.world["events"].append({"id": 99, "kind": "dragon", "status": "upcoming", "announced": T0,
|
||
"fire_at": T0 + 10, "recon": 0, "sabotage": 0.0, "jitter": 0,
|
||
"strength": 1000, "group": "a green dragon", "trait": "x"})
|
||
self.assertEqual(game.upcoming_raid(self.world)["kind"], "dragon")
|
||
game.tick(self.world, T0 + 20, self.rng)
|
||
report = next(e for e in self.world["events"] if e["id"] == 99)["report"]
|
||
self.assertIn("ablaze", report["title"])
|
||
self.assertTrue(self.city["buildings"]["houses"]["damaged"])
|
||
self.assertLess(self.city["stockpile"]["logs"], 500)
|
||
|
||
def test_tracks_fire_on_their_own_cooldowns(self):
|
||
self.world["next_fortune_at"] = T0
|
||
self.world["next_hardship_at"] = T0
|
||
game.tick(self.world, T0 + 1, self.rng)
|
||
tracks = [e.get("track") for e in self.world["events"]]
|
||
self.assertIn("fortune", tracks)
|
||
self.assertIn("hardship", tracks)
|
||
self.assertGreater(self.world["next_fortune_at"], T0 + 8 * game.HOUR)
|
||
|
||
def test_v7_worlds_get_weather_and_tracks(self):
|
||
world = world_with()
|
||
world["version"] = 7
|
||
for key in ("weather", "effects", "problems", "hired"):
|
||
del world["city"][key]
|
||
for key in ("granary", "lumber_shed", "well"):
|
||
del world["city"]["buildings"][key]
|
||
for track in ("minor", "hardship", "fortune"):
|
||
del world[f"next_{track}_at"]
|
||
game.migrate(world, T0)
|
||
self.assertEqual(world["city"]["weather"]["kind"], "clear")
|
||
self.assertIn("granary", world["city"]["buildings"])
|
||
self.assertIsNotNone(world["next_fortune_at"])
|
||
|
||
|
||
|
||
class WearTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("fred", "commoner"))
|
||
self.city = self.world["city"]
|
||
self.city["population"] = 40.0 # 20 defenders
|
||
|
||
def test_wear_follows_how_close_the_fight_was(self):
|
||
self.assertAlmostEqual(game.wear_share(3), 0.1)
|
||
self.assertAlmostEqual(game.wear_share(1.5), 0.2)
|
||
self.assertAlmostEqual(game.wear_share(1), 0.3)
|
||
self.assertAlmostEqual(game.wear_share(0.5), 0.4)
|
||
|
||
def test_only_issued_gear_breaks_worst_first(self):
|
||
self.city["stockpile"] = {"iron_sword": 10, "copper_sword": 30}
|
||
self.city["buildings"]["barracks"]["level"] = 1
|
||
self.city["wages"] = 10_000
|
||
rng = random.Random()
|
||
rng.random = lambda: 0.0
|
||
broken = game.wear_kit(self.world, 0.4, rng)
|
||
# 20 defenders take 10 iron and 10 copper; 8 break, all copper.
|
||
self.assertEqual(broken, {"copper_sword": 8})
|
||
self.assertEqual(self.city["stockpile"], {"iron_sword": 10, "copper_sword": 22})
|
||
|
||
def test_a_won_raid_breaks_some_kit_and_reports_it(self):
|
||
self.city["stockpile"] = {"copper_sword": 20, "arrows": 100}
|
||
self.city["wages"] = 10_000
|
||
event = {"strength": 1, "sabotage": 0, "group": "the Red Hand"}
|
||
game._resolve_raid(self.world, event, T0, random.Random(1))
|
||
self.assertLess(self.city["stockpile"]["copper_sword"], 20)
|
||
self.assertEqual(self.city["stockpile"]["arrows"], 70)
|
||
text = " ".join(event["report"]["lines"])
|
||
self.assertIn("used 30 arrows", text)
|
||
self.assertIn("broken in the fighting", text)
|
||
|
||
|
||
class GoldTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("ann", "commoner"), ("bob", "commoner"))
|
||
self.ann, self.bob = self.world["players"]["ann"], self.world["players"]["bob"]
|
||
self.ann["gold"] = self.bob["gold"] = 200_000
|
||
|
||
def caravan(self, wares):
|
||
self.world["caravan"] = {"id": 7, "at": T0 - 1, "until": T0 + 3600, "wares": wares, "look": "e_wolf"}
|
||
|
||
def test_rented_slots_cost_more_each_time(self):
|
||
before = game.bank_slots(self.world, self.ann)
|
||
self.assertEqual([game.rent_cost({"rented": n}) for n in range(4)], [1000, 1350, 1800, 2450])
|
||
self.assertIsNone(game.rent_slot(self.ann))
|
||
self.assertIsNone(game.rent_slot(self.ann))
|
||
self.assertEqual(game.bank_slots(self.world, self.ann), before + 2)
|
||
self.assertEqual(self.ann["gold"], 200_000 - 2350)
|
||
self.ann["gold"] = 0
|
||
self.assertEqual(game.rent_slot(self.ann), "Needs 1,800 gold")
|
||
|
||
def test_caravan_comes_on_the_prep_day_and_sells_once_per_visit(self):
|
||
self.world["next_event_at"] = T0
|
||
game.tick(self.world, T0, random.Random(4))
|
||
caravan = self.world["caravan"]
|
||
self.assertTrue(T0 + 2 * game.HOUR <= caravan["at"] <= T0 + 16 * game.HOUR)
|
||
self.assertIsNone(game.caravan_view(self.world, self.ann, T0))
|
||
game.tick(self.world, caravan["at"] + 1, random.Random(4))
|
||
view = game.caravan_view(self.world, self.ann, caravan["at"] + 1)
|
||
self.assertEqual(len(view["wares"]), game.CARAVAN_WARES)
|
||
self.assertIsNone(game.caravan_view(self.world, self.ann, caravan["until"] + 1))
|
||
|
||
def test_wares_boost_your_own_work_for_their_charges(self):
|
||
self.caravan(["prospector", "tutor"])
|
||
self.assertIsNone(game.buy_ware(self.world, self.ann, "prospector", None, T0))
|
||
self.assertEqual(game.buy_ware(self.world, self.ann, "prospector", None, T0), "One of each per visit")
|
||
self.assertEqual(self.ann["gold"], 200_000 - 1550) # mining 1: 1,500 × (1 + 1/30)
|
||
self.assertEqual(game.ware(self.ann, "yield", "mining"), 1.5)
|
||
self.assertEqual(game.ware(self.bob, "yield", "mining"), 1)
|
||
self.ann["boosts"][0]["left"] = 1
|
||
game.ware(self.ann, "yield", "mining", spend=True)
|
||
self.assertEqual(self.ann["boosts"], [])
|
||
self.assertEqual(game.buy_ware(self.world, self.ann, "tutor", None, T0), "Pick a skill")
|
||
self.assertIsNone(game.buy_ware(self.world, self.ann, "tutor", "smithing", T0))
|
||
self.assertEqual(game.ware(self.ann, "xp", "smithing"), 1.5)
|
||
|
||
def test_ware_prices_rise_with_your_level(self):
|
||
self.ann["xp"]["mining"] = game.XP_TABLE[60]
|
||
self.assertEqual(game.ware_price(self.ann, "prospector", "mining"), 4500)
|
||
|
||
def test_threat_wares_end_with_the_threat(self):
|
||
self.caravan(["spyglass"])
|
||
self.assertEqual(game.buy_ware(self.world, self.ann, "spyglass", None, T0), "There's no threat coming")
|
||
self.world["next_event_at"] = T0
|
||
game.tick(self.world, T0, random.Random(4))
|
||
self.caravan(["spyglass"])
|
||
self.assertIsNone(game.buy_ware(self.world, self.ann, "spyglass", None, T0))
|
||
self.assertEqual(game.ware(self.ann, "recon", "reconnaissance"), 1.5)
|
||
raid = game.upcoming_raid(self.world)
|
||
raid["strength"] = 0
|
||
game.tick(self.world, raid["fire_at"], random.Random(4))
|
||
self.assertEqual(game.ware(self.ann, "recon", "reconnaissance"), 1)
|
||
|
||
def test_looks_need_owning_or_earning(self):
|
||
look = {"field": "navy", "trim": "brass", "emblem": None, "title": None}
|
||
self.assertIsNone(game.set_look(self.ann, look))
|
||
self.assertEqual(game.set_look(self.ann, {**look, "emblem": "e_wolf"}), "You don't own that")
|
||
self.caravan([])
|
||
self.assertIsNone(game.buy_ware(self.world, self.ann, "e_wolf", None, T0))
|
||
self.assertIsNone(game.set_look(self.ann, {**look, "emblem": "e_wolf"}))
|
||
self.assertEqual(game.look_view(self.ann)["emblem"], "wolf-head")
|
||
self.assertEqual(game.set_look(self.ann, {**look, "title": "Master smith"}), "You haven't earned that title")
|
||
self.ann["xp"]["smithing"] = game.XP_TABLE[50]
|
||
self.assertIsNone(game.set_look(self.ann, {**look, "title": "Master smith"}))
|
||
self.assertNotIn("Master baiter", game.titles(self.ann))
|
||
self.ann["xp"]["fishing"] = game.XP_TABLE[50]
|
||
self.assertIn("Master angler", game.titles(self.ann))
|
||
self.assertIn("Master baiter", game.titles(self.ann))
|
||
self.assertIsNone(game.upgrade_home(self.ann))
|
||
self.assertEqual(game.look_view(self.ann)["home"], "Cottage")
|
||
|
||
def test_monuments_take_gold_and_carve_the_donors_in(self):
|
||
city = self.world["city"]
|
||
self.assertIsNone(game.give_monument(self.world, self.ann, "fountain", 20_000, T0))
|
||
self.assertIsNone(game.give_monument(self.world, self.bob, "fountain", "all", T0))
|
||
self.assertEqual(self.bob["gold"], 200_000 - 5_000)
|
||
self.assertEqual(city["built"][0]["donors"], {"ann": 20_000, "bob": 5_000})
|
||
self.assertEqual(game.give_monument(self.world, self.ann, "fountain", 1, T0), "It's already built")
|
||
self.assertEqual(self.ann["favour"], 0)
|
||
game.give_monument(self.world, self.ann, "statue", 30_000, T0)
|
||
game.give_monument(self.world, self.bob, "statue", 20_000, T0)
|
||
self.assertEqual(city["carving"]["by"], "ann")
|
||
self.assertEqual(game.carve_statue(self.world, self.bob, "bob", T0), "It isn't yours to pick")
|
||
self.assertIsNone(game.carve_statue(self.world, self.ann, "bob", T0))
|
||
self.assertEqual(city["built"][-1]["of"], "bob")
|
||
|
||
def test_v28_worlds_get_gold_sinks(self):
|
||
world = world_with(("ann", "commoner"))
|
||
world["version"] = 28
|
||
for key in ("rented", "boosts", "bought", "deeds", "owned", "look"):
|
||
del world["players"]["ann"][key]
|
||
del world["caravan"]
|
||
game.migrate(world, T0)
|
||
self.assertEqual(world["players"]["ann"]["look"], game.DEFAULT_LOOK)
|
||
self.assertIsNone(world["caravan"])
|
||
|
||
|
||
class ClassTest(unittest.TestCase):
|
||
def lucky_run(self, cls, action_id, bank):
|
||
world = world_with(("pat", cls))
|
||
p = world["players"]["pat"]
|
||
p["xp"] = {skill: game.XP_TABLE[50] for skill in p["xp"]}
|
||
p["bank"] = dict(bank)
|
||
game.start_action(world, p, action_id, T0)
|
||
return p, game.process_player(world, p, T0 + 3 * game.HOUR, random.Random(3))
|
||
|
||
def test_every_trade_sometimes_crafts_double(self):
|
||
for cls, action, bank in (("miner", "smith_copper_bar", {"copper_ore": 300, "logs": 300}),
|
||
("builder", "saw_plank", {"logs": 600}),
|
||
("homesteader", "cook_bread", {"wheat": 600, "logs": 300}),
|
||
("ranger", "cook_shrimp", {"raw_shrimp": 300, "logs": 300})):
|
||
_p, summary = self.lucky_run(cls, action, bank)
|
||
self.assertGreater(summary["lucky"], 0, cls)
|
||
_p, summary = self.lucky_run("commoner", action, bank)
|
||
self.assertEqual(summary["lucky"], 0, cls)
|
||
|
||
def test_trade_bonuses(self):
|
||
_p, summary = self.lucky_run("miner", "mine_stone", {})
|
||
self.assertGreater(summary["lucky"], 0) # prospector
|
||
_p, summary = self.lucky_run("builder", "mine_stone", {})
|
||
self.assertEqual(summary["lucky"], 0)
|
||
builder = game.new_player("b", "B", "builder", T0)
|
||
self.assertEqual(game.labour_per_action(builder), 2) # foreman
|
||
home = game.new_player("h", "H", "homesteader", T0)
|
||
home["bank"] = {"mining_potion_1": 1}
|
||
game.use_potion(home, "mining_potion_1")
|
||
self.assertEqual(home["potions"]["mining"]["charges"], 62) # steady hand
|
||
world = world_with()
|
||
ranger = game.new_player("r", "R", "ranger", T0)
|
||
self.assertEqual(game.recon_gain(world, ranger, game.ACTION_BY_ID["recon_roads"]), 1.5) # pathfinder
|
||
self.assertEqual(game.action_time(world, ranger, game.ACTION_BY_ID["cook_shrimp"]),
|
||
game.ACTION_BY_ID["cook_shrimp"]["time"] * game.CLASS_SPEED)
|
||
|
||
def test_old_classes_become_trades_with_a_free_pick(self):
|
||
world = world_with()
|
||
world["version"] = 11
|
||
for uid, cls in (("a", "pioneer"), ("b", "artisan"), ("c", "scout"), ("d", "warrior")):
|
||
world["players"][uid] = game.new_player(uid, uid, "miner", T0)
|
||
world["players"][uid]["cls"] = cls
|
||
game.migrate(world, T0)
|
||
self.assertEqual([p["cls"] for p in world["players"].values()], ["miner", "builder", "ranger", "builder"])
|
||
self.assertTrue(all(p["repick"] for p in world["players"].values()))
|
||
|
||
def test_overfunded_buildings_return_the_extra(self):
|
||
world = world_with()
|
||
world["version"] = 11
|
||
world["city"]["buildings"]["houses"]["fund"] = {"bricks": 400, "logs": 100}
|
||
world["city"]["stockpile"] = {}
|
||
game.migrate(world, T0)
|
||
self.assertEqual(world["city"]["buildings"]["houses"]["fund"], {"bricks": 150, "logs": 100})
|
||
self.assertEqual(world["city"]["stockpile"], {"bricks": 250})
|
||
|
||
|
||
class TradeSkillsTest(unittest.TestCase):
|
||
def test_masonry_is_part_of_construction(self):
|
||
self.assertNotIn("masonry", game.SKILL_IDS)
|
||
bricks = game.ACTION_BY_ID["mason_bricks"]
|
||
self.assertEqual((bricks["skill"], bricks["group"]), ("construction", "Masonry"))
|
||
world = world_with(("swa", "builder"))
|
||
p = world["players"]["swa"]
|
||
world["version"] = 12
|
||
p["xp"]["masonry"], p["xp"]["construction"] = 5000, 1000
|
||
p["bank"] = {"masonry_potion_2": 3}
|
||
p["potions"] = {"masonry": {"item": "masonry_potion_1", "charges": 20}}
|
||
p["action"] = {"id": "brew_masonry_potion_3", "start": T0}
|
||
game.migrate(world, T0)
|
||
self.assertEqual(p["xp"]["construction"], 6000)
|
||
self.assertNotIn("masonry", p["xp"])
|
||
self.assertEqual(p["bank"], {"construction_potion_2": 3})
|
||
self.assertEqual(p["potions"], {"construction": {"item": "construction_potion_1", "charges": 20}})
|
||
self.assertEqual(p["action"]["id"], "brew_construction_potion_3")
|
||
|
||
def test_smiths_make_hardware_for_builders(self):
|
||
nails = game.ACTION_BY_ID["smith_iron_nails"]
|
||
self.assertEqual((nails["inputs"], nails["outputs"], nails["group"]), ({"iron_bar": 1}, {"iron_nails": 5}, "Hardware"))
|
||
self.assertIn("iron_nails", game.ACTION_BY_ID["mason_beam"]["inputs"])
|
||
self.assertIn("rune_lock", game.ACTION_BY_ID["make_enchanted_chests"]["inputs"])
|
||
self.assertNotIn("iron_bar", game.build_cost("houses", 2))
|
||
self.assertIn("iron_nails", game.build_cost("houses", 2))
|
||
|
||
def test_homesteader_focuses_or_tends_two_plots(self):
|
||
world = world_with(("hom", "homesteader"), ("com", "commoner"))
|
||
h, c = world["players"]["hom"], world["players"]["com"]
|
||
h["xp"]["farming"] = game.XP_TABLE[10]
|
||
wheat = game.ACTION_BY_ID["farm_wheat"]
|
||
self.assertAlmostEqual(game.action_time(world, h, wheat), wheat["time"] * 0.7) # focused
|
||
self.assertEqual(game.plant_second_plot(world, c, "grow_thatch"), "Only homesteaders tend two plots")
|
||
self.assertEqual(game.plant_second_plot(world, h, "grow_thatch"), "Plant your first plot first")
|
||
game.start_action(world, h, "farm_wheat", T0)
|
||
self.assertEqual(game.plant_second_plot(world, h, "farm_wheat"), "That's already growing in your first plot")
|
||
self.assertIsNone(game.plant_second_plot(world, h, "grow_thatch"))
|
||
self.assertAlmostEqual(game.action_time(world, h, wheat), wheat["time"] * 0.8)
|
||
summary = game.process_player(world, h, T0 + 0.8 * (25 + 30) * 10 + 1, random.Random(1))
|
||
self.assertEqual(summary["done"], 20)
|
||
self.assertEqual((h["bank"]["wheat"], h["bank"]["thatch"]), (20, 30)) # ten harvests from each plot
|
||
# Replanting the first plot keeps the second; a "make one" elsewhere comes back to both.
|
||
game.start_action(world, h, "grow_guam_leaf", T0 + 1000)
|
||
self.assertEqual(h["action"]["plot2"], "grow_thatch")
|
||
h["bank"].update(wheat=10, logs=10)
|
||
game.start_action(world, h, "cook_bread", T0 + 1000, once=True)
|
||
game.process_player(world, h, T0 + 1000 + 30, random.Random(1))
|
||
self.assertEqual((h["action"]["id"], h["action"]["plot2"]), ("grow_guam_leaf", "grow_thatch"))
|
||
game.start_action(world, h, "mine_stone", T0 + 2000)
|
||
self.assertNotIn("plot2", h["action"])
|
||
|
||
|
||
class WeaponStyleTest(unittest.TestCase):
|
||
def armed(self, cls, weapon, shield=None, level=15):
|
||
world = world_with(("pat", cls))
|
||
p = world["players"]["pat"]
|
||
p["xp"]["combat"] = game.XP_TABLE[level]
|
||
p["hp"] = game.max_hp(p)
|
||
p["equip"].update(weapon=weapon, shield=shield, body="iron_body", helmet="iron_helmet", legs="iron_legs")
|
||
return world, p
|
||
|
||
def test_no_trade_is_funnelled_into_one_race(self):
|
||
# Every race mixes its weaknesses across its variants, so no weapon has one easy target.
|
||
styles = {"miner": "iron_warhammer", "builder": "iron_sword", "homesteader": "oak_staff", "ranger": "oak_bow"}
|
||
races = ["fight_clay_golems", "fight_cave_trolls", "fight_restless_dead", "fight_harpies"]
|
||
for cls, weapon in styles.items():
|
||
_world, p = self.armed(cls, weapon, "iron_shield" if cls == "builder" else None)
|
||
times = [game.fight_odds(p, game.ACTION_BY_ID[race])["kill_time"] for race in races]
|
||
self.assertLess(max(times) / min(times), 1.45, cls)
|
||
|
||
def test_style_traits(self):
|
||
_w, swordsman = self.armed("builder", "iron_sword", "iron_shield")
|
||
_w, smith = self.armed("miner", "iron_warhammer")
|
||
_w, archer = self.armed("ranger", "oak_bow")
|
||
brute = game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"][0] # vulnerable to blunt, resists piercing
|
||
spec = lambda p: game.fighter(p, game.NO_BLESSING, {}, brute)
|
||
self.assertEqual(spec(swordsman)["block"], game.BLOCK_CHANCE)
|
||
self.assertEqual(spec(smith)["crit"], 0.10)
|
||
self.assertEqual(spec(smith)["affinity"], game.VULNERABLE)
|
||
self.assertEqual(spec(archer)["affinity"], game.RESISTANT)
|
||
self.assertEqual(spec(swordsman)["affinity"], 1)
|
||
_w, other = self.armed("builder", "iron_warhammer")
|
||
self.assertGreater(spec(smith)["max_hit"], spec(other)["max_hit"]) # the trade's own weapon hits harder
|
||
world, archer = self.armed("ranger", "oak_bow")
|
||
fight = game.simulate_fight(world, archer, game.ACTION_BY_ID["fight_harpies"], random.Random(1))
|
||
self.assertEqual(fight["events"][0][1], archer["id"]) # the bow strikes first
|
||
|
||
def test_two_handed_weapons_leave_no_room_for_a_shield(self):
|
||
_w, p = self.armed("miner", "iron_warhammer")
|
||
self.assertEqual(game.equip_problem(p, "iron_shield"), "Your iron warhammer takes both hands")
|
||
self.assertTrue(game.ITEMS["oak_staff"]["two_handed"])
|
||
self.assertIsNone(game.equip_problem(self.armed("builder", "iron_sword")[1], "iron_shield"))
|
||
|
||
def test_weapons_past_copper_need_trophies(self):
|
||
for action_id, trophy in (("smith_iron_warhammer", "clay_core"), ("smith_iron_sword", "cave_troll_hide"),
|
||
("make_iron_shield", "cave_troll_hide"), ("brew_oak_staff", "grave_dust"),
|
||
("lw_oak_bow", "harpy_sinew"), ("smith_rune_warhammer", "magma_core")):
|
||
self.assertIn(trophy, game.ACTION_BY_ID[action_id]["inputs"], action_id)
|
||
for action_id in ("smith_copper_warhammer", "smith_copper_sword", "make_copper_shield",
|
||
"brew_wooden_staff", "lw_wooden_bow"):
|
||
self.assertFalse(any(item in game.TROPHIES for item in game.ACTION_BY_ID[action_id]["inputs"]), action_id)
|
||
|
||
def test_armour_past_copper_needs_its_areas_reagent(self):
|
||
self.assertEqual(game.ACTION_BY_ID["smith_iron_body"]["inputs"], {"iron_bar": 5, "watchtower_plating": 3})
|
||
self.assertEqual(game.ACTION_BY_ID["lw_hard_leather_cap"]["inputs"], {"hard_leather": 2, "watchtower_lacing": 1})
|
||
self.assertEqual(game.ACTION_BY_ID["sew_moonweave_legs"]["inputs"], {"moonweave": 3, "ironhill_thread": 2})
|
||
for action_id in ("smith_copper_body", "lw_cap", "sew_linen_body"):
|
||
self.assertFalse(any(item in game.REAGENTS for item in game.ACTION_BY_ID[action_id]["inputs"]), action_id)
|
||
harpies = game.ACTION_BY_ID["fight_harpies"]["drops"]
|
||
self.assertIn(["watchtower_thread", 1, game.REAGENT_ODDS], harpies)
|
||
|
||
def test_heavy_stops_blows_light_stops_magic(self):
|
||
_w, plate = self.armed("miner", "iron_warhammer")
|
||
_w, cloth = self.armed("miner", "iron_warhammer")
|
||
cloth["equip"].update(helmet="cotton_hood", body="cotton_body", legs="cotton_legs")
|
||
self.assertGreater(game.armour(plate), game.armour(cloth))
|
||
self.assertGreater(game.magic_ward(cloth), game.magic_ward(plate))
|
||
shaper = next(v for v in game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"] if v["name"] == "Mud shaper")
|
||
brute = game.ACTION_BY_ID["fight_clay_golems"]["enemy"]["variants"][0]
|
||
hurt = lambda p, foe: game._variant_odds(p, foe, game.NO_BLESSING, {})["damage"]
|
||
self.assertLess(hurt(cloth, shaper), hurt(plate, shaper))
|
||
self.assertLess(hurt(plate, brute), hurt(cloth, brute))
|
||
|
||
def test_each_fight_is_a_pack_of_the_variants(self):
|
||
world, p = self.armed("miner", "iron_warhammer", level=30)
|
||
goblins = game.ACTION_BY_ID["fight_goblins"]
|
||
packs = [game.simulate_fight(world, p, goblins, random.Random(seed))["pack"] for seed in range(60)]
|
||
self.assertEqual({foe["name"] for pack in packs for foe in pack}, {"Goblin brute", "Goblin rogue", "Goblin warlock"})
|
||
self.assertEqual({len(pack) for pack in packs}, {1, 2})
|
||
for pack in packs:
|
||
share = game.OUTNUMBERED[0] if len(pack) == 2 else 1
|
||
self.assertTrue(all(foe["share"] == share for foe in pack)) # two of them, each weaker
|
||
scouts = game.ACTION_BY_ID["fight_scouts"]
|
||
self.assertEqual({len(game.draw_pack(scouts, 3, random.Random(seed))) for seed in range(60)}, {3, 4})
|
||
|
||
def test_a_boss_comes_alone(self):
|
||
ogre = game.ACTION_BY_ID["fight_ruins"]
|
||
foe = ogre["enemy"]["variants"][0]
|
||
pair, four = game.draw_pack(ogre, 2, random.Random(1)), game.draw_pack(ogre, 4, random.Random(1))
|
||
self.assertEqual((len(pair), len(four)), (1, 1))
|
||
self.assertEqual(pair[0]["hp"], round(foe["hp"] * game.BOSS_HP * 2)) # tougher the more of you there are
|
||
self.assertEqual(four[0]["hp"], round(foe["hp"] * game.BOSS_HP * 4))
|
||
self.assertEqual(pair[0]["max_hit"], round(foe["max_hit"] * game.BOSS_HITS[0]))
|
||
self.assertGreater(four[0]["max_hit"], pair[0]["max_hit"])
|
||
|
||
def test_shield_smiths_stop_after_the_move(self):
|
||
world = world_with(("kay", "miner"))
|
||
world["version"] = 13
|
||
world["players"]["kay"]["action"] = {"id": "smith_iron_shield", "start": T0}
|
||
game.migrate(world, T0)
|
||
self.assertIsNone(world["players"]["kay"]["action"])
|
||
|
||
|
||
class FittingTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("fred", "miner"))
|
||
self.p = self.world["players"]["fred"]
|
||
self.buildings = self.world["city"]["buildings"]
|
||
|
||
def test_install_needs_the_building_level(self):
|
||
self.p["bank"] = {"shelving": 1}
|
||
self.buildings["storehouse"]["level"] = 1
|
||
self.assertEqual(game.install(self.world, self.p, "shelving", "bank", T0), "Needs Storehouse level 2")
|
||
self.buildings["storehouse"]["level"] = 2
|
||
slots = game.bank_slots(self.world, self.p)
|
||
self.assertIsNone(game.install(self.world, self.p, "shelving", "bank", T0))
|
||
self.assertEqual(game.bank_slots(self.world, self.p), slots + 1)
|
||
self.assertNotIn("shelving", self.p["bank"])
|
||
self.assertGreater(self.p["favour"], 0)
|
||
self.p["bank"] = {"shelving": 1}
|
||
self.assertEqual(game.install(self.world, self.p, "shelving", "bank", T0), "The storehouse already has that")
|
||
|
||
def test_install_from_the_stockpile(self):
|
||
self.world["city"]["stockpile"] = {"hoardings": 1}
|
||
self.buildings["walls"]["level"] = 3
|
||
walls = game.defence(self.world)["parts"]["walls"]
|
||
self.assertIsNone(game.install(self.world, self.p, "hoardings", "city", T0))
|
||
self.assertEqual(game.defence(self.world)["parts"]["walls"], walls + 25)
|
||
self.assertEqual(self.p["favour"], 0)
|
||
self.assertIn("Walls", game.city_uses("hoardings"))
|
||
|
||
def test_damage_pauses_a_fitting(self):
|
||
self.buildings["smithy"].update(level=3, fittings=["bellows"])
|
||
action = game.ACTION_BY_ID["smith_copper_bar"]
|
||
fitted = game.action_time(self.world, self.p, action)
|
||
self.buildings["smithy"]["damaged"] = True
|
||
self.assertFalse(game.fitted(self.world, "bellows"))
|
||
self.assertGreater(game.action_time(self.world, self.p, action), fitted)
|
||
|
||
def test_pump_can_put_out_a_fire(self):
|
||
self.buildings["well"].update(level=2, fittings=["well_pump"])
|
||
self.world["city"]["stockpile"] = {"logs": 100}
|
||
rng = random.Random()
|
||
rng.random = lambda: 0.1
|
||
game._hardship(self.world, T0, rng, "fire")
|
||
self.assertEqual(self.world["city"]["stockpile"], {"logs": 100})
|
||
|
||
def test_every_fitting_can_be_made(self):
|
||
for fitting in game.FITTINGS:
|
||
action = game.ACTION_BY_ID[f"make_{fitting[0]}"]
|
||
self.assertEqual(action["skill"], "construction")
|
||
self.assertEqual(action["outputs"], {fitting[0]: 1})
|
||
self.assertIn(fitting[2], game.BUILDINGS)
|
||
|
||
def test_migration_adds_fittings(self):
|
||
world = game.new_world(T0)
|
||
world["version"] = 9
|
||
for building in world["city"]["buildings"].values():
|
||
del building["fittings"]
|
||
game.migrate(world, T0)
|
||
self.assertEqual(world["city"]["buildings"]["houses"]["fittings"], [])
|
||
|
||
|
||
class PinTest(unittest.TestCase):
|
||
def test_pin_toggles(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
self.assertIsNone(game.toggle_pin(p, "mining"))
|
||
self.assertEqual(p["pinned"], ["mining"])
|
||
self.assertIsNone(game.toggle_pin(p, "mining"))
|
||
self.assertEqual(p["pinned"], [])
|
||
self.assertIsNotNone(game.toggle_pin(p, "juggling"))
|
||
|
||
def test_old_players_get_pins(self):
|
||
world = world_with(("fred", "commoner"))
|
||
del world["players"]["fred"]["pinned"]
|
||
world["version"] = 18
|
||
game.migrate(world, T0)
|
||
self.assertEqual(world["players"]["fred"]["pinned"], [])
|
||
|
||
def test_track_toggles_one_recipe(self):
|
||
world = world_with(("fred", "commoner"))
|
||
p = world["players"]["fred"]
|
||
recipes = [a["id"] for a in game.ACTIONS if a["inputs"]][:2]
|
||
self.assertIsNone(game.toggle_track(p, recipes[0]))
|
||
self.assertEqual(p["tracked"], recipes[0])
|
||
self.assertIsNone(game.toggle_track(p, recipes[1]))
|
||
self.assertEqual(p["tracked"], recipes[1])
|
||
self.assertIsNone(game.toggle_track(p, recipes[1]))
|
||
self.assertIsNone(p["tracked"])
|
||
gather = next(a["id"] for a in game.ACTIONS if not a["inputs"])
|
||
self.assertIsNotNone(game.toggle_track(p, gather))
|
||
self.assertIsNotNone(game.toggle_track(p, "juggling"))
|
||
|
||
def test_old_players_get_tracking(self):
|
||
world = world_with(("fred", "commoner"))
|
||
del world["players"]["fred"]["tracked"]
|
||
world["version"] = 21
|
||
game.migrate(world, T0)
|
||
self.assertIsNone(world["players"]["fred"]["tracked"])
|
||
|
||
|
||
class IconTest(unittest.TestCase):
|
||
def test_everything_has_a_glyph_in_the_sprite(self):
|
||
sprite = (Path(__file__).resolve().parent.parent / "static" / "icons.svg").read_text()
|
||
missing = [name for name in game.all_glyphs() if f'id="{name}"' not in sprite]
|
||
self.assertEqual(missing, [], "run tools/build_icons.py")
|
||
for item, spec in game.ITEMS.items():
|
||
self.assertIn("glyph", spec, item)
|
||
|
||
|
||
class CombatReworkMigrationTest(unittest.TestCase):
|
||
def test_old_monsters_trophies_and_dragonhide_carry_over(self):
|
||
world = world_with(("kay", "ranger"))
|
||
world["version"] = 15
|
||
p = world["players"]["kay"]
|
||
del p["xp"]["tailoring"]
|
||
p["bank"] = {"bat_sinew": 3, "harpy_sinew": 2, "green_dragonhide": 4, "red_dragon_leather": 1}
|
||
p["equip"]["body"] = "blue_dhide_body"
|
||
p["action"] = {"id": "fight_giant_bat", "start": T0}
|
||
world["players"]["kay"] = p
|
||
game.migrate(world, T0)
|
||
self.assertEqual(p["bank"], {"harpy_sinew": 3, "spider_silk": 2, "ironhill_lacing": 4, "mammoth_leather": 1})
|
||
self.assertEqual(p["equip"]["body"], "sabrecat_leather_body")
|
||
self.assertEqual(p["action"]["id"], "fight_harpies")
|
||
self.assertEqual(p["xp"]["tailoring"], 0)
|
||
|
||
def test_homesteaders_tailor(self):
|
||
self.assertIn("tailoring", game.CLASSES["homesteader"]["fast"])
|
||
self.assertIn("tailoring", game.CLASSES["homesteader"]["craft"])
|
||
|
||
|
||
class WorkOrderTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("mike", "ranger"), ("fred", "miner"), ("dave", "builder"))
|
||
self.mike, self.fred, self.dave = (self.world["players"][uid] for uid in ("mike", "fred", "dave"))
|
||
self.fred["xp"]["smithing"] = game.XP_TABLE[25]
|
||
self.dave["xp"]["smithing"] = game.XP_TABLE[25]
|
||
self.world["city"]["buildings"]["smithy"]["level"] = 1
|
||
|
||
def post_hammer(self):
|
||
self.mike["gold"] = 200
|
||
self.mike["bank"] = {"iron_bar": 3, "clay_core": 3}
|
||
return game.post_order(self.world, self.mike, "craft", "iron_warhammer", 1, ["iron_bar", "clay_core"], 140, T0)
|
||
|
||
def test_posting_holds_the_reagents_and_reward(self):
|
||
self.assertIsNone(self.post_hammer())
|
||
order = self.world["orders"][0]
|
||
self.assertEqual(order["provided"], {"iron_bar": 3, "clay_core": 3})
|
||
self.assertEqual(self.mike["bank"], {})
|
||
self.assertEqual(self.mike["gold"], 60)
|
||
|
||
def test_a_claimed_order_is_crafted_and_sent_to_the_requester(self):
|
||
self.post_hammer()
|
||
order = self.world["orders"][0]
|
||
self.fred["bank"] = {"oak_plank": 1}
|
||
self.assertIsNone(game.work_order(self.world, self.fred, order["id"], T0))
|
||
self.assertEqual(game.claim_order(self.world, self.dave, order["id"]), "Someone else has it")
|
||
game.process_player(self.world, self.fred, T0 + 600, random.Random(1))
|
||
self.assertEqual(self.mike["bank"].get("iron_warhammer"), 1)
|
||
self.assertEqual(self.fred["gold"], 140)
|
||
self.assertEqual(order["status"], "done")
|
||
self.assertNotIn("oak_plank", self.fred["bank"]) # the crafter brings what wasn't provided
|
||
self.assertIsNone(self.fred["action"])
|
||
|
||
def test_cancelling_refunds_what_is_left(self):
|
||
self.post_hammer()
|
||
order = self.world["orders"][0]
|
||
self.assertEqual(game.claim_order(self.world, self.mike, order["id"]), "That's your own order")
|
||
self.assertIsNone(game.cancel_order(self.world, self.mike, order["id"], T0))
|
||
self.assertEqual(self.mike["bank"], {"iron_bar": 3, "clay_core": 3})
|
||
self.assertEqual(self.mike["gold"], 200)
|
||
|
||
def test_materials_requests_are_open_and_pay_per_item(self):
|
||
self.mike["gold"] = 400
|
||
self.assertIsNone(game.post_order(self.world, self.mike, "gather", "oak_logs", 200, None, 2, T0))
|
||
order = self.world["orders"][0]
|
||
self.fred["bank"] = {"oak_logs": 40}
|
||
self.dave["bank"] = {"oak_logs": 500}
|
||
self.assertIsNone(game.deliver_order(self.world, self.fred, order["id"], T0))
|
||
self.assertIsNone(game.deliver_order(self.world, self.dave, order["id"], T0))
|
||
self.assertEqual(self.fred["gold"], 80)
|
||
self.assertEqual(self.dave["gold"], 320)
|
||
self.assertEqual(self.dave["bank"]["oak_logs"], 340)
|
||
self.assertEqual(self.mike["bank"]["oak_logs"], 200)
|
||
self.assertEqual(order["status"], "done")
|
||
|
||
def test_orders_expire_and_refund(self):
|
||
self.post_hammer()
|
||
game.expire_orders(self.world, T0 + 4 * game.DAY)
|
||
self.assertEqual(self.mike["gold"], 200)
|
||
self.assertEqual(self.mike["bank"]["iron_bar"], 3)
|
||
|
||
def test_finished_orders_can_be_removed(self):
|
||
self.post_hammer()
|
||
order = self.world["orders"][0]
|
||
self.assertEqual(game.remove_orders(self.world, self.mike, order["id"]), "No such order") # still open
|
||
game.cancel_order(self.world, self.mike, order["id"], T0)
|
||
self.assertEqual(game.remove_orders(self.world, self.fred, order["id"]), "No such order") # not his
|
||
self.assertIsNone(game.remove_orders(self.world, self.mike))
|
||
self.assertEqual(self.world["orders"], [])
|
||
|
||
def test_requester_can_fill_a_slot_after_posting(self):
|
||
self.mike["gold"] = 200
|
||
self.mike["bank"] = {"iron_bar": 3, "clay_core": 3}
|
||
game.post_order(self.world, self.mike, "craft", "iron_warhammer", 1, [], 140, T0)
|
||
order = self.world["orders"][0]
|
||
self.assertIsNone(game.provide_reagent(self.world, self.mike, order["id"], "iron_bar"))
|
||
self.assertEqual(order["provided"], {"iron_bar": 3})
|
||
self.fred["bank"] = {"oak_plank": 1, "clay_core": 3}
|
||
self.assertIsNone(game.work_order(self.world, self.fred, order["id"], T0))
|
||
game.process_player(self.world, self.fred, T0 + 600, random.Random(1))
|
||
self.assertEqual(self.mike["bank"].get("iron_warhammer"), 1)
|
||
self.assertEqual(self.fred["bank"], {}) # used his own plank and cores, never needed bars
|
||
|
||
def test_anyone_can_put_reagents_in_and_gets_a_share(self):
|
||
self.mike["gold"] = 200
|
||
game.post_order(self.world, self.mike, "craft", "iron_warhammer", 1, [], 140, T0)
|
||
order = self.world["orders"][0]
|
||
self.dave["bank"] = {"iron_bar": 2}
|
||
self.mike["bank"] = {"iron_bar": 5}
|
||
self.assertIsNone(game.provide_reagent(self.world, self.dave, order["id"], "iron_bar"))
|
||
self.assertIsNone(game.provide_reagent(self.world, self.mike, order["id"], "iron_bar"))
|
||
self.assertEqual(order["provided"], {"iron_bar": 3}) # only what's needed
|
||
self.assertEqual(self.mike["bank"], {"iron_bar": 4})
|
||
self.assertEqual(game.provide_reagent(self.world, self.dave, order["id"], "iron_bar"), "Already provided")
|
||
self.fred["bank"] = {"oak_plank": 1, "clay_core": 3}
|
||
self.assertIsNone(game.work_order(self.world, self.fred, order["id"], T0))
|
||
game.process_player(self.world, self.fred, T0 + 600, random.Random(1))
|
||
# Reagents are worth 84 a run; Dave's 2 bars (20) earn 20/84 of half the reward.
|
||
self.assertEqual(self.dave["gold"], 17)
|
||
self.assertEqual(self.fred["gold"], 123) # the requester's own bar leaves its share with the crafter
|
||
self.assertEqual(order["earned"], {"dave": 17})
|
||
|
||
def test_crafter_tops_up_a_short_pool(self):
|
||
self.mike["gold"] = 200
|
||
game.post_order(self.world, self.mike, "craft", "iron_warhammer", 1, [], 140, T0)
|
||
order = self.world["orders"][0]
|
||
self.dave["bank"] = {"iron_bar": 1}
|
||
game.provide_reagent(self.world, self.dave, order["id"], "iron_bar")
|
||
self.fred["bank"] = {"iron_bar": 1, "oak_plank": 1, "clay_core": 3}
|
||
self.assertEqual(game.work_order(self.world, self.fred, order["id"], T0), "Out of iron bar")
|
||
self.fred["bank"]["iron_bar"] = 2
|
||
self.assertIsNone(game.work_order(self.world, self.fred, order["id"], T0))
|
||
game.process_player(self.world, self.fred, T0 + 600, random.Random(1))
|
||
self.assertEqual(self.mike["bank"].get("iron_warhammer"), 1)
|
||
self.assertNotIn("iron_bar", self.fred["bank"])
|
||
|
||
def test_pooled_reagents_go_back_to_whoever_gave_them(self):
|
||
self.mike["gold"] = 200
|
||
game.post_order(self.world, self.mike, "craft", "iron_warhammer", 1, [], 140, T0)
|
||
order = self.world["orders"][0]
|
||
self.dave["bank"] = {"clay_core": 3}
|
||
game.provide_reagent(self.world, self.dave, order["id"], "clay_core")
|
||
game.cancel_order(self.world, self.mike, order["id"], T0)
|
||
self.assertEqual(self.dave["bank"], {"clay_core": 3})
|
||
self.assertEqual(self.mike["gold"], 200)
|
||
|
||
def test_leftover_reagents_return_when_filled_from_a_bank(self):
|
||
self.mike["gold"] = 200
|
||
game.post_order(self.world, self.mike, "craft", "iron_warhammer", 1, [], 140, T0)
|
||
order = self.world["orders"][0]
|
||
self.dave["bank"] = {"clay_core": 3}
|
||
game.provide_reagent(self.world, self.dave, order["id"], "clay_core")
|
||
self.fred["bank"] = {"iron_warhammer": 1}
|
||
self.assertIsNone(game.deliver_order(self.world, self.fred, order["id"], T0))
|
||
self.assertEqual(order["status"], "done")
|
||
self.assertEqual(self.dave["bank"], {"clay_core": 3})
|
||
|
||
|
||
class Always:
|
||
"""An rng where every roll comes up: a gem, a drop, whatever's on offer."""
|
||
def random(self):
|
||
return 0.0
|
||
|
||
def choices(self, population, weights=None, k=1):
|
||
return [population[0]] * k
|
||
|
||
def randint(self, a, b):
|
||
return a
|
||
|
||
|
||
class JewelcraftingTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("ann", "commoner"))
|
||
self.ann = self.world["players"]["ann"]
|
||
|
||
def test_recipes_take_a_band_a_gem_and_past_quartz_a_shard(self):
|
||
self.assertEqual(game.ACTION_BY_ID["jc_quartz_haste"]["inputs"], {"copper_bar": 1, "quartz": 1})
|
||
self.assertEqual(game.ACTION_BY_ID["jc_garnet_vigour"]["inputs"],
|
||
{"silver_bar": 2, "garnet": 1, "watchtower_shard": 2})
|
||
self.assertEqual(game.ACTION_BY_ID["jc_diamond_charm"]["inputs"],
|
||
{"gold_bar": 1, "diamond": 2, "dragonspine_shard": 1})
|
||
self.assertIn(["watchtower_shard", 1, game.REAGENT_ODDS], game.ACTION_BY_ID["fight_harpies"]["drops"])
|
||
|
||
def test_mining_turns_up_gems(self):
|
||
self.ann["xp"]["mining"] = game.XP_TABLE[15]
|
||
game.start_action(self.world, self.ann, "mine_iron", T0)
|
||
rock = game.ACTION_BY_ID["mine_iron"]
|
||
game.process_player(self.world, self.ann, T0 + game.action_time(self.world, self.ann, rock) + 0.1, Always())
|
||
self.assertEqual(self.ann["bank"]["garnet"], 1)
|
||
self.assertEqual(self.ann["bank"]["iron_ore"], 1)
|
||
|
||
def test_prospecting_cracks_ore_for_gems(self):
|
||
self.ann["xp"]["jewelcrafting"] = game.XP_TABLE[15]
|
||
self.ann["bank"] = {"iron_ore": 7}
|
||
game.start_action(self.world, self.ann, "prospect_garnet", T0)
|
||
summary = game.process_player(self.world, self.ann, T0 + 1000, Always())
|
||
self.assertEqual(self.ann["bank"], {"iron_ore": 2, "garnet": 1})
|
||
self.assertEqual(summary["stopped"], "Out of iron ore")
|
||
self.assertGreater(self.ann["xp"]["jewelcrafting"], game.XP_TABLE[15])
|
||
|
||
def test_jewellery_bonuses(self):
|
||
p = self.ann
|
||
p["xp"]["combat"] = game.XP_TABLE[20]
|
||
before = game.fighter(p, game.NO_BLESSING, {})
|
||
hp = game.max_hp(p)
|
||
p["equip"].update(ring="garnet_haste", amulet="garnet_vigour", trinket="garnet_hourglass")
|
||
after = game.fighter(p, game.NO_BLESSING, {})
|
||
self.assertAlmostEqual(after["interval"], round(before["interval"] * 0.93, 2))
|
||
self.assertEqual(game.max_hp(p), hp + 10)
|
||
game.gain_xp(self.world, p, "mining", 100, T0)
|
||
self.assertAlmostEqual(p["xp"]["mining"], 103)
|
||
p["equip"]["ring"] = "garnet_fury"
|
||
self.assertAlmostEqual(game.fighter(p, game.NO_BLESSING, {})["crit"], before["crit"] + 0.03)
|
||
p["equip"]["ring"] = "garnet_might"
|
||
self.assertEqual(game.combat_power(p), game.combat_power({**p, "equip": {**p["equip"], "ring": None}}) + 4)
|
||
|
||
def test_loupe_doubles_any_class_crafts(self):
|
||
self.ann["xp"]["jewelcrafting"] = game.XP_TABLE[15]
|
||
for loupe, rings in ((None, 1), ("quartz_loupe", 2)):
|
||
self.ann["equip"]["trinket"] = loupe
|
||
self.ann["bank"] = {"copper_bar": 1, "quartz": 1}
|
||
game.start_action(self.world, self.ann, "jc_quartz_haste", T0)
|
||
game.process_player(self.world, self.ann, T0 + 1000, Always())
|
||
self.assertEqual(self.ann["bank"], {"quartz_haste": rings})
|
||
|
||
def test_jewellery_tabs_by_slot(self):
|
||
self.assertEqual(game.ACTION_BY_ID["jc_garnet_haste"]["group"], "Rings")
|
||
self.assertEqual(game.ACTION_BY_ID["jc_diamond_warding"]["group"], "Amulets")
|
||
self.assertEqual(game.ACTION_BY_ID["jc_quartz_loupe"]["group"], "Trinkets")
|
||
|
||
def test_miners_cut_jewels(self):
|
||
self.assertIn("jewelcrafting", game.CLASSES["miner"]["fast"])
|
||
self.assertIn("jewelcrafting", game.CLASSES["miner"]["craft"])
|
||
|
||
def test_gems_can_be_asked_for(self):
|
||
self.assertEqual(game.ORDER_RAW["sapphire"], "mine_mithril")
|
||
self.assertEqual(game.ORDER_RECIPES["emerald_warding"], "jc_emerald_warding")
|
||
|
||
def test_migration_adds_the_skill_and_slots(self):
|
||
self.world["version"] = 17
|
||
del self.ann["xp"]["jewelcrafting"]
|
||
for slot in ("ring", "amulet", "trinket"):
|
||
del self.ann["equip"][slot]
|
||
game.migrate(self.world, T0)
|
||
self.assertEqual(self.ann["xp"]["jewelcrafting"], 0)
|
||
self.assertEqual({slot: self.ann["equip"][slot] for slot in ("ring", "amulet", "trinket")},
|
||
{"ring": None, "amulet": None, "trinket": None})
|
||
|
||
def test_rings_of_precision_became_haste(self):
|
||
self.world["version"] = 19
|
||
self.ann["bank"] = {"garnet_precision": 2, "garnet": 1}
|
||
self.ann["equip"]["ring"] = "quartz_precision"
|
||
self.ann["action"] = {"id": "jc_garnet_precision", "start": T0}
|
||
game.migrate(self.world, T0)
|
||
self.assertEqual(self.ann["bank"], {"garnet_haste": 2, "garnet": 1})
|
||
self.assertEqual(self.ann["equip"]["ring"], "quartz_haste")
|
||
self.assertEqual(self.ann["action"]["id"], "jc_garnet_haste")
|
||
|
||
|
||
class StationAndStorageTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("bob", "commoner"))
|
||
self.bob = self.world["players"]["bob"]
|
||
|
||
def test_recipes_pull_in_other_trades(self):
|
||
self.assertEqual(game.ACTION_BY_ID["make_oak_loom"]["inputs"],
|
||
{"oak_plank": 3, "iron_nails": 5, "hard_leather": 2})
|
||
self.assertEqual(game.ACTION_BY_ID["make_wooden_hearth"]["inputs"],
|
||
{"plank": 3, "copper_rivets": 5, "copper_bar": 2})
|
||
self.assertEqual(game.ACTION_BY_ID["make_oak_strongbox"]["inputs"],
|
||
{"oak_plank": 4, "iron_nails": 10, "hard_leather": 2})
|
||
self.assertEqual(game.ACTION_BY_ID["sew_cotton_bag"]["inputs"], {"cotton_cloth": 4, "hard_leather": 1})
|
||
self.assertEqual(game.ACTION_BY_ID["sew_cotton_bag"]["skill"], "tailoring")
|
||
|
||
def test_a_station_speeds_its_craft(self):
|
||
smelt = game.ACTION_BY_ID["smith_copper_bar"]
|
||
bare = game.action_time(self.world, self.bob, smelt)
|
||
self.bob["equip"]["station_smithing"] = "oak_anvil_stand"
|
||
self.assertAlmostEqual(game.action_time(self.world, self.bob, smelt), bare * 0.7)
|
||
self.bob["equip"]["tool_smithing"] = "copper_chisel"
|
||
self.assertAlmostEqual(game.action_time(self.world, self.bob, smelt), bare * 0.7 * 0.85)
|
||
|
||
def test_every_crafting_skill_has_a_tool_and_a_station(self):
|
||
for skill in game.CRAFTING:
|
||
self.assertIn(f"tool_{skill}", game.EQUIP_SLOTS)
|
||
self.assertIn(f"station_{skill}", game.EQUIP_SLOTS)
|
||
self.assertEqual(game.ACTION_BY_ID["smith_iron_mortar_and_pestle"]["inputs"], {"iron_bar": 2, "oak_logs": 1})
|
||
self.assertEqual(game.ACTION_BY_ID["make_oak_workbench"]["skill"], "construction")
|
||
|
||
def test_a_workbench_speeds_the_building_site(self):
|
||
labour = game.ACTION_BY_ID["labour"]
|
||
bare = game.action_time(self.world, self.bob, labour)
|
||
self.bob["equip"]["station_construction"] = "magic_workbench"
|
||
speed = game.ITEMS["magic_workbench"]["speed"]
|
||
self.assertAlmostEqual(game.action_time(self.world, self.bob, labour), bare * (1 - speed))
|
||
|
||
def test_stations_move_out_of_the_tool_slot(self):
|
||
self.world["version"] = 22
|
||
self.bob["equip"]["tool_smithing"] = "oak_anvil_stand"
|
||
del self.bob["equip"]["station_smithing"]
|
||
game.migrate(self.world, T0)
|
||
self.assertEqual((self.bob["equip"]["tool_smithing"], self.bob["equip"]["station_smithing"]),
|
||
(None, "oak_anvil_stand"))
|
||
|
||
def test_strongbox_adds_slots_and_bag_a_bigger_stack(self):
|
||
slots, stack = game.bank_slots(self.world, self.bob), game.stack_size(self.world, self.bob)
|
||
self.bob["bank"] = {"stone": 1200}
|
||
self.assertEqual(game.slots_used(self.world, self.bob), 2)
|
||
self.bob["equip"].update(storage="oak_strongbox", bag="linen_bag")
|
||
self.assertEqual(game.bank_slots(self.world, self.bob), slots + 4)
|
||
self.assertEqual(game.stack_size(self.world, self.bob), stack + 250)
|
||
self.assertEqual(game.slots_used(self.world, self.bob), 1)
|
||
|
||
def test_no_smaller_strongbox_while_the_bank_is_full(self):
|
||
import types
|
||
import app
|
||
handler = types.SimpleNamespace(store=types.SimpleNamespace(world=self.world))
|
||
handler._equip = lambda *args: app.Handler._equip(handler, *args)
|
||
self.bob["equip"]["storage"] = "oak_strongbox"
|
||
self.bob["bank"] = {f"{wood}_plank" if wood != "wooden" else "plank": 1 for wood in game.WOODS}
|
||
self.bob["bank"].update({item: 1 for item in ("stone", "logs", "coal", "bones", "wheat", "flax", "rope",
|
||
"bricks", "thatch", "wooden_strongbox")})
|
||
self.assertEqual(game.overfull(self.world, self.bob), 0) # 16 items in 12 + 4 slots: room for the swap, not after
|
||
self.assertEqual(app.Handler.equip(handler, self.bob, "wooden_strongbox", None), "Bank too full for that")
|
||
self.assertEqual(self.bob["equip"]["storage"], "oak_strongbox")
|
||
self.assertEqual(self.bob["bank"]["wooden_strongbox"], 1)
|
||
|
||
|
||
class FirekeepingTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.world = world_with(("kai", "commoner"), ("sam", "commoner"))
|
||
self.kai, self.sam = self.world["players"]["kai"], self.world["players"]["sam"]
|
||
self.fires = self.world["city"]["fires"]
|
||
|
||
def tend(self, player, fire, log, at, seconds):
|
||
self.assertIsNone(game.start_action(self.world, player, f"tend_{fire}_{log}", at))
|
||
return game.process_player(self.world, player, at + seconds, random.Random(1))
|
||
|
||
def test_tending_feeds_only_that_fire_and_pays_by_the_minute(self):
|
||
self.kai["bank"] = {"logs": 3}
|
||
self.tend(self.kai, "forge", "logs", T0, 95)
|
||
level = game.skill_level(self.kai, "firekeeping")
|
||
self.assertEqual(self.kai["bank"].get("logs", 0), 0)
|
||
self.assertEqual(game.heat(self.world, "forge"), 0)
|
||
self.assertEqual(game.heat(self.world, "kitchen"), -1)
|
||
self.assertAlmostEqual(self.kai["xp"]["firekeeping"], 3 * game.TEND_XP[0] * game.fire_minutes(level))
|
||
|
||
def test_better_logs_need_the_level(self):
|
||
self.kai["bank"] = {"maple_logs": 3}
|
||
self.assertEqual(game.start_action(self.world, self.kai, "tend_forge_maple_logs", T0), "Needs level 45")
|
||
|
||
def test_a_full_fire_stops_tending_and_pays_nothing(self):
|
||
self.kai["bank"] = {"logs": 100}
|
||
summary = self.tend(self.kai, "forge", "logs", T0, 3000)
|
||
self.assertEqual(summary["stopped"], "Your pile in the forge is full")
|
||
left = sum(chunk["left"] for chunk in self.fires["forge"]["fuel"])
|
||
self.assertGreater(left + game.fire_minutes(1) * 60, game.fire_bank(1) * 60) # no room for another log
|
||
xp = self.kai["xp"]["firekeeping"]
|
||
self.assertEqual(game.start_action(self.world, self.kai, "tend_forge_logs", T0 + 3000), "Your pile in the forge is full")
|
||
self.assertEqual(self.kai["xp"]["firekeeping"], xp)
|
||
|
||
def test_a_better_log_pushes_your_cheaper_fuel_out_but_not_the_reverse(self):
|
||
bank = game.fire_bank(15) * 60
|
||
self.fires["kiln"].update(fuel=[{"tier": 0, "left": bank, "by": "kai"}], at=T0) # kai's pile: full of plain logs
|
||
self.kai["xp"]["firekeeping"] = game.XP_TABLE[15]
|
||
self.kai["bank"] = {"oak_logs": 1, "logs": 1}
|
||
self.assertEqual(game.start_action(self.world, self.kai, "tend_kiln_logs", T0), "Your pile in the kiln is full")
|
||
self.assertIsNone(game.start_action(self.world, self.kai, "tend_kiln_oak_logs", T0)) # a better log still fits
|
||
game.feed_fire(self.world, self.kai, "kiln", 1)
|
||
self.assertEqual(game.heat(self.world, "kiln"), 1)
|
||
self.assertAlmostEqual(sum(chunk["left"] for chunk in self.fires["kiln"]["fuel"]), bank)
|
||
self.assertEqual(game.start_action(self.world, self.kai, "tend_kiln_logs", T0), "Your pile in the kiln is full")
|
||
|
||
def test_a_beginner_can_always_feed_a_fire_a_veteran_filled(self):
|
||
self.fires["forge"].update(fuel=[{"tier": 5, "left": 100 * 3600, "by": "kai"}], at=T0)
|
||
self.sam["bank"] = {"logs": 5}
|
||
self.tend(self.sam, "forge", "logs", T0, 95)
|
||
self.assertEqual(self.sam["bank"]["logs"], 2)
|
||
self.assertGreater(self.sam["xp"]["firekeeping"], 0)
|
||
self.assertEqual(game.heat(self.world, "forge"), 5)
|
||
|
||
def test_piles_share_the_burn_and_the_time_adds_up(self):
|
||
fire = self.fires["forge"]
|
||
fire.update(fuel=[{"tier": 5, "left": 600, "by": "kai"}, {"tier": 0, "left": 600, "by": "sam"}], at=T0)
|
||
game.burn_fires(self.world, T0 + 600) # half each
|
||
self.assertEqual([round(chunk["left"]) for chunk in fire["fuel"]], [300, 300])
|
||
self.assertEqual(game.heat(self.world, "forge"), 5) # the beginner's logs keep the violet going longer
|
||
game.burn_fires(self.world, T0 + 1199)
|
||
self.assertEqual(game.heat(self.world, "forge"), 5)
|
||
game.burn_fires(self.world, T0 + 1200) # 20 minutes of fuel, 20 minutes of fire
|
||
self.assertEqual(fire["fuel"], [])
|
||
|
||
def test_a_pile_burns_its_hottest_fuel_first_on_game_time(self):
|
||
fire = self.fires["still"]
|
||
fire["fuel"] = [{"tier": 0, "left": 600, "by": "kai"}, {"tier": 2, "left": 300, "by": "kai"}]
|
||
fire["at"] = T0
|
||
game.burn_fires(self.world, T0 + 200)
|
||
self.assertEqual(game.heat(self.world, "still"), 2)
|
||
game.burn_fires(self.world, T0 + 400)
|
||
self.assertEqual(game.heat(self.world, "still"), 0)
|
||
self.assertAlmostEqual(fire["fuel"][0]["left"], 500)
|
||
game.burn_fires(self.world, T0 + 100) # never backwards
|
||
self.assertEqual(fire["at"], T0 + 400)
|
||
|
||
def test_a_hot_forge_pays_the_log_and_a_coal_but_never_the_last(self):
|
||
steel, copper = game.ACTION_BY_ID["smith_steel_bar"], game.ACTION_BY_ID["smith_copper_bar"]
|
||
self.assertEqual(game.fire_inputs(self.world, steel), {"iron_ore": 1, "coal": 2, "willow_logs": 1})
|
||
self.fires["forge"]["fuel"] = [{"tier": 1, "left": 600, "by": "kai"}]
|
||
self.assertEqual(game.fire_inputs(self.world, steel), {"iron_ore": 1, "coal": 2, "willow_logs": 1}) # too cool
|
||
self.assertEqual(game.fire_inputs(self.world, copper), {"copper_ore": 1})
|
||
self.fires["forge"]["fuel"] = [{"tier": 3, "left": 600, "by": "kai"}]
|
||
self.assertEqual(game.fire_inputs(self.world, steel), {"iron_ore": 1, "coal": 1})
|
||
self.assertEqual(game.fire_inputs(self.world, game.ACTION_BY_ID["smith_mithril_bar"]), {"mithril_ore": 1, "coal": 2})
|
||
|
||
def test_a_potion_brewed_from_logs_keeps_its_ingredient_log(self):
|
||
brew = game.ACTION_BY_ID["brew_woodcutting_potion_1"]
|
||
self.fires["still"]["fuel"] = [{"tier": 0, "left": 600, "by": "kai"}]
|
||
self.assertEqual(game.fire_inputs(self.world, brew), {"guam_leaf": 1, "logs": 1})
|
||
|
||
def test_a_lit_fire_pays_for_work_orders_too(self):
|
||
self.fires["forge"].update(fuel=[{"tier": 0, "left": 60000, "by": "kai"}], at=T0)
|
||
self.world["players"]["mo"] = mo = game.new_player("mo", "Mo", "commoner", T0)
|
||
mo["gold"], mo["bank"] = 100, {"copper_ore": 5, "logs": 5}
|
||
self.assertEqual(game.post_order(self.world, mo, "craft", "copper_bar", 5, ["copper_ore", "logs"], 50, T0),
|
||
"The forge pays that")
|
||
self.assertIsNone(game.post_order(self.world, mo, "craft", "copper_bar", 5, ["copper_ore"], 50, T0))
|
||
order = self.world["orders"][0]
|
||
self.assertEqual(game.provide_reagent(self.world, mo, order["id"], "logs"), "The forge pays that")
|
||
self.sam["bank"] = {}
|
||
self.assertIsNone(game.work_order(self.world, self.sam, order["id"], T0))
|
||
game.process_player(self.world, self.sam, T0 + 600, random.Random(1))
|
||
self.assertEqual(order["status"], "done")
|
||
self.assertEqual(mo["bank"], {"copper_bar": 5, "logs": 5})
|
||
self.assertEqual(self.fires["forge"]["saved"], 5)
|
||
|
||
def test_warmth_goes_to_whoever_fed_the_fire(self):
|
||
self.fires["kitchen"]["fuel"] = [{"tier": 0, "left": 6000, "by": "kai"}]
|
||
self.fires["kitchen"]["at"] = T0
|
||
self.sam["bank"] = {"raw_shrimp": 4}
|
||
game.start_action(self.world, self.sam, "cook_shrimp", T0)
|
||
game.process_player(self.world, self.sam, T0 + 85, random.Random(1))
|
||
self.assertEqual(self.sam["bank"]["cooked_shrimp"], 4)
|
||
self.assertNotIn("logs", self.sam["bank"])
|
||
self.assertAlmostEqual(self.kai["xp"]["firekeeping"], 4 * 6 * game.WARMTH)
|
||
self.assertAlmostEqual(self.kai["away"]["xp"]["firekeeping"], 4 * 6 * game.WARMTH)
|
||
self.assertEqual(self.fires["kitchen"]["saved"], 4)
|
||
self.assertEqual(self.sam["xp"]["firekeeping"], 0)
|
||
|
||
def test_warmth_is_shared_by_every_pile_hot_enough_for_the_craft(self):
|
||
self.world["players"]["ash"] = ash = game.new_player("ash", "Ash", "commoner", T0)
|
||
ash["xp"]["cooking"] = game.XP_TABLE[15]
|
||
self.fires["kitchen"].update(at=T0, fuel=[{"tier": 3, "left": 6000, "by": "kai"},
|
||
{"tier": 0, "left": 6000, "by": "sam"}])
|
||
ash["bank"] = {"raw_shrimp": 1, "raw_trout": 1}
|
||
game.start_action(self.world, ash, "cook_shrimp", T0, once=True)
|
||
game.process_player(self.world, ash, T0 + 25, random.Random(1))
|
||
self.assertAlmostEqual(self.kai["xp"]["firekeeping"], 6 * game.WARMTH / 2)
|
||
self.assertAlmostEqual(self.sam["xp"]["firekeeping"], 6 * game.WARMTH / 2)
|
||
game.start_action(self.world, ash, "cook_trout", T0 + 25, once=True)
|
||
game.process_player(self.world, ash, T0 + 60, random.Random(1))
|
||
self.assertAlmostEqual(self.kai["xp"]["firekeeping"], 6 * game.WARMTH / 2 + 15 * game.WARMTH) # trout: kai's alone
|
||
self.assertAlmostEqual(self.sam["xp"]["firekeeping"], 6 * game.WARMTH / 2)
|
||
|
||
def test_catch_up_runs_everyone_and_keeps_what_they_missed(self):
|
||
game.start_action(self.world, self.sam, "mine_stone", T0)
|
||
self.assertTrue(game.catch_up(self.world, T0 + 100, random.Random(1)))
|
||
game.catch_up(self.world, T0 + 200, random.Random(1))
|
||
self.assertEqual(self.sam["away"]["done"], 10)
|
||
summary = game.take_summary(self.sam, game.process_player(self.world, self.sam, T0 + 220, random.Random(1)))
|
||
self.assertEqual(summary["done"], 11)
|
||
self.assertEqual(summary["items"], {"stone": 11})
|
||
self.assertNotIn("away", self.sam)
|
||
|
||
def test_old_worlds_get_their_fires(self):
|
||
del self.world["city"]["fires"]
|
||
del self.kai["xp"]["firekeeping"]
|
||
self.world["version"] = 23
|
||
game.migrate(self.world, T0)
|
||
self.assertEqual(set(self.world["city"]["fires"]), set(game.FIRES))
|
||
self.assertEqual(self.kai["xp"]["firekeeping"], 0)
|
||
self.assertIn("tool_firekeeping", self.kai["equip"])
|
||
|
||
|
||
class PatchNotesTest(unittest.TestCase):
|
||
def test_newest_first_and_known_kinds(self):
|
||
dates = [release["date"] for release in game.PATCH_NOTES]
|
||
self.assertEqual(dates, sorted(dates, reverse=True))
|
||
for release in game.PATCH_NOTES:
|
||
self.assertTrue(release["notes"])
|
||
for kind, text in release["notes"]:
|
||
self.assertIn(kind, ("new", "balance", "fix"))
|
||
self.assertTrue(text)
|
||
|
||
|
||
class PartyTest(unittest.TestCase):
|
||
def pair(self, level=12):
|
||
world = world_with(("wil", "commoner"), ("kay", "commoner"))
|
||
for p in world["players"].values():
|
||
p["xp"]["combat"] = game.XP_TABLE[level]
|
||
p["hp"] = game.max_hp(p)
|
||
p["equip"].update(weapon="iron_sword", helmet="iron_helmet", body="iron_body", legs="iron_legs")
|
||
p["bank"] = {"bread": 300}
|
||
return world, world["players"]["wil"], world["players"]["kay"]
|
||
|
||
def online(self, world, now, rng):
|
||
"""Everyone playing: catch up to `now` as their pages would."""
|
||
for p in world["players"].values():
|
||
p["last_active"] = now
|
||
game.catch_up(world, now, rng)
|
||
|
||
def test_tagging_in_keeps_what_already_happened(self):
|
||
world, wil, kay = self.pair()
|
||
rng = random.Random(1)
|
||
game.start_action(world, wil, "fight_scouts", T0)
|
||
self.online(world, T0 + 0.1, rng)
|
||
before = json.loads(json.dumps(wil["action"]["fight"]))
|
||
k = len(before["events"]) // 2
|
||
at = (before["events"][k - 1][0] + before["events"][k][0]) / 2
|
||
self.online(world, T0 + at, rng)
|
||
self.assertIsNone(game.join_fight(world, kay, "wil", T0 + at))
|
||
fight = game.current_fight(world, wil)
|
||
self.assertIs(fight, game.current_fight(world, kay)) # both watch the one fight
|
||
self.assertEqual(fight["events"][:k], before["events"][:k])
|
||
theirs = [e for e in fight["events"] if e[1] == "kay"]
|
||
self.assertTrue(theirs)
|
||
self.assertGreater(theirs[0][0], at)
|
||
self.assertEqual(kay["action"]["start"], wil["action"]["start"])
|
||
|
||
def test_kills_go_faster_and_everyone_gets_the_xp(self):
|
||
world, wil, kay = self.pair()
|
||
game.start_action(world, wil, "fight_scouts", T0)
|
||
self.assertIsNone(game.join_fight(world, kay, "wil", T0))
|
||
self.online(world, T0 + 3600, random.Random(2))
|
||
together = wil["away"]
|
||
self.assertEqual(together["done"], kay["away"]["done"])
|
||
self.assertEqual(together["with"], {"Kay": together["done"]})
|
||
self.assertEqual(wil["xp"]["combat"], kay["xp"]["combat"]) # the same kills, the full XP each
|
||
self.assertGreater(wil["gold"] + kay["gold"], 0)
|
||
self.assertLessEqual(abs(wil["gold"] - kay["gold"]), together["wins"] * 4) # split evenly, odd coins aside
|
||
solo, alone = self.pair()[0:2]
|
||
game.start_action(solo, alone, "fight_scouts", T0)
|
||
self.online(solo, T0 + 3600, random.Random(2))
|
||
self.assertGreater(together["xp"]["combat"], alone["away"]["xp"]["combat"] * 1.3)
|
||
|
||
def test_leaving_lets_the_other_carry_on(self):
|
||
world, wil, kay = self.pair()
|
||
rng = random.Random(3)
|
||
game.start_action(world, wil, "fight_scouts", T0)
|
||
game.join_fight(world, kay, "wil", T0)
|
||
self.online(world, T0 + 5, rng)
|
||
self.assertEqual(len(world["parties"]), 1)
|
||
self.assertIsNone(game.start_action(world, kay, "mine_stone", T0 + 5))
|
||
self.assertEqual(world["parties"], {})
|
||
self.assertNotIn("party", wil["action"])
|
||
left = game.member_of(wil["action"]["fight"], "kay") # the fight goes on without her
|
||
self.assertEqual(left["result"], "left")
|
||
self.online(world, T0 + 1200, rng)
|
||
self.assertGreater(wil["away"]["wins"], 3)
|
||
|
||
def test_away_too_long_steps_out(self):
|
||
world, wil, kay = self.pair(level=40)
|
||
for p in (wil, kay):
|
||
p["bank"] = {"bread": 100_000}
|
||
game.start_action(world, wil, "fight_scouts", T0)
|
||
game.join_fight(world, kay, "wil", T0)
|
||
now = T0 + game.OFFLINE_CAP + 3600
|
||
wil["last_active"] = now # wil kept playing, kay went to bed
|
||
game.catch_up(world, now, random.Random(4))
|
||
self.assertEqual(world["parties"], {})
|
||
self.assertEqual(kay["action"]["id"], "fight_scouts")
|
||
self.assertNotIn("party", kay["action"])
|
||
self.assertGreater(wil["away"]["done"], kay["away"]["done"]) # wil carried on alone for the last hour
|
||
|
||
def test_knocked_out_in_a_won_fight_still_shares_the_win(self):
|
||
world, wil, kay = self.pair()
|
||
game.start_action(world, wil, "fight_scouts", T0)
|
||
self.online(world, T0 + 0.1, random.Random(0))
|
||
kay["hp"] = 1 # tags in as she is
|
||
self.assertIsNone(game.join_fight(world, kay, "wil", T0 + 0.1))
|
||
fight = game.current_fight(world, wil)
|
||
mine = game.member_of(fight, "kay")
|
||
self.assertTrue(mine["down"])
|
||
self.assertEqual((fight["result"], mine["result"]), ("won", "won"))
|
||
self.online(world, T0 + fight["duration"] + 0.01, random.Random(5))
|
||
self.assertEqual(kay["away"]["wins"], 1)
|
||
self.assertAlmostEqual(kay["hp"], game.max_hp(kay) * game.KNOCKED_OUT_HP)
|
||
self.assertEqual(kay["action"]["id"], "fight_scouts")
|
||
|
||
def test_a_boss_needs_a_group(self):
|
||
world, wil, kay = self.pair(level=30)
|
||
for p in (wil, kay):
|
||
p["equip"].update(weapon="steel_sword", shield="steel_shield", helmet="steel_helmet", body="steel_body",
|
||
legs="steel_legs")
|
||
self.assertIsNone(game.start_action(world, wil, "fight_ruins", T0))
|
||
self.assertIn("Wil is looking for a group", world["ledger"][-1]["text"])
|
||
self.online(world, T0 + 3600, random.Random(1))
|
||
self.assertNotIn("fight", wil["action"]) # alone, nothing happens
|
||
self.assertEqual(wil["away"]["done"], 0) if "away" in wil else None
|
||
self.assertEqual(wil["action"]["start"], T0 + 3600)
|
||
self.assertIsNone(game.join_fight(world, kay, "wil", T0 + 3600))
|
||
self.online(world, T0 + 7200, random.Random(1))
|
||
self.assertGreater(wil["away"]["wins"], 0)
|
||
self.assertEqual(wil["away"]["wins"], kay["away"]["wins"])
|
||
self.assertIsNone(game.start_action(world, kay, "mine_stone", T0 + 7200))
|
||
self.online(world, T0 + 9000, random.Random(1))
|
||
wins = wil["away"]["wins"]
|
||
self.online(world, T0 + 12600, random.Random(1))
|
||
self.assertEqual(wil["away"]["wins"], wins) # back to waiting
|
||
self.assertEqual(wil["action"]["id"], "fight_ruins")
|
||
|
||
def test_who_can_join(self):
|
||
world, wil, kay = self.pair()
|
||
self.assertEqual(game.join_fight(world, kay, "wil", T0), "Wil isn't fighting")
|
||
game.start_action(world, wil, "mine_stone", T0)
|
||
self.assertEqual(game.join_fight(world, kay, "wil", T0), "Wil isn't fighting")
|
||
self.assertEqual(game.join_fight(world, kay, "kay", T0), "No one to join")
|
||
wil["xp"]["combat"] = game.XP_TABLE[60]
|
||
game.start_action(world, wil, "fight_ruins", T0)
|
||
self.assertEqual(game.join_fight(world, kay, "wil", T0), "Needs level 20")
|
||
game.start_action(world, wil, "fight_scouts", T0)
|
||
self.assertIsNone(game.join_fight(world, kay, "wil", T0))
|
||
self.assertEqual(game.join_fight(world, wil, "kay", T0), "Already fighting together")
|
||
|
||
def test_bosses_give_everyone_their_own_loot(self):
|
||
world, wil, kay = self.pair(level=30)
|
||
for p in (wil, kay):
|
||
p["equip"].update(weapon="steel_sword", shield="steel_shield", helmet="steel_helmet", body="steel_body",
|
||
legs="steel_legs")
|
||
p["bank"] = {"cooked_salmon": 5000}
|
||
game.start_action(world, wil, "fight_ruins", T0)
|
||
self.assertIsNone(game.join_fight(world, kay, "wil", T0))
|
||
self.online(world, T0 + 3600, random.Random(6))
|
||
wins = wil["away"]["wins"]
|
||
self.assertGreater(wins, 10)
|
||
low, high = game.ACTION_BY_ID["fight_ruins"]["gold"]
|
||
for p in (wil, kay):
|
||
self.assertTrue(wins * low <= p["away"]["gold"] <= wins * high) # a whole purse each
|
||
bones = p["away"]["items"].get("big_bones", 0) + p["away"]["overflow"].get("big_bones", 0)
|
||
self.assertEqual(bones, wins) # the sure drop, for each of them
|
||
|
||
def test_loot_is_shared_out(self):
|
||
shares = game.share_loot((7, {"bones": 1, "arrows": 5}), ["a", "b"], random.Random(1))
|
||
self.assertEqual(sorted(gold for gold, _ in shares.values()), [3, 4])
|
||
self.assertEqual(sum(got.get("arrows", 0) for _, got in shares.values()), 5)
|
||
self.assertEqual(sum(got.get("bones", 0) for _, got in shares.values()), 1)
|
||
|
||
def test_a_fight_in_progress_is_drawn_again_after_the_update(self):
|
||
world, wil, _ = self.pair()
|
||
game.start_action(world, wil, "fight_scouts", T0)
|
||
wil["action"]["fight"] = {"old": True}
|
||
world["version"] = 24
|
||
del world["parties"], world["next_party_id"]
|
||
game.migrate(world, T0)
|
||
self.assertNotIn("fight", wil["action"])
|
||
self.assertEqual(world["parties"], {})
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|