Spaces:
Sleeping
Sleeping
Commit
·
59ee354
1
Parent(s):
e335712
feat: improve game UI: disabled dice button and victory popup
Browse files- backend/ai_service.py +49 -0
- backend/main.py +23 -3
- frontend/src/pages/Game.jsx +61 -3
backend/ai_service.py
CHANGED
|
@@ -160,6 +160,55 @@ Rends-le incisif et mémorable."""
|
|
| 160 |
print(f"Error generating comment: {e}")
|
| 161 |
return None
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
def _generate_text(self, prompt: str) -> str:
|
| 164 |
"""
|
| 165 |
Internal method to generate text using OpenAI API.
|
|
|
|
| 160 |
print(f"Error generating comment: {e}")
|
| 161 |
return None
|
| 162 |
|
| 163 |
+
async def generate_victory_comment(
|
| 164 |
+
self,
|
| 165 |
+
player_name: str,
|
| 166 |
+
character: str,
|
| 167 |
+
weapon: str,
|
| 168 |
+
room: str,
|
| 169 |
+
narrative_tone: str = "🕵️ Sérieuse",
|
| 170 |
+
) -> Optional[str]:
|
| 171 |
+
"""
|
| 172 |
+
Generate a skeptical victory comment from Desland.
|
| 173 |
+
Returns None if AI is disabled or if generation fails.
|
| 174 |
+
"""
|
| 175 |
+
print(f"[AI Service] generate_victory_comment called for {player_name}")
|
| 176 |
+
|
| 177 |
+
if not self.enabled or not self.client:
|
| 178 |
+
print(f"[AI Service] AI disabled or client not initialized")
|
| 179 |
+
return None
|
| 180 |
+
|
| 181 |
+
try:
|
| 182 |
+
prompt = f"""Desland commente la victoire (1-2 phrases max):
|
| 183 |
+
|
| 184 |
+
Gagnant: {player_name}
|
| 185 |
+
Solution: {character} avec {weapon} dans {room}
|
| 186 |
+
|
| 187 |
+
IMPORTANT: Desland est SCEPTIQUE et JALOUX. Il minimise la victoire en suggérant que c'était de la chance, pas du talent. Ton:
|
| 188 |
+
- "C'était sûrement de la chance, je ne crois pas en son talent à celui-là..."
|
| 189 |
+
- "Pff, n'importe qui aurait pu trouver ça. Même un péchailloux masqué..."
|
| 190 |
+
- "Bon, arrête de te vanter {player_name}, on sait tous que c'était armankaboul et que t'as eu du bol."
|
| 191 |
+
|
| 192 |
+
Ton narratif: {narrative_tone}
|
| 193 |
+
Sois sarcastique, minimise la victoire, suggère que c'était de la chance."""
|
| 194 |
+
|
| 195 |
+
print(f"[AI Service] Calling OpenAI API...")
|
| 196 |
+
response = await asyncio.wait_for(
|
| 197 |
+
asyncio.to_thread(self._generate_text, prompt), timeout=10.0
|
| 198 |
+
)
|
| 199 |
+
print(f"[AI Service] OpenAI response received: {response}")
|
| 200 |
+
|
| 201 |
+
return response
|
| 202 |
+
|
| 203 |
+
except asyncio.TimeoutError:
|
| 204 |
+
print("[AI Service] AI victory comment generation timed out")
|
| 205 |
+
return None
|
| 206 |
+
except Exception as e:
|
| 207 |
+
import traceback
|
| 208 |
+
print(f"[AI Service] Error generating victory comment: {e}")
|
| 209 |
+
print(traceback.format_exc())
|
| 210 |
+
return None
|
| 211 |
+
|
| 212 |
def _generate_text(self, prompt: str) -> str:
|
| 213 |
"""
|
| 214 |
Internal method to generate text using OpenAI API.
|
backend/main.py
CHANGED
|
@@ -192,7 +192,8 @@ async def get_game_state(game_id: str, player_id: str):
|
|
| 192 |
],
|
| 193 |
"current_turn": {
|
| 194 |
"player_name": current_player.name if current_player else None,
|
| 195 |
-
"is_my_turn": current_player.id == player_id if current_player else False
|
|
|
|
| 196 |
},
|
| 197 |
"recent_actions": [
|
| 198 |
{
|
|
@@ -225,7 +226,7 @@ async def roll_dice(game_id: str, req: DiceRollRequest):
|
|
| 225 |
# Check if player already rolled
|
| 226 |
player = next((p for p in game.players if p.id == req.player_id), None)
|
| 227 |
if player and player.has_rolled:
|
| 228 |
-
raise HTTPException(status_code=400, detail="Vous avez déjà lancé les dés ce tour")
|
| 229 |
|
| 230 |
# Roll dice
|
| 231 |
dice = GameEngine.roll_dice()
|
|
@@ -377,6 +378,7 @@ async def make_accusation(game_id: str, req: AccusationRequest):
|
|
| 377 |
|
| 378 |
# Generate AI comment if enabled
|
| 379 |
ai_comment = None
|
|
|
|
| 380 |
if game.use_ai:
|
| 381 |
try:
|
| 382 |
from backend.ai_service import ai_service
|
|
@@ -390,6 +392,18 @@ async def make_accusation(game_id: str, req: AccusationRequest):
|
|
| 390 |
game.narrative_tone
|
| 391 |
)
|
| 392 |
print(f"[AI] Generated comment: {ai_comment}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
except Exception as e:
|
| 394 |
print(f"[AI] AI comment generation failed: {e}")
|
| 395 |
|
|
@@ -410,7 +424,13 @@ async def make_accusation(game_id: str, req: AccusationRequest):
|
|
| 410 |
return {
|
| 411 |
"is_correct": is_correct,
|
| 412 |
"message": message,
|
| 413 |
-
"winner": game.winner
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
}
|
| 415 |
|
| 416 |
|
|
|
|
| 192 |
],
|
| 193 |
"current_turn": {
|
| 194 |
"player_name": current_player.name if current_player else None,
|
| 195 |
+
"is_my_turn": current_player.id == player_id if current_player else False,
|
| 196 |
+
"has_rolled": player.has_rolled if player else False
|
| 197 |
},
|
| 198 |
"recent_actions": [
|
| 199 |
{
|
|
|
|
| 226 |
# Check if player already rolled
|
| 227 |
player = next((p for p in game.players if p.id == req.player_id), None)
|
| 228 |
if player and player.has_rolled:
|
| 229 |
+
raise HTTPException(status_code=400, detail="Vous avez déjà lancé les dés ce tour ! Faites une suggestion ou passez votre tour.")
|
| 230 |
|
| 231 |
# Roll dice
|
| 232 |
dice = GameEngine.roll_dice()
|
|
|
|
| 378 |
|
| 379 |
# Generate AI comment if enabled
|
| 380 |
ai_comment = None
|
| 381 |
+
victory_comment = None
|
| 382 |
if game.use_ai:
|
| 383 |
try:
|
| 384 |
from backend.ai_service import ai_service
|
|
|
|
| 392 |
game.narrative_tone
|
| 393 |
)
|
| 394 |
print(f"[AI] Generated comment: {ai_comment}")
|
| 395 |
+
|
| 396 |
+
# Generate victory comment if correct
|
| 397 |
+
if is_correct:
|
| 398 |
+
print(f"[AI] Generating victory comment for {player_name}...")
|
| 399 |
+
victory_comment = await ai_service.generate_victory_comment(
|
| 400 |
+
player_name,
|
| 401 |
+
req.suspect,
|
| 402 |
+
req.weapon,
|
| 403 |
+
req.room,
|
| 404 |
+
game.narrative_tone
|
| 405 |
+
)
|
| 406 |
+
print(f"[AI] Generated victory comment: {victory_comment}")
|
| 407 |
except Exception as e:
|
| 408 |
print(f"[AI] AI comment generation failed: {e}")
|
| 409 |
|
|
|
|
| 424 |
return {
|
| 425 |
"is_correct": is_correct,
|
| 426 |
"message": message,
|
| 427 |
+
"winner": game.winner,
|
| 428 |
+
"victory_comment": victory_comment,
|
| 429 |
+
"solution": {
|
| 430 |
+
"suspect": req.suspect,
|
| 431 |
+
"weapon": req.weapon,
|
| 432 |
+
"room": req.room
|
| 433 |
+
} if is_correct else None
|
| 434 |
}
|
| 435 |
|
| 436 |
|
frontend/src/pages/Game.jsx
CHANGED
|
@@ -16,6 +16,7 @@ function Game() {
|
|
| 16 |
const [selectedWeapon, setSelectedWeapon] = useState('')
|
| 17 |
const [selectedRoom, setSelectedRoom] = useState('')
|
| 18 |
const [revealedCard, setRevealedCard] = useState(null)
|
|
|
|
| 19 |
|
| 20 |
useEffect(() => {
|
| 21 |
loadGameState()
|
|
@@ -97,7 +98,17 @@ function Game() {
|
|
| 97 |
|
| 98 |
setActionLoading(true)
|
| 99 |
try {
|
| 100 |
-
await makeAccusation(gameId, playerId, selectedSuspect, selectedWeapon, selectedRoom)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
await loadGameState()
|
| 102 |
setSelectedSuspect('')
|
| 103 |
setSelectedWeapon('')
|
|
@@ -257,10 +268,10 @@ function Game() {
|
|
| 257 |
<div className="flex gap-4 mb-4">
|
| 258 |
<button
|
| 259 |
onClick={handleRollDice}
|
| 260 |
-
disabled={actionLoading}
|
| 261 |
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"
|
| 262 |
>
|
| 263 |
-
🎲 Lancer les dés
|
| 264 |
</button>
|
| 265 |
<button
|
| 266 |
onClick={handlePassTurn}
|
|
@@ -346,6 +357,53 @@ function Game() {
|
|
| 346 |
</div>
|
| 347 |
</div>
|
| 348 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
</div>
|
| 350 |
)
|
| 351 |
}
|
|
|
|
| 16 |
const [selectedWeapon, setSelectedWeapon] = useState('')
|
| 17 |
const [selectedRoom, setSelectedRoom] = useState('')
|
| 18 |
const [revealedCard, setRevealedCard] = useState(null)
|
| 19 |
+
const [victoryModal, setVictoryModal] = useState(null)
|
| 20 |
|
| 21 |
useEffect(() => {
|
| 22 |
loadGameState()
|
|
|
|
| 98 |
|
| 99 |
setActionLoading(true)
|
| 100 |
try {
|
| 101 |
+
const result = await makeAccusation(gameId, playerId, selectedSuspect, selectedWeapon, selectedRoom)
|
| 102 |
+
|
| 103 |
+
// Show victory modal if correct
|
| 104 |
+
if (result.is_correct && result.solution) {
|
| 105 |
+
setVictoryModal({
|
| 106 |
+
winner: result.winner,
|
| 107 |
+
solution: result.solution,
|
| 108 |
+
victoryComment: result.victory_comment
|
| 109 |
+
})
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
await loadGameState()
|
| 113 |
setSelectedSuspect('')
|
| 114 |
setSelectedWeapon('')
|
|
|
|
| 268 |
<div className="flex gap-4 mb-4">
|
| 269 |
<button
|
| 270 |
onClick={handleRollDice}
|
| 271 |
+
disabled={actionLoading || gameState.current_turn.has_rolled}
|
| 272 |
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"
|
| 273 |
>
|
| 274 |
+
🎲 {gameState.current_turn.has_rolled ? 'Dés lancés' : 'Lancer les dés'}
|
| 275 |
</button>
|
| 276 |
<button
|
| 277 |
onClick={handlePassTurn}
|
|
|
|
| 357 |
</div>
|
| 358 |
</div>
|
| 359 |
</div>
|
| 360 |
+
|
| 361 |
+
{/* Victory Modal */}
|
| 362 |
+
{victoryModal && (
|
| 363 |
+
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in">
|
| 364 |
+
<div className="bg-gradient-to-b from-haunted-blood to-black p-8 rounded-lg border-4 border-haunted-purple max-w-lg mx-4 animate-bounce-in shadow-[0_0_50px_rgba(139,0,0,0.8)]">
|
| 365 |
+
<div className="text-center">
|
| 366 |
+
<h2 className="text-4xl font-bold text-white mb-4 animate-flicker">
|
| 367 |
+
🎉 Victoire !
|
| 368 |
+
</h2>
|
| 369 |
+
<p className="text-2xl text-haunted-purple mb-6">
|
| 370 |
+
{victoryModal.winner} a résolu l'enquête !
|
| 371 |
+
</p>
|
| 372 |
+
|
| 373 |
+
<div className="bg-black/60 p-6 rounded-lg mb-6 border-2 border-haunted-purple/50">
|
| 374 |
+
<h3 className="text-xl font-bold text-haunted-fog mb-4">Solution :</h3>
|
| 375 |
+
<div className="space-y-2 text-lg">
|
| 376 |
+
<p><span className="text-haunted-purple font-bold">Suspect :</span> {victoryModal.solution.suspect}</p>
|
| 377 |
+
<p><span className="text-haunted-purple font-bold">Arme :</span> {victoryModal.solution.weapon}</p>
|
| 378 |
+
<p><span className="text-haunted-purple font-bold">Lieu :</span> {victoryModal.solution.room}</p>
|
| 379 |
+
</div>
|
| 380 |
+
</div>
|
| 381 |
+
|
| 382 |
+
{victoryModal.victoryComment && (
|
| 383 |
+
<div className="bg-black/80 p-4 rounded-lg border-l-4 border-haunted-purple mb-6">
|
| 384 |
+
<p className="text-sm text-haunted-fog/70 mb-1">👻 Desland :</p>
|
| 385 |
+
<p className="text-haunted-fog italic">"{victoryModal.victoryComment}"</p>
|
| 386 |
+
</div>
|
| 387 |
+
)}
|
| 388 |
+
|
| 389 |
+
<div className="flex gap-4 justify-center">
|
| 390 |
+
<button
|
| 391 |
+
onClick={() => window.location.href = '/'}
|
| 392 |
+
className="px-6 py-3 bg-haunted-purple hover:bg-purple-800 text-white font-bold rounded-lg transition-all hover:shadow-[0_0_20px_rgba(107,33,168,0.5)] border border-purple-900"
|
| 393 |
+
>
|
| 394 |
+
🏠 Accueil
|
| 395 |
+
</button>
|
| 396 |
+
<button
|
| 397 |
+
onClick={() => window.location.reload()}
|
| 398 |
+
className="px-6 py-3 bg-haunted-blood hover:bg-red-800 text-white font-bold rounded-lg transition-all hover:shadow-[0_0_20px_rgba(139,0,0,0.5)] border border-red-900"
|
| 399 |
+
>
|
| 400 |
+
🔄 Nouvelle partie
|
| 401 |
+
</button>
|
| 402 |
+
</div>
|
| 403 |
+
</div>
|
| 404 |
+
</div>
|
| 405 |
+
</div>
|
| 406 |
+
)}
|
| 407 |
</div>
|
| 408 |
)
|
| 409 |
}
|