トレーニング | 三目並べ

2 人で遊ぶ三目並べを作ります。

1. 格子を表示する

# include <Siv3D.hpp>

class GameBoard
{
private:

    // 格子を描く
    void drawGridLines() const
    {
        // 線を引く
        for (auto i : { 1, 2 })
        {
            Line(i * CellSize, 0, i * CellSize, 3 * CellSize)
                .draw(4, ColorF(0.25));

            Line(0, i * CellSize, 3 * CellSize, i * CellSize)
                .draw(4, ColorF(0.25));
        }
    }

public:

    // セルの大きさ
    static constexpr int32 CellSize = 150;

    // 描画
    void draw() const
    {
        drawGridLines();
    }
};

void Main()
{
    // 背景色
    Scene::SetBackground(ColorF(0.8, 1.0, 0.9));

    GameBoard gameBoard;

    while (System::Update())
    {
        gameBoard.draw();
    }
}

2. マウスオーバーしたセルを強調表示

# include <Siv3D.hpp>

class GameBoard
{
private:

    // 格子を描く
    void drawGridLines() const
    {
        // 線を引く
        for (auto i : { 1, 2 })
        {
            Line(i * CellSize, 0, i * CellSize, 3 * CellSize)
                .draw(4, ColorF(0.25));

            Line(0, i * CellSize, 3 * CellSize, i * CellSize)
                .draw(4, ColorF(0.25));
        }
    }

    // セルを描く
    void drawCells() const
    {
        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルがマウスオーバーされたら
            if (cell.mouseOver())
            {
                // カーソルを手のアイコンに
                Cursor::RequestStyle(CursorStyle::Hand);

                // セルの上に半透明の白を描く
                cell.stretched(-2).draw(ColorF(1.0, 0.6));
            }
        }
    }

public:

    // セルの大きさ
    static constexpr int32 CellSize = 150;

    // 描画
    void draw() const
    {
        drawGridLines();

        drawCells();
    }
};

void Main()
{
    // 背景色
    Scene::SetBackground(ColorF(0.8, 1.0, 0.9));

    GameBoard gameBoard;

    while (System::Update())
    {
        gameBoard.draw();
    }
}

3. 〇 と × を表示

セルに書き込まれたマークを Grid<int32> で管理し、書き込まれたセルはマークを表示します。

# include <Siv3D.hpp>

class GameBoard
{
private:

    // 3x3 の二次元配列 (初期値は全要素 0)
    Grid<int32> m_grid = Grid<int32>(3, 3);

    // 格子を描く
    void drawGridLines() const
    {
        // 線を引く
        for (auto i : { 1, 2 })
        {
            Line(i * CellSize, 0, i * CellSize, 3 * CellSize)
                .draw(4, ColorF(0.25));

            Line(0, i * CellSize, 3 * CellSize, i * CellSize)
                .draw(4, ColorF(0.25));
        }
    }

    // セルを描く
    void drawCells() const
    {
        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // X マークだったら
            if (mark == X_Mark)
            {
                // X マークを描く
                Shape2D::Cross(CellSize * 0.4, 10, cell.center())
                    .draw(ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }
            else if (mark == O_Mark) // O マークだったら
            {
                // 〇 マークを描く
                Circle(cell.center(), CellSize * 0.4 - 10)
                    .drawFrame(10, 0, ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }

            // セルがマウスオーバーされたら
            if (cell.mouseOver())
            {
                // カーソルを手のアイコンに
                Cursor::RequestStyle(CursorStyle::Hand);

                // セルの上に半透明の白を描く
                cell.stretched(-2).draw(ColorF(1.0, 0.6));
            }
        }
    }

public:

    // セルの大きさ
    static constexpr int32 CellSize = 150;

    // O マークの値
    static constexpr int32 O_Mark = 1;

    // X マークの値
    static constexpr int32 X_Mark = 2;

    GameBoard()
    {
        // 表示テスト用にマークを書き込む
        m_grid[{0, 0}] = O_Mark;
        m_grid[{1, 0}] = X_Mark;
    }

    // 描画
    void draw() const
    {
        drawGridLines();

        drawCells();
    }
};

void Main()
{
    // 背景色
    Scene::SetBackground(ColorF(0.8, 1.0, 0.9));

    GameBoard gameBoard;

    while (System::Update())
    {
        gameBoard.draw();
    }
}

4. クリックでマークを置けるようにする

空白のセルをクリックしたときに、交互に 〇 と × のマークを書き込めるようにします。

# include <Siv3D.hpp>

class GameBoard
{
private:

    // 3x3 の二次元配列 (初期値は全要素 0)
    Grid<int32> m_grid = Grid<int32>(3, 3);

    // これから置くマーク
    int32 m_currentMark = O_Mark;

    // 格子を描く
    void drawGridLines() const
    {
        // 線を引く
        for (auto i : { 1, 2 })
        {
            Line(i * CellSize, 0, i * CellSize, 3 * CellSize)
                .draw(4, ColorF(0.25));

            Line(0, i * CellSize, 3 * CellSize, i * CellSize)
                .draw(4, ColorF(0.25));
        }
    }

    // セルを描く
    void drawCells() const
    {
        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // X マークだったら
            if (mark == X_Mark)
            {
                // X マークを描く
                Shape2D::Cross(CellSize * 0.4, 10, cell.center())
                    .draw(ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }
            else if (mark == O_Mark) // O マークだったら
            {
                // 〇 マークを描く
                Circle(cell.center(), CellSize * 0.4 - 10)
                    .drawFrame(10, 0, ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }

            // セルがマウスオーバーされたら
            if (cell.mouseOver())
            {
                // カーソルを手のアイコンに
                Cursor::RequestStyle(CursorStyle::Hand);

                // セルの上に半透明の白を描く
                cell.stretched(-2).draw(ColorF(1.0, 0.6));
            }
        }
    }

public:

    // セルの大きさ
    static constexpr int32 CellSize = 150;

    // O マークの値
    static constexpr int32 O_Mark = 1;

    // X マークの値
    static constexpr int32 X_Mark = 2;

    void update()
    {
        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // セルが空白で、なおかつクリックされたら
            if ((mark == 0) && cell.leftClicked())
            {
                // セルに現在のマークを書き込む
                m_grid[p] = m_currentMark;

                // 現在のマークを入れ替える
                m_currentMark = ((m_currentMark == O_Mark) ? X_Mark : O_Mark);
            }
        }
    }

    // 描画
    void draw() const
    {
        drawGridLines();

        drawCells();
    }
};

void Main()
{
    // 背景色
    Scene::SetBackground(ColorF(0.8, 1.0, 0.9));

    GameBoard gameBoard;

    while (System::Update())
    {
        gameBoard.update();

        gameBoard.draw();
    }
}

5. ゲーム盤を移動させる

Transofrmer2D で、2D 描画座標とマウスカーソル座標をシフトし、ゲームの盤面を画面の中心に移動させます。

# include <Siv3D.hpp>

class GameBoard
{
private:

    // 3x3 の二次元配列 (初期値は全要素 0)
    Grid<int32> m_grid = Grid<int32>(3, 3);

    // これから置くマーク
    int32 m_currentMark = O_Mark;

    // 格子を描く
    void drawGridLines() const
    {
        // 線を引く
        for (auto i : { 1, 2 })
        {
            Line(i * CellSize, 0, i * CellSize, 3 * CellSize)
                .draw(4, ColorF(0.25));

            Line(0, i * CellSize, 3 * CellSize, i * CellSize)
                .draw(4, ColorF(0.25));
        }
    }

