LoginSignup
6
7

More than 3 years have passed since last update.

C#で3目並べ(○×ゲーム)の思考ルーチンを書いてみた

Last updated at Posted at 2019-01-22

3目並べとは

3×3の格子盤を用意し、二人が交互に「○」と「×」を書き込んでいき、先に3つ並べたほうが勝ちになるゲームでです。
日本では○×ゲーム、海外ではTic tac tooとして知られています。たぶん、ほとんどの人が一度はやったことのあるゲームだと思います。

このゲームは、両者が最善手を打った場合は、必ず引き分けになります。

作成したプログラム

人間とコンピュータが対戦を行う形式としました。コンピュータは常に最善手を打つようにプログラムしています。絶対に負けません(僕のプログラムが正しければですが...)

まずは、どんなプログラムを書いたのか、その実行例を示します。

見てののとおり、コンソール画面で動作する実にシンプルな見た目のプログラムです。

スクリーンショット 2018-12-25 20.56.00.png

上のスクリーンショットは、ゲームの途中の画面です。
人間の手番で23と入力しています。2桁の数字でどこに打つのかの位置を指定しています。23は、2行目、3列目を意味します。

次のスクリーンショットは、ゲームが終了(引き分け)した時の画面です。

スクリーンショット 2018-12-25 20.56.18.png

プログラムを構成するクラス

主要なクラスは、以下の通りです。いわゆるビューに依存したクラスは、ControllerクラスとHumanPlayerクラスだけです。

Controllerクラス

ゲームを制御するクラスです。先手後手を決め、ゲームを開始します。ゲームが終了したら再度ゲームをするかを確認します。実質的なメインプログラムです。

このクラスは、IObserver<Board> を実装しています。いわゆる購読者クラスで、ゲームの状況が変わるとその盤面の状況を受け取ることができます。盤面が変化するとその内容を表示します。いわゆるViewerの役割も担っています。

Gameクラス

1回のゲームを表すクラスです。PerfectPlayerとHumanPlayerを交互に呼び出しゲームを進めます。
このクラスはIObservable<Board> を実装しています。いわゆる発行者クラスです。1手指すたびにその状況を購読者に通知します。そのため、見た目とは完全に独立したクラスになっています。

Boardクラス

盤の状態を表すクラスです。BoardBase<T>(後述)を継承しています。型引数Tには、駒を示すStoneクラスを割り当てます。

Stoneクラス

○×の石を表すクラスです。何も置いていない場所には、空のStone(Stone.Empty)が置いてあるものとします。

IPlayerインターフェイス

プレイヤーを表すインターフェース。

PerfectPlayerクラス

コンピュータ側のプレイヤーを表すクラスです。BestMoveというメソッドを定義し、ここで最善手を見つけています。

BestMoveでは、相手が最善手を指すものと仮定して、最後まで(再帰的に)先読みをします。自分が勝つ手があればそれを指し、なければ引き分けの手を、引き分けもなければ、負けの手を指します。でも、このゲームに限っては負ける手を指すことはないはずです。最低でも引き分けの手を指します。

このルーチンをもっとシンプルなコードにしたかったのですが、この手のプログラミングに慣れていないせいか、思ったよりも長いコードになってしまいました。

HumanPlayerクラス

人間のプレイヤーを表すクラスです。キーボードから手を入力します。

C# のソースコード

クラスの数を少なくして、もっと短いコードのプログラムにすることもできると思いますが、クラスに分割したい派なので、多少長いコードになっても、僕にとってのわかりやすさを優先しました。

3×3の盤面を表すBoardクラスは他のプログラムでも利用しているBoardBase<T>から派生させています。なので、すこしオーバースペックかもしれません。

BoardBase<T>を利用している他のプログラム

などなど。

以下に、全てのソースコードを載せます。(GitHubでも公開しています)

Program.cs

using System;

namespace TicTacToo {
    class Program {
        static void Main(string[] args) {
            var controller = new Controller();
            while (true) {
                controller.Run();
                if (!Controller.Confirm("Try Again?"))
                    break;
            }
        }
    }
}

Controller.cs

using System;
namespace TicTacToo {
    class Controller : IObserver<Board> {
        private IPlayer _player1;
        private IPlayer _player2;
        private Board _board;

        // 試合の開始
        public void Run() {
            DecideFirstPlayer();
            _board = new Board();
            var game = new Game(_player1, _player2, _board);
            // 購読者は自分自身
            game.Subscribe(this);
            var win = game.Start();
        }

        // 先手を決める
        private void DecideFirstPlayer() {
            var b = Confirm("Are you first?");
            if (b) {
                _player1 = new HumanPlayer(Stone.Black);
                _player2 = new PerfectPlayer(Stone.White);
            } else {
                _player1 = new PerfectPlayer(Stone.Black);
                _player2 = new HumanPlayer(Stone.White);
            }
        }

        private IPlayer GetHumanPlayer() =>
             _player1 is HumanPlayer ? _player1 : _player2;

        // 盤面を表示
        private void Print(Board board) {
            Console.Clear();
            Console.WriteLine("You: {0}\n", GetHumanPlayer().Stone.Value);
            for (int y = 1; y <= 3; y++) {
                for (int x = 1; x <= 3; x++) {
                    Console.Write(board[x, y].Value + " ");
                }
                Console.WriteLine();
            }
        }

        // 終了した
        public void OnCompleted() {
            var win = _board.Judge();
            var human = GetHumanPlayer();
            if (win == Stone.Empty)
                Console.WriteLine("Draw");
            else if (win == human.Stone)
                Console.WriteLine("You Win");
            else
                Console.WriteLine("You Lose");
        }

        // エラー発生
        public void OnError(Exception error) {
            Console.WriteLine(error.Message);
        }

        // 状況変化
        public void OnNext(Board value) {
            Print(value);
        }

        // (y/n)の確認
        public static bool Confirm(string message) {
            Console.Write(message);
            var left = Console.CursorLeft;
            var top = Console.CursorTop;
            try {
                while (true) {
                    var key = Console.ReadKey();
                    if (key.KeyChar == 'y')
                        return true;
                    if (key.KeyChar == 'n')
                        return false;
                    Console.CursorLeft = left;
                    Console.CursorTop = top;
                }
            } finally {
                Console.WriteLine();
            }
        }
    }
}

Game.cs

using System;
using System.Collections.Generic;

namespace TicTacToo {
    public class Game : IObservable<Board> {
        private Board _board;
        private IPlayer _player1;
        private IPlayer _player2;

        public Game(IPlayer player1, IPlayer player2, Board board)
        {
            _player1 = player1;
            _player2 = player2;
            _board = board;
        }

        // Game開始
        public Stone Start() {
            IPlayer player = _player1;
            Publish(_board);
            while (!_board.IsFin()) {
                player.NextMove(_board);
                Publish(_board);
                player = Turn(player);
            }
            Complete();
            return _board.Judge();

        }

        // 次のプレイヤー
        private IPlayer Turn(IPlayer player) {
            return player == _player1 ? _player2 : _player1;
        }

        private List<IObserver<Board>> _observers = new List<IObserver<Board>>();

        // 終了を通知する
        private void Complete() {
            foreach (var observer in _observers) {
                observer.OnCompleted();
            }
        }

        // 状況変化を知らせるために購読者に通知する
        private void Publish(Board board) {
            foreach (var observer in _observers) {
                observer.OnNext(board);
            }
        }

        // 購読を申し込む -  observerが通知を受け取れるようになる
        public IDisposable Subscribe(IObserver<Board> observer) {
            _observers.Add(observer);
            return observer as IDisposable;
        }
    }
}

IPlayer.cs

using System;
namespace TicTacToo {
    public interface IPlayer {
        Stone Stone { get; }
        int NextMove(Board board);
    }
}

HumanPlayer.cs

using System;
namespace TicTacToo {
    public class HumanPlayer : IPlayer {
        public Stone Stone { get; private set; }

        public HumanPlayer(Stone mark) {
            Stone = mark;
        }

        // 次の一手を打つ
        public int NextMove(Board board) {
            while (true) {
                var line = Console.ReadLine();
                if (line.Length != 2)
                    continue;
                var x = line[1] - '0';
                var y = line[0] - '0';
                if (1 <= x && x <= 3 && 1 <= y && y <= 3) {
                    var index = board.ToIndex(x, y);
                    if (board.CanPut(index)) {
                        board[index] = Stone;
                        return index;
                    }
                }
            }
        }
    }
}

PerfectPlayer.cs

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

namespace TicTacToo {
    public class PerfectPlayer : IPlayer {

        public Stone Stone { get; private set; }

        // コンストラクタ
        public PerfectPlayer(Stone myStone) {
            Stone = myStone;
        }

        // 一手を打つ
        public int NextMove(Board board) {
            if (board.GetVacantIndexes().Count() == 9) {
                var (place, winner) = FirstMove(Stone, board);
                board[place] = Stone;
                return place;
            } else {
                var (place, winner) = BestMove(board, Stone);
                board[place] = Stone;
                return place;
            }
        }

        // 次のプレイヤー
        private Stone NextPlayer(Stone now) {
            return now == Stone.Black ? Stone.White : Stone.Black;
        }

        // 王手をみつける
        private int FindCheckmate(Board board, List<int> line, Stone myStone) {
            if (line.Count(x => board[x] == myStone) == 2) {
                var vacants = line.Where(x => board[x] == Stone.Empty);
                if (vacants.Count() == 1)
                    return vacants.First();
            }
            return -1;
        }

        // 最善手を打つ
        private (int, Stone) BestMove(Board board, Stone player, int level = 0) {
            var opponent = NextPlayer(player);
            if (board.IsFin())
                // ここで終わったということは、引き分けしかない。Stone.Emptyは引分けを示す
                return (-1, Stone.Empty);

            var drawix = -1;
            var loseix = -1;
            // 空いている手を一つずつ確かめる
            foreach (var ix in board.GetVacantIndexes()) {
                // ixに石を置いてみる
                board[ix] = player;
                try {
                    var win = board.Judge();
                    if (win == player)
                        // 勝利したのでこれを最善手として戻る
                        return (ix, player);
                    if (win == Stone.Empty) {
                        // まだ終わっていないので、さらに探索 相手も最善手を指すと仮定する
                        var (nextix, winner) = BestMove(board, opponent, level + 1);
                        if (winner == player)
                            // 相手がどこにおいても自分の勝ちなので、これを最善手として戻る
                            return (ix, winner);
                        if (winner == Stone.Empty)
                            // 相手が最善手を指して引分けだった
                            drawix = ix;
                        else
                            // 自分がixに打つと、相手の勝ち
                            loseix = ix;

                    }
                } finally {
                    // 置いた石をixから取り除く
                    board[ix] = Stone.Empty;
                }
            }
            // ここまで来たということは勝ちは無かった。
            if (drawix != -1)
                // 引分けがあったので、引分けの手を最善手として戻る。Stone.Emptyは引分けを示す
                return (drawix, Stone.Empty);
            // 負けてしまった。仕方がないので、負ける手を自分の指す手として戻る
            return (loseix, opponent);
        }

        Random _rnd = new Random();

        // 最初の一手を決める
        private (int, Stone) FirstMove(Stone player, Board board) {

            var indexes = new[] { board.ToIndex(1,1), 
            board.ToIndex(1,3),
            board.ToIndex(3,1),
            board.ToIndex(3,3),
            board.ToIndex(2,2) };
            return (indexes[_rnd.Next(0, indexes.Count())], Stone.Empty);
        }
    }
}

Board.cs

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

namespace TicTacToo {
    public class Stone {
        // 盤面に何も置いていない状態は、EmptyのStoneが置いてあると考える
        public static readonly Stone Black = new Stone { Value = 'X' };
        public static readonly Stone White = new Stone { Value = 'O' };
        public static readonly Stone Empty = new Stone { Value = '.' };

        public char Value { get; private set; }
        public override string ToString() {
            return Value.ToString();
        }
    }

    public class Board : BoardBase<Stone> {
        public Board() : base(3, 3) {
            foreach (var ix in GetAllIndexes())
                this[ix] = Stone.Empty;
        }

        // 石を置けるか
        public bool CanPut(int index) {
            return this[index] == Stone.Empty;
        }

        // 終了しかた?
        public bool IsFin() {
            if (Judge() != Stone.Empty)
                return true;
            return this.GetVacantIndexes().Count() == 0;
        }

        // 空いている場所を列挙する
        public IEnumerable<int> GetVacantIndexes() {
            return GetAllIndexes().Where(x => this[x] == Stone.Empty);
        }

        // 全ての場所を列挙する
        public override IEnumerable<int> GetAllIndexes() {
            for (int y = 1; y <= 3; y++)
                for (int x = 1; x <= 3; x++)
                    yield return ToIndex(x, y);
        }

        // 縦横斜めすべての筋を列挙する
        public IEnumerable<List<int>> GetLineIndexes() {
            // 横
            for (int y = 1; y <= 3; y++) {
                var p = ToIndex(1, y);
                yield return new List<int> { p, p + 1, p + 2 };
            }
            // 縦
            for (int x = 1; x <= 3; x++) {
                var p = ToIndex(x, 1);
                var d = ToDirection(0, 1);
                yield return new List<int> { p, p + d, p + d + d };
            }
            // 斜め 左上から右下
            {
                var p = ToIndex(1, 1);
                var d = ToDirection(1, 1);
                yield return new List<int> { p, p + d, p + d + d };
            }
            // 斜め 右上から左下
            {
                var p = ToIndex(3, 1);
                var d = ToDirection(-1, 1);
                yield return new List<int> { p, p + d, p + d + d };
            }
        }

