From 0b32b08cb19c12534017dc1cd4b1a90181727caf Mon Sep 17 00:00:00 2001 From: paumann Date: Mon, 10 Oct 2022 00:19:56 +0200 Subject: [PATCH] Initial commit --- directions.py | 8 +++ game.py | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 12 +++++ snake.py | 62 ++++++++++++++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 directions.py create mode 100644 game.py create mode 100644 main.py create mode 100644 snake.py diff --git a/directions.py b/directions.py new file mode 100644 index 0000000..3f0538f --- /dev/null +++ b/directions.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class Direction(Enum): + NORTH = 0 + EAST = 1 + SOUTH = 2 + WEST = 3 diff --git a/game.py b/game.py new file mode 100644 index 0000000..c030fbb --- /dev/null +++ b/game.py @@ -0,0 +1,132 @@ +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) diff --git a/main.py b/main.py new file mode 100644 index 0000000..cc0c487 --- /dev/null +++ b/main.py @@ -0,0 +1,12 @@ +from tkinter import Canvas, Tk + +from game import Game + +WIDTH = 20 +HEIGHT = 15 +BLOCKSIZE = 50 + +if __name__ == "__main__": + + game = Game(WIDTH, HEIGHT, blocksize=BLOCKSIZE) + game.start() diff --git a/snake.py b/snake.py new file mode 100644 index 0000000..956189c --- /dev/null +++ b/snake.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass, replace + +from directions import Direction + + +@dataclass +class Block: + x: int + y: int + + def move(self, direction: Direction) -> None: + """ + Moves the block in the given direction. + """ + if direction is Direction.NORTH: + self.y -= 1 + elif direction is Direction.EAST: + self.x += 1 + elif direction is Direction.SOUTH: + self.y += 1 + elif direction is Direction.WEST: + self.x -= 1 + + def to_tuple(self) -> tuple: + """ + Returns the block in tuple representation (x,y). + """ + return (self.x, self.y) + + def __repr__(self) -> str: + return f"Block({self.x}, {self.y})" + + +class Snake: + def __init__(self, x: int, y: int): + self.body = [Block(x, y)] + + def preview(self, direction: Direction) -> Block: + """ + Previews the head of the snake after a move in the given direction is made. + """ + head = replace(self.body[0]) + head.move(direction) + return head + + def move(self, direction: Direction, extend: bool = False) -> None: + """ + Moves the snake in the given direction. Extends the snake by one block if extend + is true. + """ + head = replace(self.body[0]) + head.move(direction) + self.body.insert(0, head) + + if not extend: + self.body.pop() + + def __len__(self) -> int: + return len(self.body) + + def __contains__(self, item: Block) -> bool: + return item in self.body