首页 > 编程语言 >基于落点打分的井字棋智能下棋算法(C语言实现)

基于落点打分的井字棋智能下棋算法(C语言实现)

时间:2023-10-17 13:12:06浏览次数:28  
标签:QP char int 落点 C语言 井字棋 score printf type

本文设计了一种基于落地打分的井字棋下棋算法,能够实现电脑不败,所以如果玩家会玩的话,一般是平局。

算法核心

电脑根据对落子位置的打分,选择分数最高的位置,若不同落点分数相同则随机选择位置(随机选择就不会显得那么呆板)

所以怎么打分是关键!

基本思想是,判断落点附近的位置的棋子类型,进行打分,进一步解释,根据井字棋的规则,横、竖、对角连成三子则判获胜,所以每一个落点和与他同一横、竖、对角的棋子类型有关。所以我们可以指定一个打分表如下:

C代表己方棋子(电脑),P代表对方(玩家)棋子,空代表没有棋子

类型 得分
C+C 100
P+P 50
C+空 6
P+空 4
空+空 2
P+C 1

简单解释一下,C+C表示己方已经有2个棋子了,下一步马上可以赢,给最高分,且其他分数相加不会超过100分。同理P+P是50分,如果不存在C+C情况,那么50分将是最高分,其他分数相加不会超过。

C+空P+空的分数高低取决于电脑是进攻型还是防守型,但是他们分数一定不能相差太多。

这里举个例子说明得分怎么计算

image-20231017114843378

我们计算黄色方格的得分为 横(C+空)+竖(C+空)+对角(P+空)=6+6+4=16。橙色方格得分为横(C+空)+竖(P+空)+对角(P+空)=6+4+4=14。所以电脑会选择走黄色方格。

也就是说,最终每个可以下的格子的打分等于横、竖、对角的打分之和,若没有对角线,则对角线为0分。

核心算法介绍如上,接下来是实现。

代码实现

代码大致可分为三个模块,制定井字棋基本操作和规则、电脑下棋、界面打印。

井字棋下棋规则

我们创建一个SZQ_basic.c文件,在头文件SZQ_basic.h进行相关的定义和声明。

//`SZQ_basic.h`
#include<stdio.h>
#include <string.h>
#include<stdlib.h>

#define N 3
#define P_pawn 'o' //玩家棋子 o
#define C_pawn 'X' //电脑棋子 X

char** creatQP(); //创建棋盘
void printQP(char** QP); //打印棋盘
int inputQZ(char** QP, int row, int column); //输入棋子
int isPlayerWin(char** QP); //判断胜负
int isQPFull(char** QP); //判断平局

基本思想是,通过二维数char[3][3]组存储棋子,空位置则用空格符填充,输入棋子时判断是不是空格符,如果是才可以输入(下棋)。

判断胜负的返回值包括 1 ,0 , -1,分别表示玩家胜,未结束,电脑胜。平局则是通过判断棋盘是否满,遍历二维数组看是否还有空格符就可以了。

  1. 创建棋盘

    /*
    * @brief    creat  chequer
    */
    char** creatQP() {
    	char** QP;
    	QP = (char**)malloc(sizeof(char*) * N);
    	if (QP == NULL)
    		return NULL;
    	for (int i = 0; i < N; i++) {
    		QP[i] = (char*)malloc(sizeof(char) *N);
    		if (QP[i] == NULL)
    			return NULL;
    		memset(QP[i], ' ', sizeof(char)*N);
    	}
    	return QP;
    }
    
  2. 打印棋盘

    /*
    * @brief    printf Chequer
    * @para     棋盘地址
    */
    void printQP(char** QP) {
    
    	if (QP == NULL) {
    		printf("print Chequer failed!");
    		return;
    	}
    
    	puts("|_____________________________    棋盘    ___________________________|");
    	printf("\t\t 行号→  1   2   3\t\t玩家棋子:“o”\n\t\t 列号↓\t\t\t\t电脑棋子:“X”\n");
    	printf("\t\t       |---|---|---|\n");
    	for (int i = 0; i < N; i++) {
    		printf("\t\t      %d|", i + 1);
    		for (int j = 0; j < N; j++) {
    			printf(" %c |", QP[i][j]);
    		}
    		printf("\n");
    		printf("\t\t       |");
    		for (int j = 0; j < N; j++) {
    			printf("---|");
    		}
    		printf("\n");
    	}
    	puts("|--------------------------------------------------------------------|");
    }
    
  3. 输入棋子

    int inputQZ(char** QP, int row, int column) {
    	if (row < 0 || row>2 || column < 0 || column>2) {
    		printf("输入棋子位置不合法!\n请重新输入:>>");
    		return 0;
    	}
    	if (QP[row][column] != ' ') {
    		printf("该位置已有棋子!\n请重新输入:>>");
    		return 0;
    	}
    	QP[row][column] = P_pawn;
    	return 1;
    }
    
  4. 判断棋盘满

    /*
    * brief  判断棋盘是否满了,满了即平局
    */
    int isQPFull(char** QP) {
    	for (int i = 0; i < N; i++)
    	{
    		for (int j = 0; j < N; j++)
    		{
    			if (QP[i][j] ==' ')
    				return 0;
    		}
    	}
    	return 1;
    }
    
  5. 判断胜负

    /*
    *@brief  判断游戏是否结束(玩家获胜?)
    *@ret    1:玩家胜   0:游戏未结束   -1:玩家败
    */
    int isPlayerWin(char** QP) {
    	int flag = 0;
    
    	//判断行成线
    	for (int i = 0; i < N; i++)
    	{
    		if (QP[i][0] == QP[i][1] && QP[i][0] == QP[i][2] && QP[i][0] == C_pawn)
    			flag = -1;
    		else if (QP[i][0] == QP[i][1] && QP[i][0] == QP[i][2] && QP[i][0] == P_pawn)
    			flag = 1;
    	}
    	//判断列成线
    	for (int j = 0; j < N; j++)
    	{
    		if (QP[0][j] == QP[1][j] && QP[0][j] == QP[2][j] && QP[0][j] == C_pawn)
    			flag = -1;
    		else if (QP[0][j] == QP[1][j] && QP[0][j] == QP[2][j] && QP[0][j] == P_pawn)
    			flag = 1;
    	}
    	//判断正对角成线
    	if (QP[0][0] == QP[1][1] && QP[0][0] == QP[2][2] && QP[0][0] == C_pawn)
    		flag = -1;
    	else if (QP[0][0] == QP[1][1] && QP[0][0] == QP[2][2] && QP[0][0] == P_pawn)
    		flag = 1;
    
    	//判断反对角成线
    	if (QP[0][2] == QP[1][1] && QP[0][2] == QP[2][0] && QP[0][2] == C_pawn)
    		flag = -1;
    	else if (QP[0][2] == QP[1][1] && QP[0][2] == QP[2][0] && QP[0][2] == P_pawn)
    		flag = 1;
    
    	return flag;
    }
    

电脑下棋算法

接下来创建SZQ_engine.c源文件实现电脑下棋,头文件相关的函数声明如下

#include <time.h>
#include "SZQ_basic.h"

int computerPlay(char** QP);

int row_score(char** QP, int row);
int column_score(char** QP, int column);
int Pdiag_score(char** QP, int postion);
int Ndiag_score(char** QP, int postion);

文件一共包含5个函数,含score的函数是计算在行、列、正对角、反对角情况下的得分,因为每一个落点位置最多只有这四种情况叠加(中心点位置特殊,这四种情况都有),所以只要把每种情况的得分相加,computerPlay函数负责汇总和确定落点,以及下棋。

image-20231017114843378

还是以这个图为例,黄色方格由于只有行、列、正对角,没有反对角,所以反对角分数为0,其他大于0。具体是多少分还得参考打分表。

为了方便,我们将打分表以数组形式存储。三个不同字符,任意两个相加,得到的数一定不会出现相同的,可以通过数学证明。

int scoretable[6][2] = { {C_pawn + C_pawn,50},
						 {P_pawn + P_pawn,30},
						 {C_pawn + ' ',6},
						 {P_pawn + ' ',4},
						 {' ' +' ',2},
						 {P_pawn + C_pawn,1},
};
  1. 行得分计算

    int row_score(char** QP,int row){
    	int score=0;
    	int type = 0;
    	for (int i = 0; i < N; i++)
    	{
    		type = QP[row][i] + type;
    	}
        //将落点对应那一行三个位置的字符相加,减去自身的空字符,得到类型type
    	type = type - ' ';
        //查询打分表,type类型的对应得分score
    	for (int k = 0; k < 6; k++)
    	{
    		if (scoretable[k][0] == type)
    		{
    			score = scoretable[k][1]; 
    			break;
    		}
    	}
    	return score;
    }
    
  2. 列得分计算

    int column_score(char** QP, int column) {
    	int score = 0;
    	int type = 0;
        //将落点对应那一列三个位置的字符相加,减去自身的空字符,得到类型type
    	for (int i = 0; i < N; i++)
    	{
    		type = QP[i][column] + type;
    	}
    	type = type - ' ';
        //查询打分表,type类型的对应得分score
    	for (int k = 0; k < 6; k++)
    	{
    		if (scoretable[k][0] == type)
    		{
    			score = scoretable[k][1];
    			break;
    		}
    	}
    	return score;
    }
    
  3. 正对角得分计算

    int Pdiag_score(char** QP, int postion) {
    	int score = 0;
    	int type ;
        //判断该位置是否存在正对角情况
    	if (postion/N==postion%N)
    	{
            //若存在,同样的将落点对应那一正对角三个位置的字符相加,减去自身的空字符,得到类型type
    		type = QP[0][0] + QP[1][1] + QP[2][2];
    		type = type - ' ';
            //查询打分表,type类型的对应得分score
    		for (int k = 0; k < 6; k++)
    		{
    			if (scoretable[k][0] == type)
    			{
    				score = scoretable[k][1];
    				break;
    			}
    		}
    	}
    	return score;
    }
    
  4. 反对角计算得分

    int Ndiag_score(char** QP, int postion) {
    	int score = 0;
    	int type;
        //判断该位置是否存在反对角情况,自行证明反对角满足(postion / N+postion % N)==2
    	if ((postion / N+postion % N)==2)
    	{
    		type = QP[0][2] + QP[1][1] + QP[2][0];
    		type = type - ' ';
    		for (int k = 0; k < 6; k++)
    		{
    			if (scoretable[k][0] == type)
    			{
    				score = scoretable[k][1];
    				break;
    			}
    		}
    	}
    	return score;
    }
    
  5. 接下来是汇总分数,确定落点

    int computerPlay(char** QP) {
    	int index=0;
    	int score = 0;
    
    	for (int i = 0; i < N*N; i++)
    	{
    		int temp_score = 0;
            //遍历棋盘,并且找出空格符,即可落子的位置,计算分数
    		if (QP[i / N][i % N] == ' ')
    		{
                //把4种情况的分数相加得到总分数
    			temp_score = row_score(QP, i/N) + column_score(QP, i%N) + Pdiag_score(QP, i) + Ndiag_score(QP, i);
    			if (temp_score > score)   //取分数最大值
    			{
    				score = temp_score;
    				index = i;
    			}
    			else if (temp_score == score)  //若分数相同,在两个随机选择一个位置作为落点
    			{
    				srand((unsigned)time(NULL));
    				index=(rand() % 2)? i:index;
    			}	
    		}
    	}
    	return index;
    }
    

    注意返回值是索引,就是把二维数组当一维数组,方便遍历和返回位置(二维数组行号和列号是两个值,不方便返回)。因此返回后需要根据索引index确定行和列。

    行:index / N

    列:index % N

打印棋盘界面

最后,我们把棋盘界面打印,就可以大功告成了。

这里我使用延时函数模拟电脑思考过程,不然打印太快了,没意思hhh。然后玩家也可以自行选择先手和后手。

这部分比较简单,C语言基础语法,直接把代码放下面。

#define _CRT_SECURE_NO_WARNINGS 
#include <windows.h> 
#include "SZQ_engine.h"
#include "SZQ_basic.h"
int main() {
	//printf("%d", C_pawn+C_pawn);
	int hang, lie;
	int cmd = 1;
	int position;
	char** TicTacToe = creatQP();
	puts("______________________________________________________________________");
	puts("|*********************** Tic-Tac-Logic (game) ***********************|");
	puts("|** author:gofan-SiTu ***********************************************|");
	puts("|************************           请选择以下功能      *************|");
	puts("|************************            0:退出游戏          ***********|");
	puts("|************************     1:作为先手开始与电脑对弈   ***********|");
	puts("|************************     2:作为后手开始与电脑对弈   ***********|");
	puts("|********************************************************************|");
	puts("|——————————————————————————————————|");
	printf(">请输入对应序号:>>");
	scanf_s("%d", &cmd);
	switch (cmd)
	{
	case 0:
		exit(0);
	case 1:
		printf(">您选择与电脑对弈,落子时请输入行号和列号确定位置,中间用空格隔开\n");
		printf(">您的棋子是“o”,电脑的棋子是“X”\n");
		printQP(TicTacToe);
		printf(">您先走棋  ");
		while (1) {
			printf(">请输入您的下一步落子位置:>>");
			do {
				scanf_s("%d%d", &hang, &lie);
			} while (!inputQZ(TicTacToe, hang - 1, lie - 1));
			printf(">您走棋后,棋盘如下\n");
			printQP(TicTacToe);
			if (isPlayerWin(TicTacToe) == 1) {
				printf("\n>!恭喜玩家获胜 !<\n >  您太强了  <\n");
				break;
			}
			else if (isPlayerWin(TicTacToe) == -1) {
				printf("\n >  电脑获胜  < \n >!你太菜拉!< \n");
				break;
			}
			else if (isQPFull(TicTacToe)) {
				printf(" >!平局 !< \n");
				break;
			}
			position = computerPlay(TicTacToe);
			printf(" 电脑正在思考......\n");
			Sleep(1000);
			printf(">电脑的落子位置:>>%d %d\n>电脑落子后,棋盘如下\n", position / N + 1, position % N + 1);
			TicTacToe[position / N][position % N] = C_pawn;
			Sleep(100);
			printQP(TicTacToe);
			if (isPlayerWin(TicTacToe) == 1) {
				printf("\n>!恭喜玩家获胜 !<\n >  您太强了  <\n");
				break;
			}
			else if (isPlayerWin(TicTacToe) == -1) {
				printf("\n >  电脑获胜  < \n >!你太菜拉!< \n");
				break;
			}
			else if (isQPFull(TicTacToe)) {
				printf(" >!平局 !< \n");
				break;
			}
		}
		break;
	case 2:
		printf(">您选择与电脑对弈,落子时请输入行号和列号确定位置,中间用空格隔开\n");
		printf(">您的棋子是“o”,电脑的棋子是“X”\n");
		printf(">电脑先走棋\n");
		while (1) {
			position = computerPlay(TicTacToe);
			printf(" 电脑正在思考......\n");
			Sleep(1000);
			printf(">电脑的落子位置:>>%d %d\n>电脑落子后,棋盘如下\n", position / N + 1, position % N + 1);
			TicTacToe[position / N][position % N] = C_pawn;
			Sleep(250);
			printQP(TicTacToe);
			if (isPlayerWin(TicTacToe) == 1) {
				printf("\n>!恭喜玩家获胜 !<\n >  您太强了  <\n");
				break;
			}
			else if (isPlayerWin(TicTacToe) == -1) {
				printf("\n >  电脑获胜  < \n >!你太菜拉!< \n");
				break;
			}
			else if (isQPFull(TicTacToe)) {
				printf(" >!平局 !< \n");
				break;
			}
			printf(">请输入您的下一步落子位置:>>");
			do {
				scanf_s("%d%d", &hang, &lie);
			} while (!inputQZ(TicTacToe, hang - 1, lie - 1));
			printf(">您走棋后,棋盘如下\n");
			printQP(TicTacToe);
			if (isPlayerWin(TicTacToe) == 1) {
				printf("\n>!恭喜玩家获胜 !<\n >  您太强了  <\n");
				break;
			}
			else if (isPlayerWin(TicTacToe) == -1) {
				printf("\n >  电脑获胜  < \n >!你太菜拉!< \n");
				break;
			}
			else if (isQPFull(TicTacToe)) {
				printf(" >!平局 !< \n");
				break;
			}

		}
		break;
	default:
		printf(">输入序号不准确,程序异常退出!\n 请重新启动!");
		exit(-1);
	}
	printf("\n  游戏结束! \n");
	for (int i = 0; i < N; i++)
		free(TicTacToe[i]);
	free(TicTacToe);
	for (int i = 5; i >= 0; i--)
	{
		printf("程序将在%ds后关闭...\n", i);
		Sleep(1000);
	}
	return 0;
}

