/** * Unit checks for the pure paint math. No test runner / no deps — run directly: * node --experimental-strip-types src/paint/coverage-math.test.ts * (Node ≥22; on Node ≥23 the flag is unnecessary.) Uses only console + throw so * it also type-checks cleanly under the project's DOM tsconfig and is never * bundled (no demo entry imports it). */ import type { CoverageReport, PaintColor } from '../contracts' import { ACTIVATE, SUPER, buffStrength, computeModifiers, countBuckets, coverageReportFromCounts, colorIndex, } from './coverage-math' let passed = 0 function ok(cond: boolean, msg: string): void { if (!cond) throw new Error('FAIL: ' + msg) passed++ } function near(a: number, b: number, msg: string, eps = 1e-9): void { ok(Math.abs(a - b) <= eps, `${msg} (got ${a}, want ${b})`) } // helper: build a CoverageReport from a partial byColor map function cov(part: Partial>): CoverageReport { const byColor = { red: 0, orange: 0, yellow: 0, green: 0, blue: 0, purple: 0, pink: 0, ...part, } const total = Object.values(byColor).reduce((a, b) => a + b, 0) return { total, byColor } } // ---- colorIndex ---------------------------------------------------------- ok(colorIndex('red') === 1, 'red index is 1') ok(colorIndex('pink') === 7, 'pink index is 7') // ---- countBuckets -------------------------------------------------------- { const mask = new Uint8Array(100) // all base (0) for (let i = 0; i < 30; i++) mask[i] = 1 // red for (let i = 30; i < 50; i++) mask[i] = 4 // green const c = countBuckets(mask, 7) ok(c[0] === 50, 'base count 50') ok(c[1] === 30, 'red count 30') ok(c[4] === 20, 'green count 20') ok(c[7] === 0, 'pink count 0') ok(c.length === 8, 'counts length 8 (base + 7)') } // ---- coverageReportFromCounts -------------------------------------------- { // counts: [base, red, orange, yellow, green, blue, purple, pink] const counts = [50, 30, 0, 0, 20, 0, 0, 0] const r = coverageReportFromCounts(counts, 100) near(r.byColor.red, 0.3, 'red fraction') near(r.byColor.green, 0.2, 'green fraction') near(r.total, 0.5, 'total painted fraction') // empty texture → all zeros, no NaN const z = coverageReportFromCounts([0, 0, 0, 0, 0, 0, 0, 0], 0) near(z.total, 0, 'empty total 0') near(z.byColor.blue, 0, 'empty blue 0') } // ---- buffStrength -------------------------------------------------------- near(buffStrength(0.0), 0, 'strength below threshold is 0') near(buffStrength(0.199), 0, 'strength just below 20% is 0') near(buffStrength(ACTIVATE), 0.15, 'strength at 20% is the floor') near(buffStrength(SUPER), 1, 'strength at 70% is 1') near(buffStrength(0.9), 1, 'strength above 70% pinned at 1') near(buffStrength(0.45), 0.15 + 0.85 * 0.5, 'strength ramps linearly at midpoint') // ---- computeModifiers ---------------------------------------------------- { const clean = computeModifiers(cov({})) near(clean.speedMul, 1, 'clean speedMul 1') near(clean.grip, 0, 'clean grip 0') ok(clean.waterproof === false, 'clean not waterproof') near(clean.massMul, 1, 'clean massMul 1') near(clean.glow, 0, 'clean no glow') near(clean.size, 1, 'clean size 1 (no grow/shrink in MVP)') } { // RED just under activation: no buff yet const m = computeModifiers(cov({ red: 0.19 })) near(m.speedMul, 1, 'red 19% → no speed buff') } { // RED at 45% → speedMul = 1 + 0.6 * buffStrength(0.45) const s = 0.15 + 0.85 * 0.5 const m = computeModifiers(cov({ red: 0.45 })) near(m.speedMul, 1 + 0.6 * s, 'red 45% speedMul') near(m.massMul, 1 + 0.8 * 0.45, 'massMul tracks total 45%') near(m.glow, 0, 'red 45% not super') } { // GREEN super (70%) → grip maxed + glow const m = computeModifiers(cov({ green: 0.7 })) near(m.grip, 1, 'green 70% grip maxed') near(m.glow, 1, 'green 70% is super (glow)') } { // BLUE ≥20% → waterproof const m = computeModifiers(cov({ blue: 0.25 })) ok(m.waterproof === true, 'blue 25% waterproof') } { // massMul clamps at 1.8 when fully painted const m = computeModifiers(cov({ red: 0.5, blue: 0.5 })) near(m.massMul, 1.8, 'full coverage massMul 1.8') } console.log(`coverage-math.test: ${passed} assertions passed ✓`)