
The Riddle Bomb - Notchmeister2 Entry
Introduction
It was mid-2020. I was on a TV production gig preparing content for real-time production when I saw Notch's post on Facebook about their new shiny contest called Notchmeister 2. The headline idea was to create something extraordinary inside a cube. Being an advanced Notch artist, I decided that this was the perfect opportunity to make something out of pure enjoyment since I could make anything.
I wanted to make a game. I took inspiration from "Keep Talking and Nobody Explodes", which I played a couple of weeks earlier, and I decided to create a bombastic interactive cube puzzle. I knew it would be a heavy-logic-driven project where I would have the opportunity to explore Notch's JavaScript capabilities. Soon I comprehended that I decided to create a game in software unsuitable for this task. But this gave me even more joy because I knew it would be a unique project.
After a couple of weeks of work after hours, this is what I created:
Walkthrough
The goal of the game is to defuse a bomb that is placed somewhere inside the box. The player can move the camera and explore the object by clicking on the cyan handles. A welcome menu is displayed at the beginning, where he can familiarize himself with this mechanic.
By clicking on a button located on the other side of the menu, the mystery cube is shown to the player, and the countdown to the explosion begins.
The first is a "Tile-sliding game" inspired by a favorite game from my childhood - The Neverhood. The player is tasked with arranging tiles by clicking on them. Setting the correct combination opens a trapdoor with a hidden button. Clicking on this button changes its color.
The second puzzle is a "Pipe game". Here the player, by clicking on the pipes, can rotate them. By arranging them in a valid path and clicking a button, water begins to pour from the top "valve" and fill the pipes, which, when full, change color. Each output valve colors the pipes a different color.
To unveil the third puzzle, follow the clue on the side of the cube. The player needs to set the color of the button to cyan and the color of the pipes to magenta. This combination opens a side door, hiding an "Electro board game".
Here I took inspiration from an old browser game, the name of which I can't remember. The idea was to move the cursor from the starting point to the finish line while holding the mouse button. Touching the black area with the cursor triggered a jump scare cut scene. In the case of my game, I skipped the jump scare and focused on making an interesting maze. To make things more complicated, the light turns off after completing half of the route, leaving the player in darkness illuminated only by rays emanating from a small sphere of energy following the cursor. After reaching the finish line, the light returns, and a LED light starts to blink on the electronic board.


Turning the cube 180 degrees around, another puzzle presents itself to the player's eyes - a keypad and morse code instructions. The player has to correctly find the element on the cube that sends an encrypted message, decrypt it and type it on the numeric keypad.
The flashing part is the LED diode from the previous puzzle, which broadcasts code 7365
. Typing it into the keypad opens the box door, where the prize in the form of a UV flashlight awaits the player.
Pressing the "U" key turns on the UV light, changing the colors visible on the cube. The player notices a hidden fingerprint on the bottom of the cube, invisible in standard lighting.


Clicking on it opens a lid at the bottom of the box. Inside there are two clues. The first tells the player to click on a small button and adjust the color of the pipes and the sliding-tiles button.
The second clue is a "Knights and Knaves" style problem in which a set of statements is provided, but some are true, and some are false. The puzzle's goal is to determine which statements are valid based on the information given.


What is the valuable information for? The player, while turning the cube, notices that the door to the bomb has been opened, and there are three cables of different colors. To stop the bomb, one must cut the blue cable - a color that solves the Knights and Knaves problem.
Below you can see the whole gameplay in a single video:
Assets
Most 3D models and textures were created by me in Cinema 4D and textured in Substance Painter. As I was primarily concerned with creating unique mechanisms and logic, I abandoned the idea of using high-quality assets and stopped at a conceptual level.
Development in Notch
As I stated in the introduction section, by any means, Notch is not a proper tool for creating heavy-logic-driven applications. One reason I think that way is because there is no idea like precomposing a group of nodes by collapsing them into a single custom one allowing for easy reuse of developed logic (like Prefabs in Unity). The node graph is linearly growing and growing until the editor becomes so slow that, at some point, inserting even one node is almost unfeasible.
The only reasonable solution is to move most of the logic into JavaScript, which, unfortunately, is an outdated version that does not implement many modern core features. Two of them, which would make my life much easier while developing this project, are class-based inheritance and import declarations.
While the first one can be replaced with prototypal inheritance, the latter causes a lot of code duplication and hinders a logic flow by forcing the usage of the node graph as a top-level script. To communicate between objects kept nicely in separate files, I used Envelope Modifier nodes (denoted with a prefix #
) inside Notch as intermediate registers. It's like using global variables, but instead of declaring them in a script file, I declare them by spawning nodes and giving them unique names. Not handy at all.
Object picking
What would be a point-and-click game without the capability of clicking and interacting with in-game objects? For sure, it would require much more imagination from players. Notch does not provide any node's method for detecting objects clicked by the user. But it gives us a Mouse Picker
node which captures the mouse cursor's location in a screen-space coordinate and informs whether the left or right button has been clicked. How can we use this information to detect the related object?
My first idea for object picking was to use a color-picking technique, as implementing raycasting in Notch is not an option. I could use Object ID buffer by using GBuffer To Image
node... but there is no way of dereferencing a node by the object's ID. Instead, I sampled from the World Position buffer to receive color=coordinates under the cursor.


Next, I created a Null node and tested its position against Hot Zone nodes. In the image below, notice the red bounding box around a pipe. It's a sign that this zone detected a null inside its boundaries.
This allowed me to relate interaction zones with proper meshes and develop logic around this solution with this information.
var button = {
...
IsHit: function() {
var node = layer.FindNode("#sliding_puzzle_button_hotzone")
return node.GetEnvelopeValue("Current Hit Zone") == 1 ? true : false;
}
}
Mechanism of a keypad
I was conferring for some time about which mechanism I shall depict to approximate the general logic I developed in this project. In the end, I decided to describe a keypad as it's straightforward and implements similar solutions I used in other puzzles.


Starting with describing its construction, let's look at the node graph. It consists of 3D models, materials, Hot Zone nodes and JS-linked envelope nodes designated by #
prefix in their names.
The Hot Zone node, in combination with the 3D cursor mechanism, detects if the user clicked on a button.
The digital screen consists out of two Text String nodes. One for a typed code (#keypad_code_text
) and one for a blinker (_
). The #keypad_code_digits
envelope controls an offset of the blinker and toggles its visibility.
Lastly, there is an LED on the right of the keypad. It's a simple 3D model with a material, which's color is controlled using JS.
Before I dive into describing the JS script part, I would like to introduce its basic anatomy briefly. Three main methods are lying in the heart of each script - Init()
, Update()
, and OnKeyPress(key)
. The first one is invoked only once when the script is loaded after all nodes on the node graph are initialized. It's an excellent place to link references to essential nodes. The second one is called by Notch every frame - equivalent to Unreal Engine's Tick()
method. And the last one is called when the editor is focused, and the key is pressed.
I divided the logic into seven distinct objects. Inside the Init()
method, I initialize each of them by fetching appropriate envelope node references.
function Init() {
var layer = UpdateContext.Layer
mouse.Init(layer);
game.Init(layer);
puzzle_state.Init(layer);
code.Init(layer);
screen.Init(layer);
led.Init(layer);
buttons.Init(layer);
}
Next, in the Update()
method I perform interaction and general update steps:
function Update(){
InteractionStep();
UpdateStep();
}
The InteractionStep()
handles a high-level logic:
function InteractionStep() {
if (mouse.IsClicked()) {
hit = buttons.GetHit();
if (hit != -1) {
buttons.Press(hit);
if (buttons.IsDigitHit(hit)) {
code.AddDigit(hit)
} else if (buttons.IsClearHit(hit)) {
code.Reset();
} else if (buttons.IsConfirmHit(hit)) {
if (code.IsCorrect()) {
buttons.BlockInput();
screen.SetText("---OK---");
led.Inform(true);
puzzle_state.Set(1);
return
} else { // Code incorrect
led.Inform(false);
code.Reset();
}
}
screen.SetText(code.Get());
}
}
}
Lastly, inside UpdateStep()
I detect whether the user requested a reset of the game and perform a reset of each object that implements a Reset()
method:
function UpdateStep() {
// Reset the script if reset has been requested in end game
if (game.ResetRequested()) {
puzzle_state.Reset();
code.Reset();
screen.Reset();
led.Reset();
}
}
How are these objects implemented? From top to bottom, the first is a mouse
object, which handles user mouse input. It does not directly detect mouse click but rather read info from intermediate envelopes, which are modified by a mouse.js
script.
var mouse = {
Init: function(layer) {
this.cursor = layer.FindNode("#cursor");
if (!this.cursor)
Log("ERROR[keypad.js/mouse.Init]: #cursor node could not be found");
this.clicked_node = layer.FindNode("#mouse_clicked");
if (!this.clicked_node)
Log("ERROR[keypad.js/mouse.Init]: #mouse_clicked node could not be found");
this.pressed_node = layer.FindNode("#mouse_pressed");
if (!this.pressed_node)
Log("ERROR[keypad.js/mouse.Init]: #mouse_clicked node could not be found");
},
BlockInput: function(unlock_after) {
this.blocked = true;
},
prev_frame_clicked: false, // Depending on a framerate, sometimes the click is captured twice
IsClicked: function() {
if (this.blocked) return false;
if (this.clicked_node.GetEnvelopeValue("Value") == 1) {
return !this.prev_frame_clicked;
} else if (this.prev_frame_clicked) {
this.prev_frame_clicked = false;
}
return false;
},
IsPressed: function() {
if (this.blocked) return false;
return this.pressed_node.GetEnvelopeValue("Value") == 1 ? true : false;
}
};
The second game
object detects if a reset of the game has been requested. After unsuccessful bomb defusal, a big button pops up, allowing the player to click it and try again.
var game = {
Init: function(layer) {
this.reset_request_node = layer.FindNode("#reset_requested");
if (!this.reset_request_node)
Log("ERROR[keypad.js/game.Init]: #reset_requested node not found!");
},
ResetRequested: function() {
return this.reset_request_node.GetEnvelopeValue('Value') == 1 ? true : false;
}
}
The puzzle_state
object outputs whether the user solved the puzzle. In this case, value 1
opens a box's lid behind the keypad.
var puzzle_state = {
Init: function(layer) {
this.output = layer.FindNode("#keypad_output_state");
if (!this.output)
Log("ERROR[keypad.js/puzzle_state.Init]: #keypad_output_state node could not be found!");
this.Reset();
},
Reset: function() {
this.Set(-1);
},
Set: function(value) {
this.output.SetEnvelopeValue("Value", value);
}
}
Next, the code
object manages entered code. The correct code is hardcoded as it refers to the morse code emitted by LED, which is stored as a video file.
var code = {
entered: "",
correct_code: "7365",
Init: function(layer) {
this.Reset();
},
Reset: function() {
this.entered = "";
},
AddDigit: function(digit) {
if (this.entered.length >= this.correct_code.length)
return;
this.entered += digit;
},
Get: function() {
return this.entered;
},
IsCorrect: function() {
return this.entered == this.correct_code;
}
}
The screen
object is connected to Text String nodes and manages the displayed text:
var screen = {
Init: function(layer) {
this.code_text = layer.FindNode("#keypad_code_text");
if (!this.code_text)
Log("ERROR[keypad.js/screen.Init]: #keypad_code_text node could not be found!");
this.code_digits_count = layer.FindNode("#keypad_code_digits_count");
if (!this.code_digits_count)
Log("ERROR[keypad.js/screen.Init]: #keypad_code_digits_count node could not be found!");
this.Reset();
},
Reset: function() {
this.SetText("");
},
SetText: function(string) {
this.code_text.SetString("Attributes.Text String", string);
this.code_digits_count.SetEnvelopeValue("Value", string.length);
}
}
The led
object is a controller for the LED light next to the keypad. Its blink capability owes to the in-built Timer method. Thank you, Notch, for implementing this fundamental feature : )
var led = {
default_color: COLOR.BLUE,
color: COLOR.BLUE,
is_on: true,
Init: function(layer) {
this.color_r = layer.FindNode("#keypad_led_r");
this.color_g = layer.FindNode("#keypad_led_g");
this.color_b = layer.FindNode("#keypad_led_b");
if (!this.color_r || !this.color_g || !this.color_b)
Log("ERROR[keypad.js/led.Init]: LED color nodes could not be found!");
this.Reset();
},
Reset: function() {
this.SetColor(this.default_color);
this.Toggle(1);
},
SetColor: function(color) {
this.color = color;
},
Blink: function(duration_seconds) {
if (!this.IsOn()) {
this.Toggle(1);
Timer(duration_seconds, function() { led.Toggle(0) }, name="", false);
} else {
this.Toggle(0);
Timer(duration_seconds, function() { led.Toggle(1) }, name="", false);
}
},
IsOn: function() {
return this.is_on;
},
Toggle: function(on) {
this.is_on = on;
this.color_r.SetEnvelopeValue('Value', this.color[0] * (on == true ? 1 : 0));
this.color_g.SetEnvelopeValue('Value', this.color[1] * (on == true ? 1 : 0));
this.color_b.SetEnvelopeValue('Value', this.color[2] * (on == true ? 1 : 0));
},
Inform: function(correct) {
this.SetColor(correct == true ? COLOR.GREEN : COLOR.RED);
this.Blink(0.3); // Blink once now
Timer(0.7, function() { led.Blink(0.3) }, name="", false); // Blink once again after 0.4 second
if (!correct) {
Timer(1.4, function() { led.Blink(0.3) }, name="", false); // Blink once again after 0.4 second
Timer(2.1, function() { led.Reset() }, name="", false); // Reset color after 0.4 second
}
}
}
And the last, the buttons
object detects whether the user clicked on a button and sends feedback information to nodes controlling 3D models Z position:
var buttons = {
blocked: false,
Init: function(layer) {
this.animation = [];
this.hotzone = [];
var animation_node;
var hotzone_node;
for (var i = 0; i < 12; i++) {
if (i < 10) {
animation_node = layer.FindNode("#keypad_button_animation_" + i)
if (!animation_node)
Log("ERROR[keypad.js/buttons.Init]: Keypad button " + i + " animation node could not be found!");
else
this.animation.push(animation_node);
hotzone_node = layer.FindNode("#keypad_button_hotzone_" + i)
if (!hotzone_node)
Log("ERROR[keypad.js/buttons.Init]: Keypad button " + i + " hotzone node could not be found!");
else
this.hotzone.push(hotzone_node);
} else if (i == 10) { // CLEAR button
animation_node = layer.FindNode("#keypad_button_animation_x")
if (!animation_node)
Log("ERROR[keypad.js/buttons.Init]: Keypad CLEAR button animation node could not be found!");
else
this.animation.push(animation_node);
hotzone_node = layer.FindNode("#keypad_button_hotzone_x")
if (!hotzone_node)
Log("ERROR[keypad.js/buttons.Init]: Keypad CLEAR button hotzone node could not be found!");
else
this.hotzone.push(hotzone_node);
} else if (i == 11) { // OK button
animation_node = layer.FindNode("#keypad_button_animation_ok")
if (!animation_node)
Log("ERROR[keypad.js/buttons.Init]: Keypad OK button animation node could not be found!");
else
this.animation.push(animation_node);
hotzone_node = layer.FindNode("#keypad_button_hotzone_ok")
if (!hotzone_node)
Log("ERROR[keypad.js/buttons.Init]: Keypad OK button hotzone node could not be found!");
else
this.hotzone.push(hotzone_node);
}
}
},
BlockInput: function() {
this.blocked = true;
},
GetHit: function() {
if (this.blocked) return -1;
for (i = 0; i < this.hotzone.length; i++) {
var is_hit = this.hotzone[i].GetEnvelopeValue("Current Hit Zone") == 1 ? true : false;
if (is_hit) {
return i;
}
}
return -1;
},
Press: function(which_button) {
var current_value = this.animation[which_button].GetEnvelopeValue('Value');
this.animation[which_button].SetEnvelopeValue('Value', (current_value + 1) % 2);
},
IsDigitHit: function(which_button) { return which_button < 10; },
IsClearHit: function(which_button) { return which_button == 10; },
IsConfirmHit: function(which_button) { return which_button == 11; }
}
Conclusions
Every time I write a publication for my blog, I remind myself of the fantastic things I have created and the valuable skills I possess. After the Notchmeister2 contest ended, I claimed my prize and archived this project on a hard drive deep inside my desk. I never tried to show and explain all the magic that happened inside.
This blog made me open it again, and I am thankful for it. Giving my code a subtle touch of refactoring after such a long time was a gratifying experience, as I had an opportunity to don my old perspective and notice how much I learned over the past years.
One thing I would write differently would be using prototypal inheritance and creating more OOP structure in combination with programming patterns, e.g., a command pattern for handling button click actions.
Links and files
Full Notch .dfx
project (requires at least PLE Notch license) >>> LINK <<<
GitHub repository with JS scripts: >>> LINK <<<