diff --git a/src/Renderer.ts b/src/Renderer.ts index 6b62cf3..725e90e 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -337,7 +337,10 @@ export default class Renderer { this.renderer.clear(); if (Config.viewMode === "side") { - const data = generateSideViewWorld(Config.worldSize); + const data = generateSideViewWorld( + Config.worldSize, + Config.seedWorld, + ); const initTexture = new THREE.DataTexture( data, Config.worldSize, diff --git a/src/WorldInit.ts b/src/WorldInit.ts index 8cc86c8..e1ce139 100644 --- a/src/WorldInit.ts +++ b/src/WorldInit.ts @@ -1,6 +1,20 @@ -import { MAT_HOME, MAT_SAND } from "./constants"; +import { MAT_FOOD, MAT_HOME, MAT_SAND } from "./constants"; -export function generateSideViewWorld(worldSize: number): Float32Array { +// simple seeded PRNG for deterministic placement +function mulberry32(seed: number) { + return () => { + seed += 0x6d2b79f5; + let t = seed; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +export function generateSideViewWorld( + worldSize: number, + seed: boolean, +): Float32Array { const data = new Float32Array(worldSize * worldSize * 4); const sandHeight = Math.floor(worldSize * 0.6); const surfaceRow = sandHeight - 1; @@ -14,10 +28,23 @@ export function generateSideViewWorld(worldSize: number): Float32Array { } // top 40% stays MAT_AIR (Float32Array is zero-initialized) - // place home on surface near center - const centerX = Math.floor(worldSize / 2); - const homeIdx = (surfaceRow * worldSize + centerX) * 4; - data[homeIdx] = MAT_HOME; + if (seed) { + const rng = mulberry32(Date.now()); + // place home at random surface position (middle 60% of world width) + const margin = Math.floor(worldSize * 0.2); + const homeX = margin + Math.floor(rng() * (worldSize - 2 * margin)); + + const homeIdx = (surfaceRow * worldSize + homeX) * 4; + data[homeIdx] = MAT_HOME; + + // place food at a different random surface position + let foodX = homeX; + while (foodX === homeX) { + foodX = margin + Math.floor(rng() * (worldSize - 2 * margin)); + } + const foodIdx = (surfaceRow * worldSize + foodX) * 4; + data[foodIdx] = MAT_FOOD; + } return data; } diff --git a/src/__tests__/worldInit.test.ts b/src/__tests__/worldInit.test.ts index bc82087..ba102e0 100644 --- a/src/__tests__/worldInit.test.ts +++ b/src/__tests__/worldInit.test.ts @@ -1,87 +1,78 @@ import { describe, expect, test } from "bun:test"; -import { MAT_AIR, MAT_HOME, MAT_SAND } from "../constants"; +import { MAT_AIR, MAT_FOOD, MAT_HOME, MAT_SAND } from "../constants"; import { generateSideViewWorld } from "../WorldInit"; -const SIZE = 64; - describe("generateSideViewWorld", () => { - test("output length is worldSize * worldSize * 4", () => { - const data = generateSideViewWorld(SIZE); - expect(data.length).toBe(SIZE * SIZE * 4); + const worldSize = 64; + + test("bottom 60% is sand", () => { + const data = generateSideViewWorld(worldSize, true); + const sandHeight = Math.floor(worldSize * 0.6); + const midY = Math.floor(sandHeight / 2); + const idx = (midY * worldSize + 10) * 4; + expect(data[idx]).toBe(MAT_SAND); }); - test("bottom 60% of rows have R = MAT_SAND", () => { - const data = generateSideViewWorld(SIZE); - const sandHeight = Math.floor(SIZE * 0.6); - for (let y = 0; y < sandHeight; y++) { - for (let x = 0; x < SIZE; x++) { - const idx = (y * SIZE + x) * 4; - const mat = data[idx]; - // home is allowed on the surface row - if (y === sandHeight - 1) { - expect([MAT_SAND, MAT_HOME]).toContain(mat); - } else { - expect(mat).toBe(MAT_SAND); - } - } - } + test("top 40% is air", () => { + const data = generateSideViewWorld(worldSize, true); + const sandHeight = Math.floor(worldSize * 0.6); + const airY = sandHeight + 5; + const idx = (airY * worldSize + 10) * 4; + expect(data[idx]).toBe(MAT_AIR); }); - test("top 40% of rows have R = MAT_AIR", () => { - const data = generateSideViewWorld(SIZE); - const sandHeight = Math.floor(SIZE * 0.6); - for (let y = sandHeight; y < SIZE; y++) { - for (let x = 0; x < SIZE; x++) { - const idx = (y * SIZE + x) * 4; - expect(data[idx]).toBe(MAT_AIR); - } - } - }); - - test("G, B, A channels are 0 everywhere", () => { - const data = generateSideViewWorld(SIZE); - for (let i = 0; i < SIZE * SIZE; i++) { - expect(data[i * 4 + 1]).toBe(0); // G - expect(data[i * 4 + 2]).toBe(0); // B - expect(data[i * 4 + 3]).toBe(0); // A - } - }); - - test("exactly one cell has R = MAT_HOME on the surface row near center X", () => { - const data = generateSideViewWorld(SIZE); - const surfaceRow = Math.floor(SIZE * 0.6) - 1; - const centerX = Math.floor(SIZE / 2); - const tolerance = Math.floor(SIZE * 0.1); - + test("seed=true places exactly one home and one food on surface", () => { + const data = generateSideViewWorld(worldSize, true); let homeCount = 0; - for (let i = 0; i < SIZE * SIZE; i++) { - if (data[i * 4] === MAT_HOME) { - homeCount++; - } + let foodCount = 0; + for (let i = 0; i < data.length; i += 4) { + if (data[i] === MAT_HOME) homeCount++; + if (data[i] === MAT_FOOD) foodCount++; } - expect(homeCount).toBe(1); - - // verify it's on the surface row — find the row - for (let x = 0; x < SIZE; x++) { - const idx = (surfaceRow * SIZE + x) * 4; - if (data[idx] === MAT_HOME) { - expect(Math.abs(x - centerX)).toBeLessThanOrEqual(tolerance); - return; - } - } - // if we reach here, home wasn't on the surface row - throw new Error("home not on surface row"); + expect(foodCount).toBe(1); }); - test("no food placed on the surface row", () => { - const data = generateSideViewWorld(SIZE); - const surfaceRow = Math.floor(SIZE * 0.6) - 1; - - for (let x = 0; x < SIZE; x++) { - const idx = (surfaceRow * SIZE + x) * 4; - const mat = data[idx]; - expect(mat === MAT_SAND || mat === MAT_HOME).toBe(true); + test("seed=true places home and food on the surface row", () => { + const data = generateSideViewWorld(worldSize, true); + const sandHeight = Math.floor(worldSize * 0.6); + const surfaceRow = sandHeight - 1; + let homeY = -1; + let foodY = -1; + for (let y = 0; y < worldSize; y++) { + for (let x = 0; x < worldSize; x++) { + const idx = (y * worldSize + x) * 4; + if (data[idx] === MAT_HOME) homeY = y; + if (data[idx] === MAT_FOOD) foodY = y; + } } + expect(homeY).toBe(surfaceRow); + expect(foodY).toBe(surfaceRow); + }); + + test("seed=false places no home or food", () => { + const data = generateSideViewWorld(worldSize, false); + let homeCount = 0; + let foodCount = 0; + for (let i = 0; i < data.length; i += 4) { + if (data[i] === MAT_HOME) homeCount++; + if (data[i] === MAT_FOOD) foodCount++; + } + expect(homeCount).toBe(0); + expect(foodCount).toBe(0); + }); + + test("seed=true places home and food at different x positions", () => { + const data = generateSideViewWorld(worldSize, true); + let homeX = -1; + let foodX = -1; + for (let y = 0; y < worldSize; y++) { + for (let x = 0; x < worldSize; x++) { + const idx = (y * worldSize + x) * 4; + if (data[idx] === MAT_HOME) homeX = x; + if (data[idx] === MAT_FOOD) foodX = x; + } + } + expect(homeX).not.toBe(foodX); }); });