diff options
Diffstat (limited to 'src/toys/turing/js/ui.js')
| -rw-r--r-- | src/toys/turing/js/ui.js | 386 |
1 files changed, 0 insertions, 386 deletions
diff --git a/src/toys/turing/js/ui.js b/src/toys/turing/js/ui.js deleted file mode 100644 index ae01a4b..0000000 --- a/src/toys/turing/js/ui.js +++ /dev/null @@ -1,386 +0,0 @@ -import { Tape } from "./tape.js"; -import { TuringMachine } from "./machine.js"; -import { parseInstructionSet } from "./parser.js"; -import { EXAMPLE_PROGRAMS } from "./samples.js"; - -const SCROLL_THRESHOLD = 3; - -export class TuringMachineUI { - constructor() { - this.machine = null; - this.editor = null; - this.intervalId = null; - this.isRunning = false; - this.initialTapeSize = 50; - this.blankSymbol = "B"; - this.currentProgramIndex = 0; - this.loadedFromURL = false; - this.urlTapeState = ""; - this.lastScrollPosition = 0; - this.renderedTapeLength = 0; - this.simulationInterval = 200; - - this.elements = { - tape: document.getElementById("tape"), - stateText: document.getElementById("state-text"), - runBtn: document.getElementById("run-btn"), - stepBtn: document.getElementById("step-btn"), - resetBtn: document.getElementById("reset-btn"), - copyBtn: document.getElementById("copy-btn"), - programSelect: document.getElementById("program-select") - }; - - this.init(); - } - - init() { - this.setupEditor(); - this.populateProgramSelect(); - this.setupEventListeners(); - this.loadFromURL(); - } - - setupEditor() { - this.editor = adelieEditor.init("#code-editor", { - language: "javascript" - }); - - this.editor.dom.addEventListener("input", () => { - this.machine = null; - }); - } - - populateProgramSelect() { - if (!this.elements.programSelect) { - return; - } - - this.elements.programSelect.innerHTML = ""; - EXAMPLE_PROGRAMS.forEach((program, index) => { - const option = document.createElement("option"); - option.value = index.toString(); - option.textContent = program.name; - this.elements.programSelect.appendChild(option); - }); - } - - setupEventListeners() { - this.elements.runBtn.addEventListener("click", () => this.toggleRun()); - this.elements.stepBtn.addEventListener("click", () => this.step()); - this.elements.resetBtn.addEventListener("click", () => this.reset()); - this.elements.copyBtn.addEventListener("click", () => this.copyState()); - - this.elements.programSelect.addEventListener("change", (event) => { - this.loadProgram(parseInt(event.target.value, 10)); - }); - - document.addEventListener("keydown", (event) => { - if (event.ctrlKey && event.key === "Enter") { - event.preventDefault(); - this.reset(); - setTimeout(() => this.run(), 0); - } - }); - - this.elements.tape.addEventListener("input", (event) => this.handleTapeInput(event)); - this.elements.tape.addEventListener("focusin", () => { - if (this.isRunning) { - this.pause(); - } - }); - } - - handleTapeInput(event) { - if (!this.machine) { - return; - } - const target = event.target; - if (!(target instanceof HTMLInputElement)) { - return; - } - - const parentCell = target.closest(".cell"); - if (!parentCell) { - return; - } - - const index = Number(parentCell.dataset.index); - if (Number.isNaN(index)) { - return; - } - - const sanitized = (target.value || this.blankSymbol).slice(0, 1); - target.value = sanitized; - this.machine.tape.writeAt(index, sanitized || this.blankSymbol); - } - - loadFromURL() { - const urlParams = new URLSearchParams(window.location.search); - const startState = urlParams.get("start") ?? ""; - const instructions = urlParams.get("instructions"); - - if (!instructions) { - this.loadProgram(0); - return; - } - - try { - const code = atob(instructions); - this.setEditorContent(code); - this.loadedFromURL = true; - this.urlTapeState = startState; - this.compile(this.urlTapeState); - } catch (error) { - console.error("Failed to load from URL", error); - this.loadedFromURL = false; - this.loadProgram(0); - } - } - - setEditorContent(content) { - const length = this.editor.state.doc.toString().length; - this.editor.dispatch({ - changes: { from: 0, to: length, insert: content } - }); - } - - getEditorContent() { - return this.editor.state.doc.toString(); - } - - loadProgram(index = 0) { - const program = EXAMPLE_PROGRAMS[index]; - if (!program) { - return; - } - - this.loadedFromURL = false; - this.currentProgramIndex = index; - this.elements.programSelect.value = index.toString(); - this.setEditorContent(program.code); - - try { - this.compile(program.initialTape); - } catch (error) { - this.elements.stateText.innerHTML = `<span class="text-error">Error: ${error.message}</span>`; - } - } - - compile(initialTape = "") { - this.pause(); - - const code = this.getEditorContent(); - const instructionSet = parseInstructionSet(code); - const tapeSeed = initialTape || EXAMPLE_PROGRAMS[this.currentProgramIndex]?.initialTape || ""; - - const tape = new Tape({ - initialContent: tapeSeed, - blankSymbol: this.blankSymbol, - minLength: Math.max(this.initialTapeSize, tapeSeed.length + 40) - }); - - this.machine = new TuringMachine({ - tape, - rules: instructionSet.rules, - startState: instructionSet.startState, - acceptStates: instructionSet.acceptStates, - rejectStates: instructionSet.rejectStates - }); - - this.renderedTapeLength = 0; - this.lastScrollPosition = tape.getHeadIndex(); - this.updateStateDisplay(); - this.updateTape(true); - } - - reset() { - this.pause(); - - if (this.loadedFromURL) { - try { - this.compile(this.urlTapeState); - return; - } catch (error) { - this.elements.stateText.innerHTML = `<span class="text-error">Error: ${error.message}</span>`; - } - } - - this.loadProgram(this.currentProgramIndex); - } - - step() { - if (!this.machine) { - try { - this.compile(); - } catch (error) { - this.elements.stateText.innerHTML = `<span class="text-error">Error: ${error.message}</span>`; - return; - } - } - - const canContinue = this.machine.step(); - this.updateStateDisplay(); - this.updateTape(); - - if (!canContinue) { - this.pause(); - } - } - - toggleRun() { - if (this.isRunning) { - this.pause(); - } else { - this.run(); - } - } - - run() { - if (this.isRunning) { - return; - } - - if (!this.machine) { - try { - this.compile(); - } catch (error) { - this.elements.stateText.innerHTML = `<span class="text-error">Error: ${error.message}</span>`; - return; - } - } - - this.isRunning = true; - this.elements.runBtn.textContent = "⏸ Pause"; - this.elements.runBtn.classList.remove("primary"); - - this.intervalId = setInterval(() => { - const canContinue = this.machine.step(); - this.updateStateDisplay(); - this.updateTape(); - if (!canContinue) { - this.pause(); - } - }, this.simulationInterval); - } - - pause() { - if (!this.isRunning) { - return; - } - - this.isRunning = false; - clearInterval(this.intervalId); - this.intervalId = null; - - this.elements.runBtn.textContent = "▶ Run (Ctrl + Enter)"; - this.elements.runBtn.classList.add("primary"); - } - - updateStateDisplay() { - if (!this.machine) { - this.elements.stateText.textContent = "State: _, Step: 0"; - return; - } - - const status = this.machine.getStateStatus(); - - if (!this.machine.canStep()) { - if (this.machine.isAccepting()) { - this.elements.stateText.innerHTML = `<span class="text-success">Accept(${status})</span>`; - } else if (this.machine.isRejecting()) { - this.elements.stateText.innerHTML = `<span class="text-error">Reject(${status})</span>`; - } else { - this.elements.stateText.innerHTML = `<span class="text-error">Halt(${status})</span>`; - } - return; - } - - this.elements.stateText.textContent = status; - } - - updateTape(forceRender = false) { - if (!this.machine) { - return; - } - - const tape = this.machine.tape; - if (forceRender || this.renderedTapeLength !== tape.length) { - this.renderTape(); - } - - const cells = this.elements.tape.querySelectorAll(".cell"); - const headIndex = tape.getHeadIndex(); - - cells.forEach((cell, index) => { - cell.dataset.index = index.toString(); - const input = cell.querySelector("input"); - const value = tape.getCell(index); - if (input.value !== value) { - input.value = value; - } - - if (index === headIndex) { - cell.classList.add("active"); - this.maybeScrollIntoView(cell, headIndex, forceRender); - } else { - cell.classList.remove("active"); - } - }); - } - - renderTape() { - const fragment = document.createDocumentFragment(); - const tape = this.machine.tape; - - for (let i = 0; i < tape.length; i++) { - fragment.appendChild(this.createCell(i, tape.getCell(i))); - } - - this.elements.tape.innerHTML = ""; - this.elements.tape.appendChild(fragment); - this.renderedTapeLength = tape.length; - } - - createCell(index, value) { - const cell = document.createElement("div"); - cell.classList.add("cell"); - cell.dataset.index = index.toString(); - - const input = document.createElement("input"); - input.type = "text"; - input.maxLength = 1; - input.value = value; - - cell.appendChild(input); - return cell; - } - - maybeScrollIntoView(cell, headIndex, forceImmediate = false) { - if (forceImmediate) { - cell.scrollIntoView({ behavior: "auto", block: "nearest", inline: "center" }); - this.lastScrollPosition = headIndex; - return; - } - - if (Math.abs(headIndex - this.lastScrollPosition) < SCROLL_THRESHOLD) { - return; - } - - cell.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); - this.lastScrollPosition = headIndex; - } - - copyState() { - if (!this.machine) { - return; - } - - const tapeState = this.machine.tape.getContents(); - const instructions = btoa(this.getEditorContent()); - const url = `${window.location.href.split("?")[0]}?start=${tapeState}&instructions=${instructions}`; - - navigator.clipboard.writeText(url) - .then(() => alert("State copied to clipboard!")) - .catch(() => alert("Failed to copy to clipboard")); - } -} |
