The game is called Box & Dots
This is the game we all used to play in our childhood. Those were good old days. It happened some days ago when I was sitting at home with my wife, my life partner, playing this game on paper with a pen. There were many games we used to enjoy back in the day that children nowadays don’t even know. So I thought, why not digitalize it? That’s where the journey started.
There is something about this game that never gets old. Two people, a pen, and a grid of dots, and suddenly the next thirty minutes are gone. No app, no screen, and no battery required. Just focus on strategy and that satisfying moment when you complete a box and write your initial inside it before your opponent can say anything.
I built this for all of us. It is free. Go play it and tell me if it brought back any memories.
Somewhere in the middle of that game with my wife, it hit me. There are so many people who used to play this who would love to play it again, but the people they want to play with are not in the same room anymore. Some are in different cities. Some are in different countries. And yet this game deserves to be played properly, not forgotten in the back pages of old notebooks.
So I built it.
What the game is, if you somehow forgot
You start with a grid of dots. Players take turns drawing one line at a time, connecting two dots either horizontally or vertically. The goal is to complete the fourth side of a box. The moment you do, that box is yours and you get an extra turn, just like the original rules we all played by. Once every box is claimed, whoever has the most wins. Simple as it always was.
How I built it
The entire game is a single HTML file. No frameworks, no libraries, no backend server. Just HTML, CSS, and vanilla JavaScript. I wanted it to be something anyone could open in a browser and play instantly, with no installation, no account, and nothing standing between you and the game.
Everything that is in it
Play with a friend on the same device
The game tracks turns automatically. After every move it checks whether a box was completed. If yes, the same player goes again. If not, it switches to the other player.
const gained = checkBoxes(currentPlayer);
if (!gained) currentPlayer = 1 - currentPlayer;
Simple as that. One line does the turn switching.
Drawing the board with Canvas
The board is drawn on an HTML Canvas element. Every dot, line, and filled box is rendered using the Canvas 2D API. When you hover over a gap between two dots, it previews your line in your color. When a box is completed, it fills softly, and your emoji appears inside it.
// Draw a placed line with a glow effect
cx.strokeStyle = COLORS[playerIndex];
cx.lineWidth = 3.5;
cx.shadowColor = COLORS[playerIndex];
cx.shadowBlur = 14;
cx.beginPath();
cx.moveTo(a.x, a.y);
cx.lineTo(b.x, b.y);
cx.stroke();
// Draw emoji inside a claimed box
cx.font = `${CELL * 0.28}px serif`;
cx.textAlign = 'center';
cx.textBaseline = 'middle';
cx.fillText(playerAvatar, x + CELL/2, y + CELL/2);
Checking if a box is complete
Every time a line is placed, the game checks all four sides of every neighboring box. If all four sides are drawn, the box is claimed.
function checkBoxes(player) {
let gained = 0;
for (let r = 0; r < G-1; r++) {
for (let c = 0; c < G-1; c++) {
if (!boxes[r][c] &&
hL[r][c] && hL[r+1][c] &&
vL[r][c] && vL[r][c+1]) {
boxes[r][c] = player + 1;
gained++;
}
}
}
return gained;
}
hL holds horizontal lines, vL holds vertical lines. Each cell in the grid just needs all four sides filled.
Undo
Before every move the full game state is pushed onto a stack. Hitting undo pops the last snapshot and restores everything exactly as it was.
// Save state before placing a line
undoStack.push({
hL: hL.map(r => [...r]),
vL: vL.map(r => [...r]),
boxes: boxes.map(r => [...r]),
cur,
scores: [...scores]
});
// Restore on undo
const s = undoStack.pop();
hL = s.hL; vL = s.vL;
boxes = s.boxes;
cur = s.cur;
scores = [...s.scores];
Sound with Web Audio API
No audio files, no external assets. Every sound is generated programmatically using oscillators. A short tone when a line is drawn, a small rising melody when a box is claimed.
function beep(freq, type, duration, volume = 0.18) {
const ac = new AudioContext();
const oscillator = ac.createOscillator();
const gain = ac.createGain();
oscillator.connect(gain);
gain.connect(ac.destination);
oscillator.type = type;
oscillator.frequency.setValueAtTime(freq, ac.currentTime);
gain.gain.linearRampToValueAtTime(volume, ac.currentTime + 0.01);
gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + duration);
oscillator.start();
oscillator.stop(ac.currentTime + duration);
}
// Line placed
beep(360, 'sine', 0.07);// Box claimed — three rising tones
beep(520, 'triangle', 0.08, 0.18);
beep(660, 'sine', 0.10, 0.18);
beep(780, 'sine', 0.12, 0.18);
Play online with a friend anywhere in the world
This was the most interesting part to build. I used PeerJS, which sits on top of WebRTC, to create a direct peer-to-peer connection between two browsers. No server in the middle. No data leaving your device except the move itself.
When you create a room, a unique ID is generated and embedded into a shareable link. Your friend opens that link, both browsers handshake, and from that point every move is sent directly between devices.
// Host creates a room
const peer = new Peer(roomId);
peer.on('connection', conn => {
// Send initial game config to guest
conn.send({ type: 'init', cfg, mySlot: 1 });
});
// Guest joins
const conn = peer.connect(roomId);
conn.on('data', data => {
if (data.type === 'init') startOnlineGame(data.cfg);
if (data.type === 'move') applyRemoteMove(data.line);
});// Sending a move
conn.send({ type: 'move', line: { t: 'h', r: 2, c: 1 } });
The connection stays alive through restarts and the results screen. If you click Leave it asks for confirmation first so your friend does not get disconnected by accident.
Computer opponent
Three difficulty levels. Easy picks randomly. Medium takes any completing move and avoids gifting boxes. Hard does both and favors the center of the board for maximum control.
function aiMove() {
const moves = allMoves();
// Always take a completing move if available
const winning = moves.filter(completesBox);
if (winning.length) return winning[0]; // Avoid giving opponent an easy box
const safe = moves.filter(m => !givesBox(m));
const pool = safe.length ? safe : moves; // Hard: prefer centre of board
if (difficulty === 'hard') {
const mid = (G - 1) / 2;
pool.sort((a, b) =>
Math.hypot(a.r - mid, a.c - mid) -
Math.hypot(b.r - mid, b.c - mid)
);
return pool[0];
} return pool[Math.floor(Math.random() * pool.length)];
}
Results and sharing
When the game ends, a result card is generated with both player names, scores, board size, and total moves. One tap copies it, shares it to WhatsApp, or posts it to Twitter.
const result =
`🎮 Dots & Boxes Result
─────────────────
${p0.av} ${p0.name}: ${s0} boxes
${p1.av} ${p1.name}: ${s1} boxes
─────────────────
${winner} · ${G}×${G} grid · ${totalMoves} moves`;
// Share to WhatsApp
window.open('https://wa.me/?text=' + encodeURIComponent(result));
It is the same game. Same rules, same tension, same joy when you trap your opponent into giving you three boxes in a row. My wife still beat me, by the way. Some things never change.
I built this for all of us. It is free. Go play it and tell me if it brought back any memories.
Go send it to someone you used to pass a notebook with. See if they still have it in them.
box-and-dots.netlify.app








Top comments (0)