小游戏和GUI编程(6) | 基于 SFML 的井字棋

news2024/11/25 0:30:18

小游戏和GUI编程(6) | 基于 SFML 的井字棋

0. 简介

使用 SFML 实现井字棋(tic-tac-toe), 规划如下:

  • 了解规则, 使用命令行实现(已经实现了)
  • 使用 SFML,提供极简的交互(预计 1 小时)
  • 制作 SVG 图像, 美化界面(预计 1 小时)

1. 基于命令行的实现

实现了两个用户 X 和 O 的交互下棋, 判断了输赢、 平局:

  • 有胜负: 每个用户落下棋子后, 检查整个棋盘中的能获胜的 8 个线段上三个点,如果都等于当前落子的值(X或O)那么赢了
  • 没胜负: 如果没有判断出有人赢了, 并且扫描棋盘网格出现了空格, 那么继续下棋; 没有扫描到空格, 说明没法落子了,是平局。
#include <stdio.h>
#include <string.h>

char board[3][3];

void show_board()
{
    for (int i = 0; i < 3; i++)
    {
        if (i > 0) printf("-----\n");
        for (int j = 0; j < 3; j++)
        {
            if (j > 0) printf("|");
            printf("%c", board[i][j]);
        }
        printf("\n");
    }
}

char user = 'X';

enum State {
    PLAYING = 0,
    WIN = 1,
    DRAW = 2
};
State state = PLAYING;
bool played = false;

bool user_play()
{
    printf("[user=%c] please input position ([1-3] [1-3])): ", user);
    int x;
    int y;
    scanf("%d %d", &x, &y);
    if (x < 1 || x > 3 || y < 1 || y > 3)
    {
        printf("invalid position\n");
        return false;
    }
    if (board[x-1][y-1] != ' ')
    {
        printf("invalid position\n");
        return false;
    }
    board[x-1][y-1] = user;
    return true;
}

void update_user()
{
    if (!played) return;
    if (user == 'X')
    {
        user = 'O';
    }
    else
    {
        user = 'X';
    }
}

void judge()
{
    int data[8][6] = {
        {0, 0, 0, 1, 0, 2},
        {1, 0, 1, 1, 1, 2},
        {2, 0, 2, 1, 2, 2},
        {0, 0, 1, 0, 2, 0},
        {0, 1, 1, 1, 2, 1},
        {0, 2, 1, 2, 2, 2},
        {0, 0, 1, 1, 2, 2},
        {0, 2, 1, 1, 2, 0}
    };

    for (int i = 0; i < 8; i++)
    {
        int x0 = data[i][0];
        int y0 = data[i][1];
        int x1 = data[i][2];
        int y1 = data[i][3];
        int x2 = data[i][4];
        int y2 = data[i][5];
        if (board[x0][y0] == user && board[x1][y1] == user && board[x2][y2] == user)
        {
            state = WIN;
            return;
        }
    }

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            if (board[i][j] == ' ')
            {
                return;
            }
        }
    }

    state = DRAW;
}

int main()
{
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            board[i][j] = ' ';
        }
    }

    while (true)
    {
        show_board();
        judge();
        if (state != PLAYING) break;
        update_user();
        played = user_play();
    }

    if (state == WIN)
    {
        printf("Game end, user %c win\n", user);
    }
    else if (state == DRAW)
    {
        printf("Game end, draw\n");
    }
    return 0;
}

2. 基于 SFML 的极简实现

所谓极简是说, 先不考虑美观性, 实现鼠标交互落子即可。

规划:

  • 2.1 绘制窗口
  • 2.2 鼠标点击后绘制单个棋子
  • 2.3 绘制棋盘网格
  • 2.4 轮流绘制棋子
  • 2.5 显示局面信息

2.1 绘制窗口

#include <SFML/Graphics.hpp>

int main()
{
    constexpr int win_width = 500;
    constexpr int win_height = 500;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);

    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }
        }

        window.clear();

        // draw everything here...

        window.display();
    }

    return 0;
}

2.2 鼠标点击后绘制单个棋子

拆解为两部分: 获取到鼠标点击的位置; 根据位置绘制棋子。

根据位置绘制棋子
先直接绘制棋子 ‘X’. 可以使用 sf::RectangleShape 创建线段对象, 它具有宽度, 旋转它45° 和-45° 可以得到交叉效果, 比较麻烦的地方在于, 需要复杂的计算才能确保交叉点是中心点。

另一个思路是使用 sf::Vertext, 通过在两个 vertex 之间用 sf::Lines 类型执行渲染, 省去了计算, 缺点是线段的宽度很窄。 不过够用了。

window.clear(sf::Color::White);

// draw everything here...
// A    B
// +----+
// |    |
// +----+
// C    D
sf::Vector2f A(100, 100);
sf::Vector2f D(150, 150);

// draw line AD
sf::Vertex vertex[2];
vertex[0].position = A;
vertex[0].color  = sf::Color::Blue;
vertex[1].position = D;
vertex[1].color = sf::Color::Blue;
window.draw(vertex, 2, sf::Lines);

// draw line BC
sf::Vector2f B(150, 100);
sf::Vector2f C(100, 150);
vertex[0].position = B;
vertex[0].color  = sf::Color::Blue;
vertex[1].position = C;
vertex[1].color = sf::Color::Blue;
window.draw(vertex, 2, sf::Lines);

window.display();

在这里插入图片描述

获取鼠标位置

以获取到的鼠标点击位置为中心, 上下左右各自扩展 50 个像素, 得到的 ABCD 区域里面, 绘制 ‘X’.

获取鼠标点击位置, 是在 sfml 教程的 window - keyboard, mouse event 里:

    sf::Vector2i localPosition(-1, -1);
    while (window.pollEvent(event))
    {
        if (event.type == sf::Event::Closed) { window.close(); }

        if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
        {
            // get the local mouse position (relative to a window)
            localPosition = sf::Mouse::getPosition(window);
        }
    }

为了避免屏幕闪烁, 如果当前帧没有获取到新的鼠标位置, 那么 A,B,C,D 四个点的坐标不变,仍然执行渲染和绘制; 如果鼠标点击了, 才更新 A,B,C,D。 效果:

在这里插入图片描述

对应代码:

#include <SFML/Graphics.hpp>
#include <iostream>

int main()
{
    constexpr int win_width = 500;
    constexpr int win_height = 500;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);

    sf::Vector2f A, B, C, D;
    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear();

        constexpr int grid_len = 50;
        
        if (localPosition != sf::Vector2i(-1, -1))
        {
            // print the local position to console
            std::cout << "localPosition: " << localPosition.x << ", " << localPosition.y << std::endl;
            A = sf::Vector2f(localPosition.x - grid_len, localPosition.y - grid_len);
            D = sf::Vector2f(localPosition.x + grid_len, localPosition.y + grid_len);
            B = sf::Vector2f(localPosition.x + grid_len, localPosition.y - grid_len);
            C = sf::Vector2f(localPosition.x - grid_len, localPosition.y + grid_len);
        }
    
        // draw everything here...
        // A    B
        // +----+
        // |    |
        // +----+
        // C    D

        // draw line AD
        sf::Vertex vertex[2];
        vertex[0].position = A;
        vertex[0].color  = sf::Color::Yellow;
        vertex[1].position = D;
        vertex[1].color = sf::Color::Yellow;
        window.draw(vertex, 2, sf::Lines);

        // draw line BC
        vertex[0].position = B;
        vertex[0].color  = sf::Color::Yellow;
        vertex[1].position = C;
        vertex[1].color = sf::Color::Yellow;
        window.draw(vertex, 2, sf::Lines);

        window.display();
    }

    return 0;
}

2.3 绘制棋盘网格

拆解为 2 个部分: 绘制 3x3 的网格; 对于每个网格,如果鼠标点击了它,就执行绘制。

绘制3x3网格

横向 2 条线, 纵向 2 条线。 绘制它们后就得到了网格。

window.clear(sf::Color::White);

sf::Color grid_color(74, 74, 74);

// draw a 3x3 grid lines
sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
horizon_line1.setPosition(100, 100 + grid_len);
horizon_line1.setFillColor(grid_color);
window.draw(horizon_line1);

sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
horizon_line2.setPosition(100, 100 + 2 * grid_len);
horizon_line2.setFillColor(grid_color);
window.draw(horizon_line2);

sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
vertical_line1.setPosition(100 + grid_len, 100);
vertical_line1.setFillColor(grid_color);
window.draw(vertical_line1);

sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
vertical_line2.setPosition(100 + 2 * grid_len, 100);
vertical_line2.setFillColor(grid_color);
window.draw(vertical_line2);

window.display();

在这里插入图片描述

每个网格内,如果鼠标有点击则执行绘制

根据前一步绘制的网格, 可以确定每个小格子的坐标范围。 根据鼠标点击获取的位置,遍历每个格子, 如果是在当前格子内部, 那么执行绘制。 简单起见, 为了区分绘制内容,只要点击了当前小格子就执行绘制.

先将刚刚回执的网格的代码做重构, 根据起始点 p00(100, 100) 和网格宽度 grid_len = 50 进行绘制, 而不是硬编码每个线段的起点。

根据网格点的起始位置, 可以定义 9 个格子的A,B,C,D坐标取值。 为了避免绘制时的闪烁,定义 class Grid, 由每个网格自己决定是否绘制, 也就是提供 void draw(sf::RenderWindow& window) 函数, 如果鼠标左键点击时落在格子范围内, 那么 shouldRender 变量更新为 true, 执行有效的渲染:

// main 函数
for (int i = 0; i < 9; i++)
{
    // if the mouse is inside the grid, draw a cross
    if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
        localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y)
    {
        printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
        grid[i].shouldRender = true;
    }
    grid[i].draw(window);
}

// Grid::draw() 函数
void draw(sf::RenderWindow& window)
{
    if (shouldRender)
    {
        sf::Vertex vertex[2];
        vertex[0].position = A;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = D;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);

        vertex[0].position = B;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = C;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);
    }
}

效果:
在这里插入图片描述

完整代码:

int draw_grid_and_response_mouse()
{
    constexpr int win_width = 350;
    constexpr int win_height = 350;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);
    constexpr int grid_len = 50;
    sf::Color grid_color(74, 74, 74);

    // p00   p01   p02   p03
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p10   p11   p12   p13
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p20   p21   p22   p23
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p30   p31   p32   p33

    sf::Vector2f p00(100, 100);
    sf::Vector2f p01(100 + grid_len, 100);
    sf::Vector2f p02(100 + 2 * grid_len, 100);
    
    sf::Vector2f p10(100, 100 + grid_len);
    sf::Vector2f p11(100 + grid_len, 100 + grid_len);
    sf::Vector2f p12(100 + 2 * grid_len, 100 + grid_len);

    sf::Vector2f p20(100, 100 + 2 * grid_len);
    sf::Vector2f p21(100 + grid_len, 100 + 2 * grid_len);
    sf::Vector2f p22(100 + 2 * grid_len, 100 + 2 * grid_len);

    std::array<Grid, 9> grid;
    grid[0].update(p00, grid_len);
    grid[1].update(p01, grid_len);
    grid[2].update(p02, grid_len);
    grid[3].update(p10, grid_len);
    grid[4].update(p11, grid_len);
    grid[5].update(p12, grid_len);
    grid[6].update(p20, grid_len);
    grid[7].update(p21, grid_len);
    grid[8].update(p22, grid_len);

    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear(sf::Color::White);
        
        // draw a 3x3 grid lines
        sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
        horizon_line1.setPosition(p10);
        horizon_line1.setFillColor(grid_color);
        window.draw(horizon_line1);

        sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
        horizon_line2.setPosition(p20);
        horizon_line2.setFillColor(grid_color);
        window.draw(horizon_line2);

        sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
        vertical_line1.setPosition(p01);
        vertical_line1.setFillColor(grid_color);
        window.draw(vertical_line1);

        sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
        vertical_line2.setPosition(p02);
        vertical_line2.setFillColor(grid_color);
        window.draw(vertical_line2);

        for (int i = 0; i < 9; i++)
        {
            // if the mouse is inside the grid, draw a cross
            if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
                localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y)
            {
                printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
                grid[i].shouldRender = true;
            }
            grid[i].draw(window);
        }
        
        window.display();
    }

    return 0;
}

2.4 轮流绘制棋子

拆解为如下部分: 绘制单个圆形⭕️, 交替绘制 ❌ 和 ⭕️.

绘制单个圆形

sf::CircleShape circle(grid_len / 3);
circle.setFillColor(bg_color);
circle.setOutlineThickness(4);
circle.setOutlineColor(sf::Color::Red);
circle.setPosition(200, 200);
window.draw(circle);

交替绘制 ❌ 和 ⭕️

每个网格被点击后, 要根据现有情况来执行绘制: 如果之前没有绘制过(没有落子过), 则绘制当前用户的棋子; 如果已经绘制过, 那么不能绘制, 说明当前用户落子无效, 要换一个地方落子。 如果鼠标点击的位置不在 9 个格子的范围内,也是落子无效。