    // セルを描く
    void drawCells() const
    {
        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // X マークだったら
            if (mark == X_Mark)
            {
                // X マークを描く
                Shape2D::Cross(CellSize * 0.4, 10, cell.center())
                    .draw(ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }
            else if (mark == O_Mark) // O マークだったら
            {
                // 〇 マークを描く
                Circle(cell.center(), CellSize * 0.4 - 10)
                    .drawFrame(10, 0, ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }

            // セルがマウスオーバーされたら
            if (cell.mouseOver())
            {
                // カーソルを手のアイコンに
                Cursor::RequestStyle(CursorStyle::Hand);

                // セルの上に半透明の白を描く
                cell.stretched(-2).draw(ColorF(1.0, 0.6));
            }
        }
    }

public:

    // セルの大きさ
    static constexpr int32 CellSize = 150;

    // O マークの値
    static constexpr int32 O_Mark = 1;

    // X マークの値
    static constexpr int32 X_Mark = 2;

    void update()
    {
        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // セルが空白で、なおかつクリックされたら
            if ((mark == 0) && cell.leftClicked())
            {
                // セルに現在のマークを書き込む
                m_grid[p] = m_currentMark;

                // 現在のマークを入れ替える
                m_currentMark = ((m_currentMark == O_Mark) ? X_Mark : O_Mark);
            }
        }
    }

    // 描画
    void draw() const
    {
        drawGridLines();

        drawCells();
    }
};

void Main()
{
    // 背景色
    Scene::SetBackground(ColorF(0.8, 1.0, 0.9));

    constexpr Point offset(175, 30);

    GameBoard gameBoard;

    while (System::Update())
    {
        {
            // 2D 描画とマウスカーソル座標を移動
            Transformer2D tr(Mat3x2::Translate(offset), true);

            gameBoard.update();

            gameBoard.draw();
        }
    }
}

6. ゲーム終了判定

残りの空白セルが 0 になるか、マークがつながったらゲームを終了し、つながったラインを表示します。

# include <Siv3D.hpp>

// マークがつながったかを返す関数
bool CheckLine(const Grid<int32>& grid, const Point& cellA, const Point& cellB, const Point& cellC)
{
    const int32 a = grid[cellA];
    const int32 b = grid[cellB];
    const int32 c = grid[cellC];
    return ((a != 0) && a == b && b == c);
}

// マークがつながったラインの一覧を返す関数
Array<std::pair<Point, Point>> CheckLines(const Grid<int32>& grid)
{
    Array<std::pair<Point, Point>> results;

    // 縦 3 列を調べる
    for (auto x : step(3))
    {
        if (CheckLine(grid, Point(x, 0), Point(x, 1), Point(x, 2)))
        {
            results.emplace_back(Point(x, 0), Point(x, 2));
        }
    }

    // 横 3 行を調べる
    for (auto y : step(3))
    {
        if (CheckLine(grid, Point(0, y), Point(1, y), Point(2, y)))
        {
            results.emplace_back(Point(0, y), Point(2, y));
        }
    }

    // 斜め(左上 -> 右下) を調べる
    if (CheckLine(grid, Point(0, 0), Point(1, 1), Point(2, 2)))
    {
        results.emplace_back(Point(0, 0), Point(2, 2));
    }

    // 斜め(右上 -> 左下) を調べる
    if (CheckLine(grid, Point(2, 0), Point(1, 1), Point(0, 2)))
    {
        results.emplace_back(Point(2, 0), Point(0, 2));
    }

    return results;
}

class GameBoard
{
private:

    // 3x3 の二次元配列 (初期値は全要素 0)
    Grid<int32> m_grid = Grid<int32>(3, 3);

    // これから置くマーク
    int32 m_currentMark = O_Mark;

    // ゲーム終了フラグ
    bool m_gameOver = false;

    // 3 つ連続したラインの一覧
    Array<std::pair<Point, Point>> m_lines;

    // 格子を描く
    void drawGridLines() const
    {
        // 線を引く
        for (auto i : { 1, 2 })
        {
            Line(i * CellSize, 0, i * CellSize, 3 * CellSize)
                .draw(4, ColorF(0.25));

            Line(0, i * CellSize, 3 * CellSize, i * CellSize)
                .draw(4, ColorF(0.25));
        }
    }

