Last active
July 20, 2020 16:21
-
-
Save matthewmorrone/9b4082b0a4989f574f7ab90db84968aa to your computer and use it in GitHub Desktop.
nyt updated their sudoku implementation
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
.su-candidate-button { | |
-webkit-transition: opacity 1; | |
-moz-transition: opacity 1; | |
-o-transition: opacity 1; | |
transition: opacity 1; | |
-webkit-transition-delay: 0; | |
-moz-transition-delay: 0; | |
-o-transition-delay: 0; | |
transition-delay: 0; | |
opacity: 0; | |
} | |
.unit, .single /*:not(.selected) */, [single], [unit], .checked { | |
/*background-color: #00FF0080;*/ | |
} | |
.selected { | |
background-color: #FFDA00FF !important; | |
} | |
.attention { | |
background-color: #00FF0040; | |
} | |
#pzm-congrats .su-modal__message { | |
visibility: hidden; | |
} |
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
console.clear(); | |
Object.defineProperty(Object.prototype, "define", { | |
writable: false, | |
enumerable: false, | |
configurable: true, | |
value: function(key, value) { | |
if (Object[key]) { | |
delete Object[key]; | |
} | |
Object.defineProperty(this, key, { | |
writable: false, | |
enumerable: false, | |
configurable: true, | |
value: value | |
}); | |
return this.key; | |
} | |
}); | |
function round(numToRound, numToRoundTo) { | |
return Math.round(numToRound / numToRoundTo) * numToRoundTo; | |
} | |
function floor(numToFloor, numToFloorTo) { | |
return Math.floor(numToFloor / numToFloorTo) * numToFloorTo; | |
} | |
Array.prototype.define("each", Array.prototype.forEach); | |
Object.prototype.define("getKeys", function() { | |
return Object.keys(this); | |
}); | |
Object.prototype.define("getValues", function() { | |
return Object.values(this); | |
}); | |
Object.prototype.define("getEntries", function() { | |
return Object.entries(this); | |
}); | |
Object.prototype.define("isEmpty", function() { | |
return Object.entries(this).length === 0 && this.constructor === Object; | |
}); | |
Array.prototype.define("unique", function() { | |
return [...new Set(this)]; | |
}); | |
String.prototype.define("toProperCase", function() { | |
return this.replace(/\w\S*/g, function(txt) { | |
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); | |
}); | |
}); | |
String.prototype.define("int", function() { | |
return Number.parseInt(this, 10); | |
}); | |
String.prototype.define("sort", function() { | |
return this.split("").sort().join(""); | |
}) | |
Object.prototype.define("map", function(f, ctx) { | |
let o = this; | |
ctx = ctx || this; | |
let result = {}; | |
Object.keys(o).forEach(function(k) { | |
result[k] = f.call(ctx, o[k], k, o); | |
}); | |
return result; | |
}); | |
Array.prototype.define("toSet", function() { | |
return new Set(this); | |
}); | |
Array.prototype.define("unique", function() { | |
return Array.from(new Set(this)); | |
}); | |
Object.define("size", function(...args) { | |
let len = 0; | |
for (let arg of args) { | |
len += arg.size(); | |
} | |
return len; | |
}); | |
Object.prototype.define("size", function(...args) { | |
let len = 0; | |
for (let arg of args) { | |
len += arg.size(); | |
} | |
return len + Object.entries(this).length; | |
}); | |
String.prototype.define("intersection", function(s2) { | |
let s1 = this, i, a = {}; | |
for (i = 0, | |
l = s1.length; i < l; i++) { | |
a[s1[i]] = (a[s1[i]] || 0) + 1; | |
} | |
for (i = 0, | |
l = s2.length; i < l; i++) { | |
a[s2[i]] = (a[s2[i]] || 0) + 1; | |
} | |
for (var p in a) { | |
if (a[p] <= 1) { | |
delete a[p]; | |
} | |
} | |
return a.getKeys().join("").trim(); | |
}); | |
function unwrap({candidates, frequencies}, index) { | |
frequencies = frequencies[index]; | |
return {frequencies, candidates}; | |
} | |
$.each({ | |
bottom: "bottom", | |
left: "left", | |
right: "right", | |
top: "top" | |
}, function() { | |
let attr = arguments[0] | |
$.fn[attr] = function() { | |
if (arguments.length === 0) { | |
return parseFloat(this.css(attr)) | |
} | |
if (arguments.length === 1) { | |
this.css(attr, arguments[0]) | |
} | |
return this | |
} | |
}) | |
function getActive() { | |
return $(".su-cell.selected"); | |
} | |
function getCandidateContainer(cell) { | |
return $(`div[data-candidate=${$(cell).attr("data-cell")}]`).eq(0) | |
} | |
function getCandidates($cell) { | |
return getCandidateContainer($cell).find("svg") | |
.toArray().filter(el => $(el).attr("class") === "su-candidate") | |
.map(el => $(el).attr("number")).unique().sort().join(""); | |
} | |
function getCoordinates($cell) { | |
$cell = $($cell); | |
let id = Number.parseInt($cell.attr("data-cell"), 10); | |
let x = id % 9 + 1; | |
let y = Math.floor(id / 9) + 1; | |
return { | |
x: x, | |
y: y, | |
id: id, | |
c: getCandidates($cell) | |
}; | |
} | |
function getSec(k) { | |
return getSecs()[k - 1]; | |
} | |
function getSecs() { | |
let rows = getRows(), secs = []; | |
for (let k of Array(9).keys()) { | |
[j, i] = [k % 3 * 3, Math.floor(k / 3) * 3]; | |
let stuff = rows.slice(i, i + 3).map(r => r.slice(j, j + 3)) | |
for (let l = 0; l < rows.length; l++) { | |
stuff.push(rows[l].slice(j, j + 3)); | |
} | |
secs.push(stuff); | |
} | |
secs = secs.map(function($a) { | |
$a = $a.map(a => Array.from(a)); | |
$a = $a[0].concat($a[1]).concat($a[2]); | |
return $a; | |
}); | |
return secs; | |
} | |
function getRow(i) { | |
return $(".su-board").find("[data-cell]").slice(i * 9, (i + 1) * 9); | |
} | |
function getRows() { | |
let cells = $(".su-board").find("[data-cell]"), rows = []; | |
while (cells.length) { | |
rows.push(cells.splice(0, 9)); | |
} | |
return rows; | |
} | |
function getCol(j) { | |
let cells = $(".su-board").find("[data-cell]"), col = []; | |
return cells.filter(function(index) { | |
return index % 9 == j; | |
}); | |
} | |
function getCols($table) { | |
let rows = getRows(), cols = []; | |
for (let i of Array(9).keys()) { | |
let col = []; | |
for (let j of Array(9).keys()) { | |
col.push(rows[j][i]); | |
} | |
cols.push(col); | |
} | |
return cols; | |
} | |
function removeDuplicateCells(cells) { | |
let i, j, coords = [], cell, flag; | |
for (i = 0; i < cells.length; i++) { | |
cell = cells[i]; | |
flag = false; | |
for (j = 0; j < coords.length; j++) { | |
if (cell.id === coords[j].id) { | |
flag = true; | |
} | |
} | |
if (flag === false) { | |
coords.push(cell); | |
} | |
} | |
return coords; | |
} | |
function count(arr) { | |
let a = [], b = [], prev; | |
arr.sort(); | |
for (let i = 0; i < arr.length; i++) { | |
if (arr[i] !== prev) { | |
a.push(arr[i]); | |
b.push(1); | |
} | |
else { | |
b[b.length - 1]++; | |
} | |
prev = arr[i]; | |
} | |
return [a, b]; | |
} | |
function getFrequencies(str, f=1, g=false) { | |
let fre = {}, i, chr, ret = {}; | |
for (i = 0; i < str.length; i++) { | |
chr = str.charAt(i); | |
fre[chr] ? fre[chr]++ : fre[chr] = 1; | |
} | |
if (f == 0) { | |
return fre; | |
} | |
if (g) { | |
Object.entries(fre).forEach(function(entry) { | |
let[key, val] = entry; | |
ret[val] ? ret[val].push(entry) : ret[val] = [entry]; | |
}) | |
return ret; | |
} | |
else { | |
Object.keys(fre).forEach(function(key) { | |
if (fre[key] === f) { | |
ret[key] = fre[key]; | |
} | |
}) | |
return ret; | |
} | |
} | |
function groupData(thing) { | |
let disp = $(thing).map(function(a, sq) { | |
return getCandidates($(sq)); | |
}).toArray(); | |
let fre = getFrequencies(disp.join(""), 1, true); | |
let result = { | |
cells: thing, | |
candidates: disp, | |
frequencies: fre | |
} | |
if (fre[1] && fre[1].length === 1) { | |
result.answer = fre[1][0][0] | |
} | |
if (fre[1] && fre[1].length > 1) { | |
result.answer = fre[1].map(x => x[0]).join(", ") | |
} | |
return result; | |
} | |
function perform($things, n=2, info={parity: "", group: ""}) { | |
let results = {}, $single, data, freq; | |
if (n === 1) { | |
$things.forEach(function(thing, x) { | |
data = groupData(thing); | |
$single = "" | |
if (!data.frequencies.isEmpty()) { | |
if (data.frequencies[n] && data.frequencies[n][0]) { | |
data.cells.each(function(el) { | |
if ($(el).hasClass("prefilled")) return | |
if (getCandidates($(el)).includes(data.frequencies[n][0][0])) { | |
$single = $(el) | |
} | |
}) | |
if (!$single) return | |
$single.attr("parity", $things.parity); | |
$single.attr("single", true) | |
data.coordinates = getCoordinates($single); | |
data.frequencies = data.frequencies.map(x => x.map(y => y[0])); | |
results[x] = data; | |
} | |
} | |
}) | |
return results; | |
} | |
$things.forEach(function(thing, x) { | |
let data = groupData(thing); | |
let d1 = data.candidates.filter(Boolean); | |
let d2 = d1.join("").split("").unique().sort().join(""); | |
if (!d1.length || !d2.length) { | |
return; | |
} | |
if ((n === d1.length) && (d1.length === d2.length)) { | |
data.parity = 'bare'; | |
results[x] = data; | |
return; | |
} | |
data.squares = data.cells.map(cell => getCoordinates(cell)); | |
if (data.frequencies[n]) { | |
let nset = data.frequencies[n].map(m => m[0]).join(""); | |
data.squares = data.squares.filter(square => square.c.intersection(nset)); | |
} | |
let d3 = data.candidates.filter(function(a) { | |
return a.length === n | |
}) | |
let d4 = Array.from(new Set(d3)) | |
if (d3.length > d4.length) { | |
data.set = data.candidates.join("").split("").unique().sort().join("") | |
// if () { | |
// isolate the relevant pair of squares, and check if their frequencies exceed their parity | |
// } | |
if (data.set.replace(d4.join("").sort(), "").length > 0) { | |
data.parity = 'plain'; | |
} | |
else { | |
data.parity = 'bare'; | |
} | |
delete data.set; | |
delete data.squares; | |
data.frequencies = data.frequencies.map(x => x.map(y => y[0])); | |
results[x] = data; | |
return; | |
} | |
let fre = getFrequencies(data.candidates.join(""), 0); | |
let th = [], tn = [], res = {}; | |
Object.entries(fre).forEach(function(entry) { | |
let[key, val] = entry; | |
if (val === n) { | |
th.push(key); | |
} | |
}) | |
if (th.length > 1) { | |
$(thing).each(function() { | |
let obj = getCoordinates($(this)); | |
th.forEach(function(y) { | |
if (obj.c.includes(y)) { | |
tn.push(obj) | |
} | |
}) | |
}) | |
tn = removeDuplicateCells(tn); | |
if (tn.length === n && Object.keys(data.frequencies[n]).length === n && Object.keys(data.frequencies).length > 1) { | |
data.parity = 'mask'; | |
data.frequencies = data.frequencies.map(x => x.map(y => y[0])); | |
delete data.n; | |
delete data.squares; | |
results[x] = data; | |
} | |
} | |
}) | |
return results; | |
} | |
function sudoku() { | |
// next, implement checks for confinement of possibilities in a sec to a single row or col | |
let display = { | |
unit: [] | |
} | |
$cells.each(function() { | |
let coordinates = getCoordinates($(this)) | |
if (coordinates.c.length === 1) { | |
$(this).attr("unit", true) | |
display.unit.push(coordinates) | |
} | |
}) | |
if (!$.isEmptyObject(display.unit)) { | |
console.group("Unit") | |
if (display.unit.length === 1) { | |
console.log("1 unit") | |
} | |
else { | |
console.log(display.unit.length + " units") | |
} | |
console.groupEnd("Unit") | |
} | |
function displayGroups(group, name, index) { | |
if (!$.isEmptyObject(group)) { | |
group.size() === 1 ? name = name.slice(0, -1) : name | |
console.group(name) | |
Object.entries(group).forEach(function (entry) { | |
console.log(Number.parseInt(entry[0])+1, entry[1]) | |
}) | |
console.groupEnd(name) | |
} | |
} | |
let groups = [ | |
"singles", "doubles"// , "triples" // , "quadruples", "pentuples", "sixtuples", "heptuples", "octuples" | |
] | |
groups.each(function(group, n) { | |
let secs = perform($secs, n+1, {parity: "sec", group: groups[n-1]}) | |
, rows = perform($rows, n+1, {parity: "row", group: groups[n-1]}) | |
, cols = perform($cols, n+1, {parity: "col", group: groups[n-1]}) | |
, empty = Object.size(rows, cols, secs) < 1 | |
, prevEmpty = display[groups[n-1]] | |
? display[groups[n-1]].empty | |
: false | |
display[group] = { | |
secs: secs, | |
rows: rows, | |
cols: cols, | |
empty: empty, | |
prevEmpty: prevEmpty | |
} | |
}) | |
function groupsToConsole(groups, name, collapsed) { | |
if (!collapsed) { | |
console.group(name) | |
} | |
else { | |
console.groupCollapsed(name) | |
} | |
displayGroups(groups.rows, "Rows", name.toLowerCase()) | |
displayGroups(groups.cols, "Cols", name.toLowerCase()) | |
displayGroups(groups.secs, "Secs", name.toLowerCase()) | |
console.groupEnd(name) | |
} | |
let displayed = 0 | |
groups.each(function(group, n) { | |
if (display[group].empty) { | |
return | |
} | |
displayed++ | |
groupsToConsole(display[group], group.toProperCase(), displayed !== 1) | |
}) | |
let count = $("[single]").length + display.unit.length | |
$("#count").text(`(${count})`) | |
$("[single]").removeAttr("single") | |
} | |
$(".su-keyboard__auto").after(`<div class="su-keyboard__auto"> | |
<label style='display: contents;'> | |
<input class="su-keyboard__checkbox" type="checkbox" name="helper" id="helper"> | |
<div>Helper Mode <span id='count'></span></div> | |
</label> | |
</div>`) | |
$(function() { | |
$cells = $(".su-cell") | |
$cands = $(".su-candidates") | |
$rows = getRows() | |
$cols = getCols() | |
$secs = getSecs() | |
let top = $(".su-board").offset().top | |
let left = $(".su-board").offset().left | |
let height = $(".su-board").height() | |
let width = $(".su-board").width() | |
let x, y, id | |
$(".su-candidates").each(function(a, b) { | |
x = $(this).find("svg").eq(0).css("left").replace("px", "").int() | |
y = $(this).find("svg").eq(0).css("top") .replace("px", "").int() | |
x = round(floor(x, 78) / width, .11) / .11 | |
y = round(floor(y, 78) / height, .11) / .11 | |
id = 0 + Number.parseInt(y, 10) * 9 + Number.parseInt(x, 10) | |
$(this).attr("x", x+1) | |
$(this).attr("y", y+1) | |
$(this).attr("cell", id) | |
}) | |
$cands.each(function(index, cand) { | |
$(cand).removeAttr("style") | |
let $svg = $(cand).find("svg").eq(0) | |
let cell = $(document.elementsFromPoint($svg.position().left, $svg.position().top)).filter(".su-cell")[0] | |
$svg.parent().attr("data-candidate", $(cell).attr("data-cell")) | |
}) | |
$("#helper").click(function() { | |
$("[single]").removeAttr("single") | |
$("[unit]").removeAttr("unit") | |
$("[parity]").removeAttr("parity") | |
$(".attention").removeClass("attention") | |
$("#count").text("") | |
console.clear() | |
if (!$("#helper").is(":checked")) { | |
return | |
} | |
sudoku() | |
}) | |
$(".su-board").off("mouseup").on("mouseup", function(e) { | |
if (e.which === 116) return | |
$active1 = {x: 0, y: 0} | |
$active2 = {x: 0, y: 0} | |
$active1 = getCoordinates(getActive()) | |
setTimeout(function() { | |
$active2 = getCoordinates(getActive()) | |
if ($active1.x !== $active2.x || $active1.y !== $active2.y) { | |
return | |
} | |
if ($(".conflicted").length) { | |
return | |
} | |
$("[single]").removeAttr("single") | |
$("[unit]").removeAttr("unit") | |
$("[parity]").removeAttr("parity") | |
$(".attention").removeClass("attention") | |
$("#count").text("") | |
console.clear() | |
if (!$("#helper").is(":checked")) { | |
return | |
} | |
sudoku() | |
}) | |
}) | |
$(document).off("keyup").on("keyup", function(e) { | |
if (e.which === 116) return | |
if (e.which >= 37 && e.which <= 40) { | |
return | |
} | |
e.preventDefault() | |
$active1 = {x: 0, y: 0} | |
$active2 = {x: 0, y: 0} | |
$active1 = getCoordinates(getActive()) | |
setTimeout(function() { | |
$active2 = getCoordinates(getActive()) | |
if ($active1.x !== $active2.x || $active1.y !== $active2.y) { | |
return | |
} | |
if ($(".conflicted").length) { | |
return | |
} | |
$("[single]").removeAttr("single") | |
$("[unit]").removeAttr("unit") | |
$("[parity]").removeAttr("parity") | |
$(".attention").removeClass("attention") | |
$("#count").text("") | |
console.clear() | |
if (!$("#helper").is(":checked")) { | |
return | |
} | |
sudoku() | |
}) | |
}) | |
if ($("#helper").is(":checked")) { | |
sudoku() | |
} | |
$(".su-app__print").before(`<span role="button" class="pz-toolbar-button su-app__reset">Reset</span>`) | |
$(".su-app__reset").click(function() { | |
$(".su-toolbar__help-menu-toggle").click() | |
$(".su-toolbar__help-menu .su-button__dropdown-item").eq(5).click() | |
}) | |
$("#portal-game-toolbar").prepend(`<span role="button" class="pz-toolbar-button"><a href="/crosswords">Home</a></span>`) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment