Initial commit

This commit is contained in:
2022-10-10 00:19:56 +02:00
commit 0b32b08cb1
4 changed files with 214 additions and 0 deletions

8
directions.py Normal file
View File

@ -0,0 +1,8 @@
from enum import Enum
class Direction(Enum):
NORTH = 0
EAST = 1
SOUTH = 2
WEST = 3

132
game.py Normal file
View File

@ -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("<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)

12
main.py Normal file
View File

@ -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()

62
snake.py Normal file
View File

@ -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