Changeset - ef10e626e192
[Not reviewed]
default
1 3 1
Laman - 8 years ago 2017-02-14 21:43:45

exporting game info, added GameRecord wrapper
5 files changed with 69 insertions and 40 deletions:
0 comments (0 inline, 0 general)
src/diana.py
Show inline comments
 
import os
 
import re
 

	
 
from jinja2 import Environment,FileSystemLoader
 

	
 
import config as cfg
 
import go
 
from go import BLACK,WHITE,EMPTY
 
from sgfParser import ParserError
 
from sgfParser.collection import Collection
 
from drawer.svg import Svg
 
from drawer.tikz import Tikz
 

	
 

	
 
curDir=os.path.dirname(__file__)
 
templateDir=os.path.join(curDir,"templ")
 
env=Environment(loader=FileSystemLoader(templateDir))
 

	
 

	
 
def collectMoves(root):
 
	node=root
 
	while len(node.children)>0:
 
		b=node.getProp("B")
 
		w=node.getProp("W")
 
		if b is not None: yield ("b",b)
 
		elif w is not None: yield ("w",w)
 
		# else: yield None # !! not really robust
 

	
 
		node=node.children[0]
 

	
 

	
 
class SourceFile:
 
	def __init__(self,fileName):
 
		self.fileName=fileName
 
		self._shortName= "".join(re.split(r'[/\\]', fileName)[-1].split('.')[:-1])
 
		self._game=go.Go()
 
		self._moveNumber=0
 
		self._record=None
 
		self._moves=[]
 

	
 
	def process(self):
 
		print("{0}... ".format(self.fileName), end="")
 

	
 
		try:
 
			games=Collection(open(self.fileName, 'r', encoding=cfg.encoding).read()).listGames()
 
		except ParserError as e:
 
			print("Couldn't parse {0}, following error occured: {1}".format(self.fileName,e))
 
			return False
 
		record=list(games)[0]
 

	
 
		self._moves=list(collectMoves(record))
 
		self._record=list(games)[0]
 
		self._moves=list(collectMoves(self._record.root))
 

	
 
		diagramsNeeded=(len(self._moves)-cfg.minMovesPerDiagram)//cfg.movesPerDiagram+1
 

	
 
		for i in range(diagramsNeeded):
 
			self.createDiagram(i+1)
 

	
 
		notes=open(os.path.join(cfg.outputDir,"{0}.txt".format(self._shortName)), 'w')
 
		# notes.write(overlays)
 
		notes.write(self.createGameInfo())
 
		notes.close()
 
		print("done")
 

	
 
	def createDiagram(self,diagramNumber):
 
		# initialize the diagram
 
		template=Svg()
 
		overlays=[]
 
		letters=dict()
 
		letter="a"
 

	
 
		for lineNumber,line in enumerate(self._game.board):
 
			for itemNumber,item in enumerate(line):
 
				if item==BLACK: template.addStone(itemNumber,lineNumber,"b")
 
				if item==WHITE: template.addStone(itemNumber,lineNumber,"w")
 
			localBoard={(a,b):self._game.board[b][a]-1 for a in range(19) for b in range(19) if self._game.board[b][a]!=EMPTY}
 

	
 
		for j in range(cfg.movesPerDiagram):
 
			# draw the moves
 
			if self._moveNumber>=len(self._moves): break
 

	
 
			c,move=self._moves[self._moveNumber]
 
			c=c.lower()
 
			if move==tuple():
 
				overlays.append("{0} pass".format(self._moveNumber))
 
				self._moveNumber+=1
 
				continue
 
			else:
 
				(x,y)=move
 

	
 
			if not self._game.move(BLACK if c=='b' else WHITE, x,y):
 
				# !! we do not honor http://red-bean.com/sgf/ff5/m_vs_ax.htm at the moment
 
				msg="illegal move: {0} at {1},{2}".format(self._moveNumber+1,x,y)
 
				if cfg.keepBroken:
 
					print(msg)
 
					self._moveNumber+=1
 
					continue
 
				else:
 
					msg+=". aborted"
 
					print(msg)
 
					return False
 

	
 
			# draw the move on an empty intersection
 
			if not (x,y) in localBoard:
 
				localBoard[(x,y)]=self._moveNumber+1
 
				template.addMove(x,y,c,self._moveNumber+1)
 
			# intersection occupied by an unlabeled stone
 
			elif localBoard[(x,y)]<1:
 
				# intersection not labeled even by a letter
 
				if not (x,y) in letters:
 
					letters[(x,y)]=letter
 
					color='b' if localBoard[(x,y)]==EMPTY else 'w'
 
					template.addMove(x,y,color,letter)
 
					letter=chr(ord(letter)+1)
 
				overlays.append("{0} = {1}".format(self._moveNumber+1,letters[(x,y)]))
 
			# intersection occupied by a numbered stone
 
			else: overlays.append("{0} = {1}".format(self._moveNumber+1,localBoard[(x,y)]))
 

	
 
			self._moveNumber+=1
 
		
 
		# finish and save the diagram
 
		file=open(os.path.join(cfg.outputDir,"{0}-{1}.{2}".format(self._shortName,diagramNumber,template.extension)),'w') # a new file
 
		file.write(template.render(env.get_template("templ.svg")))
 
		file.close()
 

	
 
		notes=open(os.path.join(cfg.outputDir,"{0}-{1}.txt".format(self._shortName,diagramNumber)), 'w')
 
		notes.write("\n".join(overlays))
 
		notes.close()
 

	
 
	def createGameInfo(self):
 
		rec=self._record
 
		return """{title}
 
B: {black} {bRank}
 
W: {white} {wRank}
 
{date}
 
{result}""".format(title=rec.get("GN",""), black=rec.get("PB",""), bRank=rec.get("BR",""), white=rec.get("PW",""), wRank=rec.get("WR",""), date=rec.get("DT",""), result=rec.get("RE",""))
 

	
 

	
 
