Zum Inhalt

tests/engine/seeding.test.ts

// 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);
  });
});