File size: 17,810 Bytes
bf22ad3
166ccea
 
 
 
b293718
 
 
 
 
 
 
 
 
 
 
166ccea
bf22ad3
 
79fe228
7bc796e
aa815df
0356fb2
79fe228
b293718
 
e17de57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ded7221
 
b293718
166ccea
b293718
79fe228
ded7221
 
166ccea
 
 
 
ded7221
 
166ccea
 
 
ded7221
 
79fe228
166ccea
b293718
 
 
166ccea
 
b293718
 
 
 
 
 
 
 
 
 
 
ded7221
166ccea
 
b293718
 
 
166ccea
b293718
 
 
 
 
 
ded7221
b293718
bf22ad3
 
b293718
 
 
79fe228
bf22ad3
e17de57
 
 
 
79fe228
 
bf22ad3
b293718
166ccea
b293718
 
bf22ad3
 
b293718
bf22ad3
1a3554c
b293718
166ccea
7bc796e
 
79fe228
ded7221
79fe228
bf22ad3
79fe228
b293718
bf22ad3
 
79fe228
bf22ad3
ded7221
bf22ad3
 
 
 
79fe228
 
 
bf22ad3
 
ded7221
 
 
 
 
 
166ccea
bf22ad3
 
 
b293718
79fe228
 
 
ded7221
79fe228
 
bf22ad3
 
79fe228
 
bf22ad3
ded7221
 
79fe228
 
bf22ad3
79fe228
bf22ad3
ded7221
b293718
bf22ad3
0356fb2
ded7221
 
bf22ad3
 
ded7221
 
bf22ad3
ded7221
 
bf22ad3
b293718
bf22ad3
166ccea
bf22ad3
79fe228
bf22ad3
 
ded7221
 
 
 
 
166ccea
56eb498
ded7221
 
 
e17de57
 
ded7221
 
 
 
 
 
 
 
166ccea
ded7221
 
 
 
 
166ccea
b293718
 
 
 
 
 
 
 
 
 
166ccea
ded7221
 
b293718
ded7221
 
b293718
166ccea
 
 
bf22ad3
 
79fe228
 
 
 
bf22ad3
 
79fe228
bf22ad3
 
 
 
 
 
 
 
79fe228
bf22ad3
ded7221
 
 
79fe228
 
 
 
 
 
 
ded7221
 
79fe228
 
 
 
bf22ad3
 
 
79fe228
bf22ad3
166ccea
56eb498
bf22ad3
 
79fe228
 
ded7221
56eb498
ded7221
bf22ad3
79fe228
bf22ad3
ded7221
 
bf22ad3
 
 
 
 
79fe228
 
 
166ccea
79fe228
 
ded7221
79fe228
ded7221
166ccea
b293718
 
 
 
 
 
 
 
 
 
 
bf22ad3
79fe228
bf22ad3
79fe228
56eb498
ded7221
bf22ad3
79fe228
bf22ad3
 
 
79fe228
166ccea
79fe228
56eb498
79fe228
 
 
0efcaab
 
166ccea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
from flask import Flask, render_template, request, jsonify, session
import sys
import os
import inspect # Ajout pour inspecter

# --- VÉRIFICATION DE L'IMPORT DE STOCKFISH (Doit être au tout début) ---
# Cet import doit être celui de la bibliothèque zhelyabuzhsky-stockfish
try:
    from stockfish import Stockfish, StockfishException
    STOCKFISH_LIB_PATH = inspect.getfile(Stockfish)
    print(f"--- Stockfish class imported from: {STOCKFISH_LIB_PATH} ---")
except ImportError as e:
    print(f"ERREUR CRITIQUE: Impossible d'importer la bibliothèque Stockfish: {e}")
    print("Assurez-vous que la bibliothèque zhelyabuzhsky-stockfish est dans votre PYTHONPATH ou installée.")
    sys.exit(1) # Arrêter si la lib principale manque
# --- Fin de la vérification ---

import chess
import chess.svg
import logging

app = Flask(__name__)

# --- Configuration de la clé secrète ---
IS_DEBUG_MODE = True # Forcer le mode debug pour le développement local
# Pour la production, gérez FLASK_DEBUG et FLASK_SECRET_KEY via les variables d'environnement
default_depth_for_max_strength = 22 

        max_strength_params = {
            "Debug Log File": "",
            "Contempt": 0,
            "Min Split Depth": 0,
            "Threads": 4,
            "Ponder": "false",
            "Hash": 8192,
            "MultiPV": 1,
            "Skill Level": 20,
            "Move Overhead": 30,
            "Minimum Thinking Time": 20,
            "Slow Mover": 100,
            "UCI_Chess960": "false",
            "UCI_LimitStrength": "false",
            "UCI_Elo": 3500 
        }

if IS_DEBUG_MODE:
    app.secret_key = 'dev_secret_key_pour_eviter_perte_session_rechargement_et_tests_v3' # Changez si besoin
    logging.warning("MODE DEBUG ACTIVÉ. Utilisation d'une clé secrète statique.")
    app.debug = True
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)

