def _do_test_reward_and_episode_end(self, q_pre_update, expected_discount): """Core implementation of `testRewardAndEpisodeEndWith*Discount` tests. Args: q_pre_update: Pre-`update` code to inject for the Q sprite. expected_discount: `discount` we expect to observe after the final game step. """ # Not helpful in this test, since argument lists are long: # pylint: disable=g-long-lambda # Our test takes place in this tiny world: art = ['.........', '...Q.R...', '.........'] # Here we make the game. engine = ascii_art.ascii_art_to_game( art=art, what_lies_beneath='.', # Q and R are sprites. sprites=dict(Q=tt.TestSprite, R=tt.TestSprite), # We set a fixed update schedule for deterministic tests. update_schedule='QR') ### GAME ITERATION #0. Nothing happens. No entity has issued a reward, so ### the reward is None. unused_observation, reward, discount = engine.its_showtime() self.assertIsNone(reward) self.assertEqual(discount, 1.0) self.assertFalse(engine.game_over) ### GAME ITERATION #1. Have the sprites credit us with some reward. Note ### how reward is accumulated across all entities. tt.pre_update( engine, 'Q', lambda actions, board, layers, backdrop, things, the_plot: (the_plot.add_reward('pyco'))) tt.pre_update( engine, 'R', lambda actions, board, layers, backdrop, things, the_plot: (the_plot.add_reward('lab!'))) unused_observation, reward, discount = engine.play('mound of beans') self.assertEqual(reward, 'pycolab!') self.assertEqual(discount, 1.0) self.assertFalse(engine.game_over) ### GAME ITERATION #2. Have Q call the whole thing off. tt.pre_update(engine, 'Q', q_pre_update) tt.pre_update( engine, 'R', lambda actions, board, layers, backdrop, things, the_plot: (the_plot.add_reward('trousers'))) unused_observation, reward, discount = engine.play('mound of beans') self.assertEqual(reward, 'trousers') self.assertEqual(discount, expected_discount) self.assertTrue(engine.game_over)
def testChangingZOrdering(self): """Game entities can change the Z-ordering.""" # Not helpful in this test, since argument lists are long: # pylint: disable=g-long-lambda # Our test takes place in this very tiny world: art = ['.abc.'] # Here we make the game. engine = ascii_art.ascii_art_to_game( art=art, what_lies_beneath='.', # a, b, and c are sprites. sprites=dict(a=ascii_art.Partial(tt.TestMazeWalker, impassable=''), b=ascii_art.Partial(tt.TestMazeWalker, impassable=''), c=ascii_art.Partial(tt.TestMazeWalker, impassable='')), # Note this initial z-ordering. z_order='abc') ### GAME ITERATION #0. Nothing happens; we just get the game started. engine.its_showtime() ### GAME ITERATION #1. All of our sprites move to stand on top of one ### another. No Z-order change yet. observation, unused_reward, unused_discount = engine.play({ 'a': 'e', 'c': 'w' }) self.assertBoard(observation.board, ['..c..']) ### GAME ITERATION #2. b moves in front of c. Z-ordering should be 'acb'. tt.pre_update( engine, 'b', lambda actions, board, layers, backdrop, things, the_plot: (the_plot.change_z_order('b', 'c'))) observation, unused_reward, unused_discount = engine.play(None) self.assertBoard(observation.board, ['..b..']) ### GAME ITERATION #2. c moves to the back. Z-ordering should be 'cab'. tt.pre_update( engine, 'c', lambda actions, board, layers, backdrop, things, the_plot: (the_plot.change_z_order('c', None))) observation, unused_reward, unused_discount = engine.play(None) self.assertBoard(observation.board, ['..b..']) ### GAME ITERATION #3. b moves to the back. Z-ordering should be 'bca'. tt.pre_update( engine, 'b', lambda actions, board, layers, backdrop, things, the_plot: (the_plot.change_z_order('b', None))) observation, unused_reward, unused_discount = engine.play(None) self.assertBoard(observation.board, ['..a..'])
def testScrolly(self): """Verify correct interoperation of `Scrolly` and `MazeWalker`.""" # Not helpful in this test, since argument lists are long: # pylint: disable=g-long-lambda # The test takes place in this scrolling world. maze_art = [ '########################', '# # # #', '# # ###### # ### # # #', '# # # # # # # #', '######## +#### ### # # #', # Note top-left corner. '# # # # #N # #', # A non-player character. '# # ###### # # #########', '# #P #', # The player. '# #################### #', '# #', '########################' ] board_art = [ '~~~~~', # The maze art will drape across this board art. '~~~~~', '~~~~~', '~~~~~', '~~~~~' ] # Build and test the helper object that helps us construct a Scrolly object. scrolly_info = prefab_drapes.Scrolly.PatternInfo( maze_art, board_art, board_northwest_corner_mark='+', what_lies_beneath='#') self.assertEqual((5, 5), scrolly_info.kwargs('#')['board_shape']) self.assertEqual((4, 9), scrolly_info.kwargs('#')['board_northwest_corner']) np.testing.assert_array_equal( scrolly_info.kwargs('#')['whole_pattern'], np.array([ list(row) for row in [ '111111111111111111111111', '100000000001000000010001', '101011111101000111010101', '101000000100010001010101', '111111110111110111010101', '101000000100010100000101', '101011111101010111111111', '100000000001000000000001', '101111111111111111111101', '100000000000000000000001', '111111111111111111111111' ] ]).astype(bool)) # Here we make the game. In this game, scrolling will only occur if it looks # like we are getting to within one pixel of the edge of the game board. engine = ascii_art.ascii_art_to_game( art=board_art, what_lies_beneath='~', # N and P are MazeWalkers, and P is an egocentric scroller. sprites=dict(N=ascii_art.Partial(tt.TestMazeWalker, impassable=''), P=ascii_art.Partial(tt.TestMazeWalker, impassable='#N', egocentric_scroller=True)), # The world itself is a Scrolly drape with narrow margins. drapes={ '#': ascii_art.Partial(tt.TestScrolly, scroll_margins=(1, 1), **scrolly_info.kwargs('#')) }, # The update schedule is in a separate and later update group from the # group containing the pycolab entities that control non-traversable # characters (i.e. the wall, '#'). update_schedule=[['#'], ['N', 'P']]) # Go ahead and start the game. We will take this opportunity to teleport the # two sprites into their proper locations in the scrolling world. This is # often something you take care of in your Sprite's constructor, and doing # it this way is not encouraged at all. tt.pre_update( engine, 'N', thing_to_do=( lambda actions, board, layers, backdrop, things, the_plot: (things['N']._teleport(scrolly_info.virtual_position('N'))))) tt.pre_update( engine, 'P', thing_to_do=( lambda actions, board, layers, backdrop, things, the_plot: (things['P']._teleport(scrolly_info.virtual_position('P'))))) engine.its_showtime() # We will soon want to execute a sequence of motions that the player Sprite # P and the moving Scrolly drape # should pay attention to, but the # non-player Sprite N should ignore. We can send motion commands just to P # and # by using "dict format" actions for TestMazeWalker and TestScrolly. go = lambda direction: {'P': direction, '#': direction} # Now we execute a sequence of motions, comparing the actual observations # against ASCII-art depictions of our expected observations. self.assertMachinima( engine=engine, frames=[ # Let's start by doing nothing and seeing that the world looks as # we expect. (None, ['#####', '#~~~#', '#~#~#', '~~#P~', '#####']), # Let's try to go in directions that are blocked off and verify that # we stay put. (go('w'), ['#####', '#~~~#', '#~#~#', '~~#P~', '#####']), (go('sw'), ['#####', '#~~~#', '#~#~#', '~~#P~', '#####']), (go('s'), ['#####', '#~~~#', '#~#~#', '~~#P~', '#####']), # Now let's try to go drive by our friend N. (go('e'), ['####~', '~~~#~', '~#~#~', '~#~P~', '#####']), (go('e'), ['###~#', '~~#~#', '#~#~#', '#~~P~', '#####']), (go('e'), ['##~##', '~#~#N', '~#~##', '~~~P~', '#####']), (go('nw'), ['##~##', '~#~#N', '~#P##', '~~~~~', '#####']), (go('n'), ['##~##', '~#P#N', '~#~##', '~~~~~', '#####']), (go('n'), ['~#~~~', '##P##', '~#~#N', '~#~##', '~~~~~']), # And back to our starting place. The 'sw' move in particular is # tricky, since it only requires a scroll in one dimension, even # though the Sprite is moving in two dimensions at once. (go('s'), ['~#~~~', '##~##', '~#P#N', '~#~##', '~~~~~']), (go('s'), ['~#~~~', '##~##', '~#~#N', '~#P##', '~~~~~']), (go('sw'), ['##~##', '~#~#N', '~#~##', '~P~~~', '#####']), (go('w'), ['###~#', '~~#~#', '#~#~#', '#P~~~', '#####']), # Now exercise that same logic again along the horizontal axis (at # the "se" move in particular). (go('e'), ['###~#', '~~#~#', '#~#~#', '#~P~~', '#####']), (go('ne'), ['###~#', '~~#~#', '#~#P#', '#~~~~', '#####']), (go('se'), ['##~##', '~#~#N', '~#~##', '~~~P~', '#####']), ], ) # Now let's start over with a new game. In this next game, we set no margins # at all, so scrolling the world is mandatory at each game iteration. engine = ascii_art.ascii_art_to_game( art=board_art, what_lies_beneath='~', # N and P are MazeWalkers, and P is an egocentric scroller. sprites=dict(N=ascii_art.Partial(tt.TestMazeWalker, impassable=''), P=ascii_art.Partial(tt.TestMazeWalker, impassable='#N', egocentric_scroller=True)), # The world itself is a Scrolly drape with narrow margins. drapes={ '#': ascii_art.Partial(tt.TestScrolly, scroll_margins=None, **scrolly_info.kwargs('#')) }, # The update schedule is in a separate and later update group from the # group containing the pycolab entities that control non-traversable # characters (i.e. the wall, '#'). update_schedule=[['#'], ['N', 'P']]) # Again we use our start-of-game "teleportation trick", which is not really # appropriate for anything but tests, and even then it's pushing it... tt.pre_update( engine, 'N', thing_to_do=( lambda actions, board, layers, backdrop, things, the_plot: (things['N']._teleport(scrolly_info.virtual_position('N'))))) tt.pre_update( engine, 'P', thing_to_do=( lambda actions, board, layers, backdrop, things, the_plot: (things['P']._teleport(scrolly_info.virtual_position('P'))))) engine.its_showtime() # Here is a sequence of motions and the observations we expect to see. self.assertMachinima( engine=engine, frames=[ # Let's start by doing nothing and seeing that the world looks as # we expect. (None, ['#####', '#~~~#', '#~#~#', '~~#P~', '#####']), # As before, let's try to go in directions that are blocked off and # verify that we stay put. (go('w'), ['#####', '#~~~#', '#~#~#', '~~#P~', '#####']), (go('sw'), ['#####', '#~~~#', '#~#~#', '~~#P~', '#####']), (go('s'), ['#####', '#~~~#', '#~#~#', '~~#P~', '#####']), # We're going to head for the western edge of the map. First we need # to head over this short vertically-oriented wall... (go('n'), ['#~~~#', '#####', '#~~~#', '#~#P#', '~~#~~']), (go('nw'), ['##~#~', '~#~~~', '~####', '~#~P~', '##~#~']), (go('sw'), ['~~#~~', '#~###', '~~#~~', '###P#', '~~~~#']), (go('sw'), ['##~##', '~~~#~', '####~', '~~~P~', '#####']), # Now, westward ho! An interesting thing will happen when the # scrolling world runs out of scenery---the sprite will actually # have to change its location on the game board. (go('w'), ['###~#', '~~~~#', '#####', '~~~P~', '#####']), (go('w'), ['####~', '~~~~~', '#####', '~~~P~', '#####']), (go('w'), ['#####', '~~~~~', '~####', '~~~P~', '#####']), (go('w'), ['#####', '#~~~~', '#~###', '~~~P~', '#####']), (go('w'), ['#####', '~#~~~', '~#~##', '~~~P~', '~####']), (go('w'), ['#####', '#~#~~', '#~#~#', '#~~P~', '#~###']), ( go( 'w' ), # Behold: eppur non si muove. The world is stuck, and [ '#####', # the Sprite is forced to move around inside if it. '#~#~~', '#~#~#', '#~P~~', '#~###' ]), # Now, what happens if we try to go southwest? The board can (and # should) scroll vertically to keep the Sprite on the same row, but # it still can't scroll horizontally. (go('sw'), ['#~#~~', '#~#~#', '#~~~~', '#P###', '#~~~~']), # Now we head up and back east, and then that's about enough. In # both of these motions, the world can scroll around the Sprite, so # it's back to business as usual. (go('ne'), ['#####', '~#~~~', '~#~##', '~P~~~', '~####']), (go('e'), ['#####', '#~~~~', '#~###', '~P~~~', '#####']), ], )
def testUpdateScheduleAndZOrder(self): """The engine abides by the update schedule and the Z-ordering.""" # Our test takes place in this 3x5 world... art = ['.acb.', '..c..', '.dce.'] # Here we make the game. engine = ascii_art.ascii_art_to_game( art=art, what_lies_beneath='.', # a, b, c, and d are sprites. sprites=dict(a=ascii_art.Partial(tt.TestMazeWalker, impassable=''), b=ascii_art.Partial(tt.TestMazeWalker, impassable=''), d=ascii_art.Partial(tt.TestMazeWalker, impassable=''), e=ascii_art.Partial(tt.TestMazeWalker, impassable='')), # c is a drape that just sits there; Z is an invisible drape that # also just sits there. drapes=dict(c=tt.TestDrape, Z=tt.TestDrape), # This update schedule means that in a single game iteration: # 1. the Backdrop, a, and b all update, then the board is re-rendered, # 2. c updates, then the board is re-rendered, # 3. d and e update, then the board is re-rendered, # 4. Z updates, then the board is re-rendered. update_schedule=[['a', 'b'], ['c'], ['d', 'e'], ['Z']], # The Z-ordering says to render the entities in this order, from # back to front. z_order='Zabcde') ### GAME ITERATION #0. During the first update sweep, since none of the ### Sprites change their locations and none of the Drapes change their ### curtains, all entities will see the initial rendering of the board. tt.pre_update(engine, 'a', self.expectBoard(art, err_msg='a @ 0')) tt.pre_update(engine, 'b', self.expectBoard(art, err_msg='b @ 0')) tt.pre_update(engine, 'c', self.expectBoard(art, err_msg='c @ 0')) tt.pre_update(engine, 'd', self.expectBoard(art, err_msg='d @ 0')) tt.pre_update(engine, 'e', self.expectBoard(art, err_msg='e @ 0')) tt.pre_update(engine, 'Z', self.expectBoard(art, err_msg='Z @ 0')) observation, unused_reward, discount = engine.its_showtime() # Check that the observation is right and that discount is 1. self.assertBoard(observation.board, art, err_msg='obs @ 0') self.assertEqual(discount, 1.0) # Check that miscellaneous properties work. self.assertEqual(engine.rows, 3) self.assertEqual(engine.cols, 5) self.assertEqual(engine.z_order, ['Z', 'a', 'b', 'c', 'd', 'e']) self.assertSetEqual(set(engine.things.keys()), {'a', 'b', 'c', 'd', 'e', 'Z'}) self.assertIn('.', engine.backdrop.palette) ### GAME ITERATION #1. All sprites take a step to the right. As the ### update sweep takes place, the segmented update schedule causes ### different entities to see the board in different configurations. # a and b see the board as it was rendered after the last iteration. tt.pre_update(engine, 'a', self.expectBoard(art, err_msg='a @ 1')) tt.pre_update(engine, 'b', self.expectBoard(art, err_msg='b @ 1')) # c sees the board after a and b have moved right, but not d and e. Note # the Z-ordering determining how c and d overlap. tt.pre_update( engine, 'c', self.expectBoard(['..c.b', '..c..', '.dce.'], err_msg='c @ 1')) ## d and e see the board after c's update, but of course c didn't change... tt.pre_update( engine, 'd', self.expectBoard(['..c.b', '..c..', '.dce.'], err_msg='d @ 1')) tt.pre_update( engine, 'e', self.expectBoard(['..c.b', '..c..', '.dce.'], err_msg='e @ 1')) # Z sees the board after everyone else has moved. tt.pre_update( engine, 'Z', self.expectBoard(['..c.b', '..c..', '..d.e'], err_msg='Z @ 1')) observation, unused_reward, unused_discount = engine.play('e') # Check that the observation is right and that discount is 1. self.assertBoard(observation.board, ['..c.b', '..c..', '..d.e'], err_msg='obs @ 1') self.assertEqual(discount, 1.0) ### GAME ITERATION #2. All sprites take a step to the left. We'll trust ### that this took place in the expected order and just check that the ### observation is correct. observation, unused_reward, unused_discount = engine.play('w') self.assertBoard(observation.board, art, err_msg='obs @ 2') self.assertEqual(discount, 1.0) ### GAME ITERATION #3. All sprites take another step to the left. We ### check everything again, this time. # First update group. tt.pre_update(engine, 'a', self.expectBoard(art, err_msg='a @ 3')) tt.pre_update(engine, 'b', self.expectBoard(art, err_msg='b @ 3')) # Second update group. tt.pre_update( engine, 'c', self.expectBoard(['a.c..', '..c..', '.dce.'], err_msg='c @ 3')) # Second update group. tt.pre_update( engine, 'd', self.expectBoard(['a.c..', '..c..', '.dce.'], err_msg='d @ 3')) tt.pre_update( engine, 'e', self.expectBoard(['a.c..', '..c..', '.dce.'], err_msg='e @ 3')) observation, unused_reward, unused_discount = engine.play('w') # Check that the observation is right and that discount is 1. self.assertBoard(observation.board, ['a.c..', '..c..', 'd.e..'], err_msg='obs @ 3') self.assertEqual(discount, 1.0)
def testPlotStateVariables(self): """State variables inside the Plot are updated correctly.""" # Our test takes place in this very tiny world: art = ['.abc.'] # Here we make the game. engine = ascii_art.ascii_art_to_game( art=art, what_lies_beneath='.', # a, b, and c are sprites. sprites=dict(a=tt.TestSprite, b=tt.TestSprite, c=tt.TestSprite), # We will test to see that these update groups are reflected in the # update_group property of the Plot. The ascii_art_to_game function # comes up with its own names for update groups, though, and those are # off limits to us, so we can't just check values directly... update_schedule=[['a', 'b'], ['c']]) # ...so, we will store game iterations and update group values in this # dict, and check that all is as expected. state_info = [] def add_state_info(actions, board, layers, backdrop, things, the_plot): del actions, board, layers, backdrop, things # Unused. state_info.append((the_plot.frame, the_plot.update_group)) ### GAME ITERATION #0. tt.pre_update(engine, 'a', add_state_info) tt.pre_update(engine, 'b', add_state_info) tt.pre_update(engine, 'c', add_state_info) engine.its_showtime() [(a_frame, a_update_group), (b_frame, b_update_group), (c_frame, c_update_group)] = state_info[:] self.assertEqual([0, 0, 0], [a_frame, b_frame, c_frame]) self.assertEqual(a_update_group, b_update_group) self.assertNotEqual(a_update_group, c_update_group) ### GAME ITERATION #1. tt.pre_update(engine, 'a', add_state_info) tt.pre_update(engine, 'b', add_state_info) tt.pre_update(engine, 'c', add_state_info) engine.play('↑↑↓↓←→←→BA★') [(a_frame, a_new_update_group), (b_frame, b_new_update_group), (c_frame, c_new_update_group)] = state_info[3:] # note 3: self.assertEqual([1, 1, 1], [a_frame, b_frame, c_frame]) self.assertEqual(a_update_group, a_new_update_group) self.assertEqual(b_update_group, b_new_update_group) self.assertEqual(c_update_group, c_new_update_group)