Skip to content

Commit

Permalink
Updated library structure for pip, and updated README
Browse files Browse the repository at this point in the history
  • Loading branch information
M-J-Murray committed Nov 1, 2018
1 parent 058b664 commit 99ec2af
Show file tree
Hide file tree
Showing 70 changed files with 117 additions and 76 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.idea
*__pycache__*
MAMEToolkit/emulator/mame/roms/sfiii3n.zip
MAMEToolkit/emulator/mame/mame
2 changes: 2 additions & 0 deletions MAMEToolkit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from MAMEToolkit import emulator
from MAMEToolkit import sf_environment
File renamed without changes.
File renamed without changes.
7 changes: 4 additions & 3 deletions src/emulator/Console.py → MAMEToolkit/emulator/Console.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from pathlib import Path
from subprocess import Popen, PIPE
from src.emulator.StreamGobbler import StreamGobbler
from MAMEToolkit.emulator.StreamGobbler import StreamGobbler
import queue
import logging

Expand All @@ -13,10 +14,10 @@ class Console(object):
# render is for displaying the frames to the emulator window, disabling it has little to no effect
# throttle enabled will run any game at the intended gameplay speed, disabling it will run the game as fast as the computer can handle
# debug enabled will print everything that comes out of the Lua engine console
def __init__(self, game_id, render=True, throttle=False, debug=False):
def __init__(self, roms_path, game_id, render=True, throttle=False, debug=False):
self.logger = logging.getLogger("Console")

command = "exec ./mame -rompath roms -pluginspath plugins -skip_gameinfo -sound none -console "+game_id
command = f"exec ./mame -rompath '{str(Path(roms_path).absolute())}' -pluginspath plugins -skip_gameinfo -sound none -console "+game_id
if not render:
command += " -video none"
if throttle:
Expand Down
16 changes: 8 additions & 8 deletions src/emulator/Emulator.py → MAMEToolkit/emulator/Emulator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import atexit
import os
from src.emulator.Console import Console
from src.emulator.pipes.Pipe import Pipe
from src.emulator.pipes.DataPipe import DataPipe
from MAMEToolkit.emulator.Console import Console
from MAMEToolkit.emulator.pipes.Pipe import Pipe
from MAMEToolkit.emulator.pipes.DataPipe import DataPipe


# Converts a list of action Enums into the relevant Lua engine representation
Expand All @@ -11,8 +11,8 @@ def actions_to_string(actions):
return '+'.join(action_strings)


def list_actions(game_id):
console = Console(game_id)
def list_actions(roms_path, game_id):
console = Console(roms_path, game_id)
console.writeln('iop = manager:machine():ioport()')
actions = []
ports = console.writeln("for k,v in pairs(iop.ports) do print(k) end", expect_output=True, timeout=0.5)
Expand All @@ -27,17 +27,17 @@ def list_actions(game_id):
# An interface for using the Lua engine console functionality
class Emulator(object):

# env_id - the unique id of the emulator
# 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, game_id, memory_addresses, frame_ratio=3, render=True, throttle=False, debug=False):
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(game_id, render=render, throttle=throttle, debug=debug)
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()
Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions MAMEToolkit/emulator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from MAMEToolkit.emulator.Action import Action
from MAMEToolkit.emulator.Emulator import Emulator, list_actions
from MAMEToolkit.emulator.Address import Address
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import numpy as np
from src.emulator.pipes.Pipe import Pipe
from MAMEToolkit.emulator.pipes.Pipe import Pipe


# A special implementation of a Linux FIFO pipe which is used for reading all of the frame data and memory address values from the emulator
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from src.emulator.StreamGobbler import StreamGobbler
from MAMEToolkit.emulator.StreamGobbler import StreamGobbler
from pathlib import Path
from queue import Queue
from threading import Thread
Expand All @@ -25,11 +25,13 @@ class Pipe(object):
def __init__(self, env_id, pipe_id, mode, pipes_path):
self.pipeId = pipe_id + "Pipe"
self.mode = mode
self.pipes_path = pipes_path
self.path = Path(pipes_path + "/" + pipe_id + "-" + str(env_id) + ".pipe")
self.pipes_path = Path(pipes_path)
if not self.pipes_path.exists():
self.pipes_path.mkdir()
self.path = self.pipes_path.joinpath(Path(pipe_id + "-" + str(env_id) + ".pipe"))
self.logger = logging.getLogger("Pipe: "+str(self.path.absolute()))
if self.path.exists():
os.remove(str(self.path.absolute()))
self.path.unlink()
os.mkfifo(str(self.path.absolute()))
self.logger.info("Created pipe file")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from src.emulator.Action import Action
from MAMEToolkit.emulator.Action import Action


# An enumerable class used to specify which actions can be used to interact with a game
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from src.emulator.Emulator import Emulator
from src.emulator.pipes.Address import Address
from src.sf_environment.Steps import *
from src.sf_environment.Actions import Actions
from MAMEToolkit.emulator.Emulator import Emulator
from MAMEToolkit.emulator.Address import Address
from MAMEToolkit.sf_environment.Steps import *
from MAMEToolkit.sf_environment.Actions import Actions


# Combines the data of multiple time steps
Expand Down Expand Up @@ -58,15 +58,16 @@ def index_to_attack_action(action):
# The Street Fighter specific interface for training an agent against the game
class Environment(object):

# env_id - the unique identifier of the emulator environment, used to create fifo pipes
# difficulty - the difficult to be used in story mode gameplay
# frameRatio, framesPerStep - see emulator class
# frame_ratio, frames_per_step - see Emulator class
# render, throttle, debug - see Console class
def __init__(self, env_id, difficulty=3, frame_ratio=2, frames_per_step=3, render=True, throttle=False, debug=False):
def __init__(self, env_id, roms_path, difficulty=3, frame_ratio=3, frames_per_step=3, render=True, throttle=False, debug=False):
self.difficulty = difficulty
self.frame_ratio = frame_ratio
self.frames_per_step = frames_per_step
self.throttle = throttle
self.emu = Emulator(env_id, "sfiii3n", setup_memory_addresses(), frame_ratio=frame_ratio, render=render, throttle=throttle, debug=debug)
self.emu = Emulator(env_id, roms_path, "sfiii3n", setup_memory_addresses(), frame_ratio=frame_ratio, render=render, throttle=throttle, debug=debug)
self.started = False
self.expected_health = {"P1": 0, "P2": 0}
self.expected_wins = {"P1": 0, "P2": 0}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.sf_environment.Actions import Actions
from MAMEToolkit.sf_environment.Actions import Actions

