Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
    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.')
Esempio n. 4
0
    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)
Esempio n. 5
0
    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.')
Esempio n. 6
0
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