Files
snake/game.py
2022-10-10 00:19:56 +02:00

133 lines
4.1 KiB
Python

import sys
from random import randint
from tkinter import Canvas, Tk
from PIL import Image, ImageTk
from directions import Direction
from snake import Block, Snake
ILLEGAL_DIRECTION_CHANGES = (
set((Direction.NORTH, Direction.SOUTH)),
set((Direction.EAST, Direction.WEST)),
)
class Game:
def __init__(self, width: int, height: int, blocksize: int = 10):
self.width = width
self.height = height
self.blocksize = blocksize
# Window
self._root = Tk()
self._canvas = Canvas(
self._root, width=width * blocksize, height=height * blocksize, bg="white"
)
self._canvas.pack()
self._bitmap_tk = None
# Register inputs
self._root.bind("<Left>", lambda e: self._key_event(Direction.WEST))
self._root.bind("<Up>", lambda e: self._key_event(Direction.NORTH))
self._root.bind("<Right>", lambda e: self._key_event(Direction.EAST))
self._root.bind("<Down>", lambda e: self._key_event(Direction.SOUTH))
# Number of milliseconds between updates
self._update_delta = 125
# Game state
self.snake = Snake(width // 2, height // 2)
self.direction = Direction.WEST
self._queued_direction = None
self.food = self._spawn_food()
self.score = 0
# Helper functions
def _key_event(self, direction: Direction) -> None:
"""
Queues the given direction.
"""
self._queued_direction = direction
# Game functions
def _oob(self, block: int) -> bool:
"""
Returns true if the given block is out of bounds.
"""
return not (0 <= block.x < self.width and 0 <= block.y < self.height)
def _legal_direction_change(self) -> bool:
"""
Returns true if the queued direction is a valid direction change (not 180 degrees).
"""
s = set((self.direction, self._queued_direction))
return not s in ILLEGAL_DIRECTION_CHANGES
def _spawn_food(self) -> Block:
"""
Returns a random block inside the current game window.
"""
for _ in range(100):
food = Block(randint(0, self.width - 1), randint(0, self.height - 1))
if food not in self.snake:
break
return food
# Game control
def start(self, ticks: int = 8) -> None:
"""
Starts the game.
"""
self._update_delta = 1000 // ticks
self._root.after(self._update_delta, self.tick)
self._root.mainloop()
def draw(self) -> Image:
"""
Draws the current state onto an Image.
"""
bitmap = Image.new("RGB", size=(self.width, self.height), color=(255, 255, 255))
for block in self.snake.body:
bitmap.putpixel(block.to_tuple(), (0, 0, 0))
bitmap.putpixel(self.food.to_tuple(), (255, 233, 124))
return bitmap
def tick(self) -> None:
"""
Executes all game actions and updates the window.
"""
# Update direction if possible
if self._queued_direction is not None and self._legal_direction_change():
self.direction = self._queued_direction
self._queued_direction = None
# Check if move is allowed
head = self.snake.preview(self.direction)
if self._oob(head):
sys.exit("Out ouf bounds!")
if head in self.snake:
sys.exit("Eaten itself!")
fed = head == self.food
# Move and let snake eat
self.snake.move(self.direction, extend=fed)
if fed:
self.food = self._spawn_food()
self.score += 1
# Check if game is won
if self.score == (self.width * self.height) - 1:
sys.exit("You win!")
# Update image
bitmap = self.draw().resize(
(self.width * self.blocksize, self.height * self.blocksize),
resample=Image.NEAREST,
)
self._bitmap_tk = ImageTk.PhotoImage(bitmap)
self._canvas.create_image((0, 0), anchor="nw", image=self._bitmap_tk)
self._root.after(self._update_delta, self.tick)