Skip to content

Commit b0280f6

Browse files
author
Robb Kidd
committed
day 10, part 1
1 parent 95b7326 commit b0280f6

File tree

3 files changed

+430
-0
lines changed

3 files changed

+430
-0
lines changed

2023/ruby/day10.rb

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
require_relative 'day'
2+
require_relative 'grid'
3+
require_relative 'ugly_sweater'
4+
5+
class Day10 < Day # >
6+
include UglySweater
7+
# @example
8+
# day.part1 #=> 4
9+
# @example more complex input
10+
# day = Day10.new(MORE_COMPLEX_INPUT)
11+
# day.part1 #=> 8
12+
def part1
13+
pipe_maze = PipeMaze.new(input)
14+
# puts pipe_maze.to_ugly_sweater
15+
pipe_maze.loop_path.length / 2
16+
end
17+
18+
# @example
19+
# day.part2 #=> 'how are you'
20+
def part2
21+
end
22+
23+
EXAMPLE_INPUT = <<~INPUT
24+
.....
25+
.S-7.
26+
.|.|.
27+
.L-J.
28+
.....
29+
INPUT
30+
31+
MORE_COMPLEX_INPUT= <<~INPUT
32+
..F7.
33+
.FJ|.
34+
SJ.L7
35+
|F--J
36+
LJ...
37+
INPUT
38+
end
39+
40+
class PipeMaze
41+
attr_reader :start
42+
43+
# @example
44+
# maze = PipeMaze.new(Day10::EXAMPLE_INPUT)
45+
# maze.start #=> [1,1]
46+
def initialize(input)
47+
@input = input
48+
@start = nil
49+
@grid = Grid.new(input)
50+
@grid.parse { |coords, value| @start = coords if value == "S" }
51+
end
52+
53+
def loop_path
54+
@loop_path ||= find_loop
55+
end
56+
57+
def find_loop
58+
probes =
59+
DIRECTION_OFFSETS.keys
60+
.map {|direction|
61+
step_from(start, direction)
62+
}
63+
.compact!
64+
.map {|first_step, direction| [ [start, first_step], direction ] }
65+
66+
while probes do
67+
path, direction = probes.pop
68+
next_coords, direction = step_from(path.last, direction)
69+
70+
case direction
71+
when :north, :south, :west, :east
72+
probes << [(path + [next_coords]), direction]
73+
when :stop
74+
return path
75+
end
76+
end
77+
end
78+
79+
def step_from(coords, direction)
80+
maybe_next_coords = towards(coords, direction)
81+
return nil unless @grid.cover?(maybe_next_coords)
82+
83+
maybe_next_direction = PIPE_FLOWS.fetch([direction, at(maybe_next_coords)], nil)
84+
return nil unless maybe_next_direction
85+
[
86+
maybe_next_coords,
87+
maybe_next_direction
88+
]
89+
end
90+
91+
def at(coords)
92+
@grid.at(coords)
93+
end
94+
95+
def towards(from_coords, direction)
96+
raise("what kind of direction is #{direction.inspect}?") if !DIRECTION_OFFSETS.keys.include?(direction)
97+
from_coords
98+
.zip(DIRECTION_OFFSETS[direction])
99+
.map { |p| p.reduce(&:+) }
100+
end
101+
102+
DIRECTION_OFFSETS = {
103+
north: [-1, 0],
104+
south: [ 1, 0],
105+
west: [ 0,-1],
106+
east: [ 0, 1],
107+
stop: [ 0, 0],
108+
}
109+
110+
# [step travel direction, pipe type] => next step travel direction
111+
PIPE_FLOWS = {
112+
# | is a vertical pipe connecting north and south.
113+
[:north, "|"] => :north,
114+
[:south, "|"] => :south,
115+
# - is a horizontal pipe connecting east and west.
116+
[:east, "-"] => :east,
117+
[:west, "-"] => :west,
118+
# L is a 90-degree bend connecting north and east.
119+
[:south, "L"] => :east,
120+
[:west, "L"] => :north,
121+
# J is a 90-degree bend connecting north and west.
122+
[:south, "J"] => :west,
123+
[:east, "J"] => :north,
124+
# 7 is a 90-degree bend connecting south and west.
125+
[:east, "7"] => :south,
126+
[:north, "7"] => :west,
127+
# F is a 90-degree bend connecting south and east.
128+
[:west, "F"] => :south,
129+
[:north, "F"] => :east,
130+
# . is ground; there is no pipe in this tile.
131+
"." => :cannot_enter_ground,
132+
# S is the starting position of the animal;
133+
# there is a pipe on this tile, but your sketch doesn't
134+
# show what shape the pipe has.
135+
# Let start be entered from any direction and then
136+
# there are no further steps.
137+
[:north, "S"] => :stop,
138+
[:south, "S"] => :stop,
139+
[:east, "S"] => :stop,
140+
[:west, "S"] => :stop,
141+
}
142+
143+
def to_ugly_sweater
144+
@grid.to_s { |coords, value|
145+
if loop_path.include?(coords)
146+
value.make_it_red
147+
else
148+
value.make_it_green
149+
end
150+
}
151+
end
152+
end

2023/ruby/grid.rb

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

0 commit comments

Comments
 (0)