The Hetzner on-demand compute implementation is complete: - ARM64 builds working in CI - Shardok running on Hetzner CAX41 in Helsinki - Production Eagle connected via TLS + token auth - IPv6/NAT64 networking configured Delete the completed planning doc and add a new doc covering future latency optimization strategies: 1. Client-side animation masking (recommended first step) 2. Split Shardok architecture (future if needed) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
6.7 KiB
Shardok Latency Hiding Strategies
Problem Statement
With Shardok running on Hetzner (Helsinki) and Eagle on DigitalOcean (US), the round-trip latency for human commands is ~200-400ms:
Human posts command:
Unity → Eagle (DO) → Shardok (Hetzner) → Eagle (DO) → Unity
~10ms ~100ms ~100ms ~10ms
Total: ~220ms round-trip
This latency is acceptable for AI turns (users watch animations anyway), but creates noticeable lag when humans post commands.
Strategy 1: Client-Side Animation Masking
Concept
Start animations immediately when the user clicks, before server confirmation arrives. The animation duration masks the network latency.
Implementation by Command Type
Movement Commands (deterministic):
- Client knows the destination hex and movement path
- Start movement animation immediately on click
- Server confirms the move (should always match)
- If server rejects (invalid state), snap unit back to origin
Attack Commands (RNG-dependent):
- Show attack animation immediately (unit swings sword, fires arrow)
- Wait for server to return dice roll result
- Show damage numbers / hit effects when server responds
- Animation typically takes 300-500ms, masking most of the latency
End Turn:
- Latency not noticeable (user expects transition delay)
Unity Implementation Sketch
// In CommandHandler.cs
public void OnCommandSelected(Command command) {
// Start animation immediately
if (command.Type == CommandType.Move) {
unitController.StartMoveAnimation(command.TargetHex);
} else if (command.Type == CommandType.Attack) {
unitController.StartAttackAnimation(command.TargetUnit);
}
// Send to server in parallel
connection.SendCommand(command, (response) => {
if (response.Success) {
// Animation continues, apply result
ApplyCommandResult(response);
} else {
// Rollback animation
unitController.CancelAnimation();
ShowError(response.ErrorMessage);
}
});
}
Pros
- Simple implementation
- No server-side changes
- Works with existing architecture
Cons
- Doesn't eliminate latency for attacks with RNG (must wait for dice roll)
- Rollback needed if server rejects command (rare but possible)
Estimated Improvement
- Movement: ~200ms latency hidden (feels instant)
- Attacks: ~100-200ms hidden by animation, ~100ms visible wait for dice result
Strategy 2: Split Shardok Architecture
Concept
Run two Shardok instances:
- Shardok-Primary (DigitalOcean): Handles command processing, source of truth
- Shardok-AI (Hetzner): AI computation only
Human commands go to the nearby Primary for low latency. AI computation uses the powerful Hetzner instance.
Architecture
Human commands (low latency ~20ms)
Unity ←→ Eagle ←→ Shardok-Primary (DigitalOcean)
↓ state sync (when AI turn starts)
Shardok-AI (Hetzner)
↑ AI command response
How It Works
Human Turn:
- Human posts command → Eagle → Shardok-Primary (DO)
- Primary processes command immediately (~10ms local)
- Primary streams result to client via Eagle (~10ms)
- Total latency: ~20ms (vs ~220ms current)
AI Turn:
- When AI's turn starts, Primary sends game state snapshot to Hetzner
- Shardok-AI computes best command using full CPU power
- Shardok-AI returns command index to Primary
- Primary executes command locally and streams to client
- Repeat until AI turn ends
Protocol Changes
// New service for AI-only computation
service ShardokAIService {
// Send game state, receive AI's chosen command
rpc GetAICommand(AICommandRequest) returns (AICommandResponse);
}
message AICommandRequest {
bytes game_state = 1; // Serialized game state
int32 player_id = 2; // Which AI player
repeated bytes available_commands = 3; // Available command descriptors
}
message AICommandResponse {
int32 command_index = 1; // Index into available_commands
int32 search_depth = 2; // For debugging
double best_score = 3; // For debugging
}
Shardok-Primary Requirements
Shardok-Primary on DigitalOcean needs to:
- Process all commands (human and AI)
- Maintain authoritative game state
- Serialize/deserialize game state for AI requests
- Run on minimal CPU (command processing is fast)
This is essentially the current Shardok, but without running the AI search.
Shardok-AI Requirements
Shardok-AI on Hetzner needs to:
- Receive game state snapshots
- Run AI evaluation (IterativeDeepeningAI or MCTS)
- Return best command index
- No persistent state (stateless worker)
AI Turn Latency
Each AI command has ~200ms network latency. This is acceptable because:
- User is watching animations anyway
- Natural pacing lets user observe AI decisions
- AI computation is fast on Hetzner's 16 cores
For a typical AI turn with 5 commands: 5 × 200ms = 1 second network overhead, plus AI thinking time. With animations, this feels natural.
Implementation Phases
Phase 1: Add Shardok-Primary (minimal)
- Deploy existing Shardok container to DigitalOcean
- Configure Eagle to use local Shardok for all commands
- Human latency immediately improves
Phase 2: Add AI offload
- Implement
ShardokAIServiceRPC - Shardok-Primary calls Hetzner for AI commands
- Shardok-AI processes requests statelessly
Phase 3: Optimize
- Batch multiple AI actions if possible
- Pre-warm Shardok-AI connection
- Add fallback if Hetzner unavailable
Pros
- Human command latency drops from ~220ms to ~20ms
- AI still gets Hetzner's CPU power
- Clear separation of concerns
- Shardok-Primary can fall back to local AI if Hetzner unavailable
Cons
- Two Shardok instances to maintain
- State serialization overhead for AI requests
- Each AI action has network round-trip (acceptable with animations)
Comparison
| Approach | Human Latency | AI Throughput | Complexity | Changes Required |
|---|---|---|---|---|
| Current | ~220ms | High | - | - |
| Animation masking | ~220ms (perceived ~50ms) | High | Low | Unity only |
| Split architecture | ~20ms | High | Medium | New RPC, two deployments |
Recommendation
Phase 1 (now): Implement animation masking in Unity client
- Quick win, no server changes
- Improves perceived latency significantly for movement
- Attacks still show dice animation while waiting
Phase 2 (future): Split architecture if animation masking insufficient
- Only needed if users complain about attack latency
- More complex but provides true low latency
- Natural evolution of current architecture