import { describe, expect, it } from "@suaveplan/testing/runner"; import fc from "fast-check"; import { BitSet } from "../src/bitset/bitset.js"; import { defineQuery } from "../src/query/query.js"; import { defineComponent } from "../src/schema/schema.js"; import { createWorld } from "../src/world/world.js"; const MAX_BITS = 128; const MAX_ENTITIES = 32; function bitsetFromBits(size: number, bits: ReadonlyArray): BitSet { const set = new BitSet(size); for (const b of bits) set.set(b); return set; } function bruteContainsAll(a: BitSet, b: BitSet, size: number): boolean { for (let i = 0; i < size; i++) { if (b.has(i) && !a.has(i)) return false; } return true; } function bruteIntersects(a: BitSet, b: BitSet, size: number): boolean { for (let i = 0; i < size; i++) { if (a.has(i) && b.has(i)) return true; } return false; } describe("property invariants", () => { it("bitset algebra matches brute-force on random masks", { timeout: 10000, }, () => { fc.assert( fc.property( fc.integer({ min: 1, max: MAX_BITS }), fc.array(fc.nat(MAX_BITS - 1), { maxLength: 16 }), fc.array(fc.nat(MAX_BITS - 1), { maxLength: 16 }), (rawSize, aBits, bBits) => { const size = Math.max(rawSize, ...aBits, ...bBits, 0) + 1; const a = bitsetFromBits(size, aBits); const b = bitsetFromBits(size, bBits); expect(a.containsAll(b)).toBe(bruteContainsAll(a, b, size)); expect(a.intersects(b)).toBe(b.intersects(a)); expect(a.intersects(b)).toBe(bruteIntersects(a, b, size)); } ), { numRuns: 100 } ); }); it("query result equals brute-force scan on random worlds", { timeout: 30000, }, () => { const Position = defineComponent("Position", { x: 0 }); const Velocity = defineComponent("Velocity", { dx: 0 }); const Disabled = defineComponent("Disabled", { d: true }); const schemas = [Position, Velocity, Disabled] as const; fc.assert( fc.property( fc.array(fc.tuple(fc.boolean(), fc.boolean(), fc.boolean()), { maxLength: MAX_ENTITIES, }), fc.subarray([Position, Velocity, Disabled], { minLength: 1, maxLength: 3, }), fc.subarray([Position, Velocity, Disabled], { minLength: 0, maxLength: 3, }), (entitySpec, include, exclude) => { const world = createWorld(); for (const s of schemas) world.registerComponent(s); const entities: number[] = []; for (const flags of entitySpec) { const entity = world.spawn(); entities.push(entity); for (let i = 0; i < schemas.length; i++) { if (flags[i] === true) { world.addComponent(entity, schemas[i]!); } } } const descriptor = defineQuery( include, exclude.length > 0 ? exclude : undefined ); const queried = [...world.query(descriptor)].sort( (a, b) => a - b ); const expected = entities .filter((e) => { for (const s of include) { if (!world.hasComponent(e, s)) return false; } for (const s of exclude) { if (world.hasComponent(e, s)) return false; } return true; }) .sort((a, b) => a - b); expect(queried).toEqual(expected); } ), { numRuns: 50 } ); }); it("entity count tracks spawn / despawn monotonically", { timeout: 10000, }, () => { fc.assert( fc.property( fc.array(fc.boolean(), { maxLength: MAX_ENTITIES }), (ops) => { const world = createWorld(); const live: number[] = []; for (const op of ops) { if (op || live.length === 0) { live.push(world.spawn()); } else { const last = live.pop(); if (last !== undefined) world.despawn(last); } } expect(world.entityCount).toBe(live.length); } ), { numRuns: 100 } ); }); it("snapshot then restore preserves observable state", { timeout: 15000, }, () => { const Position = defineComponent("Position", { x: 0, y: 0 }); fc.assert( fc.property( fc.array(fc.record({ x: fc.integer(), y: fc.integer() }), { maxLength: MAX_ENTITIES, }), (positions) => { const a = createWorld(); a.registerComponent(Position); for (const p of positions) { const entity = a.spawn(); a.addComponent(entity, Position, p); } const snap = a.snapshot(); const b = createWorld(); b.registerComponent(Position); b.restore(snap); expect(b.entityCount).toBe(a.entityCount); expect(b.archetypeCount).toBe(a.archetypeCount); } ), { numRuns: 30 } ); }); });