Spaces:
Sleeping
Sleeping
Commit
·
33dc256
1
Parent(s):
0344b6d
feat: improve app UI
Browse files- ARCHITECTURE_V3.md +2 -2
- api.py +0 -304
- app.py +0 -1274
- backend/game_engine.py +1 -1
- backend/game_manager.py +3 -3
- backend/main.py +4 -4
- config.py +0 -37
- frontend/src/App.jsx +5 -7
- frontend/src/pages/Game.jsx +49 -40
- frontend/src/pages/Home.jsx +24 -14
- frontend/src/pages/Join.jsx +24 -15
- frontend/tailwind.config.js +39 -0
- game_engine.py +0 -294
- game_manager.py +0 -151
- models.py +0 -182
ARCHITECTURE_V3.md
CHANGED
|
@@ -59,13 +59,13 @@ Suspects: ["Mme Leblanc", "Col. Moutarde", "Mlle Rose", "Prof. Violet", "Mme Per
|
|
| 59 |
│ [Plateau avec positions] │
|
| 60 |
│ │
|
| 61 |
├─────────────────────────────────────┤
|
| 62 |
-
│ Vos cartes: [🃏] [🃏] [🃏]
|
| 63 |
├─────────────────────────────────────┤
|
| 64 |
│ Tour de: Alice │
|
| 65 |
│ [🎲 Lancer dés] ou [⏭️ Passer] │
|
| 66 |
│ │
|
| 67 |
│ Si déplacé: │
|
| 68 |
-
│ [💭 Suggérer] [⚡ Accuser]
|
| 69 |
└─────────────────────────────────────┘
|
| 70 |
```
|
| 71 |
|
|
|
|
| 59 |
│ [Plateau avec positions] │
|
| 60 |
│ │
|
| 61 |
├─────────────────────────────────────┤
|
| 62 |
+
│ Vos cartes: [🃏] [🃏] [🃏] │
|
| 63 |
├─────────────────────────────────────┤
|
| 64 |
│ Tour de: Alice │
|
| 65 |
│ [🎲 Lancer dés] ou [⏭️ Passer] │
|
| 66 |
│ │
|
| 67 |
│ Si déplacé: │
|
| 68 |
+
│ [💭 Suggérer] [⚡ Accuser] │
|
| 69 |
└─────────────────────────────────────┘
|
| 70 |
```
|
| 71 |
|
api.py
DELETED
|
@@ -1,304 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
FastAPI backend for Cluedo Custom game.
|
| 3 |
-
Provides REST API endpoints for game management and actions.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from fastapi import FastAPI, HTTPException
|
| 7 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
-
from pydantic import BaseModel
|
| 9 |
-
from typing import List, Optional
|
| 10 |
-
|
| 11 |
-
from models import (
|
| 12 |
-
CreateGameRequest,
|
| 13 |
-
JoinGameRequest,
|
| 14 |
-
GameAction,
|
| 15 |
-
Game,
|
| 16 |
-
Player,
|
| 17 |
-
GameStatus
|
| 18 |
-
)
|
| 19 |
-
from game_manager import game_manager
|
| 20 |
-
from game_engine import GameEngine
|
| 21 |
-
from ai_service import ai_service
|
| 22 |
-
from config import settings
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
app = FastAPI(title=settings.APP_NAME)
|
| 26 |
-
|
| 27 |
-
# CORS disabled - not needed when API and Gradio are on same host (localhost)
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
class GameResponse(BaseModel):
|
| 31 |
-
"""Response when creating or joining a game."""
|
| 32 |
-
game_id: str
|
| 33 |
-
player_id: Optional[str] = None
|
| 34 |
-
message: str
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
class ActionResponse(BaseModel):
|
| 38 |
-
"""Response for game actions."""
|
| 39 |
-
success: bool
|
| 40 |
-
message: str
|
| 41 |
-
data: Optional[dict] = None
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
@app.get("/")
|
| 45 |
-
async def root():
|
| 46 |
-
"""Health check endpoint."""
|
| 47 |
-
return {
|
| 48 |
-
"app": settings.APP_NAME,
|
| 49 |
-
"status": "running",
|
| 50 |
-
"ai_enabled": settings.USE_OPENAI
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
@app.post("/games/create", response_model=GameResponse)
|
| 55 |
-
async def create_game(request: CreateGameRequest):
|
| 56 |
-
"""
|
| 57 |
-
Create a new game.
|
| 58 |
-
Returns the game ID.
|
| 59 |
-
"""
|
| 60 |
-
# Validate room count
|
| 61 |
-
if len(request.rooms) < settings.MIN_ROOMS:
|
| 62 |
-
raise HTTPException(
|
| 63 |
-
status_code=400,
|
| 64 |
-
detail=f"At least {settings.MIN_ROOMS} rooms are required"
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
if len(request.rooms) > settings.MAX_ROOMS:
|
| 68 |
-
raise HTTPException(
|
| 69 |
-
status_code=400,
|
| 70 |
-
detail=f"Maximum {settings.MAX_ROOMS} rooms allowed"
|
| 71 |
-
)
|
| 72 |
-
|
| 73 |
-
# Create game
|
| 74 |
-
game = game_manager.create_game(request)
|
| 75 |
-
|
| 76 |
-
# Generate AI scenario if enabled
|
| 77 |
-
if game.use_ai and settings.USE_OPENAI:
|
| 78 |
-
from game_engine import DEFAULT_CHARACTERS
|
| 79 |
-
scenario = await ai_service.generate_scenario(
|
| 80 |
-
game.rooms,
|
| 81 |
-
DEFAULT_CHARACTERS
|
| 82 |
-
)
|
| 83 |
-
if scenario:
|
| 84 |
-
game.scenario = scenario
|
| 85 |
-
game_manager.save_games()
|
| 86 |
-
|
| 87 |
-
return GameResponse(
|
| 88 |
-
game_id=game.game_id,
|
| 89 |
-
message=f"Game '{game.name}' created successfully"
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
@app.post("/games/join", response_model=GameResponse)
|
| 94 |
-
async def join_game(request: JoinGameRequest):
|
| 95 |
-
"""
|
| 96 |
-
Join an existing game.
|
| 97 |
-
Returns the player ID.
|
| 98 |
-
"""
|
| 99 |
-
game = game_manager.get_game(request.game_id)
|
| 100 |
-
|
| 101 |
-
if not game:
|
| 102 |
-
raise HTTPException(status_code=404, detail="Game not found")
|
| 103 |
-
|
| 104 |
-
if game.status != GameStatus.WAITING:
|
| 105 |
-
raise HTTPException(status_code=400, detail="Game has already started")
|
| 106 |
-
|
| 107 |
-
if game.is_full():
|
| 108 |
-
raise HTTPException(status_code=400, detail="Game is full")
|
| 109 |
-
|
| 110 |
-
player = game_manager.join_game(request.game_id, request.player_name)
|
| 111 |
-
|
| 112 |
-
if not player:
|
| 113 |
-
raise HTTPException(status_code=400, detail="Could not join game")
|
| 114 |
-
|
| 115 |
-
return GameResponse(
|
| 116 |
-
game_id=game.game_id,
|
| 117 |
-
player_id=player.id,
|
| 118 |
-
message=f"{request.player_name} joined the game"
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
@app.post("/games/{game_id}/start", response_model=ActionResponse)
|
| 123 |
-
async def start_game(game_id: str):
|
| 124 |
-
"""
|
| 125 |
-
Start a game (initialize cards and begin play).
|
| 126 |
-
"""
|
| 127 |
-
success = game_manager.start_game(game_id)
|
| 128 |
-
|
| 129 |
-
if not success:
|
| 130 |
-
raise HTTPException(
|
| 131 |
-
status_code=400,
|
| 132 |
-
detail="Could not start game. Check player count and game status."
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
return ActionResponse(
|
| 136 |
-
success=True,
|
| 137 |
-
message="Game started successfully"
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
@app.get("/games/{game_id}", response_model=Game)
|
| 142 |
-
async def get_game(game_id: str):
|
| 143 |
-
"""
|
| 144 |
-
Get full game state.
|
| 145 |
-
"""
|
| 146 |
-
game = game_manager.get_game(game_id)
|
| 147 |
-
|
| 148 |
-
if not game:
|
| 149 |
-
raise HTTPException(status_code=404, detail="Game not found")
|
| 150 |
-
|
| 151 |
-
return game
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
@app.get("/games/{game_id}/player/{player_id}")
|
| 155 |
-
async def get_player_view(game_id: str, player_id: str):
|
| 156 |
-
"""
|
| 157 |
-
Get game state from a specific player's perspective.
|
| 158 |
-
Hides other players' cards and the solution.
|
| 159 |
-
"""
|
| 160 |
-
game = game_manager.get_game(game_id)
|
| 161 |
-
|
| 162 |
-
if not game:
|
| 163 |
-
raise HTTPException(status_code=404, detail="Game not found")
|
| 164 |
-
|
| 165 |
-
player = next((p for p in game.players if p.id == player_id), None)
|
| 166 |
-
|
| 167 |
-
if not player:
|
| 168 |
-
raise HTTPException(status_code=404, detail="Player not found")
|
| 169 |
-
|
| 170 |
-
# Build safe view
|
| 171 |
-
other_players = [
|
| 172 |
-
{
|
| 173 |
-
"name": p.name,
|
| 174 |
-
"is_active": p.is_active,
|
| 175 |
-
"card_count": len(p.cards)
|
| 176 |
-
}
|
| 177 |
-
for p in game.players if p.id != player_id
|
| 178 |
-
]
|
| 179 |
-
|
| 180 |
-
current_player = game.get_current_player()
|
| 181 |
-
|
| 182 |
-
return {
|
| 183 |
-
"game_id": game.game_id,
|
| 184 |
-
"game_name": game.name,
|
| 185 |
-
"status": game.status,
|
| 186 |
-
"scenario": game.scenario,
|
| 187 |
-
"rooms": game.rooms,
|
| 188 |
-
"characters": [c.name for c in game.characters],
|
| 189 |
-
"weapons": [w.name for w in game.weapons],
|
| 190 |
-
"my_cards": [c.name for c in player.cards],
|
| 191 |
-
"other_players": other_players,
|
| 192 |
-
"current_turn": current_player.name if current_player else None,
|
| 193 |
-
"is_my_turn": current_player.id == player_id if current_player else False,
|
| 194 |
-
"recent_turns": game.turns[-5:] if game.turns else [],
|
| 195 |
-
"winner": game.winner
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
@app.post("/games/{game_id}/action", response_model=ActionResponse)
|
| 200 |
-
async def perform_action(game_id: str, action: GameAction):
|
| 201 |
-
"""
|
| 202 |
-
Perform a game action (suggest, accuse, pass).
|
| 203 |
-
"""
|
| 204 |
-
game = game_manager.get_game(game_id)
|
| 205 |
-
|
| 206 |
-
if not game:
|
| 207 |
-
raise HTTPException(status_code=404, detail="Game not found")
|
| 208 |
-
|
| 209 |
-
# Verify it's the player's turn
|
| 210 |
-
if not GameEngine.can_player_act(game, action.player_id):
|
| 211 |
-
raise HTTPException(status_code=403, detail="Not your turn or game not in progress")
|
| 212 |
-
|
| 213 |
-
player = next((p for p in game.players if p.id == action.player_id), None)
|
| 214 |
-
if not player:
|
| 215 |
-
raise HTTPException(status_code=404, detail="Player not found")
|
| 216 |
-
|
| 217 |
-
result_message = ""
|
| 218 |
-
result_data = {}
|
| 219 |
-
|
| 220 |
-
if action.action_type == "pass":
|
| 221 |
-
# Pass turn
|
| 222 |
-
GameEngine.add_turn_record(game, action.player_id, "pass", "Passed turn")
|
| 223 |
-
game.next_turn()
|
| 224 |
-
result_message = f"{player.name} passed their turn"
|
| 225 |
-
|
| 226 |
-
elif action.action_type == "suggest":
|
| 227 |
-
# Make a suggestion
|
| 228 |
-
if not all([action.character, action.weapon, action.room]):
|
| 229 |
-
raise HTTPException(status_code=400, detail="Suggestion requires character, weapon, and room")
|
| 230 |
-
|
| 231 |
-
can_disprove, disprover, card = GameEngine.check_suggestion(
|
| 232 |
-
game,
|
| 233 |
-
action.player_id,
|
| 234 |
-
action.character,
|
| 235 |
-
action.weapon,
|
| 236 |
-
action.room
|
| 237 |
-
)
|
| 238 |
-
|
| 239 |
-
suggestion_text = f"Suggested: {action.character} with {action.weapon} in {action.room}"
|
| 240 |
-
|
| 241 |
-
if can_disprove and disprover and card:
|
| 242 |
-
result_message = f"{disprover} disproved the suggestion by showing: {card.name}"
|
| 243 |
-
result_data = {"disproved_by": disprover, "card_shown": card.name}
|
| 244 |
-
else:
|
| 245 |
-
result_message = "No one could disprove the suggestion!"
|
| 246 |
-
result_data = {"disproved_by": None}
|
| 247 |
-
|
| 248 |
-
GameEngine.add_turn_record(game, action.player_id, "suggest", suggestion_text)
|
| 249 |
-
game.next_turn()
|
| 250 |
-
|
| 251 |
-
elif action.action_type == "accuse":
|
| 252 |
-
# Make an accusation
|
| 253 |
-
if not all([action.character, action.weapon, action.room]):
|
| 254 |
-
raise HTTPException(status_code=400, detail="Accusation requires character, weapon, and room")
|
| 255 |
-
|
| 256 |
-
accusation_text = f"Accused: {action.character} with {action.weapon} in {action.room}"
|
| 257 |
-
|
| 258 |
-
is_correct, message = GameEngine.process_accusation(
|
| 259 |
-
game,
|
| 260 |
-
action.player_id,
|
| 261 |
-
action.character,
|
| 262 |
-
action.weapon,
|
| 263 |
-
action.room
|
| 264 |
-
)
|
| 265 |
-
|
| 266 |
-
GameEngine.add_turn_record(game, action.player_id, "accuse", accusation_text)
|
| 267 |
-
|
| 268 |
-
if not is_correct and game.status == GameStatus.IN_PROGRESS:
|
| 269 |
-
game.next_turn()
|
| 270 |
-
|
| 271 |
-
result_message = message
|
| 272 |
-
result_data = {"correct": is_correct, "game_finished": game.status == GameStatus.FINISHED}
|
| 273 |
-
|
| 274 |
-
else:
|
| 275 |
-
raise HTTPException(status_code=400, detail="Invalid action type")
|
| 276 |
-
|
| 277 |
-
game_manager.save_games()
|
| 278 |
-
|
| 279 |
-
return ActionResponse(
|
| 280 |
-
success=True,
|
| 281 |
-
message=result_message,
|
| 282 |
-
data=result_data
|
| 283 |
-
)
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
@app.get("/games/list")
|
| 287 |
-
async def list_games():
|
| 288 |
-
"""
|
| 289 |
-
List all active games.
|
| 290 |
-
"""
|
| 291 |
-
return {"games": game_manager.list_active_games()}
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
@app.delete("/games/{game_id}")
|
| 295 |
-
async def delete_game(game_id: str):
|
| 296 |
-
"""
|
| 297 |
-
Delete a game.
|
| 298 |
-
"""
|
| 299 |
-
success = game_manager.delete_game(game_id)
|
| 300 |
-
|
| 301 |
-
if not success:
|
| 302 |
-
raise HTTPException(status_code=404, detail="Game not found")
|
| 303 |
-
|
| 304 |
-
return {"message": "Game deleted successfully"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
DELETED
|
@@ -1,1274 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Main application file for Cluedo Custom.
|
| 3 |
-
Integrates FastAPI backend with Gradio frontend interface.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import gradio as gr
|
| 7 |
-
import requests
|
| 8 |
-
import json
|
| 9 |
-
from typing import Optional, List
|
| 10 |
-
from config import settings
|
| 11 |
-
import uvicorn
|
| 12 |
-
import threading
|
| 13 |
-
import time
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
# Determine runtime environment
|
| 17 |
-
import os
|
| 18 |
-
|
| 19 |
-
IS_HUGGINGFACE = os.getenv("SPACE_ID") is not None
|
| 20 |
-
|
| 21 |
-
# API base URL (only used in local mode)
|
| 22 |
-
API_BASE = "http://localhost:8000"
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
class GameState:
|
| 26 |
-
"""Client-side game state management."""
|
| 27 |
-
|
| 28 |
-
def __init__(self):
|
| 29 |
-
self.game_id: Optional[str] = None
|
| 30 |
-
self.player_id: Optional[str] = None
|
| 31 |
-
self.player_name: Optional[str] = None
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
# Global state
|
| 35 |
-
state = GameState()
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def create_game(
|
| 39 |
-
game_name: str,
|
| 40 |
-
narrative_tone: str,
|
| 41 |
-
custom_prompt: str,
|
| 42 |
-
rooms_text: str,
|
| 43 |
-
weapons_text: str,
|
| 44 |
-
suspects_text: str,
|
| 45 |
-
use_ai: bool
|
| 46 |
-
):
|
| 47 |
-
"""
|
| 48 |
-
Create a new game with custom elements.
|
| 49 |
-
"""
|
| 50 |
-
if not game_name or not rooms_text:
|
| 51 |
-
return "❌ Fournissez un titre d'enquête et une liste de lieux", ""
|
| 52 |
-
|
| 53 |
-
# Parse inputs (comma or newline separated)
|
| 54 |
-
rooms = [r.strip() for r in rooms_text.replace("\n", ",").split(",") if r.strip()]
|
| 55 |
-
weapons = [w.strip() for w in weapons_text.replace("\n", ",").split(",") if w.strip()]
|
| 56 |
-
suspects = [s.strip() for s in suspects_text.replace("\n", ",").split(",") if s.strip()]
|
| 57 |
-
|
| 58 |
-
# Validation
|
| 59 |
-
if len(rooms) < settings.MIN_ROOMS:
|
| 60 |
-
return f"❌ Il faut au moins {settings.MIN_ROOMS} lieux", ""
|
| 61 |
-
|
| 62 |
-
if len(rooms) > settings.MAX_ROOMS:
|
| 63 |
-
return f"❌ Maximum {settings.MAX_ROOMS} lieux autorisés", ""
|
| 64 |
-
|
| 65 |
-
if len(weapons) < 3:
|
| 66 |
-
return "❌ Il faut au moins 3 armes", ""
|
| 67 |
-
|
| 68 |
-
if len(suspects) < 3:
|
| 69 |
-
return "❌ Il faut au moins 3 suspects", ""
|
| 70 |
-
|
| 71 |
-
try:
|
| 72 |
-
if IS_HUGGINGFACE:
|
| 73 |
-
from game_manager import game_manager
|
| 74 |
-
from models import CreateGameRequest
|
| 75 |
-
|
| 76 |
-
request = CreateGameRequest(
|
| 77 |
-
game_name=game_name,
|
| 78 |
-
narrative_tone=narrative_tone,
|
| 79 |
-
custom_prompt=custom_prompt if custom_prompt else None,
|
| 80 |
-
rooms=rooms,
|
| 81 |
-
custom_weapons=weapons,
|
| 82 |
-
custom_suspects=suspects,
|
| 83 |
-
use_ai=use_ai
|
| 84 |
-
)
|
| 85 |
-
game = game_manager.create_game(request)
|
| 86 |
-
|
| 87 |
-
# Generate AI scenario if enabled
|
| 88 |
-
if game.use_ai and settings.USE_OPENAI:
|
| 89 |
-
from ai_service import ai_service
|
| 90 |
-
import asyncio
|
| 91 |
-
|
| 92 |
-
try:
|
| 93 |
-
scenario = asyncio.run(
|
| 94 |
-
ai_service.generate_scenario(
|
| 95 |
-
game.rooms,
|
| 96 |
-
game.custom_suspects,
|
| 97 |
-
game.narrative_tone
|
| 98 |
-
)
|
| 99 |
-
)
|
| 100 |
-
if scenario:
|
| 101 |
-
game.scenario = scenario
|
| 102 |
-
game_manager.save_games()
|
| 103 |
-
except:
|
| 104 |
-
pass # AI is optional
|
| 105 |
-
|
| 106 |
-
state.game_id = game.game_id
|
| 107 |
-
return (
|
| 108 |
-
f"✅ Enquête créée !\n\n"
|
| 109 |
-
f"🔑 Code d'Enquête : **{game.game_id}**\n\n"
|
| 110 |
-
f"📤 Partagez ce code avec les autres joueurs\n"
|
| 111 |
-
f"ℹ️ Minimum {settings.MIN_PLAYERS} joueurs requis pour démarrer",
|
| 112 |
-
game.game_id,
|
| 113 |
-
)
|
| 114 |
-
else:
|
| 115 |
-
# HTTP API call (local mode)
|
| 116 |
-
response = requests.post(
|
| 117 |
-
f"{API_BASE}/games/create",
|
| 118 |
-
json={
|
| 119 |
-
"game_name": game_name,
|
| 120 |
-
"narrative_tone": narrative_tone,
|
| 121 |
-
"custom_prompt": custom_prompt if custom_prompt else None,
|
| 122 |
-
"rooms": rooms,
|
| 123 |
-
"custom_weapons": weapons,
|
| 124 |
-
"custom_suspects": suspects,
|
| 125 |
-
"use_ai": use_ai
|
| 126 |
-
},
|
| 127 |
-
timeout=5,
|
| 128 |
-
)
|
| 129 |
-
|
| 130 |
-
if response.status_code == 200:
|
| 131 |
-
data = response.json()
|
| 132 |
-
state.game_id = data["game_id"]
|
| 133 |
-
return (
|
| 134 |
-
f"✅ Enquête créée !\n\n"
|
| 135 |
-
f"🔑 Code : **{data['game_id']}**\n\n"
|
| 136 |
-
f"📤 Partagez ce code\n"
|
| 137 |
-
f"ℹ️ Min. {settings.MIN_PLAYERS} joueurs",
|
| 138 |
-
data["game_id"],
|
| 139 |
-
)
|
| 140 |
-
else:
|
| 141 |
-
return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}", ""
|
| 142 |
-
|
| 143 |
-
except Exception as e:
|
| 144 |
-
return f"❌ Erreur : {str(e)}", ""
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def join_game(game_id: str, player_name: str):
|
| 148 |
-
"""
|
| 149 |
-
Join an existing game.
|
| 150 |
-
"""
|
| 151 |
-
if not game_id or not player_name:
|
| 152 |
-
return "❌ Fournissez le code d'enquête et votre nom !"
|
| 153 |
-
|
| 154 |
-
try:
|
| 155 |
-
game_id = game_id.strip().upper()
|
| 156 |
-
player_name = player_name.strip()
|
| 157 |
-
|
| 158 |
-
if IS_HUGGINGFACE:
|
| 159 |
-
# Direct backend call
|
| 160 |
-
from game_manager import game_manager
|
| 161 |
-
from models import GameStatus
|
| 162 |
-
|
| 163 |
-
game = game_manager.get_game(game_id)
|
| 164 |
-
if not game:
|
| 165 |
-
return "❌ Enquête introuvable !"
|
| 166 |
-
|
| 167 |
-
if game.status != GameStatus.WAITING:
|
| 168 |
-
return "❌ La partie a déjà commencé !"
|
| 169 |
-
|
| 170 |
-
if game.is_full():
|
| 171 |
-
return "❌ Partie complète (maximum 8 joueurs) !"
|
| 172 |
-
|
| 173 |
-
player = game_manager.join_game(game_id, player_name)
|
| 174 |
-
if not player:
|
| 175 |
-
return "❌ Impossible de rejoindre l'enquête !"
|
| 176 |
-
|
| 177 |
-
state.game_id = game_id
|
| 178 |
-
state.player_id = player.id
|
| 179 |
-
state.player_name = player_name
|
| 180 |
-
|
| 181 |
-
return (
|
| 182 |
-
f"✅ Enquête rejointe !\n\n"
|
| 183 |
-
f"👋 Bienvenue, {player_name} !\n\n"
|
| 184 |
-
f"⏳ Attendez que le créateur démarre la partie\n"
|
| 185 |
-
f"📖 Consultez l'onglet 🔎 Enquêter pour voir l'état"
|
| 186 |
-
)
|
| 187 |
-
else:
|
| 188 |
-
# HTTP API call (local mode)
|
| 189 |
-
response = requests.post(
|
| 190 |
-
f"{API_BASE}/games/join",
|
| 191 |
-
json={
|
| 192 |
-
"game_id": game_id,
|
| 193 |
-
"player_name": player_name,
|
| 194 |
-
},
|
| 195 |
-
timeout=5,
|
| 196 |
-
)
|
| 197 |
-
|
| 198 |
-
if response.status_code == 200:
|
| 199 |
-
data = response.json()
|
| 200 |
-
state.game_id = data["game_id"]
|
| 201 |
-
state.player_id = data["player_id"]
|
| 202 |
-
state.player_name = player_name
|
| 203 |
-
|
| 204 |
-
return (
|
| 205 |
-
f"✅ Enquête rejointe avec succès !\n\n"
|
| 206 |
-
f"👋 Bienvenue, {player_name} !\n\n"
|
| 207 |
-
f"⏳ Attendez que le chnawax originel (le créateur) démarre la partie...\n"
|
| 208 |
-
f"📖 Allez dans l'onglet 🔎 Enquêter pour voir l'état de la partie."
|
| 209 |
-
)
|
| 210 |
-
else:
|
| 211 |
-
return f"❌ All RS5, erreur réseau (fourlestourtes et les bourbillats) : {response.json().get('detail', 'Erreur inconnue')}"
|
| 212 |
-
|
| 213 |
-
except Exception as e:
|
| 214 |
-
return f"❌ Koikoubaiseyyyyy ! Erreur : {str(e)}"
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
def start_game(game_id: str):
|
| 218 |
-
"""
|
| 219 |
-
Start the game.
|
| 220 |
-
"""
|
| 221 |
-
if not game_id:
|
| 222 |
-
return "❌ Triple monstre coucouuuuu ! Aucune enuête sélectionnée !"
|
| 223 |
-
|
| 224 |
-
try:
|
| 225 |
-
game_id = game_id.strip().upper()
|
| 226 |
-
|
| 227 |
-
if IS_HUGGINGFACE:
|
| 228 |
-
# Direct backend call
|
| 229 |
-
from game_manager import game_manager
|
| 230 |
-
|
| 231 |
-
success = game_manager.start_game(game_id)
|
| 232 |
-
|
| 233 |
-
if not success:
|
| 234 |
-
return f"❌ Impossible de démarrer ! Minimum {settings.MIN_PLAYERS} joueurs requis."
|
| 235 |
-
|
| 236 |
-
game = game_manager.get_game(game_id)
|
| 237 |
-
|
| 238 |
-
return (
|
| 239 |
-
f"🎬 L'ENQUÊTE COMMENCE !\n\n"
|
| 240 |
-
f"🃏 Les cartes ont été distribuées\n"
|
| 241 |
-
f"🎲 Tous les joueurs démarrent dans : {game.rooms[0] if game.rooms else '(aucun lieu)'}\n"
|
| 242 |
-
f"🔍 À vous de découvrir qui a commis le crime, avec quelle arme et dans quel lieu !\n\n"
|
| 243 |
-
f"➡️ Allez dans l'onglet 🔎 Enquêter pour commencer à jouer"
|
| 244 |
-
)
|
| 245 |
-
else:
|
| 246 |
-
# HTTP API call (local mode)
|
| 247 |
-
response = requests.post(f"{API_BASE}/games/{game_id}/start", timeout=5)
|
| 248 |
-
|
| 249 |
-
if response.status_code == 200:
|
| 250 |
-
return (
|
| 251 |
-
f"🎬 L'ENQUÊTE COMMENCE !\n\n"
|
| 252 |
-
f"🃏 Les cartes ont été distribuées\n"
|
| 253 |
-
f"🔍 À vous de découvrir qui a commis le crime !\n\n"
|
| 254 |
-
f"➡️ Allez dans l'onglet 🔎 Enquêter pour commencer"
|
| 255 |
-
)
|
| 256 |
-
else:
|
| 257 |
-
return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
|
| 258 |
-
|
| 259 |
-
except Exception as e:
|
| 260 |
-
return f"❌ Yamete coudasai ! Erreur au démarrage : {str(e)}"
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
def get_player_view():
|
| 264 |
-
"""
|
| 265 |
-
Get current game state for the player.
|
| 266 |
-
"""
|
| 267 |
-
if not state.game_id or not state.player_id:
|
| 268 |
-
return (
|
| 269 |
-
"❌ Eskilibass ! Vous n'êtes pas dans une enquête.\n\n"
|
| 270 |
-
"➡️ Créer une nouvelle enquête ou rejoignez d'autres péchailloux masqués."
|
| 271 |
-
)
|
| 272 |
-
|
| 273 |
-
try:
|
| 274 |
-
if IS_HUGGINGFACE:
|
| 275 |
-
# Direct backend call
|
| 276 |
-
from game_manager import game_manager
|
| 277 |
-
|
| 278 |
-
game = game_manager.get_game(state.game_id)
|
| 279 |
-
|
| 280 |
-
if not game:
|
| 281 |
-
return "❌ All RS5, erreur réseau ! Enquête introuvable... Fourlestourtes et les bourbillats !"
|
| 282 |
-
|
| 283 |
-
player = next((p for p in game.players if p.id == state.player_id), None)
|
| 284 |
-
|
| 285 |
-
if not player:
|
| 286 |
-
return "❌ Poupée en pénitence calisse ! Péchailloux masqué..."
|
| 287 |
-
|
| 288 |
-
# Build safe view
|
| 289 |
-
all_players = [
|
| 290 |
-
{
|
| 291 |
-
"name": p.name,
|
| 292 |
-
"is_active": p.is_active,
|
| 293 |
-
"card_count": len(p.cards),
|
| 294 |
-
"position": p.current_room_index
|
| 295 |
-
}
|
| 296 |
-
for p in game.players
|
| 297 |
-
]
|
| 298 |
-
|
| 299 |
-
current_player = game.get_current_player()
|
| 300 |
-
|
| 301 |
-
data = {
|
| 302 |
-
"game_id": game.game_id,
|
| 303 |
-
"game_name": game.name,
|
| 304 |
-
"status": game.status,
|
| 305 |
-
"scenario": game.scenario,
|
| 306 |
-
"rooms": game.rooms,
|
| 307 |
-
"characters": [c.name for c in game.characters],
|
| 308 |
-
"weapons": [w.name for w in game.weapons],
|
| 309 |
-
"my_cards": [c.name for c in player.cards],
|
| 310 |
-
"my_position": player.current_room_index,
|
| 311 |
-
"all_players": all_players,
|
| 312 |
-
"current_turn": current_player.name if current_player else None,
|
| 313 |
-
"is_my_turn": (
|
| 314 |
-
current_player.id == state.player_id if current_player else False
|
| 315 |
-
),
|
| 316 |
-
"recent_turns": game.turns[-5:] if game.turns else [],
|
| 317 |
-
"winner": game.winner,
|
| 318 |
-
}
|
| 319 |
-
else:
|
| 320 |
-
# HTTP API call (local mode)
|
| 321 |
-
response = requests.get(
|
| 322 |
-
f"{API_BASE}/games/{state.game_id}/player/{state.player_id}", timeout=5
|
| 323 |
-
)
|
| 324 |
-
|
| 325 |
-
if response.status_code == 200:
|
| 326 |
-
data = response.json()
|
| 327 |
-
else:
|
| 328 |
-
return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
|
| 329 |
-
|
| 330 |
-
# Format output (common for both modes)
|
| 331 |
-
output = []
|
| 332 |
-
output.append(f"═══ 🎮 {data['game_name']} ═══\n")
|
| 333 |
-
|
| 334 |
-
status_map = {
|
| 335 |
-
"waiting": "⏳ En attente de joueurs...",
|
| 336 |
-
"in_progress": "🎲 Partie en cours",
|
| 337 |
-
"finished": "🏆 Partie terminée",
|
| 338 |
-
}
|
| 339 |
-
output.append(f"📊 Statut : {status_map.get(data['status'], data['status'])}\n")
|
| 340 |
-
|
| 341 |
-
if data.get("scenario"):
|
| 342 |
-
output.append(f"\n📜 Scénario :\n{data['scenario']}\n")
|
| 343 |
-
|
| 344 |
-
# Show player's current position if game started
|
| 345 |
-
if data.get("my_position") is not None and data["rooms"]:
|
| 346 |
-
current_room = data["rooms"][data["my_position"]]
|
| 347 |
-
output.append(f"\n📍 VOTRE POSITION : **{current_room}**")
|
| 348 |
-
|
| 349 |
-
output.append(f"\n━━━ 🃏 VOS CARTES ━━━")
|
| 350 |
-
output.append("(Ces éléments NE SONT PAS la solution)")
|
| 351 |
-
for card in data["my_cards"]:
|
| 352 |
-
output.append(f" 🔸 {card}")
|
| 353 |
-
|
| 354 |
-
output.append(f"\n━━━ 🏠 PLATEAU DE JEU (Circuit) ━━━")
|
| 355 |
-
# Show rooms with player positions in circuit order
|
| 356 |
-
rooms_display = []
|
| 357 |
-
for idx, room in enumerate(data["rooms"]):
|
| 358 |
-
players_here = [p["name"] for p in data.get("all_players", []) if p.get("position") == idx]
|
| 359 |
-
|
| 360 |
-
# Visual indicator
|
| 361 |
-
if players_here:
|
| 362 |
-
icon = "👥"
|
| 363 |
-
player_names = ', '.join(players_here)
|
| 364 |
-
rooms_display.append(f" {idx+1}. {icon} **{room}** → {player_names}")
|
| 365 |
-
else:
|
| 366 |
-
icon = "🚪"
|
| 367 |
-
rooms_display.append(f" {idx+1}. {icon} {room}")
|
| 368 |
-
|
| 369 |
-
output.extend(rooms_display)
|
| 370 |
-
output.append(f" └─→ Circuit fermé (retour à {data['rooms'][0]})")
|
| 371 |
-
|
| 372 |
-
output.append(f"\n━━━ ℹ️ ÉLÉMENTS DU JEU ━━━")
|
| 373 |
-
output.append(f"👤 Suspects : {', '.join(data['characters'])}")
|
| 374 |
-
output.append(f"🔪 Armes : {', '.join(data['weapons'])}")
|
| 375 |
-
|
| 376 |
-
output.append(f"\n━━━ 👥 JOUEURS ━━━")
|
| 377 |
-
for player in data.get("all_players", []):
|
| 378 |
-
status_icon = "✅" if player["is_active"] else "❌"
|
| 379 |
-
position = data["rooms"][player["position"]] if player.get("position") is not None else "?"
|
| 380 |
-
output.append(
|
| 381 |
-
f" {status_icon} {player['name']} - {position} ({player['card_count']} cartes)"
|
| 382 |
-
)
|
| 383 |
-
|
| 384 |
-
if data["current_turn"]:
|
| 385 |
-
turn_marker = (
|
| 386 |
-
"👉 C'EST VOTRE TOUR !" if data["is_my_turn"] else ""
|
| 387 |
-
)
|
| 388 |
-
output.append(f"\n━━━ 🎯 TOUR ACTUEL ━━━")
|
| 389 |
-
output.append(f"🎲 {data['current_turn']} {turn_marker}")
|
| 390 |
-
|
| 391 |
-
if data.get("winner"):
|
| 392 |
-
output.append(
|
| 393 |
-
f"\n\n🏆🏆🏆 VAINQUEUR : {data['winner']} 🏆🏆🏆"
|
| 394 |
-
)
|
| 395 |
-
|
| 396 |
-
if data["recent_turns"]:
|
| 397 |
-
output.append(f"\n━━━ 📰 HISTORIQUE (5 dernières actions) ━━━")
|
| 398 |
-
for turn in data["recent_turns"][-5:]:
|
| 399 |
-
output.append(f" • {turn['player_name']}: {turn['action']}")
|
| 400 |
-
if turn.get("details"):
|
| 401 |
-
output.append(f" ↪ {turn['details']}")
|
| 402 |
-
if turn.get("ai_comment"):
|
| 403 |
-
output.append(f" 🗣️ Desland: {turn['ai_comment']}")
|
| 404 |
-
|
| 405 |
-
return "\n".join(output)
|
| 406 |
-
|
| 407 |
-
except Exception as e:
|
| 408 |
-
return f"❌ Erreur réseau (fourlestourtes et les bourbillats) : {str(e)}"
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
def make_suggestion(character: str, weapon: str, room: str):
|
| 412 |
-
"""
|
| 413 |
-
Make a suggestion.
|
| 414 |
-
"""
|
| 415 |
-
if not state.game_id or not state.player_id:
|
| 416 |
-
return "❌ Vous n'êtes pas dans une enquête"
|
| 417 |
-
|
| 418 |
-
if not all([character, weapon, room]):
|
| 419 |
-
return "❌ Choisissez un suspect, une arme et un lieu"
|
| 420 |
-
|
| 421 |
-
try:
|
| 422 |
-
if IS_HUGGINGFACE:
|
| 423 |
-
from game_manager import game_manager
|
| 424 |
-
from game_engine import GameEngine
|
| 425 |
-
from ai_service import ai_service
|
| 426 |
-
import asyncio
|
| 427 |
-
|
| 428 |
-
game = game_manager.get_game(state.game_id)
|
| 429 |
-
if not game:
|
| 430 |
-
return "❌ Enquête introuvable"
|
| 431 |
-
|
| 432 |
-
if not GameEngine.can_player_act(game, state.player_id):
|
| 433 |
-
return "❌ Ce n'est pas votre tour !"
|
| 434 |
-
|
| 435 |
-
# Check if player is in the correct room
|
| 436 |
-
can_suggest, error_msg = GameEngine.can_make_suggestion(game, state.player_id, room)
|
| 437 |
-
if not can_suggest:
|
| 438 |
-
return f"❌ {error_msg}"
|
| 439 |
-
|
| 440 |
-
player = next((p for p in game.players if p.id == state.player_id), None)
|
| 441 |
-
if not player:
|
| 442 |
-
return "❌ Joueur introuvable"
|
| 443 |
-
|
| 444 |
-
# Process suggestion
|
| 445 |
-
can_disprove, disprover, card = GameEngine.check_suggestion(
|
| 446 |
-
game, state.player_id, character, weapon, room
|
| 447 |
-
)
|
| 448 |
-
|
| 449 |
-
suggestion_text = f"{character} avec {weapon} dans {room}"
|
| 450 |
-
|
| 451 |
-
# Generate AI comment if enabled
|
| 452 |
-
ai_comment = None
|
| 453 |
-
if game.use_ai and settings.USE_OPENAI:
|
| 454 |
-
try:
|
| 455 |
-
ai_comment = asyncio.run(
|
| 456 |
-
ai_service.generate_suggestion_comment(
|
| 457 |
-
player.name,
|
| 458 |
-
character,
|
| 459 |
-
weapon,
|
| 460 |
-
room,
|
| 461 |
-
can_disprove,
|
| 462 |
-
game.narrative_tone
|
| 463 |
-
)
|
| 464 |
-
)
|
| 465 |
-
except:
|
| 466 |
-
pass
|
| 467 |
-
|
| 468 |
-
# Build response message
|
| 469 |
-
if can_disprove and disprover and card:
|
| 470 |
-
message = f"💭 {disprover} réfute en montrant : **{card.name}**"
|
| 471 |
-
else:
|
| 472 |
-
message = "💭 Personne ne peut réfuter cette suggestion !"
|
| 473 |
-
|
| 474 |
-
# Add AI comment if available
|
| 475 |
-
if ai_comment:
|
| 476 |
-
message += f"\n\n🗣️ **Desland** : {ai_comment}"
|
| 477 |
-
|
| 478 |
-
GameEngine.add_turn_record(
|
| 479 |
-
game, state.player_id, "suggest", suggestion_text, ai_comment
|
| 480 |
-
)
|
| 481 |
-
game.next_turn()
|
| 482 |
-
game_manager.save_games()
|
| 483 |
-
|
| 484 |
-
message += "\n\n➡️ Notez cette information dans votre feuille d'enquête !"
|
| 485 |
-
return message
|
| 486 |
-
|
| 487 |
-
else:
|
| 488 |
-
# HTTP API call (local mode)
|
| 489 |
-
response = requests.post(
|
| 490 |
-
f"{API_BASE}/games/{state.game_id}/action",
|
| 491 |
-
json={
|
| 492 |
-
"game_id": state.game_id,
|
| 493 |
-
"player_id": state.player_id,
|
| 494 |
-
"action_type": "suggest",
|
| 495 |
-
"character": character,
|
| 496 |
-
"weapon": weapon,
|
| 497 |
-
"room": room,
|
| 498 |
-
},
|
| 499 |
-
timeout=5,
|
| 500 |
-
)
|
| 501 |
-
|
| 502 |
-
if response.status_code == 200:
|
| 503 |
-
data = response.json()
|
| 504 |
-
return data.get("message", "Suggestion effectuée")
|
| 505 |
-
else:
|
| 506 |
-
return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
|
| 507 |
-
|
| 508 |
-
except Exception as e:
|
| 509 |
-
return f"❌ Erreur : {str(e)}"
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
def make_accusation(character: str, weapon: str, room: str):
|
| 513 |
-
"""
|
| 514 |
-
Make an accusation.
|
| 515 |
-
"""
|
| 516 |
-
if not state.game_id or not state.player_id:
|
| 517 |
-
return "❌ Vous n'êtes pas dans une enquête"
|
| 518 |
-
|
| 519 |
-
if not all([character, weapon, room]):
|
| 520 |
-
return "❌ Choisissez un suspect, une arme et un lieu"
|
| 521 |
-
|
| 522 |
-
try:
|
| 523 |
-
if IS_HUGGINGFACE:
|
| 524 |
-
from game_manager import game_manager
|
| 525 |
-
from game_engine import GameEngine
|
| 526 |
-
from models import GameStatus
|
| 527 |
-
from ai_service import ai_service
|
| 528 |
-
import asyncio
|
| 529 |
-
|
| 530 |
-
game = game_manager.get_game(state.game_id)
|
| 531 |
-
if not game:
|
| 532 |
-
return "❌ Enquête introuvable"
|
| 533 |
-
|
| 534 |
-
if not GameEngine.can_player_act(game, state.player_id):
|
| 535 |
-
return "❌ Ce n'est pas votre tour !"
|
| 536 |
-
|
| 537 |
-
player = next((p for p in game.players if p.id == state.player_id), None)
|
| 538 |
-
if not player:
|
| 539 |
-
return "❌ Joueur introuvable"
|
| 540 |
-
|
| 541 |
-
accusation_text = f"{character} avec {weapon} dans {room}"
|
| 542 |
-
|
| 543 |
-
is_correct, message = GameEngine.process_accusation(
|
| 544 |
-
game, state.player_id, character, weapon, room
|
| 545 |
-
)
|
| 546 |
-
|
| 547 |
-
# Generate AI comment if enabled
|
| 548 |
-
ai_comment = None
|
| 549 |
-
if game.use_ai and settings.USE_OPENAI:
|
| 550 |
-
try:
|
| 551 |
-
ai_comment = asyncio.run(
|
| 552 |
-
ai_service.generate_accusation_comment(
|
| 553 |
-
player.name,
|
| 554 |
-
character,
|
| 555 |
-
weapon,
|
| 556 |
-
room,
|
| 557 |
-
is_correct,
|
| 558 |
-
game.narrative_tone
|
| 559 |
-
)
|
| 560 |
-
)
|
| 561 |
-
except:
|
| 562 |
-
pass
|
| 563 |
-
|
| 564 |
-
GameEngine.add_turn_record(game, state.player_id, "accuse", accusation_text, ai_comment)
|
| 565 |
-
|
| 566 |
-
if not is_correct and game.status == GameStatus.IN_PROGRESS:
|
| 567 |
-
game.next_turn()
|
| 568 |
-
|
| 569 |
-
game_manager.save_games()
|
| 570 |
-
|
| 571 |
-
# Build response
|
| 572 |
-
if is_correct:
|
| 573 |
-
response = f"🎉🏆 {message} 🎉🏆\n\nVous avez résolu le mystère !"
|
| 574 |
-
else:
|
| 575 |
-
response = f"💀 {message}\n\nVous avez été éliminé !\nVous pouvez toujours aider en réfutant les théories des autres."
|
| 576 |
-
|
| 577 |
-
if ai_comment:
|
| 578 |
-
response += f"\n\n🗣️ **Desland** : {ai_comment}"
|
| 579 |
-
|
| 580 |
-
return response
|
| 581 |
-
|
| 582 |
-
else:
|
| 583 |
-
# HTTP API call (local mode)
|
| 584 |
-
response = requests.post(
|
| 585 |
-
f"{API_BASE}/games/{state.game_id}/action",
|
| 586 |
-
json={
|
| 587 |
-
"game_id": state.game_id,
|
| 588 |
-
"player_id": state.player_id,
|
| 589 |
-
"action_type": "accuse",
|
| 590 |
-
"character": character,
|
| 591 |
-
"weapon": weapon,
|
| 592 |
-
"room": room,
|
| 593 |
-
},
|
| 594 |
-
timeout=5,
|
| 595 |
-
)
|
| 596 |
-
|
| 597 |
-
if response.status_code == 200:
|
| 598 |
-
data = response.json()
|
| 599 |
-
return data.get("message", "Accusation effectuée")
|
| 600 |
-
else:
|
| 601 |
-
return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
|
| 602 |
-
|
| 603 |
-
except Exception as e:
|
| 604 |
-
return f"❌ Erreur : {str(e)}"
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
def roll_and_move():
|
| 608 |
-
"""
|
| 609 |
-
Roll dice and move the player.
|
| 610 |
-
"""
|
| 611 |
-
if not state.game_id or not state.player_id:
|
| 612 |
-
return "❌ Vous n'êtes pas dans une enquête"
|
| 613 |
-
|
| 614 |
-
try:
|
| 615 |
-
if IS_HUGGINGFACE:
|
| 616 |
-
from game_manager import game_manager
|
| 617 |
-
from game_engine import GameEngine
|
| 618 |
-
|
| 619 |
-
game = game_manager.get_game(state.game_id)
|
| 620 |
-
if not game:
|
| 621 |
-
return "❌ Enquête introuvable"
|
| 622 |
-
|
| 623 |
-
if not GameEngine.can_player_act(game, state.player_id):
|
| 624 |
-
return "❌ Ce n'est pas votre tour !"
|
| 625 |
-
|
| 626 |
-
# Roll dice
|
| 627 |
-
dice_roll = GameEngine.roll_dice()
|
| 628 |
-
|
| 629 |
-
# Move player
|
| 630 |
-
success, message, new_room_index = GameEngine.move_player(
|
| 631 |
-
game, state.player_id, dice_roll
|
| 632 |
-
)
|
| 633 |
-
|
| 634 |
-
if not success:
|
| 635 |
-
return f"❌ {message}"
|
| 636 |
-
|
| 637 |
-
# Record turn
|
| 638 |
-
GameEngine.add_turn_record(game, state.player_id, "move", message)
|
| 639 |
-
game_manager.save_games()
|
| 640 |
-
|
| 641 |
-
current_room = game.rooms[new_room_index]
|
| 642 |
-
return f"🎲 {message}\n\n📍 Vous êtes maintenant dans : **{current_room}**\n\nVous pouvez faire une suggestion dans cette pièce ou passer votre tour."
|
| 643 |
-
|
| 644 |
-
else:
|
| 645 |
-
# HTTP API (local mode)
|
| 646 |
-
response = requests.post(
|
| 647 |
-
f"{API_BASE}/games/{state.game_id}/action",
|
| 648 |
-
json={
|
| 649 |
-
"game_id": state.game_id,
|
| 650 |
-
"player_id": state.player_id,
|
| 651 |
-
"action_type": "move"
|
| 652 |
-
},
|
| 653 |
-
timeout=5,
|
| 654 |
-
)
|
| 655 |
-
|
| 656 |
-
if response.status_code == 200:
|
| 657 |
-
data = response.json()
|
| 658 |
-
return data.get("message", "Déplacé avec succès")
|
| 659 |
-
else:
|
| 660 |
-
return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
|
| 661 |
-
|
| 662 |
-
except Exception as e:
|
| 663 |
-
return f"❌ Erreur : {str(e)}"
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
def pass_turn():
|
| 667 |
-
"""
|
| 668 |
-
Pass the current turn.
|
| 669 |
-
"""
|
| 670 |
-
if not state.game_id or not state.player_id:
|
| 671 |
-
return "❌ Vous n'êtes pas dans une enquête"
|
| 672 |
-
|
| 673 |
-
try:
|
| 674 |
-
if IS_HUGGINGFACE:
|
| 675 |
-
from game_manager import game_manager
|
| 676 |
-
from game_engine import GameEngine
|
| 677 |
-
|
| 678 |
-
game = game_manager.get_game(state.game_id)
|
| 679 |
-
if not game:
|
| 680 |
-
return "❌ Enquête introuvable"
|
| 681 |
-
|
| 682 |
-
if not GameEngine.can_player_act(game, state.player_id):
|
| 683 |
-
return "❌ Ce n'est pas votre tour !"
|
| 684 |
-
|
| 685 |
-
# Pass turn
|
| 686 |
-
GameEngine.add_turn_record(game, state.player_id, "pass", "Tour passé")
|
| 687 |
-
game.next_turn()
|
| 688 |
-
game_manager.save_games()
|
| 689 |
-
|
| 690 |
-
next_player = game.get_current_player()
|
| 691 |
-
return f"✅ Tour passé !\n\n➡️ C'est maintenant au tour de {next_player.name if next_player else 'quelqu\'un'}."
|
| 692 |
-
|
| 693 |
-
else:
|
| 694 |
-
response = requests.post(
|
| 695 |
-
f"{API_BASE}/games/{state.game_id}/action",
|
| 696 |
-
json={
|
| 697 |
-
"game_id": state.game_id,
|
| 698 |
-
"player_id": state.player_id,
|
| 699 |
-
"action_type": "pass",
|
| 700 |
-
},
|
| 701 |
-
timeout=5,
|
| 702 |
-
)
|
| 703 |
-
|
| 704 |
-
if response.status_code == 200:
|
| 705 |
-
return "✅ Tour passé !"
|
| 706 |
-
else:
|
| 707 |
-
return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
|
| 708 |
-
|
| 709 |
-
except Exception as e:
|
| 710 |
-
return f"❌ Erreur : {str(e)}"
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
# Sample lists for dropdowns
|
| 714 |
-
from game_engine import DEFAULT_CHARACTERS, DEFAULT_WEAPONS
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
def create_gradio_interface():
|
| 718 |
-
"""
|
| 719 |
-
Create the Gradio interface.
|
| 720 |
-
"""
|
| 721 |
-
# Custom dark detective/horror theme
|
| 722 |
-
custom_theme = gr.themes.Base(
|
| 723 |
-
primary_hue="red",
|
| 724 |
-
secondary_hue="slate",
|
| 725 |
-
neutral_hue="stone",
|
| 726 |
-
font=("ui-serif", "Georgia", "serif"),
|
| 727 |
-
).set(
|
| 728 |
-
body_background_fill="*neutral_950",
|
| 729 |
-
body_background_fill_dark="*neutral_950",
|
| 730 |
-
body_text_color="*neutral_200",
|
| 731 |
-
body_text_color_dark="*neutral_200",
|
| 732 |
-
button_primary_background_fill="*primary_700",
|
| 733 |
-
button_primary_background_fill_dark="*primary_800",
|
| 734 |
-
button_primary_text_color="white",
|
| 735 |
-
button_secondary_background_fill="*neutral_700",
|
| 736 |
-
button_secondary_background_fill_dark="*neutral_800",
|
| 737 |
-
input_background_fill="*neutral_800",
|
| 738 |
-
input_background_fill_dark="*neutral_900",
|
| 739 |
-
input_border_color="*neutral_700",
|
| 740 |
-
block_background_fill="*neutral_900",
|
| 741 |
-
block_background_fill_dark="*neutral_900",
|
| 742 |
-
block_border_color="*neutral_700",
|
| 743 |
-
block_label_text_color="*primary_400",
|
| 744 |
-
block_title_text_color="*primary_300",
|
| 745 |
-
)
|
| 746 |
-
|
| 747 |
-
custom_css = """
|
| 748 |
-
@import url('https://fonts.googleapis.com/css2?family=Creepster&family=Cinzel:wght@400;600&display=swap');
|
| 749 |
-
|
| 750 |
-
.gradio-container {
|
| 751 |
-
background:
|
| 752 |
-
linear-gradient(180deg, rgba(10,0,0,0.95) 0%, rgba(26,0,0,0.9) 50%, rgba(10,5,5,0.95) 100%),
|
| 753 |
-
repeating-linear-gradient(90deg, transparent, transparent 2px, rgba(0,0,0,0.3) 2px, rgba(0,0,0,0.3) 4px),
|
| 754 |
-
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><filter id="noise"><feTurbulence baseFrequency="0.9" /></filter><rect width="100" height="100" filter="url(%23noise)" opacity="0.05"/></svg>') !important;
|
| 755 |
-
font-family: 'Cinzel', 'Georgia', serif !important;
|
| 756 |
-
position: relative;
|
| 757 |
-
}
|
| 758 |
-
|
| 759 |
-
.gradio-container::before {
|
| 760 |
-
content: '';
|
| 761 |
-
position: fixed;
|
| 762 |
-
top: 0;
|
| 763 |
-
left: 0;
|
| 764 |
-
width: 100%;
|
| 765 |
-
height: 100%;
|
| 766 |
-
background: radial-gradient(ellipse at center, transparent 0%, rgba(0,0,0,0.7) 100%);
|
| 767 |
-
pointer-events: none;
|
| 768 |
-
z-index: 0;
|
| 769 |
-
}
|
| 770 |
-
|
| 771 |
-
h1 {
|
| 772 |
-
color: #8b0000 !important;
|
| 773 |
-
text-shadow:
|
| 774 |
-
0 0 10px rgba(139,0,0,0.8),
|
| 775 |
-
0 0 20px rgba(139,0,0,0.6),
|
| 776 |
-
3px 3px 6px rgba(0,0,0,0.9),
|
| 777 |
-
0 0 40px rgba(220,20,60,0.4);
|
| 778 |
-
font-family: 'Creepster', 'Georgia', cursive !important;
|
| 779 |
-
letter-spacing: 4px;
|
| 780 |
-
font-size: 3em !important;
|
| 781 |
-
animation: flicker 3s infinite alternate;
|
| 782 |
-
}
|
| 783 |
-
|
| 784 |
-
@keyframes flicker {
|
| 785 |
-
0%, 100% { opacity: 1; text-shadow: 0 0 10px rgba(139,0,0,0.8), 0 0 20px rgba(139,0,0,0.6), 3px 3px 6px rgba(0,0,0,0.9); }
|
| 786 |
-
50% { opacity: 0.95; text-shadow: 0 0 15px rgba(139,0,0,1), 0 0 25px rgba(139,0,0,0.8), 3px 3px 6px rgba(0,0,0,0.9); }
|
| 787 |
-
}
|
| 788 |
-
|
| 789 |
-
h2, h3 {
|
| 790 |
-
color: #b91c1c !important;
|
| 791 |
-
text-shadow: 2px 2px 4px rgba(0,0,0,0.9), 0 0 10px rgba(185,28,28,0.5);
|
| 792 |
-
font-family: 'Cinzel', 'Georgia', serif !important;
|
| 793 |
-
letter-spacing: 3px;
|
| 794 |
-
border-bottom: 1px solid rgba(139,0,0,0.3);
|
| 795 |
-
padding-bottom: 8px;
|
| 796 |
-
}
|
| 797 |
-
|
| 798 |
-
.tabs button {
|
| 799 |
-
background: linear-gradient(180deg, #1a0f0f 0%, #0a0000 100%) !important;
|
| 800 |
-
border: 1px solid #44403c !important;
|
| 801 |
-
color: #d6d3d1 !important;
|
| 802 |
-
transition: all 0.3s ease;
|
| 803 |
-
font-family: 'Cinzel', serif !important;
|
| 804 |
-
letter-spacing: 1px;
|
| 805 |
-
}
|
| 806 |
-
|
| 807 |
-
.tabs button:hover {
|
| 808 |
-
background: linear-gradient(180deg, #2a0f0f 0%, #1a0000 100%) !important;
|
| 809 |
-
border-color: #8b0000 !important;
|
| 810 |
-
box-shadow: 0 0 15px rgba(139,0,0,0.5);
|
| 811 |
-
}
|
| 812 |
-
|
| 813 |
-
.tabs button[aria-selected="true"] {
|
| 814 |
-
background: linear-gradient(180deg, #8b0000 0%, #5a0000 100%) !important;
|
| 815 |
-
border-color: #dc2626 !important;
|
| 816 |
-
color: #fef2f2 !important;
|
| 817 |
-
box-shadow:
|
| 818 |
-
0 0 20px rgba(139,0,0,0.6),
|
| 819 |
-
inset 0 0 10px rgba(0,0,0,0.5);
|
| 820 |
-
}
|
| 821 |
-
|
| 822 |
-
.gr-button {
|
| 823 |
-
background: linear-gradient(180deg, #7c2d12 0%, #5a1a0a 100%) !important;
|
| 824 |
-
border: 1px solid #8b0000 !important;
|
| 825 |
-
color: #fef2f2 !important;
|
| 826 |
-
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
|
| 827 |
-
transition: all 0.3s ease;
|
| 828 |
-
font-family: 'Cinzel', serif !important;
|
| 829 |
-
letter-spacing: 1px;
|
| 830 |
-
}
|
| 831 |
-
|
| 832 |
-
.gr-button:hover {
|
| 833 |
-
background: linear-gradient(180deg, #8b0000 0%, #6a0000 100%) !important;
|
| 834 |
-
box-shadow: 0 0 20px rgba(139,0,0,0.7), 0 5px 15px rgba(0,0,0,0.5);
|
| 835 |
-
transform: translateY(-2px);
|
| 836 |
-
}
|
| 837 |
-
|
| 838 |
-
.gr-button-primary {
|
| 839 |
-
background: linear-gradient(180deg, #991b1b 0%, #7f1d1d 100%) !important;
|
| 840 |
-
border: 2px solid #dc2626 !important;
|
| 841 |
-
box-shadow: 0 0 15px rgba(153,27,27,0.5);
|
| 842 |
-
}
|
| 843 |
-
|
| 844 |
-
.gr-button-stop {
|
| 845 |
-
background: linear-gradient(180deg, #450a0a 0%, #1a0000 100%) !important;
|
| 846 |
-
border: 2px solid #7f1d1d !important;
|
| 847 |
-
animation: pulse-danger 2s infinite;
|
| 848 |
-
}
|
| 849 |
-
|
| 850 |
-
@keyframes pulse-danger {
|
| 851 |
-
0%, 100% { box-shadow: 0 0 10px rgba(127,29,29,0.5); }
|
| 852 |
-
50% { box-shadow: 0 0 25px rgba(127,29,29,0.9), 0 0 40px rgba(220,38,38,0.5); }
|
| 853 |
-
}
|
| 854 |
-
|
| 855 |
-
.gr-textbox, .gr-dropdown {
|
| 856 |
-
background: rgba(20,10,10,0.8) !important;
|
| 857 |
-
border: 1px solid #44403c !important;
|
| 858 |
-
color: #e7e5e4 !important;
|
| 859 |
-
box-shadow: inset 0 2px 4px rgba(0,0,0,0.5);
|
| 860 |
-
}
|
| 861 |
-
|
| 862 |
-
.gr-textbox:focus, .gr-dropdown:focus {
|
| 863 |
-
border-color: #8b0000 !important;
|
| 864 |
-
box-shadow: 0 0 15px rgba(139,0,0,0.4), inset 0 2px 4px rgba(0,0,0,0.5);
|
| 865 |
-
}
|
| 866 |
-
|
| 867 |
-
.gr-group, .gr-box {
|
| 868 |
-
background: rgba(15,5,5,0.6) !important;
|
| 869 |
-
border: 1px solid rgba(68,64,60,0.5) !important;
|
| 870 |
-
border-radius: 8px !important;
|
| 871 |
-
box-shadow:
|
| 872 |
-
0 4px 6px rgba(0,0,0,0.5),
|
| 873 |
-
inset 0 1px 2px rgba(139,0,0,0.1);
|
| 874 |
-
}
|
| 875 |
-
|
| 876 |
-
.gr-accordion {
|
| 877 |
-
background: rgba(26,10,10,0.7) !important;
|
| 878 |
-
border: 1px solid rgba(139,0,0,0.3) !important;
|
| 879 |
-
border-radius: 6px;
|
| 880 |
-
}
|
| 881 |
-
|
| 882 |
-
label {
|
| 883 |
-
color: #fca5a5 !important;
|
| 884 |
-
font-family: 'Cinzel', serif !important;
|
| 885 |
-
letter-spacing: 1px;
|
| 886 |
-
text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
|
| 887 |
-
}
|
| 888 |
-
|
| 889 |
-
.markdown {
|
| 890 |
-
color: #d6d3d1 !important;
|
| 891 |
-
}
|
| 892 |
-
|
| 893 |
-
.warning-text {
|
| 894 |
-
color: #fca5a5 !important;
|
| 895 |
-
font-style: italic;
|
| 896 |
-
text-shadow: 1px 1px 2px rgba(0,0,0,0.8), 0 0 10px rgba(252,165,165,0.3);
|
| 897 |
-
}
|
| 898 |
-
|
| 899 |
-
/* Effet de brouillard fantomatique */
|
| 900 |
-
@keyframes ghost-float {
|
| 901 |
-
0%, 100% { opacity: 0.05; transform: translateY(0px); }
|
| 902 |
-
50% { opacity: 0.15; transform: translateY(-20px); }
|
| 903 |
-
}
|
| 904 |
-
|
| 905 |
-
.gradio-container::after {
|
| 906 |
-
content: '';
|
| 907 |
-
position: fixed;
|
| 908 |
-
top: -50%;
|
| 909 |
-
left: -50%;
|
| 910 |
-
width: 200%;
|
| 911 |
-
height: 200%;
|
| 912 |
-
background: radial-gradient(circle, rgba(139,0,0,0.03) 0%, transparent 50%);
|
| 913 |
-
animation: ghost-float 10s infinite ease-in-out;
|
| 914 |
-
pointer-events: none;
|
| 915 |
-
z-index: 1;
|
| 916 |
-
}
|
| 917 |
-
"""
|
| 918 |
-
|
| 919 |
-
with gr.Blocks(title=settings.APP_NAME, theme=custom_theme, css=custom_css) as demo:
|
| 920 |
-
gr.Markdown(f"# 🔍 {settings.APP_NAME} 🔪")
|
| 921 |
-
gr.Markdown("*Un mystère mortel vous attend dans votre propre lieu...*")
|
| 922 |
-
|
| 923 |
-
# Rules section (collapsible)
|
| 924 |
-
with gr.Accordion("📖 Règles du Jeu & Guide", open=False):
|
| 925 |
-
gr.Markdown(
|
| 926 |
-
"""
|
| 927 |
-
## 🎯 Objectif
|
| 928 |
-
Soyez le premier détective à résoudre le meurtre en identifiant correctement :
|
| 929 |
-
- **Le meurtrier** (personnage)
|
| 930 |
-
- **L'arme du crime** (arme)
|
| 931 |
-
- **Le lieu du crime** (pièce)
|
| 932 |
-
|
| 933 |
-
## 🎮 Étapes de Jeu
|
| 934 |
-
|
| 935 |
-
### 1️⃣ Création de la Partie
|
| 936 |
-
- Un joueur crée la partie en définissant 6-12 lieux personnalisés
|
| 937 |
-
- Partagez le **Code d'Enquête** avec les autres détectives
|
| 938 |
-
|
| 939 |
-
### 2️⃣ Rejoindre l'Enquête
|
| 940 |
-
- Les détectives rejoignent avec le code partagé (min. 3 joueurs)
|
| 941 |
-
- Le créateur lance l'enquête quand tous sont prêts
|
| 942 |
-
|
| 943 |
-
### 3️⃣ Distribution des Cartes
|
| 944 |
-
- Une solution secrète est créée (1 personnage + 1 arme + 1 lieu)
|
| 945 |
-
- Les cartes restantes sont distribuées équitablement entre les joueurs
|
| 946 |
-
- Vous voyez vos propres cartes = ces éléments **NE SONT PAS** la solution
|
| 947 |
-
|
| 948 |
-
### 4️⃣ Votre Tour
|
| 949 |
-
Trois actions possibles :
|
| 950 |
-
|
| 951 |
-
**💭 Proposer une Théorie** (Suggestion)
|
| 952 |
-
- Proposez une combinaison personnage + arme + lieu
|
| 953 |
-
- Les autres joueurs essaient de réfuter en montrant UNE carte correspondante
|
| 954 |
-
- Seul VOUS voyez la carte révélée
|
| 955 |
-
- Utilisez cela pour éliminer des possibilités
|
| 956 |
-
|
| 957 |
-
**⚡ Accusation Finale**
|
| 958 |
-
- Si vous pensez connaître la solution, faites une accusation
|
| 959 |
-
- ✅ **Correct** = Vous gagnez immédiatement !
|
| 960 |
-
- ❌ **Faux** = Vous êtes éliminé de l'enquête (mais pouvez encore réfuter)
|
| 961 |
-
|
| 962 |
-
**⏭️ Passer le Tour**
|
| 963 |
-
- Passez votre tour si vous n'avez rien à proposer
|
| 964 |
-
|
| 965 |
-
## 🏆 Conditions de Victoire
|
| 966 |
-
- Premier joueur à faire une **accusation correcte**
|
| 967 |
-
- Dernier joueur actif si tous les autres sont éliminés
|
| 968 |
-
|
| 969 |
-
## 💡 Conseils Stratégiques
|
| 970 |
-
- Notez les cartes que vous voyez (sur papier)
|
| 971 |
-
- Déduisez les cartes des autres joueurs par élimination
|
| 972 |
-
- Ne faites pas d'accusation tant que vous n'êtes pas sûr !
|
| 973 |
-
- Les suggestions peuvent forcer les joueurs à révéler des informations
|
| 974 |
-
|
| 975 |
-
## 🤖 Mode IA Narrateur (optionnel)
|
| 976 |
-
Active une narration générée par IA incarnée dans Desland, un vieux jardinier mystérieux qui semble toujours en savoir plus qu'il ne devrait... Il se trompe souvent sur son nom (Leland? Non, c'est Desland...) et parle de manière étrangement suspicieuse, comme s'il cachait quelque chose de très sombre.
|
| 977 |
-
"""
|
| 978 |
-
)
|
| 979 |
-
|
| 980 |
-
with gr.Tab("🕯️ Créer une Partie"):
|
| 981 |
-
gr.Markdown("### 🎮 Créer Votre Enquête Personnalisée")
|
| 982 |
-
gr.Markdown("*Créez votre propre manoir, vos suspects et vos armes...*")
|
| 983 |
-
|
| 984 |
-
with gr.Group():
|
| 985 |
-
game_name_input = gr.Textbox(
|
| 986 |
-
label="🎭 Titre de l'enquête",
|
| 987 |
-
placeholder="Meurtre au Coworking",
|
| 988 |
-
info="Donnez un titre à votre affaire",
|
| 989 |
-
)
|
| 990 |
-
|
| 991 |
-
from models import NarrativeTone
|
| 992 |
-
narrative_tone_dropdown = gr.Dropdown(
|
| 993 |
-
label="🎨 Tonalité narrative",
|
| 994 |
-
choices=[tone.value for tone in NarrativeTone],
|
| 995 |
-
value=NarrativeTone.SERIOUS.value,
|
| 996 |
-
info="Choisissez l'ambiance du jeu",
|
| 997 |
-
)
|
| 998 |
-
|
| 999 |
-
custom_prompt_input = gr.Textbox(
|
| 1000 |
-
label="✍️ Prompt personnalisé (optionnel)",
|
| 1001 |
-
placeholder="Style Agatha Christie avec humour noir...",
|
| 1002 |
-
lines=2,
|
| 1003 |
-
info="Personnalisez le style narratif de l'IA",
|
| 1004 |
-
)
|
| 1005 |
-
|
| 1006 |
-
with gr.Group():
|
| 1007 |
-
gr.Markdown("#### 🏠 Configuration du Plateau")
|
| 1008 |
-
gr.Markdown("**Important** : L'ordre des pièces définit le plateau de jeu (circuit circulaire)")
|
| 1009 |
-
|
| 1010 |
-
rooms_input = gr.Textbox(
|
| 1011 |
-
label=f"🚪 Lieux ({settings.MIN_ROOMS}-{settings.MAX_ROOMS}) - DANS L'ORDRE",
|
| 1012 |
-
placeholder="Cuisine, Toit, Salle serveurs, Cafétéria, Bureau, Salle de réunion",
|
| 1013 |
-
lines=5,
|
| 1014 |
-
info="⚠️ L'ORDRE EST IMPORTANT ! Les joueurs se déplaceront dans cet ordre (circuit). Une ligne = une pièce.",
|
| 1015 |
-
)
|
| 1016 |
-
|
| 1017 |
-
gr.Markdown(
|
| 1018 |
-
"""
|
| 1019 |
-
💡 **Exemple de circuit** :
|
| 1020 |
-
```
|
| 1021 |
-
1. Cuisine (départ)
|
| 1022 |
-
2. Salon
|
| 1023 |
-
3. Bureau
|
| 1024 |
-
4. Chambre
|
| 1025 |
-
5. Garage
|
| 1026 |
-
6. Jardin
|
| 1027 |
-
→ retour à Cuisine (circuit fermé)
|
| 1028 |
-
```
|
| 1029 |
-
Les joueurs avancent dans cet ordre selon les dés.
|
| 1030 |
-
"""
|
| 1031 |
-
)
|
| 1032 |
-
|
| 1033 |
-
with gr.Group():
|
| 1034 |
-
gr.Markdown("#### 🎭 Éléments du Mystère")
|
| 1035 |
-
|
| 1036 |
-
suspects_input = gr.Textbox(
|
| 1037 |
-
label="👤 Suspects (min. 3)",
|
| 1038 |
-
placeholder="Claire, Pierre, Daniel, Marie, Thomas, Sophie",
|
| 1039 |
-
lines=2,
|
| 1040 |
-
info="Qui pourrait être le coupable ?",
|
| 1041 |
-
)
|
| 1042 |
-
|
| 1043 |
-
weapons_input = gr.Textbox(
|
| 1044 |
-
label="🔪 Armes (min. 3)",
|
| 1045 |
-
placeholder="Clé USB, Capsule de café, Câble HDMI, Agrafeuse, Souris d'ordinateur, Plante verte",
|
| 1046 |
-
lines=2,
|
| 1047 |
-
info="Quelle arme a été utilisée ?",
|
| 1048 |
-
)
|
| 1049 |
-
|
| 1050 |
-
use_ai_checkbox = gr.Checkbox(
|
| 1051 |
-
label="🤖 Activer le Narrateur IA - Desland (le jardinier sarcastique)",
|
| 1052 |
-
value=False,
|
| 1053 |
-
visible=settings.USE_OPENAI,
|
| 1054 |
-
info="Desland commente vos actions avec sarcasme et suspicion...",
|
| 1055 |
-
)
|
| 1056 |
-
|
| 1057 |
-
create_btn = gr.Button(
|
| 1058 |
-
"🎬 Créer l'Enquête", variant="primary", size="lg"
|
| 1059 |
-
)
|
| 1060 |
-
create_output = gr.Textbox(
|
| 1061 |
-
label="📋 Résultat", lines=5, show_copy_button=True
|
| 1062 |
-
)
|
| 1063 |
-
game_id_display = gr.Textbox(
|
| 1064 |
-
label="🔑 Code d'Enquête (à partager)",
|
| 1065 |
-
interactive=False,
|
| 1066 |
-
show_copy_button=True,
|
| 1067 |
-
)
|
| 1068 |
-
|
| 1069 |
-
create_btn.click(
|
| 1070 |
-
create_game,
|
| 1071 |
-
inputs=[
|
| 1072 |
-
game_name_input,
|
| 1073 |
-
narrative_tone_dropdown,
|
| 1074 |
-
custom_prompt_input,
|
| 1075 |
-
rooms_input,
|
| 1076 |
-
weapons_input,
|
| 1077 |
-
suspects_input,
|
| 1078 |
-
use_ai_checkbox
|
| 1079 |
-
],
|
| 1080 |
-
outputs=[create_output, game_id_display],
|
| 1081 |
-
)
|
| 1082 |
-
|
| 1083 |
-
with gr.Tab("🕵️ Rejoindre"):
|
| 1084 |
-
gr.Markdown("### 👥 Entrer sur la Scène de Crime")
|
| 1085 |
-
gr.Markdown("*Rassemblez vos confrères détectives...*")
|
| 1086 |
-
|
| 1087 |
-
with gr.Group():
|
| 1088 |
-
join_game_id = gr.Textbox(
|
| 1089 |
-
label="🔑 Code d'Enquête",
|
| 1090 |
-
placeholder="ABC123",
|
| 1091 |
-
info="Code fourni par le créateur de la partie",
|
| 1092 |
-
)
|
| 1093 |
-
|
| 1094 |
-
join_player_name = gr.Textbox(
|
| 1095 |
-
label="🎩 Nom du Détective",
|
| 1096 |
-
placeholder="Chnawax Masquée",
|
| 1097 |
-
info="Votre nom d'enquêteur",
|
| 1098 |
-
)
|
| 1099 |
-
|
| 1100 |
-
join_btn = gr.Button(
|
| 1101 |
-
"🚪 Rejoindre l'Enquête", variant="primary", size="lg"
|
| 1102 |
-
)
|
| 1103 |
-
join_output = gr.Textbox(
|
| 1104 |
-
label="📋 Statut", lines=3, show_copy_button=True
|
| 1105 |
-
)
|
| 1106 |
-
|
| 1107 |
-
join_btn.click(
|
| 1108 |
-
join_game,
|
| 1109 |
-
inputs=[join_game_id, join_player_name],
|
| 1110 |
-
outputs=join_output,
|
| 1111 |
-
)
|
| 1112 |
-
|
| 1113 |
-
gr.Markdown("---")
|
| 1114 |
-
gr.Markdown("### 🎬 Lancer l'Enquête")
|
| 1115 |
-
gr.Markdown(
|
| 1116 |
-
"*Une fois que tous les détectives sont présents (min. 3 poupouilles)*"
|
| 1117 |
-
)
|
| 1118 |
-
|
| 1119 |
-
with gr.Group():
|
| 1120 |
-
start_game_id = gr.Textbox(
|
| 1121 |
-
label="🔑 Code d'Enquête",
|
| 1122 |
-
placeholder="ABC123",
|
| 1123 |
-
info="Seul le chnawax originel (le créateur) peut démarrer la partie",
|
| 1124 |
-
)
|
| 1125 |
-
|
| 1126 |
-
start_btn = gr.Button(
|
| 1127 |
-
"⚡ Démarrer le Mystère", variant="secondary", size="lg"
|
| 1128 |
-
)
|
| 1129 |
-
start_output = gr.Textbox(label="📋 Statut", lines=2)
|
| 1130 |
-
|
| 1131 |
-
start_btn.click(start_game, inputs=start_game_id, outputs=start_output)
|
| 1132 |
-
|
| 1133 |
-
with gr.Tab("🔎 Enquêter"):
|
| 1134 |
-
gr.Markdown("### 🕹️ Tableau de Jeu")
|
| 1135 |
-
gr.Markdown("*Lancez les dés, déplacez-vous, et menez l'enquête...*")
|
| 1136 |
-
|
| 1137 |
-
with gr.Group():
|
| 1138 |
-
refresh_btn = gr.Button(
|
| 1139 |
-
"🔄 Actualiser le Dossier", size="lg", variant="secondary"
|
| 1140 |
-
)
|
| 1141 |
-
game_view = gr.Textbox(
|
| 1142 |
-
label="🗂️ État de la Partie",
|
| 1143 |
-
lines=20,
|
| 1144 |
-
max_lines=30,
|
| 1145 |
-
show_copy_button=True,
|
| 1146 |
-
info="Cliquez sur Actualiser pour voir l'état actuel",
|
| 1147 |
-
)
|
| 1148 |
-
|
| 1149 |
-
refresh_btn.click(get_player_view, outputs=game_view)
|
| 1150 |
-
|
| 1151 |
-
gr.Markdown("---")
|
| 1152 |
-
gr.Markdown("### 🎲 Votre Tour")
|
| 1153 |
-
gr.Markdown("**Étape 1 :** Lancez les dés pour vous déplacer")
|
| 1154 |
-
|
| 1155 |
-
with gr.Group():
|
| 1156 |
-
roll_btn = gr.Button(
|
| 1157 |
-
"🎲 Lancer les Dés", variant="primary", size="lg"
|
| 1158 |
-
)
|
| 1159 |
-
move_output = gr.Textbox(
|
| 1160 |
-
label="📍 Déplacement", lines=3
|
| 1161 |
-
)
|
| 1162 |
-
|
| 1163 |
-
roll_btn.click(roll_and_move, outputs=move_output)
|
| 1164 |
-
|
| 1165 |
-
gr.Markdown("---")
|
| 1166 |
-
gr.Markdown("### 💭 Faire une Suggestion")
|
| 1167 |
-
gr.Markdown("**Étape 2 :** Faites une suggestion *dans la pièce où vous êtes*")
|
| 1168 |
-
|
| 1169 |
-
with gr.Group():
|
| 1170 |
-
with gr.Row():
|
| 1171 |
-
suggest_character = gr.Dropdown(
|
| 1172 |
-
label="👤 Suspect",
|
| 1173 |
-
choices=[], # Will be populated from game
|
| 1174 |
-
info="Qui est le coupable ?",
|
| 1175 |
-
)
|
| 1176 |
-
suggest_weapon = gr.Dropdown(
|
| 1177 |
-
label="🔪 Arme",
|
| 1178 |
-
choices=[], # Will be populated from game
|
| 1179 |
-
info="Quelle arme ?",
|
| 1180 |
-
)
|
| 1181 |
-
suggest_room = gr.Dropdown(
|
| 1182 |
-
label="🚪 Lieu",
|
| 1183 |
-
choices=[], # Will be populated from game
|
| 1184 |
-
info="Dans quel lieu ?",
|
| 1185 |
-
)
|
| 1186 |
-
|
| 1187 |
-
suggest_btn = gr.Button(
|
| 1188 |
-
"💭 Faire la Suggestion", variant="primary", size="lg"
|
| 1189 |
-
)
|
| 1190 |
-
suggest_output = gr.Textbox(
|
| 1191 |
-
label="🗨️ Résultat", lines=5, show_copy_button=True
|
| 1192 |
-
)
|
| 1193 |
-
|
| 1194 |
-
suggest_btn.click(
|
| 1195 |
-
make_suggestion,
|
| 1196 |
-
inputs=[suggest_character, suggest_weapon, suggest_room],
|
| 1197 |
-
outputs=suggest_output,
|
| 1198 |
-
)
|
| 1199 |
-
|
| 1200 |
-
gr.Markdown("---")
|
| 1201 |
-
gr.Markdown("### ⚖️ Accusation Finale")
|
| 1202 |
-
gr.Markdown(
|
| 1203 |
-
"⚠️ **ATTENTION :** Une fausse accusation vous élimine !"
|
| 1204 |
-
)
|
| 1205 |
-
|
| 1206 |
-
with gr.Group():
|
| 1207 |
-
with gr.Row():
|
| 1208 |
-
accuse_character = gr.Dropdown(
|
| 1209 |
-
label="👤 Le Coupable",
|
| 1210 |
-
choices=[],
|
| 1211 |
-
info="Qui ?",
|
| 1212 |
-
)
|
| 1213 |
-
accuse_weapon = gr.Dropdown(
|
| 1214 |
-
label="🔪 L'Arme",
|
| 1215 |
-
choices=[],
|
| 1216 |
-
info="Avec quoi ?",
|
| 1217 |
-
)
|
| 1218 |
-
accuse_room = gr.Dropdown(
|
| 1219 |
-
label="🚪 Le Lieu",
|
| 1220 |
-
choices=[],
|
| 1221 |
-
info="Où ?",
|
| 1222 |
-
)
|
| 1223 |
-
|
| 1224 |
-
accuse_btn = gr.Button(
|
| 1225 |
-
"⚡ ACCUSER", variant="stop", size="lg"
|
| 1226 |
-
)
|
| 1227 |
-
accuse_output = gr.Textbox(
|
| 1228 |
-
label="⚖️ Verdict", lines=5, show_copy_button=True
|
| 1229 |
-
)
|
| 1230 |
-
|
| 1231 |
-
accuse_btn.click(
|
| 1232 |
-
make_accusation,
|
| 1233 |
-
inputs=[accuse_character, accuse_weapon, accuse_room],
|
| 1234 |
-
outputs=accuse_output,
|
| 1235 |
-
)
|
| 1236 |
-
|
| 1237 |
-
gr.Markdown("---")
|
| 1238 |
-
|
| 1239 |
-
with gr.Group():
|
| 1240 |
-
pass_btn = gr.Button(
|
| 1241 |
-
"⏭️ Passer Mon Tour", variant="secondary", size="lg"
|
| 1242 |
-
)
|
| 1243 |
-
pass_output = gr.Textbox(label="📋 Statut", lines=1)
|
| 1244 |
-
|
| 1245 |
-
pass_btn.click(pass_turn, outputs=pass_output)
|
| 1246 |
-
|
| 1247 |
-
return demo
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
def run_fastapi():
|
| 1251 |
-
"""
|
| 1252 |
-
Run FastAPI server in a separate thread.
|
| 1253 |
-
"""
|
| 1254 |
-
from api import app
|
| 1255 |
-
|
| 1256 |
-
uvicorn.run(app, host=settings.HOST, port=settings.PORT, log_level="info")
|
| 1257 |
-
|
| 1258 |
-
|
| 1259 |
-
if __name__ == "__main__":
|
| 1260 |
-
# IS_HUGGINGFACE is already defined at the top of the file
|
| 1261 |
-
|
| 1262 |
-
# Note: We're always in HUGGINGFACE mode (direct backend calls)
|
| 1263 |
-
# No need for FastAPI server
|
| 1264 |
-
|
| 1265 |
-
# Create and launch Gradio interface
|
| 1266 |
-
demo = create_gradio_interface()
|
| 1267 |
-
|
| 1268 |
-
# Launch on available port
|
| 1269 |
-
demo.launch(
|
| 1270 |
-
server_name="127.0.0.1",
|
| 1271 |
-
server_port=7862,
|
| 1272 |
-
share=False,
|
| 1273 |
-
show_error=True,
|
| 1274 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/game_engine.py
CHANGED
|
@@ -5,7 +5,7 @@ Handles game logic, card distribution, turn management, and game rules.
|
|
| 5 |
|
| 6 |
import random
|
| 7 |
from typing import List, Optional, Tuple
|
| 8 |
-
from models import Game, Card, CardType, Solution, GameStatus, Player
|
| 9 |
from datetime import datetime
|
| 10 |
|
| 11 |
|
|
|
|
| 5 |
|
| 6 |
import random
|
| 7 |
from typing import List, Optional, Tuple
|
| 8 |
+
from backend.models import Game, Card, CardType, Solution, GameStatus, Player
|
| 9 |
from datetime import datetime
|
| 10 |
|
| 11 |
|
backend/game_manager.py
CHANGED
|
@@ -6,9 +6,9 @@ Provides in-memory storage and game lifecycle management.
|
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
from typing import Dict, Optional, List
|
| 9 |
-
from models import Game, Player, CreateGameRequest, GameStatus
|
| 10 |
-
from game_engine import GameEngine
|
| 11 |
-
from config import settings
|
| 12 |
|
| 13 |
|
| 14 |
class GameManager:
|
|
|
|
| 6 |
import json
|
| 7 |
import os
|
| 8 |
from typing import Dict, Optional, List
|
| 9 |
+
from backend.models import Game, Player, CreateGameRequest, GameStatus
|
| 10 |
+
from backend.game_engine import GameEngine
|
| 11 |
+
from backend.config import settings
|
| 12 |
|
| 13 |
|
| 14 |
class GameManager:
|
backend/main.py
CHANGED
|
@@ -11,10 +11,10 @@ from pydantic import BaseModel
|
|
| 11 |
from typing import Optional, List
|
| 12 |
import os
|
| 13 |
|
| 14 |
-
from models import CreateGameRequest, GameStatus
|
| 15 |
-
from game_manager import game_manager
|
| 16 |
-
from game_engine import GameEngine
|
| 17 |
-
from defaults import get_default_game_config, DEFAULT_THEMES
|
| 18 |
|
| 19 |
app = FastAPI(title="Cluedo Custom API")
|
| 20 |
|
|
|
|
| 11 |
from typing import Optional, List
|
| 12 |
import os
|
| 13 |
|
| 14 |
+
from backend.models import CreateGameRequest, GameStatus
|
| 15 |
+
from backend.game_manager import game_manager
|
| 16 |
+
from backend.game_engine import GameEngine
|
| 17 |
+
from backend.defaults import get_default_game_config, DEFAULT_THEMES
|
| 18 |
|
| 19 |
app = FastAPI(title="Cluedo Custom API")
|
| 20 |
|
config.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Configuration module for Cluedo Custom application.
|
| 3 |
-
Loads environment variables and provides application settings.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
from dotenv import load_dotenv
|
| 8 |
-
|
| 9 |
-
# Load environment variables from .env file
|
| 10 |
-
load_dotenv()
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class Settings:
|
| 14 |
-
"""Application settings loaded from environment variables."""
|
| 15 |
-
|
| 16 |
-
# Application settings
|
| 17 |
-
APP_NAME: str = os.getenv("APP_NAME", "Cluedo Custom")
|
| 18 |
-
MAX_PLAYERS: int = int(os.getenv("MAX_PLAYERS", "8"))
|
| 19 |
-
|
| 20 |
-
# AI settings
|
| 21 |
-
USE_OPENAI: bool = os.getenv("USE_OPENAI", "false").lower() == "true"
|
| 22 |
-
OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
|
| 23 |
-
|
| 24 |
-
# Game settings
|
| 25 |
-
MIN_PLAYERS: int = 3
|
| 26 |
-
MIN_ROOMS: int = 6
|
| 27 |
-
MAX_ROOMS: int = 12
|
| 28 |
-
|
| 29 |
-
# Server settings
|
| 30 |
-
HOST: str = "0.0.0.0"
|
| 31 |
-
PORT: int = 7860
|
| 32 |
-
|
| 33 |
-
# Game data file
|
| 34 |
-
GAMES_FILE: str = "games.json"
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
settings = Settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/App.jsx
CHANGED
|
@@ -6,13 +6,11 @@ import Game from './pages/Game'
|
|
| 6 |
function App() {
|
| 7 |
return (
|
| 8 |
<BrowserRouter>
|
| 9 |
-
<
|
| 10 |
-
<
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
</Routes>
|
| 15 |
-
</div>
|
| 16 |
</BrowserRouter>
|
| 17 |
)
|
| 18 |
}
|
|
|
|
| 6 |
function App() {
|
| 7 |
return (
|
| 8 |
<BrowserRouter>
|
| 9 |
+
<Routes>
|
| 10 |
+
<Route path="/" element={<Home />} />
|
| 11 |
+
<Route path="/join" element={<Join />} />
|
| 12 |
+
<Route path="/game/:gameId/:playerId" element={<Game />} />
|
| 13 |
+
</Routes>
|
|
|
|
|
|
|
| 14 |
</BrowserRouter>
|
| 15 |
)
|
| 16 |
}
|
frontend/src/pages/Game.jsx
CHANGED
|
@@ -107,8 +107,8 @@ function Game() {
|
|
| 107 |
|
| 108 |
if (loading) {
|
| 109 |
return (
|
| 110 |
-
<div className="min-h-screen flex items-center justify-center">
|
| 111 |
-
<div className="text-
|
| 112 |
</div>
|
| 113 |
)
|
| 114 |
}
|
|
@@ -118,43 +118,52 @@ function Game() {
|
|
| 118 |
const canStart = me?.is_creator && gameState.status === 'waiting' && gameState.players.length >= 3
|
| 119 |
|
| 120 |
return (
|
| 121 |
-
<div className="min-h-screen p-4">
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
| 123 |
{/* Header */}
|
| 124 |
-
<div className="bg-
|
| 125 |
<div className="flex justify-between items-center">
|
| 126 |
<div>
|
| 127 |
-
<h1 className="text-
|
| 128 |
-
|
|
|
|
|
|
|
| 129 |
</div>
|
| 130 |
<div className="text-right">
|
| 131 |
-
<p className="text-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
</div>
|
| 134 |
</div>
|
| 135 |
{canStart && (
|
| 136 |
<button
|
| 137 |
onClick={handleStartGame}
|
| 138 |
disabled={actionLoading}
|
| 139 |
-
className="mt-4 px-6 py-2 bg-
|
| 140 |
>
|
| 141 |
-
🚀
|
| 142 |
</button>
|
| 143 |
)}
|
| 144 |
</div>
|
| 145 |
|
| 146 |
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 147 |
{/* Plateau */}
|
| 148 |
-
<div className="bg-
|
| 149 |
-
<h2 className="text-xl font-bold text-
|
| 150 |
<div className="space-y-2">
|
| 151 |
{gameState.board?.rooms.map((room, idx) => (
|
| 152 |
-
<div key={idx} className="bg-
|
| 153 |
-
<div className="font-semibold text-
|
| 154 |
-
<div className="text-sm text-
|
| 155 |
-
|
| 156 |
gameState.players.find(p => p.id === id)?.name || id
|
| 157 |
-
).join(', ') || '
|
| 158 |
</div>
|
| 159 |
</div>
|
| 160 |
))}
|
|
@@ -162,14 +171,14 @@ function Game() {
|
|
| 162 |
</div>
|
| 163 |
|
| 164 |
{/* Mes cartes */}
|
| 165 |
-
<div className="bg-
|
| 166 |
-
<h2 className="text-xl font-bold text-
|
| 167 |
<div className="space-y-2">
|
| 168 |
{me?.cards.map((card, idx) => (
|
| 169 |
-
<div key={idx} className="bg-
|
| 170 |
{card.type === 'suspect' && '👤 '}
|
| 171 |
{card.type === 'weapon' && '🔪 '}
|
| 172 |
-
{card.type === 'room' && '
|
| 173 |
{card.name}
|
| 174 |
</div>
|
| 175 |
))}
|
|
@@ -179,9 +188,9 @@ function Game() {
|
|
| 179 |
|
| 180 |
{/* Actions */}
|
| 181 |
{gameState.status === 'playing' && (
|
| 182 |
-
<div className="bg-
|
| 183 |
-
<h2 className="text-
|
| 184 |
-
{isMyTurn ? '⚡ À
|
| 185 |
</h2>
|
| 186 |
|
| 187 |
{isMyTurn && (
|
|
@@ -191,14 +200,14 @@ function Game() {
|
|
| 191 |
<button
|
| 192 |
onClick={handleRollDice}
|
| 193 |
disabled={actionLoading}
|
| 194 |
-
className="px-6 py-3 bg-
|
| 195 |
>
|
| 196 |
🎲 Lancer les dés
|
| 197 |
</button>
|
| 198 |
<button
|
| 199 |
onClick={handlePassTurn}
|
| 200 |
disabled={actionLoading}
|
| 201 |
-
className="px-6 py-3 bg-
|
| 202 |
>
|
| 203 |
⏭️ Passer
|
| 204 |
</button>
|
|
@@ -207,11 +216,11 @@ function Game() {
|
|
| 207 |
<div className="space-y-4">
|
| 208 |
<div className="grid grid-cols-3 gap-4">
|
| 209 |
<div>
|
| 210 |
-
<label className="block text-sm text-
|
| 211 |
<select
|
| 212 |
value={selectedSuspect}
|
| 213 |
onChange={(e) => setSelectedSuspect(e.target.value)}
|
| 214 |
-
className="w-full px-3 py-2 bg-
|
| 215 |
>
|
| 216 |
<option value="">--</option>
|
| 217 |
{gameState.board?.suspects.map((s, i) => (
|
|
@@ -220,11 +229,11 @@ function Game() {
|
|
| 220 |
</select>
|
| 221 |
</div>
|
| 222 |
<div>
|
| 223 |
-
<label className="block text-sm text-
|
| 224 |
<select
|
| 225 |
value={selectedWeapon}
|
| 226 |
onChange={(e) => setSelectedWeapon(e.target.value)}
|
| 227 |
-
className="w-full px-3 py-2 bg-
|
| 228 |
>
|
| 229 |
<option value="">--</option>
|
| 230 |
{gameState.board?.weapons.map((w, i) => (
|
|
@@ -233,11 +242,11 @@ function Game() {
|
|
| 233 |
</select>
|
| 234 |
</div>
|
| 235 |
<div>
|
| 236 |
-
<label className="block text-sm text-
|
| 237 |
<select
|
| 238 |
value={selectedRoom}
|
| 239 |
onChange={(e) => setSelectedRoom(e.target.value)}
|
| 240 |
-
className="w-full px-3 py-2 bg-
|
| 241 |
>
|
| 242 |
<option value="">--</option>
|
| 243 |
{gameState.board?.rooms.map((r, i) => (
|
|
@@ -250,21 +259,21 @@ function Game() {
|
|
| 250 |
<button
|
| 251 |
onClick={handleSuggestion}
|
| 252 |
disabled={actionLoading}
|
| 253 |
-
className="px-6 py-3 bg-
|
| 254 |
>
|
| 255 |
💬 Suggérer
|
| 256 |
</button>
|
| 257 |
<button
|
| 258 |
onClick={handleAccusation}
|
| 259 |
disabled={actionLoading}
|
| 260 |
-
className="px-6 py-3 bg-
|
| 261 |
>
|
| 262 |
⚠️ Accuser
|
| 263 |
</button>
|
| 264 |
<button
|
| 265 |
onClick={handlePassTurn}
|
| 266 |
disabled={actionLoading}
|
| 267 |
-
className="px-6 py-3 bg-
|
| 268 |
>
|
| 269 |
⏭️ Passer
|
| 270 |
</button>
|
|
@@ -277,11 +286,11 @@ function Game() {
|
|
| 277 |
)}
|
| 278 |
|
| 279 |
{/* Historique */}
|
| 280 |
-
<div className="bg-
|
| 281 |
-
<h2 className="text-xl font-bold text-
|
| 282 |
-
<div className="space-y-2">
|
| 283 |
{gameState.history?.slice(-10).reverse().map((event, idx) => (
|
| 284 |
-
<div key={idx} className="text-
|
| 285 |
{event}
|
| 286 |
</div>
|
| 287 |
))}
|
|
|
|
| 107 |
|
| 108 |
if (loading) {
|
| 109 |
return (
|
| 110 |
+
<div className="min-h-screen bg-haunted-gradient flex items-center justify-center">
|
| 111 |
+
<div className="text-haunted-blood text-2xl animate-flicker">🕯️ Chargement des ténèbres...</div>
|
| 112 |
</div>
|
| 113 |
)
|
| 114 |
}
|
|
|
|
| 118 |
const canStart = me?.is_creator && gameState.status === 'waiting' && gameState.players.length >= 3
|
| 119 |
|
| 120 |
return (
|
| 121 |
+
<div className="min-h-screen bg-haunted-gradient p-4 relative overflow-hidden">
|
| 122 |
+
{/* Animated fog effect */}
|
| 123 |
+
<div className="absolute inset-0 bg-fog-gradient opacity-10 animate-pulse-slow pointer-events-none"></div>
|
| 124 |
+
|
| 125 |
+
<div className="max-w-6xl mx-auto space-y-6 relative z-10">
|
| 126 |
{/* Header */}
|
| 127 |
+
<div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-blood/30 shadow-[0_0_30px_rgba(139,0,0,0.2)]">
|
| 128 |
<div className="flex justify-between items-center">
|
| 129 |
<div>
|
| 130 |
+
<h1 className="text-4xl font-bold text-haunted-blood mb-1 animate-flicker drop-shadow-[0_0_10px_rgba(139,0,0,0.5)]">
|
| 131 |
+
🏰 Manoir {gameState.game_code}
|
| 132 |
+
</h1>
|
| 133 |
+
<p className="text-haunted-fog/80">👤 {me?.name} {me?.is_eliminated && '💀 (Éliminé)'}</p>
|
| 134 |
</div>
|
| 135 |
<div className="text-right">
|
| 136 |
+
<p className="text-haunted-fog">
|
| 137 |
+
{gameState.status === 'waiting' ? '⏳ En attente des âmes' :
|
| 138 |
+
gameState.status === 'playing' ? '🎮 Enquête en cours' :
|
| 139 |
+
'🏆 Mystère résolu'}
|
| 140 |
+
</p>
|
| 141 |
+
<p className="text-haunted-fog/60 text-sm">{gameState.players.length} âmes perdues</p>
|
| 142 |
</div>
|
| 143 |
</div>
|
| 144 |
{canStart && (
|
| 145 |
<button
|
| 146 |
onClick={handleStartGame}
|
| 147 |
disabled={actionLoading}
|
| 148 |
+
className="mt-4 px-6 py-2 bg-haunted-blood hover:bg-red-800 disabled:bg-dark-600 disabled:opacity-50 text-white font-bold rounded-lg transition-all hover:shadow-[0_0_20px_rgba(139,0,0,0.5)] border border-red-900"
|
| 149 |
>
|
| 150 |
+
🚀 Commencer l'enquête
|
| 151 |
</button>
|
| 152 |
)}
|
| 153 |
</div>
|
| 154 |
|
| 155 |
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 156 |
{/* Plateau */}
|
| 157 |
+
<div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-shadow">
|
| 158 |
+
<h2 className="text-xl font-bold text-haunted-blood mb-4 animate-flicker">🏰 Les Pièces du Manoir</h2>
|
| 159 |
<div className="space-y-2">
|
| 160 |
{gameState.board?.rooms.map((room, idx) => (
|
| 161 |
+
<div key={idx} className="bg-black/40 p-3 rounded border border-haunted-shadow hover:border-haunted-blood/50 transition-all">
|
| 162 |
+
<div className="font-semibold text-haunted-fog">{room.name}</div>
|
| 163 |
+
<div className="text-sm text-haunted-fog/60">
|
| 164 |
+
👥 {room.player_ids.map(id =>
|
| 165 |
gameState.players.find(p => p.id === id)?.name || id
|
| 166 |
+
).join(', ') || 'Aucune âme'}
|
| 167 |
</div>
|
| 168 |
</div>
|
| 169 |
))}
|
|
|
|
| 171 |
</div>
|
| 172 |
|
| 173 |
{/* Mes cartes */}
|
| 174 |
+
<div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-shadow">
|
| 175 |
+
<h2 className="text-xl font-bold text-haunted-blood mb-4 animate-flicker">🃏 Vos Indices</h2>
|
| 176 |
<div className="space-y-2">
|
| 177 |
{me?.cards.map((card, idx) => (
|
| 178 |
+
<div key={idx} className="bg-black/40 px-4 py-2 rounded text-haunted-fog border border-haunted-shadow hover:border-haunted-blood/50 transition-all">
|
| 179 |
{card.type === 'suspect' && '👤 '}
|
| 180 |
{card.type === 'weapon' && '🔪 '}
|
| 181 |
+
{card.type === 'room' && '🏚️ '}
|
| 182 |
{card.name}
|
| 183 |
</div>
|
| 184 |
))}
|
|
|
|
| 188 |
|
| 189 |
{/* Actions */}
|
| 190 |
{gameState.status === 'playing' && (
|
| 191 |
+
<div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-blood/30">
|
| 192 |
+
<h2 className="text-2xl font-bold text-haunted-blood mb-4 animate-flicker">
|
| 193 |
+
{isMyTurn ? '⚡ À vous de jouer !' : '⏳ ' + gameState.players.find(p => p.id === gameState.current_player_id)?.name + ' enquête...'}
|
| 194 |
</h2>
|
| 195 |
|
| 196 |
{isMyTurn && (
|
|
|
|
| 200 |
<button
|
| 201 |
onClick={handleRollDice}
|
| 202 |
disabled={actionLoading}
|
| 203 |
+
className="px-6 py-3 bg-haunted-blood hover:bg-red-800 disabled:bg-dark-600 disabled:opacity-50 text-white font-bold rounded-lg transition-all hover:shadow-[0_0_20px_rgba(139,0,0,0.5)] border border-red-900"
|
| 204 |
>
|
| 205 |
🎲 Lancer les dés
|
| 206 |
</button>
|
| 207 |
<button
|
| 208 |
onClick={handlePassTurn}
|
| 209 |
disabled={actionLoading}
|
| 210 |
+
className="px-6 py-3 bg-black/40 hover:bg-black/60 disabled:opacity-50 text-haunted-fog border-2 border-haunted-shadow font-semibold rounded-lg transition-all"
|
| 211 |
>
|
| 212 |
⏭️ Passer
|
| 213 |
</button>
|
|
|
|
| 216 |
<div className="space-y-4">
|
| 217 |
<div className="grid grid-cols-3 gap-4">
|
| 218 |
<div>
|
| 219 |
+
<label className="block text-sm text-haunted-fog mb-2">👤 Suspect</label>
|
| 220 |
<select
|
| 221 |
value={selectedSuspect}
|
| 222 |
onChange={(e) => setSelectedSuspect(e.target.value)}
|
| 223 |
+
className="w-full px-3 py-2 bg-black/40 text-haunted-fog rounded border-2 border-haunted-shadow focus:border-haunted-blood focus:outline-none"
|
| 224 |
>
|
| 225 |
<option value="">--</option>
|
| 226 |
{gameState.board?.suspects.map((s, i) => (
|
|
|
|
| 229 |
</select>
|
| 230 |
</div>
|
| 231 |
<div>
|
| 232 |
+
<label className="block text-sm text-haunted-fog mb-2">🔪 Arme</label>
|
| 233 |
<select
|
| 234 |
value={selectedWeapon}
|
| 235 |
onChange={(e) => setSelectedWeapon(e.target.value)}
|
| 236 |
+
className="w-full px-3 py-2 bg-black/40 text-haunted-fog rounded border-2 border-haunted-shadow focus:border-haunted-blood focus:outline-none"
|
| 237 |
>
|
| 238 |
<option value="">--</option>
|
| 239 |
{gameState.board?.weapons.map((w, i) => (
|
|
|
|
| 242 |
</select>
|
| 243 |
</div>
|
| 244 |
<div>
|
| 245 |
+
<label className="block text-sm text-haunted-fog mb-2">🏚️ Pièce</label>
|
| 246 |
<select
|
| 247 |
value={selectedRoom}
|
| 248 |
onChange={(e) => setSelectedRoom(e.target.value)}
|
| 249 |
+
className="w-full px-3 py-2 bg-black/40 text-haunted-fog rounded border-2 border-haunted-shadow focus:border-haunted-blood focus:outline-none"
|
| 250 |
>
|
| 251 |
<option value="">--</option>
|
| 252 |
{gameState.board?.rooms.map((r, i) => (
|
|
|
|
| 259 |
<button
|
| 260 |
onClick={handleSuggestion}
|
| 261 |
disabled={actionLoading}
|
| 262 |
+
className="px-6 py-3 bg-haunted-purple hover:bg-purple-800 disabled:bg-dark-600 disabled:opacity-50 text-white font-bold rounded-lg transition-all hover:shadow-[0_0_20px_rgba(107,33,168,0.5)] border border-purple-900"
|
| 263 |
>
|
| 264 |
💬 Suggérer
|
| 265 |
</button>
|
| 266 |
<button
|
| 267 |
onClick={handleAccusation}
|
| 268 |
disabled={actionLoading}
|
| 269 |
+
className="px-6 py-3 bg-haunted-blood hover:bg-red-800 disabled:bg-dark-600 disabled:opacity-50 text-white font-bold rounded-lg transition-all hover:shadow-[0_0_20px_rgba(139,0,0,0.5)] border border-red-900"
|
| 270 |
>
|
| 271 |
⚠️ Accuser
|
| 272 |
</button>
|
| 273 |
<button
|
| 274 |
onClick={handlePassTurn}
|
| 275 |
disabled={actionLoading}
|
| 276 |
+
className="px-6 py-3 bg-black/40 hover:bg-black/60 disabled:opacity-50 text-haunted-fog border-2 border-haunted-shadow font-semibold rounded-lg transition-all"
|
| 277 |
>
|
| 278 |
⏭️ Passer
|
| 279 |
</button>
|
|
|
|
| 286 |
)}
|
| 287 |
|
| 288 |
{/* Historique */}
|
| 289 |
+
<div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-shadow">
|
| 290 |
+
<h2 className="text-xl font-bold text-haunted-blood mb-4 animate-flicker">📜 Journal de l'Enquête</h2>
|
| 291 |
+
<div className="space-y-2 max-h-64 overflow-y-auto">
|
| 292 |
{gameState.history?.slice(-10).reverse().map((event, idx) => (
|
| 293 |
+
<div key={idx} className="text-haunted-fog/80 text-sm border-l-2 border-haunted-blood pl-3 py-1 hover:bg-black/20 transition-all">
|
| 294 |
{event}
|
| 295 |
</div>
|
| 296 |
))}
|
frontend/src/pages/Home.jsx
CHANGED
|
@@ -26,16 +26,26 @@ function Home() {
|
|
| 26 |
}
|
| 27 |
|
| 28 |
return (
|
| 29 |
-
<div className="min-h-screen flex items-center justify-center p-4">
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
<div className="text-center">
|
| 32 |
-
<h1 className="text-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
</div>
|
| 35 |
|
| 36 |
<div className="space-y-4">
|
| 37 |
<div>
|
| 38 |
-
<label className="block text-sm font-medium text-
|
| 39 |
Votre nom
|
| 40 |
</label>
|
| 41 |
<input
|
|
@@ -43,32 +53,32 @@ function Home() {
|
|
| 43 |
value={playerName}
|
| 44 |
onChange={(e) => setPlayerName(e.target.value)}
|
| 45 |
onKeyPress={(e) => e.key === 'Enter' && handleCreateGame()}
|
| 46 |
-
placeholder="
|
| 47 |
-
className="w-full px-4 py-3 bg-
|
| 48 |
/>
|
| 49 |
</div>
|
| 50 |
|
| 51 |
<button
|
| 52 |
onClick={handleCreateGame}
|
| 53 |
disabled={loading}
|
| 54 |
-
className="w-full py-3 px-4 bg-
|
| 55 |
>
|
| 56 |
-
{loading ? 'Création...' : '
|
| 57 |
</button>
|
| 58 |
|
| 59 |
<div className="text-center pt-4">
|
| 60 |
<button
|
| 61 |
onClick={() => navigate('/join')}
|
| 62 |
-
className="text-
|
| 63 |
>
|
| 64 |
-
Rejoindre une partie existante
|
| 65 |
</button>
|
| 66 |
</div>
|
| 67 |
</div>
|
| 68 |
|
| 69 |
-
<div className="mt-8 pt-6 border-t border-
|
| 70 |
-
<p
|
| 71 |
-
<p className="mt-1">3-6
|
| 72 |
</div>
|
| 73 |
</div>
|
| 74 |
</div>
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
return (
|
| 29 |
+
<div className="min-h-screen bg-haunted-gradient flex items-center justify-center p-4 relative overflow-hidden">
|
| 30 |
+
{/* Animated fog effect */}
|
| 31 |
+
<div className="absolute inset-0 bg-fog-gradient opacity-20 animate-pulse-slow pointer-events-none"></div>
|
| 32 |
+
|
| 33 |
+
{/* Floating ghost elements */}
|
| 34 |
+
<div className="absolute top-20 left-10 w-32 h-32 bg-haunted-ghost opacity-5 rounded-full blur-3xl animate-float"></div>
|
| 35 |
+
<div className="absolute bottom-20 right-10 w-40 h-40 bg-haunted-purple opacity-5 rounded-full blur-3xl animate-float" style={{animationDelay: '2s'}}></div>
|
| 36 |
+
|
| 37 |
+
<div className="max-w-md w-full space-y-8 bg-black/60 backdrop-blur-md p-8 rounded-lg shadow-2xl border-2 border-haunted-blood/30 animate-fade-in relative z-10">
|
| 38 |
<div className="text-center">
|
| 39 |
+
<h1 className="text-6xl font-bold text-haunted-blood mb-2 animate-flicker drop-shadow-[0_0_10px_rgba(139,0,0,0.5)]">
|
| 40 |
+
🕯️ Cluedo Custom
|
| 41 |
+
</h1>
|
| 42 |
+
<p className="text-haunted-fog/80 text-lg italic">Le manoir vous attend dans l'obscurité...</p>
|
| 43 |
+
<div className="mt-2 text-xs text-haunted-ghost/50">💀 Osez-vous entrer ? 💀</div>
|
| 44 |
</div>
|
| 45 |
|
| 46 |
<div className="space-y-4">
|
| 47 |
<div>
|
| 48 |
+
<label className="block text-sm font-medium text-haunted-fog mb-2">
|
| 49 |
Votre nom
|
| 50 |
</label>
|
| 51 |
<input
|
|
|
|
| 53 |
value={playerName}
|
| 54 |
onChange={(e) => setPlayerName(e.target.value)}
|
| 55 |
onKeyPress={(e) => e.key === 'Enter' && handleCreateGame()}
|
| 56 |
+
placeholder="Qui ose s'aventurer..."
|
| 57 |
+
className="w-full px-4 py-3 bg-black/40 border-2 border-haunted-shadow rounded-lg text-haunted-fog placeholder-dark-500 focus:outline-none focus:ring-2 focus:ring-haunted-blood focus:border-haunted-blood transition-all"
|
| 58 |
/>
|
| 59 |
</div>
|
| 60 |
|
| 61 |
<button
|
| 62 |
onClick={handleCreateGame}
|
| 63 |
disabled={loading}
|
| 64 |
+
className="w-full py-3 px-4 bg-haunted-blood hover:bg-red-800 disabled:bg-dark-600 disabled:opacity-50 text-white font-bold rounded-lg transition-all transform hover:scale-105 hover:shadow-[0_0_20px_rgba(139,0,0,0.5)] border border-red-900"
|
| 65 |
>
|
| 66 |
+
{loading ? '🕯️ Création...' : '🚪 Entrer dans le Manoir'}
|
| 67 |
</button>
|
| 68 |
|
| 69 |
<div className="text-center pt-4">
|
| 70 |
<button
|
| 71 |
onClick={() => navigate('/join')}
|
| 72 |
+
className="text-haunted-fog/70 hover:text-haunted-blood underline transition-colors"
|
| 73 |
>
|
| 74 |
+
👻 Rejoindre une partie existante
|
| 75 |
</button>
|
| 76 |
</div>
|
| 77 |
</div>
|
| 78 |
|
| 79 |
+
<div className="mt-8 pt-6 border-t border-haunted-shadow text-center text-sm text-haunted-fog/60">
|
| 80 |
+
<p className="italic">⚰️ Thème : Meurtre au Manoir ⚰️</p>
|
| 81 |
+
<p className="mt-1">3-6 âmes perdues recommandées</p>
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
</div>
|
frontend/src/pages/Join.jsx
CHANGED
|
@@ -27,30 +27,39 @@ function Join() {
|
|
| 27 |
}
|
| 28 |
|
| 29 |
return (
|
| 30 |
-
<div className="min-h-screen flex items-center justify-center p-4">
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
<div className="text-center">
|
| 33 |
-
<h1 className="text-
|
| 34 |
-
|
|
|
|
|
|
|
| 35 |
</div>
|
| 36 |
|
| 37 |
<div className="space-y-4">
|
| 38 |
<div>
|
| 39 |
-
<label className="block text-sm font-medium text-
|
| 40 |
-
Code de partie
|
| 41 |
</label>
|
| 42 |
<input
|
| 43 |
type="text"
|
| 44 |
value={gameCode}
|
| 45 |
onChange={(e) => setGameCode(e.target.value.toUpperCase())}
|
| 46 |
-
placeholder="
|
| 47 |
maxLength={4}
|
| 48 |
-
className="w-full px-4 py-3 bg-
|
| 49 |
/>
|
| 50 |
</div>
|
| 51 |
|
| 52 |
<div>
|
| 53 |
-
<label className="block text-sm font-medium text-
|
| 54 |
Votre nom
|
| 55 |
</label>
|
| 56 |
<input
|
|
@@ -58,25 +67,25 @@ function Join() {
|
|
| 58 |
value={playerName}
|
| 59 |
onChange={(e) => setPlayerName(e.target.value)}
|
| 60 |
onKeyPress={(e) => e.key === 'Enter' && handleJoinGame()}
|
| 61 |
-
placeholder="
|
| 62 |
-
className="w-full px-4 py-3 bg-
|
| 63 |
/>
|
| 64 |
</div>
|
| 65 |
|
| 66 |
<button
|
| 67 |
onClick={handleJoinGame}
|
| 68 |
disabled={loading}
|
| 69 |
-
className="w-full py-3 px-4 bg-
|
| 70 |
>
|
| 71 |
-
{loading ? 'Connexion...' : '
|
| 72 |
</button>
|
| 73 |
|
| 74 |
<div className="text-center pt-4">
|
| 75 |
<button
|
| 76 |
onClick={() => navigate('/')}
|
| 77 |
-
className="text-
|
| 78 |
>
|
| 79 |
-
←
|
| 80 |
</button>
|
| 81 |
</div>
|
| 82 |
</div>
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
return (
|
| 30 |
+
<div className="min-h-screen bg-haunted-gradient flex items-center justify-center p-4 relative overflow-hidden">
|
| 31 |
+
{/* Animated fog effect */}
|
| 32 |
+
<div className="absolute inset-0 bg-fog-gradient opacity-20 animate-pulse-slow pointer-events-none"></div>
|
| 33 |
+
|
| 34 |
+
{/* Floating ghost elements */}
|
| 35 |
+
<div className="absolute top-20 left-10 w-32 h-32 bg-haunted-ghost opacity-5 rounded-full blur-3xl animate-float"></div>
|
| 36 |
+
<div className="absolute bottom-20 right-10 w-40 h-40 bg-haunted-purple opacity-5 rounded-full blur-3xl animate-float" style={{animationDelay: '2s'}}></div>
|
| 37 |
+
|
| 38 |
+
<div className="max-w-md w-full space-y-8 bg-black/60 backdrop-blur-md p-8 rounded-lg shadow-2xl border-2 border-haunted-blood/30 animate-fade-in relative z-10">
|
| 39 |
<div className="text-center">
|
| 40 |
+
<h1 className="text-5xl font-bold text-haunted-blood mb-2 animate-flicker drop-shadow-[0_0_10px_rgba(139,0,0,0.5)]">
|
| 41 |
+
👻 Rejoindre la Séance
|
| 42 |
+
</h1>
|
| 43 |
+
<p className="text-haunted-fog/80 italic">Entrez le code maudit...</p>
|
| 44 |
</div>
|
| 45 |
|
| 46 |
<div className="space-y-4">
|
| 47 |
<div>
|
| 48 |
+
<label className="block text-sm font-medium text-haunted-fog mb-2">
|
| 49 |
+
🔮 Code de partie
|
| 50 |
</label>
|
| 51 |
<input
|
| 52 |
type="text"
|
| 53 |
value={gameCode}
|
| 54 |
onChange={(e) => setGameCode(e.target.value.toUpperCase())}
|
| 55 |
+
placeholder="?????"
|
| 56 |
maxLength={4}
|
| 57 |
+
className="w-full px-4 py-3 bg-black/40 border-2 border-haunted-shadow rounded-lg text-haunted-blood text-center text-3xl font-mono placeholder-dark-500 focus:outline-none focus:ring-2 focus:ring-haunted-blood focus:border-haunted-blood transition-all uppercase tracking-widest"
|
| 58 |
/>
|
| 59 |
</div>
|
| 60 |
|
| 61 |
<div>
|
| 62 |
+
<label className="block text-sm font-medium text-haunted-fog mb-2">
|
| 63 |
Votre nom
|
| 64 |
</label>
|
| 65 |
<input
|
|
|
|
| 67 |
value={playerName}
|
| 68 |
onChange={(e) => setPlayerName(e.target.value)}
|
| 69 |
onKeyPress={(e) => e.key === 'Enter' && handleJoinGame()}
|
| 70 |
+
placeholder="Qui ose s'aventurer..."
|
| 71 |
+
className="w-full px-4 py-3 bg-black/40 border-2 border-haunted-shadow rounded-lg text-haunted-fog placeholder-dark-500 focus:outline-none focus:ring-2 focus:ring-haunted-blood focus:border-haunted-blood transition-all"
|
| 72 |
/>
|
| 73 |
</div>
|
| 74 |
|
| 75 |
<button
|
| 76 |
onClick={handleJoinGame}
|
| 77 |
disabled={loading}
|
| 78 |
+
className="w-full py-3 px-4 bg-haunted-blood hover:bg-red-800 disabled:bg-dark-600 disabled:opacity-50 text-white font-bold rounded-lg transition-all transform hover:scale-105 hover:shadow-[0_0_20px_rgba(139,0,0,0.5)] border border-red-900"
|
| 79 |
>
|
| 80 |
+
{loading ? '🕯️ Connexion...' : '🚪 Entrer dans le Manoir'}
|
| 81 |
</button>
|
| 82 |
|
| 83 |
<div className="text-center pt-4">
|
| 84 |
<button
|
| 85 |
onClick={() => navigate('/')}
|
| 86 |
+
className="text-haunted-fog/70 hover:text-haunted-blood underline transition-colors"
|
| 87 |
>
|
| 88 |
+
← Retourner dans les ténèbres
|
| 89 |
</button>
|
| 90 |
</div>
|
| 91 |
</div>
|
frontend/tailwind.config.js
CHANGED
|
@@ -32,6 +32,45 @@ export default {
|
|
| 32 |
900: '#0f172a',
|
| 33 |
950: '#020617',
|
| 34 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
},
|
| 36 |
},
|
| 37 |
},
|
|
|
|
| 32 |
900: '#0f172a',
|
| 33 |
950: '#020617',
|
| 34 |
},
|
| 35 |
+
accent: {
|
| 36 |
+
300: '#fca5a5',
|
| 37 |
+
400: '#f87171',
|
| 38 |
+
500: '#ef4444',
|
| 39 |
+
600: '#dc2626',
|
| 40 |
+
700: '#b91c1c',
|
| 41 |
+
},
|
| 42 |
+
haunted: {
|
| 43 |
+
fog: '#e5e7eb',
|
| 44 |
+
shadow: '#1f1f1f',
|
| 45 |
+
blood: '#8b0000',
|
| 46 |
+
ghost: '#f3f4f6',
|
| 47 |
+
purple: '#6b21a8',
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
backgroundImage: {
|
| 51 |
+
'haunted-gradient': 'linear-gradient(135deg, #0a0a0a 0%, #1a0a14 50%, #0a0a0a 100%)',
|
| 52 |
+
'fog-gradient': 'linear-gradient(to bottom, transparent, rgba(255,255,255,0.05), transparent)',
|
| 53 |
+
},
|
| 54 |
+
animation: {
|
| 55 |
+
'flicker': 'flicker 3s infinite',
|
| 56 |
+
'float': 'float 6s ease-in-out infinite',
|
| 57 |
+
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 58 |
+
'fade-in': 'fadeIn 0.5s ease-in',
|
| 59 |
+
},
|
| 60 |
+
keyframes: {
|
| 61 |
+
flicker: {
|
| 62 |
+
'0%, 100%': { opacity: '1' },
|
| 63 |
+
'50%': { opacity: '0.8' },
|
| 64 |
+
'75%': { opacity: '0.9' },
|
| 65 |
+
},
|
| 66 |
+
float: {
|
| 67 |
+
'0%, 100%': { transform: 'translateY(0px)' },
|
| 68 |
+
'50%': { transform: 'translateY(-10px)' },
|
| 69 |
+
},
|
| 70 |
+
fadeIn: {
|
| 71 |
+
'0%': { opacity: '0' },
|
| 72 |
+
'100%': { opacity: '1' },
|
| 73 |
+
},
|
| 74 |
},
|
| 75 |
},
|
| 76 |
},
|
game_engine.py
DELETED
|
@@ -1,294 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Game engine for Cluedo Custom.
|
| 3 |
-
Handles game logic, card distribution, turn management, and game rules.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import random
|
| 7 |
-
from typing import List, Optional, Tuple
|
| 8 |
-
from models import Game, Card, CardType, Solution, GameStatus, Player
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
# Default character names (fallback if no custom suspects provided)
|
| 13 |
-
DEFAULT_CHARACTERS = [
|
| 14 |
-
"Miss Scarlett",
|
| 15 |
-
"Colonel Mustard",
|
| 16 |
-
"Mrs. White",
|
| 17 |
-
"Reverend Green",
|
| 18 |
-
"Mrs. Peacock",
|
| 19 |
-
"Professor Plum"
|
| 20 |
-
]
|
| 21 |
-
|
| 22 |
-
# Default weapon names (fallback if no custom weapons provided)
|
| 23 |
-
DEFAULT_WEAPONS = [
|
| 24 |
-
"Candlestick",
|
| 25 |
-
"Knife",
|
| 26 |
-
"Lead Pipe",
|
| 27 |
-
"Revolver",
|
| 28 |
-
"Rope",
|
| 29 |
-
"Wrench"
|
| 30 |
-
]
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
class GameEngine:
|
| 34 |
-
"""Core game logic engine for Cluedo Custom."""
|
| 35 |
-
|
| 36 |
-
@staticmethod
|
| 37 |
-
def initialize_game(game: Game) -> Game:
|
| 38 |
-
"""
|
| 39 |
-
Initialize a game with cards and solution.
|
| 40 |
-
Distributes cards among players after setting aside the solution.
|
| 41 |
-
"""
|
| 42 |
-
# Use custom suspects or defaults
|
| 43 |
-
suspects = game.custom_suspects if game.custom_suspects else DEFAULT_CHARACTERS
|
| 44 |
-
weapons = game.custom_weapons if game.custom_weapons else DEFAULT_WEAPONS
|
| 45 |
-
|
| 46 |
-
# Create character cards
|
| 47 |
-
game.characters = [
|
| 48 |
-
Card(name=name, card_type=CardType.CHARACTER)
|
| 49 |
-
for name in suspects
|
| 50 |
-
]
|
| 51 |
-
|
| 52 |
-
# Create weapon cards
|
| 53 |
-
game.weapons = [
|
| 54 |
-
Card(name=name, card_type=CardType.WEAPON)
|
| 55 |
-
for name in weapons
|
| 56 |
-
]
|
| 57 |
-
|
| 58 |
-
# Create room cards
|
| 59 |
-
game.room_cards = [
|
| 60 |
-
Card(name=room, card_type=CardType.ROOM)
|
| 61 |
-
for room in game.rooms
|
| 62 |
-
]
|
| 63 |
-
|
| 64 |
-
# Select solution (one of each type)
|
| 65 |
-
solution_character = random.choice(game.characters)
|
| 66 |
-
solution_weapon = random.choice(game.weapons)
|
| 67 |
-
solution_room = random.choice(game.room_cards)
|
| 68 |
-
|
| 69 |
-
game.solution = Solution(
|
| 70 |
-
character=solution_character,
|
| 71 |
-
weapon=solution_weapon,
|
| 72 |
-
room=solution_room
|
| 73 |
-
)
|
| 74 |
-
|
| 75 |
-
# Remaining cards to distribute
|
| 76 |
-
remaining_cards = []
|
| 77 |
-
remaining_cards.extend([c for c in game.characters if c.name != solution_character.name])
|
| 78 |
-
remaining_cards.extend([w for w in game.weapons if w.name != solution_weapon.name])
|
| 79 |
-
remaining_cards.extend([r for r in game.room_cards if r.name != solution_room.name])
|
| 80 |
-
|
| 81 |
-
# Shuffle and distribute
|
| 82 |
-
random.shuffle(remaining_cards)
|
| 83 |
-
GameEngine._distribute_cards(game, remaining_cards)
|
| 84 |
-
|
| 85 |
-
# Set game status to in progress
|
| 86 |
-
game.status = GameStatus.IN_PROGRESS
|
| 87 |
-
game.current_player_index = 0
|
| 88 |
-
|
| 89 |
-
return game
|
| 90 |
-
|
| 91 |
-
@staticmethod
|
| 92 |
-
def _distribute_cards(game: Game, cards: List[Card]):
|
| 93 |
-
"""
|
| 94 |
-
Distribute cards evenly among all players.
|
| 95 |
-
"""
|
| 96 |
-
num_players = len(game.players)
|
| 97 |
-
if num_players == 0:
|
| 98 |
-
return
|
| 99 |
-
|
| 100 |
-
for i, card in enumerate(cards):
|
| 101 |
-
player_index = i % num_players
|
| 102 |
-
game.players[player_index].cards.append(card)
|
| 103 |
-
|
| 104 |
-
@staticmethod
|
| 105 |
-
def check_suggestion(
|
| 106 |
-
game: Game,
|
| 107 |
-
player_id: str,
|
| 108 |
-
character: str,
|
| 109 |
-
weapon: str,
|
| 110 |
-
room: str
|
| 111 |
-
) -> Tuple[bool, Optional[str], Optional[Card]]:
|
| 112 |
-
"""
|
| 113 |
-
Process a player's suggestion.
|
| 114 |
-
Returns (can_disprove, disprover_name, card_shown).
|
| 115 |
-
|
| 116 |
-
Starting with the next player clockwise, check if anyone can disprove
|
| 117 |
-
the suggestion by showing one matching card.
|
| 118 |
-
"""
|
| 119 |
-
player_index = next(
|
| 120 |
-
(i for i, p in enumerate(game.players) if p.id == player_id),
|
| 121 |
-
None
|
| 122 |
-
)
|
| 123 |
-
if player_index is None:
|
| 124 |
-
return False, None, None
|
| 125 |
-
|
| 126 |
-
num_players = len(game.players)
|
| 127 |
-
|
| 128 |
-
# Check other players clockwise
|
| 129 |
-
for offset in range(1, num_players):
|
| 130 |
-
check_index = (player_index + offset) % num_players
|
| 131 |
-
checker = game.players[check_index]
|
| 132 |
-
|
| 133 |
-
# Find matching cards
|
| 134 |
-
matching_cards = [
|
| 135 |
-
card for card in checker.cards
|
| 136 |
-
if card.name in [character, weapon, room]
|
| 137 |
-
]
|
| 138 |
-
|
| 139 |
-
if matching_cards:
|
| 140 |
-
# Show one random matching card
|
| 141 |
-
card_to_show = random.choice(matching_cards)
|
| 142 |
-
return True, checker.name, card_to_show
|
| 143 |
-
|
| 144 |
-
# No one can disprove
|
| 145 |
-
return False, None, None
|
| 146 |
-
|
| 147 |
-
@staticmethod
|
| 148 |
-
def check_accusation(
|
| 149 |
-
game: Game,
|
| 150 |
-
character: str,
|
| 151 |
-
weapon: str,
|
| 152 |
-
room: str
|
| 153 |
-
) -> bool:
|
| 154 |
-
"""
|
| 155 |
-
Check if an accusation is correct.
|
| 156 |
-
Returns True if the accusation matches the solution.
|
| 157 |
-
"""
|
| 158 |
-
if not game.solution:
|
| 159 |
-
return False
|
| 160 |
-
|
| 161 |
-
return (
|
| 162 |
-
game.solution.character.name == character and
|
| 163 |
-
game.solution.weapon.name == weapon and
|
| 164 |
-
game.solution.room.name == room
|
| 165 |
-
)
|
| 166 |
-
|
| 167 |
-
@staticmethod
|
| 168 |
-
def process_accusation(
|
| 169 |
-
game: Game,
|
| 170 |
-
player_id: str,
|
| 171 |
-
character: str,
|
| 172 |
-
weapon: str,
|
| 173 |
-
room: str
|
| 174 |
-
) -> Tuple[bool, str]:
|
| 175 |
-
"""
|
| 176 |
-
Process a player's accusation.
|
| 177 |
-
Returns (is_correct, message).
|
| 178 |
-
|
| 179 |
-
If correct, player wins.
|
| 180 |
-
If incorrect, player is eliminated from the game.
|
| 181 |
-
"""
|
| 182 |
-
player = next((p for p in game.players if p.id == player_id), None)
|
| 183 |
-
if not player:
|
| 184 |
-
return False, "Player not found"
|
| 185 |
-
|
| 186 |
-
is_correct = GameEngine.check_accusation(game, character, weapon, room)
|
| 187 |
-
|
| 188 |
-
if is_correct:
|
| 189 |
-
game.winner = player.name
|
| 190 |
-
game.status = GameStatus.FINISHED
|
| 191 |
-
return True, f"{player.name} wins! The accusation was correct."
|
| 192 |
-
else:
|
| 193 |
-
# Eliminate player
|
| 194 |
-
player.is_active = False
|
| 195 |
-
|
| 196 |
-
# Check if only one or no players remain active
|
| 197 |
-
active_players = [p for p in game.players if p.is_active]
|
| 198 |
-
if len(active_players) <= 1:
|
| 199 |
-
game.status = GameStatus.FINISHED
|
| 200 |
-
if active_players:
|
| 201 |
-
game.winner = active_players[0].name
|
| 202 |
-
return False, f"{player.name}'s accusation was wrong. {game.winner} wins by elimination!"
|
| 203 |
-
else:
|
| 204 |
-
return False, "All players eliminated. Game over!"
|
| 205 |
-
|
| 206 |
-
return False, f"{player.name}'s accusation was wrong and is eliminated from the game."
|
| 207 |
-
|
| 208 |
-
@staticmethod
|
| 209 |
-
def add_turn_record(
|
| 210 |
-
game: Game,
|
| 211 |
-
player_id: str,
|
| 212 |
-
action: str,
|
| 213 |
-
details: Optional[str] = None,
|
| 214 |
-
ai_comment: Optional[str] = None
|
| 215 |
-
):
|
| 216 |
-
"""Add a turn record to the game history."""
|
| 217 |
-
from models import Turn
|
| 218 |
-
|
| 219 |
-
player = next((p for p in game.players if p.id == player_id), None)
|
| 220 |
-
if not player:
|
| 221 |
-
return
|
| 222 |
-
|
| 223 |
-
turn = Turn(
|
| 224 |
-
player_id=player_id,
|
| 225 |
-
player_name=player.name,
|
| 226 |
-
action=action,
|
| 227 |
-
details=details,
|
| 228 |
-
ai_comment=ai_comment,
|
| 229 |
-
timestamp=datetime.now().isoformat()
|
| 230 |
-
)
|
| 231 |
-
game.turns.append(turn)
|
| 232 |
-
|
| 233 |
-
@staticmethod
|
| 234 |
-
def get_player_card_names(player: Player) -> List[str]:
|
| 235 |
-
"""Get list of card names for a player."""
|
| 236 |
-
return [card.name for card in player.cards]
|
| 237 |
-
|
| 238 |
-
@staticmethod
|
| 239 |
-
def can_player_act(game: Game, player_id: str) -> bool:
|
| 240 |
-
"""Check if it's the player's turn and they can act."""
|
| 241 |
-
current_player = game.get_current_player()
|
| 242 |
-
if not current_player:
|
| 243 |
-
return False
|
| 244 |
-
|
| 245 |
-
return (
|
| 246 |
-
current_player.id == player_id and
|
| 247 |
-
current_player.is_active and
|
| 248 |
-
game.status == GameStatus.IN_PROGRESS
|
| 249 |
-
)
|
| 250 |
-
|
| 251 |
-
@staticmethod
|
| 252 |
-
def roll_dice() -> int:
|
| 253 |
-
"""Roll two dice and return the sum (2-12)."""
|
| 254 |
-
return random.randint(1, 6) + random.randint(1, 6)
|
| 255 |
-
|
| 256 |
-
@staticmethod
|
| 257 |
-
def move_player(game: Game, player_id: str, dice_roll: int) -> Tuple[bool, str, int]:
|
| 258 |
-
"""
|
| 259 |
-
Move a player on the board based on dice roll.
|
| 260 |
-
Returns (success, message, new_room_index).
|
| 261 |
-
"""
|
| 262 |
-
player = next((p for p in game.players if p.id == player_id), None)
|
| 263 |
-
if not player:
|
| 264 |
-
return False, "Joueur introuvable", -1
|
| 265 |
-
|
| 266 |
-
num_rooms = len(game.rooms)
|
| 267 |
-
if num_rooms == 0:
|
| 268 |
-
return False, "Pas de pièces disponibles", -1
|
| 269 |
-
|
| 270 |
-
# Calculate new position (circular movement)
|
| 271 |
-
new_room_index = (player.current_room_index + dice_roll) % num_rooms
|
| 272 |
-
old_room = game.rooms[player.current_room_index]
|
| 273 |
-
new_room = game.rooms[new_room_index]
|
| 274 |
-
|
| 275 |
-
player.current_room_index = new_room_index
|
| 276 |
-
|
| 277 |
-
message = f"🎲 Dés: {dice_roll} | {old_room} → {new_room}"
|
| 278 |
-
return True, message, new_room_index
|
| 279 |
-
|
| 280 |
-
@staticmethod
|
| 281 |
-
def can_make_suggestion(game: Game, player_id: str, room: str) -> Tuple[bool, str]:
|
| 282 |
-
"""
|
| 283 |
-
Check if a player can make a suggestion.
|
| 284 |
-
Players can only suggest in the room they're currently in.
|
| 285 |
-
"""
|
| 286 |
-
player = next((p for p in game.players if p.id == player_id), None)
|
| 287 |
-
if not player:
|
| 288 |
-
return False, "Joueur introuvable"
|
| 289 |
-
|
| 290 |
-
current_room = game.rooms[player.current_room_index]
|
| 291 |
-
if current_room != room:
|
| 292 |
-
return False, f"Tu dois être dans {room} pour faire cette suggestion ! Tu es actuellement dans {current_room}."
|
| 293 |
-
|
| 294 |
-
return True, ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
game_manager.py
DELETED
|
@@ -1,151 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Game manager for handling multiple concurrent games.
|
| 3 |
-
Provides in-memory storage and game lifecycle management.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import json
|
| 7 |
-
import os
|
| 8 |
-
from typing import Dict, Optional, List
|
| 9 |
-
from models import Game, Player, CreateGameRequest, GameStatus
|
| 10 |
-
from game_engine import GameEngine
|
| 11 |
-
from config import settings
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class GameManager:
|
| 15 |
-
"""Manages multiple game instances in memory."""
|
| 16 |
-
|
| 17 |
-
def __init__(self):
|
| 18 |
-
self.games: Dict[str, Game] = {}
|
| 19 |
-
self.load_games()
|
| 20 |
-
|
| 21 |
-
def create_game(self, request: CreateGameRequest) -> Game:
|
| 22 |
-
"""
|
| 23 |
-
Create a new game instance.
|
| 24 |
-
"""
|
| 25 |
-
game_id = Game.generate_game_id()
|
| 26 |
-
|
| 27 |
-
# Ensure unique game ID
|
| 28 |
-
while game_id in self.games:
|
| 29 |
-
game_id = Game.generate_game_id()
|
| 30 |
-
|
| 31 |
-
game = Game(
|
| 32 |
-
game_id=game_id,
|
| 33 |
-
name=request.game_name,
|
| 34 |
-
narrative_tone=request.narrative_tone,
|
| 35 |
-
custom_prompt=request.custom_prompt,
|
| 36 |
-
rooms=request.rooms,
|
| 37 |
-
custom_weapons=request.custom_weapons,
|
| 38 |
-
custom_suspects=request.custom_suspects,
|
| 39 |
-
use_ai=request.use_ai,
|
| 40 |
-
max_players=settings.MAX_PLAYERS
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
self.games[game_id] = game
|
| 44 |
-
self.save_games()
|
| 45 |
-
|
| 46 |
-
return game
|
| 47 |
-
|
| 48 |
-
def get_game(self, game_id: str) -> Optional[Game]:
|
| 49 |
-
"""Retrieve a game by ID."""
|
| 50 |
-
return self.games.get(game_id)
|
| 51 |
-
|
| 52 |
-
def join_game(self, game_id: str, player_name: str) -> Optional[Player]:
|
| 53 |
-
"""
|
| 54 |
-
Add a player to an existing game.
|
| 55 |
-
Returns the player if successful, None otherwise.
|
| 56 |
-
"""
|
| 57 |
-
game = self.get_game(game_id)
|
| 58 |
-
|
| 59 |
-
if not game:
|
| 60 |
-
return None
|
| 61 |
-
|
| 62 |
-
if game.status != GameStatus.WAITING:
|
| 63 |
-
return None # Can't join a game in progress
|
| 64 |
-
|
| 65 |
-
if game.is_full():
|
| 66 |
-
return None # Game is full
|
| 67 |
-
|
| 68 |
-
player = game.add_player(player_name)
|
| 69 |
-
self.save_games()
|
| 70 |
-
|
| 71 |
-
return player
|
| 72 |
-
|
| 73 |
-
def start_game(self, game_id: str) -> bool:
|
| 74 |
-
"""
|
| 75 |
-
Start a game (initialize cards and solution).
|
| 76 |
-
Returns True if successful.
|
| 77 |
-
"""
|
| 78 |
-
game = self.get_game(game_id)
|
| 79 |
-
|
| 80 |
-
if not game:
|
| 81 |
-
return False
|
| 82 |
-
|
| 83 |
-
if game.status != GameStatus.WAITING:
|
| 84 |
-
return False # Game already started
|
| 85 |
-
|
| 86 |
-
if len(game.players) < settings.MIN_PLAYERS:
|
| 87 |
-
return False # Not enough players
|
| 88 |
-
|
| 89 |
-
# Initialize the game
|
| 90 |
-
GameEngine.initialize_game(game)
|
| 91 |
-
self.save_games()
|
| 92 |
-
|
| 93 |
-
return True
|
| 94 |
-
|
| 95 |
-
def list_active_games(self) -> List[Dict]:
|
| 96 |
-
"""
|
| 97 |
-
List all active games (waiting or in progress).
|
| 98 |
-
Returns simplified game info for listing.
|
| 99 |
-
"""
|
| 100 |
-
active_games = []
|
| 101 |
-
|
| 102 |
-
for game in self.games.values():
|
| 103 |
-
if game.status in [GameStatus.WAITING, GameStatus.IN_PROGRESS]:
|
| 104 |
-
active_games.append({
|
| 105 |
-
"game_id": game.game_id,
|
| 106 |
-
"name": game.name,
|
| 107 |
-
"status": game.status,
|
| 108 |
-
"players": len(game.players),
|
| 109 |
-
"max_players": game.max_players,
|
| 110 |
-
})
|
| 111 |
-
|
| 112 |
-
return active_games
|
| 113 |
-
|
| 114 |
-
def delete_game(self, game_id: str) -> bool:
|
| 115 |
-
"""Delete a game from memory."""
|
| 116 |
-
if game_id in self.games:
|
| 117 |
-
del self.games[game_id]
|
| 118 |
-
self.save_games()
|
| 119 |
-
return True
|
| 120 |
-
return False
|
| 121 |
-
|
| 122 |
-
def save_games(self):
|
| 123 |
-
"""Persist games to JSON file."""
|
| 124 |
-
try:
|
| 125 |
-
games_data = {
|
| 126 |
-
game_id: game.model_dump()
|
| 127 |
-
for game_id, game in self.games.items()
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
with open(settings.GAMES_FILE, 'w') as f:
|
| 131 |
-
json.dump(games_data, f, indent=2)
|
| 132 |
-
except Exception as e:
|
| 133 |
-
print(f"Error saving games: {e}")
|
| 134 |
-
|
| 135 |
-
def load_games(self):
|
| 136 |
-
"""Load games from JSON file if it exists."""
|
| 137 |
-
if not os.path.exists(settings.GAMES_FILE):
|
| 138 |
-
return
|
| 139 |
-
|
| 140 |
-
try:
|
| 141 |
-
with open(settings.GAMES_FILE, 'r') as f:
|
| 142 |
-
games_data = json.load(f)
|
| 143 |
-
|
| 144 |
-
for game_id, game_dict in games_data.items():
|
| 145 |
-
self.games[game_id] = Game(**game_dict)
|
| 146 |
-
except Exception as e:
|
| 147 |
-
print(f"Error loading games: {e}")
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
# Global game manager instance
|
| 151 |
-
game_manager = GameManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
models.py
DELETED
|
@@ -1,182 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Data models for Cluedo Custom game.
|
| 3 |
-
Defines the structure of players, cards, and game state.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from typing import List, Optional, Dict
|
| 7 |
-
from pydantic import BaseModel, Field
|
| 8 |
-
from enum import Enum
|
| 9 |
-
import random
|
| 10 |
-
import string
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class NarrativeTone(str, Enum):
|
| 14 |
-
"""Narrative tone options for the game."""
|
| 15 |
-
SERIOUS = "🕵️ Sérieuse"
|
| 16 |
-
PARODY = "😂 Parodique"
|
| 17 |
-
FANTASY = "🧙♂️ Fantastique"
|
| 18 |
-
THRILLER = "🎬 Thriller"
|
| 19 |
-
HORROR = "👻 Horreur"
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
class CardType(str, Enum):
|
| 23 |
-
"""Types of cards in the game."""
|
| 24 |
-
CHARACTER = "character"
|
| 25 |
-
WEAPON = "weapon"
|
| 26 |
-
ROOM = "room"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
class Card(BaseModel):
|
| 30 |
-
"""Represents a single card in the game."""
|
| 31 |
-
name: str
|
| 32 |
-
card_type: CardType
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
class Player(BaseModel):
|
| 36 |
-
"""Represents a player in the game."""
|
| 37 |
-
id: str
|
| 38 |
-
name: str
|
| 39 |
-
cards: List[Card] = Field(default_factory=list)
|
| 40 |
-
is_active: bool = True
|
| 41 |
-
current_room_index: int = 0 # Position on the board
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
class GameStatus(str, Enum):
|
| 45 |
-
"""Status of a game."""
|
| 46 |
-
WAITING = "waiting" # Waiting for players to join
|
| 47 |
-
IN_PROGRESS = "in_progress" # Game is running
|
| 48 |
-
FINISHED = "finished" # Game has ended
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
class Turn(BaseModel):
|
| 52 |
-
"""Represents a turn action in the game."""
|
| 53 |
-
player_id: str
|
| 54 |
-
player_name: str
|
| 55 |
-
action: str # "move", "suggest", "accuse", "pass"
|
| 56 |
-
details: Optional[str] = None
|
| 57 |
-
ai_comment: Optional[str] = None # Desland's sarcastic comment
|
| 58 |
-
timestamp: str
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
class Solution(BaseModel):
|
| 62 |
-
"""The secret solution to the mystery."""
|
| 63 |
-
character: Card
|
| 64 |
-
weapon: Card
|
| 65 |
-
room: Card
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
class InvestigationNote(BaseModel):
|
| 69 |
-
"""Player's notes on the investigation."""
|
| 70 |
-
player_id: str
|
| 71 |
-
element_name: str # Name of suspect/weapon/room
|
| 72 |
-
element_type: str # "suspect", "weapon", "room"
|
| 73 |
-
status: str # "unknown", "eliminated", "maybe"
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
class Game(BaseModel):
|
| 77 |
-
"""Represents a complete game instance."""
|
| 78 |
-
game_id: str
|
| 79 |
-
name: str
|
| 80 |
-
status: GameStatus = GameStatus.WAITING
|
| 81 |
-
|
| 82 |
-
# Theme and narrative
|
| 83 |
-
narrative_tone: str = NarrativeTone.SERIOUS.value
|
| 84 |
-
custom_prompt: Optional[str] = None
|
| 85 |
-
|
| 86 |
-
# Game elements (customizable)
|
| 87 |
-
rooms: List[str]
|
| 88 |
-
custom_weapons: List[str] = Field(default_factory=list)
|
| 89 |
-
custom_suspects: List[str] = Field(default_factory=list)
|
| 90 |
-
|
| 91 |
-
use_ai: bool = False
|
| 92 |
-
|
| 93 |
-
# Players
|
| 94 |
-
players: List[Player] = Field(default_factory=list)
|
| 95 |
-
max_players: int = 8
|
| 96 |
-
current_player_index: int = 0
|
| 97 |
-
|
| 98 |
-
# Cards
|
| 99 |
-
characters: List[Card] = Field(default_factory=list)
|
| 100 |
-
weapons: List[Card] = Field(default_factory=list)
|
| 101 |
-
room_cards: List[Card] = Field(default_factory=list)
|
| 102 |
-
|
| 103 |
-
# Solution
|
| 104 |
-
solution: Optional[Solution] = None
|
| 105 |
-
|
| 106 |
-
# Game state
|
| 107 |
-
turns: List[Turn] = Field(default_factory=list)
|
| 108 |
-
winner: Optional[str] = None
|
| 109 |
-
|
| 110 |
-
# Investigation notes (for UI)
|
| 111 |
-
investigation_notes: List[InvestigationNote] = Field(default_factory=list)
|
| 112 |
-
|
| 113 |
-
# AI-generated content
|
| 114 |
-
scenario: Optional[str] = None
|
| 115 |
-
|
| 116 |
-
@staticmethod
|
| 117 |
-
def generate_game_id() -> str:
|
| 118 |
-
"""Generate a unique 4-character game ID (like AB7F)."""
|
| 119 |
-
chars = string.ascii_uppercase + string.digits
|
| 120 |
-
return ''.join(random.choices(chars, k=4))
|
| 121 |
-
|
| 122 |
-
def add_player(self, player_name: str) -> Player:
|
| 123 |
-
"""Add a new player to the game."""
|
| 124 |
-
player_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
| 125 |
-
# All players start in the first room
|
| 126 |
-
player = Player(id=player_id, name=player_name, current_room_index=0)
|
| 127 |
-
self.players.append(player)
|
| 128 |
-
return player
|
| 129 |
-
|
| 130 |
-
def get_current_player(self) -> Optional[Player]:
|
| 131 |
-
"""Get the player whose turn it is."""
|
| 132 |
-
if not self.players:
|
| 133 |
-
return None
|
| 134 |
-
return self.players[self.current_player_index]
|
| 135 |
-
|
| 136 |
-
def next_turn(self):
|
| 137 |
-
"""Move to the next active player's turn."""
|
| 138 |
-
if not self.players:
|
| 139 |
-
return
|
| 140 |
-
|
| 141 |
-
# Skip eliminated players
|
| 142 |
-
attempts = 0
|
| 143 |
-
while attempts < len(self.players):
|
| 144 |
-
self.current_player_index = (self.current_player_index + 1) % len(self.players)
|
| 145 |
-
if self.players[self.current_player_index].is_active:
|
| 146 |
-
break
|
| 147 |
-
attempts += 1
|
| 148 |
-
|
| 149 |
-
def is_full(self) -> bool:
|
| 150 |
-
"""Check if the game has reached maximum players."""
|
| 151 |
-
return len(self.players) >= self.max_players
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
class CreateGameRequest(BaseModel):
|
| 155 |
-
"""Request to create a new game."""
|
| 156 |
-
game_name: str
|
| 157 |
-
narrative_tone: str = NarrativeTone.SERIOUS.value
|
| 158 |
-
custom_prompt: Optional[str] = None
|
| 159 |
-
rooms: List[str]
|
| 160 |
-
custom_weapons: List[str]
|
| 161 |
-
custom_suspects: List[str]
|
| 162 |
-
use_ai: bool = False
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
class JoinGameRequest(BaseModel):
|
| 166 |
-
"""Request to join an existing game."""
|
| 167 |
-
game_id: str
|
| 168 |
-
player_name: str
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
class GameAction(BaseModel):
|
| 172 |
-
"""Request to perform a game action."""
|
| 173 |
-
game_id: str
|
| 174 |
-
player_id: str
|
| 175 |
-
action_type: str # "move", "suggest", "accuse", "pass"
|
| 176 |
-
# For movement
|
| 177 |
-
dice_roll: Optional[int] = None
|
| 178 |
-
target_room_index: Optional[int] = None
|
| 179 |
-
# For suggestions/accusations
|
| 180 |
-
character: Optional[str] = None
|
| 181 |
-
weapon: Optional[str] = None
|
| 182 |
-
room: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|