CLAUDE: Fix resolution phase control and add demo mode

Bug fix: During resolution phase (dice rolling), isMyTurn was false
for both players, preventing anyone from seeing the dice roller.
Now the batting team has control during resolution since they read
their card.

Demo mode: myTeamId now returns whichever team needs to act,
allowing single-player testing of both sides.

Changes:
- Add creator_discord_id to GameState (backend + frontend types)
- Add get_current_user_optional dependency for optional auth
- Update quick-create to capture creator's discord_id
- Fix isMyTurn to give batting team control during resolution
- Demo mode: myTeamId returns active team based on phase

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-13 23:47:21 -06:00
parent 581bc33f15
commit 38fb76c849
6 changed files with 90 additions and 17 deletions

View File

@ -0,0 +1,49 @@
"""
FastAPI dependencies for authentication and database access.
These dependencies can be injected into route handlers using FastAPI's Depends() mechanism.
"""
import logging
from fastapi import Request
from jose import JWTError
from app.utils.auth import verify_token
from app.utils.cookies import ACCESS_TOKEN_COOKIE
logger = logging.getLogger(f"{__name__}.dependencies")
async def get_current_user_optional(request: Request) -> dict | None:
"""
Get current authenticated user from cookie (optional).
This dependency returns the user dict if authenticated, or None if not.
Use this when authentication is optional but you want to capture user info
if available (e.g., for tracking game creators).
Args:
request: FastAPI request object
Returns:
User dict with discord_id, user_id, username if authenticated, None otherwise
"""
token = request.cookies.get(ACCESS_TOKEN_COOKIE)
if not token:
return None
try:
payload = verify_token(token)
return {
"discord_id": payload.get("discord_id"),
"user_id": payload.get("user_id"),
"username": payload.get("username"),
}
except JWTError:
logger.debug("Invalid token in optional auth check")
return None
except Exception as e:
logger.debug(f"Error verifying token in optional auth: {e}")
return None

View File

@ -1,9 +1,10 @@
import logging
from uuid import UUID, uuid4
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field, field_validator
from app.api.dependencies import get_current_user_optional
from app.core.game_engine import game_engine
from app.core.state_manager import state_manager
from app.database.operations import DatabaseOperations
@ -325,13 +326,16 @@ async def create_game(request: CreateGameRequest):
@router.post("/quick-create", response_model=CreateGameResponse)
async def quick_create_game():
async def quick_create_game(user: dict | None = Depends(get_current_user_optional)):
"""
Quick-create endpoint for testing - creates a game with pre-configured lineups.
Uses the lineup configuration from the most recent game (Team 35 vs Team 38).
This eliminates the 2-minute lineup configuration process during testing.
The authenticated user's discord_id is stored as creator_discord_id, allowing
them to control the home team regardless of actual team ownership.
Returns:
CreateGameResponse with game_id
"""
@ -344,16 +348,21 @@ async def quick_create_game():
away_team_id = 38
league_id = "sba"
# Get creator's discord_id from authenticated user
creator_discord_id = user.get("discord_id") if user else None
logger.info(
f"Quick-creating game {game_id}: {home_team_id} vs {away_team_id}"
f"Quick-creating game {game_id}: {home_team_id} vs {away_team_id} (creator: {creator_discord_id})"
)
# Create game in state manager
# Creator controls both teams for solo testing
state = await state_manager.create_game(
game_id=game_id,
league_id=league_id,
home_team_id=home_team_id,
away_team_id=away_team_id,
creator_discord_id=creator_discord_id,
)
# Save to database

View File

@ -70,6 +70,7 @@ class StateManager:
home_team_is_ai: bool = False,
away_team_is_ai: bool = False,
auto_mode: bool = False,
creator_discord_id: str | None = None,
) -> GameState:
"""
Create a new game state in memory.
@ -111,6 +112,7 @@ class StateManager:
home_team_is_ai=home_team_is_ai,
away_team_is_ai=away_team_is_ai,
auto_mode=auto_mode,
creator_discord_id=creator_discord_id,
current_batter=placeholder_batter, # Will be replaced by _prepare_next_play() when game starts
)

View File

@ -396,6 +396,9 @@ class GameState(BaseModel):
home_team_is_ai: bool = False
away_team_is_ai: bool = False
# Creator (for demo/testing - creator can control home team)
creator_discord_id: str | None = None
# Resolution mode
auto_mode: bool = (
False # True = auto-generate outcomes (PD only), False = manual submissions

View File

@ -438,20 +438,23 @@ const isLoading = ref(true)
const connectionStatus = ref<'connecting' | 'connected' | 'disconnected'>('connecting')
const showSubstitutions = ref(false)
// Determine which team (if any) the current user owns in this game
// Determine which team the user controls
// For demo/testing: user controls whichever team needs to act
const myTeamId = computed(() => {
if (!gameState.value) return null
const userTeamIds = authStore.userTeams.map(t => t.id)
if (userTeamIds.includes(gameState.value.home_team_id)) {
return gameState.value.home_team_id
// Return the team that currently needs to make a decision
if (gameState.value.half === 'top') {
// Top: away bats, home fields
return gameState.value.decision_phase === 'awaiting_defensive'
? gameState.value.home_team_id
: gameState.value.away_team_id
} else {
// Bottom: home bats, away fields
return gameState.value.decision_phase === 'awaiting_defensive'
? gameState.value.away_team_id
: gameState.value.home_team_id
}
if (userTeamIds.includes(gameState.value.away_team_id)) {
return gameState.value.away_team_id
}
return null // Spectator - doesn't own either team
})
// Dynamic ScoreBoard height tracking
@ -483,13 +486,13 @@ const currentTeam = computed(() => {
return gameState.value?.half === 'top' ? 'away' : 'home'
})
// Determine if it's the current user's turn to act
const isMyTurn = computed(() => {
if (!myTeamId.value || !gameState.value) return false
// Determine which team needs to act based on decision phase
// During decision phases, check which team needs to decide
if (needsDefensiveDecision.value) {
// Fielding team makes defensive decisions
// Top of inning: home fields, Bottom: away fields
const fieldingTeamId = gameState.value.half === 'top'
? gameState.value.home_team_id
: gameState.value.away_team_id
@ -498,14 +501,18 @@ const isMyTurn = computed(() => {
if (needsOffensiveDecision.value) {
// Batting team makes offensive decisions
// Top of inning: away bats, Bottom: home bats
const battingTeamId = gameState.value.half === 'top'
? gameState.value.away_team_id
: gameState.value.home_team_id
return myTeamId.value === battingTeamId
}
return false
// During resolution phase (dice rolling, outcome submission),
// the BATTING team has control (they read their card)
const battingTeamId = gameState.value.half === 'top'
? gameState.value.away_team_id
: gameState.value.home_team_id
return myTeamId.value === battingTeamId
})
const decisionPhase = computed(() => {

View File

@ -72,6 +72,9 @@ export interface GameState {
home_team_is_ai: boolean
away_team_is_ai: boolean
// Creator (for demo/testing - creator can control home team)
creator_discord_id: string | null
// Resolution mode
auto_mode: boolean