マップ読み込みクラス(その2)

RPGにおけるマップ格納アルゴリズム
http://blog.livedoor.jp/abars/archives/50454661.html

そもそもmapのvalueなんてほとんど0で疎なんだから、
ベタに持つのはばからしい。

なるほど。そういえば、そうですねー。
 
スクロールタイプのアクションやシューティングなどでのフィールドマップは、
すんごい長いマップになるので、これはとてもいいですねー。
 
あと、Pythonのタプルは、書き込み不可なので、
壊れるブロックとかどうしようかな、、
listにしようかな、、、
だけどインデックスのアクセスが遅いし、、、
などと考えてたところだったので、ちょうどいい情報でしたー。
 
ただ、フィールドマップは基本的にReadOnlyな領域なので、
書き込みは注意してやらないと、設計が破綻してしまいそう。
注意せねば>自分。
 
ということで、Pythonの実装例。

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

import struct

class Layer2D:
	""" 2次元レイヤー """
	def __init__(self, width, height, data):
		"""
		コンストラクタ
		@param width  幅
		@param height 高さ
		@param data   データ
		"""
		self.width  = width
		self.height = height
		self.data   = data
	def _getIndex(self, args):
		if "idx" in args:
			return args["idx"]
		else:
			x = args["x"]
			y = args["y"]
			if 0 <= x < self.width:
				if 0 <= y < self.height:
					return self.toIndex(x, y)
			return None
	def get(self, **args):
		"""
		座標を指定して値を取得
		@param x, y, idx 座標 or インデックス
		@return 値
		"""
		idx = self._getIndex(args)
		if idx is None:
			return None
		return self.data[idx]
	def coordinate(self, idx):
		""" 直交座標に変換 """
		return (idx%self.width, idx/self.width)
	def toIndex(self, x, y):
		""" インデックスに変換 """
		return y*self.width + x
	def size(self):
		""" 縦×横 """
		return self.width*self.height
	def __str__(self):
		strData = "  "
		for i, data in enumerate(self.data):
			strData += "%03d,"%data
			if (i+1) % self.width == 0:
				strData += "\n  "
		return "(Width,Height)=(%d,%d)\n%s"%(
			self.width, self.height, strData)

class Layer2DEx(Layer2D):
	""" マップによるLayer2D """
	def __init__(self, width, height, data):
		newdata = {}
		for i, d in enumerate(data):
			if d != 0: # 0を除去
				newdata[i] = d
		Layer2D.__init__(self, width, height, newdata)
	def get(self, **args):
		"""
		座標を指定して値を取得
		@param x, y, idx 座標 or インデックス
		@return 値
		"""
		return self.data.get(self._getIndex(args), None)
	def set(self, value, **args):
		"""
		座標を指定して値を設定
		@param x, y, idx 座標 or インデックス
		@param value 値
		"""
		idx = self._getIndex(args)
		if idx is not None:
			self.data[idx] = value
	def find(self, value):
		"""
		valueを検索
		@param value 検索する値
		@return 最初に見つかったインデックス
		"""
		for k, v in self.data.items():
			if v == value:
				return k
		return None
	def pop(self, **args):
		"""
		指定のインデックスをpop
		@return 値(存在しない場合、None)
		"""
		return self.data.pop(self._getIndex(args), None)
	def items(self):
		"""
		キーと値のリストを返す
		@return (キー, 値)のリスト
		"""
		return self.data.items()
	def __str__(self):
		strData = "  "
		for i in range(self.width*self.height):
			strData += "%03d,"%self.data.get(i, 0)
			if (i+1) % self.width == 0:
				strData += "\n  "
		return "(width,height)=(%d,%d)\n%s"%(self.width, self.height, strData)

class FMFLoader:
	""" FMFファイル読込クラス """
	# データのbit数に対応するバイナリコード変換関数テーブル
	CONVERT_BIN_FUNC_TBL = {
		8:  lambda cnt, b: struct.unpack("%dB"%cnt, b),
		16: lambda cnt, b: struct.unpack("%dH"%cnt, b),
	}
	def __init__(self, filepath, layer2d=Layer2D):
		"""
		コンストラクタ
		@param filepath FMFファイルパス
		@param layer    レイヤーとして使用するクラス(Layer2Dの派生クラス)
		"""
		self.filepath = filepath
		self.file     = open(filepath, "rb")
		# ヘッダ
		self._readHeader()
		# データ( レイヤーリスト)
		self.layerList = map(self._createLayer, [layer2d]*self.layerCount)
	def _readHeader(self):
		""" ヘッダ読込[20byte] """
		b = self.file.read(4)
		assert b == "FMF_", "FileformatError: Expected 'FMF'file. (file:'%s')"%self.filepath
		self.identifier = b  # ファイル識別子       [4byte]
		b = self.file.read(16)
		(self.dataSize,      # ヘッダを除いたサイズ [4byte]
			self.width,      # マップの幅           [4byte]
			self.height,     # マップの高さ         [4byte]
			self.chipWidth,  # チップの幅           [1byte]
			self.chipHeight, # チップの高さ         [1byte]
			self.layerCount, # レイヤー数           [1byte]
			self.bitCount    # データのbit数(8/16)  [1byte]
		) = struct.unpack("3L4B", b)
	def _createLayer(self, layer2d):
		""" レイヤーの生成 """
		return apply(layer2d,
			(self.width, self.height,
				self._createLayerData(self.width*self.height)))
	def _createLayerData(self, size):
		"""
		レイヤーデータを生成
		@param size レイヤーデータのサイズ
		"""
		return self.CONVERT_BIN_FUNC_TBL[self.bitCount](
			size,
			self.file.read(size*self.bitCount/8))
	def getLayer(self, index):
		"""
		レイヤーオブジェクト取得
		@parma index レイヤー番号
		"""
		if 0 <= index < self.layerCount:
			return self.layerList[index]
		return None
	def toStringHeader(self):
		""" ヘッダを文字列に変換 """
		return "%s%s%s%s%s%s%s%s"%(
		"  Identifier         :%s\n"%self.identifier,
		"  Size(except Header):%i\n"%self.dataSize,
		"  Width              :%i\n"%self.width,
		"  Height             :%i\n"%self.height,
		"  ChipWidth          :%i\n"%self.chipWidth,
		"  ChipHeight         :%i\n"%self.chipHeight,
		"  LayerCount         :%i\n"%self.layerCount,
		"  BitCount           :%i\n"%self.bitCount,
		)
	def toStringData(self):
		""" データを文字列に変換 """
		result = "  "
		for idx, layer in enumerate(self.layerList):
			result += "[Layer:%d]\n  "%idx
			for i, data in enumerate(layer.data):
				result += "%03d,"%data
				if (i+1) % layer.width == 0:
					result += "\n  "
		return result
	def __str__(self):
		return "Header ... \n%s\nData ... \n%s"%(
			self.toStringHeader(),
			self.toStringData())
def main():
	fmf = FMFLoader("area.fmf", Layer2DEx)
	print fmf.getLayer(0)

if __name__ == "__main__":
	main()

インデックスと直交座標のどちらでもアクセスできるように、
ディクショナリ引数に変更しまちた。
 
インデックスアクセスの場合は、

layer.get(idx=14) # 14番目の要素にアクセス

などとやり、
直交座標アクセスの場合は、

layer.get(x=3, y=5) # X座標3、Y座標5にアクセス

などとやります。