// simulation speed let TICKS_PER_SECOND = 15; let MS_PER_TICK = 1_000 / TICKS_PER_SECOND; // one per page const CANVAS_SELECTOR = '#arena canvas'; const TICK_BUTTON_SELECTOR = '#tick'; const TICK_RATE_SELECTOR = '#tick-rate-select'; const BENCH_BUTTON_SELECTOR = '#bench'; const RUN_BUTTON_SELECTOR = '#run'; const FPS_SELECTOR = '#fps'; // one per bot const SRC_SELECT_SELECTOR = '#src-select'; const COMPILE_BUTTON_SELECTOR = '#compile'; const WORDLIST_SELECTOR = '#wordlist'; const STACK_SELECTOR = '#stack'; const CALLSTACK_SELECTOR = '#callstack'; const VARS_SELECTOR = '#vars'; const SRC_SELECTOR = '#src'; const IP_SELECTOR = '#wordlist .ip'; function selectorForIP(word, offset) { return `#wordlist x-bytecode[x-index='${word}'] x-op[x-index='${offset}']`; } function wordlistElts(wordlist) { return wordlist.map((bc, i) => { const bcElt = document.createElement('x-bytecode'); bcElt.setAttribute('x-index', i); bc.forEach((op, i) => { const opElt = document.createElement('x-op'); opElt.setAttribute('x-index', i); opElt.textContent = op; bcElt.appendChild(opElt); }) return bcElt; }) } function initWordlist() { const sel = selectorForIP(0, 0); document.querySelectorAll(sel).forEach(e => { e.classList.add('ip') }); } function renderStack(vmstack) { document.querySelectorAll(STACK_SELECTOR).forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } vmstack.reverse() .forEach(datum => { const elt = document.createElement('li'); elt.textContent = datum; e.appendChild(elt); return elt; }); }); } function renderCallStack(vmcallstack) { document.querySelectorAll(CALLSTACK_SELECTOR).forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } vmcallstack.reverse() .forEach(datum => { const elt = document.createElement('li'); elt.textContent = `${datum.word}@${datum.offset}`; e.appendChild(elt); return elt; }); }); } function renderVars(vmvars) { document.querySelectorAll(VARS_SELECTOR).forEach(e => { while (e.lastChild) { e.removeChild(e.lastChild); } ['out', 'heading', 'speed', 'doppler'].forEach(name => { const dt = document.createElement('dt'); dt.textContent = name; e.appendChild(dt); const dd = document.createElement('dd'); dd.textContent = vmvars[name]; e.appendChild(dd); }); }); } function clamp(radius, x, y, width, height) { const xx = Math.min(Math.max(radius, x), width-radius); const yy = Math.min(Math.max(radius, y), height-radius); return [xx, yy]; } function renderRobo(ctx, x, y) { ctx.fillStyle = 'rgb(200 0 0)'; ctx.beginPath(); //ctx.arc(Math.floor(x), Math.floor(y), 25, 0, 2 * Math.PI); ctx.arc(x, y, 25, 0, 2 * Math.PI); ctx.fill(); } function renderArena(robos, delta=0) { // interpolation factor for smoother movement independent of tick // rate, but never tween more than 1 tick. const timeScale = Math.min(delta / MS_PER_TICK, 1.0); const canvas = document.querySelector(CANVAS_SELECTOR); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); robos.forEach(robo => { let [x, y] = [robo.x, robo.y]; if (delta > 0) { const [velx, vely] = [ robo.speedx, robo.speedy ].map(x => timeScale * x); [x, y] = clamp(25, robo.x + velx, robo.y + vely, canvas.width, canvas.height); } renderRobo(ctx, x, y); }); } const highRange = new Range(); const highlight = new Highlight(highRange); CSS.highlights.set('exec', highlight); function renderTextHighlight(vm) { const ip = vm.ip; const anno = vm.annos[ip.word][ip.offset]; const src = document.querySelector(SRC_SELECTOR); // this assumes the text node is the first child, maybe it isn't? highRange.setStart(src.childNodes[0], anno.start); highRange.setEnd(src.childNodes[0], anno.end); } function loadForth(taintedPath) { // ascii only + ‘-’, ‘_’, ‘.’, and ‘/’, but no ‘../’ const path = taintedPath .replace(/[^-_A-Za-z./]/g, '') .replace(/\.\.\//g, ''); fetch(`./samples/${path}`) .then(resp => { if (!resp.ok) { throw `http status ${resp.status}` } return resp.text() }) .then(text => { document.querySelector(SRC_SELECTOR).textContent = text; }) .catch(e => { console.error(`couldn't fetch ‘${path}’`, e); }); } async function loaded() { const canvas = document.querySelector(CANVAS_SELECTOR); const roboWorker = new Worker('robo.mjs', { type: 'module' }); const robo = { worker: roboWorker, lastTick: undefined, x: 132, y: 25, heading: 0, speed: 0, speedx: 0, speedy: 0, }; roboWorker.onmessage = e => { const { kind, res, trans } = e.data; switch (kind) { case 'compile': if (res) { let wordlistContainer = document.querySelector(WORDLIST_SELECTOR); const wordlist = wordlistElts(trans.wordlist); wordlist.forEach(elt => wordlistContainer.appendChild(elt)); initWordlist(); renderStack(trans.stack); renderCallStack(trans.callstack); renderVars(trans.vars); renderTextHighlight(trans); renderArena([robo]); } break; case 'tick': const { word, offset } = trans.ip; document.querySelectorAll(IP_SELECTOR).forEach(e => e.classList.remove('ip')); const sel = selectorForIP(word, offset); document.querySelectorAll(sel).forEach(e => { e.classList.add('ip'); }); renderStack(trans.stack); renderCallStack(trans.callstack); renderVars(trans.vars); renderTextHighlight(trans); robo.lastTick = document.timeline.currentTime; const [x, y] = clamp(25, robo.x + robo.speedx, robo.y + robo.speedy, canvas.width, canvas.height); robo.x = x; robo.y = y; renderArena([robo]); robo.heading = trans.vars.heading; robo.speed = trans.vars.speed; const [speedx, speedy] = [ Math.cos(2 * Math.PI * robo.heading / 360), Math.sin(2 * Math.PI * robo.heading / 360) ].map(x => robo.speed * x); robo.speedx = speedx; robo.speedy = speedy; break; default: console.error('invalid message from robo worker', e.data); } }; roboWorker.onerror = e => { console.error('error in roboWorker', e); }; document.querySelectorAll(SRC_SELECT_SELECTOR).forEach(sel => { sel.onchange = _ => loadForth(sel.value); loadForth(sel.value); }); document.querySelector(COMPILE_BUTTON_SELECTOR).onclick = e => { console.debug('compile clicked', e); let wordlistContainer = document.querySelector(WORDLIST_SELECTOR); while (wordlistContainer.lastChild) { console.debug('removing child', wordlistContainer.lastChild) wordlistContainer.removeChild(wordlistContainer.lastChild); } // always add a newline until i decide what to do with the parser. const text = document.querySelector(SRC_SELECTOR).textContent + '\n'; console.debug('compiling', text); roboWorker.postMessage({ kind: 'compile', text }); }; document.querySelector(TICK_BUTTON_SELECTOR).onclick = e => { console.debug('tick clicked', e); roboWorker.postMessage({ kind: 'tick' }); }; document.querySelector(TICK_RATE_SELECTOR).onchange = e => { console.debug('tick rate changed', e, Number(e.target.value)); TICKS_PER_SECOND = e.target.value; MS_PER_TICK = 1_000 / TICKS_PER_SECOND; } document.querySelector(TICK_RATE_SELECTOR).onchange({target: { value: TICKS_PER_SECOND }}); let blinkenRun = false; let lastTime = 0; function renderFrame(t) { if (!blinkenRun) { return; } self.requestAnimationFrame(renderFrame); // we often get called with the same time stamp many times // in a row due to fingerprint-reduction tech. const ms = t - lastTime; if (ms > 0) { lastTime = t; const delta = robo.lastTick ? t - robo.lastTick : 0; renderArena([robo], delta); const fps = document.querySelector(FPS_SELECTOR); fps.textContent = ms > 0 ? Math.floor(1_000 / ms) : '0'; } } function timeout() { if (!blinkenRun) { return } self.setTimeout(timeout, MS_PER_TICK); robo.worker.postMessage({ kind: 'tick' }); } document.querySelector(RUN_BUTTON_SELECTOR).onclick = e => { console.debug('blinken clicked', e); blinkenRun = !blinkenRun; if (blinkenRun) { e.currentTarget.classList.remove('halten'); e.currentTarget.classList.add('blinken'); e.currentTarget.querySelector('title').textContent = 'halten'; setTimeout(timeout); renderFrame(document.timeline.currentTime); } else { e.currentTarget.classList.remove('blinken'); e.currentTarget.classList.add('halten'); e.currentTarget.querySelector('title').textContent = 'blinken'; document.querySelector(FPS_SELECTOR).textContent = 'n/a'; } } } document.addEventListener('DOMContentLoaded', loaded);