在这里插入图片描述

#include <SFML/Graphics.hpp>
#include <iostream>
#include <array>

char user = 'X';

enum class DrawShape
{
    NONE = 0,
    CROSS = 1, 
    CIRCLE = 2
};

class Grid
{
public:
    Grid() = default;
    Grid(sf::Vector2f start_pos, int a_grid_len)
    {
        update(start_pos, a_grid_len);
    }

    void update(sf::Vector2f start_pos, int a_grid_len)
    {
        grid_len = a_grid_len;
        A = start_pos;
        B = sf::Vector2f(start_pos.x + grid_len, start_pos.y);
        C = sf::Vector2f(start_pos.x, start_pos.y + grid_len);
        D = sf::Vector2f(start_pos.x + grid_len, start_pos.y + grid_len);
    }

    void draw(sf::RenderWindow& window)
    {
        if (drawShape == DrawShape::CROSS)
        {
            drawCross(window);
        }
        else if (drawShape == DrawShape::CIRCLE)
        {
            drawCircle(window);
        }
    }

    void drawCross(sf::RenderWindow& window)
    {
        sf::Vertex vertex[2];
        vertex[0].position = A;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = D;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);

        vertex[0].position = B;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = C;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);
    }

    void drawCircle(sf::RenderWindow& window)
    {
        // draw a circle
        float radius = grid_len / 3;
        sf::CircleShape circle(radius);
        int thickness = 4;
        circle.setOutlineThickness(thickness);
        circle.setOutlineColor(sf::Color::Red);

        // M is middle point of A and D
        sf::Vector2f M((A.x + D.x) / 2, (A.y + D.y) / 2);
        sf::Vector2f circle_position(M.x - radius + thickness, M.y - radius + thickness);
        circle.setPosition(circle_position);

        window.draw(circle);
    }

    sf::Vector2f A, B, C, D;
    DrawShape drawShape{};
    int grid_len;
};

int draw_grid_and_response_mouse()
{
    constexpr int win_width = 350;
    constexpr int win_height = 350;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);
    constexpr int grid_len = 50;
    sf::Color grid_color(74, 74, 74);

    // p00   p01   p02   p03
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p10   p11   p12   p13
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p20   p21   p22   p23
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p30   p31   p32   p33

    sf::Vector2f p00(100, 100);
    sf::Vector2f p01(100 + grid_len, 100);
    sf::Vector2f p02(100 + 2 * grid_len, 100);
    
    sf::Vector2f p10(100, 100 + grid_len);
    sf::Vector2f p11(100 + grid_len, 100 + grid_len);
    sf::Vector2f p12(100 + 2 * grid_len, 100 + grid_len);

    sf::Vector2f p20(100, 100 + 2 * grid_len);
    sf::Vector2f p21(100 + grid_len, 100 + 2 * grid_len);
    sf::Vector2f p22(100 + 2 * grid_len, 100 + 2 * grid_len);

    std::array<Grid, 9> grid;
    grid[0].update(p00, grid_len);
    grid[1].update(p01, grid_len);
    grid[2].update(p02, grid_len);
    grid[3].update(p10, grid_len);
    grid[4].update(p11, grid_len);
    grid[5].update(p12, grid_len);
    grid[6].update(p20, grid_len);
    grid[7].update(p21, grid_len);
    grid[8].update(p22, grid_len);

    bool played = false;

    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear(sf::Color::White);
        
        // draw a 3x3 grid lines
        sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
        horizon_line1.setPosition(p10);
        horizon_line1.setFillColor(grid_color);
        window.draw(horizon_line1);

        sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
        horizon_line2.setPosition(p20);
        horizon_line2.setFillColor(grid_color);
        window.draw(horizon_line2);

        sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
        vertical_line1.setPosition(p01);
        vertical_line1.setFillColor(grid_color);
        window.draw(vertical_line1);

        sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
        vertical_line2.setPosition(p02);
        vertical_line2.setFillColor(grid_color);
        window.draw(vertical_line2);

        for (int i = 0; i < 9; i++)
        {
            // if the mouse is inside the grid, draw a cross
            if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
                localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y &&
                grid[i].drawShape == DrawShape::NONE)
            {
                printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
                if (user == 'X')
                {
                    grid[i].drawShape = DrawShape::CROSS;
                }
                else if (user == 'O')
                {
                    grid[i].drawShape = DrawShape::CIRCLE;
                }
                played = true;
            }
            grid[i].draw(window);
        }

        if (played)
        {
            user = (user == 'X') ? 'O' : 'X';
            played = false;
        }
        
        window.display();
    }

    return 0;
}

2.5 显示局面信息

显示这些信息:

  • 轮到哪个用户落子, X 还是 O?
  • 落子后判断输赢
  • 如果平局, 显示平局
  • 允许中途或结束时,重新来一局

显示轮到谁落子

// draw a text to show the current user
sf::Text text;
text.setFont(font);
text.setString("Current user: " + std::string(1, user));
text.setCharacterSize(24);
text.setFillColor(sf::Color::Black);
text.setPosition(10, 10);
window.draw(text);

落子后判断输赢

在基于控制台的实现中, 使用的是 char board[3][3] 记录棋子, 相比于 enum class DrawShape 要直观的多。 因此我们抛弃 enum class DrawShape, 在 Grid 类中使用 char 类型的数据来存储当前网格里的棋子情况:

class Grid
{
public:
    ...
    void draw(sf::RenderWindow& window)
    {
        if (data == 'X')
        {
            drawCross(window);
        }
        else if (data == 'O')
        {
            drawCircle(window);
        }
    }
private:
    char data = ' ';
};

