darkmaria / app.py
Docfile's picture
Update app.py
166ccea verified
raw
history blame
19.6 kB
from flask import Flask, render_template, request, jsonify, session
# -- Modification pour vérifier l'import --
import sys
import os
import inspect # Ajout pour inspecter
# --- Forcer l'utilisation de la bibliothèque locale si elle est à côté ---
# Supposons la structure:
# votre_projet/
# ├── app.py
# └── zhelyabuzhsky-stockfish/ <-- le répertoire complet de la bibliothèque
# ├── stockfish/
# │ ├── __init__.py
# │ └── models.py
# └── ... autres fichiers de la lib (setup.py, etc.)
# Pour que "from stockfish import Stockfish" fonctionne avec cette structure,
# le répertoire "zhelyabuzhsky-stockfish" doit être traitable comme un package.
# Python recherche d'abord dans le répertoire courant, puis dans PYTHONPATH, puis dans les sites-packages.
# Si vous avez une autre lib "stockfish" installée globalement, elle pourrait prendre le dessus.
# On peut ajouter le répertoire parent de zhelyabuzhsky-stockfish/stockfish au path,
# ou directement le chemin vers zhelyabuzhsky-stockfish si on l'importe comme un package.
# Option 1: Ajouter le répertoire parent de votre projet au sys.path
# pour que zhelyabuzhsky-stockfish soit vu comme un package de haut niveau.
# current_script_dir = os.path.dirname(os.path.abspath(__file__))
# project_root = os.path.dirname(current_script_dir) # Si app.py est dans un sous-dossier
# if project_root not in sys.path:
# sys.path.insert(0, project_root)
# Option plus simple si zhelyabuzhsky-stockfish est à côté de app.py
# et que vous voulez importer "from stockfish import ..."
# Cela suppose que Python va scanner "zhelyabuzhsky-stockfish" et trouver le sous-module "stockfish".
# Le plus sûr est d'installer votre bibliothèque localement.
# `pip install -e ./zhelyabuzhsky-stockfish` depuis le répertoire parent.
from stockfish import Stockfish, StockfishException # L'import
print(f"--- Stockfish class imported from: {inspect.getfile(Stockfish)} ---") # VÉRIFICATION
# --- Fin de la modification pour vérifier l'import --
import chess
import chess.svg
import logging
app = Flask(__name__)
# --- Configuration de la clé secrète ---
# FORCER LE MODE DEBUG POUR LE DÉVELOPPEMENT LOCAL
IS_DEBUG_MODE = True # Modifier cette ligne manuellement pour tester en mode prod-like si besoin
# OU utiliser la variable d'environnement:
# IS_DEBUG_MODE = os.environ.get("FLASK_DEBUG", "0") == "1"
if IS_DEBUG_MODE:
app.secret_key = 'dev_secret_key_pour_eviter_perte_session_rechargement_et_tests_v2'
logging.warning("MODE DEBUG ACTIVÉ. Utilisation d'une clé secrète statique.")
app.debug = True # S'assurer que Flask est aussi en mode debug
else:
app.secret_key = os.environ.get("FLASK_SECRET_KEY")
if not app.secret_key:
app.secret_key = os.urandom(32)
logging.warning("MODE PRODUCTION. FLASK_SECRET_KEY non définie. Clé aléatoire générée.")
app.debug = False
# --- Configuration des Cookies de Session ---
app.config.update(
SESSION_COOKIE_SECURE=False,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax',
)
logging.basicConfig(level=logging.DEBUG)
# ... (le reste du code de app.py reste identique à la version précédente)
# ... (find_stockfish, get_stockfish_engine, routes, etc.)
# --- Assurez-vous que le find_stockfish et HAS_STOCKFISH sont définis globalement ---
STOCKFISH_PATH_GLOBAL = "stockfish" # Variable globale pour le chemin
HAS_STOCKFISH_GLOBAL = False
def find_stockfish_on_startup():
global STOCKFISH_PATH_GLOBAL, HAS_STOCKFISH_GLOBAL
# Tenter le chemin par défaut ou celui de l'env
stockfish_env_path = os.environ.get("STOCKFISH_EXECUTABLE_PATH", "stockfish")
paths_to_check = [stockfish_env_path, STOCKFISH_PATH_GLOBAL, "/usr/games/stockfish", "/usr/local/bin/stockfish", "/opt/homebrew/bin/stockfish", "stockfish.exe", "./stockfish"]
for p in paths_to_check:
if p and os.path.exists(p) and os.access(p, os.X_OK):
STOCKFISH_PATH_GLOBAL = p
HAS_STOCKFISH_GLOBAL = True
app.logger.info(f"Stockfish exécutable trouvé à: {STOCKFISH_PATH_GLOBAL}")
return
app.logger.warning(f"AVERTISSEMENT: Stockfish non trouvé. L'IA ne fonctionnera pas.")
HAS_STOCKFISH_GLOBAL = False
find_stockfish_on_startup() # Exécuter au démarrage
def get_stockfish_engine():
"""Initialise et retourne une instance de Stockfish."""
if not HAS_STOCKFISH_GLOBAL:
app.logger.error("Stockfish non disponible (vérification au démarrage échouée). L'IA ne peut pas démarrer.")
return None
try:
# Utiliser STOCKFISH_PATH_GLOBAL qui a été déterminé au démarrage
engine = Stockfish(path=STOCKFISH_PATH_GLOBAL, depth=15, parameters={"Threads": 1, "Hash": 64})
if engine._stockfish.poll() is not None:
app.logger.error("Le processus Stockfish s'est terminé de manière inattendue lors de l'initialisation.")
raise StockfishException("Stockfish process terminated unexpectedly on init.")
engine.set_fen_position(chess.STARTING_FEN)
engine.get_best_move()
engine.set_fen_position(chess.STARTING_FEN)
app.logger.info(f"Moteur Stockfish initialisé avec succès depuis {STOCKFISH_PATH_GLOBAL}.")
return engine
except Exception as e:
app.logger.error(f"Erreur lors de l'initialisation de Stockfish ({STOCKFISH_PATH_GLOBAL}): {e}", exc_info=True)
return None
# --- Collez ici le reste de vos routes : index, get_outcome_message, make_move, reset_game, set_mode_route ---
# (Pour éviter de répéter 500 lignes, je ne les remets pas, mais elles restent inchangées par rapport à votre dernière version
# SAUF si le AttributeError persiste, auquel cas il faudra vérifier la version de la lib Stockfish)
# ... (Collez ici le reste des routes : index, get_outcome_message, make_move, reset_game, set_mode_route)
# ... (depuis la version précédente que je vous ai donnée)
# Par exemple, la route make_move commencerait comme ça:
@app.route('/make_move', methods=['POST'])
def make_move():
app.logger.debug(f"Session au début de POST /make_move: {dict(session.items())}")
if 'board_fen' not in session:
app.logger.error("ERREUR CRITIQUE: 'board_fen' non trouvé dans la session pour POST /make_move!")
return jsonify({'error': 'Erreur de session, "board_fen" manquant. Veuillez rafraîchir ou réinitialiser.',
'fen': chess.Board().fen(), 'game_over': False, 'board_svg': chess.svg.board(board=chess.Board(), size=400)}), 500
# ... et ainsi de suite pour toute la fonction make_move et les autres routes.
# (Le code des routes est omis pour la brièveté, mais doit être inclus)
@app.route('/')
def index():
app.logger.debug(f"Session au début de GET /: {dict(session.items())}")
if 'board_fen' not in session or 'game_mode' not in session:
app.logger.info("Initialisation de la session (board_fen, game_mode, player_color) dans GET /")
session['board_fen'] = chess.Board().fen()
session['game_mode'] = 'pvp'
session['player_color'] = 'white' # Couleur du joueur humain si mode AI
board = chess.Board(session['board_fen'])
app.logger.debug(f"Session à la fin de GET /: {dict(session.items())}")
return render_template('index.html',
initial_board_svg=chess.svg.board(board=board, size=400),
initial_fen=session['board_fen'],
game_mode=session['game_mode'],
player_color=session.get('player_color', 'white'),
is_game_over=board.is_game_over(),
outcome=get_outcome_message(board) if board.is_game_over() else "",
current_turn = 'white' if board.turn == chess.WHITE else 'black')
def get_outcome_message(board):
if board.is_checkmate():
winner_color = "Blancs" if board.turn == chess.BLACK else "Noirs"
return f"Échec et mat ! {winner_color} gagnent."
if board.is_stalemate(): return "Pat ! Partie nulle."
if board.is_insufficient_material(): return "Matériel insuffisant. Partie nulle."
if board.is_seventyfive_moves(): return "Règle des 75 coups. Partie nulle."
if board.is_fivefold_repetition(): return "Répétition (5 fois). Partie nulle."
if board.is_game_over(): return "Partie terminée."
return ""
@app.route('/make_move', methods=['POST'])
def make_move_impl(): # Renommé pour éviter conflit de nom si vous avez collé
app.logger.debug(f"Session au début de POST /make_move: {dict(session.items())}")
if 'board_fen' not in session:
app.logger.error("ERREUR CRITIQUE: 'board_fen' non trouvé dans la session pour POST /make_move!")
return jsonify({'error': 'Erreur de session, "board_fen" manquant. Veuillez rafraîchir ou réinitialiser.',
'fen': chess.Board().fen(), 'game_over': False, 'board_svg': chess.svg.board(board=chess.Board(), size=400)}), 500
board = chess.Board(session['board_fen'])
if board.is_game_over():
app.logger.info("Tentative de jouer alors que la partie est terminée.")
return jsonify({'error': 'La partie est terminée.', 'fen': board.fen(), 'game_over': True, 'outcome': get_outcome_message(board), 'board_svg': chess.svg.board(board=board, size=400)})
move_uci_san = request.json.get('move')
if not move_uci_san:
app.logger.warning("Aucun mouvement fourni dans la requête POST /make_move.")
return jsonify({'error': 'Mouvement non fourni.', 'fen': board.fen(), 'game_over': board.is_game_over(), 'board_svg': chess.svg.board(board=board, size=400)})
ai_move_made = False
ai_move_uci = None
last_player_move = None
move_to_highlight = None
move = None # initialisation
try:
move = board.parse_uci(move_uci_san)
app.logger.info(f"Mouvement joueur (tentative UCI): {move.uci()}")
except ValueError:
try:
move = board.parse_san(move_uci_san)
app.logger.info(f"Mouvement joueur (tentative SAN): {board.san(move)}")
except ValueError:
app.logger.warning(f"Mouvement invalide (ni UCI ni SAN): {move_uci_san} pour FEN {board.fen()}")
return jsonify({'error': f'Mouvement invalide: {move_uci_san}', 'fen': board.fen(), 'game_over': board.is_game_over(), 'board_svg': chess.svg.board(board=board, size=400)})
if move and move in board.legal_moves: # Vérifier que 'move' est non None
board.push(move)
last_player_move = move
move_to_highlight = move
session['board_fen'] = board.fen()
app.logger.info(f"Mouvement joueur {move.uci()} appliqué. Nouveau FEN: {board.fen()}")
if session.get('game_mode') == 'ai' and not board.is_game_over():
app.logger.info(f"Mode IA, tour de l'IA. Joueur humain est {session.get('player_color')}.")
current_turn_is_ai_turn = (session.get('player_color') == 'white' and board.turn == chess.BLACK) or \
(session.get('player_color') == 'black' and board.turn == chess.WHITE)
if not current_turn_is_ai_turn:
app.logger.error(f"Logique d'alternance erronée: C'est le tour de {'Blanc' if board.turn == chess.WHITE else 'Noir'}, mais l'IA ne devrait pas jouer.")
else:
stockfish_engine = get_stockfish_engine()
if stockfish_engine:
stockfish_engine.set_fen_position(board.fen())
best_move_ai = stockfish_engine.get_best_move()
if best_move_ai:
app.logger.info(f"Stockfish propose le coup: {best_move_ai}")
try:
ai_move_obj = board.parse_uci(best_move_ai)
board.push(ai_move_obj)
session['board_fen'] = board.fen()
ai_move_made = True
ai_move_uci = best_move_ai
move_to_highlight = ai_move_obj
app.logger.info(f"Mouvement IA {ai_move_uci} appliqué. Nouveau FEN: {board.fen()}")
except Exception as e:
app.logger.error(f"Erreur en appliquant le coup de l'IA {best_move_ai}: {e}", exc_info=True)
else:
app.logger.warning("L'IA (Stockfish) n'a pas retourné de coup.")
# Vérifier si la méthode existe avant de l'appeler
if hasattr(stockfish_engine, 'send_quit_command'):
stockfish_engine.send_quit_command()
app.logger.info("Moteur Stockfish arrêté après le coup de l'IA.")
elif hasattr(stockfish_engine, '__del__'):
# Si send_quit_command n'existe pas, on espère que __del__ fait le travail.
# Ou on pourrait juste faire `del stockfish_engine` pour déclencher __del__
app.logger.warning("send_quit_command() n'existe pas, __del__ sera utilisé implicitement.")
else:
app.logger.error("Ni send_quit_command ni __del__ ne semblent disponibles pour arrêter Stockfish proprement.")
else:
app.logger.error("Moteur Stockfish non disponible pour le coup de l'IA.")
elif move: # 'move' est défini mais pas légal
app.logger.warning(f"Mouvement illégal tenté: {move.uci() if hasattr(move, 'uci') else move_uci_san} depuis FEN: {session['board_fen']}")
legal_moves_str = ", ".join([board.san(m) for m in board.legal_moves])
app.logger.debug(f"Coups légaux: {legal_moves_str[:200]}")
return jsonify({'error': f'Mouvement illégal: {move.uci() if hasattr(move, "uci") else move_uci_san}', 'fen': board.fen(), 'game_over': board.is_game_over(), 'board_svg': chess.svg.board(board=board, size=400)})
else: # 'move' est None, n'a pas pu être parsé
# L'erreur a déjà été gérée par le bloc try/except du parsing
# Ce bloc else ne devrait pas être atteint si le parsing échoue et retourne.
app.logger.error("La variable 'move' est None après la tentative de parsing, ce qui ne devrait pas arriver ici.")
return jsonify({'error': 'Erreur interne de parsing de coup.', 'fen': board.fen(), 'game_over': board.is_game_over(), 'board_svg': chess.svg.board(board=board, size=400)}), 500
game_over = board.is_game_over()
outcome = get_outcome_message(board) if game_over else ""
if game_over:
app.logger.info(f"Partie terminée. Résultat: {outcome}")
app.logger.debug(f"Session à la fin de POST /make_move: {dict(session.items())}")
return jsonify({
'fen': board.fen(),
'board_svg': chess.svg.board(board=board, size=400, lastmove=move_to_highlight),
'game_over': game_over,
'outcome': outcome,
'ai_move_uci': ai_move_uci,
'turn': 'white' if board.turn == chess.WHITE else 'black'
})
@app.route('/reset_game', methods=['POST'])
def reset_game():
app.logger.debug(f"Session au début de POST /reset_game: {dict(session.items())}")
session['board_fen'] = chess.Board().fen()
if 'game_mode' not in session: session['game_mode'] = 'pvp'
if 'player_color' not in session: session['player_color'] = 'white'
app.logger.info(f"Jeu réinitialisé. Nouveau FEN: {session['board_fen']}. Mode: {session.get('game_mode')}, Couleur joueur: {session.get('player_color')}")
board = chess.Board(session['board_fen'])
app.logger.debug(f"Session à la fin de POST /reset_game: {dict(session.items())}")
return jsonify({
'fen': session['board_fen'],
'board_svg': chess.svg.board(board=board, size=400),
'game_mode': session.get('game_mode'),
'player_color': session.get('player_color'),
'turn': 'white' if board.turn == chess.WHITE else 'black',
'game_over': False,
'outcome': ""
})
@app.route('/set_mode', methods=['POST'])
def set_mode_route():
app.logger.debug(f"Session au début de POST /set_mode: {dict(session.items())}")
mode = request.json.get('game_mode')
player_color = request.json.get('player_color', 'white')
if mode in ['pvp', 'ai']:
session['game_mode'] = mode
session['player_color'] = player_color
session['board_fen'] = chess.Board().fen()
app.logger.info(f"Mode de jeu réglé sur {mode}. Joueur humain: {player_color if mode == 'ai' else 'N/A'}. FEN réinitialisé.")
board = chess.Board()
initial_ai_move_uci = None
move_to_highlight_init = None
if mode == 'ai' and player_color == 'black':
app.logger.info("L'IA (Blancs) commence la partie.")
stockfish_engine = get_stockfish_engine()
if stockfish_engine:
stockfish_engine.set_fen_position(board.fen())
best_move_ai = stockfish_engine.get_best_move()
if best_move_ai:
try:
ai_move_obj = board.parse_uci(best_move_ai)
board.push(ai_move_obj)
session['board_fen'] = board.fen()
initial_ai_move_uci = best_move_ai
move_to_highlight_init = ai_move_obj
app.logger.info(f"Premier coup de l'IA (Blancs): {initial_ai_move_uci}. Nouveau FEN: {board.fen()}")
except Exception as e:
app.logger.error(f"Erreur en appliquant le premier coup de l'IA {best_move_ai}: {e}", exc_info=True)
if hasattr(stockfish_engine, 'send_quit_command'):
stockfish_engine.send_quit_command()
app.logger.info("Moteur Stockfish arrêté après le premier coup de l'IA.")
elif hasattr(stockfish_engine, '__del__'):
app.logger.warning("send_quit_command() n'existe pas, __del__ sera utilisé implicitement pour le premier coup IA.")
else:
app.logger.error("Ni send_quit_command ni __del__ pour arrêter Stockfish (premier coup IA).")
else:
app.logger.error("Moteur Stockfish non disponible pour le premier coup de l'IA.")
app.logger.debug(f"Session à la fin de POST /set_mode: {dict(session.items())}")
return jsonify({
'message': f'Mode de jeu réglé sur {mode.upper()}. {"Vous jouez " + player_color.capitalize() if mode == "ai" else ""}',
'fen': session['board_fen'],
'board_svg': chess.svg.board(board=board, size=400, lastmove=move_to_highlight_init),
'game_mode': mode,
'player_color': player_color,
'turn': 'white' if board.turn == chess.WHITE else 'black',
'initial_ai_move_uci': initial_ai_move_uci,
'game_over': board.is_game_over(),
'outcome': get_outcome_message(board) if board.is_game_over() else ""
})
else:
app.logger.warning(f"Tentative de définir un mode de jeu invalide: {mode}")
return jsonify({'error': 'Mode invalide'}), 400
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000) # IS_DEBUG_MODE et app.debug sont gérés plus haut