Technique swapping implementation
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user