Skip to content

Commit

Permalink
Merge pull request #61 from rk1a/improve-world-seeding
Browse files Browse the repository at this point in the history
Improve world seeding and config handling
  • Loading branch information
AI-WAIFU committed Aug 29, 2023
2 parents 315cb46 + ad97c20 commit d1afd70
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 78 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ newworld/
!/mods/rewards/
!/mods/treechop_shaped_v*/
/worlds
/configs
/world/
/clientmods/*
!/clientmods/rewards/
Expand Down
142 changes: 76 additions & 66 deletions minetester/minetest_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
import matplotlib.pyplot as plt
import numpy as np
import zmq

from minetester.utils import (KEY_MAP, pack_pb_action, start_minetest_client,
start_minetest_server, start_xserver,
unpack_pb_obs)
from minetester.utils import (
KEY_MAP,
pack_pb_action,
start_minetest_client,
start_minetest_server,
read_config_file,
write_config_file,
unpack_pb_obs,
start_xserver,
)

import pkg_resources

Expand All @@ -30,7 +36,8 @@ def __init__(
config_path: Optional[os.PathLike] = None,
display_size: Tuple[int, int] = default_display_size,
fov: int = 72,
seed: Optional[int] = None,
base_seed: int = 0,
world_seed: Optional[int] = None,
start_minetest: bool = True,
game_id: str = "minetest",
client_name: str = "minetester",
Expand All @@ -45,19 +52,15 @@ def __init__(
):
self.unique_env_id = str(uuid.uuid4())

# Seed the environment
if seed is not None:
self.seed(seed)

# Graphics settings
self._set_graphics(headless, display_size, fov)

# Define action and observation space
self._configure_spaces()

# Define Minetest paths
self._set_artefact_dirs(artefact_dir, world_dir, config_path, config_dict) #Stores minetest artefacts and outputs
self._set_minetest_dirs(minetest_root) #Stores actual minetest dirs and executable
self._set_artefact_dirs(artefact_dir, world_dir, config_path) # Stores minetest artefacts and outputs
self._set_minetest_dirs(minetest_root) # Stores actual minetest dirs and executable

# Whether to start minetest server and client
self.start_minetest = start_minetest
Expand Down Expand Up @@ -85,6 +88,19 @@ def __init__(
self.render_fig = None
self.render_img = None

# Seed the environment
self.base_seed = base_seed
self.world_seed = world_seed
# If no world_seed is provided
# seed the world with a random seed
# generated by the RNG from base_seed
self.reseed_on_reset = world_seed is None
self.seed(self.base_seed)

# Write minetest.conf
self.config_dict = config_dict
self._write_config()

# Configure logging
logging.basicConfig(
filename=os.path.join(self.log_dir, f"env_{self.unique_env_id}.log"),
Expand All @@ -102,7 +118,7 @@ def __init__(
self.servermods += ["rewards"] # require the server rewards mod
self._enable_servermods()
else:
self.clientmods += ["rewards"] # require the client rewards mod
self.clientmods += ["rewards"] # require the client rewards med
# add client mod names in case they entail a server side component
self.servermods += clientmods
self._enable_clientmods()
Expand Down Expand Up @@ -178,7 +194,7 @@ def _set_minetest_dirs(self, minetest_root):
"mouse_cursor_white_16x16.png",
)

def _set_artefact_dirs(self, artefact_dir, world_dir, config_path, config_dict):
def _set_artefact_dirs(self, artefact_dir, world_dir, config_path):
if artefact_dir is None:
self.artefact_dir = os.path.join(os.getcwd(), "artefacts")
else:
Expand All @@ -204,10 +220,6 @@ def _set_artefact_dirs(self, artefact_dir, world_dir, config_path, config_dict):
os.makedirs(self.log_dir, exist_ok=True)
os.makedirs(self.media_cache_dir, exist_ok=True)

# Write minetest.conf
self.config_dict = config_dict
self._write_config()

def _enable_clientmods(self):
clientmods_folder = os.path.realpath(
os.path.join(os.path.dirname(self.minetest_executable), "../clientmods"),
Expand Down Expand Up @@ -305,7 +317,7 @@ def _check_world_dir(self):

def _delete_world(self):
if os.path.exists(self.world_dir):
shutil.rmtree(self.world_dir)
shutil.rmtree(self.world_dir, ignore_errors=True)

def _check_config_path(self):
if self.config_path is None:
Expand All @@ -319,59 +331,56 @@ def _delete_config(self):
os.remove(self.config_path)

def _write_config(self):
with open(self.config_path, "w") as config_file:
# Update default settings
# TODO load these values from custom minetest config
config_file.write("mute_sound = true\n")
config_file.write("show_debug = false\n")
config_file.write("enable_client_modding = true\n")
# config_file.write("video_driver = null\n")
# config_file.write("enable_shaders = false\n")
config_file.write("csm_restriction_flags = 0\n")
config_file.write("enable_mod_channels = true\n")
config_file.write("server_map_save_interval = 1000000\n")
config_file.write("profiler_print_interval = 0\n")
config_file.write("active_block_range = 2\n")
config_file.write("abm_time_budget = 0.01\n")
config_file.write("abm_interval = 0.1\n")
config_file.write("active_block_mgmt_interval = 4.0\n")
config_file.write("server_unload_unused_data_timeout = 1000000\n")
config_file.write("client_unload_unused_data_timeout = 1000000\n")
config_file.write("debug_log_level = verbose\n")
config_file.write("full_block_send_enable_min_time_from_building = 0.\n")
config_file.write("max_block_send_distance = 100\n")
config_file.write("max_block_generate_distance = 100\n")
config_file.write("num_emerge_threads = 0\n")
config_file.write("emergequeue_limit_total = 1000000\n")
config_file.write("emergequeue_limit_diskonly = 1000000\n")
config_file.write("emergequeue_limit_generate = 1000000\n")

# Set display size
config_file.write(f"screen_w = {self.display_size[0]}\n")
config_file.write(f"screen_h = {self.display_size[1]}\n")
config = dict(
# Base config
mute_sound=True,
show_debug=False,
enable_client_modding=True,
csm_restriction_flags=0,
enable_mod_channels=True,
screen_w=self.display_size[0],
screen_h=self.display_size[1],
fov=self.fov_y,
# Adapt HUD size to display size
hud_scale = self.display_size[0] / Minetest.default_display_size[0]
config_file.write(f"hud_scaling = {hud_scale}\n")

# Set FOV
config_file.write(f"fov = {self.fov_y}\n")

# Seed the map generator
if self.the_seed:
config_file.write(f"fixed_map_seed = {self.the_seed}\n")
hud_scaling=self.display_size[0] / Minetest.default_display_size[0],
# Experimental settings to improve performance
server_map_save_interval=1000000,
profiler_print_interval=0,
active_block_range=2,
abm_time_budget=0.01,
abm_interval=0.1,
active_block_mgmt_interval=4.0,
server_unload_unused_data_timeout=1000000,
client_unload_unused_data_timeout=1000000,
full_block_send_enable_min_time_from_building=0.,
max_block_send_distance=100,
max_block_generate_distance=100,
num_emerge_threads=0,
emergequeue_limit_total=1000000,
emergequeue_limit_diskonly=1000000,
emergequeue_limit_generate=1000000,
)

# Set from custom config dict
# TODO enable overwriting of default settings
for key, value in self.config_dict.items():
config_file.write(f"{key} = {value}\n")
# Seed the map generator if not using a custom map
if self.world_seed:
config.update(fixed_map_seed=self.world_seed)
# Update config from existing config file
if os.path.exists(self.config_path):
config.update(read_config_file(self.config_path))
# Set from custom config dict
config.update(self.config_dict)
write_config_file(self.config_path, config)

def seed(self, seed: int):
self.the_seed = seed
def seed(self, seed: Optional[int] = None):
self._np_random = np.random.RandomState(seed or 0)

def reset(self):
def reset(self, seed: Optional[int] = None, options: Optional[Dict[str, Any]] = None):
self.seed(seed=seed)
if self.start_minetest:
if self.reset_world:
self._delete_world()
if self.reseed_on_reset:
self.world_seed = self._np_random.randint(np.iinfo(np.int64).max)
self._enable_servermods()
self._reset_minetest()
self._reset_zmq()
Expand All @@ -382,7 +391,7 @@ def reset(self):
obs, _, _, _, _ = unpack_pb_obs(byte_obs)
self.last_obs = obs
logging.debug("Received first obs: {}".format(obs.shape))
return obs
return obs, {}

def step(self, action: Dict[str, Any]):
# Send action
Expand All @@ -407,10 +416,11 @@ def step(self, action: Dict[str, Any]):

self.last_obs = next_obs
logging.debug(f"Received obs - {next_obs.shape}; reward - {rew}; info - {info}")
return next_obs, rew, done, info
return next_obs, rew, done, False, {"info": info}

def render(self, render_mode: str = "human"):
if render_mode == "human":
# TODO replace with pygame
if self.render_img is None:
# Setup figure
plt.rcParams["toolbar"] = "None"
Expand Down
6 changes: 3 additions & 3 deletions minetester/scripts/test_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from minetester import Minetest

env = Minetest(
seed=42,
base_seed=42,
start_minetest=True,
sync_port=30010,
sync_dtime=0.05,
Expand All @@ -15,12 +15,12 @@
)

render = True
obs = env.reset()
obs, _ = env.reset()
done = False
while not done:
try:
action = env.action_space.sample()
obs, rew, done, info = env.step(action)
obs, rew, done, _, info = env.step(action)
print(rew, done, info)
if render:
env.render()
Expand Down
19 changes: 10 additions & 9 deletions minetester/scripts/test_loop_parallel.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import random
from typing import Any, Dict, Optional

from gym.wrappers import TimeLimit
from gymnasium.wrappers import TimeLimit
from gymnasium.vector import AsyncVectorEnv
from minetester import Minetest
from minetester.utils import start_xserver
from stable_baselines3.common.vec_env import DummyVecEnv, SubprocVecEnv

if __name__ == "__main__":

Expand All @@ -23,11 +23,12 @@ def _init():
env = Minetest(
env_port=5555 + rank,
server_port=30000 + rank,
seed=seed + rank,
base_seed=seed + rank,
sync_port=30010 + rank,
**env_kwargs,
)
env = TimeLimit(env, max_episode_steps=int(max_steps * random.random()))
# Assign random timelimit to check that resets work properly
env = TimeLimit(env, max_episode_steps=random.randint(max_steps // 2, max_steps))
return env

return _init
Expand All @@ -46,7 +47,7 @@ def _init():

# Create a vectorized environment
num_envs = 2 # Number of envs to use (<= number of avail. cpus)
vec_env_cls = SubprocVecEnv # DummyVecEnv
vec_env_cls = AsyncVectorEnv
venv = vec_env_cls(
[
make_env(rank=i, seed=seed, max_steps=max_steps, env_kwargs=env_kwargs)
Expand All @@ -59,15 +60,15 @@ def _init():

# Start loop
render = True
obs = venv.reset()
obs, _ = venv.reset()
done = [False] * num_envs
step = 0
while step < max_steps:
print(f"Elapsed steps: {venv.get_attr('_elapsed_steps')}")
actions = [venv.action_space.sample() for _ in range(num_envs)]
obs, rew, done, info = venv.step(actions)
actions = venv.action_space.sample()
obs, rew, done, _, info = venv.step(actions)
if render:
venv.render()
venv.call("render")
step += 1
venv.close()
xserver.terminate()
27 changes: 27 additions & 0 deletions minetester/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,30 @@ def start_xserver(
]
xserver_process = subprocess.Popen(cmd)
return xserver_process


def read_config_file(file_path):
config = {}
with open(file_path, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
if value.isdigit():
value = int(value)
elif value.replace('.', '', 1).isdigit():
value = float(value)
elif value.lower() == 'true':
value = True
elif value.lower() == 'false':
value = False
config[key] = value
return config


def write_config_file(file_path, config):
with open(file_path, 'w') as f:
for key, value in config.items():
f.write(f'{key} = {value}\n')

0 comments on commit d1afd70

Please sign in to comment.