const prompt = require("prompt-sync")({ sigint: true }); const util = require("util"); const Stack = require("./Stack"); const ComputerParameterMode = require("./ComputerParameterMode"); 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 {boolean} options.followPointer When true, the memory will be dumped every call to Execute with the current instruction highlighted * @param {number} options.tickRate The number of milliseconds between calls to Execute. Initializes to 0. */ constructor(stack, options = {}) { 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, 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 = { followPointer: options.followPointer ?? false, tickRate: options.tickRate ?? 0, }; } /** * 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.tickRate) { // Sleep // eslint-disable-next-line no-await-in-loop, no-promise-executor-return, arrow-parens await new Promise(resolve => setTimeout(resolve, this.options.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.skipNext = false; if (this.options.followPointer) { this.DumpMemory(true); } 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: { this.Operation_Input(); 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: status = false; break; default: throw Error(`Opcode ${opcode} not found\nMemdump: ${JSON.stringify(this.stack.Dump())}\nPointer: ${this.stack.pointer}`); } 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. * * @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 * @returns {void} */ Operation_Add(rawOpcode) { const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2); const operandLeft = this.stack.Next().Get(operandLeftMode); const operandRight = this.stack.Next().Get(operandRightMode); const outputPosition = this.stack.Next().Get(ComputerParameterMode.IMMEDIATE_MODE); 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 * @returns {void} */ Operation_Multiply(rawOpcode) { const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2); const operandLeft = this.stack.Next().Get(operandLeftMode); const operandRight = this.stack.Next().Get(operandRightMode); const outputPosition = this.stack.Next().Get(ComputerParameterMode.IMMEDIATE_MODE); const newValue = operandLeft * operandRight; this.stack.Put(outputPosition, newValue); } /** * Execute the Input opcode * * Prompts the user to input a value from the command line * * @returns {void} */ Operation_Input() { const outputPosition = this.stack.Next().Get(ComputerParameterMode.IMMEDIATE_MODE); let userInput; do { userInput = Number(prompt("Please enter a number: ")); } while (Number.isNaN(userInput)); this.stack.Put(outputPosition, userInput); } /** * Execute the OUTPUT opcode * * @param {number} rawOpcode The opcode in memory used to make this call * @returns {void} */ Operation_Output(rawOpcode) { const currAddress = this.stack.pointer; const outputPositionMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const output = this.stack.Next().Get(outputPositionMode); console.log(`OUTPUT FROM ADDRESS ${currAddress}: ${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 * @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 * @returns {void} */ Operation_Equality(rawOpcode, testCondition) { const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1); const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2); const operandLeft = this.stack.Next().Get(operandLeftMode); const operandRight = this.stack.Next().Get(operandRightMode); const outputPosition = this.stack.Next().Get(ComputerParameterMode.IMMEDIATE_MODE); 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)); } /** * 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)); } console.log(util.inspect(memory, { breakLength: Infinity, colors: true, compact: true })); } /** * Resets the computer's memory to the value it was created with * * @returns {void} */ Reset() { this.stack = new Stack(this._initialMemory); } /** * 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); } };