rdgc-dm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2010 parrot_studio<parrot *at* users.sourceforge.jp>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,148 @@
1
+ = rdgc-dm
2
+ Author:: parrot_studio <parrot *at* users.sourceforge.jp>
3
+ License:: The MIT License
4
+
5
+ * 要求されたサイズのランダムダンジョンを生成
6
+ * ダンジョンは部屋と道で構成される
7
+
8
+ == Install
9
+ gem install gemcutter # インストール済みなら不要
10
+ gem install rdgc-dm
11
+
12
+ == Usage
13
+ require 'rubygems'
14
+ require 'rdgc-dm'
15
+
16
+ include RDGC::Maker
17
+
18
+ board = DivideDungeonMaker.create(30, 40) # width=30, height=40のMap::Boardを作る場合
19
+ board = DivideDungeonMaker.create(30, 40, :min_room_count = 4) # パラメータ指定(4つ以上の部屋数を期待)
20
+
21
+ board.each do |x, y| # each で各座標を順に処理
22
+ t = board.tile(x, y) # Tileオブジェクト取得
23
+ case
24
+ when t.wall? # 壁
25
+ ...
26
+ when t.room? # 部屋
27
+ ...
28
+ when t.road? # 道
29
+ ...
30
+ end
31
+ end
32
+
33
+ board.each_tile do |x, y, t| # each_tileで座標とTileを一緒に取得
34
+ ...
35
+ end
36
+
37
+ rooms = board.rooms # Map::Roomオブジェクトの配列取得
38
+ roads = board.roads # Map::Roadオブジェクトの配列取得
39
+
40
+ r = rooms.choice
41
+ x, y = r.random_point # あるエリアのランダムな座標を取得
42
+
43
+ board.room?(2, 3) # 指定座標(x, y)が部屋か判定
44
+ board.road?(2, 3) # 指定座標(x, y)が道か判定
45
+ board.movable?(2, 3) # 指定座標(x, y)が移動可能(=部屋or道)か判定
46
+
47
+ # その他、Map::Areaに定義されたメソッドは全て使える
48
+
49
+ # RDGC::Util::RandomUtilで定義され、top-levelにinclude済みのメソッド
50
+ # 数値は全て整数を指定すること
51
+
52
+ bool_rand # trueかfalseを返す
53
+ range_rand(min, max) # minからmaxまでのどれかの整数値を返す
54
+ select_rand(:a => 3, :b => 2, :c => 1) # :aを3/(3+2+1)、:bを2/(3+2+1)...の確率で返す
55
+
56
+ dice(5, 10) # 10面のサイコロを5回振った合計を返す
57
+ 5.dice(10) # Integer#dice(max)が定義済みで、この場合はdice(5, 10)と同じ
58
+ 5.d10 # TRPGプレイヤーにおなじみの書き方
59
+
60
+ # その他、細かなメソッドはソースやspec等参照
61
+
62
+ == Create Parameters
63
+
64
+ 前提として、生成パラメータは努力目標
65
+ できるだけ指定を満たそうとはするが、ランダムなので保証はできない
66
+
67
+ まず全体を一つのBlockとして定義し、それを再帰的に分割した後、
68
+ 各Blockに部屋か交差点を作るため、Blockとは1:1の関係になる
69
+
70
+ * :min_block_size => 分割Blockの最低サイズ
71
+ * :max_block_count => Blockの最大生成数
72
+ * :min_room_size => 部屋の最低サイズ(デフォルトは4で、4以下は強制的に4)
73
+ * :max_room_size => 部屋の最大サイズ
74
+ * :min_room_count => 部屋の最低生成数(デフォルトは2)
75
+ * :max_room_count => 部屋の最大生成数
76
+ * :max_depth => 分割再帰の深さ max_depth=nのとき、Blockの最大数は2^nになる
77
+ * :cross_road_ratio => 交差点生成率(0 <= x <= 9)
78
+
79
+ == FAQ
80
+
81
+ === パラメータが適用されない
82
+
83
+ パラメータには適用優先順位があります
84
+
85
+ 1. min_block_size
86
+ 2. max_block_count
87
+ 3. max_depth
88
+ 4. min_room_count
89
+ 5. max_room_count
90
+ 6. cross_road_ratio
91
+ 7. max_room_size
92
+ 8. min_room_size
93
+
94
+ 上位のパラメータに対し、下位のパラメータが矛盾した場合、
95
+ 無視はしませんが、保証はされません
96
+
97
+ === max_room_count=1なのに部屋が2個できる
98
+
99
+ min_room_countのデフォルト値が2なので、
100
+ 上記の優先順位に従い、部屋が2個できます
101
+ (デフォルトが2個なのは、スタート地点とゴール地点を作るためです)
102
+ 明示的にmin_room_count=1をあわせて指定すると、一つだけできるはずです
103
+
104
+ === 道に(部屋でない)行き止まりができる
105
+
106
+ 仕様です
107
+
108
+ それで片付けるのもあれなので補足すると、
109
+ つなげられそうな交差点を、できるだけつなぐようにしているものの、
110
+ 周囲に残りBlockがない等、どうしようもない場合に行き止まりができます
111
+
112
+ あまりたくさんできると問題ですが、
113
+ たまにあるくらいはゲームとしていいんじゃないかと
114
+
115
+ === 最大分割深度(max_depth)って何?
116
+
117
+ DivideDungeonMakerは最初のBlockを起点にして、再帰的な分割をしようとします
118
+
119
+ この分割回数の最大値がmax_depthの指定で、これを小さくすることで、
120
+ 小さいBlockだらけになるのを防げます
121
+ max_depth=nの時、作られるBlockの最大値は2^nです
122
+
123
+ === イベントやBOSS用に、広い部屋が1つだけ欲しい
124
+
125
+ min_room_sizeをx/y以上にしたうえで、
126
+ min_block_sizeをx/y以上にするか、max_block_count=1を指定してください
127
+ 全体が1Blockになり、限界までRoomを大きくしようとします
128
+
129
+ === スタートとゴール(階段)って無いの?
130
+
131
+ rdgc-dmはあくまで部屋と道(の座標系)を作るための仕組みです
132
+ スタートやゴールの概念は各ゲームによって異なるため、
133
+ rdgc-dmには含んでいません(RDGCとしては存在します)
134
+
135
+ Area#ramdom_pointで各Areaのランダムな座標が取れますので、
136
+ それを使ってスタートやゴールやモンスターを配置してください
137
+
138
+ === 自分でロジックを書きたい
139
+
140
+ * RDGC::Maker::DungeonMakerをincludeしたクラス
141
+ * RDGC::Maker::TempBlockを継承したクラス
142
+
143
+ これらを組み合わせると自分のロジックが書けます
144
+ 詳しくはDivideDungeonMaker/DivideTempBlockのソースを見てください
145
+
146
+ == Copyright
147
+
148
+ Copyright (c) 2010 parrot_studio. See LICENSE for details
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rdgc-dm"
8
+ gem.summary = %Q{Random Dungeon Maker from RDGC}
9
+ gem.description = <<-TXT
10
+ This gem is part of RDGC - Ruby(Random) Dungeon Game Core.
11
+ RDGC is core of random dungeon game (like rogue), make dungeon, manage mnsters etc.
12
+ TXT
13
+ gem.email = "parrot@users.sourceforge.jp"
14
+ gem.homepage = "http://github.com/parrot-studio/rdgc-dm"
15
+ gem.authors = ["parrot_studio"]
16
+ gem.required_ruby_version = ">= 1.8.7"
17
+ gem.add_development_dependency "rspec", ">= 1.2.9"
18
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+
25
+ require 'spec/rake/spectask'
26
+ Spec::Rake::SpecTask.new(:spec) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.spec_files = FileList['spec/**/*_spec.rb']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ task :spec => :check_dependencies
38
+
39
+ task :default => :spec
40
+
41
+ require 'rake/rdoctask'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "rdgc-dm #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,371 @@
1
+ # coding: UTF-8
2
+ module RDGC
3
+ module Maker
4
+ class DivideDungeonMaker
5
+ include DungeonMaker
6
+
7
+ def self.create(width, height, params = nil)
8
+ dm = self.new
9
+ list = dm.make(width, height, params)
10
+ Map::Board.create_from_blocks(list)
11
+ end
12
+
13
+ DEFAULT_CROSS_ROAD_RATIO = 2
14
+
15
+ # override
16
+ def create_whole_block(width, height)
17
+ tb = DivideTempBlock.create_whole_block(width, height)
18
+ tb.min_size = min_block_size
19
+ tb.dividable
20
+ tb
21
+ end
22
+
23
+ def make_blocks(tb)
24
+ # 分割キューに最初のBlockを入れる
25
+ divide_queue << tb
26
+
27
+ # 再帰分割
28
+ divide
29
+
30
+ # 完了キューの中身を返す
31
+ done_queue
32
+ end
33
+
34
+ # override
35
+ def create_room
36
+ # 部屋と交差点を分ける
37
+ room_blocks = []
38
+ cross_blocks = []
39
+ force_room_blocks = []
40
+
41
+ param = {
42
+ :room => (10 - cross_road_ratio),
43
+ :cross => cross_road_ratio
44
+ }
45
+
46
+ blocks.each do |b|
47
+ # 初回分割のBlockは行き止まりになるので避ける
48
+ if b.depth < 2
49
+ force_room_blocks << b
50
+ next
51
+ end
52
+
53
+ r = select_rand(param)
54
+ case r
55
+ when :room
56
+ room_blocks << b
57
+ when :cross
58
+ cross_blocks << b
59
+ end
60
+ end
61
+
62
+ room_count = room_blocks.size + force_room_blocks.size
63
+ if max_room_count > 0 && room_count > max_room_count
64
+ # 超えた分だけ移動する
65
+ (room_count - max_room_count).times do
66
+ break if room_blocks.size + force_room_blocks.size <= 1
67
+ break if room_blocks.empty?
68
+ b = room_blocks.pickup!
69
+ cross_blocks << b if b
70
+ end
71
+ end
72
+
73
+ room_count = room_blocks.size + force_room_blocks.size
74
+ if room_count < min_room_count
75
+ # 足りない分を移動する
76
+ (min_room_count - room_count).times do
77
+ break if cross_blocks.empty?
78
+ b = cross_blocks.pickup!
79
+ room_blocks << b if b
80
+ end
81
+ end
82
+
83
+ # それぞれのblockを処理
84
+ [room_blocks, force_room_blocks].flatten.each do |b|
85
+ b.create_room(:min => min_room_size, :max => max_room_size)
86
+ end
87
+ cross_blocks.each{|b| b.create_cross_point}
88
+ end
89
+
90
+ # override
91
+ def create_road
92
+ return if blocks.size <= 1
93
+
94
+ # 再帰的に道を作成
95
+ recursive_road_create(blocks.choice)
96
+
97
+ # 道がない部屋で、既存と接しているところを処理
98
+ connect_cling_block_has_road
99
+
100
+ # 道がなく、孤立した部屋を移動
101
+ move_room_and_connect
102
+
103
+ # 行き止まりの交差点を処理
104
+ add_road_for_dead_end
105
+ end
106
+
107
+ # -------------------------------------------------------------
108
+
109
+ def min_block_size
110
+ unless @min_block_size
111
+ val = params[:min_block_size]
112
+ if val
113
+ # 指定がある場合はそれを評価
114
+ val = val.to_i
115
+ else
116
+ # 指定が無く、min_room_sizeが存在するならそちらに合わせる
117
+ val = (min_room_size ? min_room_size + 3 : 0)
118
+ end
119
+ val = Util::Config.min_block_size if val < Util::Config.min_block_size
120
+ @min_block_size = val
121
+ end
122
+ @min_block_size
123
+ end
124
+
125
+ def min_room_size
126
+ params[:min_room_size]
127
+ end
128
+
129
+ def max_room_size
130
+ params[:max_room_size]
131
+ end
132
+
133
+ def max_block_count
134
+ params[:max_block_count].to_i
135
+ end
136
+
137
+ def min_room_count
138
+ unless @min_room_count
139
+ val = params[:min_room_count].to_i
140
+ # 明示的に「1」という指定がない限り2部屋は作る
141
+ val = 2 if val <= 0
142
+ @min_room_count = val
143
+ end
144
+ @min_room_count
145
+ end
146
+
147
+ def max_room_count
148
+ params[:max_room_count].to_i
149
+ end
150
+
151
+ def max_depth
152
+ params[:max_depth].to_i
153
+ end
154
+
155
+ def cross_road_ratio
156
+ unless @cross_road_ratio
157
+ val = params[:cross_road_ratio]
158
+ if val
159
+ val = val.to_i
160
+ # 交差点生成率は 1<=x<=9 / 10
161
+ val = DEFAULT_CROSS_ROAD_RATIO if (val < 0 || val > 9)
162
+ else
163
+ # 指定なし => デフォルト
164
+ val = DEFAULT_CROSS_ROAD_RATIO
165
+ end
166
+ @cross_road_ratio = val
167
+ end
168
+ @cross_road_ratio
169
+ end
170
+
171
+ def divide_queue
172
+ @divide_queue ||= []
173
+ @divide_queue
174
+ end
175
+
176
+ def done_queue
177
+ @done_queue ||= []
178
+ @done_queue
179
+ end
180
+
181
+ def queue_size
182
+ divide_queue.size + done_queue.size
183
+ end
184
+
185
+ def finish?
186
+ return true if divide_queue.empty?
187
+ return true if (max_block_count > 0 && queue_size >= max_block_count)
188
+ false
189
+ end
190
+
191
+ def dividable_block?(b)
192
+ # そもそも分割対象ではない => false
193
+ return false unless b.dividable?
194
+
195
+ # 最大深度の指定がない => true
196
+ return true if max_depth <= 0
197
+
198
+ # 最大深度に達したら分割しない
199
+ b.depth >= max_depth ? false : true
200
+ end
201
+
202
+ def divide
203
+ # 再帰処理
204
+ loop do
205
+ break if finish?
206
+
207
+ tb = divide_queue.shift
208
+ break unless tb
209
+
210
+ list = tb.divide
211
+ unless list
212
+ # 分割できなかったので、元をdone_queueへ
213
+ done_queue << tb
214
+ break
215
+ end
216
+
217
+ list.each do |b|
218
+ if dividable_block?(b)
219
+ divide_queue << b
220
+ else
221
+ done_queue << b
222
+ end
223
+ end
224
+ end
225
+
226
+ # queueをまとめる
227
+ divide_queue.each{|b| done_queue << b}
228
+ end
229
+
230
+ # -------------------------------------------------------------
231
+
232
+ def recursive_road_create(target)
233
+ # 全部道がつながったら終了
234
+ return if blocks.all?{|b| b.has_road?}
235
+
236
+ # まだ道の処理をしてない、接しているblockを探す
237
+ yet_block = blocks.reject{|b| b.road_created?}
238
+ cling_list = create_cling_list(target, yet_block)
239
+
240
+ # 行き止まり => 終了
241
+ return if cling_list.size <= 0
242
+
243
+ # 接しているblockに道を作る
244
+ next_block = connect_cling_blocks(target, cling_list)
245
+
246
+ # 作成完了
247
+ target.road_created
248
+
249
+ # 次を再帰的呼び出し
250
+ recursive_road_create(next_block)
251
+ end
252
+
253
+ def connect_cling_block_has_road
254
+ # 道がない部屋を探す
255
+ remains = blocks.select{|b| b.has_room? && ! b.has_road?}
256
+ return if remains.empty?
257
+
258
+ # すでに完了しているblockの数を確認
259
+ done_count = blocks.select{|b| b.has_room? && b.has_road?}.size
260
+
261
+ remains.each do |target|
262
+ # min_room_countを満たすならランダム
263
+ if done_count >= min_room_count
264
+ next unless bool_rand
265
+ end
266
+
267
+ # 接しているblockに道があるならつなぐ
268
+ c_list = create_cling_list(target, blocks.select{|b| b.has_road?}).flatten
269
+ return if c_list.empty?
270
+ target.create_road_to(c_list.pickup!)
271
+ c_list.each{|b| target.add_remain_cling_blocks(b)}
272
+
273
+ done_count += 1
274
+ end
275
+ end
276
+
277
+ def move_room_and_connect
278
+ # 完了してしたblockの数を確認し、min_room_countを満たしていたら終わり
279
+ done_count = blocks.select{|b| b.has_room? && b.has_road?}.size
280
+ return if done_count >= min_room_count
281
+
282
+ # まだ道がない部屋を探す
283
+ remains = blocks.select{|b| b.has_room? && ! b.has_road?}
284
+ return if remains.empty?
285
+
286
+ remains.each do |target|
287
+ # 元の部屋等の削除
288
+ target.remove_all
289
+
290
+ # 改めて部屋を作る先を決める
291
+ enable_blocks = blocks.select{|b| b.has_remain_cling_blocks?}
292
+ next if enable_blocks.empty?
293
+
294
+ org_block = enable_blocks.choice
295
+ room_block = org_block.remain_cling_blocks.pickup!
296
+
297
+ # 部屋作成
298
+ room_block.create_room(:min => min_room_size, :max => max_room_size)
299
+
300
+ # 接続
301
+ room_block.create_road_to(org_block)
302
+ end
303
+ end
304
+
305
+ def create_cling_list(block, list)
306
+ top_list = collect_cling_block(block, list,:top)
307
+ bottom_list = collect_cling_block(block, list, :bottom)
308
+ left_list = collect_cling_block(block, list, :left)
309
+ right_list = collect_cling_block(block, list, :right)
310
+
311
+ [top_list, bottom_list, left_list, right_list].select{|a| a.size > 0}
312
+ end
313
+
314
+ def collect_cling_block(block, list, direction)
315
+ case direction
316
+ when :top
317
+ list.select{|b| block.cling_to_top?(b)}
318
+ when :bottom
319
+ list.select{|b| block.cling_to_bottom?(b)}
320
+ when :left
321
+ list.select{|b| block.cling_to_left?(b)}
322
+ when :right
323
+ list.select{|b| block.cling_to_right?(b)}
324
+ end
325
+ end
326
+
327
+ def connect_cling_blocks(target, cling_list)
328
+ return unless cling_list
329
+ return if cling_list.size <= 0
330
+
331
+ # 4方向で選択可能なblock配列から一つ選ぶ
332
+ direction_list = cling_list.pickup!
333
+ next_block = direction_list.pickup!
334
+
335
+ # ブロックに道をつなぐ
336
+ target.create_road_to(next_block)
337
+
338
+ # その方向の残りを接しているblockとして記録
339
+ direction_list.each{|b| target.add_remain_cling_blocks(b)}
340
+
341
+ # 残りの方向もランダムにつなぐ
342
+ cling_list.each do |d_list|
343
+ bl = d_list.pickup! if bool_rand
344
+ target.create_road_to(bl) if bl
345
+ d_list.each{|b| target.add_remain_cling_blocks(b)}
346
+ end
347
+
348
+ # 次の対象返却
349
+ next_block
350
+ end
351
+
352
+ def add_road_for_dead_end
353
+ deadends = blocks.select{|b| b.dead_end?}
354
+ return if deadends.empty?
355
+
356
+ deadends.each do |target|
357
+ # まだ作成していない方向に何か接しているか?
358
+ c_list = []
359
+ block_list = blocks.select{|b| b.has_road?}
360
+ target.remain_direction.each do |d|
361
+ ret = collect_cling_block(target, block_list, d)
362
+ c_list += ret.flatten
363
+ end
364
+ next if c_list.empty?
365
+ target.create_road_to(c_list.pickup!)
366
+ end
367
+ end
368
+
369
+ end
370
+ end
371
+ end