Category: Prog
Difficulty: Easy
Tags: PVE
Author: Log_s
This is the first Pirate Race PVE challenge.
You must code your own bot that controls a ship on a 2D sea and race against a predefined bot.
Each turn:
angle (integer, 0–359)acceleration (integer in [-100, 100])data (short string used as persistent memory between turns)The goal is to beat the predefined bot and obtain the flag.
To access the platform, you need a CTFd access token:
Game platform:
http://pirate.heroctf.frThe exact endpoints and protocol (e.g., /state, /move) are documented on the challenge platform.
A typical (simplified) game state looks like:
{
"sea_size": 1000,
"your_ship": {
"position": { "x": 123, "y": 456 },
"velocity": { "x": 1.2, "y": -0.4 },
"angle": 90
},
"islands": [
{
"position": { "x": 100, "y": 200 },
"radius": 30,
"type": 1,
"validated": false
}
],
"barrels": [
{
"position": { "x": 500, "y": 600 },
"collected": false
}
],
"data": "..." // our own memory, <= 64 chars
}
(x, y) define positions on the map.0° = North90° = East180° = South270° = WestEach turn, we respond with:
{
"acceleration": 100,
"angle": 42,
"data": "..."
}
The author provides a reference bot that beats the first (spiral) bot.
High-level idea:
(tx, ty) and radius tr in data.(tx, ty) using _angle_from_A_to_B.radius + 20), switch to circling:
(tx, ty) becomes validated, we:
The persistent data string format used is:
"{stage}-{tx:04},{ty:04},{tr:02}"
This allows the bot to remember:
0, 1, or 2)tx, ty)tr)Even though the author’s bot completely ignores rhum barrels, it is already sufficient to beat the initial spiral bot. Barrels are mostly introduced for the PVP version of the challenge.
I implemented a slightly different bot that:
data memory format to:
import math
def distance(p1, p2):
return math.hypot(p1[0] - p2[0], p1[1] - p2[1])
def angle_to_target(ship_pos, target_pos):
"""Convert target vector into game angle (0° = North, 90° = East)."""
sx, sy = ship_pos
tx, ty = target_pos
dx = tx - sx
dy = ty - sy
if dx == 0 and dy == 0:
return 0
# Game direction: vector(angle) = (sin(angle), -cos(angle))
ang_rad = math.atan2(dx, -dy)
ang_deg = (math.degrees(ang_rad) + 360) % 360
return ang_deg
def normalize_angle_diff(a, b):
# minimal signed difference a-b in degrees, in [-180, 180]
return (a - b + 540) % 360 - 180
The key is matching the game’s angle convention (0° at North instead of East).
Each island has a type:
Instead of going to the center, I compute a validation point slightly outside the island toward the required side:
def island_validation_point(island, sea_size):
ix = island["position"]["x"]
iy = island["position"]["y"]
r = island["radius"]
t = island["type"]
margin = 5
border_margin = 5
offset = r + margin # a bit outside island radius
if t == 1: # East => x > ix
x = min(ix + offset, sea_size - border_margin)
y = iy
elif t == 3: # West => x < ix
x = max(ix - offset, border_margin)
y = iy
elif t == 2: # South => y > iy
x = ix
y = min(iy + offset, sea_size - border_margin)
else: # t == 4, North => y < iy
x = ix
y = max(iy - offset, border_margin)
return (x, y)
Then I pick the closest unvalidated island by this validation point:
def choose_next_island(ship_pos, islands, sea_size):
best_idx = None
best_dist = float("inf")
for idx, island in enumerate(islands):
if island.get("validated"):
continue
vp = island_validation_point(island, sea_size)
d = distance(ship_pos, vp)
if d < best_dist:
best_dist = d
best_idx = idx
return best_idx, best_dist
I use a small memory format that fits under 64 characters:
"Bx1,y1;x2,y2|Ppx,py,c"
B... → up to 3 banned barrels (coordinates).P... → previous barrel target (px, py) and c = how many turns we were close to it.If we chase the same barrel for too long while being within a small distance and it still isn’t collected, we ban it so we never waste time on it again.
I only detour to a barrel if:
barrel_dist < 0.7 * island_dist)barrel_dist < 150 in absolute terms.Once a target is chosen:
angle_to_target.100, 90, 60).Finally, I return:
return {
"acceleration": acceleration,
"angle": int(angle) % 360,
"data": data_str,
}
A typical client loop (pseudocode) looks like:
import requests
import time
import math
TOKEN = "YOUR_CTFD_TOKEN"
BASE = "http://pirate.heroctf.fr"
headers = {"Authorization": f"Token {TOKEN}"}
while True:
state = requests.get(f"{BASE}/state", headers=headers).json()
move = make_move(state) # your bot logic
requests.post(f"{BASE}/move", json=move, headers=headers)
time.sleep(0.05) # adjust to challenge rules
Check the platform documentation for exact endpoints and rate limits.