diff --git a/js/Geometry/CellSet.js b/js/Geometry/CellSet.js index 037529f..0d17557 100644 --- a/js/Geometry/CellSet.js +++ b/js/Geometry/CellSet.js @@ -1,99 +1,58 @@ -import {diagonal, isSmaller, key, plus, ofKey, zero} from Geometry.Vector; -import {at, generate} from Grid.Util; +import size from Config; +import {isSmaller, plus, zero} from Geometry.Vector; +import * as Vector from Geometry.Vector; +import {at, iter} from Grid.Util; +import * as Matrix from Grid.Util; +import Set; -function CellSet(definition) { - definition = definition || {}; - this.cells = {}; - if(definition.type == 'rectangle') { - this.fromRectangle(definition.origin, definition.offset); - } else if(definition.type == 'isochrome') { - this.fromIsochrome(definition.grid, definition.origin); - } else if(definition.type == 'union') { - this.fromUnion(definition.sets); - } -} +var CellSet = Set.make(Vector); -CellSet.prototype.fromRectangle = function(origin, maxOffset) { - generate( - maxOffset.row, - maxOffset.column, - function(offset) { - this.add(plus(origin, offset)); - }.bind(this) - ); +return { + CellSet: CellSet, + union: CellSet.union, + rectangle: rectangle, + isochrome: isochrome, + row: row, + column: column }; -CellSet.prototype.fromIsochrome = function(grid, origin) { - var originColor = at(grid, origin); +function rectangle(origin, delta) { + var cellSet = new CellSet(); + iter(Matrix.make(delta), function(_, cell) {cellSet.add(plus(origin, cell));}); + return cellSet; +} + +function isochrome(coloring, origin) { + var cellSet = new CellSet(); + var color = at(coloring, origin); var queue = [origin]; while(queue.length > 0) { var cell = queue.shift(); - this.add(cell); + cellSet.add(cell); for(var d = -1; d < 2; d += 2) { - [plus(cell, vertical(d)), plus(cell, horizontal(d))].forEach( - gateKeeper(this, grid, queue, originColor) + [plus(cell, Vector.vertical(d)), plus(cell, Vector.horizontal(d))].forEach( + gateKeeper(cellSet, coloring, queue, color) ); } } -}; - -CellSet.prototype.fromUnion = function(sets) { - for(var i = 0; i < sets.length; i++) { - for(var k in sets[i].cells) { - this.cells[k] = true; - } - } + return cellSet; } -CellSet.prototype.add = function(v) { - this.cells[key(v)] = true; -}; - -CellSet.prototype.remove = function(v) { - delete this.cells[key(v)]; -}; - -CellSet.prototype.contains = function(v) { - return !!this.cells[key(v)]; -}; - -CellSet.prototype.isEmpty = function() { - return Object.keys(this.cells).length < 1; -}; - -CellSet.prototype.map = function(f) { - var result = []; - for(var k in this.cells) { - result.push(f(ofKey(k))); - } - return result; -} - -CellSet.prototype.iter = function(f) { - this.map(f); -} - -CellSet.prototype.difference = function(cellSet) { - var newCellSet = new CellSet(); - this.iter(function(v) { - if(!cellSet.contains(v)) { - newCellSet.add(v); - } - }); - return newCellSet; -} - -return { - make: function(definition) {return new CellSet(definition);}, - union: function(sets) {return new CellSet({type: 'union', sets: sets});} -}; - -function gateKeeper(cellSet, grid, queue, originColor) { +function gateKeeper(cellSet, coloring, queue, color) { return function(cell) { - if(isSmaller(zero(), cell) && isSmaller(cell, diagonal(grid.length-1)) + if(isSmaller(zero(), cell) + && isSmaller(cell, Vector.diagonal(size-1)) && !cellSet.contains(cell) - && at(grid, cell) == originColor) { + && at(coloring, cell) == color) { queue.push(cell); } } } + +function row(n) { + return rectangle(Vector.vertical(n), Vector.make(1, size)); +} + +function column(n) { + return rectangle(Vector.horizontal(n), Vector.make(size, 1)); +} diff --git a/js/Geometry/Vector.js b/js/Geometry/Vector.js index cd102c4..0e37832 100644 --- a/js/Geometry/Vector.js +++ b/js/Geometry/Vector.js @@ -1,5 +1,7 @@ return { make: make, + getRow: getRow, + getColumn: getColumn, vertical: vertical, horizontal: horizontal, diagonal: diagonal, @@ -7,12 +9,20 @@ return { plus: plus, opposite: opposite, isSmaller: isSmaller, - key: key, - ofKey: ofKey + toKey: toKey, + ofKey: ofKey, }; function make(row, column) { - return {row: row, column: column}; + return {row: parseInt(row), column: parseInt(column)}; +} + +function getRow(v) { + return v.row; +} + +function getColumn(v) { + return v.column; } function zero() { @@ -32,21 +42,23 @@ function diagonal(length) { } function plus(v0, v1) { - return {row: v0.row + v1.row, column: v0.column + v1.column}; + return make(v0.row + v1.row, v0.column + v1.column); } function opposite(v) { - return {row: -v.row, column: -v.column}; + return make(-v.row, -v.column); } function isSmaller(v0, v1) { return v0.row <= v1.row && v0.column <= v1.column; } -function key(v) { +function toKey(v) { return v.row + ':' + v.column; + //return 8*v.row + v.column; } function ofKey(k) { return make.apply(null, k.split(':')); + //return make.call(null, Math.floor(k / 8), k % 8); } diff --git a/js/Grid.js b/js/Grid.js index 3020dfd..df33609 100644 --- a/js/Grid.js +++ b/js/Grid.js @@ -1,3 +1,4 @@ +import size from Config; import * as CellSet from Geometry.CellSet; import * as Dom from UnitJS.Dom; import {at, generate, iter, square} from Grid.Util; @@ -8,7 +9,6 @@ var grid = { cells: null, colors: null, missing: null, - size: null }; return { @@ -17,9 +17,8 @@ return { init: init, }; -function init(size, eventHandlers) { - grid.size = size; - grid.cells = generate(size, size, function(cell) { +function init(eventHandlers) { + grid.cells = square(size, function(cell) { return Dom.make('td', eventHandlers(cell)); }); for(var row = 0; row < size; row++) { @@ -29,13 +28,9 @@ function init(size, eventHandlers) { } function clear() { - grid.colors = square(grid.size); - grid.missing = CellSet.make( - {type: 'rectangle', origin: zero(), offset: diagonal(grid.size)} - ); - iter(grid.colors, function(cell) { - at(grid.cells, cell).className = ''; - }); + grid.colors = square(size); + grid.missing = CellSet.rectangle(zero(), diagonal(size)); + iter(grid.cells, function(td) {td.className = '';}); } function get() { diff --git a/js/Grid/Color.js b/js/Grid/Color.js index ebbfd4f..ef630f6 100644 --- a/js/Grid/Color.js +++ b/js/Grid/Color.js @@ -12,29 +12,27 @@ return { function colorize(cell, color) { var grid = Grid.get(); - set(grid.colors, cell, color || Toolbox.color()); + set(grid.colors, cell, color ?? Toolbox.color()); at(Grid.get().cells, cell).className = 'color' + at(grid.colors, cell); grid.missing.remove(cell); } function paint(origin) { - var cellSet = CellSet.make( - {type: 'isochrome', grid: Grid.get().colors, origin: origin} - ); - cellSet.iter(colorize); + CellSet.isochrome(Grid.get().colors, origin) + .iter(function(cell) {colorize(cell);}); } -function setColors(grid) { - if(grid != undefined) { - iter(grid, function(cell) { - if(at(grid, cell) != undefined) { - colorize(cell, at(grid, cell)); +function setColors(coloring) { + if(coloring != undefined) { + iter(coloring, function(color, cell) { + if(color != undefined) { + colorize(cell, color); } }); - if(Grid.get().missing.isEmpty()) { - Mode.setEnabled(true, ['play', 'solve']); - } else { + if(Grid.get().missing.size() > 0) { Mode.set('edit'); + } else { + Mode.setEnabled(true, ['play', 'solve']); } } } diff --git a/js/Grid/Util.js b/js/Grid/Util.js index a55b936..6e9bad5 100644 --- a/js/Grid/Util.js +++ b/js/Grid/Util.js @@ -1,7 +1,9 @@ import * as Vector from Geometry.Vector; return { + make: make, at: at, + asAFunction: asAFunction, column: column, generate: generate, iter: iter, @@ -21,18 +23,39 @@ function generate(width, height, f) { return result; } +function make(width, height) { + if(width instanceof Object && width.row != undefined) { + height = width.row; + width = width.column; + } + return Array.from({length: height}).map( + function() {return Array.from({length: width});} + ); +} + function iter(grid, f) { - generate(grid.length > 0 ? grid[0].length : null, grid.length, f); + map(grid, f); +} + +function map(grid, f) { + return grid.map(function(rowArray, row) {return rowArray.map( + function(value, column) {return f(value, Vector.make(row, column), grid);} + )}); } function square(size, value) { - return generate(size, size, function() {return value;}); + var generator = value instanceof Function ? value : function() {return value;}; + return map(make(size, size), function(_, cell) {return generator(cell);}); } function at(grid, vector) { return grid[vector.row][vector.column]; } +function asAFunction(grid) { + return function (vector) {return at(grid, vector);}; +} + function set(grid, vector, value) { return grid[vector.row][vector.column] = value; } diff --git a/js/Main.js b/js/Main.js index 3e268cf..cced0be 100644 --- a/js/Main.js +++ b/js/Main.js @@ -8,17 +8,15 @@ import * as Play from Mode.Play; import * as Solve from Mode.Solve; import * as Edit from Mode.Edit; -var size = 8; - -Toolbox.init(size); +Toolbox.init(); Mode.init({ play: Play, solve: Solve, edit: Edit }); -Grid.init(size, Mode.dispatch); +Grid.init(Mode.dispatch); if(window.location.search.length > 0) { var urlSearchParameters = new URLSearchParams(window.location.search); - Grid.Color.set(Share.decode(size, urlSearchParameters.get('game'))); + Grid.Color.set(Share.decode(urlSearchParameters.get('game'))); } Grid.IO.init(); diff --git a/js/Mode/Edit.js b/js/Mode/Edit.js index f3493da..37c2cc3 100644 --- a/js/Mode/Edit.js +++ b/js/Mode/Edit.js @@ -23,7 +23,7 @@ return { function onEnter() { GUI.activate(true, [Grid.get().root, Toolbox.get(), save]); - if(!Grid.get().missing.isEmpty()) { + if(Grid.get().missing.size() > 0) { Mode.setEnabled(false, ['play', 'solve']); } else { Share.link(Grid.get().colors); @@ -65,7 +65,7 @@ function colorCell(cell) { } function checkCompleteness() { - if(Grid.get().missing.isEmpty()) { + if(Grid.get().missing.size() < 1) { Mode.setEnabled(true, ['play', 'solve']); Share.link(Grid.get().colors); } diff --git a/js/Mode/Play.js b/js/Mode/Play.js index 7a4a7c3..c8afd88 100644 --- a/js/Mode/Play.js +++ b/js/Mode/Play.js @@ -9,7 +9,7 @@ return { }; function onClick(e, cell) { - if(Grid.get().missing.isEmpty()) { + if(Grid.get().missing.size() < 1) { rotateState(at(Grid.get().cells, cell)); } } diff --git a/js/Mode/Solve.js b/js/Mode/Solve.js index 1ea9ee0..0084fb9 100644 --- a/js/Mode/Solve.js +++ b/js/Mode/Solve.js @@ -8,5 +8,5 @@ return { }; function onEnter() { - console.log(Solver.step(Grid.get().colors)); + console.log(Solver.solve(Grid.get().colors)); } diff --git a/js/Set.js b/js/Set.js index fa6d264..469b679 100644 --- a/js/Set.js +++ b/js/Set.js @@ -1,12 +1,14 @@ return { - make: make + make: make, + Int: make({toKey: id, ofKey: parseInt}) }; +function id(x) { + return x; +} + function make(type) { - if(type == undefined) { - var id = function(x) {return x;}; - type = {toKey: id, ofKey: id}; - } + type = type ?? {toKey: id, ofKey: id}; function Set(s) { this.elements = {}; @@ -65,5 +67,9 @@ function make(type) { return newSet; } + Set.uniq = function(t) { + return (new Set(t)).toList(); + } + return Set; } diff --git a/js/Share.js b/js/Share.js index 9931331..db3692e 100644 --- a/js/Share.js +++ b/js/Share.js @@ -1,6 +1,8 @@ +import size from Config; import {at, generate, iter, square} from Grid.Util; import * as Decode from Share.Decoder.Protocol; import * as Encode from Share.Encoder.Protocol; +import * as Encoder from Share.Encoder.Class; import GUI; import Grid; @@ -16,23 +18,24 @@ function get() { return share; } -function naiveEncode(grid) { +function naiveEncode(coloring) { var encoder = Encoder.make(); - iter(grid, function(cell) { - encoder.int(3)(at(grid, cell)); + iter(coloring, function(color) { + encoder.int(3)(color); }); return encoder.output(); } -function naiveDecode(size, input) { +function naiveDecode(input) { if(input != undefined) { var decoder = Decoder.make(input); return generate(size, size, function() {return decoder.int(3);}); } } -function link(grid) { +function link() { //share.href = '?game=' + naiveEncode(Grid.get().colors); + console.log(naiveEncode(Grid.get().colors)); share.href = '?game=' + Encode.grid(Grid.get().colors); GUI.activate(true, share); } diff --git a/js/Share/Decoder/Protocol.js b/js/Share/Decoder/Protocol.js index 157a6f5..76a7f07 100644 --- a/js/Share/Decoder/Protocol.js +++ b/js/Share/Decoder/Protocol.js @@ -1,3 +1,4 @@ +import size from Config; import * as CellSet from Geometry.CellSet; import * as Decoder from Share.Decoder.Class; import {diagonal, plus, zero} from Geometry.Vector; @@ -8,12 +9,12 @@ return { grid: decodeGrid }; -function decodeGrid(size, input) { +function decodeGrid(input) { if(input != undefined) { return decoderLoop( Decoder.make(input), square(size), - CellSet.make({type: 'rectangle', origin: zero(), offset: diagonal(size)}) + CellSet.rectangle(zero(), diagonal(size)) ); } } diff --git a/js/Share/Encoder/Class.js b/js/Share/Encoder/Class.js index 253cf1e..71a1988 100644 --- a/js/Share/Encoder/Class.js +++ b/js/Share/Encoder/Class.js @@ -44,7 +44,7 @@ Encoder.prototype.variableLength6 = function(n) { Encoder.prototype.int = function(size) { return function(n) { for(var i = 0; i < size; i++) { - encoder.push(n > 3); + this.push(n > 3); n = (2*n) & 7; } }.bind(this); diff --git a/js/Share/Encoder/Protocol.js b/js/Share/Encoder/Protocol.js index 7c7e540..e91aecd 100644 --- a/js/Share/Encoder/Protocol.js +++ b/js/Share/Encoder/Protocol.js @@ -1,5 +1,8 @@ +import {diagonal, isSmaller, plus} from Geometry.Vector; +import * as Vector from Geometry.Vector; import * as Encoder from Share.Encoder.Class; import {at, iter, square} from Grid.Util; +import * as Grid from Grid.Util; import * as Protocol from Share.Protocol; return { @@ -10,65 +13,55 @@ function encodeGrid(grid) { var encoder = Encoder.make(); var done = square(grid.length, false); var gradients = square(grid.length); - iter(grid, function(cell) { + iter(grid, function(_, cell) { if(!at(done, cell)) { let block = getLongestBlock(grid, cell); - if(block != undefined) { - encodeBlock(encoder, block); + if(block.size >= Protocol.MIN_BLOCK_SIZE) { + encodeBlock(encoder, done, block); } else { encodeSingleCell( encoder, - getColorGradient(gradients, grid, cell) + getColorGradient(gradients, grid, done, cell) ); } } }); + return encoder.output(); } function getLongestBlock(grid, origin) { var color = at(grid, origin); - var longestDirection = findLongestDirection(grid, origin, color); - var size = extend(grid, origin, color, longestDirection); - if(size >= Protocol.MIN_BLOCK_SIZE) { - return { - direction: longestDirection.coordinate == 'row' ? 'vertical' : 'horizontal', - size: size, - color: color - }; - } + var hSize = getStreak(Grid.row(grid, origin.row), origin.column, color); + var vSize = getStreak(Grid.column(grid, origin.column), origin.row, color); + return { + isVertical: vSize > hSize, + size: Math.max(hSize, vSize), + color: color, + origin: origin + }; } -function findLongestDirection(grid, p, originColor) { - var delta = 1; - while(true) { - var newRow = p.row + delta; - var newColumn = p.column + delta; - if(newRow >= grid.length || grid[newRow][p.column] != originColor) { - return {coordinate: 'column', delta: delta}; - } else if(newColumn >= grid[p.row].length || grid[p.row][newColumn] != originColor) { - return {coordinate: 'row', delta: delta+1}; - } - delta++; - } +function getStreak(t, firstIndex, color) { + var fromFirst = t.slice(firstIndex); + var index = fromFirst.findIndex( + function(x) {return x != color;} + ); + return index >= 0 ? index : fromFirst.length; } -function extend(grid, p, originColor, direction) { - var origin = p[direction.coordinate]; - p[direction.coordinate] += direction.delta; - while(isSmaller(p, diagonal(grid.length)) && at(grid, p) == originColor) { - p[direction.coordinate]++; - } - return p[direction.coordinate] - origin; -} - -function encodeBlock(encoder, block) { +function encodeBlock(encoder, done, block) { encoder.push(1); - encoder.push(block.direction == 'vertical'); + encoder.push(block.isVertical); encoder.variableLength6(block.size - Protocol.MIN_BLOCK_SIZE); encoder.int(3)(block.color); + var cell = Vector.make(block.origin.row, block.origin.column); + for(var i = 0; i < block.size; i++) { + Grid.set(done, cell, true); + cell[block.isVertical ? 'row' : 'column']++; + } } -function getColorGradient(gradients, grid, cell) { +function getColorGradient(gradients, grid, done, cell) { if(at(gradients, cell) == undefined) { } diff --git a/js/Solver.js b/js/Solver.js index 5a34888..667ab49 100644 --- a/js/Solver.js +++ b/js/Solver.js @@ -1,114 +1,54 @@ import * as CellSet from Geometry.CellSet; +import set from Grid.Util; +import * as Inclusion from Solver.Inclusion; +import * as SingleCell from Solver.SingleCell; +import * as State from Solver.State; +import * as Strategy from Solver.Strategy; return { - step: step + rate: rate, + solve: solve, + findNextStep: findNextStep }; -function step(grid) { - var zones = getZones(grid); - var lines = getLines(grid.length); - var rowClusters = checkRowsInclusions(grid); - if(rowClusters.length > 0) { - rowClusters.forEach(function(rowCluster) { - rowCluster.toClear = difference( - rowCluster.colors.map(function(color) {return zones[color];}), - rowCluster.rows.map(function(row) {return lines.rows[row];}) - ); - }); +function solve(coloring) { + var solvingState = State.start(coloring); + var stuck = false; + while(!stuck && solvingState.missing.size() > 0) { + Strategy.execute( + findNextStep(solvingState), + applyStep(solvingState), + function() {console.log('Solver is stuck'); stuck = true;} + ); } - return rowClusters; + return solvingState.constellation; } -function getZones(grid) { - var zones = {}; - for(var row = 0; row < grid.length; row++) { - for(var column = 0; column < grid[row].length; column++) { - var color = grid[row][column]; - if(zones[color] == undefined) { - zones[color] = CellSet.make( - {type: 'isochrome', row: row, column: column, grid: grid} - ); +function rate(coloring) { +} + +function findNextStep(solvingState) { + return Strategy.tryEach([ + Inclusion.find(solvingState), + SingleCell.find(solvingState) + ]); +} + +function applyStep(solvingState) { + return function(step) { + console.log(step); + ['empty', 'star'].forEach(function(attribute) { + if(step[attribute] != undefined) { + step[attribute].iter(function(cell) { + set(solvingState.constellation, cell, attribute == 'star'); + forget(solvingState, cell); + }); } - } - } - return zones; -} - -function line(type, size, i) { - if(type == 'row') { - return CellSet.make( - {type: 'rectangle', row: i, column: 0, width: size, height: 1} - ); - } else if(type == 'column') { - return CellSet.make( - {type: 'rectangle', row: 0, column: i, width: 1, height: size} - ); - } -} - -function getLines(size) { - var empty = Array.from({length: size}); - return { - rows: empty.map(function(x, i) {return line('row', size, i);}), - columns: empty.map(function(x, i) {return line('column', size, i);}), + }); }; } -function getColorsByRow(grid) { - var colorsByRow = []; - for(var row = 0; row < grid.length; row++) { - colorsByRow.push( - quotient(grid[row], function(c0, c1) {return c0 == c1;}).map( - function(colorClass) {return colorClass.specimen;} - ) - ); - } - return colorsByRow; -} - -function checkRowsInclusions(grid) { - var colorsByRow = getColorsByRow(grid); - var colorSets = quotient(colorsByRow, sameColorsSet); - return colorSets.reduce(function(commands, colorSet) { - if(colorSet.occurrences.length == colorSet.specimen.length) { - commands.push({ - reason: 'rowsInColors', - rows: colorSet.occurrences, - colors: colorSet.specimen - }); - } - return commands; - }, []); -} - -function quotient(elements, equivalence) { - var classes = []; - elements.forEach(function(element, i) { - for(var c = 0; c < classes.length; c++) { - if(equivalence(element, classes[c].specimen)) { - classes[c].occurrences.push(i); - return; - } - } - classes.push({specimen: element, occurrences: [i]}); - }); - return classes; -} - -function sameColorsSet(s0, s1) { - if(s0.length != s1.length) { - return false; - } - var o0 = {}; - s0.forEach(function(x) {o0[x] = true;}); - for(var i = 0; i < s1.length; i++) { - if(!o0[s1[i]]) { - return false; - } - } - return true; -} - -function difference(setsFrom, setsSubstracted) { - return CellSet.union(setsFrom).difference(CellSet.union(setsSubstracted)); +function forget(solvingState, cell) { + State.getCellSets(solvingState).concat(solvingState.missing) + .forEach(function(cellSet) {cellSet.remove(cell);}); } diff --git a/js/Solver/SingleCell.js b/js/Solver/SingleCell.js index d00c870..12fa09e 100644 --- a/js/Solver/SingleCell.js +++ b/js/Solver/SingleCell.js @@ -10,7 +10,7 @@ return { function find(solvingState) { var cellSets = getCellSets(solvingState); return Strategy.map( - stepOfCell, + stepOfCell(solvingState.missing), Strategy.tryEach(cellSets.map(getSingleCellIn)) ); } @@ -23,11 +23,13 @@ function getSingleCellIn(cellSet) { }; } -function stepOfCell(cell) { - return { - reason: 'singleCell', - empty: toEmpty(cell), - star: new CellSet.CellSet(cell) +function stepOfCell(missing) { + return function(cell) { + return { + reason: 'singleCell', + empty: toEmpty(cell).intersection(missing), + star: new CellSet.CellSet(cell) + }; }; } diff --git a/js/Solver/State.js b/js/Solver/State.js index cc65fd5..0dd641e 100644 --- a/js/Solver/State.js +++ b/js/Solver/State.js @@ -1,3 +1,6 @@ +import size from Config; +import {asAFunction, iter, square} from Grid.Util; +import {diagonal, zero} from Geometry.Vector; import * as CellSet from Geometry.CellSet; return { diff --git a/js/Toolbox.js b/js/Toolbox.js index e24922a..f5dc2de 100644 --- a/js/Toolbox.js +++ b/js/Toolbox.js @@ -1,3 +1,4 @@ +import size from Config; import * as Dom from UnitJS.Dom; var toolbox; @@ -11,7 +12,7 @@ return { tool: tool }; -function init(size, elementId) { +function init(elementId) { toolbox = document.getElementById(elementId || 'toolbox'); colors = toolbox.querySelector('#colors'); for(var i = 0; i < size; i++) {