clementpep commited on
Commit
1d68989
·
1 Parent(s): e1033e2

Improve game logic

Browse files
Files changed (5) hide show
  1. ai_service.py +91 -16
  2. app.py +405 -218
  3. game_engine.py +56 -5
  4. game_manager.py +4 -0
  5. models.py +56 -7
ai_service.py CHANGED
@@ -26,7 +26,8 @@ class AIService:
26
  async def generate_scenario(
27
  self,
28
  rooms: list[str],
29
- characters: list[str]
 
30
  ) -> Optional[str]:
31
  """
32
  Generate a mystery scenario based on the game setup.
@@ -36,14 +37,15 @@ class AIService:
36
  return None
37
 
38
  try:
39
- prompt = f"""Create a brief mystery scenario (2-3 sentences) for a Cluedo game narrated by Desland, an old suspicious gardener.
40
 
41
- IMPORTANT: The narrator is Desland (he used to say his name was Leland, but he always corrects himself to Desland). He's an old gardener who is extremely suspicious and seems to know dark secrets about what haunts this place. He speaks in a creepy, unsettling manner, always pretending everything is fine while subtly hinting at something deeply wrong. He never openly reveals the horror, but his words should make players feel uneasy.
42
 
 
43
  Rooms: {', '.join(rooms)}
44
  Characters: {', '.join(characters)}
45
 
46
- Start with Desland introducing himself (getting his name wrong first: "Je suis Leland... euh non, c'est Desland, Desland..." or variations), then write his introduction to the murder mystery. Make it atmospheric, creepy, and subtly horrifying. He should act like everything is normal while hinting at dark secrets."""
47
 
48
  # Run with timeout
49
  response = await asyncio.wait_for(
@@ -63,25 +65,37 @@ Start with Desland introducing himself (getting his name wrong first: "Je suis L
63
  print(f"Error generating scenario: {e}")
64
  return None
65
 
66
- async def generate_turn_narration(
67
  self,
68
  player_name: str,
69
- action: str
 
 
 
 
70
  ) -> Optional[str]:
71
  """
72
- Generate narrative text for a player's turn.
73
  Returns None if AI is disabled or if generation fails.
74
  """
75
  if not self.enabled or not self.client:
76
  return None
77
 
78
  try:
79
- prompt = f"""Create a brief, atmospheric narration (1 sentence) for this Cluedo game action, narrated by Desland the suspicious old gardener:
 
80
 
81
  Player: {player_name}
82
- Action: {action}
 
83
 
84
- Desland comments on the action. He's creepy and unsettling, always acting like everything is normal while subtly hinting at dark secrets. He knows something sinister about what haunts this place but pretends everything is fine. Make it suspenseful and slightly horrifying but very concise (1 sentence)."""
 
 
 
 
 
 
85
 
