rdgc-dm 0.1.0

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.
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