Skip to content
This repository has been archived by the owner on Aug 23, 2022. It is now read-only.

Commit

Permalink
Merge pull request #152 from SerpentAI/environments
Browse files Browse the repository at this point in the history
added base environment class; added concept of environments to game c…
  • Loading branch information
nbrochu committed Feb 27, 2018
2 parents aeb2a41 + 0967d50 commit a931419
Show file tree
Hide file tree
Showing 9 changed files with 346 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1,4 +1,5 @@
.idea
.vscode

__pycache__/
*.py[cod]
Expand Down
1 change: 1 addition & 0 deletions config/config.plugins.yml
@@ -0,0 +1 @@
# Stub to make unit testing possible...
34 changes: 34 additions & 0 deletions config/config.yml
@@ -0,0 +1,34 @@
# Stub to make unit testing possible...

redis:
host: 127.0.0.1
port: 6379
db: 0

analytics:
host: 127.0.0.1
port: 9999
realm: serpent
auth:
username: serpent
password: serpent
topic: ANALYTICS_TOPIC

frame_handlers:
COLLECT_FRAMES_FOR_CONTEXT:
context: game
interval: 1

frame_grabber:
redis_key: SERPENT:FRAMES

input_recorder:
redis_key: SERPENT:INPUTS

visual_debugger:
redis_key_prefix: SERPENT:VISUAL_DEBUGGER
available_buckets:
- "0"
- "1"
- "2"
- "3"
67 changes: 67 additions & 0 deletions serpent/environment.py
@@ -0,0 +1,67 @@
import time
import collections


class Environment:

def __init__(self, name, game_api=None, input_controller=None):
self.name = name

self.game_api = game_api
self.input_controller = input_controller

self.game_state = dict()

self.reset()

@property
def episode_duration(self):
return time.time() - self.episode_started_at

@property
def episode_over(self):
if self.episode_maximum_steps is not None:
return self.episode_steps >= self.episode_maximum_steps
else:
return False

def new_episode(self, maximum_steps=None, reset=False):
self.recent_game_inputs = collections.deque(list(), maxlen=10)

self.episode_steps = 0
self.episode_maximum_steps = maximum_steps

self.episode_started_at = time.time()

if not reset:
self.episode += 1

def episode_step(self):
self.episode_steps += 1
self.total_steps += 1

def reset(self):
self.recent_game_inputs = None

self.total_steps = 0

self.episode = 0
self.episode_steps = 0

self.episode_maximum_steps = None

self.episode_started_at = None

def update_game_state(self):
raise NotImplementedError()

def perform_input(self, label, game_input):
# TODO: Mouse support
# TODO: Consider using sneakysnek events?

self.input_controller.handle_keys(game_input)
self.recent_game_inputs.appendleft(label)

def clear_input(self):
# TODO: Mouse support
self.input_controller.handle_keys([])
39 changes: 38 additions & 1 deletion serpent/game.py
Expand Up @@ -43,7 +43,7 @@ class Game(offshoot.Pluggable):

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.config = config.get(f"{self.__class__.__name__}Plugin")
self.config = config.get(f"{self.__class__.__name__}Plugin", dict())

self.platform = kwargs.get("platform")

Expand All @@ -66,12 +66,20 @@ def __init__(self, **kwargs):
self.api_class = None
self.api_instance = None

self.environments = dict()
self.environment_data = dict()

self.sprites = self._discover_sprites()

self.redis_client = StrictRedis(**config["redis"])

self.kwargs = kwargs

@property
@offshoot.forbidden
def game_name(self):
return self.__class__.__name__.replace("Serpent", "").replace("Game", "")

@property
@offshoot.forbidden
def game_launcher(self):
Expand Down Expand Up @@ -118,6 +126,35 @@ def launch(self, dry_run=False):

self.after_launch()

@offshoot.forbidden
def relaunch(self, before_relaunch=None, after_relaunch=None):
clear_terminal()
print("")
print("Relaunching the game...")

self.stop_frame_grabber()

time.sleep(1)

if before_relaunch is not None:
before_relaunch()

time.sleep(1)

subprocess.call(shlex.split(f"serpent launch {self.game_name}"))
self.launch(dry_run=True)

self.start_frame_grabber()
self.redis_client.delete(config["frame_grabber"]["redis_key"])

