Tic Tac Toe

In today’s lecture, we will experiment with a graphics package to build a simple text-based board game (Tic Tac Toe).

Using the Graphics Module

We will experiment with a few commands to get started with the graphics module (provided in graphics.py).

from graphics import *
# takes title and size of window
win = GraphWin("Name", 400, 400) 
# create point obj at x, y coordinate in window
pt = Point(200, 200)
# create circle with center at pt and radius 100
c = Circle(Point(200,200), 100)
# draw the circle on the window
c.draw(win)
# set color to blue
c.setFill("blue")
# Pause until a mouse is clicked on window
# win.getMouse() 
# Close window when done
win.close()    

Building a Board Class

Let’s think about the design of a basic board class for building games. Even though we are thinking about Tic Tac Toe today, this class is a generic class that we want to use for multiple games. Thus we should not include any game-specific details.

from graphics import *

class Board:
    # _win: graphical window on which we will draw our board
    # _xInset: avoids drawing in corner of window
    # _yInset: avoids drawing in corner of window
    # _rows: number of rows in grid of squares
    # _cols: number of columns in grid of squares
    # _size: edge size of each square

    __slots__ = [ '_xInset', '_yInset', '_rows', '_cols', '_size', \
                '_win', '_exitButton', '_resetButton', \
                '_textArea', '_lowerWord', '_upperWord']

    def __init__(self, win, xInset=50, yInset=50, rows=3, cols=3, size=50):
        # update class attributes
        self._xInset = xInset
        self._yInset = yInset
        self._rows = rows
        self._cols = cols
        self._size = size
        self._win = win

    # getter methods for attributes
    def getWin(self):
        return self._win

    def getXInset(self):
        return self._xInset

    def getYInset(self):
        return self._yInset

    def getRows(self):
        return self._rows

    def getCols(self):
        return self._cols

    def getSize(self):
        return self._size

    def __initTextAreas(self):
        # initialize text areas
        self._textArea = Text(Point(self._xInset * self._rows + self._size * 2,
                                    self._yInset + 50), "")
        self._textArea.setSize(14)
        self._lowerWord = Text(Point(160, 275), "")
        self._lowerWord.setSize(18)
        self._upperWord = Text(Point(160, 25), "")
        self._upperWord.setSize(18)
        self._upperWord.setTextColor("red")

    def __drawTextAreas(self):
        """Draw the text area to the right/lower/upper side of main grid"""

        #initialize before drawing
        self.__initTextAreas()

        # draw main text area (right of grid)
        self._textArea.draw(self._win)

        #draw the text area below grid
        self._lowerWord.draw(self._win)

        #draw the text area above grid
        self._upperWord.draw(self._win)

    def __makeGrid(self):
        """Creates a row x col grid, filled with squares"""
        for x in range(self._cols):
            for y in range(self._rows):
                # create first point
                p1 = Point(self._xInset + self._size * x, self._yInset + self._size * y)
                # create second point
                p2 = Point(self._xInset + self._size * (x + 1), self._yInset + self._size * (y + 1))
                # create rectangle
                r = Rectangle(p1, p2)
                r.setFill("white")
                # add rectangle to graphical window
                r.draw(self._win)

                # print coord (for debugging)
                #Text(Point(self._xInset + 15 + self._size * x, \
                #           self._yInset + 15+ self._size * y), \
                #           "{},{}".format(x,y)).draw(win)

    def __makeResetButton(self):
        """Add a reset button to board"""
        self._resetButton = Rectangle(Point(50, 300), Point(130, 350))
        self._resetButton.setFill("white")
        self._resetButton.draw(self._win)
        Text(Point(90, 325), "RESET").draw(self._win)

    def __makeExitButton(self):
        """Add exit button to board"""
        self._exitButton = Rectangle(Point(170, 300), Point(250, 350))
        self._exitButton.draw(self._win)
        self._exitButton.setFill("white")
        Text(Point(210, 325), "EXIT").draw(self._win)

    def drawBoard(self):
        # this creates a row x col grid, filled with squares, including buttons
        self._win.setBackground("white smoke")
        self.__makeGrid()
        self.__makeResetButton()
        self.__makeExitButton()
        self.__drawTextAreas()

    # HELPER METHODS
        
    # convert grid position to window coordinate
    def _getLocation(self, position):
        '''
        Converts a grid position (tuple) to a window location (tuple).
        Window locations are x, y coordinates.
        '''
        x = position[0] * self._size + self._xInset
        y = position[1] * self._size + self._yInset
        return (x, y)

    # convert window coordinate (tuple) to grid position (tuple)
    def getPosition(self, location):
        '''
        Converts a window location (tuple) to a grid position (tuple).
        Window locations are x, y coordinates.
        Note: Grid positions are always returned as col, row.
        '''
        if location[1] < self._yInset:
            row = -1
        else:
            row = int((location[1] - self._yInset) / self._size)

        if location[0] < self._xInset:
            col = -1
        else:
            col = int((location[0] - self._xInset) / self._size)
        return (col, row)

    # check for click inside specific rectangular region
    def _inRect(self, point, rect):
        '''
        Returns True if a Point (point) exists inside a specific
        Rectangle (rect) on screen.
        '''
        pX = point.getX()
        pY = point.getY()
        rLeft = rect.getP1().getX()
        rTop = rect.getP1().getY()
        rRight = rect.getP2().getX()
        rBottom = rect.getP2().getY()
        return pX > rLeft and pX < rRight and pY > rTop and pY < rBottom

    # check for click in grid
    def inGrid(self, point):
        '''
        Returns True if a Point (point) exists inside the grid of squares.
        '''
        ptX = point.getX()
        ptY = point.getY()
        maxY = self._size * (self._rows + 1)
        maxX = self._size * (self._cols + 1)
        return ptX <= maxX and ptY <= maxY and ptX >= self._xInset and ptY >= self._yInset

    # clicked in exit button?
    def inExit(self, point):
        '''
        Returns true if point is inside exit button (rectangle)
        '''
        return self._inRect(point, self._exitButton)

    # clicked in reset button?
    def inReset(self, point):
        '''
        Returns true if point is inside exit button (rectangle)
        '''
        return self._inRect(point, self._resetButton)

    # add text to text area on right
    def addStringToTextArea(self, text):
        '''
        Add text to text area to right of grid.
        Does not overwrite existing text.
        '''
        str = self._textArea.getText()
        self._textArea.setText( str + text )

    # set text to text area on right
    def setStringToTextArea(self, text):
        '''
        Sets text to text area to right of grid.
        Overwrites existing text.
        '''
        self._textArea.setText(text)

    # clear text from text area on right
    def clearTextArea(self):
        '''
        Clear text in text area to right of grid.
        '''
        self._textArea.setText("")

    # add text to text area below grid
    def addStringToLowerText(self, text):
        '''
        Add text to text area below grid.
        Does not overwrite existing text.
        '''
        str = self._lowerWord.getText()
        self._lowerWord.setText( str + text )

    # add text to text area below grid
    def setStringToLowerText(self, text):
        '''
        Set text to text area below grid.
        Overwrites existing text.
        '''
        self._lowerWord.setText( text )

    # clear word below grid
    def clearLowerText(self):
        '''
        Clear text area below grid.
        '''
        self._lowerWord.setText("")

    # add text to text area above grid
    def addStringToUpperText(self, text):
        '''
        Add text to text area above grid.
        Does not overwrites existing text.'''
        self._upperWord.setText(text)

    # set text to text area above grid
    def setStringToUpperText(self, text):
        '''
        Set text to text area above grid.
        Overwrites existing text.
        '''
        self._upperWord.setText(text)

    # clear text above grid
    def clearUpperText(self):
        '''
        Clear text area above grid.
        '''
        self._upperWord.setText("")
win = GraphWin("Tic Tac Toe", 400, 400)  # we need to create a window before we call draw
# create new board with default values
board = Board(win)
type(board)
# draw Board
board.drawBoard()
# test getters and verify attribute values
board.getRows()
board.getCols()
board.getSize()
# set string above grid
board.setStringToUpperText("Upper text area")
# set and update string below grid
board.setStringToLowerText("Lower text area: ")
board.addStringToLowerText("hi!")
# set string to text area to right of grid
board.setStringToTextArea("Text area")
# wait for a mouse click
point = win.getMouse()

# calculate x and y value from point
x,y = point.getX(), point.getY()
print("Clicked coord {}".format((x,y)))

print("Clicked in reset?", board.inReset(point))
print("Clicked in exit?", board.inExit(point))
print("Clicked in grid?", board.inGrid(point))

if board.inGrid(point):
    print("Grid coord: {}".format(board.getPosition((x,y))))
    
win.close()