Last active
May 25, 2024 17:34
-
-
Save dclamage/3d76a56c9153a8546888a31d65765cb7 to your computer and use it in GitHub Desktop.
Adds more constraints 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-NewConstraints | |
// @namespace http://tampermonkey.net/ | |
// @version 1.9 | |
// @description Adds more constraints 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-start | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
// Adding a new constraint: | |
// 1. Add a new entry to the newConstraintInfo array | |
// 2. If the type is not already supported, add it to the following: | |
// a. exportPuzzle | |
// b. importPuzzle | |
// c. categorizeTools | |
// d. Add a drawing helper function for what it looks like | |
// 3. Add conflict highlighting logic to candidatePossibleInCell | |
// 4. Add a new constraint class (see 'Constraint classes' comment) | |
const newConstraintInfo = [{ | |
name: 'Renban', | |
type: 'line', | |
color: '#F067F0', | |
colorDark: '#642B64', | |
lineWidth: 0.4, | |
tooltip: [ | |
'Numbers on a renban line must be consecutive, but in any order.', | |
'Digits cannot repeat on a renban line.', | |
'', | |
'Click and drag to draw a renban line.', | |
'Click on a renban line to remove it.', | |
'Shift click and drag to draw overlapping renban lines.', | |
] | |
}, | |
{ | |
name: 'Whispers', | |
type: 'line', | |
color: '#67F067', | |
colorDark: '#357D35', | |
lineWidth: 0.3, | |
tooltip: [ | |
'Adjacent numbers on a whispers line must have a difference of 5 or greater.', | |
'[For non-9x9 grid sizes, this adjust to be (size / 2) rounded up.]', | |
'', | |
'Click and drag to draw a whispers line.', | |
'Click on a whispers line to remove it.', | |
'Shift click and drag to draw overlapping whispers lines.', | |
] | |
}, | |
] | |
const doShim = function() { | |
// Additional import/export data | |
const origExportPuzzle = exportPuzzle; | |
exportPuzzle = function(includeCandidates) { | |
const compressed = origExportPuzzle(includeCandidates); | |
const puzzle = JSON.parse(compressor.decompressFromBase64(compressed)); | |
// Add cosmetic version of constraints for those not using the solver plugin | |
for (let constraintInfo of newConstraintInfo) { | |
const id = cID(constraintInfo.name); | |
const puzzleEntry = puzzle[id]; | |
if (puzzleEntry && puzzleEntry.length > 0) { | |
if (constraintInfo.type === 'line') { | |
if (!puzzle.line) { | |
puzzle.line = []; | |
} | |
for (let instance of puzzleEntry) { | |
puzzle.line.push({ | |
lines: instance.lines, | |
outlineC: constraintInfo.color, | |
width: constraintInfo.lineWidth, | |
isNewConstraint: true | |
}); | |
} | |
} | |
} | |
} | |
return compressor.compressToBase64(JSON.stringify(puzzle)); | |
} | |
const origImportPuzzle = importPuzzle; | |
importPuzzle = function(string, clearHistory) { | |
// Remove any generated cosmetics | |
const puzzle = JSON.parse(compressor.decompressFromBase64(string)); | |
if (puzzle.line) { | |
puzzle.line = puzzle.line.filter(line => !line.isNewConstraint); | |
if (puzzle.line.length === 0) { | |
delete puzzle.line; | |
} | |
} | |
string = compressor.compressToBase64(JSON.stringify(puzzle)); | |
origImportPuzzle(string, clearHistory); | |
} | |
// Draw the new constraints | |
const origDrawConstraints = drawConstraints; | |
drawConstraints = function(layer) { | |
if (layer === 'Bottom') { | |
for (let info of newConstraintInfo) { | |
const id = cID(info.name); | |
const constraint = constraints[id]; | |
if (constraint) { | |
for (let a = 0; a < constraint.length; a++) { | |
constraint[a].show(); | |
} | |
} | |
} | |
} | |
origDrawConstraints(layer); | |
} | |
// Conflict highlighting for new constraints | |
const origCandidatePossibleInCell = candidatePossibleInCell; | |
candidatePossibleInCell = function(n, cell, options) { | |
if (!options) { | |
options = {}; | |
} | |
if (!options.bruteForce && cell.value) { | |
return cell.value === n; | |
} | |
if (!origCandidatePossibleInCell(n, cell, options)) { | |
return false; | |
} | |
// Renban | |
const constraintsRenban = constraints[cID('Renban')]; | |
if (constraintsRenban && constraintsRenban.length > 0) { | |
for (let renban of constraintsRenban) { | |
for (let line of renban.lines) { | |
const index = line.indexOf(cell); | |
if (index > -1) { | |
let numMatchingValue = 0; | |
let minValue = -1; | |
let maxValue = -1; | |
for (let lineCell of line) { | |
if (lineCell.value) { | |
minValue = minValue === -1 || minValue > lineCell.value ? lineCell.value : minValue; | |
maxValue = maxValue === -1 || maxValue < lineCell.value ? lineCell.value : maxValue; | |
if (lineCell.value === n) { | |
numMatchingValue++; | |
if (numMatchingValue > 1) { | |
return false; | |
} | |
} | |
} | |
} | |
if (minValue !== -1 && maxValue !== -1) { | |
if (n - minValue > line.length - 1 || maxValue - n > line.length - 1) { | |
return false; | |
} | |
} | |
} | |
} | |
} | |
} | |
// Whispers | |
const constraintsWhispers = constraints[cID('Whispers')]; | |
if (constraintsWhispers && constraintsWhispers.length > 0) { | |
const whispersDiff = Math.ceil(size / 2); | |
for (let whispers of constraintsWhispers) { | |
for (let line of whispers.lines) { | |
const index = line.indexOf(cell); | |
if (index > -1) { | |
if (n - whispersDiff <= 0 && n + whispersDiff > size) { | |
return false; | |
} | |
if (index > 0) { | |
const prevCell = line[index - 1]; | |
if (prevCell.value && Math.abs(prevCell.value - n) < whispersDiff) { | |
return false; | |
} | |
} | |
if (index < line.length - 1) { | |
const nextCell = line[index + 1]; | |
if (nextCell.value && Math.abs(nextCell.value - n) < whispersDiff) { | |
return false; | |
} | |
} | |
} | |
} | |
} | |
} | |
return true; | |
} | |
// Drawing helpers | |
const drawLine = function(line, color, colorDark, lineWidth) { | |
ctx.lineWidth = cellSL * lineWidth * 0.5; | |
ctx.fillStyle = boolSettings['Dark Mode'] ? colorDark : color; | |
ctx.strokeStyle = boolSettings['Dark Mode'] ? colorDark : color; | |
ctx.beginPath(); | |
ctx.arc(line[0].x + cellSL / 2, line[0].y + cellSL / 2, ctx.lineWidth / 2, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.moveTo(line[0].x + cellSL / 2, line[0].y + cellSL / 2); | |
for (var b = 1; b < line.length; b++) { | |
ctx.lineTo(line[b].x + cellSL / 2, line[b].y + cellSL / 2); | |
} | |
ctx.stroke(); | |
ctx.beginPath(); | |
ctx.arc(line[line.length - 1].x + cellSL / 2, line[line.length - 1].y + cellSL / 2, ctx.lineWidth / 2, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
// Constraint classes | |
// Renban | |
window.renban = function(cell) { | |
this.lines = [ | |
[cell] | |
]; | |
this.show = function() { | |
const renbanInfo = newConstraintInfo.filter(c => c.name === 'Renban')[0]; | |
for (var a = 0; a < this.lines.length; a++) { | |
drawLine(this.lines[a], renbanInfo.color, renbanInfo.colorDark, renbanInfo.lineWidth); | |
} | |
} | |
this.addCellToLine = function(cell) { | |
if (this.lines[this.lines.length - 1].length < size) { | |
this.lines[this.lines.length - 1].push(cell); | |
} | |
} | |
} | |
// Whispers | |
window.whispers = function(cell) { | |
this.lines = [ | |
[cell] | |
]; | |
this.show = function() { | |
const whispersInfo = newConstraintInfo.filter(c => c.name === 'Whispers')[0]; | |
for (var a = 0; a < this.lines.length; a++) { | |
drawLine(this.lines[a], whispersInfo.color, whispersInfo.colorDark, whispersInfo.lineWidth); | |
} | |
} | |
this.addCellToLine = function(cell) { | |
this.lines[this.lines.length - 1].push(cell); | |
} | |
} | |
const origCategorizeTools = categorizeTools; | |
categorizeTools = function() { | |
origCategorizeTools(); | |
let toolLineIndex = toolConstraints.indexOf('Palindrome'); | |
for (let info of newConstraintInfo) { | |
if (info.type === 'line') { | |
toolConstraints.splice(++toolLineIndex, 0, info.name); | |
lineConstraints.push(info.name); | |
} | |
} | |
draggableConstraints = [...new Set([...lineConstraints, ...regionConstraints])]; | |
multicellConstraints = [...new Set([...lineConstraints, ...regionConstraints, ...borderConstraints, ...cornerConstraints])]; | |
betweenCellConstraints = [...borderConstraints, ...cornerConstraints]; | |
allConstraints = [...boolConstraints, ...toolConstraints]; | |
tools = [...toolConstraints, ...toolCosmetics]; | |
selectableTools = [...selectableConstraints, ...selectableCosmetics]; | |
lineTools = [...lineConstraints, ...lineCosmetics]; | |
regionTools = [...regionConstraints, ...regionCosmetics]; | |
diagonalRegionTools = [...diagonalRegionConstraints, ...diagonalRegionCosmetics]; | |
outsideTools = [...outsideConstraints, ...outsideCosmetics]; | |
outsideCornerTools = [...outsideCornerConstraints, ...outsideCornerCosmetics]; | |
oneCellAtATimeTools = [...perCellConstraints, ...draggableConstraints, ...draggableCosmetics]; | |
draggableTools = [...draggableConstraints, ...draggableCosmetics]; | |
multicellTools = [...multicellConstraints, ...multicellCosmetics]; | |
} | |
// Tooltips | |
for (let info of newConstraintInfo) { | |
descriptions[info.name] = info.tooltip; | |
} | |
// Puzzle title | |
// Unfortuantely, there's no way to shim this so it's duplicated in full. | |
getPuzzleTitle = function() { | |
var title = ''; | |
ctx.font = titleLSize + 'px Arial'; | |
if (customTitle.length) { | |
title = customTitle; | |
} else { | |
if (size !== 9) | |
title += size + 'x' + size + ' '; | |
if (getCells().some(a => a.region !== (Math.floor(a.i / regionH) * regionH) + Math.floor(a.j / regionW))) | |
title += 'Irregular '; | |
if (constraints[cID('Extra Region')].length) | |
title += 'Extra-Region '; | |
if (constraints[cID('Odd')].length && !constraints[cID('Even')].length) | |
title += 'Odd '; | |
if (!constraints[cID('Odd')].length && constraints[cID('Even')].length) | |
title += 'Even '; | |
if (constraints[cID('Odd')].length && constraints[cID('Even')].length) | |
title += 'Odd-Even '; | |
if (constraints[cID('Diagonal +')] !== constraints[cID('Diagonal -')]) | |
title += 'Single-Diagonal '; | |
if (constraints[cID('Nonconsecutive')] && !(constraints[cID('Difference')].length && constraints[cID('Difference')].some(a => ['', '1'].includes(a.value))) && !constraints[cID('Ratio')].negative) | |
title += 'Nonconsecutive '; | |
if (constraints[cID('Nonconsecutive')] && constraints[cID('Difference')].length && constraints[cID('Difference')].some(a => ['', '1'].includes(a.value)) && !constraints[cID('Ratio')].negative) | |
title += 'Consecutive '; | |
if (!constraints[cID('Nonconsecutive')] && constraints[cID('Difference')].length && constraints[cID('Difference')].every(a => ['', '1'].includes(a.value))) | |
title += 'Consecutive-Pairs '; | |
if (constraints[cID('Antiknight')]) | |
title += 'Antiknight '; | |
if (constraints[cID('Antiking')]) | |
title += 'Antiking '; | |
if (constraints[cID('Disjoint Groups')]) | |
title += 'Disjoint-Group '; | |
if (constraints[cID('XV')].length || constraints[cID('XV')].negative) | |
title += 'XV ' + (constraints[cID('XV')].negative ? '(-) ' : ''); | |
if (constraints[cID('Little Killer Sum')].length) | |
title += 'Little Killer '; | |
if (constraints[cID('Sandwich Sum')].length) | |
title += 'Sandwich '; | |
if (constraints[cID('Thermometer')].length) | |
title += 'Thermo '; | |
if (constraints[cID('Palindrome')].length) | |
title += 'Palindrome '; | |
if (constraints[cID('Difference')].length && constraints[cID('Difference')].some(a => !['', '1'].includes(a.value)) && !(constraints[cID('Nonconsecutive')] && constraints[cID('Ratio')].negative)) | |
title += 'Difference '; | |
if ((constraints[cID('Ratio')].length || constraints[cID('Ratio')].negative) && !(constraints[cID('Nonconsecutive')] && constraints[cID('Ratio')].negative)) | |
title += 'Ratio ' + (constraints[cID('Ratio')].negative ? '(-) ' : '');; | |
if (constraints[cID('Nonconsecutive')] && constraints[cID('Ratio')].negative) | |
title += 'Kropki '; | |
if (constraints[cID('Killer Cage')].length) | |
title += 'Killer '; | |
if (constraints[cID('Clone')].length) | |
title += 'Clone '; | |
if (constraints[cID('Arrow')].length) | |
title += 'Arrow '; | |
if (constraints[cID('Between Line')].length) | |
title += 'Between '; | |
if (constraints[cID('Quadruple')].length) | |
title += 'Quadruples ' | |
if (constraints[cID('Minimum')].length || constraints[cID('Maximum')].length) | |
title += 'Extremes ' | |
for (let info of newConstraintInfo) { | |
if (constraints[cID(info.name)] && constraints[cID(info.name)].length > 0) { | |
title += `${info.name} `; | |
} | |
} | |
title += 'Sudoku'; | |
if (constraints[cID('Diagonal +')] && constraints[cID('Diagonal -')]) | |
title += ' X'; | |
if (title === 'Sudoku') | |
title = 'Classic Sudoku'; | |
if (ctx.measureText(title).width > (canvas.width - 711)) | |
title = 'Extreme Variant Sudoku'; | |
} | |
buttons[buttons.findIndex(a => a.id === 'EditInfo')].x = canvas.width / 2 + ctx.measureText(title).width / 2 + 40; | |
return title; | |
} | |
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 grid === 'undefined' || | |
typeof exportPuzzle === 'undefined' || | |
typeof importPuzzle === 'undefined' || | |
typeof drawConstraints === 'undefined' || | |
typeof candidatePossibleInCell === 'undefined' || | |
typeof categorizeTools === 'undefined' || | |
typeof drawPopups === 'undefined') { | |
return; | |
} | |
clearInterval(intervalId); | |
doShim(); | |
}, 16); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment