class DataPipe(object): def __init__(self, env_id, screen_dims, addresses, pipes_path): self.pipe = Pipe(env_id, "data", 'r', pipes_path) self.screenDims = screen_dims self.addresses = addresses def open(self, console): self.pipe.open(console) def close(self): self.pipe.close() # Generates the equivalent Lua code specifying what data the Lua engine should return every time step def get_lua_string(self): return self.pipe.get_lua_string(args=[address.get_lua_string() for address in self.addresses.values()]+['s:bitmap_binary()']) def read_data(self, timeout=10): data = {} line = self.pipe.readln(timeout=timeout) cursor = 0 for k in self.addresses.keys(): cursor_end = cursor while line[cursor_end] != 43: cursor_end += 1 part = line[cursor:cursor_end] data[k] = int(part.decode("utf-8")) cursor = cursor_end+1 data["frame"] = np.frombuffer(line[cursor:], dtype='uint8').reshape(self.screenDims["height"], self.screenDims["width"], 3) return data
class Emulator(object): # env_id - the unique id of the emulator, used for fifo pipes # game_id - the game id being used # memory_addresses - The internal memory addresses of the game which this class will return the value of at every time step # frame_ratio - the ratio of frames that will be returned, 3 means 1 out of every 3 frames will be returned. Note that his also effects how often memory addresses are read and actions are sent # See console for render, throttle & debug def __init__(self, env_id, roms_path, game_id, memory_addresses, frame_ratio=3, render=True, throttle=False, debug=False): self.memoryAddresses = memory_addresses self.frameRatio = frame_ratio # setup lua engine self.console = Console(roms_path, game_id, render=render, throttle=throttle, debug=debug) atexit.register(self.close) self.wait_for_resource_registration() self.create_lua_variables() bitmap_format = self.get_bitmap_format() screen_width = self.setup_screen_width() screen_height = self.setup_screen_height() self.screenDims = {"width": screen_width, "height": screen_height} # open pipes pipes_path = f"{os.path.dirname(os.path.abspath(__file__))}/mame/pipes" self.actionPipe = Pipe(env_id, "action", 'w', pipes_path) self.actionPipe.open(self.console) self.dataPipe = DataPipe(env_id, self.screenDims, bitmap_format, memory_addresses, pipes_path) self.dataPipe.open(self.console) # Connect inter process communication self.setup_frame_access_loop() def get_bitmap_format(self): bitmap_format = self.console.writeln('print(s:bitmap_format())', expect_output=True) if len(bitmap_format) != 1: raise IOError( 'Expected one result from "print(s:bitmap_format())", but received: ', bitmap_format) try: return { "RGB32 - 32bpp 8-8-8 RGB": BitmapFormat.RGB32, "ARGB32 - 32bpp 8-8-8-8 ARGB": BitmapFormat.ARGB32 }[bitmap_format[0]] except KeyError: self.console.close() raise EnvironmentError( "MAMEToolkit only supports RGB32 and ARGB32 frame bit format games" ) def create_lua_variables(self): self.console.writeln('iop = manager:machine():ioport()') self.console.writeln('s = manager:machine().screens[":screen"]') self.console.writeln( 'mem = manager:machine().devices[":maincpu"].spaces["program"]') self.console.writeln('releaseQueue = {}') def wait_for_resource_registration(self, max_attempts=10): screen_registered = False program_registered = False attempt = 0 while not screen_registered or not program_registered: if not screen_registered: result = self.console.writeln( 'print(manager:machine().screens[":screen"])', expect_output=True, timeout=3, raiseError=False) screen_registered = result is not None and result is not "nil" if not program_registered: result = self.console.writeln( 'print(manager:machine().devices[":maincpu"].spaces["program"])', expect_output=True, timeout=3, raiseError=False) program_registered = result is not None and result is not "nil" if attempt == max_attempts: raise EnvironmentError("Failed to register MAME resources!") attempt += 1 # Gets the game screen width in pixels def setup_screen_width(self): output = self.console.writeln('print(s:width())', expect_output=True, timeout=1) if len(output) != 1: raise IOError( 'Expected one result from "print(s:width())", but received: ', output) return int(output[0]) # Gets the game screen height in pixels def setup_screen_height(self): output = self.console.writeln('print(s:height())', expect_output=True, timeout=1) if len(output) != 1: raise IOError( 'Expected one result from "print(s:height())"", but received: ', output) return int(output[0]) # Pauses the emulator def pause_game(self): self.console.writeln('emu.pause()') # Unpauses the emulator def unpause_game(self): self.console.writeln('emu.unpause()') # Sets up the callback function written in Lua that the Lua engine will execute each time a frame done def setup_frame_access_loop(self): pipe_data_func = 'function pipeData() ' \ 'if (math.fmod(tonumber(s:frame_number()),' + str(self.frameRatio) +') == 0) then ' \ 'for i=1,#releaseQueue do ' \ 'releaseQueue[i](); ' \ 'releaseQueue[i]=nil; ' \ 'end; ' \ '' + self.dataPipe.get_lua_string() + '' \ 'actions = ' + self.actionPipe.get_lua_string() + '' \ 'if (string.len(actions) > 1) then ' \ 'for action in string.gmatch(actions, "[^+]+") do ' \ 'actionFunc = loadstring(action..":set_value(1)"); ' \ 'actionFunc(); ' \ 'releaseFunc = loadstring(action..":set_value(0)"); ' \ 'table.insert(releaseQueue, releaseFunc); ' \ 'end; ' \ 'end; ' \ 'end; ' \ 'end' self.console.writeln(pipe_data_func) self.console.writeln('emu.register_frame_done(pipeData, "data")') # Steps the emulator along one time step def step(self, actions): # Will need to shuffle this for self-play?, right data = self.dataPipe.read_data( timeout=10) # gathers the frame data and memory address values action_string = actions_to_string(actions) self.actionPipe.writeln( action_string ) # sends the actions for the game to perform before the next step return data # Testing # Safely stops all of the processes related to running the emulator def close(self): self.console.close() self.actionPipe.close() self.dataPipe.close()