井字游戏第1部分:Svelte and Canvas 2D

井字游戏第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(); // vertical lines for (let x = 0; x <= boardWidth; x += cellWidth) { ctx.moveTo(0.5 + x, 0); ctx.lineTo(0.5 + x, boardHeight); } // horizontal lines for (let y = 0; y <= boardHeight; y += cellHeight) { ctx.moveTo(0, 0.5 + y); ctx.lineTo(boardWidth, 0.5 + y); } // draw the board ctx.strokeStyle = colorStroke; ctx.stroke(); ctx.closePath(); }); </script> <canvas bind:this={canvas} width={boardWidth} height={boardHeight} ></canvas> 

起始代码显示一个空的网格。 它使用HTML5 canvas元素显示。 有关使用此元素的更多信息,请参见上一篇文章在Svelte开发突破 。 如何在此处绘制间谍网格。 棋盘组件可以在其他游戏中重复使用。 通过更改widthheight变量,可以调整网格的大小;通过更改cellWidthcellHeight变量的值,可以调整单元格的大小。


用零填充单元格

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。 在此存储库中定义了更改游戏状态的state1state2方法。


 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) { // TODO: remove all redo states this.current++; this.history.push(state); } } function createHistory() { const { subscribe, set, update } = writable(new History); return { subscribe, push: (state) => update(h => { h.push(state); return h; }), }; } export const history = createHistory(); 

状态存储已被删除, 历史存储已被添加以存储移动的历史。 我们使用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 => { // create a copy of the current state const state = h.currentState().slice(); // change the value of the selected cell to X state[i] = 'X'; // add the new state to the history h.push(state); console.log(h.history); return h; }), 

Board组件中,将callClick()存储方法调用添加到handleClick()函数中:


 history.clickCell(i); 

在这里的浏览器控制台中,我们还可以查看每次单击鼠标后移动历史的状态如何变化。


在下面的文章中,我们将使用一个取消/返回步骤以及随机访问游戏任何步骤的界面来完成游戏的最后部分。 考虑使用命令设计模式实现撤消/重做系统。 考虑与后端的交互,玩家将与后端的智能代理竞争。


助焊剂架构

在开发该游戏时,观察到使用Flux体系结构解决方案 。 在stores.js文件的历史记录商店定义中,将操作实现为方法。 有一个历史存储库 ,以历史类的形式描述。 视图被实现为AppBoard组件。 对我来说,所有这些都与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 /

Source: https://habr.com/ru/post/zh-CN458752/


All Articles