Tic Tac Toe, часть 0: Сравнение Svelte и React
Tic Tac Toe, часть 1: Svelte и Canvas 2D
Tic Tac Toe, часть 2: Undo/Redo с хранением состояний
Tic Tac Toe, часть 3: Undo/Redo с хранением команд
Tic Tac Toe, часть 4: Взаимодействие с бэкендом на Flask с помощью HTTP
В статье "Сравнение: Svelte и React" я попробовал повторить разработку игры Tic Tac Toe. Там я выполнил только первую часть исходного туториала для React'а без поддержки истории ходов. В этой статье мы начнем разработку этой игры с применением фреймворка Svelte с поддержкой истории ходов. История ходов на самом деле представляет собой систему Undo/Redo. В исходном туториале на React'e реализована система Undo/Redo с хранением состояний с произвольным доступом к любому состоянию. При реализации системы Undo/Redo обычно используется паттерн Command, и в списке команд хранятся команды Undo/Redo. Такой подход мы попробуем реализовать позже, сейчас выполним систему Undo/Redo с хранением состояний.
В разработке применено архитектурное решение Flux с использованием хранилища. Об этом отдельный раздел в статье.
Стартовый код
<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. Более подробно об использовании этого элемента рассказано в предыдущей статье Разработка игры Breakout на Svelte. Как рисовать сетку подсмотрел здесь. Компонент Board можно использовать повторно в других играх. Изменив переменные 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();
Добавляем хранилище state
Код на REPL
В этом разделе проверяем изменение состояний с применением пользовательского хранилища. Добавил хранилище state в отдельном файле 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 добавил две кнопки State 1 и State 2. Нажимая на кнопки вызываем соответствующие методы в хранилище.
<button on:click={state.state1}>State 1</button> <button on:click={state.state2}>State 2</button>
В компоненте Board поменял строчку вывода нулей вывод данными их хранилища state. Здесь мы используем автоподписку на хранилище.
ctx.fillText($state[k], j * cellWidth + d + 1, (i + 1) * cellHeight - d);
На этом этапе игровое поле по-умолчанию заполняется нулями, нажимаем на кнопку State 1 — поле заполняется единицами, нажимаем на кпопку State 2 — поле заполняется двойками.
Заполнение клетки крестиком по клику мышкой
Код на REPL
В хранилище state добавил метод setCell(), который заполняет выбранную клетку крестиком.
setCell: (i) => update(a => {a[i] = 'X'; return a;}),
К canvas'у добавил обработчик события по клику мышкой, здесь определяем индекс клетки и вызываем метод setCell() хранилища state.
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
Напомню, что мы сейчас выполняем систему Undo/Redo с хранением состояний с произвольным доступом.
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();
Хранилище state удалено, добавлено хранилище history для хранения истории ходов. Описываем его с помощью клаcса History. Для хранения состояний используем массив history. Иногда при реализации системы Undo/Redo используется два LIFO стека: undo-стек и redo-стек. Мы используем один массив history для хранения состояний в History. Свойство current используется для определения текущего состояния игры. Все состояния в history с начала массива до состояния с индексом current, можно так сказать, находятся в списке Undo, а все остальные в списке Redo. Уменьшая или увеличивая свойство current, другими словами выполняя команды Undo или Redo, мы выбираем состояние ближе к началу, или к концу игры. Методы undo и redo пока не реализованы, они будут добавлены позже. В класс History добавлены методы currentState() и push(). Метод currentState() возвращает текущее состояние игры, c помощью метода push() мы добавляем новое состояние в историю ходов.
В компоненте App мы убрали кнопки State 1 и State 2. И добавили кнопку Push:
<button on:click={() => history.push(Array(9).fill($history.current + 1))}>Push</button>
По нажатии на эту кнопку в историю ходов добавляется новое состояние, массив history просто заполняется значением индекса текущего состояния current.
В компоненте Board выводим текущее состояние из истории ходов. Здесь продемонстрировано использование автоподписки на хранилище:
ctx.fillText($history.currentState()[k], j * cellWidth + d + 1, (i + 1) * cellHeight - d);
В методе push хранилища history можно добавить вывод в консоль браузера и наблюдать как оно изменяется после нажатия на кнопку Push.
h.push(state); console.log(h); return h;
Выполнение клика по клетке
Код на REPL
В компоненте App убрали кнопку Push.
В хранилище history определили метод 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 в функцию handleClick() добавили вызов метода хранилища clickCell():
history.clickCell(i);
В консоли браузера здесь также мы можем наблюдать как изменяется состояние истории ходов после каждого клика мышкой.
В следующих статьях мы доделаем игру до конца, с интерфейсом отмены/возврата шагов и произвольным доступом к любому шагу игры. Рассмотрим вариант реализации системы Undo/Redo с использованием паттерна проектирования Command. Рассмотрим взаимодействие с бэкендом, игрок будет соревноваться с интеллектуальным агентом в бэкенде.
Flux-архитектура
В разработке данной игры наблюдается применение архитектурного решения Flux. Действия реализованы в виде методов в определении хранилища history в файле stores.js. Присутствует хранилище history, которое описано в виде класса History. Представления реализованы в виде компонентов 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/.
