Skip to content

Latest commit

 

History

History
275 lines (210 loc) · 18.8 KB

File metadata and controls

275 lines (210 loc) · 18.8 KB

Техническое задание: Серверная логика игры "Длинные нарды" для Telegram Mini Apps

1. Общие сведения

1.1 Описание игры

Длинные нарды (нард) - настольная игра для двух игроков на доске из 24 позиций (пунктов). Каждый игрок имеет 15 шашек. Цель игры - провести все свои шашки в дом и снять их с доски быстрее противника.

1.2 Технический стек

  • Backend: NestJS + Colyseus
  • Платформа: Telegram Mini Apps
  • Количество игроков: 2

2. Игровое поле и начальная расстановка

2.1 Структура доски

Доска состоит из 24 пунктов, пронумерованных от 1 до 24:
- Пункты 1-6: дом белых (white)
- Пункты 7-12: двор белых
- Пункты 13-18: дом черных (black)
- Пункты 19-24: двор черных

Визуализация нумерации (вид для белых):
13 14 15 16 17 18    19 20 21 22 23 24 ← СТАРТ белых
12 11 10  9  8  7     6  5  4  3  2  1  ← ДОМ белых

2.2 Начальная позиция

const INITIAL_POSITION = {
  white: {
    24: 15  // все 15 белых шашек на пункте 24
  },
  black: {
    12: 15  // все 15 черных шашек на пункте 12
  }
}

2.3 Направление движения

  • Белые: движутся от пункта 24 к пункту 1 (по убыванию)

    • Маршрут: 24 → 23 → 22 → ... → 2 → 1
    • Дом: пункты 1-6
  • Черные: движутся от пункта 12 через пункт 1 к пункту 13 (против часовой стрелки)

    • Маршрут: 12 → 11 → 10 → 9 → 8 → 7 → 6 → 5 → 4 → 3 → 2 → 1 → 24 → 23 → 22 → 21 → 20 → 19 → 18 → 17 → 16 → 15 → 14 → 13
    • Дом: пункты 13-18

Важно: Оба игрока движутся в одном направлении (против часовой стрелки), но стартуют с противоположных точек доски.

2.4 Визуализация для каждого игрока

Каждый игрок видит доску "со своей стороны". Доска поворачивается на 180° для игрока с черными шашками.

Для игрока WHITE (видит доску нормально):

[Соперник вверху]
13 14 15 16 17 18    19 20 21 22 23 24 ← МОИ шашки (старт)
                      ↓  ↓  ↓  ↓  ↓  ↓  (движение против часовой)
12 11 10  9  8  7     6  5  4  3  2  1  ← МОЙ дом (финиш)
[Я внизу]

Для игрока BLACK (видит доску повернутой на 180°):

[Соперник вверху]
24 23 22 21 20 19    18 17 16 15 14 13 ← МОИ шашки (старт, логически пункт 12)
                      ↓  ↓  ↓  ↓  ↓  ↓  (движение против часовой)
 1  2  3  4  5  6     7  8  9 10 11 12 ← МОЙ дом (финиш, логически пункты 13-18)
[Я внизу]

Результат: Оба игрока видят свои шашки стартующими в верхнем правом углу и двигающимися против часовой стрелки к своему дому в нижнем правом углу.

3. Игровая механика

3.1 Состояния игры

enum GameState {
  WAITING_FOR_PLAYER = 'waiting_for_player',
  PLAYING = 'playing',
  FINISHED = 'finished'
}

3.2 Броски костей

interface DiceRoll {
  dice1: number;  // 1-6
  dice2: number;  // 1-6
}

// При дубле (dice1 === dice2) игрок получает 4 хода с этим значением.
// Например, при броске 3-3, игрок может сделать четыре хода на 3.

4. Логика ходов и валидации (Сервер)

4.1 Архитектура валидации

В отличие от простой пошаговой валидации, сервер использует упреждающую генерацию всех возможных последовательностей ходов.

  1. После броска костей сервер рекурсивно вычисляет все возможные полные последовательности ходов, которые игрок может совершить.
  2. Эти последовательности учитывают все правила игры:
    • Запрет на ход в занятый противником пункт.
    • Обязательное использование максимального количества костей.
    • Приоритет большего хода: Если из двух костей можно сыграть только одной (но не обеими), игрок обязан использовать большую кость, если ею возможен ход.
    • Завершение игры: Алгоритм корректно обрабатывает выигрышный ход (сброс последней фишки), даже если у игрока остались неиспользованные кости.
    • Правила снятия с головы в начале игры.
    • Правила снятия шашек с доски (bearing off).
  3. Сгенерированный список валидных последовательностей ходов сохраняется в state.possibleMoves.
  4. Клиент получает этот список и использует его для отображения подсвеченных ходов, не производя собственной валидации.

