オセロAIオセロAI
ゲーム木を用いたオセロAIの開発

概要
オセロの強いAIをゲーム木を用いて開発するのがこの研究の目標。ただし一般PCで扱える実行速度であることが条件である。
なお、開発したオセロAIはこちらから相手としてプレイすることができます。ぜひたたかってみてください。
ゲーム木及びその探索法の実装についてはこちらの記事を参考のこと。

強さの指標
様々な戦略をもつプレイヤーやAIが相手になる。特定のAIだけに強いのは望ましくない。
あらゆる戦略を確率的に取りうるランダムに手を打つ(隅は優先的に取る)AIに対する勝率を強さの指標とする。

評価関数の工夫
石のひっくり返されない度(信頼度)を3つに分け評価する
不確定石  次の手でひっくり返され得る石
暫定石    次の手ではひっくり返され得ない石
確定石    今後ひっくり返されることがない石

石の配置による評価テーブルを3つの信頼度に分け用意する。重み付けは遺伝的アルゴリズムで学習させる。(PCをフル稼働させて数日掛かりました・・・)
オセロ重み付け

評価
・強さ
オセロ勝率

・実行速度(ノードの評価数)
オセロ速度

実装
盤面クラス

namespace ReversiEngine {

    /// <summary>盤面</summary>
    public class Board : ICloneable{
        private int stone_count;
        private readonly Stone[,] board;

        /// <summary>コンストラクタ</summary>
        public Board() {
            this.stone_count = 0;
            this.board = new Stone[Size, Size];
        }

        /// <summary>コンストラクタ</summary>
        /// <param name="board">石配列</param>
        public Board(Stone[,] board) {
            if(board == null) {
                throw new ArgumentNullException();
            }

            if(Size != board.GetLength(0) || Size != board.GetLength(1)) {
                throw new ArgumentException();
            }

            this.board = (Stone[,])board.Clone();

            for(int x, y = 0; y < Size; y++) {
                for(x = 0; x < Size; x++) {
                    if(this.board[x, y] != Stone.None) {
                        stone_count++;
                    }
                }
            }
        }

        /// <summary>コンストラクタ</summary>
        public Board(Board board) {
            if(board == null) {
                throw new ArgumentNullException();
            }

            this.board = (Stone[,])board.board.Clone();
            this.stone_count = board.stone_count;
        }

        /// <summary>盤面のサイズ</summary>
        public const int Size = 8;

        /// <summary>盤面インデクサ</summary>
        public Stone this [int x, int y]{
            get {
                return board[x, y];
            }
            set {
                if(board[x, y] == Stone.None && value != Stone.None) {
                    stone_count++;
                }
                else if(board[x, y] != Stone.None && value == Stone.None) {
                    stone_count--;
                }

                board[x, y] = value;
            }
        }

        /// <summary>石を配置可能か判定</summary>
        public bool IsLocatable(int x, int y, Stone color) {
            if(color == Stone.None) {
                throw new ArgumentException();
            }

            if(board[x, y] != Stone.None) {
                return false;
            }

            Func<int, int, bool> is_locatable_dxy = (dx, dy) => {
                int px = x + dx, py = y + dy;

                if(px < 0 || px >= Size || py < 0 || py >= Size) {
                    return false;
                }

                if(board[px, py] == color || board[px, py] == Stone.None) {
                    return false;
                }

                for(;;) {
                    px += dx;
                    py += dy;

                    if(px < 0 || px >= Size || py < 0 || py >= Size) {
                        return false;
                    }
                    
                    if(board[px, py] == Stone.None) {
                        return false;
                    }
                    if(board[px, py] == color) {
                        return true;
                    }
                }
            };

            if(is_locatable_dxy(-1, -1)) {
                return true;
            }
            if(is_locatable_dxy(-1,  0)) {
                return true;
            }
            if(is_locatable_dxy(-1, +1)) {
                return true;
            }
            if(is_locatable_dxy( 0, -1)) {
                return true;
            }
            if(is_locatable_dxy( 0, +1)) {
                return true;
            }
            if(is_locatable_dxy(+1, -1)) {
                return true;
            }
            if(is_locatable_dxy(+1,  0)) {
                return true;
            }
            if(is_locatable_dxy(+1, +1)) {
                return true;
            }

            return false;
        }

