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; /* 全部下满,平局 */
}
小爷学了这么久编程,终于做出来一个像软件的东西了!