Computer.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. const prompt = require("prompt-sync")({ sigint: true });
  2. const util = require("util");
  3. const uuid = require("uuid");
  4. const Stack = require("./Stack");
  5. const ComputerParameterMode = require("./ComputerParameterMode");
  6. const InputModes = require("./InputModes");
  7. const { DeepClone } = require("./common");
  8. module.exports = class Computer {
  9. /**
  10. * An Intcode Computer for the Advent of Code 2019 challenge
  11. *
  12. * @author Apis Necros
  13. *
  14. * @param {number[]} stack The initial memory to load into the computer
  15. * @param {Object} options Options that can be enabled within the computer
  16. * @param {object} options.debug A set of debugging options
  17. * @param {number} options.debug.tickRate The number of milliseconds between calls to Execute. Initializes to 0.
  18. * @param {boolean} options.debug.followPointer When true, the memory will be dumped during every call to Execute with the current instruction highlighted
  19. * @param {boolean} options.debug.followRuntimeInput When true, the runtime input array will be dumped during every call to Execute
  20. * @param {boolean} options.debug.followOutputValues When true, the output values array will be dumped during every call to Execute
  21. * @param {boolean} options.debug.followRelativeBaseOffset When true, the stack's relative base offset will be dumped during every call to Execute
  22. * @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.
  23. * @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.
  24. * @param {number[]} options.inputModeMap Map calls to the INPUT opcode to user input or the input array
  25. */
  26. constructor(stack, options = {}) {
  27. this.name = uuid.v4();
  28. this._initialMemory = DeepClone(stack);
  29. this.stack = new Stack(stack);
  30. this.OPCODES = {
  31. ADD: 1,
  32. MULTIPLY: 2,
  33. INPUT: 3,
  34. OUTPUT: 4,
  35. JUMP_IF_TRUE: 5,
  36. JUMP_IF_FALSE: 6,
  37. LESS_THAN: 7,
  38. EQUALS: 8,
  39. MODIFY_RELATIVE_BASE: 9,
  40. HALT: 99,
  41. };
  42. this.EQUALITY = {
  43. EQUALS: 0,
  44. LESS_THAN: 1,
  45. };
  46. this.parameterMode = ComputerParameterMode.POSITION_MODE;
  47. /**
  48. * Whether the Execute loop should skip moving the pointer after running the opcode
  49. *
  50. * Some opcodes, such as JUMP_IF_TRUE set the stack pointer, and as such shouldn't have
  51. * the Execute function move it after the opcode finishes executing.
  52. */
  53. this.skipNext = false;
  54. this.options = {
  55. debug: {
  56. followPointer: options?.debug?.followPointer ?? false,
  57. followRuntimeInput: options?.debug?.followRuntimeInput ?? false,
  58. followOutputValues: options?.debug?.followOutputValues ?? false,
  59. followRelativeBaseOffset: options?.debug?.followRelativeBaseOffset ?? false,
  60. tickRate: options?.debug?.tickRate ?? 0,
  61. },
  62. inputFromConsole: options.inputFromConsole ?? false,
  63. outputToConsole: options.outputToConsole ?? false,
  64. inputModeMap: options.inputModeMap ?? [],
  65. _initialInputModeMap: options.inputModeMap ?? [],
  66. };
  67. /**
  68. * A function to pass the value from OUTPUT opcodes to
  69. *
  70. * If the outputToConsole option is false, and a function is provided here,
  71. * any values from an OUTPUT opcode will be passed here.
  72. * @type {?Computer}
  73. */
  74. this.outputComputer = null;
  75. /**
  76. * An array of input values provided at runtime
  77. *
  78. * This value will be passed to the first INPUT opcode made by the computer,
  79. * and will then be cleared. If the program being ran tries to make another
  80. * call to INPUT, and `options.inputFromConsole` is false, then an error
  81. * will be thrown.
  82. * @type {number[]}
  83. */
  84. this.runtimeInput = [];
  85. /**
  86. * An array of outputs from the computer
  87. *
  88. * Outputs from the computer that weren't output to the console, or to a
  89. * callback function.
  90. *
  91. * Ideally, when not in outputToConsole mode, the computer should only ever have
  92. * one output, but I'm not certain that will be the case. In that case, this array
  93. * will have the outputs in chronological order.
  94. * @type {number[]}
  95. */
  96. this.outputValues = [];
  97. /**
  98. * Is the computer currently running
  99. * @type {boolean}
  100. */
  101. this.running = false;
  102. /**
  103. * Is the computer waiting for input
  104. *
  105. * Triggered by the INPUT opcode when the runtimeInputs array is empty
  106. * @type {boolean}
  107. */
  108. this.awaitingInput = false;
  109. }
  110. /**
  111. * Run the computer
  112. *
  113. * Runs opcodes on the stack until either the a HALT command is
  114. * encountered, or an error is thrown.
  115. * @returns {void}
  116. */
  117. async Run() {
  118. while (this.Execute(this.stack.Get(ComputerParameterMode.IMMEDIATE_MODE)) === true) {
  119. if (this.options.debug.tickRate) {
  120. // Sleep
  121. // eslint-disable-next-line no-await-in-loop, no-promise-executor-return, arrow-parens
  122. await new Promise(resolve => setTimeout(resolve, this.options.debug.tickRate));
  123. }
  124. }
  125. }
  126. /**
  127. * Run the computer with an initial input value
  128. *
  129. * Runs opcodes on the stack until either the a HALT command is
  130. * encountered, or an error is thrown. Stores the input to be used
  131. * by the first INPUT opcode encountered.
  132. * @param {number[]} input An array of input values to initialize the comptuer with
  133. * @returns {void}
  134. */
  135. async RunWithInput(input) {
  136. this.runtimeInput = input;
  137. while (this.Execute(this.stack.Get(ComputerParameterMode.IMMEDIATE_MODE)) === true) {
  138. if (this.options.debug.tickRate) {
  139. // Sleep
  140. // eslint-disable-next-line no-await-in-loop, no-promise-executor-return, arrow-parens
  141. await new Promise(resolve => setTimeout(resolve, this.options.debug.tickRate));
  142. }
  143. }
  144. }
  145. /**
  146. * Execute a call using the provided opcode
  147. *
  148. * @param {number} rawOpcode A opcode to execute
  149. * @returns {boolean} False if the opcode was HALT, otherwise true
  150. */
  151. Execute(rawOpcode) {
  152. let status = true;
  153. this.running = true;
  154. this.skipNext = false;
  155. this.FollowExecution();
  156. const opcode = rawOpcode % 100;
  157. // console.log(`DEBUG: opcode: ${opcode}`);
  158. switch (opcode) {
  159. case this.OPCODES.ADD: {
  160. this.Operation_Add(rawOpcode);
  161. break;
  162. }
  163. case this.OPCODES.MULTIPLY: {
  164. this.Operation_Multiply(rawOpcode);
  165. break;
  166. }
  167. case this.OPCODES.INPUT: {
  168. // If we're awaiting input, and still haven't received any, keep waiting
  169. if (this.awaitingInput == true && this.runtimeInput.length == 0) {
  170. this.skipNext = true;
  171. status = false;
  172. break;
  173. }
  174. this.Operation_Input(rawOpcode);
  175. break;
  176. }
  177. case this.OPCODES.OUTPUT: {
  178. this.Operation_Output(rawOpcode);
  179. break;
  180. }
  181. case this.OPCODES.JUMP_IF_TRUE: {
  182. this.Operation_JumpIf(rawOpcode, true);
  183. break;
  184. }
  185. case this.OPCODES.JUMP_IF_FALSE: {
  186. this.Operation_JumpIf(rawOpcode, false);
  187. break;
  188. }
  189. case this.OPCODES.LESS_THAN: {
  190. this.Operation_Equality(rawOpcode, this.EQUALITY.LESS_THAN);
  191. break;
  192. }
  193. case this.OPCODES.EQUALS: {
  194. this.Operation_Equality(rawOpcode, this.EQUALITY.EQUALS);
  195. break;
  196. }
  197. case this.OPCODES.HALT:
  198. this.running = false;
  199. status = false;
  200. break;
  201. case this.OPCODES.MODIFY_RELATIVE_BASE: {
  202. this.Operation_ModifyRelativeBase(rawOpcode);
  203. break;
  204. }
  205. default:
  206. this.ThrowError(`Opcode ${opcode} not found`);
  207. }
  208. if (!this.skipNext) {
  209. this.stack.Next();
  210. }
  211. return status;
  212. }
  213. /**
  214. * Parse operands based on the current parameter mode
  215. *
  216. * When the int computer is in Immediate Mode, the values are returned
  217. * as-is. When in Position Mode, the operands are used as memory
  218. * addresses, and the values at those addresses are returned instead.
  219. *
  220. * @private
  221. * @returns {number[]} The parsed list of operands
  222. */
  223. _ParseOperands(...operands) {
  224. if (this.parameterMode == ComputerParameterMode.IMMEDIATE_MODE) { return operands; }
  225. return operands.map((operand) => this.stack.Get(operand));
  226. }
  227. /**
  228. * Execute the Add opcode
  229. *
  230. * Adds two numbers and stores the result at the provided position
  231. * on the stack.
  232. *
  233. * Parses the operand Parameter Mode out of the opcode used to make
  234. * this call.
  235. *
  236. * @param {number} rawOpcode The opcode in memory used to make this call
  237. * @private
  238. * @returns {void}
  239. */
  240. Operation_Add(rawOpcode) {
  241. const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  242. const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2);
  243. const outputMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 3) || 1;
  244. const operandLeft = this.stack.Next().Get(operandLeftMode);
  245. const operandRight = this.stack.Next().Get(operandRightMode);
  246. const outputPosition = this.stack.Next().Get(outputMode, true);
  247. const newValue = operandLeft + operandRight;
  248. this.stack.Put(outputPosition, newValue);
  249. }
  250. /**
  251. * Execute the Multiply opcode
  252. *
  253. * Multiplies two numbers and stores the result at the provided
  254. * position on the stack.
  255. *
  256. * @param {number} rawOpcode The opcode in memory used to make this call
  257. * @private
  258. * @returns {void}
  259. */
  260. Operation_Multiply(rawOpcode) {
  261. const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  262. const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2);
  263. const outputMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 3) || 1;
  264. const operandLeft = this.stack.Next().Get(operandLeftMode);
  265. const operandRight = this.stack.Next().Get(operandRightMode);
  266. const outputPosition = this.stack.Next().Get(outputMode, true);
  267. const newValue = operandLeft * operandRight;
  268. this.stack.Put(outputPosition, newValue);
  269. }
  270. /**
  271. * Execute the Input opcode
  272. *
  273. * Checks to see if the computer's `runtimeInput` is set. If so, uses that
  274. * value as the input, and stores that at a specified address, and then
  275. * empties the `runtimeInput` value. If `runtimeInput` is empty, and if
  276. * the computer is set to accept input from the console, prompts the
  277. * user for input.
  278. *
  279. * @private
  280. * @param {number} rawOpcode The opcode in memory used to make this call
  281. * @returns {void}
  282. */
  283. Operation_Input(rawOpcode) {
  284. // Disallow Position Parameter Mode
  285. const outputParamMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1) || 1;
  286. const outputPosition = this.stack.Next().Get(outputParamMode, true);
  287. /** A variable to store the input in before putting it on the stack */
  288. let userInput;
  289. /** The input mode to use to get the value */
  290. let inputMode = this.options.inputModeMap.shift();
  291. // If no input mode was found, attempt to set on using the runtime options
  292. if (inputMode === undefined) {
  293. if (this.options.inputFromConsole) { inputMode = InputModes.INPUT_FROM_CONSOLE; }
  294. else { inputMode = InputModes.INPUT_FROM_RUNTIME_STACK; }
  295. }
  296. // Get the input based on the input mode
  297. switch (inputMode) {
  298. case InputModes.INPUT_FROM_RUNTIME_STACK: {
  299. userInput = this.runtimeInput.shift();
  300. // If no input was found, await input
  301. if (userInput === undefined) {
  302. // Set the stack back to the INPUT opcode
  303. this.stack.Prev();
  304. this.stack.Prev();
  305. // Set the awaitingInput flag
  306. this.awaitingInput = true;
  307. // Exit the function
  308. return;
  309. }
  310. this.awaitingInput = false;
  311. break;
  312. }
  313. case InputModes.INPUT_FROM_CONSOLE: {
  314. do {
  315. userInput = Number(prompt("Please enter a number: "));
  316. } while (Number.isNaN(userInput));
  317. break;
  318. }
  319. default:
  320. this.ThrowError("No input found");
  321. }
  322. // Put the input value onto the stack
  323. this.stack.Put(outputPosition, userInput);
  324. }
  325. /**
  326. * Execute the OUTPUT opcode
  327. *
  328. * @param {number} rawOpcode The opcode in memory used to make this call
  329. * @private
  330. * @returns {void}
  331. */
  332. Operation_Output(rawOpcode) {
  333. const currAddress = this.options.outputToConsole ? this.stack.pointer : undefined;
  334. const outputPositionMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  335. const output = this.stack.Next().Get(outputPositionMode);
  336. if (this.options.outputToConsole) {
  337. console.log(`OUTPUT FROM ADDRESS ${currAddress}: ${output}`);
  338. }
  339. else if (this.outputComputer !== null) {
  340. this.outputComputer.Input(output);
  341. }
  342. else {
  343. this.outputValues.push(output);
  344. }
  345. }
  346. /**
  347. * Execute the Jump_If_True and Jump_If_False opcodes
  348. *
  349. * Jumps to a given address in memory if the value at next address is memory matches
  350. * the given true/false condition.
  351. *
  352. * @param {number} rawOpcode The opcode in memory used to make this call
  353. * @param {boolean} testCondition The value the memory value should be compared against
  354. * @private
  355. * @returns {void}
  356. */
  357. Operation_JumpIf(rawOpcode, testCondition) {
  358. const paramMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  359. const jumpAddressMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2);
  360. const param = this.stack.Next().Get(paramMode);
  361. const jumpAddress = this.stack.Next().Get(jumpAddressMode);
  362. const performJump = !!param == testCondition;
  363. if (performJump) {
  364. this.skipNext = true;
  365. this.stack.SetPointerAddress(jumpAddress);
  366. }
  367. }
  368. /**
  369. * Execute the various equality checking opcodes
  370. *
  371. * @param {number} rawOpcode The opcode in memory used to make this call
  372. * @param {number} testCondition The type of equality check to perform as defined in the computer's constructor
  373. * @private
  374. * @returns {void}
  375. */
  376. Operation_Equality(rawOpcode, testCondition) {
  377. const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  378. const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2);
  379. const outputMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 3) || 1;
  380. const operandLeft = this.stack.Next().Get(operandLeftMode);
  381. const operandRight = this.stack.Next().Get(operandRightMode);
  382. const outputPosition = this.stack.Next().Get(outputMode, true);
  383. let testPassed = false;
  384. switch (testCondition) {
  385. case this.EQUALITY.EQUALS:
  386. testPassed = operandLeft == operandRight;
  387. break;
  388. case this.EQUALITY.LESS_THAN:
  389. testPassed = operandLeft < operandRight;
  390. break;
  391. default:
  392. break;
  393. }
  394. this.stack.Put(outputPosition, Number(testPassed));
  395. }
  396. /**
  397. * Adjusts the current relative base offset of the computer
  398. *
  399. * @param {number} rawOpcode The opcode in memory used to make this call
  400. * @returns {void}
  401. */
  402. Operation_ModifyRelativeBase(rawOpcode) {
  403. const operandMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  404. const adjustmentValue = this.stack.Next().Get(operandMode);
  405. this.stack.AdjustRelativeBaseOffset(adjustmentValue);
  406. }
  407. /**
  408. * Inspects various parts of the computer before every call to Execution
  409. *
  410. * Based on the debug options set at runtime, may output any combination of the
  411. * computer's core memory, the runtime input stack, and or the output values
  412. * array.
  413. *
  414. * @returns {void}
  415. */
  416. FollowExecution() {
  417. if (this.options.debug.followPointer) { this.DumpMemory(true); }
  418. if (this.options.debug.followRuntimeInput) { this.InspectProperty("Inputs", "runtimeInput"); }
  419. if (this.options.debug.followOutputValues) { this.InspectProperty("Outputs", "outputValues"); }
  420. if (this.options.debug.followRelativeBaseOffset) { this.InspectProperty("Relative Base Offset", null, this.stack.relativeBaseOffset); }
  421. }
  422. /**
  423. * Outputs the computer's stack to the console
  424. *
  425. * @param {boolean} [highlightPointer=false] Should the memory address of the current pointer be highlighted
  426. * @returns {void}
  427. */
  428. DumpMemory(highlightPointer = false) {
  429. let memory = this.stack.Dump();
  430. if (highlightPointer) {
  431. memory = memory.map((instruction, idx) => (idx == this.stack.pointer ? `{${instruction}}` : instruction));
  432. }
  433. this.InspectProperty("Memory", null, memory);
  434. }
  435. /**
  436. * Inspect a property of this object in the console
  437. *
  438. * @param {string} [outputMessage] An optional message to prepend the inspection with
  439. * @param {?string} [propertyName] The name of the Computer class property to inspect
  440. * @param {?unknown} [overrideValue] If provided, this value is inspected as-is instead of the class property
  441. * @returns {void}
  442. */
  443. InspectProperty(outputMessage = "", propertyName = null, overrideValue = null) {
  444. let toInspect;
  445. if (overrideValue !== null) {
  446. toInspect = overrideValue;
  447. }
  448. else if (this[propertyName] !== undefined) {
  449. toInspect = this[propertyName];
  450. }
  451. console.log(this.name, outputMessage, util.inspect(toInspect, { breakLength: Infinity, colors: true, compact: true }));
  452. }
  453. /**
  454. * Check if the computer has any values in the output array
  455. *
  456. * @returns {boolean} True or false if there are any values in the output array
  457. */
  458. HasOutput() {
  459. return !!this.outputValues.length;
  460. }
  461. /**
  462. * Get a value from the unhandled OUTPUT values in FIFO order
  463. *
  464. * @returns {number|undefined} An unhandled value from an output call, or undefined if the array is empty
  465. */
  466. FetchOutputValue() {
  467. return this.HasOutput() ? this.outputValues.shift() : undefined;
  468. }
  469. /**
  470. * Accept input from an external source
  471. *
  472. * The input given here is pushed onto the end of the computer's runtime
  473. * input array.
  474. *
  475. * @param {number} inputValue The number to push onto the runtime input array
  476. * @returns {void}
  477. */
  478. Input(inputValue) {
  479. this.runtimeInput.push(inputValue);
  480. if (this.running && this.awaitingInput) { this.Run(); }
  481. }
  482. /**
  483. * Resets the computer's memory to the value it was created with
  484. *
  485. * @returns {void}
  486. */
  487. Reset() {
  488. this.stack = new Stack(DeepClone(this._initialMemory));
  489. this.outputValues = [];
  490. this.options.inputModeMap = DeepClone(this.options._initialInputModeMap);
  491. this.running = false;
  492. this.awaitingInput = false;
  493. }
  494. /**
  495. * Sets the computer's memory to a new stack
  496. *
  497. * Note: This resets the computer's initial memory, so `Reset` will use this value
  498. *
  499. * @param {number[]} stack The new memory stack for the computer
  500. * @returns {void}
  501. */
  502. SetMemory(stack) {
  503. this._initialMemory = DeepClone(stack);
  504. this.stack = new Stack(stack);
  505. }
  506. /**
  507. * Throws an error with the given message and a memory dump
  508. *
  509. * @param {string} [msg] The error message to display to the user
  510. * @returns {void}
  511. */
  512. ThrowError(msg = "") {
  513. throw new Error(`** IntCode Computer Error **\nError Message: ${msg}\nMemdump: ${JSON.stringify(this.stack.Dump())}\nPointer: ${this.stack.pointer}`);
  514. }
  515. };