太长不看:直接点击这里查看代码

BoardGame.io 是一个专门为回合制游戏打造的游戏引擎。无需一行网络或存储相关的代码,只需要编写简单函数描述游戏动作如何影响游戏状态,即可自动帮你生成一个支持多人在线的完整游戏。它支持回合制游戏的方方面面,比如状态管理、多人在线、AI、游戏进程或回合管理、游戏大厅等诸多功能。今天我们就用它结合 React 制作一个五子棋游戏。

准备

首先,我们准备一个 React 项目,并添加 boadgame.io 依赖。

npx create-react-app gomoku
cd gomoku
npm install boardgame.io

定义游戏

接下来,我们要定义游戏。定义游戏相当于告诉 boardgame.io 游戏是怎么玩的。由于引擎会管理好当前玩家、游戏是否结束等这些状态,对于五子棋游戏来说,只剩下局面信息需要定义了。通过创建一个满足特定接口的对象,即可定义一个游戏。这些接口当中,setup 函数是起点,它负责创建游戏状态 G。而 moves 则定义了此游戏中有多少种可以执行的动作。

动作实际是一个函数,它接收当前状态 G 作为参数,并对它进行修改,使它成为新的状态。动作函数还接收另外一个参数 ctx, 它包含当前玩家、当前回合等这些信息,由 boardgame.io 管理,无法修改。除 Gctx 外,其它参数你可自由定义,它们将被视为执行该动作的额外参数。

五子棋游戏中,我们只有一个动作,就是落子。且把它命名为 putStone,它需要一个 ID 参数告诉它该在哪里下子。

创建 src/Game.js 文件,并添加以下内容:

export const Gomoku = {setup: () => ({ stones: Array(15*15).fill(0) }),moves: {putStone: (G, ctx, id) => {G.stones[id] = [1,-1][ctx.currentPlayer];},},
};

定义客户端

客户端相当于一个可以玩游戏,只不过只能通过 API 玩。将 src/App.js 内容替换为以下内容:

import { Client } from 'boardgame.io/react';
import { Gomoku } from './Game';const App = Client({ game: Gomoku});export default App;

若此时运行 npm start ,会看到一个 UI,这是 boardgame.io 渲染的 Debug 面板。当前状态下,使用这个 UI 下虽然也可以进行游戏,但玩起来相当费劲,就不介绍了。

Debug 面板在生产环境构建时 (NODE_ENV='production')会被自动去除,也可以在 Client 的配置中添加 debug: false 关闭。

改善游戏逻辑

着法验证

目前为止,若玩家对已经落子的位置调用 putStone , 则那个位置的棋子会被覆盖。这挺扯的,需要防止。

要让 boardgame.io 就能知道此着法是无效的,需返回一个从 boardgame.io 中引入的特别常量:

import { INVALID_MOVE } from 'boardgame.io/core';

现在,我们在 putStone 中验证着法并返回 INVALID_MOVE:

putStone: (G, ctx, id) => {if (G.stones[id] !== 0) {return INVALID_MOVE;}G.stones[id] = [1,-1][ctx.currentPlayer];
}

回合管理

不同的游戏,玩家在一回合可以进行的动作数量可能不同,结束回合的条件也不同。有的游戏一回合可以进行多个动作,有的一回合一个动作。五子棋就是一回合一个动作的游戏。在 Debug 面板中,我们可以点击 endTurn 手动结束回合。其实,客户端代码也可以执行完动作后自动结束回合。

在 boardgame.io 中有多种方法管理回合,使用 moveLimit 就是其中之一。下面我们就使用这种方法让引擎自动帮我们结束当前回合:

export const Gomoku = {setup: () => { /* ... */ },turn: {moveLimit: 1,},moves: { /* ... */ },
};

胜利条件

我们的五子棋游戏定义得差不多了,只差最后一环:胜利局面判断。

首先,我们先加几个函数:

