• 基于C++的简易的国际象棋双人对战程序设计


    一、程序介绍

    1.1 现实背景

    国际象棋是世界上一个古老的棋种。据现有史料记载,国际象棋的发展历史已将近2000年。关于它的起源,有多种不同的说法,诸如起源于古印度、中国、阿拉伯国家等。

    国际象棋分为黑白两方共32枚,每方各16枚;棋盘为正方形,由64个黑白(深色与浅色)相间的格子组成。每方有王、后、象、车、马、兵六种棋子,不同棋子走子和吃子的方法不同,不再赘述。

    1.2 设计目的

    使用Qt自带的Socket编程,实现客户端和服务端一对一的网络对战,正确复现国际象棋的规则。在基本的走子和吃子回合制下,要求有:

    对局中,出现下列情况之一,本方算输,对方赢:

    己方王被将死(不允许送吃); 2)己方发出认输请求; 3)己方走棋超出步时限制;实现兵生变,用户可选择生变为的棋子。

    加入逼和和车马易位功能。车马易位不考虑车和马是否移动过,只要在固定位置,满足车马易位的其他条件即可进行易位。

    一局结束时(胜负,逼和)在两端弹出对话框告知结果。

    加入残局载入和保存功能。

    1.3 程序使用方法

    程序初始界面如下

    在这里插入图片描述

    本程序采用客户端和服务端集成的设计,点击初始化—连接,用户自主选择创建客户端抑或服务端,以及ip地址和端口号。

    在这里插入图片描述

    选择创建服务端,点击OK,处于等待连接状态;可随时取消等待。

    在这里插入图片描述

    再开一个程序,选择创建客户端,设置对应端口号,点击OK连接服务端。这时客户端和服务端的创建连接窗口销毁,代表二者成功建立连接。这时两端的界面分别变为
    在这里插入图片描述

    在这里插入图片描述

    服务端

    客户端

    本程序的特色之一是客户端和服务端的不对称权限。一个不成文的规定是,数据的运算和主动权握在服务端手里,于是只有服务端能点击开始游戏,能够载入残局等。在服务端点击开始游戏,双方同时开始

    在这里插入图片描述
    在这里插入图片描述

    双方均可正常走子。此时黑方不走,倒计时结束后,双方均弹出消息框

    在这里插入图片描述

    认输和将死效果相同,不再演示。载入兵升变残局,将兵走到底线,出现如下界面

    在这里插入图片描述

    若选择后,则将死对方

    在这里插入图片描述

    若选择象,则逼和
    在这里插入图片描述

    最后演示车马易位。载入残局

    在这里插入图片描述

    此时由于王受黑车威胁,无法进行车马易位

    在这里插入图片描述

    将局势走成上图所示,此时王以及王经过的路径均不受威胁,可进行长短易位。长易位后效果如下

    在这里插入图片描述

    二、程序设计

    技术基础本程序在Qt Creator中编写测试,共约1400行代码,使用自带编译器(mingW)编译。

    使用了Qt自带的QString文本库,QFile文件库,QMap、QSet、QVector、QList等容器库存储数据,QTimer进行计时,QPaintEvent、QMouseEvent处理鼠标和绘图事件,QMessageBox、

    QFileDialog标准消息框和文件对话框,QPainter绘图方法;没有使用QThread进行多线程并发操作;网络通信部分使用的是Qt Socket部分的QTcpServer和QTcpSocket类,以及数据打包和处理时的QDataStream,QByteArray等。

    类的职责和关系

    utils.h中首先定义了方便数据传输和运算的结构体包装,并重载了 QDataStream 的流运算符用于结构 体的序列化、二进制化,编码和解码的过程

    struct Pos{//坐标二元组
        int x,y;
        bool operator == (const Pos & value) const{//用于比较以及取得作QSet键值的资格
            return (value.x==x&&value.y==y);
        }
        bool operator < (const Pos & value) const{//用于比较以及取得作QMap键的资格
            if(xvalue.x)return false;
            if(yvalue.y)return false;
            return false;
        }
        friend QDataStream& operator>>(QDataStream& in, Pos& s);
        friend QDataStream& operator<<(QDataStream& out,const Pos& s);
    };
    struct Piece{//棋子
        bool white;//棋子属于白方与否
        int type;//棋子类型,0~5 queen bishop rook knight pawn king
        friend QDataStream& operator>>(QDataStream& in, Piece& s);
        friend QDataStream& operator<<(QDataStream& out,const Piece& s);
    };
    struct Walk{//棋子的移动,将王车易位也看做一种移动,在下面的move()函数中一视同仁进行充分处理
        Pos pos;//目标位置
        int attack;//移动的附加属性 0:走 1:攻击 2:王车易位
    };
    struct Situation{//对局面的快照
        //单独标记王的位置
        Pos black_king;
        Pos white_king;
        QMap pieces;//记录全局所有位置-棋子映射
        friend QDataStream& operator>>(QDataStream& in, Situation& s);
        friend QDataStream& operator<<(QDataStream& out,const Situation& s);
    };
    inline uint qHash(const Pos key){//用于结构体契合QSet的key,与大作业一的功能相同,不再赘述
        return key.x + key.y;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    需要将死、逼和、王车易位功能,在得到棋子可走范围时需要进行虚拟的模拟,于是有了两个步骤的处理。这样的处理也使得将死和逼和的判断轻而易举:当某方所有子的FinalRange的并集为空集时,若此时此方被将军,则是将死;否则为逼和。

    updialog.h中的UpDialog继承自QDialog,是进行兵生变选择的对话框,调用其int getSetting()函数将执行exec()并阻塞,窗口销毁后返回选择的生变类型。 connectiondialog.h提供了一开始选择服务端/客户端并创建连接的窗口。

    public:
    explicit ConnectionDialog(QWidget *parent); signals:
    void serverdone(QTcpSocket*);
    void clientdone(QTcpSocket*);
    
    • 1
    • 2
    • 3
    • 4

    父窗口只需新建ConnectionDialog对象进行初始化并进行信号的连接即可。当用户选择建立服务器并成功创建连接时触发携带socket的void serverdone(QTcpSocket*)信号,当用户选择建立客户端并成功创建连接时触发携带socket的void clientdone(QTcpSocket*)信号。

    chessboard.h是国际象棋棋盘的显示控件,内部将处理鼠标的点击事件、可移动区域的计算和显示等。对外层来说重要的只有

    public:
    explicit ChessBoard(QWidget *parent);
    void setSituation(Situation s);//设置局面,立刻repaint()生效
    void setStatus(int id);//设置棋盘显示状态 //0:empty 1:loaded 2:selecting
    //下面两个函数控制着某棋子能否被点击等
    void setSide(bool iw);//设置本端是白方还是黑方     bool isnowwhite;//设置当前是哪一方走棋 signals:
    void move(Pos pos,Pos pos2);//当用户点击鼠标进行棋子的移动时触发信号,携带棋子位置和目标位置
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    mainwindow.h包含了主窗口MainWindow。在本次设计中主窗口负责的功能较少,它主要是起到调整界面的作用。它在一开始创建了ChessBoard*,但当客户端或服务端创建完成时,便对应创建了 chessclient.h中的ChessClient或chessserver.h中的ChessServer,并将chessboard*交予管理。可以认为ChessClient和ChessServer起到的是proxy的作用,它们负责客户端和服务端间的网络通信,在一端构成了ChessBoardProxyMainWindow的沟通模式。客户端、服务端工作流程下面列出了客户端/服务端单方面ChessBoardProxyMainWindow可能出现的沟通类型。

    在程序中,这些可能的情况均使用信号槽或者公共方法来进行,不再详细叙述。mainwindow主要负责

    主界面的调整(按钮的disable等),proxy负责网络通信和模块间通信,board则进行棋盘的更新显示。

    connect(socket,SIGNAL(readyRead()),this,SLOT(received()));
    
    • 1

    即托管了socket*的数据接收,处理后再以void newPacket(QByteArray data)的signal发送出去。

    Packet的出现是为了规避TCP/IP协议可能出现的粘包/拆包问题(这在此程序中是必要的,由于此程序设计的思想所致,客户端和服务端的通信来回次数较多,程序中具有连续发送两个包的情况,因此必须

    可以看到处理方式是在数据的头部加入数据的长度信息,再发送出去。而对接收到信息的处理为

    当接收数据的长度不足以读取头部的长度信息,或接收的数据量小于应接收的数据长度时,程序将等待下一次接收,这样就解决了拆包。而数据量足够时,程序读取头部的数据长度信息并读取对应长度的数据,这样便完成了一个独立的数据包的提取;而每成功提取一个包,就发送携带它的信号,这样就解决了粘包。经过这一个中间层的包装,数据的收发轻易很多,如在chessclient中,开始时进行信号连接

    2.1 通信协议

    之前提到过,本程序采用不对称通信设计。这个构想的初衷是实现一定程度的反作弊功能,这个框架下若期望拓展到游戏平台的构建(而非双人对战),修改会减少很多。对于游戏平台来说,要不惮以最坏的恶意来揣测玩家。因此应该将一切的计算、控制权限交予服务端,否则,举例说客户端采用强行修改内存等方法像服务端发出了非法、不合理的动作(如开局直接用兵将对方王吃掉),服务端若想避免惨剧只能加入繁琐的检测机制(实际上很多即时性的网游就是这样干的,不过这只是因为射击游戏等对实时性的刚需,因为网络延迟的原因全部交予服务器处理数据不成立)。而国际象棋这样的对战不需要很高的即时性,所以完全可以采取客户端只向服务端发送可行的动作,服务端处理局面后将结果返回客户端(同时也更新服务端的界面和变量等),客户端进行显示的方法。

    下面是客户端和服务端的通信协议。

    //server to client //0:发送局面 //1:发送双方计时 //2:发送胜/负/和 0胜1负2和 //3:换为X边 0白1黑 //4:
    
    • 1

    请求兵升变 0~3 queen bishop rook knight //5.开始 白方先走 只需调整变量 紧接着还要发换边信息

    //client to server //0:走棋 //1:升变 //3:认输
    
    • 1

    从上一个部分的代码中可以看到,每个数据包的头部都是一个整数,代表的就是信息类型;之后是具体数据。对于客户端的动作,客户端向服务端发送请求,服务端处理后更新自身界面、变量并发送使客户端更新界面、变量的数据包,客户端接收后进行更新。计时的变动、胜负和的消息、换边的消息、改变局面棋子排布、弹出生变对话框、开始一局的动作都是服务端发起,客户端只能接收并显示。

    :升变 //3:认输

    
    从上一个部分的代码中可以看到,每个数据包的头部都是一个整数,代表的就是信息类型;之后是具体数据。对于客户端的动作,客户端向服务端发送请求,服务端处理后更新自身界面、变量并发送使客户端更新界面、变量的数据包,客户端接收后进行更新。计时的变动、胜负和的消息、换边的消息、改变局面棋子排布、弹出生变对话框、开始一局的动作都是服务端发起,客户端只能接收并显示。
    
    
    • 1
    • 2
    • 3
  • 相关阅读:
    关于Python的局部变量和全局变量使用介绍
    Hive 分桶表
    埋点上报系统
    2310d编译不过
    红黑树的插入(C++实现)
    k8s配置ingress访问集群外部资源
    Java项目论文+PPT+源码等]基于+PPT+源码等]基于javaweb的问卷调查系统|投票
    技能大赛试题剖析:文件上传渗透测试
    网络通信协议-HTTP、WebSocket、MQTT的比较与应用
    浅析 ArrayList
  • 原文地址:https://blog.csdn.net/newlw/article/details/126814900