CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Event-Based Animations in Tkinter
Part 2: Case Studies


  1. Example: Grids (with modelToView and viewToModel)
  2. Optional Example: Pong!

  1. Example: Grids (with modelToView and viewToModel)
    from cmu_112_graphics import * def appStarted(app): app.rows = 4 app.cols = 8 app.margin = 5 # margin around grid app.selection = (-1, -1) # (row, col) of selection, (-1,-1) for none def pointInGrid(app, x, y): # return True if (x, y) is inside the grid defined by app. return ((app.margin <= x <= app.width-app.margin) and (app.margin <= y <= app.height-app.margin)) def getCell(app, x, y): # aka "viewToModel" # return (row, col) in which (x, y) occurred or (-1, -1) if outside grid. if (not pointInGrid(app, x, y)): return (-1, -1) gridWidth = app.width - 2*app.margin gridHeight = app.height - 2*app.margin cellWidth = gridWidth / app.cols cellHeight = gridHeight / app.rows # Note: we have to use int() here and not just // because # row and col cannot be floats and if any of x, y, app.margin, # cellWidth or cellHeight are floats, // would still produce floats. row = int((y - app.margin) / cellHeight) col = int((x - app.margin) / cellWidth) return (row, col) def getCellBounds(app, row, col): # aka "modelToView" # returns (x0, y0, x1, y1) corners/bounding box of given cell in grid gridWidth = app.width - 2*app.margin gridHeight = app.height - 2*app.margin cellWidth = gridWidth / app.cols cellHeight = gridHeight / app.rows x0 = app.margin + col * cellWidth x1 = app.margin + (col+1) * cellWidth y0 = app.margin + row * cellHeight y1 = app.margin + (row+1) * cellHeight return (x0, y0, x1, y1) def mousePressed(app, event): (row, col) = getCell(app, event.x, event.y) # select this (row, col) unless it is selected if (app.selection == (row, col)): app.selection = (-1, -1) else: app.selection = (row, col) def redrawAll(app, canvas): # draw grid of cells for row in range(app.rows): for col in range(app.cols): (x0, y0, x1, y1) = getCellBounds(app, row, col) fill = "orange" if (app.selection == (row, col)) else "cyan" canvas.create_rectangle(x0, y0, x1, y1, fill=fill) canvas.create_text(app.width/2, app.height/2 - 15, text="Click in cells!", font="Arial 26 bold", fill="darkBlue") runApp(width=400, height=400)

  2. Optional Example: Pong!
    # 112_pong.py # This is a simplified version of Pong, one of the earliest # arcade games. We have kept it simple for learning purposes. from cmu_112_graphics import * def appStarted(app): # This is a Controller app.waitingForKeyPress = True resetApp(app) def resetApp(app): # This is a helper function for Controllers # This initializes most of our model (stored in app.xyz) # This is called when they start the app, and also after # the game is over when we restart the app. app.timerDelay = 50 # milliseconds app.dotsLeft = 2 app.score = 0 app.paddleX0 = 20 app.paddleX1 = 40 app.paddleY0 = 20 app.paddleY1 = 80 app.margin = 5 app.paddleSpeed = 10 app.dotR = 15 app.gameOver = False app.paused = False resetDot(app) def resetDot(app): # This is a helper function for Controllers # Get the dot ready for the next round. Move the dot to # the center of the screen and give it an initial velocity. app.dotCx = app.width//2 app.dotCy = app.height//2 app.dotDx = -10 app.dotDy = -3 def movePaddleDown(app): # This is a helper function for Controllers # Move the paddle down while keeping it inside the play area dy = min(app.paddleSpeed, app.height - app.margin - app.paddleY1) app.paddleY0 += dy app.paddleY1 += dy def movePaddleUp(app): # This is a helper function for Controllers # Move the paddle up while keeping it inside the play area dy = min(app.paddleSpeed, app.paddleY0 - app.margin) app.paddleY0 -= dy app.paddleY1 -= dy def keyPressed(app, event): # This is a Controller if app.gameOver: resetApp(app) elif app.waitingForKeyPress: app.waitingForKeyPress = False app.dotsLeft -= 1 elif (event.key == 'Down'): movePaddleDown(app) elif (event.key == 'Up'): movePaddleUp(app) elif (event.key == 'p'): app.paused = not app.paused elif (event.key == 's') and app.paused: doStep(app) def timerFired(app): # This is a Controller if (not app.paused): doStep(app) def doStep(app): # This is a helper function for Controllers # The dot should move only when we are not waiting for # a key press or in the game-over state if not app.waitingForKeyPress and not app.gameOver: moveDot(app) def dotWentOffLeftSide(app): # This is a helper function for Controllers # Called when the dot went off the left side of the screen, # so the round is over. If there are no dots left, then # the game is over. if app.dotsLeft == 0: app.gameOver = True else: app.waitingForKeyPress = True resetDot(app) def dotIntersectsPaddle(app): # This is a helper function for Controllers # Check if the dot intersects the paddle. To keep this # simple here, we will only test that the center of the dot # is inside the paddle. We could be more precise here # (that's an interesting exercise!). return ((app.paddleX0 <= app.dotCx <= app.paddleX1) and (app.paddleY0 <= app.dotCy <= app.paddleY1)) def moveDot(app): # This is a helper function for Controllers # Move the dot by the current velocity (dotDx and dotDy). # Then handle all the special cases: # * bounce the dot if it went off the top, right, or bottom # * bounce the dot if it went off the paddle # * lose the round (or the game) if it went off the left side app.dotCx += app.dotDx app.dotCy += app.dotDy if (app.dotCy + app.dotR >= app.height): # The dot went off the bottom! app.dotCy = app.height - app.dotR app.dotDy = -app.dotDy elif (app.dotCy - app.dotR <= 0): # The dot went off the top! app.dotCy = app.dotR app.dotDy = -app.dotDy if (app.dotCx + app.dotR >= app.width): # The dot went off the right! app.dotCx = app.width - app.dotR app.dotDx = -app.dotDx elif dotIntersectsPaddle(app): # The dot hit the paddle! app.score += 1 # hurray! app.dotDx = -app.dotDx app.dotCx = app.paddleX1 dToMiddleY = app.dotCy - (app.paddleY0 + app.paddleY1)/2 dampeningFactor = 3 # smaller = more extreme bounces app.dotDy = dToMiddleY / dampeningFactor elif (app.dotCx - app.dotR <= 0): # The dot went off the left side dotWentOffLeftSide(app) def drawAppInfo(app, canvas): # This is a helper function for the View # This draws the title, the score, and the dots left font = 'Arial 18 bold' title ='112 Pong!' canvas.create_text(app.width/2, 20, text=title, font=font, fill='black') canvas.create_text(app.width-70, 20, text=f'Score: {app.score}', font=font, fill='black') canvas.create_text(app.width-70, app.height-20, text=f'Dots Left: {app.dotsLeft}', font=font, fill='black') def drawPaddle(app, canvas): # This is a helper function for the View canvas.create_rectangle(app.paddleX0, app.paddleY0, app.paddleX1, app.paddleY1, fill='black') def drawDot(app, canvas): # This is a helper function for the View cx, cy, r = app.dotCx, app.dotCy, app.dotR canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='black') def drawGameOver(app, canvas): # This is a helper function for the View canvas.create_text(app.width/2, app.height/2, text='Game Over!', font='Arial 18 bold', fill='black') canvas.create_text(app.width/2, app.height/2 + 50, text='Press any key to restart', font='Arial 16 bold', fill='black') def drawPressAnyKey(app, canvas): # This is a helper function for the View canvas.create_text(app.width/2, app.height/2, text='Press any key to start!', font='Arial 18 bold', fill='black') def redrawAll(app, canvas): # This is the View drawAppInfo(app, canvas) drawPaddle(app, canvas) if app.gameOver: drawGameOver(app, canvas) elif app.waitingForKeyPress: drawPressAnyKey(app, canvas) else: drawDot(app, canvas) def main(): # This runs the app runApp(width=400, height=300) if __name__ == '__main__': main()