summaryrefslogtreecommitdiff
path: root/tests/lop/lop_out_to_triplets.py
blob: 627e4f5a0e6af24cbfd1d2951fae5c91b68e6099 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python3
"""Convert a typhoon --logfile transcript of a lop.txt run (plain
`setboard <fen>` / `go` script, xboard protocol) into the triplet
format tests/lop/compare_lop expects: <fen>\\n<score>\\n<move>\\n
repeated, one group per position.

- <move> must be UCI coordinate notation (e.g. "d2c2"), matching the
  convention already used in tests/lop/lop_10k_sf.txt and
  lop_10k_lc0.txt -- NOT the SAN the engine prints ("move Kg5" in the
  --logfile transcript). SAN is resolved to UCI via python-chess,
  using the FEN that was actually sent for that position (the
  "SCRIPT> <fen>" echo line), not the engine's internal re-print
  ("FEN: ...", which zeroes the move-count fields and is not what was
  asked).
- <score> is an integer centipawn-ish value, side-to-move-relative,
  from the "tellothers d<depth>, <score>, ..." summary line. A
  "+MATE<N>"/"-MATE<N>" score is mapped to +-(100000 - N), matching
  the existing reference files' observed range (e.g. mate-in-1 ->
  99999) -- there is no mate notation in those files to confirm this
  exact formula against, so this is a best-effort match to their
  magnitude/sign convention, not a verified-identical one. A bare
  "+MATE"/"-MATE" (no digits -- util.c's FormatScore prints this when
  the reported move delivers mate itself) is treated as N=1.

- A position where the engine never prints a "move"/"tellothers" line
  before the next FEN arrives (observed for K-vs-K insufficient-
  material draws, where the engine short-circuits without searching)
  is written as score=0 with a blank move field, rather than silently
  dropped. Detected generically, by absence (no move captured by the
  time the next FEN shows up) -- not by matching the draw-announcement
  text itself ("tellics draw" is written by main.c's Trace() call but
  does not actually appear in --logfile output in --batch mode, so
  matching for it directly does not work). A blank move field never
  strcmp-matches a real reference move, which is correct: there is
  nothing to agree or disagree with.
"""
import argparse
import re
import sys

import chess

FEN_RE = re.compile(r"^SCRIPT> (.+)$")
MOVE_RE = re.compile(r"^move (\S+)$")
SCORE_RE = re.compile(r"^tellothers d\d+, ([+-][\d.]+|[+-]MATE\d*),")


def convert_score(score_str):
    if "MATE" in score_str:
        sign = 1 if score_str[0] == "+" else -1
        digits = score_str.split("MATE")[1]
        # util.c's FormatScore prints bare "+MATE"/"-MATE" (no digits) when
        # the move being reported delivers mate itself (distance-to-mate 0
        # plies past this move) -- treat that as mate-in-1 to match the
        # existing reference files' observed mate-in-1 value (99999).
        n = int(digits) if digits else 1
        return sign * (100000 - n)
    return int(round(float(score_str) * 100))


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("logfile", help="engine --logfile transcript, e.g. src/lop.out")
    ap.add_argument("out", help="output triplet file for compare_lop")
    args = ap.parse_args()

    cur_fen = None
    cur_move_san = None
    n_written = 0
    n_san_failed = 0
    n_bare_draws = 0

    with open(args.logfile) as f, open(args.out, "w") as out:
        for line in f:
            line = line.rstrip("\n")

            m = FEN_RE.match(line)
            if m:
                fen = m.group(1).strip()
                if fen != "go":
                    # A prior FEN that never got a move+score before this
                    # next one arrived means the engine short-circuited
                    # without searching (observed for K-vs-K insufficient-
                    # material draws -- "tellics draw" isn't actually
                    # written to --logfile output in --batch mode, so
                    # detecting this generically, by absence, is more
                    # robust than matching that text). Flush it as a
                    # score=0/blank-move row rather than silently
                    # dropping the position.
                    if cur_fen is not None and cur_move_san is None:
                        out.write(f"{cur_fen}\n0\n\n")
                        n_written += 1
                        n_bare_draws += 1
                    cur_fen = fen
                    cur_move_san = None
                continue

            mm = MOVE_RE.match(line)
            if mm and cur_fen is not None:
                cur_move_san = mm.group(1)
                continue

            ms = SCORE_RE.match(line)
            if ms and cur_fen is not None and cur_move_san is not None:
                score = convert_score(ms.group(1))
                try:
                    board = chess.Board(cur_fen)
                    uci = board.parse_san(cur_move_san).uci()
                except ValueError:
                    n_san_failed += 1
                    uci = cur_move_san  # fallback, will mismatch on strcmp
                out.write(f"{cur_fen}\n{score}\n{uci}\n")
                n_written += 1
                cur_fen = None
                cur_move_san = None

        # Same short-circuit flush as above, for the last position in the
        # file (no following SCRIPT> line to trigger it in the loop).
        if cur_fen is not None and cur_move_san is None:
            out.write(f"{cur_fen}\n0\n\n")
            n_written += 1
            n_bare_draws += 1

    print(f"{n_written} positions written to {args.out}", file=sys.stderr)
    if n_bare_draws:
        print(f"  ({n_bare_draws} of those are bare-draw short-circuits "
              f"with score=0 and a blank move field)", file=sys.stderr)
    if n_san_failed:
        print(f"WARNING: {n_san_failed} SAN moves failed to parse against "
              f"their FEN -- written verbatim (will not UCI-match the "
              f"reference file)", file=sys.stderr)


if __name__ == "__main__":
    main()