From 82242e7539c7d9e5570e361afcab9fabd72d3853 Mon Sep 17 00:00:00 2001 From: Guillaume Verger Date: Thu, 21 Jul 2022 15:15:40 +0200 Subject: [PATCH 1/2] Optimize grid updates instead of going through all cells to get their tile options, we keep a cache of options, and only modify cells that might have changed. Idea: We start from the picked cell, and we check its neighbours. For each neighbour (up, right, down, left), if the list of options has changed, we add its own neighbours to the list of cells to check. If it didn't change, we just skip its neighbours. It is even better than going through the old grid because it propagates changes until we come to a stable state. In the old way, we only check cells once, according to the state at t-1, which can miss some option reductions and lead to infeasibility. --- cell.js | 29 ++++++++++++++- sketch.js | 109 ++++++++++++++++++++++-------------------------------- tile.js | 28 +++++++++++--- 3 files changed, 95 insertions(+), 71 deletions(-) diff --git a/cell.js b/cell.js index 28e592f..c6cca2c 100644 --- a/cell.js +++ b/cell.js @@ -1,6 +1,7 @@ class Cell { - constructor(value) { - this.collapsed = false; + constructor(i, j, value) { + this.i = i; + this.j = j; if (value instanceof Array) { this.options = value; } else { @@ -9,5 +10,29 @@ class Cell { this.options[i] = i; } } + this.collapsed = this.options.length == 1; + } + + draw(w, h) { + if (this.collapsed) { + let index = this.options[0]; + image(tiles[index].img, this.i * w, this.j * h, w, h); + } else { + noFill(); + stroke(51); + rect(this.i * w, this.j * h, w, h); + } + } + + validOptions(dir) { + let validOptions = new Set(); + for (let option of this.options) { + let valid = tiles[option].compatibles(dir); + for (let opt of valid) { + validOptions.add(opt); + } + } + + return validOptions; } } diff --git a/sketch.js b/sketch.js index a3696aa..60ae4cd 100644 --- a/sketch.js +++ b/sketch.js @@ -1,6 +1,5 @@ let tiles = []; const tileImages = []; - let grid = []; const DIM = 25; @@ -20,7 +19,7 @@ function preload() { function removeDuplicatedTiles(tiles) { const uniqueTilesMap = {}; for (const tile of tiles) { - const key = tile.edges.join(','); // ex: "ABB,BCB,BBA,AAA" + const key = tile.edges.join(','); // ex: 'ABB,BCB,BBA,AAA' uniqueTilesMap[key] = tile; } return Object.values(uniqueTilesMap); @@ -80,7 +79,7 @@ function setup() { function startOver() { // Create cell for each spot on the grid for (let i = 0; i < DIM * DIM; i++) { - grid[i] = new Cell(tiles.length); + grid[i] = new Cell(i % DIM, floor(i / DIM), tiles.length); } } @@ -92,7 +91,7 @@ function checkValid(arr, valid) { // result in removing UP, DOWN, LEFT let element = arr[i]; // console.log(element, valid.includes(element)); - if (!valid.includes(element)) { + if (!valid.has(element)) { arr.splice(i, 1); } } @@ -111,15 +110,7 @@ function draw() { const h = height / DIM; for (let j = 0; j < DIM; j++) { for (let i = 0; i < DIM; i++) { - let cell = grid[i + j * DIM]; - if (cell.collapsed) { - let index = cell.options[0]; - image(tiles[index].img, i * w, j * h, w, h); - } else { - noFill(); - stroke(51); - rect(i * w, j * h, w, h); - } + grid[posIdx(i, j)].draw(w, h); } } @@ -155,60 +146,50 @@ function draw() { } cell.options = [pick]; - const nextGrid = []; - for (let j = 0; j < DIM; j++) { - for (let i = 0; i < DIM; i++) { - let index = i + j * DIM; - if (grid[index].collapsed) { - nextGrid[index] = grid[index]; - } else { - let options = new Array(tiles.length).fill(0).map((x, i) => i); - // Look up - if (j > 0) { - let up = grid[i + (j - 1) * DIM]; - let validOptions = []; - for (let option of up.options) { - let valid = tiles[option].down; - validOptions = validOptions.concat(valid); - } - checkValid(options, validOptions); - } - // Look right - if (i < DIM - 1) { - let right = grid[i + 1 + j * DIM]; - let validOptions = []; - for (let option of right.options) { - let valid = tiles[option].left; - validOptions = validOptions.concat(valid); - } - checkValid(options, validOptions); - } - // Look down - if (j < DIM - 1) { - let down = grid[i + (j + 1) * DIM]; - let validOptions = []; - for (let option of down.options) { - let valid = tiles[option].up; - validOptions = validOptions.concat(valid); - } - checkValid(options, validOptions); - } - // Look left - if (i > 0) { - let left = grid[i - 1 + j * DIM]; - let validOptions = []; - for (let option of left.options) { - let valid = tiles[option].right; - validOptions = validOptions.concat(valid); - } - checkValid(options, validOptions); - } + grid = optimizedNextGrid(cell); +} + +// propagate options from src to dest. If dest is above src, dir == UP. +function propagate(src, dest, dir) { + let oldLen = dest.options.length; + checkValid(dest.options, src.validOptions(dir)); + return oldLen != dest.options.length; +} - // I could immediately collapse if only one option left? - nextGrid[index] = new Cell(options); +function optimizedNextGrid(pick) { + let touched = [posIdx(pick.i, pick.j)]; + + while (touched.length > 0) { + let cell = grid[touched.pop()]; + + let check = function (i, j, dir) { + const idx = posIdx(i, j); + if (propagate(cell, grid[idx], dir)) { + if (!touched.includes(idx)) { + touched.push(idx); + } } + }; + + if (cell.i > 0) { + check(cell.i - 1, cell.j, LEFT); + } + + if (cell.i < DIM - 1) { + check(cell.i + 1, cell.j, RIGHT); + } + + if (cell.j > 0) { + check(cell.i, cell.j - 1, UP); + } + + if (cell.j < DIM - 1) { + check(cell.i, cell.j + 1, DOWN); } } + return grid; +} - grid = nextGrid; +function posIdx(i, j) { + return i + j * DIM; } diff --git a/tile.js b/tile.js index faac022..a8bd3df 100644 --- a/tile.js +++ b/tile.js @@ -4,10 +4,15 @@ function reverseString(s) { return arr.join(''); } -function compareEdge(a, b) { +function compatibleEdges(a, b) { return a == reverseString(b); } +const UP = 0; +const RIGHT = 1; +const DOWN = 2; +const LEFT = 3; + class Tile { constructor(img, edges, i) { this.img = img; @@ -30,24 +35,37 @@ class Tile { if (tile.index == 5 && this.index == 5) continue; // UP - if (compareEdge(tile.edges[2], this.edges[0])) { + if (compatibleEdges(tile.edges[DOWN], this.edges[UP])) { this.up.push(i); } // RIGHT - if (compareEdge(tile.edges[3], this.edges[1])) { + if (compatibleEdges(tile.edges[LEFT], this.edges[RIGHT])) { this.right.push(i); } // DOWN - if (compareEdge(tile.edges[0], this.edges[2])) { + if (compatibleEdges(tile.edges[UP], this.edges[DOWN])) { this.down.push(i); } // LEFT - if (compareEdge(tile.edges[1], this.edges[3])) { + if (compatibleEdges(tile.edges[RIGHT], this.edges[LEFT])) { this.left.push(i); } } } + compatibles(dir) { + switch (dir) { + case UP: + return this.up; + case RIGHT: + return this.right; + case DOWN: + return this.down; + case LEFT: + return this.left; + } + } + rotate(num) { const w = this.img.width; const h = this.img.height; From 02a127a9ac4d52e348a0e802e0f2edba247d0608 Mon Sep 17 00:00:00 2001 From: Guillaume Verger Date: Thu, 21 Jul 2022 23:21:47 +0200 Subject: [PATCH 2/2] Possible to make several updates for 1 draw() --- sketch.js | 61 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/sketch.js b/sketch.js index 60ae4cd..e6ab7dd 100644 --- a/sketch.js +++ b/sketch.js @@ -3,6 +3,7 @@ const tileImages = []; let grid = []; const DIM = 25; +const NB_UPDATES_PER_TICK=5; function preload() { // const path = 'rail'; @@ -114,39 +115,41 @@ function draw() { } } - // Pick cell with least entropy - let gridCopy = grid.slice(); - gridCopy = gridCopy.filter((a) => !a.collapsed); - // console.table(grid); - // console.table(gridCopy); + for (let idxUpdate = 0; idxUpdate < NB_UPDATES_PER_TICK; idxUpdate++) { + // Pick cell with least entropy + let gridCopy = grid.slice(); + gridCopy = gridCopy.filter((a) => !a.collapsed); + // console.table(grid); + // console.table(gridCopy); - if (gridCopy.length == 0) { - return; - } - gridCopy.sort((a, b) => { - return a.options.length - b.options.length; - }); - - let len = gridCopy[0].options.length; - let stopIndex = 0; - for (let i = 1; i < gridCopy.length; i++) { - if (gridCopy[i].options.length > len) { - stopIndex = i; - break; + if (gridCopy.length == 0) { + return; + } + gridCopy.sort((a, b) => { + return a.options.length - b.options.length; + }); + + let len = gridCopy[0].options.length; + let stopIndex = 0; + for (let i = 1; i < gridCopy.length; i++) { + if (gridCopy[i].options.length > len) { + stopIndex = i; + break; + } } - } - if (stopIndex > 0) gridCopy.splice(stopIndex); - const cell = random(gridCopy); - cell.collapsed = true; - const pick = random(cell.options); - if (pick === undefined) { - startOver(); - return; - } - cell.options = [pick]; + if (stopIndex > 0) gridCopy.splice(stopIndex); + const cell = random(gridCopy); + cell.collapsed = true; + const pick = random(cell.options); + if (pick === undefined) { + startOver(); + return; + } + cell.options = [pick]; - grid = optimizedNextGrid(cell); + grid = optimizedNextGrid(cell); + } } // propagate options from src to dest. If dest is above src, dir == UP.