Computer.js 11 KB

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