Tic Tac Toe, часть 2

Продолжение статьи Tic Tac Toe, часть 1, в которой мы начали разработку этой игры на Svelte. В этой части мы доделаем игру до конца. Добавим команды Undo/Redo, произвольный доступ к любому шагу игры, попеременные ходы с противником, вывод статуса игры, определение победителя.

Команды Undo/Redo

Код на REPL

На этом этапе в приложение были добавлены команды 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>

Смена хода

Код на REPL

Выполнено попеременное появление крестика или нолика после клика мышкой.

Метод 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 сейчас может воспринимать состояния описанные любым образом.

Произвольный доступ к истории ходов

Код на REPL

В хранилище 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>

Определение победителя, вывод статуса игры

Код на REPL

Добавлена функция определения победителя 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/.

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *