✅作者简介:大家好我是原始豌豆,感谢支持。
本文由 原始豌豆 原创 CSDN首发 如需转载还请通知⚠
欢迎各位→点赞 + 收藏⭐️ + 留言
系列专栏:C语言项目实践
相信大家都玩过三子棋或者五子棋游戏,但你会通过编程完美实现吗,本篇将带领你真正学会AI智能三子棋,让程序拥有人工智能,可以实现堵棋,赢棋,自由落子的功能。玩家VS电脑,玩家VS玩家。谁会是最终赢家?
一、项目背景与目标
二、最终成果展示
三、模块化编程
四、整体结构组成
五、代码及思路讲解
(1)控制台属性设置函数
(2)颜色控制函数
(3)背景音乐函数
(4)光标函数
(5)三子棋字幕动画函数
(6)主菜单界面player vs computer 动画展示函数
(7)主菜单函数
(8)初始化棋盘函数
(9)打印棋盘函数
(10)玩家下棋函数
(11)玩家下棋提示框函数
(12)AI人工智能下棋函数
(13)AI人工智能三连赢棋函数
(14)AI人工智能堵棋函数
(15)先手后手函数
(16)遍历棋盘函数
(17)电脑下棋提示框函数
(18)玩家VS玩家下棋函数
(19)人机对战中玩家赢或电脑赢或平局的提示框函数
(20)游戏主体函数
(21)游戏说明函数
(22)开发人员菜单函数
(23)退出游戏函数
(24)主函数
六.全部源码
七.项目总结
三子棋,又称为井字棋,是一种古老而简单的两人对弈游戏。在3x3的棋盘上,两位玩家轮流在空格上放置自己的标记(通常为X和#),先连成三子一线(横、竖或斜)的玩家获胜。本项目旨在使用C语言实现一个能够与人类玩家进行对战并具有一定智能水平的三子棋AI。在人机对战中这个AI会尝试找到一个胜利的模式或阻止对手胜利的模式。如果没有找到,它将会选择其余可以干扰到玩家的位置下棋。除此之外该程序还具备人人对战的功能。
话不多说,先给大家看下做好后的最终效果!!!
三子棋游戏最终效果图GIF展示(带背景音乐)
实现智能三子棋代码逻辑前,我们来说说什么是模块化编程吧?
什么是模块化编程?
传统方式编程:所有的函数均放在主函数main.c里,一个文件内会有很多的代码,不利于代码的组织和管理,而且很影响编程者的思路。
模块化编程:把各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用函数的声明,其它.c文件想使用其中的代码时,只需要#include
"XXX.h"文件即可。使用模块化编程可极大的提高代码的可阅读性、可维护性、可移植性等。
游戏代码主要由四部分组成。
beautify.c
该文件内主要存放三子棋游戏的控制台设置函数,颜色控制函数,音乐播放函数,光标定位函数,游戏开场动画函数,菜单界面动画函数,主菜单界面,打印玩家赢或电脑赢或平局的提示框函数,游戏规则说明函数,开发人员菜单函数,打印玩家下棋对应的提示框函数,打印电脑下棋坐标提示框的函数,打印退出游戏动态效果
game.c
该文件内主要存放初始化棋盘函数 ,打印棋盘函数 ,玩家下棋函数,电脑下棋函数,分别调用AI智能三连赢棋函数,AI智能堵棋函数,按比重权值范围随机落子,实现人工智能下棋,判断棋盘是否为空函数,AI人工智能三连赢棋函数,AI人工智能三连赢棋函数,玩家VS玩家下棋函数 ,判断输赢函数
test.c
该文件内主要存放人机对战中选择玩家先手或电脑先手的函数,游戏主体函数,主函数sanziqi.h 头文件
声明游戏内容中所包含的所有函数
(1)控制台属性设置函数
智能三子棋是基于C语言编写的控制台应用程序,我们首先要把控制台的各种参数设置好,控制台显示的宽度和高度,控制台缓冲区的大小和控制台标题的设置。
//控制台属性设置
void ConsoleSet()
{
system("mode con cols=141 lines=50");//窗口宽度和高度
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);//使用GetStdHandle函数来获取标准输出句柄(STD_OUTPUT_HANDLE),通常用于控制台应用程序。
COORD bufferSize = { 141, 500 }; // 缓冲区大小为141*500字符 缓冲区的大小以字符宽和高为单位。//设置的控制台缓冲区大小要超过控制台大小
SetConsoleScreenBufferSize(hOut, bufferSize);
system("title 智能三子棋");//界面美化
}
(2)颜色控制函数
智能三子棋游戏里使用了很多不同颜色的动态效果展示和界面,所以我们先编写一个color函数用来控制程序的各个界面的颜色,让程序运行看上去更加美观、鲜明。
//颜色控制函数
void color(short x)
{
if (x >= 0 && x <= 15)
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), x);
else
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 7);
}
(3)背景音乐函数
我们的游戏是带有背景音乐的,所以这里我们创建一个背景音乐函数BackgroundMusic,通过调用windows api来实现音乐的播放和音效的实现,“AI-sanziqi.wav”,引号内部的内容为音频文件的名称(音频文件必须是wav格式的,音频文件放在代码对应的release文件底下程序才能有声音),在这里给
BackgroundMusic函数设置了参数x,通过参数用来控制音乐的打开或关闭。传参数e时音乐停止,音频文件我会放到本文的最后。
void BackgroundMusic(char x) //参数传s代表start,开始播放音乐,参数传e代表end,终止播放音乐
{
if (x == 's')
{
PlaySound(TEXT("AI-sanziqi.wav"), NULL, SND_FILENAME | SND_LOOP | SND_ASYNC);//播放音频文件,循环播放,异步播放
}
if (x == 'e')
{
PlaySound(NULL, NULL, SND_PURGE); // 停止所有声音
}
}
(4)光标函数
从本文最开始的最终效果展示中可以看到,部分图案和文字的的打印定位是通过光标位置到指定坐标,然后开始打印的,所以我们编写一个光标控制函数。
//定位光标位置到指定坐标
void goto_xy(int x, int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);;
COORD pos = { x,y };
SetConsoleCursorPosition(handle, pos);
}
(5)三子棋字幕动画函数
游戏开场动画,用字符数组存储并通过循环搭配Sleep函数和光标函数实现显示动画和动画清除的效果,本函数有两个参数,参数传p打印图案,传c清屏图案。
//游戏开场动画,三子棋字幕显示(函数内部不带颜色)
void sanziqi(char print_or_cls)//参数传p打印图案,传c清屏图案
{
char str[] =
"######################################################################\n\
# # # ## ### ### #\n\
# # ## # ## ### ### #\n\
# ############### # ############## # ## #### #### #\n\
# ############### # ############## # ## ########### #\n\
# # #### # ################## #\n\
# # ##### # ####### ## ### #\n\
# # #### # ####### ######## #\n\
# # ### # ### ######## #\n\
# ## ## # ################### # ##### ## ### #\n\
# ############ # ################### # ###### ######## #\n\
# ############ # ################### # ####### ######## #\n\
# # ## # ####### ## ### #\n\
# # ## # ### ## ## ## ##### #\n\
# # ## # ## ## ############# #\n\
# # ## # ## ############# #\n\
# ## ### # ## # ## ### #### #\n\
# ################## # ##### # ## ### ##### #\n\
# ################## # ##### # ## #### #### #\n\
# # #### # ## ## ## #\n\
# # # #\n\
# # # #\n\
######################################################################";
if (print_or_cls == 'p') //参数传p代表打印图案printf
for (int i = 0; i < strlen(str); i++)
{
putchar(str[i]);
if (str[i] == '\n')
{
Sleep(50);
}
}
else if (print_or_cls == 'c') //参数传c代表清除图案cls
{
goto_xy(0, 0); //将坐标传到(0,0),再依次按行打印空格,以达到清屏效果
for (int i = 0; i < strlen(str); i++)
{
putchar(' ');
if (str[i] == '\n')
{
Sleep(50);
}
}
}
}
(6)主菜单界面player vs computer 动画展示函数
从本文最开始的最终效果展示中可以看到,主菜单界面的左侧是有player vs computer 这个动态效果的展示的,所以我们编写一个player vs computer动画展示函数,用字符数组存储图案,并通过循环搭配Sleep函数和光标函数实现显示动画和动画清除的效果,我将图案分成了三部分,并设置了红,绿,蓝三种颜色对一部分每一部图案。
//游戏主菜单界面左侧player vs computer 动画展示(函数内部自带颜色,红,绿,蓝)
void player_vs_computer()
{
int line = 0;//控制player vs computer在goto_xy函数操纵下的的行数位置
char ch1[] =
" _ \n"
" _ __ | | __ _ _ _ ___ _ __ \n"
"| '_ \\ | | / _` | | | | | / _ \\ | '__|\n"
"| |_)| | | | (_| | | |_| | | __/ | | \n"
"|.__./ |_| \\__,_| \\__, | \\___| | _|\n"
"|_| |___/ \n";
char ch2[] =
" __ __ ___ \n"
" \\ \\ / // __|\n"
" \\ V / \\__ \\\n"
" \\_/ |___/\n";
char ch3[] =
" _\n"
" ___ ___ _ __ ___ _ __ _ _ | |_ ___ _ __ \n"
"/ __| / _ \\ | '_ ` _ \\ | '_ \\ | | | | | __| / _ \\ | '__|\n"
"| (__ | (_) || | | | | || |_) || |_| | | |_ | __/ | | \n"
"\\___ | \\___/ |_| |_| |_|| .__ / \\__,_| \\___| \\___| | _|\n"
" |_| \n";
color(4);//红色
goto_xy(5, 0);
for (int i = 0; i < strlen(ch1); i++)
{
printf("%c", ch1[i]);
if (ch1[i] == '\n')
{
Sleep(30);
goto_xy(5, ++line);
}
}
color(10);//绿色
goto_xy(15, ++line);
for (int i = 0; i < strlen(ch2); i++)
{
printf("%c", ch2[i]);
if (ch2[i] == '\n')
{
Sleep(30);
goto_xy(15, ++line);
}
}
color(3);//蓝色
goto_xy(0, ++line);
for (int i = 0; i < strlen(ch3); i++)
{
printf("%c", ch3[i]);
if (ch3[i] == '\n')
{
Sleep(30);
goto_xy(0, ++line);
}
}
}
(7)主菜单函数
我们既然做游戏,就得有一个菜单,供玩家选择开始游戏或退出游戏等功能。我们先做一个Menu菜单函数并写上每个功能,使用时在主函数里调用就可以展示给玩家了。
菜单函数也可以设计的很精美,可以参考我的设计样式.最后的goto_xy(75, 16);是为了将输入处的光标定位在括号里面
//游戏主菜单界面菜单界面(函数内部不带颜色)
void Menu()
{
goto_xy(60, 5);
printf("________________________\n"); Sleep(70);
goto_xy(60, 6);
printf("| 1.开始游戏 |\n"); Sleep(70);
goto_xy(60, 7);
printf("|_______________________|\n"); Sleep(70);
goto_xy(60, 8);
printf("| 2.人人对战 |\n"); Sleep(70);
goto_xy(60, 9);
printf("|_______________________|\n"); Sleep(70);
goto_xy(60, 10);
printf("| 3.游戏说明 |\n"); Sleep(70);
goto_xy(60, 11);
printf("|_______________________|\n"); Sleep(70);
goto_xy(60, 12);
printf("| 4.开发人员 |\n"); Sleep(70);
goto_xy(60, 13);
printf("|_______________________|\n"); Sleep(70);
goto_xy(60, 14);
printf("| 5.退出游戏 |\n"); Sleep(70);
goto_xy(60, 15);
printf("|_______________________|\n"); Sleep(70);
goto_xy(60, 16);
printf("请输入选项->【 】");
goto_xy(75, 16);//将输入处的光标定位在括号里面
}
(8)初始化棋盘函数
菜单函数写好以后,游戏拥有的基本功能框架也就定位好了,接下来我们要做的就是实现菜单函数里所希望实现的所有功能,先来实现人机对战。
创建棋盘并进行初始化,因为我们是要把符号输入到二维数组中,结合我们构造出来的图案形成三子棋的模样,所以我们需要创建一个二维数组并进行初始化(因为棋盘最开始为空白的,所以二维数组初始化的内容为空格),三子棋的棋盘一共三行三列,共有九个空可以下棋,这用一个三行三列的数组来存放每个空的数据,为了代码的灵活性,这里不要把行和列的值写死,用define定义的常量来写,方便以后代码的更新。
//初始化棋盘函数
void InitBoard(char Board[ROW][COL], int row, int col) // 二维数组作为参数传递给函数时,必须指明数组的列数
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
Board[i][j] = ' ';//将棋盘的每个位置初始化为空格
}
}
}
(9)打印棋盘函数
我们创建并初始化棋盘后,需要打印出三子棋的棋盘供玩家进行观察和下棋,我们同样不要把行和列写死,而是采用define定义的常量ROW和COL,这样玩家如果不想玩3*3格的三子棋,或者想把三子棋改为五子棋,只需在头文件中改变define定义的常量值,棋盘的大小也会随之改变。
//打印棋盘函数 (亮黄色棋盘)
int DisplayBoard(char Board[ROW][COL], int row, int col)
{
color(14);//黄色棋盘
int k = 0; //goto_xy函数的列坐标
goto_xy(50, ++k); //棋盘打印的初始坐标
for (int i = 1; i <= col; i++)//打印棋盘列序号
{
printf(" %d", i);
}
goto_xy(52, ++k);
for (int i = 0; i < col; i++)
{
printf("____");
}
for (int i = 0; i < row; i++)//控制行数
{
goto_xy(50, ++k);//k不断自增以对应棋盘不同的大小,起到换行后居中的效果
printf("%d|", i + 1);//打印棋盘行序号和最左侧边框
for (int j = 0; j < col; j++)
{
printf(" %c |", Board[i][j]);//将" |"看作一组,根据col打印
}
goto_xy(50, ++k);//k不断自增以对应棋盘不同的大小,起到换行后居中的效果
printf(" |");//打印棋盘无序号行和最左侧边框
for (int j = 0; j < col; j++)
{
printf("---|");//将"---|"看作一组,根据col打印
}
}
printf("\n\n");
}
三子棋棋盘ROW和COL都为3效果图:
例如将ROW和COL都改成9:效果图
(10)玩家下棋函数
在玩家下棋这部分中,我们定义玩家通过输入坐标来进行下棋。需要注意的是:数组所定义的下标是从0开始的,而玩家认知的坐标是从1开始的(例如:玩家想输入第一行第一列的坐标是,输入的是(1,1),而不是(0,0),这里需要将玩家输入的横、纵坐标减1,转换成数组的坐标。
在玩家输入完坐标后,我们还需要判断该坐标是否合法,如果玩家输入的坐标超过棋盘界限,或者该坐标已经下过棋了,那么需要重新输入坐标。所以我们采用while循环结构,直到玩家下棋成功,跳出循环。
这里玩家下的棋,用 X表示。定义PlayerMove用来实现玩家下棋的这部分。
//玩家下棋函数 (玩家下棋显示红色提示框)
void PlayerMove(char Board[ROW][COL], int row, int col)
{
color(4);//玩家下棋显示红色提示框
int i = 0, j = 0;//定义了两个整数变量i和j,用于存储玩家输入的坐标。
int ch = 0;
PrintPlayerMove(1);
while (1)//开始一个无限循环,这样程序会持续等待玩家的输入,直到玩家输入有效的坐标并下棋break。
{
scanf("%d%d", &i, &j);
while ((ch = getchar()) != '\n' && ch != EOF);//对玩家输入数据进行处理,防止字符造成死循环。
if ((i >= 1 && i <= row) && (j >= 1 && j <= col))//如果坐标在棋盘范围内(即1到行数和1到列数之间),则执行以下代码:
{
if (Board[i - 1][j - 1] == ' ')//检查该坐标位置是否为空。如果为空,表示这个位置可以被下棋
{
Board[i - 1][j - 1] = 'X';
break;//成功下棋后跳出循环
}
else
{
PrintPlayerMove(2);
}
}
else//如果坐标不在棋盘范围内,重新输入。
{
PrintPlayerMove(3);
}
}
}
(11)玩家下棋提示框函数
玩家下棋回合得有提示框提示玩家到下棋回合了,或者玩家坐标输入错误,或者坐标不合法都会有对应的提示框,我们单独做一个玩家下棋提示框函数来显示这些,并通过不同的参数判断该打印哪条提示框信息。上面写的玩家下棋函数就可以调用玩家提示框函数,因为后续还会有人人对战模式,所以我们这里直接把人人对战的提示框也写好。
//打印玩家下棋对应的提示框函数 (函数内部不带颜色)
void PrintPlayerMove(int n)
{
if (n == 1)
{
printf(" __________________________________\n");
printf("| 玩家下棋回合, 棋子‘X’ |\n");
printf("|__________________________________|\n");
printf(" 输入坐标:");
}
else if (n == 2)
{
printf(" __________________________________\n");
printf("| 坐标被占用, 请重新输入 |\n");
printf("|__________________________________|\n");
printf(" 输入坐标:");//如果该位置已被占据,程序会提示玩家重新输入:
}
else if (n == 3)//如果坐标不在棋盘范围内,重新输入。
{
printf(" __________________________________\n");
printf("| 坐标不合法,请重新输入 |\n");
printf("|__________________________________|\n");
printf(" 输入坐标:");
}
else if (n == 4)
{
printf(" __________________________________\n");
printf("| 玩家一下棋回合, 棋子‘X’ |\n");
printf("|__________________________________|\n");
printf(" 玩家一输入坐标:");
}
else if (n == 5)
{
printf(" __________________________________\n");
printf("| 玩家二下棋回合, 棋子‘#’ |\n");
printf("|__________________________________|\n");
printf(" 玩家二输入坐标:");
}
}
(12)AI人工智能下棋函数
看了CSDN大部分文章写的三子棋,都是没有智能的,玩家轻松就能击败电脑,那么如何赋予电脑智能呢?让电脑可以击败玩家或立于不败之地呢?其实并不复杂。
电脑实现AI人工智能下棋分为三个部分:赢棋、堵棋、按权值自由落子。
这里的优先级是赢棋>堵棋>自由落子,在写代码的时候要遵循:能赢优先赢,不能赢则优先堵,以上情况都不符合的时候自由落子。(自由落子不等于随机下棋,找到以自身坐标为中心,周围8个坐标有玩家棋子的位置进行落子,可以对玩家形成一定的干扰)
定义三个函数:
智能三连赢棋函数:AI_WinChess(如果函数执行并落子,返回’s’;否则返回’f’)
智能堵棋函数: AI_BlockingChess(如果函数执行并落子,返回1;否则返回0)
自由落子函数:ComputerMove(构建整体框架,根据前两个函数的返回值来判断是否落子)
//电脑下棋函数,分别调用AI智能三连赢棋函数,AI智能堵棋函数,按比重权值范围随机落子,实现人工智能下棋
void ComputerMove(char Board[ROW][COL], int row, int col)
{
int x, y;//定义了两个整数变量x和y,用于存储电脑下棋的坐标。
//1.智能三连赢棋函数优先级最高,//如果智能三连赢棋函数没有成功找到可以三连的棋子则返回f,然后进入内层运行智能堵棋函数
if (AI_WinChess(Board, ROW, COL) == 'f')
{
//智能堵棋函数优先级第二,//如果智能堵棋函数没有成功找到可以堵棋的棋子则返回0,然后进入内层进行随机落子
if (AI_BlockingChess(Board, ROW, COL) == 0)
{
//分支判断电脑是否是先手且第一步下棋,是则在中心位置下棋,
if (TraverseBoard(Board, ROW, COL) == 'e')
{
color(15);
Board[row / 2][col / 2] = '#';
AI_Coordinate(row / 2, col / 2);
}
else if (TraverseBoard(Board, ROW, COL) == 'n')//否则则找到以自身为中心,其余八个坐标内有玩家棋子的位置进行随机下棋
{
while (1)
{
color(11);//随机下棋湖蓝色
x = rand() % row;//在循环内,使用rand()函数%row和col,生成一个坐标值不会越界的随机的坐标(x, y)
y = rand() % col;
if (Board[x][y] == ' ' && (Board[x - 1][y - 1] == 'X' || Board[x - 1][y] == 'X' || Board[x - 1][y - 1] == 'X' || Board[x][y - 1] == 'X' || Board[x][y + 1] == 'X' || Board[x + 1][y - 1] == 'X' || Board[x + 1][y] == 'X' || Board[x + 1][y + 1] == 'X'))
{
Board[x][y] = '#';
AI_Coordinate(x, y);
break;
}
}
}
}
}
}
(13)AI人工智能三连赢棋函数
对玩家的的落子进行分析,实现智能赢棋 (优先级最高) ,电脑下棋用‘#’表示,该函数在AI人工智能下棋函数中被调用。
1 判断所有行里是否有某行出现 # # 空 , # 空 # ,空 # #,三种情况进行智能赢棋,
2.判断所有列里是否有某列出现
# # 空
# 空 #
空 # #
3.判断一条角线(\)里是否有出现
# # 空
# 空 #
空 # #
4.判断另一条角线(/)里是否有出现
# # 空
# 空 #
空 # #
// AI人工智能三连赢棋函数(对玩家的的落子进行分析,实现智能赢棋 (优先级最高) (亮白色提示框)
char AI_WinChess(char Board[ROW][COL], int row, int col)// 返回值s代表success电脑三连赢棋成功,返回值f代表failure电脑未找到能够三连的的棋子
{
int i = 0;
int j = 0;
color(15);
//1.判断所有行里是否有某行出现 ##空 #空# 空##,三种情况进行智能赢棋
for (i = 0; i < row; i++)
{
for (j = 0; j < col - 2; j++)
{
if (Board[i][j] == Board[i][j + 1] && Board[i][j] == '#' && Board[i][j + 2] == ' ')
{
Board[i][j + 2] = '#';
AI_Coordinate(i, j + 2);//将下棋坐标传入AI_Coordinate函数
return 's';
}
if (Board[i][j] == Board[i][j + 2] && Board[i][j] == '#' && Board[i][j + 1] == ' ')
{
Board[i][j + 1] = '#';
AI_Coordinate(i, j + 1);//将下棋坐标传入AI_Coordinate函数
return 's';
}
if (Board[i][j + 1] == Board[i][j + 2] && Board[i][j + 1] == '#' && Board[i][j] == ' ')
{
Board[i][j] = '#';
AI_Coordinate(i, j);//将下棋坐标传入AI_Coordinate函数
return 's';
}
}
}
//2.判断所有列里是否有某列出现
// # # 空
// # 空 #
// 空 # #
//以上三种情况,进行智能三连赢棋
for (j = 0; j < col; j++)
{
for (i = 0; i < row - 2; i++)
{
if (Board[i][j] == Board[i + 1][j] && Board[i][j] == '#' && Board[i + 2][j] == ' ')
{
Board[i + 2][j] = '#';
AI_Coordinate(i + 2, j);//将下棋坐标传入AI_Coordinate函数
return 's';
}
if (Board[i][j] == Board[i + 2][j] && Board[i][j] == '#' && Board[i + 1][j] == ' ')
{
Board[i + 1][j] = '#';
AI_Coordinate(i + 1, j);//将下棋坐标传入AI_Coordinate函数
return 's';
}
if (Board[i + 1][j] == Board[i + 2][j] && Board[i + 1][j] == '#' && Board[i][j] == ' ')
{
Board[i][j] = '#';
AI_Coordinate(i, j);//将下棋坐标传入AI_Coordinate函数
return 's';
}
}
}
//3.判断一条角线(\)里是否有出现
//# # 空
// # 空 #
// 空 # #
//以上三种情况,进行智能赢棋
for (i = 0; i < row - 2; i++)
{
for (j = 0; j < col - 2; j++)
{
if (Board[i][j] == Board[i + 1][j + 1] && Board[i][j] == '#' && Board[i + 2][j + 2] == ' ')
{
Board[i + 2][j + 2] = '#';
AI_Coordinate(i + 2, j + 2);
return 's';
}
if (Board[i][j] == Board[i + 2][j + 2] && Board[i][j] == '#' && Board[i + 1][j + 1] == ' ')
{
Board[i + 1][j + 1] = '#';
AI_Coordinate(i + 1, j + 1);
return 's';
}
if (Board[i + 1][j + 1] == Board[i + 2][j + 2] && Board[i + 1][j + 1] == '#' && Board[i][j] == ' ')
{
Board[i][j] = '#';
AI_Coordinate(i, j);
return 's';
}
}
}
//4.判断一条角线(/)里是否有出现
// # # 空
// # 空 #
// 空 # #
//以上三种情况,进行智能赢棋
for (i = 0; i < row - 2; i++)
{
for (j = col - 1; j > 1; j--)
{
if (Board[i][j] == Board[i + 1][j - 1] && Board[i][j] == '#' && Board[i + 2][j - 2] == ' ')
{
Board[i + 2][j - 2] = '#';
AI_Coordinate(i + 2, j - 2);
return 's';//返回这个棋子的字符
}
if (Board[i][j] == Board[i + 2][j - 2] && Board[i][j] == '#' && Board[i + 1][j - 1] == ' ')
{
Board[i + 1][j - 1] = '#';
AI_Coordinate(i + 1, j - 1);
return 's';//返回这个棋子的字符
}
if (Board[i + 1][j - 1] == Board[i + 2][j - 2] && Board[i + 1][j - 1] == '#' && Board[i][j] == ' ')
{
Board[i][j] = '#';
AI_Coordinate(i, j);
return 's';//返回这个棋子的字符
}
}
}
return 'f';
}
(14)AI人工智能堵棋函数
(对玩家的的落子进行分析,实现智能堵棋)(优先级第二),该函数在AI人工智能下棋函数中被调用。
堵棋的优先度在下新棋之上,在赢棋的优先度之下。堵棋共有四种堵法:横着堵、竖着堵、左斜堵、右斜堵。
将堵棋的大致框架写出,并且为了代码的适用性,这里的代码也不要写死,既要适用三行三列的棋盘,也要同样适用于更大的棋盘。(五行五列、十行十列等等)
当棋盘中不需要堵棋时,返回0;而只要当其中一种情况成立,电脑便落子堵棋,返回1。
//AI人工智能堵棋函数(对玩家的的落子进行分析,实现智能堵棋)(优先级第二) (紫色提示框)
int AI_BlockingChess(char Board[ROW][COL], int row, int col)
{
color(5);//紫色
int i = 0;
int j = 0;
//判断所有行里是否有某行出现 XX空 X空X 空XX,三种情况进行智能堵棋
for (i = 0; i < row; i++)
{
for (j = 0; j < col - 2; j++)
{
if (Board[i][j] == Board[i][j + 1] && Board[i][j] == 'X' && Board[i][j + 2] == ' ')
{
Board[i][j + 2] = '#';
AI_Coordinate(i, j + 2);//将下棋坐标传入AI_Coordinate函数
return 1;
}
if (Board[i][j] == Board[i][j + 2] && Board[i][j] == 'X' && Board[i][j + 1] == ' ')
{
Board[i][j + 1] = '#';
AI_Coordinate(i, j + 1);//将下棋坐标传入AI_Coordinate函数
return 1;
}
if (Board[i][j + 1] == Board[i][j + 2] && Board[i][j + 1] == 'X' && Board[i][j] == ' ')
{
Board[i][j] = '#';
AI_Coordinate(i, j);//将下棋坐标传入AI_Coordinate函数
return 1;
}
}
}
//判断所有列里是否有某列出现
// X X 空
// X 空 X
// 空 X X
//以上三种情况,进行智能堵棋
for (j = 0; j < col; j++)
{
for (i = 0; i < row - 2; i++)
{
if (Board[i][j] == Board[i + 1][j] && Board[i][j] == 'X' && Board[i + 2][j] == ' ')
{
Board[i + 2][j] = '#';
AI_Coordinate(i + 2, j);//将下棋坐标传入AI_Coordinate函数
return 1;
}
if (Board[i][j] == Board[i + 2][j] && Board[i][j] == 'X' && Board[i + 1][j] == ' ')
{
Board[i + 1][j] = '#';
AI_Coordinate(i + 1, j);//将下棋坐标传入AI_Coordinate函数
return 1;
}
if (Board[i + 1][j] == Board[i + 2][j] && Board[i + 1][j] == 'X' && Board[i][j] == ' ')
{
Board[i][j] = '#';
AI_Coordinate(i, j);//将下棋坐标传入AI_Coordinate函数
return 1;
}
}
}
//判断一条角线(\)里是否有出现
//X X 空
// X 空 X
// 空 X X
//以上三种情况,进行智能堵棋
for (i = 0; i < row - 2; i++)
{
for (j = 0; j < col - 2; j++)
{
if (Board[i][j] == Board[i + 1][j + 1] && Board[i][j] == 'X' && Board[i + 2][j + 2] == ' ')
{
Board[i + 2][j + 2] = '#';
AI_Coordinate(i + 2, j + 2);//将下棋坐标传入AI_Coordinate函数
return 1;
}
if (Board[i][j] == Board[i + 2][j + 2] && Board[i][j] == 'X' && Board[i + 1][j + 1] == ' ')
{
Board[i + 1][j + 1] = '#';
AI_Coordinate(i + 1, j + 1);//将下棋坐标传入AI_Coordinate函数
return 1;
}
if (Board[i + 1][j + 1] == Board[i + 2][j + 2] && Board[i + 1][j + 1] == 'X' && Board[i][j] == ' ')
{
Board[i][j] = '#';
AI_Coordinate(i, j);//将下棋坐标传入AI_Coordinate函数
return 1;
}
}
}
//判断一条角线(/)里是否有出现
// X X 空
// X 空 X
// 空 X X
//以上三种情况,进行智能堵棋
for (i = 0; i < row - 2; i++)
{
for (j = col - 1; j > 1; j--)
{
if (Board[i][j] == Board[i + 1][j - 1] && Board[i][j] == 'X' && Board[i + 2][j - 2] == ' ')
{
Board[i + 2][j - 2] = '#';
AI_Coordinate(i + 2, j - 2);//将下棋坐标传入AI_Coordinate函数
return 1;//返回这个棋子的字符
}
if (Board[i][j] == Board[i + 2][j - 2] && Board[i][j] == 'X' && Board[i + 1][j - 1] == ' ')
{
Board[i + 1][j - 1] = '#';
AI_Coordinate(i + 1, j - 1);//将下棋坐标传入AI_Coordinate函数
return 1;//返回这个棋子的字符
}
if (Board[i + 1][j - 1] == Board[i + 2][j - 2] && Board[i + 1][j - 1] == 'X' && Board[i][j] == ' ')
{
Board[i][j] = '#';
AI_Coordinate(i, j);//将下棋坐标传入AI_Coordinate函数
return 1;//返回这个棋子的字符
}
}
}
return 0;
}
(15)先手后手函数
人机对战中选择玩家先手或电脑先手的函数
//人机对战中选择玩家先手或电脑先手的函数
int Xianshou_Houshou()
{
int control = 0;
while (1)
{
printf(" _____________________________________\n"); Sleep(30);
printf("| 1.玩家先手(简单) |\n"); Sleep(30);
printf("| 2.电脑先手(困难) |\n"); Sleep(30);
printf("|_____________________________________|\n"); Sleep(30);
printf(" 请选择>:");
scanf("%d", &control);
while (getchar() != '\n');
if (control == 1)
return 1;
else if (control == 2)
return 2;
else
{
printf("选择错误,重新选择");
Sleep(750);
system("cls");
}
}
}
(16)遍历棋盘函数
智能三子棋游戏中,我们设置人机对战中玩家可以选择电脑先手下棋或玩家先手下棋,如果电脑先手下棋,棋盘开始时应该是空的,所以我们做一个遍历棋盘函数,根据返回值判断电脑第一步下棋时应该落子的坐标,该函数在AI人工智能下棋函数中被调用。
//遍历棋盘判断棋盘是否为空,电脑先手且棋盘为空的情况下,返回e,非空返回n。
char TraverseBoard(char Board[ROW][COL], int row, int col)
{
int flag = 0;
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (Board[i][j] != ' ')
{
flag = 'n'; //nonempty 非空
return flag;
}
}
}
return 'e'; //empty 空
}
(17)电脑下棋提示框函数
玩家下棋回合有玩家提示框,那么电脑下棋也得有电脑下棋提示框,该函数还可以显示电脑下棋的坐标,在智能三连赢棋函数AI_WinChess,智能堵棋函数:AI_BlockingChess, 自由落子函数ComputerMove被调用。
//打印电脑下棋坐标提示框的函数,参数xy为电脑下棋位置 (函数内部不带颜色)
void AI_Coordinate(int x, int y)
{
printf(" __________________________________\n");
printf("| 电脑回合,棋子‘#’电脑思考中... |\n");
printf("|__________________________________|\n");
Sleep(100);
printf(" __________________________________\n");
printf("| 电脑下棋坐标(%d , %d) |\n", x + 1, y + 1);//将电脑下棋的坐标展示给玩家。展示时横纵坐标+1后展示。
printf("|__________________________________|\n");
Sleep(400);
}
(18)玩家VS玩家下棋函数
智能三子棋游戏中除了人机对战还有人人对战的功能,所以应该写一个玩家VS玩家下棋函数 ,来实现人人对战。
//玩家VS玩家下棋函数 (玩家一下棋显示淡绿色提示框)(玩家二下棋显示淡蓝色提示框)
void Player1_VS_Player2(char Board[ROW][COL], int row, int col, int x)
{
int i = 0, j = 0;//定义了两个整数变量i和j,用于存储玩家输入的坐标。
int ch = 0;
if (x == 1)//玩家一
{
color(10);//淡绿色
PrintPlayerMove(4);
}
if (x == 2)//玩家二
{
color(9);//淡蓝色
PrintPlayerMove(5);
}
while (1)//开始一个无限循环,这样程序会持续等待玩家的输入,直到玩家输入有效的坐标并下棋break。
{
scanf("%d%d", &i, &j);
while ((ch = getchar()) != '\n' && ch != EOF);//对玩家输入数据进行处理,防止字符造成死循环。
if ((i >= 1 && i <= row) && (j >= 1 && j <= col))//如果坐标在棋盘范围内(即1到行数和1到列数之间),则执行以下代码:
{
if (Board[i - 1][j - 1] == ' ')//检查该坐标位置是否为空。如果为空,表示这个位置可以被下棋
{
if (x == 1) //人人对战专用,玩家一回合传参x=1
{
Board[i - 1][j - 1] = 'X';
break;//成功下棋后跳出循环
}
else if (x == 2) //人人对战专用,玩家二回合传参x=2
{
Board[i - 1][j - 1] = '#';
break;//成功下棋后跳出循环
}
}
else
{
PrintPlayerMove(2);//坐标被占用提示框
}
}
else//如果坐标不在棋盘范围内,重新输入。
{
PrintPlayerMove(3);
}
}
}
三子棋的输赢判定方式分为五种,一种是横向三个,一种是纵向三个,一种是右下倾斜(\),一种是左下倾斜(/),还有就是是否平局。也就是棋盘下满且双方皆未达成棋子三连的结果。
将框架写好,随后在for循环的内部加入判定部分即可。
//判断输赢函数
char Judgment(char Board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)//判断i行里是否有棋子连成一行的情况出现
{
for (j = 0; j < col - 2; j++)
{
if (Board[i][j] == Board[i][j + 1] && Board[i][j + 1] == Board[i][j + 2] && Board[i][j] != ' ')
return Board[i][j];//返回这个棋子的字符
}
}
for (j = 0; j < col; j++)//判断j列里是否有棋子连成一列的情况出现
{
for (i = 0; i < row - 2; i++)
{
if (Board[i][j] == Board[i + 1][j] && Board[i + 1][j] == Board[i + 2][j] && Board[i][j] != ' ')
return Board[i][j];//返回这个棋子的字符
}
}
//判断斜线上是否有三个棋子连线的情况出现 (\)
for (i = 0; i < row - 2; i++)
{
for (j = 0; j < col - 2; j++)
{
if (Board[i][j] == Board[i + 1][j + 1] && Board[i + 1][j + 1] == Board[i + 2][j + 2] && Board[i][j] != ' ')
return Board[i][j];//返回这个棋子的字符
}
}
//判断斜线上是否有三个棋子连线的情况出现 (/)
for (i = 0; i < row - 2; i++)
{
for (j = col - 1; j > 1; j--)
{
if (Board[i][j] == Board[i + 1][j - 1] && Board[i + 1][j - 1] == Board[i + 2][j - 2] && Board[i][j] != ' ')
return Board[i][j];//返回这个棋子的字符
}
}
//上述四种情况都未发生的情况下,说明此时无论玩家或电脑双方皆未赢,程序才会运行到此,此时只剩下继续或平局两种情况
for (i = 0; i < row; i++)//通过遍历棋盘,判断棋盘上是否已经下满了,以此判断继续还是平局
{
for (j = 0; j < col; j++)
{
if (Board[i][j] == ' ')//如果有空格,说明没下满,游戏继续。
return 'C';//continue
}
}
return 'Q';//五种情况都未发生,平局。
}
(19)人机对战中玩家赢或电脑赢或平局的提示框函数
无论是哪一方赢,总得有个结果,所以这里我们做一个提示框函数,作为赢方的结束动画展示。
//打印人机对战中玩家赢或电脑赢或平局的提示框(函数内部不带颜色)
void display_win_or_lost(char x) //参数传w代表win,打印ch1[],参数传l代表lost,打印ch2[],参数传p代表平局,打印ch3[]
{
char ch1[] =
"__ __ _\n"
"\\ \\ / / (_)\n"
" \\ V / ___ _ _ __ __ _ _ __\n"
" \\ / / _ \\ | | | | \\ \\/ \\ / /| || '_ \\ \n"
" | | | (_) || |_| | \\ V V / | || | | |\n"
" \\_/ \\___/ \\__,_| \\_/\\_/ |_||_| |_|\n";
char ch2[] =
"__ __ _ _\n"
"\\ \\ / / | | | | \n"
" \\ V / ___ _ _ | | ___ ___ | |_\n"
" \\ / / _ \\ | | | | | | / _ \\ / __|| __|\n"
" | | | (_)| | |_| | | | |(_) |\\__ \\| |_ \n"
" \\_/ \\___/ \\__,_| |_| \\___/ |___/ \\__|\n";
char ch3[] =
" \n"
" ################# \n"
" # ############### \n"
" # # # ### ## \n"
" ## # ## ## ## \n"
" ### # ## ################ \n"
" ## # ## ## \n"
" ### # ### ## \n"
" ## # ### ################# \n"
" ## # ### ## ## \n"
" ## # ## ## ######### ## \n"
" # # ## ## ## ## \n"
" ################### ## # # ## \n"
" # ## # # ## \n"
" # ## # # ## \n"
" # ## ## ## ## \n"
" # ## ######### ## \n"
" # ## ## \n"
" # ## ######## \n"
" # ## ####### \n";
if (x == 'w')//参数传w代表win,打印ch1[]
{
for (int i = 0; i < strlen(ch1) - 1; i++)
{
printf("%c", ch1[i]);
if (ch1[i] == '\n')
{
Sleep(50);
}
}
}
else if (x == 'l')//参数传w代表lost,打印ch2[]
{
for (int i = 0; i < strlen(ch2) - 1; i++)
{
printf("%c", ch2[i]);
if (ch1[i] == '\n')
{
Sleep(50);
}
}
}
else if (x == 'p')//参数传p代表平局,打印ch3[]
{
for (int i = 0; i < strlen(ch3) - 1; i++)
{
printf("%c", ch3[i]);
if (ch3[i] == '\n')
{
Sleep(10);
}
}
}
}
效果图展示
(20)游戏主体函数
这块是游戏的重点部分,上面写的所有函数都是为了这里做准备工作,上面所有的逻辑写好以后,我们就可以编写游戏主体函数了,按照一定次序排列,实现整个下棋逻辑。通过不同的参数判断玩家选择是人机对战或人人对战,玩家电脑依次下棋,直到有一方胜出为止,或玩家一,玩家二依次下棋,直到有一方胜出为止。
void Game(int n)//游戏主体函数
{
char ret = 0; //用于接收判断输赢函数的返回值,返回的四种字符对应棋局的四种情况,定义在头文件中
char Board[ROW][COL] = { 0 }; //定义二维数组
InitBoard(Board, ROW, COL); //初始化二维数组
DisplayBoard(Board, ROW, COL); //打印棋盘
while (1)//循环内部为人机双方下棋过程或人人对战过程
{
if (n == 1)//人机对战玩家先手
PlayerMove(Board, ROW, COL);
if (n == 2)//人机对战电脑先手
ComputerMove(Board, ROW, COL);
if (n == 3) //人人对战玩家一
Player1_VS_Player2(Board, ROW, COL, 1);
ret = Judgment(Board, ROW, COL); //判断输赢
if (ret != 'C') //等于C棋局继续,不等于C跳出循环,根据ret的值判断输赢
{
break;
}
system("cls"); //确认落子后清屏,再打印棋盘
DisplayBoard(Board, ROW, COL); //打印棋盘
if (n == 1)//人机对战电脑后手
ComputerMove(Board, ROW, COL);
if (n == 2) //人机对战玩家后手
PlayerMove(Board, ROW, COL);
if (n == 3) //人人对战玩家二
Player1_VS_Player2(Board, ROW, COL, 2);
ret = Judgment(Board, ROW, COL);
if (ret != 'C')
{
break;
}
system("cls");
DisplayBoard(Board, ROW, COL);//打印棋盘
}
if (n == 1 || n == 2)//人机对战的情况下
{
if (ret == 'X')
{
display_win_or_lost('w');
}
else if (ret == '#')
{
display_win_or_lost('l');
}
else
{
color(0XA);
display_win_or_lost('p');
}
}
else //人人对战的情况下
{
if (ret == 'X')
{
printf("\n 玩家一胜利\n");
}
else if (ret == '#')
{
printf("\n 玩家二胜利\n");
}
else
{
color(0XA);
display_win_or_lost('p');
}
}
DisplayBoard(Board, ROW, COL);//打印棋盘
}
(21)游戏说明函数
菜单函数编写好并且游戏主体函数也编写好后,基本的功能框架算是基本完成了了,接下来我们要做的就是实现菜单函数里所希望实现的其他功能,
先实现菜单函数里的游戏说明功能。
void Introduction()
{
printf("\n1.游戏分为人机对战和人人对战,双方依次在9宫格棋盘上摆放棋子。\n");
printf("\n2.双方轮流落子,每次只能在一个格子上放置一个棋子。\n");
printf("\n3.如果将自己的3个棋子连成一条线,则宣告胜利。连线的方向可以是水平、垂直或对角线。\n");
printf("\n4.如果在棋盘上形成平局,即双方都没有机会将自己的3个棋子连成一条线,那么游戏结束。\n\n");
}
(22)开发人员菜单函数
此函数对应主菜单界面的第4个选项开发人员,介绍了版本号,作者信息(我本人),和制作时间,背景音乐等信息,该函数可以控制程序内的音乐打开或关闭。
//开发人员菜单函数(开发人员信息和背景音乐控制)
void kaifa()
{
int control = 0;
do
{
system("color B0");
printf(" ________________________________________________________\n"); Sleep(30);
printf("| 智能三子棋版本v1.1 作者 --->原始豌豆 |\n"); Sleep(30);
printf("| |\n"); Sleep(30);
printf("| 制作时间 2024.2.11 音乐 --->纯音乐 |\n"); Sleep(30);
printf("|________________________________________________________|\n"); Sleep(30);
printf("**************1.开启音乐 0.关闭音乐********************\n");
printf("************** 2.回到主界面 *********************\n");
printf("请选择>:");
scanf("%d", &control);
while (getchar() != '\n');
switch (control)
{
case 1:
BackgroundMusic('s');
break;
case 0:
BackgroundMusic('e');
break;
case 2:
system("cls");//回到主界面前先清屏
break;
default:
printf("\n 选择错误,请重新输入!");
system("pause");//界面美化---->控制台停留,实现按任意键继续的效果
system("cls"); //界面美化---->清屏,防止玩家多次输入错误造成屏幕占满
break;
}
} while (control != 2);
}
(23)退出游戏函数
此函数对应主菜单界面的第5个选项退出游戏,在玩家退出游戏时打印由符号组成的退出两个汉字。
//打印退出游戏字幕
void GameOver()
{
char str[] = " \n"
" # ############ ## \n"
" ### ############## ## \n"
" ### ### ### ## ## ##\n"
" ### ## ## ## ## ##\n"
" ## ## ## ## ## ##\n"
" ### ############## ## ## ##\n"
" ## ############## ## ## ##\n"
" ## ## ## ## ##\n"
" ## ## ## ## ##\n"
" ###### ############## ## ## ##\n"
" ###### ############## ### ## ##\n"
" ## ###### #####################\n"
" ## ## #### ## #################### \n"
" ## ## #### ### # ## #\n"
" ## ## #### #### ### ## ##\n"
" ## ## ####### ### ## ##\n"
" ## ## #### ### ## ##\n"
" ## ## ### ### ## ##\n"
" ## ## ### ### ### ## ##\n"
" ## ## #### #### ### ## ##\n"
" ### ###### ### ### ## ##\n"
" ##### #### ### ### ## ##\n"
" ######### ### ## ##\n"
" ##### ################### #######################\n"
" #### ################## #####################\n\n";
for (int i = 0; i < strlen(str) - 1; i++)
{
printf("%c", str[i]);
if (str[i] == '\n')
{
Sleep(10);
}
}
}
上述函数都写好后,在主函数中调用各个函数,即可完成智能三子棋游戏游戏的制作
int main()
{
int n = 0;
ConsoleSet();//控制台设置
BackgroundMusic('s');
srand((unsigned int)time(NULL));//生成随机数生成起点//time函数参数是指针,返回值是time_t类型,而srand的参数是无符号整型,所以强制类型转换一下。
system("color 02");
sanziqi('p');
Sleep(2000);
sanziqi('c');
int input = 0;
do {
player_vs_computer();
color(14);//将菜单界面设置为亮黄色。
Menu();//打印菜单
scanf("%d", &input);
while (getchar() != '\n');
system("cls");
switch (input)
{
case 1:
n = Xianshou_Houshou();
Game(n); //game
goto_xy(50, 12);
system("pause");
Sleep(1000);
system("cls");
break;
case 2:
Game(3);
goto_xy(50, 12);
system("pause");
Sleep(1000);
system("cls");
break;
case 3:
system("color 70");
Introduction();
system("pause");
system("cls");
system("color 0E");
break;
case 4:
kaifa();
system("cls");
system("color 0E");
break;
case 5:
color(8);
GameOver();
break;
default:
goto_xy(64, 16);//将输入处的光标定位在括号里面
printf("选择错误,重新选择");
Sleep(800);
system("cls");
}
} while (input != 5);//当input==5时退出游戏
return 0;
}
后续会发出来,敬请期待
经过多次测试和调整,我们的AI在三子棋游戏中展现出了较高的智能水平。它能够在电脑先手的大多数情况下战胜人类玩家,并且具有一定的战术意识和预判能力。虽然我们的AI在三子棋游戏中已经取得了一定的成绩,但仍有很大的提升空间。未来,我们可以进一步优化评估函数和搜索算法,提高AI的智能水平和对抗性。同时,也可以尝试将本项目应用到其他类似的棋类游戏中,以检验算法的通用性和可扩展性。