项目集锦之一——GOMOKU五子棋项目
项目集锦系列博文开坑啦!本系列将分享Deadpool博主这些年来的所有课程项目,请持续关注哦😄!
题外话,今天发现有人看出我是大司饭友了,我只能说这波不亏
前言
本项目地址在这里,其为分布式系统大作业。由团队HOIT-23o2倾情制作呈现。
能够学到什么?
- React-JS基本使用方法;
- WebSocket基本编程方法;
React-JS项目配置
- 安装Node.js与Npm
网上有很多教程了,再次就不细说了,附个链接 - 配置ReactJS环境
打开cmd,输入如下命令
接下来,你可以看到当前目录下出现了# 安装create-react-app,帮助搭建项目结构 npm install -g create-react-app # 利用create-react-app创建项目 npm create-react-app gomokuClient
gomokuClient
文件夹,里面的结构类似这个样子:
好了,在当前目录下再次打开cmd
在命令行中键入如下命令
就可在浏览器http://localhost:3000/端口访问了npm start
这一部分的配置在网上也有很多教程,戳这里即可查看更多
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至少由Home
、Login
、Room
、Me
四个页面构成,且布局方式是经典上部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.js与Board.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)