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(10*(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 + 1; row < max; ++row)
             {
                   if(getStoneId(row, col1) != -1)  ++count;  //遍历非死棋,返回 行列 满足所求的id
             }        
  }
      else if(row1 == row2 && col1 != col2)
      {
             int min = col1 < col2 ? col1 : col2;
             int max = col1 > col2 ? col1 : col2;
             for(int col = min + 1; col < max; ++col)
             {
                   if(getStoneId(row1, col) != -1)  ++count;
             }
      }
      else
            return -1;

  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 != -1) //轮到执棋方走时,选择空白地或是对方棋子无效
     {
             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 = -1;     //_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==-1) 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 = -300000;

    while(steps.count())
    {
        Step* step = steps.last();
        steps.removeLast();

        fakeMove(step);
        int minScore = getMinScore(this->_level-1, 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 == 0)
        return score();

    QVector<Step*> steps;
    getAllPossibleMove(steps);
    int minInAllMaxScore = 300000;

    while(steps.count())
    {
        Step* step = steps.last();
        steps.removeLast();

        fakeMove(step);
        int maxScore = getMaxScore(level-1, 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 == 0)
        return score();

    QVector<Step*> steps;
    getAllPossibleMove(steps);
    int minInAllMaxScore = 300000;

    while(steps.count())
    {
        Step* step = steps.last();
        steps.removeLast();

        fakeMove(step);
        int maxScore = getMaxScore(level-1, 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 = 0, max = 16;
    }
    else
    {
        min = 16, max = 32;
    }

    for(int i=min;i<max; i++)
    {
        if(this->_s[i]._dead) continue;    //死棋不计
        for(int row = 0; row<=9; ++row)
        {
            for(int col=0; col<=8; ++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, 9899);
        connect(_server, SIGNAL(newConnection()), this, SLOT(slotNewConnection()));
    }
    else
    {
        _socket = new QTcpSocket(this);
        _socket->connectToHost(QHostAddress("127.0.0.1"), 9899);
        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()%2>0;    //主程序使用qsrand()对种子进行了初始化
    init(bRedSide);

    /* 发送给对方 */
    QByteArray buf;
    buf.append(1);
    buf.append(bRedSide>0?0:1);      //若bRedSide为1,就将0存在buf数组的第二个位置,发送给客户端
    _socket->write(buf);
}

客户端的槽函数:

void NetGame::slotDataArrive()
{
    QByteArray buf = _socket->readAll();
    switch (buf.at(0)) {
    case 1:
        initFromNetwork(buf);       //flag为1,初始化棋盘
        break;
    case 2:
        clickFromNetwork(buf);      //flag为2,给对方发送走棋数据,对方需要根据棋盘的中心对称性质进行转换
        break;
    case 3:
        backFromNetwork(buf);       //flag为3,让对方棋盘红黑各悔一步,与己方保持一致
        break;
    default:
        break;
    }
}

flag对应的函数:

void NetGame::backFromNetwork(QByteArray)
{
    backOne();
    backOne();
}
void NetGame::clickFromNetwork(QByteArray buf)
{
    Board::click(buf[1], 9-buf[2], 8-buf[3]);
}
void NetGame::initFromNetwork(QByteArray buf)
{
    bool bRedSide = buf.at(1)>0?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(2);     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(3);
    _socket->write(buf);
}
时间: 2024-10-10 06:25:32

chineseChess的相关文章

网游练习总结(1)

最近一段时间在校也闲得没事干,反正是好长一段时间,干脆就做一个<中国象棋>网游耍耍打发时间.弄了好久没有写总结,以及整个过程中遇到的问题,今天就赶紧写一哈,难免后面就会忘了. 一.注册登录界面: 可能会说这么简单的游戏,网上可能例子很多,也没有必要弄注册这样的功能,其实我只是学着玩玩哈. 关于注册我使用的是php与as3.0交互做的,有与php学的非常浅,也遇到了不少问题: 1.检测是否注册成功:这个也困了时间不是很长,但是觉得比较重要,我搜了一些资料找到的: mysql_affected_r

面向对象设计程序设计

面向对象设计方法:Java面向对象程序设计的基本思想是通过构造和组织对象来求解问题的.对象是很好的,任何一种物体都可以被认为是对象,例如,汽车就是一种对象.对象具有两个最基本的属性:具有一定的物质基础和一定的功能,这两种基本属性在Java语言中表现为拥有一定的存储单元并具备一定的功能.理解了Java的这种面向对象程序设计的基本思想之后,在以后的编程过程中,就应当个构造人们容易理解的更接近于真实物理世界物体的对象.Java面向对象程序设计的过程非常简单.这里介绍一种自顶向下的程序设计方法,首先构造

网游练习总结(2)

php获取js变量:document.write(string); $name=" <script type='text/javascript'> document.write(getName()); function getName(){ var str=window.location.search; var args=str.split('?'); var retval=''; if(args[0]==str){return '';}//参数为空: return args[1].

中国象棋

题目简介 此次对弈系统的开发研究是大学专业知识的一次综合应用于提高,计算机主要安装 jdk 运行环境和相关应用程序开发工具.运用 java , gui 监听器和标准类库等知识,首先 编写一个主类 ChineseChess ,构造一个主窗口,在主窗口上添加一工具栏,有相应的按 钮,用此来用来对“开始,悔棋 ,信息提示,退出,重新开始”进行操作. 1. 中国象棋对弈系统是:图形界面,监听器等技术的综合运用. 2. 对弈的实现:利用鼠标的监听来实现对象棋走法的操作. 中国象棋对弈系统要求实现下棋界面,

中国象棋(IOS)

////  ViewController.m//  ChineseChess////  Created by 晚起的蚂蚁 on 2016/11/20.//  Copyright ? 2016年 晚起的蚂蚁. All rights reserved.// #import "ViewController.h" @interface ViewController ()@property(assign)CGFloat wigth;@property(assign)CGFloat height;

1.2 中国象棋将帅问题进一步讨论与扩展:如何用1个变量实现N重循环?[chinese chess]

[题目] 假设在中国象棋中只剩下将帅两个棋子,国人都知道基本规则:将帅不能出九宫格,只能上下左右移动,不能斜向移动,同时将帅不能照面.问在这样条件下,所有可能将帅位置.要求在代码中只能使用一个字节存储变量. [分析] 3种方案: 1)位运算实现1个byte存入和读取2个变量. 2)使用位域把几个不同的对象用一个字节的二进制位域来表示.比如 C++ Code 12345   struct {     unsigned char a: 4;     unsigned char b: 4; } i;

Linux Sed命令学习笔记

1 功能说明 sed是一种在线编辑器,它一次处理一行内容.处理时,把当前处理的行存储在临时缓冲区中,称为"模式空间"(pattern space),接着用sed命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕.接着处理下一行,这样不断重复,直到文件末尾.文件内容并没有改变,除非你使用重定向存储输出或者加入i参数.Sed主要用来自动编辑一个或多个文件:简化对文件的反复操作:编写转换程序等. 小结:sed的功能是,对字符串进行增加.删除.改变.查找,即增删改查! 2 语法格式 s

团队-象棋游戏-最终程序

托管平台地址:https://gitee.com/zixiao520/Chinesechess/blob/master/ChineseChess.zip 小组名称:Narcissu 小组成员合照: 程序运行方法:右键点击使用浏览器运行 程序运行示例及运行结果: 其他附加内容:无

【原创】使用JS封装的一个小型游戏引擎及源码分享

1 /** 2 * @description: 引擎的设计与实现 3 * @user: xiugang 4 * @time: 2018/10/01 5 */ 6 7 /* 8 * V1.0: 引擎实现的基本模块思路 9 * 1.创建一个游戏引擎对象及精灵对象 10 * 2.将精灵对象添加到引擎中去,并实现播放动画效果以及需要用到的回调方法 11 * 3.启动引擎 12 * */ 13 14 /* 15 * V2.0: 实现游戏循环模块 16 * 1.如果游戏暂停了,就跳过以下各步骤,并在100毫