diff --git a/ARCHITECTURE_deprecated.md b/ARCHITECTURE_deprecated.md deleted file mode 100644 index 42f5787..0000000 --- a/ARCHITECTURE_deprecated.md +++ /dev/null @@ -1,1579 +0,0 @@ -# Services Architecture (DEPRECATED) - -> This document describes the previous multi-service architecture. Superseded -> by `ARCHITECTURE.md` as of 2026-05-03. It is kept for historical reference -> and must not be used for new implementation work. - -Galaxy: Turn-based Strategy Game - -## Purpose - -This document defines the high-level architecture of the Galaxy Ga,e platform as a single source of truth for implementing all core microservices. - -It describes: - -* public and trusted service boundaries; -* ownership of main business entities and state; -* request routing and transport rules; -* interaction rules between services; -* runtime model for game containers; -* notification and event propagation model; -* recommended implementation order. - -Detailed behavior of each concrete service belongs in its own README. -This document fixes the system-level structure and the architectural rules that must remain stable across service implementations. - -## Scope - -Galaxy Game is a multiplayer turn-based online strategy game platform. - -Core product properties: - -* many game sessions may exist simultaneously; -* one user may participate in multiple games at once; -* users authenticate by e-mail confirmation code; -* users have platform roles and tariff/entitlement state; -* games may be public or private; -* public games are managed by system administrators; -* private games are created and managed by eligible paid users; -* each running game is executed inside its own dedicated game engine container; -* each running game is bound to one concrete engine version; -* in-place upgrade of a running game is allowed only as a patch update within the same semver major/minor line; -* player commands are turn-bound and are accepted only before the next scheduled turn generation cutoff. - -The platform stores durable business state in PostgreSQL (one shared database, schema per service) and uses Redis with Redis Streams for ephemeral state, caches, and the internal event bus. The backend split, library stack, and staged migration plan live in [`PG_PLAN.md`](PG_PLAN.md) and the [Persistence Backends](#persistence-backends) section below. - -## Main Principles - -* The platform exposes a single external entry point: **Edge Gateway**. -* Public unauthenticated flows use REST/JSON. -* Authenticated user edge traffic uses signed gRPC over HTTP/2 with protobuf control envelopes and FlatBuffers payload bytes. -* Trusted synchronous inter-service traffic uses REST/JSON unless a service-specific contract states otherwise. -* For the direct `Gateway -> User` self-service boundary, gateway keeps the external authenticated gRPC + FlatBuffers contract and performs REST/JSON transcoding toward `User Service` internally. -* The gateway handles only edge concerns: parsing, authentication, integrity checks, anti-replay, rate limiting, routing, and push delivery. Business authorization and domain rules remain in downstream services. -* `Auth / Session Service` is the source of truth for `device_session`, but it is not on the hot path of every authenticated request. Gateway authenticates steady-state traffic from session cache and lifecycle updates. -* `Game Lobby` owns platform-level metadata of game sessions. -* `Game Master` owns runtime and operational state of running games. -* `Runtime Manager` is the only service allowed to access Docker API directly. -* `Notification Service` is the platform-level delivery/orchestration layer for push and most non-auth email notifications. -* `Mail Service` sends email; auth-code mail is sent directly by `Auth / Session Service`, while all other platform mail is initiated through `Notification Service`. -* `Geo Profile Service` is auxiliary and fail-open relative to gameplay; it never blocks the currently processed request and may affect only later requests. -* If a user-facing request must complete with a deterministic result in the same flow, the critical internal chain must be synchronous. If the interaction is propagation, notification, cache update, runtime job completion, telemetry, or denormalized read-model update, it should be asynchronous. - -## Security and Transport Model - -The former standalone security model is part of the main architecture and is no longer treated as a separate subsystem. - -### Public and authenticated transport classes - -The gateway already distinguishes: - -* public REST/JSON for unauthenticated traffic such as health checks and public auth; -* authenticated gRPC over HTTP/2 for verified commands and push delivery. - -For downstream business services, the current default trusted transport is -strict REST/JSON. Gateway may therefore authenticate and verify one external -FlatBuffers command, then transcode it to one trusted downstream REST call. - -When forwarding an authenticated command to a downstream service, `Edge Gateway` -enriches the REST call with the `X-User-ID` header carrying the verified platform -user identifier. Downstream services derive the acting user identity exclusively -from this header and must never accept identity claims from request body fields. - -The public auth contract is: - -* `send-email-code(email) -> challenge_id` -* `confirm-email-code(challenge_id, code, client_public_key, time_zone) -> device_session_id` - -The authenticated request contract is based on: - -* `device_session_id` -* `message_type` -* `timestamp_ms` -* `request_id` -* `payload_hash` -* Ed25519 client signature over canonical envelope fields. - -Server responses and push events are signed by the gateway so clients can verify server-originated messages. Push streams are bound to authenticated `user_id` and `device_session_id`, and session revoke closes only streams bound to the revoked session. - -### Verification boundary - -Before routing an authenticated request, gateway must: - -1. validate envelope presence and protocol version; -2. resolve session from session cache; -3. reject unknown or revoked sessions; -4. verify `payload_hash`; -5. verify client signature; -6. verify freshness window; -7. verify anti-replay by `device_session_id + request_id`; -8. apply edge rate limits and basic policy checks; -9. build an authenticated internal command context and only then route downstream. - -Downstream services must never receive unauthenticated external traffic. - -## High-Level System Diagram - -```mermaid -flowchart LR - Client["Game Client\n(native / browser)"] - AdminUI["Admin UI"] - Gateway["Edge Gateway\nPublic REST\nAuthenticated gRPC\nAdmin REST"] - Auth["Auth / Session Service"] - User["User Service"] - Lobby["Game Lobby Service"] - GM["Game Master"] - Runtime["Runtime Manager"] - Notify["Notification Service"] - Mail["Mail Service"] - Geo["Geo Profile Service"] - Billing["Billing Service\nfuture"] - Redis["Redis\nCache, Streams, Leases"] - Postgres["PostgreSQL\nDurable Business State"] - Telemetry["Telemetry"] - - Client --> Gateway - AdminUI --> Gateway - - Gateway --> Auth - Gateway --> User - Gateway --> Lobby - Gateway --> GM - Gateway --> Geo - - Auth --> User - Auth --> Mail - Auth --> Redis - - User --> Redis - - Lobby --> User - Lobby --> GM - Lobby --> Runtime - Lobby --> Redis - - User --> Lobby - - GM --> Lobby - GM --> Runtime - GM --> Redis - - Geo --> Auth - Geo --> User - Geo --> Redis - - Notify --> Gateway - Notify --> Mail - Notify --> Redis - - Runtime --> Redis - - Mail --> Redis - User --> Postgres - Mail --> Postgres - Notify --> Postgres - Lobby --> Postgres - - Billing --> User - Telemetry --- Gateway - Telemetry --- Auth - Telemetry --- User - Telemetry --- Lobby - Telemetry --- GM - Telemetry --- Runtime - Telemetry --- Notify - Telemetry --- Geo -``` - -The baseline gateway/auth/session/pub-sub model above is consistent with the existing architecture and service READMEs. - -## Service List and Responsibility Boundaries - -## 1. [Edge Gateway](gateway/README.md) - -`Edge Gateway` is the only public entry point for all external traffic. It already owns transport parsing, session-cache-based authentication, signature verification, freshness/replay checks, edge rate limiting, routing, and push delivery. It must remain free of domain-specific business logic. - -External surfaces: - -* public REST: - - * health and readiness; - * public auth commands; - * browser/bootstrap and public route classes where needed. -* authenticated gRPC: - - * generic `ExecuteCommand`; - * authenticated `SubscribeEvents`. -* admin REST: - - * separate public administrative surface for system administrators; - * routed only for authenticated users with admin role. - -The gateway does not directly access game engine containers. -For running games it routes to `Game Master`. -For pre-game platform flows it routes to `Game Lobby`. -For user-profile requests it routes to `User Service`. -For public auth it routes to `Auth / Session Service`. - -## 2. [Auth / Session Service](authsession/README.md) - -`Auth / Session Service` owns: - -* challenge lifecycle; -* e-mail-code authentication; -* creation of `device_session`; -* registration of the client Ed25519 public key; -* revoke/logout/block state; -* trusted internal read/revoke/block API; -* projection of session lifecycle state into gateway-consumable Redis data. - -It is the source of truth for: - -* authentication challenges; -* `device_session`; -* revoke/block state. - -Important architectural rules: - -* public auth stays synchronous; -* `confirm-email-code` returns a ready `device_session_id`; -* no async “pending session provisioning” step exists; -* session source of truth and gateway-facing projection remain separate; -* active-session limits are configuration-driven; -* `send-email-code` stays success-shaped for existing, new, blocked, and throttled email flows. - -When `confirm-email-code` reaches first successful completion for an e-mail -address that does not yet belong to a user, auth may pass create-only -registration context to `User Service` during the synchronous ensure/create -step. - -Direct integrations: - -* synchronous to `User Service` for user resolution/create/block decision; -* synchronous to `Mail Service` for auth-code delivery; -* asynchronous session lifecycle projection into Redis for gateway consumption. - -## 3. [User Service](user/README.md) - -`User Service` owns regular-user identity and profile as platform-level -business data. - -It is the source of truth for: - -* `user_id` of regular platform users; -* `user_name` — immutable auto-generated unique platform handle in - `player-` form; never used as foreign key in other models; -* `display_name` — mutable free-text user-editable label validated through - `pkg/util/string.go:ValidateTypeName`; not required to be unique; default - empty for new accounts; -* editable user settings (`preferred_language`, `time_zone`); -* current tariff/entitlement state including `max_registered_race_names`; -* user-specific limits and platform sanctions (including - `permanent_block` and `max_registered_race_names` override limits); -* latest effective `declared_country`; -* soft-delete state via `DeleteUser`. - -`User Service` does not own in-game `race_name` values; those live in -`Game Lobby` Race Name Directory. - -System-administrator identity remains outside this service and belongs to the -later `Admin Service`. Trusted administrative reads and mutations against -regular-user state do not make `User Service` the owner of administrator -identity. - -It is directly reachable through gateway for selected user-facing operations such as: - -* reading and editing allowed profile fields; -* viewing tariff and entitlement state; -* viewing user settings; -* viewing current restrictions and sanctions. - -Not every profile mutation goes directly here. For example: - -* email change must use a code-confirm flow; -* `declared_country` change remains under admin approval flow via `Geo Profile Service`. - -Architectural rules fixed for this service: - -* `User Service` owns regular-user identity only; system-admin identity is out - of scope. -* `User Service` stores only the current effective `declared_country`; review - workflow and history belong to `Geo Profile Service`. -* `User Service` does not own in-game `race_name` values. All in-game name - state (registered, reserved, pending registration) lives in the Game Lobby - Race Name Directory. The only identity strings owned by `User Service` are - `user_name` (immutable) and `display_name` (mutable, non-unique). -* `permanent_block` is a dedicated sanction code that collapses every - `can_*` eligibility marker to false and triggers RND cascade release via - the `user:lifecycle_events` stream. -* `DeleteUser` is a trusted internal endpoint that soft-deletes the account, - rejects all subsequent operations with `subject_not_found`, and triggers - the same RND cascade release. -* During the current auth-registration rollout, `Auth / Session Service` - passes a preferred-language candidate derived from public - `Accept-Language`, falling back to `en` when no supported value is - available, plus the confirmed `time_zone` into `User Service`. - -Future billing does not become a direct dependency of other services. `Billing Service` will feed entitlement/payment outcomes into `User Service`, and the rest of the platform will continue to use `User Service` as the source of truth for current entitlements. - -## 4. [Mail Service](mail/README.md) - -`Mail Service` is the internal email delivery service. - -Split of responsibility: - -* auth code emails: `Auth / Session Service -> Mail Service` directly; -* all other user/admin notification emails: `Notification Service -> Mail Service`. - -Transport rules: - -* `Auth / Session Service -> Mail Service` uses the dedicated synchronous - trusted internal REST contract `POST /api/v1/internal/login-code-deliveries`; -* `Notification Service -> Mail Service` is an asynchronous internal command - flow carried through dedicated queue-backed handoff after durable route - acceptance inside `Notification Service`. - -This split is covered by integration tests: auth-code delivery bypasses -`Notification Service`, while notification-generated mail uses template-mode -commands whose `template_id` equals `notification_type`. - -`Mail Service` may internally queue both flows. -Its trusted operator read and resend APIs are part of the v1 service surface, -not a later add-on. -For auth callers, a successful result means the request was durably accepted -into the mail-delivery pipeline or intentionally suppressed; it does not -require that the external SMTP exchange already completed before the response -is returned. -Stable service-local delivery rules, retry semantics, and storage details -(PostgreSQL for the durable delivery record, attempt history, dead letters, -and audit; Redis for the inbound `mail:delivery_commands` stream and its -consumer offset) belong in [`mail/README.md`](mail/README.md), not in the -root architecture document. - -## 5. [Geo Profile Service](geoprofile/README.md) - -`Geo Profile Service` is an internal trusted auxiliary service for country-level connection signals of authenticated users. - -It integrates with: - -* gateway as asynchronous ingest producer; -* `User Service` for current effective `declared_country`; -* `Auth / Session Service` for suspicious session blocking; -* `Notification Service` for optional admin notifications. - -It owns: - -* observed country facts; -* per-session country aggregation; -* `usual_connection_country`; -* `country_review_recommended`; -* history of `declared_country` changes. - -It does not block the request that triggered suspicion. -It can only request block of suspicious sessions for subsequent requests. -It does not call `Mail Service` directly; optional admin mail must flow -through `Notification Service`. - -In this document, references to `Edge Service` in older geo documentation should be understood as `Edge Gateway`. - -## 6. Admin Service - -`Admin Service` is the external backend/orchestration layer for the administrative UI. - -It is not a heavy domain owner. -Its job is to: - -* expose administrator-facing workflows; -* call trusted internal APIs of other services; -* aggregate administrative views where needed; -* enforce system-admin role checks at the gateway/admin boundary. - -System administrators can view and operate on all games, including private ones. - -## 7. [Game Lobby Service](lobby/README.md) - -`Game Lobby` owns platform-level metadata and lifecycle of game sessions as platform entities. - -It is the source of truth for: - -* game records before and after runtime existence; -* public/private game type; -* owner of a private game; -* user-bound invitations and invite lifecycle; -* applications and approvals; -* membership and roster; -* blocked/removed participants at platform level; -* turn schedule configuration; -* target engine version for launch; -* user-facing lists of games; -* denormalized runtime snapshot imported from `Game Master`. - -`Game Lobby` is the source of truth for: - -* party membership; -* invited / pending / active / finished / removed status of players relative to games; -* user-visible lists such as `active / finished / pending / invited games`. - -It also stores a denormalized runtime snapshot for convenience, at least: - -* `current_turn`; -* `runtime_status`; -* `engine_health_summary`. - -Additionally, `Game Lobby` aggregates per-member game statistics from -`player_turn_stats` carried on each `runtime_snapshot_update` event: -current and running-max of `planets` and `population`. The aggregate is -retained from game start until capability evaluation at `game_finished`. - -This prevents user-facing list/read flows from fan-out requests into `Game Master`. - -### Lobby status model - -Minimum platform-level status set: - -* `draft` -* `enrollment_open` -* `ready_to_start` -* `starting` -* `start_failed` -* `running` -* `paused` -* `finished` -* `cancelled` - -`Lobby.paused` is a business/platform pause, distinct from engine/runtime failure states. - -`start_failed` indicates that the runtime container could not be started or that -metadata persistence failed after a successful container start. -From `start_failed` an admin or owner may retry (→ `ready_to_start`) or cancel (→ `cancelled`). - -### Enrollment rules - -Each game stores three enrollment configuration fields set at creation: - -* `min_players` — minimum approved participants required before the game may start. -* `max_players` — target roster size that activates the gap admission window. -* `start_gap_hours` — hours to keep enrollment open after `max_players` is reached. -* `start_gap_players` — additional players admitted during the gap window. -* `enrollment_ends_at` — UTC Unix timestamp at which enrollment closes automatically. - -Transition from `enrollment_open` to `ready_to_start` occurs via one of three paths: - -1. **Manual**: an admin (public game) or owner (private game) issues a close-enrollment - command when `approved_count >= min_players`. -2. **Deadline**: `enrollment_ends_at` is reached and `approved_count >= min_players`. -3. **Gap exhaustion**: `approved_count >= max_players` activates a gap window of - `start_gap_hours` during which up to `start_gap_players` additional participants - may join; the transition fires when the gap window expires or - `approved_count >= max_players + start_gap_players`. - -All pending invites transition to `expired` when the game moves to `ready_to_start`. - -### Membership rules - -* `User Service` owns users of the platform as identities. -* `Game Lobby` owns membership in concrete games. -* game engine does not own platform membership; -* `Game Master` may cache membership for runtime authorization, but `Game Lobby` remains the source of truth. - -### Public vs private game rules - -Public games: - -* created and controlled by system administrators; -* visible in public list; -* joining is based on application and manual admin approval in v1. - -Private games: - -* can be created only by eligible paid users; -* visible only to their owner and to invited users whose invitation is bound - to a concrete `user_id` and later accepted; -* joining uses a user-bound invite; accepting the invite immediately creates active - membership without a separate owner-approval step; -* invite lifecycle belongs entirely to `Game Lobby`. - -Private-party owners get a limited owner-admin capability set, not full system admin power. - -### Race Name Directory - -`Race Name Directory` (RND) is the platform source of truth for in-game player -names (`race_name`). It is owned by `Game Lobby` in v1 and is scheduled to move -to a dedicated `Race Name Service` later without changing the domain or -service-layer logic. - -RND owns three levels of state per name: - -* **registered** — platform-unique permanent names owned by one regular user. - A registered name cannot be transferred, released, or renamed; the only path - back to availability is `permanent_block` or `DeleteUser` on the owning - account. The number of registered names a user can hold is bounded by the - current tariff (`max_registered_race_names` in the `User Service` eligibility - snapshot): `free=1`, `paid_monthly=2`, `paid_yearly=6`, - `paid_lifetime=unlimited`. Tariff downgrade never revokes existing - registrations; it only constrains new ones. -* **reservation** — per-game binding created when a participant joins a game - through application approval or invite redeem. The reservation key is - `(game_id, canonical_key)`. One user may hold the same name simultaneously - across multiple active games. A reservation survives until the game - finishes, then either becomes a `pending_registration` (see below) or is - released. -* **pending_registration** — a reservation that survived a capable finish and - is now waiting up to 30 days for the owner to upgrade it into a registered - name via `lobby.race_name.register`. Expiration releases the binding. - -**Canonical key** — RND uses a canonical key (lowercase + frozen -confusable-pair policy) to enforce uniqueness. A name is considered taken for -another user when any `registered`, active `reservation`, or -`pending_registration` with a different `user_id` exists under the same -canonical key. The confusable-pair policy lives in Lobby -(`lobby/internal/domain/racename/policy.go`). - -**Capability gating** — at `game_finished` `Game Lobby` evaluates per-member -capability: `capable = max_planets > initial_planets AND max_population > -initial_population`, computed from the `player_turn_stats` stream published by -`Game Master`. Capable reservations transition to `pending_registration` with -`eligible_until = finished_at + 30 days`; non-capable reservations are -released immediately. - -**Registration** — a user initiates registration via `lobby.race_name.register` -inside the 30-day window. Registration succeeds only when the user is still -eligible (no `permanent_block`, tariff slot available) and the pending entry -is still within its window. Expired pending entries are released by a -background worker. - -**Cascade release** — `User Service` publishes -`user.lifecycle.permanent_blocked` and `user.lifecycle.deleted` events to -`user:lifecycle_events`. `Game Lobby` consumes this stream and calls -`RND.ReleaseAllByUser(user_id)` atomically with membership/application/invite -cancellations for the affected user. - -## 8. [Game Master](gamemaster/README.md) - -`Game Master` owns runtime and operational metadata of already running games. - -It is the only trusted service allowed to communicate with game engine containers. - -It owns: - -* runtime mapping of running game to container endpoint/binding; -* current turn number; -* runtime status; -* generation status; -* engine health; -* patch state; -* engine version registry and version-specific engine options; -* runtime mapping `platform user_id -> engine player UUID` for each running game. - -### Topology - -`Game Master` runs as a single process in v1. The in-process scheduler is -authoritative; multi-instance with leader election is an explicit future -iteration. Every other service that interacts with `Game Master` -(`Edge Gateway`, `Game Lobby`, `Admin Service`, `Runtime Manager`) treats -GM as a singleton on the trusted network segment. - -### Engine container contract - -`Game Master` is the only platform component that talks to the engine. The -engine container exposes two route classes: - -* admin paths under `/api/v1/admin/*` — `init`, `status`, `turn`, and - `race/banish`. They are unauthenticated and reachable only inside the - trusted network segment that connects GM to the engine container; -* player paths under `/api/v1/{command, order, report}` — invoked by GM on - behalf of an authenticated platform user; the actor field on each call - is set by GM from the verified user identity, never from the inbound - payload; -* `GET /healthz` — liveness probe used by `Runtime Manager` and operator - tooling. - -Two engine-side fields are part of the contract: - -* `StateResponse.finished:bool` — when `true` on a turn-generation - response, GM transitions the runtime to `finished`, publishes - `game_finished`, and dispatches the finish notification. The conditional - logic that flips the flag lives in the engine's domain code and is not - GM's concern; -* `POST /api/v1/admin/race/banish` with body `{race_name}` — invoked by GM - in response to the Lobby-driven banish flow after a permanent - platform-level membership removal. The engine returns `204` on success. - -### Game Master status model - -Minimum runtime-level status set: - -* `starting` -* `running` -* `generation_in_progress` -* `generation_failed` -* `stopped` -* `engine_unreachable` -* `finished` - -`running` here means `running_accepting_commands`. `finished` is terminal: -the runtime record stays in this state indefinitely; no further turn -generation, command, or order is accepted, and operator cleanup is the -only path out. - -### Game command routing - -All game-related `message_type` include `game_id`. - -Gateway enriches them with authenticated `user_id` and routes them to `Game Master`. -`Game Master` checks whether this user may access this running game, using membership data sourced from `Game Lobby`, then routes the command to the correct engine container using [Game Engine](./game/README.md)'s API. - -The gateway never routes directly to game engine containers. - -### Runtime admin operations - -For already running games, `Game Master` handles: - -* `stop game` -* `force next turn` -* `patch engine` -* admin/runtime status reads -* player deactivation/removal inside engine when required -* regular collection of game runtime metrics - -System admin can use all of them. -Private-game owner can use the subset allowed for the owner of that game. - -### Turn cutoff and scheduling - -`Game Master` is the owner of authoritative platform time for turn cutoff -decisions. - -The cutoff is enforced by a single status compare-and-swap: every player -command, order, and report read requires `runtime_status=running` at the -moment of the call, and turn generation begins by CAS-ing -`running → generation_in_progress`. There is no separately tracked shadow -window or grace period — the status transition itself is the boundary. -Commands arriving after the CAS are rejected with `runtime_not_running`. - -The scheduler is a subsystem inside `Game Master`. It triggers turn -generation according to the game schedule. - -If a manual `force next turn` is executed, the next scheduled turn slot -must be skipped so that players still get at least one full normal -schedule interval before the following generated turn. The skip is -recorded as `runtime_records.skip_next_tick=true`; the scheduler advances -`next_generation_at` by one extra cron step the next time it computes the -tick and clears the flag. - -### Runtime snapshot publishing - -`Game Master` publishes runtime updates to the `gm:lobby_events` Redis Stream -consumed by `Game Lobby`. Events include: - -* `runtime_snapshot_update` — carries the current `current_turn`, - `runtime_status`, `engine_health_summary`, and a `player_turn_stats` array - with one entry per active member (`user_id`, `planets`, `population`). - `Game Lobby` maintains a per-game per-user stats aggregate from these - events for capability evaluation at game finish. -* `game_finished` — carries the final snapshot values and triggers the - platform status transition plus Race Name Directory capability evaluation - inside `Game Lobby`. - -Publication cadence is event-driven. GM publishes a snapshot when: - -* a turn was generated (success or failure); -* `runtime_status` transitioned (e.g., - `running ↔ generation_in_progress`, `running → engine_unreachable`, - `* → finished`); -* `engine_health_summary` changed in response to a `runtime:health_events` - observation; consecutive observations with identical summaries are - debounced. - -There is no periodic heartbeat. `Game Master` does not retain the -aggregate; it only publishes the per-turn observation. `Game Lobby` is -responsible for holding initial values and running maxima across the -lifetime of the game. - -### Runtime/engine finish flow - -When the engine determines that a game is finished: - -1. engine reports finish to `Game Master`; -2. `Game Master` updates runtime state; -3. `Game Master` notifies `Game Lobby`; -4. `Game Lobby` updates the platform-level game record to `finished`. - -### Player removal after start - -After a game has started, two different actions exist: - -* temporary removal/block at platform level: - - * the player cannot send commands through gateway/platform; - * the engine still keeps the player slot; -* final removal or account-level block: - - * `Game Master` must additionally send an admin command to the engine to deactivate/remove the player inside the game. - -This distinction is architectural and must remain explicit. - -## 9. [Runtime Manager](rtmanager/README.md) - -`Runtime Manager` is the only internal service allowed to access Docker API directly. - -It owns: - -* starting game engine containers; -* stopping containers; -* restarting containers where allowed; -* patching/replacing containers (semver patch only) where allowed; -* technical runtime inspection/status; -* monitoring containers via Docker events, periodic inspect, and active HTTP probe; -* publishing technical runtime events (`runtime:job_results`, `runtime:health_events`); -* publishing admin-only notification intents for first-touch start failures. - -It does **not** own platform metadata of games. -It does **not** own runtime business state of games. -It does **not** resolve engine versions; the producer (`Game Lobby` in v1, `Game Master` later) supplies `image_ref`. -It executes runtime jobs for `Game Lobby` and `Game Master`. - -### Container model - -* one game = one container; -* one container = one game. - -This is a hard invariant. - -Each container is created with hostname `galaxy-game-{game_id}` and attached to the -single user-defined Docker bridge network configured by `RTMANAGER_DOCKER_NETWORK`. -The network is provisioned outside `Runtime Manager` (compose, Terraform, or operator -runbook); a missing network is a fail-fast condition at startup. The published -`engine_endpoint` is the stable URL `http://galaxy-game-{game_id}:8080`; restart and -patch keep the same DNS name even though `current_container_id` changes. - -### Image policy - -`Runtime Manager` never resolves engine versions. The producer (`Game Lobby` in v1, -`Game Master` once implemented) computes `image_ref` from its own template and -hands it to `Runtime Manager` on the start envelope. `Runtime Manager` accepts the -reference verbatim, applies the configured pull policy -(`RTMANAGER_IMAGE_PULL_POLICY`), and reads container resource limits from labels -on the resolved image. - -The producer-supplied `image_ref` rule decouples `Runtime Manager` from any -engine-version arbitration logic, lets the v1 launch ship without `Game Master`'s -engine-version registry, and cleanly separates "which image to run" (Lobby/GM -concern) from "how to run it" (RTM concern). Two alternatives were rejected: -RTM holding its own image map (would need to consume upstream tariff or -compatibility signals that belong in the producers) and RTM resolving the -image at start time by querying GM (would create a circular dependency for -v1 and add a synchronous hop on the hot path). - -Patch is restart with a new `image_ref` and is allowed only as a semver patch -within the same major/minor line; cross-major or cross-minor patch attempts fail -with `semver_patch_only`. Producers that need to change the major/minor line must -stop the game and start a new container. - -### State ownership - -Engine state lives on the host filesystem under the per-game directory -`/{game_id}` and is bind-mounted into the container at -`RTMANAGER_ENGINE_STATE_MOUNT_PATH`. The mount path is exposed to the engine through -`GAME_STATE_PATH` and, for backward compatibility, also as `STORAGE_PATH`. Both -names are accepted by `galaxy/game` in v1. - -`Runtime Manager` never deletes the host state directory. Removing a container -through the cleanup endpoint or the retention TTL leaves the directory intact. -Backup, archival, and operator cleanup of state directories belong to operator -tooling or a future Admin Service workflow. - -### Reconcile policy - -`Runtime Manager` reconciles its `runtime_records` with Docker reality at startup -(blocking, before workers start) and on a periodic interval -(`RTMANAGER_RECONCILE_INTERVAL`). Two rules apply unconditionally: - -* unrecorded containers labelled `com.galaxy.owner=rtmanager` are **adopted** into - `runtime_records` as `running`, never killed; operators may have launched one - manually for diagnostics; -* recorded `running` rows whose container is missing in Docker are marked - `removed`, with a `container_disappeared` event emitted on - `runtime:health_events`. - -## 10. [Notification Service](notification/README.md) - -`Notification Service` is the async delivery/orchestration layer for platform notifications. - -It has a deliberately minimal role: - -* consume normalized notification intents from services through dedicated - Redis Stream `notification:intents`; -* validate idempotency and persist durable notification route state; -* enrich user-targeted routes with `email` and `preferred_language` from - `User Service`; -* decide whether a given notification type results in `push`, `email`, or - both; -* send user-targeted `push` events toward gateway by `user_id`; -* send non-auth email asynchronous commands toward `Mail Service`. - -It is not a source of truth for user preferences in v1 unless a later feature requires it. - -For user-targeted intents, upstream producers publish the concrete recipient -`user_id` values. `Notification Service` resolves user email and locale from -`User Service`, uses configured administrator email lists per -`notification_type` for admin-only notifications, keeps -`template_id == notification_type` for notification-generated email, and -treats private-game invite flows in v1 as user-bound by internal `user_id`. -Go producers use the shared `galaxy/notificationintent` module to build and -append compatible intents into `notification:intents`; a failed append is a -notification degradation signal and must not roll back already committed source -business state. -Acceptance of a user-targeted notification intent is complete only after every -published recipient `user_id` resolves through `User Service`; unresolved user -ids are treated as producer input defects and are recorded as malformed -notification intents rather than deferred publication failures. - -User-facing notifications use `push+email` unless a type explicitly opts out of -one channel. Administrator-facing notifications are `email`-only in v1. - -All platform notifications except auth-code delivery flow through this service, including: - -* game lifecycle notifications; -* invite/application updates; -* new turn notifications; -* operational/admin notifications where appropriate. - -The current process surface exposes only one private probe HTTP listener with -`GET /healthz` and `GET /readyz`; that probe surface is documented in -[`notification/openapi.yaml`](notification/openapi.yaml). The canonical -notification-intent stream contract remains -[`notification/api/intents-asyncapi.yaml`](notification/api/intents-asyncapi.yaml). -It does not expose an operator REST API. - -## 11. Billing Service (future) - -`Billing Service` is not part of the first implementation wave. - -When introduced, it will: - -* process payment/billing events; -* calculate or validate payment outcomes; -* feed resulting entitlement changes into `User Service`. - -`User Service` remains the source of truth for current entitlement used by the rest of the platform. - -Billing-driven tariff changes alter only the headroom for *new* registered -race names: tariff downgrade never revokes already registered names. The -affected ceiling is materialized as `max_registered_race_names` in the -eligibility snapshot consumed by `Game Lobby`. - -## Data Ownership Summary - -```mermaid -flowchart TD - U["User Service"] - A["Auth / Session Service"] - L["Game Lobby"] - G["Game Master"] - R["Runtime Manager"] - P["Geo Profile Service"] - N["Notification Service"] - M["Mail Service"] - - U -->|"regular users, user_name/display_name, settings, tariffs, limits, sanctions, declared_country, soft-delete"| X1["Platform user identity"] - A -->|"challenges, device sessions, revoke/block state"| X2["Auth/session state"] - L -->|"game metadata, invites, applications, membership, roster, race names (registered/reservations/pending)"| X3["Platform game records"] - G -->|"runtime state, current turn, engine health, engine mapping, engine version registry"| X4["Running-game state"] - R -->|"container execution and technical runtime control"| X5["Container runtime"] - P -->|"observed country, usual_connection_country, review state, declared_country history"| X6["Geo state"] - N -->|"notification routing only"| X7["Notification orchestration"] - M -->|"email delivery only"| X8["Email transport"] -``` - -## Internal Transport Semantics - -The platform uses one simple rule: - -* if the user-facing request must complete with a deterministic result in the same flow, the critical internal chain is synchronous; -* if the interaction is propagation, notification, cache invalidation, runtime job completion, telemetry, or denormalized read-model update, it is asynchronous. - -The `Lobby ↔ Runtime Manager` transport is the canonical asynchronous case: -Lobby drives RTM exclusively through Redis Streams (`runtime:start_jobs`, -`runtime:stop_jobs`, `runtime:job_results`); there is no synchronous -Lobby→RTM REST call in v1, and no plan to add one. Synchronous coupling -would force Lobby to block on Docker pull/start latency, which is -unbounded in the worst case. `Game Master` and `Admin Service`, by contrast, -drive RTM synchronously over REST because they operate on already-running -containers and need deterministic per-request outcomes (for example, -"restart this game's container now"); routing those operations through -streams would force operators to correlate async results back to admin -requests for no operational benefit. - -### Fixed synchronous interactions - -* `Gateway -> Auth / Session Service` -* `Gateway -> Admin Service` -* `Gateway -> User Service` -* `Gateway -> Game Lobby` -* `Gateway -> Game Master` for verified player command, order, and report - calls; -* `Auth / Session Service -> User Service` -* `Auth / Session Service -> Mail Service` -* `Geo Profile Service -> Auth / Session Service` -* `Geo Profile Service -> User Service` -* `Game Lobby -> User Service` -* `Game Lobby -> Game Master` for `register-runtime` after a successful - container start, engine-version `image-ref` resolve, membership - invalidation hook, banish, and the liveness reply consumed by Lobby's - resume flow; -* `Game Master -> Runtime Manager` for inspect, restart, patch, stop, and cleanup REST calls -* `Admin Service -> Runtime Manager` for operational inspect, restart, patch, stop, and cleanup REST calls - -### Fixed asynchronous interactions - -* session lifecycle projection toward gateway cache; -* revoke propagation; -* `Lobby -> Runtime Manager` runtime jobs through `runtime:start_jobs` (`{game_id, image_ref, requested_at_ms}`) and `runtime:stop_jobs` (`{game_id, reason, requested_at_ms}`); -* `Runtime Manager -> Lobby` job outcomes through `runtime:job_results`; -* `Runtime Manager -> Notification Service` admin-only failure intents (image pull, container start, start config) through `notification:intents`; -* `Runtime Manager` outbound technical health stream `runtime:health_events` - consumed by `Game Master`; `Game Lobby` and `Admin Service` are reserved - as future consumers; -* all event-bus propagation; -* `Game Master -> Game Lobby` runtime snapshot updates (including - `player_turn_stats` for capability aggregation) and game-finish events - through the `gm:lobby_events` Redis Stream consumed by `Game Lobby`, - published event-only with no periodic heartbeat (turn generation, - status transition, or debounced engine-health summary change); -* `User Service -> Game Lobby` user lifecycle events - (`user.lifecycle.permanent_blocked`, `user.lifecycle.deleted`) through the - `user:lifecycle_events` Redis Stream, consumed by `Game Lobby` to cascade - RND release and membership/application/invite cancellation; -* `Game Master -> Notification Service` notification intents through - `notification:intents`; -* `Game Lobby -> Notification Service` notification intents through - `notification:intents`; -* `Geo Profile Service -> Notification Service` notification intents through - `notification:intents`; -* `Notification Service -> Gateway`; -* `Notification Service -> Mail Service`; -* geo auxiliary ingest from gateway to geo service; -* runtime health events from `Runtime Manager`. - -### Mixed interactions - -Some service pairs may use both styles for different flows. -The main example is `Lobby -> Game Master`: - -* synchronous for critical registration/update after successful start; -* asynchronous for secondary propagation and denormalized status fan-out. - -## Persistence Backends - -The platform splits durable state across two backends. - -PostgreSQL is the source of truth for table-shaped business state: - -* user identity, profile settings, tariffs/entitlements, sanctions, limits, - and the blocked-email registry; -* mail deliveries, attempt history, dead letters, payloads, and - malformed-command audit; -* notification records, route materialisations, dead letters, and - malformed-intent audit; -* lobby games, applications, invites, memberships, and the race-name - registry (registered/reservation/pending tiers); -* runtime manager runtime records (`game_id -> current_container_id`), - per-operation audit log, and latest health snapshot per game; -* game master runtime records (`game_id -> engine_endpoint`, - status/turn/scheduling), the engine version registry (`engine_versions`), - per-game player mappings (`game_id, user_id -> race_name, - engine_player_uuid`), and the GM operation log; -* idempotency records, expressed as `UNIQUE` constraints on the durable - table — not as a separate kv; -* retry scheduling state, expressed as a `next_attempt_at` column on the - durable table and worked off via `SELECT ... FOR UPDATE SKIP LOCKED`. - -Redis is the source of truth for ephemeral and runtime-coordination state: - -* the platform event bus implemented as Redis Streams (`user:domain_events`, - `user:lifecycle_events`, `gm:lobby_events`, `runtime:start_jobs`, - `runtime:stop_jobs`, `runtime:job_results`, `runtime:health_events`, - `notification:intents`, `gateway:client-events`, `mail:delivery_commands`); -* stream consumer offsets; -* gateway session cache, replay reservations, rate-limit counters, and - short-lived runtime locks/leases (e.g. notification `route_leases`, - runtime manager per-game operation leases `rtmanager:game_lease:{game_id}`); -* `Auth / Session Service` challenges and active session tokens, which are - TTL-bounded and where loss is recoverable by re-authentication; -* lobby per-game runtime aggregates that are deleted at game finish - (`game_turn_stats`, `gap_activated_at`, capability evaluation marker). - -### Database topology - -* Single PostgreSQL database `galaxy`. -* Schema per service: `user`, `mail`, `notification`, `lobby`, `rtmanager`, - `gamemaster`. Reserved for future use: `geoprofile`. Not allocated unless - needed: `gateway`, `authsession`. -* Each service connects with its own PostgreSQL role whose grants are - restricted to its own schema (defense-in-depth). -* Authentication is username + password only. `sslmode=disable`. No client - certificates and no SCRAM channel binding. -* Each service connects to one primary plus zero-or-more read-only - replicas. Only the primary is used in this iteration; the replica pool - is wired but receives no traffic. Future read-routing is a non-breaking - change. - -### Redis topology - -* Each service connects to one master plus zero-or-more replicas. -* All connections require a password. `USERNAME`/ACL is not used. TLS is - off. -* Only the master is used in this iteration; the replica list is wired but - unused. Failover/read routing is added later without a config break. -* The legacy env vars `*_REDIS_TLS_ENABLED` and `*_REDIS_USERNAME` are - removed without a backward-compat shim. - -### Library stack and migration discipline - -* Driver: `github.com/jackc/pgx/v5`, exposed as `*sql.DB` via - `github.com/jackc/pgx/v5/stdlib` so it is consumable by query builders - written against `database/sql`. -* Query layer: `github.com/go-jet/jet/v2` (PostgreSQL dialect). Generated - code lives under each service `internal/adapters/postgres/jet/`, - regenerated by a per-service `make jet` target (testcontainers + goose + - jet) and committed to the repo so consumers don't need Docker just to - build. -* Migrations: `github.com/pressly/goose/v3` library API. Migration files - are embedded via `//go:embed *.sql`, applied at service startup before - any listener opens; the service exits non-zero on failure. Files are - forward-only, sequence-numbered, and use the standard `-- +goose Up` / - `-- +goose Down` markers. -* Single-init policy during pre-launch development: each PG-backed - service ships exactly one migration file, `00001_init.sql`, that - represents the full current schema. New tables, columns, and indexes - are added by editing that file directly rather than by appending - `00002_*.sql`, `00003_*.sql`, etc. The trade-off is intentional — - schema clarity beats migration-history granularity while no production - database exists. Once the platform reaches its first production - deploy, future schema evolution switches to additive sequence-numbered - migrations. -* Test infrastructure: `github.com/testcontainers/testcontainers-go` plus - the `modules/postgres` submodule for unit tests and for `make jet`. - -Per-service decision records that capture schema and adapter choices live -at `galaxy//docs/postgres-migration.md`. - -### Timestamp handling - -Every time-valued column in every Galaxy schema is `timestamptz`. The -adapter layer is responsible for ensuring that all `time.Time` values -crossing the SQL boundary carry `time.UTC` as their location. - -* **Writes.** Every `time.Time` parameter bound through `database/sql` - (`ExecContext`, `QueryContext`, `QueryRowContext`) is normalised with - `.UTC()` at the binding site. Optional `*time.Time` columns are bound - through a shared helper (`nullableTime` or equivalent per adapter) that - returns `value.UTC()` when non-nil and SQL `NULL` otherwise. Helper - bindings of `cutoff`, `now`, etc. (retention, schedulers) follow the - same rule even when the input was already produced via - `clock.Now().UTC()` — defensive `.UTC()` calls are intentional and - cheap. -* **Reads.** Every `time.Time` scanned out of PostgreSQL is re-wrapped - with `.UTC()` (directly or via a small helper that mirrors - `nullableTime` for the read path) before it leaves the adapter. The - domain layer therefore never observes a `time.Time` whose location is - anything other than `time.UTC`. -* **Why.** PostgreSQL stores `timestamptz` as UTC at rest, but the Go - driver returns scanned values in `time.Local`. Mixing locations across - the boundary produces inequalities in tests, drift in JSON output, and - comparison bugs against pointer fields. The defensive `.UTC()` rule on - both sides removes that class of bug entirely. - -### Configuration - -For each service `` ∈ { `USERSERVICE`, `MAIL`, `NOTIFICATION`, -`LOBBY`, `RTMANAGER`, `GAMEMASTER`, `GATEWAY`, `AUTHSESSION` }, the Redis -connection accepts: - -* `_REDIS_MASTER_ADDR` (required) -* `_REDIS_REPLICA_ADDRS` (optional, comma-separated) -* `_REDIS_PASSWORD` (required) -* `_REDIS_DB`, `_REDIS_OPERATION_TIMEOUT` - -For PG-backed services (`USERSERVICE`, `MAIL`, `NOTIFICATION`, `LOBBY`, -`RTMANAGER`, `GAMEMASTER`) the Postgres connection accepts: - -* `_POSTGRES_PRIMARY_DSN` (required; - `postgres://:@:5432/galaxy?search_path=&sslmode=disable`) -* `_POSTGRES_REPLICA_DSNS` (optional, comma-separated) -* `_POSTGRES_OPERATION_TIMEOUT`, `_POSTGRES_MAX_OPEN_CONNS`, - `_POSTGRES_MAX_IDLE_CONNS`, `_POSTGRES_CONN_MAX_LIFETIME` - -Stream- and key-shape env vars (`*_REDIS_DOMAIN_EVENTS_STREAM`, -`*_REDIS_LIFECYCLE_EVENTS_STREAM`, `*_REDIS_KEYSPACE_PREFIX`, -`MAIL_REDIS_COMMAND_STREAM`, `NOTIFICATION_INTENTS_STREAM`, -`RTMANAGER_REDIS_START_JOBS_STREAM`, `RTMANAGER_REDIS_STOP_JOBS_STREAM`, -`RTMANAGER_REDIS_JOB_RESULTS_STREAM`, `RTMANAGER_REDIS_HEALTH_EVENTS_STREAM`, -etc.) keep their current names and semantics — they describe stream/key -shapes, not connection topology. - -## Test and Contract Conventions - -The repository follows a small set of cross-service rules for contract -specifications and test doubles. Each rule is captured below with the -rejected alternatives so future services do not re-litigate them. - -### AsyncAPI version: 3.1.0 - -Every AsyncAPI spec in the repository declares `asyncapi: 3.1.0` -(`notification/api/intents-asyncapi.yaml`, -`rtmanager/api/runtime-jobs-asyncapi.yaml`, -`rtmanager/api/runtime-health-asyncapi.yaml`). Operators read the same -shape across services — channel with `address`, separate `operations` -block, `action: send | receive` vocabulary. - -Alternatives rejected: - -* AsyncAPI 2.6.0 — would carry the same information under different - field names (`publish` / `subscribe` blocks living inside the channel) - and the shared YAML walker assertions would not transfer cleanly; -* adding a typed AsyncAPI parser library — no Galaxy service uses one - today; introducing a new dependency for the existing specs would - break the established pattern that all AsyncAPI freeze tests are pure - YAML walkers using `gopkg.in/yaml.v3`. - -The `oneOf`-based polymorphism on the `details` field in -`runtime-health-asyncapi.yaml` is plain JSON Schema and works -identically in 3.1.0; no AsyncAPI-version-specific feature is used. If -`notification/api/intents-asyncapi.yaml` ever moves to a newer major, -every downstream service moves with it as a cross-service contract bump. - -### Contract freeze tests - -OpenAPI freeze tests use `github.com/getkin/kin-openapi/openapi3`. The -library is already a workspace-wide dependency -(`lobby/contract_openapi_test.go`, `game/openapi_contract_test.go`, -`rtmanager/contract_openapi_test.go`). It validates OpenAPI 3.0 -syntactic correctness, exposes a typed AST, and lets assertions reach -operation IDs, schema references, required fields, and enum membership -without a hand-rolled parser. - -AsyncAPI freeze tests use `gopkg.in/yaml.v3` plus a small set of -helpers (`getMapValue`, `getStringValue`, `getStringSlice`, -`getSliceValue`, `getBoolValue`). AsyncAPI 3.1.0 is itself a JSON -Schema document; the freeze tests only need to assert on field paths, -enum membership, required fields, and `$ref` targets — none of which -require type-aware parsing. - -Both freeze tests live at the module root (`package ` next to -`go.mod`) for every service. A subpackage like `/contracts/` -would have to import the service's domain types to share constants, -which would create the exact import cycle the freeze tests are meant -to prevent. - -### Test doubles: `mockgen` for narrow recorder ports, `*inmem` for behavioural fakes - -Test doubles in the repository follow a three-track convention: - -* **Narrow recorder ports** (interfaces whose implementation has no - domain semantics — record calls, return injectable errors, expose - accessor methods) use `go.uber.org/mock` mocks. Examples: - `lobby/internal/ports/{RuntimeManager, IntentPublisher, GMClient, - UserService}`, `rtmanager/internal/ports/DockerClient`, - `rtmanager/internal/api/internalhttp/handlers/{Start,Stop,Restart, - Patch,Cleanup}Service`. `//go:generate` directives live next to the - interface declaration; generated mocks are committed under - `/internal/adapters/mocks/` (or `handlers/mocks/`); the - `make -C mocks` target regenerates them. -* **Behavioural in-memory adapters** (re-implement the production - contract — CAS, domain transitions, monotonic invariants, two-tier - invariants like the Race Name Directory) live under - `/internal/adapters/inmem/` and stay hand-rolled. - Replacing them with `mockgen` would force every consumer site to - script `EXPECT()` chains for behaviour the fake currently handles - automatically, and would lose the cross-implementation parity guarantee. -* **Dead test doubles** with no consumers are deleted on sight. - -Per-test recorder helpers (small structs holding captured slices and -per-test error injection) live **inside the test files that use them** -rather than in a shared `mockrec` / `testfixtures` package. A shared -package would re-create the retired `*stub` convention in a different -namespace; per-test recorders are easy to specialise without polluting -a shared surface. - -`racenameinmem` is a special case: it is also one of two selectable -Race Name Directory backends chosen via -`LOBBY_RACE_NAME_DIRECTORY_BACKEND=stub` (the config token name is -preserved while the package name follows the `*inmem` convention; both -backends pass the shared conformance suite at -`lobby/internal/ports/racenamedirtest/`). - -The maintained `go.uber.org/mock` fork is preferred over the archived -`github.com/golang/mock`. - -## Main End-to-End Flows - -## 1. Public authentication flow - -```mermaid -sequenceDiagram - participant Client - participant Gateway - participant Auth - participant User - participant Mail - participant Redis - - Client->>Gateway: POST send-email-code - Gateway->>Auth: send-email-code - Auth->>User: resolve existing/creatable/blocked - User-->>Auth: decision - Auth->>Mail: send or suppress code - Auth-->>Gateway: challenge_id - Gateway-->>Client: challenge_id - - Client->>Gateway: POST confirm-email-code(time_zone) - Gateway->>Auth: confirm-email-code(time_zone) - Auth->>Auth: validate challenge/code/public key/time_zone - Auth->>User: resolve/create/block with create-only registration context when needed - User-->>Auth: user_id or deny - Auth->>Auth: create device_session - Auth->>Redis: write gateway session projection - Auth->>Redis: publish session lifecycle update - Auth-->>Gateway: device_session_id - Gateway-->>Client: device_session_id -``` - -This preserves the existing gateway/auth contract and the rule that auth is not on the steady-state hot path. - -## 2. Authenticated game/platform request flow - -```mermaid -sequenceDiagram - participant Client - participant Gateway - participant Lobby - participant GM as Game Master - - Client->>Gateway: ExecuteCommand(message_type, payload, signature) - Gateway->>Gateway: verify session, signature, freshness, replay - alt platform-level command - Gateway->>Lobby: verified authenticated command - Lobby-->>Gateway: response - else running-game command - Gateway->>GM: verified authenticated command with game_id - GM-->>Gateway: response - end - Gateway-->>Client: signed response -``` - -## 3. Game creation and pre-start lifecycle - -```mermaid -sequenceDiagram - participant Client - participant Gateway - participant Lobby - participant User - - Client->>Gateway: create/apply/invite/approve/start-preparation commands - Gateway->>Lobby: verified platform command - Lobby->>User: entitlement/limit checks when needed - User-->>Lobby: allow/deny and user metadata - Lobby->>Lobby: update game metadata, roster, schedule, target engine version - Lobby-->>Gateway: response - Gateway-->>Client: signed response -``` - -## 4. Game start flow - -```mermaid -sequenceDiagram - participant Owner as Admin or Private Owner - participant Gateway - participant Lobby - participant Runtime - participant GM as Game Master - participant Engine as Game Engine Container - participant Redis - - Owner->>Gateway: start game - Gateway->>Lobby: verified start command - Lobby->>Lobby: validate ready_to_start and roster - Lobby->>Runtime: async start job - Runtime-->>Redis: runtime job result event - - alt start failed - Lobby->>Lobby: keep failure / starting error state - Lobby-->>Gateway: failure or accepted-then-observed failure path - else container started - Lobby->>Lobby: persist game metadata and runtime binding - Lobby->>GM: sync running-game registration - GM->>Engine: initial engine setup API - GM->>GM: initialize runtime state - GM-->>Lobby: registration result - Lobby->>Lobby: mark game running or paused - end -``` - -Critical rule: -if the container starts but `Lobby` cannot persist metadata, the launch is considered a full failure and the container must be removed. -If metadata is persisted but `Game Master` is unavailable, the game is placed into `paused` and administrators are notified. - -## 5. Running-game command flow - -```mermaid -sequenceDiagram - participant Client - participant Gateway - participant GM as Game Master - participant Lobby - participant Engine - - Client->>Gateway: game-related ExecuteCommand(game_id,...) - Gateway->>GM: verified authenticated command - GM->>GM: check runtime status - GM->>Lobby: resolve/cached-check membership if needed - Lobby-->>GM: membership / permissions - GM->>Engine: game or runtime-admin API call - Engine-->>GM: result - GM-->>Gateway: response payload - Gateway-->>Client: signed response -``` - -## 6. Scheduled turn generation flow - -```mermaid -sequenceDiagram - participant Scheduler as Game Master Scheduler - participant GM as Game Master - participant Engine - participant Lobby - participant Notify as Notification Service - participant Gateway - - Scheduler->>GM: due turn slot reached - GM->>GM: switch runtime_status to generation_in_progress - GM->>Engine: generate next turn - alt generation success - Engine-->>GM: new turn result / maybe finished - GM->>GM: update current_turn and runtime state - GM->>Lobby: sync runtime snapshot - GM->>Notify: publish new-turn intent - Notify->>Gateway: client-facing push events - else generation failed - Engine-->>GM: error / timeout - GM->>GM: mark generation_failed - GM->>Lobby: sync runtime snapshot - GM->>Notify: notify administrators only - end -``` - -Players receive only a lightweight push notification that a new turn exists. -They then request their own per-player game state separately. - -If `force next turn` is used, the next scheduled slot is skipped so that the effective time between turns never becomes shorter than the schedule spacing. - -## 7. Game finish flow - -```mermaid -sequenceDiagram - participant Engine - participant GM as Game Master - participant Lobby - participant Notify as Notification Service - participant Gateway - - Engine->>GM: game finished - GM->>GM: update runtime state - GM->>Lobby: mark platform game finished - Lobby->>Lobby: finalize game record - GM->>Notify: publish game-finished intent - Notify->>Gateway: push user-facing/platform events -``` - -## 8. Geo profile auxiliary flow - -```mermaid -sequenceDiagram - participant Gateway - participant Geo - participant User - participant Auth - - Gateway-->>Geo: async observation(user_id, device_session_id, ip_addr) - Geo->>Geo: derive observed_country and aggregates - alt suspicious multi-country pattern - Geo->>Auth: sync block suspicious session(s) - end - alt declared_country admin change approved later - Geo->>User: sync current declared_country update - end -``` - -This flow is intentionally fail-open relative to gameplay. - -## Separation of Platform Metadata and Engine State - -This distinction is fundamental. - -### Platform-level state - -Owned by `Game Lobby`: - -* who owns the game; -* who is invited; -* who applied; -* who was approved; -* who is currently a platform participant; -* what the schedule is; -* whether the game is public/private; -* whether the game is `draft`, `running`, `paused`, `finished`, etc. as a platform entity. - -### Runtime/operational state - -Owned by `Game Master`: - -* current turn; -* runtime status; -* generation state; -* engine reachability; -* patch state; -* mapping to engine player UUIDs; -* engine version registry; -* operational metadata of the running game. - -### Full game state - -Owned only by the game engine container: - -* actual per-player game state; -* internal mechanics and progression; -* player-visible game state snapshots; -* win/lose logic; -* domain truth of the game world. - -The platform must not attempt to duplicate the full game state outside the engine. - -## Versioning of Game Engines - -Every game runs on one specific game engine version. - -Rules: - -* active games stay on the version with which they were started; -* upgrade during a running game is allowed only as a patch update within the same major/minor line; -* game-engine version management is manual in v1; -* each engine version may carry version-specific engine options; -* `Game Master` owns the engine version registry from v1 — `(version, - image_ref, options, status)` rows live in the `gamemaster` schema and - are managed exclusively through GM's internal REST surface; -* `Game Lobby` resolves `image_ref` synchronously through GM at game start - by calling `GET /api/v1/internal/engine-versions/{version}/image-ref`; - `LOBBY_ENGINE_IMAGE_TEMPLATE` and any Lobby-side template-based - resolution are removed without a backward-compat shim. If GM is - unavailable when Lobby attempts the resolve, the start fails with - `service_unavailable` and `runtime:start_jobs` is never published; -* `Runtime Manager` continues to receive a verbatim `image_ref` from the - start envelope and never resolves engine versions itself. - -## Administrative Access Model - -Two distinct external admin modes exist. - -### System administrator - -Uses a separate admin-facing REST surface via gateway and `Admin Service`. - -System administrator can: - -* manage public games; -* see and operate on all private games; -* inspect platform operational state; -* launch, stop, patch, pause, and monitor games; -* approve/reject participation in public games; -* perform user/game administrative actions. - -### Private-game owner - -Uses the normal authenticated client protocol, not the separate system admin UI. - -Allowed owner-admin actions are limited to the owner’s own private games and include at least: - -* initiate enrollment; -* create and manage user-bound invites inside the system; -* approve/reject applicants; -* start game after enrollment; -* force next turn while running; -* stop game; -* temporarily or permanently remove/block players from that game according to allowed policy. - -These operations use dedicated admin-related `message_type` values in the normal authenticated game/client protocol. - -## Non-Goals - -The architecture intentionally does not try to solve all future concerns now. - -Current non-goals: - -* a separate policy engine; -* automatic billing integration in v1; -* automatic match balancing in v1; -* direct external access to internal services; -* pushing full per-player game state over notification channels; -* allowing game engine containers to be called directly by clients or by services other than `Game Master`; -* using `Auth / Session Service` as a hot synchronous dependency for all authenticated traffic; -* making `Notification Service` the source of truth for notification preferences in v1. - -## Recommended Order of Service Implementation - -Recommended order for implementation is: - -1. **Edge Gateway Service** (implemented) - First public ingress, transport boundary, authentication boundary, signed request/response model, push delivery, session cache, replay protection. - -2. **Auth / Session Service** (implemented) - Public auth flow, `device_session`, revoke/block lifecycle, gateway session projection. - -3. **User Service** (implemented) - Regular-user identity, profile/settings, tariffs/entitlements, user limits, sanctions, and current `declared_country`. - -4. **Mail Service** (implemented) - Internal email delivery for auth codes and platform notification mail. - -5. **Notification Service** (implemented) - Unified async delivery of push and non-auth email notifications, with - real Gateway and Mail Service boundary coverage. - -6. **Game Lobby Service** (implemented) - Platform game records, membership, invites, applications, approvals, schedules, user-facing lists, pre-start lifecycle. - -7. **Runtime Manager** (implemented) - Dedicated Docker-control service for container lifecycle (start, stop, - restart, semver-patch, cleanup) and inspect/health monitoring through - Docker events, periodic inspect, and active HTTP probes. Driven - asynchronously from `Game Lobby` via `runtime:start_jobs` / - `runtime:stop_jobs` and synchronously from `Game Master` and - `Admin Service` via the trusted internal REST surface. - -8. **Game Master** - Single-instance running-game orchestrator. Owns the runtime state - (`game_id → engine_endpoint`, status, current turn, scheduling, engine - health), the engine version registry consumed synchronously by - `Game Lobby` for `image_ref` resolution, and the platform mapping - `(user_id, race_name, engine_player_uuid)` per running game. Drives - the turn scheduler with the force-next-turn skip rule, mediates every - engine HTTP call (admin paths under `/api/v1/admin/*`, player paths - under `/api/v1/{command, order, report}`), and reacts to - `StateResponse.finished` by transitioning the runtime to `finished` and - publishing `game_finished`. Drives `Runtime Manager` synchronously over - REST for stop, restart, and patch; consumes `runtime:health_events` - from RTM; publishes `gm:lobby_events` (event-only, no heartbeat) and - `notification:intents`. Never opens the Docker SDK. - -9. **Admin Service** - Admin UI backend that orchestrates trusted APIs of other services. - -10. **Geo Profile Service** (planned) - Auxiliary geo aggregation, review recommendation, suspicious-session blocking, declared-country workflow. - -11. **Billing Service** - Future payment and subscription source feeding entitlements into `User Service`. - -This order gives the platform a usable public perimeter first, then identity/auth, then core gameplay lifecycle, then runtime orchestration, and only afterward secondary auxiliary services. diff --git a/CLAUDE.md b/CLAUDE.md index 2f81856..bf731d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,19 +30,29 @@ This repository hosts the Galaxy Game project. - `galaxy//PLAN.md` — staged implementation plan for the service. May be already complete and resides for historical reasons. -- `galaxy//docs/` — per-stage decision records - (one file per decision, re-organized after full implementation - of `PLAN.md`). +- `galaxy//docs/` — live topic-based documentation that's + deeper than what fits in `README.md` (per-feature design notes, + protocol specs, runbooks). Not stage-by-stage history. -## Decision records when implementing stages from PLAN.md +## Decisions during stage implementation -- Stage-related discussion and decisions do NOT live in `README.md` or - `docs/ARCHITECTURE.md`. Those files describe the current state, not the history. -- Each non-trivial decision gets its own `.md` under the module's `docs/`, - referenced from the relevant `README.md`. -- Any agreement reached during interactive planning that is not obvious from - the code must be captured — either as a decision record or as an entry in - the module's README. +Stages from `PLAN.md` produce decisions. Those decisions never live in a +separate per-decision history file. Instead, every non-obvious decision is +baked back into the live state in three places: + +1. **The plan itself.** Update the relevant stage's text, acceptance + criteria, or targeted tests so it reflects what was decided. If + earlier already-implemented stages need to follow the new agreement, + correct their code, tests, and live docs in the same patch. +2. **Later, not-yet-implemented stages.** When a decision affects later + stages — scope, dependencies, deliverables, or tests — update those + stages now, do not leave the future to re-derive them. +3. **Live documentation.** Module `README.md`, project + `docs/ARCHITECTURE.md`, `docs/FUNCTIONAL.md` (with its + `docs/FUNCTIONAL_ru.md` mirror), the affected service `openapi.yaml` + or `*.proto`, and any topic doc under `galaxy//docs/` that + the decision touches. `README.md` and `ARCHITECTURE.md` always + describe current state, not the history of how it was reached. ## Scope of PLAN.md changes @@ -82,8 +92,8 @@ details. The same behaviour is described in several parallel sources: code, `docs/ARCHITECTURE.md`, `docs/FUNCTIONAL.md` (with its Russian mirror `docs/FUNCTIONAL_ru.md`), the affected service `README.md`, the -relevant `openapi.yaml` or `*.proto`, and the per-stage decision -records under `galaxy//docs/`. They must never disagree. +relevant `openapi.yaml` or `*.proto`, and the topic-based docs under +`galaxy//docs/`. They must never disagree. - Any patch that changes user-visible behaviour, an API contract, or a cross-service flow updates every affected source in the same change @@ -103,6 +113,22 @@ records under `galaxy//docs/`. They must never disagree. `docs/FUNCTIONAL_ru.md` (translate only the touched paragraphs). Skipping the mirror is treated as an incomplete patch. +## Code compactness + +- Prefer compact code over speculative universality. Three similar + occurrences are not yet a pattern — wait for the third real caller + before extracting an abstraction. +- Do not add seams, hooks, or configuration knobs for hypothetical + future requirements. If the next stage of `PLAN.md` will need + something, the next stage will add it. +- A bug fix does not need surrounding cleanup; a one-shot operation + does not need a helper function; a single concrete value does not + need a parameter. +- When the plan can be satisfied by reusing an existing function or + type, do that instead of introducing a new one. +- This rule is about scope, not laziness — well-named identifiers, + precise types, and full test coverage stay non-negotiable. + ## Dependencies - Before adding a new module, check its upstream repository for the latest diff --git a/backend/PLAN.md b/backend/PLAN.md deleted file mode 100644 index 4aa3c06..0000000 --- a/backend/PLAN.md +++ /dev/null @@ -1,868 +0,0 @@ -# backend — Implementation Plan - -This plan has been already implemented and stays here for historical reasons. - -It should NOT be threated as source of truth for service functionality. - ---- - -## Summary - -This plan is the technical specification for implementing the -consolidated Galaxy `backend` service. It is read together with -`../docs/ARCHITECTURE.md` (architecture and security model) and -`README.md` (module layout, configuration, operations). - -After reading those two documents and this plan, an implementing -engineer should not need to ask architectural questions. Every stage is -self-contained inside its domain area; stages run in order; each stage -has explicit Critical files. - -The plan does not invent new domain concepts. It catalogues the work -required to assemble what the architecture document already defines. - -## ~~Stage 1~~ — Repository cleanup - -This stage was implemented and marked as done. - -Goal: remove every module whose responsibility moves into `backend`, -and prepare the workspace for the new module. - -Actions: - -1. `git rm -r authsession/ lobby/ mail/ notification/ gamemaster/ - rtmanager/ geoprofile/ user/ integration/ pkg/redisconn/ - pkg/notificationintent/`. -2. Edit `go.work`: - - Remove `use` lines for the deleted modules. - - Remove `replace` lines for `galaxy/redisconn` and - `galaxy/notificationintent`. - - Do not add `./backend` yet — the module is created in Stage 2. -3. Confirm that surviving modules still build: - `go build ./gateway/... ./game/... ./client/... ./pkg/...`. - Any compile error here means a surviving module imported a - removed package and must be patched (the only realistic culprit is - `gateway`, which references `pkg/redisconn` and the deleted streams; - patches there belong to Stage 6, not Stage 1 — for Stage 1 it is - acceptable to leave gateway broken if and only if the only failures - come from imports of removed packages). -4. Run `go vet ./pkg/...` and confirm no diagnostic. - -Out of scope: any code change inside surviving modules. Stage 1 is -purely deletion plus `go.work` edits. - -Critical files: - -- `go.work` -- the deletion of `authsession/`, `lobby/`, `mail/`, `notification/`, - `gamemaster/`, `rtmanager/`, `geoprofile/`, `user/`, `integration/`, - `pkg/redisconn/`, `pkg/notificationintent/`. - -Done criteria: - -- `git status` shows only deletions plus the `go.work` edit. -- `go build ./pkg/...` is clean. -- `go vet ./pkg/...` is clean. - -## ~~Stage 2~~ — Backend skeleton & shared infrastructure - -This stage was implemented and marked as done. - -Goal: stand up the new module with its boot path, configuration, -telemetry, logger, HTTP listener, Postgres pool, and gRPC listener — all -with empty handlers. After this stage `go run ./backend/cmd/backend` -must boot to a state where probes return 200 and migrations run (with an -empty migration file). - -Actions: - -1. Create `backend/go.mod` with module path `galaxy/backend` and Go - version matching `go.work`. Add direct dependencies: - `github.com/gin-gonic/gin`, `github.com/jackc/pgx/v5`, - `github.com/go-jet/jet/v2`, `github.com/pressly/goose/v3`, - `go.uber.org/zap`, `go.opentelemetry.io/otel` and the OTLP - trace/metric exporters used by other services, and the `galaxy/*` - pkg modules (`postgres`, `model`, `geoip`, `cronutil`, `error`, - `util`). -2. Add `./backend` to `go.work` `use(...)`. -3. `backend/cmd/backend/main.go` — boot order: - 1. Load `config.LoadFromEnv()`; `cfg.Validate()`. - 2. Initialise telemetry (`telemetry.NewProcess(cfg.Telemetry)`). Set - global tracer and meter providers. - 3. Construct the zap logger; inject trace fields helper. - 4. Open Postgres pool. Apply embedded migrations with goose. Fail - fast on any error. - 5. Construct module wiring (empty for now; populated in Stage 5). - 6. Start the HTTP server (gin engine with empty route groups, plus - `/healthz` and `/readyz`). - 7. Start the gRPC push server (no streams accepted yet — Stage 6). - 8. Block on `signal.NotifyContext(ctx, SIGINT, SIGTERM)`; on signal, - drain in the order described in `README.md` §16. -4. `backend/internal/config/config.go` — env-loader following the - pattern used by surviving services. Cover every variable listed in - `README.md` §4. Provide `DefaultConfig()` and `Validate()`. -5. `backend/internal/telemetry/runtime.go` — port the existing service - pattern verbatim: configurable OTLP gRPC/HTTP exporter, optional - stdout exporter, Prometheus pull endpoint when configured. Expose - `TraceFieldsFromContext(ctx) []zap.Field`. -6. `backend/internal/server/server.go` — gin engine, three empty route - groups, request id middleware, panic recovery middleware, otel - middleware. Probe handlers in `server/probes.go`. -7. `backend/internal/postgres/pool.go` — pgx pool factory using the - shared `galaxy/postgres` helper. -8. `backend/internal/postgres/migrations/00001_init.sql` — empty file - containing the `-- +goose Up` and `-- +goose Down` markers and a - single `CREATE SCHEMA IF NOT EXISTS backend;` statement so the - migration is non-empty and can be verified. -9. `backend/internal/postgres/migrations/embed.go` — `embed.FS` and - exported `Migrations() fs.FS` helper. -10. `backend/internal/push/server.go` — gRPC server skeleton bound to - `cfg.GRPCPushListenAddr`. No service registered yet. -11. `backend/Makefile` — at minimum a `jet` target stub that prints - "not generated yet"; will be filled in Stage 4. - -Critical files: - -- `backend/go.mod`, `go.work` -- `backend/cmd/backend/main.go` -- `backend/internal/config/config.go` -- `backend/internal/telemetry/runtime.go` -- `backend/internal/server/server.go`, `backend/internal/server/probes.go` -- `backend/internal/postgres/pool.go`, - `backend/internal/postgres/migrations/00001_init.sql`, - `backend/internal/postgres/migrations/embed.go` -- `backend/internal/push/server.go` -- `backend/Makefile` - -Done criteria: - -- `go build ./backend/...` is clean. -- `go run ./backend/cmd/backend` starts, applies the placeholder - migration, opens HTTP and gRPC listeners, and serves `/healthz` 200 - and `/readyz` 200. -- Telemetry output (stdout exporter) shows trace and metric activity on - a probe hit. - -## ~~Stage~~ 3 — API contract & routing - -This stage was implemented and marked as done. - -Goal: define the entire backend REST contract in `openapi.yaml` and -register every handler as a placeholder that returns -`501 Not Implemented`. Wire the middleware stack for each route group. -The contract test suite must validate every endpoint round-trip against -the OpenAPI document and pass on the placeholders. - -Actions: - -1. Author `backend/openapi.yaml` — single document with three tags - (`Public`, `User`, `Admin`) and the endpoint set below. Reuse - schemas from `pkg/model` where possible; keep the rest under - `components/schemas/*`. -2. Implement middleware in `backend/internal/server/middleware/`: - - `requestid` — assigns and propagates a request id (Stage 2 may - have already done this; consolidate here). - - `logging` — emits an access log entry with trace fields. - - `metrics` — counters and histograms per route group. - - `panicrecovery` — converts panics to 500 with structured logging. - - `userid` — required on `/api/v1/user/*`. Reads `X-User-ID`, - parses as UUID, places it in the request context. Rejects with - 400 if missing or malformed. Backend trusts the value (see - architecture trust note). - - `basicauth` — required on `/api/v1/admin/*`. Stage 3 uses a stub - verifier that accepts any non-empty username and a fixed password - read from a test-only env var so contract tests can pass; Stage - 5.3 replaces the verifier with the real Postgres-backed one. -3. Implement handlers per endpoint in - `backend/internal/server/handlers__.go`. Every handler - returns `501 Not Implemented` with the standard error body - `{"error":{"code":"not_implemented","message":"..."}}`. -4. Implement the contract test: - `backend/internal/server/contract_test.go`. Loads - `backend/openapi.yaml` via `kin-openapi`, builds the gin engine, - walks every operation, sends a representative request, and - validates both the request and response against the OpenAPI - document. -5. Document `openapi.yaml` location and contract test pattern in - `backend/docs/api-contract.md` (a brief decision record). - -### Endpoint inventory - -Public (`/api/v1/public/*`): - -- `POST /auth/send-email-code` — request body `{email, locale?}`; - response `{challenge_id}`. -- `POST /auth/confirm-email-code` — request body - `{challenge_id, code, client_public_key, time_zone}`; response - `{device_session_id}`. - -Probes (root): - -- `GET /healthz` — `200` always when the process is alive. -- `GET /readyz` — `200` once Postgres reachable, migrations applied, - gRPC listener bound; `503` otherwise. - -User (`/api/v1/user/*`, all require `X-User-ID`): - -- `GET /account` — current account view (profile + settings + - entitlements). -- `PATCH /account/profile` — update mutable profile fields - (`display_name`). -- `PATCH /account/settings` — update `preferred_language`, `time_zone`. -- `POST /account/delete` — soft delete; cascade is in process. - -- `GET /lobby/games` — public list with paging. -- `POST /lobby/games` — create. -- `GET /lobby/games/{game_id}`. -- `PATCH /lobby/games/{game_id}`. -- `POST /lobby/games/{game_id}/open-enrollment`. -- `POST /lobby/games/{game_id}/ready-to-start`. -- `POST /lobby/games/{game_id}/start`. -- `POST /lobby/games/{game_id}/pause`. -- `POST /lobby/games/{game_id}/resume`. -- `POST /lobby/games/{game_id}/cancel`. -- `POST /lobby/games/{game_id}/retry-start`. -- `POST /lobby/games/{game_id}/applications`. -- `POST /lobby/games/{game_id}/applications/{application_id}/approve`. -- `POST /lobby/games/{game_id}/applications/{application_id}/reject`. -- `POST /lobby/games/{game_id}/invites`. -- `POST /lobby/games/{game_id}/invites/{invite_id}/redeem`. -- `POST /lobby/games/{game_id}/invites/{invite_id}/decline`. -- `POST /lobby/games/{game_id}/invites/{invite_id}/revoke`. -- `GET /lobby/games/{game_id}/memberships`. -- `POST /lobby/games/{game_id}/memberships/{membership_id}/remove`. -- `POST /lobby/games/{game_id}/memberships/{membership_id}/block`. - -- `GET /lobby/my/games`. -- `GET /lobby/my/applications`. -- `GET /lobby/my/invites`. -- `GET /lobby/my/race-names`. - -- `POST /lobby/race-names/register` — promote a `pending_registration` - to `registered` within the 30-day window. - -- `POST /games/{game_id}/commands` — proxy to engine command path. -- `POST /games/{game_id}/orders` — proxy to engine order validation. -- `GET /games/{game_id}/reports/{turn}` — proxy to engine report path. - -Admin (`/api/v1/admin/*`, all require Basic Auth): - -- `GET /admin-accounts`, `POST /admin-accounts`, - `GET /admin-accounts/{username}`, - `POST /admin-accounts/{username}/disable`, - `POST /admin-accounts/{username}/enable`, - `POST /admin-accounts/{username}/reset-password`. - -- `GET /users`, `GET /users/{user_id}`, - `POST /users/{user_id}/sanctions`, - `POST /users/{user_id}/limits`, - `POST /users/{user_id}/entitlements`, - `POST /users/{user_id}/soft-delete`. - -- `GET /games`, `GET /games/{game_id}`, - `POST /games/{game_id}/force-start`, - `POST /games/{game_id}/force-stop`, - `POST /games/{game_id}/ban-member`. - -- `GET /runtimes/{game_id}`, - `POST /runtimes/{game_id}/restart`, - `POST /runtimes/{game_id}/patch`, - `POST /runtimes/{game_id}/force-next-turn`, - `GET /engine-versions`, `POST /engine-versions`, - `PATCH /engine-versions/{id}`, - `POST /engine-versions/{id}/disable`. - -- `GET /mail/deliveries`, - `GET /mail/deliveries/{delivery_id}`, - `GET /mail/deliveries/{delivery_id}/attempts`, - `POST /mail/deliveries/{delivery_id}/resend`, - `GET /mail/dead-letters`. - -- `GET /notifications`, `GET /notifications/{notification_id}`, - `GET /notifications/dead-letters`, - `GET /notifications/malformed`. - -- `GET /geo/users/{user_id}/countries` — counter listing. - -Internal (gateway-only, `/api/v1/internal/*`): - -- `GET /sessions/{device_session_id}` — gateway session lookup. -- `POST /sessions/{device_session_id}/revoke` — admin or self revoke - passthrough; backend emits `session_invalidation`. -- `POST /sessions/users/{user_id}/revoke-all`. -- `GET /users/{user_id}/account-internal` — server-to-server fetch - used by gateway flows that need account state alongside the session. - -The internal group is on `/api/v1/internal/*`. The trust model treats -it as part of the user surface (no extra auth in MVP). - -Critical files: - -- `backend/openapi.yaml` -- `backend/internal/server/router.go` -- `backend/internal/server/middleware/{requestid,logging,metrics,panicrecovery,userid,basicauth}.go` -- `backend/internal/server/handlers_*.go` -- `backend/internal/server/contract_test.go` -- `backend/docs/api-contract.md` - -Done criteria: - -- `go test ./backend/internal/server/...` is green; the contract test - exercises every endpoint and validates against `openapi.yaml`. -- Every endpoint returns `501 Not Implemented` with the standard error - body. -- gin route table at startup matches the OpenAPI inventory exactly. - -## ~~Stage 4~~ — Persistence layer - -This stage was implemented and marked as done. - -Goal: define every `backend` schema table, generate jet code, and make -the wiring of the persistence layer ready for the domain modules. - -Actions: - -1. Replace `backend/internal/postgres/migrations/00001_init.sql` with - the full DDL. The schema is `backend`. The expected tables and - their primary purposes: - - Auth: - - `device_sessions(device_session_id uuid pk, user_id uuid not null, - client_public_key bytea not null, status text not null, - created_at, revoked_at, last_seen_at)` plus indexes on - `user_id` and `status`. - - `auth_challenges(challenge_id uuid pk, email text not null, - code_hash bytea not null, created_at, expires_at, consumed_at, - attempts int not null default 0)`. Index on `email`. - - `blocked_emails(email text pk, blocked_at, reason text)`. - - User: - - `accounts(user_id uuid pk, email text unique not null, - user_name text unique not null, display_name text not null, - preferred_language text not null, time_zone text not null, - declared_country text, permanent_block bool not null default false, - created_at, updated_at, deleted_at)`. - - `entitlement_records(record_id uuid pk, user_id uuid not null, - tier text not null, source text not null, created_at)`. - - `entitlement_snapshots(user_id uuid pk, tier text not null, - max_registered_race_names int not null, taken_at timestamptz)`. - Updated on every entitlement change. - - `sanction_records`, `sanction_active`, `limit_records`, - `limit_active` — same shape as the previous `user` service had - (record + active rollup pattern). - - Admin: - - `admin_accounts(username text pk, password_hash bytea not null, - created_at, last_used_at, disabled_at)`. - - Lobby: - - `games(game_id uuid pk, owner_user_id uuid not null, - visibility text not null, status text not null, ...)` covering - enrollment state machine fields documented in - `ARCHITECTURE_deprecated.md` § Game Lobby. - - `applications(application_id uuid pk, game_id uuid not null, - applicant_user_id uuid not null, status text not null, ...)`. - - `invites(invite_id uuid pk, game_id uuid not null, - invited_user_id uuid, code text unique, status text, ...)`. - - `memberships(membership_id uuid pk, game_id uuid not null, - user_id uuid not null, race_name text not null, status text, - ...)` plus `unique(game_id, user_id)`. - - `race_names(name text not null, canonical text not null, - status text not null, owner_user_id uuid, game_id uuid, - expires_at, registered_at, ...)` plus - `unique(canonical) where status in ('registered','reservation','pending_registration')`. - - Runtime: - - `runtime_records(game_id uuid pk, current_container_id text, - status text not null, image_ref text, started_at, last_observed_at, - ...)`. - - `engine_versions(version text pk, image_ref text not null, - enabled bool not null default true, created_at, ...)`. - - `player_mappings(game_id uuid not null, user_id uuid not null, - race_name text not null, engine_player_uuid uuid not null, - primary key(game_id, user_id))`. - - `runtime_operation_log(operation_id uuid pk, game_id uuid, - op text, status text, started_at, finished_at, error text)`. - - `runtime_health_snapshots(snapshot_id uuid pk, game_id uuid, - observed_at, payload jsonb)`. - - Mail: - - `mail_deliveries(delivery_id uuid pk, template_id text not null, - idempotency_key text not null, status text not null, - attempts int not null default 0, next_attempt_at timestamptz, - payload_id uuid not null, created_at, ...)` plus - `unique(template_id, idempotency_key)`. - - `mail_recipients(recipient_id uuid pk, delivery_id uuid not null, - address text not null, kind text not null)`. - - `mail_attempts(attempt_id uuid pk, delivery_id uuid, attempt_no int, - started_at, finished_at, outcome text, error text)`. - - `mail_dead_letters(dead_letter_id uuid pk, delivery_id uuid, - archived_at, reason text)`. - - `mail_payloads(payload_id uuid pk, content_type text not null, - subject text, body bytea not null)`. - - Notification: - - `notifications(notification_id uuid pk, kind text not null, - idempotency_key text not null, user_id uuid, payload jsonb, - created_at)` plus `unique(kind, idempotency_key)`. - - `notification_routes(route_id uuid pk, notification_id uuid, - channel text not null, status text not null, last_attempt_at, - ...)`. - - `notification_dead_letters(dead_letter_id uuid pk, notification_id - uuid, archived_at, reason text)`. - - `notification_malformed_intents(id uuid pk, received_at, payload - jsonb, reason text)`. - - Geo: - - `user_country_counters(user_id uuid not null, country text not null, - count bigint not null default 0, last_seen_at timestamptz, - primary key(user_id, country))`. - -2. Add `created_at TIMESTAMPTZ DEFAULT now()` to every table; add - `updated_at` and `deleted_at` where the domain reasons in - `ARCHITECTURE_deprecated.md` apply. UTC normalisation is performed - in Go on read and write (the existing `pkg/postgres` helpers cover - this). - -3. `backend/cmd/jetgen/main.go` — port the existing pattern from a - surviving reference (the previous services' `cmd/jetgen` is a good - template; adjust import paths to `galaxy/backend`). The tool spins - up a transient Postgres container, applies the embedded migrations, - and runs `jet -dsn=...` writing into `internal/postgres/jet/`. - -4. `backend/Makefile` — fill in the `jet` target. - -5. Run `make jet` and commit `internal/postgres/jet/`. - -6. Add `backend/internal/postgres/jet/jet.go` — package doc and - `//go:generate` comment pointing to `cmd/jetgen`. - -7. Sanity test in `backend/internal/postgres/migrations_test.go`: - spin up a Postgres testcontainer, apply migrations, assert that - the `backend` schema exists and that every expected table is - present. - -Critical files: - -- `backend/internal/postgres/migrations/00001_init.sql` -- `backend/internal/postgres/jet/**` -- `backend/cmd/jetgen/main.go` -- `backend/Makefile` -- `backend/internal/postgres/migrations_test.go` - -Done criteria: - -- `go test ./backend/internal/postgres/...` is green. -- `make jet` regenerates without diff. -- All tables listed above exist after a fresh migration. - -## ~~Stage 5~~ — Domain implementation - -Goal: implement domain modules in dependency order. After each substage -the backend is functional for the substage's slice of behaviour. The -contract tests from Stage 3 progressively flip from `501` to actual -responses as each substage replaces placeholders. - -Substages run strictly in order. Each substage: - -- Implements package code in `backend/internal//`. -- Replaces the corresponding `501` handler bodies in - `backend/internal/server/handlers_*.go` with real logic that calls - the domain package. -- Adds focused unit and contract coverage for the substage's - endpoints. -- Wires the new package into `backend/cmd/backend/main.go`. - -### ~~5.1~~ — auth - -This substage was implemented and marked as done. See -[`docs/stage05_1-auth.md`](docs/stage05_1-auth.md) for the decisions -taken during implementation. - -Behaviour: - -- `POST /api/v1/public/auth/send-email-code` — generates a challenge, - hashes the code, persists in `auth_challenges`, calls - `mail.EnqueueLoginCode(email, code)`. Returns `{challenge_id}` for - every non-blocked email (existing user, new user, throttled — all - return identical shape; blocked email rejects with 400 only when the - block is permanent). -- `POST /api/v1/public/auth/confirm-email-code` — looks up the - challenge, verifies the code (constant-time), enforces attempt - ceiling, marks consumed, calls `user.EnsureByEmail(email, - preferred_language, time_zone)` to obtain the user_id, stores the - Ed25519 public key, creates a `device_session` row, populates the - in-memory cache, calls - `geo.SetDeclaredCountryAtRegistration(user_id, source_ip)`, and - returns `{device_session_id}`. -- `GET /api/v1/internal/sessions/{device_session_id}` — sync session - lookup for gateway. -- `POST /api/v1/internal/sessions/{device_session_id}/revoke` and - `POST /api/v1/internal/sessions/users/{user_id}/revoke-all` — mark - sessions revoked, evict from in-memory cache, emit - `session_invalidation` push event (Stage 6 wires the actual - emission; until then `auth` calls a no-op publisher injected at - wiring). - -Cache: full session table read at startup; write-through on every -mutation. - -### ~~5.2~~ — user - -This substage was implemented and marked as done. See -[`docs/stage05_2-user.md`](docs/stage05_2-user.md) for the decisions -taken during implementation. - -Behaviour: - -- Account CRUD limited to allowed mutations on profile and settings. -- `EnsureByEmail` and `ResolveByEmail` for `auth`. -- Entitlement records and snapshots; tier downgrades never revoke - already-registered race names. -- Sanctions and limits using the record + active rollup pattern. -- Soft delete: writes `deleted_at` and triggers in-process cascade — - `lobby.OnUserDeleted(user_id)`, `notification.OnUserDeleted(user_id)`, - `geo.OnUserDeleted(user_id)`. Permanent block triggers - `lobby.OnUserBlocked(user_id)`. -- Cache: latest entitlement snapshot per user; warmed on startup; - write-through on entitlement mutation. - -### ~~5.3~~ — admin - -This substage was implemented and marked as done. See -[`docs/stage05_3-admin.md`](docs/stage05_3-admin.md) for the decisions -taken during implementation. - -Behaviour: - -- `admin_accounts` CRUD with bcrypt hashing. -- Bootstrap on startup via env vars (`BACKEND_ADMIN_BOOTSTRAP_USER`, - `BACKEND_ADMIN_BOOTSTRAP_PASSWORD`); idempotent. -- Replace the Stage 3 stub `basicauth` middleware with the real - Postgres-backed verifier. Constant-time comparison via bcrypt. -- Admin CRUD endpoints across users, games, runtime, mail, - notification, geo. Each admin endpoint delegates to the domain - package's admin-facing methods. - -Cache: full admin table at startup; write-through on mutation. - -### ~~5.4~~ — lobby - -This substage was implemented and marked as done. See -[`docs/stage05_4-lobby.md`](docs/stage05_4-lobby.md) for the decisions -taken during implementation. - -Behaviour: - -- Games CRUD with the enrollment state machine. -- Applications and invites with their lifecycles. -- Memberships with race name binding. -- Race Name Directory: registered, reservation, and - pending_registration tiers; canonical key via `disciplinedware/go-confusables`; - uniqueness across all three tiers; capability promotion based on - `max_planets > initial AND max_population > initial` from the - runtime snapshot. -- Pending-registration sweeper: scheduled job, releases entries past - the 30-day window; uses `pkg/cronutil`. The same sweeper auto-closes - enrollment-expired games whose `approved_count >= min_players`. -- Hooks consumed from other modules: - - `OnUserBlocked(user_id)` — release all RND/applications/invites/ - memberships in one transaction. - - `OnUserDeleted(user_id)` — same. - - `OnRuntimeSnapshot(snapshot)` — update denormalised runtime view - on the game (current_turn, status, per-member max stats). - - `OnGameFinished(game_id)` — drive race name promotion logic and - move game to `finished`. - -Cache: active games and memberships, RND canonical set; warmed on -startup; write-through on mutation. - -### ~~5.5~~ — runtime (with dockerclient and engineclient) - -This substage was implemented and marked as done. See -[`docs/stage05_5-runtime.md`](docs/stage05_5-runtime.md) for the -decisions taken during implementation. - -Behaviour: - -- Engine version registry CRUD. -- `engineclient` is a thin `net/http` client over `pkg/model` types, - one method per engine endpoint listed in `README.md` §8. -- `dockerclient` wraps `github.com/docker/docker` for: pull, create, - start, stop, remove, inspect, list (filtered by the - `galaxy.backend=1` label), patch (semver-only, validated against - `engine_versions`). -- Per-game serialisation: a `sync.Map[game_id]*sync.Mutex` ensures - concurrent ops on the same game are sequential. -- Worker pool for long-running operations: started in Stage 5.5; jobs - enqueued on a buffered channel; bounded concurrency. -- `runtime_operation_log` records every op (start time, finish time, - outcome, error). -- Reconciliation: on startup and on a `pkg/cronutil` schedule, list - containers labelled `galaxy.backend=1`, match against - `runtime_records`, adopt unrecorded labelled containers, mark - recorded but missing as removed. Emit - `lobby.OnRuntimeJobResult` for each removed. -- Snapshot publication: after every successful engine read or a - health-probe transition, synthesise a snapshot and call - `lobby.OnRuntimeSnapshot(snapshot)` synchronously. -- Turn scheduler: `pkg/cronutil` schedule per running game; each tick - invokes the engine `admin/turn`, on success snapshots and publishes; - force-next-turn sets a one-shot skip flag stored in - `runtime_records`. - -Cache: active runtime records, engine version registry; warmed on -startup; write-through on mutation. - -### ~~5.6~~ — mail - -This substage was implemented and marked as done. See -[`docs/stage05_6-mail.md`](docs/stage05_6-mail.md) for the decisions -taken during implementation. - -Behaviour: - -- Outbox tables defined in Stage 4. -- Worker goroutine: scans `mail_deliveries` with - `SELECT ... FOR UPDATE SKIP LOCKED` ordered by `next_attempt_at`, - attempts SMTP delivery via `wneessen/go-mail`, records in - `mail_attempts`, updates status, schedules backoff with jitter, or - dead-letters past the configured maximum attempts. -- Drain on startup: replays all `pending` and `retrying` rows. -- Public API for producers: `EnqueueLoginCode(email, code, ttl)`, - `EnqueueTemplate(template_id, recipient, payload, idempotency_key)`. -- Admin endpoints implemented: list, view, resend. - -### ~~5.7~~ — notification - -This substage was implemented and marked as done. See -[`docs/stage05_7-notification.md`](docs/stage05_7-notification.md) for -the decisions taken during implementation. - -Behaviour: - -- `Submit(intent)` — validate intent shape, enforce idempotency, - persist `notifications`, materialise `notification_routes`, fan out - to push (Stage 6 wires the actual push emission; until then a no-op - publisher) and email (`mail.EnqueueTemplate`). -- Each kind has a fixed channel set documented in `README.md` §10. -- Malformed intents go to `notification_malformed_intents` and never - block the producer. -- Dead-letter handling: a failed route past max attempts moves to - `notification_dead_letters`. -- Producers (lobby, runtime, geo, auth) are wired via direct function - calls. - -### ~~5.8~~ — geo - -This substage was implemented and marked as done. See -[`docs/stage05_8-geo.md`](docs/stage05_8-geo.md) for the decisions -taken during implementation. - -Behaviour: - -- Load GeoLite2 Country DB at startup from `BACKEND_GEOIP_DB_PATH`. -- `SetDeclaredCountryAtRegistration(user_id, ip)` — sync; lookup, - update `accounts.declared_country`. No-op on lookup error. -- `IncrementCounterAsync(user_id, ip)` — fire-and-forget goroutine; - upsert `user_country_counters` with `count = count + 1`, - `last_seen_at = now()`. -- Middleware on `/api/v1/user/*` extracts the source IP from - `X-Forwarded-For` (or `RemoteAddr`) and calls - `IncrementCounterAsync` after the handler returns successfully. -- `OnUserDeleted(user_id)` — delete the user's counter rows. - -Critical files (Stage 5 as a whole): - -- `backend/internal/auth/**` -- `backend/internal/user/**` -- `backend/internal/admin/**` -- `backend/internal/lobby/**` -- `backend/internal/runtime/**` -- `backend/internal/dockerclient/**` -- `backend/internal/engineclient/**` -- `backend/internal/mail/**` -- `backend/internal/notification/**` -- `backend/internal/geo/**` -- `backend/internal/server/handlers_*.go` (replacing 501 stubs) -- `backend/cmd/backend/main.go` (wiring expansion) - -Done criteria: - -- All Stage 3 contract tests pass against real responses. -- Each substage adds focused unit tests (`testify`, mocks where - external boundaries justify them). -- `go run ./backend/cmd/backend` boots, all caches warm, all workers - start. - -## ~~Stage 6~~ — Push gRPC interface and gateway adaptation - -Goal: stand up the bidirectional control channel between backend and -gateway. Backend pushes `client_event` and `session_invalidation`; -gateway opens the stream, signs and forwards client events, immediately -acts on session invalidations. Remove every Redis dependency from -gateway except anti-replay reservations. - -### ~~6.1~~ — Backend push server - -This substage was implemented and marked as done. See -[`docs/stage06_1-push.md`](docs/stage06_1-push.md) for the decisions -taken during implementation. - -Actions: - -1. Author `backend/proto/push/v1/push.proto` with - `service Push { rpc SubscribePush(GatewaySubscribeRequest) returns - (stream PushEvent); }` and the message types defined in - `README.md` §7. Include a `cursor` field (string). -2. `backend/buf.yaml`, `backend/buf.gen.yaml` mirroring the gateway - pattern; generate Go bindings into `backend/proto/push/v1/`. -3. `backend/internal/push/server.go` — gRPC service implementation: - - Maintains a connection registry keyed by gateway client id (the - `GatewaySubscribeRequest` provides one; if multiple gateway - instances connect, each gets its own queue). - - Holds an in-memory ring buffer keyed by cursor, with TTL equal to - `BACKEND_FRESHNESS_WINDOW`. Cursors past TTL are discarded. - - Resume: if the client's cursor is still in the buffer, replay - from there; otherwise replay nothing and start fresh. - - Backpressure: per-connection buffered channel; on overflow, drop - the oldest events for that connection and log. -4. Provide a publisher API consumed by `auth`, `lobby`, `notification`, - and `runtime`: - - `push.PublishClientEvent(user_id, device_session_id?, payload, kind)`. - - `push.PublishSessionInvalidation(device_session_id|user_id, reason)`. - -### ~~6.2~~ — Gateway adaptation - -This substage was implemented and marked as done. See -[`docs/stage06_2-gateway.md`](docs/stage06_2-gateway.md) for the -decisions taken during implementation. - -Actions: - -1. Remove `redisconn` usage for session projection and for the two - stream consumers. Keep `redisconn` only for anti-replay - reservations. -2. Remove `gateway/internal/config` env vars - `GATEWAY_SESSION_EVENTS_REDIS_STREAM` and - `GATEWAY_CLIENT_EVENTS_REDIS_STREAM`. Add - `GATEWAY_BACKEND_HTTP_URL` and `GATEWAY_BACKEND_GRPC_PUSH_URL`. -3. Add `gateway/internal/backendclient/` with: - - `RESTClient` — HTTP client for `/api/v1/internal/sessions/...` and - for forwarding public/user requests. - - `PushClient` — gRPC client to `SubscribePush` with reconnect - loop, exponential backoff with jitter, and cursor persistence in - process memory. -4. Replace gateway session validation with a sync REST call to - backend per request. -5. Replace gateway client-events Redis consumer with the - `SubscribePush` consumer. On `client_event`: sign envelope (Ed25519) - and deliver to the matching client subscription. On - `session_invalidation`: look up active subscriptions for the target - sessions, close them, and reject any in-flight authenticated - request bound to those sessions. -6. Anti-replay request_id reservations remain in Redis (unchanged). -7. Update gateway tests to use a mocked backend HTTP and gRPC server. - -Critical files: - -- `backend/proto/push/v1/push.proto` -- `backend/buf.yaml`, `backend/buf.gen.yaml` -- `backend/internal/push/server.go`, - `backend/internal/push/publisher.go` -- `gateway/internal/backendclient/*.go` -- `gateway/internal/config/config.go` (env var changes) -- `gateway/internal/handlers/*.go` (route forwarding to backend) -- `gateway/internal/auth/*.go` (session lookup → REST) -- `gateway/internal/eventfanout/*.go` (replace Redis consumer with - gRPC consumer; rename if helpful) - -Done criteria: - -- `go run ./backend/cmd/backend` and `go run ./gateway/cmd/gateway` - cooperate end-to-end with no Redis stream usage. -- A revocation through the admin surface causes immediate stream - closure on the affected client. -- Gateway anti-replay still rejects duplicates. -- gateway test suite green. - -## ~~Stage 7~~ — Integration testing - -This stage was implemented and marked as done. See -[`docs/stage07-integration.md`](docs/stage07-integration.md) for the -decisions taken during implementation, including the testenv layout, -the signed-envelope gRPC client, and the per-scenario coverage notes. - -Goal: end-to-end coverage of the platform with real binaries and real -infrastructure where practical. - -Actions: - -1. Recreate the top-level `integration/` module, registered in - `go.work`. The module hosts black-box test suites that drive - `gateway` from outside and verify behaviour at the public boundary - (with `backend` and `game` running in containers). -2. Add testcontainers fixtures: Postgres, an SMTP capture server (for - example `axllent/mailpit`), the `galaxy/game` engine image, the - `galaxy/backend` image (built from this repo), and the - `galaxy/gateway` image. The Docker daemon used by testcontainers - is the same one backend will use to manage engines. -3. Add a synthetic GeoLite2 mmdb (use `pkg/geoip/test-data/`). -4. Cover scenarios: - - Registration flow: send-email-code → confirm-email-code → - `declared_country` populated from synthetic mmdb. - - User account fetch: `X-User-ID` path returns the expected - account; geo counter increments per request. - - Lobby flow: create game → invite → application → ready-to-start - → start (engine container starts, healthz green, status read) → - command → force-next-turn → finish → race name promotion. - - Mail flow: trigger an email-bound notification → SMTP capture - receives it → admin resend works. - - Notification flow: lobby invite triggers a push event reaching - the test client's gateway subscription, plus an email captured - by SMTP. - - Admin flow: bootstrap admin authenticates; CRUD admin creates a - second admin; second admin disables the first. - - Soft delete flow: user soft-delete cascades; their RND entries, - memberships, applications, invites, geo counters are released - or removed. - - Session revocation: admin revokes a session → push - `session_invalidation` arrives at gateway → active subscription - closes; subsequent requests with that `device_session_id` - rejected by gateway. - - Anti-replay: same `request_id` replayed within freshness window - is rejected by gateway. -5. CI: run `go test ./integration/... -tags=integration` (or whichever - flag the team prefers). Tests requiring real Docker run only when - a Docker daemon is available; otherwise they skip with a clear - message. - -Critical files: - -- `integration/go.mod` -- `integration/auth_flow_test.go` -- `integration/lobby_flow_test.go` -- `integration/mail_flow_test.go` -- `integration/notification_flow_test.go` -- `integration/admin_flow_test.go` -- `integration/soft_delete_test.go` -- `integration/session_revoke_test.go` -- `integration/anti_replay_test.go` -- `integration/testenv/*.go` (shared fixtures) - -Done criteria: - -- `go test ./integration/...` runs the full suite. -- All listed scenarios pass green on a developer machine with Docker - available. -- Failures produce actionable diagnostics (logs from each component - attached to the test report). - -## Stage acceptance and decision records - -After each stage, the implementing engineer writes a short decision -record under `backend/docs/stage-.md` capturing any -non-trivial choice made during implementation that is not obvious from -the code or from this plan. Records that contradict this plan must be -brought to the architecture conversation before merge — the plan and -the architecture document are the agreed contract. diff --git a/game/rules.txt b/game/rules.txt new file mode 100644 index 0000000..362cdb9 --- /dev/null +++ b/game/rules.txt @@ -0,0 +1,1448 @@ + "ГАЛАКТИКА" + ("GALAXY GAME") + + + Руководство игрока + + + Популярно про Galaxy + ~~~~~~~~~~~~~~~~~~~~ + + +Galaxy - это многопользовательская стратегическая фантастическая сетевая +компьютерная игра. Каждый из игроков - предводитель расы в Галактике. Цель +игры - управляя своей расой, распространить свою власть на всю Галактику. В +Galaxy играют с давних пор. Многие люди находят ее настолько увлекательной и +интересной, что проводят играя долгие часы. Хотя вся игра может быть сведена +к отсылке нескольких строк с командами на Galaxy-сервер, эти команды +настолько важны для игроков, что над их содержанием игроки проводят большую +часть времени. Кроме того в Galaxy есть возможность общаться с остальными +игроками. А поскольку, настоящие адреса игроков хранятся в тайне, и никто без +желания самих игроков не сможет узнать их настоящий адрес, возникает +возможность ролевого общения, когда каждый из участников действует и говорит +от лица вымышленного им персонажа. Персонажа, имеющего свои черты, свой +характер свои взгляды на жизнь, которые могут не совпадать со взглядами +самого игрока. + + +Словом, Galaxy - это свой мир, настоящая виртуальная реальность. В Galaxy +бурлит настоящая жизнь. То здесь то там слышны слова дружбы и вражды, +проклятий и благодарности, злорадства и мольбы о помощи. На космических +верфях строятся звездные суда самых разнообразных и неимовернейших +характеристик. Развиваются новые планеты, разрабатываются технологии, +осваиваются новые месторождения полезных ископаемых, заключаются союзы и +объявляются войны. На каждом ходу в каждой Галактике совершается несчетное +множество событий. Космические лайнеры совершают перелеты от планеты к +планете, доставляя все необходимое. Рождаются и умирают расы. Нескончаемый +поток писем течет через сервер. Здесь и команды лидеров рас, и личная +переписка игроков, и отчеты, которые генерирует неутомимый сервер, и статьи, +присланные специально для помещения в Галактическую газету. Здесь слезы и +смех, надежды и отчаяния, честность и подлость, коварство и благородство, +беззаветная дружба и расчетливое предательство. Здесь жизнь и смерть. Здесь +почта от разбойников и рыцарей, прекрасных дам и безобразных мутантов, +драконов и насекомых, великанов и карликов, людей и нет. Это Галактика и +здесь есть все! Если вы решили попробовать свои силы в Galaxy, то +приготовьтесь вступить в настоящий мир. Мир, который создан для того, чтобы +люди получили возможность общения на новом, необычном для них уровне. Мир, +где вы, как и в реальной жизни, должны будете отвечать за свои поступки. Мир, +где вы сможете приобрести друзей и врагов. Мир, который может стать для вас +второй жизнью. + + + Процесс игры + ~~~~~~~~~~~~ + +Весь процесс игры разделен на ходы, которые в свою очередь можно разделить на +два чередующихся процесса: "производство хода" и "ожидание производства +нового хода". Считается, что "производство хода" процесс не требующий времени +и условно происходящий мгновенно. В этот момент совершается само действие, +т.е. выполняется процесс производства на планетах, происходят сражения флотов +враждующих рас и передвижение кораблей от планеты к планете +(подробнее см. "Последовательность действий"). + + +Существует также понятие "состояние игры", которое соответствует текущему +положению дел (в том числе, у кого сколько чего есть, кто что делает, какие +корабли куда летят и т.п.). Состояние игры соответствует отчёту, который +получает игрок сразу после производства очередного хода. + + +Производство хода происходит регулярно в заранее установленное время, +известное всем участникам. Он заканчивается рассылкой всем участникам отчетов +с информацией о состоянии их рас на этот ход. После чего начинается ожидание +производства нового хода. Это процесс специально предназначен, чтобы игроки +имели возможность обдумывать команды на будущий ход и тем самым изменять +состояние игры. + + +Каждая из команд может лишь задать действие (например, тип производства на +планете), но не приведёт к моментальному выполнения этого действия. Так, +например, можно дать команду постройки корабля, но строиться сам корабль +будет лишь во время выполнения очередного хода. Или можно дать приказ об +отправке кораблей куда-либо, но полетят они лишь во время производства хода. + + +Когда приказ (последовательность команд) поступает на сервер, игрок получает +уведомление о том, что его команды приняты к производству. Каждая команда из +приказа проверяется на корректность и получает отдельное подтверждение. Игрок +может послать любое количество приказов по своему усмотрению, однако, каждый +новый приказ отменяет предыдущий. Таким образом, можно исправить неверно +составленный приказ, но при этом необходимо повторить те команды, которые +были отданы верно. К счастью, программа-клиент помогает игроку не запутаться +в этом процессе и берёт на себя контроль за целостностью приказов. + + +Во время ожидания производства нового хода можно также производить +дипломатическую переписку между расами. Все ходы нумеруются, чтобы удобнее +было планировать свои действия. + + +Игра начинается с того, что происходит производство хода с номером ноль, во +время которого, собственно, и создается сама галактика (в галактике +размещаются планеты, участники получают свои развитые планеты и т.д.). После +чего всем участникам рассылаются отчеты с информацией об их начальном +состоянии. Получив такой отчет, каждый из участников должен внимательно его +изучить и на его основе разработать (или скорректировать) план развития +собственной расы. Важно отметить, что с момента рассылки отчетов за ход с +номером N и вплоть до рассылки отчетов за ход с номером N+1 длится ход номер +N (происходит ожидание производства нового хода с номером N+1). + + +Для более полного понимания приведем пример работы сервера: + + +- всем участникам рассылаются отчеты за ход с номером N (начался ход номер N); + +- принимаются приказы, рассылаются подтверждения приказов в соответствии с + полученными командами, распространяется дипломатическая почта, новости и + т.п. (продолжается ход номер N - идет "ожидание производства нового хода"); + +- согласно расписанию, обозначенному при наборе в партию, сервер просчитывает + новый ход с номером N+1 (происходит процесс производства хода); + +- всем участникам рассылаются отчеты за ход с номером N+1 (начался ход номер + N+1); + + + Галактика + ~~~~~~~~~ + +Пространство Галактики, где разворачиваются действия, представляет собой +поверхность тора, а визуально это просто квадрат у которого закольцованы +противоположные стороны. Галактика содержит некоторое число звёздных систем. +Несмотря на то, что любая звёздная система может содержать некоторое число +планет и других небесных тел, в рамках игры такая детализация несущественна: +при добыче ресурсов используется суммарные ресурсы всей звёздной системы, а +организации полётов основана на перемещении между центрами масс в галактике. + + +Поэтому для простоты обозначения игра оперирует термином "планета", что на +самом деле могло бы соответствовать понятию "звёздная система". + + + Единицы измерений + ~~~~~~~~~~~~~~~~~ + +Единицы измерений в игре соответствуют действительным. Расстония между +планетами измеряются в световых годах. Размеры планет исчисляются в десятках +километров в диаметре. Каждая единица населения обозначает 10 миллионов +человек и каждая единица товаров, сырья и т.п. представляет собой 10 +миллионов тонн. Каждый ход игры соответствует одному году жизни Галактики. В +большинстве случаев вместо указания реальных единиц, удобнее пользоваться +определением "единица измерения". Например: "5 единиц массы" или "10 единиц +населения". + + + Числовые величины + ~~~~~~~~~~~~~~~~~ + +Числа, используемые в командах и отчетах, представляются с точностью до трёх +десятичных знаков после запятой. Сервер хранит числа и производит вычисления +с большей точностью, округляя результаты лишь для представления игрокам в +отчётах. Например, если в отчете стоит число 2.000, это может означать, что +на самом деле число может колебаться от 1.9995 до 2.0004. + +Исключение составляют технологические уровни кораблей (см. "Технолгии"). + + + Наименования + ~~~~~~~~~~~~ + +Произвольные наименования, выбираемые игроком, могут иметь классы кораблей, +планеты, флоты и науки. Имена не могут быть длиннее 30 символов. Символы +могут быть буквами алфавита, цифрами и спецсимволами '!@#$%^*-_=+~()[]{}'. +Спецсимволы не могут находиться в начале или конце имени а так же повторяться +более двух раза подряд. + + +Выбирая имя, игроку рекомендуется руководствоваться в первую очередь здравым +смыслом и не злоупотреблять возможностью создавать нечитаемые имена. Хоть это +напрямую и не запрещено, но такой способ коммуникации с внешним миром будет +выглядеть не самым лучшим образом в глазах других участников, ведь многие +выбранные игроком имена увидит вся галактика. Не следует использовать в +наименованиях лексику, которая наносит грубые оскорбления кому-либо из +игроков, а так же разжигает межнациональную или межконфессиональную рознь +(разумеется, речь про реальный, не игровой мир). Администрация игры оставляет +за собой право принимать меры, вплоть до исключения игрока с сервера, за +злоупотребление наименованиями. + + + + Планеты + ~~~~~~~ + + +Каждая из рас, начиная игру, владеет одной или тремя планетами (в зависимости +от типа партии), все остальные планеты - необитаемы. В процессе игры +допустимо как заселение необитаемых планет, так и завоевание планет, +заселенных другими расами. + + +В начале игры все планеты имеют уникальные имена. После колонизации +планеты можно изменить ее имя. Вы можете также пожелать изменить имя +Ваших первых планет сразу после начала, чтобы придать им более +интригующий вид. + + +Любая планета имеет две неизменяемые характеристики: размер и природные +ресурсы. В стандартной партии каждый участник в начале игры владеет одной +планетой размером 1000 (такая планета называется "Домашним Миром" или "Home +World", HW) и двумя планетам размером по 500 (такие планеты называются +"Дочерними Мирами" или "Daughter World", DW). HW игроков находятся +на расстоянии не менее 30 световых лет друг от друга, DW - на расстоянии +от 5 до 15 световых лет от HW. + + +Все остальные планеты не обладают жестко установленными характеристиками и +встречаются в галактике в следующих пропорциях: + + +---------- ------ ------- ---------------------- +тип планет размер ресурсы примерное количество + от общего числа планет +---------- ------ ------- ---------------------- + +Супер большие планеты 1500-2500 0-3 6% + +Просто большие планеты 1000-2000 0.1-10 18% + +Обычные планеты 0-1000 0.1-10 50% + +Маленькие, но сказочно богатые 0-500 5-25 18% + +Астероиды 0 0 8% + + +При этом супер большие планеты могут встречаться лишь на расстоянии не менее +20 св. лет от HW и друг от друга; просто большие - 10 св. лет. Как можно +заметить, диапазоны характеристик супер больших планет и просто больших +планет пересекаются. Это не ошибка, а реальность. В среднем на каждого +участника в галактике приходится по 10 планет(включая три начальные). + + +Прочие характеристики планет могут изменяться в течение игры как в результате +действий игроков, так и в результате естественного развития планет. Такие +характеристики включаются в себя: + +- Население +- Колонисты - избыток населения. +- Сырьё +- Промышленность + + + Население + ~~~~~~~~~ + +Каждая планета имеет атрибут "Размер". Он характеризует не только физический +размер планеты, но также обуславливает наличие гор, пустынь и океанов, климат +и т. п. Население планеты не может превышать её размер, но может быть меньше. +Ваша первая планета имеет размер и население равными 1000. Население планеты +увеличивается на 8% на каждом ходу. Часть населения планеты, превышающая ее +размер, превращается в колонистов. Следует помнить, что количество населения +планеты ограничивает рост промышленности (см. ниже), поскольку количество +единиц промышленности не может превышать количество единиц населения. + + + Колонисты + ~~~~~~~~~ + +Каждые 8 единиц населения, превышающих размер планеты, автоматически +превращаются в единицу колонистов. Эти жители сохраняются в контейнерах при +низкой температуре. Если колонисты перевозятся на другие планеты, имеющие +место для расселения, они автоматически размораживаются и добавляются к +населению планеты. Этим способом могут быть обжиты необитаемые планеты. Из +каждой единицы колонистов вновь получается 8 единиц населения. Современные +технологии позволяют производить процесс заморозки и разморозки колонистов +без каких-либо производственных и материальных затрат. + + + Промышленность + ~~~~~~~~~~~~~~ + +Уровень промышленности планеты (или "количество промышленности") соответствует +таким вещам, как инструменты, компьютеры, транспорт и т.п. В начале игры все +ваши планеты имеют максимальный уровень промышленности, равный населению +(1000 для HW и 500 для DW). + + + Производство на планете + ~~~~~~~~~~~~~~~~~~~~~~~ + +Доступные производственные единицы могут быть израсходованы на добычу сырья, +постройку кораблей, производство промышленности или на исследования +технологий. На одной планете за один ход можно производить только однотипный +продукт (например, только один тип кораблей). + + + Производственные единицы + ~~~~~~~~~~~~~~~~~~~~~~~~ + +Каждая заселенная планета имеет определенный производственный потенциал, +выраженные в единицах производства. Он не может превышать население планеты, +но может оказаться меньше. Производственный потенциал определяет уровень +производительности планеты и складывается из производственной мощности +населения и уровня промышленности. + + Производственный потенциал = + промышленность * 0.75 + население * 0.25 + +В начале игры Вы располагаете одной планетой с производственным потенциалом +1000 и двумя планетами с производственным потенциалом по 500. Это означает, +что каждый ход Вы располагаете 1000, 500 и еще 500 единицами производства, +которые Вы можете заставить работать так, как Вам понравится. Если Ваша +планета имеет 500 единиц промышленности и население 1000, то такая планета +может произвести лишь около 625 единиц выбранной продукции за один ход. +Другими словами - Ваша планета имеет производственный потенциал в 625 +производственных единиц. + + + Технологии + ~~~~~~~~~~ + +Вы начинаете с технологическим уровнем 1 в следующих областях: Двигатели +(Drive), Оружие (Weapons), Защита (Shields) и Грузоперевозки (Cargo). Эти +уровни могут быть повышены переключением производства планет на исследования. +Чтобы увеличить технологический уровень на единицу, необходимо затратить 5000 +производственных единиц. Дробные показатели затрат на исследования технологий +непременно будут полезными. Так, если вы затратили 500 единиц на исследования +в области Оружия, Ваш технологический уровень в этой области возрастет на +одну десятую и это даст немедленный эффект при постройке кораблей, нет +необходимости ждать, пока уровень возрастет на целую единицу. В момент +постройки корабля, он получает такие уровни технологий, какие Вы имели на +момент начала производства хода (см. "Последовательность действий"). Уровни +технологий уже построенных кораблей в дальнейшем можно обновить с помощью +команды модернизации. + +В целях избежания неточностей в расчетах, технологические уровни кораблей +округляются до третьего знака после запятой в момент постройки или +модернизации. Поэтому в отчетах у кораблей всегда указываются действительные, +а не округлённые уровни технологий. + +Все начальные планеты игроков по умолчанию заняты исследованием технологии +"Drive". + + + Науки + ~~~~~ + +Вы можете комбинировать технологии в науки. Каждая наука состоит из известных +технологий, взятых в определяемых Вами пропорциях. Когда Вы переключаете +производство на планете на исследования в области определенной Вами науки, +производственные единицы расходуются на те технологии, из которых состоит +данная наука, причем в соответствии с заданной Вами пропорции. Общая сумма +частей различных технологий в каждой науке равна 100% или единице в дробном +исчислении. + +Например, Вы определили науку с именем "First Step", которая состоит из 10 +частей технологии Двигателей, 5 частей технологии Вооружения, 30 частей +технологии Защиты и 0 частей технологии Грузоперевозок. Тогда при изучении +такой науки у Вас 22% доступных производственных единиц планеты будут +израсходованы на разработки в области Двигателей, 11% - на Вооружение и 67% - +на технологию Защиты. Таким образом за один ход на одной планете Вы имеете +возможность повысить сразу несколько технологических уровней. + + + Сырьё (Материалы) + ~~~~~~~~~~~~~~~~~ + +Производство чего-либо, кроме технологий, требует затрат сырья так же, как и +производственных затрат. Сырье соответствует таким материалам, как листовая +сталь, медная проволока, древесина и нефть и т.п., необходимые для +производства. Каждая планета может иметь запас произведённого или +привезённого сырья, которое можно использовать при производстве кораблей. +Если такой запас отсутствует, часть производственных единиц может быть +ориентирована на выпуск сырья. + +Как известно, каждая планета имеет неизменную характеристику - Природные +Ресурсы, которая показывает, насколько планета богата запасами металлов, +угля, нефти и т.п. Планеты с высоким показателем Ресурсов требуют меньших +затрат на производство сырья. Показатель находится в диапазоне от 0.1 до 20, +среднее значение 1.5. Ваши первые планеты имеют показатели ресурсов 10, что +означает, что каждая производственная единица может произвести 10 единиц +сырья. Планета с показателем сырья 0.1 может произвести только 0.1 единиц +сырья на каждую производственную единицу. Произведённое сырьё складируется +и может быть транспортировано на другие планеты с помощью грузовых кораблей. + +Когда Вы колонизируете планеты с низким показателем природных ресурсов, Вам +стоит производить сырье на планетах с высоким показателем и затем перевозить +их на другие планеты, чтобы большое количество производственных единиц не +тратилось на добычу сырья. Количество сырья на планете можно увеличить и +путем демонтажа кораблей, находящихся на планете. В этом случае каждая +единица массы демонтируемых кораблей превращается в единицу сырья. + +Например, Вы ориентировали производство на постройку космических кораблей. Для +постройки требуется количество сырья, эквивалентное массе строящегося +корабля. Если Вы начинаете без запаса сырья, оно будет произведено +автоматически. Этот процесс полностью невидим для Вас, единственный заметный +эффект это то, что кое-где процесс производства будет меньше, чем Вы +ожидаете. + +Другими словами, на постройку корабля останется столько производственных +единиц, сколько может дать данная планета, за минусом количества, которое +будет израсходовано на добычу необходимого для постройки количества сырья. По +этой причине необходимо учитывать, что при постройке кораблей не все +производственные единицы планеты будут непосредственно участвовать в +постройке, некоторым из них придется заняться добычей недостающего для +постройки сырья. На практике это условие является одним из самых важных при +проектировании кораблей, поскольку оно непосредственно определяет время +постройки кораблей определённого класса на выбранной планете. + + + Производство промышленности + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +При развитии планет количество промышленности может быть повышено путём +переключения производства планеты на промышленность. Производство одной +единицы промышленности требует занятости 5 производственных единиц и одной +единицы сырья. + +Когда на планете установлено производство промышленности и производственный +потенциал планеты ниже уровня населения, то производственный потенциал +увеличивается. В противном случае промышленность накапливается и может быть +транспортирована на другие планеты. При перевозке промышленности на планеты, +где производственный потенциал ниже уровня населения, она будет добавлена к +производственному потенциалу планеты и он возрастет. Таким образом происходит +интенсивное развитие колонизируемых планет. + + + Конструирование кораблей + ~~~~~~~~~~~~~~~~~~~~~~~~ + +Перед тем, как давать команды постройки кораблей на планетах, необходимо +сконструировать нужные классы кораблей. Готовых классов кораблей не +существует, каждая раса в галактике разрабатывает свои собственные, исходя из +поставленных стратегических задач. Чтобы создать класс корабля, нужно дать +ему имя и определить следующие характеристики: + +Двигатель (Drive) - мощность гипердвигателя +Вооруженность (Armament) - количество несомых орудий +Оружие (Weapons) - мощность орудий +Защита (Shields) - мощность генератора защитного поля +Размер трюма (Cargo) - объём грузовых отсеков + +Выбранные Вами характеристики корабля могут быть либо равными 0, либо быть не +менее 1. Разумеется, все характеристики корабля не могут быть нулевыми +одновременно. "Вооруженность" и "Оружие" должны быть оба либо нулевыми либо +оба ненулевыми. "Вооруженность" задаётся целым числом, все остальные могут +быть дробными. Например, корабль может иметь "Защиту" 1.5, но не может 0.5. + +Конструирование классов кораблей не занимает времени или ресурсов, новый класс +корабля становится доступным сразу после отдачи соответствующей команды. Для +определения эффекта от характеристик корабля смотрите разделы +"Перемещение", "Грузы" и "Сражения". + +Приведём несколько примеров классов кораблей. Несмотря на то, что такие классы +могут встречаться в галактике у различных рас, Вы не обязаны конструировать +корабли с точно такими характеристиками, гораздо важнее исходить их тех +задач, которые Ваши корабли будут решать. + +Наименование D A W S C + +Drone 1 0 0 0 0 +Fighter 1 1 1 1 0 +Gunship 4 2 2 4 0 +Destroyer 6 1 8 4 0 +Cruiser 15 1 15 15 0 +Battle_Cruiser 30 3 10 30 0 +Battleship 25 1 30 35 0 +Battle_Station 60 3 30 100 0 +Orbital_Fort 0 3 30 100 0 +Space_Gun 0 1 1 0 0 +Freighter 8 0 0 2 10 +Megafreighter 80 2 2 30 100 + + + Постройка кораблей + ~~~~~~~~~~~~~~~~~~ + +Корабли строятся с уровнями технологий, которые были у расы на начало хода. +Иначе говоря, при получения очередного отчёта только что построенные корабли +получат технологические уровни предыдущего хода. + +Корабль без вооружения имеет массу равную "Двигатели" + "Защита" + "Размер +Трюма", указанные при его проектировании. Корабль с одной пушкой имеет массу, +равную "Двигатели" + "Оружие" + "Защита" + "Размер Трюма". Для кораблей, +несущих несколько орудий, каждое орудие после первого добавляет массу, равную +половине "Оружия". + +Массы некоторых из приведённых выше кораблей: + +- Freighter: 20 единиц массы. +- Cruiser: 45 единиц массы. +- Gunship: 11 единиц массы. + +Вы можете установить на планете производство кораблей определенного класса, +который был ранее сконструирован Вами. Для постройки корабля необходимо +затратить количество сырья, равное массе корабля и еще по 10 производственных +единиц на каждую единицу массы корабля. + +Например, Ваш HW производит корабли класса "Drone" из списка приведенного +выше, и существует достаточный запас сырья, следовательно, Вы можете +произвести 100 таких кораблей (без запаса сырья, то будет произведено +немногим больше 99 кораблей). Однако, если Вы будете производить Battleship, +то сможете произвести только 10/9 корабля за ход. После первого хода один +корабль будет полностью построен и 1/9 будет в стадии производства. После +второго хода 2 корабля будут находиться на орбите и 2/9 незавершенны. Если Вы +затем перенастроите производство на другой тип кораблей или что-нибудь +совершенно иное, эти 2/9 будут демонтированы и добавлены к запасу сырья. +Запас сырья увеличится ровно на столько, сколько было использовано сырья при +постройке этих 2/9 частей корабля. Производственные единицы, которые были +использованы для постройки самого корабля будут безвозвратно утеряны как +бесполезный труд. + +Как видно, невыгодно часто переключать производство при постройке больших +кораблей. Кроме того, очевидно, что экономически выгодным является способ +постройки, при котором масса корабля является кратной (или приблизительно +кратной) массе, которую может произвести за один ход данная планета. + +Планета с промышленностью 1000, ресурсами 10 и без запасов сырья может +произвести за один ход 99.0099 единиц массы. Разумно производить на такой +планете корабли массами: 99.00, 11.00, 198.01 или 297.02. И весьма невыгодно, +хотя и возможно, пытаться строить что-либо массой 140, например. + +Важно отметить, что сырье тратится в только в самом конце постройки кораблей. +Поэтому для кораблей, рассчитанных на длительную постройку (более одного +хода), необходимое сырье можно подвозить на планету на протяжении всей +постройки. + +Правильный расчет производимого корабля - одна из самых важных задач в игре. +Необходимо досконально представлять себе весь механизм расчетов. К примеру, +если корабли, которые, по Вашим расчетам должны были на определённом ходу +долететь до нужной планеты, оказались на ничтожном расстоянии 0.001 св. лет +(или меньше) от этой планеты и не долетели, это означает, что Вы неправильно +рассчитали скорость/массу и т.д. + + + Группы кораблей + ~~~~~~~~~~~~~~~ + +На поздних стадиях игры Вы можете иметь сотни и даже тысячи кораблей, которыми +крайне неудобно было бы управлять отдельно. По этой причине корабли +объединяются в группы. Все команды манипулирования ранее построенными +кораблями оперируют группами кораблей, даже если в какой-то группе находится +всего один корабль. Вы можете загружать группы кораблей грузом, посылать их +на другую планету, передавать другой расе и т.п. + +Группой является некоторое количество кораблей одного класса, находящихся в +одном месте, перевозящих одинаковое количество однотипного груза, имеющих +одинаковую принадлежность к флотам и имеющих одни и те же технологические +уровни. Корабли с отсутствующим компонентом помечаются как имеющие +технологический уровень 0 для этого компонента, т.е. невооруженные корабли, +например, всегда имеют технологический уровень 0 для "Оружия". + +Когда необходимо выполнить команду с меньшим количеством кораблей, чем +находится в группе (например, послать на другую планету 8 из 10 кораблей), +необходимо сначала выделить эти корабли в отдельную, новую группу. +Программа-клиент упрощает такие действия для игрока, однако, необходимо +помнить, что в приказе такое действие будет состоять из двух команд: сначала +произойдёт выделение кораблей в новую группу, затем - действие с новой +группой. Объединение эквивалентных групп происходит автоматически перед +началом каждого хода или по команде игрока. + + + Передача кораблей между расами + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +В процессе игры можно передавать группы кораблей между расами. Если у расы, +которой передается группа, уже определен класс кораблей с таким же названием, +но другими характеристиками, принимающая раса так же получит новый класс +кораблей, к названию которого будет добавлен некоторой случайный суффикс. + + + Модернизация кораблей + ~~~~~~~~~~~~~~~~~~~~~ + +В процессе развития технологических уровней расы, ранее построенные корабли +могут устареть и перестать соответствовать текущим возможностям расы. Такие +корабли можно модернизировать до необходимых уровенй технологий. + +Важно помнить, что процесс модернизации технологических уровней кораблей +завершается лишь к окончанию хода, поэтому те корабли, которым была отдана +команда модернизации, сперва примут участие в сражениях, если на одной с ними +орбите окажутся вражеские корабли. + +Для проведения процесса модернизации группа кораблей должна находиться на +одной из принадлежащих Вам планет. Корабли из этой группы будут +модернизироваться в соответствии с Вашими текущими технологиями (если они уже +имеют Ваши последние технологические уровни, то ничего не произойдет). Но +можно и ограничить уровень технологий, до которого происходит модернизация, +задав конечный уровень модернизации технологий. + +Разумеется, модернизация кораблей имеет свою цену. Цена модернизации корабля +равна частичной стоимости от постройки нового корабля. Например, если корабль +имеет технологии равные 2/3 от необходимых технологий, то цена модернизации +будет равна 1/3 от стоимости постройки нового корабля. Экономическая +эффективность процесса модернизации будет выше, нежели постройка нового, так +как модернизация не требует затрат сырья. Каждый из блоков корабля можно +модернизировать отдельно. Конечная формула стоимости модернизации блока +одного корабля выглядит так: + + (1-текущая_технология/конечная_технология)*10*масса_блока + + > Например, если модернизировать корабль типа "Cruiser" со всеми единичными + технологиями до уровня технологий 2.0, то для полной его модернизации + потребуется 225 производственных единиц. + +Легко заметить, что стоимость модернизации тем выше, чем больше +разрыв между текущей технологией корабля и конечной технологией. + +Можно модернизировать либо всю группу кораблей, либо только необходимое +количество. В случае, если количества производственных единиц планеты не +достаточно для полной модернизации даже одного корабля, то указанные +технологии одного корабля поднимаются на столько, на сколько это возможно +(если делается комплексная модернизация, то все технологии корабля +поднимаются пропорционально массе соответствующих компонентов). Таким +образом, можно модернизировать корабли в течении нескольких ходов, выполняя +каждый ход команду модернизации. + +Разумеется, производственные единицы, потраченные на модернизацию кораблей, не +будут принимать участие в производственном процессе на данной планете в +течении этого хода. Так, если производственных единиц не хватает для +выполнения полной модернизации указанной группы, используется весь запас +производственных единиц на данной планете и она уже будет не в состоянии +что-либо производить в течении этого хода. + +На планете можно модернизировать любое количество групп кораблей, пока +остаются свободные производственные единицы. Оставшееся после модернизации +кораблей количество производственных единиц не будет потрачено на частичную +модернизацию, а остаётся свободным для дальнейшего производства. + +При модернизации нескольких групп кораблей на планете, сперва будут +подвергнуты модернизации группы с наибольшей стоимостью и при условии, что на +планете достаточно производственных единиц для модернизации каждой группы. +Если производственных единиц окажется недостаточно для конкретной группы +(например, случилась бомбардировка вражескими кораблями), она остаётся с +изначальным уровнем технологий. + + + Демонтаж кораблей + ~~~~~~~~~~~~~~~~~ + +Корабли, находящиеся на орбите планеты, могут быть разобраны на составляющие +материалы. Запас сырья на планете, где находились корабли, будет увеличен на +массу этих кораблей. Если корабли несли какой-либо груз, он сперва будет +выгружен на планету, за исключением колонистов: при демонтаже корабля над +чужой планетой колонисты не смогут быть выгружены и навсегда останутся в +стадии заморозки на просторах Галактики. + + + Флоты + ~~~~~ + +Флоты могут быть составлены из групп кораблей разных типов. К флотам применима +команда перемещения, точно также, как и к отдельно взятой группе. В отличие +от отдельных групп, группы, входящие в состав флота, не перемещаются по +установленным для планет грузовым маршрутам. Скорость флота равна скорости +самой медленной группы, входящей в флот. Загрузка и разгрузка кораблей, +входящих в флот может влиять лишь на скорость флота. При выделении +какого-либо количества кораблей из группы, входящей в состав флота, эти +выделенные корабли не будут входить в состав флота. Флот существует до тех +пор, пока он содержит хоть одну группу. Между флотами может осуществляться +передача групп и флоты могут объединяться в один, но только в том случае, +если флоты находятся на одной планете. + + + Движение + ~~~~~~~~ + +Физические законы, которым повинуются корабли, путешествующие в +гиперпространстве, говорят, что перемещаться можно только от одного большого +центра масс до другого. Это означает, что Вы можете посылать корабли только с +одной планеты на другую. Нельзя послать корабль просто в некую точку +пространства. Когда корабли находятся в гиперпространстве, они уже не могут +изменить курс, вернуться назад, изменить скорость или быть атакованы. + +Космические корабли оборудуются гипердвигателем, эффективность которого равна +мощности Двигателей умноженной на текущий технологический уровень блока +Двигателей. Корабли с мощностью двигателя 0 навсегда останутся на орбите +планеты, на которой они были построены. Это не означает, впрочем, что таких +кораблей строить нельзя, наоборот, они могут быть прекрасным средством защиты +планет от вражеских кораблей. + +Корабли перемещаются за один ход на количество световых лет, равное +эффективности двигателя, умноженной на 20 и делённой на полную массу +корабля. + +"Полная масса" означает массу самого корабля плюс массу перевозимого им +груза. "Масса перевозимого груза" отличается от просто "массы груза" тем, +что "масса груза" это общее количество единиц груза, а "масса перевозимого +груза" это общее количество единиц груза деленное на технологический уровень +Грузоперевозок, т.е. величина меньшая. + +Следовательно, транспорты движутся быстрее, когда они не несут груза. Когда +уровень технологии Двигателей низок, крупные корабли должны иметь +соответствующие их массе Двигатели, иначе они будут очень медленны. Самые +быстрые корабли могут двигаться со скоростью: + + + 20 * технологический_уровень_Двигателей + + +Исключением из общих правил расчета скорости кораблей являются корабли, +входящие в состав какого-либо сформированного Вами флота. В этом случае, вне +зависимости от возможностей кораблей, их скорость будет равна скорости самой +медленной группы данного флота. + +Чтобы проделать путь от одной планеты к другой, должно быть затрачено столько +ходов, за сколько посылаемая группа кораблей может преодолеть расстояние +между этими планетами. + +От технологии двигателей расы зависит также и зона свободного перелета +кораблей (максимальная дальность полета кораблей от своих планет). Любой +корабль может улететь на любую планету, удаленную от планет, принадлежащих +расе, на расстояние не более чем: + + + 40 * технологический_уровень_Двигателей + + +Если у расы не осталось планет, то оставшиеся у неё корабли не смогут покинуть +своего места пребывания, поскольку нет возможности вычислить максимальное +расстояние для полёта. + +Если передаваемый другой расе корабль находится вне пределов досягаемости +расы, которой его передали, то он либо продолжит свой полет, либо сможет быть +отправлен на планету, находящуюся в зоне досягаемости расы владельца. + +В отчётах Вы можете получить направление и скорость движения чужих кораблей, +но только в случае, когда они направляются на одну из Ваших планет. В +остальных случаях Вы можете получить только координаты центра масс чужих +групп, двигающихся в гиперпространстве не далее чем на расстоянии + + + 30 * Ваш_технологический_уровень_Двигателей + + +от ближайшей планеты, принадлежащей Вам. Корабли, находящиеся за пределами +зоны видимости, вообще не будут показываться в Ваших отчетах, даже в том +случае, если эти корабли летят на одну из Ваших планет. + +Необходимо также помнить, что события, происходящие на планете, не +принадлежащей расе, эта раса может наблюдать только в том случае, когда на +планете находятся ее корабли. Если все корабли отсылаются с такой планеты, то +состояние планеты сразу исчезает из поля зрения. + + + Грузоподъемность + ~~~~~~~~~~~~~~~~ + +Грузоподъемность корабля означает размер его грузового отсека. Количество +груза, который может нести корабль, вычисляется по формуле: + + + Тех.ур._Грузоперевозок*(Размер_Трюма+(Размер_Трюма^2)/20) + + +где под понятием "Технологический_уровень_Грузоперевозок" +подразумевается текущий технологический уровень Грузоперевозок +корабля, а Размер трюма указывается при проектировании этого корабля. + +Несколько примеров Грузоподъемности кораблей при технологии Грузоперевозок +равной 1.0: + + Размер Трюма Количество груза + + 1 1.05 + 5 6.25 + 10 15.00 + 50 175.00 + 100 600.00 + +При технологии Грузоперевозок 2.0 эти показатели удваиваются и т.д. Заметим, +что большие транспорты могут нести очень большое количество груза, но если +они будут полностью загружены, то они будут очень медленно передвигаться +(например, полностью загруженный Megafreighter при технологии двигателей 1 +будет иметь скорость лишь 1.97 световых года за один ход). + +Маленькая скорость тяжело груженых кораблей, вообще говоря, может быть +компенсирована более высокой технологией Грузоперевозок. При технологическом +уровне 2.0, масса любого груза на борту корабля будет считаться как половина +от нормальной массы, используемой для вычислений скорости корабля и мощности +защитного поля (см. "Сражения"). При тех. уровне 3.0, масса груза при +вычислениях будет делиться на 3 и т.д. Например, корабль типа Freighter при +тех. уровне Грузоперевозок 1 может нести 15 единиц груза. При тех. уровне 2.0 +количество груза возрастет до 30 единиц, однако они будут замедлять корабль +точно также, как 15 единиц груза при технологическом уровне Грузоперевозок +1.0. Таким образом при тех. уровне 2.0 и загруженных 30 единицах груза +Freighter будет двигаться также быстро (учитывая тех. уровень Двигателей) как +Freighter загруженный 15 ед. груза при технологии Грузоперевозок 1.0. + + > TODO: WTF? Может, это какой-то другой лор? Надо подумать. + > + > Иными словами, технология Грузоперевозок есть ни что иное как + > технология размещения контейнеров с грузом. Следовательно, при + > увеличении грузоподъемности корабля путем повышения технологии + > Грузоперевозок этого корабля, его объем не изменяется, а значит не + > изменяется его скорость (она сильно зависит от площади поверхности + > корабля), равно как и его защита (см. "Сражения"). Словом, развивая + > данную технологию Вы находите новые способы рационального размещения + > контейнеров с грузами в грузовых отсеках. + +Корабль может нести только один тип груза одновременно. Возможные типы груза - +это колонисты, сырье и промышленность. Груз может быть доставлен на борт +корабля с Вашей или не занятой планеты, на которой он имеется. Промышленность +и Сырье могут быть выгружены на любой планете. Колонисты могут быть высажены +только на планеты, принадлежащие Вам или на необитаемые планеты. + + + Грузовые маршруты + ~~~~~~~~~~~~~~~~~ + +Чтобы перемещать грузы между планетами, Вы можете устанавливать грузовые +маршруты, вместо того, чтобы делать это вручную. Грузовой маршрут с планеты A +на планету B с грузом определенного типа означает, что сервер будет пытаться +доставить этот груз с планеты A на планету B, используя все доступные +транспортные корабли. Таким образом, если такой маршрут установлен, любой +незагруженный корабль на планете A на каждом ходу будет загружен (если, +конечно, груз нужного типа на этой планете есть) и послан на планету B. Любой +корабль, прибывший на планету B с грузом нужного типа будет автоматически +разгружен (даже если он прибыл не с планеты A). + +Вы можете установить до 4-х грузовых маршрутов для каждой планеты, которой +владеете: по одному на каждый тип груза и еще один для пустых кораблей, что +полезно для возвращения транспортов с планет, потребляющих ресурсы на +планеты, их производящие. Вы можете устанавливать грузовые маршруты только +с планет, которыми Вы владеете, однако эти маршруты могут вести на любые +планеты, так что Вы можете транспортировать таким образом колонистов на +необитаемые планеты. + +Если с планеты установлено несколько типов маршрутов, корабли загружаются и +отправляются в следующем порядке: сначала колонисты, затем промышленность, +затем сырье и, наконец, пустые корабли. При избытке количества конкретного +типа груза на планете, группы кораблей будут загружаться в порядке убывания +размеров их трюмов. + +На особом положении находятся корабли входящие в состав какого-либо флота. +Подразумевается, что эти корабли предназначены для выполнения некой +специальной миссии и на них не распространяется обязанность следовать +установленным грузовым маршрутам. + +Грузовой маршрут может быть установлен лишь на планету, которая находится в +зоне полета кораблей (см. "Движение"). + + + Сражения + ~~~~~~~~ + +Когда на планете встречаются вооруженные корабли враждующих рас, происходит +сражение. В случае агрессии одной из сторон, другая сторона, даже если она +находится с агрессором в состоянии мира, также вступит в сражение. Это вовсе +не означает, что у нападающих есть право первого выстрела, все сражающиеся +стороны находятся в абсолютно равных условиях ведения сражения, и первым +выстрелит тот, кто более удачлив. Отосланные с планеты вручную или по +маршруту корабли уже вошли в гиперпространство и участия в сражениях не +принимают (см. "Последовательность действий"). + +В каждом раунде сражения все корабли получают шанс выстрелить по противнику, +разумеется, если в этом же раунде его противники не были более удачливы и не +успели своими выстрелами уничтожить корабль, ожидающий своей очереди +атаковать. + +В начале раунда случайным образом из участников сражения выбирается один +корабль. Он случайным образом выбирает себе в качестве мишени вражеский +корабль и стреляет по нему. Цель может быть, а может и не быть уничтожена, +что зависит от вооружения, защиты и Фортуны. Атакующий корабль будет +продолжать стрелять по случайным целям, пока не выстрелят все его орудия и +остаются возможные цели для поражения. + +Затем вновь случайным образом выбирается корабль, который в данном раунде ещё +не стрелял и имеет шансы поразить вражеский корабль. Так продолжается до тех +пор, пока в раунде не отстреляются все корабли. Если после этого остаются +корабли, способные поразить друг друга, начинается новый раунд сражения. + +Сражение прекращается, когда ни у одного из враждующих кораблей не остаётся +цели, которую он способен поразить. Такое может, например, быть при встрече +маленького истребителя с огромным, но невооруженным транспортом, защиту +которого тот не в состоянии пробить. + +Формула вероятности уничтожения корабля: + + + (log[4]( + (Оружие*Т.У.Оружия)/ + (Защита*Т.У.Защиты/масса^(1/3)*30^(1/3)) + )+1) / 2 + + + > Пояснение: log[4] (a) - это логарифм по основанию 4 от а; X^Y - это Х в + степени Y; термин Оружие относится к стреляющему кораблю, а Защита и + масса к кораблю-цели. + +Эффективность атаки равна Оружию, умноженному его на технологический уровень. +Эффективность защиты равна Защите, умноженной на технологический уровень +Защиты, и деленной на диаметр корабля-цели, который равен корню кубическому +от его массы. И это совершенно естественно, ибо большие корабли должны +защищать большую поверхность и, при прочих равных условиях, слабее. Корабль с +параметрами D=8 A=1 W=8 S=8 C=0 будет иметь лишь в 4 раза более эффективную +защиту, чем корабль с параметрами D=1 A=1 W=1 S=1 C=0, хотя его Защита в 8 +раз больше. + +Параметры подобраны так, что корабль D=10 A=1 W=10 S=10 C=0, стреляя по такому +же кораблю, уничтожит цель с вероятностью 50%. Если посчитать, то очевидно, +что защита такого корабля равна ~3.21. Для того, чтобы уравнять вероятности +атаки и защиты, защита нормируется - умножается на число ~3.11, таким +образом, достигается ситуация, когда единица защиты обеспечивает единицу +защищенности. Если эффективность атаки в 4 раза выше, чем у нормированной +защиты - цель всегда уничтожается. Если эффективность нормированной защиты в +4 раза выше - атака всегда безуспешна. + +Заметим, что любое количество груза на борту корабля увеличивает его "полную +массу" (---ссылка---) при расчетах мощности защиты корабля, а генератор +защитного поля должен защищать груз также хорошо, как и сам корабль. Иначе +говоря, для транспорта, загруженного известным количеством груза, +эффективность защиты будет тем слабее, чем ниже уровень технолгии +Грузоперевозок. + +Если вооруженный корабль остался на вражеской планете после завершения +сражения, он начинает бомбить планету, уничтожая на ней население и +промышленность в зависимости от мощности его орудий. + + + Бомбардировка планет + ~~~~~~~~~~~~~~~~~~~~ + +Вражеские корабли, находящиеся на орбите обитаемой планеты, выполняют +бомбардировку с целью захвата этой планеты путём последующей колонизации. +Механизм бомбардировки состоит в следующем. На планете уничтожается население +и колонисты в количестве равном суммарной мощности бомбардировки всех +атакующих групп. Такое же количество промышленности превращается в сырье. +Мощность бомбардировки одной группы вычисляется так: + + + Мощность=(((Оружие*Тех.Ур.Оружия)^(1/2))/10+1)*Оружие* + Тех.Ур.Оружия*Вооруженность*Количество_кораблей_в_группе + + +Таким образом, один корабль Battle_Station при технологии Вооружения 1.0 будет +иметь мощность бомбардировки, равную 139.30. Это означает, что за один ход, +бомбардируя планету, такой корабль может уничтожить 139.30 ед. населения, +139.30 ед. колонистов и превратить 139.30 ед. промышленности в сырье. Два +таких корабля будут иметь уже мощность 278.60. + +Бомбардировка, в отличие от сражений, не происходит раундами - каждая из +атакущих групп кораблей имеет возможность лишь один раз атаковать планету имеющимися силами. + +Так же не играет роли очередность бомбардировки, поскольку планета будет атакована +одновременно всеми группами, начиная с самой большой мощности бомбардировки, +пока на планете остаётся население. + +Если после бомбардировки на планете остается население, то она продолжает +производство и может к следующему ходу построить, например, корабль. Если на +планете остались, также и колонисты, то они превращаются в население, а +накопленная промышленность возмещает потери производства. + +В том случае, если после бомбардировки на планете не остается населения, +планета становится необитаемой и может быть колонизирована заново. Таким +образом Вы можете захватывать планеты, принадлежащие другим расам. Всё сырьё +и все запасы промышленности, оставшиеся на планете после бомбардировки, +сохраняются и перейдут к новому владельцу, который сможет колонизировать +планету. Колонисты, находившиеся на планете, так же погибают, т.к. не +остаётся ни активного населения, способного вывести их из состояния +заморозки, ни промышленности на их поддержание. + + + Колонизация планет + ~~~~~~~~~~~~~~~~~~ + +Любая необитаемая планета может быть колонизирована, т.е. заселена и таким +образом добавления к владениям расы. Это происходит в случае, если колонисты +высаживаются на необитаемую планету. + +Если корабли нескольких рас, загруженные колонистами, одновременно прибывают +на необитаемую планету и у этих рас установлены грузовые маршруты на доставку +колонистов на эту планету, либо несколько игроков отдали команду выгрузки +колонистов, то преимущество заселения планеты будет определено в следующем +порядке: + +- Наибольшее количество выгружаемых колонистов; +- Наибольшее количество населения расы; +- Случайный выбор претендента на колонизацию. + +Разумеется, если раса, колонизирующая планету, не смогла получить планету во +владение, тогда все последующие команды, рассчитанные на то, что именно Ваши +колонисты были выгружены на планету, могут быть отменены в процессе +производства хода. Если Вы не уверены, что планета будет колонизирована +именно Вами, имеет смысл вступать в дипломатическую переписку, заключать +договоры, продавать право колонизации за материальные блага и т.п. + +Следует помнить, что флоты не подчиняются грузовым маршрутам, следовательно, +колонисты на кораблях флотов не участвуют в подсчёте общечего числа +колонистов для приоритетной высадки в конце маршрутов. + +По умолчанию, на колонизированных планетах устанавливается производство +промышленности. + + + Война и мир + ~~~~~~~~~~~ + +В начале игры предполагается, что Вы находитесь в состоянии войны со всеми +остальными расами. Вы можете заключить мир с другой расой в любое время. Это +означает, что Ваши корабли не будут ни стрелять по кораблям этой расы, ни +бомбить ее планеты. Однако любая раса при этом может находиться в состоянии +войны с Вами, и до тех пор, пока она, со своей стороны, не заключит мир с +Вами, её корабли еще могут атаковать Вас. Находясь в состоянии мира, Вы +можете в любой момент снова объявить войну и наоборот. + +В Вашем отчете будет указан дипломатический статус по отношению к каждой расе, +однако, это ни в коей мере не показывает отношение к Вам остальных рас. Вы не +знаете, как они к Вам относятся, до тех пор, пока не встретите один из их +боевых кораблей. И только после того, как боевой корабль чужой расы за время +хода не произвел выстрелов по Вашим кораблям, можно считать, что эта раса +находится с Вами в мире. + +Разумеется, удостовериться в том, что у той или иной расы по отношению к Вам +только мирные намерения, можно прибегнув к дипломатической переписке, +заключив, например, пакт о ненападении до определённого хода, вступив в +альянс до конца партии и т.п. Однако, не стоит забывать, что в Галактике в +равной степени есть место как для доблести, так и для коварства. + + + Выход из игры + ~~~~~~~~~~~~~ + +Раса считается полностью погибшей, если она не владеет ни одной планетой и ни +одним кораблем. Если раса приходит в подобное состояние, то она удаляется из +списка участвующих рас перед началом просчета очередного хода. Так, если раса +лишилась всех своих планет и кораблей, друзья вполне могут оказать ей помощь, +передав в её владение корабли до начала производства хода. + +Любая раса может воспользоваться командой досрочного выхода из игры. После +подачи этой команды, раса будет удалена из игры через 3 хода. Команда выхода +из игры должна быть последней в приказе. + +Существует возможность принудительного окончания игры. Если раса 10 ходов +подряд не присылала приказы на сервер (к дипломатической почте это не +относится), то такая раса так же удаляется из игры. + +Любая раса также может быть исключена из игры в любое время по решению +администрации за грубые нарушения. + +При исключении из игры удаляются все группы кораблей, принадлежащие расе, а +все её планеты становятся необитаемыми и с них исчезает вся промышленность, +материалы при этом остаются. Вышедшие из игры расы невозможно восстановить в +списках участников партии. + +За 5 ходов до принудительного исключения, раса с каждым новым отчетом начинает +получать предупреждение. + +За 3 хода до исключения, в каждом из следующих отчетов все участники узнают о +грядущем выходе этой расы из игры. Любая команда, пришедшая на сервер от ещё +живой расы, выключает механизм выхода расы из игры(это не относится к +дипломатической почте). + + + Победы и поражения + ~~~~~~~~~~~~~~~~~~ + +В каждом Вашем отчете после просчета хода в списке состояния рас указано +количество голосов, полученных каждой из рас в процессе хода. Суммарное +количество голосов также указано в отчёте. Каждая тысяча единиц населения +планет, принадлежащих расе, дают один голос. Если в начале игры каждая из рас +имеет одну полностью развитую планету размером 1000 и две по 500, значит, +каждая из рас имеет по два голоса. В процессе колонизации других планет и их +развития каждая из рас может увеличить число своих голосов. + +Процесс голосования происходит при производстве хода. В промежутках между +ходами каждая из рас может изменить своего избранника. Если несколько рас по +цепочке отдали свои голоса друг другу, то такие расы считаются находящимися в +альянсе. Количество голосов альянса считается простым суммированием голосов +всех членов альянса без учета голосов остальных рас проголосовавших за членов +альянса, но не входящих в него. + +Победителем считается та раса (или альянс), которая набрала 2/3 от общего +числа голосов всех рас. Вообще говоря, есть возможность закончить игру с +первых же ходов, если 2/3 всех рас изберут для себя единственную достойную и +проголосуют за нее или образуют альянс, но стоит ли ради этого играть? Игра +также может быть закончена и по безапелляционному решению администрации. + + + Последовательность действий + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + После того, как получены приказы от всех рас, определено +производство, загружены товары, корабли вошли в гиперпространство и +т.д. происходит сам ход, т.е. следующая последовательность действий: + +- Корабли передаются новым владельцам. + +- Расы, покинувшие игру, освобождаются от своего имущества. + +- ------------------------------------ \ + } "Выполнение отданных приказов (всех?)" +- Корабли разгружаются согласно отданным приказам. / + +- Корабли, где это возможно, объединяются в группы. + +- Товары загружаются на корабли, находящиеся в начале грузовых + маршрутов. + +- Корабли входят в гиперпространство. + +- Враждующие корабли вступают в схватку. + +- Корабли пролетают сквозь гиперпространство. + +- Корабли, где это возможно, объединяются в группы. + +- Враждующие корабли вступают в схватку (после выхода из гиперпространства). + +- Корабли бомбят вражеские планеты. + ++ На планетах модернизируются корабли. + +- На планетах строятся корабли (с учётом производственного потенциала, + оставшегося от модернизации кораблей). + ++ Корабли, где это возможно, объединяются в группы. + +- На планетах производится промышленность, добывается сырье, + разрабатываются новые технологии. + +- Увеличивается население планет. + +- Корабли разгружаются в конце грузовых маршрутов. + ++ Выгруженные колонисты увеличивают население планеты (если население + планеты ниже её размера). + +- Накопленная и выгруженная промышленность увеличивает + производственный уровень планеты (если производственный уровень + планеты ниже уровня населения). + +- Происходит отмена маршрутов, выходящих за зону полета кораблей. + +- Происходит голосование. + + + Отчет о результатах хода + ~~~~~~~~~~~~~~~~~~~~~~~~ + +Отчет, который Вы будете получать после каждого хода, содержит необходимую и +достаточную информацию о состоянии Галактики, с учётом доступной видимости +действий. + +Перечень разделов отчёта приведён ниже. + +- Размер галактики, количество планет галактики и количество оставшихся рас. + +- Ваше общее количество голосов. +- Имя расы, которой Вы отдаете свои голоса. + +- Статус игроков. + + (N) Имя + (D) Уровень технологии Двигателей + (W) Уровень технологии Вооружений + (S) Уровень технологии Защиты + (C) Уровень технологии Грузоперевозок + (P) Общее население + (I) Общее производство + (#) Число планет во владении + (R) Война или мир (Ваше отношение к указанной расе, но не наоборот) + (V) Количество голосов, отданных расе + +- Ваши науки. + + (N) Название науки + (D) Доля технологии Двигателей + (W) Доля технологии Вооружений + (S) Доля технологии Защиты + (C) Доля технологии Грузоперевозок + +- Чужие науки. + + Список наук чужих рас доступен в том случае, если Ваши корабли + находятся на одной из чужих планет, где разрабатываются технологии при + использовании этих наук. + +- Классы Ваших кораблей. + + (N) Имя + (D) Двигатели + (A) Вооруженность + (W) Оружие + (S) Защита + (C) Размер Трюма + (М) Масса одного корабля этого типа + +- Классы чужих кораблей. + + Описание каждого класса чужих кораблей, которые были встречены Вами на + этом ходу. + +- Сражения. + + Это описание всех сражений, в которых Вы участвовали либо были + их свидетелями на этом ходу. Для каждого сражения указан список + групп, присутствовавших на месте сражения к началу битвы, за + которым следует описание обмена ударами. Дополнительно указываются: + + - Количество кораблей группы, не уничтоженных в процессе сражения; + + - Статус участника сражения: "In_Battle" или "Out_Battle", причём, + последнее состояние указывает на то, что группа не участвовала в + сражении, а являлась лишь свидетелем происходящих событий. Такое + возможно, если у группы не оказалось врагов. + +- Бомбардировки. + + Список планет, которые подверглись бомбардировке на этом ходу в + пределах Вашей видимости, содержащий информацию об атаке: + + (W) Имя расы, производящей бомбардировку + (O) Имя расы-владельца + (N) Название планеты + (P) Население + (I) Производственный потенциал + (P) Тип производства + ($) Запасы промышленности + (M) Запасы сырья + (C) Количество колонистов + (A) Мощность атаки + ( ) Состояние на момент после бомбардировки: "Damaged"/"Wiped" + +- Приближающиеся группы. + + Список всех групп чужих кораблей, находящихся в гиперпространстве и + направляющихся на Ваши планеты. + + (O) Откуда (с какой планеты отправлена группа) + (D) Куда (на какую планету группа направляется) + (R) Оставшееся расстояние + (S) Скорость + (M) Полная масса + +- Ваши планеты. + + Это список всех Ваших планет. Приводится следующая информация: + + (#) Галактический номер планеты + (X) X координата + (Y) Y координата + (N) Название планеты + (S) Размер + (P) Население + (I) Производственный потенциал + (R) Природные ресурсы + (P) Тип производства (промышленность, сырье, исследования или + корабли) + ($) Запасы промышленности + (M) Запасы сырья + (C) Количество колонистов + (L) Свободный производственный потенциал + ( ) TODO: добавить ЗАНЯТЫЙ производственный потенциал + + Параметр (L) используется для определения реального промышленного + потенциала на данный ход. + +- Корабли в производстве. + + (#) Галактический номер планеты + (N) Название планеты + (S) Наименование типа строящегося корабля + (C) Стоимость постройки одного такого корабля (в + производственных ед.) без учета расходов на добычу сырья + (P) Сколько производственных единиц уже было затрачено на + постройку этого корабля (уже учитывая производство сырья) + (L) Свободный производственный потенциал + + Необходимо обратить внимание на то, что Стоимость постройки одного + корабля не учитывает расходов на добычу сырья, в то время как, в + количество затраченных производственных единиц, уже включены расходы + на добычу сырья. Поэтому вполне нормальная ситуация, когда (P) немного + превышает (C). Это может говорить лишь о том, что необходимое для + постройки корабля количество сырья еще не было произведено. + +- Грузовые маршруты. + +- Чьи-то планеты. + + Список планет чужих рас, на которых находятся Ваши наблюдатели. + +- Необитаемые планеты. + + Список незаселенных планет, за которыми Вы можете наблюдать, т.е. на + них присутствуют Ваши корабли. + +- Неизвестные планеты. + + Список планет в пределах Вашей досягаемости, за которыми Вы не можете + наблюдать. Указывается только номер планеты и ее координаты. + +- Ваши флоты. + +- Группы Ваших кораблей. + +- Группы чужих кораблей. + + Список групп кораблей, принадлежащих другим игрокам, за которыми Вы + можете наблюдать. + +- Неопознанные группы кораблей. + + Список координат групп чужих кораблей, находящихся в гиперпространстве + и не направляющиеся на Ваши планеты. + + + Дипломатическая почта + ~~~~~~~~~~~~~~~~~~~~~ + +Игроки в Galaxy анонимны. Это означает, что никто, кроме администрации +сервера, не знает адресов и имен других игроков. Это сделано для того, чтобы +не переносить игровые отношения и конфликты на реальную жизнь и тем самым +дать игрокам возможность вести себя менее скованно. + +В процессе игры каждая из рас имеет возможность общаться с другими расами. +Процесс общения происходит посредством пересылки дипломатических писем через +сервер. Цель написания писем может быть самой разнообразной. Например для +заключения союзов, совместных военных действий, разрыва союзов и т.п. + + + Вопросы, на которые необходимо знать ответы: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. В чем различие между промышленностью и производственным потенциалом? А + между промышленностью и запасами промышленности? + +2. Какую массу имеет корабль с параметрами D=0 A=20 W=5 S=0 C=0? + +3. С какой скоростью будет лететь корабль с параметрами D=5 A=0 W=0 S=0 C=0, + если его технологический уровень Двигателей равен 1.0? + +4. На каком ходу корабль, имеющий скорость 18 св. лет за ход, прибудет к месту + назначения, если расстояние между планетой отправки и планетой назначения + 40 св. лет, а отправлен он был на 15 ходу? + +5. Какой технологический уровень Двигателей будет иметь корабль с параметрами + D=1 A=0 W=0 S=0 C=0, если до его постройки технологический уровень + Двигателей был равен 1.8, а на другой планете одновременно с его + постройкой технология Двигателей увеличилась на 0.2? + +6. Какую массу будет иметь корабль с параметрами D=10 A=0 W=0 S=0 C=2 и + технологическим уровнем Грузоперевозок 1.2, если его полностью загрузить? + +7. Произойдет ли сражение, если на планете встретились корабль с параметрами + D=1 A=0 W=0 S=0 C=0, принадлежащий расе A, и корабль с параметрами D=1 A=1 + W=1 S=0 C=0, принадлежащий расе B, причем у расы B установлен "мир" с + расой A, а у расы A - "война" с расой B? + + + Некоторые советы + ~~~~~~~~~~~~~~~~ + +На ранних стадиях игры нет необходимости сражаться за планеты. Следует +начинать с постройки грузовых кораблей, накопления промышленности и доставки +колонистов и промышленности на ближайшие необитаемые планеты. Разумно также +установить контакты с другими расами с целью создания альянсов, которые очень +пригодятся, когда настанет время сражений. Для этого просто необходимо +осуществлять дипломатическую переписку. Не ведут переписки только обреченные +расы. + +Карта в Вашем отчете показывает только планеты, колонизированные чужими +расами, и полную массу групп чужих кораблей, направляющихся к одной из Ваших +планет. Чтобы иметь подробную информацию о вражеских флотах, которые могут +угрожать Вашей безопасности, нужно посылать на чужие планеты корабли +исключительно со шпионскими целями. + +В случае приближающейся атаки на ваши планеты самое важное - убедиться, что +для обороны достаточно сил. Для каждой такой группы разделите расстояние на +скорость, чтобы получить количество ходов оставшихся до того, как группа +достигнет планеты. Оцените полную массу: чем она больше, тем больше +потенциальная угроза. Вы, конечно, не можете знать, огромный ли это линкор, +или флот небольших истребителей, а, может, нечто среднее. Можно еще +попытаться прибегнуть к дипломатии: владелец группы хоть и не может повернуть +ее назад, но он может объявить себя в мире с Вами, так что группа не будет по +прибытии атаковать Вас. + +На более поздних стадиях игры, вполне вероятно, что одна из рас достигнет +большего развития нежели остальные и займет доминирующую позицию в Галактике. +С этого момента для остальных игроков жизненно важно немедленно отбросить в +сторону все разногласия между собой и совместно атаковать эту расу. Ибо, если +дать возможность этой расе захватывать расы одну за другой, она получит +превосходный шанс победить в этой игре. + +В силу специфики работы алгоритма ведения сражений (см. "Сражения"), обычно +флот разделяется на три дополняющих друг друга части: + +- много маленьких защитных кораблей прикрытия, для отвлечения пушек + противника; + +- один или более кораблей с несколькими маленькими орудиями, для скорейшего + поражения маленьких кораблей прикрытия; + +- один или более кораблей с одной или несколькими большими орудиями, для + поражения основных вражеских кораблей. + +Это необходимо учитывать при создании обороны планет. + + + Пересчет ходов + ~~~~~~~~~~~~~~ + +Как бы хорошо ни работал сервер и интернет, иногда возникает необходимость +повторить процесс производства хода. Это исключение из общих правил работы +сервера. Вот в каких случаях может произойти пересчет хода: + +1. Найдена ошибка в логике сервера, повлекшая необратимые изменения в + состоянии рас нескольких игроков. + +2. Была нарушена система доставки приказов на сервер или отчётов от сервера на + срок, превышающий время ожидания между двумя ходами, т.е. все игроки не + имели возможности управления своими расами. + +3. Произошло форс-мажорное событие и большинство участников партии однозначно + пришли к выводу о необходимости пересчета хода. + +4. Принято безапелляционное решение администрации на этот счет. + +Каждый из участников может попросить администрацию о пересчете хода, +сославшись на любой из этих пунктов и приведя соответствующие доказательства. +И если администрация решит, что выполнено одно из указанных условий, ход +будет пересчитан. + +Ход не будет пересчитан, если условий не достаточно или есть возможность +динамического исправления ситуации. + + + Этика игры + ~~~~~~~~~~ + +Учтите прежде Всего, что "Galaxy" - это игра, не стоит отождествлять лидера +какой-либо из рас и реального игрока. Не стоит обижаться, если кто-либо +обошелся с Вами некорректно. Здесь допустимы обман и коварство, учтите это в +Вашей политике. Администрация не рассматривает жалобы подобного рода и не +принимает никаких санкций к игрокам, совершивших подобные +"проступки". + +Дипломатическая почта, циркулирующая между игроками, является личной почтой. +Вы можете использовать в ней любые формы и выражения. Однако грубость +недопустима в широковещательных сообщениях, даже если эта грубость часть +Вашего имиджа. Кроме того, администрация сервера оставляет за собой право +цензурировать сообщения, не являющиеся личной дипломатической почтой. + +Сервер игры "Galaxy" - это программа, которая, к сожалению, может содержать +ошибки. Мы будем благодарны Вам, если Вы сообщите о находках подобного рода. +Однако, если Вы попытаетесь использовать найденную ошибку в собственных +интересах, администрация сервера может принять решение об исключении Вас из +партии. \ No newline at end of file diff --git a/gateway/PLAN.md b/gateway/PLAN.md deleted file mode 100644 index 9e3033a..0000000 --- a/gateway/PLAN.md +++ /dev/null @@ -1,552 +0,0 @@ -# Edge Gateway Implementation Plan - -This plan has been already implemented and stays here for historical reasons. - -It should NOT be threated as source of truth for service functionality. - ---- - -## Summary - -This plan breaks implementation into small, reviewable phases. -Each phase has a single primary goal, clear deliverables, explicit dependencies, -acceptance criteria, and focused tests. - -The intended v1 architecture is: - -- unauthenticated public ingress over REST/JSON; -- authenticated ingress over gRPC on HTTP/2; -- FlatBuffers payloads for authenticated business commands; -- protobuf-based gRPC control envelopes; -- authenticated server-streaming push through gRPC; -- separate public traffic classes and isolated anti-abuse counters. - -## Assumptions and Defaults - -- `message_type` is the stable downstream routing key. -- `protocol_version` covers transport and envelope compatibility, not business - payload schema compatibility. -- FlatBuffers are used for business payload bytes only. -- Phase 3 public auth uses a challenge-token REST flow: - `send-email-code(email) -> challenge_id` and - `confirm-email-code(challenge_id, code, client_public_key) -> device_session_id`. -- Phase 3 uses a consumer-side `AuthServiceClient` inside `gateway`; the - default process wiring keeps public auth routes mounted and returns - `503 service_unavailable` until a concrete upstream adapter is added. -- Browser bootstrap and asset traffic are within gateway scope, even when backed - by a pluggable proxy or handler. -- Long-polling is out of scope for v1. - -## ~~Phase 1.~~ Module Skeleton - -Status: implemented. - -Goal: create the runnable gateway process skeleton. - -Artifacts: - -- `cmd/gateway` -- `internal/app` -- base configuration types -- startup and shutdown wiring - -Dependencies: none. - -Acceptance criteria: - -- the process starts with config; -- the process shuts down cleanly on signal; -- lifecycle wiring is testable. - -Targeted tests: - -- startup with valid config; -- shutdown without leaked goroutines. - -## ~~Phase 2.~~ Public REST Server - -Status: implemented. - -Goal: add the unauthenticated HTTP server shell. - -Artifacts: - -- public REST listener -- `GET /healthz` -- `GET /readyz` -- base error serialization -- request classification hook - -Dependencies: Phase 1. - -Acceptance criteria: - -- health endpoints respond deterministically; -- public requests are classified at least into `public_auth` and `browser_*`. - -Targeted tests: - -- health endpoint responses; -- request classification smoke tests. - -## ~~Phase 3.~~ Public Auth REST Handlers - -Status: implemented. - -Goal: expose unauthenticated auth commands through REST/JSON. - -Artifacts: - -- `POST /api/v1/public/auth/send-email-code` -- `POST /api/v1/public/auth/confirm-email-code` -- request and response DTOs -- adapter calls into `AuthServiceClient` - -Dependencies: Phase 2. - -Acceptance criteria: - -- no session authentication is required for these routes; -- handlers delegate only through the auth service adapter. - -Targeted tests: - -- success and validation errors for both routes; -- no session lookup on public auth paths. - -## ~~Phase 4.~~ Public Traffic Classification - -Status: implemented. - -Goal: isolate public traffic into stable anti-abuse classes. - -Artifacts: - -- `PublicTrafficClassifier` -- classes `public_auth`, `browser_bootstrap`, `browser_asset`, `public_misc` -- isolated rate-limit bucket keys - -Dependencies: Phase 2. - -Acceptance criteria: - -- browser traffic does not share buckets with public auth; -- auth counters remain unaffected by asset bursts. - -Targeted tests: - -- per-class routing tests; -- bucket isolation tests. - -## ~~Phase 5.~~ Public REST Anti-Abuse - -Status: implemented. - -Goal: add coarse protection to unauthenticated REST traffic. - -Artifacts: - -- body size limits -- method allow-lists -- malformed request counters -- per-class rate-limit thresholds - -Dependencies: Phase 4. - -Acceptance criteria: - -- first-load browser bursts are not marked hostile because of burst pattern - alone; -- malformed or oversized requests are rejected predictably. - -Targeted tests: - -- bootstrap burst stays outside auth abuse counters; -- invalid methods and oversized bodies are rejected. - -## ~~Phase 6.~~ gRPC Server and Public Contracts - -Status: implemented. - -Goal: bring up authenticated transport over gRPC and HTTP/2. - -Artifacts: - -- gRPC listener -- protobuf service definitions -- `ExecuteCommand` -- `SubscribeEvents` - -Dependencies: Phase 1. - -Acceptance criteria: - -- unary and server-streaming RPCs are reachable; -- the server runs only over HTTP/2. - -Targeted tests: - -- unary transport smoke test; -- stream transport smoke test. - -## ~~Phase 7.~~ Envelope Parsing and Protocol Gate - -Status: implemented. - -Goal: validate the gRPC control envelope before security checks continue. - -Artifacts: - -- envelope parser -- required-field validation -- protocol version gate - -Dependencies: Phase 6. - -Acceptance criteria: - -- unsupported or malformed envelopes are rejected before routing. - -Targeted tests: - -- missing field rejection; -- unsupported `protocol_version` rejection. - -## ~~Phase 8.~~ Session Cache Lookup - -Status: implemented. - -Goal: resolve authenticated identity from cache. - -Artifacts: - -- `SessionCache` -- session lookup pipeline -- revoked versus active session handling - -Dependencies: Phase 7. - -Acceptance criteria: - -- unknown and revoked sessions are blocked before signature verification. - -Targeted tests: - -- cache hit with active session; -- cache miss reject; -- revoked session reject. - -## ~~Phase 9.~~ Payload Hash and Signing Input - -Status: implemented. - -Goal: verify payload integrity before signature verification. - -Artifacts: - -- `payload_hash` verification -- canonical signing input builder - -Dependencies: Phase 8. - -Acceptance criteria: - -- changing payload bytes or envelope fields breaks the signing input. - -Targeted tests: - -- payload hash mismatch reject; -- canonical bytes differ when signed fields change. - -## ~~Phase 10.~~ Client Signature Verification - -Status: implemented. - -Goal: authenticate the request origin using the session public key. - -Artifacts: - -- signature verifier -- deterministic auth reject mapping - -Dependencies: Phase 9. - -Acceptance criteria: - -- wrong key and invalid signature produce stable rejects. - -Targeted tests: - -- success case with valid signature; -- bad signature reject; -- wrong-key reject. - -## ~~Phase 11.~~ Freshness and Anti-Replay - -Status: implemented. - -Goal: enforce transport freshness and replay protection. - -Artifacts: - -- timestamp freshness window -- `ReplayStore` -- replay reservation and rejection logic - -Dependencies: Phase 10. - -Acceptance criteria: - -- stale requests and duplicate `request_id` values are rejected. - -Targeted tests: - -- stale timestamp reject; -- replay reject for same session and request ID; -- distinct sessions do not collide. - -## ~~Phase 12.~~ Authenticated Rate Limits and Policy - -Status: implemented. - -Goal: apply edge policy after transport authenticity is established. - -Artifacts: - -- rate-limit keys for IP, session, user, and message class -- authenticated policy evaluation hook - -Dependencies: Phase 11. - -Acceptance criteria: - -- authenticated buckets are independent from public REST buckets. - -Targeted tests: - -- per-dimension throttling; -- bucket isolation from public traffic. - -## ~~Phase 13.~~ Internal Authenticated Command and Routing - -Status: implemented. -Note: delivered together with Phase 14 signed unary responses. - -Goal: forward only verified context to downstream services. - -Artifacts: - -- `AuthenticatedCommand` -- `DownstreamRouter` -- `DownstreamClient` - -Dependencies: Phase 12. - -Acceptance criteria: - -- downstream services receive verified context only; -- raw transport details do not leak as authoritative input. - -Targeted tests: - -- route selection by `message_type`; -- downstream receives the expected authenticated context. - -## ~~Phase 14.~~ Signed Unary Responses - -Status: implemented as part of Phase 13 delivery. - -Goal: return verifiable server responses to authenticated clients. - -Artifacts: - -- response envelope builder -- payload hash generation -- `ResponseSigner` - -Dependencies: Phase 13. - -Acceptance criteria: - -- unary responses always carry the original `request_id`, `payload_hash`, and - server signature. - -Targeted tests: - -- response correlation test; -- server signature generation test. - -## ~~Phase 15.~~ Session Update and Revocation Events - -Status: implemented. - -Goal: keep gateway session state current without synchronous hot-path lookups. - -Artifacts: - -- `EventSubscriber` -- session update handlers -- session revoke handlers - -Dependencies: Phase 8. - -Acceptance criteria: - -- session updates change gateway behavior without per-request sync calls to the - auth service. - -Targeted tests: - -- cache update from event; -- revocation event invalidates cached session. - -## ~~Phase 16.~~ Authenticated Push Stream - -Status: implemented. - -Goal: open a verified server-streaming channel for client-facing delivery. - -Artifacts: - -- `SubscribeEvents` handler -- stream binding to `user_id` and `device_session_id` -- initial server time event - -Dependencies: Phase 15. - -Acceptance criteria: - -- the stream opens only after the full auth pipeline succeeds. - -Targeted tests: - -- authorized stream open; -- rejected stream open for invalid session; -- first event contains server time. - -## ~~Phase 17.~~ Event Fan-Out - -Status: implemented. - -Goal: deliver client-facing events from internal pub/sub to active streams. - -Artifacts: - -- `PushHub` -- event fan-out logic -- user and session targeting rules - -Dependencies: Phase 16. - -Acceptance criteria: - -- events are delivered to the correct active streams only. - -Targeted tests: - -- single-session delivery; -- multi-device delivery for one user; -- unrelated sessions do not receive the event. - -## ~~Phase 18.~~ Revocation-Driven Stream Teardown - -Status: implemented. - -Goal: terminate active delivery channels when a session is revoked. - -Artifacts: - -- stream teardown on revoke -- connection cleanup logic - -Dependencies: Phase 17. - -Acceptance criteria: - -- revocation blocks new unary requests and closes active streams for the same - session. - -Targeted tests: - -- revoke closes active stream; -- revoked session cannot reopen the stream. - -## ~~Phase 19.~~ Observability and Shutdown Hardening - -Status: implemented. -Note: delivered with `zap` structured logging, OpenTelemetry tracing and -metrics, the optional private admin `/metrics` listener, timeout budgets, and -shutdown-driven push-stream teardown. - -Goal: make the service operable in production. - -Artifacts: - -- structured logs -- metrics -- trace propagation -- timeout budgets -- graceful shutdown for unary and streaming traffic - -Dependencies: Phase 18. - -Acceptance criteria: - -- shutdown is deterministic; -- logs and metrics expose stable edge outcomes without leaking secrets. - -Targeted tests: - -- shutdown closes listeners and active streams; -- secret and signature values are not logged. - -## ~~Phase 20.~~ Acceptance Pass - -Status: implemented. -Note: acceptance pass reconciled README/OpenAPI/root architecture -documentation, fixed the documented public-auth projected-error contract, and -added focused regression coverage including OpenAPI validation. - -Goal: reconcile implementation, documentation, and regression coverage. - -Artifacts: - -- updated README and PLAN -- final protocol and interface review -- focused regression test run - -Dependencies: Phases 1 through 19. - -Acceptance criteria: - -- implementation matches documented contracts and ordering guarantees; -- docs describe the actual gateway behavior. - -Targeted tests: - -- run focused package tests for gateway packages; -- rerun cross-cutting regression scenarios. - -## Cross-Cutting Regression Scenarios - -- `send_email_code` and `confirm_email_code` are available without session auth - and are still limited by public auth policy. -- Public browser bootstrap and asset bursts do not increase auth abuse counters - and are not rejected as hostile because of intensity alone. -- Any gRPC command without a valid session is rejected before routing. -- Unknown and revoked sessions are handled predictably and consistently where - policy requires identical behavior. -- Signature verification fails when `payload_bytes`, `payload_hash`, - `message_type`, `request_id`, or the signing key changes. -- `payload_hash` is verified before downstream execution. -- Requests outside the freshness window are rejected. -- Reused `request_id` values are rejected within the session replay window. -- Public REST and authenticated gRPC traffic use independent buckets and - independent abuse telemetry. -- Downstream services receive `AuthenticatedCommand`, not raw REST or gRPC - transport requests. -- Unary responses preserve `request_id` correlation and are server-signed. -- Streaming connections open only after the auth pipeline and close on revoke. -- Session cache updates from events change gateway behavior without synchronous - auth-service lookups per request. -- Graceful shutdown terminates unary and streaming traffic cleanly. diff --git a/ui/PLAN.md b/ui/PLAN.md new file mode 100644 index 0000000..608f4b0 --- /dev/null +++ b/ui/PLAN.md @@ -0,0 +1,1760 @@ +# UI Client Implementation Plan + +This plan stages the implementation of the cross-platform UI client for +Galaxy. The client builds from a single TypeScript + Svelte codebase to +five targets: web, web-mobile, standalone PC (mac/win/linux), iOS, and +Android. A shared Go module (`ui/core`) carries envelope cryptography, +FlatBuffers codec, keypair management, and a thin bridge over `pkg/calc/` +for UI-side game math; it is compiled to WASM (web), gomobile native +libraries (iOS/Android), and embedded directly in Wails (desktop). All +network I/O lives on the TypeScript side via ConnectRPC, so the Go +module is a pure compute boundary on every platform. + +The existing Fyne client in `client/` is deprecated and is not modified +or imported by the new code. The strategy and rationale behind these +choices live in the plan file at +`/Users/id/.claude/plans/buzzing-questing-fountain.md`; the architectural +overview is mirrored into `ui/README.md` as part of Phase 1. + +Each phase ends with a runnable artifact. The visual progression is: +empty page → navigation skeleton → stubbed views → live data → real +actions. Phases are sized so that any one of them can be shipped, run, +and reviewed before the next starts; if a direction proves wrong, the +plan can be adjusted with at most one phase of rework. + +--- + +## Summary + +This plan breaks implementation into 36 small reviewable phases. Each +phase has a single primary goal, clear deliverables, explicit +dependencies, acceptance criteria, and focused tests. Tests live +alongside the code added in the phase; a phase is not closed until its +tests are green on the targets it claims to support. + +The intended v1 architecture is: + +- TypeScript + Svelte 5 frontend, shared across all five build targets; +- PixiJS v8 with dual WebGPU/WebGL backend for the world map renderer; +- Go module `ui/core` as a compute-only library (canonical bytes, + sign/verify, FlatBuffers codec, keypair, thin bridge to `pkg/calc/`) + compiled to WASM, gomobile, and Wails-embedded native; +- TypeScript-side `Core` interface with three adapters (`WasmCore`, + `WailsCore`, `CapacitorCore`) selected at build time; +- `GalaxyClient` on top of `Core` performs all network I/O via + ConnectRPC (`@connectrpc/connect-web`) on every platform; +- per-platform storage: WebCrypto + IndexedDB on web, OS keychain + + SQLite on desktop, iOS Keychain / Android Keystore + SQLite on mobile, + all behind a single `KeyStore` and `Cache` TypeScript interface; +- mobile-first navigation: one active view occupies the main area at a + time; sidebar holds a single tool (calculator, inspector, or order) + with persistent state on switch. + +## Assumptions and Defaults + +- Target Go version follows `go.mod` of the parent module; TinyGo for + WASM must support `crypto/ed25519` and `crypto/sha256`. If TinyGo + support is insufficient, fall back to standard Go `GOOS=js + GOARCH=wasm` with a larger bundle (~2 MB). +- The gateway exposes server-streaming gRPC. Browsers cannot speak raw + gRPC; ConnectRPC support is added to the gateway so a single set of + Go handlers serves native gRPC and browser clients simultaneously. +- TypeScript-side network code uses `@connectrpc/connect-web` for unary + calls and server-streaming push events on every platform. +- Ed25519 private keys never leave the device. Loss of secure storage is + acceptable on every platform and triggers a re-login flow. +- Build pipeline is a single `pnpm` workspace at `ui/`; Make targets + wrap TinyGo, gomobile, Wails CLI, Capacitor CLI, and Vite. +- All file/directory names, code, comments, identifiers, and docs in + `ui/` are written in English. Russian appears only in i18n bundles + delivered in Phase 35. +- Pre-production migration rule from the project root applies: schema + changes are inlined into the existing init schema rather than + producing new migrations; clean rebuilds on every checkout. +- The existing `client/` package is deprecated. New code does not import + from it. Existing types in `pkg/model/client/` are not migrated; UI + types are written from scratch in `ui/core/types/` as needed. +- The `client/world/` algorithm is treated as a reference description + for the new TypeScript renderer. Tile-based spatial indexing is + intentionally omitted in the first iteration; PixiJS native culling + and bounds-based hit testing carry the renderer until profiling + proves otherwise. +- Game math that must stay synchronised between server and client lives + in `pkg/calc/`. The UI client never duplicates calc functions; instead + a bridge layer in `ui/core/calc/` wraps selected `pkg/calc/` functions + for the `Core` API. New shared math is added to `pkg/calc/` first; gaps + are surfaced at the start of each phase that needs them. +- State preservation is a global rule: switching active view or sidebar + tab does not reset state. State resets only on explicit user + `discard` actions or logout. +- History mode is a global read-only toggle that applies to every active + view. The Order sidebar tab is hidden in history mode. +- Wails v2 is the desktop baseline. At the start of Phase 31, the + current state of Wails v3 is re-evaluated; if v3 has reached a stable + release, the migration is folded into that phase. +- CI uses Gitea Actions (workflow files under `.gitea/workflows/`, + format-compatible with GitHub Actions). Linux runners cover Tier 1 + tests; a macOS runner is provisioned only when Tier 2 iOS smoke is + needed. + +## Information Architecture and Navigation + +The client is a single-page application with **one active view at a +time**. Navigation is mobile-first: floating panels never overlap the +map, the main area never splits into multiple visible panels on small +screens. Desktop and mobile share the same model; on desktop, the +sidebar sits beside the active view, on mobile it lives behind a +bottom-tab bar. + +### View model + +``` +ActiveView ∈ { + /login, (anonymous only) + /lobby, (auth required) + /games/:id/map, (default in-game view) + /games/:id/table/:entity, (entity ∈ + planets | ship-classes | + ship-groups | fleets | + sciences | races) + /games/:id/report, + /games/:id/battle/:battleId, + /games/:id/mail, + /games/:id/designer/ship-class/:id?, + /games/:id/designer/science/:id?, +} +``` + +Switching between views happens through the header dropdown (desktop) +or hamburger menu (mobile). Double-tapping a row in a `table:` view +returns to `/map` with `focus=`. Some views can push a +transient map overlay with a back affordance (for example, ship-class +designer pushes a range-preview overlay onto the map). The transient +overlay clears when the user navigates to any other view. + +### Layout per breakpoint + +Desktop (≥ 1024 px): + +``` +┌──────────────────────────────────────────────────────────┐ +│ Header: race · turn N · countdown · view dropdown · ⚙ │ +├────────────────────────────────────────────┬─────────────┤ +│ │ tabs │ +│ active view │ ┌─────────┐ │ +│ (map / table / │ │ Calc │ │ +│ battle / mail / │ │ Inspect │ │ +│ designer / report) │ │ Order │ │ +│ │ └─────────┘ │ +│ │ tool │ +│ │ content │ +│ │ │ +└────────────────────────────────────────────┴─────────────┘ +``` + +Tablet (768–1024 px): same as desktop but sidebar collapses to a +swipe-from-right drawer; a tab bar of three icons sits in the header +right corner. + +Mobile (< 768 px): + +``` +┌──────────────────────┐ +│ ☰ race · turn N · ⚙ │ +├──────────────────────┤ +│ │ +│ active view │ +│ │ +│ │ +│ │ +├──────────────────────┤ +│ ▣ 🧮 📝 ☰ │ +│ Map Calc Order More│ +└──────────────────────┘ +``` + +On mobile, Inspector is not a bottom tab — tapping an object on the map +raises a bottom-sheet showing inspector content. The sheet swipes down +to dismiss. `More` opens a hamburger menu that lists Mail, Battle log, +Tables (planets, ship classes, ship groups, fleets, sciences, races), +History, Settings, Logout. + +### Sidebar tools (single-tool with state preservation) + +- **Calculator** — independent ship/path calculator, callable from any + view. Holds in-progress inputs across navigation. +- **Inspector** — context-sensitive details for the currently selected + map object. Empty state when nothing is selected: `select an object on + the map`. +- **Order** — the draft order being composed. Vertical list of commands, + top-to-bottom. Each command shows its local-validation result while + composing and its server result after submit. Order persists across + page reloads and across view switches. + +### Map active view + +PixiJS canvas with pan/zoom over the torus. A gear icon in the corner +opens a popover (desktop) or bottom sheet (mobile) with category +toggles: + +| Toggleable | Default | +|------------|---------| +| Hyperspace groups | on | +| Incoming groups (not necessarily enemy) | on | +| Cargo routes | on | +| Reach / visibility zones | off | +| Battle and bombing markers | on | + +Planets are always shown and cannot be hidden. + +### Header turn counter and history mode + +The turn counter is clickable. Click expands to a turn navigator +(popover desktop, bottom sheet mobile) listing recent turns with a +search field for jumping to a specific turn number. Selecting a past +turn enters history mode: every active view switches its data source to +that turn's snapshot, the Order sidebar tab disappears, and a +persistent banner reads `Viewing turn N · read-only` with a `Return to +turn current` action. + +### Cross-cutting shell + +- Push-event toasts surface from the top of the screen for: turn ready, + lobby state changes, invitations, session revoked, incoming attack. +- A connection-state indicator in the header shows online / reconnecting + / offline based on push-stream state and last successful unary call. +- The account menu (top-right on desktop, last hamburger entry on + mobile) holds Settings, Sessions, Theme, Language, Logout. + +### Authenticated route transitions + +- `/login` → `/lobby` after successful confirm-email-code. +- `/lobby` → `/games/:id/map` when a game card is selected. +- Any view → `/login` immediately on session revocation push event. +- Designer views can push a transient overlay onto `/map`; the back + affordance returns to the originating designer. + +Per-screen behaviour (validations, exact field names, error mappings) +is derived from `docs/FUNCTIONAL.md` sections cited in the relevant +phases below. UI-specific decisions (animations, layout, microcopy) +live in per-phase topic docs under `ui/docs/`. + +--- + +## Phase 1. Workspace Skeleton + +Status: pending. + +Goal: bring up the `ui/` workspace with a runnable empty Svelte+Vite +frontend and architectural anchors. + +Artifacts: + +- `ui/README.md` mirroring the architectural overview from this plan +- `ui/Makefile` with placeholder targets for every build type (`web`, + `wasm`, `gomobile`, `desktop-{mac,win,linux}`, `ios`, `android`, + `all`) +- `ui/frontend/` Svelte 5 + Vite + TypeScript project scaffolded with + `pnpm create vite` +- `ui/frontend/src/routes/` minimal landing page rendering the app + version string in the page footer +- `ui/.gitignore` covering `node_modules`, `dist`, `*.wasm`, build + outputs for Wails and Capacitor, Playwright artefacts +- `ui/docs/` empty directory ready for per-phase topic docs + +Dependencies: none. + +Acceptance criteria: + +- `pnpm install && pnpm dev` from `ui/frontend` starts a dev server + that serves the landing page at a free local port; +- `make` lists every planned build target as a placeholder; +- `ui/README.md` lists the five target platforms, the layered + architecture, and points readers to per-phase topic docs under + `ui/docs/`. + +Targeted tests: + +- a single Vitest smoke test that mounts the landing component and + asserts the rendered version string is non-empty. + +## Phase 2. Testing Infrastructure + +Status: pending. + +Goal: install and configure the test toolchain that every later phase +depends on, including Tier 1 (per-PR) and Tier 2 (release) targets. + +Artifacts: + +- `ui/frontend/package.json` dev-dependencies: `vitest`, + `@testing-library/svelte`, `@testing-library/jest-dom`, `jsdom`, + `playwright`, `@playwright/test` +- `ui/frontend/vitest.config.ts` configured for Svelte + JSDOM +- `ui/frontend/playwright.config.ts` with three projects: + `chromium-desktop`, `webkit-desktop`, `chromium-mobile-iphone-13`, + `chromium-mobile-pixel-5`; tracing and screenshots enabled on failure +- `ui/.gitea/workflows/test.yaml` running Tier 1 on every push and PR + on a Linux runner: `go test ./...`, `pnpm test`, `pnpm exec + playwright install --with-deps`, `pnpm exec playwright test` +- `ui/.gitea/workflows/release.yaml` running Tier 2 on tag push: + visual regression baseline check, optional macOS runner block for + iOS smoke (Phase 32+ only) +- `ui/docs/testing.md` topic doc naming the two tiers, the tools + per tier, and the rule that visual regression baselines live in + `ui/frontend/tests/__snapshots__/` until shifted to Argos + +Dependencies: Phase 1. + +Acceptance criteria: + +- a placeholder Vitest test passes locally and in CI; +- a placeholder Playwright test passes in `chromium-desktop` and + `webkit-desktop` projects locally; +- the Gitea Actions Tier 1 workflow runs end-to-end against a clean + clone of the repo on a Linux runner. + +Targeted tests: + +- placeholder Vitest test from Phase 1 runs in CI and passes; +- placeholder Playwright test runs in CI on Linux runner and passes + in both `chromium-desktop` and `webkit-desktop` projects; +- intentional failure produces a Playwright trace artefact in CI. + +## Phase 3. Go Core: Canonical Bytes and Keypair + +Status: pending. + +Goal: implement the canonical-bytes serializer and Ed25519 keypair +management in pure Go, with bit-for-bit parity to the gateway-side +implementation. No network, no UI. + +Artifacts: + +- `ui/core/go.mod` module declared in the project Go workspace +- `ui/core/canon/` canonical bytes for `galaxy-request-v1`, + `galaxy-response-v1`, and `galaxy-event-v1`, matching + `docs/ARCHITECTURE.md` §15 byte-for-byte +- `ui/core/keypair/` Ed25519 generate, marshal, unmarshal helpers + returning opaque blobs to upper layers +- `ui/core/types/` envelope structs and result codes +- `ui/core/canon/testdata/` test vectors copied from gateway-side + canonicalisation fixtures +- `ui/core/README.md` documenting the public API and the + network-free / storage-free invariant + +Dependencies: Phase 1. + +Acceptance criteria: + +- canonical-bytes output matches gateway-side fixtures byte-for-byte + for at least three message types (`user.account.read`, + `user.lobby.list`, `user.games.command`); +- a request signed by `ui/core` is accepted by the gateway's own + verifier in a unit test; +- a response signed by gateway test fixtures is accepted by `ui/core`'s + verifier; +- freshness window violations and tampered hashes are rejected with + stable error codes. + +Targeted tests: + +- canonical-bytes equality tests on shared fixtures; +- round-trip sign-then-verify across all three envelope kinds; +- negative tests: tampered `payload_hash`, wrong `request_id`, expired + timestamp, invalid signature length. + +## Phase 4. ConnectRPC Support in Gateway + +Status: pending. Cross-service phase — work happens in `gateway/`, +not `ui/`. + +Goal: enable browsers to call the gateway's authenticated gRPC surface +through ConnectRPC, while preserving the existing native gRPC ingress +for desktop and mobile clients. + +Artifacts: + +- ConnectRPC handler registered alongside existing gRPC server in + `gateway/internal/...` using `connectrpc.com/connect` +- `gateway/buf.gen.yaml` extended to generate Connect-Go code from + existing `.proto` files +- updated `gateway/README.md` and `gateway/openapi.yaml` reflecting + Connect ingress endpoints +- updated `docs/ARCHITECTURE.md` §15 if the deployment topology changes +- `gateway/internal/.../connect_server_test.go` integration test + exercising a unary Connect call and a server-streaming Connect call + +Dependencies: Phase 3 (canonical bytes are needed for the integration +fixtures used here). + +Acceptance criteria: + +- a curl-based unary Connect call from outside the gateway process + succeeds end-to-end against the authenticated surface; +- server-streaming `SubscribeEvents` works over Connect with at least + one delivered event; +- existing native gRPC clients continue to work unchanged; +- both gRPC and Connect handlers share the same upstream business code + (no duplication beyond the protocol layer). + +Targeted tests: + +- Connect unary integration test against a running gateway+backend; +- Connect streaming integration test asserting at least one push event + delivery; +- existing gateway test suite stays green. + +## Phase 5. WASM Build, `WasmCore` Adapter, `GalaxyClient` Skeleton + +Status: pending. + +Goal: package `ui/core` as a WASM module, expose it to TypeScript +through a typed adapter, and prove the WASM-side crypto pipeline at +unit level. End-to-end Connect round-trip is validated in Phase 7 +(authenticated calls only become possible after login). + +Artifacts: + +- `ui/wasm/main.go` TinyGo entry point exporting `Core` API to JS +- `ui/Makefile` target `wasm` producing `core.wasm` and `wasm_exec.js` + under `ui/frontend/static/` +- `ui/frontend/src/platform/core/index.ts` `Core` interface and + build-time target resolver +- `ui/frontend/src/platform/core/wasm.ts` `WasmCore` adapter +- `ui/frontend/src/api/galaxy-client.ts` `GalaxyClient` orchestrating + `Core.signRequest` → ConnectRPC fetch → `Core.verifyResponse` +- `ui/frontend/src/api/connect.ts` typed Connect client built from + generated stubs (Connect codegen via `@bufbuild/protoc-gen-es` and + `@connectrpc/protoc-gen-connect-es`) +- topic doc `ui/docs/wasm-toolchain.md` documenting TinyGo vs + standard Go choice and bundle size measured + +Dependencies: Phases 2, 3, 4. + +Acceptance criteria: + +- `make wasm` produces a deterministic bundle under 1 MB (TinyGo) or + under 3 MB (standard Go fallback); +- `WasmCore.signRequest` produces canonical bytes byte-for-byte + identical to the gateway-side verifier output on shared fixtures + (validated via Vitest with the WASM module loaded in JSDOM); +- `WasmCore` exposes the same TypeScript types as the future + `WailsCore` and `CapacitorCore` will need to satisfy. + +Targeted tests: + +- Vitest unit tests for `WasmCore` calling each public method with a + fixture WASM module loaded in JSDOM; +- Vitest unit tests for `GalaxyClient` using a mock `Core` and a mock + Connect transport; +- Vitest tests asserting `WasmCore.signRequest` output matches gateway + fixtures byte-for-byte for at least three message types. + +## Phase 6. Storage Layer (Web) + +Status: pending. + +Goal: persist the device session keypair securely in browsers, and +provide a generic local cache for game state. Defines the +TypeScript-side `KeyStore` and `Cache` interfaces that desktop and +mobile adapters will satisfy in later phases. + +Artifacts: + +- `ui/frontend/src/platform/store/index.ts` defining `KeyStore` and + `Cache` interfaces +- `ui/frontend/src/platform/store/idb-cache.ts` IndexedDB-backed + `Cache` using the `idb` library +- `ui/frontend/src/platform/store/webcrypto-keystore.ts` WebCrypto + non-exportable Ed25519 key generation and IndexedDB handle + persistence +- `ui/frontend/src/api/session.ts` thin layer that loads or creates the + device session at app startup + +Dependencies: Phase 5. + +Acceptance criteria: + +- a freshly generated keypair survives page reloads and produces + signatures that the gateway accepts; +- clearing site data removes the keypair, and the next request + triggers a re-login flow; +- `KeyStore` and `Cache` interfaces have full TypeScript types and + zero web-specific imports in their public signatures. + +Targeted tests: + +- Vitest unit tests for `IDBCache` with `fake-indexeddb`; +- Vitest unit tests for `WebCryptoKeyStore` exercising generate, load, + sign, clear; +- Playwright integration test: generate keypair, sign a request + through `WasmCore`, send through Connect, verify gateway accepts, + reload the page, sign another request, verify accepted. + +## Phase 7. Auth Flow UI + +Status: pending. + +Goal: implement the full email-code login flow with device session +registration and post-login redirect to a placeholder lobby. + +Artifacts: + +- `ui/frontend/src/routes/login` two-step form (email → code) +- `ui/frontend/src/api/auth.ts` wraps `public.auth.send_email_code` and + `public.auth.confirm_email_code`, registers the public key, persists + via `KeyStore` +- `ui/frontend/src/lib/session-store.ts` Svelte store exposing the + current session state +- `ui/frontend/src/routes/+layout.svelte` redirect to `/login` for + unauthenticated routes; redirect to `/lobby` on successful confirm +- placeholder `ui/frontend/src/routes/lobby/+page.svelte` rendering + `you are logged in` +- topic doc `ui/docs/auth-flow.md` describing error UX, code + resend, expired challenge handling, and re-login on revocation + +Dependencies: Phase 6. + +Acceptance criteria: + +- a fresh browser completes login end-to-end against a local + gateway+backend stack; +- the first authenticated Connect call after login (e.g. + `user.account.read`) succeeds end-to-end through `WasmCore` → + `GalaxyClient` → ConnectRPC → gateway, with the response signature + verified and the payload decoded back to JSON; +- a returning browser resumes the session without re-login; +- gateway-side session revocation closes the active client immediately + and routes back to `/login`. + +Targeted tests: + +- Vitest component tests for the login forms with mocked + `GalaxyClient`; +- Playwright e2e test driving the full flow against a local stack in + desktop and mobile viewports, asserting the first authenticated + Connect call returns successfully after login; +- regression test for revocation: server-side revoke causes client + redirect within one second. + +## Phase 8. Lobby UI + +Status: pending. + +Goal: replace the placeholder lobby with a working list of games +allowing the user to view membership, accept invitations, join public +games, and create new games. + +Artifacts: + +- `ui/frontend/src/routes/lobby/+page.svelte` landing page sections: + my games (`docs/FUNCTIONAL.md` §4.5), public games (§4.2), pending + invitations (§4.3), action to create a new game (§3.3) +- `ui/frontend/src/api/lobby.ts` typed wrappers over the relevant + authenticated RPCs +- `ui/frontend/src/routes/lobby/create/+page.svelte` create-game form + matching backend contract +- routing wiring: clicking a game card navigates to + `/games/:gameId/map` (placeholder until Phase 10) + +Dependencies: Phase 7. + +Acceptance criteria: + +- the user can list, create, join a game, and accept an invitation + end-to-end against a local stack; +- mobile viewport renders without horizontal scroll; +- empty states are explicit (`no games yet`, `no public games`). + +Targeted tests: + +- Vitest component tests for each section with mocked API responses; +- Playwright e2e: complete a create-game flow and confirm the new game + appears in `my games`; +- mobile-viewport Playwright run for the same flow. + +## Phase 9. Map Renderer with Fixture Data + +Status: pending. + +Goal: stand up the PixiJS map renderer with pan/zoom, primitive +drawing, torus wrap behaviour and bounded-plane (no-wrap) mode against +a fixture dataset, before wiring it to live game state. Both modes +are first-class — no-wrap is a real user-selectable view option, not +a deferred nicety. + +Artifacts: + +- `ui/frontend/src/map/world.ts` data model (`Point2D`, `Primitive`, + `Style`, theme bindings) with fixed-point coordinate handling +- `ui/frontend/src/map/render.ts` PixiJS scene graph: background + layer, primitive container, viewport pan/zoom, torus wrap copies, + dual WebGPU/WebGL backend selection +- `ui/frontend/src/map/hit-test.ts` PixiJS-native hit test wrapping + `eventMode` and per-primitive hit slop +- `ui/frontend/src/map/no-wrap.ts` camera clamp helpers + (`CorrectCameraZoom`, `ClampCameraNoWrapViewport`, + `ClampRenderParamsNoWrap`, `PivotZoomCameraNoWrap`) for bounded + plane mode +- `ui/frontend/src/routes/playground/+page.svelte` development page + rendering a fixture world with a mode switch between torus and + no-wrap for visual verification +- topic doc `ui/docs/renderer.md` describing departures from the + Go reference algorithm in `client/world/`, the rationale for + skipping tile-based spatial indexing, and the no-wrap semantics + +Dependencies: Phase 1. + +Acceptance criteria: + +- a 1000-primitive fixture world pans and zooms at 60 fps on a + mid-range laptop with WebGPU and falls back cleanly to WebGL in + both torus and no-wrap modes; +- hit testing returns the same primitive as the reference Go algorithm + on a shared set of fixture cursor positions, in both modes; +- torus wrap renders all four corner copies correctly across the + viewport edges; +- no-wrap mode clamps the camera at world boundaries; pivot zoom + keeps the world point under the cursor stable; viewport never + becomes larger than the world. + +Targeted tests: + +- Vitest unit tests for fixed-point math, torus-shortest distance, + no-wrap clamps, no-wrap pivot zoom invariants; +- Vitest hit-test parity tests against fixtures derived from the Go + reference, covering both torus and no-wrap fixtures; +- Playwright visual smoke test of the playground page in + `chromium-desktop` and `webkit-desktop`, exercising mode switch + torus → no-wrap and back, and verifying camera clamp behaviour at + bounded-plane edges. + +## Phase 10. In-Game Shell with View-Replacement Skeleton + +Status: pending. + +Goal: assemble the in-game layout shell (header, sidebar, main area) +with empty placeholder content for every view, so navigation works +end-to-end before any data is wired. + +Artifacts: + +- `ui/frontend/src/routes/games/[id]/+layout.svelte` shell layout with + responsive breakpoints (desktop / tablet / mobile) +- `ui/frontend/src/lib/header/` header component: race name, turn + counter (static placeholder `turn ?`), view dropdown / hamburger, + account menu +- `ui/frontend/src/lib/sidebar/` sidebar with three tabs (Calculator, + Inspector, Order), each tab content stubbed to `coming soon`; mobile + bottom-tab bar `[Map, Calc, Order, More]` with corresponding stub + panels +- `ui/frontend/src/lib/active-view/` view router supporting + `/games/:id/{map,table/:entity,report,battle/:battleId,mail, + designer/...}` with stub content per view +- topic doc `ui/docs/navigation.md` documenting the active-view + model, the state-preservation rule, and the transient map-overlay + concept (the back-stack mechanism itself is implemented in Phase 34 + when the first overlay user, ship-designer reach circles, ships) + +Dependencies: Phase 8. + +Acceptance criteria: + +- entering `/games/:id/map` from the lobby renders the shell with all + navigation chrome; +- header dropdown switches to every other view; mobile hamburger does + the same; +- sidebar tabs preserve their stub state across switches; +- the responsive layout matches the breakpoint diagrams in + `Information Architecture and Navigation`. + +Targeted tests: + +- Vitest component tests for header navigation actions; +- Playwright e2e: visit every view stub via header dropdown, assert + empty state copy renders; +- multi-viewport Playwright run validating layout switches at the 768 + px and 1024 px breakpoints. + +## Phase 11. Map Wired to Live Game State + +Status: pending. + +Goal: replace the map fixture with real planet data fetched from the +gateway for the selected game; planets only, read-only. + +Artifacts: + +- `ui/frontend/src/api/game-state.ts` fetch latest game state via + `user.games.report` +- `ui/frontend/src/map/state-binding.ts` map-state synchroniser + applying planets to the renderer +- `ui/frontend/src/lib/active-view/map.svelte` integrates the renderer + with live data and a loading state, defaulting to torus mode and + reading the per-game wrap-scrolling preference from `Cache` (toggle + itself is exposed in Phase 29) +- `ui/frontend/src/lib/header/turn-counter.svelte` reads the live + turn number from game state + +Dependencies: Phases 9, 10. + +Acceptance criteria: + +- entering `/games/:id/map` for a game with real planets renders them + on the map; +- the turn counter in the header reflects the actual turn number; +- map state refreshes on tab focus; +- view mode (torus / no-wrap) honours the per-game preference if set, + defaults to torus otherwise. + +Targeted tests: + +- Vitest unit tests for `state-binding.ts` translating report data to + primitives; +- Playwright e2e against a local stack with seeded game state; +- regression test: zero-planet game shows the map empty without errors; +- regression test: per-game wrap-scrolling preference persists and is + applied on next visit to the game. + +## Phase 12. Order Composer Skeleton + +Status: pending. + +Goal: implement the empty order composer as a persistent vertical list +that survives navigation and reloads, ready to receive commands in +later phases. + +Artifacts: + +- `ui/frontend/src/lib/sidebar/order-tab.svelte` vertical command list + with empty state copy +- `ui/frontend/src/sync/order-draft.ts` draft order store backed by + `Cache`, persisting across reloads +- `ui/frontend/src/sync/order-types.ts` typed command shape + (`OrderCommand` discriminated union) +- topic doc `ui/docs/order-composer.md` describing the + draft-replaces-server-order model, the local-validation invariant, + and command status semantics + +Dependencies: Phases 6, 10. + +Acceptance criteria: + +- programmatically adding a stub command shows it in the order tab; +- closing and reopening the browser preserves the draft; +- removing a command from the order tab persists the removal; +- the order tab is hidden in history mode (Phase 26) — wired here as a + prop so later phases can flip it. + +Targeted tests: + +- Vitest unit tests for `order-draft` covering add, remove, reorder, + persistence; +- Playwright e2e: programmatically add three stub commands, reload, + assert all three persist. + +## Phase 13. Inspector — Planet (Read-Only) + +Status: pending. + +Goal: show planet details in the inspector when a planet is clicked +on the map; read-only, no actions yet. + +Artifacts: + +- `ui/frontend/src/lib/sidebar/inspector-tab.svelte` empty state + (`select an object on the map`) and routing per selected-object kind +- `ui/frontend/src/lib/inspectors/planet.svelte` read-only display of + every planet field documented in `docs/FUNCTIONAL.md` §6 and the + [`rules.txt`](../game/rules.txt) planet section: name, coordinates, size, population, + industry, materials stockpile, industry stockpile, colonists, + natural resources, current production type, free production + potential +- map click handler that selects the planet and switches sidebar to + Inspector (or raises bottom-sheet on mobile) +- selection store `ui/frontend/src/lib/selection.ts` holding the + currently selected map object id + +Dependencies: Phase 11. + +Acceptance criteria: + +- clicking any visible planet on the map shows its details in the + inspector tab on desktop and bottom-sheet on mobile; +- selection state persists across view switches (per global state- + preservation rule); +- empty inspector renders the empty-state message when no planet is + selected. + +Targeted tests: + +- Vitest component tests for the planet inspector with fixture data; +- Playwright e2e: click a seeded planet, assert all expected fields are + rendered; +- mobile-viewport Playwright run validating the bottom-sheet + presentation. + +## Phase 14. First End-to-End Command — Rename Planet + +Status: pending. + +Goal: prove the entire pipeline (inspector → composer → submit → +server → state refresh) by wiring up exactly one action: renaming a +planet. + +Artifacts: + +- `ui/frontend/src/lib/inspectors/planet.svelte` adds a `Rename` action + that opens a small inline editor and adds a `RenamePlanet` command + to the order draft on confirm +- `ui/frontend/src/sync/submit.ts` `submitOrder()` function that POSTs + the entire draft via `GalaxyClient.execute('user.games.order', ...)` + and applies per-command results +- `ui/frontend/src/lib/sidebar/order-tab.svelte` adds a `Submit order` + button calling `submitOrder()` and renders accepted / rejected + status per command after submit +- on successful submit, refresh game state so the rename appears on the + map and in the inspector + +Dependencies: Phases 12, 13. + +Acceptance criteria: + +- the user can select a planet, click `Rename`, type a new name, see + the command appear in the order tab, click `Submit`, and observe the + planet's name change everywhere within one second; +- attempting an empty or invalid name is blocked locally (button + disabled with tooltip); +- a server-side rejection (race condition) is surfaced as `rejected` + status in the order tab. + +Targeted tests: + +- Vitest unit tests for `submitOrder` with mocked `GalaxyClient`; +- Vitest component test for the inline rename editor including + validation; +- Playwright e2e: rename a seeded planet, reload, confirm the new name + persists. + +## Phase 15. Inspector — Planet Production Controls + +Status: pending. + +Goal: let the user switch a planet's production type to industry, +materials, research a science, or build a ship class; each change +appends a command to the order draft. + +Artifacts: + +- `ui/frontend/src/lib/inspectors/planet/production.svelte` segmented + control with the four production options; a sub-picker for science + and ship class targets +- `ui/frontend/src/sync/order-types.ts` extends with + `SetProductionType` command variant +- references to `pkg/calc/` predictions (free production potential, + forecast output for current type) — wired through `ui/core/calc/` +- audit `ui/docs/calc-bridge.md` updates this phase's required calc + functions; if any are missing in `pkg/calc/`, raise as blocker + +Dependencies: Phase 14. + +Acceptance criteria: + +- changing production type adds exactly one `SetProductionType` + command to the order draft; +- repeated changes for the same planet collapse to the latest choice + (no duplicate commands per planet); +- forecast output number reflects the chosen production type and + matches `pkg/calc/` outputs. + +Targeted tests: + +- Vitest unit tests for the collapse-duplicates logic in order draft; +- Vitest component tests for forecast number rendering; +- Playwright e2e: switch production three times, submit, confirm + server reflects the latest choice. + +## Phase 16. Inspector — Cargo Routes + +Status: pending. + +Goal: configure up to four cargo routes per planet (colonists, +industry, materials, empty) through the inspector. + +Artifacts: + +- `ui/frontend/src/lib/inspectors/planet/cargo-routes.svelte` + four-slot UI listing existing routes and offering add / edit / + remove +- `ui/frontend/src/sync/order-types.ts` extends with + `SetCargoRoute` and `RemoveCargoRoute` command variants +- destination-planet picker filtered by reach (uses `pkg/calc/` reach + function via `ui/core/calc/`) +- `ui/frontend/src/map/cargo-routes.ts` renders route arrows on the + map between source and destination planet, styled per cargo type +- topic doc `ui/docs/cargo-routes-ux.md` capturing the priority + semantics from [`rules.txt`](../game/rules.txt) (`colonists → industry → materials → + empty`) + +Dependencies: Phase 15. + +Acceptance criteria: + +- the user can add, edit, and remove cargo routes through the + inspector; +- destination picker disables planets outside reach with a tooltip + explaining the constraint; +- the four route types are mutually exclusive — only one route per + type per source planet; +- configured routes are rendered as arrows on the map between source + and destination planets, distinguishable per cargo type. + +Targeted tests: + +- Vitest unit tests for slot-conflict detection; +- Vitest unit tests for cargo-route arrow rendering on torus and + no-wrap fixtures; +- Playwright e2e: add a route end-to-end, confirm server applies it + on next turn and the arrow is visible on the map. + +## Phase 17. Ship Classes — CRUD Without Calc + +Status: pending. + +Goal: list, view, and edit ship classes through a dedicated table view +and a designer view; numeric calculations are stubbed pending Phase +18. + +Artifacts: + +- `ui/frontend/src/routes/games/[id]/table/ship-classes/+page.svelte` + table of ship classes with sort and filter +- `ui/frontend/src/routes/games/[id]/designer/ship-class/[id]/+page.svelte` + designer form with all five fields (Drive, Armament, Weapons, + Shields, Cargo) plus name; validation rules from [`rules.txt`](../game/rules.txt) + (each field 0 or ≥1; armament integer; weapons and armament both + zero or both nonzero) +- `ui/frontend/src/sync/order-types.ts` extends with + `CreateShipClass` and `UpdateShipClass` command variants + +Dependencies: Phase 14. + +Acceptance criteria: + +- the user can create, list, edit, and delete ship classes; +- field validation matches [`rules.txt`](../game/rules.txt) constraints with disabled + Submit + tooltip when invalid; +- double-tapping a row in the ship-classes table opens its designer. + +Targeted tests: + +- Vitest component tests for designer field validation; +- Playwright e2e: create a class, list it, edit it, delete it. + +## Phase 18. Ship Classes — Calc Bridge + +Status: pending. + +Goal: wire `pkg/calc/` ship math into the designer for live mass, +speed, range, and cargo capacity previews. + +Artifacts: + +- `ui/core/calc/ship.go` thin Go bridge wrapping `pkg/calc/.FullMass`, + `EmptyMass`, `Speed`, `CargoCapacity`, `WeaponsBlockMass`, + `DriveEffective` in JSON-marshallable signatures, exported through + the `Core` API +- `ui/frontend/src/platform/core/index.ts` extends `Core` interface + with the new calc methods +- live-updating preview pane in the ship-class designer showing mass, + full-load mass, max speed, range, and cargo capacity at the player's + current tech levels +- audit step recorded in `ui/docs/calc-bridge.md`: every wired + function listed against its `pkg/calc/` source +- if any required `pkg/calc/` function is missing, this phase raises a + blocker and the function is added to `pkg/calc/` first (owner-led) + +Dependencies: Phases 5 (Core skeleton), 17. + +Acceptance criteria: + +- changing any field in the designer updates the preview within one + frame on a mid-range laptop; +- preview values are byte-for-byte identical to direct `pkg/calc/` + calls on shared fixtures; +- the bridge contains zero math beyond marshalling adapters. + +Targeted tests: + +- Go parity tests in `ui/core/calc/` against `pkg/calc/` outputs on + shared fixtures; +- Vitest snapshot tests for the preview pane on canonical inputs; +- Playwright e2e: edit a ship class, observe preview updates and + submit, confirm server-side mass matches. + +## Phase 19. Inspector — Ship Group (Read-Only) + +Status: pending. + +Goal: render ship groups on the map and display group details in the +inspector when a group is selected; read-only, no actions yet. + +Artifacts: + +- `ui/frontend/src/map/ship-groups.ts` renders ship groups on the map: + own and visible foreign groups stationed on planets, groups in + hyperspace at their current coordinates, and incoming groups with a + distinct visual style and an ETA label +- `ui/frontend/src/map/state-binding.ts` extends to feed groups into + the renderer alongside planets +- `ui/frontend/src/lib/inspectors/ship-group.svelte` read-only display + of group fields: class, count, tech levels of components, location + (planet or hyperspace coordinates), cargo type and amount, fleet + membership +- map click handler that selects a group and switches sidebar to + Inspector (or raises bottom-sheet on mobile) +- selection store extended to support `ShipGroup` selections + +Dependencies: Phases 11, 13. + +Acceptance criteria: + +- own and visible foreign ship groups render on the map for a seeded + game in both torus and no-wrap modes; +- incoming groups are visually distinct and show ETA; +- clicking any rendered group shows its details in the inspector; +- groups in hyperspace show coordinates and remaining distance in the + inspector; +- cargo type and amount display when applicable. + +Targeted tests: + +- Vitest unit tests for the rendering of each group variant + (on-planet, in-hyperspace, incoming); +- Vitest component tests for the ship-group inspector with fixture + data covering planet-based, hyperspace, and cargo-loaded variants; +- Playwright e2e: click each variant from a seeded game, assert all + expected fields render. + +## Phase 20. Inspector — Ship Group Actions + +Status: pending. + +Goal: enable group operations from the inspector: split, send, load, +unload, modernize, dismantle, transfer to race, add to fleet. + +Artifacts: + +- action buttons in `ui/frontend/src/lib/inspectors/ship-group.svelte` + with disabled-state and tooltip when local validation rejects +- `ui/frontend/src/sync/order-types.ts` extends with `SplitGroup`, + `SendGroup`, `LoadCargo`, `UnloadCargo`, `Modernize`, `Dismantle`, + `TransferToRace`, `AssignToFleet` command variants +- `Send` action picks destination through a planet picker filtered by + the group's reach (uses `pkg/calc/` reach function via Core) +- `Modernize` cost preview using `pkg/calc/` formula via Core +- confirmation dialog for `Dismantle` over a foreign planet with + colonists onboard (special-case from [`rules.txt`](../game/rules.txt): colonists die) + +Dependencies: Phases 18, 19. + +Acceptance criteria: + +- every action either adds the corresponding command to the order draft + or is disabled with a tooltip explaining why; +- splitting a group of N into K and N-K results in two valid commands + (the implicit split + the action); +- destructive actions surface explicit confirmation dialogs; +- end-to-end execution: send a group, submit order, observe arrival + next turn. + +Targeted tests: + +- Vitest unit tests for action enablement logic per action; +- Vitest component tests for the dismantle-with-colonists confirmation; +- Playwright e2e for at least one complete flow (send a group between + two planets) against a local stack. + +## Phase 21. Sciences — CRUD List + Designer + +Status: pending. + +Goal: define and manage sciences (named mixes of tech proportions +summing to 1.0) through a table view and a designer. + +Artifacts: + +- `ui/frontend/src/routes/games/[id]/table/sciences/+page.svelte` + list of sciences with name and four tech proportions +- `ui/frontend/src/routes/games/[id]/designer/science/[id]/+page.svelte` + designer with four numeric inputs that auto-normalise to 1.0 and a + name field +- `ui/frontend/src/sync/order-types.ts` extends with `CreateScience` + and `UpdateScience` command variants +- topic doc `ui/docs/science-designer-ux.md` covering + auto-normalisation, validation, and the relationship to the planet + production picker (Phase 15) + +Dependencies: Phase 17. + +Acceptance criteria: + +- the user can create, edit, and delete sciences; +- proportions auto-normalise on edit so the sum is always 1.0; +- the planet production picker (Phase 15) lists the user's sciences + and lets the user select one for research production; +- name validation matches [`rules.txt`](../game/rules.txt) constraints (length, allowed + characters, special characters not at start/end, no triple repeats). + +Targeted tests: + +- Vitest unit tests for proportion normalisation; +- Vitest unit tests for science name validation; +- Playwright e2e: create a science, set a planet to research it, + submit, confirm. + +## Phase 22. Races View — War/Peace Toggle and Votes + +Status: pending. + +Goal: list other races with their visible stats, expose war/peace +toggle and the voting UI. + +Artifacts: + +- `ui/frontend/src/routes/games/[id]/table/races/+page.svelte` table + with one row per race, including name, tech levels, total + population, total production, planet count, war-or-peace from this + race's perspective, votes received +- per-row toggle for declaring war or peace (adds + `SetDiplomaticStance` command) +- voting control: a single slot for `give my votes to ` (adds + `SetVoteRecipient` command) +- alliance summary panel showing the current vote graph and any + alliance reaching ≥ 2/3 of total votes + +Dependencies: Phase 14. + +Acceptance criteria: + +- the user can toggle war / peace and change vote recipient; +- the alliance summary updates after a server roundtrip; +- vote counts match server state byte-for-byte. + +Targeted tests: + +- Vitest component tests for the alliance summary on canonical fixtures + (chain of votes, fork, win condition); +- Playwright e2e: change diplomatic stance and vote, submit, confirm. + +## Phase 23. Reports View — Current Turn Sections + +Status: pending. + +Goal: present every section of the current turn's report as readable +panels, mirroring the structure documented in [`rules.txt`](../game/rules.txt) and +`docs/FUNCTIONAL.md` §6.4. + +Artifacts: + +- `ui/frontend/src/routes/games/[id]/report/+page.svelte` scrollable + layout with one section per report category (galaxy summary, votes, + player status, my sciences, foreign sciences, my ship classes, + foreign ship classes, battles, bombings, approaching groups, my + planets, ships in production, cargo routes, foreign planets, + uninhabited planets, unknown planets, my fleets, my ship groups, + foreign ship groups, unidentified groups) +- per-section anchor navigation in a sticky sidebar for quick jumping +- a `back to map` action visible at all times + +Dependencies: Phase 11. + +Acceptance criteria: + +- every report section renders for a seeded game with non-empty data; +- empty sections render explicit empty-state copy; +- scroll position resets when switching to another view and is + restored on return. + +Targeted tests: + +- Vitest component tests for one representative section per data shape + (table, list, sub-table); +- Playwright e2e: open the report, scroll to each section via anchor + navigation, assert content present. + +## Phase 24. Push Events — Turn-Ready + +Status: pending. + +Goal: subscribe to the server push stream and refresh client state +when a turn-ready event arrives. + +Artifacts: + +- `ui/frontend/src/api/events.ts` push-stream subscription wired + through `GalaxyClient.subscribeEvents` and Connect server-streaming +- on `game.turn.ready` event: invalidate `(game_id, current_turn)` + cache entries and trigger a fresh report fetch +- a top-of-screen toast: `Turn N+1 is ready. View now.` with a button + that re-renders the active view against the new turn +- mandatory event signature verification through `ui/core` — any + verification failure tears down the stream and reconnects with + exponential backoff + +Dependencies: Phases 23, 4 (Connect streaming in gateway). + +Acceptance criteria: + +- a server-side turn cutoff produces a toast within one second; +- accepting the toast refreshes the active view to the new turn's data + without a full page reload; +- a forged event (test fixture with bad signature) is rejected and the + stream reconnects. + +Targeted tests: + +- Vitest unit tests for `events.ts` handling subscribe, event + dispatch, error backoff; +- Playwright e2e: trigger a server turn, observe toast and refresh. + +## Phase 25. Sync Protocol — Order Queue, Retry, Conflict + +Status: pending. + +Goal: make the order draft survive network failures and turn cutoffs +gracefully, with explicit user feedback on conflicts. + +Artifacts: + +- `ui/frontend/src/sync/order-queue.ts` send loop: on disconnect, hold + the most recent submit; on reconnect, retry once; on persistent + failure, surface error to the order tab +- conflict detection: if the server returns `turn_already_closed` for + a submit, mark the entire draft as `conflict` and surface a + `Turn N closed before your order was accepted. Edit and resubmit.` + banner in the order tab +- topic doc `ui/docs/sync-protocol.md` covering queue semantics, + retry budgets, and conflict UX + +Dependencies: Phases 14, 24. + +Acceptance criteria: + +- submitting an order while offline queues it and submits successfully + on reconnect; +- a turn cutoff between draft and submit produces a visible conflict + banner with no data loss; +- the order tab clearly distinguishes `draft`, `submitting`, + `accepted`, `rejected`, `conflict` states per command. + +Targeted tests: + +- Vitest unit tests for `order-queue` covering all state transitions; +- Playwright e2e: simulate network drop using Playwright's offline + mode, submit an order, restore network, confirm submission; +- regression test: force a turn cutoff during submit, assert conflict + banner appears. + +## Phase 26. History Mode + +Status: pending. + +Goal: let the user navigate to past turns and view all data as it was, +with no order composition allowed. + +Artifacts: + +- `ui/frontend/src/lib/header/turn-navigator.svelte` clickable turn + counter expansion: popover (desktop) / bottom-sheet (mobile) listing + recent turns and a search field for jumping to a turn number +- `ui/frontend/src/lib/history-mode.ts` global toggle wired into every + view's data source: when active, all `state-binding`, table, report, + inspector, and map sources read from the historical snapshot for the + selected turn +- `ui/frontend/src/lib/header/history-banner.svelte` persistent banner + reading `Viewing turn N · read-only` with a `Return to current turn` + action +- order tab hidden in history mode (already prepared in Phase 12) + +Dependencies: Phases 11, 12, 23. + +Acceptance criteria: + +- selecting a past turn from the navigator switches every view to that + turn's data; +- order tab disappears from the sidebar; calculator tab remains + available; +- returning to the current turn restores live data and re-shows the + order tab with the prior draft intact (state preservation); +- all UI views (map, tables, report, battle, mail) work in history + mode. + +Targeted tests: + +- Vitest unit tests for `history-mode` toggle and per-view source + selection; +- Playwright e2e: enter history mode, navigate three views, return, + confirm the order draft survived. + +## Phase 27. Battle Viewer + +Status: pending. + +Goal: render battles as a dedicated view with playback controls +(play, pause, step forward, step backward, rewind), driven by the +server-side combat log; render battle and bombing markers on the map. + +Artifacts: + +- `ui/frontend/src/map/battle-markers.ts` renders markers on the map + for current-turn battles and bombings within visibility, clickable + to open the battle viewer +- `ui/frontend/src/routes/games/[id]/battle/[battleId]/+page.svelte` + view with the combatant list, the round-by-round log, and a player + control bar +- `ui/frontend/src/lib/battle-player/` round timeline, current-round + highlight, per-shot animation +- entry points to the viewer: marker on map, row in the report's + battles section, push-event toast when a battle this turn involved + the player +- topic doc `ui/docs/battle-viewer-ux.md` covering playback + semantics, accessibility (the combat log must be readable as text + for users who skip animations) + +Dependencies: Phase 23. + +Acceptance criteria: + +- battle and bombing markers render on the map for the seeded + current-turn report and are clickable to open the viewer; +- the viewer plays back any battle in the seeded report including + multi-round and one-sided battles; +- step controls allow precise inspection; +- the same data is accessible as a static text log for accessibility. + +Targeted tests: + +- Vitest unit tests for round-state transitions; +- Vitest unit tests for marker rendering on torus and no-wrap + fixtures; +- Playwright e2e: click a battle marker on the map, play through, + step backward, return to the report. + +## Phase 28. Diplomatic Mail View + +Status: pending. + +Goal: implement a mail inbox and compose flow as a dedicated view that +replaces the map. + +Artifacts: + +- `ui/frontend/src/routes/games/[id]/mail/+page.svelte` two-pane on + desktop (list + reading), one-pane stack on mobile +- compose form for new messages targeting any other race in the game +- inbox sorted by arrival time, with read/unread state persisted via + `Cache` +- push-event integration: new mail surfaces a toast and increments an + unread badge in the header + +Dependencies: Phases 22, 24. + +Acceptance criteria: + +- the user can read incoming mail, compose new mail, and reply to mail + end-to-end; +- unread state persists across reloads; +- server-side delivery confirmations appear on the message thread. + +Targeted tests: + +- Vitest component tests for the compose form including field + validation; +- Playwright e2e: send a message between two seeded players, confirm + arrival. + +## Phase 29. Map Toggles + +Status: pending. + +Goal: deliver the gear-icon control for hiding categories of map +content and switching between torus and no-wrap view modes. All +toggleable categories are already rendered by earlier phases; this +phase only exposes the controls. + +Artifacts: + +- `ui/frontend/src/lib/active-view/map-toggles.svelte` gear icon in + the map view's corner; popover (desktop) / bottom sheet (mobile) +- two sections inside the popover: + - object visibility: hyperspace groups, incoming groups, cargo + routes, reach / visibility zones, battle and bombing markers + - view options: wrap scrolling (torus / no-wrap) +- planets are always rendered and not toggleable +- `ui/frontend/src/lib/map/reach-zones.ts` implementation of reach / + visibility zone overlays, off by default (the only category not yet + rendered by earlier phases) +- toggle state persists per game in `Cache` + +Dependencies: Phases 9 (no-wrap engine), 11 (planets), 16 (cargo +routes), 19 (groups, incoming), 27 (battle markers). + +Acceptance criteria: + +- toggling each object visibility category hides or shows the + corresponding objects on the map within one frame; +- switching wrap scrolling switches the renderer between torus and + no-wrap mode without losing camera position when possible; +- toggle state persists across reloads per game; +- the gear popover is reachable on mobile through a comfortable tap + target (≥ 44 px). + +Targeted tests: + +- Vitest component tests for toggle state persistence; +- Vitest unit tests for reach-zone rendering on torus and no-wrap + fixtures; +- Playwright e2e in desktop and mobile viewports: toggle each + category and the wrap scrolling, assert visual change. + +## Phase 30. Calculator Tab + +Status: pending. + +Goal: ship an independent calculator in the sidebar, callable from any +view, exposing the full set of `pkg/calc/` functions wired through +`Core`. + +Artifacts: + +- `ui/frontend/src/lib/sidebar/calculator-tab.svelte` UI with mode + selector (ship calculator, path calculator, modernization cost, + bombing power) and per-mode forms +- bridge entries in `ui/core/calc/` for any function not already + wrapped by Phase 18 +- topic doc `ui/docs/calculator-ux.md` documenting modes, + layouts, and the rule that calculator inputs persist across + navigation + +Dependencies: Phase 18. + +Acceptance criteria: + +- every calculator mode produces results identical to direct + `pkg/calc/` calls; +- inputs persist across view switches per global state-preservation + rule; +- calculator works in history mode against the snapshot's tech levels. + +Targeted tests: + +- Vitest snapshot tests per mode on canonical inputs; +- Playwright e2e: switch modes, confirm input persistence. + +## Phase 31. Wails Desktop Wrapper + +Status: pending. Re-evaluate Wails v2 vs v3 at phase start. + +Goal: build a native desktop app for macOS, Windows, and Linux that +runs the same frontend bundle and replaces the WASM core with embedded +Go code. + +Artifacts: + +- topic doc `ui/docs/wails-version.md` recording the v2-vs-v3 + decision made at phase start with rationale +- `ui/desktop/main.go` Wails entry point +- `ui/desktop/app.go` IPC bindings exposing `ui/core` API to the + WebView through a structured adapter +- `ui/desktop/keychain/` per-OS secure-storage helpers (macOS Keychain + via `Security` framework, Windows DPAPI, Linux Secret Service / file + fallback at `~/.config/galaxy/keypair` with mode `0600`) +- `ui/desktop/sqlite/` `modernc.org/sqlite` cache wired through Wails + IPC +- `ui/frontend/src/platform/core/wails.ts` `WailsCore` adapter +- `ui/frontend/src/platform/store/wails.ts` `WailsKeyStore` and + `WailsCache` adapters +- `ui/desktop/build/icon.icns` macOS app icon +- `ui/desktop/build/icon.ico` Windows app icon +- `ui/desktop/build/icon.png` Linux app icon +- `ui/Makefile` targets `desktop-mac`, `desktop-win`, `desktop-linux` +- topic doc `ui/docs/desktop-secure-storage.md` documenting the + Linux/Windows file fallback for missing keychains + +Dependencies: Phase 6 (KeyStore and Cache interfaces); Phases 7 +through 30 in their web form (the desktop wrapper exercises the same +TypeScript code). + +Acceptance criteria: + +- the macOS, Windows, and Linux binaries each launch, complete login, + and preserve the keypair across restarts on a fresh user profile; +- a single source codebase produces all three OS bundles; +- the same `Core` and `Storage` TypeScript interfaces are satisfied as + on web, with no platform-specific code outside `platform/`; +- Linux file fallback activates when Secret Service is absent and + writes with `0600` permissions. + +Targeted tests: + +- Go unit tests for each keychain helper, including file fallback; +- desktop e2e smoke test driven by Wails headless mode running the + Phase 7 login Playwright scenario via CDP; +- regression test: keychain absence on a Linux container without + libsecret falls back to file storage. + +## Phase 32. Capacitor Mobile Wrapper + +Status: pending. + +Goal: build native iOS and Android apps that run the same frontend +bundle and call into a gomobile-compiled `ui/core`. + +Artifacts: + +- `ui/mobile-bridge/bridge.go` gomobile-friendly façade over `ui/core` +- `ui/Makefile` target `gomobile` producing `Galaxy.framework` and + `galaxy.aar` +- `ui/mobile/capacitor.config.ts` Capacitor project configuration +- `ui/mobile/plugins/galaxy-core/` custom Capacitor plugin (Swift + + Kotlin) wrapping the gomobile artifacts +- `ui/frontend/src/platform/core/capacitor.ts` `CapacitorCore` adapter +- `ui/frontend/src/platform/store/capacitor.ts` `CapacitorKeyStore` + and `CapacitorCache` using `@capacitor-community/secure-storage-plugin` + and `@capacitor-community/sqlite` +- `ui/mobile/ios/App/Assets.xcassets/AppIcon.appiconset/` iOS app + icon set +- `ui/mobile/android/app/src/main/res/mipmap-*/` Android app icon + set +- iOS launch screen and Android splash screen +- `ui/Makefile` targets `ios` and `android` +- topic doc `ui/docs/mobile-bridge.md` describing the plugin + API, marshalling strategy, and the manual smoke procedure for this + phase + +Dependencies: Phase 6; Phases 7 through 30 in their web form. + +Acceptance criteria: + +- both the iOS Simulator and an Android Emulator launch the app, + complete login, and preserve the keypair across restarts (validated + by manual smoke); +- the same `Core` and `Storage` TypeScript interfaces are satisfied as + on web and desktop; +- gomobile build produces deterministic outputs reproducible in CI on + a macOS runner. + +Targeted tests: + +- Go unit tests for the `mobile-bridge` façade; +- Capacitor plugin unit tests on iOS (XCTest) and Android (Espresso); +- manual smoke procedure: login flow on iOS Simulator and Android + Emulator, recorded in `ui/docs/mobile-bridge.md`. Full Appium + automation lands in Phase 36 as part of the acceptance pass. + +## Phase 33. PWA — Service Worker, Manifest, Web Icons + +Status: pending. + +Goal: make the web build installable and offline-tolerant on every +browser. Native packaging icons live with their respective wrapper +phases (31 for desktop, 32 for mobile) — this phase is web-only. + +Artifacts: + +- `ui/frontend/src/service-worker.ts` cache-first asset strategy with + stale invalidation on app update +- `ui/frontend/static/manifest.webmanifest` PWA manifest +- `ui/frontend/static/icons/` web icon set sized per + `manifest.webmanifest` requirements +- topic doc `ui/docs/pwa-strategy.md` covering update flow and + offline scope + +Dependencies: Phase 25 (offline order queue). + +Acceptance criteria: + +- the web app installs as a PWA on Chrome, Edge, and iOS Safari; +- the service worker survives an app update without serving stale code + on the next reload. + +Targeted tests: + +- Lighthouse PWA audit at score ≥ 90; +- Playwright test: install the app, take it offline, verify the cached + login route still loads; +- regression test: bumping the app version invalidates the prior + service worker. + +## Phase 34. Multi-Turn Projection — Single-Turn Forecast and Range Circles + +Status: pending. Long-term scope deferred but this phase ships real +features. + +Goal: ship two concrete projection features (planet next-turn +forecast and ship-designer reach circles) plus the transient +map-overlay back-stack mechanism that the reach-circles feature is +the first user of. + +Artifacts: + +- `ui/frontend/src/lib/projection/` minimal projection engine that + computes one-turn-ahead state for a single planet using `pkg/calc/` +- planet inspector forecast section showing next-turn population, + industry, materials stockpile, and production progress +- `ui/frontend/src/lib/navigation/transient-overlay.ts` push/pop + back-stack mechanism for map overlays driven by other views, with + a back-button affordance on the map that returns to the originating + view with state preserved +- ship-designer `Preview range on map` action that pushes a transient + overlay onto the map showing concentric reach circles for 1, 2, 3, + 4 turns from a chosen origin, computed from the in-progress ship + design and the player's current Drive tech via `ui/core/calc/` +- topic doc `ui/docs/multi-turn-projection.md` describing the + long-term vision (multi-turn planning mode, scenario branches) and + the phased path to it + +Dependencies: Phases 17, 18. + +Acceptance criteria: + +- the planet inspector shows a forecast section with next-turn values + matching `pkg/calc/` outputs; +- the ship-designer `Preview range on map` button transitions to the + map with reach circles drawn from the chosen origin; back returns + to the designer with all in-progress state intact; +- the transient overlay is cleared if the user navigates to any other + view via the header dropdown. + +Targeted tests: + +- Vitest unit tests for the projection engine on canonical fixtures; +- Vitest unit tests for the transient-overlay push/pop logic and + state preservation; +- Playwright e2e: open a planet inspector, observe one-turn forecast; + open a ship designer, click `Preview range on map`, see reach + circles, click back, return with state intact. + +## Phase 35. Polish — Accessibility, Localisation, Error UX + +Status: pending. + +Goal: prepare the client for technical beta with end-user-quality +polish. + +Artifacts: + +- `ui/frontend/src/lib/i18n/` translation bundles for English and + Russian, covering every visible string +- `ui/frontend/src/lib/error/` central error surface with stable codes + and retry / escalation guidance +- accessibility audit results recorded under `ui/docs/a11y.md` +- keyboard-only navigation paths for lobby, game view, and login +- focus rings, ARIA labels, screen-reader-only text where needed + +Dependencies: Phase 33. + +Acceptance criteria: + +- WCAG 2.2 AA compliance on lobby, login, and the in-game shell per + axe-core scan; +- the entire UI is reachable by keyboard only with visible focus + rings; +- every server-side error is mapped to a translated, actionable user + message in both languages; +- locale switch persists across reloads on every platform. + +Targeted tests: + +- axe-core integration tests on every top-level view; +- Vitest tests for the i18n bundle structure and missing-translation + detection; +- Playwright keyboard-only navigation tests. + +## Phase 36. Acceptance Pass + +Status: pending. + +Goal: reconcile implementation, documentation, and regression coverage +before declaring the client ready for technical beta. + +Artifacts: + +- updated `ui/README.md`, topic docs, and any drift in + `docs/ARCHITECTURE.md` or `docs/FUNCTIONAL.md` (mirrored to + `docs/FUNCTIONAL_ru.md`) +- final cross-platform regression run on a release-candidate build +- `ui/docs/release-checklist.md` for repeatable releases +- visual regression baselines committed under + `ui/frontend/tests/__snapshots__/`; if maintenance proves heavy, + follow-up issue to switch to self-hosted Argos +- Appium harness for iOS Simulator and Android Emulator covering the + login flow, push-event flow, and at least one full turn loop; + `.gitea/workflows/release.yaml` extended with macOS-runner Appium + job (mandatory pre-release gate) + +Dependencies: Phases 1 through 35. + +Acceptance criteria: + +- implementation matches every documented contract and live topic + doc; +- the cross-cutting regression scenarios listed below pass on web, + desktop, and mobile; +- Appium smoke passes on both iOS and Android in CI. + +Targeted tests: + +- run focused package tests for `ui/core` and every TypeScript + module; +- rerun cross-platform Playwright suites against release-candidate + builds; +- run Tier 2 visual regression baselines; +- run Appium smoke suites on iOS and Android. + +--- + +## Cross-Cutting Regression Scenarios + +- A fresh device generates a keypair, completes email-code login, and + successfully signs a follow-up authenticated request on every target + platform. +- A returning device resumes its session without re-login, preserves + queued orders, and continues receiving push events without gaps. +- Server-side session revocation tears down the active push stream and + forces a re-login on every target platform within one second. +- Tampering with `payload_bytes`, `payload_hash`, `request_id`, + `message_type`, or any signature byte is rejected by the verifier + in `ui/core` with a stable error code. +- Requests outside the freshness window are rejected before they + reach network, and the client surfaces a clock-skew warning when + its local clock disagrees with the server time event by more than + the freshness window. +- The map renderer holds 60 fps with a 1000-primitive fixture on + mid-range hardware on web (Chrome, Edge, Safari, Firefox), desktop + (Wails on macOS, Windows, Linux), and mobile (latest iPhone, mid- + range Android). +- The single-tool sidebar preserves state across tab switches; the + active view preserves state across view switches; designers + preserve their in-progress state when navigating to the map and + back through a transient overlay. +- Order draft is preserved across page reloads, view switches, network + drops, and history-mode entry / exit. +- Orders queued offline are flushed in order on reconnect; a turn- + cutoff conflict surfaces as a clearly failed-order banner without + retrying forever. +- History mode applies to every view; the order tab disappears in + history mode and the prior draft is restored on return to the + current turn. +- The ship-class designer's calculations match `pkg/calc/` byte-for- + byte; any drift between client mirror and server fails CI. +- Linux desktop builds without Secret Service still complete login by + falling back to the `0600` file under `~/.config/galaxy/`. +- The web service worker invalidates correctly on app update and + never serves stale code on the first load after a deploy. +- Push-event signature verification is mandatory; any verification + failure tears down the stream and reconnects with backoff. +- Locale switch persists across reloads and applies to every visible + string on every platform.