Продолжение статьи Tic Tac Toe, часть 1, в которой мы начали разработку этой игры на Svelte. В этой части мы доделаем игру до конца. Добавим команды Undo/Redo, произвольный доступ к любому шагу игры, попеременные ходы с противником, вывод статуса игры, определение победителя.
Команды Undo/Redo
На этом этапе в приложение были добавлены команды Undo/Redo. В хранилище history добавлены методы push и redo.
undo: () => update(h => { h.undo(); return h; }), redo: () => update(h => { h.redo(); return h; }),
В класс History добавлены методы push, redo, canUndo, canRedo.
canUndo() { return this.current > 0; } canRedo() { return this.current < this.history.length - 1; } undo() { if (this.canUndo()) this.current--; } redo() { if (this.canRedo()) this.current++; }
В метод push класса History добавлено удаление всех состояний от текущего до последнего. Если мы несколько раз выполним команду Undo и выполним клик в игровом поле, то все состояния справа от текущего до последнего будут удалены из хранилища и будет добавлено новое состояние.
push(state) { // remove all redo states if (this.canRedo()) this.history.splice(this.current + 1); // add a new state this.current++; this.history.push(state); }
В компоненте App добавлены кнопки Undo и Redo. Если выполнение команд не возможно, то они деактивируются.
<div> {#if $history.canUndo()} <button on:click={history.undo}>Undo</button> {:else} <button disabled>Undo</button> {/if} {#if $history.canRedo()} <button on:click={history.redo}>Redo</button> {:else} <button disabled>Redo</button> {/if} </div>
Смена хода
Выполнено попеременное появление крестика или нолика после клика мышкой.
Метод clickCell() убран их хранилища history, весь код метода перенесен в обработчик handleClick() компонента Board.
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; const state = $history.currentState(); const squares = state.squares.slice(); squares[i] = state.xIsNext ? 'X' : 'O'; let newState = { squares: squares, xIsNext: !state.xIsNext, }; history.push(newState); }
Таким образом была устранена ранее допущенная ошибка, в хранилище была зависимость от логики этой конкретной игры. Сейчас эта ошибка устранена, и хранилище можно использовать повторно в других играх и приложениях без изменений.
Ранее состояние шага игры описывалось только массивом из 9 значений. Сейчас состояние игры определяется объектом содержащим массив и свойством xIsNext. Инициализация этого объекта в начале игры выглядит так:
let state = { squares: Array(9).fill(''), xIsNext: true, };
И еще можно отметить, что хранилище history сейчас может воспринимать состояния описанные любым образом.
Произвольный доступ к истории ходов
В хранилище history добавили метод setCurrent(current), с помощью которого устанавливаем выбранное текущее состояние игры.
setCurrent(current) { if (current >= 0 && current < this.history.length) this.current = current; }
setCurrent: (current) => update(h => { h.setCurrent(current); return h; }),
В компоненте App добавили вывод истории ходов в виде кнопок.
<ol> {#each $history.history as value, i} {#if i==0} <li><button on:click={() => history.setCurrent(i)}>Go to game start</button></li> {:else} <li><button on:click={() => history.setCurrent(i)}>Go to move #{i}</button></li> {/if} {/each} </ol>
Определение победителя, вывод статуса игры
Добавлена функция определения победителя calculateWinner() в отдельном файле helpers.js:
export function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }
Добавлено производное хранилище status для определения статуса игры, здесь определяется исход игры: победитель или ничья:
export const status = derived( history, $history => { if ($history.currentState()) { if (calculateWinner($history.currentState().squares)) return 1; else if ($history.current == 9) return 2; } return 0; } );
В компоненте App добавлен вывод статуса игры:
<div class="status"> {#if $status === 1} <b>Winner: {!$history.currentState().xIsNext ? 'X' : 'O'}</b> {:else if $status === 2} <b>Draw</b> {:else} Next player: {$history.currentState().xIsNext ? 'X' : 'O'} {/if} </div>
В компоненте Board в обработчик клика handleClick() добавлены ограничения: невозможно выполнить клик в заполненной клетке и по окончании игры.
const state = $history.currentState(); if ($status == 1 || state.squares[i]) return;
Игра закончена! В следующей статье рассмотрим реализацию этой же игры с помощью паттерна Command, т.е. с хранением команд Undo/Redo вместо хранения отдельных состояний.
Репозиторий на GitHub
https://github.com/nomhoi/tic-tac-toe-part2
Установка игры на локальном компьютере:
git clone https://github.com/nomhoi/tic-tac-toe-part2.git cd tic-tac-toe-part2 npm install npm run dev
Запускаем игру в браузере по адресу: http://localhost:5000/.
ссылка на оригинал статьи https://habr.com/ru/post/459630/
Добавить комментарий