CMU 15-112: Fundamentals of Programming and Computer Science
Class Notes: Animations: Worked Examples


  1. Mode Demo
  2. Grid Demo
  3. Undo/Redo Demo
  4. Images Demo
  5. Snake Demo
  6. Side Scroller Demo
  7. Tetris

  1. Mode Demo
    # mode-demo.py from tkinter import * #################################### # init #################################### def init(data): # There is only one init, not one-per-mode data.mode = "splashScreen" data.score = 0 #################################### # mode dispatcher #################################### def mousePressed(event, data): if (data.mode == "splashScreen"): splashScreenMousePressed(event, data) elif (data.mode == "playGame"): playGameMousePressed(event, data) elif (data.mode == "help"): helpMousePressed(event, data) def keyPressed(event, data): if (data.mode == "splashScreen"): splashScreenKeyPressed(event, data) elif (data.mode == "playGame"): playGameKeyPressed(event, data) elif (data.mode == "help"): helpKeyPressed(event, data) def timerFired(data): if (data.mode == "splashScreen"): splashScreenTimerFired(data) elif (data.mode == "playGame"): playGameTimerFired(data) elif (data.mode == "help"): helpTimerFired(data) def redrawAll(canvas, data): if (data.mode == "splashScreen"): splashScreenRedrawAll(canvas, data) elif (data.mode == "playGame"): playGameRedrawAll(canvas, data) elif (data.mode == "help"): helpRedrawAll(canvas, data) #################################### # splashScreen mode #################################### def splashScreenMousePressed(event, data): pass def splashScreenKeyPressed(event, data): data.mode = "playGame" def splashScreenTimerFired(data): pass def splashScreenRedrawAll(canvas, data): canvas.create_text(data.width/2, data.height/2-20, text="This is a splash screen!", font="Arial 26 bold") canvas.create_text(data.width/2, data.height/2+20, text="Press any key to play!", font="Arial 20") #################################### # help mode #################################### def helpMousePressed(event, data): pass def helpKeyPressed(event, data): data.mode = "playGame" def helpTimerFired(data): pass def helpRedrawAll(canvas, data): canvas.create_text(data.width/2, data.height/2-40, text="This is help mode!", font="Arial 26 bold") canvas.create_text(data.width/2, data.height/2-10, text="How to play:", font="Arial 20") canvas.create_text(data.width/2, data.height/2+15, text="Do nothing and score points!", font="Arial 20") canvas.create_text(data.width/2, data.height/2+40, text="Press any key to keep playing!", font="Arial 20") #################################### # playGame mode #################################### def playGameMousePressed(event, data): data.score = 0 def playGameKeyPressed(event, data): if (event.keysym == 'h'): data.mode = "help" def playGameTimerFired(data): data.score += 1 def playGameRedrawAll(canvas, data): canvas.create_text(data.width/2, data.height/2-40, text="This is a fun game!", font="Arial 26 bold") canvas.create_text(data.width/2, data.height/2-10, text="Score = " + str(data.score), font="Arial 20") canvas.create_text(data.width/2, data.height/2+15, text="Click anywhere to reset score", font="Arial 20") canvas.create_text(data.width/2, data.height/2+40, text="Press 'h' for help!", font="Arial 20") #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds init(data) # create the root and the canvas root = Tk() canvas = Canvas(root, width=data.width, height=data.height) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(300, 300)

  2. Grid Demo
    # grid-demo.py from tkinter import * def init(data): data.rows = 4 data.cols = 8 data.margin = 5 # margin around grid data.selection = (-1, -1) # (row, col) of selection, (-1,-1) for none def pointInGrid(x, y, data): # return True if (x, y) is inside the grid defined by data. return ((data.margin <= x <= data.width-data.margin) and (data.margin <= y <= data.height-data.margin)) def getCell(x, y, data): # aka "viewToModel" # return (row, col) in which (x, y) occurred or (-1, -1) if outside grid. if (not pointInGrid(x, y, data)): return (-1, -1) gridWidth = data.width - 2*data.margin gridHeight = data.height - 2*data.margin cellWidth = gridWidth / data.cols cellHeight = gridHeight / data.rows row = (y - data.margin) // cellHeight col = (x - data.margin) // cellWidth # triple-check that we are in bounds row = min(data.rows-1, max(0, row)) col = min(data.cols-1, max(0, col)) return (row, col) def getCellBounds(row, col, data): # aka "modelToView" # returns (x0, y0, x1, y1) corners/bounding box of given cell in grid gridWidth = data.width - 2*data.margin gridHeight = data.height - 2*data.margin columnWidth = gridWidth / data.cols rowHeight = gridHeight / data.rows x0 = data.margin + col * columnWidth x1 = data.margin + (col+1) * columnWidth y0 = data.margin + row * rowHeight y1 = data.margin + (row+1) * rowHeight return (x0, y0, x1, y1) def mousePressed(event, data): (row, col) = getCell(event.x, event.y, data) # select this (row, col) unless it is selected if (data.selection == (row, col)): data.selection = (-1, -1) else: data.selection = (row, col) def keyPressed(event, data): pass def timerFired(data): pass def redrawAll(canvas, data): # draw grid of cells for row in range(data.rows): for col in range(data.cols): (x0, y0, x1, y1) = getCellBounds(row, col, data) fill = "orange" if (data.selection == (row, col)) else "cyan" canvas.create_rectangle(x0, y0, x1, y1, fill=fill) canvas.create_text(data.width/2, data.height/2 - 15, text="Click in cells!", font="Arial 26 bold", fill="darkBlue") #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds init(data) # create the root and the canvas root = Tk() canvas = Canvas(root, width=data.width, height=data.height) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(300, 300)

  3. Undo/Redo Demo
    # undo-redo-demo.py from tkinter import * def init(data): data.points = [ ] data.undoList = [ ] def mousePressed(event, data): data.points.append((event.x, event.y)) data.undoList = [ ] def keyPressed(event, data): if (event.keysym == "u"): if (len(data.points) > 0): data.undoList.append(data.points.pop()) elif (event.keysym == "r"): if (len(data.undoList) > 0): data.points.append(data.undoList.pop()) elif (event.keysym == "c"): data.points = [ ] data.undoList = [ ] def timerFired(data): pass def redrawAll(canvas, data): if (data.points != []): canvas.create_polygon(data.points, fill="gold", outline="black") canvas.create_text(data.width/2, 20, text="click to add point. u=undo. r=redo. c=clear.") canvas.create_text(data.width/2, 40, text=str(len(data.points)) + " point(s) in polygon") canvas.create_text(data.width/2, 60, text=str(len(data.undoList)) + " point(s) on undoList") #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds init(data) # create the root and the canvas root = Tk() canvas = Canvas(root, width=data.width, height=data.height) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(300, 300)

  4. Images Demo
    To run this demo, first download playing-card-gifs.zip and unzip that file, so the folder playing-card-gifs is at the same level as this code.
    # images-demo.py from tkinter import * def init(data): data.step = 0 loadPlayingCardImages(data) # always load images in init! def loadPlayingCardImages(data): cards = 55 # cards 1-52, back, joker1, joker2 data.cardImages = [ ] for card in range(cards): rank = (card%13)+1 suit = "cdhsx"[card//13] filename = "playing-card-gifs/%s%d.gif" % (suit, rank) data.cardImages.append(PhotoImage(file=filename)) def getPlayingCardImage(data, rank, suitName): suitName = suitName[0].lower() # only car about first letter suitNames = "cdhsx" assert(1 <= rank <= 13) assert(suitName in suitNames) suit = suitNames.index(suitName) return data.cardImages[13*suit + rank - 1] def getSpecialPlayingCardImage(data, name): specialNames = ["back", "joker1", "joker2"] return getPlayingCardImage(data, specialNames.index(name)+1, "x") def mousePressed(event, data): pass def keyPressed(event, data): pass def timerFired(data): data.step += 1 def redrawAll(canvas, data): suitNames = ["Clubs", "Diamonds", "Hearts", "Spades", "Xtras"] suit = (data.step//10) % len(suitNames) suitName = suitNames[suit] cards = 3 if (suitName == "Xtras") else 13 margin = 10 (left, top) = (margin, 40) for rank in range(1,cards+1): image = getPlayingCardImage(data, rank, suitName) if (left + image.width() > data.width): (left, top) = (margin, top + image.height() + margin) canvas.create_image(left, top, anchor=NW, image=image) left += image.width() + margin canvas.create_text(data.width/2, 20, text=suitName, font="Arial 28 bold") #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Create root before calling init (so we can create images in init) root = Tk() # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 250 # milliseconds init(data) # create the root and the canvas canvas = Canvas(root, width=data.width, height=data.height) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(420, 360)

  5. Snake Demo
    # snake-demo.py # Note: there is a snake tutorial from previous semesters here: # http://www.kosbie.net/cmu/fall-11/15-112/handouts/snake/snake.html # But this is different in two key ways: # 1) This uses this semester's framework (run function, and in Python3) # 2) This uses a list of tuples to represent the snake # You should understand both solutions, and be able to adapt that # tutorial to use this semester's framework. from tkinter import * import random def init(data): data.rows = 10 data.cols = 10 data.margin = 5 # margin around grid data.snake = [(data.rows/2, data.cols/2)] data.direction = (0, +1) # (drow, dcol) placeFood(data) data.timerDelay = 250 data.gameOver = False data.paused = True # getCellBounds from grid-demo.py def getCellBounds(row, col, data): # aka "modelToView" # returns (x0, y0, x1, y1) corners/bounding box of given cell in grid gridWidth = data.width - 2*data.margin gridHeight = data.height - 2*data.margin x0 = data.margin + gridWidth * col / data.cols x1 = data.margin + gridWidth * (col+1) / data.cols y0 = data.margin + gridHeight * row / data.rows y1 = data.margin + gridHeight * (row+1) / data.rows return (x0, y0, x1, y1) def mousePressed(event, data): data.paused = False def keyPressed(event, data): if (event.keysym == "p"): data.paused = True; return elif (event.keysym == "r"): init(data); return if (data.paused or data.gameOver): return if (event.keysym == "Up"): data.direction = (-1, 0) elif (event.keysym == "Down"): data.direction = (+1, 0) elif (event.keysym == "Left"): data.direction = (0, -1) elif (event.keysym == "Right"): data.direction = (0, +1) # for debugging, take one step on any keypress takeStep(data) def timerFired(data): if (data.paused or data.gameOver): return takeStep(data) def takeStep(data): (drow, dcol) = data.direction (headRow, headCol) = data.snake[0] (newRow, newCol) = (headRow+drow, headCol+dcol) if ((newRow < 0) or (newRow >= data.rows) or (newCol < 0) or (newCol >= data.cols) or ((newRow, newCol) in data.snake)): data.gameOver = True else: data.snake.insert(0, (newRow, newCol)) if (data.foodPosition == (newRow, newCol)): placeFood(data) else: # didn't eat, so remove old tail (slither forward) data.snake.pop() def placeFood(data): data.foodPosition = None row0 = random.randint(0, data.rows-1) col0 = random.randint(0, data.cols-1) for drow in range(data.rows): for dcol in range(data.cols): row = (row0 + drow) % data.rows col = (col0 + dcol) % data.cols if (row,col) not in data.snake: data.foodPosition = (row, col) return def drawBoard(canvas, data): for row in range(data.rows): for col in range(data.cols): (x0, y0, x1, y1) = getCellBounds(row, col, data) canvas.create_rectangle(x0, y0, x1, y1, fill="white") def drawSnake(canvas, data): for (row, col) in data.snake: (x0, y0, x1, y1) = getCellBounds(row, col, data) canvas.create_oval(x0, y0, x1, y1, fill="blue") def drawFood(canvas, data): if (data.foodPosition != None): (row, col) = data.foodPosition (x0, y0, x1, y1) = getCellBounds(row, col, data) canvas.create_oval(x0, y0, x1, y1, fill="green") def drawGameOver(canvas, data): if (data.gameOver): canvas.create_text(data.width/2, data.height/2, text="Game over!", font="Arial 26 bold") def redrawAll(canvas, data): drawBoard(canvas, data) drawSnake(canvas, data) drawFood(canvas, data) drawGameOver(canvas, data) #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds init(data) # create the root and the canvas root = Tk() canvas = Canvas(root, width=data.width, height=data.height) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(300, 300)

  6. Side Scroller Demo
    # side-scroller-demo.py from tkinter import * def init(data): data.scrollX = 0 # amount view is scrolled to the right data.scrollMargin = 50 # closest player may come to either canvas edge data.playerX = data.scrollMargin # player's left edge data.playerY = 0 # player's bottom edge (distance above the base line) data.playerWidth = 10 data.playerHeight = 20 data.walls = 5 data.wallPoints = [0]*data.walls data.wallWidth = 20 data.wallHeight = 40 data.wallSpacing = 90 # wall left edges are at 90, 180, 270,... data.currentWallHit = -1 # start out not hitting a wall def getPlayerBounds(data): # returns absolute bounds, not taking scrollX into account (x0, y1) = (data.playerX, data.height/2 - data.playerY) (x1, y0) = (x0 + data.playerWidth, y1 - data.playerHeight) return (x0, y0, x1, y1) def getWallBounds(wall, data): # returns absolute bounds, not taking scrollX into account (x0, y1) = ((1+wall) * data.wallSpacing, data.height/2) (x1, y0) = (x0 + data.wallWidth, y1 - data.wallHeight) return (x0, y0, x1, y1) def getWallHit(data): # 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(data) for wall in range(data.walls): wallBounds = getWallBounds(wall, data) if (boundsIntersect(playerBounds, wallBounds) == True): return wall return -1 def boundsIntersect(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 movePlayer(dx, dy, data): data.playerX += dx data.playerY += dy # scroll to make player visible as needed if (data.playerX < data.scrollX + data.scrollMargin): data.scrollX = data.playerX - data.scrollMargin if (data.playerX > data.scrollX + data.width - data.scrollMargin): data.scrollX = data.playerX - data.width + data.scrollMargin # and check for a new wall hit wall = getWallHit(data) if (wall != data.currentWallHit): data.currentWallHit = wall if (wall >= 0): data.wallPoints[wall] += 1 def mousePressed(event, data): pass def keyPressed(event, data): if (event.keysym == "Left"): movePlayer(-5, 0, data) elif (event.keysym == "Right"): movePlayer(+5, 0, data) elif (event.keysym == "Up"): movePlayer(0, +5, data) elif (event.keysym == "Down"): movePlayer(0, -5, data) def timerFired(data): pass def redrawAll(canvas, data): # draw the base line lineY = data.height/2 lineHeight = 5 canvas.create_rectangle(0, lineY, data.width, lineY+lineHeight,fill="black") # draw the walls # (Note: should optimize to only consider walls that can be visible now!) sx = data.scrollX for wall in range(data.walls): (x0, y0, x1, y1) = getWallBounds(wall, data) fill = "orange" if (wall == data.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(data.wallPoints[wall])) cy = lineY + 5 canvas.create_text(cx, cy, text=str(wall), anchor=N) # draw the player (x0, y0, x1, y1) = getPlayerBounds(data) 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(data.width/2, 20, text=msg) #################################### # use the run function as-is #################################### def run(width=300, height=300): def redrawAllWrapper(canvas, data): canvas.delete(ALL) redrawAll(canvas, data) canvas.update() def mousePressedWrapper(event, canvas, data): mousePressed(event, data) redrawAllWrapper(canvas, data) def keyPressedWrapper(event, canvas, data): keyPressed(event, data) redrawAllWrapper(canvas, data) def timerFiredWrapper(canvas, data): timerFired(data) redrawAllWrapper(canvas, data) # pause, then call timerFired again canvas.after(data.timerDelay, timerFiredWrapper, canvas, data) # Set up data and call init class Struct(object): pass data = Struct() data.width = width data.height = height data.timerDelay = 100 # milliseconds init(data) # create the root and the canvas root = Tk() canvas = Canvas(root, width=data.width, height=data.height) canvas.pack() # set up events root.bind("<Button-1>", lambda event: mousePressedWrapper(event, canvas, data)) root.bind("<Key>", lambda event: keyPressedWrapper(event, canvas, data)) timerFiredWrapper(canvas, data) # and launch the app root.mainloop() # blocks until window is closed print("bye!") run(300, 300)

  7. Tetris
    This (see here) technically is not a worked example, but rather a step-by-step tutorial that we use as a hw assignment. But given the detailed tutorial, it makes sense to include it here.