CMU Coding Bootcamp
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()