Tic Tac Toe, часть 1: Svelte и Canvas 2D

  • Tutorial
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 с использованием хранилища. Об этом отдельный раздел в статье.


Стартовый код

Код на REPL


App.svelte
<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>

Board.svelte
<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/.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 5

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое