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