115115# Number -> kanji rank for display (index 0 unused, 1="一" through 9="九").
116116KANJI_RANK = ["" , "一" , "二" , "三" , "四" , "五" , "六" , "七" , "八" , "九" ]
117117
118+ GAME_END_MARKERS = (
119+ "投了" , "切れ負け" , "詰み" , "千日手" , "入玉宣言" , "反則勝ち" , "反則負け" , "中断"
120+ )
121+
118122PIECE_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"\n OK: { 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+
278389def 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):
513624def 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"\n OK: { 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