def __init__(self, render_croppers=None, observation_cropper=None, n_actions=5, **renderer_kwargs): if render_croppers is None: render_croppers = [cropping.ObservationCropper()] self.render_croppers = render_croppers if observation_cropper is None: observation_cropper = cropping.ObservationCropper() self.observation_cropper = observation_cropper # Initialize the game inorder to get characters of the game game = self._init_game() self.observation_cropper.set_engine(game) # chars = set(game.things.keys()).union(game._backdrop.palette) chars = sorted(set(game.things.keys())) # Observation space is a 3D space where the depth is the number of # unqiue characters in the game map where as spatial dimensions are # number of rows and columns of the observation croper # (default: whole map). self.observation_space = spaces.Box( low=0, high=1, shape=(len(chars), self.observation_cropper.rows, self.observation_cropper.cols)) # Action space is a simple discrete space with 5 actions including # moving left, right, up, down, and no-ops self.action_space = spaces.Discrete(n_actions) self.to_feature = ObservationToFeatureArray(chars) # Define game atribute as None to check if reset is called before step self.game = None self.renderer_kwargs = renderer_kwargs
def __init__(self, rows, cols, delay, croppers = None, repainter = None, keys_to_actions = None, ): self.rows = rows self.cols = cols self.qt_renderer = None self._delay = delay self._repainter = repainter self._game = None self._keys_to_actions = keys_to_actions if croppers is None: self._croppers = [cropping.ObservationCropper()] else: self._croppers = croppers
def __init__(self, keys_to_actions, delay=None, repainter=None, colour_fg=None, colour_bg=None, croppers=None, target_sequence=None): """Construct and configure a `CursesUi` for pycolab games. A `CursesUi` object can be used over and over again to play any number of games implemented by using pycolab---however, the configuration options used to build the object will probably only be suitable for some. `CursesUi` provides colour support, but only in terminals that support true colour text colour attributes, and only when there is sufficient space in the terminal's colourmap, as well as a sufficient number of free "colour pairs", to accommodate all the characters given custom colours in `colour_fg` and `colour_bg`. Your terminal may be compatible out of the box; it may be compatible but only after setting the `$TERM` environment variable; or it may simply not work. Some specific guidance: To use this: do this: ------------ -------- iTerm2 nothing (should work out of the box) MacOS Terminal switch to iTerm2 (it doesn't really work) GNOME Terminal set $TERM to xterm-256color Modern xterm set $TERM to xterm-256color tmux, screen follow guidance at [1] ASR-33 buy a box of crayons Like any self-respecting game interface, `CursesUi` features a game console, which can be revealed and hidden via the Page Up and Page Down keys respectively. Log strings accumulated in the `Plot` object via its `log` method are shown in the console. [1]: https://sanctum.geek.nz/arabesque/256-colour-terminals/ Args: keys_to_actions: a dict mapping characters or key codes to whatever action symbols your pycolab games understand. For example, if your game uses the integer 5 to mean "shoot laser", you could map ' ' to 5 so that the player can shoot the laser by tapping the space bar. "Key codes" are the numerical ASCII (and other) codes that the Python `curses` library uses to mark keypresses besides letters, numbers, and punctuation; codes like `curses.KEY_LEFT` may be especially useful. For more on these, visit the [`curses` manual]( https://docs.python.org/2/library/curses.html#constants). Any character or key code received from the user but absent from this dict will be ignored. See also the note about `-1` in the documentation for the `delay` argument. delay: whether to timeout when retrieving a keypress from the user, and if so, for how long. If None, `CursesUi` will wait indefinitely for the user to press a key between game iterations; if positive, `CursesUi` will wait that many milliseconds. If the waiting operation times out, `CursesUi` will look up the keycode `-1` in the `keys_to_actions` dict and pass the corresponding action on to the game. So, if you use this delay option, make certain that you have an action mapped to the keycode `-1` in `keys_to_actions`; otherwise, the timeouts will be ignored, and `CursesUi` will behave as if `delay` were None. repainter: an optional `ObservationCharacterRepainter` that changes the characters used by the observation returned from the game engine, presumably to a character set that makes the observations match your preferred look for the game. colour_fg: an optional mapping from single ASCII character strings to 3-tuples (or length 3 lists) encoding an RGB colour. These colours are used (in compatible terminals only) as the foreground colour when printing those characters. If unspecified for a particular character, or if None, some boring default colour is used instead. *NOTE: RGB component values range in `[0,999]`, not `[0,255]` as you may have expected.* colour_bg: exactly like `colour_fg`, but for charcter background colours. If unspecified, the same colours specified by `colour_fg` (or its defaults) are used instead (so characters basically become tall rectangular pixels). croppers: None, or a list of `cropping.ObservationCropper` instances and/or None values. If a list of `ObservationCropper`s, each cropper in the list will make its own crop of the observation, and the cropped observations will all be shown side-by-side. A None value in the list means observations returned by the pycolab game supplied to the `play` method should be shown directly instead of cropped. A single None value for this argument is a shorthand for `[None]`. Raises: TypeError: if any key in the `keys_to_actions` dict is neither a numerical keycode nor a length-1 ASCII string. """ # This slot holds a reference to the game currently being played, or None # if no game is being played at the moment. self._game = None # What time did the game start? Or None if there is no game being played. self._start_time = None # What is our total so far? Or None if there is no game being played. self._total_return = None # For displaying messages logged by game entities in a game console. self._log_messages = [] # The agent that'll take all the action self.agent = AgentNetwork(target_sequence=target_sequence) # The curses `getch` routine returns numeric keycodes, but users can specify # keyboard input as strings as well, so we convert strings to keycodes. try: self._keycodes_to_actions = { ord(key) if isinstance(key, str) else key: action for key, action in six.iteritems(keys_to_actions) } self._action_list = list(self._keycodes_to_actions.values()) except TypeError: raise TypeError( 'keys in the keys_to_actions argument must either be ' 'numerical keycodes or single ASCII character strings.') # We'd like to see whether the user is using any reserved keys here in the # constructor, but we have to wait until curses is actually running to do # that. So, the reserved key check happens in _init_curses_and_play. # Save colour mappings and other parameters from the user. Note injection # of defaults and the conversion of character keys to ASCII codepoints. self._delay = delay self._repainter = repainter self._colour_fg = ({ ord(char): colour for char, colour in six.iteritems(colour_fg) } if colour_fg is not None else {}) self._colour_bg = ({ ord(char): colour for char, colour in six.iteritems(colour_bg) } if colour_bg is not None else self._colour_fg) # This slot will hold a mapping from characters to the curses colour pair # we'll use when we're displaying that character. None for now, since we # can't set it up until curses is running. self._colour_pair = None # If the user specified no croppers or any None croppers, replace them with # pass-through croppers that don't do any cropping. if croppers is None: self._croppers = [cropping.ObservationCropper()] else: self._croppers = croppers try: self._croppers = tuple( cropping.ObservationCropper() if c is None else c for c in self._croppers) except TypeError: raise TypeError( 'The croppers argument to the CursesUi constructor must ' 'be a sequence or None, not a "bare" object.')
def assertMachinima(self, engine, frames, pre_updates=None, post_updates=None, result_checker=None, croppers=None): """Assert that gameplay produces a "movie" of expected observations. [Machinima](https://en.wikipedia.org/wiki/Machinima) is the creation of movies with game engines. This test method allows you to demonstrate that a sequence of canned actions would produce a sequence of observations. Other tests and behaviours may be imposed on the `Sprite`s and `Drape`s in the sequence as well. Args: engine: a pycolab game engine whose `its_showtime` method has already been called. Note: if you are using croppers, you may want to supply the observation result from `its_showtime` to the croppers so that they will have a chance to see the first observation in the same way they do in the `CursesUi`. frames: a sequence of n-tuples, where `n >= 2`. The first element in each tuple is the action that should be submitted to `engine` via the `play` method; the second is an ASCII-art diagram (see `assertBoard`) portraying the observation we expect the `play` method to return, or a list of such diagrams if the `croppers` argument is not None (see below). Any further elements of the tuple are stored in the engine's `Plot` object under the key `'machinima_args'`. These are commonly used to pass expected values to `assertEqual` tests to callables provided via `pre_updates` and `post_updates`. pre_updates: optional dict mapping single-character strings (which should correspond to `Sprite`s and `Drape`s that inherit from test classes in this module) to a callable that is injected into the entity via `pre_update` at each game iteration. These callables are usually used to specify additional testing asserts. post_updates: optional dict mapping single-character strings (which should correspond to `Sprite`s and `Drape`s that inherit from test classes in this module) to a callable that is injected into the entity via `post_update` at each game iteration. These callables are usually used to specify additional testing asserts. result_checker: optional callable that, at every game iteration, receives arguments `observation`, `reward`, `discount`, and `args`. The first three are the return values of the engine's `play` method; `args` is the `machinima_args` elements for that game iteration (see `frames`). The `observation` is the original game engine observation; `result_checker` does not receive the output of any of the `croppers`. (If you need to check cropped observations, consider passing your croppers to `result_checker` via `machinima_args` and cropping the observation yourself.) croppers: None, or a list of `cropping.ObservationCropper` instances and/or None values. If None, then `frames[i][1]` should be an ASCII-art diagram to compare against frames as emitted by the engine; if a list, then `frames[i][1]` should be a list of diagrams to compare against the outputs of each of the croppers. A None value in `croppers` is a "null" or "pass-through" cropper: the corresponding entry in `frames[i][1]` should expect the original game engine observation. NB: See important usage note in the documentation for the `engine` arg. Raises: AssertionError: an observation produced by the game engine does not match one of the observation art diagrams in `frames`. ValueError: if croppers is non-None and the number of `ObservationCropper`s it contains differs from the number of ASCII-art diagrams in one of the elements of `frames`. """ if pre_updates is None: pre_updates = {} if post_updates is None: post_updates = {} # If we have croppers, replace None values with pass-through croppers, then # tell all croppers which game we're playing. if croppers is not None: try: croppers = tuple( cropping.ObservationCropper() if c is None else c for c in croppers) except TypeError: raise TypeError( 'The croppers argument to assertMachinima must be a ' 'sequence or None, not a "bare" object.') for cropper in croppers: cropper.set_engine(engine) # Step through the game and verify expected results at each frame. for i, frame in enumerate(frames): action = frame[0] art = frame[1] args = frame[2:] engine.the_plot['machinima_args'] = args for character, thing_to_do in six.iteritems(pre_updates): pre_update(engine, character, thing_to_do) for character, thing_to_do in six.iteritems(post_updates): post_update(engine, character, thing_to_do) observation, reward, discount = engine.play(action) if croppers is None: self.assertBoard( observation.board, art, err_msg='Frame {} observation mismatch'.format(i)) else: # It will be popular to construct iterables of ASCII art using zip (as # shown in cropping_test.py); we graciously convert art to a tuple, # since the result of Python 3's zip does not support len(). art = tuple(art) if len(art) != len(croppers): raise ValueError( 'Frame {} in the call to assertMachinima has {} ASCII-art diagrams ' 'for {} croppers. These counts should be the same.'. format(i, len(art), len(croppers))) for j, (cropped_art, cropper) in enumerate(zip(art, croppers)): self.assertBoard( cropper.crop(observation).board, cropped_art, err_msg='Frame {}, crop {} observation mismatch'. format(i, j)) if result_checker is not None: result_checker(observation, reward, discount, args)
def __init__(self, rand_to_actions, delay=None, repainter=None, colour_fg=None, colour_bg=None, croppers=None): # This slot holds a reference to the game currently being played, or None # if no game is being played at the moment. self._game = None # What time did the game start? Or None if there is no game being played. self._start_time = None # What is our total so far? Or None if there is no game being played. self._total_return = None # For displaying messages logged by game entities in a game console. self._log_messages = [] # The curses `getch` routine returns numeric keycodes, but users can specify # keyboard input as strings as well, so we convert strings to keycodes. self._randcodes_to_actions = { key: action for key, action in six.iteritems(rand_to_actions) } # We'd like to see whether the user is using any reserved keys here in the # constructor, but we have to wait until curses is actually running to do # that. So, the reserved key check happens in _init_curses_and_play. # Save colour mappings and other parameters from the user. Note injection # of defaults and the conversion of character keys to ASCII codepoints. self._delay = delay self._repainter = repainter self._colour_fg = ({ ord(char): colour for char, colour in six.iteritems(colour_fg) } if colour_fg is not None else {}) self._colour_bg = ({ ord(char): colour for char, colour in six.iteritems(colour_bg) } if colour_bg is not None else self._colour_fg) # This slot will hold a mapping from characters to the curses colour pair # we'll use when we're displaying that character. None for now, since we # can't set it up until curses is running. self._colour_pair = None # If the user specified no croppers or any None croppers, replace them with # pass-through croppers that don't do any cropping. if croppers is None: self._croppers = [cropping.ObservationCropper()] else: self._croppers = croppers try: self._croppers = tuple( cropping.ObservationCropper() if c is None else c for c in self._croppers) except TypeError: raise TypeError( 'The croppers argument to the CursesUi constructor must ' 'be a sequence or None, not a "bare" object.')
def _check_and_normalise_story_init_args(chapters, first_chapter, croppers): """Helper: check and normalise arguments for `Story.__init__`. Args: chapters: as in `Story.__init__`. first_chapter: as in `Story.__init__`. croppers: as in `Story.__init__`. Returns: a 2-tuple with the following members: [0]: A shallow copy of the contents of `chapters` into a dict. If `chapters` was a list, the resulting dict will have keys 0..`len(chapters)-1`. [1]: A normalised version of `croppers`: always the same structure as the first tuple element, with each value a `cropping.ObservationCropper`; if no cropping was desired for a game, "no-op" croppers are supplied. Raises: ValueError: any of several argument check failures. See the error messages themselves for details. """ # All this checking aims to be instructive to users, but is a bit verbose for # inclusion in the class constructor itself. if not chapters: raise ValueError( 'The chapters argument to the Story constructor must not be empty.' ) # First, if the `chapters` argument is a list or tuple, convert it into a # dict, and convert a list/tuple `croppers` argument into a dict as well. if isinstance(chapters, collections.Sequence): chapters = dict(enumerate(chapters)) if isinstance(croppers, collections.Sequence): croppers = dict(enumerate(croppers)) if not isinstance(chapters, collections.Mapping): raise ValueError( 'The chapters argument to the Story constructor must be either a dict ' 'or a list.') if None in chapters: raise ValueError('None may not be a key in a Story chapters dict.') if first_chapter not in chapters: raise ValueError( 'The key "{}", specified as a Story\'s first_chapter, does not appear in ' 'the chapters supplied to the Story constructor.'.format( first_chapter)) # Normalise croppers argument into a dict of croppers. Note that # cropping.ObservationCropper is a "null cropper" that makes no changes. if croppers is None: croppers = cropping.ObservationCropper() if isinstance(croppers, cropping.ObservationCropper): croppers = {k: croppers for k in chapters.keys()} if (not isinstance(croppers, collections.Mapping) or set(chapters.keys()) != set(croppers.keys())): raise ValueError( 'Since the croppers argument to the Story constructor was not None ' 'or a single ObservationCropper, it must be a collection with the ' 'same keys or indices as the chapters argument.') croppers = { k: cropping.ObservationCropper() if c is None else c for k, c in croppers.items() } # Normalise chapters to be a dict; croppers already is. chapters = dict(chapters) return chapters, croppers