Решение на Трета задача от Георги Шопов

Обратно към всички решения

Към профила на Георги Шопов

Резултати

  • 6 точки от тестове
  • 1 бонус точка
  • 7 точки общо
  • 64 успешни тест(а)
  • 5 неуспешни тест(а)

Код

require 'matrix'
class Matrix
def []=(i, j, x)
@rows[i][j] = x
end
end
module Graphics
module Renderers
private
class Renderer
attr_accessor :visible, :invisible, :line_break
def render_pixel(pixel)
pixel ? @visible : @invisible
end
def render_canvas(canvas)
canvas.each_slice(canvas.width).map do |row|
row.map { |pixel| render_pixel(pixel) } << @line_break
end.join.chomp(@line_break)
end
end
public
class Ascii < Renderer
def initialize
super
@visible = '@'
@invisible = '-'
@line_break = "\n"
end
end
class Html < Renderer
HEADER = '<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">'
FOOTER = ' </div>
</body>
</html>'
def initialize
super
@visible = '<b></b>'
@invisible = '<i></i>'
@line_break = '<br>'
end
def render_canvas(canvas)
HEADER + super + FOOTER
end
end
end
class BresenhamAlgorithm
FIRST_ROTATION = Matrix[ [1, 0], [0, 1] ]
SECOND_ROTATION = Matrix[ [0, 1], [1, 0] ]
THIRD_ROTATION = Matrix[ [1, 0], [0, -1] ]
FOURTH_ROTATION = Matrix[ [0, -1], [1, 0] ]
def initialize(line)
set_rotation_matrix(line.from, line.to)
from = line.from.transform(@rotation_matrix)
to = line.to.transform(@rotation_matrix)
set_variables(from, to)
end
def rasterize
starting_point = Point.new(@point_x, @point_y).transform(@inverse_matrix)
[starting_point] + 1.upto(@delta_x).map do |_|
next_step
Point.new(@point_x, @point_y).transform(@inverse_matrix)
end
end
private
def set_rotation_matrix(from, to)
delta_x = (from.x - to.x).abs
delta_y = (from.y - to.y).abs
if from.y <= to.y
@rotation_matrix = delta_x >= delta_y ? FIRST_ROTATION : SECOND_ROTATION
else
@rotation_matrix = delta_x >= delta_y ? THIRD_ROTATION : FOURTH_ROTATION
end
@inverse_matrix = @rotation_matrix.inverse
end
def set_variables(from, to)
@point_x = from.x
@point_y = from.y
@delta_x = (from.x - to.x).abs
@delta_y = (from.y - to.y).abs
@delta = 2 * @delta_y - @delta_x
end
def next_step
if @delta < 0
@point_x += 1
@delta += 2 * @delta_y
else
@point_x += 1
@point_y += 1
@delta += 2 * @delta_y - 2 * @delta_x
end
end
end
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def each
yield self
end
def transform(matrix)
Point.new(matrix[0, 0] * @x + matrix[0, 1] * @y,
matrix[1, 0] * @x + matrix[1, 1] * @y)
end
def to_s
"Point(#{@x}, #{@y})"
end
def hash
to_s.hash
end
def ==(other)
@x == other.x and @y == other.y
end
alias_method :eql?, :==
end
class Line
attr_reader :from, :to
def initialize(from, to)
@from = from
@to = to
if @from.x > @to.x or (@from.x == @to.x and @from.y > @to.y)
@from, @to = @to, @from
end
end
def each(&block)
rasterize.each(&block)
end
def to_s
"Line(#{@from}, #{@to})"
end
def hash
to_s.hash
end
def ==(other)
@from == other.from and @to == other.to
end
alias_method :eql?, :==
def rasterize
BresenhamAlgorithm.new(self).rasterize
end
end
class Rectangle
attr_reader :left, :right, :top_left, :top_right, :bottom_left, :bottom_right
def initialize(point_a, point_b)
initialize_left_and_right(point_a, point_b)
initialize_vertices
end
def each(&block)
[
Line.new(@top_left, @top_right ),
Line.new(@bottom_left, @bottom_right),
Line.new(@top_left, @bottom_left ),
Line.new(@top_right, @bottom_right),
].map(&:rasterize).flatten.each(&block)
end
def to_s
"Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{@bottom_right})"
end
def hash
to_s.hash
end
def ==(other)
@top_left == other.top_left and @bottom_right == other.bottom_right
end
alias_method :eql?, :==
private
def initialize_left_and_right(point_a, point_b)
@left = point_a
@right = point_b
if point_a.x > point_b.x or point_a.x == point_b.x and point_a.y > point_b.y
@left, @right = @right, @left
end
end
def initialize_vertices
@top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
@top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y )
@bottom_left = @left != @top_left ? @left : Point.new(@left.x, @right.y)
@bottom_right = @right != @top_right ? @right : Point.new(@right.x, @left.y )
end
end
class Canvas
include Enumerable
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@data = Matrix.build(height, width) { false }
end
def each(&block)
@data.each(&block)
end
def set_pixel(x, y)
@data[y, x] = true
end
def pixel_at?(x, y)
@data[y, x]
end
def draw(figure)
figure.each { |point| @data[point.y, point.x] = true }
end
def render_as(renderer)
renderer.new.render_canvas(self)
end
end
end

Лог от изпълнението

..........F.............................................F...F..F...F.

Failures:

  1) Graphics Canvas drawing of shapes and rasterization of points works for multiple ones
     Failure/Error: canvas.set_pixel 4, 4
     NoMethodError:
       undefined method `[]=' for nil:NilClass
     # /tmp/d20131223-4637-4e90s5/solution.rb:5:in `[]='
     # /tmp/d20131223-4637-4e90s5/solution.rb:278:in `set_pixel'
     # /tmp/d20131223-4637-4e90s5/spec.rb:57:in `block (5 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (3 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (2 levels) in <top (required)>'

  2) Graphics shapes Rectangle initialization puts the leftmost point in its left field
     Failure/Error: rect.left.x.should eq 4
       
       expected: 4
            got: 7
       
       (compared using ==)
     # /tmp/d20131223-4637-4e90s5/spec.rb:507:in `block (5 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (3 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (2 levels) in <top (required)>'

  3) Graphics shapes Rectangle comparison for equality is true for rectangles defined with different diagonal corners
     Failure/Error: (a == b).should be_true
       expected: true value
            got: false
     # /tmp/d20131223-4637-4e90s5/spec.rb:540:in `block (5 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (3 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (2 levels) in <top (required)>'

  4) Graphics shapes Rectangle comparison for equality returns the same hash for rectangles defined with different diagonal corners
     Failure/Error: a.hash.should eq b.hash
       
       expected: 427258855
            got: -432947477
       
       (compared using ==)
     # /tmp/d20131223-4637-4e90s5/spec.rb:563:in `block (5 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (3 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (2 levels) in <top (required)>'

  5) Graphics shapes Rectangle corners bottom right
     Failure/Error: rect.bottom_right.x.should eq 5
       
       expected: 5
            got: 1
       
       (compared using ==)
     # /tmp/d20131223-4637-4e90s5/spec.rb:589:in `block (5 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (3 levels) in <top (required)>'
     # ./lib/language/ruby/run_with_timeout.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.09143 seconds
69 examples, 5 failures

Failed examples:

rspec /tmp/d20131223-4637-4e90s5/spec.rb:51 # Graphics Canvas drawing of shapes and rasterization of points works for multiple ones
rspec /tmp/d20131223-4637-4e90s5/spec.rb:504 # Graphics shapes Rectangle initialization puts the leftmost point in its left field
rspec /tmp/d20131223-4637-4e90s5/spec.rb:536 # Graphics shapes Rectangle comparison for equality is true for rectangles defined with different diagonal corners
rspec /tmp/d20131223-4637-4e90s5/spec.rb:559 # Graphics shapes Rectangle comparison for equality returns the same hash for rectangles defined with different diagonal corners
rspec /tmp/d20131223-4637-4e90s5/spec.rb:587 # Graphics shapes Rectangle corners bottom right

История (7 версии и 4 коментара)

Георги обнови решението на 21.12.2013 13:19 (преди над 10 години)

+module Graphics
+ module Renderers
+ class Ascii
+ class << self
+ attr_accessor :foreword, :afterword, :visible, :invisible, :line_break
+ end
+
+ @foreword = ''
+ @afterword = ''
+ @visible = '@'
+ @invisible = '-'
+ @line_break = "\n"
+ end
+
+ class Html
+ class << self
+ attr_accessor :foreword, :afterword, :visible, :invisible, :line_break
+ end
+
+ @foreword = '<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Rendered Canvas</title>
+ <style type="text/css">
+ .canvas {
+ font-size: 1px;
+ line-height: 1px;
+ }
+ .canvas * {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 5px;
+ }
+ .canvas i {
+ background-color: #eee;
+ }
+ .canvas b {
+ background-color: #333;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="canvas">'
+ @visible = '<b></b>'
+ @invisible = '<i></i>'
+ @line_break = '<br>'
+ @afterword = ' </div>
+ </body>
+ </html>'
+ end
+ end
+
+ class Point
+ attr_reader :x, :y
+
+ def initialize(x, y)
+ @x = x
+ @y = y
+ end
+
+ def each
+ yield self
+ end
+
+ def to_s
+ "Point(#{@x}, #{@y})"
+ end
+
+ def hash
+ to_s.hash
+ end
+
+ def ==(other)
+ @x == other.x and @y == other.y
+ end
+
+ alias_method :eql?, :==
+ end
+
+ class Line
+ attr_reader :from, :to
+
+ def initialize(from, to)
+ @from = from
+ @to = to
+
+ if @from.x > @to.x or @from.x == @to.x and @from.y > @to.y
+ @from, @to = @to, @from
+ end
+ end
+
+ def each
+ rasterize.each { |point| yield point }
+ end
+
+ def to_s
+ "Line(#{@from}, #{@to})"
+ end
+
+ def hash
+ to_s.hash
+ end
+
+ def ==(other)
+ @from == other.from and @to == other.to
+ end
+
+ alias_method :eql?, :==
+
+ def rasterize
+ d_x = (@from.x - @to.x).abs
+ d_y = (@from.y - @to.y).abs
+
+ if d_x < d_y
+ d = 2*d_x - d_y
+ else
+ d = 2*d_y - d_x
+ end
+
+ bresenham(@from.x, @from.y, d_x, d_y, d)
+ end
+
+ private
+
+ def bresenham(p_x, p_y, d_x, d_y, d)
+ 0.upto([d_x, d_y].max).each_with_object([]) do |_, list|
+ list << Point.new(p_x, p_y)
+ p_x, p_y, d = next_step(p_x, p_y, d_x, d_y, d)
+ end
+ end
+
+ def next_step(p_x, p_y, d_x, d_y, d)
+ if @from.y <= @to.y
+ d_x < d_y ? first_case(p_x, p_y, d_x, d_y, d) : second_case(p_x, p_y, d_x, d_y, d)
+ else
+ d_x < d_y ? third_case(p_x, p_y, d_x, d_y, d) : fourth_case(p_x, p_y, d_x, d_y, d)
+ end
+ end
+
+ def first_case(p_x, p_y, d_x, d_y, d)
+ if d < 0
+ [p_x, p_y + 1, d + 2*d_x]
+ else
+ [p_x + 1, p_y + 1, d + 2*d_x - 2*d_y]
+ end
+ end
+
+ def second_case(p_x, p_y, d_x, d_y, d)
+ if d < 0
+ [p_x + 1, p_y, d + 2*d_y]
+ else
+ [p_x + 1, p_y + 1, d + 2*d_y - 2*d_x]
+ end
+ end
+
+ def third_case(p_x, p_y, d_x, d_y, d)
+ if d < 0
+ [p_x, p_y - 1, d + 2*d_x]
+ else
+ [p_x + 1, p_y - 1, d + 2*d_x - 2*d_y]
+ end
+ end
+
+ def fourth_case(p_x, p_y, d_x, d_y, d)
+ if d < 0
+ [p_x + 1, p_y, d + 2*d_y]
+ else
+ [p_x + 1, p_y - 1, d + 2*d_y - 2*d_x]
+ end
+ end
+ end
+
+ class Rectangle
+ attr_reader :left, :right, :top_left, :top_right, :bottom_left, :bottom_right
+
+ def initialize(point_a, point_b)
+ initialize_left_and_right(point_a, point_b)
+ initialize_skeptic_sucks
+ end
+
+ def each
+ top = Line.new(@top_left, @top_right).rasterize
+ bottom = Line.new(@bottom_left, @bottom_right).rasterize
+ left = Line.new(@top_left, @bottom_left).rasterize
+ right = Line.new(@top_right, @bottom_right).rasterize
+
+ (top + bottom + left + right).each { |point| yield point }
+ end
+
+ def to_s
+ "Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{bottom_right})"
+ end
+
+ def hash
+ to_s.hash
+ end
+
+ def ==(other)
+ @top_left == other.top_left and @bottom_right == other.bottom_right
+ end
+
+ alias_method :eql?, :==
+
+ private
+
+ def initialize_left_and_right(point_a, point_b)
+ @left = point_a
+ @right = point_b
+ if point_a.x > point_b.x or point_a.x == point_b.x and point_a.y > point_b.y
+ @left, @right = @right, @left
+ end
+ end
+
+ def initialize_skeptic_sucks
+ @top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
+ @top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y)
+ @bottom_left = @left == @top_left ? Point.new(@left.x, @right.y) : @left
+ @bottom_right = @right == @top_right ? Point.new(@right.x, @left.y) : @right
+ end
+ end
+
+ class Pixel
+ attr_accessor :active
+
+ def initialize(active = false)
+ @active = active
+ end
+
+ def draw(renderer)
+ active ? renderer.visible : renderer.invisible
+ end
+ end
+
+ class Canvas
+ attr_reader :width, :height
+
+ def initialize(width, height)
+ @width = width
+ @height = height
+ @data = Array.new(height) { Array.new(width) { Pixel.new } }
+ end
+
+ def set_pixel(x, y)
+ @data[y][x].active = true
+ end
+
+ def pixel_at?(x, y)
+ @data[y][x].active
+ end
+
+ def draw(figure)
+ figure.each { |point| @data[point.y][point.x].active = true }
+ end
+
+ def render_as(renderer)
+ renderer.foreword + @data.map do |row|
+ rendered_row = row.map { |pixel| pixel.draw(renderer) }.reduce(:+)
+ rendered_row + (row != @data.last ? renderer.line_break : renderer.afterword)
+ end.reduce(:+)
+ end
+ end
+end

Георги обнови решението на 21.12.2013 14:10 (преди над 10 години)

