Spaces:
Sleeping
Sleeping
Commit ·
56dd4cc
1
Parent(s): 33dc256
feat: improve game logic and add a board
Browse files- ARCHITECTURE_V3.md +0 -199
- CHANGELOG.md +86 -0
- README.md +169 -224
- ai_service.py +10 -25
- backend/game_manager.py +1 -0
- backend/main.py +61 -5
- backend/models.py +18 -0
- frontend/src/components/AINavigator.jsx +89 -0
- frontend/src/components/GameBoard.jsx +93 -0
- frontend/src/components/InvestigationGrid.jsx +113 -0
- frontend/src/pages/Game.jsx +62 -60
- specifications.txt +14 -32
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 |
-
|
| 24 |
|
| 25 |
-
##
|
| 26 |
|
| 27 |
-
- **
|
| 28 |
-
- **
|
| 29 |
-
- **
|
| 30 |
-
- **
|
| 31 |
-
- **
|
| 32 |
-
- **
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
##
|
| 35 |
|
| 36 |
-
###
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 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 |
-
#
|
|
|
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
```
|
| 74 |
|
| 75 |
-
|
| 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 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
- Upload all project files to your Space
|
| 92 |
-
- Ensure `app.py` is the main entry point
|
| 93 |
|
| 94 |
-
|
| 95 |
-
-
|
| 96 |
-
-
|
| 97 |
|
| 98 |
-
|
| 99 |
```
|
| 100 |
USE_OPENAI=true
|
| 101 |
-
|
| 102 |
-
MAX_PLAYERS=8
|
| 103 |
```
|
| 104 |
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
-
##
|
| 117 |
|
| 118 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
-
- Enter the Game ID provided by the game creator
|
| 129 |
-
- Enter your player name
|
| 130 |
-
- Click "Join Game"
|
| 131 |
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
|
| 135 |
-
- The game creator clicks "Start Game"
|
| 136 |
-
- Cards are automatically distributed to all players
|
| 137 |
|
| 138 |
-
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
|
| 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 |
-
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
-
##
|
| 153 |
|
| 154 |
-
|
|
|
|
| 155 |
|
| 156 |
-
|
| 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 |
-
|
| 162 |
|
| 163 |
-
|
| 164 |
-
- All other cards are distributed evenly among players
|
| 165 |
|
| 166 |
-
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
|
| 173 |
-
|
| 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 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
|
| 182 |
-
|
|
|
|
| 183 |
|
| 184 |
-
|
|
|
|
|
|
|
| 185 |
|
| 186 |
-
|
| 187 |
-
- **Turn Narration** (optional): Brief narrative elements during gameplay
|
| 188 |
|
| 189 |
-
|
| 190 |
-
-
|
| 191 |
-
- 3
|
| 192 |
-
-
|
| 193 |
|
| 194 |
-
##
|
| 195 |
|
| 196 |
```
|
| 197 |
custom-cluedo/
|
| 198 |
-
├──
|
| 199 |
-
├──
|
| 200 |
-
├──
|
| 201 |
-
├──
|
| 202 |
-
├──
|
| 203 |
-
├──
|
| 204 |
-
├──
|
| 205 |
-
|
| 206 |
-
├──
|
| 207 |
-
├──
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
#
|
| 212 |
-
|
| 213 |
-
#
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
#
|
| 220 |
-
|
| 221 |
-
|
| 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 |
-
##
|
| 264 |
|
| 265 |
-
|
| 266 |
-
-
|
| 267 |
-
-
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
-
###
|
| 272 |
-
|
| 273 |
-
Change the port in `config.py` or use environment variable:
|
| 274 |
-
```bash
|
| 275 |
-
PORT=8000 python app.py
|
| 276 |
-
```
|
| 277 |
|
| 278 |
-
##
|
| 279 |
|
| 280 |
-
-
|
| 281 |
-
-
|
| 282 |
-
-
|
| 283 |
-
-
|
|
|
|
| 284 |
|
| 285 |
-
##
|
| 286 |
|
| 287 |
-
|
| 288 |
-
-
|
| 289 |
-
-
|
|
|
|
| 290 |
|
| 291 |
-
##
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
-
##
|
| 296 |
|
| 297 |
-
|
| 298 |
-
1. Fork the repository
|
| 299 |
-
2. Create a feature branch
|
| 300 |
-
3. Submit a pull request
|
| 301 |
|
| 302 |
-
##
|
| 303 |
|
| 304 |
-
|
| 305 |
-
-
|
| 306 |
-
-
|
| 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-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 117 |
-
const isMyTurn = gameState.
|
| 118 |
-
const canStart =
|
| 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 |
-
🏰
|
| 132 |
</h1>
|
| 133 |
-
<p className="text-haunted-fog/80">👤 {me?.name} {me?.
|
| 134 |
</div>
|
| 135 |
<div className="text-right">
|
| 136 |
<p className="text-haunted-fog">
|
| 137 |
{gameState.status === 'waiting' ? '⏳ En attente des âmes' :
|
| 138 |
-
gameState.status === '
|
| 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-
|
| 156 |
-
{/*
|
| 157 |
-
<div className="
|
| 158 |
-
<
|
| 159 |
-
|
| 160 |
-
{gameState.
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 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 |
-
{/*
|
| 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 |
-
{
|
| 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 === '
|
| 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 === '
|
| 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.
|
| 194 |
</h2>
|
| 195 |
|
| 196 |
{isMyTurn && (
|
| 197 |
<div className="space-y-4">
|
| 198 |
-
|
| 199 |
-
<
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 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.
|
| 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.
|
| 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.
|
| 253 |
-
<option key={i} value={r
|
| 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.
|
| 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 |
-
{
|
| 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 :
|
| 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.
|