C语言项目实战:从0到1开发经典三子棋小游戏

1 阅读30分钟

🧑 博主简介:现任阿里巴巴嵌入式技术专家,15年工作经验,深耕嵌入式+人工智能领域,精通嵌入式领域开发、技术管理、简历招聘面试。CSDN优质创作者,全网11W+粉丝博主,提供产品测评、学习辅导、简历面试辅导、毕设辅导、项目开发、C/C++/Java/Python/Linux/AI等方面的服务,同时还运营着十几个不同主题的技术交流群,如有需要请站内私信或者联系VX(gylzbk),互相学习共同进步。

一、项目背景:为什么选择三子棋作为 C 语言入门案例

image.png

在 C 语言的学习旅程中,实战项目是提升编程技能的关键一环。对于初学者而言,选择一个合适的项目至关重要,而三子棋游戏正是这样一个理想之选。它以简单的规则和清晰的逻辑,成为了踏入 C 语言编程世界的绝佳切入点。

三子棋的规则简洁明了,在一个 3×3 的棋盘上,玩家与电脑交替落子,率先将自己的三个棋子连成直线(包括横线、竖线和对角线)的一方获胜;若棋盘被填满且双方均未达成胜利条件,则判定为平局。这种简单易懂的规则,让初学者能够迅速理解游戏的逻辑,将更多的精力集中在 C 语言的编程实现上。

从编程知识的角度来看,实现三子棋游戏涵盖了 C 语言的多个核心知识点。其中,二维数组是构建棋盘的基础,通过对二维数组的操作,我们能够有效地存储和管理棋盘上每个位置的状态。函数模块化设计则是实现游戏功能的关键,将游戏的各个功能模块(如初始化棋盘、打印棋盘、玩家下棋、电脑下棋、判断胜负等)封装成独立的函数,不仅提高了代码的可读性和可维护性,还培养了良好的编程习惯。人机交互逻辑的实现,让玩家能够与程序进行互动,增加了游戏的趣味性和实用性。

完成三子棋项目,会给初学者带来强烈的成就感,从而进一步激发对编程的兴趣。当看到自己编写的代码能够实现一个完整的游戏,那种喜悦和满足感将成为继续学习编程的强大动力。

接下来,本文将详细讲解如何用 C 语言实现一个基础版的三子棋游戏,支持人机对战(电脑随机落子)。希望通过这个项目,能够帮助 C 语言初学者更好地掌握 C 语言的基础知识和编程技巧,开启精彩的编程之旅。

二、项目架构:模块化设计提升代码可维护性

在 C 语言编程中,合理的项目架构是构建高质量程序的基础。对于三子棋游戏项目,采用模块化设计不仅能使代码结构清晰,还能显著提升代码的可维护性和可扩展性。下面,我们将详细介绍该项目的文件结构规划以及核心常量定义。

(一)文件结构规划

本项目主要由三个文件构成,分别是test.c、game.h和game.c,它们各自承担着不同的职责,相互协作,共同实现三子棋游戏的功能。

  • test.c:作为游戏测试入口,包含了主函数和菜单逻辑。主函数负责程序的整体流程控制,通过调用其他模块的函数来实现游戏的初始化、运行和结束。菜单逻辑则为玩家提供了交互界面,让玩家可以选择开始游戏或退出游戏。在test.c中,我们可以看到如下代码片段:

    #include "game.h" void menu() { printf("*************************\n"); printf("*****1.play *\n"); printf("*******0.exit *\n"); printf("***************************\n"); } void game() { // 游戏实现逻辑 } int main() { int input = 0; srand((unsigned int)time(NULL)); do { menu(); printf("请选择:>"); scanf("%d", &input); switch (input) { case 1: game(); break; case 0: printf("退出游戏\n"); break; default: printf("输入错误,请重新输入\n"); break; } } while (input); return 0; }

这段代码展示了test.c的基本结构,通过menu函数打印菜单,game函数实现游戏功能,main函数控制整个程序的流程,根据玩家的选择执行相应的操作。

  • game.h:这是一个头文件,主要声明了游戏相关的函数,定义了常量和结构体。通过将函数声明和常量定义放在头文件中,可以方便其他文件引用,提高代码的可复用性。在game.h中,我们定义了如下内容:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 3
#define COL 3
// 函数声明
void InitBoard(char board[ROW][COL], int row, int col);
void DisplayBoard(char board[ROW][COL], int row, int col);
void PlayerMove(char board[ROW][COL], int row, int col);
void ComputerMove(char board[ROW][COL], int row, int col);
char IsWin(char board[ROW][COL], int row, int col);
int IsFull(char board[ROW][COL], int row, int col);

在这个头文件中,我们首先使用#pragma once来防止头文件被重复包含。然后定义了棋盘的行数ROW和列数COL为 3。接着声明了一系列函数,这些函数将在game.c中实现,分别用于初始化棋盘、显示棋盘、玩家下棋、电脑下棋、判断胜负以及判断棋盘是否已满。

  • game.c:是具体函数实现文件,包含了棋盘初始化、落子、胜负判断等核心功能。在这个文件中,我们实现了game.h中声明的各个函数,例如棋盘初始化函数InitBoard的实现如下:
#include "game.h"
void InitBoard(char board[ROW][COL], int row, int col) {
    int i = 0, j = 0;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            board[i][j] ='';
        }
    }
}

这个函数通过双重循环遍历二维数组board,将每个元素初始化为空格,表示棋盘上的空位。这样,在游戏开始时,棋盘上没有任何棋子,为玩家和电脑下棋做好准备。通过这种模块化的文件结构规划,每个文件专注于实现特定的功能,使得代码结构清晰,易于理解和维护。当需要修改或扩展游戏功能时,可以方便地在相应的文件中进行操作,而不会影响到其他部分的代码。

(二)核心常量定义

在三子棋游戏中,我们通过定义常量来表示棋盘的尺寸,这样做有诸多好处。一方面,使用常量可以提高代码的可读性,让其他开发者能够一目了然地了解棋盘的大小;另一方面,当需要修改棋盘尺寸时,只需要在常量定义处进行修改,而不需要在整个代码中查找和替换所有使用棋盘尺寸的地方,从而降低了代码维护的难度。同时,这种方式也方便后续将游戏扩展为五子棋或其他规格,只需要简单地修改常量值即可。在game.h头文件中,我们定义了如下常量:

#define ROW 3
#define COL 3

这里将棋盘的行数ROW和列数COL都定义为 3,这是传统三子棋的棋盘规格。在游戏的各个功能实现中,我们都会使用这两个常量来操作棋盘,例如在初始化棋盘函数InitBoard中:

void InitBoard(char board[ROW][COL], int row, int col) {
    int i = 0, j = 0;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            board[i][j] ='';
        }
    }
}

通过使用ROW和COL常量,我们可以确保棋盘的初始化操作与棋盘的尺寸定义保持一致。如果后续需要将游戏扩展为五子棋,只需要将ROW和COL的值修改为 5,其他与棋盘尺寸相关的代码逻辑都不需要做过多调整,大大提高了代码的可扩展性。这种基于常量定义的方式,不仅在三子棋游戏中具有重要意义,在其他各种编程项目中也是一种良好的编程习惯,能够有效提高代码的质量和可维护性。

三、功能实现:分步骤解析核心逻辑

(一)交互模块:打造友好的游戏菜单

在三子棋游戏中,交互模块是玩家与游戏程序进行沟通的桥梁,而游戏菜单则是这个交互过程的起点。通过精心设计的游戏菜单,玩家可以轻松地选择开始游戏或退出游戏,使得游戏的操作更加便捷和直观。

在 C 语言中,我们可以使用循环结构来实现菜单的交互功能。以test.c文件中的代码为例,在main函数中,通过do-while循环来不断显示菜单,直到玩家选择退出游戏。在循环内部,使用switch语句来处理玩家的选择:

#include "game.h"
void menu() {
    printf("***************************\n");
    printf("*******1.play     *****\n");
    printf("*******0.exit     *****\n");
    printf("***************************\n");
}
void game() {
    // 游戏实现逻辑
}
int main() {
    int input = 0;
    srand((unsigned int)time(NULL));
    do {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        switch (input) {
        case 1:
            game();
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default:
            printf("输入错误,请重新输入\n");
            break;
        }
    } while (input);
    return 0;
}

在这段代码中,menu函数负责打印出游戏菜单,向玩家展示可供选择的操作。main函数中的do-while循环会持续执行,不断显示菜单并等待玩家输入。当玩家输入选择后,switch语句会根据输入的值进行判断。如果玩家选择 1,就会调用game函数开始游戏;如果选择 0,则打印 “退出游戏” 并结束循环,从而退出程序;如果输入其他值,会提示 “输入错误,请重新输入”,然后继续循环,让玩家重新输入选择。这种设计确保了玩家可以重复操作,方便快捷地进行游戏或退出。同时,对于非法输入的提示,增强了用户体验,使游戏更加友好和易用。整体逻辑清晰易读,符合模块化设计原则,将菜单显示、玩家选择处理等功能分别封装在不同的函数中,提高了代码的可读性和可维护性。

(二)棋盘模块:二维数组实现棋盘存储

棋盘是三子棋游戏的核心载体,它承载着游戏的状态和进程。在 C 语言中,我们巧妙地运用二维数组来实现棋盘的存储,这种数据结构能够直观地表示棋盘上每个位置的状态,为游戏的逻辑实现提供了坚实的基础。

  1. 初始化棋盘:在游戏开始前,我们需要对棋盘进行初始化,确保棋盘上的每个位置都是空的,为后续的落子操作做好准备。使用双重循环可以轻松地将二维数组的每个元素初始化为空格字符。在game.c文件中,InitBoard函数的实现如下:
#include "game.h"
void InitBoard(char board[ROW][COL], int row, int col) {
    int i = 0, j = 0;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            board[i][j] =' ';
        }
    }
}

在这个函数中,通过外层循环控制行,内层循环控制列,遍历二维数组board的每一个元素,并将其赋值为空格字符' '。这样,当游戏开始时,棋盘上没有任何棋子,处于初始的空白状态,等待玩家和电脑的落子。

  1. 打印棋盘:为了让玩家能够直观地看到棋盘的状态,我们需要实现一个打印棋盘的函数。该函数不仅要打印出棋盘上的棋子,还要包含行列分隔线,使棋盘的布局更加清晰明了。在game.c文件中,DisplayBoard函数实现了这一功能,并且采用了动态格式打印,支持任意尺寸棋盘(此处以 3x3 为例):
#include "game.h"
void DisplayBoard(char board[ROW][COL], int row, int col) {
    int i = 0;
    for (i = 0; i < row; i++) {
        int j = 0;
        for (j = 0; j < col; j++) {
            printf(" %c ", board[i][j]);
            if (j < col - 1) {
                printf("|");
            }
        }
        printf("\n");
        if (i < row - 1) {
            for (j = 0; j < col; j++) {
                printf("---");
                if (j < col - 1) {
                    printf("|");
                }
            }
            printf("\n");
        }
    }
}

在这个函数中,外层循环控制行的打印。对于每一行,首先通过内层循环打印该行的棋子,并在棋子之间打印竖线分隔符(除了最后一个棋子)。打印完一行棋子后,换行。然后,通过另一个内层循环打印行分隔线,同样在分隔线之间打印竖线分隔符(除了最后一个分隔线),并在打印完行分隔线后换行。通过这种方式,能够清晰地打印出棋盘的布局,使玩家能够清楚地看到棋盘上的棋子分布情况。这种动态逻辑的设计使得该函数可以适配不同尺寸的棋盘,只需要修改ROW和COL常量的值,就可以实现不同规格棋盘的打印,为后续游戏逻辑的扩展提供了便利。棋盘模块通过二维数组存储棋子状态,初始化函数确保棋盘清空,打印函数使用动态逻辑适配不同尺寸,为后续游戏逻辑提供了基础数据结构支持,体现了 C 语言数组操作的核心应用。它不仅在三子棋游戏中起着关键作用,也是理解和掌握 C 语言二维数组操作的重要实践案例。

(三)对战模块:人机交互逻辑实现

对战模块是三子棋游戏的核心部分,它实现了玩家与电脑之间的交互对战逻辑。在这个模块中,我们分别处理玩家落子和电脑落子的逻辑,通过严谨的条件判断和输入输出操作,确保游戏的公平性和趣味性。

  1. 玩家落子:玩家落子是玩家与游戏进行交互的重要环节。在game.c文件中,PlayerMove函数负责获取玩家输入的坐标,并对其进行合法性检查。具体实现如下:
#include "game.h"
void PlayerMove(char board[ROW][COL], int row, int col) {
    int x = 0, y = 0;
    printf("玩家落子\n");
    while (1) {
        printf("请输入坐标:>");
        scanf("%d %d", &x, &y);
        if (x >= 1 && x <= row && y >= 1 && y <= col) {
            if (board[x - 1][y - 1] ==' ') {
                board[x - 1][y - 1] = '*';
                break;
            }
            else {
                printf("该坐标被占用,请重新输入\n");
            }
        }
        else {
            printf("输入非法,重新输入\n");
        }
    }
}

