Computer.js 13 KB

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