Ko-Wahi Wastes
Published
After playing through MNOG 2 I got curious how exactly the maze works, and if there are any tricks to solving it. In the process I discovered a rare bug and ended up providing a few different patches for the game.
The only prior work I could find is this BZPower post from 2014, which shows the code but didn’t explain it in detail. So I decided to use my computer powers and dig into the code myself :3
How the maze is generated
The code is located in KokoroWastes.swf (scripts, frame 4, DoAction [3]), decompiled with
JPEXS, and repeated here, split up into several parts along with an explanation and visual example of what each part does. It defines a createMaze function, which is called once when entering the maze for the first time, and the maze is saved until you close the game.
All code in this section is from “Mata Nui Online Game II: The Final Chronicle” by Templar Studios, LEGO.
Step 1: Setup
unlinkedNodes = [];
var i = 1;
while(i < 10) {
unlinkedNodes.push({ name: "Scene" + i, direction: "north", link: "" });
unlinkedNodes.push({ name: "Scene" + i, direction: "south", link: "" });
unlinkedNodes.push({ name: "Scene" + i, direction: "east", link: "" });
unlinkedNodes.push({ name: "Scene" + i, direction: "west", link: "" });
i++;
}
finalResults = [];As a first step, it creates nodes for each of the 4 cardinal directions for every scene. As the loop variable i starts at 1 and excludes 10, it creates 9 scenes, resulting in a list of 36 total nodes. These nodes represent the different directions you can walk in and will be linked to other nodes.

Step 2: Main Entrace
startNode = unlinkedNodes[random(unlinkedNodes.length)];
startNode.link = { name: "KokoroWastesEntrance" };
var j = 0;
while(j < unlinkedNodes.length) {
if(unlinkedNodes[j].name == startNode.name and unlinkedNodes[j].direction == startNode.direction) {
unlinkedNodes.splice(j, 1);
j--;
break;
}
j++;
}
finalResults.push(startNode);Next it picks a random node and links that to the main entrance (KokoroWastesEntrance). This node is then removed from the list of unlinked nodes and added to the list of processed ones.

Step 3: Beginning the maze
var picking = 1;
while (picking == 1) {
fromNode = unlinkedNodes[random(unlinkedNodes.length)];
if (fromNode.name == startNode.name) {
picking = 0;
}
}Now that it has a main entrance, it can start linking different nodes to each other and make a maze! To begin with, it chooses a random “from” node that is on the same scene as the main entrance, and remembers that for later.
Step 4: The Loop™
safetyCount = 0;
while (unlinkedNodes.length > 1) {With that, it enters the main loop. As long as there is more than 1 unlinked node left, it keeps repeating the following steps:
Loop step 1: Finding a target
var toNode = unlinkedNodes[random(unlinkedNodes.length)];
safetyCount++;
if (safetyCount > 10 and (toNode.name != fromNode.name or toNode.direction != fromNode.direction)) {
safetyCheck = 1;
} else {
safetyCheck = 0;
}
if (toNode.name != fromNode.name or safetyCheck == 1) {It picks a random “to” node. The rest of the main loop, however, is ideally only executed if the “from” and “to” nodes are on different scenes. As a precaution, the safetyCheck allows it to bypass that condition, if it failed to find a suitable node at least ten times. But even then it still ensures that a node is never linked to itself.
Loop step 2: Making the connection
safetyCount = 0;
fromNode.link = toNode;
toNode.link = fromNode;
finalResults.push(fromNode);
finalResults.push(toNode);
var j = 0;
while (j < unlinkedNodes.length) {
if (unlinkedNodes[j].name == fromNode.name and unlinkedNodes[j].direction == fromNode.direction) {
unlinkedNodes.splice(j, 1);
j--;
} else if(unlinkedNodes[j].name == toNode.name and unlinkedNodes[j].direction == toNode.direction) {
unlinkedNodes.splice(j, 1);
j--;
}
j++;
}Once it found two nodes it’s happy with, it links them to each other, and removes them from the unlinked list.
Loop step 3: Keep threading
var picking = 1;
while (picking == 1) {
fromNode = unlinkedNodes[random(unlinkedNodes.length)];
if (fromNode.name == toNode.name) {
picking = 0;
}
}At the end of the loop, it picks a new random “from” node. One critical detail here, is that this new “from” node must be on the same scene as the previous “to” node.
Visualising the loop
A nice way to visualise this loop is to imagine threading a string through the 9 scenes. Beginning on the scene with the main entrance, going out a random direction, ideally to another scene, entering one direction and leaving another, until all scenes are tightly threaded together. This also ensures that the entire maze is connected and you can always reach every scene.





Step 5: First exit
}
}
lastNode = unlinkedNodes[0];
lastNode.link = { name: "KantaisEntrance" };
finalResults.push(lastNode);And finally, it closes the main loop. Because it started with an even number of nodes, picked one out as the main entrance, and then linked pairs together, there is guaranteed to be exactly one unlinked node left. This last node is linked to Kantai’s hut (KantaisEntrance), where you can train willpower with the balancing minigame.

Step 6: More exits
selectingNode = 1;
while (selectingNode == 1) {
secondExitNode = finalResults[random(finalResults.length)];
if (secondExitNode.link != "start" and secondExitNode.link != "exit") {
selectingNode = 0;
thirdExitNode = secondExitNode.link;
secondExitNode.link = { name: "KokoroRuinsApproach" };
thirdExitNode.link = { name: "KokoroWastesDeadEnd" };
}
}
return finalResults;And finally, it actually undoes one of the previous pairs, instead linking one node to the temple (KokoroRuinsApproach), where you get the Crystal of Peace, and the other to the cliffside (KokoroWastesDeadEnd), where you get the Charm of Willpower. As a safeguard it checks and retries if it would undo the link to the main entrance or to Kantai’s hut.


Summary
The maze is generated completely randomly. The exits could be on the same scene as the main entrance, or they could be on the complete other side of the maze. Multiple exits and loops within the maze mean you have to check every direction on every scene. Flags only last 2 minutes, causing scenes you’ve been through before to look like brand new scenes. It’s bad, y’all. :(
Errors in the algorithm
But wait, it gets worse! :)
There is a small chance that step 6 actually splits the maze into two groups, with no connections between them. While highly unlikely, the result would be an unsolvable maze. Two of the four different destinations would be completely unreachable until the maze is regenerated.

Errors in the code
But wait, it gets worse! :)
The safeguard from step 6 is actually ineffective because it uses the wrong names (start and exit instead of KokoroWastesEntrance and KantaisEntrance), allowing it to overwrite the main entrance or the exit to Kantai’s hut. The consequences of that depend on where you enter the maze from.

The cliffside and whichever of the main entrance or Kantai’s hut got overwritten become unlinked from the maze.
Should you enter the maze from a still linked location, nothing immediately obvious happens. The game continues to run normally, but the unlinked locations become unreachable.
Should you enter the maze from one of the two unlinked locations, the result is more severe. The game gets confused, with the only visual tell being Hahli getting placed at the bottom edge of the screen, and trying to take any of the paths freezes the game.
Screenshots of the game freezing (take my word for it)


Either way, savedata doesn’t seem to get corrupted by this. And when you restart the game you are placed in your last location outside the maze and the maze is regenerated, this time probably without any errors.
How likely are the errors
With the help of Korohpu we did some statistical analysis to figure how likely these errors are as the maze gets bigger. Remember that the base game uses 9 scenes.

Thankfully split mazes are very unlikely, but I was very surprised to see how high the chance of a broken maze is. Further dividing by 2, to get the chance the entrance was overridden causing the game to crash upon first entering the maze, comes out to almost 2.8%. I wonder how many players actually had their game crash when they got to the maze.
I also want to take a paragraph to show some of Korohpu’s work. She determined that there are 4066 non-isomorphic graphs a fully connected maze can take. (Two graphs are isomorphic if one can be turned into the other simply by renaming the vertices.) She calculated this by first determining all possible threading paths (around 34.5 million), and then collapsing them into groups of isomorphic graphs.
A couple examples of non-isomorphic graphs representing elementary maze shapes with 9 scenes



Patching the maze
The final goal of this project is to provide alternative mazes. Initially only a version that skips it all together, since I heard and felt how tedious this part of the game is. Then after discovering the errors I wanted a version that just fixed those and kept the original intent. And finally I thought a little evil version might also be fun, as a treat.
Download the version you want and replace the KokoroWastes.swf file in your MNOG 2 installation folder. For convenience there is also the
original version
(14.1 KB .swf), which I got from the
BioMedia Project, in case you want to undo the change. And if you’re curious, you can expand each version to see my maze generation code for them.
Trivial version
(12.6 KB .swf). The maze is just a single scene, directly connecting all four locations. No more getting lost.
function createMaze() {
_root.KokoroWastesMaze = [
{ name: "Scene1", direction: "north", link: { name: "KantaisEntrance" } },
{ name: "Scene1", direction: "south", link: { name: "KokoroWastesEntrance" } },
{ name: "Scene1", direction: "east", link: { name: "KokoroRuinsApproach" } },
{ name: "Scene1", direction: "west", link: { name: "KokoroWastesDeadEnd" } }
];
_root.KokoroWastesFlags = [];
}Fixed version
(14.4 KB .swf). Changes the algorithm to prevent the maze from crashing the game or being unsolvable. Should be about as difficult as the original.
function createMaze() {
var scenes = [];
for (var s = 1; s <= 9; s++) {
scenes.push({
name: "Scene" + s,
nodes: [
"north",
"south",
"east",
"west"
]
});
}
var finalResults = [];
// Scene order doesn't affect the maze yet, so treat as shuffled.
// First scene has a random node connected to Entrance.
finalResults.push({
name: scenes[0].name,
direction: scenes[0].nodes.splice(random(scenes[0].nodes.length), 1)[0],
link: { name: "KokoroWastesEntrance" }
});
// Last scene has a random node connected to Kantai's Hut.
finalResults.push({
name: scenes[scenes.length - 1].name,
direction: scenes[scenes.length - 1].nodes.splice(random(scenes[scenes.length - 1].nodes.length), 1)[0],
link: { name: "KantaisEntrance" }
});
// Connect each adjacent pair of scenes with random nodes.
// The first and last scene have one node connected to an exit, and another node connected to its one adjacent scene.
// All other scenes have two nodes connecting to their two adjacent scenes.
for (var s = 0; s < scenes.length - 1; s++) {
var from = scenes[s].nodes.splice(random(scenes[s].nodes.length), 1)[0];
var to = scenes[s + 1].nodes.splice(random(scenes[s + 1].nodes.length), 1)[0];
finalResults.push({
name: scenes[s].name,
direction: from,
link: {
name: scenes[s + 1].name,
direction: to
}
});
finalResults.push({
name: scenes[s + 1].name,
direction: to,
link: {
name: scenes[s].name,
direction: from
}
});
}
// Pluck out a random scene, and connect a random node to the Ruins.
var third = scenes.splice(random(scenes.length), 1)[0];
finalResults.push({
name: third.name,
direction: third.nodes.splice(random(third.nodes.length), 1)[0],
link: { name: "KokoroRuinsApproach" }
});
// Pluck out another random scene, and connect a random node to the Dead End.
var fourth = scenes.splice(random(scenes.length), 1)[0];
finalResults.push({
name: fourth.name,
direction: fourth.nodes.splice(random(fourth.nodes.length), 1)[0],
link: { name: "KokoroWastesDeadEnd" }
});
// The `third` and `fourth` scenes each now only have one unconnected node left.
// Shuffle the rest of scenes.
for (var s = 0; s < scenes.length - 1; s++) {
var i = random(scenes.length - s) + s;
if (i == s) {
continue;
}
var tmp = scenes[s];
scenes[s] = scenes[i];
scenes[i] = tmp;
}
// Put the `third` scene at the beginning, and the `fourth` scene at the end.
scenes.unshift(third);
scenes.push(fourth);
// Connect each adjacent pair of scenes with random nodes.
// The first and last scene (previously `thrird` and `fourth`) have their one remaining node connected to its one adjacent scene.
// All other scenes have their two remaining nodes connected to their two adjacent scenes.
for (var s = 0; s < scenes.length - 1; s++) {
var from = scenes[s].nodes.splice(random(scenes[s].nodes.length), 1)[0];
var to = scenes[s + 1].nodes.splice(random(scenes[s + 1].nodes.length), 1)[0];
finalResults.push({
name: scenes[s].name,
direction: from,
link: {
name: scenes[s + 1].name,
direction: to
}
});
finalResults.push({
name: scenes[s + 1].name,
direction: to,
link: {
name: scenes[s].name,
direction: from
}
});
}
_root.KokoroWastesMaze = finalResults;
_root.KokoroWastesFlags = [];
}Evil version
(14.4 KB .swf). Don’t worry, it only has 6 scenes. Have fun! :)
function createMaze() {
var scenes = [];
for (var s = 1; s <= 6; s++) {
scenes.push({
name: "Scene" + s,
nodes: [
"north",
"south",
"east",
"west"
]
});
}
var directions = [
"north",
"south",
"east",
"west"
];
var finalResults = [];
// Scene order doesn't affect the maze yet, so treat as shuffled.
// First scene has a random node connected to Entrance.
finalResults.push({
name: scenes[0].name,
direction: scenes[0].nodes.splice(random(scenes[0].nodes.length), 1)[0],
link: { name: "KokoroWastesEntrance" }
});
// Last scene has a random node connected to Kantai's Hut.
finalResults.push({
name: scenes[scenes.length - 1].name,
direction: scenes[scenes.length - 1].nodes.splice(random(scenes[scenes.length - 1].nodes.length), 1)[0],
link: { name: "KantaisEntrance" }
});
// A random `third` scene has a random node connected to the Ruins.
var third = random(scenes.length - 2) + 1;
finalResults.push({
name: scenes[third].name,
direction: scenes[third].nodes.splice(random(scenes[third].nodes.length), 1)[0],
link: { name: "KokoroRuinsApproach" }
});
// Another random `fourth` scene has a random node connected to the Dead End.
var fourth = random(scenes.length - 3) + 1;
if (third == fourth) {
fourth += 1;
}
finalResults.push({
name: scenes[fourth].name,
direction: scenes[fourth].nodes.splice(random(scenes[fourth].nodes.length), 1)[0],
link: { name: "KokoroWastesDeadEnd" }
});
// 1. Make a *directed* thread from the first to the last scene.
for (var s = 0; s < scenes.length - 1; s++) {
var from = scenes[s].nodes.splice(random(scenes[s].nodes.length), 1)[0];
finalResults.push({
name: scenes[s].name,
direction: from,
link: {
name: scenes[s + 1].name,
direction: directions[random(directions.length)]
}
});
}
// 2. Flip first and last, and shuffle the other scenes.
var last = scenes.pop();
var first = scenes.shift();
// shuffle rest of scenes
for (var s = 0; s < scenes.length - 1; s++) {
var i = random(scenes.length - s) + s;
if (i == s) {
continue;
}
var tmp = scenes[s];
scenes[s] = scenes[i];
scenes[i] = tmp;
}
// add back backwards
scenes.unshift(last);
scenes.push(first);
// 3. Make a *directed* thread from the last back to the first scene.
// This guarantees that there is a loop connecting all scenes, making them all reachable.
for (var s = 0; s < scenes.length - 1; s++) {
var from = scenes[s].nodes.splice(random(scenes[s].nodes.length), 1)[0];
finalResults.push({
name: scenes[s].name,
direction: from,
link: {
name: scenes[s + 1].name,
direction: directions[random(directions.length)]
}
});
}
// 4. Connect all remaining nodes to random locations. :)
for (var s = 0; s < scenes.length; s++) {
while (scenes[s].nodes.length) {
var from = scenes[s].nodes.pop();
finalResults.push({
name: scenes[s].name,
direction: from,
link: {
name: scenes[random(scenes.length)].name,
direction: directions[random(directions.length)]
}
});
}
}
_root.KokoroWastesMaze = finalResults;
_root.KokoroWastesFlags = [];
}