diff --git a/2048-wasm/article.en.md b/2048-wasm/article.en.md new file mode 100644 index 0000000..f01efd7 --- /dev/null +++ b/2048-wasm/article.en.md @@ -0,0 +1,321 @@ +This article is an experiment with Rust with its subsequent compilation in WASM. +It was interesting to try these technologies on something more difficult than calculating factorial, so +the choice fell on the well-known 2048 game. + +# Game + +The original game represents a 4 by 4 square field. Each square can be either empty or occupied by a tile +with a number that is a power of 2. The starting state of the field has 2 filled cells. + +

+ +

+ +The player can make a move in one of 4 directions: left, right, up and down, moving all tiles all the way in the +selected direction. A new tile appears after each move. The new tile will contain a 2 with a 90% probability or a 4 +with a probability of 10%. + +For example, lets move from the starting state shown above to the right: + +

+ +

+ +The tile on the second row rested against the right border, and a 2 appeared on the first row, the third column. + +If a tile rests on another tile containing the same number, they are merge into one tile of the next power of two. + +Lets move up from the previous state: + +

+ +

+ +Twos of the last column of the first and second rows merged into one, and in addition, a four appeared on the third row. + +Purpose of the game: reach the tile with the number 2048. + +# Preparation + +Since one of the goals of this experiment for me was to play with Rust, there is no task of choosing a language. + +[This page](https://awesomeopensource.com/project/flosse/rust-web-framework-comparison) provides a list of front-end frameworks for Rust. +The most popular option - [Yew](https://yew.rs/) - looks interesting, especially if you have experience with React. + +Instructions for creating a project can be found [here](https://yew.rs/docs/en/getting-started/build-a-sample-app): + +```bash +cargo new --lib rust-2048 && cd rust-2048 +``` + +Next, add dependencies to Cargo.toml: + +```toml +[dependencies] +yew = "0.17" +wasm-bindgen = "0.2.67" +``` + +The example provides code for creating a simple counter. `src/lib.rs`: + +```rust +use wasm_bindgen::prelude::*; +use yew::prelude::*; + +struct Model { + link: ComponentLink, + value: i64, +} + +enum Msg { + AddOne, +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + fn create(_: Self::Properties, link: ComponentLink) -> 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! { +
+ +

{ self.value }

+
+ } + } +} + +#[wasm_bindgen(start)] +pub fn run_app() { + App::::new().mount_to_body(); +} +``` + +Create a directory for static files: + +```bash +mkdir static +``` + +Inside it we create index.html with the following content: + +```html + + + + + Yew Sample App + + + + +``` + +Build the project using wasm-pack (installed via cargo install wasm-pack): + +```bash +wasm-pack build --target web --out-name wasm --out-dir ./static +``` + +I got the following error while building: + +```text +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"] +``` + +Okay, so let's do it. Open Cargo.toml and add at the end: + +```toml +[lib] +crate-type = ["cdylib", "rlib"] +``` + +Now we can start any static server and check the result (in the example they suggest installing miniserve, but I will use a standard Python module): + +```bash +cd static +python -m SimpleHTTPServer +``` + +Open http://127.0.0.1:8000 and see the working counter. Great, now we can start building the game itself. + +# Basics + +Commit: https://github.com/dev-family/wasm-2048/commit/6bc015fbc88c1633f4605944fd920b2780e261c1 + +Here I described the basic logic of the game and implemented moving tiles without merging. Also, I've added +helper structures and methods for convenience. + +Let's consider the logic of the movement of tiles using this field as an example: + +```text +2, 4, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +Suppose a move was made to the right. The following result is expected: + +```text +0, 0, 2, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +This behavior can be achieved if you go from the end of the selected direction and move all the tiles in the selected direction. +I will iterate manually for a better understanding: + +1. Selected direction is right => go from right to left. + +```text + /-------- empty cell, skip +2, 4, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +2. + +```text + /-------- empty cell, skip +2, 4, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +3. + +```text + /-------- start to shift to the right +2, 4, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +4. + +```text + /-------- continue +2, 0, 4, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +5. + +```text + /-------- reached the end, return to the next position from which we began to move +2, 0, 0, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +6. + +```text + /-------- move to the right +2, 0, 0, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +7. + +```text + /-------- continue +0, 2, 0, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +8. + +```text + /-------- stop and go to the next line +0, 0, 2, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +It is also worth noting that in the case of horizontal moves, the rows move independently of each other, and in the case of vertical moves, the columns. + +In the code, this algorithm is implemented by the methods `Direction#build_traversal` (building a path to traverse the field), +`Grid#traverse_from` (movement of a specific cell in the direction) and` Grid#move_in` is a public method using the previous two. + +# Merging + +Commit: https://github.com/dev-family/wasm-2048/commit/7e08b5af6008e6f9d716d7c9a41b0810e869df9e + +Combining tiles has one nuance: tiles do not merge more than once, +those in the case of overlapping 4 identical tiles, the result will be 2 tiles +of the next power. So, the tiles should have a certain state that is needed +to be dropped before each move. + +Additionally, I had to change the structure of TestCase, because correct behavior can only be tested in more than +a one move. + +# Adding new tiles + +Commit: https://github.com/dev-family/wasm-2048/commit/6082409412f6c19943c453c5d706d57bbcef538b + +The [rand package](https://crates.io/crates/rand) is used for random, which also works in the WASM environment +by adding the `wasm-bindgen` feature. + +In order not to break previous tests, which are not prepared for a new tiles +I've added a new flag field `enable_new_tiles` to the `Grid` structure. + +Since new tiles need to be added only in the case of a valid move (i.e. at least one field change occurred during the move), +the `traverse_from` signature changes. The method now returns a boolean value that indicates whether there was movement. + +# UI + +Commit: https://github.com/dev-family/wasm-2048/commit/356e0889a84d7fc2582662e76238f94fc69bfed7 + +The UI part is pretty simple to implement, especially for those familiar with React and/or JSX. You can read about the `html!` Macro from Yew [here](https://yew.rs/docs/en/concepts/html/), +and about components in general [here](https://yew.rs/docs/en/concepts/components). It reminds me a lot of React in pre-hook times. + +There was no documentation of working with the keyboard, and, in principle, the services are not documented in any way, so you need to read the sources, watch examples. + +# Animations + +Commit: https://github.com/dev-family/wasm-2048/commit/e258748ab114ec5f930dbeb77d674bdbc5e63b1a. + +To make the interface look more live, we need to add animations. + +They are implemented on top of regular CSS transitions. We make tiles to remember their previous position then while rendering we display the tile immediately in the old position, and on the next tick - in the new one. diff --git a/2048-wasm/article.ru.md b/2048-wasm/article.ru.md new file mode 100644 index 0000000..bb0fae3 --- /dev/null +++ b/2048-wasm/article.ru.md @@ -0,0 +1,322 @@ +Данная статья представляет из-себя эксперимент с Rust'ом с последующей его компиляцией в WASM. +Было интересно пощупать данные технологии на чем-то сложнее, чем вычисление факториала, поэтому +выбор пал на всем известную игру 2048. + +# Описание игры + +Оригинальная игра представляет из себя клеточное поле 4 на 4. Каждая клетка может быть либо пустой, либо занята плиткой +с числом, являющегося степенью двойки. Стартовое состояние поля имеет 2 заполненные клетки. + +

+ +

+ +Игрок может совершать ход в одном из 4 направлений: влево, вправо, вверх и вниз, что сдвигает все плитки до упора в выбранном +направлении. После каждого хода появляется новая плитка. Новая плитка будет содержать двойку с вероятностью 90% или четверку +с вероятностью 10%. + +Походим из стартового состояния, показанного выше, вправо: + +

+ +

+ +Плитка на втором ряду уперлась в правую границу, также появилась двойка на первом ряду, третьем столбце. + +Если плитка упирается в другую плитку, содержащее такое же число, они соединяются в одну плитку следующей степени двойки. + +Походим из предыдущего состояния вверх: + +

+ +

+ +Двойки последнего столбца первой и второй строк объединилсь в одну, а кроме этого на третьем ряду появилась четверка. + +Цель игры: дойти до плитки с числом 2048. + +# Подготовка + +Поскольку одной из целей этого эксперимента для меня было поиграться с Rust, задача выбора языка не стоит. + +На [данной странице](https://awesomeopensource.com/project/flosse/rust-web-framework-comparison) представлен список фронтовых фреймворков для Rust. +Самый популярный вариант - [Yew](https://yew.rs/) - выглядит интересно, особенно при наличии опыта с React'ом, документация и примеры присутствуют, +так что выбор пал на него. + +Инструкцию по созданию проекта можно найти [здесь](https://yew.rs/docs/en/getting-started/build-a-sample-app) и выполняется она тривиально: + +```bash +cargo new --lib rust-2048 && cd rust-2048 +``` + +Дальше, в Cargo.toml прописываем зависимости: + +```toml +[dependencies] +yew = "0.17" +wasm-bindgen = "0.2.67" +``` + +В примере предоставлен код для создания простого счетчика, который нужно поместить в `src/lib.rs`: + +```rust +use wasm_bindgen::prelude::*; +use yew::prelude::*; + +struct Model { + link: ComponentLink, + value: i64, +} + +enum Msg { + AddOne, +} + +impl Component for Model { + type Message = Msg; + type Properties = (); + fn create(_: Self::Properties, link: ComponentLink) -> 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! { +
+ +

