diff --git a/.gitignore b/.gitignore index ac95efe..12f0eec 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ build* # Dummy folder with all gym recordings and metadata /dummy + +#.vscode folders +/cpp/.vscode/* +/.vscode/* diff --git a/README.md b/README.md index 3c789fe..395d32a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gym-tcp-api -This project provides a distributed infrastructure (TCP API) to the OpenAI Gym toolkit, allowing development in languages other than python. +This project provides a distributed infrastructure (TCP API) to the Farama-Foundation Gymnasium toolkit, allowing development in languages other than python. The server is written in elixir, enabling a distributed infrastructure. Where each node makes use of a limitted set of processes that can be used to perform time consuming tasks (4 python instances per default). @@ -20,7 +20,7 @@ The server has the following dependencies: Python3 Elixir >= 1.0 - OpenAI Gym. + Farama-Foundation Gymnasium. The c++ example agent has the following dependencies: @@ -105,7 +105,7 @@ We use JSON as the format to cimmunicate with the server. Create the specified environment: - {"env" {"name": "CartPole-v0"}} + {"env" {"name": "CartPole-v0", "render_mode": "human"}} Close the environment: diff --git a/cpp/environment.hpp b/cpp/environment.hpp index d97657b..9144509 100644 --- a/cpp/environment.hpp +++ b/cpp/environment.hpp @@ -71,7 +71,8 @@ class Environment */ Environment(const std::string& host, const std::string& port, - const std::string& environment); + const std::string& environment, + const std::string& render_mode); /* * Instantiate the environment object using the specified environment name. @@ -79,7 +80,7 @@ class Environment * @param environment Name of the environments used to train/evaluate * the model. */ - void make(const std::string& environment); + void make(const std::string& environment, const std::string& render_mode); /* * Renders the environment. diff --git a/cpp/environment_impl.hpp b/cpp/environment_impl.hpp index 83f3938..b7053b4 100644 --- a/cpp/environment_impl.hpp +++ b/cpp/environment_impl.hpp @@ -27,16 +27,17 @@ inline Environment::Environment(const std::string& host, const std::string& port inline Environment::Environment( const std::string& host, const std::string& port, - const std::string& environment) : + const std::string& environment, + const std::string& render_mode="") : renderValue(false) { client.connect(host, port); - make(environment); + make(environment, render_mode); } -inline void Environment::make(const std::string& environment) +inline void Environment::make(const std::string& environment, const std::string& render_mode=nullptr) { - client.send(messages::EnvironmentName(environment)); + client.send(messages::EnvironmentName(environment, render_mode)); std::string json; client.receive(json); diff --git a/cpp/example.cpp b/cpp/example.cpp index ecc9666..1ddc509 100644 --- a/cpp/example.cpp +++ b/cpp/example.cpp @@ -16,11 +16,12 @@ int main(int argc, char* argv[]) const std::string environment = "CartPole-v1"; const std::string host = "127.0.0.1"; const std::string port = "4040"; + const std::string render_mode = "human"; double totalReward = 0; size_t totalSteps = 0; - Environment env(host, port, environment); + Environment env(host, port, environment, render_mode); env.compression(9); env.record_episode_stats.start(); diff --git a/cpp/messages.hpp b/cpp/messages.hpp index 15b4305..dc52993 100644 --- a/cpp/messages.hpp +++ b/cpp/messages.hpp @@ -16,9 +16,9 @@ namespace gym { namespace messages { //! Create message to set the enviroment name. -static inline std::string EnvironmentName(const std::string& name) +static inline std::string EnvironmentName(const std::string& name, const std::string& render_mode) { - return "{\"env\":{\"name\": \"" + name + "\"}}"; + return "{\"env\":{\"name\": \"" + name + "\", \"render_mode\": \"" + render_mode + "\"}}"; } //! Create message to reset the enviroment. diff --git a/python/server.py b/python/server.py index 58f5fc3..d5e9afa 100644 --- a/python/server.py +++ b/python/server.py @@ -3,11 +3,12 @@ import numpy as np import socket import os +import platform from _thread import * import glob -import gym -from gym.wrappers import RecordEpisodeStatistics +import gymnasium as gym +from gymnasium.wrappers import RecordEpisodeStatistics try: import zlib @@ -30,7 +31,7 @@ except socket.error as e: print(str(e)) -print('Waitiing for a Connection..') +print('Waiting for a Connection..') ServerSocket.listen(5) @@ -77,9 +78,9 @@ def _remove_env(self, instance_id): except KeyError: raise InvalidUsage('Instance_id {} unknown'.format(instance_id)) - def create(self, env_id): + def create(self, env_id, render_mode=None): try: - env = gym.make(env_id) + env = gym.make(env_id, render_mode=render_mode) except gym.error.Error: raise InvalidUsage( "Attempted to look up malformed environment ID '{}'".format(env_id)) @@ -90,17 +91,17 @@ def create(self, env_id): def reset(self, instance_id): env = self._lookup_env(instance_id) - obs = env.reset() + obs, _ = env.reset() return env.observation_space.to_jsonable(obs) def step(self, instance_id, action, render): env = self._lookup_env(instance_id) - action_from_json = env.action_space.from_jsonable(action) + action_from_json = env.action_space.from_jsonable([action]) if (not isinstance(action_from_json, (list))): action_from_json = int(action_from_json) if render: env.render() - [observation, reward, done, info] = env.step(action_from_json) + [observation, reward, done, _, info] = env.step(action_from_json[0]) obs_jsonable = env.observation_space.to_jsonable(observation) return [obs_jsonable, reward, done, info] @@ -264,12 +265,15 @@ def process_response(response, connection, envs, enviroment, instance_id, close, print(jsonMessage) enviroment = get_optional_params(jsonMessage, "env", "name") + render_mode = get_optional_params(jsonMessage, "env", "render_mode") + render_mode = render_mode if render_mode != "" else None + if isinstance(enviroment, basestring): compressionLevel = 0 if instance_id != None: envs.env_close(instance_id) - instance_id = envs.create(enviroment) + instance_id = envs.create(enviroment, render_mode) data = json.dumps({"instance" : instance_id}, cls = NDArrayEncoder) connection.send(process_data(data, compressionLevel)) return enviroment, instance_id, close, compressionLevel @@ -321,9 +325,7 @@ def process_response(response, connection, envs, enviroment, instance_id, close, render = True if (render is not None and render == 1) else False - [obs, reward, done, info] = envs.step( - instance_id, action, render) - + [obs, reward, done, info] = envs.step(instance_id, action, render) data = json.dumps({"observation" : obs, "reward" : reward, "done" : done, @@ -367,10 +369,23 @@ def process_response(response, connection, envs, enviroment, instance_id, close, return connection.close() -while True: - Client, address = ServerSocket.accept() - print('Connected to: ' + address[0] + ':' + str(address[1])) - start_new_thread(threaded_client, (Client, )) - ThreadCount += 1 - print('Thread Number: ' + str(ThreadCount)) -ServerSocket.close() \ No newline at end of file + +if __name__ == "__main__": + if platform.system() == 'Darwin': + # This is needed to make it work on MacOS. In fact, MacOS doesn't allow to rendere on threads + # other than the main thread + while True: + Client, address = ServerSocket.accept() + print('Connected to: ' + address[0] + ':' + str(address[1])) + ThreadCount += 1 + print('Thread Number: ' + str(ThreadCount)) + threaded_client(Client) # Directly call the function without threading + ServerSocket.close() + else: + while True: + Client, address = ServerSocket.accept() + print('Connected to: ' + address[0] + ':' + str(address[1])) + start_new_thread(threaded_client, (Client, )) + ThreadCount += 1 + print('Thread Number: ' + str(ThreadCount)) + ServerSocket.close() \ No newline at end of file