EBFB HIDDEN

This sketch is a multi-stage interactive puzzle game hidden inside a deceptively cheerful interface. Players click past a smiling face to enter a series of 10 riddle-based puzzles, each requiring specific text input to advance. Solving all puzzles triggers a final eerie screen with a purple glowing shape and opens a secret Google Docs link.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the welcome screen color — The pastel green background becomes dark blue, completely transforming the mood from cheerful to ominous.
  2. Speed up the sequence countdown — The mysterious sequence disappears in 30 frames instead of 120, making it harder to memorize.
  3. Make the final screen purple — The starfield background changes from near-black to deep purple, creating a different atmosphere in the final reveal.
  4. Stop the oscillating ellipse from changing size — The pulsing effect disappears and the ellipse stays at a fixed 70-pixel diameter.
  5. Add more glitchy X characters — The X button becomes more visually chaotic by drawing 10 copies instead of 5, each in a random position.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch creates a hidden puzzle game disguised as a cheerful greeting. The interface has three distinct visual states: a bright, unsettling welcome screen with a smiling face, a dark puzzle-solving screen where players type answers to cryptic riddles, and a final eerie screen where 'it noticed you.' The code demonstrates advanced p5.js techniques including state management, encrypted answer validation, responsive canvas resizing, and dynamic text-based interaction.

The sketch is organized around a state variable S that switches between three screens, a puzzle index p that tracks progress through 10 puzzles, and helper functions that encode/decode answers to hide solutions from casual inspection. By studying this code, you will learn how to build multi-screen applications, validate user input, manage complex state transitions, and create puzzle logic that drives narrative progression.

⚙️ How It Works

  1. When the sketch loads, setup() initializes a full-window canvas and centers all text. State S is 0, showing a bright pastel screen with a yellow smiley face, floating colorful shapes, and a glitchy red X in the top-right corner.
  2. Clicking the red X in the top-right corner transitions to state S=1 and calls nextPuzzle(), which selects puzzle 0 and encodes the expected answer using a Caesar cipher shifted by 3 and Base64 encoding.
  3. While S=1, the draw loop displays the current puzzle message and a text input box. Players type into variable i, which updates on-screen character by character. Pressing ENTER calls check(), which decodes the stored answer and compares it to the player's input.
  4. If the input matches, puzzle counter p increments and the next puzzle loads with a new message and new encoded answer. If it fails, the input clears and displays 'That felt wrong.' The game includes special puzzles: one requiring an empty input, one showing a sequence that vanishes, and one asking to recall that sequence.
  5. After solving all 10 puzzles (p reaches 10), the final check() call sets ok=true, initializes 300 random colored points, waits 1.5 seconds, then transitions to S=2.
  6. In state S=2, the background turns black and displays those static points to create a glitchy starfield effect. A purple glowing ellipse oscillates at the center, text announces 'It noticed you,' and a hidden window automatically opens a secret Google Docs link.

🎓 Concepts You'll Learn

State management with variablesConditional branching (if/else if)Switch statements for puzzle selectionText input and string manipulationEncryption and encoding (Caesar cipher + Base64)Animation with trigonometric functionsResponsive canvas resizingArray iteration and dynamic point generation

📝 Code Breakdown

setup()

setup() runs once at the start. Here it prepares the canvas size and text alignment that will be used throughout the entire sketch.

function setup(){
  createCanvas(windowWidth,windowHeight);
  textAlign(CENTER,CENTER);
}
Line-by-line explanation (2 lines)
createCanvas(windowWidth,windowHeight);
Creates a canvas that fills the entire browser window, making the sketch responsive to screen size
textAlign(CENTER,CENTER);
Sets all future text to be centered both horizontally and vertically, so text appears at the exact coordinates you specify

windowResized()

This function ensures the sketch stays responsive: when the window resizes, the canvas scales, and if you're on the final screen, the starfield regenerates to fill the new space perfectly.

function windowResized(){
  resizeCanvas(windowWidth,windowHeight);
  // Re-populate static points if we are in the S==2 state and the window is resized
  if (S === 2) {
    initStaticPoints();
  }
}
Line-by-line explanation (3 lines)

🔧 Subcomponents:

function-call Canvas resize resizeCanvas(windowWidth,windowHeight);

Automatically adjusts canvas size when the browser window is resized

conditional Regenerate points on final screen if (S === 2) { initStaticPoints(); }

If the player is on the final eerie screen, regenerates the starfield to match the new window dimensions

resizeCanvas(windowWidth,windowHeight);
p5.js calls this automatically when the window size changes—it updates the canvas dimensions to fit
if (S === 2) {
Checks whether the player is currently viewing the final screen (state 2)
initStaticPoints();
If on the final screen, regenerates the 300 random glitchy points so they fill the new canvas correctly

initStaticPoints()

This function pre-generates 300 random colored points once, then reuses them throughout the final screen's animation. This is a performance optimization—instead of creating new random points every frame (which is expensive), we create them once and just draw them repeatedly.

🔬 This object controls each point's position and color. What happens if you change a: 120 to a: 255 (fully opaque) or a: 30 (barely visible)?

    staticPoints.push({
      x: random(width),
      y: random(height),
      r: random(150, 255),
      g: 0,
      b: random(255),
      a: 120
    });
function initStaticPoints() {
  staticPoints = []; // Clear previous points if any
  for (let k = 0; k < 300; k++) {
    staticPoints.push({
      x: random(width),
      y: random(height),
      r: random(150, 255),
      g: 0,
      b: random(255),
      a: 120
    });
  }
}
Line-by-line explanation (9 lines)

🔧 Subcomponents:

assignment Clear previous points staticPoints = [];

Empties the points array so it starts fresh without duplicates

for-loop Point generation loop for (let k = 0; k < 300; k++) {

Repeats 300 times to create 300 individual random points

staticPoints = [];
Clears the array of any previous points, starting with a blank slate
for (let k = 0; k < 300; k++) {
Loops 300 times, creating 300 separate point objects
staticPoints.push({
Adds a new object containing position and color data to the staticPoints array
x: random(width),
Assigns a random x position anywhere across the canvas width
y: random(height),
Assigns a random y position anywhere across the canvas height
r: random(150, 255),
Red channel is random between 150–255, creating bright reds
g: 0,
Green channel is always 0, so points stay red and purple without green
b: random(255),
Blue channel is random between 0–255, mixing with red to create purple tones
a: 120
Alpha (transparency) is set to 120, making points semi-transparent so they look like a glitchy starfield

draw()

The draw() function is where all three screens are managed. Every frame, it checks the current state S and draws the appropriate visuals. This is the heart of the state machine—a simple if/else chain that branches the entire sketch into three distinct experiences.

🔬 This loop creates 12 floating shapes. What happens if you change the third argument of ellipse from 25 to 50 (doubling their diameter)? What if you change 12 to 6 (fewer shapes)?

    for(let k=0;k<12;k++){
      fill(255,random(180,255),random(180,255),120);
      ellipse((frameCount*0.7+k*70)%width,300+sin(frameCount*0.04+k)*50,25);
    }

🔬 This loop draws all 300 starfield points using their pre-computed colors. What happens if you multiply pointData.a (alpha) by 2, like pointData.a * 2? Will the points become more or less transparent?

    for (let pointData of staticPoints) {
      stroke(pointData.r, pointData.g, pointData.b, pointData.a);
      point(pointData.x, pointData.y);
    }
function draw(){
  if(S==0){
    background(200,255,220);

    // TOO HAPPY
    fill(255,230,0);
    ellipse(width/2,100,90);

    fill(0);
    textSize(32);
    text("Everything is fine 🙂",width/2,200);

    textSize(16);
    text("Stay here. It's better here.",width/2,240);

    // floating shapes
    for(let k=0;k<12;k++){
      fill(255,random(180,255),random(180,255),120);
      ellipse((frameCount*0.7+k*70)%width,300+sin(frameCount*0.04+k)*50,25);
    }

    // GLITCHY X
    glitch++;
    for(let g=0;g<5;g++){
      fill(random(255),0,0);
      textSize(20+random(-8,8));
      text("X",width-30+random(-5,5),25+random(-5,5));
    }

  } else if(S==1){
    background(0);

    fill(255);
    textSize(18);
    text("PUZZLE "+(p+1),width/2,height/2-120);

    textSize(16);
    text(msg,width/2,height/2-60);

    fill(150);
    text(hint,width/2,height/2-30);

    // input
    stroke(255);
    noFill();
    rect(width/2-120,height/2,240,40);

    noStroke();
    fill(0,255,100);
    text(i,width/2,height/2+20);

    // special sequence display
    if(showSeq && seq!=""){
      fill(255,0,0);
      text(seq,width/2,height/2+80);
      timer--;
      if(timer<=0){
        showSeq=false;
        seq="";
      }
    }

    // subtle flicker
    if(random()<0.05){
      fill(255,0,0,40);
      rect(0,0,width,height);
    }

  } else if(S==2){
    background(10);

    // Optimized drawing of static points (generated once in check() or windowResized())
    for (let pointData of staticPoints) {
      stroke(pointData.r, pointData.g, pointData.b, pointData.a);
      point(pointData.x, pointData.y);
    }

    noStroke();
    fill(255);
    textSize(26);
    text("It noticed you.",width/2,80);

    let x=width/2+sin(frameCount*0.02)*200;
    let y=height/2+cos(frameCount*0.017)*140;

    fill(180,0,255);
    ellipse(x,y,70+sin(frameCount*0.1)*25);

    fill(200);
    textSize(14);
    text("You were not supposed to solve them.",width/2,height-40);

    // Open the Google Docs link once when the final screen is reached
    if (!linkOpened) {
      window.open("https://docs.google.com/document/d/1Q-hdZeGLsSQWWoHEOzofhrfxaKRLjqy4MjRL3j6hFxc/view?usp=sharing", '_blank');
      linkOpened = true;
    }
  }
}
Line-by-line explanation (34 lines)

🔧 Subcomponents:

conditional Welcome screen (S==0) if(S==0){

Draws the bright pastel welcome screen with smiley face and floating shapes

conditional Puzzle screen (S==1) } else if(S==1){

Draws the dark puzzle interface with text input box and current puzzle message

conditional Final screen (S==2) } else if(S==2){

Draws the eerie final screen with starfield, oscillating shape, and opens the secret link

for-loop Floating shapes animation for(let k=0;k<12;k++){

Creates 12 horizontally-scrolling pastel circles that loop across the screen

for-loop Glitchy X button for(let g=0;g<5;g++){

Draws the red X five times with random positions and sizes to create a glitch effect

conditional Sequence countdown if(showSeq && seq!=""){

Displays a temporary red sequence number that counts down and disappears

conditional Red flicker glitch if(random()<0.05){

5% chance each frame to flash a red transparent rectangle across the entire screen

for-loop Starfield rendering for (let pointData of staticPoints) {

Loops through all 300 pre-generated points and draws them with their stored colors

calculation Purple ellipse animation let x=width/2+sin(frameCount*0.02)*200;

Uses sine and cosine waves to make a purple ellipse drift in a smooth elliptical path

if(S==0){
Checks if the sketch is in state 0 (welcome screen). If true, draw the bright welcome screen
background(200,255,220);
Fills the entire canvas with a soft pastel green color
fill(255,230,0);
Sets the next shape to bright yellow
ellipse(width/2,100,90);
Draws a yellow circle at the top-center of the canvas (a smiley face)
text("Everything is fine 🙂",width/2,200);
Displays the sinister reassuring message below the smiley face
for(let k=0;k<12;k++){
Repeats 12 times to create 12 floating circles
ellipse((frameCount*0.7+k*70)%width,300+sin(frameCount*0.04+k)*50,25);
Draws circles spaced 70 pixels apart that scroll left-to-right infinitely (the % width wraps them around), and bob up-down using a sine wave
glitch++;
Increments the glitch counter every frame (this variable isn't used elsewhere but could drive visual effects)
for(let g=0;g<5;g++){
Repeats 5 times to draw the X button
text("X",width-30+random(-5,5),25+random(-5,5));
Draws a red 'X' character that jitters ±5 pixels randomly in all directions each frame, creating a glitch effect
} else if(S==1){
If state is 1, draw the puzzle screen instead
background(0);
Fills the canvas with black
text("PUZZLE "+(p+1),width/2,height/2-120);
Displays which puzzle number the player is on (p+1 because puzzles are numbered 1–10 but stored 0–9)
text(msg,width/2,height/2-60);
Shows the current puzzle's question message
text(hint,width/2,height/2-30);
Displays a hint if one is set, in gray color
rect(width/2-120,height/2,240,40);
Draws a white rectangle border (no fill) as the text input box
text(i,width/2,height/2+20);
Displays the player's typed input so far in bright green, centered in the input box
if(showSeq && seq!=""){
If a sequence should be displayed and seq is not empty, show it
text(seq,width/2,height/2+80);
Displays the sequence (like '7 2 9 4') in red below the input box
timer--;
Decrements the timer every frame
if(timer<=0){ showSeq=false; seq=""; }
When the timer reaches 0, hides the sequence and clears it from display
if(random()<0.05){
5% chance each frame to trigger a red flash
rect(0,0,width,height);
Draws a transparent red rectangle over the entire screen briefly, creating a glitch flicker
} else if(S==2){
If state is 2, draw the final eerie screen
background(10);
Fills with almost-black (dark gray), much darker than state 0
for (let pointData of staticPoints) {
Loops through each of the 300 pre-generated points
stroke(pointData.r, pointData.g, pointData.b, pointData.a);
Sets the stroke color to each point's stored red, green, blue, and alpha values
point(pointData.x, pointData.y);
Draws a single pixel at each point's stored x and y position
let x=width/2+sin(frameCount*0.02)*200;
Calculates an x position that oscillates ±200 pixels horizontally from the center using sine
let y=height/2+cos(frameCount*0.017)*140;
Calculates a y position that oscillates ±140 pixels vertically from the center using cosine at a slightly different rate
ellipse(x,y,70+sin(frameCount*0.1)*25);
Draws a purple ellipse at the calculated (x,y) position, with a diameter that grows and shrinks between 45 and 95 pixels
if (!linkOpened) {
Checks if the link has already been opened. If not, open it now
window.open("https://docs.google.com/document/d/1Q-hdZeGLsSQWWoHEOzofhrfxaKRLjqy4MjRL3j6hFxc/view?usp=sharing", '_blank');
Opens the secret Google Docs link in a new browser tab
linkOpened = true;
Sets the flag to true so this window.open only runs once, never again

mousePressed()

This function listens for mouse clicks and advances the game from the welcome screen to the first puzzle. It uses a simple hitbox check—if the click lands within the top-right 60×60 pixels, the game begins.

function mousePressed(){
  if(S==0){
    if(mouseX>width-60 && mouseY<60){
      S=1;
      nextPuzzle();
    }
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

conditional Welcome screen state check if(S==0){

Only allow clicks on the welcome screen; ignore clicks on other screens

conditional X button hitbox detection if(mouseX>width-60 && mouseY<60){

Detects whether the click was in the top-right 60×60 pixel region where the X button lives

if(S==0){
Only process mouse clicks while on the welcome screen (S==0). Clicks on other screens are ignored
if(mouseX>width-60 && mouseY<60){
Checks if the click was within 60 pixels of the right edge AND within 60 pixels of the top—this is the hitbox for the glitchy X button
S=1;
Changes state to 1, triggering the puzzle screen to appear in draw() on the next frame
nextPuzzle();
Calls nextPuzzle() to load the first puzzle (puzzle 0)

keyPressed()

This function handles keyboard input on the puzzle screen. It accumulates typed characters and detects the ENTER key to submit answers. It's a simple input handler that prevents the input from growing unbounded.

🔬 This snippet adds characters to the input and caps it at 20. What happens if you change the slice to i.slice(0, 20) instead of i.slice(-20)? Which slice removes old characters, and which removes new ones?

    if(key.length===1){
      i+=key;
      if(i.length>20)i=i.slice(-20);
    }
function keyPressed(){
  if(S==1){
    if(key.length===1){
      i+=key;
      if(i.length>20)i=i.slice(-20);
    }
    if(keyCode===ENTER){
      check();
    }
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

conditional Puzzle screen state check if(S==1){

Only process keyboard input while on the puzzle screen; ignore keys on other screens

conditional Character input accumulation if(key.length===1){

If a single character was pressed (not a special key), add it to the input

conditional Input length clamping if(i.length>20)i=i.slice(-20);

Prevents the input from exceeding 20 characters by keeping only the last 20 typed

conditional Enter key submission if(keyCode===ENTER){

Detects when ENTER is pressed and submits the current input for checking

if(S==1){
Only process keyboard input while on the puzzle screen. Keys are ignored on the welcome and final screens
if(key.length===1){
Checks if the pressed key is a regular character (length 1) and not a special key like Shift or Control
i+=key;
Appends the character to the input string i
if(i.length>20)i=i.slice(-20);
If the input exceeds 20 characters, removes older characters from the left, keeping only the newest 20. This prevents the input from growing too large
if(keyCode===ENTER){
Detects when the ENTER key is pressed (keyCode 13)
check();
Calls check() to validate the input against the puzzle's encoded answer

encodeAnswer()

This function uses a Caesar cipher (shift by 3) combined with Base64 encoding to hide the puzzle answers in the source code. Together they make the answers hard to spot by casual inspection, but a determined player can still find them by reading the sketch.

function encodeAnswer(str) {
  let shifted = str.split('').map(char => {
    let code = char.charCodeAt(0);
    // Only shift alphanumeric characters, keep others as is
    if (code >= 97 && code <= 122) { // a-z
      return String.fromCharCode(((code - 97 + 3) % 26) + 97);
    } else if (code >= 65 && code <= 90) { // A-Z
      return String.fromCharCode(((code - 65 + 3) % 26) + 65);
    } else if (code >= 48 && code <= 57) { // 0-9
      return String.fromCharCode(((code - 48 + 3) % 10) + 48);
    }
    return char; // Return non-alphanumeric as is
  }).join('');
  return btoa(shifted); // Base64 encode
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

string-manipulation Character splitting let shifted = str.split('').map(char => {

Converts the string into an array of individual characters and transforms each one

conditional Lowercase Caesar shift if (code >= 97 && code <= 122) { // a-z

Shifts lowercase letters forward by 3 positions in the alphabet

conditional Uppercase Caesar shift } else if (code >= 65 && code <= 90) { // A-Z

Shifts uppercase letters forward by 3 positions

conditional Digit shift } else if (code >= 48 && code <= 57) { // 0-9

Shifts digits forward by 3 positions (0→3, 9→2, wrapping around)

function-call Base64 encoding return btoa(shifted);

Converts the shifted string into Base64 encoding for obfuscation

let shifted = str.split('').map(char => {
Splits the input string into individual characters, then uses map() to transform each character
let code = char.charCodeAt(0);
Gets the numeric character code of the current character (e.g., 'a' = 97)
if (code >= 97 && code <= 122) { // a-z
Checks if this is a lowercase letter (ASCII 97–122)
return String.fromCharCode(((code - 97 + 3) % 26) + 97);
Shifts the character forward by 3 positions in the alphabet using modulo 26 to wrap around (a→d, x→a, y→b, z→c)
} else if (code >= 65 && code <= 90) { // A-Z
Checks if this is an uppercase letter (ASCII 65–90)
return String.fromCharCode(((code - 65 + 3) % 26) + 65);
Shifts uppercase letters forward by 3 positions (A→D, X→A, etc.)
} else if (code >= 48 && code <= 57) { // 0-9
Checks if this is a digit (ASCII 48–57)
return String.fromCharCode(((code - 48 + 3) % 10) + 48);
Shifts digits forward by 3 positions with modulo 10 wrapping (0→3, 7→0, 8→1, 9→2)
return char; // Return non-alphanumeric as is
For special characters like spaces or punctuation, returns them unchanged
}).join('');
Combines all shifted characters back into a single string
return btoa(shifted); // Base64 encode
Converts the shifted string to Base64 and returns it, making the answer unreadable in the source code

decodeAnswer()

This is the inverse of encodeAnswer(). It reverses the Caesar cipher shift and Base64 encoding to retrieve the original answer. When a player presses ENTER, check() calls this function to decode the stored answer, then compares it to the player's input.

function decodeAnswer(encodedStr) {
  let decodedB64 = atob(encodedStr);
  let shiftedBack = decodedB64.split('').map(char => {
    let code = char.charCodeAt(0);
    if (code >= 97 && code <= 122) { // a-z
      return String.fromCharCode(((code - 97 - 3 + 26) % 26) + 97);
    } else if (code >= 65 && code <= 90) { // A-Z
      return String.fromCharCode(((code - 65 - 3 + 26) % 26) + 65);
    } else if (code >= 48 && code <= 57) { // 0-9
      return String.fromCharCode(((code - 48 - 3 + 10) % 10) + 48);
    }
    return char;
  }).join('');
  return shiftedBack;
}
Line-by-line explanation (12 lines)

🔧 Subcomponents:

function-call Base64 decoding let decodedB64 = atob(encodedStr);

Converts the Base64-encoded string back to its Caesar-shifted form

string-manipulation Character reverse-shifting let shiftedBack = decodedB64.split('').map(char => {

Shifts each character backward by 3 positions to reverse the Caesar cipher

let decodedB64 = atob(encodedStr);
Uses atob() to decode the Base64 string back into its Caesar-shifted form
let shiftedBack = decodedB64.split('').map(char => {
Splits the decoded string into characters and begins transforming each one
let code = char.charCodeAt(0);
Gets the character code (e.g., 'd' = 100)
if (code >= 97 && code <= 122) { // a-z
Checks if this is a lowercase letter
return String.fromCharCode(((code - 97 - 3 + 26) % 26) + 97);
Shifts backward by 3 positions using + 26 to ensure the result stays positive before modulo (d→a, a→x, b→y, c→z)
} else if (code >= 65 && code <= 90) { // A-Z
Checks if this is an uppercase letter
return String.fromCharCode(((code - 65 - 3 + 26) % 26) + 65);
Shifts uppercase letters backward by 3 positions
} else if (code >= 48 && code <= 57) { // 0-9
Checks if this is a digit
return String.fromCharCode(((code - 48 - 3 + 10) % 10) + 48);
Shifts digits backward by 3 positions with + 10 to keep results positive (3→0, 2→9, 1→8, 0→7)
return char;
Non-alphanumeric characters are returned unchanged
}).join('');
Combines all shifted characters back into the original string
return shiftedBack;
Returns the decoded, unshifted answer

nextPuzzle()

This function uses a switch statement to select which puzzle to display based on the puzzle index p. Each case sets the puzzle message, encodes the expected answer, and optionally displays hints or sequences. It's the puzzle definition engine.

🔬 This case expects the answer '7' because it was the first number in puzzle 3's sequence. What happens if you change ans to encodeAnswer("9") (the third number)? The player would have to answer differently.

    case 4:
      msg="What was the FIRST number you saw earlier?";
      ans=encodeAnswer("7");
      break;
function nextPuzzle(){
  i="";
  showSeq=false;
  hint="";
  // Reset linkOpened flag if somehow we loop back to puzzles
  linkOpened = false;

  switch(p){

    case 0:
      msg="Type the word: 'open'";
      ans=encodeAnswer("open");
      break;

    case 1:
      msg="Type the opposite of 'closed'";
      ans=encodeAnswer("open");
      break;

    case 2:
      msg="Do NOT type anything. Press ENTER.";
      ans=encodeAnswer(""); // Encoded empty string
      break;

    case 3:
      msg="Remember this sequence";
      seq="7 2 9 4"; // This is meant to be visible temporarily
      ans=encodeAnswer("7294"); // Encoded answer
      showSeq=true;
      timer=120;
      hint="It will disappear.";
      break;

    case 4:
      msg="What was the FIRST number you saw earlier?";
      ans=encodeAnswer("7");
      break;

    case 5:
      msg="Type the word you typed twice already.";
      ans=encodeAnswer("open");
      break;

    case 6:
      msg="Ignore this text. Type: 13";
      hint="Why are you still reading?";
      ans=encodeAnswer("13");
      break;

    case 7:
      msg="Type what you think it wants.";
      ans=encodeAnswer("5262"); // Subconscious hint
      hint="It already showed you.";
      break;

    case 8:
      msg="Erase your input. Leave it empty.";
      ans=encodeAnswer(""); // Encoded empty string
      break;

    case 9:
      msg="Final memory: what code unlocks everything?";
      ans=encodeAnswer("5262");
      break;
  }
}
Line-by-line explanation (15 lines)

🔧 Subcomponents:

assignment Reset input and display variables i=""; showSeq=false; hint="";

Clears the player's input, hides any previous sequence display, and removes hints from the previous puzzle

switch-case Puzzle selection by index switch(p){

Selects which puzzle to load based on the current puzzle index p (0–9)

case Puzzle 0: literal instruction case 0: msg="Type the word: 'open'"; ans=encodeAnswer("open"); break;

First puzzle asks the player to type a word explicitly

case Puzzle 2: empty input case 2: msg="Do NOT type anything. Press ENTER."; ans=encodeAnswer(""); break;

Tests whether the player can follow an inverse instruction (do nothing)

case Puzzle 3: sequence memorization case 3: msg="Remember this sequence"; seq="7 2 9 4"; ... timer=120;

Shows a temporary sequence and tests memory by asking about it in a later puzzle

i="";
Clears the player's previous input so the new puzzle starts with a blank input box
showSeq=false;
Hides any sequence that was visible from the previous puzzle
hint="";
Clears the previous puzzle's hint
linkOpened = false;
Resets the linkOpened flag in case the sketch somehow loops back to puzzles
switch(p){
Branches based on the current puzzle index p (0–9)
case 0: msg="Type the word: 'open'"; ans=encodeAnswer("open"); break;
Puzzle 0: displays 'Type the word: open' and encodes the answer 'open'
case 1: msg="Type the opposite of 'closed'"; ans=encodeAnswer("open"); break;
Puzzle 1: asks for the opposite of 'closed' (which is 'open'), reusing the same answer as puzzle 0
case 2: msg="Do NOT type anything. Press ENTER."; ans=encodeAnswer(""); break;
Puzzle 2: tests if the player can resist typing by asking for an empty string
case 3: msg="Remember this sequence"; seq="7 2 9 4"; showSeq=true; timer=120;
Puzzle 3: displays a temporary sequence '7 2 9 4' for 120 frames (2 seconds at 60fps), expecting the player to remember it
case 4: msg="What was the FIRST number you saw earlier?"; ans=encodeAnswer("7"); break;
Puzzle 4: asks the player to recall the first digit from puzzle 3's sequence
case 5: msg="Type the word you typed twice already."; ans=encodeAnswer("open"); break;
Puzzle 5: hints that the player has already typed 'open' twice (puzzles 0 and 1)
case 6: msg="Ignore this text. Type: 13"; hint="Why are you still reading?"; ans=encodeAnswer("13");
Puzzle 6: tests obedience by explicitly telling the player to type 13 while adding a sarcastic hint
case 7: msg="Type what you think it wants."; ans=encodeAnswer("5262"); hint="It already showed you.";
Puzzle 7: cryptically asks for '5262' (which encodes 2529 when reversed—hinting at the sequence pattern), with a mysterious hint
case 8: msg="Erase your input. Leave it empty."; ans=encodeAnswer(""); break;
Puzzle 8: tests obedience again by asking for an empty input
case 9: msg="Final memory: what code unlocks everything?"; ans=encodeAnswer("5262");
Puzzle 9: the final puzzle, asking for the same code as puzzle 7 ('5262')

check()

This function is the game's validator. It compares the player's input to the encoded answer and either advances to the next puzzle or shows an error. For the final puzzle (p=10), it initiates the ending sequence with a brief delay for dramatic effect.

🔬 This code increments p and loads the next puzzle when correct. What happens if you remove the p++ line? Try predicting first: would the game loop on the same puzzle, or jump forward?

  if(p<10){
    // Decode the stored answer before comparison
    if(i===decodeAnswer(ans)){
      p++;
      nextPuzzle();
    } else {
      i="";
      msg="That felt wrong.";
    }
function check(){
  if(p<10){
    // Decode the stored answer before comparison
    if(i===decodeAnswer(ans)){
      p++;
      nextPuzzle();
    } else {
      i="";
      msg="That felt wrong.";
    }
  } else {
    // Final check for the last answer (which is from p=9, encoded '5262')
    if(i===decodeAnswer(ans)){
      ok=true;
      initStaticPoints(); // Prepare static points before transitioning to S=2
      setTimeout(()=>S=2,1500);
    } else {
      i="";
    }
  }
}
Line-by-line explanation (13 lines)

🔧 Subcomponents:

conditional Ongoing puzzle validation if(p<10){

Handles answer checking for puzzles 0–9

conditional Answer verification if(i===decodeAnswer(ans)){

Compares the player's input to the decoded answer

assignment Advance to next puzzle p++; nextPuzzle();

Increments the puzzle counter and loads the next puzzle on correct answer

assignment Handle wrong answer i=""; msg="That felt wrong.";

Clears the input and displays an error message on incorrect answer

conditional Final puzzle completion if(i===decodeAnswer(ans)){

Validates the final puzzle's answer and triggers the ending sequence

if(p<10){
Checks if the player is still on puzzles 0–9 (p ranges from 0 to 9, so p<10 means not yet on the final check)
if(i===decodeAnswer(ans)){
Compares the player's input i to the decoded answer. The === operator checks for exact equality.
p++;
Increments the puzzle counter, advancing from puzzle 0→1, 1→2, etc.
nextPuzzle();
Calls nextPuzzle() to load the next puzzle's message and answer
} else {
If the answer was wrong
i="";
Clears the input box so the player can try again
msg="That felt wrong.";
Displays a spooky error message
} else {
If p is not < 10 (meaning p equals 10, the final check), execute the final validation
if(i===decodeAnswer(ans)){
Compares the final answer
ok=true;
Sets the ok flag to true, marking that the game is completed
initStaticPoints();
Pre-generates the 300 starfield points before transitioning to the final screen
setTimeout(()=>S=2,1500);
Waits 1.5 seconds (1500 milliseconds) then transitions to state S=2 (the final screen), giving a moment of tension before the reveal
} else { i=""; }
If the final answer is wrong, just clears the input. No error message is displayed on the final attempt.

📦 Key Variables

S number

State variable that switches between three screens: 0=welcome, 1=puzzles, 2=final eerie screen

let S=0;
p number

Puzzle counter tracking which of the 10 puzzles (0–9) the player is currently on

let p=0;
i string

Input buffer storing the characters the player has typed so far

let i="";
msg string

Current puzzle's question or instruction message displayed to the player

let msg="";
hint string

Optional hint displayed below the puzzle message in gray text

let hint="";
seq string

A temporary sequence (like '7 2 9 4') displayed in red that disappears after a countdown

let seq="";
showSeq boolean

Flag controlling whether the sequence is currently visible on the puzzle screen

let showSeq=true;
timer number

Countdown counter in frames that determines how long the sequence stays visible before disappearing

let timer=0;
ok boolean

Flag set to true when the final puzzle is solved correctly, triggering the ending

let ok=false;
glitch number

Counter that increments every frame on the welcome screen to drive glitch effects

let glitch=0;
linkOpened boolean

Flag preventing the Google Docs link from opening more than once when the final screen is reached

let linkOpened=false;
staticPoints array

Array of objects storing x, y, r, g, b, a data for the 300 glitchy starfield points on the final screen

let staticPoints=[];
ans string

The encoded (Caesar cipher + Base64) answer for the current puzzle, set by nextPuzzle()

let ans="";

🔧 Potential Improvements (6)

Here are some ways this code could be enhanced:

BUG keyPressed() function

The input box allows up to 20 characters, but some puzzles expect answers longer than 20 chars (e.g., '7 2 9 4' is 7 chars but with whitespace). Edge case if a future puzzle needs more.

💡 Increase the character limit to 30 or 50, or make it configurable per puzzle.

PERFORMANCE draw() function, state S==0

The glitchy X button is redrawn 5 times per frame with random positions and sizes, creating visual noise. This works but is slightly inefficient.

💡 Pre-render the glitchy X to a graphics buffer once, then display the buffer instead of recalculating it every frame.

STYLE nextPuzzle() function

The function is long with 10 switch cases, making it harder to read and modify. Adding new puzzles requires careful formatting.

💡 Extract puzzles into a separate array of objects: const puzzles = [{msg: '...', ans: encodeAnswer(...), ...}, ...]; then loop through it.

FEATURE check() function

There is no penalty or hint system for repeated wrong answers. Players can brute-force by guessing randomly.

💡 Add a wrong-answer counter and provide larger hints (or lock input temporarily) after 3+ wrong attempts.

BUG draw() function, state S==2

The window.open() call for the Google Docs link happens inside the draw loop, which runs 60 times per second. Even with the linkOpened flag, this could theoretically open multiple windows if the flag logic fails.

💡 Move the window.open() call outside draw() entirely, or use a setTimeout in check() instead.

STYLE global variables

Many global variables are used (S, p, i, msg, hint, seq, showSeq, timer, ok, glitch, linkOpened, staticPoints, ans). This makes the code harder to trace and refactor.

💡 Consider organizing related variables into a single 'game' or 'puzzle' object: let game = {state: 0, puzzleIndex: 0, input: '', ...}.

🔄 Code Flow

Code flow showing setup, windowresized, initstaticpoints, draw, mousepressed, keypressed, encodeanswer, decodeanswer, nextpuzzle, check

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> canvas-resize[canvas-resize] draw --> state-check[state-check] state-check -->|S==0| state-0-welcome[state-0-welcome] state-0-welcome --> floating-shapes-loop[floating-shapes-loop] state-0-welcome --> glitch-x-loop[glitch-x-loop] state-check -->|S==1| state-1-puzzle[state-1-puzzle] state-1-puzzle --> puzzle-screen-check[puzzle-screen-check] puzzle-screen-check --> character-append[character-append] character-append --> input-limit[input-limit] input-limit --> enter-press[enter-press] enter-press --> string-split[string-split] string-split --> lowercase-shift[lowercase-shift] string-split --> uppercase-shift[uppercase-shift] string-split --> digit-shift[digit-shift] lowercase-shift --> base64-encode[base64-encode] uppercase-shift --> base64-encode digit-shift --> base64-encode base64-encode --> check[check] check --> puzzle-progression-check[puzzle-progression-check] puzzle-progression-check --> answer-comparison[answer-comparison] answer-comparison -->|correct| puzzle-advance[puzzle-advance] answer-comparison -->|wrong| wrong-answer[wrong-answer] state-check -->|S==2| state-2-final[state-2-final] state-2-final --> static-points-loop[static-points-loop] state-2-final --> oscillating-shape[oscillating-shape] state-2-final --> link-opening[link-opening] link-opening -->|once| state-2-final draw --> windowresized[windowresized] windowresized --> canvas-resize canvas-resize --> state-check initstaticpoints --> generation-loop[generation-loop] generation-loop --> clear-array[clear-array] clear-array -->|300 times| generation-loop click setup href "#fn-setup" click draw href "#fn-draw" click windowresized href "#fn-windowresized" click initstaticpoints href "#fn-initstaticpoints" click check href "#fn-check" click state-check href "#sub-state-check" click canvas-resize href "#sub-canvas-resize" click clear-array href "#sub-clear-array" click generation-loop href "#sub-generation-loop" click state-0-welcome href "#sub-state-0-welcome" click floating-shapes-loop href "#sub-floating-shapes-loop" click glitch-x-loop href "#sub-glitch-x-loop" click state-1-puzzle href "#sub-state-1-puzzle" click puzzle-screen-check href "#sub-puzzle-screen-check" click character-append href "#sub-character-append" click input-limit href "#sub-input-limit" click enter-press href "#sub-enter-press" click string-split href "#sub-string-split" click lowercase-shift href "#sub-lowercase-shift" click uppercase-shift href "#sub-uppercase-shift" click digit-shift href "#sub-digit-shift" click base64-encode href "#sub-base64-encode" click check href "#sub-check" click puzzle-progression-check href "#sub-puzzle-progression-check" click answer-comparison href "#sub-answer-comparison" click puzzle-advance href "#sub-puzzle-advance" click wrong-answer href "#sub-wrong-answer" click state-2-final href "#sub-state-2-final" click static-points-loop href "#sub-static-points-loop" click oscillating-shape href "#sub-oscillating-shape" click link-opening href "#sub-link-opening"

❓ Frequently Asked Questions

What visual elements can users expect to see in the EBFB HIDDEN sketch?

The sketch features a dynamic display of colorful static points that create an abstract visual experience as users interact with it.

How can users interact with the EBFB HIDDEN sketch?

Users can interact with the sketch by clicking or moving their mouse, which may trigger visual changes or animations.

What creative coding concepts does the EBFB HIDDEN sketch showcase?

This sketch demonstrates techniques such as character obfuscation, Base64 encoding, and the generation of random visual elements to create an engaging experience.

Preview

EBFB HIDDEN - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of EBFB HIDDEN - Code flow showing setup, windowresized, initstaticpoints, draw, mousepressed, keypressed, encodeanswer, decodeanswer, nextpuzzle, check
Code Flow Diagram