clementpep commited on
Commit
496e0a2
·
1 Parent(s): 56dd4cc

fix: game logic and ai comments

Browse files
ai_service.py → backend/ai_service.py RENAMED
@@ -5,7 +5,7 @@ Only active when USE_OPENAI environment variable is set to true.
5
 
6
  from typing import Optional
7
  from openai import OpenAI
8
- from config import settings
9
  import asyncio
10
 
11
 
 
5
 
6
  from typing import Optional
7
  from openai import OpenAI
8
+ from backend.config import settings
9
  import asyncio
10
 
11
 
backend/game_engine.py CHANGED
@@ -214,7 +214,7 @@ class GameEngine:
214
  ai_comment: Optional[str] = None
215
  ):
216
  """Add a turn record to the game history."""
217
- from models import Turn
218
 
219
  player = next((p for p in game.players if p.id == player_id), None)
220
  if not player:
@@ -250,13 +250,14 @@ class GameEngine:
250
 
251
  @staticmethod
252
  def roll_dice() -> int:
253
- """Roll two dice and return the sum (2-12)."""
254
- return random.randint(1, 6) + random.randint(1, 6)
255
 
256
  @staticmethod
257
  def move_player(game: Game, player_id: str, dice_roll: int) -> Tuple[bool, str, int]:
258
  """
259
- Move a player on the board based on dice roll.
 
260
  Returns (success, message, new_room_index).
261
  """
262
  player = next((p for p in game.players if p.id == player_id), None)
@@ -267,14 +268,14 @@ class GameEngine:
267
  if num_rooms == 0:
268
  return False, "Pas de pièces disponibles", -1
269
 
270
- # Calculate new position (circular movement)
271
- new_room_index = (player.current_room_index + dice_roll) % num_rooms
272
- old_room = game.rooms[player.current_room_index]
273
  new_room = game.rooms[new_room_index]
274
 
275
  player.current_room_index = new_room_index
276
 
277
- message = f"🎲 Dés: {dice_roll} | {old_room} → {new_room}"
278
  return True, message, new_room_index
279
 
280
  @staticmethod
 
214
  ai_comment: Optional[str] = None
215
  ):
216
  """Add a turn record to the game history."""
217
+ from backend.models import Turn
218
 
219
  player = next((p for p in game.players if p.id == player_id), None)
220
  if not player:
 
250
 
251
  @staticmethod
252
  def roll_dice() -> int:
253
+ """Roll a single die (1-6)."""
254
+ return random.randint(1, 6)
255
 
256
  @staticmethod
257
  def move_player(game: Game, player_id: str, dice_roll: int) -> Tuple[bool, str, int]:
258
  """
259
+ Move a player to a room based on dice roll.
260
+ Dice value maps directly to room index (1→room 0, 2→room 1, etc.)
261
  Returns (success, message, new_room_index).
262
  """
263
  player = next((p for p in game.players if p.id == player_id), None)
 
268
  if num_rooms == 0:
269
  return False, "Pas de pièces disponibles", -1
270
 
271
+ # Map dice roll to room (1-6 maps to rooms, if more rooms, use modulo)
272
+ new_room_index = (dice_roll - 1) % num_rooms
273
+ old_room = game.rooms[player.current_room_index] if player.current_room_index < num_rooms else "???"
274
  new_room = game.rooms[new_room_index]
275
 
276
  player.current_room_index = new_room_index
277
 
278
+ message = f"🎲 Dé: {dice_roll} → {new_room}"
279
  return True, message, new_room_index
280
 
281
  @staticmethod
backend/main.py CHANGED
@@ -69,7 +69,7 @@ async def quick_create_game(req: QuickCreateRequest):
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
 
@@ -128,9 +128,24 @@ async def start_game(game_id: str):
128
  raise HTTPException(status_code=400, detail="Cannot start game (need min 3 players)")
129
 
130
  game = game_manager.get_game(game_id.upper())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  return {
132
  "status": "started",
133
- "first_player": game.get_current_player().name if game else None
 
134
  }
135
 
136
 
@@ -155,6 +170,7 @@ async def get_game_state(game_id: str, player_id: str):
155
  "game_name": game.name,
156
  "status": game.status.value,
157
  "scenario": game.scenario,
 
158
  "rooms": game.rooms,
159
  "suspects": [c.name for c in game.characters],
160
  "weapons": [w.name for w in game.weapons],
@@ -183,7 +199,7 @@ async def get_game_state(game_id: str, player_id: str):
183
  "details": t.details,
184
  "ai_comment": t.ai_comment
185
  }
186
- for t in game.turns[-5:]
187
  ],
188
  "winner": game.winner
189
  }
@@ -204,6 +220,11 @@ async def roll_dice(game_id: str, req: DiceRollRequest):
204
  if not GameEngine.can_player_act(game, req.player_id):
205
  raise HTTPException(status_code=400, detail="Not your turn")
206
 
 
 
 
 
 
207
  # Roll dice
208
  dice = GameEngine.roll_dice()
209
 
@@ -213,8 +234,32 @@ async def roll_dice(game_id: str, req: DiceRollRequest):
213
  if not success:
214
  raise HTTPException(status_code=400, detail=msg)
215
 
216
- # Record turn
217
- GameEngine.add_turn_record(game, req.player_id, "move", msg)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  game_manager.save_games()
219
 
220
  return {
@@ -261,7 +306,7 @@ async def make_suggestion(game_id: str, req: SuggestionRequest):
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,
@@ -278,7 +323,10 @@ async def make_suggestion(game_id: str, req: SuggestionRequest):
278
  "suggestion": f"{req.suspect} + {req.weapon} + {req.room}",
279
  "was_disproven": can_disprove,
280
  "disprover": disprover if can_disprove else None,
281
- "card_shown": card.name if card else None
 
 
 
282
  }
283
 
284
  # Record turn with AI comment
@@ -327,7 +375,7 @@ async def make_accusation(game_id: str, req: AccusationRequest):
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,
 
69
  rooms=config["rooms"],
70
  custom_weapons=config["weapons"],
71
  custom_suspects=config["suspects"],
72
+ use_ai=True, # Enable AI by default
73
  board_layout=board_layout
74
  )
75
 
 
128
  raise HTTPException(status_code=400, detail="Cannot start game (need min 3 players)")
129
 
130
  game = game_manager.get_game(game_id.upper())
131
+
132
+ # Generate AI scenario if enabled
133
+ if game and game.use_ai and not game.scenario:
134
+ try:
135
+ from backend.ai_service import ai_service
136
+ game.scenario = await ai_service.generate_scenario(
137
+ game.rooms,
138
+ [c.name for c in game.characters],
139
+ game.narrative_tone
140
+ )
141
+ game_manager.save_games()
142
+ except Exception as e:
143
+ print(f"AI scenario generation failed: {e}")
144
+
145
  return {
146
  "status": "started",
147
+ "first_player": game.get_current_player().name if game else None,
148
+ "scenario": game.scenario if game else None
149
  }
150
 
151
 
 
170
  "game_name": game.name,
171
  "status": game.status.value,
172
  "scenario": game.scenario,
173
+ "use_ai": game.use_ai,
174
  "rooms": game.rooms,
175
  "suspects": [c.name for c in game.characters],
176
  "weapons": [w.name for w in game.weapons],
 
199
  "details": t.details,
200
  "ai_comment": t.ai_comment
201
  }
202
+ for t in game.turns[-10:] # Show more history
203
  ],
204
  "winner": game.winner
205
  }
 
220
  if not GameEngine.can_player_act(game, req.player_id):
221
  raise HTTPException(status_code=400, detail="Not your turn")
222
 
223
+ # Check if player already rolled
224
+ player = next((p for p in game.players if p.id == req.player_id), None)
225
+ if player and player.has_rolled:
226
+ raise HTTPException(status_code=400, detail="Vous avez déjà lancé les dés ce tour")
227
+
228
  # Roll dice
229
  dice = GameEngine.roll_dice()
230
 
 
234
  if not success:
235
  raise HTTPException(status_code=400, detail=msg)
236
 
237
+ # Get player name
238
+ player = next((p for p in game.players if p.id == req.player_id), None)
239
+ player_name = player.name if player else "Inconnu"
240
+
241
+ # Generate AI comment if enabled
242
+ ai_comment = None
243
+ if game.use_ai:
244
+ try:
245
+ from backend.ai_service import ai_service
246
+ # Simple comment about movement
247
+ prompts = [
248
+ f"{player_name} se dirige vers {game.rooms[new_pos]}... Intéressant choix.",
249
+ f"Ah, {game.rooms[new_pos]}. {player_name} pense y trouver quelque chose ?",
250
+ f"{player_name} va fouiner dans {game.rooms[new_pos]}. Bonne chance avec ça."
251
+ ]
252
+ import random
253
+ ai_comment = random.choice(prompts)
254
+ except Exception as e:
255
+ print(f"AI comment generation failed: {e}")
256
+
257
+ # Mark player as having rolled
258
+ if player:
259
+ player.has_rolled = True
260
+
261
+ # Record turn with AI comment
262
+ GameEngine.add_turn_record(game, req.player_id, "move", msg, ai_comment=ai_comment)
263
  game_manager.save_games()
264
 
265
  return {
 
306
  ai_comment = None
307
  if game.use_ai:
308
  try:
309
+ from backend.ai_service import ai_service
310
  import asyncio
311
  ai_comment = await ai_service.generate_suggestion_comment(
312
  player_name,
 
323
  "suggestion": f"{req.suspect} + {req.weapon} + {req.room}",
324
  "was_disproven": can_disprove,
325
  "disprover": disprover if can_disprove else None,
326
+ "card_shown": {
327
+ "name": card.name,
328
+ "type": card.card_type.value
329
+ } if card else None
330
  }
331
 
332
  # Record turn with AI comment
 
375
  ai_comment = None
376
  if game.use_ai:
377
  try:
378
+ from backend.ai_service import ai_service
379
  ai_comment = await ai_service.generate_accusation_comment(
380
  player_name,
381
  req.suspect,
backend/models.py CHANGED
@@ -39,6 +39,7 @@ class Player(BaseModel):
39
  cards: List[Card] = Field(default_factory=list)
40
  is_active: bool = True
41
  current_room_index: int = 0 # Position on the board
 
42
 
43
 
44
  class GameStatus(str, Enum):
@@ -155,6 +156,10 @@ class Game(BaseModel):
155
  if not self.players:
156
  return
157
 
 
 
 
 
158
  # Skip eliminated players
159
  attempts = 0
160
  while attempts < len(self.players):
 
39
  cards: List[Card] = Field(default_factory=list)
40
  is_active: bool = True
41
  current_room_index: int = 0 # Position on the board
42
+ has_rolled: bool = False # Track if player rolled dice this turn
43
 
44
 
45
  class GameStatus(str, Enum):
 
156
  if not self.players:
157
  return
158
 
159
+ # Reset has_rolled for current player
160
+ if self.current_player_index < len(self.players):
161
+ self.players[self.current_player_index].has_rolled = False
162
+
163
  # Skip eliminated players
164
  attempts = 0
165
  while attempts < len(self.players):
backend/requirements.txt CHANGED
@@ -2,3 +2,4 @@ fastapi==0.104.1
2
  uvicorn[standard]==0.24.0
3
  pydantic==2.5.0
4
  python-multipart==0.0.6
 
 
2
  uvicorn[standard]==0.24.0
3
  pydantic==2.5.0
4
  python-multipart==0.0.6
5
+ openai==1.3.0
frontend/src/pages/Game.jsx CHANGED
@@ -15,6 +15,7 @@ function Game() {
15
  const [selectedSuspect, setSelectedSuspect] = useState('')
16
  const [selectedWeapon, setSelectedWeapon] = useState('')
17
  const [selectedRoom, setSelectedRoom] = useState('')
 
18
 
19
  useEffect(() => {
20
  loadGameState()
@@ -63,7 +64,19 @@ function Game() {
63
  }
64
  setActionLoading(true)
65
  try {
66
- await makeSuggestion(gameId, playerId, selectedSuspect, selectedWeapon, selectedRoom)
 
 
 
 
 
 
 
 
 
 
 
 
67
  await loadGameState()
68
  setSelectedSuspect('')
69
  setSelectedWeapon('')
@@ -117,8 +130,9 @@ function Game() {
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">
@@ -155,6 +169,14 @@ function Game() {
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">
@@ -198,14 +220,39 @@ function Game() {
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
 
15
  const [selectedSuspect, setSelectedSuspect] = useState('')
16
  const [selectedWeapon, setSelectedWeapon] = useState('')
17
  const [selectedRoom, setSelectedRoom] = useState('')
18
+ const [revealedCard, setRevealedCard] = useState(null)
19
 
20
  useEffect(() => {
21
  loadGameState()
 
64
  }
65
  setActionLoading(true)
66
  try {
67
+ const result = await makeSuggestion(gameId, playerId, selectedSuspect, selectedWeapon, selectedRoom)
68
+
69
+ // Show revealed card if any
70
+ if (result.data.card_shown) {
71
+ setRevealedCard({
72
+ ...result.data.card_shown,
73
+ disprover: result.data.disprover
74
+ })
75
+ setTimeout(() => setRevealedCard(null), 5000) // Hide after 5 seconds
76
+ } else {
77
+ alert('Personne ne peut réfuter votre suggestion !')
78
+ }
79
+
80
  await loadGameState()
81
  setSelectedSuspect('')
82
  setSelectedWeapon('')
 
130
  }
131
 
132
  const me = gameState.players.find(p => p.is_me)
133
+ const isMyTurn = gameState.current_turn?.is_my_turn && me?.is_active
134
  const canStart = gameState.status === 'waiting' && gameState.players.length >= 3
135
+ const isEliminated = me && !me.is_active
136
 
137
  return (
138
  <div className="min-h-screen bg-haunted-gradient p-4 relative overflow-hidden">
 
169
  )}
170
  </div>
171
 
172
+ {/* Scenario */}
173
+ {gameState.scenario && gameState.status === 'in_progress' && (
174
+ <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-purple/30">
175
+ <h2 className="text-xl font-bold text-haunted-purple mb-3 animate-flicker">📜 Le Mystère</h2>
176
+ <p className="text-haunted-fog italic">{gameState.scenario}</p>
177
+ </div>
178
+ )}
179
+
180
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
181
  {/* Game Board */}
182
  <div className="lg:col-span-2">
 
220
  />
221
  )}
222
 
223
+ {/* Revealed Card Modal */}
224
+ {revealedCard && (
225
+ <div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 animate-fade-in">
226
+ <div className="bg-black/90 border-4 border-haunted-blood p-8 rounded-lg shadow-2xl max-w-md">
227
+ <h3 className="text-2xl font-bold text-haunted-blood mb-4 animate-flicker">🃏 Carte Révélée</h3>
228
+ <p className="text-haunted-fog mb-2">
229
+ <span className="font-semibold">{revealedCard.disprover}</span> vous montre :
230
+ </p>
231
+ <div className="bg-haunted-blood/20 border-2 border-haunted-blood p-4 rounded-lg">
232
+ <p className="text-2xl font-bold text-haunted-fog text-center">
233
+ {revealedCard.type === 'character' && '👤 '}
234
+ {revealedCard.type === 'weapon' && '🔪 '}
235
+ {revealedCard.type === 'room' && '🏚️ '}
236
+ {revealedCard.name}
237
+ </p>
238
+ </div>
239
+ <p className="text-haunted-fog/60 text-sm mt-4 text-center italic">
240
+ Cette carte disparaîtra dans quelques secondes...
241
+ </p>
242
+ </div>
243
+ </div>
244
+ )}
245
+
246
  {/* Actions */}
247
  {gameState.status === 'in_progress' && (
248
  <div className="bg-black/60 backdrop-blur-md p-6 rounded-lg border-2 border-haunted-blood/30">
249
  <h2 className="text-2xl font-bold text-haunted-blood mb-4 animate-flicker">
250
+ {isEliminated ? '💀 Vous êtes éliminé - Vous pouvez toujours observer' :
251
+ isMyTurn ? '⚡ À vous de jouer !' :
252
+ '⏳ ' + gameState.current_turn?.player_name + ' enquête...'}
253
  </h2>
254
 
255
+ {isMyTurn && !isEliminated && (
256
  <div className="space-y-4">
257
  <div className="flex gap-4 mb-4">
258
  <button