Mahjong Solitaire is the classic puzzle of removing matching pairs from 144 tiles stacked into a three-dimensional pile. It packs in everything that makes puzzle programming interesting: managing 3D coordinates, deciding geometrically whether a piece can be taken, and generating problems that are guaranteed to be solvable.
Each tile becomes an object holding a kind and a position. The position is three-dimensional: x and y move in steps of 0.5 (some tiles sit half-offset on the pile) and z is the layer. Keep all 144 coordinates in a constant so they form the classic "turtle" shape.
// One tile = a kind plus 3D coordinates
// x and y step by 0.5 (some tiles overlap by half), z is the layer
var tile = { kind: 'm1', x: 6.5, y: 3.5, z: 4 };
// Turtle layout: expand rows of [z, y, starting x, count]
var TURTLE_ROWS = [
[0, 0, 1, 12], // bottom layer, first row: 12 tiles
[0, 1, 3, 8], // second row: 8 tiles ...
[1, 1, 4, 6], // second layer: 6x6 ...
];
Unicode mahjong characters (๐) render differently from font to font, so load an SVG image per tile instead (this game uses public-domain artwork from Wikimedia Commons; drawing your own in CSS works too). Position each div absolutely with left = x × tile width and top = y × tile height, then shift higher layers slightly up and to the left while raising their z-index — that is all it takes to read as a stack.
The trick here is to express the offset as a ratio of the tile size, not a fixed pixel count. Writing it as calc(z × var(--tile-width) × 0.19) keeps the pile looking equally stacked on a small phone and a large desktop. With fixed pixels, the 3D effect quietly disappears on big screens and the board looks flat.
The heart of the rules is deciding which tiles may be taken. There are two conditions: nothing is stacked on top, and at least one side is clear. Both can be tested with nothing more than coordinate subtraction.
function isFree(tile, tiles) {
// Blocked if a tile one layer up overlaps even by half
var blockedTop = tiles.some(function (t) {
return t.z === tile.z + 1 &&
Math.abs(t.x - tile.x) < 1 && Math.abs(t.y - tile.y) < 1;
});
if (blockedTop) return false;
// The left and right neighbours on the same layer
var leftBlocked = tiles.some(function (t) {
return t.z === tile.z && t.x === tile.x - 1 &&
Math.abs(t.y - tile.y) < 1;
});
var rightBlocked = tiles.some(function (t) {
return t.z === tile.z && t.x === tile.x + 1 &&
Math.abs(t.y - tile.y) < 1;
});
return !leftBlocked || !rightBlocked; // free if either side is open
}
The important detail is Math.abs(...) < 1. A tile is one unit wide, so any coordinate difference below 1 means the tiles overlap — which correctly catches the half-offset tiles too. When a tapped tile returns false from this function, give it a little wobble to show it cannot be taken.
Placing tiles at random produces boards that simply cannot be finished, and fairly often. The answer is to run the game backwards. Treat every coordinate as filled, pick two slots that are currently free, assign them the same tile kind, and remove them. Repeat 72 times and every coordinate has a tile.
function generateSolvable() {
var pool = buildPairPool(); // 72 pairs of tile kinds, pre-shuffled
var slots = TURTLE_LAYOUT.map(...); // every coordinate starts filled=true
var assign = {};
for (var p = 0; p < 72; p++) {
// List the slots that are free right now
var freeSlots = slots.filter(s => s.filled && isFreeSlot(s, slots));
if (freeSlots.length < 2) return generateSolvable(); // rare dead end: retry
var a = pick(freeSlots);
var b = pick(freeSlots.filter(s => s !== a));
assign[a.id] = pool[p][0]; assign[b.id] = pool[p][1];
a.filled = b.filled = false; // mark them as removed
}
return assign; // every coordinate now has a kind: the board is ready
}
Read the removal order backwards and you have the instructions for building the pile; read it forwards and you have a winning line of play. That is exactly why a board built this way always has an answer. The hint feature is then only a matter of finding a currently takeable pair and flashing it — a few lines, reusing isFree from step 2.
Nice work! Managing tiles in 3D coordinates, deciding freedom by geometry, and guaranteeing solvability through reverse generation — those three are the heart of Mahjong Solitaire. To take it further, add a hint penalty and turn it into a time attack, offer a shuffle to rescue players who get stuck, or build alternative layouts such as a pyramid or a dragon. The idea that "building it backwards guarantees a solution" carries over to generating mazes, chess problems and many other puzzles.
A: The coordinate thinking takes a little getting used to, but the drawing is just positioning divs, which is friendlier than Canvas. Getting comfortable with arrays through Solitaire or Memory first makes this a smooth next step. If you get stuck, you can always ask at a HinaTech class.
A: About 40 minutes to get the tiles on screen, another 30 for free-tile detection and pair removal, and one and a half to two hours in total once solvable layout generation is included.