- Revised `AGENTS.md` to specify that the same seed, map, name set, and native language must produce the same roster in tests. - Updated `protocol.md` to clarify that everyone in the school is generated with the selected native language, and related languages may appear at a low skill. - Enhanced `ai.md` to explain skill growth mechanics based on state and the introduction of minimum ranges for skills not yet acquired. - Improved `people.md` to detail the generation of skills and traits, emphasizing that not every person receives every skill and the implications of body attributes on skill acquisition. - Adjusted `projects.md` to reflect the inclusion of native language in roster generation, ensuring comprehensive documentation of project components.
173 lines
9.9 KiB
Markdown
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, name set and
|
|
native language 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.
|