Skip to content

Commit 6c1fb04

Browse files
author
Robb Kidd
committed
2022 grid and such
1 parent bd145db commit 6c1fb04

File tree

4 files changed

+305
-3
lines changed

4 files changed

+305
-3
lines changed

2022/ruby/Gemfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
source "https://rubygems.org"
44

5-
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
5+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
66

77
gem "rake"
88
gem "pry"
99
gem "yard-doctest"
10+
gem "rspec"
1011
gem "priority_queue_cxx"
12+
13+
gem "debug"
14+
gem "ruby-lsp", "~> 0.3.7", group: :development

2022/ruby/Gemfile.lock

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,44 @@ GEM
22
remote: https://rubygems.org/
33
specs:
44
coderay (1.1.3)
5+
debug (1.7.0)
6+
irb (>= 1.5.0)
7+
reline (>= 0.3.1)
8+
diff-lcs (1.5.0)
9+
io-console (0.5.11)
10+
irb (1.6.1)
11+
reline (>= 0.3.0)
12+
language_server-protocol (3.17.0.2)
513
method_source (1.0.0)
614
minitest (5.16.3)
15+
prettier_print (1.1.0)
716
priority_queue_cxx (0.3.5)
817
pry (0.14.1)
918
coderay (~> 1.1)
1019
method_source (~> 1.0)
1120
rake (13.0.6)
21+
reline (0.3.2)
22+
io-console (~> 0.5)
23+
rspec (3.12.0)
24+
rspec-core (~> 3.12.0)
25+
rspec-expectations (~> 3.12.0)
26+
rspec-mocks (~> 3.12.0)
27+
rspec-core (3.12.0)
28+
rspec-support (~> 3.12.0)
29+
rspec-expectations (3.12.1)
30+
diff-lcs (>= 1.2.0, < 2.0)
31+
rspec-support (~> 3.12.0)
32+
rspec-mocks (3.12.1)
33+
diff-lcs (>= 1.2.0, < 2.0)
34+
rspec-support (~> 3.12.0)
35+
rspec-support (3.12.0)
36+
ruby-lsp (0.3.7)
37+
language_server-protocol (~> 3.17.0)
38+
sorbet-runtime
39+
syntax_tree (>= 4.0.2, < 5.0.0)
40+
sorbet-runtime (0.5.10590)
41+
syntax_tree (4.3.0)
42+
prettier_print (>= 1.0.2)
1243
webrick (1.7.0)
1344
yard (0.9.28)
1445
webrick (~> 1.7.0)
@@ -18,13 +49,15 @@ GEM
1849

1950
PLATFORMS
2051
arm64-darwin-22
21-
ruby
2252

2353
DEPENDENCIES
54+
debug
2455
priority_queue_cxx
2556
pry
2657
rake
58+
rspec
59+
ruby-lsp (~> 0.3.7)
2760
yard-doctest
2861

2962
BUNDLED WITH
30-
2.3.26
63+
2.3.7

2022/ruby/doctest_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require "grid"
12
require "day"
23
require WhichDay.klass_name.downcase
34

2022/ruby/grid.rb

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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 loaded." }
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("S1234\n56789\n")
27+
# grid.at([0,0]) #=> raise("No data loaded.")
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(EXAMPLE_INPUT)
69+
# grid.neighbors_for([2,5]) #=> raise "No data loaded."
70+
#
71+
# @example in bounds
72+
# grid = new(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(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+
# @example
86+
# grid = new(EXAMPLE_INPUT).parse
87+
# grid.line_between([8,4],[8,7]) #=> [[8,4],[8,5],[8,6],[8,7]]
88+
def line_between(from, to)
89+
[from, to].transpose.map { |dimension_ends| Range.new(dimension_ends) }
90+
end
91+
92+
# default cost to step to a neighbor
93+
# if some neighbors ought to be excluded outright, return Float::INFINITY as their cost
94+
DEFAULT_STEP_COST_CALCULATOR_PROC =
95+
proc { |_grid, _from_coords, _to_coords| 1 }
96+
97+
# just in case you want to keep multiple procs around and swap them in and out
98+
attr_writer :step_cost_calculator_proc
99+
100+
# or use this to chain setting it during grid initialization
101+
def set_step_cost_calculator(&block)
102+
raise "#{__method__} must be called with a block." unless block_given?
103+
@step_cost_calculator_proc = block
104+
105+
self
106+
end
107+
108+
def shortest_path_between(start, goal, &block)
109+
cost_calculator =
110+
if block_given?
111+
block
112+
elsif @step_cost_calculator_proc
113+
@step_cost_calculator_proc
114+
else
115+
DEFAULT_STEP_COST_CALCULATOR_PROC
116+
end
117+
118+
backsteps = Hash.new
119+
backsteps[start] = nil # there's no previous step from the start of a path
120+
121+
costs = Hash.new(Float::INFINITY) # until computed, the cost to step to a neighbor is infinitely expensive
122+
costs[start] = 0 # we start here, though, so it's cheap
123+
124+
survey_queue = FastContainers::PriorityQueue.new(:min)
125+
survey_queue.push(start, 0)
126+
while !survey_queue.empty?
127+
check_pos = survey_queue.pop
128+
break if check_pos == goal
129+
130+
neighbors_for(check_pos).each do |neighbor|
131+
neighbor_cost =
132+
costs[check_pos] + cost_calculator.call(self, check_pos, neighbor)
133+
134+
if neighbor_cost < costs[neighbor]
135+
costs[neighbor] = neighbor_cost
136+
backsteps[neighbor] = check_pos
137+
138+
survey_queue.push(neighbor, neighbor_cost)
139+
end
140+
end
141+
end
142+
143+
return [] unless backsteps.include?(goal)
144+
145+
path = [goal]
146+
step_backwards = backsteps[goal]
147+
while step_backwards
148+
path.push(step_backwards)
149+
step_backwards = backsteps[step_backwards]
150+
end
151+
path
152+
end
153+
154+
def render_path(path)
155+
step_directions = path_to_direction_map(path)
156+
157+
self.to_s do |coords, value|
158+
if coords == path.first
159+
at(coords)
160+
elsif dir = step_directions[coords]
161+
"\e[41m\e[1m#{dir}\e[0m"
162+
else
163+
"\e[32m.\e[0m"
164+
end
165+
end
166+
end
167+
168+
def path_to_direction_map(path)
169+
direction_of_travel_from = Hash.new
170+
path.each_cons(2) do |to, from|
171+
offset = [to[0] - from[0], to[1] - from[1]]
172+
direction_of_travel_from[from] = OFFSET_TO_DIRECTION.fetch(offset)
173+
end
174+
direction_of_travel_from
175+
end
176+
177+
def parse
178+
parse_rows_and_columns
179+
loaded!
180+
181+
self
182+
end
183+
184+
def loaded!
185+
@the_grid.default_proc =
186+
proc do |_hash, key|
187+
raise KeyError, "Coordinates not found on grid: #{key}"
188+
end
189+
190+
@row_bounds, @column_bounds =
191+
@the_grid.keys.transpose.map { |dimension| Range.new(*dimension.minmax) }
192+
193+
self
194+
end
195+
196+
def parse_rows_and_columns
197+
@input
198+
.split("\n")
199+
.map { |line| line.chars }
200+
.each_with_index do |row, r|
201+
row.each_with_index do |char, c|
202+
@the_grid[[r, c]] = char
203+
yield [r, c], char if block_given?
204+
end
205+
end
206+
207+
self
208+
end
209+
210+
attr_writer :to_s_proc
211+
DEFAULT_TO_S_PROC = proc { |_coords, value| value.to_s }
212+
#
213+
# @example by default, stringifies input value for a grid location
214+
# grid = new("abcd\nefgh\n").parse
215+
# grid.to_s #=> "abcd\nefgh\n"
216+
#
217+
# @example transforms with an assigned to_s_proc
218+
# grid = new("abcd\nefgh\n").parse
219+
# grid.to_s #=> "abcd\nefgh\n"
220+
# grid.to_s_proc = proc {|_coords, value| (value.ord + 1).chr }
221+
# grid.to_s #=> "bcde\nfghi\n"
222+
# grid.to_s_proc = nil
223+
# grid.to_s #=> "abcd\nefgh\n"
224+
#
225+
# @example transforms with a given block
226+
# grid = new("abcd\nefgh\n").parse
227+
# grid.to_s #=> "abcd\nefgh\n"
228+
# grid.to_s { |_coords, value| (value.ord + 1).chr } #=> "bcde\nfghi\n"
229+
# grid.to_s #=> "abcd\nefgh\n"
230+
#
231+
# @example block given takes priority over assigned to_s_proc
232+
# grid = new("abcd\nefgh\n").parse
233+
# grid.to_s #=> "abcd\nefgh\n"
234+
# grid.to_s_proc = proc {|_, _| raise "to_s_proc called" }
235+
# grid.to_s #=> raise "to_s_proc called"
236+
# grid.to_s { |_coords, value| (value.ord + 1).chr } #=> "bcde\nfghi\n"
237+
#
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+
257+
EXAMPLE_INPUT = <<~INPUT
258+
Sabqponm
259+
abcryxxl
260+
accszExk
261+
acctuvwj
262+
abdefghi
263+
INPUT
264+
end

0 commit comments

Comments
 (0)