Десять вех одним состоянием: разделять их пофайлово нечем, потому что
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>
156 lines
9.6 KiB
C++
156 lines
9.6 KiB
C++
#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);
|