-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.py
More file actions
259 lines (220 loc) · 9.81 KB
/
app.py
File metadata and controls
259 lines (220 loc) · 9.81 KB
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
"""
Flask Web Application for Chess Cheat Detection System
This application provides a web interface for uploading PGN files and
displaying comprehensive chess analysis results including cheat detection metrics.
"""
import os
import logging
from flask import Flask, request, render_template, jsonify, send_from_directory
from werkzeug.utils import secure_filename
import json
import traceback
from datetime import datetime
import numpy as np
import chess
import chess.engine
import chess.pgn
import io
from pathlib import Path
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Create Flask app
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev-secret-key-change-in-production'
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
# Allowed file extensions
ALLOWED_EXTENSIONS = {'pgn', 'txt'}
def get_stockfish_path():
"""Get Stockfish executable path based on platform."""
possible_paths = [
Path("stockfish.exe"),
Path("stockfish"),
Path.home() / "OneDrive" / "Documents" / "stockfish" / "stockfish.exe",
Path.home() / "OneDrive" / "Documents" / "stockfish-windows-x86-64-avx2" / "stockfish" / "stockfish.exe",
]
for path in possible_paths:
if path.exists():
return str(path)
raise FileNotFoundError("Stockfish not found. Please install Stockfish.")
def convert_numpy_types(obj):
"""Convert NumPy types to Python native types for JSON serialization."""
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, dict):
return {key: convert_numpy_types(value) for key, value in obj.items()}
elif isinstance(obj, list):
return [convert_numpy_types(item) for item in obj]
else:
return obj
def allowed_file(filename):
"""Check if the uploaded file has an allowed extension."""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def analyze_pgn(pgn_content):
"""Analyze a PGN file using the enhanced EngineAnalyzer."""
try:
# Import the EngineAnalyzer here to avoid circular imports
from analyzer.engine_analyzer import EngineAnalyzer
# Create analyzer instance and analyze the game
analyzer = EngineAnalyzer()
analysis_result = analyzer.analyze_game(pgn_content)
if not analysis_result.get('success', False):
return {
'success': False,
'error': analysis_result.get('error', 'Analysis failed')
}
# Process the analysis result to match expected frontend format
game_info = analysis_result.get('game_info', {})
move_analyses = analysis_result.get('move_analyses', [])
metrics = analysis_result.get('metrics', {})
# Separate moves by player for frontend compatibility
white_moves = []
black_moves = []
for move_analysis in move_analyses:
# Extract move data with enhanced complexity information
move_data = {
'move_number': move_analysis.get('move_number', 0),
'player': move_analysis.get('player', 'white'),
'move': move_analysis.get('move', ''),
'move_time': move_analysis.get('move_time', 0),
'clock_time': move_analysis.get('clock_time', 0),
'centipawn_loss': move_analysis.get('engine_analysis', {}).get('centipawn_loss', 0),
'move_rank': move_analysis.get('engine_analysis', {}).get('move_rank', 0),
'evaluation': move_analysis.get('engine_analysis', {}).get('evaluation', 0),
'legal_moves_count': move_analysis.get('legal_moves_count', 0),
'complexity': move_analysis.get('complexity', {}), # This now contains PCS data
'engine_analysis': move_analysis.get('engine_analysis', {}),
'opening_analysis': move_analysis.get('opening_analysis', {})
}
if move_data['player'] == 'white':
white_moves.append(move_data)
else:
black_moves.append(move_data)
# Extract player metrics
def flatten_player_metrics(raw_metrics):
"""Flatten nested metrics (accuracy_metrics, engine_matching, temporal_metrics)"""
if not raw_metrics:
return {}
accuracy = raw_metrics.get('accuracy_metrics', {})
engine_match = raw_metrics.get('engine_matching', {})
temporal = raw_metrics.get('temporal_metrics', {})
flattened = {
# Accuracy metrics
'accuracy_score': accuracy.get('accuracy_score', 0),
'avg_centipawn_loss': accuracy.get('avg_centipawn_loss', 0),
'std_centipawn_loss': accuracy.get('std_centipawn_loss', 0),
'blunder_count': accuracy.get('blunder_count', 0),
'mistake_count': accuracy.get('mistake_count', 0),
'total_moves': accuracy.get('total_moves', 0),
# Engine matching (rename pv1_percentage -> best_move_rate)
'best_move_rate': engine_match.get('pv1_percentage', 0),
'pv1_count': engine_match.get('pv1_count', 0),
'pv2_count': engine_match.get('pv2_count', 0),
'pv3_count': engine_match.get('pv3_count', 0),
'top2_match_rate': engine_match.get('pv2_percentage', 0),
'top3_match_rate': engine_match.get('pv3_percentage', 0),
# Alias for backward compatibility with frontend
'top3_move_rate': engine_match.get('pv3_percentage', 0),
# Temporal metrics (include mean move time, etc.)
'move_time_mean': temporal.get('move_time_mean', 0),
'avg_move_time': temporal.get('move_time_mean', 0),
'move_time_std': temporal.get('move_time_std', 0),
'move_time_cv': temporal.get('move_time_cv', 0),
'total_moves_with_time': temporal.get('total_moves_with_time', 0),
'time_consistency_score': temporal.get('time_consistency_score', 0),
# Opening theory
'opening_move_count': raw_metrics.get('opening_metrics_player', {}).get('opening_move_count', 0)
}
return flattened
white_metrics = flatten_player_metrics(metrics.get('white_player', {}))
black_metrics = flatten_player_metrics(metrics.get('black_player', {}))
return {
'success': True,
'game_info': game_info,
'white_player': {
'name': game_info.get('white', 'Unknown'),
'moves': white_moves,
'metrics': white_metrics
},
'black_player': {
'name': game_info.get('black', 'Unknown'),
'moves': black_moves,
'metrics': black_metrics
},
'metrics': metrics, # Include overall game metrics
'analysis_settings': analysis_result.get('analysis_settings', {})
}
except Exception as e:
logger.error(f"Error analyzing PGN: {e}")
logger.error(traceback.format_exc())
return {
'success': False,
'error': str(e)
}
@app.route('/')
def index():
"""Main page with file upload form."""
return render_template('index.html')
@app.route('/about')
def about():
"""About page with methodology and technical details."""
return render_template('about.html')
@app.route('/upload', methods=['POST'])
def upload_file():
"""Handle PGN file upload and analysis."""
try:
# Check if file was uploaded
if 'file' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
if not allowed_file(file.filename):
return jsonify({'error': 'Invalid file type. Please upload a PGN file.'}), 400
# Read PGN content
pgn_content = file.read().decode('utf-8')
# Analyze the game
logger.info("Starting game analysis...")
analysis_result = analyze_pgn(pgn_content)
if not analysis_result['success']:
return jsonify({'error': analysis_result['error']}), 500
logger.info("Analysis completed successfully")
# Convert NumPy types to JSON-serializable types
clean_result = convert_numpy_types(analysis_result)
return jsonify({
'success': True,
'analysis': clean_result,
'filename': file.filename
})
except Exception as e:
logger.error(f"Error in file upload/analysis: {str(e)}")
logger.error(traceback.format_exc())
return jsonify({
'error': f'Analysis failed: {str(e)}'
}), 500
@app.route('/favicon.ico')
def favicon():
"""Serve favicon to prevent 404 errors."""
return '', 204
@app.errorhandler(404)
def not_found(error):
"""Handle 404 errors."""
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
"""Handle 500 errors."""
logger.error(f"Internal server error: {str(error)}")
return render_template('500.html'), 500
if __name__ == '__main__':
# Create upload directory if it doesn't exist
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
# Run the application
app.run(debug=True, host='0.0.0.0', port=5000)