-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsnake.py
97 lines (83 loc) · 3.01 KB
/
snake.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#!/usr/bin/env python3.10
"""
Snake Eater
Made with PyGame
"""
import logging
from dataclasses import dataclass
from serializers import SerializerMixin
from utils import Coordinate, Direction
@dataclass(frozen=True, eq=True, order=True, kw_only=True, slots=True)
class Snake(SerializerMixin):
segments: tuple[Coordinate, ...] = (
Coordinate(0, 1),
Coordinate(0, 0),
Coordinate(1, 0),
)
def validate_segments(self) -> bool:
if len(self.segments) < 2:
# 2 because we depend on the 2nd segment to find direction
return False
for previous, next in zip(self.segments[:-1], self.segments[1:]):
if abs(previous.X - next.X) + abs(previous.Y - next.Y) != 1:
return False
return True
@property
def valid_actions(self) -> set[Direction]:
"""Returns the _new_ directions that the snake can _change_ to.
Based on the snake's current direction."""
directions: set[Direction] = self.direction.next()
return directions
@property
def ouroboros(self) -> bool:
"""True iff the snake is eating itself."""
return self.head in self.segments[1:]
@property
def direction(self) -> Direction:
if len(self.segments) == 1:
return Direction.random()
# fmt: off
match (self.head.X - self.neck.X, self.head.Y - self.neck.Y):
case (-1, 0): return Direction.LEFT
case (1, 0): return Direction.RIGHT
case (0, 1): return Direction.DOWN
case (0, -1): return Direction.UP
case _: raise Exception()
# fmt: on
@property
def head(self) -> Coordinate:
"""Return the position of the snake's head."""
return self.segments[0]
@property
def tail(self) -> Coordinate:
"""Return the position of the snake's tail."""
return self.segments[-1]
@property
def neck(self) -> Coordinate:
"""Return the element at index 1"""
# we use this for finding direction
try:
return self.segments[1]
except IndexError:
raise Exception(
f"Attempted to access snake.neck, but the snake is too short! {self.segments=}"
)
def move(self, direction: Direction | None, grow: bool) -> "Snake":
if direction is None:
direction = self.direction
if direction not in self.valid_actions:
raise Exception(f"Invalid movement direction {direction=}")
new_x, new_y = self.head
match direction:
case Direction.UP:
new_y -= 1
case Direction.DOWN:
new_y += 1
case Direction.LEFT:
new_x -= 1
case Direction.RIGHT:
new_x += 1
logging.debug(f"Snake moved: {grow=} {self.head=}")
new_head = Coordinate(X=new_x, Y=new_y)
new_body = self.segments[:] if grow else self.segments[:-1]
return Snake(segments=(new_head,) + new_body)