Computer.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. const prompt = require("prompt-sync")({ sigint: true });
  2. const util = require("util");
  3. const Stack = require("./Stack");
  4. const ComputerParameterMode = require("./ComputerParameterMode");
  5. const InputModes = require("./InputModes");
  6. const { DeepClone } = require("./common");
  7. module.exports = class Computer {
  8. /**
  9. * An Intcode Computer for the Advent of Code 2019 challenge
  10. *
  11. * @author Apis Necros
  12. *
  13. * @param {number[]} stack The initial memory to load into the computer
  14. * @param {Object} options Options that can be enabled within the computer
  15. * @param {boolean} options.followPointer When true, the memory will be dumped every call to Execute with the current instruction highlighted
  16. * @param {number} options.tickRate The number of milliseconds between calls to Execute. Initializes to 0.
  17. * @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.
  18. * @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.
  19. * @param {number[]} options.inputModeMap Map calls to the INPUT opcode to user input or the input array
  20. */
  21. constructor(stack, options = {}) {
  22. this._initialMemory = DeepClone(stack);
  23. this.stack = new Stack(stack);
  24. this.OPCODES = {
  25. ADD: 1,
  26. MULTIPLY: 2,
  27. INPUT: 3,
  28. OUTPUT: 4,
  29. JUMP_IF_TRUE: 5,
  30. JUMP_IF_FALSE: 6,
  31. LESS_THAN: 7,
  32. EQUALS: 8,
  33. HALT: 99,
  34. };
  35. this.EQUALITY = {
  36. EQUALS: 0,
  37. LESS_THAN: 1,
  38. };
  39. this.parameterMode = ComputerParameterMode.POSITION_MODE;
  40. /**
  41. * Whether the Execute loop should skip moving the pointer after running the opcode
  42. *
  43. * Some opcodes, such as JUMP_IF_TRUE set the stack pointer, and as such shouldn't have
  44. * the Execute function move it after the opcode finishes executing.
  45. */
  46. this.skipNext = false;
  47. this.options = {
  48. followPointer: options.followPointer ?? false,
  49. tickRate: options.tickRate ?? 0,
  50. inputFromConsole: options.inputFromConsole ?? false,
  51. outputToConsole: options.outputToConsole ?? false,
  52. inputModeMap: options.inputModeMap ?? [],
  53. _initialInputModeMap: options.inputModeMap ?? [],
  54. };
  55. /**
  56. * A function to pass the value from OUTPUT opcodes to
  57. *
  58. * If the outputToConsole option is false, and a function is provided here,
  59. * any values from an OUTPUT opcode will be passed here.
  60. * @type {?Computer}
  61. */
  62. this.outputComputer = null;
  63. /**
  64. * An array of input values provided at runtime
  65. *
  66. * This value will be passed to the first INPUT opcode made by the computer,
  67. * and will then be cleared. If the program being ran tries to make another
  68. * call to INPUT, and `options.inputFromConsole` is false, then an error
  69. * will be thrown.
  70. * @type {number[]}
  71. */
  72. this.runtimeInput = [];
  73. /**
  74. * An array of outputs from the computer
  75. *
  76. * Outputs from the computer that weren't output to the console, or to a
  77. * callback function.
  78. *
  79. * Ideally, when not in outputToConsole mode, the computer should only ever have
  80. * one output, but I'm not certain that will be the case. In that case, this array
  81. * will have the outputs in chronological order.
  82. * @type {number[]}
  83. */
  84. this.outputValues = [];
  85. }
  86. /**
  87. * Run the computer
  88. *
  89. * Runs opcodes on the stack until either the a HALT command is
  90. * encountered, or an error is thrown.
  91. * @returns {void}
  92. */
  93. async Run() {
  94. while (this.Execute(this.stack.Get(ComputerParameterMode.IMMEDIATE_MODE)) === true) {
  95. if (this.options.tickRate) {
  96. // Sleep
  97. // eslint-disable-next-line no-await-in-loop, no-promise-executor-return, arrow-parens
  98. await new Promise(resolve => setTimeout(resolve, this.options.tickRate));
  99. }
  100. }
  101. }
  102. /**
  103. * Run the computer with an initial input value
  104. *
  105. * Runs opcodes on the stack until either the a HALT command is
  106. * encountered, or an error is thrown. Stores the input to be used
  107. * by the first INPUT opcode encountered.
  108. * @param {number[]} input An array of input values to initialize the comptuer with
  109. * @returns {void}
  110. */
  111. async RunWithInput(input) {
  112. this.runtimeInput = input;
  113. while (this.Execute(this.stack.Get(ComputerParameterMode.IMMEDIATE_MODE)) === true) {
  114. if (this.options.tickRate) {
  115. // Sleep
  116. // eslint-disable-next-line no-await-in-loop, no-promise-executor-return, arrow-parens
  117. await new Promise(resolve => setTimeout(resolve, this.options.tickRate));
  118. }
  119. }
  120. }
  121. /**
  122. * Execute a call using the provided opcode
  123. *
  124. * @param {number} rawOpcode A opcode to execute
  125. * @returns {boolean} False if the opcode was HALT, otherwise true
  126. */
  127. Execute(rawOpcode) {
  128. let status = true;
  129. this.skipNext = false;
  130. if (this.options.followPointer) {
  131. this.DumpMemory(true);
  132. }
  133. const opcode = rawOpcode % 100;
  134. // console.log(`DEBUG: opcode: ${opcode}`);
  135. switch (opcode) {
  136. case this.OPCODES.ADD: {
  137. this.Operation_Add(rawOpcode);
  138. break;
  139. }
  140. case this.OPCODES.MULTIPLY: {
  141. this.Operation_Multiply(rawOpcode);
  142. break;
  143. }
  144. case this.OPCODES.INPUT: {
  145. this.Operation_Input();
  146. break;
  147. }
  148. case this.OPCODES.OUTPUT: {
  149. this.Operation_Output(rawOpcode);
  150. break;
  151. }
  152. case this.OPCODES.JUMP_IF_TRUE: {
  153. this.Operation_JumpIf(rawOpcode, true);
  154. break;
  155. }
  156. case this.OPCODES.JUMP_IF_FALSE: {
  157. this.Operation_JumpIf(rawOpcode, false);
  158. break;
  159. }
  160. case this.OPCODES.LESS_THAN: {
  161. this.Operation_Equality(rawOpcode, this.EQUALITY.LESS_THAN);
  162. break;
  163. }
  164. case this.OPCODES.EQUALS: {
  165. this.Operation_Equality(rawOpcode, this.EQUALITY.EQUALS);
  166. break;
  167. }
  168. case this.OPCODES.HALT:
  169. status = false;
  170. break;
  171. default:
  172. throw Error(`Opcode ${opcode} not found\nMemdump: ${JSON.stringify(this.stack.Dump())}\nPointer: ${this.stack.pointer}`);
  173. }
  174. if (!this.skipNext) {
  175. this.stack.Next();
  176. }
  177. return status;
  178. }
  179. /**
  180. * Parse operands based on the current parameter mode
  181. *
  182. * When the int computer is in Immediate Mode, the values are returned
  183. * as-is. When in Position Mode, the operands are used as memory
  184. * addresses, and the values at those addresses are returned instead.
  185. *
  186. * @returns {number[]} The parsed list of operands
  187. */
  188. _ParseOperands(...operands) {
  189. if (this.parameterMode == ComputerParameterMode.IMMEDIATE_MODE) { return operands; }
  190. return operands.map((operand) => this.stack.Get(operand));
  191. }
  192. /**
  193. * Execute the Add opcode
  194. *
  195. * Adds two numbers and stores the result at the provided position
  196. * on the stack.
  197. *
  198. * Parses the operand Parameter Mode out of the opcode used to make
  199. * this call.
  200. *
  201. * @param {number} rawOpcode The opcode in memory used to make this call
  202. * @returns {void}
  203. */
  204. Operation_Add(rawOpcode) {
  205. const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  206. const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2);
  207. const operandLeft = this.stack.Next().Get(operandLeftMode);
  208. const operandRight = this.stack.Next().Get(operandRightMode);
  209. const outputPosition = this.stack.Next().Get(ComputerParameterMode.IMMEDIATE_MODE);
  210. const newValue = operandLeft + operandRight;
  211. this.stack.Put(outputPosition, newValue);
  212. }
  213. /**
  214. * Execute the Multiply opcode
  215. *
  216. * Multiplies two numbers and stores the result at the provided
  217. * position on the stack.
  218. *
  219. * @param {number} rawOpcode The opcode in memory used to make this call
  220. * @returns {void}
  221. */
  222. Operation_Multiply(rawOpcode) {
  223. const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  224. const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2);
  225. const operandLeft = this.stack.Next().Get(operandLeftMode);
  226. const operandRight = this.stack.Next().Get(operandRightMode);
  227. const outputPosition = this.stack.Next().Get(ComputerParameterMode.IMMEDIATE_MODE);
  228. const newValue = operandLeft * operandRight;
  229. this.stack.Put(outputPosition, newValue);
  230. }
  231. /**
  232. * Execute the Input opcode
  233. *
  234. * Checks to see if the computer's `runtimeInput` is set. If so, uses that
  235. * value as the input, and stores that at a specified address, and then
  236. * empties the `runtimeInput` value. If `runtimeInput` is empty, and if
  237. * the computer is set to accept input from the console, prompts the
  238. * user for input.
  239. *
  240. * @returns {void}
  241. */
  242. Operation_Input() {
  243. const outputPosition = this.stack.Next().Get(ComputerParameterMode.IMMEDIATE_MODE);
  244. /** A variable to store the input in before putting it on the stack */
  245. let userInput;
  246. /** The input mode to use to get the value */
  247. let inputMode = this.options.inputModeMap.shift();
  248. // If no input mode was found, attempt to set on using the runtime options
  249. if (inputMode === undefined) {
  250. if (this.options.inputFromConsole) { inputMode = InputModes.INPUT_FROM_CONSOLE; }
  251. else { inputMode = InputModes.INPUT_FROM_RUNTIME_STACK; }
  252. }
  253. // Get the input based on the input mode
  254. switch (inputMode) {
  255. case InputModes.INPUT_FROM_RUNTIME_STACK: {
  256. userInput = this.runtimeInput.shift();
  257. if (userInput === undefined) {
  258. throw new Error("runtime Input array empty");
  259. }
  260. break;
  261. }
  262. case InputModes.INPUT_FROM_CONSOLE: {
  263. do {
  264. userInput = Number(prompt("Please enter a number: "));
  265. } while (Number.isNaN(userInput));
  266. break;
  267. }
  268. default:
  269. throw new Error("No input found");
  270. }
  271. // Put the input value onto the stack
  272. this.stack.Put(outputPosition, userInput);
  273. }
  274. /**
  275. * Execute the OUTPUT opcode
  276. *
  277. * @param {number} rawOpcode The opcode in memory used to make this call
  278. * @returns {void}
  279. */
  280. Operation_Output(rawOpcode) {
  281. const currAddress = this.options.outputToConsole ? this.stack.pointer : undefined;
  282. const outputPositionMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  283. const output = this.stack.Next().Get(outputPositionMode);
  284. if (this.options.outputToConsole) {
  285. console.log(`OUTPUT FROM ADDRESS ${currAddress}: ${output}`);
  286. }
  287. else if (this.outputComputer !== null) {
  288. this.outputComputer.RunWithInput(output);
  289. }
  290. else {
  291. this.outputValues.push(output);
  292. }
  293. }
  294. /**
  295. * Execute the Jump_If_True and Jump_If_False opcodes
  296. *
  297. * Jumps to a given address in memory if the value at next address is memory matches
  298. * the given true/false condition.
  299. *
  300. * @param {number} rawOpcode The opcode in memory used to make this call
  301. * @param {boolean} testCondition The value the memory value should be compared against
  302. * @returns {void}
  303. */
  304. Operation_JumpIf(rawOpcode, testCondition) {
  305. const paramMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  306. const jumpAddressMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2);
  307. const param = this.stack.Next().Get(paramMode);
  308. const jumpAddress = this.stack.Next().Get(jumpAddressMode);
  309. const performJump = !!param == testCondition;
  310. if (performJump) {
  311. this.skipNext = true;
  312. this.stack.SetPointerAddress(jumpAddress);
  313. }
  314. }
  315. /**
  316. * Execute the various equality checking opcodes
  317. *
  318. * @param {number} rawOpcode The opcode in memory used to make this call
  319. * @param {number} testCondition The type of equality check to perform as defined in the computer's constructor
  320. * @returns {void}
  321. */
  322. Operation_Equality(rawOpcode, testCondition) {
  323. const operandLeftMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 1);
  324. const operandRightMode = ComputerParameterMode.ParseParameterMode(rawOpcode, 2);
  325. const operandLeft = this.stack.Next().Get(operandLeftMode);
  326. const operandRight = this.stack.Next().Get(operandRightMode);
  327. const outputPosition = this.stack.Next().Get(ComputerParameterMode.IMMEDIATE_MODE);
  328. let testPassed = false;
  329. switch (testCondition) {
  330. case this.EQUALITY.EQUALS:
  331. testPassed = operandLeft == operandRight;
  332. break;
  333. case this.EQUALITY.LESS_THAN:
  334. testPassed = operandLeft < operandRight;
  335. break;
  336. default:
  337. break;
  338. }
  339. this.stack.Put(outputPosition, Number(testPassed));
  340. }
  341. /**
  342. * Outputs the computer's stack to the console
  343. *
  344. * @param {boolean} [highlightPointer=false] Should the memory address of the current pointer be highlighted
  345. * @returns {void}
  346. */
  347. DumpMemory(highlightPointer = false) {
  348. let memory = this.stack.Dump();
  349. if (highlightPointer) {
  350. memory = memory.map((instruction, idx) => (idx == this.stack.pointer ? `{${instruction}}` : instruction));
  351. }
  352. console.log(util.inspect(memory, { breakLength: Infinity, colors: true, compact: true }));
  353. }
  354. /**
  355. * Get a value from the unhandled OUTPUT values in FIFO order
  356. *
  357. * @returns {number} An unhandled value from an output call
  358. */
  359. FetchOutputValue() {
  360. return this.outputValues.shift();
  361. }
  362. /**
  363. * Resets the computer's memory to the value it was created with
  364. *
  365. * @returns {void}
  366. */
  367. Reset() {
  368. this.stack = new Stack(this._initialMemory);
  369. this.outputValues = null;
  370. this.outputComputer = null;
  371. this.options.inputModeMap = DeepClone(this.options._initialInputModeMap);
  372. }
  373. /**
  374. * Sets the computer's memory to a new stack
  375. *
  376. * Note: This resets the computer's initial memory, so `Reset` will use this value
  377. *
  378. * @param {number[]} stack The new memory stack for the computer
  379. * @returns {void}
  380. */
  381. SetMemory(stack) {
  382. this._initialMemory = DeepClone(stack);
  383. this.stack = new Stack(stack);
  384. }
  385. };