【C#】【オセロ】最短で黒一色になる手順57通りをすべて求める(協力最短詰めオセロ)

はじめに

YouTubeを見ているとオセロにおいて9手で白が全滅する(黒一色になる)手順が57種類あると紹介されていました。
秒で勝つオセロ 9手で全滅 - YouTube

突然ですが今回はその57種類の手順をすべて求めるプログラミングに挑戦してみます。

結果

ルービックキューブのプログラミングに以前挑戦しました。その時と同じ「深さ優先探索」を行いました。
なんと2秒で探索が終わりました。

Start searching length 2
Start searching length 3
Start searching length 4
Start searching length 5
Start searching length 6
Start searching length 7
Start searching length 8
Start searching length 9
37 29 18 42 34 43 21 26 50
37 29 18 42 34 43 30 26 50
37 29 18 44 53 34 33 45 21
37 29 18 44 53 38 39 45 21
37 29 18 44 53 45 21 9 0
37 29 18 44 53 45 21 20 12
37 29 18 44 53 45 21 34 33
37 29 18 44 53 45 21 38 39
37 29 18 44 53 45 21 52 60
37 29 18 44 53 45 21 54 63
37 29 18 44 53 46 30 38 39
37 29 19 43 51 20 21 22 15
37 29 19 43 51 20 21 34 33
37 29 19 43 51 20 21 38 39
37 29 19 43 51 20 21 50 57
37 29 21 45 53 20 19 18 9
37 29 21 45 53 20 19 34 33
37 29 21 45 53 20 19 38 39
37 29 21 45 53 20 19 54 63
37 29 22 44 51 34 33 43 19
37 29 22 44 51 38 39 43 19
37 29 22 44 51 42 26 34 33
37 29 22 44 51 43 19 20 12
37 29 22 44 51 43 19 34 33
37 29 22 44 51 43 19 38 39
37 29 22 44 51 43 19 50 57
37 29 22 44 51 43 19 52 60
37 29 22 46 38 45 19 30 54
37 29 22 46 38 45 26 30 54
37 43 18 21 20 29 51 19 22
37 43 34 29 20 45 38 44 52
37 43 34 29 52 38 39 44 20
37 43 34 29 52 45 38 44 20
37 43 34 45 52 29 38 44 20
37 45 19 20 13 29 38 44 52
37 45 19 20 13 38 39 44 52
37 45 19 20 21 29 53 18 9
37 45 19 20 21 29 53 34 33
37 45 19 20 21 29 53 38 39
37 45 19 20 21 29 53 54 63
37 45 19 20 21 29 54 13 5
37 45 19 20 21 29 54 53 61
37 45 19 20 21 34 33 29 53
37 45 19 20 21 38 39 29 53
37 45 26 20 13 29 38 44 52
37 45 26 20 13 38 39 44 52
37 45 26 34 53 17 33 44 8
37 45 53 29 19 20 21 18 9
37 45 53 29 19 20 21 34 33
37 45 53 29 19 20 21 38 39
37 45 53 29 19 20 21 54 63
37 45 53 29 21 20 19 18 9
37 45 53 29 21 20 19 34 33
37 45 53 29 21 20 19 38 39
37 45 53 29 21 20 19 54 63
37 45 53 34 26 17 33 44 8
37 45 53 38 31 34 33 29 20
Finished!(00:00:02.0492755)

C#コード

メインコード(Program.cs)

盤面を表すBoardクラスと深さ優先探索のためのSearchクラスをあとで紹介します。
メインはSearchクラスのメソッドを実行するだけです。

using System.Diagnostics;

namespace reversi;

class Program
{
    static void Main(string[] args)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();

        Search search = new Search();
        search.start_search();

        sw.Stop();

        TimeSpan ts = new TimeSpan();
        ts = sw.Elapsed;
        Console.WriteLine("Finished!({0})", ts);
    }
}

Boardクラス(Board.cs)

namespace reversi;

