Changeset - f9ab2070bd69
[Not reviewed]
default
0 7 2
Laman - 7 years ago 2017-12-14 14:11:07

Engine: more tests and fixes
9 files changed with 241 insertions and 71 deletions:
0 comments (0 inline, 0 general)
src/analyzer/__init__.py
Show inline comments
 
import logging as log
 

	
 
from .grid import Grid
 
from util import BLACK,WHITE,EMPTY
 
from go.core import exportBoard
 
from util import BLACK,WHITE,EMPTY, exportBoard
 

	
 

	
 
class ImageAnalyzer:
 
	def __init__(self,tresB=30,tresW=60):
 
		self.board=[[EMPTY]*19 for r in range(19)]
 
		self.grid=None
 

	
 
		self.tresB=tresB
 
		self.tresW=tresW
 

	
 
	# let's not concern ourselves with sizecoef and shift here anymore. we want corners to come already properly recomputed
 
	def analyze(self,image):
src/go/core.py
Show inline comments
 
@@ -6,27 +6,24 @@ from .gamerecord import GameRecord
 

	
 

	
 
class Go:
 
	## Initializes self.board to a list[r][c]=EMPTY.
 
	def __init__(self,boardSize=19):
 
		self.boardSize=boardSize
 
		self.board=[[EMPTY]*boardSize for x in range(boardSize)]
 
		self.toMove=BLACK
 
		self._helper=HelperBoard(self.board)
 
		self._hashes=[]
 
		self._record=GameRecord()
 

	
 
	def listMoves(self,diff=[]):
 
		return []
 

	
 
	## Executes a move.
 
	#
 
	#  Doesn't check for kos. Suicide not allowed.
 
	#
 
	#  @param color BLACK or WHITE
 
	#  @return True on success, False on failure (illegal move)
 
	def doMove(self,color,row,col):
 
		if color!=self.toMove: log.warning("move by %s out of order",colorNames[color])
 
		if self.board[row][col]!=EMPTY: return False
 

	
 
		self.board[row][col]=color
 

	
 
@@ -38,31 +35,33 @@ class Go:
 
		# check for suicide and prevent it
 
		self._helper.clear()
 
		if not self._helper.floodFill(color,row,col,EMPTY):
 
			self.board[row][col]=EMPTY
 
			return False
 
		self._record.move(color,row,col)
 
		self.toMove=-1*color
 
		self._hashes.append(self.hash())
 
		return True
 

	
 
	def undoMove(self,r,c,captures):
 
		assert self.board[r][c]==-1*self.toMove, "{0}!={1}".format(self.board[r][c],-1*self.toMove)
 
		self.toMove*=-1
 
		self.board[r][c]=self.toMove
 

	
 
		if len(captures)>0:
 
			self._helper.clear()
 
			for (r,c) in captures:
 
				self._helper.floodFill(EMPTY,r,c)
 
			self._fill(-self.toMove)
 
			for (ri,ci) in captures:
 
				self._helper.floodFill(EMPTY,ri,ci)
 
			self._fill(self.toMove)
 

	
 
		self.board[r][c]=EMPTY
 
		self.toMove*=-1
 
		self._hashes.pop()
 

	
 
	def transitionMove(self,board):
 
		res=transitionMove(self.board,board)
 
		if not res: return res
 
		(r,c,color)=res
 
		return self.doMove(color,r,c)
 

	
 
	def load(self,board):
 
		for (r,row) in enumerate(board):
 
			for (c,x) in enumerate(row):
 
				self.board[r][c]=x
 
@@ -70,29 +69,24 @@ class Go:
 
	def hash(self):
 
		return hashBoard(self.board)
 

	
 
	## Removes stones at coordinates marked with True in self.helper.
 
	def _remove(self):
 
		self._fill(EMPTY)
 

	
 
	def _fill(self,filling):
 
		for (r,c) in self._helper.getContinuousArea():
 
			self.board[r][c]=filling
 

	
 

	
 
def exportBoard(board):
 
	substitutions={EMPTY:".", BLACK:"X", WHITE:"O"}
 
	return "\n".join("".join(substitutions.get(x,"?") for x in row) for row in board)
 

	
 

	
 
