# 4. Перемещение шаров, линии и очки

Table of Contents

4. Перемещение шаров, линии и очки

Цель

Связать правила из текстового описания с работающим прототипом партии: один цикл действий человека включает произвольное число «продолжающихся» перемещений, пока после очередного перемещения с поля что‑то удаляется; при первом перемещении без удаления очередь переходит к противнику. Одновременно закрепить формулу очков и общую архитектуру данных доски так, чтобы следующий шаг плана мог заменить только стратегию ИИ.

Описание выполненной работы

Этот текст рассчитан на начинающего разработчика Godot 4.x, который уже собрал интерфейс поля по шагу 3 и хочет понять, как аккуратно отделить «математику доски» от представления Control‑узлов.

Зачем выносить состояние в отдельный класс

На игровом экране уже есть сетка из экземпляров CellView, но полезный инвариант звучит так: ни один игровой закон не должен зависеть от того, как именно Panel выглядит или масштабируется. Поэтому добавлен ресурс‑скрипт BoardState (src/scripts/board_state.gd) с массивом cells типа PackedInt32Array. Значение -1 означает пустую клетку, неотрицательное целое — индекс цвета в палитре «планет». Такой тип компактнее и надёжнее, чем хранить в модели объекты Color: сравнение цветов с плавающей точкой позже приведёт к неприятным ошибкам при изменении палитры.

Класс содержит три ключевые группы методов:

  1. Геометрия индексов. Функции idx_to_rc и rc_to_idx задают однозначное соответствие между порядковым номером ребёнка GridContainer (строки заполняются слева направо и сверху вниз при columns = 8) и парой координат Vector2i. Это избавляет UI‑код от дублирования формул.

  2. Достижимость. Правило из описания игры формулируется классически для задач на сетке: шар можно переместить из клетки from_idx в пустую клетку to_idx, если существует ортогональный путь по клеткам, которые сейчас пусты. Первая «волна» обхода берёт только тех соседей источника, которые уже свободны; дальше обход расширяется как обычный поиск в ширину. Так мы явно моделируем фразу «путь из пустых клеток», не смешивая её со случаями диагонального хода или «прыжка».

  3. Поиск линий. После каждого перемещения доска проверяется на наличие отрезков длины три и более одинакового вида шара по четырём направлениям: строки, столбцы и обе диагонали. Внутренний метод _process_index_list принимает упорядоченный список индексов клеток вдоль одной геометрической линии и использует простейший алгоритм подсчёта «пробегов» одинакового значения в массиве cells. Для каждого пробега длины L ≥ 3 к счёту добавляется величина 3 * (L - 2), что совпадает с таблицей из описания (три шара — три очка, четыре — шесть, пять — девять и так далее). Одновременно индексы попадают в словарь‑множество to_remove, чтобы удалить все затронутые клетки за один проход. Если один шар входит сразу в две пересекающиеся линии, он удаляется один раз, но обе линии дают вклад в сумму очков — это сознательное упрощение; при желании можно позже заменить модель начисления на более строгую.

Открытый метод collect_matches возвращает словарь с ключами score и to_remove. Такой интерфейс удобен вызывающему коду сцены: достаточно одной строки вида «получить результат → прибавить очки текущей стороне → очистить клетки модели → перерисовать вид».

Методы any_legal_move и list_legal_moves понадобились сразу по двум причинам: завершение партии при полном отсутствии ходов и временная заглушка искусственного интеллекта на случайном выборе среди допустимых пар «источник–цель». Полный перебор для поля восьми на шестнадцать клеток на современном телефоне остаётся дешёвым.

Связка модели и интерфейса в game_board.gd

Корневая сцена поля по‑прежнему подгоняет размер ячеек в _reflow_cells, но игровое состояние теперь хранится в переменной _board, созданной из предзагруженного скрипта (src/scenes/game_board.gd). Чтобы редактор не ругался на порядок парсинга class_name, использован явный preload("res://scripts/board_state.gd").

Очередь будущих шаров реализована массивом целых _placement_queue. Перед каждым полным ходом стороны из очереди извлекаются три значения функцией _pop_three_for_placement, который дополнительно гарантирует, что после изъятия сразу же генерируются новые случайные индексы до длины три. Таким образом, блок превью «Следующие» всегда показывает именно те виды шаров, которые появятся при следующей закладке тройки на свободные клетки. Размещение выполняется методом _place_kinds_on_board: собираются все индексы пустых клеток, массив перемешивается генератором _rng, затем цвет из партии записывается в первые min(3, количество пустых) позиций. Если свободных клеток меньше трёх (краевой случай плотной заполненности), закладывается столько, сколько возможно — это простое поведение, которое можно ужесточить позже отдельными правилами окончания.

