// ==UserScript== // @name LinkedIn Games Solver (Queens + Sudoku + Tango + Patches) // @namespace https://docs.scriptcat.org/ // @version 4.0.0 // @description Automatically solves LinkedIn Queens, Mini-Sudoku, Tango, and Patches games // @author You // @match https://www.linkedin.com/games/queens/ // @match https://www.linkedin.com/games/mini-sudoku/ // @match https://www.linkedin.com/games/tango/ // @match https://www.linkedin.com/games/patches/ // @icon https://www.google.com/s2/favicons?sz=64&domain=www.linkedin.com // @grant none // @noframes // ==/UserScript== (function () { 'use strict'; const LOG = '[LinkedIn Solver]'; const IS_QUEENS = location.href.includes('/games/queens'); const IS_SUDOKU = location.href.includes('/games/mini-sudoku'); const IS_TANGO = location.href.includes('/games/tango'); const IS_PATCHES = location.href.includes('/games/patches'); function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Simulate realistic pointer+mouse click chain (bypasses React synthetic events) function simulateClick(el) { const rect = el.getBoundingClientRect(); const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; const base = { bubbles: true, cancelable: true, clientX: x, clientY: y }; el.dispatchEvent(new PointerEvent('pointerdown', { ...base, pointerId: 1 })); el.dispatchEvent(new MouseEvent('mousedown', base)); el.dispatchEvent(new PointerEvent('pointerup', { ...base, pointerId: 1 })); el.dispatchEvent(new MouseEvent('mouseup', base)); el.dispatchEvent(new MouseEvent('click', base)); } // ═══════════════════════════════════════════════════════════════════════ // QUEENS SOLVER // ═══════════════════════════════════════════════════════════════════════ function parseQueensBoard(cells, n) { const board = Array.from({ length: n }, () => new Array(n).fill(null)); const colorSet = new Set(); for (const cell of cells) { const label = cell.getAttribute('aria-label') || ''; const m = label.match(/of\s+color\s+([^,]+),\s*row\s+(\d+),\s*column\s+(\d+)/i); if (m) { const color = m[1].trim(); const row = parseInt(m[2], 10) - 1; const col = parseInt(m[3], 10) - 1; if (row >= 0 && row < n && col >= 0 && col < n) { board[row][col] = color; colorSet.add(color); } } } return { board, colorSet }; } function solveQueens(board, n) { const solution = new Array(n).fill(-1); const usedCols = new Set(); const usedColors = new Set(); function canPlace(row, col) { if (usedCols.has(col)) return false; if (!board[row][col] || usedColors.has(board[row][col])) return false; for (let r = 0; r < row; r++) { if (Math.abs(r - row) <= 1 && Math.abs(solution[r] - col) <= 1) return false; } return true; } function backtrack(row) { if (row === n) return true; for (let col = 0; col < n; col++) { if (canPlace(row, col)) { solution[row] = col; usedCols.add(col); usedColors.add(board[row][col]); if (backtrack(row + 1)) return true; solution[row] = -1; usedCols.delete(col); usedColors.delete(board[row][col]); } } return false; } return backtrack(0) ? solution : null; } function isQueen(cell) { return (cell.getAttribute('aria-label') || '').toLowerCase().startsWith('queen'); } async function placeQueen(grid, idx) { let cell = grid.querySelector(`[data-cell-idx="${idx}"]`); if (!cell) { console.error(`${LOG} Cell ${idx} not found`); return; } simulateClick(cell); await delay(150); cell = grid.querySelector(`[data-cell-idx="${idx}"]`); if (!cell) return; if (isQueen(cell)) return; simulateClick(cell); await delay(200); cell = grid.querySelector(`[data-cell-idx="${idx}"]`); if (cell && !isQueen(cell)) { console.warn(`${LOG} Cell ${idx} not queen after 2 clicks, retrying...`); simulateClick(cell); await delay(150); cell = grid.querySelector(`[data-cell-idx="${idx}"]`); if (cell && !isQueen(cell)) { simulateClick(cell); await delay(150); } } } async function runQueens() { const grid = document.querySelector('[data-testid="interactive-grid"]'); if (!grid) { console.warn(`${LOG} Queens board not found, retrying...`); return false; } const cells = Array.from(grid.querySelectorAll('[data-cell-idx]')); if (cells.length === 0) return false; const n = Math.round(Math.sqrt(cells.length)); if (n * n !== cells.length) { console.error(`${LOG} Unexpected cell count: ${cells.length}`); return true; } console.log(`${LOG} [Queens] Detected ${n}x${n} grid`); const { board, colorSet } = parseQueensBoard(cells, n); console.log(`${LOG} [Queens] Colors (${colorSet.size}):`, [...colorSet].join(', ')); const solution = solveQueens(board, n); if (!solution) { console.error(`${LOG} [Queens] No solution, board may not be loaded.`); return false; } console.log(`${LOG} [Queens] Solution:`, solution.map((col, row) => `R${row + 1}C${col + 1}(${board[row][col]})`).join(' | ')); for (let row = 0; row < n; row++) { const col = solution[row]; console.log(`${LOG} [Queens] Placing queen R${row + 1}C${col + 1} (${board[row][col]})`); await placeQueen(grid, row * n + col); } console.log(`${LOG} [Queens] Done!`); return true; } // ═══════════════════════════════════════════════════════════════════════ // SUDOKU SOLVER // ═══════════════════════════════════════════════════════════════════════ function parseSudokuBoard(cells, n) { const board = Array.from({ length: n }, () => new Array(n).fill(0)); for (const cell of cells) { const idx = parseInt(cell.getAttribute('data-cell-idx'), 10); const row = Math.floor(idx / n); const col = idx % n; // Try multiple possible content selectors const content = cell.querySelector('.sudoku-cell-content') || cell.querySelector('[class*="cell-content"]') || cell.querySelector('[class*="number"]') || cell.querySelector('span') || cell; if (content) { const val = parseInt(content.textContent.trim(), 10); if (!isNaN(val) && val > 0) board[row][col] = val; } } return board; } function solveSudoku(board, n, boxRows, boxCols) { for (let row = 0; row < n; row++) { for (let col = 0; col < n; col++) { if (board[row][col] === 0) { for (let num = 1; num <= n; num++) { // Check row if (board[row].includes(num)) continue; // Check col let colOk = true; for (let r = 0; r < n; r++) { if (board[r][col] === num) { colOk = false; break; } } if (!colOk) continue; // Check box const sr = Math.floor(row / boxRows) * boxRows; const sc = Math.floor(col / boxCols) * boxCols; let boxOk = true; for (let r = sr; r < sr + boxRows && boxOk; r++) for (let c = sc; c < sc + boxCols && boxOk; c++) if (board[r][c] === num) boxOk = false; if (!boxOk) continue; board[row][col] = num; if (solveSudoku(board, n, boxRows, boxCols)) return true; board[row][col] = 0; } return false; } } } return true; } async function runSudoku() { // Try multiple selectors for the grid container let grid = document.querySelector('[data-sudoku-grid="true"]') || document.querySelector('[data-testid="sudoku-grid"]') || document.querySelector('[data-testid*="sudoku"]') || document.querySelector('[class*="sudoku-grid"]'); // Fallback: find any container holding data-cell-idx children if (!grid) { const anyCells = document.querySelectorAll('[data-cell-idx]'); if (anyCells.length > 0) { grid = anyCells[0].parentElement; } } if (!grid) { console.warn(`${LOG} Sudoku board not found, retrying...`); return false; } const cells = Array.from(grid.querySelectorAll('[data-cell-idx]')); if (cells.length === 0) { console.warn(`${LOG} [Sudoku] Grid found but no cells inside. grid:`, grid); return false; } // Determine grid size: prefer CSS vars, fall back to sqrt(cellCount) const style = grid.getAttribute('style') || ''; const rowsMatch = style.match(/--rows:\s*(\d+)/); let n; if (rowsMatch) { n = parseInt(rowsMatch[1], 10); } else { n = Math.round(Math.sqrt(cells.length)); if (n * n !== cells.length) { console.warn(`${LOG} [Sudoku] Cannot determine grid size from ${cells.length} cells`); return false; } console.log(`${LOG} [Sudoku] No --rows CSS var found, inferred n=${n} from cell count`); } if (cells.length !== n * n) { console.warn(`${LOG} [Sudoku] Expected ${n * n} cells, got ${cells.length}`); return false; } // 6x6 box = 2 rows x 3 cols; 9x9 box = 3x3 const boxRows = n === 6 ? 2 : Math.round(Math.sqrt(n)); const boxCols = n / boxRows; console.log(`${LOG} [Sudoku] Detected ${n}x${n} grid (box: ${boxRows}x${boxCols})`); const board = parseSudokuBoard(cells, n); const original = board.map(r => [...r]); if (!solveSudoku(board, n, boxRows, boxCols)) { console.error(`${LOG} [Sudoku] No solution found.`); return true; } console.log(`${LOG} [Sudoku] Solution found, filling board...`); for (let row = 0; row < n; row++) { for (let col = 0; col < n; col++) { if (original[row][col] === 0) { const idx = row * n + col; const cell = grid.querySelector(`[data-cell-idx="${idx}"]`); if (!cell) continue; const num = board[row][col]; console.log(`${LOG} [Sudoku] R${row + 1}C${col + 1} = ${num}`); simulateClick(cell); await delay(80); const btn = document.querySelector(`button[data-number="${num}"]`); if (btn) { simulateClick(btn); await delay(80); } else console.error(`${LOG} [Sudoku] Button ${num} not found`); } } } console.log(`${LOG} [Sudoku] Done!`); return true; } // ═══════════════════════════════════════════════════════════════════════ // TANGO SOLVER // ═══════════════════════════════════════════════════════════════════════ function parseTangoBoard(cells) { const board = {}; const locked = new Set(); for (const cell of cells) { const idx = parseInt(cell.getAttribute('data-cell-idx'), 10); const svg = cell.querySelector('svg[aria-label]'); if (!svg) continue; const label = (svg.getAttribute('aria-label') || '').toLowerCase(); if (label === 'sun') { board[idx] = 0; locked.add(idx); } else if (label === 'moon') { board[idx] = 1; locked.add(idx); } // else: empty } return { board, locked }; } // Parse = (same) and × (opposite) constraints from cell borders. // LinkedIn encodes them as child elements with data-testid="cell-border-*" // or as specific aria attributes. Falls back to empty list if not found. function parseTangoConstraints(grid, n) { const constraints = []; // Try data-testid="constraint-*" or similar patterns const markers = grid.querySelectorAll('[data-constraint-type], [data-testid*="border"], [data-testid*="constraint"]'); for (const m of markers) { const a = parseInt(m.getAttribute('data-cell-a') ?? m.getAttribute('data-from'), 10); const b = parseInt(m.getAttribute('data-cell-b') ?? m.getAttribute('data-to'), 10); const type = (m.getAttribute('data-constraint-type') || m.getAttribute('data-testid') || '').includes('equal') ? 'same' : 'diff'; if (!isNaN(a) && !isNaN(b)) constraints.push({ a, b, type }); } if (constraints.length > 0) console.log(`${LOG} [Tango] Found ${constraints.length} constraints`); return constraints; } function solveTango(initialBoard, n, constraints) { const half = n / 2; const board = new Array(n * n).fill(-1); // Copy pre-filled values for (const [k, v] of Object.entries(initialBoard)) board[parseInt(k)] = v; function isValid(idx, val) { const row = Math.floor(idx / n); const col = idx % n; // Constraint checks for (const c of constraints) { let other = -1; if (c.a === idx) other = c.b; else if (c.b === idx) other = c.a; if (other < 0) continue; const ov = board[other]; if (ov < 0) continue; if (c.type === 'same' && ov !== val) return false; if (c.type === 'diff' && ov === val) return false; } // No 3 consecutive in row const r = row * n; if (col >= 2 && board[r + col - 1] === val && board[r + col - 2] === val) return false; if (col + 2 < n && board[r + col + 1] === val && board[r + col + 2] === val) return false; if (col >= 1 && col + 1 < n && board[r + col - 1] === val && board[r + col + 1] === val) return false; // No 3 consecutive in col if (row >= 2 && board[(row - 1) * n + col] === val && board[(row - 2) * n + col] === val) return false; if (row + 2 < n && board[(row + 1) * n + col] === val && board[(row + 2) * n + col] === val) return false; if (row >= 1 && row + 1 < n && board[(row - 1) * n + col] === val && board[(row + 1) * n + col] === val) return false; // Row balance: count existing (excluding current slot which is -1) let rowCount = 0; for (let c = 0; c < n; c++) if (board[r + c] === val) rowCount++; if (rowCount >= half) return false; // Col balance let colCount = 0; for (let r2 = 0; r2 < n; r2++) if (board[r2 * n + col] === val) colCount++; if (colCount >= half) return false; return true; } function backtrack(idx) { if (idx === n * n) return true; if (board[idx] !== -1) return backtrack(idx + 1); for (const val of [0, 1]) { if (isValid(idx, val)) { board[idx] = val; if (backtrack(idx + 1)) return true; board[idx] = -1; } } return false; } return backtrack(0) ? board : null; } async function runTango() { const container = document.querySelector('[data-testid="tango-game-container"]'); if (!container) { console.warn(`${LOG} Tango container not found, retrying...`); return false; } const grid = container.querySelector('[data-testid="interactive-grid"]'); if (!grid) { console.warn(`${LOG} [Tango] Interactive grid not found`); return false; } // Grid size from inline CSS variable (e.g. --a1248a3f: 4) const styleStr = grid.getAttribute('style') || ''; const sizeMatch = styleStr.match(/--[\w]+:\s*(\d+)/); const cells = Array.from(grid.querySelectorAll('[data-cell-idx]')); if (cells.length === 0) { console.warn(`${LOG} [Tango] No cells found`); return false; } const n = sizeMatch ? parseInt(sizeMatch[1], 10) : Math.round(Math.sqrt(cells.length)); if (cells.length !== n * n) { console.warn(`${LOG} [Tango] Expected ${n * n} cells, got ${cells.length}`); return false; } if (n % 2 !== 0) { console.error(`${LOG} [Tango] Grid size ${n} is odd, cannot solve`); return true; } console.log(`${LOG} [Tango] Detected ${n}x${n} grid`); const { board: initialBoard, locked } = parseTangoBoard(cells); const prefilled = Object.keys(initialBoard).length; if (prefilled === 0) { console.warn(`${LOG} [Tango] Board appears empty, retrying...`); return false; } console.log(`${LOG} [Tango] ${prefilled} pre-filled cells`); const constraints = parseTangoConstraints(grid, n); const solution = solveTango(initialBoard, n, constraints); if (!solution) { console.error(`${LOG} [Tango] No solution found`); return true; } const labels = ['Sun', 'Moon']; console.log(`${LOG} [Tango] Solution:`, Array.from({ length: n }, (_, r) => solution.slice(r * n, r * n + n).map(v => v === 0 ? '☀' : '🌙').join('') ).join(' | ')); for (let idx = 0; idx < n * n; idx++) { if (locked.has(idx)) continue; const cell = grid.querySelector(`[data-cell-idx="${idx}"]`); if (!cell) continue; const val = solution[idx]; const row = Math.floor(idx / n) + 1; const col = (idx % n) + 1; console.log(`${LOG} [Tango] R${row}C${col} = ${labels[val]}`); // Click cycle: Empty→Sun(1 click), Empty→Moon(2 clicks) for (let i = 0; i <= val; i++) { simulateClick(cell); await delay(130); } } console.log(`${LOG} [Tango] Done!`); return true; } // ═══════════════════════════════════════════════════════════════════════ // PATCHES SOLVER // ═══════════════════════════════════════════════════════════════════════ function parsePatchesClues(cells, n) { const clues = []; for (const cell of cells) { const idx = parseInt(cell.getAttribute('data-cell-idx'), 10); const clueEl = cell.querySelector('[data-testid^="patches-clue-number-"]'); if (!clueEl) continue; const count = parseInt(clueEl.textContent.trim(), 10); const shapeEl = cell.querySelector('[data-shape]'); const shape = shapeEl ? shapeEl.getAttribute('data-shape') : ''; clues.push({ idx, count, shape }); } return clues; } // Generate all valid rectangle placements for a clue // HORIZONTAL: w > h | VERTICAL: h > w | SQUARE: w == h function getPatchPlacements(anchorIdx, count, shape, n) { const ar = Math.floor(anchorIdx / n); const ac = anchorIdx % n; const results = []; for (let h = 1; h <= count; h++) { if (count % h !== 0) continue; const w = count / h; if (w > n || h > n) continue; if (shape.includes('HORIZONTAL') && w <= h) continue; if (shape.includes('VERTICAL') && h <= w) continue; if (shape.includes('SQUARE') && w !== h) continue; // All top-left positions where anchor is inside the rect for (let r0 = Math.max(0, ar - h + 1); r0 <= Math.min(n - h, ar); r0++) { for (let c0 = Math.max(0, ac - w + 1); c0 <= Math.min(n - w, ac); c0++) { const patch = []; for (let dr = 0; dr < h; dr++) for (let dc = 0; dc < w; dc++) patch.push((r0 + dr) * n + (c0 + dc)); results.push(patch); } } } return results; } function solvePatches(clues, n) { const assignment = new Array(n * n).fill(-1); const placements = clues.map(c => getPatchPlacements(c.idx, c.count, c.shape, n)); function backtrack(ci) { if (ci === clues.length) return assignment.every(v => v >= 0); for (const patch of placements[ci]) { if (patch.every(i => assignment[i] === -1)) { patch.forEach(i => assignment[i] = ci); if (backtrack(ci + 1)) return true; patch.forEach(i => assignment[i] = -1); } } return false; } return backtrack(0) ? { assignment, placements } : null; } async function runPatches() { const container = document.querySelector('[data-testid="patches-game-container"]'); if (!container) { console.warn(`${LOG} Patches container not found, retrying...`); return false; } const grid = container.querySelector('[data-testid="interactive-grid"]'); if (!grid) { console.warn(`${LOG} [Patches] Interactive grid not found`); return false; } const cells = Array.from(grid.querySelectorAll('[data-cell-idx]')); if (cells.length === 0) { console.warn(`${LOG} [Patches] No cells found`); return false; } const styleStr = grid.getAttribute('style') || ''; const sizeMatch = styleStr.match(/--[\w]+:\s*(\d+)/); const n = sizeMatch ? parseInt(sizeMatch[1], 10) : Math.round(Math.sqrt(cells.length)); if (cells.length !== n * n) { console.warn(`${LOG} [Patches] Expected ${n}*${n} cells, got ${cells.length}`); return false; } const clues = parsePatchesClues(cells, n); if (clues.length === 0) { console.warn(`${LOG} [Patches] No clues found, retrying...`); return false; } console.log(`${LOG} [Patches] ${n}x${n} grid, ${clues.length} clues:`, clues.map(c => `idx=${c.idx} n=${c.count} ${c.shape.replace('PatchesShapeConstraint_', '')}`).join(' | ')); const result = solvePatches(clues, n); if (!result) { console.error(`${LOG} [Patches] No solution found`); return true; } // Log solution grid console.log(`${LOG} [Patches] Solution:`, Array.from({ length: n }, (_, r) => result.assignment.slice(r * n, r * n + n).join('') ).join(' | ')); for (let ci = 0; ci < clues.length; ci++) { const clue = clues[ci]; // Click the anchor cell first to activate this patch const anchor = grid.querySelector(`[data-cell-idx="${clue.idx}"]`); if (anchor) { simulateClick(anchor); await delay(150); } // Click all non-anchor cells belonging to this patch for (let idx = 0; idx < n * n; idx++) { if (result.assignment[idx] === ci && idx !== clue.idx) { const cell = grid.querySelector(`[data-cell-idx="${idx}"]`); if (cell) { simulateClick(cell); await delay(100); } } } } console.log(`${LOG} [Patches] Done!`); return true; } // ═══════════════════════════════════════════════════════════════════════ // ENTRY POINT // ═══════════════════════════════════════════════════════════════════════ async function waitAndRun() { const runner = IS_QUEENS ? runQueens : IS_SUDOKU ? runSudoku : IS_TANGO ? runTango : IS_PATCHES ? runPatches : null; if (!runner) return; let attempts = 0; while (attempts < 30) { attempts++; const done = await runner(); if (done) break; console.log(`${LOG} Waiting for game to load... (attempt ${attempts})`); await delay(1500); } } waitAndRun(); })();