/// <summary> /// 拡張子で使う文字列に変換する。 /// 返す文字列には、"."も含まれる。 /// 例) ".kif" /// </summary> /// <returns></returns> public static string ToExtensions(this KifuFileType type) { switch (type) { case KifuFileType.KIF: return(".kif"); case KifuFileType.KI2: return(".ki2"); case KifuFileType.CSA: return(".csa"); case KifuFileType.PSN: return(".psn"); case KifuFileType.PSN2: return(".psn2"); case KifuFileType.SFEN: return(".sfen"); case KifuFileType.JSON: return(".json"); case KifuFileType.JKF: return(".jkf"); case KifuFileType.SVG: return(".svg"); case KifuFileType.UNKNOWN: return(".unknown"); } return(""); }
/// <summary> /// 読み込み形式手動指定、とりあえず各形式のルーチンを直接テストするため。 /// </summary> /// <param name="content"></param> /// <param name="kf"></param> /// <returns></returns> public string FromString(string content, KifuFileType kf) { Init(); var lines = content.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None); switch (kf) { case KifuFileType.SFEN: return(FromSfenString(content)); case KifuFileType.CSA: return(FromCsaString(lines, kf)); case KifuFileType.KIF: case KifuFileType.KI2: return(FromKifString(lines, kf)); case KifuFileType.PSN: case KifuFileType.PSN2: return(FromPsnString(lines, kf)); case KifuFileType.JKF: return(FromJkfString(content, kf)); case KifuFileType.JSON: return(FromLiveJsonString(content, kf)); default: return(string.Empty); } }
/// <summary> /// 局面文字列を書き出す /// フォーマットは引数のkfで指定する。 /// </summary> /// <param name="filename"></param> /// <param name="kf"></param> public string ToPositionString(KifuFileType kt) { string result = string.Empty; switch (kt) { case KifuFileType.SVG: // SVG形式は専用ルーチンを使用 result = ToSvgString(); break; default: // SVG形式以外は、棋譜書き出しルーチンを流用して出力する var kifu = new KifuManager(); // 経路を消すためにsfen化して代入しなおして書き出す kifu.FromString($"sfen {Position.ToSfen()}"); kifu.KifuHeader.PlayerNameBlack = KifuHeader.PlayerNameBlack; kifu.KifuHeader.PlayerNameWhite = KifuHeader.PlayerNameWhite; result = kifu.ToString(kt); break; // ToDo : 他の形式もサポートする } return(result); }
/// <summary> /// 局面文字列を書き出す /// フォーマットは引数のkfで指定する。 /// </summary> /// <param name="filename"></param> /// <param name="kf"></param> public string ToPositionString(KifuFileType kt) { string result = string.Empty; switch (kt) { // 局面出力の専用ルーチンを使用 case KifuFileType.KIF: case KifuFileType.KI2: result = ToKifPositionString(kt); break; case KifuFileType.SVG: result = ToSvgPositionString(); break; // 局面出力を用意していないものは、棋譜書き出しルーチンを流用して出力する default: var kifu = new KifuManager(); // 経路を消すためにsfen化して代入しなおして書き出す kifu.FromString($"sfen {Position.ToSfen()}"); kifu.KifuHeader.PlayerNameBlack = KifuHeader.PlayerNameBlack; kifu.KifuHeader.PlayerNameWhite = KifuHeader.PlayerNameWhite; result = kifu.ToString(kt); break; // ToDo : 他の形式もサポートする } return(result); }
/// <summary> /// 棋譜ファイルを書き出す /// フォーマットは引数のkfで指定する。 /// </summary> /// <param name="filename"></param> /// <param name="kf"></param> public string ToString(KifuFileType kt) { // 局面をrootに移動(あとで戻す) var moves = Tree.RewindToRoot(); string result = string.Empty; switch (kt) { case KifuFileType.SFEN: result = ToSfenString(); break; case KifuFileType.PSN: case KifuFileType.PSN2: result = ToPsnString(kt); break; // ToDo : 他の形式もサポートする } // 呼び出された時の局面に戻す Tree.RewindToRoot(); Tree.FastForward(moves); return(result); }
// -- static method /// <summary> /// rootSfen(開始局面のsfen)とmoves(手順)を指定して、棋譜形式の文字列を得る。 /// /// エラーがあればnullが返る。 /// </summary> /// <param name="rootSfen"></param> /// <param name="moves"></param> /// <returns></returns> public static string ToStringFromRootSfenAndMoves(KifuFileType type, string rootSfen, List <Move> moves) { // 一度、sfenから生成するのがお手軽か? var kifuManager = new KifuManager(); var sfen = Core.Util.RootSfenAndMovesToUsiString(rootSfen, moves); var error = kifuManager.FromString(sfen); return(error == null?kifuManager.ToString(type) : null); }
/// <summary> /// CSA形式の棋譜ファイルのparser /// エラーがあった場合は、そのエラーの文字列が返る。 /// エラーがなければstring.Emptyが返る。 /// </summary> private string FromCsaString(string[] lines, KifuFileType kf) { var lineNo = 1; /* * 例) * * V2.2 * N+人間 * N-人間 * P1-KY-KE-GI-KI-OU-KI-GI-KE-KY * P2 * -HI * * * * * -KA * * P3-FU-FU-FU-FU-FU-FU-FU-FU-FU * P4 * * * * * * * * * * P5 * * * * * * * * * * P6 * * * * * * * * * * P7+FU+FU+FU+FU+FU+FU+FU+FU+FU * P8 * +KA * * * * * +HI * * P9+KY+KE+GI+KI+OU+KI+GI+KE+KY * P+ * P- + +7776FU,T3 + -8384FU,T1 */ string line = string.Empty; for (; lineNo <= lines.Length; ++lineNo) { line = lines[lineNo - 1]; if (line.StartsWith("N+")) { playerName[(int)Color.BLACK] = line.Substring(2); continue; } if (line.StartsWith("N-")) { playerName[(int)Color.WHITE] = line.Substring(2); continue; } } if (!line.StartsWith("P1")) // 局面図が来た { return(string.Format("CSA形式の{0}行目で局面図が来ずにファイルが終了しました。", lineNo)); } // 書きかけ return(string.Empty); }
// JSON形式棋譜の読み込み private string FromJsonString(string content, KifuFileType kf) { switch (kf) { case KifuFileType.JKF: return(FromJkfString(content, kf)); case KifuFileType.JSON: return(FromLiveJsonString(content, kf)); default: return(string.Empty); } }
/// <summary> /// 棋譜ファイルを書き出す /// フォーマットは引数のkfで指定する。 /// </summary> /// <param name="filename"></param> /// <param name="kf"></param> public string ToString(KifuFileType kt) { // -- イベントの一時抑制 // KifuListの更新通知がいっぱい発生すると棋譜ウィンドウが乱れるため。 var e1 = Tree.PropertyChangedEventEnable; Tree.PropertyChangedEventEnable = false; // 棋譜ウィンドウを操作してはならないので棋譜ウィンドウとのsyncを停止させておく。 var e2 = Tree.EnableKifuList; Tree.EnableKifuList = false; // 局面をrootに移動(あとで戻す) var moves = Tree.RewindToRoot(); string result = string.Empty; switch (kt) { case KifuFileType.SFEN: result = ToSfenString(); break; case KifuFileType.PSN: case KifuFileType.PSN2: result = ToPsnString(kt); break; case KifuFileType.CSA: result = ToCsaString(); break; case KifuFileType.KIF: result = ToKifString(); break; case KifuFileType.KI2: result = ToKi2String(); break; case KifuFileType.JKF: result = ToJkfString(); break; case KifuFileType.JSON: result = ToJsonString(); break; // ToDo : 他の形式もサポートする } // 呼び出された時の局面に戻す Tree.RewindToRoot(); Tree.FastForward(moves); Tree.EnableKifuList = e2; Tree.PropertyChangedEventEnable = e1; return(result); }
// とりあえずJSON中継棋譜形式に部分対応 private string FromLiveJsonString(string content, KifuFileType kf) { try { var times = KifuMoveTimes.Zero; var timeSettings = KifuTimeSettings.TimeLimitless.Clone(); var epoch = new DateTime(1970, 1, 1, 0, 0, 0).ToLocalTime(); DateTime?lasttime = null; var jsonObj = LiveJsonUtil.FromString(content); if (jsonObj == null || jsonObj.data == null || jsonObj.data.Count == 0) { return("有効なデータが得られませんでした"); } // 先頭のデータのみ読み込む var data = jsonObj.data[0]; { // 対局者名 if (data.side != "後手") { KifuHeader.PlayerNameBlack = data.player1; KifuHeader.PlayerNameWhite = data.player2; } else { KifuHeader.PlayerNameBlack = data.player2; KifuHeader.PlayerNameWhite = data.player1; } } if (data.realstarttime != null) { var starttime = epoch.AddMilliseconds((double)data.realstarttime); lasttime = starttime; Tree.RootKifuLog.moveTime = starttime; Tree.rootNode.comment = starttime.ToString("o"); } else if (!string.IsNullOrWhiteSpace(data.starttime)) { Tree.RootKifuLog.moveTime = DateTime.ParseExact(data.starttime, "s", null); } if (!string.IsNullOrWhiteSpace(data.timelimit) && int.TryParse(data.timelimit, out int time_limit)) { timeSettings = new KifuTimeSettings( new KifuTimeSetting[] { new KifuTimeSetting() { Hour = 0, Minute = time_limit, Second = 0 }, new KifuTimeSetting() { Hour = 0, Minute = time_limit, Second = 0 }, }, false ); } if (!string.IsNullOrWhiteSpace(data.countdown) && int.TryParse(data.countdown, out int countdown)) { foreach (var players in timeSettings.Players) { if (players.TimeLimitless) { players.Hour = 0; players.Minute = 0; players.Second = 0; players.TimeLimitless = false; } players.Byoyomi = countdown; players.ByoyomiEnable = true; } } // 残り時間の計算用。 times = timeSettings.GetInitialKifuMoveTimes(); Tree.SetKifuMoveTimes(times.Clone()); // root局面での残り時間の設定 Tree.KifuTimeSettings = timeSettings.Clone(); foreach (var kif in data.kif) { Move move; DateTime?time = null; if (kif.time != null) { time = epoch.AddMilliseconds((double)kif.time); } // 特殊な着手 if (kif.frX == null || kif.frY == null || kif.toX == null || kif.toY == null || kif.prmt == null) { switch (kif.move) { case "投了": move = Move.RESIGN; break; case "中断": case "封じ手": move = Move.INTERRUPT; break; default: return(string.Empty); } } // varidation else if (kif.frX < 1 || kif.frX > 9 || kif.toY < 0 || kif.toY > 10) { return("無効な移動元座標を検出しました"); } else if (kif.toX < 1 || kif.toX > 9 || kif.toY < 1 || kif.toY > 9) { return("無効な移動先座標を検出しました"); } // 先手駒台からの着手 else if (kif.frY == 10) { Piece pc; switch (kif.frX) { case 1: pc = Piece.PAWN; break; case 2: pc = Piece.LANCE; break; case 3: pc = Piece.KNIGHT; break; case 4: pc = Piece.SILVER; break; case 5: pc = Piece.GOLD; break; case 6: pc = Piece.BISHOP; break; case 7: pc = Piece.ROOK; break; default: return("先手の無効な駒打ちを検出しました"); } var toSq = Util.MakeSquare((File)(kif.toX - 1), (Rank)(kif.toY - 1)); move = Util.MakeMoveDrop(pc, toSq); } // 後手駒台からの着手 else if (kif.frY == 0) { Piece pc; switch (kif.frX) { case 9: pc = Piece.PAWN; break; case 8: pc = Piece.LANCE; break; case 7: pc = Piece.KNIGHT; break; case 6: pc = Piece.SILVER; break; case 5: pc = Piece.GOLD; break; case 4: pc = Piece.BISHOP; break; case 3: pc = Piece.ROOK; break; default: return("後手の無効な駒打ちを検出しました"); } var toSq = Util.MakeSquare((File)(kif.toX - 1), (Rank)(kif.toY - 1)); move = Util.MakeMoveDrop(pc, toSq); } // 通常の着手 else { var frSq = Util.MakeSquare((File)(kif.frX - 1), (Rank)(kif.frY - 1)); var toSq = Util.MakeSquare((File)(kif.toX - 1), (Rank)(kif.toY - 1)); if (kif.prmt == 1) { move = Util.MakeMovePromote(frSq, toSq); } else { move = Util.MakeMove(frSq, toSq); } } TimeSpan thinking_time = TimeSpan.FromSeconds((double)(kif.spend ?? 0)); TimeSpan realthinking_time = (time != null && lasttime != null) ? time.GetValueOrDefault().Subtract(lasttime.GetValueOrDefault()) : thinking_time; var turn = Tree.position.sideToMove; times.Players[(int)turn] = times.Players[(int)turn].Create( timeSettings.Player(turn), thinking_time, realthinking_time ); // 棋譜ツリーへの追加処理 Tree.AddNode(move, times.Clone()); if (time != null) { lasttime = time; var kifumove = Tree.currentNode.moves.Find((x) => x.nextMove == move); kifumove.moveTime = (DateTime)time; Tree.currentNode.comment = ((DateTime)time).ToString("o"); } if (move.IsSpecial()) { return(string.Empty); } if (!Tree.position.IsLegal(move)) { return($"{Tree.gamePly}手目で不正着手を検出しました"); } Tree.DoMove(move); continue; } } catch (Exception e) { return(e.ToString()); } return(string.Empty); }
// JSON Kifu Format private string FromJkfString(string content, KifuFileType kf) { try { var CSA_PIECE = new string[] { " ", "FU", "KY", "KE", "GI", "KA", "HI", "KI", "OU", "TO", "NY", "NK", "NG", "UM", "RY", "QU", " ", "FU", "KY", "KE", "GI", "KA", "HI", "KI", "OU", "TO", "NY", "NK", "NG", "UM", "RY", "QU", }; var jsonObj = JkfUtil.FromString(content); if (jsonObj == null) { return("有効なデータが得られませんでした"); } if (jsonObj.header != null) { foreach (var key in jsonObj.header.Keys) { var trimedKey = key.Trim(' ', '\t', '\n', '\r', ' ', '\x0b', '\x00'); KifuHeader.header_dic[trimedKey] = jsonObj.header[key]; } } // Treeに局面をセットする void SetTree(BoardType bt) { Tree.rootSfen = bt.ToSfen(); Tree.position.SetSfen(Tree.rootSfen); Tree.rootBoardType = bt; } if (jsonObj.initial != null) { switch (jsonObj.initial.preset) { case "HIRATE": SetTree(BoardType.NoHandicap); break; case "KY": SetTree(BoardType.HandicapKyo); break; case "KY_R": SetTree(BoardType.HandicapRightKyo); break; case "KA": SetTree(BoardType.HandicapKaku); break; case "HI": SetTree(BoardType.HandicapHisya); break; case "HIKY": SetTree(BoardType.HandicapHisyaKyo); break; case "2": SetTree(BoardType.Handicap2); break; case "3": SetTree(BoardType.Handicap3); break; case "4": SetTree(BoardType.Handicap4); break; case "5": SetTree(BoardType.Handicap5); break; case "5_L": SetTree(BoardType.HandicapLeft5); break; case "6": SetTree(BoardType.Handicap6); break; case "8": SetTree(BoardType.Handicap8); break; case "10": SetTree(BoardType.Handicap10); break; case "OTHER": Tree.rootBoardType = BoardType.Others; if (jsonObj.initial.data == null) { return("初期局面が指定されていません"); } var color = jsonObj.initial.data.color == 0 ? Color.BLACK : Color.WHITE; var board = new Piece[81]; for (File f = File.FILE_1; f <= File.FILE_9; ++f) { for (Rank r = Rank.RANK_1; r <= Rank.RANK_9; ++r) { var sqi = Util.MakeSquare(f, r).ToInt(); var p = jsonObj.initial.data.board[f.ToInt(), r.ToInt()]; switch (p.color) { case 0: switch (p.kind) { case "FU": board[sqi] = Piece.B_PAWN; break; case "KY": board[sqi] = Piece.B_LANCE; break; case "KE": board[sqi] = Piece.B_KNIGHT; break; case "GI": board[sqi] = Piece.B_SILVER; break; case "KA": board[sqi] = Piece.B_BISHOP; break; case "HI": board[sqi] = Piece.B_ROOK; break; case "KI": board[sqi] = Piece.B_GOLD; break; case "OU": board[sqi] = Piece.B_KING; break; case "TO": board[sqi] = Piece.B_PRO_PAWN; break; case "NY": board[sqi] = Piece.B_PRO_LANCE; break; case "NK": board[sqi] = Piece.B_PRO_KNIGHT; break; case "NG": board[sqi] = Piece.B_PRO_SILVER; break; case "UM": board[sqi] = Piece.B_HORSE; break; case "RY": board[sqi] = Piece.B_DRAGON; break; default: board[sqi] = Piece.NO_PIECE; break; } break; case 1: switch (p.kind) { case "FU": board[sqi] = Piece.W_PAWN; break; case "KY": board[sqi] = Piece.W_LANCE; break; case "KE": board[sqi] = Piece.W_KNIGHT; break; case "GI": board[sqi] = Piece.W_SILVER; break; case "KA": board[sqi] = Piece.W_BISHOP; break; case "HI": board[sqi] = Piece.W_ROOK; break; case "KI": board[sqi] = Piece.W_GOLD; break; case "OU": board[sqi] = Piece.W_KING; break; case "TO": board[sqi] = Piece.W_PRO_PAWN; break; case "NY": board[sqi] = Piece.W_PRO_LANCE; break; case "NK": board[sqi] = Piece.W_PRO_KNIGHT; break; case "NG": board[sqi] = Piece.W_PRO_SILVER; break; case "UM": board[sqi] = Piece.W_HORSE; break; case "RY": board[sqi] = Piece.W_DRAGON; break; default: board[sqi] = Piece.NO_PIECE; break; } break; default: board[sqi] = Piece.NO_PIECE; break; } } } var hands = new Hand[2] { Hand.ZERO, Hand.ZERO }; if (jsonObj.initial.data.hands != null && jsonObj.initial.data.hands.Count >= 2) { foreach (var c in new Color[] { Color.BLACK, Color.WHITE }) { if (jsonObj.initial.data.hands[c.ToInt()] != null) { foreach (var p in new Piece[] { Piece.PAWN, Piece.LANCE, Piece.KNIGHT, Piece.SILVER, Piece.GOLD, Piece.BISHOP, Piece.ROOK }) { int value; if (jsonObj.initial.data.hands[c.ToInt()].TryGetValue(CSA_PIECE[p.ToInt()], out value)) { hands[c.ToInt()].Add(p, value); } } } } } Tree.rootSfen = Position.SfenFromRawdata(board, hands, color, 1); Tree.position.SetSfen(Tree.rootSfen); break; default: return("初期局面が不明です"); } } if (jsonObj.moves != null) { Move m = Move.NONE; foreach (var jkfMove in jsonObj.moves) { TimeSpan spend = (jkfMove.time != null && jkfMove.time.now != null) ? new TimeSpan(jkfMove.time.now.h ?? 0, jkfMove.time.now.m, jkfMove.time.now.s): TimeSpan.Zero; if (!string.IsNullOrWhiteSpace(jkfMove.special)) { switch (jkfMove.special) { case "TORYO": m = Move.RESIGN; break; case "CHUDAN": m = Move.INTERRUPT; break; case "SENNICHITE": m = Move.REPETITION_DRAW; break; case "TIME_UP": m = Move.TIME_UP; break; case "JISHOGI": m = Move.MAX_MOVES_DRAW; break; case "KACHI": m = Move.WIN; break; case "HIKIWAKE": m = Move.DRAW; break; case "TSUMI": m = Move.MATED; break; case "ILLEGAL_MOVE": m = Move.ILLEGAL_MOVE; break; case "+ILLEGAL_ACTION": m = Tree.position.sideToMove == Color.BLACK ? Move.ILLEGAL_ACTION_LOSE : Move.ILLEGAL_ACTION_WIN; break; case "-ILLEGAL_ACTION": m = Tree.position.sideToMove == Color.BLACK ? Move.ILLEGAL_ACTION_WIN : Move.ILLEGAL_ACTION_LOSE; break; // 以下、適切な変換先不明 case "ERROR": case "FUZUMI": case "MATTA": default: m = Move.NONE; break; } } else if (jkfMove.move == null) { m = Move.NONE; } else if (jkfMove.move.to == null) { m = Move.NONE; } else if (jkfMove.move.from == null) { File f = (File)(jkfMove.move.to.x - 1); Rank r = (Rank)(jkfMove.move.to.y - 1); if (f < File.FILE_1 || f > File.FILE_9 || r < Rank.RANK_1 || r > Rank.RANK_9) { m = Move.NONE; } else { Square sq = Util.MakeSquare(f, r); switch (jkfMove.move.piece) { case "FU": m = Util.MakeMoveDrop(Piece.PAWN, sq); break; case "KY": m = Util.MakeMoveDrop(Piece.LANCE, sq); break; case "KE": m = Util.MakeMoveDrop(Piece.KNIGHT, sq); break; case "GI": m = Util.MakeMoveDrop(Piece.SILVER, sq); break; case "KI": m = Util.MakeMoveDrop(Piece.GOLD, sq); break; case "KA": m = Util.MakeMoveDrop(Piece.BISHOP, sq); break; case "HI": m = Util.MakeMoveDrop(Piece.ROOK, sq); break; default: m = Move.NONE; break; } } } else { File frF = (File)(jkfMove.move.from.x - 1); Rank frR = (Rank)(jkfMove.move.from.y - 1); File toF = (File)(jkfMove.move.to.x - 1); Rank toR = (Rank)(jkfMove.move.to.y - 1); if ( frF < File.FILE_1 || frF > File.FILE_9 || frR < Rank.RANK_1 || frR > Rank.RANK_9 || frF < File.FILE_1 || toF > File.FILE_9 || frR < Rank.RANK_1 || toR > Rank.RANK_9 ) { m = Move.NONE; } else { Square frSq = Util.MakeSquare(frF, frR); Square toSq = Util.MakeSquare(toF, toR); if (jkfMove.move.promote == true) { m = Util.MakeMovePromote(frSq, toSq); } else { m = Util.MakeMove(frSq, toSq); } } } Tree.AddNode(m, KifuMoveTimes.Zero /*ToDo:あとでちゃんと書く*/ /* spend */); if (m.IsSpecial() || !Tree.position.IsLegal(m)) { break; } Tree.DoMove(m); } } // ToDo: 分岐棋譜を読み込む } catch (Exception e) { return(e.ToString()); } return(string.Empty); }
/// <summary> /// CSA形式の棋譜ファイルのparser /// エラーがあった場合は、そのエラーの文字列が返る。 /// エラーがなければstring.Emptyが返る。 /// </summary> private string FromCsaString(string[] lines, KifuFileType kf) { // 消費時間、残り時間、消費時間を管理する。 var timeSettings = KifuTimeSettings.TimeLimitless; var times = timeSettings.GetInitialKifuMoveTimes(); var lineNo = 1; /* * 例) * * V2.2 * N+人間 * N-人間 * P1-KY-KE-GI-KI-OU-KI-GI-KE-KY * P2 * -HI * * * * * -KA * * P3-FU-FU-FU-FU-FU-FU-FU-FU-FU * P4 * * * * * * * * * * P5 * * * * * * * * * * P6 * * * * * * * * * * P7+FU+FU+FU+FU+FU+FU+FU+FU+FU * P8 * +KA * * * * * +HI * * P9+KY+KE+GI+KI+OU+KI+GI+KE+KY * P+ * P- + +7776FU,T3 + -8384FU,T1 */ string line = string.Empty; var posLines = new List <string>(); var headFlag = true; KifuMove lastKifuMove = null; var move = Move.NONE; var rTimeLimit = new Regex(@"([0-9]+):([0-9]+)\+([0-9]+)"); var rEvent = new Regex(@"^[0-9A-Za-z-_]+\+[0-9A-Za-z_]+-([0-9]+)-([0-9]+)(F?)\+"); for (; lineNo <= lines.Length; ++lineNo) { line = lines[lineNo - 1]; // コメント文 if (line.StartsWith("'")) { Tree.currentNode.comment += line.Substring(1).TrimEnd('\r', '\n') + "\n"; continue; } // セパレータ検出 if (line.StartsWith("/")) { // "/"だけの行を挟んで、複数の棋譜・局面を記述することができる。 // 現時点ではこの書式に対応せず、先頭の棋譜のみを読み込む。 // そもそも初期局面が異なる可能性もあり、Treeを構成できるとは限らないため。 break; } // マルチステートメント検出 string[] sublines = line.Split(','); foreach (var subline in sublines) { if (subline.StartsWith("$")) { // 棋譜ヘッダ var keyvalue = subline.Substring(1).Split(":".ToCharArray(), 2); if (string.IsNullOrWhiteSpace(keyvalue[0])) { continue; } var key = keyvalue[0]; var value = keyvalue[1] ?? ""; KifuHeader.header_dic[key] = value; switch (key) { case "TIME_LIMIT": var mTimeLimit = rTimeLimit.Match(value); if (mTimeLimit.Success) { int.TryParse(mTimeLimit.Groups[1].Value, out int hour); int.TryParse(mTimeLimit.Groups[2].Value, out int minute); int.TryParse(mTimeLimit.Groups[3].Value, out int byoyomi); timeSettings = new KifuTimeSettings( new KifuTimeSetting[] { new KifuTimeSetting() { Hour = hour, Minute = minute, Second = 0, Byoyomi = byoyomi, ByoyomiEnable = true, IncTime = 0 }, new KifuTimeSetting() { Hour = hour, Minute = minute, Second = 0, Byoyomi = byoyomi, ByoyomiEnable = true, IncTime = 0 }, }, false ); times = timeSettings.GetInitialKifuMoveTimes(); Tree.SetKifuMoveTimes(times.Clone()); // root局面での残り時間の設定 Tree.KifuTimeSettings = timeSettings.Clone(); } break; case "EVENT": // floodgate特例、TIME_LIMITが設定されていない時にEVENT文字列から時間設定を拾う if (KifuHeader.header_dic.ContainsKey("TIME_LIMIT")) { continue; } var mEvent = rEvent.Match(value); if (mEvent.Success) { int.TryParse(mEvent.Groups[1].Value, out int initial); int.TryParse(mEvent.Groups[2].Value, out int add); if (mEvent.Groups[3].Value == "F") { timeSettings = new KifuTimeSettings( new KifuTimeSetting[] { new KifuTimeSetting() { Hour = 0, Minute = 0, Second = initial, Byoyomi = 0, ByoyomiEnable = false, IncTime = add, IncTimeEnable = true }, new KifuTimeSetting() { Hour = 0, Minute = 0, Second = initial, Byoyomi = 0, ByoyomiEnable = false, IncTime = add, IncTimeEnable = true }, }, false ); } else { timeSettings = new KifuTimeSettings( new KifuTimeSetting[] { new KifuTimeSetting() { Hour = 0, Minute = 0, Second = initial, Byoyomi = add, ByoyomiEnable = true, IncTime = 0, IncTimeEnable = false }, new KifuTimeSetting() { Hour = 0, Minute = 0, Second = initial, Byoyomi = add, ByoyomiEnable = true, IncTime = 0, IncTimeEnable = false }, }, false ); } times = timeSettings.GetInitialKifuMoveTimes(); Tree.SetKifuMoveTimes(times.Clone()); // root局面での残り時間の設定 Tree.KifuTimeSettings = timeSettings.Clone(); } break; } continue; } if (subline.StartsWith("N+")) { KifuHeader.PlayerNameBlack = subline.Substring(2); continue; } if (subline.StartsWith("N-")) { KifuHeader.PlayerNameWhite = subline.Substring(2); continue; } if (subline.StartsWith("P")) { posLines.Add(subline); continue; } if (subline.StartsWith("+") || subline.StartsWith("-")) { if (headFlag) { // 1回目は局面の先後とみなす headFlag = false; posLines.Add(subline); Tree.rootSfen = CsaExtensions.CsaToSfen(posLines.ToArray()); Tree.position.SetSfen(Tree.rootSfen); continue; } // 2回目以降は指し手とみなす // 消費時間の情報がまだないが、取り敢えず追加 move = Tree.position.FromCSA(subline); Tree.AddNode(move, times.Clone()); lastKifuMove = Tree.currentNode.moves.FirstOrDefault((x) => x.nextMove == move); // 特殊な指し手や不正な指し手ならDoMove()しない if (move.IsSpecial() || !Tree.position.IsLegal(move)) { continue; } Tree.DoMove(move); continue; } if (subline.StartsWith("T")) { // 着手時間 var state = Tree.position.State(); if (state == null) { return($"line {lineNo}: 初期局面で着手時間が指定されました。"); } long.TryParse(subline.Substring(1), out long time); var lastMove = state.lastMove; if (move == lastMove && move == lastKifuMove.nextMove) { var turn = Tree.position.sideToMove.Not(); var thinking_time = TimeSpan.FromSeconds(time); times.Players[(int)turn] = times.Players[(int)turn].Create( timeSettings.Player(turn), thinking_time, thinking_time ); lastKifuMove.kifuMoveTimes = times.Clone(); } // 特殊な指し手や不正な指し手ならDoMove()しない if (move.IsSpecial() || !Tree.position.IsLegal(move)) { continue; } Tree.DoMove(move); continue; } if (subline.StartsWith("%")) { var match = new Regex("^%[A-Z_+-]+").Match(subline); if (!match.Success) { continue; } switch (match.Groups[0].Value) { case "%TORYO": move = Move.RESIGN; break; case "%CHUDAN": move = Move.INTERRUPT; break; case "%SENNICHITE": move = Move.REPETITION_DRAW; break; case "%TIME_UP": move = Move.TIME_UP; break; case "%JISHOGI": move = Move.MAX_MOVES_DRAW; break; case "%KACHI": move = Move.WIN; break; case "%TSUMI": move = Move.MATED; break; case "%ILLEGAL_MOVE": move = Move.ILLEGAL_MOVE; break; case "%+ILLEGAL_ACTION": move = Tree.position.sideToMove == Color.BLACK ? Move.ILLEGAL_ACTION_LOSE : Move.ILLEGAL_ACTION_WIN; break; case "%-ILLEGAL_ACTION": move = Tree.position.sideToMove == Color.BLACK ? Move.ILLEGAL_ACTION_WIN : Move.ILLEGAL_ACTION_LOSE; break; case "%HIKIWAKE": move = Move.DRAW; break; // 以下、適切な変換先不明 case "%FUZUMI": case "%MATTA": case "%ERROR": default: move = Move.NONE; break; } Tree.AddNode(move, times.Clone()); lastKifuMove = Tree.currentNode.moves.FirstOrDefault((x) => x.nextMove == move); continue; } } } if (headFlag) // まだ局面図が終わってない { return($"CSA形式の{lineNo}行目で局面図が来ずにファイルが終了しました。"); } return(null); }
/// <summary> /// Kif/KI2形式の読み込み /// </summary> /// <param name="lines"></param> /// <param name="kf"></param> /// <returns></returns> private string FromKifString(string[] lines, KifuFileType kf) { // 消費時間、残り時間、消費時間を管理する。 // TimeLimitlessの設定を書き換えてしまう恐れがあるためCloneする KifuTimeSettings timeSettings = KifuTimeSettings.TimeLimitless.Clone(); KifuMoveTimes times = KifuMoveTimes.Zero; var lineNo = 1; try { // ヘッダ検出用正規表現 var rHead = new Regex(@"^([^:]+):(.*)"); // 変化手数用正規表現 var rHenka = new Regex(@"^([0-9]+)手?"); // KIF指し手検出用正規表現 var rKif = new Regex(@"^\s*([0-9]+)\s*(?:((?:[1-91-9][1-91-9一二三四五六七八九]|同\s?)成?[歩香桂銀金角飛と杏圭全馬竜龍玉王][打不成左直右上寄引]*(?:\([1-9][1-9]\))?)|(\S+))\s*(\(\s*([0-9]+):([0-9]+(?:\.[0-9]+)?)\s*\/\s*([0-9]+):([0-9]+):([0-9]+(?:\.[0-9]+)?)\))?"); // KI2指し手検出用正規表現 var rKi2 = new Regex(@"[-+▲△▼▽☗☖⛊⛉](?:[1-91-9][1-91-9一二三四五六七八九]|同\s?)成?[歩香桂銀金角飛と杏圭全馬竜龍玉王][打不成左直右上寄引]*"); // 終局検出用正規表現 var rSpecial = new Regex(@"^まで([0-9]+)手(.+)"); // 持ち時間/秒読み検出用正規表現 var rTime = new Regex(@"^各?(\d+)(時間|分|秒)"); var bod = new List <string>(); var isBody = false; KifuHeader.header_dic.Clear(); // 初期局面の遅延処理 Func <string> lazyHead = () => { isBody = true; if (bod.Count > 0) { // 柿木将棋IXでは、初期局面指定(詰将棋など)の時でも、KIF形式で書き出すと「手合割:平手」とヘッダ出力される。 // その場合の手合割の意味が理解出来ないが、エラーを出さずに黙って初期局面図の方で上書きする。 // if (KifuHeader.header_dic.ContainsKey("手合割")) return "手合割と初期局面文字列が同時に指定されています。"; var sfen = Converter.KifExtensions.BodToSfen(bod.ToArray()); Tree.SetRootSfen(sfen); } if (KifuHeader.header_dic.ContainsKey("持ち時間")) { var mTime = rTime.Match(KifuHeader.header_dic["持ち時間"]); if (mTime.Success) { var sb = new StringBuilder(); foreach (char c in mTime.Groups[1].Value) { sb.Append((c <'0' || c> '9') ? c : (char)(c - '0' + '0')); } if (int.TryParse(sb.ToString(), out int mTimeVal)) { //if (int.TryParse(Regex.Replace(mTime.Groups[1].Value, "[0-9]", p => ((char)(p.Value[0] - '0' + '0')).ToString())), out int mTimeVal); switch (mTime.Groups[2].Value) { case "時間": timeSettings = new KifuTimeSettings( new KifuTimeSetting[] { new KifuTimeSetting() { Hour = mTimeVal, Minute = 0, Second = 0 }, new KifuTimeSetting() { Hour = mTimeVal, Minute = 0, Second = 0 }, }, false ); break; case "分": timeSettings = new KifuTimeSettings( new KifuTimeSetting[] { new KifuTimeSetting() { Hour = 0, Minute = mTimeVal, Second = 0 }, new KifuTimeSetting() { Hour = 0, Minute = mTimeVal, Second = 0 }, }, false ); break; case "秒": timeSettings = new KifuTimeSettings( new KifuTimeSetting[] { new KifuTimeSetting() { Hour = 0, Minute = 0, Second = mTimeVal }, new KifuTimeSetting() { Hour = 0, Minute = 0, Second = mTimeVal }, }, false ); break; } } } } if (KifuHeader.header_dic.ContainsKey("秒読み")) { var mTime = rTime.Match(KifuHeader.header_dic["秒読み"]); if (mTime.Success) { var sb = new StringBuilder(); foreach (char c in mTime.Groups[1].Value) { sb.Append((c <'0' || c> '9') ? c : (char)(c - '0' + '0')); } if (int.TryParse(sb.ToString(), out int mTimeVal)) { //if (int.TryParse(Regex.Replace(mTime.Groups[1].Value, "[0-9]", p => ((char)(p.Value[0] - '0' + '0')).ToString())), out int mTimeVal); foreach (var players in timeSettings.Players) { if (players.TimeLimitless) { players.Hour = 0; players.Minute = 0; players.Second = 0; players.TimeLimitless = false; } switch (mTime.Groups[2].Value) { case "時間": players.Byoyomi = int.Parse(mTime.Groups[1].Value) * 3600; players.ByoyomiEnable = true; break; case "分": players.Byoyomi = int.Parse(mTime.Groups[1].Value) * 60; players.ByoyomiEnable = true; break; case "秒": players.Byoyomi = int.Parse(mTime.Groups[1].Value); players.ByoyomiEnable = true; break; } } } } } // 残り時間の計算用。 times = timeSettings.GetInitialKifuMoveTimes(); Tree.SetKifuMoveTimes(times.Clone()); // root局面での残り時間の設定 Tree.KifuTimeSettings = timeSettings.Clone(); return(string.Empty); }; // ブロック分割走査 for (; lineNo <= lines.Length; ++lineNo) { var line = lines[lineNo - 1].Trim('\r', '\n'); // 空文 if (string.IsNullOrWhiteSpace(line)) { continue; } var firstChar = line[0]; // 無効行 if (firstChar == '#') { continue; } if (firstChar == '&') { continue; // Kifu for Windows "しおり"機能 } // 棋譜コメント文 if (firstChar == '*') { Tree.currentNode.comment += line + "\n"; continue; } // 局面図 if (firstChar == '|') { bod.Add(line); continue; } // ヘッダ検出 var mHead = rHead.Match(line); if (mHead.Success) { var headerKey = mHead.Groups[1].Value; var headerValue = mHead.Groups[2].Value; switch (headerKey) { case "先手の持駒": case "下手の持駒": case "後手の持駒": case "上手の持駒": if (isBody) { throw new KifuException("対局開始後にヘッダが指定されました。", line); } bod.Add(line); goto nextline; case "変化": if (!isBody) { throw new KifuException("初期局面からは変化できません。"); } var mHenka = rHenka.Match(headerValue); if (!mHenka.Success) { throw new KifuException("変化する手数を検出できませんでした。"); } var ply = int.Parse(mHenka.Groups[1].Value); while (ply < Tree.gamePly) { Tree.UndoMove(); } // このnodeでの残り時間に戻す times = Tree.GetKifuMoveTimes(); goto nextline; case "手合割": if (isBody) { throw new KifuException("対局開始後にヘッダが指定されました。", line); } KifuHeader.header_dic.Add(headerKey, headerValue); // 局面を指定されたBoardTypeで初期化する。 void SetTree(BoardType bt) { Tree.SetRootBoardType(bt); } switch (headerValue) { case "平手": SetTree(BoardType.NoHandicap); goto nextline; case "香落ち": SetTree(BoardType.HandicapKyo); goto nextline; case "右香落ち": SetTree(BoardType.HandicapRightKyo); goto nextline; case "角落ち": SetTree(BoardType.HandicapKaku); goto nextline; case "飛車落ち": SetTree(BoardType.HandicapHisya); goto nextline; case "飛香落ち": SetTree(BoardType.HandicapHisyaKyo); goto nextline; case "二枚落ち": SetTree(BoardType.Handicap2); goto nextline; case "三枚落ち": SetTree(BoardType.Handicap3); goto nextline; case "四枚落ち": SetTree(BoardType.Handicap4); goto nextline; case "五枚落ち": SetTree(BoardType.Handicap5); goto nextline; case "左五枚落ち": SetTree(BoardType.HandicapLeft5); goto nextline; case "六枚落ち": SetTree(BoardType.Handicap6); goto nextline; case "八枚落ち": SetTree(BoardType.Handicap8); goto nextline; case "十枚落ち": SetTree(BoardType.Handicap10); goto nextline; default: // このときlazyHead()で設定される。 break; } goto nextline; case "先手": case "下手": if (isBody) { throw new KifuException("対局開始後にヘッダが指定されました。", line); } KifuHeader.header_dic.Add(headerKey, headerValue); KifuHeader.PlayerNameBlack = headerValue; goto nextline; case "後手": case "上手": if (isBody) { throw new KifuException("対局開始後にヘッダが指定されました。", line); } KifuHeader.header_dic.Add(headerKey, headerValue); KifuHeader.PlayerNameWhite = headerValue; goto nextline; default: if (isBody) { throw new KifuException("対局開始後にヘッダが指定されました。", line); } KifuHeader.header_dic.Add(headerKey, headerValue); goto nextline; } } foreach (var bodKey in new string[] { "先手番", "後手番", "上手番", "下手番", "手数=", }) { if (line.StartsWith(bodKey)) { if (isBody) { throw new KifuException("対局開始後にヘッダが指定されました。", line); } bod.Add(line); goto nextline; } } // KIF形式検出 var mKif = rKif.Match(line); if (mKif.Success) { if (!isBody) { var headRes = lazyHead(); if (headRes != string.Empty) { return(headRes); } } var ply = int.Parse(mKif.Groups[1].Value); if (Tree.gamePly != ply) { throw new KifuException($"手数({Tree.gamePly})が一致しません。", line); } Move move; if (mKif.Groups[2].Success) { move = Tree.position.FromKif(mKif.Groups[2].Value); if (!Tree.position.IsLegal(move)) { // これだと不正着手後の棋譜コメントを取れないがとりあえず解析を中止する throw new KifuException("不正着手を検出しました。", line); } } else { switch (mKif.Groups[3].Value) { case "投了": move = Move.RESIGN; break; case "中断": case "封じ手": move = Move.INTERRUPT; break; case "千日手": move = Move.REPETITION_DRAW; break; case "詰み": move = Move.MATED; break; case "時間切れ": case "切れ負け": move = Move.TIME_UP; break; case "パス": move = Move.NULL; break; case "持将棋": move = Move.MAX_MOVES_DRAW; break; case "勝ち宣言": move = Move.WIN; break; default: move = Move.NONE; break; } } if (move == Move.NONE) { throw new KifuException("指し手を解析できませんでした。", line); } TimeSpan thinking_time = TimeSpan.Zero; TimeSpan total_time = TimeSpan.Zero; if (mKif.Groups[4].Success) { // TimeSpan.TryParse 系では "80:00" とかを解釈しないので自前処理する thinking_time = TimeSpan.FromMinutes(double.Parse(mKif.Groups[5].Value)) + TimeSpan.FromSeconds(double.Parse(mKif.Groups[6].Value)); total_time = TimeSpan.FromHours(double.Parse(mKif.Groups[7].Value)) + TimeSpan.FromMinutes(double.Parse(mKif.Groups[8].Value)) + TimeSpan.FromSeconds(double.Parse(mKif.Groups[9].Value)); } var turn = Tree.position.sideToMove; times.Players[(int)turn] = times.Players[(int)turn].Create( timeSettings.Player(turn), thinking_time, thinking_time, total_time /*消費時間は棋譜に記録されているものをそのまま使用する*/ /*残り時間は棋譜上に記録されていない*/ ); Tree.AddNode(move, times.Clone()); if (move.IsOk()) { Tree.DoMove(move); } goto nextline; } // KI2形式検出 var mKi2 = rKi2.Matches(line); if (mKi2.Count > 0) { if (!isBody) { var headRes = lazyHead(); if (headRes != string.Empty) { return(headRes); } } foreach (Match m in mKi2) { var move = Tree.position.FromKif(m.Groups[0].Value); if (move == Move.NONE) { throw new KifuException("指し手を解析できませんでした。", line); } Tree.AddNode(move, KifuMoveTimes.Zero); if (!Tree.position.IsLegal(move)) { // これだと不正着手後の棋譜コメントを取れないがとりあえず解析を中止する throw new KifuException($"不正着手を検出しました。", line); } if (move.IsOk()) { Tree.DoMove(move); } } goto nextline; } var mSpecial = rSpecial.Match(line); if (mSpecial.Success) { var move = Move.NONE; var reason = mSpecial.Groups[2].Value; switch (reason) { case "で先手の勝ち": case "で下手の勝ち": move = Tree.position.sideToMove == Color.BLACK ? Move.ILLEGAL_ACTION_WIN: Move.RESIGN; break; case "で後手の勝ち": case "で上手の勝ち": move = Tree.position.sideToMove == Color.WHITE ? Move.ILLEGAL_ACTION_WIN: Move.RESIGN; break; case "で先手の反則勝ち": case "で下手の反則勝ち": case "で後手の反則負け": case "で上手の反則負け": move = Tree.position.sideToMove == Color.BLACK ? Move.ILLEGAL_ACTION_WIN: Move.ILLEGAL_ACTION_LOSE; break; case "で後手の反則勝ち": case "で上手の反則勝ち": case "で先手の反則負け": case "で下手の反則負け": move = Tree.position.sideToMove == Color.WHITE ? Move.ILLEGAL_ACTION_WIN: Move.ILLEGAL_ACTION_LOSE; break; case "で時間切れにより先手の勝ち": case "で時間切れにより後手の勝ち": case "で時間切れにより上手の勝ち": case "で時間切れにより下手の勝ち": move = Move.TIME_UP; break; case "で中断": move = Move.INTERRUPT; break; case "で持将棋": move = Move.MAX_MOVES_DRAW; break; case "で千日手": move = Move.REPETITION_DRAW; break; case "詰": case "詰み": case "で詰": case "で詰み": move = Move.MATED; break; } if (move != Move.NONE) { Tree.AddNode(move, KifuMoveTimes.Zero); } } nextline :; continue; } if (!isBody) { var headRes = lazyHead(); if (headRes != string.Empty) { return(headRes); } } } catch (Exception e) { return($"棋譜読み込みエラー : {lineNo}行目。\n{e.Message}"); } return(null); }
/// <summary> /// </summary> /// <returns></returns> private string ToKifPositionString(KifuFileType kt = KifuFileType.KIF) { var sb = new StringBuilder(); // Kifu for Windows V7 ( http://kakinoki.o.oo7.jp/Kifuw7.htm ) 向けのヘッダ、これが無いとUTF-8形式の棋譜と認識して貰えない switch (kt) { case KifuFileType.KIF: sb.AppendLine("#KIF version=2.0 encoding=UTF-8"); break; case KifuFileType.KI2: sb.AppendLine("#KI2 version=2.0 encoding=UTF-8"); break; } // 局面出力 sb.AppendLine(Tree.position.ToBod().TrimEnd('\r', '\n')); // 先手対局者名 if (KifuHeader.header_dic.ContainsKey("先手")) { sb.AppendLine($"先手:{KifuHeader.PlayerNameBlack}"); } else if (KifuHeader.header_dic.ContainsKey("下手")) { sb.AppendLine($"下手:{KifuHeader.PlayerNameBlack}"); } else { switch (Tree.rootBoardType) { case BoardType.NoHandicap: case BoardType.Others: sb.AppendLine($"先手:{KifuHeader.PlayerNameBlack}"); break; default: sb.AppendLine($"下手:{KifuHeader.PlayerNameBlack}"); break; } } // 後手対局者名 if (KifuHeader.header_dic.ContainsKey("後手")) { sb.AppendLine($"後手:{KifuHeader.PlayerNameWhite}"); } else if (KifuHeader.header_dic.ContainsKey("上手")) { sb.AppendLine($"上手:{KifuHeader.PlayerNameWhite}"); } else { switch (Tree.rootBoardType) { case BoardType.NoHandicap: case BoardType.Others: sb.AppendLine($"後手:{KifuHeader.PlayerNameWhite}"); break; default: sb.AppendLine($"上手:{KifuHeader.PlayerNameWhite}"); break; } } // その他ヘッダ情報 foreach (var key in KifuHeader.header_dic.Keys) { switch (key) { case "先手": case "後手": case "上手": case "下手": case "手合割": break; default: sb.AppendLine($"{key}:{KifuHeader.header_dic[key]}"); break; } } return(sb.ToString()); }
/// <summary> /// PSN形式で書き出す。 /// </summary> /// <returns></returns> private string ToPsnString(KifuFileType kt) { var sb = new StringBuilder(); // 対局者名 if (kt == KifuFileType.PSN) { sb.AppendLine(string.Format(@"[Sente ""{0}""]", KifuHeader.PlayerNameBlack)); sb.AppendLine(string.Format(@"[Gote ""{0}""]", KifuHeader.PlayerNameWhite)); } else if (kt == KifuFileType.PSN2) { sb.AppendLine(string.Format(@"[Black ""{0}""]", KifuHeader.PlayerNameBlack)); sb.AppendLine(string.Format(@"[White ""{0}""]", KifuHeader.PlayerNameWhite)); // 持ち時間設定も合わせて書き出す sb.AppendLine($"[BlackTimeSetting \"{Tree.KifuTimeSettings.Player(Color.BLACK).ToKifuString()}\"]"); sb.AppendLine($"[WhiteTimeSetting \"{Tree.KifuTimeSettings.Player(Color.WHITE).ToKifuString()}\"]"); } // 初期局面 sb.AppendLine(string.Format(@"[SFEN ""{0}""]", Tree.position.ToSfen())); // -- 局面を出力していく // Treeのmoves[0]を選択してDoMove()を繰り返したものがPVで、これを最初に出力しなければならないから、 // わりと難しい。 // 再帰で書くの難しいので分岐地点をstackに積んでいく実装。 var stack = new Stack <Node>(); bool endNode = false; while (!endNode || stack.Count != 0) { int select = 0; if (endNode) { endNode = false; // 次の分岐まで巻き戻して、また出力していく。 var node = stack.Pop(); sb.AppendLine(); sb.AppendLine(string.Format("Variation:{0}", node.ply)); while (node.ply < Tree.gamePly) { Tree.UndoMove(); } select = node.select; goto SELECT; } int count = Tree.currentNode.moves.Count; if (count == 0) { // ここで指し手終わっとる。終端ノードであるな。 endNode = true; continue; } // このnodeの分岐の数 if (count != 1) { // あとで分岐しないといけないので残りをstackに記録しておく for (int i = 1; i < count; ++i) { stack.Push(new Node(Tree.gamePly, i)); } } SELECT :; var move = Tree.currentNode.moves[select]; var m = move.nextMove; // DoMove()する前の現局面の手番 var turn = Tree.position.sideToMove; string mes; if (m.IsSpecial()) { // 特殊な指し手なら、それを出力して終わり。 endNode = true; if (kt == KifuFileType.PSN) { switch (m) { case Move.MATED: mes = "Mate"; break; case Move.INTERRUPT: mes = "Interrupt"; break; case Move.REPETITION_WIN: mes = "Sennichite"; break; case Move.REPETITION_DRAW: mes = "Sennichite"; break; case Move.WIN: mes = "Jishogi"; break; case Move.WIN_THEM: mes = "Jishogi"; break; case Move.MAX_MOVES_DRAW: mes = "Jishogi"; break; case Move.RESIGN: mes = "Resigns"; break; case Move.TIME_UP: mes = "Timeup"; break; default: mes = ""; break; } } else if (kt == KifuFileType.PSN2) { switch (m) { case Move.MATED: mes = "Mate"; break; case Move.INTERRUPT: mes = "Interrupt"; break; case Move.REPETITION_WIN: mes = "RepetitionWin"; break; case Move.REPETITION_DRAW: mes = "RepetitionDraw"; break; case Move.WIN: mes = "DeclarationWin"; break; case Move.WIN_THEM: mes = "TryRuleWin"; break; case Move.MAX_MOVES_DRAW: mes = "MaxMovesDraw"; break; case Move.RESIGN: mes = "Resigns"; break; case Move.TIME_UP: mes = "Timeup"; break; default: mes = ""; break; } } else { mes = ""; } mes = Tree.gamePly + "." + mes; } else { // この指し手を出力する if (kt == KifuFileType.PSN) { var to = m.To(); Piece pc; if (m.IsDrop()) { pc = m.DroppedPiece().PieceType(); mes = string.Format("{0}.{1}*{2}", Tree.gamePly, pc.ToUsi(), to.ToUsi()); } else { var c = Tree.position.IsCapture(m) ? 'x' : '-'; var c2 = m.IsPromote() ? "+" : ""; var from = m.From(); pc = Tree.position.PieceOn(m.From()).PieceType(); mes = string.Format("{0}.{1}{2}{3}{4}{5}", Tree.gamePly, pc.ToUsi(), from.ToUsi(), c, to.ToUsi(), c2); } } else if (kt == KifuFileType.PSN2) { // PSN2形式なら指し手表現はUSIの指し手文字列そのまま!!簡単すぎ!! mes = string.Format("{0}.{1}", Tree.gamePly, m.ToUsi()); } else { mes = ""; } Tree.DoMove(move); } var time_string1 = (kt == KifuFileType.PSN) ? move.kifuMoveTimes.Player(turn).ThinkingTime.ToString("mm\\:ss") : move.kifuMoveTimes.Player(turn).ThinkingTime.ToString("hh\\:mm\\:ss"); var time_string2 = move.kifuMoveTimes.Player(turn).TotalTime.ToString("hh\\:mm\\:ss"); sb.AppendLine(string.Format("{0,-18}({1} / {2})", mes, time_string1, time_string2)); } return(sb.ToString()); }
/// <summary> /// PSN形式の棋譜ファイルのparser /// エラーがあった場合は、そのエラーの文字列が返る。 /// エラーがなければnullが返る。 /// </summary> private string FromPsnString(string[] lines, KifuFileType kf) { // 消費時間、残り時間、消費時間を管理する。 var timeSettings = KifuTimeSettings.TimeLimitless; var lineNo = 1; try { var r1 = new Regex(@"\[([^\s]+)\s*""(.*)""\]"); for (; lineNo <= lines.Length; ++lineNo) { var line = lines[lineNo - 1]; var m1 = r1.Match(line); if (m1.Success) { var token = m1.Groups[1].Value.ToLower(); var body = m1.Groups[2].Value; switch (token) { case "sente": case "black": KifuHeader.PlayerNameBlack = body; break; case "gote": case "white": KifuHeader.PlayerNameWhite = body; break; case "sfen": // 将棋所で出力したPSNファイルはここに必ず"SFEN"が来るはず。平手の局面であっても…。 // 互換性のためにも、こうなっているべきだろう。 Tree.rootSfen = body; Tree.position.SetSfen(body); Tree.rootBoardType = BoardType.Others; break; case "blacktimesetting": var black_setting = KifuTimeSetting.FromKifuString(body); if (black_setting != null) { timeSettings.Players[(int)Color.BLACK] = black_setting; } break; case "whitetimesetting": var white_setting = KifuTimeSetting.FromKifuString(body); if (white_setting != null) { timeSettings.Players[(int)Color.WHITE] = white_setting; timeSettings.WhiteEnable = true; // 後手は個別設定 } break; } } else { break; } } // 残り時間の計算用。 var times = timeSettings.GetInitialKifuMoveTimes(); Tree.SetKifuMoveTimes(times.Clone()); // root局面での残り時間の設定 Tree.KifuTimeSettings = timeSettings.Clone(); // PSNフォーマットのサイトを見ると千日手とか宣言勝ちについて規定がない。 // どう見ても欠陥フォーマットである。 // http://genedavis.com/articles/2014/05/09/psn/ // PSN2フォーマットを策定した // cf. https://github.com/yaneurao/MyShogi/blob/master/docs/PSN2format.md // -- そこ以降は指し手文字列などが来るはず.. // e.g. // 1.P7g-7f (00:03 / 00:00:03) // 2.P5c-5d (00:02 / 00:00:02) // 3.B8hx3c+ (00:03 / 00:00:06) // 5.B*4e (00:01 / 00:00:04) // 15.+B4d-3c (00:01 / 00:00:12) // 16.Sennichite (00:01 / 00:00:10) // 9.Resigns (00:03 / 00:00:08) // 駒種(成り駒は先頭に"+")、移動元、移動先をUSI文字列で書いて、captureのときは"x"、非captureは"-" // 成りは末尾に"+"が入る。駒打ちは"*" // 入玉宣言勝ちは将棋所では次のようになっているようだ。 // 75.Jishogi (00:02 / 00:00:44) // { // 入玉宣言により勝ち。 // } // 変化手順の表現 // 6手目からの変化手順の場合 // Variation:6 // 6.K5a-6b (00:02 / 00:00:04) // 指し手の正規表現 var r4 = new Regex(@"(\d+)\.([^\s]+)\s*\((.+?)\s*\/\s*(.+)\)"); // 正規表現のデバッグ難しすぎワロタ // 正規表現デバッグ用の神サイトを使う : https://regex101.com/ // 変化手順 var r5 = new Regex(@"Variation:(\d+)"); var moves = new Move[(int)Move.MAX_MOVES]; for (; lineNo <= lines.Length; ++lineNo) { var line = lines[lineNo - 1]; // コメントブロックのスキップ // "{"で始まる行に遭遇したら、"}"で終わる行まで読み飛ばす if (line.StartsWith("{")) { for (++lineNo; lineNo <= lines.Length; ++lineNo) { line = lines[lineNo - 1]; if (line.EndsWith("}")) { break; } } continue; } // 変化手順 var m5 = r5.Match(line); if (m5.Success) { // 正規表現で数値にマッチングしているのでこのparseは100%成功する。 int ply = int.Parse(m5.Groups[1].Value); // ply手目まで局面を巻き戻す while (ply < Tree.gamePly) { Tree.UndoMove(); } // このnodeでの残り時間に戻す times = Tree.GetKifuMoveTimes(); continue; } var m4 = r4.Match(line); if (m4.Success) { // 正規表現で数値にマッチングしているのでこのparseは100%成功する。 var ply2 = int.Parse(m4.Groups[1].Value); // ply1 == ply2のはずなのだが…。 // まあいいか…。 var move_string = m4.Groups[2].Value; var time_string1 = m4.Groups[3].Value; var time_string2 = m4.Groups[4].Value; Move move = Move.NONE; // move_stringが"Sennichite"などであるか。 if (kf == KifuFileType.PSN) { switch (move_string) { case "Sennichite": // どちらが勝ちかはわからない千日手 move = Move.REPETITION; goto FINISH_MOVE_PARSE; case "Resigns": move = Move.RESIGN; goto FINISH_MOVE_PARSE; case "Interrupt": move = Move.INTERRUPT; goto FINISH_MOVE_PARSE; case "Mate": move = Move.MATED; goto FINISH_MOVE_PARSE; case "Jishogi": // 入玉宣言勝ちなのか最大手数による引き分けなのか不明 move = Move.WIN; goto FINISH_MOVE_PARSE; case "Timeup": move = Move.TIME_UP; goto FINISH_MOVE_PARSE; } } else if (kf == KifuFileType.PSN2) { switch (move_string) { case "Resigns": move = Move.RESIGN; goto FINISH_MOVE_PARSE; case "Interrupt": move = Move.INTERRUPT; goto FINISH_MOVE_PARSE; case "Mate": move = Move.MATED; goto FINISH_MOVE_PARSE; case "Timeup": move = Move.TIME_UP; goto FINISH_MOVE_PARSE; case "MaxMovesDraw": move = Move.MAX_MOVES_DRAW; goto FINISH_MOVE_PARSE; case "TryRuleWin": move = Move.WIN_THEM; goto FINISH_MOVE_PARSE; case "DeclarationWin": move = Move.WIN; goto FINISH_MOVE_PARSE; case "RepetitionDraw": move = Move.REPETITION_DRAW; goto FINISH_MOVE_PARSE; case "RepetitionWin": move = Move.REPETITION_WIN; goto FINISH_MOVE_PARSE; } } if (kf == KifuFileType.PSN2) { // PSN2ならparse簡単すぎワロタ move = Util.FromUsiMove(move_string); goto FINISH_MOVE_PARSE; } int seek_pos = 0; // 1文字ずつmove_stringから切り出す。終端になると'\0'が返る。 char get_char() { return(seek_pos < move_string.Length ? move_string[seek_pos++] : '\0'); } // 1文字先読みする。終端だと'\0'が返る char peek_char() { return(seek_pos < move_string.Length ? move_string[seek_pos] : '\0'); } bool promote_piece = false; // 先頭の"+"は成り駒の移動を意味する if (peek_char() == '+') { get_char(); promote_piece = true; } char piece_name = get_char(); var pc = Util.FromUsiPiece(piece_name); if (pc == Piece.NO_PIECE) { throw new KifuException("指し手文字列の駒名がおかしいです。", line); } pc = pc.PieceType(); bool drop_move = false; if (peek_char() == '*') { get_char(); drop_move = true; if (promote_piece) { throw new KifuException("指し手文字列で成駒を打とうとしました。", line); } } // 移動元の升 var c1 = get_char(); var c2 = get_char(); var from = Util.FromUsiSquare(c1, c2); if (from == Square.NB) { throw new KifuException("指し手文字列の移動元の表現がおかしいです。", line); } if (drop_move) { // この升に打てばOk. move = Util.MakeMoveDrop(pc, from); goto FINISH_MOVE_PARSE; } //bool is_capture = false; if (peek_char() == '-') { get_char(); } else if (peek_char() == 'x') { get_char(); //is_capture = true; } // 移動先の升 var c3 = get_char(); var c4 = get_char(); var to = Util.FromUsiSquare(c3, c4); if (to == Square.NB) { throw new KifuException("指し手文字列の移動先の表現がおかしいです。", line); } bool is_promote = false; if (peek_char() == '+') { is_promote = true; } move = !is_promote?Util.MakeMove(from, to) : Util.MakeMovePromote(from, to); // この指し手でcaptureになるかどうかだとか // 移動元の駒が正しいかを検証する必要があるが、 // 非合法手が含まれる可能性はあるので、それは無視する。 // ここで指し手のパースは終わり。 FINISH_MOVE_PARSE :; // 消費時間、総消費時間のparse。これは失敗しても構わない。 // 消費時間のほうはmm:ssなのでhh:mm:ss形式にしてやる。 if (time_string1.Length <= 5) { time_string1 = "00:" + time_string1; } TimeSpan.TryParse(time_string1, out TimeSpan thinking_time); TimeSpan.TryParse(time_string2, out TimeSpan total_time); // -- 千日手の判定 var rep = Tree.position.IsRepetition(); switch (rep) { case RepetitionState.NONE: break; // do nothing case RepetitionState.DRAW: move = Move.REPETITION_DRAW; break; case RepetitionState.WIN: move = Move.REPETITION_WIN; break; case RepetitionState.LOSE: move = Move.REPETITION_LOSE; break; } if (move == Move.REPETITION) { // 千日手らしいが、上で千日手判定をしているのに、それに引っかからなかった。 // おかしな千日手出力であるので、ここ以降の読み込みに失敗させる。 throw new KifuException("千日手となっていますが千日手ではないです。", line); } // -- 詰みの判定 if (Tree.position.IsMated(moves)) { // move == Move.MATEDでないとおかしいのだが中断もありうるので強制的に詰み扱いにしておく。 move = Move.MATED; } // -- 持将棋の判定 if (move == Move.WIN) { // 持将棋の条件が異なるかも知れないので、この判定はしないことにする。 //if (Tree.position.DeclarationWin(EnteringKingRule.POINT27) == Move.WIN) // return string.Format("PSN形式の{0}行目が持将棋となっているが持将棋ではないです。", lineNo); } var turn = Tree.position.sideToMove; times.Players[(int)turn] = times.Players[(int)turn].Create( timeSettings.Player(turn), thinking_time, thinking_time, total_time /*消費時間は棋譜に記録されているものをそのまま使用する*/ /*残り時間は棋譜上に記録されていない*/ ); // -- DoMove() Tree.AddNode(move, times.Clone()); // 特殊な指し手、もしくはLegalでないならDoMove()は行わない if (!move.IsSpecial() && !Tree.position.IsLegal(move)) { // まだ次の分岐棋譜があるかも知れないので読み進める continue; } // special moveであってもTree.DoMove()は出来る Tree.DoMove(move); continue; } else { // 空行など、parseに失敗したものは読み飛ばす } } } catch (Exception e) { return($"棋譜読み込みエラー : {lineNo}行目\n{e.Message}"); } return(null); }