Spaces:
Sleeping
Sleeping
Commit
Β·
bb06bb4
1
Parent(s):
500ef8a
a new way
Browse files- app.py +97 -103
- openings.py +73 -0
app.py
CHANGED
|
@@ -11,6 +11,7 @@ import time
|
|
| 11 |
import re
|
| 12 |
import chess.polyglot
|
| 13 |
import logging
|
|
|
|
| 14 |
|
| 15 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 16 |
logger = logging.getLogger(__name__)
|
|
@@ -107,78 +108,6 @@ def parse_pgn_content(pgn_content):
|
|
| 107 |
logger.info(f"Successfully parsed {len(games)} games")
|
| 108 |
return games
|
| 109 |
|
| 110 |
-
def get_opening_name_from_eco(eco_code):
|
| 111 |
-
eco_openings = {
|
| 112 |
-
'A00': 'Uncommon Opening',
|
| 113 |
-
'A01': "Nimzowitsch-Larsen Attack",
|
| 114 |
-
'A02': "Bird's Opening",
|
| 115 |
-
'A03': "Bird's Opening: Dutch Variation",
|
| 116 |
-
'A04': 'Reti Opening',
|
| 117 |
-
'A05': 'Reti Opening: 1...Nf6',
|
| 118 |
-
'A06': 'Reti Opening: 2.b3',
|
| 119 |
-
'A10': 'English Opening',
|
| 120 |
-
'A15': 'English Opening: Anglo-Indian Defense',
|
| 121 |
-
'A20': 'English Opening: 1...e5',
|
| 122 |
-
'A30': 'English Opening: Symmetrical Variation',
|
| 123 |
-
'A40': 'Queen Pawn Game',
|
| 124 |
-
'A45': 'Indian Defense',
|
| 125 |
-
'A46': 'Indian Defense: 2.Nf3',
|
| 126 |
-
'A50': 'Indian Defense: Normal Variation',
|
| 127 |
-
'B00': 'Uncommon King Pawn Opening',
|
| 128 |
-
'B01': 'Scandinavian Defense',
|
| 129 |
-
'B02': "Alekhine's Defense",
|
| 130 |
-
'B03': "Alekhine's Defense: Four Pawns Attack",
|
| 131 |
-
'B10': 'Caro-Kann Defense',
|
| 132 |
-
'B12': 'Caro-Kann Defense: Advance Variation',
|
| 133 |
-
'B20': 'Sicilian Defense',
|
| 134 |
-
'B22': 'Sicilian Defense: Alapin Variation',
|
| 135 |
-
'B23': 'Sicilian Defense: Closed',
|
| 136 |
-
'B30': 'Sicilian Defense: 2...Nc6',
|
| 137 |
-
'B40': 'Sicilian Defense: French Variation',
|
| 138 |
-
'B50': 'Sicilian Defense: 2...d6',
|
| 139 |
-
'B90': 'Sicilian Defense: Najdorf',
|
| 140 |
-
'C00': 'French Defense',
|
| 141 |
-
'C02': 'French Defense: Advance Variation',
|
| 142 |
-
'C10': 'French Defense: Rubinstein Variation',
|
| 143 |
-
'C20': 'King Pawn Game',
|
| 144 |
-
'C30': "King's Gambit",
|
| 145 |
-
'C40': "King's Knight Opening",
|
| 146 |
-
'C41': 'Philidor Defense',
|
| 147 |
-
'C42': 'Russian Game (Petrov Defense)',
|
| 148 |
-
'C44': 'Scotch Game',
|
| 149 |
-
'C50': 'Italian Game',
|
| 150 |
-
'C60': 'Spanish Opening (Ruy Lopez)',
|
| 151 |
-
'C65': 'Spanish Opening: Berlin Defense',
|
| 152 |
-
'C70': 'Spanish Opening',
|
| 153 |
-
'C78': 'Spanish Opening: Morphy Defense',
|
| 154 |
-
'C80': 'Spanish Opening: Open Variation',
|
| 155 |
-
'D00': 'Queen Pawn Game',
|
| 156 |
-
'D02': 'Queen Pawn Game: 2.Nf3',
|
| 157 |
-
'D10': 'Slav Defense',
|
| 158 |
-
'D20': "Queen's Gambit Accepted",
|
| 159 |
-
'D30': "Queen's Gambit Declined",
|
| 160 |
-
'D50': "Queen's Gambit Declined: 4.Bg5",
|
| 161 |
-
'E00': 'Indian Defense',
|
| 162 |
-
'E10': 'Indian Defense: 3.Nf3',
|
| 163 |
-
'E20': 'Nimzo-Indian Defense',
|
| 164 |
-
'E30': 'Nimzo-Indian Defense: Leningrad Variation',
|
| 165 |
-
'E60': "King's Indian Defense",
|
| 166 |
-
'E70': "King's Indian Defense: Normal Variation",
|
| 167 |
-
'E90': "King's Indian Defense: Orthodox Variation",
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
return eco_openings.get(eco_code, f"Opening ECO {eco_code}")
|
| 171 |
-
|
| 172 |
-
def detect_opening(game):
|
| 173 |
-
opening = game.headers.get("Opening", "")
|
| 174 |
-
eco = game.headers.get("ECO", "")
|
| 175 |
-
|
| 176 |
-
if opening:
|
| 177 |
-
return opening
|
| 178 |
-
elif eco:
|
| 179 |
-
return get_opening_name_from_eco(eco)
|
| 180 |
-
else:
|
| 181 |
-
return "Unknown Opening"
|
| 182 |
|
| 183 |
def analyze_game_detailed(game, username):
|
| 184 |
board = game.board()
|
|
@@ -382,11 +311,50 @@ def fetch_lichess_puzzles(themes, count=5):
|
|
| 382 |
logger.info(f"Selected Lichess themes: {lichess_themes}")
|
| 383 |
|
| 384 |
try:
|
|
|
|
| 385 |
for theme in lichess_themes:
|
| 386 |
if len(puzzles) >= count:
|
| 387 |
break
|
| 388 |
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
response = requests.get(url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 391 |
|
| 392 |
if response.status_code == 200:
|
|
@@ -403,27 +371,22 @@ def fetch_lichess_puzzles(themes, count=5):
|
|
| 403 |
'themes': daily['puzzle'].get('themes', []),
|
| 404 |
'url': f"https://lichess.org/training/{puzzle_id}"
|
| 405 |
})
|
| 406 |
-
logger.info(f"Added puzzle {puzzle_id}
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
except Exception as e:
|
| 410 |
-
logger.error(f"Error fetching puzzles: {str(e)}")
|
| 411 |
|
|
|
|
| 412 |
while len(puzzles) < count:
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
})
|
| 424 |
-
logger.info(f"Added fallback puzzle link {len(puzzles)}")
|
| 425 |
-
except:
|
| 426 |
-
break
|
| 427 |
|
| 428 |
logger.info(f"Total puzzles prepared: {len(puzzles)}")
|
| 429 |
return puzzles[:count]
|
|
@@ -482,23 +445,44 @@ MUHIM: Javobni FAQAT O'ZBEK TILIDA yozing! Aniq va amaliy maslahatlar bering."""
|
|
| 482 |
logger.error(f"AI analysis failed: {str(e)}")
|
| 483 |
return f"AI tahlil hozircha mavjud emas: {str(e)}"
|
| 484 |
|
| 485 |
-
def analyze_games(
|
| 486 |
logger.info("=== Starting game analysis ===")
|
| 487 |
-
actual_username =
|
|
|
|
| 488 |
|
| 489 |
-
|
| 490 |
-
|
|
|
|
| 491 |
if error:
|
| 492 |
logger.error(f"Failed to fetch games: {error}")
|
| 493 |
return error, "", "", "", None, None, None, None, None
|
| 494 |
-
actual_username =
|
|
|
|
|
|
|
| 495 |
elif pgn_file:
|
| 496 |
logger.info("Processing uploaded PGN file")
|
| 497 |
-
actual_username = "Player"
|
| 498 |
pgn_content = pgn_file.decode('utf-8') if isinstance(pgn_file, bytes) else pgn_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
else:
|
| 500 |
logger.error("No username or file provided")
|
| 501 |
-
return "β
|
| 502 |
|
| 503 |
games = parse_pgn_content(pgn_content)
|
| 504 |
|
|
@@ -644,16 +628,26 @@ with gr.Blocks(title="Chess Study Plan Pro", theme=gr.themes.Soft()) as demo:
|
|
| 644 |
|
| 645 |
with gr.Row():
|
| 646 |
with gr.Column():
|
| 647 |
-
|
|
|
|
| 648 |
label="Chess.com foydalanuvchi nomi",
|
| 649 |
placeholder="Foydalanuvchi nomini kiriting",
|
| 650 |
)
|
|
|
|
|
|
|
|
|
|
| 651 |
pgn_upload = gr.File(
|
| 652 |
-
label="
|
| 653 |
file_types=[".pgn"],
|
| 654 |
type="binary"
|
| 655 |
)
|
| 656 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
|
| 658 |
with gr.Row():
|
| 659 |
stats_output = gr.Markdown(label="Statistika")
|
|
@@ -680,7 +674,7 @@ with gr.Blocks(title="Chess Study Plan Pro", theme=gr.themes.Soft()) as demo:
|
|
| 680 |
|
| 681 |
analyze_btn.click(
|
| 682 |
fn=analyze_games,
|
| 683 |
-
inputs=[
|
| 684 |
outputs=[
|
| 685 |
stats_output,
|
| 686 |
ai_output,
|
|
|
|
| 11 |
import re
|
| 12 |
import chess.polyglot
|
| 13 |
import logging
|
| 14 |
+
from openings import get_opening_name_from_eco, detect_opening
|
| 15 |
|
| 16 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 17 |
logger = logging.getLogger(__name__)
|
|
|
|
| 108 |
logger.info(f"Successfully parsed {len(games)} games")
|
| 109 |
return games
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
def analyze_game_detailed(game, username):
|
| 113 |
board = game.board()
|
|
|
|
| 311 |
logger.info(f"Selected Lichess themes: {lichess_themes}")
|
| 312 |
|
| 313 |
try:
|
| 314 |
+
# Fetch puzzles from Lichess database API
|
| 315 |
for theme in lichess_themes:
|
| 316 |
if len(puzzles) >= count:
|
| 317 |
break
|
| 318 |
|
| 319 |
+
# Use the correct API endpoint with theme filter
|
| 320 |
+
url = f"https://lichess.org/api/puzzle/batch/mix?nb=1&themes={theme}"
|
| 321 |
+
response = requests.get(url, timeout=10, headers={
|
| 322 |
+
'User-Agent': 'Mozilla/5.0',
|
| 323 |
+
'Accept': 'application/x-ndjson'
|
| 324 |
+
})
|
| 325 |
+
|
| 326 |
+
if response.status_code == 200:
|
| 327 |
+
# Parse NDJSON response
|
| 328 |
+
lines = response.text.strip().split('\n')
|
| 329 |
+
for line in lines:
|
| 330 |
+
if len(puzzles) >= count:
|
| 331 |
+
break
|
| 332 |
+
try:
|
| 333 |
+
puzzle_data = eval(line) # or use json.loads(line)
|
| 334 |
+
puzzle_id = puzzle_data['puzzle']['id']
|
| 335 |
+
|
| 336 |
+
if not any(p['id'] == puzzle_id for p in puzzles):
|
| 337 |
+
puzzles.append({
|
| 338 |
+
'id': puzzle_id,
|
| 339 |
+
'fen': puzzle_data['puzzle'].get('fen', ''),
|
| 340 |
+
'moves': puzzle_data['puzzle'].get('plays', ''),
|
| 341 |
+
'solution': puzzle_data['puzzle'].get('solution', []),
|
| 342 |
+
'rating': puzzle_data['puzzle'].get('rating', 1500),
|
| 343 |
+
'themes': puzzle_data['puzzle'].get('themes', []),
|
| 344 |
+
'url': f"https://lichess.org/training/{puzzle_id}"
|
| 345 |
+
})
|
| 346 |
+
logger.info(f"Added puzzle {puzzle_id} with rating {puzzle_data['puzzle'].get('rating')}")
|
| 347 |
+
except Exception as e:
|
| 348 |
+
logger.warning(f"Failed to parse puzzle line: {str(e)}")
|
| 349 |
+
|
| 350 |
+
time.sleep(0.5)
|
| 351 |
+
except Exception as e:
|
| 352 |
+
logger.error(f"Error fetching puzzles: {str(e)}")
|
| 353 |
+
|
| 354 |
+
# Fill remaining with daily puzzle if needed
|
| 355 |
+
if len(puzzles) < count:
|
| 356 |
+
try:
|
| 357 |
+
url = "https://lichess.org/api/puzzle/daily"
|
| 358 |
response = requests.get(url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
|
| 359 |
|
| 360 |
if response.status_code == 200:
|
|
|
|
| 371 |
'themes': daily['puzzle'].get('themes', []),
|
| 372 |
'url': f"https://lichess.org/training/{puzzle_id}"
|
| 373 |
})
|
| 374 |
+
logger.info(f"Added daily puzzle {puzzle_id}")
|
| 375 |
+
except Exception as e:
|
| 376 |
+
logger.error(f"Error fetching daily puzzle: {str(e)}")
|
|
|
|
|
|
|
| 377 |
|
| 378 |
+
# Fill with fallback links if still not enough
|
| 379 |
while len(puzzles) < count:
|
| 380 |
+
puzzles.append({
|
| 381 |
+
'id': f'puzzle_{len(puzzles)+1}',
|
| 382 |
+
'fen': '',
|
| 383 |
+
'moves': '',
|
| 384 |
+
'solution': [],
|
| 385 |
+
'rating': 1500,
|
| 386 |
+
'themes': ['Mixed'],
|
| 387 |
+
'url': "https://lichess.org/training"
|
| 388 |
+
})
|
| 389 |
+
logger.info(f"Added fallback puzzle link {len(puzzles)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
logger.info(f"Total puzzles prepared: {len(puzzles)}")
|
| 392 |
return puzzles[:count]
|
|
|
|
| 445 |
logger.error(f"AI analysis failed: {str(e)}")
|
| 446 |
return f"AI tahlil hozircha mavjud emas: {str(e)}"
|
| 447 |
|
| 448 |
+
def analyze_games(username_chesscom, pgn_file, username_pgn):
|
| 449 |
logger.info("=== Starting game analysis ===")
|
| 450 |
+
actual_username = None
|
| 451 |
+
pgn_content = None
|
| 452 |
|
| 453 |
+
# Chess.com user
|
| 454 |
+
if username_chesscom:
|
| 455 |
+
pgn_content, error = get_user_games_from_chess_com(username_chesscom)
|
| 456 |
if error:
|
| 457 |
logger.error(f"Failed to fetch games: {error}")
|
| 458 |
return error, "", "", "", None, None, None, None, None
|
| 459 |
+
actual_username = username_chesscom
|
| 460 |
+
|
| 461 |
+
# PGN file upload
|
| 462 |
elif pgn_file:
|
| 463 |
logger.info("Processing uploaded PGN file")
|
|
|
|
| 464 |
pgn_content = pgn_file.decode('utf-8') if isinstance(pgn_file, bytes) else pgn_file
|
| 465 |
+
|
| 466 |
+
if username_pgn and username_pgn.strip():
|
| 467 |
+
actual_username = username_pgn.strip()
|
| 468 |
+
else:
|
| 469 |
+
# Try to extract username from first game headers
|
| 470 |
+
try:
|
| 471 |
+
first_game = chess.pgn.read_game(io.StringIO(pgn_content))
|
| 472 |
+
if first_game:
|
| 473 |
+
white = first_game.headers.get("White", "")
|
| 474 |
+
black = first_game.headers.get("Black", "")
|
| 475 |
+
actual_username = white if white else black if black else "Player"
|
| 476 |
+
else:
|
| 477 |
+
actual_username = "Player"
|
| 478 |
+
except:
|
| 479 |
+
actual_username = "Player"
|
| 480 |
+
|
| 481 |
+
logger.info(f"Extracted username from PGN: {actual_username}")
|
| 482 |
+
|
| 483 |
else:
|
| 484 |
logger.error("No username or file provided")
|
| 485 |
+
return "β Chess.com foydalanuvchi nomini kiriting yoki PGN faylni yuklang", "", "", "", None, None, None, None, None
|
| 486 |
|
| 487 |
games = parse_pgn_content(pgn_content)
|
| 488 |
|
|
|
|
| 628 |
|
| 629 |
with gr.Row():
|
| 630 |
with gr.Column():
|
| 631 |
+
gr.Markdown("### π Chess.com dan tahlil")
|
| 632 |
+
username_chesscom = gr.Textbox(
|
| 633 |
label="Chess.com foydalanuvchi nomi",
|
| 634 |
placeholder="Foydalanuvchi nomini kiriting",
|
| 635 |
)
|
| 636 |
+
|
| 637 |
+
with gr.Column():
|
| 638 |
+
gr.Markdown("### π PGN fayl yuklash")
|
| 639 |
pgn_upload = gr.File(
|
| 640 |
+
label="PGN faylni yuklang",
|
| 641 |
file_types=[".pgn"],
|
| 642 |
type="binary"
|
| 643 |
)
|
| 644 |
+
username_pgn = gr.Textbox(
|
| 645 |
+
label="Foydalanuvchi nomi (PGN uchun)",
|
| 646 |
+
placeholder="PGN dagi o'yinchi nomi (ixtiyoriy)",
|
| 647 |
+
info="Bo'sh qoldiring, avtomatik aniqlanadi"
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
analyze_btn = gr.Button("π To'liq tahlil qilish", variant="primary", size="lg")
|
| 651 |
|
| 652 |
with gr.Row():
|
| 653 |
stats_output = gr.Markdown(label="Statistika")
|
|
|
|
| 674 |
|
| 675 |
analyze_btn.click(
|
| 676 |
fn=analyze_games,
|
| 677 |
+
inputs=[username_chesscom, pgn_upload, username_pgn],
|
| 678 |
outputs=[
|
| 679 |
stats_output,
|
| 680 |
ai_output,
|
openings.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
'A40': 'Queen Pawn Game',
|
| 16 |
+
'A45': 'Indian Defense',
|
| 17 |
+
'A46': 'Indian Defense: 2.Nf3',
|
| 18 |
+
'A50': 'Indian Defense: Normal Variation',
|
| 19 |
+
'B00': 'Uncommon King Pawn Opening',
|
| 20 |
+
'B01': 'Scandinavian Defense',
|
| 21 |
+
'B02': "Alekhine's Defense",
|
| 22 |
+
'B03': "Alekhine's Defense: Four Pawns Attack",
|
| 23 |
+
'B10': 'Caro-Kann Defense',
|
| 24 |
+
'B12': 'Caro-Kann Defense: Advance Variation',
|
| 25 |
+
'B20': 'Sicilian Defense',
|
| 26 |
+
'B22': 'Sicilian Defense: Alapin Variation',
|
| 27 |
+
'B23': 'Sicilian Defense: Closed',
|
| 28 |
+
'B30': 'Sicilian Defense: 2...Nc6',
|
| 29 |
+
'B40': 'Sicilian Defense: French Variation',
|
| 30 |
+
'B50': 'Sicilian Defense: 2...d6',
|
| 31 |
+
'B90': 'Sicilian Defense: Najdorf',
|
| 32 |
+
'C00': 'French Defense',
|
| 33 |
+
'C02': 'French Defense: Advance Variation',
|
| 34 |
+
'C10': 'French Defense: Rubinstein Variation',
|
| 35 |
+
'C20': 'King Pawn Game',
|
| 36 |
+
'C30': "King's Gambit",
|
| 37 |
+
'C40': "King's Knight Opening",
|
| 38 |
+
'C41': 'Philidor Defense',
|
| 39 |
+
'C42': 'Russian Game (Petrov Defense)',
|
| 40 |
+
'C44': 'Scotch Game',
|
| 41 |
+
'C50': 'Italian Game',
|
| 42 |
+
'C60': 'Spanish Opening (Ruy Lopez)',
|
| 43 |
+
'C65': 'Spanish Opening: Berlin Defense',
|
| 44 |
+
'C70': 'Spanish Opening',
|
| 45 |
+
'C78': 'Spanish Opening: Morphy Defense',
|
| 46 |
+
'C80': 'Spanish Opening: Open Variation',
|
| 47 |
+
'D00': 'Queen Pawn Game',
|
| 48 |
+
'D02': 'Queen Pawn Game: 2.Nf3',
|
| 49 |
+
'D10': 'Slav Defense',
|
| 50 |
+
'D20': "Queen's Gambit Accepted",
|
| 51 |
+
'D30': "Queen's Gambit Declined",
|
| 52 |
+
'D50': "Queen's Gambit Declined: 4.Bg5",
|
| 53 |
+
'E00': 'Indian Defense',
|
| 54 |
+
'E10': 'Indian Defense: 3.Nf3',
|
| 55 |
+
'E20': 'Nimzo-Indian Defense',
|
| 56 |
+
'E30': 'Nimzo-Indian Defense: Leningrad Variation',
|
| 57 |
+
'E60': "King's Indian Defense",
|
| 58 |
+
'E70': "King's Indian Defense: Normal Variation",
|
| 59 |
+
'E90': "King's Indian Defense: Orthodox Variation",
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
return eco_openings.get(eco_code, f"Opening ECO {eco_code}")
|
| 63 |
+
|
| 64 |
+
def detect_opening(game):
|
| 65 |
+
opening = game.headers.get("Opening", "")
|
| 66 |
+
eco = game.headers.get("ECO", "")
|
| 67 |
+
|
| 68 |
+
if opening:
|
| 69 |
+
return opening
|
| 70 |
+
elif eco:
|
| 71 |
+
return get_opening_name_from_eco(eco)
|
| 72 |
+
else:
|
| 73 |
+
return "Unknown Opening"
|