/*** 检查一条线上某一方是否成 5* @param {number[]} stones* @param {number} clr* @param {number} start* @param {number} end* @param {number} stride*/
function checkWinnerByLine(stones, clr, start, end, stride) {let cnt = 0;for (; cnt < 5 && start !== end; start += stride) {if (stones[start] === clr) cnt++;else cnt = 0;}return cnt >= 5;
}/*** 检查某位玩家是否获胜* @param {number[]} stones* @param {number} clr*/
function isVictory(stones, clr) {const boardSize = 15;let x = 0,y = 0;let start = 0,end = 0,stride = 1;// horizontalstride = 1;start = 0;end = start + boardSize;for (y = 0; y < boardSize; y++) {let win= checkWinnerByLine(stones, clr, start, end, stride);if (win) return true;start += boardSize;end += boardSize;}// verticalstride = boardSize;start = 0;end = start + stride * boardSize;for (x = 0; x < boardSize; x++) {let win= checkWinnerByLine(stones, clr, start, end, stride);if (win) return true;start += 1;end += 1;}// major diagstride = boardSize + 1;start = 0;end = start + stride * boardSize;for (x = 0; x < boardSize - 4; x++) {let win= checkWinnerByLine(stones, clr, start, end, stride);if (win) return true;start += 1;end -= boardSize;}start = boardSize;end = start + stride * (boardSize - 1);for (y = 1; y < boardSize - 4; y++) {let win= checkWinnerByLine(stones, clr, start, end, stride);if (win) return true;start += boardSize;end -= 1;}// secondary diagstride = boardSize - 1;start = 4;end = start + stride * 5;for (x = 4; x < boardSize; x++) {let win= checkWinnerByLine(stones, clr, start, end, stride);if (win) return true;start += 1;end += boardSize;}start = 2 * boardSize - 1;end = start + stride * (boardSize - 1);for (y = 1; y < boardSize - 4; y++) {let win= checkWinnerByLine(stones, clr, start, end, stride);if (win) return true;start += boardSize;end += 1;}return false;
}/*** @param {number[]} stones*/
function isDraw(stones) {return stones.every(s => s !== 0);
}

这几个函数用来判断胜利局面和平局。其中,胜利局面判断方式是四个方向逐行扫描 “成 5 ” 棋型。由于现在我们不知道上次落子点,所以无法做局部扫描。

接下来,我们添加 endIf 函数到我们的游戏定义中。此函数会在每次游戏状态更新后被调用,引擎借此知道游戏是否结束。

export const Gomoku = {// setup, moves, etc.endIf: (G, ctx) => {if (isVictory(G.stones, [1, -1][ctx.currentPlayer])) {return { winner: ctx.currentPlayer };}if (isDraw(G.stones)) {return { draw: true };}},
};

若游戏未结束,endIf 必须返回 undefined 或者 null,其它值都会视为游戏结束。若游戏结束,函数返回值将赋值给 ctx.gameOver

制作棋盘

到这里,整个游戏只差 UI 了,有了 UI 我们就可以使用鼠标点击棋盘进行游戏了。UI 可以先简单做,直接把游戏状态 G 转换成可以点击的格子即可。

创建 src/Board.js 文件,并添加以下内容:

export function GomokuBoard({ G, ctx, moves }) {function handleClick(id) {moves.putStone(id);}let winner = "";if (ctx.gameover) {winner =ctx.gameover.winner !== undefined ? (<div id="winner">Winner: {ctx.gameover.winner}</div>) : (<div id="winner">Draw!</div>);}const cellStyle = {border: "1px solid #555",width: "50px",height: "50px",lineHeight: "50px",textAlign: "center"};let tbody = [];for (let i = 0; i < BOARD_SIZE; i++) {let cells = [];for (let j = 0; j < BOARD_SIZE; j++) {const id = BOARD_SIZE * i + j;cells.push(<td style={cellStyle} key={id} onClick={() => handleClick(id)}>{G.stones[id]}</td>);}tbody.push(<tr key={i}>{cells}</tr>);}return (<div><table id="board"><tbody>{tbody}</tbody></table>{winner}</div>);
}

渲染效果如下

查看效果

在使用 CodeSandbox.io 过程中我发现,使用 BoardGame.io 的 react 组件会出现某个模块找不到的情况,而使用非 react 的模块时不会出现。可以把 src/App.js 替换为如下内容解决:

//import { Client } from "boardgame.io/react";
import React, { useEffect, useMemo, useState } from "react";
import { Client } from "boardgame.io/client";
import { Gomoku } from "./Game";
import { GomokuBoard } from "./Board";//const App = Client({ game: Gomoku });
function App() {const client = useMemo(() => Client({ game: Gomoku }), []);const [boardProps, setBoardProps] = useState(client.getInitialState());useEffect(() => {let c = client;c.subscribe((s) => {setBoardProps({ ...s });});c.start();return () => c.stop();}, [client]);return <GomokuBoard {...boardProps} moves={client.moves} />;
}export default App;

添加机器人

前面提到 boardgame.io 支持 AI,只需要告诉引擎当前状态下的可行动作有哪些,AI 就会尝试搜索出最可能获胜的那个动作。

要添加 AI,需要在我们的游戏定义中添加 ai 配置项。其中 enumerate 函数应该返回当前状态 G 的所有可行动作(数组表示)。以五子棋为例,每个空白点位都可以执行一个 putStone 动作,函数就应该返回每个空白点对应的 putStone 动作。

export const Gomoku = {// setup, turn, moves, endIf ...ai: {enumerate: (G, ctx) => {let moves = [];for (let i = 0; i < G.stones.length; i++) {if (G.stones[i] === 0) {moves.push({ move: "putStone", args: [i] });}}return moves;}}
};

大功造成。现在打开 Debug 面板中的 AI 页,就可以使用 AI 了:

  • play 用来让 AI 执行一个动作。在五子棋中,就是走一步棋
  • simulate 用来让 AI 自己完成一局游戏。相当于自我对局

点击体验

美化棋盘

虽然已经做了一个棋盘,不过表格做的棋盘看起来不太美观,接下来我们美化一下。

wgo.js 是专门为制作网页围棋而开发的库,好在它的棋盘部分通用性和扩展性很好,完全能用于五子棋棋盘。它功能丰富,支持多种棋子渲染样式,还拥有主题、局部棋盘、标记等功能。接下来,我们就使用它的默认主题,制作一个简单的棋盘。

首先,添加 wgo 依赖项

npm install wgo

然后,更新 src/Board.js

import React, { useEffect, useMemo, useRef } from "react";
import { BOARD_SIZE } from "./Consts";
import { FieldBoardObject, SVGBoard } from "wgo";export function GomokuBoard({ G, ctx, moves }) {const boardContainerRef = useRef();const cachedStonesRef = useRef([]);const board = useMemo(() => {return new SVGBoard(document.createElement("div"), {size: BOARD_SIZE,width: 500,heith: 500,coordinates: true});}, []);useEffect(() => {const elem = board.element;boardContainerRef.current.appendChild(elem);return () => {boardContainerRef.current.removeChild(elem);};}, [board]);useEffect(() => {const handler = (ev, pos) => {let id = pos.y * BOARD_SIZE + pos.x;moves.putStone(id);};board.on("click", handler);return () => {board.off("click", handler);};}, [board, moves]);if (cachedStonesRef.current !== G.stones) {const cachedStones = cachedStonesRef.current;for (let i = 0; i < G.stones.length; i++) {if (cachedStones[i] === G.stones[i]) continue;let x = i % BOARD_SIZE;let y = Math.floor(i / BOARD_SIZE);if (cachedStones[i] !== 0) {board.removeObjectsAt(x, y);}if (G.stones[i] === 1) {board.addObject(new FieldBoardObject("B", x, y));} else if (G.stones[i] === -1) {board.addObject(new FieldBoardObject("W", x, y));} else {board.removeObjectsAt(x, y);}}cachedStonesRef.current = G.stones;}let winner = "";if (ctx.gameover) {winner =ctx.gameover.winner !== undefined ? (<div id="winner">Winner: {ctx.gameover.winner}</div>) : (<div id="winner">Draw!</div>);}return (<div><div ref={boardContainerRef} />{winner}</div>);
}

修改后的棋盘如下图,好看多了。

查看效果

wgo.js 目前的文档还是旧版本的,旧版本采用 Canvas 渲染,且只能加载到全局名字空间。新的 3.0 版本支持 SVG 和 UMD 模块,但还处在 alpha 阶段,没有文档和示例代码,需要直接参照它的源码使用。

总结

本文介绍了如何使用 boardgame.io 制作一个五子棋游戏,并添加了简单的 AI 功能,还介绍了如何使用 wgo.js 制作美观的棋盘,也算是一个像模像样的五子棋游戏了。接下来,还可以再美化下界面,强化一下 AI,甚至添加在线对局功能。