    // セルを描く
    void drawCells() const
    {
        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // X マークだったら
            if (mark == X_Mark)
            {
                // X マークを描く
                Shape2D::Cross(CellSize * 0.4, 10, cell.center())
                    .draw(ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }
            else if (mark == O_Mark) // O マークだったら
            {
                // 〇 マークを描く
                Circle(cell.center(), CellSize * 0.4 - 10)
                    .drawFrame(10, 0, ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }

            // セルがマウスオーバーされたら
            if (!m_gameOver && cell.mouseOver())
            {
                // カーソルを手のアイコンに
                Cursor::RequestStyle(CursorStyle::Hand);

                // セルの上に半透明の白を描く
                cell.stretched(-2).draw(ColorF(1.0, 0.6));
            }
        }
    }

    // つながったラインを描く
    void drawResults() const
    {
        for (const auto& line : m_lines)
        {
            // つながったラインの始点と終点のセルを取得
            const Rect cellBegin(line.first * CellSize, CellSize);
            const Rect cellEnd(line.second * CellSize, CellSize);

            // 線を引く
            Line(cellBegin.center(), cellEnd.center())
                .stretched(CellSize * 0.45)
                .draw(LineStyle::RoundCap, 5, ColorF(0.6));
        }
    }

public:

    // セルの大きさ
    static constexpr int32 CellSize = 150;

    // O マークの値
    static constexpr int32 O_Mark = 1;

    // X マークの値
    static constexpr int32 X_Mark = 2;

    void update()
    {
        if (m_gameOver)
        {
            return;
        }

        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // セルが空白で、なおかつクリックされたら
            if ((mark == 0) && cell.leftClicked())
            {
                // セルに現在のマークを書き込む
                m_grid[p] = m_currentMark;

                // 現在のマークを入れ替える
                m_currentMark = ((m_currentMark == O_Mark) ? X_Mark : O_Mark);

                // つながったラインを探す
                m_lines = CheckLines(m_grid);

                // 空白セルが 0 になるか、つながったラインが見つかったら
                if (m_grid.count(0) == 0 || m_lines)
                {
                    // ゲーム終了
                    m_gameOver = true;
                }
            }
        }
    }

    // 描画
    void draw() const
    {
        drawGridLines();

        drawCells();

        drawResults();
    }
};

void Main()
{
    // 背景色
    Scene::SetBackground(ColorF(0.8, 1.0, 0.9));

    constexpr Point offset(175, 30);

    GameBoard gameBoard;

    while (System::Update())
    {
        {
            // 2D 描画とマウスカーソル座標を移動
            Transformer2D tr(Mat3x2::Translate(offset), true);

            gameBoard.update();

            gameBoard.draw();
        }
    }
}

7. ゲーム終了後にリセットできるようにする

ゲームが終了すると「Reset」ボタンが現れ、ゲームをリセットできるようにします。

# include <Siv3D.hpp>

// 3 つのマークがつながったかを返す関数
bool CheckLine(const Grid<int32>& grid, const Point& cellA, const Point& cellB, const Point& cellC)
{
    const int32 a = grid[cellA];
    const int32 b = grid[cellB];
    const int32 c = grid[cellC];
    return ((a != 0) && a == b && b == c);
}

// マークがつながったラインの一覧を返す関数
Array<std::pair<Point, Point>> CheckLines(const Grid<int32>& grid)
{
    Array<std::pair<Point, Point>> results;

    // 縦 3 列を調べる
    for (auto x : step(3))
    {
        if (CheckLine(grid, Point(x, 0), Point(x, 1), Point(x, 2)))
        {
            results.emplace_back(Point(x, 0), Point(x, 2));
        }
    }

    // 横 3 行を調べる
    for (auto y : step(3))
    {
        if (CheckLine(grid, Point(0, y), Point(1, y), Point(2, y)))
        {
            results.emplace_back(Point(0, y), Point(2, y));
        }
    }

    // 斜め(左上 -> 右下) を調べる
    if (CheckLine(grid, Point(0, 0), Point(1, 1), Point(2, 2)))
    {
        results.emplace_back(Point(0, 0), Point(2, 2));
    }

    // 斜め(右上 -> 左下) を調べる
    if (CheckLine(grid, Point(2, 0), Point(1, 1), Point(0, 2)))
    {
        results.emplace_back(Point(2, 0), Point(0, 2));
    }

    return results;
}

class GameBoard
{
private:

    // 3x3 の二次元配列 (初期値は全要素 0)
    Grid<int32> m_grid = Grid<int32>(3, 3);

