/// <summary> /// ユーザーによる対局中の2手戻し /// 受理できるかどうかは別 /// </summary> public void UndoCommand() { AddCommand( () => { if (InTheGame) { var stm = kifuManager.Position.sideToMove; var stmPlayer = Player(stm); // 人間の手番でなければ受理しない if (stmPlayer.PlayerType == PlayerTypeEnum.Human) { // 棋譜を消すUndo() kifuManager.UndoMoveInTheGame(); kifuManager.UndoMoveInTheGame(); // 時刻を巻き戻さないといけない PlayTimers.SetKifuMoveTimes(kifuManager.Tree.GetKifuMoveTimes()); // これにより、2手目の局面などであれば1手しかundoできずに手番が変わりうるので手番の更新を通知。 NotifyTurnChanged(); } } }); }
/// <summary> /// 棋譜の選択行が変更になった。 /// 対局中でなければ、現在局面をその棋譜の局面に変更する。 /// /// このメソッドを直接呼び出さずに、this.KifuListSelectedIndexのsetterか、UpdateKifuSelectedIndex()を用いること。 /// </summary> private void KifuListSelectedIndexChangedCommand(PropertyChangedEventArgs args) { AddCommand( () => { if (GameMode.IsConsideration()) { // 現在の局面と違う行であるかを判定して、同じ行である場合は、 // このイベントを処理してはならない。 // 無理やりではあるが棋譜のN行目に移動出来るのであった…。 int selectedIndex = (int)args.value; kifuManager.Tree.GotoSelectedIndex(selectedIndex); PlayTimers.SetKifuMoveTimes(kifuManager.Tree.GetKifuMoveTimes()); // 検討モードのとき) // 思考エンジンでの検討中 → 局面が変わったので思考しなおすためにNotifyTurnChanged()が必要。 // 思考エンジンで検討していない → 掴んでいる駒を離す必要があるためにNotifyTurnChanged()が必要。 if (GameMode.IsConsideration()) { NotifyTurnChanged(); } } }); }
/// <summary> /// ユーザーによる対局中の2手戻し /// 受理できるかどうかは別 /// </summary> public void UndoCommand() { AddCommand( () => { if (InTheGame) { var stm = kifuManager.Position.sideToMove; var stmPlayer = Player(stm); // 人間の手番でなければ受理しない if (stmPlayer.PlayerType == PlayerTypeEnum.Human) { // 棋譜を消すUndo() kifuManager.UndoMoveInTheGame(); kifuManager.UndoMoveInTheGame(); // 残り持ち時間などを巻き戻さないといけない。 // ただし、再開局で開始局面以上に巻き戻すと、いまより持ち時間が減ってしまう可能性がある。 // 開始局面以上に巻き戻す場合は、再開時の時間にしてやる必要がある。 // しかし、中断局であるなら、遡れなければならない。難しい。 var nextTime = kifuManager.Tree.GetKifuMoveTimes(); PlayTimers.SetKifuMoveTimes(nextTime); // これにより、2手目の局面などであれば1手しかundoできずに手番が変わりうるので手番の更新を通知。 NotifyTurnChanged(); } } }); }
/// <summary> /// 棋譜(string型)の読み込みコマンド /// ファイルから読み込むには、MainMenu.ReadKifuFile()のようなメソッドを用いること。 /// </summary> /// <param name="kifuText"></param> public void KifuReadCommand(string kifuText) { AddCommand( () => { if (GameMode.CanUserMove()) { // 対局中ではないので、EnableKifuList == falseになっているが、 // 一時的にこれをtrueにしないと、読み込んだ棋譜に対して、Tree.KifuListが同期しない。 // ゆえに、読み込みの瞬間だけtrueにして、そのあとfalseに戻す。 kifuManager.EnableKifuList = true; var error = kifuManager.FromString(kifuText); kifuManager.EnableKifuList = false; if (error != null) { TheApp.app.MessageShow("棋譜の読み込みに失敗しました。\n" + error, MessageShowType.Error); kifuManager.Init(); // 不正な局面のままになるとまずいので初期化。 } else { // 読み込みが完了すれば自動的に末尾の局面に行っているはずだが、 // 棋譜ウィンドウを更新した結果、分岐局面などに戻ってしまうといけない。 // 棋譜に書かれていた持ち時間設定・残り時間を画面に反映させる。(GameSettingには反映させない) PlayTimers.SetKifuTimeSettings(kifuManager.Tree.KifuTimeSettings); PlayTimers.SetKifuMoveTimes(kifuManager.Tree.GetKifuMoveTimes()); UpdateTimeString(); // 末尾の局面に移動するコマンドを叩いておく。 UpdateKifuSelectedIndex(int.MaxValue); // -- 棋譜上の名前を表示名に反映させる。 foreach (var c in All.Colors()) { var name = kifuManager.KifuHeader.GetPlayerName(c); SetPlayerName(c, name); } } // 棋譜が綺麗になった扱いにする。(この棋譜はファイルなどに丸ごと保存されているはずであるから) KifuDirty = false; // 駒を持ち上げていたりしたらそれをリセットする必要があるので。 RaisePropertyChanged("TurnChanged"); } }); }
/// <summary> /// 棋譜の選択行が変更になった。 /// 対局中でなければ、現在局面をその棋譜の局面に変更する。 /// </summary> public void KifuSelectedIndexChangedCommand(int selectedIndex) { AddCommand( () => { if (GameMode.IsConsideration()) { // 現在の局面と違う行であるかを判定して、同じ行である場合は、 // このイベントを処理してはならない。 // 無理やりではあるが棋譜のN行目に移動出来るのであった…。 kifuManager.Tree.GotoSelectedIndex(selectedIndex); PlayTimers.SetKifuMoveTimes(kifuManager.Tree.GetKifuMoveTimes()); } }); }
/// <summary> /// 棋譜の選択行が変更になった。 /// 対局中でなければ、現在局面をその棋譜の局面に変更する。 /// /// このメソッドを直接呼び出さずに、this.KifuListSelectedIndexのsetterを使うこと。 /// </summary> private void KifuListSelectedIndexChangedCommand(PropertyChangedEventArgs args) { AddCommand( () => { if (GameMode.IsConsideration()) { // 現在の局面と違う行であるかを判定して、同じ行である場合は、 // このイベントを処理してはならない。 // 無理やりではあるが棋譜のN行目に移動出来るのであった…。 int selectedIndex = (int)args.value; kifuManager.Tree.GotoSelectedIndex(selectedIndex); PlayTimers.SetKifuMoveTimes(kifuManager.Tree.GetKifuMoveTimes()); // 局面が変わったので思考しなおす。 if (GameMode.IsConsiderationWithEngine()) { NotifyTurnChanged(); } } }); }
/// <summary> /// ユーザーによる対局中の2手戻し /// 受理できるかどうかは別 /// </summary> public void UndoCommand() { AddCommand( () => { if (InTheGame) { var stm = kifuManager.Position.sideToMove; var stmPlayer = Player(stm); // 人間の手番でなければ受理しない if (stmPlayer.PlayerType == PlayerTypeEnum.Human) { // 棋譜を消すUndo() kifuManager.UndoMoveAndRemoveKifu(); kifuManager.UndoMoveAndRemoveKifu(); // 残り持ち時間などを巻き戻さないといけない。 // ただし、再開局で開始局面以上に巻き戻すと、いまより持ち時間が減ってしまう可能性がある。 // 開始局面以上に巻き戻す場合は、再開時の時間にしてやる必要がある。 // しかし、中断局であるなら、遡れなければならない。難しい。 var nextTime = kifuManager.Tree.GetKifuMoveTimes(); PlayTimers.SetKifuMoveTimes(nextTime); // これにより、2手目の局面などであれば1手しかundoできずに手番が変わりうるので手番の更新を通知。 NotifyTurnChanged(); // TreeのUndoにより、KifuListの更新がかかり、選択行が変化して、 // 更新通知が来て、そのハンドラのなかでSetKifuMoveTimes()とNotifyTurnChanged()を // 行っているので、本来、上の文は不要なのだが、そのハンドラでは、Consideration中しか // その処理を行わない(ユーザーの棋譜選択でそこが変更されると困る)ので上記処理は必要。 } } }); }
/// <summary> /// 棋譜の読み込みコマンド /// </summary> /// <param name="kifuText"></param> public void KifuReadCommand(string kifuText) { AddCommand( () => { if (GameMode.CanUserMove()) { // 対局中ではないので、EnableKifuList == falseになっているが、 // 一時的にこれをtrueにしないと、読み込んだ棋譜に対して、Tree.KifuListが同期しない。 // ゆえに、読み込みの瞬間だけtrueにして、そのあとfalseに戻す。 kifuManager.EnableKifuList = true; var error = kifuManager.FromString(kifuText); kifuManager.EnableKifuList = false; if (error != null) { TheApp.app.MessageShow("棋譜の読み込みに失敗しました。\n" + error, "読み込みエラー"); kifuManager.Init(); // 不正な局面のままになるとまずいので初期化。 } else { // 読み込みが完了すれば自動的に末尾の局面に行っているはずだが、 // 棋譜ウィンドウを更新した結果、分岐局面などに戻ってしまうといけない。 // 棋譜に書かれていた持ち時間設定・残り時間を画面に反映させる。(GameSettingには反映させない) PlayTimers.SetKifuTimeSettings(kifuManager.Tree.KifuTimeSettings); PlayTimers.SetKifuMoveTimes(kifuManager.Tree.GetKifuMoveTimes()); UpdateTimeString(); // 末尾の局面に移動するコマンドを叩いておく。 RaisePropertyChanged("SetKifuListIndex", kifuManager.KifuList.Count - 1); } } }); }
/// <summary> /// 棋譜(string型)の読み込みコマンド /// ファイルから読み込むには、MainMenu.ReadKifuFile()のようなメソッドを用いること。 /// /// merge == trueなら、棋譜を分岐棋譜としてマージする。 /// dirtyは、このあと、KifuDirty(棋譜が汚れているかのフラグ)に設定される値。 /// </summary> /// <param name="kifuText"></param> public void KifuReadCommand(string kifuText, bool merge = false, bool dirty = false) { AddCommand( () => { if (GameMode.CanUserMove()) { // 対局中ではないので、EnableKifuList == falseになっているが、 // 一時的にこれをtrueにしないと、読み込んだ棋譜に対して、Tree.KifuListが同期しない。 // ゆえに、読み込みの瞬間だけtrueにして、そのあとfalseに戻す。 kifuManager.EnableKifuList = true; var oldSelectedIndex = kifuManager.Tree.pliesFromRoot; var error = merge ? kifuManager.MergeFromString(kifuText) : kifuManager.FromString(kifuText); // マージした場合、その棋譜の末尾に行って欲しい気もするが…。 kifuManager.EnableKifuList = false; if (error != null) { TheApp.app.MessageShow((merge ? "棋譜のマージに失敗しました。\n" : "棋譜の読み込みに失敗しました。\n") + error, MessageShowType.Error); kifuManager.Init(); // 不正な局面のままになるとまずいので初期化。 } else { // 読み込みが完了すれば自動的に末尾の局面に行っているはずだが、 // 棋譜ウィンドウを更新した結果、分岐局面などに戻ってしまうといけない。 // 棋譜に書かれていた持ち時間設定・残り時間を画面に反映させる。(GameSettingには反映させない) PlayTimers.SetKifuTimeSettings(kifuManager.Tree.KifuTimeSettings); PlayTimers.SetKifuMoveTimes(kifuManager.Tree.GetKifuMoveTimes()); UpdateTimeString(); // 末尾の局面に移動するコマンドを叩いておく。 // マージ時は、マージ起点に移動してやる。たぶん現在の選択していた行がそれ。 if (!merge) { UpdateKifuSelectedIndex(int.MaxValue); } else { UpdateKifuSelectedIndex(oldSelectedIndex + 1); } // -- 棋譜上の名前を表示名に反映させる。 foreach (var c in All.Colors()) { var name = kifuManager.KifuHeader.GetPlayerName(c); SetPlayerName(c, name); } } // 棋譜が綺麗になった扱いにする。(この棋譜はファイルなどに丸ごと保存されているはずであるから) // ただし、mergeのときは汚れたという扱いにする。(ファイルに残っていないので) KifuDirty = dirty; // 駒を持ち上げていたりしたらそれをリセットする必要があるので。 RaisePropertyChanged("TurnChanged"); } }); }
/// <summary> /// 対局開始のためにGameSettingの設定に従い、ゲームを初期化する。 /// </summary> /// <param name="gameSetting"></param> private void GameStart(GameSetting gameSetting) { // 音声:「よろしくお願いします。」 TheApp.app.soundManager.Stop(); // 再生中の読み上げをすべて停止 TheApp.app.soundManager.ReadOut(SoundEnum.Start); // 初期化中である。 Initializing = true; // 初回の指し手で、「先手」「後手」と読み上げるためのフラグ sengo_read_out = new bool[2] { false, false }; // プレイヤーの生成 foreach (var c in All.Colors()) { var playerType = gameSetting.Player(c).IsHuman ? PlayerTypeEnum.Human : PlayerTypeEnum.UsiEngine; Players[(int)c] = PlayerBuilder.Create(playerType); } // 局面の設定 kifuManager.EnableKifuList = true; if (gameSetting.Board.BoardTypeCurrent) { // 現在の局面からなので、いま以降の局面を削除する。 // ただし、いまの局面と棋譜ウィンドウとが同期しているとは限らない。 // まず現在局面以降の棋譜を削除しなくてはならない。 // 元nodeが、special moveであるなら、それを削除しておく。 if (kifuManager.Tree.IsSpecialNode()) { kifuManager.Tree.UndoMove(); } kifuManager.Tree.ClearForward(); // 分岐棋譜かも知れないので、現在のものを本譜の手順にする。 kifuManager.Tree.MakeCurrentNodeMainBranch(); } else // if (gameSetting.Board.BordTypeEnable) { kifuManager.Init(); kifuManager.InitBoard(gameSetting.Board.BoardType); } // 本譜の手順に変更したので現在局面と棋譜ウィンドウのカーソルとを同期させておく。 UpdateKifuSelectedIndex(); // 現在の時間設定を、KifuManager.Treeに反映させておく(棋譜保存時にこれが書き出される) kifuManager.Tree.KifuTimeSettings = gameSetting.KifuTimeSettings; // 対局者氏名の設定 // 人間の時のみ有効。エンジンの時は、エンジン設定などから取得することにする。(TODO:あとで考える) foreach (var c in All.Colors()) { var player = Player(c); string name; switch (player.PlayerType) { case PlayerTypeEnum.Human: name = gameSetting.Player(c).PlayerName; break; default: name = c.Pretty(); break; } kifuManager.KifuHeader.SetPlayerName(c, name); } // 持ち時間などの設定が必要なので、コピーしておく。 GameSetting = gameSetting; // 消費時間計算用 foreach (var c in All.Colors()) { var pc = PlayTimer(c); pc.KifuTimeSetting = GameSetting.KifuTimeSettings.Player(c); pc.GameStart(); } // rootの持ち時間設定をここに反映させておかないと待ったでrootまで持ち時間が戻せない。 // 途中の局面からだとここではなく、現局面のところに書き出す必要がある。 kifuManager.Tree.SetKifuMoveTimes(PlayTimers.GetKifuMoveTimes()); // コンピュータ vs 人間である場合、人間側を手前にしてやる。 foreach (var c in All.Colors()) { if (gameSetting.Player(c).IsHuman&& gameSetting.Player(c.Not()).IsCpu) { BoardReverse = (c == Color.WHITE); } } // 盤面編集中である可能性がある。リセットする。 TheApp.app.config.InTheBoardEdit = false; InTheGame = true; }
/// <summary> /// 指し手が指されたかのチェックを行う /// </summary> private void CheckPlayerMove() { // 現状の局面の手番側 var stm = Position.sideToMove; var stmPlayer = Player(stm); var config = TheApp.app.Config; // -- 指し手 Move bestMove; if (GameMode.IsConsiderationWithEngine()) { // 検討モードなのでエンジンから送られてきたbestMoveの指し手は無視。 bestMove = stmPlayer.SpecialMove; } else { // 対局設定ダイアログの「コンピューターは1手に必ずこれだけ使う」が設定されていれば、 // その時間になるまでbest moveを無視する。 var stmBestMove = stmPlayer.BestMove; if (stmBestMove != Move.NONE && GameMode == GameModeEnum.InTheGame && /* 通常対局中 */ stmPlayer.PlayerType == PlayerTypeEnum.UsiEngine && GameSetting.MiscSettings.EnableBestMoveIgnoreTimeForEngine && PlayTimer(stm).ElapsedTime() < GameSetting.MiscSettings.BestMoveIgnoreTimeForEngine ) { stmBestMove = Move.NONE; } // TIME_UPなどのSpecialMoveが積まれているなら、そちらを優先して解釈する。 bestMove = stmPlayer.SpecialMove != Move.NONE ? stmPlayer.SpecialMove : stmBestMove; } if (bestMove != Move.NONE) { PlayTimer(stm).ChangeToThemTurn(bestMove == Move.TIME_UP); stmPlayer.SpecialMove = Move.NONE; // クリア // 駒が動かせる状況でかつ合法手であるなら、受理する。 bool specialMove = false; if (GameMode == GameModeEnum.InTheGame) { // 送信されうる特別な指し手であるか? specialMove = bestMove.IsSpecial(); // エンジンから送られてきた文字列が、illigal moveであるならエラーとして表示する必要がある。 if (specialMove) { switch (bestMove) { // 入玉宣言勝ち case Move.WIN: var rule = (EnteringKingRule)GameSetting.MiscSettings.EnteringKingRule; if (Position.DeclarationWin(rule) != Move.WIN) { // 入玉宣言条件を満たしていない入玉宣言 goto ILLEGAL_MOVE; } break; // 中断 // コンピューター同士の対局の時にも人間判断で中断できなければならないので // 対局中であればこれを無条件で受理する。 case Move.INTERRUPT: // 時間切れ // 時間切れになるとBestMoveに自動的にTIME_UPが積まれる。これも無条件で受理する。 case Move.TIME_UP: break; // 投了 case Move.RESIGN: break; // 手番側の投了は無条件で受理 // それ以外 default: // それ以外は受理しない goto ILLEGAL_MOVE; } } else if (!Position.IsLegal(bestMove)) { // 合法手ではない goto ILLEGAL_MOVE; } // -- bestMoveを受理して、局面を更新する。 kifuManager.Tree.AddNode(bestMove, PlayTimers.GetKifuMoveTimes()); // 受理できる性質の指し手であることは検証済み // special moveであってもDoMove()してしまう。 kifuManager.DoMove(bestMove); KifuDirty = true; // 新しいnodeに到達したので棋譜は汚れた扱い。 // -- 音声の読み上げ var soundManager = TheApp.app.SoundManager; // special moveはMoveを直接渡して再生。 if (bestMove.IsSpecial()) { soundManager.ReadOut(bestMove); } else { // -- 駒音 PlayPieceSound(GameMode, bestMove.To(), stm); // -- 棋譜の読み上げ // 「先手」と「後手」と読み上げる。 if (!sengo_read_out[(int)stm] || config.ReadOutSenteGoteEverytime != 0) { sengo_read_out[(int)stm] = true; // 駒落ちの時は、「上手(うわて)」と「下手(したて)」 if (!Position.Handicapped) { soundManager.ReadOut(stm == Color.BLACK ? SoundEnum.Sente : SoundEnum.Gote); } else { soundManager.ReadOut(stm == Color.BLACK ? SoundEnum.Shitate : SoundEnum.Uwate); } } // 棋譜文字列をそのまま頑張って読み上げる。 // ただし、棋譜ウィンドウの表示形式を変更できるので…。 var kif = kifuManager.Tree.LastKifuString; soundManager.ReadOut(kif); } } // -- 次のPlayerに、自分のturnであることを通知してやる。 if (!specialMove) { // 相手番になったので諸々通知。 NotifyTurnChanged(); } else { // 特殊な指し手だったので、これにてゲーム終了 GameEnd(bestMove); } } return; ILLEGAL_MOVE: // これ、棋譜に記録すべき Move m = Move.ILLEGAL_MOVE; kifuManager.Tree.AddNode(m, PlayTimers.GetKifuMoveTimes()); kifuManager.Tree.AddNodeComment(m, stmPlayer.BestMove.ToUsi() /* String あとでなおす*/ /* 元のテキスト */); kifuManager.Tree.DoMove(m); GameEnd(m); // これにてゲーム終了。 }
/// <summary> /// GameStart()とGameRestart()の共通部分 /// </summary> private void GameStartInCommon(GameModeEnum nextGameMode) { var gameSetting = GameSetting; // 現在の時間設定を、KifuManager.Treeに反映させておく(棋譜保存時にこれが書き出される) kifuManager.Tree.KifuTimeSettings = gameSetting.KifuTimeSettings; // 対局者氏名の設定 // 対局設定ダイアログの名前をそのまま引っ張ってくる。 foreach (var c in All.Colors()) { var name = gameSetting.PlayerSetting(c).PlayerName; SetPlayerName(c, name); } // 持ち時間設定の表示文字列の構築(最初に構築してしまい、対局中には変化させない) foreach (var c in All.Colors()) { var pc = PlayTimer(c); pc.KifuTimeSetting = GameSetting.KifuTimeSettings.Player(c); pc.GameStart(); var left = pc.KifuTimeSetting.ToShortString(); var right = PresetWithBoardTypeString(c); string total; if (right.Empty()) { total = left; } else if (left.Empty()) { total = right; // このとき連結のための"/"要らない } else if (left.UnicodeLength() + right.UnicodeLength() < 24) { total = $"{left}/{right}"; // 1行で事足りる } else { total = $"{left}\r\n{right}"; // 2行に分割する。 } timeSettingStrings[(int)c] = total; } // rootの持ち時間設定をここに反映させておかないと待ったでrootまで持ち時間が戻せない。 // 途中の局面からだとここではなく、現局面のところに書き出す必要がある。 kifuManager.Tree.SetKifuMoveTimes(PlayTimers.GetKifuMoveTimes()); // コンピュータ vs 人間である場合、人間側を手前にしてやる。 // 人間 vs 人間の場合も最初の手番側を手前にしてやる。 // ただし、人間 vs 人間で振り駒でこの手番が決まったのであれば、 // それを反映しなければならない。 var stm = kifuManager.Position.sideToMove; if (gameSetting.PlayerSetting(stm).IsHuman) { // 1. 両方が人間である場合、普通は手番側が手前だが、振り駒をしたなら // 対局設定の左側のプレイヤーがつねに手前。 if (gameSetting.PlayerSetting(stm.Not()).IsHuman) { BoardReverse = (stm == Color.WHITE) ^ continuousGame.Swapped; } // 2. 手番側が人間である場合(非手番側がCPU) else { BoardReverse = (stm == Color.WHITE); } } // 3. 手番側がCPUで、非手番側が人間である場合。 else if (gameSetting.PlayerSetting(stm).IsCpu&& gameSetting.PlayerSetting(stm.Not()).IsHuman) { BoardReverse = (stm == Color.BLACK); } // プレイヤー情報などを検討ダイアログに反映させる。 InitEngineConsiderationInfo(nextGameMode); // 検討モードならそれを停止させる必要があるが、それはGameModeのsetterがやってくれる。 GameMode = nextGameMode; }
/// <summary> /// 対局開始のためにGameSettingの設定に従い、ゲームを初期化する。 /// </summary> /// <param name="gameSetting"></param> private void GameStart(GameSetting gameSetting) { // 以下の初期化中に駒が動かされるの気持ち悪いのでユーザー操作を禁止しておく。 CanUserMove = false; Initializing = true; var nextGameMode = GameModeEnum.InTheGame; // 音声:「よろしくお願いします。」 TheApp.app.soundManager.Stop(); // 再生中の読み上げをすべて停止 TheApp.app.soundManager.ReadOut(SoundEnum.Start); // 初回の指し手で、「先手」「後手」と読み上げるためのフラグ sengo_read_out = new bool[2] { false, false }; // プレイヤーの生成 UsiEngineHashManager.Init(); foreach (var c in All.Colors()) { var gamePlayer = gameSetting.PlayerSetting(c); var playerType = gamePlayer.IsHuman ? PlayerTypeEnum.Human : PlayerTypeEnum.UsiEngine; Players[(int)c] = PlayerBuilder.Create(playerType); if (playerType == PlayerTypeEnum.UsiEngine) { var engineDefineEx = TheApp.app.EngineDefines.Find(x => x.FolderPath == gamePlayer.EngineDefineFolderPath); if (engineDefineEx == null) { TheApp.app.MessageShow("エンジンがありませんでした。" + gamePlayer.EngineDefineFolderPath, MessageShowType.Error); return; } var usiEnginePlayer = Players[(int)c] as UsiEnginePlayer; var ponder = gamePlayer.Ponder; InitUsiEnginePlayer(c, usiEnginePlayer, engineDefineEx, gamePlayer.SelectedEnginePreset, nextGameMode, ponder); } } // 局面の設定 kifuManager.EnableKifuList = true; if (gameSetting.BoardSetting.BoardTypeCurrent) { // 現在の局面からなので、いま以降の局面を削除する。 // ただし、いまの局面と棋譜ウィンドウとが同期しているとは限らない。 // まず現在局面以降の棋譜を削除しなくてはならない。 // 元nodeが、special moveであるなら、それを削除しておく。 if (kifuManager.Tree.IsSpecialNode()) { kifuManager.Tree.UndoMove(); } kifuManager.Tree.ClearForward(); // 分岐棋譜かも知れないので、現在のものを本譜の手順にする。 kifuManager.Tree.MakeCurrentNodeMainBranch(); } else // if (gameSetting.Board.BoardTypeEnable) { kifuManager.Init(); kifuManager.InitBoard(gameSetting.BoardSetting.BoardType); } // 本譜の手順に変更したので現在局面と棋譜ウィンドウのカーソルとを同期させておく。 UpdateKifuSelectedIndex(); // エンジンに与えるHashSize,Threadsの計算 if (UsiEngineHashManager.CalcHashSize() != 0) { // Hash足りなくてダイアログ出した時にキャンセルボタン押されとる Disconnect(); // ゲームが終局したことを通知するために音声があったほうがよさげ。 TheApp.app.soundManager.ReadOut(SoundEnum.End); return; } // 現在の時間設定を、KifuManager.Treeに反映させておく(棋譜保存時にこれが書き出される) kifuManager.Tree.KifuTimeSettings = gameSetting.KifuTimeSettings; // 対局者氏名の設定 // 人間の時のみ有効。エンジンの時は、エンジン設定などから取得することにする。 foreach (var c in All.Colors()) { var player = Player(c); string name; switch (player.PlayerType) { case PlayerTypeEnum.Human: name = gameSetting.PlayerSetting(c).PlayerName; break; default: name = c.Pretty(); break; } kifuManager.KifuHeader.SetPlayerName(c, name); } // 持ち時間などの設定が必要なので、コピーしておく。 GameSetting = gameSetting; // 消費時間計算用 foreach (var c in All.Colors()) { var pc = PlayTimer(c); pc.KifuTimeSetting = GameSetting.KifuTimeSettings.Player(c); pc.GameStart(); timeSettingStrings[(int)c] = pc.KifuTimeSetting.ToShortString(); } // rootの持ち時間設定をここに反映させておかないと待ったでrootまで持ち時間が戻せない。 // 途中の局面からだとここではなく、現局面のところに書き出す必要がある。 kifuManager.Tree.SetKifuMoveTimes(PlayTimers.GetKifuMoveTimes()); // コンピュータ vs 人間である場合、人間側を手前にしてやる。 // 人間 vs 人間の場合も最初の手番側を手前にしてやる。 var stm = kifuManager.Position.sideToMove; // 1. 手番側が人間である場合(非手番側が人間 or CPU) if (gameSetting.PlayerSetting(stm).IsHuman) { BoardReverse = (stm == Color.WHITE); } // 2. 手番側がCPUで、非手番側が人間である場合。 else if (gameSetting.PlayerSetting(stm).IsCpu&& gameSetting.PlayerSetting(stm.Not()).IsHuman) { BoardReverse = (stm == Color.BLACK); } // プレイヤー情報などを検討ダイアログに反映させる。 InitEngineConsiderationInfo(nextGameMode); // 検討モードならそれを停止させる必要があるが、それはGameModeのsetterがやってくれる。 GameMode = nextGameMode; }
/// <summary> /// 対局開始のためにGameSettingの設定に従い、ゲームを初期化する。 /// </summary> /// <param name="gameSetting"></param> private void GameStart(GameSetting gameSetting) { // 以下の初期化中に駒が動かされるの気持ち悪いのでユーザー操作を禁止しておく。 CanUserMove = false; lastInitializing = true; // 音声:「よろしくお願いします。」 TheApp.app.soundManager.Stop(); // 再生中の読み上げをすべて停止 TheApp.app.soundManager.ReadOut(SoundEnum.Start); // 初回の指し手で、「先手」「後手」と読み上げるためのフラグ sengo_read_out = new bool[2] { false, false }; // プレイヤーの生成 foreach (var c in All.Colors()) { var playerType = gameSetting.Player(c).IsHuman ? PlayerTypeEnum.Human : PlayerTypeEnum.UsiEngine; Players[(int)c] = PlayerBuilder.Create(playerType); } // 局面の設定 kifuManager.EnableKifuList = true; if (gameSetting.Board.BoardTypeCurrent) { // 現在の局面からなので、いま以降の局面を削除する。 // ただし、いまの局面と棋譜ウィンドウとが同期しているとは限らない。 // まず現在局面以降の棋譜を削除しなくてはならない。 // 元nodeが、special moveであるなら、それを削除しておく。 if (kifuManager.Tree.IsSpecialNode()) { kifuManager.Tree.UndoMove(); } kifuManager.Tree.ClearForward(); // 分岐棋譜かも知れないので、現在のものを本譜の手順にする。 kifuManager.Tree.MakeCurrentNodeMainBranch(); } else // if (gameSetting.Board.BoardTypeEnable) { kifuManager.Init(); kifuManager.InitBoard(gameSetting.Board.BoardType); } // 本譜の手順に変更したので現在局面と棋譜ウィンドウのカーソルとを同期させておく。 UpdateKifuSelectedIndex(); // 現在の時間設定を、KifuManager.Treeに反映させておく(棋譜保存時にこれが書き出される) kifuManager.Tree.KifuTimeSettings = gameSetting.KifuTimeSettings; // 対局者氏名の設定 // 人間の時のみ有効。エンジンの時は、エンジン設定などから取得することにする。(TODO:あとで考える) foreach (var c in All.Colors()) { var player = Player(c); string name; switch (player.PlayerType) { case PlayerTypeEnum.Human: name = gameSetting.Player(c).PlayerName; break; default: name = c.Pretty(); break; } kifuManager.KifuHeader.SetPlayerName(c, name); } // 持ち時間などの設定が必要なので、コピーしておく。 GameSetting = gameSetting; // 消費時間計算用 foreach (var c in All.Colors()) { var pc = PlayTimer(c); pc.KifuTimeSetting = GameSetting.KifuTimeSettings.Player(c); pc.GameStart(); } // rootの持ち時間設定をここに反映させておかないと待ったでrootまで持ち時間が戻せない。 // 途中の局面からだとここではなく、現局面のところに書き出す必要がある。 kifuManager.Tree.SetKifuMoveTimes(PlayTimers.GetKifuMoveTimes()); // コンピュータ vs 人間である場合、人間側を手前にしてやる。 // 人間 vs 人間の場合も最初の手番側を手前にしてやる。 var stm = kifuManager.Position.sideToMove; // 1. 手番側が人間である場合(非手番側が人間 or CPU) if (gameSetting.Player(stm).IsHuman) { BoardReverse = (stm == Color.WHITE); } // 2. 手番側がCPUで、非手番側が人間である場合。 else if (gameSetting.Player(stm).IsCpu&& gameSetting.Player(stm.Not()).IsHuman) { BoardReverse = (stm == Color.BLACK); } // プレイヤー情報などを検討ダイアログに反映させる。 var nextGameMode = GameModeEnum.InTheGame; InitEngineConsiderationInfo(nextGameMode); // 検討モードならそれを停止させる必要があるが、それはGameModeのsetterがやってくれる。 GameMode = nextGameMode; }
/// <summary> /// 指し手が指されたかのチェックを行う /// </summary> private void CheckPlayerMove() { // 現状の局面の手番側 var stm = Position.sideToMove; var stmPlayer = Player(stm); var config = TheApp.app.config; // -- 指し手 Move bestMove; if (GameMode.IsWithEngine()) { // 検討モードなのでエンジンから送られてきたbestMoveの指し手は無視。 bestMove = stmPlayer.SpecialMove; } else { // TIME_UPなどのSpecialMoveが積まれているなら、そちらを優先して解釈する。 bestMove = stmPlayer.SpecialMove != Move.NONE ? stmPlayer.SpecialMove : stmPlayer.BestMove; } if (bestMove != Move.NONE) { PlayTimer(stm).ChageToThemTurn(bestMove == Move.TIME_UP); stmPlayer.SpecialMove = Move.NONE; // クリア // 駒が動かせる状況でかつ合法手であるなら、受理する。 bool specialMove = false; if (GameMode == GameModeEnum.InTheGame) { // 送信されうる特別な指し手であるか? specialMove = bestMove.IsSpecial(); // エンジンから送られてきた文字列が、illigal moveであるならエラーとして表示する必要がある。 if (specialMove) { switch (bestMove) { // 入玉宣言勝ち case Move.WIN: if (Position.DeclarationWin(EnteringKingRule.POINT27) != Move.WIN) { // 入玉宣言条件を満たしていない入玉宣言 goto ILLEGAL_MOVE; } break; // 中断 // コンピューター同士の対局の時にも人間判断で中断できなければならないので // 対局中であればこれを無条件で受理する。 case Move.INTERRUPT: // 時間切れ // 時間切れになるとBestMoveに自動的にTIME_UPが積まれる。これも無条件で受理する。 case Move.TIME_UP: break; // 投了 case Move.RESIGN: break; // 手番側の投了は無条件で受理 // それ以外 default: // それ以外は受理しない goto ILLEGAL_MOVE; } } else if (!Position.IsLegal(bestMove)) { // 合法手ではない goto ILLEGAL_MOVE; } // -- bestMoveを受理して、局面を更新する。 kifuManager.Tree.AddNode(bestMove, PlayTimers.GetKifuMoveTimes()); // 受理できる性質の指し手であることは検証済み // special moveであってもDoMove()してしまう。 kifuManager.DoMove(bestMove); // -- 音声の読み上げ var soundManager = TheApp.app.soundManager; var kif = kifuManager.KifuList[kifuManager.KifuList.Count - 1]; // special moveはMoveを直接渡して再生。 if (bestMove.IsSpecial()) { soundManager.ReadOut(bestMove); } else { // -- 駒音 if (TheApp.app.config.PieceSoundInTheGame != 0) { // 移動先の升の下に別の駒があるときは、駒がぶつかる音になる。 var to = bestMove.To(); var delta = stm == Color.BLACK ? Square.SQ_D : Square.SQ_U; var to2 = to + (int)delta; // to2が盤外であることがあるので、IsOk()を通すこと。 var e = (to2.IsOk() && Position.PieceOn(to2) != Piece.NO_PIECE) ? SoundEnum.KOMA_B1 /*ぶつかる音*/: SoundEnum.KOMA_S1 /*ぶつからない音*/; #if false // あまりいい効果音作れなかったのでコメントアウトしとく。 if (TheApp.app.config.CrashPieceSoundInTheGame != 0) { // ただし、captureか捕獲する指し手であるなら、衝撃音に変更する。 if (Position.State().capturedPiece != Piece.NO_PIECE || Position.InCheck()) { e = SoundEnum.KOMA_C1; } } #endif soundManager.PlayPieceSound(e); } // -- 棋譜の読み上げ // 「先手」と「後手」と読み上げる。 if (!sengo_read_out[(int)stm] || config.ReadOutSenteGoteEverytime != 0) { sengo_read_out[(int)stm] = true; // 駒落ちの時は、「上手(うわて)」と「下手(したて)」 if (!Position.Handicapped) { soundManager.ReadOut(stm == Color.BLACK ? SoundEnum.Sente : SoundEnum.Gote); } else { soundManager.ReadOut(stm == Color.BLACK ? SoundEnum.Shitate : SoundEnum.Uwate); } } // 棋譜文字列をそのまま頑張って読み上げる。 soundManager.ReadOut(kif); } } // -- 次のPlayerに、自分のturnであることを通知してやる。 if (!specialMove) { NotifyTurnChanged(); } else { // 特殊な指し手だったので、これにてゲーム終了 GameEnd(); } } return; ILLEGAL_MOVE: // これ、棋譜に記録すべき Move m = Move.ILLEGAL_MOVE; kifuManager.Tree.AddNode(m, PlayTimers.GetKifuMoveTimes()); kifuManager.Tree.AddNodeComment(m, stmPlayer.BestMove.ToUsi() /* String あとでなおす*/ /* 元のテキスト */); kifuManager.Tree.DoMove(m); GameEnd(); // これにてゲーム終了。 }
/// <summary> /// プレイヤーの消費時間を計測する用 /// </summary> /// <param name="c"></param> /// <returns></returns> public PlayTimer PlayTimer(Color c) { return(PlayTimers.Player(c)); }
/// <summary> /// GameStart()とGameRestart()の共通部分 /// </summary> private void GameStartInCommon(GameModeEnum nextGameMode) { var gameSetting = GameSetting; // 現在の時間設定を、KifuManager.Treeに反映させておく(棋譜保存時にこれが書き出される) kifuManager.Tree.KifuTimeSettings = gameSetting.KifuTimeSettings; // 対局者氏名の設定 // 人間の時のみ有効。エンジンの時は、エンジン設定などから取得することにする。 foreach (var c in All.Colors()) { var player = Player(c); string name; switch (player.PlayerType) { case PlayerTypeEnum.Human: name = gameSetting.PlayerSetting(c).PlayerName; break; default: name = c.Pretty(); break; } kifuManager.KifuHeader.SetPlayerName(c, name); } // 消費時間計算用 foreach (var c in All.Colors()) { var pc = PlayTimer(c); pc.KifuTimeSetting = GameSetting.KifuTimeSettings.Player(c); pc.GameStart(); timeSettingStrings[(int)c] = pc.KifuTimeSetting.ToShortString(); } // rootの持ち時間設定をここに反映させておかないと待ったでrootまで持ち時間が戻せない。 // 途中の局面からだとここではなく、現局面のところに書き出す必要がある。 kifuManager.Tree.SetKifuMoveTimes(PlayTimers.GetKifuMoveTimes()); // コンピュータ vs 人間である場合、人間側を手前にしてやる。 // 人間 vs 人間の場合も最初の手番側を手前にしてやる。 var stm = kifuManager.Position.sideToMove; // 1. 手番側が人間である場合(非手番側が人間 or CPU) if (gameSetting.PlayerSetting(stm).IsHuman) { BoardReverse = (stm == Color.WHITE); } // 2. 手番側がCPUで、非手番側が人間である場合。 else if (gameSetting.PlayerSetting(stm).IsCpu&& gameSetting.PlayerSetting(stm.Not()).IsHuman) { BoardReverse = (stm == Color.BLACK); } // プレイヤー情報などを検討ダイアログに反映させる。 InitEngineConsiderationInfo(nextGameMode); // 検討モードならそれを停止させる必要があるが、それはGameModeのsetterがやってくれる。 GameMode = nextGameMode; }