CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Event-Based Animations in Tkinter
Part 1: Getting Started with MVC


  1. Our First Example: A KeyPress Counter
  2. Model-View-Controller (MVC)
  3. Legal event.key values
  4. Moving a Dot with Key Presses
    1. Moving a Dot with Arrows
    2. Moving a Dot with Arrows and Bounds
    3. Moving a Dot with Arrows and Wraparound
    4. Moving a Dot in Two Dimensions
  5. Moving a Dot with Mouse Presses
  6. Moving a Dot with a Timer
  7. MVC Violations
  8. Example: Adding and Deleting Shapes
  9. Example: Grids (with modelToView and viewToModel)
  10. Example: Bouncing Square
  11. Example: Snake
  12. Snake and MVC
  13. Example: Letter Displayer
  14. Example: Sidescrollers 1-3

Notes:
  1. To run these examples, first download cmu_112_graphics.py and be sure it is in the same folder as the file you are running.
  2. Important: This library requires that you install two other libraries as well: Pillow and Requests.
  3. As with Tkinter graphics, the examples here will not run using Brython in your browser.

  1. Our First Example: A KeyPress Counter
    from cmu_112_graphics import * def appStarted(app): app.counter = 0 def keyPressed(app, event): app.counter += 1 def redrawAll(app, canvas): canvas.create_text(app.width/2, app.height/2, text=f'{app.counter} keypresses', font='Arial 30 bold') runApp(width=400, height=400)

  2. Model-View-Controller (MVC)
    Note:
    1. We will write animations using the Model-View-Controller (MVC) paradigm.
    2. The model contains all the data we need for the animation. We can store the model in the app object's attributes.
      • In the example above, app.counter is our model.
    3. The view draws the app using the values in the model.
      • In the example above, redrawAll is our view.
    4. The controller responds to keyboard, mouse, timer and other events and updates the model.
      • In the example above, keyPressed is our controller.
    And...
    1. You never call the view or the controllers. The animation framework calls these for you.
      • In the example above, we never call redrawAll or keyPressed. They are called for us.
    2. Controllers can only update the model, they cannot update the view.
      • In the example above, keyPressed cannot call redrawAll.
    3. The view can never update the model.
      • In the example above, redrawAll cannot change app.counter or any other values in the model.
    4. If you violate these rules, it is called an MVC Violation. If that happens, your code will stop running and will display the runtime error for you.

  3. Legal event.key values
    # Note: Tkinter uses event.keysym for some keys, and event.char # for others, and it can be confusing how to use these properly. # Instead, cmu_112_graphics replaces both of these with event.key, # which simply works as expected in all cases. from cmu_112_graphics import * def appStarted(app): app.message = 'Press any key' def keyPressed(app, event): app.message = f"event.key == '{event.key}'" def redrawAll(app, canvas): canvas.create_text(app.width/2, 40, text=app.message, font='Arial 30 bold') keyNamesText = '''Here are the legal event.key names: * Keyboard key labels (letters, digits, punctuation) * Arrow directions ('Up', 'Down', 'Left', 'Right') * Whitespace ('Space', 'Enter', 'Tab', 'Backspace') * Other commands ('Delete', 'Escape')''' y = 80 for line in keyNamesText.splitlines(): canvas.create_text(app.width/2, y, text=line.strip(), font='Arial 20') y += 30 runApp(width=600, height=400)

  4. Moving a Dot with Key Presses

    1. Moving a Dot with Arrows
      from cmu_112_graphics import * def appStarted(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def keyPressed(app, event): if (event.key == 'Left'): app.cx -= 10 elif (event.key == 'Right'): app.cx += 10 def redrawAll(app, canvas): canvas.create_text(app.width/2, 20, text='Move dot with left and right arrows') canvas.create_oval(app.cx-app.r, app.cy-app.r, app.cx+app.r, app.cy+app.r, fill='darkGreen') runApp(width=400, height=400)

    2. Moving a Dot with Arrows and Bounds
      # This version bounds the dot to remain entirely on the canvas from cmu_112_graphics import * def appStarted(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def keyPressed(app, event): if (event.key == 'Left'): app.cx -= 10 if (app.cx - app.r < 0): app.cx = app.r elif (event.key == 'Right'): app.cx += 10 if (app.cx + app.r > app.width): app.cx = app.width - app.r def redrawAll(app, canvas): canvas.create_text(app.width/2, 20, text='Move dot with left and right arrows') canvas.create_text(app.width/2, 40, text='See how it is bounded by the canvas edges') canvas.create_oval(app.cx-app.r, app.cy-app.r, app.cx+app.r, app.cy+app.r, fill='darkGreen') runApp(width=400, height=400)

    3. Moving a Dot with Arrows and Wraparound
      # This version wraps around, so leaving one side enters the opposite side from cmu_112_graphics import * def appStarted(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def keyPressed(app, event): if (event.key == 'Left'): app.cx -= 10 if (app.cx + app.r <= 0): app.cx = app.width + app.r elif (event.key == 'Right'): app.cx += 10 if (app.cx - app.r >= app.width): app.cx = 0 - app.r def redrawAll(app, canvas): canvas.create_text(app.width/2, 20, text='Move dot with left and right arrows') canvas.create_text(app.width/2, 40, text='See how it uses wraparound on the edges') canvas.create_oval(app.cx-app.r, app.cy-app.r, app.cx+app.r, app.cy+app.r, fill='darkGreen') runApp(width=400, height=400)

    4. Moving a Dot in Two Dimensions
      # This version moves in both x and y dimensions. from cmu_112_graphics import * def appStarted(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def keyPressed(app, event): if (event.key == 'Left'): app.cx -= 10 elif (event.key == 'Right'): app.cx += 10 elif (event.key == 'Up'): app.cy -= 10 elif (event.key == 'Down'): app.cy += 10 def redrawAll(app, canvas): canvas.create_text(app.width/2, 20, text='Move dot with up, down, left, and right arrows') canvas.create_oval(app.cx-app.r, app.cy-app.r, app.cx+app.r, app.cy+app.r, fill='darkGreen') runApp(width=400, height=400)

  5. Moving a Dot with Mouse Presses
    from cmu_112_graphics import * def appStarted(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def mousePressed(app, event): app.cx = event.x app.cy = event.y def redrawAll(app, canvas): canvas.create_text(app.width/2, 20, text='Move dot with mouse presses') canvas.create_oval(app.cx-app.r, app.cy-app.r, app.cx+app.r, app.cy+app.r, fill='darkGreen') runApp(width=400, height=400)

  6. Moving a Dot with a Timer
    from cmu_112_graphics import * def appStarted(app): app.cx = app.width/2 app.cy = app.height/2 app.r = 40 def timerFired(app): app.cx -= 10 if (app.cx + app.r <= 0): app.cx = app.width + app.r def redrawAll(app, canvas): canvas.create_text(app.width/2, 20, text='Watch the dot move!') canvas.create_oval(app.cx-app.r, app.cy-app.r, app.cx+app.r, app.cy+app.r, fill='darkGreen') runApp(width=400, height=400)

  7. MVC Violations

    1. Cannot change the model while drawing the view
      from cmu_112_graphics import * def appStarted(app): app.x = 0 def redrawAll(app, canvas): canvas.create_text(app.width/2, 20, text='This has an MVC Violation!') app.x = 10 # This is an MVC Violation! # We cannot change the model from the view (redrawAll) runApp(width=400, height=400)

    2. Once again, but with a mutable value (such as a list)
      # Since this version modifies a mutable value in the model, # the exception does not occur immediately on the line of the change, # but only after redrawAll has entirely finished. from cmu_112_graphics import * def appStarted(app): app.L = [ ] def redrawAll(app, canvas): canvas.create_text(app.width/2, 20, text='This also has an MVC Violation!') app.L.append(42) # This is an MVC Violation! # We cannot change the model from the view (redrawAll) runApp(width=400, height=400)

  8. Example: Adding and Deleting Shapes
    • With non-OOPy Circles
      from cmu_112_graphics import * def appStarted(app): app.circleCenters = [ ] def mousePressed(app, event): newCircleCenter = (event.x, event.y) app.circleCenters.append(newCircleCenter) def keyPressed(app, event): if (event.key == 'd'): if (len(app.circleCenters) > 0): app.circleCenters.pop(0) else: print('No more circles to delete!') def redrawAll(app, canvas): # draw the circles for circleCenter in app.circleCenters: (cx, cy) = circleCenter r = 20 canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='cyan') # draw the text canvas.create_text(app.width/2, 20, text='Example: Adding and Deleting Shapes') canvas.create_text(app.width/2, 40, text='Mouse clicks create circles') canvas.create_text(app.width/2, 60, text='Pressing "d" deletes circles') runApp(width=400, height=400)

    • With OOPy Circles (Dots)
      from cmu_112_graphics import * import random class Dot(object): def __init__(self, cx, cy): self.cx = cx self.cy = cy # let's add random sizes and colors, too # (since it's so easy to store these with each Dot instance) colors = ['red', 'orange', 'yellow', 'green', 'blue'] self.fill = random.choice(colors) self.r = random.randint(5, 40) def appStarted(app): app.dots = [ ] def mousePressed(app, event): newDot = Dot(event.x, event.y) app.dots.append(newDot) def keyPressed(app, event): if (event.key == 'd'): if (len(app.dots) > 0): app.dots.pop(0) else: print('No more circles to delete!') def redrawAll(app, canvas): # draw the circles for dot in app.dots: canvas.create_oval(dot.cx-dot.r, dot.cy-dot.r, dot.cx+dot.r, dot.cy+dot.r, fill=dot.fill) # draw the text canvas.create_text(app.width/2, 20, text='Example: Adding and Deleting Shapes') canvas.create_text(app.width/2, 40, text='Mouse clicks create circles') canvas.create_text(app.width/2, 60, text='Pressing "d" deletes circles') runApp(width=400, height=400)

  9. 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 columnWidth = gridWidth / app.cols rowHeight = gridHeight / app.rows x0 = app.margin + col * columnWidth x1 = app.margin + (col+1) * columnWidth y0 = app.margin + row * rowHeight y1 = app.margin + (row+1) * rowHeight 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)

  10. Example: Bouncing Square
    from cmu_112_graphics import * def appStarted(app): app.squareLeft = app.width//2 app.squareTop = app.height//2 app.squareSize = 25 app.dx = 10 app.dy = 15 app.isPaused = False app.timerDelay = 50 # milliseconds def keyPressed(app, event): if (event.key == "p"): app.isPaused = not app.isPaused elif (event.key == "s"): doStep(app) def timerFired(app): if (not app.isPaused): doStep(app) def doStep(app): # Move horizontally app.squareLeft += app.dx # Check if the square has gone out of bounds, and if so, reverse # direction, but also move the square right to the edge (instead of # past it). Note: there are other, more sophisticated ways to # handle the case where the square extends beyond the edges... if app.squareLeft < 0: # if so, reverse! app.squareLeft = 0 app.dx = -app.dx elif app.squareLeft > app.width - app.squareSize: app.squareLeft = app.width - app.squareSize app.dx = -app.dx # Move vertically the same way app.squareTop += app.dy if app.squareTop < 0: # if so, reverse! app.squareTop = 0 app.dy = -app.dy elif app.squareTop > app.height - app.squareSize: app.squareTop = app.height - app.squareSize app.dy = -app.dy def redrawAll(app, canvas): # draw the square canvas.create_rectangle(app.squareLeft, app.squareTop, app.squareLeft + app.squareSize, app.squareTop + app.squareSize, fill="yellow") # draw the text canvas.create_text(app.width/2, 20, text="Pressing 'p' pauses/unpauses timer") canvas.create_text(app.width/2, 40, text="Pressing 's' steps the timer once") runApp(width=400, height=150)

  11. Example: Snake
    Here is a 4-part video explaining how to write this version of Snake:
    1. Draw the board and the Snake
    2. Add motion and gameOver
    3. Add food and self-collision
    4. Add the timer and finish the game
    from cmu_112_graphics import * import random def appStarted(app): app.rows = 10 app.cols = 10 app.margin = 5 # margin around grid app.timerDelay = 250 initSnakeAndFood(app) app.waitingForFirstKeyPress = True def initSnakeAndFood(app): app.snake = [(0,0)] app.direction = (0, +1) # (drow, dcol) placeFood(app) app.gameOver = False # getCellBounds from grid-demo.py 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 x0 = app.margin + gridWidth * col / app.cols x1 = app.margin + gridWidth * (col+1) / app.cols y0 = app.margin + gridHeight * row / app.rows y1 = app.margin + gridHeight * (row+1) / app.rows return (x0, y0, x1, y1) def keyPressed(app, event): if (app.waitingForFirstKeyPress): app.waitingForFirstKeyPress = False elif (event.key == 'r'): initSnakeAndFood(app) elif app.gameOver: return elif (event.key == 'Up'): app.direction = (-1, 0) elif (event.key == 'Down'): app.direction = (+1, 0) elif (event.key == 'Left'): app.direction = (0, -1) elif (event.key == 'Right'): app.direction = (0, +1) # elif (event.key == 's'): # this was only here for debugging, before we turned on the timer # takeStep(app) def timerFired(app): if app.gameOver or app.waitingForFirstKeyPress: return takeStep(app) def takeStep(app): (drow, dcol) = app.direction (headRow, headCol) = app.snake[0] (newRow, newCol) = (headRow+drow, headCol+dcol) if ((newRow < 0) or (newRow >= app.rows) or (newCol < 0) or (newCol >= app.cols) or ((newRow, newCol) in app.snake)): app.gameOver = True else: app.snake.insert(0, (newRow, newCol)) if (app.foodPosition == (newRow, newCol)): placeFood(app) else: # didn't eat, so remove old tail (slither forward) app.snake.pop() def placeFood(app): # Keep trying random positions until we find one that is not in # the snake. Note: there are more sophisticated ways to do this. while True: row = random.randint(0, app.rows-1) col = random.randint(0, app.cols-1) if (row,col) not in app.snake: app.foodPosition = (row, col) return def drawBoard(app, canvas): for row in range(app.rows): for col in range(app.cols): (x0, y0, x1, y1) = getCellBounds(app, row, col) canvas.create_rectangle(x0, y0, x1, y1, fill='white') def drawSnake(app, canvas): for (row, col) in app.snake: (x0, y0, x1, y1) = getCellBounds(app, row, col) canvas.create_oval(x0, y0, x1, y1, fill='blue') def drawFood(app, canvas): if (app.foodPosition != None): (row, col) = app.foodPosition (x0, y0, x1, y1) = getCellBounds(app, row, col) canvas.create_oval(x0, y0, x1, y1, fill='green') def drawGameOver(app, canvas): if (app.gameOver): canvas.create_text(app.width/2, app.height/2, text='Game over!', font='Arial 26 bold') canvas.create_text(app.width/2, app.height/2+40, text='Press r to restart!', font='Arial 26 bold') def redrawAll(app, canvas): if (app.waitingForFirstKeyPress): canvas.create_text(app.width/2, app.height/2, text='Press any key to start!', font='Arial 26 bold') else: drawBoard(app, canvas) drawSnake(app, canvas) drawFood(app, canvas) drawGameOver(app, canvas) runApp(width=400, height=400)

  12. Snake and MVC
    Model View Controller
    app.rows redrawAll() keyPressed()
    app.cols drawGameOver() timerFired()
    app.margin drawFood() takeStep()
    app.waitingForFirstKeyPress drawSnake() placeFood()
    app.snake drawBoard()
    app.direction
    app.foodPosition
    + all game state + all drawing functions + all event-triggered actions

  13. Example: Letter Displayer
    """ A program to display a message one letter at a time. """ from cmu_112_graphics import * def appStarted(app): # The current thing the game is doing. Can be: # start: Waiting at the start screen # showLetter: Show a letter # showNothing: Show nothing app.gameMode = "start" # The message I will display app.msg = "Hi112" # The current letter to show app.curLetter = 0 # A countdown timer I'll use to setup how long each letter runs # For now it is 0, because I don't need it yet. app.letterTimer = 0 # The number of timerFireds to spend on each letter. # 20 timerFireds is 2 seconds. We'll spend 1 second showing a letter, and # 1 second showing nothing before moving on to the next letter. app.letterDuration = 20 def timerFired(app): # If we are currently showing something, then we have things to do if (app.gameMode == "showLetter" or app.gameMode == "showNothing") and app.letterTimer > 0: app.letterTimer -= 1 # Are we half-way done with our time? Then switch to showing nothing if app.letterTimer == app.letterDuration//2: app.gameMode = "showNothing" return # Is our timer up, but there are still more letters? Change the letter, # reset the timer, and go back to showLetter mode if app.letterTimer == 0 and app.curLetter < len(app.msg)-1: app.gameMode = "showLetter" app.curLetter += 1 app.letterTimer = app.letterDuration return # Is our timer up, there are not more letters? Go back to start. if app.letterTimer == 0 and app.curLetter >= len(app.msg)-1: app.gameMode = "start" return def keyPressed(app, event): # If the user presses space bar, we start up the letter showing procedure # by setting the game mode, the letter to start, and the duration for # letters to show. Setting these variables will automatically start # displaying them because of the code in timerFired and redrawAll. if app.gameMode == "start" and event.key == "Space": app.gameMode = "showLetter" app.curLetter = 0 app.letterTimer = app.letterDuration def redrawAll(app, canvas): # Are we in start mode? Just display the start message if app.gameMode == "start": canvas.create_text(app.width/2,app.height/2,text="Press space bar to start") # Are we supposed to show a letter? Do that. elif app.gameMode == "showLetter": letter = app.msg[app.curLetter] canvas.create_text(app.width/2,app.height/2,text=f"{letter}", font=f"Arial {app.height//4}") # If gameMode == "showNothing", we get here. Since we want to show nothing, # we aren't doing anything. runApp(width=400, height=400)

  14. Example: Sidescrollers 1-3
    • SideScroller1
      # SideScroller1: from cmu_112_graphics import * import random def appStarted(app): app.scrollX = 0 app.dots = [(random.randrange(app.width), random.randrange(60, app.height)) for _ in range(50)] def keyPressed(app, event): if (event.key == "Left"): app.scrollX -= 5 elif (event.key == "Right"): app.scrollX += 5 def redrawAll(app, canvas): # draw the player fixed to the center of the scrolled canvas cx, cy, r = app.width/2, app.height/2, 10 canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='cyan') # draw the dots, shifted by the scrollX offset for (cx, cy) in app.dots: cx -= app.scrollX # <-- This is where we scroll each dot!!! canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='lightGreen') # draw the x and y axes x = app.width/2 - app.scrollX # <-- This is where we scroll the axis! y = app.height/2 canvas.create_line(x, 0, x, app.height) canvas.create_line(0, y, app.width, y) # draw the instructions and the current scrollX x = app.width/2 canvas.create_text(x, 20, text='Use arrows to move left or right', fill='black') canvas.create_text(x, 40, text=f'app.scrollX = {app.scrollX}', fill='black') runApp(width=300, height=300)

    • SideScroller2
      # SideScroller2: # Now with a scroll margin, so player does not stay fixed # at the center of the scrolled canvas, and we only scroll # if the player's center (in this case) gets closer than the # margin to the left or right edge of the canvas. from cmu_112_graphics import * import random def appStarted(app): app.scrollX = 0 app.scrollMargin = 50 app.playerX = app.width//2 # player's center app.dots = [(random.randrange(app.width), random.randrange(60, app.height)) for _ in range(50)] def makePlayerVisible(app): # scroll to make player visible as needed if (app.playerX < app.scrollX + app.scrollMargin): app.scrollX = app.playerX - app.scrollMargin if (app.playerX > app.scrollX + app.width - app.scrollMargin): app.scrollX = app.playerX - app.width + app.scrollMargin def movePlayer(app, dx, dy): app.playerX += dx makePlayerVisible(app) def keyPressed(app, event): if (event.key == "Left"): movePlayer(app, -5, 0) elif (event.key == "Right"): movePlayer(app, +5, 0) def redrawAll(app, canvas): # draw the player, shifted by the scrollX offset cx, cy, r = app.playerX, app.height/2, 10 cx -= app.scrollX # <-- This is where we scroll the player!!! canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='cyan') # draw the dots, shifted by the scrollX offset for (cx, cy) in app.dots: cx -= app.scrollX # <-- This is where we scroll each dot!!! canvas.create_oval(cx-r, cy-r, cx+r, cy+r, fill='lightGreen') # draw the x and y axes x = app.width/2 - app.scrollX # <-- This is where we scroll the axis! y = app.height/2 canvas.create_line(x, 0, x, app.height) canvas.create_line(0, y, app.width, y) # draw the instructions and the current scrollX x = app.width/2 canvas.create_text(x, 20, text='Use arrows to move left or right', fill='black') canvas.create_text(x, 40, text=f'app.scrollX = {app.scrollX}', fill='black') runApp(width=300, height=300)

    • SideScroller3
      # SideScroller3: # Now with walls that track when you run into them (but # ignore while you are still crossing them). from cmu_112_graphics import * def appStarted(app): app.scrollX = 0 app.scrollMargin = 50 app.playerX = app.scrollMargin app.playerY = 0 app.playerWidth = 10 app.playerHeight = 20 app.walls = 5 app.wallPoints = [0]*app.walls app.wallWidth = 20 app.wallHeight = 40 app.wallSpacing = 90 # wall left edges are at 90, 180, 270,... app.currentWallHit = -1 # start out not hitting a wall def getPlayerBounds(app): # returns absolute bounds, not taking scrollX into account (x0, y1) = (app.playerX, app.height/2 - app.playerY) (x1, y0) = (x0 + app.playerWidth, y1 - app.playerHeight) return (x0, y0, x1, y1) def getWallBounds(app, wall): # returns absolute bounds, not taking scrollX into account (x0, y1) = ((1+wall) * app.wallSpacing, app.height/2) (x1, y0) = (x0 + app.wallWidth, y1 - app.wallHeight) return (x0, y0, x1, y1) def getWallHit(app): # return wall that player is currently hitting # note: this should be optimized to only check the walls that are visible # or even just directly compute the wall without a loop playerBounds = getPlayerBounds(app) for wall in range(app.walls): wallBounds = getWallBounds(app, wall) if (boundsIntersect(app, playerBounds, wallBounds) == True): return wall return -1 def boundsIntersect(app, boundsA, boundsB): # return l2<=r1 and t2<=b1 and l1<=r2 and t1<=b2 (ax0, ay0, ax1, ay1) = boundsA (bx0, by0, bx1, by1) = boundsB return ((ax1 >= bx0) and (bx1 >= ax0) and (ay1 >= by0) and (by1 >= ay0)) def checkForNewWallHit(app): # check if we are hitting a new wall for the first time wall = getWallHit(app) if (wall != app.currentWallHit): app.currentWallHit = wall if (wall >= 0): app.wallPoints[wall] += 1 def makePlayerVisible(app): # scroll to make player visible as needed if (app.playerX < app.scrollX + app.scrollMargin): app.scrollX = app.playerX - app.scrollMargin if (app.playerX > app.scrollX + app.width - app.scrollMargin): app.scrollX = app.playerX - app.width + app.scrollMargin def movePlayer(app, dx, dy): app.playerX += dx app.playerY += dy makePlayerVisible(app) checkForNewWallHit(app) def sizeChanged(app): makePlayerVisible(app) def mousePressed(app, event): app.playerX = event.x + app.scrollX checkForNewWallHit(app) def keyPressed(app, event): if (event.key == "Left"): movePlayer(app, -5, 0) elif (event.key == "Right"): movePlayer(app, +5, 0) elif (event.key == "Up"): movePlayer(app, 0, +5) elif (event.key == "Down"): movePlayer(app, 0, -5) def redrawAll(app, canvas): # draw the base line lineY = app.height/2 lineHeight = 5 canvas.create_rectangle(0, lineY, app.width, lineY+lineHeight,fill="black") # draw the walls # (Note: should optimize to only consider walls that can be visible now!) sx = app.scrollX for wall in range(app.walls): (x0, y0, x1, y1) = getWallBounds(app, wall) fill = "orange" if (wall == app.currentWallHit) else "pink" canvas.create_rectangle(x0-sx, y0, x1-sx, y1, fill=fill) (cx, cy) = ((x0+x1)/2 - sx, (y0 + y1)/2) canvas.create_text(cx, cy, text=str(app.wallPoints[wall]), fill='black') cy = lineY + 5 canvas.create_text(cx, cy, text=str(wall), anchor=N, fill='black') # draw the player (x0, y0, x1, y1) = getPlayerBounds(app) canvas.create_oval(x0 - sx, y0, x1 - sx, y1, fill="cyan") # draw the instructions msg = "Use arrows to move, hit walls to score" canvas.create_text(app.width/2, 20, text=msg, fill='black') runApp(width=300, height=300)