Данная статья представляет из-себя эксперимент с Rust'ом с последующей его компиляцией в WASM. Было интересно пощупать данные технологии на чем-то сложнее, чем вычисление факториала, поэтому выбор пал на всем известную игру 2048.
Описание игры
Оригинальная игра представляет из себя клеточное поле 4 на 4.
Каждая клетка может быть либо пустой, либо занята плиткой
с числом, являющегося степенью двойки. Стартовое состояние поля
имеет 2 заполненные клетки.
Игрок может совершать ход в одном из 4 направлений: влево,
вправо, вверх и вниз, что сдвигает все плитки до упора в
выбранном
направлении. После каждого хода появляется новая плитка. Новая
плитка будет содержать двойку с вероятностью 90% или четверку
с вероятностью 10%.
Походим из стартового состояния, показанного выше, вправо:
Плитка на втором ряду уперлась в правую границу, также появилась двойка на первом ряду, третьем столбце.
Если плитка упирается в другую плитку, содержащее такое же число, они соединяются в одну плитку следующей степени двойки.
Походим из предыдущего состояния вверх:
Двойки последнего столбца первой и второй строк объединилсь в одну, а кроме этого на третьем ряду появилась четверка.
Цель игры: дойти до плитки с числом 2048.
Подготовка
Поскольку одной из целей этого эксперимента для меня было поиграться с Rust, задача выбора языка не стоит.
На данной странице представлен
список фронтовых фреймворков для Rust.
Самый популярный вариант Yew выглядит интересно, особенно
при наличии опыта с React'ом, документация и примеры
присутствуют,
так что выбор пал на него.
Инструкцию по созданию проекта можно найти здесь и выполняется она тривиально:
cargo new --lib rust-2048 && cd rust-2048
Дальше, в Cargo.toml прописываем зависимости:
[dependencies]yew = "0.17"wasm-bindgen = "0.2.67"
В примере предоставлен код для создания простого счетчика,
который нужно поместить в src/lib.rs
:
use wasm_bindgen::prelude::*;use yew::prelude::*;struct Model { link: ComponentLink<Self>, value: i64,}enum Msg { AddOne,}impl Component for Model { type Message = Msg; type Properties = (); fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { Self { link, value: 0, } } fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { Msg::AddOne => self.value += 1 } true } fn change(&mut self, _props: Self::Properties) -> ShouldRender { // Should only return "true" if new properties are different to // previously received properties. // This component has no properties so we will always return "false". false } fn view(&self) -> Html { html! { <div> <button onclick=self.link.callback(|_| Msg::AddOne)>{ "+1" }</button> <p>{ self.value }</p> </div> } }}#[wasm_bindgen(start)]pub fn run_app() { App::<Model>::new().mount_to_body();}
Создаем директорию для статичных файлов:
mkdir static
Внутри которой создаем index.html со следующим содержимым:
<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8" /> <title>Yew Sample App</title> <script type="module"> import init from "./wasm.js"; init(); </script> </head> <body></body></html>
Собираем проект при помощи wasm-pack (устанавливается через
cargo install wasm-pack
):
wasm-pack build --target web --out-name wasm --out-dir ./static
У меня при сборке появилась следующая ошибка:
Error: crate-type must be cdylib to compile to wasm32-unknown-unknown. Add the following to your Cargo.toml file:[lib]crate-type = ["cdylib", "rlib"]
Окей, так и сделаем. Открываем Cargo.toml и прописываем в конце:
[lib]crate-type = ["cdylib", "rlib"]
Теперь можем запустить любой сервер статики и проверить результат (в примере предлагают установить miniserve, я же воспользуюсь стандартным питоновским модулем):
cd staticpython -m SimpleHTTPServer
Открываем http://127.0.0.1:8000 и видим рабочий счетчик. Отлично, теперь можно приступать к самой игре.
Основа
Коммит: https://github.com/dev-family/wasm-2048/commit/6bc015fbc88c1633f4605944fd920b2780e261c1
Здесь я описал основные сущности игры и реализовал перемещение
плиток без объединений. Также были добавлены
вспомогательные структуры и методы для удобства.
Рассмотрим логику движения плиток на примере данного поля:
2, 4, 0, 00, 0, 0, 00, 0, 0, 00, 0, 0, 0
Предположим, что был совершен ход вправо. Ожидается следующий результат:
0, 0, 2, 40, 0, 0, 00, 0, 0, 00, 0, 0, 0
Такого поведения можно добиться, если идти от конца выбранного
направления и сдвигать все плитки по выбранному направлению.
Проитерирую вручную для лучшего понимания:
- Выбрано направление вправо => идем справа влево.
/-------- пустая клетка, пропускаем2, 4, 0, 00, 0, 0, 00, 0, 0, 00, 0, 0, 0
2.
/-------- пустая клетка, пропускаем2, 4, 0, 00, 0, 0, 00, 0, 0, 00, 0, 0, 0
3.
/-------- начинаем сдвигать вправо2, 4, 0, 00, 0, 0, 00, 0, 0, 00, 0, 0, 0
4.
/-------- продолжаем2, 0, 4, 00, 0, 0, 00, 0, 0, 00, 0, 0, 0
5.
/-------- дошли до конца, возвращаемся на следующую позицию от которой начали передвижение2, 0, 0, 40, 0, 0, 00, 0, 0, 00, 0, 0, 0
6.
/-------- двигаем вправо2, 0, 0, 40, 0, 0, 00, 0, 0, 00, 0, 0, 0
7.
/-------- продолжаем0, 2, 0, 40, 0, 0, 00, 0, 0, 00, 0, 0, 0
8.
/-------- остонавливаемся и переходим к следующей строке0, 0, 2, 40, 0, 0, 00, 0, 0, 00, 0, 0, 0
Также стоит заметить, что в случае горизонтальных ходов строки двигаются независимо друг от друга, а в случае вертикальных столбцы.
В коде данный алгоритм реализован методами
Direction#build_traversal
(построение пути обхода
поля),
Grid#traverse_from
(продвижение конкретной клетки по
направлению) и Grid#move_in
публичный метод,
использующий предыдущие два.
Объединения
Коммит: https://github.com/dev-family/wasm-2048/commit/7e08b5af6008e6f9d716d7c9a41b0810e869df9e
У объединения плиток есть одна особенность: плитки не
объединяются больше одного раза,
т.е. в случае наезжающих друг на друга 4 одинаковых плит,
результатом будет являться 2 плитки
следующей степени. Из этого следует, что у плиток должно появиться
некое состояние, которое нужно
сбрасывать перед каждым ходом.
Плюс, пришлось изменить структуру TestCase, т.к. правильное поведение можно протестировать только за несколько ходов.
Добавление новых плиток
Коммит: https://github.com/dev-family/wasm-2048/commit/6082409412f6c19943c453c5d706d57bbcef538b
Для рандома используется пакет rand, который работает и в WASM
среде
путем добавления wasm-bindgen
feature.
Для того, чтобы не ломать предыдущие тесты, которые не
рассчитаны на добавление новых случайных
плиток в структуру Grid
было добавлено новое флаговое
поле enable_new_tiles
.
Поскольку новые плитки добавлять нужно только в случае валидного
хода (т.е. за ход произошло хоть одно изменение поля),
сигнатура traverse_from
меняется. Теперь метод
возращает булево значение, которое показывает, было ли
движение.
UI
Коммит: https://github.com/dev-family/wasm-2048/commit/356e0889a84d7fc2582662e76238f94fc69bfed7
UI часть реализуется довольно просто, особенно для знакомых с
React и/или JSX. Прочитать про html!
макрос из Yew
можно здесь,
а про компоненты в общем здесь. Мне очень напомнило
React в пре-хуковых времен.
Документации по работе с клавиатурой не нашлось, да и в принципе сервисы никак не документированы, нужно читать исходники, смотреть примеры.
Анимации
Коммит: https://github.com/dev-family/wasm-2048/commit/e258748ab114ec5f930dbeb77d674bdbc5e63b1a.
Чтобы интерфейс смотрелся более живым, необходимо добавить анимации.
Они реализованы поверх обычных CSS transitions. Мы учим плитки запоминать их предыдущую позицию и при рендере отображаем плитку сразу в старой позиции, а на следующем тике в новой.