Skip to content

Commit

Permalink
Improve menu navigation & eliminate ghost on retry (#36)
Browse files Browse the repository at this point in the history
* Refactor menu navigation

* Change post-race retry to avoid ghost

When doing a retry, the subsequent race will be against the ghost character. Doing a course 'change' rather than a retry avoids this (see #25)

* Improvements for PR #36
  • Loading branch information
bzier authored Dec 29, 2017
1 parent 944bd48 commit ce6de44
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 89 deletions.
190 changes: 103 additions & 87 deletions gym_mupen64plus/envs/MarioKart64/mario_kart_env.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import abc
import inspect
import itertools
import os
import yaml
from termcolor import cprint
Expand Down Expand Up @@ -49,16 +50,18 @@ def _reset(self):
if self.reset_count > 0:

if self.episode_over:
self._wait(count=59)
self._navigate_post_race_menu()
self._wait(count=40, wait_for='map select screen')
self._navigate_map_select()
self._wait(count=50, wait_for='race to load')
self.episode_over = False
else:
self.controller_server.send_controls(ControllerState.NO_OP, start_button=1)
self.controller_server.send_controls(ControllerState.NO_OP)
self.controller_server.send_controls(ControllerState.JOYSTICK_DOWN)
self.controller_server.send_controls(ControllerState.NO_OP)
self.controller_server.send_controls(ControllerState.A_BUTTON)
for i in range(77):
self.controller_server.send_controls(ControllerState.NO_OP)
self._act(ControllerState.NO_OP)
self._press_button(ControllerState.JOYSTICK_DOWN)
self._press_button(ControllerState.A_BUTTON)
self._wait(count=76, wait_for='race to load')


return super(MarioKartEnv, self)._reset()
Expand Down Expand Up @@ -147,92 +150,105 @@ def _evaluate_end_state(self):
return False

def _navigate_menu(self):
frame = 0
self._wait(count=10, wait_for='Nintendo screen')
self._press_button(ControllerState.A_BUTTON)

self._wait(count=68, wait_for='Mario Kart splash screen')
self._press_button(ControllerState.A_BUTTON)

self._wait(count=68, wait_for='Game Select screen')
self._navigate_game_select()

self._wait(count=14, wait_for='Player Select screen')
self._navigate_player_select()

self._wait(count=31, wait_for='Map Select screen')
self._navigate_map_select()

self._wait(count=50, wait_for='race to load')

def _navigate_game_select(self):
# Select number of players (1 player highlighted by default)
self._press_button(ControllerState.A_BUTTON)
self._wait(count=3, wait_for='animation')

# Select GrandPrix or TimeTrials (GrandPrix highlighted by default - down to switch to TimeTrials)
self._press_button(ControllerState.JOYSTICK_DOWN)
self._wait(count=3, wait_for='animation')

# Select TimeTrials
self._press_button(ControllerState.A_BUTTON)

# Select Begin
self._press_button(ControllerState.A_BUTTON)

# Press OK
self._press_button(ControllerState.A_BUTTON)

def _navigate_player_select(self):
cur_row = 0
cur_col = 0
print('Player row: ' + str(self.PLAYER_ROW))
print('Player col: ' + str(self.PLAYER_COL))

if cur_row != self.PLAYER_ROW:
self._press_button(ControllerState.JOYSTICK_DOWN)
cur_row += 1

while cur_col != self.PLAYER_COL:
self._press_button(ControllerState.JOYSTICK_RIGHT)
cur_col += 1

# Select character
self._press_button(ControllerState.A_BUTTON)

# Press OK
self._press_button(ControllerState.A_BUTTON)

def _navigate_map_select(self):
cur_row = 0
cur_col = 0
print('Map series: ' + str(self.MAP_SERIES))
print('Map choice: ' + str(self.MAP_CHOICE))

# Select map series
while cur_col != self.MAP_SERIES:
self._press_button(ControllerState.JOYSTICK_RIGHT)
cur_col += 1
self._press_button(ControllerState.A_BUTTON)

# Select map choice
while cur_row != self.MAP_CHOICE:
self._press_button(ControllerState.JOYSTICK_DOWN)
cur_row += 1
self._press_button(ControllerState.A_BUTTON)

while frame < 284:
action = ControllerState.NO_OP

# 10 - Nintendo screen
# 80 - Mario Kart splash screen
# 120 - Select number of players
# 125 - Select GrandPrix or TimeTrials
# 130 - Select TimeTrials
# 132 - Select Begin
# 134 - OK
# 160 - Select player
# 162 - OK
# 202 - Select map series
# 230 - Select map choice
# 232 - OK
# 284 - <Level loaded; turn over control>
if frame in [10, 80, 120, 130, 132, 134, 160, 162, 202, 230, 232]:
action = ControllerState.A_BUTTON
elif frame in [125]:
action = ControllerState.JOYSTICK_DOWN

# Frame 150 is the 'Player Select' screen
if frame == 150:
print('Player row: ' + str(self.PLAYER_ROW))
print('Player col: ' + str(self.PLAYER_COL))

if cur_row != self.PLAYER_ROW:
action = ControllerState.JOYSTICK_DOWN
cur_row += 1

if frame in range(151, 156) and frame % 2 == 0:
if cur_col != self.PLAYER_COL:
action = ControllerState.JOYSTICK_RIGHT
cur_col += 1

# Frame 195 is the 'Map Select' screen
if frame == 195:
cur_row = 0
cur_col = 0
print('Map series: ' + str(self.MAP_SERIES))
print('Map choice: ' + str(self.MAP_CHOICE))

if frame in range(195, 202) and frame %2 == 0:
if cur_col != self.MAP_SERIES:
action = ControllerState.JOYSTICK_RIGHT
cur_col += 1

if frame in range(223, 230) and frame %2 == 0:
if cur_row != self.MAP_CHOICE:
action = ControllerState.JOYSTICK_DOWN
cur_row += 1

if action != ControllerState.NO_OP:
print('Frame ' + str(frame) + ': ' + str(action))

self.controller_server.send_controls(action)
frame += 1
# Press OK
self._press_button(ControllerState.A_BUTTON)

def _navigate_post_race_menu(self):
frame = 0
while frame < 138:
action = ControllerState.NO_OP

# Post race menu (previous choice selected by default)
# - Retry
# - Course Change
# - Driver Change
# - Quit
# - Replay
# - Save Ghost

# 60 - Times screen
# 75 - Post race menu
# 138 - <Level loaded; turn over control>
if frame in [60, 75]:
action = ControllerState.A_BUTTON

if action != ControllerState.NO_OP:
print('Frame ' + str(frame) + ': ' + str(action))

self.controller_server.send_controls(action)
frame += 1
# Times screen
self._press_button(ControllerState.A_BUTTON)
self._wait(count=13)

# Post race menu (previous choice selected by default)
# - Retry
# - Course Change
# - Driver Change
# - Quit
# - Replay
# - Save Ghost

# Because the previous choice is selected by default, we navigate to the top entry so our
# navigation is consistent. The menu doesn't cycle top to bottom or bottom to top, so we can
# just make sure we're at the top by hitting up a few times
for _ in itertools.repeat(None, 5):
self._press_button(ControllerState.JOYSTICK_UP)

# Now we are sure to have the top entry selected
# Go down to 'course change'
self._press_button(ControllerState.JOYSTICK_DOWN)
self._press_button(ControllerState.A_BUTTON)

def _set_character(self, character):
characters = {'mario' : (0, 0),
Expand Down
16 changes: 14 additions & 2 deletions gym_mupen64plus/envs/mupen64plus_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import abc
import array
import inspect
import itertools
import json
import os
import subprocess
Expand Down Expand Up @@ -69,14 +70,25 @@ def __init__(self, rom_name):

def _step(self, action):
#cprint('Step %i: %s' % (self.step_count, action), 'green')
self.controller_server.send_controls(action)
self._act(action)
obs = self._observe()
self.episode_over = self._evaluate_end_state()
reward = self._get_reward()

self.step_count += 1
return obs, reward, self.episode_over, {}

def _act(self, action, count=1):
for _ in itertools.repeat(None, count):
self.controller_server.send_controls(action)

def _wait(self, count=1, wait_for='Unknown'):
self._act(ControllerState.NO_OP, count=count)

def _press_button(self, button):
self._act(button) # Press
self._act(ControllerState.NO_OP) # and release

def _observe(self):
#cprint('Observe called!', 'yellow')

Expand Down Expand Up @@ -261,7 +273,7 @@ def _start_emulator(self,
def _kill_emulator(self):
#cprint('Kill Emulator called!', 'yellow')
try:
self.controller_server.send_controls(ControllerState.NO_OP)
self._act(ControllerState.NO_OP)
if self.emulator_process is not None:
self.emulator_process.kill()
if self.xvfb_process is not None:
Expand Down

0 comments on commit ce6de44

Please sign in to comment.