在这个函数中,首先定义变量x和y用于存储玩家输入的坐标。然后,通过一个无限循环while (1)来持续获取玩家输入,直到玩家输入合法的坐标并成功落子。在循环内部,提示玩家输入坐标,使用scanf函数获取玩家输入的坐标值。接着,对输入的坐标进行合法性检查,判断坐标是否在棋盘范围内(即x >= 1 && x <= row && y >= 1 && y <= col)。如果坐标在范围内,再进一步检查该位置是否为空(即board[x - 1][y - 1] ==' ')。如果为空,则将玩家的棋子标记为'*',并使用break语句跳出循环,表示玩家成功落子;如果该位置已被占用,则提示 “该坐标被占用,请重新输入”。如果输入的坐标不在棋盘范围内,则提示 “输入非法,重新输入”。通过这样的逻辑,确保了玩家输入的坐标合法且落子位置有效,保证了游戏的正常进行。

  1. 电脑落子:电脑落子是实现人机对战的关键部分。为了增加游戏的趣味性和挑战性,我们利用rand()函数生成随机坐标,让电脑随机落子。在game.c文件中,ComputerMove函数实现了这一功能,具体代码如下:
#include "game.h"
void ComputerMove(char board[ROW][COL], int row, int col) {
    int x = 0, y = 0;
    printf("电脑下棋:>\n");
    while (1) {
        x = rand() % row;
        y = rand() % col;
        if (board[x][y] ==' ') {
            board[x][y] = '#';
            break;
        }
    }
}

在这个函数中,同样定义变量x和y用于存储电脑落子的坐标。通过一个无限循环while (1)来持续生成随机坐标,直到电脑找到一个空位置并成功落子。在循环内部,使用rand() % row和rand() % col分别生成 0 到row - 1和 0 到col - 1之间的随机数,作为电脑落子的坐标。然后,检查该坐标对应的位置是否为空(即board[x][y] ==' ')。如果为空,则将电脑的棋子标记为'#',并使用break语句跳出循环,表示电脑成功落子。通过这种方式,实现了电脑的随机落子功能,为玩家提供了一个具有挑战性的对手。对战模块区分玩家和电脑的落子逻辑,玩家输入需进行合法性校验,电脑落子采用随机算法,两者均需检查空位置以确保游戏规则正确。该模块体现了 C 语言输入输出、随机数生成和条件判断的综合应用,是三子棋游戏实现人机对战的核心部分,为玩家带来了丰富的游戏体验。

(四)胜负判断:多条件检测游戏状态

胜负判断是三子棋游戏的关键环节,它决定了游戏的走向和最终结果。在这个模块中,我们通过多条件检测来判断游戏是否结束,并确定获胜方或平局情况。这种设计确保了每一步落子后都能及时更新游戏状态,使游戏的进程更加流畅和公平。

  1. 行 / 列 / 对角线胜利检测:在game.c文件中,IsWin函数负责检查是否有玩家或电脑在任意行、列或对角线形成三子连线,具体实现如下:
#include "game.h"
char IsWin(char board[ROW][COL], int row, int col) {
    int i = 0;
    // 判断行
    for (i = 0; i < row; i++) {
        if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1]!=' ') {
            return board[i][1];
        }
    }
    // 判断列
    for (i = 0; i < col; i++) {
        if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i]!=' ') {
            return board[1][i];
        }
    }
    // 判断对角线
    if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1]!=' ') {
        return board[1][1];
    }
    if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1]!=' ') {
        return board[1][1];
    }
    // 判断平局
    if (IsFull(board, row, col)) {
        return 'Q';
    }
    return 'C';
}
int IsFull(char board[ROW][COL], int row, int col) {
    int i = 0, j = 0;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            if (board[i][j] ==' ') {
                return 0;
            }
        }
    }
    return 1;
}

在IsWin函数中,首先通过两层循环判断行。外层循环遍历每一行,内层循环判断该行的三个元素是否相等且不为空格。如果满足条件,则返回该行的棋子字符,表示该行的玩家或电脑获胜。接着,通过类似的两层循环判断列。外层循环遍历每一列,内层循环判断该列的三个元素是否相等且不为空格。如果满足条件,则返回该列的棋子字符,表示该列的玩家或电脑获胜。然后,分别判断两条对角线。如果主对角线(从左上角到右下角)或副对角线(从右上角到左下角)的三个元素相等且不为空格,则返回对角线上的棋子字符,表示该对角线的玩家或电脑获胜。最后,调用IsFull函数判断棋盘是否已满。如果棋盘已满且没有玩家或电脑获胜,则返回'Q'表示平局;否则,返回'C'表示游戏继续。IsFull函数通过两层循环遍历棋盘的每一个元素,判断是否存在空格。如果存在空格,则返回 0,表示棋盘未满;如果所有元素都不为空格,则返回 1,表示棋盘已满。胜负判断模块通过多重条件检测实现游戏状态更新,包含胜利检测和平局检测,返回不同标识(玩家胜、电脑胜、平局、继续),确保每一步落子后及时更新游戏状态,体现了 C 语言逻辑判断和函数封装的重要性。它不仅是三子棋游戏的核心功能之一,也是学习 C 语言条件判断和函数设计的重要实践案例。

四、完整代码解析

本文完整代码包含详细注释和模块化实现,欢迎学习。

代码结构说明

  1. test.c:主函数入口,菜单驱动游戏流程。通过main函数控制整个程序的运行,其中包含游戏菜单的显示和玩家选择的处理逻辑。当玩家选择开始游戏时,调用game函数进入游戏环节;当玩家选择退出游戏时,程序结束运行。
#include "game.h"
void menu() {
    printf("***************************\n");
    printf("*******1.play     *****\n");
    printf("*******0.exit     *****\n");
    printf("***************************\n");
}
void game() {
    char board[ROW][COL];
    InitBoard(board, ROW, COL);
    DisplayBoard(board, ROW, COL);
    char ret = 0;
    while (1) {
        PlayerMove(board, ROW, COL);
        DisplayBoard(board, ROW, COL);
        ret = IsWin(board, ROW, COL);
        if (ret != 'C') {
            break;
        }
        ComputerMove(board, ROW, COL);
        DisplayBoard(board, ROW, COL);
        ret = IsWin(board, ROW, COL);
        if (ret != 'C') {
            break;
        }
    }
    if (ret == '*') {
        printf("玩家胜利\n");
    }
    else if (ret == '#') {
        printf("电脑胜利\n");
    }
    else {
        printf("平局\n");
    }
}
int main() {
    int input = 0;
    srand((unsigned int)time(NULL));
    do {
        menu();
        printf("请选择:>");
        scanf("%d", &input);
        switch (input) {
        case 1:
            game();
            break;
        case 0:
            printf("退出游戏\n");
            break;
        default:
            printf("输入错误,请重新输入\n");
            break;
        }
    } while (input);
    return 0;
}

2. game.h:声明函数、定义常量,包含必要头文件。在这个头文件中,定义了棋盘的行数ROW和列数COL,并声明了初始化棋盘、显示棋盘、玩家下棋、电脑下棋、判断胜负等函数,同时包含了stdio.h、stdlib.h和time.h等头文件,为游戏的实现提供了必要的支持。

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 3
#define COL 3
// 函数声明
void InitBoard(char board[ROW][COL], int row, int col);
void DisplayBoard(char board[ROW][COL], int row, int col);
void PlayerMove(char board[ROW][COL], int row, int col);
void ComputerMove(char board[ROW][COL], int row, int col);
char IsWin(char board[ROW][COL], int row, int col);
int IsFull(char board[ROW][COL], int row, int col);

3. game.c:实现棋盘操作、落子逻辑、胜负判断等核心功能。在这个文件中,实现了game.h中声明的各个函数。例如,InitBoard函数用于初始化棋盘,将棋盘上的每个位置都设置为空字符;PlayerMove函数实现玩家落子的逻辑,获取玩家输入的坐标并进行合法性检查,若坐标合法且该位置为空,则玩家成功落子;IsWin函数判断游戏是否结束,并返回相应的结果,包括玩家胜利、电脑胜利、平局或游戏继续。

