First off is "procedural event driven programming" a thing? My question is about global variables in the context of an event driven program that doesn't use OOP...
I have written a simple Snake game in Python which makes modest use of global variables. I have been been warned about the evils of globals so often that I thought I'd try and refactor the program to pass arguments instead.
It seems to me that any semblance of elegance or simplicity was lost in this endeavor. The version with globals is shown at the bottom of this post. Here's an excerpt from my attempt at refactoring:
def go_up(screen, pen, snake, snake_direction, food):
if snake_direction != "down":
snake_direction = "up"
move_snake(screen, pen, snake, snake_direction, food)
# Event handlers
screen.listen()
screen.onkey(lambda: go_up(screen, pen, snake, snake_direction, food), "Up")
screen.onkey(lambda: go_right(screen, pen, snake, snake_direction, food), "Right")
screen.onkey(lambda: go_down(screen, pen, snake, snake_direction, food), "Down")
screen.onkey(lambda: go_left(screen, pen, snake, snake_direction, food), "Left")
# Let's go
snake = []
snake_direction = ""
screen, pen, snake, snake_direction, food = reset(screen, pen, snake, snake_direction, food)
turtle.done()
It got very messy very quickly and I was unable to recreate the same functionality.
I'm pretty sure that even with a simple game like this, using OOP would be preferable to a procedural approach which involves so much argument passing.
But assuming I don't want to use OOP - say I want to teach kids how to write fun games without getting in that deep - is there any merit to using the argument passing approach over what appears to me to be the far more simple approach of having a few global variables?
Is this code evil?
"""
A simple snake game using Turtle Graphics.
Todo: border collision?, score
"""
import turtle
import random
WIDTH = 500
HEIGHT = 500
FOOD_SIZE = 10
DELAY = 100 # milliseconds
offsets = {
"up": (0, 20),
"down": (0, -20),
"left": (-20, 0),
"right": (20, 0)
}
def reset():
global snake, snake_direction
snake = [[0, 0], [0, 20], [0, 40], [0, 50], [0, 60]]
snake_direction = "up"
food_pos = get_random_food_pos()
food.goto(food_pos)
# screen.update() Only needed if we are fussed about drawing food before call to `draw_snake()`.
move_snake()
def move_snake():
# Next position for head of snake.
new_head = snake[-1].copy()
new_head[0] = snake[-1][0] + offsets[snake_direction][0]
new_head[1] = snake[-1][1] + offsets[snake_direction][1]
# Check self-collision
if new_head in snake[:-1]: # Or collision with walls?
reset()
else:
# No self-collision so we can continue moving the snake.
snake.append(new_head)
# Check food collision
if not food_collision():
snake.pop(0) # Keep the snake the same length unless fed.
# Allow screen wrapping
if snake[-1][0] > WIDTH / 2:
snake[-1][0] -= WIDTH
elif snake[-1][0] < - WIDTH / 2:
snake[-1][0] += WIDTH
elif snake[-1][1] > HEIGHT / 2:
snake[-1][1] -= HEIGHT
elif snake[-1][1] < -HEIGHT / 2:
snake[-1][1] += HEIGHT
# Clear previous snake stamps
pen.clearstamps()
# Draw snake
for segment in snake:
pen.goto(segment[0], segment[1])
pen.stamp()
# Refresh screen
screen.update()
# Rinse and repeat
turtle.ontimer(move_snake, DELAY)
def food_collision():
if get_distance(snake[-1], (food.xcor(), food.ycor())) < 20:
food_pos = get_random_food_pos()
food.goto(food_pos)
return True
return False
def get_random_food_pos():
x = random.randint(- WIDTH / 2 + FOOD_SIZE, WIDTH / 2 - FOOD_SIZE)
y = random.randint(- HEIGHT / 2 + FOOD_SIZE, HEIGHT / 2 - FOOD_SIZE)
return (x, y)
def get_distance(pos1, pos2):
x1, y1 = pos1
x2, y2 = pos2
distance = ((y2 - y1) ** 2 + (x2 - x1) ** 2) ** 0.5
return distance
def go_up():
global snake_direction
if snake_direction != "down":
snake_direction = "up"
def go_right():
global snake_direction
if snake_direction != "left":
snake_direction = "right"
def go_down():
global snake_direction
if snake_direction != "up":
snake_direction = "down"
def go_left():
global snake_direction
if snake_direction != "right":
snake_direction = "left"
def main():
global screen, pen, food
# Screen
screen = turtle.Screen()
screen.setup(WIDTH, HEIGHT)
screen.title("Snake")
screen.bgcolor("green")
screen.setup(500, 500)
screen.tracer(0)
# Pen
pen = turtle.Turtle("square")
pen.penup()
# Food
food = turtle.Turtle()
food.shape("circle")
food.color("red")
food.shapesize(FOOD_SIZE / 20) # Default size of turtle "square" shape is 20.
food.penup()
# Event handlers
screen.listen()
screen.onkey(go_up, "Up")
screen.onkey(go_right, "Right")
screen.onkey(go_down, "Down")
screen.onkey(go_left, "Left")
# Let's go
reset()
turtle.done()
if __name__ == "__main__":
main()