Changeset - a362783e3bec
[Not reviewed]
default
1 4 0
Laman - 8 years ago 2017-01-24 20:57:58

exporting SGF back to string, also useful Composed and Point property classes
5 files changed with 61 insertions and 7 deletions:
0 comments (0 inline, 0 general)
src/__init__.py
Show inline comments
 
deleted file
src/sgfParser/collection.py
Show inline comments
 
@@ -20,72 +20,72 @@ class Collection:
 
 
class GameTree:
 
	def __init__(self):
 
		self.nodes=[]
 
		self.branches=[]
 
 
	@staticmethod
 
	def create(s,start):
 
		res=GameTree()
 
		i=skipWhitespace(s,start)
 
		if i>=len(s) or s[i]!="(":
 
			# print("error when parsing GameTree")
 
			return (start,None)
 
		i,x=Node.create(s,i+1)
 
		if x is None:
 
			# print("error when parsing GameTree")
 
			return (i,None)
 
		y=None
 
		while x is not None:
 
			res.nodes.append(x)
 
			if y: y.addChild(x)
 
			x.setParent(y)
 
			y=x
 
			i=skipWhitespace(s,i)
 
			i,x= Node.create(s, i)
 
			i,x=Node.create(s, i)
 
		i=skipWhitespace(s,i)
 
		i,x=GameTree.create(s,i)
 
		while x is not None:
 
			res.branches.append(x)
 
			subroot=x.getNode(0)
 
			if subroot:
 
				subroot.setParent(y)
 
			if y: y.addChild(subroot)
 
			i=skipWhitespace(s,i)
 
			i,x=GameTree.create(s,i)
 
		if s[i]!=")":
 
			# print("error when parsing GameTree")
 
			return (i,None)
 
		return (i+1,res)
 
 
	## Expand multiple games into distinct GameTrees and yield each.
 
	def listGames(self):
 
		for node in self._listGINodes():
 
			yield self._buildSubtree(node)
 
 
	def getNode(self,i):
 
		if 0<=i<len(self.nodes):
 
			return self.nodes[i]
 
		return None
 
 
	## Create and return a new game tree containing the provided Node.
 
	#
 
	# Ancestor nodes are copied, descendants are taken directly.
 
	# Ancestor nodes are copied, descendants are shared.
 
	def _buildSubtree(self,seedNode):
 
		node=seedNode.copy()
 
 
		while node.parent:
 
			newNode=node.parent.copy()
 
			node.parent=newNode
 
			newNode.setChildren([node])
 
			node=newNode
 
 
		return node
 
 
	## Find and yield Game Info nodes.
 
	def _listGINodes(self):
 
		for node in self.nodes:
 
			if node.isGINode():
 
				yield node
 
		for tree in self.branches:
 
			for node in tree._listGINodes():
 
				yield node
src/sgfParser/node.py
Show inline comments
 
@@ -47,24 +47,50 @@ class Node:
 
	def removeChild(self,node):
 
		if node not in self.children:
 
			return None
 
		del self.children[self.children.index(node)]
 
		return node
 

	
 
	def removeChildAt(self,i):
 
		if -len(self.children)<i<len(self.children):
 
			res=self.children[i]
 
			del self.children[i]
 
			return res
 
		return None
 

	
 
	## Create a copy of the Node, with deep copied propeties and shallow copied parent and children.
 
	def copy(self):
 
		res=Node()
 
		res.properties={k: v.copy() for (k,v) in self.properties.items()}
 
		res.parent=self.parent
 
		res.setChildren(self.children[:])
 
		return res
 

	
 
	def getProperty(self,name):
 
		if name in self.properties: return self.properties[name]
 
		else: return None
 

	
 
	## Returns textual representation of the Node itself, but disregards its children.
 
	def __str__(self):
 
		return ";" + "".join(str(p) for p in self.properties.values())
 

	
 
	def export(self):
 
		# there is a beatiful recursive solution, which this stack is too narrow to contain
 
		stack=[(self,1,1)]
 
		output=[]
 

	
 
		while len(stack)>0:
 
			node,left,right=stack.pop()
 
			if left>0: output.append("("*left)
 
			output.append(str(node))
 

	
 
			childCount=len(node.children)
 
			if childCount==0: # a leaf
 
				output.append(")"*right)
 
			elif childCount==1: # a line
 
				stack.append((node.children[0],0,right))
 
			else: # a branching node
 
				# first child pops first, last child closes parent's parentheses
 
				children=zip(node.children,[1]*childCount,[1]*(childCount-1)+[1+right])
 
				stack.extend(reversed(children))
 

	
 
		return "".join(output)