module Graphics
module Renderers
class Ascii
class << self
attr_accessor :foreword, :afterword, :visible, :invisible, :line_break
end
@foreword = ''
@afterword = ''
@visible = '@'
@invisible = '-'
@line_break = "\n"
end
class Html
class << self
attr_accessor :foreword, :afterword, :visible, :invisible, :line_break
end
@foreword = '<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">'
@visible = '<b></b>'
@invisible = '<i></i>'
@line_break = '<br>'
@afterword = ' </div>
</body>
</html>'
end
end
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def each
yield self
end
def to_s
"Point(#{@x}, #{@y})"
end
def hash
to_s.hash
end
def ==(other)
@x == other.x and @y == other.y
end
alias_method :eql?, :==
end
class Line
attr_reader :from, :to
def initialize(from, to)
@from = from
@to = to
if @from.x > @to.x or @from.x == @to.x and @from.y > @to.y
@from, @to = @to, @from
end
end
def each
rasterize.each { |point| yield point }
end
def to_s
"Line(#{@from}, #{@to})"
end
def hash
to_s.hash
end
def ==(other)
@from == other.from and @to == other.to
end
alias_method :eql?, :==
def rasterize
d_x = (@from.x - @to.x).abs
d_y = (@from.y - @to.y).abs
if d_x < d_y
d = 2*d_x - d_y
else
d = 2*d_y - d_x
end
bresenham(@from.x, @from.y, d_x, d_y, d)
end
private
def bresenham(p_x, p_y, d_x, d_y, d)
0.upto([d_x, d_y].max).each_with_object([]) do |_, list|
list << Point.new(p_x, p_y)
p_x, p_y, d = next_step(p_x, p_y, d_x, d_y, d)
end
end
def next_step(p_x, p_y, d_x, d_y, d)
if @from.y <= @to.y
d_x < d_y ? first_case(p_x, p_y, d_x, d_y, d) : second_case(p_x, p_y, d_x, d_y, d)
else
d_x < d_y ? third_case(p_x, p_y, d_x, d_y, d) : fourth_case(p_x, p_y, d_x, d_y, d)
end
end
def first_case(p_x, p_y, d_x, d_y, d)
if d < 0
[p_x, p_y + 1, d + 2*d_x]
else
[p_x + 1, p_y + 1, d + 2*d_x - 2*d_y]
end
end
def second_case(p_x, p_y, d_x, d_y, d)
if d < 0
[p_x + 1, p_y, d + 2*d_y]
else
[p_x + 1, p_y + 1, d + 2*d_y - 2*d_x]
end
end
def third_case(p_x, p_y, d_x, d_y, d)
if d < 0
[p_x, p_y - 1, d + 2*d_x]
else
[p_x + 1, p_y - 1, d + 2*d_x - 2*d_y]
end
end
def fourth_case(p_x, p_y, d_x, d_y, d)
if d < 0
[p_x + 1, p_y, d + 2*d_y]
else
[p_x + 1, p_y - 1, d + 2*d_y - 2*d_x]
end
end
end
class Rectangle
attr_reader :left, :right, :top_left, :top_right, :bottom_left, :bottom_right
def initialize(point_a, point_b)
initialize_left_and_right(point_a, point_b)
- initialize_skeptic_sucks
+ initialize_vertices
end
def each
top = Line.new(@top_left, @top_right).rasterize
bottom = Line.new(@bottom_left, @bottom_right).rasterize
left = Line.new(@top_left, @bottom_left).rasterize
right = Line.new(@top_right, @bottom_right).rasterize
(top + bottom + left + right).each { |point| yield point }
end
def to_s
"Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{bottom_right})"
end
def hash
to_s.hash
end
def ==(other)
@top_left == other.top_left and @bottom_right == other.bottom_right
end
alias_method :eql?, :==
private
def initialize_left_and_right(point_a, point_b)
@left = point_a
@right = point_b
if point_a.x > point_b.x or point_a.x == point_b.x and point_a.y > point_b.y
@left, @right = @right, @left
end
end
- def initialize_skeptic_sucks
+ def initialize_vertices
@top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
@top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y)
@bottom_left = @left == @top_left ? Point.new(@left.x, @right.y) : @left
@bottom_right = @right == @top_right ? Point.new(@right.x, @left.y) : @right
end
end
class Pixel
attr_accessor :active
def initialize(active = false)
@active = active
end
def draw(renderer)
active ? renderer.visible : renderer.invisible
end
end
class Canvas
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@data = Array.new(height) { Array.new(width) { Pixel.new } }
end
def set_pixel(x, y)
@data[y][x].active = true
end
def pixel_at?(x, y)
@data[y][x].active
end
def draw(figure)
figure.each { |point| @data[point.y][point.x].active = true }
end
def render_as(renderer)
renderer.foreword + @data.map do |row|
rendered_row = row.map { |pixel| pixel.draw(renderer) }.reduce(:+)
rendered_row + (row != @data.last ? renderer.line_break : renderer.afterword)
end.reduce(:+)
end
end
end

Георги обнови решението на 21.12.2013 14:33 (преди над 10 години)

module Graphics
module Renderers
class Ascii
class << self
attr_accessor :foreword, :afterword, :visible, :invisible, :line_break
end
@foreword = ''
@afterword = ''
@visible = '@'
@invisible = '-'
@line_break = "\n"
end
class Html
class << self
attr_accessor :foreword, :afterword, :visible, :invisible, :line_break
end
@foreword = '<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">'
@visible = '<b></b>'
@invisible = '<i></i>'
@line_break = '<br>'
@afterword = ' </div>
</body>
</html>'
end
end
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def each
yield self
end
def to_s
"Point(#{@x}, #{@y})"
end
def hash
to_s.hash
end
def ==(other)
@x == other.x and @y == other.y
end
alias_method :eql?, :==
end
class Line
attr_reader :from, :to
def initialize(from, to)
@from = from
@to = to
if @from.x > @to.x or @from.x == @to.x and @from.y > @to.y
@from, @to = @to, @from
end
end
def each
rasterize.each { |point| yield point }
end
def to_s
"Line(#{@from}, #{@to})"
end
def hash
to_s.hash
end
def ==(other)
@from == other.from and @to == other.to
end
alias_method :eql?, :==
def rasterize
d_x = (@from.x - @to.x).abs
d_y = (@from.y - @to.y).abs
if d_x < d_y
d = 2*d_x - d_y
else
d = 2*d_y - d_x
end
bresenham(@from.x, @from.y, d_x, d_y, d)
end
private
def bresenham(p_x, p_y, d_x, d_y, d)
0.upto([d_x, d_y].max).each_with_object([]) do |_, list|
list << Point.new(p_x, p_y)
p_x, p_y, d = next_step(p_x, p_y, d_x, d_y, d)
end
end
def next_step(p_x, p_y, d_x, d_y, d)
if @from.y <= @to.y
d_x < d_y ? first_case(p_x, p_y, d_x, d_y, d) : second_case(p_x, p_y, d_x, d_y, d)
else
d_x < d_y ? third_case(p_x, p_y, d_x, d_y, d) : fourth_case(p_x, p_y, d_x, d_y, d)
end
end
def first_case(p_x, p_y, d_x, d_y, d)
if d < 0
[p_x, p_y + 1, d + 2*d_x]
else
[p_x + 1, p_y + 1, d + 2*d_x - 2*d_y]
end
end
def second_case(p_x, p_y, d_x, d_y, d)
if d < 0
[p_x + 1, p_y, d + 2*d_y]
else
[p_x + 1, p_y + 1, d + 2*d_y - 2*d_x]
end
end
def third_case(p_x, p_y, d_x, d_y, d)
if d < 0
[p_x, p_y - 1, d + 2*d_x]
else
[p_x + 1, p_y - 1, d + 2*d_x - 2*d_y]
end
end
def fourth_case(p_x, p_y, d_x, d_y, d)
if d < 0
[p_x + 1, p_y, d + 2*d_y]
else
[p_x + 1, p_y - 1, d + 2*d_y - 2*d_x]
end
end
end
class Rectangle
attr_reader :left, :right, :top_left, :top_right, :bottom_left, :bottom_right
def initialize(point_a, point_b)
initialize_left_and_right(point_a, point_b)
initialize_vertices
end
def each
top = Line.new(@top_left, @top_right).rasterize
bottom = Line.new(@bottom_left, @bottom_right).rasterize
left = Line.new(@top_left, @bottom_left).rasterize
right = Line.new(@top_right, @bottom_right).rasterize
(top + bottom + left + right).each { |point| yield point }
end
def to_s
"Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{bottom_right})"
end
def hash
to_s.hash
end
def ==(other)
@top_left == other.top_left and @bottom_right == other.bottom_right
end
alias_method :eql?, :==
private
def initialize_left_and_right(point_a, point_b)
@left = point_a
@right = point_b
if point_a.x > point_b.x or point_a.x == point_b.x and point_a.y > point_b.y
@left, @right = @right, @left
end
end
def initialize_vertices
@top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
@top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y)
@bottom_left = @left == @top_left ? Point.new(@left.x, @right.y) : @left
@bottom_right = @right == @top_right ? Point.new(@right.x, @left.y) : @right
end
end
class Pixel
attr_accessor :active
def initialize(active = false)
@active = active
end
- def draw(renderer)
+ def render_as(renderer)
active ? renderer.visible : renderer.invisible
end
end
class Canvas
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@data = Array.new(height) { Array.new(width) { Pixel.new } }
end
def set_pixel(x, y)
@data[y][x].active = true
end
def pixel_at?(x, y)
@data[y][x].active
end
def draw(figure)
figure.each { |point| @data[point.y][point.x].active = true }
end
def render_as(renderer)
renderer.foreword + @data.map do |row|
- rendered_row = row.map { |pixel| pixel.draw(renderer) }.reduce(:+)
+ rendered_row = row.map { |pixel| pixel.render_as(renderer) }.reduce(:+)
rendered_row + (row != @data.last ? renderer.line_break : renderer.afterword)
end.reduce(:+)
end
end
end

Като цяло, спазваш конвенциите и дизайнът ти е приличен. Имаш добри попадения.

Ето над какво може да поработиш:

  • Ред 88, внимавай с приоритета на and и or, еднакъв е.
  • Слагай интервали около оператори като * (ред 116, 118, ...)
  • Променливата d не ми говори много какво е. Именувай я по-добре. Донякъде разбирам какво е d_x, но бих предпочел да е, например, delta_x.
  • Методите за растеризация на линия по Брезенхам са ми доста нечетими и имената на променливите не помагат. Поработи над тях. Помисли дали не е добра идея да ги изведеш в отделен клас.
  • На ред 127 може би си искал да кажеш map, а не each_with_object([]) :)
  • Бих написал Line#each така:

      def each(&block)
        [
          @top_left,    @top_right,
          @bottom_left, @bottom_right,
          @top_left,    @bottom_left,
          @top_right,   @bottom_right,
        ].each_slice(2).map { |a, b| Line.new(a, b).rasterize }.flatten.each(&block)
      end
    

    Или така, въпреки, че има повече повторение, защото ми се струва по-четимо:

      def each(&block)
        [
          Line.new(@top_left,    @top_right   ),
          Line.new(@bottom_left, @bottom_right),
          Line.new(@top_left,    @bottom_left ),
          Line.new(@top_right,   @bottom_right),
        ].map(&:rasterize).flatten.each(&block)
      end
    

    Забележи как подравнявам неща вертикално. За мен това подобрява четимостта. Може да го приложиш и на други места в кода си.

  • initialize_vertices има нужда от преработка, или поне преподреждане, много ми е нагъчкан и нечетим.
  • Помисли дали няма друг по-оптимален начин в Ruby за реализация вътрешното представяне на пано от масив от масиви. Ruby не е C и има по-удобни структури от данни :)
  • Пробвай да направиш методите за рендериране с map и join.
  • Помисли кой трбява да носи отговорността за "рендериране" на пано. Самото пано или рендерера? Помисли кой обект каква отговорност носи. Една основна идея на ОО-програмирането е да разпределиш различните отговорности между различни обекти. Стреми се всеки обект да отговаря за едно конкретно нещо и само за него. Напълно нормално е да имаш голям брой малки обекти в системата, с тясно специализирани отговорности.
  • Въпреки това, не виждам голям смисъл от класа Pixel. Според мен, може да се мине без него и дизайнът да е достатъчно прост и елегантен.
  • Ползваш класовете за рендериране като кофи за данни, по много неидиоматичен начин. Помисли дали това не може да се промени, в контекста на казаното по-горе за ОО дизайна.
  • В случая е удачно HTML кодът на Html да се пази в константа/и в класа.
  • "Foreword" и "afterword" са по-неподходящи имена, отколкото "header" и "footer" за HTML кода в тази задача.

Георги обнови решението на 21.12.2013 23:25 (преди над 10 години)

