Skip to content

12. Machine Week: Cooper, Amalia, Elle

Whack A Mole!

Mechanical Design (part 1 of 2):

  • Design a machine that includes mechanism + actuation + automation + application
  • Build the mechanical parts and operate it manually
  • Document the group project

Machine Design (part 2 of 2):

  • Actuate and automate your machine
  • Document the group project
Member Role
Cooper, Amalia, Elle Documentation
Cooper and Elle Programming and Making Heads Move
Cooper Making PCB
Amalia Casing
Elle 3D Printed Heads

Overview

We wanted to make something fun for machine week, so we decided to make a Whack-A-Mole game. We asked ChatGPT for a bunch of ideas, and ultimately decided that Whack-A-Mole would be the most exciting.

Video

Slide

Moving the Heads

Touch Sensor – Amalia

We decided to use a touch sensor at the top of each head so that when the head is whacked, it knows to go down without too much force being applied. We tried using the HW-139 touch sensor and had initial concerns about if the touch sensor would still be able to work if not touched by a human finger because our Whack-A-Mole machine will use a mallet-like tool to hit the heads. This worked, but when testing it, I learned that the touch sensor needs to be firmly held down in order to read the signal.

The HW-139 is very simple and only has 3 pins: one to ground, one to power, and one to read signal.

This is the wiring using a XIAO ESP32-C3:

This is the code I used:

const int touchPin = 3;  // GPIO3 on ESP32-C3

void setup() {
  pinMode(touchPin, INPUT);
  Serial.begin(115200);
  Serial.println("Touch sensor ready");
}

void loop() {
  int touchState = digitalRead(touchPin);

  if (touchState == HIGH) {
    Serial.println("Touched!");
  }

  delay(100); // Debounce delay
}

This is the touch sensor working using a taped caliper as the model mallet:

On the computer screen, you can see the Arduino terminal says “Touched!” whenever the touch sensor is hit. The ultimate plan with this touch sensor in the project is to place a touch sensor on top of each of the heads and send a signal when the touch sensor is hit to the motor for that head.

Solenoids – Cooper

For my part in the Whack-A-Mole machine, I wired six solenoids to pop the moles up, but I wasn’t totally sure how to control them. I originally had an ESP32 board in the mix, but I eventually switched to a Raspberry Pi Pico because I needed more GPIO pinouts to manage everything cleanly. This was beacues I needed 6 pinouts for the solenoids and 6 more for the touch sensors. That made wiring simpler and gave me more flexibility with controlling the solenoids.

At first, I was kind of stuck trying to figure out how I’d actually power and control those solenoids—each one needs more current than a microcontroller pin can handle. That’s when ChatGPT explained the concept of using relays to control high-current devices with low-power signals. It finally clicked. I ended up using two 4-station relays to control all six solenoids, with two spare channels left over just in case I need them later (or want to get a little fancy with the gameplay).

Since my power supply can only deliver 5 amps, I decided to only activate five solenoids at a time, even though there are six total. That way, I’m not pushing the system past its limits. Everything feels more stable, and now I have a solid control setup that makes the whole project feel way more doable.

Looking back, figuring out how to bridge the gap between logic signals and high-current loads was one of the biggest breakthroughs— once I got it, the rest started falling into place.

This all turned out not to be used for the final project though since I did not account for how little the solonids actually pushed up.

Servos – Cooper

Knowing now that the soloniods went such a small distance, I went back to brainstorming with the help of ChatGPT until it suggested a rack pinon system connected to a servo. This stood out to me beacause I had been testing servos for my final project and knew how to use them. Also the height provided by the rackpin on system was the perfect amount. Heres what it looked like in the example:

And heres the new pushing mechanism which was able to push the heads much higher:

Once I printed out one of them to test, I got a servo with a cicular attactment, screwed it onto the servo and hotglued the gear to the attachment. and the servo to the base. I ran into a slight issue where it was diificult to get the servo to move the gear in the right direction at first due to its angle but after a bit of trial and error, I was able to move the push system with the servo when I tapped the touch sensor. While this wasnt what we wanted to do for the end goal of the project, it showed proof of conecept and I was happy with what I had acompished.

Heres a picture of the setup:

And heres a video of the servo working:

A final thing to note is that I did try to test all the servos and sensors together before I glued the rack pinon to them but I ran into an issue with the amount of amps the servos needed so I got an external power supply of 5 volts and 5 amps for the servos to run on.

Combining Servos and Touch Sensors – Elle

Making the PCB – Cooper

Once we had a working code for all 5 servos and sensors I started designing the PCB. For the PCB I knew it was going to need to be double sided since I had made similar board for my output week. With that in mind I first made a quick sketch of how all the wiring would go so that it wouldnt overlap in a drawing software:

Once I made a sketch where the wires didnt overlap, I started work on the schematic design in KiCAD, with the help of this tutorial explaing how I could make a double sided board. All the componets besides the ESP32C3 were set as 1x3 pinheaders for ease of use but given distinctive names so when in the PCB editor I would know were to put each part before wiring. One key aspect I had to work around was the throughhole componets since thats how I was going to be able to the double sided board. To acomplish this, I made a connection from the throughole to the part of the pinheader that was going to be considered GND. Finally after about 20 minutes of wiring, this is what I made:

Finally in the PCB editor while looking at my initial sketch, I made the board with all the correct wiring. One thing to note is that not only was the double sided board nessicary because of all the wiring, it also was needed to create a common ground shared between the ESP32C3, all the servos, all the touch sensors, and the external power supply for the servos. After a bit of struggling to postion all of them this is the final result:

For the milling, I had already made a board similar to this one so I followed the same procedure where I would do the holes and fcuts first, flip the board over, and then do the bcuts and the edge cuts. After milling the board heres the fianl result of the board on both sides. After milling and soldering heres what the final design turned out like:

And heres the board with the pin headers soldered onto it:

Remaking the PCB – Cooper

After we decided to change to the touch sensors in the ESP32S3 I knew we needed a new board for the new design. Thankfully I was pretty involved in the redoing of the electronics so desgining the board was easy. It also helped that I already had a design I could modify so after a bit of altercations to the schematic design this is what I had:

Wiring in the PCB Editor didn’t take long either. In total redesining the board probably took me 10-30 minutes:

After the board was designed, I milled out my first board but when I soldered it multiple traces ripped so I had to give up on it. Heres the failed board:

After milling the board again, this time I was able to solder on the pin headers and here was the final result:

Since I had designed the board and been apart of the rethinking the electronics, I was able to test one servo on the board and heres the video on that test:

Making the Heads – Elle

To make a 3D print of Neil’s head, Elle Hahn and I used the Makerworld Make My Statue feature.

This is the full statue the AI generated:

I then put the statue in Fusion 360 and cut off the stand because I only wanted the head.

Then, I sent that file to Bambu Lab and made sure to print it with high quality.

( width=50% ) ( width=50% )

Making the Case – Amalia

While Elle and Cooper worked on getting all the heads to move up and down, I worked on making the case for our game. I wanted to laser cut the case because I feel very comfortable using the laser cutter and thought it would be the easiest thing to use for precise cuts for this project. I designed the file in CorelDraw, cardboard cut it, and then eventually, when all the dimensions were essentially perfect, I laser cut on 1/8” wood. It took several cardboard cuts with different dimensions to finally get the box to fit nicely. Once I laser cut all the pieces, I used hot glue to stick them together, but I left the lid off, so that the Servos could be placed inside.

Cooper recommended that we make it press fit, so I designed a case that was approximately 7 inches tall and 12 inches wide. I made a 1” by 1” hole in the back side for the power source and on the top, I made 5 evenly spaced circles with diameters around 3”. At the back of the top, I made some grooves for the backboard of our game.

Here is the CorelDraw box design:

I laser cut the box 3 times on cardboard before finally getting it to look good enough. One of Andrew Puky’s friends was visiting from Germany and wanted to participate, so he helped hot glue the second and third cardboard tests together.

The first time I laser cut the box, I hot glued it together and it ended up ot fitting together even though I thought I had the dimensions correct. I assumed that the issue was because of my mesy hot gluing, so laser cut the same design again and had some help with the hot gluing to see if someone else could make it fit. However, that did not resolve the issue, so instead of going back to the old CorelDraw file and testing every line to make sure it was the right dimension, I just fully redesigned it because the height needed to be increased as well.

Laser cutting the case:

Raster:

Vector:

Here is the laser cut first rendition of the cardboard case:

I did not glue the lid on because I wanted Elle to be able to test the electronics inside of the case.

Here is the laser cut second rendition of the cardboard case:

After figuring out the motor situation, I needed to resize the casing to make sure the head would be able to come up enough and go down enough.

Here are some pictures and a video of Elle and I measuring the situation with the cardboard:

Here is the laser cut third rendition of the case being assembled:

For the backboard, I found a picture online, removed the background, traced it in CorelDraw, and then rastered it onto a piece of wood using the laser cutter.

This is the picture I found online:

This is the picture with the background removed using remove.bg:

This is the picture after tracing it in CorelDraw:

It took me several tries to get the imaged traced without details being removed. I also added the outline for the sign.

Here is the backboard being rastered:

Here is the laser cut backboard on the assembled cardboard casing:

We ended up only using three heads, so we had to recut the top piece of wood.

Assembling Everything

This is the final code that we used:

#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>

// Wi-Fi credentials

// Web server
WebServer server(80);

// Pins and thresholds
const int servoPins[] = {7, 44, 43};     // Servo pins
const int touchPins[] = {2, 3, 4};       // Touch input pins
const long touchThresholds[] = {35000, 35000, 60000};  // Adjust if needed

const int numMoles = 3;
Servo servos[numMoles];

// Angles
const int popUpAngle = 130;  // Mole visible (servo up)
const int hideAngle = 20;    // Mole hidden (servo down)

// Game state
int currentMole = -1;
int score = 0;
int highScore = 0;
bool gameRunning = false;  // Track if game is running
bool lastTouchState = false;  // Track previous touch state to prevent multiple triggers

// Timer variables
unsigned long gameStartTime = 0;
const unsigned long gameDuration = 30000;  // 30 seconds in milliseconds

void setup() {
  Serial.begin(115200);
  randomSeed(analogRead(0));

  // Attach servos and ensure all start hidden
  for (int i = 0; i < numMoles; i++) {
    servos[i].attach(servoPins[i]);
    servos[i].write(hideAngle);
  }
  delay(500); // Give servos time to move to hide position

  // Wi-Fi
  WiFi.begin(ssid, password);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println("\nConnected! IP: " + WiFi.localIP().toString());

  // Define routes
  server.on("/", handleRoot);
  server.on("/start", handleStart);
  server.on("/stop", handleStop);
  server.begin();

  // Initially game is stopped
  currentMole = -1;
}

void loop() {
  server.handleClient();

  if (!gameRunning) return;  // Skip mole logic if game stopped

  // Check if time is up
  unsigned long elapsed = millis() - gameStartTime;
  if (elapsed >= gameDuration) {
    Serial.println("Time's up! Stopping game.");
    stopGame();
    return;
  }

  // Only check for touches if there's an active mole
  if (currentMole == -1) return;

  // Only check the touch sensor corresponding to the current active mole
  long touchVal = touchRead(touchPins[currentMole]);
  bool currentTouchState = (touchVal > touchThresholds[currentMole]);

  // Debug output to help calibrate thresholds
  Serial.printf("Mole %d - Touch value: %ld (threshold: %ld) - %s\n",
                currentMole, touchVal, touchThresholds[currentMole],
                currentTouchState ? "TOUCHED" : "not touched");

  // Only trigger on a new touch (edge detection)
  if (currentTouchState && !lastTouchState) {
    Serial.printf("Hit mole %d! Score: %d -> %d\n", currentMole, score, score + 1);

    // Hide the hit mole immediately
    servos[currentMole].write(hideAngle);
    score++;

    // Update high score
    if (score > highScore) {
      highScore = score;
    }

    // Reset touch state
    lastTouchState = false;

    // Brief delay for visual feedback and debouncing
    delay(300);

    // Spawn the next mole
    spawnNewMole();
  } else {
    lastTouchState = currentTouchState;
  }

  delay(50); // Small delay to prevent overwhelming serial output
}

void spawnNewMole() {
  // Ensure all servos are hidden first
  for (int i = 0; i < numMoles; i++) {
    servos[i].write(hideAngle);
  }

  // Small delay to ensure servos have moved
  delay(200);

  // Select next mole (different from current one)
  int nextMole;
  do {
    nextMole = random(numMoles);
  } while (nextMole == currentMole && numMoles > 1);  // Avoid repeating if possible

  // Update current mole and pop it up
  currentMole = nextMole;
  servos[currentMole].write(popUpAngle);

  // Reset touch state when spawning new mole
  lastTouchState = false;

  Serial.printf("Mole %d popped up!\n", currentMole);
}

void hideAllMoles() {
  for (int i = 0; i < numMoles; i++) {
    servos[i].write(hideAngle);
  }
  currentMole = -1;
}

void stopGame() {
  gameRunning = false;
  hideAllMoles();
}

void handleStart() {
  if (!gameRunning) {
    score = 0;
    gameRunning = true;
    gameStartTime = millis();
    spawnNewMole();
  }
  server.sendHeader("Location", "/");
  server.send(302, "text/plain", "");
}

void handleStop() {
  if (gameRunning) {
    stopGame();
  }
  server.sendHeader("Location", "/");
  server.send(302, "text/plain", "");
}

void handleRoot() {
  unsigned long remaining = 0;
  if (gameRunning) {
    unsigned long elapsed = millis() - gameStartTime;
    if (elapsed < gameDuration) {
      remaining = (gameDuration - elapsed) / 1000;
    }
  }

  String html = "<html><head>"
                "<meta http-equiv='refresh' content='1'/>"
                "<style>"
                "body { font-family: sans-serif; text-align: center; }"
                "button { padding: 10px 20px; font-size: 20px; margin: 10px; }"
                "</style></head><body>";
  html += "<h1>Whack-a-Mole</h1>";

  html += "<p><strong>Score:</strong> " + String(score) + "</p>";
  html += "<p><strong>High Score:</strong> " + String(highScore) + "</p>";
  html += "<p><strong>Current Mole:</strong> " + (currentMole == -1 ? "None" : String(currentMole + 1)) + "</p>";

  if (gameRunning) {
    html += "<p><strong>Time Remaining:</strong> " + String(remaining) + " seconds</p>";
    html += "<p><button onclick=\"location.href='/stop'\">Stop Game</button></p>";
  } else {
    html += "<p><button onclick=\"location.href='/start'\">Start Game</button></p>";
  }

  html += "</body></html>";

  server.send(200, "text/html", html);
}

Final Product:

Possible Improvements

In the future, we could make the final case more aesthetically pleasing and possibly make it so that the final product does not contain open wires. The wires are very nicely hidden in the current game, but we could work on making them completely hidden. We could also maybe add more heads or improve the web server to include different features, like speed and levels.

Files

Casing c3d File

PCB Design

Pushing Mechanism

Head File

Code File

pdf of some ChatGPT searches for the week


Last update: June 3, 2025