Changeset - 9ab11204b0f9
[Not reviewed]
default
0 6 0
Laman - 6 years ago 2018-12-03 22:08:58

StateBag exportuje záznam partie, BoardView zobrazuje pořadí tahů
6 files changed with 50 insertions and 16 deletions:
0 comments (0 inline, 0 general)
src/core.py
Show inline comments
 
@@ -28,53 +28,57 @@ class Core:
 

	
 
		self._guiProc=multiprocessing.Process(name="gui", target=gui, args=(self._guiMessages,self._ownMessages))
 
		self._guiProc.start()
 
		self.relativeFrame(0)
 

	
 
	def setCorners(self,corners):
 
		self.detector.setGridCorners(corners)
 
		self.analyze()
 

	
 
	def setTresholds(self,tresB=None,tresW=None):
 
		if tresB is not None: self.detector.tresB=tresB
 
		if tresW is not None: self.detector.tresW=tresW
 
		self.analyze()
 

	
 
	def relativeFrame(self,step):
 
		self._imgIndex=(self._imgIndex+step)%len(self._imgs)
 
		imgPath=os.path.join(cfg.misc.imgDir,self._imgs[self._imgIndex])
 
		self._frame=PIL.Image.open(imgPath)
 
		self._guiMessages.send("setCurrentFrame",(self._frame.copy(),))
 
		self.analyze()
 

	
 
	def analyze(self):
 
		if self.detector.analyze(self._frame):
 
			if isLegalPosition(self.detector.board):
 
				self.states.pushState(self.detector.board)
 
				self._guiMessages.send("setGameState",(self.detector.board,))
 
				state=self.states.pushState(self.detector.board)
 
				rec=[]
 
				if state:
 
					rec=state.exportRecord()
 
					log.debug("progressive game record: %s",rec)
 
				self._guiMessages.send("setGameState", (self.detector.board,rec))
 

	
 
				self.go.transitionMove(self.detector.board)
 
				log.debug("game record: %s",self.go._record)
 
				log.debug("conservative game record: %s",self.go._record)
 
			else:
 
				log.info("illegal position detected")
 

	
 
	def listen(self):
 
		listenerThread=threading.Thread(target=lambda: self._ownMessages.listen())
 
		listenerThread.start()
 

	
 
	def joinGui(self):
 
		self._guiProc.join()
 
		self._ownMessages.send("!kill",("core",))
 

	
 
	def _handleEvent(self,e):
 
		actions={
 
			"setCorners": self.setCorners,
 
			"setTresholds": self.setTresholds,
 
			"prevFrame": lambda: self.relativeFrame(-1),
 
			"nextFrame": lambda: self.relativeFrame(1)
 
		}
 
		(actionName,args,kwargs)=e
 

	
 
		return actions[actionName](*args,**kwargs)
 

	
 
core=Core()
 
core.listen()
src/go/engine.py
Show inline comments
 
from .core import PASS
 
from .transpositiontable import TranspositionTable
 
from . import core
 

	
 

	
 
## Compute move sequence from state1 to state2.
 
#
 
# @param colorIn {BLACK,WHITE}: color to start the sequence
 
# @param colorOut {BLACK,WHITE}: color to close the sequence
 
# @return [(c,row,col), ...] or None. c in {BLACK,WHITE} == {1,-1}
 
def getTransitionSequence(state1,state2,colorIn,colorOut,diff):
 
	eng.load(state1)
 
	return eng.iterativelyDeepen(state2,diff,colorIn,colorOut)
 

	
 

	
 
