// tests/engine/seeding.test.ts
import { describe, expect, it } from 'vitest';
import { AGENT_BYTE_STRIDE, seedAgents } from '~/engine/seeding';
const W = 800;
const H = 600;
const STRIDE = AGENT_BYTE_STRIDE / 4; // 6 float slots
function views(buf: ArrayBuffer, count: number) {
const f = new Float32Array(buf);
const u = new Uint32Array(buf);
const agents = [];
for (let i = 0; i < count; i++) {
agents.push({
x: f[i * STRIDE] ?? 0,
y: f[i * STRIDE + 1] ?? 0,
angle: f[i * STRIDE + 2] ?? 0,
species: u[i * STRIDE + 3] ?? 0,
rhythmPhase: f[i * STRIDE + 4] ?? 0,
});
}
return agents;
}
describe('seedAgents', () => {
it('returns a buffer of 24 bytes per agent', () => {
const buf = seedAgents('random', 1000, [1, 1, 1], W, H);
expect(buf.byteLength).toBe(1000 * 24);
});
it('places every agent inside the field bounds', () => {
const agents = views(seedAgents('random', 2000, [1, 1, 1], W, H), 2000);
for (const a of agents) {
expect(a.x).toBeGreaterThanOrEqual(0);
expect(a.x).toBeLessThanOrEqual(W);
expect(a.y).toBeGreaterThanOrEqual(0);
expect(a.y).toBeLessThanOrEqual(H);
}
});
it('assigns species 0..2 only', () => {
const agents = views(seedAgents('random', 3000, [1, 1, 1], W, H), 3000);
for (const a of agents) expect([0, 1, 2]).toContain(a.species);
});
it('initialises rhythmPhase within 0..2π', () => {
const agents = views(seedAgents('random', 1000, [1, 1, 1], W, H), 1000);
for (const a of agents) {
expect(a.rhythmPhase).toBeGreaterThanOrEqual(0);
expect(a.rhythmPhase).toBeLessThanOrEqual(Math.PI * 2);
}
});
it('distributes species roughly by population share', () => {
const agents = views(seedAgents('random', 10000, [3, 1, 0], W, H), 10000);
const counts: [number, number, number] = [0, 0, 0];
for (const a of agents) counts[a.species] = (counts[a.species] ?? 0) + 1;
expect(counts[0]).toBeGreaterThan(counts[1]); // 75% vs 25%
expect(counts[2]).toBe(0); // share 0 → no agents
});
it('clusters: agents of a species sit near one shared point', () => {
const agents = views(seedAgents('clusters', 6000, [1, 1, 1], W, H), 6000);
for (const s of [0, 1, 2]) {
const mine = agents.filter((a) => a.species === s);
const cx = mine.reduce((acc, a) => acc + a.x, 0) / mine.length;
const cy = mine.reduce((acc, a) => acc + a.y, 0) / mine.length;
const maxDist = Math.max(...mine.map((a) => Math.hypot(a.x - cx, a.y - cy)));
expect(maxDist).toBeLessThan(Math.min(W, H) * 0.3);
}
});
it('center: all agents start near the field centre', () => {
const agents = views(seedAgents('center', 2000, [1, 1, 1], W, H), 2000);
for (const a of agents) {
expect(Math.hypot(a.x - W / 2, a.y - H / 2)).toBeLessThan(Math.min(W, H) * 0.15);
}
});
it('ring: all agents sit on a circle around the centre', () => {
const agents = views(seedAgents('ring', 2000, [1, 1, 1], W, H), 2000);
const r = Math.min(W, H) * 0.35;
for (const a of agents) {
const d = Math.hypot(a.x - W / 2, a.y - H / 2);
expect(Math.abs(d - r)).toBeLessThan(Math.min(W, H) * 0.05);
}
});
it('handles all-zero shares without crashing (falls back to species 0)', () => {
const agents = views(seedAgents('random', 500, [0, 0, 0], W, H), 500);
for (const a of agents) expect(a.species).toBe(0);
});
});