+require 'matrix'
+
+class Matrix
+ def []=(i, j, x)
+ @rows[i][j] = x
+ end
+end
+
module Graphics
module Renderers
class Ascii
class << self
- attr_accessor :foreword, :afterword, :visible, :invisible, :line_break
+ attr_accessor :visible, :invisible, :line_break
+
+ def render_pixel(pixel)
+ pixel ? @visible : @invisible
+ end
+
+ def render_canvas(canvas)
+ canvas.each_slice(canvas.width).map do |row|
+ row.map { |pixel| render_pixel(pixel) } << @line_break
+ end.join.chomp(@line_break)
+ end
end
- @foreword = ''
- @afterword = ''
@visible = '@'
@invisible = '-'
@line_break = "\n"
end
class Html
+ HEADER = '<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Rendered Canvas</title>
+ <style type="text/css">
+ .canvas {
+ font-size: 1px;
+ line-height: 1px;
+ }
+ .canvas * {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ border-radius: 5px;
+ }
+ .canvas i {
+ background-color: #eee;
+ }
+ .canvas b {
+ background-color: #333;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="canvas">'
+
+ FOOTER = ' </div>
+ </body>
+ </html>'
+
class << self
- attr_accessor :foreword, :afterword, :visible, :invisible, :line_break
+ attr_accessor :visible, :invisible, :line_break
+
+ def render_pixel(pixel)
+ pixel ? @visible : @invisible
+ end
+
+ def render_canvas(canvas)
+ HEADER + canvas.each_slice(canvas.width).map do |row|
+ row.map { |pixel| render_pixel(pixel) } << @line_break
+ end.join.chomp(@line_break) + FOOTER
+ end
end
- @foreword = '<!DOCTYPE html>
- <html>
- <head>
- <title>Rendered Canvas</title>
- <style type="text/css">
- .canvas {
- font-size: 1px;
- line-height: 1px;
- }
- .canvas * {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 5px;
- }
- .canvas i {
- background-color: #eee;
- }
- .canvas b {
- background-color: #333;
- }
- </style>
- </head>
- <body>
- <div class="canvas">'
@visible = '<b></b>'
@invisible = '<i></i>'
@line_break = '<br>'
- @afterword = ' </div>
- </body>
- </html>'
end
end
+ class BresenhamAlgorithm
+ FIRST_ROTATION = Matrix[ [1, 0], [0, 1] ]
+ SECOND_ROTATION = Matrix[ [0, 1], [1, 0] ]
+ THIRD_ROTATION = Matrix[ [1, 0], [0, -1] ]
+ FOURTH_ROTATION = Matrix[ [0, -1], [1, 0] ]
+
+ @@rotation_matrix = FIRST_ROTATION
+
+ class << self
+ def rasterize_line(line)
+ set_rotation_matrix(line.from, line.to)
+
+ from, to = rotate_endpoints(line.from, line.to)
+
+ delta_x = (from.x - to.x).abs
+ delta_y = (from.y - to.y).abs
+ delta = 2 * delta_y - delta_x
+
+ compute_points(from.x, from.y, delta_x, delta_y, delta)
+ end
+
+ private
+
+ def set_rotation_matrix(from, to)
+ delta_x = (from.x - to.x).abs
+ delta_y = (from.y - to.y).abs
+
+ if from.y <= to.y
+ @@rotation_matrix = delta_x >= delta_y ? FIRST_ROTATION : SECOND_ROTATION
+ else
+ @@rotation_matrix = delta_x >= delta_y ? THIRD_ROTATION : FOURTH_ROTATION
+ end
+ end
+
+ def rotate_endpoints(from, to)
+ from = from.rotate_by_matrix(@@rotation_matrix)
+ to = to.rotate_by_matrix(@@rotation_matrix)
+
+ [from, to]
+ end
+
+ def compute_points(point_x, point_y, delta_x, delta_y, delta)
+ inverse_matrix = @@rotation_matrix.inverse
+ starting_point = Point.new(point_x, point_y).rotate_by_matrix(inverse_matrix)
+
+ [starting_point] + 1.upto(delta_x).map do |_|
+ point_x, point_y, delta = next_step(point_x, point_y, delta_x, delta_y, delta)
+ Point.new(point_x, point_y).rotate_by_matrix(inverse_matrix)
+ end
+ end
+
+ def next_step(point_x, point_y, delta_x, delta_y, delta)
+ if delta < 0
+ [point_x + 1, point_y, delta + 2 * delta_y]
+ else
+ [point_x + 1, point_y + 1, delta + 2 * delta_y - 2 * delta_x]
+ end
+ end
+ end
+ end
+
class Point
attr_reader :x, :y
+ alias_method :eql?, :==
+
def initialize(x, y)
@x = x
@y = y
end
def each
yield self
end
+ def rotate_by_matrix(matrix)
+ Point.new(matrix[0, 0] * @x + matrix[0, 1] * @y,
+ matrix[1, 0] * @x + matrix[1, 1] * @y)
+ end
+
def to_s
"Point(#{@x}, #{@y})"
end
def hash
to_s.hash
end
def ==(other)
@x == other.x and @y == other.y
end
-
- alias_method :eql?, :==
end
class Line
attr_reader :from, :to
+ alias_method :eql?, :==
+
def initialize(from, to)
@from = from
@to = to
- if @from.x > @to.x or @from.x == @to.x and @from.y > @to.y
+ if @from.x > @to.x or (@from.x == @to.x and @from.y > @to.y)
@from, @to = @to, @from
end
end
- def each
- rasterize.each { |point| yield point }
+ def each(&block)
+ rasterize.each(&block)
end
def to_s
"Line(#{@from}, #{@to})"
end
def hash
to_s.hash
end
def ==(other)
@from == other.from and @to == other.to
end
- alias_method :eql?, :==
-
def rasterize
- d_x = (@from.x - @to.x).abs
- d_y = (@from.y - @to.y).abs
-
- if d_x < d_y
- d = 2*d_x - d_y
- else
- d = 2*d_y - d_x
- end
-
- bresenham(@from.x, @from.y, d_x, d_y, d)
+ BresenhamAlgorithm.rasterize_line(self)
end
-
- private
-
- def bresenham(p_x, p_y, d_x, d_y, d)
- 0.upto([d_x, d_y].max).each_with_object([]) do |_, list|
- list << Point.new(p_x, p_y)
- p_x, p_y, d = next_step(p_x, p_y, d_x, d_y, d)
- end
- end
-
- def next_step(p_x, p_y, d_x, d_y, d)
- if @from.y <= @to.y
- d_x < d_y ? first_case(p_x, p_y, d_x, d_y, d) : second_case(p_x, p_y, d_x, d_y, d)
- else
- d_x < d_y ? third_case(p_x, p_y, d_x, d_y, d) : fourth_case(p_x, p_y, d_x, d_y, d)
- end
- end
-
- def first_case(p_x, p_y, d_x, d_y, d)
- if d < 0
- [p_x, p_y + 1, d + 2*d_x]
- else
- [p_x + 1, p_y + 1, d + 2*d_x - 2*d_y]
- end
- end
-
- def second_case(p_x, p_y, d_x, d_y, d)
- if d < 0
- [p_x + 1, p_y, d + 2*d_y]
- else
- [p_x + 1, p_y + 1, d + 2*d_y - 2*d_x]
- end
- end
-
- def third_case(p_x, p_y, d_x, d_y, d)
- if d < 0
- [p_x, p_y - 1, d + 2*d_x]
- else
- [p_x + 1, p_y - 1, d + 2*d_x - 2*d_y]
- end
- end
-
- def fourth_case(p_x, p_y, d_x, d_y, d)
- if d < 0
- [p_x + 1, p_y, d + 2*d_y]
- else
- [p_x + 1, p_y - 1, d + 2*d_y - 2*d_x]
- end
- end
end
class Rectangle
attr_reader :left, :right, :top_left, :top_right, :bottom_left, :bottom_right
+ alias_method :eql?, :==
+
def initialize(point_a, point_b)
initialize_left_and_right(point_a, point_b)
initialize_vertices
end
- def each
- top = Line.new(@top_left, @top_right).rasterize
- bottom = Line.new(@bottom_left, @bottom_right).rasterize
- left = Line.new(@top_left, @bottom_left).rasterize
- right = Line.new(@top_right, @bottom_right).rasterize
-
- (top + bottom + left + right).each { |point| yield point }
+ def each(&block)
+ [
+ Line.new(@top_left, @top_right ),
+ Line.new(@bottom_left, @bottom_right),
+ Line.new(@top_left, @bottom_left ),
+ Line.new(@top_right, @bottom_right),
+ ].map(&:rasterize).flatten.each(&block)
end
def to_s
- "Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{bottom_right})"
+ "Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{@bottom_right})"
end
def hash
to_s.hash
end
def ==(other)
@top_left == other.top_left and @bottom_right == other.bottom_right
end
- alias_method :eql?, :==
-
private
def initialize_left_and_right(point_a, point_b)
@left = point_a
@right = point_b
+
if point_a.x > point_b.x or point_a.x == point_b.x and point_a.y > point_b.y
@left, @right = @right, @left
end
end
def initialize_vertices
- @top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
- @top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y)
- @bottom_left = @left == @top_left ? Point.new(@left.x, @right.y) : @left
- @bottom_right = @right == @top_right ? Point.new(@right.x, @left.y) : @right
- end
- end
+ @top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
+ @top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y )
- class Pixel
- attr_accessor :active
-
- def initialize(active = false)
- @active = active
+ @bottom_left = @left != @top_left ? @left : Point.new(@left.x, @right.y)
+ @bottom_right = @right != @top_right ? @right : Point.new(@right.x, @left.y )
end
-
- def render_as(renderer)
- active ? renderer.visible : renderer.invisible
- end
end
class Canvas
+ include Enumerable
+
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
- @data = Array.new(height) { Array.new(width) { Pixel.new } }
+ @data = Matrix.build(height, width) { false }
end
def set_pixel(x, y)
- @data[y][x].active = true
+ @data[y, x] = true
end
def pixel_at?(x, y)
- @data[y][x].active
+ @data[y, x]
end
def draw(figure)
- figure.each { |point| @data[point.y][point.x].active = true }
+ figure.each { |point| @data[point.y, point.x] = true }
end
def render_as(renderer)
- renderer.foreword + @data.map do |row|
- rendered_row = row.map { |pixel| pixel.render_as(renderer) }.reduce(:+)
- rendered_row + (row != @data.last ? renderer.line_break : renderer.afterword)
- end.reduce(:+)
+ renderer.render_canvas(self)
+ end
+
+ def each(&block)
+ @data.each(&block)
end
end
end

Георги обнови решението на 21.12.2013 23:50 (преди над 10 години)

