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 `_. :: # 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