feat(web): add replay-schema-v1.json downloadable schema file
Add comprehensive JSON Schema for replay format (v1) as specified in plan §15.2. This enables third-party tooling to validate and understand replay files programmatically. Schema documents: - Root replay object (format_version, match_id, config, timestamps) - Match result (winner, reason, scores, stats) - Player information - Map data (walls, cores, energy nodes) - Turn-by-turn state (bots, cores, energy, scores, events) - Optional win probability curve and critical moments - Event types (bot_spawned, bot_died, energy_collected, core_captured, combat_death, collision_death) - Debug telemetry for bot reasoning visualization All fields include descriptions, types, constraints, and examples. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0f44672634
commit
2022baffac
2 changed files with 606 additions and 1 deletions
|
|
@ -1 +1 @@
|
|||
f80df218f250d4f79c7ccf75f5daf2a8b108d1ea
|
||||
2e7eec49516984eb00a641fd03ec7a44fc7212a1
|
||||
|
|
|
|||
605
web/public/replay-schema-v1.json
Normal file
605
web/public/replay-schema-v1.json
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://aicodebattle.com/replay-schema-v1.json",
|
||||
"title": "AI Code Battle Replay Format",
|
||||
"description": "Complete replay format for AI Code Battle matches. Documents the full game state for each turn, enabling third-party tooling and replay viewers.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"format_version",
|
||||
"match_id",
|
||||
"config",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"result",
|
||||
"players",
|
||||
"map",
|
||||
"turns"
|
||||
],
|
||||
"properties": {
|
||||
"format_version": {
|
||||
"type": "string",
|
||||
"description": "Schema format version (semver). Current version is '1.0'. Additive changes only in future versions.",
|
||||
"pattern": "^\\d+\\.\\d+(\\.\\d+)?$"
|
||||
},
|
||||
"match_id": {
|
||||
"type": "string",
|
||||
"description": "Unique match identifier (format: m_ + 8 hex chars).",
|
||||
"pattern": "^m_[a-f0-9]+$"
|
||||
},
|
||||
"config": {
|
||||
"$ref": "#/$defs/Config"
|
||||
},
|
||||
"start_time": {
|
||||
"type": "string",
|
||||
"description": "Match start timestamp (RFC3339 UTC).",
|
||||
"format": "date-time",
|
||||
"examples": ["2026-03-29T20:07:12.514110787Z"]
|
||||
},
|
||||
"end_time": {
|
||||
"type": "string",
|
||||
"description": "Match end timestamp (RFC3339 UTC).",
|
||||
"format": "date-time",
|
||||
"examples": ["2026-03-29T20:07:12.520460415Z"]
|
||||
},
|
||||
"result": {
|
||||
"$ref": "#/$defs/MatchResult"
|
||||
},
|
||||
"players": {
|
||||
"type": "array",
|
||||
"description": "Player information for all participants.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ReplayPlayer"
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"$ref": "#/$defs/ReplayMap"
|
||||
},
|
||||
"turns": {
|
||||
"type": "array",
|
||||
"description": "Turn-by-turn game state snapshots. Array length equals number of turns played.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ReplayTurn"
|
||||
}
|
||||
},
|
||||
"win_prob": {
|
||||
"type": "array",
|
||||
"description": "Win probability per turn from Monte Carlo rollouts. Optional; may be computed post-match.",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": "Array of win probabilities [0.0-1.0] per player. Length equals player count.",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"critical_moments": {
|
||||
"type": "array",
|
||||
"description": "Significant turns where win probability shifted dramatically. Optional.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/CriticalMoment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"Config": {
|
||||
"type": "object",
|
||||
"description": "Game configuration parameters.",
|
||||
"required": [
|
||||
"rows",
|
||||
"cols",
|
||||
"max_turns",
|
||||
"vision_radius2",
|
||||
"attack_radius2",
|
||||
"spawn_cost",
|
||||
"energy_interval",
|
||||
"cores_per_player"
|
||||
],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "integer",
|
||||
"description": "Grid height (number of rows).",
|
||||
"minimum": 30,
|
||||
"maximum": 120,
|
||||
"default": 60
|
||||
},
|
||||
"cols": {
|
||||
"type": "integer",
|
||||
"description": "Grid width (number of columns).",
|
||||
"minimum": 30,
|
||||
"maximum": 120,
|
||||
"default": 60
|
||||
},
|
||||
"max_turns": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of turns before match ends.",
|
||||
"minimum": 100,
|
||||
"maximum": 1000,
|
||||
"default": 500
|
||||
},
|
||||
"vision_radius2": {
|
||||
"type": "integer",
|
||||
"description": "Squared vision distance for fog of war. Actual vision radius is sqrt(vision_radius2).",
|
||||
"minimum": 1,
|
||||
"default": 49
|
||||
},
|
||||
"attack_radius2": {
|
||||
"type": "integer",
|
||||
"description": "Squared attack distance for combat. Actual attack radius is sqrt(attack_radius2).",
|
||||
"minimum": 1,
|
||||
"default": 5
|
||||
},
|
||||
"spawn_cost": {
|
||||
"type": "integer",
|
||||
"description": "Energy cost to spawn a new bot.",
|
||||
"minimum": 1,
|
||||
"default": 3
|
||||
},
|
||||
"energy_interval": {
|
||||
"type": "integer",
|
||||
"description": "Number of turns between energy spawns at energy nodes.",
|
||||
"minimum": 1,
|
||||
"default": 10
|
||||
},
|
||||
"cores_per_player": {
|
||||
"type": "integer",
|
||||
"description": "Number of starting cores per player.",
|
||||
"minimum": 1,
|
||||
"maximum": 2,
|
||||
"default": 1
|
||||
},
|
||||
"map_id": {
|
||||
"type": "string",
|
||||
"description": "Map identifier from the map library.",
|
||||
"pattern": "^map_[a-f0-9]+$"
|
||||
},
|
||||
"season_id": {
|
||||
"type": "string",
|
||||
"description": "Season identifier if this is a seasonal match.",
|
||||
"pattern": "^s\\d+$"
|
||||
},
|
||||
"rules_version": {
|
||||
"type": "string",
|
||||
"description": "Rules version for seasonal rule variations.",
|
||||
"pattern": "^\\d+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MatchResult": {
|
||||
"type": "object",
|
||||
"description": "Final match outcome.",
|
||||
"required": [
|
||||
"winner",
|
||||
"reason",
|
||||
"turns",
|
||||
"scores",
|
||||
"energy",
|
||||
"bots_alive"
|
||||
],
|
||||
"properties": {
|
||||
"winner": {
|
||||
"type": "integer",
|
||||
"description": "Winning player ID (0-indexed), or -1 for a draw.",
|
||||
"minimum": -1
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "How the match ended.",
|
||||
"enum": [
|
||||
"elimination",
|
||||
"dominance",
|
||||
"turns",
|
||||
"draw",
|
||||
"annihilation"
|
||||
]
|
||||
},
|
||||
"turns": {
|
||||
"type": "integer",
|
||||
"description": "Number of turns played.",
|
||||
"minimum": 1
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"description": "Final score per player.",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"energy": {
|
||||
"type": "array",
|
||||
"description": "Total energy collected per player (tiebreaker stat).",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"bots_alive": {
|
||||
"type": "array",
|
||||
"description": "Number of living bots per player at match end.",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"crashed": {
|
||||
"type": "array",
|
||||
"description": "Per-player crash status. True if player's bot crashed during match.",
|
||||
"items": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReplayPlayer": {
|
||||
"type": "object",
|
||||
"description": "Player information in a replay.",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Player ID (0-indexed).",
|
||||
"minimum": 0
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Player/bot name.",
|
||||
"minLength": 1,
|
||||
"maxLength": 64
|
||||
}
|
||||
}
|
||||
},
|
||||
"Position": {
|
||||
"type": "object",
|
||||
"description": "Grid position with toroidal (wraparound) topology.",
|
||||
"required": ["row", "col"],
|
||||
"properties": {
|
||||
"row": {
|
||||
"type": "integer",
|
||||
"description": "Row coordinate (0-indexed). Wraps around grid height.",
|
||||
"minimum": 0
|
||||
},
|
||||
"col": {
|
||||
"type": "integer",
|
||||
"description": "Column coordinate (0-indexed). Wraps around grid width.",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReplayCore": {
|
||||
"type": "object",
|
||||
"description": "Core position and ownership in the map definition.",
|
||||
"required": ["position", "owner"],
|
||||
"properties": {
|
||||
"position": {
|
||||
"$ref": "#/$defs/Position"
|
||||
},
|
||||
"owner": {
|
||||
"type": "integer",
|
||||
"description": "Owning player ID (0-indexed).",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReplayMap": {
|
||||
"type": "object",
|
||||
"description": "Static map data.",
|
||||
"required": ["rows", "cols", "walls", "cores", "energy_nodes"],
|
||||
"properties": {
|
||||
"rows": {
|
||||
"type": "integer",
|
||||
"description": "Map height (should match config.rows).",
|
||||
"minimum": 30
|
||||
},
|
||||
"cols": {
|
||||
"type": "integer",
|
||||
"description": "Map width (should match config.cols).",
|
||||
"minimum": 30
|
||||
},
|
||||
"walls": {
|
||||
"type": "array",
|
||||
"description": "All wall tile positions.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Position"
|
||||
}
|
||||
},
|
||||
"cores": {
|
||||
"type": "array",
|
||||
"description": "All core positions and ownership.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ReplayCore"
|
||||
}
|
||||
},
|
||||
"energy_nodes": {
|
||||
"type": "array",
|
||||
"description": "All energy node positions (where energy spawns periodically).",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Position"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReplayBot": {
|
||||
"type": "object",
|
||||
"description": "Bot state at a specific turn.",
|
||||
"required": ["id", "owner", "position", "alive"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "Unique bot identifier within the match.",
|
||||
"minimum": 0
|
||||
},
|
||||
"owner": {
|
||||
"type": "integer",
|
||||
"description": "Owning player ID (0-indexed).",
|
||||
"minimum": 0
|
||||
},
|
||||
"position": {
|
||||
"$ref": "#/$defs/Position"
|
||||
},
|
||||
"alive": {
|
||||
"type": "boolean",
|
||||
"description": "True if bot is alive, false if dead (dead bots persist for one turn for death animation)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReplayCoreState": {
|
||||
"type": "object",
|
||||
"description": "Core state at a specific turn.",
|
||||
"required": ["position", "owner", "active"],
|
||||
"properties": {
|
||||
"position": {
|
||||
"$ref": "#/$defs/Position"
|
||||
},
|
||||
"owner": {
|
||||
"type": "integer",
|
||||
"description": "Owning player ID (0-indexed).",
|
||||
"minimum": 0
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"description": "True if core can spawn bots, false if razed (captured by enemy)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Event": {
|
||||
"type": "object",
|
||||
"description": "Game event that occurred during a turn.",
|
||||
"required": ["type", "turn", "details"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Event type.",
|
||||
"enum": [
|
||||
"bot_spawned",
|
||||
"bot_died",
|
||||
"energy_collected",
|
||||
"core_captured",
|
||||
"combat_death",
|
||||
"collision_death"
|
||||
]
|
||||
},
|
||||
"turn": {
|
||||
"type": "integer",
|
||||
"description": "Turn number when event occurred.",
|
||||
"minimum": 0
|
||||
},
|
||||
"details": {
|
||||
"description": "Event-specific details. Structure depends on event type.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Details for bot_spawned event.",
|
||||
"properties": {
|
||||
"bot_id": { "type": "integer" },
|
||||
"owner": { "type": "integer" },
|
||||
"position": { "$ref": "#/$defs/Position" }
|
||||
},
|
||||
"required": ["bot_id", "owner", "position"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Details for bot_died event.",
|
||||
"properties": {
|
||||
"bot_id": { "type": "integer" },
|
||||
"owner": { "type": "integer" },
|
||||
"position": { "$ref": "#/$defs/Position" }
|
||||
},
|
||||
"required": ["bot_id", "owner", "position"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Details for energy_collected event.",
|
||||
"properties": {
|
||||
"owner": { "type": "integer" },
|
||||
"position": { "$ref": "#/$defs/Position" }
|
||||
},
|
||||
"required": ["owner", "position"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Details for core_captured event.",
|
||||
"properties": {
|
||||
"position": { "$ref": "#/$defs/Position" },
|
||||
"previous_owner": { "type": "integer" },
|
||||
"captured_by": { "type": "integer" }
|
||||
},
|
||||
"required": ["position", "previous_owner", "captured_by"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Details for combat_death event.",
|
||||
"properties": {
|
||||
"bot_id": { "type": "integer" },
|
||||
"owner": { "type": "integer" },
|
||||
"position": { "$ref": "#/$defs/Position" },
|
||||
"killers": {
|
||||
"type": "array",
|
||||
"description": "Enemy bots that contributed to the kill (within attack radius).",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bot_id": { "type": "integer" },
|
||||
"owner": { "type": "integer" },
|
||||
"position": { "$ref": "#/$defs/Position" }
|
||||
},
|
||||
"required": ["bot_id", "owner", "position"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["bot_id", "owner", "position", "killers"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Details for collision_death event (two friendly bots moved to same tile).",
|
||||
"properties": {
|
||||
"bot_id": { "type": "integer" },
|
||||
"owner": { "type": "integer" },
|
||||
"position": { "$ref": "#/$defs/Position" }
|
||||
},
|
||||
"required": ["bot_id", "owner", "position"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"DebugTarget": {
|
||||
"type": "object",
|
||||
"description": "Debug target marker for bot telemetry visualization.",
|
||||
"required": ["position", "label", "priority"],
|
||||
"properties": {
|
||||
"position": {
|
||||
"$ref": "#/$defs/Position"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Label for the target (e.g., 'energy', 'threat').",
|
||||
"maxLength": 64
|
||||
},
|
||||
"priority": {
|
||||
"type": "number",
|
||||
"description": "Priority value for visual encoding (e.g., 0.0-1.0).",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"DebugInfo": {
|
||||
"type": "object",
|
||||
"description": "Optional bot debug telemetry (stored if bot provides debug field in move response).",
|
||||
"properties": {
|
||||
"reasoning": {
|
||||
"type": "string",
|
||||
"description": "Bot's reasoning for this turn's decisions.",
|
||||
"maxLength": 1000
|
||||
},
|
||||
"targets": {
|
||||
"type": "array",
|
||||
"description": "Target markers for visualization.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/DebugTarget"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReplayTurn": {
|
||||
"type": "object",
|
||||
"description": "Complete game state at the end of a turn.",
|
||||
"required": [
|
||||
"turn",
|
||||
"bots",
|
||||
"cores",
|
||||
"energy",
|
||||
"scores",
|
||||
"energy_held"
|
||||
],
|
||||
"properties": {
|
||||
"turn": {
|
||||
"type": "integer",
|
||||
"description": "Turn number (0-indexed; turn 0 is initial state before any moves).",
|
||||
"minimum": 0
|
||||
},
|
||||
"bots": {
|
||||
"type": "array",
|
||||
"description": "All bots in the match (including dead ones for death animation).",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ReplayBot"
|
||||
}
|
||||
},
|
||||
"cores": {
|
||||
"type": "array",
|
||||
"description": "All cores and their states.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/ReplayCoreState"
|
||||
}
|
||||
},
|
||||
"energy": {
|
||||
"type": "array",
|
||||
"description": "Positions where energy is currently available.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Position"
|
||||
}
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"description": "Current score per player.",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"energy_held": {
|
||||
"type": "array",
|
||||
"description": "Current energy held per player.",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"type": "array",
|
||||
"description": "Events that occurred this turn. Optional for turns with no events.",
|
||||
"items": {
|
||||
"$ref": "#/$defs/Event"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"type": "object",
|
||||
"description": "Per-player debug telemetry. Keys are player IDs.",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/DebugInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CriticalMoment": {
|
||||
"type": "object",
|
||||
"description": "A turn where win probability shifted significantly.",
|
||||
"required": ["turn", "delta", "player", "description"],
|
||||
"properties": {
|
||||
"turn": {
|
||||
"type": "integer",
|
||||
"description": "Turn number of the critical moment.",
|
||||
"minimum": 0
|
||||
},
|
||||
"delta": {
|
||||
"type": "number",
|
||||
"description": "Change in win probability (absolute value).",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"player": {
|
||||
"type": "integer",
|
||||
"description": "Player whose win probability changed.",
|
||||
"minimum": 0
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Human-readable description of what happened.",
|
||||
"maxLength": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue