Current section
Files
Jump to
Current section
Files
lib/alex.ex
defmodule Alex do
alias Alex.Interface
alias Alex.RAM
alias Alex.ROM
alias Alex.State
alias Alex.Screen
@moduledoc """
(A)rcade (L)earning (E)nvironment for Eli(x)ir.
ALEx is an interface for the ALE in Elixir. There are two ways to interact with the ALE from ALEx: through the `Alex.Interface` module which mimics the ALE C Lib and provides NIFs for interacting directly with the ALE C++ Interface, or through the `Alex` module which is a safer wrapper of `Alex.Interface`.
`Alex` allows you to interact with the ALE without worrying about some of the lower level details of the ALE Interface. Every function in `Alex` returns an `Alex.Interface` struct which contains details about the current ALE Interface. See `Alex.Interface` for information about the Interface struct.
"""
@doc """
Initializes a new ALE Interface.
Returns `%Interface{}`.
# Parameters
- `opts`: `Keyword` options.
# Options
- `:display_screen`: `true` or `false` to display screen. Defaults to `false`.
- `:sound`: `true` or `false` to play sound. Defaults to `false`.
- `:random_seed`: `Integer` random seed.
"""
def new(opts \\ []) do
with {:ok, ale_ref} <- Interface.ale_new() do
opts
|> Enum.reduce(
%Interface{ref: ale_ref},
fn {key, val}, int ->
set_option(int, key, val)
end
)
else
err -> raise err
end
end
@doc """
Loads the specified ROM and populates fields in `%Interface{}`.
Returns `%Interface{}`.
# Parameters
- `interface`: `%Interface{}`.
"""
def load(%Interface{} = interface, path_to_rom) do
ale_ref = interface.ref
with :ok <- ROM.rom_exists?(path_to_rom),
:ok <- ROM.rom_supported?(path_to_rom),
:ok <- Interface.load_rom(ale_ref, path_to_rom),
{:ok, modes} <- Interface.get_available_modes(ale_ref),
{:ok, difficulties} <- Interface.get_available_difficulties(ale_ref),
{:ok, difficulty} <- Interface.get_difficulty(ale_ref),
{:ok, legal_actions} <- Interface.get_legal_action_set(ale_ref),
{:ok, min_actions} <- Interface.get_minimal_action_set(ale_ref),
{:ok, lives} <- Interface.lives(ale_ref),
{:ok, frame} <- Interface.get_frame_number(ale_ref),
{:ok, episode_frame} <- Interface.get_episode_frame_number(ale_ref),
{:ok, state} <- State.new(interface),
{:ok, screen} <- Screen.new(interface),
{:ok, ram} <- RAM.new(interface) do
%Interface{
interface
| rom: path_to_rom,
modes: MapSet.new(modes),
difficulties: MapSet.new(difficulties),
difficulty: difficulty,
legal_actions: MapSet.new(legal_actions),
minimal_actions: MapSet.new(min_actions),
lives: lives,
frame: frame,
ram: ram,
episode_frame: episode_frame,
screen: screen,
state: state
}
else
{:error, err} -> raise err
end
end
@doc """
Performs a step with provided `action` and updates the interface.
Returns `%Interface`.
# Parameters
- `interface`: `%Interface{}`.
- `action`: `Integer` valid action.
"""
def step(%Interface{} = interface, action) do
ale_ref = interface.ref
if not MapSet.member?(interface.legal_actions, action) do
else
with {:ok, reward} <- Interface.act(ale_ref, action),
{:ok, lives} <- Interface.lives(ale_ref),
{:ok, legal_actions} <- Interface.get_legal_action_set(ale_ref),
{:ok, min_actions} <- Interface.get_minimal_action_set(ale_ref),
{:ok, frame} <- Interface.get_frame_number(ale_ref),
{:ok, episode_frame} <- Interface.get_episode_frame_number(ale_ref),
{:ok, state} <- State.new(interface) do
%Interface{
interface
| reward: interface.reward + reward,
lives: lives,
legal_actions: MapSet.new(legal_actions),
minimal_actions: MapSet.new(min_actions),
frame: frame,
episode_frame: episode_frame,
state: state
}
else
err -> raise err
end
end
end
@doc """
Checks if game is over.
Returns `boolean`.
# Parameters
- `interface`: `%Interface{}`.
"""
def game_over?(%Interface{} = interface) do
{:ok, game_over} = Interface.game_over(interface.ref)
game_over
end
@doc """
Resets the interface to beginning of an episode.
Returns `%Interface{}`.
# Parameters
- `interface`: `%Interface{}`.
"""
def reset(%Interface{} = interface) do
ale_ref = interface.ref
Interface.reset_game(ale_ref)
with {:ok, frame} <- Interface.get_frame_number(ale_ref),
{:ok, episode_frame} <- Interface.get_episode_frame_number(ale_ref),
{:ok, lives} <- Interface.lives(ale_ref),
{:ok, legal_actions} <- Interface.get_legal_action_set(ale_ref),
{:ok, min_actions} <- Interface.get_minimal_action_set(ale_ref),
{:ok, state} <- State.new(interface),
{:ok, screen} <- Screen.new(interface) do
%Interface{
interface
| frame: frame,
episode_frame: episode_frame,
lives: lives,
legal_actions: MapSet.new(legal_actions),
minimal_actions: MapSet.new(min_actions),
state: state,
screen: screen
}
else
err -> raise err
end
end
@doc """
Sets option for provided interface.
Returns `%Interface{}`.
# Parameters
- `interface`: `%Interface{}` to set option for.
- `key`: `Atom` or `String` key.
- `val`: `String`, `Integer`, `Boolean`, or `Float` value.
# Options
- `:repeat_action_probability`: `Float` probability that agent will repeat action in next frame regardless of it's choice. Defaults to 0.
- `:color_averaging`: `true` or `false` to enable color averaging. Defaults to `false`.
- `:max_num_frames`: `Integer` maximum frames to run. Defaults to `0` or no max.
- `:max_num_frames_per_episode`: maximum frames to run per episode. Defaults to `0` or no max.
- `:frame_skip`: `Integer` frame skipping rate. Defaults to `1` or no skip.
- `:difficulty`: `Integer` game difficulty. Defaults to `0`.
- `:mode`: `Integer` game mode. Defaults to `0`.
"""
def set_option(%Interface{} = interface, :difficulty, val) do
ale_ref = interface.ref
case interface.difficulties do
nil ->
raise "Could not find difficultues. Did you load a ROM?"
_ ->
if not MapSet.member?(interface.difficulties, val) do
raise "#{val} not a valid difficulty. Must be one of: #{
MapSet.to_list(interface.difficulties)
}"
else
:ok = Interface.set_difficulty(ale_ref, val)
%Interface{interface | difficulty: val}
end
end
end
def set_option(%Interface{} = interface, "difficulty", val) do
ale_ref = interface.ref
case interface.difficulties do
nil ->
raise "Could not find difficultues. Did you load a ROM?"
_ ->
if not MapSet.member?(interface.difficulties, val) do
raise "#{val} not a valid difficulty. Must be one of: #{
MapSet.to_list(interface.difficulties)
}"
else
:ok = Interface.set_difficulty(ale_ref, val)
%Interface{interface | difficulty: val}
end
end
end
def set_option(%Interface{} = interface, :mode, val) do
ale_ref = interface.ref
case interface.modes do
nil ->
raise "Could not find modes. Did you load a ROM?"
_ ->
if not MapSet.member?(interface.modes, val) do
raise "#{val} not a valid mode. Must be one of: #{MapSet.to_list(interface.modes)}"
else
:ok = Interface.set_mode(ale_ref, val)
%Interface{interface | mode: val}
end
end
end
def set_option(%Interface{} = interface, "mode", val) do
ale_ref = interface.ref
case interface.modes do
nil ->
raise "Could not find modes. Did you load a ROM?"
_ ->
if not MapSet.member?(interface.modes, val) do
raise "#{val} not a valid mode. Must be one of: #{MapSet.to_list(interface.modes)}"
else
:ok = Interface.set_mode(ale_ref, val)
%Interface{interface | mode: val}
end
end
end
def set_option(%Interface{} = interface, key, val) when is_atom(key),
do: set_option(interface, Atom.to_string(key), val)
def set_option(%Interface{} = interface, key, val) when is_binary(key) and is_binary(val) do
ale_ref = interface.ref
case Interface.set_string(ale_ref, key, val) do
:ok -> Map.update!(interface, String.to_atom(key), fn _ -> val end)
err -> {:error, err}
end
end
def set_option(%Interface{} = interface, key, val) when is_binary(key) and is_integer(val) do
ale_ref = interface.ref
case Interface.set_int(ale_ref, key, val) do
:ok -> Map.update!(interface, String.to_atom(key), fn _ -> val end)
err -> {:error, err}
end
end
def set_option(%Interface{} = interface, key, val) when is_binary(key) and is_boolean(val) do
ale_ref = interface.ref
case Interface.set_bool(ale_ref, key, val) do
:ok -> Map.update!(interface, String.to_atom(key), fn _ -> val end)
err -> {:error, err}
end
end
def set_option(%Interface{} = interface, key, val) when is_binary(key) and is_float(val) do
ale_ref = interface.ref
case Interface.set_float(ale_ref, key, val) do
:ok -> Map.update!(interface, String.to_atom(key), fn _ -> val end)
err -> {:error, err}
end
end
def set_option(_int, _key, _val),
do:
raise("""
Invalid arguments passed to set_option/3.
interface must be %Interface{}.
key must be binary or atom.
value must be binary, integer, boolean, or float.
""")
@doc """
Sets interface to given state.
Returns `%Interface{}`.
# Parameters
- `interface`: `%Interface{}`.
"""
def set_state(%Interface{} = interface, %State{} = state) do
ale_ref = interface.ref
with :ok <- Interface.restore_state(ale_ref, state.ref),
{:ok, modes} <- Interface.get_available_modes(ale_ref),
{:ok, difficulties} <- Interface.get_available_difficulties(ale_ref),
{:ok, difficulty} <- Interface.get_difficulty(ale_ref),
{:ok, legal_actions} <- Interface.get_legal_action_set(ale_ref),
{:ok, min_actions} <- Interface.get_minimal_action_set(ale_ref),
{:ok, lives} <- Interface.lives(ale_ref),
{:ok, frame} <- Interface.get_frame_number(ale_ref),
{:ok, episode_frame} <- Interface.get_episode_frame_number(ale_ref),
{:ok, state} <- State.new(interface),
{:ok, screen} <- Screen.new(interface) do
%Interface{
interface
| modes: MapSet.new(modes),
difficulties: MapSet.new(difficulties),
difficulty: difficulty,
legal_actions: MapSet.new(legal_actions),
minimal_actions: MapSet.new(min_actions),
lives: lives,
frame: frame,
episode_frame: episode_frame,
state: state,
screen: screen
}
else
err -> raise err
end
end
@doc """
Takes a screenshot.
Returns `:ok`.
# Parameters
- `interface`: `%Interface{}`.
"""
def screenshot(%Interface{} = interface, path \\ "") do
ale_ref = interface.ref
path =
if path == "" do
dtg = DateTime.utc_now()
dtg
|> DateTime.to_string()
|> String.replace(" ", "_")
|> String.replace(":", "_")
|> Kernel.<>(".png")
else
path
end
Interface.save_screen_png(ale_ref, path)
end
end