add grid and generator

This commit is contained in:
anna 2022-10-26 22:25:28 +02:00
parent 446a052832
commit fb6f58817c
Signed by: fef
GPG key ID: EC22E476DC2D3D84
3 changed files with 171 additions and 12 deletions

View file

@ -1,12 +1,12 @@
import {fetchPalette, Palette} from "./sprite"; import {fetchPalette} from "./sprite";
import {Chunk} from "./chunk";
export class App { export class App {
private canvas: HTMLCanvasElement; private canvas: HTMLCanvasElement;
private readonly context: CanvasRenderingContext2D; private readonly context: CanvasRenderingContext2D;
private lastTime: DOMHighResTimeStamp = 0; private lastTime: DOMHighResTimeStamp = 0;
private isRunning: boolean = false; private isRunning: boolean = false;
private chunk?: Chunk;
private palette?: Palette;
public constructor(canvas: HTMLCanvasElement) { public constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas; this.canvas = canvas;
@ -19,7 +19,7 @@ export class App {
this.updateSize(window.innerWidth, window.innerHeight); this.updateSize(window.innerWidth, window.innerHeight);
window.onresize = () => this.updateSize(window.innerWidth, window.innerHeight); window.onresize = () => this.updateSize(window.innerWidth, window.innerHeight);
fetchPalette("").then(p => this.palette = p); fetchPalette("").then(palette => this.chunk = new Chunk(palette));
} }
public run() { public run() {
@ -33,14 +33,8 @@ export class App {
private draw(time: DOMHighResTimeStamp) { private draw(time: DOMHighResTimeStamp) {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
if (this.palette) { if (this.chunk) {
let x = 0; this.chunk.render(this.context);
for (let s of this.palette.sprites) {
for (let y = 0; y < 4; y++) {
s.draw(this.context, y * 128, x, y);
}
x += 128;
}
} }
this.lastTime = time; this.lastTime = time;
@ -56,4 +50,5 @@ export class App {
} }
const app = new App(document.getElementById("render") as HTMLCanvasElement); const app = new App(document.getElementById("render") as HTMLCanvasElement);
(window as any)._app = app;
app.run(); app.run();

161
src/chunk.ts Normal file
View file

@ -0,0 +1,161 @@
import { IRender } from "./render";
import { Direction, Palette, Sprite } from "./sprite";
/** Either a tile (if collapsed), or a list of possible tiles (if not) */
type MaybeTile = Tile | Tile[];
/** Number of tiles per row and column of a chunk */
const CHUNK_DIM = 16 >>> 0;
export class Chunk implements IRender {
private palette: Palette;
private grid: MaybeTile[] = [];
private complete: boolean = false;
public constructor(palette: Palette) {
this.palette = palette;
// pregenerate an array with all possible tiles
let allTileVariants = [];
for (let s of palette.sprites) {
for (let dir = 0; dir < 4; dir++) {
allTileVariants.push(new Tile(s, dir));
}
}
// initialize the grid with perfect entropy
for (let i = 0; i < CHUNK_DIM ** 2; i++) {
this.grid.push(allTileVariants);
}
// collapse a single tile to generate an initial disturbance in entropy
this.collapseAndUpdate(0, 0);
window.onmousedown = () => this.tick();
}
public render(context: CanvasRenderingContext2D) {
const res = this.palette.resolution;
this.grid.forEach((tile, index) => {
const y = Math.floor(index / CHUNK_DIM);
const x = index % CHUNK_DIM;
if (!Array.isArray(tile)) {
tile.draw(context, x * res, y * res);
}
});
this.tick();
}
private tick() {
if (this.complete) {
return;
}
let lowest: number = Number.MAX_SAFE_INTEGER;
let lowestCoords: number[][] = [];
this.grid.forEach((tile, index) => {
if (Array.isArray(tile)) {
if (tile.length <= lowest) {
const x = index % CHUNK_DIM;
const y = Math.floor(index / CHUNK_DIM);
if (tile.length < lowest) {
lowest = tile.length;
lowestCoords = [[x, y]];
} else {
lowestCoords.push([x, y]);
}
}
}
});
if (lowestCoords.length > 0) {
let coords = pickRandom(lowestCoords);
this.collapseAndUpdate(coords[0], coords[1]);
} else {
this.complete = true;
}
}
/** Collapse a tile and update entropy on surrounding tiles */
private collapseAndUpdate(x: number, y: number) {
const index = y * CHUNK_DIM + x;
const candidates = this.grid[index];
if (!Array.isArray(candidates)) {
return; // tile is already collapsed (or out of bounds)
}
this.grid[index] = pickRandom(candidates);
if (y > 0) {
this.updateEntropy(x, y - 1); // north
}
if (x < CHUNK_DIM - 1) {
this.updateEntropy(x + 1, y); // east
}
if (y < CHUNK_DIM - 1) {
this.updateEntropy(x, y + 1); // south
}
if (x > 0) {
this.updateEntropy(x - 1, y); // west
}
}
/** Remove all candidates that are no longer possible */
private updateEntropy(x: number, y: number) {
const i = y * CHUNK_DIM + x;
const candidates = this.grid[i];
if (!Array.isArray(candidates)) {
return; // tile is already collapsed (or out of bounds)
}
const filterTile = (tile: MaybeTile) => Array.isArray(tile) ? null : tile;
const neighs: (Tile | null)[] = [null, null, null, null];
if (y > 0) {
neighs[Direction.NORTH] = filterTile(this.grid[i - CHUNK_DIM]);
}
if (x < CHUNK_DIM - 1) {
neighs[Direction.EAST] = filterTile(this.grid[i + 1]);
}
if (y < CHUNK_DIM - 1) {
neighs[Direction.SOUTH] = filterTile(this.grid[i + CHUNK_DIM]);
}
if (x > 0) {
neighs[Direction.WEST] = filterTile(this.grid[i - 1]);
}
// we have to be careful to create an entirely new array here
// because the constructor inserts a reference to the same array
// of MaybeTiles for every cell, so manipulating the array in-place
// would break everything
this.grid[i] = candidates.filter(c => !neighs.some((n, dir) => n && c.conflicts(dir, n)));
}
}
export class Tile {
private readonly sprite: Sprite;
private readonly orientation: Direction;
public constructor(sprite: Sprite, orientation: Direction) {
this.sprite = sprite;
this.orientation = orientation;
}
public matches(ownEdge: Direction, neighbor: Tile): boolean {
const ownEdgeAbs = (ownEdge + 4 - this.orientation) % 4;
const neighEdge = (ownEdge + 2) % 4;
const neighEdgeAbs = (neighEdge + 4 - neighbor.orientation) % 4;
return this.sprite.matches(ownEdgeAbs, neighbor.sprite, neighEdgeAbs);
}
public conflicts(ownEdge: Direction, neighbor: Tile): boolean {
return !this.matches(ownEdge, neighbor);
}
public draw(context: CanvasRenderingContext2D, x: number, y: number) {
this.sprite.draw(context, x, y, this.orientation);
}
}
function pickRandom<T>(a: T[]): T {
return a[Math.round(Math.random() * (a.length - 1))];
}

3
src/render.ts Normal file
View file

@ -0,0 +1,3 @@
export interface IRender {
render(context: CanvasRenderingContext2D): void;
}