Builderパターンによる調合システムの構築

今回は、Builderパターンを使って、最近はやりの「調合システム」を構築してみます。
 
kenmoの勝手な考察ですが、調合システムは、

  • 単体では弱いが、組み合わせをすることにより強力なアイテムになる
  • 「あとアレがあれば、強力なアイテムが作れるのにー」といった「待たされ感」
  • 未知のアイテムを組み合わせによって探し出すという「隠し」要素

など、ゲームデザイン的に優れた要素がいくつかあるため、
最近のゲームでは良く使われるシステムであると思います。
 
それはさておき、Builderパターンです。
 

Builderパターンとは

例えば、物を作る場合には、「素材」と「作成手順」が必要になります。
 
Builderパターンの特徴は、
その「素材」と「生成手順」を明確に分離して分かりやすくし、
そして拡張しやすくすることにあります。
 

クラス図

では例を見てみます。
以下のクラス図は、某アトリエシリーズの調合システムを実現するものです。

説明としては、
Materialクラスが、「素材」の基底クラスとして存在し、
派生クラスとして、

  • Carrot(にんじん)
  • Gram(ひよこ豆
  • WellWater(井戸水)
  • Bohnensuppe(豆のスープ)

が存在します。
これらは、調合するための「素材」になり、
調合した結果の「生産物」ともなります。
 
BlendingMakerというのは調合を管理するクラスです。
「作成手順」を実装するところです。
ここに素材を詰め込んで、実行すると、「生産物」のインスタンスを返してくれます。
 
Viorartというのは、プレイヤーです。
プレイヤーは、「素材」をどこかから拾ってきたり、
BlendingMakerに「素材」を詰め込んだり、実行したりします。
 

実装例

Pythonによる実装例は以下のようになります。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class Material:
	""" 素材基底クラス """
	def getId(self):
		return self.ID
	def getName(self):
		return self.NAME
class Carrot(Material):
	""" にんじん """
	ID   = 1
	NAME = "にんじん"
class Gram(Material):
	""" ひよこ豆 """
	ID   = 2
	NAME = "ひよこ豆"
class WellWater(Material):
	""" 井戸水 """
	ID   = 3
	NAME = "井戸水"
class Bohnensuppe(Material):
	""" 豆のスープ """
	ID   = 10
	NAME =  "豆のスープ"

class BlendingMaker:
	""" 調合Director """
	def init(self):
		self.materials = []
	def setMaterial(self, material):
		self.materials.append(material)
	def blend(self):
		""" 調合実行 """
		tmp = []
		for material in self.materials:
			print "%sを調合します。"%material.getName()
			tmp.append(material.getId())
		print "|\n|\nV"
		# 調合判定
		if(tmp.count(Carrot.ID) == 1
			and tmp.count(Gram.ID) == 2
			and tmp.count(WellWater.ID) == 1):
			return Bohnensuppe()
		return None

def main():
	# 素材生成
	carrot    = Carrot()
	gram1     = Gram()
	gram2     = Gram()
	wellwater = WellWater()
	# 調合クラス生成
	maker = BlendingMaker()
	maker.init()
	maker.setMaterial(carrot)
	maker.setMaterial(gram1)
	maker.setMaterial(gram2)
	maker.setMaterial(wellwater)
	# 調合実行
	ret = maker.blend()
	if ret is None:
		print "調合に失敗したよ..."
	else:
		print "%sの調合に成功したよ!"%ret.getName()
if __name__ == "__main__":
	main()

ポイントは、調合判定の部分ですね。

		# 調合判定
		if(tmp.count(Carrot.ID) == 1
			and tmp.count(Gram.ID) == 2
			and tmp.count(WellWater.ID) == 1):
			return Bohnensuppe()
		return None

豆のスープのレシピは、

なので、このような書き方をしています。
 
今、重要なことに気がついたのですが、、、
この判定は「ポーカー」とか「麻雀」とかに応用できそうですね。
 
…なるほど。そもそも調合も、麻雀の「役」をそろえていくのと同じシステムですねー。
 
 
それはさておき、実行結果はこんな感じです。

にんじんを調合します。
ひよこ豆を調合します。
ひよこ豆を調合します。
井戸水を調合します。
|
|
V
豆のスープの調合に成功したよ!

 
 
以上、Builderパターンでしたー。
 

補足1

例では「素材」の生成は、「プレイヤー」が行っていますが、
本当のBuilderパターンは、Director(例におけるBlendingMaker)が
「素材」のインスタンス生成も管理しています。

補足2

例では調合判定をべた書きしていますが、
実際は判定処理が膨大な量になるので、外部スクリプトなどで用意した方が、
メンテナンス性が向上すると思います。