井字游戏第0部分:比较苗条和反应
井字游戏第1部分:Svelte and Canvas 2D
井字游戏第2部分:无状态复原/重做
井字游戏,第3部分:使用命令存储撤消/重做
井字游戏第4部分:使用HTTP与Flask后端交互
在文章“比较:Svelte和React”中,我试图重复游戏Tic Tac Toe的开发。 在那里,我只完成了React原始教程的第一部分,而没有支持移动的历史。 在本文中,我们将开始使用Svelte框架并支持移动历史来开发这款游戏。 移动的历史实际上是一个撤消/重做系统。 在React的原始教程中,实现了具有状态存储和对任何状态的随机访问权的Undo / Redo系统。 实施撤消/重做系统时,通常使用命令模式,并且撤消/重做命令存储在命令列表中。 我们将稍后尝试实现此方法;现在,我们将使用状态存储执行撤消/重做系统。
该开发使用使用存储的Flux架构解决方案 。 这是本文的单独部分。
起始码
REPL代码
瘦身<script> import Board from './Board.svelte'; </script> <div class="game"> <div class="game-board"> <Board /> </div> <div class="game-info"> <div class="status">Next player: X</div> <div></div> <ol></ol> </div> </div> <style> .game { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; display: flex; flex-direction: row; } .game-info { margin-left: 20px; } .status { margin-bottom: 10px; } ol { padding-left: 30px; } </style>
滑板 <script> import { onMount } from 'svelte'; export let width = 3; export let height = 3; export let cellWidth = 34; export let cellHeight = 34; export let colorStroke = "#999"; let boardWidth = 1 + (width * cellWidth); let boardHeight = 1 + (height * cellHeight); let canvas; onMount(() => { const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, boardWidth, boardHeight); ctx.beginPath(); </script> <canvas bind:this={canvas} width={boardWidth} height={boardHeight} ></canvas>
起始代码显示一个空的网格。 它使用HTML5 canvas元素显示。 有关使用此元素的更多信息,请参见上一篇文章在Svelte上开发突破 。 如何在此处绘制间谍网格。 棋盘组件可以在其他游戏中重复使用。 通过更改width和height变量,可以调整网格的大小;通过更改cellWidth和cellHeight变量的值,可以调整单元格的大小。
用零填充单元格
REPL代码
在函数onMount()中 ,在网格输出之后的单元格中添加了零输出。 还有一些幻数与单元格中的定位值有关。
ctx.beginPath(); ctx.font = "bold 22px Century Gothic"; let d = 8; for (let i = 0; i < height; i+=1) { for (let j = 0; j < width; j+=1) { ctx.fillText("O", j * cellWidth + d + 1, (i + 1) * cellHeight - d); } } ctx.closePath();
添加状态存储
REPL代码
在本节中,我们使用用户存储库验证状态更改。 在单独的stores.js文件中添加了状态存储,该存储被导入到两个组件:App和Board。 在此存储库中定义了更改游戏状态的state1和state2方法。
import { writable } from 'svelte/store'; function createState() { const { subscribe, set, update } = writable(Array(9).fill('O')); return { subscribe, state1: () => set(Array(9).fill('1')), state2: () => set(Array(9).fill('2')), }; } export const state = createState();
在 App组件中添加了两个状态1和状态2按钮。 通过单击按钮,我们在存储库中调用相应的方法。
<button on:click={state.state1}>State 1</button> <button on:click={state.state2}>State 2</button>
在Board组件中,我将零的输出行更改为状态存储中的数据输出。 在这里,我们使用自动订阅进行存储 。
ctx.fillText($state[k], j * cellWidth + d + 1, (i + 1) * cellHeight - d);
在此阶段,默认情况下,游戏字段填充为零,单击“ 状态1”按钮-该字段填充单位,单击“ 状态2”按钮-该字段填充平局。
单击鼠标填充单元格
REPL代码
在状态存储区中,我添加了setCell()方法,该方法用一个十字形填充了选定的单元格。
setCell: (i) => update(a => {a[i] = 'X'; return a;}),
在画布上添加了一个鼠标单击事件处理程序,在这里我们确定单元格的索引并调用状态存储的setCell()方法。
function handleClick(event) { let x = Math.trunc((event.offsetX + 0.5) / cellWidth); let y = Math.trunc((event.offsetY + 0.5) / cellHeight); let i = y * width + x; state.setCell(i); }
默认情况下,比赛区域用零填充,我们单击任何单元格,将零替换为叉号。
行动史
REPL代码
让我提醒您,我们当前正在运行具有状态存储和随机访问权限的撤消/重做系统。
import { writable } from 'svelte/store'; class History { constructor() { this.history = new Array; this.current = -1; } currentState() { return this.history[this.current]; } push(state) {
状态存储已被删除, 历史存储已被添加以存储移动的历史。 我们使用History类描述它。 要存储状态,请使用history数组。 有时,在实现撤消/重做系统时,会使用两个LIFO堆栈:撤消堆栈和重做堆栈。 我们使用单个历史记录数组将状态存储在History中 。 current属性用于确定游戏的当前状态。 可以说,从数组开始到具有当前索引的状态, 历史上的所有状态都在“撤消”列表中,所有其他状态都在“重做”列表中。 减少或增加当前属性,换句话说,执行“撤消”或“重做”命令,我们选择更接近游戏开始或结束的状态。 撤消和重做方法尚未实现,将在以后添加。 CurrentState()和push()方法已添加到History类。 currentState()方法返回游戏的当前状态,使用push()方法,我们向移动历史添加新状态。
在App组件中,我们删除了状态1和状态2按钮。 并添加了一个按钮:
<button on:click={() => history.push(Array(9).fill($history.current + 1))}>Push</button>
通过单击此按钮,将新状态添加到移动历史记录中,该历史记录数组仅用当前状态索引值current填充。
在棋盘组件中,显示移动历史记录中的当前状态。 这是使用自动订阅存储的方法 :
ctx.fillText($history.currentState()[k], j * cellWidth + d + 1, (i + 1) * cellHeight - d);
在历史记录存储的push方法中,可以将输出添加到浏览器控制台,并在单击Push按钮后观察其变化。
h.push(state); console.log(h); return h;
点击一个单元格
REPL代码
在App组件中,“按钮”已被删除。
clickCell方法已在历史记录 存储区中定义。 在这里,我们创建游戏状态的完整副本,更改所选单元格的状态,并将新状态添加到移动历史记录中:
clickCell: (i) => update(h => {
在Board组件中,将callClick()存储方法调用添加到handleClick()函数中:
history.clickCell(i);
在这里的浏览器控制台中,我们还可以查看每次单击鼠标后移动历史的状态如何变化。
在下面的文章中,我们将使用一个取消/返回步骤以及随机访问游戏任何步骤的界面来完成游戏的最后部分。 考虑使用命令设计模式实现撤消/重做系统。 考虑与后端的交互,玩家将与后端的智能代理竞争。
助焊剂架构
在开发该游戏时,观察到使用Flux体系结构解决方案 。 在stores.js文件的历史记录商店定义中,将操作实现为方法。 有一个历史存储库 ,以历史类的形式描述。 视图被实现为App和Board组件。 对我来说,所有这些都与MVC架构的侧视图相同。 动作-控制器,存储-模型,视图-视图。 两种架构的描述实际上是一致的。
GitHub存储库
https://github.com/nomhoi/tic-tac-toe-part1
在本地计算机上安装游戏:
git clone https://github.com/nomhoi/tic-tac-toe-part1.git cd tic-tac-toe-part1 npm install npm run dev
我们在以下地址的浏览器中启动游戏: http:// localhost:5000 / 。