开发工具 Qt5.9
前置知识 , QPainter , paintEvent, mouseReleaseEvent
本次实现的是双人对战的象棋。需要实现的功能如下
- 棋盘的显示
- 棋子的生成
- 棋子的移动
- 走棋
- 轮流下棋
- 判断胜利
棋盘的显示
象棋的棋盘一般如上图所示, 包括9条竖线,10条横线和两个九宫格。两边的竖线是从上到下,其余竖线要分两次画分别画上面和下面。下面开始画
竖线和横线的代码
// 这里 _d 是一个小格子的长度
_d = std::min(this->width(), this->height())/11;
int d = _d;
// 10条横线
for(int i=1; i<=10; i++)
{
painter.drawLine(QPoint(d, i*d), QPoint(9*d, i*d));
}
// 9条竖线
for(int i=1; i<=9; i++)
{
if(i==1 || i==9){
painter.drawLine(QPoint(i*d, d), QPoint(i*d, 10*d));
}
else {
painter.drawLine(QPoint(i*d, d), QPoint(i*d, 5*d));
painter.drawLine(QPoint(i*d, 6*d), QPoint(i*d, 10*d));
}
}
九宫格的代码
// 九宫格
painter.drawLine(QPoint(4*d, d), QPoint(6*d, 3*d));
painter.drawLine(QPoint(6*d, d), QPoint(4*d, 3*d));
// 下九宫
painter.drawLine(QPoint(4*d, 10*d), QPoint(6*d, 8*d));
painter.drawLine(QPoint(6*d, 10*d), QPoint(4*d, 8*d));
这样一个简易的棋盘就画好了
棋子的生成
棋盘画好了,下面需要画棋子,我们先把棋子类创建出来,先想一想棋子大致有什么属性。棋子需要知道自己在棋盘上面的位置,棋子需要一个类型(车马炮…),棋子有颜色之分,棋子有可能被吃。为了方便操作棋子还需要一个编号,所以创建棋子类如下
class Stone
{
public:
enum TYPE { CHE, MA, XIANG, SHI, JIANG, PAO, BING};
Stone();
void init(int id);
int row() { return _row; }
int col() { return _col; }
QString getText();
public:
int _col; // 棋子所在列
int _row; // 棋子所在行
int _id; // 棋子编号
TYPE _type; // 棋子类型
bool _red; // 棋子颜色
bool _dead; // 棋子是否被吃
};
象棋是有初始局面的,所以每个棋子需要初始化,初始化函数 init 如下。因为棋盘是对称的,象棋初始摆盘的位置数组只要 16就可以了。
// 初始化棋子
void Stone::init(int id)
{
struct {
int row, col;
Stone::TYPE type;
} pos[16] = {
{0, 0, Stone::CHE},
{0, 1, Stone::MA},
{0, 2, Stone::XIANG},
{0, 3, Stone::SHI},
{0, 4, Stone::JIANG},
{0, 5, Stone::SHI},
{0, 6, Stone::XIANG},
{0, 7, Stone::MA},
{0, 8, Stone::CHE},
{2, 1, Stone::PAO},
{2, 7, Stone::PAO},
{3, 0, Stone::BING},
{3, 2, Stone::BING},
{3, 4, Stone::BING},
{3, 6, Stone::BING},
{3, 8, Stone::BING}
};
_id = id;
_dead = false;
_red = id<16;
if(id < 16)
{
_row = pos[id].row;
_col = pos[id].col;
_type = pos[id].type;
}
else
{
_row = 9-pos[id-16].row;
_col = 8-pos[id-16].col;
_type = pos[id-16].type;
}
}
现在每一个棋子都初始化好了,接下来是将棋子画到棋盘上面。QPainter提供了画圆的函数,void QPainter::drawEllipse(const QPoint ¢er, int rx, int ry) 需要一个圆心。我们每个棋子都有行列的属性,这里的center是像素位置。所以我们需要一个转换函数将 行列坐标转换成 像素点。
QPoint Board::center(int col, int row)
{
int x = (col+1)*_d;
int y = (row+1)*_d;
return QPoint(x, y);
}
画好圆后,我们填充上颜色并写入文本。一个棋子就好了,代码如下
QPoint c = center(_stone[id].col(), _stone[id].row());
//qDebug() <<"draw" << _stone[id]._row << " : " << _stone[id]._col;
p->setPen(Qt::black);
if(true == _stone[id]._red)
p->setPen(Qt::red);
p->drawEllipse(c, _d/2, _d/2);
p->setFont(QFont("system", _d/2, 700));
// 画字
QRect rect = QRect(c.x()-_d/2, c.y()-_d/2, _d, _d);
p->drawText(rect, _stone[id].getText(), QTextOption(Qt::AlignCenter));
棋子的移动
棋子的移动,其实就是棋子坐标的改变。当我们点击某个棋子后,然后再点击另一个位置。棋子的 col 和 row 值更新一下。然后重新绘一下棋盘,就实现了棋子的移动。 Qt提供了 mouseReleaseEvent() 事件,这个函数在鼠标点击后被调用,提供了 点击的坐标位置。我们重写这个函数,获取每次点击的位置,然后判断是否落在棋盘上,若落在棋盘上,将像素坐标转换成 col 和 row。 然后判断是否点击到某个棋子并记录该棋子的Id(Id 必 >= 0)。因为一次移动要点击两个位置,所以我们需要记住两个位置,一个是当前选中的棋子 _selectId,一个是当前点击的位置clickId。 每次判断之前是否有选中棋子,若有则 更新棋子的row 和 col,如果目标位置也有棋子(clickId > 0)就将 clickId 的那颗棋子的属性_dead 设置为 true。 若之前未选中棋子,则将 clickId 赋值给 selectId。 上述步骤做好以后,更新界面。
思路想好了,开始动手实现。 这里的 CanMove函数是判断可不可以移动。下一节说,先注释掉。 update() 函数会自动调用绘图事件。
// 鼠标点击释放事件
void Board::mouseReleaseEvent(QMouseEvent *e)
{
QPoint curPos = e->pos();
int col, row; // 当前点击位置
// 判断点是否落在棋盘
bool ret = getCurPos(curPos, col, row);
if(!ret) return; // 落在棋盘外,忽略
int clickId = -1; // 本次点击的棋子
// 获取本次点击的棋子
clickId = getStoneId(row, col);
// 如果是第一次点击到棋子
if(_selectId == -1)
{
// 设置 _selectId
_selectId = clickId;
}
else { // 如果之前点击了,实现移动和吃子逻辑
if(clickId == -1)
{
// 可不可以移动
// if(canMove(_selectId, row, col, clickId))
{
_stone[_selectId]._row = row;
_stone[_selectId]._col = col;
_redRound = !_redRound; // 切换回合
}
_selectId = -1;
}
else {
if(_stone[_selectId]._red == _stone[clickId]._red)
{
_selectId = clickId;
}else {
// 可不可以吃
// if(canMove(_selectId, row, col, clickId))
{
_stone[_selectId]._row = row;
_stone[_selectId]._col = col;
_stone[clickId]._dead = true;
_redRound = !_redRound;
}
_selectId = -1;
}
}
}
update();
}
判断是否落在棋盘上 getCurPos 代码
// 当前点击是否有效
bool Board::getCurPos(QPoint pos, int &col, int &row)
{
for(int i=0; i<10; i++)
{
for(int j=0; j<9; j++)
{
QPoint c = center(j, i);
if(pos.x() < c.x()+_d/2 && pos.x() > c.x()-_d/2)
{
if(pos.y() < c.y()+_d/2 && pos.y() > c.y() - _d/2){
col = j;
row = i;
return true;
}
}
}
}
return false;
}
获取本次点击的棋子Id getStoneId 代码
//判断该行列位置有没没棋子,有就返回ID,没有就返回-1
int Board::getStoneId(int row, int col)
{
for (int i = 0; i < 32; i++)
if (row == _stone[i]._row && col == _stone[i]._col && !_stone[i]._dead)
return i; //有棋子,返回棋子ID
return -1; //该行列位置没棋子
}
走棋
到目前为止,我们的象棋已经可以移动了,接下来就是象棋的规则,比如马走日,相飞田什么的。这部分代码贴在下面,不解释。有兴趣自己看
// 走棋逻辑
bool Board::canMove(int id, int row, int col, int clickId)
{
switch (_stone[id]._type) {
case Stone::JIANG:
if (col > 2 && col < 6 &&
((_stone[id]._red && row < 3 || !_stone[id]._red && row > 6) && (abs(row - _stone[id]._row) + abs(col - _stone[id]._col) == 1)) //0+1=1
|| countAtLine(_stone[4]._row, _stone[4]._col, _stone[20]._row, _stone[20]._col) == 0)
return true;
break;
case Stone::CHE:
if (countAtLine(row, col, _stone[id]._row, _stone[id]._col) == 0)
return true;
break;
case Stone::MA:
if ( (abs(row-_stone[id]._row)==1 && abs(col-_stone[id]._col)==2 //左右跳
&& getStoneId(_stone[id]._row,(col+_stone[id]._col)>>1) == -1 //没拐脚
) || (abs(row-_stone[id]._row)==2 && abs(col-_stone[id]._col)==1 //上下跳
&& getStoneId((row+_stone[id]._row)>>1,_stone[id]._col) == -1)) //没拐脚
return true;
break;
case Stone::PAO:
if (getStoneId(row, col) == -1 && countAtLine(row, col, _stone[id]._row, _stone[id]._col) == 0 //移动
|| getStoneId(row, col) != -1 && countAtLine(row, col, _stone[id]._row, _stone[id]._col) == 1) //吃子
return true;
break;
case Stone::XIANG:
if ((_stone[id]._red && row < 5 || !_stone[id]._red && row > 4) //没过河
&& abs(row - _stone[id]._row) == 2 && abs(col - _stone[id]._col) == 2 //象步
&& getStoneId((row+_stone[id]._row)>>1, (col+_stone[id]._col)>>1) == -1 ) //没象眼
return true;
break;
case Stone::SHI:
if (col > 2 && col < 6 && (_stone[id]._red && row < 3 || !_stone[id]._red && row > 6)
&& abs(row - _stone[id]._row) == 1 && abs(col - _stone[id]._col) == 1)
return true;
break;
case Stone::BING:
if (abs(row - _stone[id]._row) + abs(col - _stone[id]._col) != 1)
break;
if (_stone[id]._red) { //红棋在上
if (row < _stone[id]._row) break;
if (_stone[id]._row <= 4 && row == _stone[id]._row) break;
} else { //黑棋在下
if (row > _stone[id]._row) break;
if (_stone[id]._row >= 5 && row == _stone[id]._row) break;
}
return true;
}
return false;
}
//统计直线上棋子个数
int Board::countAtLine(int row1, int col1, int row2, int col2)
{
int min, max, cnt = 0;
if (row1 != row2 && col1 != col2)
return -1;
if (row1 == row2) {
if (col1 < col2) {
min = col1;
max = col2;
} else {
min = col2;
max = col1;
}
for (int col = min+1; col < max; col++)
if (getStoneId(row1, col) >= 0)
cnt++;
} else if (col1 == col2) {
if (row1 < row2) {
min = row1;
max = row2;
} else {
min = row2;
max = row1;
}
for (int row = min+1; row < max; row++)
if (getStoneId(row, col1) >= 0)
cnt++;
}
return cnt;
}
轮流下棋
轮流下棋其实很简单,就是轮到红(黑)方的时候,点击黑(红)棋没有效果。 我们在棋盘类中加一个属性 _redRound 判断当前是否是红方,初始化为红方。这个值的修改在 mouseReleaseEvent这个函数里面修改。 如果当前点击的棋的颜色和当前回合不一样,这次的 鼠标点击事件不处理,任何一方走棋完成后,将_redRound的值取反。 修改后的 mouseReleaseEvent 代码如下
checkWin函数就是判断是否胜利,这个函数还没有写,下一节说。
// 鼠标点击释放事件
void Board::mouseReleaseEvent(QMouseEvent *e)
{
QPoint curPos = e->pos();
int col, row; // 当前点击位置
// 判断点是否落在棋盘
bool ret = getCurPos(curPos, col, row);
if(!ret) return; // 落在棋盘外,忽略
int clickId = -1; // 本次点击的棋子
// 获取本次点击的棋子
clickId = getStoneId(row, col);
// 如果是第一次点击到棋子
if(_selectId == -1)
{
// 不是当前回合
if(clickId != -1 && _stone[clickId]._red != _redRound) return;
// 设置 _selectId
_selectId = clickId;
}
else { // 如果之前点击了,实现移动和吃子逻辑
if(clickId == -1)
{
// 可不可以移动
if(canMove(_selectId, row, col, clickId))
{
_stone[_selectId]._row = row;
_stone[_selectId]._col = col;
_redRound = !_redRound; // 切换回合
}
_selectId = -1;
}
else {
if(_stone[_selectId]._red == _stone[clickId]._red)
{
_selectId = clickId;
}else {
// 可不可以吃
if(canMove(_selectId, row, col, clickId))
{
_stone[_selectId]._row = row;
_stone[_selectId]._col = col;
_stone[clickId]._dead = true;
_redRound = !_redRound;
}
_selectId = -1;
}
}
}
update();
// checkWin();
}
判断胜利
判断胜利就是判断,红方老将和黑方老将有没有被吃,被吃则对方赢。代码如下
void Board::checkWin()
{
if (_stone[4]._dead) {
QMessageBox::information(this, "结束啦", "黑胜");
init();
} else if (_stone[20]._dead) {
QMessageBox::information(this, "结束啦", "红胜");
init();
}
}
这样一个双人对战的中国象棋就完成了。还比较简陋,没有悔棋和棋谱残局等功能。后面会添加人机对战和网络对战功能,至于悔棋和棋谱,残局这些功能也许会添加吧。