/** * 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, MEGA_SIZE, MINI_SIZE, MEGA_MASS, MINI_MASS, MASS_FLOOR, 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 (no purple/pink → base only) const m = computeModifiers(cov({ red: 0.5, blue: 0.5 })) near(m.massMul, 1.8, 'full coverage massMul 1.8') } // ---- purple MEGA (grow + heavy) ------------------------------------------ { // below 20% purple → no size change, no extra mass const m = computeModifiers(cov({ purple: 0.19 })) near(m.size, 1, 'purple 19% → size unchanged') near(m.massMul, 1 + 0.8 * 0.19, 'purple 19% → base mass only') } { // purple super (70%) → size maxed to 1.45, MEGA mass added, glow const m = computeModifiers(cov({ purple: 0.7 })) near(m.size, 1 + MEGA_SIZE, 'purple 70% → size 1.45') near(m.massMul, 1 + 0.8 * 0.7 + MEGA_MASS, 'purple 70% → base + full MEGA mass') near(m.glow, 1, 'purple 70% is super (glow)') ok(m.massMul > 1.25, 'MEGA is heavy enough to trip a *1.25 plate') } { // purple mid (45%) → partial grow via buffStrength const s = 0.15 + 0.85 * 0.5 const m = computeModifiers(cov({ purple: 0.45 })) near(m.size, 1 + MEGA_SIZE * s, 'purple 45% → partial grow') } // ---- pink MINI (shrink + light) ------------------------------------------ { // below 20% pink → no size change const m = computeModifiers(cov({ pink: 0.19 })) near(m.size, 1, 'pink 19% → size unchanged') } { // pink super (70%) → size shrunk to 0.62, lighter than clean const m = computeModifiers(cov({ pink: 0.7 })) near(m.size, 1 - MINI_SIZE, 'pink 70% → size 0.62') ok(m.size < 1, 'pink shrinks the blob') near(m.massMul, 1 + 0.8 * 0.7 - MINI_MASS, 'pink 70% → base mass minus full MINI reduction') ok(m.massMul < 1, 'MINI is lighter than a clean blob at high %') } { // pink can never drive mass to/below zero (MASS_FLOOR guards it) const m = computeModifiers(cov({ pink: 1 })) ok(m.massMul >= MASS_FLOOR, 'pink mass floored at MASS_FLOOR') } // ---- purple vs pink fight ------------------------------------------------- { // equal purple + pink strength cancel → net normal size const m = computeModifiers(cov({ purple: 0.5, pink: 0.5 })) near(m.size, 1, 'equal purple+pink → size nets to 1 (they fight)') } { // more purple than pink → still grows, but less than pure purple const strong = computeModifiers(cov({ purple: 0.6 })) const fought = computeModifiers(cov({ purple: 0.6, pink: 0.3 })) ok(fought.size > 1, 'purple-dominant mix still grows') ok(fought.size < strong.size, 'pink drags the grow down') } console.log(`coverage-math.test: ${passed} assertions passed ✓`)