The best software I wrote for a spring dinner was not a dashboard or an internal tool. It was a set of deliberately over-the-top games: a multiplayer memory game that uses people's phones as controllers, a giant rock-paper-scissors display, and a lottery machine that looks like it belongs in a science-fiction movie.
I originally made these as small event toys for one evening. After removing private names, internal references, and event-specific details, I put the three projects on GitHub because someone else may need exactly this kind of software for a party, classroom, meetup, or company event. This is not a product launch post. It is a tour of what I actually built, the small implementation decisions that made the games work in a room full of people, and the limitations I would want to know before running them in public.
One event, three different interaction models
The three projects look like variations on the same idea - put something fun on a large screen - but they solve different coordination problems.
- Multiplayer Dance Memory Game has a host screen and many player screens. The host shows an arrow sequence; players reproduce it on their phones. A wrong answer eliminates a player.
- Rock Paper Scissors Display is a single-screen game. Nobody has to connect or enter a name; the display chooses a result and turns that result into a dramatic 3D animation.
- Open Lottery Display is an event-friendly number drawer. It draws without replacement, supports excluded numbers, and keeps a visible history.
That difference matters. The first game needs a server to be authoritative. The second needs an animation that never visually lands on the wrong card. The third needs a pool model that makes repeated winners impossible until someone resets it.
All three are intentionally small Node.js applications. They serve static files with Express, keep the browser UI in plain HTML/CSS/JavaScript, and avoid a database. That makes them easy to copy to a laptop connected to a projector, but it also defines their boundaries.
The multiplayer game treats the server as the referee
The dance game is the most technically different of the three. The player page and the host page are separate views, but both connect to the same Socket.IO server. The server owns one in-memory game state:
let gameState = {
players: {}, // { socketId: { id, name, avatar, alive } }
round: 0,
sequence: [],
isInputPhase: false,
difficulty: 3,
};
When the host starts, the server generates a sequence from four directions and broadcasts it to every connected browser:
function startRound() {
gameState.sequence = [];
for (let i = 0; i < gameState.difficulty; i++) {
gameState.sequence.push(ARROWS[Math.floor(Math.random() * ARROWS.length)]);
}
io.emit('round_start', {
round: gameState.round,
sequence: gameState.sequence,
});
const showTime = gameState.sequence.length * 800 + 1500;
setTimeout(() => {
gameState.isInputPhase = true;
io.emit('start_input_phase', {
duration: 5000,
hideSequence: gameState.round >= 4,
});
setTimeout(() => {
endRound();
}, 5000);
}, showTime);
}
The timing is intentionally simple: each arrow is shown for 800 milliseconds, then there is a 1.5-second buffer before the five-second input window. Starting with round four, the host tells players to enter the pattern after hiding it. The sequence is not generated independently in each browser, so every participant is answering the same challenge.
The important part is where the answer is checked. The client sends the array it collected from button taps, but the server compares it with the server-owned sequence:
socket.on('submit_input', (inputData) => {
const player = gameState.players[socket.id];
if (!player || !player.alive) return;
const isCorrect =
JSON.stringify(inputData) === JSON.stringify(gameState.sequence);
if (isCorrect) {
io.emit('player_correct', { id: socket.id });
} else {
player.alive = false;
io.emit('player_eliminated', { id: socket.id });
socket.emit('you_died');
}
});
This is not a security-grade protocol, but it is the right authority boundary for a party game. A client does not get to declare itself correct. The host display receives the same player_correct and player_eliminated events, which lets it animate surviving players dancing and eliminated players disappearing.
The game also refuses new players after the first round starts. When the surviving player count falls to ten or fewer, the server broadcasts the winners and returns the room to its lobby state. A restart is enough to clear everything because there is no persistence.
The rock-paper-scissors result is chosen before the animation
The rock-paper-scissors project has no multiplayer protocol. Its main problem is psychological: if three cards orbit for several seconds and the animation happens to stop between cards, the result feels broken. If the code chooses the winner after the animation, it becomes even harder to guarantee that the visual state and the logical state agree.
I choose the result first:
const choices = ['rock', 'paper', 'scissors'];
const finalChoice = choices[Math.floor(Math.random() * choices.length)];
const finalChoiceIndex = choices.indexOf(finalChoice);
const baseAngles = [0, 120, 240]; // rock, paper, scissors
const winnerBaseAngle = baseAngles[finalChoiceIndex];
Then I calculate a rotation that guarantees the selected card ends at the front:
const minRotations = 12;
const minTotalRotation = minRotations * 360;
const n = Math.ceil((minTotalRotation + winnerBaseAngle) / 360);
const targetRotation = n * 360 - winnerBaseAngle;
The cards are updated manually in a requestAnimationFrame loop rather than relying on a CSS animation with an approximate duration. The first 80 percent of the run is linear, and the last 20 percent uses an ease-out cubic curve. At the end, every card receives its exact final transform, and only then does the code mark the selected card as the winner and launch fireworks.
That separation is useful beyond this toy. Whenever an animation represents a discrete result - a roulette wheel, a slot reel, a prize picker - the state should be decided before the animation begins. The animation is presentation; it should not be the source of truth.
The display also has a few event-friendly controls. Pressing Space starts a round, the duration can be changed from three to seven seconds, and the effect quality can be set to low, medium, or high. Settings are saved in localStorage, so a presenter can tune the display once without configuring it before every round.
The lottery pool uses removal instead of rerolling
The lottery display has a slightly different invariant: a number that has already been drawn must not be available again. The initial pool is configured for 3 through 76, excluding 25 and 65:
this.poolMin = 3;
this.poolMax = 76;
this.excludedNumbers = [25, 65];
this.availableNumbers = [];
this.drawnNumbers = [];
The pool is built by walking the inclusive range and skipping excluded values. Drawing is then a single random index followed by splice:
generateRandomNumber() {
if (this.availableNumbers.length === 0) {
return null;
}
const randomIndex = Math.floor(Math.random() * this.availableNumbers.length);
const number = this.availableNumbers.splice(randomIndex, 1)[0];
return number;
}
That is simpler and more reliable than repeatedly generating a random number and rerolling when it has appeared before. The array itself is the source of truth: if a number is present, it can be drawn; once removed, it cannot be drawn again until reset.
The visual result is a separate pipeline. The chosen number is split into tens and ones, and both slot reels roll concurrently, with the tens reel stopping after two seconds and the ones reel after three:
await Promise.all([
this.smoothRollSingleReel(this.slotReelTens, tensDigit, 2000),
this.smoothRollSingleReel(this.slotReelOnes, onesDigit, 3000)
]);
await this.aiScanEffect();
await this.revealResult(result);
this.particleExplosion();
this.addToHistory(result);
The history is only an in-memory list for the current page. Reset rebuilds the pool, clears the history, removes the celebration effects, and puts the slot reels back to ??. The settings panel can change the inclusive range from 0 through 99 and add more excluded values.
What I would not pretend these games are
These projects are useful precisely because they are small, but small also means there are sharp edges.
The multiplayer game stores everything in one process. A server restart clears the room, and there is no authentication, HTTPS, rate limiting, or input validation. Player names and avatar data are inserted into host-side HTML, so I would only run the unmodified version on a trusted local network. For a public deployment, I would add validation and safe DOM rendering before worrying about visual polish.
The two single-screen games use Math.random(). That is fine for a fun event where nobody is claiming cryptographic fairness. It is not appropriate for a regulated lottery, a prize draw with legal requirements, or anything where an audit trail matters. The lottery's no-repeat guarantee is a data-structure guarantee, not proof of unpredictable randomness.
The animations also assume a reasonably capable modern browser. The low-quality setting reduces canvas node counts and visual work, but it does not remove every effect. On a very old laptop connected to a large display, I would test the actual hardware before the audience arrives.
Running them locally
Each repository has its own README and can be started independently with Node.js 18 or later.
For the multiplayer version, run npm install and npm start, then open /host on the shared display and / on each phone. Use the host machine's local IP address instead of localhost when the phones are on other devices.
The rock-paper-scissors display runs on port 3001 by default, while the lottery display runs on port 3000. Both also include Electron entry points for a Windows desktop build. The projects are intentionally independent, so you can run only the game that matches your event instead of bringing up a larger application.
I made these for one spring dinner, removed the event-specific details, and published them so other developers can try them, adapt them, or simply borrow the ideas. The three repositories are Multiplayer Dance Memory Game, Rock Paper Scissors Display, and Open Lottery Display. If you run one at your own event, I would genuinely like to hear what broke first.
This article was originally published by DEV Community and written by Joe Lin.
Read original article on DEV Community