Skip to content

Instantly share code, notes, and snippets.

@dclamage
Last active January 3, 2022 04:51
Show Gist options
  • Save dclamage/54934b266608f79f8e534736213975b2 to your computer and use it in GitHub Desktop.
Save dclamage/54934b266608f79f8e534736213975b2 to your computer and use it in GitHub Desktop.
Adds multicolor and blue cage functionality to f-puzzles
// ==UserScript==
// @name Fpuzzles-Multicolor
// @namespace http://tampermonkey.net/
// @version 1.13
// @description Adds multicolor and blue cage functionality to f-puzzles
// @author Rangsk
// @match https://*.f-puzzles.com/*
// @match https://f-puzzles.com/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
const doShim = function() {
'use strict';
const degToRad = Math.PI / 180;
function equalValues(a, b) {
return Math.abs(a - b) < 0.001;
}
function pointOnSquare(angle) {
let x = Math.cos(angle);
let y = Math.sin(angle);
const norm = 1.0 / Math.max(Math.abs(x), Math.abs(y));
x = x * norm;
y = y * norm;
return { x: x, y: y };
}
const origEnter = enter;
let isFirstCell = true;
let pendingEnterCells = [];
enter = function(num, cell, logProgress) {
isFirstCell = true;
origEnter(num, cell, logProgress);
}
const origCell = cell;
cell = function(i, j, outside) {
const c = new origCell(i, j, outside);
c.showBottom = function() {
let colors = [];
for (let i = 0; i < this.cArray.length; i++) {
if (this.cArray[i] || !previewMode && this.highlightArray[i]) {
colors.push(i);
}
}
if (colors.length == 0) {
return;
}
if (colors.length === 1) {
ctx.fillStyle = highlightCs[colors[0]];
ctx.fillRect(this.x, this.y, cellSL, cellSL);
return;
}
const halfWidth = cellSL * 0.5;
let centerX = this.x + halfWidth;
let centerY = this.y + halfWidth;
let baseAngle = -60 * degToRad;
let angleStep = 360 * degToRad / colors.length;
for (let i = 0; i < colors.length; i++) {
const startAngle = baseAngle + i * angleStep;
const endAngle = startAngle + angleStep;
const p1 = pointOnSquare(startAngle);
const p2 = pointOnSquare(endAngle);
const region = new Path2D();
region.moveTo(centerX, centerY);
region.lineTo(centerX + p1.x * halfWidth, centerY + p1.y * halfWidth);
if (equalValues(p1.x, -1)) {
if (!equalValues(p1.x, p2.x)) {
region.lineTo(this.x, this.y);
}
if (equalValues(p2.x, 1)) {
region.lineTo(this.x + cellSL, this.y);
}
} else if (equalValues(p1.y, -1)) {
if (!equalValues(p1.y, p2.y)) {
region.lineTo(this.x + cellSL, this.y);
}
if (equalValues(p2.y, 1)) {
region.lineTo(this.x + cellSL, this.y + cellSL);
}
} else if (equalValues(p1.x, 1)) {
if (!equalValues(p1.x, p2.x)) {
region.lineTo(this.x + cellSL, this.y + cellSL);
}
if (equalValues(p2.x, -1)) {
region.lineTo(this.x, this.y + cellSL);
}
} else {
if (!equalValues(p1.y, p2.y)) {
region.lineTo(this.x, this.y + cellSL);
}
if (equalValues(p2.y, -1)) {
region.lineTo(this.x, this.y);
}
}
region.lineTo(centerX + p2.x * halfWidth, centerY + p2.y * halfWidth);
region.closePath();
ctx.fillStyle = highlightCs[colors[i]];
ctx.fill(region);
}
}
// Outline code provided by Sven Neumann
const getOutline = function(cells, os) {
let edgePoints = [],
grid = [],
segs = [],
shapes = [];
const checkRC = (r, c) => ((grid[r] !== undefined) && (grid[r][c] !== undefined)) || false;
const pointOS = {
tl: [os, os],
tr: [os, 1 - os],
bl: [1 - os, os],
br: [1 - os, 1 - os],
tc: [os, 0.5],
rc: [0.5, 1 - os],
bc: [1 - os, 0.5],
lc: [0.5, os],
};
const dirRC = { t: [-1, 0], r: [0, 1], b: [1, 0], l: [0, -1] };
const flipDir = { t: 'b', r: 'l', b: 't', l: 'r' };
const patterns = [
{ name: 'otl', bits: '_0_011_1_', enter: 'bl', exit: 'rt', points: 'tl' },
{ name: 'otr', bits: '_0_110_1_', enter: 'lt', exit: 'br', points: 'tr' },
{ name: 'obr', bits: '_1_110_0_', enter: 'tr', exit: 'lb', points: 'br' },
{ name: 'obl', bits: '_1_011_0_', enter: 'rb', exit: 'tl', points: 'bl' },
{ name: 'itl', bits: '01_11____', enter: 'lt', exit: 'tl', points: 'tl' },
{ name: 'itr', bits: '_10_11___', enter: 'tr', exit: 'rt', points: 'tr' },
{ name: 'ibr', bits: '____11_10', enter: 'rb', exit: 'br', points: 'br' },
{ name: 'ibl', bits: '___11_01_', enter: 'bl', exit: 'lb', points: 'bl' },
{ name: 'et', bits: '_0_111___', enter: 'lt', exit: 'rt', points: 'tc' },
{ name: 'er', bits: '_1__10_1_', enter: 'tr', exit: 'br', points: 'rc' },
{ name: 'eb', bits: '___111_0_', enter: 'rb', exit: 'lb', points: 'bc' },
{ name: 'el', bits: '_1_01__1_', enter: 'bl', exit: 'tl', points: 'lc' },
{ name: 'out', bits: '_0_010_1_', enter: 'bl', exit: 'br', points: 'tl,tr' },
{ name: 'our', bits: '_0_110_0_', enter: 'lt', exit: 'lb', points: 'tr,br' },
{ name: 'oub', bits: '_1_010_0_', enter: 'tr', exit: 'tl', points: 'br,bl' },
{ name: 'oul', bits: '_0_011_0_', enter: 'rb', exit: 'rt', points: 'bl,tl' },
{ name: 'solo', bits: '_0_010_0_', enter: '', exit: '', points: 'tl,tr,br,bl' },
];
const checkPatterns = (row, col) => patterns
.filter(({ name, bits }) => {
let matches = true;
bits.split('').forEach((b, i) => {
let r = row + Math.floor(i / 3) - 1,
c = col + i % 3 - 1,
check = checkRC(r, c);
matches = matches && ((b === '_') || (b === '1' && check) || (b === '0' && !check));
});
return matches;
});
const getSeg = (segs, rc, enter) => segs.find(([r, c, _, pat]) => r === rc[0] && c === rc[1] && pat.enter === enter);
const followShape = segs => {
let shape = [],
seg = segs[0];
const getNext = ([r, c, cell, pat]) => {
if (pat.exit === '') return;
let [exitDir, exitSide] = pat.exit.split('');
let nextRC = [r + dirRC[exitDir][0], c + dirRC[exitDir][1]];
let nextEnter = flipDir[exitDir] + exitSide;
return getSeg(segs, nextRC, nextEnter);
};
do {
shape.push(seg);
segs.splice(segs.indexOf(seg), 1);
seg = getNext(seg);
} while (seg !== undefined && shape.indexOf(seg) === -1);
return shape;
};
const shapeToPoints = shape => {
let points = [];
shape.forEach(([r, c, cell, pat]) => pat.points
.split(',')
.map(point => pointOS[point])
.map(([ros, cos]) => [r + ros, c + cos])
.forEach(rc => points.push(rc))
);
return points;
};
cells.forEach(cell => {
const { i: col, j: row } = cell;
grid[row] = grid[row] || [];
grid[row][col] = { cell };
});
cells.forEach(cell => {
const { i: col, j: row } = cell, matchedPatterns = checkPatterns(row, col);
matchedPatterns.forEach(pat => segs.push([row, col, cell, pat]));
});
while (segs.length > 0) {
const shape = followShape(segs);
if (shape.length > 0) shapes.push(shape);
}
shapes.forEach(shape => {
edgePoints = edgePoints.concat(shapeToPoints(shape).map(([r, c], idx) => [idx === 0 ? 'M' : 'L', r, c]));
edgePoints.push(['Z']);
});
return edgePoints;
};
c.showSelection = function() {
if (previewMode) {
return;
}
if (cellsSeenBySelection.includes(this) && !selection.includes(this)) {
ctx.fillStyle = boolSettings['Dark Mode'] ? '#FFFFFF' : '#FFFF00';
ctx.globalAlpha = 0.08;
ctx.fillRect(this.x, this.y, cellSL, cellSL);
ctx.globalAlpha = 1.00;
}
if (!selection || selection.length == 0 || this.i !== 0 || this.j !== 0) {
return;
}
const lineOffset = 1.0 / 16.0;
const lineWidth = cellSL * lineOffset * 2.0;
const outline = getOutline(selection, lineOffset);
const prevStrokeStyle = ctx.strokeStyle;
const prevLineWidth = ctx.lineWidth;
const prevLineCap = ctx.lineCap;
ctx.beginPath();
for (let i = 0; i < outline.length; i++) {
const point = outline[i];
if (point[0] === 'Z') {
ctx.closePath();
} else if (point[0] == 'M') {
ctx.moveTo(gridX + point[1] * cellSL, gridY + point[2] * cellSL);
} else {
ctx.lineTo(gridX + point[1] * cellSL, gridY + point[2] * cellSL);
}
}
ctx.fillStyle = '#FFFFFF';
ctx.globalAlpha = 0.4;
ctx.fill();
ctx.strokeStyle = '#007FFF';
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.globalAlpha = 0.7;
ctx.stroke();
ctx.globalAlpha = 1.00;
ctx.strokeStyle = prevStrokeStyle;
ctx.lineWidth = prevLineWidth;
ctx.lineCap = prevLineCap;
}
const numColors = highlightCs.length;
c.highlightArray = new Array(numColors).fill(false);
c.cArray = new Array(numColors).fill(false);
c.origEnter = c.enter;
c.enter = function(value, forced, isLast) {
this.origEnter(value, forced);
if (forced || tempEnterMode !== 'Highlight') {
return;
}
if (isFirstCell) {
pendingEnterCells = [];
isFirstCell = false;
}
pendingEnterCells.push(this);
if (!isLast) {
return;
}
const color = value - 1;
const isClear = color <= 0;
// Determine if there are any cells which do not have this highlight set
const setColor = !isClear && pendingEnterCells.some(cell => !cell.cArray[color] && !cell.highlightArray[color]);
for (var i = 0; i < pendingEnterCells.length; i++) {
const cell = pendingEnterCells[i];
if (mode === 'Setting') {
if (!isClear) {
cell.c = setColor ? color : 0;
cell.highlight = 0;
cell.cArray[color] = setColor;
cell.highlightArray[color] = false;
} else {
cell.c = 0;
cell.highlight = 0;
cell.cArray.fill(false);
cell.highlightArray.fill(false);
}
} else {
if (!isClear) {
cell.highlight = setColor ? color : 0;
cell.highlightArray[color] = setColor && !cell.cArray[color];
} else {
cell.highlight = 0;
cell.highlightArray.fill(false);
}
}
}
}
return c;
}
function exportColorArray(colorArray) {
if (!colorArray) {
return null;
}
let exportedArray = [];
for (let i = 0; i < colorArray.length; i++) {
if (colorArray[i]) {
exportedArray.push(highlightCs[i]);
}
}
return (exportedArray.length > 0) ? exportedArray : null;
}
function importColorArray(colorArray) {
let importedArray = new Array(highlightCs.length).fill(false);
if (colorArray && colorArray.length > 0) {
for (let i = 0; i < colorArray.length; i++) {
importedArray[highlightCs.indexOf(colorArray[i])] = true;
}
}
return importedArray;
}
const origExportPuzzle = exportPuzzle;
exportPuzzle = function(includeCandidates) {
const compressed = origExportPuzzle(includeCandidates);
const puzzle = JSON.parse(compressor.decompressFromBase64(compressed));
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
const cArray = exportColorArray(grid[i][j].cArray);
if (cArray) {
puzzle.grid[i][j].cArray = cArray;
}
const highlightArray = exportColorArray(grid[i][j].highlightArray);
if (highlightArray) {
puzzle.grid[i][j].highlightArray = highlightArray;
}
}
}
return compressor.compressToBase64(JSON.stringify(puzzle));
}
const origImportPuzzle = importPuzzle;
importPuzzle = function(string, clearHistory) {
origImportPuzzle(string, clearHistory);
const puzzle = JSON.parse(compressor.decompressFromBase64(string));
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
grid[i][j].cArray = importColorArray(puzzle.grid[i][j].cArray);
grid[i][j].highlightArray = importColorArray(puzzle.grid[i][j].highlightArray);
if (!puzzle.grid[i][j].cArray && grid[i][j].c) {
grid[i][j].cArray[grid[i][j].c] = true;
}
if (!puzzle.grid[i][j].highlightArray && grid[i][j].highlight) {
grid[i][j].highlightArray[grid[i][j].highlight] = true;
}
}
}
if (clearHistory) {
generateCandidates();
resetKnownPuzzleInformation();
clearChangeHistory();
}
}
if (window.boolConstraints) {
let prevButtons = buttons.splice(0, buttons.length);
window.onload();
buttons.splice(0, buttons.length);
for (let i = 0; i < prevButtons.length; i++) {
buttons.push(prevButtons[i]);
}
}
}
let intervalId = setInterval(() => {
if (typeof cell === 'undefined' ||
typeof exportPuzzle === 'undefined' ||
typeof importPuzzle === 'undefined') {
return;
}
clearInterval(intervalId);
doShim();
}, 16);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment