3-7 项目-图形版井字棋(下)

3-7 项目-图形版井字棋(下)

⏱ 本节预计用时:20分钟

本节意义:通过对井字棋核心逻辑的完善,你将做出自己第一个能看的项目,毫无疑问,这个时刻是具有里程碑意义的!

本节目标:学会把"鼠标点中了哪个格子"翻译成"修改 boardrowcol";结合 model 的 checkWin / checkDraw 完成落子、换人、判断结束;画出"轮到谁"提示和结算界面;亲眼体会 model 一行没改、只换 view 就换了种形态的架构威力。

1 第三步:点格子落子、轮流、判胜负

棋子的变量存在哪?board 数组,规则 checkWin、checkDraw,这些东西全在 model 里。所以我们这步要做的,本质上就是把 2-5 里的那一小段下棋逻辑,从"键盘输坐标"换成"鼠标点格子",别的啥也不用动。

先打开game.h,看我们的棋盘存在哪里:

c 复制代码
typedef enum { CELL_EMPTY = 0, CELL_X = 1, CELL_O = 2 } CellType;
extern CellType board[3][3];

好了,我们知道棋子是存在一个二维数组board里的了,和第二章里绘制棋子的思路一样,我们要把行、列和屏幕里实际的格子显示关联起来:

c 复制代码
/* 把棋盘第 row 行第 col 列,转成屏幕上的一个矩形(row、col 和 board 下标一样是 0~2) */
Rectangle cellRect(int row, int col)
{
    Rectangle rec;
    rec.x = BOARD_X + col * CELL_SIZE;
    rec.y = BOARD_Y + row * CELL_SIZE;
    rec.width = CELL_SIZE;
    rec.height = CELL_SIZE;
    return rec;
}

ok,关联好了,我们开始加画棋子。view.c中的showBoard函数格子画完之后,再遍历 board,哪一格有棋子就在哪一格写上 X 或 O。

c 复制代码
/* 画棋盘:深灰色大矩形当底 + 9 个浅灰色格子矩形,再画棋子 */
void showBoard(void)
{
    int row, col;

    /* 1. 深灰色大矩形:覆盖整个棋盘区域 */
    DrawRectangle(BOARD_X, BOARD_Y, 3 * CELL_SIZE, 3 * CELL_SIZE, DARKGRAY);

    /* 2. 9 个浅灰色格子:每个比一格小一圈(四周各让出 LINE_SIZE / 2),盖在深灰底上 */
    for (row = 0; row < 3; row++) {
        for (col = 0; col < 3; col++) {
            Rectangle r = cellRect(row, col);
            DrawRectangle((int)r.x + LINE_SIZE / 2,
                          (int)r.y + LINE_SIZE / 2,
                          CELL_SIZE - LINE_SIZE,
                          CELL_SIZE - LINE_SIZE,
                          LIGHTGRAY);
        }
    }

    /* 3. 棋子 */
    for (row = 0; row < 3; row++) {
        for (col = 0; col < 3; col++) {
            Rectangle r = cellRect(row, col);
            int cx = (int)(r.x + CELL_SIZE / 2) - 15;   /* 让字大概落在格子中央 */
            int cy = (int)(r.y + CELL_SIZE / 2) - 15;
            if (board[row][col] == CELL_X) {
                DrawText("X", cx, cy, 40, BLUE);
            } else if (board[row][col] == CELL_O) {
                DrawText("O", cx, cy, 40, RED);
            }
        }
    }
}

这里的双重循环遍历 board ,本质和 2-5 里 printf 版的 showBoard 一模一样。board[row][col] 是 X 就画 X,是 O 就画 O。只是画的方式不一样了,从往终端打字符,变成了往窗口画字。

知道凭board[row][col]画图了,那它在哪做修改呢?在main里做修改。

凭啥修改呢?凭你点了对应的格子。先在view.c 加入"点中了哪一格棋盘"的判断。

