diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 00000000..873d3c5c --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,29 @@ +name: install +on: + push: + pull_request: + +jobs: + test: + name: test ${{ matrix.py }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + py: + - "3.11" + - "3.10" + - "3.9" + steps: + - name: Setup python for test ${{ matrix.py }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py }} + - uses: actions/checkout@v3 + - name: Upgrade pip + run: python -m pip install -U pip + - name: Install pufferlib + run: pip3 install -e . diff --git a/.gitignore b/.gitignore index 5bb9cfc8..5906b917 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +# Annoying temp files generated by Cython +c_gae.c +pufferlib/extensions.c +pufferlib/ocean/grid/c_grid.c +pufferlib/ocean/tactical/c_tactical.c +pufferlib/puffernet.c + +# Raylib +raylib_wasm/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/MANIFEST.in b/MANIFEST.in index 454282d5..451161a7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,6 @@ global-include *.pyx global-include *.pxd +global-include *.h +global-include *.py +recursive-include pufferlib/resources * + diff --git a/README.md b/README.md index 387ec6c1..dc1bfaa8 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,16 @@ ![figure](https://pufferai.github.io/source/resource/header.png) [![PyPI version](https://badge.fury.io/py/pufferlib.svg)](https://badge.fury.io/py/pufferlib) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pufferlib) +![Github Actions](https://github.com/PufferAI/PufferLib/actions/workflows/install.yml/badge.svg) [![](https://dcbadge.vercel.app/api/server/spT4huaGYV?style=plastic)](https://discord.gg/spT4huaGYV) [![Twitter](https://img.shields.io/twitter/url/https/twitter.com/cloudposse.svg?style=social&label=Follow%20%40jsuarez5341)](https://twitter.com/jsuarez5341) -You have an environment, a PyTorch model, and a reinforcement learning framework that are designed to work together but don’t. PufferLib is a wrapper layer that makes RL on complex game environments as simple as RL on Atari. You write a native PyTorch network and a short binding for your environment; PufferLib takes care of the rest. +PufferLib is the reinforcement learning library I wish existed during my PhD. It started as a compatibility layer to make working with complex environments a breeze. Now, it's a high-performance toolkit for research and industry with optimized parallel simulation, environments that run and train at 1M+ steps/second, and tons of quality of life improvements for practitioners. All our tools are free and open source. We also offer priority service for companies, startups, and labs! -All of our [Documentation](https://pufferai.github.io "PufferLib Documentation") is hosted by github.io. @jsuarez5341 on [Discord](https://discord.gg/spT4huaGYV) for support -- post here before opening issues. I am also looking for contributors interested in adding bindings for other environments and RL frameworks. +![Trailer](https://github.com/PufferAI/puffer.ai/blob/main/docs/assets/puffer_2.gif?raw=true) -## Demo - -The current `demo.py` is a souped-up version of CleanRL PPO with optimized LSTM support, detailed performance metrics, a local dashboard, async envpool sampling, checkpointing, wandb sweeps, and more. It has a powerful `--help` that generates options based on the specified environment and policy. Hyperparams are in `config.yaml`. A few examples: - -``` -# Train minigrid with multiprocessing. Save it as a baseline. -python demo.py --env minigrid --mode train --vec multiprocessing -``` - -![figure](https://raw.githubusercontent.com/PufferAI/pufferai.github.io/1.0/docs/source/resource/puffer-dash.png) - -``` -# Load the current minigrid baseline and render it locally -python demo.py --env minigrid --mode eval --baseline - -# Train squared with serial vectorization and save it as a wandb baseline -# The, load the current squared baseline and render it locally -python demo.py --env squared --mode train --baseline -python demo.py --env squared --mode eval --baseline - -# Render NMMO locally with a random policy -python demo.py --env nmmo --mode eval - -# Autotune vectorization settings for your machine -python demo.py --env breakout --mode autotune -``` +All of our documentation is hosted at [puffer.ai](https://puffer.ai "PufferLib Documentation"). @jsuarez5341 on [Discord](https://discord.gg/puffer) for support -- post here before opening issues. We're always looking for new contributors, too! ## Star to puff up the project! diff --git a/clean_pufferl.py b/clean_pufferl.py index a21aef8e..247a8127 100644 --- a/clean_pufferl.py +++ b/clean_pufferl.py @@ -126,21 +126,6 @@ def evaluate(data): data.vecenv.send(actions) with profile.eval_misc: - # Moves into models... maybe. Definitely moves. - # You could also just return infos and have it in demo - if 'pokemon_exploration_map' in infos: - for pmap in infos['pokemon_exploration_map']: - if not hasattr(data, 'pokemon_map'): - import pokemon_red_eval - data.map_updater = pokemon_red_eval.map_updater() - data.pokemon_map = pmap - - data.pokemon_map = np.maximum(data.pokemon_map, pmap) - - if len(infos['pokemon_exploration_map']) > 0: - rendered = data.map_updater(data.pokemon_map) - data.stats['Media/exploration_map'] = data.wandb.Image(rendered) - for k, v in infos.items(): if '_map' in k and data.wandb is not None: data.stats[f'Media/{k}'] = data.wandb.Image(v[0]) @@ -518,13 +503,13 @@ def __init__(self, delay=1, maxlen=20): def run(self): while not self.stopped: - self.cpu_util.append(psutil.cpu_percent()) + self.cpu_util.append(100*psutil.cpu_percent()) mem = psutil.virtual_memory() - self.cpu_mem.append(mem.active / mem.total) + self.cpu_mem.append(100*mem.active/mem.total) if torch.cuda.is_available(): self.gpu_util.append(torch.cuda.utilization()) free, total = torch.cuda.mem_get_info() - self.gpu_mem.append(free / total) + self.gpu_mem.append(100*free/total) else: self.gpu_util.append(0) self.gpu_mem.append(0) @@ -598,7 +583,7 @@ def rollout(env_creator, env_kwargs, policy_cls, rnn_cls, agent_creator, agent_k frames = [] tick = 0 - while tick <= 1000: + while tick <= 2000: if tick % 1 == 0: render = driver.render() if driver.render_mode == 'ansi': @@ -703,7 +688,7 @@ def print_dashboard(env_name, utilization, global_step, epoch, table.add_column(justify="center", width=13) table.add_column(justify="right", width=13) table.add_row( - f':blowfish: {c1}PufferLib {b2}1.0.0', + f':blowfish: {c1}PufferLib {b2}2.0.0', f'{c1}CPU: {c3}{cpu_percent:.1f}%', f'{c1}GPU: {c3}{gpu_percent:.1f}%', f'{c1}DRAM: {c3}{dram_percent:.1f}%', diff --git a/config/ocean/go.ini b/config/ocean/go.ini index 481e04d3..9790355f 100644 --- a/config/ocean/go.ini +++ b/config/ocean/go.ini @@ -1,25 +1,53 @@ [base] package = ocean env_name = puffer_go -policy_name = Policy +policy_name = Go rnn_name = Recurrent [env] -num_envs = 512 +num_envs = 2048 +reward_move_pass = -0.47713279724121094 +reward_move_valid = 0 +reward_move_invalid = -0.47179355621337893 +reward_opponent_capture = -0.5240603446960449 +reward_player_capture = 0.22175729274749756 +grid_size = 7 [train] -total_timesteps = 150_000_000 +total_timesteps = 2_000_000_000 checkpoint_interval = 50 num_envs = 2 num_workers = 2 env_batch_size =1 -batch_size = 131072 -update_epochs = 4 -minibatch_size = 32768 +batch_size = 524288 +update_epochs = 1 +minibatch_size = 131072 bptt_horizon = 16 -learning_rate = 0.002 +learning_rate = 0.0015 +ent_coef = 0.013460194258584548 +gae_lambda = 0.90 +gamma = 0.95 +max_grad_norm = 0.8140400052070618 +vf_coef = 0.48416485817685223 anneal_lr = False -device = cuda +device = cpu +[sweep.parameters.env.parameters.reward_move_invalid] +distribution = uniform +min = -1.0 +max = 0.0 +[sweep.parameters.env.parameters.reward_move_pass] +distribution = uniform +min = -1.0 +max = 0.0 +[sweep.parameters.env.parameters.reward_player_capture] +distribution = uniform +min = 0.0 +max = 1.0 + +[sweep.parameters.env.parameters.reward_opponent_capture] +distribution = uniform +min = -1.0 +max = 0.0 diff --git a/config/nmmo3.ini b/config/ocean/nmmo3.ini similarity index 61% rename from config/nmmo3.ini rename to config/ocean/nmmo3.ini index b6549d5d..3b4d8329 100644 --- a/config/nmmo3.ini +++ b/config/ocean/nmmo3.ini @@ -1,19 +1,20 @@ [base] -package = nmmo3 +package = ocean env_name = nmmo3 -rnn_name = Recurrent +policy_name = NMMO3 +rnn_name = NMMO3LSTM [train] -total_timesteps = 10000000000 +total_timesteps = 107000000000 checkpoint_interval = 1000 -learning_rate = 0.000972332726277282 +learning_rate = 0.0004573146765703167 num_envs = 2 num_workers = 2 env_batch_size = 1 update_epochs = 1 -gamma = 0.8602833367538562 -gae_lambda = 0.916381394950097 -ent_coef = 0.004758679104391214 +gamma = 0.7647543366891623 +gae_lambda = 0.996005622445478 +ent_coef = 0.01210084358004069 max_grad_norm = 0.6075578331947327 vf_coef = 0.3979089612467003 bptt_horizon = 16 @@ -23,12 +24,11 @@ compile = False anneal_lr = False [env] -reward_combat_level = 1.305025339126587 -reward_prof_level = 1.1842153072357178 -reward_item_level = 1.0236146450042725 +reward_combat_level = 2.9437930583953857 +reward_prof_level = 1.445250153541565 +reward_item_level = 1.3669428825378418 reward_market = 0 -#reward_market = 0.23154078423976895 -reward_death_mmo = -1.033899426460266 +reward_death = -2.46451187133789 [sweep.metric] goal = maximize @@ -49,11 +49,6 @@ distribution = uniform min = 0.0 max = 5.0 -[sweep.parameters.env.parameters.reward_market] -distribution = uniform -min = 0.0 -max = 1.0 - [sweep.parameters.env.parameters.reward_death_mmo] distribution = uniform min = -5.0 @@ -61,5 +56,6 @@ max = 0.0 [sweep.parameters.train.parameters.total_timesteps] distribution = uniform -min = 300_000_000 -max = 100_000_000_000 +min = 1_000_000_000 +max = 10_000_000_000 + diff --git a/config/ocean/pysquared.ini b/config/ocean/pysquared.ini new file mode 100644 index 00000000..bceb22db --- /dev/null +++ b/config/ocean/pysquared.ini @@ -0,0 +1,21 @@ +[base] +package = ocean +env_name = puffer_pysquared +policy_name = Policy +rnn_name = Recurrent + +[env] +num_envs = 1 + +[train] +total_timesteps = 40_000_000 +checkpoint_interval = 50 +num_envs = 12288 +num_workers = 12 +env_batch_size = 4096 +batch_size = 131072 +update_epochs = 1 +minibatch_size = 8192 +learning_rate = 0.0017 +anneal_lr = False +device = cuda diff --git a/config/ocean/trash_pickup.ini b/config/ocean/trash_pickup.ini new file mode 100644 index 00000000..9a07defa --- /dev/null +++ b/config/ocean/trash_pickup.ini @@ -0,0 +1,64 @@ +[base] +package = ocean +env_name = trash_pickup puffer_trash_pickup +policy_name = TrashPickup +rnn_name = Recurrent + +[env] +num_envs = 1024 # Recommended: 4096 (recommended start value) / num_agents +grid_size = 10 +num_agents = 4 +num_trash = 20 +num_bins = 1 +max_steps = 150 +report_interval = 32 +agent_sight_range = 5 # only used with 2D local crop obs space + +[train] +total_timesteps = 100_000_000 +checkpoint_interval = 200 +num_envs = 2 +num_workers = 2 +env_batch_size = 1 +batch_size = 131072 +update_epochs = 1 +minibatch_size = 16384 +bptt_horizon = 8 +anneal_lr = False +device = cuda +learning_rate=0.001 +gamma = 0.95 +gae_lambda = 0.85 +vf_ceof = 0.4 +clip_coef = 0.1 +vf_clip_coef = 0.1 +ent_coef = 0.01 + +[sweep.metric] +goal = maximize +name = environment/episode_return + +[sweep.parameters.train.parameters.learning_rate] +distribution = log_uniform_values +min = 0.000001 +max = 0.01 + +[sweep.parameters.train.parameters.gamma] +distribution = uniform +min = 0 +max = 1 + +[sweep.parameters.train.parameters.gae_lambda] +distribution = uniform +min = 0 +max = 1 + +[sweep.parameters.train.parameters.update_epochs] +distribution = int_uniform +min = 1 +max = 4 + +[sweep.parameters.train.parameters.ent_coef] +distribution = log_uniform_values +min = 1e-5 +max = 1e-1 diff --git a/demo.py b/demo.py index b9dec3e9..fbe93994 100644 --- a/demo.py +++ b/demo.py @@ -430,7 +430,7 @@ def train(args, make_env, policy_cls, rnn_cls, wandb, rnn_name = args['base']['rnn_name'] rnn_cls = None if rnn_name is not None: - rnn_cls = getattr(env_module, args['base']['rnn_name']) + rnn_cls = getattr(env_module.torch, args['base']['rnn_name']) if args['baseline']: assert args['mode'] in ('train', 'eval', 'evaluate') diff --git a/pokemon_red_eval.py b/pokemon_red_eval.py deleted file mode 100644 index 852780ea..00000000 --- a/pokemon_red_eval.py +++ /dev/null @@ -1,84 +0,0 @@ -# One-off demo for pokemon red because there isn't a clean way to put -# the custom map overlay logic into the clean_pufferl file and I want -# to keep that file as minimal as possible -from pufferlib import namespace -import numpy as np -import torch -from functools import partial - -def map_updater(): - import cv2 - bg = cv2.imread('kanto_map_dsv.png') - return partial(make_pokemon_red_overlay, bg) - -def make_pokemon_red_overlay(bg, counts): - nonzero = np.where(counts > 0, 1, 0) - scaled = np.clip(counts, 0, 1000) / 1000.0 - - # Convert counts to hue map - hsv = np.zeros((*counts.shape, 3)) - hsv[..., 0] = 2*(1-scaled)/3 - hsv[..., 1] = nonzero - hsv[..., 2] = nonzero - - # Convert the HSV image to RGB - import matplotlib.colors as mcolors - overlay = 255*mcolors.hsv_to_rgb(hsv) - - # Upscale to 16x16 - kernel = np.ones((16, 16, 1), dtype=np.uint8) - overlay = np.kron(overlay, kernel).astype(np.uint8) - mask = np.kron(nonzero, kernel[..., 0]).astype(np.uint8) - mask = np.stack([mask, mask, mask], axis=-1).astype(bool) - - # Combine with background - render = bg.copy().astype(np.int32) - render[mask] = 0.2*render[mask] + 0.8*overlay[mask] - render = np.clip(render, 0, 255).astype(np.uint8) - return render - -def rollout(env_creator, env_kwargs, agent_creator, agent_kwargs, model_path=None, device='cuda', verbose=True): - env = env_creator(**env_kwargs) - if model_path is None: - agent = agent_creator(env, **agent_kwargs) - else: - agent = torch.load(model_path, map_location=device) - - terminal = truncated = True - - import cv2 - bg = cv2.imread('kanto_map_dsv.png') - - while True: - if terminal or truncated: - if verbose: - print('--- Reset ---') - - ob, info = env.reset() - state = None - step = 0 - return_val = 0 - - ob = torch.tensor(ob).unsqueeze(0).to(device) - with torch.no_grad(): - if hasattr(agent, 'lstm'): - action, _, _, _, state = agent.get_action_and_value(ob, state) - else: - action, _, _, _ = agent.get_action_and_value(ob) - - ob, reward, terminal, truncated, _ = env.step(action[0].item()) - return_val += reward - - counts_map = env.env.counts_map - if np.sum(counts_map) > 0 and step % 500 == 0: - overlay = make_pokemon_red_overlay(bg, counts_map) - cv2.imshow('Pokemon Red', overlay[1000:][::4, ::4]) - cv2.waitKey(1) - - if verbose: - print(f'Step: {step} Reward: {reward:.4f} Return: {return_val:.2f}') - - if not env_kwargs['headless']: - env.render() - - step += 1 diff --git a/pufferlib/emulation.py b/pufferlib/emulation.py index 20a77738..8b8089b7 100644 --- a/pufferlib/emulation.py +++ b/pufferlib/emulation.py @@ -9,7 +9,7 @@ import pufferlib import pufferlib.spaces from pufferlib import utils, exceptions - +from pufferlib.environment import set_buffers from pufferlib.spaces import Discrete, Tuple, Dict @@ -23,12 +23,14 @@ def emulate(struct, sample): else: struct[()] = sample -def make_buffer(arr_dtype, struct_dtype, n=None): +def make_buffer(arr_dtype, struct_dtype, struct, n=None): '''None instead of 1 makes it work for 1 agent PZ envs''' + ''' if n is None: struct = np.zeros(1, dtype=struct_dtype) else: struct = np.zeros(n, dtype=struct_dtype) + ''' arr = struct.view(arr_dtype) @@ -37,11 +39,6 @@ def make_buffer(arr_dtype, struct_dtype, n=None): else: arr = arr.reshape(n, -1) - return arr, struct - -def emulate_copy(sample, arr_dtype, struct_dtype): - arr, struct = make_buffer(arr_dtype, struct_dtype) - emulate(struct, sample) return arr def _nativize(struct, space): @@ -60,10 +57,12 @@ def nativize(arr, space, struct_dtype): struct = np.asarray(arr).view(struct_dtype)[0] return _nativize(struct, space) +''' try: from pufferlib.extensions import emulate, nativize except ImportError: warnings.warn('PufferLib Cython extensions not installed. Using slow Python versions') +''' def dtype_from_space(space): if isinstance(space, pufferlib.spaces.Tuple): @@ -74,6 +73,10 @@ def dtype_from_space(space): dtype = [] for k, value in space.items(): dtype.append((k, dtype_from_space(value))) + elif isinstance(space, (pufferlib.spaces.Discrete)): + dtype = (np.int32, ()) + elif isinstance(space, (pufferlib.spaces.MultiDiscrete)): + dtype = (np.int32, (len(space.nvec),)) else: dtype = (space.dtype, space.shape) @@ -112,9 +115,10 @@ def emulate_observation_space(space): return emulated_space, emulated_dtype def emulate_action_space(space): - if isinstance(space, (pufferlib.spaces.Discrete, - pufferlib.spaces.MultiDiscrete, pufferlib.spaces.Box)): + if isinstance(space, pufferlib.spaces.Box): return space, space.dtype + elif isinstance(space, (pufferlib.spaces.Discrete, pufferlib.spaces.MultiDiscrete)): + return space, np.int32 emulated_dtype = dtype_from_space(space) leaves = flatten_space(space) @@ -123,7 +127,7 @@ def emulate_action_space(space): class GymnasiumPufferEnv(gymnasium.Env): - def __init__(self, env=None, env_creator=None, env_args=[], env_kwargs={}): + def __init__(self, env=None, env_creator=None, env_args=[], env_kwargs={}, buf=None): self.env = make_object(env, env_creator, env_args, env_kwargs) self.initialized = False @@ -147,52 +151,41 @@ def __init__(self, env=None, env_creator=None, env_args=[], env_kwargs={}): emulated_observation_dtype = self.obs_dtype, ) - self.buf = None # Injected buffer for shared memory optimization - self.obs, self.obs_struct = make_buffer( - self.single_observation_space.dtype, self.obs_dtype) self.render_modes = 'human rgb_array'.split() + set_buffers(self, buf) + if isinstance(self.env.observation_space, pufferlib.spaces.Box): + self.obs_struct = self.observations + else: + self.obs_struct = self.observations.view(self.obs_dtype) + @property def render_mode(self): return self.env.render_mode - def _emulate(self, ob): - if self.is_obs_emulated: - emulate(self.obs_struct, ob) - elif self.buf is not None: - self.obs[:] = ob - else: - self.obs = ob - def seed(self, seed): self.env.seed(seed) def reset(self, seed=None): - if not self.initialized: - if self.buf is not None: - self.obs = self.buf.observations[0] - - if self.is_obs_emulated: - self.obs_struct = self.obs.view(self.obs_dtype) - self.initialized = True self.done = False ob, info = _seed_and_reset(self.env, seed) - self._emulate(ob) - if not self.is_observation_checked: self.is_observation_checked = check_space( - self.obs, self.observation_space) - - buf = self.buf - if buf is not None: - buf.rewards[0] = 0 - buf.terminals[0] = False - buf.truncations[0] = False - buf.masks[0] = True + ob, self.env.observation_space) + + if self.is_obs_emulated: + emulate(self.obs_struct, ob) + else: + self.observations[:] = ob + + self.rewards[0] = 0 + self.terminals[0] = False + self.truncations[0] = False + self.masks[0] = True - return self.obs, info + return self.observations, info def step(self, action): '''Execute an action and return (observation, reward, done, info)''' @@ -215,17 +208,19 @@ def step(self, action): action, self.env.action_space) ob, reward, done, truncated, info = self.env.step(action) - self._emulate(ob) - - buf = self.buf - if buf is not None: - buf.rewards[0] = reward - buf.terminals[0] = done - buf.truncations[0] = truncated - buf.masks[0] = True - + + if self.is_obs_emulated: + emulate(self.obs_struct, ob) + else: + self.observations[:] = ob + + self.rewards[0] = reward + self.terminals[0] = done + self.truncations[0] = truncated + self.masks[0] = True + self.done = done or truncated - return self.obs, reward, done, truncated, info + return self.observations, reward, done, truncated, info def render(self): return self.env.render() @@ -234,7 +229,7 @@ def close(self): return self.env.close() class PettingZooPufferEnv: - def __init__(self, env=None, env_creator=None, env_args=[], env_kwargs={}, to_puffer=False): + def __init__(self, env=None, env_creator=None, env_args=[], buf=None, env_kwargs={}, to_puffer=False): self.env = make_object(env, env_creator, env_args, env_kwargs) self.to_puffer = to_puffer self.initialized = False @@ -260,12 +255,11 @@ def __init__(self, env=None, env_creator=None, env_args=[], env_kwargs={}, to_pu self.num_agents = len(self.possible_agents) - self.buf = None - - #self.obs = np.zeros((self.num_agents, *self.single_observation_space.shape), - # dtype=self.single_observation_space.dtype) - self.obs, self.obs_struct = make_buffer( - self.single_observation_space.dtype, self.obs_dtype, self.num_agents) + set_buffers(self, buf) + if isinstance(self.env.observation_space, pufferlib.spaces.Box): + self.obs_struct = self.observations + else: + self.obs_struct = self.observations.view(self.obs_dtype) @property def render_mode(self): @@ -283,14 +277,6 @@ def possible_agents(self): def done(self): return len(self.agents) == 0 or self.all_done - def _emulate(self, ob, i, agent): - if self.is_obs_emulated: - emulate(self.obs_struct[i], ob) - elif self.buf is not None: - self.obs[i] = ob - else: - self.dict_obs[agent] = ob - def observation_space(self, agent): '''Returns the observation space for a single agent''' if agent not in self.possible_agents: @@ -307,13 +293,7 @@ def action_space(self, agent): def reset(self, seed=None): if not self.initialized: - if self.buf is not None: - self.obs = self.buf.observations - - if self.is_obs_emulated: - self.obs_struct = self.obs.view(self.obs_dtype).reshape(self.num_agents, -1) - - self.dict_obs = {agent: self.obs[i] for i, agent in enumerate(self.possible_agents)} + self.dict_obs = {agent: self.observations[i] for i, agent in enumerate(self.possible_agents)} self.initialized = True self.all_done = False @@ -321,29 +301,28 @@ def reset(self, seed=None): obs, info = self.env.reset(seed=seed) + if not self.is_observation_checked: + for k, ob in obs.items(): + self.is_observation_checked = check_space( + ob, self.env.observation_space(k)) + # Call user featurizer and flatten the observations + self.observations[:] = 0 for i, agent in enumerate(self.possible_agents): if agent not in obs: - self.observation[i] = 0 continue ob = obs[agent] - self._emulate(ob, i, agent) self.mask[agent] = True - - if not self.is_observation_checked: - self.is_observation_checked = check_space( - self.dict_obs[self.possible_agents[0]], - self.single_observation_space - ) - - buf = self.buf - if buf is not None: - buf.rewards[:] = 0 - buf.terminals[:] = False - buf.truncations[:] = False - buf.masks[:] = True - + if self.is_obs_emulated: + emulate(self.obs_struct[i], ob) + else: + self.observations[i] = ob + + self.rewards[:] = 0 + self.terminals[:] = False + self.truncations[:] = False + self.masks[:] = True return self.dict_obs, info def step(self, actions): @@ -389,34 +368,35 @@ def step(self, actions): # TODO: Can add this assert once NMMO Horizon is ported to puffer # assert all(dones.values()) == (len(self.env.agents) == 0) self.mask = {k: False for k in self.possible_agents} + self.rewards[:] = 0 + self.terminals[:] = True + self.truncations[:] = False for i, agent in enumerate(self.possible_agents): # TODO: negative padding buf if agent not in obs: - self.obs[i] = 0 - buf = self.buf - if buf is not None: - buf.rewards[i] = 0 - buf.terminals[i] = True - buf.truncations[i] = False - buf.masks[i] = False + self.observations[i] = 0 + self.rewards[i] = 0 + self.terminals[i] = True + self.truncations[i] = False + self.masks[i] = False continue ob = obs[agent] self.mask[agent] = True - self._emulate(ob, i, agent) - - buf = self.buf - if buf is not None: - buf.rewards[i] = rewards[agent] - buf.terminals[i] = dones[agent] - buf.truncations[i] = truncateds[agent] - buf.masks[i] = True + if self.is_obs_emulated: + emulate(self.obs_struct[i], ob) + else: + self.observations[i] = ob + + self.rewards[i] = rewards[agent] + self.terminals[i] = dones[agent] + self.truncations[i] = truncateds[agent] + self.masks[i] = True self.all_done = all(dones.values()) or all(truncateds.values()) rewards = pad_agent_data(rewards, self.possible_agents, 0) dones = pad_agent_data(dones, self.possible_agents, True) # You changed this from false to match api test... is this correct? truncateds = pad_agent_data(truncateds, self.possible_agents, False) - return self.dict_obs, rewards, dones, truncateds, infos def render(self): diff --git a/pufferlib/environment.py b/pufferlib/environment.py index 41f02e53..bce092fb 100644 --- a/pufferlib/environment.py +++ b/pufferlib/environment.py @@ -8,6 +8,29 @@ calling super() before you have assigned the attribute. ''' +def set_buffers(env, buf=None): + if buf is None: + obs_space = env.single_observation_space + env.observations = np.zeros((env.num_agents, *obs_space.shape), dtype=obs_space.dtype) + env.rewards = np.zeros(env.num_agents, dtype=np.float32) + env.terminals = np.zeros(env.num_agents, dtype=bool) + env.truncations = np.zeros(env.num_agents, dtype=bool) + env.masks = np.ones(env.num_agents, dtype=bool) + + # TODO: Major kerfuffle on inferring action space dtype. This needs some asserts? + atn_space = env.single_action_space + if isinstance(env.single_action_space, pufferlib.spaces.Box): + env.actions = np.zeros((env.num_agents, *atn_space.shape), dtype=atn_space.dtype) + else: + env.actions = np.zeros((env.num_agents, *atn_space.shape), dtype=np.int32) + else: + env.observations = buf.observations + env.rewards = buf.rewards + env.terminals = buf.terminals + env.truncations = buf.truncations + env.masks = buf.masks + env.actions = buf.actions + class PufferEnv: def __init__(self, buf=None): if not hasattr(self, 'single_observation_space'): @@ -28,27 +51,7 @@ def __init__(self, buf=None): and not isinstance(self.single_action_space, pufferlib.spaces.Box)): raise APIUsageError('Native action_space must be a Discrete, MultiDiscrete, or Box') - if buf is None: - obs_space = self.single_observation_space - self.observations = np.zeros((self.num_agents, *obs_space.shape), dtype=obs_space.dtype) - self.rewards = np.zeros(self.num_agents, dtype=np.float32) - self.terminals = np.zeros(self.num_agents, dtype=bool) - self.truncations = np.zeros(self.num_agents, dtype=bool) - self.masks = np.ones(self.num_agents, dtype=bool) - - # TODO: Major kerfuffle on inferring action space dtype. This needs some asserts? - atn_space = self.single_action_space - if isinstance(self.single_action_space, pufferlib.spaces.Box): - self.actions = np.zeros((self.num_agents, *atn_space.shape), dtype=atn_space.dtype) - else: - self.actions = np.zeros((self.num_agents, *atn_space.shape), dtype=np.int32) - else: - self.observations = buf.observations - self.rewards = buf.rewards - self.terminals = buf.terminals - self.truncations = buf.truncations - self.masks = buf.masks - self.actions = buf.actions + set_buffers(self, buf) self.action_space = pufferlib.spaces.joint_space(self.single_action_space, self.num_agents) self.observation_space = pufferlib.spaces.joint_space(self.single_observation_space, self.num_agents) diff --git a/pufferlib/environments/atari/environment.py b/pufferlib/environments/atari/environment.py index 754fe694..604eb254 100644 --- a/pufferlib/environments/atari/environment.py +++ b/pufferlib/environments/atari/environment.py @@ -16,7 +16,7 @@ def env_creator(name='breakout'): def make(name, obs_type='grayscale', frameskip=4, full_action_space=False, framestack=1, - repeat_action_probability=0.0, render_mode='rgb_array'): + repeat_action_probability=0.0, render_mode='rgb_array', buf=None): '''Atari creation function''' pufferlib.environments.try_import('ale_py', 'AtariEnv') @@ -51,7 +51,7 @@ def make(name, obs_type='grayscale', frameskip=4, env = AtariPostprocessor(env) # Don't use standard postprocessor env = pufferlib.postprocess.EpisodeStats(env) - env = pufferlib.emulation.GymnasiumPufferEnv(env=env) + env = pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) return env class AtariPostprocessor(gym.Wrapper): diff --git a/pufferlib/environments/box2d/environment.py b/pufferlib/environments/box2d/environment.py index d2c75b85..f2fd12b7 100644 --- a/pufferlib/environments/box2d/environment.py +++ b/pufferlib/environments/box2d/environment.py @@ -11,11 +11,11 @@ def env_creator(name='car-racing'): return functools.partial(make, name=name) -def make(name, domain_randomize=True, continuous=False, render_mode='rgb_array'): +def make(name, domain_randomize=True, continuous=False, render_mode='rgb_array', buf=None): if name == 'car-racing': name = 'CarRacing-v2' env = gymnasium.make(name, render_mode=render_mode, domain_randomize=domain_randomize, continuous=continuous) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) diff --git a/pufferlib/environments/bsuite/environment.py b/pufferlib/environments/bsuite/environment.py index 6dfe4d7c..f39c661e 100644 --- a/pufferlib/environments/bsuite/environment.py +++ b/pufferlib/environments/bsuite/environment.py @@ -11,7 +11,7 @@ def env_creator(name='bandit/0'): return functools.partial(make, name) -def make(name='bandit/0', results_dir='experiments/bsuite', overwrite=True): +def make(name='bandit/0', results_dir='experiments/bsuite', overwrite=True, buf=None): '''BSuite environments''' bsuite = pufferlib.environments.try_import('bsuite') from bsuite.utils import gym_wrapper @@ -19,7 +19,7 @@ def make(name='bandit/0', results_dir='experiments/bsuite', overwrite=True): env = gym_wrapper.GymFromDMEnv(env) env = BSuiteStopper(env) env = pufferlib.wrappers.GymToGymnasium(env) - env = pufferlib.emulation.GymnasiumPufferEnv(env) + env = pufferlib.emulation.GymnasiumPufferEnv(env, buf=buf) return env class BSuiteStopper: diff --git a/pufferlib/environments/bsuite/squared.py b/pufferlib/environments/bsuite/squared.py deleted file mode 100644 index c5540d72..00000000 --- a/pufferlib/environments/bsuite/squared.py +++ /dev/null @@ -1,129 +0,0 @@ -from pdb import set_trace as T - -import numpy as np -import random -import gym - -from pufferlib import namespace - - -MOVES = [(0, -1), (0, 1), (-1, 0), (1, 0), (1, -1), (-1, -1), (1, 1), (-1, 1)] - -def all_possible_targets(n): - '''Generate all points on the perimeter of a square with side n''' - return ([(i, j) for i in range(n) for j in [0, n-1]] - + [(i, j) for i in [0, n-1] for j in range(1, n-1)]) - -def init(self, - distance_to_target=1, - num_targets=-1, - ): - '''Pufferlib Diamond environment - - Agent starts at the center of a square grid. - Targets are placed on the perimeter of the grid. - Reward is 1 minus the L-inf distance to the closest target. - This means that reward varies from -1 to 1. - Reward is not given for targets that have already been hit. - ''' - grid_size = 2 * distance_to_target + 1 - if num_targets == -1: - num_targets = 4 * distance_to_target - - return namespace(self, - distance_to_target=distance_to_target, - possible_targets=all_possible_targets(grid_size), - num_targets=num_targets, - grid_size = grid_size, - max_ticks = num_targets * distance_to_target, - observation_space=gym.spaces.Box(low=-1, high=1, shape=(grid_size, grid_size)), - action_space=gym.spaces.Discrete(8), - ) - -def reset(state, seed=None): - if seed is not None: - random.seed(seed) - np.random.seed(seed) - - # Allocating a new grid is faster than resetting an old one - state.grid = np.zeros((state.grid_size, state.grid_size), dtype=np.float32) - state.grid[state.distance_to_target, state.distance_to_target] = -1 - state.agent_pos = (state.distance_to_target, state.distance_to_target) - state.tick = 0 - - state.targets = random.sample(state.possible_targets, state.num_targets) - for x, y in state.targets: - state.grid[x, y] = 1 - - return state.grid, {} - -def step(state, action): - x, y = state.agent_pos - state.grid[x, y] = 0 - - dx, dy = MOVES[action] - x += dx - y += dy - - min_dist = min([max(abs(x-tx), abs(y-ty)) for tx, ty in state.targets]) - # This reward function will return 0.46 average reward for an unsuccessful - # episode with distance_to_target=4 and num_targets=1 (0.5 for solve) - # It looks reasonable but is not very discriminative - reward = 1 - min_dist / state.distance_to_target - - # This reward function will return 1 when the agent moves in the right direction - # (plus an adjustment for the 0 reset reward) to average 1 for success - # It is not much better than the previous one. - #reward = state.distance_to_target - min_dist - state.tick + 1/state.max_ticks - - # This function will return 0, 0.2, 0.4, ... 1 for successful episodes (n=5) - # And will drop rewards to 0 or less as soon as an error is made - # Somewhat smoother but actually worse than the previous ones - # reward = (state.distance_to_target - min_dist - state.tick) / (state.max_ticks - state.tick) - - - # This one nicely tracks the task completed metric but does not optimize well - #if state.distance_to_target - min_dist - state.tick == 1: - # reward = 1 - #else: - # reward = -state.tick - - - if (x, y) in state.targets: - state.targets.remove((x, y)) - #state.grid[x, y] = 0 - - dist_from_origin = max(abs(x-state.distance_to_target), abs(y-state.distance_to_target)) - if dist_from_origin >= state.distance_to_target: - state.agent_pos = state.distance_to_target, state.distance_to_target - else: - state.agent_pos = x, y - - state.grid[state.agent_pos] = -1 - state.tick += 1 - - done = state.tick >= state.max_ticks - info = {'targets_hit': state.num_targets - len(state.targets)} if done else {} - - return state.grid, reward, done, False, info - -def render(state): - for row in state.grid: - for val in row: - if val == 1: - color = 94 # Blue - elif val == -1: - color = 91 # Red - else: - color = 90 # Gray - print(f'\033[{color}m██\033[0m', end='') # Gray block - print() - print() - -class Squared(gym.Env): - __init__ = init - reset = reset - step = step - render = render - - diff --git a/pufferlib/environments/butterfly/environment.py b/pufferlib/environments/butterfly/environment.py index cd7a1eae..994e53e9 100644 --- a/pufferlib/environments/butterfly/environment.py +++ b/pufferlib/environments/butterfly/environment.py @@ -9,7 +9,7 @@ def env_creator(name='cooperative_pong_v5'): return functools.partial(make, name) -def make(name): +def make(name, buf=None): pufferlib.environments.try_import('pettingzoo.butterfly', 'butterfly') if name == 'cooperative_pong_v5': from pettingzoo.butterfly import cooperative_pong_v5 as pong @@ -22,4 +22,4 @@ def make(name): env = env_cls() env = aec_to_parallel_wrapper(env) - return pufferlib.emulation.PettingZooPufferEnv(env=env) + return pufferlib.emulation.PettingZooPufferEnv(env=env, buf=buf) diff --git a/pufferlib/environments/classic_control/environment.py b/pufferlib/environments/classic_control/environment.py index 9cdc8ff4..9da76d3e 100644 --- a/pufferlib/environments/classic_control/environment.py +++ b/pufferlib/environments/classic_control/environment.py @@ -15,7 +15,7 @@ def env_creator(name='cartpole'): return functools.partial(make, name) -def make(name, render_mode='rgb_array'): +def make(name, render_mode='rgb_array', buf=None): '''Create an environment by name''' if name in ALIASES: @@ -30,7 +30,7 @@ def make(name, render_mode='rgb_array'): #env = gymnasium.wrappers.NormalizeReward(env, gamma=gamma) env = gymnasium.wrappers.TransformReward(env, lambda reward: np.clip(reward, -1, 1)) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class MountainCarWrapper(gymnasium.Wrapper): def step(self, action): diff --git a/pufferlib/environments/classic_control_continuous/environment.py b/pufferlib/environments/classic_control_continuous/environment.py index a4cda797..47f29365 100644 --- a/pufferlib/environments/classic_control_continuous/environment.py +++ b/pufferlib/environments/classic_control_continuous/environment.py @@ -9,7 +9,7 @@ def env_creator(name='MountainCarContinuous-v0'): return functools.partial(make, name) -def make(name, render_mode='rgb_array'): +def make(name, render_mode='rgb_array', buf=None): '''Create an environment by name''' env = gymnasium.make(name, render_mode=render_mode) if name == 'MountainCarContinuous-v0': @@ -17,7 +17,7 @@ def make(name, render_mode='rgb_array'): env = pufferlib.postprocess.ClipAction(env) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class MountainCarWrapper(gymnasium.Wrapper): def step(self, action): diff --git a/pufferlib/environments/crafter/environment.py b/pufferlib/environments/crafter/environment.py index a883edc8..e320e347 100644 --- a/pufferlib/environments/crafter/environment.py +++ b/pufferlib/environments/crafter/environment.py @@ -19,7 +19,7 @@ def observation(self, observation): def env_creator(name='crafter'): return functools.partial(make, name) -def make(name): +def make(name, buf=None): '''Crafter creation function''' if name == 'crafter': name = 'CrafterReward-v1' @@ -31,7 +31,7 @@ def make(name): env = RenderWrapper(env) env = TransposeObs(env) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class RenderWrapper(gym.Wrapper): def __init__(self, env): diff --git a/pufferlib/environments/dm_control/environment.py b/pufferlib/environments/dm_control/environment.py index 32984f63..f492fa65 100644 --- a/pufferlib/environments/dm_control/environment.py +++ b/pufferlib/environments/dm_control/environment.py @@ -16,9 +16,9 @@ def env_creator(name='walker'): not support continuous action spaces.''' return functools.partial(make, name) -def make(name, task_name='walk'): - '''No PufferLib support for continuous actions yet.''' +def make(name, task_name='walk', buf=None): + '''Untested. Let us know in Discord if you want to use dmc in PufferLib.''' dm_control = pufferlib.environments.try_import('dm_control.suite', 'dmc') env = dm_control.suite.load(name, task_name) env = shimmy.DmControlCompatibilityV0(env=env) - return pufferlib.emulation.GymnasiumPufferEnv(env) + return pufferlib.emulation.GymnasiumPufferEnv(env, buf=buf) diff --git a/pufferlib/environments/dm_lab/environment.py b/pufferlib/environments/dm_lab/environment.py index 0c48edac..17bbde77 100644 --- a/pufferlib/environments/dm_lab/environment.py +++ b/pufferlib/environments/dm_lab/environment.py @@ -14,10 +14,11 @@ def env_creator(name='seekavoid_arena_01'): dm-lab requires extensive setup. Use PufferTank.''' return functools.partial(make, name=name) -def make(name): +def make(name, buf=None): '''Deepmind Lab binding creation function - dm-lab requires extensive setup. Use PufferTank.''' + dm-lab requires extensive setup. Currently dropped frop PufferTank. + Let us know if you need this for your work.''' dm_lab = pufferlib.environments.try_import('deepmind_lab', 'dm-lab') env = dm_lab.Lab(name, ['RGB_INTERLEAVED']) env = shimmy.DmLabCompatibilityV0(env=env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) diff --git a/pufferlib/environments/griddly/environment.py b/pufferlib/environments/griddly/environment.py index 88f1bdda..fdf8c243 100644 --- a/pufferlib/environments/griddly/environment.py +++ b/pufferlib/environments/griddly/environment.py @@ -17,7 +17,7 @@ def env_creator(name='spiders'): return functools.partial(make, name) # TODO: fix griddly -def make(name): +def make(name, buf=None): '''Griddly creation function Note that Griddly environments do not have observation spaces until @@ -34,4 +34,4 @@ def make(name): env = shimmy.GymV21CompatibilityV0(env=env) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env) + return pufferlib.emulation.GymnasiumPufferEnv(env, buf=buf) diff --git a/pufferlib/environments/gvgai/environment.py b/pufferlib/environments/gvgai/environment.py index f1752326..fa4da86f 100644 --- a/pufferlib/environments/gvgai/environment.py +++ b/pufferlib/environments/gvgai/environment.py @@ -17,12 +17,12 @@ def env_creator(name='zelda'): return functools.partial(make, name) def make(name, obs_type='grayscale', frameskip=4, full_action_space=False, - repeat_action_probability=0.0, render_mode='rgb_array'): + repeat_action_probability=0.0, render_mode='rgb_array', buf=None): '''Atari creation function''' pufferlib.environments.try_import('gym_gvgai') env = gym.make(name) env = pufferlib.wrappers.GymToGymnasium(env) env = pufferlib.postprocess.EpisodeStats(env) - env = pufferlib.emulation.GymnasiumPufferEnv(env=env) + env = pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) return env diff --git a/pufferlib/environments/links_awaken/environment.py b/pufferlib/environments/links_awaken/environment.py index 46ac1e8c..8016c3cf 100644 --- a/pufferlib/environments/links_awaken/environment.py +++ b/pufferlib/environments/links_awaken/environment.py @@ -7,9 +7,9 @@ import pufferlib.emulation -def make_env(headless: bool = True, state_path=None): +def make_env(headless: bool = True, state_path=None, buf=None): '''Links Awakening''' env = env_creator(headless=headless, state_path=state_path) env = gymnasium.wrappers.ResizeObservation(env, shape=(72, 80)) return pufferlib.emulation.GymnasiumPufferEnv(env=env, - postprocessor_cls=pufferlib.emulation.BasicPostprocessor) + postprocessor_cls=pufferlib.emulation.BasicPostprocessor, buf=buf) diff --git a/pufferlib/environments/magent/environment.py b/pufferlib/environments/magent/environment.py index 273b40a3..10fbfc2a 100644 --- a/pufferlib/environments/magent/environment.py +++ b/pufferlib/environments/magent/environment.py @@ -11,7 +11,7 @@ def env_creator(name='battle_v4'): return functools.partial(make, name) pufferlib.environments.try_import('pettingzoo.magent', 'magent') -def make(name): +def make(name, buf=None): '''MAgent Battle V4 creation function''' if name == 'battle_v4': from pettingzoo.magent import battle_v4 @@ -22,4 +22,4 @@ def make(name): env = env_cls() env = aec_to_parallel_wrapper(env) env = pufferlib.wrappers.PettingZooTruncatedWrapper(env) - return pufferlib.emulation.PettingZooPufferEnv(env=env) + return pufferlib.emulation.PettingZooPufferEnv(env=env, buf=buf) diff --git a/pufferlib/environments/microrts/environment.py b/pufferlib/environments/microrts/environment.py index c2a1d834..6c00f4d5 100644 --- a/pufferlib/environments/microrts/environment.py +++ b/pufferlib/environments/microrts/environment.py @@ -12,7 +12,7 @@ def env_creator(name='GlobalAgentCombinedRewardEnv'): return functools.partial(make, name) -def make(name): +def make(name, buf=None): '''Gym MicroRTS creation function This library appears broken. Step crashes in Java. @@ -31,7 +31,7 @@ def make(name): env = MicroRTS(env) env = shimmy.GymV21CompatibilityV0(env=env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class MicroRTS: def __init__(self, env): diff --git a/pufferlib/environments/minerl/environment.py b/pufferlib/environments/minerl/environment.py index ffaa255b..771c3c60 100644 --- a/pufferlib/environments/minerl/environment.py +++ b/pufferlib/environments/minerl/environment.py @@ -13,7 +13,7 @@ def env_creator(name='MineRLBasaltFindCave-v0'): return functools.partial(make, name=name) -def make(name): +def make(name, buf=None): '''Minecraft environment creation function''' pufferlib.environments.try_import('minerl') @@ -25,4 +25,4 @@ def make(name): env = gym.make(name) env = shimmy.GymV21CompatibilityV0(env=env) - return pufferlib.emulation.GymnasiumPufferEnv(env) + return pufferlib.emulation.GymnasiumPufferEnv(env, buf=buf) diff --git a/pufferlib/environments/minigrid/environment.py b/pufferlib/environments/minigrid/environment.py index 116cfa93..b08ad959 100644 --- a/pufferlib/environments/minigrid/environment.py +++ b/pufferlib/environments/minigrid/environment.py @@ -15,7 +15,7 @@ def env_creator(name='minigrid'): return functools.partial(make, name=name) -def make(name, render_mode='rgb_array'): +def make(name, render_mode='rgb_array', buf=None): if name in ALIASES: name = ALIASES[name] @@ -23,7 +23,7 @@ def make(name, render_mode='rgb_array'): env = gymnasium.make(name, render_mode=render_mode) env = MiniGridWrapper(env) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class MiniGridWrapper: def __init__(self, env): diff --git a/pufferlib/environments/minihack/environment.py b/pufferlib/environments/minihack/environment.py index 8e393320..b5ac005f 100644 --- a/pufferlib/environments/minihack/environment.py +++ b/pufferlib/environments/minihack/environment.py @@ -22,7 +22,7 @@ def env_creator(name='minihack'): return functools.partial(make, name) -def make(name): +def make(name, buf=None): '''NetHack binding creation function''' if name in ALIASES: name = ALIASES[name] @@ -33,7 +33,7 @@ def make(name): env = gym.make(name, observation_keys=obs_key) env = shimmy.GymV21CompatibilityV0(env=env) env = MinihackWrapper(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class MinihackWrapper: def __init__(self, env): diff --git a/pufferlib/environments/mujoco/environment.py b/pufferlib/environments/mujoco/environment.py index 217bc8bf..4aad701e 100644 --- a/pufferlib/environments/mujoco/environment.py +++ b/pufferlib/environments/mujoco/environment.py @@ -12,7 +12,7 @@ import pufferlib.postprocess -def single_env_creator(env_name, capture_video, gamma, run_name=None, idx=None, obs_norm=True, pufferl=False): +def single_env_creator(env_name, capture_video, gamma, run_name=None, idx=None, obs_norm=True, pufferl=False, buf=None): if capture_video and idx == 0: assert run_name is not None, "run_name must be specified when capturing videos" env = gymnasium.make(env_name, render_mode="rgb_array") @@ -31,7 +31,7 @@ def single_env_creator(env_name, capture_video, gamma, run_name=None, idx=None, env = gymnasium.wrappers.TransformReward(env, lambda reward: np.clip(reward, -10, 10)) if pufferl is True: - env = pufferlib.emulation.GymnasiumPufferEnv(env=env) + env = pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) return env diff --git a/pufferlib/environments/nethack/environment.py b/pufferlib/environments/nethack/environment.py index 7debb1ec..c49ae260 100644 --- a/pufferlib/environments/nethack/environment.py +++ b/pufferlib/environments/nethack/environment.py @@ -13,7 +13,7 @@ def env_creator(name='nethack'): return functools.partial(make, name) -def make(name): +def make(name, buf=None): '''NetHack binding creation function''' if name == 'nethack': name = 'NetHackScore-v0' @@ -24,7 +24,7 @@ def make(name): env = shimmy.GymV21CompatibilityV0(env=env) env = NethackWrapper(env) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class NethackWrapper: def __init__(self, env): diff --git a/pufferlib/environments/nmmo/environment.py b/pufferlib/environments/nmmo/environment.py index 02cde1fa..f4ef1c99 100644 --- a/pufferlib/environments/nmmo/environment.py +++ b/pufferlib/environments/nmmo/environment.py @@ -12,14 +12,14 @@ def env_creator(name='nmmo'): return functools.partial(make, name) -def make(name, *args, **kwargs): +def make(name, *args, buf=None, **kwargs): '''Neural MMO creation function''' nmmo = pufferlib.environments.try_import('nmmo') env = nmmo.Env(*args, **kwargs) env = NMMOWrapper(env) env = pufferlib.postprocess.MultiagentEpisodeStats(env) env = pufferlib.postprocess.MeanOverAgents(env) - return pufferlib.emulation.PettingZooPufferEnv(env=env) + return pufferlib.emulation.PettingZooPufferEnv(env=env, buf=buf) class NMMOWrapper(pufferlib.postprocess.PettingZooWrapper): '''Remove task spam''' diff --git a/pufferlib/environments/nmmo3/__init__.py b/pufferlib/environments/nmmo3/__init__.py deleted file mode 100644 index eff86ef0..00000000 --- a/pufferlib/environments/nmmo3/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .environment import env_creator, make - -try: - import torch -except ImportError: - pass -else: - from .torch import Policy - try: - from .torch import Recurrent - except: - Recurrent = None diff --git a/pufferlib/environments/nmmo3/environment.py b/pufferlib/environments/nmmo3/environment.py deleted file mode 100644 index 87eb3ac5..00000000 --- a/pufferlib/environments/nmmo3/environment.py +++ /dev/null @@ -1,32 +0,0 @@ -import functools -from nmmo3 import PuffEnv - -def env_creator(name='nmmo3'): - return make - -def make(num_envs=1, reward_combat_level=1.0, - reward_prof_level=1.0, reward_item_level=0.5, - reward_market=0.01, reward_death_mmo=1.0, buf=None): - return PuffEnv( - width=4*[512], - height=4*[512], - num_envs=4, - num_players=1024, - num_enemies=2048, - num_resources=2048, - num_weapons=1024, - num_gems=512, - tiers=2, - levels=16, - teleportitis_prob=0.001, - enemy_respawn_ticks=2, - item_respawn_ticks=100, - x_window=7, - y_window=5, - reward_combat_level=reward_combat_level, - reward_prof_level=reward_prof_level, - reward_item_level=reward_item_level, - reward_market=reward_market, - reward_death=reward_death_mmo, - buf=buf, - ) diff --git a/pufferlib/environments/nmmo3/torch.py b/pufferlib/environments/nmmo3/torch.py deleted file mode 100644 index c924c872..00000000 --- a/pufferlib/environments/nmmo3/torch.py +++ /dev/null @@ -1,117 +0,0 @@ -from pdb import set_trace as T -import torch -from torch import nn -import torch.nn.functional as F -import numpy as np - -import pufferlib.models -import pufferlib.pytorch - - -class Recurrent(pufferlib.models.LSTMWrapper): - def __init__(self, env, policy, input_size=256, hidden_size=256, num_layers=1): - super().__init__(env, policy, input_size, hidden_size, num_layers) - -class Decompressor(nn.Module): - def __init__(self): - super().__init__() - factors = np.array([4, 4, 16, 5, 3, 5, 5, 6, 7, 4]) - n_channels = sum(factors) - add = np.array([0, *np.cumsum(factors).tolist()[:-1]])[None, :, None] - div = np.array([1, *np.cumprod(factors).tolist()[:-1]])[None, :, None] - - factors = torch.tensor(factors)[None, :, None].cuda() - add = torch.tensor(add).cuda() - div = torch.tensor(div).cuda() - - self.register_buffer('factors', factors) - self.register_buffer('add', add) - self.register_buffer('div', div) - - def forward(self, codes): - batch = codes.shape[0] - obs = torch.zeros(batch, 59, 11*15, device=codes.device) - codes = codes.view(codes.shape[0], 1, -1) - dec = self.add + (codes//self.div) % self.factors - obs.scatter_(1, dec, 1) - return obs.view(batch, 59, 11, 15) - -class PlayerProjEncoder(nn.Module): - def __init__(self, hidden_size): - super().__init__() - self.player_embed = nn.Embedding(128, 32) - self.player_continuous = pufferlib.pytorch.layer_init( - nn.Linear(47, hidden_size//2)) - self.discrete_proj = pufferlib.pytorch.layer_init( - nn.Linear(32*47, hidden_size//2)) - self.player_proj = pufferlib.pytorch.layer_init( - nn.Linear(hidden_size, hidden_size//2)) - - def forward(self, player): - player_discrete = self.player_embed(player.int()).view(player.shape[0], -1) - player_discrete = self.discrete_proj(player_discrete) - player_continuous = self.player_continuous(player.float()) - player = torch.cat([player_discrete, player_continuous], dim=1) - player = F.relu(player) - return self.player_proj(player) - -class Policy(nn.Module): - def __init__(self, env, hidden_size=256, output_size=256): - super().__init__() - #self.dtype = pufferlib.pytorch.nativize_dtype(env.emulated) - self.num_actions = env.single_action_space.n - self.decompressor = Decompressor() - self.factors = np.array([4, 4, 17, 5, 3, 5, 5, 5, 7, 4]) - self.offsets = torch.tensor([0] + list(np.cumsum(self.factors)[:-1])).cuda().view(1, -1, 1, 1) - self.cum_facs = np.cumsum(self.factors) - - self.multihot_dim = self.factors.sum() - - self.map_2d = nn.Sequential( - pufferlib.pytorch.layer_init(nn.Conv2d(self.multihot_dim, 64, 5, stride=3)), - nn.ReLU(), - pufferlib.pytorch.layer_init(nn.Conv2d(64, 64, 3, stride=1)), - nn.Flatten(), - ) - - self.player_discrete_encoder = nn.Sequential( - nn.Embedding(128, 32), - nn.Flatten(), - ) - - self.proj = nn.Sequential( - pufferlib.pytorch.layer_init(nn.Linear(1689, hidden_size)), - nn.ReLU(), - ) - - self.actor = pufferlib.pytorch.layer_init( - nn.Linear(output_size, self.num_actions), std=0.01) - self.value_fn = pufferlib.pytorch.layer_init(nn.Linear(output_size, 1), std=1) - - def forward(self, x): - hidden, lookup = self.encode_observations(x) - actions, value = self.decode_actions(hidden, lookup) - return actions, value - - def encode_observations(self, observations, unflatten=False): - batch = observations.shape[0] - ob_map = observations[:, :11*15*10].view(batch, 11, 15, 10) - ob_player = observations[:, 11*15*10:-10] - ob_reward = observations[:, -10:] - - map_buf = torch.zeros(batch, self.multihot_dim, 11, 15, device=ob_map.device, dtype=torch.float32) - codes = ob_map.permute(0, 3, 1, 2) + self.offsets - map_buf.scatter_(1, codes, 1) - ob_map = self.map_2d(map_buf) - - player_discrete = self.player_discrete_encoder(ob_player.int()) - - obs = torch.cat([ob_map, player_discrete, ob_player.float(), ob_reward], dim=1) - obs = self.proj(obs) - return obs, None - - def decode_actions(self, flat_hidden, lookup, concat=None): - #action = [self.actor(flat_hidden)] - action = self.actor(flat_hidden) - value = self.value_fn(flat_hidden) - return action, value diff --git a/pufferlib/environments/open_spiel/environment.py b/pufferlib/environments/open_spiel/environment.py index 6e1c5ea2..a5679b75 100644 --- a/pufferlib/environments/open_spiel/environment.py +++ b/pufferlib/environments/open_spiel/environment.py @@ -17,7 +17,8 @@ def make( multiplayer=False, n_rollouts=5, max_simulations=10, - min_simulations=None + min_simulations=None, + buf=None ): '''OpenSpiel creation function''' pyspiel = pufferlib.environments.try_import('pyspiel', 'open_spiel') @@ -50,5 +51,6 @@ def make( return wrapper_cls( env=env, postprocessor_cls=pufferlib.emulation.BasicPostprocessor, + buf=buf, ) diff --git a/pufferlib/environments/pokemon_red/environment.py b/pufferlib/environments/pokemon_red/environment.py index 2284b3c4..03acee58 100644 --- a/pufferlib/environments/pokemon_red/environment.py +++ b/pufferlib/environments/pokemon_red/environment.py @@ -12,12 +12,12 @@ def env_creator(name='pokemon_red'): return functools.partial(make, name) -def make(name, headless: bool = True, state_path=None): +def make(name, headless: bool = True, state_path=None, buf=None): '''Pokemon Red''' env = Environment(headless=headless, state_path=state_path) env = RenderWrapper(env) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class RenderWrapper(gymnasium.Wrapper): def __init__(self, env): diff --git a/pufferlib/environments/procgen/environment.py b/pufferlib/environments/procgen/environment.py index a50f5fb0..7dce8a1e 100644 --- a/pufferlib/environments/procgen/environment.py +++ b/pufferlib/environments/procgen/environment.py @@ -18,8 +18,8 @@ def env_creator(name='bigfish'): return functools.partial(make, name) -def make(name, num_envs=1, num_levels=0, - start_level=0, distribution_mode='easy', render_mode=None): +def make(name, num_envs=1, num_levels=0, start_level=0, + distribution_mode='easy', render_mode=None, buf=None): '''Atari creation function with default CleanRL preprocessing based on Stable Baselines3 wrappers''' assert int(num_envs) == float(num_envs), "num_envs must be an integer" num_envs = int(num_envs) @@ -47,7 +47,7 @@ def make(name, num_envs=1, num_levels=0, #envs = gymnasium.wrappers.FrameStack(envs, 4)#, framestack) #envs = MaxAndSkipEnv(envs, skip=2) envs = pufferlib.postprocess.EpisodeStats(envs) - return pufferlib.emulation.GymnasiumPufferEnv(env=envs) + return pufferlib.emulation.GymnasiumPufferEnv(env=envs, buf=buf) class ProcgenWrapper: def __init__(self, env): diff --git a/pufferlib/environments/slimevolley/environment.py b/pufferlib/environments/slimevolley/environment.py index eb7f67a5..a46d7956 100644 --- a/pufferlib/environments/slimevolley/environment.py +++ b/pufferlib/environments/slimevolley/environment.py @@ -15,7 +15,7 @@ def env_creator(name='SlimeVolley-v0'): return functools.partial(make, name) -def make(name, render_mode='rgb_array'): +def make(name, render_mode='rgb_array', buf=None): if name == 'slimevolley': name = 'SlimeVolley-v0' @@ -27,7 +27,7 @@ def make(name, render_mode='rgb_array'): env = SkipWrapper(env, repeat_count=4) env = shimmy.GymV21CompatibilityV0(env=env) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class SlimeVolleyMultiDiscrete(gym.Wrapper): def __init__(self, env): diff --git a/pufferlib/environments/smac/environment.py b/pufferlib/environments/smac/environment.py index 82b6ec27..e104e860 100644 --- a/pufferlib/environments/smac/environment.py +++ b/pufferlib/environments/smac/environment.py @@ -9,7 +9,7 @@ def env_creator(name='smac'): return functools.partial(make, name) -def make(name): +def make(name, buf=None): '''Starcraft Multiagent Challenge creation function Support for SMAC is WIP because environments do not function without @@ -19,5 +19,5 @@ def make(name): env = smac_env(1000) env = pufferlib.wrappers.PettingZooTruncatedWrapper(env) - env = pufferlib.emulation.PettingZooPufferEnv(env) + env = pufferlib.emulation.PettingZooPufferEnv(env, buf=buf) return env diff --git a/pufferlib/environments/stable_retro/environment.py b/pufferlib/environments/stable_retro/environment.py index 0f7be223..09270861 100644 --- a/pufferlib/environments/stable_retro/environment.py +++ b/pufferlib/environments/stable_retro/environment.py @@ -12,7 +12,7 @@ def env_creator(name='Airstriker-Genesis'): return functools.partial(make, name) -def make(name='Airstriker-Genesis', framestack=4): +def make(name='Airstriker-Genesis', framestack=4, buf=None): '''Atari creation function with default CleanRL preprocessing based on Stable Baselines3 wrappers''' retro = pufferlib.environments.try_import('retro', 'stable-retro') @@ -32,7 +32,7 @@ def make(name='Airstriker-Genesis', framestack=4): env = gym.wrappers.GrayScaleObservation(env) env = gym.wrappers.FrameStack(env, framestack) return pufferlib.emulation.GymnasiumPufferEnv( - env=env, postprocessor_cls=AtariFeaturizer) + env=env, postprocessor_cls=AtariFeaturizer, buf=buf) class AtariFeaturizer(pufferlib.emulation.Postprocessor): def reset(self, obs): diff --git a/pufferlib/environments/vizdoom/environment.py b/pufferlib/environments/vizdoom/environment.py index 5e0d6576..d98a263f 100644 --- a/pufferlib/environments/vizdoom/environment.py +++ b/pufferlib/environments/vizdoom/environment.py @@ -14,7 +14,7 @@ def env_creator(name='doom'): return functools.partial(make, name) -def make(name, framestack=1, render_mode='rgb_array'): +def make(name, framestack=1, render_mode='rgb_array', buf=None): '''Atari creation function with default CleanRL preprocessing based on Stable Baselines3 wrappers''' if name == 'doom': name = 'VizdoomHealthGatheringSupreme-v0' @@ -44,7 +44,7 @@ def make(name, framestack=1, render_mode='rgb_array'): #env = ClipRewardEnv(env) #env = gym.wrappers.GrayScaleObservation(env) #env = gym.wrappers.FrameStack(env, framestack) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) class DoomWrapper(gym.Wrapper): '''Gymnasium env does not expose proper options for screen scale and diff --git a/pufferlib/ocean/breakout/cy_breakout.pyx b/pufferlib/ocean/breakout/cy_breakout.pyx index fddf966f..484d9266 100644 --- a/pufferlib/ocean/breakout/cy_breakout.pyx +++ b/pufferlib/ocean/breakout/cy_breakout.pyx @@ -113,7 +113,11 @@ cdef class CyBreakout: def render(self): cdef Breakout* env = &self.envs[0] if self.client == NULL: + import os + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) self.client = make_client(env) + os.chdir(cwd) render(self.client, env) diff --git a/pufferlib/ocean/connect4/connect4.c b/pufferlib/ocean/connect4/connect4.c index 9409fab4..a5d7ddc2 100644 --- a/pufferlib/ocean/connect4/connect4.c +++ b/pufferlib/ocean/connect4/connect4.c @@ -1,7 +1,5 @@ #include "connect4.h" #include "puffernet.h" - -#include #include "time.h" const unsigned char NOOP = 8; @@ -23,6 +21,7 @@ void interactive() { float observations[42] = {0}; int actions[1] = {0}; + int tick = 0; while (!WindowShouldClose()) { env.actions[0] = NOOP; // user inputs 1 - 7 key pressed @@ -34,7 +33,7 @@ void interactive() { if(IsKeyPressed(KEY_FIVE)) env.actions[0] = 4; if(IsKeyPressed(KEY_SIX)) env.actions[0] = 5; if(IsKeyPressed(KEY_SEVEN)) env.actions[0] = 6; - } else { + } else if (tick % 30 == 0) { for (int i = 0; i < 42; i++) { observations[i] = env.observations[i]; } @@ -42,9 +41,9 @@ void interactive() { env.actions[0] = actions[0]; } + tick = (tick + 1) % 60; if (env.actions[0] >= 0 && env.actions[0] <= 6) { step(&env); - usleep(500000); } render(client, &env); diff --git a/pufferlib/ocean/connect4/cy_connect4.pyx b/pufferlib/ocean/connect4/cy_connect4.pyx index e9b3effe..abca0fb3 100644 --- a/pufferlib/ocean/connect4/cy_connect4.pyx +++ b/pufferlib/ocean/connect4/cy_connect4.pyx @@ -85,7 +85,11 @@ cdef class CyConnect4: def render(self): cdef CConnect4* env = &self.envs[0] if self.client == NULL: + import os + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) self.client = make_client(env.width, env.height) + os.chdir(cwd) render(self.client, env) diff --git a/pufferlib/ocean/enduro/cy_enduro.pyx b/pufferlib/ocean/enduro/cy_enduro.pyx index 5f0aecd2..40e9ba6b 100644 --- a/pufferlib/ocean/enduro/cy_enduro.pyx +++ b/pufferlib/ocean/enduro/cy_enduro.pyx @@ -120,7 +120,11 @@ cdef class CyEnduro: def render(self): if not self.client: + import os + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) self.client = make_client(&self.envs[0]) + os.chdir(cwd) c_render(self.client, &self.envs[0]) diff --git a/pufferlib/ocean/enduro/enduro.c b/pufferlib/ocean/enduro/enduro.c index eb127c49..fbdb3e74 100644 --- a/pufferlib/ocean/enduro/enduro.c +++ b/pufferlib/ocean/enduro/enduro.c @@ -11,21 +11,21 @@ #include "puffernet.h" void get_input(Enduro* env) { - if (IsKeyDown(KEY_DOWN) && IsKeyDown(KEY_RIGHT)) { + if ((IsKeyDown(KEY_DOWN) && IsKeyDown(KEY_RIGHT)) || (IsKeyDown(KEY_S) && IsKeyDown(KEY_D))) { env->actions[0] = ACTION_DOWNRIGHT; // Decelerate and move right - } else if (IsKeyDown(KEY_DOWN) && IsKeyDown(KEY_LEFT)) { + } else if ((IsKeyDown(KEY_DOWN) && IsKeyDown(KEY_LEFT)) || (IsKeyDown(KEY_S) && IsKeyDown(KEY_A))) { env->actions[0] = ACTION_DOWNLEFT; // Decelerate and move left - } else if (IsKeyDown(KEY_SPACE) && IsKeyDown(KEY_RIGHT)) { + } else if (IsKeyDown(KEY_SPACE) && (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D))) { env->actions[0] = ACTION_RIGHTFIRE; // Accelerate and move right - } else if (IsKeyDown(KEY_SPACE) && IsKeyDown(KEY_LEFT)) { + } else if (IsKeyDown(KEY_SPACE) && (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A))) { env->actions[0] = ACTION_LEFTFIRE; // Accelerate and move left } else if (IsKeyDown(KEY_SPACE)) { env->actions[0] = ACTION_FIRE; // Accelerate - } else if (IsKeyDown(KEY_DOWN)) { + } else if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) { env->actions[0] = ACTION_DOWN; // Decelerate - } else if (IsKeyDown(KEY_LEFT)) { + } else if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) { env->actions[0] = ACTION_LEFT; // Move left - } else if (IsKeyDown(KEY_RIGHT)) { + } else if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) { env->actions[0] = ACTION_RIGHT; // Move right } else { env->actions[0] = ACTION_NOOP; // No action @@ -33,28 +33,24 @@ void get_input(Enduro* env) { } int demo() { - // Weights* weights = load_weights("resources/puffer_enduro/enduro_weights.bin", 142218); - Weights* weights = load_weights("resources/puffer_enduro/gamma_0.910002_weights.bin", 142218); + Weights* weights = load_weights("resources/enduro/enduro_weights.bin", 142218); LinearLSTM* net = make_linearlstm(weights, 1, 68, 9); - Enduro env; - - env.num_envs = 1; - env.max_enemies = MAX_ENEMIES; - env.obs_size = OBSERVATIONS_MAX_SIZE; + Enduro env = { + .num_envs = 1, + .max_enemies = MAX_ENEMIES, + .obs_size = OBSERVATIONS_MAX_SIZE + }; allocate(&env); - Client* client = make_client(&env); unsigned int seed = 0; init(&env, seed, 0); reset(&env); - int running = 1; - while (running) { + while (!WindowShouldClose()) { if (IsKeyDown(KEY_LEFT_SHIFT)) { - env.actions[0] = 0; get_input(&env); } else { forward_linearlstm(net, env.observations, env.actions); @@ -62,26 +58,21 @@ int demo() { c_step(&env); c_render(client, &env); - - if (WindowShouldClose()) { - running = 0; - } } free_linearlstm(net); free(weights); close_client(client, &env); - cleanup(&client->gameState); free_allocated(&env); return 0; } void perftest(float test_time) { - Enduro env; - - env.num_envs = 1; - env.max_enemies = MAX_ENEMIES; - env.obs_size = OBSERVATIONS_MAX_SIZE; + Enduro env = { + .num_envs = 1, + .max_enemies = MAX_ENEMIES, + .obs_size = OBSERVATIONS_MAX_SIZE + }; allocate(&env); @@ -92,7 +83,6 @@ void perftest(float test_time) { int start = time(NULL); int i = 0; while (time(NULL) - start < test_time) { - get_input(&env); env.actions[0] = rand()%9; c_step(&env); i++; @@ -105,6 +95,6 @@ void perftest(float test_time) { int main() { demo(); -// perftest(30.0f); + //perftest(10.0f); return 0; } diff --git a/pufferlib/ocean/enduro/enduro.h b/pufferlib/ocean/enduro/enduro.h index 77af054c..f4033b92 100644 --- a/pufferlib/ocean/enduro/enduro.h +++ b/pufferlib/ocean/enduro/enduro.h @@ -1,9 +1,3 @@ -// puffer_enduro.h - -#ifndef M_PI -#define M_PI 3.14159265358979323846 -#endif - #include #include #include @@ -22,16 +16,16 @@ #define OBSERVATIONS_MAX_SIZE (8 + (5 * MAX_ENEMIES) + 9 + 1) #define TARGET_FPS 60 // Used to calculate wiggle spawn frequency #define LOG_BUFFER_SIZE 4096 -#define SCREEN_WIDTH 152 // 160 +#define SCREEN_WIDTH 152 #define SCREEN_HEIGHT 210 #define PLAYABLE_AREA_TOP 0 #define PLAYABLE_AREA_BOTTOM 154 -#define PLAYABLE_AREA_LEFT 0 // 8 -#define PLAYABLE_AREA_RIGHT 152 // 160 +#define PLAYABLE_AREA_LEFT 0 +#define PLAYABLE_AREA_RIGHT 152 #define ACTION_HEIGHT (PLAYABLE_AREA_BOTTOM - PLAYABLE_AREA_TOP) #define CAR_WIDTH 16 #define CAR_HEIGHT 11 -#define CRASH_NOOP_DURATION_CAR_VS_CAR 90 // 60 // How long controls are disabled after car v car collision +#define CRASH_NOOP_DURATION_CAR_VS_CAR 90 // How long controls are disabled after car v car collision #define CRASH_NOOP_DURATION_CAR_VS_ROAD 20 // How long controls are disabled after car v road edge collision #define INITIAL_CARS_TO_PASS 200 #define VANISHING_POINT_Y 52 @@ -45,18 +39,15 @@ #define MIN_SPEED -2.5f #define MAX_SPEED 7.5f #define ENEMY_CAR_SPEED 0.1f + +const float MAX_SPAWN_INTERVALS[] = {0.5f, 0.25f, 0.4f}; + // Times of day logic #define NUM_BACKGROUND_TRANSITIONS 16 // Seconds spent in each time of day -// static const float BACKGROUND_TRANSITION_TIMES[] = { -// 20.0f, 40.0f, 60.0f, 100.0f, 108.0f, 114.0f, 116.0f, 120.0f, -// 124.0f, 130.0f, 134.0f, 138.0f, 170.0f, 198.0f, 214.0f, 232.0f -// }; - -// For testing static const float BACKGROUND_TRANSITION_TIMES[] = { - 2.0f, 4.0f, 6.0f, 10.0f, 10.8f, 11.0f, 11.6f, 12.0f, - 12.4f, 13.0f, 14.0f, 15.0f, 16.0f, 17.0f, 18.0f, 19.0f + 20.0f, 40.0f, 60.0f, 100.0f, 108.0f, 114.0f, 116.0f, 120.0f, + 124.0f, 130.0f, 134.0f, 138.0f, 170.0f, 198.0f, 214.0f, 232.0f }; // Curve constants @@ -64,11 +55,18 @@ static const float BACKGROUND_TRANSITION_TIMES[] = { #define CURVE_LEFT -1 #define CURVE_RIGHT 1 #define NUM_LANES 3 +#define CURVE_VANISHING_POINT_SHIFT 55.0f +#define CURVE_PLAYER_SHIFT_FACTOR 0.025f // Moves player car towards outside edge of curves + +// Curve wiggle effect timing and amplitude +#define WIGGLE_AMPLITUDE 10.0f // Maximum 'bump-in' offset in pixels +#define WIGGLE_SPEED 10.1f // Speed at which the wiggle moves down the screen +#define WIGGLE_LENGTH 26.0f // Vertical length of the wiggle effect + + // Rendering constants -// Number of digits in the scoreboard #define SCORE_DIGITS 5 #define CARS_DIGITS 4 -// Digit dimensions #define DIGIT_WIDTH 8 #define DIGIT_HEIGHT 9 @@ -83,14 +81,7 @@ static const float BACKGROUND_TRANSITION_TIMES[] = { #define ROAD_LEFT_OFFSET 46.0f // Adjusted from 50.0f #define ROAD_RIGHT_OFFSET 47.0f // Adjusted from 51.0f -#define CURVE_VANISHING_POINT_SHIFT 55.0f -#define CURVE_PLAYER_SHIFT_FACTOR 0.025f // Moves player car towards outside edge of curves -// Constants for wiggle effect timing and amplitude -#define WIGGLE_AMPLITUDE 10.0f // 8.0f // Maximum 'bump-in' offset in pixels -#define WIGGLE_SPEED 10.1f // 10.1f // Speed at which the wiggle moves down the screen -#define WIGGLE_LENGTH 26.0f // 26.0f // Vertical length of the wiggle effect - -#define CONTINUOUS_SCALE (0) // Scale enemy cars continuously with y? +#define CONTINUOUS_SCALE (1) // Scale enemy cars continuously with y? // Log structs typedef struct Log { @@ -171,8 +162,6 @@ typedef struct GameState { int carsLeftGameState; int score; // Score for scoreboard rendering // Background state vars - float backgroundTransitionTimes[16]; - int backgroundIndex; int currentBackgroundIndex; int previousBackgroundIndex; float elapsedTime; @@ -496,7 +485,6 @@ void validate_position(Enduro* env); void validate_enemy_positions(Enduro* env); void update_time_of_day(Enduro* env); void accelerate(Enduro* env); -void compute_enemy_car_rewards(Enduro* env); void c_step(Enduro* env); void update_road_curve(Enduro* env); float quadratic_bezier(float bottom_x, float control_x, float top_x, float t); @@ -510,7 +498,7 @@ void close_client(Client* client, Enduro* env); void render_car(Client* client, GameState* gameState); // GameState rendering functions -void initRaylib(); +void initRaylib(GameState* gameState); void loadTextures(GameState* gameState); void updateCarAnimation(GameState* gameState); void updateScoreboard(GameState* gameState); @@ -602,9 +590,10 @@ void init(Enduro* env, int seed, int env_index) { if (seed == 0) { // Activate with seed==0 // Start the environment at the beginning of the day + env->rng_state = 0; env->elapsedTimeEnv = 0.0f; env->currentDayTimeIndex = 0; - env->previousDayTimeIndex = NUM_BACKGROUND_TRANSITIONS - 1; + env->previousDayTimeIndex = NUM_BACKGROUND_TRANSITIONS; } else { // Randomize elapsed time within the day's total duration float total_day_duration = BACKGROUND_TRANSITION_TIMES[NUM_BACKGROUND_TRANSITIONS - 1]; @@ -705,22 +694,29 @@ void init(Enduro* env, int seed, int env_index) { } // Randomize the initial time of day for each environment - float total_day_duration = BACKGROUND_TRANSITION_TIMES[15]; - env->elapsedTimeEnv = ((float)rand_r(&env->rng_state) / (float)RAND_MAX) * total_day_duration; - env->currentDayTimeIndex = 0; - env->dayTimeIndex = 0; - env->previousDayTimeIndex = 0; + if (env->rng_state == 0) { + env->elapsedTimeEnv = 0; + env->currentDayTimeIndex = 0; + env->dayTimeIndex = 0; + env->previousDayTimeIndex = 0; + } else { + float total_day_duration = BACKGROUND_TRANSITION_TIMES[15]; + env->elapsedTimeEnv = ((float)rand_r(&env->rng_state) / (float)RAND_MAX) * total_day_duration; + env->currentDayTimeIndex = 0; + env->dayTimeIndex = 0; + env->previousDayTimeIndex = 0; - // Advance currentDayTimeIndex to match randomized elapsedTimeEnv - for (int i = 0; i < NUM_BACKGROUND_TRANSITIONS; i++) { - if (env->elapsedTimeEnv >= env->dayTransitionTimes[i]) { - env->currentDayTimeIndex = i; - } else { - break; + // Advance currentDayTimeIndex to match randomized elapsedTimeEnv + for (int i = 0; i < NUM_BACKGROUND_TRANSITIONS; i++) { + if (env->elapsedTimeEnv >= env->dayTransitionTimes[i]) { + env->currentDayTimeIndex = i; + } else { + break; + } } - } - env->previousDayTimeIndex = (env->currentDayTimeIndex > 0) ? env->currentDayTimeIndex - 1 : NUM_BACKGROUND_TRANSITIONS - 1; + env->previousDayTimeIndex = (env->currentDayTimeIndex > 0) ? env->currentDayTimeIndex - 1 : NUM_BACKGROUND_TRANSITIONS - 1; + } env->terminals[0] = 0; env->truncateds[0] = 0; @@ -743,8 +739,6 @@ void init(Enduro* env, int seed, int env_index) { } void allocate(Enduro* env) { - env->rewards = (float*)malloc(sizeof(float) * env->num_envs); - memset(env->rewards, 0, sizeof(float) * env->num_envs); env->observations = (float*)calloc(env->obs_size, sizeof(float)); env->actions = (int*)calloc(1, sizeof(int)); env->rewards = (float*)calloc(1, sizeof(float)); @@ -760,7 +754,6 @@ void free_allocated(Enduro* env) { free(env->terminals); free(env->truncateds); free_logbuffer(env->log_buffer); - } // Called when a day is failed by player @@ -886,16 +879,16 @@ void reset(Enduro* env) { unsigned char check_collision(Enduro* env, Car* car) { // Compute the scale factor based on vanishing point reference - float scale = get_car_scale(car, car->y, CONTINUOUS_SCALE); + float depth = (car->y - VANISHING_POINT_Y) / (PLAYABLE_AREA_BOTTOM - VANISHING_POINT_Y); + float scale = fmax(0.1f, 0.9f * depth); float car_width = CAR_WIDTH * scale; float car_height = CAR_HEIGHT * scale; - // Compute car x position float car_center_x = car_x_in_lane(env, car->lane, car->y); float car_x = car_center_x - car_width / 2.0f; - return !(env->player_x > car_x + car_width || - env->player_x + CAR_WIDTH < car_x || - env->player_y > car->y + car_height || - env->player_y + CAR_HEIGHT < car->y); + return !(env->player_x > car_x + car_width + || env->player_x + CAR_WIDTH < car_x + || env->player_y > car->y + car_height + || env->player_y + CAR_HEIGHT < car->y); } // Determines which of the 3 lanes the player's car is in @@ -931,23 +924,64 @@ float get_car_scale(Car* car, float y, unsigned char continuous_scale) { } } -void add_enemy_car(Enduro* env, int lane, float y_offset) { - if (env->numEnemies >= MAX_ENEMIES) return; +void add_enemy_car(Enduro* env) { + if (env->numEnemies >= MAX_ENEMIES) { + return; + } + + int player_lane = get_player_lane(env); + int possible_lanes[NUM_LANES]; + int num_possible_lanes = 0; + + // Determine the furthest lane from the player + int furthest_lane; + if (player_lane == 0) { + furthest_lane = 2; + } else if (player_lane == 2) { + furthest_lane = 0; + } else { + // Player is in the middle lane + // Decide based on player's position relative to the road center + float player_center_x = env->player_x + CAR_WIDTH / 2.0f; + float road_center_x = (road_edge_x(env, env->player_y, 0, true) + + road_edge_x(env, env->player_y, 0, false)) / 2.0f; + if (player_center_x < road_center_x) { + furthest_lane = 2; // Player is on the left side, choose rightmost lane + } else { + furthest_lane = 0; // Player is on the right side, choose leftmost lane + } + } + + if (env->speed <= 0.0f) { + // Only spawn in the lane furthest from the player + possible_lanes[num_possible_lanes++] = furthest_lane; + } else { + for (int i = 0; i < NUM_LANES; i++) { + possible_lanes[num_possible_lanes++] = i; + } + } + + if (num_possible_lanes == 0) { + return; // Rare + } - // Initialize car + // Randomly select a lane + int lane = possible_lanes[rand() % num_possible_lanes]; + // Preferentially spawn in the last_spawned_lane 30% of the time + if (rand() % 100 < 60 && env->last_spawned_lane != -1) { + lane = env->last_spawned_lane; + } + env->last_spawned_lane = lane; + // Init car Car car = { .lane = lane, .x = car_x_in_lane(env, lane, VANISHING_POINT_Y), - .last_x = car_x_in_lane(env, lane, VANISHING_POINT_Y), .y = (env->speed > 0.0f) ? VANISHING_POINT_Y + 10.0f : PLAYABLE_AREA_BOTTOM + CAR_HEIGHT, - .last_y = (env->speed > 0.0f) ? VANISHING_POINT_Y + 10.0f : PLAYABLE_AREA_BOTTOM + CAR_HEIGHT, + .last_x = car_x_in_lane(env, lane, VANISHING_POINT_Y), + .last_y = VANISHING_POINT_Y, .passed = false, .colorIndex = rand() % 6 }; - - // Apply y_offset - car.y -= y_offset; - // Ensure minimum spacing between cars in the same lane float depth = (car.y - VANISHING_POINT_Y) / (PLAYABLE_AREA_BOTTOM - VANISHING_POINT_Y); float scale = fmax(0.1f, 0.9f * depth + 0.1f); @@ -959,15 +993,14 @@ void add_enemy_car(Enduro* env, int lane, float y_offset) { for (int i = 0; i < env->numEnemies; i++) { Car* existing_car = &env->enemyCars[i]; - if (existing_car->lane == car.lane) { - float y_distance = fabs(existing_car->y - car.y); - if (y_distance < min_spacing) { - // Too close, do not spawn this car - return; - } + if (existing_car->lane != car.lane) { + continue; + } + float y_distance = fabs(existing_car->y - car.y); + if (y_distance < min_spacing) { + return; // Too close, do not spawn this car } } - // Ensure not occupying all lanes within vertical range of 6 car lengths float min_vertical_range = 6.0f * CAR_HEIGHT; int lanes_occupied = 0; @@ -1008,13 +1041,13 @@ void update_time_of_day(Enduro* env) { env->currentDayTimeIndex = env->dayTimeIndex % 16; } -void validate_speed(Enduro* env) { +void clamp_speed(Enduro* env) { if (env->speed < env->min_speed || env->speed > env->max_speed) { - env->speed = fmaxf(env->min_speed, fminf(env->speed, env->max_speed)); // Clamp speed to valid range + env->speed = fmaxf(env->min_speed, fminf(env->speed, env->max_speed)); } } -void validate_gear(Enduro* env) { +void clamp_gear(Enduro* env) { if (env->currentGear < 0 || env->currentGear > 3) { env->currentGear = 0; } @@ -1106,8 +1139,8 @@ void validate_enemy_positions(Enduro* env) { } void accelerate(Enduro* env) { - validate_speed(env); - validate_gear(env); + clamp_speed(env); + clamp_gear(env); if (env->speed < env->max_speed) { // Gear transition @@ -1121,15 +1154,14 @@ void accelerate(Enduro* env) { float multiplier = (env->currentGear == 0) ? 4.0f : 2.0f; env->speed += accel * multiplier; - // Clamp speed - validate_speed(env); + clamp_speed(env); // Cap speed to gear threshold if (env->speed > env->gearSpeedThresholds[env->currentGear]) { env->speed = env->gearSpeedThresholds[env->currentGear]; } } - validate_speed(env); + clamp_speed(env); } void c_step(Enduro* env) { @@ -1216,12 +1248,12 @@ void c_step(Enduro* env) { } else { if (env->collision_cooldown_car_vs_car > 0) { - env->collision_cooldown_car_vs_car -= 1; - env->crashed_penalty = -0.01f; + env->collision_cooldown_car_vs_car -= 1; + env->crashed_penalty = -0.01f; } if (env->collision_cooldown_car_vs_road > 0) { - env->collision_cooldown_car_vs_road -= 1; - env->crashed_penalty = -0.01f; + env->collision_cooldown_car_vs_road -= 1; + env->crashed_penalty = -0.01f; } // Drift towards furthest road edge @@ -1336,94 +1368,95 @@ void c_step(Enduro* env) { // Enemy car logic for (int i = 0; i < env->numEnemies; i++) { - Car* car = &env->enemyCars[i]; + Car* car = &env->enemyCars[i]; - // Remove off-screen cars that move below the screen - if (car->y > PLAYABLE_AREA_BOTTOM + CAR_HEIGHT * 5) { - // Remove car from array if it moves below the screen - for (int j = i; j < env->numEnemies - 1; j++) { - env->enemyCars[j] = env->enemyCars[j + 1]; - } - env->numEnemies--; - i--; - continue; + // Remove off-screen cars that move below the screen + if (car->y > PLAYABLE_AREA_BOTTOM + CAR_HEIGHT * 5) { + // Remove car from array if it moves below the screen + for (int j = i; j < env->numEnemies - 1; j++) { + env->enemyCars[j] = env->enemyCars[j + 1]; } + env->numEnemies--; + i--; + continue; + } - // Remove cars that reach or surpass the logical vanishing point if moving up (player speed negative) - if (env->speed < 0 && car->y <= LOGICAL_VANISHING_Y) { - // Remove car from array if it reaches the logical vanishing point if moving down (player speed positive) - for (int j = i; j < env->numEnemies - 1; j++) { - env->enemyCars[j] = env->enemyCars[j + 1]; - } - env->numEnemies--; - i--; - continue; + // Remove cars that reach or surpass the logical vanishing point if moving up (player speed negative) + if (env->speed < 0 && car->y <= LOGICAL_VANISHING_Y) { + // Remove car from array if it reaches the logical vanishing point if moving down (player speed positive) + for (int j = i; j < env->numEnemies - 1; j++) { + env->enemyCars[j] = env->enemyCars[j + 1]; } - - // If the car is behind the player and speed ≤ 0, move it to the furthest lane - if (env->speed <= 0 && car->y >= env->player_y + CAR_HEIGHT) { - // Determine the furthest lane - int furthest_lane; - int player_lane = get_player_lane(env); - if (player_lane == 0) { - furthest_lane = 2; - } else if (player_lane == 2) { - furthest_lane = 0; + env->numEnemies--; + i--; + continue; + } + + // If the car is behind the player and speed <= 0, move it to the furthest lane + if (env->speed <= 0 && car->y >= env->player_y + CAR_HEIGHT) { + // Determine the furthest lane + int furthest_lane; + int player_lane = get_player_lane(env); + if (player_lane == 0) { + furthest_lane = 2; + } else if (player_lane == 2) { + furthest_lane = 0; + } else { + // Player is in the middle lane + // Decide based on player's position relative to the road center + float player_center_x = env->player_x + CAR_WIDTH / 2.0f; + float road_center_x = (road_edge_x(env, env->player_y, 0, true) + + road_edge_x(env, env->player_y, 0, false)) / 2.0f; + if (player_center_x < road_center_x) { + furthest_lane = 2; // Player is on the left side } else { - // Player is in the middle lane - // Decide based on player's position relative to the road center - float player_center_x = env->player_x + CAR_WIDTH / 2.0f; - float road_center_x = (road_edge_x(env, env->player_y, 0, true) + - road_edge_x(env, env->player_y, 0, false)) / 2.0f; - if (player_center_x < road_center_x) { - furthest_lane = 2; // Player is on the left side - } else { - furthest_lane = 0; // Player is on the right side - } + furthest_lane = 0; // Player is on the right side } - car->lane = furthest_lane; - continue; } + car->lane = furthest_lane; + continue; + } - // Check for passing logic **only if not on collision cooldown** - if (env->speed > 0 && car->last_y < env->player_y + CAR_HEIGHT && car->y >= env->player_y + CAR_HEIGHT && env->collision_cooldown_car_vs_car <= 0 && env->collision_cooldown_car_vs_road <= 0) { - if (env->carsToPass > 0) { - env->carsToPass -= 1; - } - if (!car->passed) { - env->log.passed_cars += 1; - env->rewards[0] += 1.0f; // Car passed reward - env->car_passed_no_crash_active = 1; // Stepwise rewards activated - env->step_rew_car_passed_no_crash += 0.001f; // Stepwise reward - } - car->passed = true; - } else if (env->speed < 0 && car->last_y > env->player_y && car->y <= env->player_y) { - int maxCarsToPass = (env->day == 1) ? 200 : 300; // Day 1: 200 cars, Day 2+: 300 cars - if (env->carsToPass == maxCarsToPass) { - // Do nothing; log the event - env->log.passed_by_enemy += 1.0f; - } else { - env->carsToPass += 1; - env->log.passed_by_enemy += 1.0f; - env->rewards[0] -= 0.1f; - } + // Check for passing logic **only if not on collision cooldown** + if (env->speed > 0 && car->last_y < env->player_y + CAR_HEIGHT + && car->y >= env->player_y + CAR_HEIGHT + && env->collision_cooldown_car_vs_car <= 0 + && env->collision_cooldown_car_vs_road <= 0) { + if (env->carsToPass > 0) { + env->carsToPass -= 1; } + if (!car->passed) { + env->log.passed_cars += 1; + env->rewards[0] += 1.0f; // Car passed reward + env->car_passed_no_crash_active = 1; // Stepwise rewards activated + env->step_rew_car_passed_no_crash += 0.001f; // Stepwise reward + } + car->passed = true; + } else if (env->speed < 0 && car->last_y > env->player_y && car->y <= env->player_y) { + int maxCarsToPass = (env->day == 1) ? 200 : 300; // Day 1: 200 cars, Day 2+: 300 cars + if (env->carsToPass == maxCarsToPass) { + // Do nothing; log the event + env->log.passed_by_enemy += 1.0f; + } else { + env->carsToPass += 1; + env->log.passed_by_enemy += 1.0f; + env->rewards[0] -= 0.1f; + } + } // Preserve last x and y for passing, obs car->last_y = car->y; car->last_x = car->x; // Check for and handle collisions between player and enemy cars - if (env->collision_cooldown_car_vs_car <= 0) { - if (check_collision(env, car)) { - env->log.collisions_player_vs_car++; - env->rewards[0] -= 0.5f; - env->speed = 1 + MIN_SPEED; - env->collision_cooldown_car_vs_car = CRASH_NOOP_DURATION_CAR_VS_CAR; - env->drift_direction = 0; // Reset drift direction - env->car_passed_no_crash_active = 0; // Stepwise rewards deactivated until next car passed - env->step_rew_car_passed_no_crash = 0.0f; // Reset stepwise reward - } + if (env->collision_cooldown_car_vs_car <= 0 && check_collision(env, car)) { + env->log.collisions_player_vs_car++; + env->rewards[0] -= 0.5f; + env->speed = 1 + MIN_SPEED; + env->collision_cooldown_car_vs_car = CRASH_NOOP_DURATION_CAR_VS_CAR; + env->drift_direction = 0; // Reset drift direction + env->car_passed_no_crash_active = 0; // Stepwise rewards deactivated until next car passed + env->step_rew_car_passed_no_crash = 0.0f; // Reset stepwise reward } } @@ -1432,14 +1465,13 @@ void c_step(Enduro* env) { float min_spawn_interval = 0.5f; // 0.8777f; // Minimum spawn interval float max_spawn_interval; int dayIndex = env->day - 1; - float maxSpawnIntervals[] = {0.5f, 0.25f, 0.4f}; // {0.6667f, 0.3614f, 0.5405f}; - int numMaxSpawnIntervals = sizeof(maxSpawnIntervals) / sizeof(maxSpawnIntervals[0]); + int numMaxSpawnIntervals = sizeof(MAX_SPAWN_INTERVALS) / sizeof(MAX_SPAWN_INTERVALS[0]); if (dayIndex < numMaxSpawnIntervals) { - max_spawn_interval = maxSpawnIntervals[dayIndex]; + max_spawn_interval = MAX_SPAWN_INTERVALS[dayIndex]; } else { // For days beyond first, decrease max_spawn_interval further - max_spawn_interval = maxSpawnIntervals[numMaxSpawnIntervals - 1] - (dayIndex - numMaxSpawnIntervals + 1) * 0.1f; + max_spawn_interval = MAX_SPAWN_INTERVALS[numMaxSpawnIntervals - 1] - (dayIndex - numMaxSpawnIntervals + 1) * 0.1f; if (max_spawn_interval < 0.1f) { max_spawn_interval = 0.1f; } @@ -1914,11 +1946,10 @@ void loadTextures(GameState* gameState) { // Initialize time-of-day variables gameState->elapsedTime = 0.0f; gameState->currentBackgroundIndex = 0; - gameState->backgroundIndex = 0; gameState->previousBackgroundIndex = 0; // Load background and mountain textures for different times of day per original env - gameState->spritesheet = LoadTexture("resources/puffer_enduro/enduro_spritesheet.png"); + gameState->spritesheet = LoadTexture("resources/enduro/enduro_spritesheet.png"); // Initialize background and mountain indices for (int i = 0; i < 16; ++i) { @@ -1960,7 +1991,6 @@ void loadTextures(GameState* gameState) { void cleanup(GameState* gameState) { UnloadRenderTexture(gameState->renderTarget); UnloadTexture(gameState->spritesheet); - CloseWindow(); } void updateCarAnimation(GameState* gameState) { @@ -2153,17 +2183,18 @@ void renderScoreboard(GameState* gameState) { // Triggers the day completed 'victory' display // Solely for flapping flag visual effect void updateVictoryEffects(GameState* gameState) { - if (gameState->victoryAchieved) { - gameState->flagTimer++; - // Modulo triggers flag direction change - // Flag renders in that direction until next change - if (gameState->flagTimer % 50 == 0) { - gameState->showLeftFlag = !gameState->showLeftFlag; - } - gameState->victoryDisplayTimer++; - if (gameState->victoryDisplayTimer >= 10) { - gameState->victoryDisplayTimer = 0; - } + if (!gameState->victoryAchieved) { + return; + } + gameState->flagTimer++; + // Modulo triggers flag direction change + // Flag renders in that direction until next change + if (gameState->flagTimer % 50 == 0) { + gameState->showLeftFlag = !gameState->showLeftFlag; + } + gameState->victoryDisplayTimer++; + if (gameState->victoryDisplayTimer >= 10) { + gameState->victoryDisplayTimer = 0; } } @@ -2303,9 +2334,8 @@ void c_render(Client* client, Enduro* env) { } // Determine the car scale based on distance - float car_scale = get_car_scale(car, car->y, CONTINUOUS_SCALE); - - // Select the correct texture + float car_scale = get_car_scale(car->y); + // Select the correct texture based on the car's color and current tread int carAssetIndex; if (isNightStage) { carAssetIndex = (bgIndex == 13) ? gameState->enemyCarNightFogTailLightsIndex : gameState->enemyCarNightTailLightsIndex; diff --git a/pufferlib/ocean/enduro/enduro.py b/pufferlib/ocean/enduro/enduro.py index 8382b57a..b2cbb307 100644 --- a/pufferlib/ocean/enduro/enduro.py +++ b/pufferlib/ocean/enduro/enduro.py @@ -4,7 +4,6 @@ import gymnasium import pufferlib from pufferlib.ocean.enduro.cy_enduro import CyEnduro -import torch class Enduro(pufferlib.PufferEnv): def __init__(self, num_envs=1, frame_skip=1, render_mode='human', @@ -89,6 +88,7 @@ def close(self): self.c_envs.close() def validate_probabilities(prob_tensor): + import torch if torch.isnan(prob_tensor).any() or torch.isinf(prob_tensor).any() or (prob_tensor < 0).any(): raise ValueError("Invalid probability values detected") return prob_tensor diff --git a/pufferlib/ocean/environment.py b/pufferlib/ocean/environment.py index 95a3b544..91ca68b1 100644 --- a/pufferlib/ocean/environment.py +++ b/pufferlib/ocean/environment.py @@ -3,15 +3,19 @@ from .snake.snake import Snake from .squared.squared import Squared +from .squared.pysquared import PySquared from .pong.pong import Pong from .breakout.breakout import Breakout from .enduro.enduro import Enduro from .connect4.connect4 import Connect4 from .tripletriad.tripletriad import TripleTriad +from .tactical.tactical import Tactical from .moba.moba import Moba +from .nmmo3.nmmo3 import NMMO3 from .go.go import Go from .rware.rware import Rware #from .rocket_lander import rocket_lander +from .trash_pickup.trash_pickup import TrashPickupEnv def make_foraging(width=1080, height=720, num_agents=4096, horizon=512, discretize=True, food_reward=0.1, render_mode='rgb_array'): @@ -19,9 +23,7 @@ def make_foraging(width=1080, height=720, num_agents=4096, horizon=512, init_fn = grid.init_foraging reward_fn = grid.reward_foraging return grid.PufferGrid(width, height, num_agents, - horizon, discretize=discretize, food_reward=food_reward, - init_fn=init_fn, reward_fn=reward_fn, - render_mode=render_mode) + horizon, discretize=discretize, food_reward=food_reward, init_fn=init_fn, reward_fn=reward_fn, render_mode=render_mode) def make_predator_prey(width=1080, height=720, num_agents=4096, horizon=512, discretize=True, food_reward=0.1, render_mode='rgb_array'): @@ -58,80 +60,84 @@ def make_puffergrid(render_mode='rgb_array', vision_range=3): from .grid import grid return grid.PufferGrid(render_mode, vision_range) -def make_continuous(discretize=False, **kwargs): +def make_continuous(discretize=False, buf=None, **kwargs): from . import sanity env = sanity.Continuous(discretize=discretize) if not discretize: env = pufferlib.postprocess.ClipAction(env) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) -def make_squared(distance_to_target=3, num_targets=1, **kwargs): +def make_squared(distance_to_target=3, num_targets=1, buf=None, **kwargs): from . import sanity env = sanity.Squared(distance_to_target=distance_to_target, num_targets=num_targets, **kwargs) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env, **kwargs) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf, **kwargs) -def make_bandit(num_actions=10, reward_scale=1, reward_noise=1): +def make_bandit(num_actions=10, reward_scale=1, reward_noise=1, buf=None): from . import sanity env = sanity.Bandit(num_actions=num_actions, reward_scale=reward_scale, reward_noise=reward_noise) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) -def make_memory(mem_length=2, mem_delay=2, **kwargs): +def make_memory(mem_length=2, mem_delay=2, buf=None, **kwargs): from . import sanity env = sanity.Memory(mem_length=mem_length, mem_delay=mem_delay) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) -def make_password(password_length=5, **kwargs): +def make_password(password_length=5, buf=None, **kwargs): from . import sanity env = sanity.Password(password_length=password_length) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) -def make_performance(delay_mean=0, delay_std=0, bandwidth=1, **kwargs): +def make_performance(delay_mean=0, delay_std=0, bandwidth=1, buf=None, **kwargs): from . import sanity env = sanity.Performance(delay_mean=delay_mean, delay_std=delay_std, bandwidth=bandwidth) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) -def make_performance_empiric(count_n=0, count_std=0, bandwidth=1, **kwargs): +def make_performance_empiric(count_n=0, count_std=0, bandwidth=1, buf=None, **kwargs): from . import sanity env = sanity.PerformanceEmpiric(count_n=count_n, count_std=count_std, bandwidth=bandwidth) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) -def make_stochastic(p=0.7, horizon=100, **kwargs): +def make_stochastic(p=0.7, horizon=100, buf=None, **kwargs): from . import sanity env = sanity.Stochastic(p=p, horizon=100) env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf) -def make_spaces(**kwargs): +def make_spaces(buf=None, **kwargs): from . import sanity env = sanity.Spaces() env = pufferlib.postprocess.EpisodeStats(env) - return pufferlib.emulation.GymnasiumPufferEnv(env=env, **kwargs) + return pufferlib.emulation.GymnasiumPufferEnv(env=env, buf=buf, **kwargs) -def make_multiagent(**kwargs): +def make_multiagent(buf=None, **kwargs): from . import sanity env = sanity.Multiagent() env = pufferlib.postprocess.MultiagentEpisodeStats(env) - return pufferlib.emulation.PettingZooPufferEnv(env=env) + return pufferlib.emulation.PettingZooPufferEnv(env=env, buf=buf) MAKE_FNS = { 'breakout': Breakout, 'pong': Pong, 'enduro': Enduro, 'moba': Moba, + 'nmmo3': NMMO3, 'snake': Snake, 'squared': Squared, + 'pysquared': PySquared, 'connect4': Connect4, 'tripletriad': TripleTriad, + 'tactical': Tactical, 'go': Go, 'rware': Rware, + 'trash_pickup': TrashPickupEnv, #'rocket_lander': rocket_lander.RocketLander, 'foraging': make_foraging, @@ -139,7 +145,6 @@ def make_multiagent(**kwargs): 'group': make_group, 'puffer': make_puffer, 'puffer_grid': make_puffergrid, - # 'tactical': make_tactical, 'continuous': make_continuous, 'bandit': make_bandit, 'memory': make_memory, diff --git a/pufferlib/ocean/go/cy_go.pyx b/pufferlib/ocean/go/cy_go.pyx index 51ba0e41..5b39e303 100644 --- a/pufferlib/ocean/go/cy_go.pyx +++ b/pufferlib/ocean/go/cy_go.pyx @@ -11,6 +11,7 @@ cdef extern from "go.h": float episode_length int games_played float score + float winrate ctypedef struct LogBuffer: Log* logs @@ -57,6 +58,11 @@ cdef extern from "go.h": int* visited Group* groups Group* temp_groups + float reward_move_pass + float reward_move_invalid + float reward_move_valid + float reward_player_capture + float reward_opponent_capture ctypedef struct Client @@ -80,7 +86,8 @@ cdef class CyGo: float[:] rewards, unsigned char[:] terminals, int num_envs, int width, int height, int grid_size, int board_width, int board_height, int grid_square_size, int moves_made, float komi, - float score, int last_capture_position): + float score, int last_capture_position, float reward_move_pass, + float reward_move_invalid, float reward_move_valid, float reward_player_capture, float reward_opponent_capture ): self.num_envs = num_envs self.client = NULL @@ -104,7 +111,10 @@ cdef class CyGo: moves_made=moves_made, komi=komi, score=score, - last_capture_position=last_capture_position + last_capture_position=last_capture_position, + reward_move_pass=reward_move_pass, + reward_move_invalid=reward_move_invalid, + reward_move_valid=reward_move_valid ) init(&self.envs[i]) self.client = NULL @@ -134,4 +144,4 @@ cdef class CyGo: def log(self): cdef Log log = aggregate_and_clear(self.logs) - return log + return log \ No newline at end of file diff --git a/pufferlib/ocean/go/go.c b/pufferlib/ocean/go/go.c index 03e05de6..22e9bed3 100644 --- a/pufferlib/ocean/go/go.c +++ b/pufferlib/ocean/go/go.c @@ -1,7 +1,121 @@ #include #include "go.h" +#include "puffernet.h" + +typedef struct GoNet GoNet; +struct GoNet { + int num_agents; + float* obs_2d; + float* obs_1d; + Conv2D* conv1; + ReLU* relu1; + Conv2D* conv2; + Linear* flat; + CatDim1* cat; + Linear* proj; + ReLU* relu3; + LSTM* lstm; + Linear* actor; + Linear* value_fn; + Multidiscrete* multidiscrete; +}; +GoNet* init_gonet(Weights* weights, int num_agents, int grid_size) { + GoNet* net = calloc(1, sizeof(GoNet)); + int hidden_size = 128; + int cnn_channels = 64; + int conv1_output_size = grid_size - 2; + int output_size = grid_size - 4; + int cnn_flat_size = cnn_channels * output_size * output_size; + + net->num_agents = num_agents; + net->obs_2d = calloc(num_agents * grid_size * grid_size * 2, sizeof(float)); // 2 channels for player and opponent + net->obs_1d = calloc(num_agents * 2, sizeof(float)); // 2 additional features + + net->conv1 = make_conv2d(weights, num_agents, grid_size, grid_size, 2, cnn_channels, 3, 1); + net->relu1 = make_relu(num_agents, cnn_channels * conv1_output_size * conv1_output_size); + net->conv2 = make_conv2d(weights, num_agents, conv1_output_size, conv1_output_size, cnn_channels, cnn_channels, 3, 1); + net->flat = make_linear(weights, num_agents, 2, 32); + net->cat = make_cat_dim1(num_agents, cnn_flat_size, 32); + net->proj = make_linear(weights, num_agents, cnn_flat_size + 32, hidden_size); + net->relu3 = make_relu(num_agents, hidden_size); + net->actor = make_linear(weights, num_agents, hidden_size, grid_size*grid_size + 1); // +1 for pass move + net->value_fn = make_linear(weights, num_agents, hidden_size, 1); + net->lstm = make_lstm(weights, num_agents, hidden_size, 128); + int logit_sizes[6] = {grid_size*grid_size+1}; + net->multidiscrete = make_multidiscrete(num_agents, logit_sizes, 1); + return net; +} + +void free_gonet(GoNet* net) { + free(net->obs_2d); + free(net->obs_1d); + free(net->conv1); + free(net->relu1); + free(net->conv2); + free(net->flat); + free(net->cat); + free(net->relu3); + free(net->proj); + free(net->lstm); + free(net->actor); + free(net->value_fn); + free(net); +} + +void forward(GoNet* net, float* observations, int* actions, int grid_size) { + int full_board = grid_size * grid_size; + // Clear previous observations + memset(net->obs_2d, 0, net->num_agents * grid_size * grid_size * 2 * sizeof(float)); + memset(net->obs_1d, 0, net->num_agents * 2 * sizeof(float)); + + // Reshape observations into 2D boards and additional features + float (*obs_2d)[2][grid_size][grid_size] = (float (*)[2][grid_size][grid_size])net->obs_2d; + float (*obs_1d)[2] = (float (*)[2])net->obs_1d; + + for (int b = 0; b < net->num_agents; b++) { + int b_offset = b * (full_board * 2 + 2); // offset for each batch + + // Process black stones board + for (int i = 0; i < grid_size; i++) { + for (int j = 0; j < grid_size; j++) { + obs_2d[b][0][i][j] = observations[b_offset + i*grid_size + j]; + } + } + + // Process white stones board + for (int i = 0; i < grid_size; i++) { + for (int j = 0; j < grid_size; j++) { + obs_2d[b][1][i][j] = observations[b_offset + full_board + i*grid_size + j]; + } + } + + // Process additional features + obs_1d[b][0] = observations[b_offset + full_board * 2]; + obs_1d[b][1] = observations[b_offset + full_board * 2 + 1]; + } + + // Forward pass through the network + conv2d(net->conv1, net->obs_2d); + relu(net->relu1, net->conv1->output); + conv2d(net->conv2, net->relu1->output); + + linear(net->flat, net->obs_1d); + + cat_dim1(net->cat, net->conv2->output, net->flat->output); + linear(net->proj, net->cat->output); + relu(net->relu3, net->proj->output); + + lstm(net->lstm, net->relu3->output); + linear(net->actor, net->lstm->state_h); + linear(net->value_fn, net->lstm->state_h); + + // Get action by taking argmax of actor output + softmax_multidiscrete(net->multidiscrete, net->actor->output, actions); + +} void demo(int grid_size) { + CGo env = { .width = 950, .height = 64*(grid_size+1), @@ -10,51 +124,70 @@ void demo(int grid_size) { .board_height = 64*(grid_size+1), .grid_square_size = 64, .moves_made = 0, - .komi = 7.5 + .komi = 7.5, + .reward_move_pass = -0.25, + .reward_move_invalid = -0.1, + .reward_move_valid = 0.1 }; + + Weights* weights = load_weights("resources/go_weights.bin", 254867); + GoNet* net = init_gonet(weights, 1, grid_size); allocate(&env); reset(&env); Client* client = make_client(env.width, env.height); + int tick = 0; while (!WindowShouldClose()) { // User can take control of the paddle - env.actions[0] = -1; - - if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { - Vector2 mousePos = GetMousePosition(); - - // Calculate the offset for the board - int boardOffsetX = env.grid_square_size; - int boardOffsetY = env.grid_square_size; - - // Adjust mouse position relative to the board - int relativeX = mousePos.x - boardOffsetX; - int relativeY = mousePos.y - boardOffsetY; - - // Calculate cell indices for the corners - int cellX = (relativeX + env.grid_square_size / 2) / env.grid_square_size; - int cellY = (relativeY + env.grid_square_size / 2) / env.grid_square_size; - - // Ensure the click is within the game board - if (cellX >= 0 && cellX <= env.grid_size && cellY >= 0 && cellY <= env.grid_size) { - // Calculate the point index (1-19) based on the click position - int pointIndex = cellY * (env.grid_size) + cellX + 1; - env.actions[0] = (unsigned short)pointIndex; + if(tick % 12 == 0) { + tick = 0; + int human_action = env.actions[0]; + forward(net, env.observations, env.actions, grid_size); + if (IsKeyDown(KEY_LEFT_SHIFT)) { + env.actions[0] = human_action; } - // Check if pass button is clicked - int passButtonX = env.width - 300; - int passButtonY = 200; - int passButtonWidth = 100; - int passButtonHeight = 50; - - if (mousePos.x >= passButtonX && mousePos.x <= passButtonX + passButtonWidth && - mousePos.y >= passButtonY && mousePos.y <= passButtonY + passButtonHeight) { - env.actions[0] = 0; // Send action 0 for pass + step(&env); + if (IsKeyDown(KEY_LEFT_SHIFT)) { + env.actions[0] = -1; } } - step(&env); - render(client,&env); + tick++; + if (IsKeyDown(KEY_LEFT_SHIFT)) { + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + Vector2 mousePos = GetMousePosition(); + + // Calculate the offset for the board + int boardOffsetX = env.grid_square_size; + int boardOffsetY = env.grid_square_size; + + // Adjust mouse position relative to the board + int relativeX = mousePos.x - boardOffsetX; + int relativeY = mousePos.y - boardOffsetY; + + // Calculate cell indices for the corners + int cellX = (relativeX + env.grid_square_size / 2) / env.grid_square_size; + int cellY = (relativeY + env.grid_square_size / 2) / env.grid_square_size; + + // Ensure the click is within the game board + if (cellX >= 0 && cellX <= env.grid_size && cellY >= 0 && cellY <= env.grid_size) { + // Calculate the point index (1-19) based on the click position + int pointIndex = cellY * (env.grid_size) + cellX + 1; + env.actions[0] = (unsigned short)pointIndex; + } + // Check if pass button is clicked + int passButtonX = env.width - 300; + int passButtonY = 200; + int passButtonWidth = 100; + int passButtonHeight = 50; + + if (mousePos.x >= passButtonX && mousePos.x <= passButtonX + passButtonWidth && + mousePos.y >= passButtonY && mousePos.y <= passButtonY + passButtonHeight) { + env.actions[0] = 0; // Send action 0 for pass + } + } + } + render(client, &env); } close_client(client); free_allocated(&env); @@ -70,7 +203,10 @@ void performance_test() { .board_height = 600, .grid_square_size = 600/9, .moves_made = 0, - .komi = 7.5 + .komi = 7.5, + .reward_move_pass = -0.25, + .reward_move_invalid = -0.1, + .reward_move_valid = 0.1 }; allocate(&env); reset(&env); @@ -88,7 +224,7 @@ void performance_test() { } int main() { - demo(9); + demo(7); // performance_test(); return 0; } diff --git a/pufferlib/ocean/go/go.h b/pufferlib/ocean/go/go.h index 86b86bec..ca58ad53 100644 --- a/pufferlib/ocean/go/go.h +++ b/pufferlib/ocean/go/go.h @@ -9,6 +9,8 @@ #define MOVE_MIN 1 #define TICK_RATE 1.0f/60.0f #define NUM_DIRECTIONS 4 +#define ENV_WIN -1 +#define PLAYER_WIN 1 static const int DIRECTIONS[NUM_DIRECTIONS][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; // LD_LIBRARY_PATH=raylib/lib ./go #define LOG_BUFFER_SIZE 1024 @@ -19,6 +21,7 @@ struct Log { float episode_length; int games_played; float score; + float winrate; }; typedef struct LogBuffer LogBuffer; @@ -60,10 +63,12 @@ Log aggregate_and_clear(LogBuffer* logs) { log.episode_length += logs->logs[i].episode_length; log.games_played += logs->logs[i].games_played; log.score += logs->logs[i].score; + log.winrate += logs->logs[i].winrate; } log.episode_return /= logs->idx; log.episode_length /= logs->idx; log.score /= logs->idx; + log.winrate /= logs->idx; logs->idx = 0; return log; } @@ -131,6 +136,11 @@ struct CGo { int* visited; Group* groups; Group* temp_groups; + float reward_move_pass; + float reward_move_invalid; + float reward_move_valid; + float reward_player_capture; + float reward_opponent_capture; }; void generate_board_positions(CGo* env) { @@ -169,7 +179,7 @@ void init(CGo* env) { void allocate(CGo* env) { init(env); - env->observations = (float*)calloc((env->grid_size)*(env->grid_size)*2 + 3, sizeof(float)); + env->observations = (float*)calloc((env->grid_size)*(env->grid_size)*2 + 2, sizeof(float)); env->actions = (int*)calloc(1, sizeof(int)); env->rewards = (float*)calloc(1, sizeof(float)); env->dones = (unsigned char*)calloc(1, sizeof(unsigned char)); @@ -200,14 +210,25 @@ void free_allocated(CGo* env) { void compute_observations(CGo* env) { int observation_indx=0; for (int i = 0; i < (env->grid_size)*(env->grid_size); i++) { - env->observations[observation_indx] = (float)env->board_states[i]; + if(env->board_states[i] ==1 ){ + env->observations[observation_indx] = 1.0; + } + else { + env->observations[observation_indx] = 0.0; + } observation_indx++; } for (int i = 0; i < (env->grid_size)*(env->grid_size); i++) { - env->observations[observation_indx] = (float)env->previous_board_state[i]; + if(env->board_states[i] ==2 ){ + env->observations[observation_indx] = 1.0; + } + else { + env->observations[observation_indx] = 0.0; + } observation_indx++; } - env->observations[observation_indx] = env->score; + env->observations[observation_indx] = env->capture_count[0]; + env->observations[observation_indx+1] = env->capture_count[1]; } @@ -239,49 +260,75 @@ void flood_fill(CGo* env, int x, int y, int* territory, int player) { void compute_score_tromp_taylor(CGo* env) { int player_score = 0; int opponent_score = 0; - int territory[3] = {0, 0, 0}; // [neutral, player, opponent] reset_visited(env); - // Count stones and mark them as visited - for (int i = 0; i < (env->grid_size) * (env->grid_size); i++) { - env->visited[i] = 0; + + // Queue for BFS + int queue_size = (env->grid_size) * (env->grid_size); + int queue[queue_size]; + + // First count stones + for (int i = 0; i < queue_size; i++) { if (env->board_states[i] == 1) { player_score++; - env->visited[i] = 1; } else if (env->board_states[i] == 2) { opponent_score++; - env->visited[i] = 1; } } - for (int pos = 0; pos < (env->grid_size) * (env->grid_size); pos++) { - int x = pos % (env->grid_size); - int y = pos / (env->grid_size); - if (env->visited[pos]) { + + // Then process empty territories + for (int start_pos = 0; start_pos < queue_size; start_pos++) { + // Skip if not empty or already visited + if (env->board_states[start_pos] != 0 || env->visited[start_pos]) { continue; } - int player = 0; // Start as neutral - // Check adjacent positions to determine territory owner - for (int i = 0; i < 4; i++) { - int nx = x + DIRECTIONS[i][0]; - int ny = y + DIRECTIONS[i][1]; - if (!is_valid_position(env, nx, ny)) { - continue; - } - int npos = ny * (env->grid_size) + nx; - if (env->board_states[npos] == 0) { - continue; - } - if (player == 0) { - player = env->board_states[npos]; - } else if (player != env->board_states[npos]) { - player = 0; // Neutral if bordered by both players - break; + + // Initialize BFS + int front = 0, rear = 0; + int territory_size = 0; + int bordering_player = 0; // 0=neutral, 1=player1, 2=player2, 3=mixed + + queue[rear++] = start_pos; + env->visited[start_pos] = 1; + + // Process connected empty points + while (front < rear) { + int pos = queue[front++]; + territory_size++; + int x = pos % env->grid_size; + int y = pos / env->grid_size; + + // Check all adjacent positions + for (int i = 0; i < 4; i++) { + int nx = x + DIRECTIONS[i][0]; + int ny = y + DIRECTIONS[i][1]; + + if (!is_valid_position(env, nx, ny)) { + continue; + } + + int npos = ny * env->grid_size + nx; + + if (env->board_states[npos] == 0 && !env->visited[npos]) { + // Add unvisited empty points to queue + queue[rear++] = npos; + env->visited[npos] = 1; + } else if (bordering_player == 0) { + bordering_player = env->board_states[npos]; + } else if (bordering_player != env->board_states[npos]) { + bordering_player = 3; // Mixed territory + } } } - flood_fill(env, x, y, territory, player); + + // Assign territory points + if (bordering_player == 1) { + player_score += territory_size; + } else if (bordering_player == 2) { + opponent_score += territory_size; + } + // Mixed territories (bordering_player == 3) are neutral and not counted } - // Calculate final scores - player_score += territory[1]; - opponent_score += territory[2]; + env->score = (float)player_score - (float)opponent_score - env->komi; } @@ -314,7 +361,13 @@ void capture_group(CGo* env, int* board, int root, int* affected_groups, int* af int pos = queue[front++]; board[pos] = 0; // Remove stone env->capture_count[capturing_player - 1]++; // Update capturing player's count - + if(capturing_player-1 == 0){ + env->rewards[0] += env->reward_player_capture; + env->log.episode_return += env->reward_player_capture; + } else{ + env->rewards[0] += env->reward_opponent_capture; + env->log.episode_return += env->reward_opponent_capture; + } int x = pos % (env->grid_size); int y = pos / (env->grid_size); @@ -620,12 +673,15 @@ void end_game(CGo* env){ compute_score_tromp_taylor(env); if (env->score > 0) { env->rewards[0] = 1.0 ; + env->log.winrate = 1.0; } else if (env->score < 0) { - env->rewards[0] = -1.0 ; + env->rewards[0] = -1.0; + env->log.winrate = -1.0; } else { env->rewards[0] = 0.0; + env->log.winrate = 0.0; } env->log.score = env->score; env->log.games_played++; @@ -639,15 +695,16 @@ void step(CGo* env) { env->rewards[0] = 0.0; int action = (int)env->actions[0]; // useful for training , can prob be a hyper param. Recommend to increase with larger board size - if (env->log.episode_length >150) { + float max_moves = 3 * env->grid_size * env->grid_size; + if (env->log.episode_length > max_moves) { env->dones[0] = 1; end_game(env); compute_observations(env); return; } if(action == NOOP){ - env->rewards[0] -= 0.25;; - env->log.episode_return -= 0.25; + env->rewards[0] = env->reward_move_pass; + env->log.episode_return += env->reward_move_pass; enemy_greedy_hard(env); if (env->dones[0] == 1) { end_game(env); @@ -660,25 +717,29 @@ void step(CGo* env) { memcpy(env->previous_board_state, env->board_states, sizeof(int) * (env->grid_size) * (env->grid_size)); if(make_move(env, action-1, 1)) { env->moves_made++; - env->rewards[0] += 0.1; - env->log.episode_return += 0.1; + env->rewards[0] = env->reward_move_valid; + env->log.episode_return += env->reward_move_valid; enemy_greedy_hard(env); } else { - env->rewards[0] -= 0.1; - env->log.episode_return -= 0.1; + env->rewards[0] = env->reward_move_invalid; + env->log.episode_return += env->reward_move_invalid; } compute_observations(env); } - if (env->moves_made >= (env->grid_size)*(env->grid_size)*2) { - env->dones[0] = 1; + if(env->rewards[0] > 1){ + env->rewards[0] = 1; + } + if(env->rewards[0] < -1){ + env->rewards[0] = -1; } if (env->dones[0] == 1) { end_game(env); return; } + compute_observations(env); } @@ -701,7 +762,7 @@ Client* make_client(int width, int height) { client->width = width; client->height = height; InitWindow(width, height, "PufferLib Ray Go"); - SetTargetFPS(15); + SetTargetFPS(60); client->puffers = LoadTexture("resources/puffers_128.png"); return client; } @@ -778,4 +839,4 @@ void render(Client* client, CGo* env) { void close_client(Client* client) { CloseWindow(); free(client); -} +} \ No newline at end of file diff --git a/pufferlib/ocean/go/go.py b/pufferlib/ocean/go/go.py index 7e5853d7..69d54560 100644 --- a/pufferlib/ocean/go/go.py +++ b/pufferlib/ocean/go/go.py @@ -13,14 +13,19 @@ class Go(pufferlib.PufferEnv): def __init__(self, num_envs=1, render_mode=None, report_interval=1, - width=1200, height=800, - grid_size=6, + width=950, height=800, + grid_size=7, board_width=600, board_height=600, - grid_square_size=600/6, + grid_square_size=600/9, moves_made=0, komi=7.5, score = 0.0, last_capture_position=-1, + reward_move_pass = -0.25, + reward_move_invalid = -0.1, + reward_move_valid = 0.1, + reward_player_capture = 0.25, + reward_opponent_capture = -0.25, buf = None): # env @@ -28,17 +33,19 @@ def __init__(self, num_envs=1, render_mode=None, report_interval=1, self.render_mode = render_mode self.report_interval = report_interval - self.num_obs = (grid_size) * (grid_size)*2 + 3 + self.num_obs = (grid_size) * (grid_size)*2 + 2 self.num_act = (grid_size) * (grid_size) + 1 self.single_observation_space = gymnasium.spaces.Box(low=0, high=1, shape=(self.num_obs,), dtype=np.float32) self.single_action_space = gymnasium.spaces.Discrete(self.num_act) super().__init__(buf=buf) + height = 64*(grid_size+1) self.c_envs = CyGo(self.observations, self.actions, self.rewards, self.terminals, num_envs, width, height, grid_size, board_width, - board_height, grid_square_size, moves_made, komi, score,last_capture_position) - + board_height, grid_square_size, moves_made, komi, score, + last_capture_position, reward_move_pass, reward_move_invalid, + reward_move_valid, reward_player_capture, reward_opponent_capture) def reset(self, seed=None): self.c_envs.reset() @@ -49,7 +56,6 @@ def step(self, actions): self.actions[:] = actions self.c_envs.step() self.tick += 1 - info = [] if self.tick % self.report_interval == 0: log = self.c_envs.log() diff --git a/pufferlib/ocean/moba/cy_moba.pyx b/pufferlib/ocean/moba/cy_moba.pyx index 1e0a9402..13267bec 100644 --- a/pufferlib/ocean/moba/cy_moba.pyx +++ b/pufferlib/ocean/moba/cy_moba.pyx @@ -231,7 +231,11 @@ cdef class CyMOBA: self.envs = calloc(num_envs, sizeof(MOBA)) self.logs = allocate_logbuffer(LOG_BUFFER_SIZE) + import os + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) cdef unsigned char* game_map_npy = read_file("resources/moba/game_map.npy"); + os.chdir(cwd) self.ai_path_buffer = calloc(3*8*128*128, sizeof(int)) self.ai_paths = calloc(128*128*128*128, sizeof(unsigned char)) @@ -272,7 +276,11 @@ cdef class CyMOBA: def render(self, int tick): if self.client == NULL: + import os + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) self.client = init_game_renderer(32, 41, 23) + os.chdir(cwd) render_game(self.client, &self.envs[0], tick) diff --git a/pufferlib/ocean/moba/moba.h b/pufferlib/ocean/moba/moba.h index ffa3a1ac..57717f9e 100644 --- a/pufferlib/ocean/moba/moba.h +++ b/pufferlib/ocean/moba/moba.h @@ -4,10 +4,7 @@ #include #include #include -#include - -// xxd -i game_map.npy > game_map.h -#include "game_map.h" +#include // xxd -i game_map.npy > game_map.h #include "game_map.h" #include "raylib.h" @@ -444,6 +441,7 @@ void free_moba(MOBA* env) { free(env->reward_components); free(env->map->grid); free(env->map); + free(env->orig_grid); free(env->rng->rng); free(env->rng); } @@ -2133,8 +2131,8 @@ GameRenderer* init_game_renderer(int cell_size, int width, int height) { renderer->puffer = LoadTexture("resources/moba/moba_assets.png"); renderer->shader_background = GenImageColor(2560, 1440, (Color){0, 0, 0, 255}); renderer->shader_canvas = LoadTextureFromImage(renderer->shader_background); - renderer->shader = LoadShader("", TextFormat("resources/moba/map_shader_%i.fs", GLSL_VERSION)); - renderer->bloom_shader = LoadShader("", TextFormat("resources/moba/bloom_shader_%i.fs", GLSL_VERSION)); + renderer->shader = LoadShader(0, TextFormat("resources/moba/map_shader_%i.fs", GLSL_VERSION)); + renderer->bloom_shader = LoadShader(0, TextFormat("resources/moba/bloom_shader_%i.fs", GLSL_VERSION)); // TODO: These should be int locs? renderer->shader_camera_x = GetShaderLocation(renderer->shader, "camera_x"); @@ -2231,10 +2229,11 @@ int render_game(GameRenderer* renderer, MOBA* env, int frame) { } int human = renderer->human_player; + bool HUMAN_CONTROL = IsKeyDown(KEY_LEFT_SHIFT); int (*actions)[6] = (int(*)[6])env->actions; // Clears so as to not let the nn spam actions - if (frame % 12 == 0) { + if (HUMAN_CONTROL && frame % 12 == 0) { actions[human][0] = 0; actions[human][1] = 0; actions[human][2] = 0; @@ -2256,23 +2255,27 @@ int render_game(GameRenderer* renderer, MOBA* env, int frame) { renderer->last_click_y = -1; } - actions[human][0] = 300*dy; - actions[human][1] = 300*dx; + if (HUMAN_CONTROL) { + actions[human][0] = 300*dy; + actions[human][1] = 300*dx; + } } if (IsKeyDown(KEY_ESCAPE)) { return 1; } - if (IsKeyDown(KEY_Q) || IsKeyPressed(KEY_Q)) { - actions[human][3] = 1; - } - if (IsKeyDown(KEY_W) || IsKeyPressed(KEY_W)) { - actions[human][4] = 1; - } - if (IsKeyDown(KEY_E) || IsKeyPressed(KEY_E)) { - actions[human][5] = 1; - } - if (IsKeyDown(KEY_LEFT_SHIFT)) { - actions[human][2] = 2; // Target heroes + if (HUMAN_CONTROL) { + if (IsKeyDown(KEY_Q) || IsKeyPressed(KEY_Q)) { + actions[human][3] = 1; + } + if (IsKeyDown(KEY_W) || IsKeyPressed(KEY_W)) { + actions[human][4] = 1; + } + if (IsKeyDown(KEY_E) || IsKeyPressed(KEY_E)) { + actions[human][5] = 1; + } + if (IsKeyDown(KEY_LEFT_SHIFT)) { + actions[human][2] = 2; // Target heroes + } } // Num keys toggle selected player int num_pressed = GetKeyPressed(); @@ -2465,10 +2468,10 @@ int render_game(GameRenderer* renderer, MOBA* env, int frame) { } void close_game_renderer(GameRenderer* renderer) { - CloseWindow(); UnloadImage(renderer->shader_background); UnloadShader(renderer->shader); UnloadShader(renderer->bloom_shader); + CloseWindow(); free(renderer); } diff --git a/pufferlib/ocean/nmmo3/cy_nmmo3.pyx b/pufferlib/ocean/nmmo3/cy_nmmo3.pyx new file mode 100644 index 00000000..0c909194 --- /dev/null +++ b/pufferlib/ocean/nmmo3/cy_nmmo3.pyx @@ -0,0 +1,270 @@ +# distutils: define_macros=NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION +# cython: language_level=3 +# cython: boundscheck=True +# cython: initializedcheck=True +# cython: wraparound=True +# cython: cdivision=False +# cython: nonecheck=True +# cython: profile=True + +from libc.stdlib cimport calloc, free +cimport numpy as cnp +import numpy as np + +cdef extern from "nmmo3.h": + int LOG_BUFFER_SIZE + + ctypedef struct Log: + float episode_return; + float episode_length; + float return_comb_lvl; + float return_prof_lvl; + float return_item_atk_lvl; + float return_item_def_lvl; + float return_market_buy; + float return_market_sell; + float return_death; + float min_comb_prof; + float purchases; + float sales; + float equip_attack; + float equip_defense; + float r; + float c; + + ctypedef struct LogBuffer + LogBuffer* allocate_logbuffer(int) + void free_logbuffer(LogBuffer*) + Log aggregate_and_clear(LogBuffer*) + + int ATN_NOOP + + ctypedef struct Entity: + int type + int comb_lvl + int element + int dir + int anim + int hp + int hp_max + int prof_lvl + int ui_mode + int market_tier + int sell_idx + int gold + int in_combat + int equipment[5] + int inventory[12] + int is_equipped[12] + int wander_range + int ranged + int goal + int equipment_attack + int equipment_defense + int r + int c + int spawn_r + int spawn_c + int min_comb_prof[500] + int min_comb_prof_idx + int time_alive; + int purchases; + int sales; + + ctypedef struct Reward: + float total + float death; + float pioneer; + float comb_lvl; + float prof_lvl; + float item_atk_lvl; + float item_def_lvl; + float item_tool_lvl; + float market_buy; + float market_sell; + + ctypedef struct ItemMarket: + int offer_idx + int max_offers + + ctypedef struct RespawnBuffer: + int* buffer + int ticks + int size + + ctypedef struct MMO: + int width + int height + int num_players + int num_enemies + int num_resources + int num_weapons + int num_gems + char* terrain + unsigned char* rendered + Entity* players + Entity* enemies + short* pids + unsigned char* items + Reward* rewards + unsigned char* counts + unsigned char* obs + int* actions + int tick + int tiers + int levels + float teleportitis_prob + int x_window + int y_window + int obs_size + int enemy_respawn_ticks + int item_respawn_ticks + ItemMarket* market + int market_buys + int market_sells + RespawnBuffer* resource_respawn_buffer + RespawnBuffer* enemy_respawn_buffer + Log* logs + LogBuffer* log_buffer + float reward_combat_level + float reward_prof_level + float reward_item_level + float reward_market + float reward_death + + ctypedef struct Client + Client* make_client(MMO* env) + #void close_client(Client* client) + int tick(Client* client, MMO* env, float delta) + + void init_mmo(MMO* env) + void reset(MMO* env, int seed) + void step(MMO* env) + +cpdef entity_dtype(): + '''Make a dummy entity to get the dtype''' + cdef Entity entity + return np.asarray(&entity).dtype + +cpdef reward_dtype(): + '''Make a dummy reward to get the dtype''' + cdef Reward reward + return np.asarray(&reward).dtype + +cdef class Environment: + cdef: + MMO* envs + Client* client + LogBuffer* logs + int num_envs + + def __init__(self, unsigned char[:, :] observations, int[:, :] players, + int[:, :] enemies, float[:, :] rewards, int[:] actions, + list width, list height, int num_envs, list num_players, + list num_enemies, list num_resources, list num_weapons, list num_gems, + list tiers, list levels, list teleportitis_prob, list enemy_respawn_ticks, + list item_respawn_ticks, float reward_combat_level, float reward_prof_level, + float reward_item_level, float reward_market, float reward_death, + int x_window=7, int y_window=5): + + cdef: + int total_players = 0 + int total_enemies = 0 + int n_players = 0 + int n_enemies = 0 + + self.num_envs = num_envs + self.envs = calloc(num_envs, sizeof(MMO)) + self.logs = allocate_logbuffer(LOG_BUFFER_SIZE) + for i in range(num_envs): + obs_i = observations[total_players:total_players+n_players] + rewards_i = rewards[total_players:total_players+n_players] + players_i = players[total_players:total_players+n_players] + enemies_i = enemies[total_enemies:total_enemies+n_enemies] + #counts_i = counts[total_players:total_players+n_players] + #terrain_i = terrain[total_players:total_players+n_players] + #rendered_i = rendered[total_players:total_players+n_players] + actions_i = actions[total_players:total_players+n_players] + + self.envs[i] = MMO( + obs=&observations[total_players, 0], + rewards= &rewards[total_players, 0], + players= &players[total_players, 0], + enemies= &enemies[total_enemies, 0], + actions=&actions[total_players], + width=width[i], + height=height[i], + num_players=num_players[i], + num_enemies=num_enemies[i], + num_resources=num_resources[i], + num_weapons=num_weapons[i], + num_gems=num_gems[i], + tiers=tiers[i], + levels=levels[i], + teleportitis_prob=teleportitis_prob[i], + enemy_respawn_ticks=enemy_respawn_ticks[i], + item_respawn_ticks=item_respawn_ticks[i], + x_window=x_window, + y_window=y_window, + log_buffer=self.logs, + reward_combat_level=reward_combat_level, + reward_prof_level=reward_prof_level, + reward_item_level=reward_item_level, + reward_market=reward_market, + reward_death=reward_death, + ) + n_players = num_players[i] + n_enemies = num_enemies[i] + + init_mmo(&self.envs[i]) + total_players += n_players + total_enemies += n_enemies + + self.client = NULL + + def reset(self): + cdef int i + for i in range(self.num_envs): + # TODO: Seed + reset(&self.envs[i], i+1) + # Do I need to reset terrain here? + + def step(self): + cdef int i + for i in range(self.num_envs): + step(&self.envs[i]) + + def pids(self): + ary = np.zeros((512, 512), dtype=np.intc) + cdef int i, j + for i in range(512): + for j in range(512): + ary[i, j] = self.envs[0].pids[512*i + j] + return ary + + def render(self): + if self.client == NULL: + import os + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) + self.client = make_client(&self.envs[0]) + os.chdir(cwd) + + cdef int i, atn + cdef int action = ATN_NOOP; + for i in range(36): + atn = tick(self.client, &self.envs[0], i/36.0) + if atn != ATN_NOOP: + action = atn + + self.envs[0].actions[0] = action + + # TODO + def close(self): + if self.client != NULL: + #close_game_renderer(self.renderer) + self.client = NULL + + def log(self): + cdef Log log = aggregate_and_clear(self.logs) + return log diff --git a/pufferlib/ocean/nmmo3/make_sprite_sheets.py b/pufferlib/ocean/nmmo3/make_sprite_sheets.py new file mode 100644 index 00000000..be4d52c3 --- /dev/null +++ b/pufferlib/ocean/nmmo3/make_sprite_sheets.py @@ -0,0 +1,498 @@ +''' +This script is used to generate scaled and combined sprite sheets for nmmo3 +You will need the to put the following folders into the same directory. They +can be purchased from ManaSeed on itch.io + +20.04c - Summer Forest 4.2 +20.05c - Spring Forest 4.1 +20.06a - Autumn Forest 4.1 +20.07a - Winter Forest 4.0a +20.01a - Character Base 2.5c +20.01c - Peasant Farmer Pants & Hat 2.1 (comp. v01) +20.01c - Peasant Farmer Pants & Hat 2.2 (optional combat animations) +20.08b - Bow Combat 3.2 +21.07b - Sword & Shield Combat 2.3 +21.10a - Forester Pointed Hat & Tunic 2.1a (comp. v01) +21.10a - Forester Pointed Hat & Tunic 2.2 (optional, combat animations) +''' + +from itertools import product +from PIL import Image +import pyray as ray +import numpy as np +import random +import sys +import os +import cv2 + + +SHEET_SIZE = 2048 +N_GENERATE = 10 + +ELEMENTS = ( + ('neutral', 1, ray.Color(255, 255, 255, 255)), + ('fire', 5, ray.Color(255, 128, 128, 255)), + ('water', 9, ray.Color(128, 128, 255, 255)), + ('earth', 11, ray.Color(128, 255, 128, 255)), + ('air', 3, ray.Color(255, 255, 128, 255)), +) + +BASE = list(range(8)) +HAIR = list(range(14)) +CLOTHES = list(range(1, 6)) +SWORD = list(range(1, 6)) +BOW = list(range(1, 6)) +QUIVER = list(range(1, 9)) + +# Hair colors, indices into files +''' +HAIR = { + ELEM_NEUTRAL: 1, + ELEM_FIRE: 5, + ELEM_WATER: 9, + ELEM_EARTH: 11, + ELEM_AIR: 3 +} +''' + + +# Character base +character = 'char_a_p1_0bas_humn_v{i:02}.png' +demon = 'char_a_p1_0bas_demn_v{i:02}.png' +goblin = 'char_a_p1_0bas_gbln_v{i:02}.png' +hair_dap = 'char_a_p1_4har_dap1_v{i:02}.png' +hair_bob = 'char_a_p1_4har_bob1_v{i:02}.png' + +# Combat animations +sword_character = 'char_a_pONE3_0bas_humn_v{i:02}.png' +sword_weapon = 'char_a_pONE3_6tla_sw01_v{i:02}.png' +sword_hair_bob = 'char_a_pONE3_4har_bob1_v{i:02}.png' +sword_hair_dap = 'char_a_pONE3_4har_dap1_v{i:02}.png' +bow_character = 'char_a_pBOW3_0bas_humn_v{i:02}.png' +bow_hair_dap = 'char_a_pBOW3_4har_dap1_v{i:02}.png' +bow_hair_bob = 'char_a_pBOW3_4har_bob1_v{i:02}.png' +bow_weapon = 'char_a_pBOW3_6tla_bo01_v{i:02}.png' +bow_quiver = 'char_a_pBOW3_7tlb_qv01_v{i:02}.png' +arrow = 'aro_comn_v{i:02}.png' + +# Peasant character alternative +peasant_clothes = 'char_a_p1_1out_pfpn_v{i:02}.png' +sword_peasant_clothes = 'char_a_pONE3_1out_pfpn_v{i:02}.png' +bow_peasant_clothes = 'char_a_pBOW3_1out_pfpn_v{i:02}.png' + +# Forester character alternative +forester_hat = 'char_a_p1_5hat_pnty_v{i:02}.png' +forester_clothes = 'char_a_p1_1out_fstr_v{i:02}.png' +sword_forester_hat = 'char_a_pONE3_5hat_pnty_v{i:02}.png' +sword_forester_clothes = 'char_a_pONE3_1out_fstr_v{i:02}.png' +bow_forester_hat = 'char_a_pBOW3_5hat_pnty_v{i:02}.png' +bow_forester_clothes = 'char_a_pBOW3_1out_fstr_v{i:02}.png' + +sword_mask = np.array(( + (1, 1, 1, 1, 1, 1, 1, 1), + (1, 1, 1, 1, 1, 1, 1, 1), + (1, 0, 1, 1, 1, 1, 1, 1), + (1, 0, 1, 1, 1, 1, 1, 1), + (0, 0, 1, 1, 1, 1, 1, 1), + (1, 1, 1, 1, 1, 1, 1, 1), + (0, 0, 1, 1, 0, 0, 0, 0), + (0, 0, 1, 1, 0, 0, 0, 0), +)) + +bow_mask = np.array(( + (0, 0, 0, 0, 0, 0, 0, 0), + (1, 1, 1, 1, 1, 1, 1, 1), + (1, 0, 0, 0, 0, 0, 0, 0), + (1, 0, 0, 0, 0, 0, 0, 0), + (0, 0, 0, 0, 0, 0, 0, 0), + (1, 1, 1, 1, 1, 1, 1, 1), + (1, 0, 0, 0, 0, 0, 0, 0), + (1, 0, 0, 0, 0, 0, 0, 0), +)) + +quiver_mask = np.array(( + (1, 1, 1, 1, 1, 1, 1, 1), + (0, 0, 0, 0, 0, 0, 0, 0), + (1, 1, 1, 1, 1, 1, 1, 1), + (1, 1, 1, 1, 1, 1, 1, 1), + (1, 1, 1, 1, 1, 1, 1, 1), + (0, 0, 0, 0, 0, 0, 0, 0), + (1, 1, 1, 1, 1, 1, 1, 1), + (1, 1, 1, 1, 1, 1, 1, 1), +)) + +def draw_tex(path, f_name, i, x, y, tint=None): + if tint is None: + tint = ray.WHITE + + path = os.path.join(path, f_name).format(i=i) + texture = ray.load_texture(path) + source_rect = ray.Rectangle(0, 0, texture.width, -texture.height) + dest_rect = ray.Rectangle(x, y, texture.width, texture.height) + ray.draw_texture_pro(texture, source_rect, dest_rect, (0, 0), 0, tint) + +def draw_masked_tex(path, f_name, i, x, y, mask, tint=None): + if tint is None: + tint = ray.WHITE + + path = os.path.join(path, f_name).format(i=i) + texture = ray.load_texture(path) + Y, X = mask.shape + for r, row in enumerate(mask): + for c, m in enumerate(row): + if m == 0: + continue + + src_x = c * 128 + src_y = r * 128 + source_rect = ray.Rectangle(src_x, src_y, 128, -128) + + dst_x = x + src_x + dst_y = y + (Y-r-1)*128 + dest_rect = ray.Rectangle(dst_x, dst_y, 128, 128) + + ray.draw_texture_pro(texture, source_rect, dest_rect, (0, 0), 0, tint) + +def draw_arrow(tex, src_x, src_y, dst_x, dst_y, offset_x, offset_y, rot): + source_rect = ray.Rectangle(src_x*32, src_y*32, 32, -32) + dest_rect = ray.Rectangle(dst_x*128 + offset_x, SHEET_SIZE-(dst_y+1)*128+ offset_y, 32, 32) + ray.draw_texture_pro(tex, source_rect, dest_rect, (0, 0), rot, ray.WHITE) + +def draw_sheet(src, hair_i, tint, seed=None): + if seed is not None: + random.seed(seed) + + base_i = random.choice(BASE) + if hair_i is None: + hair_i = random.choice(HAIR) + clothes_i = random.choice(CLOTHES) + sword_i = random.choice(SWORD) + bow_i = random.choice(BOW) + quiver_i = random.choice(QUIVER) + + hair_variant = random.randint(0, 1) + hair = [hair_dap, hair_bob][hair_variant] + sword_hair = [sword_hair_dap, sword_hair_bob][hair_variant] + bow_hair = [bow_hair_dap, bow_hair_bob][hair_variant] + + clothes_variant = random.randint(0, 1) + clothes = [peasant_clothes, forester_clothes][clothes_variant] + sword_clothes = [sword_peasant_clothes, sword_forester_clothes][clothes_variant] + bow_clothes = [bow_peasant_clothes, bow_forester_clothes][clothes_variant] + + x = 0 + y = 1024 + draw_tex(src, character, base_i, x, y) + draw_tex(src, hair, hair_i, x, y) + draw_tex(src, clothes, clothes_i, x, y) + + x = 0 + y = 0 + draw_masked_tex(src, sword_weapon, sword_i, x, y, sword_mask, tint=tint) + draw_tex(src, sword_character, base_i, x, y) + draw_tex(src, sword_hair, hair_i, x, y) + draw_tex(src, sword_clothes, clothes_i, x, y) + draw_masked_tex(src, sword_weapon, sword_i, x, y, 1-sword_mask, tint=tint) + + x = 1024 + y = 1024 + draw_masked_tex(src, bow_weapon, bow_i, x, y, bow_mask, tint=tint) + draw_masked_tex(src, bow_quiver, quiver_i, x, y, quiver_mask, tint=tint) + draw_tex(src, bow_character, base_i, x, y) + draw_tex(src, bow_hair, hair_i, x, y) + draw_tex(src, bow_clothes, clothes_i, x, y) + draw_masked_tex(src, bow_weapon, bow_i, x, y, 1-bow_mask, tint=tint) + draw_masked_tex(src, bow_quiver, quiver_i, x, y, 1-quiver_mask, tint=tint) + + arrow_path = os.path.join(src, arrow).format(i=quiver_i) + arrow_tex = ray.load_texture(arrow_path) + + ### Arrows are manually aligned + # Left facing arrows + draw_arrow(arrow_tex, 4, 1, 9, 3, 24, 40, 0) + draw_arrow(arrow_tex, 4, 1, 10, 3, 24, 40, 0) + draw_arrow(arrow_tex, 3, 1, 11, 3, 24, 52, 0) + draw_arrow(arrow_tex, 1, 1, 12, 3, 38, 64, 0) + + # Right facing arrows + draw_arrow(arrow_tex, 4, 1, 9, 2, 64+42, 48, 120) + draw_arrow(arrow_tex, 4, 1, 10, 2, 64+42, 48, 120) + draw_arrow(arrow_tex, 3, 1, 11, 2, 64+32, 82, 180) + draw_arrow(arrow_tex, 1, 1, 12, 2, 56, 98, 180+80) + + +def scale_image(image_array, scale_factor): + if scale_factor < 1: + # Scale down with exact interpolation + scaled_image_array = image_array[::int(1/scale_factor), ::int(1/scale_factor)] + elif scale_factor > 1: + # Scale up (duplicate pixels) + scaled_image_array = np.repeat( + np.repeat( + image_array, scale_factor, axis=0 + ), scale_factor, axis=1 + ) + else: + # No scaling + scaled_image_array = image_array + + return scaled_image_array + +def copy_and_scale_files(source_directory, target_directory, scale_factor): + for root, dirs, files in os.walk(source_directory): + relative_path = os.path.relpath(root, source_directory) + target_path = os.path.join(target_directory) + os.makedirs(target_path, exist_ok=True) + + for file in files: + src_file_path = os.path.join(root, file) + target_file_path = os.path.join(target_directory, file) + + path = src_file_path.lower() + if path.endswith('.ttf'): + os.copy(src_file_path, target_file_path) + continue + + if not src_file_path.lower().endswith(('.png', '.jpg', '.jpeg')): + continue + + image = Image.open(src_file_path) + image_array = np.array(image) + scaled_image_array = scale_image(image_array, scale_factor) + scaled_image = Image.fromarray(scaled_image_array) + scaled_image.save(target_file_path) + +if len(sys.argv) != 4: + print("Usage: script.py source_directory target_directory scale_factor") + sys.exit(1) + +source_directory = sys.argv[1] +target_directory = sys.argv[2] +scale_factor = float(sys.argv[3]) + +if not os.path.exists(source_directory): + print("Source directory does not exist.") + sys.exit(1) + +valid_scales = [0.125, 0.25, 0.5, 1, 2, 4] +if scale_factor not in valid_scales: + print(f'Scale factor must be one of {valid_scales}') + +intermediate_directory = os.path.join(target_directory, 'temp') +if not os.path.exists(intermediate_directory): + os.makedirs(intermediate_directory) + copy_and_scale_files(source_directory, intermediate_directory, scale_factor) + +ray.init_window(SHEET_SIZE, SHEET_SIZE, "NMMO3") +ray.set_target_fps(60) + +output_image = ray.load_render_texture(SHEET_SIZE, SHEET_SIZE) + +i = 0 +while not ray.window_should_close() and i < N_GENERATE: + ray.set_window_title(f'Generating sheet {i+1}/{N_GENERATE}') + + for elem in ELEMENTS: + elem_name, hair_i, tint = elem + ray.begin_drawing() + ray.begin_texture_mode(output_image) + ray.clear_background(ray.BLANK) + draw_sheet(intermediate_directory, hair_i, tint, seed=i) + ray.end_texture_mode() + + image = ray.load_image_from_texture(output_image.texture) + f_path = os.path.join(target_directory, f'{elem_name}_{i}.png') + ray.export_image(image, f_path) + + ray.clear_background(ray.GRAY) + ray.draw_texture(output_image.texture, 0, 0, ray.WHITE) + ray.end_drawing() + + i += 1 + +coords = (0, 1) +spring = cv2.imread(intermediate_directory + '/spring forest.png') +summer = cv2.imread(intermediate_directory + '/summer forest.png') +autumn = cv2.imread(intermediate_directory + '/autumn forest (bare).png') +winter = cv2.imread(intermediate_directory + '/winter forest (clean).png') + +spring = scale_image(spring, 2) +summer = scale_image(summer, 2) +autumn = scale_image(autumn, 2) +winter = scale_image(winter, 2) + +SEASONS = [spring, summer, autumn, winter] + +spring_sparkle = cv2.imread(intermediate_directory + '/spring water sparkles B.png') +summer_sparkle = cv2.imread(intermediate_directory + '/summer water sparkles B 16x16.png') +autumn_sparkle = cv2.imread(intermediate_directory + '/autumn water sparkles B 16x16.png') +winter_sparkle = cv2.imread(intermediate_directory + '/winter water sparkles B 16x16.png') + +spring_sparkle = scale_image(spring_sparkle, 2) +summer_sparkle = scale_image(summer_sparkle, 2) +autumn_sparkle = scale_image(autumn_sparkle, 2) +winter_sparkle = scale_image(winter_sparkle, 2) + +SPARKLES = [spring_sparkle, summer_sparkle, autumn_sparkle, winter_sparkle] + +GRASS_OFFSET = (0, 0) +DIRT_OFFSET = (5, 0) +STONE_OFFSET = (9, 0) +WATER_OFFSET = (29, 16) + +# Not compatible with water +GRASS_1 = (0, 1) +GRASS_2 = (0, 2) +GRASS_3 = (0, 3) +GRASS_4 = (0, 4) +GRASS_5 = (0, 5) + +DIRT_1 = (8, 0) +DIRT_2 = (8, 1) +DIRT_3 = (8, 2) +DIRT_4 = (8, 3) +DIRT_5 = (8, 4) + +STONE_1 = (12, 0) +STONE_2 = (12, 1) +STONE_3 = (12, 2) +STONE_4 = (12, 3) +STONE_5 = (12, 4) + +WATER_1 = (27, 14) +WATER_2 = (28, 13) +WATER_3 = (28, 14) +WATER_4 = (29, 13) +WATER_5 = (29, 14) + +GRASS_N = [GRASS_1, GRASS_2, GRASS_3, GRASS_4, GRASS_5] +DIRT_N = [DIRT_1, DIRT_2, DIRT_3, DIRT_4, DIRT_5] +STONE_N = [STONE_1, STONE_2, STONE_3, STONE_4, STONE_5] +WATER_N = [WATER_1, WATER_2, WATER_3, WATER_4, WATER_5] + +ALL_MATERIALS = [DIRT_N, STONE_N, WATER_N] +ALL_OFFSETS = [DIRT_OFFSET, STONE_OFFSET, WATER_OFFSET] + +# These values are just sentinels +# They will be mapped to GRASS/DIRT/STONE/WATER +FULL = (-1, 0) +EMPTY = (0, -1) + +TL_CORNER = (0, 0) +T_FLAT = (1, 0) +TR_CORNER = (2, 0) +L_FLAT = (0, 1) +CENTER = (1, 1) +R_FLAT = (2, 1) +BL_CORNER = (0, 2) +B_FLAT = (1, 2) +BR_CORNER = (2, 2) +TL_DIAG = (0, 3) +TR_DIAG = (1, 3) +BL_DIAG = (0, 4) +BR_DIAG = (1, 4) +TRR_DIAG = (2, 3) +BRR_DIAG = (2, 4) + +OFFSETS = [TL_CORNER, T_FLAT, TR_CORNER, L_FLAT, CENTER, R_FLAT, BL_CORNER, + B_FLAT, BR_CORNER, TL_DIAG, TR_DIAG, BL_DIAG, BR_DIAG, TRR_DIAG, BRR_DIAG] + +TILE_SIZE = int(32 * scale_factor) +SHEET_SIZE = 64 +SHEET_PX = TILE_SIZE * SHEET_SIZE +merged_sheet = np.zeros((SHEET_PX, SHEET_PX, 3), dtype=np.uint8) + +def gen_lerps(): + valid_combinations = [] + for combo in product(range(10), repeat=4): + if sum(combo) == 9 and any(weight > 0 for weight in combo): + valid_combinations.append(combo) + + return valid_combinations + +def gen_lerps(): + valid_combinations = [] + for total_sum in range(1, 10): # Loop through all possible sums from 1 to 9 + for combo in product(range(10), repeat=4): + if sum(combo) == total_sum: + valid_combinations.append(combo) + return valid_combinations + +def slice(r, c): + return np.s_[ + r*TILE_SIZE:(r+1)*TILE_SIZE, + c*TILE_SIZE:(c+1)*TILE_SIZE + ] + +idx = 0 +for sheet in SEASONS: + for offset, material in zip(ALL_OFFSETS, ALL_MATERIALS): + src_dx, src_dy = offset + + # Write full tile textures. These are irregularly + # arranged in the source sheet and require manual offsets. + for src_x, src_y in material: + dst_r, dst_c = divmod(idx, SHEET_SIZE) + idx += 1 + + src_pos = slice(src_y, src_x) + tile_tex = sheet[src_pos] + + dst_pos = slice(dst_r, dst_c) + merged_sheet[dst_pos] = tile_tex + + # Write partial tile textures. These have fixed offsets + for dx, dy in OFFSETS: + dst_r, dst_c = divmod(idx, SHEET_SIZE) + idx += 1 + + src_pos = slice(dy+src_dy, dx+src_dx) + tile_tex = sheet[src_pos] + + dst_pos = slice(dst_r, dst_c) + merged_sheet[dst_pos] = tile_tex + +for x, y in WATER_N: + # 3 animations + for anim_y in range(3): + for season, sparkle in zip(SEASONS, SPARKLES): + src_pos = slice(y, x) + tile_tex = season[src_pos] + + # 4 frame animation + for anim_x in range(4): + dst_r, dst_c = divmod(idx, SHEET_SIZE) + idx += 1 + + src_pos = slice(anim_y, anim_x) + sparkle_tex = sparkle[src_pos] + + dst_pos = slice(dst_r, dst_c) + merged_sheet[dst_pos] = tile_tex + mask = np.where(sparkle_tex != 0) + merged_sheet[dst_pos][mask] = sparkle_tex[mask] + + +for src in range(1, 5): + tex_src = slice(src, 0) + tiles = [spring[tex_src], summer[tex_src], autumn[tex_src], winter[tex_src]] + for combo in gen_lerps(): + tex = np.zeros((TILE_SIZE, TILE_SIZE, 3)) + total_weight = sum(combo) + for i, weight in enumerate(combo): + tex += weight/total_weight * tiles[i] + + tex = tex.astype(np.uint8) + + dst_r, dst_c = divmod(idx, SHEET_SIZE) + idx += 1 + + dst_pos = slice(dst_r, dst_c) + merged_sheet[dst_pos] = tex + + print(idx) + +# save image +cv2.imwrite('merged_sheet.png', merged_sheet) +cv2.imshow('merged_sheet', merged_sheet) +cv2.waitKey(0) diff --git a/pufferlib/ocean/nmmo3/nmmo3.c b/pufferlib/ocean/nmmo3/nmmo3.c new file mode 100644 index 00000000..10057231 --- /dev/null +++ b/pufferlib/ocean/nmmo3/nmmo3.c @@ -0,0 +1,471 @@ +#include +#include +#include +#include "puffernet.h" +#include "nmmo3.h" + +// Only rens a few agents in the C +// version, and reduces for web. +// You can run the full 1024 on GPU +// with PyTorch. +#if defined(PLATFORM_WEB) + #define NUM_AGENTS 4 +#else + #define NUM_AGENTS 16 +#endif + + +typedef struct MMONet MMONet; +struct MMONet { + int num_agents; + float* ob_map; + int* ob_player_discrete; + float* ob_player_continuous; + float* ob_reward; + Conv2D* map_conv1; + ReLU* map_relu; + Conv2D* map_conv2; + Embedding* player_embed; + float* proj_buffer; + Linear* proj; + ReLU* proj_relu; + LSTM* lstm; + Linear* actor; + Linear* value_fn; + Multidiscrete* multidiscrete; +}; + +MMONet* init_mmonet(Weights* weights, int num_agents) { + MMONet* net = calloc(1, sizeof(MMONet)); + int hidden = 256; + net->num_agents = num_agents; + net->ob_map = calloc(num_agents*11*15*59, sizeof(float)); + net->ob_player_discrete = calloc(num_agents*47, sizeof(int)); + net->ob_player_continuous = calloc(num_agents*47, sizeof(float)); + net->ob_reward = calloc(num_agents*10, sizeof(float)); + net->map_conv1 = make_conv2d(weights, num_agents, 15, 11, 59, 64, 5, 3); + net->map_relu = make_relu(num_agents, 64*3*4); + net->map_conv2 = make_conv2d(weights, num_agents, 4, 3, 64, 64, 3, 1); + net->player_embed = make_embedding(weights, num_agents*47, 128, 32); + net->proj_buffer = calloc(num_agents*1689, sizeof(float)); + net->proj = make_linear(weights, num_agents, 1689, hidden); + net->proj_relu = make_relu(num_agents, hidden); + net->actor = make_linear(weights, num_agents, hidden, 26); + net->value_fn = make_linear(weights, num_agents, hidden, 1); + net->lstm = make_lstm(weights, num_agents, hidden, hidden); + int logit_sizes[1] = {26}; + net->multidiscrete = make_multidiscrete(num_agents, logit_sizes, 1); + return net; +} + +void free_mmonet(MMONet* net) { + free(net->ob_map); + free(net->ob_player_discrete); + free(net->ob_player_continuous); + free(net->ob_reward); + free(net->map_conv1); + free(net->map_relu); + free(net->map_conv2); + free(net->player_embed); + free(net->proj_buffer); + free(net->proj); + free(net->proj_relu); + free(net->actor); + free(net->value_fn); + free(net->lstm); + free(net->multidiscrete); + free(net); +} + +void forward(MMONet* net, unsigned char* observations, int* actions) { + memset(net->ob_map, 0, net->num_agents*11*15*59*sizeof(float)); + + // CNN subnetwork + int factors[10] = {4, 4, 17, 5, 3, 5, 5, 5, 7, 4}; + float (*ob_map)[59][11][15] = (float (*)[59][11][15])net->ob_map; + for (int b = 0; b < net->num_agents; b++) { + int b_offset = b*(11*15*10 + 47 + 10); + for (int i = 0; i < 11; i++) { + for (int j = 0; j < 15; j++) { + int f_offset = 0; + for (int f = 0; f < 10; f++) { + int obs_idx = f_offset + observations[b_offset + i*15*10 + j*10 + f]; + ob_map[b][obs_idx][i][j] = 1; + f_offset += factors[f]; + } + } + } + } + conv2d(net->map_conv1, net->ob_map); + relu(net->map_relu, net->map_conv1->output); + conv2d(net->map_conv2, net->map_relu->output); + + // Player embedding subnetwork + for (int b = 0; b < net->num_agents; b++) { + for (int i = 0; i < 47; i++) { + unsigned char ob = observations[b*(11*15*10 + 47 + 10) + 11*15*10 + i]; + net->ob_player_discrete[b*47 + i] = ob; + net->ob_player_continuous[b*47 + i] = ob; + } + } + embedding(net->player_embed, net->ob_player_discrete); + + for (int b = 0; b < net->num_agents; b++) { + int b_offset = b*1689; + for (int i = 0; i < 128; i++) { + net->proj_buffer[b_offset + i] = net->map_conv2->output[b*128 + i]; + } + + b_offset += 128; + for (int i = 0; i < 47*32; i++) { + net->proj_buffer[b_offset + i] = net->player_embed->output[b*47*32 + i]; + } + + b_offset += 47*32; + for (int i = 0; i < 47; i++) { + net->proj_buffer[b_offset + i] = net->ob_player_continuous[b*47 + i]; + } + + b_offset += 47; + for (int i = 0; i < 10; i++) { + net->proj_buffer[b_offset + i] = net->ob_reward[b*10 + i]; + } + } + + linear(net->proj, net->proj_buffer); + relu(net->proj_relu, net->proj->output); + + lstm(net->lstm, net->proj_relu->output); + + linear(net->actor, net->lstm->state_h); + linear(net->value_fn, net->lstm->state_h); + + softmax_multidiscrete(net->multidiscrete, net->actor->output, actions); +} + +void demo(int num_players) { + Weights* weights = load_weights("resources/nmmo3/nmmo_1500.bin", 1101403); + MMONet* net = init_mmonet(weights, num_players); + + MMO env = { + .width = 512, + .height = 512, + .num_players = num_players, + .num_enemies = 2048, + .num_resources = 2048, + .num_weapons = 1024, + .num_gems = 512, + .tiers = 5, + .levels = 40, + .teleportitis_prob = 0.0, + .enemy_respawn_ticks = 2, + .item_respawn_ticks = 100, + .x_window = 7, + .y_window = 5, + }; + allocate_mmo(&env); + + reset(&env, 42); + + // Must reset before making client + Client* client = make_client(&env); + + int human_action = ATN_NOOP; + bool human_mode = false; + int i = 0; + while (!WindowShouldClose()) { + if (IsKeyPressed(KEY_LEFT_CONTROL)) { + human_mode = !human_mode; + } + if (i % 36 == 0) { + forward(net, env.obs, env.actions); + if (human_mode) { + env.actions[0] = human_action; + } + + step(&env); + //printf("Reward: %f\n\tDeath: %f\n\tProf: %f\n\tComb: %f\n\tItem: %f\n", env.rewards[0].death, env.rewards[0].death, env.rewards[0].prof_lvl, env.rewards[0].comb_lvl, env.rewards[0].item_atk_lvl); + human_action = ATN_NOOP; + } else { + int atn = tick(client, &env, i/36.0f); + if (atn != ATN_NOOP) { + human_action = atn; + } + } + i = (i + 1) % 36; + } + + free_mmonet(net); + free(weights); + free_allocated_mmo(&env); + close_client(client); +} + +void test_mmonet_performance(int num_players, int timeout) { + Weights* weights = load_weights("nmmo3_weights.bin", 1101403); + MMONet* net = init_mmonet(weights, num_players); + + MMO env = { + .width = 512, + .height = 512, + .num_players = num_players, + .num_enemies = 128, + .num_resources = 32, + .num_weapons = 32, + .num_gems = 32, + .tiers = 5, + .levels = 7, + .teleportitis_prob = 0.001, + .enemy_respawn_ticks = 10, + .item_respawn_ticks = 200, + .x_window = 7, + .y_window = 5, + }; + allocate_mmo(&env); + reset(&env, 42); + + int start = time(NULL); + int num_steps = 0; + while (time(NULL) - start < timeout) { + forward(net, env.obs, env.actions); + step(&env); + num_steps++; + } + + int end = time(NULL); + float sps = num_players * num_steps / (end - start); + printf("Test Environment Performance FPS: %f\n", sps); + free_allocated_mmo(&env); + free_mmonet(net); + free(weights); +} + +void copy_cast(float* input, unsigned char* output, int width, int height) { + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int adr = r*width + c; + output[adr] = 255*input[adr]; + } + } +} + +void raylib_grid(unsigned char* grid, int width, int height, int tile_size) { + InitWindow(width*tile_size, height*tile_size, "Raylib Grid"); + SetTargetFPS(1); + + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(BLACK); + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int adr = r*width + c; + unsigned char val = grid[adr]; + + Color color = (Color){val, val, val, 255}; + + int x = c*tile_size; + int y = r*tile_size; + DrawRectangle(x, y, tile_size, tile_size, color); + } } + EndDrawing(); + } + CloseWindow(); +} + +void raylib_grid_colored(unsigned char* grid, int width, int height, int tile_size) { + InitWindow(width*tile_size, height*tile_size, "Raylib Grid"); + SetTargetFPS(1); + + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(BLACK); + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int adr = 3*(r*width + c); + unsigned char red = grid[adr]; + unsigned char green = grid[adr+1]; + unsigned char blue = grid[adr+2]; + + Color color = (Color){red, green, blue, 255}; + + int x = c*tile_size; + int y = r*tile_size; + DrawRectangle(x, y, tile_size, tile_size, color); + } + } + EndDrawing(); + } + CloseWindow(); +} + +void test_perlin_noise(int width, int height, + float base_frequency, int octaves, int seed) { + float terrain[width*height]; + perlin_noise((float*)terrain, width, height, base_frequency, octaves, seed, seed); + + unsigned char map[width*height]; + copy_cast((float*)terrain, (unsigned char*)map, width, height); + raylib_grid((unsigned char*)map, width, height, 1024.0/width); +} + +void test_flood_fill(int width, int height, int colors) { + unsigned char unfilled[width][height]; + memset(unfilled, 0, width*height); + + // Draw some squares + for (int i = 0; i < 32; i++) { + int w = rand() % width/4; + int h = rand() % height/4; + int start_r = rand() % (3*height/4); + int start_c = rand() % (3*width/4); + int end_r = start_r + h; + int end_c = start_c + w; + for (int r = start_r; r < end_r; r++) { + unfilled[r][start_c] = 1; + unfilled[r][end_c] = 1; + } + for (int c = start_c; c < end_c; c++) { + unfilled[start_r][c] = 1; + unfilled[end_r][c] = 1; + } + } + + char filled[width*height]; + flood_fill((unsigned char*)unfilled, (char*)filled, + width, height, colors, width*height); + + // Cast and colorize + unsigned char output[width*height]; + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int adr = r*width + c; + int val = filled[adr]; + if (val == 0) { + output[adr] = 0; + } + output[adr] = 128 + (128/colors)*val; + } + } + + raylib_grid((unsigned char*)output, width, height, 1024.0/width); +} + +void test_cellular_automata(int width, int height, int colors, int max_fill) { + char grid[width][height]; + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + grid[r][c] = -1; + } + } + + // Fill some squares + for (int i = 0; i < 32; i++) { + int w = rand() % width/4; + int h = rand() % height/4; + int start_r = rand() % (3*height/4); + int start_c = rand() % (3*width/4); + int end_r = start_r + h; + int end_c = start_c + w; + int color = rand() % colors; + for (int r = start_r; r < end_r; r++) { + for (int c = start_c; c < end_c; c++) { + grid[r][c] = color; + } + } + } + + cellular_automata((char*)grid, width, height, colors, max_fill); + + // Colorize + unsigned char output[width*height]; + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int val = grid[r][c]; + int adr = r*width + c; + if (val == 0) { + output[adr] = 0; + } + output[adr] = (255/colors)*val; + } + } + + raylib_grid((unsigned char*)output, width, height, 1024.0/width); +} + +void test_generate_terrain(int width, int height, int x_border, int y_border) { + char terrain[width][height]; + unsigned char rendered[width][height][3]; + generate_terrain((char*)terrain, (unsigned char*)rendered, width, height, x_border, y_border); + + + // Colorize + /* + unsigned char output[width*height]; + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int val = terrain[r][c]; + int adr = r*width + c; + if (val == 0) { + output[adr] = 0; + } + output[adr] = (255/4)*val; + } + } + */ + + raylib_grid_colored((unsigned char*)rendered, width, height, 1024.0/width); +} + +void test_performance(int num_players, int timeout) { + MMO env = { + .width = 512, + .height = 512, + .num_players = num_players, + .num_enemies = 128, + .num_resources = 32, + .num_weapons = 32, + .num_gems = 32, + .tiers = 5, + .levels = 7, + .teleportitis_prob = 0.001, + .enemy_respawn_ticks = 10, + .item_respawn_ticks = 200, + .x_window = 7, + .y_window = 5, + }; + allocate_mmo(&env); + reset(&env, 0); + + int start = time(NULL); + int num_steps = 0; + while (time(NULL) - start < timeout) { + for (int i = 0; i < num_players; i++) { + env.actions[i] = rand() % 23; + } + step(&env); + num_steps++; + } + + int end = time(NULL); + float sps = num_players * num_steps / (end - start); + printf("Test Environment SPS: %f\n", sps); + free_allocated_mmo(&env); +} + +int main() { + + /* + int width = 512; + int height = 512; + float base_frequency = 1.0/64.0; + int octaves = 2; + int seed = 0; + test_perlin_noise(width, height, base_frequency, octaves, seed); + test_flood_fill(width, height, 4); + test_cellular_automata(width, height, 4, 4000); + test_generate_terrain(width, height, 8, 8); + */ + //test_performance(64, 10); + demo(NUM_AGENTS); + //test_mmonet_performance(1024, 10); +} diff --git a/pufferlib/ocean/nmmo3/nmmo3.h b/pufferlib/ocean/nmmo3/nmmo3.h new file mode 100644 index 00000000..c7794bc7 --- /dev/null +++ b/pufferlib/ocean/nmmo3/nmmo3.h @@ -0,0 +1,3204 @@ +// Neural MMO 3 by Joseph Suarez +// This was the first new environment I started for Puffer Ocean. +// I started it in Cython and then ported it to C. This is why there +// are still some commented sections with features I didn't get to fully +// implement, like the command console. Feel free to add and PR! +// The assets get generated from a separate script. Message me if you need those, +// since they use ManaSeed assets. I've licensed them for the project but +// can't include the source files before they've gone through spritesheet gen. + +#include +#include +#include +#include +#include +#include +#include +#include +#include "simplex.h" +#include "tile_atlas.h" +#include "raylib.h" + +#if defined(PLATFORM_DESKTOP) + #define GLSL_VERSION 330 +#else + #define GLSL_VERSION 100 +#endif + +// Play modes +#define MODE_PLAY 0 +#define MODE_BUY_TIER 1 +#define MODE_BUY_ITEM 2 +#define MODE_SELL_SELECT 3 +#define MODE_SELL_PRICE 4 + + +// Animations +#define ANIM_IDLE 0 +#define ANIM_MOVE 1 +#define ANIM_ATTACK 2 +#define ANIM_SWORD 3 +#define ANIM_BOW 4 +#define ANIM_DEATH 5 +#define ANIM_RUN 6 + +// Actions - the order of these is used for offset math +#define ATN_DOWN 0 +#define ATN_UP 1 +#define ATN_RIGHT 2 +#define ATN_LEFT 3 +#define ATN_NOOP 4 +#define ATN_ATTACK 5 +#define ATN_UI 7 +#define ATN_ONE 8 +#define ATN_TWO 9 +#define ATN_THREE 10 +#define ATN_FOUR 11 +#define ATN_FIVE 12 +#define ATN_SIX 13 +#define ATN_SEVEN 14 +#define ATN_EIGHT 15 +#define ATN_NINE 16 +#define ATN_ZERO 17 +#define ATN_MINUS 18 +#define ATN_EQUALS 19 +#define ATN_BUY 20 +#define ATN_SELL 21 +#define ATN_DOWN_SHIFT 22 +#define ATN_UP_SHIFT 23 +#define ATN_RIGHT_SHIFT 24 +#define ATN_LEFT_SHIFT 25 + +// Entity types +#define ENTITY_NULL 0 +#define ENTITY_PLAYER 1 +#define ENTITY_ENEMY 2 + +// Elements +#define ELEM_NEUTRAL 0 +#define ELEM_FIRE 1 +#define ELEM_WATER 2 +#define ELEM_EARTH 3 +#define ELEM_AIR 4 + +// Tiles + +#define TILE_SPRING_GRASS 0 +#define TILE_SUMMER_GRASS 1 +#define TILE_AUTUMN_GRASS 2 +#define TILE_WINTER_GRASS 3 +#define TILE_SPRING_DIRT 4 +#define TILE_SUMMER_DIRT 5 +#define TILE_AUTUMN_DIRT 6 +#define TILE_WINTER_DIRT 7 +#define TILE_SPRING_STONE 8 +#define TILE_SUMMER_STONE 9 +#define TILE_AUTUMN_STONE 10 +#define TILE_WINTER_STONE 11 +#define TILE_SPRING_WATER 12 +#define TILE_SUMMER_WATER 13 +#define TILE_AUTUMN_WATER 14 +#define TILE_WINTER_WATER 15 + +// Entity +#define P_N 44 +#define P_TYPE 0 +#define P_COMB_LVL 1 +#define P_ELEMENT 2 +#define P_DIR 3 +#define P_ANIM 4 +#define P_HP 5 +#define P_HP_MAX 6 +#define P_PROF_LVL 7 +#define P_EQUIP_HELM 8 +#define P_EQUIP_CHEST 9 +#define P_EQUIP_LEGS 10 +#define P_EQUIP_WEAPON 11 +#define P_EQUIP_GEM 12 +#define P_UI_MODE 13 +#define P_MARKET_TIER 14 +#define P_SELL_IDX 15 +#define P_GOLD 16 +#define P_IN_COMBAT 17 +#define P_INVENTORY 18 +#define P_INVENTORY_SIZE 12 +#define P_EQUIP_BOOLS 30 +#define P_WANDER_RANGE 42 +#define P_RANGED 43 + +// Items +#define I_N 17 +#define I_NULL 0 +#define I_HELM 1 +#define I_CHEST 2 +#define I_LEGS 3 +#define I_SWORD 4 +#define I_BOW 5 +#define I_TOOL 6 +#define I_EARTH 7 +#define I_FIRE 8 +#define I_AIR 9 +#define I_WATER 10 +#define I_HERB 11 +#define I_ORE 12 +#define I_WOOD 13 +#define I_HILT 14 +#define I_SILVER 15 +#define I_GOLD 16 + +#define INVENTORY_SIZE 12 + +// Equipment +#define SLOT_HELM 0 +#define SLOT_CHEST 1 +#define SLOT_LEGS 2 +#define SLOT_HELD 3 +#define SLOT_GEM 4 + +// Map dims +#define D_N 2 +#define D_MAP 0 +#define D_ITEM 1 + +// Extra constants +#define IN_COMBAT_TICKS 5 +#define LEVEL_MUL 2.0 +#define EQUIP_MUL 1.0 +#define TIER_EXP_BASE 8 +#define MAX_TIERS 5 +#define NPC_AGGRO_RANGE 4 + +void range(int* array, int n) { + for (int i = 0; i < n; i++) { + array[i] = i; + } +} + +void shuffle(int* array, int n) { + for (int i = 0; i < n; i++) { + int j = rand() % n; + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } +} + +double sample_exponential(double halving_rate) { + double u = (double)rand() / RAND_MAX; // Random number u in [0, 1) + return 1 + halving_rate*(-log(1 - u) / log(2)); +} + +// Terrain gen +char ALL_GRASS[4] = {TILE_SPRING_GRASS, TILE_SUMMER_GRASS, TILE_AUTUMN_GRASS, TILE_WINTER_GRASS}; +char ALL_DIRT[4] = {TILE_SPRING_DIRT, TILE_SUMMER_DIRT, TILE_AUTUMN_DIRT, TILE_WINTER_DIRT}; +char ALL_STONE[4] = {TILE_SPRING_STONE, TILE_SUMMER_STONE, TILE_AUTUMN_STONE, TILE_WINTER_STONE}; +char ALL_WATER[4] = {TILE_SPRING_WATER, TILE_SUMMER_WATER, TILE_AUTUMN_WATER, TILE_WINTER_WATER}; + +unsigned char RENDER_COLORS[16][3] = { + {60, 220, 75}, // Spring grass + {20, 180, 40}, // Summer grass + {210, 180, 40}, // Autumn grass + {240, 250, 250}, // Winter grass + {130, 110, 70}, // Spring dirt + {160, 140, 70}, // Summer dirt + {140, 120, 90}, // Autumn dirt + {130, 120, 100}, // Winter dirt + {120, 120, 130}, // Spring stone + {110, 110, 120}, // Summer stone + {100, 100, 110}, // Autumn stone + {180, 180, 190}, // Winter stone + {70, 130, 180}, // Spring water + {0, 120, 200}, // Summer water + {50, 100, 160}, // Autumn water + {210, 240, 255}, // Winter water +}; + +#define LOG_BUFFER_SIZE 1024 + +typedef struct Log Log; +struct Log { + float episode_return; + float episode_length; + float return_comb_lvl; + float return_prof_lvl; + float return_item_atk_lvl; + float return_item_def_lvl; + float return_market_buy; + float return_market_sell; + float return_death; + float min_comb_prof; + float purchases; + float sales; + float equip_attack; + float equip_defense; + float r; + float c; +}; + +typedef struct LogBuffer LogBuffer; +struct LogBuffer { + Log* logs; + int length; + int idx; +}; + +LogBuffer* allocate_logbuffer(int size) { + LogBuffer* logs = (LogBuffer*)calloc(1, sizeof(LogBuffer)); + logs->logs = (Log*)calloc(size, sizeof(Log)); + logs->length = size; + logs->idx = 0; + return logs; +} + +void free_logbuffer(LogBuffer* buffer) { + free(buffer->logs); + free(buffer); +} + +void add_log(LogBuffer* logs, Log* log) { + if (logs->idx == logs->length) { + return; + } + logs->logs[logs->idx] = *log; + logs->idx += 1; + //printf("Log: %f, %f, %f\n", log->episode_return, log->episode_length, log->score); +} + +Log aggregate_and_clear(LogBuffer* logs) { + Log log = {0}; + if (logs->idx == 0) { + return log; + } + for (int i = 0; i < logs->idx; i++) { + log.episode_return += logs->logs[i].episode_return / logs->idx; + log.episode_length += logs->logs[i].episode_length / logs->idx; + log.return_comb_lvl += logs->logs[i].return_comb_lvl / logs->idx; + log.return_prof_lvl += logs->logs[i].return_prof_lvl / logs->idx; + log.return_item_atk_lvl += logs->logs[i].return_item_atk_lvl / logs->idx; + log.return_item_def_lvl += logs->logs[i].return_item_def_lvl / logs->idx; + log.return_market_buy += logs->logs[i].return_market_buy / logs->idx; + log.return_market_sell += logs->logs[i].return_market_sell / logs->idx; + log.return_death += logs->logs[i].return_death / logs->idx; + log.min_comb_prof += logs->logs[i].min_comb_prof / logs->idx; + log.purchases += logs->logs[i].purchases / logs->idx; + log.sales += logs->logs[i].sales / logs->idx; + log.equip_attack += logs->logs[i].equip_attack / logs->idx; + log.equip_defense += logs->logs[i].equip_defense / logs->idx; + log.r += logs->logs[i].r / logs->idx; + log.c += logs->logs[i].c / logs->idx; + } + logs->idx = 0; + return log; +} + +// TODO: This is actually simplex and we should probably use the original impl +// ALSO: Not seeded correctly +void perlin_noise(float* map, int width, int height, + float base_frequency, int octaves, int offset_x, int offset_y) { + float frequencies[octaves]; + for (int i = 0; i < octaves; i++) { + frequencies[i] = base_frequency*pow(2, i); + } + + float min_value = FLT_MAX; + float max_value = FLT_MIN; + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int adr = r*width + c; + for (int oct = 0; oct < octaves; oct++) { + float freq = frequencies[oct]; + map[adr] = noise2(freq*c + offset_x, freq*r + offset_y); + } + float val = map[adr]; + if (val < min_value) { + min_value = val; + } + if (val > max_value) { + max_value = val; + } + } + } + + // TODO: You scale wrong in the original code + float scale = 1.0/(max_value - min_value); + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int adr = r*width + c; + map[adr] = scale * (map[adr] - min_value); + } + } +} + +void flood_fill(unsigned char* input, char* output, + int width, int height, int n, int max_fill) { + + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + output[r*width + c] = -1; + } + } + + int* pos = calloc(width*height, sizeof(int)); + range((int*)pos, width*height); + shuffle((int*)pos, width*height); + + short queue[2*max_fill]; + for (int i = 0; i < 2*max_fill; i++) { + queue[i] = 0; + } + + for (int idx = 0; idx < width*height; idx++) { + int r = pos[idx] / width; + int c = pos[idx] % width; + int adr = r*width + c; + + if (input[adr] != 0 || output[adr] != -1) { + continue; + } + + int color = rand() % n; + output[adr] = color; + queue[0] = r; + queue[1] = c; + + int queue_idx = 0; + int queue_n = 1; + while (queue_idx < max_fill && queue_idx < queue_n) { + r = queue[2*queue_idx]; + c = queue[2*queue_idx + 1]; + + // These checks are done before adding, even though that is + // more verbose, to preserve the max q length. There are + // also some perf benefits with the bounds check + int dd = r-1; + adr = dd*width + c; + if (dd >= 0 && input[adr] == 0 && output[adr] == -1) { + output[adr] = color; + queue[2*queue_n] = dd; + queue[2*queue_n + 1] = c; + queue_n += 1; + if (queue_n == max_fill) { + break; + } + } + dd = c-1; + adr = r*width + dd; + if (dd >= 0 && input[adr] == 0 && output[adr] == -1) { + output[adr] = color; + queue[2*queue_n] = r; + queue[2*queue_n + 1] = dd; + queue_n += 1; + if (queue_n == max_fill) { + break; + } + } + dd = r+1; + adr = dd*width + c; + if (dd < width && input[adr] == 0 && output[adr] == -1) { + output[adr] = color; + queue[2*queue_n] = dd; + queue[2*queue_n + 1] = c; + queue_n += 1; + if (queue_n == max_fill) { + break; + } + } + dd = c+1; + adr = r*width + dd; + if (dd < width && input[adr] == 0 && output[adr] == -1) { + output[adr] = color; + queue[2*queue_n] = r; + queue[2*queue_n + 1] = dd; + queue_n += 1; + if (queue_n == max_fill) { + break; + } + } + queue_idx += 1; + } + } + free(pos); +} + +void cellular_automata(char* grid, + int width, int height, int colors, int max_fill) { + + int* pos = calloc(2*width*height, sizeof(int)); + int pos_sz = 0; + for (int r = 0; r < height; r++) { + for (int c = 0; c < width; c++) { + int inp_adr = r*width + c; + if (grid[inp_adr] != -1) { + continue; + } + pos[pos_sz] = r; + pos_sz++; + pos[pos_sz] = c; + pos_sz++; + } + } + + bool done = false; + while (!done) { + // In place shuffle on active buffer only + for (int i = 0; i < pos_sz; i+=2) { + int r = pos[i]; + int c = pos[i + 1]; + int adr = rand() % pos_sz; + if (adr % 2 == 1) { + adr--; + } + pos[i] = pos[adr]; + pos[i + 1] = pos[adr + 1]; + pos[adr] = r; + pos[adr + 1] = c; + } + + done = true; + int pos_adr = 0; + for (int i = 0; i < pos_sz; i+=2) { + int r = pos[i]; + int c = pos[i + 1]; + + int counts[colors]; + for (int i = 0; i < colors; i++) { + counts[i] = 0; + } + + bool no_neighbors = true; + for (int rr = r-1; rr <= r+1; rr++) { + for (int cc = c-1; cc <= c+1; cc++) { + if (rr < 0 || rr >= height || cc < 0 || cc >= width) { + continue; + } + int adr = rr*width + cc; + int val = grid[adr]; + if (val != -1) { + counts[val] += 1; + no_neighbors = false; + } + } + } + + if (no_neighbors) { + done = false; + pos[pos_adr] = r; + pos_adr++; + pos[pos_adr] = c; + pos_adr++; + continue; + } + + // Find maximum count and ties + int max_count = 0; + for (int i = 0; i < colors; i++) { + int val_count = counts[i]; + if (val_count > max_count) { + max_count = val_count; + } + } + int num_ties = 0; + for (int i = 0; i < colors; i++) { + if (counts[i] == max_count) { + num_ties += 1; + } + } + + int idx = 0; + int winner = rand() % num_ties; + for (int j = 0; j < colors; j++) { + if (counts[j] == max_count) { + if (idx == winner) { + int adr = r*width + c; + grid[adr] = j; + break; + } + idx += 1; + } + } + } + pos_sz = pos_adr; + pos_adr = 0; + } + free(pos); +} + +void generate_terrain(char* terrain, unsigned char* rendered, + int R, int C, int x_border, int y_border) { + // Perlin noise for the base terrain + // TODO: Not handling octaves correctly + float* perlin_map = calloc(R*C, sizeof(float)); + int offset_x = rand() % 100000; + int offset_y = rand() % 100000; + perlin_noise(perlin_map, C, R, 1.0/64.0, 2, offset_x, offset_y); + + // Flood fill connected components to determine biomes + unsigned char* ridges = calloc(R*C, sizeof(unsigned char)); + for (int r = 0; r < R; r++) { + for (int c = 0; c < C; c++) { + int adr = r*C + c; + ridges[adr] = (perlin_map[adr]>0.35) & (perlin_map[adr]<0.65); + } + } + char *biomes = calloc(R*C, sizeof(char)); + flood_fill(ridges, biomes, R, C, 4, 4000); + + // Cellular automata to cover unfilled ridges + cellular_automata(biomes, R, C, 4, 4000); + + unsigned char (*rendered_ary)[C][3] = (unsigned char(*)[C][3])rendered; + + for (int r = 0; r < R; r++) { + for (int c = 0; c < C; c++) { + int tile; + int adr = r*C + c; + if (r < y_border || r >= R-y_border || c < x_border || c >= C-x_border) { + tile = TILE_SPRING_WATER; + } else { + int season = biomes[adr]; + float val = perlin_map[adr]; + if (val > 0.75) { + tile = ALL_STONE[season]; + } else if (val < 0.25) { + tile = ALL_WATER[season]; + } else { + tile = ALL_GRASS[season]; + } + } + terrain[adr] = tile; + rendered_ary[r][c][0] = RENDER_COLORS[tile][0]; + rendered_ary[r][c][1] = RENDER_COLORS[tile][1]; + rendered_ary[r][c][2] = RENDER_COLORS[tile][2]; + } + } + free(perlin_map); + free(ridges); + free(biomes); +} + +typedef struct Entity Entity; +struct Entity { + int type; + int comb_lvl; + int element; + int dir; + int anim; + int hp; + int hp_max; + int prof_lvl; + int ui_mode; + int market_tier; + int sell_idx; + int gold; + int in_combat; + int equipment[5]; + int inventory[12]; + int is_equipped[12]; + int wander_range; + int ranged; + int goal; + int equipment_attack; + int equipment_defense; + int r; + int c; + int spawn_r; + int spawn_c; + int min_comb_prof[500]; + int min_comb_prof_idx; + int time_alive; + int purchases; + int sales; +}; + +typedef struct Item Item; +struct Item { + int id; + int type; + int tier; +}; + +Item ITEMS[(MAX_TIERS+1)*I_N + 1]; + +int item_index(int i, int tier) { + return (tier-1)*I_N + i; +} + +void init_items() { + Item* item_ptr; + for (int tier = 1; tier <= MAX_TIERS+1; tier++) { + for (int i = 1; i <= I_N; i++) { + int id = item_index(i, tier); + item_ptr = &ITEMS[id]; + item_ptr->id = id; + item_ptr->type = i; + item_ptr->tier = tier; + } + } +} + +#define MAX_MARKET_OFFERS 32 + +typedef struct MarketOffer MarketOffer; +struct MarketOffer { + int id; + int seller; + int price; +}; + +typedef struct ItemMarket ItemMarket; +struct ItemMarket { + MarketOffer offers[MAX_MARKET_OFFERS]; + int next_offer_id; + int item_id; + int stock; +}; + +// TODO: Redundant? +int peek_price(ItemMarket* market) { + int stock = market->stock; + if (stock == 0) { + return 0; + } + return market->offers[stock-1].price; +} + +typedef struct Reward Reward; +struct Reward { + float total; + float death; + float pioneer; + float comb_lvl; + float prof_lvl; + float item_atk_lvl; + float item_def_lvl; + float item_tool_lvl; + float market_buy; + float market_sell; +}; + +typedef struct Respawnable Respawnable; +struct Respawnable { + int id; + int r; + int c; +}; + +typedef struct RespawnBuffer RespawnBuffer; +struct RespawnBuffer { + Respawnable* data; + int* lengths; + int ticks; + int size; +}; + +RespawnBuffer* make_respawn_buffer(int size, int ticks) { + RespawnBuffer* buffer = calloc(1, sizeof(RespawnBuffer)); + buffer->data = calloc(ticks*size, sizeof(Respawnable)); + buffer->lengths = calloc(ticks, sizeof(int)); + buffer->ticks = ticks; + buffer->size = size; + return buffer; +} + +bool has_elements(RespawnBuffer* buffer, int tick) { + return buffer->lengths[tick % buffer->ticks] > 0; +} + +void free_respawn_buffer(RespawnBuffer* buffer) { + free(buffer->data); + free(buffer->lengths); + free(buffer); +} + +void clear_respawn_buffer(RespawnBuffer* buffer) { + for (int i = 0; i < buffer->ticks; i++) { + buffer->lengths[i] = 0; + } +} + +void add_to_buffer(RespawnBuffer* buffer, Respawnable elem, int tick) { + tick = tick % buffer->ticks; + assert(buffer->lengths[tick] < buffer->size); + int offset = tick*buffer->size + buffer->lengths[tick]; + buffer->data[offset] = elem; buffer->lengths[tick] += 1; } + +Respawnable pop_from_buffer(RespawnBuffer* buffer, int tick) { + tick = tick % buffer->ticks; + assert(buffer->lengths[tick] > 0); + buffer->lengths[tick] -= 1; + int offset = tick*buffer->size + buffer->lengths[tick]; + return buffer->data[offset]; +} + +typedef struct MMO MMO; +struct MMO { + int width; + int height; + int num_players; + int num_enemies; + int num_resources; + int num_weapons; + int num_gems; + char* terrain; // TODO: Unsigned? + unsigned char* rendered; + Entity* players; + Entity* enemies; + short* pids; + unsigned char* items; + Reward* rewards; + unsigned char* counts; + unsigned char* obs; + int* actions; + int tick; + int tiers; + int levels; + float teleportitis_prob; + int x_window; + int y_window; + int obs_size; + int enemy_respawn_ticks; + int item_respawn_ticks; + ItemMarket* market; + int market_buys; + int market_sells; + RespawnBuffer* resource_respawn_buffer; + RespawnBuffer* enemy_respawn_buffer; + RespawnBuffer* drop_respawn_buffer; + Log* logs; + LogBuffer* log_buffer; + float reward_combat_level; + float reward_prof_level; + float reward_item_level; + float reward_market; + float reward_death; +}; + +Entity* get_entity(MMO* env, int pid) { + if (pid < env->num_players) { + return &env->players[pid]; + } else { + return &env->enemies[pid - env->num_players]; + } +} + +void add_player_log(MMO* env, int pid) { + LogBuffer* logs = env->log_buffer; + Log* log = &env->logs[pid]; + Entity* player = get_entity(env, pid); + log->episode_return = (log->return_comb_lvl + log->return_prof_lvl + log->return_item_atk_lvl + + log->return_item_def_lvl + log->return_market_buy + log->return_market_sell + log->return_death); + log->episode_length = player->time_alive; + log->min_comb_prof = (player->prof_lvl > player->comb_lvl) ? player->comb_lvl : player->prof_lvl; + log->purchases = player->purchases; + log->sales = player->sales; + log->equip_attack = player->equipment_attack; + log->equip_defense = player->equipment_defense; + log->r = player->r; + log->c = player->c; + add_log(logs, log); + env->logs[pid] = (Log){0}; +} + +void init_mmo(MMO* env) { + init_items(); + + int sz = env->width*env->height; + env->counts = calloc(sz, sizeof(unsigned char)); + env->terrain = calloc(sz, sizeof(char)); + env->rendered = calloc(sz*3, sizeof(unsigned char)); + + env->pids = calloc(sz, sizeof(short)); + env->items = calloc(sz, sizeof(unsigned char)); + + // Circular buffers for respawning resources and enemies + env->resource_respawn_buffer = make_respawn_buffer(2*env->num_resources + + 2*env->num_weapons + 4*env->num_gems, env->item_respawn_ticks); + env->enemy_respawn_buffer = make_respawn_buffer( + env->num_enemies, env->enemy_respawn_ticks); + env->drop_respawn_buffer = make_respawn_buffer(2*env->num_enemies, 20); + + // TODO: Figure out how to cast to array. Size is static + int num_market = (MAX_TIERS+1)*(I_N+1); + env->market = (ItemMarket*)calloc(num_market, sizeof(ItemMarket)); + + env->logs = calloc(env->num_players, sizeof(Log)); +} + +void allocate_mmo(MMO* env) { + // TODO: Not hardcode + env->obs = calloc(env->num_players*(11*15*10+47+10), sizeof(unsigned char)); + env->rewards = calloc(env->num_players, sizeof(Reward)); + env->players = calloc(env->num_players, sizeof(Entity)); + env->enemies = calloc(env->num_enemies, sizeof(Entity)); + env->actions = calloc(env->num_players, sizeof(int)); + env->log_buffer = allocate_logbuffer(LOG_BUFFER_SIZE); + init_mmo(env); +} + +void free_mmo(MMO* env) { + free(env->counts); + free(env->terrain); + free(env->rendered); + free(env->pids); + free(env->items); + free_respawn_buffer(env->resource_respawn_buffer); + free_respawn_buffer(env->enemy_respawn_buffer); + free_respawn_buffer(env->drop_respawn_buffer); + free(env->logs); + free(env->market); +} + +void free_allocated_mmo(MMO* env) { + free(env->obs); + free(env->rewards); + free(env->players); + free(env->enemies); + free(env->actions); + free_logbuffer(env->log_buffer); + free_mmo(env); +} + +bool is_buy(int mode) { + return mode == MODE_BUY_TIER || mode == MODE_BUY_ITEM; +} + +bool is_sell(int mode) { + return mode == MODE_SELL_SELECT || mode == MODE_SELL_PRICE; +} + +bool is_move(int action) { + return action >= ATN_DOWN && action <= ATN_LEFT; +} + +bool is_run(int action) { + return action >= ATN_DOWN_SHIFT && action <= ATN_LEFT_SHIFT; +} + +bool is_num(int action) { + return action >= ATN_ONE && action <= ATN_NINE; +} + +int EFFECT_MATRIX[5][5] = { + {1, 1, 1, 1, 1}, + {1, 1, 0, 1, 2}, + {1, 2, 1, 0, 1}, + {1, 1, 2, 1, 0}, + {1, 0, 1, 2, 1}, +}; + +int DELTAS[4][2] = { + {1, 0}, + {-1, 0}, + {0, 1}, + {0, -1}, +}; + +int ATTACK_BASIC[4][1][2] = { + {{1, 0}}, + {{-1, 0}}, + {{0, 1}}, + {{0, -1}}, +}; + +int ATTACK_SWORD[4][3][2] = { + {{1, -1}, {1, 0}, {1, 1}}, + {{-1, -1}, {-1, 0}, {-1, 1}}, + {{-1, 1}, {0, 1}, {1, 1}}, + {{-1, -1}, {0, -1}, {1, -1}}, +}; + +int ATTACK_BOW[4][12][2] = { + {{1, 0}, {2, 0}, {3, 0}, {4, 0}}, + {{-1, 0}, {-2, 0}, {-3, 0}, {-4, 0}}, + {{0, 1}, {0, 2}, {0, 3}, {0, 4},}, + {{0, -1}, {0, -2}, {0, -3}, {0, -4}}, +}; + +float tier_level(float tier) { + return TIER_EXP_BASE*pow(2, tier-1); +} + +float level_tier(int level) { + if (level < TIER_EXP_BASE) { + return 1; + } + return 1 + ceil(log2(level/(float)TIER_EXP_BASE)); +} + +bool PASSABLE[16] = { + true, true, true, true, // Grass tiles + true, true, true, true, // Dirt tiles + false, false, false, false, // Stone tiles + false, false, false, false, // Water tiles +}; + +bool is_grass(int tile) { + return (tile >= TILE_SPRING_GRASS && tile <= TILE_WINTER_GRASS); +} + +bool is_dirt(int tile) { + return (tile >= TILE_SPRING_DIRT && tile <= TILE_WINTER_DIRT); +} + +bool is_stone(int tile) { + return (tile >= TILE_SPRING_STONE && tile <= TILE_WINTER_STONE); +} + +bool is_water(int tile) { + return (tile >= TILE_SPRING_WATER && tile <= TILE_WINTER_WATER); +} + +int map_offset(MMO* env, int r, int c) { + return r*env->width + c; +} + +float sell_price(int idx) { + return 0.5 + 0.1*idx; +} + +void compute_all_obs(MMO* env) { + for (int pid = 0; pid < env->num_players; pid++) { + Entity* player = get_entity(env, pid); + int r = player->r; + int c = player->c; + + int start_row = r - env->y_window; + int end_row = r + env->y_window + 1; + int start_col = c - env->x_window; + int end_col = c + env->x_window + 1; + + assert(start_row >= 0); + assert(end_row <= env->height); + assert(start_col >= 0); + assert(end_col <= env->width); + + int comb_lvl = player->comb_lvl; + int obs_adr = pid*(11*15*10+47+10); + for (int obs_r = start_row; obs_r < end_row; obs_r++) { + for (int obs_c = start_col; obs_c < end_col; obs_c++) { + int map_adr = map_offset(env, obs_r, obs_c); + + // Split by terrain type and season + unsigned char terrain = env->terrain[map_adr]; + env->obs[obs_adr] = terrain % 4; + env->obs[obs_adr+1] = terrain / 4; + + // Split by item type and tier + unsigned char item = env->items[map_adr]; + env->obs[obs_adr+2] = item % 17; + env->obs[obs_adr+3] = item / 17; + + int pid = env->pids[map_adr]; + if (pid != -1) { + Entity* seen = get_entity(env, pid); + env->obs[obs_adr+4] = seen->type; + env->obs[obs_adr+5] = seen->element; + int delta_comb_obs = (seen->comb_lvl - comb_lvl) / 2; + if (delta_comb_obs < 0) { + delta_comb_obs = 0; + } + if (delta_comb_obs > 4) { + delta_comb_obs = 4; + } + env->obs[obs_adr+6] = delta_comb_obs; + env->obs[obs_adr+7] = seen->hp / 20; // Bucketed for discrete + env->obs[obs_adr+8] = seen->anim; + env->obs[obs_adr+9] = seen->dir; + } + obs_adr += 10; + } + } + + // Player observation + env->obs[obs_adr] = player->type; + env->obs[obs_adr+1] = player->comb_lvl; + env->obs[obs_adr+2] = player->element; + env->obs[obs_adr+3] = player->dir; + env->obs[obs_adr+4] = player->anim; + env->obs[obs_adr+5] = player->hp; + env->obs[obs_adr+6] = player->hp_max; + env->obs[obs_adr+7] = player->prof_lvl; + env->obs[obs_adr+8] = player->ui_mode; + env->obs[obs_adr+9] = player->market_tier; + env->obs[obs_adr+10] = player->sell_idx; + env->obs[obs_adr+11] = player->gold; + env->obs[obs_adr+12] = player->in_combat; + for (int j = 0; j < 5; j++) { + env->obs[obs_adr+13+j] = player->equipment[j]; + } + for (int j = 0; j < 12; j++) { + env->obs[obs_adr+18+j] = player->inventory[j]; + } + for (int j = 0; j < 12; j++) { + env->obs[obs_adr+30+j] = player->is_equipped[j]; + } + env->obs[obs_adr+42] = player->wander_range; + env->obs[obs_adr+43] = player->ranged; + env->obs[obs_adr+44] = player->goal; + env->obs[obs_adr+45] = player->equipment_attack; + env->obs[obs_adr+46] = player->equipment_defense; + + // Reward observation + Reward* reward = &env->rewards[pid]; + env->obs[obs_adr+47] = (reward->death == 0) ? 0 : 1; + env->obs[obs_adr+48] = (reward->pioneer == 0) ? 0 : 1; + env->obs[obs_adr+49] = reward->comb_lvl / 20; + env->obs[obs_adr+50] = reward->prof_lvl / 20; + env->obs[obs_adr+51] = reward->item_atk_lvl / 20; + env->obs[obs_adr+52] = reward->item_def_lvl / 20; + env->obs[obs_adr+53] = reward->item_tool_lvl / 20; + env->obs[obs_adr+54] = reward->market_buy / 20; + env->obs[obs_adr+55] = reward->market_sell / 20; + } +} + +int safe_tile(MMO* env, int delta) { + bool valid = false; + int idx; + while (!valid) { + valid = true; + idx = rand() % (env->width * env->height); + char tile = env->terrain[idx]; + if (!is_grass(tile)) { + valid = false; + continue; + } + int r = idx / env->width; + int c = idx % env->width; + + for (int dr = -delta; dr <= delta; dr++) { + for (int dc = -delta; dc <= delta; dc++) { + int adr = map_offset(env, r+dr, c+dc); + if (env->pids[adr] != -1) { + valid = false; + break; + } + } + if (!valid) { + break; + } + } + } + return idx; +} + +// Spawns a player at the specified position. +// Can spawn on top of another player, but this will not corrupt the state. +// They will just move off of each other. +void spawn(MMO* env, Entity* entity) { + entity->hp = 99; + entity->time_alive = 0; + entity->purchases = 0; + entity->sales = 0; + + int idx = safe_tile(env, 5); + int r = idx / env->width; + int c = idx % env->width; + + //entity->r = entity->spawn_r; + //entity->c = entity->spawn_c; + entity->spawn_r = r; + entity->spawn_c = c; + entity->r = r; + entity->c = c; + + entity->anim = ANIM_IDLE; + entity->dir = ATN_DOWN; + entity->ui_mode = MODE_PLAY; + entity->gold = 0; + entity->in_combat = 0; + entity->equipment_attack = 0; + entity->equipment_defense = 0; + + // Try zeroing levels too + entity->prof_lvl = 1; + entity->comb_lvl = 1; + + entity->equipment[SLOT_HELM] = 0; + entity->equipment[SLOT_CHEST] = 0; + entity->equipment[SLOT_LEGS] = 0; + entity->equipment[SLOT_HELD] = 0; + entity->equipment[SLOT_GEM] = 0; + + int num_slots = sizeof(entity->inventory) / sizeof(entity->inventory[0]); + for (int idx = 0; idx < num_slots; idx++) { + entity->inventory[idx] = 0; + entity->is_equipped[idx] = 0; + } + + entity->goal = (rand() % 2) == 0; + memset(entity->min_comb_prof, 0, sizeof(entity->min_comb_prof)); + entity->min_comb_prof_idx = 0; +} + +void give_starter_gear(MMO* env, int pid, int tier) { + assert(tier >= 1); + assert(tier <= env->tiers); + + Entity* player = &env->players[pid]; + int idx = (rand() % 6) + 1; + tier = (rand() % tier) + 1; + player->inventory[0] = item_index(idx, tier); + player->gold += 50; +} + +int get_free_inventory_idx(MMO* env, int pid) { + Entity* player = &env->players[pid]; + // TODO: #define this + int num_slots = sizeof(player->inventory) / sizeof(player->inventory[0]); + for (int idx = 0; idx < num_slots; idx++) { + int item_type = player->inventory[idx]; + if (item_type == 0) { + return idx; + } + } + return -1; +} + +void pickup_item(MMO* env, int pid) { + Entity* player = &env->players[pid]; + if (player->type != ENTITY_PLAYER) { + return; + } + + int r = player->r; + int c = player->c; + int adr = map_offset(env, r, c); + int ground_id = env->items[adr]; + if (ground_id == 0) { + return; + } + + int inventory_idx = get_free_inventory_idx(env, pid); + if (inventory_idx == -1) { + return; + } + + Item* ground_item = &ITEMS[ground_id]; + int ground_type = ground_item->type; + + // This is the only item that can be picked up without a tool + if (ground_type == I_TOOL) { + player->inventory[inventory_idx] = ground_id; + env->items[adr] = 0; + return; + } + + int ground_tier = ground_item->tier; + int held_id = player->equipment[SLOT_HELD]; + Item* held_item = &ITEMS[held_id]; + int held_type = held_item->type; + int held_tier = held_item->tier; + if (held_type != I_TOOL) { + return; + } + if (held_tier < ground_tier) { + return; + } + + // Harvest resource + Respawnable respawnable = {.id = ground_id, .r = r, .c = c}; + add_to_buffer(env->resource_respawn_buffer, respawnable, env->tick); + Reward* reward = &env->rewards[pid]; + Log* log = &env->logs[pid]; + + // Level up for a worthy harvest + if (player->prof_lvl < env->levels && player->prof_lvl < tier_level(ground_tier)) { + player->prof_lvl += 1; + reward->prof_lvl = env->reward_prof_level; + log->return_prof_lvl += env->reward_prof_level; + } + + // Some items are different on the ground and in inventory + if (ground_type == I_ORE) { + int armor_id = I_HELM + rand() % 3; + ground_id = item_index(armor_id, ground_tier); + } else if (ground_type == I_HILT) { + ground_id = item_index(I_SWORD, ground_tier); + } else if (ground_type == I_WOOD) { + ground_id = item_index(I_BOW, ground_tier); + } else { + ground_id = item_index(ground_type, ground_tier); + } + player->inventory[inventory_idx] = ground_id; + env->items[adr] = 0; +} + +bool dest_check(MMO* env, int r, int c); +inline bool dest_check(MMO* env, int r, int c) { + int adr = map_offset(env, r, c); + return PASSABLE[(int)env->terrain[adr]] & (env->pids[adr] == -1); +} + +void move(MMO* env, int pid, int direction, bool run) { + Entity* entity = get_entity(env, pid); + int r = entity->r; + int c = entity->c; + int dr = DELTAS[direction][0]; + int dc = DELTAS[direction][1]; + int rr = r + dr; + int cc = c + dc; + + entity->dir = direction; + + if (!dest_check(env, rr, cc)) { + return; + } + + if (run) { + rr += dr; + cc += dc; + if (!dest_check(env, rr, cc)) { + return; + } + } + + // Move to new pos. + entity->r = rr; + entity->c = cc; + entity->anim = (run ? ANIM_RUN : ANIM_MOVE); + env->pids[map_offset(env, rr, cc)] = pid; + + int old_adr = map_offset(env, r, c); + env->pids[old_adr] = -1; + + // Update visitation map. Skips run tiles + if (entity->type == ENTITY_PLAYER) { + if (env->counts[map_offset(env, rr, cc)] == 0) { + env->rewards[pid].pioneer = 1.0; + } + if (env->counts[map_offset(env, rr, cc)] < 255) { + env->counts[map_offset(env, rr, cc)] += 1; + } + pickup_item(env, pid); + } +} + +void wander(MMO* env, int pid) { + Entity* entity = get_entity(env, pid); + int wander_range = entity->wander_range; + int spawn_r = entity->spawn_r; + int spawn_c = entity->spawn_c; + int end_r = spawn_r; + int end_c = spawn_c; + + // Return entity to wander area + if (end_r - spawn_r > wander_range) { + move(env, pid, ATN_UP, false); + return; + } + if (end_r - spawn_r < -wander_range) { + move(env, pid, ATN_DOWN, false); + return; + } + if (end_c - spawn_c > wander_range) { + move(env, pid, ATN_LEFT, false); + return; + } + if (end_c - spawn_c < -wander_range) { + move(env, pid, ATN_RIGHT, false); + return; + } + + // Move randomly + int direction = rand() % 4; + if (direction == ATN_UP) { + end_r -= 1; + } else if (direction == ATN_DOWN) { + end_r += 1; + } else if (direction == ATN_LEFT) { + end_c -= 1; + } else if (direction == ATN_RIGHT) { + end_c += 1; + } + + move(env, pid, direction, false); +} + +// Agents gain 2 damage per level and 1 per equip level. With 3 +// pieces of equipment, that is a total of 5 per level. Base damage is +// 40 and enemies start with a decrease of 25. So with a 5 level difference, +// players and enemies are equally matched. +int calc_damage(MMO* env, int pid, int target_id) { + Entity* attacker = get_entity(env, pid); + Entity* defender = get_entity(env, target_id); + + int attack = 40 + LEVEL_MUL*attacker->comb_lvl + attacker->equipment_attack; + int defense = LEVEL_MUL*defender->comb_lvl + defender->equipment_defense; + + // These buffs compensate for enemies not having equipment + if (attacker->type == ENTITY_ENEMY) { + attack += 3*EQUIP_MUL*attacker->comb_lvl - 25; + } + if (defender->type == ENTITY_ENEMY) { + defense += 3*EQUIP_MUL*defender->comb_lvl; + } + + int damage = fmax(attack - defense, 0); + + // Not very / normal / super effective + return damage * EFFECT_MATRIX[attacker->element][defender->element]; +} + +int find_target(MMO* env, int pid, int entity_type) { + Entity* entity = get_entity(env, pid); + int r = entity->r; + int c = entity->c; + int weapon_id = entity->equipment[SLOT_HELD]; + int anim; + int* flat_deltas; + int num_deltas = 0; + if (weapon_id == 0 || ITEMS[weapon_id].type == I_TOOL) { + flat_deltas = (int*)ATTACK_BASIC; + anim = ANIM_ATTACK; + num_deltas = 1; + } else if (ITEMS[weapon_id].type == I_BOW) { + flat_deltas = (int*)ATTACK_BOW; + anim = ANIM_BOW; + num_deltas = 12; + } else if (ITEMS[weapon_id].type == I_SWORD) { + flat_deltas = (int*)ATTACK_SWORD; + anim = ANIM_SWORD; + num_deltas = 3; + } else { + assert(false); + exit(1); + } + + entity->anim = anim; + int (*deltas)[num_deltas][2] = (int(*)[num_deltas][2])flat_deltas; + for (int direction = 0; direction < 4; direction++) { + for (int idx = 0; idx < num_deltas; idx++) { + int dr = deltas[direction][idx][0]; + int dc = deltas[direction][idx][1]; + int rr = r + dr; + int cc = c + dc; + + int adr = map_offset(env, rr, cc); + int target_id = env->pids[adr]; + if (target_id == -1) { + continue; + } + + Entity* target = get_entity(env, target_id); + if (target->type != entity_type) { + continue; + } + + entity->dir = direction; + return target_id; + } + } + return -1; +} + +void drop_loot(MMO* env, int pid) { + Entity* entity = get_entity(env, pid); + int loot_tier = level_tier(entity->comb_lvl); + if (loot_tier > env->tiers) { + loot_tier = env->tiers; + } + + int drop = item_index(I_TOOL, loot_tier); + int r = entity->r; + int c = entity->c; + + // Drop loot on a free tile + for (int dr = -1; dr <= 1; dr++) { + for (int dc = -1; dc <= 1; dc++) { + int adr = map_offset(env, r+dr, c+dc); + if (env->items[adr] != 0) { + continue; + } + env->items[adr] = drop; + Respawnable elem = {.id = drop, .r = r+dr, .c = c+dc}; + add_to_buffer(env->drop_respawn_buffer, elem, env->tick); + return; + } + } +} + +void attack(MMO* env, int pid, int target_id) { + Entity* attacker = get_entity(env, pid); + Entity* defender = get_entity(env, target_id); + + // Extra check avoids multiple xp/loot drops + // if two players attack the same target at the same time + if (defender->hp == 0) { + return; + } + + attacker->in_combat = IN_COMBAT_TICKS; + defender->in_combat = IN_COMBAT_TICKS; + int dmg = calc_damage(env, pid, target_id); + + // Simple case: target survives + if (dmg < defender->hp) { + defender->hp -= dmg; + return; + } + + // Defender dies + defender->hp = 0; + if (defender->type == ENTITY_PLAYER) { + Reward* reward = &env->rewards[target_id]; + Log* log = &env->logs[target_id]; + reward->death = env->reward_death; + log->return_death += env->reward_death; + env->rewards[target_id].death = -1; + add_player_log(env, target_id); + } else { + // Add to respawn buffer + Respawnable respawnable = {.id = target_id, + .r = defender->spawn_r, .c = defender->spawn_c}; + add_to_buffer(env->enemy_respawn_buffer, respawnable, env->tick); + } + + if (attacker->type == ENTITY_PLAYER) { + Reward* reward = &env->rewards[pid]; + Log* log = &env->logs[pid]; + int attacker_lvl = attacker->comb_lvl; + int defender_lvl = defender->comb_lvl; + + // Level up for defeating worthy foe + if (defender_lvl >= attacker_lvl && attacker_lvl < env->levels) { + attacker->comb_lvl += 1; + reward->comb_lvl = env->reward_combat_level; + log->return_comb_lvl += env->reward_combat_level; + } + if (defender->type == ENTITY_ENEMY) { + drop_loot(env, target_id); + attacker->gold += 1 + defender_lvl / 10; + // Overflow + if (attacker->gold > 99) { + attacker->gold = 99; + } + } + } +} + +void use_item(MMO* env, int pid, int inventory_idx) { + Entity* player = &env->players[pid]; + Reward* reward = &env->rewards[pid]; + Log* log = &env->logs[pid]; + int item_id = player->inventory[inventory_idx]; + + if (item_id == 0) { + return; + } + + Item* item = &ITEMS[item_id]; + int item_type = item->type; + int tier = item->tier; + + // Consumable + if (item_type == I_HERB) { + int hp_restore = 50 + 10*tier; + if (player->hp > player->hp_max - hp_restore) { + player->hp = player->hp_max; + } else { + player->hp += hp_restore; + } + player->inventory[inventory_idx] = 0; + return; + } + + // Cannot equip in combat + if (player->in_combat > 0) { + return; + } + + int element = -1; + int attack = 0; + int defense = 0; + int equip_slot = 0; + + if (item_type == I_HELM) { + equip_slot = SLOT_HELM; + defense = EQUIP_MUL*tier_level(tier); + } else if (item_type == I_CHEST) { + equip_slot = SLOT_CHEST; + defense = EQUIP_MUL*tier_level(tier); + } else if (item_type == I_LEGS) { + equip_slot = SLOT_LEGS; + defense = EQUIP_MUL*tier_level(tier); + } else if (item_type == I_SWORD) { + equip_slot = SLOT_HELD; + attack = 3*EQUIP_MUL*tier_level(tier); + } else if (item_type == I_BOW) { + equip_slot = SLOT_HELD; + attack = 3*EQUIP_MUL*tier_level(tier - 0.5); + } else if (item_type == I_TOOL) { + equip_slot = SLOT_HELD; + } else if (item_type == I_EARTH) { + equip_slot = SLOT_GEM; + element = ELEM_EARTH; + } else if (item_type == I_FIRE) { + equip_slot = SLOT_GEM; + element = ELEM_FIRE; + } else if (item_type == I_AIR) { + equip_slot = SLOT_GEM; + element = ELEM_AIR; + } else if (item_type == I_WATER) { + equip_slot = SLOT_GEM; + element = ELEM_WATER; + } else { + exit(1); + } + + float item_reward = env->reward_item_level * (float)tier / env->tiers; + + // Unequip item if already equipped + if (player->is_equipped[inventory_idx]) { + player->is_equipped[inventory_idx] = 0; + player->equipment[equip_slot] = 0; + player->equipment_attack -= attack; + player->equipment_defense -= defense; + if (item_type == I_TOOL) { + reward->item_tool_lvl = -item_reward; + } else { + if (attack > 0) { + reward->item_atk_lvl = -item_reward; + log->return_item_atk_lvl -= item_reward; + } + if (defense > 0) { + reward->item_def_lvl = -item_reward; + log->return_item_def_lvl -= item_reward; + } + } + if (equip_slot == SLOT_GEM) { + player->element = ELEM_NEUTRAL; + } + return; + } + + // Another item is already equipped. We don't support switching + // gear without unequipping because it adds complexity to the item repr + if (player->equipment[equip_slot] != 0) { + return; + } + + // Equip the current item + player->is_equipped[inventory_idx] = 1; + player->equipment[equip_slot] = item_id; + player->equipment_attack += attack; + player->equipment_defense += defense; + if (item_type == I_TOOL) { + reward->item_tool_lvl = item_reward; + } else { + if (attack > 0) { + reward->item_atk_lvl = item_reward; + log->return_item_atk_lvl += item_reward; + } + if (defense > 0) { + reward->item_def_lvl = item_reward; + log->return_item_def_lvl += item_reward; + } + } + + // Update element for gems + if (element != -1) { + player->element = element; + } +} + +void enemy_ai(MMO* env, int pid) { + Entity* enemy = get_entity(env, pid); + int r = enemy->r; + int c = enemy->c; + + for (int rr = r-NPC_AGGRO_RANGE; rr <= r+NPC_AGGRO_RANGE; rr++) { + for (int cc = c-NPC_AGGRO_RANGE; cc <= c+NPC_AGGRO_RANGE; cc++) { + int adr = map_offset(env, rr, cc); + int target_id = env->pids[adr]; + if (target_id == -1 || target_id >= env->num_players) { + continue; + } + + int dr = rr - r; + int dc = cc - c; + int abs_dr = abs(dr); + int abs_dc = abs(dc); + + int direction; + if (enemy->ranged) { + if (abs_dr == 0 && abs_dc <= NPC_AGGRO_RANGE) { + direction = (dc > 0) ? ATN_RIGHT : ATN_LEFT; + enemy->anim = ANIM_BOW; + attack(env, pid, target_id); + } else if (abs_dc == 0 && abs_dr <= NPC_AGGRO_RANGE) { + direction = (dr > 0) ? ATN_DOWN : ATN_UP; + enemy->anim = ANIM_BOW; + attack(env, pid, target_id); + } else { + if (abs_dr > abs_dc) { + direction = (dc > 0) ? ATN_RIGHT : ATN_LEFT; + } else { + direction = (dr > 0) ? ATN_DOWN : ATN_UP; + } + // Move along shortest axis + move(env, pid, direction, false); + } + } else { + if (abs_dr + abs_dc == 1) { + if (dr > 0) { + direction = ATN_DOWN; + } else if (dr < 0) { + direction = ATN_UP; + } else if (dc > 0) { + direction = ATN_RIGHT; + } else { + direction = ATN_LEFT; + } + enemy->anim = ANIM_SWORD; + attack(env, pid, target_id); + } else { + // Move along longest axis + if (abs_dr > abs_dc) { + direction = (dr > 0) ? ATN_DOWN : ATN_UP; + } else { + direction = (dc > 0) ? ATN_RIGHT : ATN_LEFT; + } + move(env, pid, direction, false); + } + } + enemy->dir = direction; + return; + } + } + wander(env, pid); +} + +void reset(MMO* env, int seed) { + srand(time(NULL)); + env->tick = 0; + + env->market_sells = 0; + env->market_buys = 0; + + clear_respawn_buffer(env->resource_respawn_buffer); + clear_respawn_buffer(env->enemy_respawn_buffer); + + // TODO: Check width/height args! + generate_terrain(env->terrain, env->rendered, env->width, env->height, + env->x_window, env->y_window); + + for (int i = 0; i < env->width*env->height; i++) { + env->pids[i] = -1; + env->items[i] = 0; + //env->counts[i] = 0; + } + + // Pid crops? + int ore_count = 0; + int herb_count = 0; + int wood_count = 0; + int hilt_count = 0; + int earth_gem_count = 0; + int fire_gem_count = 0; + int air_gem_count = 0; + int water_gem_count = 0; + int player_count = 0; + int enemy_count = 0; + + // Randomly generate spawn candidates + int *spawn_cands = calloc(env->width*env->height, sizeof(int)); + range((int*)spawn_cands, env->width*env->height); + shuffle((int*)spawn_cands, env->width*env->height); + + for (int cand_idx = 0; cand_idx < env->width*env->height; cand_idx++) { + int cand = spawn_cands[cand_idx]; + int r = cand / env->width; + int c = cand % env->width; + int tile = env->terrain[cand]; + + if (!is_grass(tile)) { + continue; + } + + // Materials only spawn south + //if (r < env->height/2) { + // continue; + //} + + int spawned = false; + int i_type; + for (int d = 0; d < 4; d++) { + int adr = map_offset(env, r+DELTAS[d][0], c+DELTAS[d][1]); + int tile = env->terrain[adr]; + if (is_stone(tile)) { + if (ore_count < env->num_resources) { + i_type = I_ORE; + ore_count += 1; + spawned = true; + break; + } + if (hilt_count < env->num_weapons) { + i_type = I_HILT; + hilt_count += 1; + spawned = true; + break; + } + } else if (is_water(tile)) { + if (herb_count < env->num_resources) { + i_type = I_HERB; + herb_count += 1; + spawned = true; + break; + } + if (wood_count < env->num_weapons) { + i_type = I_WOOD; + wood_count += 1; + spawned = true; + break; + } + } + } + + int adr = map_offset(env, r, c); + //int tier = 1 + env->tiers*level/env->levels; + int tier = 0; + while (tier < 1 || tier > env->tiers) { + tier = sample_exponential(1); + } + + if (spawned) { + env->items[adr] = item_index(i_type, tier); + continue; + } + + // Spawn gems + i_type = 0; + if (tile == TILE_SPRING_GRASS && earth_gem_count < env->num_gems) { + earth_gem_count += 1; + i_type = I_EARTH; + } else if (tile == TILE_SUMMER_GRASS && fire_gem_count < env->num_gems) { + fire_gem_count += 1; + i_type = I_FIRE; + } else if (tile == TILE_AUTUMN_GRASS && air_gem_count < env->num_gems) { + air_gem_count += 1; + i_type = I_AIR; + } else if (tile == TILE_WINTER_GRASS && water_gem_count < env->num_gems) { + water_gem_count += 1; + i_type = I_WATER; + } + + if (i_type > 0) { + env->items[adr] = item_index(i_type, tier); + } + + if ( + player_count == env->num_players && + enemy_count == env->num_enemies && + ore_count == env->num_resources && + herb_count == env->num_resources && + wood_count == env->num_weapons && + hilt_count == env->num_weapons && + earth_gem_count == env->num_gems && + fire_gem_count == env->num_gems && + air_gem_count == env->num_gems && + water_gem_count == env->num_gems + ) { + break; + } + } + + assert(ore_count == env->num_resources); + assert(herb_count == env->num_resources); + assert(wood_count == env->num_weapons); + assert(hilt_count == env->num_weapons); + assert(earth_gem_count == env->num_gems); + assert(fire_gem_count == env->num_gems); + assert(air_gem_count == env->num_gems); + assert(water_gem_count == env->num_gems); + free(spawn_cands); + + //int distance = abs(r - env->height/2); + for (int player_count = 0; player_count < env->num_players; player_count++) { + int pid = player_count; + Entity* player = &env->players[pid]; + player->type = ENTITY_PLAYER; + player->element = ELEM_NEUTRAL; + player->comb_lvl = 1; + player->prof_lvl = 1; + player->hp_max = 99; + spawn(env, player); + int adr = map_offset(env, player->r, player->c); + env->pids[adr] = pid; + // Debug starter gear + //give_starter_gear(env, pid, env->tiers); + } + + // Spawn enemies off of middle Y + //int level = fmax(1, env->levels * (distance-12) / (0.9*env->height/2 - 24)); + //level = fmin(level, env->levels); + for (int enemy_count = 0; enemy_count < env->num_enemies; enemy_count++) { + int level = 0; + while (level < 1 || level > env->levels) { + level = sample_exponential(8); + } + if (rand() % 8 == 0) { + level = 1; + } + //if (distance > 8 && r < env->height/2 && enemy_count < env->num_enemies) { + Entity* enemy = &env->enemies[enemy_count]; + enemy->type = ENTITY_ENEMY; + enemy->hp_max = 99; + enemy->wander_range = 3; + + spawn(env, enemy); + int adr = map_offset(env, enemy->r, enemy->c); + char tile = env->terrain[adr]; + + int element = ELEM_NEUTRAL; + int ranged = true; + if (level < 15) { + ranged = false; + } else if (tile == TILE_SPRING_GRASS) { + element = ELEM_EARTH; + } else if (tile == TILE_SUMMER_GRASS) { + element = ELEM_FIRE; + } else if (tile == TILE_AUTUMN_GRASS) { + element = ELEM_AIR; + } else if (tile == TILE_WINTER_GRASS) { + element = ELEM_WATER; + } + enemy->element = element; + enemy->ranged = ranged; + + env->pids[adr] = env->num_players + enemy_count; + enemy->comb_lvl = level; + } + + compute_all_obs(env); +} + +void step(MMO* env) { + env->tick += 1; + int tick = env->tick; + + // Respawn resources + RespawnBuffer* buffer = env->resource_respawn_buffer; + while (has_elements(buffer, tick)) { + Respawnable item = pop_from_buffer(buffer, tick); + int item_id = item.id; + assert(item_id > 0); + int adr = map_offset(env, item.r, item.c); + env->items[adr] = item_id; + } + + // Respawn enemies + buffer = env->enemy_respawn_buffer; + while (has_elements(buffer, tick)) { + int pid = pop_from_buffer(buffer, tick).id; + assert(pid >= 0); + Entity* entity = get_entity(env, pid); + int lvl = entity->comb_lvl; + spawn(env, entity); + int adr = map_offset(env, entity->r, entity->c); + env->pids[adr] = pid; + entity->comb_lvl = lvl; + } + + // Despawn dropped items + buffer = env->drop_respawn_buffer; + while (has_elements(buffer, tick)) { + Respawnable item = pop_from_buffer(buffer, tick); + int id = item.id; + int r = item.r; + int c = item.c; + int adr = map_offset(env, r, c); + if (env->items[adr] == id) { + env->items[adr] = 0; + } + } + + for (int pid = 0; pid < env->num_players + env->num_enemies; pid++) { + Entity* entity = get_entity(env, pid); + entity->time_alive += 1; + int entity_type = entity->type; + int r = entity->r; + int c = entity->c; + int adr = map_offset(env, r, c); + + // Respawn dead entity + if (entity->hp == 0) { + if (entity->anim != ANIM_DEATH) { + entity->anim = ANIM_DEATH; + } else if (env->pids[adr] == pid) { + env->pids[adr] = -1; + } else if (entity_type == ENTITY_PLAYER) { + spawn(env, entity); + adr = map_offset(env, entity->r, entity->c); + env->pids[adr] = pid; + //give_starter_gear(env, pid, env->tiers); + } + continue; + } + + // Teleportitis: Randomly teleport players and enemies + // to a safe tile. This prevents players from clumping + // and messing up training dynamics + double prob = (double)rand() / RAND_MAX; + if (prob < env->teleportitis_prob) { + r = entity->r; + c = entity->c; + adr = map_offset(env, r, c); + env->pids[adr] = -1; + + int idx = safe_tile(env, 5); + r = idx / env->width; + c = idx % env->width; + + adr = map_offset(env, r, c); + env->pids[adr] = pid; + + entity->r = r; + entity->c = c; + } + + if (entity_type == ENTITY_PLAYER) { + int min_comb_prof = entity->prof_lvl; + if (min_comb_prof > entity->comb_lvl) { + min_comb_prof = entity->comb_lvl; + } + entity->min_comb_prof[entity->min_comb_prof_idx] = min_comb_prof; + entity->min_comb_prof_idx += 1; + if (entity->min_comb_prof_idx == 500) { + entity->min_comb_prof_idx = 0; + if (min_comb_prof <= entity->min_comb_prof[0]) { + add_player_log(env, pid); + + // Has not improved in 500 ticks + r = entity->r; + c = entity->c; + adr = map_offset(env, r, c); + env->pids[adr] = -1; + int lvl = entity->comb_lvl; + spawn(env, entity); + r = entity->r; + c = entity->c; + adr = map_offset(env, r, c); + env->pids[adr] = pid; + if (entity->type == ENTITY_PLAYER) { + //give_starter_gear(env, pid, env->tiers); + } else { + entity->comb_lvl = lvl; + } + continue; + } + } + } + + entity->anim = ANIM_IDLE; + + // Restore 1 HP each tick + if (entity->hp < entity->hp_max) { + entity->hp += 1; + } + + // Decrement combat counter + if (entity->in_combat > 0) { + entity->in_combat -= 1; + } + + // Enemy AI + if (entity_type == ENTITY_ENEMY) { + enemy_ai(env, pid); + continue; + } + + Reward* reward = &env->rewards[pid]; + reward->total = 0; + reward->death = 0; + reward->pioneer = 0; + reward->comb_lvl = 0; + reward->prof_lvl = 0; + reward->item_atk_lvl = 0; + reward->item_def_lvl = 0; + reward->item_tool_lvl = 0; + reward->market_buy = 0; + reward->market_sell = 0; + + // Update entity heading + int action = env->actions[pid]; + if (is_move(action)) { + entity->dir = action - ATN_DOWN; + } else if (is_run(action)) { + entity->dir = action - ATN_DOWN_SHIFT; + } + + // Market mode + int ui_mode = entity->ui_mode; + if (is_buy(ui_mode)) { + if (action != ATN_NOOP) { + entity->ui_mode = MODE_PLAY; + } + if (!is_num(action)) { + continue; + } + if (entity->in_combat > 0) { + continue; + } + int action_idx = action - ATN_ONE; + if (ui_mode == MODE_BUY_TIER) { + if (action_idx >= env->tiers) { + continue; + } + entity->market_tier = action_idx + 1; + entity->ui_mode = MODE_BUY_ITEM; + continue; + } + if (action_idx >= 11) { + continue; + } + int market_tier = entity->market_tier; + int item_id = I_N*(market_tier - 1) + action_idx + 1; + ItemMarket* market = &env->market[item_id]; + + int stock = market->stock; + if (stock == 0) { + continue; + } + + MarketOffer* offer = &market->offers[stock-1]; + + int price = offer->price; + Entity* buyer = get_entity(env, pid); + if (buyer->gold < price) { + continue; + } + + int inventory_idx = get_free_inventory_idx(env, pid); + if (inventory_idx == -1) { + continue; + } + + buyer->gold -= price; + buyer->inventory[inventory_idx] = market->item_id; + + Entity* seller = &env->players[offer->seller]; + seller->gold += price; + if (seller->gold > 99) { + seller->gold = 99; + } + + market->stock -= 1; + env->market_buys += 1; + Log* log = &env->logs[pid]; + reward->market_buy = env->reward_market; + log->return_market_buy = env->reward_market; + // env->rewards[buyer_id].gold += price; + // if (env->rewards[buyer_id].gold > 99) { + // env->rewards[buyer_id].gold = 99; + // } + + entity->inventory[inventory_idx] = item_id; + entity->purchases += 1; + } else if (is_sell(ui_mode)) { + if (action != ATN_NOOP) { + entity->ui_mode = MODE_PLAY; + } + if (!is_num(action)) { + continue; + } + if (entity->in_combat > 0) { + continue; + } + int action_idx = action - ATN_ONE; + if (ui_mode == MODE_SELL_SELECT) { + int item_type = entity->inventory[action_idx]; + if (item_type == 0) { + continue; + } + entity->sell_idx = action_idx; + entity->ui_mode = MODE_SELL_PRICE; + continue; + } + int price = action_idx + 1; //sell_price(action_idx); + int inventory_idx = entity->sell_idx; + int item_type = entity->inventory[inventory_idx]; + if (item_type == 0) { + continue; + } + if (entity->is_equipped[inventory_idx]) { + use_item(env, pid, inventory_idx); + } + + ItemMarket* market = &env->market[item_type]; + int stock = market->stock; + + // TODO: Will have to update once prices become dynamic + if (stock == MAX_MARKET_OFFERS) { + continue; + } + + MarketOffer* offer = &market->offers[stock]; + offer->id = market->next_offer_id; + offer->seller = pid; + offer->price = price; + market->next_offer_id += 1; + market->stock += 1; + + entity->inventory[inventory_idx] = 0; + entity->sales += 1; + env->market_sells += 1; + Log* log = &env->logs[pid]; + reward->market_sell = env->reward_market; + log->return_market_sell = env->reward_market; + } else if (action == ATN_ATTACK) { + int target_id = find_target(env, pid, ENTITY_ENEMY); + if (target_id != -1) { + attack(env, pid, target_id); + } + } else if (is_move(action)) { + move(env, pid, action, false); + } else if (is_run(action)) { + move(env, pid, action - ATN_DOWN_SHIFT, true); + } else if (is_num(action)) { + use_item(env, pid, action - ATN_ONE); + } else if (action == ATN_BUY) { + entity->ui_mode = MODE_BUY_TIER; + } else if (action == ATN_SELL) { + entity->ui_mode = MODE_SELL_SELECT; + } + } + compute_all_obs(env); + for (int pid = 0; pid < env->num_players; pid++) { + Reward* reward = &env->rewards[pid]; + reward->total = reward->death + reward->comb_lvl + + reward->prof_lvl + reward->item_atk_lvl + reward->item_def_lvl + + reward->market_buy + reward->market_sell; + } +} + +#define FRAME_RATE 60 +#define TICK_FRAMES 36 +#define DELAY_FRAMES 24 +#define SPRITE_SIZE 128 +#define TILE_SIZE 64 +#define X_WINDOW 7 +#define Y_WINDOW 5 + +#define SCREEN_WIDTH TILE_SIZE * (2*X_WINDOW + 1) +#define SCREEN_HEIGHT TILE_SIZE * (2*Y_WINDOW + 1) + +#define NUM_PLAYER_TEXTURES 10 + +// Health bars +#define HEALTH_BAR_WIDTH 48 +#define HEALTH_BAR_HEIGHT 6 + +#define WATER_ANIMS 3 +#define WATER_ANIM_FRAMES 4 +#define WATER_TICKS_PER_FRAME TICK_FRAMES / WATER_ANIM_FRAMES + +#define COMMAND_CHARS 16 + +#define ANIM_IDLE 0 +#define ANIM_MOVE 1 +#define ANIM_ATTACK 2 +#define ANIM_SWORD 3 +#define ANIM_BOW 4 +#define ANIM_DEATH 5 +#define ANIM_RUN 6 + +#define MAX_ANIM_FRAMES 16 +#define SPRITE_SIZE 128 + +#define OVERLAY_NONE 0 +#define OVERLAY_COUNTS 1 +#define OVERLAY_VALUE 2 + +#define ITEM_TYPES 17 + +int ITEM_TEXTURES[ITEM_TYPES*MAX_TIERS]; + +typedef struct Client Client; +struct Client { + Texture2D tiles; + Texture2D players[5][NUM_PLAYER_TEXTURES]; + Shader shader; + int shader_camera_x_loc; + float shader_camera_x; + int shader_camera_y_loc; + float shader_camera_y; + int shader_time_loc; + float shader_time; + int shader_terrain_loc; + int shader_map_width_loc; + int shader_map_height_loc; + unsigned char *shader_terrain_data; + Texture2D shader_terrain; + int shader_texture_tiles_loc; + Texture2D shader_texture_tiles; + int shader_resolution_loc; + float shader_resolution[3]; + + //Texture2D players; + Texture2D items; + Texture2D inventory; + Texture2D inventory_equip; + Texture2D inventory_selected; + Font font; + int* terrain; + int command_mode; + char command[COMMAND_CHARS]; + int command_len; + Camera2D camera; + RenderTexture2D map_buffer; + RenderTexture2D ui_buffer; + int render_mode; + Texture2D overlay_texture; + int active_overlay; + int my_player; + int start_time; +}; + +#define TILE_SPRING_GRASS 0 +#define TILE_SUMMER_GRASS 1 +#define TILE_AUTUMN_GRASS 2 +#define TILE_WINTER_GRASS 3 +#define TILE_SPRING_DIRT 4 +#define TILE_SUMMER_DIRT 5 +#define TILE_AUTUMN_DIRT 6 +#define TILE_WINTER_DIRT 7 +#define TILE_SPRING_STONE 8 +#define TILE_SUMMER_STONE 9 +#define TILE_AUTUMN_STONE 10 +#define TILE_WINTER_STONE 11 +#define TILE_SPRING_WATER 12 +#define TILE_SUMMER_WATER 13 +#define TILE_AUTUMN_WATER 14 +#define TILE_WINTER_WATER 15 + +#define RENDER_MODE_FIXED 0 +#define RENDER_MODE_CENTERED 1 + +char* KEYS[12] = { + "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=" +}; + +int TILE_UV[16][2] = { + {0, 8*TILE_SIZE}, + {0, 13*TILE_SIZE}, + {4*TILE_SIZE, 8*TILE_SIZE}, + {2*TILE_SIZE, 8*TILE_SIZE}, + {0, 0}, + {0, 0}, + {0, 0}, + {0, 0}, + {8*TILE_SIZE, 3*TILE_SIZE}, + {8*TILE_SIZE, 3*TILE_SIZE}, + {8*TILE_SIZE, 3*TILE_SIZE}, + {8*TILE_SIZE, 3*TILE_SIZE}, + {0, 4*TILE_SIZE}, + {0, 4*TILE_SIZE}, + {0, 4*TILE_SIZE}, + {0, 4*TILE_SIZE}, +}; + +typedef struct Animation Animation; +struct Animation { + int num_frames; + int tiles_traveled; + int offset; // Number of tiles from the top of the sheet + int frames[10]; // Order of frames in sheet, left to right +}; + +Animation ANIMATIONS[7] = { + (Animation){ // ANIM_IDLE + .num_frames = 1, + .tiles_traveled = 0, + .offset = 0, + .frames = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + }, + (Animation){ // ANIM_MOVE + .num_frames = 6, + .tiles_traveled = 1, + .offset = 4, + .frames = {0, 1, 2, 3, 4, 5, 0, 0, 0, 0} + }, + (Animation){ // ANIM_ATTACK + .num_frames = 2, + .tiles_traveled = 0, + .offset = 0, + .frames = {1, 2, 0, 0, 0, 0, 0, 0, 0, 0} + }, + (Animation){ // ANIM_SWORD + .num_frames = 8, + .tiles_traveled = 0, + .offset = 8, + .frames = {0, 1, 6, 3, 4, 7, 0, 0, 0, 0} + }, + (Animation){ // ANIM_BOW + .num_frames = 8, + .tiles_traveled = 0, + .offset = 0, + .frames = {8, 9, 10, 11, 12, 13, 14, 15, 0, 0} + }, + (Animation){ // ANIM_DEATH + .num_frames = 3, + .tiles_traveled = 0, + .offset = 0, + .frames = {5, 6, 7, 0, 0, 0, 0, 0, 0, 0} + }, + (Animation){ // ANIM_RUN + .num_frames = 6, + .tiles_traveled = 2, + .offset = 4, + .frames = {0, 1, 6, 3, 4, 7, 0, 0, 0, 0} + }, +}; + +#define TEX_FULL -2 +#define TEX_EMPTY -1 +#define TEX_TL_CORNER 0 +#define TEX_T_FLAT 1 +#define TEX_TR_CORNER 2 +#define TEX_L_FLAT 3 +#define TEX_CENTER 4 +#define TEX_R_FLAT 5 +#define TEX_BL_CORNER 6 +#define TEX_B_FLAT 7 +#define TEX_BR_CORNER 8 +#define TEX_TL_DIAG 9 +#define TEX_TR_DIAG 10 +#define TEX_BL_DIAG 11 +#define TEX_BR_DIAG 12 +#define TEX_TRR_DIAG 13 +#define TEX_BRR_DIAG 14 + +#define OFF 20 +#define GRASS_OFFSET 512+32 +#define WATER_OFFSET OFF * 2 +#define STONE_OFFSET OFF * 1 +#define DIRT_OFFSET 0 + +void render_conversion(char* flat_tiles, int* flat_converted, int R, int C) { + char* tex_codes = tile_atlas; + char (*tiles)[C] = (char(*)[C])flat_tiles; + int (*converted)[C] = (int(*)[C])flat_converted; + + for (int r = 1; r < R-1; r++) { + for (int c = 1; c < C-1; c++) { + int tile = tiles[r][c]; + assert(flat_tiles[r*C + c] == tile); + int byte_code = 0; + if (is_grass(tile)) { + byte_code = 255; + } else { + if (tiles[r-1][c-1] != tile) { + byte_code += 128; + } + if (tiles[r-1][c] != tile) { + byte_code += 64; + } + if (tiles[r-1][c+1] != tile) { + byte_code += 32; + } + if (tiles[r][c-1] != tile) { + byte_code += 16; + } + if (tiles[r][c+1] != tile) { + byte_code += 8; + } + if (tiles[r+1][c-1] != tile) { + byte_code += 4; + } + if (tiles[r+1][c] != tile) { + byte_code += 2; + } + if (tiles[r+1][c+1] != tile) { + byte_code += 1; + } + } + + // Code maps local tile regions to a snapping tile index + int code = tex_codes[byte_code]; + int idx = code; + if (code == TEX_FULL) { + if (is_dirt(tile)) { + idx = DIRT_OFFSET + rand() % 5; + } else if (is_stone(tile)) { + idx = STONE_OFFSET + rand() % 5; + } else if (is_water(tile)) { + idx = WATER_OFFSET + rand() % 5; + } + } else if (is_dirt(tile)) { + idx += DIRT_OFFSET + 5; + } else if (is_stone(tile)) { + idx += STONE_OFFSET + 5; + } else if (is_water(tile)) { + idx += WATER_OFFSET + 5; + } + + if (!is_grass(tile)) { + if (tile == TILE_SUMMER_DIRT || tile == TILE_SUMMER_STONE + || tile == TILE_SUMMER_WATER) { + idx += 3*OFF; + } else if (tile == TILE_AUTUMN_DIRT || tile == TILE_AUTUMN_STONE + || tile == TILE_AUTUMN_WATER) { + idx += 6*OFF; + } else if (tile == TILE_WINTER_DIRT || tile == TILE_WINTER_STONE + || tile == TILE_WINTER_WATER) { + idx += 9*OFF; + } + } + if (is_grass(tile) || code == TEX_EMPTY) { + int num_spring = 0; + int num_summer = 0; + int num_autumn = 0; + int num_winter = 0; + for (int rr = r-1; rr <= r+1; rr++) { + for (int cc = c-1; cc <= c+1; cc++) { + int tile = tiles[rr][cc]; + if (tile == TILE_SPRING_GRASS) { + num_spring += 1; + } else if (tile == TILE_SUMMER_GRASS) { + num_summer += 1; + } else if (tile == TILE_AUTUMN_GRASS) { + num_autumn += 1; + } else if (tile == TILE_WINTER_GRASS) { + num_winter += 1; + } + } + } + + if (num_spring == 0 && num_summer == 0 + && num_autumn == 0 && num_winter == 0) { + idx = 240; + } else { + int lookup = (1000*num_spring + 100*num_summer + + 10*num_autumn + num_winter); + int offset = (rand() % 4) * 714; // num_lerps; + idx = lerps[lookup] + offset + 240 + 5*4*3*4; + } + } + if (code == TEX_FULL && is_water(tile)) { + int variant = (rand() % 5); + int anim = rand() % 3; + idx = 240 + 3*4*4*variant + 4*4*anim; + if (tile == TILE_SPRING_WATER) { + idx += 0; + } else if (tile == TILE_SUMMER_WATER) { + idx += 4; + } else if (tile == TILE_AUTUMN_WATER) { + idx += 8; + } else if (tile == TILE_WINTER_WATER) { + idx += 12; + } + } + converted[r][c] = idx; + } + } +} + +Client* make_client(MMO* env) { + + InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "NMMO3"); + SetTargetFPS(FRAME_RATE); + + Client* client = calloc(1, sizeof(Client)); + client->start_time = time(NULL); + client->command_len = 0; + + client->terrain = calloc(env->height*env->width, sizeof(int)); + render_conversion(env->terrain, client->terrain, env->height, env->width); + + client->shader = LoadShader("", TextFormat("resources/nmmo3/map_shader_%i.fs", GLSL_VERSION)); + + // TODO: These should be int locs? + client->shader_map_width_loc = GetShaderLocation(client->shader, "map_width"); + client->shader_map_height_loc = GetShaderLocation(client->shader, "map_height"); + client->shader_camera_x_loc = GetShaderLocation(client->shader, "camera_x"); + client->shader_camera_y_loc = GetShaderLocation(client->shader, "camera_y"); + client->shader_time_loc = GetShaderLocation(client->shader, "time"); + client->shader_resolution_loc = GetShaderLocation(client->shader, "resolution"); + client->shader_texture_tiles_loc = GetShaderLocation(client->shader, "texture_tiles"); + client->shader_terrain_loc = GetShaderLocation(client->shader, "terrain"); + Image img = GenImageColor(env->width, env->height, WHITE); + ImageFormat(&img, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + client->shader_terrain = LoadTextureFromImage(img); + UnloadImage(img); + client->shader_terrain_data = malloc(env->width*env->height*4); + //SetShaderValue(client->shader, client->shader_terrain_loc, &client->terrain, SHADER_UNIFORM_INT); + + for (int i = 0; i < env->width*env->height; i++) { + int tile = client->terrain[i]; + //if (tile >= 240 && tile < 240+4*4*4*4) { + // tile += 3.9*delta; + //} + + client->shader_terrain_data[4*i] = tile/64; + client->shader_terrain_data[4*i+1] = tile%64; + //client->shader_terrain_data[2*i] = 0; + //client->shader_terrain_data[2*i+1] = 0; + client->shader_terrain_data[4*i+2] = 0; + client->shader_terrain_data[4*i+3] = 255; + } + + client->render_mode = RENDER_MODE_CENTERED; + client->tiles = LoadTexture("resources/nmmo3/merged_sheet.png"); + + //client->players = LoadTexture("../resource/neutral_0.png"); + client->items = LoadTexture("resources/nmmo3/items_condensed.png"); + client->inventory = LoadTexture("resources/nmmo3/inventory_64.png"); + client->inventory_equip = LoadTexture("resources/nmmo3/inventory_64_selected.png"); + client->inventory_selected = LoadTexture("resources/nmmo3/inventory_64_press.png"); + client->font = LoadFont("resources/nmmo3/ManaSeedBody.ttf"); + for (int i = 0; i < NUM_PLAYER_TEXTURES; i++) { + client->players[0][i] = LoadTexture(TextFormat("resources/nmmo3/neutral_%d.png", i)); + client->players[1][i] = LoadTexture(TextFormat("resources/nmmo3/fire_%d.png", i)); + client->players[2][i] = LoadTexture(TextFormat("resources/nmmo3/water_%d.png", i)); + client->players[3][i] = LoadTexture(TextFormat("resources/nmmo3/earth_%d.png", i)); + client->players[4][i] = LoadTexture(TextFormat("resources/nmmo3/air_%d.png", i)); + } + + // TODO: Why do I need to cast here? + client->camera = (Camera2D){ + .target = {.x = env->width/2*TILE_SIZE, .y = env->height/2*TILE_SIZE}, + .offset = {.x = 0.0, .y = 0.0}, + .rotation = 0.0, + .zoom = 1.0, + }; + + int buffer_width = SCREEN_WIDTH + 4*TILE_SIZE; + int buffer_height = SCREEN_HEIGHT + 4*TILE_SIZE; + client->map_buffer = LoadRenderTexture(buffer_width, buffer_height); + client->ui_buffer = LoadRenderTexture(SCREEN_WIDTH, SCREEN_HEIGHT); + + return client; +} + +void close_client(Client* client) { + UnloadRenderTexture(client->map_buffer); + UnloadRenderTexture(client->ui_buffer); + for (int i = 0; i < NUM_PLAYER_TEXTURES; i++) { + for (int element = 0; element < 5; element++) { + UnloadTexture(client->players[i][element]); + } + } + UnloadFont(client->font); + UnloadTexture(client->tiles); + UnloadTexture(client->items); + UnloadTexture(client->inventory); + UnloadTexture(client->inventory_equip); + UnloadTexture(client->inventory_selected); + UnloadTexture(client->shader_terrain); + free(client->shader_terrain_data); + CloseWindow(); +} + +void draw_health_bar(int bar_x, int bar_y, int health, int max_health) { + DrawRectangle(bar_x, bar_y, HEALTH_BAR_WIDTH, + HEALTH_BAR_HEIGHT, RED); + DrawRectangle(bar_x, bar_y, + HEALTH_BAR_WIDTH * health / max_health, + HEALTH_BAR_HEIGHT, GREEN); + DrawRectangleLines(bar_x, bar_y, HEALTH_BAR_WIDTH, + HEALTH_BAR_HEIGHT, BLACK); +} + +void draw_inventory_item(Client* client, int idx, int item_type) { + if (item_type == 0) { + return; + } + Vector2 pos = { + .x = TILE_SIZE*idx + TILE_SIZE/4, + .y = SCREEN_HEIGHT - 5*TILE_SIZE/4, + }; + Rectangle source_rect = { + .x = TILE_SIZE*(ITEMS[item_type].tier - 1), + .y = TILE_SIZE*(ITEMS[item_type].type - 1), + .width = TILE_SIZE, + .height = TILE_SIZE, + }; + DrawTextureRec(client->items, source_rect, pos, WHITE); +} + +void draw_inventory_slot(Client* client, int idx, Texture2D* tex) { + Vector2 pos = { + .x = TILE_SIZE*idx + TILE_SIZE/4, + .y = SCREEN_HEIGHT - 5*TILE_SIZE/4, + }; + Rectangle source_rect = { + .x = 0, + .y = 0, + .width = TILE_SIZE, + .height = TILE_SIZE, + }; + DrawTextureRec(*tex, source_rect, pos, WHITE); +} + +void draw_inventory_label(Client* client, int idx, const char* label) { + Vector2 pos = { + .x = TILE_SIZE*idx + TILE_SIZE/2, + .y = SCREEN_HEIGHT - 5*TILE_SIZE/4 - 20, + }; + DrawTextEx(client->font, label, pos, 20, 4, YELLOW); +} + +void draw_all_slots(Client* client, Entity* player, int action) { + for (int i = 0; i < 12; i++) { + Texture2D* tex; + int mode = player->ui_mode; + if (i == action - ATN_ONE) { + tex = &client->inventory_selected; + } else if ((mode==MODE_PLAY || mode==MODE_SELL_SELECT) && + player->is_equipped[i] == 1) { + tex = &client->inventory_equip; + } else { + tex = &client->inventory; + } + //TODO: Draw inventory slot + draw_inventory_slot(client, i, tex); + } +} + +void draw_ui(Client* client, MMO* env, Entity* player, int action) { + draw_all_slots(client, player, action); + + int mode = player->ui_mode; + if (mode == MODE_PLAY || mode == MODE_SELL_SELECT) { + for (int idx = 0; idx < INVENTORY_SIZE; idx++) { + int item_type = player->inventory[idx]; + draw_inventory_item(client, idx, item_type); + } + } else if (mode == MODE_SELL_PRICE) { + for (int tier = 0; tier < 5; tier++) { + int item_type = item_index(I_SILVER, tier+1); + draw_inventory_item(client, tier, item_type); + } + for (int tier = 0; tier < 5; tier++) { + int item_type = item_index(I_GOLD, tier+1); + draw_inventory_item(client, tier+5, item_type); + } + for (int idx = 0; idx < 10; idx++) { + int price = idx + 1; + draw_inventory_label(client, idx, TextFormat("$%d", price)); + } + } else if (mode == MODE_BUY_TIER) { + for (int tier = 0; tier < MAX_TIERS; tier++) { + int item_type = item_index(I_SWORD, tier+1); + draw_inventory_item(client, tier, item_type); + draw_inventory_label(client, tier, TextFormat("T%d", tier+1)); + } + } else if (mode == MODE_BUY_ITEM) { + int tier = player->market_tier; + for (int idx = 0; idx < 11; idx++) { + int item_id = I_N*(tier-1) + idx + 1; + draw_inventory_item(client, idx, item_id); + // TODO: add prices to obs + int price = peek_price(&env->market[item_id]); + //price = extra_player_ob[idx + P.INVENTORY]; + const char* label = (price == 0) ? "Out!" : TextFormat("$%d", price); + draw_inventory_label(client, idx, label); + } + } + + // Draw number keys + for (int i = 0; i < 12; i++) { + Vector2 pos = { + .x = TILE_SIZE*i + TILE_SIZE/2 - 4, + .y = SCREEN_HEIGHT - TILE_SIZE/2 - 12, + }; + DrawTextEx(client->font, KEYS[i], pos, 20, 0, YELLOW); + } + + if (mode != MODE_PLAY) { + char* label; + if (mode == MODE_BUY_TIER || mode == MODE_BUY_ITEM) { + label = (char*) TextFormat("Buy Mode (b=cancel)\n\nYour gold: $%d", player->gold); + } else { + label = (char*) TextFormat("Sell Mode (v=cancel)"); + } + + Vector2 pos = { + .x = TILE_SIZE/2, + .y = SCREEN_HEIGHT - 2.5*TILE_SIZE, + }; + DrawTextEx(client->font, label, pos, 20, 4, YELLOW); + } + + if (player->in_combat > 0) { + Vector2 pos = { + .x = SCREEN_WIDTH - 500, + .y = TILE_SIZE/2, + }; + DrawTextEx(client->font, TextFormat("In combat. Cannot equip items."), + pos, 20, 4, RED); + } +} + +int simple_hash(int n) { + return ((n * 2654435761) & 0xFFFFFFFF) % INT_MAX; +} + +void draw_entity(Client* client, MMO* env, int pid, float delta) { + Entity* entity = get_entity(env, pid); + Animation* animation = &ANIMATIONS[entity->anim]; + + // Player texture + int element = entity->element; + int hashed = simple_hash(pid + client->start_time % 100); + Texture2D* tex = &client->players[element][hashed % NUM_PLAYER_TEXTURES]; + + int frame = delta * animation->num_frames; + Rectangle source_rect = { + .x = SPRITE_SIZE*animation->frames[frame], + .y = SPRITE_SIZE*(animation->offset + entity->dir), + .width = SPRITE_SIZE, + .height = SPRITE_SIZE, + }; + + float dx = 0; + float dy = 0; + if (entity->dir == 0) { + dy = -animation->tiles_traveled; + } else if (entity->dir == 1) { + dy = animation->tiles_traveled; + } else if (entity->dir == 2) { + dx = -animation->tiles_traveled; + } else if (entity->dir == 3) { + dx = animation->tiles_traveled; + } + dx = (1.0 - delta) * dx; + dy = (1.0 - delta) * dy; + + int x_pos = (dx + entity->c - 0.5f)*TILE_SIZE; + int y_pos = (dy + entity->r - 0.5f)*TILE_SIZE; + Vector2 pos = {.x = x_pos, .y = y_pos}; + + DrawTextureRec(*tex, source_rect, pos, WHITE); + + // Health bar + int bar_x = x_pos + TILE_SIZE - HEALTH_BAR_WIDTH/2; + int bar_y = y_pos; + draw_health_bar(bar_x, bar_y, entity->hp, entity->hp_max); + + // Overhead text + int comb_lvl = entity->comb_lvl; + int prof_lvl = entity->prof_lvl; + char* txt; + Color color; + if (entity->type == ENTITY_PLAYER) { + txt = (char*) TextFormat("%d: Lv %d/%d", pid, comb_lvl, prof_lvl); + color = GREEN; + } else { + txt = (char*) TextFormat("%d: Lv %d", pid, comb_lvl); + color = RED; + } + + Vector2 text_pos = {.x = bar_x, .y = bar_y - 20}; + DrawTextEx(client->font, txt, text_pos, 14, 1, color); +} + +void draw_min(Client* client, MMO* env, int x, int y, + int width, int height, int C, int R, float scale, float delta) { + client->shader_resolution[0] = GetRenderWidth(); + client->shader_resolution[1] = GetRenderHeight(); + client->shader_resolution[2] = client->camera.zoom; + client->shader_camera_x = client->camera.target.x; + client->shader_camera_y = client->camera.target.y; + client->shader_time = delta; + + BeginShaderMode(client->shader); + float map_width = env->width; + float map_height = env->height; + SetShaderValue(client->shader, client->shader_map_width_loc, &map_width, SHADER_UNIFORM_FLOAT); + SetShaderValue(client->shader, client->shader_map_height_loc, &map_height, SHADER_UNIFORM_FLOAT); + SetShaderValue(client->shader, client->shader_camera_x_loc, &client->shader_camera_x, SHADER_UNIFORM_FLOAT); + SetShaderValue(client->shader, client->shader_camera_y_loc, &client->shader_camera_y, SHADER_UNIFORM_FLOAT); + SetShaderValue(client->shader, client->shader_time_loc, &client->shader_time, SHADER_UNIFORM_FLOAT); + SetShaderValue(client->shader, client->shader_resolution_loc, client->shader_resolution, SHADER_UNIFORM_VEC3); + + SetShaderValueTexture(client->shader, client->shader_texture_tiles_loc, client->tiles); + + UpdateTexture(client->shader_terrain, client->shader_terrain_data); + SetShaderValueTexture(client->shader, client->shader_terrain_loc, client->shader_terrain); + + DrawRectangle( + client->camera.target.x - GetRenderWidth()/2/client->camera.zoom, + client->camera.target.y - GetRenderHeight()/2/client->camera.zoom, + GetRenderWidth()/client->camera.zoom, + GetRenderHeight()/client->camera.zoom, + WHITE + ); + + EndShaderMode(); + + for (int r = y; r < y+height; r++) { + for (int c = x; c < x+width; c++) { + int adr = r*C + c; + int tile = client->terrain[adr]; + if (tile >= 240 && tile < 240+4*4*4*4) { + tile += 3.9*delta; + } + //int u = TILE_SIZE*(tile % 64); + //int v = TILE_SIZE*(tile / 64); + Vector2 pos = { + .x = c*TILE_SIZE, + .y = r*TILE_SIZE, + }; + if (IsKeyDown(KEY_H) && env->pids[adr] != -1) { + DrawRectangle(pos.x, pos.y, TILE_SIZE, TILE_SIZE, (Color){0, 255, 255, 128}); + } + /* + Rectangle source_rect = (Rectangle){ + .x = u, + .y = v, + .width = TILE_SIZE, + .height = TILE_SIZE, + }; + + DrawTextureRec(client->tiles, source_rect, pos, (Color){255, 255, 255, 128}); + */ + + // Draw item + if (env->items[adr] != 0) { + int item_id = env->items[adr]; + int item_tier = ITEMS[item_id].tier; + int item_type = ITEMS[item_id].type; + Rectangle source_rect = { + .x = (item_tier - 1)*TILE_SIZE, + .y = (item_type - 1)*TILE_SIZE, + .width = TILE_SIZE, + .height = TILE_SIZE, + }; + DrawTextureRec(client->items, source_rect, pos, WHITE); + } + } + } + +} + +void render_centered(Client* client, MMO* env, int pid, int action, float delta) { + Entity* player = get_entity(env, pid); + int r = player->r; + int c = player->c; + + //Animation* animation = ANIM_SPRITE[player->anim]; + //float travel_x, travel_y; + //travel_x, travel_y = animation.get_travel(player.dir) + float travel_x = 0; + float travel_y = 0; + Animation* animation = &ANIMATIONS[player->anim]; + if (player->dir == 0) { + travel_y = -animation->tiles_traveled; + } else if (player->dir == 1) { + travel_y = animation->tiles_traveled; + } else if (player->dir == 2) { + travel_x = animation->tiles_traveled; + } else if (player->dir == 3) { + travel_x = -animation->tiles_traveled; + } + travel_x *= TILE_SIZE; + travel_y *= TILE_SIZE; + + client->camera.offset.x = SCREEN_WIDTH/2; + client->camera.offset.y = SCREEN_HEIGHT/2; + client->camera.target.x = (c + 0.5)*TILE_SIZE + (delta - 1)*travel_x; + client->camera.target.y = (r + 0.5)*TILE_SIZE + (1 - delta)*travel_y; + client->camera.zoom = 1.0; + + int start_c = c - X_WINDOW - 2; + if (start_c < 0) { + start_c = 0; + } + + int start_r = r - Y_WINDOW - 2; + if (start_r < 0) { + start_r = 0; + } + + int end_r = r + Y_WINDOW + 3; + if (end_r > env->height) { + end_r = env->height; + } + + int end_c = c + X_WINDOW + 3; + if (end_c > env->width) { + end_c = env->width; + } + + //BeginMode2D(client.camera); + BeginMode2D(client->camera); + draw_min(client, env, start_c, + start_r, end_c-start_c, end_r-start_r, + env->width, env->height, 1, delta); + + for (int pid = 0; pid < env->num_players+env->num_enemies; pid++) { + draw_entity(client, env, pid, delta); + } + + EndMode2D(); + draw_ui(client, env, player, action); +} + +bool up_key() { + return IsKeyDown(KEY_UP) || IsKeyDown(KEY_W); +} + +bool down_key() { + return IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S); +} + +bool left_key() { + return IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A); +} + +bool right_key() { + return IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D); +} + +bool shift_key() { + return IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); +} + +int process_centered_input() { + if (IsKeyDown(KEY_ESCAPE)) { + CloseWindow(); + } + + if (shift_key()) { + if (down_key()) { + return ATN_DOWN_SHIFT; + } else if (up_key()) { + return ATN_UP_SHIFT; + } else if (left_key()) { + return ATN_LEFT_SHIFT; + } else if (right_key()) { + return ATN_RIGHT_SHIFT; + } + } else if (up_key()) { + return ATN_UP; + } else if (down_key()) { + return ATN_DOWN; + } else if (left_key()) { + return ATN_LEFT; + } else if (right_key()) { + return ATN_RIGHT; + } else if (IsKeyDown(KEY_SPACE)) { + return ATN_ATTACK; + } else if (IsKeyDown(KEY_ONE)) { + return ATN_ONE; + } else if (IsKeyDown(KEY_TWO)) { + return ATN_TWO; + } else if (IsKeyDown(KEY_THREE)) { + return ATN_THREE; + } else if (IsKeyDown(KEY_FOUR)) { + return ATN_FOUR; + } else if (IsKeyDown(KEY_FIVE)) { + return ATN_FIVE; + } else if (IsKeyDown(KEY_SIX)) { + return ATN_SIX; + } else if (IsKeyDown(KEY_SEVEN)) { + return ATN_SEVEN; + } else if (IsKeyDown(KEY_EIGHT)) { + return ATN_EIGHT; + } else if (IsKeyDown(KEY_NINE)) { + return ATN_NINE; + } else if (IsKeyDown(KEY_ZERO)) { + return ATN_ZERO; + } else if (IsKeyDown(KEY_MINUS)) { + return ATN_MINUS; + } else if (IsKeyDown(KEY_EQUAL)) { + return ATN_EQUALS; + } else if (IsKeyDown(KEY_V)) { + return ATN_SELL; + } else if (IsKeyDown(KEY_B)) { + return ATN_BUY; + } + return ATN_NOOP; +} + +void process_fixed_input(Client* client) { + float move_speed = 20 / client->camera.zoom; + float zoom_delta = 0.05; + float zoom = client->camera.zoom; + if (shift_key()) { + move_speed *= 2; + zoom_delta *= 2; + } + if (down_key()) { + client->camera.target.y += move_speed; + } + if (up_key()) { + client->camera.target.y -= move_speed; + } + if (left_key()) { + client->camera.target.x -= move_speed; + } + if (right_key()) { + client->camera.target.x += move_speed; + } + if ((IsKeyDown(KEY_EQUAL) || IsKeyDown(KEY_E)) && zoom < 8.0) { + client->camera.zoom *= (1 + zoom_delta); + } + if ((IsKeyDown(KEY_MINUS) || IsKeyDown(KEY_Q)) && zoom > 1.0/32.0) { + client->camera.zoom *= (1 - zoom_delta); + } +} + +void render_fixed(Client* client, MMO* env, float delta) { + // Draw tilemap + float y = client->camera.target.y; + float x = client->camera.target.x; + float zoom = client->camera.zoom; + + BeginMode2D(client->camera); + + int X = GetRenderWidth(); + int Y = GetRenderHeight(); + client->camera.offset.x = X/2; + client->camera.offset.y = Y/2; + + int start_r = (y - Y/2/zoom) / TILE_SIZE; + if (start_r < 0) { + start_r = 0; + } + + int start_c = (x - X/2/zoom) / TILE_SIZE; + if (start_c < 0) { + start_c = 0; + } + + int end_r = (y + Y/2/zoom) / TILE_SIZE + 1; + if (end_r > env->height) { + end_r = env->height; + } + + int end_c = (x + X/2/zoom) / TILE_SIZE + 1; + if (end_c > env->width) { + end_c = env->width; + } + + /* + if client.active_overlay is None: + overlay = None + elif client.active_overlay == 'counts': + overlay = client.overlays.counts + overlay = smooth_cyan(overlay) + overlay = overlay[start_r:end_r, start_c:end_c] + elif client.active_overlay == 'value': + overlay = client.overlays.value_function + overlay = clip_rgb(overlay) + overlay = overlay[start_r:end_r, start_c:end_c] + */ + + draw_min(client, env, start_c, start_r, + end_c-start_c, end_r-start_r, env->width, env->height, 1, delta); + + for (int pid = 0; pid < env->num_players+env->num_enemies; pid++) { + draw_entity(client, env, pid, delta); + } + + EndMode2D(); +} + +// Did not finish porting console from Cython +void process_command_input(Client* client, MMO* env) { + int key = GetCharPressed(); + while (key > 0) { + if (key >= 32 && key <= 125 && client->command_len < COMMAND_CHARS) { + client->command[client->command_len] = key; + client->command_len += 1; + } + key = GetCharPressed(); + } + if (IsKeyPressed(KEY_BACKSPACE)) { + client->command_len = client->command_len - 1; + } + if (IsKeyPressed(KEY_ENTER)) { + char* command = client->command; + client->command_len = 0; + + if (client->command_len == 5 && strncmp(command, "help", 5) == 0) { + //client->command = COMMAND_HELP; + } else { + client->command_mode = false; + } + + if (client->command_len == 11 && strncmp(command, "overlay env", 11) == 0) { + client->active_overlay = OVERLAY_NONE; + } else if (client->command_len == 14 && strncmp(command, "overlay counts", 14) == 0) { + client->active_overlay = OVERLAY_COUNTS; + //arr = smooth_cyan(client->overlays.counts); + //Image.fromarray(arr).save('overlays/counts.png'); + //client->overlay_texture = rl.LoadTexture('overlays/counts.png'.encode()); + } else if (client->command_len == 13 && strncmp(command, "overlay value", 13) == 0) { + client->active_overlay = OVERLAY_VALUE; + //arr = clip_rgb(client->overlays.value_function); + //Image.fromarray(arr).save('overlays/values.png'); + //client->overlay_texture = rl.LoadTexture('overlays/values.png'.encode()); + } else if (client->command_len == 4 && strncmp(command, "play", 4) == 0) { + client->my_player = 0; + client->render_mode = RENDER_MODE_CENTERED; + } else if (client->command_len >= 9 && strncmp(command, "follow ", 7) == 0) { + /* + char* pid = command + 7; + pid = pid; + int pid = atoi(pid); + if (pid < 0 || pid > env->num_players) { + client->command = "Invalid player id"; + } + client->my_player = pid; + client->render_mode = RENDER_MODE_CENTERED; + */ + } + } + + Color term_color = {255, 255, 255, 200}; + DrawRectangle(0, 0, SCREEN_WIDTH, 32, term_color); + client->command[client->command_len] = '\0'; + const char* text = TextFormat("> %s", client->command); + DrawText(text, 10, 10, 20, BLACK); +} + +int tick(Client* client, MMO* env, float delta) { + BeginDrawing(); + ClearBackground(BLANK); + int action = 0; + + if (IsKeyDown(KEY_ESCAPE)) { + CloseWindow(); + exit(0); + } + if (IsKeyPressed(KEY_TAB)) { + ToggleBorderlessWindowed(); + if (client->render_mode == RENDER_MODE_CENTERED) { + client->render_mode = RENDER_MODE_FIXED; + } else { + client->render_mode = RENDER_MODE_CENTERED; + } + } + if (IsKeyPressed(KEY_GRAVE)) { // tilde + client->command_mode = !client->command_mode; + GetCharPressed(); // clear tilde key + } + if (client->render_mode == RENDER_MODE_FIXED) { + if (!client->command_mode) { + process_fixed_input(client); + } + render_fixed(client, env, delta); + } else { + if (!client->command_mode) { + action = process_centered_input(); + } + render_centered(client, env, client->my_player, action, delta); + } + if (client->command_mode) { + process_command_input(client, env); + } + + if (IsKeyDown(KEY_H)) { + DrawTextEx(client->font, TextFormat("FPS: %d", GetFPS()), + (Vector2){16, 16}, 24, 4, YELLOW); + } + + EndDrawing(); + return action; +} + + diff --git a/pufferlib/ocean/nmmo3/nmmo3.py b/pufferlib/ocean/nmmo3/nmmo3.py new file mode 100644 index 00000000..3648e4df --- /dev/null +++ b/pufferlib/ocean/nmmo3/nmmo3.py @@ -0,0 +1,273 @@ +from pdb import set_trace as T +import numpy as np +from types import SimpleNamespace +import gymnasium +import pettingzoo +import time + +from pufferlib.ocean.nmmo3.cy_nmmo3 import Environment, entity_dtype, reward_dtype + +import pufferlib + +class NMMO3(pufferlib.PufferEnv): + def __init__(self, width=4*[512], height=4*[512], num_envs=4, + num_players=1024, num_enemies=2048, num_resources=2048, + num_weapons=1024, num_gems=512, tiers=5, levels=40, + teleportitis_prob=0.001, enemy_respawn_ticks=2, + item_respawn_ticks=100, x_window=7, y_window=5, + reward_combat_level=1.0, reward_prof_level=1.0, + reward_item_level=0.5, reward_market=0.01, + reward_death=-1.0, buf=None): + if not isinstance(width, list): + width = num_envs * [width] + if not isinstance(height, list): + height = num_envs * [height] + if not isinstance(num_players, list): + num_players = num_envs * [num_players] + if not isinstance(num_enemies, list): + num_enemies = num_envs * [num_enemies] + if not isinstance(num_resources, list): + num_resources = num_envs * [num_resources] + if not isinstance(num_weapons, list): + num_weapons = num_envs * [num_weapons] + if not isinstance(num_gems, list): + num_gems = num_envs * [num_gems] + if not isinstance(tiers, list): + tiers = num_envs * [tiers] + if not isinstance(levels, list): + levels = num_envs * [levels] + if not isinstance(teleportitis_prob, list): + teleportitis_prob = num_envs * [teleportitis_prob] + if not isinstance(enemy_respawn_ticks, list): + enemy_respawn_ticks = num_envs * [enemy_respawn_ticks] + if not isinstance(item_respawn_ticks, list): + item_respawn_ticks = num_envs * [item_respawn_ticks] + + assert isinstance(width, list) + assert isinstance(height, list) + assert isinstance(num_players, list) + assert isinstance(num_enemies, list) + assert isinstance(num_resources, list) + assert isinstance(num_weapons, list) + assert isinstance(num_gems, list) + assert isinstance(tiers, list) + assert isinstance(levels, list) + assert isinstance(teleportitis_prob, list) + assert isinstance(enemy_respawn_ticks, list) + assert isinstance(item_respawn_ticks, list) + assert isinstance(x_window, int) + assert isinstance(y_window, int) + + assert len(width) == num_envs + assert len(height) == num_envs + assert len(num_players) == num_envs + assert len(num_enemies) == num_envs + assert len(num_resources) == num_envs + assert len(num_weapons) == num_envs + assert len(num_gems) == num_envs + assert len(tiers) == num_envs + assert len(levels) == num_envs + assert len(teleportitis_prob) == num_envs + assert len(enemy_respawn_ticks) == num_envs + assert len(item_respawn_ticks) == num_envs + + total_players = 0 + total_enemies = 0 + for idx in range(num_envs): + assert isinstance(width[idx], int) + assert isinstance(height[idx], int) + + if num_players[idx] is None: + num_players[idx] = width[idx] * height[idx] // 2048 + if num_enemies[idx] is None: + num_enemies[idx] = width[idx] * height[idx] // 512 + if num_resources[idx] is None: + num_resources[idx] = width[idx] * height[idx] // 1024 + if num_weapons[idx] is None: + num_weapons[idx] = width[idx] * height[idx] // 2048 + if num_gems[idx] is None: + num_gems[idx] = width[idx] * height[idx] // 4096 + if tiers[idx] is None: + if height[idx] <= 128: + tiers[idx] = 1 + elif height[idx] <= 256: + tiers[idx] = 2 + elif height[idx] <= 512: + tiers[idx] = 3 + elif height[idx] <= 1024: + tiers[idx] = 4 + else: + tiers[idx] = 5 + if levels[idx] is None: + if height[idx] <= 128: + levels[idx] = 7 + elif height[idx] <= 256: + levels[idx] = 15 + elif height[idx] <= 512: + levels[idx] = 31 + elif height[idx] <= 1024: + levels[idx] = 63 + else: + levels[idx] = 99 + + total_players += num_players[idx] + total_enemies += num_enemies[idx] + + self.players_flat = np.zeros((total_players, 51+501+3), dtype=np.intc) + self.enemies_flat = np.zeros((total_enemies, 51+501+3), dtype=np.intc) + self.rewards_flat = np.zeros((total_players, 10), dtype=np.float32) + #map_obs = np.zeros((total_players, 11*15 + 47 + 10), dtype=np.intc) + #counts = np.zeros((num_envs, height, width), dtype=np.uint8) + #terrain = np.zeros((num_envs, height, width), dtype=np.uint8) + #rendered = np.zeros((num_envs, height, width, 3), dtype=np.uint8) + actions = np.zeros((total_players), dtype=np.intc) + self.actions = actions + + self.num_agents = total_players + self.num_players = total_players + self.num_enemies = total_enemies + + self.players = np.frombuffer(self.players_flat, + dtype=entity_dtype()).view(np.recarray) + self.enemies = np.frombuffer(self.enemies_flat, + dtype=entity_dtype()).view(np.recarray) + self.struct_rewards = np.frombuffer(self.rewards_flat, + dtype=reward_dtype()).view(np.recarray) + + self.comb_goal_mask = np.array([1, 0, 1, 0, 1, 1, 0, 1, 1, 1]) + self.prof_goal_mask = np.array([0, 0, 0, 1, 0, 0, 1, 1, 1, 1]) + self.tick = 0 + + self.single_observation_space = gymnasium.spaces.Box(low=-1, + high=2**32-1, shape=(11*15*10+47+10,), dtype=np.uint8) + self.single_action_space = gymnasium.spaces.Discrete(26) + self.render_mode = 'human' + + super().__init__(buf) + self.c_env = Environment(self.observations, self.players_flat, + self.enemies_flat, self.rewards_flat, self.actions, + width, height, num_envs, num_players, num_enemies, + num_resources, num_weapons, num_gems, tiers, levels, + teleportitis_prob, enemy_respawn_ticks, item_respawn_ticks, + reward_combat_level, reward_prof_level, reward_item_level, + reward_market, reward_death, x_window, y_window) + + def reset(self, seed=None): + self.struct_rewards.fill(0) + self.rewards.fill(0) + self.is_reset = True + self.c_env.reset() + return self.observations, [] + + def step(self, actions): + if not hasattr(self, 'is_reset'): + raise Exception('Must call reset before step') + self.rewards.fill(0) + rewards = self.struct_rewards + rewards.fill(0) + self.actions[:] = actions[:] + self.c_env.step() + + rewards = rewards.total + infos = [] + if self.tick % 128 == 0: + log = self.c_env.log() + if log['episode_length'] > 0: + infos.append(log) + + ''' + print( + f'Comb lvl: {np.mean(self.players.comb_lvl)} (max {np.max(self.players.comb_lvl)})', + f'Prof lvl: {np.mean(self.players.prof_lvl)} (max {np.max(self.players.prof_lvl)})', + f'Time alive: {np.mean(self.players.time_alive)} (max {np.max(self.players.time_alive)})', + ) + ''' + + if False and self.tick % 128 == 0: + # TODO: Log images to Wandb in latest version + infos['nmmo3_map'] = self.render() + + self.tick += 1 + + self.rewards[:] = rewards.ravel() + + return self.observations, self.rewards, self.terminals, self.truncations, infos + + def render(self): + self.c_env.render() + #all_maps = [e.rendered.astype(np.float32) for e in self.c_env.envs] + #all_counts = [e.counts.astype(np.float32) for e in self.c_env.envs] + + ''' + agg_maps = np.zeros((2048, 2048, 3), dtype=np.float32) + agg_counts = np.zeros((2048, 2048), dtype=np.float32) + + agg_maps[256:512, :1024] = all_maps[0] + agg_counts[256:512, :1024] = all_counts[0] + + agg_maps[512:1024, :1024] = all_maps[1] + agg_counts[512:1024, :1024] = all_counts[1] + + agg_maps[1024:2048, :1024] = all_maps[2] + agg_counts[1024:2048, :1024] = all_counts[2] + + agg_maps[:, 1024:] = all_maps[3] + agg_counts[:, 1024:] = all_counts[3] + + agg_maps = all_maps[0] + agg_counts = all_counts[0] + + map = agg_maps + counts = agg_counts.astype(np.float32)/255 + + # Lerp rendered with counts + #counts = self.c_env.counts.astype(np.float32)/255 + counts = np.clip(25*counts, 0, 1)[:, :, None] + + lerped = map * (1 - counts) + counts * np.array([0, 255, 255]) + + num_players = self.num_players + r = self.players.r + c = self.players.c + lerped[r[:num_players], c[:num_players]] = np.array([0, 0, 0]) + + num_enemies = self.num_enemies + r = self.enemies.r + c = self.enemies.c + lerped[r[:num_enemies], c[:num_enemies]] = np.array([255, 0, 0]) + + lerped = lerped[::2, ::2] + + return lerped.astype(np.uint8) + ''' + + def close(self): + self.c_envs.close() + +class Overlays: + def __init__(self, width, height): + self.counts = np.zeros((width, height), dtype=int) + self.value_function = np.zeros((width, height), dtype=np.float32) + +def test_env_performance(env, timeout=10): + num_agents = env.num_players + + actions = {t: + {agent: np.random.randint(0, 6) for agent in range(1, num_agents+1)} + for t in range(100) + } + actions = {t: np.random.randint(0, 6, num_agents) for t in range(100)} + idx = 0 + + import time + start = time.time() + num_steps = 0 + while time.time() - start < timeout: + env.step(actions[num_steps % 100]) + num_steps += 1 + + end = time.time() + fps = num_agents * num_steps / (end - start) + print(f"Test Environment Performance FPS: {fps:.2f}") + + diff --git a/pufferlib/ocean/nmmo3/simplex.h b/pufferlib/ocean/nmmo3/simplex.h new file mode 100644 index 00000000..07b3e7db --- /dev/null +++ b/pufferlib/ocean/nmmo3/simplex.h @@ -0,0 +1,148 @@ +// Original work (noise library) Copyright (c) 2008 Casey Duncan +// Modified work (vec_noise library) Copyright (c) 2017 Zev Benjamin +// Single-file C port (this file) Copyright (c) 2024 Joseph Suarez +// Distributed under the MIT license. This is a simple copy-paste job. +// I did this because the original code mixed Python bindings into the +// C source, so there wasn't a clean way to use it as a C standalone. + +#include +const float GRAD3[][3] = { + {1,1,0},{-1,1,0},{1,-1,0},{-1,-1,0}, + {1,0,1},{-1,0,1},{1,0,-1},{-1,0,-1}, + {0,1,1},{0,-1,1},{0,1,-1},{0,-1,-1}, + {1,0,-1},{-1,0,-1},{0,-1,1},{0,1,1}}; + +const float GRAD4[][4] = { + {0,1,1,1}, {0,1,1,-1}, {0,1,-1,1}, {0,1,-1,-1}, + {0,-1,1,1}, {0,-1,1,-1}, {0,-1,-1,1}, {0,-1,-1,-1}, + {1,0,1,1}, {1,0,1,-1}, {1,0,-1,1}, {1,0,-1,-1}, + {-1,0,1,1}, {-1,0,1,-1}, {-1,0,-1,1}, {-1,0,-1,-1}, + {1,1,0,1}, {1,1,0,-1}, {1,-1,0,1}, {1,-1,0,-1}, + {-1,1,0,1}, {-1,1,0,-1}, {-1,-1,0,1}, {-1,-1,0,-1}, + {1,1,1,0}, {1,1,-1,0}, {1,-1,1,0}, {1,-1,-1,0}, + {-1,1,1,0}, {-1,1,-1,0}, {-1,-1,1,0}, {-1,-1,-1,0}}; + +// At the possible cost of unaligned access, we use char instead of +// int here to try to ensure that this table fits in L1 cache +const unsigned char PERM[] = { + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, + 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, + 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, + 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, + 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, + 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, + 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, + 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, + 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, + 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, + 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, + 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, + 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, + 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, + 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, + 141, 128, 195, 78, 66, 215, 61, 156, 180, 151, 160, 137, 91, 90, 15, 131, + 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, + 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, + 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, + 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, + 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, + 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, + 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, + 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, + 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, + 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, + 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, + 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, + 162, 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, + 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, + 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, + 180}; + +const unsigned char SIMPLEX[][4] = { + {0,1,2,3},{0,1,3,2},{0,0,0,0},{0,2,3,1},{0,0,0,0},{0,0,0,0},{0,0,0,0}, + {1,2,3,0},{0,2,1,3},{0,0,0,0},{0,3,1,2},{0,3,2,1},{0,0,0,0},{0,0,0,0}, + {0,0,0,0},{1,3,2,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0}, + {0,0,0,0},{0,0,0,0},{0,0,0,0},{1,2,0,3},{0,0,0,0},{1,3,0,2},{0,0,0,0}, + {0,0,0,0},{0,0,0,0},{2,3,0,1},{2,3,1,0},{1,0,2,3},{1,0,3,2},{0,0,0,0}, + {0,0,0,0},{0,0,0,0},{2,0,3,1},{0,0,0,0},{2,1,3,0},{0,0,0,0},{0,0,0,0}, + {0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{0,0,0,0},{2,0,1,3}, + {0,0,0,0},{0,0,0,0},{0,0,0,0},{3,0,1,2},{3,0,2,1},{0,0,0,0},{3,1,2,0}, + {2,1,0,3},{0,0,0,0},{0,0,0,0},{0,0,0,0},{3,1,0,2},{0,0,0,0},{3,2,0,1}, + {3,2,1,0}}; + +#define fastfloor(n) (int)(n) - (((n) < 0.0f) & ((n) != (int)(n))) + +// Fast sine/cosine functions from +// http://devmaster.net/forums/topic/4648-fast-and-accurate-sinecosine/page__st__80 +// Note the input to these functions is not radians +// instead x = [0, 2] for r = [0, 2*PI] + +inline float fast_sin(float x) +{ + // Convert the input value to a range of -1 to 1 + // x = x * (1.0f / PI); + + // Wrap around + volatile float z = (x + 25165824.0f); + x = x - (z - 25165824.0f); + + #if LOW_SINE_PRECISION + return 4.0f * (x - x * fabsf(x)); + #else + { + float y = x - x * fabsf(x); + const float Q = 3.1f; + const float P = 3.6f; + return y * (Q + P * fabsf(y)); + } + #endif +} + +inline float fast_cos(float x) +{ + return fast_sin(x + 0.5f); +} + +// 2D simplex skew factors +#define F2 0.3660254037844386f // 0.5 * (sqrt(3.0) - 1.0) +#define G2 0.21132486540518713f // (3.0 - sqrt(3.0)) / 6.0 + +float +noise2(float x, float y) +{ + int i1, j1, II, JJ, c; + float s = (x + y) * F2; + float i = floorf(x + s); + float j = floorf(y + s); + float t = (i + j) * G2; + + float xx[3], yy[3], f[3]; + float noise[3] = {0.0f, 0.0f, 0.0f}; + int g[3]; + + xx[0] = x - (i - t); + yy[0] = y - (j - t); + + i1 = xx[0] > yy[0]; + j1 = xx[0] <= yy[0]; + + xx[2] = xx[0] + G2 * 2.0f - 1.0f; + yy[2] = yy[0] + G2 * 2.0f - 1.0f; + xx[1] = xx[0] - i1 + G2; + yy[1] = yy[0] - j1 + G2; + + II = (int) i & 255; + JJ = (int) j & 255; + g[0] = PERM[II + PERM[JJ]] % 12; + g[1] = PERM[II + i1 + PERM[JJ + j1]] % 12; + g[2] = PERM[II + 1 + PERM[JJ + 1]] % 12; + + for (c = 0; c <= 2; c++) + f[c] = 0.5f - xx[c]*xx[c] - yy[c]*yy[c]; + + for (c = 0; c <= 2; c++) + if (f[c] > 0) + noise[c] = f[c]*f[c]*f[c]*f[c] * (GRAD3[g[c]][0]*xx[c] + GRAD3[g[c]][1]*yy[c]); + + return (noise[0] + noise[1] + noise[2]) * 70.0f; +} diff --git a/pufferlib/ocean/nmmo3/tile_atlas.h b/pufferlib/ocean/nmmo3/tile_atlas.h new file mode 100644 index 00000000..3e86b74b --- /dev/null +++ b/pufferlib/ocean/nmmo3/tile_atlas.h @@ -0,0 +1,3 @@ +int lerps[10000] = {0, 0, 4, 14, 34, 69, 125, 209, 329, 494, 1, 5, 15, 35, 70, 126, 210, 330, 495, 0, 6, 16, 36, 71, 127, 211, 331, 496, 0, 0, 17, 37, 72, 128, 212, 332, 497, 0, 0, 0, 38, 73, 129, 213, 333, 498, 0, 0, 0, 0, 74, 130, 214, 334, 499, 0, 0, 0, 0, 0, 131, 215, 335, 500, 0, 0, 0, 0, 0, 0, 216, 336, 501, 0, 0, 0, 0, 0, 0, 0, 337, 502, 0, 0, 0, 0, 0, 0, 0, 0, 503, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 18, 39, 75, 132, 217, 338, 504, 0, 8, 19, 40, 76, 133, 218, 339, 505, 0, 0, 20, 41, 77, 134, 219, 340, 506, 0, 0, 0, 42, 78, 135, 220, 341, 507, 0, 0, 0, 0, 79, 136, 221, 342, 508, 0, 0, 0, 0, 0, 137, 222, 343, 509, 0, 0, 0, 0, 0, 0, 223, 344, 510, 0, 0, 0, 0, 0, 0, 0, 345, 511, 0, 0, 0, 0, 0, 0, 0, 0, 512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 21, 43, 80, 138, 224, 346, 513, 0, 0, 22, 44, 81, 139, 225, 347, 514, 0, 0, 0, 45, 82, 140, 226, 348, 515, 0, 0, 0, 0, 83, 141, 227, 349, 516, 0, 0, 0, 0, 0, 142, 228, 350, 517, 0, 0, 0, 0, 0, 0, 229, 351, 518, 0, 0, 0, 0, 0, 0, 0, 352, 519, 0, 0, 0, 0, 0, 0, 0, 0, 520, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23, 46, 84, 143, 230, 353, 521, 0, 0, 0, 47, 85, 144, 231, 354, 522, 0, 0, 0, 0, 86, 145, 232, 355, 523, 0, 0, 0, 0, 0, 146, 233, 356, 524, 0, 0, 0, 0, 0, 0, 234, 357, 525, 0, 0, 0, 0, 0, 0, 0, 358, 526, 0, 0, 0, 0, 0, 0, 0, 0, 527, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 87, 147, 235, 359, 528, 0, 0, 0, 0, 88, 148, 236, 360, 529, 0, 0, 0, 0, 0, 149, 237, 361, 530, 0, 0, 0, 0, 0, 0, 238, 362, 531, 0, 0, 0, 0, 0, 0, 0, 363, 532, 0, 0, 0, 0, 0, 0, 0, 0, 533, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 89, 150, 239, 364, 534, 0, 0, 0, 0, 0, 151, 240, 365, 535, 0, 0, 0, 0, 0, 0, 241, 366, 536, 0, 0, 0, 0, 0, 0, 0, 367, 537, 0, 0, 0, 0, 0, 0, 0, 0, 538, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 152, 242, 368, 539, 0, 0, 0, 0, 0, 0, 243, 369, 540, 0, 0, 0, 0, 0, 0, 0, 370, 541, 0, 0, 0, 0, 0, 0, 0, 0, 542, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 244, 371, 543, 0, 0, 0, 0, 0, 0, 0, 372, 544, 0, 0, 0, 0, 0, 0, 0, 0, 545, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 373, 546, 0, 0, 0, 0, 0, 0, 0, 0, 547, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 548, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 10, 24, 49, 90, 153, 245, 374, 549, 0, 11, 25, 50, 91, 154, 246, 375, 550, 0, 0, 26, 51, 92, 155, 247, 376, 551, 0, 0, 0, 52, 93, 156, 248, 377, 552, 0, 0, 0, 0, 94, 157, 249, 378, 553, 0, 0, 0, 0, 0, 158, 250, 379, 554, 0, 0, 0, 0, 0, 0, 251, 380, 555, 0, 0, 0, 0, 0, 0, 0, 381, 556, 0, 0, 0, 0, 0, 0, 0, 0, 557, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 27, 53, 95, 159, 252, 382, 558, 0, 0, 28, 54, 96, 160, 253, 383, 559, 0, 0, 0, 55, 97, 161, 254, 384, 560, 0, 0, 0, 0, 98, 162, 255, 385, 561, 0, 0, 0, 0, 0, 163, 256, 386, 562, 0, 0, 0, 0, 0, 0, 257, 387, 563, 0, 0, 0, 0, 0, 0, 0, 388, 564, 0, 0, 0, 0, 0, 0, 0, 0, 565, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 56, 99, 164, 258, 389, 566, 0, 0, 0, 57, 100, 165, 259, 390, 567, 0, 0, 0, 0, 101, 166, 260, 391, 568, 0, 0, 0, 0, 0, 167, 261, 392, 569, 0, 0, 0, 0, 0, 0, 262, 393, 570, 0, 0, 0, 0, 0, 0, 0, 394, 571, 0, 0, 0, 0, 0, 0, 0, 0, 572, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, 102, 168, 263, 395, 573, 0, 0, 0, 0, 103, 169, 264, 396, 574, 0, 0, 0, 0, 0, 170, 265, 397, 575, 0, 0, 0, 0, 0, 0, 266, 398, 576, 0, 0, 0, 0, 0, 0, 0, 399, 577, 0, 0, 0, 0, 0, 0, 0, 0, 578, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 104, 171, 267, 400, 579, 0, 0, 0, 0, 0, 172, 268, 401, 580, 0, 0, 0, 0, 0, 0, 269, 402, 581, 0, 0, 0, 0, 0, 0, 0, 403, 582, 0, 0, 0, 0, 0, 0, 0, 0, 583, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 173, 270, 404, 584, 0, 0, 0, 0, 0, 0, 271, 405, 585, 0, 0, 0, 0, 0, 0, 0, 406, 586, 0, 0, 0, 0, 0, 0, 0, 0, 587, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 272, 407, 588, 0, 0, 0, 0, 0, 0, 0, 408, 589, 0, 0, 0, 0, 0, 0, 0, 0, 590, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 409, 591, 0, 0, 0, 0, 0, 0, 0, 0, 592, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 593, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 30, 59, 105, 174, 273, 410, 594, 0, 0, 31, 60, 106, 175, 274, 411, 595, 0, 0, 0, 61, 107, 176, 275, 412, 596, 0, 0, 0, 0, 108, 177, 276, 413, 597, 0, 0, 0, 0, 0, 178, 277, 414, 598, 0, 0, 0, 0, 0, 0, 278, 415, 599, 0, 0, 0, 0, 0, 0, 0, 416, 600, 0, 0, 0, 0, 0, 0, 0, 0, 601, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 62, 109, 179, 279, 417, 602, 0, 0, 0, 63, 110, 180, 280, 418, 603, 0, 0, 0, 0, 111, 181, 281, 419, 604, 0, 0, 0, 0, 0, 182, 282, 420, 605, 0, 0, 0, 0, 0, 0, 283, 421, 606, 0, 0, 0, 0, 0, 0, 0, 422, 607, 0, 0, 0, 0, 0, 0, 0, 0, 608, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 112, 183, 284, 423, 609, 0, 0, 0, 0, 113, 184, 285, 424, 610, 0, 0, 0, 0, 0, 185, 286, 425, 611, 0, 0, 0, 0, 0, 0, 287, 426, 612, 0, 0, 0, 0, 0, 0, 0, 427, 613, 0, 0, 0, 0, 0, 0, 0, 0, 614, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 114, 186, 288, 428, 615, 0, 0, 0, 0, 0, 187, 289, 429, 616, 0, 0, 0, 0, 0, 0, 290, 430, 617, 0, 0, 0, 0, 0, 0, 0, 431, 618, 0, 0, 0, 0, 0, 0, 0, 0, 619, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 188, 291, 432, 620, 0, 0, 0, 0, 0, 0, 292, 433, 621, 0, 0, 0, 0, 0, 0, 0, 434, 622, 0, 0, 0, 0, 0, 0, 0, 0, 623, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 293, 435, 624, 0, 0, 0, 0, 0, 0, 0, 436, 625, 0, 0, 0, 0, 0, 0, 0, 0, 626, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 437, 627, 0, 0, 0, 0, 0, 0, 0, 0, 628, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 629, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 33, 65, 115, 189, 294, 438, 630, 0, 0, 0, 66, 116, 190, 295, 439, 631, 0, 0, 0, 0, 117, 191, 296, 440, 632, 0, 0, 0, 0, 0, 192, 297, 441, 633, 0, 0, 0, 0, 0, 0, 298, 442, 634, 0, 0, 0, 0, 0, 0, 0, 443, 635, 0, 0, 0, 0, 0, 0, 0, 0, 636, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 67, 118, 193, 299, 444, 637, 0, 0, 0, 0, 119, 194, 300, 445, 638, 0, 0, 0, 0, 0, 195, 301, 446, 639, 0, 0, 0, 0, 0, 0, 302, 447, 640, 0, 0, 0, 0, 0, 0, 0, 448, 641, 0, 0, 0, 0, 0, 0, 0, 0, 642, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 120, 196, 303, 449, 643, 0, 0, 0, 0, 0, 197, 304, 450, 644, 0, 0, 0, 0, 0, 0, 305, 451, 645, 0, 0, 0, 0, 0, 0, 0, 452, 646, 0, 0, 0, 0, 0, 0, 0, 0, 647, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 198, 306, 453, 648, 0, 0, 0, 0, 0, 0, 307, 454, 649, 0, 0, 0, 0, 0, 0, 0, 455, 650, 0, 0, 0, 0, 0, 0, 0, 0, 651, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 308, 456, 652, 0, 0, 0, 0, 0, 0, 0, 457, 653, 0, 0, 0, 0, 0, 0, 0, 0, 654, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 458, 655, 0, 0, 0, 0, 0, 0, 0, 0, 656, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 657, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 121, 199, 309, 459, 658, 0, 0, 0, 0, 122, 200, 310, 460, 659, 0, 0, 0, 0, 0, 201, 311, 461, 660, 0, 0, 0, 0, 0, 0, 312, 462, 661, 0, 0, 0, 0, 0, 0, 0, 463, 662, 0, 0, 0, 0, 0, 0, 0, 0, 663, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 202, 313, 464, 664, 0, 0, 0, 0, 0, 203, 314, 465, 665, 0, 0, 0, 0, 0, 0, 315, 466, 666, 0, 0, 0, 0, 0, 0, 0, 467, 667, 0, 0, 0, 0, 0, 0, 0, 0, 668, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 204, 316, 468, 669, 0, 0, 0, 0, 0, 0, 317, 469, 670, 0, 0, 0, 0, 0, 0, 0, 470, 671, 0, 0, 0, 0, 0, 0, 0, 0, 672, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 318, 471, 673, 0, 0, 0, 0, 0, 0, 0, 472, 674, 0, 0, 0, 0, 0, 0, 0, 0, 675, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 473, 676, 0, 0, 0, 0, 0, 0, 0, 0, 677, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 678, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 124, 205, 319, 474, 679, 0, 0, 0, 0, 0, 206, 320, 475, 680, 0, 0, 0, 0, 0, 0, 321, 476, 681, 0, 0, 0, 0, 0, 0, 0, 477, 682, 0, 0, 0, 0, 0, 0, 0, 0, 683, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 207, 322, 478, 684, 0, 0, 0, 0, 0, 0, 323, 479, 685, 0, 0, 0, 0, 0, 0, 0, 480, 686, 0, 0, 0, 0, 0, 0, 0, 0, 687, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 324, 481, 688, 0, 0, 0, 0, 0, 0, 0, 482, 689, 0, 0, 0, 0, 0, 0, 0, 0, 690, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 483, 691, 0, 0, 0, 0, 0, 0, 0, 0, 692, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 693, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 208, 325, 484, 694, 0, 0, 0, 0, 0, 0, 326, 485, 695, 0, 0, 0, 0, 0, 0, 0, 486, 696, 0, 0, 0, 0, 0, 0, 0, 0, 697, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 327, 487, 698, 0, 0, 0, 0, 0, 0, 0, 488, 699, 0, 0, 0, 0, 0, 0, 0, 0, 700, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 489, 701, 0, 0, 0, 0, 0, 0, 0, 0, 702, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 703, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 328, 490, 704, 0, 0, 0, 0, 0, 0, 0, 491, 705, 0, 0, 0, 0, 0, 0, 0, 0, 706, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 492, 707, 0, 0, 0, 0, 0, 0, 0, 0, 708, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 709, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 493, 710, 0, 0, 0, 0, 0, 0, 0, 0, 711, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 712, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 713, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +char tile_atlas[256] = {-2, 9, 7, 7, 10, 7, 7, 7, 5, 5, 8, 8, 8, 8, 8, 8, 3, 6, 6, 6, 3, 6, 6, 6, -1, -1, -1, -1, -1, -1, -1, -1, 11, 5, 8, 8, 14, 8, 8, 8, 5, 5, 8, 8, 8, 8, 8, 8, 0, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2, -1, -1, 0, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2, -1, -1, 0, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 12, 13, 6, 6, 3, 6, 6, 6, 2, 2, -1, -1, -1, -1, -1, -1, 3, 6, 6, 6, 3, 6, 6, 6, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2, -1, -1, 0, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2, -1, -1, 0, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2, -1, -1, 0, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; \ No newline at end of file diff --git a/pufferlib/ocean/pong/cy_pong.pyx b/pufferlib/ocean/pong/cy_pong.pyx index c35ec913..75c652ee 100644 --- a/pufferlib/ocean/pong/cy_pong.pyx +++ b/pufferlib/ocean/pong/cy_pong.pyx @@ -123,7 +123,11 @@ cdef class CyPong: def render(self): cdef Pong* env = &self.envs[0] if self.client == NULL: + import os + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) self.client = make_client(env) + os.chdir(cwd) render(self.client, env) diff --git a/pufferlib/ocean/pong/pong.py b/pufferlib/ocean/pong/pong.py index 6ec04ecd..dc2685e0 100644 --- a/pufferlib/ocean/pong/pong.py +++ b/pufferlib/ocean/pong/pong.py @@ -11,6 +11,7 @@ import pufferlib from pufferlib.ocean.pong.cy_pong import CyPong + class Pong(pufferlib.PufferEnv): def __init__(self, num_envs=1, render_mode=None, width=500, height=640, paddle_width=20, paddle_height=70, @@ -18,8 +19,9 @@ def __init__(self, num_envs=1, render_mode=None, ball_initial_speed_x=10, ball_initial_speed_y=1, ball_speed_y_increment=3, ball_max_speed_y=13, max_score=21, frameskip=1, report_interval=1, buf=None): - self.single_observation_space = gymnasium.spaces.Box(low=0, high=1, - shape=(8,), dtype=np.float32) + self.single_observation_space = gymnasium.spaces.Box( + low=0, high=1, shape=(8,), dtype=np.float32, + ) self.single_action_space = gymnasium.spaces.Discrete(3) self.render_mode = render_mode self.num_agents = num_envs @@ -65,7 +67,7 @@ def test_performance(timeout=10, atn_cache=1024): env.reset() tick = 0 - actions = np.random.randint(0, 2, (atn_cache, env.num_envs)) + actions = np.random.randint(0, 2, (atn_cache, env.num_agents)) import time start = time.time() @@ -74,7 +76,8 @@ def test_performance(timeout=10, atn_cache=1024): env.step(atn) tick += 1 - print(f'SPS: %f', env.num_envs * tick / (time.time() - start)) + print(f'SPS: {env.num_agents * tick / (time.time() - start)}') + if __name__ == '__main__': test_performance() diff --git a/pufferlib/ocean/rware/cy_rware.pyx b/pufferlib/ocean/rware/cy_rware.pyx index 8c95fc5f..b796929a 100644 --- a/pufferlib/ocean/rware/cy_rware.pyx +++ b/pufferlib/ocean/rware/cy_rware.pyx @@ -106,7 +106,11 @@ cdef class CyRware: def render(self): cdef CRware* env = &self.envs[0] if self.client == NULL: + import os + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) self.client = make_client(env) + os.chdir(cwd) render(self.client, env) diff --git a/pufferlib/ocean/sanity.py b/pufferlib/ocean/sanity.py index 8036ce47..a90d1f09 100644 --- a/pufferlib/ocean/sanity.py +++ b/pufferlib/ocean/sanity.py @@ -370,7 +370,7 @@ def __init__(self): 'image': gymnasium.spaces.Box( low=0, high=1, shape=(5, 5), dtype=np.float32), 'flat': gymnasium.spaces.Box( - low=0, high=1, shape=(5,), dtype=np.int8), + low=-1, high=1, shape=(5,), dtype=np.int8), }) self.action_space = gymnasium.spaces.Dict({ 'image': gymnasium.spaces.Discrete(2), @@ -380,7 +380,7 @@ def __init__(self): def reset(self, seed=None): self.observation = { - 'image': np.random.randn(5, 5).astype(np.float32), + 'image': np.random.rand(5, 5).astype(np.float32), 'flat': np.random.randint(-1, 2, (5,), dtype=np.int8), } self.image_sign = np.sum(self.observation['image']) > 0 diff --git a/pufferlib/ocean/snake/snake.py b/pufferlib/ocean/snake/snake.py index 82a66aae..05f7d1cb 100644 --- a/pufferlib/ocean/snake/snake.py +++ b/pufferlib/ocean/snake/snake.py @@ -71,6 +71,9 @@ def step(self, actions): def render(self): self.c_envs.render(self.cell_size) + def close(self): + self.c_envs.close() + def test_performance(timeout=10, atn_cache=1024): env = Snake() env.reset() diff --git a/pufferlib/ocean/squared/pysquared.py b/pufferlib/ocean/squared/pysquared.py new file mode 100644 index 00000000..59c004f1 --- /dev/null +++ b/pufferlib/ocean/squared/pysquared.py @@ -0,0 +1,98 @@ +'''A simple sample environment. Use this as a template for your own envs.''' + +import gymnasium +import numpy as np + +import pufferlib +from pufferlib.ocean.squared.cy_squared import CySquared + +NOOP = 0 +DOWN = 1 +UP = 2 +LEFT = 3 +RIGHT = 4 + +EMPTY = 0 +AGENT = 1 +TARGET = 2 + +class PySquared(pufferlib.PufferEnv): + def __init__(self, num_envs=1, render_mode='ansi', size=11, buf=None): + self.single_observation_space = gymnasium.spaces.Box(low=0, high=1, + shape=(size*size,), dtype=np.uint8) + self.single_action_space = gymnasium.spaces.Discrete(5) + self.render_mode = render_mode + self.num_agents = 1 + + super().__init__(buf) + self.size = size + + def reset(self, seed=None): + self.observations[0, :] = EMPTY + self.observations[0, self.size*self.size//2] = AGENT + self.r = self.size//2 + self.c = self.size//2 + self.tick = 0 + while True: + target_r, target_c = np.random.randint(0, self.size, 2) + if target_r != self.r or target_c != self.c: + self.observations[0, target_r*self.size + target_c] = TARGET + break + + return self.observations, [] + + def step(self, actions): + atn = actions[0] + self.terminals[0] = False + self.rewards[0] = 0 + + self.observations[0, self.r*self.size + self.c] = EMPTY + + if atn == DOWN: + self.r += 1 + elif atn == RIGHT: + self.c += 1 + elif atn == UP: + self.r -= 1 + elif atn == LEFT: + self.c -= 1 + + info = [] + pos = self.r*self.size + self.c + if (self.tick > 3*self.size + or self.r < 0 + or self.c < 0 + or self.r >= self.size + or self.c >= self.size): + self.terminals[0] = True + self.rewards[0] = -1.0 + info = {'reward': -1.0} + self.reset() + elif self.observations[0, pos] == TARGET: + self.terminals[0] = True + self.rewards[0] = 1.0 + info = {'reward': 1.0} + self.reset() + else: + self.observations[0, pos] = AGENT + self.tick += 1 + + return self.observations, self.rewards, self.terminals, self.truncations, info + + def render(self): + chars = [] + grid = self.observations.reshape(self.size, self.size) + for row in grid: + for val in row: + if val == AGENT: + color = 94 + elif val == TARGET: + color = 91 + else: + color = 90 + chars.append(f'\033[{color}m██\033[0m') + chars.append('\n') + return ''.join(chars) + + def close(self): + pass diff --git a/pufferlib/ocean/tactical/c_tactical.pyx b/pufferlib/ocean/tactical/c_tactical.pyx index 762a0f57..bfdd6297 100644 --- a/pufferlib/ocean/tactical/c_tactical.pyx +++ b/pufferlib/ocean/tactical/c_tactical.pyx @@ -46,13 +46,10 @@ cdef class CTactical: def render(self): if self.renderer == NULL: import os - path = os.path.abspath(os.getcwd()) - print(path) - c_path = os.path.join(os.sep, *__file__.split('/')[:-1]) - print(c_path) - os.chdir(c_path) + cwd = os.getcwd() + os.chdir(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) self.renderer = init_game_renderer(self.env) - os.chdir(path) + os.chdir(cwd) return render_game(self.renderer, self.env) diff --git a/pufferlib/ocean/tactical/tactical.py b/pufferlib/ocean/tactical/tactical.py index e569c862..3ae3370d 100644 --- a/pufferlib/ocean/tactical/tactical.py +++ b/pufferlib/ocean/tactical/tactical.py @@ -1,11 +1,11 @@ import numpy as np import gymnasium import os -from raylib import rl -import heapq +#from raylib import rl +#import heapq import pufferlib -from pufferlib.ocean.tactical.c_tactical import CTactical, step_all +from pufferlib.ocean.tactical.c_tactical import CTactical # from pufferlib.environments.ocean import render EMPTY = 0 @@ -21,7 +21,7 @@ } -class PufferTactical: +class Tactical: def __init__(self, num_envs=200, render_mode='human'): self.num_envs = num_envs self.render_mode = render_mode @@ -78,7 +78,8 @@ def reset(self, seed=None): def step(self, actions): self.actions[:] = actions - step_all(self.c_envs) + for c_env in self.c_envs: + c_env.step() info = {} @@ -90,6 +91,11 @@ def render(self): # if self.render_mode == 'human': # return self.client.render(self.map) + def close(self): + for c_env in self.c_envs: + c_env.close() + +''' def a_star_search(map, start, goal): frontier = [] heapq.heappush(frontier, (0, start)) @@ -471,11 +477,12 @@ def render(self, map): rl.EndDrawing() return render.cdata_to_numpy() +''' if __name__ == '__main__': PROFILE = False - env = PufferTactical(num_envs=1, render_mode='human') + env = Tactical(num_envs=1, render_mode='human') env.reset() import time t0 = time.time() diff --git a/pufferlib/ocean/torch.py b/pufferlib/ocean/torch.py index 0c6e1c9b..e4d5c0cb 100644 --- a/pufferlib/ocean/torch.py +++ b/pufferlib/ocean/torch.py @@ -6,7 +6,72 @@ import pufferlib.models from pufferlib.models import Default as Policy +from pufferlib.models import Convolutional as Conv Recurrent = pufferlib.models.LSTMWrapper +import numpy as np + +class NMMO3LSTM(pufferlib.models.LSTMWrapper): + def __init__(self, env, policy, input_size=256, hidden_size=256, num_layers=1): + super().__init__(env, policy, input_size, hidden_size, num_layers) + +class NMMO3(nn.Module): + def __init__(self, env, hidden_size=256, output_size=256): + super().__init__() + #self.dtype = pufferlib.pytorch.nativize_dtype(env.emulated) + self.num_actions = env.single_action_space.n + self.factors = np.array([4, 4, 17, 5, 3, 5, 5, 5, 7, 4]) + self.offsets = torch.tensor([0] + list(np.cumsum(self.factors)[:-1])).cuda().view(1, -1, 1, 1) + self.cum_facs = np.cumsum(self.factors) + + self.multihot_dim = self.factors.sum() + + self.map_2d = nn.Sequential( + pufferlib.pytorch.layer_init(nn.Conv2d(self.multihot_dim, 64, 5, stride=3)), + nn.ReLU(), + pufferlib.pytorch.layer_init(nn.Conv2d(64, 64, 3, stride=1)), + nn.Flatten(), + ) + + self.player_discrete_encoder = nn.Sequential( + nn.Embedding(128, 32), + nn.Flatten(), + ) + + self.proj = nn.Sequential( + pufferlib.pytorch.layer_init(nn.Linear(1689, hidden_size)), + nn.ReLU(), + ) + + self.actor = pufferlib.pytorch.layer_init( + nn.Linear(output_size, self.num_actions), std=0.01) + self.value_fn = pufferlib.pytorch.layer_init(nn.Linear(output_size, 1), std=1) + + def forward(self, x): + hidden, lookup = self.encode_observations(x) + actions, value = self.decode_actions(hidden, lookup) + return actions, value + + def encode_observations(self, observations, unflatten=False): + batch = observations.shape[0] + ob_map = observations[:, :11*15*10].view(batch, 11, 15, 10) + ob_player = observations[:, 11*15*10:-10] + ob_reward = observations[:, -10:] + + map_buf = torch.zeros(batch, self.multihot_dim, 11, 15, device=ob_map.device, dtype=torch.float32) + codes = ob_map.permute(0, 3, 1, 2) + self.offsets + map_buf.scatter_(1, codes, 1) + ob_map = self.map_2d(map_buf) + + player_discrete = self.player_discrete_encoder(ob_player.int()) + + obs = torch.cat([ob_map, player_discrete, ob_player.float(), ob_reward], dim=1) + obs = self.proj(obs) + return obs, None + + def decode_actions(self, flat_hidden, lookup, concat=None): + action = self.actor(flat_hidden) + value = self.value_fn(flat_hidden) + return action, value class Snake(nn.Module): def __init__(self, env, cnn_channels=32, hidden_size=128, **kwargs): @@ -98,6 +163,65 @@ def decode_actions(self, flat_hidden, lookup, concat=None): action = self.actor(flat_hidden).split(3, dim=1) return action, value +class Go(nn.Module): + def __init__(self, env, cnn_channels=64, hidden_size=128, **kwargs): + super().__init__() + # 3 categories 2 boards. + # categories = player, opponent, empty + # boards = current, previous + self.cnn = nn.Sequential( + pufferlib.pytorch.layer_init( + nn.Conv2d(2, cnn_channels, 3, stride=1)), + nn.ReLU(), + pufferlib.pytorch.layer_init( + nn.Conv2d(cnn_channels, cnn_channels, 3, stride = 1)), + nn.Flatten(), + ) + + obs_size = env.single_observation_space.shape[0] + self.grid_size = int(np.sqrt((obs_size-2)/2)) + output_size = self.grid_size - 4 + cnn_flat_size = cnn_channels * output_size * output_size + + self.flat = pufferlib.pytorch.layer_init(nn.Linear(2,32)) + + self.proj = pufferlib.pytorch.layer_init(nn.Linear(cnn_flat_size + 32, hidden_size)) + + self.actor = pufferlib.pytorch.layer_init( + nn.Linear(hidden_size, env.single_action_space.n), std=0.01) + + self.value_fn = pufferlib.pytorch.layer_init( + nn.Linear(hidden_size, 1), std=1) + + def forward(self, observations): + hidden, lookup = self.encode_observations(observations) + actions, value = self.decode_actions(hidden, lookup) + return actions, value + + def encode_observations(self, observations): + grid_size = int(np.sqrt((observations.shape[1] - 2) / 2)) + full_board = grid_size * grid_size + black_board = observations[:, :full_board].view(-1,1, grid_size,grid_size).float() + white_board = observations[:, full_board:-2].view(-1,1, grid_size, grid_size).float() + board_features = torch.cat([black_board, white_board],dim=1) + flat_feature1 = observations[:, -2].unsqueeze(1).float() + flat_feature2 = observations[:, -1].unsqueeze(1).float() + # Pass board through cnn + cnn_features = self.cnn(board_features) + # Pass extra feature + flat_features = torch.cat([flat_feature1, flat_feature2],dim=1) + flat_features = self.flat(flat_features) + # pass all features + features = torch.cat([cnn_features, flat_features], dim=1) + features = F.relu(self.proj(features)) + + return features, None + + def decode_actions(self, flat_hidden, lookup, concat=None): + value = self.value_fn(flat_hidden) + action = self.actor(flat_hidden) + return action, value + class MOBA(nn.Module): def __init__(self, env, cnn_channels=128, hidden_size=128, **kwargs): super().__init__() @@ -172,3 +296,39 @@ def decode_actions(self, flat_hidden, lookup, concat=None): #print('argmax samples: ', argmax_samples) return action, value + +class TrashPickup(nn.Module): + def __init__(self, env, cnn_channels=32, hidden_size=128, **kwargs): + super().__init__() + self.agent_sight_range = env.agent_sight_range + self.network= nn.Sequential( + pufferlib.pytorch.layer_init( + nn.Conv2d(5, cnn_channels, 5, stride=3)), + nn.ReLU(), + pufferlib.pytorch.layer_init( + nn.Conv2d(cnn_channels, cnn_channels, 3, stride=1)), + nn.ReLU(), + nn.Flatten(), + pufferlib.pytorch.layer_init(nn.Linear(cnn_channels, hidden_size)), + nn.ReLU(), + ) + self.actor = pufferlib.pytorch.layer_init( + nn.Linear(hidden_size, env.single_action_space.n), std=0.01) + self.value_fn = pufferlib.pytorch.layer_init( + nn.Linear(hidden_size, 1), std=1) + + def forward(self, observations): + hidden, lookup = self.encode_observations(observations) + actions, value = self.decode_actions(hidden, lookup) + return actions, value + + def encode_observations(self, observations): + crop_size = 2 * self.agent_sight_range + 1 + observations = observations.view(-1, 5, crop_size, crop_size).float() + #observations = observations.view(-1, crop_size, crop_size, 5).permute(0, 3, 1, 2).float() + return self.network(observations), None + + def decode_actions(self, flat_hidden, lookup, concat=None): + action = self.actor(flat_hidden) + value = self.value_fn(flat_hidden) + return action, value diff --git a/pufferlib/ocean/trash_pickup/README.md b/pufferlib/ocean/trash_pickup/README.md new file mode 100644 index 00000000..670c1429 --- /dev/null +++ b/pufferlib/ocean/trash_pickup/README.md @@ -0,0 +1,18 @@ +# TrashPickup Environment + +A lightweight multi-agent reinforcement learning (RL) environment designed for coordination and cooperation research. Agents pick up trash and deposit it in bins for rewards. + +## Key Features +- **Multi-Agent Coordination:** Encourages teamwork, efficient planning, and resource allocation. +- **Configurable Setup:** Adjustable grid size, number of agents, trash, bins, and episode length. +- **Discrete Action Space:** Actions include `UP`, `DOWN`, `LEFT`, `RIGHT`. +- **Fast and Lightweight:** Optimized for rapid training and testing. + +## Example Research Goals +- Investigate emergent behaviors like task allocation and coordination. +- Study efficient resource collection and bin-pushing strategies. + +## Ideal For +- RL researchers exploring multi-agent cooperation. +- Students learning about multi-agent systems. +- Developers testing scalable RL algorithms. diff --git a/pufferlib/ocean/trash_pickup/cy_trash_pickup.pyx b/pufferlib/ocean/trash_pickup/cy_trash_pickup.pyx new file mode 100644 index 00000000..19d25f81 --- /dev/null +++ b/pufferlib/ocean/trash_pickup/cy_trash_pickup.pyx @@ -0,0 +1,109 @@ +cimport numpy as cnp +from libc.stdlib cimport calloc, free # Use calloc for zero-initialized allocation +from libc.stdint cimport uint64_t + +cdef extern from "trash_pickup.h": + int LOG_BUFFER_SIZE + + ctypedef struct Log: + float episode_return; + float episode_length; + float trash_collected; + + ctypedef struct LogBuffer + LogBuffer* allocate_logbuffer(int) + void free_logbuffer(LogBuffer*) + Log aggregate_and_clear(LogBuffer*) + + ctypedef struct CTrashPickupEnv: + char* observations + int* actions + float* rewards + unsigned char* dones + LogBuffer* log_buffer + + int grid_size + int num_agents + int num_trash + int num_bins + int max_steps + int agent_sight_range + + + ctypedef struct Client + + void initialize_env(CTrashPickupEnv* env) + void free_allocated(CTrashPickupEnv* env) + + Client* make_client(CTrashPickupEnv* env) + void close_client(Client* client) + void render(Client* client, CTrashPickupEnv* env) + + void reset(CTrashPickupEnv* env) + void step(CTrashPickupEnv* env) + +cdef class CyTrashPickup: + cdef: + CTrashPickupEnv* envs + Client* client + LogBuffer* logs + int num_envs + + def __init__(self, char[:, :] observations, int[:] actions, + float[:] rewards, unsigned char[:] terminals, int num_envs, + int num_agents=3, int grid_size=10, int num_trash=15, + int num_bins=2, int max_steps=300, int agent_sight_range=5): + self.num_envs = num_envs + self.envs = calloc(num_envs, sizeof(CTrashPickupEnv)) + if self.envs == NULL: + raise MemoryError("Failed to allocate memory for CTrashPickupEnv") + self.client = NULL + + self.logs = allocate_logbuffer(LOG_BUFFER_SIZE) + + cdef int inc = num_agents + + cdef int i + for i in range(num_envs): + self.envs[i] = CTrashPickupEnv( + observations=&observations[inc*i, 0], + actions=&actions[inc*i], + rewards=&rewards[inc*i], + dones=&terminals[inc*i], + log_buffer=self.logs, + grid_size=grid_size, + num_agents=num_agents, + num_trash=num_trash, + num_bins=num_bins, + max_steps=max_steps, + agent_sight_range=agent_sight_range + ) + initialize_env(&self.envs[i]) + + def reset(self): + cdef int i + for i in range(self.num_envs): + reset(&self.envs[i]) + + def step(self): + cdef int i + for i in range(self.num_envs): + step(&self.envs[i]) + + def render(self): + cdef CTrashPickupEnv* env = &self.envs[0] + if self.client == NULL: + self.client = make_client(env) + + render(self.client, env) + + def close(self): + if self.client != NULL: + close_client(self.client) + self.client = NULL + + free(self.envs) + + def log(self): + cdef Log log = aggregate_and_clear(self.logs) + return log diff --git a/pufferlib/ocean/trash_pickup/trash_pickup.c b/pufferlib/ocean/trash_pickup/trash_pickup.c new file mode 100644 index 00000000..b8021719 --- /dev/null +++ b/pufferlib/ocean/trash_pickup/trash_pickup.c @@ -0,0 +1,117 @@ +#include +#include "trash_pickup.h" +#include "puffernet.h" + +// Demo function for visualizing the TrashPickupEnv +void demo(int grid_size, int num_agents, int num_trash, int num_bins, int max_steps) { + CTrashPickupEnv env = { + .grid_size = grid_size, + .num_agents = num_agents, + .num_trash = num_trash, + .num_bins = num_bins, + .max_steps = max_steps, + .agent_sight_range = 5, + .do_human_control = true + }; + + bool use_pretrained_model = true; + + Weights* weights; + ConvLSTM* net; + + if (use_pretrained_model){ + weights = load_weights("resources/trash_pickup_weights.bin", 150245); + int vision = 2*env.agent_sight_range + 1; + net = make_convlstm(weights, env.num_agents, vision, 5, 32, 128, 4); + } + + allocate(&env); + Client* client = make_client(&env); + + reset(&env); + + int tick = 0; + while (!WindowShouldClose()) { + if (tick % 12 == 0) { + // Random actions for all agents + for (int i = 0; i < env.num_agents; i++) { + if (use_pretrained_model) + { + for (int e = 0; e < env.total_num_obs; e++) { + net->obs[e] = env.observations[e]; + } + forward_convlstm(net, net->obs, env.actions); + } + else{ + env.actions[i] = rand() % 4; // 0 = UP, 1 = DOWN, 2 = LEFT, 3 = RIGHT + } + // printf("action: %d \n", env.actions[i]); + } + + // Override human control actions + if (IsKeyDown(KEY_LEFT_SHIFT)) { + // Handle keyboard input only for selected agent + if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) { + env.actions[0] = ACTION_UP; + } + if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) { + env.actions[0] = ACTION_LEFT; + } + if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) { + env.actions[0] = ACTION_RIGHT; + } + if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) { env.actions[0] = ACTION_DOWN; } + } + + // Step the environment and render the grid + step(&env); + + } + tick++; + + render(client, &env); + } + + free_convlstm(net); + free(weights); + free_allocated(&env); + close_client(client); +} + +// Performance test function for benchmarking +void performance_test() { + long test_time = 10; // Test duration in seconds + + CTrashPickupEnv env = { + .grid_size = 10, + .num_agents = 4, + .num_trash = 20, + .num_bins = 1, + .max_steps = 150, + .agent_sight_range = 5 + }; + allocate(&env); + reset(&env); + + long start = time(NULL); + int i = 0; + int inc = env.num_agents; + while (time(NULL) - start < test_time) { + for (int e = 0; e < env.num_agents; e++) { + env.actions[e] = rand() % 4; + } + step(&env); + i += inc; + } + long end = time(NULL); + printf("SPS: %ld\n", i / (end - start)); + free_allocated(&env); +} + + +// Main entry point +int main() { + demo(10, 4, 20, 1, 150); // Visual demo + //performance_test(); // Uncomment for benchmarking + return 0; +} diff --git a/pufferlib/ocean/trash_pickup/trash_pickup.h b/pufferlib/ocean/trash_pickup/trash_pickup.h new file mode 100644 index 00000000..aa460cc7 --- /dev/null +++ b/pufferlib/ocean/trash_pickup/trash_pickup.h @@ -0,0 +1,603 @@ +#include +#include +#include +#include +#include +#include "raylib.h" + +#define EMPTY 0 +#define TRASH 1 +#define TRASH_BIN 2 +#define AGENT 3 + +#define ACTION_UP 0 +#define ACTION_DOWN 1 +#define ACTION_LEFT 2 +#define ACTION_RIGHT 3 + +#define LOG_BUFFER_SIZE 1024 + +typedef struct Log { + float episode_return; + float episode_length; + float trash_collected; +} Log; + +typedef struct LogBuffer { + Log* logs; + int length; + int idx; +} LogBuffer; + +// LogBuffer functions +LogBuffer* allocate_logbuffer(int size) { + LogBuffer* logs = (LogBuffer*)calloc(1, sizeof(LogBuffer)); + logs->logs = (Log*)calloc(size, sizeof(Log)); + logs->length = size; + logs->idx = 0; + return logs; +} + +void free_logbuffer(LogBuffer* buffer) { + free(buffer->logs); + free(buffer); +} + +Log aggregate_and_clear(LogBuffer* logs) { + Log log = {0}; + if (logs->idx == 0) { + return log; + } + for (int i = 0; i < logs->idx; i++) { + log.episode_return += logs->logs[i].episode_return; + log.episode_length += logs->logs[i].episode_length; + log.trash_collected += logs->logs[i].trash_collected; + } + log.episode_return /= logs->idx; + log.episode_length /= logs->idx; + log.trash_collected /= logs->idx; + logs->idx = 0; + return log; +} + +void add_log(LogBuffer* logs, Log* log) { + if (logs->idx == logs->length) { + return; + } + logs->logs[logs->idx] = *log; + logs->idx += 1; +} + +typedef struct { + int type; // Entity type: EMPTY, TRASH, TRASH_BIN, AGENT + int pos_x; + int pos_y; + bool presence; // Whether or not Entity is present (not applicable to all types) + bool carrying; // Whether agent is carrying trash (only applicable to Agent types) +} Entity; + +typedef struct { + Entity* entity; + int index; // Index in the positions array (-1 if not applicable) +} GridCell; + +typedef struct { + // Interface for PufferLib + char* observations; + int* actions; + float* rewards; + unsigned char* dones; + LogBuffer* log_buffer; + + int grid_size; + int num_agents; + int num_trash; + int num_bins; + int max_steps; + int current_step; + + int total_num_obs; + + int agent_sight_range; + + float positive_reward; + float negative_reward; + float total_episode_reward; + + GridCell* grid; // 1D array for grid + Entity* entities; // Indicies (0 - num_agents) for agents, (num_agents - num_bins) for bins, (num_bins - num_trash) for trash. + + bool do_human_control; +} CTrashPickupEnv; + +int get_grid_index(CTrashPickupEnv* env, int x, int y) { + return (y * env->grid_size) + x; +} + +// returns the start index of each type of entity for iteration purposes +int get_entity_type_start_index(CTrashPickupEnv* env, int type) +{ + if (type == AGENT) + return 0; + else if (type == TRASH_BIN) + return env->num_agents; + else if (type == TRASH) + return env->num_agents + env->num_bins; + else + return -1; +} + +// Entity Attribute Based Obs-Space +/* +void compute_observations(CTrashPickupEnv* env) { + float* obs = env->observations; + float norm_factor = 1.0f / env->grid_size; + + int obs_index = 0; + + for (int agent_idx = 0; agent_idx < env->num_agents; agent_idx++){ + float current_norm_pos_x = (float) (env->entities[agent_idx].pos_x) * norm_factor; + float current_norm_pos_y = (float) (env->entities[agent_idx].pos_y) * norm_factor; + + // Add the observing agent's own position and carrying status + obs[obs_index++] = current_norm_pos_x; + obs[obs_index++] = current_norm_pos_y; + obs[obs_index++] = env->entities[agent_idx].carrying ? 1.0f : 0.0f; + + // Add other observations from other entities (other agents, bins, trash) + for (int i = 0; i < env->num_agents + env->num_bins + env->num_trash; i++) { + // skip if current this agent + if (agent_idx == i) + continue; + + obs[obs_index++] = ((float) (env->entities[i].pos_x) * norm_factor) - current_norm_pos_x; + obs[obs_index++] = ((float) (env->entities[i].pos_y) * norm_factor) - current_norm_pos_y; + + if (env->entities[i].type == AGENT) { + obs[obs_index++] = env->entities[i].carrying ? 1.0f : 0.0f; + } + else if (env->entities[i].type == TRASH_BIN) { + obs[obs_index++] = env->entities[i].presence ? 1.0f : 0.0f; + } + } + } +} +*/ + +// Local crop version +void compute_observations(CTrashPickupEnv* env) { + int sight_range = env->agent_sight_range; + int num_cell_types = 4; // EMPTY, TRASH, BIN, AGENT + + char* obs = env->observations; + + int obs_dim = 2*env->agent_sight_range + 1; + int channel_offset = obs_dim*obs_dim; + memset(obs, 0, env->total_num_obs*sizeof(char)); + + for (int agent_idx = 0; agent_idx < env->num_agents; agent_idx++) { + // Add obs for whether the agent is carrying or not + //obs[obs_index++] = env->entities[agent_idx].carrying; + + // Get the agent's position + int agent_x = env->entities[agent_idx].pos_x; + int agent_y = env->entities[agent_idx].pos_y; + + // Iterate over the sight range + for (int dy = -sight_range; dy <= sight_range; dy++) { + for (int dx = -sight_range; dx <= sight_range; dx++) { + int cell_x = agent_x + dx; + int cell_y = agent_y + dy; + int obs_x = dx + env->agent_sight_range; + int obs_y = dy + env->agent_sight_range; + + // Check if the cell is within bounds + if (cell_x < 0 || cell_x >= env->grid_size || cell_y < 0 || cell_y >= env->grid_size) { + continue; + } + + Entity* thisEntity = env->grid[get_grid_index(env, cell_x, cell_y)].entity; + if (!thisEntity) { + continue; + } + + int offset = agent_idx*5*channel_offset + obs_y*obs_dim + obs_x; + int obs_idx = offset + thisEntity->type*channel_offset; + obs[obs_idx] = 1; + obs_idx = offset + 4*channel_offset; + obs[obs_idx] = (float)thisEntity->carrying; + } + } + } +} + +// Helper functions +void place_random_entities(CTrashPickupEnv* env, int count, int item_type, int gridIndexStart) { + int placed = 0; + while (placed < count) + { + int x = rand() % env->grid_size; + int y = rand() % env->grid_size; + + GridCell* gridCell = &env->grid[get_grid_index(env, x, y)]; + + if (gridCell->entity != NULL) + continue; + + // Allocate and initialize a new Entity + Entity* newEntity = &env->entities[gridIndexStart]; + newEntity->type = item_type; + newEntity->pos_x = x; + newEntity->pos_y = y; + newEntity->presence = true; + newEntity->carrying = false; + + gridCell->index = gridIndexStart; + gridCell->entity = newEntity; + + gridIndexStart++; + placed++; + } +} + +void add_reward(CTrashPickupEnv* env, int agent_idx, float reward){ + env->rewards[agent_idx] += reward; + env->total_episode_reward += reward; +} + +void move_agent(CTrashPickupEnv* env, int agent_idx, int action) { + Entity* thisAgent = &env->entities[agent_idx]; + + int move_dir_x = 0; + int move_dir_y = 0; + if (action == ACTION_UP) move_dir_y = -1; + else if (action == ACTION_DOWN) move_dir_y = 1; + else if (action == ACTION_LEFT) move_dir_x = -1; + else if (action == ACTION_RIGHT) move_dir_x = 1; + else printf("Undefined action: %d", action); + + int new_x = thisAgent->pos_x + move_dir_x; + int new_y = thisAgent->pos_y + move_dir_y; + + if (new_x < 0 || new_x >= env->grid_size || new_y < 0 || new_y >= env->grid_size) + return; + + GridCell* currentGridCell = &env->grid[get_grid_index(env, thisAgent->pos_x, thisAgent->pos_y)]; + GridCell* newGridCell = &env->grid[get_grid_index(env, new_x, new_y)]; + int cell_state_type = newGridCell->entity ? newGridCell->entity->type : EMPTY; + + if (cell_state_type == EMPTY) + { + thisAgent->pos_x = new_x; + thisAgent->pos_y = new_y; + + newGridCell->entity = currentGridCell->entity; + newGridCell->index = agent_idx; + + currentGridCell->index = -1; + currentGridCell->entity = NULL; + } + else if (cell_state_type == TRASH && thisAgent->carrying == false) + { + Entity* thisTrash = &env->entities[newGridCell->index]; + thisTrash->presence = false; // Mark as not present + thisTrash->pos_x = -1; + thisTrash->pos_y = -1; + + thisAgent->pos_x = new_x; + thisAgent->pos_y = new_y; + thisAgent->carrying = true; + + newGridCell->entity = currentGridCell->entity; + newGridCell->index = currentGridCell->index; + + currentGridCell->entity = NULL; + currentGridCell->index = -1; + + add_reward(env, agent_idx, env->positive_reward); + } + else if (cell_state_type == TRASH_BIN) + { + if (thisAgent->carrying) + { + // Deposit trash into bin + thisAgent->carrying = false; + add_reward(env, agent_idx, env->positive_reward); + } + else + { + int new_bin_x = new_x + move_dir_x; + int new_bin_y = new_y + move_dir_y; + + if (new_bin_x < 0 || new_bin_x >= env->grid_size || new_bin_y < 0 || new_bin_y >= env->grid_size) + return; + + GridCell* newGridCellForBin = &env->grid[get_grid_index(env, new_bin_x, new_bin_y)]; + if (newGridCellForBin->entity == NULL) { + // Move the bin + Entity* thisBin = newGridCell->entity; + thisBin->pos_x = new_bin_x; + thisBin->pos_y = new_bin_y; + + // Move the agent + thisAgent->pos_x = new_x; + thisAgent->pos_y = new_y; + + newGridCellForBin->entity = newGridCell->entity; + newGridCellForBin->index = newGridCell->index; + + newGridCell->entity = currentGridCell->entity; + newGridCell->index = currentGridCell->index; + + currentGridCell->entity = NULL; + currentGridCell->index = -1; + } + // else don't move the agent + } + } +} + +bool is_episode_over(CTrashPickupEnv* env) { + for (int i = 0; i < env->num_agents; i++) + { + if (env->entities[i].carrying) + return false; + } + + int start_index = get_entity_type_start_index(env, TRASH); + for (int i = start_index; i < start_index + env->num_trash; i++) + { + if (env->entities[i].presence) + return false; + } + + return true; +} + +void reset(CTrashPickupEnv* env) { + env->current_step = 0; + env->total_episode_reward = 0; + + for (int i = 0; i < env->grid_size * env->grid_size; i++) + { + env->grid[i].entity = NULL; + env->grid[i].index = -1; + } + + // Place trash, bins, and agents randomly across the grid. + place_random_entities(env, env->num_agents, AGENT, 0); + place_random_entities(env, env->num_bins, TRASH_BIN, get_entity_type_start_index(env, TRASH_BIN)); + place_random_entities(env, env->num_trash, TRASH, get_entity_type_start_index(env, TRASH)); + + compute_observations(env); +} + +// Environment functions +void initialize_env(CTrashPickupEnv* env) { + env->current_step = 0; + + env->positive_reward = 0.5f; // / env->num_trash; + env->negative_reward = -0.0f; // / (env->max_steps * env->num_agents); + + env->grid = (GridCell*)calloc(env->grid_size * env->grid_size, sizeof(GridCell)); + env->entities = (Entity*)calloc(env->num_agents + env->num_bins + env->num_trash, sizeof(Entity)); + env->total_num_obs = env->num_agents * ((((env->agent_sight_range * 2 + 1) * (env->agent_sight_range * 2 + 1)) * 5)); + + reset(env); +} + +void allocate(CTrashPickupEnv* env) { + + env->observations = (char*)calloc(env->total_num_obs, sizeof(char)); + env->actions = (int*)calloc(env->num_agents, sizeof(int)); + env->rewards = (float*)calloc(env->num_agents, sizeof(float)); + env->dones = (unsigned char*)calloc(env->num_agents, sizeof(unsigned char)); + env->log_buffer = allocate_logbuffer(LOG_BUFFER_SIZE); + + initialize_env(env); +} + +void step(CTrashPickupEnv* env) { + // Reset reward for each agent + memset(env->rewards, 0, sizeof(float) * env->num_agents); + memset(env->dones, 0, sizeof(unsigned char) * env->num_agents); + + for (int i = 0; i < env->num_agents; i++) { + move_agent(env, i, env->actions[i]); + add_reward(env, i, env->negative_reward); // small negative reward to encourage efficiency + } + + env->current_step++; + if (env->current_step >= env->max_steps || is_episode_over(env)) + { + memset(env->dones, 1, sizeof(unsigned char) * env->num_agents); + + Log log = {0}; + + log.episode_length = env->current_step; + log.episode_return = env->total_episode_reward; + + int total_trash_not_collected = 0; + for (int i = env->num_agents + 1; i < env->num_agents + env->num_trash; i++) + { + total_trash_not_collected += env->entities[i].presence; + } + + log.trash_collected = (float) (env->num_trash - total_trash_not_collected); + + add_log(env->log_buffer, &log); + + reset(env); + } + + compute_observations(env); +} + +void free_initialized(CTrashPickupEnv* env) { + free(env->grid); + free(env->entities); +} + +void free_allocated(CTrashPickupEnv* env) { + free(env->observations); + free(env->actions); + free(env->rewards); + free(env->dones); + free_logbuffer(env->log_buffer); + free_initialized(env); +} + +const Color PUFF_RED = (Color){187, 0, 0, 255}; +const Color PUFF_CYAN = (Color){0, 187, 187, 255}; +const Color PUFF_WHITE = (Color){241, 241, 241, 241}; +const Color PUFF_BACKGROUND = (Color){6, 24, 24, 255}; +const Color PUFF_LINES = (Color){50, 50, 50, 255}; + +typedef struct Client { + int window_width; + int window_height; + int header_offset; + int cell_size; + Texture2D agent_texture; +} Client; + +// Initialize a rendering client +Client* make_client(CTrashPickupEnv* env) { + const int CELL_SIZE = 40; + Client* client = (Client*)malloc(sizeof(Client)); + client->cell_size = CELL_SIZE; + client->header_offset = 60; + client->window_width = env->grid_size * CELL_SIZE; + client->window_height = client->window_width + client->header_offset; + + InitWindow(client->window_width, client->window_height, "Trash Pickup Environment"); + SetTargetFPS(60); + + client->agent_texture = LoadTexture("resources/puffers_128.png"); + + return client; +} + +// Render the TrashPickup environment +void render(Client* client, CTrashPickupEnv* env) { + BeginDrawing(); + ClearBackground(PUFF_BACKGROUND); + + // Draw header with current step and total episode reward + int start_index = get_entity_type_start_index(env, TRASH); + int total_trash_not_collected = 0; + for (int i = start_index; i < start_index + env->num_trash; i++){ + total_trash_not_collected += env->entities[i].presence; + } + + DrawText( + TextFormat( + "Step: %d\nTotal Episode Reward: %.2f\nTrash Collected: %d/%d", + env->current_step, + env->total_episode_reward, + env->num_trash - total_trash_not_collected, + env->num_trash + ), + 5, 2, 10, PUFF_WHITE + ); + + // Draw the grid and its elements + for (int x = 0; x < env->grid_size; x++) { + for (int y = 0; y < env->grid_size; y++) { + GridCell gridCell = env->grid[get_grid_index(env, x, y)]; + + int cell_type; + if (gridCell.entity) + { + cell_type = gridCell.entity->type; + } + else + { + cell_type = EMPTY; + } + + int screen_x = x * client->cell_size; + int screen_y = y * client->cell_size + client->header_offset; + + Rectangle cell_rect = { + .x = screen_x, + .y = screen_y, + .width = client->cell_size, + .height = client->cell_size + }; + + // Draw grid cell border + DrawRectangleLines((int)cell_rect.x, (int)cell_rect.y, (int)cell_rect.width, (int)cell_rect.height, PUFF_LINES); + + // Draw grid cell content + if (cell_type == EMPTY) + continue; + + if (cell_type == TRASH) { + DrawRectangle( + screen_x + client->cell_size / 4, + screen_y + client->cell_size / 4, + client->cell_size / 2, + client->cell_size / 2, + PUFF_CYAN + ); + } else if (cell_type == TRASH_BIN) { + DrawRectangle( + screen_x + client->cell_size / 8, + screen_y + client->cell_size / 8, + 3 * client->cell_size / 4, + 3 * client->cell_size / 4, + PUFF_RED + ); + } else if (cell_type == AGENT) { + Color color; + if (env->do_human_control && gridCell.index == 0) + { + // Make human controlled agent red + color = (Color){255, 128, 128, 255}; + } + else + { + // Non-human controlled agent + color = WHITE; + } + + DrawTexturePro( + client->agent_texture, + (Rectangle) {0, 0, 128, 128}, + (Rectangle) { + screen_x + client->cell_size / 2, + screen_y + client->cell_size / 2, + client->cell_size, + client->cell_size + }, + (Vector2){client->cell_size / 2, client->cell_size / 2}, + 0, + color + ); + + Entity* thisAgent = &env->entities[gridCell.index]; + + if (thisAgent->carrying) + { + DrawRectangle( + screen_x + client->cell_size / 2, + screen_y + client->cell_size / 2, + client->cell_size / 4, + client->cell_size / 4, + PUFF_CYAN + ); + } + } + } + } + + EndDrawing(); +} + +// Cleanup and free the rendering client +void close_client(Client* client) { + UnloadTexture(client->agent_texture); + CloseWindow(); + free(client); +} diff --git a/pufferlib/ocean/trash_pickup/trash_pickup.py b/pufferlib/ocean/trash_pickup/trash_pickup.py new file mode 100644 index 00000000..2b15b6a3 --- /dev/null +++ b/pufferlib/ocean/trash_pickup/trash_pickup.py @@ -0,0 +1,109 @@ +import numpy as np +from gymnasium import spaces + +import pufferlib +from pufferlib.ocean.trash_pickup.cy_trash_pickup import CyTrashPickup + + +class TrashPickupEnv(pufferlib.PufferEnv): + def __init__(self, num_envs=1, render_mode=None, report_interval=1, buf=None, + grid_size=10, num_agents=3, num_trash=15, num_bins=2, max_steps=300, agent_sight_range=5): + # Env Setup + self.render_mode = render_mode + self.report_interval = report_interval + + # Validate num_agents + if not isinstance(num_agents, int) or num_agents <= 0: + raise ValueError("num_agents must be an integer greater than 0.") + self.num_agents = num_envs * num_agents + self.num_agents_per_env = num_agents + + # Handle num_trash input + if not isinstance(num_trash, int) or num_trash <= 0: + raise ValueError("num_trash must be an int > 0") + self.num_trash = num_trash + + # Handle num_bins input + if not isinstance(num_bins, int) or num_bins <= 0: + raise ValueError("num_bins must be an int > 0") + self.num_bins = num_bins + + if not isinstance(max_steps, int) or max_steps < 10: + raise ValueError("max_steps must be an int >= 10") + self.max_steps = max_steps + + if not isinstance(agent_sight_range, int) or agent_sight_range < 2: + raise ValueError("agent sight range must be an int >= 2") + self.agent_sight_range = agent_sight_range + + # Calculate minimum required grid size + min_grid_size = int((num_agents + self.num_trash + self.num_bins) ** 0.5) + 1 + if not isinstance(grid_size, int) or grid_size < min_grid_size: + raise ValueError( + f"grid_size must be an integer >= {min_grid_size}. " + f"Received grid_size={grid_size}, with num_agents={num_agents}, num_trash={self.num_trash}, and num_bins={self.num_bins}." + ) + self.grid_size = grid_size + + # Entity Attribute Based Obs-Space + # num_obs_trash = num_trash * 3 # [presence, x pos, y pos] for each trash + # num_obs_bin = num_bins * 2 # [x pos, y pos] for each bin + # num_obs_agent = num_agents * 3 # [carrying trash, x pos, y pos] for each agent + # self.num_obs = num_obs_trash + num_obs_bin + num_obs_agent; + + # 2D Local crop obs space + self.num_obs = ((((agent_sight_range * 2 + 1) * (agent_sight_range * 2 + 1)) * 5)); # one-hot encoding for all cell types in local crop around agent (minus the cell the agent is currently in) + + self.single_observation_space = spaces.Box(low=0, high=1, + shape=(self.num_obs,), dtype=np.int8) + self.single_action_space = spaces.Discrete(4) + + super().__init__(buf=buf) + self.c_envs = CyTrashPickup(self.observations, self.actions, self.rewards, self.terminals, num_envs, num_agents, grid_size, num_trash, num_bins, max_steps, agent_sight_range) + + def reset(self, seed=None): + self.c_envs.reset() + self.tick = 0 + return self.observations, [] + + def step(self, actions): + self.actions[:] = actions + self.c_envs.step() + self.tick += 1 + + info = [] + if self.tick % self.report_interval == 0: + log = self.c_envs.log() + # print(f"tha log: {log}") + if log['episode_length'] > 0: + info.append(log) + + return (self.observations, self.rewards, + self.terminals, self.truncations, info) + + def render(self): + self.c_envs.render() + + def close(self): + self.c_envs.close() + +def test_performance(timeout=10, atn_cache=1024): + env = TrashPickupEnv(num_envs=1024, grid_size=10, num_agents=4, + num_trash=20, num_bins=1, max_steps=150, agent_sight_range=5) + + env.reset() + tick = 0 + + actions = np.random.randint(0, 4, (atn_cache, env.num_agents)) + + import time + start = time.time() + while time.time() - start < timeout: + atn = actions[tick % atn_cache] + env.step(atn) + tick += 1 + + print(f'SPS: %f', env.num_agents * tick / (time.time() - start)) + +if __name__ == '__main__': + test_performance() diff --git a/pufferlib/ocean/tripletriad/tripletriad.c b/pufferlib/ocean/tripletriad/tripletriad.c index 95f2443f..3dd64a5f 100644 --- a/pufferlib/ocean/tripletriad/tripletriad.c +++ b/pufferlib/ocean/tripletriad/tripletriad.c @@ -1,15 +1,13 @@ #include "tripletriad.h" #include "puffernet.h" -#include - int main() { Weights* weights = load_weights("resources/tripletriad_weights.bin", 148880); LinearLSTM* net = make_linearlstm(weights, 1, 114, 15); CTripleTriad env = { .width = 990, - .height = 1000, + .height = 690, .card_width = 576 / 3, .card_height = 672 / 3, .game_over = 0, @@ -19,6 +17,7 @@ int main() { reset(&env); Client* client = make_client(env.width, env.height); + int tick = 0; while (!WindowShouldClose()) { // User can take control of the player if (IsKeyDown(KEY_LEFT_SHIFT)) { @@ -54,19 +53,16 @@ int main() { env.actions[0] = cellIndex + 5; } } - } else { + } else if (tick % 45 == 0) { forward_linearlstm(net, env.observations, env.actions); } + tick = (tick + 1) % 45; if (env.actions[0] != NOOP) { step(&env); } render(client, &env); - - if (env.actions[0] >= PLACE_CARD_1) { - usleep(750000); - } } free_linearlstm(net); free(weights); diff --git a/pufferlib/ocean/tripletriad/tripletriad.h b/pufferlib/ocean/tripletriad/tripletriad.h index 5c6c36b7..2fdc5fb8 100644 --- a/pufferlib/ocean/tripletriad/tripletriad.h +++ b/pufferlib/ocean/tripletriad/tripletriad.h @@ -572,9 +572,6 @@ Client* make_client(int width, int height) { InitWindow(width, height, "PufferLib Ray TripleTriad"); SetTargetFPS(60); - //sound_path = os.path.join(*self.__module__.split(".")[:-1], "hit.wav") - //self.sound = rl.LoadSound(sound_path.encode()) - return client; } @@ -600,20 +597,20 @@ void render(Client* client, CTripleTriad* env) { } int x = env->board_x[board_idx]; int y = env->board_y[board_idx]; - DrawRectangle(x+196+10 , y+30 , env->card_width, env->card_height, piece_color); - DrawRectangleLines(x+196+10 , y+30 , env->card_width, env->card_height, PUFF_WHITE); + DrawRectangle(x+196+10 , y+10 , env->card_width, env->card_height, piece_color); + DrawRectangleLines(x+196+10 , y+10 , env->card_width, env->card_height, PUFF_WHITE); } } for(int i=0; i< 2; i++) { for(int j=0; j< 5; j++) { // starting locations for cards in hand int card_x = (i == 0) ? 10 : (env->width - env->card_width - 10); - int card_y = 30 + env->card_height/2*j; + int card_y = 10 + env->card_height/2*j; // locations if card is placed if (env->card_locations[i][j] != 0) { card_x = env->board_x[env->card_locations[i][j]-1] + 196 + 10; - card_y = env->board_y[env->card_locations[i][j]-1] + 30; + card_y = env->board_y[env->card_locations[i][j]-1] + 10; } // Draw card background // adjusts card color based on board state @@ -632,7 +629,7 @@ void render(Client* client, CTripleTriad* env) { // change background if card is selected, highlight it Rectangle rect = (Rectangle){card_x, card_y, env->card_width, env->card_height}; if (env->card_selected[i] == j) { - DrawRectangleLinesEx(rect, 2, GREEN); + DrawRectangleLinesEx(rect, 3, PUFF_RED); } else { DrawRectangleLinesEx(rect, 2, PUFF_WHITE); } @@ -665,25 +662,11 @@ void render(Client* client, CTripleTriad* env) { DrawText(TextFormat("Card %d", j+1), card_x + env->card_width -50, card_y + 5, 10, PUFF_WHITE); } if (i == 0) { - DrawText(TextFormat("%d", env->score[i]), env->card_width *0.4, env->height - 400, 100, PUFF_WHITE); + DrawText(TextFormat("%d", env->score[i]), env->card_width *0.4, env->height - 100, 100, PUFF_WHITE); } else { - DrawText(TextFormat("%d", env->score[i]), env->width - env->card_width *.6, env->height - 400, 100, PUFF_WHITE); + DrawText(TextFormat("%d", env->score[i]), env->width - env->card_width *.6, env->height - 100, 100, PUFF_WHITE); } } - DrawText("Triple Triad", 20, 10, 20, PUFF_WHITE); - - // give instructions to player 1: - DrawText("How to Play: Use 1-5 to select a card", 20, env->height - 280, 20, PUFF_WHITE); - DrawText("Click an empty space on the board to place a card", 20, env->height - 250, 20, PUFF_WHITE); - - // Explain further rules - DrawText("Goal: Place all your cards on the board. The player with the highest score wins.", 20, env->height - 220, 20, PUFF_WHITE); - DrawText("Rules: Each card has 4 values, N, S, E, W.", 20, env->height - 190, 20, PUFF_WHITE); - DrawText("You may not place a card on top of an opponent's card.", 20, env->height - 160, 20, PUFF_WHITE); - DrawText("Scoring Example: Player 1 places a card with a 2 in the North direction.", 20, env->height - 100, 20, PUFF_WHITE); - DrawText("If Player 2 has a card above Player 1's card with a 1 in the South direction. ", 20, env->height - 70, 20, PUFF_WHITE); - DrawText("Player 1 captures Player 2's card. Player 1 gains a point. Player 2 loses a point.", 20, env->height - 40, 20, PUFF_WHITE); - EndDrawing(); //PlaySound(client->sound); diff --git a/pufferlib/ocean/tripletriad/tripletriad.py b/pufferlib/ocean/tripletriad/tripletriad.py index eae5cbef..67cf9782 100644 --- a/pufferlib/ocean/tripletriad/tripletriad.py +++ b/pufferlib/ocean/tripletriad/tripletriad.py @@ -6,7 +6,7 @@ class TripleTriad(pufferlib.PufferEnv): def __init__(self, num_envs=1, render_mode=None, report_interval=1, - width=990, height=1000, piece_width=192, piece_height=224, buf=None): + width=990, height=690, piece_width=192, piece_height=224, buf=None): self.single_observation_space = gymnasium.spaces.Box(low=0, high=1, shape=(114,), dtype=np.float32) self.single_action_space = gymnasium.spaces.Discrete(15) diff --git a/pufferlib/puffernet.h b/pufferlib/puffernet.h index e2e88eec..50c16734 100644 --- a/pufferlib/puffernet.h +++ b/pufferlib/puffernet.h @@ -43,7 +43,7 @@ void _load_weights(const char* filename, float* weights, size_t num_weights) { } fseek(file, 0, SEEK_END); rewind(file); - int read_size = fread(weights, sizeof(float), num_weights, file); + size_t read_size = fread(weights, sizeof(float), num_weights, file); fclose(file); if (read_size != num_weights) { perror("Error reading file"); @@ -182,6 +182,12 @@ void _lstm(float* input, float* state_h, float* state_c, float* weights_input, } } +void _embedding(int* input, float* weights, float* output, int batch_size, int num_embeddings, int embedding_dim) { + for (int b = 0; b < batch_size; b++) { + memcpy(output + b*embedding_dim, weights + input[b]*embedding_dim, embedding_dim*sizeof(float)); + } +} + void _one_hot(int* input, int* output, int batch_size, int input_size, int num_classes) { for (int b = 0; b < batch_size; b++) { for (int i = 0; i < input_size; i++) { @@ -242,17 +248,14 @@ void _softmax_multidiscrete(float* input, int* output, int batch_size, int logit logit_exp_sum += expf(input[in_adr + i]); } float prob = rand() / (float)RAND_MAX; - bool found = false; float logit_prob = 0; for (int i = 0; i < num_action_types; i++) { logit_prob += expf(input[in_adr + i]) / logit_exp_sum; if (prob < logit_prob) { output[out_adr] = i; - found = true; break; } } - assert(found); in_adr += num_action_types; } } @@ -395,6 +398,32 @@ void lstm(LSTM* layer, float* input) { layer->buffer, layer->batch_size, layer->input_size, layer->hidden_size); } +typedef struct Embedding Embedding; +struct Embedding { + float* output; + float* weights; + int batch_size; + int num_embeddings; + int embedding_dim; +}; + +Embedding* make_embedding(Weights* weights, int batch_size, int num_embeddings, int embedding_dim) { + size_t output_size = batch_size*embedding_dim*sizeof(float); + Embedding* layer = (Embedding*)calloc(1, sizeof(Embedding) + batch_size + output_size); + *layer = (Embedding){ + .output = (float*)(layer + 1), + .weights = get_weights(weights, num_embeddings*embedding_dim), + .batch_size = batch_size, + .num_embeddings = num_embeddings, + .embedding_dim = embedding_dim, + }; + return layer; +} + +void embedding(Embedding* layer, int* input) { + _embedding(input, layer->weights, layer->output, layer->batch_size, layer->num_embeddings, layer->embedding_dim); +} + typedef struct OneHot OneHot; struct OneHot { int* output; @@ -556,8 +585,7 @@ void forward_linearlstm(LinearLSTM* net, float* observations, int* actions) { softmax_multidiscrete(net->multidiscrete, net->actor->output, actions); } -typedef struct ConvLSTM ConvLSTM; -struct ConvLSTM { +typedef struct ConvLSTM ConvLSTM; struct ConvLSTM { int num_agents; float* obs; Conv2D* conv1; @@ -575,16 +603,16 @@ ConvLSTM* make_convlstm(Weights* weights, int num_agents, int input_dim, int input_channels, int cnn_channels, int hidden_dim, int action_dim) { ConvLSTM* net = calloc(1, sizeof(ConvLSTM)); net->num_agents = num_agents; - net->obs = calloc(num_agents*input_dim*input_dim, sizeof(float)); + net->obs = calloc(num_agents*input_dim*input_dim*input_channels, sizeof(float)); net->conv1 = make_conv2d(weights, num_agents, input_dim, input_dim, input_channels, cnn_channels, 5, 3); net->relu1 = make_relu(num_agents, hidden_dim*3*3); - net->conv2 = make_conv2d(weights, num_agents, 3, 3, hidden_dim, hidden_dim, 3, 1); + net->conv2 = make_conv2d(weights, num_agents, 3, 3, cnn_channels, cnn_channels, 3, 1); net->relu2 = make_relu(num_agents, hidden_dim); - net->linear = make_linear(weights, num_agents, hidden_dim, 128); - net->actor = make_linear(weights, num_agents, 128, action_dim); - net->value_fn = make_linear(weights, num_agents, 128, 1); - net->lstm = make_lstm(weights, num_agents, 128, 128); + net->linear = make_linear(weights, num_agents, cnn_channels, hidden_dim); + net->actor = make_linear(weights, num_agents, hidden_dim, action_dim); + net->value_fn = make_linear(weights, num_agents, hidden_dim, 1); + net->lstm = make_lstm(weights, num_agents, hidden_dim, hidden_dim); int logit_sizes[1] = {action_dim}; net->multidiscrete = make_multidiscrete(num_agents, logit_sizes, 1); return net; diff --git a/pufferlib/puffernet.pyx b/pufferlib/puffernet.pyx index 90661fd6..47b0c763 100644 --- a/pufferlib/puffernet.pyx +++ b/pufferlib/puffernet.pyx @@ -22,6 +22,8 @@ cdef extern from "puffernet.h": void _conv2d(float* input, float* weights, float* bias, float* output, int batch_size, int in_width, int in_height, int in_channels, int out_channels, int kernel_size, int stride) + void _embedding(int* input, float* weights, float* output, + int batch_size, int num_embeddings, int embedding_dim) void _lstm(float* input, float* state_h, float* state_c, float* weights_input, float* weights_state, float* bias_input, float*bias_state, float *buffer, int batch_size, int input_size, int hidden_size) @@ -57,6 +59,11 @@ def puf_lstm(cnp.ndarray input, cnp.ndarray state_h, cnp.ndarray state_c, cnp.nd weights_input.data, weights_state.data, bias_input.data, bias_state.data, buffer.data, batch_size, input_size, hidden_size) +def puf_embedding(cnp.ndarray input, cnp.ndarray weights, cnp.ndarray output, + int batch_size, int num_embeddings, int embedding_dim): + _embedding( input.data, weights.data, output.data, + batch_size, num_embeddings, embedding_dim) + def puf_one_hot(cnp.ndarray input, cnp.ndarray output, int batch_size, int input_size, int num_classes): _one_hot( input.data, output.data, batch_size, input_size, num_classes) diff --git a/resources/breakout_weights.bin b/pufferlib/resources/breakout_weights.bin similarity index 100% rename from resources/breakout_weights.bin rename to pufferlib/resources/breakout_weights.bin diff --git a/resources/connect4.pt b/pufferlib/resources/connect4.pt similarity index 100% rename from resources/connect4.pt rename to pufferlib/resources/connect4.pt diff --git a/resources/connect4_weights.bin b/pufferlib/resources/connect4_weights.bin similarity index 100% rename from resources/connect4_weights.bin rename to pufferlib/resources/connect4_weights.bin diff --git a/resources/puffer_enduro/enduro_spritesheet.png b/pufferlib/resources/enduro/enduro_spritesheet.png similarity index 100% rename from resources/puffer_enduro/enduro_spritesheet.png rename to pufferlib/resources/enduro/enduro_spritesheet.png diff --git a/resources/puffer_enduro/gamma_0.910002_weights.bin b/pufferlib/resources/enduro/enduro_weights.bin similarity index 100% rename from resources/puffer_enduro/gamma_0.910002_weights.bin rename to pufferlib/resources/enduro/enduro_weights.bin diff --git a/pufferlib/resources/go_weights.bin b/pufferlib/resources/go_weights.bin new file mode 100644 index 00000000..8677889d Binary files /dev/null and b/pufferlib/resources/go_weights.bin differ diff --git a/resources/moba/bloom_shader_100.fs b/pufferlib/resources/moba/bloom_shader_100.fs similarity index 100% rename from resources/moba/bloom_shader_100.fs rename to pufferlib/resources/moba/bloom_shader_100.fs diff --git a/resources/moba/bloom_shader_330.fs b/pufferlib/resources/moba/bloom_shader_330.fs similarity index 100% rename from resources/moba/bloom_shader_330.fs rename to pufferlib/resources/moba/bloom_shader_330.fs diff --git a/resources/moba/dota_map.png b/pufferlib/resources/moba/dota_map.png similarity index 100% rename from resources/moba/dota_map.png rename to pufferlib/resources/moba/dota_map.png diff --git a/resources/moba/game_map.npy b/pufferlib/resources/moba/game_map.npy similarity index 100% rename from resources/moba/game_map.npy rename to pufferlib/resources/moba/game_map.npy diff --git a/resources/moba/map_shader_100.fs b/pufferlib/resources/moba/map_shader_100.fs similarity index 100% rename from resources/moba/map_shader_100.fs rename to pufferlib/resources/moba/map_shader_100.fs diff --git a/resources/moba/map_shader_330.fs b/pufferlib/resources/moba/map_shader_330.fs similarity index 100% rename from resources/moba/map_shader_330.fs rename to pufferlib/resources/moba/map_shader_330.fs diff --git a/resources/moba/moba_assets.png b/pufferlib/resources/moba/moba_assets.png similarity index 100% rename from resources/moba/moba_assets.png rename to pufferlib/resources/moba/moba_assets.png diff --git a/resources/moba/moba_weights.bin b/pufferlib/resources/moba/moba_weights.bin similarity index 100% rename from resources/moba/moba_weights.bin rename to pufferlib/resources/moba/moba_weights.bin diff --git a/pufferlib/resources/nmmo3/ASSETS_LICENSE.md b/pufferlib/resources/nmmo3/ASSETS_LICENSE.md new file mode 100644 index 00000000..44515579 --- /dev/null +++ b/pufferlib/resources/nmmo3/ASSETS_LICENSE.md @@ -0,0 +1 @@ +Characters and assets subject to the license of the original artists. In particular, we use Mana Seed assets by Seliel the Shaper under a valid license purchased from itch.io. You may not repurpose these assets for other projects without purchasing your own license. To mitigate abuse, we release only collated spritesheets as exported by our postprocessor. diff --git a/pufferlib/resources/nmmo3/air_0.png b/pufferlib/resources/nmmo3/air_0.png new file mode 100644 index 00000000..624223ec Binary files /dev/null and b/pufferlib/resources/nmmo3/air_0.png differ diff --git a/pufferlib/resources/nmmo3/air_1.png b/pufferlib/resources/nmmo3/air_1.png new file mode 100644 index 00000000..87f522f4 Binary files /dev/null and b/pufferlib/resources/nmmo3/air_1.png differ diff --git a/pufferlib/resources/nmmo3/air_2.png b/pufferlib/resources/nmmo3/air_2.png new file mode 100644 index 00000000..0a1a4d55 Binary files /dev/null and b/pufferlib/resources/nmmo3/air_2.png differ diff --git a/pufferlib/resources/nmmo3/air_3.png b/pufferlib/resources/nmmo3/air_3.png new file mode 100644 index 00000000..4cf5f818 Binary files /dev/null and b/pufferlib/resources/nmmo3/air_3.png differ diff --git a/pufferlib/resources/nmmo3/air_4.png b/pufferlib/resources/nmmo3/air_4.png new file mode 100644 index 00000000..047a4c46 Binary files /dev/null and b/pufferlib/resources/nmmo3/air_4.png differ diff --git a/pufferlib/resources/nmmo3/air_5.png b/pufferlib/resources/nmmo3/air_5.png new file mode 100644 index 00000000..97acec4a Binary files /dev/null and b/pufferlib/resources/nmmo3/air_5.png differ diff --git a/pufferlib/resources/nmmo3/air_6.png b/pufferlib/resources/nmmo3/air_6.png new file mode 100644 index 00000000..9b6fa25e Binary files /dev/null and b/pufferlib/resources/nmmo3/air_6.png differ diff --git a/pufferlib/resources/nmmo3/air_7.png b/pufferlib/resources/nmmo3/air_7.png new file mode 100644 index 00000000..90cae931 Binary files /dev/null and b/pufferlib/resources/nmmo3/air_7.png differ diff --git a/pufferlib/resources/nmmo3/air_8.png b/pufferlib/resources/nmmo3/air_8.png new file mode 100644 index 00000000..c9395f55 Binary files /dev/null and b/pufferlib/resources/nmmo3/air_8.png differ diff --git a/pufferlib/resources/nmmo3/air_9.png b/pufferlib/resources/nmmo3/air_9.png new file mode 100644 index 00000000..b4fa1d31 Binary files /dev/null and b/pufferlib/resources/nmmo3/air_9.png differ diff --git a/pufferlib/resources/nmmo3/earth_0.png b/pufferlib/resources/nmmo3/earth_0.png new file mode 100644 index 00000000..e8a405ed Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_0.png differ diff --git a/pufferlib/resources/nmmo3/earth_1.png b/pufferlib/resources/nmmo3/earth_1.png new file mode 100644 index 00000000..d991bf00 Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_1.png differ diff --git a/pufferlib/resources/nmmo3/earth_2.png b/pufferlib/resources/nmmo3/earth_2.png new file mode 100644 index 00000000..d396be45 Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_2.png differ diff --git a/pufferlib/resources/nmmo3/earth_3.png b/pufferlib/resources/nmmo3/earth_3.png new file mode 100644 index 00000000..4df1546a Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_3.png differ diff --git a/pufferlib/resources/nmmo3/earth_4.png b/pufferlib/resources/nmmo3/earth_4.png new file mode 100644 index 00000000..c715a5d7 Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_4.png differ diff --git a/pufferlib/resources/nmmo3/earth_5.png b/pufferlib/resources/nmmo3/earth_5.png new file mode 100644 index 00000000..75e1b70b Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_5.png differ diff --git a/pufferlib/resources/nmmo3/earth_6.png b/pufferlib/resources/nmmo3/earth_6.png new file mode 100644 index 00000000..ed2fa3d7 Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_6.png differ diff --git a/pufferlib/resources/nmmo3/earth_7.png b/pufferlib/resources/nmmo3/earth_7.png new file mode 100644 index 00000000..0105c53a Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_7.png differ diff --git a/pufferlib/resources/nmmo3/earth_8.png b/pufferlib/resources/nmmo3/earth_8.png new file mode 100644 index 00000000..215e9598 Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_8.png differ diff --git a/pufferlib/resources/nmmo3/earth_9.png b/pufferlib/resources/nmmo3/earth_9.png new file mode 100644 index 00000000..c00f505f Binary files /dev/null and b/pufferlib/resources/nmmo3/earth_9.png differ diff --git a/pufferlib/resources/nmmo3/fire_0.png b/pufferlib/resources/nmmo3/fire_0.png new file mode 100644 index 00000000..f141d886 Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_0.png differ diff --git a/pufferlib/resources/nmmo3/fire_1.png b/pufferlib/resources/nmmo3/fire_1.png new file mode 100644 index 00000000..cdd3b216 Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_1.png differ diff --git a/pufferlib/resources/nmmo3/fire_2.png b/pufferlib/resources/nmmo3/fire_2.png new file mode 100644 index 00000000..b7ffe84d Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_2.png differ diff --git a/pufferlib/resources/nmmo3/fire_3.png b/pufferlib/resources/nmmo3/fire_3.png new file mode 100644 index 00000000..6e5f48d3 Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_3.png differ diff --git a/pufferlib/resources/nmmo3/fire_4.png b/pufferlib/resources/nmmo3/fire_4.png new file mode 100644 index 00000000..2a0017a1 Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_4.png differ diff --git a/pufferlib/resources/nmmo3/fire_5.png b/pufferlib/resources/nmmo3/fire_5.png new file mode 100644 index 00000000..b8102107 Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_5.png differ diff --git a/pufferlib/resources/nmmo3/fire_6.png b/pufferlib/resources/nmmo3/fire_6.png new file mode 100644 index 00000000..64d3cf9c Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_6.png differ diff --git a/pufferlib/resources/nmmo3/fire_7.png b/pufferlib/resources/nmmo3/fire_7.png new file mode 100644 index 00000000..c23ba2db Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_7.png differ diff --git a/pufferlib/resources/nmmo3/fire_8.png b/pufferlib/resources/nmmo3/fire_8.png new file mode 100644 index 00000000..2c302a11 Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_8.png differ diff --git a/pufferlib/resources/nmmo3/fire_9.png b/pufferlib/resources/nmmo3/fire_9.png new file mode 100644 index 00000000..76b23b9b Binary files /dev/null and b/pufferlib/resources/nmmo3/fire_9.png differ diff --git a/pufferlib/resources/nmmo3/inventory_64.png b/pufferlib/resources/nmmo3/inventory_64.png new file mode 100755 index 00000000..b1eb19de Binary files /dev/null and b/pufferlib/resources/nmmo3/inventory_64.png differ diff --git a/pufferlib/resources/nmmo3/inventory_64_press.png b/pufferlib/resources/nmmo3/inventory_64_press.png new file mode 100755 index 00000000..3a7304bd Binary files /dev/null and b/pufferlib/resources/nmmo3/inventory_64_press.png differ diff --git a/pufferlib/resources/nmmo3/inventory_64_selected.png b/pufferlib/resources/nmmo3/inventory_64_selected.png new file mode 100755 index 00000000..7801e028 Binary files /dev/null and b/pufferlib/resources/nmmo3/inventory_64_selected.png differ diff --git a/pufferlib/resources/nmmo3/items_condensed.png b/pufferlib/resources/nmmo3/items_condensed.png new file mode 100644 index 00000000..874f1389 Binary files /dev/null and b/pufferlib/resources/nmmo3/items_condensed.png differ diff --git a/pufferlib/resources/nmmo3/map_shader_100.fs b/pufferlib/resources/nmmo3/map_shader_100.fs new file mode 100644 index 00000000..bf7dee1c --- /dev/null +++ b/pufferlib/resources/nmmo3/map_shader_100.fs @@ -0,0 +1,57 @@ +precision mediump float; + +// Input uniforms (unchanged from original) +uniform sampler2D terrain; +uniform sampler2D texture_tiles; +uniform vec4 colDiffuse; +uniform vec3 resolution; +uniform vec4 mouse; +uniform float time; +uniform float camera_x; +uniform float camera_y; +uniform float map_width; +uniform float map_height; + +// Constants +const float TILE_SIZE = 64.0; +const float TILES_PER_ROW = 64.0; + +void main() +{ + float ts = TILE_SIZE * resolution.z; + // Get the screen pixel coordinates + vec2 pixelPos = gl_FragCoord.xy; + + float x_offset = camera_x/64.0 + pixelPos.x/ts - resolution.x/ts/2.0; + float y_offset = camera_y/64.0 - pixelPos.y/ts + resolution.y/ts/2.0; + float x_floor = floor(x_offset); + float y_floor = floor(y_offset); + float x_frac = x_offset - x_floor; + float y_frac = y_offset - y_floor; + + // Environment size calculation + vec2 uv = vec2( + x_floor/map_width, + y_floor/map_height + ); + + vec2 tile_rg = texture2D(terrain, uv).rg; + float tile_high_byte = floor(tile_rg.r * 255.0); + float tile_low_byte = floor(tile_rg.g * 255.0); + float tile = tile_high_byte * 64.0 + tile_low_byte; + + // Handle animated tiles + if (tile >= 240.0 && tile < (240.0 + 4.0*4.0*4.0*4.0)) { + tile += floor(3.9 * time); + } + + tile_high_byte = floor(tile/64.0); + tile_low_byte = floor(mod(tile, 64.0)); + + vec2 tile_uv = vec2( + tile_low_byte/64.0 + x_frac/64.0, + tile_high_byte/64.0 + y_frac/64.0 + ); + + gl_FragColor = texture2D(texture_tiles, tile_uv); +} diff --git a/pufferlib/resources/nmmo3/map_shader_330.fs b/pufferlib/resources/nmmo3/map_shader_330.fs new file mode 100644 index 00000000..8b8ac6ee --- /dev/null +++ b/pufferlib/resources/nmmo3/map_shader_330.fs @@ -0,0 +1,68 @@ +#version 330 + +// Input vertex attributes (from vertex shader) +in vec2 fragTexCoord; +in vec4 fragColor; + +// Input uniform values +uniform sampler2D terrain; +uniform sampler2D texture_tiles; // Tile sprite sheet texture +uniform vec4 colDiffuse; +uniform vec3 resolution; +uniform vec4 mouse; +uniform float time; +uniform float camera_x; +uniform float camera_y; +uniform float map_width; +uniform float map_height; + +// Output fragment color +out vec4 outputColor; + +float TILE_SIZE = 64.0; + +// Number of tiles per row in the sprite sheet +const int TILES_PER_ROW = 64; // Adjust this based on your sprite sheet layout + +void main() +{ + float ts = TILE_SIZE * resolution.z; + + // Get the screen pixel coordinates + vec2 pixelPos = gl_FragCoord.xy; + + float x_offset = camera_x/64.0 + pixelPos.x/ts - resolution.x/ts/2.0; + float y_offset = camera_y/64.0 - pixelPos.y/ts + resolution.y/ts/2.0; + + float x_floor = floor(x_offset); + float y_floor = floor(y_offset); + + float x_frac = x_offset - x_floor; + float y_frac = y_offset - y_floor; + + // TODO: This is the env size + vec2 uv = vec2( + x_floor/map_width, + y_floor/map_height + ); + vec2 tile_rg = texture(terrain, uv).rg; + + int tile_high_byte = int(tile_rg.r*255.0); + int tile_low_byte = int(tile_rg.g*255.0); + + int tile = tile_high_byte*64 + tile_low_byte; + if (tile >= 240 && tile < 240+4*4*4*4) { + tile += int(3.9*time); + } + + tile_high_byte = int(tile/64.0); + tile_low_byte = int(tile%64); + + vec2 tile_uv = vec2( + tile_low_byte/64.0 + x_frac/64.0, + tile_high_byte/64.0 + y_frac/64.0 + ); + + outputColor = texture(texture_tiles, tile_uv); +} + diff --git a/pufferlib/resources/nmmo3/merged_sheet.png b/pufferlib/resources/nmmo3/merged_sheet.png new file mode 100644 index 00000000..1a7c78d3 Binary files /dev/null and b/pufferlib/resources/nmmo3/merged_sheet.png differ diff --git a/pufferlib/resources/nmmo3/neutral_0.png b/pufferlib/resources/nmmo3/neutral_0.png new file mode 100644 index 00000000..d76713d4 Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_0.png differ diff --git a/pufferlib/resources/nmmo3/neutral_1.png b/pufferlib/resources/nmmo3/neutral_1.png new file mode 100644 index 00000000..5dcc1e8a Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_1.png differ diff --git a/pufferlib/resources/nmmo3/neutral_2.png b/pufferlib/resources/nmmo3/neutral_2.png new file mode 100644 index 00000000..c3dfc5a6 Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_2.png differ diff --git a/pufferlib/resources/nmmo3/neutral_3.png b/pufferlib/resources/nmmo3/neutral_3.png new file mode 100644 index 00000000..64719d36 Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_3.png differ diff --git a/pufferlib/resources/nmmo3/neutral_4.png b/pufferlib/resources/nmmo3/neutral_4.png new file mode 100644 index 00000000..48418141 Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_4.png differ diff --git a/pufferlib/resources/nmmo3/neutral_5.png b/pufferlib/resources/nmmo3/neutral_5.png new file mode 100644 index 00000000..444d00b0 Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_5.png differ diff --git a/pufferlib/resources/nmmo3/neutral_6.png b/pufferlib/resources/nmmo3/neutral_6.png new file mode 100644 index 00000000..245c24c1 Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_6.png differ diff --git a/pufferlib/resources/nmmo3/neutral_7.png b/pufferlib/resources/nmmo3/neutral_7.png new file mode 100644 index 00000000..ce005436 Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_7.png differ diff --git a/pufferlib/resources/nmmo3/neutral_8.png b/pufferlib/resources/nmmo3/neutral_8.png new file mode 100644 index 00000000..52080374 Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_8.png differ diff --git a/pufferlib/resources/nmmo3/neutral_9.png b/pufferlib/resources/nmmo3/neutral_9.png new file mode 100644 index 00000000..ee1fbd0b Binary files /dev/null and b/pufferlib/resources/nmmo3/neutral_9.png differ diff --git a/pufferlib/resources/nmmo3/nmmo3_help.png b/pufferlib/resources/nmmo3/nmmo3_help.png new file mode 100644 index 00000000..a4c7a960 Binary files /dev/null and b/pufferlib/resources/nmmo3/nmmo3_help.png differ diff --git a/pufferlib/resources/nmmo3/nmmo_1500.bin b/pufferlib/resources/nmmo3/nmmo_1500.bin new file mode 100644 index 00000000..d051ac2c Binary files /dev/null and b/pufferlib/resources/nmmo3/nmmo_1500.bin differ diff --git a/pufferlib/resources/nmmo3/nmmo_2025.bin b/pufferlib/resources/nmmo3/nmmo_2025.bin new file mode 100644 index 00000000..55c480a0 Binary files /dev/null and b/pufferlib/resources/nmmo3/nmmo_2025.bin differ diff --git a/pufferlib/resources/nmmo3/water_0.png b/pufferlib/resources/nmmo3/water_0.png new file mode 100644 index 00000000..e743a38a Binary files /dev/null and b/pufferlib/resources/nmmo3/water_0.png differ diff --git a/pufferlib/resources/nmmo3/water_1.png b/pufferlib/resources/nmmo3/water_1.png new file mode 100644 index 00000000..fb41fefa Binary files /dev/null and b/pufferlib/resources/nmmo3/water_1.png differ diff --git a/pufferlib/resources/nmmo3/water_2.png b/pufferlib/resources/nmmo3/water_2.png new file mode 100644 index 00000000..1f228a41 Binary files /dev/null and b/pufferlib/resources/nmmo3/water_2.png differ diff --git a/pufferlib/resources/nmmo3/water_3.png b/pufferlib/resources/nmmo3/water_3.png new file mode 100644 index 00000000..10ec9bd0 Binary files /dev/null and b/pufferlib/resources/nmmo3/water_3.png differ diff --git a/pufferlib/resources/nmmo3/water_4.png b/pufferlib/resources/nmmo3/water_4.png new file mode 100644 index 00000000..14a015a6 Binary files /dev/null and b/pufferlib/resources/nmmo3/water_4.png differ diff --git a/pufferlib/resources/nmmo3/water_5.png b/pufferlib/resources/nmmo3/water_5.png new file mode 100644 index 00000000..d132d09b Binary files /dev/null and b/pufferlib/resources/nmmo3/water_5.png differ diff --git a/pufferlib/resources/nmmo3/water_6.png b/pufferlib/resources/nmmo3/water_6.png new file mode 100644 index 00000000..9979284b Binary files /dev/null and b/pufferlib/resources/nmmo3/water_6.png differ diff --git a/pufferlib/resources/nmmo3/water_7.png b/pufferlib/resources/nmmo3/water_7.png new file mode 100644 index 00000000..1eeec44c Binary files /dev/null and b/pufferlib/resources/nmmo3/water_7.png differ diff --git a/pufferlib/resources/nmmo3/water_8.png b/pufferlib/resources/nmmo3/water_8.png new file mode 100644 index 00000000..6c57ecee Binary files /dev/null and b/pufferlib/resources/nmmo3/water_8.png differ diff --git a/pufferlib/resources/nmmo3/water_9.png b/pufferlib/resources/nmmo3/water_9.png new file mode 100644 index 00000000..323f3850 Binary files /dev/null and b/pufferlib/resources/nmmo3/water_9.png differ diff --git a/resources/pong_weights.bin b/pufferlib/resources/pong_weights.bin similarity index 100% rename from resources/pong_weights.bin rename to pufferlib/resources/pong_weights.bin diff --git a/resources/puffers_128.png b/pufferlib/resources/puffers_128.png similarity index 100% rename from resources/puffers_128.png rename to pufferlib/resources/puffers_128.png diff --git a/resources/robocode/robocode.png b/pufferlib/resources/robocode/robocode.png similarity index 100% rename from resources/robocode/robocode.png rename to pufferlib/resources/robocode/robocode.png diff --git a/resources/rware_weights.bin b/pufferlib/resources/rware_weights.bin similarity index 100% rename from resources/rware_weights.bin rename to pufferlib/resources/rware_weights.bin diff --git a/resources/snake_weights.bin b/pufferlib/resources/snake_weights.bin similarity index 100% rename from resources/snake_weights.bin rename to pufferlib/resources/snake_weights.bin diff --git a/resources/tripletriad_weights.bin b/pufferlib/resources/tripletriad_weights.bin similarity index 100% rename from resources/tripletriad_weights.bin rename to pufferlib/resources/tripletriad_weights.bin diff --git a/pufferlib/vector.py b/pufferlib/vector.py index 925d674c..f8339e37 100644 --- a/pufferlib/vector.py +++ b/pufferlib/vector.py @@ -8,7 +8,7 @@ from pufferlib import namespace from pufferlib.emulation import GymnasiumPufferEnv, PettingZooPufferEnv -from pufferlib.environment import PufferEnv +from pufferlib.environment import PufferEnv, set_buffers from pufferlib.exceptions import APIUsageError from pufferlib.namespace import Namespace import pufferlib.spaces @@ -60,64 +60,55 @@ class Serial: def num_envs(self): return self.agents_per_batch - def __init__(self, env_creators, env_args, env_kwargs, num_envs, **kwargs): - self.envs = [creator(*args, **kwargs) for (creator, args, kwargs) - in zip(env_creators, env_args, env_kwargs)] + def __init__(self, env_creators, env_args, env_kwargs, num_envs, buf=None, **kwargs): + self.driver_env = env_creators[0](*env_args[0], **env_kwargs[0]) + self.agents_per_batch = self.driver_env.num_agents * num_envs + self.num_agents = self.agents_per_batch - if isinstance(self.envs[0], pufferlib.PufferEnv): - raise APIUsageError('Native PufferEnvs are not currently compatible with Serial vectorization. Use Native or Multiprocessing') + self.single_observation_space = self.driver_env.single_observation_space + self.single_action_space = self.driver_env.single_action_space + self.action_space = pufferlib.spaces.joint_space(self.single_action_space, self.agents_per_batch) + self.observation_space = pufferlib.spaces.joint_space(self.single_observation_space, self.agents_per_batch) + + + set_buffers(self, buf) + + self.envs = [] + ptr = 0 + for i in range(num_envs): + end = ptr + self.driver_env.num_agents + buf_i = namespace( + observations=self.observations[ptr:end], + rewards=self.rewards[ptr:end], + terminals=self.terminals[ptr:end], + truncations=self.truncations[ptr:end], + masks=self.masks[ptr:end], + actions=self.actions[ptr:end] + ) + ptr = end + env = env_creators[i](*env_args[i], buf=buf_i, **env_kwargs[i]) + self.envs.append(env) self.driver_env = driver = self.envs[0] self.emulated = self.driver_env.emulated check_envs(self.envs, self.driver_env) self.agents_per_env = [env.num_agents for env in self.envs] - self.agents_per_batch = sum(self.agents_per_env) - self.num_agents = sum(self.agents_per_env) - self.single_observation_space = driver.single_observation_space - self.single_action_space = driver.single_action_space - self.action_space = pufferlib.spaces.joint_space(self.single_action_space, self.agents_per_batch) - self.observation_space = pufferlib.spaces.joint_space(self.single_observation_space, self.agents_per_batch) + assert sum(self.agents_per_env) == self.agents_per_batch self.agent_ids = np.arange(self.num_agents) self.initialized = False self.flag = RESET - self.buf = None - - def _assign_buffers(self, buf): - '''Envs handle their own data buffers''' - ptr = 0 - self.buf = buf - for i, env in enumerate(self.envs): - end = ptr + self.agents_per_env[i] - env.buf = namespace( - observations=buf.observations[ptr:end], - rewards=buf.rewards[ptr:end], - terminals=buf.terminals[ptr:end], - truncations=buf.truncations[ptr:end], - masks=buf.masks[ptr:end] - ) - ptr = end def async_reset(self, seed=42): self.flag = RECV seed = make_seeds(seed, len(self.envs)) - if self.buf is None: - self.buf = namespace( - observations = np.zeros( - (self.agents_per_batch, *self.single_observation_space.shape), - dtype=self.single_observation_space.dtype), - rewards = np.zeros(self.agents_per_batch, dtype=np.float32), - terminals = np.zeros(self.agents_per_batch, dtype=bool), - truncations = np.zeros(self.agents_per_batch, dtype=bool), - masks = np.ones(self.agents_per_batch, dtype=bool), - ) - self._assign_buffers(self.buf) - infos = [] for env, s in zip(self.envs, seed): ob, i = env.reset(seed=s) - if i: + if isinstance(i, list): + infos.extend(i) + else: infos.append(i) self.infos = infos @@ -134,20 +125,21 @@ def send(self, actions): atns = actions[ptr:end] if env.done: o, i = env.reset() - buf = self.buf else: o, r, d, t, i = env.step(atns) if i: - self.infos.append(i) + if isinstance(i, list): + self.infos.extend(i) + else: + self.infos.append(i) ptr = end def recv(self): recv_precheck(self) - buf = self.buf - return (buf.observations, buf.rewards, buf.terminals, buf.truncations, - self.infos, self.agent_ids, buf.masks) + return (self.observations, self.rewards, self.terminals, self.truncations, + self.infos, self.agent_ids, self.masks) def close(self): for env in self.envs: @@ -171,11 +163,10 @@ def _worker_process(env_creators, env_args, env_kwargs, obs_shape, obs_dtype, at ) buf.masks[:] = True - if is_native: + if is_native and num_envs == 1: envs = env_creators[0](*env_args[0], **env_kwargs[0], buf=buf) else: - envs = Serial(env_creators, env_args, env_kwargs, num_envs) - envs._assign_buffers(buf) + envs = Serial(env_creators, env_args, env_kwargs, num_envs, buf=buf) semaphores=np.ndarray(num_workers, dtype=np.uint8, buffer=shm.semaphores) start = time.time() @@ -193,7 +184,7 @@ def _worker_process(env_creators, env_args, env_kwargs, obs_shape, obs_dtype, at elif sem == STEP: _, _, _, _, infos = envs.step(atn_arr) elif sem == CLOSE: - print("closing worker", worker_idx) + envs.close() send_pipe.send(None) break @@ -252,8 +243,6 @@ def __init__(self, env_creators, env_args, env_kwargs, # You can't send a RawArray through a pipe. self.driver_env = driver_env = env_creators[0](*env_args[0], **env_kwargs[0]) is_native = isinstance(driver_env, PufferEnv) - if is_native and envs_per_worker != 1: - raise APIUsageError('Native PufferEnvs should run multiple envs internally, not in Multiprocessing') self.emulated = False if is_native else driver_env.emulated self.num_agents = num_agents = driver_env.num_agents * num_envs self.agents_per_batch = driver_env.num_agents * batch_size @@ -434,6 +423,32 @@ def async_reset(self, seed=42): self.send_pipes[i].send(seed[start:end]) def close(self): + ''' + while self.waiting_workers: + worker = self.waiting_workers.pop(0) + sem = self.buf.semaphores[worker] + if sem >= MAIN: + self.ready_workers.append(worker) + if sem == INFO: + self.recv_pipes[worker].recv() + else: + self.waiting_workers.append(worker) + + self.buf.semaphores[:] = CLOSE + self.waiting_workers = list(range(self.num_workers)) + + while self.waiting_workers: + worker = self.waiting_workers.pop(0) + sem = self.buf.semaphores[worker] + if sem >= MAIN: + self.ready_workers.append(worker) + if sem == INFO: + self.recv_pipes[worker].recv() + + else: + self.waiting_workers.append(worker) + ''' + for p in self.processes: p.terminate() @@ -577,6 +592,8 @@ def make(env_creator_or_creators, env_args=None, env_kwargs=None, backend=Puffer vecenv = env_creator_or_creators(*env_args, **env_kwargs) if not isinstance(vecenv, PufferEnv): raise APIUsageError('Native vectorization requires a native PufferEnv. Use Serial or Multiprocessing instead.') + if num_envs != 1: + raise APIUsageError('Native vectorization is for PufferEnvs that handle all per-process vectorization internally. If you want to run multiple separate Python instances on a single process, use Serial or Multiprocessing instead') return vecenv diff --git a/pufferlib/version.py b/pufferlib/version.py index 1f356cc5..e7c12d28 100644 --- a/pufferlib/version.py +++ b/pufferlib/version.py @@ -1 +1 @@ -__version__ = '1.0.0' +__version__ = '2.0.3' diff --git a/pyproject.toml b/pyproject.toml index 62bebecf..b4d35b40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools==65.5.0", "wheel", "Cython", "numpy"] -build-backend = "setuptools.build_meta" \ No newline at end of file +requires = ["setuptools", "wheel", "Cython", "numpy"] +build-backend = "setuptools.build_meta" diff --git a/resources/puffer_enduro/enduro_weights.bin b/resources/puffer_enduro/enduro_weights.bin deleted file mode 100644 index b471affd..00000000 Binary files a/resources/puffer_enduro/enduro_weights.bin and /dev/null differ diff --git a/resources/puffer_enduro/gamma_0.940001_weights.bin b/resources/puffer_enduro/gamma_0.940001_weights.bin deleted file mode 100644 index f88ad62b..00000000 Binary files a/resources/puffer_enduro/gamma_0.940001_weights.bin and /dev/null differ diff --git a/scripts/build_ocean.sh b/scripts/build_ocean.sh index 7dbdacc8..563ca529 100755 --- a/scripts/build_ocean.sh +++ b/scripts/build_ocean.sh @@ -16,7 +16,7 @@ if [ "$MODE" = "web" ]; then emcc \ -o "$WEB_OUTPUT_DIR/game.html" \ "$SRC_DIR/$ENV.c" \ - -Os \ + -O3 \ -Wall \ ./raylib_wasm/lib/libraylib.a \ -I./raylib_wasm/include \ @@ -30,12 +30,12 @@ if [ "$MODE" = "web" ]; then -s ASYNCIFY \ -sFILESYSTEM \ -s FORCE_FILESYSTEM=1 \ - --shell-file ./minshell.html \ + --shell-file ./scripts/minshell.html \ -sINITIAL_MEMORY=512MB \ -sSTACK_SIZE=512KB \ -DPLATFORM_WEB \ -DGRAPHICS_API_OPENGL_ES3 \ - --preload-file resources@resources/ + --preload-file pufferlib/resources@resources/ echo "Web build completed: $WEB_OUTPUT_DIR/game.html" exit 0 fi @@ -48,6 +48,7 @@ FLAGS=( ./raylib/lib/libraylib.a -lm -lpthread + -DPLATFORM_DESKTOP ) diff --git a/scripts/minshell.html b/scripts/minshell.html new file mode 100644 index 00000000..4068ca36 --- /dev/null +++ b/scripts/minshell.html @@ -0,0 +1,89 @@ + + + + + + + raylib web game + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + {{{ SCRIPT }}} + + diff --git a/setup.py b/setup.py index 91c19218..1094c7a8 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ # python3 setup.py built_ext --inplace -VERSION = '1.0.0' +VERSION = '2.0.3' RAYLIB_BASE = 'https://github.com/raysan5/raylib/releases/download/5.0/' RAYLIB_NAME = 'raylib-5.0_macos' if platform.system() == "Darwin" else 'raylib-5.0_linux_amd64' @@ -66,7 +66,7 @@ 'tensorboard==2.11.2', 'torch', 'tyro==0.8.6', - 'wandb==0.13.7', + 'wandb==0.19.1', ] ray = [ @@ -242,7 +242,9 @@ ]] extension_paths = [ + 'pufferlib/ocean/nmmo3/cy_nmmo3', 'pufferlib/ocean/moba/cy_moba', + 'pufferlib/ocean/tactical/c_tactical', 'pufferlib/ocean/squared/cy_squared', 'pufferlib/ocean/snake/cy_snake', 'pufferlib/ocean/pong/cy_pong', @@ -253,8 +255,22 @@ 'pufferlib/ocean/tripletriad/cy_tripletriad', 'pufferlib/ocean/go/cy_go', 'pufferlib/ocean/rware/cy_rware', + 'pufferlib/ocean/trash_pickup/cy_trash_pickup' ] +system = platform.system() +if system == 'Darwin': + # On macOS, use @loader_path. + # The extension “.so” is typically in pufferlib/ocean/..., + # and “raylib/lib” is (maybe) two directories up from ocean/. + # So @loader_path/../../raylib/lib is common. + rpath_arg = '-Wl,-rpath,@loader_path/../../raylib/lib' +elif system == 'Linux': + # On Linux, $ORIGIN works + rpath_arg = '-Wl,-rpath,$ORIGIN/raylib/lib' +else: + raise ValueError(f'Unsupported system: {system}') + extensions = [Extension( path.replace('/', '.'), [path + '.pyx'], @@ -263,6 +279,8 @@ libraries=["raylib"], runtime_library_dirs=["raylib/lib"], extra_compile_args=['-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION', '-DPLATFORM_DESKTOP', '-O2', '-Wno-alloc-size-larger-than'],#, '-g'], + extra_link_args=[rpath_arg] + ) for path in extension_paths] setup( @@ -272,6 +290,9 @@ long_description_content_type="text/markdown", version=VERSION, packages=find_packages(), + package_data={ + "pufferlib": ["raylib/lib/libraylib.so.500", "raylib/lib/libraylib.so"] + }, include_package_data=True, install_requires=[ 'numpy==1.23.3', @@ -315,7 +336,7 @@ #compiler_directives={'profile': True},# annotate=True ), include_dirs=[numpy.get_include(), 'raylib-5.0_linux_amd64/include'], - python_requires=">=3.8", + python_requires=">=3.9", license="MIT", author="Joseph Suarez", author_email="jsuarez@puffer.ai", diff --git a/tests/test_puffernet.py b/tests/test_puffernet.py index 1d212503..7f90b426 100644 --- a/tests/test_puffernet.py +++ b/tests/test_puffernet.py @@ -1,7 +1,7 @@ import torch import numpy as np -from pufferlib.environments.ocean.moba import puffernet +from pufferlib import puffernet # TODO: Should probably add a safe mode that type checks input arrays # It's user error, but it is a big foot gun @@ -12,8 +12,8 @@ def make_dummy_data(*shape, seed=42): ary = np.random.rand(*shape).astype(np.float32) - 0.5 return np.ascontiguousarray(ary) -def make_dummy_int_data(num_classes, *shape): - np.random.seed(42) +def make_dummy_int_data(num_classes, *shape, seed=42): + np.random.seed(seed) n = np.prod(shape) ary = np.random.randint(0, num_classes, shape).astype(np.int32) return np.ascontiguousarray(ary) @@ -117,6 +117,25 @@ def test_puffernet_lstm(batch_size=16, input_size=128, hidden_size=128): assert_near(state_h_np, state_h_torch.numpy()[0]) assert_near(state_c_np, state_c_torch.numpy()[0]) +def test_puffernet_embedding(batch_size=16, num_embeddings=128, embedding_dim=32): + input_np = make_dummy_int_data(num_embeddings, batch_size, seed=42) + weights_np = make_dummy_data(num_embeddings, embedding_dim, seed=43) + output_puffer = np.zeros((batch_size, embedding_dim), dtype=np.float32) + puffernet.puf_embedding(input_np, weights_np, output_puffer, + batch_size, num_embeddings, embedding_dim) + + input_torch = torch.from_numpy(input_np).long() + weights_torch = torch.from_numpy(weights_np) + output_torch = torch.nn.functional.embedding(input_torch, weights_torch).detach() + + input_torch = torch.from_numpy(input_np).long() + weights_torch = torch.from_numpy(weights_np) + torch_embedding = torch.nn.Embedding(num_embeddings, embedding_dim) + torch_embedding.weight.data = weights_torch + output_torch = torch_embedding(input_torch).detach() + + assert_near(output_puffer, output_torch.numpy()) + def test_puffernet_one_hot(batch_size=16, input_size=128, num_classes=10): input_np = make_dummy_int_data(num_classes, batch_size, input_size) output_puffer = np.zeros((batch_size, input_size, num_classes), dtype=np.int32) @@ -158,6 +177,7 @@ def test_puffernet_argmax_multidiscrete(batch_size=16, logit_sizes=[5,7,2]): test_puffernet_linear_layer() test_puffernet_convolution_layer() test_puffernet_lstm() + test_puffernet_embedding() test_puffernet_one_hot() test_puffernet_cat_dim1() test_puffernet_argmax_multidiscrete()