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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Run the focused suite with `python -m unittest discover -s tests -v`. It uses on

`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.

`engine.json.clientImage` is optional installation guidance for the recommended Rumble Client Docker distribution. Once an eligible Tank Royale release and client image exist, it contains an immutable `ghcr.io/...@sha256:...` reference; ranked compatibility continues to use `behaviorVersion`.

## 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
16 changes: 14 additions & 2 deletions scripts/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import argparse
import re
import uuid
from dataclasses import dataclass
from datetime import datetime
Expand All @@ -29,6 +30,7 @@
"thirdPlaces",
)
PLACE_FIELDS = ("firstPlaces", "secondPlaces", "thirdPlaces")
CLIENT_IMAGE = re.compile(r"ghcr\.io/[a-z0-9._/-]+@sha256:[0-9a-f]{64}")


class ValidationError(ValueError):
Expand Down Expand Up @@ -90,9 +92,19 @@ def active_bots(root: Path) -> dict[tuple[str, str], dict[str, Any]]:
return {(str(bot.get("name")), str(bot.get("version"))): bot for bot in bots if bot.get("status") == "active"}


def engine_pin(root: Path) -> dict[str, Any]:
"""Return the engine pin after validating its additive client image guidance."""
engine = read_json(root / "engine.json")
require(engine.get("schemaVersion") == SCHEMA_VERSION, "engine.json has an unsupported schemaVersion")
client_image = engine.get("clientImage")
require(client_image is None or (isinstance(client_image, str) and CLIENT_IMAGE.fullmatch(client_image) is not None),
"engine.json.clientImage must be an immutable GHCR SHA-256 reference")
return engine


def game_settings(root: Path, game_type: str) -> tuple[dict[str, Any], int]:
"""Return validated V1 settings and the number of bots represented by each result entry."""
engine = read_json(root / "engine.json")
engine = engine_pin(root)
games = engine.get("gameTypes")
require(isinstance(games, dict) and game_type in games, f"unsupported gameType `{game_type}`")
settings = games[game_type]
Expand Down Expand Up @@ -143,7 +155,7 @@ def validate_result(root: Path, record: Any, *, account: str, client_ids: set[st
engine = record["engine"]
require(isinstance(engine, dict), "engine must be an object")
behavior_version = require_int32(engine.get("behaviorVersion"), "engine.behaviorVersion must be a positive signed 32-bit integer", minimum=1)
configured_engine = read_json(root / "engine.json")
configured_engine = engine_pin(root)
require(behavior_version == configured_engine.get("behaviorVersion"), "engine.behaviorVersion does not match engine.json")

game_type = require_string(record["gameType"], "gameType must be a non-empty string")
Expand Down
16 changes: 16 additions & 0 deletions tests/test_rumble_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from compact import compact
from ingest import ingest
from sync_catalog import synchronized_catalog
from validate import ValidationError, engine_pin


class RumbleDataTests(unittest.TestCase):
Expand Down Expand Up @@ -64,6 +65,21 @@ def testRDA001_IntegrationPositive_valid_batch_becomes_immutable_fact_and_projec
needed = json.loads((self.root / "matchmaking/matches_needed-1v1.json").read_text(encoding="utf-8"))
self.assertIn(["Alpha 1.0", "Charlie 1.0"], [pair["bots"] for pair in needed["priorityPairs"]])

def testUnitPositive_engine_pin_accepts_optional_immutable_client_image(self) -> None:
configured = json.loads((self.root / "engine.json").read_text(encoding="utf-8"))
configured["clientImage"] = "ghcr.io/robocode-dev/rumble-client@sha256:" + "a" * 64
self.write("engine.json", configured)

self.assertEqual(configured["clientImage"], engine_pin(self.root)["clientImage"])

def testUnitNegative_engine_pin_rejects_mutable_client_image_tag(self) -> None:
configured = json.loads((self.root / "engine.json").read_text(encoding="utf-8"))
configured["clientImage"] = "ghcr.io/robocode-dev/rumble-client:latest"
self.write("engine.json", configured)

with self.assertRaisesRegex(ValidationError, "immutable GHCR"):
engine_pin(self.root)

def testRDA001_IntegrationPositive_valid_records_survive_invalid_batch_neighbors(self) -> None:
envelope = self.envelope()
invalid = self.envelope(battle_id="a290f1ee-6c54-4b01-90e6-d701748f0851")["results"][0]
Expand Down
Loading