PicCompress说明之前上传博客图片过大不方便加载，还有一些微信平台的图片无法上传有大小限制，于是就打算开发个压缩图片的工具支持图片格式PNG(.png)JPEG(.jpg, .jpeg, .jpe, .jfif)主要界面主要功能压缩图片可支持多图片压缩（在点击按钮选择图片文件的时候选择多个)注：快速模式对图片质量没有影响发布地址Gitee: 传送门GitHub: 传送门
自己写一个获取哈希值且能文件校验的软件
原因学校网络安全课讲到了Hash函数，老师提了一句上机操作的时候可以用自己的写的文件校验软件，所以我干脆就自己写一个。说明支持算法MD5SHA1SHA256SHA512SHA384为什么没有其它算法因为.net原生库里就支持这几种，其它的我懒得写了（doge）功能计算文本的Hash值计算文件的Hash值文件校验窗口发布地址Gitee传送门GitHub传送门
DevC++ 报错[Error] Id returned 1 exit status起因学校机房的计算机总是二次编译总是报错报错提示[Error] Id returned 1 exit status解决方案前提首先查看下你的程序的控制台窗口有没有关闭，关闭后再重新编译是否报错。以下方案针对关闭窗口后依然报错的情况操作右键Dev C++图标 ---> 属性 --->兼容性 ---> 打钩“以管理员身份运行此程序"打开Dev C++ --> 工具 --> 编译选项 --> 代码生成/优化 --> 连接器 --> 把"链接Objective C程序"设置为 Yes将Dev C++主页面右上角的编译器切换为Debug模式(例如 TDM-GCC 4.9.2 64-bit Debug)若计算机设有还原卡，请将项目及其文件放置在非保护盘
安装Qt如标题，你首先需要到 http://download.qt.io/ 去下载并安装Qt，并在引导下安装MSVC组件（这里不做过多解释）Visual Studio 2019 配置打开VS2019点击“继续但无需代码”“扩展(X)” -> “管理扩展”搜索“Qt”找到“Qt Visual Studio Tools”,点击“下载”按钮（图中是已经安装好的所以不显示下载）安装过程中会弹出 VSIX Installer窗口，依据引导安装即可检查当我们再次打开 Visual Studio 2019 -> “创建新项目” -> 搜索“Qt”,发现已经能够创建Qt项目了但是这还没有完，因为我们还没有添加 Qt Version添加 Qt Version点击“上一步” -> “继续但无需代码” -> “扩展(X)” -> “Qt VS Tools” -> “Qt Versions”点击“add new Qt version”,然后点击Path列下的类似于文件夹的图标找到你的Qt安装目录，在里面找MSVC的 qmake.exe (例如我的的Qt安装在D盘根目录，Qt版本是5.14.2，那么对应的就应该为 D:QtQt5.14.25.14.2msvc2017_64binqmake.exe)最后点击“确定”按钮保存即可注：图片中第一行显示的版本是我之前已经配置好的Qt版本，如果你是第一次配置，是不会自动添加Qt版本的
main函数main.cpp 代码如下int main(void){ init(); while(1){ //一直检测鼠标点击 MOUSEMSG msg == GetMouseMsg(); if(msg.uMsg == WM_LBUTTONDOWN){ manGo(); if(checkOver()){ init(); continue; } AI_GO(); if(checkOver()){ init(); continue; } } } closegraph(); return 0; }项目总结学习了c语言模块化开发设计了判断鼠标点击的算法掌握了AI走棋的写法……不足之处代码缺乏优化，vector没有充分使用玩家不能选择棋子颜色无法进行玩家对战AI算法效率不够高进阶继续学习数据结构与算法对AI进行优化尝试建立服务器实现网络对战等
规则原理如图判断游戏结束chessData.h//row,col 表示当前落子 bool checkWin(ChessData* game, int row, int col);横、竖、斜（斜有两种）共四种情况，每种情况根据当前落子往后遍历5个子，有一种符合就胜利chessData.cppbool checkWin(ChessData* game, int row, int col){ //横 for(int i = 0; i < 5; i++){ if(col - i >= 0 && col - i + 4 < BOARD_GRAD_SIZE && game->chessMap[row][col-i] == game->chessMap[row][col-i+1] && game->chessMap[row][col-i] == game->chessMap[row][col-i+2] && game->chessMap[row][col-i] == game->chessMap[row][col-i+3] && game->chessMap[row][col-i] == game->chessMap[row][col-i+4]){return true;} } //竖 for(int i = 0; i < 5; i++){ if(row - i >= 0 && row - i + 4 < BOARD_GRAD_SIZE && game->chessMap[row-i][col] == game->chessMap[row-i+1][col] && game->chessMap[row-i][col] == game->chessMap[row-i+2][col] && game->chessMap[row-i][col] == game->chessMap[row-i+3][col] && game->chessMap[row-i][col] == game->chessMap[row-i+4][col]){return true;} } // “/”方向 for(int i = 0; i < 5; i++){ if(row + i < BOARD_GRAD_SIZE && row + i - 4 >= 0 && col - i >= 0 && col - i + 4 < BOARD_GRAD_SIZE && game->chessMap[row+i][col-i] == game->chessMap[row+i-1][col-i+1] && game->chessMap[row+i][col-i] == game->chessMap[row+i-2][col-i+2] && game->chessMap[row+i][col-i] == game->chessMap[row+i-3][col-i+3] && game->chessMap[row+i][col-i] == game->chessMap[row+i-4][col-i+4]){return true;} } // “\”方向 for(int i = 0; i < 5; i++){ if(row - i >= 0 && row - i - 4 < BOARD_GRAD_SIZE && col - i >= 0 && col - i + 4 < BOARD_GRAD_SIZE && game->chessMap[row-i][col-i] == game->chessMap[row-i+1][col-i+1] && game->chessMap[row-i][col-i] == game->chessMap[row-i+2][col-i+2] && game->chessMap[row-i][col-i] == game->chessMap[row-i+3][col-i+3] && game->chessMap[row-i][col-i] == game->chessMap[row-i+4][col-i+4]){return true;} } return false; }调用接口main.cpp#include <stdio.h>bool checkOver(){ if(checkWin(&game, clickPosRow, clickPosCol)){ Sleep(1500); if(game.playFlag = false){ //黑棋胜利，此时标记已经转为白棋落子 mciSendString("play res/不错.mp3", 0, 0, 0); loadimage(0, "res/胜利.jpg"); score += 100; //更新分数 }else{ mciSendString("play res/失败.mp3", 0, 0, 0); loadimage(0, "res/失败.jpg"); score -= 100; //同理 } //用于显示分数 char scoreText[64]; sprintf(scoreText, "当前分数：%d", score); outtextxy(310, 800, scoreText); //记录分数 FILE* fp = fopen("score.data", "wb"); fwrite(&score, sizeof(score), 1, fp); fclose(fp); getch(); return true; } return false; }显示分数main.cpp#define INIT_SCORE 1000 int score; //全局变量void initScore(){ //分数字体设置 settextcolor(WHITE); //color settextstyle(50, 0, "微软雅黑"); //style FILE *fp = fopen("score.data", "rb"); if(fp == NULL){ score = INIT_SCORE; }else{ fread(&score, sizeof(score), 1, fp); } if (fp)fclose(fp); }然后在main.cpp文件的 init() 函数定义中 继续 添加代码initScore(); //这一行添加到init()函数定义中结束到这里五子棋的全部功能已经实现了，但是你会发现程序无法运行。这是理所当然的，因为我们的main函数还没有写，在下一篇文章（也就是本项目的最后一章）我们将完善main函数并做该项目的总结。
AI思考落子点在之前我们已经实现计算权值了，现在要想让AI落子，应根据之前的计算结果使棋子落在分值最大点上。当然可能会出现多个分值相同的最大点，这时在其中随机取一个点落下即可。chessData.htypedef struct point{ int row; int col; } point_t; //机器下棋 point_t actionAI(ChessData* data); chessData.cpp#include <time.h> #include <stdlib.h> #include <vector>point_t actionAI(ChessData* data){ //计算评分 calcScore(data); //找出最大分数位置 int maxScore = 0; std::vector<std::pair<int, int>> maxPoints; int k = 0; for(int row = 0; row < BOARD_GRAD_SIZE; row++){ for(int col = 0; col < BOARD_GRAD_SIZE; col++){ //若该坐标为空 if(data->chessMap[row][col] == 0){ //找出最大数和坐标 if(data->scoreMap[row][col] > maxScore){ maxScore.clear(); k = 0; maxScore.push_back(std::make_pair(row, col)); k++; }else if(data->scoreMap[row][col] == maxScore){ maxPoints.push_back(std::make_pair(row, col)); k++; } } } } //如果有多个点随机落子 srand((unsigned)time(0)); int index = rend() % k; return maxPoints[index]; }实现AI落子main.cppvoid AI_GO(){ point_t point = actionAI(&game); clickPosRow = point.row; clickPosCol = point.col; Sleep(1000); chessDown(clickPosRow, clickPosCol, CHESS_WHITE); updateGameMap(&game, clickPosRow, clickPosCol); }
原理计算计算每个落子点的“权值”，找到权值最大的落子点对于每个空白点，分别计算周围的八个方向不妨以该空白点作为参照原点，以水平向右作为X轴正方向，以竖直向下为Y轴正方向建立平面直角坐标系因为在计算某个方向时，正向和反向需同时考虑，实际上只需要四个方向，即向量(1,0)的方向、向量(1,1)方向、向量(0,1)方向和向量(-1,1)方向，图示如下(灵魂画图，请勿吐槽 滑稽)走棋原理产生效果黑棋走这个点产生效果评分连210死330活340死460活4200连520000如果白棋（AI）走这个点产生效果评分连15连210死325活350死455活4300连530000权值计算chessData.hvoid calcScore(ChessData* data);chessData.cpp#include <string>void calcScore(ChessData* data){ if(!data) return; //统计玩家或AI连子 int personNum = 0; //玩家 int botNum = 0; //AI int emptyNum = 0; //各方向空白位数 //清空评分数组 memset(data->scoreMap, 0, sizeof(data->scoreMap)); for (int row = 0; row < BOARD_GRAD_SIZE; row++){ for(int col = 0; col < BOARD_GRAD_SIZE; col++){ //空白点计算 if(row >= 0 && col >= 0 && data->chessMap[row][col] == 0){ //遍历四个方向，然后分别计算正反四个方向 int directs[4][2] = {{1,0}, {1,1}, {0,1}, {-1,1}}; for(int k = 0; k < 4; k++){ int x = directs[k][0]; int y = directs[k][1]; //重置 personNum = 0; botNum = 0; emptyNum = 0; //对黑棋评分(正向) for(int i = 1; i <= 4; i++){ if(row + i * y >= 0 && row + i * y < BOARD_GRAD_SIZE && col + i * x >= 0 && col + i * x < BOARD_GRAD_SIZE && data->chessMap[row + i * y][col + i * x] == 1){ //玩家的子 personNum++; }else if(row + i * y >= 0 && row + i * y < BOARD_GRAD_SIZE && col + i * x >= 0 && col + i * x < BOARD_GRAD_SIZE && data->chessMap[row + i * y][col + i * x] == 0){ //空白位 emptyNum++; break; //遇到空白位置停止该方向搜索 }else{ break; //出边界或遇到白棋停止搜索 } } //对黑棋评分(反向) for(int i = 1; i<= 4; i++){ if(row - i * y >= 0 && row - i * y < BOARD_GRAD_SIZE && col - i * x >= 0 && col - i * x <BOARD_GRAD_SIZE && data->chessMap[row - i * y][col - i * x] == 1){ personNum++; }else if(row - i * y >= 0 && row - i * y <BOARD_GRAD_SIZE && col - i * x >= 0 && col - i * x < BOARD_GRAD_SIZE && data->chessMap[row -i * y][col - i * x] == 0){ emptyNum++; break; }else{ break; } } if(personNum == 1){ data->scoreMap[row][col] += 10; }else if(personNum == 2){ if(emptyNum == 1){ //死3 data->scoreMap[row][col] += 30; }else if(emptyNum == 2){ //活3 data->scoreMap[row][col] += 40; } }else if(personNum == 3){ if(empty == 1){ //死4 data->scoreMap[row][col] += 60; }else if (emptyNum == 2){ //活4 data->scoreMap[row][col] += 200; } }else if(personNum == 4){ data->scoreMap[row][col] += 20000; } emptyNum = 0; //清空 //对白棋评分（正向） for(int i = 1; i <= 4; i++){ if(row + i * y > 0 && row + i * y < BOARD_GRAD_SIZE && col + i * x > 0 && col + i * x < BOARD_GARD_SIZE && data->chessMap[row + i * y][col + i * x == -1]){ botNum++; }else if(row + i * y >0 && row + i * y < BOARD_GRAD_SIZE && col + i * x > 0 && col + i * x < BOARD_GRAD_SIZE && data->chessMap[row + i * y][col + i *x] == 0){ emptyNum++; break; }else{ break; } } //白棋评分(反向) for(int i = 1; i <= 4; i++){ if(row - i * y > 0 && row - i * y <BOARD_GRAD_SIZE && col - i * x > 0 && col - i * x < BOARD_GRAD_SIZE && data->chessMap[row - i * y][col -i * x] == -1){ botNum++; }else if (row - i * y >0 && row - i * y < BOARD_GRAD_SIZE && col - i * x > 0 && col - i * x < BOARD_GRAD_SIZE && data->chessMap[row - i * y][col - i * x] == 0){ emptyNum++; break; }else{ break; //出边界 } } if(botNum == 0){ //连1 data->scoreMap[row][col] += 5; }else if(botNum == 1){ //活2 data->scoreMap[row][col] += 10; }else if(botNum == 2){ if(emptyNum == 1){ //死3 data->scoreMap[row][col] += 25; }else if(emptyNum == 2){ //活3 data->scoreMap[row][col] += 50; } }else if(botNum == 3){ if(emptyNum == 1){ //死4 data->scoreMap[row][col] += 55; }else if(botNum == 2){ //活4 data->scoreMap[row][col] += 300; } }else if(botNum >= 4){ //活5 data->scoreMap[row][col] += 30000; } } } } } }
分析在鼠标左键点击时，我们不能让新棋子在已有棋子的位置落下，同时我们还要让棋子在规定位置落下——棋盘线的交点处。功能实现创建数据类型创建头文件chessData.h和源文件chessData.cpp在chessData.cpp和main.cpp中分别引用头文件chessData.h#include "chessData.h"将之前在main.cpp中写的棋盘数据剪贴到chessData.h中const float BLOCKSIZE = 67.4; const int BLOCK_GRAD_SIZE = 13;在chessData.h中定义常量 POS_OFFSET，即鼠标有效点击距离上限const int POS_OFFSET = BLOCKSIZE * 0.4;在chessData.h定义结构体 ChessDatastruct ChessData{ //储存当前游戏棋盘的情况，空白为0，黑棋为1，白棋为-1 int chessMap[BLOCK_GRAD_SIZE][BLOCK_GRAD_SIZE]; //储存各点的评分情况，用于之后的AI走棋 int scoreMap[BLOCK_GRAD_SIZE][BLOCK_GRAD_SIZE]; bool playFlag; //表示下棋放，true黑棋，false白棋（AI） };在main.cpp中添加全局变量gameChessData game;初始化数据类型在chessData.h中添加函数声明void initChessData(ChessData*);在chessData.cppvoid initChessData(ChessData *data){ if(!data) return; memset(data->chessMap,0,sizeof(data->chessMap)); memset(data->scoreMap,0,sizeof(data->scoreMap)); data->playFlag = true; }判断有效点击算法原理实现在main.cpp中添加全局变量int clickPosRow, clickPosCol; //用于储存点击位置定义函数clickBoard()//有效点击返回true，无效点击返回false //MOUSEMSG为鼠标信息类型 bool clickBoard(MOUSEMSG msg){ //（鼠标点击坐标 - 边界长度）/ 格宽 = 行（列）数 //计算列数并取整 int col = (msg.x - MARGIN_X) / BLOCKSIZE; //计算行数取整 int row = (msg.y - MARGIN_Y) / BLOCKSIZE; //计算棋子正确坐标，即格子左上角棋子应在的棋盘格线交点处坐标 int leftTopPosX = MARGIN_X + BLOCKSIZE * col; int leftTopPosY = MARGIN_Y + BLOCKSIZE * row; }在文件最上方引用头文件math.h用于后期计算#include <math.h>之后要判断棋子应在四个交点中具体哪一点上，这里我们用一个do-while循环。继续添加clickBoard()函数的定义int len; //用于计算两点见的距离（根据中学所学两点间距离公式） int selectPos = false; //作为返回值 do{ //左上角 len = sqrt((msg.x - leftTopPosX)*(msg.x - leftTopPosX) + (y - leftTopPosY)*(msg.y - leftTopPosY)); if(len < POS_OFFSET){ clickPosRow = row; clickPosCol = col; if ( game.chessMap[clickPosRow][clickPosCol] == 0 ){ selectPos = true; } break; } //右上角 len = sqrt((msg.x - leftTopPosX - BLOCKSIZE)*(msg.x - leftTopPosX - BLOCKSIZE) + (y - leftTopPosY)*(msg.y - leftTopPosY)); if(len < POS_OFFSET){ clickPosRow = row; clickPosCol = col + 1; if ( game.chessMap[clickPosRow][clickPosCol] == 0 ){ selectPos = true; } break; } //左下角 len = sqrt((msg.x - leftTopPosX)*(msg.x - leftTopPosX) + (y - leftTopPosY - BLOCKSIZE)*(msg.y - leftTopPosY - BLOCKSIZE)); if(len < POS_OFFSET){ clickPosRow = row + 1; clickPosCol = col; if ( game.chessMap[clickPosRow][clickPosCol] == 0 ){ selectPos = true; } break; } //右下角 len = sqrt((msg.x - leftTopPosX - BLOCKSIZE)*(msg.x - leftTopPosX - BLOCKSIZE) + (y - leftTopPosY - BLOCKSIZE)*(msg.y - leftTopPosY - BLOCKSIZE)); if(len < POS_OFFSET){ clickPosRow = row + 1; clickPosCol = col + 1; if ( game.chessMap[clickPosRow][clickPosCol] == 0 ){ selectPos = true; } break; } }while(0); return selectPos;更新底层数据chessData.hvoid updateGameMap(ChessData* data, int row, int col);chessData.cppvoid updateGameMap(ChessData* data, int row, int col){ if(!data) return; if(data->playFlag){ data->chessMap[row][col] = 1; }else{ data->chessMap[row][col] = -1; } data->playFlag = !data->playFlag; //换下棋方 }main.cpp//玩家走棋 void manGo(){ chessDown(clickPosRow,clickPosCol,CHESS_BLACK); updateGameMap(&game, clickPosRow, clickPosCol); }
准备我们首先要在程序中定义一个名为drawPNG的函数，用于输出png格式图片并使背景透明引入头文件(需要提前安装EasyX)#include <graphics.h>定义函数 drawPNGvoid drawPNG(IMAGE* picture, int picture_x, int picture_y) //x为载入图片的X坐标，y为Y坐标 { // 变量初始化 DWORD* dst = GetImageBuffer(); // GetImageBuffer()函数，用于获取绘图设备的显存指针，EASYX自带 DWORD* draw = GetImageBuffer(); DWORD* src = GetImageBuffer(picture); //获取picture的显存指针 int picture_width = picture->getwidth(); //获取picture的宽度，EASYX自带 int picture_height = picture->getheight(); //获取picture的高度，EASYX自带 int graphWidth = getwidth(); //获取绘图区的宽度，EASYX自带 int graphHeight = getheight(); //获取绘图区的高度，EASYX自带 int dstX = 0; //在显存里像素的角标 // 实现透明贴图 公式： Cp=αp*FP+(1-αp)*BP ， 贝叶斯定理来进行点颜色的概率计算 for (int iy = 0; iy < picture_height; iy++) { for (int ix = 0; ix < picture_width; ix++) { int srcX = ix + iy * picture_width; //在显存里像素的角标 int sa = ((src[srcX] & 0xff000000) >> 24); //0xAArrggbb;AA是透明度 int sr = ((src[srcX] & 0xff0000) >> 16); //获取RGB里的R int sg = ((src[srcX] & 0xff00) >> 8); //G int sb = src[srcX] & 0xff; //B if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight) { dstX = (ix + picture_x) + (iy + picture_y) * graphWidth; //在显存里像素的角标 int dr = ((dst[dstX] & 0xff0000) >> 16); int dg = ((dst[dstX] & 0xff00) >> 8); int db = dst[dstX] & 0xff; draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16) //公式： Cp=αp*FP+(1-αp)*BP ； αp=sa/255 , FP=sr , BP=dr | ((sg * sa / 255 + dg * (255 - sa) / 255) << 8) //αp=sa/255 , FP=sg , BP=dg | (sb * sa / 255 + db * (255 - sa) / 255); //αp=sa/255 , FP=sb , BP=db } } } }初始化游戏创建游戏界面素材大小通过查看res素材文件夹下的棋盘2.jpg可知，图片大小为897*895通过图片大小计算可得一个格子的大小为67.4先记下这些参数，后面对我们开发特别重要创建窗口首先，定义 float类型常量 BLOCKSIZE，即格子大小const float BLOCKSIZE = 67.4;然后定义 init() 如下void init(){ initgraph(897,895); //创建897*895大小的窗口，与棋盘2.jpg大小对应 loadimage(0,"res/棋盘2.jpg"); //加载图片到窗口 }加载音乐引入播放音乐的 头文件 mmsystem.h#include <mmsystem.h>加载库 winmm.lib#pragma comment(lib,"winmm.lib");继续在 init() 函数中添加播放 提示语音(res/start.wav) 的语句(注意添加位置)mciSendString("play res/start.wav",0,0,0); //提示下棋语音棋子渲染加载素材定义 IMAGE 类型的全局变量 chessWhite 和 chessBlackIMAGE chessWhite; //黑棋子变量 IMAGE chessBlack; //白棋子变量在 init() 函数定义中添加加载图片语句如下(将black.png白棋子素材和white.png黑棋子素材加载到变量)//长和宽都是BLOCKSIZE，最后一个true参数表示原比例缩放防止图片被截断 loadimage(&chessBlack, "res/black.png",BLOCKSIZE,BLOCKSIZE,true); loadimage(&chessWhite, "res/white.png", BLOCKSIZE, BLOCKSIZE, true);实现渲染定义棋子种类typedef enum{ CHESS_WHITE = -1, CHESS_BLACK = 1 } chess_kind_t;在实现输出棋子之前我们需要来看几个数据MARGIN_X为上边界大小，MARGIN_Y为左边界大小，因此我们定义同名全局常量const int MARGIN_X = 44; const int MARGIN_Y = 43; const int BOARD_GRAD_SIZE = 13; //13*13棋盘大小之后定义函数 chessDown() 用于打印棋子图片void chessDown(int row, int col, chess_kind_t kind){ mciSendString("play res/down7.wav",0,0,0); //播放下棋音乐 //EasyX是以窗口左上角为坐标原点 //定义棋子x横坐标 int x = MARGIN_X + col * BLOCKSIZE - 0.5 * BLOCKSIZE; //定义棋子y纵坐标 int y = MARGIN_Y + row * BLOCKSIZE - 0.5 * BLOCKSIZE; //判断棋子种类并打印 if(kind == CHESS_WHITE){ drawPNG(&chessWhite,x,y); }else{ drawPNG(&chessBlack,x,y); } }结束到了这里，我们已经实现了游戏界面的初始化和棋子渲染了，接下来我们就要实现获取鼠标信息来判断有效点击了