Files
pirats/src/pirats/crud_character.py
2026-06-11 15:57:08 -07:00

351 lines
13 KiB
Python

import json
import random
from sqlmodel import Session
from .models import Game, Player
from .crud_base import get_player, get_game, add_game_event
# --- Character Creation Operations ---
def auto_delegate_all(db: Session, game: Game):
"""
Assigns the Like/Hate delegated questions for every player at once.
Uses a shuffled cycle so each player answers exactly one Like and one Hate,
never their own, and (with 3+ players) from two different crewmates.
"""
players = list(game.players)
if len(players) < 2:
return
random.shuffle(players)
n = len(players)
for i, p in enumerate(players):
if not p.other_like_from_player_id:
p.other_like_from_player_id = players[(i + 1) % n].id
p.other_like = ""
if not p.other_hate_from_player_id:
p.other_hate_from_player_id = players[(i - 1) % n].id
p.other_hate = ""
db.add(p)
db.commit()
def delegate_question(db: Session, player_id: str, question_type: str, target_player_id: str):
player = get_player(db, player_id)
if not player:
return
if question_type == "like":
player.other_like_from_player_id = target_player_id
player.other_like = "" # Clear old answer
elif question_type == "hate":
player.other_hate_from_player_id = target_player_id
player.other_hate = "" # Clear old answer
db.add(player)
db.commit()
def submit_delegated_answer(db: Session, target_player_id: str, question_type: str, answer: str, from_player_id: str):
player = get_player(db, target_player_id)
if not player:
return
if question_type == "like" and player.other_like_from_player_id == from_player_id:
player.other_like = answer
elif question_type == "hate" and player.other_hate_from_player_id == from_player_id:
player.other_hate = answer
db.add(player)
db.commit()
def submit_techniques(db: Session, player_id: str, tech1: str, tech2: str, tech3: str):
"""
Saves a player's 3 techniques. Returns an error string if any of them collide
(case-insensitively) with each other or with techniques other players already
submitted, so the player can fix it immediately instead of the whole pool
being reset after everyone submits.
"""
player = get_player(db, player_id)
if not player:
return None
techs = [tech1, tech2, tech3]
cleaned = [t.strip().lower() for t in techs]
if len(set(cleaned)) < 3:
return "Your 3 techniques must all be different."
game = get_game(db, player.game_id)
if game:
taken = set()
for p in game.players:
if p.id == player.id:
continue
for t in json.loads(p.created_techniques):
taken.add(t.strip().lower())
clashes = [techs[i] for i, c in enumerate(cleaned) if c in taken]
if clashes:
return f"Already taken by a crewmate: {', '.join(clashes)}. Pick something more original!"
player.created_techniques = json.dumps(techs)
db.add(player)
db.commit()
# Check if all players have submitted techniques. If so, trigger the swap!
if not game:
return None
all_submitted = True
for p in game.players:
techs = json.loads(p.created_techniques)
if len(techs) < 3 or not all(techs):
all_submitted = False
break
if all_submitted:
trigger_technique_swap(db, game)
return None
def trigger_technique_swap(db: Session, game: Game):
"""
Shuffles and distributes 3 techniques created by OTHER players to each player.
"""
players = game.players
if not players:
return
# Build a pool of all techniques with their creators
pool = []
seen_texts = set()
has_duplicates = False
for p in players:
techs = json.loads(p.created_techniques)
for t in techs:
t_clean = t.strip().lower()
if t_clean in seen_texts:
has_duplicates = True
seen_texts.add(t_clean)
pool.append({"text": t, "creator_id": p.id})
if has_duplicates:
# Force players to resubmit
for p in players:
p.created_techniques = "[]"
db.add(p)
db.commit()
add_game_event(db, game.id, "Duplicate techniques submitted! The pool has been cleared. All players must try again with unique technique names.")
return
# Swap algorithm with simple retry backtracking
# We want to assign 3 techniques from the pool to each player, ensuring creator_id != player.id.
# For a small number of players, random shuffling with retries is highly efficient.
success = False
for attempt in range(200):
random.shuffle(pool)
assignments = {p.id: [] for p in players}
temp_pool = pool.copy()
# Greedy assignment
failed = False
for p in players:
valid_for_p = [t for t in temp_pool if t["creator_id"] != p.id]
if len(valid_for_p) < 3:
# If only 1-2 players are playing, it might be impossible to assign 3 non-owned techniques.
# In that case, let's allow assigning their own if there are no other options (e.g. 1 player).
if len(players) < 3:
valid_for_p = temp_pool.copy()
else:
failed = True
break
# Take 3
chosen = valid_for_p[:3]
for c in chosen:
assignments[p.id].append(c["text"])
temp_pool.remove(c)
if not failed:
# Successfully matched! Save assignments.
for p in players:
p.swapped_techniques = json.dumps(assignments[p.id])
p.is_ready = False # Reset ready flag for J/Q/K assignment step
db.add(p)
game.phase = "swap_techniques"
db.add(game)
db.commit()
add_game_event(db, game.id, "Phase changed: Swap Techniques")
success = True
break
if not success:
# Fallback in case of failure (e.g., edge cases, single player testing): just distribute whatever
for p in players:
# Just give them the first 3 from the pool
p.swapped_techniques = json.dumps([t["text"] for t in pool[:3]])
p.is_ready = False
db.add(p)
game.phase = "swap_techniques"
db.add(game)
db.commit()
add_game_event(db, game.id, "Phase changed: Swap Techniques")
def assign_face_card_techniques(db: Session, player_id: str, jack: str, queen: str, king: str):
player = get_player(db, player_id)
if not player:
return
player.tech_jack = jack
player.tech_queen = queen
player.tech_king = king
player.is_ready = True
db.add(player)
db.commit()
# Check if all players are ready. If so, transition to scene_setup and assign starting ranks!
game = get_game(db, player.game_id)
if not game:
return
if all(p.is_ready for p in game.players):
# 1. Randomly assign starting ranks
players_list = list(game.players)
random.shuffle(players_list)
# "Randomly choose one Player to start the game at Rank 3. This Player must play the Deep during the first scene."
# "Randomly choose one Player to start the game at Rank 1. This Player must play their Pi-Rat during the first scene."
# "All remaining Players start at Rank 2. These Players may choose to play either during the first scene."
if len(players_list) >= 2:
players_list[0].rank = 3
players_list[0].role = "deep"
players_list[0].previous_role = "deep"
players_list[1].rank = 1
players_list[1].role = "pirat"
players_list[1].previous_role = "pirat"
for p in players_list[2:]:
p.rank = 2
p.role = None
p.previous_role = None
else:
# Edge case for 1 player testing
players_list[0].rank = 3
players_list[0].role = "pirat"
players_list[0].previous_role = "pirat"
# Reset ready status for scene setup
for p in game.players:
p.is_ready = False
db.add(p)
game.phase = "scene_setup"
db.add(game)
db.commit()
add_game_event(db, game.id, "Phase changed: Scene Setup")
def roll_new_character(
db: Session,
game_id: str,
player_id: str,
avatar_look: str,
avatar_smell: str,
first_words: str,
good_at_math: str
):
player = get_player(db, player_id)
game = get_game(db, game_id)
if not player or not game:
return
# 1. Return current hand cards to the deck
from .crud_base import get_player_hand, set_player_hand, get_game_deck, set_game_deck, draw_cards_for_player, calculate_max_hand_size
hand = get_player_hand(player)
deck = get_game_deck(game)
deck.extend(hand)
random.shuffle(deck)
set_game_deck(game, deck)
set_player_hand(player, [])
# 2. Reset properties. New recruits always start at Rank 1.
player.rank = 1
player.is_ready = False
player.completed_personal_1 = False
player.completed_personal_2 = False
player.completed_personal_3 = False
player.needs_name = False
player.needs_rank_3_bonus = False
player.is_dead = False
player.is_ghost = False
player.tax_banned = False
# A dead Captain's position does not pass to their replacement recruit
if game.captain_player_id == player.id:
game.captain_player_id = None
# Save player-provided details
player.avatar_look = avatar_look.strip()
player.avatar_smell = avatar_smell.strip()
player.first_words = first_words.strip()
player.good_at_math = good_at_math.strip()
# Scent/Name is "Recruit [Smell]" — strip lead-ins like "smells like" or
# "faintly of" (the suggestion pool uses these) so the name reads naturally.
clean_smell = avatar_smell.strip()
lead_ins = [
"suspiciously smelling of", "pleasantly smelling of", "reminiscent of",
"smells like", "a mix of", "like a mix of", "strongly of", "faintly of",
"like", "of",
]
stripped = True
while stripped:
stripped = False
for lead in lead_ins:
if clean_smell.lower().startswith(lead + " "):
clean_smell = clean_smell[len(lead) + 1:]
stripped = True
clean_smell = clean_smell[:1].upper() + clean_smell[1:]
player.name = f"Recruit {clean_smell}"
# 3. Randomly assign 3 new secret techniques, avoiding any technique already
# in play on another player's sheet (collisions make for boring recruits).
from .cards import TECHNIQUE_SUGGESTIONS
in_use = set()
for p in game.players:
if p.id == player.id:
continue
for t in [p.tech_jack, p.tech_queen, p.tech_king] + json.loads(p.created_techniques) + json.loads(p.swapped_techniques):
if t:
in_use.add(t.strip().lower())
available = [t for t in TECHNIQUE_SUGGESTIONS if t.strip().lower() not in in_use]
if len(available) < 3:
available = TECHNIQUE_SUGGESTIONS
chosen_techs = random.sample(available, 3)
player.created_techniques = json.dumps(chosen_techs)
player.swapped_techniques = json.dumps(chosen_techs)
player.tech_jack = chosen_techs[0]
player.tech_queen = chosen_techs[1]
player.tech_king = chosen_techs[2]
# 4. Auto-delegate Like/Hate questions to other players
other_players = [p for p in game.players if p.id != player.id]
if other_players:
like_target = random.choice(other_players)
player.other_like_from_player_id = like_target.id
player.other_like = ""
if len(other_players) >= 2:
remaining = [op for op in other_players if op.id != like_target.id]
hate_target = random.choice(remaining)
else:
hate_target = random.choice(other_players)
player.other_hate_from_player_id = hate_target.id
player.other_hate = ""
else:
player.other_like_from_player_id = None
player.other_like = ""
player.other_hate_from_player_id = None
player.other_hate = ""
db.add(player)
db.add(game)
db.commit()
# 5. Draw cards up to new hand size based on new Rank
max_size = calculate_max_hand_size(player, game.players, game.captain_player_id)
draw_cards_for_player(db, game, player, max_size)
add_game_event(db, game.id, f"{player.name} has joined the crew as a new Rank {player.rank} recruit!")