C语言实现中国象棋(Qt实现界面,源码下载,详细注释,易移植)

前言:中国象棋的规则很多人都懂,用C语言做一个中国象棋游戏,其要点是怎么把抽象的规则变成形象、具体的代码。本项目提供详细的实现思路,源码附带大量的注释说明,源码逐步地实现了每一种棋类的走棋规则、吃棋规则,将每一条细化的规则整合起来也就实现了一个完整的游戏规则。

本项目提供两种调试方式,一种是带界面操控(QT实现),一种是终端输入操控。本项目非常适合初学者用以参考学习。

下载源码:

       点击上方链接下载源码,源码下载解压后有两个文件夹,分别是QT工程和Dev-C++工程,前者带界面后者是终端输入方式调试。

代码设计:

     两个工程的源码都包含了chess.cppchess.h两个文件,这两个文件实现了象棋的游戏规则,将一盘象棋游戏的所有操作通过接口来实现,非常方便移植到其他的嵌入式平台。下面先对这两个源码文件的部分内容作说明。

typedef struct{
    eChessColour Colour:4;
    eChessClass Class:4;
}ChessPiecesStr;//装载棋子的容器,携带棋子颜色、棋子种类信息


/*由多个棋子容器组成的虚拟棋盘 10(row)*9(col)*/
static ChessPiecesStr CheckerBoard[10][9];

ChessPiecesStr结构体表示一个象棋包含的信息(颜色、棋类),那么可以定义这样一个数组ChessPiecesStr  CheckerBoard[10][9],它代表整个棋盘,10行,9列,每个数组元素代表棋盘的每一个位置上的棋子,如果某个元素的信息是NULL,则代表该位置上没有棋子。我们在游戏过程的走棋、吃棋、悔棋等操作,其实是对数组CheckerBoard里的数据进行操作,然后将操作完的棋盘更新显示出来。

typedef struct{
    ChessPiecesStr ChPs;
    uchar col,row;//位置:列、行
}PickUpChessStruct;//被选中的棋子,携带棋子所在的位置、棋子容器里的信息

PickUpChessStruct结构体表示游戏者当前要操作的棋子,它包含了该棋子的座标信息、棋色、棋类,这么封装是为了方便实现每一种棋类的走棋规则、吃起规则,下面以炮的走棋规则为例:

bool  MovemenRules_PAO(PickUpChessStruct* pActiveChess,uchar row,uchar col)//【炮】走棋规则
{
    int i,col_min,col_max,row_min,row_max;
    //【炮】走棋规则:走直线,不能跨越棋子
    //穷举法+假设法  要么横着走,要么竖着走,其他都是违规;假设成功了,那么起点到终点之间不能有其他棋子
    if(pActiveChess->row == row){//横着走
         if(pActiveChess->col > col){
            col_min = col;
            col_max = pActiveChess->col;
         }else{
            col_min = pActiveChess->col;
            col_max = col;
         }

        for(i = col_min+1;i < col_max; i++ ){
            if(CheckerBoard[row][i].Class != 0) return false;//假设不成立
        }
        return true;//假设成立
    }else if(pActiveChess->col == col){//竖着走
        if(pActiveChess->row > row){
            row_min = row;
            row_max = pActiveChess->row;
         }else{
            row_min = pActiveChess->row;
            row_max = row;
         }

        for(i = row_min+1;i < row_max; i++ ){
            if(CheckerBoard[i][col].Class != 0) return false;//假设不成立
        }
        return true;//假设成立
    }else{
        return false;
    }
}

函数传入的参数包括了一个类型PickUpChessStruct的结构体代表拿起来将要操作的棋子,row、col代表落棋的位置,将这些信息传入MovemenRules_PAO函数就能判断是否满足炮的走棋规则,注意这里仅仅是判断,CheckerBoard数组也就是棋盘,并没有发送发生任何变化。

bool MoveChess(char Operator,uchar start_row,uchar start_col,uchar end_row,uchar end_col)//将start_row,start_col处的棋子,移到end_row,end_col位置上
{
    bool ret = false;
    PickUpChessStruct ActiveChess;

    /*判断游戏是否结束*/
    if(Winner != Cc_NULL) return ret;

    /*判断选中的起始座标上是否有棋,若无棋返回*/
    if(CheckerBoard[start_row][start_col].Class == Cn_NULL){
        return ret;
    }

    /*判断是否允许该操作者走棋*/
    if(CurOperator != Operator){
        return ret;
    }

    /*将选中的棋子信息复制到ActiveChess结构体里*/
    ActiveChess.col = start_col;
    ActiveChess.row = start_row;
    ActiveChess.ChPs.Class = CheckerBoard[start_row][start_col].Class;
    ActiveChess.ChPs.Colour = CheckerBoard[start_row][start_col].Colour;

    //判断落在位置是否有棋子
    if(CheckerBoard[end_row][end_col].Class != Cn_NULL){
        //落的位置有棋
        //1.判断是否是己方棋子
        if(ActiveChess.ChPs.Colour == CheckerBoard[end_row][end_col].Colour){
            return false;
        }
        //2.判断是否能吃
        ret = (*ChessEatRulesArry[(unsigned char)ActiveChess.ChPs.Class-1])(&ActiveChess,end_row,end_col);//该棋类的吃棋规则,将选中的棋子和要落得位置传入
        if(ret == false){
            return ret;//不能吃,返回是失败
        }else{
            //能吃,判断被吃的是否是”将/帅“
            if(CheckerBoard[end_row][end_col].Class == Cn_JIANG){
                //游戏结束
                Winner = CheckerBoard[end_row][end_col].Colour==Cc_BLACK?Cc_RED:Cc_BLACK;
            }
        }
    }else{
        //落的位置没棋,判断是否能走
        ret = (*ChessMovemenRulesArry[(unsigned char)ActiveChess.ChPs.Class-1])(&ActiveChess,end_row,end_col);//该棋类的走棋规则,将选中的棋子,要落得位置传入
        if(ret == false){
            return false;
        }
    }
    //能到这里说明符合走棋、吃棋规则,接下来进行棋子移动

    //切换操作者
    CurOperator = (CurOperator==Cc_BLACK)?Cc_RED:Cc_BLACK;

    //备份,实现悔棋功能
    pushStack(&CheckerBoard[ActiveChess.row][ActiveChess.col],ActiveChess.row,ActiveChess.col);
    pushStack(&CheckerBoard[end_row][end_col],end_row,end_col);

    //1.先棋盘中移除选中的棋
    CheckerBoard[ActiveChess.row][ActiveChess.col].Class = Cn_NULL;
    CheckerBoard[ActiveChess.row][ActiveChess.col].Colour = Cc_NULL;
    //2.将选中的棋放到新的位置 row col
    CheckerBoard[end_row][end_col].Class= ActiveChess.ChPs.Class;
    CheckerBoard[end_row][end_col].Colour= ActiveChess.ChPs.Colour;
    return ret;
}

MoveChess函数是实现走棋的功能,参数Operator代表是操作者的身份(红棋、黑棋),start_row、start_col代表选中的棋子在棋盘的位置,end_row、end_col代表选中的棋子落棋的位置。通过起始座标可以知道选中的棋子在CheckerBoard数组里的位置,从而知道该棋子是什么颜色什么棋类,再调用与该棋类相应的走棋、吃棋规则函数来判断是否合法。处理的每一步流程都有代码注释,不用看代码框架图也很容易理解。

悔棋功能是通过栈原理实现的,栈的特点是先进后出,在棋盘上成功地走一步棋子,总共有两个位置发生变化,不管是走棋还是吃棋,那么只要把这两个位置的棋子信息保存到栈里面,并且如果栈满了之后直接覆盖较早的记录,悔棋时,从栈里取出上一步变化的两个位置的信息,将其还原就行了。

Dev-C++工程使用

‘      Dev-C++开发工具可以从网上免费下载,上手容易,对于初学者来说是一款不错的学习工具;打开源码工程后,可看到包括了两个源码文件是chess.cpp、chess.hDev-C++工程文件Chess-alone.dev,编译生成的可执行文件Chess-alone.exe

main函数:

int main(int argc, char *argv[]) {

    (void)argc;
    (void)argv;
    int start_row,start_col,end_row,end_col;

    InitCheckerBoard();
    CheckerBoardPrintf();

    PrintfTips();
    while(1){
        if(CurOperator == Cc_BLACK){
            printf("黑棋走棋:");
        }else{
            printf("红棋走棋:");
        }
        fflush(stdin);
        scanf("(%d,%d)>(%d,%d)",&start_row,&start_col,&end_row,&end_col);

        if(start_row < 0 || start_row > 10 || start_col < 0 || start_col > 9||\
        end_row < 0 || end_row > 10 || end_col < 0 || end_col > 9){\
            PrintfTips();
        }

        MoveChess(CurOperator,start_row,start_col,end_row,end_col);
        CheckerBoardPrintf();
        if(Winner != Cc_NULL){
            if(Winner == Cc_BLACK){
                printf("黑棋胜利,游戏重新开始!\n");
            }else{
                printf("红旗胜利,游戏重新开始!\n");
            }
            InitCheckerBoard();
            CheckerBoardPrintf();
        }
    }

    return 0;
}

Main函数主要是从终端输入走棋的起始座标和终点座标传入MoveChess完成走棋操作,然后再将棋盘的内容打印出来,实现了一个简单的接口使用演示。这里不用纠结无法区分颜色的问题。效果图如下:

