Game Contract
The Game Contract defines the interface any web game must implement to plug into LoopLoop. The platform treats the game as a black box that exposes well-defined integration points. As long as a game satisfies these contracts, LoopLoop can collect feedback, run the AI pipeline, playtest with bots, deploy changes, and notify players — regardless of genre, engine, or complexity.
Contract Summary
| Contract | Purpose | Required |
|---|---|---|
| Config Contract | Data-driven parameters the AI can safely edit | Yes |
| State Exposure Contract | Observable game state for automated playtesting | Yes |
| Telemetry Contract | Lifecycle events the platform collects and analyzes | Yes |
| Feedback Hook Contract | Integration points for the feedback UI | Yes |
| Scene Contract | Required scenes/screens the game must implement | Yes |
| Guardrail File Contract | Design constraints the AI reads as context | Yes |
| Build Contract | How the game compiles and runs | Yes |
| Asset Contract | How generated art integrates into the game | Recommended |
Config Contract
All tunable game parameters must live in JSON files under a config/ directory. This is the AI’s primary editing surface — the safest place for the pipeline to make changes, because it’s pure data with no logic.
Requirements
- A
config/directory must exist at the game’s source root. - All numeric balance values (speeds, health, damage, cooldowns, spawn rates, XP thresholds, etc.) must live in config files, not hardcoded in source.
- Entity definitions (enemies, weapons, items, levels, etc.) must be data-driven — defined in config and loaded by the engine at boot.
- Config files must be valid JSON with a consistent, documented structure.
- The game engine must read config at startup. Changing a config file and rebuilding must change game behavior with no code modifications.
Recommended Structure
config/
├── balance.json # Global tunable numbers (player stats, timings, rates)
├── enemies.json # Enemy type definitions (name, stats, behavior, sprite)
├── weapons.json # Weapon definitions (name, stats, projectile, sprite)
├── items.json # Item/upgrade definitions
├── waves.json # Spawn patterns, difficulty curve over time
└── progression.json # XP curve, level-up pools, unlock conditions
balance.json Example Shape
{
"player": {
"maxHealth": 100,
"speed": 150,
"invincibilityFrames": 500,
"pickupRadius": 50
},
"xp": {
"baseToLevel": 10,
"scalingFactor": 1.3,
"gemValue": 1
},
"session": {
"maxDurationSeconds": 300,
"difficultyRampInterval": 30
}
}
Entity Definition Example Shape (enemies.json)
{
"enemies": [
{
"id": "bat",
"name": "Bat",
"health": 5,
"damage": 8,
"speed": 80,
"xpValue": 1,
"sprite": "enemies/bat",
"behavior": "chase_player",
"spawnWeight": 10
}
]
}
The AI pipeline edits these files directly. The guardrail file contract constrains what values the AI can set.
State Exposure Contract
The game must expose its internal state to window.__gameState so that Playwright bots can observe the game without computer vision. This object is updated every frame (or at a minimum every 100ms).
Requirements
- The game must set
window.__gameStateas a plain JavaScript object. - The object must be updated continuously during gameplay.
- The object must include at minimum the fields defined below.
- Games may add additional game-specific fields beyond the required set.
Required Fields
interface GameState {
// Session metadata
status: "loading" | "playing" | "paused" | "game_over" | "transition" | "menu" | "feedback";
sessionId: string;
elapsedSeconds: number;
gameVersion: string;
// Player state
player: {
x: number;
y: number;
health: number;
maxHealth: number;
level: number;
xp: number;
xpToNextLevel: number;
alive: boolean;
};
// Entity counts and positions
entities: {
enemies: Array<{ id: string; type: string; x: number; y: number; health: number }>;
pickups: Array<{ id: string; type: string; x: number; y: number }>;
};
// Session metrics (running totals)
metrics: {
enemiesKilled: number;
damageDealt: number;
damageTaken: number;
itemsCollected: number;
levelsGained: number;
};
}
Usage
Playwright bots query this object to make decisions (which direction to move) and to collect session metrics (how long the player survived, what killed them). The platform’s playtest harness reads window.__gameState via page.evaluate().
Telemetry Contract
The game must emit lifecycle events that the platform collects, stores, and feeds into the AI pipeline. Telemetry is the implicit feedback channel — it tells us what players actually do, not just what they say.
Requirements
- The game must call a platform-provided
LoopLoop.telemetry.emit(event)function for each event. See the API Reference (section 8. Client SDK) for the client SDK surface. - Events must conform to the schemas below.
- The platform batches events and submits them to the backend on session end (or periodically for long sessions).
Required Events
| Event | When | Required Fields |
|---|---|---|
session_start | Game scene loads and gameplay begins | sessionId, gameVersion, playerId (nullable) |
session_end | Player dies, quits, or timer expires | sessionId, durationSeconds, enemiesKilled, levelReached, causeOfDeath |
player_damaged | Player takes damage | sessionId, source (enemy type or hazard), damage, remainingHealth |
entity_killed | Player kills an enemy | sessionId, entityType, weaponUsed, elapsedSeconds |
level_up | Player gains a level | sessionId, newLevel, upgradeChosen, upgradesOffered |
item_acquired | Player picks up an item/weapon | sessionId, itemId, itemType, elapsedSeconds |
Optional Events (Game-Specific)
Games may emit additional events beyond the required set. The pipeline stores all events and the AI triage step can reference any of them. Examples:
boss_encountered,secret_found,achievement_unlocked,zone_entered
Event Shape Example
LoopLoop.telemetry.emit({
type: "level_up",
sessionId: "abc-123",
timestamp: Date.now(),
data: {
newLevel: 5,
upgradeChosen: "fire_wand_2",
upgradesOffered: ["fire_wand_2", "health_boost", "speed_up"]
}
});
Feedback Hook Contract
The game must integrate LoopLoop’s feedback UI at natural pause points in the game loop — moments where the player is already waiting or reflecting, so asking for feedback feels seamless rather than disruptive.
Requirements
- Natural feedback moments — The game must identify and flag at least one natural pause point where asking for feedback fits the flow. Examples: between rounds, after a boss fight, on a results tally, at a level transition, after an upgrade selection, when returning to a hub. There is no requirement for a dedicated “Game Over” screen — the game decides what its natural moments are.
- Quick prompt at pause points — At each flagged moment, the game triggers the quick feedback prompt. This is a lightweight one-tap rating that takes under 2 seconds and does not break flow.
- Detailed feedback via the feedback button — The SDK auto-mounts a draggable in-game feedback button. Players can tap it at any time to open the detailed form. No additional integration is needed beyond calling
startSession(). - Context passing — When invoking the feedback UI, the game must pass the current session context (sessionId, game version, player state at time of feedback).
Quick Feedback Prompt
A single-tap rating shown at natural pause points:
- Too Easy
- Just Right
- Too Hard
- Boring
- Buggy / Broken
Plus an optional “Tell us more…” link that opens the detailed form.
Detailed Feedback Form
A full form with:
- Category selector (Balance, Content, Bug, Art/Audio, UX, Other)
- Freeform text field
- Optional screenshot capture (platform provides this utility)
Accessible at any time via the in-game feedback button (auto-mounted by the SDK).
Integration
// At a natural pause point (between rounds, after a boss, level transition, etc.)
LoopLoop.feedback.showQuickPrompt({
sessionId: currentSession.id,
gameVersion: CONFIG.version,
sessionMetrics: {
durationSeconds: elapsed,
enemiesKilled: kills,
levelReached: level,
causeOfDeath: deathCause
}
});
// The detailed feedback form is always available via the in-game
// feedback button (auto-mounted by LoopLoop.telemetry.startSession).
// No additional code needed. To open it programmatically:
LoopLoop.feedback.showDetailedForm({
gameVersion: CONFIG.version,
context: "pause_menu"
});
Examples of Natural Feedback Moments
| Game Type | Natural Moment |
|---|---|
| Roguelite / Run-based | Between runs, on the results tally |
| Endless runner | After the run ends, before “play again” |
| Puzzle | Between levels, after completing a chapter |
| Racing | Post-race standings screen |
| RPG | After a boss fight, at a save point |
| Idle / Incremental | On prestige, after an offline earnings screen |
Scene Contract
The platform does not prescribe how games structure their screens, scenes, menus, or UI. Games can have whatever internal architecture they want. The only two hard rules are:
-
Direct to gameplay. The first interactive screen the player sees must be the game running. No title screens, no main menus, no “press start”. A loading screen is fine. A login popup overlay is fine (and can be dismissed). But the player must land in gameplay with zero interaction required.
-
Trigger feedback at natural pause points. The game must call
showQuickPrompt()at least once per session at a moment that fits its loop (see the Feedback Hook Contract). Everything else — menus, settings, pause screens, inventories, shops — is entirely up to the game.
Guardrail File Contract
The game must provide a set of guardrail files in a guardrails/ directory. The AI pipeline reads these files as context before making any changes. They define the game’s identity, what’s safe to change, and the hard boundaries on every parameter.
Required Files
| File | Purpose |
|---|---|
guardrails/DESIGN_DOC.md | The game’s design document / constitution. Defines genre, theme, player fantasy, core loop, tone, hard no’s. The AI reads this to understand what kind of game it’s working on. |
guardrails/BOUNDARIES.md | Defines GREEN / YELLOW / RED zones — which files and systems the AI may freely change, change with caution, or must not touch. |
guardrails/PARAMETER_BOUNDS.json | Machine-readable min/max bounds for every tunable parameter in config/. The pipeline’s validate step rejects any change that exceeds these bounds. |
guardrails/STYLE_GUIDE.md | Visual and tonal guidelines for art generation. Defines art style, color palette, sprite dimensions, visual language for entities. The art agent reads this when generating assets via Scenario.gg. |
These files are the authoritative guardrail surface: the pipeline parses them, applies them during validation, and blocks changes that violate bounds or cross RED zones.
Build Contract
The game must be buildable and deployable as a static web application.
Requirements
- Single build command — The game must build with a single shell command (e.g.,
npm run build). The command must be specified in the project’spackage.jsonscripts. - Static output — The build must produce a directory of static files (HTML, JS, CSS, assets) that can be served by any static hosting provider.
- Output directory — The build output must go to a known directory (e.g.,
dist/), configurable in the project config. - Zero runtime dependencies — The built game must run entirely in the browser. No server-side rendering or runtime server required for the game itself (API endpoints are separate).
- Error-free build — A clean build on the current
mainbranch must always succeed. The pipeline treats build failure as a hard gate.
Platform API Endpoints
The game’s runtime code communicates with the platform via HTTP endpoints. Schemas, authentication, and the full route list are documented in the API Reference. At a high level, the browser SDK talks to routes such as:
POST /api/feedback— Submit player feedbackPOST /api/telemetry— Submit telemetry event batchGET /api/auth/session— Get current player sessionGET /api/notifications— Get unread notifications for current playerGET /api/releases/latest— Get latest release notes
Asset Contract (Recommended)
For games that want to use LoopLoop’s art pipeline (Scenario.gg integration), assets should follow these conventions.
Requirements
- Generated assets directory — AI-generated art goes in
public/assets/generated/. The art agent writes to this directory; the game reads from it. - Asset references in config — Entity definitions in
config/*.jsonmust reference sprites by path (e.g.,"sprite": "enemies/bat"). The art agent updates these references when it generates new assets. - Fallback sprites — The game should handle missing sprites gracefully (show a placeholder) in case an art generation fails.
Directory Structure
public/assets/
├── sprites/ # Hand-crafted or initial placeholder sprites
├── audio/ # Sound effects and music
└── generated/ # AI-generated art (Scenario.gg output)
├── enemies/
├── weapons/
├── items/
└── environment/
Minimal Compliant Game Example
The simplest possible game that satisfies the contract:
my-game/
├── package.json # "build": "vite build"
├── vite.config.ts
├── config/
│ └── balance.json # At least one config file with tunable values
├── guardrails/
│ ├── DESIGN_DOC.md # "This is a clicker game about..."
│ ├── BOUNDARIES.md # "GREEN: config/, YELLOW: src/entities/, RED: src/engine/"
│ ├── PARAMETER_BOUNDS.json # { "clickValue": { "min": 1, "max": 100 } }
│ └── STYLE_GUIDE.md # "Flat, colorful, 64x64 icons"
├── src/
│ └── main.ts # Sets window.__gameState, emits telemetry, renders game
├── public/
│ └── index.html
└── dist/ # Build output
Even a one-button clicker game can plug into LoopLoop and receive autonomous daily improvements from player feedback. The contract is designed to be lightweight enough that compliance is trivial, while providing enough structure for the pipeline to operate effectively.