Technique swapping implementation

This commit is contained in:
2026-06-12 16:46:03 -07:00
parent 121f2f7498
commit fa37cffe47
10 changed files with 724 additions and 128 deletions

View File

@@ -70,23 +70,64 @@ def test_character_creation_and_swap(session):
crud.submit_techniques(session, p3.id, "Jaspar Blast", "Shoot", "Run")
session.refresh(game)
# The swap should have automatically triggered because all 3 submitted
# The swap phase should have automatically begun because all 3 submitted
assert game.phase == "swap_techniques"
session.refresh(p1)
session.refresh(p2)
session.refresh(p3)
# Everyone starts the swap phase holding their own creations
held_p1 = json.loads(p1.held_techniques)
assert [t["text"] for t in held_p1] == ["Carlos Strike", "Hide", "Roll"]
assert all(t["creator_id"] == p1.id for t in held_p1)
assert json.loads(p1.swapped_techniques) == []
# Can't ready up while still holding your own techniques
ok, msg = crud.set_swap_ready(session, p1.id, True)
assert not ok
# A scripted set of blind swaps that leaves nobody holding their own work:
# (offerer, offered technique, responder, technique traded back)
swaps = [
(p1, "Carlos Strike", p2, "Barry Leap"),
(p1, "Hide", p3, "Jaspar Blast"),
(p1, "Roll", p2, "Bite"),
(p2, "Scream", p3, "Shoot"),
(p3, "Run", p2, "Carlos Strike"),
]
for offerer, offered, responder, returned in swaps:
ok, msg = crud.offer_swap(session, offerer.id, responder.id, offered)
assert ok, msg
ok, msg = crud.respond_swap_offer(session, responder.id, offerer.id, True, returned)
assert ok, msg
for p in (p1, p2, p3):
session.refresh(p)
held = json.loads(p.held_techniques)
assert len(held) == 3
assert all(t["creator_id"] != p.id for t in held)
ok, msg = crud.set_swap_ready(session, p.id, True)
assert ok, msg
session.refresh(game)
assert game.phase == "assign_techniques"
# Everyone's swapped techniques were locked in from their held set
session.refresh(p1)
techs_p1 = json.loads(p1.swapped_techniques)
assert len(techs_p1) == 3
# Check that Carlos didn't get any of his own techniques
assert "Carlos Strike" not in techs_p1
# Now assign to face cards
crud.assign_face_card_techniques(session, p1.id, techs_p1[0], techs_p1[1], techs_p1[2])
crud.assign_face_card_techniques(session, p2.id, "T2", "T3", "T4")
crud.assign_face_card_techniques(session, p3.id, "T5", "T6", "T7")
# Now assign to face cards (must use exactly the swapped techniques)
ok, msg = crud.assign_face_card_techniques(session, p1.id, "T2", "T3", "T4")
assert not ok
for p in (p1, p2, p3):
session.refresh(p)
techs = json.loads(p.swapped_techniques)
ok, msg = crud.assign_face_card_techniques(session, p.id, techs[0], techs[1], techs[2])
assert ok, msg
session.refresh(game)
# All are ready, should transition to scene_setup and assign starting ranks!
assert game.phase == "scene_setup"
@@ -1083,6 +1124,134 @@ def test_submit_techniques_rejects_collisions(session):
session.refresh(game)
assert game.phase == "swap_techniques"
def make_swap_game(session):
"""Helper: 3 players in the swap_techniques phase, each holding their own techniques."""
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "Carlos", is_creator=True)
p2 = crud.add_player(session, game.id, "Barry")
p3 = crud.add_player(session, game.id, "Jaspar")
crud.submit_techniques(session, p1.id, "A1", "A2", "A3")
crud.submit_techniques(session, p2.id, "B1", "B2", "B3")
crud.submit_techniques(session, p3.id, "C1", "C2", "C3")
session.refresh(game)
assert game.phase == "swap_techniques"
return game, p1, p2, p3
def test_swap_offer_validation(session):
game, p1, p2, p3 = make_swap_game(session)
# Can't offer a technique you don't hold, or swap with yourself
ok, _ = crud.offer_swap(session, p1.id, p2.id, "B1")
assert not ok
ok, _ = crud.offer_swap(session, p1.id, p1.id, "A1")
assert not ok
# One pending offer at a time
ok, msg = crud.offer_swap(session, p1.id, p2.id, "A1")
assert ok, msg
ok, _ = crud.offer_swap(session, p1.id, p3.id, "A2")
assert not ok
ok, msg = crud.cancel_swap_offer(session, p1.id)
assert ok, msg
session.refresh(p1)
assert p1.swap_offer_to_id is None and p1.swap_offer_technique == ""
# Accepting must not trade back a technique the offerer created
ok, msg = crud.offer_swap(session, p1.id, p2.id, "A1")
assert ok, msg
crud.respond_swap_offer(session, p2.id, p1.id, True, "B1")
session.refresh(p2)
# p2 now holds A1 (created by p1) and can't offer it back to p1
ok, _ = crud.offer_swap(session, p2.id, p1.id, "A1")
assert not ok
ok, msg = crud.offer_swap(session, p1.id, p2.id, "A2")
assert ok, msg
ok, _ = crud.respond_swap_offer(session, p2.id, p1.id, True, "A1")
assert not ok # A1 was created by the offerer
# Declining clears the offer
ok, msg = crud.respond_swap_offer(session, p2.id, p1.id, False)
assert ok, msg
session.refresh(p1)
assert p1.swap_offer_to_id is None
# Only the actual recipient can respond
ok, msg = crud.offer_swap(session, p1.id, p2.id, "A2")
assert ok, msg
ok, _ = crud.respond_swap_offer(session, p3.id, p1.id, True, "C1")
assert not ok
def test_swap_accept_exchanges_and_withdraws_stale_offer(session):
game, p1, p2, p3 = make_swap_game(session)
# p2 promises B1 to p3, then trades B1 away to p1 instead
ok, msg = crud.offer_swap(session, p2.id, p3.id, "B1")
assert ok, msg
ok, msg = crud.offer_swap(session, p1.id, p2.id, "A1")
assert ok, msg
ok, msg = crud.respond_swap_offer(session, p2.id, p1.id, True, "B1")
assert ok, msg
session.refresh(p1)
session.refresh(p2)
held_p1 = json.loads(p1.held_techniques)
held_p2 = json.loads(p2.held_techniques)
# The techniques moved with their creator tags intact
assert {"text": "B1", "creator_id": p2.id} in held_p1
assert {"text": "A1", "creator_id": p1.id} in held_p2
assert not any(t["text"] == "A1" for t in held_p1)
# p2's now-impossible offer to p3 was withdrawn
assert p2.swap_offer_to_id is None and p2.swap_offer_technique == ""
def test_auto_assign_techniques(session):
game, p1, p2, p3 = make_swap_game(session)
ok, msg = crud.auto_assign_techniques(session, game)
assert ok, msg
session.refresh(game)
assert game.phase == "scene_setup"
for p in (p1, p2, p3):
session.refresh(p)
assert p.tech_jack and p.tech_queen and p.tech_king
own = {t.lower() for t in json.loads(p.created_techniques)}
held = {p.tech_jack.lower(), p.tech_queen.lower(), p.tech_king.lower()}
assert not (own & held)
# Ranks were rolled too
assert sorted(p.rank for p in (p1, p2, p3)) == [1, 2, 3]
# Wrong phase is rejected
ok, _ = crud.auto_assign_techniques(session, game)
assert not ok
def test_swap_offer_technique_hidden_in_state(session):
from fastapi.testclient import TestClient
from pirats.main import app
from pirats.database import get_session
game, p1, p2, p3 = make_swap_game(session)
ok, msg = crud.offer_swap(session, p1.id, p2.id, "A1")
assert ok, msg
def get_session_override():
yield session
app.dependency_overrides[get_session] = get_session_override
client = TestClient(app, base_url="http://testserver")
try:
# The recipient sees that an offer exists, but not which technique
state = client.get(f"/api/game/{game.id}/player/{p2.id}/state").json()
p1_blob = next(p for p in state["players"] if p["id"] == p1.id)
assert p1_blob["swap_offer_to_id"] == p2.id
assert "swap_offer_technique" not in p1_blob
# The offerer still sees their own offered technique
state = client.get(f"/api/game/{game.id}/player/{p1.id}/state").json()
assert state["player"]["swap_offer_technique"] == "A1"
p1_blob = next(p for p in state["players"] if p["id"] == p1.id)
assert p1_blob["swap_offer_technique"] == "A1"
finally:
app.dependency_overrides.clear()
def test_recruit_smell_name_normalization(session):
game = crud.create_game(session)
p1 = crud.add_player(session, game.id, "P1")