    // これから置くマーク
    int32 m_currentMark = O_Mark;

    // ゲーム終了フラグ
    bool m_gameOver = false;

    // 3 つ連続したラインの一覧
    Array<std::pair<Point, Point>> m_lines;

    // 格子を描く
    void drawGridLines() const
    {
        // 線を引く
        for (auto i : { 1, 2 })
        {
            Line(i * CellSize, 0, i * CellSize, 3 * CellSize)
                .draw(4, ColorF(0.25));

            Line(0, i * CellSize, 3 * CellSize, i * CellSize)
                .draw(4, ColorF(0.25));
        }
    }

    // セルを描く
    void drawCells() const
    {
        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // X マークだったら
            if (mark == X_Mark)
            {
                // X マークを描く
                Shape2D::Cross(CellSize * 0.4, 10, cell.center())
                    .draw(ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }
            else if (mark == O_Mark) // O マークだったら
            {
                // 〇 マークを描く
                Circle(cell.center(), CellSize * 0.4 - 10)
                    .drawFrame(10, 0, ColorF(0.2));

                // このセルはこれ以上処理しない
                continue;
            }

            // セルがマウスオーバーされたら
            if (!m_gameOver && cell.mouseOver())
            {
                // カーソルを手のアイコンに
                Cursor::RequestStyle(CursorStyle::Hand);

                // セルの上に半透明の白を描く
                cell.stretched(-2).draw(ColorF(1.0, 0.6));
            }
        }
    }

    // つながったラインを描く
    void drawResults() const
    {
        for (const auto& line : m_lines)
        {
            // つながったラインの始点と終点のセルを取得
            const Rect cellBegin(line.first * CellSize, CellSize);
            const Rect cellEnd(line.second * CellSize, CellSize);

            // 線を引く
            Line(cellBegin.center(), cellEnd.center())
                .stretched(CellSize * 0.45)
                .draw(LineStyle::RoundCap, 5, ColorF(0.6));
        }
    }

public:

    // セルの大きさ
    static constexpr int32 CellSize = 150;

    // O マークの値
    static constexpr int32 O_Mark = 1;

    // X マークの値
    static constexpr int32 X_Mark = 2;

    void update()
    {
        if (m_gameOver)
        {
            return;
        }

        // 3x3 のセル
        for (auto p : step(Size(3, 3)))
        {
            // セル
            const Rect cell(p * CellSize, CellSize);

            // セルのマーク
            const int32 mark = m_grid[p];

            // セルが空白で、なおかつクリックされたら
            if ((mark == 0) && cell.leftClicked())
            {
                // セルに現在のマークを書き込む
                m_grid[p] = m_currentMark;

                // 現在のマークを入れ替える
                m_currentMark = ((m_currentMark == O_Mark) ? X_Mark : O_Mark);

                // つながったラインを探す
                m_lines = CheckLines(m_grid);

                // 空白セルが 0 になるか、つながったラインが見つかったら
                if (m_grid.count(0) == 0 || m_lines)
                {
                    // ゲーム終了
                    m_gameOver = true;
                }
            }
        }
    }

    // ゲームをリセット
    void reset()
    {
        m_currentMark = O_Mark;

        m_grid.fill(0);

        m_lines.clear();

        m_gameOver = false;
    }

    // 描画
    void draw() const
    {
        drawGridLines();

        drawCells();

        drawResults();
    }

    // ゲームが終了したかを返す
    bool isGameOver() const
    {
        return m_gameOver;
    }
};

void Main()
{
    // 背景色
    Scene::SetBackground(ColorF(0.8, 1.0, 0.9));

    constexpr Point offset(175, 30);

    GameBoard gameBoard;

    while (System::Update())
    {
        {
            // 2D 描画とマウスカーソル座標を移動
            Transformer2D tr(Mat3x2::Translate(offset), true);

            gameBoard.update();

            gameBoard.draw();
        }

        // ゲームが終了していたら
        if (gameBoard.isGameOver())
        {
            // Reset ボタンを押せばリセット
            if (SimpleGUI::ButtonAt(U"Reset", Vec2(400, 520)))
            {
                gameBoard.reset();
            }
        }
    }
}