输赢局面的判断: 如果是有效落子, 那么在 8 条线段上分别判断。 如果判断出来赢了, 则更新 state; 如果没有有效落子, 检查是否存在能落子的地方 (’ '), 如果没有能落子的地方, 说明是平局(Draw):

if (played)
{
    int data[8][6] = {
        {0, 0, 0, 1, 0, 2},
        {1, 0, 1, 1, 1, 2},
        {2, 0, 2, 1, 2, 2},
        {0, 0, 1, 0, 2, 0},
        {0, 1, 1, 1, 2, 1},
        {0, 2, 1, 2, 2, 2},
        {0, 0, 1, 1, 2, 2},
        {0, 2, 1, 1, 2, 0}
    };   

    for (int i = 0; i < 8; i++)
    {
        int x0 = data[i][0];
        int y0 = data[i][1];
        int x1 = data[i][2];
        int y1 = data[i][3];
        int x2 = data[i][4];
        int y2 = data[i][5];
        int idx0 = x0 * 3 + y0;
        int idx1 = x1 * 3 + y1;
        int idx2 = x2 * 3 + y2;
        if (grid[idx0].data == user && grid[idx1].data == user && grid[idx2].data == user)
        {
            if (user == 'X')
            {
                state = PlayState::X_WIN;
            }
            else
            {
                state = PlayState::O_WIN;
            }
            break;
        }
    }

    user = (user == 'X') ? 'O' : 'X'; 
}

if (!played)
{
    state = PlayState::DRAW;
    for (int i = 0; i < 9; i++)
    {
        if (grid[i].data == ' ')
        {
            state = PlayState::PLAYING;
            break;
        }
    }
}

顺带, 在界面上通过绘制文字的方式, 显示当前轮到谁下子、 谁赢了、 是否平局信息, 以及随时可以点击 “restart” 重来一局:

        sf::Vector2f boxPos(130, 300);
        sf::Vector2f boxSize(100, 24);

        sf::RectangleShape box(boxSize);
        box.setFillColor(sf::Color::Red);
        box.setPosition(boxPos);
        window.draw(box);

        // A B
        // C D

        sf::Vector2f boxA(boxPos);
        sf::Vector2f boxB(boxPos.x + boxSize.x, boxPos.y);
        sf::Vector2f boxC(boxPos.x, boxPos.y + boxSize.y);
        sf::Vector2f boxD(boxPos.x + boxSize.x, boxPos.y + boxSize.y);

        if (localPosition.x >= boxA.x && localPosition.y >= boxA.y &&
            localPosition.x <= boxD.x && localPosition.y <= boxD.y)
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].data = ' ';
            }
            user = 'X';
            state = PlayState::PLAYING;
        }

        // draw a button with text "restart"
        sf::Text restart;
        restart.setFont(font);
        restart.setString("Restart");
        restart.setCharacterSize(24);
        restart.setFillColor(sf::Color::White);
        restart.setPosition(boxPos);
        window.draw(restart);

效果如下:

在这里插入图片描述

完整代码是

#include <SFML/Graphics.hpp>
#include <iostream>
#include <array>

enum class PlayState
{
    PLAYING = 0,
    X_WIN = 1,
    O_WIN = 2,
    DRAW = 3
};

class Grid
{
public:
    Grid() = default;
    Grid(sf::Vector2f start_pos, int a_grid_len)
    {
        update(start_pos, a_grid_len);
    }

    void update(sf::Vector2f start_pos, int a_grid_len)
    {
        grid_len = a_grid_len;
        A = start_pos;
        B = sf::Vector2f(start_pos.x + grid_len, start_pos.y);
        C = sf::Vector2f(start_pos.x, start_pos.y + grid_len);
        D = sf::Vector2f(start_pos.x + grid_len, start_pos.y + grid_len);
    }

    void draw(sf::RenderWindow& window)
    {
        if (data == 'X')
        {
            drawCross(window);
        }
        else if (data == 'O')
        {
            drawCircle(window);
        }
    }

    void drawCross(sf::RenderWindow& window)
    {
        sf::Vertex vertex[2];
        vertex[0].position = A;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = D;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);

        vertex[0].position = B;
        vertex[0].color  = sf::Color::Blue;
        vertex[1].position = C;
        vertex[1].color = sf::Color::Blue;
        window.draw(vertex, 2, sf::Lines);
    }

    void drawCircle(sf::RenderWindow& window)
    {
        // draw a circle
        float radius = grid_len / 3;
        sf::CircleShape circle(radius);
        int thickness = 4;
        circle.setOutlineThickness(thickness);
        circle.setOutlineColor(sf::Color::Red);

        // M is middle point of A and D
        sf::Vector2f M((A.x + D.x) / 2, (A.y + D.y) / 2);
        sf::Vector2f circle_position(M.x - radius + thickness, M.y - radius + thickness);
        circle.setPosition(circle_position);

        window.draw(circle);
    }

    sf::Vector2f A, B, C, D;
    char data = ' ';
    int grid_len;
};

char user = 'X';

int draw_grid_and_response_mouse()
{
    constexpr int win_width = 350;
    constexpr int win_height = 350;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);
    constexpr int grid_len = 50;
    sf::Color grid_color(74, 74, 74);

    // p00   p01   p02   p03
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p10   p11   p12   p13
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p20   p21   p22   p23
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p30   p31   p32   p33

    sf::Vector2f p00(100, 100);
    sf::Vector2f p01(100 + grid_len, 100);
    sf::Vector2f p02(100 + 2 * grid_len, 100);
    
    sf::Vector2f p10(100, 100 + grid_len);
    sf::Vector2f p11(100 + grid_len, 100 + grid_len);
    sf::Vector2f p12(100 + 2 * grid_len, 100 + grid_len);

    sf::Vector2f p20(100, 100 + 2 * grid_len);
    sf::Vector2f p21(100 + grid_len, 100 + 2 * grid_len);
    sf::Vector2f p22(100 + 2 * grid_len, 100 + 2 * grid_len);

    std::array<Grid, 9> grid;
    grid[0].update(p00, grid_len);
    grid[1].update(p01, grid_len);
    grid[2].update(p02, grid_len);
    grid[3].update(p10, grid_len);
    grid[4].update(p11, grid_len);
    grid[5].update(p12, grid_len);
    grid[6].update(p20, grid_len);
    grid[7].update(p21, grid_len);
    grid[8].update(p22, grid_len);

    sf::Font font;
    const std::string asset_dir = "../Resources";
    if (!font.loadFromFile(asset_dir + "/Arial.ttf"))
    {
        std::cerr << "failed to load font\n";
        return 1;
    }
    
    PlayState state = PlayState::PLAYING;
    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear(sf::Color::White);

        // draw a text to show the current user
        sf::Text text;
        text.setFont(font);

        std::string state_str;
        if (state == PlayState::DRAW)
        {
            state_str = "Draw";
        }
        else if (state == PlayState::X_WIN)
        {
            state_str = "User X wins!";
        }
        else if (state == PlayState::O_WIN)
        {
            state_str = "User O wins!";
        }
        else
        {
            state_str = "Current user: " + std::string(1, user);
        }

        printf("state: %s\n", state_str.c_str());

        text.setString(state_str);
        text.setCharacterSize(24);
        text.setFillColor(sf::Color::Black);
        text.setPosition(10, 10);
        window.draw(text);
        
        // draw a 3x3 grid lines
        sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
        horizon_line1.setPosition(p10);
        horizon_line1.setFillColor(grid_color);
        window.draw(horizon_line1);

        sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
        horizon_line2.setPosition(p20);
        horizon_line2.setFillColor(grid_color);
        window.draw(horizon_line2);

        sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
        vertical_line1.setPosition(p01);
        vertical_line1.setFillColor(grid_color);
        window.draw(vertical_line1);

        sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
        vertical_line2.setPosition(p02);
        vertical_line2.setFillColor(grid_color);
        window.draw(vertical_line2);

        bool played = false;
        if (state == PlayState::PLAYING)
        {
            for (int i = 0; i < 9; i++)
            {
                // if the mouse is inside the grid, draw a cross
                if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
                    localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y &&
                    grid[i].data == ' ')
                {
                    printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
                    grid[i].data = user;
                    played = true;
                }
                grid[i].draw(window);

                if (played)
                {
                    break;
                }
            }

            if (played)
            {
                int data[8][6] = {
                    {0, 0, 0, 1, 0, 2},
                    {1, 0, 1, 1, 1, 2},
                    {2, 0, 2, 1, 2, 2},
                    {0, 0, 1, 0, 2, 0},
                    {0, 1, 1, 1, 2, 1},
                    {0, 2, 1, 2, 2, 2},
                    {0, 0, 1, 1, 2, 2},
                    {0, 2, 1, 1, 2, 0}
                };   

                for (int i = 0; i < 8; i++)
                {
                    int x0 = data[i][0];
                    int y0 = data[i][1];
                    int x1 = data[i][2];
                    int y1 = data[i][3];
                    int x2 = data[i][4];
                    int y2 = data[i][5];
                    int idx0 = x0 * 3 + y0;
                    int idx1 = x1 * 3 + y1;
                    int idx2 = x2 * 3 + y2;
                    if (grid[idx0].data == user && grid[idx1].data == user && grid[idx2].data == user)
                    {
                        if (user == 'X')
                        {
                            state = PlayState::X_WIN;
                        }
                        else
                        {
                            state = PlayState::O_WIN;
                        }
                        break;
                    }
                }

                user = (user == 'X') ? 'O' : 'X'; 
            }

            if (!played)
            {
                state = PlayState::DRAW;
                for (int i = 0; i < 9; i++)
                {
                    if (grid[i].data == ' ')
                    {
                        state = PlayState::PLAYING;
                        break;
                    }
                }
            }
        }
        else
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].draw(window);
            }
        }

        sf::Vector2f boxPos(130, 300);
        sf::Vector2f boxSize(100, 24);

        sf::RectangleShape box(boxSize);
        box.setFillColor(sf::Color::Red);
        box.setPosition(boxPos);
        window.draw(box);

        // A B
        // C D

        sf::Vector2f boxA(boxPos);
        sf::Vector2f boxB(boxPos.x + boxSize.x, boxPos.y);
        sf::Vector2f boxC(boxPos.x, boxPos.y + boxSize.y);
        sf::Vector2f boxD(boxPos.x + boxSize.x, boxPos.y + boxSize.y);

        if (localPosition.x >= boxA.x && localPosition.y >= boxA.y &&
            localPosition.x <= boxD.x && localPosition.y <= boxD.y)
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].data = ' ';
            }
            user = 'X';
            state = PlayState::PLAYING;
        }

        // draw a button with text "restart"
        sf::Text restart;
        restart.setFont(font);
        restart.setString("Restart");
        restart.setCharacterSize(24);
        restart.setFillColor(sf::Color::White);
        restart.setPosition(boxPos);
        window.draw(restart);

        window.display();
    }

    return 0;
}

int main()
{
    draw_grid_and_response_mouse();
    return 0;
}

3. 制作 SVG 图像, 美化界面

任务分解为: 制作 ‘X’ 和 ‘O’ 棋子的 svg 图像, 使用 SFML 导入 SVG 图像并替代先前棋子的绘制。

使用在线工具(1, 2), 结合 inkscape 和 VSCode svg 插件, 得到 X 和 O 的 svg 图像:

ttt-cross.svg:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   width="300"
   height="300"
   version="1.1"
   id="svg1"
   sodipodi:docname="ttt-cross.svg"
   inkscape:version="1.3.2 (091e20e, 2023-11-25)"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <defs
     id="defs1" />
  <sodipodi:namedview
     id="namedview1"
     pagecolor="#ffffff"
     bordercolor="#000000"
     borderopacity="0.25"
     inkscape:showpageshadow="2"
     inkscape:pageopacity="0.0"
     inkscape:pagecheckerboard="0"
     inkscape:deskcolor="#d1d1d1"
     inkscape:zoom="1.616"
     inkscape:cx="247.83416"
     inkscape:cy="162.74752"
     inkscape:window-width="1408"
     inkscape:window-height="953"
     inkscape:window-x="0"
     inkscape:window-y="25"
     inkscape:window-maximized="0"
     inkscape:current-layer="svg1" />
  <!-- Created with SVG-edit - https://github.com/SVG-Edit/svgedit-->
  <g
     class="layer"
     id="g1"
     transform="translate(25.371288,-13.61386)">
    <title
       id="title1">Layer 1</title>
    <rect
       fill="#747474"
       height="27"
       id="svg_5"
       stroke="#747474"
       transform="rotate(45,229.99949,92.78901)"
       width="300"
       x="50"
       y="200" />
    <rect
       fill="#747474"
       height="27"
       id="svg_7"
       stroke="#747474"
       transform="rotate(135,175,175.58344)"
       width="300"
       x="50"
       y="200" />
  </g>
</svg>

ttt-circle.svg:

<svg id="svgelem" height="140" width="140" xmlns="http://www.w3.org/2000/svg">
    <circle cx="70" cy="70" r="50" stroke="#eee8cf" stroke-width="10" fill="none" />
</svg>

SFML 不支持 svg 的导入, 因此转换为 png 后再使用:

cairosvg ttt-cross.svg -o ttt-cross.png 
cairosvg ttt-circle.svg -o ttt-circle.png 

4. 最终结果

运行效果:
在这里插入图片描述

代码:

#include <SFML/Graphics.hpp>
#include <iostream>
#include <array>

enum class PlayState
{
    PLAYING = 0,
    X_WIN = 1,
    O_WIN = 2,
    DRAW = 3
};

class Grid
{
public:
    Grid() = default;
    Grid(sf::Vector2f start_pos, int a_grid_len)
    {
        update(start_pos, a_grid_len);
    }

    void update(sf::Vector2f start_pos, int a_grid_len)
    {
        grid_len = a_grid_len;
        A = start_pos;
        B = sf::Vector2f(start_pos.x + grid_len, start_pos.y);
        C = sf::Vector2f(start_pos.x, start_pos.y + grid_len);
        D = sf::Vector2f(start_pos.x + grid_len, start_pos.y + grid_len);
    }

    void draw(sf::RenderWindow& window)
    {
        if (data == 'X')
        {
            drawCross(window);
        }
        else if (data == 'O')
        {
            drawCircle(window);
        }
    }

    void drawCross(sf::RenderWindow& window)
    {
        if (0)
        {
            sf::Vertex vertex[2];
            vertex[0].position = A;
            vertex[0].color  = sf::Color::Blue;
            vertex[1].position = D;
            vertex[1].color = sf::Color::Blue;
            window.draw(vertex, 2, sf::Lines);

            vertex[0].position = B;
            vertex[0].color  = sf::Color::Blue;
            vertex[1].position = C;
            vertex[1].color = sf::Color::Blue;
            window.draw(vertex, 2, sf::Lines);
        }
        else
        {
            // load a svg file to create a texture
            sf::Texture texture;
            if (!texture.loadFromFile("../ttt-cross.png"))
            {
                std::cerr << "failed to load cross image\n";
                return;
            }
            // draw a sprite in box region A, D
            sf::Sprite sprite(texture);
            sprite.setPosition(A);
            sprite.setScale(0.2, 0.2);
            window.draw(sprite);
        }
    }

    void drawCircle(sf::RenderWindow& window)
    {
        if (0)
        {
            // draw a circle
            float radius = grid_len / 3;
            sf::CircleShape circle(radius);
            int thickness = 4;
            circle.setOutlineThickness(thickness);
            circle.setOutlineColor(sf::Color::Red);

            // M is middle point of A and D
            sf::Vector2f M((A.x + D.x) / 2, (A.y + D.y) / 2);
            sf::Vector2f circle_position(M.x - radius + thickness, M.y - radius + thickness);
            circle.setPosition(circle_position);

            window.draw(circle);
        }
        else
        {
            // load a svg file to create a texture
            sf::Texture texture;
            if (!texture.loadFromFile("../ttt-circle.png"))
            {
                std::cerr << "failed to load circle image\n";
                return;
            }
            // draw a sprite in box region A, D
            sf::Sprite sprite(texture);
            sprite.setPosition(A);
            sprite.setScale(0.4, 0.4);
            window.draw(sprite);   
        }
    }

    sf::Vector2f A, B, C, D;
    char data = ' ';
    int grid_len;
};

char user = 'X';

int draw_grid_and_response_mouse()
{
    constexpr int win_width = 350;
    constexpr int win_height = 350;
    const std::string title = "Tic Tac Toe SFML";
    sf::RenderWindow window(sf::VideoMode(win_width, win_height), title);
    constexpr int grid_len = 50;
    sf::Color grid_color(64, 148, 135);
    sf::Color bg_color(78, 177, 163);

    // p00   p01   p02   p03
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p10   p11   p12   p13
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p20   p21   p22   p23
    // +-----+-----+-----+
    // |     |     |     |
    // +-----+-----+-----+
    // p30   p31   p32   p33

    sf::Vector2f p00(100, 100);
    sf::Vector2f p01(100 + grid_len, 100);
    sf::Vector2f p02(100 + 2 * grid_len, 100);
    
    sf::Vector2f p10(100, 100 + grid_len);
    sf::Vector2f p11(100 + grid_len, 100 + grid_len);
    sf::Vector2f p12(100 + 2 * grid_len, 100 + grid_len);

    sf::Vector2f p20(100, 100 + 2 * grid_len);
    sf::Vector2f p21(100 + grid_len, 100 + 2 * grid_len);
    sf::Vector2f p22(100 + 2 * grid_len, 100 + 2 * grid_len);

    std::array<Grid, 9> grid;
    grid[0].update(p00, grid_len);
    grid[1].update(p01, grid_len);
    grid[2].update(p02, grid_len);
    grid[3].update(p10, grid_len);
    grid[4].update(p11, grid_len);
    grid[5].update(p12, grid_len);
    grid[6].update(p20, grid_len);
    grid[7].update(p21, grid_len);
    grid[8].update(p22, grid_len);

    sf::Font font;
    const std::string asset_dir = "../Resources";
    if (!font.loadFromFile(asset_dir + "/Arial.ttf"))
    {
        std::cerr << "failed to load font\n";
        return 1;
    }
    
    PlayState state = PlayState::PLAYING;
    while (window.isOpen())
    {
        sf::Event event;
        sf::Vector2i localPosition(-1, -1);
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed) { window.close(); }

            if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
            {
                // get the local mouse position (relative to a window)
                localPosition = sf::Mouse::getPosition(window);
            }
        }
        window.clear(bg_color);

        // draw a text to show the current user
        sf::Text text;
        text.setFont(font);

        std::string state_str;
        if (state == PlayState::DRAW)
        {
            state_str = "Draw";
        }
        else if (state == PlayState::X_WIN)
        {
            state_str = "User X wins!";
        }
        else if (state == PlayState::O_WIN)
        {
            state_str = "User O wins!";
        }
        else
        {
            state_str = "Current user: " + std::string(1, user);
        }

        printf("state: %s\n", state_str.c_str());

        text.setString(state_str);
        text.setCharacterSize(24);
        text.setFillColor(sf::Color::Black);
        text.setPosition(10, 10);
        window.draw(text);
        
        // draw a 3x3 grid lines
        sf::RectangleShape horizon_line1(sf::Vector2f(grid_len * 3, 8));
        horizon_line1.setPosition(p10);
        horizon_line1.setFillColor(grid_color);
        window.draw(horizon_line1);

        sf::RectangleShape horizon_line2(sf::Vector2f(grid_len * 3, 8));
        horizon_line2.setPosition(p20);
        horizon_line2.setFillColor(grid_color);
        window.draw(horizon_line2);

        sf::RectangleShape vertical_line1(sf::Vector2f(8, grid_len * 3));
        vertical_line1.setPosition(p01);
        vertical_line1.setFillColor(grid_color);
        window.draw(vertical_line1);

        sf::RectangleShape vertical_line2(sf::Vector2f(8, grid_len * 3));
        vertical_line2.setPosition(p02);
        vertical_line2.setFillColor(grid_color);
        window.draw(vertical_line2);

        bool played = false;
        if (state == PlayState::PLAYING)
        {
            for (int i = 0; i < 9; i++)
            {
                // if the mouse is inside the grid, draw a cross
                if (localPosition.x >= grid[i].A.x && localPosition.x <= grid[i].D.x &&
                    localPosition.y >= grid[i].A.y && localPosition.y <= grid[i].D.y &&
                    grid[i].data == ' ')
                {
                    printf("inside grid[%d]: localPosition: %d, %d\n", i, localPosition.x, localPosition.y);
                    grid[i].data = user;
                    played = true;
                }
                grid[i].draw(window);

                if (played)
                {
                    break;
                }
            }

            if (played)
            {
                int data[8][6] = {
                    {0, 0, 0, 1, 0, 2},
                    {1, 0, 1, 1, 1, 2},
                    {2, 0, 2, 1, 2, 2},
                    {0, 0, 1, 0, 2, 0},
                    {0, 1, 1, 1, 2, 1},
                    {0, 2, 1, 2, 2, 2},
                    {0, 0, 1, 1, 2, 2},
                    {0, 2, 1, 1, 2, 0}
                };   

                for (int i = 0; i < 8; i++)
                {
                    int x0 = data[i][0];
                    int y0 = data[i][1];
                    int x1 = data[i][2];
                    int y1 = data[i][3];
                    int x2 = data[i][4];
                    int y2 = data[i][5];
                    int idx0 = x0 * 3 + y0;
                    int idx1 = x1 * 3 + y1;
                    int idx2 = x2 * 3 + y2;
                    if (grid[idx0].data == user && grid[idx1].data == user && grid[idx2].data == user)
                    {
                        if (user == 'X')
                        {
                            state = PlayState::X_WIN;
                        }
                        else
                        {
                            state = PlayState::O_WIN;
                        }
                        break;
                    }
                }

                user = (user == 'X') ? 'O' : 'X'; 
            }

            if (!played)
            {
                state = PlayState::DRAW;
                for (int i = 0; i < 9; i++)
                {
                    if (grid[i].data == ' ')
                    {
                        state = PlayState::PLAYING;
                        break;
                    }
                }
            }
        }
        else
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].draw(window);
            }
        }

        sf::Vector2f boxPos(130, 300);
        sf::Vector2f boxSize(80, 30);

        // A B
        // C D

        sf::Vector2f boxA(boxPos);
        sf::Vector2f boxB(boxPos.x + boxSize.x, boxPos.y);
        sf::Vector2f boxC(boxPos.x, boxPos.y + boxSize.y);
        sf::Vector2f boxD(boxPos.x + boxSize.x, boxPos.y + boxSize.y);

        if (localPosition.x >= boxA.x && localPosition.y >= boxA.y &&
            localPosition.x <= boxD.x && localPosition.y <= boxD.y)
        {
            for (int i = 0; i < 9; i++)
            {
                grid[i].data = ' ';
            }
            user = 'X';
            state = PlayState::PLAYING;
        }

        // draw a button with text "restart"
        sf::Text restart;
        restart.setFont(font);
        restart.setString("Restart");
        restart.setCharacterSize(24);
        restart.setFillColor(sf::Color(35, 44, 44));
        restart.setPosition(boxPos);
        restart.setOutlineColor(sf::Color(35, 44, 44));
        window.draw(restart);

        window.display();
    }

    return 0;
}

int main()
{
    draw_grid_and_response_mouse();
    return 0;
}

总结

花了 4 个半小时, 在先前写好了控制台版本 tic-tac-toe 的基础上, 使用 SFML 做了简陋的界面, 让游戏先运行起来能够正常玩; 然后制作了 SVG 图像, 仿照谷歌搜索结果里的在线版本的界面, 稍微美化了一下显示效果。

在使用 SFML 制作界面的过程中, 没有一上来就苛求制作好看的 ‘X’ 形状, 因为它后期可以重新调整; 本来引入了 DrawShape 枚举类型, 不过后来发现有点冗余, 在 class Grid 中用 char data 更方便。

对于绘制的最终结果, 不是一蹴而就的, 而是分别写了小型函数来验证, ‘X’ 可以单独绘制正确, 并且利用 class Grid 类的 draw() 方法, 根据落子的情况做了绘制(没有落子则不绘制)。

对于输赢局面的判别, 是先前控制台版本代码里有的, 直接拿来用了。 先写控制台版本看来确实可以加速迭代, 套着一套 GUI 的时候想逻辑, 需要对界面代码的编写比较熟悉才会不卡壳, 为了避免卡壳, 用熟悉的控制台去写, 是很方便的。

这个基于 SFML 的 tic-tac-toe 可以进一步扩展, 例如增加音效, 在获胜的时候用动态效果把三个棋子连接起来, 增加人机对战模式, 并利用 alpha-beta 剪枝算法进行搜索优化。

References

  • https://en.sfml-dev.org/forums/index.php?topic=21620.0
  • https://www.sfml-dev.org/tutorials/2.6/window-inputs.php
  • https://www.sfml-dev.org/tutorials/2.6/graphics-shape.php
  • https://github.com/skiff/TicTacToe
  • https://github.com/juchem/tic-tac-toe
  • https://svgedit.netlify.app/editor/index.html
  • https://www.nhooo.com/note/qa09md.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1445396.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

160基于matlab的负熵和峭度信号的盲分离

基于matlab的负熵和峭度信号的盲分离。基于峭度的FastICA算法的收敛速度要快&#xff0c;迭代次数比基于负熵的FastICA算法少四倍以上。SMSE随信噪比增大两种判据下的FastICA算法都逐渐变小&#xff0c;但是基于峭度的算法的SMSE更小&#xff0c;因此基于峭度的FastICA算法性能…

Spark MLlib

目录 一、Spark MLlib简介 &#xff08;一&#xff09;什么是机器学习 &#xff08;二&#xff09;基于大数据的机器学习 &#xff08;三&#xff09;Spark机器学习库MLlib 二、机器学习流水线 &#xff08;一&#xff09;机器学习流水线概念 &#xff08;二&#xff09…

车载电子电器架构 —— 电子电气系统控制器开发体系

车载电子电器架构 —— 电子电气系统控制器开发 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费…

idea:如何连接数据库

1、在idea中打开database: 2、点击 ‘’ ---> Data Source ---> MySQL 3、输入自己的账号和密码其他空白处可以不填&#xff0c;用户和密码可以在自己的mysql数据库中查看 4、最后选择自己需要用的数据库&#xff0c;点击运用ok&#xff0c;等待刷新即可 最后&#xff1a…

《CSS 简易速速上手小册》第9章:CSS 最佳实践(2024 最新版)

