LinkedIn Mini Sudoku Solver
Mini Sudoku is LinkedIn’s 6×6 daily: fill every cell with 1–6 so each row, each column, and each region holds all six digits. The twist is the regions — they can be the usual rectangular boxes or an irregular jigsaw (“bent”) shape — so the script reads them off the board rather than assuming. It reads the grid, solves it, taps the digits in on the number pad, and verifies the result. On a fresh puzzle it fills the board in one pass and LinkedIn shows the win screen.
Script
Open the puzzle, open the console (F12), and paste this. It’s the full script from the bottom of this page, golfed as far as it will go:
(async () => {
const C = [...document.querySelectorAll('[data-cell-idx]')].sort((a, b) => a.dataset.cellIdx - b.dataset.cellIdx);
const n = Math.round(Math.sqrt(C.length));
const D = el => { const t = (el.querySelector('.sudoku-cell-content')?.textContent || '').trim(); return /^[1-9]$/.test(t) ? +t : 0; };
const par = [...C.keys()], find = x => par[x] === x ? x : par[x] = find(par[x]), uni = (a, b) => par[find(a)] = find(b);
const wl = (el, s) => el.classList.contains('sudoku-cell-wall-' + s);
for (let i = 0; i < n * n; i++) { const r = i / n | 0, c = i % n;
if (c < n - 1 && !wl(C[i], 'right') && !wl(C[i + 1], 'left')) uni(i, i + 1);
if (r < n - 1 && !wl(C[i], 'bottom') && !wl(C[i + n], 'top')) uni(i, i + n); }
const g = [], reg = [], ed = [];
for (let i = 0; i < n * n; i++) { const r = i / n | 0, c = i % n, pre = C[i].classList.contains('sudoku-cell-prefilled');
(g[r] ??= [])[c] = pre ? D(C[i]) : 0; (reg[r] ??= [])[c] = find(i); if (!pre) ed.push([r, c]); }
const B = {}; for (const b of document.querySelectorAll('.sudoku-input-button')) { const t = b.textContent.trim(); if (/^[1-9]$/.test(t)) B[t] = b; }
const rU = [...Array(n)].map(() => new Set()), cU = [...Array(n)].map(() => new Set()), gU = new Map();
for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (!gU.has(reg[r][c])) gU.set(reg[r][c], new Set());
const S = []; let out = null;
const ok = (r, c, d) => !rU[r].has(d) && !cU[c].has(d) && !gU.get(reg[r][c]).has(d);
const dfs = i => { if (i === n * n) { out = S.map(x => x.slice()); return 1; }
const r = i / n | 0, c = i % n, gv = g[r][c], cand = gv ? [gv] : [...Array(n)].map((_, k) => k + 1);
for (const d of cand) { if (!ok(r, c, d)) continue; (S[r] ??= [])[c] = d; rU[r].add(d); cU[c].add(d); gU.get(reg[r][c]).add(d);
if (dfs(i + 1)) return 1; S[r][c] = 0; rU[r].delete(d); cU[c].delete(d); gU.get(reg[r][c]).delete(d); } return 0; };
if (!dfs(0)) { const sz = {}; for (const row of reg) for (const id of row) sz[id] = (sz[id] || 0) + 1; const sl = Object.values(sz).sort((a, b) => a - b), gc = g.flat().filter(Boolean).length, wc = C.reduce((a, e) => a + [...e.classList].filter(x => x.startsWith('sudoku-cell-wall-')).length, 0), bad = sl.length !== n || sl.some(s => s !== n); return console.error(`🔢 Mini Sudoku: can't solve the board as read — ${sl.length} region(s) of sizes [${sl}], ${gc} givens, ${wc} walls.` + (bad ? (wc === 0 ? ' No borders found — the grid may still be rendering; wait a moment and re-run.' : ' A region border was misread.') : ' Read looks clean but has no solution — please report.')); }
const T = el => { const b = el.getBoundingClientRect(), o = { bubbles: 1, cancelable: 1, composed: 1, view: window, button: 0, clientX: b.left + b.width / 2, clientY: b.top + b.height / 2, pointerId: 1, pointerType: 'mouse', isPrimary: 1 }; for (const t of 'pointerdown mousedown pointerup mouseup click'.split(' ')) el.dispatchEvent(new PointerEvent(t, o)); };
for (const [r, c] of ed) { const cell = C[r * n + c], btn = B[out[r][c]]; for (let k = 0; k < 3 && D(cell) !== out[r][c]; k++) { T(cell); await new Promise(z => setTimeout(z, 60)); T(btn); await new Promise(z => setTimeout(z, 60)); } }
console.log(`🔢 Mini Sudoku ${n}×${n} solved ✅`, out.map(r => r.join('')).join(' / '));
})();
The one insight that makes it easy
Like the other LinkedIn games, the whole board is in the DOM. Each of the 36
[data-cell-idx] cells keeps its digit in a .sudoku-cell-content child, and
locked givens carry the class sudoku-cell-prefilled. The number pad is a row of
.sudoku-input-buttons, one per digit.
The genuinely new part is the regions. Their boundaries are drawn as
sudoku-cell-wall-{right,bottom,top,left} classes — the thick borders between
cells. Two neighbours belong to the same region exactly when there’s no wall
between them, so a union-find over the no-wall adjacencies recovers the six
regions — rectangular boxes or a jigsaw, whatever the day ships. No pixels, no
guessing.
The algorithm
With the regions in hand it’s an ordinary Sudoku: fill each cell with 1–6 so every row, column, and region contains each digit once. A plain depth-first backtrack over the empty cells settles it instantly — at each cell, try the digits not already used in its row, column, or region; recurse; undo on failure. Because the regions are just opaque groups of cells, the same search handles the rectangular and jigsaw layouts with no special cases. The whole prune is one check:
const canPut = (r, c, d) =>
!rowUsed[r].has(d) && !colUsed[c].has(d) && !regionUsed[region[r][c]].has(d);
Run the same search in counting mode and a real Mini Sudoku returns exactly one solution — so if the script ever reads a board that solves more than one way, it warns: that’s the tell that a wall was misread. And when the search comes back with no solution, the script doesn’t blame the puzzle — it reports what it actually read (how many regions, their sizes, how many border walls it found). A real board is six regions of six cells; a single 36-cell region with zero walls just means the grid hadn’t finished rendering, so the fix is to run it again a second later, not to give up.
Driving the board
Entering a digit takes two taps: tap the cell to select it, then tap that digit
on the number pad. As with Queens and Tango, element.click() is ignored — the
game listens for a full pointerdown → mousedown → pointerup → mouseup → click
sequence — and locked givens are never touched. It taps each cell up to three
times until the digit lands, so a dropped tap self-corrects. When the last cell
is filled the grid is complete and LinkedIn shows its win screen.
How it was built
The solver’s core is a pure function unit-tested with node --test — a
rectangular-box 6×6, a discriminating region test (the same givens solve
differently under different region layouts, proving the solver honours the
regions instead of hardcoding boxes), a hand-built 4×4 jigsaw, an infeasible
board that must return null, and a uniqueness counter. It was written
test-first, so it went through a real red → green → review cycle.
The read/solve/place/verify layer is browser-coupled, so it isn’t covered by the unit tests. It was confirmed on the live puzzle: it read today’s #351 board, recovered the regions from the walls, solved it, tapped the digits in, and LinkedIn showed its win screen.
Full script
The readable, commented version — same result as the golfed paste above, plus a
uniqueness check, self-verification, and a console.table of the solution. This
is what’s served here:
/*
* LinkedIn Mini Sudoku solver — paste into the browser console on
* https://www.linkedin.com/games/mini-sudoku/ and it solves the current puzzle.
*
* It reads the 6×6 board from the DOM — each cell's digit, which cells are
* locked, and the region each cell belongs to (recovered by flood-filling the
* "wall" borders, so it handles both rectangular and jigsaw days) — solves it
* with a backtracking search, then fills the empty cells by tapping each cell
* and its digit on the number pad, and finally verifies every rule holds.
*/
(async () => {
const TAP_DELAY = 60; // ms between synthetic taps, so the app can re-render
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const cells = () =>
[...document.querySelectorAll('[data-cell-idx]')].sort((a, b) => a.dataset.cellIdx - b.dataset.cellIdx);
// A cell's digit lives in .sudoku-cell-content (empty string → 0).
const digitOf = (cell) => {
const t = (cell.querySelector('.sudoku-cell-content')?.textContent || '').trim();
return /^[1-9]$/.test(t) ? +t : 0;
};
// Read grid size, the given/locked digits, the number-pad buttons, and the
// region of every cell. Region boundaries are the "wall" classes; two
// orthogonal neighbours share a region iff there is no wall between them.
function readBoard() {
const els = cells();
if (!els.length) throw new Error('No Mini Sudoku board found on this page.');
const n = Math.round(Math.sqrt(els.length));
if (n * n !== els.length) throw new Error(`Board is not square (${els.length} cells).`);
const wall = (el, side) => el.classList.contains(`sudoku-cell-wall-${side}`);
const noWall = (i, j, aSide, bSide) => !wall(els[i], aSide) && !wall(els[j], bSide);
const parent = Array.from({ length: n * n }, (_, i) => i);
const find = (x) => (parent[x] === x ? x : (parent[x] = find(parent[x])));
const union = (a, b) => (parent[find(a)] = find(b));
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
const i = r * n + c;
if (c < n - 1 && noWall(i, i + 1, 'right', 'left')) union(i, i + 1);
if (r < n - 1 && noWall(i, i + n, 'bottom', 'top')) union(i, i + n);
}
}
const given = Array.from({ length: n }, () => new Array(n).fill(0));
const regions = Array.from({ length: n }, () => new Array(n));
const byRC = new Map();
const editable = new Set();
for (let r = 0; r < n; r++) {
for (let c = 0; c < n; c++) {
const i = r * n + c;
const el = els[i];
byRC.set(`${r},${c}`, el);
regions[r][c] = find(i);
if (el.classList.contains('sudoku-cell-prefilled')) given[r][c] = digitOf(el);
else editable.add(`${r},${c}`);
}
}
// Number pad: one button per digit 1..n.
const numBtns = new Map();
for (const b of document.querySelectorAll('.sudoku-input-button')) {
const t = b.textContent.trim();
if (/^[1-9]$/.test(t)) numBtns.set(+t, b);
}
return { n, given, regions, byRC, editable, numBtns };
}
// --- pure solver (identical to src/projects/sudoku/solver.mjs) ------------
// One depth-first search over cells in row-major order. `count` mode tallies
// solutions up to `cap`; solve mode captures and returns the first one.
function search(puzzle, count, cap = 2) {
const n = puzzle?.n | 0;
if (!n) return count ? 0 : null;
const given = puzzle.given || [];
const regions = puzzle.regions || [];
// A digit d is placeable at (r,c) iff it is unused in that row, column and
// region. Track the used digits per row / column / region as Sets.
const rowU = Array.from({ length: n }, () => new Set());
const colU = Array.from({ length: n }, () => new Set());
const regU = new Map();
for (let r = 0; r < n; r++)
for (let c = 0; c < n; c++) if (!regU.has(regions[r][c])) regU.set(regions[r][c], new Set());
const g = Array.from({ length: n }, () => new Array(n).fill(0));
const canPut = (r, c, d) => !rowU[r].has(d) && !colU[c].has(d) && !regU.get(regions[r][c]).has(d);
const put = (r, c, d) => {
g[r][c] = d;
rowU[r].add(d);
colU[c].add(d);
regU.get(regions[r][c]).add(d);
};
const unput = (r, c, d) => {
g[r][c] = 0;
rowU[r].delete(d);
colU[c].delete(d);
regU.get(regions[r][c]).delete(d);
};
let solution = null;
let total = 0;
function dfs(idx) {
if (idx === n * n) {
total++;
if (!count && !solution) solution = g.map((row) => row.slice());
return;
}
const r = (idx / n) | 0;
const c = idx % n;
const gv = given[r]?.[c];
const fixed = typeof gv === 'number' && gv >= 1 && gv <= n;
const candidates = fixed ? [gv] : Array.from({ length: n }, (_, i) => i + 1);
for (const d of candidates) {
if (!canPut(r, c, d)) continue;
put(r, c, d);
dfs(idx + 1);
unput(r, c, d);
if (count ? total >= cap : total >= 1) return;
}
}
dfs(0);
return count ? total : solution;
}
const solveSudoku = (p) => search(p, false);
const countSudokuSolutions = (p, cap = 2) => search(p, true, cap);
// Fire the full pointer sequence because element.click() alone is ignored.
function tap(el) {
const r = el.getBoundingClientRect();
const o = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
button: 0,
clientX: r.left + r.width / 2,
clientY: r.top + r.height / 2,
pointerId: 1,
pointerType: 'mouse',
isPrimary: true,
};
el.dispatchEvent(new PointerEvent('pointerdown', o));
el.dispatchEvent(new MouseEvent('mousedown', o));
el.dispatchEvent(new PointerEvent('pointerup', o));
el.dispatchEvent(new MouseEvent('mouseup', o));
el.dispatchEvent(new MouseEvent('click', o));
}
// Select the cell, then tap its digit on the number pad. Retry until it lands.
async function setCell(cell, btn, target) {
for (let i = 0; i < 3 && digitOf(cell) !== target; i++) {
tap(cell);
await sleep(TAP_DELAY);
tap(btn);
await sleep(TAP_DELAY);
}
return digitOf(cell) === target;
}
// Summarise what readBoard actually saw, so a failure blames the *read*,
// not the puzzle. A real 6×6 is n regions of n cells each; anything else
// means the borders were misread — most often because the board hadn't
// finished rendering, so no walls were found and every cell merged into one.
function diagnose(board) {
const { n, regions, given } = board;
const sizes = {};
for (const row of regions) for (const id of row) sizes[id] = (sizes[id] || 0) + 1;
const sizeList = Object.values(sizes).sort((a, b) => a - b);
const walls = cells().reduce(
(a, el) => a + [...el.classList].filter((x) => x.startsWith('sudoku-cell-wall-')).length,
0,
);
const givens = given.flat().filter((v) => v >= 1 && v <= n).length;
const malformed = sizeList.length !== n || sizeList.some((s) => s !== n);
return { regions: sizeList.length, sizeList, givens, walls, malformed };
}
// --- run ------------------------------------------------------------------
const board = readBoard();
const sol = solveSudoku(board);
if (!sol) {
const d = diagnose(board);
const detail = `read ${d.regions} region(s) of sizes [${d.sizeList.join(', ')}], ${d.givens} givens, ${d.walls} wall borders`;
console.error(
`%c🔢 Mini Sudoku: ${
d.malformed
? `couldn't read the board cleanly — ${detail}.\n${d.walls === 0 ? 'No region borders were found: the grid was probably still rendering. Wait a second and run it again.' : 'A region border was misread, so the board is over-constrained.'}`
: `the board read cleanly (${detail}) but has no solution — unexpected for a real puzzle; please report it.`
}`,
'color:#e11;font-weight:bold',
);
return;
}
const count = countSudokuSolutions(board, 2);
if (count !== 1) {
console.warn(`🔢 Mini Sudoku: the board as read has ${count >= 2 ? '≥2' : count} solutions — a wall may have been misread. Placing one valid solution anyway.`);
}
// Fill every editable cell to its solved digit (locked givens are skipped).
for (let r = 0; r < board.n; r++) {
for (let c = 0; c < board.n; c++) {
if (!board.editable.has(`${r},${c}`)) continue;
const btn = board.numBtns.get(sol[r][c]);
if (btn) await setCell(board.byRC.get(`${r},${c}`), btn, sol[r][c]);
}
}
// Verify what actually landed on the board matches the solved grid.
let ok = true;
for (let r = 0; r < board.n && ok; r++)
for (let c = 0; c < board.n && ok; c++) if (digitOf(board.byRC.get(`${r},${c}`)) !== sol[r][c]) ok = false;
console.log(
`%c🔢 Mini Sudoku ${board.n}×${board.n} ${ok ? 'solved ✅' : 'placement mismatch ⚠️'}`,
`color:${ok ? '#16a34a' : '#d97706'};font-weight:bold;font-size:14px`,
);
console.table(sol.map((row) => row.join(' ')));
})();