Extra grids that let you try out logic permutations.
// ==UserScript==
// @name Scratchpads By Sam
// @namespace Userscripts by cethemaco
// @author cethemaco
// @description Extra grids that let you try out logic permutations.
// @supportURL https://github.com/cethemaco/scratchpads-by-sam
// @match https://cluesbysam.com/
// @match https://cluesbysam.com/s/share/*
// @match https://cluesbysam.com/s/archive/*
// @match https://cluesbysam.com/archive/*
// @match https://cluesbysam.com/s/play/*
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADlElEQVQ4T0WTfUzUdRzHX7/jOLin33EcF8hDdjwkXlC6nibEEFnTCArpSVummzNrJcOR08rSZSxXmWaFyvnAzaUtpYZUjBmYlLiEBSaYCgfTQ8XT4zi6h3i6X7+7tvr+892++35fn8/7/Xl/hXRdgoS8BEFgcnKS6OhoHs3PQ6vVEvD7iVIquXihlxvDw2jkM0mKXP9vCWFA+PH01BTmxEQ+3vc5oimOQCCARiND5N1sSuDLj3bS8NXRCCQUCv0PyNCbJWW0ktFRN7X2Q5jTzNTbahlx3WZC7kiv0zIn+z4qqzbyUkk5g/0DxKrVzMzMRCCRDu747jDPOh9703F2frINnUZNv2NIrh4kM8OC03mNVWveYHzEw6trVxMfGxeRGpYj5CbNlsqXP8+6DdU0/dCI/cBe0jMycQ47Cc2EMMXH4fP5iFHrOWg/QmfHWeq/2MeZ0+2oVCqElWXPSu/sqMF+qI6fWk8TDE4TE6vGPeoiKOsPQ8K+CVEKSh4vpnrTRvo6e6hc9Qp6US93kDhbeqtmK9aH5tNYv4PLvedQaY3c9k7IOkGvlhA1ArH4WVm9m8lpLe9WVuHod0RkCBatSfL5vTS0/UKOpplfW/bTOWCkolBA1P5N5yURiRnysz0Yi5pZWvIaPb93YDSYIkYK98YlSZ4xNx/WHuDlvCsMOtrpHbHwzCK5vGKQjgvzmA66eSBxCN/du1hWUcXwUD9KuXp4nEKWIVEa846yYu06tm5aSuh8AQZrEXjH5CGpQB0FQScj7gI8yZupWFgsj9wTCZpCofgX4JUBb76/nao1D9Jau5AlZXPp6g5y6fI4y59LwfFnH7eCT5GzzMbVwWt0neng023bZcOD4RyYZI8VfNPSSNTQLpobT5CVYcYyy8lfgRhilJOc6zbiVsyi9HUbYlwyWoOObw/aqNm8BSFTvEvyy5mvO2pnyeIF7P3sPYY9USy2jKP0d3NT9wJdfeeZjjbzZPlqbl4fkS0VyEwz8uITTyOkKHXSHOtcDssp/KPnLD+f+pGCwiLyCstwXR/AnJKJfc8WHC6BbOtj3Lp6g9yHc0iIlVhRWoFgQinttu3Hen8KTQ02LOkWRsf8PJJfSnJqFu2tx5gKuvBPSAQU95CaZGFR8QI+WL+BY0e+RkhVidLhE8fxefs4ebKN3JwspuSfqdElIMancOXib6QlmxkbD9DS1kNd3R6+/+4Ub1euRxQN/AM+GXfOzHiBSgAAAABJRU5ErkJggg==
// @grant GM_getValues
// @grant GM_setValue
// @version 1.0
// @license MIT
// @compatible firefox
// @compatible edge
// ==/UserScript==
(() => {
const WRAP_ID = "devtools-and-grid-wrap";
const STYLE_ID = "devtools-and-grid-style";
const SYNC_OBSERVER_CONFIG = { subtree: true, attributeFilter: ["class"], attributeOldValue: true };
let puzzleGrid = document.querySelector("#grid");
// preferences to remember
const preferences = GM_getValues({
synchronized: true,
pinned: true,
hidden: false,
});
// Remove prior runs
const oldWrap = document.getElementById(WRAP_ID);
if (oldWrap) oldWrap.remove();
const oldStyle = document.getElementById(STYLE_ID);
if (oldStyle) oldStyle.remove();
// Config
const rows = 5;
const cols = 4;
// Tile size (original 25x37.5 then 30% smaller)
const tileW = 25 * 0.7; // 17.5
const tileH = 37.5 * 0.7; // 26.25
const gap = 4;
const pad = 8;
const outerGap = 10;
const states = [
{ name: "clear", color: "transparent" },
{ name: "red", color: "rgba(255,0,0,0.55)" },
{ name: "green", color: "rgba(0,200,0,0.55)" },
{ name: "yellow", color: "rgba(255,255,0,0.55)", ignore: true},
];
// Styles
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
#${WRAP_ID} {
position: ${preferences.pinned ? "absolute" : "fixed"};
left: 8px;
top: 8px;
z-index: 2147483647;
display: flex;
flex-direction: column;
gap: ${outerGap}px;
user-select: none;
font: 12px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
#${WRAP_ID} .topbar {
display: flex;
align-items: center;
gap: 8px;
}
#${WRAP_ID} .row {
display: flex;
gap: ${outerGap}px;
align-items: flex-start;
}
#${WRAP_ID} .row.center {
justify-content: center;
}
#${WRAP_ID} .panel {
position: relative;
background: rgba(255,255,255,0.92);
padding: ${pad}px;
border: 1px solid rgba(0,0,0,0.25);
border-radius: 8px;
}
#${WRAP_ID} .panel.result {
background: rgba(173, 216, 230, 0.55);
border-color: rgba(0, 120, 200, 0.35);
}
#${WRAP_ID} .title {
font-weight: 800;
letter-spacing: 0.4px;
margin: 0 0 6px 0;
color: rgba(0,0,0,0.75);
text-align: center;
}
#${WRAP_ID} .grid {
display: grid;
grid-template-columns: repeat(${cols}, ${tileW}px);
grid-template-rows: repeat(${rows}, ${tileH}px);
gap: ${gap}px;
}
#${WRAP_ID} .tile {
width: ${tileW}px;
height: ${tileH}px;
box-sizing: border-box;
border: 2px solid rgba(0,0,0,0.65);
background: transparent;
cursor: pointer;
}
#${WRAP_ID} .tile:hover {
outline: 2px solid rgba(0,0,0,0.25);
outline-offset: 1px;
}
#${WRAP_ID} .panel.result .tile {
cursor: default;
pointer-events: none;
}
#${WRAP_ID} .tile.synced {
cursor: default;
pointer-events: none;
border: none;
}
#${WRAP_ID} .controls {
margin-top: 8px;
display: flex;
gap: 8px;
justify-content: center;
}
#${WRAP_ID} button {
all: unset;
cursor: pointer;
padding: 6px 10px;
border: 1px solid rgba(0,0,0,0.35);
border-radius: 6px;
background: rgba(255,255,255,0.85);
color: rgba(0,0,0,0.85);
font-weight: 700;
line-height: 1;
text-align: center;
min-width: 18px;
}
#${WRAP_ID} button:hover { background: rgba(240,240,240,0.95); }
#${WRAP_ID} button:active { transform: translateY(1px); }
#${WRAP_ID} button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
/* Close button positioned outside panel corner */
#${WRAP_ID} .close-btn {
position: absolute;
top: -10px;
right: -10px;
width: 20px;
height: 20px;
padding: 0;
min-width: 0;
border-radius: 999px;
display: grid;
place-items: center;
font-weight: 900;
line-height: 1;
background: white;
border: 1px solid rgba(0,0,0,0.5);
}
`;
document.head.appendChild(style);
function applyState(tile, idx) {
tile.dataset.stateIndex = idx;
tile.style.background = states[idx].color;
}
function getState(tile) {
return Number(tile.dataset.stateIndex || 0);
}
function tiles(grid, synced) {
return synced ? Array.from(grid.querySelectorAll(".tile.synced")) : Array.from(grid.querySelectorAll(".tile"));
}
function clearGrid(grid) {
tiles(grid).forEach(t => { if (!t.classList.contains("synced")) applyState(t, 0) });
}
function updateResult(inputs, resultGrid) {
const rTiles = tiles(resultGrid);
const inputTiles = inputs.map(g => tiles(g));
for (let i = 0; i < rTiles.length; i++) {
const first = getState(inputTiles[0][i]);
const same = inputTiles.every(arr => getState(arr[i]) === first);
const canBeIgnored = inputTiles.some(arr => states[getState(arr[i])].ignore);
const synced = inputTiles.some(arr => arr[i].classList.contains("synced"))
applyState(rTiles[i], (same && !canBeIgnored && !synced) ? first : 0);
}
}
function createGrid({ clickable, onChange }) {
const grid = document.createElement("div");
grid.className = "grid";
for (let i = 0; i < rows * cols; i++) {
const tile = document.createElement("div");
tile.className = "tile";
applyState(tile, 0);
if (clickable) {
tile.onChange = onChange
tile.addEventListener("click", e => {
e.stopPropagation();
const next = (getState(tile) + 1) % states.length;
applyState(tile, next);
tile.onChange();
});
}
grid.appendChild(tile);
}
return grid;
}
function createInputPanel(grid, onClear, withClose, onClose) {
const panel = document.createElement("div");
panel.className = "panel";
panel.appendChild(grid);
if (withClose) {
const close = document.createElement("button");
close.className = "close-btn";
close.textContent = "×";
close.addEventListener("click", e => {
e.stopPropagation();
onClose();
});
panel.appendChild(close);
}
const controls = document.createElement("div");
controls.className = "controls";
const clear = document.createElement("button");
clear.textContent = "Clear";
clear.addEventListener("click", () => {
clearGrid(grid);
onClear();
});
controls.appendChild(clear);
panel.appendChild(controls);
return panel;
}
function createResultPanel(grid) {
const panel = document.createElement("div");
panel.className = "panel result";
const title = document.createElement("div");
title.className = "title";
title.textContent = "COMMON";
panel.appendChild(title);
panel.appendChild(grid);
return panel;
}
const wrap = document.createElement("div");
wrap.id = WRAP_ID;
const topbar = document.createElement("div");
topbar.className = "topbar";
const addBtn = document.createElement("button");
addBtn.textContent = "+";
const pinBtn = document.createElement("button");
pinBtn.textContent = preferences.pinned ? "Un-📌" : "📌";
const hideBtn = document.createElement("button");
hideBtn.textContent = preferences.hidden ? "Show" : "Hide";
hideBtn.style.visibility = "visible";
wrap.style.visibility = preferences.hidden ? "hidden" : "visible";
const syncBtn = document.createElement("button");
syncBtn.textContent = preferences.synchronized ? "Desync" : "Sync";
topbar.appendChild(hideBtn);
topbar.appendChild(addBtn);
topbar.appendChild(pinBtn);
topbar.appendChild(syncBtn);
wrap.appendChild(topbar);
const topRow = document.createElement("div");
topRow.className = "row";
const bottomRow = document.createElement("div");
bottomRow.className = "row center";
wrap.appendChild(topRow);
wrap.appendChild(bottomRow);
const resultGrid = createGrid({ clickable: false });
const resultPanel = createResultPanel(resultGrid);
bottomRow.appendChild(resultPanel);
const inputs = [];
const panels = [];
let thirdIndex = -1;
function recompute() {
if (inputs.length) updateResult(inputs, resultGrid);
}
function addInput(closable=false) {
const grid = createGrid({ clickable: true, onChange: recompute });
const panel = createInputPanel(
grid,
recompute,
closable,
() => removeThird()
);
inputs.push(grid);
panels.push(panel);
topRow.appendChild(panel);
recompute();
}
function removeThird() {
if (thirdIndex === -1) return;
panels[thirdIndex].remove();
panels.splice(thirdIndex,1);
inputs.splice(thirdIndex,1);
thirdIndex = -1;
addBtn.disabled = false;
recompute();
}
addInput(false);
addInput(false);
function calculateGridPosition(coord) {
let digits = coord.split("");
return (parseInt(digits[1]) - 1) * 4 + parseInt(digits[0], 16) - 10;
}
function extractTileInfo(suspectTile) {
let tileIndex = calculateGridPosition(suspectTile.querySelector(".coord").textContent);
let colorIndex;
if (suspectTile.classList.contains("criminal")) {
colorIndex = 1;
} else if (suspectTile.classList.contains("innocent")) {
colorIndex = 2;
} else { colorIndex = 0 }
return [tileIndex, colorIndex]
}
function updateInputGrids(inputTiles, suspectTileIndex, color) {
// Updates a single tile on the input grids.
for (let i = 0; i < inputTiles.length; i++) {
let tileToBeSynced = inputTiles[i][suspectTileIndex];
applyState(tileToBeSynced, color);
tileToBeSynced.classList.add("synced");
tileToBeSynced.removeEventListener("click", tileToBeSynced.onChange)
}
}
function syncInputGrids(mainGrid, inputGrids) {
// Scans the entire main board and syncs the input grids.
const mainTiles = mainGrid.querySelectorAll(".card");
const inputTiles = inputGrids.map(g => tiles(g))
for (const mainTile of mainTiles) {
let [tileIndex, color] = extractTileInfo(mainTile);
if (color != 0) updateInputGrids(inputTiles, tileIndex, color);
}
}
function DesyncInputGrids(inputGrids) {
const inputTiles = inputGrids.map(g => tiles(g, true)).flat()
for (const inputTile of inputTiles) {
applyState(inputTile, 0);
inputTile.classList.remove("synced");
inputTile.addEventListener("click", inputTile.onChange)
}
}
const syncObserver = new MutationObserver((mutationList) => {
for (const mutation of mutationList) {
let currentClassList = Array.from(mutation.target.classList); // Actually unecessary; want to avoid includes-contains confusion
// The Clues By Sam website writes down class names with whitespaces and newlines for some reason
let previousClassList = mutation.oldValue.split("\n").map(string => string.trim()).filter(string => (string != ""));
// Ignoring every change which doesn't indicate that a suspect was revealed
if (!currentClassList.includes("card") || previousClassList.includes("innocent") || previousClassList.includes("criminal")) {
continue;
}
let [tileIndex, color] = extractTileInfo(mutation.target)
if (color == 0) { continue; } // Somehow, a suspects verdict wasn't revealed
updateInputGrids(inputs.map(g => tiles(g)), tileIndex, color);
}
})
addBtn.onclick = () => {
if (inputs.length >= 3) return;
addInput(true);
thirdIndex = inputs.length - 1;
if (preferences.synchronized) syncInputGrids(puzzleGrid, inputs.slice(-1))
addBtn.disabled = true;
};
hideBtn.onclick = () => {
preferences.hidden = !preferences.hidden;
wrap.style.visibility = preferences.hidden ? "hidden" : "visible";
hideBtn.textContent = preferences.hidden ? "Show" : "Hide";
GM_setValue("hidden", preferences.hidden);
};
pinBtn.onclick = () => {
preferences.pinned = !preferences.pinned
wrap.style.position = preferences.pinned ? "absolute" : "fixed";
pinBtn.textContent = preferences.pinned ? "Un-📌" : "📌"
GM_setValue("pinned", preferences.pinned);
}
syncBtn.onclick = () => {
preferences.synchronized = !preferences.synchronized
if (!preferences.synchronized) {
DesyncInputGrids(inputs);
syncObserver.disconnect()
} else {
syncInputGrids(puzzleGrid, inputs);
syncObserver.observe(puzzleGrid, SYNC_OBSERVER_CONFIG)
}
syncBtn.textContent = preferences.synchronized ? "Desync" : "Sync"
GM_setValue("synchronized", preferences.synchronized)
}
document.body.appendChild(wrap);
// The main puzzle grid might not be available when connecting to or refreshing the page
const gridObserver = new MutationObserver((mutationList, go) => {
console.log("Puzzle grid was added")
puzzleGrid = document.getElementById("grid")
syncInputGrids(puzzleGrid, inputs)
syncObserver.observe(puzzleGrid, SYNC_OBSERVER_CONFIG)
go.disconnect()
})
if (preferences.synchronized) {
if (puzzleGrid != null) {
syncInputGrids(puzzleGrid, inputs)
syncObserver.observe(puzzleGrid, SYNC_OBSERVER_CONFIG)
} else {
gridObserver.observe(document.getElementById("root"), { childList: true })
}
}
recompute();
})();