class Board
{
    private readonly UInt64 init = 1;
    private readonly UInt64 right_left_watcher = Convert.ToUInt64("7E7E7E7E7E7E7E7E", 16);
    private readonly UInt64 upper_lower_watcher = Convert.ToUInt64("00FFFFFFFFFFFF00", 16);
    private readonly UInt64 around_watcher = Convert.ToUInt64("007E7E7E7E7E7E00", 16);

    public UInt64 black;
    public UInt64 white;
    public int CurrentColor;

    public Dictionary<int, UInt64> possiblePos = new Dictionary<int, UInt64>();

    public Board(UInt64 black_stone, UInt64 white_stone, int turn)
    {
        black = black_stone;
        white = white_stone;

        CurrentColor = turn;

        if (CurrentColor == 1)
        {
            update_possiblePos(black, white);
        }
        else
        {
            update_possiblePos(white, black);
        }
    }

    public Board move(int x)
    {   
        UInt64 rev = possiblePos[x];
        UInt64 new_black;
        UInt64 new_white;

        if (CurrentColor == 1)
        {
            new_black = black ^ ((init << x) | rev);
            new_white = white ^ rev;
        }
        else
        {
            new_black = black ^ rev;
            new_white = white ^ (init << x | rev);
        }

        return new Board(new_black, new_white, CurrentColor * -1);
    }

    public void display()
    {
        foreach (int i in Enumerable.Range(0, 8))
        {
            foreach (int j in Enumerable.Range(0, 8))
            {
                if ((black & init << (8 * i + j)) != 0)
                {
                    Console.Write(" O  ");
                }
                else
                {
                    Console.Write(((white & init << (8 * i + j)) != 0) ? " x  " : " .  ");
                }
            }
            Console.WriteLine();
            Console.WriteLine();
        }
    }

    public void update_possiblePos(UInt64 turn, UInt64 not_turn)
    {
        UInt64 mask;
        UInt64 tmp;
        UInt64 rev;

        possiblePos.Clear();

        for (int i = 0; i < 64; i++)
        {
            //着手箇所は空白でなければダメ
            //空きマスが「0」で表されているUInt64と着手箇所との比較
            //空白でなければcontinue
            UInt64 check_position = init << i;
            if (((turn | not_turn) & (check_position)) != 0) continue;

            rev = 0;

            //右方向チェック
            tmp = 0;
            mask = check_position << 1;
            if ((mask & right_left_watcher) != 0 && (mask & not_turn) != 0)
            {
                tmp |= mask;
                mask = mask << 1;
                while((mask & right_left_watcher) != 0 && (mask & not_turn) != 0)
                {
                    tmp |= mask;
                    mask = mask << 1;
                }
                if ((mask & turn) != 0) rev |= tmp;
            }

            //左方向チェック
            tmp = 0;
            mask = check_position >> 1;
            if ((mask & right_left_watcher) != 0 && (mask & not_turn) != 0)
            {
                tmp |= mask;
                mask = mask >> 1;

                while ((mask & right_left_watcher) != 0 && (mask & not_turn) != 0)
                {
                    tmp |= mask;
                    mask = mask >> 1;
                }
                if ((mask & turn) != 0) rev |= tmp;
            }

            //上方向チェック
            tmp = 0;
            mask = check_position >> 8;
            if ((mask & upper_lower_watcher) != 0 && (mask & not_turn) != 0)
            {
                tmp |= mask;
                mask = mask >> 8;

                while ((mask & upper_lower_watcher) != 0 && (mask & not_turn) != 0)
                {
                    tmp |= mask;
                    mask = mask >> 8;
                }
                if ((mask & turn) != 0) rev |= tmp;
            }

            //下方向チェック
            tmp = 0;
            mask = check_position << 8;
            if ((mask & upper_lower_watcher) != 0 && (mask & not_turn) != 0)
            {
                tmp |= mask;
                mask = mask << 8;

                while ((mask & upper_lower_watcher) != 0 && (mask & not_turn) != 0)
                {
                    tmp |= mask;
                    mask = mask << 8;
                }
                if ((mask & turn) != 0) rev |= tmp;
            }

            //右上方向チェック
            tmp = 0;
            mask = check_position >> 7;
            if ((mask & around_watcher) != 0 && (mask & not_turn) != 0)
            {
                tmp |= mask;
                mask = mask >> 7;

                while ((mask & around_watcher) != 0 && (mask & not_turn) != 0)
                {
                    tmp |= mask;
                    mask = mask >> 7;
                }
                if ((mask & turn) != 0) rev |= tmp;
            }

            //右下方向チェック
            tmp = 0;
            mask = check_position << 9;
            if ((mask & around_watcher) != 0 && (mask & not_turn) != 0)
            {
                tmp |= mask;
                mask = mask << 9;

                while ((mask & around_watcher) != 0 && (mask & not_turn) != 0)
                {
                    tmp |= mask;
                    mask = mask << 9;
                }
                if ((mask & turn) != 0) rev |= tmp;
            }

            //左上方向チェック
            tmp = 0;
            mask = check_position >> 9;
            if ((mask & around_watcher) != 0 && (mask & not_turn) != 0)
            {
                tmp |= mask;
                mask = mask >> 9;

                while ((mask & around_watcher) != 0 && (mask & not_turn) != 0)
                {
                    tmp |= mask;
                    mask = mask >> 9;
                }
                if ((mask & turn) != 0) rev |= tmp;
            }

            //左下方向チェック
            tmp = 0;
            mask = check_position << 7;
            if ((mask & around_watcher) != 0 && (mask & not_turn) != 0)
            {
                tmp |= mask;
                mask = mask << 7;

                while ((mask & around_watcher) != 0 && (mask & not_turn) != 0)
                {
                    tmp |= mask;
                    mask = mask << 7;
                }
                if ((mask & turn) != 0) rev |= tmp;
            }
            
            if (rev != 0) possiblePos.Add(i, rev);
        }
    }
}

Searchクラス(Search.cs)

namespace reversi;

class Search
{
    public List<int> current_solution;
    public Board board;
    bool is_finished = false;

    public Search()
    {
        UInt64 black = Convert.ToUInt64("0000000810000000", 16);
        UInt64 white = Convert.ToUInt64("0000001008000000", 16);

        board = new Board(black, white, 1);

        current_solution = new List<int> { 37 };

        board = board.move(37);
    }

    private bool is_solved(Board board)
    {
        if (board.white == 0 || board.black == 0) return true;
        return false;
    }

    public void depth_limited_search(Board board, int depth)
    {
        if (depth == 0 && is_solved(board)) 
        {
            Console.WriteLine(String.Join(" ", current_solution));
            is_finished = true;
            return;
        }

        if (depth == 0) return;
        
        foreach(var x in board.possiblePos.Keys)
        {
            current_solution.Add(x);
            depth_limited_search(board.move(x), depth - 1);
            current_solution.RemoveAt(current_solution.Count - 1);
        }
        return;
    }

    public void start_search()
    {
        for (int depth = 1; depth < 20; depth++)
        {
            Console.WriteLine($"Start searching length {depth + 1}");
            depth_limited_search(board, depth);
            if (is_finished == true) break;
        }
    }
}

開発環境

ASRock DeskMini H470
CPU: Core i7 11700
メモリ:16G 2667MHz

Windows 10
Visual Studio Community 2022 Preview 4.0
.NET 6.0.0 rc1

解説

解説記事は別記事で書いています。
【C#】【オセロ】解説記事 part 1 - パソコン関連もろもろ
【C#】【オセロ】解説記事 part 2 - パソコン関連もろもろ
【C#】【オセロ】解説記事 part 3 - パソコン関連もろもろ

参考にさせて頂いたサイト

C#でパズルを解く - 協力最短詰めオセロ - Qiita
オセロをビットボードで実装する - Qiita
オセロにおけるビットボード - Enpedia