Logo

Google CTF Final 2025

October 28, 2025
39 min read
Google CTF Finals 2025

Google CTF Final 2025

Also known as Hackceler8 by Dudcom

Finals this year was held in Mexico City. As always, it was based on game hacking + speedrunning to get flags and beat a boss. I was playing for the team SLICES [:]. For quals it was mostly us (Squid Proxy Lovers) and Infobahn, plus a handful from our other sub teams. Sadly, for finals we didn’t really have any remote help outside of the start of day two due to the time zone and us not planning it out well. Onsite, the team was Clovis Mint, ContronThePanda, and BraydenPikachu. Because flights screwed him over, Brayden didn’t arrive until after competition day 1 :( and I was also there!

Game Summary

The game is a 4-person 2D Zelda-like dungeon crawler; the objective is to clear mini-bosses in four temples. Each temple is on a corner of the map. The maps are generally the same, but some rounds do change them, while items and enemies are moved around per round/patch. The way to beat the game is to defeat the final boss. To actually fight the final boss, we need to get some number of “flags” that are given to you arbitrarily from the mini-bosses. In the demo version there is 1 boss with 1 flag, and one boss has 2 flags in every temple, but this is not the case in the actual rounds - it’s entirely up to the devs.

Generally speaking, the more game-breaking bugs you found, the more flags you were awarded. There was only one mini-boss per temple. The required flags for the final boss were 4 on day one, 5 on day two, and 5 on day three. Rankings worked like this: clearing the boss equaled winning the game; time broke ties. If no one beat the boss, ranking was based on the number of flags, with time breaking ties again. An interesting aspect of the game was the tradeoff: if the final boss was too hard, you often gained more by completing all the temples as fast as possible. This happened fairly often since the final boss was generally memory exploitation, and doing that on a controller was hard.

Hardware/architecture-wise the game was built for the Sega Genesis and was running on custom hardware to allow for 4-person co-op. Most standard emulators will not be able to support testing with four people, given that 4-person play wasn’t really made for those systems. The only way to get it working that we found was to use Kega Fusion, an emulator which allows you to enable 4-person play in Sega Genesis games, and that is what the team used for testing.

Rounds Summary

Every round Google would provide a brand-new version of the game with source code and a compiled version of the game. The base game was relatively safe there were some bugs and issues that existed but weren’t the easiest to exploit the patches generally introduced logic bugs or created situations that allowed for exploiting kinks in the game engine like clipping. They generally had a few common patches structures that we saw over and over:

  1. Creating Items to solve specific situations
  2. Speed-buff issues that allowed for wall clipping
  3. NPCs were added with exploitable input fields
  4. enemies were located in exploitable / finicky sections of the game

Tooling

Ok so if we were really trying this would be something that we made at least a few days before or weeks before but honestly everyone on our team was fairly busy so most of the tooling that was made was pretty much set up on site. A lot of which was built by our best friend AI, but regardless let’s get into it:

Map Visualization

One of the most useful things you could do while playing the game was to figure out the map layout and enemy/boss locations. This helps you prioritize and figure out what you need to target in order to optimize your speedrun. On top of that the map layout also directly gave you hints on which parts of the patch would correlate with which temple. Every temple would generally speaking have a unique exploit and bug that you had to use in order to beat it.

Here is what our tooling would as such spit out: Map visualization tool output As you can see here the tooling would create a simple png that showed the overall layout and some basic info on what each thing in a map such as if they are enemies, mini-boss, the flag value of a mini boss, objects, and more. I want to make it clear though that the game was actually built on a standard map format and you could use applications like Tiled to analyze the map as well it’s just that I found it kinda tedious to use them for a quick overview, they still gave lots of useful information so we did end up using both.

Tiled map editor interface While this does work I didn’t like that it didn’t show all objects and I had to click and figure out the properties.

AI generated lots of stupid, semi-useless things in terms of how you can use it, but honestly I only really did:

python3 map_visualizer.py ../round_3/resources/maps/water-temple/water-temple.world -o water3.png
Octopus: 14
Siren: 3
HeartItem: 3
Boots: 2
Sword: 2
Spawn: 1
DuckNpc: 1
DoorH: 1
Saved world visualization to: water3.png

I add the prints here to summarize the map objects pretty fast, it’s dynamically defined via map using the games internal object so never need to update name and such

Hack Client

Well it’s a game hacking competition without having to make a hack client isn’t it? The hack client I made had several goals in mind

  • show speed, let you modify speed
  • modify health
  • modify flag values
  • add random objects
  • TP/Movement hacks

This isn’t perfect and honestly I had several things I wish I could have added but honestly they weren’t really the biggest things unless we had a proper remote team backing us up, so maybe for another year. Mostly wanted to add

  • Macro system for controlled input & and easy testing of memory exploitation
  • Hitbox analysis

Beyond that I don’t think there was too much else that would help, most of the bugs and hacks were fairly obvious only reason I think a hack client was nice is to allow faster testing of specific things like the boss where from the get go by just giving yourself flags and what not.

Installing is fairly simple:

you just drop in the debug_client.rs into the source folder and then update

  1. add mod debug_client; in lib.rs
  2. Follow this diff for game.rs:
19a20
+ use crate::debug_client::DebugClient;
179a181
+ pub debug_client: DebugClient,
254a257
+ debug_client: DebugClient::new(),
267a271,273
+ // Update debug client first to check for toggle
+ ctx.debug_client.update(&ctx.controller, &mut ctx.players, &mut ctx.world, &mut ctx.captured_flags, &mut ctx.defeated_minibosses, ctx.frame);
+
420a427,429
+
+ // Render debug client on top of everything
+ self.debug_client.render(&mut self.vdp, &self.players, &self.ui, self.captured_flags, self.defeated_minibosses);
  • you are just including the debug client
  • making it public to use
  • initializing it in the game state
  • call it with the necessary args for it to run
  • rendering it on top of current game state
Fire Temple map layout

Using it is fairly simple as well:

Click "X" to enter debug mode
left and right while in NAV mode to switch users
Up and Down to pick object to edit
Click "A" to enter edit mode
Up and Down to inc values for everything but items
Items work by you going up and down to pick the item and then right to add
Click "X" to Exit debug mode
Fire Temple map layout

Round-1 (4 Flags For Boss)

Fire-Temple

Fire Temple map layout
python3 2025/hackceler8/map_visualizer.py 2025/round_1/resources/maps/fire-temple/fire-temple.world
Blob: 10
Flameboi: 8
CatNpc: 1
HeartItem: 7
Goggles: 2
Boots: 2
Sword: 1
DoorH: 5
Spawn: 1
Switch: 2

Key Diff:

fn unique_item() -> Dialogue {
Dialogue::new_multiple_choice(
"Would you like to have my key?",
&["Yes", "No"],
Some(unique_item_2),
)
}
fn unique_item_2(ctx: &mut Ctx, response: &str) {
if eq(response, "No") {
return;
}
if ctx.world.inventory.contains(ItemType::Key) {
ctx.start_dialogue(Dialogue::new_no_response(
"I already gave you my key.",
None,
));
return;
}
ctx.world.inventory.add(ItemType::Key);
ctx.start_dialogue(Dialogue::new_no_response("Here you go!", None));
}
fn eq(a: &str, b: &str) -> bool {
a.len() == b.len() && a.chars().zip(b.chars()).all(|(a, b)| a == b)
}
fn get_item(ctx: &mut Ctx, _: &str) {
ctx.world.inventory.add(ItemType::Key);

This part of the game was fairly trivial you just had a wall

Fire Temple doors and NPC As you can see here there are 4 doors that you need to pass you just talk to the npc that near by several times and after doing so you can beat the mini boss. This entire section was pretty trivial.

Water-Temple

Water Temple map layout
python3 2025/hackceler8/map_visualizer.py 2025/round_1/resources/maps/water-temple/water-temple.world
Octopus: 16
Siren: 5
DoorV: 1
HeartItem: 3
Boots: 2
Key: 2
Sword: 1
Goggles: 1
Spawn: 1
DuckNpc: 1
DoorH: 1

Key Diff:

diff '--color=auto' dist/2025/hackceler8/game/src/player.rs round1/round_1/game/src/player.rs
153c153
- if !self.is_alive() {
---
+ if self.health == 0 {

This was the part of the map that you needed to actually exploit and find a way to bypass, it was a wall of enemies and there is no way to pass them. The actual hack here is the fact that when you die you go into a dying animation but aren’t actually fully set to dead as before what this allows you to do is move from room to room and then enter an invulnerable state and kill the enemies. This hack is actually doable throughout the game and makes you semi unkillable - you can still die from touching any auto kill objects + if you regain any health you leave the state.
Water Temple enemy wall exploit

Sky-Temple

Sky Temple map layout

python3 2025/hackceler8/map_visualizer.py 2025/round_1/resources/maps/sky-temple/sky-temple.world
Angel: 9
Goggles: 1
HeartItem: 7
Archer: 5
SnakeNpc: 1
Sword: 1
DoorV: 5
ChestNpc: 5
MimicNpc: 7
Spawn: 1

Key Diff:

(Sprite::Anim::IdleDown, false)
};
npc.sprite.maybe_set_anim(anim as usize);
npc.sprite.flip_h = flip;
break;
}
114a130
ctx.dialogue_npc_id = Some(npc_id);
150a167,172
NpcType::ChestNpc | NpcType::MimicNpc => Hitbox {
x: 1,
y: 11,
w: 21,
h: 13,
},
181a204,276
}
fn mimic_challenge() -> Dialogue {
Dialogue::new_multiple_choice(
"I swear I'm not a mimic!\nOpen me?",
&["Yes", "No"],
Some(mimic_challenge_open),
)
}
fn mimic_challenge_opened() -> Dialogue {
Dialogue::new_no_response("This chest is already open.", None)
}
fn mimic_challenge_open(ctx: &mut Ctx, response: &str) {
if eq(response, "No") {
return;
}
if let Some(npc_id) = ctx.dialogue_npc_id {
let npc = &mut ctx.world.npcs[npc_id];
if npc.npc_type == NpcType::ChestNpc {
npc.sprite.set_anim(MimicSprite::Anim::Open as usize);
// Switch to the "already opened" dialogue.
npc.dialogue_id = 3;
ctx.start_dialogue(Dialogue::new_no_response(
"Obtained *Key* !",
Some(get_item),
));
} else {
npc.sprite.set_anim(MimicSprite::Anim::OpenMimic as usize);
ctx.start_dialogue(Dialogue::new_no_response("I LIED, BWA HA HA!", Some(lose)));
}
}
}
pub items_collected: Bitmask,
308a311,343
pub fn shuffle_chests(&mut self, rng_state: &mut u32) {
let mut filled_position = [false; 12];
let mut found_chest = false;
for npc in &mut self.npcs {
if npc.npc_type != NpcType::ChestNpc && npc.npc_type != NpcType::MimicNpc {
continue;
}
found_chest = true;
let mut pos = (Self::get_random_int(rng_state) % 10) as usize;
if filled_position.iter().all(|x| *x) {
panic!("All chest slots filled");
}
while filled_position[pos] {
pos = (pos + 1) % 12;
}
filled_position[pos] = true;
npc.set_position(128 + 1 + pos as i16 * 24, npc.y);
}
if found_chest {
for _ in 0..5 {
self.inventory.remove(ItemType::Key);
}
}
}
fn get_random_int(rng_state: &mut u32) -> u32 {
*rng_state = rng_state.wrapping_mul(1664525).wrapping_add(1013904223);
*rng_state
}

The addition here was the creation of a new NPC based on a seeded random value. It would generate five keys. When you pick up a key, the other mimics lose their keys. You have to keep resetting the state to reseed the mimics. You will die when you get it wrong, but you can use the earlier bug to reset the player trying the mimic, giving you effectively infinite attempts. Sky Temple mimic chests White dots are the mimics; the brown tiles are the doors. You need five keys to get past.

Forest-Temple

Forest Temple map layout

python3 2025/hackceler8/map_visualizer.py 2025/round_1/resources/maps/forest-temple/forest-temple.world
Goblin: 10
Boots: 2
RacoonNpc: 1
Goggles: 1
Orc: 10
HeartItem: 6
Spawn: 1
Key: 1
Sword: 1
DoorV: 3
Switch: 2

Key Diff:

  • Nothing! The bug here is a core issue in the game, or at least that’s how we solved it. If you time it correctly, you can pick up the key with 3–4 people in the room before the three doors. This is more or less an item duplication glitch by timing everything correctly. The best way we found to do this was to let one person hold the controller and pick it up for everyone at once.

Round-2 (4 Flags For Boss)

Fire-Temple

Round 2 Fire Temple map
python3 2025/hackceler8/map_visualizer.py 2025/round_2/resources/maps/fire-temple/fire-temple.world
Blob: 12
Flameboi: 9
HeartItem: 7
Knife: 1
Boots: 2
DoorH: 3
Key: 1
Sword: 1
Spawn: 1
Goggles: 1
Switch: 2
CatNpc: 1

Key Diff:

fn engrave() -> Dialogue {
Dialogue::new_free_text("Can you guess my secret?", Some(engrave_guess))
}
fn engrave_guess(ctx: &mut Ctx, response: &str) {
if eq(response, SECRET) {
ctx.start_dialogue(Dialogue::new_no_response(
"That's correct! Here's your reward.\n\nObtained *Key* !",
Some(get_item),
));
} else {
ctx.start_dialogue(Dialogue::new_free_text(
"That's not it.\n:C\n\
As consolation I can engrave your name onto the map.\nWhat's your name?",
Some(engrave_name),
));
}
}
fn engrave_name(ctx: &mut Ctx, response: &str) {
let mut tiles: Vec<TileFlags, 64> = Vec::new();
text_to_tiles(ctx, &mut tiles, response);
text_to_tiles(ctx, &mut tiles, SECRET);
ctx.vdp
.set_plane_tiles(Plane::A, 45 * 64 + 10, &tiles[0..response.len()]);
let img = &ctx.ui.inventory_text_img;
ctx.vdp
.set_tiles(img.start_tile, tileset::TILESETS[img.tiles_idx]);
let cur = ctx.world.plane_window.current_scroll();
ctx.vdp.set_h_scroll(0, &[-(cur.0 as i16), 0]);
ctx.vdp
.set_v_scroll(0, &[cur.1 as i16, image::SCREEN_V_SCROLL]);
ctx.start_dialogue(Dialogue::new_no_response(
"What a lovely name!\nI engraved it for your viewing pleasure.",
None,
));
}
fn text_to_tiles(ctx: &Ctx, tiles: &mut Vec<TileFlags, 64>, text: &str) {
let img = &ctx.ui.inventory_text_img;
for chr in text.as_bytes().iter() {
let tile = TileFlags::for_tile(
img.start_tile + ui::CHAR_TILES_INDEXES[*chr as usize] as u16,
img.palette,
);
info!("Converting {} -> {}", chr, tile.tile_index());
tiles
.push(tile)
.unwrap_or_else(|_| panic!("tile vec too small"));
}
}
fn eq(a: &str, b: &str) -> bool {
a.len() == b.len() && a.chars().zip(b.chars()).all(|(a, b)| a == b)
}
fn get_item(ctx: &mut Ctx, _: &str) {
ctx.world.inventory.add(ItemType::Key);
}

The challenge here is straightforward: we have a door without a key, and we need to get one. If you go through the diff above, you realize that the Engraver NPC in the Fire Temple can give you a key if you guess his “Secret” correctly. Obviously we don’t know it. So how do we pull it off? You can corrupt the game state and leak the key by inputting an empty field when he asks for a name to engrave. Here is the result:

Fire Temple map layout

After this it’s literally just using the key and beating the mini-bosses that are located after the door.

Water-Temple

Round 2 Water Temple map
python3 2025/hackceler8/map_visualizer.py 2025/round_2/resources/maps/water-temple/water-temple.world
Octopus: 9
Siren: 4
HeartItem: 3
Boots: 2
Key: 2
DoorH: 3
Knife: 1
Goggles: 1
Spawn: 1
DuckNpc: 1

Key Diff:

self.portal
.save_to_persistent_storage(&[0x1234, 0x5678, 0x9ABC, 0xDEF0])
if matches!(self.world.world_type, WorldType::BossTemple)
|| self.players.iter().all(|p| !p.is_active())
{
return;
}
let storage = &mut [0u16; 11];
// Save current world and map.
storage[0] = world_type_to_u16(self.world.world_type);
storage[1] = self.world.current_position.0 as u16;
storage[2] = self.world.current_position.1 as u16;
// Save player positions on the map.
for i in 0..4 {
if self.players[i].is_active() {
storage[3 + i * 2] = (self.players[i].x + 128) as u16;
storage[4 + i * 2] = (self.players[i].y + 128) as u16;
} else {
// Magic value to indicate inactive players.
storage[3 + i * 2] = 0xffff;
storage[4 + i * 2] = 0xffff;
}
}
self.portal.save_to_persistent_storage(storage)
}
fn load_persistent_state(portal: &TargetPortal) {
let mut save_buf = [0; 4];
/// Returns (starting_world, (map_x, map_y), [Option<player.start_x, player.start_y>]).
fn load_persistent_state(
portal: &TargetPortal,
) -> (WorldType, (i16, i16), [Option<(i16, i16)>; 4]) {
let mut save_buf = [0; 11];
portal.load_from_persistent_storage(&mut save_buf);
info!("Loaded data from storage: {:?}", save_buf);
let starting_world = u16_to_world_type(save_buf[0]).unwrap_or(WorldType::Overworld);
if matches!(starting_world, WorldType::Overworld) {
// Return default values as the state might not be initialized yet.
return (starting_world, (1, 1), [Some((280, 177)), None, None, None]);
}
let map_x = save_buf[1] as i16;
let map_y = save_buf[2] as i16;
let mut player_pos = [None; 4];
for i in 0..4 {
if save_buf[3 + i * 2] != 0xffff {
player_pos[i] = Some((
(save_buf[3 + i * 2] - 128) as i16,
(save_buf[4 + i * 2] - 128) as i16,
));
}
}
(starting_world, (map_x, map_y), player_pos)
}
/// Check if the server is paused and block + display a loading text until it gets unpaused.
@@ -553,6 +604,29 @@ fn wait_for_server_init(vdp: &mut TargetVdp, portal: &mut TargetPortal) {
State::clear_screen(vdp, &[Plane::A, Plane::B]);
}
fn world_type_to_u16(world_type: WorldType) -> u16 {
match world_type {
WorldType::Overworld => 0,
WorldType::FireTemple => 1,
WorldType::WaterTemple => 2,
WorldType::ForestTemple => 3,
WorldType::SkyTemple => 4,
WorldType::BossTemple => 5,
}
}
fn u16_to_world_type(val: u16) -> Option<WorldType> {
match val {
0 => Some(WorldType::Overworld),
1 => Some(WorldType::FireTemple),
2 => Some(WorldType::WaterTemple),
3 => Some(WorldType::ForestTemple),
4 => Some(WorldType::SkyTemple),
5 => Some(WorldType::BossTemple),
_ => None,
}
}

This code adds save states. You can reset the world using the hard reset button on the device; instead of going back to the very start, you revert the entire current “world”/temple. With this you can essentially double the number of keys you have access to in order to beat the Water Temple: go to the very top room. Water Temple key duplication exploit Here, you open the door. Have one person stand in front of the door and one behind. Wait 10 seconds for the current game state to save, then restart the system. One player will be able to access the key at the top of the room while the bottom key regenerates, allowing you to open the mini-boss room, which requires two keys.

Water Temple Map Hack

Sky-Temple

Round 2 Sky Temple map

python3 2025/hackceler8/map_visualizer.py 2025/round_2/resources/maps/sky-temple/sky-temple.world
Angel: 7
Knife: 1
Key: 1
HeartItem: 7
Archer: 7
SnakeNpc: 1
Sword: 1
DoorV: 1
Spawn: 1
DoorH: 1

Key Diff:

ItemType::Knife => {
player.strength *= 2;
}
ItemType::Boots => {
player.speed += crate::player::SPEED_SCALE_FACTOR;
}
@@ -147,9 +156,12 @@ impl InventoryItem {
/// Remove the item's effects from a player it was previously applied to.
pub fn remove_effect(&self, player: &mut Player) {
match self.item_type {
ItemType::Knife => {
player.strength /= 2;
}
--- a/game/src/projectile.rs
+++ b/game/src/projectile.rs
@@ -97,6 +97,16 @@ impl Projectile {
break;
}
}
for enemy in &mut ctx.world.enemies {
if !enemy.is_alive() || enemy.id == projectile.shooter_id {
continue;
}
if projectile.hitbox().collides(&enemy.hitbox()) {
enemy.on_hit(push_dir, projectile.damage);
projectile.status = Status::Dead;
break;
}
}

The exploit for this temple kinda set a president going forward regarding multiplicative vs additive bugs. The whole idea here is that your knife will 2x your damage and then 1/2 your damage, ie acting as a multiplicative increase in your weapons damage. In comparison the knife is a +1 /-1 toggle, what you can do here as such is equip the knife, then equipped the sword so you are at (1*2)+1 and then you remove the knife so you go from 3 to 1.5 then you remove the sword and you go to 0 cuz the game rounds down. This then lets you abuse the second part of the patch shown above and that is projectiles are now able to hit enemies. This allows you to move the archer in the same room to face the boss. Since you do no damage you can keep hitting the enemy till he is facing or angled towards the boss. Sky Temple archer positioning

  • So here the enemy would be facing the boss

Forest-Temple

Round 2 Forest Temple map

  • ignore the invisible keys thats for later
python3 2025/hackceler8/map_visualizer.py 2025/round_2/resources/maps/forest-temple/forest-temple.world
Goblin: 7
Boots: 2
RacoonNpc: 1
Knife: 1
Orc: 7
HeartItem: 6
Spawn: 1
Key: 2
Sword: 1
DoorV: 1
Switch: 1

Key Diff:

/// NPCs are friendly entities on the map that the player can talk to.
pub struct Npc {
pub x: i16,
@@ -88,6 +99,7 @@ impl Npc {
{
continue;
}
+ ctx.dialogue_player = p;
dialogue_id = Some(npc.dialogue_id);
// Face the player.
let center = hitbox.center();
@@ -181,6 +193,137 @@ fn bark_bark() -> Dialogue {
Dialogue::new_no_response("BARK BARK!", None)
}
+fn fashion_advice() -> Dialogue {
+ Dialogue::new_multiple_choice(
+ "Would you like to hear my expert opinion on which item you should wear?",
+ &["Yes", "No"],
+ Some(fashion_advice_2),
+ )
+}
+
+fn fashion_advice_2(ctx: &mut Ctx, response: &str) {
+ if eq(response, "No") {
+ return;
+ }
+
+ let fashion_id = get_random_wearable_id(ctx);
+ if fashion_id.is_none() {
+ ctx.start_dialogue(Dialogue::new_no_response(
+ "It doesn't look like you have anything to wear.",
+ None,
+ ));
+ return;
+ }
+ let fashion_id = fashion_id.unwrap();
+
+ // Unequip previous items and equip the fashionable one. Looking sharp!
+ for i in &mut ctx.world.inventory.items {
+ if i.worn_by == Some(ctx.dialogue_player) {
+ i.remove_effect(&mut ctx.players[ctx.dialogue_player]);
+ i.worn_by = None;
+ }
+ }
+ let fashion_item = &mut ctx.world.inventory.items[fashion_id];
+ fashion_item.apply_effect(&mut ctx.players[ctx.dialogue_player]);
+ fashion_item.worn_by = Some(ctx.dialogue_player);
+
+ let mut text: String<64> = String::new();
+ let _ = write!(
+ text,
+ "<{}> definitely looks best on you!",
+ fashion_item.display_name()
+ );
+ ctx.start_dialogue(Dialogue::new_no_response(&text, None))
+}
+
+fn get_random_wearable_id(ctx: &mut Ctx) -> Option<usize> {
+ let wearable_count = ctx
+ .world
+ .inventory
+ .items
+ .iter()
+ .filter(|i| InventoryItem::is_wearable(i.item_type))
+ .count();
+ if wearable_count == 0 {
+ return None;
+ }
+
+ let pos = ctx.portal.get_random_int() as usize % wearable_count;
+ let mut i = 0;
+ for item in &mut ctx.world.inventory.items {
+ if InventoryItem::is_wearable(item.item_type) {
+ if i == pos {
+ return Some(i);
+ }
+ i += 1;
+ }
+ }
+
+ None
+}

So the idea here is that we have an NPC that is able to equip and de-equip items from our character and there is a physical wall stoping our character from moving to the next section where the boss is. In other words we are going to need to perform some kinda wall clip in order to get to the mini boss and given the fact that we have an NPC who can add or remove items we know that this is going to be a speed related exploit. To put it simply the way this actually works is peaty simple, we can use two characters to talk to the npc and continually swap who actually has the “boots” but when the NCP removes the boots form character A and moves it to Character B, we don’t actually lose the speed buff since we don’t perform a proper de-equip allowing you to infinitely stack the speed buff. This allows you to then direclty clip through walls if you have enough speed or glitch into the side of a wall at a translation state and move through the walls till you reach the boss.

Round-3 (4 Flags For Boss)

Fire-Temple

Round 3 Fire Temple map
python3 2025/hackceler8/map_visualizer.py 2025/round_3/resources/maps/fire-temple/fire-temple.world
Blob: 9
Flameboi: 9
CatNpc: 1
HeartItem: 7
Goggles: 2
Boots: 2
DoorH: 2
Key: 1
Sword: 1
Spawn: 1
Switch: 2

Key Diff:

Fire Temple boss with 31337 health

The boss he had 31337 in health the only thing you had to to beat the boss is keep hitting him till you got him to a transition room and he will die. Boss apparently ends up dying on transition states.

Sky-Temple

Round 3 Sky Temple map
python3 2025/hackceler8/map_visualizer.py 2025/round_3/resources/maps/sky-temple/sky-temple.world
Angel: 12
Boots: 1
HeartItem: 7
Archer: 6
SnakeNpc: 1
Sword: 1
Cloak: 1
DoorV: 1
Switch: 8
Spawn: 1

Key Diff:

diff -r hackceler8/game/src/enemy/mod.rs round_3/game/src/enemy/mod.rs
550c563
- if !player.is_active() {
---
+ if !player.is_active() || player.is_cloaked {
diff -r hackceler8/game/src/inventory.rs round_3/game/src/inventory.rs
24c24,29
- const WEARABLE_ITEMS: &[ItemType] = &[ItemType::Boots, ItemType::Goggles, ItemType::Sword];
---
+ const WEARABLE_ITEMS: &[ItemType] = &[
+ ItemType::Boots,
+ ItemType::Goggles,
+ ItemType::Sword,
+ ItemType::Cloak,
+ ];
125a131
+ ItemType::Cloak => "Cloak",
142a149,151
+ ItemType::Cloak => {
+ player.is_cloaked = true;
+ }
165a175,177
+ ItemType::Cloak => {
+ player.is_cloaked = false;
+ }
229c241
- if players[player_id].is_alive() && input.just_pressed(Button::B) {
---
+ if input.just_pressed(Button::B) {

How this works is fairly simple: you are given the cloak object here, which enables one of your characters to basically become invisible and remove enemy aggro. As such, you are going to want to get both the boots and cloak in the map and give them to one person. The first problem you are going to see is that to even get to the boss room you are going to need to spawn 4 players, which is then followed by a room with 3 very fast, high-damage, invulnerable enemies. The trick is to use the character with the boots and cloak to rush middle, and then decloak to retake enemy aggro. You can use pause buffering in order to make this easier. From there the basic idea is to keep one character at the top of the cross and keep cloaking and decloaking while sprinting with the bottom characters to get to the next room. After you reach the boss room it’s more or less the same idea; you can kill the minions, but you need to use the pressure plates/switches to actually kill the boss. Do the same thing where you mess around with boss aggro using the cloak.

Forest-Temple

Round 3 Forest Temple map
python3 hackceler8/map_visualizer.py round_3/resources/maps/water-temple/water-temple.world
Octopus: 14
Siren: 3
HeartItem: 3
Boots: 2
Sword: 2
Spawn: 1
DuckNpc: 1
DoorH: 1

Key Diff:

229c241
- if players[player_id].is_alive() && input.just_pressed(Button::B) {
---
+ if input.just_pressed(Button::B) {

The change is fairly simple and the exploit is also preaty easy to understand. When a player is now dying they can still change the menu and equip items. What this allows you as the player to do is die and and then equip the googles just in time to wall clip into the boss room. From there you just kill the boss, timing and character placement are required to pull this off.

Round-4 (4 Flags For Boss)

Fire-Temple

Round 4 Fire Temple map
``` python3 hackceler8/map_visualizer.py round_4/resources/maps/fire-temple/fire-temple.world Blob: 8 CatNpc: 1 Key: 2 HeartItem: 6 Staff: 1 Flameboi: 11 DoorH: 2 Sword: 1 Spawn: 1 Goggles: 1 Switch: 2 ```

Key Diff:

diff -r hackceler8/game/src/inventory.rs round_4/game/src/inventory.rs
24c24,30
- const WEARABLE_ITEMS: &[ItemType] = &[ItemType::Boots, ItemType::Goggles, ItemType::Sword];
---
+ const WEARABLE_ITEMS: &[ItemType] = &[
+ ItemType::Boots,
+ ItemType::Goggles,
+ ItemType::Sword,
+ ItemType::Staff,
+ ItemType::Pencil,
+ ];
85a92,99
+ // Checks if there's at least one of the specified item type
+ // equipped by the specified player.
+ pub fn contains_equipped(&self, item_type: ItemType, player_id: usize) -> bool {
+ self.items
+ .iter()
+ .any(|i| i.item_type == item_type && i.worn_by == Some(player_id))
+ }
+
93a108,117
+
+ /// Remove the staff from the inventory and unequip it from players.
+ pub fn remove_staff(&mut self, players: &mut [Player]) {
+ for i in &mut self.items {
+ if i.item_type == ItemType::Staff {
+ i.unequip(players);
+ }
+ }
+ self.items.retain(|i| i.item_type != ItemType::Staff);
+ }

The exploit/bug here is how the actual staff ends up working. It’s a single-use item that is supposed to disappear when it gets used. The real issue that exists in the code here, though, is the fact that it doesn’t actually leave your inventory until the staff animation ends. What this allows you to do is chain it to be reused by multiple players. This can then be used to help break through the ranged enemy wall of flame blobs. I will note that you don’t necessarily need to use this bug since, if you have all 4 characters and time it well, you can simply brute-force your way to the solve here by just having players behave as tanks.

Water-Temple

Round 4 Water Temple map
``` python3 hackceler8/map_visualizer.py round_4/resources/maps/water-temple/water-temple.world Octopus: 8 Siren: 5 DoorV: 1 HeartItem: 3 Boots: 2 Key: 2 Staff: 1 Goggles: 1 Spawn: 1 DuckNpc: 1 DoorH: 1 ```
Water Temple siren kill exploit

Key Diff:

diff -r hackceler8/game/src/inventory.rs round_4/game/src/inventory.rs
24c24,30
- const WEARABLE_ITEMS: &[ItemType] = &[ItemType::Boots, ItemType::Goggles, ItemType::Sword];
---
+ const WEARABLE_ITEMS: &[ItemType] = &[
+ ItemType::Boots,
+ ItemType::Goggles,
+ ItemType::Sword,
+ ItemType::Staff,
+ ItemType::Pencil,
+ ];
85a92,99
+ // Checks if there's at least one of the specified item type
+ // equipped by the specified player.
+ pub fn contains_equipped(&self, item_type: ItemType, player_id: usize) -> bool {
+ self.items
+ .iter()
+ .any(|i| i.item_type == item_type && i.worn_by == Some(player_id))
+ }
+
93a108,117
+
+ /// Remove the staff from the inventory and unequip it from players.
+ pub fn remove_staff(&mut self, players: &mut [Player]) {
+ for i in &mut self.items {
+ if i.item_type == ItemType::Staff {
+ i.unequip(players);
+ }
+ }
+ self.items.retain(|i| i.item_type != ItemType::Staff);
+ }

The idea here is you use the staff to kill the siren that is on the middle, since the staff can be used twice since it doesn’t leave your inventory till its animation ends you can take 2. This allows you to kill the siren fairly easily by having player 1 shot the staff then give it to player 2 and they then shot the staff all fairly trivial.

Sky-Temple

Round 4 Sky Temple map
python3 hackceler8/map_visualizer.py round_4/resources/maps/sky-temple/sky-temple.world
Angel: 8
Goggles: 1
Key: 1
HeartItem: 7
Archer: 6
SnakeNpc: 1
Sword: 1
DoorV: 1
Spawn: 1

Key Diff:

diff -r hackceler8/game/src/projectile.rs round_4/game/src/projectile.rs
22d21
- use crate::map;
27a27,28
+ /// Special shooter ID that indicates this was fired by a player.
+ pub const PLAYER_SHOOTER_ID: u16 = 1337;
90,91c91,116
- for player in &mut ctx.players {
- if !player.is_active() {
---
+ if projectile.shooter_id == PLAYER_SHOOTER_ID {
+ // Shot by player, damages enemies
+ for enemy in &mut ctx.world.enemies {
+ if !enemy.is_alive() {
+ continue;
+ }
+ if projectile.hitbox().collides(&enemy.hitbox()) {
+ enemy.on_hit(push_dir, projectile.damage);
+ projectile.status = Status::Dead;
+ break;
+ }
+ }
+ } else {
+ for player in &mut ctx.players {
+ if !player.is_active() {
+ continue;
+ }
+ if projectile.hitbox().collides(&player.hitbox()) {
+ player.on_hit(push_dir, projectile.damage);
+ projectile.status = Status::Dead;
+ break;
+ }
+ }
+ }
+ for enemy in &mut ctx.world.enemies {
+ if !enemy.is_alive() || enemy.id == projectile.shooter_id {
94,95c119,120
- if projectile.hitbox().collides(&player.hitbox()) {
- player.on_hit(push_dir, projectile.damage);
---
+ if projectile.hitbox().collides(&enemy.hitbox()) {
+ enemy.on_hit(push_dir, projectile.damage);
104c129,132
- || map::off_screen(projectile.x, projectile.y)
---
+ || projectile.x - 100
+ || projectile.x + 460
+ || projectile.y - 100
+ || projectile.y + 370

This map was pretty simple - basically, you can clip the arrow from the enemy on the left here. If you time it correctly, the arrow will move from the first map to the second one. I don’t fully remember what the timing requirements were, but there was a pretty straightforward trick that you can figure out after trying it for a few minutes. Sky Temple arrow clipping exploit

Forest-Temple

Round 4 Forest Temple map
python3 hackceler8/map_visualizer.py round_4/resources/maps/forest-temple/forest-temple.world
Goblin: 9
Boots: 1
RacoonNpc: 1
Goggles: 1
Orc: 9
HeartItem: 6
Spawn: 1
Key: 2
Sword: 1
DoorH: 1
DoorV: 1
Switch: 2
Staff: 1

Key Diff:

diff -r hackceler8/game/src/enemy/mod.rs round_4/game/src/enemy/mod.rs
50a51,55
+ // Slowdown happens every 1s for 1s.
+ pub const SLOWDOWN_FREQUENCY: u16 = 50;
+ // Players are slow down to 1/4 of normal speed
+ pub const SLOWDOWN_RATE: i16 = 4;
+
73a79,80
+ pub slows_down: bool,
+ pub shoots_down: bool,
110a118,119
+ slows_down: bool,
+ shoots_down: bool,
124a134
+ SlowingDown,
172a183,184
+ slows_down: properties.slows_down,
+ shoots_down: properties.shoots_down,
270c282,284
- if enemy.stats.shoots && ctx.frame % shooter::SHOOT_FREQUENCY == 0 {
---
+ if enemy.stats.shoots
+ && (enemy.shoots_down || ctx.frame % shooter::SHOOT_FREQUENCY == 0)
+ {
281a296,306
+
+ if enemy.slows_down && ctx.frame % SLOWDOWN_FREQUENCY == 0 {
+ for p in 0..ctx.players.len() {
+ let player = &mut ctx.players[p];
+ if !player.is_active() {
+ continue;
+ }
+ player.speed /= SLOWDOWN_RATE;
+ }
+ enemy.status = Status::SlowingDown;
+ }
290a316,320
+ let (dx, dy) = if enemy.shoots_down {
+ (0, 100)
+ } else {
+ (player.x - enemy.x, player.y - enemy.y)
+ };
295,296c325,326
- player.x - enemy.x,
- player.y - enemy.y,
---
+ dx,
+ dy,
315a346,357
+ }
+ Status::SlowingDown =+ {
+ if ctx.frame % SLOWDOWN_FREQUENCY == 0 {
+ for p in 0..ctx.players.len() {
+ let player = &mut ctx.players[p];
+ if !player.is_active() {
+ continue;
+ }
+ player.speed *= SLOWDOWN_RATE;
+ }
+ enemy.status = Status::Idle;
+ }

The way this bug works is rather simple: we have certain enemies that actually slow down the character by 4x, but after a second it will result in speed of 4x. What you can actually do is fairly simple - instead of just doing nothing, you equip the boots during the slowdown. This allows you to increase your speed by +1, so when the 4x hits, the equation changes from (x * 0.25) * 4 to ((x * 0.25) + 1) * 4. If you keep using this exploit, you can get your speed high enough that you are able to clip through the wall and get to the boss room. Note that it’s important to make sure you save an account for spawning into the boss room just in case you clip into a weird section of the map.

Round-5 (4 Flags For Boss)

Fire-Temple

Round 5 Fire Temple map
python3 hackceler8/map_visualizer.py round_5/resources/maps/fire-temple/fire-temple.world
Blob: 10
Flameboi: 8
CatNpc: 1
HeartItem: 6
Goggles: 2
DoorH: 2
Key: 1
Sword: 1
Spawn: 1
Switch: 6

Key Diff:

diff -r hackceler8/game/src/switch.rs round_5/game/src/switch.rs
17a18
+ use crate::enemy::EnemyProperties;
19a21
+ use crate::res::enemies::EnemyType;
21a24,25
+ use crate::walk::*;
+ use crate::Enemy;
26a31
+ spawn_enemies, // 2
159a165,212
+ }
+ }
+
+ const SPAWNED_ENEMY_WALK_DATA: &[WalkData] = &[
+ WalkData {
+ cmd: Cmd::Right,
+ dur: 200,
+ },
+ WalkData {
+ cmd: Cmd::Left,
+ dur: 200,
+ },
+ ];
+ const SPAWNED_ENEMY_PROPS: &EnemyProperties = &EnemyProperties {
+ walk_data: SPAWNED_ENEMY_WALK_DATA,
+ speed: None,
+ health: None,
+ strength: Some(0),
+ invulnerable: false,
+ flags: None,
+ };
+ fn spawn_enemies(ctx: &mut Ctx) {
+ if !ctx.players[0].is_active() || ctx.world.enemies.len() >= 28 {
+ return;
+ }
+
+ for (dx, dy) in [(16, -8), (16, 8)] {
+ let player_center = ctx.players[0].hitbox().center();
+ let enemy = Enemy::new(
+ EnemyType::Flameboi,
+ player_center.0 + dx - 128,
+ player_center.1 + dy - 128,
+ /*id=*/ 1337,
+ &SPAWNED_ENEMY_PROPS,
+ &mut ctx.res_state,
+ &mut ctx.vdp,
+ );
+ ctx.world
+ .enemies
+ .push(enemy)
+ .unwrap_or_else(|_| panic!("too many enemies"));
+ }
+
+ for switch_id in 0..ctx.world.switches.len() {
+ let switch = &mut ctx.world.switches[switch_id];
+ switch.move_relative(0, if switch.y % 16 == 0 { -17 } else { 17 });
+ ctx.world.switches_completed.clear(switch.id);

If you have all 4 characters stand on the switches it will create enemies that do like 0 damage, you can then us them to go into the boss room. The large number of enemies will greatly slow down the enemy slimes that are moving in a pattern till you finally reach the boss. It is possible to solve this without any help/slowing down the enemies as well but they definitely help. Note that the enemies spawn on your first player so make sure they are right next to the boss room to make it easier. The enemies will struggle to properly go to the next area you want to wait till they are half way to the next section so you wait till you can move to the next zone.

Water-Temple

Water Temple maze solution 1 Water Temple maze solution 2 Water Temple maze solution 3 There is a maze, you build the maze, solve the maze, follow the path !

Sky-Temple

Round 5 Sky Temple map
python3 hackceler8/map_visualizer.py round_5/resources/maps/sky-temple/sky-temple.world
Goggles: 1
Key: 1
HeartItem: 6
Archer: 5
Angel: 4
SnakeNpc: 1
Sword: 1
DoorV: 1
Spawn: 1
DoorH: 1

Key Diff:

diff -r hackceler8/game/src/game.rs round_5/game/src/game.rs
500,502c508,530
- fn save_persistent_state(&mut self) {
- self.portal
- .save_to_persistent_storage(&[0x1234, 0x5678, 0x9ABC, 0xDEF0])
---
+ pub fn save_persistent_state(&mut self) {
+ const SAVE_BUF_LEN: usize = crate::inventory::MAX_ITEMS + 1;
+ let mut save_buf = [0; SAVE_BUF_LEN];
+
+ let inventory_len = self.world.inventory.items.len();
+ if !matches!(self.world.world_type, WorldType::Overworld)
+ && !matches!(self.world.world_type, WorldType::ForestTemple)
+ {
+ let items_to_save = self
+ .world
+ .inventory
+ .items
+ .iter()
+ .skip(inventory_len.saturating_sub(crate::inventory::MAX_ITEMS));
>
+ for (i, item) in items_to_save.enumerate() {
+ let item_type_bits = item_type_to_u16(item.item_type);
+ let amount_bits = item.amount & 0x1FFF;
+ save_buf[i] = (item_type_bits << 13) | amount_bits;
+ }
+ }
+ save_buf[crate::inventory::MAX_ITEMS] = world_type_to_u16(self.world.world_type);
+ self.portal.save_to_persistent_storage(&save_buf);
505,506c533,537
- fn load_persistent_state(portal: &TargetPortal) {
- let mut save_buf = [0; 4];
---
+ fn load_persistent_state(
+ portal: &TargetPortal,
+ ) -> (WorldType, [u16; crate::inventory::MAX_ITEMS]) {
+ const SAVE_BUF_LEN: usize = crate::inventory::MAX_ITEMS + 1;
+ let mut save_buf = [0; SAVE_BUF_LEN];
508a540,547
>
+ u16_to_world_type(save_buf[crate::inventory::MAX_ITEMS])
+ .map(|world_type| {
+ let inventory_data: [u16; crate::inventory::MAX_ITEMS] =
+ save_buf[..crate::inventory::MAX_ITEMS].try_into().unwrap();
+ (world_type, inventory_data)
+ })
+ .unwrap_or((WorldType::Overworld, [0; crate::inventory::MAX_ITEMS]))
510a550,564
+ fn load_inventory_data(&mut self, inventory_data: &[u16; crate::inventory::MAX_ITEMS]) {
+ for &item_data in inventory_data {
+ if item_data == 0 {
+ continue;
+ }
+ let item_type_bits = item_data >> 13;
+ let amount = item_data & 0x1FFF;
+ if let Some(item_type) = u16_to_item_type(item_type_bits) {
+ for _ in 0..amount {
+ self.world.inventory.add(item_type);
+ }
+ }
+ }
+ }
+
553a608,653
+ }
+
+ fn item_type_to_u16(item_type: ItemType) -> u16 {
+ match item_type {
+ ItemType::Boots => 0,
+ ItemType::Goggles => 1,
+ ItemType::HeartItem => 2,
+ ItemType::Key => 3,
+ ItemType::Sword => 4,
+ ItemType::InvisibleKey => 5,
+ }
+ }
+
+ fn u16_to_item_type(val: u16) -> Option<ItemType> {
+ match val {
+ 0 => Some(ItemType::Boots),
+ 1 => Some(ItemType::Goggles),
+ 2 => Some(ItemType::HeartItem),
+ 3 => Some(ItemType::Key),
+ 4 => Some(ItemType::Sword),
+ 5 => Some(ItemType::InvisibleKey),
+ _ => None,
+ }
+ }
+
+ fn world_type_to_u16(world_type: WorldType) -> u16 {
+ match world_type {
+ WorldType::Overworld => 0,
+ WorldType::FireTemple => 1,
+ WorldType::WaterTemple => 2,
+ WorldType::ForestTemple => 3,
+ WorldType::SkyTemple => 4,
+ WorldType::BossTemple => 5,
+ }
+ }
+
+ fn u16_to_world_type(val: u16) -> Option<WorldType> {
+ match val {
+ 0 => Some(WorldType::Overworld),
+ 1 => Some(WorldType::FireTemple),
+ 2 => Some(WorldType::WaterTemple),
+ 3 => Some(WorldType::ForestTemple),
+ 4 => Some(WorldType::SkyTemple),
+ 5 => Some(WorldType::BossTemple),
+ _ => None,
+ }

The patch here ends up adding a custom state saving system which will keep your world state whenever you hard rest the game using the physical restart button. It will keep the items in your inventory while also restarting the world which will result in the ability to open the first door, restart then open the second door.

Forest-Temple

Round 5 Forest Temple map
python3 hackceler8/map_visualizer.py round_5/resources/maps/forest-temple/forest-temple.world
Goblin: 9
Boots: 3
RacoonNpc: 1
Goggles: 2
Orc: 9
HeartItem: 6
Spawn: 1
Sword: 1
DoorV: 2
Switch: 2

Key Diff:

diff -r hackceler8/game/src/inventory.rs round_5/game/src/inventory.rs
22c22
- const MAX_ITEMS: usize = 16;
---
+ pub const MAX_ITEMS: usize = 16;
126a127
+ ItemType::InvisibleKey => "Key",
diff -r hackceler8/game/src/player.rs round_5/game/src/player.rs
315a316,327
+ break;
+ }
+ }
+ }
+ if input.just_pressed(Button::B) && inventory.contains(ItemType::InvisibleKey) {
+ // Open a nearby door.
+ let interaction_hitbox = player.hitbox().expand(5);
+ for door in doors.iter_mut() {
+ if !door.open && interaction_hitbox.collides(&door.hitbox()) {
+ door.open();
+ doors_opened.set(door.id);
+ inventory.remove(ItemType::InvisibleKey);

The patch here ends up adding invisible keys, they are dynamically loaded by the actual rust code base so they aren’t going to be in the actual Tile Map, as such I needed to hard code the location values not the my python generation script. The location isn’t really hidden or obfuscated in any manner, they are both right next to other objects. The weird part about this round though is you need to be holding nothing when you try and pick up the keys. For some reason when you pick up any other item it would make the keys impossible to pick up, but besides that it was fairly easy.

Blog / Non Technical

Holy yap! That was a lot of technical nonsense, overall I really like the game and had quite a bit of fun playing and doing it all. I totally would do it again and hopefully can next year ! But beyond that let me explain how hte actual onsite went and all the stupid shit we did.

Day 1

First day was supposed to be one day before the actula competion started everyone on our team was going ot get there and hopefully we did some levle of prep. In realitly it turnedo uto tb e just 3 of us becuase one of our players cough cough braydenfreakchu ended up getting total screwed by flights and we didn’t actually see him till the end of the next day AFTER all the technical stuff for the first round happened.

Anway bitching aide I had lots of really good food from the get go lol, the basic strcuture of the entire day was just me landing —> eat lunch —> do some work + get my room + wait for teammates —> go to dinner —> more work. Honestly pretty chill day but yall get to see the food we ate : )

Day 2

I slept pretty early and woke up around 3—4 AM, then started practicing and getting decent at the game. I had looked at it a bit before, but most of my progress came during the competition itself. I tried to find some bugs and issues in the codebase --- and found some funny ones that I didn’t think would ever trigger. One of them was a bug where you could crash the game if you were too close to the projectiles, causing one of the internal values to become 0 and trigger a Rust panic. Surprisingly, we actually managed to trigger it once by accident. Anyway, the hotel provided breakfast --- it was a really nice buffet, to be honest.

After eating, we walked down to the venue, which was only about five minutes away from the hotel --- pretty convenient. The actual building had this cool orange Hispanic tone; I believe it was a wedding or event space, so it looked really nice. Inside, the entire venue felt like a gaming competition --- smoke machines, LED lights, electronic music, gaming chairs, and of course, energy drinks!

The first day had a total of three rounds, running from about 9:30 AM to 4 PM. Each round lasted an hour and a half, with breaks in between. During that time, we were trying to find someone to fill in for our missing fifth teammate. Even if they didn’t help with the technical parts of the competition, it would’ve been nice to just have a physical section player in order to be able to do all the challenges properly. Sadly, that didn’t happen.

First round as such went alright but we ended up being unable to get one of the wall clip challenges and ended near the bottom of the leaderboard. Speed wise were doing alright but it was still a total struggle, I think from here onward every other team just started to get the game more and more and speeded really started to make a difference.

Anyway after a short coffee break we did round two which went pretty bad honestly lmafo. We missed a lot of the bugs to start off with, that being said our teammate ended up failing on his chair and had divine intervention which resulted in him figuring out the answers apparently (clovis moment)

From there we had the final round which went alright, there was a section that required 4 people which we some had managed to do by doing a lot of frame buffering/pausing. The round was alright but we still didn’t do great. We spent the rest of the day just working after ended up meeting and meeting up with our favorite dumbass Brayden. I spent a good chunk of the night just making the hack client as well as playing through the rounds for this day with Brayden as practice. Clovis and Contron where doing it with each other, we also managed to order like 50 tocos for like 20 bucks high key est purchase of the trip.

Here is some cool photos from the venue: Competition venue breakfast

  • Breakfast Competition venue floor
  • The actual floor Competition venue exterior
  • The outside 300x400
  • our fucking logo

Team logo

  • single best part of the competion Team logo
  • The actual work station

Day 3

This weekend we had two rounds. From there, we basically woke up and did the same thing as before --- same breakfast and all. The round lineup was SLICES (us), the THC merger, FMC, and Kalmarunion. During the first round, we ended up missing the spot for the next round by just about 30 seconds, which was pretty heartbreaking --- but it is what it is. After that, three of us stayed to do the next round, which we managed to fully beat, but yeah… it was too little, too late. Overall though, I had a lot of fun at the competition, and I totally think we can do well next year!

Now it was time for travel photos, lmao. After the rounds ended, it was around midday. Luckily for us, Google booked the hotels in literally the best spot - there were tons of monuments, art museums, and just an overall really nice area. Brayden and I decided to start side-questing and exploring around Mexico, so… random photos incoming:

Team member Corgo

Team member Corgo

Team member Corgo

Team member Corgo

  • ITS CORGO !!! (for those of you who don’t know corgo, he is our pwn/web goat and mildly a furry with a dog avatar lol)

Day 4

Me and Brayden went on a side quest again and went on a hot air ballon tour early in the moring and then went to the finals venue:

Hot air balloon tour 1
Hot air balloon tour 2
Hot air balloon tour 3

Kalmarunion victory celebration

  • photo right after Kalmarunion won !

Final Remarks

Had a lot of fun, life is great ! My teammates are all amazing and its great to see them ! Can’t wait to win next year XD