#include "game.h"
void InitBoard(char board[ROW][COL], int row, int col) {
    int i = 0, j = 0;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            board[i][j] =' ';
        }
    }
}
void DisplayBoard(char board[ROW][COL], int row, int col) {
    int i = 0;
    for (i = 0; i < row; i++) {
        int j = 0;
        for (j = 0; j < col; j++) {
            printf(" %c ", board[i][j]);
            if (j < col - 1) {
                printf("|");
            }
        }
        printf("\n");
        if (i < row - 1) {
            for (j = 0; j < col; j++) {
                printf("---");
                if (j < col - 1) {
                    printf("|");
                }
            }
            printf("\n");
        }
    }
}
void PlayerMove(char board[ROW][COL], int row, int col) {
    int x = 0, y = 0;
    printf("玩家落子\n");
    while (1) {
        printf("请输入坐标:>");
        scanf("%d %d", &x, &y);
        if (x >= 1 && x <= row && y >= 1 && y <= col) {
            if (board[x - 1][y - 1] ==' ') {
                board[x - 1][y - 1] = '*';
                break;
            }
            else {
                printf("该坐标被占用,请重新输入\n");
            }
        }
        else {
            printf("输入非法,重新输入\n");
        }
    }
}
void ComputerMove(char board[ROW][COL], int row, int col) {
    int x = 0, y = 0;
    printf("电脑下棋:>\n");
    while (1) {
        x = rand() % row;
        y = rand() % col;
        if (board[x][y] ==' ') {
            board[x][y] = '#';
            break;
        }
    }
}
int IsFull(char board[ROW][COL], int row, int col) {
    int i = 0, j = 0;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            if (board[i][j] ==' ') {
                return 0;
            }
        }
    }
    return 1;
}
char IsWin(char board[ROW][COL], int row, int col) {
    int i = 0;
    // 判断行
    for (i = 0; i < row; i++) {
        if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][1]!=' ') {
            return board[i][1];
        }
    }
    // 判断列
    for (i = 0; i < col; i++) {
        if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[1][i]!=' ') {
            return board[1][i];
        }
    }
    // 判断对角线
    if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[1][1]!=' ') {
        return board[1][1];
    }
    if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[1][1]!=' ') {
        return board[1][1];
    }
    // 判断平局
    if (IsFull(board, row, col)) {
        return 'Q';
    }
    return 'C';
}

通过上述代码结构,我们实现了一个完整的三子棋游戏,各个模块之间分工明确,相互协作,使得游戏的逻辑清晰,易于理解和维护。希望这份代码能够帮助大家更好地理解 C 语言的编程实践,欢迎大家在 Gitee 仓库中进行学习和交流,共同进步!

五、运行演示:手把手教你验证游戏逻辑

现在,让我们通过实际的运行演示,来深入体验这个三子棋游戏的乐趣。在运行游戏之前,请确保你的开发环境已经配置好,这里以 GCC 编译器为例进行演示。

(一)编译运行

在终端中执行以下命令进行编译(假设你的源文件分别为test.c、game.h和game.c):

gcc test.c game.c -o tictactoe

这个命令会将test.c和game.c文件编译并链接成一个可执行文件tictactoe。其中,gcc是 GCC 编译器的命令,test.c和game.c是需要编译的源文件,-o选项用于指定生成的可执行文件的名称为tictactoe。如果编译过程中没有出现错误,将会在当前目录下生成tictactoe可执行文件。

(二)操作演示

  1. 开始游戏:在终端中输入./tictactoe运行游戏,首先会显示游戏菜单:
***************************
*******1.play     *****\n
*******0.exit     *****\n
***************************
请选择:>

输入1并回车,即可开始游戏。此时,会看到初始化后的棋盘:

   |   |   
---|---|---
   |   |   
---|---|---
   |   |   

2. 玩家落子:输入1 1并回车(表示在第一行第一列落子),玩家的棋子*会出现在相应位置,棋盘更新为:

 * |   |   
---|---|---
   |   |   
---|---|---
   |   |   

3. 电脑落子:电脑会随机在某个空位置落子,假设电脑在第二行第二列落子,棋盘显示如下:

 * |   |   
---|---|---
   | # |   
---|---|---
   |   |   

4. 游戏进行中:玩家和电脑继续交替落子,每次落子后棋盘都会更新显示。例如,玩家再次落子在第一行第三列,棋盘变为:

 * |   | * 
---|---|---
   | # |   
---|---|---
   |   |   

5. 游戏结束:游戏会实时判断胜负和平局情况。如果玩家率先连成三子(如在第一行连成*),游戏结束,显示 “玩家胜利”:

 * | * | * 
---|---|---
   | # |   
---|---|---
   |   |   
玩家胜利

如果棋盘被填满且双方均未达成胜利条件,则判定为平局,显示 “平局”:

 * | # | * 
---|---|---
 # | * | # 
---|---|---
 * | # | * 
平局

如果电脑率先连成三子,则显示 “电脑胜利”。游戏结束后,会再次显示游戏菜单,玩家可以选择重新开始游戏(输入1)或退出游戏(输入0)。通过以上运行演示,我们可以直观地看到三子棋游戏的完整流程,从初始化到玩家与电脑的对战,再到游戏结果的判定,每个环节都紧密相连,充分展示了 C 语言在实现简单游戏逻辑方面的强大能力。希望大家通过实际操作,更好地理解和掌握这个三子棋游戏的实现原理,为进一步学习 C 语言编程打下坚实的基础。

六、项目扩展:进阶优化方向建议

完成基础三子棋项目后,我们可以通过一系列进阶优化,让游戏更加完善和有趣。这些优化方向不仅能提升游戏的可玩性,还能帮助我们进一步加深对 C 语言编程的理解和应用。

(一)AI 优化:提升电脑智能水平

当前电脑落子采用随机策略,游戏挑战性不足。我们可以实现基础 AI 算法,提升电脑的智能水平,增强游戏的趣味性。例如,让电脑优先占据中心位置,因为中心位置在三子棋中具有战略优势,能增加获胜的可能性。在ComputerMove函数中,添加如下逻辑:

void ComputerMove(char board[ROW][COL], int row, int col) {
    int x = 0, y = 0;
    printf("电脑下棋:>\n");
    // 优先占据中心
    if (board[row / 2][col / 2] ==' ') {
        board[row / 2][col / 2] = '#';
        return;
    }
    while (1) {
        x = rand() % row;
        y = rand() % col;
        if (board[x][y] ==' ') {
            board[x][y] = '#';
            break;
        }
    }
}

此外,电脑还应具备阻止玩家胜利的能力。在每次电脑落子前,检查玩家是否有即将连成三子的趋势,若有,则在相应位置落子进行阻止。这需要在ComputerMove函数中增加对玩家落子趋势的判断逻辑。例如,遍历棋盘的行、列和对角线,检查玩家是否已经有两个棋子在同一条线上,若存在这种情况,则在这条线上的空位置落子。通过这些优化,电脑的下棋策略更加智能,能与玩家展开更激烈的对抗,使游戏更具挑战性。

(二)界面优化:提升视觉体验

当前游戏使用控制台输出,界面较为单调。为了提升视觉体验,我们可以考虑增加图形化界面。在 C 语言中,可借助图形库来实现这一目标,例如 Curses 库。Curses 库提供了一组函数和数据结构,用于创建文本模式下的图形界面。以下是使用 Curses 库创建一个简单图形化三子棋界面的示例代码:

#include <ncurses.h>
#include "game.h"
void drawBoard(char board[ROW][COL], int row, int col) {
    initscr();
    start_color();
    init_pair(1, COLOR_RED, COLOR_BLACK); // 玩家棋子颜色
    init_pair(2, COLOR_BLUE, COLOR_BLACK); // 电脑棋子颜色
    curs_set(0); // 隐藏光标
    int i, j;
    for (i = 0; i < row; i++) {
        for (j = 0; j < col; j++) {
            move(i * 2, j * 4);
            if (board[i][j] == '*') {
                attron(COLOR_PAIR(1));
                printw(" * ");
                attroff(COLOR_PAIR(1));
            } else if (board[i][j] == '#') {
                attron(COLOR_PAIR(2));
                printw(" # ");
                attroff(COLOR_PAIR(2));
            } else {
                printw("   ");
            }
            if (j < col - 1) {
                printw("|");
            }
        }
        printw("\n");
        if (i < row - 1) {
            for (j = 0; j < col; j++) {
                printw("---");
                if (j < col - 1) {
                    printw("|");
                }
            }
            printw("\n");
        }
    }
    refresh();
}

在main函数中,将原来的DisplayBoard函数调用替换为drawBoard函数调用,即可实现图形化界面的显示。除了使用图形库,还可以对控制台输出格式进行优化,使棋盘布局更加美观。例如,调整棋子和分隔线的显示格式,增加颜色输出(在支持 ANSI 转义序列的终端中)。通过这些界面优化措施,能够显著提升游戏的视觉效果,为玩家带来更好的游戏体验。

(三)网络对战:拓展游戏玩法

