项目集锦之一——GOMOKU五子棋项目

项目集锦系列博文开坑啦!本系列将分享Deadpool博主这些年来的所有课程项目,请持续关注哦😄!

题外话,今天发现有人看出我是大司饭友了,我只能说这波不亏

前言

本项目地址在这里,其为分布式系统大作业。由团队HOIT-23o2倾情制作呈现。
在这里插入图片描述

能够学到什么?

  1. React-JS基本使用方法
  2. WebSocket基本编程方法

React-JS项目配置

  1. 安装Node.js与Npm
    网上有很多教程了,再次就不细说了,附个链接
  2. 配置ReactJS环境
    打开cmd,输入如下命令
    # 安装create-react-app,帮助搭建项目结构
    npm install -g create-react-app
    
    # 利用create-react-app创建项目
    npm create-react-app gomokuClient
    
    接下来,你可以看到当前目录下出现了gomokuClient文件夹,里面的结构类似这个样子:
    在这里插入图片描述
    好了,在当前目录下再次打开cmd
    在这里插入图片描述在命令行中键入如下命令
    npm start
    
    就可在浏览器http://localhost:3000/端口访问了
    这一部分的配置在网上也有很多教程,戳这里即可查看更多

GOMOKU

OK,不多BB,从现在开始,我们将进入GOMOKU

项目结构


首先来看项目结构
在这里插入图片描述
由于是课程项目,因此文件组织较为简单,index是顶层,用于注册App组件:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
...
ReactDOM.render(<App />, document.getElementById("root"));

App组件用于定义页面路由、布局逻辑,例如以下代码就表明本App至少由HomeLoginRoomMe四个页面构成,且布局方式是经典上部NavigationBar和下部视图界面

class App extends React.Component{
    render(){
        return (
            <main className="main">
                <Router>
                    <Navbar />
                    <Switch>
                        <Route path='/' exact component={Home}/>
                        <Route path='/Login' component={Login}/>
                        <Route path='/Room' component={Room} />
                        <Route path='/Me' component={Me}/>
                        ...
                    </Switch>
                </Router>
            </main>
        );
    }
}

pages文件夹内定义本App所需要用到的所有页面components文件夹内定义本App用到的所有自定义组件

GOMOKU实现


1. GOMOKU核心逻辑实现

GOMOKU的核心流程如下:

  • 启动一局游戏;
  • 选择先手;
  • A下一子,游戏检测是否满足胜利条件,若满足,则宣布A获胜,重启游戏;
  • 接着B下一子,游戏检测是否满足胜利条件,若满足,则宣布B获胜,重启游戏;
  • 重复3、4步,直至某一方获胜或是棋盘占满

由此流程引出的限制如下:

  • A、B不能下子在同一个棋格
  • A、B必须轮流落子

是吧,核心逻辑十分简单,该如何实现呢?相应实现代码在Game.jsBoard.js中,下面,来稍微为大家捋一捋思路:

  • 首先,我们需要一个棋盘,该棋盘能够收集落子信息,并将其绘制在棋盘上
  • 其次,我们需要一个五子棋逻辑控制器,该控制器能够判断谁落子、谁获胜
1.1 棋盘实现

为了能够较为轻松地收集落子信息,我们不妨把每个棋格设置为Button控件,这样当用户点击棋格时就能够轻松收到该点击事件。

绘制棋盘思路在于Board中的双重For循环将棋格Square绘制为棋盘,每个Square收到后,就会根据传送的参数colorState来改变棋子Piece的颜色(通过修改css类实现)

/*棋子*/
function Piece(props) {
    const color = props.color;
    if(color === 'b'){
        return <div className='piece-black' />
    }
    else if(color === 'w'){
        return <div className='piece-white' />
    }
    else {
        return <></>;
    }
}
/*棋格*/
function Square (props) {
    return (
        <button className="square" onClick={props.clickFn}>
            <Piece color={props.color}/>
        </button>
    )
}
/*棋盘*/
class Board extends React.Component {
    ...

    renderSquare(i) {
        return (
          <Square
            color={this.props.colorState[i]}
            clickFn={() => this.clickFn(i, ME)}
            />
        );
    }
    
    render() {
        const boardRows = Array.from({length: this.rowCount}, (_, index) => index + 1);
        const boardCols = Array.from({length: this.colCount}, (_, index) => index + 1);
        
        return (
            <div className="board-container">
                {
                    boardRows.map((_, i) => (
                        <div key={i} className="board-row">
                            {
                                boardCols.map((_, j) => this.renderSquare(this.rowCount * i + j))
                            }
                        </div>
                    ))
                }
            </div>
        )
    }
    
}
1.2 控制实现

为了实现控制棋局,我们需要记录棋局状态,利用一个colorState数组即可。我们需要在游戏进行时不断更新它……

如何根据colorState判断胜利呢?这个比较简单,我们可以先做这个,如下

function JudgeIfWin(rowCount, colCount, colorState, index) {
    var lines = ['', '', '', ''];               
    ...
    var curColor = colorState[index];
    for (let i = 0; i < rowCount; i++) {
        for (let j = 0; j < colCount; j++) {
            var piece = colorState[i * rowCount + j];
            if(i === curi) {
                lines[0] += piece;
            }        
            if(j === curj){
                lines[1] += piece;
            }
            if((j === curj && i === curi) || (j - curj) / (i - curi) === 1){
                lines[2] += piece;
            }
            if((j === curj && i === curi) || (j - curj) / (i - curi) === -1){
                lines[3] += piece;
            }
            if(piece != null)
                remainCount--;
        }
    }
    if(remainCount === 0){
        return WIN_DRAW;
    }
    console.log(lines);
    var judge = curColor === 'w' ? 'wwwww' : 'bbbbb';
    for(var i = 0; i < lines.length; i++){
        if(lines[i].indexOf(judge) >=0){
            return curColor === 'w' ? WIN_WHITE : WIN_BLACK; 
        }
    }
    return WIN_NOBODY;
}

lines数组记录了落子点的垂直、水平、两条斜对角线的所有棋子的情况,最后用For循环检测有无重复5个相同色的落子即可

接下来完成下子逻辑

handleClick(i, clickFrom){
    const whosRound = this.state.whosRound;
    const colorState = this.state.colorState.slice();
    console.log(clickFrom)
    console.log(whosRound);

    /* 游戏未开始 或 玩家已离线 */
    if(!this.isGameStart){
        alert("暂无对手!");
        return;
    }
    /* 或赢、或输、或平局,都重启一局 */
    if(this.state.winState !== WIN_NOBODY){
        this.gameReset();
        return;
    }
    /* 判断当前回合是否是自己的回合,若不是,则此次点击无效,状态保持不变 */
    if(clickFrom !== whosRound){
        return;
    }

    /* 不得在同一个地方反复落子 */
    if(colorState[i] !== 'w' && colorState[i] !== 'b'){
        if(whosRound === ME){
            colorState[i] = this.myColor;
        }
        else {
            colorState[i] = this.opponentColor;
        }
        console.log(colorState);
        /* 判断是否胜利 */
        var winState = JudgeIfWin(this.rowCount, this.colCount, colorState, i);
        var whosNext = whosRound === ME ?  OPPONENT : ME;
        var tips = "";
        /* 胜利或平局 */
        if(winState !== WIN_NOBODY){
            var participants = WIN_BLACK;
            if(winState === WIN_BLACK){
                participants = this.myColor === 'b' ? ME : OPPONENT;
            }
            else if(winState === WIN_WHITE){
                participants = this.myColor === 'b' ? OPPONENT : ME;
            }
            else {
                participants = null;
            }
            tips = this.generateTips(TIP_MODE_WIN, participants);
        }
        /* 还未决出胜负 */
        else {
            tips = this.generateTips(TIP_MODE_DESC, whosNext);
            console.log(tips);
        }

        /* 更新棋局 */
        this.setState({
            colorState : colorState, 
            whosRound : whosNext,
            winState: winState,
            tips: tips,
            myName: this.myName,
            opponentName: this.opponentName
        }, () => {
        	/*电脑下子逻辑*/
           	...
            var nextStep = AIEngine(this.state.colorState);
            if(winState === WIN_NOBODY){
                this.handleClick(nextStep, OPPONENT);
            }
            ...
        });
    }   
}

ReactJS最好的一点在于当我们调用this.setState后,它会自动重绘整个布局,因此,棋盘界面能够借此机会刷新,因此,我们只需让我们的棋盘注册该函数即可:

