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 = `Error: ${error.message}`;
}
}
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 = `Error: ${error.message}`;
}
}
this.loadProgram(this.currentProgramIndex);
}
step() {
if (!this.machine) {
try {
this.compile();
} catch (error) {
this.elements.stateText.innerHTML = `Error: ${error.message}`;
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 = `Error: ${error.message}`;
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 = `Accept(${status})`;
} else if (this.machine.isRejecting()) {
this.elements.stateText.innerHTML = `Reject(${status})`;
} else {
this.elements.stateText.innerHTML = `Halt(${status})`;
}
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"));
}
}