# NEO-6 — Implementation plan

## Story reference


| Field              | Value                                                                                   |
| ------------------ | --------------------------------------------------------------------------------------- |
| **Key**            | NEO-6                                                                                   |
| **Title**          | E1.M1: Authoritative PositionState API (in-memory)                                      |
| **Linear**           | [NEO-6](https://linear.app/neon-sprawl/issue/NEO-6)                                 |
| **Parent context** | [E1.M1 InputAndMovementRuntime](https://linear.app/neon-sprawl/project/e1m1-inputandmovementruntime-20eb28dd3db4) |


## Goal, scope, and out-of-scope

**Goal:** The game server is the source of truth for a single development **PositionState**, exposed read-only over HTTP as JSON (spike alignment with [tech stack](../../docs/architecture/tech_stack.md)).

**In scope**

- ASP.NET Core endpoint: `GET /game/players/{id}/position` returning JSON for the agreed **PositionState** shape (`x`, `y`, `z`, plus optional **facing** and/or **sequence** as needed for future sync).
- Seed one logical player id for local development (configuration entry or documented constant; prefer **config** so environments stay obvious).
- **In-memory** store only (dictionary or small service holding positions); no PostgreSQL.

**Out of scope (per Linear)**

- Godot or other client integration, move submission, database persistence.

## Acceptance criteria checklist

- [ ] `GET` returns stable JSON matching the agreed **PositionState** shape; field names documented in PR description and/or XML doc comments on the response/DTO types.
- [ ] Verification via **`dotnet test`** and/or a **curl** snippet in `server/README.md` (story allows either; plan: **both** — minimal integration test + README curl for ad-hoc checks).
- [ ] Payload is **versionable**: top-level **`schemaVersion`** (integer) or an explicit wrapper DTO so v2 can extend without breaking consumers that check version first.

## Technical approach

1. **Response contract**
  - Define a small set of types (e.g. record/DTO) for the HTTP JSON body.  
  - **Suggested v1 shape** (adjust names in implementation if parent epic standardizes differently):  
    - `schemaVersion`: `1`  
    - `playerId`: string (echo path id for sanity)  
    - `position`: object with `x`, `y`, `z` as doubles (or floats serialized as JSON numbers)  
    - Optional: `facing` (e.g. yaw radians or degrees — pick one and document), `sequence` (ulong/int for future tick ordering)
  - XML doc comments on public API types document each property; PR description repeats the sample JSON.
2. **In-memory authority**
  - Implement `IPositionStateStore` (and concrete in-memory type) backed by `ConcurrentDictionary<string, …>` or immutable snapshots.  
  - On startup, ensure the **dev player id** from config exists with a default position (e.g. origin); other ids may return **404** until a later story seeds them.
  - **Do not** register the store in `Program.cs`. Use an `IServiceCollection` extension (e.g. `PositionStateServiceCollectionExtensions.AddPositionStateStore`) that binds options and registers the singleton.
3. **HTTP API (dedicated class)**
  - **Do not** define routes inline in `Program.cs`. Add a **separate class** (e.g. `PositionStateApi` or `PositionStateEndpoints`) whose responsibility is mapping this feature’s routes—typically a static class with an extension method on `IEndpointRouteBuilder` or `WebApplication`, e.g. `MapPositionStateApi()`, containing the `MapGet("/game/players/{id}/position", …)` and handler logic (or thin delegates to a small handler type). Handlers resolve `IPositionStateStore` from DI, return `Results.Json(...)` or `Results.NotFound()`.
4. **Configuration**
  - Add section in `appsettings.json` / `appsettings.Development.json` (e.g. `Game:DevPlayerId`, optional `Game:DefaultPosition` or nested object). Bind with `IOptions` or read once at startup when seeding the store.
5. **Tests**
  - Add **`NeonSprawl.Server.Tests`** (xUnit), reference **`Microsoft.AspNetCore.Mvc.Testing`**, and add **`public partial class Program { }`** to the server assembly (empty partial in a **small** file—e.g. `Program.Waf.cs` or the tail of `Program.cs`—only for the test host, not for endpoint definitions) so **`WebApplicationFactory<Program>`** works with top-level statements.
  - One integration test: `GET` for the configured dev player returns **200**, body parses, **`schemaVersion`** present, **`position.x`**, **`position.y`**, **`position.z`** present.
  - Optional second test: unknown id returns **404**.
6. **Documentation**
  - Extend `server/README.md` with “Position state (NEO-6)”: endpoint path, example `curl`, note that state is in-memory and authoritative only at the HTTP spike layer.

## Files to add


| Path | Purpose |
|------|--------|
| `server/NeonSprawl.Server.Tests/NeonSprawl.Server.Tests.csproj` | xUnit + `Microsoft.AspNetCore.Mvc.Testing` test project. |
| `server/NeonSprawl.Server.Tests/Game/PositionState/PositionStateApiTests.cs` | Integration test(s) for `GET /game/players/{id}/position`. |
| `server/NeonSprawl.Server/...` DTOs / contracts | e.g. `PositionStateResponse.cs` (or folder `Game/PositionState/`) — versioned JSON shape. |
| `server/NeonSprawl.Server/...` `IPositionStateStore` + in-memory implementation | Authority + seeding from options. |
| `server/NeonSprawl.Server/...PositionStateServiceCollectionExtensions.cs` (or `Game/DependencyInjection/…`) | `AddPositionStateStore(IServiceCollection, IConfiguration)` (or options binding); **all** registration for this feature, not `Program.cs`. |
| `server/NeonSprawl.Server/...PositionStateApi.cs` (or `PositionStateEndpoints.cs`) | **Own class** mapping `GET /game/players/{id}/position` (extension on `WebApplication` / `IEndpointRouteBuilder`); no route definitions in `Program.cs`. |
| Optional: `server/NeonSprawl.Server/Program.Waf.cs` | Empty `public partial class Program { }` for `WebApplicationFactory` only. |


## Files to modify


| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Program.cs` | Host bootstrap only: `WebApplication.CreateBuilder`, call service + endpoint extension methods (one line each or similarly minimal), `app.Run()`. **No** store registration body and **no** `MapGet` for position state here. |
| `server/NeonSprawl.Server/appsettings.json` and/or `appsettings.Development.json` | Dev player id and default position seed. |
| `server/README.md` | curl example + short contract note. |
| `NeonSprawl.sln` | Include test project in solution. |


## Tests

- **Automated:** Integration test(s) via `WebApplicationFactory<Program>` hitting `GET /game/players/{id}/position` — satisfies AC and [testing expectations](../../.cursor/rules/testing-expectations.md) (first testable server module gets a test project).
- **Manual:** `curl` snippet in README against `dotnet run` (default port from `launchSettings.json`, typically `http://localhost:5253`).

## Pull request description (Linear AC)

NEO-6 asks for stable JSON **field names** documented for reviewers outside the repo. **When you open the GitHub PR** (and optionally comment on [NEO-6](https://linear.app/neon-sprawl/issue/NEO-6)), paste the following so AC is satisfied without hunting the README.

**Endpoint:** `GET /game/players/{id}/position` — **200** when the player exists, **404** when unknown.

**Example response body (v1, formatted for readability):**

```json
{
  "schemaVersion": 1,
  "playerId": "dev-local-1",
  "position": {
    "x": 0,
    "y": 0,
    "z": 0
  },
  "sequence": 0
}
```

| JSON property | Type (v1) | Meaning |
|---------------|-----------|---------|
| `schemaVersion` | number (integer) | Contract version; increment when the payload shape or semantics change. |
| `playerId` | string | Echo of the `{id}` path segment from the request (casing preserved). |
| `position` | object | Authoritative world position. |
| `position.x` | number | X coordinate. |
| `position.y` | number | Y coordinate. |
| `position.z` | number | Z coordinate. |
| `sequence` | number (integer) | Increments on each successful move apply ([NEO-7](NEO-7-implementation-plan.md)); `0` until first move for the seeded dev player. |

**v1 omissions:** `facing` is not included (see Open questions). JSON uses **camelCase** property names (ASP.NET Core default serialization).

## Open questions / risks

- **Facing convention:** If `facing` is included, confirm units (radians vs degrees) with E1.M1 / future sync design; if unclear, omit v1 `facing` and document “reserved for v2” or add `schemaVersion` bump when added.  
- **Player id format:** Use a simple string (e.g. `dev-local-1`); path segment must be URL-safe.  
- **CI:** If no workflow runs `dotnet test` yet, local `dotnet test` still satisfies AC; consider a follow-up chore to wire CI when available.
