Skip to content

EchoSpire: Portals — Game Architecture

Status: Point-in-time assessment, 2026-08-28. Not canon and not a plan. Provenance: Grok 4.6. Reassessment requested via grok-eval-instructions.md. Scope: how the system is built, what is actually playable, WPF as shipping client, simulation harness. Unity is treated as a later presentation upgrade, not a launch path. Verification: Walked src/ projects, EchoSpire.Core combat/effects/handlers, CombatHandlerFactory, GameRunner, QuickPlayRunService, all WPF Views/ViewModels, Simulation/BatchRunner + HeuristicPolicy, ContentPipeline.md vs GameDataSeeder, SimulationController. Did not run the WPF client or the test suite. Test count ~693 is taken from the 2026-08-27 Architecture header. docs/tech/DevelopmentStatus.md is marked partially stale and was not trusted for “not started” claims. Known errors: none at publication.

Date: 2026-08-28 Companion to: 2026-08-28-overall-concept.md

The 2026-08-27 architecture evaluation was directionally right (platform ahead of product; Core is the load-bearing decision) and factually behind in two places this pass corrects: WPF now has a real Quick Play combat loop bound to CombatStateMachine, and the todo-inventory lines that say Opening Story / Combat / Map / Sanctuary have no views are stale. The screens exist. What they contain is the remaining problem.


Bottom line

You have built a studio rules engine and a Windows client that can play a three-realm run. That is more game than the March WPF README admits, and less product than a Kickstarter page needs.

The load-bearing decision is still correct: EchoSpire.Core owns rules; clients own presentation; the API owns data and persistence. Dual-target netstandard2.1 + net10.0 is real and is there for Unity later. Do not reopen Unity, PostgreSQL, or “maybe the client should be the engine.”

The architectural risk that actually matters now is two run loops plus three card truths. WPF does not implement IGameUI. Console and Simulation drive GameRunner. WPF drives QuickPlayRunService wrapping the same combat machine. Cards live in cards.json (authoritative), ProductionRiftContentCatalog (startup seed), and WPF fallbacks. A fresh machine can play a thinner game than you think you shipped.

Grade
Core as an engine A-
Content pipeline as designed B
Content pipeline as a fresh-boot path C
WPF as a playable client B- (loop exists; polish and content hydration lag)
WPF as a 2026 store client C+ (Windows-only, API-shaped, must be sold honestly)
Simulation as a balance instrument C (infrastructure yes; policy and identity no)
Architecture as a ship plan C+

What is actually in the solution

Verified under src/:

Project Role
EchoSpire.Core Pure C# rules. Combat, effects, map, economy, RNG, state, telemetry contracts, IGameUI, GameRunner. Dual-target. No ASP.NET, no DB.
EchoSpire.Contracts Shared DTOs. netstandard2.1.
EchoSpire.Infrastructure HTTP GameDataClient, console seeder, telemetry writers.
EchoSpire.API Auth, SQL Server game data, content promote, packages, runs, mini-games, storyteller, simulation queue. Does not execute combat.
EchoSpire.Wpf Shipping client. Direct Core for rules; HTTP for catalogs and save/resume.
EchoSpire.ConsoleGame Original Core proof via GameRunner + IGameUI.
EchoSpire.Simulation Headless batches via GameRunner + AutomatedSimulationUi.
EchoSpire.SvgTools SVG tooling.
admin-react Designer UI.
www-react Public site.

Architecture.md’s solution-layout block is current. Its dependency diagram still lists EchoSpire.Admin and omits WPF. Trust the layout, not the graph.

Core combat is the mature heart: CombatStateMachine + CombatHandlerPipeline + CardResolver + EffectRegistry (assembly scan, 22 IEffect classes — docs that say “15+” are understating). Class mechanics and House passives are composable handlers. Seeded RNG with namespace forks is real. Handler priority bands (core → class → faction → relics → telemetry) are how a good studio avoids rewriting combat for every combo.

NormalizeClassMechanicId in both GameRunner and QuickPlayRunService maps live JSON values (Density, EchoLock, Constructs, VolatileMutation) onto factory IDs (DENSITY, ECHO_LOCK, CONSTRUCT, MUTATION). That mapping works. Do not treat the JSON spelling as a bug.


Two loops, one engine

This is the fact I would want a future reviewer to see first.

Path Used by Loop owner
GameRunner + IGameUI Console, Simulation Core
QuickPlayRunService + ViewModels WPF shipping client WPF service wrapping Core combat/map

They share CombatStateMachine, effects, and RNG types. They do not share shop, sanctuary, artifact routing, or quest-beat flow. Divergence has already started (WPF shop services and artifact source flags vs Console EconomyManager).

IGameUI is still console-shaped (WaitForKey, Divider, GetText). WPF was right not to pretend it is a complete presentation contract. The cost is a second orchestration layer that will have to be kept honest by discipline, not by the type system.

Keep Core pure. Every mechanic implemented “just for now” in a view-model will be paid twice. Combat already goes through the state machine from WPF (CombatMapViewModel.PlaySelectedCardAsyncQuickPlayRunService.TryPlayCard_combatStateMachine.PlayCard). That is the pattern to protect.


What is actually playable in WPF

The March README says the shell exists “without pretending gameplay now lives in WPF.” That README is stale.

Screens that exist (18 XAML views): startup, login, register, main menu, opening story, Quick Play setup (also hero creation), run briefing, star map, combat, rewards/events, sanctuary/recovery (shop lives here), run summary, pause, mini-game, placeholders, two dev harnesses.

Quick Play is a real loop, verified in code if not by sitting at the machine:

  1. Login / auto-login
  2. Opening story (Storyteller beats from API)
  3. Setup: House + class from API
  4. Briefing → StartNewRunAsync
  5. Seeded three-realm DAG (QuestRealmBuilder)
  6. Combat/Elite/Boss/Protection → live CombatStateMachine, drag-and-drop cards, energy, intents, log, floating numbers
  7. Event → reward/event view
  8. Shop / sanctuary / rest / treasure / anchor → sanctuary view with mode switch
  9. Save/resume via API run blob
  10. Death → summary

CombatMapViewModel is ~1,900 lines. StarMapViewModel ~1,200. SanctuaryRecoveryViewModel ~730. This is not a stub combat screen.

Caveats that keep it off “demo-ready”:

  • Content hydration. Cards/enemies come from the API. If the 584-row JSON was never promoted, the client degrades to bootstrap catalog or BuildFallbackCards() / BuildFallbackEnemies(). Kickstarter footage of fallback Strikes would be a self-own.
  • Dev defaults. appsettings.json auto-logs in as admin, can skip opening story, and enables a screen gallery of fixtures. That must not ship.
  • Quest beats. QuestBeatViewModel exists; there is no QuestBeatView.xaml, and MainWindowViewModel.Navigate has no quest-beat case. Scripted tutorial nodes will fail silently.
  • Opening copy is the pre-rebuild slideshow (see the story evaluation). The screen is not the contract.
  • Offline. WPF requires the API for login, catalogs, and persist. Catalog disk cache is a start, not an offline game. Backers will assume they can play on a plane.
  • Settings / dedicated shop-rest XAML / keyboard nav / audio are still gaps. Combat is mouse-first.
  • CardView has a real art pipeline and an “ART PENDING” fallback. Coverage is much better than the July reference screens implied; empty paths still exist (twelve House-identity cards with blank assetFilePath).

Console: DevelopmentStatus.md still says it plays the full five-faction tutorial loop. Code says tutorials are “queued for quest-based implementation” and --seed skips tutorial seeding. Console plays a snapshot/live-data run. Tutorial data and tests still exist; the player path does not auto-launch them.


Content pipeline

Authoritative card list is data/content/cards.json: 584 published, 476 with effect payloads, 80 per class. ProductionRiftContentCatalog.cs is a one-shot bootstrap seed (~49 cards). A previous evaluation read the seed and concluded the game had five cards per class. Do not repeat that.

Intended path: JSON → LocalFileContentSourceContentService.PromoteAsync → SQL sp_*_Save → published snapshot → clients.

Verified boot path: API startup runs migrations + GameDataSeeder.SeedAsync, which still seeds from the bootstrap catalog. Promote is an admin endpoint, not a guaranteed startup step. Todo-inventory’s “startup seed → runtime snapshot reliability” item is still accurate.

Three truths in play:

  1. cards.json — what you mean
  2. Bootstrap seed — what a fresh DB may actually have
  3. WPF fallbacks — what a fight may use if 2 is empty

Until promote-on-dev-boot is boringly reliable, architecture and design are talking about different catalogs.

Snapshot pinning: RunState stores snapshot id/version/hash. WPF resume still reloads current live lists. Mid-run content edits can desync a save. Fine for a solo developer. Not fine for a public demo.


Simulation and the balancing harness

The harness exists. It is not yet a balancer.

EchoSpire.Simulation authenticates to the API, starts Quick Play runs, drives GameRunner with RandomPolicy or HeuristicPolicy, writes telemetry (NDJSON / Kusto / optional ClickHouse). Same Core as Console. Deterministic: same seed + same policy = same outcome. That is the right shape.

SimulationController does implement GET results, analytics, and outliers (win rates, card dominance, enemy lethality). The todo line calling those stubs is stale relative to this code. They only mean anything if batches have been ingested.

What it cannot do yet:

  • RandomPolicy only plays cards it can afford. Overload deficit play never happens, so Conduit identity is invisible in the default policy.
  • HeuristicPolicy scores damage, block, heal, draw, energy, lethal, construct slot. It does not score Density, Echo-Lock stacks, Overload/Burnout, Mutate, Stasis, or Logic Stream. It will play a Machinist as Ironclad with a turret.
  • Empty-effect cards can still be selected if they are cheap. They do nothing. Win-rate data will launder that as “this card is weak” instead of “this card is unimplemented.”
  • Card names collide across starter and later-wave reprints. Analytics by name will lie. Need card id.
  • Simulation tests are essentially JsonFileTelemetryStoreTests. No policy-vs-catalog tests.
  • Innate/ultimate abilities are null on every class; UseAbility() still exists on policies with nothing to fire.

To succeed as a balance instrument, the harness needs identity-aware policies, a guaranteed promoted catalog, per-id stats, and a human-labeled “this card is unimplemented” filter so empty payloads cannot pollute the report. Building more telemetry writers before that is platform-as-progress.


WPF as a 2026 shipping client (honest, not a Unity pitch)

Settled: WPF is the Kickstarter client. Unity is a later skin. Core’s netstandard2.1 target is the option, not the plan.

What that implies, factually:

  • Windows-only (net10.0-windows). Steam Deck, macOS, Linux are not in this client. Say that on the page. Do not let backers infer a 3D multi-platform build from a Unity folder that is not on the path.
  • Store footage will be compared to STS-like 2D. Current combat XAML is a custom HUD (Cinzel, card frames, floating numbers), not WinForms. That helps. It still has to move in a trailer — card play, enemy intent, a map that reads as a run.
  • Kickstarter assumes offline. You do not have it. A local published snapshot playable without login is the architectural item that protects the campaign promise. Not a new engine.
  • Velopack updater, crash log, fonts, custom CardView — you are treating this as a product. Default admin auto-login is the opposite. Ship config ≠ dev config.

I would not start a Unity combat client before a stranger has finished a WPF run and wanted a second. The engine will still be there.


What is overbuilt relative to the slice

These are real systems. They are also not what sells a first hour:

  • Dual admin history (Blazor gone, React present) plus a public site
  • Package model (builders exist; combat still uses gamedata endpoints)
  • Mini-game API (five handlers, 948 lines) beside an unfinished quest-beat router
  • ClickHouse leftover in Simulation binaries while canon telemetry is Kusto
  • Innate/ultimate counters in handlers for abilities that are null
  • Hero progression design (levels 2–10) with Specialization essentially unbuilt
  • Effect-module manifests, OTLP admin spans, CORS still AllowAnyOrigin()

Freeze infrastructure. The next architectural work that pays is boring: promote-on-boot, pin snapshots, one Quick Play path that cannot silently fall back to Strike, display-name mapping for Houses, and keeping WPF combat on Core.


Architecture recommendations (here only)

  1. Declare the shipping client in public. WPF, Windows-first, Unity later. Say it so you are not punished for “not Unity yet.”
  2. Make cards.json the only catalog a fight can see in dev. Promote on startup in Development; fail loud if payload coverage is below a threshold; delete or gate fallback Strike decks in non-dev builds.
  3. Do not merge the two loops yet unless a single engineer is about to implement the same shop twice. Document which loop is canonical for Kickstarter (WPF Quick Play) and treat Console/Sim as Core proofs.
  4. Simulation policies before more stores. A heuristic that understands Echo-Lock-to-5 and Overload debt is worth more than another writer.
  5. Keep Core pure. View-models present; handlers decide.
  6. Offline snapshot for the demo. Even a “play this folder” mode with a pinned JSON pack.
  7. Cut launch matrix to what the content calendar can finish. Architecture already supports 5×5. Launch should not.

The engine would get you hired. The ship plan is still a platform backlog wearing a game’s name — less true than in March, still true enough to say.