squad-proto/engine/level_gen.h

156 lines
9.6 KiB
C
Raw Normal View History

Tile2D: движок отдельным продуктом, игра — проектом в нём (M11-M20) Десять вех одним состоянием: разделять их пофайлово нечем, потому что src/game.cpp менялся почти в каждой. Собирается начисто без предупреждений, все проверки проходят. ДВИЖОК (engine/, цель tile2d) Каталог свой, и решение «это движок» принимается тем, куда положен файл. Компилятор нашёл три зависимости движка от игры, и все сняты: размеры кадра и карты уехали в engine/config.h, потолок населения этажа стал своим (MAX_LEVEL_SPAWNS против игровой MAX_TARGETS), запасная точка старта стала центром карты вместо позиции отряда. engine/projection.* единственное место, где движок знает про изометрию; видов сразу два — абстракция с одной реализацией ничем не проверена и обычно оказывается неправильной engine/project.* манифест проекта: пути, вид, кривая глубины engine/app.* окно, кадр, fixed timestep — вместо двух копий цикла engine/tilemap_draw движок научился показывать свою карту РЕДАКТОР (editor/, цель tile2d_editor) Открывает любой проект и ни одной игры по имени не знает. --tool одиннадцать безоконных команд: комнаты, каталог, кривая, сборка этажа, ASCII-карта, проверка набора. Правку делают файлами, последствия читают командой (docs/19-agent.md) терминал снизу: те же инструменты, git, сборка и запросы к claude/codex, открытый проект уходит потомкам через TILE2D_PROJECT ПРИМЕР (samples/walk, цель tile2d_walk) Второй потребитель движка. Игра и редактор самостоятельность движка проверить не могут: редактор мира не рисует, а игра — то место, куда общий код и уезжает незаметно. ИГРА (src/) Экстракшен: вес и два порога переноски, конечный боезапас, расходники применяются сами, общая сумка отряда, трата опыта на улучшения. Каждое из правил проверяется прогоном, а не глазами, и проверки поймали: вес дважды не связывал ничего (мерил по бинтам прогона, а не по добыче); боец с клинком не тратил найденные патроны при запасном стволе в сумке; массив виджетов экрана переполнился и молча съел хвост схрона; проверка улучшений сама ломала забег, вызывая NewRun посреди спуска. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 01:07:19 +07:00
#pragma once
// -----------------------------------------------------------------------------
// Сборка ЭТАЖА из комнат. Детерминирована по сиду и глубине, и только по ним:
// один и тот же набор комнат плюс тот же сид и тот же этаж дают побайтово ту же
// карту и то же население.
//
// Где живёт случайность. В симуляции её нет и не будет (docs/02-architecture.md,
// «Детерминизм»): RNG крутится ЗДЕСЬ, один раз, при сборке этажа, и к моменту
// первого Game::Step всё уже разложено. Поэтому прогон остаётся воспроизводимым,
// а `--accept` вообще не проходит по этому коду — приёмка меряется на прежней
// захардкоженной планировке.
//
// Раскладка. Карта 48x48 делится на сетку LEVEL_GRID x LEVEL_GRID слотов по
// ROOM_SLOT тайлов. В каждый слот ставится комната (не больше ROOM_MAX_*), она
// центрируется, и вокруг неё остаётся камень. Слоты сшиваются коридорами: сперва
// остовное дерево — оно гарантирует, что дойти можно до каждой комнаты, — потом
// несколько лишних связей, чтобы этаж не был деревом-кишкой.
//
// Что делает глубина. Всё, кроме планировки:
// * из набора отбираются комнаты, разрешённые на этом этаже;
// * из таблиц спавна отбираются сущности, разрешённые на этом этаже;
// * население тратит БЮДЖЕТ ОПАСНОСТИ, а не просто выпадает по шансу;
// * остаток бюджета доливается тварями вне комнатных точек — иначе глубина
// упиралась бы в число точек, нарисованных в комнатах, и переставала расти;
// * лут раскладывается тиром этажа.
// Сама кривая — в engine/progression.h, здесь только её применение.
// -----------------------------------------------------------------------------
#include <cstdint>
#include <string>
#include <vector>
#include "engine/math.h"
#include "engine/catalog.h"
#include "engine/progression.h"
#include "engine/room.h"
#include "engine/tilemap.h"
constexpr int LEVEL_GRID = MAP_W / ROOM_SLOT;
constexpr int LEVEL_SLOTS = LEVEL_GRID * LEVEL_GRID;
// Потолок на лежащие предметы. Не «на всякий случай»: каждый предмет —
// сущность ECS и иконка в списке отрисовки, а этаж, засыпанный лутом,
// перестаёт быть находкой.
constexpr int MAX_LEVEL_LOOT = 24;
// Потолок на населённость этажа. Раньше здесь стояла игровая константа
// MAX_TARGETS из src/tuning.h — то есть генератор уровней знал, сколько битов
// в маске удара клинком. Теперь у движка своё число, а игра проверяет, что оно
// влезает в её пределы (static_assert в src/tuning.h).
//
// Это не формальность: бюджет опасности на глубине неизбежно перерастает любое
// такое число, и лишнее не теряется молча — оно уходит в GeneratedLevel::
// spawnsDropped и в отчёт --check.
constexpr int MAX_LEVEL_SPAWNS = 64;
static_assert(MAP_W == MAP_H, "генератор рассчитан на квадратную карту");
static_assert(MAP_W % ROOM_SLOT == 0, "карта обязана делиться на слоты нацело");
// -----------------------------------------------------------------------------
// Xorshift32. Свой, а не <random>: стандартные распределения не обязаны давать
// одинаковую последовательность на разных реализациях библиотеки, а сид этажа
// обязан значить одно и то же везде.
// -----------------------------------------------------------------------------
struct LevelRng
{
uint32_t state;
explicit LevelRng(uint32_t seed) : state(seed ? seed : 0x9E3779B9u) {}
uint32_t Next()
{
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state;
}
// [0, 1). Двадцать четыре бита: больше во float всё равно не влезает.
float Float01() { return float(Next() >> 8) * (1.0f / 16777216.0f); }
// [0, n). n <= 0 — ноль: вызывающему не приходится проверять пустой список.
int Below(int n) { return (n > 0) ? int(Next() % uint32_t(n)) : 0; }
};
// Одна сущность, которую генератор выкатил в мир. id — строка каталога:
// движок не знает, во что она превратится в игре.
struct LevelSpawn
{
std::string id;
Vec2 pos{};
};
struct LevelSlot
{
int roomIndex = -1; // индекс в RoomLibrary; -1 — слот пуст
int originX = 0; // угол комнаты в тайлах карты
int originY = 0;
};
struct GeneratedLevel
{
uint32_t seed = 0;
FloorSpec floor;
Vec2 squadStart{};
// Спуск на следующий этаж. Самая дальняя от старта ДОСТИЖИМАЯ точка: этаж
// обязан заставить пройти себя, а не открываться в двух шагах от входа.
Vec2 exitPos{};
// Эвакуация: та же точка, откуда отряд вошёл на этаж, — лестница наверх.
// Отдельным полем, а не «это же squadStart», потому что смысл у них разный
// и разойтись они могут: точку входа однажды захочется двигать, а выход
// наверх — нет.
//
// Совпадение входа и выхода — не экономия, а суть напряжения: спуск лежит
// на дальнем конце этажа, и решив уйти, отряд идёт обратно через всё, что
// успел растревожить, — раненый и с полными руками.
Vec2 extractPos{};
std::vector<LevelSpawn> spawns; // твари и обстановка
std::vector<LevelSpawn> loot; // предметы на полу
LevelSlot slots[LEVEL_SLOTS];
int roomsPlaced = 0;
int corridors = 0;
// Сколько опасности реально выпущено на этаж. Не равно бюджету: точек может
// не хватить, и это нормально. Сильно меньше бюджета на глубине — признак,
// что набор комнат перестал успевать за кривой.
float dangerSpent = 0.0f;
int monstersPlaced = 0;
// Сколько тварей долито вне комнатных точек, чтобы выбрать бюджет.
int monstersFilled = 0;
// Сколько точек спавна выпало из-за потолка MAX_LEVEL_SPAWNS. Не ноль — этаж
// населён не так, как задумано, и это обязано быть видно, а не молча съедено.
int spawnsDropped = 0;
// Сколько выпало как недостижимые от точки старта. Не ноль — где-то в наборе
// комнат есть карман, отрезанный от собственной двери: тварь в нём не сможет
// ни дойти до отряда, ни быть убитой, и этаж тихо станет легче.
int spawnsUnreachable = 0;
// Сколько тайлов пола достижимо от старта, и как далеко до спуска (в шагах
// по сетке). Обе цифры — про то, «есть ли на этаже что проходить».
int reachableTiles = 0;
int exitDistance = 0;
const LevelSlot& Slot(int gx, int gy) const { return slots[gy * LEVEL_GRID + gx]; }
};
// Собирает планировку прямо в map (включая слой тем) и население в out.
// false — из набора нельзя собрать этаж; map при этом не трогается.
//
// catalog нужен, потому что глубина и цена сущности живут в нём: без каталога
// генератор не может ни отобрать тварей по этажу, ни посчитать бюджет.
bool GenerateLevel(const RoomLibrary& lib, const EntityCatalog& catalog, const FloorSpec& floor,
uint32_t seed, Tilemap& map, GeneratedLevel& out, std::string* err = nullptr);