clementpep commited on
Commit
56dd4cc
·
1 Parent(s): 33dc256

feat: improve game logic and add a board

Browse files
ARCHITECTURE_V3.md DELETED
@@ -1,199 +0,0 @@
1
- # 🏗️ Architecture v3.0 - React + FastAPI + Docker
2
-
3
- ## 🎯 Objectif
4
- Interface simple et intuitive, déployable sur Hugging Face Spaces via Docker.
5
-
6
- ## 📐 Architecture
7
-
8
- ```
9
- custom-cluedo/
10
- ├── backend/ # FastAPI
11
- │ ├── main.py # API principale
12
- │ ├── models.py # Modèles Pydantic
13
- │ ├── game_engine.py # Logique métier
14
- │ ├── game_manager.py # Gestion parties
15
- │ └── requirements.txt
16
-
17
- ├── frontend/ # React
18
- │ ├── public/
19
- │ ├── src/
20
- │ │ ├── App.jsx
21
- │ │ ├── pages/
22
- │ │ │ ├── Home.jsx
23
- │ │ │ ├── Join.jsx
24
- │ │ │ └── Game.jsx
25
- │ │ ├── components/
26
- │ │ │ ├── Board.jsx
27
- │ │ │ ├── PlayerCards.jsx
28
- │ │ │ └── ActionPanel.jsx
29
- │ │ └── api.js
30
- │ ├── package.json
31
- │ └── vite.config.js
32
-
33
- ├── Dockerfile # Multi-stage build
34
- ├── docker-compose.yml # Dev local
35
- └── README.md
36
- ```
37
-
38
- ## 🎮 Flux Simplifié
39
-
40
- ### 1. Création (Valeurs par Défaut)
41
- ```
42
- Titre: "Meurtre au Manoir"
43
- Tonalité: "Thriller"
44
- Lieux: ["Cuisine", "Salon", "Bureau", "Chambre", "Garage", "Jardin"]
45
- Armes: ["Poignard", "Revolver", "Corde", "Chandelier", "Clé anglaise", "Poison"]
46
- Suspects: ["Mme Leblanc", "Col. Moutarde", "Mlle Rose", "Prof. Violet", "Mme Pervenche", "M. Olive"]
47
- ```
48
-
49
- **Personnalisation optionnelle** (accordéon replié par défaut)
50
-
51
- ### 2. Interface de Jeu
52
-
53
- **Layout simple :**
54
- ```
55
- ┌─────────────────────────────────────┐
56
- │ Cluedo Custom - Code: AB7F │
57
- ├─────────────────────────────────────┤
58
- │ │
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
-
72
- ## 🐳 Docker pour Hugging Face
73
-
74
- ### Dockerfile
75
- ```dockerfile
76
- # Stage 1: Build frontend
77
- FROM node:18-alpine AS frontend-build
78
- WORKDIR /app/frontend
79
- COPY frontend/package*.json ./
80
- RUN npm install
81
- COPY frontend/ ./
82
- RUN npm run build
83
-
84
- # Stage 2: Run backend + serve frontend
85
- FROM python:3.11-slim
86
- WORKDIR /app
87
-
88
- # Install backend deps
89
- COPY backend/requirements.txt ./
90
- RUN pip install --no-cache-dir -r requirements.txt
91
-
92
- # Copy backend
93
- COPY backend/ ./backend/
94
-
95
- # Copy built frontend
96
- COPY --from=frontend-build /app/frontend/dist ./frontend/dist
97
-
98
- # Expose port
99
- EXPOSE 7860
100
-
101
- # Start FastAPI (serves both API and static frontend)
102
- CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
103
- ```
104
-
105
- ## 🚀 Déploiement Hugging Face
106
-
107
- ### README.md du Space
108
- ```yaml
109
- ---
110
- title: Cluedo Custom
111
- emoji: 🔍
112
- colorFrom: red
113
- colorTo: purple
114
- sdk: docker
115
- pinned: false
116
- ---
117
- ```
118
-
119
- ## ⚙️ Configuration FastAPI pour Servir React
120
-
121
- ```python
122
- from fastapi import FastAPI
123
- from fastapi.staticfiles import StaticFiles
124
- from fastapi.responses import FileResponse
125
-
126
- app = FastAPI()
127
-
128
- # API routes
129
- @app.get("/api/health")
130
- async def health():
131
- return {"status": "ok"}
132
-
133
- # ... autres routes API ...
134
-
135
- # Serve React app
136
- app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="static")
137
-
138
- @app.get("/{full_path:path}")
139
- async def serve_react(full_path: str):
140
- return FileResponse("frontend/dist/index.html")
141
- ```
142
-
143
- ## 📦 Valeurs par Défaut
144
-
145
- ### Thèmes Prédéfinis
146
- ```python
147
- DEFAULT_THEMES = {
148
- "classic": {
149
- "name": "Meurtre au Manoir",
150
- "rooms": ["Cuisine", "Salon", "Bureau", "Chambre", "Garage", "Jardin"],
151
- "weapons": ["Poignard", "Revolver", "Corde", "Chandelier", "Clé anglaise", "Poison"],
152
- "suspects": ["Mme Leblanc", "Col. Moutarde", "Mlle Rose", "Prof. Violet", "Mme Pervenche", "M. Olive"]
153
- },
154
- "office": {
155
- "name": "Meurtre au Bureau",
156
- "rooms": ["Open Space", "Salle de réunion", "Cafétéria", "Bureau CEO", "Toilettes", "Parking"],
157
- "weapons": ["Clé USB", "Agrafeuse", "Câble", "Capsule café", "Souris", "Plante"],
158
- "suspects": ["Claire", "Pierre", "Daniel", "Marie", "Thomas", "Sophie"]
159
- }
160
- }
161
- ```
162
-
163
- ## 🎨 Design Simple (TailwindCSS)
164
-
165
- - **Palette** : Tons sombres mystérieux
166
- - **Composants** : shadcn/ui ou Chakra UI
167
- - **Animations** : Framer Motion (minimales)
168
-
169
- ## 🔧 APIs Essentielles
170
-
171
- ```
172
- POST /api/games/create # Créer (avec défauts)
173
- POST /api/games/join # Rejoindre
174
- POST /api/games/{id}/start # Démarrer
175
- GET /api/games/{id}/state # État du jeu
176
- POST /api/games/{id}/roll # Lancer dés
177
- POST /api/games/{id}/suggest # Suggestion
178
- POST /api/games/{id}/accuse # Accusation
179
- POST /api/games/{id}/pass # Passer tour
180
- ```
181
-
182
- ## ✨ Fonctionnalités Simplifiées
183
-
184
- ### MVP (Version 1)
185
- - ✅ Création avec valeurs par défaut
186
- - ✅ Rejoindre avec code
187
- - ✅ Déplacement avec dés
188
- - ✅ Suggestions/Accusations
189
- - ✅ Desland (commentaires simples, pas d'IA)
190
-
191
- ### Future (Version 2)
192
- - [ ] IA avec OpenAI
193
- - [ ] Personnalisation complète
194
- - [ ] Thèmes multiples
195
- - [ ] Animations avancées
196
-
197
- ---
198
-
199
- **Cette architecture sera plus robuste, plus simple à utiliser et déployable facilement sur Hugging Face Spaces.**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CHANGELOG.md ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## [2.0.0] - 2025-10-05
4
+
5
+ ### ✨ Nouvelles Fonctionnalités
6
+
7
+ #### Interface de Jeu
8
+ - ✅ **Grille d'enquête interactive** - Système de notation avec statuts (✅ Mes cartes, ❌ Éliminé, ❓ Peut-être, ⬜ Inconnu)
9
+ - ✅ **Plateau de jeu visuel** - Affichage graphique du plateau avec positions des joueurs
10
+ - ✅ **Système de dés** - Lancer de dés et déplacement circulaire sur le plateau
11
+ - ✅ **Narrateur IA Desland** - Commentaires sarcastiques en temps réel sur les actions des joueurs
12
+
13
+ #### Architecture
14
+ - ✅ **Plateau personnalisable** - Modèle BoardLayout avec disposition des salles sur grille
15
+ - ✅ **Support React complet** - Migration vers React + Vite + TailwindCSS
16
+ - ✅ **Build Docker multi-stage** - Frontend build + Backend Python optimisé
17
+
18
+ #### Composants Frontend
19
+ - `InvestigationGrid.jsx` - Grille interactive pour noter les déductions
20
+ - `GameBoard.jsx` - Affichage visuel du plateau de jeu
21
+ - `AINavigator.jsx` - Panneau du narrateur IA avec historique
22
+
23
+ #### Backend
24
+ - Support plateau de jeu dans modèles (BoardLayout, RoomPosition)
25
+ - Intégration IA dans endpoints `/suggest` et `/accuse`
26
+ - Génération automatique layout par défaut
27
+
28
+ ### 🎨 Améliorations UI/UX
29
+ - Thème hanté avec animations et effets de brouillard
30
+ - Interface immersive avec palette de couleurs cohérente
31
+ - Affichage temps réel des positions des joueurs
32
+ - Historique des actions visible
33
+
34
+ ### 🤖 Desland - Narrateur IA
35
+
36
+ #### Personnalité
37
+ - Sarcastique et incisif
38
+ - Se moque des théories absurdes
39
+ - Confusion récurrente sur son nom (Leland → Desland)
40
+ - Commentaires courts et mémorables
41
+
42
+ #### Exemples de commentaires
43
+ ```
44
+ "Et toi ça te semble logique que Pierre ait tué Daniel avec une clé USB
45
+ à côté de l'étendoir ?? Sans surprise c'est pas la bonne réponse..."
46
+
47
+ "Une capsule de café ? Brillant. Parce que évidemment, on commet des
48
+ meurtres avec du Nespresso maintenant."
49
+ ```
50
+
51
+ #### Configuration
52
+ - gpt-5-nano avec température 0.9
53
+ - Timeout 3 secondes
54
+ - Fallback gracieux si indisponible
55
+
56
+ ### 🔧 Technique
57
+
58
+ #### Stack
59
+ - **Frontend**: React 18, Vite, TailwindCSS
60
+ - **Backend**: FastAPI, Python 3.11
61
+ - **IA**: OpenAI gpt-5-nano (optionnel)
62
+ - **Build**: Docker multi-stage
63
+
64
+ #### Endpoints ajoutés
65
+ - Board layout dans game state
66
+ - AI comments dans suggestions/accusations
67
+
68
+ ### 🐛 Corrections
69
+ - Fix imports backend pour Docker
70
+ - Amélioration état du jeu (current_turn)
71
+ - Correction affichage plateau
72
+
73
+ ### 📝 Documentation
74
+ - README complet en français
75
+ - Guide de déploiement Docker
76
+ - Documentation API endpoints
77
+ - Exemples Desland
78
+
79
+ ---
80
+
81
+ ## [1.0.0] - Initial Release
82
+
83
+ - Création de partie basique
84
+ - Système de suggestions/accusations
85
+ - Multi-joueurs (3-8 joueurs)
86
+ - Thèmes prédéfinis (Classique, Bureau, Fantastique)
README.md CHANGED
@@ -18,290 +18,235 @@ tags:
18
  - openai
19
  ---
20
 
21
- # Cluedo Custom
22
 
23
- A web-based custom Cluedo (Clue) game that transforms real-world locations into interactive murder mystery games. Players can create games with custom room names matching their physical environment and play together in real-time.
24
 
25
- ## Features
26
 
27
- - **Custom Room Setup**: Define your own rooms based on your real-world location (office, house, school, etc.)
28
- - **Multi-player Support**: Up to 8 players per game
29
- - **Real-time Gameplay**: Turn-based system with suggestions and accusations
30
- - **AI-Enhanced Narration** (Optional): Generate atmospheric scenarios and narration using OpenAI
31
- - **Mobile-First Interface**: Responsive design optimized for smartphone gameplay
32
- - **Easy Deployment**: Docker-ready and compatible with Hugging Face Spaces
 
 
33
 
34
- ## Quick Start
35
 
36
- ### Local Development
37
 
38
- 1. **Clone the repository**
39
- ```bash
40
- git clone <repository-url>
41
- cd custom-cluedo
42
- ```
43
-
44
- 2. **Install dependencies**
45
- ```bash
46
- pip install -r requirements.txt
47
- ```
48
-
49
- 3. **Configure environment** (optional)
50
- ```bash
51
- cp .env.example .env
52
- # Edit .env to configure settings
53
- ```
54
-
55
- 4. **Run the application**
56
- ```bash
57
- python app.py
58
- ```
59
-
60
- 5. **Access the interface**
61
- Open your browser at `http://localhost:7860`
62
 
63
- ### Docker Deployment
 
64
 
65
- 1. **Build the Docker image**
66
- ```bash
67
- docker build -t cluedo-custom .
68
- ```
 
69
 
70
- 2. **Run the container**
71
- ```bash
72
- docker run -p 7860:7860 cluedo-custom
73
- ```
74
 
75
- Or with environment variables:
76
- ```bash
77
- docker run -p 7860:7860 \
78
- -e USE_OPENAI=true \
79
- -e OPENAI_API_KEY=your_key_here \
80
- cluedo-custom
81
- ```
82
 
83
- ### Hugging Face Spaces Deployment
 
 
 
 
 
84
 
85
- 1. **Create a new Space**
86
- - Go to [Hugging Face Spaces](https://huggingface.co/spaces)
87
- - Click "Create new Space"
88
- - Choose "Gradio" as the SDK
 
 
 
89
 
90
- 2. **Upload files**
91
- - Upload all project files to your Space
92
- - Ensure `app.py` is the main entry point
93
 
94
- 3. **Configure secrets** (for AI mode)
95
- - Go to Settings → Repository secrets
96
- - Add `OPENAI_API_KEY` if using AI features
97
 
98
- 4. **Set environment variables** in Space settings:
99
  ```
100
  USE_OPENAI=true
101
- APP_NAME=Cluedo Custom
102
- MAX_PLAYERS=8
103
  ```
104
 
105
- ## Environment Variables
 
 
 
106
 
107
- | Variable | Description | Default |
108
- |----------|-------------|---------|
109
- | `APP_NAME` | Application name displayed in the interface | `Cluedo Custom` |
110
- | `MAX_PLAYERS` | Maximum number of players per game | `8` |
111
- | `USE_OPENAI` | Enable AI-generated content (true/false) | `false` |
112
- | `OPENAI_API_KEY` | OpenAI API key (required if USE_OPENAI=true) | `""` |
113
 
114
- ## How to Play
 
 
 
 
 
115
 
116
- ### 1. Create a Game
117
 
118
- - Navigate to the "Create Game" tab
119
- - Enter a game name
120
- - List 6-12 room names that match your real-world location
121
- - Example: `Kitchen, Living Room, Bedroom, Office, Garage, Garden`
122
- - Optionally enable AI narration
123
- - Click "Create Game" and share the Game ID with other players
124
 
125
- ### 2. Join a Game
 
 
 
126
 
127
- - Navigate to the "Join Game" tab
128
- - Enter the Game ID provided by the game creator
129
- - Enter your player name
130
- - Click "Join Game"
131
 
132
- ### 3. Start the Game
 
 
 
133
 
134
- - Once all players have joined (minimum 3 players)
135
- - The game creator clicks "Start Game"
136
- - Cards are automatically distributed to all players
137
 
138
- ### 4. Play Your Turn
 
 
139
 
140
- - Navigate to the "Play" tab
141
- - Click "Refresh Game State" to see current status
142
- - When it's your turn:
143
- - **Make a Suggestion**: Choose a character, weapon, and room. Other players will try to disprove it.
144
- - **Make an Accusation**: If you think you know the solution. Warning: wrong accusations eliminate you!
145
- - **Pass Turn**: Skip to the next player
146
 
147
- ### 5. Win the Game
148
 
149
- - The first player to make a correct accusation wins
150
- - Or be the last player standing if others are eliminated
 
 
 
 
151
 
152
- ## Game Rules
153
 
154
- ### Setup
 
155
 
156
- - Each game has:
157
- - 6 default characters (Miss Scarlett, Colonel Mustard, Mrs. White, etc.)
158
- - 6 default weapons (Candlestick, Knife, Lead Pipe, etc.)
159
- - Custom rooms defined by the players
160
 
161
- ### Solution
162
 
163
- - At the start, one character, one weapon, and one room are randomly selected as the secret solution
164
- - All other cards are distributed evenly among players
165
 
166
- ### Gameplay
167
 
168
- 1. **Suggestions**:
169
- - Player suggests a character, weapon, and room combination
170
- - Other players (clockwise) try to disprove by showing one matching card
171
- - Only the suggesting player sees the card shown
172
 
173
- 2. **Accusations**:
174
- - Player makes a final accusation of the solution
175
- - If correct: player wins immediately
176
- - If incorrect: player is eliminated and cannot act further
177
 
178
- 3. **Victory**:
179
- - First player with correct accusation wins
180
- - If all others are eliminated, the last remaining player wins
181
 
182
- ## AI Mode
 
183
 
184
- When enabled (`USE_OPENAI=true`), the application generates:
 
 
185
 
186
- - **Scenario Introduction**: Atmospheric setup describing the mystery in your chosen location
187
- - **Turn Narration** (optional): Brief narrative elements during gameplay
188
 
189
- AI features use GPT-3.5-turbo with:
190
- - Low temperature for consistency
191
- - 3-second timeout per request
192
- - Graceful fallback if unavailable
193
 
194
- ## Project Structure
195
 
196
  ```
197
  custom-cluedo/
198
- ├── app.py # Main application (Gradio interface)
199
- ├── api.py # FastAPI backend routes
200
- ├── config.py # Configuration and settings
201
- ├── models.py # Data models (Pydantic)
202
- ├── game_engine.py # Core game logic
203
- ├── game_manager.py # Game state management
204
- ├── ai_service.py # OpenAI integration
205
- ── requirements.txt # Python dependencies
206
- ├── Dockerfile # Docker configuration
207
- ├── .env.example # Environment variables template
208
- ── README.md # This file
209
- ```
210
-
211
- ## Technical Details
212
-
213
- ### Backend
214
-
215
- - **FastAPI**: REST API for game management
216
- - **In-memory storage**: Games stored in memory with JSON persistence
217
- - **Pydantic models**: Type-safe data validation
218
-
219
- ### Frontend
220
-
221
- - **Gradio**: Interactive web interface
222
- - **Mobile-optimized**: Responsive design with large touch targets
223
- - **Real-time updates**: Manual refresh for game state (polling-based)
224
-
225
- ### Storage
226
-
227
- - Games are stored in `games.json` for persistence
228
- - Data is lost when container restarts (suitable for casual play)
229
- - No database required
230
-
231
- ## API Endpoints
232
-
233
- - `GET /` - Health check
234
- - `POST /games/create` - Create new game
235
- - `POST /games/join` - Join existing game
236
- - `POST /games/{game_id}/start` - Start game
237
- - `GET /games/{game_id}` - Get full game state
238
- - `GET /games/{game_id}/player/{player_id}` - Get player-specific view
239
- - `POST /games/{game_id}/action` - Perform game action
240
- - `GET /games/list` - List active games
241
- - `DELETE /games/{game_id}` - Delete game
242
-
243
- ## Limitations
244
-
245
- - Maximum 8 players per game (configurable)
246
- - Minimum 3 players to start
247
- - 6-12 custom rooms required
248
- - No persistent database (games reset on restart)
249
- - AI features require OpenAI API key and have rate limits
250
-
251
- ## Development
252
-
253
- ### Running Tests
254
-
255
- ```bash
256
- # Install test dependencies
257
- pip install pytest pytest-asyncio
258
-
259
- # Run tests (when implemented)
260
- pytest
261
  ```
262
 
263
- ### Code Style
264
 
265
- - Code documented in English
266
- - Type hints using Pydantic models
267
- - Follows PEP 8 guidelines
 
 
 
268
 
269
- ## Troubleshooting
 
 
 
 
270
 
271
- ### Port 7860 already in use
272
-
273
- Change the port in `config.py` or use environment variable:
274
- ```bash
275
- PORT=8000 python app.py
276
- ```
277
 
278
- ### AI features not working
279
 
280
- - Verify `USE_OPENAI=true` is set
281
- - Check `OPENAI_API_KEY` is valid
282
- - Ensure OpenAI API is accessible
283
- - Check API rate limits
 
284
 
285
- ### Game state not updating
286
 
287
- - Click "Refresh Game State" button
288
- - Check network connection to API
289
- - Verify game ID and player ID are correct
 
290
 
291
- ## License
 
 
 
292
 
293
- This project is provided as-is for educational and recreational purposes.
 
 
 
294
 
295
- ## Contributing
296
 
297
- Contributions welcome! Please:
298
- 1. Fork the repository
299
- 2. Create a feature branch
300
- 3. Submit a pull request
301
 
302
- ## Support
303
 
304
- For issues and questions:
305
- - Create an issue on GitHub
306
- - Check existing documentation
307
- - Review API endpoint responses for error details
 
18
  - openai
19
  ---
20
 
21
+ # 🕯️ Cluedo Custom - Jeu de Mystère Personnalisable
22
 
23
+ Application web de Cluedo personnalisable avec narrateur IA sarcastique (Desland, le vieux jardinier). Transformez votre environnement réel en plateau de jeu interactif !
24
 
25
+ ## ✨ Fonctionnalités
26
 
27
+ - **Plateau de jeu personnalisable** - Disposez vos salles sur une grille avec drag & drop
28
+ - **Grille d'enquête interactive** - Cochez les possibilités éliminées (✅ Mes cartes, ❌ Éliminé, ❓ Peut-être)
29
+ - **Système de dés et déplacement** - Déplacez-vous sur le plateau circulaire
30
+ - **Suggestions et accusations** - Mécaniques de jeu Cluedo complètes
31
+ - **Narrateur IA Desland** - Commentaires sarcastiques en temps réel sur vos actions
32
+ - **Interface immersive** - Thème hanté avec animations et effets visuels
33
+ - ✅ **Multi-joueurs** - 3-8 joueurs, synchronisation en temps réel
34
+ - ✅ **Thèmes prédéfinis** - Classique (Manoir), Bureau, Fantastique
35
 
36
+ ## 🚀 Démarrage Rapide
37
 
38
+ ### Avec Docker (Recommandé)
39
 
40
+ ```bash
41
+ # Build l'image
42
+ docker build -t custom-cluedo .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ # Lance l'application
45
+ docker run -p 7860:7860 custom-cluedo
46
 
47
+ # Avec IA Desland activée
48
+ docker run -p 7860:7860 \
49
+ -e USE_OPENAI=true \
50
+ -e OPENAI_API_KEY=your_key_here \
51
+ custom-cluedo
52
 
53
+ # Accéder à l'application
54
+ open http://localhost:7860
55
+ ```
 
56
 
57
+ ### Développement Local
 
 
 
 
 
 
58
 
59
+ #### Backend
60
+ ```bash
61
+ cd backend
62
+ pip install -r requirements.txt
63
+ uvicorn backend.main:app --reload --port 8000
64
+ ```
65
 
66
+ #### Frontend
67
+ ```bash
68
+ cd frontend
69
+ npm install
70
+ npm run dev # Dev server on http://localhost:5173
71
+ npm run build # Build for production
72
+ ```
73
 
74
+ ### Déploiement Hugging Face Spaces
 
 
75
 
76
+ 1. **Créer un nouveau Space**
77
+ - SDK: **Docker**
78
+ - Port: **7860**
79
 
80
+ 2. **Variables d'environnement** (Settings Variables):
81
  ```
82
  USE_OPENAI=true
83
+ OPENAI_API_KEY=<votre_clé>
 
84
  ```
85
 
86
+ 3. **Push le code**
87
+ ```bash
88
+ git push
89
+ ```
90
 
91
+ ## ⚙️ Configuration
 
 
 
 
 
92
 
93
+ | Variable | Description | Défaut |
94
+ |----------|-------------|--------|
95
+ | `USE_OPENAI` | Active le narrateur IA Desland | `false` |
96
+ | `OPENAI_API_KEY` | Clé API OpenAI (si USE_OPENAI=true) | `""` |
97
+ | `MAX_PLAYERS` | Nombre max de joueurs | `8` |
98
+ | `MIN_PLAYERS` | Nombre min de joueurs | `3` |
99
 
100
+ ## 🎮 Comment Jouer
101
 
102
+ ### 1. Créer une Partie
 
 
 
 
 
103
 
104
+ 1. Entrez votre nom
105
+ 2. Cliquez sur **"🚪 Entrer dans le Manoir"**
106
+ 3. Un code de partie est généré (ex: `CBSB`)
107
+ 4. Partagez ce code avec vos amis
108
 
109
+ ### 2. Rejoindre une Partie
 
 
 
110
 
111
+ 1. Cliquez sur **"👻 Rejoindre une partie existante"**
112
+ 2. Entrez le code de partie
113
+ 3. Entrez votre nom
114
+ 4. Rejoignez !
115
 
116
+ ### 3. Démarrer le Jeu
 
 
117
 
118
+ - Minimum **3 joueurs** requis
119
+ - Le créateur clique sur **"🚀 Commencer l'enquête"**
120
+ - Les cartes sont distribuées automatiquement
121
 
122
+ ### 4. Jouer Votre Tour
 
 
 
 
 
123
 
124
+ Quand c'est votre tour :
125
 
126
+ 1. **🎲 Lancer les dés** - Déplacez-vous sur le plateau
127
+ 2. **💬 Suggérer** - Proposez un suspect + arme + salle
128
+ - Les autres joueurs tentent de réfuter
129
+ 3. **⚠️ Accuser** - Accusation finale (éliminé si faux !)
130
+ 4. **📋 Grille d'enquête** - Notez vos déductions
131
+ - Cliquez pour marquer : ✅ → ❌ → ❓ → ⬜
132
 
133
+ ### 5. Gagner
134
 
135
+ - Premier à faire une accusation correcte gagne
136
+ - Ou dernier joueur non-éliminé
137
 
138
+ ## 🤖 Narrateur IA : Desland
 
 
 
139
 
140
+ Activez `USE_OPENAI=true` pour les commentaires sarcastiques de Desland !
141
 
142
+ ### Personnalité de Desland
 
143
 
144
+ > *"Je suis Leland... euh non, Desland. Le vieux jardinier de ce manoir maudit."*
145
 
146
+ - **Sarcastique** - Se moque des théories absurdes
147
+ - **Incisif** - Commentaires tranchants et condescendants
148
+ - **Suspicieux** - Semble en savoir plus qu'il ne dit
149
+ - **Confus** - Se trompe souvent de nom (Leland → Desland)
150
 
151
+ ### Exemples de Commentaires
 
 
 
152
 
153
+ ```
154
+ "Et toi ça te semble logique que Pierre ait tué Daniel avec une clé USB
155
+ à côté de l'étendoir ?? Sans surprise c'est pas la bonne réponse..."
156
 
157
+ "Une capsule de café ? Brillant. Parce que évidemment, on commet des
158
+ meurtres avec du Nespresso maintenant."
159
 
160
+ "Ah oui, excellente déduction Sherlock. Prochaine étape : accuser le
161
+ chat du voisin."
162
+ ```
163
 
164
+ ### Configuration IA
 
165
 
166
+ - Modèle: gpt-5-nano
167
+ - Température: 0.9 (créativité élevée)
168
+ - Timeout: 3 secondes max
169
+ - Fallback gracieux si indisponible
170
 
171
+ ## 📁 Structure du Projet
172
 
173
  ```
174
  custom-cluedo/
175
+ ├── backend/
176
+ ├── main.py # API FastAPI + Serving frontend
177
+ ├── models.py # Modèles Pydantic (Game, Player, Cards...)
178
+ ├── game_engine.py # Logique du jeu (règles, vérifications)
179
+ ├── game_manager.py # Gestion des parties (CRUD)
180
+ ├── defaults.py # Thèmes prédéfinis
181
+ ├── config.py # Configuration
182
+ │ └── requirements.txt # Dépendances Python
183
+ ├── frontend/
184
+ ├── src/
185
+ │ │ ├── pages/
186
+ │ │ │ ├── Home.jsx # Accueil + création partie
187
+ │ │ │ ├── Join.jsx # Rejoindre partie
188
+ │ │ │ └── Game.jsx # Interface de jeu
189
+ │ │ ├── components/
190
+ │ │ │ ├── GameBoard.jsx # Plateau de jeu
191
+ │ │ │ ├── InvestigationGrid.jsx # Grille d'enquête
192
+ │ │ │ └── AINavigator.jsx # Narrateur Desland
193
+ │ │ └── api.js # Client API
194
+ │ ├── package.json
195
+ │ └── tailwind.config.js
196
+ ├── ai_service.py # Service IA (Desland)
197
+ ├── Dockerfile # Build multi-stage
198
+ └── README.md
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  ```
200
 
201
+ ## 🔌 API Endpoints
202
 
203
+ ### Parties
204
+ - `GET /api/health` - Santé de l'API
205
+ - `POST /api/games/quick-create` - Créer partie rapide
206
+ - `POST /api/games/join` - Rejoindre partie
207
+ - `POST /api/games/{game_id}/start` - Démarrer
208
+ - `GET /api/games/{game_id}/state/{player_id}` - État du jeu
209
 
210
+ ### Actions
211
+ - `POST /api/games/{game_id}/roll` - Lancer dés
212
+ - `POST /api/games/{game_id}/suggest` - Suggestion
213
+ - `POST /api/games/{game_id}/accuse` - Accusation
214
+ - `POST /api/games/{game_id}/pass` - Passer tour
215
 
216
+ ### Autres
217
+ - `GET /api/themes` - Thèmes disponibles
 
 
 
 
218
 
219
+ ## 🛠️ Technologies
220
 
221
+ - **Backend** : FastAPI, Python 3.11, Pydantic
222
+ - **Frontend** : React 18, Vite, TailwindCSS
223
+ - **IA** : OpenAI gpt-5-nano (optionnel)
224
+ - **Stockage** : JSON (games.json)
225
+ - **Déploiement** : Docker, Hugging Face Spaces
226
 
227
+ ## 🎨 Thèmes Disponibles
228
 
229
+ ### Classique - Meurtre au Manoir 🏰
230
+ - **Suspects** : Mme Leblanc, Col. Moutarde, Mlle Rose, Prof. Violet, Mme Pervenche, M. Olive
231
+ - **Armes** : Poignard, Revolver, Corde, Chandelier, Clé anglaise, Poison
232
+ - **Salles** : Cuisine, Salon, Bureau, Chambre, Garage, Jardin
233
 
234
+ ### Bureau - Meurtre au Bureau 💼
235
+ - **Suspects** : Claire, Pierre, Daniel, Marie, Thomas, Sophie
236
+ - **Armes** : Clé USB, Agrafeuse, Câble HDMI, Capsule de café, Souris, Plante verte
237
+ - **Salles** : Open Space, Salle de réunion, Cafétéria, Bureau CEO, Toilettes, Parking
238
 
239
+ ### Fantastique - Meurtre au Château 🧙
240
+ - **Suspects** : Merlin le Sage, Dame Morgane, Chevalier Lancelot, Elfe Aranelle, Nain Thorin, Sorcière Malva
241
+ - **Armes** : Épée enchantée, Potion empoisonnée, Grimoire maudit, Dague runique, Bâton magique, Amulette sombre
242
+ - **Salles** : Grande Salle, Tour des Mages, Donjon, Bibliothèque, Armurerie, Crypte
243
 
244
+ ## 📝 Licence
245
 
246
+ Projet personnel - Usage libre pour l'éducation et le divertissement
 
 
 
247
 
248
+ ## 🎯 Crédits
249
 
250
+ - Jeu basé sur le Cluedo classique
251
+ - Interface immersive avec thème hanté
252
+ - Narrateur IA Desland créé avec amour et sarcasme 👻
 
ai_service.py CHANGED
@@ -27,7 +27,7 @@ class AIService:
27
  self,
28
  rooms: list[str],
29
  characters: list[str],
30
- narrative_tone: str = "🕵️ Sérieuse"
31
  ) -> Optional[str]:
32
  """
33
  Generate a mystery scenario based on the game setup.
@@ -49,11 +49,7 @@ Start with Desland introducing himself (getting his name wrong: "Je suis Leland.
49
 
50
  # Run with timeout
51
  response = await asyncio.wait_for(
52
- asyncio.to_thread(
53
- self._generate_text,
54
- prompt
55
- ),
56
- timeout=3.0
57
  )
58
 
59
  return response
@@ -72,7 +68,7 @@ Start with Desland introducing himself (getting his name wrong: "Je suis Leland.
72
  weapon: str,
73
  room: str,
74
  was_disproven: bool,
75
- narrative_tone: str = "🕵️ Sérieuse"
76
  ) -> Optional[str]:
77
  """
78
  Generate a sarcastic comment from Desland about a suggestion.
@@ -98,11 +94,7 @@ Make Desland's comment fit the narrative tone: {narrative_tone}
98
  Be sarcastic, condescending, and incisive. Mock the logic (or lack thereof) of the suggestion."""
99
 
100
  response = await asyncio.wait_for(
101
- asyncio.to_thread(
102
- self._generate_text,
103
- prompt
104
- ),
105
- timeout=3.0
106
  )
107
 
108
  return response
@@ -121,7 +113,7 @@ Be sarcastic, condescending, and incisive. Mock the logic (or lack thereof) of t
121
  weapon: str,
122
  room: str,
123
  was_correct: bool,
124
- narrative_tone: str = "🕵️ Sérieuse"
125
  ) -> Optional[str]:
126
  """
