Code for the Advent of Code event
aoc
advent-of-code
1# frozen_string_literal: true
2
3module AoC
4 module Intcode
5 # An Intcode CPU.
6 class CPU
7 # @return [Hash<Integer, Symbol>]
8 OPS = {
9 1 => :add,
10 2 => :mult,
11 3 => :read,
12 4 => :write,
13 5 => :jnz,
14 6 => :jz,
15 7 => :lt,
16 8 => :eq,
17 9 => :modrel,
18 99 => :halt
19 }.freeze
20
21 # @return [Hash<Symbol, Integer>]
22 ARG_COUNTS = {
23 add: 3,
24 mult: 3,
25 read: 1,
26 write: 1,
27 jnz: 2,
28 jz: 2,
29 lt: 3,
30 eq: 3,
31 modrel: 1
32 }.tap { |h| h.default = 0 }.freeze
33
34 # @return [Hash<Integer, Symbol>]
35 MODES = {
36 0 => :addr,
37 1 => :immediate,
38 2 => :relative
39 }.freeze
40
41 # @return [Array<Integer>]
42 attr_reader :memory
43
44 # @return [Array<Integer>]
45 attr_reader :output
46
47 # @return [Queue]
48 attr_reader :input
49
50 # @param program [Array<Integer>]
51 # @param print_output [Boolean]
52 # @param debug [Boolean]
53 # @param memory [Array<Integer>]
54 # @param ip [Integer] Instruction pointer
55 # @param rb [Integer] Relative base
56 def initialize(program = nil, print_output = true, debug = false, memory = nil, ip = 0, rb = 0) # rubocop:disable Style/OptionalBooleanParameter
57 @ip = ip
58 @input = Queue.new
59 @output = []
60 @debug = debug
61 @program = program&.dup
62 @memory = memory || @program&.dup
63 @halted = false
64 @relative_base = rb
65 @print_output = print_output
66 end
67
68 # @param enabled [Boolean]
69 # @return [self]
70 def debug!(enabled)
71 @debug = enabled
72 self
73 end
74
75 # @param file [Array<Integer>, String]
76 # @return [self]
77 def load!(file)
78 if file.is_a? Array # rubocop:disable Style/ConditionalAssignment
79 @program = file
80 else
81 @program = File.read(file).split(',').map(&:to_i)
82 end
83
84 @memory = @program.dup
85 self
86 end
87
88 # @param value [Integer, String, Array<Integer, String>]
89 # @return [self]
90 def input!(value)
91 case value
92 when Array
93 value.each { |v| input! v }
94 when String
95 value.chars.map(&:ord).each { |v| input! v }
96 else
97 @input.enq value
98 end
99 self
100 end
101
102 # @param enabled [Boolean]
103 # @return [self]
104 def print_output!(enabled)
105 @print_output = enabled
106 self
107 end
108
109 # @return [void]
110 def clear_output!
111 @output.clear
112 end
113
114 # @return [self]
115 def run!
116 @running = true
117
118 while @running
119 opcode = OPS[get_opcode]
120 print '%4d: [%05d] %6s ' % [@ip, read_mem(@ip), opcode.to_s.upcase] if @debug
121 result = send(opcode)
122 @ip += 1 + ARG_COUNTS[opcode] unless result == :jumped || result == :block
123 end
124
125 self
126 end
127
128 # @return [self]
129 def reset!
130 @memory = @program.dup
131 @ip = 0
132 @input = Queue.new
133 @output = []
134 self
135 end
136
137 # @return [Boolean]
138 def running?
139 @running == true
140 end
141
142 # @return [Boolean]
143 def halted?
144 @halted == true
145 end
146
147 # @return [CPU]
148 def dup
149 CPU.new @program.dup, @print_output, @debug, @memory.dup, @ip, @relative_base
150 end
151
152 private
153
154 # @return [void]
155 def add
156 a = get_arg 1
157 print ', ' if @debug
158 b = get_arg 2
159 print ', ' if @debug
160 addr = get_addr 3
161 puts if @debug
162 @memory[addr] = a + b
163 end
164
165 # @return [void]
166 def mult
167 a = get_arg 1
168 print ', ' if @debug
169 b = get_arg 2
170 print ', ' if @debug
171 addr = get_addr 3
172 puts if @debug
173 @memory[addr] = a * b
174 end
175
176 # @return [void]
177 def read
178 if @input.empty?
179 @running = false
180 puts '(BLOCK)' if @debug
181 return :block
182 end
183 val = @input.deq
184 addr = get_addr 1
185 puts if @debug
186 @memory[addr] = val
187 end
188
189 # @return [void]
190 def write
191 val = get_arg 1
192 puts if @debug
193 output << val
194 puts val if @print_output
195 end
196
197 # @return [void]
198 def jnz
199 a = get_arg 1
200 print ', ' if @debug
201 addr = get_arg 2
202 puts if @debug
203
204 return if a == 0
205 @ip = addr
206 :jumped
207 end
208
209 # @return [void]
210 def jz
211 a = get_arg 1
212 print ', ' if @debug
213 addr = get_arg 2
214 puts if @debug
215
216 return unless a == 0
217
218 @ip = addr
219 :jumped
220 end
221
222 # @return [void]
223 def lt
224 a = get_arg 1
225 print ', ' if @debug
226 b = get_arg 2
227 print ', ' if @debug
228 addr = get_addr 3
229 puts if @debug
230
231 @memory[addr] = a < b ? 1 : 0
232 end
233
234 # @return [void]
235 def eq
236 a = get_arg 1
237 print ', ' if @debug
238 b = get_arg 2
239 print ', ' if @debug
240 addr = get_addr 3
241 puts if @debug
242
243 @memory[addr] = a == b ? 1 : 0
244 end
245
246 # @return [void]
247 def modrel
248 amount = get_arg 1
249 puts if @debug
250
251 @relative_base += amount
252 end
253
254 # @return [void]
255 def halt
256 @running = false
257 @halted = true
258 puts if @debug
259 end
260
261 # @param addr [Integer]
262 # @return [Integer]
263 def read_mem(addr)
264 @memory[addr] || 0
265 end
266
267 # @return [Integer]
268 def get_opcode # rubocop:disable Naming/AccessorMethodName
269 read_mem(@ip) % 100
270 end
271
272 # @param pos [Integer]
273 # @return [Symbol]
274 def get_mode(pos)
275 MODES[(read_mem(@ip) / (10**(pos + 1))) % 10]
276 end
277
278 # @param pos [Integer]
279 # @return [Integer]
280 def get_addr(pos)
281 val = read_mem(@ip + pos)
282 mode = get_mode pos
283
284 if mode == :relative
285 print "R#{val}=" if @debug
286 val += @relative_base
287 mode = :addr
288 end
289
290 print "##{val}" if mode == :addr && @debug
291
292 val
293 end
294
295 # @param pos [Integer]
296 # @return [Integer]
297 def get_arg(pos)
298 val = read_mem(@ip + pos)
299 mode = get_mode pos
300
301 if mode != :immediate
302 addr = get_addr pos
303 print '=' if @debug
304 val = read_mem addr
305 end
306
307 print val if @debug
308 val
309 end
310 end
311 end
312end