meiro 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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