class SpecGo(core.Go):
 
	def __init__(self,boardSize=19):
 
		super().__init__(boardSize)
 

	
 
	def listRelevantMoves(self,diff):
 
		"""There can be 3 different changes in the diff: additions, deletions and replacements.
 
		Additions can be taken as relevant right away.
 
		Deletions and replacements had to be captured, so we add their liberties.
 
		Also any non-missing stones of partially deleted (or replaced) groups had to be replayed, so add them too.
 
		Needs to handle snapback, throw-in.
 
		There's no end to what could be theoretically relevant, but such sequences are long and we will pretend they won't happen."""
 
		There's no end to what could be theoretically relevant, but such sequences are long and we will pretend they won't happen.
 

	
 
		:return: (blackMoves,whiteMoves) == ({(row,col), ...}, {(row,col), ...})"""
 
		res=({PASS},{PASS})
 
		for d in diff:
 
			(r,c,action,color)=d
 
			colorKey=(1-color)>>1 # {-1,1}->{1,0}
 
			if action!="-" and (r,c) not in res[colorKey]:
 
				res[colorKey].add((r,c))
 
				if action=="*":
 
					for (ri,ci) in self.listNeighbours(r,c): # in case a stone was played and captured. !! might want to add even more
 
						res[1-colorKey].add((ri,ci))
 
			# this is rather sloppy but correct. the time will show if it is effective enough
 
			# just floodFill from the current intersection, add everything you find and also all the neighbours to be sure
 
			if action!="+" and (r,c) not in res[colorKey] and (r,c) not in res[1-colorKey]:
 
				self._helper.clear()
 
				self._helper.floodFill(color if action=="-" else 1-color, r, c)
 
				res[colorKey].union(self._helper.getContinuousArea())
 
				for (ri,ci) in self._helper.getContinuousArea():
 
					res[colorKey].add((ri,ci))
 
					res[1-colorKey].add((ri,ci))
 
					for (rj,cj) in self.listNeighbours(ri,ci):
 
						res[colorKey].add((rj,cj))
 
						res[1-colorKey].add((rj,cj))
 
		return res
 

	
 
	def listNeighbours(self,r,c):
 
		if r>0: yield (r-1,c)
 
		if r+1<self.boardSize: yield (r+1,c)
 
		if c>0: yield (r,c-1)
 
		if c+1<self.boardSize: yield (r,c+1)
 

	
 

	
 
class Engine:
 
	"""Class searching for move sequences changing one board state into another."""
 
	def __init__(self,g=None):
 
		self._g=g or SpecGo()
 
		self._moveList=(set(),set())
 
		self._transpositions=TranspositionTable()
 

	
 
	def load(self,state1):
 
		self._g.load(state1)
 

	
 
	def iterativelyDeepen(self,state2,diff,colorIn,colorOut):
 
		"""Search for a move sequence from the loaded state to state2. Tries progressively longer sequences."""
 
		"""Search for a move sequence from the loaded state to state2. Tries progressively longer sequences.
 

	
 
		:return: [(c,row,col), ...] or None. c in {BLACK,WHITE} == {1,-1}"""
 
		self._moveList=self._g.listRelevantMoves(diff)
 
		startDepth=1 if colorIn==colorOut else 2
 
		self._g.toMove=colorIn
 

	
 
		for i in range(startDepth,5,2):
 
			seq=self._dfs(state2,i)
 
			if seq:
 
				seq.reverse()
 
				return seq
 
		return None
 

	
 
	def _dfs(self,state2,limit):
 
		"""Search for a "limit" move sequence from the loaded state to state2."""
 
		g=self._g
 
		moveSet=self._moveList[(1-g.toMove)>>1]
 
		transKey=(g.hash()*state2.hash()*limit)&0xffffffff
 

	
 
		transSeq=self._transpositions.get(transKey)
 
		if transSeq is not None:
 
			return transSeq[:] if transSeq else transSeq
 

	
 
		for m in moveSet.copy():
 
			if not g.doMove(g.toMove,*m): continue
 
			captured=g.captures[:g.captureCount]
src/gui/__init__.py
Show inline comments
 
@@ -27,28 +27,29 @@ class GUI:
 
		self.mainWindow.bind("<Destroy>",lambda e: self._ownMessages.send("!kill",("gui",)))
 

	
 
	def __call__(self,ownMessages,coreMessages):
 
		self._ownMessages=ownMessages
 
		self._coreMessages=coreMessages
 

	
 
		self.listenerThread=threading.Thread(target=lambda: ownMessages.listen(self._handleEvent))
 
		self.listenerThread.start()
 

	
 
		self.mainWindow.mainloop()
 

	
 
	def sendMsg(self,actionName,args=tuple(),kwargs=None):
 
		self._coreMessages.send(actionName,args,kwargs)
 

	
 
	def _handleEvent(self,e):
 
		actions={"setCurrentFrame":self._frameHandler, "setGameState":self._stateHandler}
 
		(actionName,args,kwargs)=e
 

	
 
		return actions[actionName](*args,**kwargs)
 

	
 
	def _frameHandler(self,newFrame):
 
		self.mainWindow.setCurrentFrame(newFrame)
 
		self.root.event_generate("<<redrawImgView>>")
 

	
 
	def _stateHandler(self,gameState):
 
		self.mainWindow.boardView.redrawState(gameState)
 
	def _stateHandler(self,gameState,moves):
 
		labels={(row,col):(i+1) for (i,(c,row,col)) in enumerate(moves)}
 
		self.mainWindow.boardView.redrawState(gameState,labels)
 

	
 
gui=GUI()
src/gui/boardview.py
Show inline comments
 
from .resizablecanvas import ResizableCanvas
 
from go.core import BLACK,WHITE
 

	
 

	
 
## Handles and presents the game state as detected by the program.
 
class BoardView(ResizableCanvas):
 
	def __init__(self, master=None):
 
		super().__init__(master)
 

	
 
		self._padding=24
 
		self.configure(width=360,height=360,background="#ffcc00")
 

	
 
		self._padding=24
 
		self._cellWidth=(self._width-2*self._padding)/18
 
		self._cellHeight=(self._height-2*self._padding)/18
 

	
 
		self._drawGrid()
 

	
 
	def redrawState(self,gameState):
 
		self.delete("black","white")
 
	def redrawState(self,gameState,labels=dict()):
 
		self.delete("black","white","label")
 
		for r,row in enumerate(gameState):
 
			for c,point in enumerate(row):
 
				self._drawStone(r, c, point)
 
				self._drawStone(r, c, point, labels.get((r,c)))
 

	
 
	def configure(self,*args,**kwargs):
 
		super().configure(*args,**kwargs)
 
		self._cellWidth=(self._width-2*self._padding)/18
 
		self._cellHeight=(self._height-2*self._padding)/18
 

	
 
	def _drawGrid(self):
 
		padding=self._padding
 
		for i in range(19):
 
			self.create_line(padding,self._cellHeight*i+padding,self._width-padding,self._cellHeight*i+padding,tags="row",fill="#000000") # rows
 
			self.create_line(self._cellWidth*i+padding,padding,self._cellWidth*i+padding,self._height-padding,tags="col",fill="#000000") # cols
 

	
 
		self._drawStars()
 
		self._drawCoordinates()
 

	
 
	def _drawStars(self):
 
		radius=2
 

	
 
		for r in range(3,19,6):
 
			for c in range(3,19,6):
 
				x=c*self._cellHeight+self._padding
 
				y=r*self._cellWidth+self._padding
 
				self.create_oval(x-radius,y-radius,x+radius,y+radius,tags="star",fill='#000000')
 

	
 
	def _drawCoordinates(self):
 
		for i in range(19):
 
			letters="ABCDEFGHJKLMNOPQRST"
 
			textWidth=12
 
			# cols
 
			self.create_text(self._padding+i*self._cellWidth,self._padding-textWidth,text=letters[i],tags="coords")
 
			self.create_text(self._padding+i*self._cellWidth,self._height-self._padding+textWidth,text=letters[i],tags="coords")
 
			# rows
 
			self.create_text(self._padding-textWidth,self._padding+i*self._cellHeight,text=str(19-i),tags="coords")
 
			self.create_text(self._width-self._padding+textWidth,self._padding+i*self._cellHeight,text=str(19-i),tags="coords")
 

	
 
	## Draws a stone at provided coordinates.
 
	#
 
	#  For an unknown color draws nothing and returns False.
 
	#
 
	#  @param r row coordinate, [0-18], counted from top
 
	#  @param c column coordinate, [0-18], counted from left
 
	#  @param color color indicator, go.BLACK or go.WHITE
 
	def _drawStone(self, r, c, color):
 
	def _drawStone(self, r, c, color, label=""):
 
		if color==BLACK:
 
			hexCode='#000000'
 
			altCode='#ffffff'
 
			tag="black"
 
		elif color==WHITE:
 
			hexCode='#ffffff'
 
			altCode='#000000'
 
			tag="white"
 
		else: return False
 

	
 
		x=c*self._cellWidth+self._padding
 
		y=r*self._cellHeight+self._padding
 
		radius=self._cellWidth/2
 
		self.create_oval(x-radius,y-radius,x+radius,y+radius,tags=tag,fill=hexCode)
 
		if label:
 
			self.create_text(x,y,text=label,tags="label",fill=altCode)
 

	
 
	def _onResize(self,event):
 
		super()._onResize(event)
 
		self._cellWidth=(self._width-2*self._padding)/18
 
		self._cellHeight=(self._height-2*self._padding)/18
src/statebag/__init__.py
Show inline comments
 
@@ -39,37 +39,37 @@ def updateDiff(diff1,diff2):
 
		else:
 
			res.append(diff2[j])
 
			j+=1
 
	if i<m: res.extend(diff1[i:])
 
	else: res.extend(diff2[j:])
 
	return res
 

	
 

	
 
def transformSingle(action1,item1,action2,item2):
 
	if action1=="+":
 
		if action2!="-":
 
			return ("+",item2)
 
	elif action2=="-": return ("-",item2)
 
	elif (action1=="*" and item1==item2) or (action1=="-" and item1!=item2): return ("*",item2)
 
	return None
 

	
 

	
 
class StateBag:
 
	def __init__(self):
 
		self._states=[]
 

	
 
	def pushState(self,board):
 
		sn=BoardState(board)
 
		if len(self._states)>0:
 
			if sn==self._states[-1]: return None # no change
 
			if sn==self._states[-1]: return self._states[-1] # no change
 
			sn.cachedDiff=sn-self._states[-1]
 
		else: sn.setWeight(1)
 

	
 
		diff=sn.cachedDiff
 
		for s in reversed(self._states):
 
			sn.tryConnect(s,diff)
 
			diff=updateDiff(s.cachedDiff,diff)
 
		self._states.append(sn)
 
		return sn
 

	
 
	def merge(self,branch):
 
		pass
src/statebag/boardstate.py
Show inline comments
 
@@ -10,76 +10,94 @@ def estimateDistance(diff):
 
	replacements=len(diff)-additions-deletions
 
	if additions>0: return additions+replacements
 
	elif replacements==0 and deletions>0: return 2 # take n, return 1
 
	return replacements+1 # ???
 

	
 

	
 
class GameTreeNode:
 
	def __init__(self,parent,color):
 
		self.parent=parent
 
		self.color=color # color closing the move sequence
 
		self.prev=None
 
		self.moves=[] # move sequence from prev to self
 
		self.weight=0
 

	
 
	## Connect itself after v if it gains itself more weight.
 
	def tryConnect(self,v,diff):
 
		moves=getTransitionSequence(v.parent,self.parent,-1*v.color,self.color,diff)
 
		if not moves: return
 
		w=v.weight+2-len(moves) # proper one move transition increases the weight by 1
 
		if w>self.weight:
 
			self.moves=moves
 
			self.prev=v
 
			self.weight=w
 

	
 
	def exportRecord(self):
 
		""":return: [(c,row,col), ...]. c in {BLACK,WHITE} == {1,-1}"""
 
		sequence=[]
 
		v=self
 
		while v is not None:
 
			sequence.append(v)
 
			v=v.prev
 

	
 
		res=[]
 
		for v in reversed(sequence):
 
			res.extend(v.moves)
 
		return res
 

	
 

	
 
class BoardState:
 
	def __init__(self,board):
 
		self._board=tuple(tuple(x for x in row) for row in board)
 
		self.nodes=(GameTreeNode(self,BLACK),GameTreeNode(self,WHITE))
 
		self.cachedDiff=[]
 
		self._hash=None
 

	
 
	def tryConnect(self,s,diff=None):
 
		if diff is None: diff=self-s
 
		distEst=estimateDistance(diff)
 
		if distEst>3: return # we couldn't find every such move sequence anyway without a clever algorithm
 
		weightEst=s.getWeight()+2-distEst
 
		if weightEst<=self.getWeight(): return
 
		for v1 in s.nodes:
 
			for v2 in self.nodes:
 
				v2.tryConnect(v1,diff)
 

	
 
	def hash(self):
 
		if self._hash is None: self._hash=hashBoard(self._board)
 
		return self._hash
 

	
 
	def export(self):
 
		return exportBoard(self._board)
 

	
 
	def exportDiff(self,s2):
 
		return "vvv\n{0}\n=== {1} ===\n{2}\n^^^".format(self.export(), s2-self, s2.export())
 

	
 
	def exportRecord(self):
 
		""":return: [(c,row,col), ...]. c in {BLACK,WHITE} == {1,-1}"""
 
		v=self.nodes[0] if self.nodes[0].weight>self.nodes[1].weight else self.nodes[1]
 
		return v.exportRecord()
 

	
 
	def __iter__(self): return iter(self._board)
 

	
 
	def __getitem__(self,key): return self._board[key]
 

	
 
	## Compute difference self-s.
 
	def __sub__(self,s):
 
		res=[]
 

	
 
		for (r,(row,rowS)) in enumerate(zip(self._board,s)):
 
			for (c,(item,itemS)) in enumerate(zip(row,rowS)):
 
				if item==itemS: continue
 
				elif itemS==EMPTY: res.append((r,c,"+",item))
 
				elif item==EMPTY: res.append((r,c,"-",itemS))
 
				else: res.append((r,c,"*",item)) # ->to
 
		return res
 

	
 
	def __eq__(self,x):
 
		return self.hash()==x.hash()
 

	
 
	def setWeight(self,val):
 
		for v in self.nodes: v.weight=val
 

	
 
	def getWeight(self):
 
		return max(v.weight for v in self.nodes)
0 comments (0 inline, 0 general)