Class Notes: Event-Based Animations in Tkinter
Part 3: Animations and Lists


  1. Example: Adding and Deleting Shapes
  2. Example: Snake
  3. Snake and MVC


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. As with Tkinter graphics, the examples here will not run using Brython in your browser.

  1. Example: Adding and Deleting Shapes
  2.  
    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', fill='black')
        canvas.create_text(app.width/2, 40,
                           text='Mouse clicks create circles', fill='black')
        canvas.create_text(app.width/2, 60,
                           text='Pressing "d" deletes circles', fill='black')
    
    runApp(width=400, height=400)

  3. Example: Snake
  4. 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', outline='black')
    
    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', fill='black')
            canvas.create_text(app.width/2, app.height/2+40,
                               text='Press r to restart!',
                               font='Arial 26 bold', fill='black')
    
    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', fill='black')
        else:
            drawBoard(app, canvas)
            drawSnake(app, canvas)
            drawFood(app, canvas)
            drawGameOver(app, canvas)
    
    runApp(width=400, height=400)

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