4.2 Правила снятия с головы в начале игры

Голова — стартовая позиция, где игрок начинает игру со всеми 15 шашками:

  • Белые: пункт 24
  • Черные: пункт 12

Основное правило: По умолчанию с головы можно снять только одну шашку за ход.

Исключение для специальных дублей: При выпадении дубля 3-3, 4-4 или 6-6 в первом ходу каждого игрока (turnCount ≤ 2) разрешается снять две шашки с головы вместо одной.

Важные детали:

  • Правило применяется к первому ходу каждого игрока: turnCount=1 (белые) и turnCount=2 (черные)
  • После снятия двух шашек с головы при специальном дубле, оставшиеся ходы (если они есть) делаются этими двумя фишками
  • Критический сценарий: При дубле 6-6 в первом ходу:
    • Снимаются 2 фишки: 24→18, 24→18 (для белых) или 12→6, 12→6 (для черных)
    • Оставшиеся 2 хода невозможны (позиция 12/24 заблокирована противником)
    • Ход валиден и завершается после 2 ходов (правило "2 с головы" физически ограничивает возможности)
    • Правило "использовать максимум костей" соблюдено: 2 хода — это максимум возможных в данной ситуации

Реализация: Счетчик turnMovesFromHead отслеживает количество фишек, снятых с головы в текущем ходу. Лимит проверяется в findAllSingleMoves и учитывается в рекурсивной генерации последовательностей.

4.3 Обработка ситуации "Нет ходов"

Если после броска костей сервер определяет, что список возможных последовательностей ходов пуст (игрок заблокирован), ход автоматически завершается, и право хода передается сопернику. Это предотвращает "зависание" игры.

4.4 Правила снятия шашек (Bearing Off)

  • Снятие разрешено только тогда, когда все 15 шашек игрока находятся в его доме.

  • Для белых (дом 1-6):

    • Можно снимать шашки с пунктов 1-6.
    • Белые двигаются по убыванию (24 → 1), поэтому снимают с младших номеров.
    • Если значение кости точно соответствует номеру пункта, шашка снимается (например, с пункта 5 костью 5).
    • Если значение кости больше номера самого дальнего пункта с шашкой, можно снять шашку с этого самого дальнего пункта.
  • Для черных (дом 13-18):

    • Можно снимать шашки с пунктов 13-18.
    • Черные двигаются к пункту 13 (маршрут: ...→15→14→13), поэтому снимают с младших номеров в диапазоне 13-18.
    • Если значение кости точно соответствует расстоянию от пункта до выхода (пункт 13 = выход), шашка снимается.
    • Пример: шашка на пункте 15, кость 2 → можно снять (15-2=13, выход).
    • Если значение кости больше расстояния от самого дальнего пункта с шашкой до выхода, можно снять шашку с этого самого дальнего пункта.
  • Сервер генерирует ходы со специальным пунктом назначения 'off' (например, from: 5, to: 'off' для белых или from: 15, to: 'off' для черных), которые клиент затем использует для отображения возможности снятия шашки.

4.5 Правило блокировки 6 пунктов подряд

Условие применения: Если у игрока нет ни одной фишки в его доме, то соперник не может создать блок из 6 пунктов подряд на маршруте этого игрока.

Определение блока:

  • Блок = 6 пунктов подряд (по маршруту игрока), на каждом из которых стоит хотя бы одна фишка соперника
  • Проверяется по полному маршруту игрока от старта до дома

Маршруты для проверки:

  • Белые (дом 1-6): маршрут 24 → 23 → 22 → ... → 2 → 1
  • Черные (дом 13-18): маршрут 12 → 11 → 10 → 9 → 8 → 7 → 6 → 5 → 4 → 3 → 2 → 1 → 24 → 23 → 22 → 21 → 20 → 19 → 18 → 17 → 16 → 15 → 14 → 13

Логика проверки:

  1. После генерации всех возможных ходов (possibleMoves)
  2. Для каждого хода в последовательности:
    • Виртуально применяем ход к доске
    • Проверяем: есть ли у противника хотя бы одна фишка в его доме?
    • Если НЕТ: проверяем, создает ли этот ход блок из 6 пунктов подряд на маршруте противника
    • Если создает блок: исключаем этот ход из possibleMoves

Пример:

