match.py (5685B)
1 #!/usr/bin/env python3 2 """match.py - partidas del motor propio (ajedrez --uci) contra Stockfish con 3 UCI_LimitStrength, para estimar el Elo de cada nivel. 4 5 Necesita python-chess (pip install chess). Ejemplo: 6 tools/match.py --level 4 --elo 2000 --games 40 --concurrency 6 --pgn m.pgn 7 8 El Elo de Stockfish limitado esta calibrado contra la lista CCRL Blitz, asi que 9 lo que sale es "Elo CCRL aproximado", no Elo FIDE ni de Lichess. 10 """ 11 import argparse, concurrent.futures as cf, math, os, sys, threading 12 import chess, chess.engine, chess.pgn 13 14 HERE = os.path.dirname(os.path.abspath(__file__)) 15 OURS = os.path.join(HERE, "..", "ajedrez") 16 LEVEL_MS = [300, 500, 600, 800, 1500] # como level_limits() en core/engine.c 17 18 19 def default_sf(): 20 for p in (os.environ.get("CHESS_STOCKFISH"), os.path.join(HERE, "..", "engine", "stockfish"), 21 os.path.join(HERE, "..", "..", "ajedrez", "engine", "stockfish")): 22 if p and os.access(p, os.X_OK): 23 return p 24 return "stockfish" 25 26 27 def play_one(i, a): 28 ours = chess.engine.SimpleEngine.popen_uci([OURS, "--uci"]) 29 sf = chess.engine.SimpleEngine.popen_uci(a.stockfish) 30 try: 31 ours.configure({"Level": a.level}) 32 sf_opts = {"Threads": 1, "Hash": 16} 33 if a.elo: 34 sf_opts.update({"UCI_LimitStrength": True, "UCI_Elo": a.elo}) 35 sf.configure(sf_opts) 36 ours_white = i % 2 == 0 37 board = chess.Board() 38 engines = {ours_white: ours, not ours_white: sf} 39 ours_ms = a.movetime or LEVEL_MS[a.level] 40 sf_ms = a.sf_movetime or ours_ms 41 # adjudicacion: los dos motores de acuerdo en que esta ganada / muerta 42 last = {True: None, False: None} # ultima evaluacion de cada motor (desde blancas) 43 streak_win, streak_draw, result = 0, 0, None 44 while not board.is_game_over(claim_draw=True): 45 eng = engines[board.turn] 46 # el motor propio con "go" a secas usa los limites de su nivel 47 limit = chess.engine.Limit(time=(ours_ms if eng is ours else sf_ms) / 1000) 48 if eng is ours and not a.movetime: 49 limit = chess.engine.Limit() 50 r = eng.play(board, limit, info=chess.engine.INFO_SCORE) 51 sc = r.info.get("score") 52 if sc is not None: 53 last[eng is ours] = sc.white().score(mate_score=100000) 54 board.push(r.move) 55 a_, b_ = last[True], last[False] 56 if a_ is not None and b_ is not None: 57 if (a_ > 1000 and b_ > 1000) or (a_ < -1000 and b_ < -1000): 58 streak_win += 1 59 else: 60 streak_win = 0 61 if board.ply() > 100 and abs(a_) <= 15 and abs(b_) <= 15: 62 streak_draw += 1 63 else: 64 streak_draw = 0 65 if streak_win >= 8: 66 result = "1-0" if a_ > 0 else "0-1" 67 break 68 if streak_draw >= 16: 69 result = "1/2-1/2" 70 break 71 if board.ply() >= 400: 72 result = "1/2-1/2" 73 break 74 if result is None: 75 result = board.result(claim_draw=True) 76 g = chess.pgn.Game.from_board(board) 77 name_o = f"Ajedrez-C L{a.level}" 78 name_s = f"Stockfish {a.elo}" if a.elo else "Stockfish" 79 g.headers["White"], g.headers["Black"] = (name_o, name_s) if ours_white else (name_s, name_o) 80 g.headers["Result"] = result 81 g.headers["Round"] = str(i + 1) 82 pts = {"1-0": 1.0, "0-1": 0.0}.get(result, 0.5) 83 return (pts if ours_white else 1 - pts), g 84 finally: 85 ours.quit() 86 sf.quit() 87 88 89 def elo_diff(score, n): 90 """diferencia de Elo y margen del 95% (aprox. normal)""" 91 s = min(max(score / n, 0.5 / n), 1 - 0.5 / n) 92 d = -400 * math.log10(1 / s - 1) 93 se = math.sqrt(s * (1 - s) / n) 94 lo = -400 * math.log10(1 / min(max(s - 1.96 * se, 1e-3), 1 - 1e-3) - 1) 95 hi = -400 * math.log10(1 / min(max(s + 1.96 * se, 1e-3), 1 - 1e-3) - 1) 96 return d, lo, hi 97 98 99 def main(): 100 ap = argparse.ArgumentParser() 101 ap.add_argument("--level", type=int, default=4) 102 ap.add_argument("--elo", type=int, default=0, help="UCI_Elo de Stockfish (0 = fuerza completa)") 103 ap.add_argument("--games", type=int, default=20) 104 ap.add_argument("--concurrency", type=int, default=4) 105 ap.add_argument("--movetime", type=int, default=0, help="ms por jugada del motor propio (0 = el de su nivel)") 106 ap.add_argument("--sf-movetime", type=int, default=0, help="ms por jugada de Stockfish (0 = igual al propio)") 107 ap.add_argument("--stockfish", default=default_sf()) 108 ap.add_argument("--pgn") 109 a = ap.parse_args() 110 lock = threading.Lock() 111 w = d = l = 0 112 pgn = open(a.pgn, "a") if a.pgn else None 113 with cf.ThreadPoolExecutor(a.concurrency) as ex: 114 for fut in cf.as_completed([ex.submit(play_one, i, a) for i in range(a.games)]): 115 pts, g = fut.result() 116 with lock: 117 w += pts == 1 118 d += pts == 0.5 119 l += pts == 0 120 if pgn: 121 print(g, file=pgn, end="\n\n") 122 pgn.flush() 123 n = w + d + l 124 print(f"L{a.level} vs SF{a.elo or 'full'}: {n}/{a.games} +{w} ={d} -{l}", flush=True) 125 n = w + d + l 126 diff, lo, hi = elo_diff(w + d / 2, n) 127 base = f" -> ~{a.elo + diff:.0f} [{a.elo + lo:.0f}, {a.elo + hi:.0f}]" if a.elo else "" 128 print(f"RESULT level={a.level} elo={a.elo} games={n} +{w} ={d} -{l} score={(w + d / 2) / n:.3f} diff={diff:+.0f}{base}") 129 130 131 if __name__ == "__main__": 132 sys.exit(main())