127
  Generate a comment from Desland about an accusation.
@@ -146,11 +138,7 @@ If wrong: Desland is condescending and mocking about their failure.
146
  Make it incisive and memorable."""
147
 
148
  response = await asyncio.wait_for(
149
- asyncio.to_thread(
150
- self._generate_text,
151
- prompt
152
- ),
153
- timeout=3.0
154
  )
155
 
156
  return response
@@ -171,7 +159,7 @@ Make it incisive and memorable."""
171
  return ""
172
 
173
  response = self.client.chat.completions.create(
174
- model="gpt-3.5-turbo",
175
  messages=[
176
  {
177
  "role": "system",
@@ -188,15 +176,12 @@ Examples of your style:
188
  "Une capsule de café ? Brillant. Parce que évidemment, on commet des meurtres avec du Nespresso maintenant."
189
  "Ah oui, excellente déduction Sherlock. Prochaine étape : accuser le chat du voisin."
190
 
191
- Keep responses brief (1 sentence), in French, sarcastic and memorable."""
192
  },
193
- {
194
- "role": "user",
195
- "content": prompt
196
- }
197
  ],
198
  temperature=0.9,
199
- max_tokens=150
200
  )
201
 
202
  return response.choices[0].message.content.strip()
 
27
  self,
28
  rooms: list[str],
29
  characters: list[str],
30
+ narrative_tone: str = "🕵️ Sérieuse",
31
  ) -> Optional[str]:
32
  """
33
  Generate a mystery scenario based on the game setup.
 
49
 
50
  # Run with timeout
51
  response = await asyncio.wait_for(
52
+ asyncio.to_thread(self._generate_text, prompt), timeout=3.0
 
 
 
 
53
  )
54
 
55
  return response
 
68
  weapon: str,
69
  room: str,
70
  was_disproven: bool,
71
+ narrative_tone: str = "🕵️ Sérieuse",
72
  ) -> Optional[str]:
73
  """
74
  Generate a sarcastic comment from Desland about a suggestion.
 
94
  Be sarcastic, condescending, and incisive. Mock the logic (or lack thereof) of the suggestion."""
95
 
96
  response = await asyncio.wait_for(
97
+ asyncio.to_thread(self._generate_text, prompt), timeout=3.0
 
 
 
 
98
  )
99
 
100
  return response
 
113
  weapon: str,
114
  room: str,
115
  was_correct: bool,
116
+ narrative_tone: str = "🕵️ Sérieuse",
117
  ) -> Optional[str]:
118
  """
119
  Generate a comment from Desland about an accusation.
 
138
  Make it incisive and memorable."""
139
 
140
  response = await asyncio.wait_for(
141
+ asyncio.to_thread(self._generate_text, prompt), timeout=3.0
 
 
 
 
142
  )
143
 
144
  return response
 
159
  return ""
160
 
161
  response = self.client.chat.completions.create(
162
+ model="gpt-5-nano",
163
  messages=[
164
  {
165
  "role": "system",
 
176
  "Une capsule de café ? Brillant. Parce que évidemment, on commet des meurtres avec du Nespresso maintenant."
177
  "Ah oui, excellente déduction Sherlock. Prochaine étape : accuser le chat du voisin."
178
 
179
+ Keep responses brief (1 sentence), in French, sarcastic and memorable.""",
180
  },
181
+ {"role": "user", "content": prompt},
 
 
 
182
  ],
183
  temperature=0.9,
184
+ max_tokens=150,
185
  )
186
 
187
  return response.choices[0].message.content.strip()
backend/game_manager.py CHANGED
@@ -37,6 +37,7 @@ class GameManager:
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
 
 
37
  custom_weapons=request.custom_weapons,
38
  custom_suspects=request.custom_suspects,
39
  use_ai=request.use_ai,
40
+ board_layout=request.board_layout,
41
  max_players=settings.MAX_PLAYERS
42
  )
43
 
backend/main.py CHANGED
@@ -51,6 +51,17 @@ async def quick_create_game(req: QuickCreateRequest):
51
  try:
52
  config = get_default_game_config(req.theme)
53
 
 
 
 
 
 
 
 
 
 
 
 
54
  game_req = CreateGameRequest(
55
  game_name=config["name"],
56
  narrative_tone=config["tone"],
@@ -58,7 +69,8 @@ async def quick_create_game(req: QuickCreateRequest):
58
  rooms=config["rooms"],
59
  custom_weapons=config["weapons"],
60
  custom_suspects=config["suspects"],
61
- use_ai=False
 
62
  )
63
 
64
  game = game_manager.create_game(game_req)
@@ -149,6 +161,7 @@ async def get_game_state(game_id: str, player_id: str):
149
  "my_cards": [{"name": c.name, "type": c.card_type.value} for c in player.cards],
150
  "my_position": player.current_room_index,
151
  "current_room": game.rooms[player.current_room_index] if game.rooms else None,
 
152
  "players": [
153
  {
154
  "name": p.name,
@@ -240,6 +253,27 @@ async def make_suggestion(game_id: str, req: SuggestionRequest):
240
  game, req.player_id, req.suspect, req.weapon, req.room
241
  )
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  result = {
244
  "suggestion": f"{req.suspect} + {req.weapon} + {req.room}",
245
  "was_disproven": can_disprove,
@@ -247,12 +281,13 @@ async def make_suggestion(game_id: str, req: SuggestionRequest):
247
  "card_shown": card.name if card else None
248
  }
249
 
250
- # Record turn
251
  GameEngine.add_turn_record(
252
  game,
253
  req.player_id,
254
  "suggest",
255
- result["suggestion"]
 
256
  )
257
 
258
  game.next_turn()
@@ -284,12 +319,33 @@ async def make_accusation(game_id: str, req: AccusationRequest):
284
  game, req.player_id, req.suspect, req.weapon, req.room
285
  )
286
 
287
- # Record turn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  GameEngine.add_turn_record(
289
  game,
290
  req.player_id,
291
  "accuse",
292
- f"{req.suspect} + {req.weapon} + {req.room}"
 
293
  )
294
 
295
  if not is_correct and game.status == GameStatus.IN_PROGRESS:
 
51
  try:
52
  config = get_default_game_config(req.theme)
53
 
54
+ # Create default board layout
55
+ from backend.models import BoardLayout, RoomPosition
56
+ board_layout = BoardLayout(
57
+ rooms=[
58
+ RoomPosition(name=room, x=i % 3, y=i // 3)
59
+ for i, room in enumerate(config["rooms"])
60
+ ],
61
+ grid_width=3,
62
+ grid_height=2
63
+ )
64
+
65
  game_req = CreateGameRequest(
66
  game_name=config["name"],
67
  narrative_tone=config["tone"],
 
69
  rooms=config["rooms"],
70
  custom_weapons=config["weapons"],
71
  custom_suspects=config["suspects"],
72
+ use_ai=False,
73
+ board_layout=board_layout
74
  )
75
 
76
  game = game_manager.create_game(game_req)
 
161
  "my_cards": [{"name": c.name, "type": c.card_type.value} for c in player.cards],
162
  "my_position": player.current_room_index,
163
  "current_room": game.rooms[player.current_room_index] if game.rooms else None,
164
+ "board_layout": game.board_layout.model_dump() if game.board_layout else None,
165
  "players": [
166
  {
167
  "name": p.name,
 
253
  game, req.player_id, req.suspect, req.weapon, req.room
254
  )
255
 
256
+ # Get player name
257
+ player = next((p for p in game.players if p.id == req.player_id), None)
258
+ player_name = player.name if player else "Inconnu"
259
+
260
+ # Generate AI comment if enabled
261
+ ai_comment = None
262
+ if game.use_ai:
263
+ try:
264
+ from ai_service import ai_service
265
+ import asyncio
266
+ ai_comment = await ai_service.generate_suggestion_comment(
267
+ player_name,
268
+ req.suspect,
269
+ req.weapon,
270
+ req.room,
271
+ can_disprove,
272
+ game.narrative_tone
273
+ )
274
+ except Exception as e:
275
+ print(f"AI comment generation failed: {e}")
276
+
277
  result = {
278
  "suggestion": f"{req.suspect} + {req.weapon} + {req.room}",
279
  "was_disproven": can_disprove,
 
281
  "card_shown": card.name if card else None
282
  }
283
 
284
+ # Record turn with AI comment
285
  GameEngine.add_turn_record(
286
  game,
287
  req.player_id,
288
  "suggest",
289
+ result["suggestion"],
290
+ ai_comment=ai_comment
291
  )
292
 
293
  game.next_turn()
 
319
  game, req.player_id, req.suspect, req.weapon, req.room
320
  )
321
 
322
+ # Get player name
323
+ player = next((p for p in game.players if p.id == req.player_id), None)
324
+ player_name = player.name if player else "Inconnu"
325
+
326
+ # Generate AI comment if enabled
327
+ ai_comment = None
328
+ if game.use_ai:
329
+ try:
330
+ from ai_service import ai_service
331
+ ai_comment = await ai_service.generate_accusation_comment(
332
+ player_name,
333
+ req.suspect,
334
+ req.weapon,
335
+ req.room,
336
+ is_correct,
337
+ game.narrative_tone
338
+ )
339
+ except Exception as e:
340
+ print(f"AI comment generation failed: {e}")
341
+
342
+ # Record turn with AI comment
343
  GameEngine.add_turn_record(
344
  game,
345
  req.player_id,
346
  "accuse",
347
+ f"{req.suspect} + {req.weapon} + {req.room}",
348
+ ai_comment=ai_comment
349
  )
350
 
351
  if not is_correct and game.status == GameStatus.IN_PROGRESS:
backend/models.py CHANGED
@@ -73,6 +73,20 @@ class InvestigationNote(BaseModel):
73
  status: str # "unknown", "eliminated", "maybe"
74
 
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  class Game(BaseModel):
77
  """Represents a complete game instance."""
78
  game_id: str
@@ -110,6 +124,9 @@ class Game(BaseModel):
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
 
@@ -160,6 +177,7 @@ class CreateGameRequest(BaseModel):
160
  custom_weapons: List[str]
161
  custom_suspects: List[str]
162
  use_ai: bool = False
 
163
 
164
 
165
  class JoinGameRequest(BaseModel):
 
73
  status: str # "unknown", "eliminated", "maybe"
74
 
75
 
76
+ class RoomPosition(BaseModel):
77
+ """Position of a room on the board."""
78
+ name: str
79
+ x: int # Grid X position
80
+ y: int # Grid Y position
81
+
82
+
83
+ class BoardLayout(BaseModel):
84
+ """Board layout configuration."""
85
+ rooms: List[RoomPosition] = Field(default_factory=list)
86
+ grid_width: int = 8
87
+ grid_height: int = 8
88
+
89
+
90
  class Game(BaseModel):
91
  """Represents a complete game instance."""
92
  game_id: str
 
124
  # Investigation notes (for UI)
125
  investigation_notes: List[InvestigationNote] = Field(default_factory=list)
126
 
127
+ # Board layout
128
+ board_layout: Optional[BoardLayout] = None
129
+
130
  # AI-generated content
131
  scenario: Optional[str] = None
132
 
 
177
  custom_weapons: List[str]
178
  custom_suspects: List[str]
179
  use_ai: bool = False
180
+ board_layout: Optional[BoardLayout] = None
181
 
182
 
183
  class JoinGameRequest(BaseModel):
frontend/src/components/AINavigator.jsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from 'react'
2
+
3
+ function AINavigator({ recentActions, gameStatus }) {
4
+ const [comments, setComments] = useState([])
5
+ const scrollRef = useRef(null)
6
+
7
+ useEffect(() => {
8
+ // Extract AI comments from recent actions
9
+ if (recentActions) {
10
+ const aiComments = recentActions
11
+ .filter(action => action.ai_comment)
12
+ .map(action => ({
13
+ id: `${action.player}-${action.action}-${Date.now()}`,
14
+ text: action.ai_comment,
15
+ player: action.player,
16
+ action: action.action
17
+ }))
18
+
19
+ setComments(aiComments)
20
+ }
21
+ }, [recentActions])
22
+
23
+ useEffect(() => {
24
+ // Auto-scroll to latest comment
25
+ if (scrollRef.current) {
26
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight
27
+ }
28
+ }, [comments])
29
+
30
+ if (gameStatus === 'waiting') {
31
+ return (
32
+ <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-purple/30">
33
+ <h2 className="text-xl font-bold text-haunted-purple mb-4 animate-flicker">
34
+ 👻 Desland, le Narrateur
35
+ </h2>
36
+ <div className="text-haunted-fog/70 italic">
37
+ <p className="mb-2">
38
+ "Je suis Leland... euh non, Desland. Le vieux jardinier de ce manoir maudit."
39
+ </p>
40
+ <p>
41
+ En attendant que les enquêteurs arrivent... s'ils osent.
42
+ </p>
43
+ </div>
44
+ </div>
45
+ )
46
+ }
47
+
48
+ return (
49
+ <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-purple/30">
50
+ <h2 className="text-xl font-bold text-haunted-purple mb-4 animate-flicker flex items-center gap-2">
51
+ 👻 Desland, le Narrateur
52
+ <span className="text-xs text-haunted-fog/60 font-normal">(IA activée)</span>
53
+ </h2>
54
+
55
+ <div
56
+ ref={scrollRef}
57
+ className="space-y-3 max-h-64 overflow-y-auto scrollbar-thin scrollbar-thumb-haunted-purple/50 scrollbar-track-black/20"
58
+ >
59
+ {comments.length === 0 ? (
60
+ <div className="text-haunted-fog/60 italic text-sm">
61
+ <p>"Alors, on attend quoi pour commencer cette enquête ridicule ?"</p>
62
+ </div>
63
+ ) : (
64
+ comments.map((comment, idx) => (
65
+ <div
66
+ key={idx}
67
+ className="bg-black/40 p-3 rounded border-l-4 border-haunted-purple animate-fade-in"
68
+ >
69
+ <div className="text-xs text-haunted-fog/50 mb-1">
70
+ {comment.player} • {comment.action}
71
+ </div>
72
+ <div className="text-haunted-fog italic">
73
+ "{comment.text}"
74
+ </div>
75
+ </div>
76
+ ))
77
+ )}
78
+ </div>
79
+
80
+ <div className="mt-4 pt-4 border-t border-haunted-shadow text-xs text-haunted-fog/60">
81
+ <p className="italic">
82
+ 💀 Desland commente vos actions avec son sarcasme légendaire...
83
+ </p>
84
+ </div>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ export default AINavigator
frontend/src/components/GameBoard.jsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+
3
+ function GameBoard({ boardLayout, players, rooms, myPosition }) {
4
+ if (!boardLayout || !boardLayout.rooms) {
5
+ return (
6
+ <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-shadow">
7
+ <h2 className="text-xl font-bold text-haunted-blood mb-4 animate-flicker">🏰 Plateau de Jeu</h2>
8
+ <p className="text-haunted-fog/60">Plateau en cours de chargement...</p>
9
+ </div>
10
+ )
11
+ }
12
+
13
+ const gridWidth = boardLayout.grid_width || 8
14
+ const gridHeight = boardLayout.grid_height || 8
15
+
16
+ // Get players in each room
17
+ const getPlayersInRoom = (roomName) => {
18
+ return players?.filter(p => {
19
+ const roomIndex = rooms?.indexOf(roomName)
20
+ return roomIndex !== -1 && p.position === roomIndex
21
+ }) || []
22
+ }
23
+
24
+ return (
25
+ <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-shadow">
26
+ <h2 className="text-xl font-bold text-haunted-blood mb-4 animate-flicker">🏰 Plateau du Manoir</h2>
27
+
28
+ <div
29
+ className="grid gap-2 mx-auto"
30
+ style={{
31
+ gridTemplateColumns: `repeat(${gridWidth}, minmax(0, 1fr))`,
32
+ maxWidth: `${gridWidth * 100}px`
33
+ }}
34
+ >
35
+ {Array.from({ length: gridHeight }).map((_, y) =>
36
+ Array.from({ length: gridWidth }).map((_, x) => {
37
+ const room = boardLayout.rooms.find(r => r.x === x && r.y === y)
38
+ const playersHere = room ? getPlayersInRoom(room.name) : []
39
+ const isMyLocation = room && rooms?.indexOf(room.name) === myPosition
40
+
41
+ return (
42
+ <div
43
+ key={`${x}-${y}`}
44
+ className={`
45
+ aspect-square rounded-lg border-2 transition-all
46
+ ${room
47
+ ? isMyLocation
48
+ ? 'bg-haunted-blood/20 border-haunted-blood shadow-[0_0_20px_rgba(139,0,0,0.3)]'
49
+ : 'bg-black/40 border-haunted-shadow hover:border-haunted-purple/50'
50
+ : 'bg-dark-800/20 border-dark-700'
51
+ }
52
+ `}
53
+ >
54
+ {room ? (
55
+ <div className="h-full flex flex-col items-center justify-center p-1 text-center">
56
+ <div className="text-xs font-semibold text-haunted-fog mb-1 truncate w-full">
57
+ {room.name}
58
+ </div>
59
+ {playersHere.length > 0 && (
60
+ <div className="flex flex-wrap gap-1 justify-center">
61
+ {playersHere.map((p, i) => (
62
+ <div
63
+ key={i}
64
+ className={`
65
+ w-6 h-6 rounded-full flex items-center justify-center text-xs
66
+ ${p.is_me
67
+ ? 'bg-haunted-blood text-white font-bold'
68
+ : 'bg-haunted-purple/70 text-white'
69
+ }
70
+ `}
71
+ title={p.name}
72
+ >
73
+ {p.name.charAt(0).toUpperCase()}
74
+ </div>
75
+ ))}
76
+ </div>
77
+ )}
78
+ </div>
79
+ ) : null}
80
+ </div>
81
+ )
82
+ })
83
+ )}
84
+ </div>
85
+
86
+ <div className="mt-4 pt-4 border-t border-haunted-shadow text-xs text-haunted-fog/60">
87
+ <p>🔴 Votre position • 🟣 Autres joueurs</p>
88
+ </div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ export default GameBoard
frontend/src/components/InvestigationGrid.jsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react'
2
+
3
+ function InvestigationGrid({ suspects, weapons, rooms, myCards }) {
4
+ const [notes, setNotes] = useState({})
5
+
6
+ // Initialize notes from localStorage
7
+ useEffect(() => {
8
+ const saved = localStorage.getItem('investigation_notes')
9
+ if (saved) {
10
+ setNotes(JSON.parse(saved))
11
+ }
12
+ }, [])
13
+
14
+ // Save notes to localStorage
15
+ useEffect(() => {
16
+ localStorage.setItem('investigation_notes', JSON.stringify(notes))
17
+ }, [notes])
18
+
19
+ const toggleNote = (item, type) => {
20
+ const key = `${type}:${item}`
21
+ setNotes(prev => {
22
+ const current = prev[key] || 'unknown'
23
+ const next = current === 'unknown' ? 'eliminated' :
24
+ current === 'eliminated' ? 'maybe' : 'unknown'
25
+ return { ...prev, [key]: next }
26
+ })
27
+ }
28
+
29
+ const getStatus = (item, type) => {
30
+ const key = `${type}:${item}`
31
+ return notes[key] || 'unknown'
32
+ }
33
+
34
+ const getIcon = (item, type) => {
35
+ // Check if this is one of my cards
36
+ const isMyCard = myCards?.some(card =>
37
+ card.name === item && card.type === type
38
+ )
39
+
40
+ if (isMyCard) return '✅' // I have this card
41
+
42
+ const status = getStatus(item, type)
43
+ if (status === 'eliminated') return '❌'
44
+ if (status === 'maybe') return '❓'
45
+ return '⬜'
46
+ }
47
+
48
+ return (
49
+ <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-shadow">
50
+ <h2 className="text-xl font-bold text-haunted-blood mb-4 animate-flicker">📋 Grille d'Enquête</h2>
51
+
52
+ <div className="space-y-4">
53
+ {/* Suspects */}
54
+ <div>
55
+ <h3 className="text-sm font-semibold text-haunted-fog mb-2">👤 SUSPECTS</h3>
56
+ <div className="grid grid-cols-2 gap-2">
57
+ {suspects?.map((suspect, i) => (
58
+ <button
59
+ key={i}
60
+ onClick={() => toggleNote(suspect, 'character')}
61
+ className="flex items-center gap-2 bg-black/40 px-3 py-2 rounded text-left text-sm text-haunted-fog border border-haunted-shadow hover:border-haunted-blood/50 transition-all"
62
+ >
63
+ <span className="text-lg">{getIcon(suspect, 'character')}</span>
64
+ <span className="flex-1 truncate">{suspect}</span>
65
+ </button>
66
+ ))}
67
+ </div>
68
+ </div>
69
+
70
+ {/* Weapons */}
71
+ <div>
72
+ <h3 className="text-sm font-semibold text-haunted-fog mb-2">🔪 ARMES</h3>
73
+ <div className="grid grid-cols-2 gap-2">
74
+ {weapons?.map((weapon, i) => (
75
+ <button
76
+ key={i}
77
+ onClick={() => toggleNote(weapon, 'weapon')}
78
+ className="flex items-center gap-2 bg-black/40 px-3 py-2 rounded text-left text-sm text-haunted-fog border border-haunted-shadow hover:border-haunted-blood/50 transition-all"
79
+ >
80
+ <span className="text-lg">{getIcon(weapon, 'weapon')}</span>
81
+ <span className="flex-1 truncate">{weapon}</span>
82
+ </button>
83
+ ))}
84
+ </div>
85
+ </div>
86
+
87
+ {/* Rooms */}
88
+ <div>
89
+ <h3 className="text-sm font-semibold text-haunted-fog mb-2">🏚️ SALLES</h3>
90
+ <div className="grid grid-cols-2 gap-2">
91
+ {rooms?.map((room, i) => (
92
+ <button
93
+ key={i}
94
+ onClick={() => toggleNote(room, 'room')}
95
+ className="flex items-center gap-2 bg-black/40 px-3 py-2 rounded text-left text-sm text-haunted-fog border border-haunted-shadow hover:border-haunted-blood/50 transition-all"
96
+ >
97
+ <span className="text-lg">{getIcon(room, 'room')}</span>
98
+ <span className="flex-1 truncate">{room}</span>
99
+ </button>
100
+ ))}
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <div className="mt-4 pt-4 border-t border-haunted-shadow text-xs text-haunted-fog/60 space-y-1">
106
+ <p>✅ Mes cartes • ❌ Éliminé • ❓ Peut-être • ⬜ Inconnu</p>
107
+ <p className="italic">Cliquez pour changer le statut</p>
108
+ </div>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ export default InvestigationGrid
frontend/src/pages/Game.jsx CHANGED
@@ -1,6 +1,9 @@
1
  import { useState, useEffect } from 'react'
2
  import { useParams } from 'react-router-dom'
3
  import { getGameState, startGame, rollDice, makeSuggestion, makeAccusation, passTurn } from '../api'
 
 
 
4
 
5
  function Game() {
6
  const { gameId, playerId } = useParams()
@@ -113,9 +116,9 @@ function Game() {
113
  )
114
  }
115
 
116
- const me = gameState.players.find(p => p.id === playerId)
117
- const isMyTurn = gameState.current_player_id === playerId
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">
@@ -128,14 +131,14 @@ function Game() {
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>
@@ -152,31 +155,24 @@ function Game() {
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
- ))}
170
- </div>
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}
@@ -186,34 +182,48 @@ function Game() {
186
  </div>
187
  </div>
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 && (
197
  <div className="space-y-4">
198
- {!me.has_moved ? (
199
- <div className="flex gap-4">
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>
214
- </div>
215
- ) : (
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>
@@ -223,7 +233,7 @@ function Game() {
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) => (
227
  <option key={i} value={s}>{s}</option>
228
  ))}
229
  </select>
@@ -236,7 +246,7 @@ function Game() {
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) => (
240
  <option key={i} value={w}>{w}</option>
241
  ))}
242
  </select>
@@ -249,8 +259,8 @@ function Game() {
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) => (
253
- <option key={i} value={r.name}>{r.name}</option>
254
  ))}
255
  </select>
256
  </div>
@@ -270,16 +280,8 @@ function Game() {
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>
280
  </div>
281
  </div>
282
- )}
283
  </div>
284
  )}
285
  </div>
@@ -289,9 +291,9 @@ function Game() {
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
  ))}
297
  </div>
 
1
  import { useState, useEffect } from 'react'
2
  import { useParams } from 'react-router-dom'
3
  import { getGameState, startGame, rollDice, makeSuggestion, makeAccusation, passTurn } from '../api'
4
+ import InvestigationGrid from '../components/InvestigationGrid'
5
+ import GameBoard from '../components/GameBoard'
6
+ import AINavigator from '../components/AINavigator'
7
 
8
  function Game() {
9
  const { gameId, playerId } = useParams()
 
116
  )
117
  }
118
 
119
+ const me = gameState.players.find(p => p.is_me)
120
+ const isMyTurn = gameState.current_turn?.is_my_turn
121
+ const canStart = gameState.status === 'waiting' && gameState.players.length >= 3
122
 
123
  return (
124
  <div className="min-h-screen bg-haunted-gradient p-4 relative overflow-hidden">
 
131
  <div className="flex justify-between items-center">
132
  <div>
133
  <h1 className="text-4xl font-bold text-haunted-blood mb-1 animate-flicker drop-shadow-[0_0_10px_rgba(139,0,0,0.5)]">
134
+ 🏰 {gameState.game_name} ({gameState.game_id})
135
  </h1>
136
+ <p className="text-haunted-fog/80">👤 {me?.name} {!me?.is_active && '💀 (Éliminé)'}</p>
137
  </div>
138
  <div className="text-right">
139
  <p className="text-haunted-fog">
140
  {gameState.status === 'waiting' ? '⏳ En attente des âmes' :
141
+ gameState.status === 'in_progress' ? '🎮 Enquête en cours' :
142
  '🏆 Mystère résolu'}
143
  </p>
144
  <p className="text-haunted-fog/60 text-sm">{gameState.players.length} âmes perdues</p>
 
155
  )}
156
  </div>
157
 
158
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
159
+ {/* Game Board */}
160
+ <div className="lg:col-span-2">
161
+ <GameBoard
162
+ boardLayout={gameState.board_layout}
163
+ players={gameState.players}
164
+ rooms={gameState.rooms}
165
+ myPosition={gameState.my_position}
166
+ />
 
 
 
 
 
 
 
167
  </div>
168
 
169
+ {/* My Cards */}
170
  <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-shadow">
171
  <h2 className="text-xl font-bold text-haunted-blood mb-4 animate-flicker">🃏 Vos Indices</h2>
172
  <div className="space-y-2">
173
+ {gameState.my_cards?.map((card, idx) => (
174
  <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">
175
+ {card.type === 'character' && '👤 '}
176
  {card.type === 'weapon' && '🔪 '}
177
  {card.type === 'room' && '🏚️ '}
178
  {card.name}
 
182
  </div>
183
  </div>
184
 
185
+ {/* Investigation Grid */}
186
+ <InvestigationGrid
187
+ suspects={gameState.suspects}
188
+ weapons={gameState.weapons}
189
+ rooms={gameState.rooms}
190
+ myCards={gameState.my_cards}
191
+ />
192
+
193
+ {/* AI Narrator */}
194
+ {gameState.use_ai && (
195
+ <AINavigator
196
+ recentActions={gameState.recent_actions}
197
+ gameStatus={gameState.status}
198
+ />
199
+ )}
200
+
201
  {/* Actions */}
202
+ {gameState.status === 'in_progress' && (
203
  <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-blood/30">
204
  <h2 className="text-2xl font-bold text-haunted-blood mb-4 animate-flicker">
205
+ {isMyTurn ? '⚡ À vous de jouer !' : '⏳ ' + gameState.current_turn?.player_name + ' enquête...'}
206
  </h2>
207
 
208
  {isMyTurn && (
209
  <div className="space-y-4">
210
+ <div className="flex gap-4 mb-4">
211
+ <button
212
+ onClick={handleRollDice}
213
+ disabled={actionLoading}
214
+ 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"
215
+ >
216
+ 🎲 Lancer les dés
217
+ </button>
218
+ <button
219
+ onClick={handlePassTurn}
220
+ disabled={actionLoading}
221
+ 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"
222
+ >
223
+ ⏭️ Passer
224
+ </button>
225
+ </div>
226
+ <div className="space-y-4">
 
 
227
  <div className="grid grid-cols-3 gap-4">
228
  <div>
229
  <label className="block text-sm text-haunted-fog mb-2">👤 Suspect</label>
 
233
  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"
234
  >
235
  <option value="">--</option>
236
+ {gameState.suspects?.map((s, i) => (
237
  <option key={i} value={s}>{s}</option>
238
  ))}
239
  </select>
 
246
  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"
247
  >
248
  <option value="">--</option>
249
+ {gameState.weapons?.map((w, i) => (
250
  <option key={i} value={w}>{w}</option>
251
  ))}
252
  </select>
 
259
  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"
260
  >
261
  <option value="">--</option>
262
+ {gameState.rooms?.map((r, i) => (
263
+ <option key={i} value={r}>{r}</option>
264
  ))}
265
  </select>
266
  </div>
 
280
  >
281
  ⚠️ Accuser
282
  </button>
 
 
 
 
 
 
 
283
  </div>
284
  </div>
 
285
  </div>
286
  )}
287
  </div>
 
291
  <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-shadow">
292
  <h2 className="text-xl font-bold text-haunted-blood mb-4 animate-flicker">📜 Journal de l'Enquête</h2>
293
  <div className="space-y-2 max-h-64 overflow-y-auto">
294
+ {gameState.recent_actions?.slice().reverse().map((action, idx) => (
295
  <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">
296
+ <span className="font-semibold">{action.player}</span> - {action.action}: {action.details}
297
  </div>
298
  ))}
299
  </div>
specifications.txt CHANGED
@@ -14,21 +14,21 @@ Aucune compétence technique requise pour les joueurs.
14
  🧩 3. Fonctionnalités principales
15
  A. Gestion des parties
16
 
17
- Créer une nouvelle partie
18
 
19
- Saisir un nom de partie.
20
 
21
- Choisir le nombre de salles et leurs noms (ex : "Cuisine", "Salle de réunion", "Terrasse"...).
22
 
23
- (Optionnel) Activer ou non le mode IA pour générer des descriptions ou scénarios.
24
 
25
- Rejoindre une partie existante
26
 
27
- Via un code ou lien de partage (UUID court).
28
 
29
- Le joueur saisit son nom ou pseudo.
30
 
31
- Lister les parties actives (en cours sur le serveur).
32
 
33
  B. Gestion du jeu
34
 
@@ -56,6 +56,8 @@ C. Interaction joueur
56
 
57
  Interface mobile first (compatible smartphone).
58
 
 
 
59
  Navigation intuitive :
60
 
61
  Vue "Salle actuelle"
@@ -74,16 +76,15 @@ USE_OPENAI=true active la génération de contenu IA :
74
 
75
  Génération de scénario d’enquête personnalisé (intro, contexte, description des personnages).
76
 
77
- Suggestions de narration (ex : “Une ombre passe dans la salle…”).
78
-
79
  Reformulation fluide des messages de jeu.
80
 
81
  USE_OPENAI=false → mode classique sans génération IA, avec textes statiques.
82
 
83
  🏗️ 4. Architecture technique
 
84
  Front-end
85
 
86
- Framework léger : Gradio, Streamlit, ou React + FastAPI backend (selon préférences).
87
 
88
  Interface responsive avec navigation fluide (sans reload complet).
89
 
@@ -123,6 +124,7 @@ USE_OPENAI Active la génération IA false
123
  OPENAI_API_KEY Clé OpenAI (optionnelle) ""
124
  APP_NAME Nom affiché dans l’interface "Cluedo Custom"
125
  MAX_PLAYERS Nombre max de joueurs par partie 8
 
126
  🧠 6. Comportement IA (si activé)
127
 
128
  Utiliser GPT (via OpenAI API) pour :
@@ -131,32 +133,12 @@ Générer la mise en scène du lieu : “Le meurtre a eu lieu dans la Salle des
131
 
132
  Personnaliser les noms et descriptions de personnages.
133
 
134
- Ajouter du dialogue narratif léger pendant les tours.
135
 
136
  Les prompts doivent être courts et à température basse pour éviter les délais.
137
 
138
  Pas d’appel IA bloquant : timeout max 3 secondes par requête.
139
 
140
- 🎨 7. Design & ergonomie
141
-
142
- Principes UX :
143
-
144
- Interface claire, à gros boutons.
145
-
146
- Lecture sur mobile en priorité.
147
-
148
- Couleurs contrastées : fond clair, éléments colorés selon statut.
149
-
150
- Écrans :
151
-
152
- Accueil : Créer / Rejoindre une partie.
153
-
154
- Configuration : Nom des salles + activer IA.
155
-
156
- Salle de jeu : Vue principale (tour actuel, actions possibles).
157
-
158
- Fin de partie : Résumé + rejouer.
159
-
160
  🔒 8. Gestion des sessions / multi-joueurs
161
 
162
  Génération d’un Game ID unique à 6 caractères.
 
14
  🧩 3. Fonctionnalités principales
15
  A. Gestion des parties
16
 
17
+ * Créer une nouvelle partie
18
 
19
+ * Saisir un nom de partie.
20
 
21
+ * Choisir le nombre de salles et leurs noms (ex : "Cuisine", "Salle de réunion", "Terrasse"...).
22
 
23
+ * (Optionnel) Activer ou non le mode IA pour générer des descriptions ou scénarios.
24
 
25
+ * Rejoindre une partie existante
26
 
27
+ * Via un code ou lien de partage (UUID court).
28
 
29
+ * Le joueur saisit son nom ou pseudo.
30
 
31
+ * Lister les parties actives (en cours sur le serveur).
32
 
33
  B. Gestion du jeu
34
 
 
56
 
57
  Interface mobile first (compatible smartphone).
58
 
59
+ Grille a cocher pour éliminer les possibilités
60
+
61
  Navigation intuitive :
62
 
63
  Vue "Salle actuelle"
 
76
 
77
  Génération de scénario d’enquête personnalisé (intro, contexte, description des personnages).
78
 
 
 
79
  Reformulation fluide des messages de jeu.
80
 
81
  USE_OPENAI=false → mode classique sans génération IA, avec textes statiques.
82
 
83
  🏗️ 4. Architecture technique
84
+
85
  Front-end
86
 
87
+ Framework léger : React + FastAPI backend (selon préférences).
88
 
89
  Interface responsive avec navigation fluide (sans reload complet).
90
 
 
124
  OPENAI_API_KEY Clé OpenAI (optionnelle) ""
125
  APP_NAME Nom affiché dans l’interface "Cluedo Custom"
126
  MAX_PLAYERS Nombre max de joueurs par partie 8
127
+
128
  🧠 6. Comportement IA (si activé)
129
 
130
  Utiliser GPT (via OpenAI API) pour :
 
133
 
134
  Personnaliser les noms et descriptions de personnages.
135
 
136
+ Ajouter du dialogue narratif léger pendant les tours. Le bot s'appelle
137
 
138
  Les prompts doivent être courts et à température basse pour éviter les délais.
139
 
140
  Pas d’appel IA bloquant : timeout max 3 secondes par requête.
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  🔒 8. Gestion des sessions / multi-joueurs
143
 
144
  Génération d’un Game ID unique à 6 caractères.