From 671bfdcb5f3114f6ca3c06a8b6aa72b5ee40e2a9 Mon Sep 17 00:00:00 2001 From: Tissevert Date: Wed, 10 Aug 2022 21:12:39 +0200 Subject: [PATCH] Add geometry tooling --- js/CellSet.js | 101 ---------------------- js/Geometry/CellSet.js | 99 +++++++++++++++++++++ js/Geometry/Vector.js | 52 +++++++++++ js/Grid.js | 36 +++----- js/Grid/Color.js | 26 +++--- js/Grid/IO.js | 2 +- js/Grid/Util.js | 28 +++++- js/Main.js | 2 +- js/Mode.js | 4 +- js/Mode/Edit.js | 22 ++--- js/Mode/Play.js | 5 +- js/Share.js | 15 ++-- js/Share/{Decoder.js => Decoder/Class.js} | 0 js/Share/Decoder/Protocol.js | 79 +++++++++++++++++ js/Share/{Encoder.js => Encoder/Class.js} | 3 +- js/Share/Encoder/Protocol.js | 81 +++++++++++++++++ js/Share/Protocol.js | 4 + js/Solver.js | 2 +- 18 files changed, 394 insertions(+), 167 deletions(-) delete mode 100644 js/CellSet.js create mode 100644 js/Geometry/CellSet.js create mode 100644 js/Geometry/Vector.js rename js/Share/{Decoder.js => Decoder/Class.js} (100%) create mode 100644 js/Share/Decoder/Protocol.js rename js/Share/{Encoder.js => Encoder/Class.js} (97%) create mode 100644 js/Share/Encoder/Protocol.js create mode 100644 js/Share/Protocol.js diff --git a/js/CellSet.js b/js/CellSet.js deleted file mode 100644 index b858f25..0000000 --- a/js/CellSet.js +++ /dev/null @@ -1,101 +0,0 @@ -function CellSet(definition) { - definition = definition || {}; - this.cells = {}; - if(definition.type == 'rectangle') { - this.fromRectangle(definition); - } else if(definition.type == 'isochrome') { - this.fromIsochrome(definition); - } else if(definition.type == 'union') { - this.fromUnion(definition.sets); - } -} - -CellSet.prototype.fromRectangle = function(definition) { - var iMax = definition.row + definition.height; - var jMax = definition.column + definition.width; - for(var i = definition.row; i < iMax; i++) { - for(var j = definition.column; j < jMax; j++) { - this.add(i, j); - } - } -}; - -CellSet.prototype.fromIsochrome = function(definition) { - var originColor = definition.grid[definition.row][definition.column]; - var queue = [{i: definition.row, j: definition.column}]; - while(queue.length > 0) { - var p = queue[0]; - this.add(p.i, p.j); - for(var d = -1; d < 2; d += 2) { - [{i: p.i + d, j: p.j}, {i: p.i, j: p.j + d}].forEach( - gateKeeper(this, definition.grid, queue, originColor) - ); - } - queue.shift(); - } -}; - -CellSet.prototype.fromUnion = function(sets) { - for(var i = 0; i < sets.length; i++) { - for(var key in sets[i].cells) { - this.cells[key] = true; - } - } -} - -CellSet.prototype.add = function(i, j) { - this.cells[id(i, j)] = true; -}; - -CellSet.prototype.remove = function(i, j) { - delete this.cells[id(i, j)]; -}; - -CellSet.prototype.contains = function(i, j) { - return !!this.cells[id(i, j)]; -}; - -CellSet.prototype.isEmpty = function() { - return Object.keys(this.cells).length < 1; -}; - -CellSet.prototype.map = function(f) { - var result = []; - for(var key in this.cells) { - result.push(f.apply(null, key.split(':'))); - } - return result; -} - -CellSet.prototype.iter = function(f) { - this.map(f); -} - -CellSet.prototype.difference = function(cellSet) { - var newCellSet = new CellSet(); - this.iter(function(i, j) { - if(!cellSet.contains(i, j)) { - newCellSet.add(i, j); - } - }); - return newCellSet; -} - -return { - make: function(definition) {return new CellSet(definition);}, - union: function(sets) {return new CellSet({type: 'union', sets: sets});} -}; - -function id(i, j) { - return i + ':' + j; -} - -function gateKeeper(cellSet, grid, queue, originColor) { - return function(p1) { - if(p1.i >= 0 && p1.i < grid.length && p1.j >= 0 && p1.j < grid.length - && !cellSet.contains(p1.i, p1.j) - && grid[p1.i][p1.j] == originColor) { - queue.push(p1); - } - } -} diff --git a/js/Geometry/CellSet.js b/js/Geometry/CellSet.js new file mode 100644 index 0000000..037529f --- /dev/null +++ b/js/Geometry/CellSet.js @@ -0,0 +1,99 @@ +import {diagonal, isSmaller, key, plus, ofKey, zero} from Geometry.Vector; +import {at, generate} from Grid.Util; + +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); + } +} + +CellSet.prototype.fromRectangle = function(origin, maxOffset) { + generate( + maxOffset.row, + maxOffset.column, + function(offset) { + this.add(plus(origin, offset)); + }.bind(this) + ); +}; + +CellSet.prototype.fromIsochrome = function(grid, origin) { + var originColor = at(grid, origin); + var queue = [origin]; + while(queue.length > 0) { + var cell = queue.shift(); + this.add(cell); + for(var d = -1; d < 2; d += 2) { + [plus(cell, vertical(d)), plus(cell, horizontal(d))].forEach( + gateKeeper(this, grid, queue, originColor) + ); + } + } +}; + +CellSet.prototype.fromUnion = function(sets) { + for(var i = 0; i < sets.length; i++) { + for(var k in sets[i].cells) { + this.cells[k] = true; + } + } +} + +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) { + return function(cell) { + if(isSmaller(zero(), cell) && isSmaller(cell, diagonal(grid.length-1)) + && !cellSet.contains(cell) + && at(grid, cell) == originColor) { + queue.push(cell); + } + } +} diff --git a/js/Geometry/Vector.js b/js/Geometry/Vector.js new file mode 100644 index 0000000..cd102c4 --- /dev/null +++ b/js/Geometry/Vector.js @@ -0,0 +1,52 @@ +return { + make: make, + vertical: vertical, + horizontal: horizontal, + diagonal: diagonal, + zero: zero, + plus: plus, + opposite: opposite, + isSmaller: isSmaller, + key: key, + ofKey: ofKey +}; + +function make(row, column) { + return {row: row, column: column}; +} + +function zero() { + return make(0, 0); +} + +function vertical(length) { + return make(length, 0); +} + +function horizontal(length) { + return make(0, length); +} + +function diagonal(length) { + return make(length, length); +} + +function plus(v0, v1) { + return {row: v0.row + v1.row, column: v0.column + v1.column}; +} + +function opposite(v) { + return {row: -v.row, column: -v.column}; +} + +function isSmaller(v0, v1) { + return v0.row <= v1.row && v0.column <= v1.column; +} + +function key(v) { + return v.row + ':' + v.column; +} + +function ofKey(k) { + return make.apply(null, k.split(':')); +} diff --git a/js/Grid.js b/js/Grid.js index fd2f172..3020dfd 100644 --- a/js/Grid.js +++ b/js/Grid.js @@ -1,53 +1,43 @@ -import CellSet; +import * as CellSet from Geometry.CellSet; import * as Dom from UnitJS.Dom; -import {iter, square} from Grid.Util; +import {at, generate, iter, square} from Grid.Util; +import {diagonal, zero} from Geometry.Vector; var grid = { - element: document.getElementById('grid'), + root: document.getElementById('grid'), + cells: null, colors: null, missing: null, size: null }; return { - cell: cell, clear: clear, - init: init, get: get, + init: init, }; function init(size, eventHandlers) { grid.size = size; + grid.cells = generate(size, size, function(cell) { + return Dom.make('td', eventHandlers(cell)); + }); for(var row = 0; row < size; row++) { - grid.element.appendChild( - makeRow({tag: 'td', attributes: eventHandlers, row: row}) - ); + grid.root.appendChild(Dom.make('tr', {}, grid.cells[row])); } clear(); } -function makeRow(config) { - var cells = []; - for(var column = 0; column < grid.size; column++) { - cells.push(Dom.make(config.tag, config.attributes(config.row, column))); - } - return Dom.make('tr', {}, cells); -} - function clear() { grid.colors = square(grid.size); grid.missing = CellSet.make( - {type: 'rectangle', row: 0, column: 0, width: 8, height: 8} + {type: 'rectangle', origin: zero(), offset: diagonal(grid.size)} ); - iter(grid.colors, function(row, column) { - cell(row, column).className = ''; + iter(grid.colors, function(cell) { + at(grid.cells, cell).className = ''; }); } function get() { return grid; } - -function cell(row, column) { - return grid.element.children[row].children[column]; -} diff --git a/js/Grid/Color.js b/js/Grid/Color.js index 6ba799f..ebbfd4f 100644 --- a/js/Grid/Color.js +++ b/js/Grid/Color.js @@ -1,34 +1,34 @@ -import CellSet; +import * as CellSet from Geometry.CellSet; import Grid; -import iter from Grid.Util; +import {at, iter, set} from Grid.Util; import Toolbox; import Mode; return { ize: colorize, paint: paint, - set: set + setColors: setColors }; -function colorize(row, column, color) { +function colorize(cell, color) { var grid = Grid.get(); - grid.colors[row][column] = color || Toolbox.color(); - Grid.cell(row, column).className = 'color' + grid.colors[row][column]; - grid.missing.remove(row, column); + set(grid.colors, cell, color || Toolbox.color()); + at(Grid.get().cells, cell).className = 'color' + at(grid.colors, cell); + grid.missing.remove(cell); } -function paint(row, column) { +function paint(origin) { var cellSet = CellSet.make( - {type: 'isochrome', row: row, column: column, grid: Grid.get().colors} + {type: 'isochrome', grid: Grid.get().colors, origin: origin} ); cellSet.iter(colorize); } -function set(grid) { +function setColors(grid) { if(grid != undefined) { - iter(grid, function(row, column) { - if(grid[row][column] != undefined) { - colorize(row, column, grid[row][column]); + iter(grid, function(cell) { + if(at(grid, cell) != undefined) { + colorize(cell, at(grid, cell)); } }); if(Grid.get().missing.isEmpty()) { diff --git a/js/Grid/IO.js b/js/Grid/IO.js index 4db8fc6..4808d8f 100644 --- a/js/Grid/IO.js +++ b/js/Grid/IO.js @@ -23,7 +23,7 @@ function load() { return File.load(input.files[0]); }, function(data) { - Grid.Color.set(JSON.parse(data)); + Grid.Color.setColors(JSON.parse(data)); return Async.wrap(); } ) diff --git a/js/Grid/Util.js b/js/Grid/Util.js index f0f3cb1..a55b936 100644 --- a/js/Grid/Util.js +++ b/js/Grid/Util.js @@ -1,6 +1,12 @@ +import * as Vector from Geometry.Vector; + return { + at: at, + column: column, generate: generate, iter: iter, + row: row, + set: set, square: square }; @@ -9,7 +15,7 @@ function generate(width, height, f) { for(var row = 0; row < height; row++) { result[row] = [];; for(var column = 0; column < width; column++) { - result[row].push(f(row, column)); + result[row].push(f(Vector.make(row, column))); } } return result; @@ -19,6 +25,22 @@ function iter(grid, f) { generate(grid.length > 0 ? grid[0].length : null, grid.length, f); } -function square(size) { - return generate(size, size, function() {return;}); +function square(size, value) { + return generate(size, size, function() {return value;}); +} + +function at(grid, vector) { + return grid[vector.row][vector.column]; +} + +function set(grid, vector, value) { + return grid[vector.row][vector.column] = value; +} + +function row(grid, n) { + return grid[n]; +} + +function column(grid, n) { + return grid.map(function(row) {return row[n];}); } diff --git a/js/Main.js b/js/Main.js index 37ceb14..3e268cf 100644 --- a/js/Main.js +++ b/js/Main.js @@ -19,6 +19,6 @@ Mode.init({ Grid.init(size, Mode.dispatch); if(window.location.search.length > 0) { var urlSearchParameters = new URLSearchParams(window.location.search); - Grid.Color.set(Share.naiveDecode(size, urlSearchParameters.get('game'))); + Grid.Color.set(Share.decode(size, urlSearchParameters.get('game'))); } Grid.IO.init(); diff --git a/js/Mode.js b/js/Mode.js index a06aa74..04b0bb7 100644 --- a/js/Mode.js +++ b/js/Mode.js @@ -48,10 +48,10 @@ function set(newMode) { } } -function dispatch(row, column) { +function dispatch(cell) { var handler = {}; mouseEvents.forEach(function(eventName) { - handler[eventName] = function(e) {runEvent(eventName, e, row, column);}; + handler[eventName] = function(e) {runEvent(eventName, e, cell);}; }); return handler; } diff --git a/js/Mode/Edit.js b/js/Mode/Edit.js index 0adc0f1..f3493da 100644 --- a/js/Mode/Edit.js +++ b/js/Mode/Edit.js @@ -6,7 +6,7 @@ import Toolbox; import Share; var down = false; -Grid.get().element.addEventListener('mouseleave', function() { +Grid.get().root.addEventListener('mouseleave', function() { down = false; }); var save = document.getElementById('save'); @@ -22,7 +22,7 @@ return { }; function onEnter() { - GUI.activate(true, [Grid.get().element, Toolbox.get(), save]); + GUI.activate(true, [Grid.get().root, Toolbox.get(), save]); if(!Grid.get().missing.isEmpty()) { Mode.setEnabled(false, ['play', 'solve']); } else { @@ -31,36 +31,36 @@ function onEnter() { } function onLeave() { - GUI.activate(false, [Grid.get().element, Toolbox.get(), save, Share.get()]); + GUI.activate(false, [Grid.get().root, Toolbox.get(), save, Share.get()]); } -function onMousedown(e, row, column) { +function onMousedown(e, cell) { if(e.button == GUI.mouse.left) { down = true; if(Toolbox.tool() == 'draw') { - colorCell(row, column); + colorCell(cell); } } } -function onMouseup(e, row, column) { +function onMouseup(e, cell) { if(e.button == GUI.mouse.left) { down = false; if(Toolbox.tool() == 'paint') { - Grid.Color.paint(row, column); + Grid.Color.paint(cell); checkCompleteness(); } } } -function onMouseenter(e, row, column) { +function onMouseenter(e, cell) { if(down && Toolbox.tool() == 'draw') { - colorCell(row, column); + colorCell(cell); } } -function colorCell(row, column) { - Grid.Color.ize(row, column); +function colorCell(cell) { + Grid.Color.ize(cell); checkCompleteness(); } diff --git a/js/Mode/Play.js b/js/Mode/Play.js index 863ecb0..7a4a7c3 100644 --- a/js/Mode/Play.js +++ b/js/Mode/Play.js @@ -1,5 +1,6 @@ import Grid; import GUI; +import at from Grid.Util; return { events: { @@ -7,9 +8,9 @@ return { } }; -function onClick(e, row, column) { +function onClick(e, cell) { if(Grid.get().missing.isEmpty()) { - rotateState(Grid.cell(row, column)); + rotateState(at(Grid.get().cells, cell)); } } diff --git a/js/Share.js b/js/Share.js index 6468723..9931331 100644 --- a/js/Share.js +++ b/js/Share.js @@ -1,6 +1,6 @@ -import * as Decoder from Share.Decoder; -import * as Encoder from Share.Encoder; -import {generate, iter, square} from Grid.Util; +import {at, generate, iter, square} from Grid.Util; +import * as Decode from Share.Decoder.Protocol; +import * as Encode from Share.Encoder.Protocol; import GUI; import Grid; @@ -8,7 +8,7 @@ var share = document.getElementById('share') return { get: get, - naiveDecode: naiveDecode, + decode: Decode.grid, link: link } @@ -18,8 +18,8 @@ function get() { function naiveEncode(grid) { var encoder = Encoder.make(); - iter(grid, function(row, column) { - encoder.int(3)(grid[row][column]); + iter(grid, function(cell) { + encoder.int(3)(at(grid, cell)); }); return encoder.output(); } @@ -32,6 +32,7 @@ function naiveDecode(size, input) { } function link(grid) { - share.href = '?game=' + naiveEncode(Grid.get().colors); + //share.href = '?game=' + naiveEncode(Grid.get().colors); + share.href = '?game=' + Encode.grid(Grid.get().colors); GUI.activate(true, share); } diff --git a/js/Share/Decoder.js b/js/Share/Decoder/Class.js similarity index 100% rename from js/Share/Decoder.js rename to js/Share/Decoder/Class.js diff --git a/js/Share/Decoder/Protocol.js b/js/Share/Decoder/Protocol.js new file mode 100644 index 0000000..157a6f5 --- /dev/null +++ b/js/Share/Decoder/Protocol.js @@ -0,0 +1,79 @@ +import * as CellSet from Geometry.CellSet; +import * as Decoder from Share.Decoder.Class; +import {diagonal, plus, zero} from Geometry.Vector; +import * as Vector from Geometry.Vector; +import * as Protocol from Share.Protocol; + +return { + grid: decodeGrid +}; + +function decodeGrid(size, input) { + if(input != undefined) { + return decoderLoop( + Decoder.make(input), + square(size), + CellSet.make({type: 'rectangle', origin: zero(), offset: diagonal(size)}) + ); + } +} + +function decoderLoop(decoder, grid, missing) { + var queue = []; + var nextBit = decoder.pop(); + var cell = zero(); + var danglingCells = []; + while(nextBit != null) { + if(nextBit == 1) { + fillBlock(grid, decodeBlock(decoder), missing); + } else { + handleCell(grid, cell, decodeDirection(decoder), danglingCells); + } + moveToNext(cell, grid, missing); + nextBit = decoder.pop(); + } + resolve(grid, danglingCells); + return grid; +} + +function moveToNext(cell, grid, missing) { + cell.column++; + while(!missing.contains(cell)) { + if(cell.column >= grid[cell.row].length) { + cell.row++; + cell.column = 0; + } else { + cell.column++; + } + } +} + +function fillBlock(grid, block, missing, cell) { + var offset = Vector[block.direction](1); + var newCell = Vector.make(cell.row, cell.column); + for(var delta = 0; delta < block.size; delta++) { + set(grid, newCell, block.color); + missing.remove(newCell); + newCell = plus(newCell, offset); + } +} + +function decodeBlock(decoder) { + return { + direction: decoder.pop() == 1 ? 'vertical' : 'horizontal', + size: decoder.variableLength6() + Protocol.MIN_BLOCK_SIZE, + color: decoder.int(3) + }; +} + +function handleCell(grid, cell, direction, danglingCells) { + +} + +function resolve(grid, danglingCells) { + +} + +function decodeDirection(decoder) { + return Protocol.directions[decoder.int(2)]; +} diff --git a/js/Share/Encoder.js b/js/Share/Encoder/Class.js similarity index 97% rename from js/Share/Encoder.js rename to js/Share/Encoder/Class.js index ffd3442..253cf1e 100644 --- a/js/Share/Encoder.js +++ b/js/Share/Encoder/Class.js @@ -42,13 +42,12 @@ Encoder.prototype.variableLength6 = function(n) { }; Encoder.prototype.int = function(size) { - var encoder = this; return function(n) { for(var i = 0; i < size; i++) { encoder.push(n > 3); n = (2*n) & 7; } - } + }.bind(this); }; return { diff --git a/js/Share/Encoder/Protocol.js b/js/Share/Encoder/Protocol.js new file mode 100644 index 0000000..7c7e540 --- /dev/null +++ b/js/Share/Encoder/Protocol.js @@ -0,0 +1,81 @@ +import * as Encoder from Share.Encoder.Class; +import {at, iter, square} from Grid.Util; +import * as Protocol from Share.Protocol; + +return { + grid: encodeGrid +}; + +function encodeGrid(grid) { + var encoder = Encoder.make(); + var done = square(grid.length, false); + var gradients = square(grid.length); + iter(grid, function(cell) { + if(!at(done, cell)) { + let block = getLongestBlock(grid, cell); + if(block != undefined) { + encodeBlock(encoder, block); + } else { + encodeSingleCell( + encoder, + getColorGradient(gradients, grid, cell) + ); + } + } + }); +} + +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 + }; + } +} + +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 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) { + encoder.push(1); + encoder.push(block.direction == 'vertical'); + encoder.variableLength6(block.size - Protocol.MIN_BLOCK_SIZE); + encoder.int(3)(block.color); +} + +function getColorGradient(gradients, grid, cell) { + if(at(gradients, cell) == undefined) { + + } + return at(gradients, cell); +} + +function encodeSingleCell(encoder, direction) { + encoder.push(0); + encoder.int(2)(Protocol.directions.indexOf(direction)); +} diff --git a/js/Share/Protocol.js b/js/Share/Protocol.js new file mode 100644 index 0000000..dfb4e54 --- /dev/null +++ b/js/Share/Protocol.js @@ -0,0 +1,4 @@ +return { + directions: ['up', 'right', 'down', 'left'], + MIN_BLOCK_SIZE: 3 +}; diff --git a/js/Solver.js b/js/Solver.js index 7b9e91c..5a34888 100644 --- a/js/Solver.js +++ b/js/Solver.js @@ -1,4 +1,4 @@ -import CellSet; +import * as CellSet from Geometry.CellSet; return { step: step