c 复制代码
/* 点中了棋盘第 row 行第 col 列 */
int cellClicked(Vector2 mouse, int row, int col)
{
    return CheckCollisionPointRec(mouse, cellRect(row, col));
    // mouse 落在"第 row 行第 col 列的格子"这个矩形里就返回真。
}

再在main里加入点击对应格子,修改 board[row][col] ,换人的逻辑。记得include "game.h",以及对这些变量初始化(不知道为啥的结合game.c,game.h理解)。

c 复制代码
#include "raylib.h"
#include "game.h"     /* 这步起要用 board、currentPlayer、checkWin 等 */
#include "view.h"

int main(void)
{
    InitWindow(800, 600, "Tic-Tac-Toe");
    SetTargetFPS(60);

    Screen screen = SCREEN_MENU;
    CellType winner = CELL_EMPTY;   /* 谁赢了(先记着,结算界面 2.3 才真正用) */

    initBoard();                    /* 先准备好棋盘数据(调 model 的函数) */
    currentPlayer = CELL_X;         /* X 先走 */
    ...

然后把处理点击的逻辑补全。

c 复制代码
        /* ===== 根据当前界面,处理这次点击 ===== */
        if (screen == SCREEN_MENU)
        {
            if (clicked && startBtnClicked(mouse)) {
                initBoard();              /* 点开始:清空棋盘,重新开局 */
                currentPlayer = CELL_X;
                winner = CELL_EMPTY;
                screen = SCREEN_GAME;
            }
            else if (clicked && exitBtnClicked(mouse)) {
                break;
            }
        }
        else if (screen == SCREEN_GAME)
        {
            if (clicked && backBtnClicked(mouse)) {
                screen = SCREEN_MENU;     /* 点返回:不玩了,回主菜单 */
            }
            else
            {
                int row, col;
                for (row = 0; row < 3; row++) {
                    for (col = 0; col < 3; col++) {
                        /* 点中了一个"空格子",才落子 */
                        if (clicked && cellClicked(mouse, row, col) &&
                            board[row][col] == CELL_EMPTY)
                        {
                            board[row][col] = currentPlayer;   /* 落子 */

                            if (checkWin(currentPlayer)) {     /* 赢了 */
                                winner = currentPlayer;
                                screen = SCREEN_RESULT;
                                // 三连或下满之后,程序会切到 SCREEN_RESULT(结算状态)。结算画面还没画,这里渲染暂时借用了棋盘的画面,你会看到"棋盘定在最后一步,点格子也没反应"。这不是卡死,是程序已经在结算状态等着了,点 MENU 就能回主菜单。
                            }
                            else if (checkDraw()) {            /* 平局 */
                                winner = CELL_EMPTY;
                                screen = SCREEN_RESULT;
                            }
                            else {                             /* 没结束,换人 */
                                currentPlayer = (currentPlayer == CELL_X) ? CELL_O : CELL_X;
                            }
                        }
                    }
                }
            }
        }
        else    /* SCREEN_RESULT */
        {
            if (clicked && backBtnClicked(mouse)) {
                screen = SCREEN_MENU;     /* 点返回:回主菜单 */
            }
        }

这时你一定感受到了model-view架构的牛逼之处!initBoard、currentPlayer、后面落子的 checkWin、checkDraw......这些model层的东西是真真的一行都没改。我不知道你爽不爽,我写这里是真爽。

渲染部分:

c 复制代码
        if (screen == SCREEN_MENU) {
            showMainMenu();
        }
        else if (screen == SCREEN_GAME) {
            showBoard();        /* 棋盘(现在带棋子了) */
            showGameMenu();     /* 返回主菜单按钮 */
        }
        else {   /* SCREEN_RESULT:暂时沿用棋盘画面,2.3 画真正的结算界面 */
            showBoard();
            showGameMenu();
        }

这步结束了,编译运行,进入游戏界面,随便下几个棋子,效果是这样的:

恭喜你,完成了这个项目的80%,理解了所有的难点,后边再也没啥难点了。我不知道你此时的心情是怎样的,不管你是开心还是头晕乎乎的,去吃点东西犒劳一下自己吧!

目前的状态是已经可以玩了。后边只需要加一些提示信息,补上结算界面就彻底结束了。

完整代码如下:

view.c :

c 复制代码
#include "view.h"
#include "raylib.h"

/* ---------- 界面的尺寸和位置(都写在这里,以后改界面就改这里) ---------- */

#define CELL_SIZE 120   /* 棋盘每个格子的边长 */
#define BOARD_X   220   /* 棋盘左上角的横坐标 */
#define BOARD_Y   140   /* 棋盘左上角的纵坐标 */
#define LINE_SIZE 6     /* 格线的粗细(格子之间的缝) */

/* 三个按钮的位置(x, y, 宽, 高) */
Rectangle startBtn = { 250, 250, 300, 80 };   /* 主菜单的"开始游戏" */
Rectangle exitBtn  = { 250, 360, 300, 80 };   /* 主菜单的"退出游戏" */
Rectangle backBtn  = { 300, 515, 200, 60 };   /* 游戏界面里的"返回主菜单" */

/* 显示主菜单 */
void showMainMenu(void)
{
    DrawText("Tic-Tac-Toe", 240, 110, 50, DARKGRAY);
    DrawRectangleRec(startBtn, GRAY);   /* 画"开始游戏"按钮 */
    DrawRectangleRec(exitBtn, GRAY);    /* 画"退出游戏"按钮 */
    DrawText("START", 355, 272, 30, WHITE);
    DrawText("EXIT",  360, 382, 30, WHITE);
}

/* 画游戏界面里的"返回主菜单"按钮 */
void showGameMenu(void)
{
    DrawRectangleRec(backBtn, LIGHTGRAY);
    DrawText("MENU", 370, 530, 25, DARKGRAY);
}

/* 把棋盘第 row 行第 col 列,转成屏幕上的一个矩形(row、col 和 board 下标一样是 0~2) */
Rectangle cellRect(int row, int col)
{
    Rectangle rec;
    rec.x = BOARD_X + col * CELL_SIZE;
    rec.y = BOARD_Y + row * CELL_SIZE;
    rec.width = CELL_SIZE;
    rec.height = CELL_SIZE;
    return rec;
}

/* 画棋盘:深灰色大矩形当底 + 9 个浅灰色格子矩形,再画棋子 */
void showBoard(void)
{
    int row, col;

    /* 1. 深灰色大矩形:覆盖整个棋盘区域 */
    DrawRectangle(BOARD_X, BOARD_Y, 3 * CELL_SIZE, 3 * CELL_SIZE, DARKGRAY);

    /* 2. 9 个浅灰色格子:每个比一格小一圈(四周各让出 LINE_SIZE / 2),盖在深灰底上 */
    for (row = 0; row < 3; row++) {
        for (col = 0; col < 3; col++) {
            Rectangle r = cellRect(row, col);
            DrawRectangle((int)r.x + LINE_SIZE / 2,
                          (int)r.y + LINE_SIZE / 2,
                          CELL_SIZE - LINE_SIZE,
                          CELL_SIZE - LINE_SIZE,
                          LIGHTGRAY);
        }
    }

    /* 3. 棋子 */
    for (row = 0; row < 3; row++) {
        for (col = 0; col < 3; col++) {
            Rectangle r = cellRect(row, col);
            int cx = (int)(r.x + CELL_SIZE / 2) - 15;   /* 让字大概落在格子中央 */
            int cy = (int)(r.y + CELL_SIZE / 2) - 15;
            if (board[row][col] == CELL_X) {
                DrawText("X", cx, cy, 40, BLUE);
            } else if (board[row][col] == CELL_O) {
                DrawText("O", cx, cy, 40, RED);
            }
        }
    }
}
/* ---------- 判断鼠标点了哪里(把鼠标坐标翻译成"点中了什么") ---------- */

int startBtnClicked(Vector2 mouse) { 
    return CheckCollisionPointRec(mouse, startBtn); 
}

int exitBtnClicked(Vector2 mouse) { 
    return CheckCollisionPointRec(mouse, exitBtn); 
}

int backBtnClicked(Vector2 mouse)  { return CheckCollisionPointRec(mouse, backBtn); }

/* 点中了棋盘第 row 行第 col 列 */
int cellClicked(Vector2 mouse, int row, int col)
{
    return CheckCollisionPointRec(mouse, cellRect(row, col));
    // mouse 落在"第 row 行第 col 列的格子"这个矩形里就返回真。
}

main.c :

c 复制代码
#include "raylib.h"
#include "view.h"

int main(void)
{
    InitWindow(800, 600, "Tic-Tac-Toe");
    SetTargetFPS(60);

    Screen screen = SCREEN_MENU;   /* 一开机,先显示主菜单 */
    CellType winner = CELL_EMPTY;   /* 谁赢了(先记着,结算界面 2.3 才真正用) */

    initBoard();                    /* 先准备好棋盘数据(调 model 的函数) */
    currentPlayer = CELL_X;         /* X 先走 */

    while (!WindowShouldClose())
    {
        // 1. 更新阶段:读取输入、更新游戏逻辑
        Vector2 mouse = GetMousePosition();
        int clicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);

        if (screen == SCREEN_MENU)
        {
            if (clicked && startBtnClicked(mouse)) {
                initBoard();              /* 点开始:清空棋盘,重新开局 */
                currentPlayer = CELL_X;
                winner = CELL_EMPTY;
                screen = SCREEN_GAME;
            }
            else if (clicked && exitBtnClicked(mouse)) {
                break;                  /* 点退出:关程序 */
            }
        }
        else if (screen == SCREEN_GAME)
        {
            if (clicked && backBtnClicked(mouse)) {
                screen = SCREEN_MENU;   /* 点返回:回主菜单 */
            }
            else
            {
                int row, col;
                for (row = 0; row < 3; row++) {
                    for (col = 0; col < 3; col++) {
                        /* 点中了一个"空格子",才落子 */
                        if (clicked && cellClicked(mouse, row, col) &&
                            board[row][col] == CELL_EMPTY)
                        {
                            board[row][col] = currentPlayer;   /* 落子 */

                            if (checkWin(currentPlayer)) {     /* 赢了 */
                                winner = currentPlayer;
                                screen = SCREEN_RESULT;
                                // 三连或下满之后,程序会切到 SCREEN_RESULT(结算状态)。结算画面还没画,这里渲染暂时借用了棋盘的画面,你会看到"棋盘定在最后一步,点格子也没反应"。这不是卡死,是程序已经在结算状态等着了,点 MENU 就能回主菜单。
                            }
                            else if (checkDraw()) {            /* 平局 */
                                winner = CELL_EMPTY;
                                screen = SCREEN_RESULT;
                            }
                            else {                             /* 没结束,换人 */
                                currentPlayer = (currentPlayer == CELL_X) ? CELL_O : CELL_X;
                            }
                        }
                    }
                }
            }
        }
        else    /* SCREEN_RESULT */
        {
            if (clicked && backBtnClicked(mouse)) {
                screen = SCREEN_MENU;     /* 点返回:回主菜单 */
            }
        }


        // 2. 渲染阶段:只负责画东西
        BeginDrawing();
        ClearBackground(RAYWHITE);

        if (screen == SCREEN_MENU) {
            showMainMenu();
        }
        else if (screen == SCREEN_GAME) {
            showBoard();        /* 棋盘(现在带棋子了) */
            showGameMenu();     /* 返回主菜单按钮 */
        }
        else {   /* SCREEN_RESULT:暂时沿用棋盘画面,2.3 画真正的结算界面 */
            showBoard();
            showGameMenu();
        }
        EndDrawing();
    }

    CloseWindow();
    return 0;
}

