generated from mwc/lab_retro
217 lines
7.1 KiB
Python
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
|
|
|
|
|
|
|