133 lines
4.1 KiB
Python
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)
|