Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/sync-catalog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Synchronize Rumble bot catalog

on:
schedule:
- cron: '23 * * * *'
workflow_dispatch:

permissions:
contents: write

jobs:
synchronize:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python scripts/sync_catalog.py --root .
- run: python scripts/aggregate.py --root .
- name: Commit synchronized catalog and projections
run: |
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add catalog.json leaderboard matchmaking clients.json site/data
git diff --cached --quiet || git commit -m 'chore: synchronize Rumble bot catalog and projections'
git push
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Create an issue titled `[result] <client-id> <UTC timestamp>`, apply the `result

Each participant supplies cataloged `name` and `version`, `isTeam`, a 1224-system `rank`, `totalScore`, `survival`, `lastSurvivorBonus`, `bulletDamage`, `bulletKillBonus`, `ramDamage`, `ramKillBonus`, `firstPlaces`, `secondPlaces`, and `thirdPlaces`. Scores and place counts are non-negative signed 32-bit integers. `isTeam`, result-entry count, rank placement, and place-count totals must match the selected ranked game type.

The submitting issue account and `clientId` must be registered. Every bot must be active in `catalog.json`; the catalog is synchronized from the reviewed Rumble bot catalog by maintainers. The drain workflow validates each record independently, keeps valid records when neighboring records are rejected, and closes every processed issue with a receipt line for every record.
The submitting issue account and `clientId` must be registered. Every bot must be active in `catalog.json`; the hourly synchronization workflow copies the reviewed Rumble bot catalog declared by that file. The drain workflow validates each record independently, keeps valid records when neighboring records are rejected, and closes every processed issue with a receipt line for every record.

## Rules

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

Run the focused suite with `python -m unittest discover -s tests -v`. It uses only Python's standard library. Run `python scripts/aggregate.py --root .` to regenerate projections from the current facts.

`catalog.json` is a local, reviewed copy of the generated Rumble bot catalog. Run `python scripts/sync_catalog.py --root .` to refresh it from its declared HTTPS source; the scheduled workflow performs the same synchronization hourly.

## Submit results

Before a client can submit ranked results, its forge account must be registered through a reviewed pull request adding `clients/<account>.json`. The client then creates an issue labelled `result-submission`, with a `[result]` title and exactly one fenced JSON batch envelope. See [CONTRIBUTING.md](CONTRIBUTING.md) for the contract and limits.
Expand Down
20 changes: 18 additions & 2 deletions catalog.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
{
"bots": [
{
"addedAt": "2026-08-02",
"authors": [
"Tank Royale Rumble maintainers"
],
"name": "Orbit",
"owner": "flemming-n-larsen",
"path": "bots/python/Orbit",
"platform": "Python",
"sourceHash": "sha256:86b63e60f231c8f736ec7dc7867c93966a59a63bb3409326516879236555cf36",
"status": "active",
"version": "1.0.2"
}
],
"schemaVersion": 1,
"source": "https://github.com/robocode-dev/rumble-bots/blob/main/bots/index.json",
"bots": []
"source": "https://raw.githubusercontent.com/robocode-dev/rumble-bots/main/bots/index.json",
"sourceCommit": "c735e6ff422e8795ffdfb0e65ec7910e9b3f943e",
"sourceGeneratedAt": "2026-08-02T15:27:36Z"
}
16 changes: 14 additions & 2 deletions leaderboard/1v1.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
{
"behaviorVersion": 1,
"entries": [],
"entries": [
{
"aps": 0.0,
"battles": 0,
"bot": "Orbit 1.0.2",
"epoch": 1,
"name": "Orbit",
"owner": "flemming-n-larsen",
"pairings": 0,
"platform": "Python",
"version": "1.0.2"
}
],
"gameType": "1v1",
"projectionId": "cd37104818ec54159fc51791c76a882498cc4f4bd35882656dc154b69a334eff",
"projectionId": "9f3e49dfac935fe42da4d43ec1cfe58290d95dc964af6eee41c79132f1682948",
"schemaVersion": 1
}
16 changes: 16 additions & 0 deletions leaderboard/bots/Orbit-1.0.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"entry": {
"aps": 0.0,
"battles": 0,
"bot": "Orbit 1.0.2",
"epoch": 1,
"name": "Orbit",
"owner": "flemming-n-larsen",
"pairings": 0,
"platform": "Python",
"version": "1.0.2"
},
"gameType": "twinduel",
"projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984",
"schemaVersion": 1
}
16 changes: 14 additions & 2 deletions leaderboard/melee.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
{
"behaviorVersion": 1,
"entries": [],
"entries": [
{
"aps": 0.0,
"battles": 0,
"bot": "Orbit 1.0.2",
"epoch": 1,
"name": "Orbit",
"owner": "flemming-n-larsen",
"pairings": 0,
"platform": "Python",
"version": "1.0.2"
}
],
"gameType": "melee",
"projectionId": "f0fd8d04f5abc28cf69e07f93fae2115fa4eab5520508b99c2c6ad9d0ae3a522",
"projectionId": "edbe2d5f2b2ef4139b78c9842f3b743f3a78220f5827781e61ef33a3f0fc522a",
"schemaVersion": 1
}
16 changes: 14 additions & 2 deletions leaderboard/twinduel.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
{
"behaviorVersion": 1,
"entries": [],
"entries": [
{
"aps": 0.0,
"battles": 0,
"bot": "Orbit 1.0.2",
"epoch": 1,
"name": "Orbit",
"owner": "flemming-n-larsen",
"pairings": 0,
"platform": "Python",
"version": "1.0.2"
}
],
"gameType": "twinduel",
"projectionId": "70c8ddb725f278462b2c4b3618704b0156fcf029133384ccd35885d5ae02602d",
"projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984",
"schemaVersion": 1
}
2 changes: 1 addition & 1 deletion matchmaking/matches_needed-1v1.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"gameType": "1v1",
"priorityPairs": [],
"projectionId": "cd37104818ec54159fc51791c76a882498cc4f4bd35882656dc154b69a334eff",
"projectionId": "9f3e49dfac935fe42da4d43ec1cfe58290d95dc964af6eee41c79132f1682948",
"schemaVersion": 1,
"targetSamplesPerPairing": 6
}
2 changes: 1 addition & 1 deletion matchmaking/matches_needed-melee.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"gameType": "melee",
"priorityPairs": [],
"projectionId": "f0fd8d04f5abc28cf69e07f93fae2115fa4eab5520508b99c2c6ad9d0ae3a522",
"projectionId": "edbe2d5f2b2ef4139b78c9842f3b743f3a78220f5827781e61ef33a3f0fc522a",
"schemaVersion": 1,
"targetSamplesPerPairing": 6
}
2 changes: 1 addition & 1 deletion matchmaking/matches_needed-twinduel.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"gameType": "twinduel",
"priorityPairs": [],
"projectionId": "70c8ddb725f278462b2c4b3618704b0156fcf029133384ccd35885d5ae02602d",
"projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984",
"schemaVersion": 1,
"targetSamplesPerPairing": 6
}
2 changes: 1 addition & 1 deletion matchmaking/pairings-1v1.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"gameType": "1v1",
"pairings": [],
"projectionId": "cd37104818ec54159fc51791c76a882498cc4f4bd35882656dc154b69a334eff",
"projectionId": "9f3e49dfac935fe42da4d43ec1cfe58290d95dc964af6eee41c79132f1682948",
"schemaVersion": 1
}
2 changes: 1 addition & 1 deletion matchmaking/pairings-melee.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"gameType": "melee",
"pairings": [],
"projectionId": "f0fd8d04f5abc28cf69e07f93fae2115fa4eab5520508b99c2c6ad9d0ae3a522",
"projectionId": "edbe2d5f2b2ef4139b78c9842f3b743f3a78220f5827781e61ef33a3f0fc522a",
"schemaVersion": 1
}
2 changes: 1 addition & 1 deletion matchmaking/pairings-twinduel.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"gameType": "twinduel",
"pairings": [],
"projectionId": "70c8ddb725f278462b2c4b3618704b0156fcf029133384ccd35885d5ae02602d",
"projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984",
"schemaVersion": 1
}
13 changes: 7 additions & 6 deletions scripts/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,14 @@ def aggregate_game_type(records: list[dict[str, Any]], catalog: list[dict[str, A
relevant = [record for record in records if record.get("gameType") == game_type and record.get("engine", {}).get("behaviorVersion") == behavior_version]
shares: dict[tuple[str, str], dict[tuple[tuple[str, str], ...], list[float]]] = defaultdict(lambda: defaultdict(list))
pairing_counts: dict[tuple[tuple[str, str], ...], int] = defaultdict(int)
pair_sample_counts: dict[tuple[tuple[str, str], tuple[str, str]], int] = defaultdict(int)
for record in relevant:
participants = record["participants"]
total = sum(float(participant["totalScore"]) for participant in participants)
bots = tuple(sorted(identity(participant) for participant in participants))
pairing_counts[bots] += 1
for pair in combinations(bots, 2):
pair_sample_counts[pair] += 1
for participant in participants:
bot = identity(participant)
if bot in eligible:
Expand All @@ -98,12 +101,10 @@ def aggregate_game_type(records: list[dict[str, Any]], catalog: list[dict[str, A
pairs = [{"bots": [f"{name} {version}" for name, version in pair], "battles": count} for pair, count in sorted(pairing_counts.items())]
pairings = {"schemaVersion": 1, "projectionId": projection_id, "gameType": game_type, "pairings": pairs}
priority_pairs = []
if game_type == "1v1":
for first, second in combinations(sorted(eligible), 2):
pair = tuple(sorted((first, second)))
count = pairing_counts.get(pair, 0)
if count < TARGET_SAMPLES_PER_PAIRING:
priority_pairs.append({"bots": [f"{name} {version}" for name, version in pair], "have": count, "reason": "new-bot" if count == 0 else "under-sampled"})
for pair in combinations(sorted(eligible), 2):
count = pair_sample_counts.get(pair, 0)
if count < TARGET_SAMPLES_PER_PAIRING:
priority_pairs.append({"bots": [f"{name} {version}" for name, version in pair], "have": count, "reason": "new-bot" if count == 0 else "under-sampled"})
needed = {"schemaVersion": 1, "projectionId": projection_id, "gameType": game_type, "targetSamplesPerPairing": TARGET_SAMPLES_PER_PAIRING, "priorityPairs": priority_pairs}
return leaderboard, pairings, needed

Expand Down
71 changes: 71 additions & 0 deletions scripts/sync_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Synchronize the published Rumble bot catalog into this repository."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Callable, Any
from urllib.request import Request, urlopen

from common import read_json, write_json

Fetch = Callable[[str], bytes]


def fetch_source(url: str) -> bytes:
"""Fetch the declared catalog source with an explicit user agent."""
request = Request(url, headers={"User-Agent": "tank-royale-rumble-data-catalog-sync"})
with urlopen(request, timeout=30) as response:
return response.read()


def source_catalog(url: str, fetch: Fetch) -> dict[str, Any]:
"""Load and minimally validate the generated Rumble bot catalog."""
try:
source = json.loads(fetch(url).decode("utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
raise ValueError(f"could not load catalog source `{url}`: {error}") from error
if not isinstance(source, dict) or source.get("schemaVersion") != 1 or not isinstance(source.get("bots"), list):
raise ValueError("catalog source has an unsupported schema")
return source


def synchronized_catalog(catalog: dict[str, Any], fetch: Fetch = fetch_source) -> dict[str, Any]:
"""Return the locally stored catalog refreshed from its declared source."""
source_url = catalog.get("source")
if not isinstance(source_url, str) or not source_url.startswith("https://"):
raise ValueError("catalog.json must declare an HTTPS source URL")
source = source_catalog(source_url, fetch)
return {
"schemaVersion": 1,
"source": source_url,
"sourceCommit": source.get("commit"),
"sourceGeneratedAt": source.get("generatedAt"),
"bots": source["bots"],
}


def sync(root: Path, fetch: Fetch = fetch_source) -> None:
"""Refresh catalog.json from its declared generated source."""
catalog_path = root / "catalog.json"
write_json(catalog_path, synchronized_catalog(read_json(catalog_path), fetch))


def main() -> int:
"""Synchronize catalog.json from the command line."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, required=True)
arguments = parser.parse_args()
try:
sync(arguments.root.resolve())
except (OSError, ValueError) as error:
print(f"catalog synchronization failed: {error}")
return 1
print("synchronized catalog")
return 0


if __name__ == "__main__":
raise SystemExit(main())
16 changes: 16 additions & 0 deletions site/data/bots/Orbit-1.0.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"entry": {
"aps": 0.0,
"battles": 0,
"bot": "Orbit 1.0.2",
"epoch": 1,
"name": "Orbit",
"owner": "flemming-n-larsen",
"pairings": 0,
"platform": "Python",
"version": "1.0.2"
},
"gameType": "twinduel",
"projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984",
"schemaVersion": 1
}
16 changes: 14 additions & 2 deletions site/data/leaderboard/1v1.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
{
"behaviorVersion": 1,
"entries": [],
"entries": [
{
"aps": 0.0,
"battles": 0,
"bot": "Orbit 1.0.2",
"epoch": 1,
"name": "Orbit",
"owner": "flemming-n-larsen",
"pairings": 0,
"platform": "Python",
"version": "1.0.2"
}
],
"gameType": "1v1",
"projectionId": "cd37104818ec54159fc51791c76a882498cc4f4bd35882656dc154b69a334eff",
"projectionId": "9f3e49dfac935fe42da4d43ec1cfe58290d95dc964af6eee41c79132f1682948",
"schemaVersion": 1
}
16 changes: 14 additions & 2 deletions site/data/leaderboard/melee.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
{
"behaviorVersion": 1,
"entries": [],
"entries": [
{
"aps": 0.0,
"battles": 0,
"bot": "Orbit 1.0.2",
"epoch": 1,
"name": "Orbit",
"owner": "flemming-n-larsen",
"pairings": 0,
"platform": "Python",
"version": "1.0.2"
}
],
"gameType": "melee",
"projectionId": "f0fd8d04f5abc28cf69e07f93fae2115fa4eab5520508b99c2c6ad9d0ae3a522",
"projectionId": "edbe2d5f2b2ef4139b78c9842f3b743f3a78220f5827781e61ef33a3f0fc522a",
"schemaVersion": 1
}
16 changes: 14 additions & 2 deletions site/data/leaderboard/twinduel.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
{
"behaviorVersion": 1,
"entries": [],
"entries": [
{
"aps": 0.0,
"battles": 0,
"bot": "Orbit 1.0.2",
"epoch": 1,
"name": "Orbit",
"owner": "flemming-n-larsen",
"pairings": 0,
"platform": "Python",
"version": "1.0.2"
}
],
"gameType": "twinduel",
"projectionId": "70c8ddb725f278462b2c4b3618704b0156fcf029133384ccd35885d5ae02602d",
"projectionId": "5b01446a5e4d7b6db3b7477213caad8e118b5f74f558f615096607a464be9984",
"schemaVersion": 1
}
Loading
Loading