迷路作成アルゴリズムまとめ
処理速度
アルゴリズム | N=100 | N=200 | N=400 | N=800 | N=1600 |
---|---|---|---|---|---|
棒倒し法 | 32 | 140 | 552 | 2396 | 12292 |
穴掘り法 | 48 | - | - | - | - |
穴掘り法 (非再帰) | 205 | 468 | 1824 | 6547 | 21709 |
壁埋め法 | 42 | - | - | - | - |
壁伸ばし法 | 122 | 244 | 971 | 4092 | 17972 |
壁伸ばし法 (非再帰) | 83 | 214 | 871 | 4617 | 22319 |
壁伸ばし法 (アレンジ) | 16 | 66 | 189 | 956 | 5067 |
迷路サイズ: N x N
単位: ms
-: Stack Overflow
特徴
- 棒倒し法
- 良い
- シンプル
- 巨大な迷路が安全に作れる
- ダメ
- 到達不能な場所ができる
- 対処するには変な補正が必要になり迷路が破綻する
- 到達不能な場所ができる
- 良い
- 穴掘り法
- 良い
- シンプル
- 完璧な迷路が作れる
- ダメ
- 大きな迷路が作れない
- 180x180 程度でスタックが死ぬ
- 大きな迷路が作れない
- 良い
- 穴掘り法 (非再帰)
- 良い
- 巨大な迷路が安全に作れる
- 完璧な迷路が作れる
- ダメ
- ひと目でコードを理解するのが難しい
- 良い
- 壁伸ばし法
- 良い
- 再帰なのに想定していたより大きな迷路が作れる
- 4096x4096 まで確認済み
- 完璧な迷路が作れる
- 複雑な迷路が作れる(と言われている)
- 感覚的なものもあってはっきり認識できてはいない
- 再帰なのに想定していたより大きな迷路が作れる
- ダメ
- どこかでサイズの限界がくる心配がある
- 良い
- 壁伸ばし法 (非再帰)
- 良い
- スタックオーバーフローの心配がない
- 良い
- 壁伸ばし法 (アレンジ)
- 更地をあえて残すようにしたもの
棒倒し法
小細工しなかったら美しい
StickKnockdown クラス
class StickKnockdown < Base
self.seed = 1
def self.build(...)
new.tap do |e|
e.build(...)
end
end
def build(...)
fill(:route)
odd_cells.each do |v|
instance_exec(v, ...)
end
end
end
1. 何も壁を置いていない状態
builder = StickKnockdown.build do |v|
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S ##
## ##
## ##
## ##
## ##
## ##
## G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
2. 等間隔に壁を配置する
左上が (0, 0) なので x と y が奇数のときだけ壁を配置する。build ブロックは x と y が奇数のときだけ呼ぶようにしている。v は x, y が入ったベクトルを表す。ここで配置した壁は絶対に壊れない。
builder = StickKnockdown.build do |v|
wall_set(v)
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S ##
## XX XX XX XX XX XX XX XX XX XX XX XX ##
## ##
## XX XX XX XX XX XX XX XX XX XX XX XX ##
## ##
## XX XX XX XX XX XX XX XX XX XX XX XX ##
## G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
ここで壁の個数を調べる。外周は無視する。
builder.field.values.count { |e| e == :wall } # => 36
3. 壁から4方向の一方に壁を生やす
builder = StickKnockdown.build do |v|
wall_set(v)
set(v + V.around.sample, :wall)
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
迷路らしくなってきた。ここでもまた壁の個数を調べると数がおかしい。
builder.field.values.count { |e| e == :wall } # => 69
これは対面する壁がお互いに対面に向かって壁を生やしたので重なってしまっている。
4. 重ならないように生やして完成
builder = StickKnockdown.build do |v|
wall_set(v)
found = V.around.shuffle.find { |e| field[v + e] == :route }
wall_set(v + found)
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
壁の個数も合っている。
builder.field.values.count { |e| e == :wall } # => 72
棒倒し法はここまでなら非常にシンプルで美しいアルゴリズムなのでこれ以上余計なことはしてはいけない。
インチキ補正
上の迷路を見ると中央に壁に囲まれた領域ができている。これを囲まれないようにするには
- 上下左右ではなく「下左右」だけに生やすようにする。
- ただし Y = 1 の行の壁たちが上に生やさなかったら Y = 0 の行が通路になってしまうため Y = 1 の行の壁たちはいままで通り上に生やす。
とすればいいのだけどこの小手先の補正が本当に美しくない。
builder = StickKnockdown.build do |v|
wall_set(v)
dirs = V.around
if v.y >= 3
dirs -= [V[0, -1]] # 上を除外する
end
found = dirs.shuffle.find { |e| field[v + e] == :route }
wall_set(v + found)
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
美しくないだけではない。上3行の通路が不自然にスカスカになるため、上3行だけを通ればスタート地点から右端まで移動できてしまうという迷路としては致命的な欠陥ができる。したがって囲まれた領域を許容できないならインチキ補正をするのではなく次の穴掘り法や壁伸ばし法を使う。
穴掘り法
シンプルで美しいが再帰を使うと巨大な迷路が作れない。
class DiggingOut < Base
self.logic_name = "穴掘り法"
def build
fill(:wall)
dig(start)
end
def dig(v0)
route_set(v0) # 今いる居る場所を掘る
V.around.shuffle.each do |e| # ランダムな順番で四方を向く
v1 = v0 + e # 1歩先
v2 = v1 + e # 2歩先
if field[v2] == :wall # 2歩先が壁なら
route_set(v1) # 1歩先を掘って
dig(v2) # 2歩先から同様に繰り返す
end
end
end
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
インチキしない棒倒し法とは異なり、絶対に囲まれた部分ができない特徴がある。そしてなによりアルゴリズムが美しい。しかし、
begin
DiggingOut.new(rect: Rect.from_w_h(180, 180)).build
rescue SystemStackError => error
error # => #<SystemStackError: stack level too deep>
end
180 x 180 程度で死んだ。
穴掘り法 (非再帰)
巨大な迷路に対応する。
class DiggingOut2 < DiggingOut
self.logic_name = "穴掘り法 (非再帰)"
def dig(v0)
stack = [v0]
until stack.empty?
v0 = stack.last
if @field[v0] == :wall # 壁のときだけ
route_set(v0) # 掘る
end
v = V.around.shuffle.find { |v| @field[v0 + v + v] == :wall }
unless v
stack.pop
next
end
route_set(v0 + v)
stack.push(v0 + v + v)
end
end
end
再帰だとすっきりするコードが、非再帰だと動いているのが不思議なほどややこしくなる。ループの中で再帰呼び出ししている場合は単にスタックに置き換えればいいわけではない。とくに route_set(v0)
が何度も呼ばれないように、再帰では必要なかった「壁のときだけ」の条件が必要な点や、last と pop を使い分ける部分の理解が難しい。
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
とりあえずこれで巨大な迷路の作成も可能になった。
begin
DiggingOut2.new(rect: Rect.from_w_h(180, 180)).build
rescue SystemStackError => error
error # =>
end
壁埋め法 (勝手に命名)
穴掘り法の壁と通路を逆にしたもの。外周に通路ができる。
class PutUpWall < Base
self.logic_name = "壁埋め法"
def build
fill(:route) # 全体を空にしておく
dig(V[1, 1]) # 左上から1つ離して開始する
end
def dig(v0)
wall_set(v0) # 今いる居る場所を掘る
V.around.shuffle.each do |e| # ランダムな順番で四方を向く
v1 = v0 + e # 1歩先
v2 = v1 + e # 2歩先
if field[v2] == :route # 2歩先が空なら
wall_set(v1) # 1歩先を壁にする
dig(v2) # 2歩先から同様に繰り返す
end
end
end
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
壁伸ばし法
壁を伸ばすうちに自分の胴体に刺さると輪ができてしまうのでそれを回避するのがやや難しい。
class GrowingTree < Base
self.logic_name = "壁伸ばし法"
def build
# 全体を通路にする
fill(:route)
# 奇数の位置がまだ通路ならそこから生やしていく
odd_cells.shuffle.each do |v0|
if field[v0] == :route
@walls = Set.new
dig(v0)
end
end
end
def dig(v0)
wall_set(v0)
V.around.shuffle.each do |e|
v1 = v0 + e # 1歩目
v2 = v1 + e # 2歩目
# 自分に刺さって輪ができるのを防ぐ
# 輪ができていいなら取ってもいい
if @walls.include?(v2)
next
end
if field[v1] == :route
wall_set(v1) # 2歩先を見る前に1歩先を壁にする
if field[v2] == :route
dig(v2)
end
break
end
end
end
def wall_set(v)
super
@walls << v
end
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
この方法は再帰を使っていてもスタックがそれほど深くならないため大きな迷路を作成することができる。
rect = Rect.from_w_h(4096, 4096)
builder = GrowingTree.new(rect: rect)
builder.build
builder.to_s.size # => 52252672
それでも不安な場合は次の非再帰を使う。
壁伸ばし法 (非再帰)
再帰をやめて安全に巨大な迷路を安全に作れるようにしたもの。
class GrowingTree2 < GrowingTree
self.logic_name = "壁伸ばし法 (非再帰)"
def dig(v0)
stack = [v0]
until stack.empty?
v0 = stack.pop
wall_set(v0)
v = V.around.shuffle.find do |v|
v1 = v0 + v # 1歩目
v2 = v1 + v # 2歩目
field[v1] == :route && !@walls.include?(v2)
end
if v
v1 = v0 + v # 1歩目
v2 = v1 + v # 2歩目
wall_set(v1) # 2歩先を見る前に1歩先を壁にする
if field[v2] == :route
stack.push(v2)
end
end
end
end
end
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
rect = Rect.from_w_h(4096, 4096)
builder = GrowingTree2.new(rect: rect)
builder.build
builder.to_s.size # => 52252672
壁伸ばし法 (アレンジ)
「壁伸ばし開始回数」と「伸ばす最大の長さ」を指定できるようにすると迷路風のステージができる。
class StageBuilder < Base
self.logic_name = "壁伸ばし法 (アレンジ)"
def build(birth_max: 3, dig_max: 8)
@dig_max = dig_max
fill(:route)
birth = 0
odd_cells.shuffle.each do |v0|
if field[v0] == :route
if birth >= birth_max
break # もう壁伸ばしを開始しない
end
@walls = Set.new
dig(v0)
birth += 1
end
end
end
def dig(v0, depth = 0)
if depth >= @dig_max
return # 伸ばすのを止める
end
wall_set(v0)
V.around.shuffle.each do |e|
v1 = v0 + e
v2 = v1 + e
if @walls.include?(v2)
next
end
if field[v1] == :route
wall_set(v1)
if field[v2] == :route
dig(v2, depth.next)
end
break
end
end
end
def wall_set(v)
super
@walls << v
end
end
builder = StageBuilder.new
builder.build(birth_max: 20, dig_max: 1)
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S ##
## XX XX XX XX XX XX XX XX ##
## XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
builder = StageBuilder.new
builder.build(birth_max: 10, dig_max: 5)
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## S XX ##
## XX XX XX XX XX XX XX XX XX ##
## XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX XX XX ##
## XX XX XX XX XX XX XX XX XX XX XX XX XX ##
## XX XX XX G ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
共通処理
コード
require "matrix"
require "pathname"
require "active_support/core_ext/class/attribute"
require "active_support/core_ext/module/delegation"
class V < Vector
class << self
def l
self[-1, 0]
end
def r
self[1, 0]
end
def u
self[0, -1]
end
def d
self[0, 1]
end
def around
[
u,
r,
d,
l,
]
end
end
def initialize(...)
super
freeze
end
def x
self[0]
end
def y
self[1]
end
def length_squared
x**2 + y**2
end
def distance_squared_to(other)
(other - self).length_squared
end
def distance_to(other)
Math.sqrt(distance_squared_to(other))
end
def to_s
"(#{x},#{y})"
end
def inspect
to_s
end
end
Rect = Data.define(:w, :h) do
def self.from_w_h(...)
new(...)
end
def cover?(v)
(0...w).cover?(v.x) && (0...h).cover?(v.y)
end
def w_h
[w, h]
end
end
class Base
class_attribute :logic_name, default: "?"
class_attribute :seed, default: 0
attr_accessor :rect
attr_accessor :field
class << self
def build(...)
new(...).tap do |e|
e.build
end
end
end
def initialize(rect: Rect.from_w_h(25, 7), seed: self.class.seed, **options)
@rect = rect
@options = options
@field = {}
@nodes_hash = {}
@counts = Hash.new(0)
if seed
srand(seed)
end
end
def build
fill(:wall)
non_recursive_dig(start)
end
def to_s(...)
border_type = " ##"
border_line = ([border_type] * (@rect.w + 2)).join
s = [
border_line,
@rect.h.times.collect { |y|
[
border_type,
@rect.w.times.collect { |x| "%3s" % cell_as_string(V[x, y], ...) },
border_type,
].join
},
border_line,
] * "\n"
s.gsub(/^\s+/, "")
end
def to_s_start_to_goal
to_s do |v|
if start_to_goal
if start_to_goal.include?(v)
"o"
end
end
end
end
def output(file)
Pathname(file).write(to_s)
end
def walk_info
{
"名前" => logic_name,
"歩数" => @counts[:walk],
"重複" => @counts[:uturn], # 同じ場所を歩いた歩数
}
end
private
def recursive_dig(v0)
route_set(v0)
V.around.shuffle.each do |e|
v1 = v0 + e
v2 = v1 + e
if @field[v2] == :wall
route_set(v1)
dig(v2)
end
end
end
def non_recursive_dig(v0)
stack = [v0]
until stack.empty?
v0 = stack.last
if @field[v0] == :wall
route_set(v0)
end
v = V.around.shuffle.find { |v| @field[v0 + v + v] == :wall }
unless v
stack.pop
next
end
route_set(v0 + v)
stack.push(v0 + v + v)
end
end
def fill(value)
cell_each do |v|
@field[v] = value
end
end
def cell_each(&block)
@rect.h.times do |y|
@rect.w.times do |x|
yield V[x, y]
end
end
end
def odd_cells
@odd_cells ||= [].tap do |av|
cell_each do |v|
if v.x.odd? && v.y.odd?
av << v
end
end
end
end
def even_cells
@even_cells ||= [].tap do |av|
cell_each do |v|
if v.x.even? && v.y.even?
av << v
end
end
end
end
def invalid?(v)
!valid?(v)
end
def valid?(v)
@rect.cover?(v)
end
def wall_set(v)
@field[v] == :route or raise ArgumentError, v.inspect
set(v, :wall)
end
def route_set(v)
@field[v] == :wall or raise ArgumentError, v.inspect
set(v, :route)
end
def top_left
V[0, 0]
end
def start
V[0, 0]
end
def goal
V[@rect.w.pred, @rect.h.pred]
end
def assert_start_and_goal_is_route
@field[start] == :route or raise
@field[goal] == :route or raise
end
def assert_inside_field(v)
valid?(v) or raise ArgumentError, v.inspect
end
def set(v, value)
assert_inside_field(v)
@field[v] = value
end
def walked_at(v, node)
assert_inside_field(v)
if @nodes_hash[v]
@counts[:uturn] += 1
end
@nodes_hash[v] = node
@counts[:walk] += 1
end
def cell_as_string(v, **options, &block)
if block
if s = block.call(v, options)
return s
end
end
case
when @field[v] == :wall
"XX"
when f = @nodes_hash[v]
case
when f.respond_to?(:to_footprint)
f.to_footprint(**options)
when f.respond_to?(:to_i)
f.to_i % 1000
else
"."
end
when v == start
"S"
when v == goal
"G"
when @field[v] == :route
""
else
raise v.inspect
end
end
def start_to_goal
@start_to_goal ||= yield_self do
if node = @nodes_hash[goal]
node.self_and_ancestors.reverse.collect(&:v)
end
end
end
end
class Node
attr_accessor :v
attr_accessor :parent
attr_accessor :path_cost
def initialize(v:, parent: nil)
@v = v
@parent = parent
@path_cost = 0
if @parent
@path_cost = @parent.path_cost.next
end
end
def total_cost
@path_cost
end
def to_footprint(**options)
total_cost
end
def self_and_ancestors
[self] + (parent ? parent.self_and_ancestors : [])
end
end
if true
require "rmagick"
include Magick
class ImageFormatter
delegate :rect, :field, to: :@base
def initialize(base, **options)
@base = base
@options = {
:route => 32,
:wall => 32,
:padding => nil,
:route_color => "hsl(0,0%,98%)",
:wall_color => "hsl(0,0%,60%)",
:frame_color => "hsl(0,0%,60%)",
}.merge(options)
end
def write
canvas.write(@options[:path])
end
private
def canvas
bg_layer.composite(fg_layer, *padding.w_h, OverCompositeOp)
end
def bg_layer
Image.new(*bg_layer_rect.w_h) do |e|
e.background_color = @options[:frame_color]
end
end
def fg_layer
layer = Image.new(*fg_layer_rect.w_h) do |e|
e.background_color = @options[:route_color]
end
wall_draw(layer)
layer
end
def wall_draw(layer)
cy = 0
rect.h.times do |y|
if y.odd?
h = wall.h
else
h = route.h
end
cx = 0
rect.w.times do |x|
if x.odd?
w = wall.w
else
w = route.w
end
v = V[x, y]
if field[v] == :wall
g = Draw.new
g.fill(@options[:wall_color])
g.rectangle(cx, cy, cx + w - 1, cy + h - 1)
g.draw(layer)
end
cx += w
end
cy += h
end
end
def fg_layer_rect
@fg_layer_rect ||= yield_self do
Rect.from_w_h(*[
(rect.w / 2 * wall.w) + ((rect.w / 2 + 1) * route.w),
(rect.h / 2 * wall.h) + ((rect.h / 2 + 1) * route.h),
])
end
end
def bg_layer_rect
@bg_layer_rect ||= Rect.from_w_h(*[
fg_layer_rect.w + padding.w * 2,
fg_layer_rect.h + padding.h * 2,
])
end
def route
@route ||= Rect.from_w_h(@options[:route], @options[:route])
end
def wall
@wall ||= Rect.from_w_h(@options[:wall], @options[:wall])
end
def padding
@padding ||= yield_self do
v = @options[:padding] || @options[:wall]
Rect.from_w_h(v, v)
end
end
end
Base.class_eval do
def write(path, **options)
options = { path: path }.merge(options)
ImageFormatter.new(self, **options).write
end
end
end
if $0 == __FILE__
require "test/unit"
class TestMain < Test::Unit::TestCase
test "works" do
object = Base.new(seed: nil)
object.build
object.to_s
end
test "works" do
10.times do |w|
10.times do |h|
object = Base.new(rect: Rect.from_w_h(w, h), seed: nil)
object.build
object.to_s
end
end
end
end
end
Discussion