const prompt = require("prompt-sync")({ sigint: true }); const util = require("util"); const uuid = require("uuid"); const Stack = require("./Stack"); const ComputerParameterMode = require("./ComputerParameterMode"); const InputModes = require("./InputModes"); const { DeepClone } = require("./common"); module.exports = class Computer { /** * An Intcode Computer for the Advent of Code 2019 challenge * * @author Apis Necros * * @param {number[]} stack The initial memory to load into the computer * @param {Object} options Options that can be enabled within the computer * @param {object} options.debug A set of debugging options * @param {number} options.debug.tickRate The number of milliseconds between calls to Execute. Initializes to 0. * @param {boolean} options.debug.followPointer When true, the memory will be dumped during every call to Execute with the current instruction highlighted * @param {boolean} options.debug.followRuntimeInput When true, the runtime input array will be dumped during every call to Execute * @param {boolean} options.debug.followOutputValues When true, the output values array will be dumped during every call to Execute * @param {boolean} options.debug.followRelativeBaseOffset When true, the stack's relative base offset will be dumped during every call to Execute * @param {boolean} options.inputFromConsole When true, the computer will prompt for input on the console. If false, it will check for an linked computer and, if one exists, will wait for input from that computer. * @param {boolean} options.outputToConsole When true, the computer will print the output of opcode 4 to the console. If false, it will check for an linked computer and, if one exists, pass the output to that computer. * @param {number[]} options.inputModeMap Map calls to the INPUT opcode to user input or the input array */ constructor(stack, options = {}) { this.name = uuid.v4(); this._initialMemory = DeepClone(stack); this.stack = new Stack(stack); this.OPCODES = { ADD: 1, MULTIPLY: 2, INPUT: 3, OUTPUT: 4, JUMP_IF_TRUE: 5, JUMP_IF_FALSE: 6, LESS_THAN: 7, EQUALS: 8, MODIFY_RELATIVE_BASE: 9, HALT: 99, }; this.EQUALITY = { EQUALS: 0, LESS_THAN: 1, }; this.parameterMode = ComputerParameterMode.POSITION_MODE; /** * Whether the Execute loop should skip moving the pointer after running the opcode * * Some opcodes, such as JUMP_IF_TRUE set the stack pointer, and as such shouldn't have * the Execute function move it after the opcode finishes executing. */ this.skipNext = false; this.options = { debug: { followPointer: options?.debug?.followPointer ?? false, followRuntimeInput: options?.debug?.followRuntimeInput ?? false, followOutputValues: options?.debug?.followOutputValues ?? false, followRelativeBaseOffset: options?.debug?.followRelativeBaseOffset ?? false, tickRate: options?.debug?.tickRate ?? 0, }, inputFromConsole: options.inputFromConsole ?? false, outputToConsole: options.outputToConsole ?? false, inputModeMap: options.inputModeMap ?? [], _initialInputModeMap: options.inputModeMap ?? [], }; /** * A function to pass the value from OUTPUT opcodes to * * If the outputToConsole option is false, and a function is provided here, * any values from an OUTPUT opcode will be passed here. * @type {?Computer} */ this.outputComputer = null; /** * An array of input values provided at runtime * * This value will be passed to the first INPUT opcode made by the computer, * and will then be cleared. If the program being ran tries to make another * call to INPUT, and `options.inputFromConsole` is false, then an error * will be thrown. * @type {number[]} */ this.runtimeInput = []; /** * An array of outputs from the computer * * Outputs from the computer that weren't output to the console, or to a * callback function. * * Ideally, when not in outputToConsole mode, the computer should only ever have * one output, but I'm not certain that will be the case. In that case, this array * will have the outputs in chronological order. * @type {number[]} */ this.outputValues = []; /** * Is the computer currently running * @type {boolean} */ this.running = false; /** * Is the computer waiting for input * * Triggered by the INPUT opcode when the runtimeInputs array is empty * @type {boolean} */ this.awaitingInput = false; } /** * Run the computer * * Runs opcodes on the stack until either the a HALT command is * encountered, or an error is thrown. * @returns {void} */ async Run() { while (this.Execute(this.stack.Get(ComputerParameterMode.IMMEDIATE_MODE)) === true) { if (this.options.debug.tickRate) { // Sleep // eslint-disable-next-line no-await-in-loop, no-promise-executor-return, arrow-parens await new Promise(resolve => setTimeout(resolve, this.options.debug.tickRate)); } } } /** * Run the computer with an initial input value * * Runs opcodes on the stack until either the a HALT command is * encountered, or an error is thrown. Stores the input to be used * by the first INPUT opcode encountered. * @param {number[]} input An array of input values to initialize the comptuer with * @returns {void} */ async RunWithInput(input) { this.runtimeInput = input; while (this.Execute(this.stack.Get(ComputerParameterMode.IMMEDIATE_MODE)) === true) { if (this.options.debug.tickRate) { // Sleep // eslint-disable-next-line no-await-in-loop, no-promise-executor-return, arrow-parens await new Promise(resolve => setTimeout(resolve, this.options.debug.tickRate)); } } } /** * Execute a call using the provided opcode * * @param {number} rawOpcode A opcode to execute * @returns {boolean} False if the opcode was HALT, otherwise true */ Execute(rawOpcode) { let status = true; this.running = true; this.skipNext = false; this.FollowExecution(); const opcode = rawOpcode % 100; // console.log(`DEBUG: opcode: ${opcode}`); switch (opcode) { case this.OPCODES.ADD: { this.Operation_Add(rawOpcode); break; } case this.OPCODES.MULTIPLY: { this.Operation_Multiply(rawOpcode); break; } case this.OPCODES.INPUT: { // If we're awaiting input, and still haven't received any, keep waiting if (this.awaitingInput == true && this.runtimeInput.length == 0) { this.skipNext = true; status = false; break; } this.Operation_Input(rawOpcode); break; } case this.OPCODES.OUTPUT: { this.Operation_Output(rawOpcode); break; } case this.OPCODES.JUMP_IF_TRUE: { this.Operation_JumpIf(rawOpcode, true); break; } case this.OPCODES.JUMP_IF_FALSE: { this.Operation_JumpIf(rawOpcode, false); break; } case this.OPCODES.LESS_THAN: { this.Operation_Equality(rawOpcode, this.EQUALITY.LESS_THAN); break; } case this.OPCODES.EQUALS: { this.Operation_Equality(rawOpcode, this.EQUALITY.EQUALS); break; } case this.OPCODES.HALT: this.running = false; status = false; break; case this.OPCODES.MODIFY_RELATIVE_BASE: { this.Operation_ModifyRelativeBase(rawOpcode); break; } default: this.ThrowError(`Opcode ${opcode} not found`); } if (!this.skipNext) { this.stack.Next(); } return status; } /** * Parse operands based on the current parameter mode * * When the int computer is in Immediate Mode, the values are returned * as-is. When in Position Mode, the operands are used as memory * addresses, and the values at those addresses are returned instead. * * @private * @returns {number[]} The parsed list of operands */ _ParseOperands(...operands) { if (this.parameterMode == ComputerParameterMode.IMMEDIATE_MODE) { return operands; } return operands.map((operand) => this.stack.Get(operand)); } /** * Execute the Add opcode * * Adds two numbers and stores the result at the provided position * on the stack. * * Parses the operand Parameter Mode out of the opcode used to make * this call. * * @param {number} rawOpcode The opcode in memory used to make this call * @private * @returns {void} */ Operation_Add(rawOpcode) { const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2); const outputMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 3) || 1; const operandLeft = this.stack.Next().Get(operandLeftMode); const operandRight = this.stack.Next().Get(operandRightMode); const outputPosition = this.stack.Next().Get(outputMode, true); const newValue = operandLeft + operandRight; this.stack.Put(outputPosition, newValue); } /** * Execute the Multiply opcode * * Multiplies two numbers and stores the result at the provided * position on the stack. * * @param {number} rawOpcode The opcode in memory used to make this call * @private * @returns {void} */ Operation_Multiply(rawOpcode) { const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2); const outputMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 3) || 1; const operandLeft = this.stack.Next().Get(operandLeftMode); const operandRight = this.stack.Next().Get(operandRightMode); const outputPosition = this.stack.Next().Get(outputMode, true); const newValue = operandLeft * operandRight; this.stack.Put(outputPosition, newValue); } /** * Execute the Input opcode * * Checks to see if the computer's `runtimeInput` is set. If so, uses that * value as the input, and stores that at a specified address, and then * empties the `runtimeInput` value. If `runtimeInput` is empty, and if * the computer is set to accept input from the console, prompts the * user for input. * * @private * @param {number} rawOpcode The opcode in memory used to make this call * @returns {void} */ Operation_Input(rawOpcode) { // Disallow Position Parameter Mode const outputParamMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1) || 1; const outputPosition = this.stack.Next().Get(outputParamMode, true); /** A variable to store the input in before putting it on the stack */ let userInput; /** The input mode to use to get the value */ let inputMode = this.options.inputModeMap.shift(); // If no input mode was found, attempt to set on using the runtime options if (inputMode === undefined) { if (this.options.inputFromConsole) { inputMode = InputModes.INPUT_FROM_CONSOLE; } else { inputMode = InputModes.INPUT_FROM_RUNTIME_STACK; } } // Get the input based on the input mode switch (inputMode) { case InputModes.INPUT_FROM_RUNTIME_STACK: { userInput = this.runtimeInput.shift(); // If no input was found, await input if (userInput === undefined) { // Set the stack back to the INPUT opcode this.stack.Prev(); this.stack.Prev(); // Set the awaitingInput flag this.awaitingInput = true; // Exit the function return; } this.awaitingInput = false; break; } case InputModes.INPUT_FROM_CONSOLE: { do { userInput = Number(prompt("Please enter a number: ")); } while (Number.isNaN(userInput)); break; } default: this.ThrowError("No input found"); } // Put the input value onto the stack this.stack.Put(outputPosition, userInput); } /** * Execute the OUTPUT opcode * * @param {number} rawOpcode The opcode in memory used to make this call * @private * @returns {void} */ Operation_Output(rawOpcode) { const currAddress = this.options.outputToConsole ? this.stack.pointer : undefined; const outputPositionMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const output = this.stack.Next().Get(outputPositionMode); if (this.options.outputToConsole) { console.log(`OUTPUT FROM ADDRESS ${currAddress}: ${output}`); } else if (this.outputComputer !== null) { this.outputComputer.Input(output); } else { this.outputValues.push(output); } } /** * Execute the Jump_If_True and Jump_If_False opcodes * * Jumps to a given address in memory if the value at next address is memory matches * the given true/false condition. * * @param {number} rawOpcode The opcode in memory used to make this call * @param {boolean} testCondition The value the memory value should be compared against * @private * @returns {void} */ Operation_JumpIf(rawOpcode, testCondition) { const paramMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const jumpAddressMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2); const param = this.stack.Next().Get(paramMode); const jumpAddress = this.stack.Next().Get(jumpAddressMode); const performJump = !!param == testCondition; if (performJump) { this.skipNext = true; this.stack.SetPointerAddress(jumpAddress); } } /** * Execute the various equality checking opcodes * * @param {number} rawOpcode The opcode in memory used to make this call * @param {number} testCondition The type of equality check to perform as defined in the computer's constructor * @private * @returns {void} */ Operation_Equality(rawOpcode, testCondition) { const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2); const outputMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 3) || 1; const operandLeft = this.stack.Next().Get(operandLeftMode); const operandRight = this.stack.Next().Get(operandRightMode); const outputPosition = this.stack.Next().Get(outputMode, true); let testPassed = false; switch (testCondition) { case this.EQUALITY.EQUALS: testPassed = operandLeft == operandRight; break; case this.EQUALITY.LESS_THAN: testPassed = operandLeft < operandRight; break; default: break; } this.stack.Put(outputPosition, Number(testPassed)); } /** * Adjusts the current relative base offset of the computer * * @param {number} rawOpcode The opcode in memory used to make this call * @returns {void} */ Operation_ModifyRelativeBase(rawOpcode) { const operandMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const adjustmentValue = this.stack.Next().Get(operandMode); this.stack.AdjustRelativeBaseOffset(adjustmentValue); } /** * Inspects various parts of the computer before every call to Execution * * Based on the debug options set at runtime, may output any combination of the * computer's core memory, the runtime input stack, and or the output values * array. * * @returns {void} */ FollowExecution() { if (this.options.debug.followPointer) { this.DumpMemory(true); } if (this.options.debug.followRuntimeInput) { this.InspectProperty("Inputs", "runtimeInput"); } if (this.options.debug.followOutputValues) { this.InspectProperty("Outputs", "outputValues"); } if (this.options.debug.followRelativeBaseOffset) { this.InspectProperty("Relative Base Offset", null, this.stack.relativeBaseOffset); } } /** * Outputs the computer's stack to the console * * @param {boolean} [highlightPointer=false] Should the memory address of the current pointer be highlighted * @returns {void} */ DumpMemory(highlightPointer = false) { let memory = this.stack.Dump(); if (highlightPointer) { memory = memory.map((instruction, idx) => (idx == this.stack.pointer ? `{${instruction}}` : instruction)); } this.InspectProperty("Memory", null, memory); } /** * Inspect a property of this object in the console * * @param {string} [outputMessage] An optional message to prepend the inspection with * @param {?string} [propertyName] The name of the Computer class property to inspect * @param {?unknown} [overrideValue] If provided, this value is inspected as-is instead of the class property * @returns {void} */ InspectProperty(outputMessage = "", propertyName = null, overrideValue = null) { let toInspect; if (overrideValue !== null) { toInspect = overrideValue; } else if (this[propertyName] !== undefined) { toInspect = this[propertyName]; } console.log(this.name, outputMessage, util.inspect(toInspect, { breakLength: Infinity, colors: true, compact: true })); } /** * Check if the computer has any values in the output array * * @returns {boolean} True or false if there are any values in the output array */ HasOutput() { return !!this.outputValues.length; } /** * Get a value from the unhandled OUTPUT values in FIFO order * * @returns {number|undefined} An unhandled value from an output call, or undefined if the array is empty */ FetchOutputValue() { return this.HasOutput() ? this.outputValues.shift() : undefined; } /** * Accept input from an external source * * The input given here is pushed onto the end of the computer's runtime * input array. * * @param {number} inputValue The number to push onto the runtime input array * @returns {void} */ Input(inputValue) { this.runtimeInput.push(inputValue); if (this.running && this.awaitingInput) { this.Run(); } } /** * Resets the computer's memory to the value it was created with * * @returns {void} */ Reset() { this.stack = new Stack(DeepClone(this._initialMemory)); this.outputValues = []; this.options.inputModeMap = DeepClone(this.options._initialInputModeMap); this.running = false; this.awaitingInput = false; } /** * Sets the computer's memory to a new stack * * Note: This resets the computer's initial memory, so `Reset` will use this value * * @param {number[]} stack The new memory stack for the computer * @returns {void} */ SetMemory(stack) { this._initialMemory = DeepClone(stack); this.stack = new Stack(stack); } /** * Throws an error with the given message and a memory dump * * @param {string} [msg] The error message to display to the user * @returns {void} */ ThrowError(msg = "") { throw new Error(`** IntCode Computer Error **\nError Message: ${msg}\nMemdump: ${JSON.stringify(this.stack.Dump())}\nPointer: ${this.stack.pointer}`); } };