BLACK (старт: пункт 12, дом: 13-18) еще не имеет ни одной фишки в доме.
WHITE занимает пункты: 10, 9, 8, 7, 6, 5 (6 пунктов подряд на маршруте BLACK)
→ Это блок! Ход WHITE, создающий эту ситуацию, должен быть исключен из possibleMoves.

Важно: Это правило защищает игрока от полной блокировки маршрута до тех пор, пока он не введет хотя бы одну фишку в свой дом.

5. Структуры данных (Colyseus Schema)

Структуры данных реализованы с помощью @colyseus/schema для автоматической синхронизации с клиентом.

import { Schema, type, MapSchema, ArraySchema } from '@colyseus/schema';

// Описывает шашки на одном пункте (треугольнике)
export class Point extends Schema {
  @type('string') player: string; // 'white' или 'black'
  @type('number') checkers: number; // Количество шашек
}

// Основная схема состояния игры
export class GameState extends Schema {
  @type({ map: Point })
  board = new MapSchema<Point>();

  @type({ map: 'number' })
  bar = new MapSchema<number>({ white: 0, black: 0 });

  @type({ map: 'number' })
  off = new MapSchema<number>({ white: 0, black: 0 });

  @type('string')
  currentPlayer: string;

  @type(['number'])
  dice = new ArraySchema<number>();

  @type('string')
  winner: string | null = null;

  // Массив ВСЕХ возможных последовательностей ходов.
  // Каждый элемент - строка, представляющая полную последовательность.
  // Пример: ["24-20,20-18", "24-22,22-18"]
  @type(['string'])
  possibleMoves = new ArraySchema<string>();

  @type({ map: 'string' })
  players = new MapSchema<string>(); // e.g., { "sessionId1": "white" }
  
  // ... другие поля, такие как playerProfiles, turnCount и т.д.
}

6. API (Colyseus)

6.1 Команды от клиента

Сервер слушает следующие сообщения от клиента:

onMessage("rollDice", (client) => { ... })

  • Инициирует бросок костей для текущего игрока.
  • Запускает на сервере генерацию possibleMoves.
  • Если ходов нет, автоматически передает ход сопернику.

onMessage("move", (client, message: { from: number | 'bar'; to: number | 'off' }) => { ... })

  • Получает один шаг хода от клиента (например, с 24 на 20).
  • Сервер находит этот шаг в одной из валидных последовательностей в possibleMoves.
  • Применяет ход, обновляет состояние (board, dice).
  • Пересчитывает possibleMoves на основе оставшихся костей.
  • Если после этого шага больше нет возможных ходов, завершает ход.

6.2 События для клиента

Основное состояние синхронизируется через room.onStateChange. Однако, для ключевых событий конца игры сервер отправляет клиентам прямые сообщения:

onMessage("game_over", (message: { result: 'win' | 'lose', message: string }) => { ... })

  • Отправляется обоим игрокам по нормальному завершению игры (когда один из игроков сбросил все фишки).
  • result: 'win' для победителя, 'lose' для проигравшего.
  • message: Текстовое сообщение с результатом (например, WIN 100 TON. или LOST 100 TON.).

onMessage("opponent_left", (message: { message: string }) => { ... })

  • Отправляется оставшемуся игроку, если его соперник покинул игру.

Помимо этого, клиент, как и прежде, реагирует на изменения в GameState через onStateChange.

7. Алгоритм генерации ходов (обзор)

Сервер использует рекурсивный алгоритм для поиска всех валидных последовательностей ходов.

  1. calculatePossibleMoves(): Основная функция, которая запускает процесс и применяет правила принуждения (использовать все кости, играть больший ход).
  2. findMoveSequences(board, dice, player, ...): Рекурсивная функция.
    • В начале рекурсии проверяет, не убраны ли уже все фишки игрока с доски. Если да, то это выигрышная позиция, и она считается валидным концом последовательности ходов.
    • Для каждой кости она находит все возможные одиночные ходы (findAllSingleMoves).
    • Для каждого найденного хода она:
      • Создает виртуальную доску с примененным ходом.
      • Рекурсивно вызывает сама себя с оставшимися костями.
      • Собирает и возвращает полные последовательности ходов.
  3. findAllSingleMoves(...): Находит все возможные одиночные ходы для одной кости с текущей позиции, включая ходы с бара и снятие шашек (bearing off). Эта функция также содержит логику для "правила головы".

Этот подход гарантирует, что клиент всегда получает полный и точный список всех легальных действий, что исключает десинхронизацию и зависание игры.