Qt工程的使用

       Qt Creator是我经常使用的一款开发工具,它可以开发跨平台的应用,比如说同一套代码,可以编译成window的应用程序,还可以编译成安卓的应用程序,Linux等,而且它提供了丰富的开发接口和说明文档;从官网上就可以下载免费的个人开源版安装包。

       打开工程后,里面的文件说明如下:

 

Mainwindow.cpp文件里重写了窗口类里的paintEvent函数,每当界面重绘时都会调用它,那么就可以将象棋的背景画出来。

棋盘的背景通过QPainter画线实现,每一个棋子则是继承了QPushButton类,棋盘上有9*10个这样的按钮,分别对应CheckerBoard数组的9*10个元素,如果某个位置上没有棋子就把该按钮隐藏以来。

MyPiece继承于QPushButton后重写了鼠标点击、移动、释放事件,实现当鼠标点击棋子时,可以拖动棋子,然后落到其他位置。当鼠标释放时,就可以得到鼠标的起始座标和终点座标,然后调用MoveChess进行走棋。

效果图:

下面展示部分代码:

piece.c

#include "piece.h"

MyPiece::MyPiece(QWidget *parent) :
    QPushButton(parent)
{
    //none
}

MyPiece::~MyPiece()
{
    //none
}

void MyPiece::Display(void)//将棋子显示出来
{
    this->setText(QString(GetChessName(this->Colour,this->Class)));
    if(this->Colour == Cc_BLACK){
       this->setStyleSheet("QPushButton{background-color: rgb(255, 170, 127);"
                         "font-size:24px;color:black;border-style:solid;"
                         "border-width:1px;border-color:black;border-radius:20px;}");
   }else{
       this->setStyleSheet("QPushButton{background-color: rgb(255, 170, 127);"
                         "font-size:24px;color:red;border-style:solid;"
                         "border-width:1px;border-color:black;border-radius:20px;}");
   }
   this->setHidden(false);
}

void MyPiece::mousePressEvent(QMouseEvent *e)//棋子点击事件
{
    //qDebug() <<e;
    Point_Start = this->pos();
    Point_Click = e->globalPos()-this->pos();
    /*将此小部件提升到父小部件堆栈的顶部*/
    this->raise();
}

void MyPiece::mouseMoveEvent(QMouseEvent *e)//棋子移动事件
{
    //qDebug() <<e;
    tPoint = e->globalPos()-Point_Click;
    /*限制棋子不能被拖到棋盘外*/
    tPoint.rx() = tPoint.rx() < 40?40:tPoint.rx();
    tPoint.rx() = tPoint.rx() > 520?520:tPoint.rx();
    tPoint.ry() = tPoint.ry() < 40?40:tPoint.ry();
    tPoint.ry() = tPoint.ry() > 580?580:tPoint.ry();
    /*让棋子跟着鼠标走*/
    this->move(tPoint);
}

void MyPiece::mouseReleaseEvent(QMouseEvent *e)//棋子释放事件
{
    (void)e;
    unsigned char x_start,y_start,x_end,y_end;

   /*将棋子的起点和终点的像素位置转换成在棋盘里的座标*/
    x_start=Point_Start.rx()/60;
    y_start=Point_Start.ry()/60;
    x_end=tPoint.rx()/60;
    y_end=tPoint.ry()/60;
    qDebug("(%d,%d)->(%d,%d)",x_start,y_start,x_end,y_end);
    /*得到起点和终点座标后,还原鼠标选中的棋子,发出走棋信号就行,判断是否真的移动到终点交给外部处理*/
    this->move(Point_Start);
    /*若位置发生了改变,则发出走棋信号*/
    if(x_start != x_end || y_start != y_end){
        emit GoChess(y_start,x_start,y_end,x_end);
    }
}

可扩展的地方

  1. 目前只是实现了在一台电脑上对战的游戏模式,可以对模式进行扩展,利用tcp/udp通讯协议,实现局域网对战模式。
  2. 可以给棋盘提供丰富的背景,调整棋子上的字体,棋子的外形、颜色。
  3. 目前走棋只能通过鼠标拖动,可以增加一种点击的方式,鼠标左键点击一个棋子,再点击另一个位置来完成走棋操作。
  4. 可以将chess.c chess.h里的内容用面向对象的方法实现,因为这里考虑到方便移植到其他的嵌入式平台,所以用C语言编写。
  5. 可以优化以下走棋规则、吃棋规则的代码,这里用的C语言充分体现了其面向过程编程的特点,其中游戏处理流程可以简化。

 

如有错误之处,还望指出,谢谢阅读,求赞。

 

 

《兼听则明,偏信则暗。———资治通鉴》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章