Personal1 min read
Online Lô Tô Game
A real-time multiplayer bingo game, containerised with Docker and deployed to a personal ARM server.
- TypeScript
- Next.js
- Tailwind CSS
- Docker
A multiplayer lô tô (bingo) game written in Next.js and containerised to run on my own Armbian server.
Why bingo#
I wanted a real-time shared-state problem small enough to actually finish, but real enough to hit genuine synchronisation issues: many viewers watching one set of numbers, late joiners needing the correct current state, and nobody missing a call.
State lives on the server#
The tempting mistake is letting each client generate numbers and reconcile. Here the server is the single source of truth and clients only render what they are told.
interface Room {
id: string;
/* Every number called, in order. Late joiners replay from this array. */
called: number[];
remaining: number[];
startedAt: number;
}
export function callNext(room: Room): number | null {
if (room.remaining.length === 0) return null;
/* Draw by splicing a random index: O(1) and keeps the distribution even. */
const index = Math.floor(Math.random() * room.remaining.length);
const [value] = room.remaining.splice(index, 1);
room.called.push(value);
return value;
}Deployment#
The whole game runs in one container, using Next.js standalone output to keep the image small:
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN corepack enable pnpm && pnpm install --frozen-lockfile && pnpm build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# standalone bundles only the dependencies actually reachable at runtime
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]Outcome#
- Consistent room state for every player, including mid-game joiners
- A small image that runs comfortably on low-powered ARM hardware
- No database: room state lives in memory for the lifetime of a round