BoardGame.io 五子棋(一)相关推荐

  1. 2020年最新前端框架大全,Web工程师人手一份!

    今天跟大家分享一些目前比较热门新鲜度靠前的50款前端工具,希望对你有所帮助. 一.构建工具 1. Parcel 地址:https://parceljs.org Parcel是一款极速零配置WEB应用打 ...

  2. 开源巨献:2017 年 Google 开源了这些超赞的项目

    点击上方"CSDN",选择"置顶公众号" 关键时刻,第一时间送达! 春节小长假,走亲访友之余,还可以利用一些零碎的时间收集学习资料,给自己充充电,好在 2018 ...

  3. 开发2d游戏要用什么引擎_下一个游戏要使用什么2D游戏引擎

    开发2d游戏要用什么引擎 A few weeks ago, I posted about my experience attempting to make a prototype in a bunch ...

  4. 50个好用的前端框架,千万收好以留备用!

    来源 | https://www.jianshu.com/p/182b69e54fe8 今天跟你分享一些目前比较热门新鲜度靠前的50款前端工具,希望对你有所帮助. 一.构建工具 1. Parcel 地 ...

  5. 前端热门工具简直不要太好用了!

    ★★★★★ Web前端开发神器:点击查看→→  WebStorm安装与使用 今日送书!包邮!10本!(初来乍到,多多指教) <Bootstrap实战> <图解CSS3核心技术与案例实 ...

  6. 2020年最新前端框架大全,Web工程师人手一份

    今天跟大家分享一些目前比较热门新鲜度靠前的50款前端工具,希望对你有所帮助. 一.构建工具 1. Parcel 地址:https://parceljs.org Parcel是一款极速零配置WEB应用打 ...

  7. 50个好用的前端工具,建议收藏!

    来源 | https://www.jianshu.com/p/182b69e54fe8 今天跟你分享一些目前比较热门新鲜度靠前的50款前端工具,希望对你有所帮助. 一.构建工具 1. Parcel 地 ...

  8. 这50款前端热门工具简直不要太好用了!

    来源 | IT智云编程 19年,又是新的一年,"前端届",又出了哪些新的"玩意",今天向你推荐目前比较热门新鲜度靠前的50款前端工具,希望在新的一年里,对你有所 ...

  9. 前端每周清单第 45 期: Safari 支持 Service Worker, Parcel 完整教程, 2017 前端大事件...

    前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点:分为新闻热点.开发教程.工程实践.深度阅读.开源项目.巅峰人生等栏目.欢迎关注[前端之巅]微信公众号(ID: fron ...

最新文章

  1. VC++软件工程师高端培训
  2. [译]2019年修炼前端开发者之路
  3. 使用matlab判断CDMA接收码片
  4. android 动态设置View的高度和宽度,ViewTreeObserver使用
  5. linux cd 命令案例,15个关于Linux的‘cd’命令的练习例子
  6. 一种基于随机投影的本地差分隐私高维数值型数据收集算法
  7. 字符串在Python中的本质是一个序列。 数字类型不是序列,不是可迭代对象,只能看做一个整体不可分割...
  8. 将一个类改成线程_看了这个有趣的例子,相信你就秒懂多线程同步了
  9. python-基于UDP通信的套接字,socketserver模块的使用
  10. 自由软件基金会官宣Zoë Kooyman担任新执行董事
  11. 计算机六级好考吗,计算机六级考什么?
  12. MD5加密(加盐),为什么要在密码里加点“盐“
  13. 自动驾驶相关的期刊和会议
  14. Wireshark 用户使用手册 ———— 配置与属性
  15. 【历史上的今天】4 月 26 日:验证码的发明者诞生;切尔诺贝利病毒爆发;诺基亚收购 Withings
  16. CentOS 7.2 配置Apache服务(httpd)--上篇
  17. Python——绑定与方法调用
  18. 使用javabean把小写金额转换成大写金额
  19. C# 索引器(Indexer) this关键字的作用
  20. 通达OA任意用户登录

热门文章

  1. android dbflow引起内存泄漏,DBFlow使用说明(1)快速入门
  2. pdf转jpg怎么转?转换软件分享
  3. 基于android的航班查询飞机票订购系统app
  4. 为什么微信的文件助手不是点对点传输?
  5. ubuntu php composer,Ubuntu16.4下安装Composer
  6. antdsign 上传音频设置mp3格式,不触发beforeUpload事件
  7. 百度统计出现不属于自己网站的域名搜索词及数据怎么处理
  8. 超级计算机子系统,超级计算机之天河一号详解
  9. easeljs的基础
  10. 网络语言C位出道是什么意思,你们一直说的C位出道到底是什么意思啊?