[Tutor] Dots-And-Boxes
Gregor Lingl
glingl@aon.at
Mon, 3 Jun 2002 04:14:19 +0200
This is a multi-part message in MIME format.
------=_NextPart_000_0060_01C20AB5.21C0B170
Content-Type: text/plain;
charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
----- Original Message -----
From: "Danny Yoo" <dyoo@hkn.eecs.berkeley.edu>
Ok, fixed, I think. I took Gregor's suggestions, and now my
_isSquareMove() returns a list of squares instead of just one potential
square.
At most 2 squares can be created by a move, via a "double-crossed" move.
The following play shows how this can be done:
----------------
gui version accordingly fixed. see attachment
Gregor
------=_NextPart_000_0060_01C20AB5.21C0B170
Content-Type: text/plain;
name="gboard2.py"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="gboard2.py"
"""Graphic user interface to Danny Yoo's 'dots and boxes' -program.
I tried to avoid changes to this program as far as possible.
So there remain the following changes:
Line 229: Return statement in play() now returns the
square_corners instead of 1 (this was the only
necessary one (in m opinion)
Line 258/259: head of for-loop --- this was the easiest way
to accomodate text-output to graphical one.
( Bad design decision for the graphics display,
may be reverted if there is time ...)
Lines 308-321, 335, 336: A somewhat safer input method for
more convenient testing
Lines 357ff: Incorporate graphics mode into __main__()
"""
from Tkinter import *
from Canvas import Rectangle
def cartesian( v1, v2 ):
""" Helper function
returns cartesian product of the two
'sets' v1, v2"""
return tuple([(x,y) for x in v1 for y in v2])
def right(x):
"""Helper function: argument x must be a dot.
Returns dot right of x."""
return (x[0]+1,x[1])
def upper(x):
"""Helper function: argument x must be a dot.
Returns dot above (actually below) x."""
return (x[0], x[1]+1)
class GameGUI:
def __init__(self, board):
"""Initializes graphic display of a rectangular gameboard."""
# Properties of gameboard
dw =3D self.dotwidth =3D 6
sw =3D self.squarewidth =3D 60
sk =3D self.skip =3D 4
fw =3D self.fieldwidth =3D dw + sw + 2*sk
ins =3D self.inset =3D sw/2
self.barcolors =3D ['red','blue']
self.squarecolors =3D ['orange', 'lightblue']
# Construct Canvas =20
self.board =3D board
width, height =3D board.width, board.height
# compute size of canvas:
w =3D width * fw=20
h =3D height * fw=20
self.root =3D Tk()
cv =3D self.cv =3D Canvas(self.root, width=3Dw, height=3Dh, =
bg=3D'white')
cv.bind('<Button-1>', self._callback)
cv.pack()
# Put geometrical objects - dots, bars and squares - on canvas
self.bars =3D {}
self.squares =3D {}
for dot in cartesian(range(width), range(height)):
# dots. Never used again
Rectangle( cv, ins+dot[0]*fw, ins+dot[1]*fw,
ins+dot[0]*fw + dw, ins+dot[1]*fw + dw,
fill=3D'black', outline=3D'' )
# horizontal bars
if dot[0] < width - 1:
x0 =3D ins+dot[0]*fw+dw+sk
y0 =3D ins+dot[1]*fw
self.bars[(dot,right(dot))] =3D\
=
Rectangle(cv,x0,y0,x0+sw,y0+dw,fill=3D'lightgray',outline=3D'')
# vertical bars
if dot[1] < height - 1:
x0 =3D ins+dot[0]*fw
y0 =3D ins+dot[1]*fw + dw + sk
self.bars[(dot,upper(dot))] =3D\
=
Rectangle(cv,x0,y0,x0+dw,y0+sw,fill=3D'lightgray',outline=3D'')
# squares
if (dot[0] < width - 1) and (dot[1] < height - 1):
x0 =3Dins+dot[0]*fw + dw + sk
y0 =3Dins+dot[1]*fw + dw + sk=20
self.squares[dot] =3D\
=
Rectangle(cv,x0,y0,x0+sw,y0+sw,fill=3D'lightyellow',outline=3D'')
cv.update()
self.root.mainloop() =20
def _coord(self,x):
"""returns pixel-coordinate corresponding to
a dot-coordinate x"""
return self.inset + self.dotwidth/2 + self.fieldwidth*x
def _find_bar(self,event):
"""returns bar next to mouse-position when clicked,
if applicable, otherwise None"""
ex, ey =3D event.x, event.y
for bar in self.bars:
((x1,y1),(x2,y2))=3Dbar
mx, my =3D (self._coord(x1)+self._coord(x2))/2, =
(self._coord(y1)+self._coord(y2))/2
if abs(ex-mx)+abs(ey-my) < self.squarewidth/2:
return bar
=20
def _callback(self, event):
"""Action following a mouse-click"""
hit =3D self._find_bar(event)
board =3D self.board
print "Hit:", hit
if not hit or board.isGameOver() or board.board.has_key(hit):
return
# Do a move
player =3D board.getPlayer()
print "Turn %d (Player %s)" % (board.turn, player)
self.bars[hit]['fill']=3Dself.barcolors[player]
# Here following Danny's bug fix
targets =3D board.play(hit)
print "Targets:", targets
for target in targets:
print "Square completed.", board.squares[target]
self.squares[target]['fill'] =3D self.squarecolors[player]
board.scores[player] +=3D 1
board.turn =3D board.turn + 1
print "\n"
if board.isGameOver():
print "Game over!"
print "Final board position:"
print board
print
print "Final score:\n\tPlayer 0: %s\n\tPlayer 1: %s" % \
tuple(board.scores)
def _gtest(width, height):=0A=
"""A small driver to make sure that the board works. It's not=0A=
safe to use this test function in production, because it uses=0A=
input()."""
print "Running _gtest... "=0A=
board =3D GameBoard(width, height)=0A=
board.turn =3D 1=0A=
board.scores =3D [0, 0]
gui =3D GameGUI(board)
##### Danny Yoo's board.py #########################################
import types=0A=
=0A=
class GameBoard:=0A=
def __init__(self, width=3D5, height=3D5):=0A=
"""Initializes a rectangular gameboard."""=0A=
self.width, self.height =3D width, height=0A=
assert 2 <=3D self.width and 2 <=3D self.height,\=0A=
"Game can't be played on this board's dimension."=0A=
self.board =3D {}=0A=
self.squares =3D {}=0A=
self.player =3D 0=0A=
=0A=
=0A=
def isGameOver(self):=0A=
"""Returns true if no more moves can be made.=0A=
=0A=
The maximum number of moves is equal to the number of possible=0A=
lines between adjacent dots. I'm calculating this to be=0A=
$2*w*h - h - w$; I think that's right. *grin*=0A=
"""=0A=
w, h =3D self.width, self.height=0A=
return len(self.board.keys()) =3D=3D 2*w*h - h - w=0A=
=0A=
=0A=
=0A=
def _isSquareMove(self, move):=0A=
"""Returns a true value if a particular move will create a=0A=
square. In particular, returns a list of the the lower left=0A=
corners of the squares captured by a move.=0A=
=0A=
(Note: I had forgotten about double crossed moves. Gregor=0A=
Lingl reported the bug; I'd better fix it now! *grin*) """=0A=
b =3D self.board=0A=
mmove =3D self._makeMove ## just to make typing easier=0A=
((x1, y1), (x2, y2)) =3D move=0A=
captured_squares =3D []=0A=
if self._isHorizontal(move):=0A=
for j in [-1, 1]:=0A=
if (b.has_key(mmove((x1, y1), (x1, y1-j)))=0A=
and b.has_key(mmove((x1, y1-j), (x1+1, y1-j)))=0A=
and b.has_key(mmove((x1+1, y1-j), (x2, y2)))):=0A=
captured_squares.append(min([(x1, y1), (x1, y1-j),=0A=
(x1+1, y1-j), (x2, =
y2)]))=0A=
else:=0A=
for j in [-1, 1]:=0A=
if (b.has_key(mmove((x1, y1), (x1-j, y1)))=0A=
and b.has_key(mmove((x1-j, y1), (x1-j, y1+1)))=0A=
and b.has_key(mmove((x1-j, y1+1), (x2, y2)))):=0A=
captured_squares.append(min([(x1, y1), (x1-j, y1),=0A=
(x1-j, y1+1), (x2, =
y2)]))=0A=
return captured_squares=0A=
=0A=
=0A=
=0A=
def _isHorizontal(self, move):=0A=
"Return true if the move is in horizontal orientation."=0A=
return abs(move[0][0] - move[1][0]) =3D=3D 1=0A=
=0A=
=0A=
def _isVertical(self, move):=0A=
"Return true if the move is in vertical orientation."=0A=
return not self.isHorizontal(self, move)=0A=
=0A=
=0A=
def play(self, move):=0A=
"""Place a particular move on the board. If any wackiness=0A=
occurs, raise an AssertionError."""=0A=
assert (self._isGoodCoord(move[0]) and=0A=
self._isGoodCoord(move[1])),\=0A=
"Bad coordinates, out of bounds of the board."=0A=
move =3D self._makeMove(move[0], move[1])=0A=
assert(not self.board.has_key(move)),\=0A=
"Bad move, line already occupied."=0A=
self.board[move] =3D self.player=0A=
## Check if a square is completed.=0A=
square_corners =3D self._isSquareMove(move)=0A=
if square_corners:=0A=
for corner in square_corners:=0A=
self.squares[corner] =3D self.player=0A=
else:=0A=
self._switchPlayer()=0A=
return square_corners # <=3D=3D here also change =
necessary!!=0A=
=0A=
=0A=
def _switchPlayer(self):=0A=
self.player =3D (self.player + 1) % 2=0A=
=0A=
=0A=
def getPlayer(self): return self.player=0A=
=0A=
=0A=
def getSquares(self):=0A=
"""Returns a dictionary of squares captured. Returns=0A=
a dict of lower left corner keys marked with the=0A=
player who captured them."""=0A=
return self.squares=0A=
=0A=
=0A=
def __str__(self):=0A=
"""Return a nice string representation of the board."""=0A=
buffer =3D []=0A=
=0A=
## do the top line=0A=
for i in range(self.width-1):=0A=
if self.board.has_key(((i, self.height-1), (i+1, =
self.height-1))):=0A=
buffer.append("+--")=0A=
else: buffer.append("+ ")=0A=
buffer.append("+\n")=0A=
=0A=
## and now do alternating vertical/horizontal passes=0A=
# for j in range(self.height-2, -1, -1): # CHANGED for =
corresponence =0A=
for j in range(self.height-1): # with graphical display=0A=
## vertical:=0A=
for i in range(self.width):=0A=
if self.board.has_key(((i, j), (i, j+1))):=0A=
buffer.append("|")=0A=
else:=0A=
buffer.append(" ")=0A=
if self.squares.has_key((i, j)):=0A=
buffer.append("%s " % self.squares[i,j])=0A=
else:=0A=
buffer.append(" ")=0A=
buffer.append("\n")=0A=
=0A=
## horizontal=0A=
for i in range(self.width-1):=0A=
if self.board.has_key(((i, j), (i+1, j))):=0A=
buffer.append("+--")=0A=
else: buffer.append("+ ")=0A=
buffer.append("+\n")=0A=
=0A=
return ''.join(buffer)=0A=
=0A=
=0A=
=0A=
def _makeMove(self, coord1, coord2):=0A=
"""Return a new "move", and ensure it's in canonical form.=0A=
(That is, force it so that it's an ordered tuple of tuples.)=0A=
"""=0A=
## TODO: do the Flyweight thing here to reduce object creation=0A=
xdelta, ydelta =3D coord2[0] - coord1[0], coord2[1] - coord1[1]=0A=
assert ((abs(xdelta) =3D=3D 1 and abs(ydelta) =3D=3D 0) or=0A=
(abs(xdelta) =3D=3D 0 and abs(ydelta) =3D=3D 1)),\=0A=
"Bad coordinates, not adjacent points."=0A=
if coord1 < coord2:=0A=
return (coord1, coord2)=0A=
else:=0A=
return (tuple(coord2), tuple(coord1))=0A=
=0A=
=0A=
def _isGoodCoord(self, coord):=0A=
"""Returns true if the given coordinate is good.=0A=
=0A=
A coordinate is "good" if it's within the boundaries of the=0A=
game board, and if the coordinates are integers."""=0A=
return (0 <=3D coord[0] < self.width=0A=
and 0 <=3D coord[1] < self.height=0A=
and isinstance(coord[0], types.IntType)=0A=
and isinstance(coord[1], types.IntType))=0A=
def getMove(self): # <=3D=3D=3D NEW NEW NEW
"""Found this little bit of error-checking useful
for testing, because I tend to mistype everything
rather often.
Moreover now it's more safe"""
done =3D 0
while not done:
try:
x1, y1, sep, x2, y2 =3D raw_input("Move?").split()
move =3D ((int(x1),int(y1)),(int(x2),int(y2)))
done =3D 1
except:
pass
return move
=0A=
=0A=
def _test(width, height):=0A=
"""A small driver to make sure that the board works. It's not=0A=
safe to use this test function in production, because it uses=0A=
input()."""=0A=
board =3D GameBoard(width, height)=0A=
turn =3D 1=0A=
scores =3D [0, 0]=0A=
while not board.isGameOver():=0A=
player =3D board.getPlayer()=0A=
print "Turn %d (Player %s)" % (turn, player)=0A=
print board=0A=
# move =3D input("Move? ")
move =3D board.getMove() # <=3D=3D CHANGED !!!=0A=
if board.play(move):=0A=
print "Square completed."=0A=
scores[player] +=3D 1=0A=
turn =3D turn + 1=0A=
print "\n"=0A=
print "Game over!"=0A=
print "Final board position:"=0A=
print board=0A=
print=0A=
print "Final score:\n\tPlayer 0: %s\n\tPlayer 1: %s" % \=0A=
(scores[0], scores[1])=0A=
=0A=
if __name__ =3D=3D "__main__":
"""If we're provided arguments,
look if first one equals 't', in which case
textmode only is invoked.
try using rest of the arguments as the
width/height of the game board."""
import sys
if len(sys.argv[1:]) > 0 and sys.argv[1] =3D=3D 't':
# textmode
if len(sys.argv[1:]) =3D=3D 3:
_test(int(sys.argv[2]), int(sys.argv[3]))
elif len(sys.argv[1:]) =3D=3D 2:
_test(int(sys.argv[2]), int(sys.argv[2]))
else:
_test(5, 5)
else:
# grachics mode
if len(sys.argv[1:]) =3D=3D 2:
_gtest(int(sys.argv[1]), int(sys.argv[2]))
elif len(sys.argv[1:]) =3D=3D 1:
_gtest(int(sys.argv[1]), int(sys.argv[1]))
else:
_gtest(5, 5)
------=_NextPart_000_0060_01C20AB5.21C0B170--