From 4ff096bd1e0e8f111b0f5c5e434a9410aaf1f6ed Mon Sep 17 00:00:00 2001 From: Guillaume Verger Date: Thu, 21 Jul 2022 15:15:40 +0200 Subject: [PATCH 1/3] 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. --- p5js/tiled-model/cell.js | 28 +++++++++- p5js/tiled-model/sketch.js | 107 +++++++++++++++---------------------- p5js/tiled-model/tile.js | 28 ++++++++-- 3 files changed, 94 insertions(+), 69 deletions(-) diff --git a/p5js/tiled-model/cell.js b/p5js/tiled-model/cell.js index 2558188..9a7ebe8 100644 --- a/p5js/tiled-model/cell.js +++ b/p5js/tiled-model/cell.js @@ -1,5 +1,7 @@ class Cell { - constructor(value) { + constructor(i, j, value) { + this.i = i; + this.j = j; // Initialize the cell as not collapsed this.collapsed = false; // Is an array passed in? @@ -13,5 +15,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/p5js/tiled-model/sketch.js b/p5js/tiled-model/sketch.js index 483b73a..9149640 100644 --- a/p5js/tiled-model/sketch.js +++ b/p5js/tiled-model/sketch.js @@ -1,6 +1,5 @@ let tiles = []; const tileImages = []; - let grid = []; const DIM = 25; @@ -70,7 +69,7 @@ function setup() { function startOver() { // Initialize grid with cells 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); } } @@ -78,7 +77,7 @@ function checkValid(arr, valid) { // Remove invalid options from array for (let i = arr.length - 1; i >= 0; i--) { let element = arr[i]; - if (!valid.includes(element)) { + if (!valid.has(element)) { arr.splice(i, 1); } } @@ -92,15 +91,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); } } @@ -134,60 +125,50 @@ function draw() { } cell.options = [pick]; - // Update grid with new options - 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; +} - 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/p5js/tiled-model/tile.js b/p5js/tiled-model/tile.js index f52c987..5db3abd 100644 --- a/p5js/tiled-model/tile.js +++ b/p5js/tiled-model/tile.js @@ -6,10 +6,15 @@ function reverseString(s) { } // Compare if one edge matches the reverse of another -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 for each tile class Tile { constructor(img, edges, i) { @@ -34,24 +39,37 @@ class Tile { if (tile.index == 5 && this.index == 5) continue; // Check if the current tile's bottom edge matches this tile's top edge - if (compareEdge(tile.edges[2], this.edges[0])) { + if (compatibleEdges(tile.edges[DOWN], this.edges[UP])) { this.up.push(i); } // Check if the current tile's left edge matches this tile's right edge - if (compareEdge(tile.edges[3], this.edges[1])) { + if (compatibleEdges(tile.edges[LEFT], this.edges[RIGHT])) { this.right.push(i); } // Check if the current tile's top edge matches this tile's bottom edge - if (compareEdge(tile.edges[0], this.edges[2])) { + if (compatibleEdges(tile.edges[UP], this.edges[DOWN])) { this.down.push(i); } // Check if the current tile's right edge matches this tile's left edge - 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 the tile image and edges rotate(num) { const w = this.img.width; From 10966ca070582d660549d2061472849cc8ba6ce2 Mon Sep 17 00:00:00 2001 From: Guillaume Verger Date: Thu, 21 Jul 2022 23:21:47 +0200 Subject: [PATCH 2/3] Possible to make several updates for 1 draw() --- p5js/tiled-model/sketch.js | 59 ++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/p5js/tiled-model/sketch.js b/p5js/tiled-model/sketch.js index 9149640..77364cf 100644 --- a/p5js/tiled-model/sketch.js +++ b/p5js/tiled-model/sketch.js @@ -3,6 +3,7 @@ const tileImages = []; let grid = []; const DIM = 25; +const NB_UPDATES_PER_TICK=1; function preload() { // Load tile images @@ -95,37 +96,39 @@ function draw() { } } - // Pick cell with least entropy - let gridCopy = grid.slice(); - gridCopy = gridCopy.filter((a) => !a.collapsed); + 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); - 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 = nextGrid(cell); + } } // propagate options from src to dest. If dest is above src, dir == UP. @@ -135,7 +138,7 @@ function propagate(src, dest, dir) { return oldLen != dest.options.length; } -function optimizedNextGrid(pick) { +function nextGrid(pick) { let touched = [posIdx(pick.i, pick.j)]; while (touched.length > 0) { From a5e2d8640f9d64d1d07a3b6363500e44e0d971c6 Mon Sep 17 00:00:00 2001 From: Guillaume Verger Date: Sun, 26 Jan 2025 12:22:50 +0000 Subject: [PATCH 3/3] compute collapsed at init --- p5js/tiled-model/cell.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/p5js/tiled-model/cell.js b/p5js/tiled-model/cell.js index 9a7ebe8..d07b9dd 100644 --- a/p5js/tiled-model/cell.js +++ b/p5js/tiled-model/cell.js @@ -2,8 +2,6 @@ class Cell { constructor(i, j, value) { this.i = i; this.j = j; - // Initialize the cell as not collapsed - this.collapsed = false; // Is an array passed in? if (value instanceof Array) { // Set options to the provided array