2 第四步:显示轮到谁

上一小步能下棋了,但界面上看不出现在轮谁下。加一行提示:当前轮到的玩家,把它的符号('X' 或 'O')显示在棋盘上方。

先给view.c 加 showPlayerInfo。

c 复制代码
/* 画"当前轮到谁"这行提示 */
void showPlayerInfo(void)
{
    DrawText("Turn:", 260, 60, 25, DARKGRAY);
    DrawText(TextFormat("%c", players[currentPlayer - 1].mark), 350, 60, 25, RED); // currentPlayer是现在轮到谁,players[currentPlayer - 1].mark是它的符号。
}

players 数组和 currentPlayer 是 model 的数据(game.h 里 extern 声明,game.c 里定义的实体)。showPlayerInfo 不生产这些数据,只负责拿去显示。

再让main.c 调 initPlayers、渲染时游戏界面加上 showPlayerInfo就结束了。

c 复制代码
    initBoard();                    /* 先准备好棋盘数据(调 model 的函数) */
    initPlayers();                  /* 初始化两位玩家:players[0]=X,players[1]=O */
    currentPlayer = CELL_X;         /* X 先走 */
c 复制代码
        else if (screen == SCREEN_GAME) {
            showPlayerInfo();   /* 显示当前轮到谁 */
            showBoard();        /* 棋盘 */
            showGameMenu();     /* 返回主菜单按钮 */
        }

编译运行:

现在能看到轮到谁了,整个项目只差一个结算界面。

3 第五步:结算界面

依旧大致规划结算界面的位置。

和前面一样,画的功能放 view.c,加一个函数:

c 复制代码
/* 画结算界面(谁赢了 / 平局,加一个返回按钮) */
void showResult(CellType winner)
{
    if (winner == CELL_EMPTY) {
        DrawText("DRAW!", 340, 200, 50, DARKGRAY);
    } else {
        DrawText(TextFormat("%c wins!", players[winner - 1].mark), 340, 200, 50, DARKGRAY);
    }

    DrawRectangleRec(backBtn, LIGHTGRAY);
    DrawText("MENU", 370, 530, 25, DARKGRAY);
}

main.c 只改渲染一个地方:把 SCREEN_RESULT 分支从"借用棋盘画面"换成真正的结算画面:

c 复制代码
        else {   /* SCREEN_RESULT */
            showResult(winner);
        }

编译运行,一切正常。

结算界面展示:

完结撒花!

4 总结及完整代码

4.1 总结

你现在可能很激动,啥也看不进去了。看不进去就先别看了,出去走走,给你的好兄弟分享吧!

图形交互 = 每帧读取输入(位置 + 点击),判断点中了哪个矩形,然后改状态、重画。

这个游戏大的状态总共就三,一个对应一个界面,它们是这样转换的:

4.2 完整代码

view.h:

c 复制代码
#ifndef VIEW_H
#define VIEW_H

#include "raylib.h"   
#include "game.h"     

/* 当前显示哪个界面 */
typedef enum { SCREEN_MENU, SCREEN_GAME, SCREEN_RESULT } Screen;

void showMainMenu(void);
void showGameMenu(void);
void showBoard(void);
void showPlayerInfo(void);
void showResult(CellType winner);

int startBtnClicked(Vector2 mouse);
int exitBtnClicked(Vector2 mouse);
int backBtnClicked(Vector2 mouse);
int cellClicked(Vector2 mouse, int row, int col);

#endif

view.c:

c 复制代码
#include "raylib.h"
#include "view.h"

#define CELL_SIZE 120   /* 棋盘每个格子的边长 */
#define BOARD_X   220   /* 棋盘左上角的横坐标 */
#define BOARD_Y   140   /* 棋盘左上角的纵坐标 */
#define LINE_SIZE 6     /* 格线的粗细(格子之间的缝) */

/* 三个按钮的位置(x, y, 宽, 高) */
Rectangle startBtn = { 250, 250, 300, 80 };   /* 主菜单的"开始游戏" */
Rectangle exitBtn  = { 250, 360, 300, 80 };   /* 主菜单的"退出游戏" */
Rectangle backBtn  = { 300, 515, 200, 60 };   /* 游戏/结算里的"返回主菜单" */

/* 把棋盘第 row 行第 col 列,转成屏幕上的一个矩形(row、col 和 board 下标一样是 0~2) */
Rectangle cellRect(int row, int col)
{
    Rectangle rec;
    rec.x = BOARD_X + col * CELL_SIZE;
    rec.y = BOARD_Y + row * CELL_SIZE;
    rec.width = CELL_SIZE;
    rec.height = CELL_SIZE;
    return rec;
}

/* ---------- 绘图:一个函数画一个界面 ---------- */

/* 画主菜单 */
void showMainMenu(void)
{
    DrawText("Tic-Tac-Toe", 240, 110, 50, DARKGRAY);

    DrawRectangleRec(startBtn, GRAY);   /* 画"开始游戏"按钮 */
    DrawRectangleRec(exitBtn, GRAY);    /* 画"退出游戏"按钮 */

    DrawText("START", 355, 272, 30, WHITE);
    DrawText("EXIT",  360, 382, 30, WHITE);
}

/* 画游戏界面里的"返回主菜单"按钮 */
void showGameMenu(void)
{
    DrawRectangleRec(backBtn, LIGHTGRAY);
    DrawText("MENU", 370, 530, 25, DARKGRAY);
}

/* 画棋盘:深灰色大矩形当底 + 9 个浅灰色格子矩形,再画棋子 */
void showBoard(void)
{
    int row, col;

    /* 1. 深灰色大矩形:覆盖整个棋盘区域 */
    DrawRectangle(BOARD_X, BOARD_Y, 3 * CELL_SIZE, 3 * CELL_SIZE, DARKGRAY);

    /* 2. 9 个浅灰色格子:每个比一格小一圈(四周各让出 LINE_SIZE / 2),盖在深灰底上 */
    for (row = 0; row < 3; row++) {
        for (col = 0; col < 3; col++) {
            Rectangle r = cellRect(row, col);
            DrawRectangle((int)r.x + LINE_SIZE / 2,
                          (int)r.y + LINE_SIZE / 2,
                          CELL_SIZE - LINE_SIZE,
                          CELL_SIZE - LINE_SIZE,
                          LIGHTGRAY);
        }
    }

    /* 3. 棋子 */
    for (row = 0; row < 3; row++) {
        for (col = 0; col < 3; col++) {
            Rectangle r = cellRect(row, col);
            int cx = (int)(r.x + CELL_SIZE / 2) - 15;   /* 让字大概落在格子中央 */
            int cy = (int)(r.y + CELL_SIZE / 2) - 15;
            if (board[row][col] == CELL_X) {
                DrawText("X", cx, cy, 40, BLUE);
            } else if (board[row][col] == CELL_O) {
                DrawText("O", cx, cy, 40, RED);
            }
        }
    }
}

/* 画"当前轮到谁"这行提示 */
void showPlayerInfo(void)
{
    DrawText("Turn:", 260, 60, 25, DARKGRAY);
    DrawText(TextFormat("%c", players[currentPlayer - 1].mark), 350, 60, 25, RED);
}

/* 画结算界面(谁赢了 / 平局,加一个返回按钮) */
void showResult(CellType winner)
{
    if (winner == CELL_EMPTY) {
        DrawText("DRAW!", 340, 200, 50, DARKGRAY);
    } else {
        DrawText(TextFormat("%c wins!", players[winner - 1].mark), 340, 200, 50, DARKGRAY);
    }

    DrawRectangleRec(backBtn, LIGHTGRAY);
    DrawText("MENU", 370, 530, 25, DARKGRAY);
}

/* ---------- 判断鼠标点了哪里(把鼠标坐标翻译成"点中了什么") ---------- */

int startBtnClicked(Vector2 mouse) { return CheckCollisionPointRec(mouse, startBtn); }
int exitBtnClicked(Vector2 mouse)  { return CheckCollisionPointRec(mouse, exitBtn); }
int backBtnClicked(Vector2 mouse)  { return CheckCollisionPointRec(mouse, backBtn); }

int cellClicked(Vector2 mouse, int row, int col)
{
    return CheckCollisionPointRec(mouse, cellRect(row, col));
}

main.c:

c 复制代码
/* =====================================================
 * 图形版井字棋 -- 主控(Controller)
 * model(game.h / game.c)一行没改,直接复用第二章的。
 * 这里负责:开窗口、读鼠标、根据"现在在哪个界面"做决定,再调 view 画出来。
 * ===================================================== */

#include "raylib.h"
#include "game.h"
#include "view.h"

int main(void)
{
    InitWindow(800, 600, "Tic-Tac-Toe");
    SetTargetFPS(60);

    Screen screen = SCREEN_MENU;   /* 一开机,先显示主菜单 */
    CellType winner = CELL_EMPTY;  /* 谁赢了(结算界面用) */

    initBoard();                   /* 先准备好棋盘数据(调 model 的函数) */
    initPlayers();
    currentPlayer = CELL_X;        /* X 先走 */

    while (!WindowShouldClose())
    {
        /* 每一帧,先把"输入"读进来 */
        Vector2 mouse = GetMousePosition();
        int clicked = IsMouseButtonPressed(MOUSE_BUTTON_LEFT);

        /* ===== 根据当前界面,处理这次点击 ===== */
        if (screen == SCREEN_MENU)
        {
            if (clicked && startBtnClicked(mouse)) {
                initBoard();              /* 点开始:清空棋盘,重新开局 */
                currentPlayer = CELL_X;
                winner = CELL_EMPTY;
                screen = SCREEN_GAME;
            }
            else if (clicked && exitBtnClicked(mouse)) {
                break;                    /* 点退出:关程序 */
            }
        }
        else if (screen == SCREEN_GAME)
        {
            if (clicked && backBtnClicked(mouse)) {
                screen = SCREEN_MENU;     /* 点返回:不玩了,回主菜单 */
            }
            else
            {
                int row, col;
                for (row = 0; row < 3; row++) {
                    for (col = 0; col < 3; col++) {
                        /* 点中了一个"空"格子,才落子 */
                        if (clicked && cellClicked(mouse, row, col) &&
                            board[row][col] == CELL_EMPTY)
                        {
                            board[row][col] = currentPlayer;   /* 落子 */

                            if (checkWin(currentPlayer)) {     /* 赢了 */
                                winner = currentPlayer;
                                screen = SCREEN_RESULT;
                            }
                            else if (checkDraw()) {            /* 平局 */
                                winner = CELL_EMPTY;
                                screen = SCREEN_RESULT;
                            }
                            else {                             /* 没结束,换人 */
                                currentPlayer = (currentPlayer == CELL_X) ? CELL_O : CELL_X;
                            }
                        }
                    }
                }
            }
        }
        else    /* SCREEN_RESULT */
        {
            if (clicked && backBtnClicked(mouse)) {
                screen = SCREEN_MENU;     /* 点返回:回主菜单 */
            }
        }

        /* ===== 根据当前界面,画这一帧 ===== */
        BeginDrawing();
        ClearBackground(RAYWHITE);

        if (screen == SCREEN_MENU) {
            showMainMenu();
        }
        else if (screen == SCREEN_GAME) {
            showPlayerInfo();   /* 显示当前轮到谁 */
            showBoard();        /* 显示棋盘 */
            showGameMenu();     /* 显示"返回主菜单"按钮 */
        }
        else {   /* SCREEN_RESULT */
            showResult(winner);
        }

        EndDrawing();
    }

    CloseWindow();
    return 0;
}

game.h :

c 复制代码
#ifndef GAME_H
#define GAME_H

/* 棋子的类型:空格、X、O */
typedef enum { CELL_EMPTY = 0, CELL_X = 1, CELL_O = 2 } CellType;

/* 玩家信息:用的棋子 + 显示符号 */
typedef struct {
    CellType piece;   /* 棋子在棋盘上的值:CELL_X 或 CELL_O */
    char mark;        /* 显示用的符号:'X' 或 'O' */
} PlayerInfo;

/* 落子位置:行号 + 列号 */
typedef struct {
    int row;   /* 行号 0~2 */
    int col;   /* 列号 0~2 */
} Position;

/* 全局变量:真正的"实体"在 game.c 里定义。
 * 其他文件想用它们,只要 include 本文件(extern 声明),就能看见。 */
extern CellType board[3][3];
extern PlayerInfo players[2];
extern CellType currentPlayer;

/* 函数声明:告诉编译器"这些函数存在,实现写在 game.c 里" */
void initBoard(void);
void initPlayers(void);
int checkWin(CellType player); // 这里把参数从上一章的 int改成了CellType。
int checkDraw(void);

#endif

game.c :

c 复制代码
/* =====================================================
 * 3-1 多文件井字棋 -- 模型实现(Model)
 * 只关心"数据"和"规则",完全不负责显示。
 * ===================================================== */

#include "game.h"

/* 全局变量的真正定义在这里 */
CellType board[3][3];
PlayerInfo players[2];
CellType currentPlayer;

/* 把棋盘全部清空 */
void initBoard(void)
{
    int row, col;

    for (row = 0; row < 3; row = row + 1) {
        for (col = 0; col < 3; col = col + 1) {
            board[row][col] = CELL_EMPTY;
        }
    }
}

/* 初始化两位玩家的信息 */
void initPlayers(void)
{
    players[0].piece = CELL_X;
    players[0].mark = 'X';
    players[1].piece = CELL_O;
    players[1].mark = 'O';
}

/* 判断指定玩家是否赢了,赢了返回 1,否则返回 0 */
int checkWin(CellType player)
{
    int i;
    /* 检查每一行 */
    for (i = 0; i < 3; i = i + 1) {
        if (board[i][0] == player && board[i][1] == player && board[i][2] == player) {
            return 1;
        }
    }

    /* 检查每一列 */
    for (i = 0; i < 3; i = i + 1) {
        if (board[0][i] == player && board[1][i] == player && board[2][i] == player) {
            return 1;
        }
    }

    /* 检查两条对角线 */
    if (board[0][0] == player && board[1][1] == player && board[2][2] == player) {
        return 1;
    }
    if (board[0][2] == player && board[1][1] == player && board[2][0] == player) {
        return 1;
    }

    return 0;
}

/* 判断是否平局:棋盘全部下满且没有人赢 */
int checkDraw(void)
{
    int row, col;

    for (row = 0; row < 3; row = row + 1) {
        for (col = 0; col < 3; col = col + 1) {
            if (board[row][col] == CELL_EMPTY) {
                return 0;   /* 还有空位置,不算平局 */
            }
        }
    }
    return 1;   /* 全部下满,平局 */
}

小爷学了这么久编程,终于做出来一个像软件的东西了!

相关推荐
从小白到软工2 小时前
3-8 用draw.io来绘制界面*
c
C++ 老炮儿的技术栈1 天前
char 造成的数据溢出
linux·服务器·c语言·c++·算法·c
从小白到软工2 天前
3-5 画矩形和按钮
c
从小白到软工2 天前
3-6 项目-图形版井字棋(上)
c
书生执笔画浮沉2 天前
Linux 内核模块为什么不能自己解决兼容性问题?
linux·c·kernel·update·灵感
书生执笔画浮沉6 天前
分享一个开源小工具CAdapt
linux·c·gnu·glibc·kernel·升级·灵感
Sylvia-girl6 天前
堆排序算法
算法·排序算法·c
liulilittle7 天前
Linux 下 select 测试函数
linux·服务器·网络·数据库·c++·select·c
从小白到软工7 天前
2-6 将井字棋拆分成多个文件
c