// ==UserScript==
// @name Single Trees Solver
// @namespace Violentmonkey Scripts
// @match https://www.playqueensgame.com/*
// @grant none
// @version 1.0
// @author Zach Kosove
// @license MIT
// @description Solver for Single Trees puzzle
// ==/UserScript==
(function () {
async function main() {
console.clear();
// --- Initialize ---
function initBoards() {
const cells = document.querySelectorAll('.grid .aspect-square');
const N = Math.sqrt(cells.length);
if (!Number.isInteger(N)) throw new Error(`Board is not square: ${cells.length} cells`);
// allocate flat buffers
const boardBuf = new Array(N * N).fill(null);
const colorBuf = new Int16Array(N * N).fill(-1);
// wrap them into N row views so you can still use [r][c]
const board = Array.from({ length: N }, (_, r) => boardBuf.slice(r * N, (r + 1) * N));
const colors = Array.from({ length: N }, (_, r) => colorBuf.subarray(r * N, (r + 1) * N));
const map = Object.create(null);
let count = 0;
cells.forEach(cell => {
colors[+cell.dataset.row][+cell.dataset.col] = map[cell.style.backgroundColor] ??= count++;
});
console.table(colors.map(row => [...row]));
return { N, colors, board };
}
const { N, colors, board } = initBoards();
// --- Constraints ---
function canPlaceDash(r, c, board) {
return board[r][c] === null;
}
function canPlaceT(r, c, board) {
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < N && nc >= 0 && nc < N && board[nr][nc] === 'T') return false;
}
}
return true;
}
// --- Place T / Dash ---
function applyBatteryRule(next) {
const out = next.map(r => r.slice());
const bitAt = Uint32Array.from({ length: N }, (_, i) => 1 << i);
const posMap = new Int16Array(N);
const seen = new Uint32Array(N);
let epoch = 1;
const ctz = x => 31 - Math.clz32(x & -x);
const popc = x => { let c = 0; while (x) { x &= x - 1; c++; } return c; };
let changed;
do {
changed = false;
for (let pass = 0; pass < 2; pass++) {
const spans = new Int32Array(N);
const colorList = [];
// build spans
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
if (out[r][c] !== null) continue;
const idx = pass === 0 ? r : c;
spans[colors[r][c]] |= bitAt[idx];
}
}
// collect active colors
for (let color = 0; color < N; color++) {
if (spans[color] && seen[color] !== epoch) {
posMap[color] = colorList.length;
seen[color] = epoch;
colorList.push(color);
}
}
epoch++;
const total = colorList.length;
if (total < 2) continue;
// iterate subsets of size 2..MAX_K
let mask = total - 3 >> 31;
let MAX_K = (3 & ~mask) | (total & mask);
for (let k = 2; k <= MAX_K; k++) {
let subset = (1 << k) - 1;
const limit = 1 << total;
while (subset < limit) {
// union mask
let mask = 0;
for (let i = 0; i < total; i++) {
if (subset & (1 << i)) mask |= spans[colorList[i]];
}
if (popc(mask) === k) {
// block outside subset
let bits = mask;
while (bits) {
const idx = ctz(bits);
bits &= bits - 1;
for (let j = 0; j < N; j++) {
const r = pass === 0 ? idx : j;
const c = pass === 0 ? j : idx;
const clr = colors[r][c];
const pos = posMap[clr];
if (pos >= 0 && (subset & (1 << pos))) continue;
if (out[r][c] === null) {
out[r][c] = "-";
changed = true;
}
}
}
}
// Gosper’s hack
const c = subset & -subset;
const r = subset + c;
subset = (((r ^ subset) >>> 2) / c) | r;
}
}
}
} while (changed);
return out;
}
function placeDash(r, c, board) {
if (!canPlaceDash(r, c, board)) return false;
const next = board.map(row => row.slice());
next[r][c] = '-';
return next;
}
function placeT(r, c, board) {
if (!canPlaceT(r, c, board)) return placeDash(r, c, board);
var next = board.map(row => row.slice());
next[r][c] = 'T';
// 1. block all other cells in the same row and column
for (let i = 0; i < N; i++) {
if (next[r][i] === null) next[r][i] = '-';
if (next[i][c] === null) next[i][c] = '-';
}
// 2. block diagonals (no-touch rule)
if (r > 0 && c > 0 && next[r - 1][c - 1] === null) next[r - 1][c - 1] = '-';
if (r > 0 && c < N - 1 && next[r - 1][c + 1] === null) next[r - 1][c + 1] = '-';
if (r < N - 1 && c > 0 && next[r + 1][c - 1] === null) next[r + 1][c - 1] = '-';
if (r < N - 1 && c < N - 1 && next[r + 1][c + 1] === null) next[r + 1][c + 1] = '-';
// 3. block other cells in the same region/color
const color = colors[r][c];
for (let i = 0; i < N; i++) {
for (let j = 0; j < N; j++) {
if (next[i][j] === null && colors[i][j] === color) next[i][j] = '-';
}
}
// 4. apply battery rule
next = applyBatteryRule(next);
// 4. color validation + forced placement
const rowNulls = new Int16Array(N);
const colNulls = new Int16Array(N);
const colorNulls = new Int16Array(N);
const colorHasT = new Uint8Array(N);
const lastRow = new Int16Array(N * 2);
const lastCol = new Int16Array(N * 2);
const lastColor = new Int16Array(N * 2);
// pass 1: count nulls and detect T’s
for (let i = 0; i < N; i++) {
const rowN = next[i], rowC = colors[i];
for (let j = 0; j < N; j++) {
const v = rowN[j], clr = rowC[j];
if (v === 'T') {
colorHasT[clr] = 1;
continue;
}
if (v !== null) continue;
// row
rowNulls[i]++;
lastRow[i<<1] = i;
lastRow[(i<<1)+1] = j;
// col
colNulls[j]++;
lastCol[j<<1] = i;
lastCol[(j<<1)+1] = j;
// color
colorNulls[clr]++;
lastColor[clr<<1] = i;
lastColor[(clr<<1)+1] = j;
}
}
// pass 2: forced placements
for (let i = 0; i < N; i++) {
if (colorNulls[i] === 0 && colorHasT[i] === 0) return false;
if (colorNulls[i] === 1) return placeT(lastColor[i <<1], lastColor[(i <<1 ) + 1], next);
if (rowNulls[i] === 1) return placeT(lastRow[i <<1], lastRow[(i << 1) + 1], next);
if (colNulls[i] === 1) return placeT(lastCol[i <<1], lastCol[(i << 1) + 1], next);
}
return next;
}
// --- Validation / Analysis ---
let evaluations = 0;
function analyze(board) {
evaluations++;
const rowT = new Int8Array(N), rowE = new Int8Array(N),
colT = new Int8Array(N), colE = new Int8Array(N),
tByColor = new Int8Array(N), eByColor = new Int8Array(N);
// count
let empties = false;
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const v = board[r][c], color = colors[r][c];
if (v === "T") (rowT[r]++, colT[c]++, tByColor[color]++);
else if (v == null) (rowE[r]++, colE[c]++, eByColor[color]++, empties = true);
}
}
// solved?
if (!empties) {
for (let i = 0; i < N; i++) {
if (rowT[i] !== 1 || colT[i] !== 1 || tByColor[i] !== 1) break;
if (i === N - 1) return true;
}
}
// candidate
let best = null, bScore = 1e9;
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
if (board[r][c] === null && canPlaceT(r, c, board)) {
const score = eByColor[colors[r][c]];
if (score < bScore) best = { r, c }, bScore = score;
}
}
}
return best;
}
// --- Search ---
function search(board) {
console.table(board);
const pick = analyze(board);
if (pick === null) return null;
if (pick === true) return board;
let res = null;
let next = placeT(pick.r, pick.c, board);
if (next && (res = search(next))) return res;
next = placeDash(pick.r, pick.c, board);
if (next && (res = search(next))) return res;
return null;
}
function solve(board) {
board = applyBatteryRule(board);
const solution = search(board);
if (!solution) throw new Error("no solution found");
console.table(solution);
console.log("Evaluations:", evaluations);
return solution;
}
let solution = solve(board);
// --- Input Solution ---
async function clickTCells(boardOrPromise) {
const b = await boardOrPromise;
const els = document.querySelectorAll(".grid [data-row][data-col]");
let i = 0;
for (let r = 0; r < N; r++) {
const row = b[r];
for (let c = 0; c < N; c++, i++) {
if (row[c] === "T") els[i].click();
}
}
}
clickTCells(solution);
}
document.addEventListener("keydown", e => e.key === "`" && main());
})();