802 lines
38 KiB
Python
802 lines
38 KiB
Python
import os
|
||
from flask import Flask, request, jsonify
|
||
from flask_sqlalchemy import SQLAlchemy
|
||
from dotenv import load_dotenv
|
||
import pandas as pd
|
||
from models import db, Player, Step, MessageLog
|
||
import requests
|
||
import logging
|
||
from io import StringIO
|
||
import re
|
||
|
||
logging.basicConfig(level=logging.INFO)
|
||
log = logging.getLogger("flask")
|
||
|
||
# Charger .env
|
||
load_dotenv()
|
||
|
||
app = Flask(__name__)
|
||
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev_secret")
|
||
app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE_URL", "sqlite:///treasure.db")
|
||
db.init_app(app)
|
||
|
||
PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY")
|
||
FLASK_SHARED_SECRET = os.getenv("FLASK_SHARED_SECRET", "secret_flask_matrix")
|
||
CONTEXT_TURNS = int(os.getenv("CONTEXT_TURNS", 10)) # nbre d'échanges pris en compte
|
||
GOOGLE_SHEET_CSV_URL = os.getenv("GOOGLE_SHEET_CSV_URL", "")
|
||
|
||
def build_perplexity_history(player, current_user_message, step):
|
||
# récupère tout l'historique du joueur, ordonné
|
||
history = MessageLog.query.filter_by(player_id=player.id).order_by(MessageLog.timestamp.asc()).all()
|
||
messages = []
|
||
for msg in history:
|
||
if msg.sender == "Katniss":
|
||
messages.append({"role": "assistant", "content": msg.content})
|
||
else:
|
||
messages.append({"role": "user", "content": msg.content})
|
||
|
||
# On ajoute le message courant et le contexte de l'étape dans UN SEUL message user à la fin
|
||
context_prefix = (
|
||
f"[Contexte: Étape {step.step if step else '?'} - {step.location if step else ''}]\n"
|
||
f"Énigme: {step.location_enigma if step else ''}\n"
|
||
)
|
||
messages.append({
|
||
"role": "user",
|
||
"content": context_prefix + current_user_message.strip()
|
||
})
|
||
|
||
# Nettoie pour garantir alternance
|
||
cleaned = []
|
||
last_role = None
|
||
for m in messages:
|
||
if m["role"] == last_role:
|
||
# Fusionne le contenu avec le dernier du même role (précaution très utile)
|
||
cleaned[-1]["content"] += "\n\n" + m["content"]
|
||
else:
|
||
cleaned.append(m)
|
||
last_role = m["role"]
|
||
|
||
# Ancien prompt_system remplacé pour prendre en compte le model Step
|
||
prompt_system = (
|
||
"Tu es Katniss, guide d'aventure pour une chasse au trésor (rôle assistant). "
|
||
"Tu reçois toujours, en contexte, un 'step' qui contient les champs suivants :\n"
|
||
"- step : numéro de l'étape\n"
|
||
"- pre_text : texte d'introduction/context avant saisie du code (si présent)\n"
|
||
"- location : titre du lieu\n"
|
||
"- location_enigma : énigme principale ou description de l'énigme à créer\n"
|
||
"- location_hint : indice lié au code (à utiliser progressivement)\n"
|
||
"- code : la réponse/código (NE JAMAIS révéler au joueur)\n"
|
||
"- question : question additionnelle éventuelle\n"
|
||
"- question_hint : indice pour la question\n"
|
||
"- answer : réponse à la question (NE JAMAIS révéler)\n"
|
||
"- comments : commentaire pour le MJ (informations internes)\n"
|
||
"- success_text : texte à afficher après réussite\n"
|
||
"- image_path, audio_path : chemins vers médias liés à l'étape (mention possible)\n\n"
|
||
"Règles strictes :\n"
|
||
"1) Ne jamais fournir directement 'code' ni 'answer'. Si le joueur demande la réponse, refuse poliment et propose un indice.\n"
|
||
"2) Proposer des indices progressifs : commencer par des indices généraux (réutiliser location_hint ou question_hint), "
|
||
"puis, si le joueur insiste, donner des indices de plus en plus directs sans révéler la solution.\n"
|
||
"3) Utiliser pre_text pour contextualiser la demande si présent, et suggérer au joueur d'examiner image_path/audio_path si fournis.\n"
|
||
"4) Si la requête montre que le joueur a trouvé le code, encourager et rappeler qu'après validation le texte success_text sera affiché.\n"
|
||
"5) Ne pas inventer d'informations non présentes dans 'step'; se limiter aux champs et à l'historique des messages.\n"
|
||
"6) Répondre en français, ton amical et encourageant, bref si l'utilisateur demande un indice simple, plus explicite si l'utilisateur a tenté plusieurs fois.\n"
|
||
)
|
||
messages_final = [{"role": "system", "content": prompt_system}] + cleaned[-20:] # Limite à 20 messages récents
|
||
return messages_final
|
||
|
||
|
||
def get_ai_hint(player, current_user_message):
|
||
step = Step.query.filter_by(step=player.current_step).first()
|
||
messages = build_perplexity_history(player, current_user_message, step)
|
||
log.info(messages)
|
||
url = "https://api.perplexity.ai/chat/completions"
|
||
headers = {
|
||
"Authorization": f"Bearer {PERPLEXITY_API_KEY}",
|
||
"Content-Type": "application/json"
|
||
}
|
||
payload = {
|
||
"model": "sonar",
|
||
"messages": messages,
|
||
"max_tokens": 500,
|
||
"temperature": 0.7,
|
||
}
|
||
try:
|
||
resp = requests.post(url, headers=headers, json=payload, timeout=20)
|
||
if resp.status_code == 400:
|
||
app.logger.error(f"Perplexity 400: {resp.text}")
|
||
return "(Katniss indisponible: historique de messages mal formé, regarde les logs serveur !)"
|
||
resp.raise_for_status()
|
||
content = resp.json()["choices"][0]["message"]["content"]
|
||
return content.strip() if content else "(Katniss n'a rien répondu, elle ne capte probablement plus au fond du bunker)"
|
||
except Exception as e:
|
||
return f"(Katniss est en échec: {e})"
|
||
|
||
|
||
def call_perplexity(messages, max_tokens=400, temperature=0.7):
|
||
"""Send messages to Perplexity and return assistant content or error string."""
|
||
url = "https://api.perplexity.ai/chat/completions"
|
||
headers = {
|
||
"Authorization": f"Bearer {PERPLEXITY_API_KEY}",
|
||
"Content-Type": "application/json"
|
||
}
|
||
payload = {
|
||
"model": "sonar",
|
||
"messages": messages,
|
||
"max_tokens": max_tokens,
|
||
"temperature": temperature,
|
||
}
|
||
try:
|
||
resp = requests.post(url, headers=headers, json=payload, timeout=20)
|
||
if resp.status_code == 400:
|
||
app.logger.error(f"Perplexity 400: {resp.text}")
|
||
return "(Katniss indisponible: historique de messages mal formé, regarde les logs serveur !)"
|
||
resp.raise_for_status()
|
||
content = resp.json()["choices"][0]["message"]["content"]
|
||
return content.strip() if content else ""
|
||
except Exception as e:
|
||
app.logger.exception("Perplexity error")
|
||
return f"(Katniss est en échec: {e})"
|
||
|
||
|
||
def is_affirmative(text: str) -> bool:
|
||
"""Return True if text looks like a positive/ready answer (oui/ok/yes/etc.)."""
|
||
if not text:
|
||
return False
|
||
normalized = re.sub(r"[^a-z0-9\s]", "", text.lower())
|
||
tokens = set(normalized.split())
|
||
affirmatives = {"oui", "ouais", "ok", "yes", "yeah", "yep", "go", "pret", "prêt", "daccord", "d'accord"}
|
||
return len(tokens & affirmatives) > 0
|
||
|
||
|
||
def matches_any(text: str, choices: str) -> bool:
|
||
"""Return True if text matches any of the semicolon-separated choices (case-insensitive).
|
||
|
||
Normalises by lowercasing, trimming and removing punctuation for a more forgiving match.
|
||
"""
|
||
if not text:
|
||
return False
|
||
if not choices:
|
||
return False
|
||
text_norm = re.sub(r"[^a-z0-9\s]", "", text.lower()).strip()
|
||
for part in str(choices).split(";"):
|
||
part_norm = re.sub(r"[^a-z0-9\s]", "", part.lower()).strip()
|
||
if part_norm == text_norm:
|
||
return True
|
||
return False
|
||
|
||
|
||
# def generate_step_intro(player, step):
|
||
# """Ask the AI to improve pre_text + location_enigma and return a short intro in French."""
|
||
# if not step:
|
||
# return "(Aucune étape trouvée)"
|
||
# system = (
|
||
# "Tu es Katniss, guide d'aventure et maître du jeu pour une chasse au trésor."
|
||
# "Améliore et synthétise en français le texte d'introduction fourni (pre_text) et l'énigme (location_enigma) "
|
||
# "pour qu'il soit engageant et clair pour le joueur. Ne révèle jamais le 'code' ni la 'answer'."
|
||
# "Parle à la première personne du singulier, comme si tu parlais directement au joueur."
|
||
# "Renvoie la réponse au format markdown."
|
||
# )
|
||
# user_content = f"Prétexte:\n{step.pre_text or ''}\n\nÉnigme:\n{step.location_enigma or ''}\n\n"
|
||
# messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}]
|
||
# return call_perplexity(messages, max_tokens=1500)
|
||
|
||
def generate_step_intro(player, step):
|
||
"""Ask the AI to improve pre_text + location_enigma and return a short intro in French."""
|
||
if not step:
|
||
return "(Aucune étape trouvée)"
|
||
|
||
system = (
|
||
"Tu es Katniss, guide d'aventure et maître du jeu pour une chasse au trésor."
|
||
"Améliore et synthétise en frle texte d'introduction fourni (pre_text) et l'énigme (location_enigma) "
|
||
"pour qu'il soit engageant et clair pour le joueur. Ne révèle jamais le 'code' ni la 'answer'."
|
||
"Parle à la première personne du singulier, comme si tu parlais directement au joueur."
|
||
"Renvoie la réponse au format markdown."
|
||
)
|
||
user_content = f"Prétexte:\n{step.pre_text or ''}\n\nÉnigme:\n{step.location_enigma or ''}\n\n"
|
||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}]
|
||
return call_perplexity(messages, max_tokens=1500)
|
||
|
||
|
||
def generate_intro_text(player, step):
|
||
"""Generate only the intro (pre_text) improved by AI, without revealing the enigma.
|
||
Returns markdown string."""
|
||
if not step:
|
||
return "(Aucune étape trouvée)"
|
||
# return step.pre_text or ""
|
||
system = (
|
||
"Renvoi le texte d'introduction fourni (pre_text) au format markdown sans fautes mais ne le modifie pas. Ne rajoute pas d'autre informations. "
|
||
"")
|
||
user_content = f"pre_text:\n{step.pre_text or ''}\n\n"
|
||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}]
|
||
return call_perplexity(messages, max_tokens=800)
|
||
|
||
|
||
def generate_enigma_text(player, step):
|
||
"""Generate only the enigma (location_enigma) improved by AI, ready to be presented to the player."""
|
||
if not step:
|
||
return "(Aucune étape trouvée)"
|
||
# return step.location_enigma or ""
|
||
system = (
|
||
"Renvoie l'énigme (location_enigma) au format markdown sans fautes mais ne la modifie pas. Ne rajoute pas d'autre informations."
|
||
""
|
||
)
|
||
user_content = f"Énigme brute:\n{step.location_enigma or ''}\n\n"
|
||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}]
|
||
return call_perplexity(messages, max_tokens=1000)
|
||
|
||
|
||
def is_player_ready_ai(player, reply_text, step):
|
||
"""Ask the AI to judge whether the player's reply indicates readiness.
|
||
|
||
Returns (bool, katniss_message). The AI is instructed to output first line TRUE or FALSE, then a short encouraging message on the next line.
|
||
"""
|
||
system = (
|
||
"Tu es Katniss, guide d'aventure. Tu dois lire la réponse courte d'un joueur et décider s'il est prêt à recevoir l'énigme suivante. "
|
||
"Réponds STRICTEMENT sur deux lignes : première ligne TRUE si le joueur est prêt, FALSE sinon ; deuxième ligne un court message d'encouragement en français (1-2 phrases)."
|
||
)
|
||
user_content = f"Contexte étape {step.step if step else '?'} - lieu: {step.location if step else ''}\nRéponse du joueur:\n{reply_text}\n\nRenvoie exactement deux lignes : TRUE/FALSE, puis le message Katniss."
|
||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}]
|
||
ai_text = call_perplexity(messages, max_tokens=200)
|
||
if not ai_text:
|
||
return False, "Prends ton temps, dis-moi quand tu seras prêt·e."
|
||
# Parse first token for true/false
|
||
first_line = ai_text.splitlines()[0].strip().lower() if ai_text else ""
|
||
kat_msg = "\n".join(ai_text.splitlines()[1:]).strip() or "Prends ton temps, dis-moi quand tu seras prêt·e."
|
||
ready = False
|
||
if "true" in first_line or "oui" in first_line or "vrai" in first_line:
|
||
ready = True
|
||
return ready, kat_msg
|
||
|
||
|
||
def generate_formatted_hint(player, step, hint_text, hint_kind="location", hint_index=0):
|
||
"""Ask the AI to present a single hint (already extracted) in a friendly way."""
|
||
system = (
|
||
"Tu es Katniss, guide d'aventure et maitre du jeu pour Magali. Renvoie l'indice (hint) SANS MODIFICATION, sans fautes, sans autres informations"
|
||
"Avant l'indice, encourage le joueur."
|
||
)
|
||
user_content = (
|
||
f"indice : {hint_text}"
|
||
""
|
||
)
|
||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}]
|
||
return call_perplexity(messages, max_tokens=1000)
|
||
|
||
|
||
def generate_question_text(player:Player, step:Step):
|
||
"""Ask the AI to format the question to the player if present."""
|
||
if not step or not step.question:
|
||
return ""
|
||
system = (
|
||
"Tu es Katniss, guide d'aventure et maitre du jeu. Reformule la question fournie de façon claire et engageante en français. "
|
||
"Parle à la première personne du singulier, comme si tu parlais directement au joueur."
|
||
"Renvoie la réponse au format markdown."
|
||
"Les questions sont liés à des évenements entre Sam et Magali"
|
||
)
|
||
user_content = f"Question brute:\n{step.question}\n\nRenvoie la question après avoir donner un encouragement pour avoir réussi à trouver le code. indique en gras que c'est une question. redonne le numero de l'etape {step.step}"
|
||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}]
|
||
return call_perplexity(messages, max_tokens=1000)
|
||
|
||
|
||
def generate_success_text(player, step):
|
||
"""Ask the AI to produce an encouraging success message (can use step.success_text as base)."""
|
||
base = step.success_text or "Bravo ! Tu as réussi cette étape."
|
||
|
||
# system = (
|
||
# "Tu es Katniss, guide d'aventure. Génère un court message d'encouragement en français à destination du joueur après réussite. "
|
||
# "Tu peux améliorer le texte de base sans révéler d'informations supplémentaires."
|
||
# "garde les élements entre crochets dans le texte de base"
|
||
# "Parle à la première personne du singulier, comme si tu parlais directement au joueur."
|
||
# "Renvoie la réponse au format markdown."
|
||
# )
|
||
system = (
|
||
"Renvoie le texte d'encouragement fourni (success_text) au format markdown sans fautes mais ne le modifie pas. Ne rajoute pas d'autre informations. "
|
||
"")
|
||
# Ask the AI to enhance the base text but ensure we keep the original base (with bracketed URLs)
|
||
# user_content = (
|
||
# f"Texte de base:\n{base}\n\n"
|
||
# "Améliore ce texte de base pour en faire un court message d'encouragement chaleureux en français."
|
||
# " NE MODIFIE PAS ni ne SUPPRIME les éléments entre crochets [] présents dans le texte de base."
|
||
# " Renvoie uniquement le texte d'encouragement (format markdown)."
|
||
# )
|
||
user_content = f"success_text:\n{base}\n\n"
|
||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user_content}]
|
||
ai_response = call_perplexity(messages, max_tokens=1000)
|
||
# Always return the original base first (to preserve bracketed URLs), then the AI enhancement
|
||
# If AI returned an empty string, fall back to base
|
||
if not ai_response or ai_response.strip() == "":
|
||
return base
|
||
return f"{ai_response.strip()}"
|
||
|
||
def fetch_steps_from_gsheet(csv_url=GOOGLE_SHEET_CSV_URL):
|
||
resp = requests.get(csv_url)
|
||
resp.raise_for_status()
|
||
csv_content = resp.text
|
||
|
||
df = pd.read_csv(StringIO(csv_content))
|
||
df = df.fillna("") # Remplace NaN par ""
|
||
steps = []
|
||
for _, row in df.iterrows():
|
||
# Only accept the exact CSV columns: step, pre_text, location, location_enigma,
|
||
# location_hint, code, question, question_hint, answer, comments, success_text
|
||
raw_step = row.get('step', "")
|
||
try:
|
||
number = int(float(raw_step)) if str(raw_step).strip() != "" else None
|
||
except Exception:
|
||
# skip invalid/empty step rows
|
||
continue
|
||
if number is None:
|
||
continue
|
||
|
||
pre_text = row.get('pre_text', "")
|
||
location = row.get('location', "")
|
||
location_enigma = row.get('location_enigma', "")
|
||
location_hint = row.get('location_hint', "")
|
||
code = str(row.get('code', ""))
|
||
question = row.get('question', "")
|
||
question_hint = row.get('question_hint', "")
|
||
answer = str(row.get('answer', ""))
|
||
comments = row.get('comments', "")
|
||
success_text = row.get('success_text', "")
|
||
|
||
# No media columns expected in this simplified mapping
|
||
image_path = ""
|
||
audio_path = ""
|
||
|
||
step = Step(
|
||
step=number,
|
||
pre_text=pre_text,
|
||
location=location,
|
||
location_enigma=location_enigma,
|
||
location_hint=location_hint,
|
||
code=code,
|
||
question=question,
|
||
question_hint=question_hint,
|
||
answer=answer,
|
||
comments=comments,
|
||
success_text=success_text,
|
||
image_path=image_path,
|
||
audio_path=audio_path,
|
||
)
|
||
steps.append(step)
|
||
return steps
|
||
|
||
|
||
@app.route("/api/matrix/incoming", methods=["POST"])
|
||
def matrix_incoming():
|
||
data = request.json
|
||
if data.get("secret") != FLASK_SHARED_SECRET:
|
||
return jsonify({"error": "Forbidden"}), 403
|
||
|
||
matrix_id = data.get("sender")
|
||
text = data.get("body", "").strip()
|
||
|
||
player = Player.query.filter_by(matrix_id=matrix_id).first()
|
||
is_new_player = False
|
||
if not player:
|
||
# initialize player with hint counters and stage (awaiting ready confirmation)
|
||
player = Player(
|
||
matrix_id=matrix_id,
|
||
current_step=1,
|
||
stage='awaiting_ready',
|
||
location_hint_index=0,
|
||
question_hint_index=0,
|
||
last_sent_step=None,
|
||
)
|
||
db.session.add(player)
|
||
db.session.commit()
|
||
is_new_player = True
|
||
|
||
# Log message reçu
|
||
db.session.add(MessageLog(player_id=player.id, sender="mag", content=text))
|
||
|
||
step = Step.query.filter_by(step=player.current_step).first()
|
||
|
||
# If no step found, respond accordingly
|
||
if not step:
|
||
reply = "(Aucune étape définie pour ce joueur pour le moment.)"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply))
|
||
db.session.commit()
|
||
return jsonify({"reply": reply})
|
||
|
||
# Ensure player has stage/hint counters attributes (in case models were not migrated)
|
||
if not hasattr(player, 'stage') or not player.stage:
|
||
player.stage = 'intro_needed'
|
||
if not hasattr(player, 'location_hint_index'):
|
||
player.location_hint_index = 0
|
||
if not hasattr(player, 'question_hint_index'):
|
||
player.question_hint_index = 0
|
||
|
||
reply = None
|
||
|
||
# If player is awaiting the "ready to play" confirmation
|
||
if player.stage == 'awaiting_ready':
|
||
# Use AI to judge whether the player's message indicates readiness and get a short Katniss reply
|
||
ready, kat_msg = is_player_ready_ai(player, text, step)
|
||
if ready:
|
||
# Prepare and send intro for the current step
|
||
player.stage = 'intro_needed'
|
||
db.session.commit()
|
||
if not step:
|
||
reply = "(Aucune étape définie pour le moment.)"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply))
|
||
db.session.commit()
|
||
return jsonify({"reply": reply})
|
||
# Instead of sending enigma now, send only the improved intro and ask readiness for enigma
|
||
intro = generate_intro_text(player, step)
|
||
player.stage = 'awaiting_ready_for_enigma'
|
||
player.location_hint_index = 0
|
||
player.question_hint_index = 0
|
||
player.last_sent_step = player.current_step
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg))
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=intro))
|
||
db.session.commit()
|
||
# Prompt the user to confirm when they're ready to see the enigma
|
||
ready_prompt = "T'es-tu prête pour l'énigme ? "
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt))
|
||
db.session.commit()
|
||
return jsonify({"reply": kat_msg + "\n\n" + intro + "\n\n" + ready_prompt})
|
||
else:
|
||
# AI says not ready: send Katniss encouraging message
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg))
|
||
db.session.commit()
|
||
return jsonify({"reply": kat_msg})
|
||
|
||
# If player answered the intro and we are waiting for them to confirm readiness for the enigma
|
||
if player.stage == 'awaiting_ready_for_enigma':
|
||
# Use AI to interpret readiness and get Katniss encouragement
|
||
ready, kat_msg = is_player_ready_ai(player, text, step)
|
||
|
||
if ready:
|
||
# send the enigma (improved by AI) and move to awaiting_code
|
||
enigma = generate_enigma_text(player, step)
|
||
player.stage = 'awaiting_code'
|
||
player.location_hint_index = 0
|
||
player.question_hint_index = 0
|
||
player.last_sent_step = player.current_step
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg))
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=enigma))
|
||
db.session.commit()
|
||
return jsonify({"reply": kat_msg + "\n\n" + enigma})
|
||
else:
|
||
# send encouraging message and remain in awaiting_ready_for_enigma
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg))
|
||
db.session.commit()
|
||
return jsonify({"reply": kat_msg})
|
||
|
||
# If player is awaiting confirmation after a success to continue to next step
|
||
if player.stage == 'awaiting_ready_after_success':
|
||
# The current_step was already incremented at success time; get the new step
|
||
next_step = Step.query.filter_by(step=player.current_step).first()
|
||
# Use AI to interpret readiness
|
||
ready, kat_msg = is_player_ready_ai(player, text, next_step)
|
||
if ready:
|
||
if not next_step:
|
||
reply = "(Aucune étape suivante définie.)"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply))
|
||
db.session.commit()
|
||
return jsonify({"reply": reply})
|
||
# send the intro for the next step (improved by AI) and move to awaiting_ready_for_enigma
|
||
next_intro = generate_intro_text(player, next_step)
|
||
player.stage = 'awaiting_ready_for_enigma'
|
||
player.location_hint_index = 0
|
||
player.question_hint_index = 0
|
||
player.last_sent_step = player.current_step
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg))
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=next_intro))
|
||
db.session.commit()
|
||
ready_prompt = "T'es-tu prête pour l'énigme ?"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt))
|
||
db.session.commit()
|
||
return jsonify({"reply": kat_msg + "\n\n" + next_intro + "\n\n" + ready_prompt})
|
||
else:
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=kat_msg))
|
||
db.session.commit()
|
||
return jsonify({"reply": kat_msg})
|
||
|
||
# 1) If we need to send the intro for this step
|
||
if player.stage == 'intro_needed' or player.current_step != getattr(player, 'last_sent_step', None):
|
||
intro = generate_intro_text(player, step)
|
||
# store that we've sent the intro and reset hint counters, but wait for readiness before enigma
|
||
player.stage = 'awaiting_ready_for_enigma'
|
||
player.location_hint_index = 0
|
||
player.question_hint_index = 0
|
||
player.last_sent_step = player.current_step
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=intro))
|
||
db.session.commit()
|
||
ready_prompt = "T'es-tu prête pour l'énigme ?"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt))
|
||
db.session.commit()
|
||
return jsonify({"reply": intro + "\n\n" + ready_prompt})
|
||
|
||
# 2) We're waiting for the code
|
||
if player.stage == 'awaiting_code':
|
||
if matches_any(text, step.code or ""):
|
||
# Correct code
|
||
# If there's a question, send it (formatted by AI) and change stage
|
||
if step.question and step.question.strip() != "":
|
||
qtext = generate_question_text(player, step)
|
||
player.stage = 'awaiting_answer'
|
||
player.question_hint_index = 0
|
||
reply = qtext or step.question
|
||
else:
|
||
# No question: generate success, advance step and ask ready-to-continue
|
||
success = generate_success_text(player, step)
|
||
player.current_step += 1
|
||
# After success, wait for player's confirmation before sending next intro
|
||
player.stage = 'awaiting_ready_after_success'
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=success))
|
||
db.session.commit()
|
||
# Check if there's a next step; if so, prepare a ready prompt as followup
|
||
next_step = Step.query.filter_by(step=player.current_step).first()
|
||
if next_step:
|
||
# Optionally pre-generate and store the intro for debugging/history
|
||
next_intro = generate_intro_text(player, next_step)
|
||
player.last_sent_step = player.current_step
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=next_intro))
|
||
db.session.commit()
|
||
ready_prompt = "Bravo ! Veux-tu continuer vers l'étape suivante ?"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt))
|
||
db.session.commit()
|
||
return jsonify({"reply": success, "followup": ready_prompt})
|
||
else:
|
||
reply = success + "\n\nTu as terminé toutes les étapes. Félicitations !"
|
||
db.session.commit()
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply))
|
||
db.session.commit()
|
||
return jsonify({"reply": reply})
|
||
else:
|
||
# Incorrect code: send next location hint (one at a time)
|
||
raw_hints = (step.location_hint or "").split(";") if step.location_hint else []
|
||
idx = int(getattr(player, 'location_hint_index', 0))
|
||
if idx < len(raw_hints) and raw_hints[idx].strip() != "":
|
||
hint_raw = raw_hints[idx].strip()
|
||
hint_text = generate_formatted_hint(player, step, hint_raw, hint_kind='location', hint_index=idx)
|
||
player.location_hint_index = idx + 1
|
||
else:
|
||
# No more location hints available; ask AI to give a gentle generic hint
|
||
hint_text = generate_formatted_hint(player, step, step.location_hint or "", hint_kind='location', hint_index=idx)
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=hint_text))
|
||
db.session.commit()
|
||
return jsonify({"reply": hint_text})
|
||
|
||
# 3) We're waiting for the answer to the question
|
||
if player.stage == 'awaiting_answer':
|
||
if matches_any(text, step.answer or ""):
|
||
# Correct answer: generate success and advance
|
||
success = generate_success_text(player, step)
|
||
player.current_step += 1
|
||
# After success, wait for player's confirmation before sending next intro
|
||
player.stage = 'awaiting_ready_after_success'
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=success))
|
||
db.session.commit()
|
||
# send next intro later after confirmation; prepare ready prompt as followup if next exists
|
||
next_step = Step.query.filter_by(step=player.current_step).first()
|
||
if next_step:
|
||
next_intro = generate_step_intro(player, next_step)
|
||
player.last_sent_step = player.current_step
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=next_intro))
|
||
db.session.commit()
|
||
ready_prompt = "Bravo Mag ! T'es tu prête pour la suite ?"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt))
|
||
db.session.commit()
|
||
return jsonify({"reply": success, "followup": ready_prompt})
|
||
else:
|
||
reply = success + "\n\nTu as terminé toutes les étapes. Félicitations !"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply))
|
||
db.session.commit()
|
||
return jsonify({"reply": reply})
|
||
else:
|
||
# Incorrect answer: provide next question hint (one at a time)
|
||
raw_qhints = (step.question_hint or "").split(";") if step.question_hint else []
|
||
qidx = int(getattr(player, 'question_hint_index', 0))
|
||
if qidx < len(raw_qhints) and raw_qhints[qidx].strip() != "":
|
||
qhint_raw = raw_qhints[qidx].strip()
|
||
qhint_text = generate_formatted_hint(player, step, qhint_raw, hint_kind='question', hint_index=qidx)
|
||
player.question_hint_index = qidx + 1
|
||
else:
|
||
qhint_text = generate_formatted_hint(player, step, step.question_hint or "", hint_kind='question', hint_index=qidx)
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=qhint_text))
|
||
db.session.commit()
|
||
return jsonify({"reply": qhint_text})
|
||
|
||
# Log réponse Katniss
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=reply))
|
||
db.session.commit()
|
||
|
||
return jsonify({"reply": reply})
|
||
|
||
|
||
@app.route("/api/admin/reset_player", methods=["POST"])
|
||
def reset_player():
|
||
data = request.json or {}
|
||
secret = data.get("secret")
|
||
matrix_id = data.get("matrix_id")
|
||
if secret != FLASK_SHARED_SECRET:
|
||
return jsonify({"error": "Forbidden"}), 403
|
||
if not matrix_id:
|
||
return jsonify({"error": "matrix_id_required"}), 400
|
||
player = Player.query.filter_by(matrix_id=matrix_id).first()
|
||
if not player:
|
||
# Create player and set initial stage to awaiting_ready so we can prompt them
|
||
player = Player(
|
||
matrix_id=matrix_id,
|
||
current_step=1,
|
||
stage='awaiting_ready',
|
||
location_hint_index=0,
|
||
question_hint_index=0,
|
||
last_sent_step=None,
|
||
)
|
||
db.session.add(player)
|
||
db.session.commit()
|
||
# Prepare and log the ready prompt so the bot can forward it to the user
|
||
ready_prompt = "Partie initialisée. Es-tu prête à commencer les jeux ?"
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt))
|
||
db.session.commit()
|
||
return jsonify({"status": "created_and_reset", "current_step": player.current_step, "reply": ready_prompt})
|
||
# Efface l’historique messages joueur seulement (pas global)
|
||
MessageLog.query.filter_by(player_id=player.id).delete()
|
||
player.current_step = 1
|
||
# After reset, ask the player if they're ready to start
|
||
player.stage = 'awaiting_ready'
|
||
player.location_hint_index = 0
|
||
player.question_hint_index = 0
|
||
player.last_sent_step = None
|
||
db.session.commit()
|
||
ready_prompt = "Partie réinitialisée. T'es-tu prête à recommencer les jeux ? "
|
||
# Log prompt
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=ready_prompt))
|
||
db.session.commit()
|
||
log.info(f"Player {matrix_id} reset to step 1 and awaiting ready confirmation.")
|
||
# Return the ready prompt to be sent to the player
|
||
return jsonify({"status": "reset_ok", "current_step": player.current_step, "reply": ready_prompt})
|
||
|
||
@app.route("/api/admin/push_message", methods=["POST"])
|
||
def admin_push():
|
||
data = request.json
|
||
matrix_id = data.get("matrix_id")
|
||
text = data.get("text")
|
||
player = Player.query.filter_by(matrix_id=matrix_id).first()
|
||
if not player:
|
||
return jsonify({"error": "player_not_found"}), 404
|
||
db.session.add(MessageLog(player_id=player.id, sender="admin", content=text))
|
||
db.session.commit()
|
||
return jsonify({"status": "ok"})
|
||
|
||
|
||
@app.route("/api/admin/gen_success", methods=["POST"])
|
||
def gen_success():
|
||
"""Generate the AI success text for the player's current step (debug endpoint).
|
||
|
||
POST JSON: { "secret": <shared_secret>, "matrix_id": "@user:server" }
|
||
Returns: { status: ok, reply: <text>, current_step: n }
|
||
"""
|
||
data = request.json or {}
|
||
secret = data.get("secret")
|
||
matrix_id = data.get("matrix_id")
|
||
if secret != FLASK_SHARED_SECRET:
|
||
return jsonify({"error": "Forbidden"}), 403
|
||
if not matrix_id:
|
||
return jsonify({"error": "matrix_id_required"}), 400
|
||
player = Player.query.filter_by(matrix_id=matrix_id).first()
|
||
if not player:
|
||
return jsonify({"error": "player_not_found"}), 404
|
||
step = Step.query.filter_by(step=player.current_step).first()
|
||
if not step:
|
||
return jsonify({"error": "step_not_found"}), 404
|
||
|
||
success = generate_success_text(player, step)
|
||
# Log the generated success text for debugging (do not advance the player)
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=success))
|
||
db.session.commit()
|
||
return jsonify({"status": "ok", "reply": success, "current_step": player.current_step})
|
||
|
||
|
||
@app.route("/api/admin/gen_question", methods=["POST"])
|
||
def gen_question():
|
||
"""Generate the AI question text for the player's current step (debug endpoint)."""
|
||
data = request.json or {}
|
||
secret = data.get("secret")
|
||
matrix_id = data.get("matrix_id")
|
||
if secret != FLASK_SHARED_SECRET:
|
||
return jsonify({"error": "Forbidden"}), 403
|
||
if not matrix_id:
|
||
return jsonify({"error": "matrix_id_required"}), 400
|
||
player = Player.query.filter_by(matrix_id=matrix_id).first()
|
||
if not player:
|
||
return jsonify({"error": "player_not_found"}), 404
|
||
step = Step.query.filter_by(step=player.current_step).first()
|
||
if not step:
|
||
return jsonify({"error": "step_not_found"}), 404
|
||
|
||
qtext = generate_question_text(player, step)
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=qtext))
|
||
db.session.commit()
|
||
return jsonify({"status": "ok", "reply": qtext, "current_step": player.current_step})
|
||
|
||
|
||
@app.route("/api/admin/gen_intro", methods=["POST"])
|
||
def gen_intro():
|
||
"""Generate the AI intro text for the player's current step (debug endpoint)."""
|
||
data = request.json or {}
|
||
secret = data.get("secret")
|
||
matrix_id = data.get("matrix_id")
|
||
if secret != FLASK_SHARED_SECRET:
|
||
return jsonify({"error": "Forbidden"}), 403
|
||
if not matrix_id:
|
||
return jsonify({"error": "matrix_id_required"}), 400
|
||
player = Player.query.filter_by(matrix_id=matrix_id).first()
|
||
if not player:
|
||
return jsonify({"error": "player_not_found"}), 404
|
||
step = Step.query.filter_by(step=player.current_step).first()
|
||
if not step:
|
||
return jsonify({"error": "step_not_found"}), 404
|
||
|
||
intro = generate_step_intro(player, step)
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=intro))
|
||
db.session.commit()
|
||
return jsonify({"status": "ok", "reply": intro, "current_step": player.current_step})
|
||
|
||
|
||
@app.route("/api/admin/gen_hint", methods=["POST"])
|
||
def gen_hint():
|
||
"""Generate a single formatted hint for the player's current step (debug endpoint).
|
||
|
||
POST JSON: { "secret": <shared_secret>, "matrix_id": "@user:server", "hint_kind": "location"|"question", "hint_index": 0, "hint_text": "optional raw hint" }
|
||
If hint_text is provided it is used directly. Otherwise the endpoint picks the hint at hint_index from the requested hint_kind field.
|
||
"""
|
||
data = request.json or {}
|
||
secret = data.get("secret")
|
||
matrix_id = data.get("matrix_id")
|
||
hint_kind = data.get("hint_kind", "location")
|
||
hint_index = int(data.get("hint_index", 0) or 0)
|
||
hint_text_override = data.get("hint_text")
|
||
|
||
if secret != FLASK_SHARED_SECRET:
|
||
return jsonify({"error": "Forbidden"}), 403
|
||
if not matrix_id:
|
||
return jsonify({"error": "matrix_id_required"}), 400
|
||
player = Player.query.filter_by(matrix_id=matrix_id).first()
|
||
if not player:
|
||
return jsonify({"error": "player_not_found"}), 404
|
||
step = Step.query.filter_by(step=player.current_step).first()
|
||
if not step:
|
||
return jsonify({"error": "step_not_found"}), 404
|
||
|
||
if hint_text_override and str(hint_text_override).strip() != "":
|
||
hint_raw = str(hint_text_override).strip()
|
||
else:
|
||
if hint_kind == 'question':
|
||
raw_list = (step.question_hint or "").split(";") if step.question_hint else []
|
||
else:
|
||
raw_list = (step.location_hint or "").split(";") if step.location_hint else []
|
||
if 0 <= hint_index < len(raw_list):
|
||
hint_raw = raw_list[hint_index].strip()
|
||
else:
|
||
# fallback to full hint field
|
||
hint_raw = (step.question_hint if hint_kind == 'question' else step.location_hint) or ""
|
||
|
||
hint_out = generate_formatted_hint(player, step, hint_raw, hint_kind=hint_kind, hint_index=hint_index)
|
||
db.session.add(MessageLog(player_id=player.id, sender="Katniss", content=hint_out))
|
||
db.session.commit()
|
||
return jsonify({"status": "ok", "reply": hint_out, "current_step": player.current_step})
|
||
|
||
# ... Commande init-db inchangée ...
|
||
|
||
|
||
# --------- Init DB ----------
|
||
@app.cli.command("init-db")
|
||
def init_db():
|
||
db.drop_all()
|
||
db.create_all()
|
||
steps = fetch_steps_from_gsheet()
|
||
for step in steps:
|
||
db.session.add(step)
|
||
db.session.commit()
|
||
print(f"{len(steps)} étapes importées avec succès depuis Google Sheet !")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
with app.app_context():
|
||
db.create_all()
|
||
app.run(host="0.0.0.0", port=5000, debug=os.getenv("DEBUG", "false").lower() == "true")
|