src/sgfParser/property.py
Show inline comments
 
import re
 

	
 

	
 
GAME_INFO=1
 
UNKNOWN=99
 

	
 

	
 
class Composed:
 
	def __init__(self,a=None,b=None):
 
		self.a=a
 
		self.b=b
 

	
 
	def __str__(self):
 
		return "{0}:{1}".format(self.a,self.b)
 

	
 

	
 
class Point:
 
	def __init__(self,c,r):
 
		self.r=r
 
		self.c=c
 

	
 
	def __iter__(self):
 
		yield self.c
 
		yield self.r
 

	
 
	def __str__(self):
 
		a=ord("a")
 
		return chr(a+self.c)+chr(a+self.r)
 

	
 

	
 
def choose(*vTypes):
 
	def f(s,start):
 
		for vType in vTypes:
 
			i,x=vType(s,start)
 
			if x is not None: return (i,x)
 
		return (start,None)
 
	return f
 

	
 

	
 
def singleton(vType):
 
	def f(s,start):
 
		if s[start]!="[":
 
			return (start,None)
 
		i,x=vType(s,start+1)
 
		if x is None: return (start,None)
 
		if s[i]!="]":
 
			return (start,None)
 
		return (i+1,x)
 
	return f
 

	
 

	
 
def listOf(vType,allowEmpty=False):
 
	def f(s,start):
 
		res=[]
 
		single=singleton(vType)
 
		i,x=single(s,start)
 
		while x!=None:
 
			res.append(x)
 
			i,x=single(s,i)
 
		if len(res)==0 and not allowEmpty: return (start,None)
 
		return (i,res)
 
	return f
 

	
 

	
 
def compose(vTypeA,vTypeB):
 
	def f(s,start):
 
		i,a=vTypeA(s,start)
 
		if a==None or s[i]!=":": return (start,None)
 
		i,b=vTypeB(s,i+1)
 
		if b==None: return start,None
 
		return (i,(a,b))
 
		return (i,Composed(a,b))
 
	return f
 

	
 

	
 
def number(s,start):
 
	r=re.compile(r"(\+|-|)\d+")
 
	m=r.match(s,start)
 
	if m is None: return (start,None)
 
	res=int(m.group(0))
 
	return (m.end(),res)
 

	
 

	
 
def real(s,start):
 
	r=re.compile(r"(\+|-|)\d+(\.\d+)?")
 
	m=r.match(s,start)
 
	if m is None: return (start,None)
 
	res=float(m.group(0))
 
	return (m.end(),res)
 

	
 

	
 
def double(s,start):
 
	r=re.compile(r"1|2")
 
	m=r.match(s,start)
 
	if m is None: return (start,None)
 
	res=int(m.group(0))
 
@@ -105,103 +128,106 @@ def text(simple=True,composed=False):
 
	return f
 

	
 

	
 
def empty(s,start): return (start,"")
 

	
 

	
 
def anything(s,start):
 
	esc=False
 
	for i,c in enumerate(s[start:],start):
 
		if esc: esc=False
 
		elif c=="\\": esc=True
 
		elif c=="]": break
 
	return (i,s[start:i])
 

	
 

	
 
# go specific
 
def point(s,start):
 
	r=re.compile(r"[a-zA-Z]{2}|") # !! limit to board size
 
	m=r.match(s,start)
 
	if m is None: return (start,None)
 
	if m.group(0)=="": # pass, !! tt
 
		return (m.end(),tuple())
 
	col=m.group(0)[0]
 
	row=m.group(0)[1]
 
	col=ord(col) - (ord("a") if "a"<=col<="z" else ord("A")-26)
 
	row=ord(row) - (ord("a") if "a"<=row<="z" else ord("A")-26)
 
	return (m.end(),(col,row))
 
	col=ord(col)-(ord("a") if "a"<=col<="z" else ord("A")-26)
 
	row=ord(row)-(ord("a") if "a"<=row<="z" else ord("A")-26)
 
	return (m.end(),Point(col,row))
 

	
 
move=point
 
stone=point
 

	
 

	
 
