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!

Note: The videos on this page use the lecture1/2 animation framework. Though the syntax of their animation framework is different, the explanations in the videos will still be helpful to you.
  1. Example: Grids (with modelToView and viewToModel)
    from cmu_graphics import * def onAppStart(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 (left, top, width, height) 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 left = app.margin + col * cellWidth top = app.margin + (row) * cellHeight return (left, top, cellWidth, cellHeight) def onMousePress(app, mouseX, mouseY): (row, col) = getCell(app, mouseX, mouseY) # 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): # draw grid of cells for row in range(app.rows): for col in range(app.cols): (left, top, width, height) = getCellBounds(app, row, col) fill = "orange" if (app.selection == (row, col)) else "cyan" drawRect(left, top, width, height, fill=fill, borderWidth=1, border='black') drawLabel('Click in cells!', app.width / 2, app.height / 2 - 15, fill='darkBlue', size=26, bold=True) 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_graphics import * def onAppStart(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.dotsLeft = 2 app.score = 0 app.paddleX = 20 app.paddleY = 20 app.paddleWidth = 20 app.paddleHeight = 60 app.margin = 5 app.paddleSpeed = 20 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 = -7 app.dotDy = -2 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.paddleY + app.paddleHeight)) app.paddleY += 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.paddleY - app.margin) app.paddleY -= dy def onKeyPress(app, key): # This is a Controller if app.gameOver: resetApp(app) elif app.waitingForKeyPress: app.waitingForKeyPress = False app.dotsLeft -= 1 elif (key == 'down'): movePaddleDown(app) elif (key == 'up'): movePaddleUp(app) elif (key == 'p'): app.paused = not app.paused elif (key == 's') and app.paused: doStep(app) def onStep(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.paddleX <= app.dotCx <= app.paddleX + app.paddleWidth) and (app.paddleY <= app.dotCy <= app.paddleY + app.paddleHeight)) 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.paddleX + app.paddleWidth dToMiddleY = app.dotCy - (app.paddleY + (app.paddleWidth / 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): # This is a helper function for the View # This draws the title, the score, and the dots left title ='112 Pong!' drawLabel(title, app.width/2, 20, size=18, bold=True) drawLabel(f'Score: {app.score}', app.width - 70, 20, size=18, bold=True) drawLabel(f'Dots Left: {app.dotsLeft}', app.width - 70, app.height - 20, size=18, bold=True) def drawPaddle(app): # This is a helper function for the View drawRect(app.paddleX, app.paddleY, app.paddleWidth, app.paddleHeight) def drawDot(app): # This is a helper function for the View cx, cy, r = app.dotCx, app.dotCy, app.dotR drawCircle(cx, cy, r) def drawGameOver(app): # This is a helper function for the View drawLabel('Game Over!', app.width / 2, app.height / 2, size=18, bold=True) drawLabel('Press any key to restart', app.width / 2, app.height / 2 + 50, size=16, bold=True) def drawPressAnyKey(app): # This is a helper function for the View drawLabel('Press any key to start!', app.width / 2, app.height / 2, size=18, bold=True) def redrawAll(app): # This is the View drawAppInfo(app) drawPaddle(app) if app.gameOver: drawGameOver(app) elif app.waitingForKeyPress: drawPressAnyKey(app) else: drawDot(app) def main(): # This runs the app runApp(width=400, height=300) main()