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("", lambda e: self._key_event(Direction.WEST)) self._root.bind("", lambda e: self._key_event(Direction.NORTH)) self._root.bind("", lambda e: self._key_event(Direction.EAST)) self._root.bind("", 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)