まるばつゲーム


誰でも知っている「まるばつゲーム」を作ってみたいと思います。2人対戦専用です。マウス操作です。



画像リソースのダウンロード

ここから画像リソースをダウンロードし、zipファイルを解凍しておいてください。
https://github.com/ymotoyama/Samples/raw/master/MaruBatsu%20Resources.zip



新規プロジェクト作成

Visual Studioから新規プロジェクト(MonoGame Windows Project)を作成します。
本記事のサンプルコードでは、プロジェクト名を「MaruBatsu」にしました。

プロジェクトを作成したら、ダウンロードした画像をMonoGamePipeline Toolで登録してビルドしておいてください。

盤面を表示する

ゲームに必要な画像を使うための変数の定義と、画像の読み込み処理を書きます。
そして、grid.pngを並べて、3×3の盤面を表示しましょう。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MaruBatsu
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // マス目の開始位置(左上の座標)
        static readonly Vector2 BoardOrigin = new Vector2(100, 100);
        // マスの大きさ(ピクセル数)
        static readonly int GridSize = 64;
        // 盤面の大きさ(マスの数)
        static readonly int BoardSize = 3;
     
        Texture2D textureGrid; // マスの画像      
        Texture2D textureMaru; // まるの画像      
        Texture2D textureBatsu; // ばつの画像      
        Texture2D textureNobandesu; // 「の番です」の画像      
        Texture2D textureNokachidesu; // 「の勝ちです」の画像      
        Texture2D textureHikiwake; // 「引き分け」の画像

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // 画像の読み込み
            textureGrid = Content.Load<Texture2D>("grid");
            textureMaru = Content.Load<Texture2D>("maru");
            textureBatsu = Content.Load<Texture2D>("batsu");
            textureNobandesu = Content.Load<Texture2D>("nobandesu");
            textureNokachidesu = Content.Load<Texture2D>("nokachidesu");
            textureHikiwake = Content.Load<Texture2D>("hikiwake");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            // SpriteBatchで描画するために必要
            spriteBatch.Begin();

            // 盤面の描画
            // マスを左上から右へ右へと描画し、一行終わったら、
            // 一つ下がってまた左から右へ右へと描画…
            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    spriteBatch.Draw(textureGrid, new Vector2(GridSize * x, GridSize * y) + BoardOrigin, Color.White);
                }
            }

            // SpriteBatchで描画するために必要
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
3×3の盤面が表示されます。

盤面の位置は定数BoardOriginによって決まっています。変えても大丈夫です。

マウス位置をマス番号に変換する

マウスでマス目を指定できるようにしていきます。
まず、マウスカーソルが表示されるようにします。

マウスカーソルが教えてくれるのはカーソルの座標(ピクセル)なので、そのままでは、どのマスが選択されているのかわかりません。座標をマス番号(配列のインデックス)に変換する必要があります。
今回はマスがBoardOrigin分だけズレて表示されているため、まずカーソルの座標からBoardOriginを引いてあげると、計算が楽になります。そして、その値をGridSize(1マスのピクセル数)で割ってあげると、マス番号がわかります。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MaruBatsu
{
    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // マス目の開始位置(左上の座標)
        static readonly Vector2 BoardOrigin = new Vector2(100, 100);
        // マスの大きさ(ピクセル数)
        static readonly int GridSize = 64;
        // 盤面の大きさ(マスの数)
        static readonly int BoardSize = 3;
        
        Texture2D textureGrid; // マスの画像        
        Texture2D textureMaru; // まるの画像        
        Texture2D textureBatsu; // ばつの画像        
        Texture2D textureNobandesu; // 「の番です」の画像        
        Texture2D textureNokachidesu; // 「の勝ちです」の画像        
        Texture2D textureHikiwake; // 「引き分け」の画像

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // マウスカーソルを表示する
            IsMouseVisible = true;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // 画像の読み込み
            textureGrid = Content.Load<Texture2D>("grid");
            textureMaru = Content.Load<Texture2D>("maru");
            textureBatsu = Content.Load<Texture2D>("batsu");
            textureNobandesu = Content.Load<Texture2D>("nobandesu");
            textureNokachidesu = Content.Load<Texture2D>("nokachidesu");
            textureHikiwake = Content.Load<Texture2D>("hikiwake");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            // マウスの状態を取得
            MouseState mouse = Mouse.GetState();

            // マウスカーソルが盤面の範囲外か?範囲内か?
            if (mouse.X < BoardOrigin.X
                || mouse.X >= BoardOrigin.X + GridSize * BoardSize
                || mouse.Y < BoardOrigin.Y
                || mouse.Y >= BoardOrigin.Y + GridSize * BoardSize)
            {
                System.Console.WriteLine("マウスは盤面の範囲外");
            }
            else
            {
                // マウスカーソルの座標をマス目の番号に変換する
                int x = (int)(mouse.X - BoardOrigin.X) / GridSize;
                int y = (int)(mouse.Y - BoardOrigin.Y) / GridSize;

                System.Console.WriteLine("x=" + x + ", y=" + y);
           }

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            // SpriteBatchで描画するために必要
            spriteBatch.Begin();

            // 盤面の描画
            // マスを左上から右へ右へと描画し、一行終わったら、
            // 一つ下がってまた左から右へ右へと描画…
            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    spriteBatch.Draw(textureGrid, new Vector2(GridSize * x, GridSize * y) + BoardOrigin, Color.White);
                }
            }

            // SpriteBatchで描画するために必要
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
 F5で実行し、出力ウィンドウを確認してください。
※Ctrl+F5だとログが出ません。


ゲームのメインロジックの実装

