/// <summary> /// 現在の局面(currentMove)に対して、指し手moveが登録されていないなら、その指し手を追加する。 /// すでに存在しているなら、その指し手は追加しない。 /// /// thinkingTimeは考慮に要した時間。新たにnodeを追加しないときは、この値は無視される。 /// ミリ秒まで計測して突っ込んでおいて良い。(棋譜出力時には秒単位で繰り上げられる) /// /// totalTimeは総消費時間。nullを指定した場合は、ここまでの総消費時間(TotalConsumptionTime()で取得できる)に /// thinkingTimeを秒単位に繰り上げたものが入る。 /// /// moveがspecial moveもありうる。 /// </summary> /// <param name="move"></param> /// <param name="thinkingTime"></param> public void AddNode(Move move, KifuMoveTimes kifuMoveTimes) { var m = currentNode.moves.FirstOrDefault((x) => x.nextMove == move); if (m == null) { // -- 見つからなかったので次のnodeを追加してやる KifuNode nextNode = new KifuNode(currentNode); currentNode.moves.Add(new KifuMove(move, nextNode, kifuMoveTimes)); } }
/// <summary> /// KifuMoveTimesを現在の局面に対して設定する。 /// </summary> /// <param name="kifuMoveTimes"></param> public void SetKifuMoveTimes(KifuMoveTimes kifuMoveTimes) { // 1つ前の局面の指し手を指した直後に記録されている。 var prevNode = currentNode.prevNode; if (prevNode != null) { // prevNodeから、この局面に至る指し手を探して、そこに記録されているKifuMoveTimeに設定する prevNode.moves.Find((x) => x.nextNode == currentNode).kifuMoveTimes = kifuMoveTimes; } else { // rootの局面では、rootKifuMoveTimesのほうに格納されている。 RootKifuMoveTimes = kifuMoveTimes; } }
/// <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); }
public KifuMove(Move nextMove_, KifuNode nextNode_, KifuMoveTimes kifuMoveTimes_) { nextMove = nextMove_; nextNode = nextNode_; kifuMoveTimes = kifuMoveTimes_; }