- 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)
- Model-View-Controller (MVC)
Note:
- We will write animations using the Model-View-Controller (MVC) paradigm.
- 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.
- The view draws the app using the values in the model.
- In the example above,
redrawAll
is our view.
- The controller responds to keyboard, mouse, timer and other
events and updates the model.
- In the example above,
keyPressed
is our controller.
And...
- 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.
- Controllers can only update the model, they cannot update the view.
- In the example above,
keyPressed
cannot call redrawAll
.
- The view can never update the model.
- In the example above,
redrawAll
cannot
change app.counter
or any
other values in the model.
- 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.
- 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)
- Moving a Dot with Key Presses
- 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)
- 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)
- 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)
- 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)
- 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)
- 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)
- MVC Violations
- 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)
- 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)
- 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)
- 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)
- 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)
- Example: Snake
Here is a 4-part video explaining how to write this version of Snake:
- Draw the board and the Snake
- Add motion and gameOver
- Add food and self-collision
- 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)
- 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 |
- 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)
- 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)