Active play and the Watchtower Depths dungeon

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-24 12:05:23 +01:00
parent db1a00e8aa
commit 9d61de2012
9 changed files with 790 additions and 56 deletions

View file

@ -736,6 +736,178 @@ class AbilityTest(unittest.TestCase):
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_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))
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_joining_only_before_the_first_pull_and_up_to_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 + 1)
self.assertIsNone(game.join_fight(self.world, self.ann, "wil", T0 + 1)) # still resting: in time
self.party()["run"]["pull"] = 1
extra = game.new_player("bo", "Bo", "ranger", T0)
extra["xp"]["combat"] = game.XP_TABLE[60]
self.world["players"]["bo"] = extra
self.assertEqual(game.join_fight(self.world, extra, "wil", T0 + 2), "The run has started")
self.assertFalse(game.joinable(self.world, self.wil))
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)
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)
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_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 NewSkillsTest(unittest.TestCase):
def test_tools_speed_up_gathering(self):
world = world_with(("pia", "commoner"))