Без проблем, давай разберем это подробно. Разделение ходов (turns) и событий (game_events) — это классический паттерн проектирования, который называется Event Sourcing (генерация событий).

Почему мы вообще вынесли события в отдельную таблицу?

Представь, что мы попытались бы “запихнуть” события внутрь таблицы ходов (turns). Ход — это строгая структура: была позиция, выпали кубики, сыграны ходы, стала новая позиция. События же (удвоения, ничьи, сдачи) — это редкие и непредсказуемые вещи.

  • Игрок может сдаться до броска кубиков.
  • Игрок может предложить удвоение, а второй — думать над ним минуту.
  • В 95% ходов вообще не происходит никаких событий. Если добавить колонки для них в таблицу turns, они будут пустыми (NULL) в 95% случаев. Это засоряет базу.

Поэтому turns хранит только “физику” на доске, а game_events хранит “социально-игровые” действия.


Как устроена таблица game_events

Вот ключевые колонки этой таблицы и их назначение:

  1. game_id — связывает событие с конкретной партией.
  2. turn_number(Ключевое поле для связи!) Указывает, к какому по счету ходу привязано это событие. Например, если игрок предложил удвоение прямо перед своим 5-м ходом, тут будет цифра 5.
  3. sequence_number — Порядковый номер события внутри партии. Бывает, что перед одним ходом происходит сразу несколько событий (Белые предлагают удвоение Черные принимают Черные предлагают ничью). Это поле позволяет выстроить их в строгом хронологическом порядке.
  4. event_type — Строка с типом события (тот самый ENUM). Например: 'double_offer', 'double_accept', 'timeout', 'resignation'.
  5. actor_color — Кто инициировал событие (‘w’ или ‘b’).
  6. payload (JSONB) — Магическое поле для деталей. Поскольку каждое событие уникально, мы не создаем под каждое свои колонки, а кладем детали в JSON.
    • Для 'double_offer' тут будет {"new_bank": 1200}.
    • Для 'timeout' тут будет {"time_left": 0}.

Пример: Как это выглядит в жизни

Представь партию в режиме удвоения (X2). Наступает 5-й ход (очередь белых). Белые решают, что у них отличная позиция, и предлагают удвоить ставку. Черные принимают. Белые кидают кубики и ходят.

В базе данных это запишется так:

Таблица game_events (События):

sequence_numberturn_numberevent_typeactor_colorpayload
15double_offerw{"proposed_bank": 1200}
25double_acceptb{"accepted": true}

Таблица turns (Ходы):

turn_numberactive_colordiceplayed_movesbank_after
5wBNP["b1c3"]1200

Как мы (или скрипт) это прочитаем потом?

Если тебе или фронтенду понадобится воспроизвести партию шаг за шагом (Replay), ты делаешь один запрос, который объединяет обе таблицы и сортирует их по turn_number.

Скрипт пойдет по порядку:

  1. Читает turn_number = 1, 2, 3, 4 из turns и двигает фигуры.
  2. Доходит до turn_number = 5. Видит, что в game_events есть записи для этого хода.
  3. Сначала показывает анимацию: “Белые предлагают удвоение”.
  4. Затем: “Черные приняли”.
  5. Затем берет данные из turns для 5-го хода: бросает кубики BNP и двигает коня b1c3.

Итог

Таблица game_events работает как журнал (лог) всего, что происходит “вокруг” доски. Поле turn_number работает как якорь, который намертво привязывает запись из журнала к конкретному моменту на доске.

Стала ли связь между физическими ходами и событиями более понятной?