Source code for snake.game

"""
Holds the snake game.

The only thing necessary to run a game of snake is :class:`.SnakeGame`.

"""

from collections import deque
import random
import itertools as it


class _Snake:
    """
    Represents a snake in the :class:`.SnakeGame`.

    """

    def __init__(self):
        """
        Initialize a :class:`_Snake`.

        """

        self._body = deque([(0, 0)])
        self._velocity = (1, 0)
        self._velocity_queue = deque([])

    def get_body(self):
        """
        Yield the positions occupied by the snake.

        Yields
        ------
        :class:`tuple`
            The position of a segment of the snake's body.

        """

        yield from self._body

    def take_step(self):
        """
        Make the snake take a step.

        Returns
        -------
        None : :class:`NoneType`

        """

        if self._velocity_queue:
            new_velocity = self._velocity_queue.popleft()
            if self._is_valid_velocity(new_velocity):
                self._velocity = new_velocity

        head_x, head_y = self._body[-1]
        velocity_x, velocity_y = self._velocity
        new_head = head_x + velocity_x, head_y + velocity_y
        self._body.append(new_head)
        self._body.popleft()

    def _is_valid_velocity(self, velocity):
        """
        Check if `velocity` is valid.

        A snake velocity is invalid if the snake is going up and the
        `velocity` is down, or vice versa. Equally, `velocity` is
        invalid if the snake is going left and `velocity` is right and
        vice versa.

        Parameters
        ----------
        velocity : :class:`tuple`
            A :class:`tuple` of the form ``(1, 0)`` representing a
            possible snake velocity.

        Returns
        -------
        :class:`bool`
            ``True`` if `velocity` is valid and ``False`` otherwise.

        """

        invalid = {
            frozenset({(0, 1), (0, -1)}),
            frozenset({(1, 0), (-1, 0)})
        }
        valid = {
            (0, 1), (0, -1), (1, 0), (-1, 0)
        }
        velocities = frozenset({velocity, self._velocity})
        return velocities not in invalid and velocity in valid

    def hit(self, walls):
        """
        Check if the snake has hit a wall.

        Parameters
        ----------
        walls : :class:`frozenset`
            A :class:`frozenset` of the form

            .. code-block:: python

                walls = {(0,2), (3, 4), (5,4)}

            holding the coordinates of each bit of the walls.

        Returns
        -------
        :class:`bool`
            ``True`` if the snake overlaps with any walls and
            ``False`` otherwise.

        """

        return any(piece in walls for piece in self._body)

    def bite(self):
        """
        Check if the snake has bitten itself.

        Returns
        -------
        :class:`bool`
            ``True`` if the snake has bitten itself and ``False``
            otherwise.

        """

        # If the snake has bitten itself, some of its body pieces
        # will overlap, which means duplicates will be present in
        # self.body.
        return len(set(self._body)) != len(self._body)

    def is_escaped(self, board_size):
        """
        Check is the snake has escaped the board.

        Parameters
        ----------
        board_size : :class:`tuple`
            A :class:`tuple` of the form ``(21, 33)`` represting the
            size of the board in the x and y directions.

        Returns
        -------
        :class:`bool`
            ``True`` if the snake has escaped the board and
            ``False`` otherwise.

        """

        min_x = min(x for x, y in self._body)
        max_x = max(x for x, y in self._body)
        min_y = min(y for x, y in self._body)
        max_y = max(y for x, y in self._body)

        board_x, board_y = board_size
        return (
            min_x < 0
            or min_y < 0
            or max_x >= board_x
            or max_y >= board_y
        )

    def eat(self, apple):
        """
        Make the snake eat the apple.

        The snake will only eat the `apple` if its head is at the
        same position as the `apple`.

        Parameters
        ----------
        apple : :class:`tuple`
            A :class:`tuple` of the form ``(21, 12)``, holding the
            coordinates of the apple the snake is meant to eat.

        Returns
        -------
        :class:`bool`
            ``True`` if the snake successfully ate the `apple` and
            ``False`` otherwise.

        """

        head_x, head_y = head = self._body[-1]
        ate = head == apple
        if ate:
            velocity_x, velocity_y = self._velocity
            new_head = head_x + velocity_x, head_y + velocity_y
            self._body.append(new_head)
        return ate

    def queue_velocity(self, velocity):
        """
        Queue a future snake velocity.

        Parameters
        ----------
        velocity : :class:`tuple`
            The velocity the snake should have.

        Returns
        -------
        None : :class:`NoneType`

        """

        self._velocity_queue.append(velocity)

    def get_num_queued_velocities(self):
        """
        Return the numbered of queued velocities.

        Returns
        -------
        :class:`int`
            The number of queued velocities.

        """

        return len(self._velocity_queue)

    def get_velocity(self, step=0):
        """
        Get the velocity of the snake a step.

        Parameters
        ----------
        step : :class:`int`, optional
            The velocity at a given step. If ``0``then the current
            velocity is returned.

        Returns
        -------
        :class:`tuple
            The velocity.

        """

        if step == 0:
            return self._velocity

        return self._velocity_queue[step-1]

    def get_length(self):
        """
        Return the length of the snake.

        Returns
        -------
        :class:`int`
            The length of the snake.

        """

        return len(self._body)


[docs]class SnakeGame: """ Represents a game of snake. The game can be run with :meth:`run`, or stepwise with :meth:`run_stepwise`. The game runs in a self contained loop and will not take input from the keyboard or display itself on the screen. The game is interacted with purely programatically. However, you can write code that captures keyboard input and sends it to the game. You can also write code that looks at the state of the game, as contained in this class, and writes it to the screen. :class:`.GameIO` does both of these things. The snake is controlled by setting its movement direction. This can be done with :meth:`queue_snake_movement_direction`. The user will use this method to queue which direction the snake will move, the next time the snake takes a step. The queue allows the user to queue several steps ahead, which results in the game feeling more responsive when played. :class:`SnakeGame` can be initialized with walls, allowing the user to create a level. If you want to interact with the game in an automated way you can do something like .. code-block:: python def get_next_direction(game): # This is a user-defined function which decides on the # direction the snake should take on the next turn. ... def apply_action(game): # This is a user-defined function which looks at the # state of the game an decides on sending actions to it. direction = get_next_direction(game) game.queue_snake_movement_direction(direction) game = SnakeGame( board_size=(23, 34), walls=((1, 1), (2, 2), (3, 3)), random_seed=12, ) for step_number in game.run_stepwise(): apply_action(game) """
[docs] def __init__(self, board_size, walls, random_seed): """ Initialize a :class:`.SnakeGame`. Parameters ---------- board_size : :class:`tuple` A :class:`tuple` of the form ``(23, 12)`` which represents the size of the board in the x and y directions. walls : :class:`iterable` of :class:`tuple` An :class:`iterable` holding the position of every wall segment. random_seed : :class:`int` The random seed to be used with the game. Used to generate apple locations. """ self._generator = random.Random(random_seed) self._board_size = board_size self._snake = _Snake() self._walls = frozenset(walls) self._apple = self._get_new_apple()
def _get_new_apple(self): """ Generate new :attr:`_apple` coordinates. Returns ------- :class:`tuple` The position of a new apple. """ snake_body = self._snake.get_body() board_x, board_y = self._board_size board_positions = it.product( range(0, board_x), range(0, board_y), ) valid_positions = ( pos for pos in board_positions if pos not in set(snake_body) and pos not in self._walls ) # The number of valid apple positions is given # by the total board size minus the walls and the length of the # snake, as each bit of the snake and each wall must occupy an # empty position. board_size = board_x * board_y max_index = ( board_size-len(self._walls)-self._snake.get_length()-1 ) # Avoid looping through all the valid apple positions by # generating the index of the chosen position ahead of time # and returning as soon as the position with that index is # found. index = self._generator.randint(0, max_index) for i, position in enumerate(valid_positions): if i == index: return position def _take_step(self): """ Take a single game step. Returns ------- None : :class:`NoneType` """ self._snake.take_step() if self._snake.eat(self._apple): self._apple = self._get_new_apple()
[docs] def run(self): """ Run the game. Returns ------- None : :class:`NoneType` """ while ( not self._snake.hit(self._walls) and not self._snake.bite() and not self._snake.is_escaped(self._board_size) ): self._take_step()
[docs] def run_stepwise(self): """ Run the game, but yield after every step. Yields ------ :class:`int` The step number. """ step_number = 0 while ( not self._snake.hit(self._walls) and not self._snake.bite() and not self._snake.is_escaped(self._board_size) ): self._take_step() step_number += 1 yield step_number
[docs] def get_snake_velocity(self, step=0): """ Return the step the snake will take. Parameters ---------- step : :class:`int`, optional The step for which the velocity is returned. ``0`` is the current step. Returns ------- :class:`tuple` The :class:`tuple` can be one of ``(0, 1)``, ``(0, -1)``, ``(1, 0)`` or ``(-1, 0)``, representing the step the snake will take. """ return self._snake.get_velocity(step)
[docs] def queue_snake_movement_direction(self, direction): """ Queue a movement direction for the snake. When this method is called multiple times between snake steps, it allows the caller to queue multiple directions, which will be resolved at a rate of one per step. Parameters ---------- direction : :class:`str` Can be ``'up'``, ``'down'``, ``'right'`` or ``'left'`` to signify the movement direction the snake will have the when it moves. Returns ------- :class:`bool` ``True`` if a movement direction was successfully queued and ``False`` otherwise. """ if direction == 'up': velocity = (0, 1) elif direction == 'down': velocity = (0, -1) elif direction == 'right': velocity = (1, 0) elif direction == 'left': velocity = (-1, 0) if self._snake.get_num_queued_velocities() < 5: self._snake.queue_velocity(velocity) return True return False
[docs] def get_snake(self): """ Yield the positions occupied by the snake. Yields ------ :class:`tuple` The position of a segment of the snake's body. """ yield from self._snake.get_body()
[docs] def get_walls(self): """ Yield the coordinates of the walls. Yields ------ :class:`tuple` The position of a wall segment. """ yield from self._walls
[docs] def get_apple(self): """ Return the coordinates of the apple. Returns ------- :class:`tuple` A :class:`tuple` of the form ``(21, 12)``, holding the coordinates of the apple the snake is meant to eat. """ return self._apple
[docs] def get_board_size(self): """ Return the board size. Returns ------- :class:`tuple` A :class:`tuple` of the form ``(23, 12)`` which represents the size of the board in the x and y directions. """ return self._board_size
[docs] def get_snake_length(self): """ Return the length of the snake. Returns ------- :class:`int` The length of the snake. """ return self._snake.get_length()