generated from mwc/project_game
game
This commit is contained in:
7
arrowkeyagent.py
Normal file
7
arrowkeyagent.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from retro.game import Game
|
||||||
|
from retro.agent import ArrowKeyAgent
|
||||||
|
|
||||||
|
agent = ArrowKeyAgent()
|
||||||
|
state = {}
|
||||||
|
game = Game([agent], state)
|
||||||
|
game.play()
|
||||||
106
class Agent source.py
Normal file
106
class Agent source.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
class Agent:
|
||||||
|
"""Represents a character in the game. To create an Agent, define a new
|
||||||
|
class with some of the attributes and methods below. You may change any of
|
||||||
|
the Agent's attributes at any time, and the result will immediately be
|
||||||
|
visible in the game.
|
||||||
|
|
||||||
|
After you create your Agents, add them to the ``Game``, either when it is created
|
||||||
|
or using ``Game.add_agent`` later on. Then the Game will take care of calling
|
||||||
|
the Agent's methods at the appropriate times.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
position: (Required) The character's ``(int, int)`` position on the game
|
||||||
|
board.
|
||||||
|
character: (Required unless display is ``False``.) A one-character string
|
||||||
|
which will be displayed at the Agent's position on the game board.
|
||||||
|
name: (Optional) If an agent has a name, it must be unique within the game.
|
||||||
|
Agent names can be used to look up agents with
|
||||||
|
:py:meth:`retro.game.Game.get_agent_by_name`.
|
||||||
|
color (str): (Optional) The agent's color.
|
||||||
|
`Available colors <https://blessed.readthedocs.io/en/latest/colors.html>`_.
|
||||||
|
display: (Optional) When ``False``, the Agent will not be displayed on the
|
||||||
|
board. This is useful when you want to create an agent which will be displayed
|
||||||
|
later, or when you want to create an agent which acts on the Game indirectly,
|
||||||
|
for example by spawning other Agents. Defaults to True.
|
||||||
|
z: (Optional) When multiple Agents have the same position on the board, the
|
||||||
|
Agent with the highest ``z`` value will be displayed.
|
||||||
|
The Game is played on a two-dimensional (x, y) board, but you can think of
|
||||||
|
``z`` as a third "up" dimension. Defaults to 0.
|
||||||
|
"""
|
||||||
|
character = "*"
|
||||||
|
position = (0, 0)
|
||||||
|
name = "agent"
|
||||||
|
color = "white_on_black"
|
||||||
|
display = True
|
||||||
|
z = 0
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
"""If an Agent has this method, it will be called once
|
||||||
|
each turn.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
game (Game): The game which is currently being played will be
|
||||||
|
passed to the Agent, in case it needs to check anything about
|
||||||
|
the game or make any changes.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
"""If an Agent has a this method, it will be called every
|
||||||
|
time a key is pressed in the game.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
keystroke (blessed.keyboard.Keystroke): The key which was pressed. You can
|
||||||
|
compare a Keystroke with a string (e.g. ``if keystroke == 'q'``) to check
|
||||||
|
whether it is a regular letter, number, or symbol on the keyboard. You can
|
||||||
|
check special keys using the keystroke's name
|
||||||
|
(e.g. ``if keystroke.name == "KEY_RIGHT"``). Run your game in debug mode to
|
||||||
|
see the names of keystrokes.
|
||||||
|
game (Game): The game which is currently being played will be
|
||||||
|
passed to the Agent, in case it needs to check anything about
|
||||||
|
the game or make any changes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ArrowKeyAgent:
|
||||||
|
"""A simple agent which can be moved around with the arrow keys.
|
||||||
|
"""
|
||||||
|
RIGHT = (1, 0)
|
||||||
|
UP = (0, -1)
|
||||||
|
LEFT = (-1, 0)
|
||||||
|
DOWN = (0, 1)
|
||||||
|
name = "ArrowKeyAgent"
|
||||||
|
character = "*"
|
||||||
|
position = (0,0)
|
||||||
|
display = True
|
||||||
|
z = 0
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
"""Moves the agent's position if the keystroke is one of the arrow keys.
|
||||||
|
One by one, checks the keystroke's name against each arrow key.
|
||||||
|
Then uses :py:meth:`try_to_move` to check whether the move is on the
|
||||||
|
game's board before moving.
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
if keystroke.name == "KEY_RIGHT":
|
||||||
|
self.try_to_move((x + 1, y), game)
|
||||||
|
elif keystroke.name == "KEY_UP":
|
||||||
|
self.try_to_move((x, y - 1), game)
|
||||||
|
elif keystroke.name == "KEY_LEFT":
|
||||||
|
self.try_to_move((x - 1, y), game)
|
||||||
|
elif keystroke.name == "KEY_DOWN":
|
||||||
|
self.try_to_move((x, y + 1), game)
|
||||||
|
|
||||||
|
|
||||||
|
def try_to_move(self, position, game):
|
||||||
|
"""Moves to the position if it is on the game board.
|
||||||
|
"""
|
||||||
|
if game.on_board(position):
|
||||||
|
self.position = position
|
||||||
|
game.log(f"Position: {self.position}")
|
||||||
222
class Game source.py
Normal file
222
class Game source.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from signal import signal, SIGWINCH
|
||||||
|
from time import sleep, perf_counter
|
||||||
|
from blessed import Terminal
|
||||||
|
from retro.view import View
|
||||||
|
from retro.validation import (
|
||||||
|
validate_agent,
|
||||||
|
validate_state,
|
||||||
|
validate_agent_name,
|
||||||
|
validate_position,
|
||||||
|
)
|
||||||
|
from retro.errors import (
|
||||||
|
AgentWithNameAlreadyExists,
|
||||||
|
AgentNotFoundByName,
|
||||||
|
IllegalMove,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
"""
|
||||||
|
Creates a playable game.
|
||||||
|
You will use Game to create games, but don't need to read or understand how
|
||||||
|
this class works. The main work in creating a
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
agents (list): A list of agents to add to the game.
|
||||||
|
state (dict): A dict containing the game's initial state.
|
||||||
|
board_size (int, int): (Optional) The two-dimensional size of the game board. D
|
||||||
|
debug (bool): (Optional) Turn on debug mode, showing log messages while playing.
|
||||||
|
framerate (int): (Optional) The target number of frames per second at which the
|
||||||
|
game should run.
|
||||||
|
color (str): (Optional) The game's background color scheme. `Available colors <https://blessed.readthedocs.io/en/latest/colors.html>`_.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# This example will create a simple game.
|
||||||
|
from retro.game import Game
|
||||||
|
from retro.agent import ArrowKeyAgent
|
||||||
|
|
||||||
|
agents = [ArrowKeyAgent()]
|
||||||
|
state = {}
|
||||||
|
game = Game(agents, state)
|
||||||
|
game.play()
|
||||||
|
|
||||||
|
"""
|
||||||
|
STATE_HEIGHT = 5
|
||||||
|
EXIT_CHARACTERS = ("KEY_ENTER", "KEY_ESCAPE")
|
||||||
|
|
||||||
|
def __init__(self, agents, state, board_size=(64, 32), debug=False, framerate=24,
|
||||||
|
color="white_on_black"):
|
||||||
|
self.log_messages = []
|
||||||
|
self.agents_by_name = {}
|
||||||
|
self.agents = []
|
||||||
|
self.state = validate_state(state)
|
||||||
|
self.board_size = board_size
|
||||||
|
self.debug = debug
|
||||||
|
self.framerate = framerate
|
||||||
|
self.turn_number = 0
|
||||||
|
self.color = color
|
||||||
|
for agent in agents:
|
||||||
|
self.add_agent(agent)
|
||||||
|
|
||||||
|
def play(self):
|
||||||
|
"""Starts the game.
|
||||||
|
"""
|
||||||
|
self.playing = True
|
||||||
|
terminal = Terminal()
|
||||||
|
with terminal.fullscreen(), terminal.hidden_cursor(), terminal.cbreak():
|
||||||
|
view = View(terminal, color=self.color)
|
||||||
|
while self.playing:
|
||||||
|
turn_start_time = perf_counter()
|
||||||
|
self.turn_number += 1
|
||||||
|
self.keys_pressed = self.collect_keystrokes(terminal)
|
||||||
|
if self.debug and self.keys_pressed:
|
||||||
|
self.log("Keys: " + ', '.join(k.name or str(k) for k in self.keys_pressed))
|
||||||
|
for agent in self.agents:
|
||||||
|
if hasattr(agent, 'handle_keystroke'):
|
||||||
|
for key in self.keys_pressed:
|
||||||
|
agent.handle_keystroke(key, self)
|
||||||
|
if hasattr(agent, 'play_turn'):
|
||||||
|
agent.play_turn(self)
|
||||||
|
if getattr(agent, 'display', True):
|
||||||
|
if not self.on_board(agent.position):
|
||||||
|
raise IllegalMove(agent, agent.position)
|
||||||
|
view.render(self)
|
||||||
|
turn_end_time = perf_counter()
|
||||||
|
time_elapsed_in_turn = turn_end_time - turn_start_time
|
||||||
|
time_remaining_in_turn = max(0, 1/self.framerate - time_elapsed_in_turn)
|
||||||
|
sleep(time_remaining_in_turn)
|
||||||
|
while True:
|
||||||
|
if terminal.inkey().name in self.EXIT_CHARACTERS:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def collect_keystrokes(self, terminal):
|
||||||
|
keys = set()
|
||||||
|
while True:
|
||||||
|
key = terminal.inkey(0.001)
|
||||||
|
if key:
|
||||||
|
keys.add(key)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
"""Write a log message.
|
||||||
|
Log messages are only shown when debug mode is on.
|
||||||
|
They can be very useful for debugging.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
message (str): The message to log.
|
||||||
|
"""
|
||||||
|
self.log_messages.append((self.turn_number, message))
|
||||||
|
|
||||||
|
|
||||||
|
def end(self):
|
||||||
|
"""Ends the game. No more turns will run.
|
||||||
|
"""
|
||||||
|
self.playing = False
|
||||||
|
|
||||||
|
|
||||||
|
def add_agent(self, agent):
|
||||||
|
"""Adds an agent to the game.
|
||||||
|
Whenever you want to add a new agent during the game, you must add it to
|
||||||
|
the game using this method.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
agent: An instance of an agent class.
|
||||||
|
"""
|
||||||
|
validate_agent(agent)
|
||||||
|
if getattr(agent, "display", True) and not self.on_board(agent.position):
|
||||||
|
raise IllegalMove(agent, agent.position)
|
||||||
|
if hasattr(agent, "name"):
|
||||||
|
if agent.name in self.agents_by_name:
|
||||||
|
raise AgentWithNameAlreadyExists(agent.name)
|
||||||
|
self.agents_by_name[agent.name] = agent
|
||||||
|
self.agents.append(agent)
|
||||||
|
|
||||||
|
|
||||||
|
def get_agent_by_name(self, name):
|
||||||
|
"""Looks up an agent by name.
|
||||||
|
This is useful when one agent needs to interact with another agent.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name (str): The agent's name. If there is no agent with this name,
|
||||||
|
you will get an error.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An agent.
|
||||||
|
"""
|
||||||
|
validate_agent_name(name)
|
||||||
|
if name in self.agents_by_name:
|
||||||
|
return self.agents_by_name[name]
|
||||||
|
else:
|
||||||
|
raise AgentNotFoundByName(name)
|
||||||
|
|
||||||
|
|
||||||
|
def is_empty(self, position):
|
||||||
|
"""Checks whether a position is occupied by any agents.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
position (int, int): The position to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A bool
|
||||||
|
"""
|
||||||
|
return position not in self.get_agents_by_position()
|
||||||
|
|
||||||
|
|
||||||
|
def get_agents_by_position(self):
|
||||||
|
"""Returns a dict where each key is a position (e.g. (10, 20)) and
|
||||||
|
each value is a list containing all the agents at that position.
|
||||||
|
This is useful when an agent needs to find out which other agents are
|
||||||
|
on the same space or nearby.
|
||||||
|
"""
|
||||||
|
positions = defaultdict(list)
|
||||||
|
for agent in self.agents:
|
||||||
|
if getattr(agent, "display", True):
|
||||||
|
validate_position(agent.position)
|
||||||
|
positions[agent.position].append(agent)
|
||||||
|
return positions
|
||||||
|
|
||||||
|
|
||||||
|
def remove_agent(self, agent):
|
||||||
|
"""Removes an agent from the game.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
agent (Agent): the agent to remove.
|
||||||
|
"""
|
||||||
|
if agent not in self.agents:
|
||||||
|
raise AgentNotInGame(agent)
|
||||||
|
else:
|
||||||
|
self.agents.remove(agent)
|
||||||
|
if hasattr(agent, "name"):
|
||||||
|
self.agents_by_name.pop(agent.name)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_agent_by_name(self, name):
|
||||||
|
"""Removes an agent from the game.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
name (str): the agent's name.
|
||||||
|
"""
|
||||||
|
validate_agent_name(name)
|
||||||
|
if name not in self.agents_by_name:
|
||||||
|
raise AgentNotFoundByName(name)
|
||||||
|
agent = self.agents_by_name.pop(name)
|
||||||
|
self.agents.remove(agent)
|
||||||
|
|
||||||
|
|
||||||
|
def on_board(self, position):
|
||||||
|
"""Checks whether a position is on the game board.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
position (int, int): The position to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A bool
|
||||||
|
"""
|
||||||
|
validate_position(position)
|
||||||
|
x, y = position
|
||||||
|
bx, by = self.board_size
|
||||||
|
return x >= 0 and x < bx and y >= 0 and y < by
|
||||||
32
game.py
Normal file
32
game.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from random import randint
|
||||||
|
from retro.game import Game
|
||||||
|
|
||||||
|
class Agent:
|
||||||
|
RIGHT = (1, 0)
|
||||||
|
UP = (0, -1)
|
||||||
|
LEFT = (-1, 0)
|
||||||
|
DOWN = (0, 1)
|
||||||
|
character = ">"
|
||||||
|
direction = RIGHT
|
||||||
|
position = (15, 0)
|
||||||
|
name = "agent"
|
||||||
|
color = "white_on_black"
|
||||||
|
display = True
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
x, y = self.position
|
||||||
|
if keystroke.name == "M":
|
||||||
|
self.direction = self.RIGHT
|
||||||
|
self.character = '>'
|
||||||
|
elif keystroke.name == "I":
|
||||||
|
self.direction = self.LEFT
|
||||||
|
self.character = '<'
|
||||||
|
|
||||||
|
def can_move(self, position, game):
|
||||||
|
on_board = game.on_board(position)
|
||||||
|
empty = game.is_empty(position)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
player = Agent()
|
||||||
|
game = Game([Agent], {'score': 0}, board_size:=(24, 20), framerate:=12)
|
||||||
|
game.play()
|
||||||
144
nav.py
Normal file
144
nav.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
from random import randint
|
||||||
|
from retro.game import Game
|
||||||
|
|
||||||
|
HEIGHT = 25
|
||||||
|
WIDTH = 25
|
||||||
|
|
||||||
|
class Spaceship:
|
||||||
|
"""A player-controlled agent which moves left and right, dodging asteroids.
|
||||||
|
Spaceship is a pretty simple class. The ship's character is ``^``, and
|
||||||
|
its position starts at the bottom center of the screen.
|
||||||
|
"""
|
||||||
|
name = "ship"
|
||||||
|
character = '^'
|
||||||
|
position = (WIDTH // 2, HEIGHT - 1)
|
||||||
|
color = "black_on_skyblue1"
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
"""When the
|
||||||
|
left or arrow key is pressed, it moves left or right. If the ship's
|
||||||
|
new position is empty, it moves to that position. If the new position
|
||||||
|
is occupied (by an asteroid!) the game ends.
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
if keystroke.name in ("KEY_LEFT", "KEY_RIGHT"):
|
||||||
|
if keystroke.name == "KEY_LEFT":
|
||||||
|
new_position = (x - 1, y)
|
||||||
|
else:
|
||||||
|
new_position = (x + 1, y)
|
||||||
|
if game.on_board(new_position):
|
||||||
|
if game.is_empty(new_position):
|
||||||
|
self.position = new_position
|
||||||
|
else:
|
||||||
|
self.explode()
|
||||||
|
game.end()
|
||||||
|
|
||||||
|
|
||||||
|
def explode(self):
|
||||||
|
"""Sets the ship's character to ``*`` and its color to red.
|
||||||
|
"""
|
||||||
|
self.color = "crimson_on_skyblue1"
|
||||||
|
self.character = '*'
|
||||||
|
|
||||||
|
|
||||||
|
class Asteroid:
|
||||||
|
"""When Asteroids are spawned, they fall down the screen until they
|
||||||
|
reach the bottom row and are removed.
|
||||||
|
An Asteroid's position is set when it is created.
|
||||||
|
Whenever an asteroid moves, it
|
||||||
|
checks whether it has it the ship.
|
||||||
|
"""
|
||||||
|
character = 'O'
|
||||||
|
color = "deepskyblue1_on_skyblue1"
|
||||||
|
|
||||||
|
def __init__(self, position):
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
"""Nothing happens unless
|
||||||
|
``game.turn_number`` is divisible by 2. The result is that asteroids
|
||||||
|
only move on even-numbered turns. If the asteroid is at the bottom of
|
||||||
|
the screen, it has run its course and should be removed from the game.
|
||||||
|
Otherwise, the asteroid's new position is one space down from its old
|
||||||
|
position. If the asteroid's new position is the same as the ship's
|
||||||
|
position, the game ends.
|
||||||
|
"""
|
||||||
|
if game.turn_number % 2 == 0:
|
||||||
|
self.set_color()
|
||||||
|
x, y = self.position
|
||||||
|
if y == HEIGHT - 1:
|
||||||
|
game.remove_agent(self)
|
||||||
|
else:
|
||||||
|
ship = game.get_agent_by_name('ship')
|
||||||
|
new_position = (x, y + 1)
|
||||||
|
if new_position == ship.position:
|
||||||
|
ship.explode()
|
||||||
|
game.end()
|
||||||
|
else:
|
||||||
|
self.position = new_position
|
||||||
|
|
||||||
|
|
||||||
|
def set_color(self):
|
||||||
|
"""To add to the game's drama, asteroids gradually become visible as they
|
||||||
|
fall down the screen. This method calculates the ratio of the asteroid's
|
||||||
|
position compared to the screen height--0 is the top of the screen and 1 is
|
||||||
|
the bottom ot the screen. Then sets the asteroid's color depending on the
|
||||||
|
ratio. (`Available colors <https://blessed.readthedocs.io/en/latest/colors.html>`_)
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
ratio = y / HEIGHT
|
||||||
|
if ratio < 0.2:
|
||||||
|
self.color = "deepskyblue1_on_skyblue1"
|
||||||
|
elif ratio < 0.4:
|
||||||
|
self.color = "deepskyblue2_on_skyblue1"
|
||||||
|
elif ratio < 0.6:
|
||||||
|
self.color = "deepskyblue3_on_skyblue1"
|
||||||
|
else:
|
||||||
|
self.color = "deepskyblue4_on_skyblue1"
|
||||||
|
|
||||||
|
|
||||||
|
class AsteroidSpawner:
|
||||||
|
"""An agent which is not displayed on the board, but which constantly spawns
|
||||||
|
asteroids.
|
||||||
|
"""
|
||||||
|
display = False
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
"""Adds 1 to the game score and then uses
|
||||||
|
:py:meth:`~retro.examples.nav.should_spawn_asteroid` to decide whether to
|
||||||
|
spawn an asteroid. When :py:meth:`~retro.examples.nav.should_spawn_asteroid`
|
||||||
|
comes back ``True``, creates a new instance of
|
||||||
|
:py:class:`~retro.examples.nav.Asteroid` at a random position along the
|
||||||
|
top of the screen and adds the asteroid to the game.
|
||||||
|
"""
|
||||||
|
game.state['score'] += 1
|
||||||
|
if self.should_spawn_asteroid(game.turn_number):
|
||||||
|
asteroid = Asteroid((randint(0, WIDTH - 1), 0))
|
||||||
|
game.add_agent(asteroid)
|
||||||
|
|
||||||
|
|
||||||
|
def should_spawn_asteroid(self, turn_number):
|
||||||
|
"""Decides whether to spawn an asteroid.
|
||||||
|
Uses a simple but effective algorithm to make the game get
|
||||||
|
progressively more difficult: choose a random number and return
|
||||||
|
``True`` if the number is less than the current turn number. At
|
||||||
|
the beginning of the game, few asteroids will be spawned. As the
|
||||||
|
turn number climbs toward 1000, asteroids are spawned almost
|
||||||
|
every turn.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
turn_number (int): The current turn in the game.
|
||||||
|
"""
|
||||||
|
return randint(0, 1000) < turn_number
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ship = Spaceship()
|
||||||
|
spawner = AsteroidSpawner()
|
||||||
|
game = Game(
|
||||||
|
[ship, spawner],
|
||||||
|
{"score": 0},
|
||||||
|
board_size=(WIDTH, HEIGHT),
|
||||||
|
color="deepskyblue4_on_skyblue1",
|
||||||
|
)
|
||||||
|
game.play()
|
||||||
203
snake.py
Normal file
203
snake.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
from random import randint
|
||||||
|
from retro.game import Game
|
||||||
|
|
||||||
|
class Apple:
|
||||||
|
"""An agent representing the Apple.
|
||||||
|
Note how Apple doesn't have ``play_turn`` or
|
||||||
|
``handle_keystroke`` methods: the Apple doesn't need to do
|
||||||
|
anything in this game. It just sits there waiting to get
|
||||||
|
eaten.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: "Apple"
|
||||||
|
character: '@'
|
||||||
|
color: "red_on_black" (`Here's documentation on how colors
|
||||||
|
work <https://blessed.readthedocs.io/en/latest/colors.html>`_
|
||||||
|
position: (0, 0). The Apple will choose a random position
|
||||||
|
as soon as the game starts, but it needs an initial
|
||||||
|
position to be assigned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = "Apple"
|
||||||
|
character = '@'
|
||||||
|
color = "red_on_black"
|
||||||
|
position = (0, 0)
|
||||||
|
|
||||||
|
def relocate(self, game):
|
||||||
|
"""Sets position to a random empty position. This method is
|
||||||
|
called whenever the snake's head touches the apple.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
game (Game): The current game.
|
||||||
|
"""
|
||||||
|
self.position = self.random_empty_position(game)
|
||||||
|
|
||||||
|
|
||||||
|
def random_empty_position(self, game):
|
||||||
|
"""Returns a randomly-selected empty position. Uses a very
|
||||||
|
simple algorithm: Get the game's board size, choose a
|
||||||
|
random x-value between 0 and the board width, and choose
|
||||||
|
a random y-value between 0 and the board height. Now use
|
||||||
|
the game to check whether any Agents are occupying this
|
||||||
|
position. If so, keep randomly choosing a new position
|
||||||
|
until the position is empty.
|
||||||
|
"""
|
||||||
|
bw, bh = game.board_size
|
||||||
|
occupied_positions = game.get_agents_by_position()
|
||||||
|
while True:
|
||||||
|
position = (randint(0, bw-1), randint(0, bh-1))
|
||||||
|
if position not in occupied_positions:
|
||||||
|
return position
|
||||||
|
|
||||||
|
|
||||||
|
class SnakeHead:
|
||||||
|
"""An Agent representing the snake's head. When the game starts, you control
|
||||||
|
the snake head using the arrow keys. The SnakeHead always has a direction, and
|
||||||
|
will keep moving in that direction every turn. When you press an arrow key,
|
||||||
|
you change the SnakeHead's direction.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name: "Snake head"
|
||||||
|
position: (0,0)
|
||||||
|
character: ``'v'`` Depending on the snake head's direction, its character
|
||||||
|
changes to ``'<'``, ``'^'``, ``'>'``, or ``'v'``.
|
||||||
|
next_segment: Initially ``None``, this is a reference to a SnakeBodySegment.
|
||||||
|
growing: When set to True, the snake will grow a new segment on its next move.
|
||||||
|
"""
|
||||||
|
RIGHT = (1, 0)
|
||||||
|
UP = (0, -1)
|
||||||
|
LEFT = (-1, 0)
|
||||||
|
DOWN = (0, 1)
|
||||||
|
name = "Snake head"
|
||||||
|
position = (0, 0)
|
||||||
|
direction = DOWN
|
||||||
|
character = 'v'
|
||||||
|
next_segment = None
|
||||||
|
growing = False
|
||||||
|
|
||||||
|
def play_turn(self, game):
|
||||||
|
"""On each turn, the snake head uses its position and direction to figure out
|
||||||
|
its next position. If the snake head is able to move there (it's on the board and
|
||||||
|
not occuppied by part of the snake's body), it moves.
|
||||||
|
|
||||||
|
Then, if the snake head is on the Apple, the Apple moves to a new random position
|
||||||
|
and ``growing`` is set to True.
|
||||||
|
|
||||||
|
Now we need to deal with two situations. First, if ``next_segment`` is not None, there is
|
||||||
|
a SnakeBodySegment attached to the head. We need the body to follow the head,
|
||||||
|
so we call ``self.next_segment.move``, passing the head's old position
|
||||||
|
(this will be the body's new position), a reference to the game, and a value for
|
||||||
|
``growing``. If the snake needs to grow, we need to pass this information along
|
||||||
|
the body until it reaches the tail--this is where the next segment will be attached.
|
||||||
|
|
||||||
|
If there is no ``next_segment`` but ``self.growing`` is True, it's time to add
|
||||||
|
a body! We set ``self.next_segment`` to a new SnakeBodySegment, set its
|
||||||
|
position to the head's old position, and add it to the game. We also add 1 to the
|
||||||
|
game's score.
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
dx, dy = self.direction
|
||||||
|
if self.can_move((x+dx, y+dy), game):
|
||||||
|
self.position = (x+dx, y+dy)
|
||||||
|
if self.is_on_apple(self.position, game):
|
||||||
|
apple = game.get_agent_by_name("Apple")
|
||||||
|
apple.relocate(game)
|
||||||
|
self.growing = True
|
||||||
|
if self.next_segment:
|
||||||
|
self.next_segment.move((x, y), game, growing=self.growing)
|
||||||
|
elif self.growing:
|
||||||
|
self.next_segment = SnakeBodySegment(1, (x, y))
|
||||||
|
game.add_agent(self.next_segment)
|
||||||
|
game.state['score'] += 1
|
||||||
|
self.growing = False
|
||||||
|
|
||||||
|
|
||||||
|
def handle_keystroke(self, keystroke, game):
|
||||||
|
"""Checks whether one of the arrow keys has been pressed.
|
||||||
|
If so, sets the SnakeHead's direction and character.
|
||||||
|
"""
|
||||||
|
x, y = self.position
|
||||||
|
if keystroke.name == "KEY_RIGHT":
|
||||||
|
self.direction = self.RIGHT
|
||||||
|
self.character = '>'
|
||||||
|
elif keystroke.name == "KEY_UP":
|
||||||
|
self.direction = self.UP
|
||||||
|
self.character = '^'
|
||||||
|
elif keystroke.name == "KEY_LEFT":
|
||||||
|
self.direction = self.LEFT
|
||||||
|
self.character = '<'
|
||||||
|
elif keystroke.name == "KEY_DOWN":
|
||||||
|
self.direction = self.DOWN
|
||||||
|
self.character = 'v'
|
||||||
|
|
||||||
|
|
||||||
|
def can_move(self, position, game):
|
||||||
|
on_board = game.on_board(position)
|
||||||
|
empty = game.is_empty(position)
|
||||||
|
on_apple = self.is_on_apple(position, game)
|
||||||
|
return on_board and (empty or on_apple)
|
||||||
|
|
||||||
|
def is_on_apple(self, position, game):
|
||||||
|
apple = game.get_agent_by_name("Apple")
|
||||||
|
return apple.position == position
|
||||||
|
|
||||||
|
|
||||||
|
class SnakeBodySegment:
|
||||||
|
"""Finally, we need an Agent for the snake's body segments.
|
||||||
|
SnakeBodySegment doesn't have ``play_turn`` or ``handle_keystroke`` methods because
|
||||||
|
it never does anything on its own. It only moves when the SnakeHead, or the previous
|
||||||
|
segment, tells it to move.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
segment_id (int): Keeps track of how far back this segment is from the head.
|
||||||
|
This is used to give the segment a unique name, and also to keep track
|
||||||
|
of how many points the player earns for eating the next apple.
|
||||||
|
position (int, int): The initial position.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
character: '*'
|
||||||
|
next_segment: Initially ``None``, this is a reference to a SnakeBodySegment
|
||||||
|
when this segment is not the last one in the snake's body.
|
||||||
|
|
||||||
|
"""
|
||||||
|
character = '*'
|
||||||
|
next_segment = None
|
||||||
|
|
||||||
|
def __init__(self, segment_id, position):
|
||||||
|
self.segment_id = segment_id
|
||||||
|
self.name = f"Snake body segment {segment_id}"
|
||||||
|
self.position = position
|
||||||
|
|
||||||
|
def move(self, new_position, game, growing=False):
|
||||||
|
"""When SnakeHead moves, it sets off a chain reaction, moving all its
|
||||||
|
body segments. Whenever the head or a body segment has another segment
|
||||||
|
(``next_segment``), it calls that segment's ``move`` method.
|
||||||
|
|
||||||
|
This method updates the SnakeBodySegment's position. Then, if
|
||||||
|
``self.next_segment`` is not None, calls that segment's ``move`` method.
|
||||||
|
If there is no next segment and ``growing`` is True, then we set
|
||||||
|
``self.next_segment`` to a new SnakeBodySegment in this segment's old
|
||||||
|
position, and update the game's score.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
new_position (int, int): The new position.
|
||||||
|
game (Game): A reference to the current game.
|
||||||
|
growing (bool): (Default False) When True, the snake needs to
|
||||||
|
add a new segment.
|
||||||
|
"""
|
||||||
|
old_position = self.position
|
||||||
|
self.position = new_position
|
||||||
|
if self.next_segment:
|
||||||
|
self.next_segment.move(old_position, game, growing=growing)
|
||||||
|
elif growing:
|
||||||
|
self.next_segment = SnakeBodySegment(self.segment_id + 1, old_position)
|
||||||
|
game.add_agent(self.next_segment)
|
||||||
|
game.state['score'] += self.segment_id + 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
head = SnakeHead()
|
||||||
|
apple = Apple()
|
||||||
|
game = Game([head, apple], {'score': 0}, board_size=(32, 16), framerate=12)
|
||||||
|
apple.relocate(game)
|
||||||
|
game.play()
|
||||||
Reference in New Issue
Block a user