meiro 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ module Meiro
2
+ class FloorError < StandardError; end
3
+ class TrySeparateLimitError < FloorError; end
4
+
5
+ class Floor
6
+
7
+ TRY_SEPARATE_LIMIT = 1000000
8
+
9
+ attr_reader :dungeon, :width, :height,
10
+ :min_room_width, :min_room_height,
11
+ :max_room_width, :max_room_height
12
+
13
+ class Config < Options
14
+ option :width, Integer, nil, lambda {|w,o| !w.nil? && w >= FLOOR_MIN_WIDTH }
15
+ option :height, Integer, nil, lambda {|h,o| !h.nil? && h >= FLOOR_MIN_HEIGHT }
16
+ option :min_room_width, Integer, nil, lambda {|w,o| !w.nil? && w >= ROOM_MIN_WIDTH }
17
+ option :min_room_height, Integer, nil, lambda {|h,o| !h.nil? && h >= ROOM_MIN_HEIGHT }
18
+ option :max_room_width, Integer, nil, lambda {|w,o| !w.nil? && w >= o[:min_room_width] }
19
+ option :max_room_height, Integer, nil, lambda {|h,o| !h.nil? && h >= o[:min_room_height] }
20
+ end
21
+
22
+ def initialize(d, w, h, min_rw, min_rh, max_rw, max_rh)
23
+ opts = {
24
+ width: w,
25
+ height: h,
26
+ min_room_width: min_rw,
27
+ min_room_height: min_rh,
28
+ max_room_width: max_rw,
29
+ max_room_height: max_rh,
30
+ }
31
+ config = Config.new(opts)
32
+ @dungeon = d
33
+ @min_room_width = config.min_room_width
34
+ @min_room_height = config.min_room_height
35
+ @max_room_width = config.max_room_width
36
+ @max_room_height = config.max_room_height
37
+ @root_block = Block.new(self, 0, 0, config.width, config.height)
38
+ fill_floor_by_wall(config.width, config.height)
39
+ end
40
+
41
+ def width
42
+ @base_map.width
43
+ end
44
+
45
+ def height
46
+ @base_map.height
47
+ end
48
+
49
+ def [](x, y)
50
+ @base_map[x, y]
51
+ end
52
+
53
+ def all_blocks
54
+ @root_block.flatten
55
+ end
56
+
57
+ def all_rooms
58
+ @all_rooms ||= all_blocks.map{|b| b.room }.compact
59
+ end
60
+
61
+ def each_line(&block)
62
+ @base_map.each_line(&block)
63
+ end
64
+
65
+ def each_tile(&block)
66
+ @base_map.each_tile(&block)
67
+ end
68
+
69
+ # このフロアに属するすべての部屋のマスのxy座標を返す
70
+ def all_room_tiles_xy
71
+ res = []
72
+ all_rooms.each do |room|
73
+ res << room.get_all_abs_coordinate
74
+ end
75
+ res.flatten(1)
76
+ end
77
+
78
+ # 指定したx座標、y座標を含むBlockを返す。そのBlockが親(分割済)であっ
79
+ # た場合は、分割されたいずれかのうち、x座標、y座標を含む方を返す。
80
+ # 該当するものがない場合はnilを返す。
81
+ def get_block(x, y, from=nil)
82
+ from ||= @root_block
83
+ if from.include?(x, y)
84
+ if from.separated?
85
+ get_block(x, y, from.upper_left) ||
86
+ get_block(x, y, from.lower_right)
87
+ else
88
+ from
89
+ end
90
+ else
91
+ nil
92
+ end
93
+ end
94
+
95
+ # 指定したx座標、y座標を含むRoomを返す。
96
+ # 該当するものがない場合はnilを返す。
97
+ def get_room(x, y)
98
+ block = get_block(x, y)
99
+ return nil if block.nil?
100
+ if block.room && block.room.include?(x, y)
101
+ block.room
102
+ else
103
+ nil
104
+ end
105
+ end
106
+
107
+ def classify!(type=:rogue_like)
108
+ @base_map.classify!(type)
109
+ end
110
+
111
+ def to_s
112
+ res = []
113
+ @base_map.each_line do |line|
114
+ res << (line.map(&:to_s) << "\n").join
115
+ end
116
+ res.join
117
+ end
118
+
119
+ def fill_floor_by_wall(w, h)
120
+ @base_map = BaseMap.new(w, h, Tile.wall)
121
+ end
122
+
123
+ # ランダムで部屋と通路を生成する
124
+ def generate_random_room(r_min, r_max, factor, randomizer)
125
+ separate_blocks(r_min, r_max, factor, randomizer)
126
+ all_blocks.each do |block|
127
+ block.put_room(randomizer)
128
+ end
129
+ connect_rooms(randomizer)
130
+ apply_rooms_to_map
131
+ self
132
+ end
133
+
134
+ # このFloorに設置された部屋に対し、互いを通路で結ぶ処理をかける
135
+ def connect_rooms(randomizer=nil)
136
+ randomizer ||= Random.new(Time.now.to_i)
137
+ all_rooms.each do |room|
138
+ room.create_passage(randomizer)
139
+ end
140
+ end
141
+
142
+ # このFloorに設置された部屋全てとそれに紐づく通路・出口を@base_map
143
+ # に反映させる
144
+ def apply_rooms_to_map
145
+ all_rooms.each do |room|
146
+ @base_map.apply_room(room, Tile.flat)
147
+ end
148
+ @base_map.apply_passage(all_rooms, Tile.gate, Tile.passage)
149
+ end
150
+
151
+ # 設定された部屋数のMIN,MAXの範囲に収まるよう区画をランダムで分割
152
+ def separate_blocks(r_min, r_max, factor, randomizer)
153
+ new_block = [@root_block]
154
+ try_count = 0
155
+
156
+ while new_block.any?
157
+ tried = []
158
+ separated = []
159
+ next_new_block = []
160
+
161
+ while b = new_block.shift
162
+ if b.separatable? && do_separate?(b, factor, randomizer)
163
+ b.separate
164
+ separated << b
165
+ next_new_block << b.upper_left
166
+ next_new_block << b.lower_right
167
+ end
168
+ tried << b
169
+ end
170
+
171
+ block_num = @root_block.flatten.size
172
+ if block_num > r_max
173
+ # MAXを越えてしまったら直前の分割を取り消してやり直し
174
+ separated.each {|b| b.unify }
175
+ new_block = tried
176
+ elsif next_new_block.empty? && block_num < r_min
177
+ # 全ての分割が完了し、MINに到達しない場合は直前の分割をやり直し
178
+ new_block = tried
179
+ else
180
+ new_block = next_new_block
181
+ end
182
+
183
+ try_count += 1
184
+ if try_count > TRY_SEPARATE_LIMIT
185
+ raise TrySeparateLimitError, "could not create expected floor"
186
+ end
187
+ end
188
+ self
189
+ end
190
+
191
+ private
192
+
193
+ def do_separate?(block, factor, randomizer)
194
+ # Blockが細分化するほど、分割されづらくなる
195
+ block.generation / factor < randomizer.rand(10)
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,117 @@
1
+ module Meiro
2
+ class MapLayer
3
+ def initialize(width, height)
4
+ @map = Array.new(height)
5
+ @map.map! {|line| Array.new(width) }
6
+ end
7
+
8
+ def [](x, y)
9
+ if 0 <= x && 0 <= y && x < width && y < height
10
+ @map[y][x]
11
+ else
12
+ nil
13
+ end
14
+ end
15
+
16
+ def []=(x, y, obj)
17
+ @map[y][x] = obj
18
+ end
19
+
20
+ def width
21
+ @map.first.size
22
+ end
23
+
24
+ def height
25
+ @map.size
26
+ end
27
+
28
+ def each_line(&block)
29
+ @map.each do |line|
30
+ yield(line)
31
+ end
32
+ end
33
+
34
+ def each_tile(&block)
35
+ @map.each_with_index do |line, y|
36
+ line.each_with_index do |tile, x|
37
+ yield(x, y, tile)
38
+ end
39
+ end
40
+ end
41
+
42
+ def get_around(x, y)
43
+ new = MapLayer.new(width, height)
44
+ around = [
45
+ [self[x-1, y-1], self[ x, y-1], self[x+1, y-1]],
46
+ [self[x-1, y], self[ x, y], self[x+1, y]],
47
+ [self[x-1, y+1], self[ x, y+1], self[x+1, y+1]],
48
+ ]
49
+ new.instance_variable_set(:@map, around)
50
+ new
51
+ end
52
+
53
+ def dup
54
+ new = self.class.new(width, height)
55
+ new.each_tile do |x, y, tile|
56
+ new[x, y] = self[x, y].dup
57
+ end
58
+ end
59
+ end
60
+
61
+ class BaseMap < MapLayer
62
+ def initialize(width, height, klass=nil)
63
+ proc = klass.nil? ? lambda { nil } : lambda { klass.new }
64
+ @map = Array.new(height)
65
+ @map.map! {|line| Array.new(width).map! {|e| proc.call } }
66
+ end
67
+
68
+ def fill_rect(x1, y1, x2, y2, klass)
69
+ x_begin, x_end = x1 <= x2 ? [x1, x2] : [x2, x1]
70
+ y_begin, y_end = y1 <= y2 ? [y1, y2] : [y2, y1]
71
+
72
+ (y_begin..y_end).each do |y|
73
+ (x_begin..x_end).each do |x|
74
+ self[x, y] = klass.new
75
+ end
76
+ end
77
+ end
78
+
79
+ def apply_room(room, klass)
80
+ room.each_coordinate do |x, y|
81
+ self[x, y] = klass.new
82
+ end
83
+ end
84
+
85
+ def apply_passage(rooms, gate_klass, pass_klass)
86
+ all_pass = []
87
+ all_gates = []
88
+ rooms.each do |room|
89
+ all_pass << room.all_pass
90
+ all_gates << room.gate_coordinates
91
+ end
92
+
93
+ all_pass.flatten.uniq.each do |p|
94
+ self.fill_rect(p.start_x, p.start_y, p.end_x, p.end_y, pass_klass)
95
+ end
96
+
97
+ all_gates.flatten(1).each do |x, y|
98
+ self[x, y] = gate_klass.new
99
+ end
100
+ end
101
+
102
+ def classify(type=:rogue_like)
103
+ res = self.class.new(width, height)
104
+ res.each_tile do |x, y, tile|
105
+ res[x, y] = Tile.classify(self.get_around(x, y), type)
106
+ end
107
+ res
108
+ end
109
+
110
+ def classify!(type=:rogue_like)
111
+ self.each_tile do |x, y, tile|
112
+ self[x, y] = Tile.classify(self.get_around(x, y), type)
113
+ end
114
+ self
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,56 @@
1
+ module Meiro
2
+ class Options
3
+ class << self
4
+ attr_reader :keys, :validators
5
+
6
+ def option(symbol, klass, default=nil, check=nil)
7
+ @keys ||= []
8
+ @keys << symbol
9
+ @validators ||= {}
10
+ @validators[symbol] ||= {}
11
+ @validators[symbol][:class] = klass
12
+ @validators[symbol][:check] = check if check && check.kind_of?(Proc)
13
+ define_method(symbol) do
14
+ option_value = @config[symbol] ||
15
+ (default.kind_of?(Proc) ? default.call(self) : default)
16
+ @applied_config[symbol] = option_value
17
+ end
18
+ end
19
+ end
20
+
21
+ def initialize(config)
22
+ @config = validate(config)
23
+ @applied_config = {}
24
+ end
25
+
26
+ def keys
27
+ self.class.keys
28
+ end
29
+
30
+ private
31
+
32
+ def validate(config)
33
+ validate_keys(config)
34
+ validate_values(config)
35
+ config
36
+ end
37
+
38
+ def validate_keys(config)
39
+ valid_symbols = self.class.keys
40
+ config.each_key do |k|
41
+ raise "Invalid option: #{k}" unless valid_symbols.include? k
42
+ end
43
+ end
44
+
45
+ def validate_values(config)
46
+ validators = self.class.validators
47
+ config.each do |k, v|
48
+ klass = validators[k][:class]
49
+ proc = validators[k][:check] || lambda {|v, config| true }
50
+ unless v.kind_of?(klass) && proc.call(v, config)
51
+ raise "Invalid option: #{k}(#{klass}) => #{v}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ module Meiro
2
+ class Partition
3
+ attr_reader :x, :y, :length
4
+
5
+ def initialize(x, y, length, shape)
6
+ @x = x
7
+ @y = y
8
+ @length = length
9
+ @shape = shape
10
+ end
11
+
12
+ def horizontal?
13
+ @shape == :horizontal
14
+ end
15
+
16
+ def vertical?
17
+ @shape == :vertical
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module Meiro
2
+ class Passage
3
+ def initialize(x1, y1, x2, y2)
4
+ @start = [x1, y1]
5
+ @end = [x2, y2]
6
+ end
7
+
8
+ def start_x
9
+ @start[0]
10
+ end
11
+
12
+ def start_y
13
+ @start[1]
14
+ end
15
+
16
+ def end_x
17
+ @end[0]
18
+ end
19
+
20
+ def end_y
21
+ @end[1]
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,233 @@
1
+ module Meiro
2
+ class Room
3
+ attr_reader :relative_x, :relative_y,
4
+ :width, :height, :block
5
+ attr_accessor :connected_rooms, :all_pass
6
+
7
+ def initialize(width, height)
8
+ if width < ROOM_MIN_WIDTH || height < ROOM_MIN_HEIGHT
9
+ raise "width/height is too small for Meiro::Room"
10
+ end
11
+ @width = width
12
+ @height = height
13
+ @connected_rooms = {}
14
+ @all_pass = []
15
+ end
16
+
17
+ # floorにおける絶対x座標
18
+ # ブロックのx座標に、ブロックのx座標を基準とした相対x座標を足しあわ
19
+ # せたものを返す
20
+ def x
21
+ return nil if @block.nil?
22
+ @block.x + @relative_x
23
+ end
24
+
25
+ # floorにおける絶対y座標
26
+ # ブロックのy座標に、ブロックのy座標を基準とした相対y座標を足しあわ
27
+ # せたものを返す
28
+ def y
29
+ return nil if @block.nil?
30
+ @block.y + @relative_y
31
+ end
32
+
33
+ # ブロックのx座標を基準とした相対x座標
34
+ def relative_x=(x)
35
+ if @block
36
+ if x < available_x_min || x > available_x_max
37
+ raise "could not set relative_x-coordinate [#{x}] in this block.\n" \
38
+ "you can use #{available_x_min}..#{available_x_max}"
39
+ end
40
+ end
41
+ @relative_x = x
42
+ end
43
+
44
+ # ブロックのy座標を基準とした相対y座標
45
+ def relative_y=(y)
46
+ if @block
47
+ if y < available_y_min || y > available_y_max
48
+ raise "could not set relative_y-coordinate [#{y}] in this block.\n" \
49
+ "you can use #{available_y_min}..#{available_y_max}"
50
+ end
51
+ end
52
+ @relative_y = y
53
+ end
54
+
55
+ # 部屋のマスの全ての絶対xy座標を返す
56
+ def get_all_abs_coordinate
57
+ res = []
58
+ self.each_coordinate do |_x, _y|
59
+ res << [_x, _y]
60
+ end
61
+ res
62
+ end
63
+
64
+ # 引数で渡した座標を部屋の内部に含むかどうかを返す
65
+ def include?(_x, _y)
66
+ self.get_all_abs_coordinate.include?([_x, _y])
67
+ end
68
+
69
+ def block=(block)
70
+ @block = block
71
+ if @relative_x && @relative_y
72
+ begin
73
+ # 再代入して x, y が適切かチェック
74
+ self.relative_x = @relative_x
75
+ self.relative_y = @relative_y
76
+ rescue => e
77
+ @block = nil
78
+ raise e
79
+ end
80
+ end
81
+ @block
82
+ end
83
+
84
+ def set_random_coordinate(randomizer=nil)
85
+ if @block.nil?
86
+ raise "Block is not found"
87
+ else
88
+ randomizer ||= Random.new(Time.now.to_i)
89
+ self.relative_x = randomizer.rand(available_x_min..available_x_max)
90
+ self.relative_y = randomizer.rand(available_y_min..available_y_max)
91
+ end
92
+ return [@relative_x, @relative_y]
93
+ end
94
+
95
+ def available_x_min
96
+ Block::MARGIN
97
+ end
98
+
99
+ def available_x_max
100
+ @block.width - (@width + Block::MARGIN)
101
+ end
102
+
103
+ def available_y_min
104
+ Block::MARGIN
105
+ end
106
+
107
+ def available_y_max
108
+ @block.height - (@height + Block::MARGIN)
109
+ end
110
+
111
+ def each_coordinate(&block)
112
+ @height.times do |h|
113
+ @width.times do |w|
114
+ yield(x + w, y + h)
115
+ end
116
+ end
117
+ end
118
+
119
+ def generation
120
+ @block.generation if @block
121
+ end
122
+
123
+ def partition
124
+ @block.partition if @block
125
+ end
126
+
127
+ def brother
128
+ @block.brother.room if @block && @block.brother
129
+ end
130
+
131
+ def gate_coordinates
132
+ @connected_rooms.keys
133
+ end
134
+
135
+ def all_connected_rooms
136
+ @connected_rooms.values
137
+ end
138
+
139
+ def create_passage(randomizer=nil)
140
+ randomizer ||= Random.new(Time.now.to_i)
141
+ connectable_rooms.each do |room|
142
+ create_passage_to(room, randomizer)
143
+ end
144
+ end
145
+
146
+ # この部屋と通路で接続可能な部屋を返す
147
+ def connectable_rooms
148
+ rooms = []
149
+ # 隣接するブロックに所属する部屋を接続可能とする
150
+ @block.neighbors.each do |b|
151
+ rooms << b.room if b.has_room?
152
+ end
153
+ rooms
154
+ end
155
+
156
+ # 接続対象の部屋との仕切り(Partition)を返す
157
+ def select_partition(room)
158
+ @block.find_ancestor(room.block).partition
159
+ end
160
+
161
+ # 部屋からPartitionに向けて伸ばす通路の出口を決める
162
+ def get_random_gate(partition, randomizer=nil)
163
+ randomizer ||= Random.new(Time.now.to_i)
164
+ if partition.horizontal?
165
+ if self.y < partition.y
166
+ # Partitionがこの部屋より下にある
167
+ gate_y = self.y + @height
168
+ else
169
+ # Partitionがこの部屋より上にある
170
+ gate_y = self.y - 1
171
+ end
172
+ gate_x = randomizer.rand((self.x + 1)...(self.x + @width - 1))
173
+ checker = [[1, 0], [-1, 0]]
174
+ else
175
+ if self.x < partition.x
176
+ # Partitionがこの部屋より右にある
177
+ gate_x = self.x + @width
178
+ else
179
+ # Partitionがこの部屋より左にある
180
+ gate_x = self.x - 1
181
+ end
182
+ gate_y = randomizer.rand((self.y + 1)...(self.y + @height - 1))
183
+ checker = [[0, 1], [0, -1]]
184
+ end
185
+
186
+ retry_flg = false
187
+ checker.each do |dx, dy|
188
+ # となり合うGateが作られた場合はやり直し
189
+ retry_flg |= true if @connected_rooms[[gate_x + dx, gate_y + dy]]
190
+ end
191
+ gate_x, gate_y = get_random_gate(partition, randomizer) if retry_flg
192
+
193
+ [gate_x, gate_y]
194
+ end
195
+
196
+ # 自身と引数で渡した部屋とを接続する通路を作成する
197
+ def create_passage_to(room, randomizer=nil)
198
+ # 接続済みの部屋とは何もしない
199
+ return true if all_connected_rooms.include?(room)
200
+ # 同じ親の同世代の部屋と接続済みなら何もしない
201
+ return true if brother && brother.all_connected_rooms.include?(room)
202
+
203
+ randomizer ||= Random.new(Time.now.to_i)
204
+ partition = select_partition(room)
205
+
206
+ # 部屋から通路への出口を決定
207
+ gate_xy = self.get_random_gate(partition, randomizer)
208
+ o_gate_xy = room.get_random_gate(partition, randomizer)
209
+
210
+ created_pass = []
211
+ # 各部屋のGateからPartitionへ伸びる通路を作成
212
+ [gate_xy, o_gate_xy].each do |gx, gy|
213
+ if partition.horizontal?
214
+ created_pass << Passage.new(gx, gy, gx, partition.y)
215
+ else
216
+ created_pass << Passage.new(gx, gy, partition.x, gy)
217
+ end
218
+ end
219
+
220
+ # 上で作られた通路同士を連結する通路を作成
221
+ created_pass << Passage.new(created_pass[0].end_x, created_pass[0].end_y,
222
+ created_pass[1].end_x, created_pass[1].end_y)
223
+
224
+ created_pass.each do |p|
225
+ @all_pass << p
226
+ room.all_pass << p
227
+ end
228
+ @connected_rooms[gate_xy] = room
229
+ room.connected_rooms[o_gate_xy] = self
230
+ true
231
+ end
232
+ end
233
+ end