fcode 2.2.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fcode/__init__.py +3 -0
- fcode/_types.py +655 -0
- fcode/api.py +146 -0
- fcode/auth.py +49 -0
- fcode/cli.py +127 -0
- fcode/commands/__init__.py +0 -0
- fcode/commands/init.py +29 -0
- fcode/commands/ladder.py +74 -0
- fcode/commands/login.py +135 -0
- fcode/commands/logout.py +21 -0
- fcode/commands/map_editor.py +91 -0
- fcode/commands/match_detail.py +173 -0
- fcode/commands/match_group.py +163 -0
- fcode/commands/matches.py +127 -0
- fcode/commands/run.py +186 -0
- fcode/commands/starter.py +119 -0
- fcode/commands/status.py +112 -0
- fcode/commands/submission.py +260 -0
- fcode/commands/team_group.py +73 -0
- fcode/commands/teams.py +73 -0
- fcode/commands/test.py +51 -0
- fcode/commands/test_matches.py +59 -0
- fcode/commands/test_run.py +125 -0
- fcode/commands/watch.py +123 -0
- fcode/compat.py +36 -0
- fcode/config.py +47 -0
- fcode/data/__init__.py +0 -0
- fcode/data/docs/spec.md +290 -0
- fcode/data/maps/__init__.py +0 -0
- fcode/data/maps/atoll.map26 +0 -0
- fcode/data/maps/aurora.map26 +0 -0
- fcode/data/maps/crossfire.map26 +0 -0
- fcode/data/maps/duel.map26 +0 -0
- fcode/data/maps/fjord.map26 +0 -0
- fcode/data/maps/hive.map26 +0 -0
- fcode/data/maps/longship.map26 +0 -0
- fcode/data/maps/pinch.map26 +0 -0
- fcode/data/maps/quarry.map26 +0 -0
- fcode/data/maps/runestone.map26 +0 -0
- fcode/data/maps/skerry.map26 +0 -0
- fcode/data/maps/sprint.map26 +0 -0
- fcode/data/maps/strait.map26 +0 -0
- fcode/data/maps/twins.map26 +0 -0
- fcode/data/maps/vault.map26 +0 -0
- fcode/data/starter_bot.py +452 -0
- fcode/data/visualiser/.directory +9 -0
- fcode/data/visualiser/Builderbot_med.png +0 -0
- fcode/data/visualiser/armoured_conveyor_gold.png +0 -0
- fcode/data/visualiser/armoured_conveyor_silver.png +0 -0
- fcode/data/visualiser/assets/app-BiRa_5u6.css +1 -0
- fcode/data/visualiser/assets/app-CK52NxpR.js +86 -0
- fcode/data/visualiser/assets/main-COIx42iq.js +1 -0
- fcode/data/visualiser/assets/map-editor-BDLYjgel.js +1 -0
- fcode/data/visualiser/axionite_ore.png +0 -0
- fcode/data/visualiser/axionite_processed.png +0 -0
- fcode/data/visualiser/axionite_raw.png +0 -0
- fcode/data/visualiser/barrier_gold.png +0 -0
- fcode/data/visualiser/barrier_silver.png +0 -0
- fcode/data/visualiser/base_gold.png +0 -0
- fcode/data/visualiser/base_silver.png +0 -0
- fcode/data/visualiser/battlecodetitle-orange.webp +0 -0
- fcode/data/visualiser/bg-light.png +0 -0
- fcode/data/visualiser/bg.png +0 -0
- fcode/data/visualiser/breach_e_gold.png +0 -0
- fcode/data/visualiser/breach_e_silver.png +0 -0
- fcode/data/visualiser/breach_n_gold.png +0 -0
- fcode/data/visualiser/breach_n_silver.png +0 -0
- fcode/data/visualiser/breach_ne_gold.png +0 -0
- fcode/data/visualiser/breach_ne_silver.png +0 -0
- fcode/data/visualiser/breach_nw_gold.png +0 -0
- fcode/data/visualiser/breach_nw_silver.png +0 -0
- fcode/data/visualiser/breach_s_gold.png +0 -0
- fcode/data/visualiser/breach_s_silver.png +0 -0
- fcode/data/visualiser/breach_se_gold.png +0 -0
- fcode/data/visualiser/breach_se_silver.png +0 -0
- fcode/data/visualiser/breach_sw_gold.png +0 -0
- fcode/data/visualiser/breach_sw_silver.png +0 -0
- fcode/data/visualiser/bridge_gold.png +0 -0
- fcode/data/visualiser/bridge_silver.png +0 -0
- fcode/data/visualiser/bridge_stand_gold.png +0 -0
- fcode/data/visualiser/bridge_stand_silver.png +0 -0
- fcode/data/visualiser/builderbot_back_gold.png +0 -0
- fcode/data/visualiser/builderbot_back_silver.png +0 -0
- fcode/data/visualiser/builderbot_front_gold.png +0 -0
- fcode/data/visualiser/builderbot_front_silver.png +0 -0
- fcode/data/visualiser/builderbot_side_gold.png +0 -0
- fcode/data/visualiser/builderbot_side_silver.png +0 -0
- fcode/data/visualiser/conveyor_gold.png +0 -0
- fcode/data/visualiser/conveyor_silver.png +0 -0
- fcode/data/visualiser/dimetric/Dimetric_Tile_Cube.png +0 -0
- fcode/data/visualiser/dimetric/Dimetric_Tile_Floor_Footprint.png +0 -0
- fcode/data/visualiser/dimetric/barrier_team1.png +0 -0
- fcode/data/visualiser/dimetric/barrier_team2.png +0 -0
- fcode/data/visualiser/dimetric/base_team1.png +0 -0
- fcode/data/visualiser/dimetric/base_team2.png +0 -0
- fcode/data/visualiser/dimetric/bg.png +0 -0
- fcode/data/visualiser/dimetric/bridge_stand_team1.png +0 -0
- fcode/data/visualiser/dimetric/bridge_stand_team2.png +0 -0
- fcode/data/visualiser/dimetric/bridge_team1.png +0 -0
- fcode/data/visualiser/dimetric/bridge_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_e_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_e_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_n_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_n_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_s_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_s_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_w_team1.png +0 -0
- fcode/data/visualiser/dimetric/builderbot_w_team2.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/conveyor_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_e_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_e_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_n_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_n_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_s_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_s_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/gunner_w_team1.png +0 -0
- fcode/data/visualiser/dimetric/gunner_w_team2.png +0 -0
- fcode/data/visualiser/dimetric/harvester_team1.png +0 -0
- fcode/data/visualiser/dimetric/harvester_team2.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team1_ne.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team1_nw.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team1_se.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team1_sw.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team2_ne.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team2_nw.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team2_se.png +0 -0
- fcode/data/visualiser/dimetric/launcher_team2_sw.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall_ne.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall_nw.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall_se.png +0 -0
- fcode/data/visualiser/dimetric/natural_wall_sw.png +0 -0
- fcode/data/visualiser/dimetric/road_team1.png +0 -0
- fcode/data/visualiser/dimetric/road_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_e_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_e_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_n_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_n_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_s_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_s_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_w_team1.png +0 -0
- fcode/data/visualiser/dimetric/sentinel_w_team2.png +0 -0
- fcode/data/visualiser/dimetric/splitter_ne_team1.png +0 -0
- fcode/data/visualiser/dimetric/splitter_ne_team2.png +0 -0
- fcode/data/visualiser/dimetric/splitter_nw_team1.png +0 -0
- fcode/data/visualiser/dimetric/splitter_nw_team2.png +0 -0
- fcode/data/visualiser/dimetric/splitter_se_team1.png +0 -0
- fcode/data/visualiser/dimetric/splitter_se_team2.png +0 -0
- fcode/data/visualiser/dimetric/splitter_sw_team1.png +0 -0
- fcode/data/visualiser/dimetric/splitter_sw_team2.png +0 -0
- fcode/data/visualiser/dimetric/titanium.png +0 -0
- fcode/data/visualiser/dimetric/titanium_ore.png +0 -0
- fcode/data/visualiser/fonts/HKGrotesk-Regular.otf +0 -0
- fcode/data/visualiser/foundry_gold.png +0 -0
- fcode/data/visualiser/foundry_silver.png +0 -0
- fcode/data/visualiser/gunner_n_gold.png +0 -0
- fcode/data/visualiser/gunner_n_silver.png +0 -0
- fcode/data/visualiser/gunner_ne_gold.png +0 -0
- fcode/data/visualiser/gunner_ne_silver.png +0 -0
- fcode/data/visualiser/gunner_nw_gold.png +0 -0
- fcode/data/visualiser/gunner_nw_silver.png +0 -0
- fcode/data/visualiser/gunner_s_gold.png +0 -0
- fcode/data/visualiser/gunner_s_silver.png +0 -0
- fcode/data/visualiser/gunner_se_gold.png +0 -0
- fcode/data/visualiser/gunner_se_silver.png +0 -0
- fcode/data/visualiser/gunner_sw_gold.png +0 -0
- fcode/data/visualiser/gunner_sw_silver.png +0 -0
- fcode/data/visualiser/gunner_w_gold.png +0 -0
- fcode/data/visualiser/gunner_w_silver.png +0 -0
- fcode/data/visualiser/harvester_gold.png +0 -0
- fcode/data/visualiser/harvester_silver.png +0 -0
- fcode/data/visualiser/index.html +20 -0
- fcode/data/visualiser/launcher_gold.png +0 -0
- fcode/data/visualiser/launcher_silver.png +0 -0
- fcode/data/visualiser/map-editor.html +20 -0
- fcode/data/visualiser/marker_gold.png +0 -0
- fcode/data/visualiser/marker_silver.png +0 -0
- fcode/data/visualiser/natural_wall.jpg +0 -0
- fcode/data/visualiser/road_gold.png +0 -0
- fcode/data/visualiser/road_silver.png +0 -0
- fcode/data/visualiser/sentinel_e_gold.png +0 -0
- fcode/data/visualiser/sentinel_e_silver.png +0 -0
- fcode/data/visualiser/sentinel_n_gold.png +0 -0
- fcode/data/visualiser/sentinel_n_silver.png +0 -0
- fcode/data/visualiser/sentinel_ne_gold.png +0 -0
- fcode/data/visualiser/sentinel_ne_silver.png +0 -0
- fcode/data/visualiser/sentinel_nw_gold.png +0 -0
- fcode/data/visualiser/sentinel_nw_silver.png +0 -0
- fcode/data/visualiser/sentinel_s_gold.png +0 -0
- fcode/data/visualiser/sentinel_s_silver.png +0 -0
- fcode/data/visualiser/sentinel_se_gold.png +0 -0
- fcode/data/visualiser/sentinel_se_silver.png +0 -0
- fcode/data/visualiser/sentinel_sw_gold.png +0 -0
- fcode/data/visualiser/sentinel_sw_silver.png +0 -0
- fcode/data/visualiser/splitter.png +0 -0
- fcode/data/visualiser/splitter_e_gold.png +0 -0
- fcode/data/visualiser/splitter_e_silver.png +0 -0
- fcode/data/visualiser/splitter_n_gold.png +0 -0
- fcode/data/visualiser/splitter_n_silver.png +0 -0
- fcode/data/visualiser/splitter_s_gold.png +0 -0
- fcode/data/visualiser/splitter_s_silver.png +0 -0
- fcode/data/visualiser/splitter_w_gold.png +0 -0
- fcode/data/visualiser/splitter_w_silver.png +0 -0
- fcode/data/visualiser/template02.png +0 -0
- fcode/data/visualiser/titanium.png +0 -0
- fcode/data/visualiser/titanium_ore.png +0 -0
- fcode/fcode_engine.cp313-win_amd64.pyd +0 -0
- fcode/py.typed +0 -0
- fcode/version_check.py +72 -0
- fcode-2.2.0.dist-info/METADATA +7 -0
- fcode-2.2.0.dist-info/RECORD +245 -0
- fcode-2.2.0.dist-info/WHEEL +4 -0
- fcode-2.2.0.dist-info/entry_points.txt +2 -0
- fcode-2.2.0.dist-info/sboms/fcode_engine.cyclonedx.json +1963 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""fcode test-matches — list your remote test runs."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from fcode.api import api_get
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _show_test_matches(limit: int = 20):
|
|
13
|
+
"""Core implementation for listing test matches."""
|
|
14
|
+
data = api_get("/api/matches/test-runs")
|
|
15
|
+
match_list = data.get("matches", [])[:limit]
|
|
16
|
+
|
|
17
|
+
if not match_list:
|
|
18
|
+
console.print("[dim]No test runs found.[/dim]")
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
table = Table(show_header=True, header_style="bold")
|
|
22
|
+
table.add_column("Match ID", style="dim")
|
|
23
|
+
table.add_column("Score", justify="center")
|
|
24
|
+
table.add_column("Status")
|
|
25
|
+
table.add_column("Stage", style="dim")
|
|
26
|
+
table.add_column("By", style="dim")
|
|
27
|
+
table.add_column("Date", style="dim")
|
|
28
|
+
|
|
29
|
+
for m in match_list:
|
|
30
|
+
score = f"{m.get('scoreA', 0)}-{m.get('scoreB', 0)}"
|
|
31
|
+
status = m.get("status", "?")
|
|
32
|
+
stage = m.get("stage") or ""
|
|
33
|
+
match_id = m.get("id", "?")
|
|
34
|
+
requested_by = m.get("requestedByName") or ""
|
|
35
|
+
|
|
36
|
+
if status == "complete":
|
|
37
|
+
status = f"[green]{status}[/green]"
|
|
38
|
+
elif status == "error":
|
|
39
|
+
status = f"[red]{status}[/red]"
|
|
40
|
+
if m.get("errorMessage"):
|
|
41
|
+
stage = m["errorMessage"][:60]
|
|
42
|
+
elif status == "running":
|
|
43
|
+
status = f"[yellow]{status}[/yellow]"
|
|
44
|
+
|
|
45
|
+
date = ""
|
|
46
|
+
raw_date = m.get("completedAt") or m.get("createdAt")
|
|
47
|
+
if raw_date:
|
|
48
|
+
date = raw_date[:16].replace("T", " ")
|
|
49
|
+
|
|
50
|
+
table.add_row(match_id, score, status, stage, requested_by, date)
|
|
51
|
+
|
|
52
|
+
console.print(table)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command("test-matches")
|
|
56
|
+
@click.option("--limit", default=20, type=int, help="Number of test runs to show")
|
|
57
|
+
def test_matches(limit: int):
|
|
58
|
+
"""List your remote test run matches."""
|
|
59
|
+
_show_test_matches(limit)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""fcode test-run — run a remote test match with local bots and TLE enforcement."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from fcode.api import api_post_multipart
|
|
12
|
+
from fcode.config import find_config
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _zip_bot(bot_path: str, bots_dir: Path) -> bytes:
|
|
18
|
+
"""Resolve a bot path and return it as a zip in memory."""
|
|
19
|
+
p = Path(bot_path)
|
|
20
|
+
if not p.exists() and not p.is_absolute():
|
|
21
|
+
candidate = bots_dir / bot_path
|
|
22
|
+
if candidate.exists():
|
|
23
|
+
p = candidate
|
|
24
|
+
|
|
25
|
+
if p.is_dir():
|
|
26
|
+
main_py = p / "main.py"
|
|
27
|
+
if not main_py.is_file():
|
|
28
|
+
raise click.BadParameter(f"Directory '{bot_path}' does not contain main.py")
|
|
29
|
+
buf = io.BytesIO()
|
|
30
|
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
31
|
+
for root, _dirs, files in os.walk(p):
|
|
32
|
+
for f in files:
|
|
33
|
+
full = Path(root) / f
|
|
34
|
+
arcname = str(full.relative_to(p))
|
|
35
|
+
zf.write(full, arcname)
|
|
36
|
+
return buf.getvalue()
|
|
37
|
+
|
|
38
|
+
if p.is_file() and p.name == "main.py":
|
|
39
|
+
buf = io.BytesIO()
|
|
40
|
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
41
|
+
zf.write(p, "main.py")
|
|
42
|
+
return buf.getvalue()
|
|
43
|
+
|
|
44
|
+
if p.is_file() and p.suffix == ".zip":
|
|
45
|
+
return p.read_bytes()
|
|
46
|
+
|
|
47
|
+
raise click.BadParameter(f"Bot not found or invalid: '{bot_path}'")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _show_test_run(bot_a: str, bot_b: str, maps: tuple[str, ...]):
|
|
51
|
+
"""Core implementation for running a remote test match."""
|
|
52
|
+
config, project_root = find_config()
|
|
53
|
+
bots_dir = (project_root / config.bots_dir).resolve()
|
|
54
|
+
maps_dir = (project_root / config.maps_dir).resolve()
|
|
55
|
+
|
|
56
|
+
console.print("[bold]Packaging bots...[/bold]")
|
|
57
|
+
try:
|
|
58
|
+
zip_a = _zip_bot(bot_a, bots_dir)
|
|
59
|
+
zip_b = _zip_bot(bot_b, bots_dir)
|
|
60
|
+
except click.BadParameter as e:
|
|
61
|
+
console.print(f"[red]{e.format_message()}[/red]")
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
|
|
64
|
+
files: dict[str, tuple[str, bytes, str]] = {
|
|
65
|
+
"bot_a": ("bot_a.zip", zip_a, "application/zip"),
|
|
66
|
+
"bot_b": ("bot_b.zip", zip_b, "application/zip"),
|
|
67
|
+
}
|
|
68
|
+
text_fields: dict[str, str] = {
|
|
69
|
+
"bot_a_name": Path(bot_a).stem,
|
|
70
|
+
"bot_b_name": Path(bot_b).stem,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if len(maps) > 5:
|
|
74
|
+
console.print("[red]Maximum 5 maps per test run.[/red]")
|
|
75
|
+
raise SystemExit(1)
|
|
76
|
+
|
|
77
|
+
if maps:
|
|
78
|
+
from fcode.commands.run import resolve_map_path
|
|
79
|
+
|
|
80
|
+
resolved_maps: list[Path] = []
|
|
81
|
+
for map_arg in maps:
|
|
82
|
+
mp = resolve_map_path(map_arg, maps_dir)
|
|
83
|
+
if not mp.is_file():
|
|
84
|
+
console.print(f"[red]Map file not found: {map_arg}[/red]")
|
|
85
|
+
raise SystemExit(1)
|
|
86
|
+
resolved_maps.append(mp)
|
|
87
|
+
|
|
88
|
+
for i, mp in enumerate(resolved_maps):
|
|
89
|
+
files[f"map_{i}"] = (mp.name, mp.read_bytes(), "application/octet-stream")
|
|
90
|
+
text_fields["num_maps"] = str(len(resolved_maps))
|
|
91
|
+
|
|
92
|
+
map_names = ", ".join(mp.stem for mp in resolved_maps)
|
|
93
|
+
console.print(f" Maps: {map_names} ({len(resolved_maps)} game{'s' if len(resolved_maps) != 1 else ''})")
|
|
94
|
+
|
|
95
|
+
console.print("[bold]Uploading and submitting test run...[/bold]")
|
|
96
|
+
data = api_post_multipart("/api/matches/test-run", files, text_fields)
|
|
97
|
+
|
|
98
|
+
match_id = data.get("matchId")
|
|
99
|
+
if not match_id:
|
|
100
|
+
console.print("[red]Unexpected response from server.[/red]")
|
|
101
|
+
raise SystemExit(1)
|
|
102
|
+
|
|
103
|
+
console.print(f"[green]Test run queued![/green] Match ID: {match_id}")
|
|
104
|
+
console.print(f" Check status: fcode match tests")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@click.command("test-run")
|
|
108
|
+
@click.argument("bot_a")
|
|
109
|
+
@click.argument("bot_b")
|
|
110
|
+
@click.argument("maps", nargs=-1)
|
|
111
|
+
def test_run(bot_a: str, bot_b: str, maps: tuple[str, ...]):
|
|
112
|
+
"""Run a remote test match between two local bots with TLE enforcement.
|
|
113
|
+
|
|
114
|
+
\b
|
|
115
|
+
BOT_A and BOT_B can be directories (containing main.py), single .py files,
|
|
116
|
+
or .zip archives — same as `fcode run`.
|
|
117
|
+
MAPS are optional .map26 files — one per game. If omitted, 5 random maps.
|
|
118
|
+
|
|
119
|
+
\b
|
|
120
|
+
Examples:
|
|
121
|
+
fcode test-run starter starter
|
|
122
|
+
fcode test-run bot_a bot_b arena.map26
|
|
123
|
+
fcode test-run bot_a bot_b arena.map26 canyon.map26 arena.map26
|
|
124
|
+
"""
|
|
125
|
+
_show_test_run(bot_a, bot_b, maps)
|
fcode/commands/watch.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""fcode watch — open replay in local or platform visualizer."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command()
|
|
10
|
+
@click.argument("replay_file", required=False, type=click.Path(exists=True))
|
|
11
|
+
@click.option("--match", "match_id", type=str, help="Match ID to view on platform")
|
|
12
|
+
@click.option("--game", "game_number", type=int, help="Game number within match")
|
|
13
|
+
def watch(replay_file: str | None, match_id: str | None, game_number: int | None):
|
|
14
|
+
"""Open replay in visualizer.
|
|
15
|
+
|
|
16
|
+
\b
|
|
17
|
+
Examples:
|
|
18
|
+
fcode watch replay.replay26 # View local replay file
|
|
19
|
+
fcode watch --match <id> # Open match on platform
|
|
20
|
+
fcode watch --match <id> --game 3 # Open specific game
|
|
21
|
+
"""
|
|
22
|
+
if match_id:
|
|
23
|
+
_open_platform(match_id, game_number)
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
if not replay_file:
|
|
27
|
+
console.print("[red]Provide a replay file or use --match <id>[/red]")
|
|
28
|
+
console.print("[dim] fcode watch replay.replay26[/dim]")
|
|
29
|
+
console.print("[dim] fcode watch --match <match-id>[/dim]")
|
|
30
|
+
raise SystemExit(1)
|
|
31
|
+
|
|
32
|
+
_serve_local(replay_file)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _open_platform(match_id: str, game_number: int | None) -> None:
|
|
36
|
+
"""Open the platform visualiser page in the user's browser."""
|
|
37
|
+
import webbrowser
|
|
38
|
+
from fcode.auth import get_api_url
|
|
39
|
+
|
|
40
|
+
base_url = get_api_url()
|
|
41
|
+
url = f"{base_url}/visualiser?matchId={match_id}"
|
|
42
|
+
if game_number is not None:
|
|
43
|
+
url += f"&game={game_number}"
|
|
44
|
+
|
|
45
|
+
console.print(f"Opening visualiser: [link={url}]{url}[/link]")
|
|
46
|
+
webbrowser.open(url)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _find_dist() -> "str | None":
|
|
50
|
+
"""Find the visualiser dist directory. Returns dist_path or None."""
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
|
|
53
|
+
mono_dist = Path(__file__).parent.parent.parent.parent.parent / "visualiser" / "dist"
|
|
54
|
+
bundled_dist = Path(__file__).parent.parent / "data" / "visualiser"
|
|
55
|
+
|
|
56
|
+
if (mono_dist / "index.html").is_file():
|
|
57
|
+
return str(mono_dist.resolve())
|
|
58
|
+
if (bundled_dist / "index.html").is_file():
|
|
59
|
+
return str(bundled_dist.resolve())
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _serve_local(replay_file: str) -> None:
|
|
64
|
+
"""Serve the visualiser locally with a replay file auto-loaded."""
|
|
65
|
+
import http.server
|
|
66
|
+
import threading
|
|
67
|
+
import webbrowser
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
|
|
70
|
+
replay_path = Path(replay_file).resolve()
|
|
71
|
+
|
|
72
|
+
dist_path = _find_dist()
|
|
73
|
+
if not dist_path:
|
|
74
|
+
console.print(
|
|
75
|
+
"[red]Visualiser not found. "
|
|
76
|
+
"Try reinstalling fcode or use --match <id> to view on the platform.[/red]"
|
|
77
|
+
)
|
|
78
|
+
raise SystemExit(1)
|
|
79
|
+
|
|
80
|
+
# Ensure correct MIME types on Windows (registry may map .js to text/plain)
|
|
81
|
+
import mimetypes
|
|
82
|
+
|
|
83
|
+
mimetypes.add_type("application/javascript", ".js")
|
|
84
|
+
mimetypes.add_type("application/javascript", ".mjs")
|
|
85
|
+
mimetypes.add_type("application/wasm", ".wasm")
|
|
86
|
+
mimetypes.add_type("text/css", ".css")
|
|
87
|
+
|
|
88
|
+
class ReplayHandler(http.server.SimpleHTTPRequestHandler):
|
|
89
|
+
def __init__(self, *args, **kwargs):
|
|
90
|
+
super().__init__(*args, directory=dist_path, **kwargs)
|
|
91
|
+
|
|
92
|
+
def do_GET(self):
|
|
93
|
+
if self.path == "/local-replay":
|
|
94
|
+
try:
|
|
95
|
+
data = replay_path.read_bytes()
|
|
96
|
+
self.send_response(200)
|
|
97
|
+
self.send_header("Content-Type", "application/octet-stream")
|
|
98
|
+
self.send_header("Content-Length", str(len(data)))
|
|
99
|
+
self.end_headers()
|
|
100
|
+
self.wfile.write(data)
|
|
101
|
+
except Exception:
|
|
102
|
+
self.send_error(500, "Failed to read replay file")
|
|
103
|
+
return
|
|
104
|
+
super().do_GET()
|
|
105
|
+
|
|
106
|
+
def log_message(self, format, *args):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
server = http.server.HTTPServer(("127.0.0.1", 0), ReplayHandler)
|
|
110
|
+
port = server.server_address[1]
|
|
111
|
+
|
|
112
|
+
url = f"http://127.0.0.1:{port}/?replayUrl=/local-replay"
|
|
113
|
+
console.print(f"Serving visualiser at [link={url}]{url}[/link]")
|
|
114
|
+
console.print(f"Replay: {replay_path.name}")
|
|
115
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]")
|
|
116
|
+
|
|
117
|
+
threading.Timer(0.5, lambda: webbrowser.open(url)).start()
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
server.serve_forever()
|
|
121
|
+
except KeyboardInterrupt:
|
|
122
|
+
console.print("\n[dim]Stopped.[/dim]")
|
|
123
|
+
server.shutdown()
|
fcode/compat.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Backwards-compatibility helpers for CLI restructuring."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
_SHOWN_WARNINGS: set[str] = set()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def deprecation_warning(old: str, new: str) -> None:
|
|
12
|
+
"""Print a one-time deprecation warning suggesting the new command."""
|
|
13
|
+
key = f"{old}->{new}"
|
|
14
|
+
if key not in _SHOWN_WARNINGS:
|
|
15
|
+
_SHOWN_WARNINGS.add(key)
|
|
16
|
+
console.print(
|
|
17
|
+
f"[yellow]Warning: `fcode {old}` is deprecated. Use: fcode {new}[/yellow]\n"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SmartGroup(click.Group):
|
|
22
|
+
"""A Click group that routes unknown subcommands to a default command.
|
|
23
|
+
|
|
24
|
+
If the first argument isn't a known subcommand, it's treated as an
|
|
25
|
+
argument to the default command (e.g. `fcode match <uuid>` routes to
|
|
26
|
+
`fcode match info <uuid>`).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, *args, default_cmd: str = "info", **kwargs):
|
|
30
|
+
super().__init__(*args, **kwargs)
|
|
31
|
+
self.default_cmd = default_cmd
|
|
32
|
+
|
|
33
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
34
|
+
if args and args[0] not in self.commands and not args[0].startswith("-"):
|
|
35
|
+
args = [self.default_cmd] + args
|
|
36
|
+
return super().parse_args(ctx, args)
|
fcode/config.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
if sys.version_info >= (3, 11):
|
|
4
|
+
import tomllib
|
|
5
|
+
else:
|
|
6
|
+
try:
|
|
7
|
+
import tomllib
|
|
8
|
+
except ModuleNotFoundError:
|
|
9
|
+
import tomli as tomllib
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
CONFIG_FILENAME = "fcode.toml"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class FcodeConfig:
|
|
18
|
+
bots_dir: str = "bots"
|
|
19
|
+
maps_dir: str = "maps"
|
|
20
|
+
replay: str = "replay.replay26"
|
|
21
|
+
seed: int = 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_config() -> tuple[FcodeConfig, Path]:
|
|
25
|
+
"""Walk up from cwd to find fcode.toml. Returns (config, project_root)."""
|
|
26
|
+
cwd = Path.cwd().resolve()
|
|
27
|
+
for directory in [cwd] + list(cwd.parents):
|
|
28
|
+
config_path = directory / CONFIG_FILENAME
|
|
29
|
+
if config_path.is_file():
|
|
30
|
+
raw = config_path.read_bytes()
|
|
31
|
+
try:
|
|
32
|
+
data = tomllib.loads(raw.decode("utf-8"))
|
|
33
|
+
except UnicodeDecodeError:
|
|
34
|
+
# Fix files written with system encoding (e.g. cp1252 on Windows)
|
|
35
|
+
data = tomllib.loads(raw.decode("utf-8", errors="replace"))
|
|
36
|
+
config_path.write_bytes(raw.decode("utf-8", errors="replace").encode("utf-8"))
|
|
37
|
+
return _parse_config(data), directory
|
|
38
|
+
return FcodeConfig(), cwd
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_config(data: dict) -> FcodeConfig:
|
|
42
|
+
return FcodeConfig(
|
|
43
|
+
bots_dir=data.get("bots_dir", FcodeConfig.bots_dir),
|
|
44
|
+
maps_dir=data.get("maps_dir", FcodeConfig.maps_dir),
|
|
45
|
+
replay=data.get("replay", FcodeConfig.replay),
|
|
46
|
+
seed=data.get("seed", FcodeConfig.seed),
|
|
47
|
+
)
|
fcode/data/__init__.py
ADDED
|
File without changes
|
fcode/data/docs/spec.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Background
|
|
2
|
+
|
|
3
|
+
The year is 2076. Titan, Saturn's largest moon, has become the new frontier for autonomous resource extraction. At least six corporations have deployed robotic fleets to its surface, claiming deposits, building infrastructure, and contesting rival operations. None of this has been publicly disclosed.
|
|
4
|
+
|
|
5
|
+
Titan is lethal to humans: −179°C, a nitrogen-methane atmosphere, and a 76-minute communication delay to Earth. All operations are carried out by robots.
|
|
6
|
+
|
|
7
|
+
You are a programmer employed by **Meridian Industries** under the classified ASO (Autonomous Systems Operations) programme. Your job is to write the software that controls Meridian's fleet: mining ore, delivering resources, and outcompeting whoever else is out there.
|
|
8
|
+
|
|
9
|
+
# Objective
|
|
10
|
+
|
|
11
|
+
The objective of the game is to collect resources and destroy the enemy's core.
|
|
12
|
+
To do this, you must find ore deposits, build harvesters, deliver resources back to the core using conveyors, and expand your territory.
|
|
13
|
+
|
|
14
|
+
If both teams' cores are still alive after 1000 rounds, the winner is decided by the following tiebreakers, in order:
|
|
15
|
+
|
|
16
|
+
- Total amount of titanium delivered to the core
|
|
17
|
+
- Number of harvesters currently alive
|
|
18
|
+
- Total titanium currently stored
|
|
19
|
+
- Coinflip
|
|
20
|
+
|
|
21
|
+
# Map
|
|
22
|
+
|
|
23
|
+
The map is a rectangular grid whose size is between 8x8 and 30x30 inclusive.
|
|
24
|
+
The top left (northwest) corner of the map is position (0, 0).
|
|
25
|
+
It is guaranteed that the map is symmetric by either reflection or rotation.
|
|
26
|
+
|
|
27
|
+
Each grid cell is one of the following:
|
|
28
|
+
|
|
29
|
+
- Wall
|
|
30
|
+
- Titanium ore
|
|
31
|
+
- Empty
|
|
32
|
+
|
|
33
|
+
Walls prevent building anything on the tile they occupy.
|
|
34
|
+
Ores are tiles on which harvesters may be built to mine resources.
|
|
35
|
+
|
|
36
|
+
# Resources
|
|
37
|
+
|
|
38
|
+
## Titanium
|
|
39
|
+
|
|
40
|
+
Titanium is the primary resource used to construct most buildings in the game.
|
|
41
|
+
Each team starts with 500 global titanium.
|
|
42
|
+
Additionally, each team gains 10 passive titanium every 4 rounds.
|
|
43
|
+
|
|
44
|
+
# Communication
|
|
45
|
+
|
|
46
|
+
Each team has a **Global Communication Store**: 16 integer slots (indexed 0-15), readable and writable by every unit on that team via `read_store(index)` / `write_store(index, value)`. Each team's store is private -- the opposing team can neither read nor write it.
|
|
47
|
+
|
|
48
|
+
Writes are buffered: a write made during a round is not visible until the following round, not even to the unit that made it. This guarantees every unit sees a consistent snapshot of the store for the entire round, regardless of the order units act in.
|
|
49
|
+
|
|
50
|
+
# Units and Buildings
|
|
51
|
+
|
|
52
|
+
## Units
|
|
53
|
+
|
|
54
|
+
Units are game entities which run an independent instance of the code that you submit.
|
|
55
|
+
The core, builder bots, and turrets are units. Harvesters are **not** units.
|
|
56
|
+
|
|
57
|
+
Each round, units take their turns in the order they were spawned.
|
|
58
|
+
After all units have taken their turn, resources are distributed (see below).
|
|
59
|
+
|
|
60
|
+
Each team may have at most 50 living units at once, including its core.
|
|
61
|
+
Bots can query their current usage of this cap with `get_unit_count()` and compare it against `GameConstants.MAX_TEAM_UNITS`.
|
|
62
|
+
|
|
63
|
+
Units have a **vision radius** and an **action radius**.
|
|
64
|
+
The vision radius is the area in which the unit can sense its environment.
|
|
65
|
+
The action radius is the area in which the unit can perform actions, such as building or destroying buildings.
|
|
66
|
+
All units have an action radius of sqrt(2), except the core, which has an action radius of sqrt(8) (measured from its centre).
|
|
67
|
+
Turrets also have a **attack range** which is different from its action radius.
|
|
68
|
+
|
|
69
|
+
All units have action and move cooldowns which are nonnegative integers that decrease by 1 at the end of each round.
|
|
70
|
+
(The move cooldown is only used by the builder bot.)
|
|
71
|
+
Actions and movement can only be done when the respective cooldown is 0.
|
|
72
|
+
|
|
73
|
+
Units may self-destruct at any time.
|
|
74
|
+
|
|
75
|
+
## Buildings
|
|
76
|
+
|
|
77
|
+
Buildings are game entities which are immovable. All entities are buildings except builder bots.
|
|
78
|
+
In particular, the core and turrets are considered _both a unit and a building_.
|
|
79
|
+
|
|
80
|
+
## Note: cost scaling
|
|
81
|
+
|
|
82
|
+
The cost of a building or unit is floor(scale \* base cost) where scale starts at 1.0 and increases with each entity built:
|
|
83
|
+
conveyors, splitters, barriers add +1% each; harvesters add +5% each; gunners, launchers add +10% each; builder bots and sentinels add +20% each.
|
|
84
|
+
When an entity is destroyed, its contribution is removed. This is to encourage you to be efficient with what you build!
|
|
85
|
+
|
|
86
|
+
## Core
|
|
87
|
+
|
|
88
|
+
The core is each team's central building -- if your core is destroyed, you lose the game.
|
|
89
|
+
Each team starts with one core.
|
|
90
|
+
|
|
91
|
+
It has a 2x2 footprint and has a vision radius of 6 and an action radius of sqrt(8) (measured from the nearest tile of its footprint).
|
|
92
|
+
Resources must be transferred to the core (via conveyors) to be added to the global pool which is used for building.
|
|
93
|
+
|
|
94
|
+
The core can spawn up to one builder bot per turn on any tile immediately surrounding its footprint (orthogonally or diagonally adjacent, but not on the footprint itself), which costs one action cooldown.
|
|
95
|
+
|
|
96
|
+
HP: 500
|
|
97
|
+
|
|
98
|
+
## Builder bot
|
|
99
|
+
|
|
100
|
+
Builder bots are the only mobile unit -- they are responsible for constructing buildings.
|
|
101
|
+
|
|
102
|
+
Builder bots have a vision radius of sqrt(20) and an action radius of sqrt(2).
|
|
103
|
+
|
|
104
|
+
Builder bots can move onto any tile that does not contain another builder bot, provided it is not a wall and either has no building on it, or has a conveyor or splitter belonging to either team.
|
|
105
|
+
These are considered _walkable_ tiles.
|
|
106
|
+
The direction of the conveyor or splitter does not matter, and neither does the presence of resources on the tile.
|
|
107
|
+
|
|
108
|
+
A builder bot may move to an adjacent (including diagonal) tile if its move cooldown is 0, increasing it by 1.
|
|
109
|
+
|
|
110
|
+
If its action cooldown is 0, a builder bot may perform one of the following actions, increasing its action cooldown by 1:
|
|
111
|
+
|
|
112
|
+
**Build**: build any building or turret on a tile without a building within its action radius.
|
|
113
|
+
Only _walkable_ buildings (conveyors and splitters) may be built on a tile which contains a builder bot.
|
|
114
|
+
|
|
115
|
+
**Attack**: spend 2 Ti to deal 2 damage to the building on the tile it is standing on.
|
|
116
|
+
|
|
117
|
+
**Heal**: spend 1 Ti to heal all friendly entities on a tile within the builder bot's action radius by 4 HP.
|
|
118
|
+
If a friendly builder bot is standing on a friendly building there, both are healed.
|
|
119
|
+
|
|
120
|
+
Builder bots may also destroy any allied building within its action radius any number of times per turn.
|
|
121
|
+
|
|
122
|
+
Finally, self-destructing a builder bot deals no damage.
|
|
123
|
+
|
|
124
|
+
HP: 40
|
|
125
|
+
Base cost: 30 Ti
|
|
126
|
+
Scaling contribution: 20%
|
|
127
|
+
|
|
128
|
+
## Note: resource distribution
|
|
129
|
+
|
|
130
|
+
Some buildings, such as conveyors, may store, accept and output resources.
|
|
131
|
+
Resources are always stored and moved in stacks of 10.
|
|
132
|
+
|
|
133
|
+
At the end of each round, resources are distributed. Buildings may accept resources, output resources, or both.
|
|
134
|
+
**Resources may be outputted to a building belonging to the opposing team.**
|
|
135
|
+
|
|
136
|
+
The details of resource distribution are given in the per-building descriptions below.
|
|
137
|
+
|
|
138
|
+
## Conveyors
|
|
139
|
+
|
|
140
|
+
All conveyors can hold one stack of any resource, and both accept input and produce output.
|
|
141
|
+
Basic conveyors and splitters point in one of the cardinal directions.
|
|
142
|
+
|
|
143
|
+
### Conveyor
|
|
144
|
+
|
|
145
|
+
Accepts resources from any of its three non-output directions.
|
|
146
|
+
Sends its contents in the direction it is pointing if that tile can accept a resource.
|
|
147
|
+
|
|
148
|
+
Base cost: 3 Ti
|
|
149
|
+
Scaling contribution: 1%
|
|
150
|
+
HP: 20
|
|
151
|
+
|
|
152
|
+
### Splitter
|
|
153
|
+
|
|
154
|
+
Alternates between outputting in three directions: the primary output direction and the two adjacent directions.
|
|
155
|
+
Only accepts input from the back.
|
|
156
|
+
Prioritises outputting in directions which were used the least recently.
|
|
157
|
+
|
|
158
|
+
Base cost: 6 Ti
|
|
159
|
+
Scaling contribution: 1%
|
|
160
|
+
HP: 20
|
|
161
|
+
|
|
162
|
+
## Harvester
|
|
163
|
+
|
|
164
|
+
Can be placed on an ore deposit. Outputs one stack of the corresponding resource to an adjacent building every 4 rounds.
|
|
165
|
+
The first resource will be outputted immediately on the round the harvester is built.
|
|
166
|
+
|
|
167
|
+
Prioritises outputting in directions which were used the least recently.
|
|
168
|
+
|
|
169
|
+
Base cost: 20 Ti
|
|
170
|
+
Scaling contribution: 5%
|
|
171
|
+
HP: 30
|
|
172
|
+
|
|
173
|
+
## Barrier
|
|
174
|
+
|
|
175
|
+
Cheap, takes up space and has high HP.
|
|
176
|
+
|
|
177
|
+
Base cost: 3 Ti
|
|
178
|
+
Scaling contribution: 1%
|
|
179
|
+
HP: 30
|
|
180
|
+
|
|
181
|
+
## Turrets
|
|
182
|
+
|
|
183
|
+
Every turret except the launcher faces in one of 8 directions.
|
|
184
|
+
Ammo must be fed to turrets via conveyors, from any direction apart except the direction the turret is facing.
|
|
185
|
+
Diagonal turrets can be fed from all four sides.
|
|
186
|
+
A turret can hold up to one stack of one type of resource and only accepts incoming resources when it is completely empty.
|
|
187
|
+
If a tile containing both a building and a builder bot is hit, only the builder bot takes damage.
|
|
188
|
+
|
|
189
|
+
### Gunner
|
|
190
|
+
|
|
191
|
+
Has a vision radius of sqrt(13). A gunner traces a straight line in the direction it is facing. Empty tiles do not block this line. Walls block the line but are not targetable. Builder bots and buildings are both targetable and blocking.
|
|
192
|
+
Gunners can also rotate to any other direction by calling `rotate(direction)`, spending 10 Ti and taking a 1-turn cooldown.
|
|
193
|
+
|
|
194
|
+
Base cost: 10 Ti
|
|
195
|
+
Scaling contribution: 10%
|
|
196
|
+
HP: 40
|
|
197
|
+
Damage: 10
|
|
198
|
+
Reload: 1
|
|
199
|
+
Resources per shot: 2
|
|
200
|
+
|
|
201
|
+
### Sentinel
|
|
202
|
+
|
|
203
|
+
High range, low damage support turret.
|
|
204
|
+
Vision range is sqrt(32). Can hit all tiles within 1 king move (chebyshev distance) of the straight line in the direction it's facing, within vision range.
|
|
205
|
+
|
|
206
|
+
Base cost: 30 Ti
|
|
207
|
+
Scaling contribution: 20%
|
|
208
|
+
HP: 30
|
|
209
|
+
Damage: 18
|
|
210
|
+
Reload: 3
|
|
211
|
+
Resources per shot: 10
|
|
212
|
+
|
|
213
|
+
### Launcher
|
|
214
|
+
|
|
215
|
+
Can pick up and throw adjacent (including diagonal) builder bots. The target tile must be bot-passable.
|
|
216
|
+
|
|
217
|
+
Vision range and attack range are both sqrt(26).
|
|
218
|
+
|
|
219
|
+
Base cost: 20 Ti
|
|
220
|
+
Scaling contribution: 10%
|
|
221
|
+
HP: 30
|
|
222
|
+
Reload: 1
|
|
223
|
+
|
|
224
|
+
# Reference table
|
|
225
|
+
|
|
226
|
+
| Entity | HP | Base cost | Scale % | Notes |
|
|
227
|
+
| ----------- | --- | --------- | ------- | ---------------------------------------------------------------- |
|
|
228
|
+
| Core | 500 | — | — | 2×2 footprint; spawns builder bots |
|
|
229
|
+
| Builder bot | 40 | 30 Ti | 20% | Mobile; can build and heal in action radius, and attack own tile |
|
|
230
|
+
| Conveyor | 20 | 3 Ti | 1% | Cardinal direction; 3 inputs, 1 output |
|
|
231
|
+
| Splitter | 20 | 6 Ti | 1% | 1 input (back), 3 rotating outputs |
|
|
232
|
+
| Harvester | 30 | 20 Ti | 5% | Outputs a stack of resources every 4 rounds |
|
|
233
|
+
| Barrier | 30 | 3 Ti | 1% | |
|
|
234
|
+
| Gunner | 40 | 10 Ti | 10% | Targets occupied tiles in facing direction; can rotate for 10 Ti |
|
|
235
|
+
| Sentinel | 30 | 30 Ti | 20% | Line AoE ±1 |
|
|
236
|
+
| Launcher | 30 | 20 Ti | 10% | Throws adjacent builder bots |
|
|
237
|
+
|
|
238
|
+
## Unit stats
|
|
239
|
+
|
|
240
|
+
| Unit | Vision r² | Action r² | Attack r² | Damage | Reload | Ammo/shot |
|
|
241
|
+
| ----------- | --------- | ---------- | ------------- | ------------------------ | ------ | --------- |
|
|
242
|
+
| Core | 36 | 8 | — | — | — | — |
|
|
243
|
+
| Builder bot | 20 | 2 | 0 (own tile) | 2 (building on own tile) | 1 | 2 Ti |
|
|
244
|
+
| Gunner | 13 | 2 | 13 (= vision) | 10 | 1 | 2 |
|
|
245
|
+
| Sentinel | 32 | 2 | 32 (= vision) | 18 | 3 | 10 |
|
|
246
|
+
| Launcher | 26 | 2 (pickup) | 26 (throw) | — | 1 | — |
|
|
247
|
+
|
|
248
|
+
# Appendix: implementation details
|
|
249
|
+
|
|
250
|
+
## Documentation
|
|
251
|
+
|
|
252
|
+
API methods are not all covered in this document.
|
|
253
|
+
Refer to the documentation for a comprehensive list of all available methods.
|
|
254
|
+
|
|
255
|
+
## Entity IDs
|
|
256
|
+
|
|
257
|
+
All entities (buildings and units) in the game have a unique integer ID.
|
|
258
|
+
All controller methods which deal with entities will accept and return entity IDs.
|
|
259
|
+
Properties of an entity can be queried with the appropriate getter function, for example, `get_hp(id)`.
|
|
260
|
+
This API design was chosen for performance reasons, as constructing Python objects to return is very slow.
|
|
261
|
+
|
|
262
|
+
Resources moving through storage and conveyors also have unique integer IDs.
|
|
263
|
+
These are resource stack IDs, not entity IDs, and can be queried with `get_stored_resource_id(id)`.
|
|
264
|
+
|
|
265
|
+
## Computation limit
|
|
266
|
+
|
|
267
|
+
Each unit is allocated 10 milliseconds of CPU time per turn.
|
|
268
|
+
If your code exceeds the time limit, execution will be interrupted, and the next turn the run() function will be called again.
|
|
269
|
+
**Your bot does not resume next turn where it left off.**
|
|
270
|
+
|
|
271
|
+
To mitigate variance in runtime, each unit has an **extra time buffer** that is 5% of the time limit.
|
|
272
|
+
If the bot takes longer than the normal time limit, then the difference is deducted from the extra time buffer.
|
|
273
|
+
Once the bot reaches (time limit + extra time) runtime, it is interrupted.
|
|
274
|
+
**If the bot takes less time than the time limit, then the difference is refunded to the extra time buffer.**
|
|
275
|
+
The extra time buffer is capped at 5% of the normal time limit.
|
|
276
|
+
|
|
277
|
+
### Testing against the time limit
|
|
278
|
+
|
|
279
|
+
The local runner will not check for the time limit, as execution time differs by machine.
|
|
280
|
+
Instead, we will provide execution servers for you to test your own bots against the time limit on the same machines that run ladder matches.
|
|
281
|
+
|
|
282
|
+
## Debugging
|
|
283
|
+
|
|
284
|
+
We provide several debugging utilities.
|
|
285
|
+
|
|
286
|
+
Printing to stdout (normal `print()`) is captured by the engine and saved to the replay.
|
|
287
|
+
You can then view your units' stdout output in the visualiser.
|
|
288
|
+
To print to the console, use stderr.
|
|
289
|
+
|
|
290
|
+
You can also use `draw_indicator_line()` and `draw_indicator_dot()` to draw debugging information on the map itself.
|
|
File without changes
|
|
Binary file
|