        // どちらが勝ったか?
        public Stone Judge() {
            foreach (var list in this.GetLineIndexes().ToList()) {

                if (list.All(x => this[x] == Stone.White))
                    return Stone.White;
                if (list.All(x => this[x] == Stone.Black))
                    return Stone.Black;
            }
            return Stone.Empty;
        }
    }
}

BoardBase.cs

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

namespace TicTacToo {
    // 汎用の盤面クラス
    // Tは、盤面に置けるオブジェクトの型。参照型でnew()ができれば何でも良い。
    public abstract class BoardBase<T> where T : class, new() {
        private T[] _pieces;

        // 盤の行数(縦方向)
        public int YSize { get; private set; }

        // 盤のカラム数(横方向)
        public int XSize { get; private set; }

        // 番兵も含めた幅のサイズ
        private int OuterWidth => XSize + 4;

        private int OuterHeight => XSize + 4;

        // コンストラクタ
        public BoardBase(int xsize, int ysize) {
            this.YSize = ysize;
            this.XSize = xsize;

            _pieces = new T[OuterWidth * OuterHeight];

            // 盤データの初期化 - 盤の周りはnull(番兵)をセットしておく
            ClearAll();
        }

        // コピー用コンストラクタ
        public BoardBase(BoardBase<T> board) {
            XSize = board.XSize;
            YSize = board.YSize;
            this._pieces = board._pieces.ToArray();
        }

        // 番兵も含めたボード配列の長さ
        public int BoardLength {
            get { return _pieces.Length; }
        }

        // インデクサ (x,y)の位置の要素へアクセスする
        public T this[int index] {
            get { return _pieces[index]; }
            set { _pieces[index] = value; }
        }

        // インデクサ (x,y)の位置の要素へアクセスする
        public T this[int x, int y] {
            get { return this[ToIndex(x, y)]; }
            set { this[ToIndex(x, y)] = value; }
        }

        // Location から _coinのIndexを求める
        public int ToIndex(int x, int y) => x + 1 + (y + 1) * OuterWidth;

        // IndexからLocationを求める (ToIndexの逆演算)
        public (int, int) ToLocation(int index) => (index % OuterWidth - 1, index / OuterWidth - 1);


        public int ToDirection(int dx, int dy) => dy * OuterWidth + dx;

        // 本来のボード上の位置(index)かどうかを調べる
        public virtual bool IsOnBoard(int index) {
            //if (0 <= index && index < BoardLength)
            return this[index] != null;
            //return false;
        }

        // 全てのPieceをクリアする
        public virtual void ClearAll() {
            for (int index = 0; index < BoardLength; index++)
                this[index] = null;       // 番兵
            foreach (var index in GetAllIndexes())
                this[index] = new T();  // 初期値
        }

        // 盤上のすべての位置(index)を列挙する
        public virtual IEnumerable<int> GetAllIndexes() {
            for (int y = 1; y <= this.YSize; y++) {
                for (int x = 1; x <= this.XSize; x++) {
                    yield return ToIndex(x, y);
                }
            }
        }

        // (x,y)からdirection方向の位置を列挙する (x,y)含む
        public virtual IEnumerable<int> EnumerateIndexes(int x, int y, int direction) {
            for (int index = ToIndex(x, y); IsOnBoard(index); index += direction)
                yield return index;
        }

        // (x,y)から右(水平)の位置を列挙する (x,y)含む
        public virtual IEnumerable<int> Horizontal(int x, int y)
            => EnumerateIndexes(x, y, ToDirection(1, 0));

        // (x,y)から下(垂直)の位置を列挙する (x,y)含む
        public virtual IEnumerable<int> Virtical(int x, int y)
            => EnumerateIndexes(x, y, ToDirection(0, 1));

        // (x,y)から右斜め下(45度)の位置を列挙する (x,y)含む
        public virtual IEnumerable<int> SlantR(int x, int y)
            => EnumerateIndexes(x, y, ToDirection(1, 1));

        // (x,y)から左斜め下(45度)の位置を列挙する (x,y)含む
        public virtual IEnumerable<int> SlantL(int x, int y)
            => EnumerateIndexes(x, y, ToDirection(-1, 1));

    }
}
6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7