Add configurable world seeding with random home and food placement
This commit is contained in:
parent
baadf6cd6d
commit
d4e48af662
3 changed files with 99 additions and 78 deletions
|
|
@ -337,7 +337,10 @@ export default class Renderer {
|
||||||
this.renderer.clear();
|
this.renderer.clear();
|
||||||
|
|
||||||
if (Config.viewMode === "side") {
|
if (Config.viewMode === "side") {
|
||||||
const data = generateSideViewWorld(Config.worldSize);
|
const data = generateSideViewWorld(
|
||||||
|
Config.worldSize,
|
||||||
|
Config.seedWorld,
|
||||||
|
);
|
||||||
const initTexture = new THREE.DataTexture(
|
const initTexture = new THREE.DataTexture(
|
||||||
data,
|
data,
|
||||||
Config.worldSize,
|
Config.worldSize,
|
||||||
|
|
|
||||||
|
|
@ -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 data = new Float32Array(worldSize * worldSize * 4);
|
||||||
const sandHeight = Math.floor(worldSize * 0.6);
|
const sandHeight = Math.floor(worldSize * 0.6);
|
||||||
const surfaceRow = sandHeight - 1;
|
const surfaceRow = sandHeight - 1;
|
||||||
|
|
@ -14,10 +28,23 @@ export function generateSideViewWorld(worldSize: number): Float32Array {
|
||||||
}
|
}
|
||||||
// top 40% stays MAT_AIR (Float32Array is zero-initialized)
|
// top 40% stays MAT_AIR (Float32Array is zero-initialized)
|
||||||
|
|
||||||
// place home on surface near center
|
if (seed) {
|
||||||
const centerX = Math.floor(worldSize / 2);
|
const rng = mulberry32(Date.now());
|
||||||
const homeIdx = (surfaceRow * worldSize + centerX) * 4;
|
// place home at random surface position (middle 60% of world width)
|
||||||
data[homeIdx] = MAT_HOME;
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,87 +1,78 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
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";
|
import { generateSideViewWorld } from "../WorldInit";
|
||||||
|
|
||||||
const SIZE = 64;
|
|
||||||
|
|
||||||
describe("generateSideViewWorld", () => {
|
describe("generateSideViewWorld", () => {
|
||||||
test("output length is worldSize * worldSize * 4", () => {
|
const worldSize = 64;
|
||||||
const data = generateSideViewWorld(SIZE);
|
|
||||||
expect(data.length).toBe(SIZE * SIZE * 4);
|
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", () => {
|
test("top 40% is air", () => {
|
||||||
const data = generateSideViewWorld(SIZE);
|
const data = generateSideViewWorld(worldSize, true);
|
||||||
const sandHeight = Math.floor(SIZE * 0.6);
|
const sandHeight = Math.floor(worldSize * 0.6);
|
||||||
for (let y = 0; y < sandHeight; y++) {
|
const airY = sandHeight + 5;
|
||||||
for (let x = 0; x < SIZE; x++) {
|
const idx = (airY * worldSize + 10) * 4;
|
||||||
const idx = (y * SIZE + x) * 4;
|
expect(data[idx]).toBe(MAT_AIR);
|
||||||
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% of rows have R = MAT_AIR", () => {
|
test("seed=true places exactly one home and one food on surface", () => {
|
||||||
const data = generateSideViewWorld(SIZE);
|
const data = generateSideViewWorld(worldSize, true);
|
||||||
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);
|
|
||||||
|
|
||||||
let homeCount = 0;
|
let homeCount = 0;
|
||||||
for (let i = 0; i < SIZE * SIZE; i++) {
|
let foodCount = 0;
|
||||||
if (data[i * 4] === MAT_HOME) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
homeCount++;
|
if (data[i] === MAT_HOME) homeCount++;
|
||||||
}
|
if (data[i] === MAT_FOOD) foodCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(homeCount).toBe(1);
|
expect(homeCount).toBe(1);
|
||||||
|
expect(foodCount).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");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("no food placed on the surface row", () => {
|
test("seed=true places home and food on the surface row", () => {
|
||||||
const data = generateSideViewWorld(SIZE);
|
const data = generateSideViewWorld(worldSize, true);
|
||||||
const surfaceRow = Math.floor(SIZE * 0.6) - 1;
|
const sandHeight = Math.floor(worldSize * 0.6);
|
||||||
|
const surfaceRow = sandHeight - 1;
|
||||||
for (let x = 0; x < SIZE; x++) {
|
let homeY = -1;
|
||||||
const idx = (surfaceRow * SIZE + x) * 4;
|
let foodY = -1;
|
||||||
const mat = data[idx];
|
for (let y = 0; y < worldSize; y++) {
|
||||||
expect(mat === MAT_SAND || mat === MAT_HOME).toBe(true);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue