File size: 14,834 Bytes
bf22ad3
 
 
79fe228
0356fb2
79fe228
7bc796e
aa815df
0356fb2
79fe228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf22ad3
79fe228
 
 
bf22ad3
 
 
79fe228
 
bf22ad3
79fe228
 
 
bf22ad3
 
 
79fe228
 
 
bf22ad3
79fe228
 
 
bf22ad3
79fe228
 
 
bf22ad3
 
79fe228
 
bf22ad3
1a3554c
7bc796e
 
79fe228
bf22ad3
79fe228
bf22ad3
79fe228
 
bf22ad3
 
79fe228
bf22ad3
79fe228
bf22ad3
 
 
 
79fe228
 
 
0356fb2
bf22ad3
 
79fe228
bf22ad3
 
 
 
 
 
 
 
 
79fe228
 
bf22ad3
 
 
1a3554c
79fe228
 
 
 
 
03755d8
79fe228
 
bf22ad3
 
79fe228
 
bf22ad3
 
 
79fe228
 
bf22ad3
79fe228
bf22ad3
79fe228
bf22ad3
0356fb2
bf22ad3
79fe228
bf22ad3
 
79fe228
 
bf22ad3
79fe228
 
bf22ad3
 
 
79fe228
bf22ad3
79fe228
bf22ad3
 
79fe228
bf22ad3
 
 
79fe228
 
bf22ad3
79fe228
 
 
 
 
 
 
 
 
 
 
 
 
 
56eb498
79fe228
 
bf22ad3
79fe228
 
bf22ad3
 
 
79fe228
 
 
 
 
 
 
 
 
 
bf22ad3
79fe228
bf22ad3
 
79fe228
bf22ad3
 
 
 
 
 
 
 
79fe228
bf22ad3
79fe228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bf22ad3
 
 
79fe228
bf22ad3
79fe228
56eb498
bf22ad3
 
79fe228
 
 
56eb498
79fe228
bf22ad3
79fe228
bf22ad3
79fe228
 
bf22ad3
 
 
 
 
79fe228
 
 
 
 
 
 
 
 
 
bf22ad3
79fe228
bf22ad3
79fe228
 
bf22ad3
79fe228
56eb498
79fe228
bf22ad3
79fe228
bf22ad3
 
 
79fe228
 
 
56eb498
79fe228
 
 
0efcaab
 
79fe228
 
 
 
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
from flask import Flask, render_template, request, jsonify, session
import chess
import chess.svg
from stockfish import Stockfish, StockfishException # Assurez-vous que ce module est accessible
import os
import logging

app = Flask(__name__)

# --- Configuration de la clé secrète ---
# IMPORTANT: Pour la production, utilisez une clé secrète forte et ne la codez pas en dur.
# Chargez-la depuis une variable d'environnement ou un fichier de configuration.
# Pour le DÉVELOPPEMENT UNIQUEMENT, une clé statique aide à éviter la perte de session due aux rechargements.
if app.debug:
    app.secret_key = 'dev_secret_key_pour_eviter_perte_session_rechargement'
    logging.warning("UTILISATION D'UNE CLÉ SECRÈTE STATIQUE POUR LE DÉVELOPPEMENT. NE PAS UTILISER EN PRODUCTION.")
else:
    app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(24))
    if app.secret_key == os.urandom(24): # Check si on a dû fallback sur urandom
        logging.warning("FLASK_SECRET_KEY non définie. Utilisation d'une clé générée aléatoirement. "
                        "Les sessions ne persisteront pas entre les redémarrages du serveur de production.")


# --- Configuration du logging ---
logging.basicConfig(level=logging.DEBUG)
# Vous pouvez ajuster le niveau de logging pour la production, ex: logging.INFO
# werkzeug_logger = logging.getLogger('werkzeug')
# werkzeug_logger.setLevel(logging.INFO) # Pour réduire le bruit des requêtes GET/POST en console


# --- Configuration de Stockfish ---
STOCKFISH_PATH = "stockfish" # Ou le chemin complet, ex: "/usr/games/stockfish"
if not os.path.exists(STOCKFISH_PATH) or not os.access(STOCKFISH_PATH, os.X_OK):
    common_paths = ["/usr/games/stockfish", "/usr/local/bin/stockfish", "/opt/homebrew/bin/stockfish", "stockfish.exe"]
    found_stockfish = False
    for p in common_paths:
        if os.path.exists(p) and os.access(p, os.X_OK):
            STOCKFISH_PATH = p
            found_stockfish = True
            app.logger.info(f"Stockfish trouvé à: {STOCKFISH_PATH}")
            break
    if not found_stockfish:
        app.logger.warning(f"AVERTISSEMENT: Stockfish non trouvé à '{STOCKFISH_PATH}' ou dans les chemins communs. "
                           "L'IA ne fonctionnera pas. Veuillez installer Stockfish et/ou configurer STOCKFISH_PATH.")

def get_stockfish_engine():
    """Initialise et retourne une instance de Stockfish."""
    if not os.path.exists(STOCKFISH_PATH) or not os.access(STOCKFISH_PATH, os.X_OK):
        app.logger.error(f"Stockfish non exécutable à {STOCKFISH_PATH}. L'IA ne peut pas démarrer.")
        return None
    try:
        engine = Stockfish(path=STOCKFISH_PATH, depth=15, parameters={"Threads": 1, "Hash": 64}) # Ajustez si besoin
        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.get_best_move() # Test rapide
        engine.set_fen_position(chess.STARTING_FEN)
        app.logger.info("Moteur Stockfish initialisé avec succès.")
        return engine
    except Exception as e:
        app.logger.error(f"Erreur lors de l'initialisation de Stockfish: {e}")
        app.logger.error(f"Vérifiez que Stockfish est installé et que STOCKFISH_PATH ('{STOCKFISH_PATH}') est correct.")
        return None

@app.route('/')
def index():
    app.logger.debug(f"Session au début de GET /: {dict(session.items())}")
    if 'board_fen' 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), # Renommé pour clarifier
                           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 = "Blancs" if board.turn == chess.BLACK else "Noirs" # Celui dont ce n'est PAS le tour gagne
        return f"Échec et mat ! {winner} 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_variant_draw() or board.is_variant_loss() or board.is_variant_win(): # Autres types de fins
        return "Partie terminée (variante)."
    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!")
        # Optionnel: essayer de réinitialiser ou informer le client
        # session['board_fen'] = chess.Board().fen() # Réinitialisation d'urgence
        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 = request.json.get('move')
    if not move_uci:
        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

    try:
        move = board.parse_uci(move_uci)
        app.logger.info(f"Mouvement joueur (UCI parsed): {move.uci()}")
    except ValueError:
        try:
            move = board.parse_san(move_uci) # Essayer SAN
            app.logger.info(f"Mouvement joueur (SAN parsed): {board.san(move)}")
        except ValueError:
            app.logger.warning(f"Mouvement invalide (ni UCI ni SAN): {move_uci}")
            return jsonify({'error': f'Mouvement invalide: {move_uci}', 'fen': board.fen(), 'game_over': board.is_game_over(), 'board_svg': chess.svg.board(board=board, size=400)})

    if move in board.legal_moves:
        board.push(move)
        last_player_move = move # Sauvegarder le dernier coup du joueur pour le SVG
        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("Mode IA, tour de l'IA.")
            stockfish_engine = get_stockfish_engine()
            if stockfish_engine:
                stockfish_engine.set_fen_position(board.fen())
                # best_move_ai = stockfish_engine.get_best_move_time(500) # 0.5 seconde pour plus de réactivité
                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
                        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}")
                else:
                    app.logger.warning("L'IA (Stockfish) n'a pas retourné de coup (peut-être mat/pat pour l'IA).")
                stockfish_engine.send_quit_command()
                app.logger.info("Moteur Stockfish arrêté après le coup de l'IA.")
            else:
                 app.logger.error("Moteur Stockfish non disponible pour le coup de l'IA.")
                 return jsonify({'error': 'Moteur Stockfish non disponible.', 'fen': board.fen(), 'game_over': board.is_game_over(), 'board_svg': chess.svg.board(board=board, size=400)})
    else:
        app.logger.warning(f"Mouvement illégal tenté: {move.uci()} depuis FEN: {session['board_fen']}")
        return jsonify({'error': f'Mouvement illégal: {move.uci()}', 'fen': board.fen(), 'game_over': board.is_game_over(), 'board_svg': chess.svg.board(board=board, size=400)})

    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}")
    
    # Déterminer le dernier coup à surligner
    move_to_highlight = None
    if ai_move_made:
        move_to_highlight = ai_move_obj
    elif last_player_move:
        move_to_highlight = last_player_move


    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()
    # Conserver le mode de jeu et la couleur du joueur actuels, ou les réinitialiser comme souhaité
    # session['game_mode'] = 'pvp' # Optionnel: forcer un mode par défaut
    # session['player_color'] = 'white' # Optionnel
    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', 'pvp'), # Fournir une valeur par défaut si non défini
        'player_color': session.get('player_color', 'white'),
        '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}. Couleur joueur (si IA): {player_color}. FEN réinitialisé: {session['board_fen']}")
        
        board = chess.Board() # Nouvelle instance pour la réponse initiale
        initial_ai_move_uci = None
        move_to_highlight_init = None

        if mode == 'ai' and player_color == 'black': # L'IA (Blanc) joue en premier
            app.logger.info("L'IA (Blanc) 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() # Mettre à jour le FEN de la session
                        initial_ai_move_uci = best_move_ai
                        move_to_highlight_init = ai_move_obj
                        app.logger.info(f"Premier coup de l'IA (Blanc): {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}")

                stockfish_engine.send_quit_command()
                app.logger.info("Moteur Stockfish arrêté après le premier coup de l'IA.")
            else:
                app.logger.error("Moteur Stockfish non disponible pour le premier coup de l'IA.")
                return jsonify({'error': 'Moteur Stockfish non disponible.'}), 500

        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()}. {"Joueur: " + 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__':
    # Pour la production, utilisez un serveur WSGI comme Gunicorn ou uWSGI.
    # Par exemple: gunicorn -w 4 -b 0.0.0.0:5000 app:app
    # Le port 7860 est souvent utilisé par Gradio, assurez-vous qu'il n'y a pas de conflit.
    app.run(debug=True, host='0.0.0.0', port=5000) # Utilisez un port standard comme 5000