summaryrefslogtreecommitdiff
path: root/src/toys/turing/js/ui.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/toys/turing/js/ui.js')
-rw-r--r--src/toys/turing/js/ui.js386
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"));
- }
-}