Skip to content

Commit 9880b5e

Browse files
committed
Add summary-only mode to shogi compare
1 parent 3e62cd3 commit 9880b5e

1 file changed

Lines changed: 123 additions & 91 deletions

File tree

tests/shogi_compare.py

Lines changed: 123 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@
115115
# Number -> kanji rank for display (index 0 unused, 1="一" through 9="九").
116116
KANJI_RANK = ["", "一", "二", "三", "四", "五", "六", "七", "八", "九"]
117117

118+
GAME_END_MARKERS = (
119+
"投了", "切れ負け", "詰み", "千日手", "入玉宣言", "反則勝ち", "反則負け", "中断"
120+
)
121+
118122
PIECE_TOKENS = [
119123
("成銀", "+S"),
120124
("成桂", "+N"),
@@ -275,6 +279,113 @@ def compare_japanese_notation(expected_text, actual_text):
275279
return hard_reasons, style_reasons
276280

277281

282+
def print_usage():
283+
print(
284+
"Usage: python3 tests/shogi_compare.py <game.kif> [--max N] [--summary-only]"
285+
)
286+
287+
288+
def print_move(verbose, text):
289+
if verbose:
290+
print(text)
291+
292+
293+
def process_game(game_idx, handicap, moves, verbose):
294+
"""Compare one game and return aggregate counters.
295+
296+
The summary-only path intentionally uses the same legality, SAN, and FEN
297+
checks as the verbose mode. It only suppresses the per-move log so large
298+
exports can be validated without spending most of the runtime on I/O.
299+
"""
300+
if handicap != "平手":
301+
print_move(
302+
verbose,
303+
f"=== Game {game_idx + 1} ({len(moves)} moves) [SKIPPED: {handicap}] ===",
304+
)
305+
return 0, 0, 0, len(moves), True
306+
307+
print_move(verbose, f"=== Game {game_idx + 1} ({len(moves)} moves) ===")
308+
fen = SHOGI_FEN
309+
last_dest_sq = None
310+
last_move_uci = None
311+
exact = 0
312+
style = 0
313+
fail = 0
314+
skip = 0
315+
316+
for move_num, kif_text in moves:
317+
# Strip origin suffix for display: "七六歩(77)" -> "七六歩"
318+
kif_notation = re.sub(r"[\((]\d+[\))]", "", kif_text).strip()
319+
320+
if kif_text in GAME_END_MARKERS:
321+
print_move(
322+
verbose,
323+
f"{move_num:4d} {kif_notation:20s} {'(end)':10s} SKIP (end)",
324+
)
325+
skip += 1
326+
continue
327+
328+
uci, is_promo, is_drop = kif_move_to_uci(kif_text, last_dest_sq)
329+
330+
if not uci:
331+
print_move(
332+
verbose,
333+
f"{move_num:4d} {kif_notation:20s} {'?':10s} SKIP (parse)",
334+
)
335+
skip += 1
336+
continue
337+
338+
legal = sf.legal_moves("shogi", fen, [])
339+
if uci not in legal:
340+
print_move(verbose, f"{move_num:4d} {kif_notation:20s} {uci:10s} ILLEGAL")
341+
fail += 1
342+
continue
343+
344+
try:
345+
our_san = sf.get_san(
346+
"shogi", fen, uci, False, sf.NOTATION_SHOGI_JAPANESE, last_move_uci
347+
)
348+
except Exception as e:
349+
our_san = f"ERROR({e})"
350+
351+
hard_reasons, style_reasons = compare_japanese_notation(kif_notation, our_san)
352+
if hard_reasons:
353+
status = f"MISMATCH ({'+'.join(hard_reasons)})"
354+
fail += 1
355+
elif style_reasons:
356+
status = f"STYLE ({'+'.join(sorted(set(style_reasons)))})"
357+
style += 1
358+
else:
359+
status = "OK"
360+
exact += 1
361+
362+
print_move(
363+
verbose,
364+
f"{move_num:4d} {kif_notation:20s} {uci:10s} "
365+
f"{our_san:20s} {status}",
366+
)
367+
368+
if len(uci) >= 4:
369+
if is_drop:
370+
last_dest_sq = uci[2:]
371+
else:
372+
last_dest_sq = uci[2:4]
373+
last_move_uci = uci
374+
375+
try:
376+
fen = sf.get_fen("shogi", fen, [uci], False, False)
377+
except Exception:
378+
pass
379+
380+
print_move(
381+
verbose,
382+
f"\nOK: {exact} STYLE: {style} MISMATCH: {fail} "
383+
f"SKIP: {skip} Total: {len(moves)}",
384+
)
385+
print_move(verbose, "")
386+
return exact, style, fail, skip, False
387+
388+
278389
def fullwidth_to_int(ch):
279390
"""Convert full-width digit (e.g. '1') or half-width digit ('1') to int."""
280391
if ord(ch) >= 0xFF11 and ord(ch) <= 0xFF19:
@@ -513,20 +624,25 @@ def extract_dest_from_san(san):
513624
def main():
514625
max_games = None
515626
filepath = None
627+
summary_only = False
516628
args = sys.argv[1:]
517629
i = 0
518630
while i < len(args):
519631
if args[i] == "--max" and i + 1 < len(args):
520632
max_games = int(args[i + 1])
521633
i += 2
634+
elif args[i] == "--summary-only":
635+
summary_only = True
636+
i += 1
522637
elif not args[i].startswith("-"):
523638
filepath = args[i]
524639
i += 1
525640
else:
526-
i += 1
641+
print_usage()
642+
sys.exit(1)
527643

528644
if not filepath:
529-
print("Usage: python3 tests/shogi_compare.py <game.kif> [--max N]")
645+
print_usage()
530646
sys.exit(1)
531647

532648
games = parse_kif_file(filepath)
@@ -539,102 +655,18 @@ def main():
539655
total_fail = 0
540656
total_skip = 0
541657
skipped_variants = 0
658+
verbose = not summary_only
542659

543660
for game_idx, (handicap, moves) in enumerate(games):
544-
# Skip non-standard game types (王手将棋, 5五将棋, etc.)
545-
# These have different starting positions than the standard FEN.
546-
if handicap != "平手":
547-
print(f"=== Game {game_idx + 1} ({len(moves)} moves) [SKIPPED: {handicap}] ===")
548-
skipped_variants += 1
549-
total_skip += len(moves)
550-
continue
551-
552-
print(f"=== Game {game_idx + 1} ({len(moves)} moves) ===")
553-
fen = SHOGI_FEN
554-
last_dest_sq = None
555-
last_move_uci = None
556-
exact = 0
557-
style = 0
558-
fail = 0
559-
skip = 0
560-
561-
for move_num, kif_text in moves:
562-
# Strip origin suffix for display: "七六歩(77)" -> "七六歩"
563-
kif_notation = re.sub(r"[\((]\d+[\))]", "", kif_text).strip()
564-
565-
# Skip game-end tokens (not actual moves)
566-
if kif_text in ("投了", "切れ負け", "詰み", "千日手", "入玉宣言",
567-
"反則勝ち", "反則負け", "中断"):
568-
print(f"{move_num:4d} {kif_notation:20s} {'(end)':10s} SKIP (end)")
569-
skip += 1
570-
continue
571-
572-
# Parse KIF move text to UCI format
573-
uci, is_promo, is_drop = kif_move_to_uci(kif_text, last_dest_sq)
574-
575-
if not uci:
576-
print(f"{move_num:4d} {kif_notation:20s} {'?':10s} SKIP (parse)")
577-
skip += 1
578-
continue
579-
580-
# Verify move is legal in current position
581-
legal = sf.legal_moves("shogi", fen, [])
582-
if uci not in legal:
583-
print(
584-
f"{move_num:4d} {kif_notation:20s} {uci:10s} ILLEGAL"
585-
)
586-
fail += 1
587-
continue
588-
589-
# Get engine's Japanese notation for this move
590-
try:
591-
our_san = sf.get_san(
592-
"shogi", fen, uci, False, sf.NOTATION_SHOGI_JAPANESE, last_move_uci
593-
)
594-
except Exception as e:
595-
our_san = f"ERROR({e})"
596-
597-
hard_reasons, style_reasons = compare_japanese_notation(kif_notation, our_san)
598-
if hard_reasons:
599-
status = f"MISMATCH ({'+'.join(hard_reasons)})"
600-
fail += 1
601-
elif style_reasons:
602-
status = f"STYLE ({'+'.join(sorted(set(style_reasons)))})"
603-
style += 1
604-
else:
605-
status = "OK"
606-
exact += 1
607-
608-
print(
609-
f"{move_num:4d} {kif_notation:20s} {uci:10s} "
610-
f"{our_san:20s} {status}"
611-
)
612-
613-
# Track last destination for 同 disambiguation.
614-
# Drops use "piece@square" format, so the square starts at index 2.
615-
# Normal moves have fixed-length format: from_sq(2) + to_sq(2).
616-
if len(uci) >= 4:
617-
if is_drop:
618-
last_dest_sq = uci[2:] # e.g. "P@e5" -> "e5"
619-
else:
620-
last_dest_sq = uci[2:4] # e.g. "g7g6" -> "g6"
621-
last_move_uci = uci
622-
623-
# Advance position for the next move
624-
try:
625-
fen = sf.get_fen("shogi", fen, [uci], False, False)
626-
except Exception:
627-
pass
628-
629-
print(
630-
f"\nOK: {exact} STYLE: {style} MISMATCH: {fail} "
631-
f"SKIP: {skip} Total: {len(moves)}"
661+
exact, style, fail, skip, variant_skipped = process_game(
662+
game_idx, handicap, moves, verbose
632663
)
633664
total_exact += exact
634665
total_style += style
635666
total_fail += fail
636667
total_skip += skip
637-
print()
668+
if variant_skipped:
669+
skipped_variants += 1
638670

639671
print(f"=== Summary ===")
640672
print(f"Games: {len(games)} (平手: {len(games) - skipped_variants}, "

0 commit comments

Comments
 (0)