ask:

The project should be a creative project that builds off or is inspired by the concepts we’ve covered this semester. You should feel free to think non-traditionally, projects do not need to be screen-based and there is no requirement to use a particular aspect of JavaScript or programming. Final projects can be collaborations with anyone in any class. Final projects can be one part of a larger project integrated with a different class.


thought:

wanted to make something that attempts to ‘learn’, being inspired from genetic algorithms in noc_class-10.

thought it’d be nice to collaborate with someone; chose aram because of his interest in ‘expressive-machines’.


outputs:

wip.


spent a little bit of time brainstorming.

was pretty sold on the idea that the computer needs to learn how to program. i’d be okay with it being a performative piece too — it has to teach itself to write a simple function of adding something.

aram is more ambitious; while i’m more conservative (keeping in mind that i have a big build for sound-studio).


after a long session going back & forth, aram & i froze on our idea:


i then began to write simple programs to demonstrate the workings of the idea.

made the basic wordle algorithm + added a dictionary for dialogues for each stage (so that the bot can shuffle between dialogues).

//260414; noc-final-wip.
 
let word;
 
/* 
we use the following states: 
generate -> await -> evaluate -> result -> if right: go to generate; else if wrong: go to wait.
*/
let state = "null";
let p_state = state;
 
//text to speech stuff:
let speech;
 
//dialogues are in key-value pairs.
let dialogues = {
  in_generate: [
    "guess this stupid word",
    "hey you — guess the word i'm thinking of",
  ],
  in_await: [],
  in_evaluate: [],
  all_correct: ["ok ... now"],
  some_correct: [
    "nooooo, try again",
    "ugh no you idiot",
    "god no",
    "close but not quite",
  ],
  all_wrong: [
    "god you're so dumb",
    "no you idiot",
    "stupid shit work ... try again and do better",
  ],
};
 
let input_str;
 
let result = [];
 
let attempts = [];
 
function setup() {
  // createCanvas(1000, 562); //in 16:9 aspect ratio.
  createCanvas(800, 800); //square to handle calculations better.
 
  speech = new p5.Speech();
 
  //assign a random voice. we wait a little bit to run this block of code for it to get all the voices.
  // setTimeout(() => {
  //   let all_voices = speech.voices;
  //   let voice = Math.floor(random(all_voices.length));
  //   speech.setVoice(voice);
  //   console.log("current voice:", all_voices[voice].name);
  // }, 500);
}
 
function draw() {
  background(0);
 
  for (let i = 0; i < attempts.length; i++) {
    attempts[i].display();
  }
 
  if (state === "generate") {
    state = "temp-hold"; //temp state to avoid looping multiple times (because it's in draw).
 
    //clear previous attempts.
    attempts = []; 
 
    fetch_word().then((result) => {
      word = result;
      console.log("garbled word -> " + word);
    });
 
    //say dialogue:
    speech.speak(random(dialogues.in_generate));
 
    //state change:
    state = "await";
 
    //prepare input string:
    input_str = "";
  } else if (state === "await") {
    // state = "temp-hold"; //temp state to avoid looping multiple times (because it's in draw).
 
    show_typing(input_str);
 
    if (input_str.length === 5) {
      //when it is 5, go to evaluate.
      state = "evaluate";
    }
  } else if (state === "evaluate") {
    result = [];
 
    for (let i = 0; i < 5; i++) {
      let c = input_str[i];
 
      if (c === word[i]) {
        result[i] = "correct";
      } else if (word.includes(c)) {
        result[i] = "wrong-pos";
      } else {
        result[i] = "wrong";
      }
    }
 
    console.log(result);
 
    attempts.push(new Attempt(input_str, result));
 
    state = "result";
  } else if (state === "result") {
    let correct = result.filter((r) => r === "correct").length;
    let wrong_pos = result.filter((r) => r === "wrong-pos").length;
    let wrong_char = result.filter((r) => r === "wrong").length;
 
    let dominant = Math.max(correct, wrong_pos, wrong_char);
 
    if (dominant === correct) {
      speech.speak(random(dialogues.all_correct));
      state = "generate";
    } else if (dominant === wrong_char) {
      speech.speak(random(dialogues.all_wrong));
      state = "await";
    } else {
      speech.speak(random(dialogues.some_correct));
      state = "await";
    }
 
    //reset input.
    input_str = "";
  }
 
  //state change:
  if (state != p_state) {
    console.log("state -> " + state);
  }
  p_state = state;
}
 
