diff --git a/test-replay.json b/test-replay.json new file mode 100644 index 0000000..441f1b1 --- /dev/null +++ b/test-replay.json @@ -0,0 +1,2322 @@ +{ + "format_version": "1.0", + "match_id": "m_qnswr7zb", + "config": { + "rows": 77, + "cols": 77, + "max_turns": 100, + "vision_radius2": 49, + "attack_radius2": 12, + "spawn_cost": 3, + "energy_interval": 10, + "cores_per_player": 1, + "zone_enabled": true, + "zone_start_turn": 10, + "zone_shrink_interval": 1, + "zone_shrink_step": 2, + "zone_min_radius": 1 + }, + "start_time": "2026-05-25T12:08:06.247617314Z", + "end_time": "2026-05-25T12:08:06.43219694Z", + "result": { + "winner": 2, + "reason": "elimination", + "turns": 4, + "scores": [ + 1, + 1, + 11, + 1, + 1, + 1 + ], + "energy": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "bots_alive": [ + 0, + 0, + 1, + 0, + 0, + 0 + ], + "crashed": [ + false, + false, + false, + false, + false, + false + ], + "combat_deaths": [ + 2, + 0, + 1, + 1, + 1, + 1 + ] + }, + "players": [ + { + "id": 0, + "name": "swarm" + }, + { + "id": 1, + "name": "hunter" + }, + { + "id": 2, + "name": "gatherer" + }, + { + "id": 3, + "name": "rusher" + }, + { + "id": 4, + "name": "guardian" + }, + { + "id": 5, + "name": "random" + } + ], + "map": { + "rows": 77, + "cols": 77, + "walls": [ + { + "row": 57, + "col": 36 + }, + { + "row": 51, + "col": 42 + }, + { + "row": 45, + "col": 39 + }, + { + "row": 13, + "col": 51 + }, + { + "row": 67, + "col": 40 + }, + { + "row": 34, + "col": 55 + }, + { + "row": 45, + "col": 52 + }, + { + "row": 33, + "col": 60 + }, + { + "row": 62, + "col": 29 + }, + { + "row": 38, + "col": 12 + }, + { + "row": 53, + "col": 11 + }, + { + "row": 64, + "col": 43 + }, + { + "row": 56, + "col": 35 + }, + { + "row": 50, + "col": 52 + }, + { + "row": 21, + "col": 62 + }, + { + "row": 42, + "col": 21 + }, + { + "row": 46, + "col": 25 + }, + { + "row": 21, + "col": 50 + }, + { + "row": 10, + "col": 38 + }, + { + "row": 25, + "col": 13 + }, + { + "row": 62, + "col": 32 + }, + { + "row": 25, + "col": 26 + }, + { + "row": 25, + "col": 34 + }, + { + "row": 41, + "col": 18 + }, + { + "row": 20, + "col": 41 + }, + { + "row": 38, + "col": 28 + }, + { + "row": 57, + "col": 46 + }, + { + "row": 36, + "col": 34 + }, + { + "row": 42, + "col": 38 + }, + { + "row": 45, + "col": 20 + }, + { + "row": 32, + "col": 62 + }, + { + "row": 21, + "col": 40 + }, + { + "row": 49, + "col": 69 + }, + { + "row": 5, + "col": 33 + }, + { + "row": 27, + "col": 7 + }, + { + "row": 58, + "col": 50 + }, + { + "row": 30, + "col": 62 + }, + { + "row": 30, + "col": 51 + }, + { + "row": 37, + "col": 54 + }, + { + "row": 31, + "col": 48 + }, + { + "row": 33, + "col": 31 + }, + { + "row": 51, + "col": 21 + }, + { + "row": 28, + "col": 14 + }, + { + "row": 17, + "col": 49 + }, + { + "row": 14, + "col": 23 + }, + { + "row": 19, + "col": 40 + }, + { + "row": 18, + "col": 22 + }, + { + "row": 61, + "col": 33 + }, + { + "row": 19, + "col": 37 + }, + { + "row": 55, + "col": 26 + }, + { + "row": 38, + "col": 29 + }, + { + "row": 38, + "col": 24 + }, + { + "row": 32, + "col": 55 + }, + { + "row": 21, + "col": 43 + }, + { + "row": 53, + "col": 17 + }, + { + "row": 17, + "col": 35 + }, + { + "row": 22, + "col": 56 + }, + { + "row": 29, + "col": 39 + }, + { + "row": 63, + "col": 25 + }, + { + "row": 42, + "col": 49 + }, + { + "row": 45, + "col": 33 + }, + { + "row": 48, + "col": 34 + }, + { + "row": 59, + "col": 41 + }, + { + "row": 12, + "col": 33 + }, + { + "row": 29, + "col": 34 + }, + { + "row": 46, + "col": 33 + }, + { + "row": 34, + "col": 53 + }, + { + "row": 71, + "col": 43 + }, + { + "row": 14, + "col": 33 + }, + { + "row": 51, + "col": 63 + }, + { + "row": 29, + "col": 45 + }, + { + "row": 21, + "col": 22 + }, + { + "row": 40, + "col": 25 + }, + { + "row": 35, + "col": 58 + }, + { + "row": 12, + "col": 36 + }, + { + "row": 46, + "col": 58 + }, + { + "row": 61, + "col": 25 + }, + { + "row": 62, + "col": 43 + }, + { + "row": 28, + "col": 25 + }, + { + "row": 31, + "col": 47 + }, + { + "row": 27, + "col": 53 + }, + { + "row": 40, + "col": 59 + }, + { + "row": 55, + "col": 14 + }, + { + "row": 54, + "col": 27 + }, + { + "row": 31, + "col": 37 + }, + { + "row": 49, + "col": 39 + }, + { + "row": 38, + "col": 61 + }, + { + "row": 19, + "col": 26 + }, + { + "row": 46, + "col": 37 + }, + { + "row": 39, + "col": 22 + }, + { + "row": 50, + "col": 31 + }, + { + "row": 37, + "col": 66 + }, + { + "row": 28, + "col": 44 + }, + { + "row": 41, + "col": 31 + }, + { + "row": 25, + "col": 29 + }, + { + "row": 43, + "col": 45 + }, + { + "row": 28, + "col": 38 + }, + { + "row": 46, + "col": 42 + }, + { + "row": 7, + "col": 38 + }, + { + "row": 65, + "col": 48 + }, + { + "row": 20, + "col": 58 + }, + { + "row": 55, + "col": 54 + }, + { + "row": 51, + "col": 33 + }, + { + "row": 35, + "col": 63 + }, + { + "row": 58, + "col": 27 + }, + { + "row": 39, + "col": 57 + }, + { + "row": 22, + "col": 49 + }, + { + "row": 30, + "col": 18 + }, + { + "row": 47, + "col": 31 + }, + { + "row": 49, + "col": 38 + }, + { + "row": 25, + "col": 43 + }, + { + "row": 38, + "col": 62 + }, + { + "row": 33, + "col": 46 + }, + { + "row": 19, + "col": 30 + }, + { + "row": 56, + "col": 18 + }, + { + "row": 23, + "col": 41 + }, + { + "row": 28, + "col": 47 + }, + { + "row": 36, + "col": 51 + }, + { + "row": 43, + "col": 30 + }, + { + "row": 39, + "col": 27 + }, + { + "row": 24, + "col": 44 + }, + { + "row": 17, + "col": 63 + }, + { + "row": 42, + "col": 67 + }, + { + "row": 51, + "col": 32 + }, + { + "row": 43, + "col": 48 + }, + { + "row": 46, + "col": 17 + }, + { + "row": 49, + "col": 43 + }, + { + "row": 33, + "col": 28 + }, + { + "row": 45, + "col": 29 + }, + { + "row": 26, + "col": 24 + }, + { + "row": 47, + "col": 42 + }, + { + "row": 25, + "col": 55 + }, + { + "row": 40, + "col": 49 + }, + { + "row": 38, + "col": 14 + }, + { + "row": 69, + "col": 38 + }, + { + "row": 16, + "col": 44 + }, + { + "row": 45, + "col": 28 + }, + { + "row": 23, + "col": 59 + }, + { + "row": 37, + "col": 19 + }, + { + "row": 27, + "col": 35 + }, + { + "row": 44, + "col": 29 + }, + { + "row": 54, + "col": 20 + }, + { + "row": 28, + "col": 42 + }, + { + "row": 31, + "col": 56 + }, + { + "row": 54, + "col": 38 + }, + { + "row": 49, + "col": 23 + }, + { + "row": 18, + "col": 26 + }, + { + "row": 44, + "col": 21 + }, + { + "row": 38, + "col": 26 + }, + { + "row": 30, + "col": 39 + }, + { + "row": 36, + "col": 31 + }, + { + "row": 21, + "col": 19 + }, + { + "row": 9, + "col": 36 + }, + { + "row": 26, + "col": 45 + }, + { + "row": 28, + "col": 30 + }, + { + "row": 45, + "col": 61 + }, + { + "row": 55, + "col": 57 + }, + { + "row": 63, + "col": 33 + }, + { + "row": 38, + "col": 52 + }, + { + "row": 25, + "col": 44 + }, + { + "row": 21, + "col": 30 + }, + { + "row": 43, + "col": 33 + }, + { + "row": 50, + "col": 45 + }, + { + "row": 29, + "col": 32 + }, + { + "row": 14, + "col": 44 + }, + { + "row": 51, + "col": 50 + }, + { + "row": 49, + "col": 41 + }, + { + "row": 27, + "col": 38 + }, + { + "row": 54, + "col": 55 + }, + { + "row": 23, + "col": 65 + }, + { + "row": 15, + "col": 56 + }, + { + "row": 40, + "col": 42 + }, + { + "row": 27, + "col": 23 + }, + { + "row": 64, + "col": 40 + }, + { + "row": 57, + "col": 50 + }, + { + "row": 52, + "col": 32 + }, + { + "row": 46, + "col": 14 + }, + { + "row": 38, + "col": 50 + }, + { + "row": 55, + "col": 56 + }, + { + "row": 34, + "col": 9 + }, + { + "row": 13, + "col": 43 + }, + { + "row": 49, + "col": 53 + }, + { + "row": 21, + "col": 20 + }, + { + "row": 42, + "col": 23 + }, + { + "row": 33, + "col": 43 + }, + { + "row": 58, + "col": 54 + }, + { + "row": 61, + "col": 51 + }, + { + "row": 38, + "col": 47 + }, + { + "row": 18, + "col": 49 + }, + { + "row": 44, + "col": 14 + }, + { + "row": 35, + "col": 46 + }, + { + "row": 66, + "col": 38 + }, + { + "row": 48, + "col": 32 + }, + { + "row": 38, + "col": 64 + }, + { + "row": 51, + "col": 39 + }, + { + "row": 41, + "col": 13 + }, + { + "row": 41, + "col": 30 + }, + { + "row": 42, + "col": 47 + }, + { + "row": 26, + "col": 11 + }, + { + "row": 61, + "col": 20 + }, + { + "row": 34, + "col": 38 + }, + { + "row": 30, + "col": 43 + }, + { + "row": 36, + "col": 25 + }, + { + "row": 61, + "col": 35 + }, + { + "row": 48, + "col": 38 + }, + { + "row": 34, + "col": 29 + }, + { + "row": 11, + "col": 28 + }, + { + "row": 32, + "col": 61 + }, + { + "row": 36, + "col": 27 + }, + { + "row": 60, + "col": 32 + }, + { + "row": 48, + "col": 29 + }, + { + "row": 31, + "col": 43 + }, + { + "row": 62, + "col": 53 + }, + { + "row": 33, + "col": 27 + }, + { + "row": 46, + "col": 63 + }, + { + "row": 38, + "col": 48 + }, + { + "row": 37, + "col": 49 + }, + { + "row": 33, + "col": 32 + }, + { + "row": 53, + "col": 51 + }, + { + "row": 57, + "col": 32 + }, + { + "row": 22, + "col": 21 + }, + { + "row": 15, + "col": 41 + }, + { + "row": 31, + "col": 15 + }, + { + "row": 43, + "col": 49 + }, + { + "row": 15, + "col": 43 + }, + { + "row": 47, + "col": 37 + }, + { + "row": 59, + "col": 13 + }, + { + "row": 22, + "col": 11 + }, + { + "row": 39, + "col": 10 + }, + { + "row": 27, + "col": 33 + }, + { + "row": 53, + "col": 35 + }, + { + "row": 30, + "col": 21 + }, + { + "row": 36, + "col": 17 + }, + { + "row": 54, + "col": 65 + }, + { + "row": 48, + "col": 51 + }, + { + "row": 32, + "col": 54 + }, + { + "row": 19, + "col": 44 + }, + { + "row": 52, + "col": 56 + }, + { + "row": 24, + "col": 20 + }, + { + "row": 27, + "col": 37 + }, + { + "row": 59, + "col": 27 + }, + { + "row": 30, + "col": 13 + }, + { + "row": 50, + "col": 65 + }, + { + "row": 27, + "col": 27 + }, + { + "row": 44, + "col": 15 + }, + { + "row": 34, + "col": 27 + }, + { + "row": 15, + "col": 51 + }, + { + "row": 49, + "col": 49 + }, + { + "row": 44, + "col": 22 + }, + { + "row": 40, + "col": 51 + }, + { + "row": 46, + "col": 55 + }, + { + "row": 32, + "col": 47 + }, + { + "row": 43, + "col": 44 + }, + { + "row": 35, + "col": 45 + }, + { + "row": 57, + "col": 39 + }, + { + "row": 25, + "col": 37 + }, + { + "row": 55, + "col": 36 + }, + { + "row": 43, + "col": 16 + }, + { + "row": 40, + "col": 45 + }, + { + "row": 48, + "col": 46 + }, + { + "row": 38, + "col": 15 + }, + { + "row": 55, + "col": 46 + }, + { + "row": 55, + "col": 33 + }, + { + "row": 51, + "col": 47 + }, + { + "row": 31, + "col": 24 + }, + { + "row": 23, + "col": 62 + }, + { + "row": 26, + "col": 31 + }, + { + "row": 47, + "col": 44 + }, + { + "row": 14, + "col": 47 + }, + { + "row": 30, + "col": 34 + }, + { + "row": 53, + "col": 14 + }, + { + "row": 22, + "col": 38 + }, + { + "row": 23, + "col": 25 + }, + { + "row": 15, + "col": 25 + }, + { + "row": 48, + "col": 62 + }, + { + "row": 30, + "col": 59 + } + ], + "cores": [ + { + "position": { + "row": 41, + "col": 38 + }, + "owner": 0 + }, + { + "position": { + "row": 39, + "col": 41 + }, + "owner": 1 + }, + { + "position": { + "row": 37, + "col": 41 + }, + "owner": 2 + }, + { + "position": { + "row": 35, + "col": 38 + }, + "owner": 3 + }, + { + "position": { + "row": 37, + "col": 35 + }, + "owner": 4 + }, + { + "position": { + "row": 39, + "col": 35 + }, + "owner": 5 + } + ], + "energy_nodes": [ + { + "row": 41, + "col": 42 + }, + { + "row": 36, + "col": 42 + }, + { + "row": 33, + "col": 38 + }, + { + "row": 35, + "col": 34 + }, + { + "row": 40, + "col": 34 + }, + { + "row": 43, + "col": 38 + }, + { + "row": 45, + "col": 41 + }, + { + "row": 38, + "col": 46 + }, + { + "row": 31, + "col": 42 + }, + { + "row": 31, + "col": 35 + }, + { + "row": 38, + "col": 30 + }, + { + "row": 45, + "col": 34 + }, + { + "row": 47, + "col": 50 + }, + { + "row": 33, + "col": 51 + }, + { + "row": 23, + "col": 39 + }, + { + "row": 29, + "col": 26 + }, + { + "row": 43, + "col": 25 + }, + { + "row": 53, + "col": 37 + }, + { + "row": 51, + "col": 38 + }, + { + "row": 43, + "col": 50 + }, + { + "row": 31, + "col": 49 + }, + { + "row": 25, + "col": 38 + }, + { + "row": 33, + "col": 26 + }, + { + "row": 45, + "col": 27 + }, + { + "row": 52, + "col": 53 + }, + { + "row": 32, + "col": 58 + }, + { + "row": 18, + "col": 42 + }, + { + "row": 24, + "col": 23 + }, + { + "row": 44, + "col": 18 + }, + { + "row": 58, + "col": 34 + }, + { + "row": 52, + "col": 53 + }, + { + "row": 32, + "col": 58 + }, + { + "row": 18, + "col": 42 + }, + { + "row": 24, + "col": 23 + }, + { + "row": 44, + "col": 18 + }, + { + "row": 58, + "col": 34 + } + ] + }, + "turns": [ + { + "turn": 0, + "bots": [ + { + "id": 0, + "owner": 0, + "position": { + "row": 41, + "col": 38 + }, + "alive": true + }, + { + "id": 1, + "owner": 1, + "position": { + "row": 39, + "col": 41 + }, + "alive": true + }, + { + "id": 2, + "owner": 2, + "position": { + "row": 37, + "col": 41 + }, + "alive": true + }, + { + "id": 3, + "owner": 3, + "position": { + "row": 35, + "col": 38 + }, + "alive": true + }, + { + "id": 4, + "owner": 4, + "position": { + "row": 37, + "col": 35 + }, + "alive": true + }, + { + "id": 5, + "owner": 5, + "position": { + "row": 39, + "col": 35 + }, + "alive": true + } + ], + "cores": [ + { + "position": { + "row": 41, + "col": 38 + }, + "owner": 0, + "active": true + }, + { + "position": { + "row": 39, + "col": 41 + }, + "owner": 1, + "active": true + }, + { + "position": { + "row": 37, + "col": 41 + }, + "owner": 2, + "active": true + }, + { + "position": { + "row": 35, + "col": 38 + }, + "owner": 3, + "active": true + }, + { + "position": { + "row": 37, + "col": 35 + }, + "owner": 4, + "active": true + }, + { + "position": { + "row": 39, + "col": 35 + }, + "owner": 5, + "active": true + } + ], + "energy": [], + "scores": [ + 1, + 1, + 1, + 1, + 1, + 1 + ], + "energy_held": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "events": [ + { + "type": "bot_spawned", + "turn": 0, + "details": { + "bot_id": 0, + "owner": 0, + "pos": { + "row": 41, + "col": 38 + } + } + }, + { + "type": "bot_spawned", + "turn": 0, + "details": { + "bot_id": 1, + "owner": 1, + "pos": { + "row": 39, + "col": 41 + } + } + }, + { + "type": "bot_spawned", + "turn": 0, + "details": { + "bot_id": 2, + "owner": 2, + "pos": { + "row": 37, + "col": 41 + } + } + }, + { + "type": "bot_spawned", + "turn": 0, + "details": { + "bot_id": 3, + "owner": 3, + "pos": { + "row": 35, + "col": 38 + } + } + }, + { + "type": "bot_spawned", + "turn": 0, + "details": { + "bot_id": 4, + "owner": 4, + "pos": { + "row": 37, + "col": 35 + } + } + }, + { + "type": "bot_spawned", + "turn": 0, + "details": { + "bot_id": 5, + "owner": 5, + "pos": { + "row": 39, + "col": 35 + } + } + } + ], + "zone_bounds": { + "center": { + "row": 38, + "col": 38 + }, + "radius": 38, + "active": false + } + }, + { + "turn": 1, + "bots": [ + { + "id": 0, + "owner": 0, + "position": { + "row": 41, + "col": 39 + }, + "alive": true + }, + { + "id": 1, + "owner": 1, + "position": { + "row": 39, + "col": 40 + }, + "alive": false + }, + { + "id": 2, + "owner": 2, + "position": { + "row": 37, + "col": 42 + }, + "alive": true + }, + { + "id": 3, + "owner": 3, + "position": { + "row": 35, + "col": 39 + }, + "alive": true + }, + { + "id": 4, + "owner": 4, + "position": { + "row": 38, + "col": 35 + }, + "alive": false + }, + { + "id": 5, + "owner": 5, + "position": { + "row": 38, + "col": 35 + }, + "alive": false + } + ], + "cores": [ + { + "position": { + "row": 41, + "col": 38 + }, + "owner": 0, + "active": true + }, + { + "position": { + "row": 39, + "col": 41 + }, + "owner": 1, + "active": true + }, + { + "position": { + "row": 37, + "col": 41 + }, + "owner": 2, + "active": true + }, + { + "position": { + "row": 35, + "col": 38 + }, + "owner": 3, + "active": true + }, + { + "position": { + "row": 37, + "col": 35 + }, + "owner": 4, + "active": true + }, + { + "position": { + "row": 39, + "col": 35 + }, + "owner": 5, + "active": true + } + ], + "energy": [], + "scores": [ + 1, + 1, + 1, + 1, + 1, + 1 + ], + "energy_held": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "events": [ + { + "type": "combat_death", + "turn": 1, + "details": { + "bot_id": 1, + "killers": [ + { + "bot_id": 0, + "owner": 0, + "position": { + "row": 41, + "col": 39 + } + }, + { + "bot_id": 2, + "owner": 2, + "position": { + "row": 37, + "col": 42 + } + } + ], + "owner": 1, + "position": { + "row": 39, + "col": 40 + } + } + }, + { + "type": "combat_death", + "turn": 1, + "details": { + "bot_id": 4, + "killers": [ + { + "bot_id": 5, + "owner": 5, + "position": { + "row": 38, + "col": 35 + } + } + ], + "owner": 4, + "position": { + "row": 38, + "col": 35 + } + } + }, + { + "type": "combat_death", + "turn": 1, + "details": { + "bot_id": 5, + "killers": [ + { + "bot_id": 4, + "owner": 4, + "position": { + "row": 38, + "col": 35 + } + } + ], + "owner": 5, + "position": { + "row": 38, + "col": 35 + } + } + } + ], + "zone_bounds": { + "center": { + "row": 38, + "col": 38 + }, + "radius": 38, + "active": false + } + }, + { + "turn": 2, + "bots": [ + { + "id": 0, + "owner": 0, + "position": { + "row": 40, + "col": 39 + }, + "alive": true + }, + { + "id": 1, + "owner": 1, + "position": { + "row": 39, + "col": 40 + }, + "alive": false + }, + { + "id": 2, + "owner": 2, + "position": { + "row": 37, + "col": 43 + }, + "alive": true + }, + { + "id": 3, + "owner": 3, + "position": { + "row": 35, + "col": 40 + }, + "alive": true + }, + { + "id": 4, + "owner": 4, + "position": { + "row": 38, + "col": 35 + }, + "alive": false + }, + { + "id": 5, + "owner": 5, + "position": { + "row": 38, + "col": 35 + }, + "alive": false + } + ], + "cores": [ + { + "position": { + "row": 41, + "col": 38 + }, + "owner": 0, + "active": true + }, + { + "position": { + "row": 39, + "col": 41 + }, + "owner": 1, + "active": true + }, + { + "position": { + "row": 37, + "col": 41 + }, + "owner": 2, + "active": true + }, + { + "position": { + "row": 35, + "col": 38 + }, + "owner": 3, + "active": true + }, + { + "position": { + "row": 37, + "col": 35 + }, + "owner": 4, + "active": true + }, + { + "position": { + "row": 39, + "col": 35 + }, + "owner": 5, + "active": true + } + ], + "energy": [], + "scores": [ + 1, + 1, + 1, + 1, + 1, + 1 + ], + "energy_held": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "zone_bounds": { + "center": { + "row": 38, + "col": 38 + }, + "radius": 38, + "active": false + } + }, + { + "turn": 3, + "bots": [ + { + "id": 0, + "owner": 0, + "position": { + "row": 39, + "col": 39 + }, + "alive": true + }, + { + "id": 1, + "owner": 1, + "position": { + "row": 39, + "col": 40 + }, + "alive": false + }, + { + "id": 2, + "owner": 2, + "position": { + "row": 37, + "col": 44 + }, + "alive": true + }, + { + "id": 3, + "owner": 3, + "position": { + "row": 35, + "col": 41 + }, + "alive": true + }, + { + "id": 4, + "owner": 4, + "position": { + "row": 38, + "col": 35 + }, + "alive": false + }, + { + "id": 5, + "owner": 5, + "position": { + "row": 38, + "col": 35 + }, + "alive": false + } + ], + "cores": [ + { + "position": { + "row": 41, + "col": 38 + }, + "owner": 0, + "active": true + }, + { + "position": { + "row": 39, + "col": 41 + }, + "owner": 1, + "active": true + }, + { + "position": { + "row": 37, + "col": 41 + }, + "owner": 2, + "active": true + }, + { + "position": { + "row": 35, + "col": 38 + }, + "owner": 3, + "active": true + }, + { + "position": { + "row": 37, + "col": 35 + }, + "owner": 4, + "active": true + }, + { + "position": { + "row": 39, + "col": 35 + }, + "owner": 5, + "active": true + } + ], + "energy": [], + "scores": [ + 1, + 1, + 1, + 1, + 1, + 1 + ], + "energy_held": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "zone_bounds": { + "center": { + "row": 38, + "col": 38 + }, + "radius": 38, + "active": false + } + }, + { + "turn": 4, + "bots": [ + { + "id": 0, + "owner": 0, + "position": { + "row": 39, + "col": 40 + }, + "alive": false + }, + { + "id": 1, + "owner": 1, + "position": { + "row": 39, + "col": 40 + }, + "alive": false + }, + { + "id": 2, + "owner": 2, + "position": { + "row": 37, + "col": 45 + }, + "alive": true + }, + { + "id": 3, + "owner": 3, + "position": { + "row": 36, + "col": 41 + }, + "alive": false + }, + { + "id": 4, + "owner": 4, + "position": { + "row": 38, + "col": 35 + }, + "alive": false + }, + { + "id": 5, + "owner": 5, + "position": { + "row": 38, + "col": 35 + }, + "alive": false + } + ], + "cores": [ + { + "position": { + "row": 41, + "col": 38 + }, + "owner": 0, + "active": true + }, + { + "position": { + "row": 39, + "col": 41 + }, + "owner": 1, + "active": true + }, + { + "position": { + "row": 37, + "col": 41 + }, + "owner": 2, + "active": true + }, + { + "position": { + "row": 35, + "col": 38 + }, + "owner": 3, + "active": true + }, + { + "position": { + "row": 37, + "col": 35 + }, + "owner": 4, + "active": true + }, + { + "position": { + "row": 39, + "col": 35 + }, + "owner": 5, + "active": true + } + ], + "energy": [], + "scores": [ + 1, + 1, + 11, + 1, + 1, + 1 + ], + "energy_held": [ + 0, + 0, + 0, + 0, + 0, + 0 + ], + "events": [ + { + "type": "combat_death", + "turn": 4, + "details": { + "bot_id": 0, + "killers": [ + { + "bot_id": 3, + "owner": 3, + "position": { + "row": 36, + "col": 41 + } + } + ], + "owner": 0, + "position": { + "row": 39, + "col": 40 + } + } + }, + { + "type": "combat_death", + "turn": 4, + "details": { + "bot_id": 3, + "killers": [ + { + "bot_id": 0, + "owner": 0, + "position": { + "row": 39, + "col": 40 + } + } + ], + "owner": 3, + "position": { + "row": 36, + "col": 41 + } + } + } + ], + "zone_bounds": { + "center": { + "row": 38, + "col": 38 + }, + "radius": 38, + "active": false + } + } + ], + "win_prob": [ + [ + 0.22, + 0.05, + 0.01, + 0.24, + 0.02, + 0.06 + ], + [ + 0.75, + 0, + 0.07, + 0.17, + 0, + 0 + ], + [ + 0.68, + 0, + 0.14, + 0.14, + 0, + 0 + ], + [ + 0.67, + 0, + 0.21, + 0.04, + 0, + 0 + ], + [ + 0, + 0, + 1, + 0, + 0, + 0 + ] + ], + "critical_moments": [ + { + "turn": 1, + "delta": 0.53, + "player": 0, + "description": "Player 0 win probability rises to 53%" + }, + { + "turn": 4, + "delta": -0.67, + "player": 0, + "description": "Player 0 win probability drops to 67%" + }, + { + "turn": 4, + "delta": 0.79, + "player": 2, + "description": "Player 2 win probability rises to 79%" + } + ] +} \ No newline at end of file diff --git a/web/src/app.ts b/web/src/app.ts index 8e70d81..22742eb 100644 --- a/web/src/app.ts +++ b/web/src/app.ts @@ -36,7 +36,7 @@ import { function getSkeletonHtml(path: string): string { if (path === '/leaderboard' || path === '/bots') return skeletonLeaderboard(); if (path.startsWith('/bot/') || path.startsWith('/compete/bot/')) return skeletonBotProfile(); - if (path.startsWith('/watch/replay') || path.startsWith('/replay/')) return skeletonReplay(); + if (path.startsWith('/watch/replay') || path.startsWith('/replay/') || path.startsWith('/embed/')) return skeletonReplay(); if (path.startsWith('/watch/playlists')) return skeletonPlaylists(); if (path === '/watch/replays' || path === '/matches') return skeletonMatches(); if (path === '/evolution') return skeletonEvolution(); @@ -90,6 +90,8 @@ const loadFeedbackPage = () => import('./pages/feedback').then(async m => { const loadDocsApiPage = () => import('./pages/docs-api').then(m => m.renderDocsApiPage); // Rivalries page (pre-computed from index builder §13.5) const loadRivalriesPage = () => import('./pages/rivalries').then(m => m.renderRivalriesPage); +// Embed page (minimal replay viewer for iframe embedding §13.4) +const loadEmbedPage = () => import('./pages/embed').then(m => m.renderEmbedPage); // 404 const loadNotFoundPage = () => import('./pages/not-found').then(m => m.renderNotFoundPage); @@ -279,6 +281,7 @@ router .on('/feedback', lazyRoute(loadFeedbackPage)) .on('/compete/feedback', lazyRoute(loadFeedbackPage)) .on('/compete/docs/api', lazyRoute(loadDocsApiPage)) + .on('/embed/:id', lazyRoute(loadEmbedPage)) .notFound(lazyRoute(loadNotFoundPage)); // ─── Initialization ──────────────────────────────────────────────────────────────── diff --git a/web/src/pages/embed.ts b/web/src/pages/embed.ts new file mode 100644 index 0000000..6a59d01 --- /dev/null +++ b/web/src/pages/embed.ts @@ -0,0 +1,123 @@ +// Embeddable replay viewer - minimal chrome for iframe embedding +// §13.4: /embed/{id} - auto-play, ~50KB, Open Graph tags + +const loadReplayViewer = () => import('../replay-viewer'); + +export function renderEmbedPage(params: Record): void { + const app = document.getElementById('app'); + if (!app) return; + + // Parse query params + const startTurn = params.start ? parseInt(params.start, 10) : 0; + const speed = params.speed ? parseInt(params.speed, 10) : 4; // Default 4x speed + const mode = (params.mode as 'dots' | 'voronoi' | 'influence' | 'standard') || 'dots'; + + // Minimal embed layout - no chrome, just the canvas + app.innerHTML = ` +
+
+ +
Loading replay…
+ +
+ + + T: 0/0 +
+
+
+ `; + + // Show controls on hover + const wrapper = app.querySelector('.canvas-wrapper') as HTMLElement; + const controls = app.querySelector('.embed-controls') as HTMLElement; + if (wrapper && controls) { + wrapper.addEventListener('mouseenter', () => controls.style.opacity = '1'); + wrapper.addEventListener('mouseleave', () => controls.style.opacity = '0'); + } + + loadReplayViewer().then(({ ReplayViewer }) => { + const replayUrl = params.id ? `/r2/replays/${params.id}.json.gz` : undefined; + if (!replayUrl) { + const noReplay = document.getElementById('no-replay'); + if (noReplay) noReplay.textContent = 'No replay specified'; + return; + } + + // Initialize viewer + const canvas = document.getElementById('replay-canvas') as HTMLCanvasElement; + if (!canvas) return; + + const viewer = new ReplayViewer(canvas, { + cellSize: 16, + showGrid: true, + fogOfWarPlayer: null, + animationSpeed: 100 / speed, // Convert speed multiplier to ms per turn + viewMode: mode, + showDebug: false, + }); + + // Track play state for button toggle + let isPlaying = false; + + // Hook into play state changes + viewer.onPlayStateChange = (playing: boolean) => { + isPlaying = playing; + const playBtn = document.getElementById('play-btn') as HTMLButtonElement; + if (playBtn) { + playBtn.textContent = playing ? '⏸' : '▶'; + } + }; + + // Load replay + fetch(replayUrl) + .then(resp => { + if (!resp.ok) throw new Error(`Failed to load replay: ${resp.status}`); + return resp.json(); + }) + .then((replay: any) => { + viewer.loadReplay(replay); + viewer.setTurn(startTurn); + viewer.play(); + + // Update controls + const resetBtn = document.getElementById('reset-btn') as HTMLButtonElement; + const playBtn = document.getElementById('play-btn') as HTMLButtonElement; + const turnInfo = document.getElementById('turn-info') as HTMLSpanElement; + + if (resetBtn) { + resetBtn.disabled = false; + resetBtn.onclick = () => { + viewer.setTurn(0); + if (!isPlaying) viewer.play(); + updateTurnInfo(); + }; + } + + if (playBtn) { + playBtn.disabled = false; + playBtn.onclick = () => { + if (isPlaying) { + // No pause method available - just stop by setting turn + viewer.setTurn(viewer.getTurn()); + } else { + viewer.play(); + } + }; + } + + function updateTurnInfo() { + if (turnInfo) { + turnInfo.textContent = `T: ${viewer.getTurn()}/${viewer.getTotalTurns()}`; + } + } + + // Update turn info periodically + setInterval(updateTurnInfo, 200); + }) + .catch(err => { + const noReplay = document.getElementById('no-replay'); + if (noReplay) noReplay.textContent = `Failed to load replay: ${err.message}`; + }); + }); +}