Files
h-school/AGENTS.md
T
Leonid Pershin 4137400621
ci / server (push) Failing after 3m39s
ci / client (push) Successful in 14s
Enhance AI and simulation components with presence management and routing capabilities
- Introduced the `HSchool.Ai` project, responsible for routing, day plans, and presence management.
- Updated the `HSchool.Simulation` project to integrate with the new AI functionalities, improving decision-making and presence tracking.
- Added `travelMinutes` to room and territory definitions, ensuring accurate movement calculations within the simulation.
- Enhanced the `School` class to manage presence and implement empty-time skipping functionality.
- Updated documentation to reflect the new AI features and their impact on school simulation.
- Added tests for presence management and routing to ensure robust functionality and reliability.
2026-08-19 16:33:52 +03:00

173 lines
9.9 KiB
Markdown

# Working agreements
Read this before changing anything. It is written for coding agents, and it is just as valid for
humans. [`docs/architecture.md`](docs/architecture.md) explains *why* the pieces are shaped this
way; this file is *how to work in them*.
## Orientation
| I want to change… | Go to |
| --- | --- |
| schools, the game clock, game rules | `src/HSchool.Simulation` |
| defs, JSONC catalog, map validation | `src/HSchool.Content` |
| skills, traits, body, needs, name sets | `src/HSchool.Content` |
| people generation, families, roster records, yearly intake | `src/HSchool.People` |
| timetable planning | `src/HSchool.Schedule` |
| routes, day plans, who comes today | `src/HSchool.Ai` |
| people in a school's World, need decay, walking | `src/HSchool.Simulation` |
| the menu API (list, create, delete, mods, catalog) and the people list/card | `src/HSchool.Server/Api` **and** `docs/protocol.md` |
| what the socket carries | `src/HSchool.Protocol` **and** `src/HSchool.Client/src/net/protocol.ts` **and** `docs/protocol.md` |
| connection handling, workers, saves | `src/HSchool.Server` |
| what runs locally | `src/HSchool.AppHost/AppHost.cs` |
| screens, dialogs, formatting, UI language | `src/HSchool.Client/src` |
## Commands
```bash
dotnet build h-school.sln
```
```bash
dotnet test
```
```bash
npm --prefix src/HSchool.Client test
```
```bash
npm --prefix src/HSchool.Client run build
```
```bash
dotnet run --project src/HSchool.AppHost
```
`run-aspire.cmd` is the same command for Windows users who want a double-clickable entry point —
keep the two in sync if the AppHost path ever moves.
`dotnet run --project src/HSchool.AppHost` starts the server *and* the Vite dev server and opens
the Aspire dashboard. The Vite port is assigned per run (`npm run dev -- --port <random>`), so read
the client URL off the dashboard instead of assuming 5173.
Do not start a dev server with a bare `npm run dev` when you meant to run the whole app — the
client only finds the backend through the Aspire-injected `SERVER_HTTP` environment variable, or
the `localhost:5180` fallback that matches the server's own launch profile.
## Invariants
These are the rules that keep the base coherent. Breaking one is a design decision, not a detail —
say so explicitly in the change description.
1. **The server is authoritative.** The client sends intents and draws what it is told. No game
logic in `src/HSchool.Client` — not even a local clock that ticks between frames.
2. **The protocol lives in three places at once.** `ProtocolCodec.cs`, `protocol.ts` and
`docs/protocol.md` change in the same commit. A layout change bumps
`ProtocolConstants.Version` / `PROTOCOL_VERSION`. Tests on both sides assert byte offsets —
if one of them has to change, so do the other two.
3. **Only a school's worker thread touches that `School` or its `World`.** The supervisor
owns the mailbox table and never calls into a world. Create/delete/name suggestions go
through `GameCommandQueue`; open/close/running/speed go to that school's mailbox; menu
reads use the snapshots workers publish; everything outbound goes through the per-client
outbox. No locks around a school, no `Task.Run` into it.
4. **The simulation knows nothing about the network.** `HSchool.Simulation` must not reference
ASP.NET Core, sockets or logging infrastructure. It stays testable without a host.
5. **Fixed timestep.** The clock advances by `SimulationOptions.FixedDeltaTime`, never by
wall-clock deltas and never from `DateTime.Now`. Same tick count, same date.
6. **Everything from the wire is untrusted.** Validate lengths and ranges before anything reaches
the simulation — names, dates and speed indexes all arrive from a browser.
7. **One intent per message.** A frame that also resends a neighbouring field overwrites it with a
stale client copy; that is why running and speed are separate messages.
## Conventions
**C#**
- File-scoped namespaces, `var` where the type is obvious, primary constructors for services.
- Private fields are `_camelCase`; `.editorconfig` enforces it.
- Nullable is on everywhere. Don't add `!` to silence it; fix the flow.
- Internal by default in `HSchool.Server`; public only where another project consumes it.
- New tunables go on `SimulationOptions` with a default, not as a constant buried in a class.
**TypeScript**
- `strict` is on, no `any`, no non-null `!` assertions.
- Relative imports carry the `.ts` extension (bundler resolution is configured for it).
- Modules stay thin: `net/` speaks to the server, `ui/` renders, `format/` formats, `i18n/`
holds the RU/EN dictionaries, `main.ts` wires them together.
- No framework, plain DOM. `ui/dom.ts` is the whole helper budget.
- UI strings go through `t(...)` in `i18n/strings.ts`, never inline. Game dates go through
`format/gameTime.ts`, which follows the active locale.
**Both**
- Comments explain *why*, not *what*. Assume the reader can read code.
- Match the surrounding style rather than introducing a new one.
## Testing policy
- Simulation changes need a `GameClock` or `SchoolRegistry` test. They are fast and need no host.
Putting a roster into `World` and ticking needs belongs there too.
- People generation belongs in `tests/HSchool.People.Tests`. Same seed, map and name set must
produce the same roster; the suite does not boot a host.
- Timetable planning belongs in `tests/HSchool.Schedule.Tests`. Same staff, map and locks must
produce the same table; the suite does not boot a host.
- Walking and day plans belong in `tests/HSchool.Ai.Tests`. Same seed and map must produce the
same route and commute; the suite does not boot a host, and it does not reference Arch.
- Catalog, inheritance, patches and map validation belong in `tests/HSchool.Content.Tests`.
Feed the loader documents, not disk paths.
- Protocol changes need a round-trip test **and** a byte-layout assertion on both sides.
- Server wiring, endpoints and the WebSocket belong in `tests/HSchool.AppHost.Tests`. That suite
shares one AppHost across all tests (`AppHostFixture`) — keep it that way, booting per test costs
about ten seconds each.
- Those tests also share one server, so schools survive between them (and now on disk too):
start each test by clearing the list (`SchoolApiTests.ResetAsync`) instead of assuming it
is empty. Reset deletes through the API, which deletes the save files. Headless AppHost
sets `HSchool:AllowSaveReload` so tests can `POST /api/dev/reload-schools` without killing
the shared fixture.
- The screens have no unit tests — a DOM environment would cost a dependency the project does not
have. Verify UI changes by running the app. Dictionaries and date formatting are covered in
Vitest (`i18n/strings.test.ts`, `format/gameTime.test.ts`).
## Dependencies
- NuGet versions are centrally managed in `Directory.Packages.props`. Add the version there and a
bare `<PackageReference Include="..." />` in the project.
- Transitive pinning is on, so a downgrade warning means you bump the central version rather than
adding a per-project override.
- Keep the dependency count low. Arch, Aspire and the test runners are the whole budget.
## Things that will bite you
- **`<dialog>`'s `close` event is not delivered by every engine** (Chromium 148 fires only
`toggle`). `ui/modal.ts` resolves its promise explicitly on every exit for that reason — never go
back to awaiting the event, or a dialog will silently freeze the screen that awaited it.
- **A modal `<dialog>` is absolutely positioned with `inset: 0`,** so `height: auto` stretches it
to its `max-height` instead of shrink-wrapping. `.dialog--screen` uses `height: fit-content` for
that reason — the map editor sized itself to 800px around a two-room school before it did.
- **Game dates are UTC on the wire and UTC when formatted.** A `DateTime` bound from configuration
arrives as `Kind=Unspecified`, serializes without a `Z`, and the browser then reads it in its own
time zone — `SimulationOptions.DefaultStartDate` forces the kind for exactly that reason.
- `PeriodicTimer` does not catch up on its own. The accumulator in each `SchoolWorker` does,
capped at 5 steps — do not "simplify" it away.
- Every school runs whether or not a connection is watching it, so anything you hang off the tick
runs six times over once six schools exist. `OpenSchool` only subscribes to clock frames.
- The menu polls `GET /api/schools` once a second and patches its cards in place. Rebuilding the
list on every refresh would drop focus and swallow clicks.
- **An abandoned `WaitToReadAsync` stays queued on its channel.** `GameClient.RunSendLoopAsync`
keeps its two wait tasks across iterations for that reason; recreating both on every idle pass
leaked the loser of `Task.WhenAny` once per clock frame.
- **A map snapshot is not bounded by `MaxMessageSize`.** That constant limits what the server
reads. Outbound snapshots are sized with `ProtocolCodec.MapSnapshotSize`, because a school the
player enlarged in the create editor passes 8 KiB at roughly sixty furnished rooms.
- **A worker that dies must tell the supervisor** (`GameCommand.WorkerFailed`). It owns the table,
so a school that removed itself would break invariant 3 — and a school that removes nothing
leaves a card in the menu whose clock never moves again.
- The client outbox drops the oldest frame under pressure. That is correct for clock frames and
wrong for anything that must arrive exactly once — such a message would need its own path.
- `erasableSyntaxOnly` is off in `tsconfig.app.json` on purpose: constructor parameter properties
are used throughout.
- **A large one-shot `Tick(week)` jumps the clock, then applies all those minutes at the new
time.** Presence equality tests must loop `FixedDeltaTime` or one game minute. `string.GetHashCode`
is randomized — commute slack goes through `Seed.Mix`, never that.