# --- Configuration Globale de Stockfish (Exécutable) ---
STOCKFISH_EXECUTABLE_PATH_GLOBAL = "stockfish" # Valeur par défaut
HAS_STOCKFISH_EXECUTABLE_GLOBAL = False

def find_stockfish_on_startup():
    global STOCKFISH_EXECUTABLE_PATH_GLOBAL, HAS_STOCKFISH_EXECUTABLE_GLOBAL
    
    # Priorité à une variable d'environnement si définie
    env_path = os.environ.get("STOCKFISH_EXECUTABLE_PATH")
    paths_to_check = [env_path] if env_path else []
    
    paths_to_check.extend([
        "stockfish", "./stockfish", # Relatif au répertoire de travail
        "/usr/games/stockfish", "/usr/local/bin/stockfish", 
        "/opt/homebrew/bin/stockfish", "stockfish.exe" # Chemins communs
    ])
    
    for p in paths_to_check:
        if p and os.path.exists(p) and os.access(p, os.X_OK):
            STOCKFISH_EXECUTABLE_PATH_GLOBAL = p
            HAS_STOCKFISH_EXECUTABLE_GLOBAL = True
            app.logger.info(f"Stockfish exécutable trouvé à: {STOCKFISH_EXECUTABLE_PATH_GLOBAL}")
            return
            
    app.logger.warning(
        "AVERTISSEMENT: Exécutable Stockfish non trouvé dans les chemins courants ou via STOCKFISH_EXECUTABLE_PATH. "
        "L'IA ne fonctionnera pas. Veuillez installer Stockfish ou définir la variable d'environnement."
    )
    HAS_STOCKFISH_EXECUTABLE_GLOBAL = False

find_stockfish_on_startup() # Exécuter au démarrage de l'application

def get_stockfish_engine():
    """Initialise et retourne une instance de la classe Stockfish."""
    if not HAS_STOCKFISH_EXECUTABLE_GLOBAL:
        app.logger.error("Exécutable Stockfish non disponible. L'IA ne peut pas démarrer.")
        return None
    try:
        #engine = Stockfish(path=STOCKFISH_EXECUTABLE_PATH_GLOBAL, depth=15, parameters={"Threads": 1, "Hash": 64})
        engine = Stockfish(path=STOCKFISH_EXECUTABLE_PATH_GLOBAL, 
                           depth=default_depth_for_max_strength, 
                           parameters=max_strength_params)
        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) # Test léger
        engine.get_best_move() 
        engine.set_fen_position(chess.STARTING_FEN) # Reset après test
        app.logger.info(f"Moteur Stockfish (classe) initialisé avec succès utilisant l'exécutable: {STOCKFISH_EXECUTABLE_PATH_GLOBAL}.")
        return engine
    except Exception as e:
        app.logger.error(f"Erreur lors de l'initialisation de la classe Stockfish (exécutable: {STOCKFISH_EXECUTABLE_PATH_GLOBAL}): {e}", exc_info=True)
        return None

# --- Définition des Routes ---

@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'

    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():
    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
    move_to_highlight = None
    move = None 

    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:
        board.push(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())
                    thinking_time_ms = 20000
                    best_move_ai = stockfish_engine.get_best_move(thinking_time_ms)
                    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.")
                    
                    # APPEL À send_quit_command() COMMENTÉ COMME DEMANDÉ
                    # 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__') and callable(getattr(stockfish_engine, '__del__')):
                    #     app.logger.warning("send_quit_command() n'existe pas, __del__ sera potentiellement utilisé.")
                    # else:
                    #     app.logger.error("Ni send_quit_command ni __del__ ne semblent disponibles pour arrêter Stockfish proprement.")
                    app.logger.info("Appel explicite à send_quit_command retiré (après coup IA). Le processus Stockfish peut rester actif.")
                    # del stockfish_engine # Optionnel: pour essayer de forcer __del__ si la lib le gère bien

                else:
                    app.logger.error("Moteur Stockfish non disponible pour le coup de l'IA.")
    elif move: 
        app.logger.warning(f"Mouvement illégal tenté: {move.uci() if hasattr(move, 'uci') else move_uci_san} depuis FEN: {session['board_fen']}")
        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: 
        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)
                
                # APPEL À send_quit_command() COMMENTÉ COMME DEMANDÉ
                # 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__') and callable(getattr(stockfish_engine, '__del__')):
                #     app.logger.warning("send_quit_command() n'existe pas, __del__ sera potentiellement utilisé.")
                # else:
                #     app.logger.error("Ni send_quit_command ni __del__ pour arrêter Stockfish (premier coup IA).")
                app.logger.info("Appel explicite à send_quit_command retiré (après premier coup IA). Le processus Stockfish peut rester actif.")
                # del stockfish_engine # Optionnel

            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