//Pirate Race - Challenge Writeup
>Challenge Overview
Category: Programming / PVE
Goal: Win the "Pirate Race" by programming a bot to control a ship. The objective is to score points by validating islands (passing them on a specific side) and collecting rum barrels.
Win Condition: To beat the level and get the flag, you must win at least 8 out of 10 games in a batch submission against the "Player 2" bot.
>Solution Strategy
The core of the solution is a robust navigation algorithm implemented in bot.py. The strategy focuses on efficiency and safety:
- Smart Target Selection:
* The bot calculates the distance to all unvalidated islands.
* It prioritizes the nearest unvalidated island to minimize travel time.
* Once all islands are validated, it switches to collecting the nearest rum barrels to maximize the score.
- Precise Validation Points:
* For each island type (1-4), the bot calculates a specific "validation point" 30 units away from the island center in the required direction (e.g., East for Type 1).
* This ensures the ship passes on the correct side to validate the island.
- Waypoint Navigation & Collision Avoidance:
* The Problem: Simply steering towards the validation point can lead to collisions if the island is in the way (e.g., trying to reach the East side of an island while currently being on the West side).
* The Solution: The bot checks if the direct path to the validation point is blocked by the target island itself.
* If blocked, it generates an intermediate waypoint 60 units perpendicular to the island's axis. This forces the ship to steer around the island first, before heading to the validation point.
* This simple "go around" logic prevents the bot from getting stuck or crashing.
- Stuck Detection:
* The bot monitors its speed. If the speed drops below a threshold (indicating a collision or getting stuck), it triggers an escape maneuver to break free.
>Implementation Details
1. The Bot Logic (bot.py)
This script contains the make_move function called by the game engine. It parses the game state, applies the strategy, and returns the acceleration and angle.
import math
def get_distance(p1, p2):
return math.sqrt((p1['x'] - p2['x'])**2 + (p1['y'] - p2['y'])**2)
def make_move(game_state):
islands = game_state['islands']
barrels = game_state['barrels']
ship = game_state['your_ship']
ship_pos = ship['position']
ship_angle = ship['angle']
# 1. Select Target
target_pos = None
target_type = "island"
# Prioritize unvalidated islands
unvalidated = [i for i in islands if not i['validated']]
if unvalidated:
# Find nearest unvalidated island
nearest = min(unvalidated, key=lambda i: get_distance(ship_pos, i['position']))
# Calculate validation point based on island type
# Type 1: East, Type 2: South, Type 3: West, Type 4: North
offset = 30
if nearest['type'] == 1:
target_pos = {'x': nearest['position']['x'] + offset, 'y': nearest['position']['y']}
elif nearest['type'] == 2:
target_pos = {'x': nearest['position']['x'], 'y': nearest['position']['y'] + offset}
elif nearest['type'] == 3:
target_pos = {'x': nearest['position']['x'] - offset, 'y': nearest['position']['y']}
elif nearest['type'] == 4:
target_pos = {'x': nearest['position']['x'], 'y': nearest['position']['y'] - offset}
# Waypoint Navigation: Check if we need to go around
# If we are on the "wrong" side of the island relative to the target, add a waypoint
dx = target_pos['x'] - ship_pos['x']
dy = target_pos['y'] - ship_pos['y']
dist_to_target = math.sqrt(dx*dx + dy*dy)
# Check if the island center is close to the line of sight
# Simple heuristic: if the island is between us and the target
ix = nearest['position']['x']
iy = nearest['position']['y']
dist_to_island = get_distance(ship_pos, nearest['position'])
if dist_to_island < dist_to_target and dist_to_island > 10: # If island is closer than target
# Check alignment. If we are blocked, offset target perpendicular to the path
# This is a simplified "go around" logic
pass
# Improved Waypoint Logic:
# If target is East (Type 1) but we are West of the island, go North-ish or South-ish
if nearest['type'] == 1 and ship_pos['x'] < nearest['position']['x']:
target_pos = {'x': nearest['position']['x'], 'y': nearest['position']['y'] - 60} # Go North-ish
elif nearest['type'] == 3 and ship_pos['x'] > nearest['position']['x']:
target_pos = {'x': nearest['position']['x'], 'y': nearest['position']['y'] + 60} # Go South-ish
elif nearest['type'] == 2 and ship_pos['y'] < nearest['position']['y']:
target_pos = {'x': nearest['position']['x'] + 60, 'y': nearest['position']['y']} # Go East-ish
elif nearest['type'] == 4 and ship_pos['y'] > nearest['position']['y']:
target_pos = {'x': nearest['position']['x'] - 60, 'y': nearest['position']['y']} # Go West-ish
else:
# All islands validated, go for barrels
available_barrels = [b for b in barrels if not b['collected']]
if available_barrels:
nearest = min(available_barrels, key=lambda b: get_distance(ship_pos, b['position']))
target_pos = nearest['position']
target_type = "barrel"
else:
# Nothing to do, just circle center
target_pos = {'x': 500, 'y': 500}
# 2. Calculate Steering
if target_pos:
dx = target_pos['x'] - ship_pos['x']
dy = target_pos['y'] - ship_pos['y']
target_angle = math.degrees(math.atan2(dx, -dy)) % 360
diff = (target_angle - ship_angle + 180) % 360 - 180
# Simple P-controller for angle
if abs(diff) > 10:
angle = ship_angle + diff # Turn towards target
else:
angle = target_angle
acceleration = 100 # Full speed ahead
# Stuck detection (simple)
speed = math.sqrt(ship['velocity']['x']**2 + ship['velocity']['y']**2)
if speed < 2:
acceleration = 100
angle = (ship_angle + 90) % 360 # Try to turn out
return {
"acceleration": acceleration,
"angle": angle,
"data": f"Target: {target_type}"
}
return {
"acceleration": 0,
"angle": ship_angle,
"data": "Idle"
}
2. The Solver Script (solve.py)
This script handles the API interactions. It authenticates, submits the bot.py code, and triggers a batch execution (is_test=False).
import requests
import json
import time
BASE_URL = "https://pirate.heroctf.fr/api/v1"
TOKEN = "ctfd_127ad7f58d9cb396740af334473dfa18ddce6c414894f3f84a675b62e49a68ef"
def get_latest_game_uuid(session, is_batch=False):
r = session.get(f"{BASE_URL}/games")
if r.status_code == 200:
games = r.json().get("games", [])
if not games: return None
# Filter for batch or single game
target_type = 'batch' if is_batch else 'single_game'
for g in games:
if g.get('type') == target_type:
return g['uuid']
return None
def main():
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {TOKEN}"})
# 1. Read bot code
with open("bot.py", "r") as f:
code = f.read()
# 2. Submit code
# Set is_test=False to trigger a batch of 10 games for the flag
payload = {
"code": code,
"is_test": False
}
print("[*] Submitting bot code (Batch Mode)...")
r = session.post(f"{BASE_URL}/queue/games", json=payload)
print(f"[*] Status: {r.status_code}")
# 3. Poll for the new batch UUID
print("[*] Polling for new batch UUID...")
initial_uuid = get_latest_game_uuid(session, is_batch=True)
for _ in range(30):
new_uuid = get_latest_game_uuid(session, is_batch=True)
if new_uuid and new_uuid != initial_uuid:
print(f"[+] New Batch UUID found: {new_uuid}")
with open("last_batch_uuid.txt", "w") as f:
f.write(new_uuid)
print("[*] Run poll_batch.py to monitor progress.")
return
time.sleep(2)
print("[-] No new batch found.")
if __name__ == "__main__":
main()
3. The Monitor Script (poll_batch.py)
This script monitors the progress of the batch submission. It checks how many games have been processed and how many have been won.
import requests
import time
import json
import sys
TOKEN = "ctfd_127ad7f58d9cb396740af334473dfa18ddce6c414894f3f84a675b62e49a68ef"
URL = "https://pirate.heroctf.fr/api/v1/games"
try:
with open("last_batch_uuid.txt", "r") as f:
BATCH_UUID = f.read().strip()
except FileNotFoundError:
print("[-] last_batch_uuid.txt not found.")
sys.exit(1)
headers = {"Authorization": f"Bearer {TOKEN}"}
print(f"[*] Monitoring Batch: {BATCH_UUID}")
while True:
try:
r = requests.get(URL, headers=headers)
if r.status_code == 200:
games = r.json().get("games", [])
for g in games:
if g["uuid"] == BATCH_UUID:
processed = g.get("number_of_processed_games", 0)
won = g.get("number_of_won_games", 0)
print(f"[*] Status: Processed={processed}/10, Won={won}")
if processed == 10:
print(f"[+] Batch Complete! Total Wins: {won}/10")
if won >= 8:
print("[+] SUCCESS! You should have the flag (check level).")
else:
print("[-] Failed to reach 8 wins.")
sys.exit(0)
break
time.sleep(5)
except Exception as e:
print(f"[-] Error: {e}")
time.sleep(5)
>Execution & Results
-
Setup: Ensure
bot.py,solve.py, andpoll_batch.pyare in the same directory. -
Submit: Run
python3 solve.py. This submits the bot and saves the new batch UUID tolast_batch_uuid.txt. -
Monitor: Run
python3 poll_batch.py. Watch the progress.
Result:
The bot successfully achieved 8/10 wins in the batch df30c1a2-ccfe-11f0-98c0-7a71d61ca89c.
Upon completion, the user profile (/api/v1/auth/me) updated the level to "PVP", confirming the challenge was solved and the flag was automatically submitted.