トップページに戻る    次のC#のサンプルへ    前のC#のサンプルへ

Cマガ電脳クラブ(第062回) クラム・ゲーム

問題

長さ25の盤に長さ2の駒をふたりで交互にマス目に沿って置いていく。
好きな場所に駒を置いてよいのだが、自分の手番で駒を置けなかったら負けとなる。
つまり、最後に置いたほうが勝ちとする。

両者が最善をつくしたら、このゲームはどちらが勝ちであろうか。
先手必勝か後手必勝かどちらなのかを調べてもらいたい。

盤の長さが偶数のときは、先手が最初に中央に駒を置けば左右に同じ長さが残るので、
後手がどう駒を置いても次に先手は反対側に同じように駒を置くことができる。
つまり盤の長さが偶数のときは先手必勝である。

盤の長さが奇数のときは、先手必勝と後手必勝のどちらの場合もある。
たとえば、長さ3なら先手必勝、長さ5なら後手必勝である。
余裕のある人は、いろいろな長さについてどちらの必勝なのか調べて、法則性を考えてみてほしい。


ソース

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static int UB;

    struct JyoutaiDef
    {
        internal int Level;
        internal int NodeID;
        internal int OyaNodeID; //親のNodeID
        internal int SameBanmenNodeID; //同一盤面のNodeID
        internal bool[] BanArr;

        //勝敗 (未決定は0、先手勝は1、後手勝は2、同一盤面の勝敗取得は3)
        internal int Syouhai;
    }

    //先手必勝かのDict
    static Dictionary<int, bool> IsSenteHissyouDict = new Dictionary<int, bool>();

    static void Main()
    {
        var sw = System.Diagnostics.Stopwatch.StartNew();

        for (int I = 2; I <= 28; I++) {
            Console.Write("盤幅={0,2}。", I);

            UB = I - 1;

            //ゲーム木の作成
            List<JyoutaiDef> GameTreeList = CreateGameTreeList();

            //ゲーム木から、先手必勝かを調べる
            if (IsSenteHissyou(GameTreeList)) {
                Console.WriteLine("先手必勝。経過時間={0}", sw.Elapsed);
                IsSenteHissyouDict[I] = true;
            }
            else {
                Console.WriteLine("後手必勝。経過時間={0}", sw.Elapsed);
                IsSenteHissyouDict[I] = false;
            }
        }
    }

    //ゲーム木の作成
    static List<JyoutaiDef> CreateGameTreeList()
    {
        var WillReturnList = new List<JyoutaiDef>();

        var stk = new Stack<JyoutaiDef>();
        JyoutaiDef WillPush;

        WillPush.Level = 0;
        WillPush.NodeID = GetNodeID();
        WillPush.OyaNodeID = -1;
        WillPush.SameBanmenNodeID = -1;
        WillPush.BanArr = new bool[UB + 1];
        WillPush.Syouhai = 0;
        WillPush.Syouhai = DeriveSyouhai(WillPush);
        WillReturnList.Add(WillPush);
        if (WillPush.Syouhai == 0) stk.Push(WillPush);

        while (stk.Count > 0) {
            JyoutaiDef Popped = stk.Pop();

            var JyoutaiList = new List<JyoutaiDef>();

            //勝ちになる手があるか?
            bool HasKatite = false;

            for (int X = 0; X <= UB; X++) {
                //1手目は対称な配置がない箇所のみ配置可能
                if (Popped.Level == 0) {
                    //偶数の場合
                    if (UB % 2 == 1 && X > UB / 2) break;

                    //奇数の場合
                    if (UB % 2 == 0 && X >= UB / 2) break;
                }

                if (CanFillPiece(X, Popped.BanArr) == false) continue;

                WillPush.BanArr = (bool[])Popped.BanArr.Clone();

                //駒を配置する経路のPush処理
                for (int wkX = 0; wkX <= 1; wkX++) {
                    WillPush.BanArr[X + wkX] = true;
                }
                WillPush.Level = Popped.Level + 1;
                WillPush.NodeID = GetNodeID();
                WillPush.OyaNodeID = Popped.NodeID;

                //同一盤面のチェック
                bool HasSameBanmen = false;
                foreach (JyoutaiDef EachNode in WillReturnList) {
                    if (EachNode.Level != WillPush.Level) continue;
                    if (EachNode.Syouhai == 3) continue;
                    if (IsSameBanmen(EachNode.BanArr, WillPush.BanArr)) {
                        HasSameBanmen = true;
                        WillPush.Syouhai = 3;
                        WillPush.SameBanmenNodeID = EachNode.NodeID;
                        JyoutaiList.Add(WillPush);
                        break;
                    }
                }

                //同一盤面が既出の場合
                if (HasSameBanmen) continue;

                WillPush.Syouhai = DeriveSyouhai(WillPush);
                WillPush.SameBanmenNodeID = -1;

                //勝ちになる手があったら、その手で確定
                if (WillPush.Syouhai == DeriveTebanFromLevel(WillPush.Level)) {
                    HasKatite = true;
                    WillReturnList.Add(WillPush);
                    break;
                }
                JyoutaiList.Add(WillPush);
            }

            if (HasKatite) continue;

            JyoutaiList.ForEach(X =>
            {
                WillReturnList.Add(X);
                if (X.Syouhai == 0) stk.Push(X);
            });
        }

        Console.Write("ゲーム木のノード数={0,6}。", WillReturnList.Count);
        return WillReturnList;
    }

    //盤面から勝敗(未決定は0、先手勝は1、後手勝は2)を求める
    static int DeriveSyouhai(JyoutaiDef pJyoutaiDef)
    {
        //連続空白の固まりの数
        var SpaceCntList = new List<int>();

        int wkSpaceCnt = 0;
        bool[] wkArr = pJyoutaiDef.BanArr;

        for (int I = 0; I <= UB; I++) {
            //連続空白の場合
            if (wkArr[I] == false &&
              (0 <= I - 1 && wkArr[I - 1] == false || I + 1 <= UB && wkArr[I + 1] == false)) {
                wkSpaceCnt++;
            }
            else if (wkSpaceCnt > 0) { //連続空白でない場合
                SpaceCntList.Add(wkSpaceCnt);
                wkSpaceCnt = 0;
            }

            if (I == UB && wkSpaceCnt > 0) SpaceCntList.Add(wkSpaceCnt);
        }

        //連続空白の固まりが1つしかない場合
        if (SpaceCntList.Count == 1) {
            int SpaceCnt = SpaceCntList[0];

            //偶数なら次の手番をもったほうの勝ち
            if (SpaceCnt % 2 == 0)
                return DeriveTebanFromLevel(pJyoutaiDef.Level + 1);

            if (IsSenteHissyouDict.ContainsKey(SpaceCnt)) {
                bool wkBool = IsSenteHissyouDict[SpaceCnt];

                //次の手番を持ったほうの勝ちの場合
                if (wkBool) return DeriveTebanFromLevel(pJyoutaiDef.Level + 1);

                //次の手番を持ったほうの負けの場合
                else return DeriveTebanFromLevel(pJyoutaiDef.Level);
            }
        }

        if (SpaceCntList.Count > 1) {
            //連続空白の固まりが2と3と5のみの場合
            if (SpaceCntList.TrueForAll(X => X == 2 || X == 3 || X == 5)) {
                int wkCnt = SpaceCntList.Count(X => X == 2 || X == 3);

                //2と3の固まりの数が偶数なら、次の手番を持ったほうの負け
                if (wkCnt % 2 == 0)
                    return DeriveTebanFromLevel(pJyoutaiDef.Level);

                //2と3の固まりの数が奇数なら、次の手番を持ったほうの勝ち
                if (wkCnt % 2 == 1)
                    return DeriveTebanFromLevel(pJyoutaiDef.Level + 1);
            }
        }

        if (SpaceCntList.Count > 1) {
            int wkCnt = SpaceCntList.Count(X => IsSenteHissyouDict[X]);

            //先手勝ちの固まりが0なら、次の手番を持ったほうの負け
            if (wkCnt == 0)
                return DeriveTebanFromLevel(pJyoutaiDef.Level);

            //先手勝ちの固まりが1つだけなら、次の手番を持ったほうの勝ち
            if (wkCnt == 1)
                return DeriveTebanFromLevel(pJyoutaiDef.Level + 1);
        }

        return 0;
    }

    //ゲーム木から、先手必勝かを調べる
    static bool IsSenteHissyou(List<JyoutaiDef> pGameTreeList)
    {
        //レベルの降順でソートしておく
        JyoutaiDef[] GameTreeArr = pGameTreeList.OrderByDescending(X => X.Level).ToArray();

        while (GameTreeArr.Length > 1) {
            int TreeHeight = GameTreeArr.Max(X => X.Level);

            //Console.WriteLine("●●●●●●●●●●●●●●");
            //Console.WriteLine("状態表示します");
            //PrintGameTreeInfo(GameTreeArr);

            //処理1
            //レベル = 木の高さで、勝敗が0のノードは、最後に駒を置いたほうの勝ち
            for (int I = 0; I <= GameTreeArr.Length - 1; I++) {
                if (GameTreeArr[I].Level < TreeHeight) break;
                if (GameTreeArr[I].Syouhai != 0) continue;
                GameTreeArr[I].Syouhai = DeriveTebanFromLevel(GameTreeArr[I].Level);
            }

            //処理2
            //同一盤面の勝敗取得ノードに勝敗をコピー
            for (int I = 0; I <= GameTreeArr.Length - 1; I++) {
                if (GameTreeArr[I].Level < TreeHeight) break;
                if (GameTreeArr[I].Syouhai != 3) continue;
                for (int J = 0; J <= GameTreeArr.Length - 1; J++) {
                    if (GameTreeArr[J].Level < TreeHeight) break;
                    if (GameTreeArr[J].NodeID != GameTreeArr[I].SameBanmenNodeID) continue;
                    GameTreeArr[I].Syouhai = GameTreeArr[J].Syouhai;
                }
            }

            //処理3
            //木の高さ-1のノードの勝敗を決定する
            for (int I = 0; I <= GameTreeArr.Length - 1; I++) {
                if (GameTreeArr[I].Level < TreeHeight - 1) break;
                if (GameTreeArr[I].Level > TreeHeight - 1) continue;
                if (GameTreeArr[I].Syouhai != 0) continue;

                int Teban = DeriveTebanFromLevel(GameTreeArr[I].Level);

                //レベルが奇数(先手)の場合は、後手が最善手で応じても先手勝ちなら、先手勝ち
                if (Teban == 1) {
                    if (Array.Exists(GameTreeArr,
                                     X => X.OyaNodeID == GameTreeArr[I].NodeID && X.Syouhai == 2))
                        GameTreeArr[I].Syouhai = 2;
                    else GameTreeArr[I].Syouhai = 1;
                }

                //レベルが偶数(後手)の場合は、先手が最善手で応じても後手勝ちなら、後手勝ち
                if (Teban == 2) {
                    if (Array.Exists(GameTreeArr,
                                     X => X.OyaNodeID == GameTreeArr[I].NodeID && X.Syouhai == 1))
                        GameTreeArr[I].Syouhai = 1;
                    else GameTreeArr[I].Syouhai = 2;
                }
            }

            //処理4
            //木の高さのノードをRemove
            List<JyoutaiDef> wkGameTreeList = GameTreeArr.ToList();
            wkGameTreeList.RemoveAll(X => X.Level == TreeHeight);
            GameTreeArr = wkGameTreeList.OrderByDescending(X => X.Level).ToArray();
        }

        //Console.WriteLine("●●●●●●●●●●●●●●");
        //Console.WriteLine("状態表示します");
        //PrintGameTreeInfo(GameTreeArr);

        return GameTreeArr[0].Syouhai == 1;
    }

    //引数のレベルに対する手番(先手は1、後手は2)を返す
    static int DeriveTebanFromLevel(int pLevel)
    {
        return (pLevel % 2 == 1) ? 1 : 2;
    }

    //NodeID(自動採番)
    static int mNodeID = 0;
    static int GetNodeID()
    {
        return ++mNodeID;
    }

    //同一盤面かを返す
    static bool IsSameBanmen(bool[] pBanArr1, bool[] pBanArr2)
    {
        bool IsDF1 = false, IsDF2 = false;

        for (int X = 0; X <= UB; X++) {
            bool CurrVal = pBanArr1[X];
            if (pBanArr2[X] != CurrVal) IsDF1 = true;
            if (pBanArr2[UB - X] != CurrVal) IsDF2 = true;
        }
        return IsDF1 == false || IsDF2 == false;
    }

    //マス目にピースを埋めれるか
    static bool CanFillPiece(int pTargetX, bool[] pBanArr)
    {
        for (int X = 0; X <= 1; X++) {
            if (pTargetX + X > UB) return false;
            if (pBanArr[pTargetX + X]) return false;
        }
        return true;
    }

    //ゲーム木の状態を表示
    static void PrintGameTreeInfo(JyoutaiDef[] pGameTreeArr)
    {
        var sb = new System.Text.StringBuilder();

        foreach (JyoutaiDef EachNode in pGameTreeArr) {
            sb.Length = 0;

            sb.AppendFormat("Level={0},ID={1},OyaID={2},SameID={3}",
                EachNode.Level, EachNode.NodeID, EachNode.OyaNodeID, EachNode.SameBanmenNodeID);
            sb.AppendLine();

            for (int X = 0; X <= UB; X++) {
                bool wkBool = EachNode.BanArr[X];
                if (wkBool) sb.Append('■');
                else sb.Append('空');
            }
            sb.Append("      ");
            string wkStr = "";
            if (EachNode.Syouhai == 0) wkStr = "未決定";
            if (EachNode.Syouhai == 1) wkStr = "先手勝";
            if (EachNode.Syouhai == 2) wkStr = "後手勝";
            if (EachNode.Syouhai == 3) wkStr = "同一盤面の勝敗取得";
            sb.AppendFormat("勝敗={0}", wkStr);
            sb.AppendLine();
            Console.WriteLine(sb.ToString());
        }
    }
}


実行結果

盤幅= 2。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.1938439
盤幅= 3。ゲーム木のノード数=     2。先手必勝。経過時間=00:00:00.2571114
盤幅= 4。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.2576738
盤幅= 5。ゲーム木のノード数=     3。後手必勝。経過時間=00:00:00.2587999
盤幅= 6。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.2606280
盤幅= 7。ゲーム木のノード数=     2。先手必勝。経過時間=00:00:00.2610493
盤幅= 8。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.2613580
盤幅= 9。ゲーム木のノード数=     6。後手必勝。経過時間=00:00:00.2647819
盤幅=10。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.2653334
盤幅=11。ゲーム木のノード数=     2。先手必勝。経過時間=00:00:00.2656748
盤幅=12。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.2659933
盤幅=13。ゲーム木のノード数=    70。先手必勝。経過時間=00:00:00.2682326
盤幅=14。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.2687291
盤幅=15。ゲーム木のノード数=   227。後手必勝。経過時間=00:00:00.2747365
盤幅=16。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.2751467
盤幅=17。ゲーム木のノード数=     2。先手必勝。経過時間=00:00:00.2754690
盤幅=18。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:00.2757822
盤幅=19。ゲーム木のノード数=  5063。先手必勝。経過時間=00:00:01.4331774
盤幅=20。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:01.4341127
盤幅=21。ゲーム木のノード数= 17835。後手必勝。経過時間=00:00:17.8533411
盤幅=22。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:17.8548779
盤幅=23。ゲーム木のノード数=     2。先手必勝。経過時間=00:00:17.8551944
盤幅=24。ゲーム木のノード数=     1。先手必勝。経過時間=00:00:17.8561646
盤幅=25。ゲーム木のノード数=193245。後手必勝。経過時間=00:31:51.3801483
盤幅=26。ゲーム木のノード数=     1。先手必勝。経過時間=00:31:51.4127672
盤幅=27。ゲーム木のノード数=     2。先手必勝。経過時間=00:31:51.4131751
盤幅=28。ゲーム木のノード数=     1。先手必勝。経過時間=00:31:51.4173125


解説

下記のアルゴリズムで、ゲーム木から、根ノードの勝敗を求めてます。

1 ゲーム木の高さとレベルが一致するノードの、勝敗を求める。
2 ゲーム木の高さ-1のレベルのノードが先手番の場合
    後手勝の子ノードが1つでもあれば、後手勝のノードである。
    後手勝の子ノードが1つもなければ、先手勝のノードである。
  ゲーム木の高さ-1のレベルのノードが後手番の場合
    先手勝の子ノードが1つでもあれば、先手勝のノードである。
    先手勝の子ノードが1つもなければ、後手勝のノードである。
3 ゲーム木の高さとレベルが一致するノードをRemoveする。
4 ゲーム木の高さが2以上なら、1に戻る