print("processing:")
 
files=cfg.inputFiles[:]
 

	
 
for item in files:
 
	if os.path.isfile(item):
 
		f=SourceFile(item)
 
		f.process()
 
	elif os.path.isdir(item):
 
		files+=[os.path.join(item,child) for child in os.listdir(item)]
 
		print("contents of the '{0}' directory added to the queue".format(item))
 
	else: print("the '{0}' path could not be resolved to either a file nor a directory".format(item))
src/gameRecord.py
Show inline comments
 
new file 100755
 
from sgfParser.node import Node
 

	
 

	
 
## Wrapper around a Node tree.
 
class GameRecord:
 
	def __init__(self,root=None):
 
		self.root=root or Node()
 
		self._gameInfoNode=next(root.listGINodes())
 

	
 
	def export(self):
 
		return self.root.export()
 

	
 
	def set(self,name,value):
 
		self._gameInfoNode.setProp(name,value)
 

	
 
	def get(self,name,default=None):
 
		return self._gameInfoNode.getProp(name,default)
src/sgf.py
Show inline comments
 
deleted file
src/sgfParser/collection.py
Show inline comments
 
modified file chmod 100755 => 100644
 
from .node import Node
 
from . import skipWhitespace, ParserError
 
from gameRecord import GameRecord
 

	
 

	
 
class Collection:
 
	def __init__(self,s):
 
		self.gameTrees=[]
 
		i=skipWhitespace(s,0)
 
		if i>=len(s): return
 
		elif not GameTree.fits(s,i):
 
			raise ParserError("expected a GameTree starting with '('",s,i)
 
		while GameTree.fits(s,i):
 
			i,x=GameTree.create(s,i)
 
			self.gameTrees.append(x)
 
		if i<len(s):
 
			raise ParserError("expected EOF",s,i)
 

	
 
	def listGames(self):
 
		for tree in self.gameTrees:
 
			for game in tree.listGames(): yield game
 

	
 

	
 
