Let players voluntarily leave a game
Adds a "🚪 Leave Game" option to the corner menu for every player, so someone who's done can remove themselves instead of waiting for an admin to kick them. leave_game reuses the kick mechanics (cards to discard, reference cleanup, phase re-check) via a shared _remove_player helper; it logs a friendlier "left the crew" event and, like kick, refuses the last Admin so the table isn't orphaned. The self-service route POST /game/{gid}/player/{pid}/leave lives next to join and authenticates on the player's own id (no admin key). The handler confirms, forgets the saved session, and routes home. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1967,3 +1967,73 @@ def test_kick_endpoint_and_last_admin_guard():
|
||||
assert crud.get_player(session, creator.id) is None
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
def test_leave_game_unblocks_phase(session):
|
||||
"""A player who leaves on their own is removed like a kick, and the phase
|
||||
advances if they were the last blocker. The log shows a leave, not a kick."""
|
||||
game = crud.create_game(session)
|
||||
p1 = crud.add_player(session, game.id, "P1", is_creator=True)
|
||||
p2 = crud.add_player(session, game.id, "P2")
|
||||
p3 = crud.add_player(session, game.id, "P3")
|
||||
game.phase = "character_creation"
|
||||
session.add(game)
|
||||
session.commit()
|
||||
|
||||
crud.submit_techniques(session, p1.id, "Alpha", "Beta", "Gamma")
|
||||
crud.submit_techniques(session, p2.id, "Delta", "Epsilon", "Zeta")
|
||||
session.refresh(game)
|
||||
assert game.phase == "character_creation" # still blocked on p3
|
||||
|
||||
ok, msg = crud.leave_game(session, game, p3)
|
||||
assert ok, msg
|
||||
session.refresh(game)
|
||||
assert game.phase == "swap_techniques"
|
||||
assert crud.get_player(session, p3.id) is None
|
||||
assert any("P3 left the crew" in e.message for e in game.events)
|
||||
|
||||
def test_leave_endpoint_and_last_admin_guard():
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlmodel import SQLModel, create_engine, Session
|
||||
from pirats.main import app
|
||||
from pirats import crud
|
||||
|
||||
engine = create_engine(
|
||||
"sqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
session = Session(engine)
|
||||
|
||||
game = crud.create_game(session)
|
||||
creator = crud.add_player(session, game.id, "Creator", is_creator=True)
|
||||
mate = crud.add_player(session, game.id, "Mate")
|
||||
extra = crud.add_player(session, game.id, "Extra")
|
||||
|
||||
def get_session_override():
|
||||
yield session
|
||||
|
||||
from pirats.database import get_session
|
||||
app.dependency_overrides[get_session] = get_session_override
|
||||
client = TestClient(app)
|
||||
|
||||
try:
|
||||
# The sole Admin can't abandon the table without handing off Admin first
|
||||
r = client.post(f"/api/game/{game.id}/player/{creator.id}/leave")
|
||||
assert r.status_code == 400
|
||||
assert "last Admin" in r.json()["error"]
|
||||
|
||||
# A regular player can leave themselves (no admin key needed)
|
||||
r = client.post(f"/api/game/{game.id}/player/{mate.id}/leave")
|
||||
assert r.status_code == 200
|
||||
assert crud.get_player(session, mate.id) is None
|
||||
|
||||
# With a co-admin present, even the creator can leave
|
||||
client.post(f"/api/game/{game.id}/admin/set-admin",
|
||||
data={"key": game.admin_key, "target_player_id": extra.id, "is_admin": True})
|
||||
r = client.post(f"/api/game/{game.id}/player/{creator.id}/leave")
|
||||
assert r.status_code == 200
|
||||
assert crud.get_player(session, creator.id) is None
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
Reference in New Issue
Block a user