diff --git a/2024/ruby/day_16.rb b/2024/ruby/day_16.rb
new file mode 100644
index 0000000..30b96f2
--- /dev/null
+++ b/2024/ruby/day_16.rb
@@ -0,0 +1,62 @@
+tiles = DATA.readlines(chomp: true).flat_map.with_index {|row, y|
+ row.chars.filter_map.with_index {|tile, x|
+ tile == ?# ? nil : [[y,x], tile]
+ }
+}.to_h
+
+start, stop = %w[ S E ].map {|t| tiles.find { _2 == t }[0] }
+
+scores = Hash.new(Float::INFINITY)
+scores[[start, [0,1]]] = 0
+
+queue = [[start, [0,1]]]
+visited = Set.new
+loop do
+ fail if queue.empty?
+
+ current, dir = queue.shift
+ visited << [current, dir]
+ score = scores.fetch([current, dir])
+ break if current == stop
+
+ # straight ahead
+ yx = current.zip(dir).map { _1 + _2 }
+ if tiles.has_key?(yx)
+ scores[[yx, dir]] = [scores[[yx, dir]], score + 1].min
+ queue << [yx, dir] unless visited.include?([yx, dir])
+ end
+
+ # sides
+ [-1, 1].each do |d|
+ delta = dir.reverse.zip([d, d]).map { _1 * _2 }
+ yx = current.zip(delta).map { _1 + _2 }
+ if tiles.has_key?(yx)
+ scores[[yx, delta]] = [scores[[yx, delta]], score + 1001].min
+ queue << [yx, delta] unless visited.include?([yx, delta])
+ end
+ end
+
+ queue.uniq!
+ queue.sort_by! { scores.fetch(_1) }
+end
+
+pp scores.find {|(yx,_),_| yx == stop }[1]
+
+__END__
+#################
+#...#...#...#..E#
+#.#.#.#.#.#.#.#^#
+#.#.#.#...#...#^#
+#.#.#.#.###.#.#^#
+#>>v#.#.#.....#^#
+#^#v#.#.#.#####^#
+#^#v..#.#.#>>>>^#
+#^#v#####.#^###.#
+#^#v#..>>>>^#...#
+#^#v###^#####.###
+#^#v#>>^#.....#.#
+#^#v#^#####.###.#
+#^#v#^........#.#
+#^#v#^#########.#
+#S#>>^..........#
+#################