一聚教程网:一个值得你收藏的教程网站

热门教程

C语言黑白棋游戏操作步骤及完整源码

时间:2026-08-13 12:09:50 编辑:袖梨 来源:一聚教程网

处理C语言黑白棋游戏操作步骤及完整源码这类问题时,先确认目标场景,再按步骤核对配置或玩法细节。

一、项目简介

这是一个可以单人游玩的黑白棋小游戏。

采用鼠标左键点击的方式下子。下子之后,处于该点和原本同颜色棋子之间的棋子会转变颜色。本游戏代码设置了可以调整难度的AI(改变内部的difficult参数),可以随自己的喜好调整。

编译环境:visual c++ 6.0

第三方库:Easyx2017

二、运行截图

三、源码解析

首先看游戏的主体部分,也就是其运行逻辑。

void play(void)                   // 游戏过程{       MOUSEMSG m;       int x, y;       // 初始化棋子       for(x = 0; x < 8; x++)              for(y = 0; y < 8; y++)                     map[x][y] = 0;       map[3][4] = map[4][3] = 'B';       map[3][3] = map[4][4] = 'W';       // 开始游戏       print();       mciSendString("play 音乐背景音乐.wma from 0 repeat", NULL, 0, NULL);       do       {              if (Canput('B'))                                                                   // 如果玩家有下子位置        {                     while(true)                     {                            while(true)                            {                                   m = GetMouseMsg();                             // 获取鼠标消息                                   if(m.uMsg == WM_LBUTTONDOWN && m.x - 26 < 37 * 8 && m.y - 26 < 37 * 8)                                                                                                  // 如果左键点击                                          break;                            }                            x = (m.y - 26) / 37;                            y = (m.x - 26) / 37;                            if(judge(x, y, 'B'))                                           // 如果当前位置有效                            {                                   draw(x, y, 'B');                                               // 下子                                   mciSendString("play 音乐下子.wma from 0", NULL, 0, NULL);                                   print();                                   putimage(37 * y, 37 * x, &img[3]);         // 标识下子点                                   break;                            }                            else                                   continue;                     }                     if (quit('W'))                                                         // 计算机是否失败                            break;              }              if (Canput('W'))                                                           // 如果计算机有下子位置              {                     clock_t start;                     start = clock();                     D('W', 1);                                                                     // 搜索解法                     while (clock() - start < CLOCKS_PER_SEC);                     draw(X, Y, 'W');                     print();                     mciSendString("play 音乐下子.wma from 0", NULL, 0, NULL);                     putimage(37 * Y, 37 * X, &img[4]); // 标识下子点                     if (quit('B'))                                                           // 玩家是否失败                            break;              }       }while (Canput('B') || Canput ('W'));

我们定义了运算用的变量x,y,以及鼠标变量m。MOUSEMSG是Easyx中的结构体,用于保存鼠标消息。

然后初始化棋盘,即在中间的四个位置放上黑白各两颗棋子。map[x][y]是一个二维字符串组,用”B”和”W”分别表示黑棋和白棋。

之后进入Do-while循环,循环条件为两方至少有一方可以下子。

首先玩家(黑)先行动。我们想要达成的目的是,如果我们点击棋盘的一个点,这里允许下子则下子,不能下子则继续检测。

这个结构采取一个双层循环来完成,内层不断调用GetMouseMsg获取鼠标信息(GetMouseMsg是Easyx中的函数,其返回值为之前提到过的MOUSEMSG结构,.x和.y分别表示鼠标点击的横纵坐标位置),如果点击,则确定该点的位置,内层循环结束。判定此次落子是否有效,有效则下子并终止外层循环,无效则返回外层循环的头部。

然后白方下子。这里利用一个动态规划函数算出一个较好的落子点(与难度相关),并在计算开始之前计时。计算结束判定是否经过一秒(CLOCKS_PER_SEC表示一秒钟内CPU运行的时钟周期数),如果不到一秒则延迟到一秒后落子,防止影响人类棋手心态。然后同样执行落子程序以及音乐播放程序。

在整个Play函数外部,前方应该还有一个初始化函数load(),之后还有胜利处理函数。

之后,我们看其中的每一个函数应当如何实现。

这是全局当中声明的函数。

void load(void);                        // 加载素材void print(void);                       // 画棋盘void draw(int, int, char);              // 下当前子int judge(int, int, char);                // 判断当前是否可以落下bool Canput(char);                          // 判断是否有棋可吃bool quit(char);                        // 判断是否有棋存活bool ask(void);                                // 弹出对话框int D(char, int);                         // 动态规划void play(void);                         // 游戏过程

下面的是全局变量。

const int difficult = 6; // 难度const int move[8][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1},                                          {-1, -1}, {1, -1}, {1, 1}, {-1, 1}};                                                 // 八个方向扩展char map[8][8];                        // 棋盘IMAGE img[5];                         // 保存图片int black, white;                 // 双方的棋子数int X, Y;                              // 白棋的下子点

加载素材并且初始化变量:

void load(void)           // 加载素材{       // 加载图片       loadimage(&img[0], "图片空位.bmp");       loadimage(&img[1], "图片黑子.bmp");       loadimage(&img[2], "图片白子.bmp");       loadimage(&img[3], "图片黑子1.bmp");       loadimage(&img[4], "图片白子1.bmp");        // 加载音乐       mciSendString("open 音乐背景音乐.wma", NULL, 0, NULL);       mciSendString("open 音乐和局.wma", NULL, 0, NULL);       mciSendString("open 音乐胜利.wma", NULL, 0, NULL);       mciSendString("open 音乐失败.wma", NULL, 0, NULL);       mciSendString("open 音乐下子.wma", NULL, 0, NULL);        // 初始化棋盘       initgraph(340, 340);       IMAGE qipan;       loadimage(&qipan, "图片棋盘.bmp");       putimage(0, 0, &qipan);       setorigin(26, 26);       SetWindowText(GetHWnd(), "黑白棋AI版");}

loadimage是Easyx库中的函数,用于加载图像。本案例中第一个参数是保存图像的 IMAGE 对象指针,第二个是图像地址。这个函数还可以拉伸图片,或者自动适应IMAGE的大小,具体用法参见Easyx的官方文档。

mciSendString是<mmsystem.h>当中的函数,用来播放多媒体文件的API指令。

initgraph这个函数用于初始化绘图窗口。

putimage用于在当前设备上绘制指定图像。本案例当中代表在(0,0)处绘制棋盘图形。

setorigin也在Easyx当中,用于设置坐标原点。

SetWindowText是Windows API宏,声明在WinUser.h当中,用于设定窗口文本

绘制棋盘:

void print(void)   // 画棋盘{       int x, y;       black = white = 0;       for(x = 0; x < 8; x++)              for(y = 0; y < 8; y++)                     switch(map[x][y])                     {                            case 0:                                          putimage(37 * y, 37 * x, &img[0]);                                   break;                            case 'B':                                          putimage(37 * y, 37 * x, &img[1]);                                   black++;                                   break;                            case 'W':                                          putimage(37 * y, 37 * x, &img[2]);                                   white++;                                   break;                     }}

利用双层循环遍历棋盘,根据map[x][y]中的字符,在对应位置绘制棋子就可以了。

落子:

void draw(int x, int y, char a)   // 下当前子{       char b = T(a);                                                               // 敌方子       int i, x1, y1, x2, y2;       bool sign;                         for (i = 0; i < 8; i++)       {              sign = false;              x1 = x + move[i][0];              y1 = y + move[i][1];              while (0 <= x1 && x1 < 8 && 0 <= y1 && y1 < 8 && map[x1][y1])              {                     if(map[x1][y1] == b)                            sign = true;                     else                     {                            if(sign)                            {                                   x1 -= move[i][0];                                   y1 -= move[i][1];                                   x2 = x + move[i][0];                                   y2 = y + move[i][1];                                   while (((x <= x2 && x2 <= x1) || (x1 <= x2 && x2 <= x)) && ((y <= y2 && y2 <= y1) || (y1 <= y2 && y2 <= y)))                                   {                                          map[x2][y2] = a;                                          x2 += move[i][0];                                          y2 += move[i][1];                                   }                            }                            break;                     }                     x1 += move[i][0];                     y1 += move[i][1];              }       }       map[x][y] = a;}

对落子点的八个方向进行检测。如果在该方向上,遇到的第一个棋子与落子颜色不同,并且沿着这条线下去,最后能找到一个和落子颜色相同的棋子,则将这之间的所有棋子改变颜色。

判断当前位置是否可以落子:

int judge(int x, int y, char a)     // 判断当前是否可以落下,同draw函数{       if(map[x][y])                                    // 如果当前不是空的返回0值              return 0;       char b = T(a);       int i, x1, y1;       int n = 0, sign;       for (i = 0; i < 8; i++)       {              sign = 0;              x1 = x + move[i][0];              y1 = y + move[i][1];              while (0 <= x1 && x1 < 8 && 0 <= y1 && y1 < 8 && map[x1][y1])              {                     if(map[x1][y1] == b)                            sign++;                     else                     {                            n += sign;                            break;                     }                     x1 += move[i][0];                     y1 += move[i][1];              }       }       return n;              // 返回可吃棋数}

和上一个函数差不多的逻辑。

判断是否有棋可吃:

bool Canput(char c){       int x, y;       for(x = 0; x < 8; x++)              for(y = 0; y < 8; y++)                     if(judge(x, y, c))                            return true;       return false;}

遍历棋盘,在每个地方都调用judge函数即可。

判断是否有棋存活

bool quit(char c){       int x, y;       bool b = false, w = false;       for(x = 0; x < 8; x++)              for(y = 0; y < 8; y++)              {                     if(map[x][y] == c)                            return false;              }       return true;}

同样是简单的遍历。判断map当中的字符是否全与参数相同即可。

bool ask(void)     // 弹出对话框{       HWND wnd = GetHWnd();       int key;       char str[50];       ostrstream strout(str, 50);       strout <<"黑:" <<black <<"  白:" <<white <<endl;       if (black == white)              strout <<"世界和平";       else if(black > white)              strout <<"恭喜你赢了!";       else              strout <<"小样,还想赢我。";       strout <<"n再来一局吗?" <<ends;       if(black == white)              key = MessageBox(wnd, str, "和局", MB_YESNO | MB_ICONQUESTION);       else if(black > white)              key = MessageBox(wnd, str, "黑胜", MB_YESNO | MB_ICONQUESTION);       else              key = MessageBox(wnd, str, "白胜", MB_YESNO | MB_ICONQUESTION);       if(key == IDYES)              return true;       else              return false;}

GetHWnd在Easyx中定义,用于返回绘图窗口句柄。在 Windows 下,句柄是一个窗口的标识,得到句柄后,可以使用 Windows API 中的函数实现对窗口的控制。

ostrstream strout(str,50);,作用是建立输出字符串流对象strout,并使strout与字符数组str关联(通过字符串流将数据输出到字符数组str),流缓冲区大小为50。

MessageBox()函数包含在头文件 windows.h中,它的功能是弹出一个标准的Windows对话框。返回值是一个int型的整数,用于判断用户点击了对话框中的哪一个按钮。

AI决定落子位置:

int D(char c, int step){       // 判断是否结束递归       if (step > difficult)      // 约束步数之内              return 0;       if (!Canput(c))       {              if (Canput(T(c)))                     return -D(T(c), step);              else                     return 0;       }        int i, j, max = 0, temp, x, y;       bool ans = false;        // 建立临时数组       char **t = new char *[8];       for (i = 0; i < 8; i++)              t[i] = new char [8];       for (i = 0; i < 8; i++)              for (j = 0; j < 8; j++)                     t[i][j] = map[i][j];        // 搜索解法       for (i = 0; i < 8; i++)              for (j = 0; j < 8; j++)                     if (temp = judge(i, j, c))                     {                            draw(i, j, c);                            temp -= D(T(c), step + 1);                            if (temp > max || !ans)                            {                                   max = temp;                                   x = i;                                   y = j;                                   ans = true;                            }                            for (int k = 0; k < 8; k++)                                   for (int l = 0; l < 8; l++)                                          map[k][l] = t[k][l];                     }        // 撤销空间       for (i = 0; i < 8; i++)              delete [] t[i];       delete [] t;        // 如果是第一步则标识白棋下子点       if (step == 1)       {              X = x;              Y = y;       }        return max;  // 返回最优解}

先看函数的输入。Char c代表落子方,step是当前函数递归了几次。要保证这个递归次数不大于difficult的值,以此来决定AI的强度。

#define T(c) ((c == 'B') ? 'W' : 'B'),意思是c为黑或者白,T(c)为对立的白或者黑。这个函数的目的是,也让AI考虑黑方应该如何下棋,以此来一步一步地推断出最佳位置。

搜索解法采取遍历的方法。如果某一点可下,则下该点(不是直接打印在棋盘上),将数据存储到临时变量当中。递归调用指定次数,在这个过程中累计翻转的棋子,如果值超过了之前的最大值,则将落子点更新为该位置。当遍历完成之后,AI就能得出每一点在预计步数之内的最大收益,从而得出最佳落点。

四、完整源码

黑白棋纯C语言完整源码

热门栏目