while self.redis_client.llen(config["frame_grabber"]["redis_key"]) == 0:
time.sleep(0.1)

self.window_controller.focus_window(self.window_id)

if after_relaunch is not None:
after_relaunch()

def before_launch(self):
pass

Expand Down
58 changes: 58 additions & 0 deletions serpent/game_api.py
@@ -1,5 +1,9 @@
from serpent.input_controller import InputController

from serpent.utilities import SerpentError

import itertools


class GameAPI:
instance = None
Expand All @@ -12,4 +16,58 @@ def __init__(self, game=None):

self.input_controller = InputController(game=game)

self.game_inputs = dict()

self.__class__.instance = self

def combine_game_inputs(self, combination):
""" Combine game input axes in a single flattened collection
Args:
combination [list] -- A combination of valid game input axis keys
"""

# Validation
if not isinstance(combination, list):
raise SerpentError("'combination' needs to be a list")

for entry in combination:
if isinstance(entry, list):
for entry_item in entry:
if entry_item not in self.game_inputs:
raise SerpentError("'combination' entries need to be valid members of self.game_input...")
else:
if entry not in self.game_inputs:
raise SerpentError("'combination' entries need to be valid members of self.game_input...")

# Concatenate Grouped Axes (if needed)
game_input_axes = list()

for entry in combination:
if isinstance(entry, str):
game_input_axes.append(self.game_inputs[entry])
elif isinstance(entry, list):
concatenated_game_input_axis = dict()

for entry_item in entry:
concatenated_game_input_axis = {**concatenated_game_input_axis, **self.game_inputs[entry_item]}

game_input_axes.append(concatenated_game_input_axis)

# Combine Game Inputs
game_inputs = dict()

if not len(game_input_axes):
return game_inputs

for keys in itertools.product(*game_input_axes):
compound_label = list()
game_input = list()

for index, key in enumerate(keys):
compound_label.append(key)
game_input += game_input_axes[index][key]

game_inputs[" - ".join(compound_label)] = game_input

return game_inputs
28 changes: 27 additions & 1 deletion serpent/templates/SerpentGamePlugin/files/api/api.py
@@ -1,16 +1,42 @@
from serpent.game_api import GameAPI

from serpent.input_controller import KeyboardKey


class MyGameAPI(GameAPI):
# A GameAPI is intended to contain functions and pieces of data that are applicable to the
# game and not agent or environment specific (e.g. Game inputs, Frame processing)

def __init__(self, game=None):
super().__init__(game=game)

# SAMPLE - Replace with your own game inputs!
self.game_inputs = {
"MOVEMENT": {
"MOVE UP": [KeyboardKey.KEY_W],
"MOVE LEFT": [KeyboardKey.KEY_A],
"MOVE DOWN": [KeyboardKey.KEY_S],
"MOVE RIGHT": [KeyboardKey.KEY_D],
"MOVE TOP-LEFT": [KeyboardKey.KEY_W, KeyboardKey.KEY_A],
"MOVE TOP-RIGHT": [KeyboardKey.KEY_W, KeyboardKey.KEY_D],
"MOVE DOWN-LEFT": [KeyboardKey.KEY_S, KeyboardKey.KEY_A],
"MOVE DOWN-RIGHT": [KeyboardKey.KEY_S, KeyboardKey.KEY_D],
"DON'T MOVE": []
},
"SHOOTING": {
"SHOOT UP": [KeyboardKey.KEY_UP],
"SHOOT LEFT": [KeyboardKey.KEY_LEFT],
"SHOOT DOWN": [KeyboardKey.KEY_DOWN],
"SHOOT RIGHT": [KeyboardKey.KEY_RIGHT],
"DON'T SHOOT": []
}
}

def my_api_function(self):
pass

class MyAPINamespace:

@classmethod
def my_namespaced_api_function(cls):
api = MyGameAPI.instance
api = MyGameAPI.instance
3 changes: 3 additions & 0 deletions serpent/templates/SerpentGamePlugin/files/serpent_game.py
Expand Up @@ -25,6 +25,9 @@ def __init__(self, **kwargs):
self.api_class = MyGameAPI
self.api_instance = None

self.environments = dict()
self.environment_data = dict()

@property
def screen_regions(self):
regions = {
Expand Down

0 comments on commit a931419

Please sign in to comment.