require 'matrix'
class Matrix
def []=(i, j, x)
@rows[i][j] = x
end
end
module Graphics
module Renderers
- class Ascii
+
+ private
+
+ class Renderer
class << self
attr_accessor :visible, :invisible, :line_break
- def render_pixel(pixel)
- pixel ? @visible : @invisible
- end
+ def render_pixel(pixel)
+ pixel ? @visible : @invisible
+ end
- def render_canvas(canvas)
- canvas.each_slice(canvas.width).map do |row|
- row.map { |pixel| render_pixel(pixel) } << @line_break
- end.join.chomp(@line_break)
- end
+ def render_canvas(canvas)
+ canvas.each_slice(canvas.width).map do |row|
+ row.map { |pixel| render_pixel(pixel) } << @line_break
+ end.join.chomp(@line_break)
+ end
end
+ end
+ public
+
+ class Ascii < Renderer
@visible = '@'
@invisible = '-'
@line_break = "\n"
end
- class Html
+ class Html < Renderer
HEADER = '<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">'
FOOTER = ' </div>
</body>
</html>'
- class << self
- attr_accessor :visible, :invisible, :line_break
-
- def render_pixel(pixel)
- pixel ? @visible : @invisible
- end
-
- def render_canvas(canvas)
- HEADER + canvas.each_slice(canvas.width).map do |row|
- row.map { |pixel| render_pixel(pixel) } << @line_break
- end.join.chomp(@line_break) + FOOTER
- end
+ def self.render_canvas(canvas)
+ HEADER + super + FOOTER
end
@visible = '<b></b>'
@invisible = '<i></i>'
@line_break = '<br>'
end
end
class BresenhamAlgorithm
FIRST_ROTATION = Matrix[ [1, 0], [0, 1] ]
SECOND_ROTATION = Matrix[ [0, 1], [1, 0] ]
THIRD_ROTATION = Matrix[ [1, 0], [0, -1] ]
FOURTH_ROTATION = Matrix[ [0, -1], [1, 0] ]
@@rotation_matrix = FIRST_ROTATION
class << self
def rasterize_line(line)
set_rotation_matrix(line.from, line.to)
from, to = rotate_endpoints(line.from, line.to)
delta_x = (from.x - to.x).abs
delta_y = (from.y - to.y).abs
delta = 2 * delta_y - delta_x
compute_points(from.x, from.y, delta_x, delta_y, delta)
end
private
def set_rotation_matrix(from, to)
delta_x = (from.x - to.x).abs
delta_y = (from.y - to.y).abs
if from.y <= to.y
@@rotation_matrix = delta_x >= delta_y ? FIRST_ROTATION : SECOND_ROTATION
else
@@rotation_matrix = delta_x >= delta_y ? THIRD_ROTATION : FOURTH_ROTATION
end
end
def rotate_endpoints(from, to)
from = from.rotate_by_matrix(@@rotation_matrix)
to = to.rotate_by_matrix(@@rotation_matrix)
[from, to]
end
def compute_points(point_x, point_y, delta_x, delta_y, delta)
inverse_matrix = @@rotation_matrix.inverse
starting_point = Point.new(point_x, point_y).rotate_by_matrix(inverse_matrix)
[starting_point] + 1.upto(delta_x).map do |_|
point_x, point_y, delta = next_step(point_x, point_y, delta_x, delta_y, delta)
Point.new(point_x, point_y).rotate_by_matrix(inverse_matrix)
end
end
def next_step(point_x, point_y, delta_x, delta_y, delta)
if delta < 0
[point_x + 1, point_y, delta + 2 * delta_y]
else
[point_x + 1, point_y + 1, delta + 2 * delta_y - 2 * delta_x]
end
end
end
end
class Point
attr_reader :x, :y
alias_method :eql?, :==
def initialize(x, y)
@x = x
@y = y
end
def each
yield self
end
def rotate_by_matrix(matrix)
Point.new(matrix[0, 0] * @x + matrix[0, 1] * @y,
matrix[1, 0] * @x + matrix[1, 1] * @y)
end
def to_s
"Point(#{@x}, #{@y})"
end
def hash
to_s.hash
end
def ==(other)
@x == other.x and @y == other.y
end
end
class Line
attr_reader :from, :to
alias_method :eql?, :==
def initialize(from, to)
@from = from
@to = to
if @from.x > @to.x or (@from.x == @to.x and @from.y > @to.y)
@from, @to = @to, @from
end
end
def each(&block)
rasterize.each(&block)
end
def to_s
"Line(#{@from}, #{@to})"
end
def hash
to_s.hash
end
def ==(other)
@from == other.from and @to == other.to
end
def rasterize
BresenhamAlgorithm.rasterize_line(self)
end
end
class Rectangle
attr_reader :left, :right, :top_left, :top_right, :bottom_left, :bottom_right
alias_method :eql?, :==
def initialize(point_a, point_b)
initialize_left_and_right(point_a, point_b)
initialize_vertices
end
def each(&block)
[
Line.new(@top_left, @top_right ),
Line.new(@bottom_left, @bottom_right),
Line.new(@top_left, @bottom_left ),
Line.new(@top_right, @bottom_right),
].map(&:rasterize).flatten.each(&block)
end
def to_s
"Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{@bottom_right})"
end
def hash
to_s.hash
end
def ==(other)
@top_left == other.top_left and @bottom_right == other.bottom_right
end
private
def initialize_left_and_right(point_a, point_b)
@left = point_a
@right = point_b
if point_a.x > point_b.x or point_a.x == point_b.x and point_a.y > point_b.y
@left, @right = @right, @left
end
end
def initialize_vertices
@top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
@top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y )
@bottom_left = @left != @top_left ? @left : Point.new(@left.x, @right.y)
@bottom_right = @right != @top_right ? @right : Point.new(@right.x, @left.y )
end
end
class Canvas
include Enumerable
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@data = Matrix.build(height, width) { false }
end
def set_pixel(x, y)
@data[y, x] = true
end
def pixel_at?(x, y)
@data[y, x]
end
def draw(figure)
figure.each { |point| @data[point.y, point.x] = true }
end
def render_as(renderer)
renderer.render_canvas(self)
end
def each(&block)
@data.each(&block)
end
end
end

Благодаря много за напътсвията. Опитах се да си взема поука от всичките! :smile:

Докато правих промените възникнаха няколко въпроса:

  • За по-оптимално представяне на паното най-вероятно имаш предвид Matrix, което вече бях пробвал, но се отказах заради този monkey-patching, който трябва да се направи, за да имаш Matrix#[]=. В този случай ОК ли е с patcha?
  • Прекалено ли е да include-вам Enumerable в Canvas само за да стане по-елегантно итерирането му и да не използвам Canvas#pixel_at? ?
  • Колко лошо е това, което съм направил с рендерите ? :fearful:

Георги обнови решението на 22.12.2013 12:29 (преди над 10 години)

require 'matrix'
class Matrix
def []=(i, j, x)
@rows[i][j] = x
end
end
module Graphics
module Renderers
private
class Renderer
class << self
attr_accessor :visible, :invisible, :line_break
def render_pixel(pixel)
pixel ? @visible : @invisible
end
def render_canvas(canvas)
canvas.each_slice(canvas.width).map do |row|
row.map { |pixel| render_pixel(pixel) } << @line_break
end.join.chomp(@line_break)
end
end
end
public
class Ascii < Renderer
@visible = '@'
@invisible = '-'
@line_break = "\n"
end
class Html < Renderer
HEADER = '<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">'
FOOTER = ' </div>
</body>
</html>'
def self.render_canvas(canvas)
HEADER + super + FOOTER
end
@visible = '<b></b>'
@invisible = '<i></i>'
@line_break = '<br>'
end
end
class BresenhamAlgorithm
FIRST_ROTATION = Matrix[ [1, 0], [0, 1] ]
SECOND_ROTATION = Matrix[ [0, 1], [1, 0] ]
THIRD_ROTATION = Matrix[ [1, 0], [0, -1] ]
- FOURTH_ROTATION = Matrix[ [0, -1], [1, 0] ]
+ FOURTH_ROTATION = Matrix[ [0, -1], [1, 0] ]
- @@rotation_matrix = FIRST_ROTATION
-
class << self
def rasterize_line(line)
set_rotation_matrix(line.from, line.to)
- from, to = rotate_endpoints(line.from, line.to)
+ from = line.from.transform(@rotation_matrix)
+ to = line.to.transform(@rotation_matrix)
- delta_x = (from.x - to.x).abs
- delta_y = (from.y - to.y).abs
- delta = 2 * delta_y - delta_x
-
- compute_points(from.x, from.y, delta_x, delta_y, delta)
+ set_variables(from, to)
+ compute_points
end
private
def set_rotation_matrix(from, to)
delta_x = (from.x - to.x).abs
delta_y = (from.y - to.y).abs
if from.y <= to.y
- @@rotation_matrix = delta_x >= delta_y ? FIRST_ROTATION : SECOND_ROTATION
+ @rotation_matrix = delta_x >= delta_y ? FIRST_ROTATION : SECOND_ROTATION
else
- @@rotation_matrix = delta_x >= delta_y ? THIRD_ROTATION : FOURTH_ROTATION
+ @rotation_matrix = delta_x >= delta_y ? THIRD_ROTATION : FOURTH_ROTATION
end
+
+ @inverse_matrix = @rotation_matrix.inverse
end
- def rotate_endpoints(from, to)
- from = from.rotate_by_matrix(@@rotation_matrix)
- to = to.rotate_by_matrix(@@rotation_matrix)
+ def set_variables(from, to)
+ @point_x = from.x
+ @point_y = from.y
- [from, to]
+ @delta_x = (from.x - to.x).abs
+ @delta_y = (from.y - to.y).abs
+
+ @delta = 2 * @delta_y - @delta_x
end
- def compute_points(point_x, point_y, delta_x, delta_y, delta)
- inverse_matrix = @@rotation_matrix.inverse
- starting_point = Point.new(point_x, point_y).rotate_by_matrix(inverse_matrix)
+ def compute_points
+ starting_point = Point.new(@point_x, @point_y).transform(@inverse_matrix)
- [starting_point] + 1.upto(delta_x).map do |_|
- point_x, point_y, delta = next_step(point_x, point_y, delta_x, delta_y, delta)
- Point.new(point_x, point_y).rotate_by_matrix(inverse_matrix)
+ [starting_point] + 1.upto(@delta_x).map do |_|
+ next_step
+ Point.new(@point_x, @point_y).transform(@inverse_matrix)
end
end
- def next_step(point_x, point_y, delta_x, delta_y, delta)
- if delta < 0
- [point_x + 1, point_y, delta + 2 * delta_y]
+ def next_step
+ if @delta < 0
+ @point_x += 1
+ @delta += 2 * @delta_y
else
- [point_x + 1, point_y + 1, delta + 2 * delta_y - 2 * delta_x]
+ @point_x += 1
+ @point_y += 1
+ @delta += 2 * @delta_y - 2 * @delta_x
end
end
end
end
class Point
attr_reader :x, :y
alias_method :eql?, :==
def initialize(x, y)
@x = x
@y = y
end
def each
yield self
end
- def rotate_by_matrix(matrix)
+ def transform(matrix)
Point.new(matrix[0, 0] * @x + matrix[0, 1] * @y,
matrix[1, 0] * @x + matrix[1, 1] * @y)
end
def to_s
"Point(#{@x}, #{@y})"
end
def hash
to_s.hash
end
def ==(other)
@x == other.x and @y == other.y
end
end
class Line
attr_reader :from, :to
alias_method :eql?, :==
def initialize(from, to)
@from = from
@to = to
if @from.x > @to.x or (@from.x == @to.x and @from.y > @to.y)
@from, @to = @to, @from
end
end
def each(&block)
rasterize.each(&block)
end
def to_s
"Line(#{@from}, #{@to})"
end
def hash
to_s.hash
end
def ==(other)
@from == other.from and @to == other.to
end
def rasterize
BresenhamAlgorithm.rasterize_line(self)
end
end
class Rectangle
attr_reader :left, :right, :top_left, :top_right, :bottom_left, :bottom_right
alias_method :eql?, :==
def initialize(point_a, point_b)
initialize_left_and_right(point_a, point_b)
initialize_vertices
end
def each(&block)
[
Line.new(@top_left, @top_right ),
Line.new(@bottom_left, @bottom_right),
Line.new(@top_left, @bottom_left ),
Line.new(@top_right, @bottom_right),
].map(&:rasterize).flatten.each(&block)
end
def to_s
"Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{@bottom_right})"
end
def hash
to_s.hash
end
def ==(other)
@top_left == other.top_left and @bottom_right == other.bottom_right
end
private
def initialize_left_and_right(point_a, point_b)
@left = point_a
@right = point_b
if point_a.x > point_b.x or point_a.x == point_b.x and point_a.y > point_b.y
@left, @right = @right, @left
end
end
def initialize_vertices
@top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
@top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y )
@bottom_left = @left != @top_left ? @left : Point.new(@left.x, @right.y)
@bottom_right = @right != @top_right ? @right : Point.new(@right.x, @left.y )
end
end
class Canvas
include Enumerable
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@data = Matrix.build(height, width) { false }
end
def set_pixel(x, y)
@data[y, x] = true
end
def pixel_at?(x, y)
@data[y, x]
end
def draw(figure)
figure.each { |point| @data[point.y, point.x] = true }
end
def render_as(renderer)
renderer.render_canvas(self)
end
def each(&block)
@data.each(&block)
end
end
end
  • Нямам предвид Matrix. Имам предвид тип данни, който можеш да ползваш без require :) Monkey-patch-ът не ми харесва а и не мисля, че има нужда от него. Спокойно може да си ползваш @data[y][x] = true. А и може да го направиш @data[x][y] и да я няма тази логическа инверсия тук и вpixel_at?`.
  • Също така, pixel_at? може да връща нещо, което се оценява като истина или лъжа; не е задължително да е true/false. Ключът към бараката е всъщност нужно ли е да описваш предварително факта, че всеки пискел е "празен", като това е състоянието по подразбиране?
  • Относно рендерерите, след като сам ми задаваш въпрос колко лошо е положението там, значи го разбираш и разбираш, че трябва да го подобриш. Не разбирам защо се опитваш да ползваш класовете като модули и не ги оставиш да си държат state, или пък дори да няма нужда от state, ги ползвай като инстанции. Много рядко се налага да имаш клас само със class-level неща. Ако ти трябва да ползваш инстанционни променливи в контекст на клас (или класови променливи, все едно), това е сигнална лампа, че нещо с дизайна ти може би не е наред. В случая, тези променливи е подоходящо да са константи в класовете, или да се ползват по друг начин.
  • Казаното по-горе важи с пълна сила за класа BresenhamAlgorithm`, който много хубаво си извел, но от който ползваш само "глобален" state. Не се страхувай да си направиш инстанция и да ползваш нея. Сегашното нещо е лоша практика, не е thread safe и т.н.
  • Да include-ваш Enumerable в Canvas си е по твоя преценка. Аз лично не го правя и използвам само публичния интерфейс на Canvas, описан в условието, за да оставя всеки рендерер да си обходи паното както си прецени – дали ще е отляво-надясно, отгоре-надолу или обратно, дали ще е през ред и през колона... Оставям рендерера да си реши.
  • Не мисля, че там поставен синонимът на == ще работи както очакваш. Припомни си как работят синоними на методи.
  • Не бих оставил толкова място преди != на ред 253 и 254. Ако държа на подравняването там, бих преместил мястото преди ? на същите редове.

Георги обнови решението на 22.12.2013 17:51 (преди над 10 години)