{ self.value }

+
+ } + } +} + +#[wasm_bindgen(start)] +pub fn run_app() { + App::::new().mount_to_body(); +} +``` + +Создаем директорию для статичных файлов: + +```bash +mkdir static +``` + +Внутри которой создаем index.html со следующим содержимым: + +```html + + + + + Yew Sample App + + + + +``` + +Собираем проект при помощи wasm-pack (устанавливается через `cargo install wasm-pack`): + +```bash +wasm-pack build --target web --out-name wasm --out-dir ./static +``` + +У меня при сборке появилась следующая ошибка: + +```text +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 и прописываем в конце: + +```toml +[lib] +crate-type = ["cdylib", "rlib"] +``` + +Теперь можем запустить любой сервер статики и проверить результат (в примере предлагают установить miniserve, я же воспользуюсь стандартным питоновским модулем): + +```bash +cd static +python -m SimpleHTTPServer +``` + +Открываем http://127.0.0.1:8000 и видим рабочий счетчик. Отлично, теперь можно приступать к самой игре. + +# Основа + +Коммит: https://github.com/dev-family/wasm-2048/commit/6bc015fbc88c1633f4605944fd920b2780e261c1 + +Здесь я описал основные сущности игры и реализовал перемещение плиток без объединений. Также были добавлены +вспомогательные структуры и методы для удобства. + +Рассмотрим логику движения плиток на примере данного поля: + +```text +2, 4, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +Предположим, что был совершен ход вправо. Ожидается следующий результат: + +```text +0, 0, 2, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +Такого поведения можно добиться, если идти от конца выбранного направления и сдвигать все плитки по выбранному направлению. +Проитерирую вручную для лучшего понимания: + +1. Выбрано направление вправо => идем справа влево. + +```text + /-------- пустая клетка, пропускаем +2, 4, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +2. + +```text + /-------- пустая клетка, пропускаем +2, 4, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +3. + +```text + /-------- начинаем сдвигать вправо +2, 4, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +4. + +```text + /-------- продолжаем +2, 0, 4, 0 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +5. + +```text + /-------- дошли до конца, возвращаемся на следующую позицию от которой начали передвижение +2, 0, 0, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +6. + +```text + /-------- двигаем вправо +2, 0, 0, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +7. + +```text + /-------- продолжаем +0, 2, 0, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 0, 0, 0 +``` + +8. + +```text + /-------- остонавливаемся и переходим к следующей строке +0, 0, 2, 4 +0, 0, 0, 0 +0, 0, 0, 0 +0, 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](https://crates.io/crates/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 можно [здесь](https://yew.rs/docs/en/concepts/html/), +а про компоненты в общем [здесь](https://yew.rs/docs/en/concepts/components). Мне очень напомнило React в пре-хуковых времен. + +Документации по работе с клавиатурой не нашлось, да и в принципе сервисы никак не документированы, нужно читать исходники, смотреть примеры. + +# Анимации + +Коммит: https://github.com/dev-family/wasm-2048/commit/e258748ab114ec5f930dbeb77d674bdbc5e63b1a. + +Чтобы интерфейс смотрелся более живым, необходимо добавить анимации. + +Они реализованы поверх обычных CSS transitions. Мы учим плитки запоминать их предыдущую позицию и при рендере +отображаем плитку сразу в старой позиции, а на следующем тике -- в новой. diff --git a/2048-wasm/images/first-move.png b/2048-wasm/images/first-move.png new file mode 100644 index 0000000..febc7f9 Binary files /dev/null and b/2048-wasm/images/first-move.png differ diff --git a/2048-wasm/images/result.png b/2048-wasm/images/result.png new file mode 100644 index 0000000..e7398fe Binary files /dev/null and b/2048-wasm/images/result.png differ diff --git a/2048-wasm/images/second-move.png b/2048-wasm/images/second-move.png new file mode 100644 index 0000000..b73eaa4 Binary files /dev/null and b/2048-wasm/images/second-move.png differ diff --git a/2048-wasm/images/starting-state.png b/2048-wasm/images/starting-state.png new file mode 100644 index 0000000..c49f796 Binary files /dev/null and b/2048-wasm/images/starting-state.png differ