class Property:
 
	def __init__(self):
 
		self.name=""
 
		self.value=""
 

	
 
	@staticmethod
 
	def create(s,start):
 
		res=Property()
 
		i,x=Property.ident(s,start)
 
		if x is None:
 
			return (start,None)
 
		res.name=x
 
		i,x=Property.createValue(s,i,res.name)
 
		if x is None:
 
			print('error when parsing property "{0}" at position {1}'.format(res.name,i))
 
			return (start,None)
 
		res.value=x
 
		return (i,res)
 

	
 
	@staticmethod
 
	def ident(s,start):
 
		r=re.compile(r"[A-Z]+")
 
		m=r.match(s,start)
 
		if m is None: return (start,None)
 
		return (m.end(),m.group())
 

	
 
	@staticmethod
 
	def createValue(s,start,name):
 
		if name in Property.patterns:
 
			return Property.patterns[name](s,start)
 
		else:
 
			print('warning, unknown property "{0}" at position {1}'.format(name,start))
 
			return singleton(anything)(s,start)
 

	
 
	@property
 
	def type(self):
 
		gameInfo={"AN","BR","BT","CP","DT","EV","GN","GC","ON","OT","PB","PC","PW","RE","RO","RU","SO","TM","US","WR","WT"}
 
		if self.name in gameInfo: return GAME_INFO
 
		else: return UNKNOWN
 

	
 
	def copy(self):
 
		res=Property()
 
		res.name=self.name
 
		# !! wouldn't work for listOf(listOf())
 
		res.value=self.value if not isinstance(self.value,list) else self.value[:]
 
		return res
 

	
 
	def __str__(self):
 
		val="[{0}]".format(self.value) if not isinstance(self.value,list) else "".join("[{0}]".format(x) for x in self.value)
 
		return "{0}{1}".format(self.name,val)
 

	
 
	patterns={
 
		"B":singleton(move),
 
		"KO":singleton(empty),
 
		"MN":singleton(number),
 
		"W":singleton(move),
 
		"AB":listOf(stone), #
 
		"AE":listOf(point), #
 
		"AW":listOf(stone), #
 
		"PL":singleton(color),
 
		"C":singleton(text(simple=False)),
 
		"DM":singleton(double),
 
		"GB":singleton(double),
 
		"GW":singleton(double),
 
		"HO":singleton(double),
 
		"N":singleton(text()),
 
		"UC":singleton(double),
 
		"V":singleton(real),
 
		"BM":singleton(double),
 
		"DO":singleton(empty),
 
		"IT":singleton(empty),
 
		"TE":singleton(double),
 
		"AR":listOf(compose(point,point)), #
 
		"CR":listOf(point), #
 
		"DD":listOf(point,allowEmpty=True), #
src/tests/testSgfParser.py
Show inline comments
 
@@ -8,38 +8,40 @@ from sgfParser.property import Property
 

	
 
dataDir=os.path.join(os.path.dirname(__file__), "data")
 

	
 

	
 
class TestProperty(TestCase):
 
	def testName(self):
 
		self.assertEqual(Property.create("[99]",0), (0,None))
 
		self.assertEqual(Property.create("99[99]",0), (0,None))
 

	
 
		i,prop=Property.create("MN[99]",0)
 
		self.assertNotEqual((i,prop), (0,None))
 
		self.assertEqual((i,prop.name), (6,"MN"))
 

	
 

	
 
class TestCollection(TestCase):
 
	def testSubtrees(self):
 
		c=Collection("""
 
(;B[aa]
 
	(;W[ab]PB[Some Black]PW[Some White];B[ac])
 
	(;W[bb]PB[Other Black]PW[Other White])
 
)""")
 
		games=list(c.listGames())
 

	
 
		self.assertEqual(len(games),2)
 
		self.assertRegex(games[0].export(), r"^\(;B\[aa];(PB\[Some Black]|PW\[Some White]|W\[ab]){3};B\[ac]\)$")
 
		self.assertRegex(games[1].export(), r"^\(;B\[aa];(PB\[Other Black]|PW\[Other White]|W\[bb]){3}\)$")
 

	
 
	def testEmptySgf(self):
 
		Collection("(;)")
 

	
 
	def testSimpleSgf(self):
 
		with open(os.path.join(dataDir, "simple.sgf")) as f:
 
			Collection(f.read())
 

	
 
	def testComplexSgf(self):
 
		with open(os.path.join(dataDir, "kogos.sgf")) as f:
 
			Collection(f.read())
 

	
 
if __name__ == '__main__':
 
	unittest.main()
0 comments (0 inline, 0 general)