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:
@@ -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` проходят
|
||||
|
||||
## Стоп
|
||||
|
||||
Не отдавать бесхозную в слот игрока. Не показывать гостю «Управление» даже чтением.
|
||||
Не менять скорости.
|
||||
@@ -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` и потолок догона воркера. Не урезать рассылку кадров часов. Не трогать
|
||||
сессию и хозяина.
|
||||
@@ -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, страйд тяжёлых систем |
|
||||
@@ -0,0 +1,34 @@
|
||||
# Журнал ревью
|
||||
|
||||
Сводка: [`../reviewed.md`](../reviewed.md). Индекс среза: [`README.md`](README.md).
|
||||
|
||||
## Срез 8. Сессия и темп
|
||||
|
||||
- **Фазы:** 38–40
|
||||
- **Проверен на:** `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` живут в списке меню.
|
||||
Reference in New Issue
Block a user