Code for the Advent of Code event
aoc
advent-of-code
1#!/usr/bin/env ruby
2# frozen_string_literal: true
3
4require 'colorize'
5require 'matrix'
6
7require_relative '../../../lib/aoc/intcode/cpu'
8
9DEBUG = ENV.fetch('DEBUG', nil)
10
11class Vector
12 def x; self[0]; end
13 def y; self[1]; end
14end
15
16def term_clear
17 print "\e[H"
18end
19
20def term_setpos(x, y)
21 print "\e[#{y},#{x}H"
22end
23
24PATH = 'input'
25DRAW = !ARGV.empty?
26
27ORIGIN = Vector[0, 0]
28
29DIRECTIONS = {
30 north: 1,
31 south: 2,
32 west: 3,
33 east: 4
34}.freeze
35
36TRANSFORMS = {
37 north: Vector[0, -1],
38 south: Vector[0, 1],
39 west: Vector[-1, 0],
40 east: Vector[1, 0]
41}.freeze
42
43STATUSES = {
44 0 => :wall,
45 1 => :empty,
46 2 => :oxygen
47}.freeze
48
49TILES = {
50 wall: '#',
51 oxygen: 'O',
52 empty: '.',
53 droid: 'D',
54 unknown: ' '
55}.freeze
56
57SIMPLE_TILES = {
58 origin: '██'.magenta,
59 wall: '██',
60 oxygen: '██'.green,
61 empty: ' ',
62 droid: 'DD',
63 deadend: '██'.red,
64 deadpath: '██'.yellow,
65 path: '██'.light_black,
66 unknown: '██'
67}.freeze
68
69# grid = Hash.new :unknown
70
71def set_status(text) # rubocop:disable Naming/AccessorMethodName
72 Curses.setpos 0, 0
73 Curses.clrtoeol
74 Curses.addstr text
75end
76
77class Bot
78 attr_reader :position
79
80 def initialize(cpu, position = Vector[0, 0])
81 @cpu = cpu
82 @position = position
83 @active = true
84 end
85
86 def dup
87 Bot.new @cpu.dup, @position
88 end
89
90 def move!(direction)
91 return unless @active
92 value = DIRECTIONS[direction]
93 @cpu.input! value
94 @cpu.run!
95 status = STATUSES[@cpu.output[-1]]
96 @cpu.clear_output!
97 @position += TRANSFORMS[direction]
98 @active = false if status == :wall
99 status
100 end
101
102 def active?
103 @active
104 end
105
106 def kill!
107 @active = false
108 end
109end
110
111Bounds = Struct.new :top, :bottom, :left, :right, :offset
112
113class Map
114 attr_reader :oxygen_position, :oxygen_distance, :fill_time
115
116 def initialize(base_cpu)
117 @base_cpu = base_cpu
118 @grid = Hash.new :unknown
119 @bots = Set.new
120 first_bot = Bot.new base_cpu.dup
121 @bots.add first_bot
122 end
123
124 def step!
125 new_bots = []
126 deleted_bots = []
127 @bots.each do |bot|
128 DIRECTIONS.each_key do |direction|
129 clone = bot.dup
130 status = clone.move! direction
131
132 if @grid[clone.position] == :unknown
133 @grid[clone.position] = status
134 new_bots << clone unless status == :wall
135 end
136 end
137
138 deleted_bots << bot
139 end
140
141 @bots.subtract deleted_bots
142 @bots.merge new_bots
143
144 draw_simple
145 end
146
147 def process!
148 empty_poses = @grid.select { |_, v| v == :empty }.map(&:first)
149
150 empty_poses.each do |pos|
151 if pos == ORIGIN
152 @grid[pos] = :origin
153 elsif deadend? pos
154 @grid[pos] = :deadend
155 end
156 end
157
158 draw_simple
159
160 # Calculate deadend paths
161 deadends = @grid.select { |_, v| v == :deadend }.map(&:first)
162 deadends.each do |de|
163 process_deadend! de
164 draw_simple
165 end
166
167 path_cells = @grid.select { |_, v| v == :empty }.map(&:first)
168 path_cells.each do |pos|
169 @grid[pos] = :path
170 end
171
172 @oxygen_position = calc_oxygen_position
173 @oxygen_distance = calc_oxygen_distance
174 @processed = true
175 fill!
176 end
177
178 def fill!
179 process! unless @processed
180
181 time = process_fill! @oxygen_position, 0
182 draw_simple
183
184 @fill_time = time
185 end
186
187 def process_deadend!(pos)
188 others = surroundings pos
189 return if others[:empty] > 1
190 @grid[pos] = :deadpath if @grid[pos] == :empty
191
192 dirs = [
193 pos + TRANSFORMS[:north],
194 pos + TRANSFORMS[:south],
195 pos + TRANSFORMS[:west],
196 pos + TRANSFORMS[:east]
197 ]
198
199 pos = dirs.find { |p| @grid[p] == :empty }
200 process_deadend! pos
201 end
202
203 def process_fill!(pos, time)
204 return time if @grid[pos] == :wall
205 return time if @grid[pos] == :oxygen && pos != @oxygen_position
206 others = surroundings pos
207 @grid[pos] = :oxygen
208 return time if others[:oxygen] + others[:wall] == 4
209
210 draw_simple
211
212 dirs = [
213 pos + TRANSFORMS[:north],
214 pos + TRANSFORMS[:south],
215 pos + TRANSFORMS[:west],
216 pos + TRANSFORMS[:east]
217 ]
218
219 dirs.map { |dir| process_fill! dir, time + 1 }.max
220 end
221
222 def done?
223 @bots.empty?
224 end
225
226 def calc_oxygen_position
227 @grid.find { |_, v| v == :oxygen }.first
228 end
229
230 def calc_oxygen_distance
231 @grid.count { |_, v| v == :path } + 1
232 end
233
234 def draw_simple(only_if_debug: true)
235 return unless DRAW
236 return if only_if_debug && !DEBUG
237 term_clear
238 bounds = calc_bounds
239 (bounds.top..bounds.bottom).each do |y|
240 (bounds.left..bounds.right).each do |x|
241 pos = Vector[x, y]
242 type = @grid[pos]
243 print SIMPLE_TILES[type]
244 end
245 puts
246 end
247 end
248
249 private
250
251 def surroundings(pos)
252 others = [
253 @grid[pos + TRANSFORMS[:north]],
254 @grid[pos + TRANSFORMS[:south]],
255 @grid[pos + TRANSFORMS[:west]],
256 @grid[pos + TRANSFORMS[:east]]
257 ]
258
259 Hash.new(0).tap { |h| others.each { |t| h[t] += 1 } }
260 end
261
262 def calc_bounds
263 xs = @grid.keys.map(&:x)
264 ys = @grid.keys.map(&:y)
265 min_x = xs.min
266 max_x = xs.max
267 min_y = ys.min
268 max_y = ys.max
269 x_offset = min_x < 0 ? min_x.abs : 0
270 y_offset = min_y < 0 ? min_y.abs : 0
271 Bounds.new min_y, max_y, min_x, max_x, Vector[x_offset, y_offset]
272 end
273
274 def deadend?(pos)
275 surroundings(pos)[:wall] == 3
276 end
277end
278
279base_cpu = AoC::Intcode::CPU.new.load!(PATH).print_output!(false)
280
281bots = Set.new
282# cpus = Set.new
283
284bots.add Bot.new base_cpu.dup
285
286map = Map.new base_cpu
287
288map.step! until map.done?
289
290map.process!
291map.draw_simple(false)
292
293puts "Part 1: #{map.oxygen_distance}"
294puts "Part 2: #{map.fill_time}"