Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ ruby file: "./.ruby-version"
gem "thor", "~> 1.3"

gem "rubocop", "~> 1.68"

gem 'minitest'
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ GEM
ast (2.4.2)
json (2.8.1)
language_server-protocol (3.17.0.3)
minitest (5.25.4)
parallel (1.26.3)
parser (3.3.6.0)
ast (~> 2.4.1)
Expand Down Expand Up @@ -32,6 +33,7 @@ PLATFORMS
x86_64-darwin-23

DEPENDENCIES
minitest
rubocop (~> 1.68)
thor (~> 1.3)

Expand Down
91 changes: 91 additions & 0 deletions katas/generic_falling_blocks_game_solver/murad/solver/plane.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# frozen_string_literal: true

require 'matrix'

class Plane
Block = Class.new(Object) do
def to_i = raise NotImplementedError
def to_s = raise NotImplementedError

def taken? = raise NotImplementedError
def empty? = !taken?
end
EmptyBlock = Class.new(Block) do
def to_i = 0
def to_s = ' '

def taken? = false
end
TakenBlock = Class.new(Block) do
def to_i = 1
def to_s = '▓▓'

def taken? = true
end

MalformedPlane = Class.new(StandardError)

def self.from_string(string_plane)
lines = string_plane.split("\n")
line_length = lines.max { _1.size }.size / 2
rows = Array.new(lines.length) { Array.new(line_length) }
lines.each_with_index do |line, line_index|
line.ljust(line_length * 2).chars.each_slice(2).with_index do |tuple, tuple_index|
rows[line_index][tuple_index] = \
case tuple
in ['▓','▓']
TakenBlock.new
in [' ', ' ']
EmptyBlock.new
else
raise MalformedPlane
end
end
end

new(Matrix[*rows])
end

def initialize(matrix)
@matrix = matrix
end

def at(row, col) = matrix[row, col]

def row_size = matrix.row_size
def col_size = matrix.column_size

def each_with_index
matrix.each_with_index do |elem, row, col|
yield elem, row, col
end
end

def rotate(n)
n %= 4
case n
when 0
self.class.new(matrix)
when 1
self.class.new(Matrix.build(col_size, row_size) { |row, col| matrix[row_size - col - 1, row] })
when 2
self.class.new(Matrix.build(row_size, col_size) { |row, col| matrix[row_size - row - 1, col_size - col - 1] })
when 3
self.class.new(Matrix.build(col_size, row_size) { |row, col| matrix[col, col_size - row - 1] })
end
end

def level?
matrix.row_vectors.all? do |row_vector|
row_vector.to_a.map(&:to_i).uniq.size == 1
end
end

def to_s
matrix.row_vectors.map { _1.to_a.join('') }.join("\n")
end

private

attr_reader :matrix
end
95 changes: 95 additions & 0 deletions katas/generic_falling_blocks_game_solver/murad/solver/shape.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

require_relative './plane'

class Shape
def self.canonical_shape = raise NotImplementedError
def self.number_of_orientations = raise NotImplementedError
def self.name = raise NotImplementedError

def initialize(orientation:)
@orientation = orientation
end

attr_reader :orientation

def height = plane.row_size
def width = plane.col_size
def at(row, col) = plane.at(row, col)

def to_s = plane.to_s

def each_with_index
plane.each_with_index do |elem, row, col|
yield elem, row, col
end
end

def taken_blocks_per_row
result = Array.new(plane.row_size, 0)
each_with_index { |elem, row, _col| result[row] += elem.to_i }
result
end

private
def plane = Plane.from_string(self.class.canonical_shape).rotate(orientation - 1)
end

class AvailableShapes
def self.call = Shape.subclasses
end

class Square < Shape
def self.canonical_shape
<<~SHAPE
▓▓▓▓
▓▓▓▓
SHAPE
end

def self.number_of_orientations = 1

def self.name = 'Square'
end

class Line < Shape
def self.canonical_shape
<<~SHAPE
▓▓
▓▓
▓▓
▓▓
SHAPE
end

def self.number_of_orientations = 2

def self.name = 'Line'
end

class Z < Shape
def self.canonical_shape
<<~SHAPE
▓▓▓▓
▓▓▓▓
SHAPE
end

def self.number_of_orientations = 2

def self.name = 'Z'
end

class L < Shape
def self.canonical_shape
<<~SHAPE
▓▓
▓▓
▓▓▓▓
SHAPE
end

def self.number_of_orientations = 4

def self.name = 'L'
end
100 changes: 100 additions & 0 deletions katas/generic_falling_blocks_game_solver/murad/solver/solver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

require_relative './plane'
require_relative './shape'

class Solver
MultiplePossibleSolutionsError = Class.new(StandardError)

def initialize(game_plane)
@plane = Plane.from_string(game_plane)
return recommend_any if plane.level?

@recommendations = []
solve!
find_lowest_row_recommendations!
if recommendations.size > 1
debug_state_msg = <<~MSG
Pretty sure all of them are equally viable. See for yourself:
PLANE:
#{plane}
RECOMMENDATIONS:
#{recommendations.map(&:to_debug_s).join("\n")}
MSG
raise MultiplePossibleSolutionsError, debug_state_msg

end
@the_recommendation = recommendations.first
end

def recommended_block
the_recommendation.shape_name
end

def recommended_orientation
the_recommendation.orientation
end

private

attr_reader :plane, :recommendations, :the_recommendation

def solve!
AvailableShapes.call.each do |shape_class|
shape_class.number_of_orientations.times do |orientation|
shape = shape_class.new(orientation: orientation + 1)

0.upto(plane.row_size - shape.height) do |row_idx|
0.upto(plane.col_size - shape.width) do |col_idx|
next unless shape_fits_at?(shape, row_idx, col_idx)

recommendations << Recommendation.new(
shape_name: shape.class.name,
orientation: shape_orientation_recommendation(shape),
row_idx:,
col_idx:,
max_depth: row_idx + shape.height,
taken_blocks_per_row: shape.taken_blocks_per_row,
shape:,
)
end
end
end
end
end

def shape_fits_at?(shape, row, col)
shape.each_with_index do |elem, erow, ecol|
return false if elem.to_i + plane.at(row + erow, col + ecol).to_i > 1
end
true
end

def shape_orientation_recommendation(shape)
return :any if shape.class.number_of_orientations == 1

"orientation_#{shape.orientation}".to_sym
end

def find_lowest_row_recommendations!
lowest_end = recommendations.map(&:max_depth).max
recommendations.reject! { _1.max_depth != lowest_end }

lowest_start = recommendations.map(&:row_idx).max
recommendations.reject! { _1.row_idx != lowest_start }

# taken_blocks_per_row will be of equal sizes here
recommendations.first.taken_blocks_per_row.size.downto(1) do |row_idx|
biggest_value = recommendations.map { _1.taken_blocks_per_row[row_idx - 1] }.max
recommendations.reject! { _1.taken_blocks_per_row[row_idx - 1] != biggest_value }
end
end

def recommend_any
@the_recommendation = Recommendation.new(shape_name: 'Any', orientation: :any)
end
end

Recommendation = Struct.new(:shape_name, :orientation, :row_idx, :col_idx, :max_depth, :taken_blocks_per_row, :shape, keyword_init: true) do
def to_debug_s = to_s + "\n" + shape.to_s
end
Loading
Loading