Player minimum of 3 to start a game or scene
Enforced server-side in lobby/start and confirm_scene_setup via crud_base.has_min_players; Dev Mode bypasses it so local single-player testing still works. Lobby and scene-setup UIs show a player-count message instead of the start button when below the minimum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2
TODO.md
2
TODO.md
@@ -18,7 +18,7 @@
|
||||
## Preventing blocked play
|
||||
|
||||
- [x] **Multiple admins.** The Admin player should be able to grant other players Admin privileges in the admin panel.
|
||||
- [ ] **Player minimum.** You need at least three players to start a game, or to start a new scene if a player dropped out mid-game.
|
||||
- [x] **Player minimum.** You need at least three players to start a game, or to start a new scene if a player dropped out mid-game. (Dev Mode bypasses the minimum for local testing.)
|
||||
- [ ] **Player kicking.** Admins should be able to kick players out of the game so that it doesn't get stuck if a player vanishes. Admins can kick other admins.
|
||||
|
||||
## Session management
|
||||
|
||||
@@ -74,14 +74,14 @@
|
||||
|
||||
<div class="lobby-action">
|
||||
{#if state.player.is_admin}
|
||||
{#if state.players.length >= 1}
|
||||
{#if state.players.length >= 3 || state.game.dev_mode}
|
||||
<button on:click={startGame}
|
||||
disabled={starting}
|
||||
class="btn btn-primary btn-large glow-effect">
|
||||
{starting ? 'Starting...' : 'Start Character Creation'}
|
||||
</button>
|
||||
{:else}
|
||||
<button class="btn btn-primary btn-large" disabled>Waiting for players...</button>
|
||||
<button class="btn btn-primary btn-large" disabled>Waiting for players... ({state.players.length}/3 minimum)</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="waiting-indicator">
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
$: allRolesAssigned = state.players.every(p => p.role !== null || p.needs_reroll);
|
||||
$: deepPlayer = state.players.find(p => p.role === 'deep');
|
||||
$: piratPlayer = state.players.find(p => p.role === 'pirat');
|
||||
$: canStart = allRolesAssigned && deepPlayer && piratPlayer && (state.player.is_admin || state.player.role === 'deep');
|
||||
$: enoughPlayers = state.players.length >= 3 || state.game.dev_mode;
|
||||
$: canStart = enoughPlayers && allRolesAssigned && deepPlayer && piratPlayer && (state.player.is_admin || state.player.role === 'deep');
|
||||
</script>
|
||||
|
||||
<div class="scene-setup-view text-center">
|
||||
@@ -137,6 +138,8 @@
|
||||
class="btn btn-primary btn-large glow-effect">
|
||||
{starting ? 'Starting Scene...' : 'Confirm Roles & Shuffle Deck'}
|
||||
</button>
|
||||
{:else if !enoughPlayers}
|
||||
<p class="text-danger italic mt-4">You need at least 3 players to start a scene — share the join link!</p>
|
||||
{:else if !allRolesAssigned}
|
||||
<p class="text-muted italic mt-4">Waiting for all players to choose a role...</p>
|
||||
{:else if !deepPlayer}
|
||||
|
||||
@@ -234,6 +234,12 @@ def dev_mode_default() -> bool:
|
||||
return True
|
||||
return val.strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
MIN_PLAYERS = 3
|
||||
|
||||
def has_min_players(game: Game) -> bool:
|
||||
"""The crew needs at least MIN_PLAYERS to start a game or a new scene; Dev Mode bypasses this for testing."""
|
||||
return game.dev_mode or len(game.players) >= MIN_PLAYERS
|
||||
|
||||
def create_game(db: Session, crew_name: Optional[str] = None) -> Game:
|
||||
deck = cards.get_fresh_deck()
|
||||
game = Game(
|
||||
|
||||
@@ -7,7 +7,7 @@ from . import cards
|
||||
from .crud_base import (
|
||||
get_player, get_game, get_player_hand, set_player_hand,
|
||||
get_game_deck, calculate_max_hand_size, add_game_event, reshuffle_discard_pile,
|
||||
change_player_rank, set_captain, evaluate_card_play
|
||||
change_player_rank, set_captain, evaluate_card_play, has_min_players, MIN_PLAYERS
|
||||
)
|
||||
|
||||
# --- Scene Setup Operations ---
|
||||
@@ -33,6 +33,9 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]:
|
||||
if not game:
|
||||
return False, "Game not found."
|
||||
|
||||
if not has_min_players(game):
|
||||
return False, f"You need at least {MIN_PLAYERS} players to start a scene."
|
||||
|
||||
players = game.players
|
||||
|
||||
# Default any unselected roles to 'pirat'. Pending recruits have no Pi-Rat
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlmodel import Session
|
||||
from .database import get_session
|
||||
from . import crud
|
||||
@@ -10,6 +11,8 @@ def lobby_start_character_creation(game_id: str, db: Session = Depends(get_sessi
|
||||
game = crud.get_game(db, game_id)
|
||||
if not game:
|
||||
raise HTTPException(status_code=404, detail="Game not found")
|
||||
if not crud.has_min_players(game):
|
||||
return JSONResponse({"error": f"You need at least {crud.MIN_PLAYERS} players to start a game."}, status_code=400)
|
||||
game.phase = "character_creation"
|
||||
db.add(game)
|
||||
db.commit()
|
||||
|
||||
@@ -1433,3 +1433,76 @@ def test_grant_and_revoke_admin():
|
||||
assert "admin_key" not in response.json()["game"]
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
def test_player_minimum(session):
|
||||
game = crud.create_game(session)
|
||||
game.dev_mode = False
|
||||
session.add(game)
|
||||
session.commit()
|
||||
crud.add_player(session, game.id, "P1", is_creator=True)
|
||||
crud.add_player(session, game.id, "P2")
|
||||
session.refresh(game)
|
||||
|
||||
# Two players can't start a scene
|
||||
assert not crud.has_min_players(game)
|
||||
ok, msg = crud.confirm_scene_setup(session, game.id)
|
||||
assert not ok
|
||||
assert "at least 3" in msg
|
||||
|
||||
# Dev Mode bypasses the minimum for testing
|
||||
game.dev_mode = True
|
||||
session.add(game)
|
||||
session.commit()
|
||||
assert crud.has_min_players(game)
|
||||
|
||||
# Three players is enough regardless
|
||||
game.dev_mode = False
|
||||
session.add(game)
|
||||
session.commit()
|
||||
crud.add_player(session, game.id, "P3")
|
||||
session.refresh(game)
|
||||
assert crud.has_min_players(game)
|
||||
|
||||
def test_lobby_start_requires_min_players():
|
||||
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)
|
||||
game.dev_mode = False
|
||||
session.add(game)
|
||||
session.commit()
|
||||
crud.add_player(session, game.id, "P1", is_creator=True)
|
||||
crud.add_player(session, game.id, "P2")
|
||||
|
||||
def get_session_override():
|
||||
yield session
|
||||
|
||||
from pirats.database import get_session
|
||||
app.dependency_overrides[get_session] = get_session_override
|
||||
client = TestClient(app)
|
||||
|
||||
try:
|
||||
response = client.post(f"/api/game/{game.id}/lobby/start")
|
||||
assert response.status_code == 400
|
||||
assert "at least 3" in response.json()["error"]
|
||||
session.refresh(game)
|
||||
assert game.phase == "lobby"
|
||||
|
||||
crud.add_player(session, game.id, "P3")
|
||||
response = client.post(f"/api/game/{game.id}/lobby/start")
|
||||
assert response.status_code == 200
|
||||
session.refresh(game)
|
||||
assert game.phase == "character_creation"
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
Reference in New Issue
Block a user