/** * The code box class */ class CodeBox { constructor(codeBoxID, initialStackID, outputID) { /** * Possible vectors the pointer can move in * @type {Object} */ this.directions = { NORTH: [ 0, -1], EAST: [ 1, 0], SOUTH: [ 0, 1], WEST: [-1, 0], }; /** * The current vector of the pointer * @type {int[]} */ this.curr_direction = this.directions.EAST; /** * The Set of instructions to execute * * Either a 1 or 2-dimensional array * @type {Array|Array[]} */ this.box = []; /** * The farthest right the box goes * @type {int} */ this.maxBoxWidth = 0; /** * The bottom of the box * * @type {int} */ this.maxBoxHeight = 0; /** * The coordinates of the currently executing instruction inside the code box * @type {Object} */ this.pointer = { X: 0, Y: 0, }; /** * Was the instruction last moving in the left direction * * Used by the {@link Fisherman} * @type {boolean} */ this.dirWasLeft = false; /** * Are we currently under the influence of the {@link Fisherman} * @type {boolean} */ this.onTheHook = false; /** * Are we currently processing code box instructions as a string * * 0 when false, otherwise it holds the char code for string delimiter, either * 34 or 39 * * @type {int} */ this.stringMode = 0; /** * The stack for the code box to work with * * @TODO Implement multiple stacks * * @type {Stack} */ this.stack = new Stack(); this.codeBoxDOM = document.getElementById(codeBoxID); if(!this.codeBoxDOM) { throw new Error(`Failed to find textarea with ID: ${codeBoxID}`); } this.outputDOM = document.getElementById(outputID); if(!this.outputDOM) { throw new Error(`Failed to find textarea with ID: ${outputID}`); } this.initialStackDOM = document.getElementById(initialStackID); if(!this.initialStackDOM) { throw new Error(`Failed to find input with ID: ${initialStackID}`); } } /** * Parse the initial code box * * Transforms the textual code box into usable matrix */ ParseCodeBox() { // Reset some field for a clean run this.box = []; this.pointer = {X: 0, Y: 0}; this.curr_direction = this.directions.EAST; this.outputDOM.value = ""; const cbRaw = this.codeBoxDOM.value; const rows = cbRaw.split("\n").filter((r) => r.length); let maxRowLength = 0; for(const row of rows) { const rowSplit = row.split(""); // Store this for later processing while(rowSplit.length > maxRowLength) { maxRowLength = rowSplit.length } this.box.push(rowSplit); } this.EqualizeBoxWidth(maxRowLength); this.maxBoxWidth = maxRowLength - 1; this.maxBoxHeight = this.box.length - 1; let fin = null; try { while(!fin) { fin = this.Swim(); } } catch(e) { console.error(e); } } /** * Make all the rows in the code box the same length * * All rows not long enough will have NOPs added until they're uniform in size. * * @param {int} [rowLength] The longest row in the code box */ EqualizeBoxWidth(rowLength = null) { if(!rowLength) { for(const row of this.box) { if(row.length > rowLength) { rowLength = row.length; } } } for(const row of this.box) { while(row.length < rowLength) { row.push(" "); } } } /** * Print the value to the display * * @TODO Set up an actual display * @param {*} value */ Output(value) { this.outputDOM.value += value; } Execute(instruction) { let output = null; try{ switch(instruction) { // NOP case " ": break; // Numbers case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": case "0": case "a": case "b": case "c": case "d": case "e": case "f": this.stack.Push(parseInt(instruction, 16)); break; // Operators case "+": { const x = this.stack.Pop(); const y = this.stack.Pop(); this.stack.Push(y + x); break; } case "-": { const x = this.stack.Pop(); const y = this.stack.Pop(); this.stack.Push(y - x); break; } case "*": { const x = this.stack.Pop(); const y = this.stack.Pop(); this.stack.Push(y * x); break; } case ",": { const x = this.stack.Pop(); const y = this.stack.Pop(); this.stack.Push(y / x); break; } case "%": { const x = this.stack.Pop(); const y = this.stack.Pop(); this.stack.Push(y % x); break; } case "(": { const x = this.stack.Pop(); const y = this.stack.Pop(); this.stack.push(y < x ? 1 : 0); break; } case ")": { const x = this.stack.Pop(); const y = this.stack.Pop(); this.stack.push(y > x ? 1 : 0); break; } case "=": { const x = this.stack.Pop(); const y = this.stack.Pop(); this.stack.push(y == x ? 1 : 0); break; } // Movement case "^": this.MoveUp(); break; case ">": this.MoveRight(); break; case "v": this.MoveDown(); break; case "<": this.MoveLeft(); break; // Mirrors case "/": this.ReflectForward(); break; case "\\": this.ReflectBack(); break; case "_": this.VerticalMirror(); break; case "|": this.HorizontalMirror(); break; case "#": this.OmniMirror(); break; // Stack manipulation case ":": this.stack.Duplicate(); break; case "~": this.stack.Remove(); break; case "$": this.stack.SwapTwo(); break; case "@": this.stack.SwapThree(); break; case "{": this.stack.ShiftLeft(); break; case "}": this.stack.ShiftRight(); break; case "r": this.stack.Reverse(); break; case "l": this.stack.PushLength(); break; // case "[": // break; // case "]": // break; // case "I": // this.curr_stack++; // break; // case "D": // this.curr_stack--; // break; // Output case "n": output = this.stack.Pop(); break; case "o": output = String.fromCharCode(this.stack.Pop()); break; // End execution case ";": output = true; break; default: throw new Error(); } } catch(e) { console.error(`Something smells fishy!\nInstruction: ${instruction}\nStack: ${JSON.stringify(this.stack.stack)}`); } return output; } Swim() { const instruction = this.box[this.pointer.Y][this.pointer.X]; if(this.stringMode != 0 && instruction != this.stringMode) { this.stack.Push(dec(instruction)); } else { const exeResult = this.Execute(instruction); if(exeResult === true) { return true; } else if(exeResult != null) { this.Output(exeResult); } } this.Move(); } Move() { let newX = this.pointer.X + this.curr_direction[0]; let newY = this.pointer.Y + this.curr_direction[1]; // Keep the X coord in the boxes bounds if(newX < 0) { newX = this.maxBoxWidth; } else if(newX > this.maxBoxWidth) { newX = 0; } // Keep the Y coord in the boxes bounds if(newY < 0) { newY = this.maxBoxHeight; } else if(newY > this.maxBoxHeight) { newY = 0; } this.SetPointer(newX, newY); } /** * Implement C and . */ SetPointer(x, y) { this.pointer = {X: x, Y: y}; } /** * Implement ^ * * Changes the swim direction upward */ MoveUp() { this.curr_direction = this.directions.NORTH; } /** * Implement > * * Changes the swim direction rightward */ MoveRight() { this.curr_direction = this.directions.EAST; this.dirWasLeft = false; } /** * Implement v * * Changes the swim direction downward */ MoveDown() { this.curr_direction = this.directions.SOUTH; } /** * Implement < * * Changes the swim direction leftward */ MoveLeft() { this.curr_direction = this.directions.WEST; this.dirWasLeft = true; } /** * Implement / * * Reflects the swim direction depending on its starting value */ ReflectForward() { if (this.curr_direction == this.directions.NORTH) { this.MoveRight(); } else if (this.curr_direction == this.directions.EAST) { this.MoveUp(); } else if (this.curr_direction == this.directions.SOUTH) { this.MoveLeft(); } else { this.MoveDown(); } } /** * Implement \ * * Reflects the swim direction depending on its starting value */ ReflectBack() { if (this.curr_direction == this.directions.NORTH) { this.MoveLeft(); } else if (this.curr_direction == this.directions.EAST) { this.MoveDown(); } else if (this.curr_direction == this.directions.SOUTH) { this.MoveRight(); } else { this.MoveUp(); } } /** * Implement | * * Swaps the horizontal swim direction to its opposite */ HorizontalMirror() { if (this.curr_direction == this.directions.EAST) { this.MoveLeft(); } else { this.MoveRight(); } } /** * Implement _ * * Swaps the horizontal swim direction to its opposite */ VerticalMirror() { if (this.curr_direction == this.directions.NORTH) { this.MoveDown(); } else { this.MoveUp(); } } /** * Implement # * * A combination of the vertical and the horizontal mirror */ OmniMirror() { if (this.curr_direction[0]) { this.VerticalMirror(); } else { this.HorizontalMirror(); } } /** * Implement x * * Pseudo-randomly switches the swim direction */ ShuffleDirection() { this.curr_direction = Object.values(this.directions)[Math.floor(Math.random() * 4)]; } /** * Implement ` * * Changes the swim direction based on the previous direction * @see https://esolangs.org/wiki/Starfish#Fisherman */ Fisherman() { if (this.curr_direction[0]) { if (this.dirWasLeft) { this.MoveLeft(); } else { this.MoveRight(); } } else { if (this.onTheHook) { this.onTheHook = false; this.MoveUp(); } else { this.onTheHook = true; this.MoveDown(); } } } } /** * The stack class */ class Stack { constructor() { /** * The stack * @type {int[]} */ this.stack = []; /** * A single value saved off the stack * @type {int} */ this.register = null; } /** * Wrapper function for Array.prototype.push * @param {*} newValue */ Push(newValue) { this.stack.push(newValue); } /** * Wrapper function for Array.prototype.pop * @returns {*} */ Pop() { const value = this.stack.pop(); if(value == undefined){ throw new Error(); } return value; } /** * Implement } * * Shifts the entire stack leftward by one value */ ShiftLeft() { const temp = this.stack.shift(); this.stack.push(temp); } /** * Implement { * * Shifts the entire stack rightward by one value */ ShiftRight() { const temp = this.stack.pop(); this.stack.unshift(temp); } /** * Implement $ * * Swaps the top two values of the stack */ SwapTwo() { if(this.stack.length < 2) { throw new Error(); } const popped = this.stack.splice(this.stack.length - 2, 2); this.stack.push(...popped.reverse()); } /** * Implement @ * * Swaps the top three values of the stack */ SwapThree() { if(this.stack.length < 3) { throw new Error(); } // Get the top three values const popped = this.stack.splice(this.stack.length - 3, 3); // Shift the elements to the right popped.unshift(popped.pop()); this.stack.push(...popped); } /** * Implement : * * Duplicates the element on the top of the stack */ Duplicate() { this.stack.push(this.stack[this.stack.length-1]); } /** * Implements ~ * * Removes the element on the top of the stack */ Remove() { this.stack.pop(); } /** * Implement r * * Reverses the entire stack */ Reverse() { this.stack.reverse(); } /** * Implement l * * Pushes the length of the stack onto the top of the stack */ PushLength() { this.stack.push(this.stack.length); } } /** * Get the char code of any character * * Can actually take any length of a value, but only returns the * char code of the first character. * * @param {*} value Any character * @returns {int} The value's char code */ function dec(value) { return value.toString().charCodeAt(0); }