An autopilot is easy to describe badly. "The AI controls the ship" sounds like a mode switch: the player has the wheel or the computer does. That is not the model in Black Skies. Manual input and AI are independent concurrent intention producers. Both submit intentions into one Gateway authority path. A player can steer while the autopilot is enabled, turn it off without changing the manual path, and turn it back on without replaying what the AI missed.
That distinction is the foundation for everything else. Ship AI does not own the actor, the tick, the target, or the tile. It proposes an intention. Gateway authenticates the producer, resolves the actor, enforces the source-tick deadline, and uses the same durable admission ledger that protects manual and server-origin actions. TileProcessor remains the final authority on whether the action is legal when it reaches the simulation.
The result is intentionally closer to power steering than a second driver. The system can add an intention while the player is present, provide offline care after the player leaves, and run an NPC through the same contract. If the AI fleet disappears, the steering wheel remains connected.
A deck is a tiny program
A decision deck is an ordered list of cards. Each card has a flat list of conditions and one action. Evaluation begins at the top on every committed tick. The first eligible card wins, at most one proposal is emitted, and the pass stops. A Hold card stops the pass without sending an intention. A PlayInstalledCard action chooses only from targets that satisfy the installed action's self, relation, range, and line-of-fire contract before applying its requested ordering. Stable entity IDs break ties, so the same brain revision and committed facts produce the same proposal.
SwitchDeck is deliberately not an instruction to resume evaluation somewhere else. It consumes the current decision, updates the active deck, and applies on the next tick. That makes a deck switch observable as one state transition rather than a hidden jump inside a partially evaluated program.
The restrictions do more work than the features. Version 1 accepts at most four predicates per card. It has no nested boolean expression, no cross-card dependency, no loop, no arbitrary intention type, and no behavior graph. More expressive systems can encode more compact behaviors, but they also make author intent harder to inspect and evaluation cost harder to print. Black Skies chooses a boring upper bound that can be compiled, reviewed, and executed inside a fixed tile-pass budget.
The evaluator is deterministic, but it is not authoritative. It can decide that a candidate looks eligible from its exact committed snapshot. Gateway and TileProcessor can still reject it because newer authoritative state exists or because a game rule fails at admission. The deck is a proposal program, not a private rules engine.
Newest state, not workflow history
Black Skies ticks at 2 Hz. That does not mean the worker fleet owns a durable job for every ship every 500 milliseconds. A committed tick makes work eligible. Notifications are hints that a tile advanced, not obligations to replay every intermediate thought.
Each worker retains only the newest state for a tile. If sequences 91, 92, and 93 arrive before the tile can be evaluated, sequence 93 replaces the others. One exact-state roster read serves the whole tile pass, then the worker evaluates the eligible brains in a bounded loop. A periodic current-sequence reconciliation repairs a lost notification by discovering the latest committed sequence. It does not reconstruct a backlog.
This is why the useful shard key is a tile, not an entity. The tile grouping buys one read of the exact committed state and one bounded pass for the ships that share it. It does not make the tile aware that a worker exists, and it does not make the worker an owner of tile lifecycle.
Logical shards keep that grouping stable while pod count changes. The fleet maps tiles onto 256 logical shards and uses Kubernetes Lease objects to coordinate shard holders. A holder UUID and lease generation fence submissions captured before lease loss. Kubernetes documents Leases as coordination records used for leader election and other shared-resource ownership; Black Skies adds its own generation check because a brief overlap must still be harmless at Gateway.
The job never owns the tile
The worker starts from the AiBrainRuntime schedule-shard index, groups enabled brains by their tile hints, and reads exact current membership and facts from Redis. The entity registry's active-tile index is a recovery and reconciliation fallback, not the normal hot path. Any tile ID found in schedule or registry metadata is still a location hint, not permission to create a simulation. The worker evaluates a deck and submits an actor-bound proposal. The tile neither starts the job nor knows that the job exists.
This matters because brain data can be older than movement. A ship may have left tile A while its schedule row still points there. The worker resolves the actor again and moves its pending evaluation to tile B's newest committed sequence. It never treats A's sequence number as comparable with B's. Sequences are local facts inside one immutable source tile.
If the movement race happens later, during admission, Gateway follows a stricter rule. An EntityNotOnTile response causes a transfer-aware actor lookup. If the actor now belongs to B, the old A-derived claim ends as AiActorRelocated. Gateway sends nothing to B and waits for a fresh B tick to produce a fresh decision. The old proposal cannot become a destination proposal merely because its actor moved.
That terminal boundary closes the dead-tile trap. A stale former tile is never probed for this proposal, so an NPC action cannot revive a tile the actor already left. The separate liveness path begins with an actor ID, resolves the actor's current tile, and may probe only that freshly resolved destination. It is allowed to wake the place where the ship is. It is not allowed to trust the place where an old schedule once said the ship was.
Target movement is different from actor movement. Gateway does not reroute an actor's intention to follow a target. The actor's authoritative tile rejects or fizzles the target, or uses an explicit cross-boundary effect rule when the card supports one. This keeps intention identity attached to the actor and source tick that produced it.
Storage follows responsibility
The design does not ask one database to be authoritative for every shape of truth.
PostgreSQL or Aurora owns deck authoring, version relationships, and the transactional outbox. An authoring edit and its publication record commit together. The outbox compiler publishes an immutable runtime version only after validation succeeds, so a runtime head never points at an artifact that failed to compile.
DynamoDB owns compiled versions, brain heads, schedules, projector checkpoints, and durable Gateway admission claims. Workers remain stateless and persist no tick backlog. Schedule and active-tile indexes are candidate enumerators, not strongly consistent authorities. AWS documents that all reads from DynamoDB global secondary indexes are eventually consistent, which is why every candidate is filtered by current facts and every action crosses Gateway again.
DAX is restricted to immutable compiled-deck point and batch reads. It is not used for mutable enablement, actor location, membership, or admission. AWS distinguishes the DAX item cache from its query cache and notes that writes to individual items do not invalidate cached Query results in the DAX consistency model. A cache with that property cannot be the membership authority for a moving fleet.
Redis owns exact current tile facts, membership, presence, and the latest per-actor AI status. Redis hashes fit the projection because each actor field can be overwritten by a higher location version and source sequence. The status hash expires when it is no longer refreshed. It is a diagnostic view, not a history.
Prometheus or CloudWatch stores bounded counters, gauges, and latency histograms. Actor IDs, target IDs, and admission IDs stay out of metric labels and belong in sampled structured logs or the latest Redis projection. There is no application time-series database and no per-tick application history. The runtime needs the newest playable truth plus operational aggregates, not an archive of every discarded thought.
Why Temporal left the hot loop
Temporal remains a good fit for durable processes that must remember and resume a particular chain of work. Its own documentation describes a Workflow Execution as durable and recoverable through Event History replay, and describes Continue-As-New as a way to checkpoint state into a fresh history when a long execution grows. Those are useful guarantees when the history is part of the product requirement.
They are the wrong guarantee for this loop. A ship decision is disposable the moment a newer committed tick exists. Persisting each 500 millisecond wake as workflow history would preserve work the design explicitly refuses to replay. A sharded Temporal workflow would move the same coalescing map and scheduler inside a long-running activity, leaving Temporal to supervise a daemon that Kubernetes already supervises.
So production ship decisions moved to a stateless worker fleet. The tile-owned NPC decision path was removed. New tutorial NPC decisions, persistent NPCs, offline player care, and online autopilot use the same evaluator and Gateway route. Legacy Temporal registrations can remain available only to drain old histories safely. Temporal still belongs in long-horizon orchestration where exact progress must survive; it no longer sits in the 2 Hz ship-decision loop. Temporal Schedules also start Workflow Executions at specified times, but generating a durable execution twice per second per ship would solve a scheduling problem Black Skies does not have.
Failure pauses AI, not the game
The system fails closed around missing or stale AI data. A missing newest snapshot skips the pass. A malformed brain quarantines only that revision. A lease loss cancels captured submissions. Dependency retries stay inside the current sequence deadline and are abandoned when a newer sequence exists. Duplicate evaluation reaches the same actor, tick, and proposal fingerprint, so Gateway's atomic admission claim can return the existing terminal result instead of accepting a second action.
If the entire AI cluster is unavailable, NPC decisions and autopilot pause. TileProcessor continues ticking. Gateway continues accepting manual input. When workers return, they query current sequences and current schedules, then resume from newest state. Missed ticks are never replayed. Offline care is therefore care from the world that exists now, not a burst of delayed actions from worlds that have already passed.
This separation is why manual and AI producers were never modeled as mutually exclusive modes. The player route has no dependency on the worker Deployment. Turning autopilot on starts another producer. Turning it off stops that producer. Neither operation reaches into the manual client's authority path.
What actually ran
The publication evidence manifest is bound to commit e72cb393ebc9e464cf1defb7afa280245e99a906 and rejects a skipped check as a pass. Its local implementation gates all passed. Its two cloud gates are still not run, so the manifest records implementationComplete: true and releaseReady: false.
| Release gate | Publication status |
|---|---|
dotnet-build |
Passed on the bound commit |
ship-ai-tests |
Passed on the bound commit |
gateway-tests |
Passed on the bound commit |
hex-contracts |
Passed on the bound commit |
tutorial-tests |
Passed on the bound commit |
source-architecture-guards |
Passed on the bound commit |
client-architecture |
Passed on the bound commit |
helm-lint-template |
Passed on the bound commit |
terraform-validate |
Passed on the bound commit |
iac-python-tests |
Passed on the bound commit |
secret-scan |
Passed on the bound commit |
worker-container-build |
Passed on the bound commit |
tile-pass-benchmark |
Passed locally: the fixed 10,000-roster and 500-AI-actor profile measured 110.6376 ms p99 against the 350 ms gate |
compose-chaos |
Passed locally across the six-scenario failure matrix |
authenticated-gateway-load |
External gate not run; no commit-bound cloud load artifact exists |
unity-editmode |
Passed locally, 95 of 95 tests |
unity-playmode |
Passed locally, 1 of 1 dedicated autopilot smoke test |
aws-ephemeral-load-chaos |
External gate not run; GitHub OIDC was denied before provisioning and no AWS resources were created |
The deterministic evaluator, actor-tick idempotency, newest-state coalescing, relocation discard, worker leases, storage boundaries, client toggle, NPC cutover, and manual-path isolation are therefore implementation-complete behind the recorded local gates. The measured in-process tile pass is not the missing authenticated cloud test. The cloud workload must still prove the Gateway route and EKS behavior under the approved sandbox profile before releaseReady can become true.
The boundary is the feature
The interesting part of ship AI is not the cleverness of a card. It is the number of things the card is forbidden to become. It cannot own authority, wake a stale location, carry a source sequence across tiles, replay a missed tick, bypass manual control, or turn a cache into truth.
Those refusals produce a system whose failure mode is understandable. The autopilot can let go of the wheel. It cannot disconnect the driver.
The ledger closes this series by collecting the wider architecture's measured results, unrun gates, and falsifiers. Ship AI brings two release invariants to it: at most one accepted AI action per actor and tick, and a permitted count of zero stale former-tile respawns. They become measured results only when their commit-bound gates run.