这个项目有启发意义,棋盘不是绘制的,而是图片贴上去的。
考察:坐标,pixmap,黑白棋的逻辑
想了解传统的绘制方式和棋类AI的传统高级方法,请看我关于五子棋的系列文章
开源项目:
https://github.com/zhuimengshaonian66/BlackWriteChess
第一步:
绘制UI
棋盘是一张图,这个项目的难点是找坐标。但是我们有不传的法宝,通过鼠标的移动时间轻松搞定。
将起点坐标和方格的大小都记录下来。
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
//去除边框
this->setWindowFlags(Qt::FramelessWindowHint);
startPoint = QPoint(147,68); //起点坐标
endPoint = QPoint(1003,670); //终点坐标
gridWidth = (endPoint.x() - startPoint.x())/8;
gridHigh = (endPoint.y() - startPoint.y())/8;
initChess();
connect(&timer, &QTimer::timeout, this, &Widget::machinePlay);
}
初始化棋盘,用
chessStatus数组的状态代表棋盘的状态。
void Widget::initChess()
{
whiteFlag = blackFlag = true;//一开始双方都能落子
overFlag = false; //一开始游戏不结束
bNumber = wNumber = 0;
memset(chessStatus,0,sizeof(chessStatus));
//中间四子:两白两黑
chessStatus[3][3] = Black;
chessStatus[4][3] = White;
chessStatus[3][4] = White;
chessStatus[4][4] = Black;
role = White; //默认白子先下
ui->labelWhite->show();
ui->labelBlack->hide();
ui->lcdNumberWhite->display(2);
ui->lcdNumberBlack->display(2);
}
绘制如下:这个项目的重点就是在一开始就把棋盘的起点坐标和方格的大小都知道了,从而当鼠标点击的时候,就会知道点击的是哪个方格。
void Widget::paintEvent(QPaintEvent *)
{
//绘图事件,画棋盘,画棋子
QPainter p(this); //创建画家,指定窗口为画布
//背景图(棋盘)
p.drawPixmap(this->rect(),QPixmap(":/image/chess2.png"));
int startX = 0;
int startY = 0;
//根据棋盘二维数组的状态画棋子
for(int i = 0; i < 8; i++){
for(int j = 0; j < 8; j++){
startX = startPoint.x() + i*(gridWidth+1);
startY = startPoint.y() + j*(gridHigh+1);
if(chessStatus[i][j] == Black){
//画黑子
p.drawPixmap(startX, startY,
gridWidth-7, gridHigh-4, QPixmap(":/image/Black.png"));
}
else if(chessStatus[i][j] == White){
//画白子
p.drawPixmap(startX, startY,
gridWidth-7, gridHigh-4, QPixmap(":/image/White.png"));
}
}
}
}
黑白棋的逻辑如下:
int Widget::judgeRole(int x, int y, ChessFlag currentRole, bool eatChess)
{
//落子的八个方向
int dir[8][2] = {{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1},{0,1},{1,1}};
int tmpX = x,tmpY = y; //临时保存棋盘坐标位置
int i = 0, eatNum = 0; //初始化数据
if(chessStatus[tmpX][tmpY] != Empty){//如果此方格中已经有棋子无效操作,直接返回
return 0;
}
//棋盘的8个方向
for(i=0; i<8; i++){
//从鼠标点击点开始
tmpX = x;
tmpY = y;
//确定一个方向:dir[i][0]
tmpX += dir[i][0];
tmpY += dir[i][1];
if(tmpX < GRID_NUMBER && tmpX >= 0 && tmpY < GRID_NUMBER && tmpY >= 0
&& (chessStatus[tmpX][tmpY] != currentRole) && (chessStatus[tmpX][tmpY] != Empty)){
//如果没有出界并且相邻棋子是对方棋子才有可能吃子
tmpX += dir[i][0];
tmpY += dir[i][1];//向前走一步,开始判断该方向还有无自己棋子
while(tmpX < GRID_NUMBER && tmpX >= 0 && tmpY < GRID_NUMBER && tmpY >= 0){
if(chessStatus[tmpX][tmpY] == Empty){
break;//遇到空位跳出循环,外部if最后一个条件也会不满足,则直接判断下一个方向
}
if(chessStatus[tmpX][tmpY] == currentRole){//找到自己的棋子代表可以吃子
//能吃子则点击点标记为自己的棋子,update后是自己的棋子,否则点击处不能落子
(true == eatChess) ? (chessStatus[x][y] = currentRole) : true;
tmpX -= dir[i][0];
tmpY -= dir[i][1];//回退一步开始吃子
//没有回到开始的位置就继续执行
while((tmpX != x) || (tmpY != y)){//没有回退到点击处则继续修改原有棋子状态
//若为true则为落子,修改为自己的棋子,如果为false则为测试,不用修改
(true == eatChess) ? (chessStatus[tmpX][tmpY] = currentRole) : true;
tmpX -= dir[i][0];
tmpY -= dir[i][1];//回退一格,继续
eatNum++; //吃子累计
}
break;//跳出循环,结束该方向的判断
}
//没找到自己的棋子就向前(指定方向)走一步,走到0或GRID_NUMBER边界条件时就结束该层if语句
tmpX += dir[i][0];
tmpY += dir[i][1];
}
}
}
return eatNum;
}
电脑下子时:找到的是最大吃子的位置,而且调用的是
judgeRole同一个方法,
void Widget::machinePlay()
{
timer.stop();//定时器停止
int max = 0, num = 0;
int px = 0, py = 0;
//判断能落子的最大值
for(int i = 0; i<8; i++){
for(int j = 0; j<8; j++){
//判断能吃的位置,机器为黑子
if( num = judgeRole(i, j, role, false) > max){//寻找最多吃子的位置
max = num;
px = i;
py = j;
}
}
}
if(max == 0){//没有可走的了
if((blackFlag | whiteFlag)){
WHITE_FLAG();
BLACK_FLAG();
changeRole();
return;
}
else{
if(overFlag == false){
overFlag = true;
//修改标志位并弹出结果提示信息,在函数返回时另一个gameOver()就不会执行了
gameOver();
}
}
}
//吃子
if(judgeRole(px, py, Black) > 0){
blackFlag = true; //机器落子则将黑子标志位置为true
changeRole();
update();
}
}