/// <summary> /// 初期化する。new KifuTree()した状態に戻る。 /// </summary> public void Init() { position.InitBoard(); // root nodeを作る currentNode = rootNode = new KifuNode(null); pliesFromRoot = 0; rootBoardType = BoardType.NoHandicap; rootSfen = Position.SFEN_HIRATE; // rootSfenのsetterで初期化されているのでここではKifuList、UsiMoveListの初期化はしない //KifuList = new List<string>(); //UsiMoveList = new List<string>(); // 対局情報などを保存するためにここを確保する。 rootKifuMove = new KifuMove(Move.NONE, rootNode, KifuMoveTimes.Zero); kifuWindowMoves = new List <KifuMove>(); KifuTimeSettings = KifuTimeSettings.TimeLimitless; KifuBranch = -1; // mutableなオブジェクトのraiseは、raiseする側でClone()してimmutableにしておかないと駄目。 RaisePropertyChanged("Position", position.Clone()); }
// ------------------------------------------------------------------------- // 局面に対する操作子 // ------------------------------------------------------------------------- // DoMove(),UndoMove()以外はcurrentNode.movesに自分で足すなり引くなりすれば良い /// <summary> /// posの現在の局面から指し手mで進める。 /// mは、currentNodeのもつ指し手の一つであるものとする /// </summary> /// <param name="m"></param> public void DoMove(KifuMove m) { Debug.Assert(m != null); position.DoMove(m.nextMove); currentNode = m.nextNode; // UsiMoveStringList.Add(m.nextMove.ToUsi()); }
// ------------------------------------------------------------------------- // 局面に対する操作子 // ------------------------------------------------------------------------- // DoMove(),UndoMove()以外はcurrentNode.movesに自分で足すなり引くなりすれば良い // CurrentNodeが設定されていないと局面を進められない。 /// <summary> /// posの現在の局面から指し手mで進める。 /// mは、currentNodeのもつ指し手の一つであるものとする /// </summary> /// <param name="m"></param> public void DoMove(KifuMove m) { Debug.Assert(m != null); // 棋譜の更新 AddKifu(m.nextMove, m.thinkingTime); position.DoMove(m.nextMove); RaisePropertyChanged("Position", position); currentNode = m.nextNode; // もし次がSpecialMoveの局面に到達したのであれば、棋譜に積む。 if (currentNode.moves.Count != 0 && currentNode.moves[0].nextMove.IsSpecial()) { AddKifuSpecialMove(currentNode.moves[0].nextMove, TimeSpan.Zero); } }
/// <summary> /// 初期化する。new KifuTree()した状態に戻る。 /// ただし、EnableKifuList == falseだと棋譜リストの初期化されないので、リセットしたいならResetKifuList()を明示的に呼び出す必要がある。 /// </summary> public void Init() { // root nodeを作る currentNode = rootNode = new KifuNode(null); pliesFromRoot = 0; SetRootBoardType(BoardType.NoHandicap); // rootSfenのsetterで初期化されているのでここではKifuList、UsiMoveListの初期化はしない //KifuList = new List<string>(); //UsiMoveList = new List<string>(); // 対局情報などを保存するためにここを確保する。 rootKifuMove = new KifuMove(Move.NONE, rootNode, KifuMoveTimes.Zero); kifuWindowMoves = new List <KifuMove>(); KifuTimeSettings = KifuTimeSettings.TimeLimitless; KifuBranch = -1; }
// ------------------------------------------------------------------------- // 局面に対する操作子 // ------------------------------------------------------------------------- // DoMove(),UndoMove()以外はcurrentNode.movesに自分で足すなり引くなりすれば良い // CurrentNodeが設定されていないと局面を進められない。 /// <summary> /// posの現在の局面から指し手mで進める。 /// mは、currentNodeのもつ指し手の一つであるものとする /// /// speical moveの時は、Position.DoMove()は呼び出さないが次のnodeに到達できることは保証される。 /// (そうしないとcurrentNodeが更新されないため) /// </summary> /// <param name="m"></param> public void DoMove(KifuMove m) { Debug.Assert(m != null); // 棋譜の更新 var stm = position.sideToMove; var thinkingTime = m.kifuMoveTimes.Player(stm).ThinkingTime; AddKifu(m, thinkingTime); // special moveに対してはDoMove()を行わない。(行っても良いが盤面が変わらないのでやるだけ無駄である) if (!m.nextMove.IsSpecial()) { position.DoMove(m.nextMove); RaisePropertyChanged("Position", position); } ++pliesFromRoot; currentNode = m.nextNode; }
/// <summary> /// DoMove()のときに棋譜に追加する /// </summary> /// <param name="m"></param> private void AddKifu(KifuMove move, TimeSpan t) { var m = move.nextMove; if (EnableKifuList) { // rootNodeからの指し手。これは棋譜リストと同期させている。 kifuWindowMoves.Add(move); // -- 棋譜をappendする // 分岐棋譜であれば、先頭に"+"を付与。 // ただし、本譜の手順でなければ"*"を付与。 char plus; if (currentNode.moves.Count != 1) { // 本譜の手順なら'+',本譜以外の手順であれば'*' // ちなみに「本譜」ボタンを押した時、 // 棋譜の先頭から最初の'*'のあるところまで戻って、そこを'+'にする。 plus = currentNode.moves[0].nextMove == m ? '+' : '*'; if (plus == '*') // 本譜ではないところを選んでいる { // 最初の分岐であるか。 if (KifuBranch == -1 || (KifuList.Count < KifuBranch)) { KifuBranch = KifuList.Count; } } else if (plus == '+') { // 本譜の手順に変わったので分岐ではない。 if (KifuBranch == KifuList.Count) { KifuBranch = -1; } } } else { plus = ' '; } // 分岐棋譜の場合、分岐以降はインデントを入れる。 var indent = ""; if (KifuBranch != -1 && KifuBranch <= KifuList.Count) { indent = ">"; } var move_text = move_to_kif_string(position, m); var move_text_game_ply = position.gamePly; // 消費時間の文字列「1秒」のように短めの文字列で表現。 // 一番上の桁は、そのあとのPadMidUnicode()でpaddingされるので、PadLeft的なpaddingはしないでおく。 string time_string; if (t.TotalSeconds < 60) { time_string = $"{t.Seconds}秒"; } else if (t.TotalMinutes < 60) { time_string = $"{t.Minutes}分{t.Seconds,2}秒"; } else if (t.TotalHours < 24) { time_string = $"{t.Hours}時間{t.Minutes,2}分{t.Seconds,2}秒"; } else { time_string = $"{t.Days}日{t.Hours,2}時間{t.Minutes,2}分{t.Seconds,2}秒"; } var move_time = move_text.PadMidUnicode(time_string, 12 /*指し手=最大全角6文字=半角12文字*/ + 1 /* space */ + 7 /*時間文字列、1分00秒で半角7文字*/); var text = $"{indent}{plus}{move_text_game_ply, 3}.{move_time}"; KifuList.Add(text); RaisePropertyChanged("KifuListAdded", text /*末尾が変更になった。変更になった行を送る。*/); } if (EnableUsiMoveList) { // special moveは、USIとしては規定されていない指し手になるのでここでは出力しない。 // ("position"コマンドで送信してしまいかねないので) // ただし削除処理がややこしくなるのは嫌なので要素は一つ増やしておく。 UsiMoveList.Add(!m.IsSpecial() ? m.ToUsi() : 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> /// DoMove()のときに棋譜に追加する /// </summary> /// <param name="m"></param> private void AddKifu(KifuMove move, TimeSpan t) { var m = move.nextMove; if (EnableKifuList) { // rootNodeからの指し手。これは棋譜リストと同期させている。 kifuWindowMoves.Add(move); // -- 棋譜をappendする // 分岐棋譜であれば、先頭に"+"を付与。 // ただし、本譜の手順でなければ"*"を付与。 char plus; if (currentNode.moves.Count != 1) { // 本譜の手順なら'+',本譜以外の手順であれば'*' // ちなみに「本譜」ボタンを押した時、 // 棋譜の先頭から最初の'*'のあるところまで戻って、そこを'+'にする。 plus = currentNode.moves[0].nextMove == m ? '+' : '*'; if (plus == '*') // 本譜ではないところを選んでいる { // 最初の分岐であるか。 if (KifuBranch == -1 || (KifuList.Count < KifuBranch)) { KifuBranch = KifuList.Count; } } else if (plus == '+') { // 本譜の手順に変わったので分岐ではない。 if (KifuBranch == KifuList.Count) { KifuBranch = -1; } } } else { plus = ' '; } // 分岐棋譜の場合、分岐以降はインデントを入れる。 var indent = ""; if (KifuBranch != -1 && KifuBranch <= KifuList.Count) { indent = ">"; } var move_text = move_to_kif_string(position, m); var move_text_game_ply = position.gamePly; // 消費時間の文字列「1秒」のように短めの文字列で表現。 // 一番上の桁は、そのあとのPadMidUnicode()でpaddingされるので、PadLeft的なpaddingはしないでおく。 string time_string; if (t.TotalSeconds < 60) { time_string = $"{t.Seconds}秒"; } else if (t.TotalMinutes < 60) { time_string = $"{t.Minutes}分{t.Seconds,2}秒"; } else if (t.TotalHours < 24) { time_string = $"{t.Hours}時間{t.Minutes,2}分{t.Seconds,2}秒"; } else { time_string = $"{t.Days}日{t.Hours,2}時間{t.Minutes,2}分{t.Seconds,2}秒"; } var ply_text = $"{indent}{plus}{move_text_game_ply,3}"; string total_consumption_time_string = null; var kifuMoveTime = currentNode.moves.Find(x => x.nextMove == m).kifuMoveTimes; // この指し手の手番側。最後、SpecialMoveに関しては、手番側とは限らないのであとで考える。 // (先手が指す→千日手成立[ここで先手の総消費時間が出て欲しい] みたいなのもあるので) // TODO : 現状、Special Moveに対してTotalTime積んでないな。これ、時間切れのときに総消費時間わからんな…。 // あとでよく考える。 var turn = position.sideToMove; var total = kifuMoveTime.Player(turn).TotalTime; // D2だと99時間までなのでそれを超える場合は、桁のある限り。 total_consumption_time_string = total.TotalHours >= 100 ? $"{(int)total.TotalHours}:{total.Minutes,2:D2}:{total.Seconds,2:D2}" : $"{(int)total.TotalHours,2:D2}:{total.Minutes,2:D2}:{total.Seconds,2:D2}"; var row = new KifuListRow(ply_text, move_text, time_string, total_consumption_time_string); KifuList.Add(row); RaisePropertyChanged("KifuListAdded", row /*末尾が変更になった。変更になった行を送る。*/); // -- この内容をLastKifuStringに反映させる。 // special moveは除外 if (m.IsOk()) { var kifu_version = TheApp.app.Config.KifuWindowKifuVersion; switch (kifu_version) { // 同じ形式なので棋譜ウィンドウに表示しているものをそのまま代入しておけば良い。 case 0: LastKifuString = move_text; break; case 1: case 2: case 3: LastKifuString = KifFormatter.Ki2C.format(position, m); break; default: Debug.Assert(false); break; } } } if (EnableUsiMoveList) { // special moveは、USIとしては規定されていない指し手になるのでここでは出力しない。 // ("position"コマンドで送信してしまいかねないので) // ただし削除処理がややこしくなるのは嫌なので要素は一つ増やしておく。 UsiMoveList.Add(!m.IsSpecial() ? m.ToUsi() : string.Empty); } }