UNPKG

@ne1410s/griddler

Version:

Complete package for creating, sharing and solving griddler grids!

1,032 lines (1,011 loc) 119 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ne_grid = {})); })(this, (function (exports) { 'use strict'; class CustomElementBase extends HTMLElement { constructor(css, html, mode = 'closed') { super(); this.root = this.attachShadow({ mode }); this.root.innerHTML = html; const style = document.createElement('style'); style.textContent = css; this.root.appendChild(style); } } function decode(b64) { const bIndex = (b64 + '').indexOf('base64,'); return bIndex === -1 ? b64 : window.atob(b64.substring(bIndex + 7)); } function reduceCss(cssIn) { return cssIn .replace(/\s+/g, ' ') .replace(/([,{}:;])\s/g, '$1') .replace(/\s([{])/g, '$1'); } function reduceHtml(htmlIn) { return htmlIn.replace(/\s+/g, ' ').replace(/(^|>)\s+(<|$)/g, '$1$2'); } /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __spreadArray(to, from, pack) { if (arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var ChainedQuery = /** @class */ (function () { function ChainedQuery(mapper) { var sources = []; for (var _i = 1; _i < arguments.length; _i++) { sources[_i - 1] = arguments[_i]; } var _this = this; this.mapper = mapper; this._items = []; this.get = function (index) { return _this._items[index]; }; this.add.apply(this, sources); } Object.defineProperty(ChainedQuery.prototype, "targets", { get: function () { return this._items.filter(function (it) { return it instanceof EventTarget; }).map(function (it) { return it; }); }, enumerable: false, configurable: true }); Object.defineProperty(ChainedQuery.prototype, "elements", { get: function () { return this._items.filter(function (it) { return it instanceof Element; }).map(function (it) { return it; }); }, enumerable: false, configurable: true }); Object.defineProperty(ChainedQuery.prototype, "nodes", { get: function () { return this._items.filter(function (it) { return it instanceof Node; }).map(function (it) { return it; }); }, enumerable: false, configurable: true }); Object.defineProperty(ChainedQuery.prototype, "containers", { get: function () { return this._items.map(function (it) { return it; }).filter(function (p) { return typeof p.append === 'function'; }); }, enumerable: false, configurable: true }); Object.defineProperty(ChainedQuery.prototype, "length", { get: function () { return this._items.length; }, enumerable: false, configurable: true }); ChainedQuery.prototype.add = function () { var _a; var sources = []; for (var _i = 0; _i < arguments.length; _i++) { sources[_i] = arguments[_i]; } (_a = this._items).push.apply(_a, this.mapper.Map(sources)); return this; }; // TODO: This must not fall over if trying to perform an action // on an inappropriate type. eg setting an attribute on window, or // trying to query a non-parent node.. ChainedQuery.prototype.each = function (func) { this._items.forEach(function (item, i) { return func(item, i); }); return this; }; ChainedQuery.prototype.prop = function (name, value) { return this.each(function (item) { if (name in item) { item[name] = value; } }); }; //#region Targets ChainedQuery.prototype.fire = function (eventName, detail) { var evt = new CustomEvent(eventName, { detail: detail }); this.targets.forEach(function (t) { return t.dispatchEvent(evt); }); return this; }; ChainedQuery.prototype.on = function (eventNames, func) { var evts = eventNames.split(ChainedQuery.WHITESPACE_RGX); this.targets.forEach(function (t) { evts.forEach(function (name) { return t.addEventListener(name, func); }); }); return this; }; //#endregion //#region Elements ChainedQuery.prototype.attr = function (name, value, ns) { this.elements.forEach(function (elem) { if (value != null) elem.setAttributeNS(ns, name, value); else elem.removeAttributeNS(ns, name); }); return this; }; ChainedQuery.prototype.toggle = function (className, doSet) { this.elements.forEach(function (elem) { var force = typeof doSet === 'function' ? doSet(elem) : !!doSet; elem.classList.toggle(className, force); }); return this; }; //#endregion //#region Nodes ChainedQuery.prototype.empty = function () { this.nodes.forEach(function (n) { while (n.firstChild) { n.removeChild(n.firstChild); } }); return this; }; ChainedQuery.prototype.remove = function () { return new (ChainedQuery.bind.apply(ChainedQuery, __spreadArray([void 0, this.mapper], this._items.filter(function (it) { var handle = it instanceof Node; if (handle) it.parentNode.removeChild(it); return !handle; }), false)))(); }; //#endregion //#region Containers ChainedQuery.prototype.append = function () { var sources = []; for (var _i = 0; _i < arguments.length; _i++) { sources[_i] = arguments[_i]; } this.appendIn.apply(this, sources); return this; }; ChainedQuery.prototype.appendIn = function () { var _this = this; var sources = []; for (var _i = 0; _i < arguments.length; _i++) { sources[_i] = arguments[_i]; } return new (ChainedQuery.bind.apply(ChainedQuery, __spreadArray([void 0, this.mapper], this.containers.reduce(function (acc, parent) { var nodes = new (ChainedQuery.bind.apply(ChainedQuery, __spreadArray([void 0, _this.mapper], sources, false)))().nodes; parent.append.apply(parent, nodes); acc.push.apply(acc, nodes); return acc; }, []), false)))(); }; ChainedQuery.prototype.find = function (selector) { return new (ChainedQuery.bind.apply(ChainedQuery, __spreadArray([void 0, this.mapper], this.containers.reduce(function (acc, parent) { acc.push.apply(acc, Array.from(parent.querySelectorAll(selector))); return acc; }, []), false)))(); }; ChainedQuery.prototype.first = function (selector) { return new (ChainedQuery.bind.apply(ChainedQuery, __spreadArray([void 0, this.mapper], this.containers.map(function (parent) { return parent.querySelector(selector); }).filter(function (found) { return !!found; }), false)))(); }; ChainedQuery.WHITESPACE_RGX = /\s+/; return ChainedQuery; }()); var SourceMapper = /** @class */ (function () { function SourceMapper() { } SourceMapper.prototype.Map = function (sources) { return sources.reduce(function (acc, item) { if (typeof item === 'string') { acc.push.apply(acc, (SourceMapper.NAIVE_HTML_RGX.test(item) ? SourceMapper.MapHTML(item) : SourceMapper.MapSelector(item))); } else if (item.tag) acc.push(SourceMapper.MapParam(item)); else if (item instanceof Node) acc.push(item); else if (item instanceof EventTarget) acc.push(item); else if (item instanceof ChainedQuery) item.each(function (i) { return acc.push(i); }); else console.warn('Unrecognised item:', item); return acc; }, []); }; SourceMapper.MapHTML = function (html) { var template = document.createElement('template'); template.innerHTML = html; return Array.from(template.content.childNodes); }; SourceMapper.MapSelector = function (selector) { return Array.from(document.querySelectorAll(selector)); }; SourceMapper.MapParam = function (p) { var n = document.createElement(p.tag); if (p.text) n.textContent = p.text; for (var key in p.attr || {}) { if (p.attr[key] != null) { n.setAttribute(key, p.attr[key]); } } for (var key in p.evts || {}) { if (typeof p.evts[key] === 'function') { n.addEventListener(key, p.evts[key]); } } return n; }; SourceMapper.NAIVE_HTML_RGX = /^\s*<.*>\s*$/m; return SourceMapper; }()); var mapper = new SourceMapper(); function q() { var input = []; for (var _i = 0; _i < arguments.length; _i++) { input[_i] = arguments[_i]; } return new (ChainedQuery.bind.apply(ChainedQuery, __spreadArray([void 0, mapper], input, false)))(); } var markupUrl$6 = "data:text/html;base64,PHNsb3Q+PC9zbG90Pg0KPHVsIGNsYXNzPSJ0b3AiPjwvdWw+DQo="; var stylesUrl$6 = "data:text/css;base64,dWwsDQpsaSB7DQogIHBhZGRpbmc6IDA7DQogIG1hcmdpbjogMDsNCiAgZm9udDogaW5oZXJpdDsNCn0NCnNsb3QsDQpsaS5zcGxpdCAuaWNvbiB7DQogIGRpc3BsYXk6IG5vbmU7DQp9DQoudG9wOm5vdCgub3BlbiksDQp1bDpub3QoLnRvcCkgew0KICB2aXNpYmlsaXR5OiBoaWRkZW47DQp9DQoNCnVsIHsNCiAgYm9yZGVyOiB2YXIoLS1ib3JkZXIsIDFweCBzb2xpZCAjYmJiKTsNCiAgYmFja2dyb3VuZDogdmFyKC0tYmcsICNmZmYpOw0KICBib3gtc2hhZG93OiB2YXIoLS1ib3gtc2hhZG93LCAycHggMnB4IDNweCAjODg4KTsNCiAgdHJhbnNpdGlvbjogYmFja2dyb3VuZCAwLjZzOw0KICB6LWluZGV4OiA5OTk5OTk5Ow0KfQ0KDQoudG9wIHsNCiAgcG9zaXRpb246IGZpeGVkOw0KICBmb250LXNpemU6IHZhcigtLWZvbnQtc2l6ZSwgMC42NXJlbSk7DQogIHVzZXItc2VsZWN0OiBub25lOw0KfQ0KDQouaWNvbi5sZWZ0IHsNCiAgbGVmdDogMWVtOw0KICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTsNCn0NCi5pY29uLnJpZ2h0IHsNCiAgcmlnaHQ6IDFlbTsNCiAgdHJhbnNmb3JtOiB0cmFuc2xhdGUoNTAlLCAtNTAlKTsNCn0NCi5pY29uIHsNCiAgbWF4LXdpZHRoOiAxZW07DQogIG1heC1oZWlnaHQ6IDFlbTsNCiAgcG9zaXRpb246IGFic29sdXRlOw0KICB0b3A6IDUwJTsNCiAgbGluZS1oZWlnaHQ6IDA7DQp9DQoNCmxpID4gcDpub3QoOmVtcHR5KSB7DQogIG1hcmdpbjogMCAxZW07DQogIHdoaXRlLXNwYWNlOiBub3dyYXA7DQp9DQpsaS5kaXNhYmxlZCB7DQogIGNvbG9yOiB2YXIoLS1kaXNhYmxlZC1mZywgI2JiYik7DQp9DQpsaS5zcGxpdCB7DQogIGJvcmRlci10b3A6IHZhcigtLXNwbGl0LWJvcmRlciwgdmFyKC0tYm9yZGVyLCAxcHggc29saWQgI2JiYikpOw0KICBwYWRkaW5nOiAwOw0KICBtYXJnaW46IDAuM2VtOw0KfQ0KbGkuc3BsaXQ6Zmlyc3QtY2hpbGQsDQpsaS5zcGxpdDpsYXN0LWNoaWxkLA0KbGkuc3BsaXQgKyBsaS5zcGxpdCB7DQogIGRpc3BsYXk6IG5vbmU7DQp9DQpsaS5ob3Zlcjpub3QoLmRpc2FiZWQpOm5vdCguc3BsaXQpIHsNCiAgYmFja2dyb3VuZDogdmFyKC0taG92ZXItaXRlbS1iZywgI2JiYik7DQp9DQoudG9wLm9wZW4gbGkuaG92ZXIgPiB1bCB7DQogIHZpc2liaWxpdHk6IHZpc2libGU7DQp9DQpsaSB7DQogIG1hcmdpbjogMC4yZW0gMDsNCiAgcGFkZGluZzogMC41ZW0gMWVtOw0KICBkaXNwbGF5OiBmbGV4Ow0KICBqdXN0aWZ5LWNvbnRlbnQ6IHNwYWNlLWJldHdlZW47DQogIHRleHQtYWxpZ246IGxlZnQ7DQogIHBvc2l0aW9uOiByZWxhdGl2ZTsNCiAgY3Vyc29yOiBkZWZhdWx0Ow0KICBjb2xvcjogdmFyKC0tZmcsICMwMDApOw0KfQ0KDQpsaS5ncm91cDo6YWZ0ZXIgew0KICBjb250ZW50OiAnXDI1YjYnOw0KICBwb3NpdGlvbjogYWJzb2x1dGU7DQogIGZvbnQtc2l6ZTogMC42NWVtOw0KICByaWdodDogMDsNCiAgdG9wOiA1MCU7DQogIHRyYW5zZm9ybTogdHJhbnNsYXRlKC01MCUsIC01MCUpOw0KfQ0KDQp1bDpub3QoLnRvcCkgew0KICBwb3NpdGlvbjogYWJzb2x1dGU7DQogIGxlZnQ6IDEwMCU7DQogIHRvcDogLTAuMjVlbTsNCn0NCg0KdWwubmVzdGxlIHsNCiAgbGVmdDogMC41ZW07DQogIHRvcDogMTAwJTsNCn0NCg=="; class NeMenu extends CustomElementBase { constructor() { super(NeMenu.Css, NeMenu.Html); this.top = this.root.querySelector('ul'); } connectedCallback() { if (!this._connected) { setTimeout(() => this.reload()); q(this.parentNode).on('contextmenu', (e) => this.onParentContext(e)); q(this, this.parentNode).on('contextmenu wheel', (e) => { e.preventDefault(); e.stopPropagation(); }); q(this).on('mousedown', (e) => e.stopPropagation()); q(window).on('mousedown resize wheel', () => this.close()); this._connected = true; } } /** Opens the menu. */ open() { // close all menus const doc = this.parentElement.getRootNode(); doc.querySelectorAll('ne14-menu').forEach((m) => m.close()); // style this one as open this.top.classList.add('open'); q(this).fire('menuopen'); } /** Closes the menu. */ close() { if (this.top.classList.contains('open')) { this.top.classList.remove('open'); q(this).fire('menuclose'); } } /** Reloads active contents based on client dom. */ reload() { q(this.top) .empty() .append(...this.walk(this, false)); } onParentContext(event) { if (this.isConnected) { // update position (y) const y = event.clientY; const height = this.top.offsetHeight; const posY = y + height + 2 > window.innerHeight ? y - height : y; this.top.style.top = `${Math.max(0, posY)}px`; // update position (x) const x = event.clientX; const width = this.top.offsetWidth; const posX = x + width + 2 > window.innerWidth ? x - width : x; this.top.style.left = `${Math.max(0, posX)}px`; // open this.open(); } } walk(ul, parentDisabled, ref = '') { let levelItemNo = 0; return Array.from(ul.children) .filter((c) => c instanceof HTMLLIElement && !c.classList.contains('hidden') && (c.textContent || c.classList.contains('split'))) .reduce((acc, li) => { var _a, _b, _c; const children = Array.from(li.children).map((el) => el); const a = children.find((n) => n instanceof HTMLAnchorElement); const ul = children.find((n) => n instanceof HTMLUListElement); const isSplit = li.classList.contains('split'); const isGrouper = !isSplit && ul && ul.querySelector('li'); const isDisabled = !isSplit && (parentDisabled || li.classList.contains('disabled')); const aChildren = Array.from((a === null || a === void 0 ? void 0 : a.children) || []).map((el) => el); const imgs = children .concat(aChildren) .filter((n) => n instanceof HTMLImageElement) .map((n) => n); if (!isSplit) levelItemNo++; const classes = []; if (isSplit) classes.push('split'); else { if (isDisabled) classes.push('disabled'); if (isGrouper) classes.push('group'); if ((a === null || a === void 0 ? void 0 : a.target) === '_blank') classes.push('click-out'); else if (a) classes.push('click-in'); } const bestTextNode = [...children, li].find((c) => c.innerText); const bestText = isSplit ? null : ((_a = bestTextNode === null || bestTextNode === void 0 ? void 0 : bestTextNode.innerText) !== null && _a !== void 0 ? _a : `Item ${levelItemNo}`); const shortcut = isSplit || isGrouper ? null : li.getAttribute('aria-keyshortcuts'); const liRef = `${ref}${levelItemNo}`; const eventDetail = { ref: liRef, title: bestText, origin: a || li }; const handleClick = () => { if (!isDisabled && !isGrouper && !isSplit) { eventDetail.origin.click(); q(this).fire('itemselect', eventDetail); this.close(); } }; const handleMouseEnter = (e) => { if (!isDisabled && !isSplit) { const domLi = e.target; if (isGrouper) { const domUl = Array.from(domLi.children).find((n) => n instanceof HTMLUListElement); const liRect = domLi.getBoundingClientRect(); domUl.classList.toggle('nestle', liRect.right + domUl.clientWidth + 2 > window.innerWidth); } domLi.classList.add('hover'); q(this).fire('itemhover', eventDetail); } }; const handleMouseLeave = (e) => { if (!isDisabled && !isSplit) { e.target.classList.remove('hover'); q(this).fire('itemunhover', eventDetail); } }; const $domItem = q({ tag: 'li' }) .attr('class', classes.length ? classes.join(' ') : null) .attr('aria-keyshortcuts', shortcut) .on('click contextmenu', handleClick) .on('mouseenter', handleMouseEnter) .on('mouseleave', handleMouseLeave); const charLeft = li.dataset.charLeft; const charRight = li.dataset.charRight; const imgLeft = imgs.find((i) => !i.classList.contains('right')); const imgRight = imgs.find((i) => i.classList.contains('right')); if (!isSplit && !isGrouper && bestText) { if (NeMenu.CHAR_REF_REGEX.test(charLeft)) $domItem.append(`<span class='icon left'>&#x${charLeft};</span>`); else if (charLeft) console.warn(`ne14-menu: Bad hex code '${charLeft}' to left of '${bestText}'.`); else if (imgLeft) $domItem.append(`<img class='icon left' src='${imgLeft.src}'/>`); if (NeMenu.CHAR_REF_REGEX.test(charRight)) $domItem.append(`<span class='icon right'>&#x${charRight};</span>`); else if (charRight) console.warn(`ne14-menu: Bad hex code '${charRight}' to right of '${bestText}'.`); else if (imgRight) $domItem.append(`<img class='icon right' src='${imgRight.src}'/>`); } if (bestText) $domItem.append({ tag: 'p', text: bestText }); if (shortcut) $domItem.append({ tag: 'p', text: shortcut }); if (isGrouper) $domItem.appendIn({ tag: 'ul' }).append(...this.walk(ul, isDisabled, `${liRef}-`)); // Do not push two consecutive splits if (!isSplit || !((_c = (_b = acc[acc.length - 1]) === null || _b === void 0 ? void 0 : _b.classList) === null || _c === void 0 ? void 0 : _c.contains('split'))) { acc.push($domItem.elements[0]); } return acc; }, []); } } NeMenu.Css = reduceCss(decode(stylesUrl$6)); NeMenu.Html = reduceHtml(decode(markupUrl$6)); NeMenu.CHAR_REF_REGEX = /^[0-9a-f]{4,5}$/i; if ('customElements' in window) { window.customElements.define('ne14-menu', NeMenu); } /** The state of a cell. Values are unicode representations of the state. */ var CellState; (function (CellState) { CellState[CellState["Blank"] = 9723] = "Blank"; CellState[CellState["Marked"] = 9635] = "Marked"; CellState[CellState["Filled"] = 9724] = "Filled"; })(CellState || (CellState = {})); /** The type of a set of cells. */ var SetType; (function (SetType) { SetType[SetType["Column"] = 0] = "Column"; SetType[SetType["Row"] = 1] = "Row"; })(SetType || (SetType = {})); class Label { /** Gets the minimum total size for a set of separated values. */ static minSize(values) { return values.reduce((tot, curr) => { return tot > 0 ? curr + tot + 1 : curr + tot; }, 0); } constructor(value, index) { this.value = value; this.index = index; this.indexRef = `L${this.index}`; } } class LabelSetLink { constructor(labelIndex, setIndex, known) { this.labelIndex = labelIndex; this.setIndex = setIndex; this.known = known; } } /** A contiguous set of cells. */ class CellSetBase { constructor(start, type, index, size) { this.start = start; this.type = type; this.index = index; this.size = size; this.end = this.start + this.size - 1; } } /** A set of consecutive 'filled' cells. */ class BlockSet extends CellSetBase { constructor(start, type, index, size, spaceIndex) { super(start, type, index, size); this.start = start; this.type = type; this.index = index; this.size = size; this.spaceIndex = spaceIndex; } } /** A set of unmarked cells. */ class SpaceSet extends CellSetBase { } /** A complete set of cells - representing a column or row. */ class FullSet extends CellSetBase { get stateRef() { return `${this.cells.map((state) => String.fromCharCode(state)).join('')}`; } get consoleRef() { return `${this.indexRef}: ${this.stateRef} ${this.labelRef}`; } get labelRef() { return this.labels.map((l) => l.value).join('.'); } get labelsRef() { return this.labels.map((l, i) => this.getLabelRef(i)).join(' / '); } get solved() { return !this.cells.some((state) => state === CellState.Blank); } constructor(start, type, index, cells, labelValues) { super(start, type, index, cells.length); this.start = start; this.type = type; this.index = index; this.cells = cells; this.labelSpaceMap = []; this.labelBlockMap = []; this.spaces = []; this.blocks = []; this.altType = this.type === SetType.Row ? SetType.Column : SetType.Row; this.labels = labelValues.map((v, i) => new Label(v, i)); this.indexRef = `${SetType[this.type].substr(0, 3)} ${this.index}`; this.performCellPass(true); this.performCellPass(false); this.setLabelSpaces(); this.setLabelBlocks(); let linksChanged; do { linksChanged = this.updateMaps(); this.applyBlockValueRanges(); linksChanged = linksChanged || this.applyBlockPositionRanges(); linksChanged = linksChanged || this.applyDistinctBlockPairing(); } while (linksChanged); } /** Marks and fills all appropriate blocks, returning indices of the mark and fill cells. */ solve() { const blanx = this.cells .map((cell, idx) => ({ c: cell, i: idx })) .filter((ic) => ic.c === CellState.Blank); const mIdx = this.labels .reduce((ac, l) => ac.filter((ib) => ib.i < l.earliest || ib.i > l.latest), blanx) .map((mark) => mark.i); const fIdx = this.labels .reduce((ac, l) => ac.concat(blanx.filter((ib) => ib.i < l.earliest + l.value && ib.i > l.latest - l.value)), []) .map((fill) => fill.i); this.blocks // Add fills for edged-out blocks .filter((b) => b.rightEdge !== b.end || b.leftEdge !== b.start) .forEach((b) => blanx .filter((ic) => ic.i >= b.leftEdge && ic.i <= b.rightEdge) .forEach((ci) => fIdx.push(ci.i))); this.blocks // Add marks to blocks at their maximum .filter((b) => 1 + b.rightEdge - b.leftEdge === b.maxSize) .forEach((b) => { mIdx.push(b.leftEdge - 1); mIdx.push(b.rightEdge + 1); }); const mIdxFilt = blanx // mIdx was originally a Set to prevent duplicates, but array means es5. .filter((ic) => mIdx.indexOf(ic.i) !== -1) .map((ic) => ic.i) .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); const fIdxFilt = blanx // fIdx was originally a Set to prevent duplicates; but array means es5. .filter((ic) => fIdx.indexOf(ic.i) !== -1) .map((ic) => ic.i) .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); mIdxFilt.forEach((mInd) => (this.cells[mInd] = CellState.Marked)); fIdxFilt.forEach((fInd) => (this.cells[fInd] = CellState.Filled)); return { marks: mIdxFilt, fills: fIdxFilt }; } getLabelRef(index) { const label = this.labels[index]; const sLinks = this.getLinksForLabel(index, true).map((sl) => sl.setIndex + (sl.known ? 'K' : 'M')); const bLinks = this.getLinksForLabel(index, false).map((bl) => bl.setIndex + (bl.known ? 'K' : 'M')); return `${label.indexRef}: R=${label.earliest}-${label.latest} S=${sLinks} B=${bLinks}`; } getLinksForLabel(lIndex, forSpace) { return (forSpace ? this.labelSpaceMap : this.labelBlockMap).filter((mi) => mi.labelIndex === lIndex); } getLinksForSet(setIndex, forSpace) { return (forSpace ? this.labelSpaceMap : this.labelBlockMap).filter((mi) => mi.setIndex === setIndex); } deleteLink(lIndex, setIndex, forSpace) { const mapName = forSpace ? 'labelSpaceMap' : 'labelBlockMap'; this[mapName] = this[mapName].filter((mi) => mi.labelIndex !== lIndex || mi.setIndex !== setIndex); } upsertLink(lIndex, setIndex, forSpace, known) { const map = forSpace ? this.labelSpaceMap : this.labelBlockMap; const mapItems = map.filter((mi) => mi.labelIndex === lIndex && mi.setIndex === setIndex); if (mapItems.length === 1) { mapItems[0].known = known; } else { map.push(new LabelSetLink(lIndex, setIndex, known)); } } upsertLinks(lIndex, forSpace, sets, known) { sets.forEach((set) => this.upsertLink(lIndex, set.index, forSpace, known)); } /** * Iterates cells in a particular direction. The forward pass sets spaces and blocks for the * set as well as earliest label positions. The backward pass sets labels latest positions only. * NB: This method is not reasonably capable of managing block-block interactions. */ performCellPass(forwards) { let spaceStart = -1; let blockStart = -1; let blockIndex = -1; let labelStart = -1; let blocks = []; let labelIndex = forwards ? 0 : this.labels.length - 1; const allCells = forwards ? this.cells.slice(0) : this.cells.slice(0).reverse(); const labelIncrementor = forwards ? 1 : -1; // Clone the array and explicitly terminate it by a marked cell allCells.concat([CellState.Marked]).forEach((cell, i) => { // START SPACE AND LABEL if (spaceStart === -1 && cell !== CellState.Marked) { spaceStart = i; labelStart = i; } // START BLOCK if (blockStart === -1 && cell === CellState.Filled) { blockStart = i; } // LABEL const label = this.labels[labelIndex]; // If the label has reached its end if (label && labelStart !== -1 && i - labelStart >= label.value) { // If the left-bunched label ends with a block... if (blockStart !== -1) { labelStart = i - label.value; } else { if (forwards) { label.earliest = labelStart; } else { label.latest = this.cells.length - (labelStart + 1); } labelStart += 1 + label.value; labelIndex += labelIncrementor; } } // END BLOCK const spaceIndex = this.spaces.length; if (blockStart !== -1 && cell !== CellState.Filled) { const blockLen = i - blockStart; blocks.push(new BlockSet(blockStart, this.type, ++blockIndex, blockLen, spaceIndex)); // Closing a block whose length exceeds the label value must reset label start if (label && blockLen > label.value) { labelStart = i + 1; } // If at the end of block, and we are still on the same label and it fitted ok if (label && i - labelStart === label.value) { if (forwards) { label.earliest = labelStart; } else { label.latest = this.cells.length - (labelStart + 1); } labelIndex += labelIncrementor; labelStart += 1 + label.value; } blockStart = -1; } // END SPACE if (spaceStart !== -1 && cell === CellState.Marked) { if (forwards) { const space = new SpaceSet(spaceStart, this.type, spaceIndex, i - spaceStart); this.spaces.push(space); this.blocks.push(...blocks); } // If at the end of space, and we are still on the same label and it fitted ok if (label && label.index === labelIndex && i - labelStart >= label.value) { if (forwards) { label.earliest = labelStart; } else { label.latest = this.cells.length - (labelStart + 1); } labelIndex += labelIncrementor; } blocks = []; spaceStart = -1; labelStart = this.cells.length; } }); } setLabelSpaces() { this.labels.forEach((l) => { const spaces = this.spaces.filter((s) => l.earliest <= s.end && l.latest >= s.start); if (spaces.length === 0) { const msg = `At least one label could not be assigned`; throw new RangeError(`${msg} - ${this.consoleRef}`); } this.upsertLinks(l.index, true, spaces, spaces.length === 1); }); } setLabelBlocks() { this.labels.forEach((l) => { const labelSpaces = this.getLinksForLabel(l.index, true); const labelBlocks = labelSpaces.reduce((acc, curr) => { // Blocks not exceeding label value, within range const ranged = this.blocks.filter((b) => b.start >= l.earliest && b.end <= l.latest && b.size <= l.value); return acc.concat(ranged); }, []); this.upsertLinks(l.index, false, labelBlocks, false); }); } /** For each block with only 1 linked label, make the linkage known */ updateMaps() { let linksChanged = false; this.blocks.forEach((block) => { const links = this.labelBlockMap.filter((bl) => bl.setIndex === block.index); if (links.length === 1) { const lIdx = links[0].labelIndex; const label = this.labels[lIdx]; // Make a block-label link 'known' this.upsertLink(lIdx, block.index, false, true); // Which requires: Updating earliest and latest values const space = this.spaces.filter((s) => s.index === block.spaceIndex)[0]; label.earliest = Math.max(label.earliest, space.start, 1 + block.end - label.value); label.latest = Math.min(label.latest, space.end, block.start + label.value - 1); // And: Removing space-links no longer in range this.getLinksForLabel(lIdx, true) .map((ls) => this.spaces[ls.setIndex]) .filter((s) => label.earliest > s.end || label.latest < s.start) .forEach((deadLink) => this.deleteLink(lIdx, deadLink.index, true)); // Which itself requires: If one maybe space, making this known this.getLinksForLabel(lIdx, true) .filter((ls, i, arr) => arr.length === 1) .forEach((knownLink) => this.upsertLink(lIdx, knownLink.setIndex, true, true)); // And finally: Removing block-links no longer in range this.labelBlockMap .filter((lb) => lb.setIndex !== block.index && lb.labelIndex === lIdx) .map((lb) => this.blocks[lb.setIndex]) .filter((b) => b.start > label.latest || b.end < label.earliest) .forEach((deadLink) => { linksChanged = true; this.deleteLink(lIdx, deadLink.index, false); }); } }); return linksChanged; } /** Now the maps are good set min and max values based on labels. */ applyBlockValueRanges() { this.blocks.forEach((block) => { block.minSize = this.cells.length; block.maxSize = 0; this.getLinksForSet(block.index, false) .map((bl) => this.labels[bl.labelIndex]) .forEach((l) => { block.minSize = Math.min(block.minSize, l.value); block.maxSize = Math.max(block.maxSize, l.value); }); }); } /** Now min and max are good, inspect for unbridgable blocks and return whether links have changed as a result. */ applyBlockPositionRanges() { let linksChanged = false; this.blocks.forEach((block, bIdx) => { const space = this.spaces.filter((s) => s.index === block.spaceIndex)[0]; const sibBlocks = this.blocks.filter((b) => b.spaceIndex === block.spaceIndex); // Neighbouring unbridgable blocks const prevUnBlk = sibBlocks.filter((b) => b.index === bIdx - 1 && 1 + block.start - b.maxSize > b.end)[0]; const nextUnBlk = sibBlocks.filter((b) => b.index === bIdx + 1 && block.end + b.maxSize - 1 < b.start)[0]; const unlEdge = prevUnBlk == null ? space.start : prevUnBlk.end + 2; const unrEdge = nextUnBlk == null ? space.end : nextUnBlk.start - 2; if (prevUnBlk) { linksChanged = linksChanged || this.tryRemoveLinks(bIdx, false); } if (nextUnBlk) { linksChanged = linksChanged || this.tryRemoveLinks(bIdx, true); } // Edging away from calculated edges block.leftEdge = Math.min(block.start, 1 + unrEdge - block.minSize); block.rightEdge = Math.max(block.end, unlEdge + block.minSize - 1); // If edged out, update known labels of the block according to new limits if (block.leftEdge < block.start || block.rightEdge > block.end) { this.labelBlockMap .filter((bl) => bl.setIndex === block.index && bl.known) .forEach((bl) => { const label = this.labels[bl.labelIndex]; label.earliest = Math.max(label.earliest, space.start, 1 + block.rightEdge - label.value); label.latest = Math.min(label.latest, space.end, block.leftEdge + label.value - 1); }); } }); return linksChanged; } /** Checks whether block and neighbouring unbridgable block need labels removing. */ tryRemoveLinks(blockIndex, forNext) { const neighbourIndex = forNext ? blockIndex + 1 : blockIndex - 1; const blockLabelLinks = this.getLinksForSet(blockIndex, false); const blockLabelIdx = this.getLinksForSet(blockIndex, false) .map((li) => li.labelIndex) .join(','); const unbrLabelIdx = this.getLinksForSet(neighbourIndex, false) .map((li) => li.labelIndex) .join(','); if (blockLabelLinks.length === 2 && unbrLabelIdx === blockLabelIdx) { this.deleteLink(blockLabelLinks[forNext ? 0 : 1].labelIndex, neighbourIndex, false); this.deleteLink(blockLabelLinks[forNext ? 1 : 0].labelIndex, blockIndex, false); return true; } return false; } /** Removes block/label links where label count matches distinct block count */ applyDistinctBlockPairing() { let linksChanged; let prevBlock; let prevInReach; let labelIndex; const labelAssignment = []; // Assemble distinct block count by label index this.blocks.forEach((currBlock, blockIndex) => { prevBlock = this.blocks[blockIndex - 1]; prevInReach = prevBlock && prevBlock.spaceIndex === currBlock.spaceIndex && prevBlock.start + prevBlock.maxSize - 1 >= currBlock.rightEdge; labelIndex = prevInReach ? labelAssignment.length - 1 : labelAssignment.length; labelAssignment[labelIndex] = labelAssignment[labelIndex] || []; labelAssignment[labelIndex].push(blockIndex); }); // If counts match, remove block links except for those assembled if (labelAssignment.length === this.labels.length) { this.labels.forEach((lbl) => { this.getLinksForLabel(lbl.index, false) .filter((ln) => labelAssignment[lbl.index].indexOf(ln.setIndex) === -1) .forEach((dl) => { linksChanged = true; this.deleteLink(dl.labelIndex, dl.setIndex, false); }); }); return linksChanged; } return false; } } /** The outcome of running a 'solve' grid method. */ class SolveResult { constructor(gridObject, solved, solvedMs) { this.solved = solved; this.solvedMs = solvedMs; if (solved) { this.grid = gridObject; } else { this.brokenGrid = gridObject; } } } class Utils { /** Returns a new array of the specified size filled with the specified value. */ static FillArray(size, valuer) { const retVal = new Array(size); for (let i = 0; i < size; i++) { retVal[i] = valuer(); } return retVal; } /** Pools multiple events, firing once per delay cycle. */ static Throttle(func, delay = 200) { let active; return function (args) { if (!active) { if (active == null) func.call(this, args); active = true; const that = this; setTimeout(() => { active = !!func.call(that, args); setTimeout(() => (active = active || null), delay / 10); }, delay); } }; } /** Pools multiple events, firing once after the delay period. */ static Debounce(func, delay = 200) { let timeout; return function (arg) { clearTimeout(timeout); const that = this; timeout = setTimeout(() => func.call(that, arg), delay); }; } } /** A griddler grid. */ class Grid { static load(gridObject) { const grid = new Grid(gridObject.columns.length, gridObject.rows.length); gridObject.columns.forEach((col, colIdx) => { grid.setLabels(SetType.Column, colIdx, col.labels); }); gridObject.rows.forEach((row, rowIdx) => { grid.setLabels(SetType.Row, rowIdx, row.labels); (row.cells || []) .map((cell, cellIdx) => ({ oState: cell, idx: cellIdx })) .filter((obj) => obj.oState !== 0) .forEach((obj) => { const state = obj.oState === 1 ? CellState.Filled : CellState.Marked; grid.setState(SetType.Row, rowIdx, obj.idx, state); }); }); return grid; } get consoleRef() { return this._rowLabelCache.map((n, r) => this.getFullSet(SetType.Row, r).stateRef).join('\r\n'); } get unsolvedCellCount() { return this._cellCache .reduce((ac, cur) => ac.concat(cur), []) .filter((state) => state === CellState.Blank).length; } get solved() { return !this._cellCache .reduce((ac, cur) => ac.concat(cur), []) .some((state) => state === CellState.Blank); } get gridObject() { return { columns: this._columnLabelCache.map((n, c) => ({ labels: this.getLabels(SetType.Column, c), })), rows: this._rowLabelCache.map((n, r) => { return { labels: this.getLabels(SetType.Row, r), cells: this.getFullSet(SetType.Row, r).cells.map((c) => c === CellState.Marked ? 2 : CellState.Filled ? 1 : 0), }; }), }; } constructor(width, height) { this.width = width; this.height = height; this._rowLabelCache = Utils.FillArray(this.height, () => []); this._columnLabelCache = Utils.FillArray(this.width, () => []); this._cellCache = this._columnLabelCache.map(() => Utils.FillArray(this.height, () => CellState.Blank)); } nextHint() { const allCols = Utils.FillArray(this.width, () => 0).map((x, i) => i); const allRows = Utils.FillArray(this.height, () => 0).map((x, i) => i); const colsrows = this.solveSetsRecursively([allCols, allRows], true); let result = null; if (colsrows) { const pass = colsrows[263 % colsrows.length]; result = { type: SetType[pass.type], idx: pass.idx }; } return result; } solve() { const t0 = new Date().getTime(); const allCols = Utils.FillArray(this.width, () => 0).map((x, i) => i); const allRows = Utils.FillArray(this.height, () => 0).map((x, i) => i); this.solveSetsRecursively([allCols, allRows]); const t1 = new Date().getTime(); return new SolveResult(this.gridObject, this.solved, t1 - t0); } setState(setType, setIndex, cellIndex, state) { if (setType === SetType.Row) { this._cellCache[cellIndex][setIndex] = state; } else { this._cellCache[setIndex][cellIndex] = state; } } setLabels(type, index, values) { const setRef = `${SetType[type].substr(0, 3)} ${index}`; const target = type === SetType.Row ? this._rowLabelCache : this._columnLabelCache; if (target[index] == null) { const msg = 'Not found'; throw new RangeError(`${setRef}: ${msg}`); } const setSize = type === SetType.Row ? this.width : this.height; const minSize = Label.minSize(values); if (minSize > setSize) { const msg = `The minimum total label size (${minSize}) exceeds the set length (${setSize})`; throw new RangeError(`${setRef}: ${msg}`); } target[index] = values; } getFullSet(type, index) { const cells = type === SetType.Row ? this._cellCache.map((val) => val[index]) : this._cellCache[index]; return new FullSet(0, type, index, cells, this.getLabels(type, index)); } getLabels(type, index) { return type === SetType.Row ? this._rowLabelCache[index] : this._columnLabelCache[index]; } solveSetsRecursively(colsrows, shallow = false) { const allUnsolvedHintworthy = colsrows[0] .map((c) => this.getFullSet(SetType.Column, c)) .concat(colsrows[1].map((r) => this.getFullSet(SetType.Row, r))) .filter((set) => !set.solved) .map((us) => { const cr = us.solve(); cr.marks.forEach((m) => this.setState(us.type, us.index, m, CellState.Marked)); cr.fills.forEach((f) => this.setState(us.type, us.index, f, CellState.Filled)); return { type: us.type, idx: us.index, // for hints crType: us.altType, crIdx: cr.marks.concat(cr.fills), // for solving }; }) .filter((obj) => obj.crIdx.length !== 0); if (shallow) { return allUnsolvedHintworthy; } const allUnsolved = allUnsolvedHintworthy .reduce((ac, obj) => { ac[obj.crType].push(...obj.crIdx); return ac; }, [[], []]) .map((arr) => arr.sort((a