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
4 changes: 3 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE/bot-submission.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
## Bot submission

- [ ] I added source only under `bots/<platform>/<BotName>/`.
- [ ] I included `<BotName>.json`, `<BotName>.sh`, and `<BotName>.cmd`.
- [ ] For an individual bot, I included `<BotName>.json`, `<BotName>.sh`, `<BotName>.cmd`, and source, with the `.sh` committed as executable (`100755`).
- [ ] For a TwinDuel team, the directory contains only `<TeamName>.json`, with exactly two active `name version` member identities and no launchers or source.
- [ ] The config declares an allowed SPDX `license` and the source uses an official Tank Royale Bot API.
- [ ] I ran `python scripts/validate_bot.py --root . --owner <my-forge-account> --smoke` successfully.
- [ ] For a source or member change to an existing entry, I increased its version.
- [ ] I certify I can publish this source under the declared license.
7 changes: 7 additions & 0 deletions .github/workflows/publish-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ jobs:
with:
distribution: temurin
java-version: '25'
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- uses: actions/setup-node@v4
with:
node-version: '24'
- run: python -m pip install -r templates/python/StarterBot/requirements.txt
- run: python scripts/validate_bot.py --root . --owner '${{ github.event.pull_request.user.login }}' --smoke --generate
- run: |
git config user.name 'github-actions[bot]'
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
paths:
- bots/**
- scripts/**
- templates/**
- tests/**
- .github/workflows/validate.yml

permissions:
Expand All @@ -13,6 +15,10 @@ permissions:
jobs:
validate:
runs-on: ubuntu-latest
env:
RUMBLE_TEMPLATE_SMOKE: '1'
DOTNET_NOLOGO: '1'
DOTNET_CLI_TELEMETRY_OPTOUT: '1'
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -25,6 +31,13 @@ jobs:
with:
distribution: temurin
java-version: '25'
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- uses: actions/setup-node@v4
with:
node-version: '24'
- run: python -m pip install -r templates/python/StarterBot/requirements.txt
- run: python -m unittest discover -s tests -v
- name: Reject hand-edited generated catalog files
run: |
Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Contributing a ranked bot

Submit source code only. Bots must use an official Tank Royale Bot API for Java, C#, Python, or TypeScript and run through the checked-in booter-convention scripts. Do not submit binary artifacts, generated dependency folders, custom protocol clients, process launchers, raw sockets, or code that writes outside its temporary directory.
Submit source code only. Bots must use an official Tank Royale Bot API for Java, C#, Python, or TypeScript and run through the checked-in booter-convention scripts. Dependencies are limited to the official API, its declared runtime dependencies and the platform's standard library. TypeScript bots explicitly declare the API's required `ws` runtime peer and commit their `package-lock.json`; unrelated bot libraries remain disallowed. Do not submit binary artifacts, generated dependency folders, custom protocol clients, process launchers, raw sockets, or code that writes outside its temporary directory.

Each bot directory's `license` field is its license grant for the complete directory. By opening a pull request, you certify that you have the right to publish the submitted code under that SPDX license, in the spirit of the Developer Certificate of Origin. Include a full license file if it helps users, but it is optional.

The first merged pull request for a bot name reserves that name for its submitting forge account. Only that account, or an account later registered in `bots/owners.json` by an already registered account, may submit later versions. A source change requires a version bump; only the latest version remains active. Each owner has five active entries by default, including a TwinDuel team.

Use `python scripts/validate_bot.py --root . --owner <your-forge-account> --smoke` before opening a pull request. CI is authoritative. A green check does not replace moderator review, especially for a first submission.

Start from the [runnable templates and copy-and-rename instructions](templates/README.md). They include pinned dependencies, both launchers and the required `RUMBLE_SMOKE_READY <BotName>` branch. The validator requires Python 3.11+ and `sh`; Windows users can install Git for Windows and make its shell available. It sets `RUMBLE_SMOKE=1`, invokes each individual bot's `.sh` launcher and requires a successful exit with the marker within 20 seconds. This verifies startup rather than a server handshake. Commit each `.sh` with executable mode `100755`, using `git update-index --chmod=+x` after staging it. Team entries contain JSON only and skip individual-bot launcher checks.

Copyright complaints, unsafe code, impersonation, and disputes are handled under [GOVERNANCE.md](GOVERNANCE.md). Do not include secrets in source, bot configuration, issues, or pull-request text.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

## Submit a bot

1. Copy [the bot template](.github/PULL_REQUEST_TEMPLATE/bot-submission.md) into `bots/<platform>/<BotName>/`.
1. Choose a [runnable bot template](templates/README.md) and follow its copy-and-rename instructions into `bots/<platform>/<BotName>/`.
2. Include `<BotName>.json`, `<BotName>.sh`, `<BotName>.cmd`, and source code that uses an official Tank Royale Bot API.
3. Declare one permitted SPDX license: `MIT`, `Apache-2.0`, `BSD-3-Clause`, or `GPL-3.0-or-later`.
4. Run `python scripts/validate_bot.py --root . --owner <your-forge-account> --smoke` before opening a pull request.

Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full submission, ownership, licensing, and moderation rules. The current validator proves the source-run entry point starts; connecting the smoke check to a temporary Tank Royale server is the next hardening increment before public ranked submissions open.
Read [CONTRIBUTING.md](CONTRIBUTING.md) for the full submission, ownership, licensing, and moderation rules. The [template guide](templates/README.md#smoke-check-and-executable-scripts) explains smoke markers, runtime prerequisites and executable bits. JSON-only TwinDuel teams use the [team template](templates/README.md#json-only-twinduel-team). The validator's smoke check starts the source entry point; it does not establish a server connection or completed battle. Connecting the smoke check to a temporary Tank Royale server is the next hardening increment before public ranked submissions open.

## Catalog

Expand Down
58 changes: 57 additions & 1 deletion scripts/validate_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import subprocess
import sys
import unicodedata
from urllib.parse import unquote
from dataclasses import dataclass, replace
from datetime import UTC, datetime
from pathlib import Path
Expand All @@ -21,10 +22,14 @@
"csharp": (".cs", "C#", "Robocode.TankRoyale.BotApi"),
"java": (".java", "JVM", "dev.robocode.tankroyale.botapi"),
"python": (".py", "Python", "robocode_tank_royale"),
"typescript": (".ts", "TypeScript", "@robocode/tank-royale"),
"typescript": (".ts", "TypeScript", "@robocode.dev/tank-royale-bot-api"),
}
FORBIDDEN_SUFFIXES = {".dll", ".exe", ".jar", ".so", ".dylib", ".pyc", ".class", ".zip", ".tar", ".gz"}
FORBIDDEN_TOKENS = ("processbuilder", "runtime.exec", "subprocess", "os.system", "child_process", "system.diagnostics.process", "socket", "ctypes", "dllimport", "eval(")
TYPESCRIPT_API = "@robocode.dev/tank-royale-bot-api"
# The official API's NodeRuntimeAdapter requires its declared `ws` runtime peer.
# Review this boundary against the published API manifest when its dependency set changes.
TYPESCRIPT_RUNTIME_PEERS = {"ws"}
LEET = str.maketrans({"0": "o", "1": "l", "3": "e", "4": "a", "5": "s", "7": "t", "8": "b", "9": "g", "@": "a", "$": "s", "!": "i", "|": "l"})


Expand Down Expand Up @@ -123,6 +128,8 @@ def validate_bot(platform_key: str, directory: Path, *, smoke: bool) -> Bot:
raise ValidationError(f"{directory}: missing required `platform` in {directory.name}.json")
if config["platform"] != expected_platform:
raise ValidationError(f"{directory}: `{platform_key}` entries require platform `{expected_platform}`")
if platform_key == "typescript":
validate_typescript_dependencies(directory)
for suffix in (".sh", ".cmd"):
script = directory / f"{directory.name}{suffix}"
if not script.is_file():
Expand All @@ -138,6 +145,8 @@ def validate_bot(platform_key: str, directory: Path, *, smoke: bool) -> Bot:
if api_token.lower() not in source_text:
raise ValidationError(f"{directory}: source must reference the official Tank Royale {expected_platform} Bot API")
for path in directory.rglob("*"):
if path.is_dir() and path.name in {"node_modules", ".venv", "bin", "obj"}:
raise ValidationError(f"{path}: generated dependency and build directories are not allowed")
if not path.is_file():
continue
if "__pycache__" in path.parts:
Expand All @@ -153,6 +162,53 @@ def validate_bot(platform_key: str, directory: Path, *, smoke: bool) -> Bot:
return bot


def validate_typescript_dependencies(directory: Path) -> None:
"""Check reviewable npm declarations without installing or running package code."""
package = read_json(directory / "package.json")
allowed = {TYPESCRIPT_API} | TYPESCRIPT_RUNTIME_PEERS
for section in ("dependencies", "devDependencies", "optionalDependencies", "peerDependencies"):
dependencies = package.get(section, {})
if not isinstance(dependencies, dict):
raise ValidationError(f"{directory}: package.json `{section}` must be an object")
for name, version in dependencies.items():
if name not in allowed:
raise ValidationError(f"{directory}: unrelated TypeScript dependency `{name}` is not allowed")
if not isinstance(version, str) or any(token in version for token in (":", "/", "\\")):
raise ValidationError(f"{directory}: `{name}` must use a registry version, not a path, URL or package alias")
dependencies = package.get("dependencies", {})
if TYPESCRIPT_API not in dependencies:
raise ValidationError(f"{directory}: package.json dependencies must declare `{TYPESCRIPT_API}`")
lock = read_json(directory / "package-lock.json")
packages = lock.get("packages")
if not isinstance(packages, dict) or not isinstance(packages.get(""), dict):
raise ValidationError(f"{directory}: package-lock.json must contain npm package metadata")
for section in ("dependencies", "devDependencies", "optionalDependencies", "peerDependencies"):
if packages[""].get(section, {}) != package.get(section, {}):
raise ValidationError(f"{directory}: package-lock.json {section} do not match package.json; regenerate the lockfile")
api = packages.get(f"node_modules/{TYPESCRIPT_API}", {})
peers = api.get("peerDependencies", {}) if isinstance(api, dict) else {}
if not isinstance(peers, dict):
raise ValidationError(f"{directory}: the official API's locked peerDependencies must be an object")
for peer in TYPESCRIPT_RUNTIME_PEERS:
if peer not in peers or peer not in dependencies or f"node_modules/{peer}" not in packages:
raise ValidationError(f"{directory}: the official TypeScript API requires its declared `{peer}` runtime peer in the manifest and lockfile")
for path in packages:
if not path:
continue
name = path.rsplit("node_modules/", 1)[-1]
if name not in allowed:
raise ValidationError(f"{directory}: unrelated locked TypeScript dependency `{name}` is not allowed")
entry = packages[path]
if not isinstance(entry, dict) or not isinstance(entry.get("resolved"), str):
raise ValidationError(f"{directory}: locked `{name}` is missing registry resolution metadata")
version = entry.get("version")
if not isinstance(version, str) or not re.fullmatch(r"\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?", version):
raise ValidationError(f"{directory}: locked `{name}` must identify a concrete registry version")
expected_url = f"https://registry.npmjs.org/{name}/-/{name.rsplit('/', 1)[-1]}-{version}.tgz"
if unquote(entry["resolved"]) != expected_url or not isinstance(entry.get("integrity"), str) or not entry["integrity"]:
raise ValidationError(f"{directory}: locked `{name}` must resolve to its official npm registry package with integrity metadata")


def resolve_teams(bots: list[Bot]) -> list[Bot]:
"""Bind every team to its member bots, which may live under any platform directory."""
by_name: dict[str, Bot] = {}
Expand Down
88 changes: 88 additions & 0 deletions templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Start a ranked bot from a template

The templates live outside `bots/`, so they do not reserve names or enter the catalog. Each individual template includes a bot configuration, Unix and Windows launchers, pinned official Bot API dependency and source entry point. The default bot turns its gun and fires when it scans an opponent.

| Language | Copy this directory | Source file to rename | Prerequisites |
|---|---|---|---|
| Java | [java/StarterBot](java/StarterBot) | `src/StarterBot.java` | Java 25 and curl |
| C# | [csharp/StarterBot](csharp/StarterBot) | `src/StarterBot.cs` | .NET SDK 10 |
| Python | [python/StarterBot](python/StarterBot) | `src/StarterBot.py` | Python 3.11+ with the package from `requirements.txt` installed |
| TypeScript | [typescript/StarterBot](typescript/StarterBot) | `src/StarterBot.ts` | Node.js 24 and npm |

The templates pin Bot API version 1.3.1 to match the current Rumble client's engine. Java and C# restore the official package from their launchers. Python uses the environment you prepare below. TypeScript uses Node's built-in TypeScript support and installs the API and its declared `ws` runtime peer with `npm ci` in a temporary run directory. Dependency caches and generated output stay outside the submitted bot directory.

## Copy and rename

Choose one language and a unique bot name using letters and digits, beginning with a letter. The example uses Java and `MyBot`; substitute your language and name throughout.

On a Unix shell:

```shell
mkdir -p bots/java
cp -R templates/java/StarterBot bots/java/MyBot
```

On PowerShell:

```powershell
New-Item -ItemType Directory -Force bots/java | Out-Null
Copy-Item -Recurse templates/java/StarterBot bots/java/MyBot
```

In the copied directory, replace every `StarterBot` identifier in file contents with `MyBot`, including the class name, entry-point paths and smoke marker. Rename `StarterBot.json`, `StarterBot.sh`, `StarterBot.cmd` and the source file to use `MyBot`. For C#, also rename `StarterBot.csproj`. Keep the `src/` directory name unchanged.

For TypeScript, retain both `package.json` and `package-lock.json`. The package name is private and need not match the bot name. If you intentionally change a dependency version, regenerate the lockfile with `npm install --package-lock-only --ignore-scripts --no-audit --no-fund --prefix bots/typescript/MyBot` and submit it for review. The allowed direct dependency set is the official API and its declared `ws` runtime peer; adding a different library is rejected.

Edit the JSON `authors`, `description` and `license` to describe your bot. Start at version `1.0.0`. The directory, JSON filename and JSON `name` must match exactly, including case. The declared SPDX license applies to the complete submitted directory; retain a license you have the right to grant.

For Python, create an environment outside the bot directory and install the declared API. On a Unix shell:

```shell
python -m venv .venv
.venv/bin/python -m pip install -r bots/python/MyBot/requirements.txt
.venv/bin/python scripts/validate_bot.py --root . --owner YOUR_GITHUB_ACCOUNT --smoke
```

On PowerShell:

```powershell
python -m venv .venv
.venv/Scripts/python.exe -m pip install -r bots/python/MyBot/requirements.txt
.venv/Scripts/python.exe scripts/validate_bot.py --root . --owner YOUR_GITHUB_ACCOUNT --smoke
```

The validator passes its own Python interpreter to Python launchers through `RUMBLE_PYTHON`. Running it from the environment above makes both the validation script and the bot use the installed API.

## Smoke check and executable scripts

Install Git and make `sh` available; on Windows, use Git for Windows with Git Bash's `sh` on `PATH`. Run the validator from the repository root:

```shell
python scripts/validate_bot.py --root . --owner YOUR_GITHUB_ACCOUNT --smoke
```

The validator checks the whole catalog checkout, so its existing entries' runtimes must also be available. CI explicitly installs Python, Java 25, .NET 10 and Node.js 24. A template's smoke branch loads its API, prints `RUMBLE_SMOKE_READY MyBot` and exits successfully. The validator sets `RUMBLE_SMOKE=1` and allows 20 seconds per launcher. This checks the source entry point and dependency loading; it does not connect to a battle server. Test your strategy in the Tank Royale GUI before submitting it.

Initial Java and NuGet downloads require network access. If a first download exceeds the smoke timeout, run the launcher once with `RUMBLE_SMOKE=1` to populate its external cache, then rerun validation. Preserve the smoke branch when editing your strategy; normal battle execution leaves `RUMBLE_SMOKE` unset.

Stage the bot and set the Unix launcher's executable bit in Git on every platform:

```shell
git add bots/java/MyBot
git update-index --chmod=+x bots/java/MyBot/MyBot.sh
git ls-files --stage bots/java/MyBot/MyBot.sh
```

The last command must show mode `100755`. On Linux and macOS, also run `chmod +x bots/java/MyBot/MyBot.sh` before local validation. On Windows, the Git index mode is what the Linux CI checkout will receive. Keep generated dependencies and output outside `bots/`; submit no `node_modules`, virtual environment, `bin`, `obj`, JAR or DLL files.

## Submit and update

Use the [bot submission checklist](../.github/PULL_REQUEST_TEMPLATE/bot-submission.md) when opening the pull request. A moderator reviews the source; after merge, the publication workflow generates the catalog. Never edit `bots/index.json` or `bots/owners.json` in the PR.

For the next version, edit your source and bump the JSON `version`, for example from `1.0.0` to `1.0.1`, then rerun validation and submit another PR. Changing source without a version bump is rejected. Verify that the generated catalog contains the new active identity after merge; older versions remain historical records.

## JSON-only TwinDuel team

Copy [team/StarterTeam/StarterTeam.json](team/StarterTeam/StarterTeam.json) into `bots/<platform>/<TeamName>/<TeamName>.json`. Replace `StarterTeam` with the team name, set your authors and license, and put the full active `name version` identity in each of the two `teamMembers` slots. Both slots may name the same individual bot. Members can come from different languages.

The team directory contains only that JSON file: no `src/`, project file or launchers. Its member bots must already be in the catalog or in the same validated submission. A later member-version change requires an explicit update to `teamMembers` and a bump of the team's own version. Use the team-specific items in the submission checklist.
5 changes: 5 additions & 0 deletions templates/csharp/StarterBot/StarterBot.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@echo off
setlocal
if "%RUMBLE_DOTNET_CACHE%"=="" set "RUMBLE_DOTNET_CACHE=%TEMP%\rumble-bots-dotnet"
dotnet run --project "%~dp0StarterBot.csproj" --artifacts-path "%RUMBLE_DOTNET_CACHE%\StarterBot-%RANDOM%-%RANDOM%" -- %*
exit /b %errorlevel%
14 changes: 14 additions & 0 deletions templates/csharp/StarterBot/StarterBot.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseAppHost>false</UseAppHost>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<Compile Include="src/**/*.cs" />
<PackageReference Include="Robocode.TankRoyale.BotApi" Version="1.3.1" />
</ItemGroup>
</Project>
Loading
Loading