目前游戏仅支持人机对战,我们可以将其扩展为双人对战模式,支持本地或网络联机,进一步丰富游戏玩法。要实现网络对战,需要涉及 Socket 编程和网络通信协议。在服务器端,使用socket函数创建套接字,绑定 IP 地址和端口号,然后使用listen函数监听客户端的连接请求。当有客户端连接时,使用accept函数接受连接,并创建新的套接字用于与客户端通信。在客户端,同样使用socket函数创建套接字,然后使用connect函数连接到服务器。连接建立后,双方可以通过send和recv函数进行数据传输,例如发送落子的位置信息,接收对方落子的位置并更新棋盘。以下是一个简单的服务器端代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8888
#define MAX_CLIENTS 2
int main() {
    int server_socket, client_socket[MAX_CLIENTS];
    struct sockaddr_in server_addr, client_addr[MAX_CLIENTS];
    socklen_t client_addr_len[MAX_CLIENTS];
    int client_count = 0;
    // 创建服务器套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    // 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    // 绑定套接字到地址
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind");
        close(server_socket);
        exit(EXIT_FAILURE);
    }
    // 监听连接请求
    if (listen(server_socket, MAX_CLIENTS) == -1) {
        perror("listen");
        close(server_socket);
        exit(EXIT_FAILURE);
    }
    printf("Waiting for clients to connect...\n");
    // 接受客户端连接
    while (client_count < MAX_CLIENTS) {
        client_socket[client_count] = accept(server_socket, (struct sockaddr *)&client_addr[client_count], &client_addr_len[client_count]);
        if (client_socket[client_count] == -1) {
            perror("accept");
            continue;
        }
        printf("Client %d connected\n", client_count + 1);
        client_count++;
    }
    // 这里开始处理两个客户端之间的通信,例如转发落子信息等
    // 关闭套接字
    for (int i = 0; i < client_count; i++) {
        close(client_socket[i]);
    }
    close(server_socket);
    return 0;
}

客户端代码也类似,需要创建套接字并连接到服务器,然后在游戏过程中与服务器进行数据交互。通过实现网络对战功能,玩家可以与远方的朋友进行对战,大大增加了游戏的互动性和趣味性。

七、总结:从项目中掌握 C 语言核心技能

通过三子棋项目,我们深入实践了以下 C 语言关键知识点:

  1. 二维数组的定义与操作:深刻理解了如何利用二维数组来构建和管理棋盘,清晰掌握了二维数组的初始化、元素访问以及遍历方法。这不仅帮助我们解决了三子棋游戏中棋盘状态存储和更新的问题,更让我们体会到数据结构在实际编程中的重要性,为今后处理更复杂的数据关系奠定了基础。
  1. 函数模块化设计:学会了将复杂的游戏逻辑拆解为多个独立的功能模块,并封装成函数。每个函数专注于实现一个特定的功能,如初始化棋盘、打印棋盘、玩家下棋、电脑下棋、判断胜负等。这种模块化的设计方式使得代码结构清晰,易于理解、维护和扩展。同时,也培养了我们良好的编程习惯,提高了代码的复用性和可维护性。
  1. 输入输出处理与用户交互逻辑:熟练掌握了控制台程序的输入输出操作,能够准确获取玩家的输入,并根据输入做出相应的处理。通过合理设计用户交互逻辑,如菜单显示、提示信息输出、输入合法性校验等,为玩家提供了友好的游戏体验。这也让我们意识到在开发程序时,用户体验的重要性,学会从用户的角度出发思考问题。
  1. 随机数生成、条件判断、循环结构等基础语法的综合运用:在电脑落子的实现中,运用rand()函数生成随机坐标,让电脑能够随机落子,增加了游戏的趣味性和挑战性。在游戏的各个环节,广泛使用条件判断语句(如if - else、switch)来判断各种条件,决定程序的执行流程。同时,通过循环结构(如for、while、do - while)实现了游戏的重复操作和持续进行。这些基础语法的综合运用,让我们更加熟练地掌握了 C 语言的基本编程技巧,能够灵活运用这些语法来解决实际问题。

作为入门级项目,本文实现的三子棋游戏为后续学习更复杂的 C 语言项目(如扫雷、俄罗斯方块)奠定了坚实基础。建议读者亲手编写代码并尝试扩展功能,在实践中加深对 C 语言的理解。如果你在学习过程中遇到问题,欢迎在评论区留言交流,让我们一起在编程之路上共同成长!关注我,获取更多技术干货和项目实战经验!