//show what is being typed.
function show_typing(input_str) {
  textSize(24);
  textAlign(CENTER, CENTER);
 
  for (let i = 0; i < input_str.length; i++) {
    fill(255);
    text(input_str[i], width / 2 - 100 + i * 50, height - 100);
  }
}
 
function mousePressed() {
  //text to speech needs a user-action to begin everything. so, we keep this to start.
  state = "generate";
}
 
function keyPressed() {
  if (state === "await") {
    input_str += key;
  }
}
 
async function fetch_word() {
  let res = await fetch("https://random-word-api.herokuapp.com/word?length=5");
  let data = await res.json();
  word = data[0];
 
  console.log("og word -> " + word);
 
  //garble the word:
  let chars = word.split("");
  for (let i = chars.length - 1; i > 0; i--) {
    let j = Math.floor(random(i + 1));
    [chars[i], chars[j]] = [chars[j], chars[i]];
  }
 
  return chars.join("");
}
 
class Attempt {
  constructor(word, result) {
    this.word = word;
    this.result = result;
  }
 
  display() {
    let index = attempts.indexOf(this);
    let y = 100 + index * 60;
 
    textSize(32);
    textAlign(CENTER, CENTER);
 
    for (let i = 0; i < this.word.length; i++) {
      let x = width / 2 - 100 + i * 50;
 
      if (this.result[i] === "correct") fill(0, 255, 0);
      else if (this.result[i] === "wrong-pos") fill(255, 200, 0);
      else fill(80);
 
      text(this.word[i], x, y);
    }
  }
}

32 x 32 display matrix: https://cdn-learn.adafruit.com/downloads/pdf/32x16-32x32-rgb-led-matrix.pdf

got the display matrix to work, but it didn’t line up well.

aram was courageous to admit his discomfort, and we playtested what we had. we didn’t like it, and were back to no concepts.


aram & i then decided by the weekend about what to do, and here’s a description that i wrote for it later:

title:

can you beat a 1-byte-per-second computer?

description:

we live in a world where ‘intelligence’ is highly-debated. with technology & artificial ‘intelligence’ on the rise, human-beings are increasingly faced with absurd questions such as am i smarter than a machine? and, if so, for how long; by how much; and in what?

this forms a compelling premise for a showdown.

in this installation, aram & arjun invite you to go head-on with a barebones machine — capable of thinking in 1-byte-per-second — in a game of wordle. the machine learns with every guess that is made in the round, with a genetic algorithm, and gets better as you or it gets closer to the answer. the first one to get the answer right is declared superior than the other.


i then began to program the game. i had to program it twice, because i couldn’t get it right the first time. i was proud of the program i wrote.

attempt 1:

/*
new sketch now. 
 
the idea is for the system to fetch two words. 
 
one is given to the person to guess, and the other is being guessed live by the computer. 
 
As of 2019, the average typing speed on a mobile phone was 36.2 wpm with 2.3% uncorrected errors—there were significant correlations with age, level of English proficiency, and number of fingers used to type.
^ wiki: https://en.wikipedia.org/wiki/Words_per_minute
 
the computer can guess once in 30s, and it takes 30s to arrive at a word. 
 
the stages remain the same as wordle; only that i will have to account for two entities: human & computer. 
 
stages: 
welcome -> generate -> await -> evaluate -> all_correct / some_correct / all_wrong.
 
each entitity will also have a local state.
*/
 
//three entitites; so each have their own class.
let human;
let machine;
let host;
 
let human_to_guess_word;
let machine_to_guess_word;
 
let global_state = "generate";
let p_global_state = "null"; //for state change detection.
 
let all_words;
 
function preload() {
  all_words = loadJSON("./words.json");
}
 
function setup() {
  createCanvas(windowWidth, windowHeight);
 
  //fix words to an array of 5-char words:
  all_words = Object.values(all_words);
  all_words = all_words.filter((w) => w.length === 5);
 
  host = new Host();
  machine = new Machine();
  human = new Human();
}
 
function draw() {
  ui();
 
  host.state_manager();
  machine.state_manager();
  human.state_manager();
 
  if (global_state == "generate") {
    human_to_guess_word = fetch_word();
    machine_to_guess_word = fetch_word();
    global_state = "first_guess";
  } else if (global_state == "first_guess") {
    host.local_state = "first_guess";
    global_state = "null";
  }
}
 
//helper to generate word:
function fetch_word() {
  return random(all_words);
}
 
function ui() {
  background(0);
}
 
function keyPressed() {}
 
function mousePressed() {
  if (global_state == "null") {
    //to initialize everything, since chrome needs user-input to start sound playback.
    host.local_state = "welcome";
  }
}
 
class Human {
  constructor() {
    this.local_state = "null";
  }
 
  state_manager() {
    if (this.local_state == "await_begin") {
      this.await_begin();
    }
  }
 
  await_begin() {
    if (!this.readyBtn) {
      this.readyBtn = createButton("ready");
      this.readyBtn.position(width / 2 - 40, height / 2);
 
      this.readyBtn.mousePressed(() => {
        global_state = "generate";
        this.readyBtn.remove();
        this.local_state = null;
        this.readyBtn = null;
      });
    }
  }
}
 
class Machine {
  constructor() {
    this.speech = new p5.Speech("Fred");
    this.speech.setRate(0.9);
 
    this.local_state = "null";
  }
 
  state_manager() {
    if (this.local_state == "ready") {
      this.speech.speak("i'm ready. i'm going to take you down.");
      this.local_state = "null";
    }
  }
}
 
class Host {
  constructor() {
    this.speech = new p5.Speech("Grandpa (English (United Kingdom))");
    this.speech.setRate(0.9);
    // this.speech.setVoice("Fred");
 
    this.local_state = "null";
  }
  state_manager() {
    if (this.local_state == "welcome") {
      this.welcome();
      this.local_state = "null";
    }
    if (this.local_state == "first_guess") {
      this.speech.speak("alright participants ... time for your first guess.");
      this.local_state = "null";
    }
  }
 
  welcome() {
    // this.speech.speak(
    //   "welcome puny human ... we've been hearing your declarations on the news about humans being smarter than computers. let's put that to the test now; shall we? ......... you & the machine on your right have been assigned a random 5-letter word. the first one to guess wins ...... are you game?",
    // );
 
    this.speech.speak("tester");
 
    setTimeout(() => {
      machine.local_state = "ready";
      human.local_state = "await_begin";
    }, 2000); // delay in milliseconds
  }
}

attempt 2:

/*
can you beat a 1-byte-per-second computer? 
 
a project by arjun & aram-pundak; april 2026. largely hand-programmed by arjun. 
 
there are three actors in this game: 
- human (player-a)
- machine (player-b)
- host
 
all three function independently. 
 
there is a global state-machine for the whole game. states are: 
welcome (t.a.) -> give word (t.a.) -> await (n.t.) -> declare result (n.t. to play again).
* t.a is triggered automatically; while n.t. is needs trigger by actors.
 
a thing i realized after a while is that p5.speech can't be instanced. there has to be a global speaker object. 
 
another annoying thing that browsers do is force a click to play any sound or do any speech thing. so, to test individual stages, go to the mousePressed function and change state from there (otherwise the audio(s) won't play).  
 
to communicate with physical-devices, we use an arduino zero & communicate via web-serial. the microcontroller is set up to read serial at 115200 baud, and do different i/o operations. these are the serial messages we send:
 
idle        -> idle face
smug        -> smug face
sad         -> sad face
win         -> win face
slap_1_on   -> pin 12 high
slap_1_off  -> pin 12 low
slap_2_on   -> pin 13 high
slap_2_off  -> pin 13 low
*/
 
let human, machine, host, speaker; //actors.
 
let dict; //dictionary to store all words.
let human_to_guess, machine_to_guess;
 
// // temp words for testing:
// let human_to_guess = "apple";
// let machine_to_guess = "apple";
 
let global_state = "begin"; //it has to be begin because everything in key pressed is wrapped inside this condition being true. to test a stage, change state in mousePressed() because chrome needs a user-activation for audio.
 
let reg_font;
let bold_font;
 
let winner;
let loser;
 
let thinking_synonyms = [
  "accomplishing",
  "actioning",
  "baking",
  "calculating",
  "cerebrating",
  "clauding",
  "computing",
  "considering",
  "cooking",
  "crafting",
  "creating",
  "crunching",
  "deliberating",
  "finagling",
  "forging",
  "forming",
  "generating",
  "hustling",
  "ideating",
  "inferring",
  "manifesting",
  "marinating",
  "moseying",
  "mulling",
  "mustereding",
  "musing",
  "noodling",
  "percolating",
  "pondering",
  "processing",
  "ruminating",
  "schlepping",
  "shucking",
  "simmering",
  "synthesizing",
  "thinking",
  "vibing",
  "working",
];
 
function preload() {
  dict = loadJSON("./words.json");
  reg_font = loadFont("../assets/fonts/JetBrainsMonoNL-Regular.ttf");
  bold_font = loadFont("../assets/fonts/JetBrainsMonoNL-Regular.ttf");
}
 
function setup() {
  createCanvas(windowWidth, windowHeight);
 
  dict = Object.values(dict).filter((w) => w.length === 5); //keep only 5-character words.
 
  //prevent default backspace operation on browsers, since we'll use the backspace.
  window.addEventListener("keydown", (e) => {
    if (e.key === "Backspace") e.preventDefault();
  });
 
  //make single instances of actors.
  human = new Human();
  machine = new Machine();
  host = new Host();
  speaker = new Speaker();
 
  send_serial("idle");
}
 
function draw() {
  //since draw loops over time, we'll use it as a state change manager.
 
  if (global_state == "welcome") {
    welcome();
  } else if (global_state == "generate") {
    generate();
  } else if (global_state == "await") {
    human.think();
    machine.think();
  }
 
  ui();
 
  if (global_state == "winner_declaration") {
    winner_declaration();
  }
}
 
//global helpers:
let word;
function evaluate(guess, from) {
  let result = [];
 
  if (from == "human") {
    word = human_to_guess;
  } else if (from == "machine") {
    word = machine_to_guess;
  }
 
  // convert word into mutable pool
  let pool = word.split("");
 
  // 1st pass: greens
  for (let i = 0; i < 5; i++) {
    let c = guess[i];
 
    if (c === word[i]) {
      result[i] = "correct";
      pool[i] = null; // consume it
    }
  }
 
  // 2nd pass: yellows / greys
  for (let i = 0; i < 5; i++) {
    if (result[i] === "correct") continue;
 
    let c = guess[i];
 
    let idx = pool.indexOf(c);
 
    if (idx !== -1) {
      result[i] = "wrong-pos";
      pool[idx] = null; // consume matched letter
    } else {
      result[i] = "wrong";
    }
  }
 
  let correct = result.filter((r) => r === "correct").length;
  let wrong_pos = result.filter((r) => r === "wrong-pos").length;
  let wrong_char = result.filter((r) => r === "wrong").length;
 
  let dominant = Math.max(correct, wrong_pos, wrong_char);
 
  if (correct === 5) {
    //all are correct.
    winner = from;
    loser = from === "human" ? "machine" : "human";
 
    if (winner === "human") {
      //we slap the machine.
      send_serial("slap_1_on");
    } else if (winner === "machine") {
      send_serial("slap_2_on");
    }
 
    global_state = "winner_declaration";
  } else if (correct === dominant) {
    //more correct characters:
    let close_dialogue = [
      "ooh, the " + from + " is close!",
      "ooh, the " + from + " is almost there!",
      "the " + from + " is close!",
      "the " + from + " is almost there!",
      from + " is close to winning!",
      from + " is getting there!",
      from + " almost",
    ];
 
    speaker.say("host", random(close_dialogue));
  } else if (wrong_char === dominant) {
    //just wrong position:
    let bad_dialogue = [
      "nope " + from + "... bad guess",
      from + " no, that's wrong",
      "no" + from + "not quite",
      "you" + from + "are not close",
      "you" + from + "are so off",
      "error wrong error error",
      "lol ... stupid" + from,
      from + "you are a monkey in a negligee",
    ];
 
    speaker.say("host", random(bad_dialogue), () => {
      let on_msg;
      let off_msg;
 
      // decide which actuator
      if (from === "human") {
        on_msg = "slap_2_on";
        off_msg = "slap_2_off";
      } else if (from === "machine") {
        on_msg = "slap_1_on";
        off_msg = "slap_1_off";
        send_serial("sad")
      }
 
      // 🔥 immediate trigger (after speech ends)
      send_serial(on_msg);
 
      // 🔥 safety reset (ensures motor always turns off even if something glitches)
      setTimeout(() => {
        send_serial(off_msg);
      }, 3000);
    });
  } else {
  }
 
  return {
    result,
    correct,
    wrong_pos,
    wrong_char,
    dominant,
  };
}
function mousePressed() {
  if (global_state === "begin") {
    userStartAudio();
    global_state = "welcome";
    connect_serial();
  }
}
 
function keyPressed() {
  if (human.local_state === "thinking") {
    human.type(key);
  }
}
 
function ui() {
  background(0);
 
  //global ui:
  fill(255);
 
  push();
  textSize(32);
  textAlign(CENTER, CENTER);
  textFont(bold_font);
  text("the ultimate battle of (wordle) wits", width / 2, 100);
  pop();
 
  if (global_state == "await" || global_state == "winner_declaration") {
    textFont(reg_font);
    textSize(16);
    textAlign(LEFT, TOP);
 
    //human stuff:
    let lx = 200;
    let ly = 200;
 
    fill(190);
    text("human-being:", lx, ly);
 
    let y = ly + 30;
 
    fill(255);
    for (let t = 0; t < human.attempts.length; t++) {
      let attempt = human.attempts[t];
 
      let line = "> " + attempt.word;
 
      for (let i = 0; i < line.length; i++) {
        let c = line[i];
 
        if (i < 2) {
          fill(120);
        } else {
          let idx = i - 2;
 
          if (attempt.result[idx] === "correct") fill(0, 255, 0);
          else if (attempt.result[idx] === "wrong-pos") fill(255, 200, 0);
          else fill(120);
        }
 
        text(c, lx + textWidth(line.slice(0, i)), y);
      }
 
      y += 28;
    }
 
    // current typing (no result yet)
    fill(255);
    text("> " + human.current, lx, y);
 
    let rx = width / 2 + 400;
    let ry = 200;
 
    fill(190);
    text("1-byte/second machine:", rx, ry);
 
    let my = ry + 30;
 
    // committed machine guesses (same rendering logic as human)
    for (let t = 0; t < machine.attempts.length; t++) {
      let attempt = machine.attempts[t];
 
      let line = "> " + attempt.word;
 
      for (let i = 0; i < line.length; i++) {
        let c = line[i];
 
        if (i < 2) {
          fill(120);
        } else {
          let idx = i - 2;
 
          if (attempt.result[idx] === "correct") fill(0, 255, 0);
          else if (attempt.result[idx] === "wrong-pos") fill(255, 200, 0);
          else fill(120);
        }
 
        text(c, rx + textWidth(line.slice(0, i)), my);
      }
 
      my += 28;
    }
 
    // live machine typing stream (NO evaluation yet)
    fill(255);
    text("> " + machine.current, rx, my);
  }
}
 
