lab_retro/retro/game.py

217 lines
7.1 KiB
Python

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