Skip to content

Commit 4666014

Browse files
committed
over-wrought day 4
Grid back from last year!
1 parent 8ea8e87 commit 4666014

File tree

2 files changed

+323
-0
lines changed

2 files changed

+323
-0
lines changed

2024/ruby/day04.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
require_relative 'day'
2+
require_relative 'grid'
3+
4+
class Day04 < Day # >
5+
6+
# @example
7+
# day.part1 #=> 18
8+
def part1
9+
exes = []
10+
word_search =
11+
Grid.new(input, raise_on_out_of_bounds: false)
12+
.parse do |coords, char|
13+
exes << coords if char == 'X'
14+
end
15+
16+
exes
17+
.map { |x_location|
18+
IN_ALL_DIRECTIONS.map { |offset|
19+
(1..3).each_with_object([x_location]) { |step, xmas_check|
20+
xmas_check << step_in_direction(x_location, offset.map { |d| d * step })
21+
}
22+
}
23+
.map {|maybe_xmas| maybe_xmas.map { |l| word_search.at(l)}.join }
24+
.keep_if { _1 == "XMAS" }
25+
}
26+
.flatten
27+
.count
28+
end
29+
30+
# @example
31+
# IN_ALL_DIRECTIONS #=> [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
32+
IN_ALL_DIRECTIONS = [-1, -1, 0, 1, 1].permutation(2).to_a.uniq.freeze
33+
34+
# @example
35+
# day.part2 #=> 9
36+
def part2
37+
ays = []
38+
word_search =
39+
Grid.new(input)
40+
.parse do |coords, char|
41+
ays << coords if char == 'A'
42+
end
43+
44+
ays
45+
.keep_if { |a_location|
46+
begin
47+
["M", "S"] == [ word_search.at(step_in_direction(a_location, [-1,-1])),
48+
word_search.at(step_in_direction(a_location, [ 1, 1])) ].sort &&
49+
["M", "S"] == [ word_search.at(step_in_direction(a_location, [-1, 1])),
50+
word_search.at(step_in_direction(a_location, [ 1,-1])) ].sort
51+
rescue Grid::OutOfBounds
52+
false
53+
end
54+
}
55+
.count
56+
end
57+
58+
# @example
59+
# day.step_in_direction([3,5], [-1,1]) #=> [2,6]
60+
def step_in_direction(location, vector)
61+
location
62+
.zip(vector)
63+
.map { |l| l.reduce(&:+) }
64+
end
65+
66+
EXAMPLE_INPUT = File.read("../inputs/day04-example-input.txt")
67+
end

