Midterm Project

Midterm Project – Critical Mass

Final sketch:

Play my game here

My Concept:

I made a two player strategy game called Critical Mass. It is played on a 5×5 grid. On your turn you drop a grain into a cell, and when a cell gets full it “topples”, which means it empties out and shoots one grain into each of its neighbors. The twist is that every grain you send out flips that neighbor to your color, so you can steal your opponent’s cells. If a cell you steal becomes full too, it topples as well, and you can get these satisfying chain reactions going off across the board. You win by owning more cells than your opponent when the turns run out, or by wiping them off the board completely.

The idea came from Mancala. I always liked how in Mancala you pick up stones and drop them one by one into the next pits, and how a single move can set off a bigger result down the line. I did not want to copy Mancala exactly though, so I turned that “spreading” feeling into a grid battle where the whole point is stealing territory from the other player. I think the chain reaction is what makes it fun because one well placed grain can flip a big chunk of the board at once.

I also wanted it to have a lot of personality instead of looking like a plain school project. So I gave it a neon arcade look, a glowing board, a space background, and I used two meme cats as the two players which I thought would be funny.

How it works:

The game moves through a few different screens, and I keep track of which one we are on with a single variable I called gameState. Figuring that out was the thing that made the whole project make sense to me, because once I had it, adding a new screen was easy.

It starts on an intro screen with the title and the two cats. From there you go to a mode select screen where you choose 1 Player or 2 Players. In 1 Player you play against a simple computer opponent, and in 2 Players two people share the same screen and take turns. There is also a How To Play page with the full rules and a little diagram showing a topple happening, because when I tested it on my friends they did not get the rules right away.

During the game there is a HUD (heads up display) at the top showing each player’s cat and their score, the turn counter in the middle, and a message at the bottom telling you whose turn it is. When a chain reaction happens the screen shakes a little, the background bursts with paws and sparkles, and a sound plays for each topple. When the game ends a result panel shows who won, and you can click to play again or press M to switch modes. I also added fullscreen with the F key, a mute toggle with the S key, and a little HOME button in the corner to go back to the start.

Visual improvements

The game over screen. At first my game over screen just drew a flat grey square over the board, and because my game area is centered with empty space on the sides, you could see the hard edges of the square and it looked broken. I changed it so it dims the whole window instead, and put the result inside a glowing panel in the winner’s color so the text is easy to read.

Before:


After:

Making the rules clearer. My first version only had a tiny bit of text on the intro. People kept asking me how toppling worked, so I made a separate How To Play page with four numbered rules and a small before and after picture of a full cell toppling and stealing its neighbors.

Before:


After:

Code I am proud of:

The part I am most proud of is the chain reaction, because it was the hardest thing to get right. The problem was that when a cell topples it can fill up its neighbors, and those can topple too, and if I did it all at once the game would try to topple the same cell twice and everything broke. I fixed it by using a queue, which is basically a waiting line of cells that need to topple. I take one cell off the front, topple it, and if that fills up a neighbor I add the neighbor to the back of the line. It keeps going until the line is empty.

let cell = this.queue.shift(); //take the first cell out of the line
    let owner = cell.owner; //remember who owned it because the grains it sends out keep this color
    cell.topple();
    this.cascadeSize++;
    this.lastToppled = cell; //i save this so the background knows where to make its burst

    //one grain shoots out to each neighbor
    let neighbors = [
      [cell.row - 1, cell.col],
      [cell.row + 1, cell.col],
      [cell.row, cell.col - 1],
      [cell.row, cell.col + 1]
    ];

    for (let n of neighbors) {
      let r = n[0];
      let c = n[1];
      //if the neighbor is off the edge of the board the grain just disappears
      if (r < 0 || r >= this.size || c < 0 || c >= this.size) continue;

      let neighbor = this.grid[r][c];
      neighbor.addGrain(owner);

      //okay so if this neighbor just got full too, it needs to topple later i add it to the
      //line but the includes() check is important i kept adding the same cell twice at first
      //and it toppled double which broke everything lmao. so only add it if its not already waiting
      if (neighbor.shouldTopple() && !this.queue.includes(neighbor)) {
        this.queue.push(neighbor);
      }
    }
  }

The other thing is that any grain that gets sent off the edge of the board just disappears, and I like that because it is actually the reason a chain reaction always stops eventually instead of going forever.

I am also proud of the computer opponent in 1 Player mode. I did not want some unbeatable genius, I wanted a “medium” opponent you can actually beat. So I gave it a few simple rules and some randomness. Most of the time it will set off a chain reaction if it can, sometimes it grabs an empty cell to spread out and otherwise it just plays andomly so it is not predictable.

//rule 1: most of the time 0.7, if it can set off a chain reaction, do it.
    //chains are how you steal cells so this is the smart move
    if (ready.length > 0 && random() < 0.7) {
      return random(ready);
    }

    //rule 2: otherwise half the time grab an empty cell to spread out and claim more space
    if (empty.length > 0 && random() < 0.5) {
      return random(empty);
    }

    //rule 3: if neither of those happened, just play any random cell its allowed to.
    //this little bit of randomness is what keeps it from being predictable
    return random(playable);
  }

Problems I ran into:

The biggest design problem was that at first, chain reactions almost never happened. I originally made every cell topple at 4 grains but when I tested it the game felt dead because in 20 turns you barely ever filled a cell up. I tested a bunch of games and a chain reaction was really rare. So I changed the rule so a cell topples based on how many neighbors it has, which means corners topple at 2 grains edges at 3 and the middle at 4. That made chains happen way more often and the game got a lot more fun.

let limit = 4;
        if (this.edgeRule) {
          //takes away 1 for each edge of the board this cell is touching.
          //a corner touches 2 edges so it ends up at 2 an edge touches 1 so its 3 middle stays 4
          if (r === 0 || r === this.size - 1) limit--;
          if (c === 0 || c === this.size - 1) limit--;
        }

The other problem that took me way too long was the background music. I load it as a sound file and I wanted it to loop forever, but after about ten seconds it would just stop. I found out that p5’s built in loop() was not playing anything in my version  the isPlaying() check kept telling me the song was still playing even after it had finished, so my code never restarted it. I fixed it by not trusting isPlaying() at all. Instead I remember the exact time the song will end and once we pass that time I just play it again.

//start it if it isnt going or restart it when this loop has finished
    if (!this.musicPlaying || millis() >= this.musicEndsAt) {
      this.music.amp(MUSIC_VOLUME);
      this.music.play();
      this.musicPlaying = true;
      this.musicEndsAt = millis() + this.music.duration() * 1000;
    }

Areas for improvement:

If I kept working on this I would make the computer opponent a bit smarter (maybe have options to set difficulty) so it actually tries to protect its own cells instead of just attacking. I would also like to add a short tutorial that makes you do one practice topple, since even with the rules page some people still learn better by doing. And I think it would be cool to add different board sizes, like a bigger grid for longer games.

References:

Classes and objects (Cell, Board, ComputerPlayer):

https://p5js.org/reference/p5/class/

https://thecodingtrain.com/tracks/code-programming-with-p5-js/code/6-objects/3-constructor

Arrays and loops for the grid:

https://thecodingtrain.com/tracks/code-programming-with-p5-js/code/7-arrays

Loading images:

https://p5js.org/reference/p5/loadImage/

Timing things with millis():

https://p5js.org/reference/p5/millis/

The neon glow and background gradients (drawingContext / canvas):

https://p5js.org/reference/p5/drawingContext/

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient

Fullscreen:

https://p5js.org/reference/p5/fullscreen/

Font (Orbitron):

https://fonts.google.com/specimen/Orbitron

Cat images: just memes I took from the internet

Use of AI:

I only used Claude on this project both to help me build and debug things. I came up with the concept, the cat theme, the rules and the design decisions myself and then worked through the code with it only when I was stuck or needed help.

The main things I used it for were getting the chain reaction queue to work without toppling cells twice and building the 1 Player computer opponent. I also used it to make my sound. I had Claude generate souund effects for me. It wrote a small script that builds each note as a sound wave and saves them as mp3 files. I picked the style I wanted (simple arcade beeps and a soft background loop) and it matched that.

I used it for debugging too. The music that kept stopping after about ten seconds was the big one, and it helped me figure out that p5’s isPlaying() was giving me wrong information so I needed a different way to loop it.

Midterm Progress

Concept:

I wanted to make a strategy game inspired by Mancala. In Mancala you pick up the stones from one pit and drop them one by one into the pits that follow, so a single move spreads across the board. I liked that feeling of one action rippling outward and I wanted to build a game around it that is easier to pick up than the full Mancala rules. The game is called ‘Critical Mass’ (might change it). Players take turns adding a grain to a cell on a shared grid. You can only add a grain to an empty cell or a cell you already own. Once a cell reaches 4 grains it topples and sends one grain to each of its neighbors, and those grains turn the neighboring cells into the color of the player who caused the topple. If a neighbor also reaches 4, it topples too, so a single move can start a chain reaction that flips a big part of the board.

To win, you have to take over the board. If one player ends up with no cells left, the other player wins right away. To keep games from dragging on there is also a turn limit. If nobody has been wiped out when the turns run out, whoever owns more cells wins.

I am hoping on doing a two player mode where two people share one mouse and a one player mode against a simple computer opponent so it still works without a second person.

Progress so far:

So far I have only made the intro screen. It shows the title and a short version of the rules and a “click/press to start” prompt. I kept the instructions to four short lines because I want someone to understand the game in a few seconds without reading a wall of text.

Next I want to build the game and the game screen and add sounds and the mode select and the game over screen (which are going to be the most difficult parts). Also add more details and design to make it better.

Week 5 – Reading Reflection

What really  really surprised me most is how little of computer vision is actually about smart code. Frame differencing and background subtraction are pretty dumb algorithms on their own nothing like how a human eye and brain read a scene using memory and context. Levin’s whole point is that you make the physical world more predictable instead like backlighting a silhouette and retroreflective material. That is the technique doing the work that smarter software would otherwise have to do, which changes how I used to think about computer vision as something you solve purely in code.

The surveillance question is where this got more complex for me. Suicide Box is unsettling specifically because frame differencing was enough to passively record something as serious as someone jumping off a bridge. It is not sophisticated AI which somehow makes it feel more invasive, like tragedy got reduced to a pixel threshold. Sorting Daemon and Standards and Double Standards push that further since both use the same basic tracking logic as their actual material, sorting or following people without consent. What struck me is that the exact techniques Levin teaches as beginner friendly are structurally identical to what gets used for surveillance. The difference is not code sophistication but intent and whether people know its happening.

That made me think differently about using webcam input in Hydra (in my live coding class i took last semester). I have treated camera feeds as just another visual source to feed into feedback loops without thinking much about it. After this, a simple motion detector pointed at a crowd is doing the same basic operation as Sorting Daemon, so if I use webcam tracking live going forward I want people in the room to actually know they are part of the input instead of just grabbing frame differences because it looks wayyy cooler!!!!

Week 4 – Assignment

My Concept:

I made a text generator that displays a random fortune and it is styled like a small fortune card. The idea came from fortune cookies and the paper slips you get with them along with Japanese omikuji which are the fortune slips you draw at shrines. I liked the idea of a small piece of text appearing almost like a little ritual, so I wanted the sketch to feel closer to drawing a slip than just reading random text on a screen. When you click the canvas, a new fortune appears, pulled randomly from a list I made stored in a CSV file.

How I made it:

The fortunes are stored as individual lines in a file called fortunes.csv, which I load using loadStrings(). In setup(), I wait for that file to finish loading before doing anything else, then call a function that picks one fortune at random to display. In draw(), I draw a simple paper background with a double border to make it look like a paper slip or card with a small title at the top and the current fortune centered inside. Clicking the canvas picks a new random fortune and redraws the screen, similar to how you would draw a new slip if you wanted to try your luck again.

Highlight of my code:

function pickFortune() {
  //random(fortunes.length) picks a random position in the array
  //int() turns it into a whole number so it can be used as an index
  currentFortune = fortunes[int(random(fortunes.length))];
}

This is the part that actually makes the generator generative. random(fortunes.length) picks a random decimal number somewhere between 0 and the total number of fortunes I have stored. Since array positions have to be whole numbers, I wrap it in int() to round it down into a usable index. That number is then used to pull one specific line out of the fortunes array and store it in currentFortune, which is what gets displayed on the card. Every time this function runs again it recalculates a new random index so a different fortune has a chance of showing up.

My Embedded Sketch:

You can click the sketch to display another result.

Problems I ran into:

I had a layout bug where my fortune text was overflowing off the card instead of staying centered. I originally assumed that when you give text() a width and height for wrapping, the x and y you provide would be treated as the center point, since I already had text align set. It turns out those x and y values are actually the top left corner of the wrapping box so my box was starting from the middle of the canvas and extending off the edge. Once I understood that fixing the position was simple.

Reflection:

I think this assignment taught me more about debugging than about the actual concept of generative text which i did bot expect. I feel like getting stuck on something small like an async function or a misunderstood parameter is where I learned the most. I thought going in that the hardest part would be designing the visuals, but it ended up being the file loading and positioning logic that took the most time to get right.

References:

https://p5js.org/reference/p5/loadStrings/

https://p5js.org/reference/p5/textAlign

Claude – I used it to generate some fortune texts for the csv file

Week 4 – Reading Reflection

Something completely unrelated to the reading that drives me a little crazy is the automatic doors on campus that only work about half the time. Sometimes they slide open right away, other times Im standing there waving my arm around because the sensor did not pick me up, and there is zero indication why. No light nothing telling me if it’s just slow or actually broken. If I redesigned it, Id add a small light near the sensor that turns on when it detects motion, so at least you know whether it registered you or the whole thing is just dead. It is basically Norman’s feedback problem in physical form.

As for applying his principles to interactive media, I think the shift for me is realizing a lot of what I call “interactive” in my own work is really just affordance without signification. In a browser based project, technically anything can be clicked since a mouse works anywhere, but that does not mean the user knows what is clickable unless I signify it, through a hover state or cursor change or something. Feedback matters the same way it does with the campus doors, if someone triggers an action and nothing visibly changes for even a beat, they’ll assume it’s broken and click again, same standing and waving problem. The takeaway is that clever mechanics don’t matter if the signifiers and feedback don’t communicate clearly. The user just experiences confusion and assumes it’s their fault which now feels like a design failure on my end rather than a user failure on theirs.

Week 3 – Assignment

Concept:

I have always loved space, especially anything to do with stars and the solar system, so for this assignment I decided to build a small generative solar system in p5.js using object oriented programming. Each orbiting body is a Planet object with its own distance from the sun, speed, size and color, all stored in an array so I could build and animate the whole system with loops instead of writing out five separate planets by hand. The exact stats for each planet are randomized within set ranges, so it looks a little different every time you refresh it, which felt fitting since no two solar systems out there actually look the same either.

How this was made:

I split the code into two files: sketch.js for the main setup/draw loop and the sun, and planet.js for the Planet class, which handles one planets data (distance, speed, size, color, current angle) plus its own update() and display() methods. In setup() I loop through and create 5 planets, spacing their orbit distances out with map() so they do not end up overlapping and giving each one a random speed, size and color. In draw() I loop through the array every frame so that update() moves each planet a little further along its orbit and display() draws it along with a faint ring showing its orbit path.

Code Highlight:

update() {
    this.angle += this.speed;
  }

  display() {
    push();
    //translate() shifts where (0,0) is on the canvas.
    //normally (0,0) is the top-left corner, so every shape would need
    //width/2 and height/2 added to its position to appear centered.
    //instead I move the origin itself to the center of the canvas once,
    //so I can treat the sun as sitting at (0,0) and calculate every
    //planets position relative to the sun instead of the corner
    translate(width / 2, height / 2);

    //orbit ring just a circle showing the path this planet travels
    //drawn centered on the new (0,0) which is now the canvas center
    noFill();
    stroke(255, 60);
    strokeWeight(1);
    ellipse(0, 0, this.distance * 2, this.distance * 2); //*2 because ellipse() takes diameter

    //this is the actual orbit math.
    //this.angle is a value in radians representing how far around
    //the circle the planet currently is (a full loop = TWO_PI, about 6.28)
    //cos(angle) and sin(angle) both output a number between -1 and 1 -
    //multiplying by this.distance scales that up to the actual orbit radius
    //I want turning it into a real pixel offset from the center.
    let x = cos(this.angle) * this.distance; //horizontal offset from center
    let y = sin(this.angle) * this.distance; //vertical offset from center

    //every frame angle increases slightly (from update()) which changes
    //the result of cos/sin slightly which moves x and y slightly 
    //that is what creates the smooth circular motion over time

    noStroke();
    fill(this.col);
    ellipse(x, y, this.size, this.size); //draw the planet at its calculated position

    pop(); //restore the coordinate system back to normal (undoes the translate)
           //so the next planet in the loop does not inherit this shift
  }

This is the part of the code in Planet class I am happy about, since it is where the actual orbiting motion happens. update() just nudges the planets angle forward a tiny bit every frame, like a slowly ticking clock hand. In display(), I use translate() to shift the canvas’s origin point to the center, so I can treat the sun as sitting at (0,0) instead of constantly adding width/2 and height/2 to every position. Then cos(this.angle) and sin(this.angle) calculate where a point at that angle would land on a circle, and multiplying by this.distance stretches that out to the planets actual orbit radius. Since the angle keeps increasing frame by frame, this position keeps recalculating slightly differently each time, which is what produces the smooth circular motion. I wrap the whole thing in push() and pop() so the translate only affects this one planet and does not carry over and throw off the position of the next planet in the loop.

Problems I ran into:

The orbit math was the main thing I struggled with like I understood conceptually that I wanted planets to move in circles but I could not figure out on my own how to turn an increasing angle into an actual x/y position on screen. I ended up going back to Claude multiple times to work through the logic and the translate() part step by step until it actually clicked. I also ran into an issue with the orbit rings not lining up with the planets real path. I had unfortunately forgotten that ellipse takes a diameter and not a radius, so my rings were drawn at half the size they should have been. On top of that, I had to switch from fully random orbit distances to using map() to space them out evenly since random values kept causing planets to spawn too close together or right on top of the sun.

Embedded Sketch:

Reflection and ideas for improvement

This was honestly one of my favorite assignments so far because of the subject matter. Using classes also made a lot more sense once I actually built something with them, since having each planet manage its own data felt way cleaner than juggling separate arrays. For future improvements, I would really like to make the orbits a little more realistic by making them elliptical instead of perfect circles and maybe add small moons orbiting the planets themselves and also add fading trails behind each planet so you can actually see the path it has traced instead of just a plain guide ring!!!

References:

https://p5js.org/reference/p5/translate

https://p5js.org/reference/p5/cos

https://p5js.org/reference/p5/sin

https://p5js.org/reference/p5/push

https://p5js.org/reference/p5/pop

https://p5js.org/reference/p5/map

 

Week 3 – Reading Reflection

I really enjoyed doing this reading. What actually got me thinking was how strict he is about needing all three steps. Like it has to actually process what the user does and hold onto some kind of memory so the response can change based on more than just the current input and then respond in a way that reflects that processing instead of just playing back a fixed effect. I think I agree with his sentiment (for now atleast).

That is actually a useful way to look at my own p5 sketches because most of what I have built so far leans hard on that same idea of reacting instantly, so it feels interactive but there is no real thinking step. A way to fix that would be introducing an actual state like tracking things like how long the users cursor lingers in an area and letting the sketchs behavior shift over time instead of resetting every frame. Even something simple, like the sketch remembering the last few interactions and varying its response based on that instead of just the instantaneous mouse position would push it from Crawford’s “reaction with good production values” into something closer to his actual definition of interaction. Though I’ll be honest I do not know how to go about doing it but I am definitely excited to learn and explore it in the coming classes!!!

Week 2 – Assignment

My concept:

For this assignment, I made an interactive starfield. The idea was to have a field of stars scattered across the canvas that glow individually when the mouse touches them and I used loops to generate and animate the stars and a conditional to control the glow effect. I was inspired by just looking at the night sky at the Al Quaa Milky Way Spot.

How I made it:

I started by creating an empty array called stars to hold each star as an object. Then in setup I used a for loop to generate 125 stars, giving each one a random x position y position and size and then pushing that data into the array as an object. After that in draw I used another for loop to go through every star in the array each frame to check its distance from the mouse using distance function and used an if/else conditional to decide whether that star should glow bigger and brighter or stay as a small dim dot.

Code highlights:

for (let i = 0; i < stars.length; i++) {
    let s = stars[i]; //named it as s for convenience

    //checking distance from mouse to this star
    let d = dist(mouseX, mouseY, s.x, s.y);

    if (d < 60) {
      //close to mouse so glows bigger and brighter
      fill(255, 255, 200);
      noStroke();
      ellipse(s.x, s.y, s.size * 3, s.size * 3);
    } else {
      //normal dim star
      fill(255);
      noStroke();
      ellipse(s.x, s.y, s.size, s.size);
    }
  }

This part is what I like the most. Since the loop runs through every star every single frame, each one is constantly rechecking its own distance from the mouse which is what makes the glow follow the cursor smoothly and only affect stars that are actually close to it.

Embedded Sketch:

Reflection and Ideas:

I really liked doing this assignment but if I had to change something, I might have added more details to the stars and like a shape to it. I could have also tried to add a moving feature that causes the stars to maybe follow or repel away from the mouse. I was inspired by the video to make the stars random positions each time. I am happy with how it looks and maybe could work on it more in the future to make it even better!!

References:

https://p5js.org/reference/p5/loop/, https://p5js.org/reference/p5/for/, https://p5js.org/reference/p5/if/

 

Week 2 – Reading Reflection

Okay so last semester I took Live Coding with Professor Aaron and this whole talk kind of took me back to that. Every time I would throw in a random() somewhere in Hydra or Tidal I always told myself I was letting go of control but honestly I was not I was just picking a range of outcomes I was fine with and calling it random. Watching Reas go from Duchamp literally dropping strings on the floor to writing actual random number generators kind of made that connection that it is the same move, just with more math. He is not really doing something new but doing the same “let something outside me cause this” thing people have been doing since like 1916.

The note that stuck with me the most was the “ordered grid with deviation of pixels” thing because that is literally what I was chasing in class without knowing how to say it. If the pattern is too locked in it just looks dead and boring, but if it is too random it just looks like noise and does not feel like anything. The good zone is somewhere in between where you can tell there is a rule happening but it still surprises you sometimes. That is basically what his 15-image series is doing: same rule every time but different result.

So when I think where the balance is between total randomness and total control, I do not really think it is a 50/50 thing. I think what I actually want is like a really good system with one small deviation in it on purpose. Full control means no surprises, which gets boring fast. Full randomness means no me in it anymore, it is just noise doing its own thing. The deviation in between is where it actually feels like art to me.

Week 1 – Self Portrait

Hello!! For my first assignment in Intro to IM, I made a self portrait using p5.js. I just combined basic shapes and a few lines for smaller details like the nose and glasses.

Embedded Sketch!!

How I made it!!

I started by making the body using a simple rectangle shape and then added an ellipse on top for the face. For the eyes, I created a wider white ellipse with a smaller black ellipse on top to create the pupil. For the hair, I used the arc() function set to only draw the top half of a circle so it sits like a dome over the head. I also added a moon in the night sky background and glasses over the eyes since I always wear glasses.

The main source I used to help me code was the p5js reference site to remind myself of the parameters of each shape especially for the arc.

Code Highlights

//glasses
    stroke(0);
    strokeWeight(4);
    noFill();
    ellipse(width/2.5, height/1.9, 45, 30);
    ellipse(width/1.66, height/1.9, 45, 30);
    line(width/2.5+22, height/1.9, width/1.66-22, height/1.9);
    line(width/2.5-22, height/1.9, width/2.5-40, height/1.9-5);
    line(width/1.66+22, height/1.9, width/1.66+40, height/1.9-5);

The glasses gave me the most trouble. I had to line up two more ellipses exactly on top of the eyes without covering the pupils so I used noFill() so I would get only an outline instead of a solid shape. Then I needed a bridge line connecting the two lenses and two more lines angled slightly outward on each side to look like the arms of the glasses. Getting all of them to actually connect at the same points took a lot of trial and error.

//moon
    fill(255, 255, 220);
    noStroke();
    ellipse(340, 50, 50, 50);
    fill('#464444');
    ellipse(352, 42, 45, 45);

I found that I could fake a crescent shape by drawing a full ellipse and then covering part of it with a second ellipse painted the exact same color as the background

Reflection and Ideas:

This assignment was fun and I found myself losing track of time while doing it, for future assignments I would love to go more in depth with p5.js and add animations and more detailed drawings.. I could have added more details in the background and maybe even designed the body to make it look more detailed.