Fight together: tag into anyone's fight
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e0e0a824a9
commit
2d2cb16aff
6 changed files with 574 additions and 102 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import random
|
||||
import sys
|
||||
import unittest
|
||||
|
|
@ -171,7 +172,7 @@ class ActionTest(unittest.TestCase):
|
|||
p["bank"] = {"bread": 500}
|
||||
game.start_action(world, p, "fight_wolves", T0)
|
||||
summary = game.process_player(world, p, T0 + 3600, random.Random(5))
|
||||
self.assertGreater(summary["done"], 50) # a fight lasts as long as it takes
|
||||
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)
|
||||
|
|
@ -521,7 +522,7 @@ class CombatTest(unittest.TestCase):
|
|||
p["bank"] = {"cooked_shrimp": 100}
|
||||
p["hp"] = 1
|
||||
fight = game.simulate_fight(world, p, game.ACTION_BY_ID["fight_clay_golems"], random.Random(3))
|
||||
mine = [event for event in fight["events"] if event[1] == "you"]
|
||||
mine = [event for event in fight["events"] if event[1] == p["id"]]
|
||||
bite = next(i for i, event in enumerate(mine) if event[2] == "eat")
|
||||
swing = next(event for event in mine[bite:] if event[2] != "eat")
|
||||
self.assertGreaterEqual(swing[0] - mine[bite][0], game.EAT_DELAY)
|
||||
|
|
@ -553,12 +554,12 @@ class CombatTest(unittest.TestCase):
|
|||
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")
|
||||
self.assertTrue(all(e[1] in ("you", "foe") and e[2] in ("hit", "crit", "miss", "eat", "wind") for e in fight["events"]))
|
||||
self.assertTrue(all(e[1] in (p["id"], "foe") and e[2] in ("hit", "crit", "miss", "eat", "wind") for e in fight["events"]))
|
||||
self.assertEqual(fight["events"][-1][5], 0) # the last swing 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"], fight["hp"], delta=0.1)
|
||||
self.assertAlmostEqual(p["hp"], game.member_of(fight, p["id"])["hp"], delta=0.1)
|
||||
self.assertAlmostEqual(p["action"]["start"], T0 + fight["duration"] + game.RESPAWN) # then a short pause
|
||||
|
||||
def test_outclassing_an_enemy_kills_faster(self):
|
||||
|
|
@ -577,7 +578,7 @@ class CombatTest(unittest.TestCase):
|
|||
game.process_player(world, p, T0 + first["duration"] + 0.01, random.Random(1))
|
||||
self.assertAlmostEqual(p["action"]["start"], T0 + first["duration"] + game.RESPAWN) # the pause starts at the kill
|
||||
second = p["action"]["fight"]
|
||||
self.assertEqual(next(e[0] for e in second["events"] if e[1] == "you"), 2.4) # a full swing, from scratch
|
||||
self.assertEqual(next(e[0] for e in second["events"] if e[1] == p["id"]), 2.4) # a full swing, from scratch
|
||||
def test_preferred_food_is_eaten_first(self):
|
||||
world, p = self.fighter()
|
||||
p["bank"] = {"bread": 5, "cooked_salmon": 5}
|
||||
|
|
@ -855,11 +856,13 @@ class PrayerTest(unittest.TestCase):
|
|||
world, p = self.priest(level=10, combat=10)
|
||||
scouts = game.ACTION_BY_ID["fight_scouts"]
|
||||
p["pp"], p["hp"] = 1000, 50
|
||||
plain = game.simulate_fight(world, p, scouts, random.Random(3))
|
||||
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(3))
|
||||
swings = sum(1 for e in plain["events"] if e[1] == "you")
|
||||
mended = game.simulate_fight(world, p, scouts, random.Random(2))
|
||||
self.assertEqual(plain["end"], mended["end"]) # the same fight, only the mending differs
|
||||
swings = sum(1 for e in plain["events"] if e[1] == p["id"])
|
||||
top = game.max_hp(p)
|
||||
plain, mended = game.member_of(plain, p["id"]), game.member_of(mended, p["id"])
|
||||
self.assertAlmostEqual(mended["hp"] - plain["hp"], swings * game.MEND * top * plain["interval"], places=6)
|
||||
self.assertLess(game.fight_odds(p, scouts)["damage"], game.fight_odds({**p, "prayers": []}, scouts)["damage"])
|
||||
|
||||
|
|
@ -1411,7 +1414,7 @@ class WeaponStyleTest(unittest.TestCase):
|
|||
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], "you") # the bow strikes first
|
||||
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")
|
||||
|
|
@ -2050,5 +2053,128 @@ class PatchNotesTest(unittest.TestCase):
|
|||
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"]["weapon"] = "iron_sword"
|
||||
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
|
||||
low, high = game.ACTION_BY_ID["fight_scouts"]["gold"]
|
||||
self.assertTrue(together["wins"] * low <= wil["gold"] + kay["gold"] <= together["wins"] * high)
|
||||
self.assertLessEqual(abs(wil["gold"] - kay["gold"]), together["wins"]) # split evenly, odd coins aside
|
||||
self.assertEqual(wil["bank"].get("bones", 0) + kay["bank"].get("bones", 0), together["wins"]) # one each kill
|
||||
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["done"], alone["away"]["done"] * 1.5)
|
||||
|
||||
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 + 600, 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_a_retreat_leaves_the_rest_fighting(self):
|
||||
world, wil, kay = self.pair()
|
||||
kay["bank"] = {}
|
||||
kay["hp"] = game.max_hp(kay) * 0.35
|
||||
game.start_action(world, wil, "fight_scouts", T0)
|
||||
self.assertIsNone(game.join_fight(world, kay, "wil", T0))
|
||||
self.online(world, T0 + 600, random.Random(5))
|
||||
self.assertIsNone(kay["action"])
|
||||
self.assertTrue(kay["stopped"].startswith("Retreated"))
|
||||
self.assertEqual(wil["action"]["id"], "fight_scouts")
|
||||
self.assertEqual(world["parties"], {})
|
||||
|
||||
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 26")
|
||||
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_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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue