A crazy idea that turned into a fun mashup of software and hardware...and all of it with LaravelAugust 27, 2025
By Mathias Hansen

Building a Jeopardy Game for Laravel Live Denmark

A crazy idea that turned into a fun mashup of software and hardware...and all of it with Laravel

As a co-organizer and the MC of Laravel Live Denmark, I was tasked with helping to come up with an idea for entertainment during the conference.

Entertaining on stage gets me pretty excited. Inspired by PHP Jeopardy, we decided to give Laravel Jeopardy a go this year.

But this wasn't going to be just Jeopardy.

I decided to go a little bit wild and build an entire Jeopardy game platform, including physical buzzers with Taylor Otwell announcing the team that buzzed in. And do it all using Laravel—hardware included.

Let's dive in to how it was built.

The plan

The initial thought was to use the existing JeoPHPardy project, but I quickly discovered that the repository hadn't seen updates in 11 years and that it was referencing a buzzer system that doesn't appear to be sold anymore.

I needed a new plan, and I needed it fast, as the conference was only weeks away.

I figured this was a good excuse to try to use Claude Code to build a new greenfield project.

Next, to figure out the idea for the buzzers. Since the conference is in Denmark, I originally considered using bicycle bells. (This is the land of bicycles, after all.) But we'd need to get bells with different sounds in order to be able to tell them apart, and it could get awfully noisy if people rang them continuously.

I've worked with some awesome large arcade-style buttons in the past, so I decided to go that route instead. I ended up finding them at BerryBase in 5 different colors. I figured it would be a fun excuse to use Dan Johnson's Pinout library as well, which lets you control hardware using Laravel. I placed an order, and went ahead with the software.

I didn't waste any time debating what technology stack to use. I went with the TALL stack since it seemed like a good fit and uses frameworks I'm already very familiar with.

After a quick laravel new laravel-jeopardy and Claude /init, I was pretty much ready to s tart. I decided to install Laravel Boost as well since it just came out. Again, why not use this as an excuse to try new things?

Planning with Claude

I fired up claude, shifted to "Plan" mode, and then I started writing up the overall gameplay plan.

First, the game rules:

  • 5 Laravel-themed teams
  • A single round of Jeopardy with a hidden "Daily Double" clue
  • A short Lightning Round of questions after the main round had concluded

After a few tweaks together with Claude, I started focusing on the technical implementation.

It was important for me that we were saving the game state so a browser refresh wouldn't reset the game. We also needed contingencies in case of technical issues and an easy way to control the game throughout.

When I was happy with the final plan, I turned off plan mode and asked Claude to write the entire plan out to a file. This is what I ended up with: PLAN.md.

The great thing about writing it to a file is that it allowed me to read through the whole plan and easily tweak a few things in the document.

Let's do this!

With the plan in hand, I simply asked Claude:

╭────────────────────────────────────────────────────╮ │ ✻ Welcome to Claude Code! │ │ │ │ /help for help, /status for your current setup │ │ │ │ cwd: /Users/codemonkey/projects/laravel-jeopardy │ ╰────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │ > Please implement @PLAN.md │ ╰───────────────────────────────────────────────────────────────────────────╯

After having Claude work through the entire plan, I discovered a few things:

  • The codebase was surprisingly well structured, easy to read, understand and reason with (...though that didn't last long. More on this later)
  • The Jeopardy UI looked a bit plain and boring
  • It didn't look great to have game controls directly in the game

Claude worked particularly well with the TALL stack. Laravel and Tailwind have been around for years and have extensive representation in LLM training data, making Claude highly proficient with these frameworks. The areas where Claude occasionally struggled were with newer additions like Livewire, but Laravel Boost really improved this experience by providing version-current documentation directly to Claude through CLAUDE.md instructions.

I improved the Jeopardy UI pretty quickly by buttering Claude up with a bunch of adjectives. ("Please make the Jeopardy game board look absolutely stunning and amazing" is a surprisingly effective prompt!)

The game controls problem required some more thinking.

The pivot

After sleeping on the problem, I decided to go with a client/server approach.

I decided that my laptop would be connected to the big screens at the conference and show the Jeopardy game board and to use an iPad to control the game flow. This would allow me to add additional controls such as manually adjusting the score for each team, manually triggering the buzzer for a team, etc. (This came in handy during the game.)

I initially didn't think that this was going to be a big problem. The game was already heavily event based, so I figured we would just turn the events into broadcast events and sync the game board with the controller using Laravel Reverb for real-time WebSocket communication.

This did, however, end up costing me some time, as I had to deal with state synchronization bugs between the game board and the control UI. This was primarily because there was no clear responsibility separation between the two interfaces, which left the game board owning some parts of the game logic and the control UI handling other parts.

These bugs manifested in frustrating ways. A clue would be selected in the control UI but wouldn't display on the game board, or team scores would update on one interface but not the other. I was able to manually work through and fix all these issues, but it took time.

Laravel Boost, combined with the Playwright MCP, was particularly handy when working through these issues. Claude would launch the browser through Playwright and then collect browser and server-side logs through the Laravel Boost tools, making debugging the issues significantly faster than manually feeding information to Claude Code.

Claude also helped write tests for the most critical parts of the game flow: things like buzzer logic, score calculations, and game state transitions. Having these tests in place was invaluable when fixing bugs and performing refactors, as they gave me confidence that the core game mechanics wouldn't break.

Coming up with Jeopardy clues

I started out by brainstorming some fun category names with Claude. I ended up with "404: Category Not Found", "Laracon Legends", "Rød Grød Med Fløde" (aka the "Danish category," named for a phrase that Danish speakers famously find it amusing to have foreigners say), "Breaking Prod", "Taylor's Version" (a mashup of Taylor Otwell and Taylor Swift references), and "Eloquently Speaking."

Jeopardy game board UI

Next, it was time to come up with the actual clues for each category. It was important to make sure that there was a mix of easy and harder clues. If the clues were too easy, one team might be able to take over the game by continuously answering correctly.

Again, I used Claude for brainstorming. I ended up generating 100+ ideas and manually created clues for each category loosely inspired by those ideas.

Here are a few of the questions:

Category Difficulty Question Answer
404: Category Not Found 1,000 This HTTP status code means "I'm a teapot" and was an April Fool's joke What is 418?
Laracon Legends 300 These two Australian cities have hosted Laracon AU What are Sydney and Brisbane?
Rød Grød Med Fløde 500 This Danish programmer created PHP in 1994 and was born in Greenland Who is Rasmus Lerdorf?
Breaking Prod 1,000 This phenomenon occurs when a popular cache key expires and multiple processes try to regenerate it simultaneously What is a cache stampede (or thundering herd)?
Taylor's Version 500 Taylor's 2014 hit about relationships gone wrong also describes what PHP displays when you echo an uninitialized variable What is "Blank Space?"

I expected the "Rød Grød Med Fløde" category to be the hardest, especially for non-Danish attendees. But to my surprise, it turned out to be the Taylor Swift-themed questions.

I used a similar approach for the Lightning Round questions and ended up with about 40 questions. The game would automatically pick 5 at random from the list.

The buzzers

A few days later, the web app was basically ready and the colorful arcade buttons had arrived in the mailbox. It was time to wire everything up.

While I've built a bunch of projects in the past using the Arduino platform with microcontroller boards such as the ESP32, I thought it would be fun to use Laravel for the hardware part of the project as well.

I used a Raspberry Pi 3 with a GPIO "hat". The screw terminals on the expansion board made it easier to make a solid connection, which was crucial for surviving getting the whole setup transported to the conference venue.

Raspberry Pi with a GPIO hat

The buttons consisted of one input and one output each. The input of course being the button switch mechanism and the output being a 12V DC LED light.

The switch was connected directly to a GPIO pin on the Raspberry Pi with a pull-up resistor. As the lights were 12V and the Raspberry Pi is running at 3.3V, it was not possible to connect them directly.

I had a bunch of relays at hand, so I used these to turn the lights on and off.

A relay works like a light switch, allowing you to turn any powered source on/off (as long as it doesn't require more current than the relay is rated for). But instead of manually turning the switch on and off, it can be programmed by a low voltage input such as our Raspberry Pi GPIO pin.

4-channel relay

This ended up requiring a fair bit of wiring. Each button had 4 wires, for a total of 10 GPIO and 10 ground connections for all 5 buttons.

I then mounted the buttons to a piece of scrap plywood. Here's what the in-progress wiring looked like:

Wiring in-progress

As soon as everything was wired up, it was finally time to write some code again!

I used the excellent pinout library, built by the incredibly innovative Dan Johnson.

Side note: Go read Dan's blog post on his "Hardware with Laravel" talk from the conference to see just how powerful the pinout library is together with Laravel.

I created a new artisan command with an infinite loop that listens for button events. Once a button has been pressed, the main loop is blocked for a few seconds while the buzz request is sent to the game via a simple API endpoint and the button LED rapidly turns on/off to indicate the team that buzzed in first.

class BuzzerServer extends Command { ... private $buttonPinIds = [ 17, // White 18, // Red 24, // Yellow 25, // Green 5, // Blue ]; private $ledPinIds = [ 21, // White 14, // Red 23, // Yellow 15, // Green 20, // Blue ]; ... public function handle() { $this->init(); while (true) { $this->checkButtons(); usleep(10000); // Sleep for 10ms } } private function checkButtons(): void { foreach ($this->buttonPins as $index => $pin) { if ($pin->isOn()) { $this->info("Button on pin #{$index} is pressed"); try { Http::timeout(1)->get('http://mbp.local:8000/api/buzzer', [ 'pin_id' => $index, ]); } catch (Exception $e) { $this->error("Failed to send request for pin #{$index}: " . $e->getMessage()); } for ($i = 0; $i < 5; $i++) { $this->ledPins[$index]->turnOn(); usleep(100000); // Sleep for 100ms $this->ledPins[$index]->turnOff(); usleep(100000); // Sleep for 100ms } } } } }

This is what that looks like in practice. And yes... I did indeed carefully comb through podcast episodes to locate audio samples with Taylor Otwell so the Laravel founder himself could call out each team name. Make sure to turn up your volume:

Once the buzzer API endpoint receives a request, it sets off a couple of things:

  • The GPIO pin id is translated to a Jeopardy team
  • A broadcast event is fired off
  • The broadcast event triggers a UI update for both the game board and control page
  • An audio clip is played in the browser for the game board
broadcast(new \App\Events\BuzzerPressed($team))->toOthers();

This event is then picked up by both the game board and control UI via Laravel Reverb channels, keeping everything in sync.

Putting it all together

The game was set up during the lunch break on the first conference day, ready for a little post-lunch entertainment to ease attendees back into an afternoon of talks.

To make it easy to set up and as reliable as possible, I decided to run the Jeopardy web app locally on my laptop (instead of deploying to e.g. Laravel Cloud) and connect everything with a portable WiFi router.

This allowed me to test and configure the WiFi connection at home without having to worry about connecting to the venue WiFi (and dealing with potential VLAN restrictions). It also removed reliance on a stable internet connection.

I didn't want to spoil the surprise of the Taylor Otwell buzzer sounds, so during the tech check we ran a simple audio test with the tech crew at the conference by just playing some random music from my laptop. In retrospect, I wish we'd tested with the actual sounds.

We got the game started with 10 volunteers from the audience, but I did hit a few snags along the way.

What I learned

Final preparations and testing before the conference

At first, I couldn't hear the buzzer sounds from the stage, which threw me off in the beginning as I thought that it was a technical issue. I found out halfway through the game that the audience could hear the sound effects just fine (the magic of directional speakers that point away from you). The tech crew later turned up the volume on their side so we could hear it on stage as well.

A side effect of this was that the game participants couldn't hear the buzzers either, so they ended up smashing the buzzers quite enthusiastically whenever they were open (and often when they weren't, too). Given that the buzzer was only locked for a few seconds, it made it possible for another team to unknowingly "steal" the buzzer, which created a bit of confusion.

The continous button-smashing also effectively DOS'ed the poor artisan serve command that was running the game board and control UI. By default, it appears that artisan serve is single-threaded, using a single PHP worker process. This caused the control UI to lock up a few times, requiring a page refresh.

We'd tested the buzzers at home, but this was in a completely quiet room and with much more gentle game participants who did not have the real-world incentive of a prize. In retrospect, this could easily have been resolved by letting the button LEDs blink for longer and adding a longer lockout period between buzz-ins.

One key lesson about working with LLMs on projects like this: as the codebase evolved and the initial clean structure degraded through rapid changes, I realized it would have been more efficient to simply update the PLAN.md file with new requirements and have Claude rebuild from scratch. Since the entire codebase was generated by an LLM in relatively short time with minimal prompting, there's no huge loss in starting over, and you get a clean, well-structured codebase that reflects your current understanding of the problem.

Another takeaway: venue testing matters more than you think. While a full test run at the conference venue wasn't logistically feasible, it would have revealed the directional speaker setup and the potential for enthusiastic button-mashing that only emerges with a live audience. Next time, I'd prioritize even a brief on-site test to catch these things.

If you want to build something similar

If you're inspired to create your own game show hardware:

Hardware recommendations:

  • Arcade buttons: BerryBase (EU) or Adafruit (US) have great options and lots of other fun hardware
  • For a minimal viable buzzer system: You can skip the relay/LED setup and just use simple push buttons connected to GPIO pins
  • Make sure to order all necessary connectors and wires if you don't already have some on hand

Software architecture tips:

  • Start with clear separation between game state, UI, and control logic
  • Use a proper web server (nginx/Caddy) instead of artisan serve for any live event
  • Test with multiple concurrent users early in development

In conclusion

Despite a few hiccups on-stage, this was an incredibly fun and rewarding project, and I learned a lot along the way. It was a great excuse to get to try out some new tech such as Laravel Boost and building something practical with the Pinout library. I'm looking forward to using them again!

Subscribe to Code and Coordinates

Get the latest articles about software development, data science, and geospatial technology

Copyright © 2014-2025 Dotsquare LLC, Norfolk, Virginia. All rights reserved.