- Published on
- views・
Hacky Holidays 2022: “Port Authority”, a WebSocket Strategy Game
- Authors
- Name
- enscribe
- Github
Intro
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
Port Authority
- enscribe
- sahuang authors:- Luuk Hofman
- Diederik Bakker points: 5/5 = 350
category: ppc
We're initially provided with a link that takes us to a nice-looking webgame called the "Port Traffic Control Interface":
Although we can't directly interact with the game using keyboard controls, there's a manual on the top-right which details the task:
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:
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:
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:
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):
Each tick, obj
will change to an object structured this way:
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:
With this new Class, we can get both our own ships
array and really clean logging from the server:
Let's finally get to solving the challenge.
Level 1
Level 1
- blueset points: 25
The last thing I want to add was a web-based "controller", which can steer the ship on-click and start new levels. I moved all my code from a local .js
file to CodePen for instant page regeneration and accessability by teammates. Here's the HTML:
Here's the JS that adds functionality to these buttons. Note that these are made to be scalable/"future-proof", meaning I can freely add more buttons without needing to copy/paste slight alterations of the same code. I also made some changes upon switching to the CodePen, including deleting the require()
method and preventing level 1 from automatically starting on-open:
The preview on CodePen will look something like this:
Let's see if it actually works:
We could totally flag the challenge right now, but currently there's no way to see the filtered output we created. I know there's a "Console" button at the bottom-left of CodePen, but I'd like to see the output on the actual webpage, outside of the IDE. To do this, let's create a log()
function to append strings to a <textarea>
we'll add in the HTML:
We'll also spice up the page slightly with flexboxes, a <fieldset>
and some CSS:
Here's the preview now:
Sorry I was being extra. Let's flag the challenge now (sped up):
We've succesfully completed Level 1!
Level 2
Level 2
- blueset points: 25
"Lets script it"? I've already scripted throughout the entirety of Level 1 to accommodate for future levels! Let's add a Level 2 button to our scalable, future-proof code 😉:
This is what appears when clicking the button:
Looks like we'll have to add two more steer buttons:
It seems as though that you also need the ships to enter in a specific order. It will be difficult to multitask all three, but it's doable! Let's try to solve it (also very sped up):
Although we've solved level 2 manually, I have a gut feeling the next few ones won't be as trivial...
Level 3
Level 3
points: 50
After adding another button to start Level 3, this is the field we start with:
They added some rocks to the board, and the ships are now moving at a faster speed. This is unfeasable to complete via multitasking, so we'll have to come up with a method to keep the ships in place.
Here's the plan: let's make it so that these ships will constantly rotate at a certain interval - in doing so, they'll complete a 360° loop within a small area, and we can commandeer them one-at-a-time by disabling the loop for certain ships. Let's start by adding checkboxes to enable the loop:
Regarding the JavaScript, I'll be using performance.now()
and checking if the difference between it and window.lastRot
is greater than 500ms. This check will happen every tick, and in theory will create a consistently steering ship that doesn't produce "ILLEGAL_MOVE"
s for inputting too quickly:
Let's see if it works:
We've managed to stabilize the playing field for a manual solve! Let's flag the level:
Level 4
Level 4
points: 50
After I added the level 4 button alongside steer/loop buttons for the extra ship that popped up, I discovered that my solution for level 3 actually worked for level 4 as well:
This means I can flag this level without needing to code at all!:
Level 5
Level 5
- sahuang points: 200
Oh boy...
Level five gives us a large increase in rocks, a tiny harbor, and six total ships to work with at max speed. Unfortunately, there's not enough room for the ships to loop around in circles, so the solution to levels 3 and 4 won't work. We'll have to figure out something else.
Luckily, during some experimenation on level 1 I found out that you can actually do a full 180° turn by calling two consecutive turns in the code. In doing so, the ship won't hit any of the objects in its surroundings as compared to if it rotated 90° twice. We can observe this phenomenon below:
Also, if you noticed in the first GIF, the ships are spawning at around the same locations every time. With this, we can come up with a plan: create "obstacles" with x-y coordinates that will cause any ship that comes into the region to turn 180°. We'll create separate "lanes" for each ship that spawns, therefore stabilizing the playfield and allowing for a feasible manual solve:
Now, how are these checks going to work? After a lot of experimenting I found that three total criteria should be met:
- The ship is travelling in the same the direction passed as an argument when the
check()
function is called - The absolute difference between the x and y values of the object and the ship's top-left is less than a certain threshold (I chose 75px)
- The global variable to determine whether or not the ship has been rotated 180° yet is false (
hasRotated
)
Here's a visual I drew in case you're lost. The red/green squares on the left indicate the status of each check during different stages of the turn:
We can now begin programming by creating an Obstacle class and manually placing them down throughout the map. This was just a lot of trial and error, so don't worry about these coordinates feeling random:
Next, let's create the aforementioned hasRotated
object alongside the check()
function, which will implement the three criteria:
Finally, let's call the check()
function for each index in the ships
array. Each tick, every single ship will go through these twelve checks. Although this might seem redundant, we have no way of assigning lanes to specific ships, as the IDs are randomized every time based on the order they're meant to dock. This method simply generalizes all of them, and shouldn't cause issues performance-wise:
In theory, these checks should cause the ships to bounce back and forth in their specific lanes. Let's check it out:
Yes! We've managed to stabilize level 5 completely! Now, we need to be able to toggle the lanes off to manually solve the challenge. Let's add more checkboxes to the HTML and adjust the JS accordingly:
Now, we can strategize on how to solve the challenge manually. Our team deduced that the most ideal order for ships would look something like this:
This order allows for the first ship to enter the port within two turns, and provides plenty of space for the second and third ships to work with. Although it would require a lot of restarting (as the order is always random), it's worth it to ease the difficulty of the challenge.
Moving on, we began work on the manual solve process. It was super tedious and involved a lot of mess-ups, especially around the port area. We discovered that the window to turn into the port was extraordinarily small, leading to many runs dying to something like this:
We decided it'd be best if we added another obstacle to perfectly turn us into the dock every time. This time around, it would have to be a 90° turn utlizing the middle of the ship instead of the top-left, as each ship is a different length and would therefore turn at different points when within the obstacles's hitbox:
Here is its implementation:
When you turn a ship through those rocks into the obstacle, the ship will now automatically turn to enter the dock perfectly:
NOW IT'S TIME TO SOLVE THE CHALLENGE MANUALLY! It took multiple hours across several days, and included some chokes as tragic as this one:
But, finally, I got the solve run clipped here, with a small reaction 🤣:
CTF{CaPT41n-j4Ck-sp4rR0w}
Here is the final script:
Afterword
If you made it to this point of the writeup, I want to sincerely thank you for reading. This writeup genuinely took longer to create than it took to solve the challenge (about 30 hours across two weeks), as I had to recreate, record, crop, and optimize every aspect of the solve. I had to create my own multi-hundred-line plugins to implement custom code blocks specifically for this writeup. Everything from the line numbers in highlighted diffs of code to the diagrams were hand-done, as this is my passion: to create for people to learn in a concise, aesthetically pleasing manner. This is also an entry for the Hacky Holidays writeup competition, so wish me luck! 🤞
- enscribe
Update: There I am! 🎉 Thanks for the support, everybody.