结果

目前这个算法可以实现电脑不败,当然,这个打分表也是我经过分析、筛选确定的分数,比较合理。但是如果把这思想拓展到五子棋上,似乎不太行,至少我现在还没想到思路,因为五子棋比井字棋复杂得多了,而且棋盘很大,就不能简单的对落点位置的附近棋子类型打分,就算可以,判断规则也相当复杂。所以结论就算,这个算法只适合井字棋子棋这种简单的类型。

标签:QP,char,int,落点,C语言,井字棋,score,printf,type
From: https://www.cnblogs.com/gofan-SiTu/p/17769249.html

相关文章

  • C语言-数据类型
    C语音-数据类型数据类型中文名称空间大小(bite-字节)char字符串数据类1short(int)短整型2int整形4long长整形4longlong更长的整形8float单精度浮点数4double双精度浮点数8include<>intmain(){ //字符类型charch......
  • C语言线性表 demo
    typedefintPosition;typedefstructLNode*List;structLNode{ElementTypeData[MAXSIZE];PositionLast;};/*初始化*/ListMakeEmpty(){ListL;L=(List)malloc(sizeof(structLNode));L->Last=-1;returnL;}/*查找*/#d......
  • 实验二 c语言分支与循环基础应用编程
    实验一源代码#include<stdio.h>#include<stdlib.h>#include<time.h>#defineN5#defineN1374#defineN2465intmain(){ intnumber; inti; srand(time(0)); for(i=0;i<N;i++) { number=rand()%(N2-N1+1)+N1; printf("20238329%04......
  • C语言实现顺序表二
    ////main.c//SeqList2////Createdbystevexiaohuzhaoon2023/10/15.//#include<stdio.h>#include<stdlib.h>#defineMAXSIZE100/*表示线性表的最大长度*///定义一个顺序表节点structSNode{//用来存储书序表中的数据(动态分配数组)......
  • 实验2 C语言分支与循环基础应用编程
    一、实验目的能正确使用if语句、switch语句实现分支结构能正确使用while语句、do...while语句、for语句实现循环结构能在具体问题场景中使用嵌套分支语句和嵌套循环语句能在具体问题场景中正确区分、使用continue和break能灵活、组合使用c语句编程解决简单应用问题二、实......
  • 实验2 C语言分支与循环基础应用编程
    练习1#include<stdio.h>#include<stdlib.h>#include<time.h>#defineN5#defineN1374#defineN2465intmain(){intnumber;inti;srand(time(0));//以当前系统时间作为随机种子for(i=0;i<N;++i){number=rand()%(N2-N1......
  • 实验2_C语言分支与循环基础应用编程
    1.task_11#include<stdio.h>2#include<stdlib.h>3#include<time.h>45#defineN56#defineN13747#defineN246589intmain()10{11intnumber;12inti;1314srand(time(0));1516for......
  • 20231407陈原《计算机科学与概论》及《C语言程序设计》第三周学习情况
    [2022-2023-1-计算机基础与程序设计]2023-2024-1计算机基础与程序设计第三周作业https://edu.cnblogs.com/campus/besti/2022-2023-1-CFAP[2022-2023-1计算机基础与程序设计第一周作业](https://www.cnblogs.com/rocedu/p/9577842.html#WEEK03)作业目标学习《计算机科......
  • 初识C语言(2)
    一、常量1.字面常量即数字本身,例如:3,100,3.14intmain(){ intnum=4; printf("%d\n",num); num=8; printf("%d\n",num); return0;2.常变量const-常属性(赋予一个变量常属性,变量→常变量(当然其本质上还是个变量),如下图,num变为const修饰的常变量,它的值无法改变intmain()......
  • C语言数据类型占用字节大小+rand_mode/randomize_mode/static constraint+I2C和SPI的
    C语言数据类型占用字节大小https://blog.csdn.net/sinan1995/article/details/79577106对于整形,最大8字节,超出8字节的计算,要么用库,要么不用。64位编译器:char/unsignedchar:1字节char*:8字节shortint:2字节int/unsignedint:4字节longint:8字节float:4字节double:8字节lon......