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

算法核心

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

所以怎么打分是关键!

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

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+空的分数高低取决于电脑是进攻型还是防守型,但是他们分数一定不能相差太多。

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

我们计算黄色方格的得分为 横(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函数负责汇总和确定落点,以及下棋。

还是以这个图为例,黄色方格由于只有行、列、正对角,没有反对角,所以反对角分数为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;
}

结果

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

基于落点打分的井字棋智能下棋算法(C语言实现)的更多相关文章

  1. 井字棋小游戏(C语言)

    最近沉迷于<NetHack>.<DCSS>等字符游戏,对其很感兴趣,于是用C语言写了个字符界面的井字棋小游戏.欢迎大家指教. 编写时遇到了一些问题,我原先准备用循环,直到读取到 ...

  2. python 游戏(井字棋)

    1. 游戏思路和流程图 实现功能,现实生活中的井字棋玩法 游戏流程图 2. 使用模块和游戏提示 import random def game_info(): print('欢迎来到井字棋游戏') pr ...

  3. [CareerCup] 17.2 Tic Tac Toe 井字棋游戏

    17.2 Design an algorithm to figure out if someone has won a game oftic-tac-toe. 这道题让我们判断玩家是否能赢井字棋游戏, ...

  4. quick cocos2d-x 入门---井字棋

    学习quick cocos2d-x 第二天 ,使用quick-x 做了一个井字棋游戏 . 我假设读者已经 http://wiki.quick-x.com/doku.php?id=zh_cn阅读了这个链 ...

  5. 程序设计入门—Java语言 第五周编程题 2井字棋(5分)

    2 井字棋(5分) 题目内容: 嗯,就是视频里说的那个井字棋.视频里说了它的基本思路,现在,需要你把它全部实现出来啦. 你的程序先要读入一个整数n,范围是[3,100],这表示井字棋棋盘的边长.比如n ...

  6. [C++] 井字棋游戏源码

    TicTac.h #define EX 1 //该点左鼠标 #define OH 2 //该点右鼠标 class CMyApp : public CWinApp { public: virtual B ...

  7. [游戏学习22] MFC 井字棋 双人对战

    >_<:太多啦,感觉用英语说的太慢啦,没想到一年做的东西竟然这么多.....接下来要加速啦! >_<:注意这里必须用MFC和前面的Win32不一样啦! >_<:这也 ...

  8. TicTacToe井字棋 by reinforcement learning

    对于初学强化学习的同学,数学公式也看不太懂, 一定希望有一些简单明了的代码实现加强对入门强化学习的直觉认识,这是一篇初级入门代码, 希望能对你们开始学习强化学习起到基本的作用. 井字棋具体玩法参考百度 ...

  9. 井字棋(Tic-Tac-Toe)

    井字棋介绍:https://en.wikipedia.org/wiki/Tic-tac-toe 井字棋简单,但是获胜策略却和直觉不同,四角比中间重要性要高,而且先手有很大的获胜概率获胜(先手胜:91, ...

  10. [HTML5实现人工智能]小游戏《井字棋》发布,据说IQ上200才能赢

    一,什么是TicTacToe(井字棋)   本 游戏 为在下用lufylegend开发的第二款小游戏.此游戏是大家想必大家小时候都玩过,因为玩它很简单,只需要一张草稿纸和一只笔就能开始游戏,所以广受儿 ...

随机推荐

  1. 深入浅出synchronized的原理与源码

    深入浅出synchronized的原理与源码 1.java对象头关于锁的标识 1.对象头 // 32 bits: // -------- // hash:25 ------------>| ag ...

  2. MYSQL之批量删除(mybatis)

    如果参数是array数组 <update id="deleteAll"> delete from C_V WHERE UUID in <foreach item= ...

  3. uniapp学习(一)

    [新课uniapp零基础入门到项目打包(微信小程序/H5/vue/安卓apk)全掌握] https://www.bilibili.com/video/BV1mT411K7nW/?p=24&sh ...

  4. Jenkins自动化测试构建完成 发送钉钉消息

    背景 有时自动化测试完成后,我们可以通过构建完成后给钉钉群发消息,这样就能及时通知到所有人员了. 接入流程 1:建立钉钉机器人,可以通过群助手,添加机器人,增加WebHook自定义接入,然后添加完成会 ...

  5. UG NX实现叉车运输货物功能遇见的问题

    在前一段时间编写模拟叉车运输功能时遇到,货物无法跟随的问题(如下动图) 后面发现是速度太快的原因导致货物没有跟着动,类似于抽桌布的感觉 解决办法有两种:第一种解决办法很简单就是把速度降低到不超过  2 ...

  6. 行行AI人才直播第14期:【国内第二波人工智能进入者、连续创业者】土豆《土豆利用GPT成功融资两次的提示词和故事》

    行行AI人才(海南行行智能科技有限公司)是博客园和顺顺智慧共同运营的AI行业人才全生命周期服务平台. 此刻,ChatGPT的火热程度已经无需多言.一时间,追逐大模型成了国内AI行业的标准动作,&quo ...

  7. 三个编程思想:面向对象编程、面向接口编程、面向过程编程【概念解析系列_1】【C# 基础】

    〇.前言 对于 .Net 中的编程思想还是十分重要的,也是编码出高效的程序的基础! 在使用之前了解其本质,那么用起来就游刃有余.下面来简单对比下三个编程思想,看下它们都是什么,它们之间又有什么关系. ...

  8. CF1855B Longest Divisors Interval 题解

    题意: 给定一个数 \(n\),求一个连续区间 \([l,r]\) 使得 \(n\) 是区间内每个数的倍数,最大化这个区间的长度(多组数据). 思路: 逆向思考一波,( 如果一个数 \(x\) 不是 ...

  9. java读取txt文件解决乱码问题

    说明:由于txt文件有bom和不同的编码方式,导致导入数据时产生乱码,以下代码完美解决乱码问题. 参考他人代码,结合自己的业务加工完成,费了大半天功夫完成,希望对大家有点用处. 废话不多说,直接上代码 ...

  10. burp抓包iPhone手机

    https://blog.csdn.net/weixin_43965597/article/details/107864200