下準備は終わりました。いよいよゲームのメインロジックを実装していきます。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MaruBatsu
{
    // ゲームの状態種別
    enum State
    {
        MaruTurn,  // まるの番
        BatsuTurn, // ばつの番
        MaruWin,   // まるの勝ち
        BatsuWin,  // ばつの勝ち
        Hikiwake,  // 引き分け
    }

    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // マス目の開始位置(左上の座標)
        static readonly Vector2 BoardOrigin = new Vector2(100, 100);
        // マスの大きさ(ピクセル数)
        static readonly int GridSize = 64;
        // 盤面の大きさ(マスの数)
        static readonly int BoardSize = 3;

        // まだ何も置いてない場所
        static readonly int Empty = 0;
        // まるが置かれた場所
        static readonly int Maru = 1;
        // ばつが置かれた場所
        static readonly int Batsu = 2;

        Texture2D textureGrid; // マスの画像        
        Texture2D textureMaru; // まるの画像        
        Texture2D textureBatsu; // ばつの画像        
        Texture2D textureNobandesu; // 「の番です」の画像        
        Texture2D textureNokachidesu; // 「の勝ちです」の画像        
        Texture2D textureHikiwake; // 「引き分け」の画像

        // 盤面の状況を表す2次元配列
        int[,] board = new int[BoardSize, BoardSize];

        // 1フレーム前のマウスの状態
        MouseState prevMouse;

        // 現在の状況
        State state = State.MaruTurn;

        // 置いた個数(まるとばつの合計)。引き分けの判定に使う
        int count = 0;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // マウスカーソルを表示する
            IsMouseVisible = true;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            // 盤面の初期化
            InitBoard();

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // 画像の読み込み
            textureGrid = Content.Load<Texture2D>("grid");
            textureMaru = Content.Load<Texture2D>("maru");
            textureBatsu = Content.Load<Texture2D>("batsu");
            textureNobandesu = Content.Load<Texture2D>("nobandesu");
            textureNokachidesu = Content.Load<Texture2D>("nokachidesu");
            textureHikiwake = Content.Load<Texture2D>("hikiwake");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            // マウスの状態を取得
            MouseState mouse = Mouse.GetState();

            // マウスの左ボタンが前フレームでは押されていたのに今回は離されている→クリック
            if (prevMouse.LeftButton == ButtonState.Pressed && mouse.LeftButton == ButtonState.Released)
            {
                OnClick(mouse);
            }

            // 次のフレームで使うために、今回のマウスの状態を保持しておく
            prevMouse = mouse;

            base.Update(gameTime);
        }

        // 盤面の初期化。全てのマスを空で埋める
        void InitBoard()
        {
            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    board[x, y] = Empty;
                }
            }
        }


        // クリックされた時の処理
        void OnClick(MouseState mouse)
        {
            // マルの番でもバツの番でもないときは何もしない
            if (state != State.MaruTurn && state != State.BatsuTurn)
            {
                return;
            }

            // マウス座標が盤面に収まっていない場合は何もしない
            if (mouse.X < BoardOrigin.X
                || mouse.X >= BoardOrigin.X + GridSize * BoardSize
                || mouse.Y < BoardOrigin.Y
                || mouse.Y >= BoardOrigin.Y + GridSize * BoardSize)
            {
                return;
            }

            // マウス座標をマス目番号に変換する
            int x = (int)(mouse.X - BoardOrigin.X) / GridSize;
            int y = (int)(mouse.Y - BoardOrigin.Y) / GridSize;

            // クリックされた場所が空でない場合は何もしない
            if (board[x, y] != Empty)
            {
                return;
            }

            // クリックされたマスに記号を置く
            if (state == State.MaruTurn)
            {
                board[x, y] = Maru;
            }
            else
            {
                board[x, y] = Batsu;
            }

            count++;

            // もし揃ったら
            if (IsMatching())
            {
                if (state == State.MaruTurn)
                {
                    state = State.MaruWin;
                }
                else
                {
                    state = State.BatsuWin;
                }
            }
            // 全てのマスが埋まったら引き分け
            else if (count == BoardSize * BoardSize)
            {
                state = State.Hikiwake;
            }
            // 相手の番へ
            else
            {
                if (state == State.MaruTurn)
                {
                    state = State.BatsuTurn;
                }
                else
                {
                    state = State.MaruTurn;
                }
            }
        }

        // 揃ったか? 揃ってたらtrue, 揃ってなければfalseを返却する
        bool IsMatching()
        {
            return
                (board[0, 0] != Empty && board[0, 0] == board[0, 1] && board[0, 0] == board[0, 2]) ||
                (board[1, 0] != Empty && board[1, 0] == board[1, 1] && board[1, 0] == board[1, 2]) ||
                (board[2, 0] != Empty && board[2, 0] == board[2, 1] && board[2, 0] == board[2, 2]) ||
                (board[0, 0] != Empty && board[0, 0] == board[1, 0] && board[0, 0] == board[2, 0]) ||
                (board[0, 1] != Empty && board[0, 1] == board[1, 1] && board[0, 1] == board[2, 1]) ||
                (board[0, 2] != Empty && board[0, 2] == board[1, 2] && board[0, 2] == board[2, 2]) ||
                (board[0, 0] != Empty && board[0, 0] == board[1, 1] && board[0, 0] == board[2, 2]) ||
                (board[2, 0] != Empty && board[2, 0] == board[1, 1] && board[2, 0] == board[0, 2]);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            // SpriteBatchで描画するために必要
            spriteBatch.Begin();

            // まるの番なら
            if (state == State.MaruTurn)
            {
                // ○の番です と表示
                spriteBatch.Draw(textureMaru, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(textureNobandesu, new Vector2(64, 0), Color.White);
            }
            // ばつの番なら
            else if (state == State.BatsuTurn)
            {
                // ×の番です と表示
                spriteBatch.Draw(textureBatsu, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(textureNobandesu, new Vector2(64, 0), Color.White);
            }
            // まるの勝ちなら
            else if (state == State.MaruWin)
            {
                // ○の勝ちです と表示
                spriteBatch.Draw(textureMaru, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(textureNokachidesu, new Vector2(64, 0), Color.White);
            }
            // ばつの勝ちなら
            else if (state == State.BatsuWin)
            {
                // ×の勝ちです と表示
                spriteBatch.Draw(textureBatsu, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(textureNokachidesu, new Vector2(64, 0), Color.White);
            }
            // 引き分けなら
            else if (state == State.Hikiwake)
            {
                // 引き分け! と表示
                spriteBatch.Draw(textureHikiwake, new Vector2(0, 0), Color.White);
            }

            // 盤面の描画
            // マスを左上から右へ右へと描画し、一行終わったら、
            // 一つ下がってまた左から右へ右へと描画…
            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    spriteBatch.Draw(textureGrid, new Vector2(GridSize * x, GridSize * y) + BoardOrigin, Color.White);
                }
            }

            // 盤面に置かれた記号の描画
            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    int data = board[x, y];

                    if (data == Empty)
                    {
                        // 何も描画しない
                    }
                    else if (data == Maru)
                    {
                        // まるを描画
                        spriteBatch.Draw(textureMaru, new Vector2(GridSize * x, GridSize * y) + BoardOrigin, Color.White);
                    }
                    else if (data == Batsu)
                    {
                        spriteBatch.Draw(textureBatsu, new Vector2(GridSize * x, GridSize * y) + BoardOrigin, Color.White);
                    }
                }
            }

            // SpriteBatchで描画するために必要
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
たかがまるばつゲーム…されどまるばつゲーム……けっこう長くなってしまいました。

盤面の状況(どのマスに何が置かれたのか)はint型の二次元配列boardで管理しています。配列の要素番号と盤面のマスの位置の関係は次のとおりです。


配列の各要素の中身が0なら空っぽ、1ならマル、2ならバツを意味しています。
プログラム中に0, 1, 2と直接書くとわかりづらいので、定数を定義して、それを使っています(Empty, Maru, Batsu)。

countという変数では、○と×合わせて盤面に何個置かれたのかをカウントしています。無くても良いのですが、そうすると、引き分けの判定が面倒になるので、数を数えています。

クリック時の処理はOnClickという別のメソッドに分けています。別に分けなくても良かったのですが、長くなってしまったので、分けました。
長い処理は、ある程度のまとまりで別のメソッドに分けると、見た目がスッキリします。また、メソッドにすると、途中returnが使えて、if文のネストが深くならないので私は好きです。

○か×が一直線に揃ったかどうかの判定はIsMatchingメソッドで行っています。
今回は盤面が3×3マスと狭いので、起こり得る全パターンを調べるという脳筋プレイで判定しています。



盤面を大きくしてみる

現在、盤面の大きさは3×3マスですが、これをもっと大きくしてみたいと思います。
定数BoardSizeを変えるだけで、表示上はすぐに変更できます。
↑BoardSizeを5にした例です。
表示はOKですが、勝敗の判定が全く駄目です。それも当然で、現状の判定処理は、3×3マスを前提にして作っているためです。

盤面のサイズを大きくすると、全パターンをチェックすることはさすがに不可能なので、判定処理が難しくなります。
…といっても、工夫すればさほど苦労せず実装できます。

今回は、5×5の盤面上で、相変わらず3マス揃えたほうが勝ちとしましょう。

判定方法は色々ありますが、今回はプログラムが楽な方法で実現します。
ある1マスを中心に、縦・横・斜めのいずれかで3マス揃っているかを調べるのは簡単ですよね?
4回調べるだけなので(縦と横と斜め2方向)。
そういう判定処理を作ったら、それを全てのマスに対して実行すれば良いのです。
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace MaruBatsu
{
    enum State
    {
        MaruTurn,  // まるの番
        BatsuTurn, // ばつの番
        MaruWin,   // まるの勝ち
        BatsuWin,  // ばつの勝ち
        Hikiwake,  // 引き分け
    }

    /// <summary>
    /// This is the main type for your game.
    /// </summary>
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // マス目の開始位置(左上の座標)
        static readonly Vector2 BoardOrigin = new Vector2(100, 100);
        // マスの大きさ(ピクセル数)
        static readonly int GridSize = 64;
        // 盤面の大きさ(マスの数)
        static readonly int BoardSize = 5;

        // まだ何も置いてない場所
        static readonly int Empty = 0;
        // まるが置かれた場所
        static readonly int Maru = 1;
        // ばつが置かれた場所
        static readonly int Batsu = 2;

        Texture2D textureGrid; // マスの画像        
        Texture2D textureMaru; // まるの画像        
        Texture2D textureBatsu; // ばつの画像        
        Texture2D textureNobandesu; // 「の番です」の画像        
        Texture2D textureNokachidesu; // 「の勝ちです」の画像        
        Texture2D textureHikiwake; // 「引き分け」の画像

        // 盤面の状況を表す2次元配列
        int[,] board = new int[BoardSize, BoardSize];

        // 1フレーム前のマウスの状態
        MouseState prevMouse;

        // 現在の状況
        State state = State.MaruTurn;

        // 置いた個数(まるとばつの合計)
        int count = 0;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // マウスカーソルを表示する
            IsMouseVisible = true;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            // 盤面の初期化
            InitBoard();

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here

            // 画像の読み込み
            textureGrid = Content.Load<Texture2D>("grid");
            textureMaru = Content.Load<Texture2D>("maru");
            textureBatsu = Content.Load<Texture2D>("batsu");
            textureNobandesu = Content.Load<Texture2D>("nobandesu");
            textureNokachidesu = Content.Load<Texture2D>("nokachidesu");
            textureHikiwake = Content.Load<Texture2D>("hikiwake");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// game-specific content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            // マウスの状態を取得
            MouseState mouse = Mouse.GetState();

            // マウスの左ボタンが前フレームでは押されていたのに今回は離されている→クリック
            if (prevMouse.LeftButton == ButtonState.Pressed && mouse.LeftButton == ButtonState.Released)
            {
                OnClick(mouse);
            }

            // 次のフレームで使うために、今回のマウスの状態を保持しておく
            prevMouse = mouse;

            base.Update(gameTime);
        }

        // 盤面の初期化。全てのマスを空で埋める
        void InitBoard()
        {
            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    board[x, y] = Empty;
                }
            }
        }


        // クリックされた時の処理
        void OnClick(MouseState mouse)
        {
            // マルの番でもバツの番でもないときは何もしない
            if (state != State.MaruTurn && state != State.BatsuTurn)
            {
                return;
            }

            // マウス座標が盤面に収まっていない場合は何もしない
            if (mouse.X < BoardOrigin.X
                || mouse.X >= BoardOrigin.X + GridSize * BoardSize
                || mouse.Y < BoardOrigin.Y
                || mouse.Y >= BoardOrigin.Y + GridSize * BoardSize)
            {
                return;
            }

            // マウス座標をマス目番号に変換する
            int x = (int)(mouse.X - BoardOrigin.X) / GridSize;
            int y = (int)(mouse.Y - BoardOrigin.Y) / GridSize;

            // クリックされた場所が空でない場合は何もしない
            if (board[x, y] != Empty)
            {
                return;
            }

            // クリックされたマスに記号を置く
            if (state == State.MaruTurn)
            {
                board[x, y] = Maru;
            }
            else
            {
                board[x, y] = Batsu;
            }

            count++;

            // もし揃ったら
            if (IsMatching())
            {
                if (state == State.MaruTurn)
                {
                    state = State.MaruWin;
                }
                else
                {
                    state = State.BatsuWin;
                }
            }
            // 全てのマスが埋まったら引き分け
            else if (count == BoardSize * BoardSize)
            {
                state = State.Hikiwake;
            }
            // 相手の番へ
            else
            {
                if (state == State.MaruTurn)
                {
                    state = State.BatsuTurn;
                }
                else
                {
                    state = State.MaruTurn;
                }
            }
        }

        // 揃ったか?
        bool IsMatching()
        {
            // 今回は、若干の無駄はあるが、最もプログラムが楽な方法で調べる。
            // 指定した1マスを中心に、縦・横・斜めのいずれかで3マス揃っているかを調べる。
            // これを全てのマスに対して行う。

            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    if (IsMatching3(x, y))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        // 指定されたマスを中心に、縦・横・斜めいずれかで3マス揃っているか?
        bool IsMatching3(int x, int y)
        {
            // 指定されたマスが空なら、揃ってるわけないのでfalse
            if (board[x, y] == Empty)
                return false;

            // 横方向に揃っているか?
            if (x > 0 && x < BoardSize - 1 && board[x, y] == board[x - 1, y] && board[x, y] == board[x + 1, y])
                return true;

            // 縦方向に揃っているか?
            if (y > 0 && y < BoardSize - 1 && board[x, y] == board[x, y - 1] && board[x, y] == board[x, y + 1])
                return true;

            // 斜め(左上から右下にかけて)に揃っているか?
            if (x > 0 && x < BoardSize - 1 && y > 0 && y < BoardSize - 1 && board[x, y] == board[x - 1, y - 1] && board[x, y] == board[x + 1, y + 1])
                return true;

            // 斜め(左下から右上にかけて)に揃っているか?
            if (x > 0 && x < BoardSize - 1 && y > 0 && y < BoardSize - 1 && board[x, y] == board[x - 1, y + 1] && board[x, y] == board[x + 1, y - 1])
                return true;

            return false;
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            // SpriteBatchで描画するために必要
            spriteBatch.Begin();

            // まるの番なら
            if (state == State.MaruTurn)
            {
                // ○の番です と表示
                spriteBatch.Draw(textureMaru, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(textureNobandesu, new Vector2(64, 0), Color.White);
            }
            // ばつの番なら
            else if (state == State.BatsuTurn)
            {
                // ×の番です と表示
                spriteBatch.Draw(textureBatsu, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(textureNobandesu, new Vector2(64, 0), Color.White);
            }
            // まるの勝ちなら
            else if (state == State.MaruWin)
            {
                // ○の勝ちです と表示
                spriteBatch.Draw(textureMaru, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(textureNokachidesu, new Vector2(64, 0), Color.White);
            }
            // ばつの勝ちなら
            else if (state == State.BatsuWin)
            {
                // ×の勝ちです と表示
                spriteBatch.Draw(textureBatsu, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(textureNokachidesu, new Vector2(64, 0), Color.White);
            }
            // 引き分けなら
            else if (state == State.Hikiwake)
            {
                // 引き分け! と表示
                spriteBatch.Draw(textureHikiwake, new Vector2(0, 0), Color.White);
            }

            // 盤面の描画
            // マスを左上から右へ右へと描画し、一行終わったら、
            // 一つ下がってまた左から右へ右へと描画…
            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    spriteBatch.Draw(textureGrid, new Vector2(GridSize * x, GridSize * y) + BoardOrigin, Color.White);
                }
            }

            // 盤面に置かれた記号の描画
            for (int y = 0; y < BoardSize; y++)
            {
                for (int x = 0; x < BoardSize; x++)
                {
                    int data = board[x, y];

                    if (data == Empty)
                    {
                        // 何も描画しない
                    }
                    else if (data == Maru)
                    {
                        // まるを描画
                        spriteBatch.Draw(textureMaru, new Vector2(GridSize * x, GridSize * y) + BoardOrigin, Color.White);
                    }
                    else if (data == Batsu)
                    {
                        spriteBatch.Draw(textureBatsu, new Vector2(GridSize * x, GridSize * y) + BoardOrigin, Color.White);
                    }
                }
            }

            // SpriteBatchで描画するために必要
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
 IsMatching3メソッド内の判定処理
// 横方向に揃っているか?
if (x > 0 && x < BoardSize - 1 && board[x, y] == board[x - 1, y] && board[x, y] == board[x + 1, y])
    return true;
の部分がややわかりづらいかもしれません。
x > 0 というのは、xが1以上ということですね。 つまり、xが0のときにはこの判定を行いたくないのです。何故かというと、x=0のマス(左端のマス)を中心にして、横方向に3つ揃っているわけがないので。左端のマスのさらに左隣のマスの内容を調べようとすると、配列外参照でエラーになります。
x < BoardSize - 1 も同様に、右端のマスのさらに右のマスを調べないようにするための判定です。

これで、5×5マスでも正しく判定が行われるようになりました!



……プログラム的には完成しましたが……

…これ、先手必勝ですね😅

是非、プログラムを改造して、五目並べとかにしてみてください!
(実は五目並べも先手必勝らしいですが…)

0 件のコメント:

コメントを投稿