class GameTree:
 
	def __init__(self):
 
		self.nodes=[]
 
		self.branches=[]
 

	
 
	@staticmethod
 
	def fits(s,i):
 
		return i<len(s) and s[i]=="("
 

	
 
	@staticmethod
 
	def create(s,start):
 
		assert GameTree.fits(s,start)
 
		res=GameTree()
 

	
 
		i=skipWhitespace(s,start+1)
 
		if not Node.fits(s,i):
 
			raise ParserError("expected a Node starting with ';'",s,i)
 

	
 
		y=None
 
		while Node.fits(s,i):
 
			i,x=Node.create(s,i)
 
			res.nodes.append(x)
 
			if y: y.addChild(x)
 
			x.setParent(y)
 
			x.parent=y
 
			y=x
 
			i=skipWhitespace(s,i)
 

	
 
		while GameTree.fits(s,i):
 
			i,x=GameTree.create(s,i)
 
			res.branches.append(x)
 
			subroot=x.getNode(0)
 
			subroot.setParent(y)
 
			subroot.parent=y
 
			if y: y.addChild(subroot)
 
			i=skipWhitespace(s,i)
 
		if i>=len(s) or s[i]!=")":
 
			raise ParserError("expected end of a GameTree marked by ')'",s,i)
 
		i=skipWhitespace(s,i+1)
 
		return (i,res)
 

	
 
	## Expand multiple games into distinct GameTrees and yield each.
 
	def listGames(self):
 
		for node in self._listGINodes():
 
			yield self._buildSubtree(node)
 
		if len(self.nodes)==0: return None
 
		for node in self.nodes[0].listGINodes():
 
			yield GameRecord(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 shared.
 
	# Ancestor nodes are copied, descendants are moved from the seedNode.
 
	def _buildSubtree(self,seedNode):
 
		node=seedNode.copy()
 
		node.setChildren(seedNode.children)
 
		seedNode.children=[]
 

	
 
		while node.parent:
 
			newNode=node.parent.copy()
 
			node.parent=newNode
 
			newNode.setChildren([node])
 
			newNode.addChild(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
 
from collections import deque
 

	
 
from . import skipWhitespace, ParserWarning
 
from .property import Property, GAME_INFO
 

	
 

	
 
class Node:
 
	def __init__(self):
 
		self.properties=dict()
 
		self.parent=None
 
		self.children=[]
 

	
 
	@staticmethod
 
	def fits(s,i):
 
		return i<len(s) and s[i]==";"
 

	
 
	@staticmethod
 
	def create(s,start):
 
		assert Node.fits(s,start)
 
		res=Node()
 

	
 
		i=skipWhitespace(s,start+1)
 
		while Property.fits(s,i):
 
			i,x=Property.create(s,i)
 
			if x.name in res.properties:
 
				# !! raise or log or ignore
 
				raise ParserWarning('duplicate "{0}" property in a node. second value ignored'.format(x.name),s,i)
 
			else:
 
				res.properties[x.name]=x
 
			i=skipWhitespace(s,i)
 
		return (i,res)
 

	
 
	def listGINodes(self):
 
		if self.isGINode(): yield self
 
		empty=not self.isGINode()
 

	
 
		node=self
 
		while node.parent:
 
			node=node.parent
 
			if node.isGINode():
 
				empty=False
 
				yield node
 

	
 
		queue=deque(self.children)
 
		while len(queue)>0:
 
			node=queue.popleft()
 
			if node.isGINode():
 
				empty=False
 
				yield node
 
			queue.extend(node.children)
 
		if empty: yield self # always yield at least self, can work as GINode as well as any other
 

	
 
	def isGINode(self):
 
		return any(prop.type==GAME_INFO for prop in self.properties.values())
 

	
 
	def setProperty(self,name,value):
 
	def setProp(self,name,value):
 
		self.properties[name]=value
 
		# zkontrolovat typ value
 

	
 
	def setParent(self,node):
 
		self.parent=node
 

	
 
	def setChildren(self,children):
 
		self.children=children
 
		for child in children: child.parent=self
 

	
 
	def addChild(self,node):
 
		if node in self.children: return node
 
		node.parent=self
 
		self.children.append(node)
 
		return node
 

	
 
	def removeChild(self,node):
 
		if node not in self.children:
 
			return None
 
		del self.children[self.children.index(node)]
 
		node.parent=None
 
		return node
 

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

	
 
	## Create a copy of the Node, with deep copied propeties and shallow copied parent and children.
 
	## Create a copy of the Node, with the same parent and deep copied properties, no copied 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 getProp(self,name):
 
	def getProp(self,name,default=None):
 
		if name in self.properties: return self.properties[name].value
 
		else: return None
 
		else: return default
 

	
 
	## 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)
0 comments (0 inline, 0 general)