Без проблем, давай разберем это подробно. Разделение ходов (turns) и событий (game_events) — это классический паттерн проектирования, который называется Event Sourcing (генерация событий).
Почему мы вообще вынесли события в отдельную таблицу?
Представь, что мы попытались бы “запихнуть” события внутрь таблицы ходов (turns). Ход — это строгая структура: была позиция, выпали кубики, сыграны ходы, стала новая позиция.
События же (удвоения, ничьи, сдачи) — это редкие и непредсказуемые вещи.
- Игрок может сдаться до броска кубиков.
- Игрок может предложить удвоение, а второй — думать над ним минуту.
- В 95% ходов вообще не происходит никаких событий. Если добавить колонки для них в таблицу
turns, они будут пустыми (NULL) в 95% случаев. Это засоряет базу.
Поэтому turns хранит только “физику” на доске, а game_events хранит “социально-игровые” действия.
Как устроена таблица game_events
Вот ключевые колонки этой таблицы и их назначение:
game_id— связывает событие с конкретной партией.turn_number— (Ключевое поле для связи!) Указывает, к какому по счету ходу привязано это событие. Например, если игрок предложил удвоение прямо перед своим 5-м ходом, тут будет цифра 5.sequence_number— Порядковый номер события внутри партии. Бывает, что перед одним ходом происходит сразу несколько событий (Белые предлагают удвоение → Черные принимают → Черные предлагают ничью). Это поле позволяет выстроить их в строгом хронологическом порядке.event_type— Строка с типом события (тот самый ENUM). Например:'double_offer','double_accept','timeout','resignation'.actor_color— Кто инициировал событие (‘w’ или ‘b’).payload(JSONB) — Магическое поле для деталей. Поскольку каждое событие уникально, мы не создаем под каждое свои колонки, а кладем детали в JSON.- Для
'double_offer'тут будет{"new_bank": 1200}. - Для
'timeout'тут будет{"time_left": 0}.
- Для
Пример: Как это выглядит в жизни
Представь партию в режиме удвоения (X2). Наступает 5-й ход (очередь белых). Белые решают, что у них отличная позиция, и предлагают удвоить ставку. Черные принимают. Белые кидают кубики и ходят.
В базе данных это запишется так:
Таблица game_events (События):
| sequence_number | turn_number | event_type | actor_color | payload |
|---|---|---|---|---|
| 1 | 5 | double_offer | w | {"proposed_bank": 1200} |
| 2 | 5 | double_accept | b | {"accepted": true} |
Таблица turns (Ходы):
| turn_number | active_color | dice | played_moves | bank_after |
|---|---|---|---|---|
| 5 | w | BNP | ["b1c3"] | 1200 |
Как мы (или скрипт) это прочитаем потом?
Если тебе или фронтенду понадобится воспроизвести партию шаг за шагом (Replay), ты делаешь один запрос, который объединяет обе таблицы и сортирует их по turn_number.
Скрипт пойдет по порядку:
- Читает
turn_number = 1,2,3,4изturnsи двигает фигуры. - Доходит до
turn_number = 5. Видит, что вgame_eventsесть записи для этого хода. - Сначала показывает анимацию: “Белые предлагают удвоение”.
- Затем: “Черные приняли”.
- Затем берет данные из
turnsдля 5-го хода: бросает кубикиBNPи двигает коняb1c3.
Итог
Таблица game_events работает как журнал (лог) всего, что происходит “вокруг” доски. Поле turn_number работает как якорь, который намертво привязывает запись из журнала к конкретному моменту на доске.
Стала ли связь между физическими ходами и событиями более понятной?