Fixes and updates
This commit is contained in:
@@ -141,13 +141,14 @@ def draw_cards_for_player(db: Session, game: Game, player: Player, count: int) -
|
|||||||
|
|
||||||
# --- CRUD Operations ---
|
# --- CRUD Operations ---
|
||||||
|
|
||||||
def create_game(db: Session) -> Game:
|
def create_game(db: Session, crew_name: Optional[str] = None) -> Game:
|
||||||
deck = cards.get_fresh_deck()
|
deck = cards.get_fresh_deck()
|
||||||
game = Game(
|
game = Game(
|
||||||
deck_cards=json.dumps(deck),
|
deck_cards=json.dumps(deck),
|
||||||
phase="lobby",
|
phase="lobby",
|
||||||
current_scene_number=1,
|
current_scene_number=1,
|
||||||
extra_obstacles=0
|
extra_obstacles=0,
|
||||||
|
crew_name=crew_name
|
||||||
)
|
)
|
||||||
db.add(game)
|
db.add(game)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -371,13 +372,31 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]:
|
|||||||
|
|
||||||
players = game.players
|
players = game.players
|
||||||
|
|
||||||
# 1. Count roles (default any unselected roles to 'pirat')
|
# Default any unselected roles to 'pirat'
|
||||||
for p in players:
|
for p in players:
|
||||||
if p.role is None:
|
if p.role is None:
|
||||||
p.role = "pirat"
|
p.role = "pirat"
|
||||||
db.add(p)
|
db.add(p)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
# 1. Check previous Deep players
|
||||||
|
# "Players who were Deep Players in the previous Scene must play their Pi-Rats in the following Scene."
|
||||||
|
# Note: Only enforce if this is scene 2+, and we have enough players to make a rotation.
|
||||||
|
if game.current_scene_number > 1 and len(players) >= 3:
|
||||||
|
for p in players:
|
||||||
|
if p.previous_role == "deep" and p.role == "deep":
|
||||||
|
return False, f"{p.name} was Deep in the last scene and must play a Pi-Rat in this scene."
|
||||||
|
|
||||||
|
# 2. Check single eligible Deep player
|
||||||
|
# If only one player is eligible to play the Deep, they must play the Deep.
|
||||||
|
if game.current_scene_number > 1:
|
||||||
|
eligible_deeps = [p for p in players if p.previous_role != "deep"]
|
||||||
|
if len(eligible_deeps) == 1:
|
||||||
|
must_be_deep_player = eligible_deeps[0]
|
||||||
|
if must_be_deep_player.role != "deep":
|
||||||
|
return False, f"{must_be_deep_player.name} is the only player eligible to play the Deep and must play the Deep in this scene."
|
||||||
|
|
||||||
|
# 3. Check counts
|
||||||
pirats_count = sum(1 for p in players if p.role == "pirat")
|
pirats_count = sum(1 for p in players if p.role == "pirat")
|
||||||
deeps_count = sum(1 for p in players if p.role == "deep")
|
deeps_count = sum(1 for p in players if p.role == "deep")
|
||||||
|
|
||||||
@@ -385,14 +404,6 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]:
|
|||||||
return False, "You need at least one Pi-Rat player to play."
|
return False, "You need at least one Pi-Rat player to play."
|
||||||
if deeps_count < 1:
|
if deeps_count < 1:
|
||||||
return False, "You need at least one Deep player."
|
return False, "You need at least one Deep player."
|
||||||
|
|
||||||
# 2. Check previous Deep players
|
|
||||||
# "Players who were Deep Players in the previous Scene must play their Pi-Rats in the following Scene."
|
|
||||||
# Note: Only enforce if this is scene 2+, and we have enough players to make a rotation.
|
|
||||||
if game.current_scene_number > 1 and len(players) >= 3:
|
|
||||||
for p in players:
|
|
||||||
if p.previous_role == "deep" and p.role == "deep":
|
|
||||||
return False, f"{p.name} was Deep in the last scene and must play a Pi-Rat in this scene."
|
|
||||||
|
|
||||||
# Everything is valid! Start the scene.
|
# Everything is valid! Start the scene.
|
||||||
# A. Reset obstacles
|
# A. Reset obstacles
|
||||||
@@ -747,45 +758,83 @@ def process_between_scenes_votes(db: Session, game: Game):
|
|||||||
db.delete(v)
|
db.delete(v)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
def redraw_deep_players_hands(db: Session, game: Game):
|
def transition_to_deep_upkeep(db: Session, game_id: str):
|
||||||
"""
|
|
||||||
Step 3 of Between Scenes: Deep players from the previous scene can discard and redraw.
|
|
||||||
To make it simple in the UI, we'll let them click a button to redraw or automatically redraw
|
|
||||||
their hands to their maximum size. Let's empty their hand, shuffle them into the deck,
|
|
||||||
and draw back up to their new max hand size!
|
|
||||||
"""
|
|
||||||
for p in game.players:
|
|
||||||
if p.previous_role == "deep":
|
|
||||||
# Return hand to deck
|
|
||||||
hand = get_player_hand(p)
|
|
||||||
deck = get_game_deck(game)
|
|
||||||
deck.extend(hand)
|
|
||||||
random.shuffle(deck)
|
|
||||||
set_game_deck(game, deck)
|
|
||||||
set_player_hand(p, [])
|
|
||||||
|
|
||||||
# Recalculate max hand size and draw back up
|
|
||||||
max_size = calculate_max_hand_size(p, game.players)
|
|
||||||
draw_cards_for_player(db, game, p, max_size)
|
|
||||||
|
|
||||||
def proceed_to_next_scene_setup(db: Session, game_id: str):
|
|
||||||
game = get_game(db, game_id)
|
game = get_game(db, game_id)
|
||||||
if not game:
|
if not game:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Process votes & redraw hands
|
# Process votes & rank up winner
|
||||||
process_between_scenes_votes(db, game)
|
process_between_scenes_votes(db, game)
|
||||||
redraw_deep_players_hands(db, game)
|
|
||||||
|
|
||||||
# Increment scene number
|
# Reset ready flags
|
||||||
game.current_scene_number += 1
|
|
||||||
game.phase = "scene_setup"
|
|
||||||
|
|
||||||
# Reset role options for everyone
|
|
||||||
for p in game.players:
|
for p in game.players:
|
||||||
p.is_ready = False
|
p.is_ready = False
|
||||||
p.role = None
|
|
||||||
db.add(p)
|
db.add(p)
|
||||||
|
|
||||||
|
# Check if we have resting Deep players (previous_role was 'deep')
|
||||||
|
has_resting_deep = any(p.previous_role == "deep" for p in game.players)
|
||||||
|
|
||||||
|
if has_resting_deep:
|
||||||
|
game.phase = "deep_upkeep"
|
||||||
|
else:
|
||||||
|
# Fallback in case there were no Deep players
|
||||||
|
# Just advance directly to scene_setup
|
||||||
|
game.current_scene_number += 1
|
||||||
|
game.phase = "scene_setup"
|
||||||
|
for p in game.players:
|
||||||
|
p.role = None
|
||||||
|
p.is_ready = False
|
||||||
|
db.add(p)
|
||||||
|
|
||||||
db.add(game)
|
db.add(game)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
def confirm_deep_refresh(db: Session, player_id: str, discard_cards: List[str]):
|
||||||
|
player = get_player(db, player_id)
|
||||||
|
if not player:
|
||||||
|
return
|
||||||
|
game = get_game(db, player.game_id)
|
||||||
|
if not game:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. Discard cards
|
||||||
|
hand = get_player_hand(player)
|
||||||
|
deck = get_game_deck(game)
|
||||||
|
|
||||||
|
for card in discard_cards:
|
||||||
|
if card in hand:
|
||||||
|
hand.remove(card)
|
||||||
|
deck.append(card)
|
||||||
|
|
||||||
|
random.shuffle(deck)
|
||||||
|
set_game_deck(game, deck)
|
||||||
|
set_player_hand(player, hand)
|
||||||
|
db.add(game)
|
||||||
|
db.add(player)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# 2. Draw cards back up to max size
|
||||||
|
# Max size for deep player is Rank + 1
|
||||||
|
max_size = player.rank + 1
|
||||||
|
if len(hand) < max_size:
|
||||||
|
diff = max_size - len(hand)
|
||||||
|
draw_cards_for_player(db, game, player, diff)
|
||||||
|
|
||||||
|
# Mark as ready
|
||||||
|
player.is_ready = True
|
||||||
|
db.add(player)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Check if all resting deep players are ready
|
||||||
|
resting_deep = [p for p in game.players if p.previous_role == "deep"]
|
||||||
|
if all(p.is_ready for p in resting_deep):
|
||||||
|
# All resting deep players have refreshed their hands!
|
||||||
|
# Transition to scene_setup!
|
||||||
|
game.current_scene_number += 1
|
||||||
|
game.phase = "scene_setup"
|
||||||
|
for p in game.players:
|
||||||
|
p.is_ready = False
|
||||||
|
p.role = None
|
||||||
|
db.add(p)
|
||||||
|
db.add(game)
|
||||||
|
db.commit()
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ engine = create_engine(sqlite_url, connect_args={"check_same_thread": False})
|
|||||||
|
|
||||||
def create_db_and_tables():
|
def create_db_and_tables():
|
||||||
SQLModel.metadata.create_all(engine)
|
SQLModel.metadata.create_all(engine)
|
||||||
|
# Ensure crew_name column exists (migration for existing DBs)
|
||||||
|
from sqlalchemy import text
|
||||||
|
with Session(engine) as session:
|
||||||
|
try:
|
||||||
|
session.execute(text("ALTER TABLE game ADD COLUMN crew_name VARCHAR"))
|
||||||
|
session.commit()
|
||||||
|
except Exception:
|
||||||
|
pass # Column already exists or table doesn't exist yet
|
||||||
|
|
||||||
def get_session():
|
def get_session():
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ def home(request: Request):
|
|||||||
return templates.TemplateResponse("index.html", {"request": request})
|
return templates.TemplateResponse("index.html", {"request": request})
|
||||||
|
|
||||||
@app.post("/game/create")
|
@app.post("/game/create")
|
||||||
def create_game_route(db: Session = Depends(get_session)):
|
def create_game_route(
|
||||||
game = crud.create_game(db)
|
crew_name: str = Form(...),
|
||||||
|
db: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
game = crud.create_game(db, crew_name=crew_name.strip())
|
||||||
# Redirect creator to the join page with creator flag
|
# Redirect creator to the join page with creator flag
|
||||||
return RedirectResponse(url=f"/game/{game.id}/join?creator=true", status_code=status.HTTP_303_SEE_OTHER)
|
return RedirectResponse(url=f"/game/{game.id}/join?creator=true", status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
@@ -194,6 +197,13 @@ def get_game_phase_view(
|
|||||||
inbox_tasks.append({"player": p, "type": "like"})
|
inbox_tasks.append({"player": p, "type": "like"})
|
||||||
if p.other_hate_from_player_id == player.id and not p.other_hate:
|
if p.other_hate_from_player_id == player.id and not p.other_hate:
|
||||||
inbox_tasks.append({"player": p, "type": "hate"})
|
inbox_tasks.append({"player": p, "type": "hate"})
|
||||||
|
|
||||||
|
# Freshly fetch votes to avoid cache stale
|
||||||
|
from sqlmodel import select
|
||||||
|
votes = db.exec(select(Vote).where(Vote.game_id == game_id)).all()
|
||||||
|
|
||||||
|
# Calculate eligible deep players
|
||||||
|
eligible_deeps = [p for p in game.players if p.previous_role != "deep"]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"request": request,
|
"request": request,
|
||||||
@@ -210,7 +220,9 @@ def get_game_phase_view(
|
|||||||
"json_loads": json.loads,
|
"json_loads": json.loads,
|
||||||
"deck_count": len(crud.get_game_deck(game)),
|
"deck_count": len(crud.get_game_deck(game)),
|
||||||
"inbox_tasks": inbox_tasks,
|
"inbox_tasks": inbox_tasks,
|
||||||
"edit_mode": edit
|
"votes": votes,
|
||||||
|
"edit_mode": edit,
|
||||||
|
"eligible_deeps": eligible_deeps
|
||||||
}
|
}
|
||||||
|
|
||||||
# Select template based on current game phase
|
# Select template based on current game phase
|
||||||
@@ -224,6 +236,8 @@ def get_game_phase_view(
|
|||||||
return templates.TemplateResponse("scene_partial.html", context)
|
return templates.TemplateResponse("scene_partial.html", context)
|
||||||
elif game.phase == "between_scenes":
|
elif game.phase == "between_scenes":
|
||||||
return templates.TemplateResponse("between_scenes_partial.html", context)
|
return templates.TemplateResponse("between_scenes_partial.html", context)
|
||||||
|
elif game.phase == "deep_upkeep":
|
||||||
|
return templates.TemplateResponse("deep_upkeep_partial.html", context)
|
||||||
|
|
||||||
return HTMLResponse("<p>Unknown game state.</p>")
|
return HTMLResponse("<p>Unknown game state.</p>")
|
||||||
|
|
||||||
@@ -613,7 +627,7 @@ def submit_vote_route(
|
|||||||
db: Session = Depends(get_session)
|
db: Session = Depends(get_session)
|
||||||
):
|
):
|
||||||
crud.submit_rank_vote(db, game_id, player_id, nominated_id)
|
crud.submit_rank_vote(db, game_id, player_id, nominated_id)
|
||||||
return HTMLResponse(status_code=204)
|
return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
# 16. Player Ready for Next Scene
|
# 16. Player Ready for Next Scene
|
||||||
@app.post("/game/{game_id}/player/{player_id}/ready-next")
|
@app.post("/game/{game_id}/player/{player_id}/ready-next")
|
||||||
@@ -626,12 +640,27 @@ def ready_next_route(game_id: str, player_id: str, db: Session = Depends(get_ses
|
|||||||
db.add(player)
|
db.add(player)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Check if all players in the game are ready. If so, transition to next scene!
|
# Check if all players in the game are ready. If so, transition to deep upkeep!
|
||||||
game = crud.get_game(db, game_id)
|
game = crud.get_game(db, game_id)
|
||||||
if game and all(p.is_ready for p in game.players):
|
if game and all(p.is_ready for p in game.players):
|
||||||
crud.proceed_to_next_scene_setup(db, game_id)
|
crud.transition_to_deep_upkeep(db, game_id)
|
||||||
|
|
||||||
return HTMLResponse(status_code=204)
|
return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
# 17. Confirm Hand Refresh for Deep player
|
||||||
|
@app.post("/game/{game_id}/player/{player_id}/confirm-refresh")
|
||||||
|
def confirm_deep_refresh_route(
|
||||||
|
game_id: str,
|
||||||
|
player_id: str,
|
||||||
|
discard_cards: str = Form("[]"),
|
||||||
|
db: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
discards = json.loads(discard_cards)
|
||||||
|
except Exception:
|
||||||
|
discards = []
|
||||||
|
crud.confirm_deep_refresh(db, player_id, discards)
|
||||||
|
return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
import argparse
|
import argparse
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from sqlmodel import Field, SQLModel, Relationship
|
|||||||
class Game(SQLModel, table=True):
|
class Game(SQLModel, table=True):
|
||||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True)
|
id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True)
|
||||||
admin_key: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
admin_key: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
||||||
|
crew_name: Optional[str] = Field(default=None)
|
||||||
phase: str = Field(default="lobby") # "lobby", "character_creation", "swap_techniques", "scene_setup", "scene", "between_scenes"
|
phase: str = Field(default="lobby") # "lobby", "character_creation", "swap_techniques", "scene_setup", "scene", "between_scenes"
|
||||||
current_scene_number: int = Field(default=1)
|
current_scene_number: int = Field(default=1)
|
||||||
extra_obstacles: int = Field(default=0) # Extra obstacles for next scene (from Jokers)
|
extra_obstacles: int = Field(default=0) # Extra obstacles for next scene (from Jokers)
|
||||||
|
|||||||
@@ -1729,3 +1729,46 @@ body {
|
|||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Deep Upkeep Hand Refresh Drag & Drop --- */
|
||||||
|
.upkeep-drag-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.upkeep-drag-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upkeep-box {
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upkeep-box h3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upkeep-flex {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px dashed var(--glass-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
align-content: flex-start;
|
||||||
|
min-height: 300px;
|
||||||
|
transition: var(--transition-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upkeep-box[ondragover]:hover .upkeep-flex {
|
||||||
|
border-color: var(--neon-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Creator Admin Panel - Game #{{ game.id[:8] }}{% endblock %}
|
{% block title %}Creator Admin Panel - {% if game.crew_name %}{{ game.crew_name }}{% else %}Game #{{ game.id[:8] }}{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="admin-panel-container glass-panel">
|
<div class="admin-panel-container glass-panel">
|
||||||
|
|||||||
@@ -8,81 +8,90 @@
|
|||||||
<h3>1. Pirate Ranking (Voting)</h3>
|
<h3>1. Pirate Ranking (Voting)</h3>
|
||||||
<p class="section-desc">Nominate one crewmate who best exemplified pirate qualities in the previous scene. Previous Deep players are ineligible to receive votes.</p>
|
<p class="section-desc">Nominate one crewmate who best exemplified pirate qualities in the previous scene. Previous Deep players are ineligible to receive votes.</p>
|
||||||
|
|
||||||
{% set has_voted = False %}
|
{% set ns_vote = namespace(has_voted=False) %}
|
||||||
{% for v in game.votes %}
|
{% for v in votes %}
|
||||||
{% if v.voter_player_id == player.id %}
|
{% if v.voter_player_id == player.id %}
|
||||||
{% set has_voted = True %}
|
{% set ns_vote.has_voted = True %}
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if has_voted %}
|
|
||||||
<div class="voted-confirmation-box glass-panel">
|
|
||||||
<p class="success-text">✔️ Vote submitted! Waiting for other players to submit their nominations.</p>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<form hx-post="/game/{{ game.id }}/player/{{ player.id }}/submit-vote" hx-swap="none" class="vote-form inline-form">
|
|
||||||
<div class="form-group inline-group">
|
|
||||||
<select name="nominated_id" class="select-field" required>
|
|
||||||
<option value="" disabled selected>Nominate crewmate...</option>
|
|
||||||
{% for other in game.players if other.id != player.id and other.previous_role != 'deep' %}
|
|
||||||
<option value="{{ other.id }}">{{ other.name }} (Rank {{ other.rank }})</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="btn btn-primary">Cast Nomination</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
<!-- Voting Roster Status -->
|
|
||||||
<div class="vote-status-table margin-top">
|
|
||||||
<h4>Nomination Progress:</h4>
|
|
||||||
<div class="player-chips list-chips" id="voting-roster"
|
|
||||||
hx-get="/game/{{ game.id }}/between/roster"
|
|
||||||
hx-trigger="every 2s"
|
|
||||||
hx-swap="innerHTML">
|
|
||||||
{% for p in game.players %}
|
|
||||||
{% set voted = False %}
|
|
||||||
{% for v in game.votes if v.voter_player_id == p.id %}
|
|
||||||
{% set voted = True %}
|
|
||||||
{% endfor %}
|
|
||||||
<div class="player-chip {% if voted %}voted-chip{% else %}not-voted-chip{% endif %}">
|
|
||||||
<span class="name">{{ p.name }}</span>
|
|
||||||
<span class="status-badge">{% if voted %}Voted{% else %}Voting...{% endif %}</span>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resting Deep Player Card -->
|
{% if ns_vote.has_voted %}
|
||||||
<div class="card glass-panel deep-rest-card">
|
<div class="voted-confirmation-box glass-panel">
|
||||||
<h3>2. Resting Deep Upkeep</h3>
|
<p class="success-text">✔️ Vote submitted! Waiting for other players to submit their nominations.</p>
|
||||||
|
</div>
|
||||||
{% if player.previous_role == "deep" %}
|
{% else %}
|
||||||
<div class="deep-rest-panel glass-panel">
|
<form hx-post="/game/{{ game.id }}/player/{{ player.id }}/submit-vote" hx-target="#game-view" hx-swap="innerHTML" class="vote-form inline-form">
|
||||||
<p class="info-text">You controlled the Deep in the last scene and your Pi-Rat was resting. You will discard your current hand and redraw back up to your maximum hand size (Rank {{ player.rank }} maximum: {{ max_hand_size }} cards).</p>
|
<div class="form-group inline-group">
|
||||||
<p class="footnote-desc">This is handled automatically when transitioning, but you must click 'Ready' below to proceed.</p>
|
<select name="nominated_id" class="select-field" required>
|
||||||
|
<option value="" disabled selected>Nominate crewmate...</option>
|
||||||
|
{% for other in game.players if other.id != player.id and other.previous_role != 'deep' %}
|
||||||
|
<option value="{{ other.id }}">{{ other.name }} (Rank {{ other.rank }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-primary">Cast Nomination</button>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</form>
|
||||||
<p class="info-text text-center">Your Pi-Rat was active. Your hand cards carry over to the next scene.</p>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
<!-- Voting Roster Status -->
|
||||||
<div class="roster-sheet-summary margin-top text-left glass-panel">
|
<div class="vote-status-table margin-top">
|
||||||
<h4>Crew Rank Ledger:</h4>
|
<h4>Nomination Progress:</h4>
|
||||||
<ul class="ranks-ledger">
|
<div class="player-chips list-chips" id="voting-roster"
|
||||||
{% for p in game.players %}
|
hx-get="/game/{{ game.id }}/between/roster"
|
||||||
<li>
|
hx-trigger="every 2s"
|
||||||
<strong>{{ p.name }}:</strong> Rank {{ p.rank }}
|
hx-swap="innerHTML">
|
||||||
{% if p.previous_role == 'deep' %}<span class="deep-badge">Deep</span>{% else %}<span class="pirat-badge">Pi-Rat</span>{% endif %}
|
{% for p in game.players %}
|
||||||
</li>
|
{% set voted = False %}
|
||||||
|
{% for v in votes if v.voter_player_id == p.id %}
|
||||||
|
{% set voted = True %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
<div class="player-chip {% if voted %}voted-chip{% else %}not-voted-chip{% endif %}">
|
||||||
|
<span class="name">{{ p.name }}</span>
|
||||||
|
<span class="status-badge">{% if voted %}Voted{% else %}Voting...{% endif %}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Next Scene Ready Up -->
|
<!-- Resting Deep Player Card -->
|
||||||
<div class="action-box margin-top">
|
<div class="card glass-panel deep-rest-card">
|
||||||
|
<h3>2. Resting Deep Upkeep</h3>
|
||||||
|
|
||||||
|
{% if player.previous_role == "deep" %}
|
||||||
|
<div class="deep-rest-panel glass-panel">
|
||||||
|
<p class="info-text">You controlled the Deep in the last scene and your Pi-Rat was resting. You will discard your current hand and redraw back up to your maximum hand size (Rank {{ player.rank }} maximum: {{ max_hand_size }} cards).</p>
|
||||||
|
<p class="footnote-desc">This is handled automatically when transitioning, but you must click 'Ready' below to proceed.</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="info-text text-center">Your Pi-Rat was active. Your hand cards carry over to the next scene.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="roster-sheet-summary margin-top text-left glass-panel">
|
||||||
|
<h4>Crew Rank Ledger:</h4>
|
||||||
|
<ul class="ranks-ledger">
|
||||||
|
{% for p in game.players %}
|
||||||
|
<li>
|
||||||
|
<strong>{{ p.name }}:</strong> Rank {{ p.rank }}
|
||||||
|
{% if p.previous_role == 'deep' %}<span class="deep-badge">Deep</span>{% else %}<span class="pirat-badge">Pi-Rat</span>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Next Scene Ready Up -->
|
||||||
|
<div class="action-box margin-top">
|
||||||
|
{% set eligible_count = namespace(value=0) %}
|
||||||
|
{% for other in game.players if other.id != player.id and other.previous_role != 'deep' %}
|
||||||
|
{% set eligible_count.value = eligible_count.value + 1 %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if eligible_count.value > 0 and not ns_vote.has_voted %}
|
||||||
|
<p class="info-text gold-text" style="margin-bottom:0.5rem;">⚠️ You must nominate a crewmate above before you can ready up.</p>
|
||||||
|
<button class="btn btn-primary btn-large" disabled>Ready for Next Scene</button>
|
||||||
|
{% else %}
|
||||||
{% if player.is_ready %}
|
{% if player.is_ready %}
|
||||||
<div class="waiting-box">
|
<div class="waiting-box">
|
||||||
<p>Ready! Waiting for other players to ready up...</p>
|
<p>Ready! Waiting for other players to ready up...</p>
|
||||||
@@ -90,12 +99,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button hx-post="/game/{{ game.id }}/player/{{ player.id }}/ready-next"
|
<button hx-post="/game/{{ game.id }}/player/{{ player.id }}/ready-next"
|
||||||
hx-swap="none"
|
hx-target="#game-view"
|
||||||
|
hx-swap="innerHTML"
|
||||||
class="btn btn-primary btn-large glow-effect">
|
class="btn btn-primary btn-large glow-effect">
|
||||||
Ready for Next Scene
|
Ready for Next Scene
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hidden phase-check trigger -->
|
<!-- Hidden phase-check trigger -->
|
||||||
@@ -103,3 +114,5 @@
|
|||||||
hx-trigger="every 2s"
|
hx-trigger="every 2s"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
style="display:none;"></div>
|
style="display:none;"></div>
|
||||||
|
|
||||||
|
{% include "header_status_snippet.html" %}
|
||||||
|
|||||||
@@ -118,3 +118,5 @@
|
|||||||
hx-trigger="every 2s"
|
hx-trigger="every 2s"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
style="display:none;"></div>
|
style="display:none;"></div>
|
||||||
|
|
||||||
|
{% include "header_status_snippet.html" %}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Game #{{ game.id[:8] }} - {% if player %}{{ player.name }}{% else %}Lobby{% endif %}{% endblock %}
|
{% block title %}{% if game.crew_name %}{{ game.crew_name }}{% else %}Game #{{ game.id[:8] }}{% endif %} - {% if player %}{{ player.name }}{% else %}Lobby{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block header_extra %}
|
{% block header_extra %}
|
||||||
<div class="header-status">
|
<div id="header-status" class="header-status">
|
||||||
{% if player %}
|
{% if player %}
|
||||||
<span class="player-pill"><span class="bullet"></span> {{ player.name }} (Rank {{ player.rank }})</span>
|
<span class="player-pill"><span class="bullet"></span> {{ player.name }} (Rank {{ player.rank }})</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
121
src/pirats/templates/deep_upkeep_partial.html
Normal file
121
src/pirats/templates/deep_upkeep_partial.html
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
{% if player.previous_role == "deep" %}
|
||||||
|
<div class="deep-upkeep-view text-center">
|
||||||
|
<h2>Resting Deep Upkeep</h2>
|
||||||
|
<p class="description">Select which cards you want to keep and which you want to discard. Drag cards between the boxes, then click "Confirm Hand Refresh". You will draw back up to your maximum hand size of {{ player.rank + 1 }} cards.</p>
|
||||||
|
|
||||||
|
<div class="upkeep-drag-container">
|
||||||
|
<!-- Keep Box -->
|
||||||
|
<div class="card glass-panel upkeep-box" id="keep-box" ondragover="allowDrop(event)" ondrop="dropToKeep(event)">
|
||||||
|
<h3>Keep (Current Hand)</h3>
|
||||||
|
<div class="hand-flex upkeep-flex" id="keep-list">
|
||||||
|
{% for card in hand %}
|
||||||
|
{% set parsed = parse_card(card) %}
|
||||||
|
<div class="card-large suit-{{ parsed.suit.lower() }} {% if parsed.is_joker %}joker-card{% endif %}"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart="drag(event)"
|
||||||
|
id="card-{{ card }}"
|
||||||
|
data-card-code="{{ card }}">
|
||||||
|
<div class="card-corner top-left">
|
||||||
|
<span class="val">{{ parsed.value }}</span>
|
||||||
|
<span class="suit">{{ parsed.symbol }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-center">
|
||||||
|
{% if parsed.is_joker %}
|
||||||
|
🃏
|
||||||
|
{% else %}
|
||||||
|
<span class="giant-symbol">{{ parsed.symbol }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-corner bottom-right">
|
||||||
|
<span class="val">{{ parsed.value }}</span>
|
||||||
|
<span class="suit">{{ parsed.symbol }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Discard Box -->
|
||||||
|
<div class="card glass-panel upkeep-box" id="discard-box" ondragover="allowDrop(event)" ondrop="dropToDiscard(event)">
|
||||||
|
<h3 style="color: var(--red-suit);">Discard</h3>
|
||||||
|
<div class="hand-flex upkeep-flex" id="discard-list">
|
||||||
|
<!-- Drop discarded cards here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-box margin-top">
|
||||||
|
{% if player.is_ready %}
|
||||||
|
<div class="waiting-box" style="justify-content: center;">
|
||||||
|
<p>Upkeep confirmed! Waiting for other resting Deep players...</p>
|
||||||
|
<div class="spinner-small"></div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<form id="confirm-refresh-form" hx-post="/game/{{ game.id }}/player/{{ player.id }}/confirm-refresh" hx-target="#game-view" hx-swap="innerHTML">
|
||||||
|
<!-- Hidden input to submit which cards are discarded -->
|
||||||
|
<input type="hidden" name="discard_cards" id="discard-input" value="[]">
|
||||||
|
<button type="submit" class="btn btn-primary btn-large glow-effect">Confirm Hand Refresh</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function allowDrop(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drag(ev) {
|
||||||
|
ev.dataTransfer.setData("text/plain", ev.currentTarget.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropToKeep(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var data = ev.dataTransfer.getData("text/plain");
|
||||||
|
var cardEl = document.getElementById(data);
|
||||||
|
if (cardEl) {
|
||||||
|
document.getElementById("keep-list").appendChild(cardEl);
|
||||||
|
updateInputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropToDiscard(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
var data = ev.dataTransfer.getData("text/plain");
|
||||||
|
var cardEl = document.getElementById(data);
|
||||||
|
if (cardEl) {
|
||||||
|
document.getElementById("discard-list").appendChild(cardEl);
|
||||||
|
updateInputs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInputs() {
|
||||||
|
var discardList = document.getElementById("discard-list");
|
||||||
|
var discards = [];
|
||||||
|
for (var i = 0; i < discardList.children.length; i++) {
|
||||||
|
var code = discardList.children[i].getAttribute("data-card-code");
|
||||||
|
if (code) {
|
||||||
|
discards.push(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById("discard-input").value = JSON.stringify(discards);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<div class="deep-upkeep-waiting text-center glass-panel" style="max-width:600px; margin:4rem auto; padding:3rem;">
|
||||||
|
<h2>🌊 Resting Deep Upkeep</h2>
|
||||||
|
<p class="description">The players who controlled the Deep in the previous scene are currently refreshing their hands.</p>
|
||||||
|
<div class="waiting-box" style="justify-content: center; margin-top:2rem;">
|
||||||
|
<p>Waiting for resting Deep players to confirm their hand refreshes...</p>
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Hidden phase-check trigger -->
|
||||||
|
<div hx-get="/game/{{ game.id }}/player/{{ player.id }}/phase-check?current={{ game.phase }}"
|
||||||
|
hx-trigger="every 2s"
|
||||||
|
hx-swap="none"
|
||||||
|
style="display:none;"></div>
|
||||||
|
|
||||||
|
{% include "header_status_snippet.html" %}
|
||||||
6
src/pirats/templates/header_status_snippet.html
Normal file
6
src/pirats/templates/header_status_snippet.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<div id="header-status" hx-swap-oob="true" class="header-status">
|
||||||
|
{% if player %}
|
||||||
|
<span class="player-pill"><span class="bullet"></span> {{ player.name }} (Rank {{ player.rank }})</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="phase-pill">{{ game.phase.replace('_', ' ').title() }}</span>
|
||||||
|
</div>
|
||||||
@@ -12,8 +12,11 @@
|
|||||||
<div class="welcome-actions">
|
<div class="welcome-actions">
|
||||||
<div class="action-card glass-panel text-center">
|
<div class="action-card glass-panel text-center">
|
||||||
<h3>Start a New Adventure</h3>
|
<h3>Start a New Adventure</h3>
|
||||||
<p>Generate a fresh game deck and take command as the crew creator.</p>
|
<p>Generate a fresh game deck and name your crew of mathematical pirates.</p>
|
||||||
<form action="/game/create" method="POST">
|
<form action="/game/create" method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" name="crew_name" placeholder="Name your Crew (e.g. The Math-Gat Rats)" required class="input-field text-center">
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-large glow-effect">Create Game</button>
|
<button type="submit" class="btn btn-primary btn-large glow-effect">Create Game</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="join-container glass-panel text-center">
|
<div class="join-container glass-panel text-center">
|
||||||
<h2>Join the Pi-Rat Crew!</h2>
|
<h2>Join the {% if game.crew_name %}{{ game.crew_name }}{% else %}Pi-Rat{% endif %} Crew!</h2>
|
||||||
<p>The mathematical transformation spell is about to cast. Enter your name below to stow away in the cargo hold.</p>
|
<p>The mathematical transformation spell is about to cast. Enter your name below to stow away in the cargo hold.</p>
|
||||||
|
|
||||||
<form action="/game/{{ game.id }}/join" method="POST" class="join-form">
|
<form action="/game/{{ game.id }}/join" method="POST" class="join-form">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="lobby-view text-center">
|
<div class="lobby-view text-center">
|
||||||
<h2>Ship's Hold (Lobby)</h2>
|
<h2>{% if game.crew_name %}{{ game.crew_name }}{% else %}Ship's Hold{% endif %} (Lobby)</h2>
|
||||||
<p class="description">Wait here as the mathematical mages gather stowaway rats for the spell. Once everyone is in, the captain will initiate the transformation.</p>
|
<p class="description">Wait here as the mathematical mages gather stowaway rats for the spell. Once everyone is in, the captain will initiate the transformation.</p>
|
||||||
|
|
||||||
<div class="lobby-status-box glass-panel">
|
<div class="lobby-status-box glass-panel">
|
||||||
@@ -64,3 +64,5 @@
|
|||||||
hx-trigger="every 2s"
|
hx-trigger="every 2s"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
style="display:none;"></div>
|
style="display:none;"></div>
|
||||||
|
|
||||||
|
{% include "header_status_snippet.html" %}
|
||||||
|
|||||||
@@ -294,6 +294,8 @@
|
|||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
style="display:none;"></div>
|
style="display:none;"></div>
|
||||||
|
|
||||||
|
{% include "header_status_snippet.html" %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.dragStart = function(ev) {
|
window.dragStart = function(ev) {
|
||||||
ev.dataTransfer.setData("text/plain", ev.currentTarget.getAttribute("data-card-code"));
|
ev.dataTransfer.setData("text/plain", ev.currentTarget.getAttribute("data-card-code"));
|
||||||
|
|||||||
@@ -25,3 +25,17 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="roster-section margin-top">
|
||||||
|
<h4>⏳ Yet to Choose</h4>
|
||||||
|
<div class="player-chips list-chips">
|
||||||
|
{% for p in game.players if not p.role %}
|
||||||
|
<div class="player-chip undecided-chip">
|
||||||
|
<span class="name">{{ p.name }}</span>
|
||||||
|
<span class="sub-label">Rank {{ p.rank }}</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty-text">Everyone has chosen!</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
{% if player.previous_role == "deep" %}
|
{% if player.previous_role == "deep" %}
|
||||||
{% set force_pirat = True %}
|
{% set force_pirat = True %}
|
||||||
{% set locked_role = "pirat" %}
|
{% set locked_role = "pirat" %}
|
||||||
|
{% elif eligible_deeps|length == 1 %}
|
||||||
|
{% set locked_role = "deep" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -30,8 +32,10 @@
|
|||||||
<p class="info-text">
|
<p class="info-text">
|
||||||
{% if game.current_scene_number == 1 %}
|
{% if game.current_scene_number == 1 %}
|
||||||
Based on starting Ranks, you are locked into:
|
Based on starting Ranks, you are locked into:
|
||||||
{% else %}
|
{% elif player.previous_role == "deep" %}
|
||||||
You were Deep in the previous scene! You are locked into:
|
You were Deep in the previous scene! You are locked into:
|
||||||
|
{% else %}
|
||||||
|
You are the only player eligible to play the Deep! You are locked into:
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<div class="role-badge large-badge {{ locked_role }}">
|
<div class="role-badge large-badge {{ locked_role }}">
|
||||||
@@ -66,33 +70,7 @@
|
|||||||
hx-get="/game/{{ game.id }}/scene/roster"
|
hx-get="/game/{{ game.id }}/scene/roster"
|
||||||
hx-trigger="every 2s"
|
hx-trigger="every 2s"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
<div class="roster-section">
|
{% include "scene_roster_snippet.html" %}
|
||||||
<h4>🌊 The Deep (NPCs & Hazards)</h4>
|
|
||||||
<div class="player-chips list-chips">
|
|
||||||
{% for p in game.players if p.role == 'deep' %}
|
|
||||||
<div class="player-chip deep-chip">
|
|
||||||
<span class="name">{{ p.name }}</span>
|
|
||||||
<span class="sub-label">Rank {{ p.rank }}</span>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="empty-text">None selected</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="roster-section margin-top">
|
|
||||||
<h4>🐀 Pi-Rats (Crew Players)</h4>
|
|
||||||
<div class="player-chips list-chips">
|
|
||||||
{% for p in game.players if p.role == 'pirat' %}
|
|
||||||
<div class="player-chip pirat-chip">
|
|
||||||
<span class="name">{{ p.name }}</span>
|
|
||||||
<span class="sub-label">Rank {{ p.rank }}{% if is_captain(p, game.players) %} (Captain) {% endif %}</span>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p class="empty-text">None selected</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,3 +95,5 @@
|
|||||||
hx-trigger="every 2s"
|
hx-trigger="every 2s"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
style="display:none;"></div>
|
style="display:none;"></div>
|
||||||
|
|
||||||
|
{% include "header_status_snippet.html" %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% for p in game.players %}
|
{% for p in game.players %}
|
||||||
{% set voted = False %}
|
{% set voted = False %}
|
||||||
{% for v in game.votes if v.voter_player_id == p.id %}
|
{% for v in votes if v.voter_player_id == p.id %}
|
||||||
{% set voted = True %}
|
{% set voted = True %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="player-chip {% if voted %}voted-chip{% else %}not-voted-chip{% endif %}">
|
<div class="player-chip {% if voted %}voted-chip{% else %}not-voted-chip{% endif %}">
|
||||||
|
|||||||
@@ -318,3 +318,190 @@ def test_set_role_endpoint():
|
|||||||
assert player.role == "pirat"
|
assert player.role == "pirat"
|
||||||
finally:
|
finally:
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
def test_game_creation_with_crew_name(session):
|
||||||
|
game = crud.create_game(session, crew_name="The Black Gats")
|
||||||
|
assert game.id is not None
|
||||||
|
assert game.crew_name == "The Black Gats"
|
||||||
|
assert game.phase == "lobby"
|
||||||
|
|
||||||
|
def test_create_game_endpoint():
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy.pool import StaticPool
|
||||||
|
from sqlmodel import SQLModel, create_engine, Session, select
|
||||||
|
from pirats.main import app
|
||||||
|
from pirats import crud
|
||||||
|
|
||||||
|
# Setup in-memory DB
|
||||||
|
engine = create_engine(
|
||||||
|
"sqlite://",
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
poolclass=StaticPool,
|
||||||
|
)
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
session = Session(engine)
|
||||||
|
|
||||||
|
def get_session_override():
|
||||||
|
yield session
|
||||||
|
|
||||||
|
# Dynamically find the get_session function object used by the route
|
||||||
|
get_session_func = None
|
||||||
|
for route in app.routes:
|
||||||
|
if route.path == "/game/create":
|
||||||
|
for dep in route.dependant.dependencies:
|
||||||
|
if dep.name == "db":
|
||||||
|
get_session_func = dep.call
|
||||||
|
break
|
||||||
|
if get_session_func:
|
||||||
|
break
|
||||||
|
|
||||||
|
assert get_session_func is not None, "Could not find get_session dependency in route"
|
||||||
|
app.dependency_overrides[get_session_func] = get_session_override
|
||||||
|
client = TestClient(app, base_url="http://testserver")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.post("/game/create", data={"crew_name": "The Math Mutineers"})
|
||||||
|
assert response.status_code in [200, 303] # starlette redirects can be 303
|
||||||
|
|
||||||
|
# Verify the database has the game with the crew name
|
||||||
|
from pirats.models import Game
|
||||||
|
games = session.exec(select(Game)).all()
|
||||||
|
assert len(games) == 1
|
||||||
|
assert games[0].crew_name == "The Math Mutineers"
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
def test_transition_to_deep_upkeep(session):
|
||||||
|
game = crud.create_game(session)
|
||||||
|
p1 = crud.add_player(session, game.id, "P1")
|
||||||
|
p2 = crud.add_player(session, game.id, "P2")
|
||||||
|
|
||||||
|
p1.previous_role = "deep"
|
||||||
|
p2.previous_role = "pirat"
|
||||||
|
session.add_all([p1, p2])
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
crud.transition_to_deep_upkeep(session, game.id)
|
||||||
|
session.refresh(game)
|
||||||
|
assert game.phase == "deep_upkeep"
|
||||||
|
|
||||||
|
def test_confirm_deep_refresh(session):
|
||||||
|
game = crud.create_game(session)
|
||||||
|
p1 = crud.add_player(session, game.id, "P1")
|
||||||
|
p1.previous_role = "deep"
|
||||||
|
p1.role = "deep"
|
||||||
|
p1.rank = 2
|
||||||
|
# Hand has 3 cards: ['2C', '3C', '4C']
|
||||||
|
p1.hand_cards = json.dumps(["2C", "3C", "4C"])
|
||||||
|
|
||||||
|
# Rest of deck has some known cards
|
||||||
|
game.deck_cards = json.dumps(["5H", "6H", "7H", "8H"])
|
||||||
|
session.add_all([game, p1])
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Stub shuffle to keep card order deterministic
|
||||||
|
import random
|
||||||
|
orig_shuffle = random.shuffle
|
||||||
|
random.shuffle = lambda x: None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Discard 2C and 3C
|
||||||
|
crud.confirm_deep_refresh(session, p1.id, ["2C", "3C"])
|
||||||
|
finally:
|
||||||
|
random.shuffle = orig_shuffle
|
||||||
|
|
||||||
|
session.refresh(p1)
|
||||||
|
session.refresh(game)
|
||||||
|
|
||||||
|
# Hand should not contain 2C and 3C, and should have drawn 5H and 6H
|
||||||
|
hand = json.loads(p1.hand_cards)
|
||||||
|
assert "2C" not in hand
|
||||||
|
assert "3C" not in hand
|
||||||
|
assert "4C" in hand
|
||||||
|
assert "5H" in hand
|
||||||
|
assert "6H" in hand
|
||||||
|
assert len(hand) == 3
|
||||||
|
|
||||||
|
# Discarded cards should be at the end of the deck
|
||||||
|
deck = json.loads(game.deck_cards)
|
||||||
|
assert "2C" in deck
|
||||||
|
assert "3C" in deck
|
||||||
|
assert deck == ["7H", "8H", "2C", "3C"]
|
||||||
|
|
||||||
|
# Since P1 was the only resting deep player and is now ready, phase should have advanced to scene_setup!
|
||||||
|
assert game.phase == "scene_setup"
|
||||||
|
|
||||||
|
def test_submit_vote_endpoint():
|
||||||
|
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
|
||||||
|
|
||||||
|
# Setup in-memory DB
|
||||||
|
engine = create_engine(
|
||||||
|
"sqlite://",
|
||||||
|
connect_args={"check_same_thread": False},
|
||||||
|
poolclass=StaticPool,
|
||||||
|
)
|
||||||
|
SQLModel.metadata.create_all(engine)
|
||||||
|
session = Session(engine)
|
||||||
|
|
||||||
|
# Create game and players
|
||||||
|
game = crud.create_game(session)
|
||||||
|
p1 = crud.add_player(session, game.id, "P1")
|
||||||
|
p2 = crud.add_player(session, game.id, "P2")
|
||||||
|
game.phase = "between_scenes"
|
||||||
|
session.add(game)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
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"/game/{game.id}/player/{p1.id}/submit-vote", data={"nominated_id": p2.id})
|
||||||
|
# TestClient follows redirects by default
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Vote submitted" in response.text
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
def test_single_eligible_deep_player(session):
|
||||||
|
game = crud.create_game(session)
|
||||||
|
p1 = crud.add_player(session, game.id, "P1")
|
||||||
|
p2 = crud.add_player(session, game.id, "P2")
|
||||||
|
p3 = crud.add_player(session, game.id, "P3")
|
||||||
|
|
||||||
|
# Scene number > 1
|
||||||
|
game.current_scene_number = 2
|
||||||
|
|
||||||
|
# 2 players were Deep in the last scene, 1 was Pi-Rat
|
||||||
|
p1.previous_role = "deep"
|
||||||
|
p2.previous_role = "deep"
|
||||||
|
p3.previous_role = "pirat"
|
||||||
|
|
||||||
|
# Set their current roles to something invalid first
|
||||||
|
# P3 is the only one eligible to be Deep, but plays Pi-Rat. This should fail!
|
||||||
|
p1.role = "pirat"
|
||||||
|
p2.role = "pirat"
|
||||||
|
p3.role = "pirat"
|
||||||
|
|
||||||
|
session.add_all([game, p1, p2, p3])
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
success, msg = crud.confirm_scene_setup(session, game.id)
|
||||||
|
assert not success
|
||||||
|
assert "is the only player eligible to play the Deep" in msg
|
||||||
|
|
||||||
|
# Now set P3 to play Deep. This should succeed!
|
||||||
|
p3.role = "deep"
|
||||||
|
session.add(p3)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
success, msg = crud.confirm_scene_setup(session, game.id)
|
||||||
|
assert success
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user