def isLegalPosition(board):
 
	boardSize=len(board)
 
	temp=[[None]*boardSize for x in range(boardSize)]
 

	
 
	for r in range(boardSize):
 
		for c in range(boardSize):
 
			if board[r][c]==EMPTY: continue
 
			if temp[r][c]: continue
 
			if not dfs([(r,c)],board,temp): return False
 
	return True
 

	
 

	
src/go/engine.py
Show inline comments
 
@@ -6,84 +6,84 @@ def transitionSequence(state1, state2, d
 
	return []
 

	
 

	
 
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.
 
		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."""
 
		res=(set(),set())
 
		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))
 
				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))
 
					if ri>0:
 
						res[colorKey].add((ri-1,ci))
 
						res[1-colorKey].add((ri-1,ci))
 
					if ri+1<self.boardSize:
 
						res[colorKey].add((ri+1,ci))
 
						res[1-colorKey].add((ri+1,ci))
 
					if ci>0:
 
						res[colorKey].add((ri,ci-1))
 
						res[1-colorKey].add((ri,ci-1))
 
					if ci+1<self.boardSize:
 
						res[colorKey].add((ri,ci+1))
 
						res[1-colorKey].add((ri,ci+1))
 
					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:
 
	def __init__(self,g=None):
 
		self._g=g or SpecGo()
 
		self._moveList=(set(),set())
 

	
 
	def load(self,state1,diff):
 
		self._g.load(state1)
 
		self._moveList=self._g.listRelevantMoves(diff)
 

	
 
	def iterativelyDeepen(self,state2):
 
	def iterativelyDeepen(self,state2,toMove=None):
 
		for i in range(1,10):
 
			for color in [BLACK,WHITE]:
 
			for color in [toMove] if toMove else [BLACK,WHITE]:
 
				self._g.toMove=color
 
				seq=self.dfs(state2,i)
 
				if seq:
 
					seq.reverse()
 
					return seq
 

	
 
	def dfs(self,state2,limit):
 
		g=self._g
 
		for (r,c) in self._moveList[(1-g.toMove)>>1]:
 
			if g.board[r][c]!=EMPTY: continue
 
			neighbours=(
 
				g.board[r-1][c] if r>0 else None,
 
				g.board[r+1][c] if r+1<g.boardSize else None,
 
				g.board[r][c-1] if c>0 else None,
 
				g.board[r][c+1] if c+1<g.boardSize else None
 
			)
 
			g.doMove(g.toMove,r,c)
 
			if not g.doMove(g.toMove,r,c): continue
 
			captured=tuple(
 
				coords for (i,coords) in enumerate(((r-1,c),(r+1,c),(r,c-1),(r,c+1)))
 
				if neighbours[i] is not None and neighbours[i]!=EMPTY and g.board[coords[0]][coords[1]]==EMPTY
 
			)
 
			if g.hash()==state2.hash(): return [(-1*g.toMove,r,c)]
 
			if limit>1:
 
				toMove=-1*g.toMove
 
				seq=self.dfs(state2,limit-1)
 
				if seq:
 
					seq.append((toMove,r,c))
 
					return seq
 
			g.undoMove(r,c,captured)
src/statebag.py
Show inline comments
 
@@ -7,25 +7,25 @@ It is relatively cheap to add new items 
 

	
 
User can change detector parameters, presumably in case the previous don't fit the (current) situation.
 
In such a case we assume the new parameters are correct from the current position onwards.
 
But the change might have been appropriate even earlier (before the user detected and fixed an error).
 
So we try to find the correct crossover point like this:
 
- construct a sequence S' = s'_i, ..., s'_n by reanalyzing the positions with a new set of parameters, where s_i is the point of previous user intervention or s_0
 
- for each s'_j:
 
	- try to append it to S[:j]
 
	- try to append it to S'[:j]
 
	- remember the better variant
 
- linearize the fork back by discarding s'_j-s preceding the crossover and s_j-s following the crossover
 
"""
 
from util import EMPTY, hashBoard
 
from util import EMPTY, hashBoard,exportBoard
 
from go.engine import transitionSequence
 

	
 

	
 
## Crude lower bound on edit distance between states.
 
def estimateDistance(diff):
 
	# lot of room for improvements
 
	additions=sum(1 for d in diff if d[2]=="+")
 
	deletions=sum(1 for d in diff if d[2]=="-")
 
	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 # ???
 
@@ -34,24 +34,30 @@ def estimateDistance(diff):
 
class BoardState:
 
	def __init__(self,board):
 
		self._board=tuple(tuple(x for x in row) for row in board)
 
		self.prev=None
 
		self._next=None
 
		self.moves=[]
 
		self.weight=0
 
		self.diff2Prev=None
 

	
 
	def hash(self):
 
		return hashBoard(self._board)
 

	
 
	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 __iter__(self): return iter(self._board)
 

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

	
 
	def __sub__(self,x):
 
		res=[]
 

	
 
		for (r,(row1,row2)) in enumerate(zip(self._board,x)):
 
			for (c,(item1,item2)) in enumerate(zip(row1,row2)):
 
				if item1==item2: continue
 
				elif item2==EMPTY: res.append((r,c,"+",item1))
 
				elif item1==EMPTY: res.append((r,c,"-",item2))
src/tests/data/O-Takao-20110106.sgf
Show inline comments
 
new file 100644
 
(;SZ[19]FF[3]
 
PW[Takao Shinji]
 
WR[9d]
 
PB[O Rissei]
 
BR[9d]
 
EV[66th Honinbo League]
 
DT[2011-01-06]
 
KM[6.5]
 
RE[B+R]
 
US[GoGoD95]
 
;B[pd];W[dd];B[pq];W[dp];B[qo];W[fq];B[cj];W[qf];B[ph];W[qc];B[qd];W[pc]
 
;B[od];W[rd];B[re];W[rc];B[qe];W[nc];B[jp];W[ch];B[me];W[cl];B[ej];W[eh]
 
;B[gj];W[dc];B[pk];W[lp];B[nq];W[ln];B[lq];W[mp];B[mq];W[jn];B[cn];W[bp]
 
;B[km];W[lm];B[kn];W[ll];B[ko];W[kl];B[jl];W[jk];B[kk];W[ok];B[kj];W[pl]
 
;B[pj];W[om];B[mj];W[ql];B[rk];W[qk];B[qj];W[rl];B[rj];W[oo];B[mc];W[nb]
 
;B[hd];W[el];B[ik];W[en];B[gg];W[jd];B[cf];W[dg];B[bc];W[be];B[ce];W[bd]
 
;B[ef];W[df];B[lb];W[ee];B[cd];W[cc];B[hf];W[fi];B[fj];W[hh];B[gh];W[kc]
 
;B[lc];W[nd];B[ne];W[hc];B[gc];W[ic];B[gb];W[if];B[ie];W[gi];B[hi];W[je]
 
;B[ig];W[ih];B[jf];W[hj];B[hk];W[ii];B[ij];W[hb];B[bb];W[bf];B[jb];W[kb]
 
;B[ka];W[gd];B[fd];W[ge];B[fe];W[fc];B[fb];W[ff];B[ec];W[ed];B[fc];W[gf]
 
;B[db];W[cb];B[ca];W[eg];B[id];W[ha];B[ga];W[ea];B[da];W[hg];B[kf];W[ba]
 
;B[aa];W[ac];B[fm];W[ad];B[jc];W[dl];B[jh];W[ji];B[ki];W[kh];B[jg];W[hi]
 
;B[jj];W[fn];B[ei];W[fh];B[bk];W[bl];B[gq];W[gr];B[hq];W[hr];B[ir];W[ro]
 
;B[qp];W[rn];B[ml];W[rq];B[rp];W[sp];B[rr];W[sr];B[qq];W[sq];B[fr];W[er]
 
;B[fp];W[fs];B[eq];W[fr];B[rs];W[qn];B[go];W[gn];B[ep];W[dq];B[hn];W[gm]
 
;B[gl];W[hm];B[po];W[pn];B[nl];W[ol];B[nj];W[lo];B[op];W[np];B[ia];W[se]
 
;B[sf];W[sd];B[rf];W[jr];B[iq];W[kp];B[kq];W[is];B[hs];W[gs];B[kr];W[fl]
 
;B[oc];W[ob];B[al];W[am];B[ak];W[bm];B[in];W[bi];B[lk];W[mm];B[im];W[nk]
 
;B[mk]
 
)
src/tests/data/Sakai-Iyama-20110110.sgf
Show inline comments
 
new file 100644
 
(;SZ[19]FF[3]
 
PW[Iyama Yuta]
 
WR[9d]
 
PB[Sakai Hideyuki]
 
BR[8d]
 
EV[20th Ryusei]
 
RO[Block B, Game 10]
 
DT[2011-01-10 (Broadcast 2011-06-17)]
 
KM[6.5]
 
RE[W+0.5]
 
US[GoGoD95]
 
;B[pd];W[dd];B[pq];W[cp];B[eq];W[qo];B[pm];W[lq];B[iq];W[oo];B[np];W[op]
 
;B[oq];W[nq];B[no];W[nr];B[on];W[qp];B[qq];W[rq];B[rr];W[rp];B[qs];W[or]
 
;B[pr];W[nn];B[lo];W[qm];B[ql];W[pn];B[om];W[rl];B[rm];W[qn];B[lp];W[qk]
 
;B[pl];W[rj];B[kq];W[kr];B[mq];W[do];B[cf];W[di];B[fc];W[df];B[cc];W[ce]
 
;B[dc];W[cg];B[gp];W[nc];B[oc];W[nd];B[qe];W[jc];B[lc];W[lb];B[cr];W[fo]
 
;B[dj];W[cj];B[ck];W[dk];B[ej];W[cl];B[ci];W[bk];B[bj];W[ck];B[dh];W[ei]
 
;B[kb];W[fi];B[kc];W[pf];B[qf];W[pg];B[lf];W[nf];B[pp];W[po];B[rn];W[sm]
 
;B[sn];W[sl];B[pj];W[sr];B[mr];W[pk];B[ok];W[qj];B[sj];W[si];B[ri];W[oj]
 
;B[nm];W[nk];B[nl];W[ol];B[sh];W[rk];B[ok];W[mk];B[ll];W[ol];B[jd];W[lk]
 
;B[ff];W[id];B[je];W[fd];B[gd];W[ec];B[eb];W[ed];B[gc];W[ge];B[hd];W[go]
 
;B[fe];W[fq];B[gq];W[fp];B[fr];W[kl];B[gi];W[eg];B[fg];W[gh];B[fh];W[eh]
 
;B[hi];W[mn];B[lm];W[ln];B[km];W[kn];B[jm];W[jn];B[im];W[jq];B[lr];W[ip]
 
;B[jp];W[in];B[io];W[hm];B[ik];W[mm];B[hl];W[ml];B[jh];W[nb];B[ob];W[rg]
 
;B[qg];W[qh];B[rh];W[qi];B[rf];W[fb];B[db];W[rs];B[ss];W[mo];B[mp];W[rs]
 
;B[ns];W[qr];B[os];W[oa];B[pa];W[bd];B[bq];W[gb];B[he];W[cq];B[br];W[bb]
 
;B[bc];W[ac];B[cd];W[be];B[ef];W[de];B[fj];W[dg];B[hb];W[gk];B[gj];W[jk]
 
;B[il];W[gl];B[hk];W[kg];B[jg];W[bp];B[gm];W[fm];B[hn];W[lg];B[kf];W[ki]
 
;B[mg];W[mh];B[mf];W[ng];B[fl];W[gn];B[fk];W[hp];B[hq];W[ho];B[em];W[jo]
 
;B[kp];W[dq];B[dr];W[ep];B[er];W[cb];B[ga];W[da];B[fa];W[fn];B[el];W[ps]
 
;B[ap];W[ao];B[aq];W[bn];B[na];W[ma];B[kh];W[lh];B[jj];W[kj];B[ji];W[oa]
 
;B[pb];W[ca];B[qs];W[ea];B[fb];W[ps];B[jl];W[kk];B[qs];W[od];B[pe];W[ps]
 
;B[oe];W[ne];B[qs];W[qc];B[pc];W[ps];B[of];W[og];B[qs];W[rd];B[rb];W[ps]
 
;B[ab];W[aa];B[qs];W[qb];B[qa];W[ps];B[hm];W[ka];B[ja];W[ld];B[la];W[ra]
 
;B[sa];W[ka];B[kd];W[qs];B[dn];W[le];B[la];W[mb];B[cn];W[cm];B[en];W[co]
 
;B[ke];W[me];B[mc];W[ka];B[ad];W[ae];B[la];W[gf];B[gg];W[ka];B[na];W[la]
 
;B[oa];W[sk];B[sg];W[si];B[hf];W[sj]
 
)
src/tests/testEngine.py
Show inline comments
 
import re
 
import os.path
 
import logging as log
 
from unittest import TestCase
 

	
 
import config as cfg
 
from util import BLACK as B,WHITE as W,EMPTY as _
 
from go.core import Go
 
from go.engine import SpecGo,Engine
 
from statebag import BoardState
 

	
 

	
 
_log=log.getLogger(__name__)
 
_log.setLevel(log.INFO)
 
_log.propagate=False
 
formatter=log.Formatter("%(asctime)s %(levelname)s: %(message)s",datefmt="%Y-%m-%d %H:%M:%S")
 
handler=log.FileHandler("/tmp/oneeye.log",mode="w")
 
handler.setFormatter(formatter)
 
_log.addHandler(handler)
 

	
 

	
 
def simpleLoadSgf(filename):
 
	with open(filename) as f:
 
		contents=f.read()
 
	g=lambda m: tuple(ord(c)-ord('a') for c in reversed(m.group(1)))
 
	return [g(m) for m in re.finditer(r"\b[BW]\[([a-z]{2})\]",contents)]
 

	
 

	
 
def listStates(moves):
 
	g=Go()
 
	res=[BoardState(g.board)]
 
	for m in moves:
 
		g.doMove(g.toMove,*m)
 
		res.append(BoardState(g.board))
 
	return res
 

	
 

	
 
class TestTransitions(TestCase):
 
	def testBasic(self):
 
		s1=BoardState([
 
			[0,0,0],
 
			[0,0,0],
 
			[0,0,0]
 
			[_,_,_],
 
			[_,_,_],
 
			[_,_,_]
 
		])
 
		s2=BoardState([
 
			[0,0,0],
 
			[0,1,0],
 
			[0,0,0]
 
			[_,_,_],
 
			[_,B,_],
 
			[_,_,_]
 
		])
 
		g=SpecGo(3)
 
		eng=Engine(g)
 
		eng.load(s1,s2-s1)
 
		self.assertEqual(eng.dfs(s2,1),[(1,1,1)])
 

	
 
	def testCapture(self):
 
		s1=BoardState([
 
			[0,-1,0],
 
			[-1,1,0],
 
			[0,-1,0]
 
			[_,W,_],
 
			[W,B,_],
 
			[_,W,_]
 
		])
 
		s2=BoardState([
 
			[0,-1,0],
 
			[-1,0,-1],
 
			[0,-1,0]
 
			[_,W,_],
 
			[W,_,W],
 
			[_,W,_]
 
		])
 
		g=SpecGo(3)
 
		g.toMove=-1
 
		g.toMove=W
 
		eng=Engine(g)
 
		eng.load(s1,s2-s1)
 
		self.assertEqual(eng.dfs(s2,1),[(-1,1,2)])
 
		self.assertEqual(eng.dfs(s2,1),[(W,1,2)])
 

	
 
	def testMulti(self):
 
		s1=BoardState([
 
			[0,0,0],
 
			[0,0,0],
 
			[0,0,0]
 
			[_,_,_],
 
			[_,_,_],
 
			[_,_,_]
 
		])
 
		s2=BoardState([
 
			[0,0,0],
 
			[0,1,-1],
 
			[0,0,0]
 
			[_,_,_],
 
			[_,B,W],
 
			[_,_,_]
 
		])
 
		g=SpecGo(3)
 
		eng=Engine(g)
 
		eng.load(s1,s2-s1)
 
		self.assertEqual(eng.dfs(s2,2),[(-1,1,2),(1,1,1)])
 
		self.assertEqual(eng.dfs(s2,2),[(W,1,2),(B,1,1)])
 

	
 
	def testSnapback(self):
 
		s1=BoardState([
 
			[B,B,B],
 
			[B,_,B],
 
			[B,W,B]
 
		])
 
		s2=BoardState([
 
			[_,_,_],
 
			[_,_,_],
 
			[_,W,_]
 
		])
 
		g=SpecGo(3)
 
		eng=Engine(g)
 
		eng.load(s1,s2-s1)
 
		self.assertEqual(eng.dfs(s2,2),[(W,2,1),(B,1,1)])
 

	
 
		s1=BoardState([
 
			[_,_,_],
 
			[W,B,B],
 
			[_,W,W]
 
		])
 
		s2=BoardState([
 
			[_,_,_],
 
			[W,B,B],
 
			[_,W,_]
 
		])
 
		eng.load(s1,s2-s1)
 
		self.assertEqual(eng.dfs(s2,2),[(W,2,1),(B,2,0)])
 

	
 
	def testReal(self):
 
		files=["O-Takao-20110106.sgf","Sakai-Iyama-20110110.sgf"]
 
		g=SpecGo()
 
		eng=Engine(g)
 

	
 
		for f in files:
 
			moves=simpleLoadSgf(os.path.join(cfg.srcDir,"tests/data",f))
 
			states=listStates(moves)
 

	
 
			for k in range(1,4):
 
				toMove=B
 
				for (s1,s2) in zip(states,states[k:]):
 
					diff=s2-s1
 
					eng.load(s1,diff)
 
					seq=eng.iterativelyDeepen(s2,toMove)
 
					msg="\n"+s1.exportDiff(s2)
 
					self.assertIsNotNone(seq,msg)
 
					self.assertLessEqual(len(seq),k,msg)
 
					if len(seq)!=k: _log.warning("shorter than expected transition sequence:" + msg + "\n" + str(seq))
 
					toMove*=-1
src/tests/testGo.py
Show inline comments
 
from unittest import TestCase
 

	
 
from util import BLACK as B,WHITE as W,EMPTY as _
 
from go.core import isLegalPosition, Go
 

	
 

	
 
class TestLegal(TestCase):
 
	def testLegal(self):
 
		board=[
 
			[0,0,0,0,0],
 
			[1,1,1,1,1],
 
			[-1,-1,0,1,0],
 
			[0,0,0,-1,-1],
 
			[0,-1,0,-1,0]
 
			[_,_,_,_,_],
 
			[B,B,B,B,B],
 
			[W,W,_,B,_],
 
			[_,_,_,W,W],
 
			[_,W,_,W,_]
 
		]
 
		self.assertTrue(isLegalPosition(board))
 

	
 
	def testIllegal(self):
 
		board=[
 
			[0,1,0,0,0],
 
			[1,-1,1,0,0],
 
			[0,1,0,0,0],
 
			[0,0,0,0,0],
 
			[0,0,0,0,0]
 
			[_,B,_,_,_],
 
			[B,W,B,_,_],
 
			[_,B,_,_,_],
 
			[_,_,_,_,_],
 
			[_,_,_,_,_]
 
		]
 
		self.assertFalse(isLegalPosition(board))
 

	
 

	
 
class TestMove(TestCase):
 
	def testCapture(self):
 
		g=Go(3)
 
		g.load([
 
			[0,1,0],
 
			[1,-1,0],
 
			[0,1,0]
 
			[_,B,_],
 
			[B,W,_],
 
			[_,B,_]
 
		])
 
		g.toMove=1
 
		g.doMove(1,1,2)
 
		g.toMove=B
 
		g.doMove(B,1,2)
 
		self.assertEqual(g.board,[
 
			[0,1,0],
 
			[1,0,1],
 
			[0,1,0]
 
			[_,B,_],
 
			[B,_,B],
 
			[_,B,_]
 
		])
 

	
 
		g._helper.clear()
 
		for row in g._helper._board:
 
			for x in row:
 
				self.assertEqual(x,0)
 

	
 
	def testUndo(self):
 
		g=Go(3)
 
		g.load([
 
			[_,B,_],
 
			[B,W,_],
 
			[_,B,_]
 
		])
 
		g.toMove=B
 
		g.doMove(B,1,2)
 
		g.undoMove(1,2,((1,1),))
 
		self.assertEqual(g.board,[
 
			[_,B,_],
 
			[B,W,_],
 
			[_,B,_]
 
		])
src/util.py
Show inline comments
 
@@ -42,12 +42,17 @@ rand.seed(361)
 
zobristNums=tuple(
 
	tuple(
 
		tuple(rand.getrandbits(32) for i in range(3)) for c in range(19)
 
	) for r in range(19)
 
)
 

	
 
def hashBoard(board):
 
	res=0
 
	for (r,row) in enumerate(board):
 
		for (c,item) in enumerate(row):
 
			res^=zobristNums[r][c][item+1]
 
	return res
 

	
 

	
 
def exportBoard(board):
 
	substitutions={EMPTY:".", BLACK:"X", WHITE:"O"}
 
	return "\n".join("".join(substitutions.get(x,"?") for x in row) for row in board)
0 comments (0 inline, 0 general)