juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

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())