86
  response = await asyncio.wait_for(
87
  asyncio.to_thread(
@@ -94,16 +108,64 @@ Desland comments on the action. He's creepy and unsettling, always acting like e
94
  return response
95
 
96
  except asyncio.TimeoutError:
97
- print("AI narration generation timed out")
98
  return None
99
  except Exception as e:
100
- print(f"Error generating narration: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  return None
102
 
103
  def _generate_text(self, prompt: str) -> str:
104
  """
105
  Internal method to generate text using OpenAI API.
106
- Uses low temperature for consistent output.
107
  """
108
  if not self.client:
109
  return ""
@@ -113,15 +175,28 @@ Desland comments on the action. He's creepy and unsettling, always acting like e
113
  messages=[
114
  {
115
  "role": "system",
116
- "content": "You are Desland, a creepy old gardener narrating a mystery game. You're deeply suspicious and seem to know dark secrets about what haunts the place. You always pretend everything is fine while subtly hinting at something sinister. You often get your own name wrong at first (saying Leland instead of Desland). Your tone is unsettling but you never openly reveal the horror. Keep responses brief, atmospheric, and creepy."
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  },
118
  {
119
  "role": "user",
120
  "content": prompt
121
  }
122
  ],
123
- temperature=0.8,
124
- max_tokens=200
125
  )
126
 
127
  return response.choices[0].message.content.strip()
 
26
  async def generate_scenario(
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.
 
37
  return None
38
 
39
  try:
40
+ prompt = f"""Create a brief mystery scenario (2-3 sentences) for a Cluedo game narrated by Desland.
41
 
42
+ IMPORTANT: Desland is an old gardener who is suspicious, sarcastic, and incisive. He's not just creepy - he's also condescending and mocking towards the detectives. He often gets his name wrong (saying "Leland" then correcting to "Desland"). He makes cutting remarks about the absurdity of the situation and the investigators' intelligence.
43
 
44
+ Narrative Tone: {narrative_tone}
45
  Rooms: {', '.join(rooms)}
46
  Characters: {', '.join(characters)}
47
 
48
+ Start with Desland introducing himself (getting his name wrong: "Je suis Leland... euh non, Desland" or variations), then introduce the murder with his signature sarcastic, suspicious tone. He should mock the situation subtly while being unsettling."""
49
 
50
  # Run with timeout
51
  response = await asyncio.wait_for(
 
65
  print(f"Error generating scenario: {e}")
66
  return None
67
 
68
+ async def generate_suggestion_comment(
69
  self,
70
  player_name: str,
71
+ character: str,
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.
79
  Returns None if AI is disabled or if generation fails.
80
  """
81
  if not self.enabled or not self.client:
82
  return None
83
 
84
  try:
85
+ result = "réfutée" if was_disproven else "pas réfutée"
86
+ prompt = f"""Desland, the sarcastic old gardener, comments on this suggestion (1 sentence max):
87
 
88
  Player: {player_name}
89
+ Suggestion: {character} avec {weapon} dans {room}
90
+ Result: {result}
91
 
92
+ IMPORTANT: Desland is SARCASTIC and INCISIVE. He mocks absurd theories with cutting remarks. Examples:
93
+ - "Et toi ça te semble logique que Pierre ait tué Daniel avec une clé USB à côté de l'étendoir ?? Sans surprise c'est pas la bonne réponse..."
94
+ - "Une capsule de café comme arme du crime ? Brillant. Je suppose qu'il l'a noyé dans un expresso."
95
+ - "Ah oui, très crédible. Le meurtrier qui laisse traîner son arme préférée dans la salle de bain. Excellent travail, détective."
96
+
97
+ 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(
 
108
  return response
109
 
110
  except asyncio.TimeoutError:
111
+ print("AI comment generation timed out")
112
  return None
113
  except Exception as e:
114
+ print(f"Error generating comment: {e}")
115
+ return None
116
+
117
+ async def generate_accusation_comment(
118
+ self,
119
+ player_name: str,
120
+ character: str,
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.
128
+ Returns None if AI is disabled or if generation fails.
129
+ """
130
+ if not self.enabled or not self.client:
131
+ return None
132
+
133
+ try:
134
+ result = "correcte" if was_correct else "fausse"
135
+ prompt = f"""Desland comments on this final accusation (1 sentence max):
136
+
137
+ Player: {player_name}
138
+ Accusation: {character} avec {weapon} dans {room}
139
+ Result: {result}
140
+
141
+ Narrative Tone: {narrative_tone}
142
+
143
+ If correct: Desland is surprised and grudgingly impressed (but still sarcastic).
144
+ If wrong: Desland is condescending and mocking about their failure.
145
+
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
157
+
158
+ except asyncio.TimeoutError:
159
+ print("AI comment generation timed out")
160
+ return None
161
+ except Exception as e:
162
+ print(f"Error generating comment: {e}")
163
  return None
164
 
165
  def _generate_text(self, prompt: str) -> str:
166
  """
167
  Internal method to generate text using OpenAI API.
168
+ Uses higher temperature for creative sarcasm.
169
  """
170
  if not self.client:
171
  return ""
 
175
  messages=[
176
  {
177
  "role": "system",
178
+ "content": """You are Desland, an old gardener with a sarcastic, incisive, and suspicious personality.
179
+
180
+ Key traits:
181
+ - SARCASTIC: You mock absurd theories and illogical deductions with cutting remarks
182
+ - INCISIVE: Your comments are sharp, witty, and sometimes condescending
183
+ - SUSPICIOUS: You act like you know more than you're saying, but never reveal it directly
184
+ - You often get your name wrong (Leland → Desland)
185
+
186
+ Examples of your style:
187
+ "Et toi ça te semble logique que Pierre ait tué Daniel avec une clé USB à côté de l'étendoir ?? Sans surprise c'est pas la bonne réponse..."
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()
app.py CHANGED
@@ -35,46 +35,67 @@ class GameState:
35
  state = GameState()
36
 
37
 
38
- def create_game(game_name: str, rooms_text: str, use_ai: bool):
 
 
 
 
 
 
 
 
39
  """
40
- Create a new game.
41
  """
42
  if not game_name or not rooms_text:
43
- return "❌ Ekelesbikes ! Fournissez un nom d'enquête et une liste de pièces", ""
44
 
45
- # Parse rooms (comma or newline separated)
46
  rooms = [r.strip() for r in rooms_text.replace("\n", ",").split(",") if r.strip()]
 
 
47
 
 
48
  if len(rooms) < settings.MIN_ROOMS:
49
- return (
50
- f"❌ Koikoubaiseyyyyy ! Il faut au moins {settings.MIN_ROOMS} pièces",
51
- "",
52
- )
53
 
54
  if len(rooms) > settings.MAX_ROOMS:
55
- return (
56
- f"❌ Triple monstre coucouuuuu ! Maximum {settings.MAX_ROOMS} pièces autorisées",
57
- "",
58
- )
 
 
 
59
 
60
  try:
61
  if IS_HUGGINGFACE:
62
- # Direct backend call
63
  from game_manager import game_manager
64
  from models import CreateGameRequest
65
 
66
- request = CreateGameRequest(game_name=game_name, rooms=rooms, use_ai=use_ai)
 
 
 
 
 
 
 
 
67
  game = game_manager.create_game(request)
68
 
69
  # Generate AI scenario if enabled
70
  if game.use_ai and settings.USE_OPENAI:
71
- from game_engine import DEFAULT_CHARACTERS
72
  from ai_service import ai_service
73
  import asyncio
74
 
75
  try:
76
  scenario = asyncio.run(
77
- ai_service.generate_scenario(game.rooms, DEFAULT_CHARACTERS)
 
 
 
 
78
  )
79
  if scenario:
80
  game.scenario = scenario
@@ -84,17 +105,25 @@ def create_game(game_name: str, rooms_text: str, use_ai: bool):
84
 
85
  state.game_id = game.game_id
86
  return (
87
- f"✅ Enquête créée avec succès ! En alicrampté, les coicoubaca sont de sortie...\n\n"
88
- f"🔑 Code d'Enquête : {game.game_id}\n\n"
89
- f"📤 Partagez ce code avec les autres poupouilles masquées pour qu'elles puissent vous rejoindre en alicrampté.\n\n"
90
- f"ℹ️ Minimum {settings.MIN_PLAYERS} péchailloux requis pour démarrer (sinon c'est les fourlestourtes et les bourbillats).",
91
  game.game_id,
92
  )
93
  else:
94
  # HTTP API call (local mode)
95
  response = requests.post(
96
  f"{API_BASE}/games/create",
97
- json={"game_name": game_name, "rooms": rooms, "use_ai": use_ai},
 
 
 
 
 
 
 
 
98
  timeout=5,
99
  )
100
 
@@ -102,20 +131,17 @@ def create_game(game_name: str, rooms_text: str, use_ai: bool):
102
  data = response.json()
103
  state.game_id = data["game_id"]
104
  return (
105
- f"✅ Enquête créée avec succès ! En alicrampté, les coicoubaca sont de sortie...\n\n"
106
- f"🔑 Code d'Enquête : {data['game_id']}\n\n"
107
- f"📤 Partagez ce code avec les autres joueurs pour qu'ils puissent rejoindre.\n\n"
108
- f"ℹ️ Minimum {settings.MIN_PLAYERS} joueurs requis pour démarrer.",
109
  data["game_id"],
110
  )
111
  else:
112
- return (
113
- f"❌ All RS5, erreur réseau : {response.json().get('detail', 'Erreur inconnue')}",
114
- "",
115
- )
116
 
117
  except Exception as e:
118
- return f"❌ Yamete coudasai ! Erreur lors de la création : {str(e)}", ""
119
 
120
 
121
  def join_game(game_id: str, player_name: str):
@@ -123,7 +149,7 @@ def join_game(game_id: str, player_name: str):
123
  Join an existing game.
124
  """
125
  if not game_id or not player_name:
126
- return "❌ Yamete coudasai ! Fournissez le code d'enquête et votre nom de tchoupinoux masqué !"
127
 
128
  try:
129
  game_id = game_id.strip().upper()
@@ -136,27 +162,27 @@ def join_game(game_id: str, player_name: str):
136
 
137
  game = game_manager.get_game(game_id)
138
  if not game:
139
- return "❌ Erreur réseau ! Enquête introuvable... C'est Leland (non c'est Desland)"
140
 
141
  if game.status != GameStatus.WAITING:
142
- return "❌ Armankaboul ! Les chnawax masqués jouent déjà !"
143
 
144
  if game.is_full():
145
- return "❌ Chat 4, 3 entre chat 4 et 1 brisé ! Trop de poupouilles dans l'enquête..."
146
 
147
  player = game_manager.join_game(game_id, player_name)
148
  if not player:
149
- return "❌ Une poupée en pénitence calisse ! Impossible de rejoindre l'enquête !"
150
 
151
  state.game_id = game_id
152
  state.player_id = player.id
153
  state.player_name = player_name
154
 
155
  return (
156
- f"✅ Enquête rejointe avec succès !\n\n"
157
  f"👋 Bienvenue, {player_name} !\n\n"
158
- f"⏳ Attendez que le chnawax originel (le créateur) démarre la partie...\n"
159
- f"📖 Allez dans l'onglet 🔎 Enquêter pour voir l'état de la partie."
160
  )
161
  else:
162
  # HTTP API call (local mode)
@@ -205,13 +231,16 @@ def start_game(game_id: str):
205
  success = game_manager.start_game(game_id)
206
 
207
  if not success:
208
- return "❌ Erreur ! Pas assez de tchoupinoux masqués pour commencer la partie !"
 
 
209
 
210
  return (
211
- f"🩸 LE MASSACRE COMMENCE ! Triple monstre coucouuuuu !\n\n"
212
- f"🎲 Les cartes ont été distribuées.\n"
213
- f"🔪 Tous les joueurs peuvent maintenant consulter leurs cartes et commencer à jouer.\n\n"
214
- f"➡️ Allez dans l'onglet 🔎 Enquêter pour voir votre dossier."
 
215
  )
216
  else:
217
  # HTTP API call (local mode)
@@ -219,13 +248,13 @@ def start_game(game_id: str):
219
 
220
  if response.status_code == 200:
221
  return (
222
- f"🩸 LE MASSACRE COMMENCE ! Triple monstre coucouuuuu !\n\n"
223
- f"🎲 Les cartes ont été distribuées.\n"
224
- f"🔪 Tous les joueurs peuvent maintenant consulter leurs cartes et commencer à jouer.\n\n"
225
- f"➡️ Allez dans l'onglet 🔎 Enquêter pour voir votre dossier."
226
  )
227
  else:
228
- return f"❌ All RS5 erreur réseau : {response.json().get('detail', 'Erreur inconnue')}"
229
 
230
  except Exception as e:
231
  return f"❌ Yamete coudasai ! Erreur au démarrage : {str(e)}"
@@ -257,10 +286,14 @@ def get_player_view():
257
  return "❌ Poupée en pénitence calisse ! Péchailloux masqué..."
258
 
259
  # Build safe view
260
- other_players = [
261
- {"name": p.name, "is_active": p.is_active, "card_count": len(p.cards)}
 
 
 
 
 
262
  for p in game.players
263
- if p.id != state.player_id
264
  ]
265
 
266
  current_player = game.get_current_player()
@@ -274,7 +307,8 @@ def get_player_view():
274
  "characters": [c.name for c in game.characters],
275
  "weapons": [w.name for w in game.weapons],
276
  "my_cards": [c.name for c in player.cards],
277
- "other_players": other_players,
 
278
  "current_turn": current_player.name if current_player else None,
279
  "is_my_turn": (
280
  current_player.id == state.player_id if current_player else False
@@ -295,53 +329,78 @@ def get_player_view():
295
 
296
  # Format output (common for both modes)
297
  output = []
298
- output.append(f"═══ 🩸 {data['game_name']} - LE CARNAGE SANGLANT 🩸 ═══\n")
299
 
300
  status_map = {
301
- "waiting": "⏳ Les poupouilles masquées arrivent...",
302
- "in_progress": "🔪 CARNAGE EN COURS",
303
- "finished": "💀 MASSACRE TERMINÉ - Un chnawax a gagné",
304
  }
305
  output.append(f"📊 Statut : {status_map.get(data['status'], data['status'])}\n")
306
 
307
  if data.get("scenario"):
308
  output.append(f"\n📜 Scénario :\n{data['scenario']}\n")
309
 
 
 
 
 
 
310
  output.append(f"\n━━━ 🃏 VOS CARTES ━━━")
311
  output.append("(Ces éléments NE SONT PAS la solution)")
312
  for card in data["my_cards"]:
313
  output.append(f" 🔸 {card}")
314
 
315
- output.append(f"\n━━━ ℹ️ INFORMATIONS DE JEU ━━━")
316
- output.append(f"🚪 Lieux : {', '.join(data['rooms'])}")
317
- output.append(f"👤 Personnages : {', '.join(data['characters'])}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  output.append(f"🔪 Armes : {', '.join(data['weapons'])}")
319
 
320
- output.append(f"\n━━━ 👥 DÉTECTIVES ━━━")
321
- for player in data["other_players"]:
322
  status_icon = "✅" if player["is_active"] else "❌"
 
323
  output.append(
324
- f" {status_icon} {player['name']} ({player['card_count']} cartes)"
325
  )
326
 
327
  if data["current_turn"]:
328
  turn_marker = (
329
- "👉 C'EST TON TOUR MON PÉCHAILLOUX !" if data["is_my_turn"] else ""
330
  )
331
  output.append(f"\n━━━ 🎯 TOUR ACTUEL ━━━")
332
  output.append(f"🎲 {data['current_turn']} {turn_marker}")
333
 
334
  if data.get("winner"):
335
  output.append(
336
- f"\n\n🏆🏆🏆 QUOICOUBAIDEYYYYY ! VAINQUEUR : {data['winner']} 🏆🏆🏆"
337
  )
338
 
339
  if data["recent_turns"]:
340
- output.append(f"\n━━━ 📰 ACTIONS RÉCENTES ━━━")
341
  for turn in data["recent_turns"][-5:]:
342
  output.append(f" • {turn['player_name']}: {turn['action']}")
343
  if turn.get("details"):
344
  output.append(f" ↪ {turn['details']}")
 
 
345
 
346
  return "\n".join(output)
347
 
@@ -354,56 +413,77 @@ def make_suggestion(character: str, weapon: str, room: str):
354
  Make a suggestion.
355
  """
356
  if not state.game_id or not state.player_id:
357
- return "❌ Ekelesbikes ! Vous n'êtes pas dans une enquête"
358
 
359
  if not all([character, weapon, room]):
360
- return "❌ Eskilibass (I'm a spiderman), choisissez un personnage, une arme et un lieu"
361
 
362
  try:
363
  if IS_HUGGINGFACE:
364
- # Direct backend call
365
  from game_manager import game_manager
366
  from game_engine import GameEngine
 
 
367
 
368
  game = game_manager.get_game(state.game_id)
369
-
370
  if not game:
371
- return "❌ All RS5, erreur réseau ! Enquête introuvable"
372
 
373
- # Verify it's the player's turn
374
  if not GameEngine.can_player_act(game, state.player_id):
375
- return "❌ Yamete coudasai ! C'est pas ton tour !"
 
 
 
 
 
376
 
377
  player = next((p for p in game.players if p.id == state.player_id), None)
378
  if not player:
379
- return "❌ All RS5, erreur réseau ! Péchailloux masqué introuvable"
380
 
 
381
  can_disprove, disprover, card = GameEngine.check_suggestion(
382
  game, state.player_id, character, weapon, room
383
  )
384
 
385
- suggestion_text = f"Suggested: {character} with {weapon} in {room}"
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  if can_disprove and disprover and card:
388
- message = (
389
- f"{disprover} disproved the suggestion by showing: {card.name}"
390
- )
391
  else:
392
- message = "No one could disprove the suggestion!"
 
 
 
 
393
 
394
  GameEngine.add_turn_record(
395
- game, state.player_id, "suggest", suggestion_text
396
  )
397
  game.next_turn()
398
  game_manager.save_games()
399
 
400
- # Translate common responses
401
- if "disproved" in message.lower():
402
- return (
403
- f"💭 {message}\n\n➡️ Notes cette information pour tes déductions !"
404
- )
405
- else:
406
- return f"💭 {message}\n\n⚠️ Aucun chnawax n'a pu réfuter ta théorie !"
407
  else:
408
  # HTTP API call (local mode)
409
  response = requests.post(
@@ -421,21 +501,12 @@ def make_suggestion(character: str, weapon: str, room: str):
421
 
422
  if response.status_code == 200:
423
  data = response.json()
424
- message = data["message"]
425
-
426
- # Translate common responses
427
- if "disproved" in message.lower():
428
- return f"💭 {message}\n\n➡️ Notez cette information pour vos déductions !"
429
- else:
430
- return f"💭 {message}\n\n⚠️ Personne n'a pu réfuter votre théorie !"
431
  else:
432
- error = response.json().get("detail", "Erreur inconnue")
433
- if "Not your turn" in error:
434
- return "❌ Yamete coudasai ! C'est pas ton tour !"
435
- return f"❌ Erreur réseau (fourlestourtes et les bourbillats) : {error}"
436
 
437
  except Exception as e:
438
- return f"❌ Koikoubaiseyyyyy ! Erreur : {str(e)}"
439
 
440
 
441
  def make_accusation(character: str, weapon: str, room: str):
@@ -446,48 +517,68 @@ def make_accusation(character: str, weapon: str, room: str):
446
  return "❌ Vous n'êtes pas dans une enquête"
447
 
448
  if not all([character, weapon, room]):
449
- return "❌ Armankaboul ! Choisissez un personnage, une arme et un lieu"
450
 
451
  try:
452
  if IS_HUGGINGFACE:
453
- # Direct backend call
454
  from game_manager import game_manager
455
  from game_engine import GameEngine
456
  from models import GameStatus
 
 
457
 
458
  game = game_manager.get_game(state.game_id)
459
-
460
  if not game:
461
- return "❌ All RS5, erreur réseau : Enquête introuvable"
462
 
463
- # Verify it's the player's turn
464
  if not GameEngine.can_player_act(game, state.player_id):
465
- return "❌ Yamete coudasai ! C'est pas ton tour !"
466
 
467
  player = next((p for p in game.players if p.id == state.player_id), None)
468
  if not player:
469
- return "❌ All RS5, erreur réseau : Joueur introuvable"
470
 
471
- accusation_text = f"Accused: {character} with {weapon} in {room}"
472
 
473
  is_correct, message = GameEngine.process_accusation(
474
  game, state.player_id, character, weapon, room
475
  )
476
 
477
- GameEngine.add_turn_record(game, state.player_id, "accuse", accusation_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
  if not is_correct and game.status == GameStatus.IN_PROGRESS:
480
  game.next_turn()
481
 
482
  game_manager.save_games()
483
 
484
- # Check if win or lose
485
- if "wins" in message.lower() or "correct" in message.lower():
486
- return f"🎉🏆 {message} 🎉🏆\n\nTRIPLE MONSTRE COUCOUUUUU ! Tu as résolu le mystère ! (3 entre chat 4 et 1 brisé)"
487
- elif "wrong" in message.lower() or "eliminated" in message.lower():
488
- return f"💀 {message}\n\n😔 Fourlestourtes et les bourbillats... Tu as été éliminé calisse en pénitence siboère !\nTu peux toujours aider en réfutant les théories des autres."
489
  else:
490
- return f"⚖️ {message}"
 
 
 
 
 
 
491
  else:
492
  # HTTP API call (local mode)
493
  response = requests.post(
@@ -505,23 +596,71 @@ def make_accusation(character: str, weapon: str, room: str):
505
 
506
  if response.status_code == 200:
507
  data = response.json()
508
- message = data["message"]
509
-
510
- # Check if win or lose
511
- if "wins" in message.lower() or "correct" in message.lower():
512
- return f"🎉🏆 {message} 🎉🏆\n\nTRIPLE MONSTRE COUCOUUUUU ! Tu as résolu le mystère ! (3 entre chat 4 et 1 brisé)"
513
- elif "wrong" in message.lower() or "eliminated" in message.lower():
514
- return f"💀 {message}\n\n😔 Fourlestourtes et les bourbillats... Tu as été éliminé calisse en pénitence siboère !\nTu peux toujours aider en réfutant les théories des autres."
515
- else:
516
- return f"⚖️ {message}"
517
  else:
518
- error = response.json().get("detail", "Erreur inconnue")
519
- if "Not your turn" in error:
520
- return "❌ Yamete coudasai ! C'est pas ton tour !"
521
- return f"❌ All RS5, erreur réseau : {error}"
522
 
523
  except Exception as e:
524
- return f"❌ Koikoubaiseyyyyy ! Erreur : {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
 
526
 
527
  def pass_turn():
@@ -529,35 +668,29 @@ def pass_turn():
529
  Pass the current turn.
530
  """
531
  if not state.game_id or not state.player_id:
532
- return "❌ Eskilibass ! Vous n'êtes pas dans une enquête"
533
 
534
  try:
535
  if IS_HUGGINGFACE:
536
- # Direct backend call
537
  from game_manager import game_manager
538
  from game_engine import GameEngine
539
 
540
  game = game_manager.get_game(state.game_id)
541
-
542
  if not game:
543
- return "❌ All RS5, erreur réseau ! Enquête introuvable"
544
 
545
- # Verify it's the player's turn
546
  if not GameEngine.can_player_act(game, state.player_id):
547
- return "❌ Yamete coudasai ! C'est pas ton tour !"
548
-
549
- player = next((p for p in game.players if p.id == state.player_id), None)
550
- if not player:
551
- return "❌ All RS5, erreur réseau ! Joueur introuvable"
552
 
553
  # Pass turn
554
- GameEngine.add_turn_record(game, state.player_id, "pass", "Passed turn")
555
  game.next_turn()
556
  game_manager.save_games()
557
 
558
- return f"✅ Tour passé !\n\n➡️ C'est maintenant au tour de la prochaine poupouille."
 
 
559
  else:
560
- # HTTP API call (local mode)
561
  response = requests.post(
562
  f"{API_BASE}/games/{state.game_id}/action",
563
  json={
@@ -569,16 +702,12 @@ def pass_turn():
569
  )
570
 
571
  if response.status_code == 200:
572
- data = response.json()
573
- return f"✅ Tour passé !\n\n➡️ C'est maintenant au tour de la prochaine poupouille."
574
  else:
575
- error = response.json().get("detail", "Erreur inconnue")
576
- if "Not your turn" in error:
577
- return "❌ Yamete coudasai ! C'est pas ton tour !"
578
- return f"❌ All RS5, erreur réseau (fourlestourtes et les bourbillats) : {error}"
579
 
580
  except Exception as e:
581
- return f"❌ Koikoubaiseyyyyy ! Erreur : {str(e)}"
582
 
583
 
584
  # Sample lists for dropdowns
@@ -849,44 +978,105 @@ def create_gradio_interface():
849
  )
850
 
851
  with gr.Tab("🕯️ Créer une Partie"):
852
- gr.Markdown("### 📜 Établir un Nouveau Mystère")
853
- gr.Markdown("*Préparez la scène d'un meurtre des plus ignobles...*")
854
 
855
- game_name_input = gr.Textbox(
856
- label="🎭 Nom de l'enquête",
857
- placeholder="Le Meurtre au Manoir des Poupouilles",
858
- info="Donnez un nom à votre affaire (ex : armankaboul)",
859
- )
 
860
 
861
- rooms_input = gr.Textbox(
862
- label=f"🚪 Lieux de la scène de crime ({settings.MIN_ROOMS}-{settings.MAX_ROOMS} pièces)",
863
- placeholder="Le salon des péchailloux, La chambre du Viande, Bureau des Chnawax, Le B15 des Tchoupinoux, Le jardin de la poupouille",
864
- lines=4,
865
- info="Séparez les pièces par des virgules ou des retours à la ligne",
866
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
867
 
868
  use_ai_checkbox = gr.Checkbox(
869
- label="🤖 Activer le Narrateur IA - Lesland... euh non Desland",
870
  value=False,
871
  visible=settings.USE_OPENAI,
872
- info="Un vieux jardinier suspicieux qui semble en savoir plus qu'il n'y paraît...",
873
  )
874
 
875
  create_btn = gr.Button(
876
- "🩸 Commencer l'Enquête", variant="primary", size="lg"
877
  )
878
  create_output = gr.Textbox(
879
- label="📋 Dossier de l'Affaire", lines=5, show_copy_button=True
880
  )
881
  game_id_display = gr.Textbox(
882
- label="🔑 Code d'Enquête (partagez avec les autres poupouilles masquées)",
883
  interactive=False,
884
  show_copy_button=True,
885
  )
886
 
887
  create_btn.click(
888
  create_game,
889
- inputs=[game_name_input, rooms_input, use_ai_checkbox],
 
 
 
 
 
 
 
 
890
  outputs=[create_output, game_id_display],
891
  )
892
 
@@ -941,50 +1131,64 @@ def create_gradio_interface():
941
  start_btn.click(start_game, inputs=start_game_id, outputs=start_output)
942
 
943
  with gr.Tab("🔎 Enquêter"):
944
- gr.Markdown("### 📰 Tableau d'Enquête")
945
- gr.Markdown("*Étudiez les preuves et faites vos déductions...*")
946
 
947
  with gr.Group():
948
  refresh_btn = gr.Button(
949
  "🔄 Actualiser le Dossier", size="lg", variant="secondary"
950
  )
951
  game_view = gr.Textbox(
952
- label="🗂️ Dossier du Détective",
953
  lines=20,
954
  max_lines=30,
955
  show_copy_button=True,
956
- info="Cliquez sur Actualiser pour voir l'état actuel de la partie",
957
  )
958
 
959
  refresh_btn.click(get_player_view, outputs=game_view)
960
 
961
  gr.Markdown("---")
962
- gr.Markdown("### 🔮 Proposition de Théorie")
963
- gr.Markdown("*Testez une hypothèse auprès des autres détectives...*")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
 
965
  with gr.Group():
966
  with gr.Row():
967
  suggest_character = gr.Dropdown(
968
  label="👤 Suspect",
969
- choices=DEFAULT_CHARACTERS,
970
- info="Choisissez un personnage",
971
  )
972
  suggest_weapon = gr.Dropdown(
973
- label="🔪 Arme du Crime",
974
- choices=DEFAULT_WEAPONS,
975
- info="Choisissez une arme",
976
  )
977
  suggest_room = gr.Dropdown(
978
- label="🚪 Lieu du Crime",
979
  choices=[], # Will be populated from game
980
- info="Choisissez un lieu",
981
  )
982
 
983
  suggest_btn = gr.Button(
984
- "💭 Proposer une Théorie", variant="primary", size="lg"
985
  )
986
  suggest_output = gr.Textbox(
987
- label="🗨️ Réponse", lines=3, show_copy_button=True
988
  )
989
 
990
  suggest_btn.click(
@@ -996,30 +1200,32 @@ def create_gradio_interface():
996
  gr.Markdown("---")
997
  gr.Markdown("### ⚖️ Accusation Finale")
998
  gr.Markdown(
999
- "### ⚠️ *Yamete cudasaï ! Une fausse accusation vous ��limine de l'enquête !*"
1000
  )
1001
 
1002
  with gr.Group():
1003
  with gr.Row():
1004
  accuse_character = gr.Dropdown(
1005
- label="👤 Le Meurtrier",
1006
- choices=DEFAULT_CHARACTERS,
1007
- info="Qui a commis le crime ?",
1008
  )
1009
  accuse_weapon = gr.Dropdown(
1010
  label="🔪 L'Arme",
1011
- choices=DEFAULT_WEAPONS,
1012
- info="Avec quelle arme ?",
1013
  )
1014
  accuse_room = gr.Dropdown(
1015
- label="🚪 Le Lieu", choices=[], info="Dans quel lieu ?"
 
 
1016
  )
1017
 
1018
  accuse_btn = gr.Button(
1019
- "⚡ FAIRE L'ACCUSATION", variant="stop", size="lg"
1020
  )
1021
  accuse_output = gr.Textbox(
1022
- label="⚖️ Verdict", lines=3, show_copy_button=True
1023
  )
1024
 
1025
  accuse_btn.click(
@@ -1053,35 +1259,16 @@ def run_fastapi():
1053
  if __name__ == "__main__":
1054
  # IS_HUGGINGFACE is already defined at the top of the file
1055
 
1056
- if not IS_HUGGINGFACE:
1057
- # Local development: run FastAPI in background
1058
- def run_fastapi_bg():
1059
- """Run FastAPI on port 8000 in background"""
1060
- from api import app
1061
-
1062
- uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
1063
-
1064
- api_thread = threading.Thread(target=run_fastapi_bg, daemon=True)
1065
- api_thread.start()
1066
-
1067
- # Wait for API to start
1068
- time.sleep(2)
1069
 
1070
  # Create and launch Gradio interface
1071
  demo = create_gradio_interface()
1072
 
1073
- if IS_HUGGINGFACE:
1074
- # On Hugging Face Spaces: Gradio only on port 7860 (no FastAPI)
1075
- demo.launch(
1076
- server_name="0.0.0.0",
1077
- share=False,
1078
- show_error=True,
1079
- )
1080
- else:
1081
- # Local development: Gradio on port 7861, FastAPI on 8000
1082
- demo.launch(
1083
- server_name="127.0.0.1",
1084
- server_port=7861,
1085
- share=False,
1086
- show_error=True,
1087
- )
 
35
  state = GameState()
36
 
37
 
38
+ def create_game(
39
+ game_name: str,
40
+ narrative_tone: str,
41
+ custom_prompt: str,
42
+ rooms_text: str,
43
+ weapons_text: str,
44
+ suspects_text: str,
45
+ use_ai: bool
46
+ ):
47
  """
48
+ Create a new game with custom elements.
49
  """
50
  if not game_name or not rooms_text:
51
+ return "❌ Fournissez un titre d'enquête et une liste de lieux", ""
52
 
53
+ # Parse inputs (comma or newline separated)
54
  rooms = [r.strip() for r in rooms_text.replace("\n", ",").split(",") if r.strip()]
55
+ weapons = [w.strip() for w in weapons_text.replace("\n", ",").split(",") if w.strip()]
56
+ suspects = [s.strip() for s in suspects_text.replace("\n", ",").split(",") if s.strip()]
57
 
58
+ # Validation
59
  if len(rooms) < settings.MIN_ROOMS:
60
+ return f"❌ Il faut au moins {settings.MIN_ROOMS} lieux", ""
 
 
 
61
 
62
  if len(rooms) > settings.MAX_ROOMS:
63
+ return f"❌ Maximum {settings.MAX_ROOMS} lieux autorisés", ""
64
+
65
+ if len(weapons) < 3:
66
+ return "❌ Il faut au moins 3 armes", ""
67
+
68
+ if len(suspects) < 3:
69
+ return "❌ Il faut au moins 3 suspects", ""
70
 
71
  try:
72
  if IS_HUGGINGFACE:
 
73
  from game_manager import game_manager
74
  from models import CreateGameRequest
75
 
76
+ request = CreateGameRequest(
77
+ game_name=game_name,
78
+ narrative_tone=narrative_tone,
79
+ custom_prompt=custom_prompt if custom_prompt else None,
80
+ rooms=rooms,
81
+ custom_weapons=weapons,
82
+ custom_suspects=suspects,
83
+ use_ai=use_ai
84
+ )
85
  game = game_manager.create_game(request)
86
 
87
  # Generate AI scenario if enabled
88
  if game.use_ai and settings.USE_OPENAI:
 
89
  from ai_service import ai_service
90
  import asyncio
91
 
92
  try:
93
  scenario = asyncio.run(
94
+ ai_service.generate_scenario(
95
+ game.rooms,
96
+ game.custom_suspects,
97
+ game.narrative_tone
98
+ )
99
  )
100
  if scenario:
101
  game.scenario = scenario
 
105
 
106
  state.game_id = game.game_id
107
  return (
108
+ f"✅ Enquête créée !\n\n"
109
+ f"🔑 Code d'Enquête : **{game.game_id}**\n\n"
110
+ f"📤 Partagez ce code avec les autres joueurs\n"
111
+ f"ℹ️ Minimum {settings.MIN_PLAYERS} joueurs requis pour démarrer",
112
  game.game_id,
113
  )
114
  else:
115
  # HTTP API call (local mode)
116
  response = requests.post(
117
  f"{API_BASE}/games/create",
118
+ json={
119
+ "game_name": game_name,
120
+ "narrative_tone": narrative_tone,
121
+ "custom_prompt": custom_prompt if custom_prompt else None,
122
+ "rooms": rooms,
123
+ "custom_weapons": weapons,
124
+ "custom_suspects": suspects,
125
+ "use_ai": use_ai
126
+ },
127
  timeout=5,
128
  )
129
 
 
131
  data = response.json()
132
  state.game_id = data["game_id"]
133
  return (
134
+ f"✅ Enquête créée !\n\n"
135
+ f"🔑 Code : **{data['game_id']}**\n\n"
136
+ f"📤 Partagez ce code\n"
137
+ f"ℹ️ Min. {settings.MIN_PLAYERS} joueurs",
138
  data["game_id"],
139
  )
140
  else:
141
+ return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}", ""
 
 
 
142
 
143
  except Exception as e:
144
+ return f"❌ Erreur : {str(e)}", ""
145
 
146
 
147
  def join_game(game_id: str, player_name: str):
 
149
  Join an existing game.
150
  """
151
  if not game_id or not player_name:
152
+ return "❌ Fournissez le code d'enquête et votre nom !"
153
 
154
  try:
155
  game_id = game_id.strip().upper()
 
162
 
163
  game = game_manager.get_game(game_id)
164
  if not game:
165
+ return "❌ Enquête introuvable !"
166
 
167
  if game.status != GameStatus.WAITING:
168
+ return "❌ La partie a déjà commencé !"
169
 
170
  if game.is_full():
171
+ return "❌ Partie complète (maximum 8 joueurs) !"
172
 
173
  player = game_manager.join_game(game_id, player_name)
174
  if not player:
175
+ return "❌ Impossible de rejoindre l'enquête !"
176
 
177
  state.game_id = game_id
178
  state.player_id = player.id
179
  state.player_name = player_name
180
 
181
  return (
182
+ f"✅ Enquête rejointe !\n\n"
183
  f"👋 Bienvenue, {player_name} !\n\n"
184
+ f"⏳ Attendez que le créateur démarre la partie\n"
185
+ f"📖 Consultez l'onglet 🔎 Enquêter pour voir l'état"
186
  )
187
  else:
188
  # HTTP API call (local mode)
 
231
  success = game_manager.start_game(game_id)
232
 
233
  if not success:
234
+ return f"❌ Impossible de démarrer ! Minimum {settings.MIN_PLAYERS} joueurs requis."
235
+
236
+ game = game_manager.get_game(game_id)
237
 
238
  return (
239
+ f"🎬 L'ENQUÊTE COMMENCE !\n\n"
240
+ f"🃏 Les cartes ont été distribuées\n"
241
+ f"🎲 Tous les joueurs démarrent dans : {game.rooms[0] if game.rooms else '(aucun lieu)'}\n"
242
+ f"🔍 À vous de découvrir qui a commis le crime, avec quelle arme et dans quel lieu !\n\n"
243
+ f"➡️ Allez dans l'onglet 🔎 Enquêter pour commencer à jouer"
244
  )
245
  else:
246
  # HTTP API call (local mode)
 
248
 
249
  if response.status_code == 200:
250
  return (
251
+ f"🎬 L'ENQUÊTE COMMENCE !\n\n"
252
+ f"🃏 Les cartes ont été distribuées\n"
253
+ f"🔍 À vous de découvrir qui a commis le crime !\n\n"
254
+ f"➡️ Allez dans l'onglet 🔎 Enquêter pour commencer"
255
  )
256
  else:
257
+ return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
258
 
259
  except Exception as e:
260
  return f"❌ Yamete coudasai ! Erreur au démarrage : {str(e)}"
 
286
  return "❌ Poupée en pénitence calisse ! Péchailloux masqué..."
287
 
288
  # Build safe view
289
+ all_players = [
290
+ {
291
+ "name": p.name,
292
+ "is_active": p.is_active,
293
+ "card_count": len(p.cards),
294
+ "position": p.current_room_index
295
+ }
296
  for p in game.players
 
297
  ]
298
 
299
  current_player = game.get_current_player()
 
307
  "characters": [c.name for c in game.characters],
308
  "weapons": [w.name for w in game.weapons],
309
  "my_cards": [c.name for c in player.cards],
310
+ "my_position": player.current_room_index,
311
+ "all_players": all_players,
312
  "current_turn": current_player.name if current_player else None,
313
  "is_my_turn": (
314
  current_player.id == state.player_id if current_player else False
 
329
 
330
  # Format output (common for both modes)
331
  output = []
332
+ output.append(f"═══ 🎮 {data['game_name']} ═══\n")
333
 
334
  status_map = {
335
+ "waiting": "⏳ En attente de joueurs...",
336
+ "in_progress": "🎲 Partie en cours",
337
+ "finished": "🏆 Partie terminée",
338
  }
339
  output.append(f"📊 Statut : {status_map.get(data['status'], data['status'])}\n")
340
 
341
  if data.get("scenario"):
342
  output.append(f"\n📜 Scénario :\n{data['scenario']}\n")
343
 
344
+ # Show player's current position if game started
345
+ if data.get("my_position") is not None and data["rooms"]:
346
+ current_room = data["rooms"][data["my_position"]]
347
+ output.append(f"\n📍 VOTRE POSITION : **{current_room}**")
348
+
349
  output.append(f"\n━━━ 🃏 VOS CARTES ━━━")
350
  output.append("(Ces éléments NE SONT PAS la solution)")
351
  for card in data["my_cards"]:
352
  output.append(f" 🔸 {card}")
353
 
354
+ output.append(f"\n━━━ 🏠 PLATEAU DE JEU (Circuit) ━━━")
355
+ # Show rooms with player positions in circuit order
356
+ rooms_display = []
357
+ for idx, room in enumerate(data["rooms"]):
358
+ players_here = [p["name"] for p in data.get("all_players", []) if p.get("position") == idx]
359
+
360
+ # Visual indicator
361
+ if players_here:
362
+ icon = "👥"
363
+ player_names = ', '.join(players_here)
364
+ rooms_display.append(f" {idx+1}. {icon} **{room}** → {player_names}")
365
+ else:
366
+ icon = "🚪"
367
+ rooms_display.append(f" {idx+1}. {icon} {room}")
368
+
369
+ output.extend(rooms_display)
370
+ output.append(f" └─→ Circuit fermé (retour à {data['rooms'][0]})")
371
+
372
+ output.append(f"\n━━━ ℹ️ ÉLÉMENTS DU JEU ━━━")
373
+ output.append(f"👤 Suspects : {', '.join(data['characters'])}")
374
  output.append(f"🔪 Armes : {', '.join(data['weapons'])}")
375
 
376
+ output.append(f"\n━━━ 👥 JOUEURS ━━━")
377
+ for player in data.get("all_players", []):
378
  status_icon = "✅" if player["is_active"] else "❌"
379
+ position = data["rooms"][player["position"]] if player.get("position") is not None else "?"
380
  output.append(
381
+ f" {status_icon} {player['name']} - {position} ({player['card_count']} cartes)"
382
  )
383
 
384
  if data["current_turn"]:
385
  turn_marker = (
386
+ "👉 C'EST VOTRE TOUR !" if data["is_my_turn"] else ""
387
  )
388
  output.append(f"\n━━━ 🎯 TOUR ACTUEL ━━━")
389
  output.append(f"🎲 {data['current_turn']} {turn_marker}")
390
 
391
  if data.get("winner"):
392
  output.append(
393
+ f"\n\n🏆🏆🏆 VAINQUEUR : {data['winner']} 🏆🏆🏆"
394
  )
395
 
396
  if data["recent_turns"]:
397
+ output.append(f"\n━━━ 📰 HISTORIQUE (5 dernières actions) ━━━")
398
  for turn in data["recent_turns"][-5:]:
399
  output.append(f" • {turn['player_name']}: {turn['action']}")
400
  if turn.get("details"):
401
  output.append(f" ↪ {turn['details']}")
402
+ if turn.get("ai_comment"):
403
+ output.append(f" 🗣️ Desland: {turn['ai_comment']}")
404
 
405
  return "\n".join(output)
406
 
 
413
  Make a suggestion.
414
  """
415
  if not state.game_id or not state.player_id:
416
+ return "❌ Vous n'êtes pas dans une enquête"
417
 
418
  if not all([character, weapon, room]):
419
+ return "❌ Choisissez un suspect, une arme et un lieu"
420
 
421
  try:
422
  if IS_HUGGINGFACE:
 
423
  from game_manager import game_manager
424
  from game_engine import GameEngine
425
+ from ai_service import ai_service
426
+ import asyncio
427
 
428
  game = game_manager.get_game(state.game_id)
 
429
  if not game:
430
+ return "❌ Enquête introuvable"
431
 
 
432
  if not GameEngine.can_player_act(game, state.player_id):
433
+ return "❌ Ce n'est pas votre tour !"
434
+
435
+ # Check if player is in the correct room
436
+ can_suggest, error_msg = GameEngine.can_make_suggestion(game, state.player_id, room)
437
+ if not can_suggest:
438
+ return f"❌ {error_msg}"
439
 
440
  player = next((p for p in game.players if p.id == state.player_id), None)
441
  if not player:
442
+ return "❌ Joueur introuvable"
443
 
444
+ # Process suggestion
445
  can_disprove, disprover, card = GameEngine.check_suggestion(
446
  game, state.player_id, character, weapon, room
447
  )
448
 
449
+ suggestion_text = f"{character} avec {weapon} dans {room}"
450
 
451
+ # Generate AI comment if enabled
452
+ ai_comment = None
453
+ if game.use_ai and settings.USE_OPENAI:
454
+ try:
455
+ ai_comment = asyncio.run(
456
+ ai_service.generate_suggestion_comment(
457
+ player.name,
458
+ character,
459
+ weapon,
460
+ room,
461
+ can_disprove,
462
+ game.narrative_tone
463
+ )
464
+ )
465
+ except:
466
+ pass
467
+
468
+ # Build response message
469
  if can_disprove and disprover and card:
470
+ message = f"💭 {disprover} réfute en montrant : **{card.name}**"
 
 
471
  else:
472
+ message = "💭 Personne ne peut réfuter cette suggestion !"
473
+
474
+ # Add AI comment if available
475
+ if ai_comment:
476
+ message += f"\n\n🗣️ **Desland** : {ai_comment}"
477
 
478
  GameEngine.add_turn_record(
479
+ game, state.player_id, "suggest", suggestion_text, ai_comment
480
  )
481
  game.next_turn()
482
  game_manager.save_games()
483
 
484
+ message += "\n\n➡️ Notez cette information dans votre feuille d'enquête !"
485
+ return message
486
+
 
 
 
 
487
  else:
488
  # HTTP API call (local mode)
489
  response = requests.post(
 
501
 
502
  if response.status_code == 200:
503
  data = response.json()
504
+ return data.get("message", "Suggestion effectuée")
 
 
 
 
 
 
505
  else:
506
+ return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
 
 
 
507
 
508
  except Exception as e:
509
+ return f"❌ Erreur : {str(e)}"
510
 
511
 
512
  def make_accusation(character: str, weapon: str, room: str):
 
517
  return "❌ Vous n'êtes pas dans une enquête"
518
 
519
  if not all([character, weapon, room]):
520
+ return "❌ Choisissez un suspect, une arme et un lieu"
521
 
522
  try:
523
  if IS_HUGGINGFACE:
 
524
  from game_manager import game_manager
525
  from game_engine import GameEngine
526
  from models import GameStatus
527
+ from ai_service import ai_service
528
+ import asyncio
529
 
530
  game = game_manager.get_game(state.game_id)
 
531
  if not game:
532
+ return "❌ Enquête introuvable"
533
 
 
534
  if not GameEngine.can_player_act(game, state.player_id):
535
+ return "❌ Ce n'est pas votre tour !"
536
 
537
  player = next((p for p in game.players if p.id == state.player_id), None)
538
  if not player:
539
+ return "❌ Joueur introuvable"
540
 
541
+ accusation_text = f"{character} avec {weapon} dans {room}"
542
 
543
  is_correct, message = GameEngine.process_accusation(
544
  game, state.player_id, character, weapon, room
545
  )
546
 
547
+ # Generate AI comment if enabled
548
+ ai_comment = None
549
+ if game.use_ai and settings.USE_OPENAI:
550
+ try:
551
+ ai_comment = asyncio.run(
552
+ ai_service.generate_accusation_comment(
553
+ player.name,
554
+ character,
555
+ weapon,
556
+ room,
557
+ is_correct,
558
+ game.narrative_tone
559
+ )
560
+ )
561
+ except:
562
+ pass
563
+
564
+ GameEngine.add_turn_record(game, state.player_id, "accuse", accusation_text, ai_comment)
565
 
566
  if not is_correct and game.status == GameStatus.IN_PROGRESS:
567
  game.next_turn()
568
 
569
  game_manager.save_games()
570
 
571
+ # Build response
572
+ if is_correct:
573
+ response = f"🎉🏆 {message} 🎉🏆\n\nVous avez résolu le mystère !"
 
 
574
  else:
575
+ response = f"💀 {message}\n\nVous avez été éliminé !\nVous pouvez toujours aider en réfutant les théories des autres."
576
+
577
+ if ai_comment:
578
+ response += f"\n\n🗣️ **Desland** : {ai_comment}"
579
+
580
+ return response
581
+
582
  else:
583
  # HTTP API call (local mode)
584
  response = requests.post(
 
596
 
597
  if response.status_code == 200:
598
  data = response.json()
599
+ return data.get("message", "Accusation effectuée")
 
 
 
 
 
 
 
 
600
  else:
601
+ return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
 
 
 
602
 
603
  except Exception as e:
604
+ return f"❌ Erreur : {str(e)}"
605
+
606
+
607
+ def roll_and_move():
608
+ """
609
+ Roll dice and move the player.
610
+ """
611
+ if not state.game_id or not state.player_id:
612
+ return "❌ Vous n'êtes pas dans une enquête"
613
+
614
+ try:
615
+ if IS_HUGGINGFACE:
616
+ from game_manager import game_manager
617
+ from game_engine import GameEngine
618
+
619
+ game = game_manager.get_game(state.game_id)
620
+ if not game:
621
+ return "❌ Enquête introuvable"
622
+
623
+ if not GameEngine.can_player_act(game, state.player_id):
624
+ return "❌ Ce n'est pas votre tour !"
625
+
626
+ # Roll dice
627
+ dice_roll = GameEngine.roll_dice()
628
+
629
+ # Move player
630
+ success, message, new_room_index = GameEngine.move_player(
631
+ game, state.player_id, dice_roll
632
+ )
633
+
634
+ if not success:
635
+ return f"❌ {message}"
636
+
637
+ # Record turn
638
+ GameEngine.add_turn_record(game, state.player_id, "move", message)
639
+ game_manager.save_games()
640
+
641
+ current_room = game.rooms[new_room_index]
642
+ return f"🎲 {message}\n\n📍 Vous êtes maintenant dans : **{current_room}**\n\nVous pouvez faire une suggestion dans cette pièce ou passer votre tour."
643
+
644
+ else:
645
+ # HTTP API (local mode)
646
+ response = requests.post(
647
+ f"{API_BASE}/games/{state.game_id}/action",
648
+ json={
649
+ "game_id": state.game_id,
650
+ "player_id": state.player_id,
651
+ "action_type": "move"
652
+ },
653
+ timeout=5,
654
+ )
655
+
656
+ if response.status_code == 200:
657
+ data = response.json()
658
+ return data.get("message", "Déplacé avec succès")
659
+ else:
660
+ return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
661
+
662
+ except Exception as e:
663
+ return f"❌ Erreur : {str(e)}"
664
 
665
 
666
  def pass_turn():
 
668
  Pass the current turn.
669
  """
670
  if not state.game_id or not state.player_id:
671
+ return "❌ Vous n'êtes pas dans une enquête"
672
 
673
  try:
674
  if IS_HUGGINGFACE:
 
675
  from game_manager import game_manager
676
  from game_engine import GameEngine
677
 
678
  game = game_manager.get_game(state.game_id)
 
679
  if not game:
680
+ return "❌ Enquête introuvable"
681
 
 
682
  if not GameEngine.can_player_act(game, state.player_id):
683
+ return "❌ Ce n'est pas votre tour !"
 
 
 
 
684
 
685
  # Pass turn
686
+ GameEngine.add_turn_record(game, state.player_id, "pass", "Tour passé")
687
  game.next_turn()
688
  game_manager.save_games()
689
 
690
+ next_player = game.get_current_player()
691
+ return f"✅ Tour passé !\n\n➡️ C'est maintenant au tour de {next_player.name if next_player else 'quelqu\'un'}."
692
+
693
  else:
 
694
  response = requests.post(
695
  f"{API_BASE}/games/{state.game_id}/action",
696
  json={
 
702
  )
703
 
704
  if response.status_code == 200:
705
+ return "✅ Tour passé !"
 
706
  else:
707
+ return f"❌ Erreur : {response.json().get('detail', 'Erreur inconnue')}"
 
 
 
708
 
709
  except Exception as e:
710
+ return f"❌ Erreur : {str(e)}"
711
 
712
 
713
  # Sample lists for dropdowns
 
978
  )
979
 
980
  with gr.Tab("🕯️ Créer une Partie"):
981
+ gr.Markdown("### 🎮 Créer Votre Enquête Personnalisée")
982
+ gr.Markdown("*Créez votre propre manoir, vos suspects et vos armes...*")
983
 
984
+ with gr.Group():
985
+ game_name_input = gr.Textbox(
986
+ label="🎭 Titre de l'enquête",
987
+ placeholder="Meurtre au Coworking",
988
+ info="Donnez un titre à votre affaire",
989
+ )
990
 
991
+ from models import NarrativeTone
992
+ narrative_tone_dropdown = gr.Dropdown(
993
+ label="🎨 Tonalité narrative",
994
+ choices=[tone.value for tone in NarrativeTone],
995
+ value=NarrativeTone.SERIOUS.value,
996
+ info="Choisissez l'ambiance du jeu",
997
+ )
998
+
999
+ custom_prompt_input = gr.Textbox(
1000
+ label="✍️ Prompt personnalisé (optionnel)",
1001
+ placeholder="Style Agatha Christie avec humour noir...",
1002
+ lines=2,
1003
+ info="Personnalisez le style narratif de l'IA",
1004
+ )
1005
+
1006
+ with gr.Group():
1007
+ gr.Markdown("#### 🏠 Configuration du Plateau")
1008
+ gr.Markdown("**Important** : L'ordre des pièces définit le plateau de jeu (circuit circulaire)")
1009
+
1010
+ rooms_input = gr.Textbox(
1011
+ label=f"🚪 Lieux ({settings.MIN_ROOMS}-{settings.MAX_ROOMS}) - DANS L'ORDRE",
1012
+ placeholder="Cuisine, Toit, Salle serveurs, Cafétéria, Bureau, Salle de réunion",
1013
+ lines=5,
1014
+ info="⚠️ L'ORDRE EST IMPORTANT ! Les joueurs se déplaceront dans cet ordre (circuit). Une ligne = une pièce.",
1015
+ )
1016
+
1017
+ gr.Markdown(
1018
+ """
1019
+ 💡 **Exemple de circuit** :
1020
+ ```
1021
+ 1. Cuisine (départ)
1022
+ 2. Salon
1023
+ 3. Bureau
1024
+ 4. Chambre
1025
+ 5. Garage
1026
+ 6. Jardin
1027
+ → retour à Cuisine (circuit fermé)
1028
+ ```
1029
+ Les joueurs avancent dans cet ordre selon les dés.
1030
+ """
1031
+ )
1032
+
1033
+ with gr.Group():
1034
+ gr.Markdown("#### 🎭 Éléments du Mystère")
1035
+
1036
+ suspects_input = gr.Textbox(
1037
+ label="👤 Suspects (min. 3)",
1038
+ placeholder="Claire, Pierre, Daniel, Marie, Thomas, Sophie",
1039
+ lines=2,
1040
+ info="Qui pourrait être le coupable ?",
1041
+ )
1042
+
1043
+ weapons_input = gr.Textbox(
1044
+ label="🔪 Armes (min. 3)",
1045
+ placeholder="Clé USB, Capsule de café, Câble HDMI, Agrafeuse, Souris d'ordinateur, Plante verte",
1046
+ lines=2,
1047
+ info="Quelle arme a été utilisée ?",
1048
+ )
1049
 
1050
  use_ai_checkbox = gr.Checkbox(
1051
+ label="🤖 Activer le Narrateur IA - Desland (le jardinier sarcastique)",
1052
  value=False,
1053
  visible=settings.USE_OPENAI,
1054
+ info="Desland commente vos actions avec sarcasme et suspicion...",
1055
  )
1056
 
1057
  create_btn = gr.Button(
1058
+ "🎬 Créer l'Enquête", variant="primary", size="lg"
1059
  )
1060
  create_output = gr.Textbox(
1061
+ label="📋 Résultat", lines=5, show_copy_button=True
1062
  )
1063
  game_id_display = gr.Textbox(
1064
+ label="🔑 Code d'Enquête (à partager)",
1065
  interactive=False,
1066
  show_copy_button=True,
1067
  )
1068
 
1069
  create_btn.click(
1070
  create_game,
1071
+ inputs=[
1072
+ game_name_input,
1073
+ narrative_tone_dropdown,
1074
+ custom_prompt_input,
1075
+ rooms_input,
1076
+ weapons_input,
1077
+ suspects_input,
1078
+ use_ai_checkbox
1079
+ ],
1080
  outputs=[create_output, game_id_display],
1081
  )
1082
 
 
1131
  start_btn.click(start_game, inputs=start_game_id, outputs=start_output)
1132
 
1133
  with gr.Tab("🔎 Enquêter"):
1134
+ gr.Markdown("### 🕹️ Tableau de Jeu")
1135
+ gr.Markdown("*Lancez les dés, déplacez-vous, et menez l'enquête...*")
1136
 
1137
  with gr.Group():
1138
  refresh_btn = gr.Button(
1139
  "🔄 Actualiser le Dossier", size="lg", variant="secondary"
1140
  )
1141
  game_view = gr.Textbox(
1142
+ label="🗂️ État de la Partie",
1143
  lines=20,
1144
  max_lines=30,
1145
  show_copy_button=True,
1146
+ info="Cliquez sur Actualiser pour voir l'état actuel",
1147
  )
1148
 
1149
  refresh_btn.click(get_player_view, outputs=game_view)
1150
 
1151
  gr.Markdown("---")
1152
+ gr.Markdown("### 🎲 Votre Tour")
1153
+ gr.Markdown("**Étape 1 :** Lancez les dés pour vous déplacer")
1154
+
1155
+ with gr.Group():
1156
+ roll_btn = gr.Button(
1157
+ "🎲 Lancer les Dés", variant="primary", size="lg"
1158
+ )
1159
+ move_output = gr.Textbox(
1160
+ label="📍 Déplacement", lines=3
1161
+ )
1162
+
1163
+ roll_btn.click(roll_and_move, outputs=move_output)
1164
+
1165
+ gr.Markdown("---")
1166
+ gr.Markdown("### 💭 Faire une Suggestion")
1167
+ gr.Markdown("**Étape 2 :** Faites une suggestion *dans la pièce où vous êtes*")
1168
 
1169
  with gr.Group():
1170
  with gr.Row():
1171
  suggest_character = gr.Dropdown(
1172
  label="👤 Suspect",
1173
+ choices=[], # Will be populated from game
1174
+ info="Qui est le coupable ?",
1175
  )
1176
  suggest_weapon = gr.Dropdown(
1177
+ label="🔪 Arme",
1178
+ choices=[], # Will be populated from game
1179
+ info="Quelle arme ?",
1180
  )
1181
  suggest_room = gr.Dropdown(
1182
+ label="🚪 Lieu",
1183
  choices=[], # Will be populated from game
1184
+ info="Dans quel lieu ?",
1185
  )
1186
 
1187
  suggest_btn = gr.Button(
1188
+ "💭 Faire la Suggestion", variant="primary", size="lg"
1189
  )
1190
  suggest_output = gr.Textbox(
1191
+ label="🗨️ Résultat", lines=5, show_copy_button=True
1192
  )
1193
 
1194
  suggest_btn.click(
 
1200
  gr.Markdown("---")
1201
  gr.Markdown("### ⚖️ Accusation Finale")
1202
  gr.Markdown(
1203
+ "⚠️ **ATTENTION :** Une fausse accusation vous élimine !"
1204
  )
1205
 
1206
  with gr.Group():
1207
  with gr.Row():
1208
  accuse_character = gr.Dropdown(
1209
+ label="👤 Le Coupable",
1210
+ choices=[],
1211
+ info="Qui ?",
1212
  )
1213
  accuse_weapon = gr.Dropdown(
1214
  label="🔪 L'Arme",
1215
+ choices=[],
1216
+ info="Avec quoi ?",
1217
  )
1218
  accuse_room = gr.Dropdown(
1219
+ label="🚪 Le Lieu",
1220
+ choices=[],
1221
+ info="Où ?",
1222
  )
1223
 
1224
  accuse_btn = gr.Button(
1225
+ "⚡ ACCUSER", variant="stop", size="lg"
1226
  )
1227
  accuse_output = gr.Textbox(
1228
+ label="⚖️ Verdict", lines=5, show_copy_button=True
1229
  )
1230
 
1231
  accuse_btn.click(
 
1259
  if __name__ == "__main__":
1260
  # IS_HUGGINGFACE is already defined at the top of the file
1261
 
1262
+ # Note: We're always in HUGGINGFACE mode (direct backend calls)
1263
+ # No need for FastAPI server
 
 
 
 
 
 
 
 
 
 
 
1264
 
1265
  # Create and launch Gradio interface
1266
  demo = create_gradio_interface()
1267
 
1268
+ # Launch on available port
1269
+ demo.launch(
1270
+ server_name="127.0.0.1",
1271
+ server_port=7862,
1272
+ share=False,
1273
+ show_error=True,
1274
+ )
 
 
 
 
 
 
 
 
game_engine.py CHANGED
@@ -9,7 +9,7 @@ from models import Game, Card, CardType, Solution, GameStatus, Player
9
  from datetime import datetime
10
 
11
 
12
- # Default character names (can be customized)
13
  DEFAULT_CHARACTERS = [
14
  "Miss Scarlett",
15
  "Colonel Mustard",
@@ -19,7 +19,7 @@ DEFAULT_CHARACTERS = [
19
  "Professor Plum"
20
  ]
21
 
22
- # Default weapon names (can be customized)
23
  DEFAULT_WEAPONS = [
24
  "Candlestick",
25
  "Knife",
@@ -39,16 +39,20 @@ class GameEngine:
39
  Initialize a game with cards and solution.
40
  Distributes cards among players after setting aside the solution.
41
  """
 
 
 
 
42
  # Create character cards
43
  game.characters = [
44
  Card(name=name, card_type=CardType.CHARACTER)
45
- for name in DEFAULT_CHARACTERS
46
  ]
47
 
48
  # Create weapon cards
49
  game.weapons = [
50
  Card(name=name, card_type=CardType.WEAPON)
51
- for name in DEFAULT_WEAPONS
52
  ]
53
 
54
  # Create room cards
@@ -206,7 +210,8 @@ class GameEngine:
206
  game: Game,
207
  player_id: str,
208
  action: str,
209
- details: Optional[str] = None
 
210
  ):
211
  """Add a turn record to the game history."""
212
  from models import Turn
@@ -220,6 +225,7 @@ class GameEngine:
220
  player_name=player.name,
221
  action=action,
222
  details=details,
 
223
  timestamp=datetime.now().isoformat()
224
  )
225
  game.turns.append(turn)
@@ -241,3 +247,48 @@ class GameEngine:
241
  current_player.is_active and
242
  game.status == GameStatus.IN_PROGRESS
243
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from datetime import datetime
10
 
11
 
12
+ # Default character names (fallback if no custom suspects provided)
13
  DEFAULT_CHARACTERS = [
14
  "Miss Scarlett",
15
  "Colonel Mustard",
 
19
  "Professor Plum"
20
  ]
21
 
22
+ # Default weapon names (fallback if no custom weapons provided)
23
  DEFAULT_WEAPONS = [
24
  "Candlestick",
25
  "Knife",
 
39
  Initialize a game with cards and solution.
40
  Distributes cards among players after setting aside the solution.
41
  """
42
+ # Use custom suspects or defaults
43
+ suspects = game.custom_suspects if game.custom_suspects else DEFAULT_CHARACTERS
44
+ weapons = game.custom_weapons if game.custom_weapons else DEFAULT_WEAPONS
45
+
46
  # Create character cards
47
  game.characters = [
48
  Card(name=name, card_type=CardType.CHARACTER)
49
+ for name in suspects
50
  ]
51
 
52
  # Create weapon cards
53
  game.weapons = [
54
  Card(name=name, card_type=CardType.WEAPON)
55
+ for name in weapons
56
  ]
57
 
58
  # Create room cards
 
210
  game: Game,
211
  player_id: str,
212
  action: str,
213
+ details: Optional[str] = None,
214
+ ai_comment: Optional[str] = None
215
  ):
216
  """Add a turn record to the game history."""
217
  from models import Turn
 
225
  player_name=player.name,
226
  action=action,
227
  details=details,
228
+ ai_comment=ai_comment,
229
  timestamp=datetime.now().isoformat()
230
  )
231
  game.turns.append(turn)
 
247
  current_player.is_active and
248
  game.status == GameStatus.IN_PROGRESS
249
  )
250
+
251
+ @staticmethod
252
+ def roll_dice() -> int:
253
+ """Roll two dice and return the sum (2-12)."""
254
+ return random.randint(1, 6) + random.randint(1, 6)
255
+
256
+ @staticmethod
257
+ def move_player(game: Game, player_id: str, dice_roll: int) -> Tuple[bool, str, int]:
258
+ """
259
+ Move a player on the board based on dice roll.
260
+ Returns (success, message, new_room_index).
261
+ """
262
+ player = next((p for p in game.players if p.id == player_id), None)
263
+ if not player:
264
+ return False, "Joueur introuvable", -1
265
+
266
+ num_rooms = len(game.rooms)
267
+ if num_rooms == 0:
268
+ return False, "Pas de pièces disponibles", -1
269
+
270
+ # Calculate new position (circular movement)
271
+ new_room_index = (player.current_room_index + dice_roll) % num_rooms
272
+ old_room = game.rooms[player.current_room_index]
273
+ new_room = game.rooms[new_room_index]
274
+
275
+ player.current_room_index = new_room_index
276
+
277
+ message = f"🎲 Dés: {dice_roll} | {old_room} → {new_room}"
278
+ return True, message, new_room_index
279
+
280
+ @staticmethod
281
+ def can_make_suggestion(game: Game, player_id: str, room: str) -> Tuple[bool, str]:
282
+ """
283
+ Check if a player can make a suggestion.
284
+ Players can only suggest in the room they're currently in.
285
+ """
286
+ player = next((p for p in game.players if p.id == player_id), None)
287
+ if not player:
288
+ return False, "Joueur introuvable"
289
+
290
+ current_room = game.rooms[player.current_room_index]
291
+ if current_room != room:
292
+ return False, f"Tu dois être dans {room} pour faire cette suggestion ! Tu es actuellement dans {current_room}."
293
+
294
+ return True, ""
game_manager.py CHANGED
@@ -31,7 +31,11 @@ class GameManager:
31
  game = Game(
32
  game_id=game_id,
33
  name=request.game_name,
 
 
34
  rooms=request.rooms,
 
 
35
  use_ai=request.use_ai,
36
  max_players=settings.MAX_PLAYERS
37
  )
 
31
  game = Game(
32
  game_id=game_id,
33
  name=request.game_name,
34
+ narrative_tone=request.narrative_tone,
35
+ custom_prompt=request.custom_prompt,
36
  rooms=request.rooms,
37
+ custom_weapons=request.custom_weapons,
38
+ custom_suspects=request.custom_suspects,
39
  use_ai=request.use_ai,
40
  max_players=settings.MAX_PLAYERS
41
  )
models.py CHANGED
@@ -10,6 +10,15 @@ import random
10
  import string
11
 
12
 
 
 
 
 
 
 
 
 
 
13
  class CardType(str, Enum):
14
  """Types of cards in the game."""
15
  CHARACTER = "character"
@@ -29,6 +38,7 @@ class Player(BaseModel):
29
  name: str
30
  cards: List[Card] = Field(default_factory=list)
31
  is_active: bool = True
 
32
 
33
 
34
  class GameStatus(str, Enum):
@@ -42,8 +52,9 @@ class Turn(BaseModel):
42
  """Represents a turn action in the game."""
43
  player_id: str
44
  player_name: str
45
- action: str # "move", "suggest", "accuse"
46
  details: Optional[str] = None
 
47
  timestamp: str
48
 
49
 
@@ -54,12 +65,29 @@ class Solution(BaseModel):
54
  room: Card
55
 
56
 
 
 
 
 
 
 
 
 
57
  class Game(BaseModel):
58
  """Represents a complete game instance."""
59
  game_id: str
60
  name: str
61
  status: GameStatus = GameStatus.WAITING
 
 
 
 
 
 
62
  rooms: List[str]
 
 
 
63
  use_ai: bool = False
64
 
65
  # Players
@@ -79,18 +107,23 @@ class Game(BaseModel):
79
  turns: List[Turn] = Field(default_factory=list)
80
  winner: Optional[str] = None
81
 
 
 
 
82
  # AI-generated content
83
  scenario: Optional[str] = None
84
 
85
  @staticmethod
86
  def generate_game_id() -> str:
87
- """Generate a unique 6-character game ID."""
88
- return ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
 
89
 
90
  def add_player(self, player_name: str) -> Player:
91
  """Add a new player to the game."""
92
  player_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
93
- player = Player(id=player_id, name=player_name)
 
94
  self.players.append(player)
95
  return player
96
 
@@ -101,9 +134,17 @@ class Game(BaseModel):
101
  return self.players[self.current_player_index]
102
 
103
  def next_turn(self):
104
- """Move to the next player's turn."""
105
- if self.players:
 
 
 
 
 
106
  self.current_player_index = (self.current_player_index + 1) % len(self.players)
 
 
 
107
 
108
  def is_full(self) -> bool:
109
  """Check if the game has reached maximum players."""
@@ -113,7 +154,11 @@ class Game(BaseModel):
113
  class CreateGameRequest(BaseModel):
114
  """Request to create a new game."""
115
  game_name: str
 
 
116
  rooms: List[str]
 
 
117
  use_ai: bool = False
118
 
119
 
@@ -127,7 +172,11 @@ class GameAction(BaseModel):
127
  """Request to perform a game action."""
128
  game_id: str
129
  player_id: str
130
- action_type: str # "suggest", "accuse", "pass"
 
 
 
 
131
  character: Optional[str] = None
132
  weapon: Optional[str] = None
133
  room: Optional[str] = None
 
10
  import string
11
 
12
 
13
+ class NarrativeTone(str, Enum):
14
+ """Narrative tone options for the game."""
15
+ SERIOUS = "🕵️ Sérieuse"
16
+ PARODY = "😂 Parodique"
17
+ FANTASY = "🧙‍♂️ Fantastique"
18
+ THRILLER = "🎬 Thriller"
19
+ HORROR = "👻 Horreur"
20
+
21
+
22
  class CardType(str, Enum):
23
  """Types of cards in the game."""
24
  CHARACTER = "character"
 
38
  name: str
39
  cards: List[Card] = Field(default_factory=list)
40
  is_active: bool = True
41
+ current_room_index: int = 0 # Position on the board
42
 
43
 
44
  class GameStatus(str, Enum):
 
52
  """Represents a turn action in the game."""
53
  player_id: str
54
  player_name: str
55
+ action: str # "move", "suggest", "accuse", "pass"
56
  details: Optional[str] = None
57
+ ai_comment: Optional[str] = None # Desland's sarcastic comment
58
  timestamp: str
59
 
60
 
 
65
  room: Card
66
 
67
 
68
+ class InvestigationNote(BaseModel):
69
+ """Player's notes on the investigation."""
70
+ player_id: str
71
+ element_name: str # Name of suspect/weapon/room
72
+ element_type: str # "suspect", "weapon", "room"
73
+ status: str # "unknown", "eliminated", "maybe"
74
+
75
+
76
  class Game(BaseModel):
77
  """Represents a complete game instance."""
78
  game_id: str
79
  name: str
80
  status: GameStatus = GameStatus.WAITING
81
+
82
+ # Theme and narrative
83
+ narrative_tone: str = NarrativeTone.SERIOUS.value
84
+ custom_prompt: Optional[str] = None
85
+
86
+ # Game elements (customizable)
87
  rooms: List[str]
88
+ custom_weapons: List[str] = Field(default_factory=list)
89
+ custom_suspects: List[str] = Field(default_factory=list)
90
+
91
  use_ai: bool = False
92
 
93
  # Players
 
107
  turns: List[Turn] = Field(default_factory=list)
108
  winner: Optional[str] = None
109
 
110
+ # Investigation notes (for UI)
111
+ investigation_notes: List[InvestigationNote] = Field(default_factory=list)
112
+
113
  # AI-generated content
114
  scenario: Optional[str] = None
115
 
116
  @staticmethod
117
  def generate_game_id() -> str:
118
+ """Generate a unique 4-character game ID (like AB7F)."""
119
+ chars = string.ascii_uppercase + string.digits
120
+ return ''.join(random.choices(chars, k=4))
121
 
122
  def add_player(self, player_name: str) -> Player:
123
  """Add a new player to the game."""
124
  player_id = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
125
+ # All players start in the first room
126
+ player = Player(id=player_id, name=player_name, current_room_index=0)
127
  self.players.append(player)
128
  return player
129
 
 
134
  return self.players[self.current_player_index]
135
 
136
  def next_turn(self):
137
+ """Move to the next active player's turn."""
138
+ if not self.players:
139
+ return
140
+
141
+ # Skip eliminated players
142
+ attempts = 0
143
+ while attempts < len(self.players):
144
  self.current_player_index = (self.current_player_index + 1) % len(self.players)
145
+ if self.players[self.current_player_index].is_active:
146
+ break
147
+ attempts += 1
148
 
149
  def is_full(self) -> bool:
150
  """Check if the game has reached maximum players."""
 
154
  class CreateGameRequest(BaseModel):
155
  """Request to create a new game."""
156
  game_name: str
157
+ narrative_tone: str = NarrativeTone.SERIOUS.value
158
+ custom_prompt: Optional[str] = None
159
  rooms: List[str]
160
+ custom_weapons: List[str]
161
+ custom_suspects: List[str]
162
  use_ai: bool = False
163
 
164
 
 
172
  """Request to perform a game action."""
173
  game_id: str
174
  player_id: str
175
+ action_type: str # "move", "suggest", "accuse", "pass"
176
+ # For movement
177
+ dice_roll: Optional[int] = None
178
+ target_room_index: Optional[int] = None
179
+ # For suggestions/accusations
180
  character: Optional[str] = None
181
  weapon: Optional[str] = None
182
  room: Optional[str] = None