Last active
January 3, 2022 04:51
-
-
Save dclamage/54934b266608f79f8e534736213975b2 to your computer and use it in GitHub Desktop.
Adds multicolor and blue cage functionality to f-puzzles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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