Spaces:
Sleeping
Sleeping
Commit
Β·
c30ca89
1
Parent(s):
054c349
added in the right github structure
Browse files- app.py +1 -473
- config.py +0 -97
- core/ai_integration.py +65 -0
- core/chess_api.py +71 -0
- core/core.py +344 -0
- core/openings.py +32 -0
- Chess Opening Reference - Sheet1.csv β data/Chess Opening Reference - Sheet1.csv +0 -0
- handlers.py +0 -378
- openings.py +0 -82
- project_info.md +48 -0
app.py
CHANGED
|
@@ -6,9 +6,8 @@ import requests
|
|
| 6 |
import google.generativeai as genai
|
| 7 |
from collections import defaultdict
|
| 8 |
import os
|
| 9 |
-
import time
|
| 10 |
import logging
|
| 11 |
-
import
|
| 12 |
|
| 13 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 14 |
logger = logging.getLogger(__name__)
|
|
@@ -17,478 +16,7 @@ GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
|
|
| 17 |
genai.configure(api_key=GEMINI_API_KEY)
|
| 18 |
model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
| 19 |
|
| 20 |
-
def load_opening_database():
|
| 21 |
-
openings = {}
|
| 22 |
-
try:
|
| 23 |
-
with open('Chess Opening Reference - Sheet1.csv', 'r', encoding='utf-8') as f:
|
| 24 |
-
reader = csv.DictReader(f)
|
| 25 |
-
for row in reader:
|
| 26 |
-
eco = row.get('ECO Code', '').strip()
|
| 27 |
-
name = row.get('Name', '').strip()
|
| 28 |
-
if eco and name:
|
| 29 |
-
openings[eco] = name
|
| 30 |
-
except Exception as e:
|
| 31 |
-
logger.error(f"Failed to load openings CSV: {e}")
|
| 32 |
-
return openings
|
| 33 |
|
| 34 |
-
OPENING_DB = load_opening_database()
|
| 35 |
-
|
| 36 |
-
def detect_opening(game):
|
| 37 |
-
eco = game.headers.get("ECO", "")
|
| 38 |
-
opening = game.headers.get("Opening", "")
|
| 39 |
-
|
| 40 |
-
if eco and eco in OPENING_DB:
|
| 41 |
-
return OPENING_DB[eco]
|
| 42 |
-
elif opening:
|
| 43 |
-
return opening
|
| 44 |
-
else:
|
| 45 |
-
return "Unknown Opening"
|
| 46 |
-
|
| 47 |
-
def get_user_games_from_chess_com(username):
|
| 48 |
-
try:
|
| 49 |
-
logger.info(f"Fetching games for: {username}")
|
| 50 |
-
username = username.strip().lower()
|
| 51 |
-
|
| 52 |
-
user_url = f"https://api.chess.com/pub/player/{username}"
|
| 53 |
-
response = requests.get(user_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 54 |
-
|
| 55 |
-
if response.status_code != 200:
|
| 56 |
-
return None, f"β Foydalanuvchi topilmadi: {username}"
|
| 57 |
-
|
| 58 |
-
archives_url = f"https://api.chess.com/pub/player/{username}/games/archives"
|
| 59 |
-
response = requests.get(archives_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 60 |
-
|
| 61 |
-
if response.status_code != 200:
|
| 62 |
-
return None, "β O'yinlar arxivi topilmadi."
|
| 63 |
-
|
| 64 |
-
archives = response.json()['archives']
|
| 65 |
-
if not archives:
|
| 66 |
-
return None, "β O'yinlar topilmadi."
|
| 67 |
-
|
| 68 |
-
all_games = []
|
| 69 |
-
for archive_url in reversed(archives[-3:]):
|
| 70 |
-
time.sleep(0.3)
|
| 71 |
-
response = requests.get(archive_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 72 |
-
if response.status_code == 200:
|
| 73 |
-
games = response.json()['games']
|
| 74 |
-
all_games.extend(games)
|
| 75 |
-
if len(all_games) >= 50:
|
| 76 |
-
break
|
| 77 |
-
|
| 78 |
-
rapid_games = [g for g in all_games if g.get('time_class') in ['rapid', 'blitz']]
|
| 79 |
-
if not rapid_games:
|
| 80 |
-
rapid_games = all_games[:50]
|
| 81 |
-
|
| 82 |
-
pgn_list = [g['pgn'] for g in rapid_games[:50] if 'pgn' in g]
|
| 83 |
-
|
| 84 |
-
if not pgn_list:
|
| 85 |
-
return None, "β PGN formatdagi o'yinlar topilmadi."
|
| 86 |
-
|
| 87 |
-
return pgn_list, None
|
| 88 |
-
|
| 89 |
-
except Exception as e:
|
| 90 |
-
logger.error(f"Error fetching games: {str(e)}")
|
| 91 |
-
return None, f"β Xatolik: {str(e)}"
|
| 92 |
-
|
| 93 |
-
def parse_pgn_content(pgn_content):
|
| 94 |
-
games = []
|
| 95 |
-
if isinstance(pgn_content, list):
|
| 96 |
-
for pgn_text in pgn_content:
|
| 97 |
-
try:
|
| 98 |
-
game = chess.pgn.read_game(io.StringIO(pgn_text))
|
| 99 |
-
if game:
|
| 100 |
-
games.append(game)
|
| 101 |
-
except:
|
| 102 |
-
pass
|
| 103 |
-
else:
|
| 104 |
-
pgn_io = io.StringIO(pgn_content)
|
| 105 |
-
while True:
|
| 106 |
-
try:
|
| 107 |
-
game = chess.pgn.read_game(pgn_io)
|
| 108 |
-
if game is None:
|
| 109 |
-
break
|
| 110 |
-
games.append(game)
|
| 111 |
-
except:
|
| 112 |
-
break
|
| 113 |
-
|
| 114 |
-
return games
|
| 115 |
-
|
| 116 |
-
def analyze_game_detailed(game, username):
|
| 117 |
-
board = game.board()
|
| 118 |
-
mistakes = []
|
| 119 |
-
move_number = 0
|
| 120 |
-
|
| 121 |
-
white_player = game.headers.get("White", "").strip().lower()
|
| 122 |
-
black_player = game.headers.get("Black", "").strip().lower()
|
| 123 |
-
username_lower = username.strip().lower()
|
| 124 |
-
|
| 125 |
-
user_color = None
|
| 126 |
-
if username_lower == white_player:
|
| 127 |
-
user_color = chess.WHITE
|
| 128 |
-
elif username_lower == black_player:
|
| 129 |
-
user_color = chess.BLACK
|
| 130 |
-
|
| 131 |
-
result = game.headers.get("Result", "*")
|
| 132 |
-
opening = detect_opening(game)
|
| 133 |
-
|
| 134 |
-
user_result = None
|
| 135 |
-
if user_color is not None and result != "*":
|
| 136 |
-
if result == "1-0":
|
| 137 |
-
user_result = "win" if user_color == chess.WHITE else "loss"
|
| 138 |
-
elif result == "0-1":
|
| 139 |
-
user_result = "win" if user_color == chess.BLACK else "loss"
|
| 140 |
-
elif result == "1/2-1/2":
|
| 141 |
-
user_result = "draw"
|
| 142 |
-
|
| 143 |
-
material_values = {chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3, chess.ROOK: 5, chess.QUEEN: 9}
|
| 144 |
-
|
| 145 |
-
def count_material(board):
|
| 146 |
-
total = 0
|
| 147 |
-
for piece_type in material_values:
|
| 148 |
-
total += len(board.pieces(piece_type, chess.WHITE)) * material_values[piece_type]
|
| 149 |
-
total -= len(board.pieces(piece_type, chess.BLACK)) * material_values[piece_type]
|
| 150 |
-
return total
|
| 151 |
-
|
| 152 |
-
for move in game.mainline_moves():
|
| 153 |
-
move_number += 1
|
| 154 |
-
|
| 155 |
-
if user_color is not None and board.turn != user_color:
|
| 156 |
-
board.push(move)
|
| 157 |
-
continue
|
| 158 |
-
|
| 159 |
-
material_before = count_material(board)
|
| 160 |
-
board.push(move)
|
| 161 |
-
material_after = count_material(board)
|
| 162 |
-
|
| 163 |
-
material_loss = abs(material_after - material_before) if board.turn == chess.WHITE else abs(material_before - material_after)
|
| 164 |
-
|
| 165 |
-
moved_piece = board.piece_at(move.to_square)
|
| 166 |
-
mistake_type = None
|
| 167 |
-
|
| 168 |
-
if material_loss >= 3:
|
| 169 |
-
mistake_type = 'blunder'
|
| 170 |
-
elif material_loss >= 1:
|
| 171 |
-
mistake_type = 'mistake'
|
| 172 |
-
elif moved_piece and board.is_attacked_by(not board.turn, move.to_square):
|
| 173 |
-
attackers = len(board.attackers(not board.turn, move.to_square))
|
| 174 |
-
defenders = len(board.attackers(board.turn, move.to_square))
|
| 175 |
-
if attackers > defenders:
|
| 176 |
-
mistake_type = 'hanging_piece'
|
| 177 |
-
|
| 178 |
-
if move_number <= 10:
|
| 179 |
-
phase = 'opening'
|
| 180 |
-
elif len(board.piece_map()) <= 10:
|
| 181 |
-
phase = 'endgame'
|
| 182 |
-
else:
|
| 183 |
-
phase = 'middlegame'
|
| 184 |
-
|
| 185 |
-
if mistake_type:
|
| 186 |
-
mistakes.append({
|
| 187 |
-
'type': mistake_type,
|
| 188 |
-
'phase': phase,
|
| 189 |
-
'move_number': move_number
|
| 190 |
-
})
|
| 191 |
-
|
| 192 |
-
return {
|
| 193 |
-
'mistakes': mistakes,
|
| 194 |
-
'opening': opening,
|
| 195 |
-
'result': result,
|
| 196 |
-
'user_color': user_color,
|
| 197 |
-
'user_result': user_result
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
def categorize_mistakes(all_analyses):
|
| 201 |
-
if not all_analyses:
|
| 202 |
-
return []
|
| 203 |
-
|
| 204 |
-
blunders = 0
|
| 205 |
-
regular_mistakes = 0
|
| 206 |
-
hanging = 0
|
| 207 |
-
opening = 0
|
| 208 |
-
middlegame = 0
|
| 209 |
-
endgame = 0
|
| 210 |
-
|
| 211 |
-
for analysis in all_analyses:
|
| 212 |
-
for mistake in analysis.get('mistakes', []):
|
| 213 |
-
mistake_type = mistake.get('type')
|
| 214 |
-
phase = mistake.get('phase')
|
| 215 |
-
|
| 216 |
-
if mistake_type == 'blunder':
|
| 217 |
-
blunders += 1
|
| 218 |
-
elif mistake_type == 'mistake':
|
| 219 |
-
regular_mistakes += 1
|
| 220 |
-
elif mistake_type == 'hanging_piece':
|
| 221 |
-
hanging += 1
|
| 222 |
-
|
| 223 |
-
if phase == 'opening':
|
| 224 |
-
opening += 1
|
| 225 |
-
elif phase == 'middlegame':
|
| 226 |
-
middlegame += 1
|
| 227 |
-
elif phase == 'endgame':
|
| 228 |
-
endgame += 1
|
| 229 |
-
|
| 230 |
-
total = blunders + regular_mistakes + hanging + opening + middlegame + endgame
|
| 231 |
-
|
| 232 |
-
if total == 0:
|
| 233 |
-
return []
|
| 234 |
-
|
| 235 |
-
weaknesses = []
|
| 236 |
-
|
| 237 |
-
if blunders > 0:
|
| 238 |
-
weaknesses.append({
|
| 239 |
-
'category': "Qo'pol xatolar",
|
| 240 |
-
'count': blunders,
|
| 241 |
-
'percentage': (blunders / total * 100)
|
| 242 |
-
})
|
| 243 |
-
|
| 244 |
-
if regular_mistakes > 0:
|
| 245 |
-
weaknesses.append({
|
| 246 |
-
'category': 'Kichik xatolar',
|
| 247 |
-
'count': regular_mistakes,
|
| 248 |
-
'percentage': (regular_mistakes / total * 100)
|
| 249 |
-
})
|
| 250 |
-
|
| 251 |
-
if hanging > 0:
|
| 252 |
-
weaknesses.append({
|
| 253 |
-
'category': 'Himoyasiz qoldirish',
|
| 254 |
-
'count': hanging,
|
| 255 |
-
'percentage': (hanging / total * 100)
|
| 256 |
-
})
|
| 257 |
-
|
| 258 |
-
if opening > 0:
|
| 259 |
-
weaknesses.append({
|
| 260 |
-
'category': 'Debyut xatolari',
|
| 261 |
-
'count': opening,
|
| 262 |
-
'percentage': (opening / total * 100)
|
| 263 |
-
})
|
| 264 |
-
|
| 265 |
-
if middlegame > 0:
|
| 266 |
-
weaknesses.append({
|
| 267 |
-
'category': "O'rta o'yin xatolari",
|
| 268 |
-
'count': middlegame,
|
| 269 |
-
'percentage': (middlegame / total * 100)
|
| 270 |
-
})
|
| 271 |
-
|
| 272 |
-
if endgame > 0:
|
| 273 |
-
weaknesses.append({
|
| 274 |
-
'category': 'Endshpil xatolari',
|
| 275 |
-
'count': endgame,
|
| 276 |
-
'percentage': (endgame / total * 100)
|
| 277 |
-
})
|
| 278 |
-
|
| 279 |
-
weaknesses.sort(key=lambda x: x['count'], reverse=True)
|
| 280 |
-
|
| 281 |
-
return weaknesses
|
| 282 |
-
|
| 283 |
-
def fetch_lichess_puzzles(themes, count=5):
|
| 284 |
-
puzzles = []
|
| 285 |
-
|
| 286 |
-
try:
|
| 287 |
-
url = "https://lichess.org/training"
|
| 288 |
-
for i in range(count):
|
| 289 |
-
puzzles.append({
|
| 290 |
-
'id': f'puzzle_{i+1}',
|
| 291 |
-
'url': url,
|
| 292 |
-
'theme': themes[0] if themes else 'Tactics',
|
| 293 |
-
'rating': 1500
|
| 294 |
-
})
|
| 295 |
-
except Exception as e:
|
| 296 |
-
logger.error(f"Error generating puzzle links: {str(e)}")
|
| 297 |
-
|
| 298 |
-
return puzzles
|
| 299 |
-
|
| 300 |
-
def get_comprehensive_analysis(weaknesses, opening_stats, color_stats, total_games):
|
| 301 |
-
weakness_text = "\n".join([f"- {w['category']}: {w['count']} marta ({w['percentage']:.1f}%)" for w in weaknesses])
|
| 302 |
-
|
| 303 |
-
opening_text = "\n".join([f"- {opening}: {stats['total']} o'yin (G'alabalar: {stats['wins']}, Yutqazishlar: {stats['losses']}, Duranglar: {stats['draws']})"
|
| 304 |
-
for opening, stats in list(opening_stats.items())[:5]])
|
| 305 |
-
|
| 306 |
-
color_text = f"Oq rangda: {color_stats['white']['wins']}G-{color_stats['white']['losses']}Y-{color_stats['white']['draws']}D\n"
|
| 307 |
-
color_text += f"Qora rangda: {color_stats['black']['wins']}G-{color_stats['black']['losses']}Y-{color_stats['black']['draws']}D"
|
| 308 |
-
|
| 309 |
-
prompt = f"""Siz professional ΡΠ°Ρ
ΠΌΠ°Ρ murabbiy va tahlilchisiz. O'yinchining {total_games} ta o'yinini tahlil qildingiz.
|
| 310 |
-
|
| 311 |
-
STATISTIKA:
|
| 312 |
-
|
| 313 |
-
Zaif tomonlar:
|
| 314 |
-
{weakness_text}
|
| 315 |
-
|
| 316 |
-
Eng ko'p o'ynaladigan debyutlar:
|
| 317 |
-
{opening_text}
|
| 318 |
-
|
| 319 |
-
Rang bo'yicha natijalar:
|
| 320 |
-
{color_text}
|
| 321 |
-
|
| 322 |
-
Quyidagilarni taqdim eting:
|
| 323 |
-
|
| 324 |
-
1. **ZAIF TOMONLAR TAHLILI**: Har bir zaif tomonni chuqur tahlil qiling va nima uchun bu muammo kelib chiqayotganini tushuntiring.
|
| 325 |
-
|
| 326 |
-
2. **SHAXSIY O'QUV REJASI**: Kundalik mashg'ulotlar rejasini tuzing:
|
| 327 |
-
- Har kuni nechta masala yechish kerak va qanday turdagi masalalar
|
| 328 |
-
- Qaysi debyutlarni o'rganish kerak
|
| 329 |
-
- Qaysi o'yin bosqichiga ko'proq e'tibor berish kerak
|
| 330 |
-
- Kompyuter yoki botlar bilan qanday mashq qilish kerak
|
| 331 |
-
|
| 332 |
-
3. **TAVSIYA ETILGAN RESURSLAR**:
|
| 333 |
-
- Kitoblar (muallif va nom bilan)
|
| 334 |
-
- Onlayn kurslar (Uzchess, Chess.com, Lichess)
|
| 335 |
-
- YouTube kanallari
|
| 336 |
-
- Mashq uchun maxsus botlar yoki dasturlar
|
| 337 |
-
|
| 338 |
-
4. **DEBYUT TAVSIYALARI**: Statistikaga asoslanib, qaysi debyutlarni davom ettirish va qaysilarini o'zgartirish kerak.
|
| 339 |
-
|
| 340 |
-
5. **MOTIVATSION XULOSA**: Qisqa va rag'batlantiruvchi xulosa.
|
| 341 |
-
|
| 342 |
-
MUHIM: Javobni FAQAT O'ZBEK TILIDA yozing! Aniq va amaliy maslahatlar bering."""
|
| 343 |
-
|
| 344 |
-
try:
|
| 345 |
-
response = model.generate_content(prompt)
|
| 346 |
-
return response.text
|
| 347 |
-
except Exception as e:
|
| 348 |
-
logger.error(f"AI analysis failed: {str(e)}")
|
| 349 |
-
return f"AI tahlil hozircha mavjud emas: {str(e)}"
|
| 350 |
-
|
| 351 |
-
def analyze_games(username_chesscom, pgn_file, username_pgn):
|
| 352 |
-
actual_username = None
|
| 353 |
-
pgn_content = None
|
| 354 |
-
|
| 355 |
-
if username_chesscom:
|
| 356 |
-
pgn_content, error = get_user_games_from_chess_com(username_chesscom)
|
| 357 |
-
if error:
|
| 358 |
-
return error, "", "", "", None, None, None, None, None
|
| 359 |
-
actual_username = username_chesscom
|
| 360 |
-
|
| 361 |
-
elif pgn_file:
|
| 362 |
-
pgn_content = pgn_file.decode('utf-8') if isinstance(pgn_file, bytes) else pgn_file
|
| 363 |
-
|
| 364 |
-
if username_pgn and username_pgn.strip():
|
| 365 |
-
actual_username = username_pgn.strip()
|
| 366 |
-
else:
|
| 367 |
-
try:
|
| 368 |
-
first_game = chess.pgn.read_game(io.StringIO(pgn_content))
|
| 369 |
-
if first_game:
|
| 370 |
-
white = first_game.headers.get("White", "")
|
| 371 |
-
black = first_game.headers.get("Black", "")
|
| 372 |
-
actual_username = white if white else black if black else "Player"
|
| 373 |
-
else:
|
| 374 |
-
actual_username = "Player"
|
| 375 |
-
except:
|
| 376 |
-
actual_username = "Player"
|
| 377 |
-
|
| 378 |
-
else:
|
| 379 |
-
return "β Chess.com foydalanuvchi nomini kiriting yoki PGN faylni yuklang", "", "", "", None, None, None, None, None
|
| 380 |
-
|
| 381 |
-
games = parse_pgn_content(pgn_content)
|
| 382 |
-
|
| 383 |
-
if not games:
|
| 384 |
-
return "β O'yinlar topilmadi yoki tahlil qilinmadi", "", "", "", None, None, None, None, None
|
| 385 |
-
|
| 386 |
-
all_analyses = []
|
| 387 |
-
opening_stats = defaultdict(lambda: {'wins': 0, 'losses': 0, 'draws': 0, 'total': 0})
|
| 388 |
-
color_stats = {
|
| 389 |
-
'white': {'wins': 0, 'losses': 0, 'draws': 0},
|
| 390 |
-
'black': {'wins': 0, 'losses': 0, 'draws': 0}
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
for game in games:
|
| 394 |
-
analysis = analyze_game_detailed(game, actual_username)
|
| 395 |
-
all_analyses.append(analysis)
|
| 396 |
-
|
| 397 |
-
opening = analysis['opening']
|
| 398 |
-
user_result = analysis.get('user_result')
|
| 399 |
-
user_color = analysis['user_color']
|
| 400 |
-
|
| 401 |
-
if user_color is not None:
|
| 402 |
-
opening_stats[opening]['total'] += 1
|
| 403 |
-
|
| 404 |
-
if user_result == 'win':
|
| 405 |
-
opening_stats[opening]['wins'] += 1
|
| 406 |
-
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 407 |
-
color_stats[color_key]['wins'] += 1
|
| 408 |
-
elif user_result == 'loss':
|
| 409 |
-
opening_stats[opening]['losses'] += 1
|
| 410 |
-
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 411 |
-
color_stats[color_key]['losses'] += 1
|
| 412 |
-
elif user_result == 'draw':
|
| 413 |
-
opening_stats[opening]['draws'] += 1
|
| 414 |
-
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 415 |
-
color_stats[color_key]['draws'] += 1
|
| 416 |
-
|
| 417 |
-
weaknesses = categorize_mistakes(all_analyses)
|
| 418 |
-
|
| 419 |
-
all_mistakes = []
|
| 420 |
-
for analysis in all_analyses:
|
| 421 |
-
all_mistakes.extend(analysis.get('mistakes', []))
|
| 422 |
-
|
| 423 |
-
stats_report = f"## π {len(games)} ta o'yin tahlili\n\n"
|
| 424 |
-
stats_report += f"**Jami xatolar:** {len(all_mistakes)} ta\n\n"
|
| 425 |
-
|
| 426 |
-
stats_report += "### π― Eng zaif 3 tomoningiz:\n\n"
|
| 427 |
-
if weaknesses:
|
| 428 |
-
for i, w in enumerate(weaknesses[:3], 1):
|
| 429 |
-
stats_report += f"**{i}. {w['category']}** - {w['count']} marta ({w['percentage']:.1f}%)\n"
|
| 430 |
-
else:
|
| 431 |
-
stats_report += "Xatolar topilmadi yoki tahlil qilinmadi.\n"
|
| 432 |
-
|
| 433 |
-
opening_report = "\n\n## π Debyut Statistikasi\n\n"
|
| 434 |
-
sorted_openings = sorted(opening_stats.items(), key=lambda x: x[1]['total'], reverse=True)[:10]
|
| 435 |
-
|
| 436 |
-
for opening, stats in sorted_openings:
|
| 437 |
-
total = stats['total']
|
| 438 |
-
wins = stats['wins']
|
| 439 |
-
losses = stats['losses']
|
| 440 |
-
draws = stats['draws']
|
| 441 |
-
win_rate = (wins / total * 100) if total > 0 else 0
|
| 442 |
-
|
| 443 |
-
opening_report += f"**{opening}** ({total} o'yin)\n"
|
| 444 |
-
opening_report += f"- G'alabalar: {wins} ({win_rate:.1f}%) | Yutqazishlar: {losses} | Duranglar: {draws}\n\n"
|
| 445 |
-
|
| 446 |
-
color_report = "\n\n## βͺβ« Rang bo'yicha natijalar\n\n"
|
| 447 |
-
|
| 448 |
-
white_total = sum(color_stats['white'].values())
|
| 449 |
-
black_total = sum(color_stats['black'].values())
|
| 450 |
-
|
| 451 |
-
if white_total > 0:
|
| 452 |
-
white_wr = color_stats['white']['wins'] / white_total * 100
|
| 453 |
-
color_report += f"**Oq figuralar bilan:**\n"
|
| 454 |
-
color_report += f"- G'alabalar: {color_stats['white']['wins']} ({white_wr:.1f}%)\n"
|
| 455 |
-
color_report += f"- Yutqazishlar: {color_stats['white']['losses']}\n"
|
| 456 |
-
color_report += f"- Duranglar: {color_stats['white']['draws']}\n\n"
|
| 457 |
-
|
| 458 |
-
if black_total > 0:
|
| 459 |
-
black_wr = color_stats['black']['wins'] / black_total * 100
|
| 460 |
-
color_report += f"**Qora figuralar bilan:**\n"
|
| 461 |
-
color_report += f"- G'alabalar: {color_stats['black']['wins']} ({black_wr:.1f}%)\n"
|
| 462 |
-
color_report += f"- Yutqazishlar: {color_stats['black']['losses']}\n"
|
| 463 |
-
color_report += f"- Duranglar: {color_stats['black']['draws']}\n"
|
| 464 |
-
|
| 465 |
-
full_report = stats_report + opening_report + color_report
|
| 466 |
-
|
| 467 |
-
ai_analysis = get_comprehensive_analysis(weaknesses, opening_stats, color_stats, len(games))
|
| 468 |
-
ai_report = f"## π€ AI Murabbiy: To'liq Tahlil va O'quv Rejasi\n\n{ai_analysis}"
|
| 469 |
-
|
| 470 |
-
weakness_themes = [w['category'] for w in weaknesses[:3]]
|
| 471 |
-
puzzles = fetch_lichess_puzzles(weakness_themes, count=5)
|
| 472 |
-
|
| 473 |
-
puzzle_text = "## π§© Sizning shaxsiy masalalaringiz\n\n"
|
| 474 |
-
puzzle_text += "Masalalarni yechish uchun quyidagi havoladan foydalaning:\n\n"
|
| 475 |
-
for i, puzzle in enumerate(puzzles, 1):
|
| 476 |
-
theme = puzzle.get('theme', 'Tactics')
|
| 477 |
-
url = puzzle.get('url', 'https://lichess.org/training')
|
| 478 |
-
puzzle_text += f"**Puzzle {i}: {theme}**\n"
|
| 479 |
-
puzzle_text += f"- [Lichess Training]({url})\n\n"
|
| 480 |
-
|
| 481 |
-
return (
|
| 482 |
-
full_report,
|
| 483 |
-
ai_report,
|
| 484 |
-
puzzle_text,
|
| 485 |
-
"",
|
| 486 |
-
None,
|
| 487 |
-
"",
|
| 488 |
-
None,
|
| 489 |
-
"",
|
| 490 |
-
None
|
| 491 |
-
)
|
| 492 |
|
| 493 |
with gr.Blocks(title="Chess Study Plan Pro", theme=gr.themes.Soft()) as demo:
|
| 494 |
gr.Markdown("""
|
|
|
|
| 6 |
import google.generativeai as genai
|
| 7 |
from collections import defaultdict
|
| 8 |
import os
|
|
|
|
| 9 |
import logging
|
| 10 |
+
from core.core import analyze_games
|
| 11 |
|
| 12 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 13 |
logger = logging.getLogger(__name__)
|
|
|
|
| 16 |
genai.configure(api_key=GEMINI_API_KEY)
|
| 17 |
model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
with gr.Blocks(title="Chess Study Plan Pro", theme=gr.themes.Soft()) as demo:
|
| 22 |
gr.Markdown("""
|
config.py
DELETED
|
@@ -1,97 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
# ============= API SOZLAMALARI =============
|
| 4 |
-
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY', '')
|
| 5 |
-
|
| 6 |
-
# ============= STOCKFISH SOZLAMALARI =============
|
| 7 |
-
# Stockfish dasturining yo'lini kiriting
|
| 8 |
-
# Windows: "C:/stockfish/stockfish.exe"
|
| 9 |
-
# Linux/Mac: "/usr/local/bin/stockfish"
|
| 10 |
-
STOCKFISH_PATH = os.environ.get('STOCKFISH_PATH', 'stockfish')
|
| 11 |
-
STOCKFISH_DEPTH = 15 # Tahlil chuqurligi (15-20 yetarli)
|
| 12 |
-
STOCKFISH_TIME = 0.1 # Har bir harakat uchun vaqt (sekundlarda)
|
| 13 |
-
|
| 14 |
-
# ============= O'YIN SOZLAMALARI =============
|
| 15 |
-
MAX_GAMES_TO_ANALYZE = 50 # Tahlil qilinadigan o'yinlar soni
|
| 16 |
-
BLUNDER_THRESHOLD = 200 # Qo'pol xato chegarasi (santipawn)
|
| 17 |
-
MISTAKE_THRESHOLD = 100 # Xato chegarasi (santipawn)
|
| 18 |
-
|
| 19 |
-
# ============= CHESS.COM API =============
|
| 20 |
-
CHESS_COM_API_BASE = "https://api.chess.com/pub"
|
| 21 |
-
REQUEST_TIMEOUT = 10 # Sekundlarda
|
| 22 |
-
|
| 23 |
-
# ============= LICHESS API =============
|
| 24 |
-
LICHESS_API_BASE = "https://lichess.org/api"
|
| 25 |
-
|
| 26 |
-
# ============= GEMINI PROMPT SHABLONLARI =============
|
| 27 |
-
|
| 28 |
-
WEAKNESS_ANALYSIS_PROMPT = """Siz ΡΠ°Ρ
ΠΌΠ°Ρ murabbiysiz. O'yinchi o'zining so'nggi o'yinlarida quyidagi zaif tomonlarni ko'rsatdi:
|
| 29 |
-
|
| 30 |
-
{weakness_text}
|
| 31 |
-
|
| 32 |
-
Har bir zaif tomonni oddiy va rag'batlantiruvchi tilda tushuntiring (har biri uchun 2-3 jumla). Yaxshilash uchun amaliy maslahatlar bering. Do'stona va motivatsion bo'ling.
|
| 33 |
-
|
| 34 |
-
MUHIM: Javobni FAQAT O'ZBEK TILIDA yozing!"""
|
| 35 |
-
|
| 36 |
-
MISTAKE_EXPLANATION_PROMPT = """Siz ΡΠ°Ρ
ΠΌΠ°Ρ o'qituvchisisiz. O'yinchining quyidagi harakati tahlil qilindi:
|
| 37 |
-
|
| 38 |
-
Harakat: {move}
|
| 39 |
-
Pozitsiya: {position}
|
| 40 |
-
Xato turi: {mistake_type}
|
| 41 |
-
Baholash farqi: {eval_diff} santipawn
|
| 42 |
-
|
| 43 |
-
Bu xatoni oddiy tilda tushuntiring va qanday qilib oldini olish mumkinligini aytib bering. 2-3 jumlada javob bering.
|
| 44 |
-
|
| 45 |
-
MUHIM: Javobni FAQAT O'ZBEK TILIDA yozing!"""
|
| 46 |
-
|
| 47 |
-
# ============= XATO KATEGORIYALARI =============
|
| 48 |
-
|
| 49 |
-
MISTAKE_CATEGORIES = {
|
| 50 |
-
'blunder': 'Qo\'pol xatolar',
|
| 51 |
-
'mistake': 'Kichik xatolar',
|
| 52 |
-
'hanging_piece': 'Himoyasiz qoldirish',
|
| 53 |
-
'tactical_miss': 'Taktik imkoniyatni o\'tkazib yuborish',
|
| 54 |
-
'positional_error': 'Pozitsion xatolar',
|
| 55 |
-
'time_trouble': 'Vaqt muammolari',
|
| 56 |
-
'opening_mistake': 'Debyut xatolari',
|
| 57 |
-
'middlegame_mistake': 'O\'rta o\'yin xatolari',
|
| 58 |
-
'endgame_mistake': 'Endshpil xatolari'
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
# ============= INTERFACE MATNLARI =============
|
| 62 |
-
|
| 63 |
-
UI_TEXTS = {
|
| 64 |
-
'title': 'βοΈ Shaxsiy Π¨Π°Ρ
ΠΌΠ°Ρ O\'quv Rejasi Generatori',
|
| 65 |
-
'description': """Chess.com foydalanuvchi nomingizni kiriting va quyidagilarni oling:
|
| 66 |
-
- π Eng zaif 3 tomoningizni tahlil
|
| 67 |
-
- π€ AI murabbiy tushuntirishlari
|
| 68 |
-
- π§© Yaxshilash uchun 5 ta shaxsiy masala""",
|
| 69 |
-
'username_label': 'Chess.com foydalanuvchi nomi',
|
| 70 |
-
'username_placeholder': 'Foydalanuvchi nomini kiriting',
|
| 71 |
-
'analyze_button': 'π O\'yinlarni tahlil qilish',
|
| 72 |
-
'upload_pgn': 'π PGN faylni yuklash',
|
| 73 |
-
'weakness_title': 'π Zaif Tomonlar',
|
| 74 |
-
'explanation_title': 'π€ AI Murabbiy Tahlili',
|
| 75 |
-
'puzzles_title': 'π§© Sizning Shaxsiy O\'quv Rejangiz',
|
| 76 |
-
'instructions': """
|
| 77 |
-
### π Qanday foydalanish:
|
| 78 |
-
1. Chess.com foydalanuvchi nomingizni kiriting
|
| 79 |
-
2. "O'yinlarni tahlil qilish" tugmasini bosing
|
| 80 |
-
3. Dastur so'nggi 50 ta blitz o'yiningizni tahlil qiladi
|
| 81 |
-
4. Zaif tomonlaringiz va shaxsiy masalalarni ko'ring
|
| 82 |
-
|
| 83 |
-
**Yoki** PGN faylni yuklashingiz mumkin
|
| 84 |
-
"""
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
# ============= PUZZLE TEMALARI =============
|
| 88 |
-
|
| 89 |
-
PUZZLE_THEMES = {
|
| 90 |
-
'blunder': 'mix',
|
| 91 |
-
'hanging_piece': 'hangingPiece',
|
| 92 |
-
'tactical_miss': 'fork,pin,skewer',
|
| 93 |
-
'positional_error': 'advantage',
|
| 94 |
-
'opening_mistake': 'opening',
|
| 95 |
-
'middlegame_mistake': 'middlegame',
|
| 96 |
-
'endgame_mistake': 'endgame'
|
| 97 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core/ai_integration.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from collections import defaultdict
|
| 5 |
+
import logging
|
| 6 |
+
import genai
|
| 7 |
+
|
| 8 |
+
GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
|
| 9 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 10 |
+
model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
| 11 |
+
|
| 12 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def get_comprehensive_analysis(weaknesses, opening_stats, color_stats, total_games):
|
| 17 |
+
weakness_text = "\n".join([f"- {w['category']}: {w['count']} marta ({w['percentage']:.1f}%)" for w in weaknesses])
|
| 18 |
+
|
| 19 |
+
opening_text = "\n".join([f"- {opening}: {stats['total']} o'yin (G'alabalar: {stats['wins']}, Yutqazishlar: {stats['losses']}, Duranglar: {stats['draws']})"
|
| 20 |
+
for opening, stats in list(opening_stats.items())[:5]])
|
| 21 |
+
|
| 22 |
+
color_text = f"Oq rangda: {color_stats['white']['wins']}G-{color_stats['white']['losses']}Y-{color_stats['white']['draws']}D\n"
|
| 23 |
+
color_text += f"Qora rangda: {color_stats['black']['wins']}G-{color_stats['black']['losses']}Y-{color_stats['black']['draws']}D"
|
| 24 |
+
|
| 25 |
+
prompt = f"""Siz professional ΡΠ°Ρ
ΠΌΠ°Ρ murabbiy va tahlilchisiz. O'yinchining {total_games} ta o'yinini tahlil qildingiz.
|
| 26 |
+
|
| 27 |
+
STATISTIKA:
|
| 28 |
+
|
| 29 |
+
Zaif tomonlar:
|
| 30 |
+
{weakness_text}
|
| 31 |
+
|
| 32 |
+
Eng ko'p o'ynaladigan debyutlar:
|
| 33 |
+
{opening_text}
|
| 34 |
+
|
| 35 |
+
Rang bo'yicha natijalar:
|
| 36 |
+
{color_text}
|
| 37 |
+
|
| 38 |
+
Quyidagilarni taqdim eting:
|
| 39 |
+
|
| 40 |
+
1. **ZAIF TOMONLAR TAHLILI**: Har bir zaif tomonni chuqur tahlil qiling va nima uchun bu muammo kelib chiqayotganini tushuntiring.
|
| 41 |
+
|
| 42 |
+
2. **SHAXSIY O'QUV REJASI**: Kundalik mashg'ulotlar rejasini tuzing:
|
| 43 |
+
- Har kuni nechta masala yechish kerak va qanday turdagi masalalar
|
| 44 |
+
- Qaysi debyutlarni o'rganish kerak
|
| 45 |
+
- Qaysi o'yin bosqichiga ko'proq e'tibor berish kerak
|
| 46 |
+
- Kompyuter yoki botlar bilan qanday mashq qilish kerak
|
| 47 |
+
|
| 48 |
+
3. **TAVSIYA ETILGAN RESURSLAR**:
|
| 49 |
+
- Kitoblar (muallif va nom bilan)
|
| 50 |
+
- Onlayn kurslar (Uzchess, Chess.com, Lichess)
|
| 51 |
+
- YouTube kanallari
|
| 52 |
+
- Mashq uchun maxsus botlar yoki dasturlar
|
| 53 |
+
|
| 54 |
+
4. **DEBYUT TAVSIYALARI**: Statistikaga asoslanib, qaysi debyutlarni davom ettirish va qaysilarini o'zgartirish kerak.
|
| 55 |
+
|
| 56 |
+
5. **MOTIVATSION XULOSA**: Qisqa va rag'batlantiruvchi xulosa.
|
| 57 |
+
|
| 58 |
+
MUHIM: Javobni FAQAT O'ZBEK TILIDA yozing! Aniq va amaliy maslahatlar bering."""
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
response = model.generate_content(prompt)
|
| 62 |
+
return response.text
|
| 63 |
+
except Exception as e:
|
| 64 |
+
logger.error(f"AI analysis failed: {str(e)}")
|
| 65 |
+
return f"AI tahlil hozircha mavjud emas: {str(e)}"
|
core/chess_api.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import requests
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_user_games_from_chess_com(username):
|
| 10 |
+
try:
|
| 11 |
+
logger.info(f"Fetching games for: {username}")
|
| 12 |
+
username = username.strip().lower()
|
| 13 |
+
|
| 14 |
+
user_url = f"https://api.chess.com/pub/player/{username}"
|
| 15 |
+
response = requests.get(user_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 16 |
+
|
| 17 |
+
if response.status_code != 200:
|
| 18 |
+
return None, f"β Foydalanuvchi topilmadi: {username}"
|
| 19 |
+
|
| 20 |
+
archives_url = f"https://api.chess.com/pub/player/{username}/games/archives"
|
| 21 |
+
response = requests.get(archives_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 22 |
+
|
| 23 |
+
if response.status_code != 200:
|
| 24 |
+
return None, "β O'yinlar arxivi topilmadi."
|
| 25 |
+
|
| 26 |
+
archives = response.json()['archives']
|
| 27 |
+
if not archives:
|
| 28 |
+
return None, "β O'yinlar topilmadi."
|
| 29 |
+
|
| 30 |
+
all_games = []
|
| 31 |
+
for archive_url in reversed(archives[-3:]):
|
| 32 |
+
time.sleep(0.3)
|
| 33 |
+
response = requests.get(archive_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 34 |
+
if response.status_code == 200:
|
| 35 |
+
games = response.json()['games']
|
| 36 |
+
all_games.extend(games)
|
| 37 |
+
if len(all_games) >= 50:
|
| 38 |
+
break
|
| 39 |
+
|
| 40 |
+
rapid_games = [g for g in all_games if g.get('time_class') in ['rapid', 'blitz']]
|
| 41 |
+
if not rapid_games:
|
| 42 |
+
rapid_games = all_games[:50]
|
| 43 |
+
|
| 44 |
+
pgn_list = [g['pgn'] for g in rapid_games[:50] if 'pgn' in g]
|
| 45 |
+
|
| 46 |
+
if not pgn_list:
|
| 47 |
+
return None, "β PGN formatdagi o'yinlar topilmadi."
|
| 48 |
+
|
| 49 |
+
return pgn_list, None
|
| 50 |
+
|
| 51 |
+
except Exception as e:
|
| 52 |
+
logger.error(f"Error fetching games: {str(e)}")
|
| 53 |
+
return None, f"β Xatolik: {str(e)}"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def fetch_lichess_puzzles(themes, count=5):
|
| 57 |
+
puzzles = []
|
| 58 |
+
|
| 59 |
+
try:
|
| 60 |
+
url = "https://lichess.org/training"
|
| 61 |
+
for i in range(count):
|
| 62 |
+
puzzles.append({
|
| 63 |
+
'id': f'puzzle_{i+1}',
|
| 64 |
+
'url': url,
|
| 65 |
+
'theme': themes[0] if themes else 'Tactics',
|
| 66 |
+
'rating': 1500
|
| 67 |
+
})
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.error(f"Error generating puzzle links: {str(e)}")
|
| 70 |
+
|
| 71 |
+
return puzzles
|
core/core.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import chess
|
| 3 |
+
import chess.pgn
|
| 4 |
+
import io
|
| 5 |
+
import re
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
from core.ai_integration import get_comprehensive_analysis
|
| 8 |
+
from core.openings import detect_opening, load_opening_database
|
| 9 |
+
from core.chess_api import get_user_games_from_chess_com, fetch_lichess_puzzles
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def analyze_games(username_chesscom, pgn_file, username_pgn):
|
| 14 |
+
actual_username = None
|
| 15 |
+
pgn_content = None
|
| 16 |
+
|
| 17 |
+
if username_chesscom:
|
| 18 |
+
pgn_content, error = get_user_games_from_chess_com(username_chesscom)
|
| 19 |
+
if error:
|
| 20 |
+
return error, "", "", "", None, None, None, None, None
|
| 21 |
+
actual_username = username_chesscom
|
| 22 |
+
|
| 23 |
+
elif pgn_file:
|
| 24 |
+
pgn_content = pgn_file.decode('utf-8') if isinstance(pgn_file, bytes) else pgn_file
|
| 25 |
+
|
| 26 |
+
if username_pgn and username_pgn.strip():
|
| 27 |
+
actual_username = username_pgn.strip()
|
| 28 |
+
else:
|
| 29 |
+
try:
|
| 30 |
+
first_game = chess.pgn.read_game(io.StringIO(pgn_content))
|
| 31 |
+
if first_game:
|
| 32 |
+
white = first_game.headers.get("White", "")
|
| 33 |
+
black = first_game.headers.get("Black", "")
|
| 34 |
+
actual_username = white if white else black if black else "Player"
|
| 35 |
+
else:
|
| 36 |
+
actual_username = "Player"
|
| 37 |
+
except:
|
| 38 |
+
actual_username = "Player"
|
| 39 |
+
|
| 40 |
+
else:
|
| 41 |
+
return "β Chess.com foydalanuvchi nomini kiriting yoki PGN faylni yuklang", "", "", "", None, None, None, None, None
|
| 42 |
+
|
| 43 |
+
games = parse_pgn_content(pgn_content)
|
| 44 |
+
|
| 45 |
+
if not games:
|
| 46 |
+
return "β O'yinlar topilmadi yoki tahlil qilinmadi", "", "", "", None, None, None, None, None
|
| 47 |
+
|
| 48 |
+
all_analyses = []
|
| 49 |
+
opening_stats = defaultdict(lambda: {'wins': 0, 'losses': 0, 'draws': 0, 'total': 0})
|
| 50 |
+
color_stats = {
|
| 51 |
+
'white': {'wins': 0, 'losses': 0, 'draws': 0},
|
| 52 |
+
'black': {'wins': 0, 'losses': 0, 'draws': 0}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
for game in games:
|
| 56 |
+
analysis = analyze_game_detailed(game, actual_username)
|
| 57 |
+
all_analyses.append(analysis)
|
| 58 |
+
|
| 59 |
+
opening = analysis['opening']
|
| 60 |
+
user_result = analysis.get('user_result')
|
| 61 |
+
user_color = analysis['user_color']
|
| 62 |
+
|
| 63 |
+
if user_color is not None:
|
| 64 |
+
opening_stats[opening]['total'] += 1
|
| 65 |
+
|
| 66 |
+
if user_result == 'win':
|
| 67 |
+
opening_stats[opening]['wins'] += 1
|
| 68 |
+
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 69 |
+
color_stats[color_key]['wins'] += 1
|
| 70 |
+
elif user_result == 'loss':
|
| 71 |
+
opening_stats[opening]['losses'] += 1
|
| 72 |
+
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 73 |
+
color_stats[color_key]['losses'] += 1
|
| 74 |
+
elif user_result == 'draw':
|
| 75 |
+
opening_stats[opening]['draws'] += 1
|
| 76 |
+
color_key = 'white' if user_color == chess.WHITE else 'black'
|
| 77 |
+
color_stats[color_key]['draws'] += 1
|
| 78 |
+
|
| 79 |
+
weaknesses = categorize_mistakes(all_analyses)
|
| 80 |
+
|
| 81 |
+
all_mistakes = []
|
| 82 |
+
for analysis in all_analyses:
|
| 83 |
+
all_mistakes.extend(analysis.get('mistakes', []))
|
| 84 |
+
|
| 85 |
+
stats_report = f"## π {len(games)} ta o'yin tahlili\n\n"
|
| 86 |
+
stats_report += f"**Jami xatolar:** {len(all_mistakes)} ta\n\n"
|
| 87 |
+
|
| 88 |
+
stats_report += "### π― Eng zaif 3 tomoningiz:\n\n"
|
| 89 |
+
if weaknesses:
|
| 90 |
+
for i, w in enumerate(weaknesses[:3], 1):
|
| 91 |
+
stats_report += f"**{i}. {w['category']}** - {w['count']} marta ({w['percentage']:.1f}%)\n"
|
| 92 |
+
else:
|
| 93 |
+
stats_report += "Xatolar topilmadi yoki tahlil qilinmadi.\n"
|
| 94 |
+
|
| 95 |
+
opening_report = "\n\n## π Debyut Statistikasi\n\n"
|
| 96 |
+
sorted_openings = sorted(opening_stats.items(), key=lambda x: x[1]['total'], reverse=True)[:10]
|
| 97 |
+
|
| 98 |
+
for opening, stats in sorted_openings:
|
| 99 |
+
total = stats['total']
|
| 100 |
+
wins = stats['wins']
|
| 101 |
+
losses = stats['losses']
|
| 102 |
+
draws = stats['draws']
|
| 103 |
+
win_rate = (wins / total * 100) if total > 0 else 0
|
| 104 |
+
|
| 105 |
+
opening_report += f"**{opening}** ({total} o'yin)\n"
|
| 106 |
+
opening_report += f"- G'alabalar: {wins} ({win_rate:.1f}%) | Yutqazishlar: {losses} | Duranglar: {draws}\n\n"
|
| 107 |
+
|
| 108 |
+
color_report = "\n\n## βͺβ« Rang bo'yicha natijalar\n\n"
|
| 109 |
+
|
| 110 |
+
white_total = sum(color_stats['white'].values())
|
| 111 |
+
black_total = sum(color_stats['black'].values())
|
| 112 |
+
|
| 113 |
+
if white_total > 0:
|
| 114 |
+
white_wr = color_stats['white']['wins'] / white_total * 100
|
| 115 |
+
color_report += f"**Oq figuralar bilan:**\n"
|
| 116 |
+
color_report += f"- G'alabalar: {color_stats['white']['wins']} ({white_wr:.1f}%)\n"
|
| 117 |
+
color_report += f"- Yutqazishlar: {color_stats['white']['losses']}\n"
|
| 118 |
+
color_report += f"- Duranglar: {color_stats['white']['draws']}\n\n"
|
| 119 |
+
|
| 120 |
+
if black_total > 0:
|
| 121 |
+
black_wr = color_stats['black']['wins'] / black_total * 100
|
| 122 |
+
color_report += f"**Qora figuralar bilan:**\n"
|
| 123 |
+
color_report += f"- G'alabalar: {color_stats['black']['wins']} ({black_wr:.1f}%)\n"
|
| 124 |
+
color_report += f"- Yutqazishlar: {color_stats['black']['losses']}\n"
|
| 125 |
+
color_report += f"- Duranglar: {color_stats['black']['draws']}\n"
|
| 126 |
+
|
| 127 |
+
full_report = stats_report + opening_report + color_report
|
| 128 |
+
|
| 129 |
+
ai_analysis = get_comprehensive_analysis(weaknesses, opening_stats, color_stats, len(games))
|
| 130 |
+
ai_report = f"## π€ AI Murabbiy: To'liq Tahlil va O'quv Rejasi\n\n{ai_analysis}"
|
| 131 |
+
|
| 132 |
+
weakness_themes = [w['category'] for w in weaknesses[:3]]
|
| 133 |
+
puzzles = fetch_lichess_puzzles(weakness_themes, count=5)
|
| 134 |
+
|
| 135 |
+
puzzle_text = "## π§© Sizning shaxsiy masalalaringiz\n\n"
|
| 136 |
+
puzzle_text += "Masalalarni yechish uchun quyidagi havoladan foydalaning:\n\n"
|
| 137 |
+
for i, puzzle in enumerate(puzzles, 1):
|
| 138 |
+
theme = puzzle.get('theme', 'Tactics')
|
| 139 |
+
url = puzzle.get('url', 'https://lichess.org/training')
|
| 140 |
+
puzzle_text += f"**Puzzle {i}: {theme}**\n"
|
| 141 |
+
puzzle_text += f"- [Lichess Training]({url})\n\n"
|
| 142 |
+
|
| 143 |
+
return (
|
| 144 |
+
full_report,
|
| 145 |
+
ai_report,
|
| 146 |
+
puzzle_text,
|
| 147 |
+
"",
|
| 148 |
+
None,
|
| 149 |
+
"",
|
| 150 |
+
None,
|
| 151 |
+
"",
|
| 152 |
+
None
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def parse_pgn_content(pgn_content):
|
| 157 |
+
games = []
|
| 158 |
+
if isinstance(pgn_content, list):
|
| 159 |
+
for pgn_text in pgn_content:
|
| 160 |
+
try:
|
| 161 |
+
game = chess.pgn.read_game(io.StringIO(pgn_text))
|
| 162 |
+
if game:
|
| 163 |
+
games.append(game)
|
| 164 |
+
except:
|
| 165 |
+
pass
|
| 166 |
+
else:
|
| 167 |
+
pgn_io = io.StringIO(pgn_content)
|
| 168 |
+
while True:
|
| 169 |
+
try:
|
| 170 |
+
game = chess.pgn.read_game(pgn_io)
|
| 171 |
+
if game is None:
|
| 172 |
+
break
|
| 173 |
+
games.append(game)
|
| 174 |
+
except:
|
| 175 |
+
break
|
| 176 |
+
|
| 177 |
+
return games
|
| 178 |
+
|
| 179 |
+
def analyze_game_detailed(game, username):
|
| 180 |
+
board = game.board()
|
| 181 |
+
mistakes = []
|
| 182 |
+
move_number = 0
|
| 183 |
+
|
| 184 |
+
white_player = game.headers.get("White", "").strip().lower()
|
| 185 |
+
black_player = game.headers.get("Black", "").strip().lower()
|
| 186 |
+
username_lower = username.strip().lower()
|
| 187 |
+
|
| 188 |
+
user_color = None
|
| 189 |
+
if username_lower == white_player:
|
| 190 |
+
user_color = chess.WHITE
|
| 191 |
+
elif username_lower == black_player:
|
| 192 |
+
user_color = chess.BLACK
|
| 193 |
+
|
| 194 |
+
result = game.headers.get("Result", "*")
|
| 195 |
+
opening = detect_opening(game)
|
| 196 |
+
|
| 197 |
+
user_result = None
|
| 198 |
+
if user_color is not None and result != "*":
|
| 199 |
+
if result == "1-0":
|
| 200 |
+
user_result = "win" if user_color == chess.WHITE else "loss"
|
| 201 |
+
elif result == "0-1":
|
| 202 |
+
user_result = "win" if user_color == chess.BLACK else "loss"
|
| 203 |
+
elif result == "1/2-1/2":
|
| 204 |
+
user_result = "draw"
|
| 205 |
+
|
| 206 |
+
material_values = {chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3, chess.ROOK: 5, chess.QUEEN: 9}
|
| 207 |
+
|
| 208 |
+
def count_material(board):
|
| 209 |
+
total = 0
|
| 210 |
+
for piece_type in material_values:
|
| 211 |
+
total += len(board.pieces(piece_type, chess.WHITE)) * material_values[piece_type]
|
| 212 |
+
total -= len(board.pieces(piece_type, chess.BLACK)) * material_values[piece_type]
|
| 213 |
+
return total
|
| 214 |
+
|
| 215 |
+
for move in game.mainline_moves():
|
| 216 |
+
move_number += 1
|
| 217 |
+
|
| 218 |
+
if user_color is not None and board.turn != user_color:
|
| 219 |
+
board.push(move)
|
| 220 |
+
continue
|
| 221 |
+
|
| 222 |
+
material_before = count_material(board)
|
| 223 |
+
board.push(move)
|
| 224 |
+
material_after = count_material(board)
|
| 225 |
+
|
| 226 |
+
material_loss = abs(material_after - material_before) if board.turn == chess.WHITE else abs(material_before - material_after)
|
| 227 |
+
|
| 228 |
+
moved_piece = board.piece_at(move.to_square)
|
| 229 |
+
mistake_type = None
|
| 230 |
+
|
| 231 |
+
if material_loss >= 3:
|
| 232 |
+
mistake_type = 'blunder'
|
| 233 |
+
elif material_loss >= 1:
|
| 234 |
+
mistake_type = 'mistake'
|
| 235 |
+
elif moved_piece and board.is_attacked_by(not board.turn, move.to_square):
|
| 236 |
+
attackers = len(board.attackers(not board.turn, move.to_square))
|
| 237 |
+
defenders = len(board.attackers(board.turn, move.to_square))
|
| 238 |
+
if attackers > defenders:
|
| 239 |
+
mistake_type = 'hanging_piece'
|
| 240 |
+
|
| 241 |
+
if move_number <= 10:
|
| 242 |
+
phase = 'opening'
|
| 243 |
+
elif len(board.piece_map()) <= 10:
|
| 244 |
+
phase = 'endgame'
|
| 245 |
+
else:
|
| 246 |
+
phase = 'middlegame'
|
| 247 |
+
|
| 248 |
+
if mistake_type:
|
| 249 |
+
mistakes.append({
|
| 250 |
+
'type': mistake_type,
|
| 251 |
+
'phase': phase,
|
| 252 |
+
'move_number': move_number
|
| 253 |
+
})
|
| 254 |
+
|
| 255 |
+
return {
|
| 256 |
+
'mistakes': mistakes,
|
| 257 |
+
'opening': opening,
|
| 258 |
+
'result': result,
|
| 259 |
+
'user_color': user_color,
|
| 260 |
+
'user_result': user_result
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
def categorize_mistakes(all_analyses):
|
| 264 |
+
if not all_analyses:
|
| 265 |
+
return []
|
| 266 |
+
|
| 267 |
+
blunders = 0
|
| 268 |
+
regular_mistakes = 0
|
| 269 |
+
hanging = 0
|
| 270 |
+
opening = 0
|
| 271 |
+
middlegame = 0
|
| 272 |
+
endgame = 0
|
| 273 |
+
|
| 274 |
+
for analysis in all_analyses:
|
| 275 |
+
for mistake in analysis.get('mistakes', []):
|
| 276 |
+
mistake_type = mistake.get('type')
|
| 277 |
+
phase = mistake.get('phase')
|
| 278 |
+
|
| 279 |
+
if mistake_type == 'blunder':
|
| 280 |
+
blunders += 1
|
| 281 |
+
elif mistake_type == 'mistake':
|
| 282 |
+
regular_mistakes += 1
|
| 283 |
+
elif mistake_type == 'hanging_piece':
|
| 284 |
+
hanging += 1
|
| 285 |
+
|
| 286 |
+
if phase == 'opening':
|
| 287 |
+
opening += 1
|
| 288 |
+
elif phase == 'middlegame':
|
| 289 |
+
middlegame += 1
|
| 290 |
+
elif phase == 'endgame':
|
| 291 |
+
endgame += 1
|
| 292 |
+
|
| 293 |
+
total = blunders + regular_mistakes + hanging + opening + middlegame + endgame
|
| 294 |
+
|
| 295 |
+
if total == 0:
|
| 296 |
+
return []
|
| 297 |
+
|
| 298 |
+
weaknesses = []
|
| 299 |
+
|
| 300 |
+
if blunders > 0:
|
| 301 |
+
weaknesses.append({
|
| 302 |
+
'category': "Qo'pol xatolar",
|
| 303 |
+
'count': blunders,
|
| 304 |
+
'percentage': (blunders / total * 100)
|
| 305 |
+
})
|
| 306 |
+
|
| 307 |
+
if regular_mistakes > 0:
|
| 308 |
+
weaknesses.append({
|
| 309 |
+
'category': 'Kichik xatolar',
|
| 310 |
+
'count': regular_mistakes,
|
| 311 |
+
'percentage': (regular_mistakes / total * 100)
|
| 312 |
+
})
|
| 313 |
+
|
| 314 |
+
if hanging > 0:
|
| 315 |
+
weaknesses.append({
|
| 316 |
+
'category': 'Himoyasiz qoldirish',
|
| 317 |
+
'count': hanging,
|
| 318 |
+
'percentage': (hanging / total * 100)
|
| 319 |
+
})
|
| 320 |
+
|
| 321 |
+
if opening > 0:
|
| 322 |
+
weaknesses.append({
|
| 323 |
+
'category': 'Debyut xatolari',
|
| 324 |
+
'count': opening,
|
| 325 |
+
'percentage': (opening / total * 100)
|
| 326 |
+
})
|
| 327 |
+
|
| 328 |
+
if middlegame > 0:
|
| 329 |
+
weaknesses.append({
|
| 330 |
+
'category': "O'rta o'yin xatolari",
|
| 331 |
+
'count': middlegame,
|
| 332 |
+
'percentage': (middlegame / total * 100)
|
| 333 |
+
})
|
| 334 |
+
|
| 335 |
+
if endgame > 0:
|
| 336 |
+
weaknesses.append({
|
| 337 |
+
'category': 'Endshpil xatolari',
|
| 338 |
+
'count': endgame,
|
| 339 |
+
'percentage': (endgame / total * 100)
|
| 340 |
+
})
|
| 341 |
+
|
| 342 |
+
weaknesses.sort(key=lambda x: x['count'], reverse=True)
|
| 343 |
+
|
| 344 |
+
return weaknesses
|
core/openings.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import csv
|
| 3 |
+
import logging
|
| 4 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
def load_opening_database():
|
| 8 |
+
openings = {}
|
| 9 |
+
try:
|
| 10 |
+
with open('data/Chess Opening Reference - Sheet1.csv', 'r', encoding='utf-8') as f:
|
| 11 |
+
reader = csv.DictReader(f)
|
| 12 |
+
for row in reader:
|
| 13 |
+
eco = row.get('ECO Code', '').strip()
|
| 14 |
+
name = row.get('Name', '').strip()
|
| 15 |
+
if eco and name:
|
| 16 |
+
openings[eco] = name
|
| 17 |
+
except Exception as e:
|
| 18 |
+
logger.error(f"Failed to load openings CSV: {e}")
|
| 19 |
+
return openings
|
| 20 |
+
|
| 21 |
+
OPENING_DB = load_opening_database()
|
| 22 |
+
|
| 23 |
+
def detect_opening(game):
|
| 24 |
+
eco = game.headers.get("ECO", "")
|
| 25 |
+
opening = game.headers.get("Opening", "")
|
| 26 |
+
|
| 27 |
+
if eco and eco in OPENING_DB:
|
| 28 |
+
return OPENING_DB[eco]
|
| 29 |
+
elif opening:
|
| 30 |
+
return opening
|
| 31 |
+
else:
|
| 32 |
+
return "Unknown Opening"
|
Chess Opening Reference - Sheet1.csv β data/Chess Opening Reference - Sheet1.csv
RENAMED
|
File without changes
|
handlers.py
DELETED
|
@@ -1,378 +0,0 @@
|
|
| 1 |
-
import chess
|
| 2 |
-
import chess.pgn
|
| 3 |
-
import chess.engine
|
| 4 |
-
import io
|
| 5 |
-
import requests
|
| 6 |
-
import google.generativeai as genai
|
| 7 |
-
from collections import Counter
|
| 8 |
-
import logging
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
import time
|
| 11 |
-
import config
|
| 12 |
-
|
| 13 |
-
# Logging sozlash
|
| 14 |
-
logging.basicConfig(
|
| 15 |
-
level=logging.INFO,
|
| 16 |
-
format='%(asctime)s - %(levelname)s - %(message)s'
|
| 17 |
-
)
|
| 18 |
-
logger = logging.getLogger(__name__)
|
| 19 |
-
|
| 20 |
-
# Gemini sozlash
|
| 21 |
-
genai.configure(api_key=config.GEMINI_API_KEY)
|
| 22 |
-
model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
| 23 |
-
|
| 24 |
-
# ============= CHESS.COM DAN O'YINLARNI OLISH =============
|
| 25 |
-
|
| 26 |
-
def get_user_games(username):
|
| 27 |
-
"""Chess.com dan foydalanuvchi o'yinlarini olish"""
|
| 28 |
-
logger.info(f"Foydalanuvchi o'yinlarini olish: {username}")
|
| 29 |
-
|
| 30 |
-
try:
|
| 31 |
-
# Foydalanuvchini tekshirish
|
| 32 |
-
user_url = f"{config.CHESS_COM_API_BASE}/player/{username}"
|
| 33 |
-
logger.info(f"Foydalanuvchini tekshirish: {user_url}")
|
| 34 |
-
|
| 35 |
-
response = requests.get(user_url, timeout=config.REQUEST_TIMEOUT)
|
| 36 |
-
if response.status_code != 200:
|
| 37 |
-
logger.error(f"Foydalanuvchi topilmadi: {username}")
|
| 38 |
-
return None, "β Foydalanuvchi topilmadi. Nomni tekshiring."
|
| 39 |
-
|
| 40 |
-
logger.info(f"Foydalanuvchi topildi: {username}")
|
| 41 |
-
|
| 42 |
-
# Arxivlarni olish
|
| 43 |
-
archives_url = f"{config.CHESS_COM_API_BASE}/player/{username}/games/archives"
|
| 44 |
-
logger.info(f"Arxivlarni olish: {archives_url}")
|
| 45 |
-
|
| 46 |
-
response = requests.get(archives_url, timeout=config.REQUEST_TIMEOUT)
|
| 47 |
-
if response.status_code != 200:
|
| 48 |
-
logger.error("Arxivlar olinmadi")
|
| 49 |
-
return None, "β O'yinlar arxivi topilmadi."
|
| 50 |
-
|
| 51 |
-
archives = response.json()['archives']
|
| 52 |
-
if not archives:
|
| 53 |
-
logger.warning("Arxivlar bo'sh")
|
| 54 |
-
return None, "β O'yinlar topilmadi."
|
| 55 |
-
|
| 56 |
-
logger.info(f"Jami arxivlar: {len(archives)}")
|
| 57 |
-
|
| 58 |
-
# So'nggi oylarning o'yinlarini olish
|
| 59 |
-
all_games = []
|
| 60 |
-
for archive_url in reversed(archives[-3:]): # So'nggi 3 oy
|
| 61 |
-
logger.info(f"Arxivdan o'yinlarni olish: {archive_url}")
|
| 62 |
-
time.sleep(0.5) # Rate limiting
|
| 63 |
-
|
| 64 |
-
response = requests.get(archive_url, timeout=config.REQUEST_TIMEOUT)
|
| 65 |
-
if response.status_code == 200:
|
| 66 |
-
games = response.json()['games']
|
| 67 |
-
all_games.extend(games)
|
| 68 |
-
logger.info(f"Olindi: {len(games)} ta o'yin")
|
| 69 |
-
|
| 70 |
-
if len(all_games) >= config.MAX_GAMES_TO_ANALYZE:
|
| 71 |
-
break
|
| 72 |
-
|
| 73 |
-
# Blitz o'yinlarni filtrlash
|
| 74 |
-
blitz_games = [g for g in all_games if g.get('time_class') == 'blitz']
|
| 75 |
-
logger.info(f"Blitz o'yinlar: {len(blitz_games)}")
|
| 76 |
-
|
| 77 |
-
if not blitz_games:
|
| 78 |
-
# Agar blitz bo'lmasa, bullet olish
|
| 79 |
-
bullet_games = [g for g in all_games if g.get('time_class') == 'bullet']
|
| 80 |
-
logger.info(f"Bullet o'yinlar: {len(bullet_games)}")
|
| 81 |
-
selected_games = bullet_games[:config.MAX_GAMES_TO_ANALYZE]
|
| 82 |
-
else:
|
| 83 |
-
selected_games = blitz_games[:config.MAX_GAMES_TO_ANALYZE]
|
| 84 |
-
|
| 85 |
-
logger.info(f"Tanlangan o'yinlar: {len(selected_games)}")
|
| 86 |
-
|
| 87 |
-
# PGN formatga o'tkazish
|
| 88 |
-
pgn_games = []
|
| 89 |
-
for game in selected_games:
|
| 90 |
-
if 'pgn' in game:
|
| 91 |
-
pgn_games.append(game['pgn'])
|
| 92 |
-
|
| 93 |
-
logger.info(f"PGN formatdagi o'yinlar: {len(pgn_games)}")
|
| 94 |
-
|
| 95 |
-
if not pgn_games:
|
| 96 |
-
return None, "β PGN formatdagi o'yinlar topilmadi."
|
| 97 |
-
|
| 98 |
-
return pgn_games, None
|
| 99 |
-
|
| 100 |
-
except Exception as e:
|
| 101 |
-
logger.error(f"Xatolik yuz berdi: {str(e)}")
|
| 102 |
-
return None, f"β Xatolik: {str(e)}"
|
| 103 |
-
|
| 104 |
-
# ============= PGN TAHLIL =============
|
| 105 |
-
|
| 106 |
-
def parse_pgn_games(pgn_list):
|
| 107 |
-
"""PGN formatdagi o'yinlarni tahlil qilish"""
|
| 108 |
-
logger.info(f"PGN tahlil qilish: {len(pgn_list)} ta o'yin")
|
| 109 |
-
|
| 110 |
-
games = []
|
| 111 |
-
for i, pgn_text in enumerate(pgn_list):
|
| 112 |
-
try:
|
| 113 |
-
pgn_io = io.StringIO(pgn_text)
|
| 114 |
-
game = chess.pgn.read_game(pgn_io)
|
| 115 |
-
if game:
|
| 116 |
-
games.append(game)
|
| 117 |
-
logger.info(f"O'yin {i+1} tahlil qilindi")
|
| 118 |
-
except Exception as e:
|
| 119 |
-
logger.error(f"O'yin {i+1} tahlil xatosi: {str(e)}")
|
| 120 |
-
|
| 121 |
-
logger.info(f"Jami tahlil qilindi: {len(games)} ta o'yin")
|
| 122 |
-
return games
|
| 123 |
-
|
| 124 |
-
# ============= STOCKFISH BILAN TAHLIL =============
|
| 125 |
-
|
| 126 |
-
def analyze_game_with_engine(game, username):
|
| 127 |
-
"""Stockfish yordamida o'yinni tahlil qilish"""
|
| 128 |
-
logger.info("Stockfish bilan tahlil boshlandi")
|
| 129 |
-
|
| 130 |
-
try:
|
| 131 |
-
# Stockfish ni ishga tushirish
|
| 132 |
-
engine = chess.engine.SimpleEngine.popen_uci(config.STOCKFISH_PATH)
|
| 133 |
-
logger.info("Stockfish muvaffaqiyatli ishga tushdi")
|
| 134 |
-
|
| 135 |
-
board = game.board()
|
| 136 |
-
mistakes = []
|
| 137 |
-
move_number = 0
|
| 138 |
-
|
| 139 |
-
# Foydalanuvchi qaysi rangda o'ynaganini aniqlash
|
| 140 |
-
white_player = game.headers.get("White", "").lower()
|
| 141 |
-
black_player = game.headers.get("Black", "").lower()
|
| 142 |
-
user_color = None
|
| 143 |
-
|
| 144 |
-
if username.lower() in white_player:
|
| 145 |
-
user_color = chess.WHITE
|
| 146 |
-
logger.info(f"Foydalanuvchi oq rangda: {username}")
|
| 147 |
-
elif username.lower() in black_player:
|
| 148 |
-
user_color = chess.BLACK
|
| 149 |
-
logger.info(f"Foydalanuvchi qora rangda: {username}")
|
| 150 |
-
|
| 151 |
-
# Har bir harakatni tahlil qilish
|
| 152 |
-
for move in game.mainline_moves():
|
| 153 |
-
move_number += 1
|
| 154 |
-
|
| 155 |
-
# Faqat foydalanuvchi harakatlarini tahlil qilish
|
| 156 |
-
if user_color is not None and board.turn != user_color:
|
| 157 |
-
board.push(move)
|
| 158 |
-
continue
|
| 159 |
-
|
| 160 |
-
# Harakat oldin pozitsiyani baholash
|
| 161 |
-
try:
|
| 162 |
-
info_before = engine.analyse(
|
| 163 |
-
board,
|
| 164 |
-
chess.engine.Limit(depth=config.STOCKFISH_DEPTH)
|
| 165 |
-
)
|
| 166 |
-
eval_before = info_before['score'].relative.score(mate_score=10000)
|
| 167 |
-
|
| 168 |
-
# Harakatni qo'llash
|
| 169 |
-
board.push(move)
|
| 170 |
-
|
| 171 |
-
# Harakat keyin pozitsiyani baholash
|
| 172 |
-
info_after = engine.analyse(
|
| 173 |
-
board,
|
| 174 |
-
chess.engine.Limit(depth=config.STOCKFISH_DEPTH)
|
| 175 |
-
)
|
| 176 |
-
eval_after = info_after['score'].relative.score(mate_score=10000)
|
| 177 |
-
|
| 178 |
-
# Baholash farqini hisoblash (foydalanuvchi nuqtai nazaridan)
|
| 179 |
-
eval_diff = eval_before - eval_after
|
| 180 |
-
|
| 181 |
-
# Xatolarni aniqlash
|
| 182 |
-
mistake_type = None
|
| 183 |
-
if eval_diff >= config.BLUNDER_THRESHOLD:
|
| 184 |
-
mistake_type = 'blunder'
|
| 185 |
-
logger.info(f"Qo'pol xato topildi: harakat {move_number}, farq {eval_diff}")
|
| 186 |
-
elif eval_diff >= config.MISTAKE_THRESHOLD:
|
| 187 |
-
mistake_type = 'mistake'
|
| 188 |
-
logger.info(f"Xato topildi: harakat {move_number}, farq {eval_diff}")
|
| 189 |
-
|
| 190 |
-
if mistake_type:
|
| 191 |
-
# Eng yaxshi harakatni topish
|
| 192 |
-
best_move = info_before['pv'][0] if 'pv' in info_before else None
|
| 193 |
-
|
| 194 |
-
mistakes.append({
|
| 195 |
-
'move_number': move_number,
|
| 196 |
-
'move': move.uci(),
|
| 197 |
-
'best_move': best_move.uci() if best_move else None,
|
| 198 |
-
'type': mistake_type,
|
| 199 |
-
'eval_before': eval_before,
|
| 200 |
-
'eval_after': eval_after,
|
| 201 |
-
'eval_diff': eval_diff,
|
| 202 |
-
'fen': board.fen(),
|
| 203 |
-
'game_phase': get_game_phase(board)
|
| 204 |
-
})
|
| 205 |
-
|
| 206 |
-
except Exception as e:
|
| 207 |
-
logger.error(f"Harakat {move_number} tahlil xatosi: {str(e)}")
|
| 208 |
-
board.push(move)
|
| 209 |
-
|
| 210 |
-
engine.quit()
|
| 211 |
-
logger.info(f"Tahlil tugadi. Topildi: {len(mistakes)} ta xato")
|
| 212 |
-
return mistakes
|
| 213 |
-
|
| 214 |
-
except Exception as e:
|
| 215 |
-
logger.error(f"Stockfish xatosi: {str(e)}")
|
| 216 |
-
return []
|
| 217 |
-
|
| 218 |
-
def get_game_phase(board):
|
| 219 |
-
"""O'yin bosqichini aniqlash"""
|
| 220 |
-
move_count = board.fullmove_number
|
| 221 |
-
piece_count = len(board.piece_map())
|
| 222 |
-
|
| 223 |
-
if move_count <= 10:
|
| 224 |
-
return 'opening_mistake'
|
| 225 |
-
elif piece_count <= 10:
|
| 226 |
-
return 'endgame_mistake'
|
| 227 |
-
else:
|
| 228 |
-
return 'middlegame_mistake'
|
| 229 |
-
|
| 230 |
-
# ============= XATOLARNI KATEGORIYALASH =============
|
| 231 |
-
|
| 232 |
-
def categorize_all_mistakes(all_mistakes):
|
| 233 |
-
"""Barcha xatolarni kategoriyalash"""
|
| 234 |
-
logger.info(f"Kategoriyalash: {len(all_mistakes)} ta xato")
|
| 235 |
-
|
| 236 |
-
# Xato turlarini sanash
|
| 237 |
-
mistake_types = []
|
| 238 |
-
for mistake in all_mistakes:
|
| 239 |
-
mistake_types.append(mistake['type'])
|
| 240 |
-
mistake_types.append(mistake['game_phase'])
|
| 241 |
-
|
| 242 |
-
counts = Counter(mistake_types)
|
| 243 |
-
|
| 244 |
-
# Eng ko'p uchragan 3 ta zaif tomonni topish
|
| 245 |
-
top_weaknesses = []
|
| 246 |
-
for mistake_type, count in counts.most_common(5):
|
| 247 |
-
if mistake_type in config.MISTAKE_CATEGORIES:
|
| 248 |
-
percentage = (count / len(all_mistakes) * 100) if all_mistakes else 0
|
| 249 |
-
top_weaknesses.append({
|
| 250 |
-
'category': config.MISTAKE_CATEGORIES[mistake_type],
|
| 251 |
-
'type': mistake_type,
|
| 252 |
-
'count': count,
|
| 253 |
-
'percentage': percentage
|
| 254 |
-
})
|
| 255 |
-
|
| 256 |
-
logger.info(f"Eng ko'p zaif tomonlar: {len(top_weaknesses)}")
|
| 257 |
-
return top_weaknesses[:3]
|
| 258 |
-
|
| 259 |
-
# ============= AI TUSHUNTIRISH =============
|
| 260 |
-
|
| 261 |
-
def get_ai_explanation(weaknesses):
|
| 262 |
-
"""Gemini dan tushuntirish olish"""
|
| 263 |
-
logger.info("AI tushuntirish olish")
|
| 264 |
-
|
| 265 |
-
weakness_text = "\n".join([
|
| 266 |
-
f"- {w['category']}: {w['count']} marta ({w['percentage']:.1f}%)"
|
| 267 |
-
for w in weaknesses
|
| 268 |
-
])
|
| 269 |
-
|
| 270 |
-
prompt = config.WEAKNESS_ANALYSIS_PROMPT.format(weakness_text=weakness_text)
|
| 271 |
-
|
| 272 |
-
try:
|
| 273 |
-
response = model.generate_content(prompt)
|
| 274 |
-
logger.info("AI javob olindi")
|
| 275 |
-
return response.text
|
| 276 |
-
except Exception as e:
|
| 277 |
-
logger.error(f"AI xatosi: {str(e)}")
|
| 278 |
-
return "AI tushuntirish hozircha mavjud emas."
|
| 279 |
-
|
| 280 |
-
# ============= LICHESS DAN MASALALAR OLISH =============
|
| 281 |
-
|
| 282 |
-
def fetch_puzzles_for_weaknesses(weaknesses, count=5):
|
| 283 |
-
"""Zaif tomonlar uchun masalalar olish"""
|
| 284 |
-
logger.info(f"Masalalar olish: {count} ta")
|
| 285 |
-
|
| 286 |
-
puzzles = []
|
| 287 |
-
|
| 288 |
-
for weakness in weaknesses:
|
| 289 |
-
theme = config.PUZZLE_THEMES.get(weakness['type'], 'mix')
|
| 290 |
-
logger.info(f"Tema: {theme}")
|
| 291 |
-
|
| 292 |
-
try:
|
| 293 |
-
# Lichess dan tasodifiy masala olish
|
| 294 |
-
url = f"{config.LICHESS_API_BASE}/puzzle/daily"
|
| 295 |
-
response = requests.get(url, timeout=config.REQUEST_TIMEOUT)
|
| 296 |
-
|
| 297 |
-
if response.status_code == 200:
|
| 298 |
-
data = response.json()
|
| 299 |
-
puzzle = data['puzzle']
|
| 300 |
-
game = data['game']
|
| 301 |
-
|
| 302 |
-
puzzles.append({
|
| 303 |
-
'id': puzzle['id'],
|
| 304 |
-
'fen': game['pgn'].split('\n')[-1], # Oxirgi pozitsiya
|
| 305 |
-
'rating': puzzle['rating'],
|
| 306 |
-
'theme': weakness['category'],
|
| 307 |
-
'solution': puzzle['solution'],
|
| 308 |
-
'url': f"https://lichess.org/training/{puzzle['id']}"
|
| 309 |
-
})
|
| 310 |
-
logger.info(f"Masala olindi: {puzzle['id']}")
|
| 311 |
-
|
| 312 |
-
except Exception as e:
|
| 313 |
-
logger.error(f"Masala olish xatosi: {str(e)}")
|
| 314 |
-
|
| 315 |
-
# Yetarlicha bo'lmasa, standart masalalar qo'shish
|
| 316 |
-
while len(puzzles) < count:
|
| 317 |
-
puzzles.append({
|
| 318 |
-
'id': f'default_{len(puzzles)}',
|
| 319 |
-
'fen': 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
|
| 320 |
-
'rating': 1500,
|
| 321 |
-
'theme': 'Amaliyot',
|
| 322 |
-
'solution': ['e2e4'],
|
| 323 |
-
'url': 'https://lichess.org/training'
|
| 324 |
-
})
|
| 325 |
-
|
| 326 |
-
logger.info(f"Jami masalalar: {len(puzzles)}")
|
| 327 |
-
return puzzles[:count]
|
| 328 |
-
|
| 329 |
-
# ============= ASOSIY TAHLIL FUNKSIYASI =============
|
| 330 |
-
|
| 331 |
-
def analyze_user_games(username):
|
| 332 |
-
"""Foydalanuvchi o'yinlarini to'liq tahlil qilish"""
|
| 333 |
-
logger.info(f"===== TAHLIL BOSHLANDI: {username} =====")
|
| 334 |
-
|
| 335 |
-
# 1. O'yinlarni olish
|
| 336 |
-
pgn_games, error = get_user_games(username)
|
| 337 |
-
if error:
|
| 338 |
-
logger.error(f"O'yinlar olinmadi: {error}")
|
| 339 |
-
return error, "", "", []
|
| 340 |
-
|
| 341 |
-
# 2. PGN tahlil qilish
|
| 342 |
-
games = parse_pgn_games(pgn_games)
|
| 343 |
-
if not games:
|
| 344 |
-
logger.error("O'yinlar tahlil qilinmadi")
|
| 345 |
-
return "β O'yinlar tahlil qilinmadi.", "", "", []
|
| 346 |
-
|
| 347 |
-
# 3. Har bir o'yinni Stockfish bilan tahlil qilish
|
| 348 |
-
all_mistakes = []
|
| 349 |
-
for i, game in enumerate(games[:10], 1): # Birinchi 10 ta
|
| 350 |
-
logger.info(f"O'yin {i}/{len(games[:10])} tahlil qilinmoqda...")
|
| 351 |
-
mistakes = analyze_game_with_engine(game, username)
|
| 352 |
-
all_mistakes.extend(mistakes)
|
| 353 |
-
|
| 354 |
-
if not all_mistakes:
|
| 355 |
-
logger.info("Xatolar topilmadi")
|
| 356 |
-
return "β
Ajoyib! Sizning o'yinlaringizda katta xatolar topilmadi!", "", "", []
|
| 357 |
-
|
| 358 |
-
# 4. Zaif tomonlarni aniqlash
|
| 359 |
-
weaknesses = categorize_all_mistakes(all_mistakes)
|
| 360 |
-
|
| 361 |
-
# 5. Hisobot tayyorlash
|
| 362 |
-
report = f"## π {len(games)} ta o'yin tahlili\n\n"
|
| 363 |
-
report += f"**Topilgan xatolar:** {len(all_mistakes)} ta\n\n"
|
| 364 |
-
report += "### π― Sizning eng zaif 3 tomoningiz:\n\n"
|
| 365 |
-
|
| 366 |
-
for i, w in enumerate(weaknesses, 1):
|
| 367 |
-
report += f"**{i}. {w['category']}**\n"
|
| 368 |
-
report += f" - {w['count']} marta ({w['percentage']:.1f}%)\n\n"
|
| 369 |
-
|
| 370 |
-
# 6. AI tushuntirish
|
| 371 |
-
explanation = get_ai_explanation(weaknesses)
|
| 372 |
-
explanation_text = f"## π€ AI Murabbiy Tahlili\n\n{explanation}"
|
| 373 |
-
|
| 374 |
-
# 7. Masalalar olish
|
| 375 |
-
puzzles = fetch_puzzles_for_weaknesses(weaknesses, count=5)
|
| 376 |
-
|
| 377 |
-
logger.info("===== TAHLIL TUGADI =====")
|
| 378 |
-
return report, explanation_text, "", puzzles
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
openings.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
def get_opening_name_from_eco(eco_code):
|
| 3 |
-
eco_openings = {
|
| 4 |
-
'A00': 'Uncommon Opening',
|
| 5 |
-
'A01': "Nimzowitsch-Larsen Attack",
|
| 6 |
-
'A02': "Bird's Opening",
|
| 7 |
-
'A03': "Bird's Opening: Dutch Variation",
|
| 8 |
-
'A04': 'Reti Opening',
|
| 9 |
-
'A05': 'Reti Opening: 1...Nf6',
|
| 10 |
-
'A06': 'Reti Opening: 2.b3',
|
| 11 |
-
'A10': 'English Opening',
|
| 12 |
-
'A15': 'English Opening: Anglo-Indian Defense',
|
| 13 |
-
'A20': 'English Opening: 1...e5',
|
| 14 |
-
'A30': 'English Opening: Symmetrical Variation',
|
| 15 |
-
"A34": "English Opening: Botvinnik System",
|
| 16 |
-
"A36": "English Opening: King's English Variation",
|
| 17 |
-
'A40': 'Queen Pawn Game',
|
| 18 |
-
'A45': 'Indian Defense',
|
| 19 |
-
'A46': 'Indian Defense: 2.Nf3',
|
| 20 |
-
'A50': 'Indian Defense: Normal Variation',
|
| 21 |
-
'B00': 'Uncommon King Pawn Opening',
|
| 22 |
-
'B01': 'Scandinavian Defense',
|
| 23 |
-
'B02': "Alekhine's Defense",
|
| 24 |
-
'B03': "Alekhine's Defense: Four Pawns Attack",
|
| 25 |
-
'B10': 'Caro-Kann Defense',
|
| 26 |
-
'B12': 'Caro-Kann Defense: Advance Variation',
|
| 27 |
-
"B13": 'Caro-Kann Defense: Classical Variation',
|
| 28 |
-
"B14": 'Caro-Kann Defense: Panov-Botvinnik Attack',
|
| 29 |
-
"B15": 'Caro-Kann Defense: Panov-Botvinnik Attack',
|
| 30 |
-
'B20': 'Sicilian Defense',
|
| 31 |
-
'B22': 'Sicilian Defense: Alapin Variation',
|
| 32 |
-
'B23': 'Sicilian Defense: Closed',
|
| 33 |
-
'B30': 'Sicilian Defense: 2...Nc6',
|
| 34 |
-
"B32": 'Sicilian Defense: Hyperaccelerated Dragon',
|
| 35 |
-
'B40': 'Sicilian Defense: French Variation',
|
| 36 |
-
'B50': 'Sicilian Defense: 2...d6',
|
| 37 |
-
'B90': 'Sicilian Defense: Najdorf',
|
| 38 |
-
'C00': 'French Defense',
|
| 39 |
-
"C01": 'French Defense: Exchange Variation',
|
| 40 |
-
'C02': 'French Defense: Advance Variation',
|
| 41 |
-
'C10': 'French Defense: Rubinstein Variation',
|
| 42 |
-
'C20': 'King Pawn Game',
|
| 43 |
-
'C30': "King's Gambit",
|
| 44 |
-
'C40': "King's Knight Opening",
|
| 45 |
-
'C41': 'Philidor Defense',
|
| 46 |
-
'C42': 'Russian Game (Petrov Defense)',
|
| 47 |
-
'C44': 'Scotch Game',
|
| 48 |
-
'C50': 'Italian Game',
|
| 49 |
-
"C54": "Italian Game: Giuoco Piano",
|
| 50 |
-
'C55': 'Italian Game: Two Knights Defense',
|
| 51 |
-
'C60': 'Spanish Opening (Ruy Lopez)',
|
| 52 |
-
'C65': 'Spanish Opening: Berlin Defense',
|
| 53 |
-
'C70': 'Spanish Opening',
|
| 54 |
-
'C78': 'Spanish Opening: Morphy Defense',
|
| 55 |
-
'C80': 'Spanish Opening: Open Variation',
|
| 56 |
-
'D00': 'Queen Pawn Game',
|
| 57 |
-
'D02': 'Queen Pawn Game: 2.Nf3',
|
| 58 |
-
'D10': 'Slav Defense',
|
| 59 |
-
'D20': "Queen's Gambit Accepted",
|
| 60 |
-
'D30': "Queen's Gambit Declined",
|
| 61 |
-
'D50': "Queen's Gambit Declined: 4.Bg5",
|
| 62 |
-
'E00': 'Indian Defense',
|
| 63 |
-
'E10': 'Indian Defense: 3.Nf3',
|
| 64 |
-
'E20': 'Nimzo-Indian Defense',
|
| 65 |
-
'E30': 'Nimzo-Indian Defense: Leningrad Variation',
|
| 66 |
-
'E60': "King's Indian Defense",
|
| 67 |
-
'E70': "King's Indian Defense: Normal Variation",
|
| 68 |
-
'E90': "King's Indian Defense: Orthodox Variation",
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
return eco_openings.get(eco_code, f"Opening ECO {eco_code}")
|
| 72 |
-
|
| 73 |
-
def detect_opening(game):
|
| 74 |
-
opening = game.headers.get("Opening", "")
|
| 75 |
-
eco = game.headers.get("ECO", "")
|
| 76 |
-
|
| 77 |
-
if opening:
|
| 78 |
-
return opening
|
| 79 |
-
elif eco:
|
| 80 |
-
return get_opening_name_from_eco(eco)
|
| 81 |
-
else:
|
| 82 |
-
return "Unknown Opening"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
project_info.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# βοΈ Chess Study Plan Pro - Professional Shaxmat O'quv Rejasi
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+

|
| 5 |
+

|
| 6 |
+
|
| 7 |
+
## π Loyiha Haqida
|
| 8 |
+
|
| 9 |
+
**Chess Study Plan Pro** - bu shaxmatchilar uchun sun'iy intellekt asosida ishlaydigan professional tahlil va o'quv rejasi yaratuvchi dastur. Loyiha UzChess AI Hackaton uchun ishlab chiqilgan bo'lib, shaxmatchilarning o'yin uslubini chuqur tahlil qilib, shaxsiy rivojlanish rejasini taklif etadi.
|
| 10 |
+
|
| 11 |
+
### π― Asosiy Maqsad
|
| 12 |
+
|
| 13 |
+
Har bir shaxmatchi o'zining zaif tomonlarini bilishi va ularni yaxshilash uchun aniq yo'l-yo'riq olishi kerak. Ushbu dastur aynan shu muammoni hal qiladi - o'yinlaringizni tahlil qilib, sizga maxsus o'quv rejasini tayyorlaydi.
|
| 14 |
+
|
| 15 |
+
## β¨ Asosiy Imkoniyatlar
|
| 16 |
+
|
| 17 |
+
### π To'liq O'yin Tahlili
|
| 18 |
+
- **Xatolarni aniqlash**: Qo'pol xatolar, kichik xatolar va himoyasiz qoldirishlarni topadi
|
| 19 |
+
- **Fazalar bo'yicha tahlil**: Debyut, o'rta o'yin va endshpil bo'yicha alohida statistika
|
| 20 |
+
- **Material yo'qotishlar**: Har bir xatoda qancha material yo'qotganingizni hisoblaydi
|
| 21 |
+
|
| 22 |
+
### π Batafsil Statistika
|
| 23 |
+
- **Debyut tahlili**: Qaysi debyutlarda yaxshi/yomon natijalar ko'rsatasiz
|
| 24 |
+
- **Rang statistikasi**: Oq va qora figuralar bilan alohida natijalar
|
| 25 |
+
- **G'alaba foizi**: Har bir debyut va rang uchun g'alaba foizi
|
| 26 |
+
- **O'yinlar soni**: 50 tagacha oxirgi o'yinlarni tahlil qiladi
|
| 27 |
+
|
| 28 |
+
### π€ AI Murabbiy (Gemini 2.0)
|
| 29 |
+
- **Zaif tomonlar tahlili**: Nima uchun xatolar sodir bo'layotganini tushuntiradi
|
| 30 |
+
- **Kundalik o'quv rejasi**: Har kuni necha masala yechish, qaysi debyutlarni o'rganish
|
| 31 |
+
- **Resurslar tavsiyasi**: Kitoblar, kurslar, YouTube kanallari
|
| 32 |
+
- **Motivatsion maslahatlar**: Psixologik qo'llab-quvvatlash
|
| 33 |
+
|
| 34 |
+
### π§© Mashq Masalalari
|
| 35 |
+
- Zaif tomonlaringizga mos Lichess masalalari havolalari
|
| 36 |
+
- Har bir muammo uchun maxsus tanlangan mashqlar
|
| 37 |
+
- To'g'ridan-to'g'ri Lichess Training bo'limiga yo'naltirish
|
| 38 |
+
|
| 39 |
+
## π O'rnatish va Ishga Tushirish
|
| 40 |
+
|
| 41 |
+
### Talablar
|
| 42 |
+
```bash
|
| 43 |
+
Python 3.8+
|
| 44 |
+
gradio
|
| 45 |
+
chess
|
| 46 |
+
requests
|
| 47 |
+
google-generativeai
|
| 48 |
+
O'rnatish
|