Чередование игрока и компьютера следует текстовому описанию хода как серии перемещений:

  1. Человек выбирает клетку с шаром и затем пустую цель при выполнимости пути.
  2. Модель обновляется немедленно, затем один раз вызывается collect_matches.
  3. Если удалений не произошло, запускается корутина _end_human_segment_pass_to_ai, которая после короткой задержки вызывает _start_ai_turn и использует await, чтобы корректно дождаться всей асинхронной цепочки ИИ.
  4. Если удаление произошло, счёт человека увеличивается, соответствующие клетки очищаются, игрок остаётся активным до следующего «немого» перемещения.

Для компьютера используется цикл _ai_play_sequence: каждые несколько десятых секунды ИИ берёт случайную допустимую пару индексов из list_legal_moves, выполняет перемещение и повторяет ту же процедуру очистки линий, что и человек. Цикл продолжается, пока каждый очередной случайный ход всё ещё даёт хотя бы одну линию; как только перемещение перестало что‑то удалять, функция возвращает управление, после чего проверяется глобальная возможность хода и при необходимости показывается диалог завершения партии.

Чтобы игрок не совершал действий во время расчётов ИИ и чтобы исключить повторное открытие диалога, введены флаги _busy и _game_ended. Флаг _busy также помогает игнорировать щелчки по полю во время первоначальной закладки тройки шаров на начало человеческого хода.

Изменения в CellView

Панель ячейки переведена в режим приёма событий мыши и тача (mouse_filter = STOP в src/scenes/board/cell_view.tscn), потому что раньше узел полностью игнорировал ввод и пользователь не мог выбрать шар. В скрипт (src/scenes/board/cell_view.gd) добавлены целочисленный идентификатор позиции grid_index, сигнал cell_clicked(cell_index) и простая подсветка выбора через свойство modulate. Обработчик _gui_input фильтрует левую кнопку мыши и вызывает accept_event(), чтобы щелчок не «просачивался» сквозь несколько слоёв интерфейса.

Дочерний узел шара по‑прежнему игнорирует события: это означает, что попадание по кругу воспринимается как попадание по родительской ячейке, что совпадает с ожиданиями игрока на сенсорном экране.

Контрольные сценарии для ручной проверки

После сборки сцены имеет смысл пройти короткий чек‑лист без автоматических тестов:

  1. Создайте искусственную линию из трёх одинаковых шаров одним перемещением и убедитесь, что очки выросли на три, а шары исчезли с поля без какого‑либо «зависания» ввода.
  2. Переместите шар без образования линии: ход должен перейти к компьютеру, счёт человека не меняется, в превью появится новая тройка будущих цветов после закладки ИИ.
  3. Постарайтесь вызвать ситуацию с отсутствием пути: клик по далёкой пустой клетке не должен нарушать выбранный шар, если has_path возвращает ложь.
  4. Заполните большую часть поля случайными ходами и дождитесь диалога «Игра окончена», убедившись, что кнопка подтверждения возвращает в главное меню.

Что сознательно отложено до шага 5

Текущий ИИ не оценивает длинные последствия хода и не пытается максимизировать очки — это ровно та граница, которую описывает следующий пункт плана про «просчёт хода». Благодаря тому что вся логика допустимости и начисления инкапсулирована в BoardState и аккуратно вызывается из обеих веток человека и машины, заменить стратегию выбора пары индексов можно будет локальным изменением одного участка _ai_play_sequence без переписывания интерфейса.

Короткая шпаргалка по синхронизации вида и модели

Всегда придерживайтесь простого правила: сначала меняется cells, затем вызывается _sync_cells, который перебирает всех детей GridContainer и для каждого индекса либо вызывает clear_ball, либо передаёт в show_ball цвет из палитры по индексу вида. Любые визуальные эффекты вроде анимации перелёта между клетками лучше добавлять поверх этой дисциплины, а не смешивать с проверками пути — иначе отладка правил игры резко усложнится.

Если потребуется расширить правила (например, добавить «гравитацию» или бонусные фишки), начните с дублирования метода collect_matches в отдельную политику очистки и оставьте в BoardState только операции над сеткой. Такой слой помогает держать один файл читабельным даже при росте механик.

Ссылка на проект: Third Planet

Ссылка на игру: Google Play Rustore

Next: 5. Просчёт хода компьютера
Аватар автора

Спасибо, что прочитали статью. Посмотрите другие материалы в архиве, там много практических разборов по разным технологиям.


godot Series

# 16. Тени

godot 16 / 19
4 min read

Тени через StyleBoxFlat у ячеек, HUD, слота баннера и карточки результата; стили кнопок на титуле и поле; лёгкий контактный затемнённый слой на шарах-планетах.

Read