Spaces:
Sleeping
Sleeping
Commit
·
1d68989
1
Parent(s):
e1033e2
Improve game logic
Browse files- ai_service.py +91 -16
- app.py +405 -218
- game_engine.py +56 -5
- game_manager.py +4 -0
- 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
|
| 40 |
|
| 41 |
-
IMPORTANT:
|
| 42 |
|
|
|
|
| 43 |
Rooms: {', '.join(rooms)}
|
| 44 |
Characters: {', '.join(characters)}
|
| 45 |
|
| 46 |
-
Start with Desland introducing himself (getting his name wrong
|
| 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
|
| 67 |
self,
|
| 68 |
player_name: str,
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
) -> Optional[str]:
|
| 71 |
"""
|
| 72 |
-
Generate
|
| 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 |
-
|
|
|
|
| 80 |
|
| 81 |
Player: {player_name}
|
| 82 |
-
|
|
|
|
| 83 |
|
| 84 |
-
Desland
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 98 |
return None
|
| 99 |
except Exception as e:
|
| 100 |
-
print(f"Error generating
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
return None
|
| 102 |
|
| 103 |
def _generate_text(self, prompt: str) -> str:
|
| 104 |
"""
|
| 105 |
Internal method to generate text using OpenAI API.
|
| 106 |
-
Uses
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
},
|
| 118 |
{
|
| 119 |
"role": "user",
|
| 120 |
"content": prompt
|
| 121 |
}
|
| 122 |
],
|
| 123 |
-
temperature=0.
|
| 124 |
-
max_tokens=
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
"""
|
| 40 |
-
Create a new game.
|
| 41 |
"""
|
| 42 |
if not game_name or not rooms_text:
|
| 43 |
-
return "❌
|
| 44 |
|
| 45 |
-
# Parse
|
| 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 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 88 |
-
f"🔑 Code d'Enquête : {game.game_id}
|
| 89 |
-
f"📤 Partagez ce code avec les autres
|
| 90 |
-
f"ℹ️ Minimum {settings.MIN_PLAYERS}
|
| 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={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 106 |
-
f"🔑 Code
|
| 107 |
-
f"📤 Partagez ce code
|
| 108 |
-
f"ℹ️
|
| 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"❌
|
| 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 "❌
|
| 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 "❌
|
| 140 |
|
| 141 |
if game.status != GameStatus.WAITING:
|
| 142 |
-
return "❌
|
| 143 |
|
| 144 |
if game.is_full():
|
| 145 |
-
return "❌
|
| 146 |
|
| 147 |
player = game_manager.join_game(game_id, player_name)
|
| 148 |
if not player:
|
| 149 |
-
return "❌
|
| 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
|
| 157 |
f"👋 Bienvenue, {player_name} !\n\n"
|
| 158 |
-
f"⏳ Attendez que le
|
| 159 |
-
f"📖
|
| 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 "❌
|
|
|
|
|
|
|
| 209 |
|
| 210 |
return (
|
| 211 |
-
f"
|
| 212 |
-
f"
|
| 213 |
-
f"
|
| 214 |
-
f"
|
|
|
|
| 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"
|
| 223 |
-
f"
|
| 224 |
-
f"
|
| 225 |
-
f"➡️ Allez dans l'onglet 🔎 Enquêter pour
|
| 226 |
)
|
| 227 |
else:
|
| 228 |
-
return f"❌
|
| 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 |
-
|
| 261 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
| 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"═══
|
| 299 |
|
| 300 |
status_map = {
|
| 301 |
-
"waiting": "⏳
|
| 302 |
-
"in_progress": "
|
| 303 |
-
"finished": "
|
| 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━━━
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
output.append(f"🔪 Armes : {', '.join(data['weapons'])}")
|
| 319 |
|
| 320 |
-
output.append(f"\n━━━ 👥
|
| 321 |
-
for player in data
|
| 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
|
| 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🏆🏆🏆
|
| 337 |
)
|
| 338 |
|
| 339 |
if data["recent_turns"]:
|
| 340 |
-
output.append(f"\n━━━ 📰
|
| 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 "❌
|
| 358 |
|
| 359 |
if not all([character, weapon, room]):
|
| 360 |
-
return "❌
|
| 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 "❌
|
| 372 |
|
| 373 |
-
# Verify it's the player's turn
|
| 374 |
if not GameEngine.can_player_act(game, state.player_id):
|
| 375 |
-
return "❌
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
player = next((p for p in game.players if p.id == state.player_id), None)
|
| 378 |
if not player:
|
| 379 |
-
return "❌
|
| 380 |
|
|
|
|
| 381 |
can_disprove, disprover, card = GameEngine.check_suggestion(
|
| 382 |
game, state.player_id, character, weapon, room
|
| 383 |
)
|
| 384 |
|
| 385 |
-
suggestion_text = f"
|
| 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 = "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 401 |
-
|
| 402 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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"❌
|
| 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 "❌
|
| 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 "❌
|
| 462 |
|
| 463 |
-
# Verify it's the player's turn
|
| 464 |
if not GameEngine.can_player_act(game, state.player_id):
|
| 465 |
-
return "❌
|
| 466 |
|
| 467 |
player = next((p for p in game.players if p.id == state.player_id), None)
|
| 468 |
if not player:
|
| 469 |
-
return "❌
|
| 470 |
|
| 471 |
-
accusation_text = f"
|
| 472 |
|
| 473 |
is_correct, message = GameEngine.process_accusation(
|
| 474 |
game, state.player_id, character, weapon, room
|
| 475 |
)
|
| 476 |
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
if not is_correct and game.status == GameStatus.IN_PROGRESS:
|
| 480 |
game.next_turn()
|
| 481 |
|
| 482 |
game_manager.save_games()
|
| 483 |
|
| 484 |
-
#
|
| 485 |
-
if
|
| 486 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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"❌
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 "❌
|
| 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 "❌
|
| 544 |
|
| 545 |
-
# Verify it's the player's turn
|
| 546 |
if not GameEngine.can_player_act(game, state.player_id):
|
| 547 |
-
return "❌
|
| 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", "
|
| 555 |
game.next_turn()
|
| 556 |
game_manager.save_games()
|
| 557 |
|
| 558 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 573 |
-
return f"✅ Tour passé !\n\n➡️ C'est maintenant au tour de la prochaine poupouille."
|
| 574 |
else:
|
| 575 |
-
|
| 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"❌
|
| 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("###
|
| 853 |
-
gr.Markdown("*
|
| 854 |
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
|
|
|
| 860 |
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
|
| 868 |
use_ai_checkbox = gr.Checkbox(
|
| 869 |
-
label="🤖 Activer le Narrateur IA -
|
| 870 |
value=False,
|
| 871 |
visible=settings.USE_OPENAI,
|
| 872 |
-
info="
|
| 873 |
)
|
| 874 |
|
| 875 |
create_btn = gr.Button(
|
| 876 |
-
"
|
| 877 |
)
|
| 878 |
create_output = gr.Textbox(
|
| 879 |
-
label="📋
|
| 880 |
)
|
| 881 |
game_id_display = gr.Textbox(
|
| 882 |
-
label="🔑 Code d'Enquête (
|
| 883 |
interactive=False,
|
| 884 |
show_copy_button=True,
|
| 885 |
)
|
| 886 |
|
| 887 |
create_btn.click(
|
| 888 |
create_game,
|
| 889 |
-
inputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("###
|
| 945 |
-
gr.Markdown("
|
| 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="🗂️
|
| 953 |
lines=20,
|
| 954 |
max_lines=30,
|
| 955 |
show_copy_button=True,
|
| 956 |
-
info="Cliquez sur Actualiser pour voir l'état actuel
|
| 957 |
)
|
| 958 |
|
| 959 |
refresh_btn.click(get_player_view, outputs=game_view)
|
| 960 |
|
| 961 |
gr.Markdown("---")
|
| 962 |
-
gr.Markdown("###
|
| 963 |
-
gr.Markdown("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 964 |
|
| 965 |
with gr.Group():
|
| 966 |
with gr.Row():
|
| 967 |
suggest_character = gr.Dropdown(
|
| 968 |
label="👤 Suspect",
|
| 969 |
-
choices=
|
| 970 |
-
info="
|
| 971 |
)
|
| 972 |
suggest_weapon = gr.Dropdown(
|
| 973 |
-
label="🔪 Arme
|
| 974 |
-
choices=
|
| 975 |
-
info="
|
| 976 |
)
|
| 977 |
suggest_room = gr.Dropdown(
|
| 978 |
-
label="🚪 Lieu
|
| 979 |
choices=[], # Will be populated from game
|
| 980 |
-
info="
|
| 981 |
)
|
| 982 |
|
| 983 |
suggest_btn = gr.Button(
|
| 984 |
-
"💭
|
| 985 |
)
|
| 986 |
suggest_output = gr.Textbox(
|
| 987 |
-
label="🗨️
|
| 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 |
-
"
|
| 1000 |
)
|
| 1001 |
|
| 1002 |
with gr.Group():
|
| 1003 |
with gr.Row():
|
| 1004 |
accuse_character = gr.Dropdown(
|
| 1005 |
-
label="👤 Le
|
| 1006 |
-
choices=
|
| 1007 |
-
info="Qui
|
| 1008 |
)
|
| 1009 |
accuse_weapon = gr.Dropdown(
|
| 1010 |
label="🔪 L'Arme",
|
| 1011 |
-
choices=
|
| 1012 |
-
info="Avec
|
| 1013 |
)
|
| 1014 |
accuse_room = gr.Dropdown(
|
| 1015 |
-
label="🚪 Le Lieu",
|
|
|
|
|
|
|
| 1016 |
)
|
| 1017 |
|
| 1018 |
accuse_btn = gr.Button(
|
| 1019 |
-
"⚡
|
| 1020 |
)
|
| 1021 |
accuse_output = gr.Textbox(
|
| 1022 |
-
label="⚖️ Verdict", lines=
|
| 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 |
-
|
| 1057 |
-
|
| 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 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 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 (
|
| 13 |
DEFAULT_CHARACTERS = [
|
| 14 |
"Miss Scarlett",
|
| 15 |
"Colonel Mustard",
|
|
@@ -19,7 +19,7 @@ DEFAULT_CHARACTERS = [
|
|
| 19 |
"Professor Plum"
|
| 20 |
]
|
| 21 |
|
| 22 |
-
# Default weapon names (
|
| 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
|
| 46 |
]
|
| 47 |
|
| 48 |
# Create weapon cards
|
| 49 |
game.weapons = [
|
| 50 |
Card(name=name, card_type=CardType.WEAPON)
|
| 51 |
-
for name in
|
| 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
|
| 88 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|