2024/ruby/grid.rb

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
class Grid
2+
include Enumerable
3+
4+
attr_reader :row_bounds, :column_bounds
5+
attr_accessor :raise_on_out_of_bounds
6+
7+
class OutOfBounds < RuntimeError ; end
8+
9+
def initialize(input, raise_on_out_of_bounds: true)
10+
@input = input
11+
@raise_on_out_of_bounds = raise_on_out_of_bounds
12+
@the_grid = Hash.new { |(r, c)| raise "No data parsed." }
13+
end
14+
15+
def each
16+
@the_grid.each { |coords, value| yield coords, value }
17+
end
18+
19+
# @example
20+
# grid = new("")
21+
# grid.at([0,0]) #=> raise("No data loaded.")
22+
# grid.set([0,0], ".")
23+
# grid.at([0,0]) #=> "."
24+
def set(coords, value)
25+
@the_grid[coords] = value
26+
end
27+
28+
# @example
29+
# grid = new("S..\n.\n..")
30+
# grid.at([0,0]) #=> raise "No data parsed."
31+
# grid.parse
32+
# grid.at([0,0]) #=> "S"
33+
# grid.at([-100,-100]) #=> raise KeyError, "Coordinates not found on grid: [-100, -100]"
34+
def at(coords)
35+
(@value_transform_proc || DEFAULT_VALUE_TRANFORM_PROC).call(
36+
@the_grid[coords]
37+
)
38+
end
39+
40+
DEFAULT_VALUE_TRANFORM_PROC = proc { |v| v }
41+
42+
def values_are_integers
43+
set_value_transform_proc { |v| v.to_i }
44+
end
45+
46+
def set_value_transform_proc(&block)
47+
raise "#{__method__} must be called with a block." unless block_given?
48+
@value_transform_proc = block
49+
50+
self
51+
end
52+
53+
def cover?(coords)
54+
raise "No data loaded." unless (@row_bounds && @column_bounds)
55+
@row_bounds.cover?(coords[0]) && @column_bounds.cover?(coords[1])
56+
end
57+
58+
def manhattan_distance(here, there)
59+
[here, there].transpose.map { |a, b| (a - b).abs }.reduce(&:+)
60+
end
61+
62+
OFFSET_TO_DIRECTION = {
63+
# r c
64+
[-1, 0] => "^", # up a row
65+
[1, 0] => "v", # down a row
66+
[0, -1] => "<", # left a column
67+
[0, 1] => ">" # right a column
68+
}
69+
70+
# @example exception when grid hasn't been populated
71+
# grid = new(Day12::EXAMPLE_INPUT)
72+
# grid.neighbors_for([2,5]) #=> raise "No data loaded."
73+
#
74+
# @example in bounds
75+
# grid = new(Day12::EXAMPLE_INPUT).parse
76+
# grid.neighbors_for([2,5]) #=> [ [1,5], [3,5], [2,4], [2,6] ]
77+
#
78+
# @example on the edge
79+
# grid = new(Day12::EXAMPLE_INPUT).parse
80+
# grid.neighbors_for([0,0]) #=> [ [1,0], [0,1] ]
81+
def neighbors_for(coords)
82+
OFFSET_TO_DIRECTION
83+
.keys
84+
.map { |offset| coords.zip(offset).map { |p| p.reduce(&:+) } }
85+
.select { |neighbor_coords| self.cover? neighbor_coords }
86+
end
87+
88+
# default cost to step to a neighbor
89+
# if some neighbors ought to be excluded outright, return Float::INFINITY as their cost
90+
DEFAULT_STEP_COST_CALCULATOR_PROC =
91+
proc { |_grid, _from_coords, _to_coords| 1 }
92+
93+
# just in case you want to keep multiple procs around and swap them in and out
94+
attr_writer :step_cost_calculator_proc
95+
96+
# or use this to chain setting it during grid initialization
97+
def set_step_cost_calculator(&block)
98+
raise "#{__method__} must be called with a block." unless block_given?
99+
@step_cost_calculator_proc = block
100+
101+
self
102+
end
103+
104+
def shortest_path_between(start, goal, &block)
105+
cost_calculator =
106+
if block_given?
107+
block
108+
elsif @step_cost_calculator_proc
109+
@step_cost_calculator_proc
110+
else
111+
DEFAULT_STEP_COST_CALCULATOR_PROC
112+
end
113+
114+
backsteps = Hash.new
115+
backsteps[start] = nil # there's no previous step from the start of a path
116+
117+
costs = Hash.new(Float::INFINITY) # until computed, the cost to step to a neighbor is infinitely expensive
118+
costs[start] = 0 # we start here, though, so it's cheap
119+
120+
survey_queue = FastContainers::PriorityQueue.new(:min)
121+
survey_queue.push(start, 0)
122+
while !survey_queue.empty?
123+
check_pos = survey_queue.pop
124+
break if check_pos == goal
125+
126+
neighbors_for(check_pos).each do |neighbor|
127+
neighbor_cost =
128+
costs[check_pos] + cost_calculator.call(self, check_pos, neighbor)
129+
130+
if neighbor_cost < costs[neighbor]
131+
costs[neighbor] = neighbor_cost
132+
backsteps[neighbor] = check_pos
133+
134+
survey_queue.push(neighbor, neighbor_cost)
135+
end
136+
end
137+
end
138+
139+
return [] unless backsteps.include?(goal)
140+
141+
path = [goal]
142+
step_backwards = backsteps[goal]
143+
while step_backwards
144+
path.push(step_backwards)
145+
step_backwards = backsteps[step_backwards]
146+
end
147+
path
148+
end
149+
150+
def render_path(path)
151+
step_directions = path_to_direction_map(path)
152+
153+
self.to_s do |coords, value|
154+
if coords == path.first
155+
at(coords)
156+
elsif dir = step_directions[coords]
157+
"\e[41m\e[1m#{dir}\e[0m"
158+
else
159+
"\e[32m.\e[0m"
160+
end
161+
end
162+
end
163+
164+
def path_to_direction_map(path)
165+
direction_of_travel_from = Hash.new
166+
path.each_cons(2) do |to, from|
167+
offset = [to[0] - from[0], to[1] - from[1]]
168+
direction_of_travel_from[from] = OFFSET_TO_DIRECTION.fetch(offset)
169+
end
170+
direction_of_travel_from
171+
end
172+
173+
# @example
174+
#
175+
def parse
176+
split_input =
177+
case @input
178+
when Array # assume the lines and chars have been split already
179+
@input
180+
when String
181+
@input
182+
.split("\n")
183+
.map { |line| line.chars }
184+
else
185+
raise("don't know how to parse #{input.inspect}")
186+
end
187+
188+
split_input
189+
.each_with_index do |row, r|
190+
row.each_with_index do |char, c|
191+
@the_grid[[r, c]] = char
192+
yield [r, c], char if block_given?
193+
end
194+
end
195+
196+
@the_grid.default_proc =
197+
proc do |_hash, key|
198+
if @raise_on_out_of_bounds
199+
raise OutOfBounds, "Coordinates not found on grid: #{key}"
200+
else
201+
:out_of_bounds
202+
end
203+
end
204+
205+
@row_bounds, @column_bounds =
206+
@the_grid.keys.transpose.map { |dimension| Range.new(*dimension.minmax) }
207+
208+
self
209+
end
210+
211+
# @example by default, stringifies input value for a grid location
212+
# grid = new("abcd\nefgh\n").parse
213+
# grid.to_s #=> "abcd\nefgh\n"
214+
#
215+
# @example transforms with an assigned to_s_proc
216+
# grid = new("abcd\nefgh\n").parse
217+
# grid.to_s #=> "abcd\nefgh\n"
218+
# grid.to_s_proc = proc {|_coords, value| (value.ord + 1).chr }
219+
# grid.to_s #=> "bcde\nfghi\n"
220+
# grid.to_s_proc = nil
221+
# grid.to_s #=> "abcd\nefgh\n"
222+
#
223+
# @example transforms with a given block
224+
# grid = new("abcd\nefgh\n").parse
225+
# grid.to_s #=> "abcd\nefgh\n"
226+
# grid.to_s { |_coords, value| (value.ord + 1).chr } #=> "bcde\nfghi\n"
227+
# grid.to_s #=> "abcd\nefgh\n"
228+
#
229+
# @example block given takes priority over assigned to_s_proc
230+
# grid = new("abcd\nefgh\n").parse
231+
# grid.to_s #=> "abcd\nefgh\n"
232+
# grid.to_s_proc = proc {|_, _| raise "to_s_proc called" }
233+
# grid.to_s #=> raise "to_s_proc called"
234+
# grid.to_s { |_coords, value| (value.ord + 1).chr } #=> "bcde\nfghi\n"
235+
#
236+
attr_writer :to_s_proc
237+
DEFAULT_TO_S_PROC = proc { |_coords, value| value.to_s }
238+
def to_s(&block)
239+
transformer =
240+
if block_given?
241+
block
242+
elsif @to_s_proc
243+
@to_s_proc
244+
else
245+
DEFAULT_TO_S_PROC
246+
end
247+
248+
@row_bounds
249+
.map do |row|
250+
@column_bounds
251+
.map { |column| transformer.call([row, column], at([row, column])) }
252+
.join("")
253+
end
254+
.join("\n") + "\n"
255+
end
256+
end

0 commit comments

Comments
 (0)