# A = Agent
# C = Computer
Expand Down
3 changes: 3 additions & 0 deletions MAMEToolkit/sf_environment/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from MAMEToolkit.sf_environment.Environment import Environment
from MAMEToolkit.sf_environment import Steps
from MAMEToolkit.sf_environment.Actions import Actions
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include MAMEToolkit/emulator/pipes/*
include MAMEToolkit/emulator/mame/mame
recursive-include MAMEToolkit/emulator/mame/plugins/ *
75 changes: 53 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,31 @@
## About
This Python library has the to potential to train your reinforcement learning algorithm on almost any arcade game. It is currently available on Linux systems and works as a wrapper around [MAME](http://mamedev.org/). The toolkit allows your algorithm to step through gameplay while recieving the frame data and internal memory address values for tracking the games state, along with sending actions to interact with the game.

## Installation
You can use `pip` to install the library, just run:
```bash
pip install MAMEToolkit
```

**DISCLAIMER: We are unable to provide you with any game ROMs. It is the users own legal responsibility to acquire a game ROM for emulation. This library should only be used for non-commercial research purposes.**

## Street Fighter Random Agent Demo
The toolkit has currently been applied to Street Fighter III Third Strike: Fight for the Future, but can modified for any game available on MAME. The following demonstrates how a random agent can be written for a street fighter environment.
The toolkit has currently been applied to Street Fighter III Third Strike: Fight for the Future (Japan 990608, NO CD), but can modified for any game available on MAME. The following demonstrates how a random agent can be written for a street fighter environment.
```python
import random
from sf_environment.Environment import Environment
from MAMEToolkit.sf_environment import Environment

env = Environment(difficulty=3, frame_ratio=3, frames_per_step=3)
roms_path = "roms/"
env = Environment("env1", roms_path)
env.start()
while True:
move_action = random.randint(0, 8)
attack_action = random.randint(0, 9)
frames, reward, round_done, stage_done = env.step(move_action, attack_action)
if stage_done:
env.next_game()
frames, reward, round_done, stage_done, game_done = env.step(move_action, attack_action)
if game_done:
env.new_game()
elif stage_done:
env.next_stage()
elif round_done:
env.next_round()
```
Expand All @@ -25,25 +36,28 @@ The toolkit also supports hogwild training:
```Python
from threading import Thread
import random
from src.sf_environment.Environment import Environment
from MAMEToolkit.sf_environment import Environment


def run_env(env):
env.start()
while True:
move_action = random.randint(0, 8)
attack_action = random.randint(0, 9)
frames, reward, round_done, stage_done = env.step(move_action, attack_action)
if stage_done:
env.next_game()
frames, reward, round_done, stage_done, game_done = env.step(move_action, attack_action)
if game_done:
env.new_game()
elif stage_done:
env.next_stage()
elif round_done:
env.next_round()


def main():
workers = 8
# Environments must be created outside of the threads
envs = [Environment(difficulty=5, frame_ratio=3, frames_per_step=3) for i in range(workers)]
roms_path = "roms/"
envs = [Environment(f"env{i}", roms_path) for i in range(workers)]
threads = [Thread(target=run_env, args=(envs[i], )) for i in range(workers)]
[thread.start() for thread in threads]
```
Expand All @@ -53,11 +67,23 @@ def main():
## Setting Up Your Own Game Environment
It doesn't take much to interact with the emulator itself using the toolkit, however the challenge comes from finding the memory address values associated with the internal state you care about, and tracking said state with your environment class.
The internal memory states of a game can be tracked using the [MAME Cheat Debugger](http://docs.mamedev.org/debugger/cheats.html), which allows you to track how the memory address values of the game change over time.
To create an emulation of the game you must first have the ROM for the game you are emulating and know the game ID used by MAME, for example for this version of street fighter it is 'sfiii3n'. Once you have these and have determined the memory addresses you wish to track you can start the emulation:
To create an emulation of the game you must first have the ROM for the game you are emulating and know the game ID used by MAME, for example for this version of street fighter it is 'sfiii3n'.

**Game ID's**<br>
The id of your game can be found by running:
```python
from emulator.Emulator import Emulator
from emulator.pipes.Address import Address
from MAMEToolkit.emulator import Emulator
emulator = Emulator("env1", "", "", memory_addresses)
```
This will bring up the MAME emulator. You can search through the list of games to find the one you want. The id of the game is always in brackets at the end of the game title.

**Memory Addresses**<br>
Once you have these and have determined the memory addresses you wish to track you can start the emulation:
```python
from MAMEToolkit.emulator import Emulator
from MAMEToolkit.emulator import Address

roms_path = "roms/"
game_id = "sfiii3n"
memory_addresses = {
"fighting": Address('0x0200EE44', 'u8'),
Expand All @@ -67,9 +93,12 @@ memory_addresses = {
"healthP2": Address('0x020691A3', 's8')
}

emulator = Emulator("sfiii3n", memory_addresses)
emulator = Emulator("env1", roms_path, "sfiii3n", memory_addresses)
```
This will immediately start the emulation and halt it when it toolkit has linked to the emulator process. Once the toolkit is linked, you can step the emulator along using the step function:
This will immediately start the emulation and halt it when it toolkit has linked to the emulator process.

**Stepping the emulator**<br>
Once the toolkit is linked, you can step the emulator along using the step function:
```python
data = emulator.step([])

Expand All @@ -82,19 +111,21 @@ player2_health = data["healthP2"]
```
The step function returns the frame data as a NumPy matrix, along with all of the memory address integer values from that timestep.

**Sending inputs**
To send actions to the emulator you also need to determine which input ports and fields the game supports. For example, with street fighter to insert a coin the following code is required:
```python
from emulator.Action import Action
from MAMEToolkit.emulator import Action

insert_coin = Action(':INPUTS', 'Coin 1')
data = emulator.step([insert_coin])
```
To identify which ports are availble use the list actions command:
```python
from emulator.Emulator import list_actions
from MAMEToolkit.emulator import list_actions

roms_path = "roms/"
game_id = "sfiii3n"
print(list_actions(game_id))
print(list_actions(roms_path, game_id))
```
which for street fighter returns the list with all the ports and fields available for sending actions to the step function:
```python
Expand Down Expand Up @@ -134,14 +165,14 @@ There is also the problem of transitioning games between non-learnable gameplay

The emulator class also has a frame_ratio argument which can be used for adjusting the frame rate seen by your algorithm. By default MAME generates frames at 60 frames per second, however, this may be too many frames for your algorithm. The toolkit by default will use a frame_ratio of 3, which means that 1 in 3 frames are sent through the toolkit, this converts the frame rate to 20 frames per second. Using a higher frame_ratio also increases the performance of the toolkit.
```Python
from emulator.Emulator import Emulator
from MAMEToolkit.emulator import Emulator

emulator = Emulator("sfiii3n", memory_addresses, frame_ratio=3)
emulator = Emulator(roms_path, game_id, memory_addresses, frame_ratio=3)
```

## Library Performance Benchmarks with PC Specs
The development and testing of this toolkit have been completed on an 8-core AMD FX-8300 3.3GHz CPU along with a 3GB GeForce GTX 1060 GPU.
With a single random agent, the street fighter environment can be run at 600%+ the normal gameplay speed. And For hogwild training with 8 random agents, the environment can be run at 550%+ the normal gameplay speed.
With a single random agent, the street fighter environment can be run at 600%+ the normal gameplay speed. And For hogwild training with 8 random agents, the environment can be run at 300%+ the normal gameplay speed.

## Simple ConvNet Agent
To ensure that the toolkit is able to train algorithms, a simple 5 layer ConvNet was setup with minimal tuning. The algorithm was able to successfully learn some simple mechanics of Street Fighter, such as combos and blocking. The Street Fighter gameplay works by having the player fight different opponents across 10 stages of increasing difficulty. Initially, the algorithm would reach stage 2 on average, but eventually could reach stage 5 on average after 2200 episodes of training. The learning rate was tracked using the net damage done vs damage taken of a single playthough for each episode.
Expand Down
2 changes: 0 additions & 2 deletions __init__.py

This file was deleted.

11 changes: 6 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import setuptools
from setuptools import setup, find_packages

with open("README.md", "r") as fh:
long_description = fh.read()

setuptools.setup(
setup(
name="MAMEToolkit",
version="0.0.1",
version="1.0.0",
author="Michael Murray",
author_email="m.j.murray123@gmail.com",
description="A library to train your RL algorithms against MAME arcade games on Linux",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/M-J-Murray/MAMEToolkit",
packages=setuptools.find_packages(),
packages=find_packages(exclude=['MAMEToolkit/emulator/mame/roms']),
include_package_data=True,
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
"Operating System :: POSIX :: Linux"
],
)
8 changes: 0 additions & 8 deletions src/emulator/mame/cfg/sfiii3n.cfg

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest
from hamcrest import *
from src.emulator.pipes.Address import Address
from MAMEToolkit.emulator.Address import Address


class AddressTest(unittest.TestCase):
Expand Down
6 changes: 3 additions & 3 deletions test/emulator/ConsoleTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from hamcrest import *
from time import sleep
from multiprocessing import set_start_method, Process, Queue
from src.emulator.Console import Console
from MAMEToolkit.emulator.Console import Console


def run_console(game_id, output_queue):
console = None
try:
console = Console(game_id)
console = Console("MAMEToolkit/emulator/mame/roms", game_id)
sleep(5)
console.writeln('s = manager:machine().screens[":screen"]')
output = console.writeln('print(s:width())', expect_output=True)
Expand All @@ -23,7 +23,7 @@ def test_write_read(self):
game_id = "sfiii3n"
console = None
try:
console = Console(game_id)
console = Console("/home/michael/dev/MAMEToolkit/MAMEToolkit/emulator/mame/roms", game_id)
sleep(5)
console.writeln('s = manager:machine().screens[":screen"]')
output = console.writeln('print(s:width())', expect_output=True)
Expand Down
Loading

0 comments on commit 99ec2af

Please sign in to comment.