Initial commit
This commit is contained in:
8
directions.py
Normal file
8
directions.py
Normal 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
132
game.py
Normal 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
12
main.py
Normal 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
62
snake.py
Normal 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
|
||||
Reference in New Issue
Block a user