文章目录 9.1 维护大型项目的 CSS9.1.1 基础知识9.1.2 重点案例&#xff1a;构建一个可复用的 UI 组件库9.1.3 拓展案例 1&#xff1a;优化现有项目的 CSS 结构9.1.4 拓展案例 2&#xff1a;实现主题切换功能 9.2 BEM、OOCSS 和 SMACSS 方法论9.2.1 基础知识9.2.2 重点案例&…

【从Python基础到深度学习】3. Winscp与Ubuntu使用及配置

一、Ubuntu的使用 1.1 开启与关闭 1.2 修改Ubuntu分辨率 选择适合自己电脑大小的分辨率 1.3 Ubuntu终端 1.4 网络测试 终端中输入&#xff1a; ping www.baidu.com ctr C 退出ping命令 1.5 下载软件 连通安装源 sudo apt update 安装 ssh vim sudo apt install ss…

零基础学python之高级编程(2)---面向对象编程组合用法及其继承特性

面向对象编程组合用法及其继承特性 文章目录 面向对象编程组合用法及其继承特性前言一、面向对象编程组合用法1 直接在类中创建实例化对象2 给入参数&#xff0c;在结尾给一个类实例化传参变成对象&#xff0c;然后再在另一个类中传入对象参数 二、面向对象编程----继承1.继承定…

浅谈人工智能之深度学习~

目录 前言&#xff1a;深度学习的进展 一&#xff1a;深度学习的基本原理和算法 二&#xff1a;深度学习的应用实例 三&#xff1a;深度学习的挑战和未来发展方向 四&#xff1a;深度学习与机器学习的关系 五&#xff1a;深度学习与人类的智能交互 悟已往之不谏&#xff0…

2023年智能可穿戴行业市场分析(电商数据查询分析):智能手表销额增长21%,手环明显下滑

近年来&#xff0c;随着技术的进步&#xff0c;智能可穿戴设备在社交网络、医疗保健、导航等诸多领域有着非常广泛的应用&#xff0c;这为大众生活带来了诸多便利。 当前的可穿戴产品形态纷繁多样&#xff0c;主要包括智能手表、智能眼镜、智能手环、健康穿戴和体感控制等等&am…

Unity学习笔记(零基础到就业)|Chapter02:C#基础

Unity学习笔记&#xff08;零基础到就业&#xff09;&#xff5c;Chapter02:C#基础 前言一、复杂数据&#xff08;变量&#xff09;类型part01&#xff1a;枚举数组1.特点2.枚举&#xff08;1&#xff09;基本概念&#xff08;2&#xff09;申明枚举变量&#xff08;3&#xff…

生态位模拟——草稿笔记

文章目录 前言ENM初识一、所需软件安装1.1. 下载ArcGIS软件&#xff1a;1.2. 下载 MaxEnt软件&#xff1a;1.3. 下载ENMtools&#xff1a; 二、数据准备与处理2.1. 物种分布数据2.2. 环境因子数据2.3. 地图数据2.4. 物种分布点去冗余2.4.1. 使用spThin包中的thin函数2.4.2. 或者…

使用耳机壳UV树脂制作私模定制耳塞有什么优点和缺点呢?

使用耳机壳UV树脂制作私模定制耳塞具有以下优点&#xff1a; 个性化定制&#xff1a;UV树脂可以根据用户的耳型进行个性化定制&#xff0c;使耳塞与用户的耳朵形状完美契合&#xff0c;提高舒适度和佩戴稳定性。高强度和耐磨性&#xff1a;UV树脂具有高强度和耐磨性&#xff0…

Spring Native 解放 JVM

一、Spring Native 是什么 Spring Native可以通过GraalVM将Spring应用程序编译成原生镜像&#xff0c;提供了一种新的方式来部署Spring应用。与Java虚拟机相比&#xff0c;原生镜像可以在许多场景下降低工作负载&#xff0c;包括微服务&#xff0c;函数式服务&#xff0c;非常…

C语言printf函数详解..

1.printf函数解析 前面我们有讲过printf函数的格式为&#xff1a; printf(“占位1 占位2 占位3……”, 替代1, 替代2, 替代3……); 今天我们进一步深入的解析一下这个函数 2.printf函数的特点 1.printf函数是一个变参函数(即参数的数量和类型都不确定) 2.printf函数的第一个…

Linux笔记之xhost +和docker的关系以及GDK_SCALE和GDK_DPI_SCALE详解

Linux笔记之xhost 和docker的关系以及GDK_SCALE和GDK_DPI_SCALE详解 ——2024-02-11 code review! 文章目录 Linux笔记之xhost 和docker的关系以及GDK_SCALE和GDK_DPI_SCALE详解xhost 的作用xhost 与 Docker 的关系 -e GDK_SCALE 和 -e GDK_DPI_SCALE详解GDK_SCALEGDK_DPI_SC…

Java集合 LinkedList

目录 LinkedList实例 LinkedList LinkedList是Java中的一个重要的数据结构&#xff0c;它实现了List接口&#xff0c;提供了链表数据结构的实现。LinkedList类中包含了各种常用的链表操作&#xff0c;如添加、删除、查找等。 LinkedList的特点是元素可以动态地添加到链表的任…

【转载】原生社区交友婚恋视频即时通讯双端APP源码 ONE兔2.0版

原生社区交友婚恋视频即时通讯双端APP源码下载ONE兔2.0版 包含后端、H5源码源码&#xff0c;Android源码&#xff0c;IOS源码

恒创科技:怎么看云主机的性价比

随着云计算技术的不断发展&#xff0c;云主机已经成为越来越多企业和个人用户的首选计算资源。然而&#xff0c;在选择云主机时&#xff0c;性价比是一个非常重要的考虑因素。那么&#xff0c;怎么看云主机的性价比呢&#xff1f; 通常来说&#xff0c;云主机的成本主要包括硬…

【调试】pstore原理和使用方法总结

什么是pstore pstore最初是用于系统发生oops或panic时&#xff0c;自动保存内核log buffer中的日志。不过在当前内核版本中&#xff0c;其已经支持了更多的功能&#xff0c;如保存console日志、ftrace消息和用户空间日志。同时&#xff0c;它还支持将这些消息保存在不同的存储…

模拟发送 Ctrl+Alt+Del 快捷键

目录 前言 一、在 XP 系统上模拟 SAS 二、在不低于 Vista 的系统上模拟 SAS 2.1 一些细节 2.2 实现原理和应用 三、完整实现代码和测试 3.1 客户端控制台程序 3.2 服务程序 3.3 编译&测试程序 四、总结&更新 参考文献 前言 对于开启了安全登陆的窗口工作站…