CMU Coding Bootcamp
at main 245 lines 8.0 kB view raw
1# Tic-Tac-Toe 2# by David Kosbie 3 4# This implements a basic game of TicTacToe. 5 6# This version does not save or load the game (that is left for you to do!) 7 8import json 9from cmu_graphics import * 10import math 11import os 12 13def onAppStart(app): 14 # Set the model values (in the app object) that never change. 15 app.rows = 3 16 app.cols = 3 17 app.boardBounds = (50, 75, 350, 375) # left, top, right, bottom 18 app.cellBorderWidth = 2 19 resetApp(app) 20 21def resetApp(app): 22 app.selection = None 23 app.board = [[None]*app.cols for row in range(app.rows)] 24 app.turn = 'X' 25 app.message = "X's turn" 26 app.turnCount = 0 27 app.gameOver = False 28 app.winningCells = None 29 30def saveGame(app): 31 with open("savedGame.txt", "w") as f: 32 json.dump({ 33 "board": app.board, 34 "turn": app.turn, 35 "gameOver": app.gameOver, 36 "winningCells": app.winningCells 37 }, f) 38 39def loadGame(app): 40 with open("savedGame.txt", "r") as f: 41 game = json.load(f) 42 app.selection = None 43 app.board = game["board"] 44 app.turn = game["turn"] 45 app.message = f"{app.turn}'s turn" 46 flat_list = [] 47 for r in app.board: 48 for c in r: 49 if c != None: 50 flat_list.append(1) 51 app.turn_count = sum(flat_list) 52 app.gameOver = game["gameOver"] 53 app.winningCells = game["winningCells"] 54 55 56def onKeyPress(app, key): 57 if key == 's': 58 saveGame(app) 59 elif key == 'l': 60 loadGame(app) 61 elif (app.gameOver) and (key == 'r'): 62 resetApp(app) 63 64def onMousePress(app, mouseX, mouseY): 65 # Always clear the selection on any mouse press. 66 app.selection = None 67 # Then, make the move, but only if the game is not over, and the move is legal 68 # (that is, it's in an empty cell). 69 if not app.gameOver: 70 cell = getCell(app, mouseX, mouseY) 71 if cell != None: 72 row, col = cell 73 if app.board[row][col] == None: 74 makeMove(app, row, col) 75 76def onMouseMove(app, mouseX, mouseY): 77 if app.gameOver: 78 return 79 # Set the cell selection as the mouse is moved, but only 80 # if there is a selected cell, and that cell on the board is empty. 81 # Otherwise, clear the cell selection. 82 selectedCell = getCell(app, mouseX, mouseY) 83 if selectedCell == None: 84 app.selection = None 85 else: 86 row, col = selectedCell 87 if app.board[row][col] == None: 88 app.selection = selectedCell 89 else: 90 app.selection = None 91 92def makeMove(app, row, col): 93 # We already know that this is a legal move, so set the board 94 # to the current player, add one to the turn count, check if the 95 # game is over, and if not, change turns. 96 app.board[row][col] = app.turn 97 app.turnCount += 1 98 checkForGameOver(app) 99 if not app.gameOver: 100 changeTurns(app) 101 102def checkForGameOver(app): 103 # Check if the game is over (tie or win), and if so, set app.gameOver to 104 # True and set the app.message as appropriate. 105 # First check for a tie game. If it is, set 106 if app.turnCount == app.rows * app.cols: 107 app.gameOver = True 108 app.message = 'Tie game!' 109 # It's not a tie game, so check if there are 3 in a row on the board, 110 # in a search that is similar to wordSearch: 111 else: 112 directions = [ (0, 1), # right 113 (1, 0), # down 114 (1, 1), # right-down diagonal 115 (1, -1) # right-up diagonal 116 ] 117 for startRow in range(app.rows): 118 for startCol in range(app.cols): 119 for drow,dcol in directions: 120 winner = checkForWin(app, startRow, startCol, drow, dcol) 121 if winner != None: 122 app.gameOver = True 123 app.message = f'{winner} wins!' 124 return 125 126def checkForWin(app, startRow, startCol, drow, dcol): 127 # Check for a winner (3 in a row) starting from (startRow, startCol) and 128 # heading in the direction (drow, dcol). Return the winner if there 129 # is one, otherwise None. Also, so that we can draw the line through 130 # the winning 3-in-a-row run, store the winning cells in the order 131 # they appear in app.winningCells. 132 player = app.board[startRow][startCol] 133 if player == None: 134 return None 135 winLength = 3 136 winningCells = [ ] 137 for i in range(winLength): 138 row = startRow + i * drow 139 col = startCol + i * dcol 140 if ((row < 0) or (row >= app.rows) or 141 (col < 0) or (col >= app.cols)): 142 # we went off the board 143 return None 144 if app.board[row][col] != player: 145 return None 146 winningCells.append((row, col)) 147 app.winningCells = winningCells 148 return player 149 150def changeTurns(app): 151 # Change the turn from 'X' to 'O' or 'O' to 'X', 152 # and set the app.message as appropriate. 153 app.turn = 'O' if (app.turn == 'X') else 'X' 154 app.message = f"{app.turn}'s turn" 155 156def redrawAll(app): 157 drawLabel('Tic-Tac-Toe', 200, 20, size=16, bold=True) 158 drawLabel('Press s to save game, l to load game', 200, 40, size=14) 159 drawAppMessage(app) 160 drawBoard(app) 161 drawWinningLine(app) 162 163def drawAppMessage(app): 164 # Draw the app.message, and if the game is over, make the message red 165 # and add a note to press r to restart. 166 if app.gameOver: 167 message = app.message + ' (press r to restart)' 168 color = 'red' 169 else: 170 message = app.message 171 color = 'black' 172 drawLabel(message, 200, 60, size=14, fill=color) 173 174def drawBoard(app): 175 # first draw each cell (with single-thickness): 176 for row in range(app.rows): 177 for col in range(app.cols): 178 drawCell(app, row, col) 179 # then draw the board outline (with double-thickness): 180 x0, y0, x1, y1 = app.boardBounds 181 drawRect(x0, y0, x1-x0, y1-y0, 182 fill=None, border='black', 183 borderWidth=2*app.cellBorderWidth) 184 185def drawCell(app, row, col): 186 x0, y0, x1, y1 = getCellBounds(app, row, col) 187 color = 'cyan' if (row, col) == app.selection else None 188 drawRect(x0, y0, x1-x0, y1-y0, 189 fill=color, border='black', borderWidth=app.cellBorderWidth) 190 label = app.board[row][col] 191 if label != None: 192 cx = x0 + (x1 - x0)/2 193 cy = y0 + (y1 - y0)/2 194 drawLabel(label, cx, cy, size=24, bold=True) 195 196def drawWinningLine(app): 197 # If there is a winner, then app.winningCells will contain the 198 # cells in order, so draw a line from the center of the first cell 199 # to the center of the last cell. 200 if app.winningCells != None: 201 cx0, cy0 = getCellCenter(app, app.winningCells[0]) 202 cx1, cy1 = getCellCenter(app, app.winningCells[-1]) 203 drawLine(cx0, cy0, cx1, cy1) 204 205def getCellCenter(app, cell): 206 # Return the center of the given cell, a (row, col) tuple. 207 row, col = cell 208 x0, y0, x1, y1 = getCellBounds(app, row, col) 209 cx = (x0 + x1) / 2 210 cy = (y0 + y1) / 2 211 return cx, cy 212 213def getCellBounds(app, row, col): 214 boardX0, boardY0, boardX1, boardY1 = app.boardBounds 215 cellWidth, cellHeight = getCellSize(app) 216 x0 = boardX0 + col * cellWidth 217 y0 = boardY0 + row * cellHeight 218 x1 = x0 + cellWidth 219 y1 = y0 + cellHeight 220 return (x0, y0, x1, y1) 221 222def getCellSize(app): 223 boardX0, boardY0, boardX1, boardY1 = app.boardBounds 224 boardWidth = boardX1 - boardX0 225 boardHeight = boardY1 - boardY0 226 cellWidth = boardWidth / app.cols 227 cellHeight = boardHeight / app.rows 228 return (cellWidth, cellHeight) 229 230def getCell(app, x, y): 231 boardX0, boardY0, boardX1, boardY1 = app.boardBounds 232 dx = x - boardX0 233 dy = y - boardY0 234 cellWidth, cellHeight = getCellSize(app) 235 row = math.floor(dy / cellHeight) 236 col = math.floor(dx / cellWidth) 237 if (0 <= row < app.rows) and (0 <= col < app.cols): 238 return (row, col) 239 else: 240 return None 241 242def main(): 243 runApp() 244 245main()