351 lines
13 KiB
Python
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!")
|