clementpep commited on
Commit
33dc256
·
1 Parent(s): 0344b6d

feat: improve app UI

Browse files
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
- <div className="min-h-screen bg-gradient-to-br from-dark-950 via-dark-900 to-dark-950">
10
- <Routes>
11
- <Route path="/" element={<Home />} />
12
- <Route path="/join" element={<Join />} />
13
- <Route path="/game/:gameId/:playerId" element={<Game />} />
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-accent-400 text-xl">Chargement...</div>
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
- <div className="max-w-6xl mx-auto space-y-6">
 
 
 
123
  {/* Header */}
124
- <div className="bg-dark-800 p-6 rounded-lg border border-dark-700">
125
  <div className="flex justify-between items-center">
126
  <div>
127
- <h1 className="text-3xl font-bold text-accent-400">🔍 Partie {gameState.game_code}</h1>
128
- <p className="text-dark-300">Joueur: {me?.name}</p>
 
 
129
  </div>
130
  <div className="text-right">
131
- <p className="text-dark-300">Status: {gameState.status === 'waiting' ? '⏳ En attente' : gameState.status === 'playing' ? '🎮 En cours' : '🏆 Terminée'}</p>
132
- <p className="text-dark-400 text-sm">{gameState.players.length} joueurs</p>
 
 
 
 
133
  </div>
134
  </div>
135
  {canStart && (
136
  <button
137
  onClick={handleStartGame}
138
  disabled={actionLoading}
139
- className="mt-4 px-6 py-2 bg-green-600 hover:bg-green-700 disabled:bg-dark-600 text-white font-semibold rounded-lg"
140
  >
141
- 🚀 Démarrer la partie
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-dark-800 p-6 rounded-lg border border-dark-700">
149
- <h2 className="text-xl font-bold text-accent-400 mb-4">🏰 Plateau</h2>
150
  <div className="space-y-2">
151
  {gameState.board?.rooms.map((room, idx) => (
152
- <div key={idx} className="bg-dark-700 p-3 rounded">
153
- <div className="font-semibold text-white">{room.name}</div>
154
- <div className="text-sm text-dark-300">
155
- Joueurs: {room.player_ids.map(id =>
156
  gameState.players.find(p => p.id === id)?.name || id
157
- ).join(', ') || 'Aucun'}
158
  </div>
159
  </div>
160
  ))}
@@ -162,14 +171,14 @@ function Game() {
162
  </div>
163
 
164
  {/* Mes cartes */}
165
- <div className="bg-dark-800 p-6 rounded-lg border border-dark-700">
166
- <h2 className="text-xl font-bold text-accent-400 mb-4">🃏 Mes cartes</h2>
167
  <div className="space-y-2">
168
  {me?.cards.map((card, idx) => (
169
- <div key={idx} className="bg-dark-700 px-4 py-2 rounded text-white">
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-dark-800 p-6 rounded-lg border border-dark-700">
183
- <h2 className="text-xl font-bold text-accent-400 mb-4">
184
- {isMyTurn ? '⚡ À votre tour !' : '⏳ Tour de ' + gameState.players.find(p => p.id === gameState.current_player_id)?.name}
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-accent-600 hover:bg-accent-700 disabled:bg-dark-600 text-white font-semibold rounded-lg"
195
  >
196
  🎲 Lancer les dés
197
  </button>
198
  <button
199
  onClick={handlePassTurn}
200
  disabled={actionLoading}
201
- className="px-6 py-3 bg-dark-600 hover:bg-dark-500 text-white font-semibold rounded-lg"
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-dark-300 mb-2">Suspect</label>
211
  <select
212
  value={selectedSuspect}
213
  onChange={(e) => setSelectedSuspect(e.target.value)}
214
- className="w-full px-3 py-2 bg-dark-700 text-white rounded border border-dark-600"
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-dark-300 mb-2">Arme</label>
224
  <select
225
  value={selectedWeapon}
226
  onChange={(e) => setSelectedWeapon(e.target.value)}
227
- className="w-full px-3 py-2 bg-dark-700 text-white rounded border border-dark-600"
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-dark-300 mb-2">Pièce</label>
237
  <select
238
  value={selectedRoom}
239
  onChange={(e) => setSelectedRoom(e.target.value)}
240
- className="w-full px-3 py-2 bg-dark-700 text-white rounded border border-dark-600"
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-blue-600 hover:bg-blue-700 disabled:bg-dark-600 text-white font-semibold rounded-lg"
254
  >
255
  💬 Suggérer
256
  </button>
257
  <button
258
  onClick={handleAccusation}
259
  disabled={actionLoading}
260
- className="px-6 py-3 bg-red-600 hover:bg-red-700 disabled:bg-dark-600 text-white font-semibold rounded-lg"
261
  >
262
  ⚠️ Accuser
263
  </button>
264
  <button
265
  onClick={handlePassTurn}
266
  disabled={actionLoading}
267
- className="px-6 py-3 bg-dark-600 hover:bg-dark-500 text-white font-semibold rounded-lg"
268
  >
269
  ⏭️ Passer
270
  </button>
@@ -277,11 +286,11 @@ function Game() {
277
  )}
278
 
279
  {/* Historique */}
280
- <div className="bg-dark-800 p-6 rounded-lg border border-dark-700">
281
- <h2 className="text-xl font-bold text-accent-400 mb-4">📜 Historique</h2>
282
- <div className="space-y-2">
283
  {gameState.history?.slice(-10).reverse().map((event, idx) => (
284
- <div key={idx} className="text-dark-300 text-sm border-l-2 border-accent-600 pl-3 py-1">
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
- <div className="max-w-md w-full space-y-8 bg-dark-800 p-8 rounded-lg shadow-2xl border border-dark-700">
 
 
 
 
 
 
 
31
  <div className="text-center">
32
- <h1 className="text-5xl font-bold text-accent-400 mb-2">🔍 Cluedo Custom</h1>
33
- <p className="text-dark-300">Créez votre partie et invitez vos amis</p>
 
 
 
34
  </div>
35
 
36
  <div className="space-y-4">
37
  <div>
38
- <label className="block text-sm font-medium text-dark-200 mb-2">
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="Entrez votre nom"
47
- className="w-full px-4 py-3 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-accent-500"
48
  />
49
  </div>
50
 
51
  <button
52
  onClick={handleCreateGame}
53
  disabled={loading}
54
- className="w-full py-3 px-4 bg-accent-600 hover:bg-accent-700 disabled:bg-dark-600 text-white font-semibold rounded-lg transition-colors"
55
  >
56
- {loading ? 'Création...' : '🎮 Créer une partie'}
57
  </button>
58
 
59
  <div className="text-center pt-4">
60
  <button
61
  onClick={() => navigate('/join')}
62
- className="text-accent-400 hover:text-accent-300 underline"
63
  >
64
- Rejoindre une partie existante
65
  </button>
66
  </div>
67
  </div>
68
 
69
- <div className="mt-8 pt-6 border-t border-dark-700 text-center text-sm text-dark-400">
70
- <p>Thème par défaut : Manoir Classique</p>
71
- <p className="mt-1">3-6 joueurs recommandés</p>
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
- <div className="max-w-md w-full space-y-8 bg-dark-800 p-8 rounded-lg shadow-2xl border border-dark-700">
 
 
 
 
 
 
 
32
  <div className="text-center">
33
- <h1 className="text-4xl font-bold text-accent-400 mb-2">🚪 Rejoindre une partie</h1>
34
- <p className="text-dark-300">Entrez le code partagé par votre hôte</p>
 
 
35
  </div>
36
 
37
  <div className="space-y-4">
38
  <div>
39
- <label className="block text-sm font-medium text-dark-200 mb-2">
40
- Code de partie
41
  </label>
42
  <input
43
  type="text"
44
  value={gameCode}
45
  onChange={(e) => setGameCode(e.target.value.toUpperCase())}
46
- placeholder="Ex: ABC4"
47
  maxLength={4}
48
- className="w-full px-4 py-3 bg-dark-700 border border-dark-600 rounded-lg text-white text-center text-2xl font-mono placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-accent-500 uppercase"
49
  />
50
  </div>
51
 
52
  <div>
53
- <label className="block text-sm font-medium text-dark-200 mb-2">
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="Entrez votre nom"
62
- className="w-full px-4 py-3 bg-dark-700 border border-dark-600 rounded-lg text-white placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-accent-500"
63
  />
64
  </div>
65
 
66
  <button
67
  onClick={handleJoinGame}
68
  disabled={loading}
69
- className="w-full py-3 px-4 bg-accent-600 hover:bg-accent-700 disabled:bg-dark-600 text-white font-semibold rounded-lg transition-colors"
70
  >
71
- {loading ? 'Connexion...' : ' Rejoindre'}
72
  </button>
73
 
74
  <div className="text-center pt-4">
75
  <button
76
  onClick={() => navigate('/')}
77
- className="text-accent-400 hover:text-accent-300 underline"
78
  >
79
- Retour à l'accueil
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