Commit Graph

96 Commits

Author SHA1 Message Date
Ilia Denisov dc621cc715 fix(game): small reconciliation fixes (science, generation, dismantle, report)
Tests · Go / test (push) Successful in 1m58s
Tests · Integration / integration (pull_request) Successful in 1m47s
Tests · Go / test (pull_request) Successful in 2m1s
A bundle of small rules-vs-engine corrections:

- Science proportions: accept a sum that equals 1 only up to float
  rounding (was an exact != 1 comparison); the rules example is reworded
  so it is unambiguous that proportions are fractions summing to 1.
- Generation: super-big planets get a resource strictly above 0 (minimum
  0.001, was a hard 0.1); the rules table is fixed for big planets (1-10,
  not 0.1-10) and the false "0.1-20 / average 1.5" resource claim removed.
- Dismantle over a neutral planet now unloads the colonists and settles
  it (the planet becomes the race's); over a foreign planet they are
  still lost. The rules clause is clarified for own / neutral / foreign.
- Report: ship-production entries are written at the compacted report
  index (was the planet's map index, which could write past the grown
  slice and panic); the incoming-group "remaining distance" is measured
  from the group's current hyperspace position, not its origin planet
  (matching OtherGroup).
- validator: the cargo-value error now carries the cargo value, not the
  shields value.

Tests added for each behavioural fix; rules.txt updated in the same patch.
2026-05-31 09:29:07 +02:00
Ilia Denisov 601970b028 refactor(game): lock-free storage, remove /command, flatten engine wrapper
Tests · Go / test (push) Successful in 2m27s
Tests · UI / test (push) Waiting to run
Tests · Integration / integration (pull_request) Successful in 1m45s
Tests · Go / test (pull_request) Successful in 3m13s
Tests · UI / test (pull_request) Successful in 3m8s
Three-stage refactor of the game-engine plumbing (game logic untouched):

Stage 1 — lock-free persistence + admin serialisation. Remove the file
lock from repo/fs (the .lock file, the Read/Write-vs-*Safe duality and the
dead ReadSafe polling) and replace the two-step rename with a single atomic
rename so concurrent reads are torn-free without a lock. Serialise the
state-mutating admin writers (init/turn/banish) with one shared router
LimitMiddleware, rewritten to block on the request context instead of a
racy shared 100ms timer.

Stage 2 — remove the obsolete immediate-command path end to end. Players
submit through PUT /api/v1/order; the legacy PUT /api/v1/command path is
deleted across game (route, handler, 24 command factories, Ctrl), backend
(Commands handler/route, engineclient.ExecuteCommands), gateway (dispatch +
executeUserGamesCommand + routing entry), the FlatBuffers/model contract
(UserGamesCommand[Response]) and transcoder, plus every affected
OpenAPI/README/FUNCTIONAL/ARCHITECTURE doc. The integration proxy test is
converted to the order path.

Stage 3 — flatten the REST->engine wrapper. Replace the executor adapter,
the controller package functions and RepoController with one concrete
controller.Service; drop the single-implementation Repo and Storage
interfaces (repo.Repo / fs.FS are now concrete). Handlers depend on a thin
handler.Engine seam and own the domain->REST projection; storage is
resolved once at startup instead of per request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:37:07 +02:00
Ilia Denisov 15d35f6f1f feat(game): canonical gameId in POST /api/v1/admin/init
Tests · Go / test (push) Successful in 1m57s
Tests · Integration / integration (pull_request) Successful in 1m48s
Tests · Go / test (pull_request) Successful in 2m0s
Engine no longer mints its own game UUID. The orchestrator (backend)
generates the game UUID at game-create time and passes it in the
admin/init request body as the required `gameId` field, so the value
that names the engine container and host bind-mount directory also
ends up inside the engine's state.json.

The engine rejects the zero UUID with 400 and any init that conflicts
with an existing state.json with 409 (a second init on the same gameId
is also a conflict; full idempotency is not part of the contract).

Updates rest.InitRequest, openapi.yaml (schema + 409 response),
controller.GenerateGame/NewGame/buildGameOnMap signatures, the engine
HTTP handler/executor, the backend runtime worker, and the relevant
unit and contract tests. Documentation in game/README.md,
docs/ARCHITECTURE.md, backend/README.md, and backend/docs/{runtime,flows}.md
is updated in the same patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:13:31 +02:00
Ilia Denisov 723885e74e fix(order): surface rejection reason, keep sync green, hydrate verdicts
Tests · UI / test (push) Has been cancelled
Tests · Go / test (push) Successful in 2m3s
Tests · Go / test (pull_request) Successful in 2m5s
Tests · Integration / integration (pull_request) Successful in 1m44s
Tests · UI / test (pull_request) Failing after 4m28s
Three issues surfaced once the per-command rejection from the previous
commit actually reached the UI:

1. Sync banner falsely red. `OrderDraftStore.runSync` flipped
   `syncStatus = "error"` whenever any command was rejected and
   advertised a Retry button. A per-command rejection is a
   player-correctable state — the round trip succeeded, the engine
   just refused that command — so the retry can't help. Keep
   `syncStatus = "synced"` on `success`; the red row highlight is
   the visible cue.

2. Rejection reason missing. Add `cmd_error_message: string` to
   `CommandItem` in `pkg/schema/fbs/order.fbs` (appended last to
   preserve existing slot offsets) and regenerate the Go + TS stubs
   for that one type. Plumb the message through `CommandMeta`,
   `Controller.applyCommand`'s `m.Result(code, message)` call, the
   Go transcoder, the UI decoders in `submit.ts` /  `order-load.ts`,
   and the `OrderDraftStore.errorMessages` map. `order-tab.svelte`
   renders it as an italic danger-coloured line under rejected
   commands, with new CSS for `.error-reason`.

3. Verdict lost on navigation. `order-load.ts.decodeCommand` never
   read `cmdApplied`/`cmdErrorCode`, so `hydrateFromServer` fell
   back to a blanket "applied" status — a previously-rejected
   command came back green after a lobby → game round trip. Extend
   the fetch decoder to populate `statuses`/`errorCodes`/
   `errorMessages` maps and have `hydrateFromServer` use them.
   Engine-side persistence already records the verdict on disk —
   verified against the live `0000/order/<id>.json`.

`flatbuffers@25` elides default-int8/int64 fields on write; the Go
transcoder force-slots `cmd_applied=false` / `cmd_error_code=0`
already, the new test fixtures flip `builder.forceDefaults(true)` to
mirror that behaviour so the round trip survives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:42:27 +02:00
Ilia Denisov af30846091 fix(game): #59 — per-command rejection on PUT /api/v1/order
Tests · Go / test (push) Successful in 2m2s
Tests · Go / test (pull_request) Successful in 3m3s
Tests · Integration / integration (pull_request) Successful in 1m40s
Validation of a player's order now applies every command against a
transient game-state snapshot and records the per-command outcome
(cmdApplied, cmdErrorCode) in each command's meta. The order is
persisted even when some commands are rejected, and the response is
202 + UserGamesOrder so clients can surface the partial failure
without the chain collapsing into "downstream service is unavailable".

Pkg/error consts are reshelved onto three explicit ranges with a
package doc and helpers (IsInternalCode/IsInputCode/IsGameStateCode):
1xxx internal/server (500/501), 2xxx structural input (400), 3xxx
game-state per-command rejection (400 when escaping HTTP, otherwise
recorded as cmdErrorCode). Two pre-existing typos fixed mechanically
(ErrBeakGroupNumberNotEnough -> ErrBreakGroupNumberNotEnough,
ErrRaceExinct -> ErrRaceExtinct) along with all callsites.

Engine errorResponse maps *GenericError by shelf rather than mapping
everything to 500. The Quit-not-last structural check in
Controller.ValidateOrder is preserved and its type assertion fixed
(was a value assertion against a pointer-typed command, so the check
silently never fired).

Backend, gateway and UI are unchanged — they were already correct on
the 202 path; only the engine collapsing per-command rejection into
500 was needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 09:36:29 +02:00
Ilia Denisov 24c68e9846 feat(model+ui): F8-05 — race on OtherGroup, real attribution + N×M label
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m6s
Tests · Go / test (push) Successful in 2m6s
Tests · Integration / integration (pull_request) Successful in 1m51s
Tests · UI / test (pull_request) Successful in 3m53s
Issue #48 п.32 ("Stationed ship groups") shipped with a fragile race
fallback: when a foreign group sat on a non-`other`-kind planet the
inspector printed a generic "foreign" label, which collapsed the
race dropdown to a single uninformative bucket. The engine FBS
contract did not carry per-group race either, so live games hit the
same gap. This patch carries race authoritatively from the engine
through every layer down to the inspector.

Wire format & engine
- `pkg/schema/fbs/report.fbs`: add `race:string` to `OtherGroup` and
  `LocalGroup` (additive — old clients ignore).
- `pkg/schema/fbs/report/`: regenerated Go bindings.
- `ui/frontend/src/proto/galaxy/fbs/report/`: regenerated TS bindings.
- `pkg/model/report.OtherGroup.Race`: new field; carried through
  `LocalGroup` via the embedded `OtherGroup`.
- `pkg/transcoder/report.go`: encode + decode `race` on both
  `LocalGroup` and `OtherGroup`.
- `game/internal/controller/report.go.otherGroup`: set `v.Race`
  from `c.g.Race[c.RaceIndex(sg.OwnerID)].Name` so every emitted
  group — own or foreign — carries the resolved race name.

Legacy parser
- `tools/local-dev/legacy-report/parser.go`: capture the
  `<Race> Groups` header into `pendingOtherGroup.race`, fill local
  group `Race` from `p.rep.Race`, propagate both into the
  `report.OtherGroup` rows.
- Tests + smoke counts updated; regenerated `KNNTS{039,041}.json`
  fixtures so the synthetic loader carries the new field.

UI
- `ui/frontend/src/api/`: `ReportShipGroupBase.race` field;
  synthetic loader + FBS decoder populate it.
- `ui/frontend/src/lib/inspectors/planet/ship-groups.svelte`: the
  stationed-groups inspector picks race directly from
  `group.race` (own falls back to `localRace`, both finally to the
  `race.unknown` placeholder). The planet-owner / "foreign"
  heuristic is gone.
- Row label changes from "N ships mass M" to a compact
  `<class>` | `<N ×>` | `<mass>` three-column layout: the count
  cell is right-aligned tabular, the mass cell is right-aligned
  monospace + tabular, matching the inspector / calculator number
  conventions. Stale i18n keys removed
  (`ship_groups.row.count`, `.row.mass`, `.race.foreign`).
- All affected unit tests (8 files) carry the new `race` field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:23:17 +02:00
Ilia Denisov e9b904332e fix(ui): calculator polish — smart input steps, unified tech/MAT lock idiom, tech floor, speed-lock ceiling fix
Tests · Go / test (push) Successful in 2m31s
Tests · UI / test (push) Waiting to run
Tests · Integration / integration (pull_request) Successful in 1m41s
Tests · Go / test (pull_request) Successful in 3m14s
Tests · UI / test (pull_request) Successful in 2m32s
- pkg/calc: DriveForSpeed treats restMass==0 as a valid ceiling-only
  case (every positive drive solves it), so locking the displayed
  speed of a D=1, W=A=S=C=0 ship is no longer a phantom "infeasible".
- ship-design-area: drive/weapons/shields/cargo inputs use a JS-driven
  smart step on ArrowUp/ArrowDown (0↔1 jump, otherwise ±0.1) and hide
  the native spinner so it cannot produce invalid (0, 1) values;
  armament keeps its native step 1.
- Tech and planet MAT cells follow the same lock idiom as goal-seek
  locks: open padlock (🔓) over the inherited value → click to open
  an input with a closed padlock (🔒). The padlock slot is always
  reserved, so the column width is stable.
- Tech overrides (design area and modernization target) are floored
  at the player's current tech on this turn — a lower value is
  flagged as invalid.
2026-05-26 14:30:43 +02:00
Ilia Denisov 8e0a1c39c0 chore: remove deprecated client
Tests · UI / test (pull_request) Has been cancelled
Tests · Go / test (push) Successful in 2m25s
Tests · UI / test (push) Waiting to run
Tests · Integration / integration (pull_request) Successful in 1m44s
Tests · Go / test (pull_request) Successful in 2m2s
2026-05-23 16:55:02 +02:00
Ilia Denisov b1b87c8521 feat(ui-calculator): input validation, load caps, ceil display, modernization layout
Tests · Go / test (push) Successful in 2m26s
Tests · UI / test (push) Successful in 2m26s
- custom load capped at cargo capacity (error when exceeded); full load shows the cargo capacity; zero cargo pins load to empty and disables the toggle

- per-input red border + tooltip for every invalid value (blocks, techs, load, MAT, modernization target); no value may be negative; locking a speed is disabled when drive is zero

- display every computed number (results + goal-seek back-solved input) rounded up to 3 decimals via a shared pkg/calc Ceil3 bridged to wasm; engine keeps its own round-to-nearest util.Fixed*

- modernization total upgrade cost spans two columns (single line)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 21:24:40 +02:00
Ilia Denisov 9ae7b88b89 feat(ui): Phase 30 ship-class calculator with goal-seek and reach circles
Tests · UI / test (push) Successful in 2m14s
Tests · Go / test (push) Successful in 2m25s
Fuse the standalone ship-class designer (Phases 17/18) into a sidebar calculator: live mass/speed/attack/defence/bombing results, a planet build-rate readout, single-target goal-seek, a modernization-cost mode, and auto reach circles on the map for the selected planet.

pkg/calc becomes the single source for the new math (no mirroring): extract BombingPower from the engine model and the per-turn ship-production loop from controller.ProduceShip into pkg/calc (engine now delegates), and add inverse goal-seek solvers in pkg/calc/solve.go. Thin-bridge the combat, planet-build, and solver functions through ui/core/calc + ui/wasm and rebuild core.wasm.

Remove the standalone designer view/route; the ship-classes table and the view/bottom menus open the calculator via a shared request store.

Docs: rewrite ui/PLAN.md Phase 30, adjust Phase 34 (realistic forecast + CAP/COL ownership), add ui/docs/calculator-ux.md, extend calc-bridge.md, fix navigation.md; remove ui/CALCULATOR.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:04:07 +02:00
Ilia Denisov 2bd1b54936 feat(ui): Phase 29 map visibility toggles
Tests · Go / test (push) Successful in 2m31s
Tests · UI / test (push) Failing after 8m7s
Adds the gear-icon popover on the map view with per-game persistence
of every category toggle plus the wrap-mode radio. Hide-by-id and
visibility-fog facilities land on the renderer so every flip applies
within one frame without a Pixi remount; the wrap-mode toggle keeps
its existing remount + camera-preserve path. A new server-side turn
force-resets every flag to defaults so a hidden category never makes
the player miss the next turn's news.

Also fixes the FligthDistance → FlightDistance typo in pkg/calc/race.go
(plus the single Go caller); the TS side keeps duplicating the formula
until a race-level WASM bridge lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:33:53 +02:00
Ilia Denisov ebd156ece2 battle-fetch: migrate to user.games.battle ConnectRPC command
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m6s
Tests · Go / test (push) Successful in 2m7s
Tests · Integration / integration (pull_request) Successful in 1m47s
Tests · UI / test (pull_request) Failing after 3m42s
The Phase 27 BattleViewer was the last UI surface still issuing raw
fetch() against the backend REST contract (`/api/v1/user/games/...
/battles/...`). The dev-deploy gateway never proxied that path, so
the viewer worked only in tools/local-dev/. Move it onto the signed
ConnectRPC channel every other authenticated surface already uses.

Wire pieces:
- FBS GameBattleRequest in pkg/schema/fbs/battle.fbs, regenerated
  Go + TS bindings.
- MessageTypeUserGamesBattle constant + GameBattleRequest struct in
  pkg/model/report/messages.go.
- pkg/transcoder/battle.go gains GameBattleRequestToPayload and
  PayloadToGameBattleRequest helpers.
- gateway games_commands.go switches on the new message type and
  GETs /api/v1/user/games/{id}/battles/{turn}/{battle_id}; the JSON
  response is re-encoded as a FlatBuffers BattleReport before being
  returned. 404 from backend surfaces as the canonical `not_found`
  gateway error.
- ui/frontend/src/api/battle-fetch.ts now builds the FBS request,
  calls GalaxyClient.executeCommand, and decodes the FBS response
  into the existing UI shape (Record<string,string> race/ship maps,
  string-form UUID). BattleFetchError carries an HTTP-style status
  derived from the result code so the active-view's not_found branch
  keeps working.
- battle.svelte pulls the GalaxyClient from the in-game shell
  context. While the layout's boot Promise.all is in flight the
  effect stays in `loading` until the client handle becomes
  non-null.
- ui/Makefile FBS_INPUTS gains battle.fbs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:41:54 +02:00
Ilia Denisov fed282f2d2 Phase 28 (Step 2): FBS schemas + message-type constants for mail
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m4s
Tests · Go / test (push) Successful in 2m5s
Tests · Integration / integration (pull_request) Successful in 1m54s
Tests · UI / test (pull_request) Successful in 2m50s
Adds the wire schema for the eight `user.games.mail.*` ConnectRPC
commands together with the shared payload types (`MailMessage`,
`MailRecipientState`, `MailBroadcastReceipt`). Send-request tables
carry the optional `recipient_race_name` introduced in Step 1.

Drops:

- `pkg/schema/fbs/diplomail.fbs` — schema sources;
- `pkg/schema/fbs/diplomail/*.go` — generated Go bindings (flatc
  `--go --go-module-name galaxy/schema/fbs`);
- `pkg/model/diplomail/diplomail.go` — message-type catalog used by
  the gateway router;
- `ui/frontend/src/proto/galaxy/fbs/diplomail/*.ts` — generated TS
  bindings consumed by the upcoming UI client wrapper;
- `ui/Makefile` `FBS_INPUTS` extended to pick the new schema up on
  the next `make -C ui fbs-ts` run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:21:23 +02:00
Ilia Denisov 7bce67462c pkg/util: harden TestRandomSuffixGenerator against birthday collisions
The previous test asserted that no two adjacent samples from a
~10 000-element space were equal across 100 iterations. The birthday
math gives that adjacency check a ~1 % flake rate per run; with the
new gitea.lan CI volume that turned into observable random failures
(go-unit #51 on feature/enable-actions-cache hit "Should not be:
'6635'").

Replace adjacency with a distinctness floor over a wider 200-sample
draw. A stuck generator (single value) lands at 1 unique; a
256-element range lands at ~196; the natural full-range generator
lands at ~198. A floor of 150 catches the failure modes the test was
actually written to guard against and never trips on legitimate
randomness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:58:02 +02:00
Ilia Denisov 46996ebf31 docs: clarify BattleSummary.shots scaling in FBS schema
Doc-only nit; triggers a CI rerun on the workflow's path filter to
verify the new Monitor permission lets local-CI polling run without
prompts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:03:10 +02:00
Ilia Denisov 969c0480ba ui/phase-27: battle viewer (radial scene, playback, map markers)
Engine wire change: Report.battle switched from []uuid.UUID to
[]BattleSummary{id, planet, shots} so the map can place battle
markers without N extra fetches. FBS schema + generated Go/TS
regenerated; transcoder + report controller updated; openapi
adds the BattleSummary schema with a freeze test.

Backend gateway forwards engine GET /api/v1/battle/:turn/:uuid as
/api/v1/user/games/{game_id}/battles/{turn}/{battle_id} (handler
plus engineclient.FetchBattle, contract test stub, openapi spec).

UI:
- BattleViewer (lib/battle-player/) is a logically isolated SVG
  radial scene that consumes a BattleReport prop. Planet at the
  centre, races on the outer ring at equal angular spacing, race
  clusters by (race, className) with <class>:<numLeft> labels;
  observer groups (inBattle: false) are not drawn; eliminated
  races drop out and survivors re-distribute on the next frame.
- Shot line per frame: red on destroyed, green otherwise; erased
  on the next frame. Playback controls: play/pause + step ± +
  rewind + 1x/2x/4x speed (400/200/100 ms per frame).
- Page wrapper (lib/active-view/battle.svelte) loads BattleReport
  via api/battle-fetch.ts; synthetic-gameId prefix routes to a
  fixture loader, otherwise REST through the gateway. Always-
  visible <ol> text protocol satisfies the accessibility ask.
- section-battles.svelte links every battle UUID into the viewer.
- map/battle-markers.ts: yellow X cross of 2 LinePrim through the
  corners of the planet's circumscribed square (stroke width
  clamps from 1 px at 1 shot to 5 px at 100+ shots); bombing
  marker is a stroke-only ring (yellow when damaged, red when
  wiped). Wired into state-binding.ts; click handler dispatches
  battle clicks to the viewer and bombing clicks to the matching
  Reports row.
- i18n keys for the viewer in en + ru.

Docs: ui/docs/battle-viewer-ux.md, FUNCTIONAL.md §6.5 + ru
mirror, ui/PLAN.md Phase 27 decisions + deferred TODOs (push
event, richer class visuals, animated re-distribution).

Tests: Vitest unit (radial layout + timeline frame builder +
marker stroke formula + marker primitives), Playwright e2e for
the viewer (Reports link → viewer, playback step, not-found),
backend engineclient FetchBattle (200 / 404 / bad input), engine
openapi freezes (BattleReport, BattleReportGroup,
BattleActionReport, BattleSummary, Report.battle items).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:24:20 +02:00
Ilia Denisov a9adbad7ef feat: game engine fetch battle api
ui-test / test (push) Failing after 47s
2026-05-13 10:50:45 +02:00
Ilia Denisov c58027c034 ui/phase-23: turn-report view with twenty sections and TOC
Replaces the Phase 10 report stub with a scrollable orchestrator that
renders every FBS array as a dedicated section (galaxy summary, votes,
player status, my/foreign sciences, my/foreign ship classes, battles,
bombings, approaching groups, my/foreign/uninhabited/unknown planets,
ships in production, cargo routes, my fleets, my/foreign/unidentified
ship groups). A sticky table of contents (a <select> on mobile),
"back to map" affordance, IntersectionObserver-driven active-section
highlight, and SvelteKit Snapshot-based scroll save/restore round out
the view.

GameReport gains six new fields (players, otherScience, otherShipClass,
battleIds, bombings, shipProductions); decodeReport, the synthetic-
report loader, the e2e fixture builder, and EMPTY_SHIP_GROUPS extend
in lockstep. ~90 new i18n keys land in en + ru together.

The legacy-report parser is extended to populate the new sections from
the dg/gplus text formats (Your Sciences, <Race> Sciences, <Race> Ship
Types, Bombings, Ships In Production). Ships-in-production prod_used
is derived through a new pkg/calc.ShipBuildCost helper; the engine's
controller.ProduceShip refactors to call the same helper without any
behaviour change (engine tests stay unchanged and green). Battles
remain in the parser's Skipped list — the legacy text carries no
stable per-battle UUID.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:33:56 +02:00
Ilia Denisov 3626998a33 ui/phase-20: ship-group inspector actions
Eight ship-group operations land on the inspector behind a single
inline-form panel: split, send, load, unload, modernize, dismantle,
transfer, join fleet. Each action either appends a typed command to
the local order draft or surfaces a tooltip explaining the
disabled state. Partial-ship operations emit an implicit
breakShipGroup command before the targeted action so the engine
sees a clean (Break, Action) pair on the wire.

`pkg/calc.BlockUpgradeCost` migrates from
`game/internal/controller/ship_group_upgrade.go` so the calc
bridge can wrap a pure pkg/calc formula; the controller now
imports it. The bridge surfaces the function as
`core.blockUpgradeCost`, which the inspector calls once per ship
block to render the modernize cost preview.

`GameReport.otherRaces` is decoded from the report's player block
(non-extinct, ≠ self) and feeds the transfer-to-race picker. The
planet inspector's stationed-ship rows become clickable for own
groups so the actions panel is reachable from the standard click
flow (the renderer continues to hide on-planet groups).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 16:27:55 +02:00
Ilia Denisov d63fe44618 pkg/calc: fix Deltas wrap on rectangular maps + add signed ShortestDelta
The pre-existing `Deltas` helper used the height to wrap the x-axis,
which silently produced wrong values on any rectangular galaxy
(`w != h`). Square galaxies — the only configuration the engine
ships today — masked the bug, so it stayed in tree.

`Deltas` is now a thin wrapper around the new `ShortestDelta(a, b,
size)`, which returns the signed per-axis shortest delta on a 1-D
circle (range `(-size/2, size/2]`). The signed flavour is what the
Phase 19 ship-group renderer needs to draw an IncomingGroup
trajectory across the torus seam; `Deltas` continues to return the
pair of absolute deltas for distance computation.

Adds `pkg/calc/map_test.go` with table-driven coverage for both
helpers, including a regression that exercises the rectangular
case the bug was hiding behind, and the half-circumference
tie-break.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:08:16 +02:00
Ilia Denisov 408097e3aa feat: move func to calc package 2026-05-10 14:55:14 +02:00
Ilia Denisov 8a236bef14 ui/phase-16: pick any planet in reach + stronger pick-mode dim
The cargo-route picker filtered out unidentified planets, so an
early-game player who had spotted but not surveyed a destination
could not configure a route to it — the engine has no such
restriction (`game/internal/controller/route.go.PlanetRouteSet`
only checks ownership of the origin and `util.ShortDistance(...) <=
FligthDistance`). Drop the unidentified guard and document the
contract in `cargo-routes-ux.md` plus a comment over `reachableSet()`.

Pick-mode dim now drops both alpha and tint on out-of-reach
planets so bright shapes (`STYLE_LOCAL` is `0x6dd2ff`) collapse
into a single muted gray. The single-channel `dimAlpha=0.3` was too
gentle against the dark theme — the user reported the dim wasn't
visible. Tighten to `dimAlpha=0.35 + dimTint=0x303841`; restore
both on tear-down.

Also threads through the user's `pkg/calc/race.go.FligthDistance`
addition: `calc-bridge.md` records the new Go-side reference (the
engine's `Race.FlightDistance()` already wraps it), and the picker
comment points at the canonical formula location.

Tests:
- `inspector-planet-cargo-routes.test.ts` adds two cases — a
  reach-spans-every-kind case (own + foreign + uninhabited +
  unidentified all picked when in range) and a successful pick to
  an unidentified destination.
- All 356 vitest cases + chromium-desktop / webkit-desktop e2e
  cargo-routes pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 20:48:42 +02:00
Ilia Denisov 229c43beb5 ui/phase-14: auto-sync order draft + always GET on boot + header headline
Replaces the manual Submit button with an auto-sync pipeline driven
by `OrderDraftStore`: every successful add / remove / move
coalesces a `submitOrder` call so the engine always mirrors the
local draft. Removing the last command sends an empty cmd[] PUT —
the engine, repo, and rest model now accept that as a valid
"player cleared their draft" state.

`hydrateFromServer` is now invoked unconditionally on game boot so
a fresh device picks up the player's stored order, and the local
cache is overwritten by the server's view (server is the source of
truth).

Header replaces the static "race ?" + turn counter with a single
headline string `<race> @ <game>, turn <n>`, sourced from the
engine's Report.race + the lobby's GameSummary.gameName + the live
turn number, with a `?` fallback while any piece is loading.

Tests:
- engine: empty PUT round-trips, repo round-trips empty Commands
- order-draft: auto-sync sends full draft on every mutation,
  rejected response surfaces error sync status, rapid mutations
  coalesce, server hydration overwrites cache
- order-tab: per-row status flips through the auto-sync lifecycle,
  remove → empty cmd[] PUT, rejected → retry button
- inspector overlay: applied + valid + submitting all participate
  in the optimistic projection
- header: live race / game / turn rendering with fall-back

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:34:10 +02:00
Ilia Denisov f80c623a74 ui/phase-14: rename planet end-to-end + order read-back
Wires the first end-to-end command through the full pipeline:
inspector rename action → local order draft → user.games.order
submit → optimistic overlay on map / inspector → server hydration
on cache miss via the new user.games.order.get message type.

Backend: GET /api/v1/user/games/{id}/orders forwards to engine
GET /api/v1/order. Gateway parses the engine PUT response into the
extended UserGamesOrderResponse FBS envelope and adds
executeUserGamesOrderGet for the read-back path. Frontend ports
ValidateTypeName to TS, lands the inline rename editor + Submit
button, and exposes a renderedReport context so consumers see the
overlay-applied snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 11:50:09 +02:00
Ilia Denisov 2a1e80053a feat: game order api methods 2026-05-09 10:36:44 +02:00
Ilia Denisov ce7a66b3e6 ui/phase-11: map wired to live game state
Replaces the Phase 10 map stub with live planet rendering driven by
`user.games.report`, and wires the header turn counter to the same
data. Phase 11's frontend sits on a per-game `GameStateStore` that
lives in `lib/game-state.svelte.ts`: the in-game shell layout
instantiates one per game, exposes it through Svelte context, and
disposes it on remount. The store discovers the game's current turn
through `lobby.my.games.list`, fetches the matching report, and
exposes a TS-friendly snapshot to the header turn counter, the map
view, and the inspector / order / calculator tabs that later phases
will plug onto the same instance.

The pipeline forced one cross-stage decision: the user surface needs
the current turn number to know which report to fetch, but
`GameSummary` did not expose it. Phase 11 extends the lobby
catalogue (FB schema, transcoder, Go model, backend
gameSummaryWire, gateway decoders, openapi, TS bindings,
api/lobby.ts) with `current_turn:int32`. The data was already
tracked in backend's `RuntimeSnapshot.CurrentTurn`; surfacing it is
a wire change only. Two alternatives were rejected: a brand-new
`user.games.state` message (full wire-flow for one field) and
hard-coding `turn=0` (works for the dev sandbox, which never
advances past zero, but renders the initial state for any real
game). The change crosses Phase 8's already-shipped catalogue per
the project's "decisions baked back into the live plan" rule —
existing tests and fixtures are updated in the same patch.

The state binding lives in `map/state-binding.ts::reportToWorld`:
one Point primitive per planet across all four kinds (local /
other / uninhabited / unidentified) with distinct fill colours,
fill alphas, and point radii so the user can tell them apart at a
glance. The planet engine number is reused as the primitive id so
a hit-test result resolves directly to a planet without an extra
lookup table. Zero-planet reports yield a well-formed empty world;
malformed dimensions fall back to 1×1 so a bad report cannot crash
the renderer.

The map view's mount effect creates the renderer once and skips
re-mount on no-op refreshes (same turn, same wrap mode); a turn
change or wrap-mode flip disposes and recreates it. The renderer's
external API does not yet expose `setWorld`; Phase 24 / 34 will
extract it once high-frequency updates land. The store installs a
`visibilitychange` listener that calls `refresh()` when the tab
regains focus.

Wrap-mode preference uses `Cache` namespace `game-prefs`, key
`<gameId>/wrap-mode`, default `torus`. Phase 11 reads through
`store.wrapMode`; Phase 29 wires the toggle UI on top of
`setWrapMode`.

Tests: Vitest unit coverage for `reportToWorld` (every kind,
ids, styling, empty / zero-dimension edges, priority order) and
for the store lifecycle (init success, missing-membership error,
forbidden-result error, `setTurn`, wrap-mode persistence across
instances, `failBootstrap`). Playwright e2e mocks the gateway for
`lobby.my.games.list` and `user.games.report` and asserts the
live data path: turn counter shows the reported turn,
`active-view-map` flips to `data-status="ready"`, and
`data-planet-count` matches the fixture count. The zero-planet
regression and the missing-membership error path are covered.

Phase 11 status stays `pending` in `ui/PLAN.md` until the local-ci
run lands green; flipping to `done` follows in the next commit per
the per-stage CI gate in `CLAUDE.md`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 21:17:17 +02:00
Ilia Denisov f57a290432 phase 8: lobby UI + cross-stack lobby command catalog + TS FlatBuffers
- Extend pkg/model/lobby and pkg/schema/fbs/lobby.fbs with public-games
  list, my-applications/invites lists, game-create, application-submit,
  invite-redeem/decline. Mirror the matching transcoder pairs and Go
  fixture round-trip tests.
- Wire the seven new lobby message types through
  gateway/internal/backendclient/{routes,lobby_commands}.go with
  per-command REST helpers, JSON-tolerant decoding of backend wire
  shapes, and httptest-based unit coverage for success / 4xx / 5xx /
  503 across each command.
- Introduce TS-side FlatBuffers via the `flatbuffers` runtime dep, a
  `make fbs-ts` target driving flatc, and the generated bindings under
  ui/frontend/src/proto/galaxy/fbs. Phase 7's `user.account.get` decode
  now uses these bindings as well, closing the JSON.parse vs
  FlatBuffers gap that would have failed against a real local stack.
- Replace the placeholder lobby with five sections (my games, pending
  invitations, my applications, public games, create new game) and the
  /lobby/create form. Submit-application uses an inline race-name
  form on the public-game card; create-game keeps name / description /
  turn_schedule / enrollment_ends_at always visible and the rest under
  an Advanced toggle with TS-side defaults.
- Update lobby/+page.svelte to throw LobbyError on non-ok result codes;
  GalaxyClient.executeCommand now returns { resultCode, payloadBytes }.
- Vitest binding round-trips, lobby.ts wrapper unit tests, lobby-page
  + lobby-create component tests, Playwright lobby-flow.spec covering
  create / submit / accept across all four projects. Phase 7 e2e was
  migrated to the FlatBuffers fixtures and to click+fill against the
  Safari-autofill readonly inputs.
- Mark Phase 8 done in ui/PLAN.md, mirror the wire-format note into
  Phase 7, append the new lobby commands to gateway/README.md and
  docs/ARCHITECTURE.md, add ui/docs/lobby.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:05:08 +02:00
Ilia Denisov 1b5749bd31 fix: make ci green on a fresh runner
Two issues surfaced by the first end-to-end ui-test.yaml run on a
clean Linux runner that don't reproduce locally:

- pkg/geoip tests load fixtures from the pkg/geoip/test-data git
  submodule (MaxMind-DB). actions/checkout@v4 does not fetch
  submodules by default, so the fixture path is missing on the
  runner. Both ui-test and ui-release workflows now check out with
  submodules: recursive.

- pkg/util/TestWritable asserts that /usr/lib is not writable, which
  holds for unprivileged users but fails inside the catthehacker
  workflow container that runs as root. Skip that branch when
  os.Geteuid() == 0; the root-only "the writable dir is writable"
  branch still runs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 08:35:34 +02:00
Ilia Denisov 604fe40bcf docs: reorder & testing 2026-05-07 00:58:53 +03:00
Ilia Denisov f446c6a2ac feat: backend service 2026-05-06 10:14:55 +03:00
Ilia Denisov 3e2622757e feat: gamemaster 2026-05-03 07:59:03 +02:00
Ilia Denisov a7cee15115 feat: runtime manager 2026-04-28 20:39:18 +02:00
Ilia Denisov b4591cabd4 game stats shows planes and population 2026-04-26 21:12:51 +02:00
Ilia Denisov fe829285a6 feat: use postgres 2026-04-26 20:34:39 +02:00
Ilia Denisov 48b0056b49 feat: game lobby service 2026-04-25 23:20:55 +02:00
Ilia Denisov 32dc29359a feat: notification service 2026-04-22 08:49:45 +02:00
Ilia Denisov 23ffcb7535 feat: user service 2026-04-10 19:05:02 +02:00
IliaDenisov e04fc663f0 docs: geoip data sources 2026-04-09 14:19:40 +02:00
Ilia Denisov 84eeaf5184 feat: geoip 2026-04-09 15:16:36 +03:00
Ilia Denisov 86a68ed9d0 feat: authsession service 2026-04-08 16:23:07 +02:00
Ilia Denisov 436c97a38b feat: edge gateway service 2026-04-02 19:18:42 +02:00
Ilia Denisov f616e3f5ca flatbuffers & transcoders 2026-03-31 19:16:34 +02:00
Ilia Denisov 6e01d73a5e fbs: order 2026-03-30 21:51:47 +02:00
Ilia Denisov 89af8194db flatc readme 2026-03-30 21:24:03 +02:00
Ilia Denisov 92889b81ab cleanup 2026-03-30 19:41:04 +02:00
Ilia Denisov a7793f5416 ui calculator 2026-03-30 19:38:24 +02:00
Ilia Denisov 960fd3a218 build: work sync 2026-03-22 19:44:44 +02:00
Ilia Denisov d6a707d60e flatbuffers 2026-03-22 19:43:45 +02:00
Ilia Denisov 73a4b0d3ec circle radius 2026-03-22 19:43:09 +02:00
Ilia Denisov 3f1776aa5f loader revisited 2026-03-16 19:52:02 +02:00