        /// <summary>石を配置可能か判定</summary>
        public bool IsLocatable(Stone color) {
            for(int x, y = 0; y < Size; y++) {
                for(x = 0; x < Size; x++) {
                    if(IsLocatable(x, y, color)) {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>石を隅に配置可能か判定</summary>
        public bool IsCornerLocatable(Stone color) {
            return IsLocatable(0, 0, color) || IsLocatable(Size - 1, 0, color) || IsLocatable(0, Size - 1, color) || IsLocatable(Size - 1, Size - 1, color);
        }

        /// <summary>石を配置</summary>
        public void Locate(int x, int y, Stone color) {
            bool flip_stone = false;

            Action<int, int> locate_dxy = (dx, dy) => {
                int px = x, py = y;

                for(;;) {
                    px += dx;
                    py += dy;

                    if(px < 0 || px >= Size || py < 0 || py >= Size) {
                        return;
                    }

                    if(board[px, py] == Stone.None) {
                        return;
                    }

                    if(board[px, py] == color) {
                        break;
                    }
                }

                px -= dx;
                py -= dy;

                while(x != px || y != py) {
                    board[px, py] = color;
                    flip_stone = true;

                    px -= dx;
                    py -= dy;
                }
            };
            
            locate_dxy(-1, -1);
            locate_dxy(-1,  0);
            locate_dxy(-1, +1);
            locate_dxy( 0, -1);
            locate_dxy( 0, +1);
            locate_dxy(+1, -1);
            locate_dxy(+1,  0);
            locate_dxy(+1, +1);

            if(flip_stone) {
                board[x, y] = color;
                stone_count++;
            }
        }

        /// <summary>石の信頼性を判定</summary>
        public StoneReliability JudgeReliability(int x, int y) {
            Stone color = board[x, y];
            StoneReliability reliability = StoneReliability.Definite;

            if(color == Stone.None) {
                return StoneReliability.None;
            }
            
            Func<int, int, bool> isexist_opposite_dxy = (dx, dy) => {
                int px = x, py = y;

                for(;;) {
                    px += dx;
                    py += dy;

                    if(px < 0 || px >= Size || py < 0 || py >= Size) {
                        return false;
                    }
                    
                    if(board[px, py] == Stone.None) {
                        return false;
                    }

                    if(board[px, py] != color) {
                        return true;
                    }
                }
            };

            Func<int, int, bool> is_edge_dxy = (dx, dy) => {
                int px = x, py = y;

                for(;;) {
                    px += dx;
                    py += dy;

                    if(px < 0 || px >= Size || py < 0 || py >= Size) {
                        return true;
                    }

                    if(board[px, py] == Stone.None) {
                        return false;
                    }
                }
            };

            Action<int, int> renovate_reliability = (dx, dy) => {
                bool wall_a, wall_b, stone_a, stone_b;

                wall_a = is_edge_dxy(dx, dy);
                wall_b = is_edge_dxy(-dx, -dy);

                if(wall_a && wall_b) {
                    return;
                }

                stone_a = isexist_opposite_dxy(dx, dy);
                stone_b = isexist_opposite_dxy(-dx, -dy);

                if(wall_a && !wall_b && !stone_a) {
                    return;
                }
                if(!wall_a && wall_b && !stone_b) {
                    return;
                }

                if(stone_a && !stone_b && !wall_b) {
                    reliability = StoneReliability.Uncertain;
                    return;
                }
                if(!stone_a && stone_b && !wall_a) {
                    reliability = StoneReliability.Uncertain;
                    return;
                }

                reliability = StoneReliability.Interim;
            };

            renovate_reliability(1, 0);
            if(reliability == StoneReliability.Uncertain) {
                return reliability;
            }
            
            renovate_reliability(0, 1);
            if(reliability == StoneReliability.Uncertain) {
                return reliability;
            }

            renovate_reliability(1, 1);
            if(reliability == StoneReliability.Uncertain) {
                return reliability;
            }

            renovate_reliability(1, -1);
            if(reliability == StoneReliability.Uncertain) {
                return reliability;
            }

            return reliability;
        }

        /// <summary>石の個数</summary>
        public int StoneCount {
            get{
                return stone_count;
            }
        }

        /// <summary>石の個数をカウント</summary>
        public int CountStone(Stone stone) {
            int cnt = 0;

            for(int x, y = 0; y < Size; y++) {
                for(x = 0; x < Size; x++) {
                    if(board[x, y] == stone) {
                        cnt++;
                    }
                }
            }

            return cnt;
        }

        /// <summary>初期配置</summary>
        public static Board Init() {
            Board ret = new Board();

            ret[3, 3] = Stone.White;
            ret[3, 4] = Stone.Black;
            ret[4, 3] = Stone.Black;
            ret[4, 4] = Stone.White;

            return ret;
        }

        /// <summary>盤面が埋まっているか否か</summary>
        public bool IsFill {
            get {
                for(int x, y = 0; y < Size; y++) {
                    for(x = 0; x < Size; x++) {
                        if(board[x, y] == Stone.None) {
                            return false;
                        }
                    }
                }

                return true;
            }
        }

        /// <summary>ゲームセットか否か</summary>
        public bool IsEndGame {
            get {
                if(IsFill || !IsExistStone(Stone.White) || !IsExistStone(Stone.Black)) {
                    return true;
                }

                if(!IsLocatable(Stone.White) && !IsLocatable(Stone.Black)) {
                    return true;
                }

                return false;
            }
        }

        /// <summary>石が存在するか否か</summary>
        public bool IsExistStone(Stone stone) {
            for(int x, y = 0; y < Size; y++) {
                for(x = 0; x < Size; x++) {
                    if(board[x, y] == stone) {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>可視化</summary>
        public override string ToString() {
            string str = "";
            for(int x, y = 0; y < Size; y++) {
                for(x = 0; x < Size; x++) {
                    if(board[x, y] == Reversi.Stone.White) {
                        str += "□";
                    }
                    else if(board[x, y] == Reversi.Stone.Black) {
                        str += "■";
                    }
                    else {
                        str += "  ";
                    }
                }

                str += "\n";
            }

            return str;
        }

        /// <summary>クローン</summary>
        public object Clone() {
            return new Board(this);
        }
    }
}

石列挙型

namespace ReversiEngine {
    ///<summary>石 列挙型</summary>
    public enum Stone : int{
        /// <summary>石が配置されていない</summary>
        None = 0 ,
        /// <summary>白</summary>
        White = +1,
        /// <summary>黒</summary>
        Black = -1
    };

    ///<summary>石 信頼性</summary>
    public enum StoneReliability : int{
        ///<summary>未配置</summary>
        None = 0, 
        ///<summary>不確実 - 次の手で返される可能性アリ</summary> 
        Uncertain,
        ///<summary>暫定 - 次の手で返される可能性ナシ</summary>
        Interim,
        ///<summary>確定 - 今後返される可能性ナシ</summary>
        Definite
    };
}

棋譜クラス

namespace ReversiEngine {

    /// <summary>棋譜</summary>
    public class Decision {

        /// <summary>X座標</summary>
        public int X {
            get; private set;
        } = 0;

        /// <summary>Y座標</summary>
        public int Y {
            get; private set;
        } = 0;

        /// <summary>パスか否か</summary>
        public bool IsPass {
            get; private set;
        } = false;

        /// <summary>配置</summary>
        public static Decision Place(int x, int y) {
            return new Decision { X = x, Y = y };
        }

        /// <summary>パス</summary>
        public static Decision Pass {
            get {
                return new Decision { IsPass = true };
            }
        }

        /// <summary>文字列化</summary>
        public override string ToString() {
            return $"{X},{Y},{IsPass}";
        }
    }
}

プレイ状態クラス

namespace ReversiEngine {

    ///<summary>プレイ状態クラス</summary>
    public class State : IState<Decision>, ICloneable {

        /// <summary>コンストラクタ</summary>
        public State(Board board, Evaluator evaluator, Stone PlayerColor, Stone NextColor, bool is_finale_mode = false) {
            if(board == null || evaluator == null) {
                throw new ArgumentNullException();
            }

            if(PlayerColor == Stone.None || NextColor == Stone.None) {
                throw new ArgumentException();
            }

            this.Board = (Board)board.Clone();
            this.Evaluator = evaluator;
            this.PlayerColor = PlayerColor;
            this.NextColor = NextColor;
            this.IsFinaleMode = is_finale_mode;
        }

        /// <summary>盤面状態</summary>
        public Board Board {
            get; private set;
        }

        /// <summary>評価法</summary>
        public Evaluator Evaluator {
            get; private set;
        }

        /// <summary>プレイヤーの石の色</summary>
        public Stone PlayerColor {
            get; private set;
        }

        /// <summary>次に打つ石の色</summary>
        public Stone NextColor {
            get; private set;
        }

        /// <summary>終局読みモードを有効にするか</summary>
        public bool IsFinaleMode {
            get; private set;
        }

        /// <summary>盤面の評価</summary>
        public double Evaluation {
            get {
                return Evaluator.Evaluate(Board, PlayerColor, IsFinaleMode);
            }
        }

        /// <summary>ゲームセットか否か</summary>
        public bool IsEndGame => Board.IsEndGame;

        /// <summary>次の手をコレクションとして返す</summary>
        public IEnumerable<Decision> NextDecisions() {
            bool exist_locatable = false;

            for(int i, j = 0; j < Board.Size / 2; j++) {
                if(Board.IsLocatable(j, j, NextColor)) {
                    exist_locatable = true;
                    yield return Decision.Place(j, j);
                }
                if(Board.IsLocatable(Board.Size - j - 1, j, NextColor)) {
                    exist_locatable = true;
                    yield return Decision.Place(Board.Size - j - 1, j);
                }
                if(Board.IsLocatable(j, Board.Size - j - 1, NextColor)) {
                    exist_locatable = true;
                    yield return Decision.Place(j, Board.Size - j - 1);
                }
                if(Board.IsLocatable(Board.Size - j - 1, Board.Size - j - 1, NextColor)) {
                    exist_locatable = true;
                    yield return Decision.Place(Board.Size - j - 1, Board.Size - j - 1);
                }

                for(i = j + 1; i < Board.Size / 2; i++) {
                    if(Board.IsLocatable(i, j, NextColor)) {
                        exist_locatable = true;
                        yield return Decision.Place(i, j);
                    }
                    if(Board.IsLocatable(Board.Size - i - 1, j, NextColor)) {
                        exist_locatable = true;
                        yield return Decision.Place(Board.Size - i - 1, j);
                    }
                    if(Board.IsLocatable(i, Board.Size - j - 1, NextColor)) {
                        exist_locatable = true;
                        yield return Decision.Place(i, Board.Size - j - 1);
                    }
                    if(Board.IsLocatable(Board.Size - i - 1, Board.Size - j - 1, NextColor)) {
                        exist_locatable = true;
                        yield return Decision.Place(Board.Size - i - 1, Board.Size - j - 1);
                    }
                    if(Board.IsLocatable(j, i, NextColor)) {
                        exist_locatable = true;
                        yield return Decision.Place(j, i);
                    }
                    if(Board.IsLocatable(j, Board.Size - i - 1, NextColor)) {
                        exist_locatable = true;
                        yield return Decision.Place(j, Board.Size - i - 1);
                    }
                    if(Board.IsLocatable(Board.Size - j - 1, i, NextColor)) {
                        exist_locatable = true;
                        yield return Decision.Place(Board.Size - j - 1, i);
                    }
                    if(Board.IsLocatable(Board.Size - j - 1, Board.Size - i - 1, NextColor)) {
                        exist_locatable = true;
                        yield return Decision.Place(Board.Size - j - 1, Board.Size - i - 1);
                    }
                }
            }

            if(!exist_locatable) {
                yield return Decision.Pass;
            }
        }

        /// <summary>次の盤面状態を生成する</summary>
        public IState<Decision> NextState(Decision decision) {
            State ret = (State)Clone();

            if(!decision.IsPass) {
                ret.Board.Locate(decision.X, decision.Y, NextColor);
            }

            if(NextColor == Stone.Black) {
                ret.NextColor = Stone.White;
            }
            else if(NextColor == Stone.White) {
                ret.NextColor = Stone.Black;
            }

            return ret;
        }

        /// <summary>次に隅が配置可能ならばその打つ手を返す</summary>
        /// <remarks>隅に配置不能ならばnullを返す</remarks>
        public Decision NextCornerDecision() {
            double ev, max_ev = double.NegativeInfinity;
            Decision decision, max_ev_decision = null;

            if(Board.IsLocatable(0, 0, NextColor)) {
                decision = Decision.Place(0, 0);
                ev = NextState(decision).Evaluation;
                if(max_ev < ev) {
                    max_ev = ev;
                    max_ev_decision = decision;
                }
            }
            if(Board.IsLocatable(0, Board.Size - 1, NextColor)) {
                decision = Decision.Place(0, Board.Size - 1);
                ev = NextState(decision).Evaluation;
                if(max_ev < ev) {
                    max_ev = ev;
                    max_ev_decision = decision;
                }
            }
            if(Board.IsLocatable(Board.Size - 1, 0, NextColor)) {
                decision = Decision.Place(Board.Size - 1, 0);
                ev = NextState(decision).Evaluation;
                if(max_ev < ev) {
                    max_ev = ev;
                    max_ev_decision = decision;
                }
            }
            if(Board.IsLocatable(Board.Size - 1, Board.Size - 1, NextColor)) {
                decision = Decision.Place(Board.Size - 1, Board.Size - 1);
                ev = NextState(decision).Evaluation;
                if(max_ev < ev) {
                    max_ev = ev;
                    max_ev_decision = decision;
                }
            }

            return max_ev_decision;
        }

        /// <summary>石の個数をカウント</summary>
        public int CountStone(Stone stone) {
            return Board.CountStone(stone);
        }

        /// <summary>クローン</summary>
        public object Clone() {
            return new State(Board, Evaluator, PlayerColor, NextColor, IsFinaleMode);
        }
    }
}

評価法クラス

namespace ReversiEngine {

    /// <summary>評価法クラス</summary>
    public class Evaluator {
        static readonly int[,] ev_table_uncertain = new int[Board.Size, Board.Size]
            { {    0, -798, -10, -546, -546, -10, -798,    0 },
              { -798, -501, -40, -150, -150, -40, -501, -798 },
              {  -10,  -40,  26,   49,   49,  26,  -40,  -10 },
              { -546, -150,  49,  -50,  -50,  49, -150, -546 },
              { -546, -150,  49,  -50,  -50,  49, -150, -546 },
              {  -10,  -40,  26,   49,   49,  26,  -40,  -10 },
              { -798, -501, -40, -150, -150, -40, -501, -798 },
              {    0, -798, -10, -546, -546, -10, -798,    0 }};

        static readonly int[,] ev_table_interim = new int[Board.Size, Board.Size]
            { {    0, -532, -224, -111, -111, -224, -532,    0 },
              { -532, -494,  -32, -156, -156,  -32, -494, -532 },
              { -224,  -32,   27,   49,   49,   27,  -32, -224 },
              { -111, -156,   49,   22,   22,   49, -156, -111 },
              { -111, -156,   49,   22,   22,   49, -156, -111 },
              { -224,  -32,   27,   49,   49,   27,  -32, -224 },
              { -532, -494,  -32, -156, -156,  -32, -494, -532 },
              {    0, -532, -224, -111, -111, -224, -532,    0 }};
        
        static readonly int[,] ev_table_definite = new int[Board.Size, Board.Size]
            { { 2500, 767, 607, 642, 642, 607, 767, 2500 },
              {  767, 344, 284, 103, 103, 284, 344,  767 },
              {  607, 284, 114, 182, 182, 114, 284,  607 },
              {  642, 103, 182, 202, 202, 182, 103,  642 },
              {  642, 103, 182, 202, 202, 182, 103,  642 },
              {  607, 284, 114, 182, 182, 114, 284,  607 },
              {  767, 344, 284, 103, 103, 284, 344,  767 },
              { 2500, 767, 607, 642, 642, 607, 767, 2500 }};
        
        /// <summary>コンストラクタ</summary>
        public Evaluator() { }

        /// <summary>評価</summary>
        /// <param name="board">盤面</param>
        /// <param name="evaluate_color">評価する石</param>
        /// <param name="finale_mode">終局読みモードを有効にするか</param>
        /// <returns>評価値</returns>
        public double Evaluate(Board board, Stone evaluate_color, bool finale_mode = false) {
            if(evaluate_color == Stone.None) {
                throw new ArgumentException();
            }

            if(finale_mode) {
                return board.CountStone(evaluate_color);
            }

            if(!board.IsExistStone(evaluate_color)) {
                return double.MinValue;
            }

            if(!board.IsExistStone((evaluate_color != Stone.Black) ? Stone.Black : Stone.White)) {
                return double.MaxValue;
            }
            
            int ev = 0, evaluate_stone_cnt = 0, opponent_stone_cnt = 0;
            bool isexist_uncertain_evaluate_stone = false, isexist_uncertain_opponent_stone = false;

            for(int x, y = 0; y < Board.Size; y++) {
                for(x = 0; x < Board.Size; x++) {
                    if(board[x, y] == Stone.None) {
                        continue;
                    }

                    if(board[x, y] == evaluate_color) {
                        evaluate_stone_cnt++;

                        var stone_reliability = board.JudgeReliability(x, y);

                        switch(stone_reliability) {
                            case StoneReliability.Uncertain:
                                isexist_uncertain_evaluate_stone = true;
                                ev += ev_table_uncertain[x, y];
                                break;
                            case StoneReliability.Interim:
                                ev += ev_table_interim[x, y];
                                break;
                            case StoneReliability.Definite:
                                ev += ev_table_definite[x, y];
                                break;
                        }
                    }
                    else{
                        opponent_stone_cnt++;

                        var stone_reliability = board.JudgeReliability(x, y);

                        switch(stone_reliability) {
                            case StoneReliability.Uncertain:
                                isexist_uncertain_opponent_stone = true;
                                ev -= ev_table_uncertain[x, y];
                                break;
                            case StoneReliability.Interim:
                                ev -= ev_table_interim[x, y];
                                break;
                            case StoneReliability.Definite:
                                ev -= ev_table_definite[x, y];
                                break;
                        }
                    }
                }
            }

            if(evaluate_stone_cnt > opponent_stone_cnt && !isexist_uncertain_evaluate_stone) {
                return ev + 1.0e+6;
            }
            if(evaluate_stone_cnt < opponent_stone_cnt && !isexist_uncertain_opponent_stone) {
                return ev - 1.0e+6;
            }
            
            return ev;
        }
    }
}

プレイヤークラス

namespace ReversiEngine {

    ///<summary>プレイヤー</summary>
    public interface Player {

        /// <summary>次の手を打つ</summary>
        /// <returns>決定オブジェクトを返す</returns>
        Decision Play(Board current_board);

        /// <summary>プレイヤーの色</summary>
        Stone Color { get; }
    }

    ///<summary>プレイヤー (ゲーム木のノード評価数 5000)</summary>
    public class WakabaTechE5000Player : Player {
        Stone player_color;
        Evaluator evaluator;
        
        /// <summary>コンストラクタ</summary>
        public WakabaTechE5000Player(Stone player_color) {
            this.player_color = player_color;

            evaluator = new Evaluator();
        }

        /// <summary>次の手を打つ</summary>
        /// <returns>決定オブジェクトを返す</returns>
        public Decision Play(Board current_board) {
            if(current_board.CountStone(Stone.None) > 10) {
                State root_state = new State(current_board, evaluator, player_color, player_color);
                return DiscontinuableIDDFSMethod<State, Decision>.Search(root_state, 5, 12, 5000, Decision.Pass); 
            }
            else {
                State root_state = new State(current_board, evaluator, player_color, player_color, is_finale_mode: true);
                return CompleteSearchMethod<State, Decision>.Search(root_state, Decision.Pass);
            }
        }

        /// <summary>プレイヤーの色</summary>
        public Stone Color {
            get {
                return player_color;
            }
        }
    }
}




ラボラボ
誤差関数の近似誤差関数の近似
オセロAIオセロAI
ランダウ分布ランダウ分布