require 'matrix'
class Matrix
def []=(i, j, x)
@rows[i][j] = x
end
end
module Graphics
module Renderers
private
class Renderer
- class << self
- attr_accessor :visible, :invisible, :line_break
+ attr_accessor :visible, :invisible, :line_break
- def render_pixel(pixel)
- pixel ? @visible : @invisible
- end
+ def render_pixel(pixel)
+ pixel ? @visible : @invisible
+ end
- def render_canvas(canvas)
- canvas.each_slice(canvas.width).map do |row|
- row.map { |pixel| render_pixel(pixel) } << @line_break
- end.join.chomp(@line_break)
- end
+ def render_canvas(canvas)
+ canvas.each_slice(canvas.width).map do |row|
+ row.map { |pixel| render_pixel(pixel) } << @line_break
+ end.join.chomp(@line_break)
end
end
public
class Ascii < Renderer
- @visible = '@'
- @invisible = '-'
- @line_break = "\n"
+ def initialize
+ super
+ @visible = '@'
+ @invisible = '-'
+ @line_break = "\n"
+ end
end
class Html < Renderer
HEADER = '<!DOCTYPE html>
<html>
<head>
<title>Rendered Canvas</title>
<style type="text/css">
.canvas {
font-size: 1px;
line-height: 1px;
}
.canvas * {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 5px;
}
.canvas i {
background-color: #eee;
}
.canvas b {
background-color: #333;
}
</style>
</head>
<body>
<div class="canvas">'
FOOTER = ' </div>
</body>
</html>'
- def self.render_canvas(canvas)
- HEADER + super + FOOTER
+ def initialize
+ super
+ @visible = '<b></b>'
+ @invisible = '<i></i>'
+ @line_break = '<br>'
end
- @visible = '<b></b>'
- @invisible = '<i></i>'
- @line_break = '<br>'
+ def render_canvas(canvas)
+ HEADER + super + FOOTER
+ end
end
end
class BresenhamAlgorithm
FIRST_ROTATION = Matrix[ [1, 0], [0, 1] ]
SECOND_ROTATION = Matrix[ [0, 1], [1, 0] ]
THIRD_ROTATION = Matrix[ [1, 0], [0, -1] ]
FOURTH_ROTATION = Matrix[ [0, -1], [1, 0] ]
- class << self
- def rasterize_line(line)
- set_rotation_matrix(line.from, line.to)
+ def initialize(line)
+ set_rotation_matrix(line.from, line.to)
- from = line.from.transform(@rotation_matrix)
- to = line.to.transform(@rotation_matrix)
+ from = line.from.transform(@rotation_matrix)
+ to = line.to.transform(@rotation_matrix)
- set_variables(from, to)
- compute_points
- end
+ set_variables(from, to)
+ end
- private
+ def rasterize
+ starting_point = Point.new(@point_x, @point_y).transform(@inverse_matrix)
- def set_rotation_matrix(from, to)
- delta_x = (from.x - to.x).abs
- delta_y = (from.y - to.y).abs
+ [starting_point] + 1.upto(@delta_x).map do |_|
+ next_step
+ Point.new(@point_x, @point_y).transform(@inverse_matrix)
+ end
+ end
- if from.y <= to.y
- @rotation_matrix = delta_x >= delta_y ? FIRST_ROTATION : SECOND_ROTATION
- else
- @rotation_matrix = delta_x >= delta_y ? THIRD_ROTATION : FOURTH_ROTATION
- end
+ private
- @inverse_matrix = @rotation_matrix.inverse
+ def set_rotation_matrix(from, to)
+ delta_x = (from.x - to.x).abs
+ delta_y = (from.y - to.y).abs
+
+ if from.y <= to.y
+ @rotation_matrix = delta_x >= delta_y ? FIRST_ROTATION : SECOND_ROTATION
+ else
+ @rotation_matrix = delta_x >= delta_y ? THIRD_ROTATION : FOURTH_ROTATION
end
- def set_variables(from, to)
- @point_x = from.x
- @point_y = from.y
+ @inverse_matrix = @rotation_matrix.inverse
+ end
- @delta_x = (from.x - to.x).abs
- @delta_y = (from.y - to.y).abs
+ def set_variables(from, to)
+ @point_x = from.x
+ @point_y = from.y
- @delta = 2 * @delta_y - @delta_x
- end
+ @delta_x = (from.x - to.x).abs
+ @delta_y = (from.y - to.y).abs
- def compute_points
- starting_point = Point.new(@point_x, @point_y).transform(@inverse_matrix)
+ @delta = 2 * @delta_y - @delta_x
+ end
- [starting_point] + 1.upto(@delta_x).map do |_|
- next_step
- Point.new(@point_x, @point_y).transform(@inverse_matrix)
- end
+ def next_step
+ if @delta < 0
+ @point_x += 1
+ @delta += 2 * @delta_y
+ else
+ @point_x += 1
+ @point_y += 1
+ @delta += 2 * @delta_y - 2 * @delta_x
end
-
- def next_step
- if @delta < 0
- @point_x += 1
- @delta += 2 * @delta_y
- else
- @point_x += 1
- @point_y += 1
- @delta += 2 * @delta_y - 2 * @delta_x
- end
- end
end
end
class Point
attr_reader :x, :y
- alias_method :eql?, :==
-
def initialize(x, y)
@x = x
@y = y
end
def each
yield self
end
def transform(matrix)
Point.new(matrix[0, 0] * @x + matrix[0, 1] * @y,
matrix[1, 0] * @x + matrix[1, 1] * @y)
end
def to_s
"Point(#{@x}, #{@y})"
end
def hash
to_s.hash
end
def ==(other)
@x == other.x and @y == other.y
end
+
+ alias_method :eql?, :==
end
class Line
attr_reader :from, :to
- alias_method :eql?, :==
-
def initialize(from, to)
@from = from
@to = to
if @from.x > @to.x or (@from.x == @to.x and @from.y > @to.y)
@from, @to = @to, @from
end
end
def each(&block)
rasterize.each(&block)
end
def to_s
"Line(#{@from}, #{@to})"
end
def hash
to_s.hash
end
def ==(other)
@from == other.from and @to == other.to
end
+ alias_method :eql?, :==
+
def rasterize
- BresenhamAlgorithm.rasterize_line(self)
+ BresenhamAlgorithm.new(self).rasterize
end
end
class Rectangle
attr_reader :left, :right, :top_left, :top_right, :bottom_left, :bottom_right
- alias_method :eql?, :==
-
def initialize(point_a, point_b)
initialize_left_and_right(point_a, point_b)
initialize_vertices
end
def each(&block)
[
Line.new(@top_left, @top_right ),
Line.new(@bottom_left, @bottom_right),
Line.new(@top_left, @bottom_left ),
Line.new(@top_right, @bottom_right),
].map(&:rasterize).flatten.each(&block)
end
def to_s
"Rectangle(#{@top_left}, #{@top_right}, #{@bottom_left}, #{@bottom_right})"
end
def hash
to_s.hash
end
def ==(other)
@top_left == other.top_left and @bottom_right == other.bottom_right
end
+ alias_method :eql?, :==
+
private
def initialize_left_and_right(point_a, point_b)
@left = point_a
@right = point_b
if point_a.x > point_b.x or point_a.x == point_b.x and point_a.y > point_b.y
@left, @right = @right, @left
end
end
def initialize_vertices
- @top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
- @top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y )
+ @top_left = @left.y <= @right.y ? @left : Point.new(@left.x, @right.y)
+ @top_right = @right.y <= @left.y ? @right : Point.new(@right.x, @left.y )
- @bottom_left = @left != @top_left ? @left : Point.new(@left.x, @right.y)
- @bottom_right = @right != @top_right ? @right : Point.new(@right.x, @left.y )
+ @bottom_left = @left != @top_left ? @left : Point.new(@left.x, @right.y)
+ @bottom_right = @right != @top_right ? @right : Point.new(@right.x, @left.y )
end
end
class Canvas
include Enumerable
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
@data = Matrix.build(height, width) { false }
end
+ def each(&block)
+ @data.each(&block)
+ end
+
def set_pixel(x, y)
@data[y, x] = true
end
def pixel_at?(x, y)
@data[y, x]
end
def draw(figure)
figure.each { |point| @data[point.y, point.x] = true }
end
def render_as(renderer)
- renderer.render_canvas(self)
- end
-
- def each(&block)
- @data.each(&block)
+ renderer.new.render_canvas(self)
end
end
end
  • Може би вече си видял, че имах предвид вътрешно представяне на пано като Hash :)
  • В Руби няма концепция за "private" класове. Тези private и public методи в Renderers са излишни.
  • А защо не да рендерираш пано така:

      canvas.each_slice(canvas.width).map do |row|
        row.map { |pixel| render_pixel(pixel) }
      end.join(@line_break)
    
  • Не ми харесва да базираш hash методите на низ, виж аз как съм го направил в моето решение.

Като изключим тези неща, решението ти ми харесва - подредено е, има интересни идеи, спазва повечето Ruby конвенции и стил. Давам ти бонус точка.