Skip to content

Instantly share code, notes, and snippets.

@ssimono
Created November 16, 2020 20:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ssimono/f28b75cefb853a7f450236cc76467eed to your computer and use it in GitHub Desktop.
Save ssimono/f28b75cefb853a7f450236cc76467eed to your computer and use it in GitHub Desktop.
Snake that plays by itself. AI far from singularity
from collections import deque
from enum import Enum
from functools import lru_cache
from random import randint
from sys import stderr
from time import sleep
from typing import Generator, Iterable, NamedTuple
MAP_WIDTH = 30
MAP_HEIGHT = 30
class Point(NamedTuple):
x: int
y: int
def on_map(self):
return (0 <= self.x < MAP_WIDTH) and (0 <= self.y < MAP_HEIGHT)
def __add__(self, b):
return Point(self.x + b.x, self.y + b.y)
def __sub__(self, b):
return Point(self.x - b.x, self.y - b.y)
def __mul__(self, k: int):
return Point(self.x * k, self.y * k)
def __str__(self):
return f"({self.x},{self.y})"
class Direction(Enum):
RIGHT = 1
TOP = 2
LEFT = 3
BOTTOM = 4
@property
@lru_cache(None)
def vector(self) -> Point:
return Point(self.value & 1, ~self.value & 1) * self._sign()
def _sign(self) -> int:
return 1 if (self.value & 2) == 0 else -1
def __str__(self) -> str:
return ("→", "↑", "←", "↓")[self.value - 1]
class GameError(Exception):
pass
class OutOfBoundError(GameError):
pass
class SelfCollisionError(GameError):
pass
class NoOptionError(GameError):
pass
class Snake:
def __init__(self, *args: Point):
if not len(args):
raise Exception("Empty snake")
self._parts = deque(args)
self._fed = False
@property
def head(self) -> Point:
return self._parts[0]
@property
def parts(self) -> Iterable[Point]:
return self._parts
def move(self, direction: Direction):
head = self._parts[0] + direction.vector
if not head.on_map():
raise OutOfBoundError
elif head in self._parts:
raise SelfCollisionError
else:
self._parts.appendleft(head)
if self._fed:
self._fed = False
else:
self._parts.pop()
def feed(self):
self._fed = True
def plant_apple(snake: Snake) -> Point:
apple = Point(randint(0, MAP_WIDTH - 1), randint(0, MAP_HEIGHT - 1))
while apple in snake.parts:
apple = Point(randint(0, MAP_WIDTH - 1), randint(0, MAP_HEIGHT - 1))
return apple
def plan_route(snake: Snake, apple: Point) -> Generator[Direction, None, None]:
while snake.head != apple:
delta = apple - snake.head
direction_score = lambda d: (d.vector.x * delta.x, d.vector.y * delta.y)
choices = sorted(
list(Direction),
key=direction_score,
reverse=True,
)
for direction in choices:
next_pos = snake.head + direction.vector
if next_pos in snake.parts or not next_pos.on_map():
continue
yield direction
break
else:
raise NoOptionError
def draw(snake: Snake, apple: Point):
matrix = [["."] * MAP_WIDTH for _ in range(MAP_HEIGHT)]
matrix[apple.y][apple.x] = "🍎"
for p in snake.parts:
matrix[p.y][p.x] = "x"
return "\n".join(["".join(line) for line in matrix])
def run():
middle_x = MAP_WIDTH // 2
middle_y = MAP_HEIGHT // 2
snake = Snake(Point(middle_x, middle_y))
apple = plant_apple(snake)
route = plan_route(snake, apple)
while True:
try:
snake.move(next(route))
if snake.head == apple:
snake.feed()
apple = plant_apple(snake)
route = plan_route(snake, apple)
print(chr(27) + "[2J")
print(draw(snake, apple))
sleep(0.2)
except GameError:
print(f"Score: {len(snake.parts)}", file=stderr)
break
except KeyboardInterrupt:
break
if __name__ == "__main__":
run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment