chineseChess
最近学习了chineseChess的Qt实现,把一些东西总结一下:
实现功能:
1.人人对战
2.人机对战
3.网络版
一、基础性工作:(人人对战)
1、棋盘和棋子的绘制(QPinter,drawLine(QPoint(0,0),QPoint(0,9))):
棋盘: 10行,9列,中间有楚河汉界;九宫格;兵和炮的梅花位置。
棋子:32颗棋子都是由圆圈和汉字组成:drawStone(painter, i)
注意:1、死棋不画 if(_s[id]._dead) return;
2、线:painter.setPen(Qt::black); 填充:painter.setBrush(QBrush(Qt::gray)); 字体:painter.setFont(QFont("system", _r, 700));
字:painter.drawText(rect, _s[id].getText(), QTextOption(Qt::AlignCenter)); getText()可用switch语句实现,返回QString类型。
2、初始化棋盘
每个棋子有自己的 _id , _row , _col , _type , _Red, _dead:
棋子的id可由数组_s[32]存储,由于棋子的中心对称性,棋子的 id 分配红棋从左上角开始是从0到15,黑棋从右下角开始是从16到31;
这么分配的优势,中心对称的两颗棋子,行列和为定值:row(red)+row(black)=9; col(red)+col(black)=8;
_s[i]._red = id<16;
_s[i].dead = false;
type有七种:车、马、象、士、将、炮、兵
红棋的 _row , _col ,_type可定义结构体数组一一初始化,黑棋可利用对称性给出。
棋子类型可使用枚举方法: enum TYPE{CHE, MA, XIANG, SHI, JIANG, PAO ,BING }_type;
枚举类型,系统默认第一个元素为0,其后元素依次加1。 不可为枚举类型赋值,但可以在初始化时给其赋值。
eg: enum TYPE{CHE=2, MA, XIANG, SHI, JIANG, PAO ,BING }; 则XIANG为4,SHI为5。
3、走棋规则的定制
七种棋判断是否可以移动,先不判断是否吃棋。
canMove(int moveId, int killedId, int row, int col)
利用switch(_s[moveId]._type)语句 返回 每种棋子的 canMoveType()函数。
特点:
车走直线,需判断 起始点 之间棋子个数是否为0;
马走日,需判断马蹄上是否有棋子;
象走田,需判断象眼上是否有棋子;且行走范围只有半边天;顶部和底部的象,规则和而不同。
士,活动范围九宫格的斜线;顶部和底部的士,规则和而不同。
将,飞将:若killId != -1 && _s[killId]._type == Stone::JIANG,若两将之间棋子个数为0,return ture;九宫格的直线;顶部和底部的将,规则和而不同。
炮,计算 起始点 之间棋子个数。棋子个数为1,则killedId != -1;棋子个数为0,则killedId == -1.
兵,不能后退,过河后才能横向走。顶部和底部的兵,规则和而不同。
为了实现七种棋子的可走性判断,需要一些辅助函数:起始点的位置关系(利用权重求和即可)、起始点之间的棋子个数、初始化在底部的棋子判断。
int relation(row1, col1, row, col)
{
return qAbs(col1 - col) + qAbs(*(row1 - row) );
}
int getStoneConutAtLine(int row1, int col1, int row2, int col2) //车和炮需要判断,特点:行和列仅有一个相同
{
if(row1 != row2 && col1 == col2)
{
int min = row1 < row2 ? row1 : row2;
int max = row1 > row2 ? row1 : row2;
for(int row = min + ; row < max; ++row)
{
if(getStoneId(row, col1) != -) ++count; //遍历非死棋,返回 行列 满足所求的id
}
}
else if(row1 == row2 && col1 != col2)
{
int min = col1 < col2 ? col1 : col2;
int max = col1 > col2 ? col1 : col2;
for(int col = min + ; col < max; ++col)
{
if(getStoneId(row1, col) != -) ++count;
}
}
else
return -; return count;
}
bool isBottomSide(int id)
{
return _bRedSibe == _s[id]._red; //_bRedSide表示红棋在下边。网络版红黑边不同,初始化利用中心对称,rotate即可
}
4、走棋
第一步:鼠标点击 到 棋子id 的转换
利用mouseReleaseEvent(QMouseEvent *ev)事件将鼠标点击像素位置传给 ev,调用ev->pos()即可返回点击点的像素坐标;
利用帮助函数(下面有实现)getClickRowCol( QPoint pt, int &row, int &col)可获取行列坐标;
利用getStoneId(row, col)可获取棋子id,从而实现了由鼠标点击到棋子id的转换。
利用virtual void click(int id, int row, int col)来实现棋子的移动;虚函数:可以在其他类型的作战中重新实现click函数,实现其多态性。
注:
void Board::click(int id, int row, int col)
{
trySelectStone(id); //与执棋方颜色一致,可选。连续选择执棋方,换棋。选后,this->_selectid == id
else if(this->_selectid != -) //轮到执棋方走时,选择空白地或是对方棋子无效
{
tryMove(id, row, col);
}
}
void trySelect(int id)
{
if(canSelect(id))
{
_selectId = id;
update();
rerurn;
}
}
bool canSelect(id)
{
return _bRedTurn == _s[id].red;
}
尝试走棋。分两种情况,点击无棋处尝试走棋,点击异色尝试吃棋。
void tryMove(int killid, int row, int col)
{
bool ret = canMove(_selectId, killId, row, col);
if(ret)
{
moveStone(_selectId, killId, row, col);
_selectId = -; //_selectId初始化
update();
}
}
void Board::moveStone(int moveid, int killid, int row, int col)
{
saveStep(moveid, killid, row, col, _steps); //保存步骤用于悔棋 killStone(killid); //吃棋
moveStone(moveid, row, col); //走棋,轮换
}
void Board::moveStone(int moveId, int row, int col)
{
_s[moveId]._row = row;
_s[moveId]._col = col;
_bRedTurn = !_bRedTurn; //轮换走棋
}
void Board::killStone(int id)
{
if(id==-) return;
_s[id]._dead = true;
}
void Board::saveStep(int moveid, int killid, int row, int col, QVector<Step*>& steps)
{
GetRowCol(row1, col1, moveid);
Step* step = new Step;
step->_colFrom = col1;
step->_colTo = col;
step->_rowFrom = row1;
step->_rowTo = row;
step->_moveid = moveid;
step->_killid = killid; steps.append(step);
}
help function:
1、_id可以通过_s[_id]._row获取行坐标:设棋盘一格为d,简单起见,设棋盘与边界距离也为d(注意d为棋盘像素坐标)根据棋盘行列与d的关系
不难推出id对应的棋盘像素坐标。 eg. point.rx() = (col + 1)*_r*2;
2、由棋盘上的 点击点QPoint 确定属于那个棋位(row,col)。
一种可行的方法是:遍历棋盘各个行列坐标,由行列坐标转化为棋盘像素坐标,并求取与 点击点的像素坐标 的距离dist,若dist<_r,则改点行列式即为所求行列值。其中 _r 为棋盘一格长度的一半。
改进1:只需要遍历点击点附近的坐标,无需遍历整个棋盘。
改进2:利用浮点型,整型转换直接找到
bool Board::getClickRowCol( QPoint pt, int &row, int &col)
{ col = d * ( int ( pt.x()/(d*1.0) - 0.5 ) );
row = d * ( int ( pt.y()/(d*1.0) - 0.5 ) );
if(row < 0 || row > 9 || col < 0 || col > 8)
rerurn false;
else
return true;
}
注:返回值可用于判断行列式是否成功得到;利用引用将行列值传递进去,可以在成功获取行列值后进行保存,用法挺好,学习一下。
二、人机对战(最大最小值算法,减枝优化算法)
利用虚函数的性质,实现click(int id, int row, int col)函数的多态性。
红棋走就调用父类的click(int id, int row, int col)函数,走完后转换走棋方,让电脑走棋。由于电脑在考虑多步时,时间很久容易阻塞主进程,所以在电脑走棋前可以调用定时器0.1s,让红旗完成走棋,重绘棋盘。
电脑利用最大最小值算法按评分最大值的步骤进行移动,即:
void SingleGame::computerMove()
{
Step* step = getBestMove();
moveStone(step->_moveid, step->_killid, step->_rowTo, step->_colTo);
delete step; //防止内存泄露
update();
}
最大最小值算法:在你选择的箱子中,找到每个箱子最小值中最大的那个箱子。
步骤:
1、找到所有可以走的步骤
2、尝试走一步(选箱子)
3、计算这一步中最小的分数
4、最小分数>预定义的最大值(很小的值)就将该分数保存为当前的最大值。
5、退回尝试走的一步,进行下一步尝试
Step* SingleGame::getBestMove()
{
Step* ret = NULL;
QVector<Step*> steps;
getAllPossibleMove(steps); //保存可能走的步子
int maxInAllMinScore = -; while(steps.count())
{
Step* step = steps.last();
steps.removeLast(); fakeMove(step);
int minScore = getMinScore(this->_level-, maxInAllMinScore); //电脑走后,人走的左右步骤后电脑可得的最低分返回
unfakeMove(step); if(minScore > maxInAllMinScore)
{
if(ret) delete ret;
ret = step;
maxInAllMinScore = minScore;
}
else
{
delete step;
}
}
return ret;
}
运用剪枝算法的最小分:
int SingleGame::getMinScore(int level, int curMin)
{
if(level == )
return score(); QVector<Step*> steps;
getAllPossibleMove(steps);
int minInAllMaxScore = ; while(steps.count())
{
Step* step = steps.last();
steps.removeLast(); fakeMove(step);
int maxScore = getMaxScore(level-, minInAllMaxScore);
unfakeMove(step);
delete step; if(maxScore <= curMin) //上层是找最小分中的最大分,所以该层所求分数小于上层的最大分,则其后就不必计算,即减枝
{
while(steps.count())
{
Step* step = steps.last();
steps.removeLast();
delete step;
}
return maxScore;
} if(maxScore < minInAllMaxScore) //找该箱子的最小分,如果有更小的分数,则保存该分数。
{
minInAllMaxScore = maxScore;
} }
return minInAllMaxScore;
}
运用剪枝算法的最大分:
int SingleGame::getMinScore(int level, int curMin)
{
if(level == )
return score(); QVector<Step*> steps;
getAllPossibleMove(steps);
int minInAllMaxScore = ; while(steps.count())
{
Step* step = steps.last();
steps.removeLast(); fakeMove(step);
int maxScore = getMaxScore(level-, minInAllMaxScore);
unfakeMove(step);
delete step; if(maxScore <= curMin)
{
while(steps.count())
{
Step* step = steps.last();
steps.removeLast();
delete step;
}
return maxScore;
} if(maxScore < minInAllMaxScore)
{
minInAllMaxScore = maxScore;
} }
return minInAllMaxScore;
}
评分:
由于是电脑判断当前分数来走棋,所以将评分定义为黑棋现有分数-红棋现有分数。
定义一个数组,分别存放相应七类棋的分数,统计一下相应颜色活期的总分,即为该方现有分数。
统计所有可走的步骤:(可优化)
_bRedTurn:遍历id为0到15的活棋子;
!_bRedTurn:遍历id为16到31的活棋子。
遇到死棋,跳过。
走棋时遇到同色棋子跳过,遍历整个棋盘其余可走可吃的位置。
void SingleGame::getAllPossibleMove(QVector<Step *> &steps)
{
int min, max;
if(this->_bRedTurn)
{
min = , max = ;
}
else
{
min = , max = ;
} for(int i=min;i<max; i++)
{
if(this->_s[i]._dead) continue; //死棋不计
for(int row = ; row<=; ++row)
{
for(int col=; col<=; ++col)
{
int killid = this->getStoneId(row, col);
if(sameColor(i, killid)) continue; //不杀同类 if(canMove(i, killid, row, col))
{
saveStep(i, killid, row, col, steps);
}
}
}
}
}
void Board::saveStep(int moveid, int killid, int row, int col, QVector<Step*>& steps) //使用容器对步伐进行保存
{
GetRowCol(row1, col1, moveid);
Step* step = new Step;
step->_colFrom = col1;
step->_colTo = col;
step->_rowFrom = row1;
step->_rowTo = row;
step->_moveid = moveid;
step->_killid = killid; steps.append(step);
}
优化:
可以对棋子分类别进行可行区域的计算。
将:前后左右四个位置,满足:1、九宫格内;2、不杀同类;
士:也是最多四个位置,条件同将;
车:行为当前车的行,列从0到8;&& 列为当前车的列,行从0到9。条件:同类不杀,起始点间棋子个数为0。
马:可走位置最多8个。条件:在棋盘内;马蹄无字;同类不吃。
象:最多四个位置,条件:在棋盘内;象眼无子;同类不吃。
炮:遍历位置同车。
兵:最多三个位置。
除了黑色部分,其余可以使用canMove()进行判断。
三、网络版(客户端与服务器)
客户端:
连接服务器:connectToHost("host",port);
发送请求:socket->write(" ");
读数据:socket->readAll();
断开连接:socket->close();
服务器:
监听:server->listen(QHostAddress::Any, port);
挑选空闲服务器:socket = server->nextPendingConnection();
服务(写数据):socket->write(" ");
断开连接:socket->close();
NetGame::NetGame(bool server, QWidget *parent) : Board(parent)
{
_server = NULL; //初始化为空
_socket = NULL;
_bServer = server; //在启动服务器和客户端时,初始化为true和false. if(_bServer)
{
_server = new QTcpServer(this);
_server->listen(QHostAddress::Any, );
connect(_server, SIGNAL(newConnection()), this, SLOT(slotNewConnection()));
}
else
{
_socket = new QTcpSocket(this);
_socket->connectToHost(QHostAddress("127.0.0.1"), );
connect(_socket, SIGNAL(readyRead()), this, SLOT(slotDataArrive()));
}
}
初始化棋盘由服务器发送数据,客户端接收。
1、初始化棋盘(flag为1,随机产生第二个数)
走棋由click(int id, int row, int col)虚函数发送数据,服务器和客户端接收,对方按照棋盘中心对称的性质进行转换。
2、走棋(flag为2,id, row, col)
悔棋由back()虚函数执行,己方棋盘红黑各悔一步后,发送flag为3,对方棋盘红黑也各悔一步,保持同步。
3、悔棋(flag为3)
服务器的槽函数:
void NetGame::slotNewConnection()
{
if(_socket) return; //已选到服务器,则返回 _socket = _server->nextPendingConnection(); //接线员连接下一个空闲服务器
connect(_socket, SIGNAL(readyRead()), this, SLOT(slotDataArrive())); //保证click函数调用后,客户端和服务器都可以接受slotDataArrive()槽函数 /* 产生随机数来决定谁走红色 */
bool bRedSide = qrand()%>; //主程序使用qsrand()对种子进行了初始化
init(bRedSide); /* 发送给对方 */
QByteArray buf;
buf.append();
buf.append(bRedSide>0?:); //若bRedSide为1,就将0存在buf数组的第二个位置,发送给客户端
_socket->write(buf);
}
客户端的槽函数:
void NetGame::slotDataArrive()
{
QByteArray buf = _socket->readAll();
switch (buf.at()) {
case :
initFromNetwork(buf); //flag为1,初始化棋盘
break;
case :
clickFromNetwork(buf); //flag为2,给对方发送走棋数据,对方需要根据棋盘的中心对称性质进行转换
break;
case :
backFromNetwork(buf); //flag为3,让对方棋盘红黑各悔一步,与己方保持一致
break;
default:
break;
}
}
flag对应的函数:
void NetGame::backFromNetwork(QByteArray)
{
backOne();
backOne();
}
void NetGame::clickFromNetwork(QByteArray buf)
{
Board::click(buf[], -buf[], -buf[]);
}
void NetGame::initFromNetwork(QByteArray buf)
{
bool bRedSide = buf.at()>?true:false;
init(bRedSide);
}
走棋:虚函数click(int id, int row, int col)的重载:
void NetGame::click(int id, int row, int col)
{
if(_bRedTurn != _bSide) //!(轮到红棋走,红旗在下方) || !(轮到黑棋走,黑旗在下方)
return;
Board::click(id, row, col); /* 发送给对方 */
QByteArray buf;
buf.append();
buf.append(id);
buf.append(row);
buf.append(col);
_socket->write(buf);
}
悔棋:虚函数back()的重载
void NetGame::back()
{
if(_bRedTurn != _bSide)
return;
backOne();
backOne(); QByteArray buf;
buf.append();
_socket->write(buf);
}
chineseChess的更多相关文章
- 不显示cmd窗口运行jar包
今天,打开导出的jar包,发现并不能运行,查看jar包中的META-INF文件夹下的MANIFEST.MF文件,发现MANIFEST.MF中并没有Main-Class,于是,就手动添加相应的信息,本项 ...
- 1.2 中国象棋将帅问题进一步讨论与扩展:如何用1个变量实现N重循环?[chinese chess]
[题目] 假设在中国象棋中只剩下将帅两个棋子,国人都知道基本规则:将帅不能出九宫格,只能上下左右移动,不能斜向移动,同时将帅不能照面.问在这样条件下,所有可能将帅位置.要求在代码中只能使用一个字节存储 ...
- cocos2d-x游戏开发系列教程-中国象棋01-工程文件概述
上一篇博文我们看到了象棋的效果图,这一张我们来看象棋代码的整体概述 让我们先对整个代码框架有个了解. 主目录: 主目录包含内容如上图: classes目录:业务代码 proj.win32:包括main ...
- 中国象棋(IOS)
//// ViewController.m// ChineseChess//// Created by 晚起的蚂蚁 on 2016/11/20.// Copyright © 2016年 晚起的 ...
- Github入门操作实录
到目前为止,我已经工作快5年了,这5年最大的感受就是,框架什么的并不难,只要知道api,就能用起来,一开始会遇到一点问题,但是天下的框架都大同小异,无非是jar包,配置文件,模板代码,jar包可以使用 ...
- Scala进阶之路-并发编程模型Akka入门篇
Scala进阶之路-并发编程模型Akka入门篇 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Akka Actor介绍 1>.Akka介绍 写并发程序很难.程序员不得不处 ...
- Cocos2d-X开发中国象棋《二》project文件概述
我在上一篇博客中介绍了象棋的功能.在接下来的博客中将向大家介绍使用Cocos2d-X怎样一步一步开发中国象棋 开发工具: Cocos2d-X2.2.3 VS2012 项目的文件夹: Classes:存 ...
- 代码管理工具 (含git、npm、gulp)
1 Git 分布式代码管理工具(基于Linux,可在本地进行提交)代码同时储存在本地和服务器中 ① Git基本操作命令 (1)初始化,创建初始化仓库 git init ------- 文件初始化,初始 ...
- 【原创】使用JS封装的一个小型游戏引擎及源码分享
1 /** * @description: 引擎的设计与实现 * @user: xiugang * @time: 2018/10/01 */ /* * V1.0: 引擎实现的基本模块思路 * 1.创建 ...
随机推荐
- Android自动化压力测试之Monkey Test Android常见的错误类型及黑白名单的使用方法(四)
Android常见的错误类型有两种 1.ANR类型 1)在5秒内没有响应输入的事件(例如,按键按下,屏幕触摸) 2)BroadcastReceiver在10秒内没有执行完毕 2.Crash类型 1)异 ...
- loadrunner生成随机身份证和银行卡号
生成银行卡号码: Action() { char card[19] = {'6','2','2','7','0','0','0','0','0','0','0','0','0','0','0','0' ...
- dubbox编译
dubbox编译要在命令行 切记切记 设置JAVA_HOME 设置maven路径 命令编译dubbox 设置M2_HOME环境变量 设置idea M2_HOME dubbox 服务端 http://w ...
- redis 存储session实现session共享
nginx 作为代理 tomcat集群 redis存储共享session nginx采用轮询方式将动态请求反向代理给tomcat,tomcat通过加载相应jar包方式实现获得redis中共享的sess ...
- Android手机自动化测试真机运行
一, 打开手机的USB调试模式 不同的手机有不同的方法打开usb调试模式,可是去网上查一下你手机的调试模式打开办法(http://wenku.baidu.com/view/3077f06c25c ...
- NumPy 学习(3): 通用函数
1. 元素级别的函数 元素级别的函数也就是函数对数组中的每一个元素进行运算.例如: In [10]: arr = np.arange(10) In [11]: np.sqrt(arr) Out[11 ...
- ssl原理,非对称加密握手,对称加密传输
SSL的基本思想是用非对称加密来建立链接(握手阶段),用对称加密来传输数据(传输阶段).这样既保证了密钥分发的安全,也保证了通信的效率. SSL握手,单方服务器认证(一般的浏览器上网) SSL握手,双 ...
- BZOJ 1355 & KMP
BZOJ放这种丝帛我也是醉了... 不过来填一下求最小循环节的坑... 以这道题为例,相同文本串粘起来的串中取一小节,可以把任意一个字符看做文本串头. 那么我们一次KMP求出next函数然后显见,最后 ...
- Android 通信 EventBus
参考: Android 框架炼成 教你如何写组件间通信框架EventBus Android EventBus源码解析 带你深入理解EventBus Android EventBus实战 没听过你就ou ...
- ACM 阶乘之和
阶乘之和 时间限制:3000 ms | 内存限制:65535 KB 难度:3 描述 给你一个非负数整数n,判断n是不是一些数(这些数不允许重复使用,且为正数)的阶乘之和,如9=1!+2!+3! ...