Ludo Game Source Code in HTML and JavaScript (Free, with Real Rules and a Computer Player)
A complete Ludo game in one index.html: 2 or 4 players, the real rules (a 6 to start, captures, safe stars, exact roll home, extra turns) and a computer player, tested move by move. Plus how to put it in an Android app, and what Flutter, Unity and online Ludo add.
Ludo is one of the most played board games in the world, and building it is a great JavaScript project: a board, dice, turns, captures and a computer opponent. This guide gives you a complete Ludo game in one index.html file with the real rules, for 2 or 4 players, with an optional computer player. We tested it move by move with fixed dice.
The rules this game follows
| Rule | How it works |
|---|---|
| Start | Each player has 4 tokens in their base. A token leaves the base only with a 6, onto its colour's start square. |
| Move | Tokens move clockwise by the number on the dice, once around the board (51 squares), then up their own coloured home column. |
| Extra turn | Rolling a 6, capturing a token, or bringing a token home gives another roll. |
| Capture | Landing on an opponent's token sends it back to its base. |
| Safe squares | Start squares and star squares are safe: tokens there cannot be captured. |
| Home | A token needs the exact number to reach home. The first player with all 4 tokens home wins. |
House rules vary (some lose a turn after three 6s in a row, some let two tokens form a block); the code below is easy to change for those.
The full Ludo source code (HTML5 and JavaScript)
Save it as index.html and open it in any browser, on a computer or a phone. Click the dice, then the glowing token you want to move.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ludo</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 16px; background: #f1f5f9; text-align: center; }
#board { display: grid; grid-template-columns: repeat(15, 1fr); grid-template-rows: repeat(15, 1fr); width: min(94vw, 540px); aspect-ratio: 1;
margin: 10px auto; border: 3px solid #1e293b; background: #fff; }
.cell { border: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: center; min-width: 0; min-height: 0;
overflow: hidden; flex-wrap: wrap; gap: 1px; font-size: 12px; color: #94a3b8; }
.red { background: #fecaca; } .green { background: #bbf7d0; } .yellow { background: #fef08a; } .blue { background: #bfdbfe; }
.base.red { background: #ef4444; } .base.green { background: #22c55e; } .base.yellow { background: #eab308; } .base.blue { background: #3b82f6; }
.centre { background: #1e293b; color: #fff; font-weight: 800; font-size: 14px; }
.spot { background: #fff !important; border-radius: 50%; }
.token { width: 40%; aspect-ratio: 1; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 1px 3px #0006; }
.token.red { background: #dc2626; } .token.green { background: #16a34a; } .token.yellow { background: #ca8a04; } .token.blue { background: #2563eb; }
.token.can { outline: 3px solid #0f172a; cursor: pointer; animation: pulse .8s infinite alternate; }
@keyframes pulse { to { transform: scale(1.25); } }
button, select { font: inherit; padding: 9px 16px; margin: 4px; }
#dice { font-size: 28px; font-weight: 800; min-width: 60px; }
</style>
</head>
<body>
<h1>Ludo</h1>
<div>
<select id="players"><option value="2">2 players</option><option value="4">4 players</option></select>
<label><input type="checkbox" id="cpu" checked> Computer plays the others</label>
<button id="new">New game</button>
</div>
<div id="board"></div>
<button id="dice">π²</button>
<p id="msg"></p>
<script>
// The 52 squares of the track, clockwise, as [row, column] on a 15 x 15 board
const TRACK = [];
const run = (r, c, dr, dc, n) => { for (let i = 0; i < n; i++) TRACK.push([r + dr * i, c + dc * i]); };
run(6, 1, 0, 1, 5); run(5, 6, -1, 0, 6); TRACK.push([0, 7]);
run(0, 8, 1, 0, 6); run(6, 9, 0, 1, 6); TRACK.push([7, 14]);
run(8, 14, 0, -1, 6); run(9, 8, 1, 0, 6); TRACK.push([14, 7]);
run(14, 6, -1, 0, 6); run(8, 5, 0, -1, 6); TRACK.push([7, 0]); TRACK.push([6, 0]);
const COLORS = ['red', 'green', 'yellow', 'blue'];
const START = [0, 13, 26, 39]; // each colour's first square on the track
const SAFE = [0, 8, 13, 21, 26, 34, 39, 47]; // start squares and star squares: no capture here
const HOME_COL = [[[7, 1], 0, 1], [[1, 7], 1, 0], [[7, 13], 0, -1], [[13, 7], -1, 0]];
const BASE = [[2, 2], [2, 11], [11, 11], [11, 2]];
const FINISH = 56; // 0-50 track, 51-55 home column, 56 home
let game;
// Run fn later, but only if the same game is still on (a new game cancels old timers)
function later(fn, ms) { const g = game; setTimeout(() => { if (g === game) fn(); }, ms); }
function newGame() {
const n = +document.getElementById('players').value;
const seats = n === 2 ? [0, 2] : [0, 1, 2, 3];
const cpu = document.getElementById('cpu').checked;
game = {
players: seats.map((c, i) => ({ color: c, tokens: [-1, -1, -1, -1], cpu: cpu && i > 0 })),
turn: 0, dice: 0, rolled: false, winner: null
};
document.getElementById('dice').textContent = 'π²';
draw();
say();
}
// Where a token stands on the board: [row, column], or null if it is home
function cellOf(color, pos, token) {
if (pos === -1) { const [r, c] = BASE[color]; return [r + (token >> 1), c + (token & 1)]; }
if (pos <= 50) return TRACK[(START[color] + pos) % 52];
if (pos < FINISH) { const [[r, c], dr, dc] = HOME_COL[color]; const k = pos - 51; return [r + dr * k, c + dc * k]; }
return null;
}
function canMove(pos, dice) {
if (pos === -1) return dice === 6;
return pos + dice <= FINISH;
}
function movable() {
const p = game.players[game.turn];
return p.tokens.map((pos, i) => (canMove(pos, game.dice) ? i : -1)).filter(i => i >= 0);
}
function roll() {
if (game.winner || game.rolled) return;
game.dice = 1 + Math.floor(Math.random() * 6);
game.rolled = true;
document.getElementById('dice').textContent = game.dice;
const moves = movable();
draw();
if (!moves.length) { say('No move with ' + game.dice + '.'); later(nextTurn, 700); return; }
if (game.players[game.turn].cpu) later(() => move(cpuChoice(moves)), 600);
else say('Tap a glowing token.');
}
function move(i) {
const p = game.players[game.turn];
if (!game.rolled || !canMove(p.tokens[i], game.dice)) return;
const from = p.tokens[i];
p.tokens[i] = from === -1 ? 0 : from + game.dice;
const pos = p.tokens[i];
let bonus = game.dice === 6;
// Capture: other colours on the same track square go back to base, unless it is a safe square
if (pos <= 50) {
const square = (START[p.color] + pos) % 52;
if (!SAFE.includes(square)) {
for (const o of game.players) {
if (o === p) continue;
o.tokens.forEach((op, k) => {
if (op >= 0 && op <= 50 && (START[o.color] + op) % 52 === square) { o.tokens[k] = -1; bonus = true; }
});
}
}
}
if (pos === FINISH) bonus = true;
if (p.tokens.every(t => t === FINISH)) { game.winner = p; draw(); say(); return; }
game.rolled = false;
draw();
if (bonus) { say('Roll again!'); if (p.cpu) later(roll, 600); }
else nextTurn();
}
// The computer's choice: finish, capture, leave base, otherwise move the token that is furthest ahead
function cpuChoice(moves) {
const p = game.players[game.turn];
const score = i => {
const from = p.tokens[i], to = from === -1 ? 0 : from + game.dice;
if (to === FINISH) return 1000;
if (to <= 50) {
const sq = (START[p.color] + to) % 52;
const hit = !SAFE.includes(sq) && game.players.some(o => o !== p && o.tokens.some(t => t >= 0 && t <= 50 && (START[o.color] + t) % 52 === sq));
if (hit) return 900;
}
if (from === -1) return 800;
return to;
};
return moves.reduce((a, b) => (score(b) > score(a) ? b : a));
}
function nextTurn() {
game.rolled = false;
game.turn = (game.turn + 1) % game.players.length;
draw();
say();
if (game.players[game.turn].cpu) later(roll, 600);
}
function say(text) {
const p = game.players[game.turn];
const name = COLORS[p.color].toUpperCase();
document.getElementById('msg').textContent = game.winner
? COLORS[game.winner.color].toUpperCase() + ' wins!'
: name + (p.cpu ? ' (computer)' : '') + ': ' + (text || 'roll the dice.');
}
function draw() {
const board = document.getElementById('board');
board.innerHTML = '';
const cells = [];
for (let r = 0; r < 15; r++) for (let c = 0; c < 15; c++) {
const d = document.createElement('div');
d.className = 'cell';
if (r < 6 && c < 6) d.classList.add('base', 'red');
else if (r < 6 && c > 8) d.classList.add('base', 'green');
else if (r > 8 && c > 8) d.classList.add('base', 'yellow');
else if (r > 8 && c < 6) d.classList.add('base', 'blue');
else if (r > 5 && r < 9 && c > 5 && c < 9) { d.classList.add('centre'); }
board.appendChild(d);
cells.push(d);
}
const at = (r, c) => cells[r * 15 + c];
COLORS.forEach((col, i) => {
at(...TRACK[START[i]]).classList.add(col);
for (let k = 51; k < FINISH; k++) at(...cellOf(i, k)).classList.add(col);
});
SAFE.forEach(s => { if (!START.includes(s)) at(...TRACK[s]).textContent = 'β
'; });
game.players.forEach(p => [0, 1, 2, 3].forEach(i => at(...cellOf(p.color, -1, i)).classList.add('spot')));
const home = { 0: 0, 1: 0, 2: 0, 3: 0 };
const canNow = game.rolled && !game.players[game.turn].cpu ? movable() : [];
game.players.forEach((p, pi) => p.tokens.forEach((pos, i) => {
if (pos === FINISH) { home[p.color]++; return; }
const t = document.createElement('div');
t.className = 'token ' + COLORS[p.color];
if (pi === game.turn && canNow.includes(i)) { t.classList.add('can'); t.onclick = () => move(i); }
at(...cellOf(p.color, pos, i)).appendChild(t);
}));
// Finished tokens: a count in each colour's side of the centre
[[7, 6], [6, 7], [7, 8], [8, 7]].forEach(([r, c], col) => { if (game.players.some(p => p.color === col)) at(r, c).textContent = home[col]; });
}
document.getElementById('dice').onclick = () => { if (!game.players[game.turn].cpu) roll(); };
document.getElementById('new').onclick = newGame;
newGame();
</script>
</body>
</html>
How the code works
| Part | What it does |
|---|---|
TRACK | The 52 squares of the shared track as row and column on a 15 × 15 grid, built from straight runs |
START and SAFE | Where each colour enters the track, and the 8 squares where nobody can be captured |
| Token position | One number per token: -1 in base, 0 to 50 on the track, 51 to 55 in the home column, 56 home. Every rule becomes simple arithmetic. |
cellOf() | Turns a colour and a position into the square to draw the token on |
canMove() | A 6 to leave the base, and never past home: the exact-roll rule |
move() | Moves the token, sends opponents on the same square home (unless it is safe), and gives an extra turn for a 6, a capture or a token home |
cpuChoice() | The computer brings a token home if it can, otherwise captures, otherwise leaves the base, otherwise moves its most advanced token |
later() | Delays the computer's moves so you can follow them, and cancels them if you start a new game |
How to create your own Ludo game: next steps
- Three 6s rule: count sixes in a row in
roll()and end the turn on the third - Blocks: in
move(), stop a token passing a square where an opponent has two tokens - Animation: move the token one square at a time with a short delay instead of jumping
- Sound and a nicer dice: a roll sound and a dice drawn with dots
- A smarter computer: avoid ending on squares an opponent can reach with one roll
Ludo game source code on GitHub
You will find many Ludo projects on GitHub, in JavaScript, Python, Java and C#. Most stop at a board and dice; the hard parts are the rules above and keeping turns right when extra rolls, captures and the computer all happen together. Whatever you start from, test those cases. Our version was checked with fixed dice for each rule. Put your own version on GitHub with a short README and a screenshot.
Ludo in Android Studio
The quickest way to put this game in an Android app is to load index.html from the app's assets folder in a WebView, as shown in our website to app guide. A native version in Kotlin or Java keeps the same logic (the track, the positions and the rules) and draws the board on a Canvas instead.
Ludo in Flutter and Unity
- Flutter: there are open-source Flutter Ludo games on GitHub; the board becomes a grid of widgets and the rules stay the same. Good if you want one code base for Android and iPhone.
- Unity: free and paid Ludo kits exist on the Unity Asset Store and GitHub, with 3D dice and animations. Unity adds polish but also a much bigger app and a steeper learning curve.
What apps like Ludo Master and Ludo King add
The popular Ludo apps are the same rules plus a lot of infrastructure: online play with friends or strangers, private rooms with codes, chat and voice during the game, coins and rewards, leaderboards, and a server that rolls the dice so nobody can cheat. That server part is what takes months: matchmaking, keeping two or four phones in sync, handling players who drop out, and stopping cheats.
Online Ludo with chat and video, ready to publish
Our chat apps include Ludo played live between people inside the chat, on your own servers:
- Live video chat app source code (Java): one-to-one and group chat, voice and video calls, and five games in the chat: Ludo, Tic Tac Toe, Words, Pool and Dare
- Video call chat app source code (Java): chat, calls, and Ludo, Tic Tac Toe and Connect 4
To publish browser games like this one on your own games website, our HTML5 game portal script gives you game pages, categories, search and an admin panel, and our guide on making a game website like Poki explains how such sites earn.