[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--