Hacky Holidays 2022: “Port Authority,” a WebSocket Game
Overview
Hacky Holidays 2022: “Port Authority,” a WebSocket Game

Hacky Holidays 2022: “Port Authority,” a WebSocket Game

July 27, 2022
3 min read (12 min read total)
5 subposts
index

Introduction

This challenge was part of the Deloitte Hackazon Hacky Holidays “Unlock the City” 2022 CTF (yeah, what a name!). Labeled under the #ppc category, which apparently stands for “professional programming challenge”, it was the final challenge under the “District 1” segment of the CTF and categorized under the Hard difficulty.

This was the first CTF problem which didn’t just challenge my ability to critically think and problem solve - it also challenged my motor control and hand-eye coordination. Why? Because I solved it by hand! I believe this challenge was meant to be solved using 100% programming, but I wanted to challenge myself. This was the process.

Port Authority
Solvers
b blueset ,
Authors
Luuk Hofman, Diederik Bakker
Category
PPC
Points
5/5 = 350

The harbour is in total chaos, the ships are no longer on course. The AI has disabled the brakes of all the ships and corrupted our control systems. The ships about to crash into each other, can you build a new AI that will rescue the ships and deliver the cargo?

Note

This is an instance-based challenge. No website URL will be provided!

Initial Inspection & Interaction

We’re initially provided with a link that takes us to a nice-looking webgame called the “Port Traffic Control Interface”:

Initial Website

Although we can’t directly interact with the game using keyboard controls, there’s a manual on the top-right which details the task:

Manual Website

According to this, we can start playing the game and controlling the ships that appear through a WebSocket connection, which is an API that enables two-way communication between a user’s browser and a server. This documentation describes the protocol alongside how to open/close and send/receive using JavaScript.

Heavily referencing the aforementioned documentation, I started off by installing the WebSocket package with npm i ws, and then creating a solve.js with the following code:

solve.js
// Make sure you install WebSocket with "npm i ws"!
const WebSocket = require('ws')
// Regex so that I can freely paste the URL when the instance is changed
const url = 'https://[REDACTED].challenge.hackazon.org/'
// Opens WebSocket connection
const socket = new WebSocket(`wss://${url.replace(/^https?:\/\//, '')}ws`)
// Runs on socket open, equivalent to .addEventListener()
socket.onopen = function () {
console.log('[+] Connected!')
// Converts object to string
socket.send(
JSON.stringify({
type: 'START_GAME',
level: 1,
}),
)
}
// Runs when output from server is received
socket.onmessage = function (event) {
// Output is received in event
console.log(`[-] ${event.data}`)
}

Look what happens when we establish a connection - the game starts running, and we start receiving per-tick input from the server in our console:

Start Website

Terminal window
$ node test.js
[+] Connected!
[-] {"type":"GAME_START","level":{"id":1,"board":{"width":1886,"height":1188,"obstructions":[{"type":"HARBOUR_BORDER","area":[{"x":577,"y":0},{"x":627,"y":215.7142857142857}]},{"type":"HARBOUR_BORDER","area":[{"x":875,"y":0},{"x":925,"y":215.7142857142857}]},{"type":"BORDER_ROCK","area":[{"x":0,"y":0},{"x":577,"y":51}]},{"type":"BORDER_ROCK","area":[{"x":925,"y":0},{"x":1886,"y":51}]}],"harbour":[{"x":700,"y":0},{"x":850,"y":107.85714285714285}]},"mechanics":{"borderCollision":false,"sequentialDocking":true},"ships":[null]}}
[-] {"type":"TICK","ships":[{"type":"SHIP_6","area":[{"x":472,"y":795},{"x":532,"y":1063.75}],"direction":"UP","speed":3,"id":0,"isDocked":false}]}
[-] {"type":"TICK","ships":[{"type":"SHIP_6","area":[{"x":472,"y":795},{"x":532,"y":1063.75}],"direction":"UP","speed":3,"id":0,"isDocked":false}]}
[-] {"type":"TICK","ships":[{"type":"SHIP_6","area":[{"x":472,"y":792},{"x":532,"y":1060.75}],"direction":"UP","speed":3,"id":0,"isDocked":false}]}
[-] {"type":"TICK","ships":[{"type":"SHIP_6","area":[{"x":472,"y":789},{"x":532,"y":1057.75}],"direction":"UP","speed":3,"id":0,"isDocked":false}]}
...

Let’s see what happens when we send the SHIP_STEER command to the server after five seconds. We can do that with the setTimeout() method in our socket.onopen listener:

solve.js
socket.onopen = function() {
console.log("[+] Connected!");
// Converts object to string
socket.send(JSON.stringify({
"type": "START_GAME",
"level": 1
}));
// Sends steer command after one second
setTimeout(() => {
socket.send(JSON.stringify({
"type": "SHIP_STEER",
"shipId": 0
}));
}, 5000);
};

First Turn

From the provided GIF, we can see that the ship will turn clockwise on its central point when told to steer!

With this, we have a goal: get the ship into the port by sending JSON instructions to the WebSocket server. However, it’s definitely a good idea to create some quality-of-life features first, such as:

  • A way to convert our JSON data into an object we can reference
  • A class which can construct objects for each ship
  • An HTML/JS “controller”, which can be used to steer the ships with UI and to start new levels

Firstly, cleaning up the output involves parsing what we receive from the server, which we can do with the JSON.parse() method. We’ll assign it into a variable named obj (and also delete our steer-testing code):

solve.js
// Sends steer command after one second
setTimeout(() => {
socket.send(JSON.stringify({
"type": "SHIP_STEER",
"shipId": 0
}));
}, 5000);
};
// Runs when output from server is received
socket.onmessage = function(event) {
// Output is received in event
console.log(`[-] ${event.data}`);
// Converts server output into object
let obj = JSON.parse(event.data);
};

Each tick, obj will change to an object structured this way:

{
"type": "TICK",
"ships": [
{
"type": "SHIP_6",
"area": [
{
"x": 472,
"y": 795
},
{
"x": 532,
"y": 1063.75
}
],
"direction": "UP",
"speed": 3,
"id": 0,
"isDocked": false
}
]
}

Check out the obj.type key - there’ll be multiple types of these (including but not limited to "LOSS", "GAME_START"). We’ll make it so that if obj.type is "TICK", it will create a new Class instance for each object in the obj.ships array:

solve.js
class Ship {
// Initializes class object instance
constructor(id, topLeft, bottomRight, direction) {
this.id = id;
this.topLeft = topLeft;
this.bottomRight = bottomRight;
this.direction = direction;
}
// Getter + abusing template literals
get printState() {
return `ID: ${this.id} | (${Math.floor(this.topLeft.x)},
${Math.floor(this.topLeft.y)}) (${Math.floor(this.bottomRight.x)},
${Math.floor(this.bottomRight.y)}) | DIR: ${this.direction}`;
}
}
// Runs when output from server is received
socket.onmessage = function(event) {
// Converts server output into object
let obj = JSON.parse(event.data);
if(obj.type == "TICK") {
let ships = [];
// For each ship in obj.ships, push class object into ships array
for(const i of obj.ships) {
ships.push(new Ship(i.id, i.area[0], i.area[1], i.direction));
}
// Call the string literal getter
for(const i of ships) {
console.log(i.printState);
}
}
};

With this new Class, we can get both our own ships array and really clean logging from the server:

Terminal window
$ node test.js
[+] Connected!
ID: 0 | (211, 256) (271, 524) | DIR: UP
ID: 0 | (211, 256) (271, 524) | DIR: UP
ID: 0 | (211, 252) (271, 520) | DIR: UP
ID: 0 | (211, 248) (271, 516) | DIR: UP
...

Let’s finally get to solving the challenge. I’ll use subposts to break down each level.