//stages:
function winner_declaration() {
  speaker.say("host", winner + " won. suck it, " + loser, () => {
    send_serial("win"); 
    noLoop();
  });
  push();
  // rectMode(CENTER, CENTER);
  // fill(255);
  translate(width / 2, height / 2);
  // angleMode(DEGREES);
  // rotate(-10);
  // rect(0, 0, 800, 200);
  textAlign(CENTER, CENTER);
  // fill(212,175,55);
  fill(207, 181, 59);
  textSize(95);
  text(winner + ` won.\n suck it,` + loser + `.`, 0, -10);
  pop();
}
 
function generate() {
  human_to_guess = random(dict);
  // machine_to_guess = random(dict);
  machine_to_guess = human_to_guess;
 
  // console.log(human_to_guess, machine_to_guess);
 
  speaker.say(
    "host",
    "the objective is to guess a randomly chosen 5-letter-english word. whoever guesses it correctly first wins / in other words, we're playing wordle. beware human — just like yourself, the machine see your input, and uses a genetic algorithm to learn from the guesses entered — .... 3 ... 2 ... 1 ... RUMBLE!",
    () => {
      global_state = "await";
    },
  );
  global_state = "null"; //prevent from looping. onEnd for the speech runs independently.
}
function welcome() {
  speaker.say(
    "host",
    "welcome viewers from this special nature of code class. we have been hearing that the world keeps debating — who is smarter: human-beings or computer-machines? ... today, we put that to the test and answer it once and for all ...//...,,, on my left ... we have a bare-bones machine ... capable of thinking only in one b.p.s ... byte per second ... and ...... on the right ... a meat-sack who supposedly thinks that they are 'smart' ...  we'll see today.",
  );
  speaker.say("host", "fighters ... are you ready?");
  speaker.say(
    "machine",
    "i'm ready ... and i'm going to take this human down.",
    () => {
      send_serial("smug");
      show_ready_btn();
    },
  );
 
  global_state = "null";
}
let ready_btn;
function show_ready_btn() {
  if (ready_btn) return;
 
  ready_btn = createButton("ready.");
  ready_btn.position(width / 2 - 40, height / 2);
 
  // what happens when player is ready
  function start_game() {
    if (!ready_btn) return;
 
    ready_btn.remove();
    ready_btn = null;
    global_state = "generate";
  }
 
  // mouse click
  ready_btn.mousePressed(start_game);
 
  // keyboard enter
  window.addEventListener("keydown", function ready_listener(e) {
    if (e.key === "Enter" && ready_btn) {
      start_game();
 
      // cleanup listener after use
      window.removeEventListener("keydown", ready_listener);
    }
  });
}
 
/* actors */
class Human {
  constructor() {
    this.current = "";
    this.log = [];
 
    this.sent_word = null;
    this.result = null;
 
    this.local_state = "null";
 
    this.attempts = [];
  }
 
  think() {
    //the human thinks themselves.
    if (global_state !== "await") return;
 
    if (this.current.length != 5) {
      this.local_state = "thinking";
    } else {
      this.send();
      this.local_state = "null";
    }
  }
 
  type(key) {
    if (keyCode === BACKSPACE || key === "backspace") {
      this.current = this.current.slice(0, -1);
      return;
    }
 
    if (key.length === 1) {
      this.current += key.toLowerCase();
    }
  }
 
  send() {
    this.sent_word = this.current;
 
    this.result = evaluate(this.sent_word, "human");
 
    this.log.push(this.current);
 
    this.attempts.push({
      word: this.current,
      result: this.result.result,
    });
 
    // 🔥 NEW: machine learns from human guess
    machine.learn(this.current, this.result.result);
 
    this.current = "";
  }
}
 
//machine is programmed with the help of chat-gpt.
class Machine {
  constructor() {
    this.current = "";
    this.log = [];
    this.sent_word = null;
    this.local_state = "thinking";
 
    this.attempts = [];
 
    this.knowledge = {
      fixed: Array(5).fill(null),
      banned: new Set(),
      mustContain: new Set(),
    };
 
    this.phase = "thinking"; // thinking → typing → sending
 
    this.timer = 0;
    this.reveal_index = 0;
    this.buffer = "";
 
    this.thinkAnim = [".", "..", "...", ".."];
    this.thinkFrame = 0;
    this.lastThinkTick = 0;
 
    this.first_think = true;
    this.thinking_synonym = "thinking";
  }
 
  think() {
    if (global_state !== "await") return;
 
    // 1. THINKING PHASE (5 sec pause)
    // 1. THINKING PHASE (5 sec pause + animated dots)
    if (this.phase === "thinking") {
      send_serial("idle"); 
      if (this.timer === 0) {
        if (this.first_think) {
          this.thinking_synonym = "thinking";
          this.first_think = false;
        } else {
          this.thinking_synonym = random(thinking_synonyms);
        }
 
        speaker.say("machine", this.thinking_synonym);
 
        this.timer = millis();
        this.thinkFrame = 0;
        this.lastThinkTick = millis();
 
        this.current = this.thinking_synonym;
      }
 
      // animate dots
      if (millis() - this.lastThinkTick > 400) {
        this.lastThinkTick = millis();
        this.thinkFrame = (this.thinkFrame + 1) % this.thinkAnim.length;
 
        this.current =
          this.thinking_synonym + " " + this.thinkAnim[this.thinkFrame];
      }
 
      if (millis() - this.timer < 5000) return;
 
      this.phase = "typing";
      this.timer = millis();
      this.reveal_index = 0;
 
      this.buffer = this.type();
    }
 
    // 2. TYPING PHASE (1 char per second)
    if (this.phase === "typing") {
      this.current = this.buffer.slice(0, this.reveal_index);
 
      if (millis() - this.timer > 1000) {
        this.timer = millis();
        this.reveal_index++;
      }
 
      // finished typing
      if (this.reveal_index > this.buffer.length) {
        this.send();
        this.phase = "thinking";
        this.timer = 0;
      }
    }
  }
 
  type() {
    let guess = "";
 
    for (let i = 0; i < 5; i++) {
      // 1. fixed letters (green)
      if (this.knowledge.fixed[i]) {
        guess += this.knowledge.fixed[i];
        continue;
      }
 
      let pool = "abcdefghijklmnopqrstuvwxyz"
        .split("")
        .filter((c) => !this.knowledge.banned.has(c));
 
      // 2. ensure mustContain letters are prioritized somewhere else
      let must = Array.from(this.knowledge.mustContain);
 
      // avoid placing a known yellow in same slot? (weak constraint, ok for now)
      pool = pool.filter((c) => c !== must[i]);
 
      let chosen;
 
      // bias: if we still need to place a mustContain letter, prefer it
      if (must.length > 0 && Math.random() < 0.5) {
        chosen = random(must);
      } else {
        chosen = random(pool);
      }
 
      guess += chosen;
    }
 
    return guess;
  }
 
  send() {
    this.sent_word = this.buffer;
 
    let result = evaluate(this.sent_word, "machine");
 
    this.log.push(this.buffer);
 
    this.attempts.push({
      word: this.buffer,
      result: result.result,
    });
 
    this.learn(this.buffer, result.result);
 
    this.current = "";
  }
  learn(word, resultArr) {
    // first pass: collect all non-wrong letters
    let confirmed = new Set();
 
    for (let i = 0; i < 5; i++) {
      let r = resultArr[i];
      let c = word[i];
 
      if (r === "correct") {
        this.knowledge.fixed[i] = c;
        confirmed.add(c);
      }
 
      if (r === "wrong-pos") {
        this.knowledge.mustContain.add(c);
        confirmed.add(c);
      }
    }
 
    // second pass: only ban if NOT confirmed anywhere
    for (let i = 0; i < 5; i++) {
      let r = resultArr[i];
      let c = word[i];
 
      if (r === "wrong" && !confirmed.has(c)) {
        this.knowledge.banned.add(c);
      }
    }
  }
}
 
class Host {
  constructor() {}
}
 
/*
p5.speech does this annoying thing where it can't make instances of the speech object. so, we have to deal with a global speaker. 
 
furthermore, inside a function, we can only have one .onEnd callback. so, to get around that, i asked chatgpt to make a queue system; with the option of a callback (so that i can do a state change). 
 
usage: 
function welcome() {
  speaker.say("host", "welcome");
  speaker.say("machine", "i'm ready. are you?");
  speaker.say("host", "let's begin", () => {
    global_state = "generate"; // 🔑 state change happens here
  });
  global_state = "null";
}
*/
 
class Speaker {
  constructor() {
    this.speech = new p5.Speech();
    this.queue = [];
    this.isSpeaking = false;
    this.currentCallback = null;
 
    this.speech.onEnd = () => {
      this.isSpeaking = false;
 
      if (this.currentCallback) {
        this.currentCallback();
        this.currentCallback = null;
      }
 
      this.next();
    };
  }
 
  say(who, txt, done = null) {
    this.queue.push({ who, txt, done });
    this.next();
  }
 
  next() {
    if (this.isSpeaking || this.queue.length === 0) return;
 
    let { who, txt, done } = this.queue.shift();
 
    if (who == "host") {
      this.speech.setVoice("Grandpa (English (United Kingdom))");
      this.speech.setRate(0.9);
    } else if (who == "machine") {
      this.speech.setVoice("Boing");
      this.speech.setRate(1.2);
      this.speech.setPitch(1.3);
    }
 
    this.currentCallback = done;
    this.isSpeaking = true;
    this.speech.speak(txt);
  }
}
 
/*
 serial stuff; written by aram's claude. 
 
 usage: 
 call connect_serial() once from a click / keypress handler. we do this when we do userStartAudio. 
 
 send_serial() with character "x" will send "x\n" to the arduino. we use this to change every physical peripheral — the arduino is set up to read serial at 115200 baud. 
*/
 
let serial_port = null;
let serial_writer = null;
 
async function connect_serial() {
  if (serial_writer || !("serial" in navigator)) return;
  try {
    serial_port = await navigator.serial.requestPort();
    await serial_port.open({ baudRate: 115200 });
    serial_writer = serial_port.writable.getWriter();
    console.log("serial: connected");
  } catch (e) {
    console.log("serial: not connected", e && e.message);
  }
}
 
function send_serial(c) {
  if (!serial_writer) return;
  serial_writer.write(new TextEncoder().encode(c + "\n")).catch((e) => {
    console.warn("serial write failed", e && e.message);
  });
}
 

i was clear in my philosophy, and even when aram decided to use ai for arduino code, i was able to prompt his llm in a specific way; so that it didn’t destroy the philosophy and structure of the code.

learning:

slap test:

in game:

daniel shiffman watching our thing felt like a wholesome moment. i learnt how to program via his videos:


from class:

raven presented her relational database that analyzes her text -messages and helps her start conversations.

kia’s god fighting game was cool too:


aram & i then worked on it quite a bit more to get it ready for the show.