render() {
    return (
        <BaseLayout span={6} offset={3}>
            <Card showBackArrow={true} backArrowClick={this.escapeGame} params={this.webSocket}>
                ...
                <Board 
                    rowCount={this.rowCount}
                    colCount={this.colCount}
                    colorState={this.state.colorState}
                    clickFn={(i, clickFrom) => this.handleClick(i, clickFrom)}
                />
                ...
            </Card>
        </BaseLayout>
    )
}

于是,每次点击棋格,都会调用handleClick函数对棋盘进行重绘,这样便实现了游戏控制器对棋盘的控制

2. GOMOKU 多人对战实现

事实上,多人对战的前端实现较前一节来说较为容易,我们只需定义包消息即可,详细消息细节可以在这里找到,下面是export的部分

/* 客户端发送给服务器的信息类型 */
TYPE_CREATE_ROOM,		//创建房间
TYPE_JOIN_ROOM,			//加入房间
TYPE_MOVE_INFO,			//移动信息
TYPE_ESCAPE,			//逃跑信息
TYPE_RESET_GAME,		//重启游戏
APICreate,
/* 服务器发送给客户端的信息类型 */
MSG_GAME_START,			//游戏开始
MSG_TYPE_MOVE_INFO,		//对方的移动
MSG_TYPE_ROOM,			//已加入房间
MSG_TYPE_ESCAPE,		//对手已逃跑
MSG_TYPE_ROOM_CREATED,	//房间已创建
ParseMsg,

在前端实现中,我们主要依靠webSocekt与服务器进行通信,核心代码如下:

constructor(props) {
	...
	this.webSocket = new WebSocket("ws://" + targetDomain + ':' + targetPort, "ws-protocol-example");
    this.webSocket.onopen = this.onWebSocketOpen;
    this.webSocket.onclose = this.onWebSocketClose;
    this.webSocket.onmessage = (msg) => this.onWebSocketMessage(msg);
	...
}
...
onWebSocketMessage(msg){
    msg = msg.data;  
    var [msgType, msgInfo] = API.ParseMsg(msg);
    switch (msgType) {
        case API.MSG_TYPE_MOVE_INFO:{		//对手移动
            var x = msgInfo.x;
            var y = msgInfo.y;
            console.log(x);
            console.log(y);
            var index = ComposeIndex(x, y, this.rowCount);
            this.handleClick(index, OPPONENT);
            break;
        }
        case API.MSG_TYPE_ESCAPE:{			//对手逃跑
            alert("对手逃跑了");
            console.log(msg);
            this.isGameStart = false;
            this.opponentName = "未连接";
            this.whosFirst = "未开始";
            this.gameReset();
            break;
        }
        case API.MSG_GAME_START: {			//游戏开始咯
            this.opponentName = msgInfo.opponentname;
            this.whosFirst = msgInfo.youfirst === true ? ME : OPPONENT;
            this.isGameStart = true;
            console.log(this.opponentName);
            this.gameReset();
        }
        default:
            break;
    }
}

服务器端采用C++实现,哈哈,本人不负责这一块,核心代码应该在这里,反正它能够为我们发送一堆信息就对了。

3. GOMOKU UI设计实现

如下是我们参考的设计原型,出处在Dribbble科学上网哈),Dribbble真的是很不错的网站!!!
在这里插入图片描述
下面是我们自己的UI设计,还是可以的对不对😄
在这里插入图片描述
具体CSS与布局代码在此便不多提了,大家可以看代码实现,图中黑框是自己实现的Card,在这里,中间是棋盘,直接拿前面我们实现的Board来用即可,这就是ReactJS组件复用的优势之一了

边上的Navbar左上角的三条杠)实现在这里,其核心在于点击后将位于-1000px的FlyoutMenu布局移入视野中,就实现了NavBar。

结语

拖了将近2个月,把这个项目简单描述了一下,博主当时和队友做了4天,效果也算是可以😄。

其实,这个项目是初次接触ReactJS的实践,在ReactJS官网有三子棋实现的例子,本项目的核心逻辑也基本仿照三子棋来完成(毕竟小白嘛)。很多地方没讲清楚也可以先去看官网三子棋教程然后修修改改来看每一个代码起什么作用,这样学习的效率无疑是最高的

这是博主写的第一个项目集锦,后续会展开介绍更多,希望能够给初学者或是在读学生带来一些帮助。

好了,今天就到这里,起飞🛫

Bonus

下一期预告:

项目集锦之二——输出全靠吼OITC(Output Is To Cry)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值