+
+
+
+
+
+
+
+
+
diff --git a/frontend/TrainingCell.js b/frontend/SourceCell.js
similarity index 80%
rename from frontend/TrainingCell.js
rename to frontend/SourceCell.js
index 1ad51d2..2a915e0 100755
--- a/frontend/TrainingCell.js
+++ b/frontend/SourceCell.js
@@ -3,13 +3,17 @@ import { Direction } from "./WfcConstants.js";
/**
* Represents a 3x3 grid of values (sub-cells), that are used as building blocks for procedurally
* generated grids. In reality, only the center value will be included in the outputted WfcGrid;
- * the 8 surrounding colors are there to establish which TrainingCells can live next to each other.
+ * the 8 surrounding colors are there to establish which SourceCells can live next to each other.
*/
-export class TrainingCell {
- /** @param {Uint8Array} values The 9 sub cells that make up this TrainingCell */
- constructor() {
- this.values = new Uint8Array(9);
+export class SourceCell {
+ /** @param {Uint8Array?} values The 9 sub cells that make up this SourceCell */
+ constructor(values) {
+ if (values === undefined) {
+ this.values = new Uint8Array(9);
+ return;
+ }
+ this.values = values;
}
/** @returns {string} The actual value of this Trainin gCell is represented by its center value */
@@ -18,7 +22,18 @@ export class TrainingCell {
}
/**
- * @param {TrainingCell} other
+ * @param {uint8} value
+ * Set the default value of this source cell */
+ set value(value) {
+ this.values[Direction.C] = value;
+ }
+
+ clone() {
+ return new SourceCell(this.values.slice());
+ }
+
+ /**
+ * @param {SourceCell} other
* @param {number} direction
*
* @returns {boolean}
@@ -26,10 +41,9 @@ export class TrainingCell {
potentialNeighbours(other, direction) {
// sadly, we're not allowed to be friends with ourselves.
if (this === other) {
- console.log("WTF were checking to be friends with ourselves!", this, other, direction);
- return false;
+ console.log("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
+ // throw new Error("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
}
-
return (
//
// if they want to live to my east,
@@ -69,7 +83,7 @@ export class TrainingCell {
// my north row must match their middle row
this.values[Direction.NW] === other.values[Direction.W] &&
this.values[Direction.N] === other.values[Direction.C] &&
- this.values[Direction.Direction.NE] === other.values[Direction.E]) ||
+ this.values[Direction.NE] === other.values[Direction.E]) ||
//
// if they want to live to my south,
// their two northern rows must match
@@ -78,7 +92,7 @@ export class TrainingCell {
// my middle row must match their north row
this.values[Direction.W] === other.values[Direction.NW] &&
this.values[Direction.C] === other.values[Direction.N] &&
- this.values[Direction.E] === other.values[Direction.SE] &&
+ this.values[Direction.E] === other.values[Direction.NE] &&
// my south row must match their middle row
this.values[Direction.SW] === other.values[Direction.W] &&
this.values[Direction.S] === other.values[Direction.C] &&
diff --git a/frontend/SourceGrid.js b/frontend/SourceGrid.js
new file mode 100755
index 0000000..73edaf6
--- /dev/null
+++ b/frontend/SourceGrid.js
@@ -0,0 +1,42 @@
+import { SourceCell } from "./SourceCell.js";
+
+export class SourceGrid {
+ /**
+ * @type {SourceCell[]} cells The cells that make up this source grid.
+ */
+ cells;
+
+ /**
+ * @type {number} the width and/or height of the source grid
+ */
+ dim;
+
+ /**
+ * @param {SourceCell[]} cells
+ */
+ constructor(cells) {
+ if (cells[0] === undefined) {
+ throw new Error("cells must be a non empty array");
+ }
+
+ if (!(cells[0] instanceof SourceCell)) {
+ throw new Error("cells arg must be an array of SourceCell, but it isn't");
+ }
+
+ this.cells = cells;
+
+ this.dim = Math.round(Math.sqrt(cells.length));
+
+ if (this.dim ** 2 !== cells.length) {
+ throw new Error("Source grid must be quadratic (height === width), but it isn't");
+ }
+ }
+
+ toString() {
+ return this.cells.map((cell) => cell.value).join(", ");
+ }
+
+ clone() {
+ return new SourceGrid(this.cells.map((sgCell) => sgCell.clone()));
+ }
+}
diff --git a/frontend/TrainingGrid.js b/frontend/TrainingGrid.js
deleted file mode 100644
index 3a9586c..0000000
--- a/frontend/TrainingGrid.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { TrainingCell } from "./TrainingCell.js";
-
-export class TrainingGrid {
- /**
- * @param {TrainingCell[]} cells
- */
- constructor(cells) {
- if (cells[0] === undefined) {
- throw new Error("cells must be a non empty array");
- }
-
- if (!(cells[0] instanceof TrainingCell)) {
- throw new Error("cells arg must be an array of TrainingCell, but it isn't");
- }
-
- /** @type {TrainingCell[]} cells*/
- this.cells = cells;
-
- /** @type {number} the width and/or height of the training grid */
- this.dim = Math.round(Math.sqrt(cells.length));
-
- if (this.dim ** 2 !== cells.length) {
- throw new Error("Training grid must be quadratic (height === width), but it isn't");
- }
- }
-
- clone() {
- return new TrainingGrid(this.cells.slice());
- }
-}
diff --git a/frontend/WfcCell.js b/frontend/WfcCell.js
index d7b7833..e29933e 100755
--- a/frontend/WfcCell.js
+++ b/frontend/WfcCell.js
@@ -1,4 +1,4 @@
-import { TrainingCell } from "./TrainingCell";
+import { SourceCell } from "./SourceCell";
/**
* Represents a single cell in a WfcGrid
@@ -9,7 +9,7 @@ export class WfcCell {
* @param {number} i index in the cell-array of this cell
* @param {number} x x-coordinate of cell
* @param {number} y y-coordinate of cell
- * @param {TrainingCell[]} options - A list of training cells that could potentially live here.
+ * @param {SourceCell[]} options - A list of source cells that could potentially live here.
*/
constructor(i, x, y, options) {
if (!options.length) {
@@ -17,7 +17,7 @@ export class WfcCell {
throw Error("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
}
- if (!(options[0] instanceof TrainingCell)) {
+ if (!(options[0] instanceof SourceCell)) {
console.log("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
throw Error("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
}
@@ -29,22 +29,14 @@ export class WfcCell {
}
getEntropy() {
- const result = this.options.length;
- return result;
+ return this.options.length;
}
get lockedIn() {
- return this.getEntropy() === 1;
- }
-
- get valid() {
- return this.options.length > 0;
+ return this.options.length === 1;
}
get value() {
- if (this.options[0] === undefined) {
- throw new Error("Bad! I do not have any options, and therefore no color");
- }
- return this.options[0].value;
+ return this.options.length === 1 ? this.options[0].value : 0;
}
}
diff --git a/frontend/WfcConstants.js b/frontend/WfcConstants.js
new file mode 100755
index 0000000..65409cb
--- /dev/null
+++ b/frontend/WfcConstants.js
@@ -0,0 +1,21 @@
+export const Direction = Object.freeze({
+ NW: 0,
+ N: 1,
+ NE: 2,
+ E: 3,
+ C: 4,
+ W: 5,
+ SW: 6,
+ S: 7,
+ SE: 8,
+
+ 0: "North west",
+ 1: "North",
+ 2: "North east",
+ 3: "East",
+ 4: "[center]",
+ 5: "West",
+ 6: "South west",
+ 7: "South",
+ 8: "South east",
+});
diff --git a/frontend/WfcGrid.js b/frontend/WfcGrid.js
index b438f7a..e6f1f52 100755
--- a/frontend/WfcGrid.js
+++ b/frontend/WfcGrid.js
@@ -1,5 +1,7 @@
import { Direction } from "./WfcConstants.js";
import { WfcCell } from "./WfcCell.js";
+import { SourceGrid } from "./SourceGrid.js";
+import { Xorshift32 } from "../utils/random.js";
/**
* A WfcGrid represents the output of a Wave Function Collapse operation.
@@ -8,18 +10,26 @@ export class WfcGrid {
/**
* @param {number} w width (in cells)
* @param {number} h height (in cells)
- * @param {TrainingGrid} trainingGrid the training grid that will be the source from which we populate this grid.
+ * @param {SourceGrid} sourceGrid the source grid that will be the source from which we populate this grid.
* @type {Xorshift32} pre-seeded pseudo random number generator
*/
- constructor(w, h, trainingGrid, rng) {
+ constructor(w, h, sourceGrid, rng) {
+ /** @type {number} */
this.width = w;
+ /** @type {number} */
this.height = h;
- this.trainingGrid = trainingGrid;
+ /** @type {SourceGrid} */
+ this.sourceGrid = sourceGrid;
+ /** @type {number[]} */
+ this.lowEntropyCellIdCache = sourceGrid.cells.keys;
+ /** @type {number} */
+ this.lowestEntropy = sourceGrid.dim ** 2;
+ /** @type {Xorshift32} */
this.rng = rng;
- //
- // Populate the cells so each has all available options
- // For now, this means *copying* all TrainingCell options into each cell
+ /** @type {WfcCell[]} */
+ this.cells = [];
+
this.reset();
}
@@ -27,139 +37,165 @@ export class WfcGrid {
console.log("Resetting Cells");
const [w, h] = [this.width, this.height];
const len = w * h;
- this.cells = new Array(len);
+ this.cells = [];
for (let i = 0; i < len; i++) {
const x = i % w;
const y = Math.floor(i / w);
- this.cells[i] = new WfcCell(i, x, y, this.trainingGrid.clone().pixels);
+ this.cells.push(new WfcCell(i, x, y, this.sourceGrid.clone().cells));
}
console.log("Done");
}
/**
* Get the cells that currently have the lowest entropy
- * @returns {number[]}
*/
- cellsIdsWithLowestEntropy() {
- console.log("Finding cells with lowest entopy");
- let result = [];
+ refreshLowEntropyCellIdCache() {
+ this.lowEntropyCellIdCache = [];
// set lowestEntropy to the highest possible entropy,
// and let's search for lower entropy in the cells
- let lowestEntropy = this.trainingGrid.dim ** 2;
+ this.lowestEntropy = this.sourceGrid.dim ** 2;
this.cells.forEach((cell, idx) => {
- console.log("\t checking cell %d (entropy: %d)", idx, cell.getEntropy());
+ const entropy = cell.getEntropy();
+
+ // Cell is locked in, and should not be included
+ if (entropy <= 1) {
+ return;
+ }
+
//
// Have we found cells with low entropy?
- if (cell.getEntropy() < lowestEntropy) {
+ if (entropy < this.lowestEntropy) {
// we've found a cell with lower entropy that the ones we've been looking
// at so far Clear the search results and start over with this cell's
// entropy as our target
- result = [idx];
- lowestEntropy = cell.getEntropy();
+ this.lowEntropyCellIdCache = [idx];
+ this.lowestEntropy = entropy;
return;
}
//
// Cell matches current entropy level, add it to search results.
- if (cell.getEntropy() === lowestEntropy) {
+ if (entropy === this.lowestEntropy) {
// Cell matches our current level of entropy, so we add it to our search results.
// at so far! Clear the results and start over.
- result.push(idx);
+ this.lowEntropyCellIdCache.push(idx);
return;
}
});
- if (result.length <= 0) {
- console.log("Found zero lowest-entropy cells.", { lowestEntropy });
+ if (this.lowEntropyCellIdCache.length === 0) {
+ console.log("Found zero lowest-entropy cells.", { entropy: this.lowestEntropy });
}
-
- return result;
}
- collapse() {
- console.log("Starting collaps()");
- let count = this.cells.length;
- while (count > 0) {
- count--;
- // Get a list of possible target cells
- const lowEntropyCellIds = this.cellIdsWithLowestEntropy();
+ /**
+ * Collapse the grid by one iteration by locking in a random option for the given cell.
+ *
+ * If no cell given, a random cell will be chosen from the cache of lowest-entropy cells.
+ *
+ * @param {number?} The index of the cell that is to be collapsed around
+ */
+ collapse(cellId = undefined) {
+ if (this.lowEntropyCellIdCache.length === 0) {
+ this.refreshLowEntropyCellIdCache();
+ }
- //
- // We've hit a dead end
- // No appropriate target cells found.
- if (lowEntropyCellIds.length === 0) {
- console.log("Found no lowest-entropy cells. This should not happen");
- return count;
- }
-
- const rCellId = this.rng.randomElement(lowEntropyCellIds);
- const rCell = this.cells[rCellId];
-
- /** @type {TrainingCell} a randomly chosen option that was available to rCell */
- const rOption = this.rng.randomElement(rCell.options);
-
- // Lock in the choice for this cell
- rCell.options = [rOption];
-
- // _____ ____ _ _
- // | ____|_ __ _ __ ___ _ __| __ ) ___| | _____ _| |
- // | _| | '__| '__/ _ \| '__| _ \ / _ \ |/ _ \ \ /\ / / |
- // | |___| | | | | (_) | | | |_) | __/ | (_) \ V V /|_|
- // |_____|_| |_| \___/|_| |____/ \___|_|\___/ \_/\_/ (_)
- // Locking in this cell has changed the grid.
- // We must look at the cell's cardinal neighbours and update their options.
- for (let nArr of this.getNeighboursFor(rCell)) {
- /** @type {number} direction of the neighbour */
- const neighbourDirection = nArr[0];
-
- /** @type {WfcCell} the neighbouring cell */
- const neighbourCell = nArr[1];
-
- // Clear the neighbour's options, and
- // repopulate with valid options.
- const newOptions = [];
-
- for (let neighbourOption of neighbourCell.options) {
- if (neighbourOption.potentialNeighbours(rOption, neighbourDirection)) {
- newOptions.push(neighbourOption);
- }
- }
-
- // We've collapsed too deep.
- if (newOptions.length === 0) {
- console.error("We've removed all options from a neighbour!", {
- rCell,
- rOption,
- neighbourCell,
- neighbourDirection,
- newOptions,
- });
- return false;
- }
-
- neighbourCell.options = newOptions;
+ if (cellId === undefined) {
+ cellId = this.rng.randomElement(this.lowEntropyCellIdCache);
+ if (cellId === undefined) {
+ throw new Error("Could not find a valid cell to start the collapse");
}
}
- console.log("Done");
- return 0;
+
+ const targetCell = this.cells[cellId];
+ if (!targetCell) {
+ throw new Error(`Could not find cell with index ${cellId}`);
+ }
+
+ /** @type {SourceCell} a randomly chosen option that was available to targetCell */
+ const targetOption = this.rng.randomElement(targetCell.options);
+
+ // Lock in the choice for this cell
+ targetCell.options = [targetOption];
+
+ // _____ ____ _ _
+ // | ____|_ __ _ __ ___ _ __| __ ) ___| | _____ _| |
+ // | _| | '__| '__/ _ \| '__| _ \ / _ \ |/ _ \ \ /\ / / |
+ // | |___| | | | | (_) | | | |_) | __/ | (_) \ V V /|_|
+ // |_____|_| |_| \___/|_| |____/ \___|_|\___/ \_/\_/ (_)
+ // Locking in this cell has changed the grid.
+ // We must look at the cell's cardinal neighbours and update their options.
+ for (let nArr of this.neighbourCells(targetCell)) {
+ /** @type {number} direction of the neighbour */
+ const neighbourDirection = nArr[0];
+
+ /** @type {WfcCell} the neighbouring cell */
+ const neighbourCell = nArr[1];
+
+ // Clear the neighbour's options, and
+ // repopulate with valid options.
+ const newOptions = [];
+
+ for (let neighbourOption of neighbourCell.options) {
+ if (neighbourOption.potentialNeighbours(targetOption, neighbourDirection)) {
+ newOptions.push(neighbourOption);
+ }
+ }
+
+ const newEntropyLevel = newOptions.length;
+
+ // We've collapsed too deep.
+ if (newOptions.length === 0) {
+ const oldOptions = neighbourCell.options;
+ neighbourCell.options = newOptions;
+ console.error("We've removed all options from a neighbour!", {
+ targetCell,
+ targetOption,
+ neighbourCell,
+ oldOptions,
+ Direction: Direction[neighbourDirection],
+ });
+ return false;
+ }
+
+ neighbourCell.options = newOptions;
+
+ if (newEntropyLevel < this.lowestEntropy) {
+ this.lowestEntropy = newEntropyLevel;
+ this.lowEntropyCellIdCache = [];
+ }
+ if (newEntropyLevel === this.lowestEntropy) {
+ if (!this.lowEntropyCellIdCache.includes(neighbourCell.i)) {
+ this.lowEntropyCellIdCache.push(neighbourCell.i);
+ }
+ }
+ }
+ return true;
}
/**
* Get the neighbours of a cell.
+ * @param {WfcCell} cell
*/
- getNeighboursFor(cell) {
+ neighbourCells(cell) {
const result = [];
+ //
+ // Northern neighbour
+ //
const yNorth = cell.y - 1;
- if (yNorth >= 0) {
+ if (yNorth > 0) {
const xNorth = cell.x;
const idx = this.width * yNorth + xNorth;
result.push([Direction.N, this.cells[idx]]);
}
+ //
+ // Southern neighbour
+ //
const ySouth = cell.y + 1;
if (ySouth < this.height) {
const xSouth = cell.x;
@@ -167,6 +203,9 @@ export class WfcGrid {
result.push([Direction.S, this.cells[idx]]);
}
+ //
+ // Eastern neighbour
+ //
const xEast = cell.x + 1;
if (xEast < this.width) {
const yEast = cell.y;
@@ -174,6 +213,9 @@ export class WfcGrid {
result.push([Direction.E, this.cells[idx]]);
}
+ //
+ // Western neighbour
+ //
const xWest = cell.x - 1;
if (xWest >= 0) {
const yWest = cell.y;
diff --git a/frontend/ascii_dungeon_crawler.html b/frontend/ascii_dungeon_crawler.html
new file mode 100644
index 0000000..1c5cf30
--- /dev/null
+++ b/frontend/ascii_dungeon_crawler.html
@@ -0,0 +1,482 @@
+
+
+
+
+
+ ASCII Dungeon Crawler
+
+
+
+
+
+
+
Use WASD or Arrow Keys to move and arrow keys to turn
+ How it works: Cellular automata uses simple rules applied iteratively. Each cell becomes a wall or floor based on its neighbors.
+ This creates organic, cave-like structures perfect for game maps!
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/client.js b/frontend/client.js
index a618676..266fd82 100755
--- a/frontend/client.js
+++ b/frontend/client.js
@@ -1,7 +1,6 @@
import { crackdown } from "../utils/crackdown.js";
import { parseArgs } from "../utils/parseArgs.js";
import { MessageType } from "../utils/messages.js";
-import { sprintf } from "sprintf-js";
/** Regex to validate if a :help [topic] command i entered correctly */
const helpRegex = /^:help(?:\s+(.*))?$/;
@@ -107,6 +106,7 @@ class MUDClient {
};
this.websocket.onerror = (error) => {
+ console.log("Websocket error", error);
this.updateStatus("Connection Error", "error");
this.writeToOutput("Connection error occurred. Retrying...", { class: "error" });
};
@@ -203,7 +203,7 @@ class MUDClient {
let help = helpRegex.exec(inputText);
if (help) {
console.log("here");
- help[1] ? this.send(MshType.HELP, help[1].trim()) : this.send(MshType.HELP);
+ help[1] ? this.send(MessageType.HELP, help[1].trim()) : this.send(MessageType.HELP);
this.echo(inputText);
return;
}
diff --git a/frontend/dungeon_generator.html b/frontend/dungeon_generator.html
new file mode 100644
index 0000000..7f2f602
--- /dev/null
+++ b/frontend/dungeon_generator.html
@@ -0,0 +1,573 @@
+
+
+
+
+
+ ASCII Dungeon Generator
+
+
+
+