Shards of Stone provides a comprehensive web backend and headless simulation suite designed for custom map distribution, community matchmaking, competitive analytics, and automated testing.
This reference covers:
- REST APIs: Public and authenticated endpoints for discovering and publishing custom maps, joining multiplayer lobbies, retrieving competitive leaderboards, and generating generative assets.
- Headless Engine SDK (
HeadlessGame): A pure Node.js simulation engine that runs the full deterministic game loop up to 1000× real-time with zero DOM, Canvas, or WebGL dependencies for balance tuning, AI benchmarking, and scenario verification. - Client Embed SDK & postMessage RPC Protocol: Embed Shards of Stone matches directly inside external portals, learning management systems, and gaming communities with bi-directional
postMessageRPC bridging and standalone.sosmodpackaging.
Part 1: REST API Reference
All REST endpoints operate over HTTP/JSON. Authenticated requests use session cookies (session=<token>) or the Authorization: Bearer <session_token> header.
flowchart LR
Client["Client / Custom Tools"] --> API["Next.js Route Handlers (/api/*)"]
API --> DB[("Turso SQLite DB (libsql)")]
API --> GenAI["Generative Providers (Gemini / ElevenLabs)"]
API --> PeerNet["PeerJS / Lockstep Signaling"]1. Community Maps API
GET /api/maps
Lists and filters community and user-authored custom maps.
-
Query Parameters:
published(boolean, optional): Whentrue, returns all public, published community maps (no authentication required).mine(boolean, optional): Whentrue, returns maps created by the authenticated user (requires auth).search(string, optional): Substring search against map titles.sort(string, optional): Sort order. Options:'newest'(default),'rating','popular','name'.page(integer, optional, default:1): Page number.limit(integer, optional, default:12, max:50): Results per page.
-
Response
200 OK:
{
"maps": [
{
"id": "cm_8f3a12b4",
"name": "Iron Peak Fortress",
"description": "A 4-player defensive mountain hold with chokepoints and gold veins.",
"width": 128,
"height": 128,
"map_type": "standard",
"biome": "snow",
"player_count": 4,
"is_published": 1,
"thumbnail": "data:image/png;base64,...",
"play_count": 342,
"avg_rating": 4.85,
"rating_count": 27,
"created_at": 1726000000000,
"updated_at": 1726100000000,
"creator_username": "ThorgarIronbeard"
}
],
"pagination": {
"page": 1,
"limit": 12,
"total": 1,
"totalPages": 1
}
}GET /api/maps/[id]
Fetches full scenario metadata and the complete serialized JSON map payload.
-
Path Parameters:
id(string): The unique map identifier.
-
Behavior: Automatically increments the map's
play_countcounter. Unpublished drafts can only be fetched by the map's creator. -
Response
200 OK:
{
"map": {
"id": "cm_8f3a12b4",
"name": "Iron Peak Fortress",
"description": "A 4-player defensive mountain hold.",
"width": 128,
"height": 128,
"map_data": "{\"version\":1,\"tiles\":[...],\"spawnPoints\":[...],\"resources\":[...],\"triggers\":{...}}",
"creator_username": "ThorgarIronbeard",
"play_count": 343,
"avg_rating": 4.85,
"rating_count": 27
}
}POST /api/maps/[id]/publish
Publishes or unpublishes a custom map scenario. When publishing, the server executes strict validation and generates a cryptographic content hash.
- Authentication: Required (must be map author).
- Request Body:
{
"publish": true
}-
Validation Rules: The publishing route executes the authoritative server-side map validator before setting
is_published = 1:- Minimum Spawn Points: Map must define at least 2 valid player spawn points within bounds.
- Economic Viability: Map must include at least 1 accessible gold mine tile or resource deposit.
- No Footprint Overlaps: Authored buildings, doodads, and starting units must not occupy conflicting grid cells.
- Elevation Invariants: Ramp slopes and cliff edges must obey height-stride limits.
-
Response
200 OK:
{
"is_published": true
}- Error
400 Bad Request:
{
"error": "Map must have at least 2 spawn points to publish"
}POST /api/maps/[id]/rate
Submits or updates a 1–5 star player rating and optional feedback comment.
- Authentication: Required.
- Rules: Users cannot rate their own maps. Maps must be published. Ratings are upserted per user.
- Request Body:
{
"rating": 5,
"comment": "Incredible tower defense wave pacing. Difficult final boss!"
}- Response
200 OK:
{
"avg_rating": 4.88,
"rating_count": 28
}2. Multiplayer Lobby & Lockstep Matchmaking
GET /api/multiplayer/games
Lists active public multiplayer game lobbies waiting for players. Automatically prunes stale lobbies whose host heartbeat has lapsed for more than 10 minutes.
- Response
200 OK:
[
{
"id": "game_7c99e2",
"hostName": "Valdor",
"hostRace": "dwarf",
"mapSize": "medium",
"mapType": "continents",
"biome": "alpine",
"status": "waiting",
"totalSlots": 4,
"humanSlots": 4,
"joinedHumans": 2,
"spectators": 0,
"maxSpectators": 4,
"gameVersion": "v2.5.0",
"createdAt": 1726480000000
}
]POST /api/multiplayer/games/[id]/join
Reserves a player or spectator slot in a lobby and initiates the peer-to-peer lockstep handshake.
- Request Body:
{
"password": "optional_room_password",
"asSpectator": false
}-
Slot Reservation Invariant: The database executes an atomic
UPDATE ... WHERE joined_humans < human_slotsquery. If two peers click the final seat simultaneously, the loser immediately receives a409 Conflictinstead of overflowing the lobby. -
Response
200 OK:
{
"id": "game_7c99e2",
"seatNumber": 2,
"asSpectator": false,
"hostPeerId": "shards-host-game_7c99e2",
"lockstepTurnLength": 3
}3. Leaderboard, Ratings & Match Outcomes
Competitive standings use a Bayesian rating framework (similar to TrueSkill/Wheng-Lin) parameterized by latent skill $\mu$ and uncertainty $\sigma$.
GET /api/leaderboard
Queries global player rankings and combat stats.
-
Query Parameters:
sort:'score'(default),'wins','winrate', or'highest'.limit: Number of entries (default:50, max:100).offset: Pagination offset (default:0).
-
Response
200 OK:
{
"players": [
{
"rank": 1,
"username": "GloinTheGreat",
"avatarId": "dwarf_thane",
"totalScore": 142850,
"highestScore": 12400,
"wins": 48,
"losses": 6,
"gamesPlayed": 54,
"winRate": 89,
"totalUnitsKilled": 3480,
"totalBuildingsRazed": 412
}
],
"total": 1280,
"currentUser": {
"rank": 42,
"username": "MyPlayerName",
"totalScore": 31200,
"wins": 12,
"losses": 8,
"gamesPlayed": 20,
"winRate": 60
}
}GET /api/ratings
Fetches Trueskill-derived ratings for auto-balancing lobbies.
-
Query Parameters:
names(string): Comma-separated list of usernames (max 16).mode(string): Rating queue mode ('1v1','team','ffa').
-
Rating Formulas:
- Match Balancing Rating:
matchRating = μ - 1.0σ - Displayed Leaderboard Rating:
rating = μ - 3.0σ
- Match Balancing Rating:
-
Response
200 OK:
{
"mode": "team",
"ratings": {
"GloinTheGreat": {
"mode": "team",
"matchRating": 28.45,
"rating": 22.10,
"games": 54,
"provisional": false
}
}
}POST /api/auth/update-stats
Authoritative settlement endpoint for reporting match results and updating ratings.
- Security & Asymmetric Corroboration Ledger:
Match outcomes require consensus across peers. The endpoint writes claims to the
match_resultsledger keyed by(mp_match_id, user_id). A victory claim remains uncommitted until corroborated by peer reports, preventing client-side spoofing. - Request Body:
{
"matchId": "match_98b2e104",
"outcome": "win",
"score": 4500,
"durationSeconds": 920,
"stats": {
"unitsKilled": 85,
"buildingsRazed": 12,
"resourcesGathered": 14200
}
}4. Generative AI Asset & Audio Hooks
These endpoints connect to generative backends during map creation and dev tooling. Both routes are guarded by devOnlyGuard() and reject calls in production environments unless running in local development or staging.
POST /api/generate-asset
Invokes the Gemini image-generation pipeline (API_KEY, API_URL) to synthesize sprites, building concepts, or minimap art.
- Request Body:
{
"prompt": "Isometric 3/4 RTS dwarf steam tank with brass boiler and iron treads, neutral magenta background"
}- Response
200 OK:
{
"candidates": [
{
"content": {
"parts": [{ "inlineData": { "mimeType": "image/png", "data": "base64..." } }]
}
}
]
}POST /api/generate-sound
Invokes the ElevenLabs sound-effect synthesis API (ELEVEN_LABS_API_KEY) for unit voice lines, spell audio, and ambient cues.
- Request Body:
{
"text": "Deep stone grinding and mechanical steam hiss",
"duration_seconds": 2.5
}- Response
200 OK: Returns anaudio/mpegbinary buffer stream.
Part 2: Headless Simulation SDK (HeadlessGame)
The HeadlessGame harness (tools/ai_loop/HeadlessGame.ts) runs complete RTS matches in pure Node.js.
flowchart TD
Config["HeadlessGameConfig (Seed, Races, Map, Slots)"] --> Init["await game.init()"]
Init --> Loop["Simulation Loop (while playing && ticks < maxTicks)"]
subgraph SimEng["Pure Node.js Sim Engine (0 DOM / 0 Canvas)"]
World["ECS World (Canonical ID Ordering)"]
Combat["CombatSystem"]
Movement["MovementSystem"]
Path["NavGrid / SpatialGrid / FlowFields"]
AI["AISystem / LLMAISystem"]
Triggers["TriggerSystem (AST Total Eval)"]
World --- Combat
World --- Movement
World --- Path
World --- AI
World --- Triggers
end
Loop --> SimEng
SimEng --> Stats["ResultCollector (APM, Economy, Deadliest Units)"]
Stats --> Res["GameResult (Winner, Ticks, Stats JSON)"]Why Run Headless?
- Zero Presentation Overhead: By stripping Canvas 2D, Three.js 3D meshes, WebGL buffers, and HTML audio, the engine executes up to 1000× faster than real-time. A standard 30-minute match (36,000 ticks) simulates in under 20 seconds on a modern CPU.
- Deterministic Parity with Browser:
HeadlessGameregisters the exact same ECS systems at the exact same priorities as the live browser game (Game.ts). - Core Use Cases:
- Balance Tuning: Run 1,000 matches overnight (Dwarves vs Goblins) across diverse seeds to calculate win rate deviations, unit kill/death ratios, and resource pacing.
- AI Benchmarking: Evaluate LLM-driven agents against rule-based heuristic bots.
- Scenario Quality Assurance: Verify that custom maps contain no unreachable spawn islands, resource starvation bugs, or infinite trigger loops.
HeadlessGame Configuration Schema
export interface HeadlessGameConfig {
/** Map generation seed. REQUIRED for reproducible deterministic runs. */
mapSeed: number;
/** Map size preset ('small' = 64, 'medium' = 96, 'large' = 128). */
mapSize: 'small' | 'medium' | 'large';
/** Map topology ('standard', 'islands', 'continents', 'coastal'). */
mapType: MapType;
/** Player roster configuration. Supports 2 to 8 players. */
slots?: Array<{
playerId?: number;
race: 'dwarf' | 'goblin' | 'ratmen' | 'lizardmen' | 'wood_elf' | 'high_elf' | 'dark_elf';
difficulty?: 'easy' | 'medium' | 'hard';
isAI?: boolean;
teamId?: number;
}>;
/** Maximum ticks before declaring a stalemate (default: 72,000 = 60 minutes). */
maxTicks?: number;
/** Periodic stdout logging interval in ticks (default: 600 = 30 seconds). */
logInterval?: number;
/** Optional raw custom map JSON string to load an authored scenario. */
customMapJson?: string;
/** Per-tick hook for telemetry capture or mid-match command injection. */
onTick?: (tick: number, game: HeadlessGame) => void;
}Complete Code Recipe: Running an Automated Simulation
The recipe below demonstrates how to configure a headless match, inject player commands, run 10,000 ticks, and inspect final combat telemetry.
import { HeadlessGame } from '@/../tools/ai_loop/HeadlessGame';
import type { HeadlessGameConfig } from '@/../tools/ai_loop/HeadlessGame';
import { TICKS_PER_SECOND } from '@/game/data/constants';
async function runAutomatedScenario() {
console.log('--- Initializing Headless Simulation ---');
const config: HeadlessGameConfig = {
mapSeed: 42891,
mapSize: 'medium',
mapType: 'standard',
maxTicks: 10000, // Simulate ~8.3 minutes of gameplay
logInterval: 1200, // Log progress every 60 game seconds
slots: [
{ playerId: 0, race: 'dwarf', difficulty: 'hard', teamId: 0 },
{ playerId: 1, race: 'goblin', difficulty: 'hard', teamId: 1 },
],
onTick: (tick, game) => {
// Example: Injecting mid-game command on Tick 100
if (tick === 100) {
console.log('[Hook] Tick 100: Dispatching emergency scout order for Player 0');
game.eventBus.emit('request-train', {
unitTypeId: 'dwarf_miner',
playerId: 0,
});
}
},
};
const game = new HeadlessGame(config);
// 1. Initialize simulation (builds NavGrid, spawns starting bases, boots ECS)
await game.init();
const startWallTime = performance.now();
// 2. Run simulation loop to completion or maxTicks
const result = await game.run();
const totalWallSeconds = ((performance.now() - startWallTime) / 1000).toFixed(2);
const simGameMinutes = (result.ticks / TICKS_PER_SECOND / 60).toFixed(1);
const speedupMultiplier = Math.round((result.ticks / TICKS_PER_SECOND) / (Number(totalWallSeconds) || 1));
// 3. Inspect and assert results
console.log('\n--- Simulation Completed ---');
console.log(`Wall clock time: ${totalWallSeconds}s`);
console.log(`Simulated time: ${result.ticks} ticks (${simGameMinutes} minutes)`);
console.log(`Execution speed: ${speedupMultiplier}x real-time`);
console.log(`Winner: Player ${result.winner} (${result.winnerRace})`);
console.log(`Outcome Reason: ${result.reason}`);
// 4. Output seat breakdown
for (const seat of result.seats) {
console.log(`\n[Seat ${seat.playerId} - ${seat.race}]`);
console.log(` Units Produced: ${seat.unitsProduced}`);
console.log(` Units Slain: ${seat.kills}`);
console.log(` Units Lost: ${seat.losses}`);
console.log(` Buildings Razed: ${seat.buildingsRazed}`);
console.log(` Gold Harvested: ${seat.totalGathered.gold}`);
console.log(` Lumber Harvested: ${seat.totalGathered.lumber}`);
console.log(` Peak Military Pop: ${seat.peakArmySupply}`);
}
}
// Execute
runAutomatedScenario().catch(console.error);Inspecting Outcome Telemetry (GameResult)
The returned GameResult object provides detailed metrics collected during the run:
export interface GameResult {
readonly winner: number; // Winning player ID (-1 for draw/stalemate)
readonly winnerRace: string;
readonly ticks: number; // Total ticks simulated
readonly reason: 'elimination' | 'wonder' | 'regicide' | 'stalemate' | 'max_ticks';
readonly seats: Array<{
readonly playerId: number;
readonly race: string;
readonly kills: number;
readonly losses: number;
readonly unitsProduced: number;
readonly buildingsRazed: number;
readonly totalGathered: {
readonly gold: number;
readonly lumber: number;
readonly stone: number;
readonly oil: number;
};
readonly finalScore: number;
readonly peakArmySupply: number;
}>;
}Part 3: Client Embed SDK & postMessage RPC Protocol
Shards of Stone matches can be embedded seamlessly into web portals, LMS platforms, external tournament websites, or custom modding tools using a standard <iframe> and bi-directional postMessage RPC protocol.
sequenceDiagram
autonumber
actor Host as Host Web Application
participant Iframe as Shards of Stone (<iframe>)
participant Engine as Game Engine & EventBus
Host->>Iframe: Mount <iframe src="/play?embed=1&customMap=...">
Iframe-->>Host: postMessage { type: "sos:ready" }
Iframe->>Engine: game.start()
Engine-->>Host: postMessage { type: "sos:match-start", payload: { tick: 0 } }
rect rgb(20, 25, 40)
Note over Host,Engine: Active Match Loop
Host->>Iframe: postMessage { type: "sos:pause" }
Host->>Iframe: postMessage { type: "sos:resume" }
Engine-->>Host: postMessage { type: "sos:trigger-event", payload: { ...data, tick } }
end
Engine-->>Host: postMessage { type: "sos:match-end", payload: { winner, tick, state } }1. Iframe Embedding & Query Parameters
To embed the game client inside an external host page, mount an <iframe> pointing to the /play route with the embed=1 parameter:
<iframe
id="game-viewport"
src="https://shardsofstone.com/play?embed=1&customMap=siege_of_iron&mod=chaos_rebalance"
width="1280"
height="720"
allow="autoplay; fullscreen"
style="border: none; width: 100%; height: 100vh;"
></iframe>Supported Query Parameters
| Parameter | Type | Description |
|---|---|---|
embed | 1 | Enforces full-bleed canvas layout, suppresses standalone page headers, hides window cramp warnings, and bypasses guest/auth gates. |
customMap | string | Scenario ID or filename (e.g. siege_of_iron or session). Automatically fetched from /maps/<name>.json or loaded from session storage. |
mod | string | Mod identifier or package name (e.g. chaos_rebalance). Resolves against /mods/<id>.sosmod, /mods/<id>.json, or active session mod storage. |
2. Standalone Mod Package (.sosmod) Specification
Standalone mod packages (.sosmod) encapsulate balance patches, definition overrides, and trigger scripts as portable, redistributable JSON archives. They can be authored in The Forge Map Editor via File -> Export Standalone Mod (.sosmod)... or imported through File -> Import Standalone Mod (.sosmod)... and the AI Import panel.
Mod Package Schema
export interface ModPackage {
/** Optional JSON schema URI */
$schema?: string;
/** Unique mod identifier (slug format: lowercase alphanumeric with hyphens/underscores) */
id: string;
/** Human-readable display title */
name: string;
/** Semantic versioning string (e.g., "1.2.0") */
version: string;
/** Author handle or organization name */
author: string;
/** Detailed description of the modifications */
description: string;
/** Categorical tags for indexing (e.g. ["balance", "hardcore", "pve"]) */
tags?: string[];
/** Primary category grouping */
category?: string;
/** Unit and building attribute overrides */
defOverrides?: DefOverrideSet;
/** Custom trigger script actions and condition trees */
triggers?: TriggerScript;
/** ISO timestamp of packaging */
created?: string;
}Layering Semantics (applyModPackageToMap)
When a match launches with a .sosmod package attached:
- Base map properties, terrain, and placements remain intact.
defOverridesare deep-merged into the map's definition overrides, preserving unshadowed values.- Variables declared in the mod's
triggers.variablesare combined with the map's variables. - Triggers defined in
triggers.triggersare appended to the map's trigger list, skipping triggers with duplicate IDs.
3. Inbound RPC Messages (Host → Iframe)
Send messages to the game client by posting to the iframe's contentWindow:
sos:load-map
Launches or reloads a custom map dynamically without refreshing the page.
- Payload Properties:
mapData(string | object): Raw serialized map JSON or parsed JavaScript map object.biome(string, optional): Biome override ('forest','snow','desert','wasteland','swamp','cave').mod(ModPackage, optional): Optional standalone.sosmodpackage to apply on top of the map.
iframe.contentWindow.postMessage({
type: 'sos:load-map',
payload: {
mapData: mySerializedMapJson,
biome: 'snow',
mod: myCustomModPackage,
},
}, '*');sos:pause
Pauses the active single-player simulation loop and displays the pause menu.
iframe.contentWindow.postMessage({ type: 'sos:pause' }, '*');sos:resume
Resumes a paused simulation loop and returns focus to the game viewport.
iframe.contentWindow.postMessage({ type: 'sos:resume' }, '*');4. Outbound RPC Messages (Iframe → Host)
The game client posts events upward to window.parent:
sos:ready
Dispatched when the play page has mounted, initialized React state, and registered the postMessage event listener.
{
"type": "sos:ready"
}sos:match-start
Dispatched immediately when game.start() initiates the fixed-timestep simulation loop.
{
"type": "sos:match-start",
"payload": {
"tick": 0
}
}sos:trigger-event
Dispatched whenever a scenario script executes the raiseEvent trigger action. Custom payloads passed to raiseEvent are forwarded verbatim along with the simulation tick.
{
"type": "sos:trigger-event",
"payload": {
"eventName": "boss_defeated",
"bossId": "dwarf_steam_tank",
"killerPlayerId": 0,
"tick": 4200
}
}sos:match-end
Dispatched when the match reaches a terminal victory, defeat, or stalemate condition.
{
"type": "sos:match-end",
"payload": {
"winner": 0,
"tick": 8420,
"state": 3
}
}(Note: state corresponds to GameState.VICTORY = 3, GameState.DEFEAT = 4).
5. Complete Host Integration Example
Below is a complete implementation showing how a host web application can initialize an embedded Shards of Stone iframe, coordinate match loads, and respond to engine telemetry:
// host-app.ts
interface EmbedMessage<T = unknown> {
type: string;
payload?: T;
}
const iframe = document.getElementById('rts-embed') as HTMLIFrameElement;
// 1. Listen for game lifecycle events
window.addEventListener('message', (event: MessageEvent<EmbedMessage>) => {
if (!event.data || typeof event.data !== 'object') return;
const { type, payload } = event.data;
switch (type) {
case 'sos:ready':
console.log('[Host] Shards of Stone embed ready for match commands');
// Ready to dispatch initial scenario
break;
case 'sos:match-start':
console.log('[Host] Match started at simulation tick:', (payload as any)?.tick);
break;
case 'sos:trigger-event':
console.log('[Host] Custom scenario trigger event:', payload);
// Example: award an achievement or update external quest tracker
break;
case 'sos:match-end': {
const { winner, tick, state } = payload as { winner: number; tick: number; state: number };
console.log(`[Host] Match concluded! Winner: Player ${winner} at tick ${tick}`);
break;
}
}
});
// 2. Control match state from external buttons
function pauseGame() {
iframe.contentWindow?.postMessage({ type: 'sos:pause' }, '*');
}
function resumeGame() {
iframe.contentWindow?.postMessage({ type: 'sos:resume' }, '*');
}
async function loadScenarioWithMod(mapData: string, modPackage: unknown) {
iframe.contentWindow?.postMessage({
type: 'sos:load-map',
payload: {
mapData,
biome: 'wasteland',
mod: modPackage,
},
}, '*');
}Previous: Engine Hooks & Event Bus. Next: Asset Catalog & Key Registry. Back to: Map Editor Overview.