Remove off-queue.md and add new design documentation files including README.md, defs.md, near-term.md, projects.md, runtime.md, people.md, staffing.md, and schedule.md to outline the structure and agreements for the game's design phases.

This commit is contained in:
Leonid Pershin
2026-08-20 12:26:13 +03:00
parent 12cc36b4dd
commit a37a5eb82d
101 changed files with 1015 additions and 921 deletions
+46
View File
@@ -0,0 +1,46 @@
# Фаза 38. Сессия
## Зависимости
- [Фаза 1](../01-shell/01-manager-shell.md)
- [Фаза 2](../01-shell/02-school-worker.md)
- [Фаза 28](../06-foundation/28-screen-tests.md)
## Зачем
В клиент не пускает без пароля альфы и имени. Имя запоминается. Сокет открывается только после
сессии. Школы пока общие — хозяин в фазе 39.
## Задачи
- [x] `HSchool:AlphaPassword` в конфиге; пустая строка — процесс не стартует. В `appsettings.json`
заглушка для локальной игры; headless AppHost задаёт тестовый пароль окружением
- [x] `POST /api/session` `{ password, userName }`: неверный пароль — `401` `bad-password`;
кривое имя — `400` `invalid-name` (те же правила, что у школы); имя с живым сокетом —
`409` `name-online`. Успех ставит HttpOnly-куку и при первом разе пишет имя в
`saves/users.json` (занятость без учёта регистра)
- [x] `GET /api/session` — текущее имя или `401`. `DELETE /api/session` снимает куку
- [x] Все игровые HTTP без куки — `401`. Живут без неё: `/health` и три ручки сессии
- [x] WebSocket без куки не шлёт Welcome, закрывается. Hello по-прежнему версия + локаль
- [x] Клиент: шаг пароля, шаг имени; при живой куке оба пропускаются. Сокет — после сессии.
Кнопка выхода на меню
- [x] Хостовые тесты логинятся в общем месте (`ResetAsync` или рядом), не в каждом факте
- [x] `docs/protocol.md` — HTTP сессии в том же коммите. Раскладку сокета и версию не трогать
## Тесты, без которых фаза не закрыта
- [x] Без сессии `GET /api/schools``401`; после `POST /api/session``200`
- [x] Верный пароль и новое имя пишут `users.json`; повтор с тем же именем (другой регистр) —
тот же человек, не вторая запись
- [x] Пока сокет имени жив, второй `POST /api/session` с ним — `409` `name-online`
- [x] Сокет без куки закрывается, Welcome не приходит
- [x] Клиентский тест: без куки виден ввод пароля, не сетка школ
## Критерий готовности
- Открыть клиент, ввести пароль и имя, попасть в меню; обновить страницу — снова меню, без формы
- `dotnet test` и `npm test` / `build` проходят
## Стоп
Не делить школы на мои/чужие. Не менять `MaxSchools`. Не трогать таблицу скоростей.
@@ -0,0 +1,49 @@
# Фаза 39. Хозяин школы
## Зависимости
- [Фаза 38](38-session.md)
- [Фаза 13](../03-staffing/13-management-tab.md)
## Зачем
У школы есть хозяин. Свои слоты — свои, чужие смотрят отдельно и не управляют.
## Задачи
- [x] `SimulationOptions.MaxSchools` — слоты **игрока**, умолчание 2.
`MaxSchoolsTotal` — воркеры процесса, умолчание 16. Подъём с диска режет по Total, не по
слотам игрока
- [x] Сейв несёт `owner`. Нет поля — бесхозная. Формат не бампить
- [x] Create пишет текущего пользователя; отказ: свои слоты кончились (`limit-reached`) или
сервер полон (`server-full`)
- [x] `GET /api/schools`: `maxSchools` — слоты игрока, `maxSchoolsTotal`, `schools` — свои,
`others` — чужие и бесхозные (`owner` имя или `null`). Карточка школы несёт `mine`
- [x] Welcome.`MaxSchools` — слоты игрока (байт тот же, смысл новый)
- [x] Мутации чужой — HTTP `403` `not-owner`. Бесхозную может удалить любой залогиненный.
`OpenSchool` — всем с сессией. Пауза / скорость / пропуск от гостя до работника не доходят.
**Исключение:** `POST .../portrait` — гость генерирует теми же пресетами школы; это не управление
- [x] Меню: блок «Мои» и блок «Чужие». У бесхозной в чужих — удалить, у чужой с хозяином — нет
- [x] Внутри чужой школы нет вкладки «Управление» и нет кнопок часов (пауза, скорость, пропуск).
Карта и люди остаются
- [x] `docs/protocol.md` — списки и коды ошибок. Версию сокета не бампить
## Тесты, без которых фаза не закрыта
- [x] Два пользователя: у каждого по две школы, третья своего — `limit-reached`; чужая в
`others`, не в `schools`
- [x] Восьмая школа на сервере с `MaxSchoolsTotal` = 7 — `server-full`, даже если у игрока слот есть
- [x] Гость: `POST` найма — `403`; `SetRunning` не меняет `running` хозяина
- [x] Сейв без `owner` поднимается, лежит в `others`, удаляется вторым пользователем
- [x] Клиентский тест: чужая карточка без кнопки удаления; в чужой школе нет «Управление»
## Критерий готовности
- Два браузера, два имени: каждый видит свою сетку и чужие отдельным блоком; в чужой школе
часы чужие не нажимаются
- `dotnet test` и `npm test` / `build` проходят
## Стоп
Не отдавать бесхозную в слот игрока. Не показывать гостю «Управление» даже чтением.
Не менять скорости.
+45
View File
@@ -0,0 +1,45 @@
# Фаза 40. Темп часов
## Зависимости
- [Фаза 2](../01-shell/02-school-worker.md)
- [Фаза 20](../05-ai/20-needs-actions.md)
## Зачем
×1 можно смотреть. Высокие скорости — ×5 и ×10, без лишней работы ходьбы и нужд на каждом
импульсе календаря.
## Задачи
- [x] `GameMinutesPerRealSecond` по умолчанию 1 (опции, `appsettings.json`, `GET /api/schools`)
- [x] `ClockSpeed.Multipliers`: `0.5, 1, 2, 5, 10`. Зеркало в `protocol.ts`. Подписи кнопок:
×½ ×1 ×2 ×5 ×10. Стартовый индекс по-прежнему 1
- [x] Воркер будит мир 20 раз в секунду на любой скорости. `GameClock.Advance`, граница лога
6:00, годовой набор, пул соискателей, погода — каждый импульс
- [x] На индексах ×5 и ×10 ходьба, действия, решения, нужды, тепло, обучение, износ — раз в
4 и раз в 2 импульса соответственно, с накопленными минутами (квант — 1 игровая минута).
На ×½ ×1 ×2 — каждый импульс, как сейчас
- [x] Не прыгать календарём одним жирным `Tick` вместо страйда
- [x] Комментарий меню про «минута за 12 секунд» привести к новой базе
- [x] `docs/protocol.md` — таблица множителей. Раскладку и версию сокета не трогать
## Тесты, без которых фаза не закрыта
- [x] 20 шагов по 1/20 с на ×1 сдвигают календарь на 1 игровую минуту, не на 5
- [x] Индексы 3 и 4 за одну реальную секунду дают 5 и 10 игровых минут
- [x] На ×10 за 20 импульсов ходьба/нужды вызваны 10 раз, календарь — 20; накопленный аргумент
минут на тяжёлый вызов — 1
- [x] Невалидный индекс 5 по-прежнему игнорируется
- [x] Клиентский тест: пять кнопок ×½ ×1 ×2 ×5 ×10
## Критерий готовности
- На ×1 секунда реального времени = минута в школе; ×10 не кладёт воркер, люди не телепортируются
на десять минут за кадр
- `dotnet test` и `npm test` / `build` проходят
## Стоп
Не менять `TickRate` и потолок догона воркера. Не урезать рассылку кадров часов. Не трогать
сессию и хозяина.
+30
View File
@@ -0,0 +1,30 @@
Индекс срезов: [`../README.md`](../README.md).
## Срез 8. Сессия и темп
Дизайн: [`../design/session.md`](../../design/08-session/session.md).
Несколько браузеров на одном сервере: общий пароль альфы, имя игрока, свои школы и чужие
отдельным списком. Часы становятся смотрибельными — минута за секунду, на высоких скоростях
тяжёлые системы тикают реже. Срез можно остановить после этапа B: мультиплеер жив, даже если
темп ещё старый.
**Этап A — кто вошёл.** Пароль, имя, кука.
| Фаза | Статус | Зачем |
| --- | --- | --- |
| [38. Сессия](38-session.md) | ✅ | Пароль альфы, имя, кука, сокет после входа |
**Этап B — чьи школы.** Хозяин, слоты, гость только смотрит.
| Фаза | Статус | Зачем |
| --- | --- | --- |
| [39. Хозяин школы](39-school-owners.md) | ✅ | `owner`, 2 слота на игрока, мои / чужие, гость без управления |
39 стоит на 38.
**Этап C — темп.** Не зависит от A/B, можно вести параллельно с 38.
| Фаза | Статус | Зачем |
| --- | --- | --- |
| [40. Темп часов](40-clock-tempo.md) | ✅ | ×1 = минута/с, кнопки ×½ ×1 ×2 ×5 ×10, страйд тяжёлых систем |
+34
View File
@@ -0,0 +1,34 @@
# Журнал ревью
Сводка: [`../reviewed.md`](../reviewed.md). Индекс среза: [`README.md`](README.md).
## Срез 8. Сессия и темп
- **Фазы:** 3840
- **Проверен на:** `1c3ae9a`, 2026-08-20
- **Пути:** `src/HSchool.Server/{Session,Api/SessionEndpoints.cs,Api/SchoolAccess.cs,Game/SchoolOwnership.cs,Game/UserStore.cs,Net/ClientRegistry.cs,Net/GameSocketHandler.cs,Game/GameLoopService.cs}`, `src/HSchool.Simulation/{ClockSpeed.cs,GameClock.cs,School.cs,SimulationOptions.cs}`, `src/HSchool.Client/src/{ui/sessionGate.ts,ui/mainMenu.ts,ui/gameScreen.ts,ui/schoolCard.ts,net/protocol.ts,net/api.ts,main.ts}`, `docs/protocol.md`, `../../design/08-session/session.md`, `tests/HSchool.AppHost.Tests/{SessionApiTests.cs,SchoolOwnerApiTests.cs,GameSocketTests.cs}`, `tests/HSchool.Simulation.Tests/{ClockTempoTests.cs,GameClockTests.cs}`, `src/HSchool.Client/src/ui/{sessionGate,gameScreen,schoolCard,mainMenu}.test.ts`
- **Итог:** обещания 38–40 на месте; дописано 4 теста; вторая вкладка с той же кукой больше не получает Welcome. Срез 9 (44/45/47) в работе — не ревьюился
Что подтверждено:
- **38:** `HSchool:AlphaPassword` (пустой — `ValidateOnStart`); `POST/GET/DELETE /api/session`; кука HttpOnly, `SameSite=Lax`, `Path=/`, `Max-Age` из `SessionCookieDays` (14); `users.json` каноническое имя; повтор другим регистром — та же запись; игровой HTTP без куки — `401`; `/health` и три ручки сессии — без неё; сокет без куки — close `PolicyViolation`, без Welcome; Hello по-прежнему версия + локаль; клиент — сначала сессия, потом сокет; хостовые тесты логинятся в `ResetAsync`/`LoginAsync`. Версия протокола не бампилась (v8).
- **39:** `MaxSchools` = 2 на игрока, `MaxSchoolsTotal` = 16 (в AppHost-тестах Total = 7); create пишет `owner`; `limit-reached` в коде — `school-limit-reached` (как в `protocol.md`); `server-full`; `GET /api/schools``schools` / `others`, `mine`, `owner` или `null`; Welcome.`MaxSchools` — слоты игрока; гость: найм `403` `not-owner`, `SetRunning` не меняет `running`; бесхозная в `others`, удаляется вторым; меню «Мои»/«Чужие»; в чужой школе нет «Управление» и кнопок часов. Портретный POST без `RequireManage`. Формат сейва не бампился.
- **40:** база 1 игровая минута/с; таблица `0.5, 1, 2, 5, 10` в `ClockSpeed`, `protocol.ts` и `protocol.md`; 20 импульсов ×1 = 1 минута, индексы 3/4 = 5 и 10 минут; на ×10 тяжёлые 10 раз с квантом 1; на ×5 — 5 раз; индекс 5 игнорируется; воркер по-прежнему 20 Гц и потолок догона 5; пять кнопок на экране. `HSchool.Simulation` без ASP.NET.
Дописано:
- `SessionApiTests.Login_SetsHttpOnlyLaxSessionCookie`
- `SessionApiTests.SecondWebSocket_ForAnOnlineName_ClosesWithoutWelcome`
- `ClockTempoTests.AtX5_TwentyImpulses_RunHeavyFiveTimesAndAdvanceFiveGameMinutes` (и календарь +1 на ×1 в `AtX1_…`)
- `mainMenu.test.ts` — чужая карточка без удаления, бесхозная с удалением
Исправлено:
- Второй WebSocket того же имени принимался и слал Welcome: `POST` проверял «онлайн», upgrade — нет. Две вкладки с живой кукой обходили `409` `name-online`. Теперь `ClientRegistry.TryClaimUserName` под тем же замком, второй сокет закрывается `PolicyViolation` без Welcome.
Открыто:
- `POST /api/session` с другим именем при живой куке перезаписывает сессию без `DELETE`. Дизайн: «Без выхода второе имя с той же куки не взять». Кода ошибки нет — новый HTTP-код молча не вводился.
- `AddDataProtection()` без ключей рядом с `saves/`. Рестарт того же профиля Windows куки, скорее всего, не сбрасывает; новый контейнер — может.
- Текст фазы 39 пишет `limit-reached`; живой код и `protocol.md``school-limit-reached` (так было до среза). Не менялось.
- Бесхозная карточка показывает «Хозяин: —», не голый прочерк. `GET /api/schools/{id}` из дизайна нет: `mine`/`owner` живут в списке меню.