1// Song code evaluated the way the REPL does and played through against a 2// stand-in Jev, for the tests that must prove code runs: every song 3// (allSongs.test.mjs), and the jev reference's examples and the MCP 4// primer's song (reference.test.mjs). Test-only: the browser never imports 5// it (it pulls in upstream's mocked Node runtime). 6import { evalScope } from '@strudel/core'; 7import { evaluate } from '@strudel/transpiler'; 8// the REPL's scope, with audio and drawing mocked (upstream's tune tests) 9import '../../../test/runtime.mjs'; 10import { whyNot } from '../../../worker/src/relay'; 11import { boothStore } from './boothStore.mjs'; 12import { askOpening, choice, createJev, JEV_MODEL, noul, score, walk } from './jevCore.mjs'; 13 14// How far the scheduler reads per tick, in cycles. 15export const STEP = 1 / 4; 16 17// A stand-in Jev: a valid answer to every question, varied by a seeded 18// generator so forms take different paths, and every request checked 19// against the relay's own rules. 20function standIn(requests, now) { 21 let seed = 7; 22 const random = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646; 23 return async (state, questions) => { 24 const body = { model: JEV_MODEL, questions, state }; 25 requests.push({ 26 why: whyNot(body), 27 bytes: new TextEncoder().encode(JSON.stringify(body)).length, 28 cycle: now(), 29 // a form's request, and whether its history says how sections were arranged 30 form: 'mayComeNext' in (state.form ?? {}) && !('decidedThisSection' in state), 31 arranged: (state.history ?? []).some((row) => row.arrangement), 32 }); 33 return Object.fromEntries( 34 Object.entries(questions).map(([name, q]) => { 35 if (q.type === 'choice') { 36 const options = Object.keys(q.criteria); 37 const pick = options[Math.floor(random() * options.length)]; 38 const probabilities = Object.fromEntries(options.map((o) => [o, o === pick ? 0.7 : 0.3 / (options.length - 1)])); 39 return [name, { type: 'choice', choice: pick, confidence: 0.6, probabilities }]; 40 } 41 if (q.type === 'score') return [name, { type: 'score', score: random() * (q.criteria.length - 1), confidence: 0.5 }]; 42 return [name, { type: 'noul', noul: random() }]; 43 }), 44 ); 45 }; 46} 47 48const tick = () => new Promise((resolve) => setImmediate(resolve)); 49 50// Evaluates `code` with a stand-in Jev, asks about its opening as the page 51// does before it starts (useReplContext), then plays it a slice at a time 52// for `maxCycles`, the answers landing between slices. Returns the requests 53// Jev was sent, the tempo the code set, how many onsets it played, the 54// cycle of its last, and the booth's views (each jev()'s decisions). 55export async function playThrough(code, maxCycles) { 56 const requests = []; 57 let t = 0; 58 let cps = null; 59 const jev = createJev(standIn(requests, () => t), { hear: () => null, random: () => 0.5 }); 60 // the sample maps are the site's to serve; nothing plays here 61 await evalScope({ jev, choice, score, noul, walk, samples: async () => {}, setcps: (x) => (cps = x) }); 62 boothStore.beginEval(); 63 const { pattern } = await evaluate(code); 64 boothStore.commitEval(); 65 const views = boothStore.starting(); 66 await askOpening(1000); 67 if (!pattern) throw new Error('the code does not evaluate to a pattern'); 68 let heard = 0; 69 let lastSound = -1; 70 for (; t < maxCycles; t += STEP) { 71 const haps = pattern.queryArc(t, t + STEP).filter((h) => h.hasOnset()); 72 if (haps.length) [heard, lastSound] = [heard + haps.length, t]; 73 await tick(); 74 } 75 return { requests, cps, heard, lastSound, views }; 76}