diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml
index ee097ee501..a7ab3a8f04 100644
--- a/.github/workflows/install.yml
+++ b/.github/workflows/install.yml
@@ -17,7 +17,6 @@ jobs:
py:
- "3.11"
- "3.10"
- - "3.9"
env:
- pip
- conda
diff --git a/.gitignore b/.gitignore
index f9082380e0..9517198506 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,10 @@
c_*.c
pufferlib/extensions.c
pufferlib/puffernet.c
+logs/
+
+# Build dir
+build/
# hipified cuda extensions dir [HIP/ROCM]
pufferlib/extensions/hip/
@@ -18,6 +22,7 @@ cy_*.c
# C extensions
*.so
+*.o
# Distribution / packaging
.Python
@@ -162,3 +167,10 @@ pufferlib/ocean/impulse_wars/*-release/
pufferlib/ocean/impulse_wars/debug-*/
pufferlib/ocean/impulse_wars/release-*/
pufferlib/ocean/impulse_wars/benchmark/
+
+# Data
+resources/drive/data/*
+resources/drive/binaries/*
+
+vendor/nle/
+vendor/fast-nle/
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 14b53eb750..0000000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,19 +0,0 @@
-global-include *.pyx
-global-include *.pxd
-global-include *.h
-global-include *.cpp
-global-include *.cu
-global-include *.py
-recursive-include pufferlib/config *.ini
-recursive-include pufferlib/resources *
-recursive-exclude experiments *
-recursive-exclude wandb *
-recursive-exclude tests *
-include raylib-5.5_linux_amd64/lib/libraylib.a
-include raylib-5.5_macos/lib/libraylib.a
-include box2d-linux-amd64/libbox2d.a
-recursive-exclude box2d-web *
-recursive-exclude raylib-5.5_webassembly *
-recursive-exclude pufferlib/ocean/impulse_wars/debug* *
-recursive-exclude pufferlib/ocean/impulse_wars/release* *
-recursive-exclude box2d* *
diff --git a/README.md b/README.md
index dc1bfaa81f..cb6f72658d 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,18 @@

-[](https://badge.fury.io/py/pufferlib)
-
-
-[](https://discord.gg/spT4huaGYV)
-[](https://twitter.com/jsuarez5341)
+[](https://discord.gg/spT4huaGYV)
+[](https://twitter.com/jsuarez)
-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!
+PufferLib is a fast and sane reinforcement learning library that can train tiny, super-human models in seconds. The included learning algorithm, hyperparameter tuning, and simulation methods are the product of our own research. All our tools are free and open source. Need a high performance environment for your application? We build them professionally and offer training + extended support. Contact jsuarez🐡puffer🐡ai.
-
-
-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!
+All of our documentation is hosted at [puffer.ai](https://puffer.ai "PufferLib Documentation"). @jsuarez5341 on [Discord](https://discord.gg/puffer) for support. Post there before opening issues. We're always looking for new contributors!
## Star to puff up the project!
-
+
-
-
-
+
+
+
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000000..19261e88c6
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,350 @@
+#!/bin/bash
+set -e
+
+# Usage:
+# ./build.sh breakout # Build _C.so with breakout statically linked
+# ./build.sh breakout --float # float32 precision (required for --slowly)
+# ./build.sh breakout --cpu # CPU fallback, torch only
+# ./build.sh breakout --debug # Debug build
+# ./build.sh breakout --local # Standalone executable (debug, sanitizers)
+# ./build.sh breakout --fast # Standalone executable (optimized)
+# ./build.sh breakout --web # Emscripten web build
+# ./build.sh breakout --profile # Kernel profiling binary
+# ./build.sh all # Build all envs with default and --float
+
+if [ -z "$1" ]; then
+ echo "Usage: ./build.sh ENV_NAME [--float] [--debug] [--local|--fast|--web|--profile|--cpu|--all]"
+ exit 1
+fi
+ENV=$1
+shift
+
+for arg in "$@"; do
+ case $arg in
+ --float) PRECISION="-DPRECISION_FLOAT" ;;
+ --debug) DEBUG=1 ;;
+ --local) MODE=local ;;
+ --fast) MODE=fast ;;
+ --web) MODE=web ;;
+ --profile) MODE=profile ;;
+ --cpu) MODE=cpu; PRECISION="-DPRECISION_FLOAT" ;;
+ *) echo "Error: unknown argument '$arg'" && exit 1 ;;
+ esac
+done
+
+if [ "$ENV" = "all" ]; then
+ FAILED=""
+ for env_dir in ocean/*/; do
+ env=$(basename "$env_dir")
+ if bash "$0" "$env" && bash "$0" "$env" --float; then
+ echo "OK: $env"
+ else
+ echo "FAIL: $env"
+ FAILED="$FAILED\n $env"
+ fi
+ done
+
+ if [ -n "$FAILED" ]; then
+ echo -e "\nFailed builds:$FAILED"
+ fi
+ exit 0
+fi
+
+# Linux/mac
+PLATFORM="$(uname -s)"
+if [ "$PLATFORM" = "Linux" ]; then
+ RAYLIB_NAME='raylib-5.5_linux_amd64'
+ OMP_LIB=-lomp5
+ SANITIZE_FLAGS=(-fsanitize=address,undefined,bounds,pointer-overflow,leak -fno-omit-frame-pointer)
+ STANDALONE_LDFLAGS=(-lGL)
+ SHARED_LDFLAGS=(-Bsymbolic-functions)
+else
+ RAYLIB_NAME='raylib-5.5_macos'
+ OMP_LIB=-lomp
+ SANITIZE_FLAGS=()
+ STANDALONE_LDFLAGS=(-framework Cocoa -framework IOKit -framework CoreVideo -framework OpenGL)
+ SHARED_LDFLAGS=(-framework Cocoa -framework OpenGL -framework IOKit -undefined dynamic_lookup)
+fi
+
+CLANG_WARN=(
+ -Wall
+ -ferror-limit=3
+ -Werror=incompatible-pointer-types
+ -Werror=return-type
+ -Wno-error=incompatible-pointer-types-discards-qualifiers
+ -Wno-incompatible-pointer-types-discards-qualifiers
+ -Wno-error=array-parameter
+)
+
+download() {
+ local name=$1 url=$2
+ [ -d "$name" ] && return
+ echo "Downloading $name..."
+ case "$url" in
+ *.zip) curl -sL "$url" -o "$name.zip" && unzip -q "$name.zip" && rm "$name.zip" ;;
+ *) curl -sL "$url" -o "$name.tar.gz" && tar xf "$name.tar.gz" && rm "$name.tar.gz" ;;
+ esac
+}
+
+RAYLIB_URL="https://github.com/raysan5/raylib/releases/download/5.5"
+if [ "$MODE" = "web" ]; then
+ RAYLIB_NAME='raylib-5.5_webassembly'
+ download "$RAYLIB_NAME" "$RAYLIB_URL/$RAYLIB_NAME.zip"
+else
+ download "$RAYLIB_NAME" "$RAYLIB_URL/$RAYLIB_NAME.tar.gz"
+fi
+
+RAYLIB_A="$RAYLIB_NAME/lib/libraylib.a"
+INCLUDES=(-I./$RAYLIB_NAME/include -I./src -I./vendor)
+LINK_ARCHIVES=("$RAYLIB_A")
+EXTRA_SRC=""
+EXTRA_LDFLAGS=()
+
+if [ "$ENV" = "constellation" ]; then
+ SRC_DIR="constellation"
+ EXTRA_SRC="vendor/cJSON.c"
+ OUTPUT_NAME="seethestars"
+elif [ "$ENV" = "trailer" ]; then
+ SRC_DIR="trailer"
+ OUTPUT_NAME="trailer/trailer"
+elif [ "$ENV" = "impulse_wars" ]; then
+ SRC_DIR="ocean/$ENV"
+ if [ "$MODE" = "web" ]; then BOX2D_NAME='box2d-web'
+ elif [ "$PLATFORM" = "Linux" ]; then BOX2D_NAME='box2d-linux-amd64'
+ else BOX2D_NAME='box2d-macos-arm64'
+ fi
+ BOX2D_URL="https://github.com/capnspacehook/box2d/releases/latest/download"
+ download "$BOX2D_NAME" "$BOX2D_URL/$BOX2D_NAME.tar.gz"
+ INCLUDES+=(-I./$BOX2D_NAME/include -I./$BOX2D_NAME/src)
+ LINK_ARCHIVES+=("./$BOX2D_NAME/libbox2d.a")
+elif [ "$ENV" = "nethack" ]; then
+ SRC_DIR="ocean/$ENV"
+ NLE_DIR="vendor/fast-nle"
+ NLE_REPO="https://github.com/FinlaySanders/fast-nle.git"
+ if [ ! -d "$NLE_DIR/src" ]; then
+ echo "Cloning fast-nle from $NLE_REPO ..."
+ git clone --depth 1 "$NLE_REPO" "$NLE_DIR"
+ fi
+ NETHACK_LIB_DIR="$(pwd)/$NLE_DIR/build"
+ if [ ! -f "$NETHACK_LIB_DIR/libnethack.so" ]; then
+ echo "Building libnethack.so ..."
+ cmake -S "$NLE_DIR" -B "$NETHACK_LIB_DIR" -DCMAKE_BUILD_TYPE=Release
+ cmake --build "$NETHACK_LIB_DIR" --target nethack -j$(nproc)
+ fi
+ INCLUDES+=(-I./$NLE_DIR/include
+ -I./$NLE_DIR/build/_deps/deboost_context-src/include)
+ EXTRA_LDFLAGS+=(-L"$NETHACK_LIB_DIR" -lnethack -Wl,-rpath,"$NETHACK_LIB_DIR" -ldl)
+elif [ -d "ocean/$ENV" ]; then
+ SRC_DIR="ocean/$ENV"
+else
+ echo "Error: environment '$ENV' not found" && exit 1
+fi
+
+OUTPUT_NAME=${OUTPUT_NAME:-$ENV}
+
+# Standalone environment build
+# -mavx2 enables AVX2 intrinsics (__m256, _mm256_*) which drive.h and
+# src/bf16.h use directly. x86_64 only — strip if porting to ARM/Apple Silicon.
+SIMD_FLAGS=(-mavx2 -mfma)
+if [ -n "$DEBUG" ] || [ "$MODE" = "local" ]; then
+ CLANG_OPT=(-g -O0 "${CLANG_WARN[@]}" "${SANITIZE_FLAGS[@]}" "${SIMD_FLAGS[@]}")
+ NVCC_OPT="-O0 -g"
+ LINK_OPT="-g"
+else
+ CLANG_OPT=(-O2 -DNDEBUG "${CLANG_WARN[@]}" "${SIMD_FLAGS[@]}")
+ NVCC_OPT="-O2 --threads 0"
+ LINK_OPT="-O2"
+fi
+if [ "$MODE" = "local" ] || [ "$MODE" = "fast" ]; then
+ FLAGS=(
+ "${INCLUDES[@]}"
+ "$SRC_DIR/$ENV.c" $EXTRA_SRC -o "$OUTPUT_NAME"
+ "${LINK_ARCHIVES[@]}"
+ "${EXTRA_LDFLAGS[@]}"
+ "${STANDALONE_LDFLAGS[@]}"
+ -lm -lpthread -fopenmp
+ -DPLATFORM_DESKTOP
+ )
+ echo "Compiling $ENV..."
+ ${CC:-clang} "${CLANG_OPT[@]}" "${FLAGS[@]}"
+ echo "Built: ./$OUTPUT_NAME"
+ exit 0
+elif [ "$MODE" = "web" ]; then
+ mkdir -p "build/web/$ENV"
+ echo "Compiling $ENV for web..."
+ emcc \
+ -o "build/web/$ENV/game.html" \
+ "$SRC_DIR/$ENV.c" $EXTRA_SRC \
+ -O3 -Wall \
+ "${LINK_ARCHIVES[@]}" \
+ "${INCLUDES[@]}" \
+ -L. -L./$RAYLIB_NAME/lib \
+ -sASSERTIONS=2 -gsource-map \
+ -sUSE_GLFW=3 -sUSE_WEBGL2=1 -sASYNCIFY -sFILESYSTEM -sFORCE_FILESYSTEM=1 \
+ --shell-file vendor/minshell.html \
+ -sINITIAL_MEMORY=512MB -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=512KB \
+ -DNDEBUG -DPLATFORM_WEB -DGRAPHICS_API_OPENGL_ES3 \
+ --preload-file resources/$ENV@resources/$ENV \
+ --preload-file resources/shared@resources/shared
+ echo "Built: build/web/$ENV/game.html"
+ exit 0
+fi
+
+# Find cuDNN path
+CUDA_HOME=${CUDA_HOME:-${CUDA_PATH:-$(dirname "$(dirname "$(which nvcc)")")}}
+CUDNN_IFLAG=""
+CUDNN_LFLAG=""
+for dir in /usr/local/cuda/include /usr/include; do
+ if [ -f "$dir/cudnn.h" ]; then
+ CUDNN_IFLAG="-I$dir"
+ break
+ fi
+done
+for dir in /usr/local/cuda/lib64 /usr/lib/x86_64-linux-gnu; do
+ if [ -f "$dir/libcudnn.so" ]; then
+ CUDNN_LFLAG="-L$dir"
+ break
+ fi
+done
+if [ -z "$CUDNN_IFLAG" ]; then
+ CUDNN_IFLAG=$(python -c "import nvidia.cudnn, os; print('-I' + os.path.join(nvidia.cudnn.__path__[0], 'include'))" 2>/dev/null || echo "")
+fi
+if [ -z "$CUDNN_LFLAG" ]; then
+ CUDNN_LFLAG=$(python -c "import nvidia.cudnn, os; print('-L' + os.path.join(nvidia.cudnn.__path__[0], 'lib'))" 2>/dev/null || echo "")
+fi
+
+# NCCL include/lib fallback (mirrors the cuDNN fallback above).
+# Needed when NCCL is provided by the nvidia-nccl-cu12 wheel in the active venv.
+NCCL_IFLAG=""
+NCCL_LFLAG=""
+for dir in /usr/include /usr/local/cuda/include; do
+ if [ -f "$dir/nccl.h" ]; then NCCL_IFLAG="-I$dir"; break; fi
+done
+for dir in /usr/lib/x86_64-linux-gnu /usr/local/cuda/lib64; do
+ if [ -f "$dir/libnccl.so" ] || [ -f "$dir/libnccl.so.2" ]; then NCCL_LFLAG="-L$dir"; break; fi
+done
+if [ -z "$NCCL_IFLAG" ]; then
+ NCCL_IFLAG=$(python -c "import nvidia.nccl, os; print('-I' + os.path.join(nvidia.nccl.__path__[0], 'include'))" 2>/dev/null || echo "")
+fi
+if [ -z "$NCCL_LFLAG" ]; then
+ NCCL_LFLAG=$(python -c "import nvidia.nccl, os; print('-L' + os.path.join(nvidia.nccl.__path__[0], 'lib'))" 2>/dev/null || echo "")
+fi
+
+WHEEL_RPATH_FLAGS=()
+for lib_flag in "$CUDNN_LFLAG" "$NCCL_LFLAG"; do
+ if [[ "$lib_flag" == -L* ]]; then
+ WHEEL_RPATH_FLAGS+=("-Wl,-rpath,${lib_flag#-L}")
+ fi
+done
+
+export CCACHE_DIR="${CCACHE_DIR:-$HOME/.ccache}"
+export CCACHE_BASEDIR="$(pwd)"
+export CCACHE_COMPILERCHECK=content
+NVCC="ccache $CUDA_HOME/bin/nvcc"
+CC="${CC:-$(command -v ccache >/dev/null && echo 'ccache clang' || echo 'clang')}"
+ARCH=${NVCC_ARCH:-native}
+
+PYTHON_INCLUDE=$(python -c "import sysconfig; print(sysconfig.get_path('include'))")
+PYBIND_INCLUDE=$(python -c "import pybind11; print(pybind11.get_include())")
+NUMPY_INCLUDE=$(python -c "import numpy; print(numpy.get_include())")
+EXT_SUFFIX=$(python -c "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")
+OUTPUT="pufferlib/_C${EXT_SUFFIX}"
+
+BINDING_SRC="$SRC_DIR/binding.c"
+mkdir -p build
+STATIC_OBJ="build/libstatic_${ENV}.o"
+STATIC_LIB="build/libstatic_${ENV}.a"
+
+if [ ! -f "$BINDING_SRC" ]; then
+ echo "Error: $BINDING_SRC not found"
+ exit 1
+fi
+
+echo "Compiling static library for $ENV..."
+${CC:-clang} -c "${CLANG_OPT[@]}" $EXTRA_CFLAGS \
+ -I. -Isrc -I$SRC_DIR -Ivendor \
+ "${INCLUDES[@]}" \
+ -I./$RAYLIB_NAME/include -I$CUDA_HOME/include \
+ -DPLATFORM_DESKTOP \
+ -fno-semantic-interposition -fvisibility=hidden \
+ -fPIC -fopenmp \
+ "$BINDING_SRC" -o "$STATIC_OBJ"
+ar rcs "$STATIC_LIB" "$STATIC_OBJ"
+
+# Brittle hack: have to extract the tensor type from the static lib to build trainer
+OBS_TENSOR_T=$(awk '/^#define OBS_TENSOR_T/{print $3}' "$BINDING_SRC")
+if [ -z "$OBS_TENSOR_T" ]; then
+ echo "Error: Could not find OBS_TENSOR_T in $BINDING_SRC"
+ exit 1
+fi
+
+if [ -z "$MODE" ]; then
+ echo "Compiling CUDA ($ARCH) training backend..."
+ $NVCC -c -arch=$ARCH -Xcompiler -fPIC \
+ -Xcompiler=-D_GLIBCXX_USE_CXX11_ABI=1 \
+ -Xcompiler=-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION \
+ -Xcompiler=-DPLATFORM_DESKTOP \
+ -std=c++17 \
+ -I. -Isrc \
+ -I$PYTHON_INCLUDE -I$PYBIND_INCLUDE -I$NUMPY_INCLUDE \
+ -I$CUDA_HOME/include $CUDNN_IFLAG $NCCL_IFLAG -I$RAYLIB_NAME/include \
+ -Xcompiler=-fopenmp \
+ -DOBS_TENSOR_T=$OBS_TENSOR_T \
+ -DENV_NAME=$ENV \
+ $PRECISION $NVCC_OPT \
+ src/bindings.cu -o build/bindings.o
+
+ LINK_CMD=(
+ ${CXX:-g++} -shared -fPIC -fopenmp
+ build/bindings.o "$STATIC_LIB" "$RAYLIB_A"
+ -L$CUDA_HOME/lib64 $CUDNN_LFLAG $NCCL_LFLAG
+ "${WHEEL_RPATH_FLAGS[@]}"
+ "${EXTRA_LDFLAGS[@]}"
+ -lcudart -lnccl -lnvidia-ml -lcublas -lcusolver -lcurand -lcudnn
+ $OMP_LIB $LINK_OPT
+ "${SHARED_LDFLAGS[@]}"
+ -o "$OUTPUT"
+ )
+ "${LINK_CMD[@]}"
+ echo "Built: $OUTPUT"
+
+elif [ "$MODE" = "cpu" ]; then
+ echo "Compiling CPU training backend..."
+ ${CXX:-g++} -c -fPIC -fopenmp \
+ -D_GLIBCXX_USE_CXX11_ABI=1 \
+ -DPLATFORM_DESKTOP \
+ -std=c++17 \
+ -I. -Isrc \
+ -I$PYTHON_INCLUDE -I$PYBIND_INCLUDE \
+ -DOBS_TENSOR_T=$OBS_TENSOR_T \
+ -DENV_NAME=$ENV \
+ $PRECISION $LINK_OPT \
+ src/bindings_cpu.cpp -o build/bindings_cpu.o
+ LINK_CMD=(
+ ${CXX:-g++} -shared -fPIC -fopenmp
+ build/bindings_cpu.o "$STATIC_LIB" "$RAYLIB_A"
+ "${EXTRA_LDFLAGS[@]}"
+ -lm -lpthread $OMP_LIB $LINK_OPT
+ "${SHARED_LDFLAGS[@]}"
+ -o "$OUTPUT"
+ )
+ "${LINK_CMD[@]}"
+ echo "Built: $OUTPUT"
+
+elif [ "$MODE" = "profile" ]; then
+ echo "Compiling profile binary ($ARCH)..."
+ $NVCC $NVCC_OPT -arch=$ARCH -std=c++17 \
+ -I. -Isrc -I$SRC_DIR -Ivendor \
+ -I$CUDA_HOME/include $CUDNN_IFLAG $NCCL_IFLAG -I$RAYLIB_NAME/include \
+ -DOBS_TENSOR_T=$OBS_TENSOR_T \
+ -DENV_NAME=$ENV \
+ -Xcompiler=-DPLATFORM_DESKTOP \
+ $PRECISION \
+ -Xcompiler=-fopenmp \
+ tests/profile_kernels.cu vendor/ini.c \
+ "$STATIC_LIB" "$RAYLIB_A" \
+ -lnccl -lnvidia-ml -lcublas -lcurand -lcudnn \
+ -lGL -lm -lpthread $OMP_LIB \
+ -o profile
+ echo "Built: ./profile"
+fi
diff --git a/config b/config
deleted file mode 120000
index 662c3e7429..0000000000
--- a/config
+++ /dev/null
@@ -1 +0,0 @@
-pufferlib/config/
\ No newline at end of file
diff --git a/pufferlib/config/ocean/asteroids.ini b/config/asteroids.ini
similarity index 87%
rename from pufferlib/config/ocean/asteroids.ini
rename to config/asteroids.ini
index 754d624184..1413b1d8af 100644
--- a/pufferlib/config/ocean/asteroids.ini
+++ b/config/asteroids.ini
@@ -1,8 +1,5 @@
[base]
-package = ocean
-env_name = puffer_asteroids
-policy_name = Policy
-rnn_name = Recurrent
+env_name = asteroids
[vec]
num_envs = 8
@@ -17,7 +14,7 @@ adam_beta2 = 0.9999436458974764
adam_eps = 6.915036275112011e-08
anneal_lr = true
batch_size = auto
-bptt_horizon = 64
+horizon = 64
checkpoint_interval = 200
clip_coef = 0.18588778503512546
ent_coef = 0.0016620361911332262
diff --git a/pufferlib/config/ocean/battle.ini b/config/battle.ini
similarity index 91%
rename from pufferlib/config/ocean/battle.ini
rename to config/battle.ini
index dcb4baa0f5..ddf3bcd3f3 100644
--- a/pufferlib/config/ocean/battle.ini
+++ b/config/battle.ini
@@ -1,16 +1,9 @@
[base]
-package = ocean
-env_name = puffer_battle
-policy_name = Policy
-rnn_name = Recurrent
+env_name = battle
[policy]
hidden_size = 512
-[rnn]
-input_size = 512
-hidden_size = 512
-
[vec]
num_envs = 16
diff --git a/config/benchmark.ini b/config/benchmark.ini
new file mode 100644
index 0000000000..93c87684d5
--- /dev/null
+++ b/config/benchmark.ini
@@ -0,0 +1,18 @@
+[base]
+env_name = benchmark
+
+[env]
+bandwidth = 512
+compute = 0
+
+[vec]
+total_agents = 8192
+num_buffers = 2
+
+[train]
+total_timesteps = 100_000_000
+gamma = 0.99
+learning_rate = 0.015
+minibatch_size = 32768
+ent_coef = 0.02
+
diff --git a/config/blastar.ini b/config/blastar.ini
new file mode 100644
index 0000000000..c91bf130b8
--- /dev/null
+++ b/config/blastar.ini
@@ -0,0 +1,23 @@
+[base]
+env_name = blastar
+
+[vec]
+total_agents = 4096
+
+[env]
+num_obs = 10
+
+[train]
+total_timesteps = 200_000_000
+gamma = 0.95
+learning_rate = 0.05
+minibatch_size = 32768
+
+[sweep]
+metric = environment/enemy_crossed_screen
+goal = minimize
+
+[sweep.parameters.train.parameters.total_timesteps]
+distribution = uniform
+min = 10_000_000
+max = 100_000_000
diff --git a/pufferlib/config/ocean/boids.ini b/config/boids.ini
similarity index 90%
rename from pufferlib/config/ocean/boids.ini
rename to config/boids.ini
index 2f8412d248..d685a42c6b 100644
--- a/pufferlib/config/ocean/boids.ini
+++ b/config/boids.ini
@@ -1,9 +1,5 @@
[base]
-package = ocean
-env_name = puffer_boids
-policy_name = Boids
-rnn_name = Recurrent
-; rnn_name = None
+env_name = boids
[env]
num_envs = 64
diff --git a/config/boxoban.ini b/config/boxoban.ini
new file mode 100644
index 0000000000..f076ab74b6
--- /dev/null
+++ b/config/boxoban.ini
@@ -0,0 +1,44 @@
+[base]
+env_name = boxoban
+
+[vec]
+total_agents = 32768
+num_buffers = 4
+num_threads = 1
+
+[env]
+num_agents = 1
+difficulty = 2
+int_r_coeff = 0.25
+target_loss_pen_coeff = 0
+max_steps = 150
+
+[policy]
+hidden_size = 512
+num_layers = 3.32422
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 880580001
+learning_rate = 0.00134234
+anneal_lr = 1
+min_lr_ratio = 0.37872
+gamma = 0.989717
+gae_lambda = 0.759273
+replay_ratio = 1.6234
+clip_coef = 0.01
+vf_coef = 5
+vf_clip_coef = 5
+max_grad_norm = 1.20325
+ent_coef = 0.000188411
+beta1 = 0.995526
+beta2 = 0.999536
+eps = 1e-14
+minibatch_size = 32768
+horizon = 64
+vtrace_rho_clip = 3.13347
+vtrace_c_clip = 2.75328
+prio_alpha = 0.453827
+prio_beta0 = 0.765589
diff --git a/config/breakout.ini b/config/breakout.ini
new file mode 100644
index 0000000000..7da69ca2b8
--- /dev/null
+++ b/config/breakout.ini
@@ -0,0 +1,125 @@
+[base]
+env_name = breakout
+
+[vec]
+total_agents = 4096
+num_buffers = 8
+num_threads = 8
+
+[policy]
+num_layers = 2
+hidden_size = 64
+
+[env]
+num_agents = 1
+frameskip = 4
+width = 576
+height = 330
+initial_paddle_width = 62
+paddle_width = 62
+paddle_height = 8
+ball_width = 32
+ball_height = 32
+brick_width = 32
+brick_height = 12
+brick_rows = 6
+brick_cols = 18
+initial_ball_speed = 256
+max_ball_speed = 448
+paddle_speed = 620
+continuous = 0
+
+[train]
+total_timesteps = 94_000_000
+beta1 = 0.7279714073125252
+beta2 = 0.9986265112492152
+clip_coef = 0.6746497927896418
+ent_coef = 0.0033240721522812535
+eps = 0.00008339460257113628
+gae_lambda = 0.948721675814334
+gamma = 0.9721246598992744
+learning_rate = 0.1
+max_grad_norm = 1.8109182724544075
+minibatch_size = 65_536
+prio_alpha = 0.1
+prio_beta0 = 0.8247156461060179
+replay_ratio = 1.4242098997083206
+vf_clip_coef = 1.2291681640124468
+vf_coef = 1.2195502588297364
+vtrace_c_clip = 1.0830442742115065
+vtrace_rho_clip = 2.1017317041552603
+
+#total_timesteps = 50_000_000
+#learning_rate = 0.045759
+#beta1 = 0.9542662897340632
+#beta2 = 0.9999020741216518
+#gamma = 0.998997162035256
+#gae_lambda = 0.5999999999999999
+#replay_ratio = 1.087305
+#clip_coef = 0.290339
+#vf_coef = 3.503804
+#vf_clip_coef = 1.700646
+#max_grad_norm = 0.269836
+#ent_coef = 0.013233
+#eps = 0.000012
+#minibatch_size = 32768.000000
+#horizon = 64.000000
+#vtrace_rho_clip = 5.000000
+#vtrace_c_clip = 4.825702
+#prio_alpha = 0.9804697934777868
+#prio_beta0 = 0.09999999999999998
+
+#total_timesteps = 120_000_000
+#adam_beta1 = 0.8166332218104871
+#adam_beta2 = 0.9984879989750705
+#adam_eps = 0.0001
+#batch_size = auto
+#horizon = 64
+#clip_coef = 0.42526610231849393
+#ent_coef = 0.0026822968018267775
+#gae_lambda = 0.995
+#gamma = 0.9731819086255716
+#learning_rate = 0.04301709139429238
+#max_grad_norm = 0.7029618837611082
+#minibatch_size = 16384
+#prio_alpha = 0.09999999999999998
+#prio_beta0 = 0.8437844355214735
+#vf_clip_coef = 0.807798225723059
+#vf_coef = 2.9089121311247554
+#vtrace_c_clip = 1.6205569942514606
+#vtrace_rho_clip = 1.1777184656786774
+
+#total_timesteps = 40_000_000
+#adam_beta1 = 0.9389740236912132
+#adam_beta2 = 0.9998225039929157
+#adam_eps = 1.0267361590791064e-8
+#batch_size = auto
+#horizon = 64
+#clip_coef = 0.01557913923814178
+#ent_coef = 0.0031759371032913
+#gae_lambda = 0.916681264452842
+#gamma = 0.9997053654668936
+#learning_rate = 0.012744235594115342
+#max_grad_norm = 1.8013800046071862
+#num_minibatches = 8
+#minibatch_size = 4096
+#prio_alpha = 0.9500430793857082
+#prio_beta0 = 0.9436845548994959
+#vf_clip_coef = 0.1
+#vf_coef = 2.5994729835919834
+#vtrace_c_clip = 2.878171091654008
+#vtrace_rho_clip = 1.3235791596831579
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 3e7
+max = 2e8
+mean = 8e7
+scale = auto
+
+[sweep.env.frameskip]
+distribution = int_uniform
+min = 1
+max = 8
+mean = 4
+scale = 2.0
diff --git a/config/cartpole.ini b/config/cartpole.ini
new file mode 100644
index 0000000000..8de0ad6147
--- /dev/null
+++ b/config/cartpole.ini
@@ -0,0 +1,57 @@
+[base]
+env_name = cartpole
+
+[vec]
+total_agents = 4096
+num_buffers = 4.78896
+num_threads = 16
+
+[env]
+cart_mass = 1
+pole_mass = 0.1
+pole_length = 0.5
+gravity = 9.8
+force_mag = 10
+dt = 0.02
+continuous = 0
+
+[policy]
+hidden_size = 32
+num_layers = 2.11327
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 5642560
+learning_rate = 0.1
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.8
+gae_lambda = 0.922151
+replay_ratio = 0.381289
+clip_coef = 0.143548
+vf_coef = 1.77975
+vf_clip_coef = 3.90833
+max_grad_norm = 0.329667
+ent_coef = 0.0367726
+beta1 = 0.942691
+beta2 = 0.907572
+eps = 4.6046e-09
+minibatch_size = 16384
+horizon = 32
+vtrace_rho_clip = 2.91145
+vtrace_c_clip = 1.66148
+prio_alpha = 0.786776
+prio_beta0 = 0.348617
+use_rnn = 1
+
+[sweep]
+method = Protein
+metric = perf
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 5e6
+max = 2e7
+mean = 1e7
diff --git a/config/chain_mdp.ini b/config/chain_mdp.ini
new file mode 100644
index 0000000000..5361efdb4a
--- /dev/null
+++ b/config/chain_mdp.ini
@@ -0,0 +1,17 @@
+[base]
+env_name = chain_mdp
+
+[vec]
+num_envs = 8
+
+[env]
+num_envs = 512
+size = 128
+
+[policy]
+hidden_size = 128
+
+[train]
+total_timesteps = 5_000_000
+horizon = 64
+entropy_coef = 0.1
\ No newline at end of file
diff --git a/config/checkers.ini b/config/checkers.ini
new file mode 100644
index 0000000000..63cdfd5fcc
--- /dev/null
+++ b/config/checkers.ini
@@ -0,0 +1,14 @@
+[base]
+env_name = checkers
+
+[env]
+num_envs = 4096
+size = 8
+
+[vec]
+num_envs = 8
+
+[train]
+total_timesteps = 1_000_000_000
+minibatch_size = 65536
+gamma = 0.95
diff --git a/config/chess.ini b/config/chess.ini
new file mode 100644
index 0000000000..92f9630b13
--- /dev/null
+++ b/config/chess.ini
@@ -0,0 +1,101 @@
+[base]
+env_name = chess
+
+[selfplay]
+enabled = 1
+max_size = 500
+swap_winrate = 0.6
+min_games = 4096
+snapshot_interval = 1_000_000_000
+opp_timeout_steps = 4_000_000_000
+
+[vec]
+total_agents = 8192
+num_buffers = 1
+num_threads = 2
+num_frozen_banks = 1
+frozen_bank_pct = 0.1
+
+[env]
+max_moves = 5000
+reward_draw = 0
+reward_invalid_piece = 0
+reward_invalid_move = 0
+reward_repetition = 0
+enable_50_move_rule = 1
+enable_threefold_repetition = 1
+mode = 1
+random_fen = 0
+render_fps = 30
+fen_curric_pct = 0.9
+
+[policy]
+hidden_size = 512
+num_layers = 3
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 1_000_000_000_000
+learning_rate = 0.000572786
+anneal_lr = 1
+min_lr_ratio = 0.086914
+gamma = 0.994795
+gae_lambda = 0.754641
+replay_ratio = 0.25
+clip_coef = 0.557019
+vf_coef = 4.37465
+vf_clip_coef = 1.69524
+max_grad_norm = 3.54293
+ent_coef = 0.0984801
+anneal_ent_coef = 1
+min_ent_coef_ratio = 0.1
+beta1 = 0.972205
+beta2 = 0.9
+eps = 8.51435e-11
+minibatch_size = 32768
+horizon = 64
+vtrace_rho_clip = 1.99404
+vtrace_c_clip = 2.19484
+prio_alpha = 1
+prio_beta0 = 0.474524
+torch_deterministic = 1
+adam_beta1 = 0.963206
+adam_beta2 = 0.99999
+adam_eps = 5.09326e-08
+update_epochs = 1
+[sweep]
+# Score each trial by winrate in a 2-policy match against a fixed enemy rather
+# than the training-time self-play env/score. 'latest' resolves to the newest
+# .bin in checkpoint_dir/chess/**. Enemy arch must match the checkpoint's arch.
+match_enemy_model_path = 'resources/chess/10b_weights.bin'
+match_num_games = 4096
+match_enemy_hidden_size = 512
+match_enemy_num_layers = 3
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 7e9
+max = 15e9
+mean = 10e9
+scale = time
+
+[sweep.selfplay.swap_winrate]
+distribution = uniform
+min = 0.55
+max = 0.90
+scale = auto
+
+[sweep.vec.num_buffers]
+distribution = uniform_pow2
+min = 1
+max = 8
+scale = auto
+
+[sweep.vec.frozen_bank_pct]
+distribution = uniform
+min = 0.05
+max = 0.5
+scale = auto
+
diff --git a/config/connect4.ini b/config/connect4.ini
new file mode 100644
index 0000000000..c7cf7d7e24
--- /dev/null
+++ b/config/connect4.ini
@@ -0,0 +1,50 @@
+[base]
+env_name = connect4
+
+[vec]
+total_agents = 4096
+num_buffers = 8
+num_threads = 2
+
+[env]
+num_agents = 1
+player_pieces = 0
+env_pieces = 0
+
+[policy]
+hidden_size = 256
+num_layers = 1
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 13272299
+learning_rate = 0.00847027
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.8
+gae_lambda = 0.962627
+replay_ratio = 3.16619
+clip_coef = 0.511829
+vf_coef = 5
+vf_clip_coef = 1.99178
+max_grad_norm = 0.552251
+ent_coef = 3.24222e-05
+beta1 = 0.878636
+beta2 = 0.986336
+eps = 3.02623e-07
+minibatch_size = 8192
+horizon = 32
+vtrace_rho_clip = 2.49786
+vtrace_c_clip = 1.52028
+prio_alpha = 1
+prio_beta0 = 0.746205
+use_rnn = 1
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1e7
+max = 2e8
+mean = 3e7
+scale = 0.5
diff --git a/pufferlib/config/ocean/continuous.ini b/config/continuous.ini
similarity index 78%
rename from pufferlib/config/ocean/continuous.ini
rename to config/continuous.ini
index fd35cd4854..3e63b3b899 100644
--- a/pufferlib/config/ocean/continuous.ini
+++ b/config/continuous.ini
@@ -1,6 +1,5 @@
[base]
-package = ocean
-env_name = puffer_continuous
+env_name = continuous
[train]
total_timesteps = 1_000_000
diff --git a/config/convert.ini b/config/convert.ini
new file mode 100644
index 0000000000..05b98568a1
--- /dev/null
+++ b/config/convert.ini
@@ -0,0 +1,20 @@
+[base]
+env_name = convert
+
+[vec]
+total_agents = 16384
+
+[env]
+num_agents = 1024
+width = 1920
+height = 1080
+num_factories = 32
+num_resources = 8
+
+[train]
+total_timesteps = 100_000_000
+gamma = 0.99
+learning_rate = 0.015
+minibatch_size = 32768
+ent_coef = 0.02
+
diff --git a/config/convert_circle.ini b/config/convert_circle.ini
new file mode 100644
index 0000000000..f4eeb7c880
--- /dev/null
+++ b/config/convert_circle.ini
@@ -0,0 +1,21 @@
+[base]
+env_name = convert_circle
+
+[vec]
+num_envs = 16
+
+[env]
+num_envs = 1
+num_agents = 1024
+num_factories = 32
+num_resources = 8
+equidistant = 1
+radius = 400
+
+[train]
+total_timesteps = 100_000_000
+gamma = 0.99
+learning_rate = 0.015
+minibatch_size = 32768
+ent_coef = 0.02
+
diff --git a/config/craftax.ini b/config/craftax.ini
new file mode 100644
index 0000000000..22e0787987
--- /dev/null
+++ b/config/craftax.ini
@@ -0,0 +1,30 @@
+[base]
+env_name = craftax
+
+[vec]
+total_agents = 16384
+num_buffers = 16
+num_threads = 16
+
+[env]
+seed_offset = 0
+# Pre-generated world pool. Each reset memcpys from a pool entry
+# instead of re-running generate_world (~ms -> ~us per reset).
+# Bounds world diversity: at most reset_pool_size unique maps are
+# ever seen per process. Set to 0 to disable (required for the
+# parity harness to maintain exact per-seed determinism).
+reset_pool_size = 1024
+
+[train]
+total_timesteps = 200_000_000
+learning_rate = 0.008
+ent_coef = 0.02
+gamma = 0.997
+gae_lambda = 0.95
+horizon = 128
+minibatch_size = 32768
+
+[policy]
+hidden_size = 32
+num_layers = 1
+expansion_factor = 1
diff --git a/config/craftax_classic.ini b/config/craftax_classic.ini
new file mode 100644
index 0000000000..25430f0e5c
--- /dev/null
+++ b/config/craftax_classic.ini
@@ -0,0 +1,12 @@
+[base]
+env_name = craftax_classic
+
+[vec]
+total_agents = 8192
+num_buffers = 4
+num_threads = 16
+
+[env]
+
+[train]
+total_timesteps = 200_000_000
diff --git a/config/default.ini b/config/default.ini
new file mode 100644
index 0000000000..29bc1808b7
--- /dev/null
+++ b/config/default.ini
@@ -0,0 +1,250 @@
+[base]
+env_name = None
+
+# Multi-GPU (single GPU defaults)
+rank = 0
+world_size = 1
+gpu_id = 0
+nccl_id = 'None'
+profile = False
+checkpoint_dir = checkpoints
+log_dir = logs
+checkpoint_interval = 500
+eval_episodes = 10000
+
+# Epoch at which to capture CUDA graphs. -1 to disable.
+cudagraphs = 10
+seed = 73
+
+# Whether to reset the LSTM state between epochs. We only use
+# this for evals and have not tested it for training.
+reset_state = True
+
+[vec]
+total_agents = 4096
+num_buffers = 2
+num_threads = 16
+
+# Selfplay-pool training. When enabled, frozen_bank_pct of agent slots (see
+# [vec]) play against a snapshot of an older policy from a disk-backed pool.
+# Primary advances to the next pool entry once it beats the current opponent
+# at >= swap_winrate over at least min_games (counted only on historical envs).
+[selfplay]
+enabled = 0
+max_size = 16
+swap_winrate = 0.8
+min_games = 2048
+elo_init = 0.0
+elo_k = 16.0
+seed = 42
+# Add a snapshot to the pool every snapshot_interval global steps, independent
+# of swap. 0 disables interval snapshotting (pool stays at bootstrap).
+snapshot_interval = 1_000_000_000
+# Force an opponent swap if the current opponent has been active for this many
+# global steps without a winrate-driven swap. 0 disables the timeout.
+opp_timeout_steps = 500_000_000
+
+# Args used by your env's binding.c go here
+[env]
+
+[policy]
+hidden_size = 128
+num_layers = 4
+expansion_factor = 1
+
+[torch]
+network = MinGRU
+encoder = DefaultEncoder
+decoder = DefaultDecoder
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 10_000_000
+learning_rate = 0.015
+anneal_lr = 1
+min_lr_ratio = 0.0
+gamma = 0.995
+gae_lambda = 0.90
+replay_ratio = 1.0
+clip_coef = 0.2
+vf_coef = 2.0
+vf_clip_coef = 0.2
+max_grad_norm = 1.5
+ent_coef = 0.001
+# Cosine-anneal ent_coef like lr does. Off by default (matches old behavior).
+# When on: ent_coef decays from its base value to min_ent_coef_ratio * ent_coef
+# over total_timesteps. Useful late in training to let a learned policy commit
+# harder on its preferences once the entropy bonus is no longer load-bearing.
+anneal_ent_coef = 0
+min_ent_coef_ratio = 0.1
+beta1 = 0.95
+beta2 = 0.999
+eps = 1e-12
+minibatch_size = 8192
+horizon = 64
+vtrace_rho_clip = 1.0
+vtrace_c_clip = 1.0
+prio_alpha = 0.8
+prio_beta0 = 0.2
+
+[sweep]
+method = Protein
+metric = score
+metric_distribution = linear
+goal = maximize
+max_suggestion_cost = 3600
+max_runs = 1200
+gpus = 0
+downsample = 5
+use_gpu = True
+prune_pareto = True
+early_stop_quantile = 0.3
+# When set, each sweep trial is scored by winrate in a match against a fixed
+# enemy checkpoint rather than by the training-time env/score. Score key emitted
+# as env/match_score; set match_enemy_model_path to '' to disable.
+match_enemy_model_path = ''
+match_num_games = 1024
+match_enemy_hidden_size = 0
+match_enemy_num_layers = 0
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 3e7
+max = 1e11
+scale = time
+
+[sweep.policy.hidden_size]
+distribution = uniform_pow2
+min = 32
+max = 1024
+scale = auto
+
+[sweep.policy.num_layers]
+distribution = uniform
+min = 1
+max = 8
+scale = auto
+
+#[sweep.vec.total_agents]
+#distribution = uniform_pow2
+#min = 256
+#max = 16384
+#scale = auto
+
+[sweep.vec.num_buffers]
+distribution = uniform
+min = 1
+max = 8
+scale = auto
+
+[sweep.train.horizon]
+distribution = uniform_pow2
+min = 8
+max = 1024
+scale = auto
+
+#[sweep.train.minibatch_size]
+#distribution = uniform_pow2
+#min = 4096
+#max = 65536
+#scale = auto
+
+[sweep.train.learning_rate]
+distribution = log_normal
+min = 0.00001
+max = 0.1
+scale = 0.5
+
+[sweep.train.ent_coef]
+distribution = log_normal
+min = 0.00001
+max = 0.2
+scale = auto
+
+[sweep.train.gamma]
+distribution = logit_normal
+min = 0.8
+max = 0.9999
+scale = auto
+
+[sweep.train.gae_lambda]
+distribution = logit_normal
+min = 0.2
+max = 0.995
+scale = auto
+
+[sweep.train.vtrace_rho_clip]
+distribution = uniform
+min = 0.1
+max = 5.0
+scale = auto
+
+[sweep.train.vtrace_c_clip]
+distribution = uniform
+min = 0.1
+max = 5.0
+scale = auto
+
+[sweep.train.replay_ratio]
+distribution = uniform
+min = 0.25
+max = 4.0
+scale = auto
+
+[sweep.train.clip_coef]
+distribution = uniform
+# Lower clip is sometimes better but less stable
+min = 0.01
+max = 1.0
+scale = auto
+
+# Optimal vf clip can be lower than 0.1,
+# but this results in jank unstable runs
+[sweep.train.vf_clip_coef]
+distribution = uniform
+min = 0.01
+max = 5.0
+scale = auto
+
+[sweep.train.vf_coef]
+distribution = uniform
+min = 0.1
+max = 5.0
+scale = auto
+
+[sweep.train.max_grad_norm]
+distribution = uniform
+min = 0.1
+max = 5.0
+scale = auto
+
+[sweep.train.beta1]
+distribution = logit_normal
+min = 0.5
+max = 0.999
+scale = auto
+
+[sweep.train.beta2]
+distribution = logit_normal
+min = 0.9
+max = 0.99999
+scale = auto
+
+[sweep.train.eps]
+distribution = log_normal
+min = 1e-14
+max = 1e-4
+scale = auto
+
+[sweep.train.prio_alpha]
+distribution = uniform
+min = 0.0
+max = 1.0
+scale = auto
+
+[sweep.train.prio_beta0]
+distribution = uniform
+min = 0.0
+max = 1.0
+scale = auto
diff --git a/config/dino.ini b/config/dino.ini
new file mode 100644
index 0000000000..294f400e75
--- /dev/null
+++ b/config/dino.ini
@@ -0,0 +1,19 @@
+[base]
+env_name = dino
+
+[env]
+num_envs = 1024
+width = 800
+height = 400
+speed_init = 6
+speed_max = 15
+spawn_rate_max = 65
+spawn_rate_min = 45
+rate_increment_rate = 600
+
+[policy]
+hidden_size = 512
+num_layers = 1.6302
+
+[train]
+total_timesteps = 220_000_000
\ No newline at end of file
diff --git a/config/docking.ini b/config/docking.ini
new file mode 100644
index 0000000000..3e24e1d593
--- /dev/null
+++ b/config/docking.ini
@@ -0,0 +1,54 @@
+[base]
+env_name = docking
+
+[vec]
+# Total parallel agents collected each rollout.
+total_agents = 4096
+# Number of rollout buffers for overlap.
+num_buffers = 2
+# CPU threads used for env stepping.
+num_threads = 4
+
+[policy]
+# Hidden width for the policy network.
+hidden_size = 128
+# Number of recurrent layers.
+num_layers = 1
+
+[env]
+# World width in simulation units.
+width = 256
+# World height in simulation units.
+height = 192
+# Max steps before timing out.
+max_ticks = 1024
+# Max ship speed.
+max_speed = 6.0
+# Heading change applied by turn actions.
+turn_rate = 0.10
+# Forward speed delta applied by thrust and brake.
+accel = 0.55
+# Passive speed decay applied every step.
+drag = 0.92
+# Distance threshold for dock contact.
+dock_radius = 18.0
+# Max speed allowed for a clean dock.
+dock_speed_threshold = 0.72
+# Max heading error allowed for a clean dock.
+dock_heading_threshold = 0.28
+# Small per-step penalty to encourage efficiency.
+step_penalty = -0.01
+# Scale for distance-progress shaping reward.
+progress_reward_scale = 0.25
+
+[train]
+# Total agent steps for a default run.
+total_timesteps = 100_000_000
+# Discount factor.
+gamma = 0.99
+# Optimizer learning rate.
+learning_rate = 0.003
+# PPO minibatch size.
+minibatch_size = 32768
+# Entropy bonus for exploration.
+ent_coef = 0.01
diff --git a/config/double_pendulum.ini b/config/double_pendulum.ini
new file mode 100644
index 0000000000..f81d9e488e
--- /dev/null
+++ b/config/double_pendulum.ini
@@ -0,0 +1,60 @@
+[base]
+env_name = double_pendulum
+
+[vec]
+total_agents = 4096
+num_buffers = 4
+num_threads = 16
+
+[env]
+cart_mass = 1.0
+link1_mass = 0.1
+link2_mass = 0.1
+link1_length = 0.5
+link2_length = 0.5
+gravity = 9.8
+force_mag = 10.0
+dt = 0.02
+substeps = 4
+# Blend between the DeepMind dense term and balance_quality.
+balance_bonus_weight = 0.5
+
+[policy]
+hidden_size = 128
+num_layers = 2
+expansion_factor = 1
+
+[torch]
+network = MLP
+encoder = DefaultEncoder
+decoder = DefaultDecoder
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 500000000
+learning_rate = 0.001
+anneal_lr = 1
+min_lr_ratio = 0.0
+gamma = 0.99
+gae_lambda = 0.95
+replay_ratio = 1
+clip_coef = 0.2
+vf_coef = 0.5
+vf_clip_coef = 1.0
+max_grad_norm = 0.5
+ent_coef = 0.001
+beta1 = 0.9
+beta2 = 0.999
+eps = 1e-8
+minibatch_size = 16384
+horizon = 64
+vtrace_rho_clip = 1.0
+vtrace_c_clip = 1.0
+prio_alpha = 0.5
+prio_beta0 = 0.5
+
+[sweep]
+method = Protein
+metric = perf
+goal = maximize
diff --git a/config/drive.ini b/config/drive.ini
new file mode 100644
index 0000000000..6a9670c8d6
--- /dev/null
+++ b/config/drive.ini
@@ -0,0 +1,77 @@
+[base]
+env_name = drive
+
+[vec]
+total_agents = 8192
+num_buffers = 2
+num_threads = 2
+
+[env]
+width = 1280
+height = 1024
+human_agent_idx = 0
+reward_vehicle_collision = -0.2
+reward_offroad_collision = -0.2
+reward_goal_post_respawn = 0
+reward_vehicle_collision_post_respawn = 0
+resample_frequency = 10000
+num_maps = 500
+
+[policy]
+hidden_size = 256
+num_layers = 4
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 1667760009
+learning_rate = 0.001
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.98
+gae_lambda = 0.95
+replay_ratio = 1
+clip_coef = 0.2
+vf_coef = 2
+vf_clip_coef = 0.2
+max_grad_norm = 1.5
+ent_coef = 0.005
+beta1 = 0.95
+beta2 = 0.999
+eps = 1e-12
+minibatch_size = 8192
+horizon = 128
+vtrace_rho_clip = 1
+vtrace_c_clip = 1
+prio_alpha = 0.8
+prio_beta0 = 0.2
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 3e8
+max = 3e9
+mean = 1e9
+scale = auto
+
+[sweep.env.reward_vehicle_collision]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -0.2
+scale = auto
+
+[sweep.env.reward_offroad_collision]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -0.2
+scale = auto
+
+[sweep.env.reward_goal_post_respawn]
+distribution = uniform
+min = 0.0
+max = 1.0
+mean = 0.5
+scale = auto
+
diff --git a/config/drmario.ini b/config/drmario.ini
new file mode 100644
index 0000000000..7ab2faa0a0
--- /dev/null
+++ b/config/drmario.ini
@@ -0,0 +1,45 @@
+[base]
+env_name = drmario
+
+[vec]
+total_agents = 2048
+num_buffers = 4
+num_threads = 4
+
+[env]
+n_rows = 16
+n_cols = 8
+n_init_viruses = 14
+
+[policy]
+hidden_size = 128
+num_layers = 1
+
+[legacy]
+torch_deterministic = 1
+cpu_offload = 0
+compile = 1
+compile_fullgraph = 0
+
+[train]
+total_timesteps = 500_000_000
+learning_rate = 0.001
+gamma = 0.99
+gae_lambda = 0.95
+clip_coef = 0.2
+vf_coef = 0.5
+ent_coef = 0.01
+minibatch_size = 16384
+horizon = 128
+use_rnn = 0
+
+#copied from Breakout
+beta1 = 0.9
+beta2 = 0.999
+eps = 1e-8
+max_grad_norm = 1.0
+replay_ratio = 2
+vtrace_rho_clip = 2.0
+vtrace_c_clip = 2.0
+prio_alpha = 0.6
+prio_beta0 = 0.9
\ No newline at end of file
diff --git a/config/drone.ini b/config/drone.ini
new file mode 100644
index 0000000000..b1ccd2e056
--- /dev/null
+++ b/config/drone.ini
@@ -0,0 +1,126 @@
+[base]
+env_name = drone
+
+[vec]
+total_agents = 2048
+num_buffers = 8
+num_threads = 1
+
+[env]
+num_drones = 64
+
+# multi-task step fractions
+hover_frac = 0.8332634358752755
+race_frac = 0.6878962707768624
+sphere_frac = 0.0
+cube_frac = 0.0
+flag_frac = 0.0
+
+# domain randomisation
+dr = 0.05
+
+# integrator, rk4 by default
+use_rk2 = 0
+
+# shared rewards
+alpha_vel = 0.0
+alpha_omega = 0.0
+alpha_action = 0.0
+
+# hover
+sphere_radius = 4.0
+hover_target_dist = 5
+hover_horizon = 1024
+alpha_hover = 1
+hover_alpha_dist = 0.8120191629018807
+
+# race
+max_rings = 10
+race_horizon = 2048
+ring_reward = 2.4450236350884
+race_alpha_dist = 2.8630645575928786
+
+[policy]
+expansion_factor = 1
+hidden_size = 64
+num_layers = 2
+
+[train]
+anneal_ent_coef = 0
+anneal_lr = 1
+beta1 = 0.9349722808636886
+beta2 = 0.9
+clip_coef = 0.05128371403269373
+ent_coef = 4.696130967260617e-05
+eps = 2.947595291085368e-14
+gae_lambda = 0.9482649804656037
+gamma = 0.9881859783105877
+gpus = 1
+horizon = 64
+learning_rate = 0.009470453254078422
+max_grad_norm = 1.4133356803570938
+min_ent_coef_ratio = 0.1
+min_lr_ratio = 0
+minibatch_size = 16384
+prio_alpha = 0.2056097946119157
+prio_beta0 = 0.7547212669825754
+replay_ratio = 2.253065396569355
+seed = 42
+total_timesteps = 88_650_750
+vf_clip_coef = 1.200252076345663
+vf_coef = 0.11125903992665012
+vtrace_c_clip = 2.902740049693881
+vtrace_rho_clip = 1.5005124861512968
+
+[sweep]
+metric = perf
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 3e7
+max = 2e8
+mean = 8e7
+scale = auto
+
+# hover
+[sweep.env.hover_alpha_dist]
+distribution = log_normal
+min = 0.001
+max = 10.0
+mean = 1.0
+scale = auto
+
+[sweep.env.alpha_hover]
+distribution = log_normal
+min = 0.0001
+max = 1.0
+mean = 0.01
+scale = auto
+
+# race
+[sweep.env.race_alpha_dist]
+distribution = log_normal
+min = 0.001
+max = 10.0
+mean = 1.0
+scale = auto
+
+[sweep.env.ring_reward]
+distribution = log_normal
+min = 0.1
+max = 100.0
+mean = 1.0
+scale = auto
+
+# fracs
+[sweep.env.hover_frac]
+distribution = uniform
+min = 0.1
+max = 1.0
+scale = auto
+
+[sweep.env.race_frac]
+distribution = uniform
+min = 0.1
+max = 1.0
+scale = auto
\ No newline at end of file
diff --git a/config/enduro.ini b/config/enduro.ini
new file mode 100644
index 0000000000..2a8236b202
--- /dev/null
+++ b/config/enduro.ini
@@ -0,0 +1,57 @@
+[base]
+env_name = enduro
+
+[vec]
+total_agents = 256
+num_buffers = 4.00702
+num_threads = 2
+
+[env]
+width = 152
+height = 210
+car_width = 16
+car_height = 11
+max_enemies = 10
+continuous = 0
+
+[policy]
+hidden_size = 128
+num_layers = 2.68359
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 58957801
+learning_rate = 0.0136314
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.979826
+gae_lambda = 0.908362
+replay_ratio = 1.54521
+clip_coef = 0.915672
+vf_coef = 0.977532
+vf_clip_coef = 1.75503
+max_grad_norm = 1.08789
+ent_coef = 0.00246004
+beta1 = 0.840696
+beta2 = 0.999975
+eps = 1.78423e-12
+minibatch_size = 16384
+horizon = 64
+vtrace_rho_clip = 2.58586
+vtrace_c_clip = 5
+prio_alpha = 1
+prio_beta0 = 0.161561
+max_minibatch_size = 32768
+use_rnn = 1
+
+[sweep]
+metric = days_completed
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 2e8
+max = 6e8
+mean = 4e8
+scale = auto
diff --git a/config/freeway.ini b/config/freeway.ini
new file mode 100644
index 0000000000..5d3ea33f6c
--- /dev/null
+++ b/config/freeway.ini
@@ -0,0 +1,68 @@
+[base]
+env_name = freeway
+
+[vec]
+total_agents = 16384
+num_buffers = 6.8738
+num_threads = 2
+num_agents = 4096
+
+[env]
+frameskip = 4
+width = 1216
+height = 720
+player_width = 64
+player_height = 64
+car_width = 64
+car_height = 40
+lane_size = 64
+difficulty = 0
+level = -1
+enable_human_player = 0
+env_randomization = 1
+use_dense_rewards = 1
+
+[policy]
+hidden_size = 128
+num_layers = 7.44076
+expansion_factor = 1
+num_units = 64
+
+[legacy]
+torch_deterministic = 1
+cpu_offload = 0
+compile = 0
+compile_fullgraph = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 403702026
+learning_rate = 0.00357256
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.988734
+gae_lambda = 0.759081
+replay_ratio = 2.08083
+clip_coef = 0.168047
+vf_coef = 3.51248
+vf_clip_coef = 0.179612
+max_grad_norm = 5
+ent_coef = 0.00023521
+beta1 = 0.973725
+beta2 = 0.99942
+eps = 4.24651e-14
+minibatch_size = 32768
+horizon = 64
+vtrace_rho_clip = 2.3679
+vtrace_c_clip = 1.29213
+prio_alpha = 0.741968
+prio_beta0 = 0.654176
+use_rnn = 1
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 3e8
+max = 6e8
+mean = 4e8
+scale = auto
diff --git a/config/g2048.ini b/config/g2048.ini
new file mode 100644
index 0000000000..3ef22c31f5
--- /dev/null
+++ b/config/g2048.ini
@@ -0,0 +1,66 @@
+[base]
+env_name = g2048
+
+[vec]
+total_agents = 8192
+num_buffers = 2.8239
+num_threads = 0
+seed = 73
+
+[env]
+scaffolding_ratio = 0.384917
+
+[policy]
+hidden_size = 512
+num_layers = 4.07473
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 1443360107
+learning_rate = 0.00224872
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.99816
+gae_lambda = 0.696214
+replay_ratio = 2.2381
+clip_coef = 0.01
+vf_coef = 0.1
+vf_clip_coef = 0.01
+max_grad_norm = 1.14364
+ent_coef = 0.0208185
+beta1 = 0.981226
+beta2 = 0.994178
+eps = 1.82223e-06
+minibatch_size = 32768
+horizon = 64
+vtrace_rho_clip = 3.24229
+vtrace_c_clip = 3.95664
+prio_alpha = 1
+prio_beta0 = 0.555562
+env = 0
+
+[sweep]
+max_suggestion_cost = 7200
+
+[sweep.env.scaffolding_ratio]
+distribution = uniform
+min = 0.1
+mean = 0.5
+max = 0.8
+scale = auto
+
+[sweep.train.learning_rate]
+distribution = uniform
+min = 0.0001
+mean = 0.0005
+max = 0.0030
+scale = 0.5
+
+[sweep.train.max_grad_norm]
+distribution = uniform
+min = 0.1
+mean = 0.5
+max = 2.0
+scale = 0.5
diff --git a/config/go.ini b/config/go.ini
new file mode 100644
index 0000000000..e108178f23
--- /dev/null
+++ b/config/go.ini
@@ -0,0 +1,89 @@
+[base]
+env_name = go
+
+[vec]
+total_agents = 256
+num_buffers = 4.61143
+num_threads = 2
+
+[env]
+width = 950
+height = 750
+grid_size = 9
+board_width = 600
+board_height = 600
+grid_square_size = 64
+moves_made = 0
+komi = 7.5
+last_capture_position = -1
+reward_move_pass = -0.518441
+reward_move_valid = 0
+reward_move_invalid = -0.0864746
+reward_opponent_capture = -0.102283
+reward_player_capture = 0.553628
+selfplay = 0
+
+[policy]
+hidden_size = 512
+num_layers = 1.6302
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 146817001
+learning_rate = 0.00034529
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.925771
+gae_lambda = 0.983152
+replay_ratio = 3.75422
+clip_coef = 0.465704
+vf_coef = 3.58533
+vf_clip_coef = 2.38262
+max_grad_norm = 0.1
+ent_coef = 7.39777e-05
+beta1 = 0.995692
+beta2 = 0.999933
+eps = 2.93928e-09
+minibatch_size = 16384
+horizon = 128
+vtrace_rho_clip = 2.18666
+vtrace_c_clip = 0.638156
+prio_alpha = 0
+prio_beta0 = 0.809042
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1e8
+max = 1e9
+mean = 3e8
+scale = auto
+
+[sweep.env.reward_move_invalid]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -0.5
+scale = 0.5
+
+[sweep.env.reward_move_pass]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -0.5
+scale = 0.5
+
+[sweep.env.reward_player_capture]
+distribution = uniform
+min = 0.0
+max = 1.0
+mean = 0.5
+scale = 0.5
+
+[sweep.env.reward_opponent_capture]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -0.5
+scale = 0.5
diff --git a/config/hex.ini b/config/hex.ini
new file mode 100644
index 0000000000..1a8673d43e
--- /dev/null
+++ b/config/hex.ini
@@ -0,0 +1,50 @@
+[base]
+env_name = hex
+
+[vec]
+total_agents = 4096
+num_buffers = 8
+num_threads = 2
+
+[env]
+random_opponent = 0
+
+[policy]
+hidden_size = 256
+num_layers = 1
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 1
+total_timesteps = 20_000_000
+learning_rate = 1e-2
+anneal_lr = 1
+min_lr_ratio = 0.01
+gamma = 0.8
+gae_lambda = 0.962627
+replay_ratio = 1.
+clip_coef = 0.511829
+vf_coef = 5
+vf_clip_coef = 1.99178
+max_grad_norm = 0.552251
+ent_coef = 1e-4
+beta1 = 0.878636
+beta2 = 0.986336
+eps = 3.02623e-07
+minibatch_size = 8192
+horizon = 16
+vtrace_rho_clip = 2.49786
+vtrace_c_clip = 1.52028
+prio_alpha = 1
+prio_beta0 = 0.746205
+use_rnn = 0
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1e7
+max = 2e8
+mean = 3e7
+scale = 0.5
+
+
diff --git a/config/impulse_wars.ini b/config/impulse_wars.ini
new file mode 100644
index 0000000000..3e7c7f7bbb
--- /dev/null
+++ b/config/impulse_wars.ini
@@ -0,0 +1,143 @@
+[base]
+env_name = impulse_wars
+
+max_suggestion_cost = 10_800
+
+[policy]
+hidden_size = 512
+cnn_channels = 64
+
+# These must match what's set in env below
+continuous = False
+num_drones = 2
+is_training = True
+
+[vec]
+num_envs = 4
+#num_workers = 4
+#batch_size = 4
+
+[env]
+num_envs = 1024
+num_drones = 2
+num_agents = 1
+enable_teams = False
+sitting_duck = False
+continuous = False
+is_training = True
+
+[train]
+total_timesteps = 1_000_000_000
+checkpoint_interval = 250
+
+learning_rate = 0.005
+
+compile = False
+compile_mode = reduce-overhead
+compile_fullgraph = False
+
+
+[sweep]
+downsample = 10
+max_cost = 900
+
+[sweep.env.num_envs]
+distribution = uniform_pow2
+min = 1
+max = 1024
+mean = 128
+scale = auto
+
+# reward parameters
+[sweep.env.reward_win]
+distribution = uniform
+min = 0.0
+mean = 2.0
+max = 5.0
+scale = auto
+
+[sweep.env.reward_self_kill]
+distribution = uniform
+min = -3.0
+mean = -1.0
+max = 0.0
+scale = auto
+
+[sweep.env.reward_enemy_death]
+distribution = uniform
+min = 0.0
+mean = 1.0
+max = 3.0
+scale = auto
+
+[sweep.env.reward_kill]
+distribution = uniform
+min = 0.0
+mean = 1.0
+max = 3.0
+scale = auto
+
+[sweep.env.reward_death]
+distribution = uniform
+min = -1.0
+mean = -0.25
+max = 0.0
+scale = auto
+
+[sweep.env.reward_energy_emptied]
+distribution = uniform
+min = -2.0
+mean = -0.75
+max = 0.0
+scale = auto
+
+[sweep.env.reward_weapon_pickup]
+distribution = uniform
+min = 0.0
+mean = 0.5
+max = 3.0
+scale = auto
+
+[sweep.env.reward_shield_break]
+distribution = uniform
+min = 0.0
+mean = 0.5
+max = 3.0
+scale = auto
+
+[sweep.env.reward_shot_hit_coef]
+distribution = log_normal
+min = 0.0005
+mean = 0.005
+max = 0.05
+scale = auto
+
+[sweep.env.reward_explosion_hit_coef]
+distribution = log_normal
+min = 0.0005
+mean = 0.005
+max = 0.05
+scale = auto
+
+# hyperparameters
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 250_000_000
+max = 1_500_000_000
+mean = 500_000_000
+scale = time
+
+[sweep.train.batch_size]
+distribution = uniform_pow2
+min = 65_536
+max = 1_048_576
+mean = 262_144
+scale = auto
+
+[sweep.train.horizon]
+distribution = uniform_pow2
+min = 64
+max = 256
+mean = 128
+scale = auto
+
diff --git a/config/laser_puzzle.ini b/config/laser_puzzle.ini
new file mode 100644
index 0000000000..6499c97882
--- /dev/null
+++ b/config/laser_puzzle.ini
@@ -0,0 +1,21 @@
+[base]
+env_name = laser_puzzle
+
+[vec]
+total_agents = 1024
+num_buffers = 2
+num_threads = 8
+
+[policy]
+hidden_size = 128
+num_layers = 2
+
+[train]
+total_timesteps = 125_000_000
+horizon = 48
+minibatch_size = 12288
+gamma = 0.99
+gae_lambda = 0.98
+learning_rate = 0.004
+ent_coef = 0.025
+vf_coef = 1.0
\ No newline at end of file
diff --git a/config/lightsout.ini b/config/lightsout.ini
new file mode 100644
index 0000000000..e7f0682f28
--- /dev/null
+++ b/config/lightsout.ini
@@ -0,0 +1,8 @@
+[base]
+env_name = lightsout
+
+[env]
+max_steps = 100
+
+[train]
+total_timesteps = 200_000_000
diff --git a/config/matsci.ini b/config/matsci.ini
new file mode 100644
index 0000000000..5b24d5e304
--- /dev/null
+++ b/config/matsci.ini
@@ -0,0 +1,15 @@
+[base]
+env_name = matsci
+
+[vec]
+num_envs = 8
+
+[env]
+num_envs = 8
+num_atoms = 128
+
+[train]
+total_timesteps = 50_000_000
+minibatch_size = 32768
+
+
diff --git a/config/maze.ini b/config/maze.ini
new file mode 100644
index 0000000000..b5d2450a6a
--- /dev/null
+++ b/config/maze.ini
@@ -0,0 +1,54 @@
+[base]
+package = ocean
+env_name = maze
+
+[vec]
+total_agents = 512
+num_buffers = 5.96311
+num_threads = 2
+
+[env]
+num_maps = 8192
+map_size = -1
+
+[policy]
+hidden_size = 512
+num_layers = 5.88042
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 337903991
+learning_rate = 0.000755471
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.993628
+gae_lambda = 0.942137
+replay_ratio = 2.85958
+clip_coef = 0.1304
+vf_coef = 3.03966
+vf_clip_coef = 3.48152
+max_grad_norm = 1.41109
+ent_coef = 1e-05
+beta1 = 0.994379
+beta2 = 0.991022
+eps = 4.95035e-07
+minibatch_size = 16384
+horizon = 256
+vtrace_rho_clip = 5
+vtrace_c_clip = 2.33779
+prio_alpha = 0.998067
+prio_beta0 = 0.884829
+use_rnn = 1
+env = 0
+
+[sweep]
+downsample = 5
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1e8
+max = 1e9
+mean = 3e8
+scale = time
diff --git a/config/memory.ini b/config/memory.ini
new file mode 100644
index 0000000000..6c7a77b7f7
--- /dev/null
+++ b/config/memory.ini
@@ -0,0 +1,12 @@
+[base]
+env_name = memory
+
+[env]
+num_envs = 1024
+
+[vec]
+num_envs = 8
+
+[train]
+total_timesteps = 50_000_000
+minibatch_size = 32768
diff --git a/config/minimal.ini b/config/minimal.ini
new file mode 100644
index 0000000000..f13abe8f8c
--- /dev/null
+++ b/config/minimal.ini
@@ -0,0 +1,9 @@
+[base]
+env_name = minimal
+
+[vec]
+total_agents = 8192
+
+[train]
+total_timesteps = 100_000_000
+
diff --git a/config/moba.ini b/config/moba.ini
new file mode 100644
index 0000000000..b8779ff2fb
--- /dev/null
+++ b/config/moba.ini
@@ -0,0 +1,70 @@
+[base]
+env_name = moba
+
+[vec]
+total_agents = 2048
+num_buffers = 2.24842
+num_threads = 16
+
+[env]
+vision_range = 5
+agent_speed = 1
+script_opponents = 1
+reward_death = -0.163764
+reward_xp = 0.00665677
+reward_distance = 0
+reward_tower = 0.642119
+
+[policy]
+hidden_size = 64
+num_layers = 5.61447
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 39056098
+learning_rate = 0.00343957
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.991949
+gae_lambda = 0.945677
+replay_ratio = 0.832202
+clip_coef = 0.565744
+vf_coef = 3.37665
+vf_clip_coef = 1.27302
+max_grad_norm = 1.08622
+ent_coef = 0.00109913
+beta1 = 0.837952
+beta2 = 0.943593
+eps = 1.89701e-12
+minibatch_size = 8192
+horizon = 64
+vtrace_rho_clip = 1.76073
+vtrace_c_clip = 1.32578
+prio_alpha = 1
+prio_beta0 = 0.5692
+
+[sweep]
+downsample = 10
+
+[sweep.env.reward_death]
+distribution = uniform
+min = -1.0
+max = 0
+mean = 0
+scale = auto
+
+[sweep.env.reward_xp]
+distribution = uniform
+min = 0.0
+max = 0.05
+mean = 0.0015
+scale = auto
+
+[sweep.env.reward_tower]
+distribution = uniform
+min = 0.0
+max = 1.0
+mean = 1.0
+scale = auto
diff --git a/config/nethack.ini b/config/nethack.ini
new file mode 100644
index 0000000000..e0955d77dc
--- /dev/null
+++ b/config/nethack.ini
@@ -0,0 +1,129 @@
+[base]
+env_name = nethack
+cudagraphs = 2
+
+[vec]
+total_agents = 512
+num_buffers = 4
+num_threads = 32
+
+[env]
+gold_coef = 0.021659979757862095
+exp_coef = 0.08341832601491733
+descent_coef = 1.9586073647172169
+xp_coef = 0.5926087261188399
+scout_coef = 0.07425725927605491
+hp_coef = 0.016768633120843694
+hunger_coef = 0.04373640516193193
+illegal_penalty = -0.009138756699997636
+death_penalty = -0.8209825710553373
+ac_coef = 0.0508544124054636
+heal_coef = 0.001
+status_coef = 0.01
+
+[policy]
+hidden_size = 512
+num_layers = 3
+expansion_factor = 1
+
+[train]
+total_timesteps = 247765094
+minibatch_size = 32768
+horizon = 64
+learning_rate = 0.011854702184553156
+gamma = 0.9973714278257158
+gae_lambda = 0.7339946788354172
+replay_ratio = 1.036717410123433
+clip_coef = 1
+vf_coef = 1.8486010633248169
+vf_clip_coef = 4.624607731543118
+max_grad_norm = 0.902165688849037
+ent_coef = 0.008389210353641681
+beta1 = 0.805399553172738
+beta2 = 0.999139492311758
+eps = 1e-14
+vtrace_rho_clip = 2.770648671516198
+vtrace_c_clip = 4.5026502564349595
+prio_alpha = 0.6754855924309153
+prio_beta0 = 0.8672463883417074
+
+[sweep]
+metric = score
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1.5e8
+max = 4e8
+scale = time
+
+[sweep.env.gold_coef]
+distribution = log_normal
+min = 0.001
+max = 1.0
+scale = auto
+
+[sweep.env.exp_coef]
+distribution = log_normal
+min = 0.01
+max = 10.0
+scale = auto
+
+[sweep.env.descent_coef]
+distribution = log_normal
+min = 0.5
+max = 10.0
+scale = auto
+
+[sweep.env.xp_coef]
+distribution = log_normal
+min = 0.05
+max = 1.0
+scale = auto
+
+[sweep.env.scout_coef]
+distribution = log_normal
+min = 0.0001
+max = 0.1
+scale = auto
+
+[sweep.env.hp_coef]
+distribution = log_normal
+min = 0.0005
+max = 0.06
+scale = auto
+
+[sweep.env.hunger_coef]
+distribution = log_normal
+min = 0.01
+max = 0.25
+scale = auto
+
+[sweep.env.illegal_penalty]
+distribution = uniform
+min = -0.01
+max = 0.0
+scale = auto
+
+[sweep.env.death_penalty]
+distribution = uniform
+min = -1.0
+max = 0.0
+scale = auto
+
+[sweep.env.heal_coef]
+distribution = log_normal
+min = 0.001
+max = 0.1
+scale = auto
+
+[sweep.env.status_coef]
+distribution = log_normal
+min = 0.01
+max = 1.0
+scale = auto
+
+[sweep.env.ac_coef]
+distribution = log_normal
+min = 0.01
+max = 0.5
+scale = auto
diff --git a/config/nmmo3.ini b/config/nmmo3.ini
new file mode 100644
index 0000000000..376f965253
--- /dev/null
+++ b/config/nmmo3.ini
@@ -0,0 +1,87 @@
+[base]
+env_name = nmmo3
+
+[vec]
+total_agents = 8192
+num_buffers = 4
+num_threads = 4
+
+[env]
+num_agents = 1024
+width = 512
+height = 512
+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 = 1.0
+reward_market = 0.0
+reward_death = -1.0
+
+[policy]
+hidden_size = 512
+
+[train]
+#total_timesteps = 642_000_000_000
+total_timesteps = 20_000_000_000
+checkpoint_interval = 10000
+learning_rate = 0.0004573146765703167
+gamma = 0.7647543366891623
+gae_lambda = 0.996005622445478
+max_grad_norm = 0.6075578331947327
+vf_coef = 0.3979089612467003
+horizon = 64
+ent_coef = 0.01210084358004069
+minibatch_size = 32768
+
+[sweep]
+downsample = 50
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 5e9
+max = 5e10
+scale = time
+
+[sweep.vec.total_agents]
+distribution = uniform_pow2
+min = 1024
+max = 16384
+scale = auto
+
+[sweep.env.reward_combat_level]
+distribution = uniform
+min = 0.0
+max = 1.0
+mean = 0.5
+scale = auto
+
+[sweep.env.reward_prof_level]
+distribution = uniform
+min = 0.0
+max = 1.0
+mean = 0.5
+scale = auto
+
+[sweep.env.reward_item_level]
+distribution = uniform
+min = 0.0
+max = 1.0
+mean = 1.0
+scale = auto
+
+[sweep.env.reward_death]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -1.0
+scale = auto
diff --git a/config/ocean/drive.ini b/config/ocean/drive.ini
new file mode 100644
index 0000000000..497bbd9881
--- /dev/null
+++ b/config/ocean/drive.ini
@@ -0,0 +1,98 @@
+[base]
+package = ocean
+env_name = puffer_drive
+policy_name = MinGRU
+rnn_name = Recurrent
+
+[vec]
+total_agents = 8192
+num_buffers = 8
+
+[policy]
+input_size = 64
+hidden_size = 256
+
+[rnn]
+input_size = 256
+hidden_size = 256
+
+[env]
+width = 1280
+height = 1024
+human_agent_idx = 0
+reward_vehicle_collision = 0
+reward_offroad_collision = 0
+spawn_immunity_timer = 50
+reward_goal_post_respawn = 0.0
+reward_vehicle_collision_post_respawn = 0.0
+resample_frequency = 910
+num_maps = 10000
+
+[train]
+total_timesteps = 2_000_000_000
+anneal_lr = True
+batch_size = auto
+minibatch_size = 32768
+num_minibatches = 16
+horizon = 128
+adam_beta1 = 0.9
+adam_beta2 = 0.999
+adam_eps = 1e-8
+clip_coef = 0.2
+ent_coef = 0.001
+gae_lambda = 0.95
+gamma = 0.98
+learning_rate = 0.005
+max_grad_norm = 1
+prio_alpha = 0.8499999999999999
+prio_beta0 = 0.8499999999999999
+update_epochs = 1
+vf_clip_coef = 0.1999999999999999
+vf_coef = 2
+vtrace_c_clip = 1
+vtrace_rho_clip = 1
+checkpoint_interval = 1000
+
+
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1e8
+max = 4e8
+mean = 2e8
+scale = time
+
+[sweep.env.reward_vehicle_collision]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -0.2
+scale = auto
+
+[sweep.env.reward_offroad_collision]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -0.2
+scale = auto
+
+[sweep.env.spawn_immunity_timer]
+distribution = uniform
+min = 1
+max = 91
+mean = 30
+scale = auto
+
+[sweep.env.reward_goal_post_respawn]
+distribution = uniform
+min = 0.0
+max = 1.0
+mean = 0.5
+scale = auto
+
+[sweep.env.reward_vehicle_collision_post_respawn]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = -0.2
+scale = auto
diff --git a/config/ocean/drmario.ini b/config/ocean/drmario.ini
new file mode 100644
index 0000000000..a127e38a26
--- /dev/null
+++ b/config/ocean/drmario.ini
@@ -0,0 +1,15 @@
+[base]
+env_name = drmario
+
+[vec]
+total_agents = 8192
+num_buffers = 4
+num_threads = 4
+
+[env]
+n_rows = 16
+n_cols = 8
+n_init_viruses = 4
+
+[train]
+total_timesteps = 200_000_000
diff --git a/pufferlib/config/ocean/oldgrid.ini b/config/oldgrid.ini
similarity index 85%
rename from pufferlib/config/ocean/oldgrid.ini
rename to config/oldgrid.ini
index 3cae63aa94..795c59e2c0 100644
--- a/pufferlib/config/ocean/oldgrid.ini
+++ b/config/oldgrid.ini
@@ -1,17 +1,10 @@
[base]
-package = ocean
-env_name = puffer_oldgrid
+env_name = oldgrid
vec = multiprocessing
-policy_name = Policy
-rnn_name = Recurrent
#[policy]
#hidden_size = 512
-#[rnn]
-#input_size = 512
-#hidden_size = 512
-
[env]
#map_size = 31
max_map_size = 31
@@ -32,11 +25,10 @@ num_envs = 1
num_workers = 1
env_batch_size = 1
update_epochs = 4
-bptt_horizon = 16
+horizon = 16
batch_size = 131072
minibatch_size = 16384
compile = False
-device = cuda
e3b_coef = 0.01
[sweep]
diff --git a/config/onestateworld.ini b/config/onestateworld.ini
new file mode 100644
index 0000000000..cdf0b3bc64
--- /dev/null
+++ b/config/onestateworld.ini
@@ -0,0 +1,17 @@
+[base]
+env_name = onestateworld
+
+[vec]
+num_envs = 8
+
+[env]
+num_envs = 512
+mean_left = 0.1
+mean_right = 0.5
+var_right = 10
+
+[policy]
+hidden_size = 128
+
+[train]
+total_timesteps = 5_000_000
diff --git a/config/onlyfish.ini b/config/onlyfish.ini
new file mode 100644
index 0000000000..4ff4b518a4
--- /dev/null
+++ b/config/onlyfish.ini
@@ -0,0 +1,14 @@
+[base]
+env_name = onlyfish
+
+[env]
+num_envs = 512
+num_agents = 8
+
+[train]
+total_timesteps = 50_000_000
+gamma = 0.99
+learning_rate = 0.015
+minibatch_size = 32768
+ent_coef = 0.005
+
diff --git a/config/overcooked.ini b/config/overcooked.ini
new file mode 100644
index 0000000000..e8136566d1
--- /dev/null
+++ b/config/overcooked.ini
@@ -0,0 +1,29 @@
+[base]
+env_name = overcooked
+
+[vec]
+total_agents = 8192
+
+[env]
+num_agents = 2
+layout = 0
+grid_size = 100
+reward_dish_served_whole_team = 1.0
+reward_dish_served_agent = 0.0
+reward_pot_started = 0.15
+reward_ingredient_added = 0.15
+reward_ingredient_picked = 0.05
+reward_plate_picked = 0.05
+reward_soup_plated = 0.20
+reward_wrong_dish_served = 0.0
+reward_step_penalty = 0.0
+
+[train]
+total_timesteps = 100_000_000
+learning_rate = 0.01
+minibatch_size = 32768
+gamma = 0.99
+ent_coef = 0.02
+gae_lambda = 0.97
+clip_coef = 0.15
+anneal_lr = 1
diff --git a/config/pacman.ini b/config/pacman.ini
new file mode 100644
index 0000000000..7dc54a2b6f
--- /dev/null
+++ b/config/pacman.ini
@@ -0,0 +1,59 @@
+[base]
+env_name = pacman
+
+[vec]
+total_agents = 8192
+num_buffers = 5.26923
+num_threads = 2
+num_envs = 4096
+
+[env]
+randomize_starting_position = 1
+min_start_timeout = 0
+max_start_timeout = 49
+frightened_time = 35
+max_mode_changes = 6
+scatter_mode_length = 70
+chase_mode_length = 140
+
+[policy]
+hidden_size = 256
+num_layers = 6.05812
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 204996994
+learning_rate = 0.00972151
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.996185
+gae_lambda = 0.970583
+replay_ratio = 1.78296
+clip_coef = 0.126507
+vf_coef = 0.1
+vf_clip_coef = 3.6265
+max_grad_norm = 0.604348
+ent_coef = 0.2
+beta1 = 0.894473
+beta2 = 0.993218
+eps = 0.0001
+minibatch_size = 16384
+horizon = 64
+vtrace_rho_clip = 2.07372
+vtrace_c_clip = 0.465232
+prio_alpha = 0.58347
+prio_beta0 = 0.415454
+use_rnn = 1
+
+[sweep]
+downsample = 10
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 5e7
+max = 5e8
+mean = 1e8
+scale = auto
+
diff --git a/config/pong.ini b/config/pong.ini
new file mode 100644
index 0000000000..51319e2923
--- /dev/null
+++ b/config/pong.ini
@@ -0,0 +1,65 @@
+[base]
+env_name = pong
+
+[vec]
+total_agents = 1024
+num_buffers = 1
+num_threads = 16
+
+[env]
+width = 500
+height = 640
+paddle_width = 20
+paddle_height = 70
+ball_width = 32
+ball_height = 32
+paddle_speed = 8
+ball_initial_speed_x = 10
+ball_initial_speed_y = 1
+ball_speed_y_increment = 3
+ball_max_speed_y = 13
+max_score = 21
+frameskip = 8
+continuous = 0
+
+[policy]
+hidden_size = 32
+num_layers = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 5000000
+learning_rate = 0.1
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.934713
+gae_lambda = 0.991989
+replay_ratio = 3.05148
+clip_coef = 0.822764
+vf_coef = 5
+vf_clip_coef = 4.95789
+max_grad_norm = 0.752747
+ent_coef = 0.000402915
+beta1 = 0.5
+beta2 = 0.947709
+eps = 0.0001
+minibatch_size = 32768
+horizon = 32
+vtrace_rho_clip = 4.87841
+vtrace_c_clip = 1.48608
+prio_alpha = 0.242089
+prio_beta0 = 0.807575
+use_rnn = 1
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 5e6
+max = 5e7
+scale = auto
+
+[sweep.env.frameskip]
+distribution = int_uniform
+min = 1
+max = 8
+scale = 2.0
diff --git a/config/pysquared.ini b/config/pysquared.ini
new file mode 100644
index 0000000000..889d50747c
--- /dev/null
+++ b/config/pysquared.ini
@@ -0,0 +1,13 @@
+[base]
+env_name = pysquared
+
+# Set up to match our C version defaults for speed comparison
+[vec]
+num_envs = 8192
+num_workers = 2
+
+[train]
+total_timesteps = 20_000_000
+gamma = 0.95
+learning_rate = 0.05
+minibatch_size = 32768
diff --git a/config/robocode.ini b/config/robocode.ini
new file mode 100644
index 0000000000..c17e3402c8
--- /dev/null
+++ b/config/robocode.ini
@@ -0,0 +1,90 @@
+[base]
+env_name = robocode
+
+[vec]
+total_agents = 8192
+num_buffers = 8
+num_threads = 8
+num_frozen_banks = 1
+frozen_bank_pct = 0.1
+
+[selfplay]
+enabled = 1
+max_size = 16
+swap_winrate = 0.8
+min_games = 2048
+elo_init = 0
+elo_k = 16
+seed = 42
+snapshot_interval = 1000000000
+opp_timeout_steps = 500000000
+
+[env]
+num_agents = 2
+num_bots = 0
+width = 800
+height = 600
+reward_damage = 0.0938468
+reward_spot = 0.00526184
+bot_policy = 3
+max_ticks = 3000
+
+[policy]
+hidden_size = 1024
+num_layers = 2.69591
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 5_000_000_000
+learning_rate = 0.00249552
+anneal_lr = 0
+min_lr_ratio = 0
+gamma = 0.98004
+gae_lambda = 0.916642
+replay_ratio = 1.09703
+clip_coef = 0.43176
+vf_coef = 0.759785
+vf_clip_coef = 2.79459
+max_grad_norm = 0.560379
+ent_coef = 0.00385733
+anneal_ent_coef = 0
+min_ent_coef_ratio = 0.1
+beta1 = 0.975562
+beta2 = 0.995251
+eps = 7.04297e-07
+minibatch_size = 32768
+horizon = 64
+vtrace_rho_clip = 5
+vtrace_c_clip = 4.64405
+prio_alpha = 0.769809
+prio_beta0 = 0.68767
+
+[sweep]
+match_enemy_model_path = 'resources/robocode/best_robo.bin'
+match_num_games = 4096
+match_max_ticks = 4096
+match_enemy_hidden_size = 1024
+match_enemy_num_layers = 2.69591
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1e8
+max = 1e9
+mean = 5e8
+scale = auto
+
+[sweep.env.reward_damage]
+distribution = uniform
+min = 0.0
+max = 0.1
+mean = 0.01
+scale = auto
+
+[sweep.env.reward_spot]
+distribution = uniform
+min = 0.0
+max = 0.01
+mean = 0.001
+scale = auto
diff --git a/config/rware.ini b/config/rware.ini
new file mode 100644
index 0000000000..36cc3ac561
--- /dev/null
+++ b/config/rware.ini
@@ -0,0 +1,23 @@
+[base]
+env_name = rware
+policy_name = MinGRU
+rnn_name = Recurrent
+
+[vec]
+total_agents = 4096
+
+[env]
+num_envs = 256
+num_agents = 8
+map_choice = 2
+num_requested_shelves = 8
+grid_square_size = 64
+human_agent_idx = 0
+reward_type = 1
+width = 1280
+height = 640
+
+[train]
+total_timesteps = 100_000_000
+learning_rate = 0.05
+minibatch_size = 32768
diff --git a/config/sanity.ini b/config/sanity.ini
new file mode 100644
index 0000000000..d82cb9dbbc
--- /dev/null
+++ b/config/sanity.ini
@@ -0,0 +1,27 @@
+[base]
+env_name = bandit memory multiagent password spaces stochastic
+
+[train]
+total_timesteps = 50_000
+learning_rate = 0.017
+num_envs = 8
+num_workers = 2
+env_batch_size = 8
+batch_size = 1024
+minibatch_size = 128
+horizon = 4
+
+
+[sweep.train.batch_size]
+distribution = uniform
+min = 512
+max = 2048
+mean = 1024
+scale = 0.5
+
+[sweep.train.minibatch_size]
+distribution = uniform
+min = 64
+max = 512
+mean = 128
+scale = 0.5
diff --git a/pufferlib/config/ocean/shared_pool.ini b/config/shared_pool.ini
similarity index 89%
rename from pufferlib/config/ocean/shared_pool.ini
rename to config/shared_pool.ini
index 36a9c5bfe3..a0ffdc1e4c 100644
--- a/pufferlib/config/ocean/shared_pool.ini
+++ b/config/shared_pool.ini
@@ -1,7 +1,5 @@
[base]
-package = ocean
-env_name = puffer_shared_pool
-rnn_name = Recurrent
+env_name = shared_pool
[env]
num_envs = 512
@@ -15,7 +13,7 @@ food_base_spawn_rate = 2e-3
[train]
total_timesteps = 60_000_000
-bptt_horizon = 16
+horizon = 16
checkpoint_interval = 200
learning_rate = 0.0008524
gamma = 0.9989
diff --git a/config/slimevolley.ini b/config/slimevolley.ini
new file mode 100644
index 0000000000..9354ee9a52
--- /dev/null
+++ b/config/slimevolley.ini
@@ -0,0 +1,52 @@
+[base]
+env_name = slimevolley
+
+[vec]
+total_agents = 16384
+num_buffers = 4.25408
+num_threads = 2
+
+[env]
+num_agents = 1
+gamma = 0.99
+
+[policy]
+hidden_size = 128
+num_layers = 3.90792
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 91750396
+learning_rate = 0.00296777
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.987692
+gae_lambda = 0.979432
+replay_ratio = 2.4235
+clip_coef = 0.112093
+vf_coef = 4.99645
+vf_clip_coef = 1.77049
+max_grad_norm = 0.1
+ent_coef = 0.00194179
+beta1 = 0.964621
+beta2 = 0.997724
+eps = 8.28081e-12
+minibatch_size = 8192
+horizon = 16
+vtrace_rho_clip = 2.23002
+vtrace_c_clip = 3.50115
+prio_alpha = 0.544741
+prio_beta0 = 0.189603
+use_rnn = 1
+
+[sweep]
+downsample = 5
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1e8
+max = 2e9
+mean = 3e8
+scale = time
diff --git a/config/snake.ini b/config/snake.ini
new file mode 100644
index 0000000000..f11b879248
--- /dev/null
+++ b/config/snake.ini
@@ -0,0 +1,61 @@
+[base]
+env_name = snake
+
+[env]
+width = 640
+height = 360
+num_agents = 256
+num_food = 4096
+vision = 5
+leave_corpse_on_death = True
+reward_food = 0.1
+reward_corpse = 0.1
+reward_death = -1.0
+max_snake_length = 1024
+cell_size = 2
+
+[vec]
+total_agents = 4096
+
+[train]
+total_timesteps = 500_000_000
+gamma = 0.99
+minibatch_size = 32768
+
+[sweep]
+max_cost = 500
+
+[sweep.env.reward_food]
+distribution = uniform
+min = 0.0
+max = 1.0
+mean = 0.0
+scale = auto
+
+[sweep.env.reward_death]
+distribution = uniform
+min = -1.0
+max = 0.0
+mean = 0.0
+scale = auto
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 2e7
+max = 5e8
+mean = 1e8
+scale = auto
+
+[sweep.policy.hidden_size]
+distribution = uniform_pow2
+min = 16
+max = 1024
+mean = 128
+scale = auto
+
+[sweep.env.num_envs]
+distribution = uniform_pow2
+min = 1
+max = 32
+mean = 8
+scale = auto
diff --git a/config/squared.ini b/config/squared.ini
new file mode 100644
index 0000000000..75b724beb5
--- /dev/null
+++ b/config/squared.ini
@@ -0,0 +1,20 @@
+[base]
+env_name = squared squared_continuous
+
+[vec]
+total_agents = 4096
+backend = Serial
+
+[policy]
+hidden_size = 128
+num_layers = 1
+
+[env]
+size = 11
+
+[train]
+total_timesteps = 100_000_000
+gamma = 0.99
+learning_rate = 0.005
+minibatch_size = 32768
+ent_coef = 0.01
diff --git a/config/tactical.ini b/config/tactical.ini
new file mode 100644
index 0000000000..afb68ef08e
--- /dev/null
+++ b/config/tactical.ini
@@ -0,0 +1,2 @@
+[base]
+env_name = tactical
diff --git a/config/target.ini b/config/target.ini
new file mode 100644
index 0000000000..31b6fff4a0
--- /dev/null
+++ b/config/target.ini
@@ -0,0 +1,14 @@
+[base]
+env_name = target
+
+[vec]
+total_agents = 4096
+num_buffers = 2
+
+[train]
+total_timesteps = 100_000_000
+gamma = 0.99
+learning_rate = 0.015
+minibatch_size = 32768
+ent_coef = 0.02
+
diff --git a/config/template.ini b/config/template.ini
new file mode 100644
index 0000000000..b1d681549a
--- /dev/null
+++ b/config/template.ini
@@ -0,0 +1,8 @@
+[base]
+env_name = template
+
+[env]
+num_envs = 4096
+
+[train]
+total_timesteps = 10_000_000
diff --git a/config/terraform.ini b/config/terraform.ini
new file mode 100644
index 0000000000..ce9ac32042
--- /dev/null
+++ b/config/terraform.ini
@@ -0,0 +1,73 @@
+[base]
+env_name = terraform
+
+[vec]
+total_agents = 2048
+num_buffers = 4.40297
+num_threads = 2
+
+[env]
+num_envs = 1024
+num_agents = 1
+size = 64
+reset_frequency = 1024
+reward_scale = 0.0907157
+
+[policy]
+hidden_size = 512
+num_layers = 5.46436
+expansion_factor = 1
+
+[rnn]
+input_size = 256
+hidden_size = 256
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 234873016
+learning_rate = 0.00551772
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.89571
+gae_lambda = 0.2
+replay_ratio = 1.78841
+clip_coef = 0.01
+vf_coef = 5
+vf_clip_coef = 1.19287
+max_grad_norm = 2.21317
+ent_coef = 0.0079761
+beta1 = 0.965804
+beta2 = 0.999389
+eps = 0.0001
+minibatch_size = 16384
+horizon = 256
+vtrace_rho_clip = 4.24942
+vtrace_c_clip = 2.12831
+prio_alpha = 0.451327
+prio_beta0 = 0.676252
+adam_beta1 = 0.879231
+adam_beta2 = 0.998046
+adam_eps = 6.00018e-06
+use_rnn = 1
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 2e8
+max = 6e8
+mean = 4e8
+scale = time
+
+#[sweep.env.reset_frequency]
+#distribution = int_uniform
+#min = 1024
+#max = 16384
+#mean = 8192
+#scale = auto
+
+[sweep.env.reward_scale]
+distribution = log_normal
+min = 0.01
+max = 1
+mean = 0.5
+scale = auto
diff --git a/config/tetris.ini b/config/tetris.ini
new file mode 100644
index 0000000000..9cada14e66
--- /dev/null
+++ b/config/tetris.ini
@@ -0,0 +1,62 @@
+[base]
+env_name = tetris
+
+[vec]
+total_agents = 4096
+num_buffers = 5.20054
+num_threads = 2
+
+[env]
+n_rows = 20
+n_cols = 10
+use_deck_obs = 1
+n_init_garbage = 4
+n_noise_obs = 0
+
+[policy]
+hidden_size = 512
+num_layers = 2.20258
+expansion_factor = 1
+
+[legacy]
+torch_deterministic = 1
+cpu_offload = 0
+compile = 0
+compile_fullgraph = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 1924140014
+learning_rate = 0.0031086
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.992822
+gae_lambda = 0.703161
+replay_ratio = 2.74669
+clip_coef = 0.0220916
+vf_coef = 2.6876
+vf_clip_coef = 1.63454
+max_grad_norm = 3.15904
+ent_coef = 0.0184801
+beta1 = 0.97197
+beta2 = 0.999989
+eps = 6.21415e-08
+minibatch_size = 65536
+horizon = 128
+vtrace_rho_clip = 2.65161
+vtrace_c_clip = 2.23837
+prio_alpha = 0.354858
+prio_beta0 = 0.950642
+use_rnn = 1
+
+[sweep]
+metric = score
+goal = maximize
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 30_000_000
+max = 3_000_000_000
+mean = 200_000_000
+scale = auto
diff --git a/config/tmaze.ini b/config/tmaze.ini
new file mode 100644
index 0000000000..e5a42a6999
--- /dev/null
+++ b/config/tmaze.ini
@@ -0,0 +1,17 @@
+[base]
+env_name = tmaze
+
+[vec]
+num_envs = 8
+
+[env]
+num_envs = 512
+size = 16
+
+[policy]
+hidden_size = 128
+
+[train]
+total_timesteps = 5_000_000
+horizon = 32
+; entropy_coef = 0.01
\ No newline at end of file
diff --git a/config/tower_climb.ini b/config/tower_climb.ini
new file mode 100644
index 0000000000..37e1a752be
--- /dev/null
+++ b/config/tower_climb.ini
@@ -0,0 +1,82 @@
+[base]
+env_name = tower_climb
+
+[vec]
+total_agents = 8192
+num_buffers = 5.1101
+num_threads = 2
+
+[env]
+num_envs = 1024
+num_maps = 50
+reward_climb_row = 0.205371
+reward_fall_row = 0
+reward_illegal_move = -0.00397522
+reward_move_block = 0
+
+[policy]
+hidden_size = 256
+num_layers = 5.09602
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 447742034
+learning_rate = 0.017405
+anneal_lr = 1
+min_lr_ratio = 0.1
+gamma = 0.933042
+gae_lambda = 0.755604
+replay_ratio = 0.707762
+clip_coef = 0.0890744
+vf_coef = 4.7512
+vf_clip_coef = 5
+max_grad_norm = 4.98401
+ent_coef = 0.2
+beta1 = 0.745044
+beta2 = 0.99774
+eps = 1e-14
+minibatch_size = 32768
+horizon = 128
+vtrace_rho_clip = 4.06891
+vtrace_c_clip = 3.13193
+prio_alpha = 1
+prio_beta0 = 1
+adam_beta1 = 0.81
+adam_beta2 = 0.95
+adam_eps = 1e-08
+
+[sweep]
+metric = perf
+metric_distribution = percentile
+
+[sweep.train.total_timesteps]
+distribution = uniform
+min = 100_000_000
+max = 2_000_000_000
+scale = 0.5
+
+[sweep.env.reward_climb_row]
+distribution = uniform
+min = 0.0
+max = 1.0
+scale = auto
+
+[sweep.env.reward_fall_row]
+distribution = uniform
+min = -1.0
+max = 0.0
+scale = auto
+
+[sweep.env.reward_illegal_move]
+distribution = uniform
+min = -1e-2
+max = -1e-4
+scale = auto
+
+[sweep.env.reward_move_block]
+distribution = uniform
+min = 0.0
+max = 1.0
+scale = auto
diff --git a/config/trash_pickup.ini b/config/trash_pickup.ini
new file mode 100644
index 0000000000..8418c8eba3
--- /dev/null
+++ b/config/trash_pickup.ini
@@ -0,0 +1,54 @@
+[base]
+env_name = trash_pickup
+
+[vec]
+total_agents = 4096
+num_buffers = 2.05803
+num_threads = 2
+
+[env]
+grid_size = 20
+num_agents = 8
+num_trash = 40
+num_bins = 2
+max_steps = 500
+report_interval = 32
+agent_sight_range = 5
+
+[policy]
+hidden_size = 128
+num_layers = 2.92514
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 15728600
+learning_rate = 0.0147993
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.930484
+gae_lambda = 0.972618
+replay_ratio = 1.83493
+clip_coef = 0.0627967
+vf_coef = 3.9924
+vf_clip_coef = 0.01
+max_grad_norm = 0.1
+ent_coef = 0.000193757
+beta1 = 0.87893
+beta2 = 0.999989
+eps = 1e-14
+minibatch_size = 16384
+horizon = 16
+vtrace_rho_clip = 3.43692
+vtrace_c_clip = 3.0755
+prio_alpha = 0.58438
+prio_beta0 = 0.416024
+use_rnn = 1
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 3e7
+max = 2e8
+mean = 1e8
+scale = 0.5
diff --git a/config/tripletriad.ini b/config/tripletriad.ini
new file mode 100644
index 0000000000..bd77d60bfa
--- /dev/null
+++ b/config/tripletriad.ini
@@ -0,0 +1,52 @@
+[base]
+env_name = tripletriad
+
+[vec]
+total_agents = 4096
+num_buffers = 4.5243
+num_threads = 2
+num_agents = 4096
+
+[env]
+width = 990
+height = 690
+card_width = 192
+card_height = 224
+
+[policy]
+hidden_size = 256
+num_layers = 2.46396
+expansion_factor = 1
+
+[train]
+gpus = 1
+seed = 42
+total_timesteps = 34537498
+learning_rate = 0.0017548
+anneal_lr = 1
+min_lr_ratio = 0
+gamma = 0.955398
+gae_lambda = 0.98264
+replay_ratio = 3.5804
+clip_coef = 0.253448
+vf_coef = 2.14518
+vf_clip_coef = 3.87724
+max_grad_norm = 4.65825
+ent_coef = 0.000501928
+beta1 = 0.944088
+beta2 = 0.999153
+eps = 1.83574e-12
+minibatch_size = 8192
+horizon = 16
+vtrace_rho_clip = 4.86264
+vtrace_c_clip = 1.79853
+prio_alpha = 0.293554
+prio_beta0 = 0.17102
+use_rnn = 1
+
+[sweep.train.total_timesteps]
+distribution = log_normal
+min = 1e7
+max = 2e8
+mean = 1e8
+scale = time
diff --git a/config/whackamole.ini b/config/whackamole.ini
new file mode 100644
index 0000000000..af4b6c0a0a
--- /dev/null
+++ b/config/whackamole.ini
@@ -0,0 +1,13 @@
+[base]
+env_name = whackamole
+
+[env]
+num_envs = 4096
+
+[policy]
+hidden_size = 64
+num_layers = 1
+
+[train]
+learning_rate = 0.001
+total_timesteps = 500_000_000
diff --git a/pufferlib/config/ocean/whisker_racer.ini b/config/whisker_racer.ini
similarity index 90%
rename from pufferlib/config/ocean/whisker_racer.ini
rename to config/whisker_racer.ini
index b7dd87f11f..1b7dd97b8d 100644
--- a/pufferlib/config/ocean/whisker_racer.ini
+++ b/config/whisker_racer.ini
@@ -1,8 +1,5 @@
[base]
-package = ocean
-env_name = puffer_whisker_racer
-policy_name = Policy
-rnn_name = Recurrent
+env_name = whisker_racer
[vec]
num_envs = 8
@@ -17,32 +14,32 @@ num_radial_sectors = 180
num_points = 16
bezier_resolution = 4
turn_pi_frac = 40
-w_ang = 0.777 # 0.586 # 0.523
+maxv = 5
+w_ang = 0.777
+max_whisker_length = 100
reward_yellow = 0.2
reward_green = -0.001
-corner_thresh = 0.5 # dot product for hairpins
-ftmp1 = 0.5 #0.9
-ftmp2 = 3.0 #1.05
-ftmp3 = 0.3 # 0.2
+gamma = 0.9
+corner_thresh = 0.5
+ftmp1 = 0.5
+ftmp2 = 3.0
+ftmp3 = 0.3
ftmp4 = 0.0
mode7 = 0
render_many = 0
rng = 6
method = 2
+continuous = 0
[policy]
hidden_size = 128
-[rnn]
-input_size = 128
-hidden_size = 128
-
[train]
adam_beta1 = 0.9446160612709289
adam_beta2 = 0.9898294105500932
adam_eps = 3.599894131847621e-14
batch_size = auto
-bptt_horizon = 64
+horizon = 64
clip_coef = 0.18182501031893042
ent_coef = 0.014660408908451323
gae_lambda = 0.9560790493173461
diff --git a/constellation/cache_data.py b/constellation/cache_data.py
new file mode 100644
index 0000000000..174b520575
--- /dev/null
+++ b/constellation/cache_data.py
@@ -0,0 +1,251 @@
+# Merges log files + filters to pareto-optimal points wrt steps, wall-clock, and score. Comment that if you want the full dataset. Also does TSNE, which is why I haven't bothered porting to C.
+import numpy as np
+
+import json
+import glob
+import os
+
+HYPERS = [
+ 'train/learning_rate',
+ 'train/ent_coef',
+ 'train/gamma',
+ 'train/gae_lambda',
+ 'train/vtrace_rho_clip',
+ 'train/vtrace_c_clip',
+ 'train/clip_coef',
+ 'train/vf_clip_coef',
+ 'train/vf_coef',
+ 'train/max_grad_norm',
+ 'train/beta1',
+ 'train/beta2',
+ 'train/eps',
+ 'train/prio_alpha',
+ 'train/prio_beta0',
+ 'train/horizon',
+ 'train/replay_ratio',
+ 'train/minibatch_size',
+ 'policy/hidden_size',
+ 'vec/total_agents',
+]
+
+METRICS = [
+ 'agent_steps',
+ 'uptime',
+ 'env/score',
+ 'env/perf',
+ 'tsne1',
+ 'tsne2',
+]
+
+ALL_KEYS = HYPERS + METRICS
+
+def unroll_nested_dict(d):
+ if not isinstance(d, dict):
+ return d
+
+ for k, v in d.items():
+ if isinstance(v, dict):
+ for k2, v2 in unroll_nested_dict(v):
+ yield f"{k}/{k2}", v2
+ else:
+ yield k, v
+
+
+def pareto_idx(steps, costs, scores):
+ idxs = []
+ for i in range(len(steps)):
+ better = [scores[j] >= scores[i] and
+ costs[j] < costs[i] and steps[j] < steps[i]
+ for j in range(len(scores))]
+ if not any(better):
+ idxs.append(i)
+
+ return idxs
+
+def cached_load(path, env_name, cache, full_dataset=False):
+ data = {}
+ num_metrics = 0
+ metric_keys = []
+ for fpath in glob.glob(path):
+ if fpath in cache:
+ exp = cache[fpath]
+ else:
+ with open(fpath, 'r') as f:
+ try:
+ exp = json.load(f)
+ except json.decoder.JSONDecodeError:
+ print(f'Skipping {fpath}')
+ continue
+
+ cache[fpath] = exp
+
+ if 'metrics' not in exp:
+ print(f'Skipping {fpath} (no metrics)')
+ continue
+
+ # Temporary: Some experiments are missing loss keys
+ for k in list(exp['metrics'].keys()):
+ if 'loss' in k:
+ del exp['metrics'][k]
+
+ if num_metrics == 0:
+ num_metrics = len(exp['metrics'])
+ metric_keys = list(exp['metrics'].keys())
+
+ skip = False
+ metrics = exp['metrics']
+
+ if len(metrics) != num_metrics:
+ print(f'Skipping {fpath} (num_metrics={len(metrics)} != {num_metrics})')
+ continue
+
+ n = len(metrics['agent_steps'])
+
+ for k, v in metrics.items():
+ if len(v) != n:
+ skip = True
+ break
+
+ if k not in data:
+ data[k] = []
+
+ if np.isnan(v).any():
+ skip = True
+ break
+
+ if skip:
+ print(f'Skipping {fpath} (bad data)')
+ continue
+
+ for k, v in metrics.items():
+ data[k].append(v)
+ if len(data[k]) != len(data['SPS']):
+ pass
+
+ sweep_metadata = exp['sweep']
+
+ for k, v in unroll_nested_dict(exp):
+ if k == 'env/score':
+ continue
+
+ if k not in data:
+ data[k] = []
+
+ data[k].append([v]*n)
+
+ for hyper in HYPERS:
+ prefix, suffix = hyper.split('/')
+ group = sweep_metadata[prefix]
+ key = f'{prefix}/{suffix}_norm'
+ if key not in data:
+ data[key] = []
+
+ if suffix in group:
+ param = group[suffix]
+ mmin = param['min']
+ mmax = param['max']
+ dist = param['distribution']
+ val = exp[prefix][suffix]
+
+ if 'log' in dist or 'pow2' in dist:
+ mmin = np.log(mmin)
+ mmax = np.log(mmax)
+ val = np.log(val)
+
+ norm = (val - mmin) / (mmax - mmin)
+ data[key].append([norm]*n)
+ else:
+ data[key].append([1]*n)
+
+ for k, v in data.items():
+ data[k] = [item for sublist in v for item in sublist]
+
+ for k in list(data.keys()):
+ if 'sweep' in k:
+ del data[k]
+
+ # Format im millions to avoid overfloat in C
+ data['agent_steps'] = [e/1e6 for e in data['agent_steps']]
+ data['train/total_timesteps'] = [e/1e6 for e in data['train/total_timesteps']]
+ del data['metrics/agent_steps']
+
+ # Filter to pareto
+ if not full_dataset:
+ steps = data['agent_steps']
+ costs = data['uptime']
+ scores = data['env/score']
+
+ idxs = pareto_idx(steps, costs, scores)
+ for k in data:
+ try:
+ data[k] = [data[k][i] for i in idxs]
+ except IndexError:
+ continue
+
+ data['sweep'] = sweep_metadata
+ return data
+
+def compute_tsne(full_dataset=False):
+ all_data = {}
+ normed = {}
+
+ cache = {}
+ cache_file = os.path.join('cache.json')
+ if os.path.exists(cache_file):
+ cache = json.load(open(cache_file, 'r'))
+
+ env_names = sorted(os.listdir('logs'))
+ for env in env_names:
+ print('Loading: ', env)
+ all_data[env] = cached_load(f'logs/{env}/*.json', env, cache, full_dataset)
+
+ with open(cache_file, 'w') as f:
+ json.dump(cache, f)
+
+ for env in env_names:
+ env_data = all_data[env]
+ normed_env = []
+ for key in HYPERS:
+ norm_key = f'{key}_norm'
+ normed_env.append(np.array(env_data[norm_key]))
+
+ normed[env] = np.stack(normed_env, axis=1)
+
+ normed = np.concatenate(list(normed.values()), axis=0)
+
+ from sklearn.manifold import TSNE
+ proj = TSNE(n_components=2)
+ reduced = proj.fit_transform(normed)
+
+ row = 0
+ for env in env_names:
+ sz = len(all_data[env]['agent_steps'])
+ all_data[env]['tsne1'] = reduced[row:row+sz, 0].tolist()
+ all_data[env]['tsne2'] = reduced[row:row+sz, 1].tolist()
+
+ row += sz
+
+ for env in all_data:
+ dat = all_data[env]
+ dat = {k: v for k, v in dat.items() if isinstance(v, list)
+ and len(v) > 0 and isinstance(v[0], (int, float))
+ and (k == 'train/max_grad_norm' or not k.endswith('_norm'))}
+ all_data[env] = dat
+ print(f"Env {env} has {len(dat['env/perf'])} points")
+ for k, v in dat.items():
+ if 'env/perf' in k or 'score' in k:
+ print(f'{env}/{k}: min={min(v)}, max={max(v)}')
+
+ for env in all_data:
+ for k, v in all_data[env].items():
+ if isinstance(v, list):
+ all_data[env][k] = ','.join([f'{x:.6g}' for x in v])
+
+ json.dump(all_data, open('resources/constellation/experiments.json', 'w'))
+
+if __name__ == '__main__':
+ import argparse
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--full', action='store_true', help='Include full dataset (no pareto filtering)')
+ args = parser.parse_args()
+ compute_tsne(full_dataset=args.full)
diff --git a/constellation/constellation.c b/constellation/constellation.c
new file mode 100644
index 0000000000..dffb5b6dc1
--- /dev/null
+++ b/constellation/constellation.c
@@ -0,0 +1,1246 @@
+#include
+#include
+#include
+#include
+
+#include "cJSON.h"
+#include "raylib.h"
+
+#define RAYGUI_IMPLEMENTATION
+#include "raygui.h"
+#include "rcamera.h"
+
+#if defined(PLATFORM_DESKTOP) || defined(PLATFORM_DESKTOP_SDL)
+ #if defined(GRAPHICS_API_OPENGL_ES2)
+ #include "glad_gles2.h" // Required for: OpenGL functionality
+ #define glGenVertexArrays glGenVertexArraysOES
+ #define glBindVertexArray glBindVertexArrayOES
+ #define glDeleteVertexArrays glDeleteVertexArraysOES
+ #define GLSL_VERSION 100
+ #else
+ #if defined(__APPLE__)
+ #define GL_SILENCE_DEPRECATION // Silence Opengl API deprecation warnings
+ #include // OpenGL 3 library for OSX
+ #include // OpenGL 3 extensions library for OSX
+ #else
+ #include "glad.h" // Required for: OpenGL functionality
+ #endif
+ #define GLSL_VERSION 330
+ #endif
+#else // PLATFORM_ANDROID, PLATFORM_WEB
+ #include
+ #define GLSL_VERSION 100
+#endif
+
+#include "rlgl.h"
+#include "raymath.h"
+
+#define CAMERA_ORBITAL_SPEED 0.05f
+void CustomUpdateCamera(Camera *camera, float orbitSpeed) {
+ float cameraOrbitalSpeed = CAMERA_ORBITAL_SPEED*GetFrameTime();
+ Matrix rotation = MatrixRotate(GetCameraUp(camera), cameraOrbitalSpeed);
+ Vector3 view = Vector3Subtract(camera->position, camera->target);
+ view = Vector3Transform(view, rotation);
+ camera->position = Vector3Add(camera->target, view);
+ CameraMoveToTarget(camera, -GetMouseWheelMove());
+ if (IsKeyPressed(KEY_KP_SUBTRACT)) CameraMoveToTarget(camera, 2.0f);
+ if (IsKeyPressed(KEY_KP_ADD)) CameraMoveToTarget(camera, -2.0f);
+}
+
+#define SETTINGS_HEIGHT 20
+#define SEP 8
+#define SPACER 25
+#define TOGGLE_WIDTH 70
+#define DROPDOWN_WIDTH 125
+
+#define LINEAR 0
+#define LOG 1
+#define LOGIT 2
+
+#define PUFF_CYAN ((Color){0, 187, 187, 255})
+#define PUFF_WHITE ((Color){241, 241, 241, 255})
+#define PUFF_BACKGROUND ((Color){6, 24, 24, 255})
+
+int hyper_count = 25;
+char *hyper_key[25] = {
+ "agent_steps",
+ "uptime",
+ "env/perf",
+ "env/score",
+ "train/learning_rate",
+ "train/ent_coef",
+ "train/gamma",
+ "train/gae_lambda",
+ "train/vtrace_rho_clip",
+ "train/vtrace_c_clip",
+ "train/clip_coef",
+ "train/vf_clip_coef",
+ "train/vf_coef",
+ "train/max_grad_norm",
+ "train/beta1",
+ "train/beta2",
+ "train/eps",
+ "train/prio_alpha",
+ "train/prio_beta0",
+ "train/horizon",
+ "train/replay_ratio",
+ "train/minibatch_size",
+ "policy/hidden_size",
+ "policy/num_layers",
+ "vec/total_agents",
+};
+
+typedef struct Glyph {
+ float x;
+ float y;
+ float i;
+ float r;
+ float g;
+ float b;
+ float a;
+} Glyph;
+
+typedef struct Point {
+ float x;
+ float y;
+ float z;
+ float c;
+} Point;
+
+typedef struct {
+ float click_x;
+ float click_y;
+ float x;
+ float y;
+ int env_idx;
+ int ary_idx;
+ bool active;
+} Tooltip;
+
+typedef struct {
+ char *key;
+ float *ary;
+ int n;
+} Hyper;
+
+typedef struct {
+ char *key;
+ Hyper *hypers;
+ int n;
+} Env;
+
+typedef struct {
+ Env *envs;
+ int n;
+} Dataset;
+
+typedef struct PlotArgs {
+ float mmin[4];
+ float mmax[4];
+ int scale[4];
+ int width;
+ int height;
+ int title_font_size;
+ int axis_font_size;
+ int axis_tick_font_size;
+ int legend_font_size;
+ int line_width;
+ int tick_length;
+ int top_margin;
+ int bottom_margin;
+ int left_margin;
+ int right_margin;
+ int tick_margin;
+ Color font_color;
+ Color background_color;
+ Color axis_color;
+ char* x_label;
+ char* y_label;
+ char* z_label;
+ Font font;
+ Font font_small;
+ Camera3D camera;
+} PlotArgs;
+
+PlotArgs DEFAULT_PLOT_ARGS = {
+ .mmin = {0.0f, 0.0f, 0.0f, 0.0f},
+ .mmax = {0.0f, 0.0f, 0.0f, 0.0f},
+ .scale = {0, 0, 0, 0},
+ .width = 960,
+ .height = 540 - SETTINGS_HEIGHT,
+ .title_font_size = 32,
+ .axis_font_size = 32,
+ .axis_tick_font_size = 16,
+ .legend_font_size = 12,
+ .line_width = 2,
+ .tick_length = 8,
+ .tick_margin = 8,
+ .top_margin = 70,
+ .bottom_margin = 70,
+ .left_margin = 100,
+ .right_margin = 100,
+ .font_color = PUFF_WHITE,
+ .background_color = PUFF_BACKGROUND,
+ .axis_color = PUFF_WHITE,
+ .x_label = "Cost",
+ .y_label = "Score",
+ .z_label = "Train/Learning Rate",
+};
+
+
+Hyper* get_hyper(Dataset *data, char *env, char* hyper) {
+ for (int i = 0; i < data->n; i++) {
+ if (strcmp(data->envs[i].key, env) != 0) {
+ continue;
+ }
+ for (int j = 0; j < data->envs[i].n; j++) {
+ if (strcmp(data->envs[i].hypers[j].key, hyper) == 0) {
+ return &data->envs[i].hypers[j];
+ }
+ }
+ }
+ printf("Error: hyper %s not found in env %s\n", hyper, env);
+ exit(1);
+ return NULL;
+}
+
+float safe_log10(float x) {
+ if (x <= 0) {
+ return x;
+ }
+ return log10(x);
+}
+
+float scale_val(int scale, float val) {
+ if (scale == LINEAR) {
+ return val;
+ } else if (scale == LOG) {
+ return safe_log10(val);
+ } else if (scale == LOGIT) {
+ return safe_log10(1 - val);
+ } else {
+ return val;
+ }
+}
+
+float unscale_val(int scale, float val) {
+ if (scale == LINEAR) {
+ return val;
+ } else if (scale == LOG) {
+ return powf(10, val);
+ } else if (scale == LOGIT) {
+ return 1 / (1 + powf(10, val));
+ }
+ return val;
+}
+
+Color rgb(float h) {
+ return ColorFromHSV(120*(1.0 + h), 0.8f, 0.15f);
+}
+
+void draw_axes(PlotArgs args) {
+ DrawLine(args.left_margin, args.top_margin,
+ args.left_margin, args.height - args.bottom_margin, PUFF_WHITE);
+ DrawLine(args.left_margin, args.height - args.bottom_margin,
+ args.width - args.right_margin, args.height - args.bottom_margin, PUFF_WHITE);
+}
+
+const char* format_tick_label(double value) {
+ static char buffer[32];
+
+ if (fabs(value) < 1e-10) {
+ strcpy(buffer, "0");
+ return buffer;
+ }
+
+ if (fabs(value) < 0.001 || fabs(value) > 10000) {
+ snprintf(buffer, sizeof(buffer), "%.3e", value);
+ } else {
+ snprintf(buffer, sizeof(buffer), "%.3f", value);
+ }
+
+ return buffer;
+}
+
+void label_ticks(char ticks[][32], PlotArgs args, int axis_idx, int tick_n) {
+ float mmin = scale_val(args.scale[axis_idx], args.mmin[axis_idx]);
+ float mmax = scale_val(args.scale[axis_idx], args.mmax[axis_idx]);
+ for (int i=0; iary;
+ float mmin = ary[0];
+ float mmax = ary[0];
+ for (int j=0; jn; j++) {
+ if (filter != NULL && !filter[j]) {
+ continue;
+ }
+ mmin = fmin(mmin, ary[j]);
+ mmax = fmax(mmax, ary[j]);
+ }
+
+ mmin = scale_val(x_scale, mmin);
+ mmax = scale_val(x_scale, mmax);
+
+ float left = args.left_margin + (mmin - x_min)/(x_max - x_min)*plot_width;
+ float right = args.left_margin + (mmax - x_min)/(x_max - x_min)*plot_width;
+
+ // TODO - rough patch
+ left = fminf(fmax(left, args.left_margin), width - args.right_margin);
+ right = fmaxf(fmin(right, width - args.right_margin), 0);
+ DrawRectangle(left, args.top_margin + i*dy, right - left, dy, color);
+}
+
+void plot_gl(Glyph* glyphs, int size, Shader* shader) {
+ int n = size;
+
+ GLuint vao = 0;
+ GLuint vbo = 0;
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+ glGenBuffers(1, &vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ glBufferData(GL_ARRAY_BUFFER, n*sizeof(Glyph), glyphs, GL_STATIC_DRAW);
+ glVertexAttribPointer(shader->locs[SHADER_LOC_VERTEX_POSITION], 3, GL_FLOAT, GL_FALSE, sizeof(Glyph), 0);
+ glEnableVertexAttribArray(shader->locs[SHADER_LOC_VERTEX_POSITION]);
+ int vertexColorLoc = shader->locs[SHADER_LOC_VERTEX_COLOR];
+ glVertexAttribPointer(vertexColorLoc, 4, GL_FLOAT, GL_FALSE, sizeof(Glyph), (void*)(3*sizeof(float)));
+ glEnableVertexAttribArray(vertexColorLoc);
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+
+ rlDrawRenderBatchActive();
+ rlSetBlendFactors(GL_ONE, GL_ONE, GL_MAX);
+ rlSetBlendMode(RL_BLEND_CUSTOM);
+ int currentTimeLoc = GetShaderLocation(*shader, "currentTime");
+ glUseProgram(shader->id);
+ glUniform1f(currentTimeLoc, GetTime());
+ Matrix modelViewProjection = MatrixMultiply(rlGetMatrixModelview(), rlGetMatrixProjection());
+ glUniformMatrix4fv(shader->locs[SHADER_LOC_MATRIX_MVP], 1, false, MatrixToFloat(modelViewProjection));
+ glBindVertexArray(vao);
+ glDrawArrays(GL_POINTS, 0, n);
+ glBindVertexArray(0);
+ glUseProgram(0);
+ glDeleteBuffers(1, &vbo);
+ glDeleteVertexArrays(1, &vao);
+ rlSetBlendMode(RL_BLEND_ALPHA);
+}
+
+void GuiDropdownFilter(int x, int y, char* options, int *selection, bool *dropdown_active,
+ Vector2 focus, char *text1, float *text1_val, char *text2, float *text2_val) {
+ Rectangle rect = {x, y, DROPDOWN_WIDTH, SETTINGS_HEIGHT};
+ if (GuiDropdownBox(rect, options, selection, *dropdown_active)) {
+ *dropdown_active = !*dropdown_active;
+ }
+ Rectangle text1_rect = {x + DROPDOWN_WIDTH, y, TOGGLE_WIDTH, SETTINGS_HEIGHT};
+ bool text1_active = CheckCollisionPointRec(focus, text1_rect);
+ if (GuiTextBox(text1_rect, text1, 32, text1_active)) {
+ *text1_val = atof(text1);
+ }
+ Rectangle text2_rect = {x + DROPDOWN_WIDTH + TOGGLE_WIDTH, y, TOGGLE_WIDTH, SETTINGS_HEIGHT};
+ bool text2_active = CheckCollisionPointRec(focus, text2_rect);
+ if (GuiTextBox(text2_rect, text2, 32, text2_active)) {
+ *text2_val = atof(text2);
+ }
+}
+
+void apply_filter(bool* filter, Hyper* param, float min, float max) {
+ for (int i=0; in; i++) {
+ float val = param->ary[i];
+ if (val < min || val > max) {
+ filter[i] = false;
+ }
+ }
+}
+
+void autoscale(Point* points, int size, PlotArgs *args) {
+ float mmin[4] = {FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX};
+ float mmax[4] = {-FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX};
+ for (int i=0; i mmax[j]) mmax[j] = val;
+ }
+ }
+ for (int j=0; j<4; j++) {
+ args->mmin[j] = mmin[j];
+ args->mmax[j] = mmax[j];
+ }
+}
+
+void toPx(Point *points, Glyph* glyphs, int size, PlotArgs args) {
+ float mmin[4];
+ float mmax[4];
+ float delta[4];
+ for (int j=0; j<4; j++) {
+ mmin[j] = scale_val(args.scale[j], args.mmin[j]);
+ mmax[j] = scale_val(args.scale[j], args.mmax[j]);
+ delta[j] = mmax[j] - mmin[j];
+ }
+
+ for (int i = 0; i < size; i++) {
+ Point p = points[i];
+ float xi = scale_val(args.scale[0], p.x);
+ float yi = scale_val(args.scale[1], p.y);
+ float zi = scale_val(args.scale[2], p.z);
+ float px, py;
+
+ if (args.mmin[2] != 0 || args.mmax[2] != 0) {
+ Vector3 v = (Vector3){
+ (xi - mmin[0])/delta[0],
+ (yi - mmin[1])/delta[1],
+ (zi - mmin[2])/delta[2]
+ };
+ assert(args.camera.fovy != 0);
+ Vector2 screen_pos = GetWorldToScreenEx(v, args.camera, args.width, args.height);
+ px = screen_pos.x;
+ py = screen_pos.y;
+ } else {
+ // TODO: Check margins
+ px = args.left_margin + (xi - mmin[0]) / delta[0] * (args.width - args.left_margin - args.right_margin);
+ py = args.height - args.bottom_margin - (yi - mmin[1]) / delta[1] * (args.height - args.top_margin - args.bottom_margin);
+ }
+
+ float cmap = points[i].c;
+ cmap = scale_val(args.scale[3], cmap);
+ float c_min = mmin[3];
+ float c_max = mmax[3];
+ if (c_min != c_max) {
+ cmap = (cmap - c_min)/(c_max - c_min);
+ }
+ Color c = rgb(cmap);
+ glyphs[i] = (Glyph){
+ px,
+ py,
+ i,
+ c.r/255.0f,
+ c.g/255.0f,
+ c.b/255.0f,
+ c.a/255.0f,
+ };
+ }
+}
+
+void update_closest(Tooltip* tooltip, Vector2 *indices, Glyph* glyphs, int size, float x_offset, float y_offset) {
+ float dx = tooltip->click_x - tooltip->x;
+ float dy = tooltip->click_y - tooltip->y;
+ float dist = sqrt(dx*dx + dy*dy);
+
+ for (int i=0; iclick_x;
+ dy = y_offset + glyphs[i].y - tooltip->click_y;
+ float d = sqrt(dx*dx + dy*dy);
+ if (d < dist) {
+ dist = d;
+ tooltip->x = x_offset + glyphs[i].x;
+ tooltip->y = y_offset + glyphs[i].y;
+ tooltip->env_idx = indices[i].x;
+ tooltip->ary_idx = indices[i].y;
+ }
+ }
+}
+
+void copy_hypers_to_clipboard(Env *env, char* buffer, int ary_idx) {
+ char* start = buffer;
+ char* prefix = NULL;
+ int prefix_len = 0;
+ for (int hyper_idx = 0; hyper_idx < env->n; hyper_idx++) {
+ Hyper *hyper = &env->hypers[hyper_idx];
+ char *slash = strchr(hyper->key, '/');
+ if (!slash || ary_idx >= hyper->n) {
+ continue;
+ }
+
+ if (prefix == NULL || strncmp(prefix, hyper->key, prefix_len) != 0) {
+ if (prefix != NULL) {
+ buffer += sprintf(buffer, "\n");
+ }
+ prefix = hyper->key;
+ prefix_len = slash - prefix;
+ buffer += sprintf(buffer, "[");
+ snprintf(buffer, prefix_len+1, "%s", prefix);
+ buffer += prefix_len;
+ buffer += sprintf(buffer, "]\n");
+ }
+
+ char* suffix = slash + 1;
+ double val = hyper->ary[ary_idx];
+ if (strcmp(suffix, "total_timesteps") == 0) {
+ // Use agent_steps (training-only) instead of total_timesteps (train+eval)
+ for (int k = 0; k < env->n; k++) {
+ if (strcmp(env->hypers[k].key, "agent_steps") == 0) {
+ val = env->hypers[k].ary[ary_idx];
+ break;
+ }
+ }
+ buffer += sprintf(buffer, "%s = %lld\n", suffix, (long long)(val * 1e6));
+ } else if (strcmp(suffix, "agent_steps") == 0) {
+ buffer += sprintf(buffer, "%s = %lld\n", suffix, (long long)(val * 1e6));
+ } else if (val == (long long)val) {
+ buffer += sprintf(buffer, "%s = %lld\n", suffix, (long long)val);
+ } else {
+ buffer += sprintf(buffer, "%s = %g\n", suffix, val);
+ }
+ }
+ buffer[0] = '\0';
+ SetClipboardText(start);
+}
+
+//strof bottlenecks loads
+float fast_atof(char **s) {
+ char *p = *s;
+ float sign = 1.0f;
+ if (*p == '-') {
+ sign = -1.0f; p++;
+ }
+ float val = 0.0f;
+ while (*p >= '0' && *p <= '9') {
+ val = val * 10.0f + (*p++ - '0');
+ }
+ if (*p == '.') {
+ p++;
+ float frac = 0.1f;
+ while (*p >= '0' && *p <= '9') {
+ val += (*p++ - '0') * frac; frac *= 0.1f;
+ }
+ }
+ if (*p == 'e' || *p == 'E') {
+ p++;
+ int esign = 1;
+ if (*p == '-') {
+ esign = -1; p++;
+ } else if (*p == '+') {
+ p++;
+ }
+ int exp = 0;
+ while (*p >= '0' && *p <= '9') {
+ exp = exp * 10 + (*p++ - '0');
+ }
+ val *= powf(10.0f, esign * exp);
+ }
+ *s = p;
+ return sign * val;
+}
+
+int main(void) {
+ FILE *file = fopen("resources/constellation/experiments.json", "r");
+ if (!file) {
+ printf("Error opening file\n");
+ return 1;
+ }
+
+ // Read in file
+ fseek(file, 0, SEEK_END);
+ long file_size = ftell(file);
+ fseek(file, 0, SEEK_SET);
+ char *json_str = malloc(file_size + 1);
+ fread(json_str, 1, file_size, file);
+ json_str[file_size] = '\0';
+ fclose(file);
+ cJSON *root = cJSON_Parse(json_str);
+ if (!root) {
+ printf("JSON parse error: %.100s\n", cJSON_GetErrorPtr());
+ free(json_str);
+ return 1;
+ }
+ if (!cJSON_IsObject(root)) {
+ printf("Error: Root is not an object\n");
+ return 1;
+ }
+
+ // Load in dataset
+ Dataset data = {NULL, 0};
+ cJSON *json_env = root->child;
+ while (json_env) {
+ data.n++;
+ json_env = json_env->next;
+ }
+
+ Env *envs = calloc(data.n, sizeof(Env));
+ data.envs = envs;
+ json_env = root->child;
+ int max_data_points = 0;
+ for (int i=0; ichild;
+ while (json_hyper) {
+ envs[i].n++;
+ json_hyper = json_hyper->next;
+ }
+ envs[i].key = json_env->string;
+ envs[i].hypers = calloc(envs[i].n, sizeof(Hyper));
+ json_hyper = json_env->child;
+ for (int j=0; jnext) {
+ envs[i].hypers[j].key = json_hyper->string;
+ int capacity = 1;
+ for (char* p = json_hyper->valuestring; *p; p++) {
+ if (*p == ',') {
+ capacity++;
+ }
+ }
+ if (capacity > max_data_points) {
+ max_data_points = capacity;
+ }
+ envs[i].hypers[j].ary = calloc(capacity, sizeof(float));
+
+ int n = 0;
+ char* s = json_hyper->valuestring;
+ while (*s) {
+ envs[i].hypers[j].ary[n++] = fast_atof(&s);
+ if (*s == ',') {
+ s++;
+ }
+ }
+ envs[i].hypers[j].n = n;
+ }
+ json_env = json_env->next;
+ }
+ int total_points = 0;
+ for (int i=0; i 0) strcat(options, ";");
+ strcat(options, hyper_key[i]);
+ }
+
+ // Options with extra "env_name;"
+ char* extra = "env_name;";
+ char *env_hyper_options = malloc(options_len + strlen(extra));
+ strcpy(env_hyper_options, extra);
+ strcat(env_hyper_options, options);
+
+ // Env names as semi-colon-separated string
+ size_t env_options_len = 4;
+ for (int i = 0; i < data.n; i++) {
+ env_options_len += strlen(data.envs[i].key) + 1;
+ }
+ char *env_options = malloc(env_options_len);
+ strcpy(env_options, "all;");
+ env_options[4] = '\0';
+ for (int i = 0; i < data.n; i++) {
+ if (i > 0) strcat(env_options, ";");
+ strcat(env_options, data.envs[i].key);
+ }
+
+ char* clipboard = malloc(16384);
+
+ // Points
+ printf("total points: %d", total_points);
+ Point* points = calloc(total_points, sizeof(Point));
+ Glyph* glyphs = calloc(total_points, sizeof(Glyph));
+ Vector2* env_indices = calloc(total_points, sizeof(Vector2));
+
+ // Initialize Raylib
+ SetConfigFlags(FLAG_MSAA_4X_HINT);
+ InitWindow(2*DEFAULT_PLOT_ARGS.width, 2*DEFAULT_PLOT_ARGS.height + 2*SETTINGS_HEIGHT, "Puffer Constellation");
+ Texture2D puffer = LoadTexture("resources/shared/puffers.png");
+
+ DEFAULT_PLOT_ARGS.font = LoadFontEx("resources/shared/JetBrainsMono-SemiBold.ttf", 32, NULL, 255);
+ DEFAULT_PLOT_ARGS.font_small = LoadFontEx("resources/shared/JetBrainsMono-SemiBold.ttf", 16, NULL, 255);
+ Font gui_font = LoadFontEx("resources/shared/JetBrainsMono-SemiBold.ttf", 14, NULL, 255);
+
+ GuiLoadStyle("resources/constellation/puffer.rgs");
+ GuiSetFont(gui_font);
+ ClearBackground(PUFF_BACKGROUND);
+ SetTargetFPS(60);
+
+ Shader shader = LoadShader(
+ TextFormat("resources/constellation/point_particle_%i.vs", GLSL_VERSION),
+ TextFormat("resources/constellation/point_particle_%i.fs", GLSL_VERSION)
+ );
+ Shader blur_shader = LoadShader(
+ TextFormat("resources/constellation/blur_%i.vs", GLSL_VERSION),
+ TextFormat("resources/constellation/blur_%i.fs", GLSL_VERSION)
+ );
+
+ // Allows the vertex shader to set the point size of each particle individually
+ #ifndef GRAPHICS_API_OPENGL_ES2
+ glEnable(GL_PROGRAM_POINT_SIZE);
+ #endif
+
+ PlotArgs args1 = DEFAULT_PLOT_ARGS;
+ args1.camera = (Camera3D){ 0 };
+ args1.camera.position = (Vector3){ 1.5f, 1.25f, 1.5f };
+ args1.camera.target = (Vector3){ 0.5f, 0.5f, 0.5f };
+ args1.camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
+ args1.camera.fovy = 45.0f;
+ args1.camera.projection = CAMERA_PERSPECTIVE;
+ args1.scale[0] = 1;
+ args1.scale[2] = 1;
+ RenderTexture2D fig1 = LoadRenderTexture(args1.width, args1.height);
+ RenderTexture2D fig1_overlay = LoadRenderTexture(args1.width, args1.height);
+ int fig_env_idx = 0;
+ bool fig_env_active = false;
+ bool fig_x_active = false;
+ int fig_x_idx = 1;
+ bool fig_xscale_active = false;
+ bool fig_y_active = false;
+ int fig_y_idx = 2;
+ bool fig_yscale_active = false;
+ bool fig_z_active = false;
+ int fig_z_idx = 0;
+ bool fig_zscale_active = false;
+ int fig_color_idx = 0;
+ bool fig_color_active = false;
+ bool fig_colorscale_active = false;
+ bool fig_range1_active = false;
+ int fig_range1_idx = 2;
+ char fig_range1_min[32] = {0};
+ char fig_range1_max[32] = {0};
+ float fig_range1_min_val = 0;
+ float fig_range1_max_val = FLT_MAX;
+ bool fig_range2_active = false;
+ int fig_range2_idx = 1;
+ char fig_range2_min[32] = {0};
+ char fig_range2_max[32] = {0};
+ float fig_range2_min_val = FLT_MIN;
+ float fig_range2_max_val = FLT_MAX;
+ int fig_box_idx = LOG;
+ bool fig_box_active = false;
+
+ char* scale_options = "linear;log;logit";
+
+ PlotArgs args2 = DEFAULT_PLOT_ARGS;
+ RenderTexture2D fig2 = LoadRenderTexture(args2.width, args2.height);
+ args2.right_margin = 50;
+ args2.scale[0] = 1;
+
+ PlotArgs args3 = DEFAULT_PLOT_ARGS;
+ RenderTexture2D fig3 = LoadRenderTexture(args3.width, args3.height);
+ RenderTexture2D fig3_overlay = LoadRenderTexture(args1.width, args1.height);
+ args3.left_margin = 10;
+ args3.right_margin = 10;
+ args3.top_margin = 10;
+ args3.bottom_margin = 10;
+ args3.x_label = "tsne1";
+ args3.y_label = "tsne2";
+
+ PlotArgs args4 = DEFAULT_PLOT_ARGS;
+ RenderTexture2D fig4 = LoadRenderTexture(args4.width, args4.height);
+ args4.x_label = "Value";
+ args4.y_label = "Hyperparameter";
+ args4.left_margin = 170;
+ args4.right_margin = 50;
+ args4.top_margin = 10;
+ args4.bottom_margin = 50;
+
+ Hyper* x;
+ Hyper* y;
+ Hyper* z;
+ Hyper* c;
+ char* x_label;
+ char* y_label;
+ char* z_label;
+
+ bool *filter = calloc(max_data_points, sizeof(bool));
+
+ Tooltip tooltip = {0};
+
+ Vector2 focus = {0, 0};
+
+ while (!WindowShouldClose()) {
+ bool right_clicked = false;
+
+ BeginDrawing();
+ ClearBackground(PUFF_BACKGROUND);
+
+ if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+ focus = GetMousePosition();
+ tooltip.active = false;
+ }
+ if (IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) {
+ Vector2 mouse_pos = GetMousePosition();
+ right_clicked = true;
+ tooltip.active = true;
+ tooltip.click_x = mouse_pos.x;
+ tooltip.click_y = mouse_pos.y;
+ }
+
+ // Figure 1
+ x_label = hyper_key[fig_x_idx];
+ y_label = hyper_key[fig_y_idx];
+ z_label = hyper_key[fig_z_idx];
+ args1.x_label = x_label;
+ args1.y_label = y_label;
+ args1.z_label = z_label;
+ int start = 0;
+ int end = data.n;
+ if (fig_env_idx != 0) {
+ start = fig_env_idx - 1;
+ end = fig_env_idx;
+ }
+ BeginTextureMode(fig1);
+ ClearBackground(PUFF_BACKGROUND);
+
+ int size = 0;
+ for (int i=start; in; j++) {
+ filter[j] = true;
+ }
+ Hyper* filter_param_1 = get_hyper(&data, env, hyper_key[fig_range1_idx]);
+ apply_filter(filter, filter_param_1, fig_range1_min_val, fig_range1_max_val);
+ Hyper* filter_param_2 = get_hyper(&data, env, hyper_key[fig_range2_idx]);
+ apply_filter(filter, filter_param_2, fig_range2_min_val, fig_range2_max_val);
+
+ for (int j=0; jn; j++) {
+ if (!filter[j]) {
+ continue;
+ }
+ points[size] = (Point){
+ x->ary[j],
+ y->ary[j],
+ z->ary[j],
+ (fig_color_idx == 0) ? i/(float)data.n : c->ary[j],
+ };
+ env_indices[size] = (Vector2){i, j};
+ size++;
+ }
+ }
+ autoscale(points, size, &args1);
+ toPx(points, glyphs, size, args1);
+ update_closest(&tooltip, env_indices, glyphs, size, 0, 2*SETTINGS_HEIGHT);
+ plot_gl(glyphs, size, &shader);
+
+ BeginMode3D(args1.camera);
+ CustomUpdateCamera(&args1.camera, CAMERA_ORBITAL_SPEED);
+ draw_axes3();
+ EndMode3D();
+ EndTextureMode();
+
+ // Figure 2
+ x_label = hyper_key[fig_x_idx];
+ y_label = hyper_key[fig_y_idx];
+ args2.scale[0] = args1.scale[0];
+ args2.scale[1] = args1.scale[1];
+ args2.x_label = x_label;
+ args2.y_label = y_label;
+ args2.top_margin = 20;
+ args2.left_margin = 100;
+ BeginTextureMode(fig2);
+ ClearBackground(PUFF_BACKGROUND);
+
+ autoscale(points, size, &args2);
+ args2.mmin[2] = 0.0f;
+ args2.mmax[2] = 0.0f;
+ toPx(points, glyphs, size, args2);
+ update_closest(&tooltip, env_indices, glyphs, size, fig1.texture.width, 2*SETTINGS_HEIGHT);
+ plot_gl(glyphs, size, &shader);
+ draw_axes(args2);
+ draw_all_ticks(args2);
+ EndTextureMode();
+
+ // Figure 3
+ BeginTextureMode(fig3);
+ ClearBackground(PUFF_BACKGROUND);
+ size = 0;
+ for (int i=0; in; j++) {
+ filter[j] = true;
+ }
+ Hyper* filter_param_1 = get_hyper(&data, env, hyper_key[fig_range1_idx]);
+ apply_filter(filter, filter_param_1, fig_range1_min_val, fig_range1_max_val);
+ Hyper* filter_param_2 = get_hyper(&data, env, hyper_key[fig_range2_idx]);
+ apply_filter(filter, filter_param_2, fig_range2_min_val, fig_range2_max_val);
+
+ for (int j=0; jn; j++) {
+ if (!filter[j]) {
+ continue;
+ }
+ points[size] = (Point){
+ x->ary[j],
+ y->ary[j],
+ 0.0f,
+ i/(float)data.n
+ };
+ env_indices[size] = (Vector2){i, j};
+ size++;
+ }
+ }
+ autoscale(points, size, &args3);
+ toPx(points, glyphs, size, args3);
+ update_closest(&tooltip, env_indices, glyphs, size, 0, fig1.texture.height + 2*SETTINGS_HEIGHT);
+ plot_gl(glyphs, size, &shader);
+
+ //draw_axes(args3);
+ EndTextureMode();
+
+ // Figure 4
+ args4.scale[0] = fig_box_idx;
+ if (args4.scale[0] == LINEAR) {
+ args4.mmin[0] = 0.0f;
+ args4.mmax[0] = 5.0f;
+ } else if (args4.scale[0] == LOG) {
+ args4.mmin[0] = 1.0e-5f;
+ args4.mmax[0] = 1.0e5f;
+ } else if (args4.scale[0] == LOGIT) {
+ args4.mmin[0] = 0.5f;
+ args4.mmax[0] = 0.999f;
+ }
+ BeginTextureMode(fig4);
+ ClearBackground(PUFF_BACKGROUND);
+ rlSetBlendFactorsSeparate(0x0302, 0x0303, 1, 0x0303, 0x8006, 0x8006);
+ BeginBlendMode(BLEND_CUSTOM_SEPARATE);
+ Color color = Fade(PUFF_CYAN, 1.0f / (float)(end - start));
+ for (int i=start; ikey, hyper_key[fig_range1_idx]);
+ Hyper* filter_param_2 = get_hyper(&data, env->key, hyper_key[fig_range2_idx]);
+ for (int j=0; jkey, hyper_key[j]);
+ for (int k=0; kn; k++) {
+ filter[k] = true;
+ }
+ apply_filter(filter, filter_param_1, fig_range1_min_val, fig_range1_max_val);
+ apply_filter(filter, filter_param_2, fig_range2_min_val, fig_range2_max_val);
+ boxplot(hyper, args4.scale[0], j, hyper_count, args4, color, filter);
+ }
+ }
+ EndBlendMode();
+ draw_axes(args4);
+ draw_box_ticks(hyper_key, hyper_count, args4);
+ EndTextureMode();
+
+ // Figure 1-4
+ DrawTextureRec(
+ fig1.texture,
+ (Rectangle){0, 0, fig1.texture.width, -fig1.texture.height },
+ (Vector2){ 0, 2*SETTINGS_HEIGHT }, WHITE
+ );
+ BeginShaderMode(blur_shader);
+ rlSetBlendMode(RL_BLEND_ADDITIVE);
+ DrawTextureRec(
+ fig1_overlay.texture,
+ (Rectangle){0, 0, fig1_overlay.texture.width, -fig1_overlay.texture.height },
+ (Vector2){ 0, 2*SETTINGS_HEIGHT }, WHITE
+ );
+ rlSetBlendMode(RL_BLEND_ALPHA);
+ EndShaderMode();
+ DrawTextureRec(
+ fig2.texture,
+ (Rectangle){ 0, 0, fig2.texture.width, -fig2.texture.height },
+ (Vector2){ fig1.texture.width, 2*SETTINGS_HEIGHT }, WHITE
+ );
+ DrawTextureRec(
+ fig3.texture,
+ (Rectangle){ 0, 0, fig3.texture.width, -fig3.texture.height },
+ (Vector2){ 0, 2*SETTINGS_HEIGHT + fig1.texture.height }, WHITE
+ );
+ BeginShaderMode(blur_shader);
+ rlSetBlendMode(RL_BLEND_ADDITIVE);
+ DrawTextureRec(
+ fig3_overlay.texture,
+ (Rectangle){0, 0, fig3_overlay.texture.width, -fig3_overlay.texture.height },
+ (Vector2){ 0, 2*SETTINGS_HEIGHT + fig1.texture.height }, WHITE
+ );
+ rlSetBlendMode(RL_BLEND_ALPHA);
+ EndShaderMode();
+ DrawTextureRec(
+ fig4.texture,
+ (Rectangle){ 0, 0, fig4.texture.width, -fig4.texture.height },
+ (Vector2){ fig1.texture.width, fig1.texture.height + 2*SETTINGS_HEIGHT }, WHITE
+ );
+
+ // UI
+ float y = SEP + SETTINGS_HEIGHT/2.0f - MeasureTextEx(args1.font_small, "Env", args1.axis_tick_font_size, 0).y/2.0f;
+ float x = SEP;
+ DrawTextEx(args1.font_small, "Env", (Vector2){x, y}, args1.axis_tick_font_size, 0, WHITE);
+ x += MeasureTextEx(args1.font_small, "Env", args1.axis_tick_font_size, 0).x + SEP;
+
+ Rectangle fig_env_rect = {x, SEP, DROPDOWN_WIDTH, SETTINGS_HEIGHT};
+ x += DROPDOWN_WIDTH + SPACER;
+ if (GuiDropdownBox(fig_env_rect, env_options, &fig_env_idx, fig_env_active)){
+ fig_env_active = !fig_env_active;
+ }
+
+ // X axis
+ DrawTextEx(args1.font_small, "X", (Vector2){x, y}, args1.axis_tick_font_size, 0, RED);
+ x += MeasureTextEx(args1.font_small, "X", args1.axis_tick_font_size, 0).x + SEP;
+
+ Rectangle fig_x_rect = {x, SEP, DROPDOWN_WIDTH, SETTINGS_HEIGHT};
+ x += DROPDOWN_WIDTH;
+ if (GuiDropdownBox(fig_x_rect, options, &fig_x_idx, fig_x_active)){
+ fig_x_active = !fig_x_active;
+ }
+ Rectangle fig_xscale_rect = {x, SEP, TOGGLE_WIDTH, SETTINGS_HEIGHT};
+ x += TOGGLE_WIDTH + SPACER;
+ if (GuiDropdownBox(fig_xscale_rect, scale_options, &args1.scale[0], fig_xscale_active)){
+ fig_xscale_active = !fig_xscale_active;
+ }
+
+ // Y axis
+ DrawTextEx(args1.font_small, "Y", (Vector2){x, y}, args1.axis_tick_font_size, 0, GREEN);
+ x += MeasureTextEx(args1.font_small, "Y", args1.axis_tick_font_size, 0).x + SEP;
+
+ Rectangle fig_y_rect = {x, SEP, DROPDOWN_WIDTH, SETTINGS_HEIGHT};
+ x += DROPDOWN_WIDTH;
+ if (GuiDropdownBox(fig_y_rect, options, &fig_y_idx, fig_y_active)){
+ fig_y_active = !fig_y_active;
+ }
+ Rectangle fig_yscale_rect = {x, SEP, TOGGLE_WIDTH, SETTINGS_HEIGHT};
+ x += TOGGLE_WIDTH + SPACER;
+ if (GuiDropdownBox(fig_yscale_rect, scale_options, &args1.scale[1], fig_yscale_active)){
+ fig_yscale_active = !fig_yscale_active;
+ }
+
+ // Z axis
+ DrawTextEx(args1.font_small, "Z", (Vector2){x, y}, args1.axis_tick_font_size, 0, BLUE);
+ x += MeasureTextEx(args1.font_small, "Z", args1.axis_tick_font_size, 0).x + SEP;
+
+ Rectangle fig_z_rect = {x, SEP, DROPDOWN_WIDTH, SETTINGS_HEIGHT};
+ x += DROPDOWN_WIDTH;
+ if (GuiDropdownBox(fig_z_rect, options, &fig_z_idx, fig_z_active)){
+ fig_z_active = !fig_z_active;
+ }
+ Rectangle fig_zscale_rect = {x, SEP, TOGGLE_WIDTH, SETTINGS_HEIGHT};
+ x += TOGGLE_WIDTH + SPACER;
+ if (GuiDropdownBox(fig_zscale_rect, scale_options, &args1.scale[2], fig_zscale_active)){
+ fig_zscale_active = !fig_zscale_active;
+ }
+
+ // Color
+ DrawTextEx(args1.font_small, "C", (Vector2){x, y}, args1.axis_tick_font_size, 0, WHITE);
+ x += MeasureTextEx(args1.font_small, "C", args1.axis_tick_font_size, 0).x + SEP;
+
+ Rectangle fig_color_rect = {x, SEP, DROPDOWN_WIDTH, SETTINGS_HEIGHT};
+ x += DROPDOWN_WIDTH;
+ if (GuiDropdownBox(fig_color_rect, env_hyper_options, &fig_color_idx, fig_color_active)){
+ fig_color_active = !fig_color_active;
+ }
+ Rectangle fig_colorscale_rect = {x, SEP, TOGGLE_WIDTH, SETTINGS_HEIGHT};
+ x += TOGGLE_WIDTH + SPACER;
+ if (GuiDropdownBox(fig_colorscale_rect, scale_options, &args1.scale[3], fig_colorscale_active)){
+ fig_colorscale_active = !fig_colorscale_active;
+ }
+
+ // Temp hack
+ args2.scale[3] = args1.scale[3];
+ args3.scale[3] = args1.scale[3];
+ args4.scale[3] = args1.scale[3];
+
+ // Filters
+ DrawTextEx(args1.font_small, "F1", (Vector2){x, y}, args1.axis_tick_font_size, 0, WHITE);
+ x += MeasureTextEx(args1.font_small, "F1", args1.axis_tick_font_size, 0).x + SEP;
+
+ GuiDropdownFilter(x, SEP, options,
+ &fig_range1_idx, &fig_range1_active, focus, fig_range1_min,
+ &fig_range1_min_val, fig_range1_max, &fig_range1_max_val);
+ x += DROPDOWN_WIDTH + 2*TOGGLE_WIDTH + SPACER;
+
+ DrawTextEx(args1.font_small, "F2", (Vector2){x, y}, args1.axis_tick_font_size, 0, WHITE);
+ x += MeasureTextEx(args1.font_small, "F2", args1.axis_tick_font_size, 0).x + SEP;
+
+ GuiDropdownFilter(x, SEP, options,
+ &fig_range2_idx, &fig_range2_active, focus, fig_range2_min,
+ &fig_range2_min_val, fig_range2_max, &fig_range2_max_val);
+ x += DROPDOWN_WIDTH + 2*TOGGLE_WIDTH + SPACER;
+
+ // Box
+ DrawTextEx(args1.font_small, "Box", (Vector2){x, y}, args1.axis_tick_font_size, 0, WHITE);
+ x += MeasureTextEx(args1.font_small, "Box", args1.axis_tick_font_size, 0).x + SEP;
+
+ Rectangle box_rect = {x, SEP, TOGGLE_WIDTH, SETTINGS_HEIGHT};
+ if (GuiDropdownBox(box_rect, scale_options, &fig_box_idx, fig_box_active)) {
+ fig_box_active = !fig_box_active;
+ }
+
+ // Puffer
+ float width = GetScreenWidth();
+ DrawTexturePro(
+ puffer,
+ (Rectangle){0, 128, 128, 128},
+ (Rectangle){width - 48, -8, 48, 48},
+ (Vector2){0, 0},
+ 0,
+ WHITE
+ );
+
+ // Tooltip
+ int env_idx = tooltip.env_idx;
+ int ary_idx = tooltip.ary_idx;
+ Env* env = &data.envs[env_idx];
+ char* env_key = env->key;
+
+ float cost = get_hyper(&data, env_key, "uptime")->ary[ary_idx];
+ float score = get_hyper(&data, env_key, "env/score")->ary[ary_idx];
+ float steps = get_hyper(&data, env_key, "agent_steps")->ary[ary_idx];
+ if (tooltip.active) {
+ const char* text = TextFormat("%s\nscore = %f\ncost = %f\nsteps = %f", env_key, score, cost, steps);
+ Vector2 text_size = MeasureTextEx(args1.font_small, text, args1.axis_tick_font_size, 0);
+ float x = tooltip.x;
+ float y = tooltip.y;
+ if (x + text_size.x + 4 > GetScreenWidth()) {
+ x = x - text_size.x - 4;
+ }
+ if (y + text_size.y + 4 > GetScreenHeight()) {
+ y = y - text_size.y - 4;
+ }
+ DrawRectangle(x, y, text_size.x + 4, text_size.y + 4, PUFF_BACKGROUND);
+ DrawCircle(tooltip.x, tooltip.y, 2, PUFF_CYAN);
+ DrawTextEx(args1.font_small, text, (Vector2){x + 2, y + 2}, args1.axis_tick_font_size, 0, WHITE);
+ }
+ EndDrawing();
+
+ // Copy hypers to clipboard
+ if (right_clicked) {
+ copy_hypers_to_clipboard(env, clipboard, ary_idx);
+ }
+ }
+
+ // Cleanup
+ for (int i = 0; i < data.n; i++) {
+ for (int j = 0; j < envs[i].n; j++) {
+ free(envs[i].hypers[j].ary);
+ }
+ free(envs[i].hypers);
+ }
+ free(envs);
+ cJSON_Delete(root);
+ free(json_str);
+ free(options);
+ free(env_hyper_options);
+ free(env_options);
+ free(clipboard);
+ free(points);
+ free(glyphs);
+ free(env_indices);
+ free(filter);
+
+ // Raylib resources
+ UnloadShader(shader);
+ UnloadShader(blur_shader);
+ UnloadRenderTexture(fig1);
+ UnloadRenderTexture(fig1_overlay);
+ UnloadRenderTexture(fig2);
+ UnloadRenderTexture(fig3);
+ UnloadRenderTexture(fig3_overlay);
+ UnloadRenderTexture(fig4);
+ CloseWindow();
+ return 0;
+}
diff --git a/examples/vectorization.py b/examples/vectorization.py
index 4a6ff2ecb0..3e614bde5a 100644
--- a/examples/vectorization.py
+++ b/examples/vectorization.py
@@ -72,7 +72,7 @@ def close(self):
try:
vecenv = pufferlib.vector.make(SamplePufferEnv,
num_envs=1, num_workers=2, batch_size=3, backend=pufferlib.vector.Multiprocessing)
- except pufferlib.APIUsageError:
+ except (AssertionError, ValueError):
#Make sure num_envs divides num_workers, and both num_envs and num_workers should divide batch_size
pass
diff --git a/pufferlib/ocean/asteroids/asteroids.c b/ocean/asteroids/asteroids.c
similarity index 100%
rename from pufferlib/ocean/asteroids/asteroids.c
rename to ocean/asteroids/asteroids.c
diff --git a/pufferlib/ocean/asteroids/asteroids.h b/ocean/asteroids/asteroids.h
similarity index 100%
rename from pufferlib/ocean/asteroids/asteroids.h
rename to ocean/asteroids/asteroids.h
diff --git a/pufferlib/ocean/asteroids/binding.c b/ocean/asteroids/binding.c
similarity index 100%
rename from pufferlib/ocean/asteroids/binding.c
rename to ocean/asteroids/binding.c
diff --git a/pufferlib/ocean/battle/battle.c b/ocean/battle/battle.c
similarity index 99%
rename from pufferlib/ocean/battle/battle.c
rename to ocean/battle/battle.c
index c6193318fb..ca46049485 100644
--- a/pufferlib/ocean/battle/battle.c
+++ b/ocean/battle/battle.c
@@ -14,7 +14,7 @@
int main() {
// Weights are exported by running puffer export
- //Weights* weights = load_weights("resources/puffer_battle_weights.bin", 137743);
+ //Weights* weights = load_weights("resources/puffer_battle_weights.bin");
//int logit_sizes[2] = {9, 5};
//LinearLSTM* net = make_linearlstm(weights, num_agents, num_obs, logit_sizes, 2);
diff --git a/pufferlib/ocean/battle/battle.h b/ocean/battle/battle.h
similarity index 100%
rename from pufferlib/ocean/battle/battle.h
rename to ocean/battle/battle.h
diff --git a/pufferlib/ocean/battle/binding.c b/ocean/battle/binding.c
similarity index 100%
rename from pufferlib/ocean/battle/binding.c
rename to ocean/battle/binding.c
diff --git a/pufferlib/ocean/battle/rlights.h b/ocean/battle/rlights.h
similarity index 100%
rename from pufferlib/ocean/battle/rlights.h
rename to ocean/battle/rlights.h
diff --git a/pufferlib/ocean/battle/simplex.h b/ocean/battle/simplex.h
similarity index 100%
rename from pufferlib/ocean/battle/simplex.h
rename to ocean/battle/simplex.h
diff --git a/ocean/benchmark/benchmark.c b/ocean/benchmark/benchmark.c
new file mode 100644
index 0000000000..4c9abaaf95
--- /dev/null
+++ b/ocean/benchmark/benchmark.c
@@ -0,0 +1,100 @@
+#include
+#include
+#include
+#include
+#include "benchmark.h"
+
+int main(int argc, char** argv) {
+ int num_envs = argc > 1 ? atoi(argv[1]) : 1;
+ int threads = argc > 2 ? atoi(argv[2]) : 1;
+ int compute = argc > 3 ? atoi(argv[3]) : 0;
+ int bandwidth = argc > 4 ? atoi(argv[4]) : 1000;
+ int timeout = argc > 5 ? atoi(argv[5]) : 3;
+
+ if (argc == 1) {
+ printf("Usage: %s [num_envs] [threads] [compute] [bandwidth] [timeout]\n", argv[0]);
+ printf(" num_envs: number of parallel environments (default: 1)\n");
+ printf(" threads: OpenMP threads (default: 1)\n");
+ printf(" compute: sinf() iterations per step (default: 0)\n");
+ printf(" bandwidth: bytes written per step (default: 1000)\n");
+ printf(" timeout: test duration in seconds (default: 3)\n\n");
+ }
+
+ if (threads > 0) {
+ omp_set_num_threads(threads);
+ }
+
+ printf("Benchmark: envs=%d, threads=%d, compute=%d, bandwidth=%d\n",
+ num_envs, threads, compute, bandwidth);
+
+ unsigned char* observations = (unsigned char*)calloc(num_envs*bandwidth, sizeof(unsigned char));
+ double* actions = (double*)calloc(num_envs, sizeof(double));
+ float* rewards = (float*)calloc(num_envs, sizeof(float));
+ float* terminals = (float*)calloc(num_envs, sizeof(float));
+
+ // Allocate env array with pointers into contiguous buffers
+ Benchmark* envs = (Benchmark*)calloc(num_envs, sizeof(Benchmark));
+ for (int i = 0; i < num_envs; i++) {
+ envs[i].bandwidth = bandwidth;
+ envs[i].compute = compute;
+ envs[i].observations = observations + i * bandwidth;
+ envs[i].actions = actions + i;
+ envs[i].rewards = rewards + i;
+ envs[i].terminals = terminals + i;
+ c_reset(&envs[i]);
+ }
+
+ // Warmup
+ for (int w = 0; w < 10; w++) {
+ if (threads > 0) {
+ #pragma omp parallel for schedule(static)
+ for (int i = 0; i < num_envs; i++) {
+ c_step(&envs[i]);
+ }
+ } else {
+ for (int i = 0; i < num_envs; i++) {
+ c_step(&envs[i]);
+ }
+ }
+ }
+
+ int start = time(NULL);
+ long num_steps = 0;
+ while (time(NULL) - start < timeout) {
+ if (threads > 0) {
+ #pragma omp parallel for schedule(static)
+ for (int i = 0; i < num_envs; i++) {
+ c_step(&envs[i]);
+ }
+ } else {
+ for (int i = 0; i < num_envs; i++) {
+ c_step(&envs[i]);
+ }
+ }
+ num_steps += num_envs;
+ }
+
+ int elapsed = time(NULL) - start;
+ double sps = (double)num_steps / elapsed;
+ double bytes_per_sec = sps * bandwidth;
+
+ // Checksum to prevent optimization
+ unsigned char checksum = 0;
+ for (int i = 0; i < num_envs; i++) {
+ checksum ^= envs[i].observations[0];
+ }
+
+ printf("Checksum: %d\n", checksum);
+ printf(" steps=%ld, elapsed=%ds\n", num_steps, elapsed);
+ printf(" throughput: %.2f M steps/s\n", sps / 1e6);
+ printf(" bandwidth: %.2f GB/s\n", bytes_per_sec / 1e9);
+
+ // Cleanup
+ free(observations);
+ free(actions);
+ free(rewards);
+ free(terminals);
+ free(envs);
+
+ return 0;
+}
diff --git a/ocean/benchmark/benchmark.h b/ocean/benchmark/benchmark.h
new file mode 100644
index 0000000000..8ee3819d3e
--- /dev/null
+++ b/ocean/benchmark/benchmark.h
@@ -0,0 +1,34 @@
+#include
+#include
+
+typedef struct {
+ float perf;
+ float score;
+ float n;
+} Log;
+
+typedef struct {
+ Log log;
+ unsigned char* observations;
+ double* actions;
+ float* rewards;
+ float* terminals;
+ int num_agents;
+ int bandwidth;
+ int compute;
+} Benchmark;
+
+void c_reset(Benchmark* env) {}
+
+void c_step(Benchmark* env) {
+ float result = 0;
+ for (int i=0; icompute; i++) {
+ result = sinf(result + 0.1f);
+ }
+
+ //memset(env->observations, result, env->bandwidth);
+}
+
+void c_render(Benchmark* env) { }
+
+void c_close(Benchmark* env) { }
diff --git a/ocean/benchmark/binding.c b/ocean/benchmark/binding.c
new file mode 100644
index 0000000000..946d1811ec
--- /dev/null
+++ b/ocean/benchmark/binding.c
@@ -0,0 +1,20 @@
+#include "benchmark.h"
+#define OBS_SIZE 512 // TODO: Current API forces you to edit this per obs size
+#define NUM_ATNS 1
+#define ACT_SIZES {2}
+#define OBS_TYPE UNSIGNED_CHAR
+#define ACT_TYPE DOUBLE
+
+#define Env Benchmark
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->compute = dict_get(kwargs, "compute")->value;
+ env->bandwidth = dict_get(kwargs, "bandwidth")->value;
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+}
diff --git a/ocean/blastar/binding.c b/ocean/blastar/binding.c
new file mode 100644
index 0000000000..b1fc45284d
--- /dev/null
+++ b/ocean/blastar/binding.c
@@ -0,0 +1,28 @@
+#include "blastar.h"
+#define OBS_SIZE 10
+#define NUM_ATNS 1
+#define ACT_SIZES {6}
+#define OBS_TYPE FLOAT
+#define ACT_TYPE DOUBLE
+
+#define Env Blastar
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ int num_obs = dict_get(kwargs, "num_obs")->value;
+ init(env, num_obs);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "lives", log->lives);
+ dict_set(out, "vertical_closeness_rew", log->vertical_closeness_rew);
+ dict_set(out, "fired_bullet_rew", log->fired_bullet_rew);
+ dict_set(out, "kill_streak", log->kill_streak);
+ dict_set(out, "hit_enemy_with_bullet_rew", log->hit_enemy_with_bullet_rew);
+ dict_set(out, "avg_score_difference", log->avg_score_difference);
+}
diff --git a/pufferlib/ocean/blastar/blastar.c b/ocean/blastar/blastar.c
similarity index 97%
rename from pufferlib/ocean/blastar/blastar.c
rename to ocean/blastar/blastar.c
index a4400288d0..8c5d322f56 100644
--- a/pufferlib/ocean/blastar/blastar.c
+++ b/ocean/blastar/blastar.c
@@ -27,7 +27,7 @@ void get_input(Blastar* env) {
}
int demo() {
- Weights* weights = load_weights(WEIGHTS_PATH, NUM_WEIGHTS);
+ Weights* weights = load_weights(WEIGHTS_PATH);
int logit_sizes[1] = {ACTIONS_SIZE};
LinearLSTM* net = make_linearlstm(weights, 1, OBSERVATIONS_SIZE, logit_sizes, 1);
Blastar env = {
diff --git a/pufferlib/ocean/blastar/blastar.h b/ocean/blastar/blastar.h
similarity index 98%
rename from pufferlib/ocean/blastar/blastar.h
rename to ocean/blastar/blastar.h
index 7020c0c1c2..6bea710e4f 100644
--- a/pufferlib/ocean/blastar/blastar.h
+++ b/ocean/blastar/blastar.h
@@ -85,9 +85,10 @@ typedef struct Blastar {
Player player;
Enemy enemy;
float* observations;
- int* actions;
+ double* actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
+ int num_agents;
Log log;
} Blastar;
@@ -151,9 +152,9 @@ void init(Blastar* env, int num_obs) {
void allocate(Blastar* env, int num_obs) {
init(env, num_obs);
env->observations = (float*)calloc(env->num_obs, sizeof(float));
- env->actions = (int*)calloc(1, sizeof(int));
+ env->actions = (double*)calloc(1, sizeof(double));
env->rewards = (float*)calloc(1, sizeof(float));
- env->terminals = (unsigned char*)calloc(1, sizeof(unsigned char));
+ env->terminals = (float*)calloc(1, sizeof(float));
}
void free_allocated(Blastar* env) {
diff --git a/pufferlib/ocean/boids/binding.c b/ocean/boids/binding.c
similarity index 100%
rename from pufferlib/ocean/boids/binding.c
rename to ocean/boids/binding.c
diff --git a/pufferlib/ocean/boids/boids.c b/ocean/boids/boids.c
similarity index 100%
rename from pufferlib/ocean/boids/boids.c
rename to ocean/boids/boids.c
diff --git a/pufferlib/ocean/boids/boids.h b/ocean/boids/boids.h
similarity index 100%
rename from pufferlib/ocean/boids/boids.h
rename to ocean/boids/boids.h
diff --git a/ocean/boxoban/binding.c b/ocean/boxoban/binding.c
new file mode 100644
index 0000000000..86c6b341d8
--- /dev/null
+++ b/ocean/boxoban/binding.c
@@ -0,0 +1,29 @@
+#define BOXOBAN_MAPS_IMPLEMENTATION //enables mmap
+#include "boxoban.h"
+#define OBS_SIZE 400
+#define NUM_ATNS 1
+#define ACT_SIZES {5}
+#define OBS_TENSOR_T ByteTensor
+
+
+#define Env Boxoban
+#include "vecenv.h"
+
+
+void my_init(Env* env, Dict* kwargs) {
+ env->difficulty_id = (int)dict_get(kwargs, "difficulty")->value;
+ env->size = 10;
+ env->num_agents = 1;
+ env->max_steps = (int)dict_get(kwargs, "max_steps")->value;
+ env->int_r_coeff = (float)dict_get(kwargs, "int_r_coeff")->value;
+ env->target_loss_pen_coeff = (float)dict_get(kwargs, "target_loss_pen_coeff")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "targets_hit", log->on_targets);
+}
diff --git a/ocean/boxoban/boxoban.c b/ocean/boxoban/boxoban.c
new file mode 100644
index 0000000000..c52d8054ec
--- /dev/null
+++ b/ocean/boxoban/boxoban.c
@@ -0,0 +1,195 @@
+/* Pure C demo file for Boxoban. Usage:
+ * bash scripts/build_ocean.sh boxoban
+ * ./boxoban [difficulty|path_to_bin]
+ *
+ * If you pass one of the known difficulty names (basic, easy, medium,
+ * hard, unfiltered) the demo looks for pufferlib/ocean/boxoban/boxoban_maps_.bin
+ * Otherwise the argument is treated as an explicit path to a bin file.
+ */
+
+#define BOXOBAN_MAPS_IMPLEMENTATION
+#include
+#include "boxoban.h"
+
+static int is_named_difficulty(const char* arg) {
+ return strcmp(arg, "basic") == 0 ||
+ strcmp(arg, "easy") == 0 ||
+ strcmp(arg, "medium") == 0 ||
+ strcmp(arg, "hard") == 0 ||
+ strcmp(arg, "unfiltered") == 0;
+}
+
+static const char* resolve_map_path(int argc, char** argv, char* buffer, size_t buf_sz) {
+ const char* arg = argc > 1 ? argv[1] : NULL;
+ if (arg == NULL) {
+ if (boxoban_prepare_maps_for_difficulty("easy", buffer, buf_sz) != 0) {
+ return NULL;
+ }
+ return buffer;
+ }
+ if (strchr(arg, '/')) {
+ return arg;
+ }
+ if (is_named_difficulty(arg)) {
+ if (boxoban_prepare_maps_for_difficulty(arg, buffer, buf_sz) != 0) {
+ return NULL;
+ }
+ return buffer;
+ }
+ snprintf(buffer, buf_sz, "pufferlib/ocean/boxoban/boxoban_maps_%s.bin", arg);
+ return buffer;
+}
+
+
+int demo(int argc, char** argv) {
+ char path_buffer[512];
+ const char* chosen_path = resolve_map_path(argc, argv, path_buffer, sizeof(path_buffer));
+ if (chosen_path == NULL) {
+ fprintf(stderr, "Failed to prepare map path\n");
+ return 1;
+ }
+ if (boxoban_set_map_path(chosen_path) != 0) {
+ fprintf(stderr, "Failed to set map path: %s\n", chosen_path);
+ return 1;
+ }
+
+ Boxoban env = {
+ .size = 10,
+ .observations = NULL,
+ .actions = NULL,
+ .rewards = NULL,
+ .terminals = NULL,
+ .max_steps = 500,
+ .int_r_coeff = 0.1f,
+ .target_loss_pen_coeff = 0.5f,
+ .tick = 0,
+ .agent_x = 0,
+ .agent_y = 0,
+ .intermediate_rewards = NULL,
+ .on_target = 0,
+ .n_boxes = 0,
+ .win = 0,
+ .difficulty_id = -1,
+ .client = NULL,
+ .n_targets = 0,
+
+ };
+
+ size_t obs_count = 4u * (size_t)env.size * (size_t)env.size;
+ env.observations = calloc(obs_count, sizeof(unsigned char));
+ env.actions = calloc(1, sizeof(int));
+ env.rewards = calloc(1, sizeof(float));
+ env.terminals = calloc(1, sizeof(unsigned char));
+
+ init(&env);
+ c_reset(&env);
+ c_render(&env);
+ while (!WindowShouldClose()) {
+ if (IsKeyPressed(KEY_LEFT_SHIFT) || IsKeyPressed(KEY_RIGHT_SHIFT)) {
+ TraceLog(LOG_INFO, "Shift key pressed");
+ }
+ bool manual = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
+ bool stepped = false;
+ if (manual) {
+ int new_action = -1;
+ if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) new_action = UP;
+ if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) new_action = DOWN;
+ if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) new_action = LEFT;
+ if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) new_action = RIGHT;
+
+ if (new_action >= 0) {
+ env.actions[0] = new_action;
+ c_step(&env);
+ stepped = true;
+ }
+ } else {
+ env.actions[0] = rand() % 5;
+ c_step(&env);
+ stepped = true;
+ }
+
+ if (!stepped) {
+ // Manual mode with no direction: stay paused
+ }
+ c_render(&env);
+ }
+ free(env.observations);
+ free(env.actions);
+ free(env.rewards);
+ free(env.terminals);
+ c_close(&env);
+ return 0;
+}
+
+void test_performance(int argc, char** argv, int timeout) {
+ char path_buffer[512];
+ const char* chosen_path = resolve_map_path(argc, argv, path_buffer, sizeof(path_buffer));
+ if (chosen_path == NULL) {
+ fprintf(stderr, "Failed to prepare map path\n");
+ return;
+ }
+ if (boxoban_set_map_path(chosen_path) != 0) {
+ fprintf(stderr, "Failed to set map path: %s\n", chosen_path);
+ return;
+ }
+ printf("Loaded map: %s\n", chosen_path);
+
+ Boxoban env = {
+ .size = 10,
+ .observations = NULL,
+ .actions = NULL,
+ .rewards = NULL,
+ .terminals = NULL,
+ .max_steps = 500,
+ .int_r_coeff = 0.1f,
+ .target_loss_pen_coeff = 0.5f,
+ .tick = 0,
+ .agent_x = 0,
+ .agent_y = 0,
+ .intermediate_rewards = NULL,
+ .on_target = 0,
+ .n_boxes = 0,
+ .win = 0,
+ .difficulty_id = -1,
+ .client = NULL,
+ .n_targets = 0,
+ };
+
+ size_t obs_count = 4u * (size_t)env.size * (size_t)env.size;
+ env.observations = calloc(obs_count, sizeof(unsigned char));
+ env.actions = calloc(1, sizeof(int));
+ env.rewards = calloc(1, sizeof(float));
+ env.terminals = calloc(1, sizeof(unsigned char));
+
+ printf("Initializing...\n");
+ init(&env);
+ printf("Resetting...\n");
+ c_reset(&env);
+ printf("Starting test...\n");
+
+ int start = time(NULL);
+ int num_steps = 0;
+ while (time(NULL) - start < timeout) {
+ env.actions[0] = rand() % 5;
+ c_step(&env);
+ num_steps++;
+ }
+
+ int end = time(NULL);
+ float sps = num_steps / (end - start);
+ printf("Test Environment SPS: %f\n", sps);
+ free(env.observations);
+ free(env.actions);
+ free(env.rewards);
+ free(env.terminals);
+ c_close(&env);
+}
+
+int main(int argc, char** argv) {
+ demo(argc, argv);
+ setbuf(stdout, NULL);
+ fprintf(stderr, "Entered main\n");
+ fflush(stderr);
+ //test_performance(argc, argv,10);
+ return 0;
+}
diff --git a/ocean/boxoban/boxoban.h b/ocean/boxoban/boxoban.h
new file mode 100644
index 0000000000..35b61a2529
--- /dev/null
+++ b/ocean/boxoban/boxoban.h
@@ -0,0 +1,409 @@
+#include
+#include
+#include
+#include
+#include "raylib.h"
+#include "boxoban_maps.h"
+
+const unsigned char NOOP = 0;
+const unsigned char DOWN = 1;
+const unsigned char UP = 2;
+const unsigned char LEFT = 3;
+const unsigned char RIGHT = 4;
+
+const unsigned char AGENT = 0;
+const unsigned char WALLS = 1;
+const unsigned char BOXES = 2;
+const unsigned char TARGET = 3;
+
+// Required struct. Only use floats!
+typedef struct {
+ float perf; // Recommended 0-1 normalized single real number perf metric
+ float score; // Recommended unnormalized single real number perf metric
+ float episode_return; // Recommended metric: sum of agent rewards over episode
+ float episode_length; // Recommended metric: number of steps of agent episode
+ // Any extra fields you add here may be exported to Python in binding.c
+ float on_targets; // Number of targets currently boxed
+ float n; // Required as the last field
+} Log;
+
+typedef struct {
+ Texture2D wall;
+ Texture2D box;
+ Texture2D target;
+ Texture2D floor;
+ Texture2D agent;
+ Texture2D box_on_target;
+} Client;
+
+// Required that you have some struct for your env
+// Recommended that you name it the same as the env file
+typedef struct {
+ Log log; // Required field. Env binding code uses this to aggregate logs
+ unsigned char* observations; // Required. You can use any obs type, but make sure it matches in Python!
+ float* actions; // Required. int* for discrete/multidiscrete, float* for box
+ float* rewards; // Required
+ float* terminals; // Required. We don't yet have truncations as standard yet
+ unsigned int rng;
+ int size;
+ int num_agents;
+ int tick;
+ int max_steps;
+ int agent_x;
+ int agent_y;
+ bool initialized;
+ unsigned char* intermediate_rewards;
+ float int_r_coeff;
+ float target_loss_pen_coeff;
+ int on_target; //num targets currently boxed
+ int n_boxes; //boxes in map
+ int n_targets; //targets in map
+ int difficulty_id; // 0=basic,1=easy,2=medium,3=hard,4=unfiltered
+ Client* client;
+ int win;
+ float episode_return;
+} Boxoban;
+
+void ensure_map_loaded(void);
+
+static int boxoban_configure_maps_from_env(Boxoban* env) {
+ if (env->difficulty_id == -1) {
+ return 0;
+ }
+
+ if (env->difficulty_id < -1) {
+ fprintf(stderr, "Invalid Boxoban difficulty id %d\n", env->difficulty_id);
+ return -1;
+ }
+
+ const char* difficulty_name = boxoban_difficulty_name_from_id(env->difficulty_id);
+ if (difficulty_name == NULL) {
+ fprintf(stderr, "Invalid Boxoban difficulty id %d\n", env->difficulty_id);
+ return -1;
+ }
+ char prepared_path[512];
+ if (boxoban_prepare_maps_for_difficulty(difficulty_name, prepared_path, sizeof(prepared_path)) != 0) {
+ return -1;
+ }
+
+ return 0;
+}
+
+//Entity,x,y convention y moves top to bottom
+
+static inline void set_entity(Boxoban *env, int entity, int x, int y, unsigned char value) {
+ env->observations[(entity)*env->size*env->size + (y)*env->size + (x)] = value;
+}
+
+static inline unsigned char get_entity(Boxoban *env, int entity, int x, int y) {
+ return env->observations[(entity)*env->size*env->size + (y)*env->size + (x)];
+}
+
+static inline void set_intermediate_reward(Boxoban *env, int x, int y, unsigned char value) {
+ env->intermediate_rewards[(y)*env->size + (x)] = value;
+}
+
+static inline unsigned char get_intermediate_reward_status(Boxoban *env, int x, int y) {
+ return env->intermediate_rewards[(y)*env->size + (x)];
+}
+
+static inline const uint32_t get_random_puzzle_idx(const Boxoban *env) {
+ int idx = rand_r(&env->rng) % PUZZLE_COUNT;
+ return idx;
+}
+
+
+void init (Boxoban* env) {
+ static int boxoban_maps_ready = 0;
+ if (!boxoban_maps_ready) {
+ if (boxoban_configure_maps_from_env(env) != 0) {
+ fprintf(stderr, "Failed to configure Boxoban maps\n");
+ abort();
+ }
+ ensure_map_loaded();
+ boxoban_maps_ready = 1;
+ }
+ env->intermediate_rewards = calloc(env->size*env->size, sizeof(unsigned char));
+ env->win = 0;
+ env->initialized = false;
+ }
+
+
+void add_log(Boxoban* env) {
+ float denom = (float)env->n_boxes;
+ float num = (float)env->on_target;
+ float perf = (env->win== 1) ? 1.0f : 0.0f;
+ env->log.perf += perf;
+ env->log.score += perf;
+ env->log.episode_length += env->tick;
+ env->log.episode_return += env->episode_return;
+ env->log.on_targets += env->on_target;
+ env->log.n++;
+}
+
+
+bool clear(Boxoban* env, int x, int y) {
+ if (x < 0 || y < 0 || x >= env->size || y >= env->size) {
+ return false;
+ }
+ return (get_entity(env, WALLS, x, y) == 0) && (get_entity(env, BOXES, x, y) == 0);
+}
+
+// Required function
+void c_reset(Boxoban* env) {
+ const uint32_t i = get_random_puzzle_idx(env);
+ const uint8_t* puzzle = MAP_BASE + (size_t)i * PUZZLE_SIZE;
+ memcpy(env->observations, puzzle, PUZZLE_OBS_BYTES);
+
+ const uint8_t* meta = puzzle + PUZZLE_OBS_BYTES;
+ env->agent_x = (int)meta[0];
+ env->agent_y = (int)meta[1];
+ env->n_boxes = (int)meta[2];
+ env->n_targets = (int)meta[3];
+ env->on_target = (int)meta[4];
+
+ memcpy(env->intermediate_rewards,
+ env->observations + TARGET * env->size * env->size,env->size * env->size);
+
+ env->tick = 0;
+ env->win = 0;
+ env->episode_return = 0;
+
+ if (!env->initialized) {
+ env->tick = rand_r(&env->rng) % env->max_steps;
+ env->initialized = true;
+ }
+}
+
+//Updates OBS for moved entity
+void move_entity(Boxoban* env,unsigned char entity,int x, int y, int dx, int dy) {
+ set_entity(env, entity, x, y, 0);
+ set_entity(env, entity, x + dx, y + dy, 1);
+}
+
+//Updates state and intermediate reward array in place
+int take_action(Boxoban* env, int action) {
+
+ int dx = 0;
+ int dy = 0;
+ int int_r = 0;
+
+ if (action == NOOP) {
+ return 0;
+ }
+ else if (action == DOWN) {
+ dy = 1;
+ }
+ else if (action == UP) {
+ dy = -1;
+ }
+ else if (action == LEFT) {
+ dx = -1;
+ }
+ else if (action == RIGHT) {
+ dx = 1;
+ }
+
+ //if move space is clear, move agent
+ if (clear(env, env->agent_x + dx, env->agent_y + dy)) {
+
+ move_entity(env, AGENT, env->agent_x, env->agent_y, dx, dy);
+ env->agent_y += dy;
+ env->agent_x += dx;
+ return 0;
+ }
+ //if its not clear, but its a box and box is clear to move, move both
+ else if (clear(env, env->agent_x+ 2*dx, env->agent_y + 2*dy)
+ && get_entity(env, BOXES, env->agent_x + dx, env->agent_y + dy) == 1) {
+
+ //if box is on target currently, remove from on_target count
+ if (get_entity(env, TARGET, env->agent_x + dx, env->agent_y + dy) == 1) {
+
+ env->on_target -= 1;
+ }
+ //move both entities
+ move_entity(env, BOXES, env->agent_x + dx, env->agent_y + dy, dx, dy);
+ move_entity(env, AGENT, env->agent_x, env->agent_y, dx, dy);
+ env->agent_y += dy;
+ env->agent_x += dx;
+
+ //if box is now on target, add to on_target count
+ //if its a new target recieve intermediate reward and zero out intermediate reward
+ if (get_entity(env, TARGET, env->agent_x + dx, env->agent_y + dy) == 1) {
+
+ env->on_target += 1;
+ int_r = get_intermediate_reward_status(env, env->agent_x + dx, env->agent_y + dy);
+ set_intermediate_reward(env, env->agent_x + dx, env->agent_y + dy, 0);
+ }
+ return int_r;
+ }
+ return 0;
+}
+
+// Required function
+void c_step(Boxoban* env) {
+ env->tick += 1;
+ env->terminals[0] = 0;
+ env->rewards[0] = 0.0;
+
+ int action = (int)env->actions[0];
+
+ float on_target = env->on_target;
+ int int_r = take_action(env, action); //int_r _new_ tgts covered, modifies observations in place
+ float on_target_after = env->on_target;
+
+ env->rewards[0] += (float)int_r * env->int_r_coeff; //coeff in .ini
+
+ if (on_target_after < on_target) { //target loss penalty
+ env->rewards[0] -= env->target_loss_pen_coeff; //coeff in .ini
+ }
+
+ //Terminals
+ if (env->on_target == env->n_targets) {
+ env->terminals[0] = 1;
+ env->rewards[0] += 1.0;
+ env->win = 1;
+ env->episode_return += env->rewards[0];
+ add_log(env);
+ c_reset(env);
+ return;
+ }
+
+ if (env->tick >= env->max_steps) {
+ env->terminals[0] = 1;
+ env->rewards[0] -= 1.0;
+ env->episode_return += env->rewards[0];
+ add_log(env);
+ c_reset(env);
+ return;
+ }
+ env->episode_return += env->rewards[0];
+
+}
+
+Client* c_create(Boxoban* env) {
+ Client* client = calloc(1,sizeof(Client));
+ client->wall = LoadTexture("resources/boxoban/Wall_Black.jpg");
+ client->box = LoadTexture("resources/boxoban/Crate_Black.jpg");
+ client->target = LoadTexture("resources/boxoban/EndPoint_Black.jpg");
+ client->floor = LoadTexture("resources/boxoban/GroundGravel_Concrete.jpg");
+ client->box_on_target = LoadTexture("resources/boxoban/EndPoint_Blue.jpg");
+ client->agent = LoadTexture("resources/shared/puffers_128.png");
+ env-> client = client;
+ return client;
+}
+
+#define TILE 32
+
+Texture2D choose_sprite(Client *c, Boxoban *env, int x, int y) {
+ int a = get_entity(env, AGENT, x, y);
+ int w = get_entity(env, WALLS, x, y);
+ int b = get_entity(env, BOXES, x, y);
+ int t = get_entity(env, TARGET, x, y);
+
+ if (w) return c->wall;
+ if (b && t) return c->box_on_target;
+ if (b) return c->box;
+ if (a) return c->agent;
+ if (t) return c->target;
+
+ return c->floor;
+}
+
+void draw_tile(Boxoban *env, int x, int y) {
+ Client *c = env->client;
+ Rectangle dest = {x * TILE, y * TILE, TILE, TILE};
+
+ // Always lay down the base tile
+ DrawTexturePro(
+ c->floor,
+ (Rectangle){0, 0, (float)c->floor.width, (float)c->floor.height},
+ dest,
+ (Vector2){0, 0},
+ 0.0f,
+ WHITE);
+
+ if (get_entity(env, TARGET, x, y)) {
+ DrawTexturePro(
+ c->target,
+ (Rectangle){0, 0, (float)c->target.width, (float)c->target.height},
+ dest,
+ (Vector2){0, 0},
+ 0.0f,
+ WHITE);
+ }
+ if (get_entity(env, BOXES, x, y)) {
+ Texture2D tex = get_entity(env, TARGET, x, y) ? c->box_on_target : c->box;
+ DrawTexturePro(
+ tex,
+ (Rectangle){0, 0, (float)tex.width, (float)tex.height},
+ dest,
+ (Vector2){0, 0},
+ 0.0f,
+ WHITE);
+ }
+ if (get_entity(env, WALLS, x, y)) {
+ DrawTexturePro(
+ c->wall,
+ (Rectangle){0, 0, (float)c->wall.width, (float)c->wall.height},
+ dest,
+ (Vector2){0, 0},
+ 0.0f,
+ WHITE);
+ }
+ if (get_entity(env, AGENT, x, y)) {
+ Rectangle src = {0, 0, c->agent.width / 2.0f, (float)c->agent.height};
+ DrawTexturePro(c->agent, src, dest, (Vector2){0, 0}, 0.0f, WHITE);
+ }
+ }
+
+
+// Required function. Should handle creating the client on first call
+void c_render(Boxoban* env) {
+ if (!IsWindowReady()) {
+ InitWindow(TILE*env->size, TILE*env->size, "PufferLib Boxoban");
+ SetTargetFPS(10);
+ }
+
+ // Standard across our envs so exiting is always the same
+ if (IsKeyDown(KEY_ESCAPE)) {
+ exit(0);
+ }
+
+ if (env->client == NULL) {
+ env->client = c_create(env);
+ }
+
+ BeginDrawing();
+ ClearBackground((Color){6, 24, 24, 255});
+
+ for (int y = 0; y < env->size; y++) {
+ for (int x = 0; x < env->size; x++) {
+ draw_tile(env, x, y);
+ }
+ }
+
+
+ EndDrawing();
+}
+
+// Required function. Should clean up anything you allocated
+// Do not free env->observations, actions, rewards, terminals
+void c_close(Boxoban* env) {
+ if (env->intermediate_rewards) {
+ free(env->intermediate_rewards);
+ env->intermediate_rewards = NULL;
+ }
+ if (IsWindowReady()) {
+ if (env->client) {
+ UnloadTexture(env->client->wall);
+ UnloadTexture(env->client->box);
+ UnloadTexture(env->client->target);
+ UnloadTexture(env->client->floor);
+ UnloadTexture(env->client->agent);
+ free(env->client);
+ env->client = NULL;
+ }
+ CloseWindow();
+ }
+}
diff --git a/ocean/boxoban/boxoban_maps.h b/ocean/boxoban/boxoban_maps.h
new file mode 100644
index 0000000000..c633c37307
--- /dev/null
+++ b/ocean/boxoban/boxoban_maps.h
@@ -0,0 +1,453 @@
+#ifndef PUFFERLIB_OCEAN_BOXOBAN_MAPS_H
+#define PUFFERLIB_OCEAN_BOXOBAN_MAPS_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "generate_maps.h"
+#include "parse_maps.h"
+
+/*
+Maps are stored in binary files keyed by difficulty.
+If the bin does not exist it is created on the fly, then mmapped and shared by envs.
+*/
+
+extern uint8_t *MAP_BASE;
+extern size_t MAP_FILESIZE;
+extern size_t PUZZLE_COUNT;
+extern size_t PUZZLE_SIZE;
+extern size_t PUZZLE_OBS_BYTES;
+
+int boxoban_prepare_maps_for_difficulty(const char* difficulty, char* out_path, size_t out_cap);
+int boxoban_set_map_path(const char *path);
+int boxoban_difficulty_id_from_name(const char* difficulty_name);
+const char* boxoban_difficulty_name_from_id(int difficulty_id);
+void ensure_map_loaded(void);
+
+#ifdef BOXOBAN_MAPS_IMPLEMENTATION
+
+uint8_t *MAP_BASE = NULL;
+size_t MAP_FILESIZE = 0;
+size_t PUZZLE_COUNT = 0;
+size_t PUZZLE_SIZE = BOXOBAN_PUZZLE_BYTES;
+size_t PUZZLE_OBS_BYTES = BOXOBAN_PUZZLE_OBS_BYTES;
+static char* BOXOBAN_MAP_PATH = NULL;
+static const char* BOXOBAN_LEVEL_ROOT = "resources/boxoban/levels";
+
+typedef struct {
+ char** items;
+ size_t count;
+ size_t cap;
+} BoxobanPathList;
+
+static int boxoban_cmp_strings(const void* a, const void* b) {
+ const char* const* sa = (const char* const*)a;
+ const char* const* sb = (const char* const*)b;
+ return strcmp(*sa, *sb);
+}
+
+static void boxoban_path_list_free(BoxobanPathList* list) {
+ for (size_t i = 0; i < list->count; i++) {
+ free(list->items[i]);
+ }
+ free(list->items);
+ list->items = NULL;
+ list->count = 0;
+ list->cap = 0;
+}
+
+static int boxoban_path_list_append(BoxobanPathList* list, const char* path) {
+ if (list->count == list->cap) {
+ size_t next_cap = list->cap == 0 ? 64 : list->cap * 2;
+ char** next = (char**)realloc(list->items, next_cap * sizeof(char*));
+ if (next == NULL) {
+ return -1;
+ }
+ list->items = next;
+ list->cap = next_cap;
+ }
+ char* copied = (char*)malloc(strlen(path) + 1);
+ if (copied == NULL) {
+ return -1;
+ }
+ strcpy(copied, path);
+ list->items[list->count++] = copied;
+ return 0;
+}
+
+static int boxoban_has_txt_suffix(const char* name) {
+ size_t len = strlen(name);
+ return len >= 4 && strcmp(name + len - 4, ".txt") == 0;
+}
+
+int boxoban_difficulty_id_from_name(const char* difficulty_name) {
+ if (difficulty_name == NULL) {
+ return -1;
+ }
+
+ if (strcmp(difficulty_name, "basic") == 0) {
+ return 0;
+ }
+ if (strcmp(difficulty_name, "easy") == 0) {
+ return 1;
+ }
+ if (strcmp(difficulty_name, "medium") == 0) {
+ return 2;
+ }
+ if (strcmp(difficulty_name, "hard") == 0) {
+ return 3;
+ }
+ if (strcmp(difficulty_name, "unfiltered") == 0) {
+ return 4;
+ }
+
+ return -1;
+}
+
+const char* boxoban_difficulty_name_from_id(int difficulty_id) {
+ switch (difficulty_id) {
+ case 0:
+ return "basic";
+ case 1:
+ return "easy";
+ case 2:
+ return "medium";
+ case 3:
+ return "hard";
+ case 4:
+ return "unfiltered";
+ default:
+ return NULL;
+ }
+}
+
+static int boxoban_dir_has_txt(const char* dir_path) {
+ DIR* dir = opendir(dir_path);
+ if (dir == NULL) {
+ return 0;
+ }
+ struct dirent* ent;
+ while ((ent = readdir(dir)) != NULL) {
+ if (boxoban_has_txt_suffix(ent->d_name)) {
+ closedir(dir);
+ return 1;
+ }
+ }
+ closedir(dir);
+ return 0;
+}
+
+static int boxoban_collect_sorted_txt_paths_in_dir(const char* dir_path, BoxobanPathList* out_paths) {
+ DIR* dir = opendir(dir_path);
+ if (dir == NULL) {
+ fprintf(stderr, "Missing level directory %s\n", dir_path);
+ return -1;
+ }
+
+ BoxobanPathList names = {0};
+ struct dirent* ent;
+ while ((ent = readdir(dir)) != NULL) {
+ if (!boxoban_has_txt_suffix(ent->d_name)) {
+ continue;
+ }
+ if (boxoban_path_list_append(&names, ent->d_name) != 0) {
+ boxoban_path_list_free(&names);
+ closedir(dir);
+ return -1;
+ }
+ }
+ closedir(dir);
+
+ qsort(names.items, names.count, sizeof(char*), boxoban_cmp_strings);
+ for (size_t i = 0; i < names.count; i++) {
+ char full_path[1400];
+ snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, names.items[i]);
+ if (boxoban_path_list_append(out_paths, full_path) != 0) {
+ boxoban_path_list_free(&names);
+ return -1;
+ }
+ }
+ boxoban_path_list_free(&names);
+ return 0;
+}
+
+static int boxoban_collect_maps_from_dir(const char* rel_path, BoxobanPathList* out_paths) {
+ char level_dir[1400];
+ struct stat st;
+
+ snprintf(level_dir, sizeof(level_dir), "%s/%s", BOXOBAN_LEVEL_ROOT, rel_path);
+ if (stat(level_dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
+ fprintf(stderr, "Missing level directory %s\n", level_dir);
+ return -1;
+ }
+
+ return boxoban_collect_sorted_txt_paths_in_dir(level_dir, out_paths);
+}
+
+static int boxoban_collect_maps(const char* difficulty, BoxobanPathList* out_paths) {
+ if (strcmp(difficulty, "basic") == 0) {
+ return boxoban_collect_maps_from_dir("basic/train", out_paths);
+ }
+ if (strcmp(difficulty, "easy") == 0) {
+ return boxoban_collect_maps_from_dir("easy/train", out_paths);
+ }
+ if (strcmp(difficulty, "medium") == 0) {
+ return boxoban_collect_maps_from_dir("medium/train", out_paths);
+ }
+ if (strcmp(difficulty, "hard") == 0) {
+ return boxoban_collect_maps_from_dir("hard", out_paths);
+ }
+ if (strcmp(difficulty, "unfiltered") == 0) {
+ return boxoban_collect_maps_from_dir("unfiltered/train", out_paths);
+ }
+
+ fprintf(stderr, "Invalid difficulty '%s'\n", difficulty);
+ return -1;
+}
+
+static int boxoban_download_text_maps(const char* difficulty) {
+ char zip_url[512];
+ snprintf(zip_url, sizeof(zip_url),
+ "https://raw.githubusercontent.com/TBBristol/pufferlib_boxoban_levels/main/%s.zip",
+ difficulty);
+ fprintf(stdout, "[Boxoban] Downloading %s maps from %s\n", difficulty, zip_url);
+
+ char tmp_template[] = "/tmp/boxoban_maps_XXXXXX";
+ char* tmp_dir = mkdtemp(tmp_template);
+ if (tmp_dir == NULL) {
+ return -1;
+ }
+
+ char zip_path[1400];
+ snprintf(zip_path, sizeof(zip_path), "%s/%s.zip", tmp_dir, difficulty);
+
+ char cmd[4096];
+ snprintf(cmd, sizeof(cmd), "curl -L --fail -o '%s' '%s' > /dev/null 2>&1", zip_path, zip_url);
+ if (system(cmd) != 0) {
+ fprintf(stderr, "Failed to download Boxoban maps with curl\n");
+ return -1;
+ }
+
+ snprintf(cmd, sizeof(cmd), "unzip -q '%s' -d '%s'", zip_path, tmp_dir);
+ if (system(cmd) != 0) {
+ fprintf(stderr, "Failed to unzip Boxoban maps archive\n");
+ return -1;
+ }
+
+ char extracted_root[1400] = {0};
+ char find_cmd[4096];
+ snprintf(find_cmd, sizeof(find_cmd), "find '%s' -type d -name '%s' | head -n 1", tmp_dir, difficulty);
+ FILE* find_pipe = popen(find_cmd, "r");
+ if (find_pipe == NULL) {
+ return -1;
+ }
+ if (fgets(extracted_root, sizeof(extracted_root), find_pipe) == NULL) {
+ pclose(find_pipe);
+ fprintf(stderr, "Downloaded zip missing '%s' directory\n", difficulty);
+ return -1;
+ }
+ pclose(find_pipe);
+ extracted_root[strcspn(extracted_root, "\r\n")] = '\0';
+
+ char dest_root[1400];
+ snprintf(dest_root, sizeof(dest_root), "%s/%s", BOXOBAN_LEVEL_ROOT, difficulty);
+ if (boxoban_mkdir_p(dest_root) != 0) {
+ return -1;
+ }
+
+ snprintf(cmd, sizeof(cmd), "cp -R '%s/.' '%s/'", extracted_root, dest_root);
+ if (system(cmd) != 0) {
+ fprintf(stderr, "Failed to copy downloaded maps into %s\n", dest_root);
+ return -1;
+ }
+ return 0;
+}
+
+static int boxoban_ensure_text_maps(const char* difficulty) {
+ if (strcmp(difficulty, "basic") == 0) {
+ char output_dir[1400];
+ snprintf(output_dir, sizeof(output_dir), "%s/basic/train", BOXOBAN_LEVEL_ROOT);
+ if (boxoban_dir_has_txt(output_dir)) {
+ return 0;
+ }
+ fprintf(stdout, "[Boxoban] Generating basic maps at %s\n", output_dir);
+ return boxoban_generate_basic_maps(output_dir, 0);
+ }
+ if (strcmp(difficulty, "easy") == 0) {
+ char output_dir[1400];
+ snprintf(output_dir, sizeof(output_dir), "%s/easy/train", BOXOBAN_LEVEL_ROOT);
+ if (boxoban_dir_has_txt(output_dir)) {
+ return 0;
+ }
+ fprintf(stdout, "[Boxoban] Generating easy maps at %s\n", output_dir);
+ return boxoban_generate_easy_maps(output_dir, 0);
+ }
+ if (strcmp(difficulty, "medium") == 0) {
+ char level_dir[1400];
+ snprintf(level_dir, sizeof(level_dir), "%s/medium/train", BOXOBAN_LEVEL_ROOT);
+ if (boxoban_dir_has_txt(level_dir)) {
+ return 0;
+ }
+ return boxoban_download_text_maps(difficulty);
+ }
+ if (strcmp(difficulty, "hard") == 0) {
+ char level_dir[1400];
+ snprintf(level_dir, sizeof(level_dir), "%s/hard", BOXOBAN_LEVEL_ROOT);
+ if (boxoban_dir_has_txt(level_dir)) {
+ return 0;
+ }
+ return boxoban_download_text_maps(difficulty);
+ }
+ if (strcmp(difficulty, "unfiltered") == 0) {
+ char level_dir[1400];
+ snprintf(level_dir, sizeof(level_dir), "%s/unfiltered/train", BOXOBAN_LEVEL_ROOT);
+ if (boxoban_dir_has_txt(level_dir)) {
+ return 0;
+ }
+ return boxoban_download_text_maps(difficulty);
+ }
+
+ return boxoban_download_text_maps(difficulty);
+}
+
+static int boxoban_bin_path(const char* difficulty, char* out_path, size_t out_cap) {
+ int written = snprintf(out_path, out_cap, "resources/boxoban/boxoban_maps_%s.bin", difficulty);
+ if (written <= 0 || (size_t)written >= out_cap) {
+ return -1;
+ }
+ return 0;
+}
+
+int boxoban_prepare_maps_for_difficulty(const char* difficulty, char* out_path, size_t out_cap) {
+ if (difficulty == NULL || out_path == NULL) {
+ return -1;
+ }
+ if (boxoban_difficulty_id_from_name(difficulty) < 0) {
+ return -1;
+ }
+ if (boxoban_bin_path(difficulty, out_path, out_cap) != 0) {
+ return -1;
+ }
+
+ if (access(out_path, F_OK) != 0) {
+ if (boxoban_ensure_text_maps(difficulty) != 0) {
+ return -1;
+ }
+
+ BoxobanPathList maps = {0};
+ size_t puzzle_count = 0;
+ if (boxoban_collect_maps(difficulty, &maps) != 0) {
+ boxoban_path_list_free(&maps);
+ return -1;
+ }
+
+ if (boxoban_write_bin_from_files((const char* const*)maps.items, maps.count, out_path, 0, &puzzle_count) != 0) {
+ boxoban_path_list_free(&maps);
+ return -1;
+ }
+ boxoban_path_list_free(&maps);
+ fprintf(stdout, "[Boxoban] Generated %zu puzzles for '%s' at %s\n", puzzle_count, difficulty, out_path);
+ }
+
+ if (boxoban_set_map_path(out_path) != 0) {
+ return -1;
+ }
+ return 0;
+}
+
+static void reset_map_cache(void) {
+ if (MAP_BASE != NULL && MAP_BASE != MAP_FAILED && MAP_FILESIZE > 0) {
+ munmap(MAP_BASE, MAP_FILESIZE);
+ }
+ MAP_BASE = NULL;
+ MAP_FILESIZE = 0;
+ PUZZLE_COUNT = 0;
+}
+
+int boxoban_set_map_path(const char *path) {
+ if (path == NULL) {
+ return -1;
+ }
+ if (BOXOBAN_MAP_PATH != NULL && strcmp(BOXOBAN_MAP_PATH, path) == 0) {
+ return 0;
+ }
+
+ char* copied = malloc(strlen(path) + 1);
+ if (copied == NULL) {
+ return -1;
+ }
+ strcpy(copied, path);
+
+ reset_map_cache();
+ free(BOXOBAN_MAP_PATH);
+ BOXOBAN_MAP_PATH = copied;
+ return 0;
+}
+
+static const char* get_default_map_path(void) {
+ const char* env_path = getenv("BOXOBAN_MAP_BIN");
+ if (env_path != NULL) {
+ return env_path;
+ }
+ return NULL;
+}
+
+void ensure_map_loaded(void) {
+ if (MAP_BASE != NULL) {
+ return;
+ }
+
+ if (BOXOBAN_MAP_PATH == NULL) {
+ const char* default_path = get_default_map_path();
+ if (default_path != NULL) {
+ if (boxoban_set_map_path(default_path) != 0) {
+ fprintf(stderr, "Failed to set default Boxoban map path\n");
+ abort();
+ }
+ } else {
+ char prepared_path[512];
+ if (boxoban_prepare_maps_for_difficulty("basic", prepared_path, sizeof(prepared_path)) != 0) {
+ fprintf(stderr, "Failed to prepare default Boxoban maps\n");
+ abort();
+ }
+ }
+ }
+
+ int fd = open(BOXOBAN_MAP_PATH, O_RDONLY);
+ if (fd < 0) {
+ perror("open");
+ abort();
+ }
+ struct stat st;
+ if (fstat(fd, &st) != 0) {
+ perror("fstat");
+ abort();
+ }
+
+ MAP_FILESIZE = st.st_size;
+ if (MAP_FILESIZE % PUZZLE_SIZE != 0) {
+ fprintf(stderr, "Invalid Boxoban map file size %zu (expected multiple of %zu)\n",
+ MAP_FILESIZE, PUZZLE_SIZE);
+ abort();
+ }
+ PUZZLE_COUNT = MAP_FILESIZE / PUZZLE_SIZE;
+
+ MAP_BASE = mmap(NULL, MAP_FILESIZE, PROT_READ, MAP_PRIVATE, fd, 0);
+ close(fd);
+
+ if (MAP_BASE == MAP_FAILED) {
+ perror("mmap");
+ abort();
+ }
+}
+
+#endif
+
+#endif
diff --git a/ocean/boxoban/generate_maps.h b/ocean/boxoban/generate_maps.h
new file mode 100644
index 0000000000..cdd39a10b7
--- /dev/null
+++ b/ocean/boxoban/generate_maps.h
@@ -0,0 +1,368 @@
+#ifndef PUFFERLIB_OCEAN_BOXOBAN_GENERATE_MAPS_H
+#define PUFFERLIB_OCEAN_BOXOBAN_GENERATE_MAPS_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define BOXOBAN_GEN_AGENT '@'
+#define BOXOBAN_GEN_WALL '#'
+#define BOXOBAN_GEN_BOX '$'
+#define BOXOBAN_GEN_TARGET '.'
+#define BOXOBAN_GEN_FLOOR ' '
+
+typedef struct {
+ int r;
+ int c;
+} BoxobanCell;
+
+typedef struct {
+ uint64_t state;
+} BoxobanRandom;
+
+static int boxoban_mkdir_p(const char* dir_path) {
+ char tmp[1024];
+ size_t len = strlen(dir_path);
+ if (len >= sizeof(tmp)) {
+ return -1;
+ }
+
+ memcpy(tmp, dir_path, len + 1);
+ for (size_t i = 1; i < len; i++) {
+ if (tmp[i] == '/') {
+ tmp[i] = '\0';
+ if (mkdir(tmp, 0777) != 0 && errno != EEXIST) {
+ return -1;
+ }
+ tmp[i] = '/';
+ }
+ }
+ if (mkdir(tmp, 0777) != 0 && errno != EEXIST) {
+ return -1;
+ }
+ return 0;
+}
+
+static void boxoban_seed(BoxobanRandom* rng, uint64_t seed) {
+ rng->state = seed ? seed : 0x9e3779b97f4a7c15ULL;
+}
+
+static uint64_t boxoban_next_u64(BoxobanRandom* rng) {
+ uint64_t x = rng->state;
+ x ^= x >> 12;
+ x ^= x << 25;
+ x ^= x >> 27;
+ rng->state = x;
+ return x * 2685821657736338717ULL;
+}
+
+static uint32_t boxoban_randbelow(BoxobanRandom* rng, uint32_t n) {
+ if (n == 0) {
+ return 0;
+ }
+
+ uint64_t threshold = (uint64_t)(-(int64_t)n) % (uint64_t)n;
+ for (;;) {
+ uint64_t r = boxoban_next_u64(rng);
+ if (r >= threshold) {
+ return (uint32_t)(r % n);
+ }
+ }
+}
+
+static int boxoban_randint(BoxobanRandom* rng, int a, int b) {
+ return a + (int)boxoban_randbelow(rng, (uint32_t)(b - a + 1));
+}
+
+static int boxoban_choice_index(BoxobanRandom* rng, int n) {
+ return (int)boxoban_randbelow(rng, (uint32_t)n);
+}
+
+static int boxoban_sample_indices(BoxobanRandom* rng, int n, int k, int* out_indices) {
+ int* pool = (int*)malloc((size_t)n * sizeof(int));
+ if (pool == NULL) {
+ return -1;
+ }
+
+ for (int i = 0; i < n; i++) {
+ pool[i] = i;
+ }
+
+ for (int i = 0; i < k; i++) {
+ int j = i + (int)boxoban_randbelow(rng, (uint32_t)(n - i));
+ int tmp = pool[i];
+ pool[i] = pool[j];
+ pool[j] = tmp;
+ out_indices[i] = pool[i];
+ }
+
+ free(pool);
+ return 0;
+}
+
+static inline int boxoban_grid_idx(int size, int r, int c) {
+ return r * size + c;
+}
+
+static int boxoban_is_inside(int size, int x, int y) {
+ return x >= 0 && x < size && y >= 0 && y < size;
+}
+
+static int boxoban_is_pushable(const char* grid, int size, int x, int y) {
+ static const int dirs[4][2] = {{1,0}, {-1,0}, {0,1}, {0,-1}};
+ for (int d = 0; d < 4; d++) {
+ int dx = dirs[d][0];
+ int dy = dirs[d][1];
+ int px = x - dx;
+ int py = y - dy;
+ int tx = x + dx;
+ int ty = y + dy;
+ if (!boxoban_is_inside(size, px, py) || !boxoban_is_inside(size, tx, ty)) {
+ continue;
+ }
+ char pre = grid[boxoban_grid_idx(size, py, px)];
+ char post = grid[boxoban_grid_idx(size, ty, tx)];
+ if ((pre == BOXOBAN_GEN_FLOOR || pre == BOXOBAN_GEN_TARGET) &&
+ (post == BOXOBAN_GEN_FLOOR || post == BOXOBAN_GEN_TARGET)) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static void boxoban_build_border_grid(char* grid, int size) {
+ for (int r = 0; r < size; r++) {
+ for (int c = 0; c < size; c++) {
+ grid[boxoban_grid_idx(size, r, c)] = BOXOBAN_GEN_FLOOR;
+ }
+ }
+ for (int i = 0; i < size; i++) {
+ grid[boxoban_grid_idx(size, 0, i)] = BOXOBAN_GEN_WALL;
+ grid[boxoban_grid_idx(size, size - 1, i)] = BOXOBAN_GEN_WALL;
+ grid[boxoban_grid_idx(size, i, 0)] = BOXOBAN_GEN_WALL;
+ grid[boxoban_grid_idx(size, i, size - 1)] = BOXOBAN_GEN_WALL;
+ }
+}
+
+static int boxoban_build_cells(int size, int margin, BoxobanCell* out_cells) {
+ int count = 0;
+ int start = 1 + margin;
+ int end = size - 1 - margin;
+ for (int r = start; r < end; r++) {
+ for (int c = start; c < end; c++) {
+ out_cells[count].r = r;
+ out_cells[count].c = c;
+ count++;
+ }
+ }
+ return count;
+}
+
+static int boxoban_make_puzzle(
+ int size,
+ BoxobanRandom* rng,
+ int num_boxes,
+ int max_attempts,
+ const BoxobanCell* agent_choices,
+ int agent_count,
+ const BoxobanCell* confined,
+ int confined_count,
+ int interior_count,
+ char* grid
+) {
+ if (num_boxes < 1) {
+ fprintf(stderr, "num_boxes must be at least 1\n");
+ return -1;
+ }
+
+ int needed = num_boxes * 2 + 1;
+ if (needed > interior_count) {
+ fprintf(stderr, "Grid interior only has %d cells, cannot place %d objects\n", interior_count, needed);
+ return -1;
+ }
+
+ BoxobanCell* box_candidates = (BoxobanCell*)malloc((size_t)confined_count * sizeof(BoxobanCell));
+ BoxobanCell* box_positions = (BoxobanCell*)malloc((size_t)num_boxes * sizeof(BoxobanCell));
+ BoxobanCell* agent_candidates = (BoxobanCell*)malloc((size_t)agent_count * sizeof(BoxobanCell));
+ int* sampled_idx = (int*)malloc((size_t)num_boxes * sizeof(int));
+ uint8_t* occupied = (uint8_t*)calloc((size_t)size * (size_t)size, sizeof(uint8_t));
+ if (box_candidates == NULL || box_positions == NULL || agent_candidates == NULL || sampled_idx == NULL || occupied == NULL) {
+ free(box_candidates);
+ free(box_positions);
+ free(agent_candidates);
+ free(sampled_idx);
+ free(occupied);
+ return -1;
+ }
+
+ for (int attempt = 0; attempt < max_attempts; attempt++) {
+ boxoban_build_border_grid(grid, size);
+ memset(occupied, 0, (size_t)size * (size_t)size);
+
+ if (boxoban_sample_indices(rng, confined_count, num_boxes, sampled_idx) != 0) {
+ free(box_candidates);
+ free(box_positions);
+ free(agent_candidates);
+ free(sampled_idx);
+ free(occupied);
+ return -1;
+ }
+
+ for (int i = 0; i < num_boxes; i++) {
+ BoxobanCell cell = confined[sampled_idx[i]];
+ grid[boxoban_grid_idx(size, cell.r, cell.c)] = BOXOBAN_GEN_TARGET;
+ occupied[boxoban_grid_idx(size, cell.r, cell.c)] = 1;
+ }
+
+ int box_candidate_count = 0;
+ for (int i = 0; i < confined_count; i++) {
+ BoxobanCell cell = confined[i];
+ if (!occupied[boxoban_grid_idx(size, cell.r, cell.c)]) {
+ box_candidates[box_candidate_count++] = cell;
+ }
+ }
+ if (box_candidate_count < num_boxes) {
+ continue;
+ }
+
+ if (boxoban_sample_indices(rng, box_candidate_count, num_boxes, sampled_idx) != 0) {
+ free(box_candidates);
+ free(box_positions);
+ free(agent_candidates);
+ free(sampled_idx);
+ free(occupied);
+ return -1;
+ }
+ for (int i = 0; i < num_boxes; i++) {
+ BoxobanCell cell = box_candidates[sampled_idx[i]];
+ box_positions[i] = cell;
+ grid[boxoban_grid_idx(size, cell.r, cell.c)] = BOXOBAN_GEN_BOX;
+ occupied[boxoban_grid_idx(size, cell.r, cell.c)] = 1;
+ }
+
+ int agent_candidate_count = 0;
+ for (int i = 0; i < agent_count; i++) {
+ BoxobanCell cell = agent_choices[i];
+ if (!occupied[boxoban_grid_idx(size, cell.r, cell.c)]) {
+ agent_candidates[agent_candidate_count++] = cell;
+ }
+ }
+ if (agent_candidate_count == 0) {
+ continue;
+ }
+
+ BoxobanCell agent_cell = agent_candidates[boxoban_choice_index(rng, agent_candidate_count)];
+ grid[boxoban_grid_idx(size, agent_cell.r, agent_cell.c)] = BOXOBAN_GEN_AGENT;
+
+ int all_pushable = 1;
+ for (int i = 0; i < num_boxes; i++) {
+ BoxobanCell cell = box_positions[i];
+ if (!boxoban_is_pushable(grid, size, cell.c, cell.r)) {
+ all_pushable = 0;
+ break;
+ }
+ }
+
+ if (all_pushable) {
+ free(box_candidates);
+ free(box_positions);
+ free(agent_candidates);
+ free(sampled_idx);
+ free(occupied);
+ return 0;
+ }
+ }
+
+ free(box_candidates);
+ free(box_positions);
+ free(agent_candidates);
+ free(sampled_idx);
+ free(occupied);
+ fprintf(stderr, "Failed to sample a solvable puzzle after many attempts\n");
+ return -1;
+}
+
+static int boxoban_generate_maps(
+ const char* output_dir,
+ int num_files,
+ int puzzles_per_file,
+ int size,
+ int num_boxes,
+ int min_boxes,
+ int max_boxes,
+ uint64_t seed
+) {
+ if (boxoban_mkdir_p(output_dir) != 0) {
+ return -1;
+ }
+
+ BoxobanRandom rng;
+ boxoban_seed(&rng, seed);
+
+ int max_cells = (size - 2) * (size - 2);
+ BoxobanCell* agent_choices = (BoxobanCell*)malloc((size_t)max_cells * sizeof(BoxobanCell));
+ BoxobanCell* confined = (BoxobanCell*)malloc((size_t)max_cells * sizeof(BoxobanCell));
+ char* grid = (char*)malloc((size_t)size * (size_t)size);
+ if (agent_choices == NULL || confined == NULL || grid == NULL) {
+ free(agent_choices);
+ free(confined);
+ free(grid);
+ return -1;
+ }
+
+ int interior_count = (size - 2) * (size - 2);
+ int agent_count = boxoban_build_cells(size, 0, agent_choices);
+ int confined_count = boxoban_build_cells(size, 1, confined);
+
+ for (int file_idx = 0; file_idx < num_files; file_idx++) {
+ char out_path[1200];
+ snprintf(out_path, sizeof(out_path), "%s/%03d.txt", output_dir, file_idx);
+ FILE* out = fopen(out_path, "w");
+ if (out == NULL) {
+ free(agent_choices);
+ free(confined);
+ free(grid);
+ return -1;
+ }
+
+ for (int puzzle_idx = 0; puzzle_idx < puzzles_per_file; puzzle_idx++) {
+ int box_count = num_boxes >= 1 ? num_boxes : boxoban_randint(&rng, min_boxes, max_boxes);
+ if (boxoban_make_puzzle(
+ size, &rng, box_count, 200, agent_choices, agent_count, confined, confined_count, interior_count, grid) != 0) {
+ fclose(out);
+ free(agent_choices);
+ free(confined);
+ free(grid);
+ return -1;
+ }
+
+ fprintf(out, "; %d\n", puzzle_idx);
+ for (int r = 0; r < size; r++) {
+ fwrite(&grid[boxoban_grid_idx(size, r, 0)], 1, (size_t)size, out);
+ fputc('\n', out);
+ }
+ fputc('\n', out);
+ }
+
+ fclose(out);
+ }
+
+ free(agent_choices);
+ free(confined);
+ free(grid);
+ return 0;
+}
+
+static int boxoban_generate_easy_maps(const char* output_dir, uint64_t seed) {
+ return boxoban_generate_maps(output_dir, 300, 1000, 10, -1, 1, 4, seed);
+}
+
+static int boxoban_generate_basic_maps(const char* output_dir, uint64_t seed) {
+ return boxoban_generate_maps(output_dir, 300, 1000, 10, 1, 1, 4, seed);
+}
+
+#endif
diff --git a/ocean/boxoban/parse_maps.h b/ocean/boxoban/parse_maps.h
new file mode 100644
index 0000000000..79e79ed8e1
--- /dev/null
+++ b/ocean/boxoban/parse_maps.h
@@ -0,0 +1,252 @@
+#ifndef PUFFERLIB_OCEAN_BOXOBAN_PARSE_MAPS_H
+#define PUFFERLIB_OCEAN_BOXOBAN_PARSE_MAPS_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define BOXOBAN_AGENT_CHAR '@'
+#define BOXOBAN_WALL_CHAR '#'
+#define BOXOBAN_BOX_CHAR '$'
+#define BOXOBAN_TARGET_CHAR '.'
+#define BOXOBAN_BOX_ON_TARGET_CHAR '*'
+#define BOXOBAN_AGENT_ON_TARGET_CHAR '+'
+
+#define BOXOBAN_EXPECTED_ROWS 10
+#define BOXOBAN_EXPECTED_COLS 10
+#define BOXOBAN_PUZZLE_OBS_BYTES (4 * BOXOBAN_EXPECTED_ROWS * BOXOBAN_EXPECTED_COLS)
+#define BOXOBAN_PUZZLE_META_BYTES 5
+#define BOXOBAN_PUZZLE_BYTES (BOXOBAN_PUZZLE_OBS_BYTES + BOXOBAN_PUZZLE_META_BYTES)
+
+typedef struct {
+ char rows[BOXOBAN_EXPECTED_ROWS][BOXOBAN_EXPECTED_COLS];
+ int row_lengths[BOXOBAN_EXPECTED_ROWS];
+ int row_count;
+} BoxobanPuzzleDraft;
+
+static int boxoban_is_blank_line(const char* line) {
+ const unsigned char* p = (const unsigned char*)line;
+ while (*p != '\0') {
+ if (!isspace(*p)) {
+ return 0;
+ }
+ p++;
+ }
+ return 1;
+}
+
+static int boxoban_validate_shape(const BoxobanPuzzleDraft* draft, char* reason, size_t reason_cap) {
+ if (draft->row_count != BOXOBAN_EXPECTED_ROWS) {
+ snprintf(reason, reason_cap, "expected %d rows, got %d", BOXOBAN_EXPECTED_ROWS, draft->row_count);
+ return -1;
+ }
+
+ for (int r = 0; r < BOXOBAN_EXPECTED_ROWS; r++) {
+ if (draft->row_lengths[r] != BOXOBAN_EXPECTED_COLS) {
+ snprintf(reason, reason_cap, "row %d expected %d cols, got %d",
+ r, BOXOBAN_EXPECTED_COLS, draft->row_lengths[r]);
+ return -1;
+ }
+ }
+
+ reason[0] = '\0';
+ return 0;
+}
+
+static int boxoban_encode_and_write_puzzle(const BoxobanPuzzleDraft* draft, FILE* out, char* reason, size_t reason_cap) {
+ uint8_t agent[BOXOBAN_EXPECTED_ROWS * BOXOBAN_EXPECTED_COLS] = {0};
+ uint8_t walls[BOXOBAN_EXPECTED_ROWS * BOXOBAN_EXPECTED_COLS] = {0};
+ uint8_t boxes[BOXOBAN_EXPECTED_ROWS * BOXOBAN_EXPECTED_COLS] = {0};
+ uint8_t targets[BOXOBAN_EXPECTED_ROWS * BOXOBAN_EXPECTED_COLS] = {0};
+ uint8_t meta[BOXOBAN_PUZZLE_META_BYTES] = {0};
+
+ int agent_x = -1;
+ int agent_y = -1;
+ int n_boxes = 0;
+ int n_targets = 0;
+ int on_target = 0;
+
+ int idx = 0;
+ for (int r = 0; r < BOXOBAN_EXPECTED_ROWS; r++) {
+ for (int c = 0; c < BOXOBAN_EXPECTED_COLS; c++, idx++) {
+ char ch = draft->rows[r][c];
+
+ int is_agent = (ch == BOXOBAN_AGENT_CHAR || ch == BOXOBAN_AGENT_ON_TARGET_CHAR);
+ int is_wall = (ch == BOXOBAN_WALL_CHAR);
+ int is_box = (ch == BOXOBAN_BOX_CHAR || ch == BOXOBAN_BOX_ON_TARGET_CHAR);
+ int is_target = (ch == BOXOBAN_TARGET_CHAR || ch == BOXOBAN_BOX_ON_TARGET_CHAR || ch == BOXOBAN_AGENT_ON_TARGET_CHAR);
+
+ if (is_agent) {
+ if (agent_x != -1) {
+ snprintf(reason, reason_cap, "Puzzle has multiple agents");
+ return -1;
+ }
+ agent_x = c;
+ agent_y = r;
+ }
+
+ n_boxes += is_box;
+ n_targets += is_target;
+ on_target += (is_box && is_target);
+
+ agent[idx] = (uint8_t)is_agent;
+ walls[idx] = (uint8_t)is_wall;
+ boxes[idx] = (uint8_t)is_box;
+ targets[idx] = (uint8_t)is_target;
+ }
+ }
+
+ if (agent_x == -1) {
+ snprintf(reason, reason_cap, "Puzzle has no agent");
+ return -1;
+ }
+
+ meta[0] = (uint8_t)agent_x;
+ meta[1] = (uint8_t)agent_y;
+ meta[2] = (uint8_t)n_boxes;
+ meta[3] = (uint8_t)n_targets;
+ meta[4] = (uint8_t)on_target;
+
+ if (fwrite(agent, 1, sizeof(agent), out) != sizeof(agent)) return -1;
+ if (fwrite(walls, 1, sizeof(walls), out) != sizeof(walls)) return -1;
+ if (fwrite(boxes, 1, sizeof(boxes), out) != sizeof(boxes)) return -1;
+ if (fwrite(targets, 1, sizeof(targets), out) != sizeof(targets)) return -1;
+ if (fwrite(meta, 1, sizeof(meta), out) != sizeof(meta)) return -1;
+
+ reason[0] = '\0';
+ return 0;
+}
+
+static int boxoban_finalize_puzzle(
+ BoxobanPuzzleDraft* draft,
+ FILE* out,
+ const char* src_path,
+ size_t* puzzle_idx,
+ size_t* written_count
+) {
+ char reason[128];
+ size_t idx = *puzzle_idx;
+ (*puzzle_idx)++;
+
+ if (boxoban_validate_shape(draft, reason, sizeof(reason)) != 0) {
+ fprintf(stdout, "[Boxoban] Skipping malformed puzzle in %s puzzle#%zu: %s\n", src_path, idx, reason);
+ draft->row_count = 0;
+ return 0;
+ }
+
+ if (boxoban_encode_and_write_puzzle(draft, out, reason, sizeof(reason)) != 0) {
+ if (reason[0] == '\0') {
+ return -1;
+ }
+ fprintf(stdout, "[Boxoban] Skipping malformed puzzle in %s puzzle#%zu: %s\n", src_path, idx, reason);
+ draft->row_count = 0;
+ return 0;
+ }
+
+ (*written_count)++;
+ draft->row_count = 0;
+ return 0;
+}
+
+static int boxoban_write_bin_from_files(
+ const char* const* files,
+ size_t file_count,
+ const char* out_path,
+ int verbose,
+ size_t* out_puzzle_count
+) {
+ FILE* out = fopen(out_path, "wb");
+ if (out == NULL) {
+ return -1;
+ }
+
+ size_t puzzle_count = 0;
+
+ for (size_t file_idx = 0; file_idx < file_count; file_idx++) {
+ const char* src_path = files[file_idx];
+ FILE* in = fopen(src_path, "r");
+ if (in == NULL) {
+ fclose(out);
+ return -1;
+ }
+
+ char* line = NULL;
+ size_t line_cap = 0;
+ ssize_t line_len;
+ BoxobanPuzzleDraft draft;
+ memset(&draft, 0, sizeof(draft));
+ size_t puzzle_idx = 0;
+
+ while ((line_len = getline(&line, &line_cap, in)) != -1) {
+ if (line_len > 0 && line[line_len - 1] == '\n') {
+ line[--line_len] = '\0';
+ }
+
+ if (line[0] == ';') {
+ if (draft.row_count > 0) {
+ if (boxoban_finalize_puzzle(&draft, out, src_path, &puzzle_idx, &puzzle_count) != 0) {
+ free(line);
+ fclose(in);
+ fclose(out);
+ return -1;
+ }
+ }
+ continue;
+ }
+
+ if (boxoban_is_blank_line(line)) {
+ continue;
+ }
+
+ if (draft.row_count < BOXOBAN_EXPECTED_ROWS) {
+ int dst_row = draft.row_count;
+ int copy_len = line_len < BOXOBAN_EXPECTED_COLS ? (int)line_len : BOXOBAN_EXPECTED_COLS;
+ memcpy(draft.rows[dst_row], line, (size_t)copy_len);
+ draft.row_lengths[dst_row] = (int)line_len;
+ draft.row_count++;
+ }
+
+ if (draft.row_count == BOXOBAN_EXPECTED_ROWS) {
+ if (boxoban_finalize_puzzle(&draft, out, src_path, &puzzle_idx, &puzzle_count) != 0) {
+ free(line);
+ fclose(in);
+ fclose(out);
+ return -1;
+ }
+ }
+ }
+
+ free(line);
+ fclose(in);
+ }
+
+ if (fflush(out) != 0) {
+ fclose(out);
+ return -1;
+ }
+
+ long bytes_written = ftell(out);
+ fclose(out);
+ if (bytes_written < 0) {
+ return -1;
+ }
+
+ size_t expected = puzzle_count * BOXOBAN_PUZZLE_BYTES;
+ if ((size_t)bytes_written != expected) {
+ fprintf(stderr, "Wrong output size: got %ld expected %zu\n", bytes_written, expected);
+ return -1;
+ }
+
+ if (verbose) {
+ fprintf(stdout, "Wrote %zu puzzles to %s\n", puzzle_count, out_path);
+ }
+ if (out_puzzle_count != NULL) {
+ *out_puzzle_count = puzzle_count;
+ }
+ return 0;
+}
+
+#endif
diff --git a/ocean/breakout/binding.c b/ocean/breakout/binding.c
new file mode 100644
index 0000000000..471e78b9c9
--- /dev/null
+++ b/ocean/breakout/binding.c
@@ -0,0 +1,35 @@
+#include "breakout.h"
+#define OBS_SIZE 118
+#define NUM_ATNS 1
+#define ACT_SIZES {3}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Breakout
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->frameskip = dict_get(kwargs, "frameskip")->value;
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->initial_paddle_width = dict_get(kwargs, "paddle_width")->value;
+ env->paddle_height = dict_get(kwargs, "paddle_height")->value;
+ env->ball_width = dict_get(kwargs, "ball_width")->value;
+ env->ball_height = dict_get(kwargs, "ball_height")->value;
+ env->brick_width = dict_get(kwargs, "brick_width")->value;
+ env->brick_height = dict_get(kwargs, "brick_height")->value;
+ env->brick_rows = dict_get(kwargs, "brick_rows")->value;
+ env->brick_cols = dict_get(kwargs, "brick_cols")->value;
+ env->initial_ball_speed = dict_get(kwargs, "initial_ball_speed")->value;
+ env->max_ball_speed = dict_get(kwargs, "max_ball_speed")->value;
+ env->paddle_speed = dict_get(kwargs, "paddle_speed")->value;
+ env->continuous = dict_get(kwargs, "continuous")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+}
diff --git a/ocean/breakout/breakout.c b/ocean/breakout/breakout.c
new file mode 100644
index 0000000000..840297402e
--- /dev/null
+++ b/ocean/breakout/breakout.c
@@ -0,0 +1,64 @@
+#include
+#include "breakout.h"
+#include "puffernet.h"
+
+void demo() {
+ Weights* weights = load_weights("resources/breakout/breakout_weights.bin");
+ int logit_sizes[1] = {3};
+ PufferNet* net = make_puffernet(weights, 1, 118, 64, 2, logit_sizes, 1);
+
+ Breakout env = {
+ .frameskip = 1,
+ .width = 576,
+ .height = 330,
+ .initial_paddle_width = 62,
+ .paddle_width = 62,
+ .paddle_height = 8,
+ .ball_width = 32,
+ .ball_height = 32,
+ .brick_width = 32,
+ .brick_height = 12,
+ .brick_rows = 6,
+ .brick_cols = 18,
+ .initial_ball_speed = 256,
+ .max_ball_speed = 448,
+ .paddle_speed = 620,
+ .continuous = 0,
+ };
+ allocate(&env);
+
+ env.client = make_client(&env);
+
+ c_reset(&env);
+ int frame = 0;
+ SetTargetFPS(60);
+ while (!WindowShouldClose()) {
+ // User can take control of the paddle
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ if(env.continuous) {
+ float move = GetMouseWheelMove();
+ float clamped_wheel = fmaxf(-1.0f, fminf(1.0f, move));
+ env.actions[0] = clamped_wheel;
+ } else {
+ env.actions[0] = 0.0;
+ if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) env.actions[0] = 1;
+ if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) env.actions[0] = 2;
+ }
+ } else if (frame % 4 == 0) {
+ // Apply frameskip outside the env for smoother rendering
+ forward_puffernet(net, env.observations, env.actions);
+ }
+
+ frame = (frame + 1) % 4;
+ c_step(&env);
+ c_render(&env);
+ }
+ free_puffernet(net);
+ free(weights);
+ free_allocated(&env);
+ close_client(env.client);
+}
+
+int main() {
+ demo();
+}
diff --git a/pufferlib/ocean/breakout/breakout.h b/ocean/breakout/breakout.h
similarity index 89%
rename from pufferlib/ocean/breakout/breakout.h
rename to ocean/breakout/breakout.h
index 8366b50658..a4a1dad425 100644
--- a/pufferlib/ocean/breakout/breakout.h
+++ b/ocean/breakout/breakout.h
@@ -42,7 +42,8 @@ typedef struct Breakout {
float* observations;
float* actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
+ int num_agents;
int score;
float paddle_x;
float paddle_y;
@@ -78,6 +79,7 @@ typedef struct Breakout {
int frameskip;
unsigned char hit_brick;
int continuous;
+ unsigned int rng;
} Breakout;
typedef struct CollisionInfo CollisionInfo;
@@ -121,7 +123,7 @@ void allocate(Breakout* env) {
env->observations = (float*)calloc(11 + env->num_bricks, sizeof(float));
env->actions = (float*)calloc(1, sizeof(float));
env->rewards = (float*)calloc(1, sizeof(float));
- env->terminals = (unsigned char*)calloc(1, sizeof(unsigned char));
+ env->terminals = (float*)calloc(1, sizeof(float));
}
void c_close(Breakout* env) {
@@ -157,9 +159,7 @@ void compute_observations(Breakout* env) {
env->observations[7] = env->score / 864.0f;
env->observations[8] = env->num_balls / 5.0f;
env->observations[9] = env->paddle_width / (2.0f * HALF_PADDLE_WIDTH);
- for (int i = 0; i < env->num_bricks; i++) {
- env->observations[10 + i] = env->brick_states[i];
- }
+ memcpy(env->observations + 10, env->brick_states, sizeof(float) * env->num_bricks);
}
// Collision of a stationary vertical line segment (xw,yw) to (xw,yw+hw)
@@ -167,8 +167,8 @@ void compute_observations(Breakout* env) {
static inline bool calc_vline_collision(float xw, float yw, float hw, float x,
float y, float vx, float vy, float h, CollisionInfo* col) {
float t_new = (xw - x) / vx;
- float topmost = fmin(yw + hw, y + h + vy * t_new);
- float botmost = fmax(yw, y + vy * t_new);
+ float topmost = fminf(yw + hw, y + h + vy * t_new);
+ float botmost = fmaxf(yw, y + vy * t_new);
float overlap_new = topmost - botmost;
// Collision finds the smallest time of collision with the greatest overlap
@@ -246,26 +246,52 @@ static inline void calc_brick_collision(Breakout* env, int idx,
}
}
static inline int column_index(Breakout* env, float x) {
- return (int)(floorf(x / env->brick_width));
+ return (int)(x / env->brick_width);
}
static inline int row_index(Breakout* env, float y) {
- return (int)(floorf((y - Y_OFFSET) / env->brick_height));
+ return (int)((y - Y_OFFSET) / env->brick_height);
}
void calc_all_brick_collisions(Breakout* env, CollisionInfo* collision_info) {
- int column_from = column_index(env, fminf(env->ball_x + env->ball_vx, env->ball_x));
- column_from = fmaxf(column_from, 0);
- int column_to = column_index(env, fmaxf(env->ball_x + env->ball_width + env->ball_vx, env->ball_x + env->ball_width));
- column_to = fminf(column_to, env->brick_cols - 1);
- int row_from = row_index(env, fminf(env->ball_y + env->ball_vy, env->ball_y));
- row_from = fmaxf(row_from, 0);
- int row_to = row_index(env, fmaxf(env->ball_y + env->ball_height + env->ball_vy, env->ball_y + env->ball_height));
- row_to = fminf(row_to, env->brick_rows - 1);
+ float ball_x = env->ball_x;
+ float ball_x_dst = ball_x + env->ball_vx;
+ float ball_y = env->ball_y;
+ float ball_y_dst = ball_y + env->ball_vy;
+ float ball_width = env->ball_width;
+ float ball_height = env->ball_height;
+
+ int row_from = row_index(env, ball_y < ball_y_dst ? ball_y : ball_y_dst);
+ if (row_from < 0) {
+ row_from = 0;
+ }
+
+ if (row_from > env->brick_rows) {
+ return;
+ }
+
+ int column_from = column_index(env, ball_x < ball_x_dst ? ball_x : ball_x_dst);
+ if (column_from < 0) {
+ column_from = 0;
+ }
+
+ float ball_x_end = ball_x + ball_width;
+ float ball_x_dst_end = ball_x_dst + ball_width;
+ int column_to = column_index(env, ball_x_dst_end > ball_x_end ? ball_x_dst_end : ball_x_end);
+ if (column_to >= env->brick_cols) {
+ column_to = env->brick_cols - 1;
+ }
+
+ float ball_y_end = ball_y + ball_height;
+ float ball_y_dst_end = ball_y_dst + ball_height;
+ int row_to = row_index(env, ball_y_dst_end > ball_y_end ? ball_y_dst_end : ball_y_end);
+ if (row_to >= env->brick_rows) {
+ row_to = env->brick_rows - 1;
+ }
for (int row = row_from; row <= row_to; row++) {
for (int column = column_from; column <= column_to; column++) {
int brick_index = row * env->brick_cols + column;
- if (env->brick_states[brick_index] == 0.0)
+ if (env->brick_states[brick_index] == 0.0f)
calc_brick_collision(env, brick_index, collision_info);
}
}
@@ -295,8 +321,8 @@ bool calc_paddle_ball_collisions(Breakout* env, CollisionInfo* collision_info) {
float relative_intersection = (
(env->ball_x + env->ball_width / 2) - env->paddle_x) / env->paddle_width;
float angle = -base_angle + relative_intersection * 2 * base_angle;
- env->ball_vx = sin(angle) * env->ball_speed * TICK_RATE;
- env->ball_vy = -cos(angle) * env->ball_speed * TICK_RATE;
+ env->ball_vx = sinf(angle) * env->ball_speed * TICK_RATE;
+ env->ball_vy = -cosf(angle) * env->ball_speed * TICK_RATE;
env->hits += 1;
if (env->hits % 4 == 0 && env->ball_speed < env->max_ball_speed) {
env->ball_speed += 64;
@@ -428,9 +454,9 @@ void step_frame(Breakout* env, float action) {
env->balls_fired = 1;
float direction = M_PI / 3.25f;
- env->ball_vy = cos(direction) * env->ball_speed * TICK_RATE;
- env->ball_vx = sin(direction) * env->ball_speed * TICK_RATE;
- if (rand() % 2 == 0) {
+ env->ball_vy = cosf(direction) * env->ball_speed * TICK_RATE;
+ env->ball_vx = sinf(direction) * env->ball_speed * TICK_RATE;
+ if (rand_r(&env->rng) % 2 == 0) {
env->ball_vx = -env->ball_vx;
}
}
diff --git a/ocean/cartpole/binding.c b/ocean/cartpole/binding.c
new file mode 100644
index 0000000000..16776c376d
--- /dev/null
+++ b/ocean/cartpole/binding.c
@@ -0,0 +1,30 @@
+#include "cartpole.h"
+#define OBS_SIZE 4
+#define NUM_ATNS 1
+#define ACT_SIZES {2}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Cartpole
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->cart_mass = dict_get(kwargs, "cart_mass")->value;
+ env->pole_mass = dict_get(kwargs, "pole_mass")->value;
+ env->pole_length = dict_get(kwargs, "pole_length")->value;
+ env->gravity = dict_get(kwargs, "gravity")->value;
+ env->force_mag = dict_get(kwargs, "force_mag")->value;
+ env->tau = dict_get(kwargs, "dt")->value;
+ env->continuous = dict_get(kwargs, "continuous")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "score", log->score);
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "x_threshold_termination", log->x_threshold_termination);
+ dict_set(out, "pole_angle_termination", log->pole_angle_termination);
+ dict_set(out, "max_steps_termination", log->max_steps_termination);
+ dict_set(out, "n", log->n);
+}
diff --git a/ocean/cartpole/cartpole.c b/ocean/cartpole/cartpole.c
new file mode 100644
index 0000000000..9078b121d4
--- /dev/null
+++ b/ocean/cartpole/cartpole.c
@@ -0,0 +1,71 @@
+// local compile/eval implemented for discrete actions only
+// eval with python demo.py --mode eval --env puffer_cartpole --eval-mode-path
+
+#include
+#include
+#include
+#include
+#include "cartpole.h"
+#include "puffernet.h"
+
+#define OBSERVATIONS_SIZE 4
+#define ACTIONS_SIZE 2
+#define CONTINUOUS 0
+
+const char* WEIGHTS_PATH = "resources/cartpole/cartpole_weights.bin";
+
+float movement(float action, int userControlMode) {
+ if (userControlMode) {
+ return (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) ? 1.0f : -1.0f;
+ } else {
+ return (action > 0.5f) ? 1.0f : -1.0f;
+ }
+}
+
+void demo() {
+ Weights* weights = load_weights(WEIGHTS_PATH);
+
+ int logit_sizes[1] = {ACTIONS_SIZE};
+ PufferNet* net = make_puffernet(weights, 1, OBSERVATIONS_SIZE, 32, 2, logit_sizes, 1);
+
+ Cartpole env = {
+ .continuous = CONTINUOUS,
+ .cart_mass = 1.0f,
+ .pole_mass = 0.1f,
+ .pole_length = 0.5f,
+ .gravity = 9.8f,
+ .force_mag = 10.0f,
+ .tau = 0.02f,
+ };
+ allocate(&env);
+ c_reset(&env);
+ c_render(&env);
+
+ while (!WindowShouldClose()) {
+ int userControlMode = IsKeyDown(KEY_LEFT_SHIFT);
+
+ if (!userControlMode) {
+ forward_puffernet(net, env.observations, env.actions);
+ env.actions[0] = movement(env.actions[0], 0);
+ } else {
+ env.actions[0] = movement(env.actions[0], userControlMode);
+ }
+
+ c_step(&env);
+ c_render(&env);
+
+ if (env.terminals[0] > 0.5f) {
+ c_reset(&env);
+ }
+ }
+
+ free_puffernet(net);
+ free(weights);
+ free_allocated(&env);
+}
+
+int main() {
+ srand(time(NULL));
+ demo();
+ return 0;
+}
diff --git a/pufferlib/ocean/cartpole/cartpole.h b/ocean/cartpole/cartpole.h
similarity index 84%
rename from pufferlib/ocean/cartpole/cartpole.h
rename to ocean/cartpole/cartpole.h
index 7d87e11d6c..0f189ca1ab 100644
--- a/pufferlib/ocean/cartpole/cartpole.h
+++ b/ocean/cartpole/cartpole.h
@@ -33,9 +33,10 @@ struct Cartpole {
float* observations;
float* actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
unsigned char* truncations;
Log log;
+ int num_agents;
Client* client;
float x;
float x_dot;
@@ -50,6 +51,7 @@ struct Cartpole {
float tau;
int continuous;
float episode_return;
+ unsigned int rng;
};
void add_log(Cartpole* env) {
@@ -76,7 +78,7 @@ void allocate(Cartpole* env) {
env->observations = (float*)calloc(4, sizeof(float));
env->actions = (float*)calloc(1, sizeof(float));
env->rewards = (float*)calloc(1, sizeof(float));
- env->terminals = (unsigned char*)calloc(1, sizeof(unsigned char));
+ env->terminals = (float*)calloc(1, sizeof(float));
}
void free_allocated(Cartpole* env) {
@@ -141,34 +143,17 @@ void compute_observations(Cartpole* env) {
void c_reset(Cartpole* env) {
env->episode_return = 0.0f;
- env->x = ((float)rand() / (float)RAND_MAX) * 0.08f - 0.04f;
- env->x_dot = ((float)rand() / (float)RAND_MAX) * 0.08f - 0.04f;
- env->theta = ((float)rand() / (float)RAND_MAX) * 0.08f - 0.04f;
- env->theta_dot = ((float)rand() / (float)RAND_MAX) * 0.08f - 0.04f;
+ env->x = ((float)rand_r(&env->rng) / (float)RAND_MAX) * 0.08f - 0.04f;
+ env->x_dot = ((float)rand_r(&env->rng) / (float)RAND_MAX) * 0.08f - 0.04f;
+ env->theta = ((float)rand_r(&env->rng) / (float)RAND_MAX) * 0.08f - 0.04f;
+ env->theta_dot = ((float)rand_r(&env->rng) / (float)RAND_MAX) * 0.08f - 0.04f;
env->tick = 0;
compute_observations(env);
}
void c_step(Cartpole* env) {
- // float force = 0.0;
- // if (env->continuous) {
- // force = env->actions[0] * FORCE_MAG;
- // } else {
- // force = (env->actions[0] > 0.5f) ? FORCE_MAG : -FORCE_MAG;
- // }
-
float a = env->actions[0];
-
- /* ===== runtime sanity check –– delete after debugging ===== */
- if (!isfinite(a) || a < -1.0001f || a > 1.0001f) {
- fprintf(stderr,
- "[BAD ACTION] tick=%d raw=%.6f\n",
- env->tick, a);
- fflush(stderr);
- }
- /* ========================================================== */
-
if (!isfinite(a)) {
a = 0.0f;
}
@@ -176,7 +161,7 @@ void c_step(Cartpole* env) {
env->actions[0] = a;
float force = env->continuous ? a * env->force_mag
- : (a > 0.5f ? env->force_mag: -env->force_mag);
+ : (a > 0.5f ? env->force_mag: -env->force_mag);
float costheta = cosf(env->theta);
float sintheta = sinf(env->theta);
diff --git a/pufferlib/ocean/chain_mdp/binding.c b/ocean/chain_mdp/binding.c
similarity index 100%
rename from pufferlib/ocean/chain_mdp/binding.c
rename to ocean/chain_mdp/binding.c
diff --git a/pufferlib/ocean/chain_mdp/chain_mdp.c b/ocean/chain_mdp/chain_mdp.c
similarity index 100%
rename from pufferlib/ocean/chain_mdp/chain_mdp.c
rename to ocean/chain_mdp/chain_mdp.c
diff --git a/pufferlib/ocean/chain_mdp/chain_mdp.h b/ocean/chain_mdp/chain_mdp.h
similarity index 100%
rename from pufferlib/ocean/chain_mdp/chain_mdp.h
rename to ocean/chain_mdp/chain_mdp.h
diff --git a/pufferlib/ocean/checkers/binding.c b/ocean/checkers/binding.c
similarity index 100%
rename from pufferlib/ocean/checkers/binding.c
rename to ocean/checkers/binding.c
diff --git a/pufferlib/ocean/checkers/checkers.c b/ocean/checkers/checkers.c
similarity index 100%
rename from pufferlib/ocean/checkers/checkers.c
rename to ocean/checkers/checkers.c
diff --git a/pufferlib/ocean/checkers/checkers.h b/ocean/checkers/checkers.h
similarity index 100%
rename from pufferlib/ocean/checkers/checkers.h
rename to ocean/checkers/checkers.h
diff --git a/ocean/chess/binding.c b/ocean/chess/binding.c
new file mode 100644
index 0000000000..1a3cd74d63
--- /dev/null
+++ b/ocean/chess/binding.c
@@ -0,0 +1,218 @@
+#include "chess.h"
+// Before embedding approach
+// #define OBS_SIZE 1082
+#define OBS_SIZE 167
+#define NUM_ATNS 1
+#define ACT_SIZES {97}
+#define OBS_TENSOR_T ByteTensor
+#define MY_ACTION_MASK 97
+
+#define MY_VEC_INIT
+#define MY_VEC_CLOSE
+#define MY_USES_PERM
+#define MY_USES_TAGS
+#define Env Chess
+#include "vecenv.h"
+
+void my_setup_perm(StaticVec* vec, Env* env, int slot_base) {
+ size_t obs_elem_size = obs_element_size();
+ for (int s = 0; s < env->num_agents; s++) {
+ int phys = vec->agent_perm ? vec->agent_perm[slot_base + s] : (slot_base + s);
+ env->obs_ptr[s] = (uint8_t*)vec->observations + (size_t)phys * OBS_SIZE * obs_elem_size;
+ env->action_mask_ptr[s] = vec->action_mask + (size_t)phys * MY_ACTION_MASK;
+ env->action_ptr[s] = vec->actions + (size_t)phys * NUM_ATNS;
+ env->reward_ptr[s] = vec->rewards + phys;
+ env->terminal_ptr[s] = vec->terminals + phys;
+ }
+}
+
+#define DEFAULT_STARTING_FEN "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
+#define FEN_CURRICULUM_PATH "resources/chess/fens.txt"
+
+static char** SHARED_FEN_CURRICULUM = NULL;
+static int SHARED_NUM_FENS = 0;
+
+static char** load_fen_file(const char* path, int* num_fens_out) {
+ FILE* f = fopen(path, "r");
+ if (f == NULL) {
+ *num_fens_out = 0;
+ return NULL;
+ }
+
+ int num_fens = 0;
+ char line[256];
+ while (fgets(line, sizeof(line), f)) {
+ if (line[0] != '#' && line[0] != '\n' && line[0] != '\r') {
+ num_fens++;
+ }
+ }
+ if (num_fens == 0) {
+ fclose(f);
+ *num_fens_out = 0;
+ return NULL;
+ }
+
+ char** fens = (char**)malloc(num_fens * sizeof(char*));
+ rewind(f);
+ int idx = 0;
+ while (fgets(line, sizeof(line), f) && idx < num_fens) {
+ if (line[0] != '#' && line[0] != '\n' && line[0] != '\r') {
+ size_t len = strlen(line);
+ while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) {
+ line[--len] = '\0';
+ }
+ fens[idx++] = strdup(line);
+ }
+ }
+ fclose(f);
+ *num_fens_out = num_fens;
+ return fens;
+}
+
+static void apply_kwargs(Env* env, Dict* kwargs) {
+ env->max_moves = (int)dict_get(kwargs, "max_moves")->value;
+ env->reward_draw = (float)dict_get(kwargs, "reward_draw")->value;
+ env->reward_invalid_piece = (float)dict_get(kwargs, "reward_invalid_piece")->value;
+ env->reward_invalid_move = (float)dict_get(kwargs, "reward_invalid_move")->value;
+ env->reward_repetition = (float)dict_get(kwargs, "reward_repetition")->value;
+ env->render_fps = (int)dict_get(kwargs, "render_fps")->value;
+ env->mode = (int)dict_get(kwargs, "mode")->value;
+ env->enable_50_move_rule = (int)dict_get(kwargs, "enable_50_move_rule")->value;
+ env->enable_threefold_repetition = (int)dict_get(kwargs, "enable_threefold_repetition")->value;
+ env->random_fen = (int)dict_get(kwargs, "random_fen")->value;
+ env->fen_curric_pct = (float)dict_get(kwargs, "fen_curric_pct")->value;
+
+ env->client = NULL;
+ env->legal_dirty = 1;
+ env->human_color = -1;
+ env->log_pgn = 0;
+ env->log_pgn_choice_made = 1;
+ env->pgn_filename[0] = '\0';
+ env->pgn_game_number = 0;
+ env->maia_pid = 0;
+ env->maia_stdin_fd = -1;
+ env->maia_stdout_fd = -1;
+ env->maia_phase = 0;
+ strcpy(env->starting_fen, DEFAULT_STARTING_FEN);
+ strcpy(env->last_result, "Game starting...");
+}
+
+Env* my_vec_init(int* num_envs_out, int* buffer_env_starts, int* buffer_env_counts,
+ Dict* vec_kwargs, Dict* env_kwargs) {
+ int total_agents = (int)dict_get(vec_kwargs, "total_agents")->value;
+ int num_buffers = (int)dict_get(vec_kwargs, "num_buffers")->value;
+ int agents_per_buffer = total_agents / num_buffers;
+
+ float curric_pct = (float)dict_get(env_kwargs, "fen_curric_pct")->value;
+ if (curric_pct > 0.0f && SHARED_FEN_CURRICULUM == NULL) {
+ SHARED_FEN_CURRICULUM = load_fen_file(FEN_CURRICULUM_PATH, &SHARED_NUM_FENS);
+ if (SHARED_FEN_CURRICULUM != NULL) {
+ printf("Loaded %d FENs from %s\n", SHARED_NUM_FENS, FEN_CURRICULUM_PATH);
+ }
+ }
+
+ int mode = (int)dict_get(env_kwargs, "mode")->value;
+ int agents_per_env = (mode == CHESS_MODE_SELFPLAY) ? 2 : 1;
+ int num_envs = total_agents / agents_per_env;
+ Env* envs = (Env*)calloc(num_envs, sizeof(Env));
+ for (int i = 0; i < num_envs; i++) {
+ Env* env = &envs[i];
+ apply_kwargs(env, env_kwargs);
+ env->num_agents = agents_per_env;
+ env->rng = i;
+ // In selfplay, learner_color is unused; the slot↔color mapping is per-env
+ // randomized so policies in fixed slots see both colors equally.
+ env->learner_color = (agents_per_env == 1) ? (i % 2) : CHESS_WHITE;
+ if (agents_per_env == 2 && (i & 1)) {
+ env->slot_for_color[CHESS_WHITE] = 1;
+ env->slot_for_color[CHESS_BLACK] = 0;
+ } else {
+ env->slot_for_color[CHESS_WHITE] = 0;
+ env->slot_for_color[CHESS_BLACK] = 1;
+ }
+ env->fen_curriculum = SHARED_FEN_CURRICULUM;
+ env->num_fens = SHARED_NUM_FENS;
+ init_bitboards();
+ }
+
+ int buf = 0;
+ int buf_agents = 0;
+ buffer_env_starts[0] = 0;
+ buffer_env_counts[0] = 0;
+ for (int i = 0; i < num_envs; i++) {
+ buf_agents += agents_per_env;
+ buffer_env_counts[buf]++;
+ if (buf_agents >= agents_per_buffer && buf < num_buffers - 1) {
+ buf++;
+ buffer_env_starts[buf] = i + 1;
+ buffer_env_counts[buf] = 0;
+ buf_agents = 0;
+ }
+ }
+
+ *num_envs_out = num_envs;
+ return envs;
+}
+
+void my_vec_close(Env* envs) {
+ if (SHARED_FEN_CURRICULUM != NULL) {
+ for (int i = 0; i < SHARED_NUM_FENS; i++) {
+ free(SHARED_FEN_CURRICULUM[i]);
+ }
+ free(SHARED_FEN_CURRICULUM);
+ SHARED_FEN_CURRICULUM = NULL;
+ SHARED_NUM_FENS = 0;
+ }
+}
+
+void my_init(Env* env, Dict* kwargs) {
+ apply_kwargs(env, kwargs);
+ env->num_agents = (env->mode == CHESS_MODE_SELFPLAY) ? 2 : 1;
+ env->learner_color = (env->num_agents == 1) ? CHESS_WHITE : CHESS_WHITE;
+ env->slot_for_color[CHESS_WHITE] = 0;
+ env->slot_for_color[CHESS_BLACK] = 1;
+ env->fen_curriculum = NULL;
+ env->num_fens = 0;
+ init_bitboards();
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "draw_rate", log->draw_rate);
+ dict_set(out, "timeout_rate", log->timeout_rate);
+ dict_set(out, "chess_moves", log->chess_moves);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "invalid_action_rate", log->invalid_action_rate);
+ dict_set(out, "slot_0_score", log->slot_0_score);
+ dict_set(out, "slot_1_score", log->slot_1_score);
+ dict_set(out, "hist_score", log->hist_score);
+ dict_set(out, "hist_n", log->hist_n);
+ // Per-bank historical stats for multi-bank selfplay. selfplay.py reads
+ // hist_score_bank_ / hist_n_bank_ to drive each bank's swap decision.
+ // dict_set stores the key pointer (vecenv.h:61), not a copy, so we MUST
+ // use string literals here — a stack buffer in a loop aliases and collapses
+ // all 16 entries into one. Sized to CHESS_MAX_BANKS = 8.
+ dict_set(out, "hist_score_bank_0", log->hist_score_bank[0]);
+ dict_set(out, "hist_score_bank_1", log->hist_score_bank[1]);
+ dict_set(out, "hist_score_bank_2", log->hist_score_bank[2]);
+ dict_set(out, "hist_score_bank_3", log->hist_score_bank[3]);
+ dict_set(out, "hist_score_bank_4", log->hist_score_bank[4]);
+ dict_set(out, "hist_score_bank_5", log->hist_score_bank[5]);
+ dict_set(out, "hist_score_bank_6", log->hist_score_bank[6]);
+ dict_set(out, "hist_score_bank_7", log->hist_score_bank[7]);
+ dict_set(out, "hist_n_bank_0", log->hist_n_bank[0]);
+ dict_set(out, "hist_n_bank_1", log->hist_n_bank[1]);
+ dict_set(out, "hist_n_bank_2", log->hist_n_bank[2]);
+ dict_set(out, "hist_n_bank_3", log->hist_n_bank[3]);
+ dict_set(out, "hist_n_bank_4", log->hist_n_bank[4]);
+ dict_set(out, "hist_n_bank_5", log->hist_n_bank[5]);
+ dict_set(out, "hist_n_bank_6", log->hist_n_bank[6]);
+ dict_set(out, "hist_n_bank_7", log->hist_n_bank[7]);
+ dict_set(out, "wins_as_white", log->wins_as_white);
+ dict_set(out, "wins_as_black", log->wins_as_black);
+ dict_set(out, "games_as_white", log->games_as_white);
+ dict_set(out, "games_as_black", log->games_as_black);
+ dict_set(out, "maia_failures", log->maia_failures);
+}
diff --git a/ocean/chess/chess.h b/ocean/chess/chess.h
new file mode 100644
index 0000000000..2853232f1e
--- /dev/null
+++ b/ocean/chess/chess.h
@@ -0,0 +1,3167 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "raylib.h"
+
+typedef uint64_t Bitboard;
+typedef uint64_t Key;
+typedef uint32_t Square;
+typedef uint32_t Move;
+typedef uint32_t Piece;
+typedef uint8_t ChessColor;
+
+enum {
+ SQ_A1, SQ_B1, SQ_C1, SQ_D1, SQ_E1, SQ_F1, SQ_G1, SQ_H1,
+ SQ_A2, SQ_B2, SQ_C2, SQ_D2, SQ_E2, SQ_F2, SQ_G2, SQ_H2,
+ SQ_A3, SQ_B3, SQ_C3, SQ_D3, SQ_E3, SQ_F3, SQ_G3, SQ_H3,
+ SQ_A4, SQ_B4, SQ_C4, SQ_D4, SQ_E4, SQ_F4, SQ_G4, SQ_H4,
+ SQ_A5, SQ_B5, SQ_C5, SQ_D5, SQ_E5, SQ_F5, SQ_G5, SQ_H5,
+ SQ_A6, SQ_B6, SQ_C6, SQ_D6, SQ_E6, SQ_F6, SQ_G6, SQ_H6,
+ SQ_A7, SQ_B7, SQ_C7, SQ_D7, SQ_E7, SQ_F7, SQ_G7, SQ_H7,
+ SQ_A8, SQ_B8, SQ_C8, SQ_D8, SQ_E8, SQ_F8, SQ_G8, SQ_H8,
+ SQ_NONE = 64
+};
+
+enum { PAWN = 1, KNIGHT, BISHOP, ROOK, QUEEN, KING };
+
+enum {
+ NO_PIECE = 0,
+ W_PAWN = 1, W_KNIGHT, W_BISHOP, W_ROOK, W_QUEEN, W_KING,
+ B_PAWN = 9, B_KNIGHT, B_BISHOP, B_ROOK, B_QUEEN, B_KING
+};
+
+enum { CHESS_WHITE = 0, CHESS_BLACK = 1 };
+
+enum {
+ NO_CASTLING = 0,
+ WHITE_OO = 1, WHITE_OOO = 2,
+ BLACK_OO = 4, BLACK_OOO = 8,
+ WHITE_CASTLING = 3, BLACK_CASTLING = 12
+};
+
+
+enum { NORMAL, PROMOTION, ENPASSANT, CASTLING };
+
+enum {
+ NORTH = 8, EAST = 1, SOUTH = -8, WEST = -1,
+ NORTH_EAST = 9, SOUTH_EAST = -7,
+ NORTH_WEST = 7, SOUTH_WEST = -9
+};
+
+#define MOVE_NONE 0
+#define MOVE_NULL 65
+
+static inline Move make_move(Square from, Square to) {
+ return (Move)(to | (from << 6));
+}
+
+static inline Move make_promotion(Square from, Square to, int pt) {
+ return (Move)(to | (from << 6) | (PROMOTION << 14) | ((pt - KNIGHT) << 12));
+}
+
+static inline Move make_enpassant(Square from, Square to) {
+ return (Move)(to | (from << 6) | (ENPASSANT << 14));
+}
+
+static inline Move make_castling(Square from, Square to) {
+ return (Move)(to | (from << 6) | (CASTLING << 14));
+}
+
+static inline Square from_sq(Move m) {
+ return (Square)((m >> 6) & 0x3f);
+}
+
+static inline Square to_sq(Move m) {
+ return (Square)(m & 0x3f);
+}
+
+static inline int type_of_m(Move m) {
+ return (int)(m >> 14);
+}
+
+static inline int promotion_type(Move m) {
+ return (int)(((m >> 12) & 3) + KNIGHT);
+}
+
+static inline Square make_square(int f, int r) {
+ return (Square)((r << 3) + f);
+}
+
+static inline int file_of(Square s) {
+ return (int)(s & 7);
+}
+
+static inline int rank_of(Square s) {
+ return (int)(s >> 3);
+}
+
+static inline Piece make_piece(int c, int pt) {
+ return (Piece)((c << 3) + pt);
+}
+
+static inline int type_of_p(Piece p) {
+ return (int)(p & 7);
+}
+
+static inline int color_of(Piece p) {
+ return (int)(p >> 3);
+}
+#define MAX_GAME_PLIES 2048
+
+#define FileABB 0x0101010101010101ULL
+#define FileBBB (FileABB << 1)
+#define FileCBB (FileABB << 2)
+#define FileDBB (FileABB << 3)
+#define FileEBB (FileABB << 4)
+#define FileFBB (FileABB << 5)
+#define FileGBB (FileABB << 6)
+#define FileHBB (FileABB << 7)
+
+#define Rank1BB 0xFFULL
+#define Rank2BB (Rank1BB << 8)
+#define Rank3BB (Rank1BB << 16)
+#define Rank4BB (Rank1BB << 24)
+#define Rank5BB (Rank1BB << 32)
+#define Rank6BB (Rank1BB << 40)
+#define Rank7BB (Rank1BB << 48)
+#define Rank8BB (Rank1BB << 56)
+
+#define SQ_FEATURES 15
+
+static const char* PIECE_CHARS[] = {
+ "",
+ "P", "N", "B", "R", "Q", "K",
+ "", "",
+ "p", "n", "b", "r", "q", "k"
+};
+
+static const char* PIECE_FILLED[] = {
+ "",
+ "♟", "♞", "♝", "♜", "♛", "♚",
+ "", "",
+ "♟", "♞", "♝", "♜", "♛", "♚"
+};
+
+
+static uint64_t prng_state = 1070372;
+static inline uint64_t prng_rand(void) {
+ prng_state ^= prng_state >> 12;
+ prng_state ^= prng_state << 25;
+ prng_state ^= prng_state >> 27;
+ return prng_state * 2685821657736338717ULL;
+}
+
+extern Bitboard SquareBB[65];
+extern Bitboard PawnAttacks[2][64];
+extern Bitboard KnightAttacks[64];
+extern Bitboard KingAttacks[64];
+extern Bitboard BetweenBB[64][64];
+extern Bitboard LineBB[64][64];
+
+static Bitboard BishopMasks[64];
+static uint64_t BishopMagics[64];
+static int BishopShifts[64];
+static Bitboard BishopTable[64 * 512];
+static Bitboard* BishopAttacks[64];
+static const uint64_t BISHOP_MAGICS[64] = {
+ 9368648609924554880ULL, 9009475591934976ULL, 4504776450605056ULL,
+ 1130334595844096ULL, 1725202480235520ULL, 288516396277699584ULL,
+ 613618303369805920ULL, 10168455467108368ULL, 9046920051966080ULL,
+ 36031066926022914ULL, 1152925941509587232ULL, 9301886096196101ULL,
+ 290536121828773904ULL, 5260205533369993472ULL, 7512287909098426400ULL,
+ 153141218749450240ULL, 9241386469758076456ULL, 5352528174448640064ULL,
+ 2310346668982272096ULL, 1154049638051909890ULL, 282645627930625ULL,
+ 2306405976892514304ULL, 11534281888680707074ULL, 72339630111982113ULL,
+ 8149474640617539202ULL, 2459884588819024896ULL, 11675583734899409218ULL,
+ 1196543596102144ULL, 5774635144585216ULL, 145242600416216065ULL,
+ 2522607328671633440ULL, 145278609400071184ULL, 5101802674455216ULL,
+ 650979603259904ULL, 9511646410653040801ULL, 1153493285013424640ULL,
+ 18016048314974752ULL, 4688397299729694976ULL, 9226754220791842050ULL,
+ 4611969694574863363ULL, 145532532652773378ULL, 5265289125480634376ULL,
+ 288239448330604544ULL, 2395019802642432ULL, 14555704381721968898ULL,
+ 2324459974457168384ULL, 23652833739932677ULL, 282583111844497ULL,
+ 4629880776036450560ULL, 5188716322066279440ULL, 146367151686549765ULL,
+ 1153170821083299856ULL, 2315697107408912522ULL, 2342448293961403408ULL,
+ 2309255902098161920ULL, 469501395595331584ULL, 4615626809856761874ULL,
+ 576601773662552642ULL, 621501155230386208ULL, 13835058055890469376ULL,
+ 3748138521932726784ULL, 9223517207018883457ULL, 9237736128969216257ULL,
+ 1127068154855556ULL,
+};
+
+static Bitboard RookMasks[64];
+static uint64_t RookMagics[64];
+static int RookShifts[64];
+static Bitboard RookTable[64 * 4096];
+static Bitboard* RookAttacks[64];
+static const uint64_t ROOK_MAGICS[64] = {
+ 612498416294952992ULL, 2377936612260610304ULL, 36037730568766080ULL,
+ 72075188908654856ULL, 144119655536003584ULL, 5836666216720237568ULL,
+ 9403535813175676288ULL, 1765412295174865024ULL, 3476919663777054752ULL,
+ 288300746238222339ULL, 9288811671472386ULL, 146648600474026240ULL,
+ 3799946587537536ULL, 704237264700928ULL, 10133167915730964ULL,
+ 2305983769267405952ULL, 9223634270415749248ULL, 10344480540467205ULL,
+ 9376496898355021824ULL, 2323998695235782656ULL, 9241527722809755650ULL,
+ 189159985010188292ULL, 2310421375767019786ULL, 4647717014536733827ULL,
+ 5585659813035147264ULL, 1442911135872321664ULL, 140814801969667ULL,
+ 1188959108457300100ULL, 288815318485696640ULL, 758869733499076736ULL,
+ 234750139167147013ULL, 2305924931420225604ULL, 9403727128727390345ULL,
+ 9223970239903959360ULL, 309094713112139074ULL, 38290492990967808ULL,
+ 3461016597114651648ULL, 181289678366835712ULL, 4927518981226496513ULL,
+ 1155212901905072225ULL, 36099167912755202ULL, 9024792514543648ULL,
+ 4611826894462124048ULL, 291045264466247688ULL, 83880127713378308ULL,
+ 1688867174481936ULL, 563516973121544ULL, 9227888831703941123ULL,
+ 703691741225216ULL, 45203259517829248ULL, 693563138976596032ULL,
+ 4038638777286134272ULL, 865817582546978176ULL, 13835621555058516608ULL,
+ 11541041685463296ULL, 288511853443695360ULL, 283749161902275ULL,
+ 176489098445378ULL, 2306124759338845321ULL, 720584805193941061ULL,
+ 4977040710267061250ULL, 10097633331715778562ULL, 325666550235288577ULL,
+ 1100057149646ULL,
+};
+
+typedef struct {
+ Key psq[16][64];
+ Key enpassant[8];
+ Key castling[16];
+ Key side;
+} Zobrist;
+
+extern Zobrist zob;
+
+typedef struct {
+ Bitboard byTypeBB[7]; // [0]=all, [1-6]=PAWN,KNIGHT,BISHOP,ROOK,QUEEN,KING
+ Bitboard byColorBB[2];
+ uint8_t board[64];
+ uint8_t pieceCount[16];
+ ChessColor sideToMove;
+ uint8_t castlingRights;
+ uint8_t epSquare;
+ uint8_t rule50;
+ Key key;
+} Position;
+
+static inline Bitboard pieces(const Position* pos) {
+ return pos->byTypeBB[0];
+}
+
+static inline Bitboard pieces_p(const Position* pos, int p) {
+ return pos->byTypeBB[p];
+}
+
+static inline Bitboard pieces_c(const Position* pos, int c) {
+ return pos->byColorBB[c];
+}
+
+static inline Bitboard pieces_cp(const Position* pos, int c, int p) {
+ return pieces_p(pos, p) & pieces_c(pos, c);
+}
+
+static inline Piece piece_on(const Position* pos, Square s) {
+ return (Piece)pos->board[s];
+}
+
+typedef struct {
+ Move move;
+} ExtMove;
+
+typedef struct {
+ ExtMove moves[256];
+ int count;
+} MoveList;
+/*
+enum {
+ // Relational NNUE tokens
+ O_TOKEN_COUNT = 0,
+ O_TOKEN_DATA = 2,
+ // Meta data
+ O_SIDE = 130,
+ O_CASTLE = 132,
+ O_EP = 148,
+ O_PICK_PHASE = 213,
+ O_SELECTED_PIECE = 215,
+ O_VALID_PIECES = 279,
+ O_VALID_DESTS = 343,
+ O_VALID_PROMOS = 407,
+
+ O_SELF_CHECK = 439,
+ O_OPP_CHECK = 440,
+ O_RULE50 = 441,
+ O_REPETITION = 442,
+ O_PASS_VALID = 443,
+
+ OBS_SIZE = 444
+};
+*/
+/*enum {
+ O_SQUARES = 0,
+ O_VALID_PROMOS = 960,
+ O_CASTLE = 992,
+ O_EP = 993,
+ O_PICK_PHASE = 994,
+ O_RULE50 = 995,
+ O_REPETITION = 996,
+ O_PASS_VALID = 997,
+ OBS_SIZE = 998
+};
+*/
+
+/*
+Selfplay branch obs layout before embedding approach
+enum {
+ O_BOARD = 0,
+ O_SIDE = 768,
+ O_CASTLE = 770,
+ O_EP = 786,
+ O_PICK_PHASE = 851,
+ O_SELECTED_PIECE = 853,
+ O_VALID_PIECES = 917,
+ O_VALID_DESTS = 981,
+ O_VALID_PROMOS = 1045,
+ O_SELF_CHECK = 1077,
+ O_OPP_CHECK = 1078,
+ O_RULE50 = 1079,
+ O_REPETITION = 1080,
+ O_PASS_VALID = 1081,
+ OBS_SIZE = 1082
+};
+*/
+enum {
+ O_BOARD = 0,
+ O_SIDE = 64,
+ O_CASTLE = 65,
+ O_EP = 69,
+ O_RULE50 = 78,
+ O_REPETITION = 79,
+ O_SELF_CHECK = 80,
+ O_OPP_CHECK = 81,
+ O_PICK_PHASE = 82,
+ O_SELECTED_PIECE = 83,
+ O_VALID_FROM_COUNT = 84,
+ O_VALID_FROM = 85,
+ O_VALID_TO_COUNT = 101,
+ O_VALID_TO = 102,
+ O_VALID_PROMOS = 134,
+ O_PASS_VALID = 166,
+ OBS_SIZE = 167
+};
+
+#define CHESS_MAX_VALID_FROM 16
+#define CHESS_MAX_VALID_TO 32
+#define CHESS_NULL_SQ 64
+
+#define PASS_ACTION 96
+#define NUM_ACTIONS 97
+enum {
+ CHESS_MODE_RANDOM = 0,
+ CHESS_MODE_SELFPLAY = 1,
+ CHESS_MODE_HUMAN = 2,
+ CHESS_MODE_HUMAN_RANDOM = 3,
+ CHESS_MODE_MAIA = 4
+};
+
+#define CHESS_TAG_SELFPLAY 0
+#define CHESS_TAG_HISTORICAL 1
+// Multi-bank selfplay: env tags are 1..CHESS_MAX_BANKS, one tag value per
+// frozen bank. tag = 0 means pure selfplay env (no historical opponent).
+// Backward compat: with num_frozen_banks=1, tag=1 still means "play bank 0".
+#define CHESS_MAX_BANKS 8
+
+typedef struct {
+ float perf;
+ float score;
+ float draw_rate;
+ float timeout_rate;
+ float chess_moves;
+ float episode_length;
+ float episode_return;
+ float invalid_action_rate;
+ // Per-slot scores (selfplay only). In match: slot 0 = primary policy A,
+ // slot 1 = frozen policy B. In selfplay training both should average ~0.5.
+ float slot_0_score;
+ float slot_1_score;
+ // Per-bank historical tracking. hist_score_bank[b] sums primary's score
+ // (1.0 win / 0.5 draw / 0 loss) on historical envs tagged b+1; hist_n_bank
+ // counts those games. Python recovers per-bank winrate as score/n. The
+ // legacy aggregates hist_score / hist_n sum across all banks for backward
+ // compat with single-bank dashboards.
+ float hist_score;
+ float hist_n;
+ float hist_score_bank[CHESS_MAX_BANKS];
+ float hist_n_bank[CHESS_MAX_BANKS];
+ float n;
+ // Eval diagnostics (non-selfplay modes). Per-color score/games let Python
+ // compute per-color win rate and surface obs-flip / perspective bugs as
+ // lopsided splits. maia_failures counts how often maia_get_move returned
+ // MOVE_NONE and we fell back to a random legal move — non-zero means part
+ // of the eval is degraded.
+ float wins_as_white;
+ float wins_as_black;
+ float games_as_white;
+ float games_as_black;
+ float maia_failures;
+} Log;
+
+typedef struct {
+ int cell_size;
+ Font piece_font;
+ int use_unicode_pieces;
+} Client;
+
+typedef struct {
+ Piece captured;
+ uint8_t castlingRights;
+ uint8_t epSquare;
+ uint8_t rule50;
+ Key key;
+ uint8_t pliesFromNull;
+} UndoInfo;
+
+typedef struct {
+ Log log;
+ Client* client;
+ uint8_t* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ unsigned char* action_mask; // (97,) — NULL unless MY_ACTION_MASK is defined
+
+ // Per-slot pointers used by the env body. Identity perm = same addresses as
+ // base+stride; non-identity perm = where this slot actually lives in vec
+ // global buffers. Populated by my_setup_perm.
+ uint8_t* obs_ptr[2];
+ unsigned char* action_mask_ptr[2];
+ float* action_ptr[2];
+ float* reward_ptr[2];
+ float* terminal_ptr[2];
+
+ unsigned int rng;
+ int num_agents;
+
+ Position pos;
+ MoveList legal_moves;
+ int legal_dirty;
+ int game_result;
+ int tick;
+ int chess_moves;
+ int max_moves;
+ float reward_draw;
+ float episode_reward;
+ int render_fps;
+ int mode;
+
+ char starting_fen[128];
+ char** fen_curriculum;
+ float fen_curric_pct;
+ int num_fens;
+ int random_fen;
+
+ UndoInfo undo_stack[MAX_GAME_PLIES];
+ int undo_stack_ptr;
+ uint8_t repetition_matches;
+
+ int invalid_actions_this_episode;
+
+ int pick_phase[2];
+ Square selected_square[2];
+ MoveList valid_destinations[2];
+ Bitboard valid_from_mask[2];
+ Bitboard valid_to_mask[2];
+ Bitboard obs_selected_view_mask[2];
+ Bitboard obs_valid_from_view_mask[2];
+ Bitboard obs_valid_to_view_mask[2];
+ uint32_t obs_valid_promo_mask[2];
+ float reward_invalid_piece;
+ float reward_invalid_move;
+ float reward_repetition;
+
+ int enable_50_move_rule;
+ int enable_threefold_repetition;
+
+ int learner_color;
+ // Selfplay-pool tagging. tag = 0 selfplay, tag = 1 historical (slot 0 =
+ // primary, slot 1 = frozen). boundary_reached set on game-end so Python can
+ // detect when historical envs have all completed at least one game since
+ // the last swap arm.
+ int tag;
+ int boundary_reached;
+ // Selfplay only: slot_for_color[c] = which slot (0 or 1) plays color c.
+ // Default (slot 0 = WHITE, slot 1 = BLACK); randomized per env to remove
+ // white-bias when running matched policies in different slots.
+ int slot_for_color[2];
+ int human_color;
+ float white_score;
+ float black_score;
+ float learner_wins;
+ float learner_losses;
+ float learner_draws;
+ char last_result[32];
+
+ Move pgn_moves[MAX_GAME_PLIES];
+ int pgn_move_count;
+ int show_game_end_popup;
+
+ int log_pgn;
+ int log_pgn_choice_made;
+ char pgn_filename[128];
+ int pgn_game_number;
+
+ int white_captured[6];
+ int black_captured[6];
+ int render_paused;
+ int has_last_move_highlight;
+ Square last_move_from;
+ Square last_move_to;
+
+ // CHESS_MODE_MAIA: per-env lc0 subprocess pipes. Initialized lazily on the
+ // first opponent move. -1 / 0 means "not yet spawned".
+ int maia_pid;
+ int maia_stdin_fd;
+ int maia_stdout_fd;
+ // Maia commits its move in one UCI round-trip, but selfplay training shows
+ // the learner a 2-step opponent wait (pick + place phases). Splitting
+ // Maia's move into 2 c_steps (no-op + commit) keeps the learner's LSTM in
+ // the training distribution. 0 = next c_step is the no-op phase, 1 = the
+ // commit phase.
+ int maia_phase;
+} Chess;
+
+static inline Bitboard sq_bb(Square s) {
+ return SquareBB[s];
+}
+
+static inline int popcount(Bitboard b) {
+ return __builtin_popcountll(b);
+}
+
+static inline Square lsb(Bitboard b) {
+ assert(b);
+ return __builtin_ctzll(b);
+}
+
+static inline Square pop_lsb(Bitboard* b) {
+ Square s = lsb(*b);
+ *b &= *b - 1;
+ return s;
+}
+
+static inline Bitboard shift_bb(int Direction, Bitboard b) {
+ return Direction == NORTH ? b << 8
+ : Direction == SOUTH ? b >> 8
+ : Direction == EAST ? (b & ~FileHBB) << 1
+ : Direction == WEST ? (b & ~FileABB) >> 1
+ : Direction == NORTH_EAST ? (b & ~FileHBB) << 9
+ : Direction == SOUTH_EAST ? (b & ~FileHBB) >> 7
+ : Direction == NORTH_WEST ? (b & ~FileABB) << 7
+ : Direction == SOUTH_WEST ? (b & ~FileABB) >> 9
+ : 0;
+}
+
+static inline Bitboard pawn_attacks_bb(ChessColor c, Square s) {
+ return PawnAttacks[c][s];
+}
+
+static inline Bitboard knight_attacks_bb(Square s) {
+ return KnightAttacks[s];
+}
+
+static inline Bitboard king_attacks_bb(Square s) {
+ return KingAttacks[s];
+}
+
+
+static inline Bitboard rook_attacks_bb(Square s, Bitboard occupied) {
+ occupied &= RookMasks[s];
+ return RookAttacks[s][(occupied * RookMagics[s]) >> RookShifts[s]];
+}
+
+static inline Bitboard bishop_attacks_bb(Square s, Bitboard occupied) {
+ occupied &= BishopMasks[s];
+ return BishopAttacks[s][(occupied * BishopMagics[s]) >> BishopShifts[s]];
+}
+
+static inline Bitboard queen_attacks_bb(Square s, Bitboard occupied) {
+ return rook_attacks_bb(s, occupied) | bishop_attacks_bb(s, occupied);
+}
+
+
+Bitboard SquareBB[65];
+Bitboard PawnAttacks[2][64];
+Bitboard KnightAttacks[64];
+Bitboard KingAttacks[64];
+Bitboard BetweenBB[64][64];
+Bitboard LineBB[64][64];
+Zobrist zob;
+
+static bool bitboards_initialized = false;
+
+static Bitboard index_to_occupancy(int index, Bitboard mask) {
+ Bitboard occ = 0;
+ int bits = popcount(mask);
+ for (int i = 0; i < bits; i++) {
+ Square sq = lsb(mask);
+ mask &= mask - 1;
+ if (index & (1 << i)) occ |= sq_bb(sq);
+ }
+ return occ;
+}
+static Bitboard compute_bishop_mask(Square s) {
+ Bitboard mask = 0;
+ int r = rank_of(s), f = file_of(s);
+ for (int rr = r + 1, ff = f + 1; rr < 7 && ff < 7; rr++, ff++) mask |= sq_bb(make_square(ff, rr));
+ for (int rr = r - 1, ff = f + 1; rr > 0 && ff < 7; rr--, ff++) mask |= sq_bb(make_square(ff, rr));
+ for (int rr = r - 1, ff = f - 1; rr > 0 && ff > 0; rr--, ff--) mask |= sq_bb(make_square(ff, rr));
+ for (int rr = r + 1, ff = f - 1; rr < 7 && ff > 0; rr++, ff--) mask |= sq_bb(make_square(ff, rr));
+ return mask;
+}
+
+static void init_bishop_magics(void) {
+ Bitboard* table_ptr = BishopTable;
+
+ for (Square sq = 0; sq < 64; sq++) {
+ BishopMasks[sq] = compute_bishop_mask(sq);
+ BishopMagics[sq] = BISHOP_MAGICS[sq];
+
+ int bits = popcount(BishopMasks[sq]);
+ BishopShifts[sq] = 64 - bits;
+ BishopAttacks[sq] = table_ptr;
+
+ int num_entries = 1 << bits;
+ memset(table_ptr, 0, num_entries * sizeof(Bitboard));
+
+ for (int i = 0; i < num_entries; i++) {
+ Bitboard occ = index_to_occupancy(i, BishopMasks[sq]);
+
+ Bitboard attacks = 0;
+ int r = rank_of(sq), f = file_of(sq);
+ for (int rr = r + 1, ff = f + 1; rr < 8 && ff < 8; rr++, ff++) {
+ Square tsq = make_square(ff, rr);
+ attacks |= sq_bb(tsq);
+ if (occ & sq_bb(tsq)) break;
+ }
+ for (int rr = r - 1, ff = f + 1; rr >= 0 && ff < 8; rr--, ff++) {
+ Square tsq = make_square(ff, rr);
+ attacks |= sq_bb(tsq);
+ if (occ & sq_bb(tsq)) break;
+ }
+ for (int rr = r - 1, ff = f - 1; rr >= 0 && ff >= 0; rr--, ff--) {
+ Square tsq = make_square(ff, rr);
+ attacks |= sq_bb(tsq);
+ if (occ & sq_bb(tsq)) break;
+ }
+ for (int rr = r + 1, ff = f - 1; rr < 8 && ff >= 0; rr++, ff--) {
+ Square tsq = make_square(ff, rr);
+ attacks |= sq_bb(tsq);
+ if (occ & sq_bb(tsq)) break;
+ }
+
+ uint64_t idx = (occ * BishopMagics[sq]) >> BishopShifts[sq];
+ table_ptr[idx] = attacks;
+ }
+ table_ptr += num_entries;
+ }
+}
+
+static Bitboard compute_rook_mask(Square s) {
+ Bitboard mask = 0;
+ int r = rank_of(s), f = file_of(s);
+ for (int rr = r + 1; rr < 7; rr++) mask |= sq_bb(make_square(f, rr));
+ for (int rr = r - 1; rr > 0; rr--) mask |= sq_bb(make_square(f, rr));
+ for (int ff = f + 1; ff < 7; ff++) mask |= sq_bb(make_square(ff, r));
+ for (int ff = f - 1; ff > 0; ff--) mask |= sq_bb(make_square(ff, r));
+ return mask;
+}
+
+static void init_rook_magics(void) {
+ Bitboard* table_ptr = RookTable;
+
+ for (Square sq = 0; sq < 64; sq++) {
+ RookMasks[sq] = compute_rook_mask(sq);
+ RookMagics[sq] = ROOK_MAGICS[sq];
+
+ int bits = popcount(RookMasks[sq]);
+ RookShifts[sq] = 64 - bits;
+ RookAttacks[sq] = table_ptr;
+
+ int num_entries = 1 << bits;
+ memset(table_ptr, 0, num_entries * sizeof(Bitboard));
+
+ for (int i = 0; i < num_entries; i++) {
+ Bitboard occ = index_to_occupancy(i, RookMasks[sq]);
+
+ Bitboard attacks = 0;
+ int r = rank_of(sq), f = file_of(sq);
+ for (int rr = r + 1; rr < 8; rr++) {
+ Square tsq = make_square(f, rr);
+ attacks |= sq_bb(tsq);
+ if (occ & sq_bb(tsq)) break;
+ }
+ for (int rr = r - 1; rr >= 0; rr--) {
+ Square tsq = make_square(f, rr);
+ attacks |= sq_bb(tsq);
+ if (occ & sq_bb(tsq)) break;
+ }
+ for (int ff = f + 1; ff < 8; ff++) {
+ Square tsq = make_square(ff, r);
+ attacks |= sq_bb(tsq);
+ if (occ & sq_bb(tsq)) break;
+ }
+ for (int ff = f - 1; ff >= 0; ff--) {
+ Square tsq = make_square(ff, r);
+ attacks |= sq_bb(tsq);
+ if (occ & sq_bb(tsq)) break;
+ }
+
+ uint64_t idx = (occ * RookMagics[sq]) >> RookShifts[sq];
+ table_ptr[idx] = attacks;
+ }
+ table_ptr += num_entries;
+ }
+}
+
+void init_bitboards(void) {
+ if (bitboards_initialized) return;
+
+ for (int c = 0; c < 2; c++) {
+ for (int pt = PAWN; pt <= KING; pt++) {
+ for (int s = 0; s < 64; s++) {
+ zob.psq[make_piece(c, pt)][s] = prng_rand();
+ }
+ }
+ }
+ for (int f = 0; f < 8; f++) {
+ zob.enpassant[f] = prng_rand();
+ }
+ for (int cr = 0; cr < 16; cr++) {
+ zob.castling[cr] = prng_rand();
+ }
+ zob.side = prng_rand();
+
+ for (int i = 0; i < 64; i++) {
+ SquareBB[i] = 1ULL << i;
+ }
+ SquareBB[64] = 0;
+
+ for (int s = 0; s < 64; s++) {
+ Bitboard bb = sq_bb(s);
+ PawnAttacks[CHESS_WHITE][s] = shift_bb(NORTH_WEST, bb) | shift_bb(NORTH_EAST, bb);
+ PawnAttacks[CHESS_BLACK][s] = shift_bb(SOUTH_WEST, bb) | shift_bb(SOUTH_EAST, bb);
+ }
+
+ int knight_dirs[] = {-17, -15, -10, -6, 6, 10, 15, 17};
+ for (int s = 0; s < 64; s++) {
+ Bitboard attack = 0;
+ int file = file_of(s);
+ int rank = rank_of(s);
+
+ for (int i = 0; i < 8; i++) {
+ int to = s + knight_dirs[i];
+ if (to >= 0 && to < 64) {
+ int to_file = file_of(to);
+ int to_rank = rank_of(to);
+ if (abs(to_file - file) <= 2 && abs(to_rank - rank) <= 2) {
+ attack |= sq_bb(to);
+ }
+ }
+ }
+ KnightAttacks[s] = attack;
+ }
+
+ int king_dirs[] = {-9, -8, -7, -1, 1, 7, 8, 9};
+ for (int s = 0; s < 64; s++) {
+ Bitboard attack = 0;
+ int file = file_of(s);
+
+ for (int i = 0; i < 8; i++) {
+ int to = s + king_dirs[i];
+ if (to >= 0 && to < 64) {
+ int to_file = file_of(to);
+ if (abs(to_file - file) <= 1) {
+ attack |= sq_bb(to);
+ }
+ }
+ }
+ KingAttacks[s] = attack;
+ }
+
+ for (int s1 = 0; s1 < 64; s1++) {
+ for (int s2 = 0; s2 < 64; s2++) {
+ BetweenBB[s1][s2] = 0;
+ LineBB[s1][s2] = 0;
+
+ if (s1 == s2) continue;
+
+ int f1 = file_of(s1), r1 = rank_of(s1);
+ int f2 = file_of(s2), r2 = rank_of(s2);
+ int df = f2 - f1, dr = r2 - r1;
+
+ if (df == 0 || dr == 0 || abs(df) == abs(dr)) {
+ int step_f = df == 0 ? 0 : (df > 0 ? 1 : -1);
+ int step_r = dr == 0 ? 0 : (dr > 0 ? 1 : -1);
+
+ // BetweenBB: squares strictly between s1 and s2
+ int f = f1 + step_f;
+ int r = r1 + step_r;
+ while (f != f2 || r != r2) {
+ Square sq = make_square(f, r);
+ BetweenBB[s1][s2] |= sq_bb(sq);
+ f += step_f;
+ r += step_r;
+ }
+
+ f = f1;
+ r = r1;
+ while (f - step_f >= 0 && f - step_f < 8 && r - step_r >= 0 && r - step_r < 8) {
+ f -= step_f;
+ r -= step_r;
+ }
+ while (f >= 0 && f < 8 && r >= 0 && r < 8) {
+ LineBB[s1][s2] |= sq_bb(make_square(f, r));
+ f += step_f;
+ r += step_r;
+ }
+ }
+ }
+ }
+ init_bishop_magics();
+ init_rook_magics();
+ bitboards_initialized = true;
+}
+
+static void pos_set(Position* pos, const char* fen) {
+ memset(pos, 0, sizeof(Position));
+
+ int rank = 7, file = 0;
+ const char* ptr = fen;
+
+ while (*ptr && *ptr != ' ') {
+ char c = *ptr++;
+
+ if (c == '/') {
+ rank--;
+ file = 0;
+ } else if (c >= '1' && c <= '8') {
+ file += c - '0';
+ } else {
+ Square sq = make_square(file, rank);
+ Piece pc = NO_PIECE;
+ int pt = 0, color = 0;
+
+ switch (c) {
+ case 'P': pc = W_PAWN; pt = PAWN; color = CHESS_WHITE; break;
+ case 'N': pc = W_KNIGHT; pt = KNIGHT; color = CHESS_WHITE; break;
+ case 'B': pc = W_BISHOP; pt = BISHOP; color = CHESS_WHITE; break;
+ case 'R': pc = W_ROOK; pt = ROOK; color = CHESS_WHITE; break;
+ case 'Q': pc = W_QUEEN; pt = QUEEN; color = CHESS_WHITE; break;
+ case 'K': pc = W_KING; pt = KING; color = CHESS_WHITE; break;
+ case 'p': pc = B_PAWN; pt = PAWN; color = CHESS_BLACK; break;
+ case 'n': pc = B_KNIGHT; pt = KNIGHT; color = CHESS_BLACK; break;
+ case 'b': pc = B_BISHOP; pt = BISHOP; color = CHESS_BLACK; break;
+ case 'r': pc = B_ROOK; pt = ROOK; color = CHESS_BLACK; break;
+ case 'q': pc = B_QUEEN; pt = QUEEN; color = CHESS_BLACK; break;
+ case 'k': pc = B_KING; pt = KING; color = CHESS_BLACK; break;
+ }
+
+ if (pc != NO_PIECE) {
+ pos->board[sq] = pc;
+ pos->byTypeBB[pt] |= sq_bb(sq);
+ pos->byColorBB[color] |= sq_bb(sq);
+ pos->byTypeBB[0] |= sq_bb(sq);
+ pos->pieceCount[pc]++;
+ }
+ file++;
+ }
+ }
+
+ if (*ptr == ' ') ptr++;
+
+ pos->sideToMove = (*ptr == 'w') ? CHESS_WHITE : CHESS_BLACK;
+ ptr += 2;
+
+ pos->castlingRights = NO_CASTLING;
+ while (*ptr && *ptr != ' ') {
+ if (*ptr == 'K') pos->castlingRights |= WHITE_OO;
+ else if (*ptr == 'Q') pos->castlingRights |= WHITE_OOO;
+ else if (*ptr == 'k') pos->castlingRights |= BLACK_OO;
+ else if (*ptr == 'q') pos->castlingRights |= BLACK_OOO;
+ ptr++;
+ }
+
+
+ if (*ptr == ' ') ptr++;
+
+ pos->epSquare = SQ_NONE;
+ if (*ptr != '-') {
+ int ep_file = ptr[0] - 'a';
+ int ep_rank = ptr[1] - '1';
+ pos->epSquare = make_square(ep_file, ep_rank);
+ }
+
+ pos->key = 0;
+ for (Square sq = SQ_A1; sq <= SQ_H8; sq++) {
+ Piece pc = pos->board[sq];
+ if (pc != NO_PIECE) {
+ pos->key ^= zob.psq[pc][sq];
+ }
+ }
+ if (pos->sideToMove == CHESS_BLACK) {
+ pos->key ^= zob.side;
+ }
+ if (pos->castlingRights) {
+ pos->key ^= zob.castling[pos->castlingRights];
+ }
+ if (pos->epSquare != SQ_NONE) {
+ pos->key ^= zob.enpassant[file_of(pos->epSquare)];
+ }
+}
+
+static void do_move(Position* pos, Move m, UndoInfo* undo_stack, int* undo_stack_ptr) {
+ if (m == MOVE_NULL) {
+ undo_stack[*undo_stack_ptr].captured = NO_PIECE;
+ undo_stack[*undo_stack_ptr].castlingRights = pos->castlingRights;
+ undo_stack[*undo_stack_ptr].epSquare = pos->epSquare;
+ undo_stack[*undo_stack_ptr].rule50 = pos->rule50;
+ undo_stack[*undo_stack_ptr].key = pos->key;
+ undo_stack[*undo_stack_ptr].pliesFromNull = 0;
+ (*undo_stack_ptr)++;
+
+ if (pos->epSquare != SQ_NONE) {
+ pos->key ^= zob.enpassant[file_of(pos->epSquare)];
+ pos->epSquare = SQ_NONE;
+ }
+ pos->sideToMove = !pos->sideToMove;
+ pos->key ^= zob.side;
+ return;
+ }
+
+ Square from = from_sq(m);
+ Square to = to_sq(m);
+ int move_type = type_of_m(m);
+ Piece pc = piece_on(pos, from);
+ Piece captured = piece_on(pos, to);
+ int pt = type_of_p(pc);
+ ChessColor us = pos->sideToMove;
+ ChessColor them = !us;
+
+ undo_stack[*undo_stack_ptr].captured = captured;
+ undo_stack[*undo_stack_ptr].castlingRights = pos->castlingRights;
+ undo_stack[*undo_stack_ptr].epSquare = pos->epSquare;
+ undo_stack[*undo_stack_ptr].rule50 = pos->rule50;
+ undo_stack[*undo_stack_ptr].key = pos->key;
+ undo_stack[*undo_stack_ptr].pliesFromNull = (*undo_stack_ptr > 0) ? undo_stack[*undo_stack_ptr - 1].pliesFromNull + 1 : 0;
+ (*undo_stack_ptr)++;
+
+ if (pt == PAWN || captured != NO_PIECE) {
+ pos->rule50 = 0;
+ undo_stack[*undo_stack_ptr - 1].pliesFromNull = 0;
+ }
+ else {
+ pos->rule50++;
+ }
+
+ if (pos->epSquare != SQ_NONE) {
+ pos->key ^= zob.enpassant[file_of(pos->epSquare)];
+ }
+ pos->epSquare = SQ_NONE;
+
+ switch (move_type) {
+ case CASTLING: {
+ pos->key ^= zob.psq[pc][from];
+
+ pos->board[from] = NO_PIECE;
+ pos->board[to] = pc;
+ pos->byTypeBB[pt] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byColorBB[us] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byTypeBB[0] ^= sq_bb(from) ^ sq_bb(to);
+ pos->key ^= zob.psq[pc][to];
+
+ Square rook_from, rook_to;
+ if (to > from) {
+ rook_from = from + 3;
+ rook_to = from + 1;
+ } else {
+ rook_from = from - 4;
+ rook_to = from - 1;
+ }
+
+ Piece rook = piece_on(pos, rook_from);
+ pos->key ^= zob.psq[rook][rook_from];
+ pos->board[rook_from] = NO_PIECE;
+ pos->board[rook_to] = rook;
+ pos->byTypeBB[ROOK] ^= sq_bb(rook_from) ^ sq_bb(rook_to);
+ pos->byColorBB[us] ^= sq_bb(rook_from) ^ sq_bb(rook_to);
+ pos->byTypeBB[0] ^= sq_bb(rook_from) ^ sq_bb(rook_to);
+ pos->key ^= zob.psq[rook][rook_to];
+ break;
+ }
+ case ENPASSANT: {
+ pos->key ^= zob.psq[pc][from];
+
+ pos->board[from] = NO_PIECE;
+ pos->board[to] = pc;
+ pos->byTypeBB[pt] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byColorBB[us] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byTypeBB[0] ^= sq_bb(from) ^ sq_bb(to);
+ pos->key ^= zob.psq[pc][to];
+
+ Square cap_sq = to + (us == CHESS_WHITE ? SOUTH : NORTH);
+ Piece cap_pawn = piece_on(pos, cap_sq);
+ pos->key ^= zob.psq[cap_pawn][cap_sq];
+ pos->board[cap_sq] = NO_PIECE;
+ pos->byTypeBB[PAWN] ^= sq_bb(cap_sq);
+ pos->byColorBB[them] ^= sq_bb(cap_sq);
+ pos->byTypeBB[0] ^= sq_bb(cap_sq);
+ pos->pieceCount[cap_pawn]--;
+ break;
+ }
+ case NORMAL:
+ case PROMOTION: {
+ pos->key ^= zob.psq[pc][from];
+
+ if (captured != NO_PIECE) {
+ pos->key ^= zob.psq[captured][to];
+ int cap_pt = type_of_p(captured);
+ pos->byTypeBB[cap_pt] ^= sq_bb(to);
+ pos->byColorBB[them] ^= sq_bb(to);
+ pos->byTypeBB[0] ^= sq_bb(to);
+ pos->pieceCount[captured]--;
+ }
+
+ pos->board[from] = NO_PIECE;
+ pos->board[to] = pc;
+ pos->byTypeBB[pt] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byColorBB[us] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byTypeBB[0] ^= sq_bb(from) ^ sq_bb(to);
+ pos->key ^= zob.psq[pc][to];
+
+ if (move_type == PROMOTION) {
+ int promo_pt = promotion_type(m);
+ Piece promo_pc = make_piece(us, promo_pt);
+ pos->key ^= zob.psq[pc][to];
+ pos->board[to] = promo_pc;
+ pos->byTypeBB[pt] ^= sq_bb(to);
+ pos->byTypeBB[promo_pt] ^= sq_bb(to);
+ pos->pieceCount[pc]--;
+ pos->pieceCount[promo_pc]++;
+ pos->key ^= zob.psq[promo_pc][to];
+ }
+
+ if (pt == PAWN) {
+ int diff = to - from;
+ if (diff == 16 || diff == -16) {
+ Square ep_sq = (from + to) / 2;
+ if (pawn_attacks_bb(us, ep_sq) & pieces_cp(pos, them, PAWN)) {
+ pos->epSquare = ep_sq;
+ pos->key ^= zob.enpassant[file_of(ep_sq)];
+ }
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ uint8_t old_castling = pos->castlingRights;
+ if (pt == KING) {
+ pos->castlingRights &= us == CHESS_WHITE ? ~WHITE_CASTLING : ~BLACK_CASTLING;
+ }
+ if (from == SQ_A1 || to == SQ_A1) pos->castlingRights &= ~WHITE_OOO;
+ if (from == SQ_H1 || to == SQ_H1) pos->castlingRights &= ~WHITE_OO;
+ if (from == SQ_A8 || to == SQ_A8) pos->castlingRights &= ~BLACK_OOO;
+ if (from == SQ_H8 || to == SQ_H8) pos->castlingRights &= ~BLACK_OO;
+
+ if (old_castling != pos->castlingRights) {
+ pos->key ^= zob.castling[old_castling];
+ pos->key ^= zob.castling[pos->castlingRights];
+ }
+
+ pos->sideToMove = them;
+ pos->key ^= zob.side;
+}
+
+static void undo_move(Position* pos, Move m, UndoInfo* undo_stack, int* undo_stack_ptr) {
+ (*undo_stack_ptr)--;
+ UndoInfo* undo = &undo_stack[*undo_stack_ptr];
+
+ if (m == MOVE_NULL) {
+ pos->castlingRights = undo->castlingRights;
+ pos->epSquare = undo->epSquare;
+ pos->rule50 = undo->rule50;
+ pos->key = undo->key;
+ pos->sideToMove = !pos->sideToMove;
+ return;
+ }
+
+ Square from = from_sq(m);
+ Square to = to_sq(m);
+ int move_type = type_of_m(m);
+ ChessColor us = !pos->sideToMove;
+ ChessColor them = pos->sideToMove;
+
+ Piece pc = piece_on(pos, to);
+ int pt = type_of_p(pc);
+
+ pos->castlingRights = undo->castlingRights;
+ pos->epSquare = undo->epSquare;
+ pos->rule50 = undo->rule50;
+ pos->key = undo->key;
+ pos->sideToMove = us;
+
+ switch (move_type) {
+ case CASTLING: {
+ pos->board[to] = NO_PIECE;
+ pos->board[from] = pc;
+ pos->byTypeBB[pt] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byColorBB[us] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byTypeBB[0] ^= sq_bb(from) ^ sq_bb(to);
+
+ Square rook_from, rook_to;
+ if (to > from) {
+ rook_from = from + 3;
+ rook_to = from + 1;
+ } else {
+ rook_from = from - 4;
+ rook_to = from - 1;
+ }
+
+ Piece rook = piece_on(pos, rook_to);
+ pos->board[rook_to] = NO_PIECE;
+ pos->board[rook_from] = rook;
+ pos->byTypeBB[ROOK] ^= sq_bb(rook_from) ^ sq_bb(rook_to);
+ pos->byColorBB[us] ^= sq_bb(rook_from) ^ sq_bb(rook_to);
+ pos->byTypeBB[0] ^= sq_bb(rook_from) ^ sq_bb(rook_to);
+ break;
+ }
+ case ENPASSANT: {
+ pos->board[to] = NO_PIECE;
+ pos->board[from] = pc;
+ pos->byTypeBB[pt] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byColorBB[us] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byTypeBB[0] ^= sq_bb(from) ^ sq_bb(to);
+
+ Square cap_sq = to + (us == CHESS_WHITE ? SOUTH : NORTH);
+ Piece cap_pawn = make_piece(them, PAWN);
+ pos->board[cap_sq] = cap_pawn;
+ pos->byTypeBB[PAWN] ^= sq_bb(cap_sq);
+ pos->byColorBB[them] ^= sq_bb(cap_sq);
+ pos->byTypeBB[0] ^= sq_bb(cap_sq);
+ pos->pieceCount[cap_pawn]++;
+ break;
+ }
+ case NORMAL:
+ case PROMOTION: {
+ if (move_type == PROMOTION) {
+ int promo_pt = promotion_type(m);
+ Piece promo_pc = make_piece(us, promo_pt);
+ pc = make_piece(us, PAWN);
+ pt = PAWN;
+ pos->board[to] = NO_PIECE;
+ pos->byTypeBB[promo_pt] ^= sq_bb(to);
+ pos->byTypeBB[pt] ^= sq_bb(to);
+ pos->pieceCount[promo_pc]--;
+ pos->pieceCount[pc]++;
+ }
+
+ pos->board[to] = undo->captured;
+ pos->board[from] = pc;
+ pos->byTypeBB[pt] ^= sq_bb(from) ^ sq_bb(to);
+ pos->byColorBB[us] ^= sq_bb(from) ^ sq_bb(to);
+
+ if (undo->captured != NO_PIECE) {
+ int cap_pt = type_of_p(undo->captured);
+ pos->byTypeBB[cap_pt] ^= sq_bb(to);
+ pos->byColorBB[them] ^= sq_bb(to);
+ pos->byTypeBB[0] ^= sq_bb(from);
+ pos->pieceCount[undo->captured]++;
+ } else {
+ pos->byTypeBB[0] ^= sq_bb(from) ^ sq_bb(to);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static inline void add_move(MoveList* ml, Move m) {
+ ml->moves[ml->count].move = m;
+ ml->count++;
+}
+
+static void generate_pawn_moves(Position* pos, MoveList* ml, ChessColor us) {
+ ChessColor them = !us;
+ int up = (us == CHESS_WHITE) ? NORTH : SOUTH;
+ Bitboard rank7 = (us == CHESS_WHITE) ? Rank7BB : Rank2BB;
+ Bitboard rank3 = (us == CHESS_WHITE) ? Rank3BB : Rank6BB;
+
+ Bitboard pawns = pieces_cp(pos, us, PAWN);
+ Bitboard pawnsOn7 = pawns & rank7;
+ Bitboard pawnsNotOn7 = pawns & ~rank7;
+
+ Bitboard enemies = pieces_c(pos, them);
+ Bitboard empty = ~pieces(pos);
+
+ Bitboard b1 = shift_bb(up, pawnsNotOn7) & empty;
+ Bitboard b2 = shift_bb(up, b1 & rank3) & empty;
+
+ while (b1) {
+ Square to = pop_lsb(&b1);
+ add_move(ml, make_move(to - up, to));
+ }
+
+ while (b2) {
+ Square to = pop_lsb(&b2);
+ add_move(ml, make_move(to - up - up, to));
+ }
+
+ if (pawnsOn7) {
+ Bitboard b3 = shift_bb(up, pawnsOn7) & empty;
+ while (b3) {
+ Square to = pop_lsb(&b3);
+ Square from = to - up;
+ add_move(ml, make_promotion(from, to, QUEEN));
+ add_move(ml, make_promotion(from, to, ROOK));
+ add_move(ml, make_promotion(from, to, BISHOP));
+ add_move(ml, make_promotion(from, to, KNIGHT));
+ }
+ }
+
+ Bitboard b4 = shift_bb(up + WEST, pawnsNotOn7) & enemies;
+ Bitboard b5 = shift_bb(up + EAST, pawnsNotOn7) & enemies;
+
+ while (b4) {
+ Square to = pop_lsb(&b4);
+ add_move(ml, make_move(to - up - WEST, to));
+ }
+
+ while (b5) {
+ Square to = pop_lsb(&b5);
+ add_move(ml, make_move(to - up - EAST, to));
+ }
+
+ if (pawnsOn7) {
+ Bitboard b6 = shift_bb(up + WEST, pawnsOn7) & enemies;
+ Bitboard b7 = shift_bb(up + EAST, pawnsOn7) & enemies;
+
+ while (b6) {
+ Square to = pop_lsb(&b6);
+ Square from = to - up - WEST;
+ add_move(ml, make_promotion(from, to, QUEEN));
+ add_move(ml, make_promotion(from, to, ROOK));
+ add_move(ml, make_promotion(from, to, BISHOP));
+ add_move(ml, make_promotion(from, to, KNIGHT));
+ }
+
+ while (b7) {
+ Square to = pop_lsb(&b7);
+ Square from = to - up - EAST;
+ add_move(ml, make_promotion(from, to, QUEEN));
+ add_move(ml, make_promotion(from, to, ROOK));
+ add_move(ml, make_promotion(from, to, BISHOP));
+ add_move(ml, make_promotion(from, to, KNIGHT));
+ }
+ }
+
+ if (pos->epSquare != SQ_NONE) {
+ Bitboard ep_pawns = pawnsNotOn7 & pawn_attacks_bb(them, pos->epSquare);
+ while (ep_pawns) {
+ Square from = pop_lsb(&ep_pawns);
+ add_move(ml, make_enpassant(from, pos->epSquare));
+ }
+ }
+}
+
+static void generate_castling(Position* pos, MoveList* ml, ChessColor us) {
+ Bitboard occupied = pieces(pos);
+
+ if (us == CHESS_WHITE) {
+ if (pos->castlingRights & WHITE_OO) {
+ if (!(occupied & (sq_bb(SQ_F1) | sq_bb(SQ_G1)))) {
+ add_move(ml, make_castling(SQ_E1, SQ_G1));
+ }
+ }
+ if (pos->castlingRights & WHITE_OOO) {
+ if (!(occupied & (sq_bb(SQ_D1) | sq_bb(SQ_C1) | sq_bb(SQ_B1)))) {
+ add_move(ml, make_castling(SQ_E1, SQ_C1));
+ }
+ }
+ } else {
+ if (pos->castlingRights & BLACK_OO) {
+ if (!(occupied & (sq_bb(SQ_F8) | sq_bb(SQ_G8)))) {
+ add_move(ml, make_castling(SQ_E8, SQ_G8));
+ }
+ }
+ if (pos->castlingRights & BLACK_OOO) {
+ if (!(occupied & (sq_bb(SQ_D8) | sq_bb(SQ_C8) | sq_bb(SQ_B8)))) {
+ add_move(ml, make_castling(SQ_E8, SQ_C8));
+ }
+ }
+ }
+}
+
+static Bitboard attackers_to_sq(Position* pos, Square sq, Bitboard occupied) {
+ return (pawn_attacks_bb(CHESS_WHITE, sq) & pieces_cp(pos, CHESS_BLACK, PAWN) & occupied)
+ | (pawn_attacks_bb(CHESS_BLACK, sq) & pieces_cp(pos, CHESS_WHITE, PAWN) & occupied)
+ | (knight_attacks_bb(sq) & pieces_p(pos, KNIGHT) & occupied)
+ | (king_attacks_bb(sq) & pieces_p(pos, KING) & occupied)
+ | (bishop_attacks_bb(sq, occupied) & (pieces_p(pos, BISHOP) | pieces_p(pos, QUEEN)))
+ | (rook_attacks_bb(sq, occupied) & (pieces_p(pos, ROOK) | pieces_p(pos, QUEEN)));
+}
+
+static bool is_check(Position* pos, ChessColor c) {
+ Bitboard king_bb = pieces_cp(pos, c, KING);
+ if (!king_bb) return false;
+ Square king_sq = lsb(king_bb);
+ return (attackers_to_sq(pos, king_sq, pieces(pos)) & pieces_c(pos, !c)) != 0;
+}
+
+static Bitboard compute_pinned(Position* pos, ChessColor c) {
+ Bitboard pinned = 0;
+ Bitboard our_pieces = pieces_c(pos, c);
+ Bitboard king_bb = pieces_cp(pos, c, KING);
+ if (!king_bb) return 0;
+
+ Square ksq = lsb(king_bb);
+ ChessColor them = !c;
+ Bitboard occupied = pieces(pos);
+
+ Bitboard diag_pinners = (pieces_cp(pos, them, BISHOP) | pieces_cp(pos, them, QUEEN))
+ & bishop_attacks_bb(ksq, 0);
+
+ while (diag_pinners) {
+ Square pinner_sq = pop_lsb(&diag_pinners);
+ Bitboard between = BetweenBB[ksq][pinner_sq] & occupied;
+ if (popcount(between) == 1) {
+ pinned |= between & our_pieces;
+ }
+ }
+
+ Bitboard rook_pinners = (pieces_cp(pos, them, ROOK) | pieces_cp(pos, them, QUEEN))
+ & rook_attacks_bb(ksq, 0);
+
+ while (rook_pinners) {
+ Square pinner_sq = pop_lsb(&rook_pinners);
+ Bitboard between = BetweenBB[ksq][pinner_sq] & occupied;
+ if (popcount(between) == 1) {
+ pinned |= between & our_pieces;
+ }
+ }
+
+ return pinned;
+}
+
+static inline bool is_legal_move_fast(Position* pos, Move m, Bitboard pinned, Square ksq, ChessColor us) {
+ Square from = from_sq(m);
+ Square to = to_sq(m);
+ int mt = type_of_m(m);
+
+ if (from == ksq) {
+ if (mt == CASTLING) {
+ ChessColor them = !us;
+ if (is_check(pos, us)) return false;
+ Square mid = (from + to) / 2;
+ Bitboard occ = pieces(pos) ^ sq_bb(from);
+ if (attackers_to_sq(pos, mid, occ) & pieces_c(pos, them)) return false;
+ if (attackers_to_sq(pos, to, occ) & pieces_c(pos, them)) return false;
+ return true;
+ }
+ Bitboard occ = pieces(pos) ^ sq_bb(from);
+ return !(attackers_to_sq(pos, to, occ) & pieces_c(pos, !us));
+ }
+
+ if (mt == ENPASSANT) {
+ Bitboard occ = pieces(pos) ^ sq_bb(from) ^ sq_bb(to);
+ Square capsq = to + (us == CHESS_WHITE ? -8 : 8);
+ occ ^= sq_bb(capsq);
+ return !(attackers_to_sq(pos, ksq, occ) & pieces_c(pos, !us));
+ }
+
+ if (!(pinned & sq_bb(from))) {
+ return true;
+ }
+
+ return LineBB[ksq][from] & sq_bb(to);
+}
+
+static inline bool is_legal_move(Position* pos, Move m) {
+ ChessColor us = pos->sideToMove;
+ ChessColor them = (ChessColor)!us;
+ int mt = type_of_m(m);
+ if (mt == CASTLING) {
+ if (is_check(pos, us)) return false;
+ Square from = from_sq(m), to = to_sq(m);
+ Square mid = (from + to) / 2;
+ Bitboard occ = pieces(pos);
+ if ((attackers_to_sq(pos, mid, occ) & pieces_c(pos, them))
+ || (attackers_to_sq(pos, to, occ) & pieces_c(pos, them))) return false;
+ return true;
+ }
+ if (mt == ENPASSANT) {
+ Bitboard king_bb = pieces_cp(pos, us, KING);
+ if (!king_bb) return false;
+ Square ksq = lsb(king_bb);
+ Square from = from_sq(m), to = to_sq(m);
+ Square capsq = (us == CHESS_WHITE) ? (to - 8) : (to + 8);
+ Bitboard occ = pieces(pos) ^ sq_bb(from) ^ sq_bb(capsq) ^ sq_bb(to);
+ return (attackers_to_sq(pos, ksq, occ) & pieces_c(pos, them)) == 0;
+ }
+ UndoInfo u[1]; int p = 0;
+ do_move(pos, m, u, &p);
+ bool ok = !is_check(pos, us);
+ undo_move(pos, m, u, &p);
+ return ok;
+}
+
+static inline void generate_pseudo_legal(Position* pos, MoveList* ml, ChessColor us) {
+ ml->count = 0;
+ generate_pawn_moves(pos, ml, us);
+
+ Bitboard occupied = pieces(pos);
+ Bitboard target = ~pieces_c(pos, us);
+ Bitboard bb = pieces_cp(pos, us, KNIGHT);
+ while (bb) {
+ Square from = pop_lsb(&bb);
+ Bitboard attacks = knight_attacks_bb(from) & target;
+ while (attacks) {
+ Square to = pop_lsb(&attacks);
+ add_move(ml, make_move(from, to));
+ }
+ }
+
+ bb = pieces_cp(pos, us, BISHOP);
+ while (bb) {
+ Square from = pop_lsb(&bb);
+ Bitboard attacks = bishop_attacks_bb(from, occupied) & target;
+ while (attacks) {
+ Square to = pop_lsb(&attacks);
+ add_move(ml, make_move(from, to));
+ }
+ }
+
+ bb = pieces_cp(pos, us, ROOK);
+ while (bb) {
+ Square from = pop_lsb(&bb);
+ Bitboard attacks = rook_attacks_bb(from, occupied) & target;
+ while (attacks) {
+ Square to = pop_lsb(&attacks);
+ add_move(ml, make_move(from, to));
+ }
+ }
+
+ bb = pieces_cp(pos, us, QUEEN);
+ while (bb) {
+ Square from = pop_lsb(&bb);
+ Bitboard attacks = queen_attacks_bb(from, occupied) & target;
+ while (attacks) {
+ Square to = pop_lsb(&attacks);
+ add_move(ml, make_move(from, to));
+ }
+ }
+
+ bb = pieces_cp(pos, us, KING);
+ while (bb) {
+ Square from = pop_lsb(&bb);
+ Bitboard attacks = king_attacks_bb(from) & target;
+ while (attacks) {
+ Square to = pop_lsb(&attacks);
+ add_move(ml, make_move(from, to));
+ }
+ }
+
+ generate_castling(pos, ml, us);
+}
+
+static void generate_legal(Position* pos, MoveList* ml, UndoInfo* undo_stack, int* undo_stack_ptr) {
+ generate_pseudo_legal(pos, ml, pos->sideToMove);
+ ChessColor us = pos->sideToMove;
+ ChessColor them = (ChessColor)!us;
+ Bitboard king_bb = pieces_cp(pos, us, KING);
+ Square ksq = king_bb ? lsb(king_bb) : SQ_NONE;
+ Bitboard pinned = compute_pinned(pos, us);
+ bool in_check = is_check(pos, us);
+ int check_count = 0;
+ Bitboard evasion_mask = 0;
+ if (in_check) {
+ Bitboard checkers = attackers_to_sq(pos, ksq, pieces(pos)) & pieces_c(pos, them);
+ check_count = popcount(checkers);
+ if (check_count == 1) {
+ Square checker_sq = lsb(checkers);
+ evasion_mask = sq_bb(checker_sq);
+ Piece checker = piece_on(pos, checker_sq);
+ if (checker != NO_PIECE) {
+ int checker_type = type_of_p(checker);
+ if (checker_type == BISHOP || checker_type == ROOK || checker_type == QUEEN) {
+ evasion_mask |= BetweenBB[ksq][checker_sq];
+ }
+ }
+ }
+ }
+
+ int write = 0;
+ for (int i = 0; i < ml->count; i++) {
+ Move m = ml->moves[i].move;
+ Square from = from_sq(m);
+ bool legal;
+ if (!in_check) {
+ legal = is_legal_move_fast(pos, m, pinned, ksq, us);
+ } else if (from == ksq) {
+ legal = is_legal_move_fast(pos, m, pinned, ksq, us);
+ } else if (check_count > 1) {
+ legal = false;
+ } else if (type_of_m(m) == ENPASSANT) {
+ legal = is_legal_move(pos, m);
+ } else if ((sq_bb(to_sq(m)) & evasion_mask) == 0) {
+ legal = false;
+ } else {
+ legal = is_legal_move_fast(pos, m, pinned, ksq, us);
+ }
+ if (legal) {
+ ml->moves[write++] = ml->moves[i];
+ }
+ }
+ ml->count = write;
+}
+
+static inline bool is_insufficient_material(const Position* pos) {
+ if (pieces_p(pos, PAWN) | pieces_p(pos, ROOK) | pieces_p(pos, QUEEN))
+ return false;
+
+ int wN = popcount(pieces_cp(pos, CHESS_WHITE, KNIGHT));
+ int bN = popcount(pieces_cp(pos, CHESS_BLACK, KNIGHT));
+ int wB = popcount(pieces_cp(pos, CHESS_WHITE, BISHOP));
+ int bB = popcount(pieces_cp(pos, CHESS_BLACK, BISHOP));
+ int totalMinors = wN + bN + wB + bB;
+
+ if (totalMinors == 0)
+ return true;
+
+ if (totalMinors == 1)
+ return true;
+
+ if (totalMinors == 2) {
+ if ((wN == 2 && wB == 0 && bN == 0 && bB == 0) || (bN == 2 && bB == 0 && wN == 0 && wB == 0))
+ return true;
+ if ((wN + wB) == 1 && (bN + bB) == 1)
+ return true;
+ }
+
+ return false;
+}
+
+static void clear_player_selection(Chess* env, int side) {
+ env->pick_phase[side] = 0;
+ env->selected_square[side] = SQ_NONE;
+ env->valid_destinations[side].count = 0;
+ env->valid_to_mask[side] = 0;
+}
+
+static void rebuild_legal_state(Chess* env) {
+ generate_legal(&env->pos, &env->legal_moves, env->undo_stack, &env->undo_stack_ptr);
+ env->legal_dirty = 0;
+ env->valid_from_mask[0] = 0;
+ env->valid_from_mask[1] = 0;
+ env->valid_to_mask[0] = 0;
+ env->valid_to_mask[1] = 0;
+ int side = (int)env->pos.sideToMove;
+ Bitboard from_mask = 0;
+ for (int i = 0; i < env->legal_moves.count; i++) {
+ from_mask |= sq_bb(from_sq(env->legal_moves.moves[i].move));
+ }
+ env->valid_from_mask[side] = from_mask;
+}
+void populate_observations(Chess* env) {
+ Position* pos = &env->pos;
+
+ int num_players = env->mode == CHESS_MODE_SELFPLAY ? 2 : 1;
+ for (int player_iter = 0; player_iter < num_players; player_iter++) {
+ int player = env->mode == CHESS_MODE_SELFPLAY ? player_iter : env->learner_color;
+ // Selfplay: slot ↔ color mapping is randomized per env (slot_for_color).
+ // Single-agent modes: only the learner has a slot (idx 0).
+ int buffer_idx = (env->mode == CHESS_MODE_SELFPLAY) ? env->slot_for_color[player] : 0;
+ uint8_t* player_obs = env->obs_ptr[buffer_idx];
+ memset(player_obs, 0, OBS_SIZE);
+ uint8_t* board_planes = player_obs + O_BOARD;
+
+ // Selfplay: each iteration writes into its own per-slot mask.
+ // Single-agent modes: only the learner iter writes into the (single) mask.
+ unsigned char* my_mask = NULL;
+ bool fill_mask = false;
+ if (env->action_mask != NULL) {
+ if (env->mode == CHESS_MODE_SELFPLAY) {
+ my_mask = env->action_mask_ptr[buffer_idx];
+ fill_mask = true;
+ } else if (player == env->learner_color) {
+ my_mask = env->action_mask_ptr[0];
+ fill_mask = true;
+ }
+ }
+ if (fill_mask) {
+ memset(my_mask, 0, NUM_ACTIONS * sizeof(unsigned char));
+ }
+
+ ChessColor us = (ChessColor)player; // 0=White, 1=Black
+ ChessColor them = (ChessColor)!us;
+
+ int flip = player * 56;
+
+
+ // Compact ego-centric board: one byte per square. 0 = empty,
+ // own P..K = 1..6, enemy P..K = 7..12.
+ for (int pt = PAWN; pt <= KING; pt++) {
+ Bitboard bb = pieces_cp(pos, player, pt);
+ while (bb) {
+ Square sq = pop_lsb(&bb);
+ board_planes[sq ^ flip] = (uint8_t)pt;
+ }
+ }
+
+ for (int pt = PAWN; pt <= KING; pt++) {
+ Bitboard bb = pieces_cp(pos, them, pt);
+ while (bb) {
+ Square sq = pop_lsb(&bb);
+ board_planes[sq ^ flip] = (uint8_t)(6 + pt);
+ }
+ }
+
+ ChessColor side_to_move = pos->sideToMove;
+
+ player_obs[O_SIDE] = (pos->sideToMove == us) ? 1 : 0;
+
+ uint8_t castle_rights = pos->castlingRights;
+ if (player == 1) {
+ uint8_t flipped = 0;
+ if (castle_rights & BLACK_OO) flipped |= WHITE_OO;
+ if (castle_rights & BLACK_OOO) flipped |= WHITE_OOO;
+ if (castle_rights & WHITE_OO) flipped |= BLACK_OO;
+ if (castle_rights & WHITE_OOO) flipped |= BLACK_OOO;
+ castle_rights = flipped;
+ }
+ player_obs[O_CASTLE + 0] = (castle_rights & WHITE_OO) ? 1 : 0;
+ player_obs[O_CASTLE + 1] = (castle_rights & WHITE_OOO) ? 1 : 0;
+ player_obs[O_CASTLE + 2] = (castle_rights & BLACK_OO) ? 1 : 0;
+ player_obs[O_CASTLE + 3] = (castle_rights & BLACK_OOO) ? 1 : 0;
+
+ if (pos->epSquare < 64) {
+ int ep_sq = (player == 1) ? (pos->epSquare ^ 56) : pos->epSquare;
+ player_obs[O_EP + file_of((Square)ep_sq)] = 1;
+ } else {
+ player_obs[O_EP + 8] = 1;
+ }
+
+ uint8_t* valid_from_indices = player_obs + O_VALID_FROM;
+ uint8_t* valid_to_indices = player_obs + O_VALID_TO;
+ for (int k = 0; k < CHESS_MAX_VALID_FROM; k++) valid_from_indices[k] = CHESS_NULL_SQ;
+ for (int k = 0; k < CHESS_MAX_VALID_TO; k++) valid_to_indices[k] = CHESS_NULL_SQ;
+ int valid_from_count = 0;
+ int valid_to_count = 0;
+
+ int player_idx = (int)us;
+
+ if (side_to_move == us) {
+ if (env->pick_phase[player_idx] == 0) {
+ Bitboard added = 0;
+ for (int i = 0; i < env->legal_moves.count; i++) {
+ Square from = from_sq(env->legal_moves.moves[i].move);
+ int view_from = (player == 1) ? (from ^ 56) : from;
+ Bitboard bit = sq_bb((Square)view_from);
+ if (!(added & bit)) {
+ added |= bit;
+ if (valid_from_count < CHESS_MAX_VALID_FROM) {
+ valid_from_indices[valid_from_count++] = (uint8_t)view_from;
+ }
+ if (fill_mask) my_mask[view_from] = 1;
+ }
+ }
+ } else {
+ Bitboard added = 0;
+ for (int i = 0; i < env->valid_destinations[player_idx].count; i++) {
+ Square to = to_sq(env->valid_destinations[player_idx].moves[i].move);
+ int view_to = (player == 1) ? (to ^ 56) : to;
+ Bitboard bit = sq_bb((Square)view_to);
+ if (!(added & bit)) {
+ added |= bit;
+ if (valid_to_count < CHESS_MAX_VALID_TO) {
+ valid_to_indices[valid_to_count++] = (uint8_t)view_to;
+ }
+ if (fill_mask) my_mask[view_to] = 1;
+ }
+ }
+ }
+ }
+ player_obs[O_VALID_FROM_COUNT] = (uint8_t)valid_from_count;
+ player_obs[O_VALID_TO_COUNT] = (uint8_t)valid_to_count;
+ player_obs[O_PASS_VALID] = (side_to_move != us) ? 255 : 0;
+ if (fill_mask && side_to_move != us) {
+ my_mask[PASS_ACTION] = 1;
+ }
+
+ player_obs[O_PICK_PHASE] = env->pick_phase[player_idx] ? 1 : 0;
+
+ uint8_t selected_byte = (uint8_t)CHESS_NULL_SQ;
+ if (env->pick_phase[player_idx] == 1 && env->selected_square[player_idx] != SQ_NONE) {
+ int view_selected = (player == 1)
+ ? (env->selected_square[player_idx] ^ 56)
+ : env->selected_square[player_idx];
+ selected_byte = (uint8_t)view_selected;
+ }
+ player_obs[O_SELECTED_PIECE] = selected_byte;
+
+ uint8_t* valid_promos = player_obs + O_VALID_PROMOS;
+
+ if (env->pick_phase[player_idx] == 1 && env->valid_destinations[player_idx].count > 0) {
+ for (int i = 0; i < env->valid_destinations[player_idx].count; i++) {
+ Move m = env->valid_destinations[player_idx].moves[i].move;
+ if (type_of_m(m) == PROMOTION) {
+ int type_idx = QUEEN - promotion_type(m);
+ int file_idx = file_of(to_sq(m));
+ valid_promos[type_idx * 8 + file_idx] = 1;
+ if (fill_mask) my_mask[64 + type_idx * 8 + file_idx] = 1;
+ }
+ }
+ }
+
+ player_obs[O_SELF_CHECK] = is_check(pos, us) ? 255 : 0;
+ player_obs[O_OPP_CHECK] = is_check(pos, them) ? 255 : 0;
+
+ int rule50 = pos->rule50;
+ if (rule50 > 100) rule50 = 100;
+ player_obs[O_RULE50] = (uint8_t)((rule50 * 255) / 100);
+
+ uint8_t rep_val = 0;
+ if (env->undo_stack_ptr >= 4) {
+ uint8_t plies = env->undo_stack[env->undo_stack_ptr - 1].pliesFromNull;
+ if (plies >= 4) {
+ int repetitions = 0;
+ for (int i = 4; i <= plies; i += 2) {
+ int idx = env->undo_stack_ptr - i;
+ if (idx >= 0 && env->undo_stack[idx].key == pos->key) {
+ repetitions++;
+ }
+ }
+ if (repetitions >= 2) {
+ rep_val = 255;
+ } else if (repetitions == 1) {
+ rep_val = 128;
+ }
+ }
+ }
+ player_obs[O_REPETITION] = rep_val;
+ }
+}
+
+static int move_to_san(Position* pos, Move m, char* buf, UndoInfo* undo_stack, int* undo_stack_ptr) {
+ const char files[] = "abcdefgh";
+ const char ranks[] = "12345678";
+ const char piece_chars[] = ".PNBRQK";
+ char* ptr = buf;
+
+ Square from = from_sq(m);
+ Square to = to_sq(m);
+ int move_type = type_of_m(m);
+ Piece pc = piece_on(pos, from);
+ int pt = type_of_p(pc);
+ ChessColor us = pos->sideToMove;
+
+ if (move_type == CASTLING) {
+ if (to > from) {
+ strcpy(ptr, "O-O");
+ ptr += 3;
+ } else {
+ strcpy(ptr, "O-O-O");
+ ptr += 5;
+ }
+ } else {
+ if (pt != PAWN) {
+ *ptr++ = piece_chars[pt];
+
+ Bitboard same_pieces = pieces_cp(pos, us, pt) & ~sq_bb(from);
+ Bitboard attackers = 0;
+
+ if (pt == KNIGHT) {
+ attackers = knight_attacks_bb(to) & same_pieces;
+ } else if (pt == BISHOP) {
+ attackers = bishop_attacks_bb(to, pieces(pos)) & same_pieces;
+ } else if (pt == ROOK) {
+ attackers = rook_attacks_bb(to, pieces(pos)) & same_pieces;
+ } else if (pt == QUEEN) {
+ attackers = (bishop_attacks_bb(to, pieces(pos)) | rook_attacks_bb(to, pieces(pos))) & same_pieces;
+ } else if (pt == KING) {
+ attackers = king_attacks_bb(to) & same_pieces;
+ }
+
+ Bitboard legal_attackers = 0;
+ while (attackers) {
+ Square attacker_sq = pop_lsb(&attackers);
+ Move test_move = make_move(attacker_sq, to);
+ if (is_legal_move(pos, test_move)) {
+ legal_attackers |= sq_bb(attacker_sq);
+ }
+ }
+
+ if (legal_attackers) {
+ int same_file = 0, same_rank = 0;
+ Bitboard temp = legal_attackers;
+ while (temp) {
+ Square s = pop_lsb(&temp);
+ if (file_of(s) == file_of(from)) same_file++;
+ if (rank_of(s) == rank_of(from)) same_rank++;
+ }
+
+ if (same_file == 0) {
+ *ptr++ = files[file_of(from)];
+ } else if (same_rank == 0) {
+ *ptr++ = ranks[rank_of(from)];
+ } else {
+ *ptr++ = files[file_of(from)];
+ *ptr++ = ranks[rank_of(from)];
+ }
+ }
+ }
+
+ Piece captured = piece_on(pos, to);
+ bool is_capture = (captured != NO_PIECE) || (move_type == ENPASSANT);
+
+ if (is_capture) {
+ if (pt == PAWN) {
+ *ptr++ = files[file_of(from)];
+ }
+ *ptr++ = 'x';
+ }
+
+ *ptr++ = files[file_of(to)];
+ *ptr++ = ranks[rank_of(to)];
+
+ if (move_type == PROMOTION) {
+ *ptr++ = '=';
+ const char promo_pieces[] = "..NBRQ";
+ *ptr++ = promo_pieces[promotion_type(m)];
+ }
+ }
+
+ do_move(pos, m, undo_stack, undo_stack_ptr);
+
+ ChessColor them = pos->sideToMove;
+ if (is_check(pos, them)) {
+ MoveList ml;
+ generate_legal(pos, &ml, undo_stack, undo_stack_ptr);
+ if (ml.count == 0) {
+ *ptr++ = '#';
+ } else {
+ *ptr++ = '+';
+ }
+ }
+
+ undo_move(pos, m, undo_stack, undo_stack_ptr);
+
+ *ptr = '\0';
+ return ptr - buf;
+}
+
+static void export_pgn_append(Chess* env, const char* filename, int append) {
+ FILE* f = fopen(filename, append ? "a" : "w");
+ if (!f) return;
+
+ if (env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM) {
+ const char* opponent_name = env->mode == CHESS_MODE_HUMAN ? "AI" : "Random";
+ const char* event_name = env->mode == CHESS_MODE_HUMAN ? "Human vs AI" : "Human vs Random";
+ fprintf(f, "[Event \"%s\"]\n", event_name);
+ fprintf(f, "[White \"%s\"]\n", env->human_color == CHESS_WHITE ? "Human" : opponent_name);
+ fprintf(f, "[Black \"%s\"]\n", env->human_color == CHESS_BLACK ? "Human" : opponent_name);
+ } else {
+ fprintf(f, "[Event \"Selfplay Eval Game %d\"]\n", env->pgn_game_number);
+ fprintf(f, "[White \"%s\"]\n", env->learner_color == CHESS_BLACK ? "Learner" : "Opponent");
+ fprintf(f, "[Black \"%s\"]\n", env->learner_color == CHESS_BLACK ? "Opponent" : "Learner");
+ }
+ fprintf(f, "[Site \"PufferLib\"]\n");
+ fprintf(f, "[Result \"%s\"]\n\n", env->last_result);
+
+ Position replay_pos;
+ pos_set(&replay_pos, env->starting_fen);
+
+ UndoInfo replay_undo[MAX_GAME_PLIES];
+ int replay_undo_ptr = 0;
+
+ char san_buf[16];
+
+ for (int i = 0; i < env->pgn_move_count; i++) {
+ if (i % 2 == 0) {
+ fprintf(f, "%d. ", i/2 + 1);
+ }
+
+ Move m = env->pgn_moves[i];
+ move_to_san(&replay_pos, m, san_buf, replay_undo, &replay_undo_ptr);
+ fprintf(f, "%s ", san_buf);
+
+ do_move(&replay_pos, m, replay_undo, &replay_undo_ptr);
+
+ if ((i + 1) % 8 == 0) fprintf(f, "\n");
+ }
+
+ if (strcmp(env->last_result, "White Wins") == 0) {
+ fprintf(f, "1-0");
+ } else if (strcmp(env->last_result, "Black Wins") == 0) {
+ fprintf(f, "0-1");
+ } else {
+ fprintf(f, "1/2-1/2");
+ }
+
+ fprintf(f, "\n\n");
+ fclose(f);
+}
+
+static void generate_random_fen(Chess* env, char* fen_out) {
+ char board[64];
+ memset(board, '.', 64);
+
+ int wk_sq, bk_sq;
+ do {
+ wk_sq = rand_r(&env->rng) % 64;
+ bk_sq = rand_r(&env->rng) % 64;
+ int wk_rank = wk_sq / 8, wk_file = wk_sq % 8;
+ int bk_rank = bk_sq / 8, bk_file = bk_sq % 8;
+ int rank_diff = abs(wk_rank - bk_rank);
+ int file_diff = abs(wk_file - bk_file);
+ if (wk_sq != bk_sq && (rank_diff > 1 || file_diff > 1)) break;
+ } while (1);
+
+ board[wk_sq] = 'K';
+ board[bk_sq] = 'k';
+
+ const char* white_pieces = "QRRNNBBPP";
+ const char* black_pieces = "qrrnnbbpp";
+ int num_white = rand_r(&env->rng) % 16;
+ int num_black = rand_r(&env->rng) % 16;
+
+ for (int i = 0; i < num_white; i++) {
+ int sq, rank;
+ char piece;
+ do {
+ sq = rand_r(&env->rng) % 64;
+ rank = sq / 8;
+ piece = white_pieces[rand_r(&env->rng) % 9];
+ } while (board[sq] != '.' || (piece == 'P' && (rank == 0 || rank == 7)));
+ board[sq] = piece;
+ }
+
+ for (int i = 0; i < num_black; i++) {
+ int sq, rank;
+ char piece;
+ do {
+ sq = rand_r(&env->rng) % 64;
+ rank = sq / 8;
+ piece = black_pieces[rand_r(&env->rng) % 9];
+ } while (board[sq] != '.' || (piece == 'p' && (rank == 0 || rank == 7)));
+ board[sq] = piece;
+ }
+
+ char* ptr = fen_out;
+ for (int rank = 7; rank >= 0; rank--) {
+ int empty = 0;
+ for (int file = 0; file < 8; file++) {
+ char piece = board[rank * 8 + file];
+ if (piece == '.') {
+ empty++;
+ } else {
+ if (empty > 0) {
+ *ptr++ = '0' + empty;
+ empty = 0;
+ }
+ *ptr++ = piece;
+ }
+ }
+ if (empty > 0) *ptr++ = '0' + empty;
+ if (rank > 0) *ptr++ = '/';
+ }
+ strcpy(ptr, " w - - 0 1");
+}
+
+static inline int apply_move_to_env(Chess* env, Move chosen, int* is_timeout) {
+ env->chess_moves++;
+ env->last_move_from = from_sq(chosen);
+ env->last_move_to = to_sq(chosen);
+ env->has_last_move_highlight = 1;
+
+ if ((env->mode == CHESS_MODE_HUMAN
+ || env->mode == CHESS_MODE_HUMAN_RANDOM
+ || env->log_pgn) && env->pgn_move_count < MAX_GAME_PLIES) {
+ env->pgn_moves[env->pgn_move_count++] = chosen;
+ }
+
+ ChessColor side_before = env->pos.sideToMove;
+ do_move(&env->pos, chosen, env->undo_stack, &env->undo_stack_ptr);
+ env->legal_dirty = 1;
+ clear_player_selection(env, (int)env->pos.sideToMove);
+
+ if (env->undo_stack_ptr > 0) {
+ Piece cap = env->undo_stack[env->undo_stack_ptr - 1].captured;
+ if (cap != NO_PIECE) {
+ int pt = type_of_p(cap) - 1;
+ if (pt >= 0 && pt < 6) {
+ if (color_of(cap) == CHESS_WHITE) env->white_captured[pt]++;
+ else env->black_captured[pt]++;
+ }
+ } else if ((int)type_of_m(chosen) == ENPASSANT) {
+ Piece cap_pawn = (side_before == CHESS_WHITE) ? B_PAWN : W_PAWN;
+ int pt = type_of_p(cap_pawn) - 1;
+ if (pt >= 0 && pt < 6) {
+ if (color_of(cap_pawn) == CHESS_WHITE) env->white_captured[pt]++;
+ else env->black_captured[pt]++;
+ }
+ }
+ if (env->undo_stack[env->undo_stack_ptr - 1].pliesFromNull > 99) {
+ env->undo_stack[env->undo_stack_ptr - 1].pliesFromNull = 99;
+ }
+ }
+
+ env->repetition_matches = 0;
+ if (env->undo_stack_ptr >= 4) {
+ int max_back = env->undo_stack[env->undo_stack_ptr - 1].pliesFromNull;
+ if (max_back > env->undo_stack_ptr) max_back = env->undo_stack_ptr;
+ for (int i = 4; i <= max_back; i += 2) {
+ if (env->undo_stack[env->undo_stack_ptr - i].key != env->pos.key) continue;
+ env->repetition_matches++;
+ if (env->repetition_matches == 2) break;
+ }
+ }
+
+ rebuild_legal_state(env);
+
+ int game_result = 0;
+ *is_timeout = 0;
+ if (env->chess_moves >= env->max_moves || env->undo_stack_ptr >= MAX_GAME_PLIES - 2) {
+ *is_timeout = 1;
+ game_result = 3;
+ } else if (env->legal_moves.count == 0) {
+ if (is_check(&env->pos, env->pos.sideToMove)) {
+ game_result = env->pos.sideToMove == CHESS_WHITE ? 1 : 2;
+ } else {
+ game_result = 3;
+ }
+ } else if (is_insufficient_material(&env->pos)) {
+ game_result = 3;
+ } else if (env->enable_50_move_rule && env->pos.rule50 >= 100) {
+ game_result = 3;
+ } else if (env->enable_threefold_repetition && env->repetition_matches >= 2) {
+ game_result = 3;
+ }
+
+ return game_result;
+}
+
+// ---- CHESS_MODE_MAIA: external lc0/Maia UCI engine ----
+// Each env owns one lc0 child process. Communication is line-based UCI:
+// parent → child: "position fen \ngo nodes \n"
+// child → parent: ... "info ..." ... "bestmove \n"
+// Configured via env vars: MAIA_LC0_PATH, MAIA_WEIGHTS_PATH, MAIA_NODES,
+// MAIA_BACKEND. MAIA_NODES=1 ≈ weakest Maia setting; raise for stronger play.
+
+#include
+#include
+#include
+#include
+#include
+
+static void position_to_fen(const Position* pos, char* out) {
+ char* p = out;
+ // Indexed by Piece enum: W_* are 1..6, B_* are 9..14. Gap at 7..8.
+ static const char pchars[16] = ".PNBRQK??pnbrqk?";
+ for (int rank = 7; rank >= 0; rank--) {
+ int empty = 0;
+ for (int file = 0; file < 8; file++) {
+ Piece pc = piece_on(pos, make_square(file, rank));
+ if (pc == NO_PIECE) {
+ empty++;
+ } else {
+ if (empty > 0) { *p++ = '0' + empty; empty = 0; }
+ *p++ = pchars[pc];
+ }
+ }
+ if (empty > 0) *p++ = '0' + empty;
+ if (rank > 0) *p++ = '/';
+ }
+ *p++ = ' ';
+ *p++ = (pos->sideToMove == CHESS_WHITE) ? 'w' : 'b';
+ *p++ = ' ';
+ int wrote_castle = 0;
+ if (pos->castlingRights & 1) { *p++ = 'K'; wrote_castle = 1; }
+ if (pos->castlingRights & 2) { *p++ = 'Q'; wrote_castle = 1; }
+ if (pos->castlingRights & 4) { *p++ = 'k'; wrote_castle = 1; }
+ if (pos->castlingRights & 8) { *p++ = 'q'; wrote_castle = 1; }
+ if (!wrote_castle) *p++ = '-';
+ *p++ = ' ';
+ if (pos->epSquare != SQ_NONE && (int)pos->epSquare < 64) {
+ *p++ = 'a' + (int)file_of(pos->epSquare);
+ *p++ = '1' + (int)rank_of(pos->epSquare);
+ } else {
+ *p++ = '-';
+ }
+ *p++ = ' ';
+ // Fullmove number isn't tracked on Position; hardcode 1. Halfmove (rule50)
+ // matters for the 50-move rule and is passed through.
+ p += sprintf(p, "%d 1", pos->rule50);
+ *p = '\0';
+}
+
+// Parse a UCI move like "e2e4" or "g7g8q" against the env's current legal list.
+// Returns MOVE_NONE if not found.
+static Move uci_to_move(const char* uci, const MoveList* legal) {
+ if (uci[0] < 'a' || uci[0] > 'h' || uci[2] < 'a' || uci[2] > 'h') return MOVE_NONE;
+ int from_file = uci[0] - 'a';
+ int from_rank = uci[1] - '1';
+ int to_file = uci[2] - 'a';
+ int to_rank = uci[3] - '1';
+ if (from_rank < 0 || from_rank > 7 || to_rank < 0 || to_rank > 7) return MOVE_NONE;
+ Square from = make_square(from_file, from_rank);
+ Square to = make_square(to_file, to_rank);
+ int promo_pt = -1;
+ if (uci[4] == 'q') promo_pt = QUEEN;
+ else if (uci[4] == 'r') promo_pt = ROOK;
+ else if (uci[4] == 'b') promo_pt = BISHOP;
+ else if (uci[4] == 'n') promo_pt = KNIGHT;
+ for (int i = 0; i < legal->count; i++) {
+ Move m = legal->moves[i].move;
+ if (from_sq(m) != from || to_sq(m) != to) continue;
+ if (promo_pt >= 0) {
+ if (type_of_m(m) != PROMOTION) continue;
+ if ((int)promotion_type(m) != promo_pt) continue;
+ }
+ return m;
+ }
+ return MOVE_NONE;
+}
+
+static int maia_write_all(int fd, const char* buf, int len) {
+ int n = 0;
+ while (n < len) {
+ int w = (int)write(fd, buf + n, (size_t)(len - n));
+ if (w < 0) {
+ if (errno == EINTR) continue;
+ return -1;
+ }
+ n += w;
+ }
+ return 0;
+}
+
+// Read one line (up to '\n' or buf-1 chars). Returns # bytes (excluding NUL) or
+// -1 on error / EOF. Blocks until a line is available.
+static int maia_read_line(int fd, char* buf, int bufsz) {
+ int n = 0;
+ while (n < bufsz - 1) {
+ char c;
+ int r = (int)read(fd, &c, 1);
+ if (r == 0) return -1; // EOF
+ if (r < 0) {
+ if (errno == EINTR) continue;
+ return -1;
+ }
+ if (c == '\n') break;
+ buf[n++] = c;
+ }
+ buf[n] = '\0';
+ return n;
+}
+
+static void maia_close(Chess* env);
+
+static void maia_init(Chess* env) {
+ if (env->maia_pid > 0) return; // already spawned
+
+ const char* lc0_path = getenv("MAIA_LC0_PATH");
+ const char* weights_path = getenv("MAIA_WEIGHTS_PATH");
+ const char* backend_arg = getenv("MAIA_BACKEND");
+ if (lc0_path == NULL) lc0_path = "./lc0";
+ if (weights_path == NULL) weights_path = "lc0/maia-1100.pb.gz";
+
+ int in_pipe[2], out_pipe[2];
+ if (pipe(in_pipe) < 0 || pipe(out_pipe) < 0) {
+ fprintf(stderr, "maia_init: pipe() failed\n");
+ env->maia_pid = -1;
+ return;
+ }
+ pid_t pid = fork();
+ if (pid < 0) {
+ close(in_pipe[0]); close(in_pipe[1]);
+ close(out_pipe[0]); close(out_pipe[1]);
+ fprintf(stderr, "maia_init: fork() failed\n");
+ env->maia_pid = -1;
+ return;
+ }
+ if (pid == 0) {
+ // Child: stdin←in_pipe[0], stdout→out_pipe[1].
+ dup2(in_pipe[0], STDIN_FILENO);
+ dup2(out_pipe[1], STDOUT_FILENO);
+ // Redirect stderr to /dev/null so info logs don't spam the parent.
+ int devnull = open("/dev/null", O_WRONLY);
+ if (devnull >= 0) { dup2(devnull, STDERR_FILENO); close(devnull); }
+ close(in_pipe[0]); close(in_pipe[1]);
+ close(out_pipe[0]); close(out_pipe[1]);
+ char weights_arg[512];
+ snprintf(weights_arg, sizeof(weights_arg), "--weights=%s", weights_path);
+ if (backend_arg) {
+ char backend_buf[128];
+ snprintf(backend_buf, sizeof(backend_buf), "--backend=%s", backend_arg);
+ execlp(lc0_path, "lc0", weights_arg, backend_buf, (char*)NULL);
+ } else {
+ execlp(lc0_path, "lc0", weights_arg, (char*)NULL);
+ }
+ _exit(127);
+ }
+ // Parent.
+ close(in_pipe[0]); // we write to in_pipe[1]
+ close(out_pipe[1]); // we read from out_pipe[0]
+ env->maia_pid = (int)pid;
+ env->maia_stdin_fd = in_pipe[1];
+ env->maia_stdout_fd = out_pipe[0];
+
+ // UCI handshake: send "uci", drain until "uciok"; then "isready", drain
+ // until "readyok". Engine init also loads weights so this can take a few
+ // seconds the first time.
+ char line[1024];
+ if (maia_write_all(env->maia_stdin_fd, "uci\n", 4) < 0) { maia_close(env); return; }
+ while (maia_read_line(env->maia_stdout_fd, line, sizeof(line)) >= 0) {
+ if (strncmp(line, "uciok", 5) == 0) break;
+ }
+ if (maia_write_all(env->maia_stdin_fd, "isready\n", 8) < 0) { maia_close(env); return; }
+ while (maia_read_line(env->maia_stdout_fd, line, sizeof(line)) >= 0) {
+ if (strncmp(line, "readyok", 7) == 0) break;
+ }
+}
+
+static void maia_close(Chess* env) {
+ if (env->maia_pid > 0) {
+ if (env->maia_stdin_fd >= 0) {
+ (void)maia_write_all(env->maia_stdin_fd, "quit\n", 5);
+ close(env->maia_stdin_fd);
+ env->maia_stdin_fd = -1;
+ }
+ if (env->maia_stdout_fd >= 0) {
+ close(env->maia_stdout_fd);
+ env->maia_stdout_fd = -1;
+ }
+ int status;
+ if (waitpid((pid_t)env->maia_pid, &status, WNOHANG) == 0) {
+ kill((pid_t)env->maia_pid, SIGTERM);
+ waitpid((pid_t)env->maia_pid, &status, 0);
+ }
+ }
+ env->maia_pid = 0;
+}
+
+// Ask Maia for the best move at the current env position. Returns a Move that
+// is guaranteed to be in env->legal_moves (or MOVE_NONE on engine failure).
+static Move maia_get_move(Chess* env) {
+ if (env->maia_pid <= 0) {
+ maia_init(env);
+ if (env->maia_pid <= 0) return MOVE_NONE;
+ }
+
+ int nodes = 1;
+ const char* nodes_str = getenv("MAIA_NODES");
+ if (nodes_str) nodes = atoi(nodes_str);
+ if (nodes < 1) nodes = 1;
+
+ char fen[128];
+ position_to_fen(&env->pos, fen);
+ char cmd[256];
+ int n = snprintf(cmd, sizeof(cmd), "position fen %s\ngo nodes %d\n", fen, nodes);
+ if (maia_write_all(env->maia_stdin_fd, cmd, n) < 0) {
+ maia_close(env);
+ return MOVE_NONE;
+ }
+
+ char line[1024];
+ while (1) {
+ int len = maia_read_line(env->maia_stdout_fd, line, sizeof(line));
+ if (len < 0) { maia_close(env); return MOVE_NONE; }
+ if (strncmp(line, "bestmove ", 9) != 0) continue;
+ char uci[8] = {0};
+ int j = 9, k = 0;
+ while (line[j] && line[j] != ' ' && line[j] != '\n' && line[j] != '\r' && k < 7) {
+ uci[k++] = line[j++];
+ }
+ return uci_to_move(uci, &env->legal_moves);
+ }
+}
+
+void c_reset(Chess* env) {
+ env->tick = 0;
+ env->chess_moves = 0;
+ env->game_result = 0;
+ env->undo_stack_ptr = 0;
+ env->repetition_matches = 0;
+ env->invalid_actions_this_episode = 0;
+ env->episode_reward = 0.0f;
+ env->pgn_move_count = 0;
+ env->show_game_end_popup = 0;
+ env->has_last_move_highlight = 0;
+ clear_player_selection(env, 0);
+ clear_player_selection(env, 1);
+ env->valid_from_mask[0] = 0;
+ env->valid_from_mask[1] = 0;
+
+ memset(env->white_captured, 0, sizeof(env->white_captured));
+ memset(env->black_captured, 0, sizeof(env->black_captured));
+
+ if (env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM) {
+ env->human_color = -1;
+ } else if (env->mode != CHESS_MODE_SELFPLAY) {
+ env->learner_color = 1 - env->learner_color;
+ }
+ env->maia_phase = 0;
+
+ if (env->fen_curriculum != NULL && env->num_fens > 0) {
+ float randvalue = (float)rand_r(&env->rng) / (float)(RAND_MAX);
+ if(env->fen_curric_pct >= randvalue){
+ int idx = rand_r(&env->rng) % env->num_fens;
+ pos_set(&env->pos, env->fen_curriculum[idx]);
+ }
+ else {
+ pos_set(&env->pos, env->starting_fen);
+ }
+
+ } else if (env->random_fen) {
+ char fen_buf[128];
+ generate_random_fen(env, fen_buf);
+ pos_set(&env->pos, fen_buf);
+ } else {
+ pos_set(&env->pos, env->starting_fen);
+ }
+
+ rebuild_legal_state(env);
+ populate_observations(env);
+
+}
+
+void c_step(Chess* env) {
+ if (env->render_paused && env->client != NULL) {
+ return;
+ }
+ if ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->human_color == -1) {
+ return;
+ }
+
+ if (env->mode == CHESS_MODE_SELFPLAY && !env->log_pgn_choice_made) {
+ if (env->client != NULL) {
+ return;
+ }
+ env->log_pgn = 0;
+ env->log_pgn_choice_made = 1;
+ }
+
+ if ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->show_game_end_popup) {
+ *env->reward_ptr[0] = 0.0f;
+ *env->terminal_ptr[0] = 0;
+ return;
+ }
+
+ if (env->legal_dirty) {
+ rebuild_legal_state(env);
+ }
+
+ env->tick++;
+ int move_completed = 0;
+ ChessColor mover = env->pos.sideToMove;
+ int mover_idx = (int)mover;
+ int game_result = 0;
+ int is_timeout = 0;
+
+ if ((env->mode == CHESS_MODE_RANDOM && env->pos.sideToMove != env->learner_color)
+ || (env->mode == CHESS_MODE_HUMAN_RANDOM && env->pos.sideToMove != env->human_color)) {
+ if (env->legal_moves.count > 0) {
+ int idx = rand_r(&env->rng) % env->legal_moves.count;
+ clear_player_selection(env, mover_idx);
+ game_result = apply_move_to_env(env, env->legal_moves.moves[idx].move, &is_timeout);
+ move_completed = 1;
+ }
+ } else if (env->mode == CHESS_MODE_MAIA && env->pos.sideToMove != env->learner_color) {
+ if (env->legal_moves.count > 0) {
+ if (env->maia_phase == 0) {
+ // First c_step of Maia's "move": no-op so the learner's LSTM
+ // sees a 2-step opponent wait (matches selfplay's pick+place
+ // cadence the policy was trained on).
+ env->maia_phase = 1;
+ move_completed = 1;
+ } else {
+ Move maia_mv = maia_get_move(env);
+ if (maia_mv == MOVE_NONE) {
+ // Engine failure / unparseable bestmove: fall back to random
+ // so the trial completes. Logged via log.maia_failures so
+ // Python can flag a degraded eval.
+ env->log.maia_failures += 1.0f;
+ int idx = rand_r(&env->rng) % env->legal_moves.count;
+ maia_mv = env->legal_moves.moves[idx].move;
+ }
+ clear_player_selection(env, mover_idx);
+ game_result = apply_move_to_env(env, maia_mv, &is_timeout);
+ env->maia_phase = 0;
+ move_completed = 1;
+ }
+ }
+ } else {
+ // Selfplay: side-to-move's action lives in whichever slot plays that color.
+ int action = (env->mode == CHESS_MODE_SELFPLAY)
+ ? *env->action_ptr[env->slot_for_color[mover_idx]]
+ : *env->action_ptr[0];
+ if ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->pos.sideToMove == env->human_color) {
+ action = -1;
+ *env->action_ptr[0] = -1;
+ }
+
+ mover = env->pos.sideToMove;
+ mover_idx = (int)mover;
+
+ // In selfplay both players train, so charge whichever slot moved.
+ // In single-agent modes, only the learner's slot exists.
+ int penalty_slot = (env->mode == CHESS_MODE_SELFPLAY) ? env->slot_for_color[mover_idx]
+ : (mover == env->learner_color) ? 0 : -1;
+
+ if (env->legal_moves.count == 0) {
+ clear_player_selection(env, mover_idx);
+ } else if (action < 0 || action >= PASS_ACTION) {
+ if (penalty_slot >= 0) {
+ *env->reward_ptr[penalty_slot] += (env->pick_phase[mover_idx] == 0)
+ ? env->reward_invalid_piece : env->reward_invalid_move;
+ env->invalid_actions_this_episode++;
+ }
+ if (env->pick_phase[mover_idx] == 1) {
+ clear_player_selection(env, mover_idx);
+ }
+ } else {
+ bool is_promo = (action >= 64 && action < 96);
+
+ if (env->pick_phase[mover_idx] == 0) {
+ clear_player_selection(env, mover_idx);
+
+ bool valid_pick = !is_promo;
+ Square picked_sq = SQ_NONE;
+ if (valid_pick) {
+ picked_sq = (mover == CHESS_BLACK) ? (Square)(action ^ 56) : (Square)action;
+ Piece pc = piece_on(&env->pos, picked_sq);
+ valid_pick = (pc != NO_PIECE && color_of(pc) == mover);
+ }
+
+ if (valid_pick) {
+ MoveList* dests = &env->valid_destinations[mover_idx];
+ dests->count = 0;
+ Bitboard to_mask = 0;
+ for (int i = 0; i < env->legal_moves.count; i++) {
+ if (from_sq(env->legal_moves.moves[i].move) == picked_sq) {
+ dests->moves[dests->count++] = env->legal_moves.moves[i];
+ to_mask |= sq_bb(to_sq(env->legal_moves.moves[i].move));
+ }
+ }
+
+ if (dests->count > 0) {
+ env->selected_square[mover_idx] = picked_sq;
+ env->pick_phase[mover_idx] = 1;
+ env->valid_to_mask[mover_idx] = to_mask;
+ } else {
+ valid_pick = false;
+ clear_player_selection(env, mover_idx);
+ }
+ }
+
+ if (!valid_pick && penalty_slot >= 0) {
+ *env->reward_ptr[penalty_slot] += env->reward_invalid_piece;
+ env->invalid_actions_this_episode++;
+ }
+ } else {
+ if (env->selected_square[mover_idx] == SQ_NONE || env->valid_destinations[mover_idx].count == 0) {
+ fprintf(stderr, "c_step: pick_phase=1 but selected_square=%u, valid_destinations.count=%d (mover=%d)\n",
+ env->selected_square[mover_idx], env->valid_destinations[mover_idx].count, mover_idx);
+ exit(1);
+ }
+
+ Square target_sq = SQ_NONE;
+ Move chosen_move = MOVE_NONE;
+ int desired_promo = -1;
+ int desired_file = -1;
+
+ if (is_promo) {
+ int promo_row = (action - 64) / 8;
+ desired_file = (action - 64) % 8;
+ desired_promo = QUEEN - promo_row;
+ } else {
+ target_sq = (mover == CHESS_BLACK) ? (Square)(action ^ 56) : (Square)action;
+ }
+
+ for (int i = 0; i < env->valid_destinations[mover_idx].count; i++) {
+ Move m = env->valid_destinations[mover_idx].moves[i].move;
+ if (!is_promo) {
+ if ((int)to_sq(m) == (int)target_sq) {
+ chosen_move = m;
+ break;
+ }
+ } else {
+ if ((int)type_of_m(m) == PROMOTION
+ && (int)promotion_type(m) == desired_promo
+ && (int)file_of(to_sq(m)) == desired_file) {
+ chosen_move = m;
+ break;
+ }
+ }
+ }
+
+ if (chosen_move == MOVE_NONE) {
+ if (penalty_slot >= 0) {
+ *env->reward_ptr[penalty_slot] += env->reward_invalid_move;
+ env->invalid_actions_this_episode++;
+ }
+ clear_player_selection(env, mover_idx);
+ } else {
+ game_result = apply_move_to_env(env, chosen_move, &is_timeout);
+ if (env->reward_repetition != 0.0f
+ && penalty_slot >= 0
+ && env->repetition_matches >= 1) {
+ *env->reward_ptr[penalty_slot] += env->reward_repetition;
+ }
+ move_completed = 1;
+ }
+ }
+ }
+ }
+
+ if (!move_completed) {
+ if (env->chess_moves >= env->max_moves || env->undo_stack_ptr >= MAX_GAME_PLIES - 2) {
+ game_result = 3;
+ is_timeout = 1;
+ } else {
+ if (env->legal_moves.count == 0) {
+ if (is_check(&env->pos, env->pos.sideToMove)) {
+ game_result = env->pos.sideToMove == CHESS_WHITE ? 1 : 2;
+ } else {
+ game_result = 3;
+ }
+ } else if (is_insufficient_material(&env->pos)) {
+ game_result = 3;
+ } else if (env->enable_50_move_rule && env->pos.rule50 >= 100) {
+ game_result = 3;
+ } else if (env->enable_threefold_repetition && env->repetition_matches >= 2) {
+ game_result = 3;
+ }
+ }
+ }
+
+ if (game_result != 0) {
+ *env->terminal_ptr[0] = 1;
+ if (env->mode == CHESS_MODE_SELFPLAY) {
+ *env->terminal_ptr[1] = 1;
+ }
+ env->game_result = game_result;
+ float win_value = 0.0f;
+
+ switch (game_result) {
+ case 3:
+ *env->reward_ptr[0] = env->reward_draw;
+ if (env->mode == CHESS_MODE_SELFPLAY) {
+ *env->reward_ptr[1] = env->reward_draw;
+ env->log.slot_0_score += 0.5f;
+ env->log.slot_1_score += 0.5f;
+ }
+ win_value = 0.5f;
+ env->log.draw_rate += 1.0f;
+ if (is_timeout) {
+ env->log.timeout_rate += 1.0f;
+ }
+ env->white_score += 0.5f;
+ env->black_score += 0.5f;
+ env->learner_draws += 1.0f;
+ strcpy(env->last_result, "Draw");
+ break;
+ case 1:
+ env->black_score += 1.0f;
+ if (env->mode == CHESS_MODE_SELFPLAY) {
+ *env->reward_ptr[env->slot_for_color[CHESS_WHITE]] = -1.0f;
+ *env->reward_ptr[env->slot_for_color[CHESS_BLACK]] = 1.0f;
+ if (env->slot_for_color[CHESS_BLACK] == 0) env->log.slot_0_score += 1.0f;
+ else env->log.slot_1_score += 1.0f;
+ win_value = 0.5f; // zero-sum: averaged across both slots
+ } else if (env->learner_color == CHESS_WHITE) {
+ *env->reward_ptr[0] = -1.0f;
+ env->learner_losses += 1.0f;
+ } else {
+ *env->reward_ptr[0] = 1.0f;
+ win_value = 1.0f;
+ env->learner_wins += 1.0f;
+ }
+ strcpy(env->last_result, "Black Wins");
+ break;
+ case 2:
+ env->white_score += 1.0f;
+ if (env->mode == CHESS_MODE_SELFPLAY) {
+ *env->reward_ptr[env->slot_for_color[CHESS_WHITE]] = 1.0f;
+ *env->reward_ptr[env->slot_for_color[CHESS_BLACK]] = -1.0f;
+ if (env->slot_for_color[CHESS_WHITE] == 0) env->log.slot_0_score += 1.0f;
+ else env->log.slot_1_score += 1.0f;
+ win_value = 0.5f;
+ } else if (env->learner_color == CHESS_WHITE) {
+ *env->reward_ptr[0] = 1.0f;
+ win_value = 1.0f;
+ env->learner_wins += 1.0f;
+ } else {
+ *env->reward_ptr[0] = -1.0f;
+ env->learner_losses += 1.0f;
+ }
+ strcpy(env->last_result, "White Wins");
+ break;
+ default:
+ break;
+ }
+
+ if (env->mode == CHESS_MODE_SELFPLAY) {
+ env->episode_reward += *env->reward_ptr[0] + *env->reward_ptr[1];
+ } else {
+ env->episode_reward += *env->reward_ptr[0];
+ }
+ env->log.episode_return += env->episode_reward;
+ env->log.perf += win_value;
+ env->log.score += win_value;
+ env->log.chess_moves += env->chess_moves;
+ env->log.episode_length += env->tick;
+ env->log.invalid_action_rate += (env->tick > 0)
+ ? ((float)env->invalid_actions_this_episode / (float)env->tick) : 0.0f;
+
+ env->log.n += 1.0f;
+
+ // Per-color split for non-selfplay eval. learner_color is the color the
+ // learner just played; win_value already accounts for whether it won.
+ // A lopsided split (e.g. high white win rate, near-zero black) is the
+ // signature of a broken obs flip / wrong perspective for one color.
+ if (env->mode != CHESS_MODE_SELFPLAY) {
+ if (env->learner_color == CHESS_WHITE) {
+ env->log.wins_as_white += win_value;
+ env->log.games_as_white += 1.0f;
+ } else {
+ env->log.wins_as_black += win_value;
+ env->log.games_as_black += 1.0f;
+ }
+ }
+
+ // Per-bank historical tracking. Tag = 1..CHESS_MAX_BANKS picks the bank
+ // index (tag-1) this env was assigned to play. Tag = 0 is pure selfplay
+ // (skip historical accounting). Backward compat: tag=1 with single bank
+ // still routes into hist_score_bank[0] / hist_n_bank[0].
+ if (env->tag > 0 && env->tag <= CHESS_MAX_BANKS) {
+ int bank_idx = env->tag - 1;
+ float primary_score;
+ if (game_result == 3) {
+ primary_score = 0.5f;
+ } else if (game_result == 2) { // White wins
+ primary_score = (env->slot_for_color[CHESS_WHITE] == 0) ? 1.0f : 0.0f;
+ } else { // Black wins
+ primary_score = (env->slot_for_color[CHESS_BLACK] == 0) ? 1.0f : 0.0f;
+ }
+ env->log.hist_score_bank[bank_idx] += primary_score;
+ env->log.hist_n_bank[bank_idx] += 1.0f;
+ // Legacy aggregate fields — sum across all banks.
+ env->log.hist_score += primary_score;
+ env->log.hist_n += 1.0f;
+ env->boundary_reached = 1;
+ }
+
+ if (env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM) {
+ env->show_game_end_popup = 1;
+ } else {
+ if (env->log_pgn && env->pgn_filename[0] != '\0') {
+ env->pgn_game_number++;
+ export_pgn_append(env, env->pgn_filename, 1);
+ }
+ c_reset(env);
+ }
+ } else {
+ if (env->mode == CHESS_MODE_SELFPLAY) {
+ env->episode_reward += *env->reward_ptr[0] + *env->reward_ptr[1];
+ } else {
+ env->episode_reward += *env->reward_ptr[0];
+ }
+ }
+
+ populate_observations(env);
+}
+static Font load_piece_font(int cell_size, int* loaded) {
+ const char* candidates[] = {
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
+ "/usr/share/fonts/truetype/noto/NotoSansSymbols2-Regular.ttf",
+ "/System/Library/Fonts/Supplemental/Apple Symbols.ttf",
+ "C:\\Windows\\Fonts\\seguisym.ttf"
+ };
+
+ int codepoints[] = {0x2654, 0x2655, 0x2656, 0x2657, 0x2658, 0x2659, 0x265A, 0x265B, 0x265C, 0x265D, 0x265E, 0x265F};
+ Font font = (Font){0};
+ size_t candidate_count = sizeof(candidates) / sizeof(candidates[0]);
+ size_t codepoint_count = sizeof(codepoints) / sizeof(codepoints[0]);
+
+ for (size_t i = 0; i < candidate_count; i++) {
+ if (!FileExists(candidates[i])) {
+ continue;
+ }
+ font = LoadFontEx(candidates[i], cell_size, codepoints, (int)codepoint_count);
+ if (font.texture.id != 0) {
+ if (loaded) {
+ *loaded = 1;
+ }
+ SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR);
+ return font;
+ }
+ }
+
+ if (loaded) {
+ *loaded = 0;
+ }
+ return GetFontDefault();
+}
+
+static void draw_piece(Chess* env, Piece pc, int file, int rank, int cell_size) {
+ if (pc == NO_PIECE) {
+ return;
+ }
+
+ Color pc_color = color_of(pc) == CHESS_WHITE
+ ? (Color){255, 255, 255, 255}
+ : (Color){0, 0, 0, 255};
+
+ Color outline = (color_of(pc) == CHESS_WHITE)
+ ? (Color){0, 0, 0, 220}
+ : (Color){255, 255, 255, 180};
+
+ int draw_x = file * cell_size;
+ int draw_y = (7 - rank) * cell_size;
+
+ if (env->client && env->client->use_unicode_pieces) {
+ float icon_size = cell_size * 0.85f;
+ Vector2 pos = (Vector2){
+ draw_x + (cell_size - icon_size) / 2.0f,
+ draw_y + (cell_size - icon_size) / 2.0f - cell_size * 0.05f
+ };
+ const char* str = PIECE_FILLED[pc];
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx != 0 || dy != 0) {
+ Vector2 opos = (Vector2){pos.x + dx, pos.y + dy};
+ DrawTextEx(env->client->piece_font, str, opos, icon_size, 0, outline);
+ }
+ }
+ }
+ DrawTextEx(env->client->piece_font, str, pos, icon_size, 0, pc_color);
+ } else {
+ int x = draw_x + cell_size / 4;
+ int y = draw_y + cell_size / 8;
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx != 0 || dy != 0) {
+ DrawText(PIECE_CHARS[pc], x + dx, y + dy, cell_size / 2, outline);
+ }
+ }
+ }
+ DrawText(PIECE_CHARS[pc], x, y, cell_size / 2, pc_color);
+ }
+}
+
+static void init_chess_client(Chess* env, int cell_size) {
+ SetConfigFlags(FLAG_MSAA_4X_HINT);
+ int board_size = 8 * cell_size;
+ InitWindow(board_size, board_size + 140, "PufferLib Chess - AI vs Opponent");
+ SetTargetFPS(env->render_fps > 0 ? env->render_fps : 30);
+ env->client = (Client*)calloc(1, sizeof(Client));
+ env->client->cell_size = cell_size;
+ int font_loaded = 0;
+ env->client->piece_font = load_piece_font(cell_size, &font_loaded);
+ env->client->use_unicode_pieces = font_loaded;
+ if (env->mode == CHESS_MODE_SELFPLAY) env->log_pgn_choice_made = 0;
+}
+
+void c_render(Chess* env) {
+ const int cell_size = 64;
+ const int board_size = 8 * cell_size;
+ const int scoreboard_y = board_size + 10;
+ static int speed_idx = 3;
+ static const int SPEED_FPS[] = {2, 5, 10, 30, 60, 120, 0};
+ static const int NUM_SPEEDS = 7;
+ static int selected_sq = -1;
+
+ if (env->client == NULL) {
+ init_chess_client(env, cell_size);
+ }
+
+human_wait_retry:
+ if (IsKeyDown(KEY_ESCAPE) || WindowShouldClose()) { CloseWindow(); exit(0); }
+
+ int flip_board = ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->human_color == CHESS_BLACK) ? 1 : 0;
+ Vector2 mouse = GetMousePosition();
+ int clicked = IsMouseButtonPressed(MOUSE_LEFT_BUTTON);
+
+ if (IsKeyPressed(KEY_SPACE)) env->render_paused = !env->render_paused;
+ if (IsKeyPressed(KEY_EQUAL) || IsKeyPressed(KEY_KP_ADD)) {
+ if (speed_idx < NUM_SPEEDS - 1) { speed_idx++; SetTargetFPS(SPEED_FPS[speed_idx]); }
+ }
+ if (IsKeyPressed(KEY_MINUS) || IsKeyPressed(KEY_KP_SUBTRACT)) {
+ if (speed_idx > 0) { speed_idx--; SetTargetFPS(SPEED_FPS[speed_idx]); }
+ }
+
+ if (!env->render_paused
+ && (env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->human_color != -1
+ && !env->show_game_end_popup
+ && clicked) {
+ int file = (int)(mouse.x) / cell_size;
+ int rank = 7 - ((int)(mouse.y) / cell_size);
+ if (flip_board) { file = 7 - file; rank = 7 - rank; }
+ if (file >= 0 && file < 8 && rank >= 0 && rank < 8) {
+ int clicked_sq = (int)make_square(file, rank);
+ if (selected_sq == -1) {
+ if (env->pos.sideToMove == env->human_color) {
+ Piece pc = piece_on(&env->pos, (Square)clicked_sq);
+ if (pc != NO_PIECE && color_of(pc) == env->human_color) {
+ bool has_from = false;
+ for (int i = 0; i < env->legal_moves.count; i++) {
+ if ((int)from_sq(env->legal_moves.moves[i].move) == clicked_sq) { has_from = true; break; }
+ }
+ if (has_from) selected_sq = clicked_sq;
+ }
+ }
+ } else {
+ Move chosen = MOVE_NONE;
+ for (int i = 0; i < env->legal_moves.count; i++) {
+ Move m = env->legal_moves.moves[i].move;
+ if ((int)from_sq(m) == selected_sq && (int)to_sq(m) == clicked_sq) { chosen = m; break; }
+ }
+ if (chosen != MOVE_NONE) {
+ int is_timeout = 0;
+ apply_move_to_env(env, chosen, &is_timeout);
+ *env->action_ptr[0] = -1;
+ }
+ selected_sq = -1;
+ }
+ }
+ }
+
+ BeginDrawing();
+ ClearBackground((Color){40, 40, 40, 255});
+
+ if ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->show_game_end_popup) {
+ int pw = 300, ph = 200;
+ int px = (board_size - pw) / 2, py = (board_size - ph) / 2;
+ DrawRectangle(px, py, pw, ph, (Color){60, 60, 60, 255});
+ DrawRectangleLines(px, py, pw, ph, WHITE);
+ DrawText("Game Over!", px + 70, py + 20, 24, WHITE);
+ DrawText(env->last_result, px + 80, py + 55, 18, YELLOW);
+
+ Rectangle save_btn = {px + 20, py + 110, 120, 35};
+ Rectangle new_btn = {px + 160, py + 110, 120, 35};
+ DrawRectangleRec(save_btn, DARKGREEN);
+ DrawRectangleLinesEx(save_btn, 2, WHITE);
+ DrawText("Save PGN", px + 35, py + 120, 16, WHITE);
+ DrawRectangleRec(new_btn, DARKBLUE);
+ DrawRectangleLinesEx(new_btn, 2, WHITE);
+ DrawText("New Game", px + 175, py + 120, 16, WHITE);
+
+ if (clicked) {
+ if (CheckCollisionPointRec(mouse, save_btn)) {
+ char filename[64];
+ snprintf(filename, sizeof(filename), "game_%d.pgn", (int)time(NULL));
+ export_pgn_append(env, filename, 0);
+ printf("Saved PGN to %s\n", filename);
+ } else if (CheckCollisionPointRec(mouse, new_btn)) {
+ c_reset(env);
+ }
+ }
+ } else if (env->mode == CHESS_MODE_SELFPLAY && !env->log_pgn_choice_made) {
+ int cx = board_size / 2;
+ DrawText("Log PGN Files?", cx - 80, 180, 24, WHITE);
+ DrawText("Games will be appended to a timestamped file", cx - 160, 220, 14, LIGHTGRAY);
+
+ Rectangle yes_btn = {cx - 70, 270, 140, 40};
+ Rectangle no_btn = {cx - 70, 330, 140, 40};
+ DrawRectangleRec(yes_btn, DARKGREEN);
+ DrawRectangleLinesEx(yes_btn, 2, WHITE);
+ DrawText("Yes, Log PGN", cx - 55, 282, 16, WHITE);
+ DrawRectangleRec(no_btn, MAROON);
+ DrawRectangleLinesEx(no_btn, 2, WHITE);
+ DrawText("No Logging", cx - 45, 342, 16, WHITE);
+
+ if (clicked) {
+ if (CheckCollisionPointRec(mouse, yes_btn)) {
+ env->log_pgn = 1;
+ env->log_pgn_choice_made = 1;
+ env->pgn_game_number = 0;
+ snprintf(env->pgn_filename, sizeof(env->pgn_filename), "run_%d_pgns.pgn", (int)time(NULL));
+ printf("PGN logging enabled: %s\n", env->pgn_filename);
+ } else if (CheckCollisionPointRec(mouse, no_btn)) {
+ env->log_pgn = 0;
+ env->log_pgn_choice_made = 1;
+ printf("PGN logging disabled\n");
+ }
+ }
+ } else if ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->human_color == -1) {
+ int cx = board_size / 2;
+ DrawText("Choose Your Color", cx - 100, 200, 24, WHITE);
+
+ Rectangle white_btn = {cx - 60, 280, 120, 40};
+ Rectangle black_btn = {cx - 60, 340, 120, 40};
+ DrawRectangleRec(white_btn, LIGHTGRAY);
+ DrawRectangleLinesEx(white_btn, 2, BLACK);
+ DrawText("Play White", cx - 45, 292, 18, BLACK);
+ DrawRectangleRec(black_btn, GRAY);
+ DrawRectangleLinesEx(black_btn, 2, BLACK);
+ DrawText("Play Black", cx - 45, 352, 18, WHITE);
+
+ if (clicked) {
+ if (CheckCollisionPointRec(mouse, white_btn)) {
+ env->human_color = CHESS_WHITE;
+ env->learner_color = CHESS_BLACK;
+ } else if (CheckCollisionPointRec(mouse, black_btn)) {
+ env->human_color = CHESS_BLACK;
+ env->learner_color = CHESS_WHITE;
+ }
+ }
+ } else {
+ Bitboard selected_destinations = 0;
+ if (selected_sq != -1) {
+ for (int i = 0; i < env->legal_moves.count; i++) {
+ Move m = env->legal_moves.moves[i].move;
+ if ((int)from_sq(m) == selected_sq) {
+ selected_destinations |= sq_bb(to_sq(m));
+ }
+ }
+ }
+ int selected_file = -1;
+ int selected_rank = -1;
+ if (selected_sq != -1) {
+ selected_file = file_of((Square)selected_sq);
+ selected_rank = rank_of((Square)selected_sq);
+ }
+ for (int rank = 0; rank < 8; rank++) {
+ for (int file = 0; file < 8; file++) {
+ Color sq_color = ((rank + file) % 2 == 1) ? (Color){240, 217, 181, 255} : (Color){181, 136, 99, 255};
+ int draw_file = flip_board ? (7 - file) : file;
+ int draw_rank = flip_board ? (7 - rank) : rank;
+ int draw_x = draw_file * cell_size;
+ int draw_y = (7 - draw_rank) * cell_size;
+ DrawRectangle(draw_x, draw_y, cell_size, cell_size, sq_color);
+
+ if (env->has_last_move_highlight) {
+ Square lf = env->last_move_from;
+ Square lt = env->last_move_to;
+ if ((file == (int)file_of(lf) && rank == (int)rank_of(lf))
+ || (file == (int)file_of(lt) && rank == (int)rank_of(lt))) {
+ Color last_mv = (Color){247, 247, 105, 255};
+ DrawRectangle(draw_x, draw_y, cell_size, cell_size, Fade(last_mv, 0.52f));
+ }
+ }
+
+ if (selected_sq != -1 && selected_file == file && selected_rank == rank) {
+ DrawRectangleLines(draw_x, draw_y, cell_size, cell_size, (Color){255, 215, 0, 255});
+ }
+ if (selected_sq != -1 && (selected_destinations & sq_bb(make_square(file, rank)))) {
+ DrawRectangleLines(draw_x + 2, draw_y + 2, cell_size - 4, cell_size - 4, (Color){0, 200, 0, 255});
+ }
+ }
+ }
+ for (int pt = PAWN; pt <= KING; pt++) {
+ Bitboard bb = pieces_p(&env->pos, pt);
+ while (bb) {
+ Square sq = pop_lsb(&bb);
+ Piece pc = piece_on(&env->pos, sq);
+ int f = file_of(sq), r = rank_of(sq);
+ int draw_f = flip_board ? (7 - f) : f;
+ int draw_r = flip_board ? (7 - r) : r;
+ draw_piece(env, pc, draw_f, draw_r, cell_size);
+ }
+ }
+
+ char buf[128];
+ snprintf(buf, sizeof(buf), "White: %.1f Black: %.1f", env->white_score, env->black_score);
+ DrawText(buf, 10, scoreboard_y, 20, WHITE);
+
+ snprintf(buf, sizeof(buf), "Learner: %.0f-%.0f-%.0f (W-L-D)", env->learner_wins, env->learner_losses, env->learner_draws);
+ DrawText(buf, 10, scoreboard_y + 22, 16, GREEN);
+
+ snprintf(buf, sizeof(buf), "Move: %d", env->chess_moves);
+ DrawText(buf, board_size - 100, scoreboard_y, 18, LIGHTGRAY);
+
+ if (env->mode != CHESS_MODE_HUMAN && env->mode != CHESS_MODE_HUMAN_RANDOM) {
+ DrawText(env->learner_color == CHESS_WHITE ? "Learner: White" : "Learner: Black",
+ board_size - 120, scoreboard_y + 22, 16, LIGHTGRAY);
+ }
+
+ int cap_y = scoreboard_y + 42;
+ int cap_x_start = 10;
+ Color white_cap_color = (Color){240, 217, 181, 255};
+ Color black_cap_color = (Color){100, 100, 100, 255};
+ int white_x = cap_x_start;
+ int black_x = cap_x_start;
+
+ for (int pt = 0; pt < 6; pt++) {
+ int wc = env->white_captured[pt];
+ if (wc > 0) {
+ Piece wpc = (Piece)(W_PAWN + pt);
+ if (env->client && env->client->use_unicode_pieces) {
+ DrawTextEx(env->client->piece_font, PIECE_FILLED[wpc],
+ (Vector2){(float)white_x, (float)(cap_y - 1)}, 16.0f, 0.0f, white_cap_color);
+ white_x += 16;
+ } else {
+ DrawText(PIECE_CHARS[wpc], white_x, cap_y, 14, white_cap_color);
+ white_x += 12;
+ }
+ if (wc > 1) {
+ char mult[8];
+ snprintf(mult, sizeof(mult), "x%d", wc);
+ DrawText(mult, white_x, cap_y + 2, 10, white_cap_color);
+ white_x += MeasureText(mult, 10) + 4;
+ } else {
+ white_x += 4;
+ }
+ }
+
+ int bc = env->black_captured[pt];
+ if (bc > 0) {
+ Piece bpc = (Piece)(B_PAWN + pt);
+ Color outline = (Color){255, 255, 255, 180};
+ if (env->client && env->client->use_unicode_pieces) {
+ Vector2 pos = {(float)black_x, (float)(cap_y + 17)};
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx != 0 || dy != 0) {
+ DrawTextEx(env->client->piece_font, PIECE_FILLED[bpc],
+ (Vector2){pos.x + dx, pos.y + dy}, 16.0f, 0.0f, outline);
+ }
+ }
+ }
+ DrawTextEx(env->client->piece_font, PIECE_FILLED[bpc], pos, 16.0f, 0.0f, black_cap_color);
+ black_x += 16;
+ } else {
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx != 0 || dy != 0)
+ DrawText(PIECE_CHARS[bpc], black_x + dx, cap_y + 18 + dy, 14, outline);
+ }
+ }
+ DrawText(PIECE_CHARS[bpc], black_x, cap_y + 18, 14, black_cap_color);
+ black_x += 12;
+ }
+ if (bc > 1) {
+ char mult[8];
+ snprintf(mult, sizeof(mult), "x%d", bc);
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx != 0 || dy != 0)
+ DrawText(mult, black_x + dx, cap_y + 20 + dy, 10, outline);
+ }
+ }
+ DrawText(mult, black_x, cap_y + 20, 10, black_cap_color);
+ black_x += MeasureText(mult, 10) + 4;
+ } else {
+ black_x += 4;
+ }
+ }
+ }
+
+ if (env->last_result[0] != '\0') {
+ Color rc = YELLOW;
+ if (strstr(env->last_result, "White")) rc = (Color){240, 217, 181, 255};
+ else if (strstr(env->last_result, "Black")) rc = (Color){100, 100, 100, 255};
+ DrawText(env->last_result, 10, cap_y + 40, 18, rc);
+ }
+
+ int btn_w = 36;
+ int btn_h = 24;
+ int btn_y = scoreboard_y + 100;
+ int btn_x = (env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ ? board_size / 2 - 100 : board_size / 2 - 70;
+ Rectangle minus_btn = {btn_x, btn_y, btn_w, btn_h};
+ Rectangle pause_btn = {btn_x + btn_w + 5, btn_y, btn_w + 10, btn_h};
+ Rectangle plus_btn = {btn_x + 2 * btn_w + 20, btn_y, btn_w, btn_h};
+ DrawRectangleRec(minus_btn, DARKGRAY);
+ DrawRectangleLinesEx(minus_btn, 2, LIGHTGRAY);
+ DrawText("-", btn_x + 14, btn_y + 4, 20, WHITE);
+ DrawRectangleRec(pause_btn, env->render_paused ? MAROON : DARKGREEN);
+ DrawRectangleLinesEx(pause_btn, 2, LIGHTGRAY);
+ DrawText(env->render_paused ? ">" : "||", btn_x + btn_w + 14, btn_y + 4, 18, WHITE);
+ DrawRectangleRec(plus_btn, DARKGRAY);
+ DrawRectangleLinesEx(plus_btn, 2, LIGHTGRAY);
+ DrawText("+", btn_x + 2 * btn_w + 32, btn_y + 4, 20, WHITE);
+ char speed_buf[32];
+ if (SPEED_FPS[speed_idx] == 0) {
+ snprintf(speed_buf, sizeof(speed_buf), "max");
+ } else {
+ snprintf(speed_buf, sizeof(speed_buf), "%dfps", SPEED_FPS[speed_idx]);
+ }
+ DrawText(speed_buf, btn_x + 3 * btn_w + 30, btn_y + 4, 14, env->render_paused ? RED : LIGHTGRAY);
+
+ Rectangle restart_btn = {0, 0, 0, 0};
+ if (env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM) {
+ restart_btn = (Rectangle){board_size - 60, minus_btn.y, 55, minus_btn.height};
+ DrawRectangleRec(restart_btn, MAROON);
+ DrawRectangleLinesEx(restart_btn, 2, LIGHTGRAY);
+ DrawText("Exit", board_size - 53, minus_btn.y + 4, 16, WHITE);
+ }
+
+ if (env->render_paused) {
+ DrawRectangle(0, 0, board_size, board_size, (Color){0, 0, 0, 120});
+ DrawText("PAUSED", board_size / 2 - 60, board_size / 2 - 15, 30, RED);
+ }
+
+ if (clicked) {
+ if (CheckCollisionPointRec(mouse, minus_btn)) {
+ if (speed_idx > 0) { speed_idx--; SetTargetFPS(SPEED_FPS[speed_idx]); }
+ }
+ if (CheckCollisionPointRec(mouse, pause_btn)) env->render_paused = !env->render_paused;
+ if (CheckCollisionPointRec(mouse, plus_btn)) {
+ if (speed_idx < NUM_SPEEDS - 1) { speed_idx++; SetTargetFPS(SPEED_FPS[speed_idx]); }
+ }
+ if ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && CheckCollisionPointRec(mouse, restart_btn)) c_reset(env);
+ }
+ }
+
+ EndDrawing();
+
+ // Human-mode only: stay in c_render (on the window-owning thread) until
+ // the human commits a move via mouse clicks. Re-poll input + redraw each
+ // iteration. Non-human modes fall through to a single c_render call as
+ // before. Refresh obs after the commit so the next rollout inference sees
+ // the post-human-move state instead of the stale "human's turn" obs.
+ if ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->human_color != -1
+ && env->pos.sideToMove == env->human_color
+ && !env->show_game_end_popup
+ && !env->render_paused
+ && !WindowShouldClose()) {
+ goto human_wait_retry;
+ }
+ if ((env->mode == CHESS_MODE_HUMAN || env->mode == CHESS_MODE_HUMAN_RANDOM)
+ && env->human_color != -1
+ && env->pos.sideToMove != env->human_color
+ && !env->show_game_end_popup) {
+ if (env->legal_dirty) rebuild_legal_state(env);
+ populate_observations(env);
+ }
+}
+
+void c_close(Chess* env) {
+ if (env->client != NULL) {
+ if (env->client->use_unicode_pieces && env->client->piece_font.texture.id != 0) {
+ UnloadFont(env->client->piece_font);
+ }
+ if (IsWindowReady()) {
+ CloseWindow();
+ }
+ free(env->client);
+ env->client = NULL;
+ }
+ maia_close(env);
+ env->fen_curriculum = NULL;
+ env->num_fens = 0;
+}
diff --git a/ocean/connect4/binding.c b/ocean/connect4/binding.c
new file mode 100644
index 0000000000..4c68006442
--- /dev/null
+++ b/ocean/connect4/binding.c
@@ -0,0 +1,23 @@
+#include "connect4.h"
+#define OBS_SIZE 42
+#define NUM_ATNS 1
+#define ACT_SIZES {7}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Connect4
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->player_pieces = dict_get(kwargs, "player_pieces")->value;
+ env->env_pieces = dict_get(kwargs, "env_pieces")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "n", log->n);
+}
diff --git a/ocean/connect4/connect4.c b/ocean/connect4/connect4.c
new file mode 100644
index 0000000000..df7059e24f
--- /dev/null
+++ b/ocean/connect4/connect4.c
@@ -0,0 +1,51 @@
+#include "connect4.h"
+#include "puffernet.h"
+#include "time.h"
+
+const unsigned char NOOP = 8;
+
+void demo() {
+ Weights* weights = load_weights("resources/connect4/connect4_weights.bin");
+ int logit_sizes[] = {7};
+ PufferNet* net = make_puffernet(weights, 1, 42, 256, 1, logit_sizes, 1);
+
+ Connect4 env = {
+ };
+ allocate_cconnect4(&env);
+ c_reset(&env);
+
+ env.client = make_client();
+
+ int tick = 0;
+ while (!WindowShouldClose()) {
+ env.actions[0] = NOOP;
+ // user inputs 1 - 7 key pressed
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ if(IsKeyPressed(KEY_ONE)) env.actions[0] = 0;
+ if(IsKeyPressed(KEY_TWO)) env.actions[0] = 1;
+ if(IsKeyPressed(KEY_THREE)) env.actions[0] = 2;
+ if(IsKeyPressed(KEY_FOUR)) env.actions[0] = 3;
+ 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 if (tick % 30 == 0) {
+ forward_puffernet(net, env.observations, env.actions);
+ }
+
+ tick = (tick + 1) % 60;
+ if (env.actions[0] >= 0 && env.actions[0] <= 6) {
+ c_step(&env);
+ }
+
+ c_render(&env);
+ }
+ free_puffernet(net);
+ free(weights);
+ close_client(env.client);
+ free_allocated_cconnect4(&env);
+}
+
+int main() {
+ demo();
+ return 0;
+}
diff --git a/pufferlib/ocean/connect4/connect4.h b/ocean/connect4/connect4.h
similarity index 89%
rename from pufferlib/ocean/connect4/connect4.h
rename to ocean/connect4/connect4.h
index dde12a4a16..5fe2d1d5e4 100644
--- a/pufferlib/ocean/connect4/connect4.h
+++ b/ocean/connect4/connect4.h
@@ -7,8 +7,6 @@
#define WIN_CONDITION 4
const int PLAYER_WIN = 1.0;
const int ENV_WIN = -1.0;
-const unsigned char DONE = 1;
-const unsigned char NOT_DONE = 0;
const int ROWS = 6;
const int COLUMNS = 7;
const int WIDTH = 672;
@@ -30,13 +28,14 @@ struct Log {
};
typedef struct Client Client;
-typedef struct CConnect4 CConnect4;
-struct CConnect4 {
+typedef struct Connect4 Connect4;
+struct Connect4 {
// Pufferlib inputs / outputs
float* observations;
- int* actions;
+ float* actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
+ int num_agents;
Log log;
Client* client;
@@ -47,35 +46,36 @@ struct CConnect4 {
uint64_t env_pieces;
int tick;
+ int end_game;
+ unsigned int rng;
};
-void allocate_cconnect4(CConnect4* env) {
+void allocate_cconnect4(Connect4* env) {
env->observations = (float*)calloc(42, sizeof(float));
- env->actions = (int*)calloc(1, sizeof(int));
- env->terminals = (unsigned char*)calloc(1, sizeof(unsigned char));
+ env->actions = (float*)calloc(1, sizeof(float));
+ env->terminals = (float*)calloc(1, sizeof(float));
env->rewards = (float*)calloc(1, sizeof(float));
}
-void free_allocated_cconnect4(CConnect4* env) {
+void free_allocated_cconnect4(Connect4* env) {
free(env->actions);
free(env->observations);
free(env->terminals);
free(env->rewards);
}
-void c_close(CConnect4* env) {
+void c_close(Connect4* env) {
}
-void add_log(CConnect4* env) {
+void add_log(Connect4* env) {
env->log.perf += (float)(env->rewards[0] == PLAYER_WIN);
env->log.score += env->rewards[0];
env->log.episode_return += env->rewards[0];
- env->log.episode_length += env->log.episode_length;
+ env->log.episode_length += env->tick;
env->log.n += 1;
}
-void init(CConnect4* env) {
- env->log = (Log){0};
+void init(Connect4* env) {
env->tick = 0;
}
@@ -162,7 +162,7 @@ float negamax(uint64_t pieces, uint64_t other_pieces, int depth) {
return value;
}
-int compute_env_move(CConnect4* env) {
+int compute_env_move(Connect4* env) {
uint64_t piece_mask = env->player_pieces | env->env_pieces;
uint64_t hash = env->player_pieces + piece_mask + c_bottom();
@@ -205,7 +205,8 @@ int compute_env_move(CConnect4* env) {
}
}
//printf("Values: %f, %f, %f, %f, %f, %f, %f\n", values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
- int best_tie = rand() % num_ties;
+ //int best_tie = rand() % num_ties;
+ int best_tie = rand_r(&env->rng) % num_ties;
for (uint64_t column = 0; column < 7; column ++) {
if (values[column] == best_value) {
if (best_tie == 0) {
@@ -218,7 +219,7 @@ int compute_env_move(CConnect4* env) {
return 0;
}
-void compute_observation(CConnect4* env) {
+void compute_observation(Connect4* env) {
// Populate observations from bitstring game representation
// http://blog.gamesolver.org/solving-connect-four/06-bitboard/
uint64_t player_pieces = env->player_pieces;
@@ -243,9 +244,10 @@ void compute_observation(CConnect4* env) {
}
}
-void c_reset(CConnect4* env) {
- env->log = (Log){0};
- env->terminals[0] = NOT_DONE;
+void c_reset(Connect4* env) {
+ env->end_game = 0;
+ env->tick=0;
+ env->terminals[0] = 0;
env->player_pieces = 0;
env->env_pieces = 0;
for (int i = 0; i < 42; i ++) {
@@ -253,24 +255,25 @@ void c_reset(CConnect4* env) {
}
}
-void finish_game(CConnect4* env, float reward) {
+void finish_game(Connect4* env, float reward) {
env->rewards[0] = reward;
- env->terminals[0] = DONE;
+ env->terminals[0] = 1;
add_log(env);
- compute_observation(env);
+ env->end_game = 1;
}
-void c_step(CConnect4* env) {
- env->log.episode_length += 1;
+void c_step(Connect4* env) {
+ env->tick+=1;
env->rewards[0] = 0.0;
+ env->terminals[0] = 0;
- if (env->terminals[0] == DONE) {
+ if(env->end_game == 1) {
c_reset(env);
return;
}
// Player action (PLAYER_WIN)
- uint64_t column = env->actions[0];
+ uint64_t column = (uint64_t)env->actions[0];
uint64_t piece_mask = env->player_pieces | env->env_pieces;
if (invalid_move(column, piece_mask)) {
finish_game(env, ENV_WIN);
@@ -324,7 +327,7 @@ Client* make_client() {
return client;
}
-void c_render(CConnect4* env) {
+void c_render(Connect4* env) {
if (IsKeyDown(KEY_ESCAPE)) {
exit(0);
}
diff --git a/ocean/convert/binding.c b/ocean/convert/binding.c
new file mode 100644
index 0000000000..4209a27e06
--- /dev/null
+++ b/ocean/convert/binding.c
@@ -0,0 +1,25 @@
+#include "convert.h"
+#define OBS_SIZE 28
+#define NUM_ATNS 2
+#define ACT_SIZES {9, 5}
+#define OBS_TYPE FLOAT
+#define ACT_TYPE DOUBLE
+
+#define Env Convert
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = dict_get(kwargs, "num_agents")->value;
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->num_factories = dict_get(kwargs, "num_factories")->value;
+ env->num_resources = dict_get(kwargs, "num_resources")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+}
diff --git a/pufferlib/ocean/convert/convert.c b/ocean/convert/convert.c
similarity index 98%
rename from pufferlib/ocean/convert/convert.c
rename to ocean/convert/convert.c
index 09f0d41f43..327dd807f5 100644
--- a/pufferlib/ocean/convert/convert.c
+++ b/ocean/convert/convert.c
@@ -17,7 +17,7 @@ int main() {
env.rewards = calloc(env.num_agents, sizeof(float));
env.terminals = calloc(env.num_agents, sizeof(unsigned char));
- Weights* weights = load_weights("resources/convert/convert_weights.bin", 137743);
+ Weights* weights = load_weights("resources/convert/convert_weights.bin");
int logit_sizes[2] = {9, 5};
LinearLSTM* net = make_linearlstm(weights, env.num_agents, num_obs, logit_sizes, 2);
diff --git a/pufferlib/ocean/convert/convert.h b/ocean/convert/convert.h
similarity index 99%
rename from pufferlib/ocean/convert/convert.h
rename to ocean/convert/convert.h
index d7d6828bdf..c3cf3305c3 100644
--- a/pufferlib/ocean/convert/convert.h
+++ b/ocean/convert/convert.h
@@ -43,12 +43,12 @@ typedef struct {
Agent* agents;
Factory* factories;
float* observations;
- int* actions;
+ double* actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
+ int num_agents;
int width;
int height;
- int num_agents;
int num_factories;
int num_resources;
} Convert;
diff --git a/pufferlib/ocean/convert_circle/binding.c b/ocean/convert_circle/binding.c
similarity index 100%
rename from pufferlib/ocean/convert_circle/binding.c
rename to ocean/convert_circle/binding.c
diff --git a/pufferlib/ocean/convert_circle/convert_circle.c b/ocean/convert_circle/convert_circle.c
similarity index 94%
rename from pufferlib/ocean/convert_circle/convert_circle.c
rename to ocean/convert_circle/convert_circle.c
index d889361081..bd8784d2bb 100644
--- a/pufferlib/ocean/convert_circle/convert_circle.c
+++ b/ocean/convert_circle/convert_circle.c
@@ -23,7 +23,7 @@ int main() {
env.terminals = calloc(env.num_agents, sizeof(unsigned char));
Weights *weights =
- load_weights("resources/convert/convert_weights.bin", 137743);
+ load_weights("resources/convert/convert_weights.bin");
int logit_sizes[2] = {9, 5};
LinearLSTM *net =
make_linearlstm(weights, env.num_agents, num_obs, logit_sizes, 2);
diff --git a/pufferlib/ocean/convert_circle/convert_circle.h b/ocean/convert_circle/convert_circle.h
similarity index 100%
rename from pufferlib/ocean/convert_circle/convert_circle.h
rename to ocean/convert_circle/convert_circle.h
diff --git a/ocean/craftax/PORT_NOTES.md b/ocean/craftax/PORT_NOTES.md
new file mode 100644
index 0000000000..4542b1dcb8
--- /dev/null
+++ b/ocean/craftax/PORT_NOTES.md
@@ -0,0 +1,543 @@
+# Craftax Full Ocean Port Notes
+
+## Verification coverage
+
+The standalone parity harness now supports deterministic action policies beyond
+uniform random exploration:
+
+- `uniform`: the original random action stream.
+- `combat`: biases toward `DO`, arrows, fireballs, and iceballs when mobs and
+ resources make those actions meaningful, otherwise moves toward live mobs.
+- `descend`: uses the mirrored state to push toward down ladders, clear blocked
+ levels through combat, and exercise placement and crafting actions.
+- `suicide`: steers into adjacent lava, water, mob-occupied, or projectile-heavy
+ danger and otherwise paths toward the nearest known hazard.
+- `boss`: warms up with downward navigation and then repeatedly attempts
+ descent while continuing to route toward ladders.
+- `mixed`: round-robins the above every 500 steps.
+
+`tests/craftax_parity.py` now reports the policy, seed, step, action, reward
+delta, terminal delta, first symbolic-observation field, suspected subsystem,
+and the last 10 actions on any divergence. With `--reset-on-done` enabled, the
+harness tracks terminal counts and mean episode length by seed. JAX stepping is
+run through the no-auto-reset path with the same per-step key split used by the
+native env; when a terminal is observed, the mirrored state is advanced through
+the native reset helper keyed by the same auto-reset key, and that reset state
+and observation are checked field-by-field before continuing.
+
+The stress battery in `tests/craftax_parity_stress.py` runs:
+
+- 64 seeds times 10000 steps with `mixed`.
+- 16 seeds times 30000 steps with `descend`.
+- 32 seeds times 5000 steps with `suicide`.
+- 16 seeds times 5000 steps with `combat`.
+
+All stress cases use `atol=1e-5` for observations and rewards and exact terminal
+matching. The phase-10a run completed with zero divergences in 1033.0 seconds:
+2883 terminals in `mixed`, 2498 in `descend`, 622 in `suicide`, and 355 in
+`combat`.
+
+Residual caveats:
+
+- The harness observes live C step state through the public vector API, so step
+ diagnostics identify the first differing observation field and subsystem class
+ rather than dumping the entire private C state after every step.
+- CPU XLA can fuse reset worldgen noise normalization differently from
+ materialized JAX by one ULP on exact threshold cells. Materialized JAX
+ worldgen and native reset agree on the targeted sand-threshold keys covered by
+ `tests/craftax_worldgen_test.py`, so terminal continuation uses the native
+ reset helper after explicit reset-state verification.
+
+## 2026-04-18 Native Step Integration and Proxy Removal
+
+This phase wires the green native reset and all green native step subsystems
+into the live Ocean `c_step` path. The Python/JAX proxy has been fully removed:
+`c_init`, `c_reset`, `c_step`, and `c_close` are now 100% native.
+
+- `c_step_native` now mirrors the installed `craftax_step` subsystem order:
+ floor changes, crafting, action, placement, projectiles, spells, potions,
+ books, enchantment, boss logic, attributes, movement, mobs, spawning, plants,
+ intrinsics, clipping, inventory achievements, reward, timestep, light level,
+ terminal, and symbolic observation encoding.
+- The live env keeps the same outer RNG schedule as the old auto-reset proxy:
+ reset uses the reset key's inner worldgen split, each step splits the external
+ key once, then splits the per-step key into gameplay and auto-reset keys.
+- Step observations reuse the native symbolic encoder, now with mob channels and
+ boss-vulnerable special value populated for non-reset states.
+- `tests/craftax_step_full_test.py` adds the full side-by-side parity check for
+ 16 seeds times 2000 random-action steps. `tests/craftax_parity.py` remains as
+ the standalone harness.
+
+Native-step roadmap checklist:
+
+- [x] Native reset PRNG, noise, 9-floor world generation, and reset observation.
+- [x] Standalone native simple step subsystems with JAX-parity tests.
+- [x] Standalone native medium step subsystems with JAX-parity tests.
+- [x] Standalone native crafting and placement subsystems with JAX-parity tests.
+- [x] Standalone native `do_action` subsystem with JAX-parity tests.
+- [x] Standalone native `spawn_mobs` subsystem with JAX-parity tests.
+- [x] Standalone native `update_mobs` subsystem with JAX-parity tests.
+- [x] Native reward, terminal, timestep, light-level, RNG, and achievement-delta
+ bookkeeping around the subsystem calls.
+- [x] Integrate all green subsystem ports into native `c_step` and remove all
+ Python/JAX proxy code paths.
+
+Remaining proxy paths:
+
+- None. The Craftax Ocean env no longer loads CPython symbols, constructs a JAX
+ env, or delegates reset/step/close through Python.
+
+Next phase:
+
+- Optimize the native path after correctness is locked down. Likely targets are
+ SIMD-friendly loops, cache-tiled symbolic observation encoding, and mob update
+ hot paths. Performance claims need measurement.
+
+## 2026-04-18 Standalone Update Mobs Step Subsystem
+
+This phase adds a native C port for the `update_mobs` subsystem, still
+deliberately without integrating it into `c_step`. The live Ocean environment
+continues to delegate step to the Python/JAX proxy.
+
+- `step_update_mobs.h` contains the standalone in-place helper for:
+ - `update_mobs`
+- The helper mirrors the installed JAX update order for melee mobs, passive
+ mobs, ranged mobs, mob projectiles, and player projectiles. It preserves the
+ scan-level Threefry threading, including the melee loop's final right-key
+ carry, and the top-level split before each mob class.
+- Mob movement and collision use the installed collision tables for land,
+ flying, aquatic, and amphibian mobs, including JAX-style clamped reads,
+ scatter-drop writes, mob-map exclusion, water/lava/solid checks, despawn
+ distance, boss-floor despawn suppression, and sequential mob-map updates.
+- Combat covers melee player attacks, ranged projectile spawning, projectile
+ movement, player damage with armour and enchantment defenses, sleeping/resting
+ wakeups, player projectile damage scaling, first-target mob attacks, kill
+ achievements, mob-map clearing, and `monsters_killed` updates.
+- `tests/craftax_step_update_mobs_test.py` builds a temporary C wrapper around
+ the inline helper and compares full copied states against the installed JAX
+ function for 16 reset-plus-RNG-action-stepped states. Targeted coverage
+ includes every mob class on every floor, melee attacks, ranged projectile
+ firing, mob projectiles hitting the player, walls, and out-of-bounds, player
+ projectile mob kills, despawn, cooldown decrement, and empty-mask live-effect
+ checks.
+
+Native-step roadmap checklist:
+
+- [x] Native reset PRNG, noise, 9-floor world generation, and reset observation.
+- [x] Standalone native simple step subsystems with JAX-parity tests.
+- [x] Standalone native medium step subsystems with JAX-parity tests.
+- [x] Standalone native crafting and placement subsystems with JAX-parity tests.
+- [x] Standalone native `do_action` subsystem with JAX-parity tests.
+- [x] Standalone native `spawn_mobs` subsystem with JAX-parity tests.
+- [x] Standalone native `update_mobs` subsystem with JAX-parity tests.
+- [ ] Native reward, terminal, timestep, light-level, RNG, and achievement-delta
+ bookkeeping around the subsystem calls.
+- [ ] Integrate all green subsystem ports into a native `c_step` behind one
+ explicit switch, then remove the Python/JAX proxy from the normal step path.
+- [ ] Restore production vector sizes in `config/ocean/craftax.ini` after native
+ step is the default.
+- [ ] Benchmark CPU throughput only after the proxy path is gone.
+
+Remaining proxy paths:
+
+- `c_step` still delegates to the Python/JAX proxy. None of the standalone
+ subsystem helpers are wired into the live environment yet.
+- All gameplay step subsystems now have standalone native ports with parity
+ tests. Reward/terminal bookkeeping, light-level updates, timestep updates,
+ RNG threading between subsystems, and achievement-delta logging are still not
+ integrated natively.
+- Rendering remains a no-op.
+- `config/ocean/craftax.ini` still uses a small proxy-friendly vector size. The
+ native port should raise this once step no longer calls Python.
+
+## 2026-04-18 Standalone Spawn Mobs Step Subsystem
+
+This phase adds a native C port for the `spawn_mobs` subsystem, still
+deliberately without integrating it into `c_step`. The live Ocean environment
+continues to delegate step to the Python/JAX proxy.
+
+- `step_spawn_mobs.h` contains the standalone in-place helper for:
+ - `spawn_mobs`
+- The helper mirrors the installed JAX split order: passive chance, passive
+ position, melee chance, melee position, ranged chance, ranged position. It
+ also keeps the JAX behavior where the selected slot's `type_id` is written
+ even when the spawn gate fails.
+- Spawn maps match the installed function's terrain and distance rules,
+ including passive distance rejection near the player, monster range gates,
+ overworld night-zombie light scaling, deep-thing water spawning, grave-only
+ boss-wave spawning, mob-map exclusion, caps, and sequential mob-map updates
+ between passive, melee, and ranged attempts.
+- `tests/craftax_step_spawn_mobs_test.py` builds a temporary C wrapper around
+ the inline helper and compares full copied states against the installed JAX
+ function for 16 reset-plus-NOOP-step seeds. Targeted coverage includes all
+ nine floors, full mob caps, empty-slot spawns at single candidate positions,
+ day versus night overworld melee chances, boss spawn-wave pacing, player-
+ adjacent candidate rejection, and land, water, and grave terrain constraints.
+
+Native-step roadmap checklist:
+
+- [x] Native reset PRNG, noise, 9-floor world generation, and reset observation.
+- [x] Standalone native simple step subsystems with JAX-parity tests.
+- [x] Standalone native medium step subsystems with JAX-parity tests.
+- [x] Standalone native crafting and placement subsystems with JAX-parity tests.
+- [x] Standalone native `do_action` subsystem with JAX-parity tests.
+- [x] Standalone native `spawn_mobs` subsystem with JAX-parity tests.
+- [ ] Standalone native `update_mobs` subsystem with JAX-parity tests.
+- [ ] Native reward, terminal, timestep, light-level, RNG, and achievement-delta
+ bookkeeping around the subsystem calls.
+- [ ] Integrate all green subsystem ports into a native `c_step` behind one
+ explicit switch, then remove the Python/JAX proxy from the normal step path.
+- [ ] Restore production vector sizes in `config/ocean/craftax.ini` after native
+ step is the default.
+- [ ] Benchmark CPU throughput only after the proxy path is gone.
+
+Remaining proxy paths:
+
+- `c_step` still delegates to the Python/JAX proxy. None of the standalone
+ subsystem helpers are wired into the live environment yet.
+- The only gameplay step subsystem still without a standalone native port is
+ `update_mobs`. Reward/terminal bookkeeping, light-level updates, timestep
+ updates, RNG threading between subsystems, and achievement-delta logging are
+ also still not integrated natively.
+- Rendering remains a no-op.
+- `config/ocean/craftax.ini` still uses a small proxy-friendly vector size. The
+ native port should raise this once step no longer calls Python.
+
+## 2026-04-18 Standalone Do Action Step Subsystem
+
+This phase adds a native C port for the `do_action` subsystem, still
+deliberately without integrating it into `c_step`. The live Ocean environment
+continues to delegate step to the Python/JAX proxy.
+
+- `step_do_action.h` contains the standalone in-place helper for:
+ - `do_action`
+- The helper mirrors the installed JAX ordering: mob attack resolution runs
+ before block interaction; block mining/eating/drinking/inventory/achievement
+ effects are gated by in-bounds and no mob attack; chest-open flags and boss
+ progress keep the JAX side effects that are not part of that gate.
+- Chest looting calls the existing native `craftax_add_items_from_chest_native`
+ helper after consuming the sapling RNG split, so first-open bow/book rewards
+ see the old `chests_opened` value and the chest RNG thread matches JAX.
+- Mob attacks cover passive, melee, and ranged mob arrays, including first-match
+ target selection, defense mapping, sword enchantment damage, strength and
+ intelligence scaling, passive food refill, kill achievements, mob-map updates,
+ and monster kill counts.
+- `tests/craftax_step_do_action_test.py` builds a temporary C wrapper around the
+ inline helper and compares full copied states against the installed JAX
+ function for 16 reset-plus-step-through seeds. Coverage includes a seeded
+ no-op-then-DO sequence, mining success and missing-pickaxe cases, sapling RNG
+ rolls, plant/passive food and water/fountain drink cases, all chest levels,
+ all passive/melee/ranged kill achievement mappings, damage modifier cases,
+ out-of-bounds targets, no-op target blocks, projectile-occupied targets, and
+ mob-on-chest gating.
+
+Native-step roadmap checklist:
+
+- [x] Native reset PRNG, noise, 9-floor world generation, and reset observation.
+- [x] Standalone native simple step subsystems with JAX-parity tests.
+- [x] Standalone native medium step subsystems with JAX-parity tests.
+- [x] Standalone native crafting and placement subsystems with JAX-parity tests.
+- [x] Standalone native `do_action` subsystem with JAX-parity tests.
+- [ ] Standalone native ports for the remaining mob step subsystems:
+ `update_mobs` and `spawn_mobs`.
+- [ ] Native reward, terminal, timestep, light-level, RNG, and achievement-delta
+ bookkeeping around the subsystem calls.
+- [ ] Integrate all green subsystem ports into a native `c_step` behind one
+ explicit switch, then remove the Python/JAX proxy from the normal step path.
+- [ ] Restore production vector sizes in `config/ocean/craftax.ini` after native
+ step is the default.
+- [ ] Benchmark CPU throughput only after the proxy path is gone.
+
+Remaining proxy paths:
+
+- `c_step` still delegates to the Python/JAX proxy. None of the standalone
+ subsystem helpers are wired into the live environment yet.
+- The only gameplay step subsystems still without standalone native ports are
+ `update_mobs` and `spawn_mobs`. Reward/terminal bookkeeping, light-level
+ updates, timestep updates, RNG threading between subsystems, and
+ achievement-delta logging are also still not integrated natively.
+- Rendering remains a no-op.
+- `config/ocean/craftax.ini` still uses a small proxy-friendly vector size. The
+ native port should raise this once step no longer calls Python.
+
+## 2026-04-18 Standalone Crafting And Placement Step Subsystems
+
+This phase adds native C ports for two more action subsystems, still
+deliberately without integrating them into `c_step`. The live Ocean environment
+continues to delegate step to the Python/JAX proxy.
+
+- `step_crafting.h` contains standalone in-place helpers for:
+ - `do_crafting`
+ - `place_block`
+ - `add_new_growing_plant`, used by plant placement and exposed to the test
+ wrapper as a translation-unit-local helper
+- `do_crafting` mirrors the JAX recipe order and sequential inventory updates
+ for all twelve `MAKE_*` actions present in the current Action enum:
+ pickaxes, swords, iron/diamond armour, arrows, and torches.
+- `place_block` mirrors table, furnace, stone, plant, and torch placement,
+ including original-block placement tests, item-map gating, mob/out-of-bounds
+ rollback, first-empty growing-plant slot selection, and the padded 9x9 torch
+ light update near map boundaries.
+- `tests/craftax_step_crafting_test.py` builds a temporary C wrapper around the
+ inline helpers and compares each subsystem against the installed JAX function
+ on reset-plus-step-through states for 16 seeds. Coverage includes success,
+ missing-resource/tool-cap, missing-station crafting cases; every JAX-legal
+ placement target block for each placement action; illegal wall/item/mob/water
+ cases where applicable; map-boundary rollback; and direct first-available-slot
+ checks for growing plants.
+
+Native-step roadmap checklist:
+
+- [x] Native reset PRNG, noise, 9-floor world generation, and reset observation.
+- [x] Standalone native simple step subsystems with JAX-parity tests.
+- [x] Standalone native medium step subsystems with JAX-parity tests.
+- [x] Standalone native crafting and placement subsystems with JAX-parity tests.
+- [x] Standalone native `do_action` subsystem with JAX-parity tests.
+- [ ] Standalone native ports for the remaining mob step subsystems:
+ `update_mobs` and `spawn_mobs`.
+- [ ] Native reward, terminal, timestep, light-level, RNG, and achievement-delta
+ bookkeeping around the subsystem calls.
+- [ ] Integrate all green subsystem ports into a native `c_step` behind one
+ explicit switch, then remove the Python/JAX proxy from the normal step path.
+- [ ] Restore production vector sizes in `config/ocean/craftax.ini` after native
+ step is the default.
+- [ ] Benchmark CPU throughput only after the proxy path is gone.
+
+Remaining proxy paths:
+
+- `c_step` still delegates to the Python/JAX proxy. None of the standalone
+ subsystem helpers are wired into the live environment yet.
+- The only gameplay step subsystems still without standalone native ports are
+ `update_mobs` and `spawn_mobs`. Reward/terminal bookkeeping, light-level
+ updates, timestep updates, RNG threading, and achievement-delta logging are
+ also still not integrated natively.
+- Rendering remains a no-op.
+- `config/ocean/craftax.ini` still uses a small proxy-friendly vector size. The
+ native port should raise this once step no longer calls Python.
+
+## 2026-04-18 Standalone Medium Step Subsystems
+
+This phase adds native C ports for five more step subsystems, again deliberately
+without integrating them into `c_step`. The live Ocean environment still
+delegates step to the Python/JAX proxy, so the full parity harness should remain
+unchanged.
+
+- `step_medium.h` contains standalone in-place helpers for:
+ - `shoot_projectile`
+ - `cast_spell`
+ - `enchant`
+ - `change_floor`
+ - `add_items_from_chest`
+- `add_items_from_chest` takes read-only `CraftaxState` context plus the
+ `CraftaxInventory` being mutated because the JAX helper's special chest drops
+ depend on `player_level` and `chests_opened`.
+- `tests/craftax_step_medium_test.py` builds a temporary C wrapper around the
+ inline helpers and compares each subsystem against the installed JAX function
+ on copied reset-plus-step-through states for 16 seeds and targeted cases:
+ projectile slot and resource gating, learned/unlearned spells, enchantment
+ table/gem/mana/item gating, every floor transition direction, and chest potion
+ and special-drop paths.
+- The helpers do not allocate, do not call Python, and preserve the JAX details
+ that matter for these routines, including clamped gather-style indexing,
+ first-free projectile slot selection, cumulative-probability `choice` with
+ `1 - uniform`, sequential Threefry split ordering, and the chest helper's
+ intentionally unused wood roll.
+
+Native-step roadmap checklist:
+
+- [x] Native reset PRNG, noise, 9-floor world generation, and reset observation.
+- [x] Standalone native simple step subsystems with JAX-parity tests.
+- [x] Standalone native medium step subsystems with JAX-parity tests.
+- [x] Standalone native crafting and placement subsystems with JAX-parity tests.
+- [x] Standalone native `do_action` subsystem with JAX-parity tests.
+- [ ] Standalone native ports for the remaining mob step subsystems:
+ `update_mobs` and `spawn_mobs`.
+- [ ] Native reward, terminal, timestep, light-level, RNG, and achievement-delta
+ bookkeeping around the subsystem calls.
+- [ ] Integrate all green subsystem ports into a native `c_step` behind one
+ explicit switch, then remove the Python/JAX proxy from the normal step path.
+- [ ] Restore production vector sizes in `config/ocean/craftax.ini` after native
+ step is the default.
+- [ ] Benchmark CPU throughput only after the proxy path is gone.
+
+Remaining proxy paths:
+
+- `c_step` still delegates to the Python/JAX proxy. None of the new medium
+ helpers are wired into the live environment yet.
+- The only gameplay step subsystems still without standalone native ports are
+ `update_mobs` and `spawn_mobs`. Reward/terminal bookkeeping, light-level
+ updates, timestep updates, RNG threading, and achievement-delta logging are
+ also still not integrated natively.
+- Rendering remains a no-op.
+- `config/ocean/craftax.ini` still uses a small proxy-friendly vector size. The
+ native port should raise this once step no longer calls Python.
+
+## 2026-04-18 Standalone Simple Step Subsystems
+
+This phase adds native C ports for the easy step subsystems, but deliberately
+does not integrate them into `c_step`. The live Ocean environment still delegates
+step to the Python/JAX proxy, so the full parity harness should remain unchanged.
+
+- `step_simple.h` contains standalone in-place helpers for:
+ - `move_player`
+ - `update_plants`
+ - `boss_logic`
+ - `level_up_attributes`
+ - `clip_inventory_and_intrinsics`
+ - `calculate_inventory_achievements`
+ - `update_player_intrinsics`
+ - `drink_potion`
+ - `read_book`
+- `tests/craftax_state_fixtures.py` provides test-only pickle payloads for JAX
+ `EnvState` values, a ctypes mirror of `CraftaxState`, C-to-JAX conversion, and
+ strict state diffing with exact integer/bool checks and `atol=1e-6` float
+ checks.
+- `tests/craftax_step_subsystem_test.py` builds a temporary C wrapper around the
+ inline helpers and compares each subsystem against the JAX function on copied
+ reset-plus-step-through states for 16 seeds and targeted stress cases.
+- The helpers do not allocate, do not call Python, and keep JAX details that
+ matter for these routines, including clamped gather-style indexing, `where` and
+ `select` ordering, potion `-1` indexing, and the `read_book` split plus
+ probability-choice path.
+
+Native-step roadmap checklist:
+
+- [x] Native reset PRNG, noise, 9-floor world generation, and reset observation.
+- [x] Standalone native simple step subsystems with JAX-parity tests.
+- [x] Standalone native medium step subsystems with JAX-parity tests.
+- [x] Standalone native crafting and placement subsystems with JAX-parity tests.
+- [x] Standalone native `do_action` subsystem with JAX-parity tests.
+- [ ] Standalone native ports for the remaining mob step subsystems:
+ `update_mobs` and `spawn_mobs`.
+- [ ] Native reward, terminal, timestep, light-level, RNG, and achievement-delta
+ bookkeeping around the subsystem calls.
+- [ ] Integrate all green subsystem ports into a native `c_step` behind one
+ explicit switch, then remove the Python/JAX proxy from the normal step path.
+- [ ] Restore production vector sizes in `config/ocean/craftax.ini` after native
+ step is the default.
+- [ ] Benchmark CPU throughput only after the proxy path is gone.
+
+## 2026-04-18 Native 9-Floor Reset Worldgen
+
+This phase replaces the JAX reset call with native C reset world generation for
+the default `Craftax-Symbolic-v1` environment parameters.
+
+- `worldgen.h` now mirrors `generate_world` for all nine floors:
+ - floor 0 overworld smoothworld
+ - floor 1 dungeon
+ - floor 2 gnomish mines smoothworld
+ - floor 3 sewers dungeon
+ - floor 4 vaults dungeon
+ - floor 5 troll mines smoothworld
+ - floor 6 fire smoothworld
+ - floor 7 ice smoothworld
+ - floor 8 boss smoothworld
+- Native reset generation covers `map`, `item_map`, `mob_map`, `light_map`,
+ ladders, chest flags, `monsters_killed[0] = 10`, empty mob/projectile arrays,
+ projectile directions, empty plants, the random `potion_mapping`, `state_rng`,
+ and the scalar reset fields used by symbolic observations.
+- `craftax_encode_reset_observation` encodes the native reset state into the
+ flat symbolic observation, so `c_reset` no longer imports Python or calls JAX.
+- `tests/craftax_worldgen_test.py` compares the native C reset state against JAX
+ `generate_world` for 16 seeds, with exact map/item/ladder/potion/scalar checks
+ and `atol=1e-6` for light and float state.
+- The Python/JAX proxy is still used for `c_step`. Because step state is still
+ JAX-owned, native `c_reset` marks the proxy dirty and the first delegated step
+ lazily calls the proxy reset before applying the action. This keeps reset
+ Python-free while preserving current step parity.
+
+Remaining proxy paths:
+
+- All step logic, rewards, achievements, auto-reset behavior after a delegated
+ step, mob updates, inventory updates, and logging data still come from the
+ Python/JAX proxy.
+- `c_step` still allocates through Python/JAX and serializes on the GIL. The
+ next porting phase should move gameplay state transitions native and remove
+ the lazy step-side proxy reset.
+- Rendering remains a no-op.
+- `config/ocean/craftax.ini` still uses a small proxy-friendly vector size. The
+ native port should raise this once step no longer calls Python.
+
+## 2026-04-18 Native Floor-0 Reset Slice
+
+This phase added the first native C replacement pieces while keeping the JAX
+proxy as the oracle for all live game state and step logic.
+
+- `threefry.h` ports JAX's `threefry2x32` PRNG for uint32 seeds, including
+ `PRNGKey(seed)`, partitionable `split`/`split_n`, `fold_in`, and
+ `uniform_u32`/float32 uniform helpers. `tests/craftax_threefry_test.py`
+ compares bitwise against `jax.random.PRNGKey`, `split`, `fold_in`, and
+ `bits`.
+- `noise.h` ports `craftax/craftax/util/noise.py` for Perlin and fractal 2D
+ noise. The test uses soft parity because C `sinf`/`cosf` and XLA
+ transcendental lowering can differ by a few ulps; no JAX FFT path is used.
+ `tests/craftax_noise_test.py` enforces `atol=rtol=2e-6`.
+- `worldgen.h` ports default overworld `generate_smoothworld` for floor 0:
+ `map`, `item_map`, `light_map`, `ladder_down`, and `ladder_up`.
+ `tests/craftax_worldgen_floor0_test.py` compares these arrays against JAX for
+ default reset seeds.
+- `c_reset` still calls the JAX proxy to build the full observation and retain
+ the JAX-owned state, then overwrites the visible floor-0 map/item/light
+ observation channels from native C. Because native floor-0 generation matches
+ the JAX reset data for default seeds, end-to-end step parity remains intact.
+
+Remaining proxy paths:
+
+- Floors 1..8 are still generated by JAX.
+- The live `EnvState`, all step logic, rewards, achievements, auto-reset, mobs,
+ inventory, and logging data still come from the Python/JAX proxy.
+- The native floor-0 arrays are not yet installed into the JAX state object;
+ this is safe only because the native generator currently matches the JAX
+ oracle for the covered default reset path.
+
+## Current Implementation
+
+`ocean/craftax/` is wired as a full Craftax Ocean environment with the correct
+symbolic observation size (`8268`) and action count (`43`). The C header declares
+the full Craftax enum set and an `EnvState`-shaped C struct matching the field
+order in `craftax_state.py`.
+
+Reset is native for the full initial `generate_world` state and symbolic
+observation. Step remains reference-backed: the C env acquires the Python GIL,
+calls the installed JAX `Craftax-Symbolic-v1` implementation, and copies the
+resulting float32 observation, reward, terminal flag, and terminal achievement
+log into PufferLib-owned buffers. After a native reset, the first delegated step
+performs a proxy reset internally so the JAX-owned step state starts from the
+same seed and remains aligned with the native reset observation.
+
+## Deliberate Divergences From The Requested Native Port
+
+- The Craftax game logic is not yet native C. Step logic, achievements, rewards,
+ auto-reset behavior after delegated steps, mobs, inventory updates, and other
+ transition logic are delegated to the JAX oracle.
+- `c_step` allocates through Python/JAX and serializes on the GIL. This violates
+ the final performance target and the intended no-allocation step path.
+- `c_close` asks the proxy to drop JAX arrays, then intentionally leaks the small
+ Python proxy wrapper objects. DECREFing JAX/XLA-owned wrappers during
+ PufferLib shutdown segfaulted in the proxy baseline; the native port removes
+ this path.
+- Rendering is a no-op.
+- `config/ocean/craftax.ini` uses a small proxy-friendly vector size. The native
+ port should raise this once step no longer calls Python.
+
+## Known Risks
+
+- Training throughput is expected to be poor. This baseline is for parity and ABI
+ validation, not for the Ryzen 9950X3D optimization target.
+- `uv run puffer train craftax` currently reaches rollout/train work, but a
+ 128-step smoke run exits with code 139 during shutdown. The parity harness and
+ direct `VecEnv` close path exit cleanly; this appears specific to the GPU
+ trainer plus proxy/JAX runtime cleanup.
+- The helper forces `JAX_PLATFORM_NAME=cpu` before importing JAX to avoid using
+ the shared GPU from inside environment steps.
+- `build.sh` now embeds rpaths for wheel-provided CUDA libraries so
+ `pufferlib._C` can find `libnccl.so.2`. The parity harness still preloads NCCL
+ defensively for older local builds.
+
+## Next Native Port Steps
+
+1. Replace one step subsystem at a time with native logic and keep the proxy as a
+ local oracle until each subsystem matches.
+2. Remove Python/JAX calls from `c_step`, restore large vector sizes, then measure
+ CPU throughput before optimizing observation encoding, mob updates, and light
+ propagation.
diff --git a/ocean/craftax/binding.c b/ocean/craftax/binding.c
new file mode 100644
index 0000000000..bbed325951
--- /dev/null
+++ b/ocean/craftax/binding.c
@@ -0,0 +1,160 @@
+#define CRAFTAX_ENABLE_ENV_IMPL
+#include "craftax.h"
+#include "step_crafting.h"
+#include "step_update_mobs.h"
+#include "step_spawn_mobs.h"
+
+#define OBS_SIZE CRAFTAX_OBS_SIZE
+#define NUM_ATNS 1
+#define ACT_SIZES {CRAFTAX_NUM_ACTIONS}
+#define OBS_TENSOR_T FloatTensor
+
+#define CRAFTAX_VEC_TILE_SIZE 128
+#define MY_VEC_INIT
+#define MY_VEC_CLOSE
+#define MY_VEC_STEP craftax_vec_step
+#define MY_VEC_STEP_RANGE craftax_vec_step_range
+#define Env Craftax
+#include "vecenv.h"
+
+// Tiled vector step: process agents in tiles that fit comfortably in cache.
+// Each thread processes a contiguous block of lightweight env handles while
+// the heavier CraftaxState storage lives in a separate arena.
+void craftax_vec_step(StaticVec* vec) {
+ memset(vec->rewards, 0, vec->total_agents * sizeof(float));
+ memset(vec->terminals, 0, vec->total_agents * sizeof(float));
+ Craftax* envs = (Craftax*)vec->envs;
+ int size = vec->size;
+ #pragma omp parallel for schedule(static)
+ for (int tile = 0; tile < size; tile += CRAFTAX_VEC_TILE_SIZE) {
+ int end = tile + CRAFTAX_VEC_TILE_SIZE;
+ if (end > size) end = size;
+ for (int i = tile; i < end; i++) {
+ c_step_gameplay(&envs[i]);
+ c_step_encode(&envs[i]);
+ }
+ }
+}
+
+void craftax_vec_step_range(StaticVec* vec, int env_start, int env_count, int num_workers) {
+ (void)num_workers;
+ Craftax* envs = (Craftax*)vec->envs;
+ int env_end = env_start + env_count;
+ for (int tile = env_start; tile < env_end; tile += CRAFTAX_VEC_TILE_SIZE) {
+ int end = tile + CRAFTAX_VEC_TILE_SIZE;
+ if (end > env_end) end = env_end;
+ for (int i = tile; i < end; i++) {
+ c_step_gameplay(&envs[i]);
+ c_step_encode(&envs[i]);
+ }
+ }
+}
+
+static CraftaxState* craftax_alloc_state_arena(int num_envs) {
+ return (CraftaxState*)calloc((size_t)num_envs, sizeof(CraftaxState));
+}
+
+Env* my_vec_init(
+ int* num_envs_out,
+ int* buffer_env_starts,
+ int* buffer_env_counts,
+ Dict* vec_kwargs,
+ Dict* env_kwargs
+) {
+ int total_agents = (int)dict_get(vec_kwargs, "total_agents")->value;
+ int num_buffers = (int)dict_get(vec_kwargs, "num_buffers")->value;
+ int agents_per_buffer = total_agents / num_buffers;
+ int num_envs = total_agents;
+
+ Env* envs = (Env*)calloc((size_t)num_envs, sizeof(Env));
+ CraftaxArena* arena = (CraftaxArena*)calloc(1, sizeof(CraftaxArena));
+ arena->states = craftax_alloc_state_arena(num_envs);
+ arena->num_envs = num_envs;
+ arena->packet_size = CRAFTAX_ARENA_PACKET_SIZE;
+ arena->num_packets = (num_envs + CRAFTAX_ARENA_PACKET_SIZE - 1)
+ / CRAFTAX_ARENA_PACKET_SIZE;
+
+ int buf = 0;
+ int buf_agents = 0;
+ buffer_env_starts[0] = 0;
+ buffer_env_counts[0] = 0;
+
+ for (int i = 0; i < num_envs; i++) {
+ Env* env = &envs[i];
+ env->rng = (unsigned int)i;
+ env->arena = arena;
+ env->state = &arena->states[i];
+ env->packet_id = i / arena->packet_size;
+ env->lane_id = i % arena->packet_size;
+ env->owns_state_storage = false;
+ my_init(env, env_kwargs);
+
+ buf_agents += env->num_agents;
+ buffer_env_counts[buf]++;
+ if (buf_agents >= agents_per_buffer && buf < num_buffers - 1) {
+ buf++;
+ buffer_env_starts[buf] = i + 1;
+ buffer_env_counts[buf] = 0;
+ buf_agents = 0;
+ }
+ }
+
+ *num_envs_out = num_envs;
+ return envs;
+}
+
+void my_vec_close(Env* envs) {
+ if (envs == NULL || envs[0].arena == NULL) {
+ return;
+ }
+
+ CraftaxArena* arena = envs[0].arena;
+ free(arena->states);
+ free(arena);
+}
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+
+ uint64_t seed_offset = 0;
+ DictItem* item = dict_get_unsafe(kwargs, "seed_offset");
+ if (item != NULL) {
+ seed_offset = (uint64_t)item->value;
+ }
+ env->seed = seed_offset + (uint64_t)env->rng;
+
+ // Process-wide reset pool (first caller wins, rest block until ready).
+ // 0 disables caching -- regenerate every reset (exact parity mode).
+ int reset_pool_size = 0;
+ DictItem* pool_item = dict_get_unsafe(kwargs, "reset_pool_size");
+ if (pool_item != NULL) reset_pool_size = (int)pool_item->value;
+ craftax_set_reset_pool_size(reset_pool_size);
+
+ c_init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+
+ // Log 8 checkpoint achievements that form the tech / exploration curve.
+ // perf (above) already aggregates all 67 into a normalized score; the
+ // individual lines here are the milestones worth watching on a dashboard.
+ // The env still tracks all 67 internally for reward and perf; we just
+ // don't send every one through the log Dict.
+ struct { const char* name; int idx; } checkpoints[] = {
+ {"collect_wood", 0},
+ {"make_wood_pickaxe", 5},
+ {"make_stone_pickaxe", 13},
+ {"collect_iron", 18},
+ {"make_iron_pickaxe", 20},
+ {"collect_diamond", 19},
+ {"enter_gnomish_mines", 28},
+ {"defeat_necromancer", 48},
+ };
+ for (int i = 0; i < (int)(sizeof(checkpoints) / sizeof(checkpoints[0])); i++) {
+ dict_set(out, checkpoints[i].name, log->achievements[checkpoints[i].idx]);
+ }
+}
diff --git a/ocean/craftax/craftax.c b/ocean/craftax/craftax.c
new file mode 100644
index 0000000000..cec3eb2cec
--- /dev/null
+++ b/ocean/craftax/craftax.c
@@ -0,0 +1,76 @@
+// Standalone viewer for Craftax (random-action policy).
+//
+// Build:
+// ./build.sh craftax --fast # optimized
+// ./build.sh craftax --local # debug with sanitizers
+// Run:
+// ./craftax
+
+#define CRAFTAX_ENABLE_ENV_IMPL
+#include "craftax.h"
+#include "step_crafting.h"
+#include "step_update_mobs.h"
+#include "step_spawn_mobs.h"
+
+#include
+#include
+#include
+
+static uint32_t xorshift32(uint32_t* s) {
+ uint32_t x = *s;
+ x ^= x << 13; x ^= x >> 17; x ^= x << 5;
+ *s = x ? x : 0xdeadbeef;
+ return x;
+}
+
+int main(int argc, char** argv) {
+ uint64_t seed = (argc > 1) ? strtoull(argv[1], NULL, 10) : (uint64_t)time(NULL);
+
+ Craftax env;
+ memset(&env, 0, sizeof(env));
+ env.num_agents = 1;
+ env.seed = seed;
+ env.rng = (uint32_t)seed;
+
+ // Minimal buffers for a single agent
+ env.observations = calloc(CRAFTAX_OBS_SIZE, sizeof(float));
+ env.actions = calloc(1, sizeof(float));
+ env.rewards = calloc(1, sizeof(float));
+ env.terminals = calloc(1, sizeof(float));
+
+ c_init(&env);
+ c_reset(&env);
+
+ uint32_t action_rng = (uint32_t)(seed ^ 0x9E3779B9u);
+ bool human_control = false;
+ int human_action = CRAFTAX_ACTION_NOOP;
+
+ while (!WindowShouldClose()) {
+ // Toggle human control
+ if (IsKeyPressed(KEY_H)) human_control = !human_control;
+
+ if (human_control) {
+ human_action = CRAFTAX_ACTION_NOOP;
+ if (IsKeyPressed(KEY_A) || IsKeyPressed(KEY_LEFT)) human_action = CRAFTAX_ACTION_LEFT;
+ if (IsKeyPressed(KEY_D) || IsKeyPressed(KEY_RIGHT)) human_action = CRAFTAX_ACTION_RIGHT;
+ if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) human_action = CRAFTAX_ACTION_UP;
+ if (IsKeyPressed(KEY_S) || IsKeyPressed(KEY_DOWN)) human_action = CRAFTAX_ACTION_DOWN;
+ if (IsKeyPressed(KEY_SPACE)) human_action = CRAFTAX_ACTION_DO;
+ if (IsKeyPressed(KEY_Z)) human_action = CRAFTAX_ACTION_SLEEP;
+ env.actions[0] = (float)human_action;
+ if (human_action != CRAFTAX_ACTION_NOOP || IsKeyPressed(KEY_PERIOD)) c_step(&env);
+ } else {
+ env.actions[0] = (float)(xorshift32(&action_rng) % CRAFTAX_NUM_ACTIONS);
+ c_step(&env);
+ }
+
+ c_render(&env);
+ }
+
+ c_close(&env);
+ free(env.observations);
+ free(env.actions);
+ free(env.rewards);
+ free(env.terminals);
+ return 0;
+}
diff --git a/ocean/craftax/craftax.h b/ocean/craftax/craftax.h
new file mode 100644
index 0000000000..48d64bf22f
--- /dev/null
+++ b/ocean/craftax/craftax.h
@@ -0,0 +1,1177 @@
+// Full native Craftax environment for PufferLib Ocean.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "worldgen.h"
+#include "raylib.h"
+#include
+#include
+#include
+
+// ============================================================
+// Optional step profiling (compile with -DCRAFTAX_PROFILE)
+// ============================================================
+#ifdef CRAFTAX_PROFILE
+
+#define CRAFTAX_NUM_PROFILE_ZONES 18
+
+typedef struct {
+ const char* name;
+ uint64_t total_ns;
+ uint64_t count;
+} CraftaxProfileZone;
+
+static CraftaxProfileZone craftax_profile_zones[CRAFTAX_NUM_PROFILE_ZONES] = {
+ {"change_floor", 0, 0},
+ {"crafting", 0, 0},
+ {"do_action", 0, 0},
+ {"place+shoot+spell+potion", 0, 0},
+ {"read_book", 0, 0},
+ {"enchant", 0, 0},
+ {"boss+attr+move", 0, 0},
+ {"update_mobs", 0, 0},
+ {"spawn_mobs", 0, 0},
+ {"plants+intrinsics+achieve", 0, 0},
+ {"reward+bookkeeping", 0, 0},
+ {"encode_obs", 0, 0},
+ {"rng_split", 0, 0},
+ {"is_game_over", 0, 0},
+ {"reset_on_done", 0, 0},
+ {"copy_achievements", 0, 0},
+ {"reward_bookkeeping", 0, 0},
+ {"unprofiled", 0, 0},
+};
+
+static inline uint64_t craftax_profile_now(void) {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
+}
+
+static inline void craftax_profile_record(int zone, uint64_t start) {
+ craftax_profile_zones[zone].total_ns += craftax_profile_now() - start;
+ craftax_profile_zones[zone].count++;
+}
+
+static inline void craftax_profile_report(void) {
+ fprintf(stderr, "\n=== Craftax Step Profile ===\n");
+ uint64_t total = 0;
+ for (int i = 0; i < CRAFTAX_NUM_PROFILE_ZONES; i++) {
+ total += craftax_profile_zones[i].total_ns;
+ }
+ for (int i = 0; i < CRAFTAX_NUM_PROFILE_ZONES; i++) {
+ CraftaxProfileZone* z = &craftax_profile_zones[i];
+ if (z->count == 0) continue;
+ double pct = total > 0 ? (100.0 * (double)z->total_ns / (double)total) : 0.0;
+ double avg_us = (double)z->total_ns / (double)z->count / 1000.0;
+ fprintf(stderr, "%-28s %8.3f%% %10.2f us/step (%lu calls)\n",
+ z->name, pct, avg_us, (unsigned long)z->count);
+ }
+ fprintf(stderr, "%-28s %8.3f%% %10.2f us/step\n",
+ "TOTAL", 100.0, (double)total / (double)craftax_profile_zones[0].count / 1000.0);
+}
+
+#define CRAFTAX_PROFILE_START() uint64_t _prof_start = craftax_profile_now(); uint64_t _prof_zone_start;
+#define CRAFTAX_PROFILE_ZONE(n) do { _prof_zone_start = craftax_profile_now(); } while(0)
+#define CRAFTAX_PROFILE_END(n) craftax_profile_record((n), _prof_zone_start)
+#define CRAFTAX_PROFILE_FINAL(n) craftax_profile_record((n), _prof_start)
+
+#else
+
+#define CRAFTAX_PROFILE_START() ((void)0)
+#define CRAFTAX_PROFILE_ZONE(n) ((void)0)
+#define CRAFTAX_PROFILE_END(n) ((void)0)
+#define CRAFTAX_PROFILE_FINAL(n) ((void)0)
+#define craftax_profile_report() ((void)0)
+
+#endif // CRAFTAX_PROFILE
+
+// ============================================================
+// Constants
+// ============================================================
+#define CRAFTAX_OBS_ROWS 9
+#define CRAFTAX_OBS_COLS 11
+#define CRAFTAX_MAP_SIZE 48
+#define CRAFTAX_NUM_LEVELS 9
+
+#define CRAFTAX_NUM_BLOCK_TYPES 37
+#define CRAFTAX_NUM_ITEM_TYPES 5
+#define CRAFTAX_NUM_MOB_CLASSES 5
+#define CRAFTAX_NUM_MOB_TYPES 8
+#define CRAFTAX_INVENTORY_OBS_SIZE 51
+#define CRAFTAX_OBS_SIZE CRAFTAX_WG_OBS_SIZE
+
+#define CRAFTAX_NUM_ACTIONS 43
+#define CRAFTAX_NUM_ACHIEVEMENTS 67
+
+#define CRAFTAX_MAX_MELEE_MOBS 3
+#define CRAFTAX_MAX_PASSIVE_MOBS 3
+#define CRAFTAX_MAX_RANGED_MOBS 2
+#define CRAFTAX_MAX_MOB_PROJECTILES 3
+#define CRAFTAX_MAX_PLAYER_PROJECTILES 3
+#define CRAFTAX_MAX_GROWING_PLANTS 10
+
+#define CRAFTAX_DEFAULT_MAX_TIMESTEPS 100000
+#define CRAFTAX_DAY_LENGTH 300
+#define CRAFTAX_MAX_ATTRIBUTE 5
+#define CRAFTAX_MOB_DESPAWN_DISTANCE 14
+#define CRAFTAX_MONSTERS_KILLED_TO_CLEAR_LEVEL 8
+
+// ============================================================
+// Enums copied from craftax/craftax/constants.py
+// ============================================================
+typedef enum CraftaxBlockType {
+ CRAFTAX_BLOCK_INVALID = 0,
+ CRAFTAX_BLOCK_OUT_OF_BOUNDS = 1,
+ CRAFTAX_BLOCK_GRASS = 2,
+ CRAFTAX_BLOCK_WATER = 3,
+ CRAFTAX_BLOCK_STONE = 4,
+ CRAFTAX_BLOCK_TREE = 5,
+ CRAFTAX_BLOCK_WOOD = 6,
+ CRAFTAX_BLOCK_PATH = 7,
+ CRAFTAX_BLOCK_COAL = 8,
+ CRAFTAX_BLOCK_IRON = 9,
+ CRAFTAX_BLOCK_DIAMOND = 10,
+ CRAFTAX_BLOCK_CRAFTING_TABLE = 11,
+ CRAFTAX_BLOCK_FURNACE = 12,
+ CRAFTAX_BLOCK_SAND = 13,
+ CRAFTAX_BLOCK_LAVA = 14,
+ CRAFTAX_BLOCK_PLANT = 15,
+ CRAFTAX_BLOCK_RIPE_PLANT = 16,
+ CRAFTAX_BLOCK_WALL = 17,
+ CRAFTAX_BLOCK_DARKNESS = 18,
+ CRAFTAX_BLOCK_WALL_MOSS = 19,
+ CRAFTAX_BLOCK_STALAGMITE = 20,
+ CRAFTAX_BLOCK_SAPPHIRE = 21,
+ CRAFTAX_BLOCK_RUBY = 22,
+ CRAFTAX_BLOCK_CHEST = 23,
+ CRAFTAX_BLOCK_FOUNTAIN = 24,
+ CRAFTAX_BLOCK_FIRE_GRASS = 25,
+ CRAFTAX_BLOCK_ICE_GRASS = 26,
+ CRAFTAX_BLOCK_GRAVEL = 27,
+ CRAFTAX_BLOCK_FIRE_TREE = 28,
+ CRAFTAX_BLOCK_ICE_SHRUB = 29,
+ CRAFTAX_BLOCK_ENCHANTMENT_TABLE_FIRE = 30,
+ CRAFTAX_BLOCK_ENCHANTMENT_TABLE_ICE = 31,
+ CRAFTAX_BLOCK_NECROMANCER = 32,
+ CRAFTAX_BLOCK_GRAVE = 33,
+ CRAFTAX_BLOCK_GRAVE2 = 34,
+ CRAFTAX_BLOCK_GRAVE3 = 35,
+ CRAFTAX_BLOCK_NECROMANCER_VULNERABLE = 36,
+} CraftaxBlockType;
+
+typedef enum CraftaxItemType {
+ CRAFTAX_ITEM_NONE = 0,
+ CRAFTAX_ITEM_TORCH = 1,
+ CRAFTAX_ITEM_LADDER_DOWN = 2,
+ CRAFTAX_ITEM_LADDER_UP = 3,
+ CRAFTAX_ITEM_LADDER_DOWN_BLOCKED = 4,
+} CraftaxItemType;
+
+typedef enum CraftaxAction {
+ CRAFTAX_ACTION_NOOP = 0,
+ CRAFTAX_ACTION_LEFT = 1,
+ CRAFTAX_ACTION_RIGHT = 2,
+ CRAFTAX_ACTION_UP = 3,
+ CRAFTAX_ACTION_DOWN = 4,
+ CRAFTAX_ACTION_DO = 5,
+ CRAFTAX_ACTION_SLEEP = 6,
+ CRAFTAX_ACTION_PLACE_STONE = 7,
+ CRAFTAX_ACTION_PLACE_TABLE = 8,
+ CRAFTAX_ACTION_PLACE_FURNACE = 9,
+ CRAFTAX_ACTION_PLACE_PLANT = 10,
+ CRAFTAX_ACTION_MAKE_WOOD_PICKAXE = 11,
+ CRAFTAX_ACTION_MAKE_STONE_PICKAXE = 12,
+ CRAFTAX_ACTION_MAKE_IRON_PICKAXE = 13,
+ CRAFTAX_ACTION_MAKE_WOOD_SWORD = 14,
+ CRAFTAX_ACTION_MAKE_STONE_SWORD = 15,
+ CRAFTAX_ACTION_MAKE_IRON_SWORD = 16,
+ CRAFTAX_ACTION_REST = 17,
+ CRAFTAX_ACTION_DESCEND = 18,
+ CRAFTAX_ACTION_ASCEND = 19,
+ CRAFTAX_ACTION_MAKE_DIAMOND_PICKAXE = 20,
+ CRAFTAX_ACTION_MAKE_DIAMOND_SWORD = 21,
+ CRAFTAX_ACTION_MAKE_IRON_ARMOUR = 22,
+ CRAFTAX_ACTION_MAKE_DIAMOND_ARMOUR = 23,
+ CRAFTAX_ACTION_SHOOT_ARROW = 24,
+ CRAFTAX_ACTION_MAKE_ARROW = 25,
+ CRAFTAX_ACTION_CAST_FIREBALL = 26,
+ CRAFTAX_ACTION_CAST_ICEBALL = 27,
+ CRAFTAX_ACTION_PLACE_TORCH = 28,
+ CRAFTAX_ACTION_DRINK_POTION_RED = 29,
+ CRAFTAX_ACTION_DRINK_POTION_GREEN = 30,
+ CRAFTAX_ACTION_DRINK_POTION_BLUE = 31,
+ CRAFTAX_ACTION_DRINK_POTION_PINK = 32,
+ CRAFTAX_ACTION_DRINK_POTION_CYAN = 33,
+ CRAFTAX_ACTION_DRINK_POTION_YELLOW = 34,
+ CRAFTAX_ACTION_READ_BOOK = 35,
+ CRAFTAX_ACTION_ENCHANT_SWORD = 36,
+ CRAFTAX_ACTION_ENCHANT_ARMOUR = 37,
+ CRAFTAX_ACTION_MAKE_TORCH = 38,
+ CRAFTAX_ACTION_LEVEL_UP_DEXTERITY = 39,
+ CRAFTAX_ACTION_LEVEL_UP_STRENGTH = 40,
+ CRAFTAX_ACTION_LEVEL_UP_INTELLIGENCE = 41,
+ CRAFTAX_ACTION_ENCHANT_BOW = 42,
+} CraftaxAction;
+
+typedef enum CraftaxMobType {
+ CRAFTAX_MOB_PASSIVE = 0,
+ CRAFTAX_MOB_MELEE = 1,
+ CRAFTAX_MOB_RANGED = 2,
+ CRAFTAX_MOB_PROJECTILE = 3,
+} CraftaxMobType;
+
+typedef enum CraftaxProjectileType {
+ CRAFTAX_PROJECTILE_ARROW = 0,
+ CRAFTAX_PROJECTILE_DAGGER = 1,
+ CRAFTAX_PROJECTILE_FIREBALL = 2,
+ CRAFTAX_PROJECTILE_ICEBALL = 3,
+ CRAFTAX_PROJECTILE_ARROW2 = 4,
+ CRAFTAX_PROJECTILE_SLIMEBALL = 5,
+ CRAFTAX_PROJECTILE_FIREBALL2 = 6,
+ CRAFTAX_PROJECTILE_ICEBALL2 = 7,
+} CraftaxProjectileType;
+
+typedef enum CraftaxAchievement {
+ CRAFTAX_ACH_COLLECT_WOOD = 0,
+ CRAFTAX_ACH_PLACE_TABLE = 1,
+ CRAFTAX_ACH_EAT_COW = 2,
+ CRAFTAX_ACH_COLLECT_SAPLING = 3,
+ CRAFTAX_ACH_COLLECT_DRINK = 4,
+ CRAFTAX_ACH_MAKE_WOOD_PICKAXE = 5,
+ CRAFTAX_ACH_MAKE_WOOD_SWORD = 6,
+ CRAFTAX_ACH_PLACE_PLANT = 7,
+ CRAFTAX_ACH_DEFEAT_ZOMBIE = 8,
+ CRAFTAX_ACH_COLLECT_STONE = 9,
+ CRAFTAX_ACH_PLACE_STONE = 10,
+ CRAFTAX_ACH_EAT_PLANT = 11,
+ CRAFTAX_ACH_DEFEAT_SKELETON = 12,
+ CRAFTAX_ACH_MAKE_STONE_PICKAXE = 13,
+ CRAFTAX_ACH_MAKE_STONE_SWORD = 14,
+ CRAFTAX_ACH_WAKE_UP = 15,
+ CRAFTAX_ACH_PLACE_FURNACE = 16,
+ CRAFTAX_ACH_COLLECT_COAL = 17,
+ CRAFTAX_ACH_COLLECT_IRON = 18,
+ CRAFTAX_ACH_COLLECT_DIAMOND = 19,
+ CRAFTAX_ACH_MAKE_IRON_PICKAXE = 20,
+ CRAFTAX_ACH_MAKE_IRON_SWORD = 21,
+ CRAFTAX_ACH_MAKE_ARROW = 22,
+ CRAFTAX_ACH_MAKE_TORCH = 23,
+ CRAFTAX_ACH_PLACE_TORCH = 24,
+ CRAFTAX_ACH_MAKE_DIAMOND_SWORD = 25,
+ CRAFTAX_ACH_MAKE_IRON_ARMOUR = 26,
+ CRAFTAX_ACH_MAKE_DIAMOND_ARMOUR = 27,
+ CRAFTAX_ACH_ENTER_GNOMISH_MINES = 28,
+ CRAFTAX_ACH_ENTER_DUNGEON = 29,
+ CRAFTAX_ACH_ENTER_SEWERS = 30,
+ CRAFTAX_ACH_ENTER_VAULT = 31,
+ CRAFTAX_ACH_ENTER_TROLL_MINES = 32,
+ CRAFTAX_ACH_ENTER_FIRE_REALM = 33,
+ CRAFTAX_ACH_ENTER_ICE_REALM = 34,
+ CRAFTAX_ACH_ENTER_GRAVEYARD = 35,
+ CRAFTAX_ACH_DEFEAT_GNOME_WARRIOR = 36,
+ CRAFTAX_ACH_DEFEAT_GNOME_ARCHER = 37,
+ CRAFTAX_ACH_DEFEAT_ORC_SOLIDER = 38,
+ CRAFTAX_ACH_DEFEAT_ORC_MAGE = 39,
+ CRAFTAX_ACH_DEFEAT_LIZARD = 40,
+ CRAFTAX_ACH_DEFEAT_KOBOLD = 41,
+ CRAFTAX_ACH_DEFEAT_TROLL = 42,
+ CRAFTAX_ACH_DEFEAT_DEEP_THING = 43,
+ CRAFTAX_ACH_DEFEAT_PIGMAN = 44,
+ CRAFTAX_ACH_DEFEAT_FIRE_ELEMENTAL = 45,
+ CRAFTAX_ACH_DEFEAT_FROST_TROLL = 46,
+ CRAFTAX_ACH_DEFEAT_ICE_ELEMENTAL = 47,
+ CRAFTAX_ACH_DAMAGE_NECROMANCER = 48,
+ CRAFTAX_ACH_DEFEAT_NECROMANCER = 49,
+ CRAFTAX_ACH_EAT_BAT = 50,
+ CRAFTAX_ACH_EAT_SNAIL = 51,
+ CRAFTAX_ACH_FIND_BOW = 52,
+ CRAFTAX_ACH_FIRE_BOW = 53,
+ CRAFTAX_ACH_COLLECT_SAPPHIRE = 54,
+ CRAFTAX_ACH_LEARN_FIREBALL = 55,
+ CRAFTAX_ACH_CAST_FIREBALL = 56,
+ CRAFTAX_ACH_LEARN_ICEBALL = 57,
+ CRAFTAX_ACH_CAST_ICEBALL = 58,
+ CRAFTAX_ACH_COLLECT_RUBY = 59,
+ CRAFTAX_ACH_MAKE_DIAMOND_PICKAXE = 60,
+ CRAFTAX_ACH_OPEN_CHEST = 61,
+ CRAFTAX_ACH_DRINK_POTION = 62,
+ CRAFTAX_ACH_ENCHANT_SWORD = 63,
+ CRAFTAX_ACH_ENCHANT_ARMOUR = 64,
+ CRAFTAX_ACH_DEFEAT_KNIGHT = 65,
+ CRAFTAX_ACH_DEFEAT_ARCHER = 66,
+} CraftaxAchievement;
+
+// ============================================================
+// State layout declarations matching craftax_state.py field order
+// ============================================================
+typedef struct CraftaxInventory {
+ int32_t wood;
+ int32_t stone;
+ int32_t coal;
+ int32_t iron;
+ int32_t diamond;
+ int32_t sapling;
+ int32_t pickaxe;
+ int32_t sword;
+ int32_t bow;
+ int32_t arrows;
+ int32_t armour[4];
+ int32_t torches;
+ int32_t ruby;
+ int32_t sapphire;
+ int32_t potions[6];
+ int32_t books;
+} CraftaxInventory;
+
+typedef struct CraftaxMobs3 {
+ int32_t position[CRAFTAX_NUM_LEVELS][3][2];
+ float health[CRAFTAX_NUM_LEVELS][3];
+ bool mask[CRAFTAX_NUM_LEVELS][3];
+ int32_t attack_cooldown[CRAFTAX_NUM_LEVELS][3];
+ int32_t type_id[CRAFTAX_NUM_LEVELS][3];
+} CraftaxMobs3;
+
+typedef struct CraftaxMobs2 {
+ int32_t position[CRAFTAX_NUM_LEVELS][2][2];
+ float health[CRAFTAX_NUM_LEVELS][2];
+ bool mask[CRAFTAX_NUM_LEVELS][2];
+ int32_t attack_cooldown[CRAFTAX_NUM_LEVELS][2];
+ int32_t type_id[CRAFTAX_NUM_LEVELS][2];
+} CraftaxMobs2;
+
+typedef struct CraftaxState {
+ // === Hot data (accessed every step) ===
+ int32_t player_position[2];
+ int32_t player_level;
+ int32_t player_direction;
+
+ float player_health;
+ int32_t player_food;
+ int32_t player_drink;
+ int32_t player_energy;
+ int32_t player_mana;
+ bool is_sleeping;
+ bool is_resting;
+
+ float player_recover;
+ float player_hunger;
+ float player_thirst;
+ float player_fatigue;
+ float player_recover_mana;
+
+ int32_t player_xp;
+ int32_t player_dexterity;
+ int32_t player_strength;
+ int32_t player_intelligence;
+
+ CraftaxInventory inventory;
+
+ CraftaxMobs3 melee_mobs;
+ CraftaxMobs3 passive_mobs;
+ CraftaxMobs2 ranged_mobs;
+
+ CraftaxMobs3 mob_projectiles;
+ int32_t mob_projectile_directions[CRAFTAX_NUM_LEVELS][CRAFTAX_MAX_MOB_PROJECTILES][2];
+ CraftaxMobs3 player_projectiles;
+ int32_t player_projectile_directions[CRAFTAX_NUM_LEVELS][CRAFTAX_MAX_PLAYER_PROJECTILES][2];
+
+ int32_t growing_plants_positions[CRAFTAX_MAX_GROWING_PLANTS][2];
+ int32_t growing_plants_age[CRAFTAX_MAX_GROWING_PLANTS];
+ bool growing_plants_mask[CRAFTAX_MAX_GROWING_PLANTS];
+
+ int32_t potion_mapping[6];
+ bool learned_spells[2];
+
+ int32_t sword_enchantment;
+ int32_t bow_enchantment;
+ int32_t armour_enchantments[4];
+
+ int32_t boss_progress;
+ int32_t boss_timesteps_to_spawn_this_round;
+
+ float light_level;
+ bool achievements[CRAFTAX_NUM_ACHIEVEMENTS];
+ uint32_t state_rng[2];
+ int32_t timestep;
+ int32_t fractal_noise_angles[4];
+
+ // === Medium-hot bitmaps, read during mob updates, spawn scans, encode_obs ===
+ uint64_t mob_bits[CRAFTAX_NUM_LEVELS][CRAFTAX_MAP_SIZE];
+ uint64_t spawn_all_bits[CRAFTAX_NUM_LEVELS][CRAFTAX_MAP_SIZE];
+ uint64_t spawn_grave_bits[CRAFTAX_NUM_LEVELS][CRAFTAX_MAP_SIZE];
+ uint64_t spawn_water_bits[CRAFTAX_NUM_LEVELS][CRAFTAX_MAP_SIZE];
+
+ // === Cold data (large maps, scattered access) ===
+ uint8_t map[CRAFTAX_NUM_LEVELS][CRAFTAX_MAP_SIZE][CRAFTAX_MAP_SIZE];
+ uint8_t item_map[CRAFTAX_NUM_LEVELS][CRAFTAX_MAP_SIZE][CRAFTAX_MAP_SIZE];
+ uint8_t light_map[CRAFTAX_NUM_LEVELS][CRAFTAX_MAP_SIZE][CRAFTAX_MAP_SIZE];
+
+ int32_t down_ladders[CRAFTAX_NUM_LEVELS][2];
+ int32_t up_ladders[CRAFTAX_NUM_LEVELS][2];
+ bool chests_opened[CRAFTAX_NUM_LEVELS];
+ int32_t monsters_killed[CRAFTAX_NUM_LEVELS];
+} CraftaxState;
+
+typedef char CraftaxStateMatchesWorldState[
+ (sizeof(CraftaxState) == sizeof(CraftaxWorldState)) ? 1 : -1
+];
+
+static inline uint64_t craftax_spawn_all_bit(uint8_t block) {
+ return (uint64_t)(
+ block == CRAFTAX_BLOCK_GRASS
+ || block == CRAFTAX_BLOCK_PATH
+ || block == CRAFTAX_BLOCK_FIRE_GRASS
+ || block == CRAFTAX_BLOCK_ICE_GRASS
+ );
+}
+
+static inline uint64_t craftax_spawn_grave_bit(uint8_t block) {
+ return (uint64_t)(
+ block == CRAFTAX_BLOCK_GRAVE
+ || block == CRAFTAX_BLOCK_GRAVE2
+ || block == CRAFTAX_BLOCK_GRAVE3
+ );
+}
+
+static inline uint64_t craftax_spawn_water_bit(uint8_t block) {
+ return (uint64_t)(block == CRAFTAX_BLOCK_WATER);
+}
+
+static inline void craftax_refresh_spawn_bits_cell(
+ CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col
+) {
+ uint64_t bit = 1ULL << col;
+ uint8_t block = state->map[level][row][col];
+
+ state->spawn_all_bits[level][row] =
+ (state->spawn_all_bits[level][row] & ~bit)
+ | ((0ULL - craftax_spawn_all_bit(block)) & bit);
+ state->spawn_grave_bits[level][row] =
+ (state->spawn_grave_bits[level][row] & ~bit)
+ | ((0ULL - craftax_spawn_grave_bit(block)) & bit);
+ state->spawn_water_bits[level][row] =
+ (state->spawn_water_bits[level][row] & ~bit)
+ | ((0ULL - craftax_spawn_water_bit(block)) & bit);
+}
+
+static inline void craftax_set_map_block(
+ CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col,
+ int32_t block
+) {
+ state->map[level][row][col] = (uint8_t)block;
+ craftax_refresh_spawn_bits_cell(state, level, row, col);
+}
+
+static inline void craftax_refresh_spawn_bits_all(CraftaxState* state) {
+ for (int32_t level = 0; level < CRAFTAX_NUM_LEVELS; level++) {
+ for (int32_t row = 0; row < CRAFTAX_MAP_SIZE; row++) {
+ uint64_t all_bits = 0;
+ uint64_t grave_bits = 0;
+ uint64_t water_bits = 0;
+ for (int32_t col = 0; col < CRAFTAX_MAP_SIZE; col++) {
+ uint8_t block = state->map[level][row][col];
+ uint64_t bit = 1ULL << col;
+ all_bits |= (0ULL - craftax_spawn_all_bit(block)) & bit;
+ grave_bits |= (0ULL - craftax_spawn_grave_bit(block)) & bit;
+ water_bits |= (0ULL - craftax_spawn_water_bit(block)) & bit;
+ }
+ state->spawn_all_bits[level][row] = all_bits;
+ state->spawn_grave_bits[level][row] = grave_bits;
+ state->spawn_water_bits[level][row] = water_bits;
+ }
+ }
+}
+
+#define CRAFTAX_ARENA_PACKET_SIZE 64
+
+typedef struct CraftaxArena {
+ CraftaxState* states;
+ int num_envs;
+ int packet_size;
+ int num_packets;
+} CraftaxArena;
+
+#ifdef CRAFTAX_ENABLE_ENV_IMPL
+static inline void craftax_change_floor_native(CraftaxState* state, int32_t action);
+static inline void craftax_do_crafting_native(CraftaxState* state, int32_t action);
+static inline void craftax_do_action_native(
+ CraftaxState* state,
+ int32_t action,
+ CraftaxThreefryKey rng
+);
+static inline void craftax_place_block_native(CraftaxState* state, int32_t action);
+static inline void craftax_shoot_projectile_native(
+ CraftaxState* state,
+ int32_t action
+);
+static inline void craftax_cast_spell_native(CraftaxState* state, int32_t action);
+static inline void craftax_drink_potion_native(CraftaxState* state, int32_t action);
+static inline void craftax_read_book_native(
+ CraftaxState* state,
+ const uint32_t rng_words[2],
+ int32_t action
+);
+static inline void craftax_enchant_native(
+ CraftaxState* state,
+ int32_t action,
+ CraftaxThreefryKey rng
+);
+static inline void craftax_boss_logic_native(CraftaxState* state);
+static inline void craftax_level_up_attributes_native(
+ CraftaxState* state,
+ int32_t action,
+ int32_t max_attribute
+);
+static inline void craftax_move_player_native(
+ CraftaxState* state,
+ int32_t action,
+ bool god_mode
+);
+static inline void craftax_update_mobs_native(
+ CraftaxState* state,
+ CraftaxThreefryKey rng
+);
+static inline void craftax_spawn_mobs_native(
+ CraftaxState* state,
+ CraftaxThreefryKey rng
+);
+static inline void craftax_update_plants_native(CraftaxState* state);
+static inline void craftax_update_player_intrinsics_native(
+ CraftaxState* state,
+ int32_t action
+);
+static inline void craftax_clip_inventory_and_intrinsics_native(
+ CraftaxState* state,
+ bool god_mode
+);
+static inline void craftax_calculate_inventory_achievements_native(
+ CraftaxState* state
+);
+#endif
+
+typedef struct Log {
+ float perf;
+ float score;
+ float episode_return;
+ float episode_length;
+ float achievements[CRAFTAX_NUM_ACHIEVEMENTS];
+ float n;
+} Log;
+
+typedef struct Client {
+ int unused;
+} Client;
+
+typedef struct Craftax {
+ Client* client;
+ Log log;
+
+ float* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ int num_agents;
+
+ unsigned int rng;
+ uint64_t seed;
+ CraftaxThreefryKey rng_key;
+ CraftaxArena* arena;
+ CraftaxState* state;
+ int32_t packet_id;
+ int32_t lane_id;
+ bool owns_state_storage;
+
+ float achievements[CRAFTAX_NUM_ACHIEVEMENTS];
+ float episode_return_accum;
+ int32_t episode_length_accum;
+} Craftax;
+
+#ifdef CRAFTAX_ENABLE_ENV_IMPL
+
+// ============================================================
+// Native reset, observation, reward, and step glue
+// ============================================================
+static const float CRAFTAX_ACHIEVEMENT_REWARD_MAP[CRAFTAX_NUM_ACHIEVEMENTS] = {
+ 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
+ 1.0f, 3.0f, 3.0f, 3.0f, 3.0f, 3.0f, 5.0f, 5.0f,
+ 5.0f, 8.0f, 8.0f, 8.0f, 3.0f, 3.0f, 3.0f, 3.0f,
+ 5.0f, 5.0f, 5.0f, 5.0f, 8.0f, 8.0f, 8.0f, 8.0f,
+ 8.0f, 8.0f, 3.0f, 3.0f, 3.0f, 3.0f, 3.0f, 5.0f,
+ 5.0f, 5.0f, 5.0f, 3.0f, 3.0f, 3.0f, 3.0f, 5.0f,
+ 5.0f, 5.0f, 5.0f,
+};
+
+static inline CraftaxThreefryKey craftax_step_native_next_key(
+ CraftaxThreefryKey* rng
+) {
+ CraftaxThreefryKey subkey;
+ craftax_threefry_split(*rng, rng, &subkey);
+ return subkey;
+}
+
+static inline void craftax_copy_world_state_to_state(
+ CraftaxState* dst,
+ const CraftaxWorldState* src
+) {
+ memcpy(dst, src, sizeof(*dst));
+}
+
+static inline void craftax_generate_state_from_world_key(
+ CraftaxThreefryKey world_key,
+ CraftaxState* out
+) {
+ CraftaxWorldState world_state;
+ craftax_generate_world_from_key(world_key, &world_state);
+ craftax_copy_world_state_to_state(out, &world_state);
+ craftax_refresh_spawn_bits_all(out);
+}
+
+static inline void craftax_reset_state_from_reset_key(
+ CraftaxState* out,
+ CraftaxThreefryKey reset_key
+) {
+ CraftaxThreefryKey unused;
+ CraftaxThreefryKey world_key;
+ craftax_threefry_split(reset_key, &unused, &world_key);
+ craftax_generate_state_from_world_key(world_key, out);
+}
+
+// ============================================================
+// Reset pool: pre-generate N worlds once, then memcpy on reset.
+// Trades world diversity (<= pool_size unique maps per process) for
+// ~500x faster reset. Set pool_size=0 to disable (exact per-seed
+// world; required for the parity harness).
+// ============================================================
+static int g_craftax_reset_pool_size = 0;
+static CraftaxState* g_craftax_reset_pool = NULL;
+static int g_craftax_reset_pool_ready = 0;
+
+// Called from my_init which runs single-threaded during env creation
+// (vecenv.h iterates envs sequentially). First caller populates the
+// pool; subsequent callers are no-ops.
+static inline void craftax_set_reset_pool_size(int n) {
+ if (g_craftax_reset_pool_ready) return;
+ g_craftax_reset_pool_size = n;
+ if (n > 0) {
+ g_craftax_reset_pool = (CraftaxState*)calloc((size_t)n, sizeof(CraftaxState));
+ for (int i = 0; i < n; i++) {
+ CraftaxThreefryKey init_key = craftax_prng_key((uint32_t)i);
+ CraftaxThreefryKey discard, reset_key;
+ craftax_threefry_split(init_key, &discard, &reset_key);
+ craftax_reset_state_from_reset_key(&g_craftax_reset_pool[i], reset_key);
+ }
+ }
+ g_craftax_reset_pool_ready = 1;
+}
+
+static inline void craftax_ensure_state_storage(Craftax* env) {
+ if (env->state != NULL) {
+ return;
+ }
+
+ CraftaxArena* arena = (CraftaxArena*)calloc(1, sizeof(CraftaxArena));
+ arena->states = (CraftaxState*)calloc(1, sizeof(CraftaxState));
+ arena->num_envs = 1;
+ arena->packet_size = 1;
+ arena->num_packets = 1;
+
+ env->arena = arena;
+ env->state = arena->states;
+ env->packet_id = 0;
+ env->lane_id = 0;
+ env->owns_state_storage = true;
+}
+
+static inline void craftax_reset_state_from_seed(Craftax* env) {
+ craftax_ensure_state_storage(env);
+ CraftaxThreefryKey initial_key = craftax_prng_key((uint32_t)env->seed);
+ if (g_craftax_reset_pool_size > 0) {
+ CraftaxThreefryKey discard;
+ craftax_threefry_split(initial_key, &env->rng_key, &discard);
+ int idx = (int)(env->seed % (uint64_t)g_craftax_reset_pool_size);
+ memcpy(env->state, &g_craftax_reset_pool[idx], sizeof(CraftaxState));
+ return;
+ }
+ CraftaxThreefryKey reset_key;
+ craftax_threefry_split(initial_key, &env->rng_key, &reset_key);
+ craftax_reset_state_from_reset_key(env->state, reset_key);
+}
+
+// Hot-path reset used by c_step on episode-done. Consults the reset pool
+// when enabled, falls through to generate_world otherwise. Pool index is
+// derived from the reset_key so different done events pick different
+// pooled worlds. The direct craftax_reset_state_from_reset_key stays
+// pool-free so the parity harness and any other direct caller get exact
+// per-key determinism.
+static inline void craftax_reset_state_on_done(
+ CraftaxState* out,
+ CraftaxThreefryKey reset_key
+) {
+ if (g_craftax_reset_pool_size > 0) {
+ uint32_t idx = reset_key.word[0] % (uint32_t)g_craftax_reset_pool_size;
+ memcpy(out, &g_craftax_reset_pool[idx], sizeof(CraftaxState));
+ return;
+ }
+ craftax_reset_state_from_reset_key(out, reset_key);
+}
+
+static inline void craftax_encode_native_observation(
+ const CraftaxState* state,
+ float* obs
+) {
+ if (obs == NULL) {
+ return;
+ }
+ craftax_encode_reset_observation((const CraftaxWorldState*)(const void*)state, obs);
+}
+
+static inline float craftax_calculate_light_level_native(int32_t timestep) {
+ float progress = fmodf(
+ (float)timestep / (float)CRAFTAX_DAY_LENGTH,
+ 1.0f
+ ) + 0.3f;
+ float c = cosf(CRAFTAX_WG_PI * progress);
+ return 1.0f - powf(fabsf(c), 3.0f);
+}
+
+static inline bool craftax_is_game_over_native(const CraftaxState* state) {
+ return state->timestep >= CRAFTAX_DEFAULT_MAX_TIMESTEPS
+ || state->player_health <= 0.0f;
+}
+
+static inline void craftax_copy_achievements_to_env(
+ Craftax* env,
+ const CraftaxState* state
+) {
+ for (int i = 0; i < CRAFTAX_NUM_ACHIEVEMENTS; i++) {
+ env->achievements[i] = state->achievements[i] ? 1.0f : 0.0f;
+ }
+}
+
+static void add_log(Craftax* env) {
+ int unlocked = 0;
+ for (int i = 0; i < CRAFTAX_NUM_ACHIEVEMENTS; i++) {
+ if (env->achievements[i] > 0.5f) {
+ unlocked++;
+ env->log.achievements[i] += 1.0f;
+ }
+ }
+ env->log.perf += (float)unlocked / (float)CRAFTAX_NUM_ACHIEVEMENTS;
+ env->log.score += env->episode_return_accum;
+ env->log.episode_return += env->episode_return_accum;
+ env->log.episode_length += (float)env->episode_length_accum;
+ env->log.n += 1.0f;
+}
+
+static float craftax_gameplay_step_native(
+ CraftaxState* state,
+ int32_t action,
+ CraftaxThreefryKey rng
+) {
+ CRAFTAX_PROFILE_START();
+ bool init_achievements[CRAFTAX_NUM_ACHIEVEMENTS];
+ memcpy(init_achievements, state->achievements, sizeof(init_achievements));
+ float init_health = state->player_health;
+
+ action = state->is_sleeping ? CRAFTAX_ACTION_NOOP : action;
+ action = state->is_resting ? CRAFTAX_ACTION_NOOP : action;
+
+ CRAFTAX_PROFILE_ZONE(0);
+ craftax_change_floor_native(state, action);
+ craftax_do_crafting_native(state, action);
+ CRAFTAX_PROFILE_END(0);
+
+ CraftaxThreefryKey subkey = craftax_step_native_next_key(&rng);
+ CRAFTAX_PROFILE_ZONE(2);
+ craftax_do_action_native(state, action, subkey);
+ CRAFTAX_PROFILE_END(2);
+
+ CRAFTAX_PROFILE_ZONE(3);
+ craftax_place_block_native(state, action);
+ craftax_shoot_projectile_native(state, action);
+ craftax_cast_spell_native(state, action);
+ craftax_drink_potion_native(state, action);
+ CRAFTAX_PROFILE_END(3);
+
+ subkey = craftax_step_native_next_key(&rng);
+ CRAFTAX_PROFILE_ZONE(4);
+ craftax_read_book_native(state, subkey.word, action);
+ CRAFTAX_PROFILE_END(4);
+
+ subkey = craftax_step_native_next_key(&rng);
+ CRAFTAX_PROFILE_ZONE(5);
+ craftax_enchant_native(state, action, subkey);
+ CRAFTAX_PROFILE_END(5);
+
+ CRAFTAX_PROFILE_ZONE(6);
+ craftax_boss_logic_native(state);
+ craftax_level_up_attributes_native(state, action, CRAFTAX_MAX_ATTRIBUTE);
+ craftax_move_player_native(state, action, false);
+ CRAFTAX_PROFILE_END(6);
+
+ subkey = craftax_step_native_next_key(&rng);
+ CRAFTAX_PROFILE_ZONE(7);
+ craftax_update_mobs_native(state, subkey);
+ CRAFTAX_PROFILE_END(7);
+
+ subkey = craftax_step_native_next_key(&rng);
+ CRAFTAX_PROFILE_ZONE(8);
+ craftax_spawn_mobs_native(state, subkey);
+ CRAFTAX_PROFILE_END(8);
+
+ CRAFTAX_PROFILE_ZONE(9);
+ craftax_update_plants_native(state);
+ craftax_update_player_intrinsics_native(state, action);
+ craftax_clip_inventory_and_intrinsics_native(state, false);
+ craftax_calculate_inventory_achievements_native(state);
+ CRAFTAX_PROFILE_END(9);
+
+ CRAFTAX_PROFILE_ZONE(10);
+ float reward = 0.0f;
+ for (int i = 0; i < CRAFTAX_NUM_ACHIEVEMENTS; i++) {
+ int32_t delta = (int32_t)state->achievements[i]
+ - (int32_t)init_achievements[i];
+ reward += (float)delta * CRAFTAX_ACHIEVEMENT_REWARD_MAP[i];
+ }
+ reward += (state->player_health - init_health) * 0.1f;
+
+ subkey = craftax_step_native_next_key(&rng);
+ state->timestep += 1;
+ state->light_level = craftax_calculate_light_level_native(state->timestep);
+ state->state_rng[0] = subkey.word[0];
+ state->state_rng[1] = subkey.word[1];
+ CRAFTAX_PROFILE_END(10);
+
+ return reward;
+}
+
+// ============================================================
+// Public API expected by vecenv.h
+// ============================================================
+static void c_init(Craftax* env) {
+ env->client = NULL;
+ env->num_agents = 1;
+ craftax_ensure_state_storage(env);
+ env->episode_return_accum = 0.0f;
+ env->episode_length_accum = 0;
+ memset(env->achievements, 0, sizeof(env->achievements));
+ memset(&env->log, 0, sizeof(env->log));
+ craftax_wg_init_cell_templates();
+ craftax_reset_state_from_seed(env);
+}
+
+static void c_reset(Craftax* env) {
+ if (env->rewards != NULL) {
+ env->rewards[0] = 0.0f;
+ }
+ if (env->terminals != NULL) {
+ env->terminals[0] = 0.0f;
+ }
+ env->episode_return_accum = 0.0f;
+ env->episode_length_accum = 0;
+ memset(env->achievements, 0, sizeof(env->achievements));
+
+ craftax_reset_state_from_seed(env);
+ craftax_encode_native_observation(env->state, env->observations);
+}
+
+#ifdef CRAFTAX_PROFILE
+static void c_step_native(Craftax* env) {
+ CRAFTAX_PROFILE_START();
+ env->rewards[0] = 0.0f;
+ env->terminals[0] = 0.0f;
+
+ int action = (int)env->actions[0];
+ if (action < 0) {
+ action = CRAFTAX_ACTION_NOOP;
+ }
+ if (action >= CRAFTAX_NUM_ACTIONS) {
+ action = CRAFTAX_NUM_ACTIONS - 1;
+ }
+
+ CRAFTAX_PROFILE_ZONE(12);
+ CraftaxThreefryKey step_key;
+ craftax_threefry_split(env->rng_key, &env->rng_key, &step_key);
+
+ CraftaxThreefryKey step_rng;
+ CraftaxThreefryKey reset_key;
+ craftax_threefry_split(step_key, &step_rng, &reset_key);
+ CRAFTAX_PROFILE_END(12);
+
+ float reward = craftax_gameplay_step_native(env->state, action, step_rng);
+
+ CRAFTAX_PROFILE_ZONE(13);
+ bool done = craftax_is_game_over_native(env->state);
+ CRAFTAX_PROFILE_END(13);
+
+ CRAFTAX_PROFILE_ZONE(15);
+ craftax_copy_achievements_to_env(env, env->state);
+ CRAFTAX_PROFILE_END(15);
+
+ CRAFTAX_PROFILE_ZONE(16);
+ env->rewards[0] = reward;
+ env->terminals[0] = done ? 1.0f : 0.0f;
+ env->episode_return_accum += reward;
+ env->episode_length_accum += 1;
+ CRAFTAX_PROFILE_END(16);
+
+ if (done) {
+ add_log(env);
+ env->episode_return_accum = 0.0f;
+ env->episode_length_accum = 0;
+ memset(env->achievements, 0, sizeof(env->achievements));
+ CRAFTAX_PROFILE_ZONE(14);
+ craftax_reset_state_on_done(env->state, reset_key);
+ CRAFTAX_PROFILE_END(14);
+ }
+
+ CRAFTAX_PROFILE_ZONE(11);
+ craftax_encode_native_observation(env->state, env->observations);
+ CRAFTAX_PROFILE_END(11);
+
+ // Record unprofiled time
+ CRAFTAX_PROFILE_ZONE(17);
+ CRAFTAX_PROFILE_END(17);
+
+#ifdef CRAFTAX_PROFILE
+ static int profile_step_count = 0;
+ profile_step_count++;
+ if (profile_step_count >= 100000) {
+ craftax_profile_report();
+ profile_step_count = 0;
+ }
+
+#endif
+}
+
+#endif
+
+static void c_step_gameplay(Craftax* env) {
+ env->rewards[0] = 0.0f;
+ env->terminals[0] = 0.0f;
+
+ int action = (int)env->actions[0];
+ if (action < 0) action = CRAFTAX_ACTION_NOOP;
+ if (action >= CRAFTAX_NUM_ACTIONS) action = CRAFTAX_NUM_ACTIONS - 1;
+
+ CraftaxThreefryKey step_key;
+ craftax_threefry_split(env->rng_key, &env->rng_key, &step_key);
+ CraftaxThreefryKey step_rng;
+ CraftaxThreefryKey reset_key;
+ craftax_threefry_split(step_key, &step_rng, &reset_key);
+
+ float reward = craftax_gameplay_step_native(env->state, action, step_rng);
+ bool done = craftax_is_game_over_native(env->state);
+ craftax_copy_achievements_to_env(env, env->state);
+
+ env->rewards[0] = reward;
+ env->terminals[0] = done ? 1.0f : 0.0f;
+ env->episode_return_accum += reward;
+ env->episode_length_accum += 1;
+
+ if (done) {
+ add_log(env);
+ env->episode_return_accum = 0.0f;
+ env->episode_length_accum = 0;
+ memset(env->achievements, 0, sizeof(env->achievements));
+ craftax_reset_state_on_done(env->state, reset_key);
+ }
+}
+
+static void c_step_encode(Craftax* env) {
+ craftax_encode_native_observation(env->state, env->observations);
+}
+
+static void c_step(Craftax* env) {
+ c_step_gameplay(env);
+ c_step_encode(env);
+}
+
+static void c_close(Craftax* env) {
+ if (!env->owns_state_storage || env->arena == NULL) {
+ return;
+ }
+ free(env->arena->states);
+ free(env->arena);
+ env->arena = NULL;
+ env->state = NULL;
+ env->owns_state_storage = false;
+}
+
+// ------------------------------------------------------------
+// Tile-based renderer using upstream Craftax 16x16 PNG assets
+// ------------------------------------------------------------
+// Packed layout (see ocean/craftax/pack_textures.py):
+// [0..36] block textures (indexed by CraftaxBlockType)
+// [37..41] player: down, up, left, right, sleep
+// [42..46] items: none, torch, ladder_down, ladder_up, ladder_down_blocked
+
+#define CRAFTAX_TEX_TILE_PX 16
+#define CRAFTAX_TEX_SCALE 4 // on-screen px = 64
+#define CRAFTAX_TEX_DRAW_PX (CRAFTAX_TEX_TILE_PX * CRAFTAX_TEX_SCALE)
+#define CRAFTAX_TEX_NUM (37 + 5 + 5 + 3 + 4)
+
+// Render viewport (independent of agent obs window)
+#define CRAFTAX_RENDER_ROWS 16
+#define CRAFTAX_RENDER_COLS 16
+
+#define CRAFTAX_TEX_PLAYER_DOWN 37
+#define CRAFTAX_TEX_PLAYER_UP 38
+#define CRAFTAX_TEX_PLAYER_LEFT 39
+#define CRAFTAX_TEX_PLAYER_RIGHT 40
+#define CRAFTAX_TEX_PLAYER_SLEEP 41
+#define CRAFTAX_TEX_ITEM_BASE 42
+
+static Texture2D craftax_textures[CRAFTAX_TEX_NUM];
+static bool craftax_textures_loaded = false;
+
+static void craftax_load_textures(void) {
+ if (craftax_textures_loaded) return;
+ const char* candidates[] = {
+ "resources/craftax/textures.bin",
+ "../resources/craftax/textures.bin",
+ "../../resources/craftax/textures.bin",
+ };
+ FILE* f = NULL;
+ for (size_t i = 0; i < sizeof(candidates)/sizeof(candidates[0]); i++) {
+ f = fopen(candidates[i], "rb");
+ if (f) break;
+ }
+ if (!f) {
+ fprintf(stderr, "craftax: textures.bin not found in resources/craftax -- run ocean/craftax/pack_textures.py\n");
+ exit(1);
+ }
+ const size_t tile_bytes = CRAFTAX_TEX_TILE_PX * CRAFTAX_TEX_TILE_PX * 4;
+ uint8_t* buf = (uint8_t*)malloc(tile_bytes);
+ for (int i = 0; i < CRAFTAX_TEX_NUM; i++) {
+ if (fread(buf, 1, tile_bytes, f) != tile_bytes) {
+ fprintf(stderr, "craftax: short read on textures.bin at tile %d\n", i);
+ exit(1);
+ }
+ Image img = {
+ .data = buf,
+ .width = CRAFTAX_TEX_TILE_PX,
+ .height = CRAFTAX_TEX_TILE_PX,
+ .mipmaps = 1,
+ .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
+ };
+ craftax_textures[i] = LoadTextureFromImage(img);
+ SetTextureFilter(craftax_textures[i], TEXTURE_FILTER_POINT);
+ }
+ free(buf);
+ fclose(f);
+ craftax_textures_loaded = true;
+}
+
+static int craftax_player_tex_id(int32_t direction, bool sleeping) {
+ if (sleeping) return CRAFTAX_TEX_PLAYER_SLEEP;
+ switch (direction) {
+ case 1: return CRAFTAX_TEX_PLAYER_LEFT;
+ case 2: return CRAFTAX_TEX_PLAYER_RIGHT;
+ case 3: return CRAFTAX_TEX_PLAYER_UP;
+ case 4: return CRAFTAX_TEX_PLAYER_DOWN;
+ default: return CRAFTAX_TEX_PLAYER_DOWN;
+ }
+}
+
+static void craftax_draw_tile(int tex_id, int dst_x, int dst_y, float tint_alpha) {
+ if (tex_id < 0 || tex_id >= CRAFTAX_TEX_NUM) return;
+ Rectangle src = {0, 0, CRAFTAX_TEX_TILE_PX, CRAFTAX_TEX_TILE_PX};
+ Rectangle dst = {(float)dst_x, (float)dst_y, CRAFTAX_TEX_DRAW_PX, CRAFTAX_TEX_DRAW_PX};
+ Color tint = {255, 255, 255, (unsigned char)(tint_alpha * 255.0f)};
+ DrawTexturePro(craftax_textures[tex_id], src, dst, (Vector2){0, 0}, 0.0f, tint);
+}
+
+static void c_render(Craftax* env) {
+ const int view_w = CRAFTAX_RENDER_COLS * CRAFTAX_TEX_DRAW_PX;
+ const int view_h = CRAFTAX_RENDER_ROWS * CRAFTAX_TEX_DRAW_PX;
+ const int hud_h = 80;
+
+ if (!IsWindowReady()) {
+ InitWindow(view_w, view_h + hud_h, "PufferLib Craftax");
+ SetTargetFPS(30);
+ }
+ if (!craftax_textures_loaded) craftax_load_textures();
+ if (IsKeyDown(KEY_ESCAPE)) exit(0);
+
+ CraftaxState* s = env->state;
+ int lvl = s->player_level;
+ int pr = s->player_position[0];
+ int pc = s->player_position[1];
+ int half_r = CRAFTAX_RENDER_ROWS / 2;
+ int half_c = CRAFTAX_RENDER_COLS / 2;
+
+ BeginDrawing();
+ ClearBackground(BLACK);
+
+ for (int vr = 0; vr < CRAFTAX_RENDER_ROWS; vr++) {
+ for (int vc = 0; vc < CRAFTAX_RENDER_COLS; vc++) {
+ int wr = pr - half_r + vr;
+ int wc = pc - half_c + vc;
+ int dst_x = vc * CRAFTAX_TEX_DRAW_PX;
+ int dst_y = vr * CRAFTAX_TEX_DRAW_PX;
+
+ int blk = CRAFTAX_BLOCK_OUT_OF_BOUNDS;
+ if (wr >= 0 && wr < CRAFTAX_MAP_SIZE && wc >= 0 && wc < CRAFTAX_MAP_SIZE) {
+ blk = s->map[lvl][wr][wc];
+ if (s->light_map[lvl][wr][wc] <= 12) blk = CRAFTAX_BLOCK_DARKNESS;
+ }
+ if (blk < 0 || blk >= CRAFTAX_NUM_BLOCK_TYPES) blk = 0;
+ craftax_draw_tile(blk, dst_x, dst_y, 1.0f);
+
+ // item overlay
+ if (wr >= 0 && wr < CRAFTAX_MAP_SIZE && wc >= 0 && wc < CRAFTAX_MAP_SIZE) {
+ int it = s->item_map[lvl][wr][wc];
+ if (it > 0 && it < 5) {
+ craftax_draw_tile(CRAFTAX_TEX_ITEM_BASE + it, dst_x, dst_y, 1.0f);
+ }
+ }
+ }
+ }
+
+ // player in center
+ int pid = craftax_player_tex_id(s->player_direction, s->is_sleeping);
+ craftax_draw_tile(pid, half_c * CRAFTAX_TEX_DRAW_PX, half_r * CRAFTAX_TEX_DRAW_PX, 1.0f);
+
+ // night dim overlay
+ if (s->light_level < 1.0f) {
+ unsigned char a = (unsigned char)((1.0f - s->light_level) * 140.0f);
+ DrawRectangle(0, 0, view_w, view_h, (Color){0, 0, 40, a});
+ }
+
+ // HUD
+ int hud_y = view_h;
+ DrawRectangle(0, hud_y, view_w, hud_h, (Color){20, 20, 20, 255});
+ DrawText(TextFormat("HP:%.0f F:%d D:%d E:%d M:%d L:%d t:%d",
+ s->player_health, s->player_food, s->player_drink,
+ s->player_energy, s->player_mana, s->player_level, s->timestep),
+ 4, hud_y + 4, 14, WHITE);
+ DrawText(TextFormat("XP:%d DEX:%d STR:%d INT:%d light:%.2f",
+ s->player_xp, s->player_dexterity, s->player_strength,
+ s->player_intelligence, s->light_level),
+ 4, hud_y + 22, 14, (Color){200, 200, 200, 255});
+ int ach_count = 0;
+ for (int i = 0; i < CRAFTAX_NUM_ACHIEVEMENTS; i++) ach_count += s->achievements[i] ? 1 : 0;
+ DrawText(TextFormat("achievements: %d / %d", ach_count, CRAFTAX_NUM_ACHIEVEMENTS),
+ 4, hud_y + 40, 14, (Color){180, 220, 180, 255});
+ DrawText(TextFormat("ret:%.2f len:%d", env->episode_return_accum, env->episode_length_accum),
+ 4, hud_y + 58, 14, (Color){200, 200, 140, 255});
+
+ EndDrawing();
+}
+
+#endif
diff --git a/ocean/craftax/noise.h b/ocean/craftax/noise.h
new file mode 100644
index 0000000000..e81e398509
--- /dev/null
+++ b/ocean/craftax/noise.h
@@ -0,0 +1,206 @@
+// Native C port of craftax/craftax/util/noise.py.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include "threefry.h"
+
+#ifndef CRAFTAX_NOISE_PI2
+#define CRAFTAX_NOISE_PI2 6.28318530717958647692f
+#endif
+
+#ifndef CRAFTAX_NOISE_SQRT2
+#define CRAFTAX_NOISE_SQRT2 1.41421356237309504880f
+#endif
+
+static inline float craftax_noise_interpolant(float t) {
+ return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f);
+}
+
+static inline float craftax_noise_gradient_angle(
+ CraftaxThreefryKey angle_key,
+ int res_cols,
+ int row,
+ int col,
+ const float* override_angles
+) {
+ int width = res_cols + 1;
+ uint64_t index = (uint64_t)row * (uint64_t)width + (uint64_t)col;
+ float unit = override_angles == NULL
+ ? craftax_threefry_uniform_f32_at(angle_key, index)
+ : override_angles[index];
+ return CRAFTAX_NOISE_PI2 * unit;
+}
+
+static inline void craftax_noise_gradient(
+ CraftaxThreefryKey angle_key,
+ int res_cols,
+ int row,
+ int col,
+ const float* override_angles,
+ float* gx,
+ float* gy
+) {
+ float angle = craftax_noise_gradient_angle(
+ angle_key,
+ res_cols,
+ row,
+ col,
+ override_angles
+ );
+ *gx = cosf(angle);
+ *gy = sinf(angle);
+}
+
+static inline void craftax_generate_perlin_noise_2d(
+ CraftaxThreefryKey rng,
+ int rows,
+ int cols,
+ int res_rows,
+ int res_cols,
+ const float* override_angles,
+ float* out
+) {
+ CraftaxThreefryKey unused;
+ CraftaxThreefryKey angle_key;
+ craftax_threefry_split(rng, &unused, &angle_key);
+
+ int cell_rows = rows / res_rows;
+ int cell_cols = cols / res_cols;
+
+ for (int row = 0; row < rows; row++) {
+ int grad_row = row / cell_rows;
+ float local_row = (float)(row - grad_row * cell_rows) / (float)cell_rows;
+ float interp_row = craftax_noise_interpolant(local_row);
+
+ for (int col = 0; col < cols; col++) {
+ int grad_col = col / cell_cols;
+ float local_col = (float)(col - grad_col * cell_cols) / (float)cell_cols;
+ float interp_col = craftax_noise_interpolant(local_col);
+
+ float g00x;
+ float g00y;
+ float g10x;
+ float g10y;
+ float g01x;
+ float g01y;
+ float g11x;
+ float g11y;
+ craftax_noise_gradient(
+ angle_key,
+ res_cols,
+ grad_row,
+ grad_col,
+ override_angles,
+ &g00x,
+ &g00y
+ );
+ craftax_noise_gradient(
+ angle_key,
+ res_cols,
+ grad_row + 1,
+ grad_col,
+ override_angles,
+ &g10x,
+ &g10y
+ );
+ craftax_noise_gradient(
+ angle_key,
+ res_cols,
+ grad_row,
+ grad_col + 1,
+ override_angles,
+ &g01x,
+ &g01y
+ );
+ craftax_noise_gradient(
+ angle_key,
+ res_cols,
+ grad_row + 1,
+ grad_col + 1,
+ override_angles,
+ &g11x,
+ &g11y
+ );
+
+ float n00 = local_row * g00x;
+ n00 += local_col * g00y;
+ float n10 = (local_row - 1.0f) * g10x;
+ n10 += local_col * g10y;
+ float n01 = local_row * g01x;
+ n01 += (local_col - 1.0f) * g01y;
+ float n11 = (local_row - 1.0f) * g11x;
+ n11 += (local_col - 1.0f) * g11y;
+
+ float n0 = n00 * (1.0f - interp_row) + interp_row * n10;
+ float n1 = n01 * (1.0f - interp_row) + interp_row * n11;
+ out[(size_t)row * (size_t)cols + (size_t)col] =
+ CRAFTAX_NOISE_SQRT2 * ((1.0f - interp_col) * n0 + interp_col * n1);
+ }
+ }
+}
+
+static inline void craftax_generate_fractal_noise_2d(
+ CraftaxThreefryKey rng,
+ int rows,
+ int cols,
+ int res_rows,
+ int res_cols,
+ int octaves,
+ float persistence,
+ int lacunarity,
+ const float* override_angles,
+ float* out
+) {
+ size_t size = (size_t)rows * (size_t)cols;
+ for (size_t i = 0; i < size; i++) {
+ out[i] = 0.0f;
+ }
+
+ int frequency = 1;
+ float amplitude = 1.0f;
+ float perlin[size];
+
+ for (int octave = 0; octave < octaves; octave++) {
+ CraftaxThreefryKey next_rng;
+ CraftaxThreefryKey noise_key;
+ craftax_threefry_split(rng, &next_rng, &noise_key);
+ rng = next_rng;
+
+ craftax_generate_perlin_noise_2d(
+ noise_key,
+ rows,
+ cols,
+ frequency * res_rows,
+ frequency * res_cols,
+ override_angles,
+ perlin
+ );
+
+ for (size_t i = 0; i < size; i++) {
+ out[i] += amplitude * perlin[i];
+ }
+
+ frequency *= lacunarity;
+ amplitude *= persistence;
+ }
+
+ float min_value = out[0];
+ float max_value = out[0];
+ for (size_t i = 1; i < size; i++) {
+ if (out[i] < min_value) {
+ min_value = out[i];
+ }
+ if (out[i] > max_value) {
+ max_value = out[i];
+ }
+ }
+
+ float scale = max_value - min_value;
+ for (size_t i = 0; i < size; i++) {
+ out[i] = (out[i] - min_value) / scale;
+ }
+}
diff --git a/ocean/craftax/pack_textures.py b/ocean/craftax/pack_textures.py
new file mode 100644
index 0000000000..078d7b9fbd
--- /dev/null
+++ b/ocean/craftax/pack_textures.py
@@ -0,0 +1,138 @@
+"""Pack Craftax upstream 16x16 PNG assets into a single shared textures.bin.
+
+Consumed by both ocean/craftax (full) and ocean/craftax_classic. All files
+live in craftax's asset dir; the classic PNGs that overlap are byte-identical
+to the full ones.
+
+Layout: contiguous 16*16*4 RGBA tiles. Order must match the
+CRAFTAX_TEX_* / CC_TEX_* enums in the two env headers.
+
+ [0..36] block textures (37) -- BlockType; first 17 entries also valid for classic
+ [37..41] player: down, up, left, right, sleep
+ [42..46] items: none(blank), torch, ladder_down, ladder_up, ladder_down_blocked
+ [47..49] mobs: zombie, skeleton, cow
+ [50..53] arrows: down, up, left, right
+"""
+
+from pathlib import Path
+from PIL import Image
+import numpy as np
+
+ASSETS = Path(__file__).resolve().parents[2] / (
+ ".venv/lib/python3.12/site-packages/craftax/craftax/assets"
+)
+OUT_BIN = Path(__file__).resolve().parents[2] / "resources" / "craftax" / "textures.bin"
+
+TILE = 16
+
+BLOCK_FILES = [
+ "debug_tile.png", # 0 INVALID
+ "debug_tile.png", # 1 OUT_OF_BOUNDS (overwritten solid grey below)
+ "grass.png", # 2
+ "water.png", # 3
+ "stone.png", # 4
+ "tree.png", # 5
+ "wood.png", # 6
+ "path.png", # 7
+ "coal.png", # 8
+ "iron.png", # 9
+ "diamond.png", # 10
+ "table.png", # 11 crafting table
+ "furnace.png", # 12
+ "sand.png", # 13
+ "lava.png", # 14
+ "plant_on_grass.png", # 15
+ "ripe_plant_on_grass.png", # 16
+ "wall2.png", # 17
+ "debug_tile.png", # 18 DARKNESS (overwritten solid black below)
+ "wall_moss.png", # 19
+ "stalagmite.png", # 20
+ "sapphire.png", # 21
+ "ruby.png", # 22
+ "chest.png", # 23
+ "fountain.png", # 24
+ "fire_grass.png", # 25
+ "ice_grass.png", # 26
+ "gravel.png", # 27
+ "fire_tree.png", # 28
+ "ice_shrub.png", # 29
+ "enchantment_table_fire.png",# 30
+ "enchantment_table_ice.png", # 31
+ "necromancer.png", # 32
+ "grave.png", # 33
+ "grave2.png", # 34
+ "grave3.png", # 35
+ "necromancer_vulnerable.png",# 36
+]
+
+PLAYER_FILES = [
+ "player-down.png",
+ "player-up.png",
+ "player-left.png",
+ "player-right.png",
+ "player-sleep.png",
+]
+
+ITEM_FILES = [
+ None, # NONE -> fully transparent
+ "torch_on_path.png",
+ "ladder_down.png",
+ "ladder_up.png",
+ "ladder_down_blocked.png",
+]
+
+MOB_FILES = [
+ "zombie.png",
+ "skeleton.png",
+ "cow.png",
+]
+
+ARROW_FILES = [
+ "arrow-down.png",
+ "arrow-up.png",
+ "arrow-left.png",
+ "arrow-right.png",
+]
+
+
+def load_tile(name: str | None) -> np.ndarray:
+ if name is None:
+ return np.zeros((TILE, TILE, 4), dtype=np.uint8)
+ p = ASSETS / name
+ img = Image.open(p).convert("RGBA").resize((TILE, TILE), Image.NEAREST)
+ return np.asarray(img, dtype=np.uint8)
+
+
+def main() -> None:
+ tiles: list[np.ndarray] = []
+ for f in BLOCK_FILES:
+ tiles.append(load_tile(f))
+
+ # manual overrides to match upstream renderer
+ tiles[1] = np.full((TILE, TILE, 4), 128, dtype=np.uint8)
+ tiles[1][..., 3] = 255 # out of bounds
+ tiles[18] = np.zeros((TILE, TILE, 4), dtype=np.uint8)
+ tiles[18][..., 3] = 255 # darkness
+
+ for f in PLAYER_FILES:
+ tiles.append(load_tile(f))
+
+ # torch_in_walls doesn't exist in assets; fall back to torch.png if needed
+ for f in ITEM_FILES:
+ if f is not None and not (ASSETS / f).exists():
+ alt = "torch.png" if "torch" in f else f
+ tiles.append(load_tile(alt))
+ else:
+ tiles.append(load_tile(f))
+
+ for f in MOB_FILES + ARROW_FILES:
+ tiles.append(load_tile(f))
+
+ blob = np.stack(tiles, axis=0) # (N, 16, 16, 4) uint8
+ assert blob.dtype == np.uint8
+ OUT_BIN.write_bytes(blob.tobytes(order="C"))
+ print(f"wrote {OUT_BIN} — {blob.shape[0]} tiles, {OUT_BIN.stat().st_size} bytes")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ocean/craftax/step_crafting.h b/ocean/craftax/step_crafting.h
new file mode 100644
index 0000000000..60779364b7
--- /dev/null
+++ b/ocean/craftax/step_crafting.h
@@ -0,0 +1,424 @@
+// Standalone native ports of Craftax crafting and placement subsystems.
+//
+// These helpers intentionally are not integrated into c_step yet. They mutate a
+// full CraftaxState in place so tests can compare each subsystem directly
+// against the installed JAX implementation.
+
+#pragma once
+
+#include "step_simple.h"
+
+static inline bool craftax_crafting_is_near_block(
+ const CraftaxState* state,
+ int32_t block_type
+) {
+ static const int32_t close_blocks[8][2] = {
+ {0, -1},
+ {0, 1},
+ {-1, 0},
+ {1, 0},
+ {-1, -1},
+ {-1, 1},
+ {1, -1},
+ {1, 1},
+ };
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ for (int32_t i = 0; i < 8; i++) {
+ int32_t row = state->player_position[0] + close_blocks[i][0];
+ int32_t col = state->player_position[1] + close_blocks[i][1];
+ bool in_bounds = row >= 0
+ && row < CRAFTAX_MAP_SIZE
+ && col >= 0
+ && col < CRAFTAX_MAP_SIZE;
+ if (in_bounds && state->map[level][row][col] == block_type) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static inline int32_t craftax_crafting_first_armour_below(
+ const CraftaxInventory* inventory,
+ int32_t threshold,
+ int32_t* count
+) {
+ int32_t first = 0;
+ *count = 0;
+ for (int32_t i = 0; i < 4; i++) {
+ bool below = inventory->armour[i] < threshold;
+ first = (*count == 0 && below) ? i : first;
+ *count += (int32_t)below;
+ }
+ return first;
+}
+
+static inline void craftax_do_crafting_native(
+ CraftaxState* state,
+ int32_t action
+) {
+ bool is_at_crafting_table = craftax_crafting_is_near_block(
+ state,
+ CRAFTAX_BLOCK_CRAFTING_TABLE
+ );
+ bool is_at_furnace = craftax_crafting_is_near_block(
+ state,
+ CRAFTAX_BLOCK_FURNACE
+ );
+
+ CraftaxInventory* inventory = &state->inventory;
+
+ bool can_craft_wood_pickaxe = inventory->wood >= 1;
+ bool is_crafting_wood_pickaxe =
+ action == CRAFTAX_ACTION_MAKE_WOOD_PICKAXE
+ && can_craft_wood_pickaxe
+ && is_at_crafting_table
+ && inventory->pickaxe < 1;
+ inventory->wood -= 1 * (int32_t)is_crafting_wood_pickaxe;
+ inventory->pickaxe =
+ inventory->pickaxe * (1 - (int32_t)is_crafting_wood_pickaxe)
+ + 1 * (int32_t)is_crafting_wood_pickaxe;
+
+ bool can_craft_stone_pickaxe =
+ inventory->wood >= 1 && inventory->stone >= 1;
+ bool is_crafting_stone_pickaxe =
+ action == CRAFTAX_ACTION_MAKE_STONE_PICKAXE
+ && can_craft_stone_pickaxe
+ && is_at_crafting_table
+ && inventory->pickaxe < 2;
+ inventory->stone -= 1 * (int32_t)is_crafting_stone_pickaxe;
+ inventory->wood -= 1 * (int32_t)is_crafting_stone_pickaxe;
+ inventory->pickaxe =
+ inventory->pickaxe * (1 - (int32_t)is_crafting_stone_pickaxe)
+ + 2 * (int32_t)is_crafting_stone_pickaxe;
+
+ bool can_craft_iron_pickaxe =
+ inventory->wood >= 1
+ && inventory->stone >= 1
+ && inventory->iron >= 1
+ && inventory->coal >= 1;
+ bool is_crafting_iron_pickaxe =
+ action == CRAFTAX_ACTION_MAKE_IRON_PICKAXE
+ && can_craft_iron_pickaxe
+ && is_at_furnace
+ && is_at_crafting_table
+ && inventory->pickaxe < 3;
+ inventory->iron -= 1 * (int32_t)is_crafting_iron_pickaxe;
+ inventory->wood -= 1 * (int32_t)is_crafting_iron_pickaxe;
+ inventory->stone -= 1 * (int32_t)is_crafting_iron_pickaxe;
+ inventory->coal -= 1 * (int32_t)is_crafting_iron_pickaxe;
+ inventory->pickaxe =
+ inventory->pickaxe * (1 - (int32_t)is_crafting_iron_pickaxe)
+ + 3 * (int32_t)is_crafting_iron_pickaxe;
+
+ bool can_craft_diamond_pickaxe =
+ inventory->wood >= 1 && inventory->diamond >= 3;
+ bool is_crafting_diamond_pickaxe =
+ action == CRAFTAX_ACTION_MAKE_DIAMOND_PICKAXE
+ && can_craft_diamond_pickaxe
+ && is_at_crafting_table
+ && inventory->pickaxe < 4;
+ inventory->diamond -= 3 * (int32_t)is_crafting_diamond_pickaxe;
+ inventory->wood -= 1 * (int32_t)is_crafting_diamond_pickaxe;
+ inventory->pickaxe =
+ inventory->pickaxe * (1 - (int32_t)is_crafting_diamond_pickaxe)
+ + 4 * (int32_t)is_crafting_diamond_pickaxe;
+
+ bool can_craft_wood_sword = inventory->wood >= 1;
+ bool is_crafting_wood_sword =
+ action == CRAFTAX_ACTION_MAKE_WOOD_SWORD
+ && can_craft_wood_sword
+ && is_at_crafting_table
+ && inventory->sword < 1;
+ inventory->wood -= 1 * (int32_t)is_crafting_wood_sword;
+ inventory->sword =
+ inventory->sword * (1 - (int32_t)is_crafting_wood_sword)
+ + 1 * (int32_t)is_crafting_wood_sword;
+
+ bool can_craft_stone_sword =
+ inventory->stone >= 1 && inventory->wood >= 1;
+ bool is_crafting_stone_sword =
+ action == CRAFTAX_ACTION_MAKE_STONE_SWORD
+ && can_craft_stone_sword
+ && is_at_crafting_table
+ && inventory->sword < 2;
+ inventory->wood -= 1 * (int32_t)is_crafting_stone_sword;
+ inventory->stone -= 1 * (int32_t)is_crafting_stone_sword;
+ inventory->sword =
+ inventory->sword * (1 - (int32_t)is_crafting_stone_sword)
+ + 2 * (int32_t)is_crafting_stone_sword;
+
+ bool can_craft_iron_sword =
+ inventory->iron >= 1
+ && inventory->wood >= 1
+ && inventory->stone >= 1
+ && inventory->coal >= 1;
+ bool is_crafting_iron_sword =
+ action == CRAFTAX_ACTION_MAKE_IRON_SWORD
+ && can_craft_iron_sword
+ && is_at_furnace
+ && is_at_crafting_table
+ && inventory->sword < 3;
+ inventory->wood -= 1 * (int32_t)is_crafting_iron_sword;
+ inventory->iron -= 1 * (int32_t)is_crafting_iron_sword;
+ inventory->stone -= 1 * (int32_t)is_crafting_iron_sword;
+ inventory->coal -= 1 * (int32_t)is_crafting_iron_sword;
+ inventory->sword =
+ inventory->sword * (1 - (int32_t)is_crafting_iron_sword)
+ + 3 * (int32_t)is_crafting_iron_sword;
+
+ bool can_craft_diamond_sword =
+ inventory->diamond >= 2 && inventory->wood >= 1;
+ bool is_crafting_diamond_sword =
+ action == CRAFTAX_ACTION_MAKE_DIAMOND_SWORD
+ && can_craft_diamond_sword
+ && is_at_crafting_table
+ && inventory->sword < 4;
+ inventory->wood -= 1 * (int32_t)is_crafting_diamond_sword;
+ inventory->diamond -= 2 * (int32_t)is_crafting_diamond_sword;
+ inventory->sword =
+ inventory->sword * (1 - (int32_t)is_crafting_diamond_sword)
+ + 4 * (int32_t)is_crafting_diamond_sword;
+
+ int32_t armour_count = 0;
+ int32_t iron_armour_index_to_craft =
+ craftax_crafting_first_armour_below(inventory, 1, &armour_count);
+ bool can_craft_iron_armour =
+ armour_count > 0 && inventory->iron >= 3 && inventory->coal >= 3;
+ bool is_crafting_iron_armour =
+ action == CRAFTAX_ACTION_MAKE_IRON_ARMOUR
+ && can_craft_iron_armour
+ && is_at_crafting_table
+ && is_at_furnace;
+ inventory->iron -= 3 * (int32_t)is_crafting_iron_armour;
+ inventory->coal -= 3 * (int32_t)is_crafting_iron_armour;
+ inventory->armour[iron_armour_index_to_craft] =
+ (int32_t)is_crafting_iron_armour * 1
+ + (1 - (int32_t)is_crafting_iron_armour)
+ * inventory->armour[iron_armour_index_to_craft];
+ state->achievements[CRAFTAX_ACH_MAKE_IRON_ARMOUR] =
+ state->achievements[CRAFTAX_ACH_MAKE_IRON_ARMOUR]
+ || is_crafting_iron_armour;
+
+ int32_t diamond_armour_count = 0;
+ int32_t diamond_armour_index_to_craft =
+ craftax_crafting_first_armour_below(inventory, 2, &diamond_armour_count);
+ bool can_craft_diamond_armour =
+ diamond_armour_count > 0 && inventory->diamond >= 3;
+ bool is_crafting_diamond_armour =
+ action == CRAFTAX_ACTION_MAKE_DIAMOND_ARMOUR
+ && can_craft_diamond_armour
+ && is_at_crafting_table;
+ inventory->diamond -= 3 * (int32_t)is_crafting_diamond_armour;
+ inventory->armour[diamond_armour_index_to_craft] =
+ (int32_t)is_crafting_diamond_armour * 2
+ + (1 - (int32_t)is_crafting_diamond_armour)
+ * inventory->armour[diamond_armour_index_to_craft];
+ state->achievements[CRAFTAX_ACH_MAKE_DIAMOND_ARMOUR] =
+ state->achievements[CRAFTAX_ACH_MAKE_DIAMOND_ARMOUR]
+ || is_crafting_diamond_armour;
+
+ bool can_craft_arrow = inventory->stone >= 1 && inventory->wood >= 1;
+ bool is_crafting_arrow =
+ action == CRAFTAX_ACTION_MAKE_ARROW
+ && can_craft_arrow
+ && is_at_crafting_table
+ && inventory->arrows < 99;
+ inventory->wood -= 1 * (int32_t)is_crafting_arrow;
+ inventory->stone -= 1 * (int32_t)is_crafting_arrow;
+ inventory->arrows += 2 * (int32_t)is_crafting_arrow;
+
+ bool can_craft_torch = inventory->coal >= 1 && inventory->wood >= 1;
+ bool is_crafting_torch =
+ action == CRAFTAX_ACTION_MAKE_TORCH
+ && can_craft_torch
+ && is_at_crafting_table
+ && inventory->torches < 99;
+ inventory->wood -= 1 * (int32_t)is_crafting_torch;
+ inventory->coal -= 1 * (int32_t)is_crafting_torch;
+ inventory->torches += 4 * (int32_t)is_crafting_torch;
+}
+
+static inline bool craftax_crafting_can_place_item(int32_t block) {
+ switch (block) {
+ case CRAFTAX_BLOCK_GRASS:
+ case CRAFTAX_BLOCK_SAND:
+ case CRAFTAX_BLOCK_PATH:
+ case CRAFTAX_BLOCK_FIRE_GRASS:
+ case CRAFTAX_BLOCK_ICE_GRASS:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static inline float craftax_crafting_torch_light(int32_t row, int32_t col) {
+ static const float torch_light_map[9][9] = {
+ {0.0f, 0.0f, 0.10557288f, 0.17537886f, 0.19999999f, 0.17537886f, 0.10557288f, 0.0f, 0.0f},
+ {0.0f, 0.15147191f, 0.27888972f, 0.36754447f, 0.39999998f, 0.36754447f, 0.27888972f, 0.15147191f, 0.0f},
+ {0.10557288f, 0.27888972f, 0.43431455f, 0.55278647f, 0.6f, 0.55278647f, 0.43431455f, 0.27888972f, 0.10557288f},
+ {0.17537886f, 0.36754447f, 0.55278647f, 0.71715724f, 0.8f, 0.71715724f, 0.55278647f, 0.36754447f, 0.17537886f},
+ {0.19999999f, 0.39999998f, 0.6f, 0.8f, 1.0f, 0.8f, 0.6f, 0.39999998f, 0.19999999f},
+ {0.17537886f, 0.36754447f, 0.55278647f, 0.71715724f, 0.8f, 0.71715724f, 0.55278647f, 0.36754447f, 0.17537886f},
+ {0.10557288f, 0.27888972f, 0.43431455f, 0.55278647f, 0.6f, 0.55278647f, 0.43431455f, 0.27888972f, 0.10557288f},
+ {0.0f, 0.15147191f, 0.27888972f, 0.36754447f, 0.39999998f, 0.36754447f, 0.27888972f, 0.15147191f, 0.0f},
+ {0.0f, 0.0f, 0.10557288f, 0.17537886f, 0.19999999f, 0.17537886f, 0.10557288f, 0.0f, 0.0f},
+ };
+ return torch_light_map[row][col];
+}
+
+static inline void craftax_crafting_add_torch_light(
+ CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col
+) {
+ for (int32_t dr = -4; dr <= 4; dr++) {
+ int32_t map_row = row + dr;
+ if (map_row < 0 || map_row >= CRAFTAX_MAP_SIZE) {
+ continue;
+ }
+ for (int32_t dc = -4; dc <= 4; dc++) {
+ int32_t map_col = col + dc;
+ if (map_col < 0 || map_col >= CRAFTAX_MAP_SIZE) {
+ continue;
+ }
+ float light = state->light_map[level][map_row][map_col] / 255.0f
+ + craftax_crafting_torch_light(dr + 4, dc + 4);
+ state->light_map[level][map_row][map_col] =
+ (uint8_t)(craftax_step_minf32(craftax_step_maxf32(light, 0.0f), 1.0f) * 255.0f);
+ }
+ }
+}
+
+static inline void craftax_add_new_growing_plant_native(
+ CraftaxState* state,
+ const int32_t position[2],
+ bool is_placing_sapling
+) {
+ int32_t plant_index = 0;
+ int32_t empty_count = 0;
+ for (int32_t i = 0; i < CRAFTAX_MAX_GROWING_PLANTS; i++) {
+ bool is_empty = !state->growing_plants_mask[i];
+ plant_index = (empty_count == 0 && is_empty) ? i : plant_index;
+ empty_count += (int32_t)is_empty;
+ }
+
+ bool is_adding_plant = empty_count > 0 && is_placing_sapling;
+ if (!is_adding_plant) {
+ return;
+ }
+
+ state->growing_plants_positions[plant_index][0] = position[0];
+ state->growing_plants_positions[plant_index][1] = position[1];
+ state->growing_plants_age[plant_index] = 0;
+ state->growing_plants_mask[plant_index] = true;
+}
+
+static inline void craftax_place_block_native(
+ CraftaxState* state,
+ int32_t action
+) {
+ int32_t direction[2];
+ craftax_step_direction(state->player_direction, direction);
+
+ int32_t row = state->player_position[0] + direction[0];
+ int32_t col = state->player_position[1] + direction[1];
+ bool in_bounds = row >= 0
+ && row < CRAFTAX_MAP_SIZE
+ && col >= 0
+ && col < CRAFTAX_MAP_SIZE;
+ bool in_mob = in_bounds && craftax_step_is_in_mob(state, row, col);
+ if (!in_bounds || in_mob) {
+ return;
+ }
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ int32_t original_block = state->map[level][row][col];
+ int32_t original_item = state->item_map[level][row][col];
+ bool is_placement_on_solid_block_or_item =
+ craftax_step_is_solid_block(original_block)
+ || original_item != CRAFTAX_ITEM_NONE;
+
+ CraftaxInventory* inventory = &state->inventory;
+
+ bool is_placing_crafting_table =
+ action == CRAFTAX_ACTION_PLACE_TABLE
+ && !is_placement_on_solid_block_or_item
+ && inventory->wood >= 2;
+ if (is_placing_crafting_table) {
+ craftax_set_map_block(state, level, row, col, CRAFTAX_BLOCK_CRAFTING_TABLE);
+ }
+ inventory->wood -= 2 * (int32_t)is_placing_crafting_table;
+ state->achievements[CRAFTAX_ACH_PLACE_TABLE] =
+ state->achievements[CRAFTAX_ACH_PLACE_TABLE]
+ || is_placing_crafting_table;
+
+ bool is_placing_furnace =
+ action == CRAFTAX_ACTION_PLACE_FURNACE
+ && !is_placement_on_solid_block_or_item
+ && inventory->stone > 0;
+ if (is_placing_furnace) {
+ craftax_set_map_block(state, level, row, col, CRAFTAX_BLOCK_FURNACE);
+ }
+ inventory->stone -= 1 * (int32_t)is_placing_furnace;
+ state->achievements[CRAFTAX_ACH_PLACE_FURNACE] =
+ state->achievements[CRAFTAX_ACH_PLACE_FURNACE]
+ || is_placing_furnace;
+
+ bool is_placing_on_valid_stone_block =
+ original_block == CRAFTAX_BLOCK_WATER
+ || !is_placement_on_solid_block_or_item;
+ bool is_placing_stone =
+ action == CRAFTAX_ACTION_PLACE_STONE
+ && is_placing_on_valid_stone_block
+ && inventory->stone > 0;
+ if (is_placing_stone) {
+ craftax_set_map_block(state, level, row, col, CRAFTAX_BLOCK_STONE);
+ }
+ inventory->stone -= 1 * (int32_t)is_placing_stone;
+ state->achievements[CRAFTAX_ACH_PLACE_STONE] =
+ state->achievements[CRAFTAX_ACH_PLACE_STONE]
+ || is_placing_stone;
+
+ bool is_placing_on_valid_torch_block =
+ craftax_crafting_can_place_item(original_block)
+ && state->item_map[level][row][col] == CRAFTAX_ITEM_NONE;
+ bool is_placing_torch =
+ action == CRAFTAX_ACTION_PLACE_TORCH
+ && is_placing_on_valid_torch_block
+ && inventory->torches > 0;
+ if (is_placing_torch) {
+ state->item_map[level][row][col] = CRAFTAX_ITEM_TORCH;
+ craftax_crafting_add_torch_light(state, level, row, col);
+ }
+ inventory->torches -= 1 * (int32_t)is_placing_torch;
+ state->achievements[CRAFTAX_ACH_PLACE_TORCH] =
+ state->achievements[CRAFTAX_ACH_PLACE_TORCH]
+ || is_placing_torch;
+
+ bool is_placing_sapling =
+ action == CRAFTAX_ACTION_PLACE_PLANT
+ && state->map[level][row][col] == CRAFTAX_BLOCK_GRASS
+ && inventory->sapling > 0
+ && state->item_map[level][row][col] == CRAFTAX_ITEM_NONE;
+ if (is_placing_sapling) {
+ int32_t position[2] = {row, col};
+ craftax_set_map_block(state, level, row, col, CRAFTAX_BLOCK_PLANT);
+ craftax_add_new_growing_plant_native(
+ state,
+ position,
+ is_placing_sapling
+ );
+ }
+ inventory->sapling -= 1 * (int32_t)is_placing_sapling;
+ state->achievements[CRAFTAX_ACH_PLACE_PLANT] =
+ state->achievements[CRAFTAX_ACH_PLACE_PLANT]
+ || is_placing_sapling;
+}
diff --git a/ocean/craftax/step_do_action.h b/ocean/craftax/step_do_action.h
new file mode 100644
index 0000000000..7aaab44b64
--- /dev/null
+++ b/ocean/craftax/step_do_action.h
@@ -0,0 +1,610 @@
+// Standalone native port of Craftax do_action.
+//
+// This helper intentionally is not integrated into c_step yet. It mutates a
+// full CraftaxState in place so tests can compare the subsystem directly
+// against the installed JAX implementation.
+
+#pragma once
+
+#include "step_medium.h"
+
+#define CRAFTAX_DO_ACTION_BOSS_FIGHT_SPAWN_TURNS 7
+
+static inline float craftax_do_action_mob_defense(
+ int32_t type_id,
+ int32_t mob_class_index,
+ int32_t damage_index
+) {
+ static const float defenses[8][4][3] = {
+ {
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {0.5f, 0.0f, 0.0f},
+ {0.5f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {0.2f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {0.9f, 1.0f, 0.0f},
+ {0.9f, 1.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {0.9f, 0.0f, 1.0f},
+ {0.9f, 0.0f, 1.0f},
+ {0.0f, 0.0f, 0.0f},
+ },
+ };
+
+ int32_t type_index = craftax_step_jax_index(type_id, 8);
+ int32_t class_index = craftax_step_jax_index(mob_class_index, 4);
+ int32_t component = craftax_step_jax_index(damage_index, 3);
+ return defenses[type_index][class_index][component];
+}
+
+static inline int32_t craftax_do_action_mob_achievement(
+ int32_t mob_class_index,
+ int32_t type_id
+) {
+ static const int32_t achievements[3][8] = {
+ {
+ CRAFTAX_ACH_EAT_COW,
+ CRAFTAX_ACH_EAT_BAT,
+ CRAFTAX_ACH_EAT_SNAIL,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ },
+ {
+ CRAFTAX_ACH_DEFEAT_ZOMBIE,
+ CRAFTAX_ACH_DEFEAT_GNOME_WARRIOR,
+ CRAFTAX_ACH_DEFEAT_ORC_SOLIDER,
+ CRAFTAX_ACH_DEFEAT_LIZARD,
+ CRAFTAX_ACH_DEFEAT_KNIGHT,
+ CRAFTAX_ACH_DEFEAT_TROLL,
+ CRAFTAX_ACH_DEFEAT_PIGMAN,
+ CRAFTAX_ACH_DEFEAT_FROST_TROLL,
+ },
+ {
+ CRAFTAX_ACH_DEFEAT_SKELETON,
+ CRAFTAX_ACH_DEFEAT_GNOME_ARCHER,
+ CRAFTAX_ACH_DEFEAT_ORC_MAGE,
+ CRAFTAX_ACH_DEFEAT_KOBOLD,
+ CRAFTAX_ACH_DEFEAT_ARCHER,
+ CRAFTAX_ACH_DEFEAT_DEEP_THING,
+ CRAFTAX_ACH_DEFEAT_FIRE_ELEMENTAL,
+ CRAFTAX_ACH_DEFEAT_ICE_ELEMENTAL,
+ },
+ };
+
+ int32_t class_index = craftax_step_jax_index(mob_class_index, 3);
+ int32_t type_index = craftax_step_jax_index(type_id, 8);
+ return achievements[class_index][type_index];
+}
+
+static inline void craftax_do_action_player_damage_vector(
+ const CraftaxState* state,
+ float damage_vector[3]
+) {
+ static const float physical_damages[5] = {1.0f, 2.0f, 3.0f, 5.0f, 8.0f};
+
+ int32_t sword_index = craftax_step_jax_index(state->inventory.sword, 5);
+ float physical_damage = physical_damages[sword_index];
+ float fire_damage =
+ physical_damage * (float)(state->sword_enchantment == 1) * 0.5f;
+ float ice_damage =
+ physical_damage * (float)(state->sword_enchantment == 2) * 0.5f;
+
+ physical_damage *= 1.0f + 0.25f * (float)(state->player_strength - 1);
+ fire_damage *= 1.0f + 0.05f * (float)(state->player_intelligence - 1);
+ ice_damage *= 1.0f + 0.05f * (float)(state->player_intelligence - 1);
+
+ damage_vector[0] = physical_damage;
+ damage_vector[1] = fire_damage;
+ damage_vector[2] = ice_damage;
+}
+
+static inline float craftax_do_action_damage_done(
+ const float damage_vector[3],
+ int32_t type_id,
+ int32_t mob_class_index
+) {
+ float damage = 0.0f;
+ for (int32_t i = 0; i < 3; i++) {
+ float defense = craftax_do_action_mob_defense(
+ type_id,
+ mob_class_index,
+ i
+ );
+ damage += (1.0f - defense) * damage_vector[i];
+ }
+ return damage;
+}
+
+static inline void craftax_do_action_refresh_mobs3_masks(CraftaxMobs3* mobs) {
+ for (int32_t level = 0; level < CRAFTAX_NUM_LEVELS; level++) {
+ for (int32_t i = 0; i < 3; i++) {
+ mobs->mask[level][i] =
+ mobs->mask[level][i] && mobs->health[level][i] > 0.0f;
+ }
+ }
+}
+
+static inline void craftax_do_action_refresh_mobs2_masks(CraftaxMobs2* mobs) {
+ for (int32_t level = 0; level < CRAFTAX_NUM_LEVELS; level++) {
+ for (int32_t i = 0; i < 2; i++) {
+ mobs->mask[level][i] =
+ mobs->mask[level][i] && mobs->health[level][i] > 0.0f;
+ }
+ }
+}
+
+static inline void craftax_do_action_attack_mobs3(
+ CraftaxState* state,
+ CraftaxMobs3* mobs,
+ int32_t row,
+ int32_t col,
+ const float damage_vector[3],
+ bool can_get_achievement,
+ int32_t mob_class_index,
+ bool* did_kill_mob,
+ bool* is_attacking_mob
+) {
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ bool is_attacking_array[3];
+ *is_attacking_mob = false;
+ int32_t target_mob_index = 0;
+
+ for (int32_t i = 0; i < 3; i++) {
+ bool in_mob = mobs->position[level][i][0] == row
+ && mobs->position[level][i][1] == col;
+ is_attacking_array[i] = in_mob && mobs->mask[level][i];
+ if (is_attacking_array[i] && !*is_attacking_mob) {
+ target_mob_index = i;
+ }
+ *is_attacking_mob = *is_attacking_mob || is_attacking_array[i];
+ }
+
+ int32_t target_type_id = mobs->type_id[level][target_mob_index];
+ float damage = craftax_do_action_damage_done(
+ damage_vector,
+ target_type_id,
+ mob_class_index
+ );
+ mobs->health[level][target_mob_index] -=
+ damage * (float)(int32_t)(*is_attacking_mob);
+
+ bool old_mask = mobs->mask[level][target_mob_index];
+ craftax_do_action_refresh_mobs3_masks(mobs);
+ *did_kill_mob = old_mask && !mobs->mask[level][target_mob_index];
+
+ int32_t achievement_for_kill = craftax_do_action_mob_achievement(
+ mob_class_index,
+ target_type_id
+ );
+ bool unlock = *did_kill_mob && can_get_achievement;
+ state->achievements[achievement_for_kill] =
+ state->achievements[achievement_for_kill] || unlock;
+}
+
+static inline void craftax_do_action_attack_mobs2(
+ CraftaxState* state,
+ CraftaxMobs2* mobs,
+ int32_t row,
+ int32_t col,
+ const float damage_vector[3],
+ bool can_get_achievement,
+ int32_t mob_class_index,
+ bool* did_kill_mob,
+ bool* is_attacking_mob
+) {
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ bool is_attacking_array[2];
+ *is_attacking_mob = false;
+ int32_t target_mob_index = 0;
+
+ for (int32_t i = 0; i < 2; i++) {
+ bool in_mob = mobs->position[level][i][0] == row
+ && mobs->position[level][i][1] == col;
+ is_attacking_array[i] = in_mob && mobs->mask[level][i];
+ if (is_attacking_array[i] && !*is_attacking_mob) {
+ target_mob_index = i;
+ }
+ *is_attacking_mob = *is_attacking_mob || is_attacking_array[i];
+ }
+
+ int32_t target_type_id = mobs->type_id[level][target_mob_index];
+ float damage = craftax_do_action_damage_done(
+ damage_vector,
+ target_type_id,
+ mob_class_index
+ );
+ mobs->health[level][target_mob_index] -=
+ damage * (float)(int32_t)(*is_attacking_mob);
+
+ bool old_mask = mobs->mask[level][target_mob_index];
+ craftax_do_action_refresh_mobs2_masks(mobs);
+ *did_kill_mob = old_mask && !mobs->mask[level][target_mob_index];
+
+ int32_t achievement_for_kill = craftax_do_action_mob_achievement(
+ mob_class_index,
+ target_type_id
+ );
+ bool unlock = *did_kill_mob && can_get_achievement;
+ state->achievements[achievement_for_kill] =
+ state->achievements[achievement_for_kill] || unlock;
+}
+
+static inline bool craftax_do_action_update_index(
+ int32_t index,
+ int32_t size,
+ int32_t* mapped_index
+) {
+ if (index < -size || index >= size) {
+ return false;
+ }
+ *mapped_index = index < 0 ? index + size : index;
+ return true;
+}
+
+static inline void craftax_do_action_update_mob_map(
+ CraftaxState* state,
+ int32_t row,
+ int32_t col,
+ bool did_kill_mob
+) {
+ int32_t update_row;
+ int32_t update_col;
+ if (!craftax_do_action_update_index(row, CRAFTAX_MAP_SIZE, &update_row)
+ || !craftax_do_action_update_index(col, CRAFTAX_MAP_SIZE, &update_col)) {
+ return;
+ }
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ int32_t read_row = craftax_step_jax_index(row, CRAFTAX_MAP_SIZE);
+ int32_t read_col = craftax_step_jax_index(col, CRAFTAX_MAP_SIZE);
+ bool old_value = (state->mob_bits[level][read_row] >> read_col) & 1ULL;
+ bool new_value = old_value && !did_kill_mob;
+ if (new_value) {
+ state->mob_bits[level][update_row] |= (1ULL << update_col);
+ } else {
+ state->mob_bits[level][update_row] &= ~(1ULL << update_col);
+ }
+}
+
+static inline void craftax_do_action_attack_mob(
+ CraftaxState* state,
+ int32_t row,
+ int32_t col,
+ bool can_eat,
+ bool* did_attack_mob,
+ bool* did_kill_mob
+) {
+ float damage_vector[3];
+ craftax_do_action_player_damage_vector(state, damage_vector);
+
+ bool did_kill_melee_mob = false;
+ bool is_attacking_melee_mob = false;
+ craftax_do_action_attack_mobs3(
+ state,
+ &state->melee_mobs,
+ row,
+ col,
+ damage_vector,
+ true,
+ 1,
+ &did_kill_melee_mob,
+ &is_attacking_melee_mob
+ );
+
+ bool did_kill_passive_mob = false;
+ bool is_attacking_passive_mob = false;
+ craftax_do_action_attack_mobs3(
+ state,
+ &state->passive_mobs,
+ row,
+ col,
+ damage_vector,
+ can_eat,
+ 0,
+ &did_kill_passive_mob,
+ &is_attacking_passive_mob
+ );
+
+ if (did_kill_passive_mob && can_eat) {
+ state->player_food = craftax_step_mini32(
+ craftax_step_get_max_food(state),
+ state->player_food + 6
+ );
+ state->player_hunger = 0.0f;
+ }
+
+ bool did_kill_ranged_mob = false;
+ bool is_attacking_ranged_mob = false;
+ craftax_do_action_attack_mobs2(
+ state,
+ &state->ranged_mobs,
+ row,
+ col,
+ damage_vector,
+ true,
+ 2,
+ &did_kill_ranged_mob,
+ &is_attacking_ranged_mob
+ );
+
+ *did_attack_mob = is_attacking_melee_mob
+ || is_attacking_passive_mob
+ || is_attacking_ranged_mob;
+ bool did_kill_monster = did_kill_melee_mob || did_kill_ranged_mob;
+ *did_kill_mob = did_kill_monster || did_kill_passive_mob;
+
+ craftax_do_action_update_mob_map(state, row, col, *did_kill_mob);
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ state->monsters_killed[level] += (int32_t)did_kill_monster;
+}
+
+static inline bool craftax_do_action_in_bounds(int32_t row, int32_t col) {
+ return row >= 0
+ && row < CRAFTAX_MAP_SIZE
+ && col >= 0
+ && col < CRAFTAX_MAP_SIZE;
+}
+
+static inline bool craftax_do_action_boss_vulnerable(
+ const CraftaxState* state
+) {
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ int32_t melee_count = 0;
+ int32_t ranged_count = 0;
+ for (int32_t i = 0; i < CRAFTAX_MAX_MELEE_MOBS; i++) {
+ melee_count += (int32_t)state->melee_mobs.mask[level][i];
+ }
+ for (int32_t i = 0; i < CRAFTAX_MAX_RANGED_MOBS; i++) {
+ ranged_count += (int32_t)state->ranged_mobs.mask[level][i];
+ }
+ return melee_count == 0
+ && ranged_count == 0
+ && state->boss_timesteps_to_spawn_this_round <= 0;
+}
+
+static inline void craftax_do_action_update_plants_with_eat(
+ CraftaxState* state,
+ int32_t row,
+ int32_t col
+) {
+ int32_t plant_index = 0;
+ bool found = false;
+ for (int32_t i = 0; i < CRAFTAX_MAX_GROWING_PLANTS; i++) {
+ bool is_plant = state->growing_plants_positions[i][0] == row
+ && state->growing_plants_positions[i][1] == col;
+ if (is_plant && !found) {
+ plant_index = i;
+ found = true;
+ }
+ }
+ state->growing_plants_age[plant_index] = 0;
+}
+
+static inline void craftax_do_action_native(
+ CraftaxState* state,
+ int32_t action,
+ CraftaxThreefryKey rng
+) {
+ if (action != CRAFTAX_ACTION_DO) {
+ return;
+ }
+
+ int32_t direction[2];
+ craftax_step_direction(state->player_direction, direction);
+ int32_t target_row = state->player_position[0] + direction[0];
+ int32_t target_col = state->player_position[1] + direction[1];
+
+ bool did_attack_mob = false;
+ bool did_kill_mob = false;
+ craftax_do_action_attack_mob(
+ state,
+ target_row,
+ target_col,
+ true,
+ &did_attack_mob,
+ &did_kill_mob
+ );
+ (void)did_kill_mob;
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ int32_t read_row = craftax_step_jax_index(target_row, CRAFTAX_MAP_SIZE);
+ int32_t read_col = craftax_step_jax_index(target_col, CRAFTAX_MAP_SIZE);
+ int32_t target_block = state->map[level][read_row][read_col];
+
+ CraftaxThreefryKey sapling_key = craftax_medium_next_random_key(&rng);
+ CraftaxThreefryKey chest_key = craftax_medium_next_random_key(&rng);
+
+ bool is_opening_chest = target_block == CRAFTAX_BLOCK_CHEST;
+ bool is_damaging_boss = target_block == CRAFTAX_BLOCK_NECROMANCER
+ && craftax_do_action_boss_vulnerable(state)
+ && craftax_step_is_fighting_boss(state);
+
+ bool action_block_in_bounds =
+ craftax_do_action_in_bounds(target_row, target_col) && !did_attack_mob;
+
+ if (action_block_in_bounds) {
+ bool is_block_tree = target_block == CRAFTAX_BLOCK_TREE;
+ bool is_block_fire_tree = target_block == CRAFTAX_BLOCK_FIRE_TREE;
+ bool is_block_ice_shrub = target_block == CRAFTAX_BLOCK_ICE_SHRUB;
+ bool is_mining_tree =
+ is_block_tree || is_block_fire_tree || is_block_ice_shrub;
+ if (is_mining_tree) {
+ int32_t replacement = is_block_tree
+ ? CRAFTAX_BLOCK_GRASS
+ : (is_block_fire_tree
+ ? CRAFTAX_BLOCK_FIRE_GRASS
+ : CRAFTAX_BLOCK_ICE_GRASS);
+ craftax_set_map_block(state, level, target_row, target_col, replacement);
+ state->inventory.wood += 1;
+ }
+
+ bool is_mining_stone = target_block == CRAFTAX_BLOCK_STONE
+ && state->inventory.pickaxe >= 1;
+ if (is_mining_stone) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ state->inventory.stone += 1;
+ }
+
+ if (target_block == CRAFTAX_BLOCK_FURNACE) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ }
+
+ if (target_block == CRAFTAX_BLOCK_CRAFTING_TABLE) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ }
+
+ bool is_mining_coal = target_block == CRAFTAX_BLOCK_COAL
+ && state->inventory.pickaxe >= 1;
+ if (is_mining_coal) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ state->inventory.coal += 1;
+ }
+
+ bool is_mining_iron = target_block == CRAFTAX_BLOCK_IRON
+ && state->inventory.pickaxe >= 2;
+ if (is_mining_iron) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ state->inventory.iron += 1;
+ }
+
+ bool is_mining_diamond = target_block == CRAFTAX_BLOCK_DIAMOND
+ && state->inventory.pickaxe >= 3;
+ if (is_mining_diamond) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ state->inventory.diamond += 1;
+ }
+
+ bool is_mining_sapphire = target_block == CRAFTAX_BLOCK_SAPPHIRE
+ && state->inventory.pickaxe >= 4;
+ if (is_mining_sapphire) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ state->inventory.sapphire += 1;
+ }
+
+ bool is_mining_ruby = target_block == CRAFTAX_BLOCK_RUBY
+ && state->inventory.pickaxe >= 4;
+ if (is_mining_ruby) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ state->inventory.ruby += 1;
+ }
+
+ bool is_mining_sapling = target_block == CRAFTAX_BLOCK_GRASS
+ && craftax_threefry_uniform_f32(sapling_key) < 0.1f;
+ state->inventory.sapling += (int32_t)is_mining_sapling;
+
+ bool is_drinking_water = target_block == CRAFTAX_BLOCK_WATER
+ || target_block == CRAFTAX_BLOCK_FOUNTAIN;
+ if (is_drinking_water) {
+ state->player_drink = craftax_step_mini32(
+ craftax_step_get_max_drink(state),
+ state->player_drink + 1
+ );
+ state->player_thirst = 0.0f;
+ state->achievements[CRAFTAX_ACH_COLLECT_DRINK] = true;
+ }
+
+ bool is_eating_plant = target_block == CRAFTAX_BLOCK_RIPE_PLANT;
+ if (is_eating_plant) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PLANT);
+ state->player_food = craftax_step_mini32(
+ craftax_step_get_max_food(state),
+ state->player_food + 4
+ );
+ state->player_hunger = 0.0f;
+ state->achievements[CRAFTAX_ACH_EAT_PLANT] = true;
+ craftax_do_action_update_plants_with_eat(
+ state,
+ target_row,
+ target_col
+ );
+ }
+
+ bool is_mining_stalagmite = target_block == CRAFTAX_BLOCK_STALAGMITE
+ && state->inventory.pickaxe >= 1;
+ if (is_mining_stalagmite) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ state->inventory.stone += 1;
+ }
+
+ if (is_opening_chest) {
+ craftax_set_map_block(state, level, target_row, target_col, CRAFTAX_BLOCK_PATH);
+ craftax_add_items_from_chest_native(
+ state,
+ &state->inventory,
+ true,
+ chest_key
+ );
+ state->achievements[CRAFTAX_ACH_OPEN_CHEST] = true;
+ }
+
+ if (is_damaging_boss) {
+ state->achievements[CRAFTAX_ACH_DAMAGE_NECROMANCER] = true;
+ }
+ }
+
+ state->chests_opened[level] =
+ state->chests_opened[level] || is_opening_chest;
+
+ state->boss_progress += (int32_t)is_damaging_boss;
+ if (is_damaging_boss) {
+ state->boss_timesteps_to_spawn_this_round =
+ CRAFTAX_DO_ACTION_BOSS_FIGHT_SPAWN_TURNS;
+ }
+}
diff --git a/ocean/craftax/step_medium.h b/ocean/craftax/step_medium.h
new file mode 100644
index 0000000000..9f5ac1aae1
--- /dev/null
+++ b/ocean/craftax/step_medium.h
@@ -0,0 +1,459 @@
+// Standalone native ports of medium Craftax step subsystems.
+//
+// These helpers intentionally are not integrated into c_step yet. They mutate a
+// full CraftaxState, or an Inventory plus read-only state context, so tests can
+// compare each subsystem directly against the installed JAX implementation.
+
+#pragma once
+
+#include "step_simple.h"
+
+static inline CraftaxThreefryKey craftax_medium_next_random_key(
+ CraftaxThreefryKey* rng
+) {
+ CraftaxThreefryKey draw;
+ craftax_threefry_split(*rng, rng, &draw);
+ return draw;
+}
+
+static inline int32_t craftax_medium_randint(
+ CraftaxThreefryKey key,
+ int32_t minval,
+ int32_t maxval
+) {
+ return craftax_randint_i32_at(key, 0u, minval, maxval);
+}
+
+static inline int32_t craftax_medium_choice_weighted(
+ CraftaxThreefryKey key,
+ const float* weights,
+ int32_t count
+) {
+ float total = 0.0f;
+ for (int32_t i = 0; i < count; i++) {
+ total += weights[i];
+ }
+
+ float draw = total * (1.0f - craftax_threefry_uniform_f32(key));
+ float cumulative = 0.0f;
+ for (int32_t i = 0; i < count; i++) {
+ cumulative += weights[i];
+ if (cumulative >= draw) {
+ return i;
+ }
+ }
+ return count - 1;
+}
+
+static inline int32_t craftax_medium_projectile_count(const CraftaxState* state) {
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ int32_t count = 0;
+ for (int32_t i = 0; i < CRAFTAX_MAX_PLAYER_PROJECTILES; i++) {
+ count += (int32_t)state->player_projectiles.mask[level][i];
+ }
+ return count;
+}
+
+static inline int32_t craftax_medium_first_projectile_slot(
+ const CraftaxState* state
+) {
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ for (int32_t i = 0; i < CRAFTAX_MAX_PLAYER_PROJECTILES; i++) {
+ if (!state->player_projectiles.mask[level][i]) {
+ return i;
+ }
+ }
+ return 0;
+}
+
+static inline void craftax_medium_spawn_player_projectile(
+ CraftaxState* state,
+ bool is_spawning_projectile,
+ const int32_t new_projectile_position[2],
+ const int32_t direction[2],
+ int32_t projectile_type
+) {
+ if (!is_spawning_projectile) {
+ return;
+ }
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ int32_t index = craftax_medium_first_projectile_slot(state);
+ state->player_projectiles.position[level][index][0] = new_projectile_position[0];
+ state->player_projectiles.position[level][index][1] = new_projectile_position[1];
+ state->player_projectiles.mask[level][index] = true;
+ state->player_projectiles.type_id[level][index] = projectile_type;
+ state->player_projectile_directions[level][index][0] = direction[0];
+ state->player_projectile_directions[level][index][1] = direction[1];
+}
+
+static inline int32_t craftax_medium_level_achievement(int32_t level) {
+ switch (craftax_step_jax_index(level, CRAFTAX_NUM_LEVELS)) {
+ case 1:
+ return CRAFTAX_ACH_ENTER_DUNGEON;
+ case 2:
+ return CRAFTAX_ACH_ENTER_GNOMISH_MINES;
+ case 3:
+ return CRAFTAX_ACH_ENTER_SEWERS;
+ case 4:
+ return CRAFTAX_ACH_ENTER_VAULT;
+ case 5:
+ return CRAFTAX_ACH_ENTER_TROLL_MINES;
+ case 6:
+ return CRAFTAX_ACH_ENTER_FIRE_REALM;
+ case 7:
+ return CRAFTAX_ACH_ENTER_ICE_REALM;
+ case 8:
+ return CRAFTAX_ACH_ENTER_GRAVEYARD;
+ default:
+ return CRAFTAX_ACH_COLLECT_WOOD;
+ }
+}
+
+static inline void craftax_shoot_projectile_native(
+ CraftaxState* state,
+ int32_t action
+) {
+ bool is_shooting_arrow = action == CRAFTAX_ACTION_SHOOT_ARROW
+ && state->inventory.bow >= 1
+ && state->inventory.arrows >= 1
+ && craftax_medium_projectile_count(state) < CRAFTAX_MAX_PLAYER_PROJECTILES;
+
+ int32_t direction[2];
+ craftax_step_direction(state->player_direction, direction);
+ craftax_medium_spawn_player_projectile(
+ state,
+ is_shooting_arrow,
+ state->player_position,
+ direction,
+ CRAFTAX_PROJECTILE_ARROW2
+ );
+
+ state->achievements[CRAFTAX_ACH_FIRE_BOW] =
+ state->achievements[CRAFTAX_ACH_FIRE_BOW] || is_shooting_arrow;
+ state->inventory.arrows -= (int32_t)is_shooting_arrow;
+}
+
+static inline void craftax_cast_spell_native(
+ CraftaxState* state,
+ int32_t action
+) {
+ bool has_projectile_slot =
+ craftax_medium_projectile_count(state) < CRAFTAX_MAX_PLAYER_PROJECTILES;
+ bool has_mana = state->player_mana >= 2;
+ bool is_casting_fireball = action == CRAFTAX_ACTION_CAST_FIREBALL
+ && has_mana
+ && has_projectile_slot
+ && state->learned_spells[0];
+ bool is_casting_iceball = action == CRAFTAX_ACTION_CAST_ICEBALL
+ && has_mana
+ && has_projectile_slot
+ && state->learned_spells[1];
+ bool is_casting_spell = is_casting_fireball || is_casting_iceball;
+
+ int32_t projectile_type =
+ (int32_t)is_casting_fireball * CRAFTAX_PROJECTILE_FIREBALL
+ + (int32_t)is_casting_iceball * CRAFTAX_PROJECTILE_ICEBALL;
+
+ int32_t direction[2];
+ craftax_step_direction(state->player_direction, direction);
+ craftax_medium_spawn_player_projectile(
+ state,
+ is_casting_spell,
+ state->player_position,
+ direction,
+ projectile_type
+ );
+
+ if (is_casting_fireball) {
+ state->achievements[CRAFTAX_ACH_CAST_FIREBALL] = true;
+ }
+ if (is_casting_iceball) {
+ state->achievements[CRAFTAX_ACH_CAST_ICEBALL] = true;
+ }
+ state->player_mana -= (int32_t)is_casting_spell * 2;
+}
+
+static inline void craftax_enchant_native(
+ CraftaxState* state,
+ int32_t action,
+ CraftaxThreefryKey rng
+) {
+ int32_t direction[2];
+ craftax_step_direction(state->player_direction, direction);
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ int32_t target_row = craftax_step_jax_index(
+ state->player_position[0] + direction[0],
+ CRAFTAX_MAP_SIZE
+ );
+ int32_t target_col = craftax_step_jax_index(
+ state->player_position[1] + direction[1],
+ CRAFTAX_MAP_SIZE
+ );
+ int32_t target_block = state->map[level][target_row][target_col];
+
+ bool is_fire_table = target_block == CRAFTAX_BLOCK_ENCHANTMENT_TABLE_FIRE;
+ bool is_ice_table = target_block == CRAFTAX_BLOCK_ENCHANTMENT_TABLE_ICE;
+ bool target_block_is_enchantment_table = is_fire_table || is_ice_table;
+ int32_t enchantment_type = is_fire_table ? 1 : 2;
+ int32_t num_gems = is_fire_table
+ ? state->inventory.ruby
+ : state->inventory.sapphire;
+
+ bool could_enchant = state->player_mana >= 9
+ && target_block_is_enchantment_table
+ && num_gems >= 1;
+ bool is_enchanting_bow = could_enchant
+ && action == CRAFTAX_ACTION_ENCHANT_BOW
+ && state->inventory.bow > 0;
+ bool is_enchanting_sword = could_enchant
+ && action == CRAFTAX_ACTION_ENCHANT_SWORD
+ && state->inventory.sword > 0;
+
+ int32_t armour_count = 0;
+ for (int32_t i = 0; i < 4; i++) {
+ armour_count += state->inventory.armour[i];
+ }
+ bool is_enchanting_armour = could_enchant
+ && action == CRAFTAX_ACTION_ENCHANT_ARMOUR
+ && armour_count > 0;
+
+ CraftaxThreefryKey armour_key = craftax_medium_next_random_key(&rng);
+ int32_t unenchanted_count = 0;
+ for (int32_t i = 0; i < 4; i++) {
+ unenchanted_count += (int32_t)(state->armour_enchantments[i] == 0);
+ }
+
+ float armour_targets[4];
+ for (int32_t i = 0; i < 4; i++) {
+ bool unenchanted = state->armour_enchantments[i] == 0;
+ bool opposite_enchanted = state->armour_enchantments[i] != 0
+ && state->armour_enchantments[i] != enchantment_type;
+ armour_targets[i] = (unenchanted || (
+ unenchanted_count == 0 && opposite_enchanted
+ )) ? 1.0f : 0.0f;
+ }
+ int32_t armour_target = craftax_medium_choice_weighted(
+ armour_key,
+ armour_targets,
+ 4
+ );
+
+ bool is_enchanting = is_enchanting_sword
+ || is_enchanting_bow
+ || is_enchanting_armour;
+ if (is_enchanting_sword) {
+ state->sword_enchantment = enchantment_type;
+ state->achievements[CRAFTAX_ACH_ENCHANT_SWORD] = true;
+ }
+ if (is_enchanting_bow) {
+ state->bow_enchantment = enchantment_type;
+ }
+ if (is_enchanting_armour) {
+ state->armour_enchantments[armour_target] = enchantment_type;
+ state->achievements[CRAFTAX_ACH_ENCHANT_ARMOUR] = true;
+ }
+
+ state->inventory.sapphire -=
+ (int32_t)is_enchanting * (int32_t)(enchantment_type == 2);
+ state->inventory.ruby -=
+ (int32_t)is_enchanting * (int32_t)(enchantment_type == 1);
+ state->player_mana -= (int32_t)is_enchanting * 9;
+}
+
+static inline void craftax_change_floor_native(
+ CraftaxState* state,
+ int32_t action
+) {
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ int32_t player_row = craftax_step_jax_index(
+ state->player_position[0],
+ CRAFTAX_MAP_SIZE
+ );
+ int32_t player_col = craftax_step_jax_index(
+ state->player_position[1],
+ CRAFTAX_MAP_SIZE
+ );
+
+ bool on_down_ladder =
+ state->item_map[level][player_row][player_col] == CRAFTAX_ITEM_LADDER_DOWN;
+ bool is_moving_down = action == CRAFTAX_ACTION_DESCEND
+ && on_down_ladder
+ && state->monsters_killed[level] >= CRAFTAX_MONSTERS_KILLED_TO_CLEAR_LEVEL
+ && state->player_level < CRAFTAX_NUM_LEVELS - 1;
+
+ bool on_up_ladder =
+ state->item_map[level][player_row][player_col] == CRAFTAX_ITEM_LADDER_UP;
+ bool is_moving_up = action == CRAFTAX_ACTION_ASCEND
+ && on_up_ladder
+ && state->player_level > 0;
+
+ int32_t delta_floor = (int32_t)is_moving_down - (int32_t)is_moving_up;
+ int32_t new_level = state->player_level + delta_floor;
+ int32_t achievement = craftax_medium_level_achievement(new_level);
+ bool new_floor = new_level != 0 && !state->achievements[achievement];
+
+ if (is_moving_down) {
+ int32_t ladder_level = craftax_step_jax_index(
+ state->player_level + 1,
+ CRAFTAX_NUM_LEVELS
+ );
+ state->player_position[0] = state->up_ladders[ladder_level][0];
+ state->player_position[1] = state->up_ladders[ladder_level][1];
+ } else if (is_moving_up) {
+ int32_t ladder_level = craftax_step_jax_index(
+ state->player_level - 1,
+ CRAFTAX_NUM_LEVELS
+ );
+ state->player_position[0] = state->down_ladders[ladder_level][0];
+ state->player_position[1] = state->down_ladders[ladder_level][1];
+ }
+
+ state->player_level = new_level;
+ state->achievements[achievement] =
+ state->achievements[achievement] || new_level != 0;
+ state->player_xp += (int32_t)new_floor;
+}
+
+static inline void craftax_add_items_from_chest_native(
+ const CraftaxState* state,
+ CraftaxInventory* inventory,
+ bool is_opening_chest,
+ CraftaxThreefryKey rng
+) {
+ CraftaxThreefryKey draw_key;
+
+ draw_key = craftax_medium_next_random_key(&rng);
+ bool is_looting_wood = craftax_threefry_uniform_f32(draw_key) < 0.6f;
+ draw_key = craftax_medium_next_random_key(&rng);
+ int32_t wood_loot_amount =
+ craftax_medium_randint(draw_key, 1, 6) * (int32_t)is_looting_wood;
+ (void)wood_loot_amount;
+
+ draw_key = craftax_medium_next_random_key(&rng);
+ bool is_looting_torch = craftax_threefry_uniform_f32(draw_key) < 0.6f;
+ draw_key = craftax_medium_next_random_key(&rng);
+ int32_t torch_loot_amount =
+ craftax_medium_randint(draw_key, 4, 8) * (int32_t)is_looting_torch;
+
+ draw_key = craftax_medium_next_random_key(&rng);
+ bool is_looting_ore = craftax_threefry_uniform_f32(draw_key) < 0.6f;
+ draw_key = craftax_medium_next_random_key(&rng);
+ float ore_weights[5] = {0.3f, 0.3f, 0.15f, 0.125f, 0.125f};
+ int32_t ore_loot_id = craftax_medium_choice_weighted(
+ draw_key,
+ ore_weights,
+ 5
+ );
+ draw_key = craftax_medium_next_random_key(&rng);
+
+ int32_t coal_loot_amount =
+ craftax_medium_randint(draw_key, 1, 4)
+ * (int32_t)(ore_loot_id == 0)
+ * (int32_t)is_looting_ore;
+ int32_t iron_loot_amount =
+ craftax_medium_randint(draw_key, 1, 3)
+ * (int32_t)(ore_loot_id == 1)
+ * (int32_t)is_looting_ore;
+ int32_t diamond_loot_amount =
+ craftax_medium_randint(draw_key, 1, 2)
+ * (int32_t)(ore_loot_id == 2)
+ * (int32_t)is_looting_ore;
+ int32_t sapphire_loot_amount =
+ craftax_medium_randint(draw_key, 1, 2)
+ * (int32_t)(ore_loot_id == 3)
+ * (int32_t)is_looting_ore;
+ int32_t ruby_loot_amount =
+ craftax_medium_randint(draw_key, 1, 2)
+ * (int32_t)(ore_loot_id == 4)
+ * (int32_t)is_looting_ore;
+
+ draw_key = craftax_medium_next_random_key(&rng);
+ bool is_looting_potion = craftax_threefry_uniform_f32(draw_key) < 0.5f;
+ draw_key = craftax_medium_next_random_key(&rng);
+ int32_t potion_loot_index = craftax_medium_randint(draw_key, 0, 6);
+ draw_key = craftax_medium_next_random_key(&rng);
+ int32_t potion_loot_amount = craftax_medium_randint(draw_key, 1, 3);
+
+ draw_key = craftax_medium_next_random_key(&rng);
+ bool is_looting_arrows = craftax_threefry_uniform_f32(draw_key) < 0.25f;
+ draw_key = craftax_medium_next_random_key(&rng);
+ int32_t arrows_loot_amount =
+ craftax_medium_randint(draw_key, 1, 5) * (int32_t)is_looting_arrows;
+
+ draw_key = craftax_medium_next_random_key(&rng);
+ bool is_looting_tool = craftax_threefry_uniform_f32(draw_key) < 0.2f;
+ draw_key = craftax_medium_next_random_key(&rng);
+ int32_t tool_id = craftax_medium_randint(draw_key, 0, 2);
+
+ bool is_looting_pickaxe = is_looting_tool
+ && tool_id == 0
+ && is_opening_chest;
+ draw_key = craftax_medium_next_random_key(&rng);
+ float tool_weights[4] = {0.4f, 0.3f, 0.2f, 0.1f};
+ int32_t pickaxe_loot_level = (
+ craftax_medium_choice_weighted(draw_key, tool_weights, 4) + 1
+ ) * (int32_t)is_looting_pickaxe;
+ pickaxe_loot_level = craftax_step_maxi32(
+ pickaxe_loot_level,
+ inventory->pickaxe
+ );
+ int32_t new_pickaxe_level = is_looting_pickaxe
+ ? pickaxe_loot_level
+ : inventory->pickaxe;
+
+ bool is_looting_sword = is_looting_tool
+ && tool_id == 1
+ && is_opening_chest;
+ draw_key = craftax_medium_next_random_key(&rng);
+ int32_t sword_loot_level = (
+ craftax_medium_choice_weighted(draw_key, tool_weights, 4) + 1
+ ) * (int32_t)is_looting_sword;
+ sword_loot_level = craftax_step_maxi32(sword_loot_level, inventory->sword);
+ int32_t new_sword_level = is_looting_sword
+ ? sword_loot_level
+ : inventory->sword;
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ bool is_looting_bow = is_opening_chest
+ && state->player_level == 1
+ && !state->chests_opened[level];
+ int32_t new_bow_level = is_looting_bow ? 1 : inventory->bow;
+
+ bool is_looting_book = !state->chests_opened[level]
+ && (state->player_level == 3 || state->player_level == 4);
+
+ int32_t opening = (int32_t)is_opening_chest;
+ inventory->torches += torch_loot_amount * opening;
+ inventory->coal += coal_loot_amount * opening;
+ inventory->iron += iron_loot_amount * opening;
+ inventory->diamond += diamond_loot_amount * opening;
+ inventory->sapphire += sapphire_loot_amount * opening;
+ inventory->ruby += ruby_loot_amount * opening;
+ inventory->arrows += arrows_loot_amount * opening;
+ inventory->pickaxe = new_pickaxe_level;
+ inventory->sword = new_sword_level;
+ inventory->potions[potion_loot_index] +=
+ potion_loot_amount * (int32_t)is_looting_potion * opening;
+ inventory->bow = new_bow_level;
+ inventory->books += (int32_t)is_looting_book * opening;
+}
diff --git a/ocean/craftax/step_simple.h b/ocean/craftax/step_simple.h
new file mode 100644
index 0000000000..c643160f4b
--- /dev/null
+++ b/ocean/craftax/step_simple.h
@@ -0,0 +1,556 @@
+// Standalone native ports of simple Craftax step subsystems.
+//
+// These helpers intentionally are not integrated into c_step yet. They mutate a
+// full CraftaxState in place so tests can compare each subsystem directly
+// against the installed JAX implementation.
+
+#pragma once
+
+#include "craftax.h"
+
+static inline int32_t craftax_step_jax_index(int32_t index, int32_t size) {
+ if (index < 0) {
+ index += size;
+ }
+ if (index < 0) {
+ return 0;
+ }
+ if (index >= size) {
+ return size - 1;
+ }
+ return index;
+}
+
+static inline int32_t craftax_step_mini32(int32_t a, int32_t b) {
+ return a < b ? a : b;
+}
+
+static inline int32_t craftax_step_maxi32(int32_t a, int32_t b) {
+ return a > b ? a : b;
+}
+
+static inline float craftax_step_minf32(float a, float b) {
+ if (isnan(a) || isnan(b)) {
+ return NAN;
+ }
+ return a < b ? a : b;
+}
+
+static inline float craftax_step_maxf32(float a, float b) {
+ if (isnan(a) || isnan(b)) {
+ return NAN;
+ }
+ return a > b ? a : b;
+}
+
+static inline int32_t craftax_step_get_max_health(const CraftaxState* state) {
+ return 8 + state->player_strength;
+}
+
+static inline int32_t craftax_step_get_max_food(const CraftaxState* state) {
+ return 7 + 2 * state->player_dexterity;
+}
+
+static inline int32_t craftax_step_get_max_drink(const CraftaxState* state) {
+ return 7 + 2 * state->player_dexterity;
+}
+
+static inline int32_t craftax_step_get_max_energy(const CraftaxState* state) {
+ return 7 + 2 * state->player_dexterity;
+}
+
+static inline int32_t craftax_step_get_max_mana(const CraftaxState* state) {
+ return 6 + 3 * state->player_intelligence;
+}
+
+static inline bool craftax_step_is_fighting_boss(const CraftaxState* state) {
+ return state->player_level == CRAFTAX_NUM_LEVELS - 1;
+}
+
+static inline bool craftax_step_has_beaten_boss(const CraftaxState* state) {
+ return state->boss_progress >= CRAFTAX_NUM_LEVELS - 1;
+}
+
+static inline void craftax_step_direction(int32_t action, int32_t direction[2]) {
+ direction[0] = 0;
+ direction[1] = 0;
+ int32_t direction_index = craftax_step_jax_index(action, 16);
+ if (direction_index == CRAFTAX_ACTION_LEFT) {
+ direction[1] = -1;
+ } else if (direction_index == CRAFTAX_ACTION_RIGHT) {
+ direction[1] = 1;
+ } else if (direction_index == CRAFTAX_ACTION_UP) {
+ direction[0] = -1;
+ } else if (direction_index == CRAFTAX_ACTION_DOWN) {
+ direction[0] = 1;
+ }
+}
+
+static inline bool craftax_step_is_solid_block(int32_t block) {
+ switch (block) {
+ case CRAFTAX_BLOCK_STONE:
+ case CRAFTAX_BLOCK_TREE:
+ case CRAFTAX_BLOCK_COAL:
+ case CRAFTAX_BLOCK_IRON:
+ case CRAFTAX_BLOCK_DIAMOND:
+ case CRAFTAX_BLOCK_CRAFTING_TABLE:
+ case CRAFTAX_BLOCK_FURNACE:
+ case CRAFTAX_BLOCK_PLANT:
+ case CRAFTAX_BLOCK_RIPE_PLANT:
+ case CRAFTAX_BLOCK_WALL:
+ case CRAFTAX_BLOCK_WALL_MOSS:
+ case CRAFTAX_BLOCK_STALAGMITE:
+ case CRAFTAX_BLOCK_RUBY:
+ case CRAFTAX_BLOCK_SAPPHIRE:
+ case CRAFTAX_BLOCK_CHEST:
+ case CRAFTAX_BLOCK_FOUNTAIN:
+ case CRAFTAX_BLOCK_FIRE_TREE:
+ case CRAFTAX_BLOCK_ENCHANTMENT_TABLE_FIRE:
+ case CRAFTAX_BLOCK_ENCHANTMENT_TABLE_ICE:
+ case CRAFTAX_BLOCK_GRAVE:
+ case CRAFTAX_BLOCK_GRAVE2:
+ case CRAFTAX_BLOCK_GRAVE3:
+ case CRAFTAX_BLOCK_NECROMANCER:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static inline bool craftax_step_is_in_mob(
+ const CraftaxState* state,
+ int32_t row,
+ int32_t col
+) {
+ int32_t level = craftax_step_jax_index(state->player_level, CRAFTAX_NUM_LEVELS);
+ int32_t map_row = craftax_step_jax_index(row, CRAFTAX_MAP_SIZE);
+ int32_t map_col = craftax_step_jax_index(col, CRAFTAX_MAP_SIZE);
+ bool player_here = state->player_position[0] == row
+ && state->player_position[1] == col;
+ return ((state->mob_bits[level][map_row] >> map_col) & 1ULL) || player_here;
+}
+
+static inline bool craftax_step_valid_land_position(
+ const CraftaxState* state,
+ int32_t row,
+ int32_t col
+) {
+ bool pos_in_bounds = row >= 0
+ && row < CRAFTAX_MAP_SIZE
+ && col >= 0
+ && col < CRAFTAX_MAP_SIZE;
+ int32_t level = craftax_step_jax_index(state->player_level, CRAFTAX_NUM_LEVELS);
+ int32_t map_row = craftax_step_jax_index(row, CRAFTAX_MAP_SIZE);
+ int32_t map_col = craftax_step_jax_index(col, CRAFTAX_MAP_SIZE);
+ int32_t block = state->map[level][map_row][map_col];
+ bool in_solid_block = craftax_step_is_solid_block(block);
+ bool in_mob = craftax_step_is_in_mob(state, row, col);
+ bool in_lava = block == CRAFTAX_BLOCK_LAVA;
+ bool in_water = block == CRAFTAX_BLOCK_WATER;
+
+ bool valid_move = pos_in_bounds && !in_mob && !in_solid_block;
+ valid_move = valid_move && !in_water;
+ valid_move = valid_move && !in_lava;
+ return valid_move;
+}
+
+static inline void craftax_move_player_native(
+ CraftaxState* state,
+ int32_t action,
+ bool god_mode
+) {
+ int32_t direction[2];
+ craftax_step_direction(action, direction);
+
+ int32_t proposed_row = state->player_position[0] + direction[0];
+ int32_t proposed_col = state->player_position[1] + direction[1];
+ bool valid_move = craftax_step_valid_land_position(
+ state,
+ proposed_row,
+ proposed_col
+ );
+ valid_move = valid_move || god_mode;
+
+ state->player_position[0] += (int32_t)valid_move * direction[0];
+ state->player_position[1] += (int32_t)valid_move * direction[1];
+
+ bool is_new_direction = direction[0] != 0 || direction[1] != 0;
+ state->player_direction = state->player_direction * (1 - (int32_t)is_new_direction)
+ + action * (int32_t)is_new_direction;
+}
+
+static inline void craftax_update_plants_native(CraftaxState* state) {
+ bool finished_growing_plants[CRAFTAX_MAX_GROWING_PLANTS];
+
+ for (int plant = 0; plant < CRAFTAX_MAX_GROWING_PLANTS; plant++) {
+ state->growing_plants_age[plant] =
+ (state->growing_plants_age[plant] + 1)
+ * (int32_t)state->growing_plants_mask[plant];
+ finished_growing_plants[plant] = state->growing_plants_age[plant] >= 600;
+ }
+
+ for (int plant = 0; plant < CRAFTAX_MAX_GROWING_PLANTS; plant++) {
+ int32_t row = craftax_step_jax_index(
+ state->growing_plants_positions[plant][0],
+ CRAFTAX_MAP_SIZE
+ );
+ int32_t col = craftax_step_jax_index(
+ state->growing_plants_positions[plant][1],
+ CRAFTAX_MAP_SIZE
+ );
+ int32_t new_block = finished_growing_plants[plant]
+ ? CRAFTAX_BLOCK_RIPE_PLANT
+ : state->map[0][row][col];
+ craftax_set_map_block(state, 0, row, col, new_block);
+ }
+}
+
+static inline void craftax_boss_logic_native(CraftaxState* state) {
+ state->achievements[CRAFTAX_ACH_DEFEAT_NECROMANCER] =
+ state->achievements[CRAFTAX_ACH_DEFEAT_NECROMANCER]
+ || craftax_step_has_beaten_boss(state);
+ state->boss_timesteps_to_spawn_this_round -=
+ (int32_t)craftax_step_is_fighting_boss(state);
+}
+
+static inline void craftax_level_up_attributes_native(
+ CraftaxState* state,
+ int32_t action,
+ int32_t max_attribute
+) {
+ bool can_level_up = state->player_xp >= 1;
+ bool is_levelling_up_dex = can_level_up
+ && action == CRAFTAX_ACTION_LEVEL_UP_DEXTERITY
+ && state->player_dexterity < max_attribute;
+ bool is_levelling_up_str = can_level_up
+ && action == CRAFTAX_ACTION_LEVEL_UP_STRENGTH
+ && state->player_strength < max_attribute;
+ bool is_levelling_up_int = can_level_up
+ && action == CRAFTAX_ACTION_LEVEL_UP_INTELLIGENCE
+ && state->player_intelligence < max_attribute;
+ bool is_levelling_up = is_levelling_up_dex
+ || is_levelling_up_str
+ || is_levelling_up_int;
+
+ state->player_dexterity += (int32_t)is_levelling_up_dex;
+ state->player_strength += (int32_t)is_levelling_up_str;
+ state->player_intelligence += (int32_t)is_levelling_up_int;
+ state->player_xp -= (int32_t)is_levelling_up;
+}
+
+static inline void craftax_clip_inventory_and_intrinsics_native(
+ CraftaxState* state,
+ bool god_mode
+) {
+ state->inventory.wood = craftax_step_mini32(state->inventory.wood, 99);
+ state->inventory.stone = craftax_step_mini32(state->inventory.stone, 99);
+ state->inventory.coal = craftax_step_mini32(state->inventory.coal, 99);
+ state->inventory.iron = craftax_step_mini32(state->inventory.iron, 99);
+ state->inventory.diamond = craftax_step_mini32(state->inventory.diamond, 99);
+ state->inventory.sapling = craftax_step_mini32(state->inventory.sapling, 99);
+ state->inventory.pickaxe = craftax_step_mini32(state->inventory.pickaxe, 99);
+ state->inventory.sword = craftax_step_mini32(state->inventory.sword, 99);
+ state->inventory.bow = craftax_step_mini32(state->inventory.bow, 99);
+ state->inventory.arrows = craftax_step_mini32(state->inventory.arrows, 99);
+ for (int i = 0; i < 4; i++) {
+ state->inventory.armour[i] = craftax_step_mini32(
+ state->inventory.armour[i],
+ 99
+ );
+ }
+ state->inventory.torches = craftax_step_mini32(state->inventory.torches, 99);
+ state->inventory.ruby = craftax_step_mini32(state->inventory.ruby, 99);
+ state->inventory.sapphire = craftax_step_mini32(state->inventory.sapphire, 99);
+ for (int i = 0; i < 6; i++) {
+ state->inventory.potions[i] = craftax_step_mini32(
+ state->inventory.potions[i],
+ 99
+ );
+ }
+ state->inventory.books = craftax_step_mini32(state->inventory.books, 99);
+
+ float min_health = god_mode ? 9.0f : 0.0f;
+ state->player_health = craftax_step_minf32(
+ craftax_step_maxf32(state->player_health, min_health),
+ (float)craftax_step_get_max_health(state)
+ );
+ state->player_food = craftax_step_mini32(
+ craftax_step_maxi32(state->player_food, 0),
+ craftax_step_get_max_food(state)
+ );
+ state->player_drink = craftax_step_mini32(
+ craftax_step_maxi32(state->player_drink, 0),
+ craftax_step_get_max_drink(state)
+ );
+ state->player_energy = craftax_step_mini32(
+ craftax_step_maxi32(state->player_energy, 0),
+ craftax_step_get_max_energy(state)
+ );
+ state->player_mana = craftax_step_mini32(
+ craftax_step_maxi32(state->player_mana, 0),
+ craftax_step_get_max_mana(state)
+ );
+}
+
+static inline void craftax_calculate_inventory_achievements_native(
+ CraftaxState* state
+) {
+ state->achievements[CRAFTAX_ACH_COLLECT_WOOD] =
+ state->achievements[CRAFTAX_ACH_COLLECT_WOOD] || state->inventory.wood > 0;
+ state->achievements[CRAFTAX_ACH_COLLECT_STONE] =
+ state->achievements[CRAFTAX_ACH_COLLECT_STONE] || state->inventory.stone > 0;
+ state->achievements[CRAFTAX_ACH_COLLECT_COAL] =
+ state->achievements[CRAFTAX_ACH_COLLECT_COAL] || state->inventory.coal > 0;
+ state->achievements[CRAFTAX_ACH_COLLECT_IRON] =
+ state->achievements[CRAFTAX_ACH_COLLECT_IRON] || state->inventory.iron > 0;
+ state->achievements[CRAFTAX_ACH_COLLECT_DIAMOND] =
+ state->achievements[CRAFTAX_ACH_COLLECT_DIAMOND] || state->inventory.diamond > 0;
+ state->achievements[CRAFTAX_ACH_COLLECT_RUBY] =
+ state->achievements[CRAFTAX_ACH_COLLECT_RUBY] || state->inventory.ruby > 0;
+ state->achievements[CRAFTAX_ACH_COLLECT_SAPPHIRE] =
+ state->achievements[CRAFTAX_ACH_COLLECT_SAPPHIRE]
+ || state->inventory.sapphire > 0;
+ state->achievements[CRAFTAX_ACH_COLLECT_SAPLING] =
+ state->achievements[CRAFTAX_ACH_COLLECT_SAPLING]
+ || state->inventory.sapling > 0;
+ state->achievements[CRAFTAX_ACH_FIND_BOW] =
+ state->achievements[CRAFTAX_ACH_FIND_BOW] || state->inventory.bow > 0;
+ state->achievements[CRAFTAX_ACH_MAKE_ARROW] =
+ state->achievements[CRAFTAX_ACH_MAKE_ARROW] || state->inventory.arrows > 0;
+ state->achievements[CRAFTAX_ACH_MAKE_TORCH] =
+ state->achievements[CRAFTAX_ACH_MAKE_TORCH] || state->inventory.torches > 0;
+
+ state->achievements[CRAFTAX_ACH_MAKE_WOOD_PICKAXE] =
+ state->achievements[CRAFTAX_ACH_MAKE_WOOD_PICKAXE]
+ || state->inventory.pickaxe >= 1;
+ state->achievements[CRAFTAX_ACH_MAKE_STONE_PICKAXE] =
+ state->achievements[CRAFTAX_ACH_MAKE_STONE_PICKAXE]
+ || state->inventory.pickaxe >= 2;
+ state->achievements[CRAFTAX_ACH_MAKE_IRON_PICKAXE] =
+ state->achievements[CRAFTAX_ACH_MAKE_IRON_PICKAXE]
+ || state->inventory.pickaxe >= 3;
+ state->achievements[CRAFTAX_ACH_MAKE_DIAMOND_PICKAXE] =
+ state->achievements[CRAFTAX_ACH_MAKE_DIAMOND_PICKAXE]
+ || state->inventory.pickaxe >= 4;
+
+ state->achievements[CRAFTAX_ACH_MAKE_WOOD_SWORD] =
+ state->achievements[CRAFTAX_ACH_MAKE_WOOD_SWORD]
+ || state->inventory.sword >= 1;
+ state->achievements[CRAFTAX_ACH_MAKE_STONE_SWORD] =
+ state->achievements[CRAFTAX_ACH_MAKE_STONE_SWORD]
+ || state->inventory.sword >= 2;
+ state->achievements[CRAFTAX_ACH_MAKE_IRON_SWORD] =
+ state->achievements[CRAFTAX_ACH_MAKE_IRON_SWORD]
+ || state->inventory.sword >= 3;
+ state->achievements[CRAFTAX_ACH_MAKE_DIAMOND_SWORD] =
+ state->achievements[CRAFTAX_ACH_MAKE_DIAMOND_SWORD]
+ || state->inventory.sword >= 4;
+}
+
+static inline void craftax_update_player_intrinsics_native(
+ CraftaxState* state,
+ int32_t action
+) {
+ bool is_starting_sleep = action == CRAFTAX_ACTION_SLEEP
+ && state->player_energy < craftax_step_get_max_energy(state);
+ state->is_sleeping = state->is_sleeping || is_starting_sleep;
+
+ bool is_waking_up = state->player_energy >= craftax_step_get_max_energy(state)
+ && state->is_sleeping;
+ state->is_sleeping = state->is_sleeping && !is_waking_up;
+ state->achievements[CRAFTAX_ACH_WAKE_UP] =
+ state->achievements[CRAFTAX_ACH_WAKE_UP] || is_waking_up;
+
+ bool is_starting_rest = action == CRAFTAX_ACTION_REST
+ && state->player_health < (float)craftax_step_get_max_health(state);
+ state->is_resting = state->is_resting || is_starting_rest;
+
+ is_waking_up = state->is_resting
+ && (
+ state->player_health >= (float)craftax_step_get_max_health(state)
+ || state->player_food <= 0
+ || state->player_drink <= 0
+ );
+ state->is_resting = state->is_resting && !is_waking_up;
+
+ bool not_boss = !craftax_step_is_fighting_boss(state);
+ float intrinsic_decay_coeff =
+ 1.0f - (0.125f * (float)(state->player_dexterity - 1));
+
+ float hunger_add = (state->is_sleeping ? 0.5f : 1.0f) * intrinsic_decay_coeff;
+ float new_hunger = state->player_hunger + hunger_add;
+ int32_t hungered_food = craftax_step_maxi32(
+ state->player_food - (int32_t)not_boss,
+ 0
+ );
+ int32_t new_food = new_hunger > 25.0f ? hungered_food : state->player_food;
+ new_hunger = new_hunger > 25.0f ? 0.0f : new_hunger;
+ state->player_hunger = new_hunger;
+ state->player_food = new_food;
+
+ float thirst_add = (state->is_sleeping ? 0.5f : 1.0f) * intrinsic_decay_coeff;
+ float new_thirst = state->player_thirst + thirst_add;
+ int32_t thirsted_drink = craftax_step_maxi32(
+ state->player_drink - (int32_t)not_boss,
+ 0
+ );
+ int32_t new_drink = new_thirst > 20.0f ? thirsted_drink : state->player_drink;
+ new_thirst = new_thirst > 20.0f ? 0.0f : new_thirst;
+ state->player_thirst = new_thirst;
+ state->player_drink = new_drink;
+
+ float new_fatigue = state->is_sleeping
+ ? craftax_step_minf32(state->player_fatigue - 1.0f, 0.0f)
+ : state->player_fatigue + intrinsic_decay_coeff;
+ int32_t new_energy = new_fatigue > 30.0f
+ ? craftax_step_maxi32(state->player_energy - (int32_t)not_boss, 0)
+ : state->player_energy;
+ new_fatigue = new_fatigue > 30.0f ? 0.0f : new_fatigue;
+ new_energy = new_fatigue < -10.0f
+ ? craftax_step_mini32(
+ state->player_energy + 1,
+ craftax_step_get_max_energy(state)
+ )
+ : new_energy;
+ new_fatigue = new_fatigue < -10.0f ? 0.0f : new_fatigue;
+ state->player_fatigue = new_fatigue;
+ state->player_energy = new_energy;
+
+ bool all_necessities = state->player_food > 0
+ && state->player_drink > 0
+ && (state->player_energy > 0 || state->is_sleeping);
+ float recover_all = state->is_sleeping ? 2.0f : 1.0f;
+ float recover_not_all = (state->is_sleeping ? -0.5f : -1.0f)
+ * (float)(int32_t)not_boss;
+ float recover_add = all_necessities ? recover_all : recover_not_all;
+ float new_recover = state->player_recover + recover_add;
+
+ float recovered_health = craftax_step_minf32(
+ state->player_health + 1.0f,
+ (float)craftax_step_get_max_health(state)
+ );
+ float derecovered_health = state->player_health - 1.0f;
+ float new_health = new_recover > 25.0f
+ ? recovered_health
+ : state->player_health;
+ new_recover = new_recover > 25.0f ? 0.0f : new_recover;
+ new_health = new_recover < -15.0f ? derecovered_health : new_health;
+ new_recover = new_recover < -15.0f ? 0.0f : new_recover;
+ state->player_recover = new_recover;
+ state->player_health = new_health;
+
+ float mana_recover_coeff =
+ 1.0f + 0.25f * (float)(state->player_intelligence - 1);
+ float new_recover_mana = (
+ state->is_sleeping
+ ? state->player_recover_mana + 2.0f
+ : state->player_recover_mana + 1.0f
+ ) * mana_recover_coeff;
+ int32_t new_mana = new_recover_mana > 30.0f
+ ? state->player_mana + 1
+ : state->player_mana;
+ new_recover_mana = new_recover_mana > 30.0f ? 0.0f : new_recover_mana;
+ state->player_recover_mana = new_recover_mana;
+ state->player_mana = new_mana;
+}
+
+static inline void craftax_drink_potion_native(
+ CraftaxState* state,
+ int32_t action
+) {
+ int32_t drinking_potion_index = -1;
+ bool is_drinking_potion = false;
+
+ bool is_drinking_red_potion = action == CRAFTAX_ACTION_DRINK_POTION_RED
+ && state->inventory.potions[0] > 0;
+ drinking_potion_index = (int32_t)is_drinking_red_potion * 0
+ + (1 - (int32_t)is_drinking_red_potion) * drinking_potion_index;
+ is_drinking_potion = is_drinking_potion || is_drinking_red_potion;
+
+ bool is_drinking_green_potion = action == CRAFTAX_ACTION_DRINK_POTION_GREEN
+ && state->inventory.potions[1] > 0;
+ drinking_potion_index = (int32_t)is_drinking_green_potion * 1
+ + (1 - (int32_t)is_drinking_green_potion) * drinking_potion_index;
+ is_drinking_potion = is_drinking_potion || is_drinking_green_potion;
+
+ bool is_drinking_blue_potion = action == CRAFTAX_ACTION_DRINK_POTION_BLUE
+ && state->inventory.potions[2] > 0;
+ drinking_potion_index = (int32_t)is_drinking_blue_potion * 2
+ + (1 - (int32_t)is_drinking_blue_potion) * drinking_potion_index;
+ is_drinking_potion = is_drinking_potion || is_drinking_blue_potion;
+
+ bool is_drinking_pink_potion = action == CRAFTAX_ACTION_DRINK_POTION_PINK
+ && state->inventory.potions[3] > 0;
+ drinking_potion_index = (int32_t)is_drinking_pink_potion * 3
+ + (1 - (int32_t)is_drinking_pink_potion) * drinking_potion_index;
+ is_drinking_potion = is_drinking_potion || is_drinking_pink_potion;
+
+ bool is_drinking_cyan_potion = action == CRAFTAX_ACTION_DRINK_POTION_CYAN
+ && state->inventory.potions[4] > 0;
+ drinking_potion_index = (int32_t)is_drinking_cyan_potion * 4
+ + (1 - (int32_t)is_drinking_cyan_potion) * drinking_potion_index;
+ is_drinking_potion = is_drinking_potion || is_drinking_cyan_potion;
+
+ bool is_drinking_yellow_potion = action == CRAFTAX_ACTION_DRINK_POTION_YELLOW
+ && state->inventory.potions[5] > 0;
+ drinking_potion_index = (int32_t)is_drinking_yellow_potion * 5
+ + (1 - (int32_t)is_drinking_yellow_potion) * drinking_potion_index;
+ is_drinking_potion = is_drinking_potion || is_drinking_yellow_potion;
+
+ int32_t potion_index = craftax_step_jax_index(drinking_potion_index, 6);
+ int32_t potion_effect_index = state->potion_mapping[potion_index];
+
+ int32_t delta_health = 0;
+ delta_health += (int32_t)is_drinking_potion * (int32_t)(potion_effect_index == 0) * 8;
+ delta_health += (int32_t)is_drinking_potion * (int32_t)(potion_effect_index == 1) * -3;
+
+ int32_t delta_mana = 0;
+ delta_mana += (int32_t)is_drinking_potion * (int32_t)(potion_effect_index == 2) * 8;
+ delta_mana += (int32_t)is_drinking_potion * (int32_t)(potion_effect_index == 3) * -3;
+
+ int32_t delta_energy = 0;
+ delta_energy += (int32_t)is_drinking_potion * (int32_t)(potion_effect_index == 4) * 8;
+ delta_energy += (int32_t)is_drinking_potion * (int32_t)(potion_effect_index == 5) * -3;
+
+ state->achievements[CRAFTAX_ACH_DRINK_POTION] =
+ state->achievements[CRAFTAX_ACH_DRINK_POTION] || is_drinking_potion;
+ state->inventory.potions[potion_index] =
+ state->inventory.potions[potion_index] - (int32_t)is_drinking_potion;
+ state->player_health += (float)delta_health;
+ state->player_mana += delta_mana;
+ state->player_energy += delta_energy;
+}
+
+static inline void craftax_read_book_native(
+ CraftaxState* state,
+ const uint32_t rng_words[2],
+ int32_t action
+) {
+ bool is_reading_book = action == CRAFTAX_ACTION_READ_BOOK
+ && state->inventory.books > 0;
+
+ CraftaxThreefryKey rng = {{rng_words[0], rng_words[1]}};
+ CraftaxThreefryKey unused;
+ CraftaxThreefryKey choice_key;
+ craftax_threefry_split(rng, &unused, &choice_key);
+
+ float p0 = state->learned_spells[0] ? 0.0f : 1.0f;
+ float p1 = state->learned_spells[1] ? 0.0f : 1.0f;
+ float p_sum = p0 + p1;
+ int32_t spell_to_learn_index = 0;
+ if (p_sum != 0.0f) {
+ p0 /= p_sum;
+ float r = 1.0f - craftax_threefry_uniform_f32(choice_key);
+ spell_to_learn_index = r <= p0 ? 0 : 1;
+ }
+
+ int32_t learn_spell_achievement = spell_to_learn_index
+ ? CRAFTAX_ACH_LEARN_ICEBALL
+ : CRAFTAX_ACH_LEARN_FIREBALL;
+
+ state->achievements[learn_spell_achievement] =
+ state->achievements[learn_spell_achievement] || is_reading_book;
+ state->inventory.books -= (int32_t)is_reading_book;
+ state->learned_spells[spell_to_learn_index] =
+ state->learned_spells[spell_to_learn_index] || is_reading_book;
+}
diff --git a/ocean/craftax/step_spawn_mobs.h b/ocean/craftax/step_spawn_mobs.h
new file mode 100644
index 0000000000..3dcb2bb1c0
--- /dev/null
+++ b/ocean/craftax/step_spawn_mobs.h
@@ -0,0 +1,846 @@
+// Craftax spawn_mobs, optimized for CPU.
+//
+// Bitwise-equivalent to the prior JAX-transliterated baseline (verified by
+// ocean/craftax_exp/parity_vs_baseline.c over 1.28M paired steps), ~6-9x
+// faster per step by stripping JAX-isms:
+// - full-grid validity masks -> compact coord list collected in one pass
+// - bounding-box scan (only cells within MOB_DESPAWN_DISTANCE)
+// - early return on mob-cap / probability-roll failure (no dead writes)
+// - merged count + first_empty loops
+//
+// The prior reference implementation is archived at
+// ocean/craftax_exp/step_spawn_mobs_baseline.h.
+
+#pragma once
+
+#include "step_medium.h"
+
+#define CRAFTAX_SPAWN_MAP_CELLS (CRAFTAX_MAP_SIZE * CRAFTAX_MAP_SIZE)
+#define CRAFTAX_SPAWN_BBOX_MAX_CELLS 729 // (2*DESPAWN-1)^2 at 14 = 27*27
+#define CRAFTAX_SPAWN_ALL_VALID_BLOCK_MASK ( \
+ (1ULL << CRAFTAX_BLOCK_GRASS) \
+ | (1ULL << CRAFTAX_BLOCK_PATH) \
+ | (1ULL << CRAFTAX_BLOCK_FIRE_GRASS) \
+ | (1ULL << CRAFTAX_BLOCK_ICE_GRASS))
+#define CRAFTAX_SPAWN_GRAVE_BLOCK_MASK ( \
+ (1ULL << CRAFTAX_BLOCK_GRAVE) \
+ | (1ULL << CRAFTAX_BLOCK_GRAVE2) \
+ | (1ULL << CRAFTAX_BLOCK_GRAVE3))
+#define CRAFTAX_SPAWN_WATER_BLOCK_MASK (1ULL << CRAFTAX_BLOCK_WATER)
+
+typedef struct { int8_t dr, dc0, dc1; } CraftaxSpawnOffsetSpan;
+
+static CraftaxSpawnOffsetSpan craftax_spawn_passive_spans[CRAFTAX_SPAWN_BBOX_MAX_CELLS];
+static CraftaxSpawnOffsetSpan craftax_spawn_hostile_spans[CRAFTAX_SPAWN_BBOX_MAX_CELLS];
+static CraftaxSpawnOffsetSpan craftax_spawn_boss_spans[CRAFTAX_SPAWN_BBOX_MAX_CELLS];
+static int32_t craftax_spawn_passive_span_count = 0;
+static int32_t craftax_spawn_hostile_span_count = 0;
+static int32_t craftax_spawn_boss_span_count = 0;
+static int32_t craftax_spawn_offsets_initialized = 0;
+
+static inline void craftax_spawn_append_span(
+ CraftaxSpawnOffsetSpan* spans,
+ int32_t* count,
+ int32_t dr,
+ int32_t dc0,
+ int32_t dc1
+) {
+ spans[*count] = (CraftaxSpawnOffsetSpan){
+ (int8_t)dr, (int8_t)dc0, (int8_t)dc1
+ };
+ *count += 1;
+}
+
+static inline void craftax_spawn_build_spans_for_row(
+ CraftaxSpawnOffsetSpan* spans,
+ int32_t* count,
+ int32_t dr,
+ int32_t limit,
+ int32_t min_exclusive,
+ int32_t max_exclusive
+) {
+ bool active = false;
+ int32_t start = 0;
+ for (int32_t dc = -limit; dc <= limit; dc++) {
+ int32_t distance2 = dr * dr + dc * dc;
+ bool valid = distance2 > min_exclusive && distance2 < max_exclusive;
+ if (valid && !active) {
+ active = true;
+ start = dc;
+ } else if (!valid && active) {
+ craftax_spawn_append_span(spans, count, dr, start, dc - 1);
+ active = false;
+ }
+ }
+ if (active) {
+ craftax_spawn_append_span(spans, count, dr, start, limit);
+ }
+}
+
+static inline void craftax_spawn_init_offsets_once(void) {
+ if (__atomic_load_n(
+ &craftax_spawn_offsets_initialized, __ATOMIC_ACQUIRE
+ )) return;
+
+ #pragma omp critical(craftax_spawn_offsets_init)
+ {
+ if (!__atomic_load_n(
+ &craftax_spawn_offsets_initialized, __ATOMIC_RELAXED
+ )) {
+ int32_t passive_count = 0;
+ int32_t hostile_count = 0;
+ int32_t boss_count = 0;
+ int32_t limit = CRAFTAX_MOB_DESPAWN_DISTANCE - 1;
+ int32_t limit2 = CRAFTAX_MOB_DESPAWN_DISTANCE
+ * CRAFTAX_MOB_DESPAWN_DISTANCE;
+ for (int32_t dr = -limit; dr <= limit; dr++) {
+ craftax_spawn_build_spans_for_row(
+ craftax_spawn_passive_spans,
+ &passive_count,
+ dr,
+ limit,
+ 9,
+ limit2
+ );
+ craftax_spawn_build_spans_for_row(
+ craftax_spawn_hostile_spans,
+ &hostile_count,
+ dr,
+ limit,
+ 81,
+ limit2
+ );
+ craftax_spawn_build_spans_for_row(
+ craftax_spawn_boss_spans,
+ &boss_count,
+ dr,
+ limit,
+ -1,
+ 37
+ );
+ }
+ craftax_spawn_passive_span_count = passive_count;
+ craftax_spawn_hostile_span_count = hostile_count;
+ craftax_spawn_boss_span_count = boss_count;
+ __atomic_store_n(
+ &craftax_spawn_offsets_initialized, 1, __ATOMIC_RELEASE
+ );
+ }
+ }
+}
+
+static inline bool craftax_spawn_block_matches(uint8_t block, uint64_t mask) {
+ return ((mask >> block) & 1ULL) != 0;
+}
+
+static inline uint64_t craftax_spawn_row_bits_for_mask(
+ const CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ uint64_t terrain_mask
+) {
+ if (terrain_mask == CRAFTAX_SPAWN_ALL_VALID_BLOCK_MASK) {
+ return state->spawn_all_bits[level][row];
+ }
+ if (terrain_mask == CRAFTAX_SPAWN_GRAVE_BLOCK_MASK) {
+ return state->spawn_grave_bits[level][row];
+ }
+ return state->spawn_water_bits[level][row];
+}
+
+static inline uint64_t craftax_spawn_col_mask(int32_t col0, int32_t col1) {
+ uint64_t hi = (1ULL << (col1 + 1)) - 1ULL;
+ uint64_t lo = col0 <= 0 ? 0ULL : ((1ULL << col0) - 1ULL);
+ return hi & ~lo;
+}
+
+static inline CraftaxThreefryKey craftax_spawn_next_random_key(
+ CraftaxThreefryKey* rng
+) {
+ CraftaxThreefryKey draw;
+ craftax_threefry_split(*rng, rng, &draw);
+ return draw;
+}
+
+static inline int32_t craftax_spawn_floor_mob_type(
+ int32_t floor, int32_t mob_class
+) {
+ static const int32_t mapping[CRAFTAX_NUM_LEVELS][3] = {
+ {0, 0, 0}, {2, 2, 2}, {1, 1, 1}, {2, 3, 3}, {2, 4, 4},
+ {1, 5, 5}, {1, 6, 6}, {1, 7, 7}, {0, 0, 0},
+ };
+ int32_t level = craftax_step_jax_index(floor, CRAFTAX_NUM_LEVELS);
+ int32_t class_index = craftax_step_jax_index(mob_class, 3);
+ return mapping[level][class_index];
+}
+
+static inline float craftax_spawn_floor_spawn_chance(
+ int32_t floor, int32_t chance_index
+) {
+ static const float chances[CRAFTAX_NUM_LEVELS][4] = {
+ {0.1f, 0.02f, 0.05f, 0.1f},
+ {0.1f, 0.06f, 0.05f, 0.0f},
+ {0.1f, 0.06f, 0.05f, 0.0f},
+ {0.1f, 0.06f, 0.05f, 0.0f},
+ {0.1f, 0.06f, 0.05f, 0.0f},
+ {0.1f, 0.06f, 0.05f, 0.0f},
+ {0.1f, 0.06f, 0.05f, 0.0f},
+ {0.0f, 0.06f, 0.05f, 0.0f},
+ {0.1f, 0.06f, 0.05f, 0.0f},
+ };
+ int32_t level = craftax_step_jax_index(floor, CRAFTAX_NUM_LEVELS);
+ int32_t index = craftax_step_jax_index(chance_index, 4);
+ return chances[level][index];
+}
+
+static inline float craftax_spawn_mob_type_health(
+ int32_t mob_type, int32_t mob_class
+) {
+ static const float health[CRAFTAX_NUM_MOB_TYPES][4] = {
+ {3.0f, 5.0f, 3.0f, 0.0f}, {4.0f, 7.0f, 5.0f, 0.0f},
+ {6.0f, 9.0f, 6.0f, 0.0f}, {8.0f, 11.0f, 8.0f, 0.0f},
+ {0.0f, 12.0f, 12.0f, 0.0f}, {0.0f, 20.0f, 4.0f, 0.0f},
+ {0.0f, 20.0f, 14.0f, 0.0f}, {0.0f, 24.0f, 16.0f, 0.0f},
+ };
+ int32_t type_index = craftax_step_jax_index(mob_type, CRAFTAX_NUM_MOB_TYPES);
+ int32_t class_index = craftax_step_jax_index(mob_class, 4);
+ return health[type_index][class_index];
+}
+
+static inline bool craftax_spawn_is_all_valid_block(int32_t block) {
+ static const uint8_t flags[CRAFTAX_NUM_BLOCK_TYPES] = {
+ [CRAFTAX_BLOCK_GRASS] = 1,
+ [CRAFTAX_BLOCK_PATH] = 1,
+ [CRAFTAX_BLOCK_FIRE_GRASS] = 1,
+ [CRAFTAX_BLOCK_ICE_GRASS] = 1,
+ };
+ int32_t idx = craftax_step_jax_index(block, CRAFTAX_NUM_BLOCK_TYPES);
+ return flags[idx] != 0;
+}
+
+static inline bool craftax_spawn_is_grave_block(int32_t block) {
+ static const uint8_t flags[CRAFTAX_NUM_BLOCK_TYPES] = {
+ [CRAFTAX_BLOCK_GRAVE] = 1,
+ [CRAFTAX_BLOCK_GRAVE2] = 1,
+ [CRAFTAX_BLOCK_GRAVE3] = 1,
+ };
+ int32_t idx = craftax_step_jax_index(block, CRAFTAX_NUM_BLOCK_TYPES);
+ return flags[idx] != 0;
+}
+
+static inline bool craftax_spawn_is_water_block(int32_t block) {
+ static const uint8_t flags[CRAFTAX_NUM_BLOCK_TYPES] = {
+ [CRAFTAX_BLOCK_WATER] = 1,
+ };
+ int32_t idx = craftax_step_jax_index(block, CRAFTAX_NUM_BLOCK_TYPES);
+ return flags[idx] != 0;
+}
+
+static inline int32_t craftax_spawn_player_distance_squared(
+ const CraftaxState* state, int32_t row, int32_t col
+) {
+ int32_t dr = row - state->player_position[0];
+ int32_t dc = col - state->player_position[1];
+ if (dr < 0) dr = -dr;
+ if (dc < 0) dc = -dc;
+ return dr * dr + dc * dc;
+}
+
+static inline int32_t craftax_spawn_count_mobs3(
+ const CraftaxMobs3* mobs, int32_t level
+) {
+ int32_t count = 0;
+ for (int32_t i = 0; i < 3; i++) count += (int32_t)mobs->mask[level][i];
+ return count;
+}
+
+static inline int32_t craftax_spawn_count_mobs2(
+ const CraftaxMobs2* mobs, int32_t level
+) {
+ int32_t count = 0;
+ for (int32_t i = 0; i < 2; i++) count += (int32_t)mobs->mask[level][i];
+ return count;
+}
+
+static inline int32_t craftax_spawn_first_empty_mobs3(
+ const CraftaxMobs3* mobs, int32_t level
+) {
+ for (int32_t i = 0; i < 3; i++) if (!mobs->mask[level][i]) return i;
+ return 0;
+}
+
+static inline int32_t craftax_spawn_first_empty_mobs2(
+ const CraftaxMobs2* mobs, int32_t level
+) {
+ for (int32_t i = 0; i < 2; i++) if (!mobs->mask[level][i]) return i;
+ return 0;
+}
+
+static inline void craftax_spawn_mobs3_count_and_empty(
+ const CraftaxMobs3* mobs, int32_t level,
+ int32_t* count_out, int32_t* first_empty_out
+) {
+ int32_t count = 0, first_empty = 0;
+ bool found = false;
+ for (int32_t i = 0; i < 3; i++) {
+ bool m = mobs->mask[level][i];
+ count += (int32_t)m;
+ if (!m && !found) { first_empty = i; found = true; }
+ }
+ *count_out = count;
+ *first_empty_out = first_empty;
+}
+
+static inline void craftax_spawn_mobs2_count_and_empty(
+ const CraftaxMobs2* mobs, int32_t level,
+ int32_t* count_out, int32_t* first_empty_out
+) {
+ int32_t count = 0, first_empty = 0;
+ bool found = false;
+ for (int32_t i = 0; i < 2; i++) {
+ bool m = mobs->mask[level][i];
+ count += (int32_t)m;
+ if (!m && !found) { first_empty = i; found = true; }
+ }
+ *count_out = count;
+ *first_empty_out = first_empty;
+}
+
+// Baseline algorithm on a bool mask:
+// draw = valid_count * (1.0 - uniform_f32(key));
+// cum = 0;
+// for i: if valid[i] { cum += 1.0; if (cum >= draw) return i; }
+// Over a compact list of length valid_count this collapses to a short loop
+// using the same FP arithmetic, preserving bitwise-identical choice.
+static inline int32_t craftax_spawn_pick_kth(
+ int32_t valid_count, CraftaxThreefryKey key
+) {
+ float draw = (float)valid_count * (1.0f - craftax_threefry_uniform_f32(key));
+ float cum = 0.0f;
+ for (int32_t k = 0; k < valid_count; k++) {
+ cum += 1.0f;
+ if (cum >= draw) return k;
+ }
+ return valid_count - 1;
+}
+
+typedef struct { int16_t row, col; } CraftaxSpawnCoord;
+
+typedef struct {
+ CraftaxSpawnCoord passive[CRAFTAX_SPAWN_BBOX_MAX_CELLS];
+ CraftaxSpawnCoord melee[CRAFTAX_SPAWN_BBOX_MAX_CELLS];
+ CraftaxSpawnCoord ranged[CRAFTAX_SPAWN_BBOX_MAX_CELLS];
+ int32_t passive_count;
+ int32_t melee_count;
+ int32_t ranged_count;
+} CraftaxSpawnLists;
+
+static inline int32_t craftax_spawn_collect_spans(
+ const CraftaxState* state,
+ int32_t level,
+ const CraftaxSpawnOffsetSpan* spans,
+ int32_t span_count,
+ uint64_t terrain_mask,
+ CraftaxSpawnCoord* coords
+) {
+ int32_t pr = state->player_position[0];
+ int32_t pc = state->player_position[1];
+ int32_t n = 0;
+ for (int32_t i = 0; i < span_count; i++) {
+ int32_t row = pr + spans[i].dr;
+ if ((uint32_t)row >= CRAFTAX_MAP_SIZE) continue;
+ int32_t col0 = pc + spans[i].dc0;
+ int32_t col1 = pc + spans[i].dc1;
+ if (col0 < 0) col0 = 0;
+ if (col1 >= CRAFTAX_MAP_SIZE) col1 = CRAFTAX_MAP_SIZE - 1;
+ if (col0 > col1) continue;
+ uint64_t candidates =
+ craftax_spawn_row_bits_for_mask(state, level, row, terrain_mask)
+ & ~state->mob_bits[level][row]
+ & craftax_spawn_col_mask(col0, col1);
+ while (candidates != 0) {
+ int32_t col = __builtin_ctzll(candidates);
+ coords[n].row = (int16_t)row;
+ coords[n].col = (int16_t)col;
+ n++;
+ candidates &= candidates - 1;
+ }
+ }
+ return n;
+}
+
+static inline bool craftax_spawn_scan_spans(
+ const CraftaxState* state,
+ int32_t level,
+ const CraftaxSpawnOffsetSpan* spans,
+ int32_t span_count,
+ uint64_t terrain_mask,
+ CraftaxThreefryKey pos_key,
+ int32_t* out_row,
+ int32_t* out_col
+) {
+ CraftaxSpawnCoord coords[CRAFTAX_SPAWN_BBOX_MAX_CELLS];
+ int32_t n = craftax_spawn_collect_spans(
+ state, level, spans, span_count, terrain_mask, coords
+ );
+ if (n == 0) return false;
+ int32_t k = craftax_spawn_pick_kth(n, pos_key);
+ *out_row = coords[k].row;
+ *out_col = coords[k].col;
+ return true;
+}
+
+static inline bool craftax_spawn_coord_matches(
+ CraftaxSpawnCoord coord, bool exclude, int32_t row, int32_t col
+) {
+ return exclude && coord.row == row && coord.col == col;
+}
+
+static inline bool craftax_spawn_pick_excluding(
+ const CraftaxSpawnCoord* coords, int32_t count, CraftaxThreefryKey key,
+ bool exclude_a, int32_t row_a, int32_t col_a,
+ bool exclude_b, int32_t row_b, int32_t col_b,
+ int32_t* out_row, int32_t* out_col
+) {
+ int32_t valid_count = 0;
+ for (int32_t i = 0; i < count; i++) {
+ bool excluded = craftax_spawn_coord_matches(
+ coords[i], exclude_a, row_a, col_a
+ ) || craftax_spawn_coord_matches(coords[i], exclude_b, row_b, col_b);
+ valid_count += excluded ? 0 : 1;
+ }
+ if (valid_count == 0) return false;
+
+ int32_t k = craftax_spawn_pick_kth(valid_count, key);
+ for (int32_t i = 0; i < count; i++) {
+ bool excluded = craftax_spawn_coord_matches(
+ coords[i], exclude_a, row_a, col_a
+ ) || craftax_spawn_coord_matches(coords[i], exclude_b, row_b, col_b);
+ if (excluded) continue;
+ if (k == 0) {
+ *out_row = coords[i].row;
+ *out_col = coords[i].col;
+ return true;
+ }
+ k--;
+ }
+ return false;
+}
+
+static inline void craftax_spawn_scan_all(
+ const CraftaxState* state,
+ int32_t level,
+ int32_t ranged_type,
+ bool fighting_boss,
+ bool need_passive,
+ bool need_melee,
+ bool need_ranged,
+ CraftaxSpawnLists* out
+) {
+ out->passive_count = 0;
+ out->melee_count = 0;
+ out->ranged_count = 0;
+
+ craftax_spawn_init_offsets_once();
+
+ if (need_passive) {
+ out->passive_count = craftax_spawn_collect_spans(
+ state,
+ level,
+ craftax_spawn_passive_spans,
+ craftax_spawn_passive_span_count,
+ CRAFTAX_SPAWN_ALL_VALID_BLOCK_MASK,
+ out->passive
+ );
+ }
+
+ if (!need_melee && !need_ranged) return;
+
+ int32_t pr = state->player_position[0];
+ int32_t pc = state->player_position[1];
+ const CraftaxSpawnOffsetSpan* spans = fighting_boss
+ ? craftax_spawn_boss_spans
+ : craftax_spawn_hostile_spans;
+ int32_t span_count = fighting_boss
+ ? craftax_spawn_boss_span_count
+ : craftax_spawn_hostile_span_count;
+ bool ranged_water_type = (ranged_type == 5);
+
+ uint64_t melee_terrain_mask = fighting_boss
+ ? CRAFTAX_SPAWN_GRAVE_BLOCK_MASK
+ : CRAFTAX_SPAWN_ALL_VALID_BLOCK_MASK;
+ uint64_t ranged_terrain_mask;
+ if (fighting_boss) {
+ ranged_terrain_mask = CRAFTAX_SPAWN_GRAVE_BLOCK_MASK;
+ } else if (ranged_water_type) {
+ ranged_terrain_mask = CRAFTAX_SPAWN_WATER_BLOCK_MASK;
+ } else {
+ ranged_terrain_mask = CRAFTAX_SPAWN_ALL_VALID_BLOCK_MASK;
+ }
+
+ for (int32_t i = 0; i < span_count; i++) {
+ int32_t row = pr + spans[i].dr;
+ if ((uint32_t)row >= CRAFTAX_MAP_SIZE) continue;
+ int32_t col0 = pc + spans[i].dc0;
+ int32_t col1 = pc + spans[i].dc1;
+ if (col0 < 0) col0 = 0;
+ if (col1 >= CRAFTAX_MAP_SIZE) col1 = CRAFTAX_MAP_SIZE - 1;
+ if (col0 > col1) continue;
+ uint64_t open_bits =
+ ~state->mob_bits[level][row] & craftax_spawn_col_mask(col0, col1);
+
+ if (need_melee) {
+ uint64_t melee_candidates =
+ craftax_spawn_row_bits_for_mask(
+ state, level, row, melee_terrain_mask
+ ) & open_bits;
+ while (melee_candidates != 0) {
+ int32_t col = __builtin_ctzll(melee_candidates);
+ int32_t n = out->melee_count++;
+ out->melee[n].row = (int16_t)row;
+ out->melee[n].col = (int16_t)col;
+ melee_candidates &= melee_candidates - 1;
+ }
+ }
+
+ if (need_ranged) {
+ uint64_t ranged_candidates =
+ craftax_spawn_row_bits_for_mask(
+ state, level, row, ranged_terrain_mask
+ ) & open_bits;
+ while (ranged_candidates != 0) {
+ int32_t col = __builtin_ctzll(ranged_candidates);
+ int32_t n = out->ranged_count++;
+ out->ranged[n].row = (int16_t)row;
+ out->ranged[n].col = (int16_t)col;
+ ranged_candidates &= ranged_candidates - 1;
+ }
+ }
+ }
+}
+
+static inline bool craftax_spawn_scan_passive(
+ const CraftaxState* state, int32_t level, CraftaxThreefryKey pos_key,
+ int32_t* out_row, int32_t* out_col
+) {
+ craftax_spawn_init_offsets_once();
+ return craftax_spawn_scan_spans(
+ state,
+ level,
+ craftax_spawn_passive_spans,
+ craftax_spawn_passive_span_count,
+ CRAFTAX_SPAWN_ALL_VALID_BLOCK_MASK,
+ pos_key,
+ out_row,
+ out_col
+ );
+}
+
+static inline bool craftax_spawn_scan_melee(
+ const CraftaxState* state, int32_t level, bool fighting_boss,
+ CraftaxThreefryKey pos_key, int32_t* out_row, int32_t* out_col
+) {
+ craftax_spawn_init_offsets_once();
+ const CraftaxSpawnOffsetSpan* spans = fighting_boss
+ ? craftax_spawn_boss_spans
+ : craftax_spawn_hostile_spans;
+ int32_t span_count = fighting_boss
+ ? craftax_spawn_boss_span_count
+ : craftax_spawn_hostile_span_count;
+ uint64_t terrain_mask = fighting_boss
+ ? CRAFTAX_SPAWN_GRAVE_BLOCK_MASK
+ : CRAFTAX_SPAWN_ALL_VALID_BLOCK_MASK;
+ return craftax_spawn_scan_spans(
+ state, level, spans, span_count, terrain_mask, pos_key,
+ out_row, out_col
+ );
+}
+
+static inline bool craftax_spawn_scan_ranged(
+ const CraftaxState* state, int32_t level, int32_t new_type,
+ bool fighting_boss, CraftaxThreefryKey pos_key,
+ int32_t* out_row, int32_t* out_col
+) {
+ craftax_spawn_init_offsets_once();
+ const CraftaxSpawnOffsetSpan* spans = fighting_boss
+ ? craftax_spawn_boss_spans
+ : craftax_spawn_hostile_spans;
+ int32_t span_count = fighting_boss
+ ? craftax_spawn_boss_span_count
+ : craftax_spawn_hostile_span_count;
+ uint64_t terrain_mask;
+ if (fighting_boss) {
+ terrain_mask = CRAFTAX_SPAWN_GRAVE_BLOCK_MASK;
+ } else if (new_type == 5) {
+ terrain_mask = CRAFTAX_SPAWN_WATER_BLOCK_MASK;
+ } else {
+ terrain_mask = CRAFTAX_SPAWN_ALL_VALID_BLOCK_MASK;
+ }
+ return craftax_spawn_scan_spans(
+ state, level, spans, span_count, terrain_mask, pos_key,
+ out_row, out_col
+ );
+}
+
+// Both RNG keys are always consumed (preserves baseline RNG sequence).
+// Baseline quirk: type_id[level][slot] is written unconditionally, even
+// when no mob spawns. We match that for bitwise parity.
+
+static inline void craftax_spawn_passive_mob(
+ CraftaxState* state, CraftaxThreefryKey* rng,
+ int32_t level, bool fighting_boss
+) {
+ int32_t count, slot;
+ craftax_spawn_mobs3_count_and_empty(&state->passive_mobs, level, &count, &slot);
+
+ CraftaxThreefryKey prob_key = craftax_spawn_next_random_key(rng);
+ CraftaxThreefryKey pos_key = craftax_spawn_next_random_key(rng);
+
+ int32_t type = craftax_spawn_floor_mob_type(level, CRAFTAX_MOB_PASSIVE);
+ state->passive_mobs.type_id[level][slot] = type;
+
+ if (fighting_boss) return;
+ if (count >= CRAFTAX_MAX_PASSIVE_MOBS) return;
+ if (craftax_threefry_uniform_f32(prob_key)
+ >= craftax_spawn_floor_spawn_chance(level, 0)) return;
+
+ int32_t row, col;
+ if (!craftax_spawn_scan_passive(state, level, pos_key, &row, &col)) return;
+
+ state->passive_mobs.position[level][slot][0] = row;
+ state->passive_mobs.position[level][slot][1] = col;
+ state->passive_mobs.health[level][slot] =
+ craftax_spawn_mob_type_health(type, CRAFTAX_MOB_PASSIVE);
+ state->passive_mobs.mask[level][slot] = true;
+ state->mob_bits[level][row] |= (1ULL << col);
+}
+
+static inline void craftax_spawn_melee_mob(
+ CraftaxState* state, CraftaxThreefryKey* rng,
+ int32_t level, bool fighting_boss, int32_t monster_spawn_coeff
+) {
+ int32_t count, slot;
+ craftax_spawn_mobs3_count_and_empty(&state->melee_mobs, level, &count, &slot);
+
+ int32_t type = fighting_boss
+ ? craftax_spawn_floor_mob_type(state->boss_progress, CRAFTAX_MOB_MELEE)
+ : craftax_spawn_floor_mob_type(level, CRAFTAX_MOB_MELEE);
+
+ CraftaxThreefryKey prob_key = craftax_spawn_next_random_key(rng);
+ float night_coeff = 1.0f - state->light_level;
+ float spawn_chance = craftax_spawn_floor_spawn_chance(level, 1)
+ + craftax_spawn_floor_spawn_chance(level, 3) * night_coeff * night_coeff;
+ CraftaxThreefryKey pos_key = craftax_spawn_next_random_key(rng);
+
+ state->melee_mobs.type_id[level][slot] = type;
+
+ if (count >= CRAFTAX_MAX_MELEE_MOBS) return;
+ if (craftax_threefry_uniform_f32(prob_key)
+ >= spawn_chance * (float)monster_spawn_coeff) return;
+
+ int32_t row, col;
+ if (!craftax_spawn_scan_melee(state, level, fighting_boss, pos_key, &row, &col))
+ return;
+
+ state->melee_mobs.position[level][slot][0] = row;
+ state->melee_mobs.position[level][slot][1] = col;
+ state->melee_mobs.health[level][slot] =
+ craftax_spawn_mob_type_health(type, CRAFTAX_MOB_MELEE);
+ state->melee_mobs.mask[level][slot] = true;
+ state->mob_bits[level][row] |= (1ULL << col);
+}
+
+static inline void craftax_spawn_ranged_mob(
+ CraftaxState* state, CraftaxThreefryKey* rng,
+ int32_t level, bool fighting_boss, int32_t monster_spawn_coeff
+) {
+ int32_t count, slot;
+ craftax_spawn_mobs2_count_and_empty(&state->ranged_mobs, level, &count, &slot);
+
+ int32_t type = fighting_boss
+ ? craftax_spawn_floor_mob_type(state->boss_progress, CRAFTAX_MOB_RANGED)
+ : craftax_spawn_floor_mob_type(level, CRAFTAX_MOB_RANGED);
+
+ CraftaxThreefryKey prob_key = craftax_spawn_next_random_key(rng);
+ CraftaxThreefryKey pos_key = craftax_spawn_next_random_key(rng);
+
+ state->ranged_mobs.type_id[level][slot] = type;
+
+ if (count >= CRAFTAX_MAX_RANGED_MOBS) return;
+ if (craftax_threefry_uniform_f32(prob_key)
+ >= craftax_spawn_floor_spawn_chance(level, 2) * (float)monster_spawn_coeff)
+ return;
+
+ int32_t row, col;
+ if (!craftax_spawn_scan_ranged(state, level, type, fighting_boss, pos_key,
+ &row, &col)) return;
+
+ state->ranged_mobs.position[level][slot][0] = row;
+ state->ranged_mobs.position[level][slot][1] = col;
+ state->ranged_mobs.health[level][slot] =
+ craftax_spawn_mob_type_health(type, CRAFTAX_MOB_RANGED);
+ state->ranged_mobs.mask[level][slot] = true;
+ state->mob_bits[level][row] |= (1ULL << col);
+}
+
+static inline void craftax_spawn_mobs_native(
+ CraftaxState* state, CraftaxThreefryKey rng
+) {
+ int32_t level = craftax_step_jax_index(
+ state->player_level, CRAFTAX_NUM_LEVELS
+ );
+ bool fighting_boss = craftax_step_is_fighting_boss(state);
+ int32_t monster_spawn_coeff =
+ 1
+ + (int32_t)(state->monsters_killed[level]
+ < CRAFTAX_MONSTERS_KILLED_TO_CLEAR_LEVEL) * 2;
+
+ bool boss_spawn_wave =
+ fighting_boss && state->boss_timesteps_to_spawn_this_round >= 1;
+ if (fighting_boss) {
+ monster_spawn_coeff *= (int32_t)boss_spawn_wave * 1000;
+ }
+
+ int32_t passive_count, passive_slot;
+ craftax_spawn_mobs3_count_and_empty(
+ &state->passive_mobs, level, &passive_count, &passive_slot
+ );
+ CraftaxThreefryKey passive_prob_key = craftax_spawn_next_random_key(&rng);
+ CraftaxThreefryKey passive_pos_key = craftax_spawn_next_random_key(&rng);
+ int32_t passive_type = craftax_spawn_floor_mob_type(
+ level, CRAFTAX_MOB_PASSIVE
+ );
+ state->passive_mobs.type_id[level][passive_slot] = passive_type;
+
+ int32_t melee_count, melee_slot;
+ craftax_spawn_mobs3_count_and_empty(
+ &state->melee_mobs, level, &melee_count, &melee_slot
+ );
+ int32_t melee_type = fighting_boss
+ ? craftax_spawn_floor_mob_type(state->boss_progress, CRAFTAX_MOB_MELEE)
+ : craftax_spawn_floor_mob_type(level, CRAFTAX_MOB_MELEE);
+ CraftaxThreefryKey melee_prob_key = craftax_spawn_next_random_key(&rng);
+ float night_coeff = 1.0f - state->light_level;
+ float melee_spawn_chance = craftax_spawn_floor_spawn_chance(level, 1)
+ + craftax_spawn_floor_spawn_chance(level, 3) * night_coeff * night_coeff;
+ CraftaxThreefryKey melee_pos_key = craftax_spawn_next_random_key(&rng);
+ state->melee_mobs.type_id[level][melee_slot] = melee_type;
+
+ int32_t ranged_count, ranged_slot;
+ craftax_spawn_mobs2_count_and_empty(
+ &state->ranged_mobs, level, &ranged_count, &ranged_slot
+ );
+ int32_t ranged_type = fighting_boss
+ ? craftax_spawn_floor_mob_type(state->boss_progress, CRAFTAX_MOB_RANGED)
+ : craftax_spawn_floor_mob_type(level, CRAFTAX_MOB_RANGED);
+ CraftaxThreefryKey ranged_prob_key = craftax_spawn_next_random_key(&rng);
+ CraftaxThreefryKey ranged_pos_key = craftax_spawn_next_random_key(&rng);
+ state->ranged_mobs.type_id[level][ranged_slot] = ranged_type;
+
+ bool try_passive = !fighting_boss
+ && passive_count < CRAFTAX_MAX_PASSIVE_MOBS
+ && craftax_threefry_uniform_f32(passive_prob_key)
+ < craftax_spawn_floor_spawn_chance(level, 0);
+ bool try_melee = melee_count < CRAFTAX_MAX_MELEE_MOBS
+ && craftax_threefry_uniform_f32(melee_prob_key)
+ < melee_spawn_chance * (float)monster_spawn_coeff;
+ bool try_ranged = ranged_count < CRAFTAX_MAX_RANGED_MOBS
+ && craftax_threefry_uniform_f32(ranged_prob_key)
+ < craftax_spawn_floor_spawn_chance(level, 2)
+ * (float)monster_spawn_coeff;
+
+ if (!try_passive && !try_melee && !try_ranged) return;
+
+ int32_t try_count = (int32_t)try_passive
+ + (int32_t)try_melee
+ + (int32_t)try_ranged;
+ if (try_count == 1) {
+ int32_t row, col;
+ if (try_passive && craftax_spawn_scan_passive(
+ state, level, passive_pos_key, &row, &col
+ )) {
+ state->passive_mobs.position[level][passive_slot][0] = row;
+ state->passive_mobs.position[level][passive_slot][1] = col;
+ state->passive_mobs.health[level][passive_slot] =
+ craftax_spawn_mob_type_health(
+ passive_type, CRAFTAX_MOB_PASSIVE
+ );
+ state->passive_mobs.mask[level][passive_slot] = true;
+ state->mob_bits[level][row] |= (1ULL << col);
+ } else if (try_melee && craftax_spawn_scan_melee(
+ state, level, fighting_boss, melee_pos_key, &row, &col
+ )) {
+ state->melee_mobs.position[level][melee_slot][0] = row;
+ state->melee_mobs.position[level][melee_slot][1] = col;
+ state->melee_mobs.health[level][melee_slot] =
+ craftax_spawn_mob_type_health(melee_type, CRAFTAX_MOB_MELEE);
+ state->melee_mobs.mask[level][melee_slot] = true;
+ state->mob_bits[level][row] |= (1ULL << col);
+ } else if (try_ranged && craftax_spawn_scan_ranged(
+ state, level, ranged_type, fighting_boss, ranged_pos_key,
+ &row, &col
+ )) {
+ state->ranged_mobs.position[level][ranged_slot][0] = row;
+ state->ranged_mobs.position[level][ranged_slot][1] = col;
+ state->ranged_mobs.health[level][ranged_slot] =
+ craftax_spawn_mob_type_health(ranged_type, CRAFTAX_MOB_RANGED);
+ state->ranged_mobs.mask[level][ranged_slot] = true;
+ state->mob_bits[level][row] |= (1ULL << col);
+ }
+ return;
+ }
+
+ CraftaxSpawnLists lists;
+ craftax_spawn_scan_all(
+ state, level, ranged_type, fighting_boss,
+ try_passive, try_melee, try_ranged, &lists
+ );
+
+ bool passive_spawned = false;
+ int32_t passive_row = 0;
+ int32_t passive_col = 0;
+ if (try_passive && craftax_spawn_pick_excluding(
+ lists.passive, lists.passive_count, passive_pos_key,
+ false, 0, 0, false, 0, 0, &passive_row, &passive_col
+ )) {
+ state->passive_mobs.position[level][passive_slot][0] = passive_row;
+ state->passive_mobs.position[level][passive_slot][1] = passive_col;
+ state->passive_mobs.health[level][passive_slot] =
+ craftax_spawn_mob_type_health(passive_type, CRAFTAX_MOB_PASSIVE);
+ state->passive_mobs.mask[level][passive_slot] = true;
+ state->mob_bits[level][passive_row] |= (1ULL << passive_col);
+ passive_spawned = true;
+ }
+
+ bool melee_spawned = false;
+ int32_t melee_row = 0;
+ int32_t melee_col = 0;
+ if (try_melee && craftax_spawn_pick_excluding(
+ lists.melee, lists.melee_count, melee_pos_key,
+ passive_spawned, passive_row, passive_col,
+ false, 0, 0, &melee_row, &melee_col
+ )) {
+ state->melee_mobs.position[level][melee_slot][0] = melee_row;
+ state->melee_mobs.position[level][melee_slot][1] = melee_col;
+ state->melee_mobs.health[level][melee_slot] =
+ craftax_spawn_mob_type_health(melee_type, CRAFTAX_MOB_MELEE);
+ state->melee_mobs.mask[level][melee_slot] = true;
+ state->mob_bits[level][melee_row] |= (1ULL << melee_col);
+ melee_spawned = true;
+ }
+
+ int32_t ranged_row = 0;
+ int32_t ranged_col = 0;
+ if (try_ranged && craftax_spawn_pick_excluding(
+ lists.ranged, lists.ranged_count, ranged_pos_key,
+ passive_spawned, passive_row, passive_col,
+ melee_spawned, melee_row, melee_col, &ranged_row, &ranged_col
+ )) {
+ state->ranged_mobs.position[level][ranged_slot][0] = ranged_row;
+ state->ranged_mobs.position[level][ranged_slot][1] = ranged_col;
+ state->ranged_mobs.health[level][ranged_slot] =
+ craftax_spawn_mob_type_health(ranged_type, CRAFTAX_MOB_RANGED);
+ state->ranged_mobs.mask[level][ranged_slot] = true;
+ state->mob_bits[level][ranged_row] |= (1ULL << ranged_col);
+ }
+}
diff --git a/ocean/craftax/step_update_mobs.h b/ocean/craftax/step_update_mobs.h
new file mode 100644
index 0000000000..2b81681d77
--- /dev/null
+++ b/ocean/craftax/step_update_mobs.h
@@ -0,0 +1,1119 @@
+// Standalone native port of Craftax update_mobs.
+//
+// This helper intentionally is not integrated into c_step yet. It mutates a
+// full CraftaxState in place so tests can compare the subsystem directly
+// against the installed JAX implementation.
+
+#pragma once
+
+#include "step_do_action.h"
+
+#define CRAFTAX_UPDATE_BOSS_FIGHT_EXTRA_DAMAGE 0.5f
+
+static inline CraftaxThreefryKey craftax_update_mobs_next_random_key(
+ CraftaxThreefryKey* rng
+) {
+ CraftaxThreefryKey draw;
+ craftax_threefry_split(*rng, rng, &draw);
+ return draw;
+}
+
+static inline bool craftax_update_mobs_scatter_index(
+ int32_t index,
+ int32_t size,
+ int32_t* mapped_index
+) {
+ if (index < -size || index >= size) {
+ return false;
+ }
+ *mapped_index = index < 0 ? index + size : index;
+ return true;
+}
+
+static inline bool craftax_update_mobs_in_bounds(
+ int32_t row,
+ int32_t col
+) {
+ return row >= 0
+ && row < CRAFTAX_MAP_SIZE
+ && col >= 0
+ && col < CRAFTAX_MAP_SIZE;
+}
+
+static inline int32_t craftax_update_mobs_read_block(
+ const CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col
+) {
+ int32_t map_level = craftax_step_jax_index(level, CRAFTAX_NUM_LEVELS);
+ int32_t map_row = craftax_step_jax_index(row, CRAFTAX_MAP_SIZE);
+ int32_t map_col = craftax_step_jax_index(col, CRAFTAX_MAP_SIZE);
+ return state->map[map_level][map_row][map_col];
+}
+
+static inline void craftax_update_mobs_set_block(
+ CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col,
+ int32_t block
+) {
+ int32_t map_level;
+ int32_t map_row;
+ int32_t map_col;
+ if (!craftax_update_mobs_scatter_index(
+ level,
+ CRAFTAX_NUM_LEVELS,
+ &map_level
+ )
+ || !craftax_update_mobs_scatter_index(
+ row,
+ CRAFTAX_MAP_SIZE,
+ &map_row
+ )
+ || !craftax_update_mobs_scatter_index(
+ col,
+ CRAFTAX_MAP_SIZE,
+ &map_col
+ )) {
+ return;
+ }
+ craftax_set_map_block(state, map_level, map_row, map_col, block);
+}
+
+static inline bool craftax_update_mobs_read_mob_map(
+ const CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col
+) {
+ int32_t map_level = craftax_step_jax_index(level, CRAFTAX_NUM_LEVELS);
+ int32_t map_row = craftax_step_jax_index(row, CRAFTAX_MAP_SIZE);
+ int32_t map_col = craftax_step_jax_index(col, CRAFTAX_MAP_SIZE);
+ return (state->mob_bits[map_level][map_row] >> map_col) & 1ULL;
+}
+
+static inline void craftax_update_mobs_set_mob_map(
+ CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col,
+ bool value
+) {
+ int32_t map_level;
+ int32_t map_row;
+ int32_t map_col;
+ if (!craftax_update_mobs_scatter_index(
+ level,
+ CRAFTAX_NUM_LEVELS,
+ &map_level
+ )
+ || !craftax_update_mobs_scatter_index(
+ row,
+ CRAFTAX_MAP_SIZE,
+ &map_row
+ )
+ || !craftax_update_mobs_scatter_index(
+ col,
+ CRAFTAX_MAP_SIZE,
+ &map_col
+ )) {
+ return;
+ }
+ if (value) {
+ state->mob_bits[map_level][map_row] |= (1ULL << map_col);
+ } else {
+ state->mob_bits[map_level][map_row] &= ~(1ULL << map_col);
+ }
+}
+
+static inline void craftax_update_mobs_clear_old_map_entry(
+ CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col,
+ bool old_mask
+) {
+ bool old_value = craftax_update_mobs_read_mob_map(state, level, row, col);
+ craftax_update_mobs_set_mob_map(
+ state,
+ level,
+ row,
+ col,
+ old_value && !old_mask
+ );
+}
+
+static inline void craftax_update_mobs_enter_new_map_entry(
+ CraftaxState* state,
+ int32_t level,
+ int32_t row,
+ int32_t col,
+ bool new_mask
+) {
+ bool old_value = craftax_update_mobs_read_mob_map(state, level, row, col);
+ craftax_update_mobs_set_mob_map(
+ state,
+ level,
+ row,
+ col,
+ old_value || new_mask
+ );
+}
+
+static inline void craftax_update_mobs_damage_vector(
+ int32_t type_id,
+ int32_t mob_class_index,
+ float damage[3]
+) {
+ static const float damages[CRAFTAX_NUM_MOB_TYPES][4][3] = {
+ {
+ {0.0f, 0.0f, 0.0f},
+ {2.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {2.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {4.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {4.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {3.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 3.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {5.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 3.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {6.0f, 0.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {5.0f, 0.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {6.0f, 1.0f, 1.0f},
+ {0.0f, 0.0f, 0.0f},
+ {4.0f, 3.0f, 3.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {3.0f, 5.0f, 0.0f},
+ {0.0f, 0.0f, 0.0f},
+ {3.0f, 5.0f, 0.0f},
+ },
+ {
+ {0.0f, 0.0f, 0.0f},
+ {4.0f, 0.0f, 5.0f},
+ {0.0f, 0.0f, 0.0f},
+ {4.0f, 0.0f, 5.0f},
+ },
+ };
+
+ int32_t type_index = craftax_step_jax_index(
+ type_id,
+ CRAFTAX_NUM_MOB_TYPES
+ );
+ int32_t class_index = craftax_step_jax_index(mob_class_index, 4);
+ for (int32_t i = 0; i < 3; i++) {
+ damage[i] = damages[type_index][class_index][i];
+ }
+}
+
+static inline void craftax_update_mobs_collision_map(
+ int32_t type_id,
+ int32_t mob_class_index,
+ bool collision[3]
+) {
+ static const bool collisions[CRAFTAX_NUM_MOB_TYPES][4][3] = {
+ {
+ {false, true, true},
+ {false, true, true},
+ {false, true, true},
+ {false, false, false},
+ },
+ {
+ {false, false, false},
+ {false, true, true},
+ {false, true, true},
+ {false, false, false},
+ },
+ {
+ {false, true, true},
+ {false, true, true},
+ {false, true, true},
+ {false, false, false},
+ },
+ {
+ {false, true, true},
+ {false, false, true},
+ {false, true, true},
+ {false, false, false},
+ },
+ {
+ {false, true, true},
+ {false, true, true},
+ {false, true, true},
+ {false, false, false},
+ },
+ {
+ {false, true, true},
+ {false, true, true},
+ {true, false, true},
+ {false, false, false},
+ },
+ {
+ {false, true, true},
+ {false, true, true},
+ {false, false, false},
+ {false, false, false},
+ },
+ {
+ {false, true, true},
+ {false, true, true},
+ {false, false, false},
+ {false, false, false},
+ },
+ };
+
+ int32_t type_index = craftax_step_jax_index(
+ type_id,
+ CRAFTAX_NUM_MOB_TYPES
+ );
+ int32_t class_index = craftax_step_jax_index(mob_class_index, 4);
+ for (int32_t i = 0; i < 3; i++) {
+ collision[i] = collisions[type_index][class_index][i];
+ }
+}
+
+static inline int32_t craftax_update_mobs_projectile_type_for_ranged(
+ int32_t ranged_type
+) {
+ static const int32_t mapping[CRAFTAX_NUM_MOB_TYPES] = {
+ CRAFTAX_PROJECTILE_ARROW,
+ CRAFTAX_PROJECTILE_ARROW,
+ CRAFTAX_PROJECTILE_FIREBALL,
+ CRAFTAX_PROJECTILE_DAGGER,
+ CRAFTAX_PROJECTILE_ARROW2,
+ CRAFTAX_PROJECTILE_SLIMEBALL,
+ CRAFTAX_PROJECTILE_FIREBALL2,
+ CRAFTAX_PROJECTILE_ICEBALL2,
+ };
+ int32_t type_index = craftax_step_jax_index(
+ ranged_type,
+ CRAFTAX_NUM_MOB_TYPES
+ );
+ return mapping[type_index];
+}
+
+static inline void craftax_update_mobs_direction_choice(
+ CraftaxThreefryKey key,
+ int32_t count,
+ int32_t direction[2]
+) {
+ int32_t choice = craftax_medium_randint(key, 0, count);
+ direction[0] = 0;
+ direction[1] = 0;
+ if (choice == 0) {
+ direction[1] = -1;
+ } else if (choice == 1) {
+ direction[1] = 1;
+ } else if (choice == 2) {
+ direction[0] = -1;
+ } else if (choice == 3) {
+ direction[0] = 1;
+ }
+}
+
+static inline int32_t craftax_update_mobs_abs_i32(int32_t value) {
+ return value < 0 ? -value : value;
+}
+
+static inline int32_t craftax_update_mobs_sign_i32(int32_t value) {
+ if (value < 0) {
+ return -1;
+ }
+ return value > 0 ? 1 : 0;
+}
+
+static inline int32_t craftax_update_mobs_player_axis_choice(
+ CraftaxThreefryKey key,
+ int32_t distance_row,
+ int32_t distance_col
+) {
+ int32_t max_distance = distance_row > distance_col
+ ? distance_row
+ : distance_col;
+ int32_t total_distance = distance_row + distance_col;
+ if (total_distance == 0) {
+ return 1;
+ }
+
+ float weights[2] = {
+ (distance_row == max_distance) ? 1.0f / (float)total_distance : 0.0f,
+ (distance_col == max_distance) ? 1.0f / (float)total_distance : 0.0f,
+ };
+ return craftax_medium_choice_weighted(key, weights, 2);
+}
+
+static inline bool craftax_update_mobs_valid_position(
+ const CraftaxState* state,
+ int32_t row,
+ int32_t col,
+ const bool collision[3]
+) {
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ bool pos_in_bounds = craftax_update_mobs_in_bounds(row, col);
+ int32_t block = craftax_update_mobs_read_block(state, level, row, col);
+ bool in_solid_block = craftax_step_is_solid_block(block);
+ bool in_mob = craftax_step_is_in_mob(state, row, col);
+ bool in_lava = block == CRAFTAX_BLOCK_LAVA;
+ bool in_water = block == CRAFTAX_BLOCK_WATER;
+ bool on_ground_block = !in_solid_block && !in_water && !in_lava;
+
+ bool valid_move = pos_in_bounds && !in_mob && !in_solid_block;
+ valid_move = valid_move && (!collision[0] || !on_ground_block);
+ valid_move = valid_move && (!collision[1] || !in_water);
+ valid_move = valid_move && (!collision[2] || !in_lava);
+ return valid_move;
+}
+
+static inline int32_t craftax_update_mobs_manhattan_to_player(
+ const CraftaxState* state,
+ int32_t row,
+ int32_t col
+) {
+ return craftax_update_mobs_abs_i32(row - state->player_position[0])
+ + craftax_update_mobs_abs_i32(col - state->player_position[1]);
+}
+
+static inline float craftax_update_mobs_damage_done_to_player(
+ const CraftaxState* state,
+ const float damage_vector[3]
+) {
+ float defense_vector[3] = {0.0f, 0.0f, 0.0f};
+ for (int32_t i = 0; i < 4; i++) {
+ defense_vector[0] += (float)state->inventory.armour[i] * 0.1f;
+ defense_vector[1] +=
+ (float)(int32_t)(state->armour_enchantments[i] == 1) * 0.2f;
+ defense_vector[2] +=
+ (float)(int32_t)(state->armour_enchantments[i] == 2) * 0.2f;
+ }
+
+ float boss_coeff = craftax_step_is_fighting_boss(state)
+ ? 1.0f + CRAFTAX_UPDATE_BOSS_FIGHT_EXTRA_DAMAGE
+ : 1.0f;
+ float damage = 0.0f;
+ for (int32_t i = 0; i < 3; i++) {
+ damage += (1.0f - defense_vector[i]) * damage_vector[i] * boss_coeff;
+ }
+ return damage;
+}
+
+static inline int32_t craftax_update_mobs_count_mob_projectiles(
+ const CraftaxState* state,
+ int32_t level
+) {
+ const bool* mask = state->mob_projectiles.mask[level];
+ return (int32_t)mask[0] + (int32_t)mask[1] + (int32_t)mask[2];
+}
+
+static inline int32_t craftax_update_mobs_first_empty_mob_projectile(
+ const CraftaxState* state,
+ int32_t level
+) {
+ const bool* mask = state->mob_projectiles.mask[level];
+ if (!mask[0]) return 0;
+ if (!mask[1]) return 1;
+ if (!mask[2]) return 2;
+ return 0;
+}
+
+static inline void craftax_update_mobs_spawn_mob_projectile(
+ CraftaxState* state,
+ int32_t level,
+ bool is_spawning_projectile,
+ const int32_t position[2],
+ const int32_t direction[2],
+ int32_t projectile_type
+) {
+ if (!is_spawning_projectile) {
+ return;
+ }
+
+ int32_t index = craftax_update_mobs_first_empty_mob_projectile(
+ state,
+ level
+ );
+ state->mob_projectiles.position[level][index][0] = position[0];
+ state->mob_projectiles.position[level][index][1] = position[1];
+ state->mob_projectiles.mask[level][index] = true;
+ state->mob_projectiles.type_id[level][index] = projectile_type;
+ state->mob_projectile_directions[level][index][0] = direction[0];
+ state->mob_projectile_directions[level][index][1] = direction[1];
+}
+
+static inline void craftax_update_mobs_attack_mob_with_damage(
+ CraftaxState* state,
+ int32_t row,
+ int32_t col,
+ const float damage_vector[3],
+ bool can_eat,
+ bool* did_attack_mob,
+ bool* did_kill_mob
+) {
+ bool did_kill_melee_mob = false;
+ bool is_attacking_melee_mob = false;
+ craftax_do_action_attack_mobs3(
+ state,
+ &state->melee_mobs,
+ row,
+ col,
+ damage_vector,
+ true,
+ CRAFTAX_MOB_MELEE,
+ &did_kill_melee_mob,
+ &is_attacking_melee_mob
+ );
+
+ bool did_kill_passive_mob = false;
+ bool is_attacking_passive_mob = false;
+ craftax_do_action_attack_mobs3(
+ state,
+ &state->passive_mobs,
+ row,
+ col,
+ damage_vector,
+ can_eat,
+ CRAFTAX_MOB_PASSIVE,
+ &did_kill_passive_mob,
+ &is_attacking_passive_mob
+ );
+
+ if (did_kill_passive_mob && can_eat) {
+ state->player_food = craftax_step_mini32(
+ craftax_step_get_max_food(state),
+ state->player_food + 6
+ );
+ state->player_hunger = 0.0f;
+ }
+
+ bool did_kill_ranged_mob = false;
+ bool is_attacking_ranged_mob = false;
+ craftax_do_action_attack_mobs2(
+ state,
+ &state->ranged_mobs,
+ row,
+ col,
+ damage_vector,
+ true,
+ CRAFTAX_MOB_RANGED,
+ &did_kill_ranged_mob,
+ &is_attacking_ranged_mob
+ );
+
+ *did_attack_mob = is_attacking_melee_mob
+ || is_attacking_passive_mob
+ || is_attacking_ranged_mob;
+ bool did_kill_monster = did_kill_melee_mob || did_kill_ranged_mob;
+ *did_kill_mob = did_kill_monster || did_kill_passive_mob;
+
+ craftax_do_action_update_mob_map(state, row, col, *did_kill_mob);
+
+ int32_t level = craftax_step_jax_index(
+ state->player_level,
+ CRAFTAX_NUM_LEVELS
+ );
+ state->monsters_killed[level] += (int32_t)did_kill_monster;
+}
+
+static inline void craftax_update_mobs_player_projectile_damage_vector(
+ const CraftaxState* state,
+ int32_t level,
+ int32_t projectile_index,
+ float damage_vector[3]
+) {
+ int32_t projectile_type =
+ state->player_projectiles.type_id[level][projectile_index];
+ craftax_update_mobs_damage_vector(
+ projectile_type,
+ CRAFTAX_MOB_PROJECTILE,
+ damage_vector
+ );
+
+ float mask = (float)(int32_t)
+ state->player_projectiles.mask[level][projectile_index];
+ for (int32_t i = 0; i < 3; i++) {
+ damage_vector[i] *= mask;
+ }
+
+ bool is_arrow = projectile_type == CRAFTAX_PROJECTILE_ARROW
+ || projectile_type == CRAFTAX_PROJECTILE_ARROW2;
+ if (is_arrow) {
+ float arrow_damage_add[3] = {0.0f, 0.0f, 0.0f};
+ int32_t enchantment_index;
+ if (craftax_update_mobs_scatter_index(
+ state->bow_enchantment,
+ 3,
+ &enchantment_index
+ )) {
+ arrow_damage_add[enchantment_index] = damage_vector[0] / 2.0f;
+ }
+ arrow_damage_add[0] = 0.0f;
+ for (int32_t i = 0; i < 3; i++) {
+ damage_vector[i] += arrow_damage_add[i];
+ }
+ }
+
+ if (is_arrow) {
+ float arrow_damage_coeff =
+ 1.0f + 0.2f * (float)(state->player_dexterity - 1);
+ for (int32_t i = 0; i < 3; i++) {
+ damage_vector[i] *= arrow_damage_coeff;
+ }
+ }
+
+ bool is_magic_projectile = projectile_type == CRAFTAX_PROJECTILE_FIREBALL
+ || projectile_type == CRAFTAX_PROJECTILE_ICEBALL;
+ if (is_magic_projectile) {
+ float magic_damage_coeff =
+ 1.0f + 0.5f * (float)(state->player_intelligence - 1);
+ for (int32_t i = 0; i < 3; i++) {
+ damage_vector[i] *= magic_damage_coeff;
+ }
+ }
+}
+
+static inline void craftax_update_mobs_move_melee(
+ CraftaxState* state,
+ CraftaxThreefryKey* rng,
+ int32_t index
+) {
+ int32_t level = state->player_level;
+ bool old_mask = state->melee_mobs.mask[level][index];
+ // Dead slot early-out: no observable effect on obs/reward/terminal.
+ // Skip body and RNG draws for speed. Breaks per-seed replay against
+ // JAX; define CRAFTAX_JAX_PARITY at build time to restore the
+ // branchless slow path (same pattern in every move_* below).
+#ifndef CRAFTAX_JAX_PARITY
+ if (!old_mask) return;
+#endif
+ int32_t old_row = state->melee_mobs.position[level][index][0];
+ int32_t old_col = state->melee_mobs.position[level][index][1];
+ int32_t old_cooldown = state->melee_mobs.attack_cooldown[level][index];
+ int32_t mob_type = state->melee_mobs.type_id[level][index];
+
+ CraftaxThreefryKey draw_key =
+ craftax_update_mobs_next_random_key(rng);
+ int32_t random_direction[2];
+ craftax_update_mobs_direction_choice(draw_key, 4, random_direction);
+ int32_t random_row = old_row + random_direction[0];
+ int32_t random_col = old_col + random_direction[1];
+
+ int32_t distance_row =
+ craftax_update_mobs_abs_i32(state->player_position[0] - old_row);
+ int32_t distance_col =
+ craftax_update_mobs_abs_i32(state->player_position[1] - old_col);
+ draw_key = craftax_update_mobs_next_random_key(rng);
+ int32_t player_move_axis = craftax_update_mobs_player_axis_choice(
+ draw_key,
+ distance_row,
+ distance_col
+ );
+ int32_t player_direction[2] = {0, 0};
+ if (player_move_axis == 0) {
+ player_direction[0] =
+ craftax_update_mobs_sign_i32(state->player_position[0] - old_row);
+ } else {
+ player_direction[1] =
+ craftax_update_mobs_sign_i32(state->player_position[1] - old_col);
+ }
+ int32_t player_row = old_row + player_direction[0];
+ int32_t player_col = old_col + player_direction[1];
+
+ int32_t distance_to_player = distance_row + distance_col;
+ bool close_to_player = distance_to_player < 10
+ || craftax_step_is_fighting_boss(state);
+ draw_key = craftax_update_mobs_next_random_key(rng);
+ close_to_player = close_to_player
+ && craftax_threefry_uniform_f32(draw_key) < 0.75f;
+
+ int32_t proposed_row = close_to_player ? player_row : random_row;
+ int32_t proposed_col = close_to_player ? player_col : random_col;
+
+ bool is_attacking_player = distance_to_player == 1
+ && old_cooldown <= 0
+ && old_mask;
+ if (is_attacking_player) {
+ proposed_row = old_row;
+ proposed_col = old_col;
+ }
+
+ float base_damage[3];
+ craftax_update_mobs_damage_vector(
+ mob_type,
+ CRAFTAX_MOB_MELEE,
+ base_damage
+ );
+ float sleeping_coeff = 1.0f + 2.5f * (float)(int32_t)state->is_sleeping;
+ for (int32_t i = 0; i < 3; i++) {
+ base_damage[i] *= sleeping_coeff;
+ }
+ float damage = craftax_update_mobs_damage_done_to_player(
+ state,
+ base_damage
+ );
+
+ int32_t new_cooldown = is_attacking_player ? 5 : old_cooldown - 1;
+ bool is_waking_player = state->is_sleeping && is_attacking_player;
+ state->player_health -= damage * (float)(int32_t)is_attacking_player;
+ state->is_sleeping = state->is_sleeping && !is_attacking_player;
+ state->is_resting = state->is_resting && !is_attacking_player;
+ state->achievements[CRAFTAX_ACH_WAKE_UP] =
+ state->achievements[CRAFTAX_ACH_WAKE_UP] || is_waking_player;
+
+ bool collision[3];
+ craftax_update_mobs_collision_map(
+ mob_type,
+ CRAFTAX_MOB_MELEE,
+ collision
+ );
+ bool valid_move = craftax_update_mobs_valid_position(
+ state,
+ proposed_row,
+ proposed_col,
+ collision
+ );
+ int32_t new_row = valid_move ? proposed_row : old_row;
+ int32_t new_col = valid_move ? proposed_col : old_col;
+
+ bool should_not_despawn = distance_to_player < CRAFTAX_MOB_DESPAWN_DISTANCE
+ || craftax_step_is_fighting_boss(state);
+
+ CraftaxThreefryKey unused_left;
+ CraftaxThreefryKey returned_key;
+ craftax_threefry_split(*rng, &unused_left, &returned_key);
+ *rng = returned_key;
+
+ craftax_update_mobs_clear_old_map_entry(
+ state,
+ level,
+ old_row,
+ old_col,
+ old_mask
+ );
+ bool new_mask = old_mask && should_not_despawn;
+ craftax_update_mobs_enter_new_map_entry(
+ state,
+ level,
+ new_row,
+ new_col,
+ new_mask
+ );
+
+ state->melee_mobs.position[level][index][0] = new_row;
+ state->melee_mobs.position[level][index][1] = new_col;
+ state->melee_mobs.attack_cooldown[level][index] = new_cooldown;
+ state->melee_mobs.mask[level][index] = new_mask;
+}
+
+static inline void craftax_update_mobs_move_passive(
+ CraftaxState* state,
+ CraftaxThreefryKey* rng,
+ int32_t index
+) {
+ int32_t level = state->player_level;
+ bool old_mask = state->passive_mobs.mask[level][index];
+#ifndef CRAFTAX_JAX_PARITY
+ if (!old_mask) return;
+#endif
+ int32_t old_row = state->passive_mobs.position[level][index][0];
+ int32_t old_col = state->passive_mobs.position[level][index][1];
+ int32_t mob_type = state->passive_mobs.type_id[level][index];
+
+ CraftaxThreefryKey draw_key =
+ craftax_update_mobs_next_random_key(rng);
+ int32_t direction[2];
+ craftax_update_mobs_direction_choice(draw_key, 8, direction);
+ int32_t proposed_row = old_row + direction[0];
+ int32_t proposed_col = old_col + direction[1];
+
+ bool collision[3];
+ craftax_update_mobs_collision_map(
+ mob_type,
+ CRAFTAX_MOB_PASSIVE,
+ collision
+ );
+ bool valid_move = craftax_update_mobs_valid_position(
+ state,
+ proposed_row,
+ proposed_col,
+ collision
+ );
+ int32_t new_row = valid_move ? proposed_row : old_row;
+ int32_t new_col = valid_move ? proposed_col : old_col;
+
+ int32_t distance_to_player = craftax_update_mobs_manhattan_to_player(
+ state,
+ old_row,
+ old_col
+ );
+ bool should_not_despawn =
+ distance_to_player < CRAFTAX_MOB_DESPAWN_DISTANCE;
+
+ craftax_update_mobs_clear_old_map_entry(
+ state,
+ level,
+ old_row,
+ old_col,
+ old_mask
+ );
+ bool new_mask = old_mask && should_not_despawn;
+ craftax_update_mobs_enter_new_map_entry(
+ state,
+ level,
+ new_row,
+ new_col,
+ new_mask
+ );
+
+ state->passive_mobs.position[level][index][0] = new_row;
+ state->passive_mobs.position[level][index][1] = new_col;
+ state->passive_mobs.mask[level][index] = new_mask;
+}
+
+static inline void craftax_update_mobs_move_ranged(
+ CraftaxState* state,
+ CraftaxThreefryKey* rng,
+ int32_t index
+) {
+ int32_t level = state->player_level;
+ bool old_mask = state->ranged_mobs.mask[level][index];
+#ifndef CRAFTAX_JAX_PARITY
+ if (!old_mask) return;
+#endif
+ int32_t old_row = state->ranged_mobs.position[level][index][0];
+ int32_t old_col = state->ranged_mobs.position[level][index][1];
+ int32_t old_cooldown = state->ranged_mobs.attack_cooldown[level][index];
+ int32_t mob_type = state->ranged_mobs.type_id[level][index];
+
+ CraftaxThreefryKey draw_key =
+ craftax_update_mobs_next_random_key(rng);
+ int32_t random_direction[2];
+ craftax_update_mobs_direction_choice(draw_key, 4, random_direction);
+ int32_t random_row = old_row + random_direction[0];
+ int32_t random_col = old_col + random_direction[1];
+
+ int32_t distance_row =
+ craftax_update_mobs_abs_i32(state->player_position[0] - old_row);
+ int32_t distance_col =
+ craftax_update_mobs_abs_i32(state->player_position[1] - old_col);
+ draw_key = craftax_update_mobs_next_random_key(rng);
+ int32_t player_move_axis = craftax_update_mobs_player_axis_choice(
+ draw_key,
+ distance_row,
+ distance_col
+ );
+ int32_t player_direction[2] = {0, 0};
+ if (player_move_axis == 0) {
+ player_direction[0] =
+ craftax_update_mobs_sign_i32(state->player_position[0] - old_row);
+ } else {
+ player_direction[1] =
+ craftax_update_mobs_sign_i32(state->player_position[1] - old_col);
+ }
+ int32_t towards_row = old_row + player_direction[0];
+ int32_t towards_col = old_col + player_direction[1];
+ int32_t away_row = old_row - player_direction[0];
+ int32_t away_col = old_col - player_direction[1];
+
+ int32_t distance_to_player = distance_row + distance_col;
+ bool far_from_player = distance_to_player >= 6;
+ bool too_close_to_player = distance_to_player <= 3;
+ int32_t proposed_row = far_from_player ? towards_row : random_row;
+ int32_t proposed_col = far_from_player ? towards_col : random_col;
+ if (too_close_to_player) {
+ proposed_row = away_row;
+ proposed_col = away_col;
+ }
+
+ draw_key = craftax_update_mobs_next_random_key(rng);
+ if (!(craftax_threefry_uniform_f32(draw_key) > 0.85f)) {
+ proposed_row = random_row;
+ proposed_col = random_col;
+ }
+
+ bool collision[3];
+ craftax_update_mobs_collision_map(
+ mob_type,
+ CRAFTAX_MOB_RANGED,
+ collision
+ );
+
+ bool is_attacking_player =
+ distance_to_player >= 4 && distance_to_player <= 5;
+ bool proposed_valid = craftax_update_mobs_valid_position(
+ state,
+ proposed_row,
+ proposed_col,
+ collision
+ );
+ is_attacking_player = is_attacking_player
+ || (too_close_to_player && !proposed_valid);
+ is_attacking_player = is_attacking_player
+ && old_cooldown <= 0
+ && old_mask;
+
+ bool can_spawn_projectile =
+ craftax_update_mobs_count_mob_projectiles(state, level)
+ < CRAFTAX_MAX_MOB_PROJECTILES;
+ bool is_spawning_projectile =
+ is_attacking_player && can_spawn_projectile;
+ int32_t projectile_position[2] = {old_row, old_col};
+ int32_t projectile_type =
+ craftax_update_mobs_projectile_type_for_ranged(mob_type);
+ craftax_update_mobs_spawn_mob_projectile(
+ state,
+ level,
+ is_spawning_projectile,
+ projectile_position,
+ player_direction,
+ projectile_type
+ );
+
+ if (is_attacking_player) {
+ proposed_row = old_row;
+ proposed_col = old_col;
+ }
+ int32_t new_cooldown = is_attacking_player ? 4 : old_cooldown - 1;
+
+ bool valid_move = craftax_update_mobs_valid_position(
+ state,
+ proposed_row,
+ proposed_col,
+ collision
+ );
+ int32_t new_row = valid_move ? proposed_row : old_row;
+ int32_t new_col = valid_move ? proposed_col : old_col;
+
+ bool should_not_despawn = distance_to_player < CRAFTAX_MOB_DESPAWN_DISTANCE
+ || craftax_step_is_fighting_boss(state);
+
+ craftax_update_mobs_clear_old_map_entry(
+ state,
+ level,
+ old_row,
+ old_col,
+ old_mask
+ );
+ bool new_mask = old_mask && should_not_despawn;
+ craftax_update_mobs_enter_new_map_entry(
+ state,
+ level,
+ new_row,
+ new_col,
+ new_mask
+ );
+
+ state->ranged_mobs.position[level][index][0] = new_row;
+ state->ranged_mobs.position[level][index][1] = new_col;
+ state->ranged_mobs.attack_cooldown[level][index] = new_cooldown;
+ state->ranged_mobs.mask[level][index] = new_mask;
+}
+
+static inline void craftax_update_mobs_move_mob_projectile(
+ CraftaxState* state,
+ int32_t index
+) {
+ int32_t level = state->player_level;
+ bool old_mask = state->mob_projectiles.mask[level][index];
+#ifndef CRAFTAX_JAX_PARITY
+ if (!old_mask) return;
+#endif
+ int32_t old_row = state->mob_projectiles.position[level][index][0];
+ int32_t old_col = state->mob_projectiles.position[level][index][1];
+ int32_t proposed_row =
+ old_row + state->mob_projectile_directions[level][index][0];
+ int32_t proposed_col =
+ old_col + state->mob_projectile_directions[level][index][1];
+
+ bool proposed_in_player =
+ proposed_row == state->player_position[0]
+ && proposed_col == state->player_position[1];
+ bool proposed_in_bounds = craftax_update_mobs_in_bounds(
+ proposed_row,
+ proposed_col
+ );
+ int32_t proposed_block = craftax_update_mobs_read_block(
+ state,
+ level,
+ proposed_row,
+ proposed_col
+ );
+ bool in_wall = craftax_step_is_solid_block(proposed_block)
+ && proposed_block != CRAFTAX_BLOCK_WATER;
+ bool in_mob = craftax_step_is_in_mob(state, proposed_row, proposed_col);
+ bool continue_move = proposed_in_bounds && !in_wall && !in_mob;
+
+ bool hit_player0 =
+ old_row == state->player_position[0]
+ && old_col == state->player_position[1]
+ && old_mask;
+ bool hit_player1 = proposed_in_player && old_mask;
+ bool hit_player = hit_player0 || hit_player1;
+ continue_move = continue_move && !hit_player;
+
+ bool new_mask = continue_move && old_mask;
+
+ bool hit_bench_or_furnace = proposed_block == CRAFTAX_BLOCK_FURNACE
+ || proposed_block == CRAFTAX_BLOCK_CRAFTING_TABLE;
+ bool removing_block = hit_bench_or_furnace && old_mask;
+ int32_t new_block = removing_block ? CRAFTAX_BLOCK_PATH : proposed_block;
+
+ int32_t projectile_type =
+ state->mob_projectiles.type_id[level][index];
+ float damage_vector[3];
+ craftax_update_mobs_damage_vector(
+ projectile_type,
+ CRAFTAX_MOB_PROJECTILE,
+ damage_vector
+ );
+ float damage = craftax_update_mobs_damage_done_to_player(
+ state,
+ damage_vector
+ );
+
+ state->mob_projectiles.position[level][index][0] = proposed_row;
+ state->mob_projectiles.position[level][index][1] = proposed_col;
+ state->mob_projectiles.mask[level][index] = new_mask;
+ state->player_health -= damage * (float)(int32_t)hit_player;
+ state->is_sleeping = state->is_sleeping && !hit_player;
+ state->is_resting = state->is_resting && !hit_player;
+ craftax_update_mobs_set_block(
+ state,
+ level,
+ proposed_row,
+ proposed_col,
+ new_block
+ );
+}
+
+static inline void craftax_update_mobs_move_player_projectile(
+ CraftaxState* state,
+ int32_t index
+) {
+ int32_t level = state->player_level;
+ bool old_mask = state->player_projectiles.mask[level][index];
+#ifndef CRAFTAX_JAX_PARITY
+ if (!old_mask) return;
+#endif
+ int32_t old_row = state->player_projectiles.position[level][index][0];
+ int32_t old_col = state->player_projectiles.position[level][index][1];
+ int32_t proposed_row =
+ old_row + state->player_projectile_directions[level][index][0];
+ int32_t proposed_col =
+ old_col + state->player_projectile_directions[level][index][1];
+
+ float damage_vector[3];
+ craftax_update_mobs_player_projectile_damage_vector(
+ state,
+ level,
+ index,
+ damage_vector
+ );
+
+ bool proposed_in_bounds = craftax_update_mobs_in_bounds(
+ proposed_row,
+ proposed_col
+ );
+ int32_t proposed_block = craftax_update_mobs_read_block(
+ state,
+ level,
+ proposed_row,
+ proposed_col
+ );
+ bool in_wall = craftax_step_is_solid_block(proposed_block)
+ && proposed_block != CRAFTAX_BLOCK_WATER;
+
+ bool did_attack_mob0 = false;
+ bool did_kill_mob0 = false;
+ craftax_update_mobs_attack_mob_with_damage(
+ state,
+ old_row,
+ old_col,
+ damage_vector,
+ false,
+ &did_attack_mob0,
+ &did_kill_mob0
+ );
+ (void)did_kill_mob0;
+
+ float second_damage_vector[3];
+ for (int32_t i = 0; i < 3; i++) {
+ second_damage_vector[i] =
+ damage_vector[i] * (float)(int32_t)(!did_attack_mob0);
+ }
+
+ bool did_attack_mob1 = false;
+ bool did_kill_mob1 = false;
+ craftax_update_mobs_attack_mob_with_damage(
+ state,
+ proposed_row,
+ proposed_col,
+ second_damage_vector,
+ false,
+ &did_attack_mob1,
+ &did_kill_mob1
+ );
+ (void)did_kill_mob1;
+
+ bool did_attack_mob = did_attack_mob0 || did_attack_mob1;
+ bool continue_move = proposed_in_bounds && !in_wall && !did_attack_mob;
+ bool new_mask = continue_move && old_mask;
+
+ state->player_projectiles.position[level][index][0] = proposed_row;
+ state->player_projectiles.position[level][index][1] = proposed_col;
+ state->player_projectiles.mask[level][index] = new_mask;
+}
+
+static inline void craftax_update_mobs_native(
+ CraftaxState* state,
+ CraftaxThreefryKey rng
+) {
+ CraftaxThreefryKey unused;
+
+ craftax_threefry_split(rng, &rng, &unused);
+ craftax_update_mobs_move_melee(state, &rng, 0);
+ craftax_update_mobs_move_melee(state, &rng, 1);
+ craftax_update_mobs_move_melee(state, &rng, 2);
+
+ craftax_threefry_split(rng, &rng, &unused);
+ craftax_update_mobs_move_passive(state, &rng, 0);
+ craftax_update_mobs_move_passive(state, &rng, 1);
+ craftax_update_mobs_move_passive(state, &rng, 2);
+
+ craftax_threefry_split(rng, &rng, &unused);
+ craftax_update_mobs_move_ranged(state, &rng, 0);
+ craftax_update_mobs_move_ranged(state, &rng, 1);
+
+ craftax_threefry_split(rng, &rng, &unused);
+ craftax_update_mobs_move_mob_projectile(state, 0);
+ craftax_update_mobs_move_mob_projectile(state, 1);
+ craftax_update_mobs_move_mob_projectile(state, 2);
+
+ craftax_threefry_split(rng, &rng, &unused);
+ craftax_update_mobs_move_player_projectile(state, 0);
+ craftax_update_mobs_move_player_projectile(state, 1);
+ craftax_update_mobs_move_player_projectile(state, 2);
+}
diff --git a/ocean/craftax/threefry.h b/ocean/craftax/threefry.h
new file mode 100644
index 0000000000..4d4004e9bd
--- /dev/null
+++ b/ocean/craftax/threefry.h
@@ -0,0 +1,126 @@
+// Fast RNG helpers for Craftax.
+// Replaces JAX Threefry with SplitMix64-based hashing for ~20-50x speedup.
+// NOT cryptographically secure and NOT JAX-compatible.
+
+#pragma once
+
+#include
+#include
+#include
+
+typedef struct CraftaxThreefryKey {
+ uint32_t word[2];
+} CraftaxThreefryKey;
+
+static inline uint64_t craftax_key_to_u64(CraftaxThreefryKey key) {
+ return ((uint64_t)key.word[1] << 32) | key.word[0];
+}
+
+static inline CraftaxThreefryKey craftax_u64_to_key(uint64_t x) {
+ CraftaxThreefryKey key = {{(uint32_t)x, (uint32_t)(x >> 32)}};
+ return key;
+}
+
+static inline uint32_t craftax_rotl32(uint32_t x, uint32_t k) {
+ return (uint32_t)((x << k) | (x >> (32u - k)));
+}
+
+static inline CraftaxThreefryKey craftax_prng_key(uint32_t seed) {
+ CraftaxThreefryKey key = {{seed, seed ^ 0x9E3779B9u}};
+ return key;
+}
+
+// MurmurHash3 64-bit finalizer — fast and good mixing
+static inline uint64_t craftax_mix64(uint64_t x) {
+ x ^= x >> 33;
+ x *= 0xff51afd7ed558ccdULL;
+ x ^= x >> 33;
+ x *= 0xc4ceb9fe1a85ec53ULL;
+ x ^= x >> 33;
+ return x;
+}
+
+// Core hash: mixes key state with counter, returns 64 bits of pseudo-randomness
+static inline uint64_t craftax_fast_hash64(CraftaxThreefryKey key, uint64_t counter) {
+ uint64_t x = craftax_key_to_u64(key);
+ x ^= counter;
+ return craftax_mix64(x);
+}
+
+static inline void craftax_threefry2x32(
+ CraftaxThreefryKey key,
+ uint32_t count0,
+ uint32_t count1,
+ uint32_t out[2]
+) {
+ uint64_t h = craftax_fast_hash64(key, ((uint64_t)count1 << 32) | count0);
+ out[0] = (uint32_t)h;
+ out[1] = (uint32_t)(h >> 32);
+}
+
+static inline CraftaxThreefryKey craftax_threefry_counter_key(
+ CraftaxThreefryKey key,
+ uint32_t count0,
+ uint32_t count1
+) {
+ return craftax_u64_to_key(craftax_fast_hash64(key, ((uint64_t)count1 << 32) | count0));
+}
+
+// Fast split: sequential PCG-style advancement
+static inline void craftax_threefry_split(
+ CraftaxThreefryKey key,
+ CraftaxThreefryKey* left,
+ CraftaxThreefryKey* right
+) {
+ uint64_t state = craftax_key_to_u64(key);
+ uint64_t s1 = state * 6364136223846793005ULL + 1;
+ uint64_t s2 = s1 * 6364136223846793005ULL + 1;
+ *left = craftax_u64_to_key(s1);
+ *right = craftax_u64_to_key(s2);
+}
+
+static inline void craftax_threefry_split_n(
+ CraftaxThreefryKey key,
+ CraftaxThreefryKey* out,
+ size_t count
+) {
+ uint64_t state = craftax_key_to_u64(key);
+ for (size_t i = 0; i < count; i++) {
+ state = state * 6364136223846793005ULL + 1;
+ out[i] = craftax_u64_to_key(state);
+ }
+}
+
+static inline CraftaxThreefryKey craftax_threefry_fold_in(
+ CraftaxThreefryKey key,
+ uint32_t data
+) {
+ return craftax_threefry_counter_key(key, 0u, data);
+}
+
+static inline uint32_t craftax_threefry_uniform_u32_at(
+ CraftaxThreefryKey key,
+ uint64_t index
+) {
+ uint64_t h = craftax_fast_hash64(key, index);
+ return (uint32_t)h ^ (uint32_t)(h >> 32);
+}
+
+static inline uint32_t craftax_threefry_uniform_u32(CraftaxThreefryKey key) {
+ return craftax_threefry_uniform_u32_at(key, 0u);
+}
+
+static inline float craftax_threefry_uniform_f32_at(
+ CraftaxThreefryKey key,
+ uint64_t index
+) {
+ uint32_t bits = craftax_threefry_uniform_u32_at(key, index);
+ uint32_t float_bits = (bits >> 9u) | 0x3F800000u;
+ float value;
+ memcpy(&value, &float_bits, sizeof(value));
+ return value - 1.0f;
+}
+
+static inline float craftax_threefry_uniform_f32(CraftaxThreefryKey key) {
+ return craftax_threefry_uniform_f32_at(key, 0u);
+}
diff --git a/ocean/craftax/worldgen.h b/ocean/craftax/worldgen.h
new file mode 100644
index 0000000000..fc50fa7283
--- /dev/null
+++ b/ocean/craftax/worldgen.h
@@ -0,0 +1,1862 @@
+// Native Craftax reset world generation.
+//
+// This mirrors craftax/craftax/world_gen/world_gen.py for the default
+// EnvParams and StaticEnvParams used by Craftax-Symbolic-v1 reset.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "noise.h"
+
+#define CRAFTAX_WG_MAP_SIZE 48
+#define CRAFTAX_WG_MAP_CELLS (CRAFTAX_WG_MAP_SIZE * CRAFTAX_WG_MAP_SIZE)
+#define CRAFTAX_WG_NUM_LEVELS 9
+#define CRAFTAX_WG_OBS_ROWS 9
+#define CRAFTAX_WG_OBS_COLS 11
+#define CRAFTAX_WG_NUM_BLOCK_TYPES 37
+#define CRAFTAX_WG_NUM_ITEM_TYPES 5
+#define CRAFTAX_WG_NUM_MOB_CLASSES 5
+#define CRAFTAX_WG_NUM_MOB_TYPES 8
+#define CRAFTAX_WG_INVENTORY_OBS_SIZE 51
+
+// Compact binary observation encoding.
+// Each cell uses binary channels instead of one-hot:
+// 6 bits: block type (0-63, covers 37 block types)
+// 3 bits: item type+1 (0=no item, 1-5=item types)
+// 4 bits per mob class: mob type+1 (0=no mob, 1-8=types) x 5 classes
+// 1 bit : visibility
+// Total: 30 binary channels per cell.
+#define CRAFTAX_WG_BINARY_BLOCK_BITS 6
+#define CRAFTAX_WG_BINARY_ITEM_BITS 3
+#define CRAFTAX_WG_BINARY_MOB_BITS 4
+#define CRAFTAX_WG_BINARY_VISIBILITY_BITS 1
+
+#define CRAFTAX_WG_BINARY_CHANNELS_PER_CELL ( \
+ CRAFTAX_WG_BINARY_BLOCK_BITS + \
+ CRAFTAX_WG_BINARY_ITEM_BITS + \
+ CRAFTAX_WG_NUM_MOB_CLASSES * CRAFTAX_WG_BINARY_MOB_BITS + \
+ CRAFTAX_WG_BINARY_VISIBILITY_BITS \
+)
+
+#define CRAFTAX_WG_BINARY_MAP_OBS_SIZE ( \
+ CRAFTAX_WG_OBS_ROWS * CRAFTAX_WG_OBS_COLS * CRAFTAX_WG_BINARY_CHANNELS_PER_CELL \
+)
+#define CRAFTAX_WG_OBS_WINDOW_CELLS (CRAFTAX_WG_OBS_ROWS * CRAFTAX_WG_OBS_COLS)
+#define CRAFTAX_WG_CELL_TEMPLATE_BYTES ( \
+ CRAFTAX_WG_BINARY_CHANNELS_PER_CELL * sizeof(float) \
+)
+#define CRAFTAX_WG_FULL_OBS_SIZE ( \
+ CRAFTAX_WG_BINARY_MAP_OBS_SIZE + CRAFTAX_WG_INVENTORY_OBS_SIZE \
+)
+
+// Moonshot symbolic observation. Each visible cell stores compact float IDs:
+// block, item+1, visible, and one mob type+1 slot for each mob class.
+// The 51 scalar channels remain exact floats for oracle-expandability.
+#define CRAFTAX_WG_PACKED_CHANNELS_PER_CELL (3 + CRAFTAX_WG_NUM_MOB_CLASSES)
+#define CRAFTAX_WG_PACKED_MAP_OBS_SIZE ( \
+ CRAFTAX_WG_OBS_ROWS * CRAFTAX_WG_OBS_COLS * CRAFTAX_WG_PACKED_CHANNELS_PER_CELL \
+)
+#define CRAFTAX_WG_PACKED_OBS_SIZE ( \
+ CRAFTAX_WG_PACKED_MAP_OBS_SIZE + CRAFTAX_WG_INVENTORY_OBS_SIZE \
+)
+
+// Lookup tables for fast binary bit writing (eliminates loops/branches)
+static const float CRAFTAX_WG_BLOCK_LUT[64][6] = {
+ {0.0f,0.0f,0.0f,0.0f,0.0f,0.0f},{1.0f,0.0f,0.0f,0.0f,0.0f,0.0f},{0.0f,1.0f,0.0f,0.0f,0.0f,0.0f},{1.0f,1.0f,0.0f,0.0f,0.0f,0.0f},
+ {0.0f,0.0f,1.0f,0.0f,0.0f,0.0f},{1.0f,0.0f,1.0f,0.0f,0.0f,0.0f},{0.0f,1.0f,1.0f,0.0f,0.0f,0.0f},{1.0f,1.0f,1.0f,0.0f,0.0f,0.0f},
+ {0.0f,0.0f,0.0f,1.0f,0.0f,0.0f},{1.0f,0.0f,0.0f,1.0f,0.0f,0.0f},{0.0f,1.0f,0.0f,1.0f,0.0f,0.0f},{1.0f,1.0f,0.0f,1.0f,0.0f,0.0f},
+ {0.0f,0.0f,1.0f,1.0f,0.0f,0.0f},{1.0f,0.0f,1.0f,1.0f,0.0f,0.0f},{0.0f,1.0f,1.0f,1.0f,0.0f,0.0f},{1.0f,1.0f,1.0f,1.0f,0.0f,0.0f},
+ {0.0f,0.0f,0.0f,0.0f,1.0f,0.0f},{1.0f,0.0f,0.0f,0.0f,1.0f,0.0f},{0.0f,1.0f,0.0f,0.0f,1.0f,0.0f},{1.0f,1.0f,0.0f,0.0f,1.0f,0.0f},
+ {0.0f,0.0f,1.0f,0.0f,1.0f,0.0f},{1.0f,0.0f,1.0f,0.0f,1.0f,0.0f},{0.0f,1.0f,1.0f,0.0f,1.0f,0.0f},{1.0f,1.0f,1.0f,0.0f,1.0f,0.0f},
+ {0.0f,0.0f,0.0f,1.0f,1.0f,0.0f},{1.0f,0.0f,0.0f,1.0f,1.0f,0.0f},{0.0f,1.0f,0.0f,1.0f,1.0f,0.0f},{1.0f,1.0f,0.0f,1.0f,1.0f,0.0f},
+ {0.0f,0.0f,1.0f,1.0f,1.0f,0.0f},{1.0f,0.0f,1.0f,1.0f,1.0f,0.0f},{0.0f,1.0f,1.0f,1.0f,1.0f,0.0f},{1.0f,1.0f,1.0f,1.0f,1.0f,0.0f},
+ {0.0f,0.0f,0.0f,0.0f,0.0f,1.0f},{1.0f,0.0f,0.0f,0.0f,0.0f,1.0f},{0.0f,1.0f,0.0f,0.0f,0.0f,1.0f},{1.0f,1.0f,0.0f,0.0f,0.0f,1.0f},
+ {0.0f,0.0f,1.0f,0.0f,0.0f,1.0f},{1.0f,0.0f,1.0f,0.0f,0.0f,1.0f},{0.0f,1.0f,1.0f,0.0f,0.0f,1.0f},{1.0f,1.0f,1.0f,0.0f,0.0f,1.0f},
+ {0.0f,0.0f,0.0f,1.0f,0.0f,1.0f},{1.0f,0.0f,0.0f,1.0f,0.0f,1.0f},{0.0f,1.0f,0.0f,1.0f,0.0f,1.0f},{1.0f,1.0f,0.0f,1.0f,0.0f,1.0f},
+ {0.0f,0.0f,1.0f,1.0f,0.0f,1.0f},{1.0f,0.0f,1.0f,1.0f,0.0f,1.0f},{0.0f,1.0f,1.0f,1.0f,0.0f,1.0f},{1.0f,1.0f,1.0f,1.0f,0.0f,1.0f},
+ {0.0f,0.0f,0.0f,0.0f,1.0f,1.0f},{1.0f,0.0f,0.0f,0.0f,1.0f,1.0f},{0.0f,1.0f,0.0f,0.0f,1.0f,1.0f},{1.0f,1.0f,0.0f,0.0f,1.0f,1.0f},
+ {0.0f,0.0f,1.0f,0.0f,1.0f,1.0f},{1.0f,0.0f,1.0f,0.0f,1.0f,1.0f},{0.0f,1.0f,1.0f,0.0f,1.0f,1.0f},{1.0f,1.0f,1.0f,0.0f,1.0f,1.0f},
+ {0.0f,0.0f,0.0f,1.0f,1.0f,1.0f},{1.0f,0.0f,0.0f,1.0f,1.0f,1.0f},{0.0f,1.0f,0.0f,1.0f,1.0f,1.0f},{1.0f,1.0f,0.0f,1.0f,1.0f,1.0f},
+ {0.0f,0.0f,1.0f,1.0f,1.0f,1.0f},{1.0f,0.0f,1.0f,1.0f,1.0f,1.0f},{0.0f,1.0f,1.0f,1.0f,1.0f,1.0f},{1.0f,1.0f,1.0f,1.0f,1.0f,1.0f},
+};
+static const float CRAFTAX_WG_ITEM_LUT[8][3] = {
+ {0.0f,0.0f,0.0f},{1.0f,0.0f,0.0f},{0.0f,1.0f,0.0f},{1.0f,1.0f,0.0f},
+ {0.0f,0.0f,1.0f},{1.0f,0.0f,1.0f},{0.0f,1.0f,1.0f},{1.0f,1.0f,1.0f},
+};
+static const float CRAFTAX_WG_MOB_LUT[16][4] = {
+ {0.0f,0.0f,0.0f,0.0f},{1.0f,0.0f,0.0f,0.0f},{0.0f,1.0f,0.0f,0.0f},{1.0f,1.0f,0.0f,0.0f},
+ {0.0f,0.0f,1.0f,0.0f},{1.0f,0.0f,1.0f,0.0f},{0.0f,1.0f,1.0f,0.0f},{1.0f,1.0f,1.0f,0.0f},
+ {0.0f,0.0f,0.0f,1.0f},{1.0f,0.0f,0.0f,1.0f},{0.0f,1.0f,0.0f,1.0f},{1.0f,1.0f,0.0f,1.0f},
+ {0.0f,0.0f,1.0f,1.0f},{1.0f,0.0f,1.0f,1.0f},{0.0f,1.0f,1.0f,1.0f},{1.0f,1.0f,1.0f,1.0f},
+};
+static float CRAFTAX_WG_VISIBLE_CELL_TEMPLATE_LUT[64][8][CRAFTAX_WG_BINARY_CHANNELS_PER_CELL];
+static float CRAFTAX_WG_EMPTY_CELL_TEMPLATE[CRAFTAX_WG_BINARY_CHANNELS_PER_CELL];
+static bool CRAFTAX_WG_CELL_TEMPLATE_READY = false;
+
+static inline void craftax_wg_init_cell_templates(void) {
+ if (CRAFTAX_WG_CELL_TEMPLATE_READY) {
+ return;
+ }
+
+ for (int block = 0; block < 64; block++) {
+ for (int item = 0; item < 8; item++) {
+ float* cell = CRAFTAX_WG_VISIBLE_CELL_TEMPLATE_LUT[block][item];
+ memcpy(cell, CRAFTAX_WG_BLOCK_LUT[block], 6 * sizeof(float));
+ memcpy(cell + CRAFTAX_WG_BINARY_BLOCK_BITS, CRAFTAX_WG_ITEM_LUT[item], 3 * sizeof(float));
+ cell[CRAFTAX_WG_BINARY_CHANNELS_PER_CELL - 1] = 1.0f;
+ }
+ }
+
+ CRAFTAX_WG_CELL_TEMPLATE_READY = true;
+}
+
+#define CRAFTAX_WG_OBS_SIZE CRAFTAX_WG_PACKED_OBS_SIZE
+#define CRAFTAX_WG_NUM_ACHIEVEMENTS 67
+#define CRAFTAX_WG_MAX_MELEE_MOBS 3
+#define CRAFTAX_WG_MAX_PASSIVE_MOBS 3
+#define CRAFTAX_WG_MAX_RANGED_MOBS 2
+#define CRAFTAX_WG_MAX_MOB_PROJECTILES 3
+#define CRAFTAX_WG_MAX_PLAYER_PROJECTILES 3
+#define CRAFTAX_WG_MAX_GROWING_PLANTS 10
+#define CRAFTAX_WG_MONSTERS_KILLED_TO_CLEAR_LEVEL 8
+
+// Backwards-compatible names used by the phase-1 floor-0 test.
+#define CRAFTAX_OVERWORLD_SIZE CRAFTAX_WG_MAP_SIZE
+#define CRAFTAX_OVERWORLD_CELLS CRAFTAX_WG_MAP_CELLS
+
+#define CRAFTAX_WG_BLOCK_INVALID 0
+#define CRAFTAX_WG_BLOCK_OUT_OF_BOUNDS 1
+#define CRAFTAX_WG_BLOCK_GRASS 2
+#define CRAFTAX_WG_BLOCK_WATER 3
+#define CRAFTAX_WG_BLOCK_STONE 4
+#define CRAFTAX_WG_BLOCK_TREE 5
+#define CRAFTAX_WG_BLOCK_WOOD 6
+#define CRAFTAX_WG_BLOCK_PATH 7
+#define CRAFTAX_WG_BLOCK_COAL 8
+#define CRAFTAX_WG_BLOCK_IRON 9
+#define CRAFTAX_WG_BLOCK_DIAMOND 10
+#define CRAFTAX_WG_BLOCK_CRAFTING_TABLE 11
+#define CRAFTAX_WG_BLOCK_FURNACE 12
+#define CRAFTAX_WG_BLOCK_SAND 13
+#define CRAFTAX_WG_BLOCK_LAVA 14
+#define CRAFTAX_WG_BLOCK_PLANT 15
+#define CRAFTAX_WG_BLOCK_RIPE_PLANT 16
+#define CRAFTAX_WG_BLOCK_WALL 17
+#define CRAFTAX_WG_BLOCK_DARKNESS 18
+#define CRAFTAX_WG_BLOCK_WALL_MOSS 19
+#define CRAFTAX_WG_BLOCK_STALAGMITE 20
+#define CRAFTAX_WG_BLOCK_SAPPHIRE 21
+#define CRAFTAX_WG_BLOCK_RUBY 22
+#define CRAFTAX_WG_BLOCK_CHEST 23
+#define CRAFTAX_WG_BLOCK_FOUNTAIN 24
+#define CRAFTAX_WG_BLOCK_FIRE_GRASS 25
+#define CRAFTAX_WG_BLOCK_ICE_GRASS 26
+#define CRAFTAX_WG_BLOCK_GRAVEL 27
+#define CRAFTAX_WG_BLOCK_FIRE_TREE 28
+#define CRAFTAX_WG_BLOCK_ICE_SHRUB 29
+#define CRAFTAX_WG_BLOCK_ENCHANTMENT_TABLE_FIRE 30
+#define CRAFTAX_WG_BLOCK_ENCHANTMENT_TABLE_ICE 31
+#define CRAFTAX_WG_BLOCK_NECROMANCER 32
+#define CRAFTAX_WG_BLOCK_GRAVE 33
+#define CRAFTAX_WG_BLOCK_GRAVE2 34
+#define CRAFTAX_WG_BLOCK_GRAVE3 35
+#define CRAFTAX_WG_BLOCK_NECROMANCER_VULNERABLE 36
+
+#define CRAFTAX_WG_ITEM_NONE 0
+#define CRAFTAX_WG_ITEM_TORCH 1
+#define CRAFTAX_WG_ITEM_LADDER_DOWN 2
+#define CRAFTAX_WG_ITEM_LADDER_UP 3
+#define CRAFTAX_WG_ITEM_LADDER_DOWN_BLOCKED 4
+
+#define CRAFTAX_WG_ACTION_UP 3
+#define CRAFTAX_WG_BOSS_FIGHT_SPAWN_TURNS 7
+#define CRAFTAX_WG_PI 3.14159265358979323846f
+
+typedef struct CraftaxOverworldFloor {
+ uint8_t map[CRAFTAX_OVERWORLD_SIZE][CRAFTAX_OVERWORLD_SIZE];
+ uint8_t item_map[CRAFTAX_OVERWORLD_SIZE][CRAFTAX_OVERWORLD_SIZE];
+ uint8_t light_map[CRAFTAX_OVERWORLD_SIZE][CRAFTAX_OVERWORLD_SIZE];
+ int32_t ladder_down[2];
+ int32_t ladder_up[2];
+} CraftaxOverworldFloor;
+
+typedef struct CraftaxWGInventory {
+ int32_t wood;
+ int32_t stone;
+ int32_t coal;
+ int32_t iron;
+ int32_t diamond;
+ int32_t sapling;
+ int32_t pickaxe;
+ int32_t sword;
+ int32_t bow;
+ int32_t arrows;
+ int32_t armour[4];
+ int32_t torches;
+ int32_t ruby;
+ int32_t sapphire;
+ int32_t potions[6];
+ int32_t books;
+} CraftaxWGInventory;
+
+typedef struct CraftaxWGMobs3 {
+ int32_t position[CRAFTAX_WG_NUM_LEVELS][3][2];
+ float health[CRAFTAX_WG_NUM_LEVELS][3];
+ bool mask[CRAFTAX_WG_NUM_LEVELS][3];
+ int32_t attack_cooldown[CRAFTAX_WG_NUM_LEVELS][3];
+ int32_t type_id[CRAFTAX_WG_NUM_LEVELS][3];
+} CraftaxWGMobs3;
+
+typedef struct CraftaxWGMobs2 {
+ int32_t position[CRAFTAX_WG_NUM_LEVELS][2][2];
+ float health[CRAFTAX_WG_NUM_LEVELS][2];
+ bool mask[CRAFTAX_WG_NUM_LEVELS][2];
+ int32_t attack_cooldown[CRAFTAX_WG_NUM_LEVELS][2];
+ int32_t type_id[CRAFTAX_WG_NUM_LEVELS][2];
+} CraftaxWGMobs2;
+
+typedef struct CraftaxWorldState {
+ // === Hot data (accessed every step) ===
+ int32_t player_position[2];
+ int32_t player_level;
+ int32_t player_direction;
+
+ float player_health;
+ int32_t player_food;
+ int32_t player_drink;
+ int32_t player_energy;
+ int32_t player_mana;
+ bool is_sleeping;
+ bool is_resting;
+
+ float player_recover;
+ float player_hunger;
+ float player_thirst;
+ float player_fatigue;
+ float player_recover_mana;
+
+ int32_t player_xp;
+ int32_t player_dexterity;
+ int32_t player_strength;
+ int32_t player_intelligence;
+
+ CraftaxWGInventory inventory;
+
+ CraftaxWGMobs3 melee_mobs;
+ CraftaxWGMobs3 passive_mobs;
+ CraftaxWGMobs2 ranged_mobs;
+
+ CraftaxWGMobs3 mob_projectiles;
+ int32_t mob_projectile_directions[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAX_MOB_PROJECTILES][2];
+ CraftaxWGMobs3 player_projectiles;
+ int32_t player_projectile_directions[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAX_PLAYER_PROJECTILES][2];
+
+ int32_t growing_plants_positions[CRAFTAX_WG_MAX_GROWING_PLANTS][2];
+ int32_t growing_plants_age[CRAFTAX_WG_MAX_GROWING_PLANTS];
+ bool growing_plants_mask[CRAFTAX_WG_MAX_GROWING_PLANTS];
+
+ int32_t potion_mapping[6];
+ bool learned_spells[2];
+
+ int32_t sword_enchantment;
+ int32_t bow_enchantment;
+ int32_t armour_enchantments[4];
+
+ int32_t boss_progress;
+ int32_t boss_timesteps_to_spawn_this_round;
+
+ float light_level;
+ bool achievements[CRAFTAX_WG_NUM_ACHIEVEMENTS];
+ uint32_t state_rng[2];
+ int32_t timestep;
+ int32_t fractal_noise_angles[4];
+
+ // === Medium-hot bitmaps ===
+ uint64_t mob_bits[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAP_SIZE];
+ uint64_t spawn_all_bits[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAP_SIZE];
+ uint64_t spawn_grave_bits[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAP_SIZE];
+ uint64_t spawn_water_bits[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAP_SIZE];
+
+ // === Cold data (large maps) ===
+ uint8_t map[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE];
+ uint8_t item_map[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE];
+ uint8_t light_map[CRAFTAX_WG_NUM_LEVELS][CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE];
+
+ int32_t down_ladders[CRAFTAX_WG_NUM_LEVELS][2];
+ int32_t up_ladders[CRAFTAX_WG_NUM_LEVELS][2];
+ bool chests_opened[CRAFTAX_WG_NUM_LEVELS];
+ int32_t monsters_killed[CRAFTAX_WG_NUM_LEVELS];
+} CraftaxWorldState;
+
+typedef struct CraftaxSmoothGenConfig {
+ int32_t default_block;
+ int32_t sea_block;
+ int32_t coast_block;
+ int32_t mountain_block;
+ int32_t path_block;
+ int32_t inner_mountain_block;
+ int32_t ore_requirement_blocks[5];
+ int32_t ores[5];
+ float ore_chances[5];
+ int32_t tree_requirement_block;
+ int32_t tree;
+ int32_t lava;
+ int32_t player_spawn;
+ int32_t valid_ladder;
+ bool ladder_up;
+ bool ladder_down;
+ float player_proximity_map_water_strength;
+ float player_proximity_map_water_max;
+ float player_proximity_map_mountain_strength;
+ float player_proximity_map_mountain_max;
+ float default_light;
+ float water_threshold;
+ float sand_threshold;
+ float tree_threshold_uniform;
+ float tree_threshold_perlin;
+} CraftaxSmoothGenConfig;
+
+typedef struct CraftaxDungeonConfig {
+ int32_t special_block;
+ int32_t fountain_block;
+ int32_t rare_path_replacement_block;
+} CraftaxDungeonConfig;
+
+static const CraftaxSmoothGenConfig CRAFTAX_SMOOTHGEN_CONFIGS[6] = {
+ {
+ CRAFTAX_WG_BLOCK_GRASS,
+ CRAFTAX_WG_BLOCK_WATER,
+ CRAFTAX_WG_BLOCK_SAND,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_PATH,
+ {CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE},
+ {CRAFTAX_WG_BLOCK_COAL, CRAFTAX_WG_BLOCK_IRON, CRAFTAX_WG_BLOCK_DIAMOND, CRAFTAX_WG_BLOCK_OUT_OF_BOUNDS, CRAFTAX_WG_BLOCK_OUT_OF_BOUNDS},
+ {0.03f, 0.02f, 0.001f, 0.0f, 0.0f},
+ CRAFTAX_WG_BLOCK_GRASS,
+ CRAFTAX_WG_BLOCK_TREE,
+ CRAFTAX_WG_BLOCK_LAVA,
+ CRAFTAX_WG_BLOCK_GRASS,
+ CRAFTAX_WG_BLOCK_PATH,
+ false,
+ true,
+ 5.0f,
+ 1.0f,
+ 5.0f,
+ 1.0f,
+ 1.0f,
+ 0.7f,
+ 0.6f,
+ 0.8f,
+ 0.5f,
+ },
+ {
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_WATER,
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_STONE,
+ {CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE},
+ {CRAFTAX_WG_BLOCK_COAL, CRAFTAX_WG_BLOCK_IRON, CRAFTAX_WG_BLOCK_DIAMOND, CRAFTAX_WG_BLOCK_SAPPHIRE, CRAFTAX_WG_BLOCK_RUBY},
+ {0.04f, 0.02f, 0.005f, 0.0025f, 0.0025f},
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_STALAGMITE,
+ CRAFTAX_WG_BLOCK_LAVA,
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_PATH,
+ true,
+ true,
+ 5.0f,
+ 1.0f,
+ 17.0f,
+ 1.5f,
+ 0.0f,
+ 0.7f,
+ 0.6f,
+ 0.8f,
+ 0.5f,
+ },
+ {
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_WATER,
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_STONE,
+ {CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE},
+ {CRAFTAX_WG_BLOCK_COAL, CRAFTAX_WG_BLOCK_IRON, CRAFTAX_WG_BLOCK_DIAMOND, CRAFTAX_WG_BLOCK_SAPPHIRE, CRAFTAX_WG_BLOCK_RUBY},
+ {0.04f, 0.03f, 0.01f, 0.01f, 0.01f},
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_STALAGMITE,
+ CRAFTAX_WG_BLOCK_LAVA,
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_PATH,
+ true,
+ true,
+ 5.0f,
+ 1.0f,
+ 17.0f,
+ 1.5f,
+ 0.0f,
+ 0.7f,
+ 0.6f,
+ 0.8f,
+ 0.5f,
+ },
+ {
+ CRAFTAX_WG_BLOCK_FIRE_GRASS,
+ CRAFTAX_WG_BLOCK_LAVA,
+ CRAFTAX_WG_BLOCK_SAND,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_STONE,
+ {CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE},
+ {CRAFTAX_WG_BLOCK_COAL, CRAFTAX_WG_BLOCK_IRON, CRAFTAX_WG_BLOCK_DIAMOND, CRAFTAX_WG_BLOCK_SAPPHIRE, CRAFTAX_WG_BLOCK_RUBY},
+ {0.05f, 0.0f, 0.0f, 0.0f, 0.025f},
+ CRAFTAX_WG_BLOCK_FIRE_GRASS,
+ CRAFTAX_WG_BLOCK_FIRE_TREE,
+ CRAFTAX_WG_BLOCK_LAVA,
+ CRAFTAX_WG_BLOCK_FIRE_GRASS,
+ CRAFTAX_WG_BLOCK_FIRE_GRASS,
+ true,
+ true,
+ 5.0f,
+ 1.0f,
+ 5.0f,
+ 1.0f,
+ 1.0f,
+ 0.5f,
+ 0.6f,
+ 0.8f,
+ 0.5f,
+ },
+ {
+ CRAFTAX_WG_BLOCK_ICE_GRASS,
+ CRAFTAX_WG_BLOCK_WATER,
+ CRAFTAX_WG_BLOCK_ICE_GRASS,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_STONE,
+ CRAFTAX_WG_BLOCK_STONE,
+ {CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE, CRAFTAX_WG_BLOCK_STONE},
+ {CRAFTAX_WG_BLOCK_COAL, CRAFTAX_WG_BLOCK_IRON, CRAFTAX_WG_BLOCK_DIAMOND, CRAFTAX_WG_BLOCK_SAPPHIRE, CRAFTAX_WG_BLOCK_RUBY},
+ {0.0f, 0.0f, 0.005f, 0.02f, 0.0f},
+ CRAFTAX_WG_BLOCK_ICE_GRASS,
+ CRAFTAX_WG_BLOCK_ICE_SHRUB,
+ CRAFTAX_WG_BLOCK_WATER,
+ CRAFTAX_WG_BLOCK_ICE_GRASS,
+ CRAFTAX_WG_BLOCK_ICE_GRASS,
+ true,
+ true,
+ 5.0f,
+ 1.0f,
+ 17.0f,
+ 1.5f,
+ 0.0f,
+ 0.5f,
+ 0.6f,
+ 0.4f,
+ 0.5f,
+ },
+ {
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_WALL,
+ CRAFTAX_WG_BLOCK_WALL,
+ CRAFTAX_WG_BLOCK_WALL,
+ {CRAFTAX_WG_BLOCK_WALL, CRAFTAX_WG_BLOCK_GRAVE, CRAFTAX_WG_BLOCK_GRAVE, CRAFTAX_WG_BLOCK_WALL, CRAFTAX_WG_BLOCK_WALL},
+ {CRAFTAX_WG_BLOCK_WALL_MOSS, CRAFTAX_WG_BLOCK_GRAVE2, CRAFTAX_WG_BLOCK_GRAVE3, CRAFTAX_WG_BLOCK_SAPPHIRE, CRAFTAX_WG_BLOCK_RUBY},
+ {0.1f, 0.333f, 0.5f, 0.0f, 0.0f},
+ CRAFTAX_WG_BLOCK_PATH,
+ CRAFTAX_WG_BLOCK_GRAVE,
+ CRAFTAX_WG_BLOCK_WALL,
+ CRAFTAX_WG_BLOCK_NECROMANCER,
+ CRAFTAX_WG_BLOCK_PATH,
+ false,
+ false,
+ 5.0f,
+ 1.0f,
+ 10.0f,
+ 10.0f,
+ 0.0f,
+ 0.7f,
+ 0.6f,
+ 0.95f,
+ -1.0f,
+ },
+};
+
+static const CraftaxDungeonConfig CRAFTAX_DUNGEON_CONFIGS[3] = {
+ {CRAFTAX_WG_BLOCK_PATH, CRAFTAX_WG_BLOCK_FOUNTAIN, CRAFTAX_WG_BLOCK_PATH},
+ {CRAFTAX_WG_BLOCK_ENCHANTMENT_TABLE_ICE, CRAFTAX_WG_BLOCK_WATER, CRAFTAX_WG_BLOCK_WATER},
+ {CRAFTAX_WG_BLOCK_ENCHANTMENT_TABLE_FIRE, CRAFTAX_WG_BLOCK_FOUNTAIN, CRAFTAX_WG_BLOCK_PATH},
+};
+
+static inline float craftax_wg_clampf(float value, float low, float high) {
+ if (value < low) {
+ return low;
+ }
+ if (value > high) {
+ return high;
+ }
+ return value;
+}
+
+static inline int craftax_wg_clampi(int value, int low, int high) {
+ if (value < low) {
+ return low;
+ }
+ if (value > high) {
+ return high;
+ }
+ return value;
+}
+
+static inline size_t craftax_wg_index(int row, int col) {
+ return (size_t)row * (size_t)CRAFTAX_WG_MAP_SIZE + (size_t)col;
+}
+
+static inline void craftax_threefry_split3(
+ CraftaxThreefryKey key,
+ CraftaxThreefryKey* first,
+ CraftaxThreefryKey* second,
+ CraftaxThreefryKey* third
+) {
+ CraftaxThreefryKey keys[3];
+ craftax_threefry_split_n(key, keys, 3);
+ *first = keys[0];
+ *second = keys[1];
+ *third = keys[2];
+}
+
+static inline CraftaxThreefryKey craftax_worldgen_key_from_seed(uint32_t seed) {
+ CraftaxThreefryKey key = craftax_prng_key(seed);
+ CraftaxThreefryKey carry;
+ CraftaxThreefryKey reset_key;
+ craftax_threefry_split(key, &carry, &reset_key);
+
+ CraftaxThreefryKey reset_carry;
+ CraftaxThreefryKey world_key;
+ craftax_threefry_split(reset_key, &reset_carry, &world_key);
+ return world_key;
+}
+
+static inline CraftaxThreefryKey craftax_overworld_rng_from_seed(uint32_t seed) {
+ CraftaxThreefryKey world_key = craftax_worldgen_key_from_seed(seed);
+ CraftaxThreefryKey world_keys[7];
+ craftax_threefry_split_n(world_key, world_keys, 7);
+ return world_keys[1];
+}
+
+static inline uint32_t craftax_randint_u32_at(
+ CraftaxThreefryKey key,
+ uint64_t index,
+ uint32_t minval,
+ uint32_t maxval
+) {
+ uint32_t span = maxval > minval ? maxval - minval : 1u;
+ // Fast path for power-of-2 spans: just mask
+ if ((span & (span - 1)) == 0) {
+ uint32_t bits = craftax_threefry_uniform_u32_at(key, index);
+ return minval + (bits & (span - 1));
+ }
+ // General path: use top-32 of hash, scale to span
+ uint64_t h = craftax_fast_hash64(key, index);
+ return minval + (uint32_t)(((h >> 32) * (uint64_t)span) >> 32);
+}
+
+static inline int32_t craftax_randint_i32_at(
+ CraftaxThreefryKey key,
+ uint64_t index,
+ int32_t minval,
+ int32_t maxval
+) {
+ return (int32_t)craftax_randint_u32_at(
+ key,
+ index,
+ (uint32_t)minval,
+ (uint32_t)maxval
+ );
+}
+
+static inline int craftax_choice_bool_flat(
+ CraftaxThreefryKey key,
+ const bool* valid,
+ int count
+) {
+ int valid_count = 0;
+ int last_valid = 0;
+ for (int i = 0; i < count; i++) {
+ if (valid[i]) {
+ valid_count++;
+ last_valid = i;
+ }
+ }
+ if (valid_count == 0) {
+ return 0;
+ }
+
+ float draw = (float)valid_count * (1.0f - craftax_threefry_uniform_f32(key));
+ float cumulative = 0.0f;
+ for (int i = 0; i < count; i++) {
+ if (valid[i]) {
+ cumulative += 1.0f;
+ }
+ if (cumulative >= draw) {
+ return i;
+ }
+ }
+ return last_valid;
+}
+
+static inline float craftax_torch_light_value(int row, int col, float default_light) {
+ float dr = (float)(row - 4);
+ float dc = (float)(col - 4);
+ float distance = sqrtf(dr * dr + dc * dc);
+ float torch = craftax_wg_clampf(1.0f - distance / 5.0f, 0.0f, 1.0f);
+ return torch * (1.0f - default_light) + default_light;
+}
+
+static inline void craftax_apply_ladder_light(
+ uint8_t light_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ const int32_t ladder_up[2],
+ float default_light
+) {
+ int start_row = ladder_up[0] - 4;
+ int start_col = ladder_up[1] - 4;
+ if (start_row < 0) {
+ start_row += CRAFTAX_WG_MAP_SIZE;
+ }
+ if (start_col < 0) {
+ start_col += CRAFTAX_WG_MAP_SIZE;
+ }
+ start_row = craftax_wg_clampi(start_row, 0, CRAFTAX_WG_MAP_SIZE - 9);
+ start_col = craftax_wg_clampi(start_col, 0, CRAFTAX_WG_MAP_SIZE - 9);
+ for (int row = 0; row < 9; row++) {
+ for (int col = 0; col < 9; col++) {
+ light_map[start_row + row][start_col + col] =
+ (uint8_t)(craftax_torch_light_value(row, col, default_light) * 255.0f);
+ }
+ }
+}
+
+static inline void craftax_add_lava_light(
+ uint8_t light_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ const bool lava_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ bool lava_emits_light
+) {
+ if (!lava_emits_light) {
+ return;
+ }
+
+ static const float kernel[3][3] = {
+ {0.2f, 0.7f, 0.2f},
+ {0.7f, 1.0f, 0.7f},
+ {0.2f, 0.7f, 0.2f},
+ };
+
+ for (int row = 0; row < CRAFTAX_WG_MAP_SIZE; row++) {
+ for (int col = 0; col < CRAFTAX_WG_MAP_SIZE; col++) {
+ float add = 0.0f;
+ for (int kr = 0; kr < 3; kr++) {
+ int src_row = row + kr - 1;
+ if (src_row < 0 || src_row >= CRAFTAX_WG_MAP_SIZE) {
+ continue;
+ }
+ for (int kc = 0; kc < 3; kc++) {
+ int src_col = col + kc - 1;
+ if (src_col < 0 || src_col >= CRAFTAX_WG_MAP_SIZE) {
+ continue;
+ }
+ add += lava_map[src_row][src_col] ? kernel[kr][kc] : 0.0f;
+ }
+ }
+ float new_light = craftax_wg_clampf(light_map[row][col] / 255.0f + add, 0.0f, 1.0f);
+ light_map[row][col] = (uint8_t)(new_light * 255.0f);
+ }
+ }
+}
+
+static inline int craftax_smooth_config_index_for_floor(int floor_idx) {
+ switch (floor_idx) {
+ case 0:
+ return 0;
+ case 2:
+ return 1;
+ case 5:
+ return 2;
+ case 6:
+ return 3;
+ case 7:
+ return 4;
+ case 8:
+ return 5;
+ default:
+ return -1;
+ }
+}
+
+static inline int craftax_dungeon_config_index_for_floor(int floor_idx) {
+ switch (floor_idx) {
+ case 1:
+ return 0;
+ case 3:
+ return 1;
+ case 4:
+ return 2;
+ default:
+ return -1;
+ }
+}
+
+static inline void craftax_generate_smoothworld_config(
+ CraftaxThreefryKey rng,
+ int config_idx,
+ uint8_t map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ uint8_t item_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ uint8_t light_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ int32_t ladder_down[2],
+ int32_t ladder_up[2]
+) {
+ const CraftaxSmoothGenConfig* config = &CRAFTAX_SMOOTHGEN_CONFIGS[config_idx];
+ const int size = CRAFTAX_WG_MAP_SIZE;
+ const int player_row = CRAFTAX_WG_MAP_SIZE / 2;
+ const int player_col = CRAFTAX_WG_MAP_SIZE / 2;
+ const size_t cells = CRAFTAX_WG_MAP_CELLS;
+
+ CraftaxThreefryKey subkey;
+ float water[CRAFTAX_WG_MAP_CELLS];
+ float mountain[CRAFTAX_WG_MAP_CELLS];
+ float path_x[CRAFTAX_WG_MAP_CELLS];
+ float tree_noise[CRAFTAX_WG_MAP_CELLS];
+ bool lava_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE];
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ craftax_generate_fractal_noise_2d(subkey, size, size, 3, 3, 1, 0.5f, 2, NULL, water);
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ (void)subkey;
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ craftax_generate_fractal_noise_2d(subkey, size, size, 3, 3, 1, 0.5f, 2, NULL, mountain);
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ craftax_generate_fractal_noise_2d(subkey, size, size, 6, 24, 1, 0.5f, 2, NULL, path_x);
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ (void)subkey;
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ CraftaxThreefryKey tree_uniform_key = rng;
+ craftax_generate_fractal_noise_2d(subkey, size, size, 12, 12, 1, 0.5f, 2, NULL, tree_noise);
+
+ for (int row = 0; row < size; row++) {
+ int dr = row > player_row ? row - player_row : player_row - row;
+ for (int col = 0; col < size; col++) {
+ int dc = col > player_col ? col - player_col : player_col - col;
+ float distance = sqrtf((float)(dr * dr + dc * dc));
+ float proximity_water = craftax_wg_clampf(
+ distance / config->player_proximity_map_water_strength,
+ 0.0f,
+ config->player_proximity_map_water_max
+ );
+ float proximity_mountain = craftax_wg_clampf(
+ distance / config->player_proximity_map_mountain_strength,
+ 0.0f,
+ config->player_proximity_map_mountain_max
+ );
+ size_t idx = craftax_wg_index(row, col);
+
+ water[idx] = water[idx] + proximity_water - 1.0f;
+ int32_t block = water[idx] > config->water_threshold
+ ? config->sea_block
+ : config->default_block;
+ bool sand = water[idx] > config->sand_threshold && block != config->sea_block;
+ if (sand) {
+ block = config->coast_block;
+ }
+
+ mountain[idx] = mountain[idx] + 0.05f + proximity_mountain - 1.0f;
+ if (mountain[idx] > 0.7f) {
+ block = config->mountain_block;
+ }
+
+ bool path = mountain[idx] > 0.7f && path_x[idx] > 0.8f;
+ if (path) {
+ block = config->path_block;
+ }
+
+ float path_y = path_x[craftax_wg_index(col, row)];
+ path = mountain[idx] > 0.7f && path_y > 0.8f;
+ if (path) {
+ block = config->path_block;
+ }
+
+ bool cave = mountain[idx] > 0.85f && water[idx] > 0.4f;
+ if (cave) {
+ block = config->inner_mountain_block;
+ }
+
+ float tree_draw = craftax_threefry_uniform_f32_at(tree_uniform_key, idx);
+ bool tree = tree_noise[idx] > config->tree_threshold_perlin
+ && tree_draw > config->tree_threshold_uniform;
+ if (tree && block == config->tree_requirement_block) {
+ block = config->tree;
+ }
+
+ map[row][col] = (uint8_t)block;
+ item_map[row][col] = CRAFTAX_WG_ITEM_NONE;
+ light_map[row][col] = (uint8_t)(config->default_light * 255.0f);
+ }
+ }
+
+ CraftaxThreefryKey ore_rng;
+ craftax_threefry_split(rng, &rng, &ore_rng);
+ for (int ore_index = 0; ore_index < 5; ore_index++) {
+ CraftaxThreefryKey ore_key;
+ craftax_threefry_split(ore_rng, &ore_rng, &ore_key);
+ for (int row = 0; row < size; row++) {
+ for (int col = 0; col < size; col++) {
+ size_t idx = craftax_wg_index(row, col);
+ bool is_ore = map[row][col] == config->ore_requirement_blocks[ore_index]
+ && craftax_threefry_uniform_f32_at(ore_key, idx) < config->ore_chances[ore_index];
+ if (is_ore) {
+ map[row][col] = (uint8_t)config->ores[ore_index];
+ }
+ }
+ }
+ }
+
+ for (int row = 0; row < size; row++) {
+ for (int col = 0; col < size; col++) {
+ size_t idx = craftax_wg_index(row, col);
+ lava_map[row][col] = mountain[idx] > 0.85f && tree_noise[idx] > 0.7f;
+ if (lava_map[row][col]) {
+ map[row][col] = (uint8_t)config->lava;
+ }
+ }
+ }
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ bool valid_diamond[CRAFTAX_WG_MAP_CELLS];
+ for (int row = 0; row < size; row++) {
+ for (int col = 0; col < size; col++) {
+ valid_diamond[craftax_wg_index(row, col)] = map[row][col] == CRAFTAX_WG_BLOCK_STONE;
+ }
+ }
+ int diamond_index = craftax_choice_bool_flat(subkey, valid_diamond, (int)cells);
+ map[diamond_index / size][diamond_index % size] = (uint8_t)CRAFTAX_WG_BLOCK_STONE;
+
+ map[player_row][player_col] = (uint8_t)config->player_spawn;
+
+ bool valid_ladder[CRAFTAX_WG_MAP_CELLS];
+ for (int row = 0; row < size; row++) {
+ for (int col = 0; col < size; col++) {
+ valid_ladder[craftax_wg_index(row, col)] = map[row][col] == config->valid_ladder;
+ }
+ }
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ int ladder_down_index = craftax_choice_bool_flat(subkey, valid_ladder, (int)cells);
+ ladder_down[0] = ladder_down_index / size;
+ ladder_down[1] = ladder_down_index % size;
+ if (config->ladder_down) {
+ item_map[ladder_down[0]][ladder_down[1]] = CRAFTAX_WG_ITEM_LADDER_DOWN;
+ }
+
+ craftax_threefry_split(rng, &rng, &subkey);
+ int ladder_up_index = craftax_choice_bool_flat(subkey, valid_ladder, (int)cells);
+ ladder_up[0] = ladder_up_index / size;
+ ladder_up[1] = ladder_up_index % size;
+
+ craftax_apply_ladder_light(light_map, ladder_up, config->default_light);
+ craftax_add_lava_light(light_map, lava_map, config->lava == CRAFTAX_WG_BLOCK_LAVA);
+
+ if (config->ladder_up) {
+ item_map[ladder_up[0]][ladder_up[1]] = CRAFTAX_WG_ITEM_LADDER_UP;
+ }
+}
+
+static inline void craftax_generate_smoothworld_floor(
+ CraftaxThreefryKey seed_key,
+ int floor_idx,
+ uint8_t map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ uint8_t item_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ uint8_t light_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ int32_t ladder_down[2],
+ int32_t ladder_up[2]
+) {
+ int config_idx = craftax_smooth_config_index_for_floor(floor_idx);
+ if (config_idx < 0) {
+ memset(map, 0, CRAFTAX_WG_MAP_CELLS * sizeof(uint8_t));
+ memset(item_map, 0, CRAFTAX_WG_MAP_CELLS * sizeof(uint8_t));
+ memset(light_map, 0, CRAFTAX_WG_MAP_CELLS * sizeof(uint8_t));
+ ladder_down[0] = 0;
+ ladder_down[1] = 0;
+ ladder_up[0] = 0;
+ ladder_up[1] = 0;
+ return;
+ }
+ craftax_generate_smoothworld_config(
+ seed_key,
+ config_idx,
+ map,
+ item_map,
+ light_map,
+ ladder_down,
+ ladder_up
+ );
+}
+
+static inline void craftax_generate_dungeon_config(
+ CraftaxThreefryKey rng,
+ int config_idx,
+ uint8_t map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ uint8_t item_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ uint8_t light_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ int32_t ladder_down[2],
+ int32_t ladder_up[2]
+) {
+ const CraftaxDungeonConfig* config = &CRAFTAX_DUNGEON_CONFIGS[config_idx];
+ const int chunk_size = 16;
+ const int world_chunk_height = CRAFTAX_WG_MAP_SIZE / chunk_size;
+ const int num_rooms = 8;
+ const int min_room_size = 5;
+ const int max_room_size = 10;
+ const int padded_size = CRAFTAX_WG_MAP_SIZE + 2 * max_room_size;
+
+ uint8_t padded_map[68][68];
+ uint8_t padded_item_map[68][68];
+ bool room_occupancy_chunks[9];
+ int32_t room_sizes[8][2];
+ int32_t room_positions[8][2];
+
+ for (int row = 0; row < padded_size; row++) {
+ for (int col = 0; col < padded_size; col++) {
+ bool inner = row >= max_room_size
+ && row < max_room_size + CRAFTAX_WG_MAP_SIZE
+ && col >= max_room_size
+ && col < max_room_size + CRAFTAX_WG_MAP_SIZE;
+ padded_map[row][col] = inner ? CRAFTAX_WG_BLOCK_WALL : 0;
+ padded_item_map[row][col] = CRAFTAX_WG_ITEM_NONE;
+ }
+ }
+ for (int i = 0; i < 9; i++) {
+ room_occupancy_chunks[i] = true;
+ }
+
+ CraftaxThreefryKey room_scan_ignored_key;
+ CraftaxThreefryKey room_size_key;
+ craftax_threefry_split3(rng, &rng, &room_scan_ignored_key, &room_size_key);
+ (void)room_scan_ignored_key;
+ for (int room = 0; room < num_rooms; room++) {
+ room_sizes[room][0] = craftax_randint_i32_at(room_size_key, (uint64_t)room * 2u, min_room_size, max_room_size);
+ room_sizes[room][1] = craftax_randint_i32_at(room_size_key, (uint64_t)room * 2u + 1u, min_room_size, max_room_size);
+ }
+
+ CraftaxThreefryKey room_rng;
+ craftax_threefry_split(rng, &rng, &room_rng);
+
+ for (int room_index = 0; room_index < num_rooms; room_index++) {
+ CraftaxThreefryKey choice_key;
+ craftax_threefry_split(room_rng, &room_rng, &choice_key);
+ int room_chunk = craftax_choice_bool_flat(choice_key, room_occupancy_chunks, 9);
+ room_occupancy_chunks[room_chunk] = false;
+
+ int room_row = (room_chunk % world_chunk_height) * chunk_size + max_room_size;
+ int room_col = (room_chunk / world_chunk_height) * chunk_size + max_room_size;
+ CraftaxThreefryKey position_key;
+ craftax_threefry_split(room_rng, &room_rng, &position_key);
+ room_row += craftax_randint_i32_at(position_key, 0, 0, chunk_size - min_room_size);
+ room_col += craftax_randint_i32_at(position_key, 1, 0, chunk_size - min_room_size);
+ room_positions[room_index][0] = room_row;
+ room_positions[room_index][1] = room_col;
+
+ for (int row = 0; row < max_room_size; row++) {
+ for (int col = 0; col < max_room_size; col++) {
+ if (row < room_sizes[room_index][0] && col < room_sizes[room_index][1]) {
+ padded_map[room_row + row][room_col + col] = CRAFTAX_WG_BLOCK_PATH;
+ }
+ }
+ }
+
+ padded_item_map[room_row][room_col] = CRAFTAX_WG_ITEM_TORCH;
+ padded_item_map[room_row + room_sizes[room_index][0] - 1][room_col] = CRAFTAX_WG_ITEM_TORCH;
+ padded_item_map[room_row][room_col + room_sizes[room_index][1] - 1] = CRAFTAX_WG_ITEM_TORCH;
+ padded_item_map[room_row + room_sizes[room_index][0] - 1][room_col + room_sizes[room_index][1] - 1] = CRAFTAX_WG_ITEM_TORCH;
+
+ CraftaxThreefryKey chest_key;
+ craftax_threefry_split(room_rng, &room_rng, &chest_key);
+ int chest_row = craftax_randint_i32_at(chest_key, 0, 1, room_sizes[room_index][0] - 1);
+ int chest_col = craftax_randint_i32_at(chest_key, 1, 1, room_sizes[room_index][1] - 1);
+ padded_map[room_row + chest_row][room_col + chest_col] = CRAFTAX_WG_BLOCK_CHEST;
+
+ CraftaxThreefryKey fountain_key;
+ CraftaxThreefryKey fountain_uniform_key;
+ craftax_threefry_split3(room_rng, &room_rng, &fountain_key, &fountain_uniform_key);
+ int fountain_row = craftax_randint_i32_at(fountain_key, 0, 1, room_sizes[room_index][0] - 1);
+ int fountain_col = craftax_randint_i32_at(fountain_key, 1, 1, room_sizes[room_index][1] - 1);
+ bool room_has_fountain = craftax_threefry_uniform_f32(fountain_uniform_key) > 0.5f;
+ if (room_has_fountain) {
+ padded_map[room_row + fountain_row][room_col + fountain_col] = config->fountain_block;
+ }
+ }
+
+ CraftaxThreefryKey path_rng;
+ craftax_threefry_split(rng, &rng, &path_rng);
+ bool included_rooms_mask[8] = {false, false, false, false, false, false, false, true};
+
+ for (int path_index = 0; path_index < num_rooms; path_index++) {
+ int source_row = room_positions[path_index][0];
+ int source_col = room_positions[path_index][1];
+
+ CraftaxThreefryKey sink_key;
+ craftax_threefry_split(path_rng, &path_rng, &sink_key);
+ int sink_index = craftax_choice_bool_flat(sink_key, included_rooms_mask, num_rooms);
+ int sink_row = room_positions[sink_index][0];
+ int sink_col = room_positions[sink_index][1];
+
+ int horizontal_distance = sink_col - source_col;
+ int horizontal_sign = (horizontal_distance > 0) - (horizontal_distance < 0);
+ if (horizontal_sign != 0) {
+ int abs_distance = horizontal_distance > 0 ? horizontal_distance : -horizontal_distance;
+ for (int col = 0; col < padded_size; col++) {
+ int path_index_col = (col - source_col) * horizontal_sign;
+ bool horizontal_mask = path_index_col >= 0
+ && path_index_col <= abs_distance
+ && padded_map[source_row][col] == CRAFTAX_WG_BLOCK_WALL;
+ if (horizontal_mask) {
+ padded_map[source_row][col] = CRAFTAX_WG_BLOCK_PATH;
+ }
+ }
+ }
+
+ int vertical_distance = sink_row - source_row;
+ int vertical_sign = (vertical_distance > 0) - (vertical_distance < 0);
+ if (vertical_sign != 0) {
+ int abs_distance = vertical_distance > 0 ? vertical_distance : -vertical_distance;
+ for (int row = 0; row < padded_size; row++) {
+ int path_index_row = (row - source_row) * vertical_sign;
+ bool vertical_mask = path_index_row >= 0
+ && path_index_row <= abs_distance
+ && padded_map[row][sink_col] == CRAFTAX_WG_BLOCK_WALL;
+ if (vertical_mask) {
+ padded_map[row][sink_col] = CRAFTAX_WG_BLOCK_PATH;
+ }
+ }
+ }
+
+ CraftaxThreefryKey unused_left;
+ CraftaxThreefryKey next_path_rng;
+ craftax_threefry_split(path_rng, &unused_left, &next_path_rng);
+ path_rng = next_path_rng;
+ included_rooms_mask[path_index] = true;
+ }
+
+ int special_row = room_positions[0][0] + 2;
+ int special_col = room_positions[0][1] + 2;
+ padded_map[special_row][special_col] = config->special_block;
+
+ for (int row = 0; row < CRAFTAX_WG_MAP_SIZE; row++) {
+ for (int col = 0; col < CRAFTAX_WG_MAP_SIZE; col++) {
+ map[row][col] = padded_map[row + max_room_size][col + max_room_size];
+ item_map[row][col] = padded_item_map[row + max_room_size][col + max_room_size];
+ }
+ }
+
+ bool adjacent_path[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE];
+ for (int row = 0; row < CRAFTAX_WG_MAP_SIZE; row++) {
+ for (int col = 0; col < CRAFTAX_WG_MAP_SIZE; col++) {
+ bool adjacent = map[row][col] != CRAFTAX_WG_BLOCK_WALL;
+ adjacent = adjacent || (row > 0 && map[row - 1][col] != CRAFTAX_WG_BLOCK_WALL);
+ adjacent = adjacent || (row + 1 < CRAFTAX_WG_MAP_SIZE && map[row + 1][col] != CRAFTAX_WG_BLOCK_WALL);
+ adjacent = adjacent || (col > 0 && map[row][col - 1] != CRAFTAX_WG_BLOCK_WALL);
+ adjacent = adjacent || (col + 1 < CRAFTAX_WG_MAP_SIZE && map[row][col + 1] != CRAFTAX_WG_BLOCK_WALL);
+ adjacent_path[row][col] = adjacent;
+ }
+ }
+
+ CraftaxThreefryKey rare_key;
+ craftax_threefry_split(rng, &rng, &rare_key);
+ for (int row = 0; row < CRAFTAX_WG_MAP_SIZE; row++) {
+ for (int col = 0; col < CRAFTAX_WG_MAP_SIZE; col++) {
+ size_t idx = craftax_wg_index(row, col);
+ bool rare = (1.0f - craftax_threefry_uniform_f32_at(rare_key, idx)) > 0.9f;
+ int32_t wall_map = rare ? CRAFTAX_WG_BLOCK_WALL_MOSS : CRAFTAX_WG_BLOCK_WALL;
+ bool rare_path = rare && map[row][col] == CRAFTAX_WG_BLOCK_PATH && item_map[row][col] == CRAFTAX_WG_ITEM_NONE;
+ int32_t path_map = rare_path ? config->rare_path_replacement_block : map[row][col];
+ bool is_wall_map = map[row][col] == CRAFTAX_WG_BLOCK_WALL && adjacent_path[row][col];
+ bool is_darkness_map = !adjacent_path[row][col];
+
+ if (is_darkness_map) {
+ map[row][col] = CRAFTAX_WG_BLOCK_DARKNESS;
+ } else if (is_wall_map) {
+ map[row][col] = wall_map;
+ } else {
+ map[row][col] = path_map;
+ }
+ light_map[row][col] = 255;
+ }
+ }
+
+ bool valid_ladder[CRAFTAX_WG_MAP_CELLS];
+ for (int row = 0; row < CRAFTAX_WG_MAP_SIZE; row++) {
+ for (int col = 0; col < CRAFTAX_WG_MAP_SIZE; col++) {
+ valid_ladder[craftax_wg_index(row, col)] = map[row][col] == CRAFTAX_WG_BLOCK_PATH;
+ }
+ }
+
+ CraftaxThreefryKey ladder_down_key;
+ craftax_threefry_split(rng, &rng, &ladder_down_key);
+ int ladder_down_index = craftax_choice_bool_flat(ladder_down_key, valid_ladder, CRAFTAX_WG_MAP_CELLS);
+ ladder_down[0] = ladder_down_index / CRAFTAX_WG_MAP_SIZE;
+ ladder_down[1] = ladder_down_index % CRAFTAX_WG_MAP_SIZE;
+ item_map[ladder_down[0]][ladder_down[1]] = CRAFTAX_WG_ITEM_LADDER_DOWN;
+
+ CraftaxThreefryKey ladder_up_key;
+ craftax_threefry_split(rng, &rng, &ladder_up_key);
+ int ladder_up_index = craftax_choice_bool_flat(ladder_up_key, valid_ladder, CRAFTAX_WG_MAP_CELLS);
+ ladder_up[0] = ladder_up_index / CRAFTAX_WG_MAP_SIZE;
+ ladder_up[1] = ladder_up_index % CRAFTAX_WG_MAP_SIZE;
+ item_map[ladder_up[0]][ladder_up[1]] = CRAFTAX_WG_ITEM_LADDER_UP;
+}
+
+static inline void craftax_generate_dungeon_floor(
+ CraftaxThreefryKey seed_key,
+ int floor_idx,
+ uint8_t map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ uint8_t item_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ uint8_t light_map[CRAFTAX_WG_MAP_SIZE][CRAFTAX_WG_MAP_SIZE],
+ int32_t ladder_down[2],
+ int32_t ladder_up[2]
+) {
+ int config_idx = craftax_dungeon_config_index_for_floor(floor_idx);
+ if (config_idx < 0) {
+ memset(map, 0, CRAFTAX_WG_MAP_CELLS * sizeof(uint8_t));
+ memset(item_map, 0, CRAFTAX_WG_MAP_CELLS * sizeof(uint8_t));
+ memset(light_map, 0, CRAFTAX_WG_MAP_CELLS * sizeof(uint8_t));
+ ladder_down[0] = 0;
+ ladder_down[1] = 0;
+ ladder_up[0] = 0;
+ ladder_up[1] = 0;
+ return;
+ }
+ craftax_generate_dungeon_config(
+ seed_key,
+ config_idx,
+ map,
+ item_map,
+ light_map,
+ ladder_down,
+ ladder_up
+ );
+}
+
+static inline void craftax_permutation_6(CraftaxThreefryKey key, int32_t out[6]) {
+ CraftaxThreefryKey carry;
+ CraftaxThreefryKey sort_key;
+ craftax_threefry_split(key, &carry, &sort_key);
+ (void)carry;
+
+ uint32_t keys[6];
+ for (int i = 0; i < 6; i++) {
+ keys[i] = craftax_threefry_uniform_u32_at(sort_key, (uint64_t)i);
+ out[i] = i;
+ }
+
+ for (int i = 1; i < 6; i++) {
+ uint32_t key_value = keys[i];
+ int32_t value = out[i];
+ int j = i - 1;
+ while (j >= 0 && keys[j] > key_value) {
+ keys[j + 1] = keys[j];
+ out[j + 1] = out[j];
+ j--;
+ }
+ keys[j + 1] = key_value;
+ out[j + 1] = value;
+ }
+}
+
+static inline float craftax_calculate_initial_light_level(void) {
+ float progress = 0.3f;
+ float c = cosf(CRAFTAX_WG_PI * progress);
+ return 1.0f - powf(fabsf(c), 3.0f);
+}
+
+static inline void craftax_init_empty_mobs3(CraftaxWGMobs3* mobs) {
+ for (int level = 0; level < CRAFTAX_WG_NUM_LEVELS; level++) {
+ for (int mob = 0; mob < 3; mob++) {
+ mobs->health[level][mob] = 1.0f;
+ }
+ }
+}
+
+static inline void craftax_init_empty_mobs2(CraftaxWGMobs2* mobs) {
+ for (int level = 0; level < CRAFTAX_WG_NUM_LEVELS; level++) {
+ for (int mob = 0; mob < 2; mob++) {
+ mobs->health[level][mob] = 1.0f;
+ }
+ }
+}
+
+static inline void craftax_generate_world_from_key(
+ CraftaxThreefryKey rng,
+ CraftaxWorldState* out
+) {
+ memset(out, 0, sizeof(*out));
+
+ CraftaxThreefryKey smooth_split[7];
+ craftax_threefry_split_n(rng, smooth_split, 7);
+ rng = smooth_split[0];
+
+ static const int smooth_floor_order[6] = {0, 2, 5, 6, 7, 8};
+ for (int i = 0; i < 6; i++) {
+ int level = smooth_floor_order[i];
+ craftax_generate_smoothworld_config(
+ smooth_split[i + 1],
+ i,
+ out->map[level],
+ out->item_map[level],
+ out->light_map[level],
+ out->down_ladders[level],
+ out->up_ladders[level]
+ );
+ }
+
+ CraftaxThreefryKey dungeon_split[4];
+ craftax_threefry_split_n(rng, dungeon_split, 4);
+ rng = dungeon_split[0];
+
+ static const int dungeon_floor_order[3] = {1, 3, 4};
+ for (int i = 0; i < 3; i++) {
+ int level = dungeon_floor_order[i];
+ craftax_generate_dungeon_config(
+ dungeon_split[i + 1],
+ i,
+ out->map[level],
+ out->item_map[level],
+ out->light_map[level],
+ out->down_ladders[level],
+ out->up_ladders[level]
+ );
+ }
+
+ craftax_init_empty_mobs3(&out->melee_mobs);
+ craftax_init_empty_mobs3(&out->passive_mobs);
+ craftax_init_empty_mobs2(&out->ranged_mobs);
+ craftax_init_empty_mobs3(&out->mob_projectiles);
+ craftax_init_empty_mobs3(&out->player_projectiles);
+ for (int level = 0; level < CRAFTAX_WG_NUM_LEVELS; level++) {
+ for (int projectile = 0; projectile < CRAFTAX_WG_MAX_MOB_PROJECTILES; projectile++) {
+ out->mob_projectile_directions[level][projectile][0] = 1;
+ out->mob_projectile_directions[level][projectile][1] = 1;
+ }
+ for (int projectile = 0; projectile < CRAFTAX_WG_MAX_PLAYER_PROJECTILES; projectile++) {
+ out->player_projectile_directions[level][projectile][0] = 1;
+ out->player_projectile_directions[level][projectile][1] = 1;
+ }
+ }
+
+ CraftaxThreefryKey potion_key;
+ craftax_threefry_split(rng, &rng, &potion_key);
+ craftax_permutation_6(potion_key, out->potion_mapping);
+
+ CraftaxThreefryKey state_key;
+ craftax_threefry_split(rng, &rng, &state_key);
+ (void)rng;
+ out->state_rng[0] = state_key.word[0];
+ out->state_rng[1] = state_key.word[1];
+
+ out->monsters_killed[0] = 10;
+ out->player_position[0] = CRAFTAX_WG_MAP_SIZE / 2;
+ out->player_position[1] = CRAFTAX_WG_MAP_SIZE / 2;
+ out->player_level = 0;
+ out->player_direction = CRAFTAX_WG_ACTION_UP;
+ out->player_health = 9.0f;
+ out->player_food = 9;
+ out->player_drink = 9;
+ out->player_energy = 9;
+ out->player_mana = 9;
+ out->player_dexterity = 1;
+ out->player_strength = 1;
+ out->player_intelligence = 1;
+ out->boss_timesteps_to_spawn_this_round = CRAFTAX_WG_BOSS_FIGHT_SPAWN_TURNS;
+ out->light_level = craftax_calculate_initial_light_level();
+}
+
+static inline void craftax_generate_world_from_seed(
+ uint32_t seed,
+ CraftaxWorldState* out
+) {
+ craftax_generate_world_from_key(craftax_worldgen_key_from_seed(seed), out);
+}
+
+static inline void craftax_generate_overworld_from_rng(
+ CraftaxThreefryKey rng,
+ CraftaxOverworldFloor* out
+) {
+ craftax_generate_smoothworld_config(
+ rng,
+ 0,
+ out->map,
+ out->item_map,
+ out->light_map,
+ out->ladder_down,
+ out->ladder_up
+ );
+}
+
+static inline void craftax_generate_overworld_from_seed(
+ uint32_t seed,
+ CraftaxOverworldFloor* out
+) {
+ craftax_generate_overworld_from_rng(craftax_overworld_rng_from_seed(seed), out);
+}
+
+static inline int craftax_wg_jax_index(int32_t index, int32_t size) {
+ if (index < 0) {
+ index += size;
+ }
+ if (index < 0) {
+ return 0;
+ }
+ if (index >= size) {
+ return size - 1;
+ }
+ return index;
+}
+
+static inline bool craftax_wg_scatter_index(
+ int32_t index,
+ int32_t size,
+ int* mapped_index
+) {
+ if (index < -size || index >= size) {
+ return false;
+ }
+ *mapped_index = index < 0 ? index + size : index;
+ return true;
+}
+
+static inline bool craftax_wg_is_boss_vulnerable(
+ const CraftaxWorldState* state
+) {
+ int level = craftax_wg_jax_index(state->player_level, CRAFTAX_WG_NUM_LEVELS);
+ bool has_melee = false;
+ bool has_ranged = false;
+ for (int i = 0; i < CRAFTAX_WG_MAX_MELEE_MOBS; i++) {
+ has_melee = has_melee || state->melee_mobs.mask[level][i];
+ }
+ for (int i = 0; i < CRAFTAX_WG_MAX_RANGED_MOBS; i++) {
+ has_ranged = has_ranged || state->ranged_mobs.mask[level][i];
+ }
+ return !has_melee
+ && !has_ranged
+ && state->boss_timesteps_to_spawn_this_round <= 0;
+}
+
+static inline void craftax_encode_mobs3_observation(
+ const CraftaxWorldState* state,
+ const CraftaxWGMobs3* mobs,
+ int mob_class_index,
+ int channels,
+ int mob_channels_offset,
+ float* obs
+) {
+ int level = craftax_wg_jax_index(state->player_level, CRAFTAX_WG_NUM_LEVELS);
+ for (int i = 0; i < 3; i++) {
+ int local_row = mobs->position[level][i][0]
+ - state->player_position[0]
+ + CRAFTAX_WG_OBS_ROWS / 2;
+ int local_col = mobs->position[level][i][1]
+ - state->player_position[1]
+ + CRAFTAX_WG_OBS_COLS / 2;
+ int type_id = mobs->type_id[level][i];
+ int scatter_row;
+ int scatter_col;
+ if (!craftax_wg_scatter_index(
+ local_row,
+ CRAFTAX_WG_OBS_ROWS,
+ &scatter_row
+ )
+ || !craftax_wg_scatter_index(
+ local_col,
+ CRAFTAX_WG_OBS_COLS,
+ &scatter_col
+ )
+ || type_id < 0
+ || type_id >= CRAFTAX_WG_NUM_MOB_TYPES) {
+ continue;
+ }
+
+ bool on_screen = local_row >= 0
+ && local_row < CRAFTAX_WG_OBS_ROWS
+ && local_col >= 0
+ && local_col < CRAFTAX_WG_OBS_COLS;
+ int world_row = mobs->position[level][i][0];
+ int world_col = mobs->position[level][i][1];
+ bool in_bounds = world_row >= 0
+ && world_row < CRAFTAX_WG_MAP_SIZE
+ && world_col >= 0
+ && world_col < CRAFTAX_WG_MAP_SIZE;
+ bool visible = in_bounds && state->light_map[level][world_row][world_col] > 12;
+ int obs_base = (scatter_row * CRAFTAX_WG_OBS_COLS + scatter_col) * channels;
+ int channel = mob_channels_offset
+ + mob_class_index * CRAFTAX_WG_NUM_MOB_TYPES
+ + type_id;
+ obs[obs_base + channel] =
+ mobs->mask[level][i] && on_screen && visible ? 1.0f : 0.0f;
+ }
+}
+
+static inline void craftax_encode_mobs2_observation(
+ const CraftaxWorldState* state,
+ const CraftaxWGMobs2* mobs,
+ int mob_class_index,
+ int channels,
+ int mob_channels_offset,
+ float* obs
+) {
+ int level = craftax_wg_jax_index(state->player_level, CRAFTAX_WG_NUM_LEVELS);
+ for (int i = 0; i < 2; i++) {
+ int local_row = mobs->position[level][i][0]
+ - state->player_position[0]
+ + CRAFTAX_WG_OBS_ROWS / 2;
+ int local_col = mobs->position[level][i][1]
+ - state->player_position[1]
+ + CRAFTAX_WG_OBS_COLS / 2;
+ int type_id = mobs->type_id[level][i];
+ int scatter_row;
+ int scatter_col;
+ if (!craftax_wg_scatter_index(
+ local_row,
+ CRAFTAX_WG_OBS_ROWS,
+ &scatter_row
+ )
+ || !craftax_wg_scatter_index(
+ local_col,
+ CRAFTAX_WG_OBS_COLS,
+ &scatter_col
+ )
+ || type_id < 0
+ || type_id >= CRAFTAX_WG_NUM_MOB_TYPES) {
+ continue;
+ }
+
+ bool on_screen = local_row >= 0
+ && local_row < CRAFTAX_WG_OBS_ROWS
+ && local_col >= 0
+ && local_col < CRAFTAX_WG_OBS_COLS;
+ int world_row = mobs->position[level][i][0];
+ int world_col = mobs->position[level][i][1];
+ bool in_bounds = world_row >= 0
+ && world_row < CRAFTAX_WG_MAP_SIZE
+ && world_col >= 0
+ && world_col < CRAFTAX_WG_MAP_SIZE;
+ bool visible = in_bounds && state->light_map[level][world_row][world_col] > 12;
+ int obs_base = (scatter_row * CRAFTAX_WG_OBS_COLS + scatter_col) * channels;
+ int channel = mob_channels_offset
+ + mob_class_index * CRAFTAX_WG_NUM_MOB_TYPES
+ + type_id;
+ obs[obs_base + channel] =
+ mobs->mask[level][i] && on_screen && visible ? 1.0f : 0.0f;
+ }
+}
+
+static inline void craftax_write_binary_bits(
+ float* obs,
+ int base,
+ int value,
+ int num_bits
+) {
+ if (num_bits == 6) {
+ memcpy(obs + base, CRAFTAX_WG_BLOCK_LUT[value], 6 * sizeof(float));
+ } else if (num_bits == 3) {
+ memcpy(obs + base, CRAFTAX_WG_ITEM_LUT[value], 3 * sizeof(float));
+ } else if (num_bits == 4) {
+ memcpy(obs + base, CRAFTAX_WG_MOB_LUT[value], 4 * sizeof(float));
+ } else {
+ for (int i = 0; i < num_bits; i++) {
+ obs[base + i] = (value & (1 << i)) ? 1.0f : 0.0f;
+ }
+ }
+}
+
+static inline void craftax_encode_mobs3_binary(
+ const CraftaxWorldState* state,
+ const CraftaxWGMobs3* mobs,
+ int mob_class_index,
+ int channels_per_cell,
+ int mob_bits_offset,
+ float* obs
+) {
+ int level = craftax_wg_jax_index(state->player_level, CRAFTAX_WG_NUM_LEVELS);
+ for (int i = 0; i < 3; i++) {
+ int type_id = mobs->type_id[level][i];
+ if (type_id < 0 || type_id >= CRAFTAX_WG_NUM_MOB_TYPES
+ || !mobs->mask[level][i]) {
+ continue;
+ }
+
+ int local_row = mobs->position[level][i][0]
+ - state->player_position[0]
+ + CRAFTAX_WG_OBS_ROWS / 2;
+ int local_col = mobs->position[level][i][1]
+ - state->player_position[1]
+ + CRAFTAX_WG_OBS_COLS / 2;
+ if (local_row < 0 || local_row >= CRAFTAX_WG_OBS_ROWS
+ || local_col < 0 || local_col >= CRAFTAX_WG_OBS_COLS) {
+ continue;
+ }
+
+ int world_row = mobs->position[level][i][0];
+ int world_col = mobs->position[level][i][1];
+ if (world_row < 0 || world_row >= CRAFTAX_WG_MAP_SIZE
+ || world_col < 0 || world_col >= CRAFTAX_WG_MAP_SIZE
+ || state->light_map[level][world_row][world_col] <= 12) {
+ continue;
+ }
+
+ int obs_base = (local_row * CRAFTAX_WG_OBS_COLS + local_col)
+ * channels_per_cell;
+ int class_offset = mob_bits_offset
+ + mob_class_index * CRAFTAX_WG_BINARY_MOB_BITS;
+ memcpy(obs + obs_base + class_offset,
+ CRAFTAX_WG_MOB_LUT[type_id + 1],
+ CRAFTAX_WG_BINARY_MOB_BITS * sizeof(float));
+ }
+}
+
+static inline void craftax_encode_mobs2_binary(
+ const CraftaxWorldState* state,
+ const CraftaxWGMobs2* mobs,
+ int mob_class_index,
+ int channels_per_cell,
+ int mob_bits_offset,
+ float* obs
+) {
+ int level = craftax_wg_jax_index(state->player_level, CRAFTAX_WG_NUM_LEVELS);
+ for (int i = 0; i < 2; i++) {
+ int type_id = mobs->type_id[level][i];
+ if (type_id < 0 || type_id >= CRAFTAX_WG_NUM_MOB_TYPES
+ || !mobs->mask[level][i]) {
+ continue;
+ }
+
+ int local_row = mobs->position[level][i][0]
+ - state->player_position[0]
+ + CRAFTAX_WG_OBS_ROWS / 2;
+ int local_col = mobs->position[level][i][1]
+ - state->player_position[1]
+ + CRAFTAX_WG_OBS_COLS / 2;
+ if (local_row < 0 || local_row >= CRAFTAX_WG_OBS_ROWS
+ || local_col < 0 || local_col >= CRAFTAX_WG_OBS_COLS) {
+ continue;
+ }
+
+ int world_row = mobs->position[level][i][0];
+ int world_col = mobs->position[level][i][1];
+ if (world_row < 0 || world_row >= CRAFTAX_WG_MAP_SIZE
+ || world_col < 0 || world_col >= CRAFTAX_WG_MAP_SIZE
+ || state->light_map[level][world_row][world_col] <= 12) {
+ continue;
+ }
+
+ int obs_base = (local_row * CRAFTAX_WG_OBS_COLS + local_col)
+ * channels_per_cell;
+ int class_offset = mob_bits_offset
+ + mob_class_index * CRAFTAX_WG_BINARY_MOB_BITS;
+ memcpy(obs + obs_base + class_offset,
+ CRAFTAX_WG_MOB_LUT[type_id + 1],
+ CRAFTAX_WG_BINARY_MOB_BITS * sizeof(float));
+ }
+}
+
+static inline void craftax_encode_map_base_observation(
+ const CraftaxWorldState* state,
+ float* obs
+) {
+ const int channels = CRAFTAX_WG_BINARY_CHANNELS_PER_CELL;
+ const int top = state->player_position[0] - CRAFTAX_WG_OBS_ROWS / 2;
+ const int left = state->player_position[1] - CRAFTAX_WG_OBS_COLS / 2;
+ const int level = state->player_level;
+ const float* empty_cell = CRAFTAX_WG_EMPTY_CELL_TEMPLATE;
+
+ for (int row = 0; row < CRAFTAX_WG_OBS_ROWS; row++) {
+ int world_row = top + row;
+ bool row_in_bounds = world_row >= 0 && world_row < CRAFTAX_WG_MAP_SIZE;
+ for (int col = 0; col < CRAFTAX_WG_OBS_COLS; col++) {
+ int world_col = left + col;
+ int obs_base = (row * CRAFTAX_WG_OBS_COLS + col) * channels;
+ const float* cell = empty_cell;
+
+ if (row_in_bounds && world_col >= 0 && world_col < CRAFTAX_WG_MAP_SIZE
+ && state->light_map[level][world_row][world_col] > 12) {
+ uint8_t block = state->map[level][world_row][world_col];
+ uint8_t item = state->item_map[level][world_row][world_col];
+ cell = CRAFTAX_WG_VISIBLE_CELL_TEMPLATE_LUT[block][item + 1];
+ }
+
+ memcpy(obs + obs_base, cell, CRAFTAX_WG_CELL_TEMPLATE_BYTES);
+ }
+ }
+}
+
+static inline void craftax_encode_packed_map_base_observation(
+ const CraftaxWorldState* state,
+ float* obs
+) {
+ const int channels = CRAFTAX_WG_PACKED_CHANNELS_PER_CELL;
+ const int top = state->player_position[0] - CRAFTAX_WG_OBS_ROWS / 2;
+ const int left = state->player_position[1] - CRAFTAX_WG_OBS_COLS / 2;
+ const int level = state->player_level;
+
+ memset(obs, 0, CRAFTAX_WG_PACKED_MAP_OBS_SIZE * sizeof(float));
+ for (int row = 0; row < CRAFTAX_WG_OBS_ROWS; row++) {
+ int world_row = top + row;
+ bool row_in_bounds = world_row >= 0 && world_row < CRAFTAX_WG_MAP_SIZE;
+ for (int col = 0; col < CRAFTAX_WG_OBS_COLS; col++) {
+ int world_col = left + col;
+ int obs_base = (row * CRAFTAX_WG_OBS_COLS + col) * channels;
+ if (row_in_bounds && world_col >= 0 && world_col < CRAFTAX_WG_MAP_SIZE
+ && state->light_map[level][world_row][world_col] > 12) {
+ obs[obs_base + 0] = (float)state->map[level][world_row][world_col];
+ obs[obs_base + 1] = (float)state->item_map[level][world_row][world_col] + 1.0f;
+ obs[obs_base + 2] = 1.0f;
+ }
+ }
+ }
+}
+
+static inline void craftax_clear_mob_channels_observation(float* obs) {
+ const int channels = CRAFTAX_WG_BINARY_CHANNELS_PER_CELL;
+ const int mob_bits_offset = CRAFTAX_WG_BINARY_BLOCK_BITS + CRAFTAX_WG_BINARY_ITEM_BITS;
+ const size_t mob_channel_bytes =
+ CRAFTAX_WG_NUM_MOB_CLASSES * CRAFTAX_WG_BINARY_MOB_BITS * sizeof(float);
+
+ for (int cell = 0; cell < CRAFTAX_WG_OBS_WINDOW_CELLS; cell++) {
+ memset(obs + cell * channels + mob_bits_offset, 0, mob_channel_bytes);
+ }
+}
+
+static inline void craftax_encode_mobs3_packed(
+ const CraftaxWorldState* state,
+ const CraftaxWGMobs3* mobs,
+ int mob_class_index,
+ float* obs
+) {
+ const int level = craftax_wg_jax_index(state->player_level, CRAFTAX_WG_NUM_LEVELS);
+ const int mob_slot_offset = 3 + mob_class_index;
+ for (int i = 0; i < 3; i++) {
+ int type_id = mobs->type_id[level][i];
+ if (type_id < 0 || type_id >= CRAFTAX_WG_NUM_MOB_TYPES
+ || !mobs->mask[level][i]) {
+ continue;
+ }
+
+ int local_row = mobs->position[level][i][0]
+ - state->player_position[0]
+ + CRAFTAX_WG_OBS_ROWS / 2;
+ int local_col = mobs->position[level][i][1]
+ - state->player_position[1]
+ + CRAFTAX_WG_OBS_COLS / 2;
+ if (local_row < 0 || local_row >= CRAFTAX_WG_OBS_ROWS
+ || local_col < 0 || local_col >= CRAFTAX_WG_OBS_COLS) {
+ continue;
+ }
+
+ int world_row = mobs->position[level][i][0];
+ int world_col = mobs->position[level][i][1];
+ if (world_row < 0 || world_row >= CRAFTAX_WG_MAP_SIZE
+ || world_col < 0 || world_col >= CRAFTAX_WG_MAP_SIZE
+ || state->light_map[level][world_row][world_col] <= 12) {
+ continue;
+ }
+
+ int obs_base = (local_row * CRAFTAX_WG_OBS_COLS + local_col)
+ * CRAFTAX_WG_PACKED_CHANNELS_PER_CELL;
+ obs[obs_base + mob_slot_offset] = (float)(type_id + 1);
+ }
+}
+
+static inline void craftax_encode_mobs2_packed(
+ const CraftaxWorldState* state,
+ const CraftaxWGMobs2* mobs,
+ int mob_class_index,
+ float* obs
+) {
+ const int level = craftax_wg_jax_index(state->player_level, CRAFTAX_WG_NUM_LEVELS);
+ const int mob_slot_offset = 3 + mob_class_index;
+ for (int i = 0; i < 2; i++) {
+ int type_id = mobs->type_id[level][i];
+ if (type_id < 0 || type_id >= CRAFTAX_WG_NUM_MOB_TYPES
+ || !mobs->mask[level][i]) {
+ continue;
+ }
+
+ int local_row = mobs->position[level][i][0]
+ - state->player_position[0]
+ + CRAFTAX_WG_OBS_ROWS / 2;
+ int local_col = mobs->position[level][i][1]
+ - state->player_position[1]
+ + CRAFTAX_WG_OBS_COLS / 2;
+ if (local_row < 0 || local_row >= CRAFTAX_WG_OBS_ROWS
+ || local_col < 0 || local_col >= CRAFTAX_WG_OBS_COLS) {
+ continue;
+ }
+
+ int world_row = mobs->position[level][i][0];
+ int world_col = mobs->position[level][i][1];
+ if (world_row < 0 || world_row >= CRAFTAX_WG_MAP_SIZE
+ || world_col < 0 || world_col >= CRAFTAX_WG_MAP_SIZE
+ || state->light_map[level][world_row][world_col] <= 12) {
+ continue;
+ }
+
+ int obs_base = (local_row * CRAFTAX_WG_OBS_COLS + local_col)
+ * CRAFTAX_WG_PACKED_CHANNELS_PER_CELL;
+ obs[obs_base + mob_slot_offset] = (float)(type_id + 1);
+ }
+}
+
+static inline void craftax_encode_packed_mobs_observation(
+ const CraftaxWorldState* state,
+ float* obs
+) {
+ craftax_encode_mobs3_packed(state, &state->melee_mobs, 0, obs);
+ craftax_encode_mobs3_packed(state, &state->passive_mobs, 1, obs);
+ craftax_encode_mobs2_packed(state, &state->ranged_mobs, 2, obs);
+ craftax_encode_mobs3_packed(state, &state->mob_projectiles, 3, obs);
+ craftax_encode_mobs3_packed(state, &state->player_projectiles, 4, obs);
+}
+
+static inline void craftax_encode_mobs_observation(
+ const CraftaxWorldState* state,
+ float* obs
+) {
+ const int channels = CRAFTAX_WG_BINARY_CHANNELS_PER_CELL;
+ const int mob_bits_offset = CRAFTAX_WG_BINARY_BLOCK_BITS + CRAFTAX_WG_BINARY_ITEM_BITS;
+
+ craftax_encode_mobs3_binary(
+ state,
+ &state->melee_mobs,
+ 0,
+ channels,
+ mob_bits_offset,
+ obs
+ );
+ craftax_encode_mobs3_binary(
+ state,
+ &state->passive_mobs,
+ 1,
+ channels,
+ mob_bits_offset,
+ obs
+ );
+ craftax_encode_mobs2_binary(
+ state,
+ &state->ranged_mobs,
+ 2,
+ channels,
+ mob_bits_offset,
+ obs
+ );
+ craftax_encode_mobs3_binary(
+ state,
+ &state->mob_projectiles,
+ 3,
+ channels,
+ mob_bits_offset,
+ obs
+ );
+ craftax_encode_mobs3_binary(
+ state,
+ &state->player_projectiles,
+ 4,
+ channels,
+ mob_bits_offset,
+ obs
+ );
+}
+
+static inline void craftax_encode_scalar_observation_tail_at(
+ const CraftaxWorldState* state,
+ float* obs,
+ int index
+) {
+ const int level = state->player_level;
+ obs[index++] = sqrtf((float)state->inventory.wood) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.stone) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.coal) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.iron) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.diamond) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.sapphire) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.ruby) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.sapling) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.torches) / 10.0f;
+ obs[index++] = sqrtf((float)state->inventory.arrows) / 10.0f;
+ obs[index++] = (float)state->inventory.books / 2.0f;
+ obs[index++] = (float)state->inventory.pickaxe / 4.0f;
+ obs[index++] = (float)state->inventory.sword / 4.0f;
+ obs[index++] = (float)state->sword_enchantment;
+ obs[index++] = (float)state->bow_enchantment;
+ obs[index++] = (float)state->inventory.bow;
+
+ for (int i = 0; i < 6; i++) {
+ obs[index++] = sqrtf((float)state->inventory.potions[i]) / 10.0f;
+ }
+
+ obs[index++] = state->player_health / 10.0f;
+ obs[index++] = (float)state->player_food / 10.0f;
+ obs[index++] = (float)state->player_drink / 10.0f;
+ obs[index++] = (float)state->player_energy / 10.0f;
+ obs[index++] = (float)state->player_mana / 10.0f;
+ obs[index++] = (float)state->player_xp / 10.0f;
+ obs[index++] = (float)state->player_dexterity / 10.0f;
+ obs[index++] = (float)state->player_strength / 10.0f;
+ obs[index++] = (float)state->player_intelligence / 10.0f;
+
+ int direction_index = state->player_direction - 1;
+ for (int i = 0; i < 4; i++) {
+ obs[index++] = i == direction_index ? 1.0f : 0.0f;
+ }
+
+ for (int i = 0; i < 4; i++) {
+ obs[index++] = (float)state->inventory.armour[i] / 2.0f;
+ }
+ for (int i = 0; i < 4; i++) {
+ obs[index++] = (float)state->armour_enchantments[i];
+ }
+
+ obs[index++] = state->light_level;
+ obs[index++] = state->is_sleeping ? 1.0f : 0.0f;
+ obs[index++] = state->is_resting ? 1.0f : 0.0f;
+ obs[index++] = state->learned_spells[0] ? 1.0f : 0.0f;
+ obs[index++] = state->learned_spells[1] ? 1.0f : 0.0f;
+ obs[index++] = (float)state->player_level / 10.0f;
+ obs[index++] = state->monsters_killed[level] >= CRAFTAX_WG_MONSTERS_KILLED_TO_CLEAR_LEVEL ? 1.0f : 0.0f;
+ obs[index++] = craftax_wg_is_boss_vulnerable(state) ? 1.0f : 0.0f;
+}
+
+static inline void craftax_encode_scalar_observation_tail(
+ const CraftaxWorldState* state,
+ float* obs
+) {
+ craftax_encode_scalar_observation_tail_at(state, obs, CRAFTAX_WG_BINARY_MAP_OBS_SIZE);
+}
+
+static inline void craftax_encode_reset_observation(
+ const CraftaxWorldState* state,
+ float* obs
+) {
+ craftax_encode_packed_map_base_observation(state, obs);
+ craftax_encode_packed_mobs_observation(state, obs);
+ craftax_encode_scalar_observation_tail_at(state, obs, CRAFTAX_WG_PACKED_MAP_OBS_SIZE);
+}
diff --git a/ocean/craftax_classic/binding.c b/ocean/craftax_classic/binding.c
new file mode 100644
index 0000000000..16a6270943
--- /dev/null
+++ b/ocean/craftax_classic/binding.c
@@ -0,0 +1,34 @@
+#include "craftax_classic.h"
+
+#define OBS_SIZE 1345
+#define NUM_ATNS 1
+#define ACT_SIZES {17}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env CraftaxClassic
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ // No per-env kwargs for Craftax-Classic: the 64x64 map, inventory sizes,
+ // mob caps, etc. are all compile-time constants.
+ c_init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+
+ static const char* ACH_NAMES[NUM_ACHIEVEMENTS] = {
+ "collect_wood", "place_table", "eat_cow", "collect_sapling",
+ "collect_drink", "make_wood_pick", "make_wood_sword","place_plant",
+ "defeat_zombie", "collect_stone", "place_stone", "eat_plant",
+ "defeat_skeleton","make_stone_pick","make_stone_sword","wake_up",
+ "place_furnace", "collect_coal", "collect_iron", "collect_diamond",
+ "make_iron_pick", "make_iron_sword",
+ };
+ for (int i = 0; i < NUM_ACHIEVEMENTS; i++) {
+ dict_set(out, ACH_NAMES[i], log->achievements[i]);
+ }
+}
diff --git a/ocean/craftax_classic/craftax_classic.h b/ocean/craftax_classic/craftax_classic.h
new file mode 100644
index 0000000000..375f0004fa
--- /dev/null
+++ b/ocean/craftax_classic/craftax_classic.h
@@ -0,0 +1,1163 @@
+// Craftax-Classic environment for PufferLib Ocean.
+//
+// Single-header per-env implementation. PufferLib's vec layer owns the
+// observation/action/reward/terminal buffers and parallelizes c_step
+// across env instances via OpenMP; this file never allocates its own
+// threads or batches.
+//
+// Game rules follow Matthews et al. 2024 "Craftax-Classic" (ICML 2024).
+// This port is derived from the CPU port at github.com/Infatoshi/craftax.c
+// (47.8M SPS standalone), restructured to match the Ocean conventions
+// used by breakout/drmario/etc.
+//
+// Observation: 1345 float32:
+// - 63 tiles (7x9 local view) x 21 channels (17 block one-hot + 4 mob) = 1323
+// - 12 inventory (0..9) / 10
+// - 4 intrinsics (health, food, drink, energy / 10)
+// - 4 direction one-hot
+// - 1 light level [0, 1]
+// - 1 is_sleeping {0, 1}
+// Matches the JAX/CUDA Craftax-Classic-Symbolic-v1 layout exactly.
+//
+// Action: 1 discrete in 0..16 (NOOP, 4 moves, DO, SLEEP,
+// 4 place, 3 make-pick, 3 make-sword).
+
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include "raylib.h"
+
+// ============================================================
+// Constants
+// ============================================================
+#define MAP_SIZE 64
+#define MAP_PACKED_ROW 32
+#define MAP_PACKED_SIZE (MAP_SIZE * MAP_PACKED_ROW)
+
+#define MAX_ZOMBIES 3
+#define MAX_COWS 3
+#define MAX_SKELETONS 2
+#define MAX_ARROWS 3
+#define MAX_PLANTS 10
+#define NUM_ACHIEVEMENTS 22
+#define NUM_ACTIONS 17
+#define NUM_BLOCK_TYPES 17
+#define OBS_DIM 1345
+#define NUM_INVENTORY 12
+#define MAX_TIMESTEPS 10000
+#define DAY_LENGTH 300
+#define MOB_DESPAWN_DIST 14
+
+// Block types
+#define BLK_INVALID 0
+#define BLK_OUT_OF_BOUNDS 1
+#define BLK_GRASS 2
+#define BLK_WATER 3
+#define BLK_STONE 4
+#define BLK_TREE 5
+#define BLK_WOOD 6
+#define BLK_PATH 7
+#define BLK_COAL 8
+#define BLK_IRON 9
+#define BLK_DIAMOND 10
+#define BLK_TABLE 11
+#define BLK_FURNACE 12
+#define BLK_SAND 13
+#define BLK_LAVA 14
+#define BLK_PLANT 15
+#define BLK_RIPE_PLANT 16
+
+// Actions
+#define ACT_NOOP 0
+#define ACT_LEFT 1
+#define ACT_RIGHT 2
+#define ACT_UP 3
+#define ACT_DOWN 4
+#define ACT_DO 5
+#define ACT_SLEEP 6
+#define ACT_PLACE_STONE 7
+#define ACT_PLACE_TABLE 8
+#define ACT_PLACE_FURNACE 9
+#define ACT_PLACE_PLANT 10
+#define ACT_MAKE_WOOD_PICK 11
+#define ACT_MAKE_STONE_PICK 12
+#define ACT_MAKE_IRON_PICK 13
+#define ACT_MAKE_WOOD_SWORD 14
+#define ACT_MAKE_STONE_SWORD 15
+#define ACT_MAKE_IRON_SWORD 16
+
+// Achievements (index in env->log.achievements[])
+#define ACH_COLLECT_WOOD 0
+#define ACH_PLACE_TABLE 1
+#define ACH_EAT_COW 2
+#define ACH_COLLECT_SAPLING 3
+#define ACH_COLLECT_DRINK 4
+#define ACH_MAKE_WOOD_PICK 5
+#define ACH_MAKE_WOOD_SWORD 6
+#define ACH_PLACE_PLANT 7
+#define ACH_DEFEAT_ZOMBIE 8
+#define ACH_COLLECT_STONE 9
+#define ACH_PLACE_STONE 10
+#define ACH_EAT_PLANT 11
+#define ACH_DEFEAT_SKELETON 12
+#define ACH_MAKE_STONE_PICK 13
+#define ACH_MAKE_STONE_SWORD 14
+#define ACH_WAKE_UP 15
+#define ACH_PLACE_FURNACE 16
+#define ACH_COLLECT_COAL 17
+#define ACH_COLLECT_IRON 18
+#define ACH_COLLECT_DIAMOND 19
+#define ACH_MAKE_IRON_PICK 20
+#define ACH_MAKE_IRON_SWORD 21
+
+static const int DIR_DR[5] = {0, 0, 0, -1, 1};
+static const int DIR_DC[5] = {0, -1, 1, 0, 0};
+
+// ============================================================
+// Tiny PCG-style RNG (single 64-bit state)
+// ============================================================
+static inline uint32_t cr_pcg(uint64_t* s) {
+ *s = *s * 6364136223846793005ULL + 1442695040888963407ULL;
+ uint32_t x = (uint32_t)(((*s >> 18u) ^ *s) >> 27u);
+ uint32_t rot = (uint32_t)(*s >> 59u);
+ return (x >> rot) | (x << ((-(int32_t)rot) & 31));
+}
+static inline float cr_rf(uint64_t* s) { return (cr_pcg(s) >> 8) * (1.0f / 16777216.0f); }
+static inline int cr_ri(uint64_t* s, int n) { return (int)(cr_pcg(s) % (uint32_t)n); }
+
+// ============================================================
+// PufferLib-required structs
+// ============================================================
+typedef struct Log {
+ float perf; // 0-1 normalized progress (achievements / 22)
+ float score; // sum of episode returns seen so far
+ float episode_return; // last episode return
+ float episode_length; // last episode length
+ float achievements[NUM_ACHIEVEMENTS];
+ float n; // required counter (last field)
+} Log;
+
+typedef struct Client {
+ int dummy; // handled by raylib globally; no per-env handle needed
+} Client;
+
+// ============================================================
+// Env struct
+// ============================================================
+typedef struct CraftaxClassic {
+ Client* client;
+ Log log;
+
+ float* observations; // (OBS_DIM,) fp32, PufferLib-owned
+ float* actions; // (1,) fp32
+ float* rewards; // (1,)
+ float* terminals; // (1,)
+
+ int num_agents; // = 1
+
+ unsigned int rng; // populated by default my_vec_init (env index)
+ uint64_t pcg; // actual RNG state (seeded from rng in my_init)
+
+ // Packed map (2 blocks/byte)
+ uint8_t map_packed[MAP_PACKED_SIZE];
+
+ // Per-type occupancy bitmaps: bit c of bits[r] = "mob-type at (r,c)"
+ uint64_t mob_bits[MAP_SIZE]; // zombie | cow | skel (used by has_mob_at / can_move_mob)
+ uint64_t zombie_bits[MAP_SIZE];
+ uint64_t cow_bits[MAP_SIZE];
+ uint64_t skel_bits[MAP_SIZE];
+ uint64_t arrow_bits[MAP_SIZE];
+
+ // Player
+ int16_t player_r, player_c;
+ int8_t player_dir;
+
+ // Intrinsics
+ int8_t health, food, drink, energy;
+ bool is_sleeping;
+ float recover, hunger, thirst, fatigue;
+
+ // Inventory (wood, stone, coal, iron, diamond, sapling,
+ // wpick, spick, ipick, wsword, ssword, isword)
+ int8_t inv[NUM_INVENTORY];
+
+ // Mobs
+ int16_t zombie_r[MAX_ZOMBIES], zombie_c[MAX_ZOMBIES];
+ int8_t zombie_hp[MAX_ZOMBIES], zombie_cd[MAX_ZOMBIES];
+ bool zombie_mask[MAX_ZOMBIES];
+
+ int16_t cow_r[MAX_COWS], cow_c[MAX_COWS];
+ int8_t cow_hp[MAX_COWS];
+ bool cow_mask[MAX_COWS];
+
+ int16_t skel_r[MAX_SKELETONS], skel_c[MAX_SKELETONS];
+ int8_t skel_hp[MAX_SKELETONS], skel_cd[MAX_SKELETONS];
+ bool skel_mask[MAX_SKELETONS];
+
+ int16_t arrow_r[MAX_ARROWS], arrow_c[MAX_ARROWS];
+ int8_t arrow_dr[MAX_ARROWS], arrow_dc[MAX_ARROWS];
+ bool arrow_mask[MAX_ARROWS];
+
+ int16_t plant_r[MAX_PLANTS], plant_c[MAX_PLANTS];
+ int16_t plant_age[MAX_PLANTS];
+ bool plant_mask[MAX_PLANTS];
+
+ float light_level;
+ bool achievements[NUM_ACHIEVEMENTS];
+ int32_t timestep;
+
+ // Episode stats (accumulated; flushed into env->log on terminal)
+ float episode_return_accum;
+ int32_t episode_length_accum;
+
+ // Scratch for per-step reward computation
+ int8_t old_health;
+ bool old_achievements[NUM_ACHIEVEMENTS];
+} CraftaxClassic;
+
+// ============================================================
+// Map accessors + small helpers
+// ============================================================
+static inline int8_t map_get(const CraftaxClassic* s, int r, int c) {
+ int idx = r * MAP_PACKED_ROW + (c >> 1);
+ uint8_t b = s->map_packed[idx];
+ return (c & 1) ? (int8_t)(b >> 4) : (int8_t)(b & 0x0F);
+}
+static inline void map_set(CraftaxClassic* s, int r, int c, int8_t v) {
+ int idx = r * MAP_PACKED_ROW + (c >> 1);
+ uint8_t b = s->map_packed[idx];
+ if (c & 1) s->map_packed[idx] = (b & 0x0F) | ((v & 0x0F) << 4);
+ else s->map_packed[idx] = (b & 0xF0) | (v & 0x0F);
+}
+static inline bool in_bounds(int r, int c) { return (unsigned)r < MAP_SIZE && (unsigned)c < MAP_SIZE; }
+static inline bool is_solid(int8_t b) {
+ return b == BLK_WATER || b == BLK_STONE || b == BLK_TREE ||
+ b == BLK_COAL || b == BLK_IRON || b == BLK_DIAMOND ||
+ b == BLK_TABLE || b == BLK_FURNACE ||
+ b == BLK_PLANT || b == BLK_RIPE_PLANT;
+}
+static inline int l1_dist(int r1, int c1, int r2, int c2) {
+ int dr = r1 - r2; if (dr < 0) dr = -dr;
+ int dc = c1 - c2; if (dc < 0) dc = -dc;
+ return dr + dc;
+}
+static inline int cr_clamp_i(int v, int lo, int hi){ return vhi?hi:v); }
+static inline int cr_min_i(int a,int b){return ab?a:b;}
+static inline float cr_min_f(float a,float b){return a0)-(v<0);}
+
+// Bitmap maintenance
+static inline void mb_set(uint64_t* bits, int r, int c) { bits[r] |= (1ULL << c); }
+static inline void mb_clear(uint64_t* bits, int r, int c) { bits[r] &= ~(1ULL << c); }
+static inline bool mb_get(const uint64_t* bits, int r, int c) { return (bits[r] >> c) & 1ULL; }
+
+static inline bool has_mob_at(const CraftaxClassic* s, int r, int c) {
+ if ((unsigned)r >= MAP_SIZE || (unsigned)c >= MAP_SIZE) return false;
+ return ((s->mob_bits[r] >> c) & 1ULL) != 0;
+}
+
+static bool is_near_block(const CraftaxClassic* s, int8_t blk) {
+ int pr = s->player_r, pc = s->player_c;
+ static const int dr8[8] = {0, 0, -1, 1, -1, -1, 1, 1};
+ static const int dc8[8] = {-1, 1, 0, 0, -1, 1, -1, 1};
+ for (int i = 0; i < 8; i++) {
+ int nr = pr + dr8[i], nc = pc + dc8[i];
+ if (in_bounds(nr, nc) && map_get(s, nr, nc) == blk) return true;
+ }
+ return false;
+}
+
+static inline int get_damage(const CraftaxClassic* s) {
+ if (s->inv[11] > 0) return 5;
+ if (s->inv[10] > 0) return 3;
+ if (s->inv[9] > 0) return 2;
+ return 1;
+}
+
+// ============================================================
+// Perlin worldgen (AVX-512, per-env)
+// ============================================================
+static inline float perlin_interp(float t) { return t*t*t*(t*(t*6.0f-15.0f)+10.0f); }
+
+#if defined(__clang__) || defined(__GNUC__)
+__attribute__((target("avx512f,avx512bw,avx512dq,avx512vl")))
+#endif
+static void generate_world(CraftaxClassic* s) {
+ // Reset maps and bitmaps
+ for (int i = 0; i < MAP_PACKED_SIZE; i++)
+ s->map_packed[i] = (uint8_t)(BLK_GRASS | (BLK_GRASS << 4));
+ memset(s->mob_bits, 0, sizeof(s->mob_bits));
+ memset(s->zombie_bits, 0, sizeof(s->zombie_bits));
+ memset(s->cow_bits, 0, sizeof(s->cow_bits));
+ memset(s->skel_bits, 0, sizeof(s->skel_bits));
+ memset(s->arrow_bits, 0, sizeof(s->arrow_bits));
+
+ // Perlin gradient tables (precompute cos/sin of the per-grid random angles).
+ // Padded by +16 floats so AVX-512 permute-load at the last grid row doesn't
+ // read out of bounds.
+ enum { GRID = 10, GRID_PAD = GRID * GRID + 16 };
+ _Alignas(64) float cos_a[4][GRID_PAD];
+ _Alignas(64) float sin_a[4][GRID_PAD];
+ for (int layer = 0; layer < 4; layer++) {
+ for (int i = 0; i < GRID * GRID; i++) {
+ float a = cr_rf(&s->pcg) * 2.0f * 3.14159265f;
+ cos_a[layer][i] = cosf(a);
+ sin_a[layer][i] = sinf(a);
+ }
+ for (int i = GRID * GRID; i < GRID_PAD; i++) { cos_a[layer][i] = 0; sin_a[layer][i] = 0; }
+ }
+
+ float scale = (float)MAP_SIZE / (float)(GRID - 1);
+ float inv_scale = 1.0f / scale;
+ int center = MAP_SIZE / 2;
+
+ _Alignas(64) float noise[4][MAP_SIZE][MAP_SIZE];
+ {
+ const __m512 c_lane = _mm512_setr_ps(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
+ const __m512 one = _mm512_set1_ps(1.0f);
+ const __m512 half = _mm512_set1_ps(0.5f);
+ const __m512 c6 = _mm512_set1_ps(6.0f);
+ const __m512 c15 = _mm512_set1_ps(15.0f);
+ const __m512 c10 = _mm512_set1_ps(10.0f);
+ const __m512 invs = _mm512_set1_ps(inv_scale);
+ const __m512i i_one = _mm512_set1_epi32(1);
+
+ for (int r = 0; r < MAP_SIZE; r++) {
+ float nr = (float)r * inv_scale;
+ int x0 = (int)nr;
+ float fx = nr - x0;
+ float fx1 = fx - 1.0f;
+ float u = perlin_interp(fx);
+ int row0 = x0 * GRID, row1 = row0 + GRID;
+ __m512 fx_v = _mm512_set1_ps(fx);
+ __m512 fx1_v = _mm512_set1_ps(fx1);
+ __m512 u_v = _mm512_set1_ps(u);
+
+ for (int c_base = 0; c_base < MAP_SIZE; c_base += 16) {
+ __m512 c_v = _mm512_add_ps(_mm512_set1_ps((float)c_base), c_lane);
+ __m512 nc_v = _mm512_mul_ps(c_v, invs);
+ __m512i y0_v = _mm512_cvttps_epi32(nc_v);
+ __m512 y0_f = _mm512_cvtepi32_ps(y0_v);
+ __m512 fy_v = _mm512_sub_ps(nc_v, y0_f);
+ __m512 fy1_v = _mm512_sub_ps(fy_v, one);
+ __m512 t = _mm512_fmsub_ps(fy_v, c6, c15);
+ t = _mm512_fmadd_ps(fy_v, t, c10);
+ __m512 fy2 = _mm512_mul_ps(fy_v, fy_v);
+ __m512 fy3 = _mm512_mul_ps(fy2, fy_v);
+ __m512 v_v = _mm512_mul_ps(fy3, t);
+ __m512i y1_v = _mm512_add_epi32(y0_v, i_one);
+
+ for (int k = 0; k < 4; k++) {
+ __m512 cos_r0 = _mm512_loadu_ps(&cos_a[k][row0]);
+ __m512 cos_r1 = _mm512_loadu_ps(&cos_a[k][row1]);
+ __m512 sin_r0 = _mm512_loadu_ps(&sin_a[k][row0]);
+ __m512 sin_r1 = _mm512_loadu_ps(&sin_a[k][row1]);
+
+ __m512 c00 = _mm512_permutexvar_ps(y0_v, cos_r0);
+ __m512 c10v= _mm512_permutexvar_ps(y0_v, cos_r1);
+ __m512 c01 = _mm512_permutexvar_ps(y1_v, cos_r0);
+ __m512 c11 = _mm512_permutexvar_ps(y1_v, cos_r1);
+ __m512 s00 = _mm512_permutexvar_ps(y0_v, sin_r0);
+ __m512 s10 = _mm512_permutexvar_ps(y0_v, sin_r1);
+ __m512 s01 = _mm512_permutexvar_ps(y1_v, sin_r0);
+ __m512 s11 = _mm512_permutexvar_ps(y1_v, sin_r1);
+
+ __m512 n00 = _mm512_fmadd_ps(c00, fx_v, _mm512_mul_ps(s00, fy_v));
+ __m512 n10 = _mm512_fmadd_ps(c10v, fx1_v, _mm512_mul_ps(s10, fy_v));
+ __m512 n01 = _mm512_fmadd_ps(c01, fx_v, _mm512_mul_ps(s01, fy1_v));
+ __m512 n11 = _mm512_fmadd_ps(c11, fx1_v, _mm512_mul_ps(s11, fy1_v));
+
+ __m512 nx0 = _mm512_fmadd_ps(u_v, _mm512_sub_ps(n10, n00), n00);
+ __m512 nx1 = _mm512_fmadd_ps(u_v, _mm512_sub_ps(n11, n01), n01);
+ __m512 n = _mm512_fmadd_ps(v_v, _mm512_sub_ps(nx1, nx0), nx0);
+ n = _mm512_mul_ps(_mm512_add_ps(n, one), half);
+
+ _mm512_storeu_ps(&noise[k][r][c_base], n);
+ }
+ }
+ }
+ }
+
+ // Tile-logic sweep -- reads precomputed noise, writes blocks
+ for (int r = 0; r < MAP_SIZE; r++) {
+ for (int c = 0; c < MAP_SIZE; c++) {
+ float water_noise = noise[0][r][c];
+ float mountain_noise = noise[1][r][c];
+ float tree_noise = noise[2][r][c];
+ float path_noise = noise[3][r][c];
+
+ float dist = sqrtf((float)((r-center)*(r-center) + (c-center)*(c-center)));
+ float prox = 1.0f - cr_min_f(dist / 20.0f, 1.0f);
+
+ float water_val = water_noise - prox * 0.3f;
+ float mountain_val = mountain_noise - prox * 0.3f;
+
+ int8_t blk = BLK_GRASS;
+ if (water_val > 0.7f) blk = BLK_WATER;
+ else if (water_val > 0.6f && water_val <= 0.75f) blk = BLK_SAND;
+ else if (mountain_val > 0.7f) {
+ blk = BLK_STONE;
+ if (path_noise > 0.8f) blk = BLK_PATH;
+ if (mountain_val > 0.85f && water_noise > 0.4f) blk = BLK_PATH;
+ if (mountain_val > 0.85f && tree_noise > 0.7f) blk = BLK_LAVA;
+ }
+ if (blk == BLK_STONE) {
+ float ore = cr_rf(&s->pcg);
+ if (ore < 0.005f && mountain_val > 0.8f) blk = BLK_DIAMOND;
+ else if (ore < 0.035f) blk = BLK_IRON;
+ else if (ore < 0.075f) blk = BLK_COAL;
+ }
+ if (blk == BLK_GRASS && tree_noise > 0.5f && cr_rf(&s->pcg) > 0.8f)
+ blk = BLK_TREE;
+ map_set(s, r, c, blk);
+ }
+ }
+
+ map_set(s, center, center, BLK_GRASS); // player spawn always grass
+
+ bool has_diamond = false;
+ for (int r = 0; r < MAP_SIZE && !has_diamond; r++)
+ for (int c = 0; c < MAP_SIZE && !has_diamond; c++)
+ if (map_get(s, r, c) == BLK_DIAMOND) has_diamond = true;
+ if (!has_diamond) {
+ for (int att = 0; att < 1000; att++) {
+ int r = cr_ri(&s->pcg, MAP_SIZE), c = cr_ri(&s->pcg, MAP_SIZE);
+ if (map_get(s, r, c) == BLK_STONE) { map_set(s, r, c, BLK_DIAMOND); break; }
+ }
+ }
+
+ // Initial intrinsics + inventory + mobs
+ s->player_r = center; s->player_c = center; s->player_dir = 4;
+ s->health = 9; s->food = 9; s->drink = 9; s->energy = 9;
+ s->is_sleeping = false;
+ s->recover = s->hunger = s->thirst = s->fatigue = 0;
+ memset(s->inv, 0, sizeof(s->inv));
+ memset(s->zombie_mask, 0, sizeof(s->zombie_mask));
+ memset(s->zombie_hp, 0, sizeof(s->zombie_hp));
+ memset(s->zombie_cd, 0, sizeof(s->zombie_cd));
+ memset(s->cow_mask, 0, sizeof(s->cow_mask));
+ memset(s->cow_hp, 0, sizeof(s->cow_hp));
+ memset(s->skel_mask, 0, sizeof(s->skel_mask));
+ memset(s->skel_hp, 0, sizeof(s->skel_hp));
+ memset(s->skel_cd, 0, sizeof(s->skel_cd));
+ memset(s->arrow_mask, 0, sizeof(s->arrow_mask));
+ memset(s->plant_mask, 0, sizeof(s->plant_mask));
+ memset(s->plant_age, 0, sizeof(s->plant_age));
+ memset(s->achievements, 0, sizeof(s->achievements));
+ s->timestep = 0;
+ s->light_level = 1.0f;
+}
+
+// ============================================================
+// Step sub-actions
+// ============================================================
+static void do_crafting(CraftaxClassic* s, int action) {
+ bool t = is_near_block(s, BLK_TABLE);
+ bool f = is_near_block(s, BLK_FURNACE);
+ if (action == ACT_MAKE_WOOD_PICK && t && s->inv[0] >= 1) { s->inv[0]--; s->inv[6]++; s->achievements[ACH_MAKE_WOOD_PICK] = true; }
+ if (action == ACT_MAKE_STONE_PICK && t && s->inv[0] >= 1 && s->inv[1] >= 1) { s->inv[0]--; s->inv[1]--; s->inv[7]++; s->achievements[ACH_MAKE_STONE_PICK] = true; }
+ if (action == ACT_MAKE_IRON_PICK && t && f && s->inv[0] >= 1 && s->inv[1] >= 1 && s->inv[3] >= 1 && s->inv[2] >= 1) {
+ s->inv[0]--; s->inv[1]--; s->inv[3]--; s->inv[2]--; s->inv[8]++; s->achievements[ACH_MAKE_IRON_PICK] = true;
+ }
+ if (action == ACT_MAKE_WOOD_SWORD && t && s->inv[0] >= 1) { s->inv[0]--; s->inv[9]++; s->achievements[ACH_MAKE_WOOD_SWORD] = true; }
+ if (action == ACT_MAKE_STONE_SWORD && t && s->inv[0] >= 1 && s->inv[1] >= 1) { s->inv[0]--; s->inv[1]--; s->inv[10]++; s->achievements[ACH_MAKE_STONE_SWORD] = true; }
+ if (action == ACT_MAKE_IRON_SWORD && t && f && s->inv[0] >= 1 && s->inv[1] >= 1 && s->inv[3] >= 1 && s->inv[2] >= 1) {
+ s->inv[0]--; s->inv[1]--; s->inv[3]--; s->inv[2]--; s->inv[11]++; s->achievements[ACH_MAKE_IRON_SWORD] = true;
+ }
+}
+
+static void do_action(CraftaxClassic* s) {
+ int tr = s->player_r + DIR_DR[s->player_dir];
+ int tc = s->player_c + DIR_DC[s->player_dir];
+ if (!in_bounds(tr, tc)) return;
+ int dmg = get_damage(s);
+ bool attacked = false;
+
+ for (int i = 0; i < MAX_ZOMBIES && !attacked; i++)
+ if (s->zombie_mask[i] && s->zombie_r[i] == tr && s->zombie_c[i] == tc) {
+ s->zombie_hp[i] -= dmg;
+ if (s->zombie_hp[i] <= 0) {
+ s->zombie_mask[i] = false;
+ mb_clear(s->mob_bits, tr, tc); mb_clear(s->zombie_bits, tr, tc);
+ s->achievements[ACH_DEFEAT_ZOMBIE] = true;
+ }
+ attacked = true;
+ }
+ for (int i = 0; i < MAX_COWS && !attacked; i++)
+ if (s->cow_mask[i] && s->cow_r[i] == tr && s->cow_c[i] == tc) {
+ s->cow_hp[i] -= dmg;
+ if (s->cow_hp[i] <= 0) {
+ s->cow_mask[i] = false;
+ mb_clear(s->mob_bits, tr, tc); mb_clear(s->cow_bits, tr, tc);
+ s->achievements[ACH_EAT_COW] = true;
+ s->food = (int8_t)cr_min_i(9, s->food + 6); s->hunger = 0;
+ }
+ attacked = true;
+ }
+ for (int i = 0; i < MAX_SKELETONS && !attacked; i++)
+ if (s->skel_mask[i] && s->skel_r[i] == tr && s->skel_c[i] == tc) {
+ s->skel_hp[i] -= dmg;
+ if (s->skel_hp[i] <= 0) {
+ s->skel_mask[i] = false;
+ mb_clear(s->mob_bits, tr, tc); mb_clear(s->skel_bits, tr, tc);
+ s->achievements[ACH_DEFEAT_SKELETON] = true;
+ }
+ attacked = true;
+ }
+ if (attacked) return;
+
+ int8_t blk = map_get(s, tr, tc);
+ switch (blk) {
+ case BLK_TREE:
+ map_set(s, tr, tc, BLK_GRASS);
+ s->inv[0] = (int8_t)cr_min_i(9, s->inv[0] + 1);
+ s->achievements[ACH_COLLECT_WOOD] = true; break;
+ case BLK_STONE:
+ if (s->inv[6] > 0 || s->inv[7] > 0 || s->inv[8] > 0) {
+ map_set(s, tr, tc, BLK_PATH);
+ s->inv[1] = (int8_t)cr_min_i(9, s->inv[1] + 1);
+ s->achievements[ACH_COLLECT_STONE] = true;
+ } break;
+ case BLK_COAL:
+ if (s->inv[6] > 0 || s->inv[7] > 0 || s->inv[8] > 0) {
+ map_set(s, tr, tc, BLK_PATH);
+ s->inv[2] = (int8_t)cr_min_i(9, s->inv[2] + 1);
+ s->achievements[ACH_COLLECT_COAL] = true;
+ } break;
+ case BLK_IRON:
+ if (s->inv[7] > 0 || s->inv[8] > 0) {
+ map_set(s, tr, tc, BLK_PATH);
+ s->inv[3] = (int8_t)cr_min_i(9, s->inv[3] + 1);
+ s->achievements[ACH_COLLECT_IRON] = true;
+ } break;
+ case BLK_DIAMOND:
+ if (s->inv[8] > 0) {
+ map_set(s, tr, tc, BLK_PATH);
+ s->inv[4] = (int8_t)cr_min_i(9, s->inv[4] + 1);
+ s->achievements[ACH_COLLECT_DIAMOND] = true;
+ } break;
+ case BLK_GRASS:
+ if (cr_rf(&s->pcg) < 0.1f) {
+ s->inv[5] = (int8_t)cr_min_i(9, s->inv[5] + 1);
+ s->achievements[ACH_COLLECT_SAPLING] = true;
+ } break;
+ case BLK_WATER:
+ s->drink = (int8_t)cr_min_i(9, s->drink + 1); s->thirst = 0;
+ s->achievements[ACH_COLLECT_DRINK] = true; break;
+ case BLK_RIPE_PLANT:
+ map_set(s, tr, tc, BLK_PLANT);
+ s->food = (int8_t)cr_min_i(9, s->food + 4); s->hunger = 0;
+ s->achievements[ACH_EAT_PLANT] = true;
+ for (int i = 0; i < MAX_PLANTS; i++)
+ if (s->plant_mask[i] && s->plant_r[i] == tr && s->plant_c[i] == tc) {
+ s->plant_age[i] = 0; break;
+ }
+ break;
+ }
+}
+
+static void place_block(CraftaxClassic* s, int action) {
+ int tr = s->player_r + DIR_DR[s->player_dir];
+ int tc = s->player_c + DIR_DC[s->player_dir];
+ if (!in_bounds(tr, tc)) return;
+ if (has_mob_at(s, tr, tc)) return;
+ int8_t blk = map_get(s, tr, tc);
+ if (action == ACT_PLACE_TABLE && s->inv[0] >= 2 && !is_solid(blk)) {
+ map_set(s, tr, tc, BLK_TABLE); s->inv[0] -= 2;
+ s->achievements[ACH_PLACE_TABLE] = true;
+ } else if (action == ACT_PLACE_FURNACE && s->inv[1] >= 1 && !is_solid(blk)) {
+ map_set(s, tr, tc, BLK_FURNACE); s->inv[1] -= 1;
+ s->achievements[ACH_PLACE_FURNACE] = true;
+ } else if (action == ACT_PLACE_STONE && s->inv[1] >= 1 && (!is_solid(blk) || blk == BLK_WATER)) {
+ map_set(s, tr, tc, BLK_STONE); s->inv[1] -= 1;
+ s->achievements[ACH_PLACE_STONE] = true;
+ } else if (action == ACT_PLACE_PLANT && s->inv[5] >= 1 && blk == BLK_GRASS) {
+ map_set(s, tr, tc, BLK_PLANT); s->inv[5] -= 1;
+ s->achievements[ACH_PLACE_PLANT] = true;
+ for (int i = 0; i < MAX_PLANTS; i++) {
+ if (!s->plant_mask[i]) {
+ s->plant_r[i] = tr; s->plant_c[i] = tc;
+ s->plant_age[i] = 0; s->plant_mask[i] = true; break;
+ }
+ }
+ }
+}
+
+static void move_player(CraftaxClassic* s, int action) {
+ if (action < 1 || action > 4) return;
+ int nr = s->player_r + DIR_DR[action];
+ int nc = s->player_c + DIR_DC[action];
+ s->player_dir = (int8_t)action;
+ if (!in_bounds(nr, nc)) return;
+ if (is_solid(map_get(s, nr, nc))) return;
+ if (has_mob_at(s, nr, nc)) return;
+ s->player_r = (int16_t)nr; s->player_c = (int16_t)nc;
+}
+
+static bool can_move_mob(const CraftaxClassic* s, int r, int c) {
+ if (!in_bounds(r, c)) return false;
+ int8_t blk = map_get(s, r, c);
+ if (is_solid(blk)) return false;
+ if (blk == BLK_LAVA) return false;
+ if (has_mob_at(s, r, c)) return false;
+ if (r == s->player_r && c == s->player_c) return false;
+ return true;
+}
+
+static void update_mobs(CraftaxClassic* s) {
+ int pr = s->player_r, pc = s->player_c;
+
+ for (int i = 0; i < MAX_ZOMBIES; i++) {
+ if (!s->zombie_mask[i]) continue;
+ int zr = s->zombie_r[i], zc = s->zombie_c[i];
+ int dist = l1_dist(zr, zc, pr, pc);
+ if (dist >= MOB_DESPAWN_DIST) {
+ s->zombie_mask[i] = false;
+ mb_clear(s->mob_bits, zr, zc); mb_clear(s->zombie_bits, zr, zc);
+ continue;
+ }
+ if (dist <= 1 && s->zombie_cd[i] <= 0) {
+ int dmg = s->is_sleeping ? 7 : 2;
+ s->health -= dmg;
+ s->zombie_cd[i] = 5;
+ s->is_sleeping = false;
+ }
+ s->zombie_cd[i] = (int8_t)cr_max_i(0, s->zombie_cd[i] - 1);
+
+ int dr = 0, dc = 0;
+ if (dist < 10 && cr_rf(&s->pcg) < 0.75f) {
+ int adr = abs(pr - zr), adc = abs(pc - zc);
+ if (adr > adc || (adr == adc && cr_rf(&s->pcg) < 0.5f)) dr = cr_sign_i(pr - zr);
+ else dc = cr_sign_i(pc - zc);
+ } else {
+ int d = cr_ri(&s->pcg, 4);
+ dr = DIR_DR[d+1]; dc = DIR_DC[d+1];
+ }
+ int nr = zr + dr, nc = zc + dc;
+ if (can_move_mob(s, nr, nc)) {
+ mb_clear(s->mob_bits, zr, zc); mb_clear(s->zombie_bits, zr, zc);
+ s->zombie_r[i] = (int16_t)nr; s->zombie_c[i] = (int16_t)nc;
+ mb_set(s->mob_bits, nr, nc); mb_set(s->zombie_bits, nr, nc);
+ }
+ }
+
+ for (int i = 0; i < MAX_COWS; i++) {
+ if (!s->cow_mask[i]) continue;
+ int cr = s->cow_r[i], cc = s->cow_c[i];
+ int dist = l1_dist(cr, cc, pr, pc);
+ if (dist >= MOB_DESPAWN_DIST) {
+ s->cow_mask[i] = false;
+ mb_clear(s->mob_bits, cr, cc); mb_clear(s->cow_bits, cr, cc);
+ continue;
+ }
+ int d = cr_ri(&s->pcg, 8);
+ if (d < 4) {
+ int dr = DIR_DR[d+1], dc2 = DIR_DC[d+1];
+ int nr = cr + dr, nc = cc + dc2;
+ if (can_move_mob(s, nr, nc)) {
+ mb_clear(s->mob_bits, cr, cc); mb_clear(s->cow_bits, cr, cc);
+ s->cow_r[i] = (int16_t)nr; s->cow_c[i] = (int16_t)nc;
+ mb_set(s->mob_bits, nr, nc); mb_set(s->cow_bits, nr, nc);
+ }
+ }
+ }
+
+ for (int i = 0; i < MAX_SKELETONS; i++) {
+ if (!s->skel_mask[i]) continue;
+ int sr = s->skel_r[i], sc = s->skel_c[i];
+ int dist = l1_dist(sr, sc, pr, pc);
+ if (dist >= MOB_DESPAWN_DIST) {
+ s->skel_mask[i] = false;
+ mb_clear(s->mob_bits, sr, sc); mb_clear(s->skel_bits, sr, sc);
+ continue;
+ }
+ if (dist >= 4 && dist <= 5 && s->skel_cd[i] <= 0) {
+ for (int a = 0; a < MAX_ARROWS; a++) {
+ if (!s->arrow_mask[a]) {
+ s->arrow_mask[a] = true;
+ s->arrow_r[a] = (int16_t)sr; s->arrow_c[a] = (int16_t)sc;
+ mb_set(s->arrow_bits, sr, sc);
+ int adr = abs(pr - sr), adc = abs(pc - sc);
+ s->arrow_dr[a] = (int8_t)((adr > 0) ? cr_sign_i(pr - sr) : 0);
+ s->arrow_dc[a] = (int8_t)((adc > 0) ? cr_sign_i(pc - sc) : 0);
+ break;
+ }
+ }
+ s->skel_cd[i] = 4;
+ }
+ s->skel_cd[i] = (int8_t)cr_max_i(0, s->skel_cd[i] - 1);
+
+ int dr = 0, dc = 0;
+ bool random_move = cr_rf(&s->pcg) < 0.15f;
+ if (!random_move) {
+ if (dist >= 10) {
+ int adr = abs(pr - sr), adc = abs(pc - sc);
+ if (adr > adc || (adr == adc && cr_rf(&s->pcg) < 0.5f)) dr = cr_sign_i(pr - sr);
+ else dc = cr_sign_i(pc - sc);
+ } else if (dist <= 3) {
+ int adr = abs(pr - sr), adc = abs(pc - sc);
+ if (adr > adc || (adr == adc && cr_rf(&s->pcg) < 0.5f)) dr = -cr_sign_i(pr - sr);
+ else dc = -cr_sign_i(pc - sc);
+ } else {
+ random_move = true;
+ }
+ }
+ if (random_move) {
+ int d = cr_ri(&s->pcg, 4);
+ dr = DIR_DR[d+1]; dc = DIR_DC[d+1];
+ }
+ int nr = sr + dr, nc = sc + dc;
+ if (can_move_mob(s, nr, nc)) {
+ mb_clear(s->mob_bits, sr, sc); mb_clear(s->skel_bits, sr, sc);
+ s->skel_r[i] = (int16_t)nr; s->skel_c[i] = (int16_t)nc;
+ mb_set(s->mob_bits, nr, nc); mb_set(s->skel_bits, nr, nc);
+ }
+ }
+
+ for (int i = 0; i < MAX_ARROWS; i++) {
+ if (!s->arrow_mask[i]) continue;
+ int ar = s->arrow_r[i], ac = s->arrow_c[i];
+ int nr = ar + s->arrow_dr[i], nc = ac + s->arrow_dc[i];
+ if (!in_bounds(nr, nc)) { s->arrow_mask[i] = false; mb_clear(s->arrow_bits, ar, ac); continue; }
+ int8_t blk = map_get(s, nr, nc);
+ if (is_solid(blk) && blk != BLK_WATER) {
+ if (blk == BLK_FURNACE || blk == BLK_TABLE) map_set(s, nr, nc, BLK_PATH);
+ s->arrow_mask[i] = false; mb_clear(s->arrow_bits, ar, ac); continue;
+ }
+ if (nr == pr && nc == pc) {
+ s->health -= 2; s->is_sleeping = false;
+ s->arrow_mask[i] = false; mb_clear(s->arrow_bits, ar, ac); continue;
+ }
+ mb_clear(s->arrow_bits, ar, ac);
+ s->arrow_r[i] = (int16_t)nr; s->arrow_c[i] = (int16_t)nc;
+ mb_set(s->arrow_bits, nr, nc);
+ }
+}
+
+static bool try_spawn(CraftaxClassic* s, int min_d, int max_d, bool need_grass, bool need_path,
+ int* or_, int* oc_) {
+ int pr = s->player_r, pc = s->player_c;
+ for (int att = 0; att < 20; att++) {
+ int r = cr_ri(&s->pcg, MAP_SIZE), c = cr_ri(&s->pcg, MAP_SIZE);
+ int dist = l1_dist(r, c, pr, pc);
+ if (dist < min_d || dist >= max_d) continue;
+ if (has_mob_at(s, r, c)) continue;
+ if (r == pr && c == pc) continue;
+ int8_t blk = map_get(s, r, c);
+ if (need_grass && blk != BLK_GRASS) continue;
+ if (need_path && blk != BLK_PATH ) continue;
+ if (!need_grass && !need_path && blk != BLK_GRASS && blk != BLK_PATH) continue;
+ *or_ = r; *oc_ = c; return true;
+ }
+ return false;
+}
+
+static void spawn_mobs(CraftaxClassic* s) {
+ int n_cows = 0, n_z = 0, n_sk = 0;
+ for (int i = 0; i < MAX_COWS; i++) n_cows += s->cow_mask[i];
+ for (int i = 0; i < MAX_ZOMBIES; i++) n_z += s->zombie_mask[i];
+ for (int i = 0; i < MAX_SKELETONS; i++) n_sk += s->skel_mask[i];
+
+ if (n_cows < MAX_COWS && cr_rf(&s->pcg) < 0.1f) {
+ int r, c;
+ if (try_spawn(s, 3, MOB_DESPAWN_DIST, true, false, &r, &c)) {
+ for (int i = 0; i < MAX_COWS; i++) if (!s->cow_mask[i]) {
+ s->cow_mask[i] = true; s->cow_r[i] = (int16_t)r; s->cow_c[i] = (int16_t)c; s->cow_hp[i] = 3;
+ mb_set(s->mob_bits, r, c); mb_set(s->cow_bits, r, c);
+ break;
+ }
+ }
+ }
+ float zombie_chance = 0.02f + 0.1f * (1.0f - s->light_level) * (1.0f - s->light_level);
+ if (n_z < MAX_ZOMBIES && cr_rf(&s->pcg) < zombie_chance) {
+ int r, c;
+ if (try_spawn(s, 9, MOB_DESPAWN_DIST, false, false, &r, &c)) {
+ for (int i = 0; i < MAX_ZOMBIES; i++) if (!s->zombie_mask[i]) {
+ s->zombie_mask[i] = true; s->zombie_r[i] = (int16_t)r; s->zombie_c[i] = (int16_t)c;
+ s->zombie_hp[i] = 5; s->zombie_cd[i] = 0;
+ mb_set(s->mob_bits, r, c); mb_set(s->zombie_bits, r, c);
+ break;
+ }
+ }
+ }
+ if (n_sk < MAX_SKELETONS && cr_rf(&s->pcg) < 0.05f) {
+ int r, c;
+ if (try_spawn(s, 9, MOB_DESPAWN_DIST, false, true, &r, &c)) {
+ for (int i = 0; i < MAX_SKELETONS; i++) if (!s->skel_mask[i]) {
+ s->skel_mask[i] = true; s->skel_r[i] = (int16_t)r; s->skel_c[i] = (int16_t)c;
+ s->skel_hp[i] = 3; s->skel_cd[i] = 0;
+ mb_set(s->mob_bits, r, c); mb_set(s->skel_bits, r, c);
+ break;
+ }
+ }
+ }
+}
+
+static void update_plants(CraftaxClassic* s) {
+ for (int i = 0; i < MAX_PLANTS; i++) {
+ if (!s->plant_mask[i]) continue;
+ s->plant_age[i]++;
+ if (s->plant_age[i] >= 600) {
+ int r = s->plant_r[i], c = s->plant_c[i];
+ if (in_bounds(r, c) && map_get(s, r, c) == BLK_PLANT)
+ map_set(s, r, c, BLK_RIPE_PLANT);
+ }
+ }
+}
+
+static void update_intrinsics(CraftaxClassic* s, int action) {
+ if (action == ACT_SLEEP && s->energy < 9) s->is_sleeping = true;
+ if (s->energy >= 9 && s->is_sleeping) {
+ s->is_sleeping = false;
+ s->achievements[ACH_WAKE_UP] = true;
+ }
+ float mul = s->is_sleeping ? 0.5f : 1.0f;
+ s->hunger += mul; if (s->hunger > 25.0f) { s->food--; s->hunger = 0; }
+ s->thirst += mul; if (s->thirst > 20.0f) { s->drink--; s->thirst = 0; }
+ if (s->is_sleeping) s->fatigue -= 1.0f; else s->fatigue += 1.0f;
+ if (s->fatigue > 30.0f) { s->energy--; s->fatigue = 0; }
+ if (s->fatigue < -10.0f) { s->energy = (int8_t)cr_min_i(s->energy + 1, 9); s->fatigue = 0; }
+ bool ok = (s->food > 0) && (s->drink > 0) && (s->energy > 0 || s->is_sleeping);
+ if (ok) s->recover += s->is_sleeping ? 2.0f : 1.0f;
+ else s->recover += s->is_sleeping ? -0.5f : -1.0f;
+ if (s->recover > 25.0f) { s->health = (int8_t)cr_min_i(s->health + 1, 9); s->recover = 0; }
+ if (s->recover < -15.0f) { s->health--; s->recover = 0; }
+}
+
+// ============================================================
+// Observation builder (writes OBS_DIM floats into env->observations)
+// ============================================================
+static void compute_observations(CraftaxClassic* s) {
+ float* obs = s->observations;
+ int pr = s->player_r, pc = s->player_c;
+ int idx = 0;
+ for (int dr = -3; dr <= 3; dr++) {
+ int r = pr + dr;
+ bool row_ok = (unsigned)r < MAP_SIZE;
+ uint64_t zb = row_ok ? s->zombie_bits[r] : 0;
+ uint64_t cb = row_ok ? s->cow_bits[r] : 0;
+ uint64_t sb = row_ok ? s->skel_bits[r] : 0;
+ uint64_t ab = row_ok ? s->arrow_bits[r] : 0;
+ for (int dc = -4; dc <= 4; dc++) {
+ int c = pc + dc;
+ int8_t blk = (row_ok && (unsigned)c < MAP_SIZE) ? map_get(s, r, c) : BLK_OUT_OF_BOUNDS;
+ float* dst = obs + idx;
+ for (int b = 0; b < NUM_BLOCK_TYPES; b++) dst[b] = 0.0f;
+ if ((unsigned)blk < NUM_BLOCK_TYPES) dst[blk] = 1.0f;
+ idx += NUM_BLOCK_TYPES;
+ float mz = 0, mc = 0, ms = 0, ma = 0;
+ if (row_ok && (unsigned)c < MAP_SIZE) {
+ uint64_t bit = 1ULL << c;
+ mz = (zb & bit) ? 1.0f : 0.0f;
+ mc = (cb & bit) ? 1.0f : 0.0f;
+ ms = (sb & bit) ? 1.0f : 0.0f;
+ ma = (ab & bit) ? 1.0f : 0.0f;
+ }
+ obs[idx++] = mz; obs[idx++] = mc; obs[idx++] = ms; obs[idx++] = ma;
+ }
+ }
+ for (int i = 0; i < NUM_INVENTORY; i++) obs[idx++] = (float)s->inv[i] * 0.1f;
+ obs[idx++] = (float)s->health * 0.1f;
+ obs[idx++] = (float)s->food * 0.1f;
+ obs[idx++] = (float)s->drink * 0.1f;
+ obs[idx++] = (float)s->energy * 0.1f;
+ for (int d = 1; d <= 4; d++) obs[idx++] = (s->player_dir == d) ? 1.0f : 0.0f;
+ obs[idx++] = s->light_level;
+ obs[idx++] = s->is_sleeping ? 1.0f : 0.0f;
+}
+
+// ============================================================
+// Logging (stats accumulated into env->log; flushed at vec-level by PufferLib)
+// ============================================================
+static void add_log(CraftaxClassic* env) {
+ int unlocked = 0;
+ for (int i = 0; i < NUM_ACHIEVEMENTS; i++) {
+ if (env->achievements[i]) {
+ unlocked++;
+ env->log.achievements[i] += 1.0f;
+ }
+ }
+ env->log.perf += (float)unlocked / (float)NUM_ACHIEVEMENTS;
+ env->log.score += env->episode_return_accum;
+ env->log.episode_return += env->episode_return_accum;
+ env->log.episode_length += (float)env->episode_length_accum;
+ env->log.n += 1.0f;
+}
+
+// ============================================================
+// Public API: c_init / c_reset / c_step / c_close / c_render
+// ============================================================
+static void c_init(CraftaxClassic* env) {
+ env->num_agents = 1;
+ env->client = NULL;
+ // env->rng was seeded by default my_vec_init to the env index; use it to
+ // initialize a proper 64-bit PCG state.
+ uint64_t seed = (uint64_t)env->rng;
+ env->pcg = seed * 0x9E3779B97F4A7C15ULL + 0x87C37B91114253D5ULL;
+ // Warm the RNG a bit so small seeds don't produce correlated worlds.
+ for (int i = 0; i < 8; i++) (void)cr_pcg(&env->pcg);
+ memset(&env->log, 0, sizeof(env->log));
+}
+
+static void c_reset(CraftaxClassic* env) {
+ env->episode_return_accum = 0.0f;
+ env->episode_length_accum = 0;
+ generate_world(env);
+ compute_observations(env);
+}
+
+static void c_step(CraftaxClassic* env) {
+ env->rewards[0] = 0.0f;
+ env->terminals[0] = 0.0f;
+
+ int action = (int)env->actions[0];
+ if (action < 0) action = 0;
+ if (action >= NUM_ACTIONS) action = NUM_ACTIONS - 1;
+
+ // Snapshot for reward computation
+ env->old_health = env->health;
+ memcpy(env->old_achievements, env->achievements, sizeof(env->achievements));
+
+ int eff_action = env->is_sleeping ? ACT_NOOP : action;
+ do_crafting(env, eff_action);
+ if (eff_action == ACT_DO) do_action(env);
+ if (eff_action >= ACT_PLACE_STONE && eff_action <= ACT_PLACE_PLANT) place_block(env, eff_action);
+ move_player(env, eff_action);
+ update_mobs(env);
+ spawn_mobs(env);
+ update_plants(env);
+ update_intrinsics(env, action);
+
+ for (int i = 0; i < NUM_INVENTORY; i++)
+ env->inv[i] = (int8_t)cr_clamp_i(env->inv[i], 0, 9);
+
+ env->timestep++;
+ float t_frac = fmodf((float)env->timestep / (float)DAY_LENGTH, 1.0f) + 0.3f;
+ float cv = cosf(3.14159265f * t_frac);
+ env->light_level = 1.0f - fabsf(cv * cv * cv);
+
+ // Reward: new achievements + health change * 0.1
+ float ach_r = 0.0f;
+ for (int i = 0; i < NUM_ACHIEVEMENTS; i++)
+ ach_r += (float)(env->achievements[i] && !env->old_achievements[i]);
+ float hp_r = (float)(env->health - env->old_health) * 0.1f;
+ float r = ach_r + hp_r;
+ env->rewards[0] = r;
+ env->episode_return_accum += r;
+ env->episode_length_accum += 1;
+
+ // Terminal conditions
+ bool done = (env->timestep >= MAX_TIMESTEPS) || (env->health <= 0);
+ if (in_bounds(env->player_r, env->player_c)
+ && map_get(env, env->player_r, env->player_c) == BLK_LAVA) done = true;
+
+ if (done) {
+ env->terminals[0] = 1.0f;
+ add_log(env);
+ c_reset(env); // auto-reset (observation written inside)
+ } else {
+ compute_observations(env);
+ }
+}
+
+static void c_close(CraftaxClassic* env) {
+ (void)env;
+}
+
+// ============================================================
+// Tile-based renderer sharing the full-Craftax textures.bin
+// ============================================================
+// Shared layout (see ocean/craftax/pack_textures.py):
+// [0..36] block textures (first 17 used by classic, indexed by BLK_*)
+// [37..41] player: down, up, left, right, sleep
+// [42..46] items (unused by classic)
+// [47..49] mobs: zombie, skeleton, cow
+// [50..53] arrows: down, up, left, right
+
+#include
+
+#define CC_TEX_TILE_PX 16
+#define CC_TEX_SCALE 4
+#define CC_TEX_DRAW_PX (CC_TEX_TILE_PX * CC_TEX_SCALE)
+#define CC_TEX_NUM (37 + 5 + 5 + 3 + 4)
+
+#define CC_TEX_PLAYER_DOWN 37
+#define CC_TEX_PLAYER_UP 38
+#define CC_TEX_PLAYER_LEFT 39
+#define CC_TEX_PLAYER_RIGHT 40
+#define CC_TEX_PLAYER_SLEEP 41
+#define CC_TEX_MOB_ZOMBIE 47
+#define CC_TEX_MOB_SKELETON 48
+#define CC_TEX_MOB_COW 49
+#define CC_TEX_ARROW_DOWN 50
+#define CC_TEX_ARROW_UP 51
+#define CC_TEX_ARROW_LEFT 52
+#define CC_TEX_ARROW_RIGHT 53
+
+#define CC_RENDER_ROWS 16
+#define CC_RENDER_COLS 16
+
+static Texture2D cc_textures[CC_TEX_NUM];
+static bool cc_textures_loaded = false;
+
+static void cc_load_textures(void) {
+ if (cc_textures_loaded) return;
+ const char* candidates[] = {
+ "resources/craftax/textures.bin",
+ "../resources/craftax/textures.bin",
+ "../../resources/craftax/textures.bin",
+ };
+ FILE* f = NULL;
+ for (size_t i = 0; i < sizeof(candidates)/sizeof(candidates[0]); i++) {
+ f = fopen(candidates[i], "rb");
+ if (f) break;
+ }
+ if (!f) {
+ fprintf(stderr, "craftax_classic: textures.bin not found in resources/craftax -- run ocean/craftax/pack_textures.py\n");
+ exit(1);
+ }
+ const size_t tile_bytes = CC_TEX_TILE_PX * CC_TEX_TILE_PX * 4;
+ uint8_t* buf = (uint8_t*)malloc(tile_bytes);
+ for (int i = 0; i < CC_TEX_NUM; i++) {
+ if (fread(buf, 1, tile_bytes, f) != tile_bytes) {
+ fprintf(stderr, "craftax_classic: short read on textures.bin at tile %d\n", i);
+ exit(1);
+ }
+ Image img = {
+ .data = buf,
+ .width = CC_TEX_TILE_PX,
+ .height = CC_TEX_TILE_PX,
+ .mipmaps = 1,
+ .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
+ };
+ cc_textures[i] = LoadTextureFromImage(img);
+ SetTextureFilter(cc_textures[i], TEXTURE_FILTER_POINT);
+ }
+ free(buf);
+ fclose(f);
+ cc_textures_loaded = true;
+}
+
+static int cc_player_tex_id(int8_t dir, bool sleeping) {
+ if (sleeping) return CC_TEX_PLAYER_SLEEP;
+ switch (dir) {
+ case 1: return CC_TEX_PLAYER_LEFT;
+ case 2: return CC_TEX_PLAYER_RIGHT;
+ case 3: return CC_TEX_PLAYER_UP;
+ case 4: return CC_TEX_PLAYER_DOWN;
+ default: return CC_TEX_PLAYER_DOWN;
+ }
+}
+
+static int cc_arrow_tex_id(int8_t dr, int8_t dc) {
+ if (dr < 0) return CC_TEX_ARROW_UP;
+ if (dr > 0) return CC_TEX_ARROW_DOWN;
+ if (dc < 0) return CC_TEX_ARROW_LEFT;
+ return CC_TEX_ARROW_RIGHT;
+}
+
+static void cc_draw_tile(int tex_id, int dst_x, int dst_y) {
+ if (tex_id < 0 || tex_id >= CC_TEX_NUM) return;
+ Rectangle src = {0, 0, CC_TEX_TILE_PX, CC_TEX_TILE_PX};
+ Rectangle dst = {(float)dst_x, (float)dst_y, CC_TEX_DRAW_PX, CC_TEX_DRAW_PX};
+ DrawTexturePro(cc_textures[tex_id], src, dst, (Vector2){0, 0}, 0.0f, WHITE);
+}
+
+static void c_render(CraftaxClassic* env) {
+ const int view_w = CC_RENDER_COLS * CC_TEX_DRAW_PX;
+ const int view_h = CC_RENDER_ROWS * CC_TEX_DRAW_PX;
+ const int hud_h = 60;
+
+ if (!IsWindowReady()) {
+ InitWindow(view_w, view_h + hud_h, "PufferLib Craftax-Classic");
+ SetTargetFPS(30);
+ }
+ if (!cc_textures_loaded) cc_load_textures();
+ if (IsKeyDown(KEY_ESCAPE)) exit(0);
+
+ int pr = env->player_r;
+ int pc = env->player_c;
+ int half_r = CC_RENDER_ROWS / 2;
+ int half_c = CC_RENDER_COLS / 2;
+
+ BeginDrawing();
+ ClearBackground(BLACK);
+
+ for (int vr = 0; vr < CC_RENDER_ROWS; vr++) {
+ for (int vc = 0; vc < CC_RENDER_COLS; vc++) {
+ int wr = pr - half_r + vr;
+ int wc = pc - half_c + vc;
+ int dst_x = vc * CC_TEX_DRAW_PX;
+ int dst_y = vr * CC_TEX_DRAW_PX;
+
+ int blk = BLK_OUT_OF_BOUNDS;
+ if (in_bounds(wr, wc)) blk = map_get(env, wr, wc);
+ if (blk < 0 || blk >= 17) blk = 0;
+ cc_draw_tile(blk, dst_x, dst_y);
+ }
+ }
+
+ // Mobs
+ for (int i = 0; i < MAX_ZOMBIES; i++) {
+ if (!env->zombie_mask[i]) continue;
+ int vr = env->zombie_r[i] - pr + half_r;
+ int vc = env->zombie_c[i] - pc + half_c;
+ if (vr < 0 || vr >= CC_RENDER_ROWS || vc < 0 || vc >= CC_RENDER_COLS) continue;
+ cc_draw_tile(CC_TEX_MOB_ZOMBIE, vc * CC_TEX_DRAW_PX, vr * CC_TEX_DRAW_PX);
+ }
+ for (int i = 0; i < MAX_SKELETONS; i++) {
+ if (!env->skel_mask[i]) continue;
+ int vr = env->skel_r[i] - pr + half_r;
+ int vc = env->skel_c[i] - pc + half_c;
+ if (vr < 0 || vr >= CC_RENDER_ROWS || vc < 0 || vc >= CC_RENDER_COLS) continue;
+ cc_draw_tile(CC_TEX_MOB_SKELETON, vc * CC_TEX_DRAW_PX, vr * CC_TEX_DRAW_PX);
+ }
+ for (int i = 0; i < MAX_COWS; i++) {
+ if (!env->cow_mask[i]) continue;
+ int vr = env->cow_r[i] - pr + half_r;
+ int vc = env->cow_c[i] - pc + half_c;
+ if (vr < 0 || vr >= CC_RENDER_ROWS || vc < 0 || vc >= CC_RENDER_COLS) continue;
+ cc_draw_tile(CC_TEX_MOB_COW, vc * CC_TEX_DRAW_PX, vr * CC_TEX_DRAW_PX);
+ }
+ for (int i = 0; i < MAX_ARROWS; i++) {
+ if (!env->arrow_mask[i]) continue;
+ int vr = env->arrow_r[i] - pr + half_r;
+ int vc = env->arrow_c[i] - pc + half_c;
+ if (vr < 0 || vr >= CC_RENDER_ROWS || vc < 0 || vc >= CC_RENDER_COLS) continue;
+ cc_draw_tile(cc_arrow_tex_id(env->arrow_dr[i], env->arrow_dc[i]),
+ vc * CC_TEX_DRAW_PX, vr * CC_TEX_DRAW_PX);
+ }
+
+ // Player in center
+ cc_draw_tile(cc_player_tex_id(env->player_dir, env->is_sleeping),
+ half_c * CC_TEX_DRAW_PX, half_r * CC_TEX_DRAW_PX);
+
+ // Night dim
+ if (env->light_level < 1.0f) {
+ unsigned char a = (unsigned char)((1.0f - env->light_level) * 140.0f);
+ DrawRectangle(0, 0, view_w, view_h, (Color){0, 0, 40, a});
+ }
+
+ // HUD
+ int hud_y = view_h;
+ DrawRectangle(0, hud_y, view_w, hud_h, (Color){20, 20, 20, 255});
+ DrawText(TextFormat("HP:%d F:%d D:%d E:%d t:%d light:%.2f",
+ env->health, env->food, env->drink, env->energy,
+ env->timestep, env->light_level),
+ 4, hud_y + 4, 14, WHITE);
+ int ach_count = 0;
+ for (int i = 0; i < NUM_ACHIEVEMENTS; i++) ach_count += env->achievements[i] ? 1 : 0;
+ DrawText(TextFormat("ach:%d/%d ret:%.2f len:%d", ach_count, NUM_ACHIEVEMENTS,
+ env->episode_return_accum, env->episode_length_accum),
+ 4, hud_y + 22, 14, (Color){180, 220, 180, 255});
+ DrawText(TextFormat("inv: w=%d s=%d c=%d i=%d d=%d sap=%d pick w/s/i:%d/%d/%d sword w/s/i:%d/%d/%d",
+ env->inv[0], env->inv[1], env->inv[2], env->inv[3], env->inv[4], env->inv[5],
+ env->inv[6], env->inv[7], env->inv[8], env->inv[9], env->inv[10], env->inv[11]),
+ 4, hud_y + 40, 12, (Color){180, 180, 180, 255});
+ EndDrawing();
+}
diff --git a/ocean/dino/binding.c b/ocean/dino/binding.c
new file mode 100644
index 0000000000..3bc64c0286
--- /dev/null
+++ b/ocean/dino/binding.c
@@ -0,0 +1,28 @@
+#include "dino.h"
+
+#define OBS_SIZE (5 + 3 * 9)
+#define NUM_ATNS 1
+#define ACT_SIZES {3}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Dinosaur
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->speed_init = dict_get(kwargs, "speed_init")->value;
+ env->speed_max = dict_get(kwargs, "speed_max")->value;
+ env->spawn_rate_min = dict_get(kwargs, "spawn_rate_min")->value;
+ env->spawn_rate_max = dict_get(kwargs, "spawn_rate_max")->value;
+ env->rate_increment_rate = dict_get(kwargs, "rate_increment_rate")->value;
+ c_init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+}
\ No newline at end of file
diff --git a/ocean/dino/dino.c b/ocean/dino/dino.c
new file mode 100644
index 0000000000..fcf0b94e70
--- /dev/null
+++ b/ocean/dino/dino.c
@@ -0,0 +1,45 @@
+#include "dino.h"
+#include "puffernet.h"
+
+int main() {
+ Weights* weights = load_weights("resources/dino/dino_weights.bin");
+ int logit_sizes[1] = {3};
+ int obs_size = 5 + 3 * 9;
+ PufferNet* net = make_puffernet(weights, 1, obs_size, 512, 1, logit_sizes, 1);
+
+ Dinosaur env = {
+ .width = 800,
+ .height = 400,
+ .speed_init = 6,
+ .speed_max = 14,
+ .spawn_rate_max = 65,
+ .spawn_rate_min = 45,
+ .rate_increment_rate = 600,
+ };
+ env.client = make_client(&env);
+
+ c_init(&env);
+ c_reset(&env);
+ c_render(&env);
+
+ while (!WindowShouldClose()) {
+ env.actions[0] = rand() % 3;
+ if(IsKeyDown(KEY_LEFT_SHIFT)){
+ env.actions[0] = (float) NOOP;
+ if(IsKeyDown(KEY_UP)) env.actions[0] = (float) JUMP;
+ if(IsKeyDown(KEY_DOWN)) env.actions[0] = (float) CROUCH;
+ } else {
+ forward_puffernet(net, env.observations, env.actions);
+ }
+ c_step(&env);
+ c_render(&env);
+ }
+
+ free_puffernet(net);
+ free(weights);
+ free(env.observations);
+ free(env.actions);
+ free(env.rewards);
+ free(env.terminals);
+ c_close(&env);
+}
\ No newline at end of file
diff --git a/ocean/dino/dino.h b/ocean/dino/dino.h
new file mode 100644
index 0000000000..5ada905ed8
--- /dev/null
+++ b/ocean/dino/dino.h
@@ -0,0 +1,381 @@
+#include
+#include
+#include
+#include
+#include "raylib.h"
+
+#define MAX_OBSTACLES 9
+#define OBS_SIZE (5 + 3 * 9)
+
+#define PLAYER_HEIGHT 48
+#define PLAYER_WIDTH 32
+#define PLAYER_JUMP 10.0f
+#define GRAVITY 0.5f
+
+#define CACTUS_HEIGHT 24
+#define CACTUS_WIDTH 24
+#define CACTUS_Y 0
+
+#define BIRD_HEIGHT 24
+#define BIRD_WIDTH 48
+#define BIRD_Y 44
+
+#define NOOP 0
+#define JUMP 1
+#define CROUCH 2
+
+typedef struct {
+ float perf;
+ float score;
+ float episode_length;
+ float episode_return;
+ float n;
+} Log;
+
+typedef struct {
+ Texture2D dinosaur_up;
+ Texture2D dinosaur_down;
+ Texture2D cactus;
+ Texture2D bird;
+} Client;
+
+typedef struct {
+ float x;
+ float y;
+ float y_velocity;
+ float jump_strength;
+ int ticks;
+ float width;
+ float height;
+ float x_offset;
+} Agent;
+
+enum ObstacleType {
+ CACTUS,
+ BIRD
+};
+
+typedef struct{
+ float x;
+ float y;
+ float width;
+ float height;
+ enum ObstacleType type;
+} Obstacle;
+
+typedef struct {
+ /* Mandatory */
+ Log log;
+ float* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ unsigned int rng;
+ int num_agents;
+ /* Not customizable */
+ Client* client;
+ Agent* agent;
+ Obstacle* obstacles;
+ int num_obstacles;
+ int speed;
+ int spawn_rate;
+ float gravity;
+ int spawn_ticks;
+ int max_obstacles;
+ /* Customizable */
+ int width;
+ int height;
+ int speed_init;
+ int speed_max;
+ int spawn_rate_min;
+ int spawn_rate_max;
+ int rate_increment_rate;
+} Dinosaur;
+
+Client* make_client(Dinosaur* env){
+ Client* client = (Client*)calloc(1, sizeof(Client));
+
+ InitWindow(env->width, env->height, "Pufferlib Dinosaur");
+ SetTargetFPS(60);
+
+ client->cactus = LoadTexture("resources/dino/cactus.png");
+ client->bird = LoadTexture("resources/dino/bird.png");
+ client->dinosaur_up = LoadTexture("resources/dino/dino.png");
+ client->dinosaur_down = LoadTexture("resources/dino/dino_down.png");
+ return client;
+}
+
+void allocate(Dinosaur* env) {
+ env->observations = (float *)calloc(OBS_SIZE, sizeof(float));
+ env->actions = (float *)calloc(1, sizeof(float));
+ env->rewards = (float *)calloc(1, sizeof(float));
+ env->terminals = (float *)calloc(1, sizeof(float));
+}
+
+void c_init(Dinosaur* env){
+ allocate(env);
+ env->gravity = GRAVITY;
+ env->spawn_rate = 1;
+ env->spawn_ticks = 0;
+ env->max_obstacles = MAX_OBSTACLES;
+
+ env->agent = calloc(1, sizeof(Agent));
+ env->agent->x = 0.0f + 2.0f * PLAYER_WIDTH;
+ env->agent->y = 0;
+ env->agent->jump_strength = PLAYER_JUMP;
+ env->agent->width = PLAYER_WIDTH;
+ env->agent->height = PLAYER_HEIGHT;
+
+ env->num_agents = 1;
+}
+
+void compute_observations(Dinosaur* env) {
+ int obs_idx = 0;
+
+ memset(env->observations, 0, OBS_SIZE * sizeof(float));
+
+ env->observations[obs_idx++] = env->agent->y / (pow(env->agent->jump_strength, 2) / (2 * env->gravity));
+ env->observations[obs_idx++] = env->agent->width / (PLAYER_WIDTH * 2.0f);
+ env->observations[obs_idx++] = env->agent->height / (float) PLAYER_HEIGHT;
+ env->observations[obs_idx++] = (float) env->speed / 10.0f;
+ env->observations[obs_idx++] = env->agent->ticks/100.0f;
+
+ for(int o = 0; o < env->max_obstacles; o++){
+ if (o < env->num_obstacles) {
+ Obstacle* obstacle = &env->obstacles[o];
+ env->observations[obs_idx++] = obstacle->type == CACTUS ? 0.0f : 1.0f;
+ env->observations[obs_idx++] = ((obstacle->x - env->agent->x) / env->width);
+ env->observations[obs_idx++] = obstacle->y/(env->height / 2.0f);
+ } else {
+ env->observations[obs_idx++] = -1.0f;
+ env->observations[obs_idx++] = 0.0f;
+ env->observations[obs_idx++] = 0.0f;
+ }
+ }
+}
+
+void c_reset(Dinosaur* env){
+ env->speed = env->speed_init;
+ env->spawn_rate = 1;
+ env->spawn_ticks = 0;
+
+ env->agent->ticks = 0;
+ env->agent->y_velocity = 0.0f;
+ env->agent->y = 0.0f;
+
+ env->num_obstacles = 0;
+ if (env->obstacles != NULL) {
+ free(env->obstacles);
+ env->obstacles = NULL;
+ }
+
+ compute_observations(env);
+}
+
+void process_input(Dinosaur* env){
+ int action = (int)env->actions[0];
+ switch(action){
+ case NOOP:
+ env->agent->y_velocity = -env->agent->jump_strength;
+ env->agent->height = PLAYER_HEIGHT;
+ env->agent->width = PLAYER_WIDTH;
+ env->agent->x_offset = 0.0f;
+ break;
+ case CROUCH:
+ env->agent->y_velocity = -env->agent->jump_strength;
+ env->agent->height = PLAYER_HEIGHT / 2.f;
+ env->agent->width = PLAYER_WIDTH * 2.0f;
+ env->agent->x_offset = PLAYER_WIDTH;
+ break;
+ case JUMP:
+ if(env->agent->y == 0.0f) env->agent->y_velocity = env->agent->jump_strength;
+ env->agent->height = PLAYER_HEIGHT;
+ env->agent->width = PLAYER_WIDTH;
+ env->agent->x_offset = 0.0f;
+ break;
+ }
+}
+
+void process_gravity(Dinosaur* env){
+ env->agent->y_velocity -= env->gravity;
+ env->agent->y += env->agent->y_velocity;
+ if(env->agent->y <= 0){
+ env->agent->y = 0;
+ env->agent->y_velocity = 0;
+ }
+}
+
+void process_obstacle_collisions(Dinosaur* env){
+ float agent_x_max = env->agent->x + env->agent->x_offset;
+ float agent_x_min = agent_x_max - env->agent->width;
+ float agent_y_min = env->agent->y;
+ float agent_y_max = agent_y_min + env->agent->height;
+
+ for(int o = 0; o < env->num_obstacles; o++){
+ Obstacle* obstacle = &env->obstacles[o];
+ obstacle->x -= env->speed;
+
+ float obstacle_x_max = obstacle->x;
+ float obstacle_x_min = obstacle_x_max - obstacle->width;
+ float obstacle_y_min = obstacle->y;
+ float obstacle_y_max = obstacle_y_min + obstacle->height;
+
+ bool colliding_x = ((agent_x_max <= obstacle_x_max && agent_x_max >= obstacle_x_min) || (agent_x_min <= obstacle_x_max && agent_x_min >= obstacle_x_min));
+ bool colliding_y = ((agent_y_max <= obstacle_y_max && agent_y_max >= obstacle_y_min) || (agent_y_min <= obstacle_y_max && agent_y_min >= obstacle_y_min));
+
+ if(colliding_x && colliding_y){
+ *env->terminals = 1.0f;
+ *env->rewards -= 1.0f;
+
+ env->log.episode_return += env->agent->ticks / 100.0f - 1.0f;
+ env->log.episode_length += env->agent->ticks;
+ env->log.score += env->agent->ticks / 100.0f - 1.0f;
+ env->log.perf += env->agent->ticks / 100.0f - 1.0f;
+ env->log.n += 1;
+ c_reset(env);
+ return;
+ }
+
+ bool out_of_bounds = obstacle->x < 0 - obstacle->width;
+ if(out_of_bounds){
+ for(int j = o; j < env->num_obstacles - 1; j++){
+ env->obstacles[j] = env->obstacles[j+1];
+ }
+ env->num_obstacles--;
+ env->obstacles = realloc(env->obstacles, env->num_obstacles * sizeof(Obstacle));
+ o--;
+ }
+ }
+}
+
+void process_obstacle_spawns(Dinosaur* env){
+ bool time_to_spawn = env->spawn_ticks % env->spawn_rate == 0;
+
+ if(time_to_spawn){
+ int spawn_rng = rand_r(&env->rng) % 4 + 1;
+
+ if(spawn_rng < 4){
+ int max_loop = 0;
+ while(spawn_rng + env->num_obstacles >= env->max_obstacles && max_loop < 100){
+ spawn_rng = rand_r(&env->rng) % 3;
+ max_loop++;
+ }
+
+ for(int i = 0; i < spawn_rng; i++){
+ env->num_obstacles++;
+ env->obstacles = realloc(env->obstacles, env->num_obstacles * sizeof(Obstacle));
+ env->obstacles[env->num_obstacles-1] = (Obstacle) {
+ .x = env->width + i * (CACTUS_WIDTH + 10.0f),
+ .y = 0,
+ .width = CACTUS_WIDTH,
+ .height = CACTUS_HEIGHT,
+ .type = CACTUS
+ };
+ }
+ } else if (env->num_obstacles <= env->max_obstacles){
+ env->num_obstacles++;
+ env->obstacles = realloc(env->obstacles, env->num_obstacles * sizeof(Obstacle));
+ env->obstacles[env->num_obstacles-1] = (Obstacle) {
+ .x = env->width + BIRD_WIDTH + 10.0f,
+ .y = BIRD_Y,
+ .width = BIRD_WIDTH,
+ .height = BIRD_HEIGHT,
+ .type = BIRD
+ };
+ }
+ env->spawn_rate = rand() % (env->spawn_rate_max - env->spawn_rate_min) + env->spawn_rate_min;
+ env->spawn_rate = env->spawn_rate / (env->speed / (float) env->speed_init);
+ env->spawn_ticks = 0;
+ }
+}
+
+void c_step(Dinosaur* env){
+ env->agent->ticks += 1;
+ env->spawn_ticks += 1;
+ *env->rewards += 0.01f;
+ *env->terminals = 0.0f;
+
+ process_input(env);
+ process_gravity(env);
+ process_obstacle_collisions(env);
+ process_obstacle_spawns(env);
+
+ if(env->agent->ticks > 0 && env->agent->ticks % env->rate_increment_rate == 0){
+ if(env->speed <= env->speed_max) env->speed+=1;
+ }
+
+ compute_observations(env);
+}
+
+void c_render(Dinosaur* env){
+ if(env->client == NULL) {
+ env->client = make_client(env);
+ }
+
+ if(IsKeyDown(KEY_ESCAPE)) {
+ exit(0);
+ }
+
+ BeginDrawing();
+
+ ClearBackground((Color){255, 255, 255, 255});
+ DrawRectangle(0, env->height/2.0f, env->width, env->height, (Color){95, 87, 79, 255});
+
+ for(int o = 0; o < env->num_obstacles; o++){
+ Obstacle* obstacle = &env->obstacles[o];
+ Texture2D tex;
+ tex = obstacle->type == CACTUS ? env->client->cactus : env->client->bird;
+ DrawTexturePro(
+ tex,
+ (Rectangle){0, 0, obstacle->width, obstacle->height},
+ (Rectangle){
+ obstacle->x - obstacle->width,
+ env->height/2.0f - obstacle->height - obstacle->y,
+ obstacle->width,
+ obstacle->height,
+ },
+ (Vector2){0, 0},
+ 0.0,
+ WHITE
+ );
+ }
+
+ Texture2D tex;
+ int action = (int)env->actions[0];
+ switch(action){
+ case NOOP:
+ case JUMP:
+ tex = env->client->dinosaur_up;
+ break;
+ case CROUCH:
+ tex = env->client->dinosaur_down;
+ break;
+ }
+ DrawTexturePro(
+ tex,
+ (Rectangle){0, 0, env->agent->width, env->agent->height},
+ (Rectangle){
+ env->agent->x - env->agent->width + env->agent->x_offset,
+ env->height/2.0f - env->agent->height - env->agent->y,
+ env->agent->width,
+ env->agent->height
+ },
+ (Vector2){0, 0},
+ 0.0f,
+ WHITE
+ );
+
+ EndDrawing();
+}
+
+void c_close(Dinosaur* env){
+ free(env->agent);
+ free(env->obstacles);
+ if(env->client != NULL){
+ UnloadTexture(env->client->cactus);
+ UnloadTexture(env->client->dinosaur_up);
+ UnloadTexture(env->client->dinosaur_down);
+ CloseWindow();
+ free(env->client);
+ }
+}
\ No newline at end of file
diff --git a/ocean/docking/binding.c b/ocean/docking/binding.c
new file mode 100644
index 0000000000..19b192c4a6
--- /dev/null
+++ b/ocean/docking/binding.c
@@ -0,0 +1,38 @@
+#include "docking.h"
+
+#define OBS_SIZE DOCKING_OBS_SIZE
+#define NUM_ATNS 1
+#define ACT_SIZES {5}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Docking
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->width = (int)dict_get(kwargs, "width")->value;
+ env->height = (int)dict_get(kwargs, "height")->value;
+ env->max_ticks = (int)dict_get(kwargs, "max_ticks")->value;
+ env->max_speed = (float)dict_get(kwargs, "max_speed")->value;
+ env->turn_rate = (float)dict_get(kwargs, "turn_rate")->value;
+ env->accel = (float)dict_get(kwargs, "accel")->value;
+ env->drag = (float)dict_get(kwargs, "drag")->value;
+ env->dock_radius = (float)dict_get(kwargs, "dock_radius")->value;
+ env->dock_speed_threshold = (float)dict_get(kwargs, "dock_speed_threshold")->value;
+ env->dock_heading_threshold = (float)dict_get(kwargs, "dock_heading_threshold")->value;
+ env->step_penalty = (float)dict_get(kwargs, "step_penalty")->value;
+ env->progress_reward_scale = (float)dict_get(kwargs, "progress_reward_scale")->value;
+ c_init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "success_rate", log->success_rate);
+ dict_set(out, "crash_rate", log->crash_rate);
+ dict_set(out, "timeout_rate", log->timeout_rate);
+ dict_set(out, "final_distance", log->final_distance);
+ dict_set(out, "alignment_error", log->alignment_error);
+}
diff --git a/ocean/docking/docking.c b/ocean/docking/docking.c
new file mode 100644
index 0000000000..3dac623a15
--- /dev/null
+++ b/ocean/docking/docking.c
@@ -0,0 +1,64 @@
+#include "docking.h"
+#include "puffernet.h"
+
+int main() {
+ Weights* weights = load_weights("resources/docking/docking_weights.bin");
+ int logit_sizes[1] = {5};
+ PufferNet* net = make_puffernet(weights, 1, DOCKING_OBS_SIZE, 128, 1, logit_sizes, 1);
+
+ Docking env = {0};
+ env.width = 256;
+ env.height = 192;
+ env.max_ticks = 1024;
+ env.max_speed = 6.0f;
+ env.turn_rate = 0.10f;
+ env.accel = 0.55f;
+ env.drag = 0.90f;
+ env.dock_radius = 18.0f;
+ env.dock_speed_threshold = 0.72f;
+ env.dock_heading_threshold = 0.28f;
+ env.step_penalty = -0.01f;
+ env.progress_reward_scale = 0.25f;
+
+ env.observations = (float*)calloc(DOCKING_OBS_SIZE, sizeof(float));
+ env.actions = (float*)calloc(1, sizeof(float));
+ env.rewards = (float*)calloc(1, sizeof(float));
+ env.terminals = (float*)calloc(1, sizeof(float));
+
+ c_init(&env);
+ c_reset(&env);
+ c_render(&env);
+
+ while (!WindowShouldClose()) {
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ env.actions[0] = DOCK_NOOP;
+ if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) {
+ env.actions[0] = DOCK_TURN_LEFT;
+ } else if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) {
+ env.actions[0] = DOCK_TURN_RIGHT;
+ } else if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) {
+ env.actions[0] = DOCK_THRUST;
+ } else if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) {
+ env.actions[0] = DOCK_BRAKE;
+ }
+ } else {
+ forward_puffernet(net, env.observations, env.actions);
+ }
+
+ if (IsKeyPressed(KEY_R)) {
+ c_reset(&env);
+ } else {
+ c_step(&env);
+ }
+ c_render(&env);
+ }
+
+ free_puffernet(net);
+ free(weights);
+ free(env.observations);
+ free(env.actions);
+ free(env.rewards);
+ free(env.terminals);
+ c_close(&env);
+ return 0;
+}
diff --git a/ocean/docking/docking.h b/ocean/docking/docking.h
new file mode 100644
index 0000000000..76cda0ab24
--- /dev/null
+++ b/ocean/docking/docking.h
@@ -0,0 +1,560 @@
+#include
+#include
+#include
+#include "raylib.h"
+
+#define DOCKING_OBS_SIZE 8
+
+const unsigned char DOCK_NOOP = 0;
+const unsigned char DOCK_TURN_LEFT = 1;
+const unsigned char DOCK_TURN_RIGHT = 2;
+const unsigned char DOCK_THRUST = 3;
+const unsigned char DOCK_BRAKE = 4;
+
+const unsigned char DOCK_RESULT_SUCCESS = 0;
+const unsigned char DOCK_RESULT_CRASH = 1;
+const unsigned char DOCK_RESULT_TIMEOUT = 2;
+
+typedef struct {
+ float perf;
+ float score;
+ float episode_return;
+ float episode_length;
+ float success_rate;
+ float crash_rate;
+ float timeout_rate;
+ float final_distance;
+ float alignment_error;
+ float n;
+} Log;
+
+typedef struct {
+ Log log;
+ float* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ int num_agents;
+ int width;
+ int height;
+ int max_ticks;
+ int tick;
+ float ship_x;
+ float ship_y;
+ float ship_heading;
+ float ship_speed;
+ float dock_x;
+ float dock_y;
+ float dock_heading;
+ float prev_distance;
+ float episode_return;
+ float max_speed;
+ float turn_rate;
+ float accel;
+ float drag;
+ float dock_radius;
+ float dock_speed_threshold;
+ float dock_heading_threshold;
+ float step_penalty;
+ float progress_reward_scale;
+ int feedback_timer;
+ unsigned char last_result;
+ unsigned char last_action;
+ unsigned char reset_pending;
+ unsigned int rng;
+} Docking;
+
+static inline float docking_clipf(float val, float min, float max) {
+ if (val < min) return min;
+ if (val > max) return max;
+ return val;
+}
+
+static inline float docking_randf(Docking* env) {
+ return rand_r(&env->rng) / (float)RAND_MAX;
+}
+
+static inline float docking_wrap_angle(float angle) {
+ while (angle < 0.0f) {
+ angle += 2.0f*PI;
+ }
+ while (angle >= 2.0f*PI) {
+ angle -= 2.0f*PI;
+ }
+ return angle;
+}
+
+static inline float docking_angle_error(float a, float b) {
+ float diff = fabsf(docking_wrap_angle(a) - docking_wrap_angle(b));
+ return fminf(diff, 2.0f*PI - diff);
+}
+
+static inline float docking_distance(Docking* env) {
+ float dx = env->dock_x - env->ship_x;
+ float dy = env->dock_y - env->ship_y;
+ return sqrtf(dx*dx + dy*dy);
+}
+
+static inline float docking_diag(Docking* env) {
+ return sqrtf((float)(env->width*env->width + env->height*env->height));
+}
+
+void c_init(Docking* env) {
+ env->num_agents = 1;
+ env->width = env->width < 64 ? 64 : env->width;
+ env->height = env->height < 64 ? 64 : env->height;
+ env->max_ticks = env->max_ticks == 0 ? 1024 : env->max_ticks;
+ if (env->max_ticks > 0 && env->max_ticks < 16) env->max_ticks = 16;
+ env->max_speed = env->max_speed <= 0.0f ? 6.0f : env->max_speed;
+ env->turn_rate = env->turn_rate <= 0.0f ? 0.10f : env->turn_rate;
+ env->accel = env->accel <= 0.0f ? 0.55f : env->accel;
+ env->drag = docking_clipf(env->drag, 0.0f, 1.0f);
+ if (env->drag == 0.0f) env->drag = 0.92f;
+ env->dock_radius = env->dock_radius <= 1.0f ? 24.0f : env->dock_radius;
+ env->dock_speed_threshold = env->dock_speed_threshold <= 0.0f ? 0.75f : env->dock_speed_threshold;
+ env->dock_heading_threshold = env->dock_heading_threshold <= 0.0f ? 0.30f : env->dock_heading_threshold;
+ env->step_penalty = env->step_penalty == 0.0f ? -0.01f : env->step_penalty;
+ env->progress_reward_scale = env->progress_reward_scale == 0.0f ? 0.25f : env->progress_reward_scale;
+ env->feedback_timer = 0;
+ env->last_result = DOCK_RESULT_TIMEOUT;
+ env->last_action = DOCK_NOOP;
+ env->reset_pending = 0;
+}
+
+void compute_observations(Docking* env) {
+ float dx = env->dock_x - env->ship_x;
+ float dy = env->dock_y - env->ship_y;
+ float dist = sqrtf(dx*dx + dy*dy);
+ float diag = docking_diag(env);
+
+ memset(env->observations, 0, DOCKING_OBS_SIZE * sizeof(float));
+ env->observations[0] = dx / env->width;
+ env->observations[1] = dy / env->height;
+ env->observations[2] = cosf(env->ship_heading);
+ env->observations[3] = sinf(env->ship_heading);
+ env->observations[4] = cosf(env->dock_heading);
+ env->observations[5] = sinf(env->dock_heading);
+ env->observations[6] = env->ship_speed / env->max_speed;
+ env->observations[7] = dist / diag;
+}
+
+void add_log(Docking* env, unsigned char result) {
+ float dist = docking_distance(env) / docking_diag(env);
+ float angle_error = docking_angle_error(env->ship_heading, env->dock_heading) / PI;
+
+ env->log.perf += result == DOCK_RESULT_SUCCESS ? 1.0f : 0.0f;
+ env->log.score += env->rewards[0];
+ env->log.episode_return += env->episode_return;
+ env->log.episode_length += env->tick;
+ env->log.success_rate += result == DOCK_RESULT_SUCCESS ? 1.0f : 0.0f;
+ env->log.crash_rate += result == DOCK_RESULT_CRASH ? 1.0f : 0.0f;
+ env->log.timeout_rate += result == DOCK_RESULT_TIMEOUT ? 1.0f : 0.0f;
+ env->log.final_distance += dist;
+ env->log.alignment_error += angle_error;
+ env->log.n += 1.0f;
+}
+
+void c_reset(Docking* env) {
+ float width = (float)env->width;
+ float height = (float)env->height;
+
+ env->tick = 0;
+ env->episode_return = 0.0f;
+
+ env->ship_x = width * (0.10f + 0.20f * docking_randf(env));
+ env->ship_y = height * (0.20f + 0.60f * docking_randf(env));
+ env->ship_heading = docking_wrap_angle((-0.35f + 0.70f * docking_randf(env)) * PI);
+ env->ship_speed = 0.0f;
+ env->last_action = DOCK_NOOP;
+ env->reset_pending = 0;
+
+ env->dock_x = width * (0.65f + 0.20f * docking_randf(env));
+ env->dock_y = height * (0.20f + 0.60f * docking_randf(env));
+ env->dock_heading = 0.0f;
+
+ env->prev_distance = docking_distance(env);
+ compute_observations(env);
+}
+
+void finish_episode(Docking* env, float reward, unsigned char result) {
+ env->rewards[0] = reward;
+ env->terminals[0] = 1.0f;
+ env->episode_return += reward;
+ env->last_result = result;
+ env->feedback_timer = 72;
+ add_log(env, result);
+ env->reset_pending = 1;
+}
+
+void c_step(Docking* env) {
+ if (env->reset_pending) {
+ if (IsWindowReady() && env->feedback_timer > 66) {
+ env->feedback_timer -= 1;
+ return;
+ }
+ c_reset(env);
+ }
+
+ env->tick += 1;
+ if (env->feedback_timer > 0) {
+ env->feedback_timer -= 1;
+ }
+
+ memset(env->observations, 0, DOCKING_OBS_SIZE * sizeof(float));
+ env->rewards[0] = 0.0f;
+ env->terminals[0] = 0.0f;
+
+ int action = (int)env->actions[0];
+ env->last_action = (unsigned char)action;
+ if (action == DOCK_TURN_LEFT) {
+ env->ship_heading -= env->turn_rate;
+ } else if (action == DOCK_TURN_RIGHT) {
+ env->ship_heading += env->turn_rate;
+ }
+ env->ship_heading = docking_wrap_angle(env->ship_heading);
+
+ if (action == DOCK_THRUST) {
+ env->ship_speed += env->accel;
+ } else if (action == DOCK_BRAKE) {
+ env->ship_speed -= 2.2f * env->accel;
+ }
+ env->ship_speed = docking_clipf(env->ship_speed, 0.0f, env->max_speed);
+ env->ship_speed *= env->drag;
+
+ env->ship_x += env->ship_speed * cosf(env->ship_heading);
+ env->ship_y += env->ship_speed * sinf(env->ship_heading);
+
+ int hit_x = 0;
+ int hit_y = 0;
+ if (env->ship_x < 0.0f) {
+ env->ship_x = 0.0f;
+ hit_x = 1;
+ } else if (env->ship_x > env->width) {
+ env->ship_x = (float)env->width;
+ hit_x = 1;
+ }
+ if (env->ship_y < 0.0f) {
+ env->ship_y = 0.0f;
+ hit_y = 1;
+ } else if (env->ship_y > env->height) {
+ env->ship_y = (float)env->height;
+ hit_y = 1;
+ }
+ if (hit_x || hit_y) {
+ if (env->ship_speed > 0.55f * env->max_speed) {
+ finish_episode(env, -1.0f, DOCK_RESULT_CRASH);
+ return;
+ }
+ if (hit_x) {
+ env->ship_heading = PI - env->ship_heading;
+ }
+ if (hit_y) {
+ env->ship_heading = -env->ship_heading;
+ }
+ env->ship_heading = docking_wrap_angle(env->ship_heading);
+ env->ship_speed *= 0.32f;
+ }
+
+ float dist = docking_distance(env);
+ float dist_delta = (env->prev_distance - dist) / docking_diag(env);
+ env->prev_distance = dist;
+
+ float reward = env->step_penalty + env->progress_reward_scale * dist_delta;
+ reward = docking_clipf(reward, -1.0f, 1.0f);
+ env->rewards[0] = reward;
+ env->episode_return += reward;
+
+ float angle_error = docking_angle_error(env->ship_heading, env->dock_heading);
+ float bay_depth = env->dock_radius * 1.75f;
+ float dock_entrance_x = env->dock_x - cosf(env->dock_heading) * bay_depth * 0.50f;
+ float dock_entrance_y = env->dock_y - sinf(env->dock_heading) * bay_depth * 0.50f;
+ float dx_to_entrance = env->ship_x - dock_entrance_x;
+ float dy_to_entrance = env->ship_y - dock_entrance_y;
+ float dist_to_entrance = sqrtf(dx_to_entrance * dx_to_entrance + dy_to_entrance * dy_to_entrance);
+
+ float success_radius = env->dock_radius * 1.40f;
+ float soft_radius = env->dock_radius * 1.60f;
+ float crash_radius = env->dock_radius * 1.01f;
+
+ if (dist_to_entrance <= soft_radius) {
+ int can_dock = (angle_error <= env->dock_heading_threshold + 0.12f
+ && env->ship_speed <= 2.0f * env->dock_speed_threshold);
+ int soft_approach = (env->ship_speed <= 2.6f * env->dock_speed_threshold
+ && angle_error <= env->dock_heading_threshold + 0.35f);
+
+ if (dist_to_entrance <= success_radius && can_dock) {
+ env->ship_x = dock_entrance_x;
+ env->ship_y = dock_entrance_y;
+ env->ship_speed = 0.0f;
+ env->ship_heading = env->dock_heading;
+ finish_episode(env, 1.0f, DOCK_RESULT_SUCCESS);
+ } else if (dist_to_entrance <= crash_radius) {
+ finish_episode(env, -1.0f, DOCK_RESULT_CRASH);
+ } else if (soft_approach) {
+ env->ship_speed *= 0.35f;
+ env->prev_distance = docking_distance(env);
+ compute_observations(env);
+ } else {
+ float inv_dist = dist_to_entrance > 1e-5f ? 1.0f / dist_to_entrance : 0.0f;
+ if (dist_to_entrance <= 1e-5f) {
+ dx_to_entrance = -cosf(env->dock_heading);
+ dy_to_entrance = -sinf(env->dock_heading);
+ inv_dist = 1.0f;
+ }
+ env->ship_x = dock_entrance_x + dx_to_entrance * inv_dist * (soft_radius + 1.5f);
+ env->ship_y = dock_entrance_y + dy_to_entrance * inv_dist * (soft_radius + 1.5f);
+ env->ship_speed *= 0.25f;
+
+ float old_reward = env->rewards[0];
+ env->rewards[0] = docking_clipf(old_reward - 0.10f, -1.0f, 1.0f);
+ env->episode_return += env->rewards[0] - old_reward;
+ env->prev_distance = docking_distance(env);
+ compute_observations(env);
+ }
+ return;
+ }
+
+ if (env->max_ticks > 0 && env->tick >= env->max_ticks) {
+ finish_episode(env, -0.25f, DOCK_RESULT_TIMEOUT);
+ return;
+ }
+
+ compute_observations(env);
+}
+
+void draw_docking_bay(Vector2 center, Vector2 dir, float radius, Color bay_color, Color target_color) {
+ Vector2 side = {-dir.y, dir.x};
+ float bay_half_width = radius * 1.25f;
+ float bay_depth = radius * 1.75f;
+ float target_half_width = radius * 0.72f;
+ float target_depth = radius * 0.95f;
+
+ Vector2 mouth_center = {
+ center.x - dir.x * bay_depth * 0.50f,
+ center.y - dir.y * bay_depth * 0.50f,
+ };
+ Vector2 back_center = {
+ center.x + dir.x * bay_depth * 0.50f,
+ center.y + dir.y * bay_depth * 0.50f,
+ };
+ Vector2 target_center = {
+ center.x + dir.x * bay_depth * 0.10f,
+ center.y + dir.y * bay_depth * 0.10f,
+ };
+
+ Vector2 mouth_left = {
+ mouth_center.x + side.x * bay_half_width,
+ mouth_center.y + side.y * bay_half_width,
+ };
+ Vector2 mouth_right = {
+ mouth_center.x - side.x * bay_half_width,
+ mouth_center.y - side.y * bay_half_width,
+ };
+ Vector2 back_left = {
+ back_center.x + side.x * bay_half_width,
+ back_center.y + side.y * bay_half_width,
+ };
+ Vector2 back_right = {
+ back_center.x - side.x * bay_half_width,
+ back_center.y - side.y * bay_half_width,
+ };
+ Vector2 target_mouth_left = {
+ target_center.x - dir.x * target_depth * 0.50f + side.x * target_half_width,
+ target_center.y - dir.y * target_depth * 0.50f + side.y * target_half_width,
+ };
+ Vector2 target_mouth_right = {
+ target_center.x - dir.x * target_depth * 0.50f - side.x * target_half_width,
+ target_center.y - dir.y * target_depth * 0.50f - side.y * target_half_width,
+ };
+ Vector2 target_back_left = {
+ target_center.x + dir.x * target_depth * 0.50f + side.x * target_half_width,
+ target_center.y + dir.y * target_depth * 0.50f + side.y * target_half_width,
+ };
+ Vector2 target_back_right = {
+ target_center.x + dir.x * target_depth * 0.50f - side.x * target_half_width,
+ target_center.y + dir.y * target_depth * 0.50f - side.y * target_half_width,
+ };
+
+ DrawLineEx(mouth_left, back_left, 6.0f, bay_color);
+ DrawLineEx(mouth_right, back_right, 6.0f, bay_color);
+ DrawLineEx(back_left, back_right, 6.0f, bay_color);
+ DrawTriangle(target_mouth_left, target_mouth_right, target_back_left, Fade(target_color, 0.35f));
+ DrawTriangle(target_mouth_right, target_back_left, target_back_right, Fade(target_color, 0.35f));
+ DrawLineEx(target_mouth_left, target_back_left, 3.0f, target_color);
+ DrawLineEx(target_mouth_right, target_back_right, 3.0f, target_color);
+ DrawLineEx(target_back_left, target_back_right, 3.0f, target_color);
+}
+
+void draw_ship(Vector2 center, Vector2 dir, float ship_len, float ship_wid, Color hull_color) {
+ Vector2 side = {-dir.y, dir.x};
+ Vector2 nose = {
+ center.x + dir.x * ship_len,
+ center.y + dir.y * ship_len,
+ };
+ Vector2 left = {
+ center.x - dir.x * ship_len * 0.55f + side.x * ship_wid,
+ center.y - dir.y * ship_len * 0.55f + side.y * ship_wid,
+ };
+ Vector2 right = {
+ center.x - dir.x * ship_len * 0.55f - side.x * ship_wid,
+ center.y - dir.y * ship_len * 0.55f - side.y * ship_wid,
+ };
+ Vector2 cockpit = {
+ center.x + dir.x * ship_len * 0.18f,
+ center.y + dir.y * ship_len * 0.18f,
+ };
+
+ DrawTriangle(nose, left, right, hull_color);
+ DrawTriangleLines(nose, left, right, RAYWHITE);
+ DrawCircleV(cockpit, ship_wid * 0.22f, (Color){220, 245, 255, 255});
+}
+
+void draw_thruster(Vector2 center, Vector2 dir, float ship_len, float ship_wid) {
+ Vector2 side = {-dir.y, dir.x};
+ Vector2 base = {
+ center.x - dir.x * ship_len * 0.72f,
+ center.y - dir.y * ship_len * 0.72f,
+ };
+ float flame_len = ship_len * 0.90f;
+ Vector2 flame_tip = {
+ base.x - dir.x * flame_len,
+ base.y - dir.y * flame_len,
+ };
+ Vector2 left = {
+ base.x + side.x * ship_wid * 0.32f,
+ base.y + side.y * ship_wid * 0.32f,
+ };
+ Vector2 right = {
+ base.x - side.x * ship_wid * 0.32f,
+ base.y - side.y * ship_wid * 0.32f,
+ };
+ Vector2 inner_tip = {
+ base.x - dir.x * flame_len * 0.55f,
+ base.y - dir.y * flame_len * 0.55f,
+ };
+
+ DrawTriangle(flame_tip, left, right, (Color){255, 140, 40, 220});
+ DrawTriangle(inner_tip, left, right, (Color){255, 220, 100, 220});
+}
+
+void c_render(Docking* env) {
+ if (!IsWindowReady()) {
+ int screen_width = 960;
+ int screen_height = (int)(screen_width * (env->height / (float)env->width));
+ if (screen_height < 540) screen_height = 540;
+ InitWindow(screen_width, screen_height, "PufferLib Docking");
+ SetTargetFPS(60);
+ }
+
+ if (IsKeyDown(KEY_ESCAPE)) {
+ exit(0);
+ }
+
+ float margin = 40.0f;
+ float scale_x = (GetScreenWidth() - 2.0f * margin) / env->width;
+ float scale_y = (GetScreenHeight() - 2.0f * margin) / env->height;
+ float scale = fminf(scale_x, scale_y);
+ float ox = 0.5f * (GetScreenWidth() - env->width * scale);
+ float oy = 0.5f * (GetScreenHeight() - env->height * scale);
+
+ Vector2 dock_center = {
+ ox + env->dock_x * scale,
+ oy + env->dock_y * scale,
+ };
+ Vector2 dock_dir = {
+ cosf(env->dock_heading),
+ sinf(env->dock_heading),
+ };
+ Vector2 ship_center = {
+ ox + env->ship_x * scale,
+ oy + env->ship_y * scale,
+ };
+ Vector2 ship_dir = {
+ cosf(env->ship_heading),
+ sinf(env->ship_heading),
+ };
+
+ float ship_len = fmaxf(24.0f, env->dock_radius * scale * 1.35f);
+ float ship_wid = ship_len * 0.50f;
+ float speed_ratio = env->ship_speed / env->max_speed;
+ float angle_error = docking_angle_error(env->ship_heading, env->dock_heading);
+ Color bay_color = (Color){120, 180, 255, 255};
+ Color target_color = (Color){60, 255, 140, 255};
+ Color ship_color = (Color){0, 205, 215, 255};
+
+ BeginDrawing();
+ ClearBackground((Color){6, 24, 24, 255});
+ if (env->feedback_timer > 0) {
+ float alpha = env->feedback_timer / 72.0f;
+ if (env->last_result == DOCK_RESULT_SUCCESS) {
+ DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade((Color){40, 170, 90, 255}, 0.28f * alpha));
+ } else if (env->last_result == DOCK_RESULT_CRASH) {
+ DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade((Color){180, 40, 40, 255}, 0.12f * alpha));
+ }
+ }
+
+ DrawRectangleLines(
+ (int)ox, (int)oy,
+ (int)(env->width * scale), (int)(env->height * scale),
+ (Color){60, 110, 110, 255}
+ );
+
+ draw_docking_bay(dock_center, dock_dir, env->dock_radius * scale, bay_color, target_color);
+ DrawLineEx(
+ (Vector2){
+ dock_center.x - dock_dir.x * env->dock_radius * scale * 2.4f,
+ dock_center.y - dock_dir.y * env->dock_radius * scale * 2.4f,
+ },
+ dock_center,
+ 2.0f,
+ Fade(target_color, 0.55f)
+ );
+
+ if (env->last_action == DOCK_THRUST && env->ship_speed > 0.05f) {
+ draw_thruster(ship_center, ship_dir, ship_len, ship_wid);
+ }
+ draw_ship(ship_center, ship_dir, ship_len, ship_wid, ship_color);
+
+ DrawRectangle(20, 18, 220, 118, Fade(BLACK, 0.25f));
+ DrawText("Dock slowly into the green bay", 32, 28, 22, RAYWHITE);
+ DrawText(TextFormat("speed %.2f / %.2f", env->ship_speed, env->max_speed), 32, 58, 20, RAYWHITE);
+ DrawText(TextFormat("align %.2f", angle_error), 32, 84, 20, RAYWHITE);
+ if (env->max_ticks > 0) {
+ DrawText(TextFormat("steps %d / %d", env->tick, env->max_ticks), 32, 110, 20, RAYWHITE);
+ } else {
+ DrawText(TextFormat("steps %d", env->tick), 32, 110, 20, RAYWHITE);
+ }
+
+ DrawRectangle(32, GetScreenHeight() - 36, 180, 12, Fade(RAYWHITE, 0.15f));
+ DrawRectangle(32, GetScreenHeight() - 36, (int)(180.0f * speed_ratio), 12, ship_color);
+
+ if (env->feedback_timer > 0) {
+ const char* text = "TIMEOUT";
+ Color text_color = (Color){255, 220, 120, 255};
+ if (env->last_result == DOCK_RESULT_SUCCESS) {
+ text = "DOCKED";
+ text_color = (Color){60, 255, 140, 255};
+ } else if (env->last_result == DOCK_RESULT_CRASH) {
+ text = "CRASH";
+ text_color = (Color){255, 90, 90, 255};
+ }
+ int font_size = env->last_result == DOCK_RESULT_SUCCESS ? 58 : 42;
+ int text_width = MeasureText(text, font_size);
+ DrawRectangle(
+ GetScreenWidth()/2 - text_width/2 - 24,
+ 18,
+ text_width + 48,
+ env->last_result == DOCK_RESULT_SUCCESS ? 78 : 60,
+ Fade(BLACK, 0.35f)
+ );
+ DrawText(text, GetScreenWidth()/2 - text_width/2, env->last_result == DOCK_RESULT_SUCCESS ? 28 : 30, font_size, text_color);
+ }
+
+ EndDrawing();
+}
+
+void c_close(Docking* env) {
+ if (IsWindowReady()) {
+ CloseWindow();
+ }
+}
diff --git a/ocean/double_pendulum/binding.c b/ocean/double_pendulum/binding.c
new file mode 100644
index 0000000000..60360fde8b
--- /dev/null
+++ b/ocean/double_pendulum/binding.c
@@ -0,0 +1,36 @@
+#include "double_pendulum.h"
+
+#define OBS_SIZE DP_OBS_SIZE
+#define NUM_ATNS 1
+#define ACT_SIZES {DP_ACTIONS}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env DoublePendulum
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->cart_mass = dict_get(kwargs, "cart_mass")->value;
+ env->link1_mass = dict_get(kwargs, "link1_mass")->value;
+ env->link2_mass = dict_get(kwargs, "link2_mass")->value;
+ env->link1_length = dict_get(kwargs, "link1_length")->value;
+ env->link2_length = dict_get(kwargs, "link2_length")->value;
+ env->gravity = dict_get(kwargs, "gravity")->value;
+ env->force_mag = dict_get(kwargs, "force_mag")->value;
+ env->dt = dict_get(kwargs, "dt")->value;
+ env->substeps = (int)dict_get(kwargs, "substeps")->value;
+ env->balance_bonus_weight = dict_get(kwargs, "balance_bonus_weight")->value;
+ if (env->substeps < 1) env->substeps = 1;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "score", log->score);
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "x_threshold_termination", log->x_threshold_termination);
+ dict_set(out, "max_steps_termination", log->max_steps_termination);
+ dict_set(out, "hold_time", log->hold_time);
+ dict_set(out, "n", log->n);
+}
diff --git a/ocean/double_pendulum/double_pendulum.c b/ocean/double_pendulum/double_pendulum.c
new file mode 100644
index 0000000000..554364de96
--- /dev/null
+++ b/ocean/double_pendulum/double_pendulum.c
@@ -0,0 +1,40 @@
+#include "double_pendulum.h"
+
+int main(void) {
+ float observations[DP_OBS_SIZE] = {0};
+ float actions[1] = {0};
+ float rewards[1] = {0};
+ float terminals[1] = {0};
+
+ DoublePendulum env = {
+ .observations = observations,
+ .actions = actions,
+ .rewards = rewards,
+ .terminals = terminals,
+ .num_agents = 1,
+ .rng = 1,
+ .cart_mass = 1.0f,
+ .link1_mass = 0.1f,
+ .link2_mass = 0.1f,
+ .link1_length = 0.5f,
+ .link2_length = 0.5f,
+ .gravity = 9.8f,
+ .force_mag = 10.0f,
+ .dt = 0.02f,
+ .substeps = 4,
+ .balance_bonus_weight = 0.5f,
+ };
+
+ init(&env);
+ c_reset(&env);
+ c_render(&env);
+ while (!WindowShouldClose()) {
+ if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) actions[0] = 0;
+ else if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) actions[0] = 2;
+ else actions[0] = 1;
+ c_step(&env);
+ c_render(&env);
+ }
+ c_close(&env);
+ return 0;
+}
diff --git a/ocean/double_pendulum/double_pendulum.h b/ocean/double_pendulum/double_pendulum.h
new file mode 100644
index 0000000000..666962d47d
--- /dev/null
+++ b/ocean/double_pendulum/double_pendulum.h
@@ -0,0 +1,358 @@
+// Double pendulum swing-up and balance task with discrete cart forces.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include "raylib.h"
+
+#define DP_OBS_SIZE 8
+#define DP_ACTIONS 3
+#define DP_MAX_STEPS 600
+#define DP_X_THRESHOLD 5.0f
+#define DP_WIDTH 800
+#define DP_HEIGHT 420
+#define DP_SCALE 65.0f
+
+// DeepMind Control Suite-style dense reward constants.
+#define DP_CENTER_MARGIN 2.0f
+#define DP_ANG_VEL_MARGIN 5.0f
+#define DP_VELOCITY_FLOOR 0.60f
+#define DP_LN_10 2.302585093f
+#define DP_BALANCE_ANGLE_SCALE 0.30f
+#define DP_BALANCE_ANG_VEL_SCALE 1.0f
+#define DP_BALANCE_CART_SCALE 2.0f
+
+typedef struct Log {
+ float perf;
+ float score;
+ float episode_return;
+ float episode_length;
+ float x_threshold_termination;
+ float max_steps_termination;
+ float hold_time;
+ float n;
+} Log;
+
+typedef struct DoublePendulum {
+ float* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ int num_agents;
+ unsigned int rng;
+ Log log;
+
+ float x;
+ float x_dot;
+ float theta1;
+ float theta1_dot;
+ float theta2;
+ float theta2_dot;
+ int tick;
+ float episode_return;
+ int upright_steps;
+ int max_upright_steps;
+
+ float cart_mass;
+ float link1_mass;
+ float link2_mass;
+ float link1_length;
+ float link2_length;
+ float gravity;
+ float force_mag;
+ float dt;
+ int substeps; // physics substeps per control step (RK4)
+ float balance_bonus_weight; // blend between deepmind term and balance_quality
+} DoublePendulum;
+
+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, 255};
+const Color PUFF_BACKGROUND = (Color){6, 24, 24, 255};
+const Color PUFF_YELLOW = (Color){245, 197, 66, 255};
+
+static inline float dp_randf(DoublePendulum* env, float lo, float hi) {
+ float t = (float)rand_r(&env->rng) / (float)RAND_MAX;
+ return lo + t * (hi - lo);
+}
+
+static inline float wrap_pi(float x) {
+ while (x > M_PI) x -= 2.0f * M_PI;
+ while (x < -M_PI) x += 2.0f * M_PI;
+ return x;
+}
+
+void compute_observations(DoublePendulum* env) {
+ env->observations[0] = env->x / DP_X_THRESHOLD;
+ env->observations[1] = env->x_dot / 5.0f;
+ env->observations[2] = sinf(env->theta1);
+ env->observations[3] = cosf(env->theta1);
+ env->observations[4] = env->theta1_dot / 8.0f;
+ env->observations[5] = sinf(env->theta2);
+ env->observations[6] = cosf(env->theta2);
+ env->observations[7] = env->theta2_dot / 8.0f;
+}
+
+void add_log(DoublePendulum* env, bool x_done, bool timeout) {
+ float normalized = env->episode_return / (float)DP_MAX_STEPS;
+ env->log.perf += fminf(fmaxf(normalized, 0.0f), 1.0f);
+ env->log.score += env->episode_return;
+ env->log.episode_return += env->episode_return;
+ env->log.episode_length += (float)env->tick;
+ env->log.x_threshold_termination += x_done ? 1.0f : 0.0f;
+ env->log.max_steps_termination += timeout ? 1.0f : 0.0f;
+ env->log.hold_time += (float)env->max_upright_steps;
+ env->log.n += 1.0f;
+}
+
+void init(DoublePendulum* env) {
+ env->num_agents = 1;
+}
+
+void c_reset(DoublePendulum* env) {
+ env->x = dp_randf(env, -0.04f, 0.04f);
+ env->x_dot = dp_randf(env, -0.04f, 0.04f);
+ env->theta1 = M_PI + dp_randf(env, -0.08f, 0.08f);
+ env->theta1_dot = dp_randf(env, -0.04f, 0.04f);
+ env->theta2 = M_PI + dp_randf(env, -0.08f, 0.08f);
+ env->theta2_dot = dp_randf(env, -0.04f, 0.04f);
+ env->tick = 0;
+ env->episode_return = 0.0f;
+ env->upright_steps = 0;
+ env->max_upright_steps = 0;
+ compute_observations(env);
+}
+
+static void solve_3x3(float A[3][3], float b[3], float x[3]) {
+ for (int i = 0; i < 3; i++) {
+ int pivot = i;
+ float best = fabsf(A[i][i]);
+ for (int r = i + 1; r < 3; r++) {
+ float v = fabsf(A[r][i]);
+ if (v > best) {
+ best = v;
+ pivot = r;
+ }
+ }
+ if (pivot != i) {
+ for (int c = i; c < 3; c++) {
+ float tmp = A[i][c];
+ A[i][c] = A[pivot][c];
+ A[pivot][c] = tmp;
+ }
+ float tmp = b[i];
+ b[i] = b[pivot];
+ b[pivot] = tmp;
+ }
+
+ float inv = 1.0f / A[i][i];
+ for (int c = i; c < 3; c++) A[i][c] *= inv;
+ b[i] *= inv;
+ for (int r = 0; r < 3; r++) {
+ if (r == i) continue;
+ float f = A[r][i];
+ for (int c = i; c < 3; c++) A[r][c] -= f * A[i][c];
+ b[r] -= f * b[i];
+ }
+ }
+ x[0] = b[0];
+ x[1] = b[1];
+ x[2] = b[2];
+}
+
+// Acceleration is a pure function of the angles, angular velocities and force
+// (it does not depend on cart position/velocity), so RK4 can evaluate it at
+// trial states. qdd = [xdd, th1dd, th2dd].
+static void dp_accel(DoublePendulum* env, const float th[2], const float w[2],
+ float force, float qdd[3]) {
+ float m0 = env->cart_mass;
+ float m1 = env->link1_mass;
+ float m2 = env->link2_mass;
+ float l1 = env->link1_length;
+ float l2 = env->link2_length;
+ float t1 = th[0];
+ float t2 = th[1];
+ float w1 = w[0];
+ float w2 = w[1];
+ float c1 = cosf(t1);
+ float c2 = cosf(t2);
+ float s1 = sinf(t1);
+ float s2 = sinf(t2);
+ float c12 = cosf(t1 - t2);
+ float s12 = sinf(t1 - t2);
+
+ float A[3][3] = {
+ {m0 + m1 + m2, (m1 + m2) * l1 * c1, m2 * l2 * c2},
+ {(m1 + m2) * l1 * c1, (m1 + m2) * l1 * l1, m2 * l1 * l2 * c12},
+ {m2 * l2 * c2, m2 * l1 * l2 * c12, m2 * l2 * l2},
+ };
+ float b[3] = {
+ force + (m1 + m2) * l1 * s1 * w1 * w1 + m2 * l2 * s2 * w2 * w2,
+ (m1 + m2) * env->gravity * l1 * s1 - m2 * l1 * l2 * s12 * w2 * w2,
+ m2 * env->gravity * l2 * s2 + m2 * l1 * l2 * s12 * w1 * w1,
+ };
+ solve_3x3(A, b, qdd);
+}
+
+// q = [x, theta1, theta2], v = [x_dot, theta1_dot, theta2_dot].
+static void dp_state_deriv(DoublePendulum* env, const float q[3], const float v[3],
+ float force, float dq[3], float dv[3]) {
+ for (int i = 0; i < 3; i++) dq[i] = v[i];
+ float th[2] = {q[1], q[2]};
+ float w[2] = {v[1], v[2]};
+ dp_accel(env, th, w, force, dv);
+}
+
+static void dp_rk4_step(DoublePendulum* env, float force, float h,
+ float q[3], float v[3]) {
+ float k1q[3], k1v[3], k2q[3], k2v[3], k3q[3], k3v[3], k4q[3], k4v[3];
+ float tq[3], tv[3];
+ dp_state_deriv(env, q, v, force, k1q, k1v);
+ for (int i = 0; i < 3; i++) { tq[i] = q[i] + 0.5f*h*k1q[i]; tv[i] = v[i] + 0.5f*h*k1v[i]; }
+ dp_state_deriv(env, tq, tv, force, k2q, k2v);
+ for (int i = 0; i < 3; i++) { tq[i] = q[i] + 0.5f*h*k2q[i]; tv[i] = v[i] + 0.5f*h*k2v[i]; }
+ dp_state_deriv(env, tq, tv, force, k3q, k3v);
+ for (int i = 0; i < 3; i++) { tq[i] = q[i] + h*k3q[i]; tv[i] = v[i] + h*k3v[i]; }
+ dp_state_deriv(env, tq, tv, force, k4q, k4v);
+ for (int i = 0; i < 3; i++) {
+ q[i] += (h/6.0f) * (k1q[i] + 2.0f*k2q[i] + 2.0f*k3q[i] + k4q[i]);
+ v[i] += (h/6.0f) * (k1v[i] + 2.0f*k2v[i] + 2.0f*k3v[i] + k4v[i]);
+ }
+}
+
+void integrate_physics(DoublePendulum* env, float force) {
+ float q[3] = {env->x, env->theta1, env->theta2};
+ float v[3] = {env->x_dot, env->theta1_dot, env->theta2_dot};
+ int substeps = env->substeps > 0 ? env->substeps : 1;
+ float h = env->dt / (float)substeps;
+ for (int s = 0; s < substeps; s++) {
+ dp_rk4_step(env, force, h, q, v);
+ v[0] = fminf(fmaxf(v[0], -20.0f), 20.0f);
+ v[1] = fminf(fmaxf(v[1], -30.0f), 30.0f);
+ v[2] = fminf(fmaxf(v[2], -30.0f), 30.0f);
+ }
+ env->x = q[0];
+ env->x_dot = v[0];
+ env->theta1 = wrap_pi(q[1]);
+ env->theta1_dot = v[1];
+ env->theta2 = wrap_pi(q[2]);
+ env->theta2_dot = v[2];
+}
+
+float upright_reward(DoublePendulum* env, float force) {
+ float tip_y = env->link1_length * cosf(env->theta1)
+ + env->link2_length * cosf(env->theta2);
+ float max_y = env->link1_length + env->link2_length;
+ float height = 0.5f * (tip_y / max_y + 1.0f);
+
+ bool stable = height > 0.9f
+ && fabsf(env->theta1_dot) < 1.5f
+ && fabsf(env->theta2_dot) < 1.5f
+ && fabsf(env->x_dot) < 1.0f;
+ if (stable) env->upright_steps += 1;
+ else env->upright_steps = 0;
+ if (env->upright_steps > env->max_upright_steps) {
+ env->max_upright_steps = env->upright_steps;
+ }
+
+ // Reward: (1-w) * [h * centered * small_control * small_velocity] + w * balance_quality
+ float h = fminf(fmaxf(height, 0.0f), 1.0f);
+ float sw1 = env->theta1_dot / DP_ANG_VEL_MARGIN;
+ float sw2 = env->theta2_dot / DP_ANG_VEL_MARGIN;
+ float min_vel_tol = fminf(expf(-DP_LN_10 * sw1 * sw1),
+ expf(-DP_LN_10 * sw2 * sw2));
+ float small_velocity = DP_VELOCITY_FLOOR
+ + (1.0f - DP_VELOCITY_FLOOR) * min_vel_tol;
+ float scaled_x = env->x / DP_CENTER_MARGIN;
+ float centered = 0.5f * (1.0f + expf(-DP_LN_10 * scaled_x * scaled_x));
+ float a = env->force_mag > 0.0f ? force / env->force_mag : 0.0f;
+ float small_control = 0.2f * (4.0f + fmaxf(0.0f, 1.0f - a * a));
+ float deepmind = h * centered * small_control * small_velocity;
+ float angle_mse = 0.5f * (env->theta1 * env->theta1
+ + env->theta2 * env->theta2);
+ float ang_vel_mse = 0.5f * (env->theta1_dot * env->theta1_dot
+ + env->theta2_dot * env->theta2_dot);
+ float balance_x = env->x / DP_BALANCE_CART_SCALE;
+ float balance_quality = expf(
+ -angle_mse / (DP_BALANCE_ANGLE_SCALE * DP_BALANCE_ANGLE_SCALE)
+ -ang_vel_mse / (DP_BALANCE_ANG_VEL_SCALE * DP_BALANCE_ANG_VEL_SCALE)
+ -balance_x * balance_x);
+ float w = env->balance_bonus_weight;
+ return (1.0f - w) * deepmind + w * balance_quality;
+}
+
+void c_step(DoublePendulum* env) {
+ float a = env->actions[0];
+ if (!isfinite(a)) a = 1.0f;
+ int action = (int)a;
+ if ((unsigned)action >= DP_ACTIONS) action = 1;
+ float force = 0.0f;
+ if (action == 0) force = -env->force_mag;
+ else if (action == 2) force = env->force_mag;
+
+ integrate_physics(env, force);
+ env->tick += 1;
+
+ bool invalid = !isfinite(env->x) || !isfinite(env->x_dot)
+ || !isfinite(env->theta1) || !isfinite(env->theta1_dot)
+ || !isfinite(env->theta2) || !isfinite(env->theta2_dot);
+ bool x_done = env->x < -DP_X_THRESHOLD || env->x > DP_X_THRESHOLD;
+ bool timeout = env->tick >= DP_MAX_STEPS;
+ bool done = invalid || x_done || timeout;
+ env->rewards[0] = upright_reward(env, force);
+ env->episode_return += env->rewards[0];
+ env->terminals[0] = (invalid || x_done) ? 1.0f : 0.0f;
+
+ if (done) {
+ add_log(env, invalid || x_done, timeout);
+ c_reset(env);
+ return;
+ }
+ compute_observations(env);
+}
+
+void c_render(DoublePendulum* env) {
+ if (!IsWindowReady()) {
+ InitWindow(DP_WIDTH, DP_HEIGHT, "PufferLib Double Pendulum");
+ SetTargetFPS(30);
+ }
+ if (IsKeyDown(KEY_ESCAPE)) exit(0);
+ if (IsKeyPressed(KEY_TAB)) ToggleFullscreen();
+ if (!isfinite(env->x) || !isfinite(env->theta1) || !isfinite(env->theta2)) return;
+
+ float rail_y = DP_HEIGHT * 0.72f;
+ float cart_x = DP_WIDTH / 2.0f + env->x * DP_SCALE;
+ cart_x = fminf(fmaxf(cart_x, 32.0f), DP_WIDTH - 32.0f);
+ float cart_y = rail_y - 16.0f;
+ float l1 = env->link1_length * 2.0f * DP_SCALE;
+ float l2 = env->link2_length * 2.0f * DP_SCALE;
+ Vector2 p0 = {cart_x, cart_y};
+ Vector2 p1 = {cart_x + sinf(env->theta1) * l1, cart_y - cosf(env->theta1) * l1};
+ Vector2 p2 = {p1.x + sinf(env->theta2) * l2, p1.y - cosf(env->theta2) * l2};
+
+ BeginDrawing();
+ ClearBackground(PUFF_BACKGROUND);
+ DrawLine(0, (int)rail_y, DP_WIDTH, (int)rail_y, PUFF_CYAN);
+ DrawRectangle((int)(cart_x - 28), (int)(cart_y - 12), 56, 24, PUFF_CYAN);
+ DrawLineEx(p0, p1, 7.0f, PUFF_RED);
+ DrawLineEx(p1, p2, 6.0f, PUFF_YELLOW);
+ DrawCircleV(p0, 8.0f, PUFF_WHITE);
+ DrawCircleV(p1, 8.0f, PUFF_WHITE);
+ DrawCircleV(p2, 10.0f, PUFF_WHITE);
+ DrawText(TextFormat("steps %d return %.1f hold %d/%d",
+ env->tick, env->episode_return, env->upright_steps, env->max_upright_steps),
+ 20, 20, 20, PUFF_WHITE);
+ DrawText(TextFormat("x %.2f theta1 %.1f theta2 %.1f",
+ env->x, env->theta1 * 180.0f / M_PI, env->theta2 * 180.0f / M_PI),
+ 20, 48, 20, PUFF_WHITE);
+ EndDrawing();
+}
+
+void c_close(DoublePendulum* env) {
+ if (IsWindowReady()) {
+ CloseWindow();
+ }
+}
diff --git a/ocean/drive/binding.c b/ocean/drive/binding.c
new file mode 100644
index 0000000000..e68a04448e
--- /dev/null
+++ b/ocean/drive/binding.c
@@ -0,0 +1,155 @@
+#include "drive.h"
+#define NUM_ATNS 2
+#define ACT_SIZES {7, 13}
+#define OBS_TENSOR_T FloatTensor
+
+#define MAP_BINARY_DIR "drive_data/binaries"
+
+#define MY_VEC_INIT
+#define Env Drive
+#include "vecenv.h"
+
+Env* my_vec_init(int* num_envs_out, int* buffer_env_starts, int* buffer_env_counts, Dict* vec_kwargs, Dict* env_kwargs) {
+ int total_agents = (int)dict_get(vec_kwargs, "total_agents")->value;
+ int num_buffers = (int)dict_get(vec_kwargs, "num_buffers")->value;
+ int num_maps = (int)dict_get(env_kwargs, "num_maps")->value;
+ int agents_per_buffer = total_agents / num_buffers;
+
+ float reward_vehicle_collision = dict_get(env_kwargs, "reward_vehicle_collision")->value;
+ float reward_offroad_collision = dict_get(env_kwargs, "reward_offroad_collision")->value;
+ float reward_goal_post_respawn = dict_get(env_kwargs, "reward_goal_post_respawn")->value;
+ float reward_vehicle_collision_post_respawn = dict_get(env_kwargs, "reward_vehicle_collision_post_respawn")->value;
+ int human_agent_idx = (int)dict_get(env_kwargs, "human_agent_idx")->value;
+
+ // Verify that the path has valid binaries
+ char first_map[512];
+ snprintf(first_map, sizeof(first_map), "%s/map_%03d.bin", MAP_BINARY_DIR, 0);
+ FILE* test_fp = fopen(first_map, "rb");
+ if (!test_fp) {
+ printf("ERROR: Cannot find map files at %s/\n", MAP_BINARY_DIR);
+ *num_envs_out = 0;
+ return NULL;
+ }
+ fclose(test_fp);
+
+ // Scan all maps for agent counts; collect valid (>0) ones
+ int agents_per_map[num_maps];
+ int valid_map_ids[num_maps];
+ int num_valid_maps = 0;
+ for (int m = 0; m < num_maps; m++) {
+ char map_file[512];
+ snprintf(map_file, sizeof(map_file), "%s/map_%03d.bin", MAP_BINARY_DIR, m);
+ Env temp_env = {0};
+ temp_env.map_name = map_file;
+ init(&temp_env);
+ agents_per_map[m] = temp_env.active_agent_count < MAX_AGENTS
+ ? temp_env.active_agent_count : MAX_AGENTS;
+ c_close(&temp_env);
+ if (agents_per_map[m] > 0) {
+ valid_map_ids[num_valid_maps++] = m;
+ }
+ }
+ printf("Scanned %d maps from %s/, %d valid\n", num_maps, MAP_BINARY_DIR, num_valid_maps);
+
+ if (num_valid_maps == 0) {
+ printf("ERROR: No valid maps found\n");
+ *num_envs_out = 0;
+ return NULL;
+ }
+
+ // Build per-env layout. Each buffer advances the global cursor so different
+ // buffers get different maps. If the next full map would overflow a buffer,
+ // pack remaining slots with 1-agent envs advancing the cursor each time.
+ int max_envs = agents_per_buffer * num_buffers; // upper bound (all 1-agent)
+ int* env_map_ids = (int*)malloc(max_envs * sizeof(int));
+ int* env_max_agents = (int*)malloc(max_envs * sizeof(int));
+ int total_envs = 0;
+ int cursor = 0; // advances across buffers
+
+ for (int b = 0; b < num_buffers; b++) {
+ buffer_env_starts[b] = total_envs;
+ int buffer_agents = 0;
+ while (buffer_agents < agents_per_buffer) {
+ int m = valid_map_ids[cursor % num_valid_maps];
+ int cap = agents_per_map[m];
+ int remaining = agents_per_buffer - buffer_agents;
+ if (cap <= remaining) {
+ // Full map fits
+ env_map_ids[total_envs] = m;
+ env_max_agents[total_envs] = cap;
+ buffer_agents += cap;
+ total_envs++;
+ cursor++;
+ } else {
+ // Pack remaining slots as 1-agent envs, one map each
+ while (buffer_agents < agents_per_buffer) {
+ int mm = valid_map_ids[cursor % num_valid_maps];
+ env_map_ids[total_envs] = mm;
+ env_max_agents[total_envs] = 1;
+ buffer_agents++;
+ total_envs++;
+ cursor++;
+ }
+ }
+ }
+ buffer_env_counts[b] = total_envs - buffer_env_starts[b];
+ }
+
+ printf("total envs: %d (%d maps cycled)\n", total_envs, cursor);
+
+ // Initialize all envs
+ Env* envs = (Env*)calloc(total_envs, sizeof(Env));
+ for (int i = 0; i < total_envs; i++) {
+ char map_file[512];
+ snprintf(map_file, sizeof(map_file), "%s/map_%03d.bin", MAP_BINARY_DIR, env_map_ids[i]);
+ Env* env = &envs[i];
+ memset(env, 0, sizeof(Env));
+ env->map_name = strdup(map_file);
+ env->human_agent_idx = human_agent_idx;
+ env->reward_vehicle_collision = reward_vehicle_collision;
+ env->reward_offroad_collision = reward_offroad_collision;
+ env->reward_goal_post_respawn = reward_goal_post_respawn;
+ env->reward_vehicle_collision_post_respawn = reward_vehicle_collision_post_respawn;
+ env->max_agents = env_max_agents[i];
+ init(env);
+ env->num_agents = env->active_agent_count;
+ }
+
+ free(env_map_ids);
+ free(env_max_agents);
+
+ printf("Created %d envs, %d total agents (target %d)\n",
+ total_envs, total_agents, total_agents);
+
+ *num_envs_out = total_envs;
+ return envs;
+}
+
+void my_init(Env* env, Dict* kwargs) {
+ env->human_agent_idx = dict_get(kwargs, "human_agent_idx")->value;
+ env->reward_vehicle_collision = dict_get(kwargs, "reward_vehicle_collision")->value;
+ env->reward_offroad_collision = dict_get(kwargs, "reward_offroad_collision")->value;
+ env->reward_goal_post_respawn = dict_get(kwargs, "reward_goal_post_respawn")->value;
+ env->reward_vehicle_collision_post_respawn = dict_get(kwargs, "reward_vehicle_collision_post_respawn")->value;
+ int map_id = dict_get(kwargs, "map_id")->value;
+ int max_agents = dict_get(kwargs, "max_agents")->value;
+
+ char map_file[512];
+ snprintf(map_file, sizeof(map_file), "%s/map_%03d.bin", MAP_BINARY_DIR, map_id);
+ env->num_agents = max_agents;
+ env->map_name = strdup(map_file);
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "offroad_rate", log->offroad_rate);
+ dict_set(out, "collision_rate", log->collision_rate);
+ dict_set(out, "dnf_rate", log->dnf_rate);
+ dict_set(out, "n", log->n);
+ dict_set(out, "completion_rate", log->completion_rate);
+ dict_set(out, "clean_collision_rate", log->clean_collision_rate);
+}
diff --git a/ocean/drive/dataset.py b/ocean/drive/dataset.py
new file mode 100644
index 0000000000..bf59f27705
--- /dev/null
+++ b/ocean/drive/dataset.py
@@ -0,0 +1,279 @@
+"""Convert Waymo Open Motion Dataset (WOMD) JSON maps to binary format for PufferLib drive env.
+
+Step 0: Download the preprocessed JSON scenarios from HuggingFace e.g.,
+ https://huggingface.co/datasets/daphne-cornelisse/pufferdrive_womd_train_1000
+
+ uv pip install huggingface_hub
+ python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='daphne-cornelisse/pufferdrive_womd_train_1000', repo_type='dataset', local_dir='drive_data')"
+
+Step 1: Unzip to get folder with .json files
+ mkdir -p drive_data/training
+ tar xzf drive_data/pufferdrive_womd_train_1000.tar.gz --strip-components=1 -C drive_data/training/
+
+Step 2: Process to map binaries
+ python ocean/drive/dataset.py --data_folder drive_data/training --output_dir drive_data/binaries
+"""
+
+import json
+import struct
+import os
+from multiprocessing import Pool, cpu_count
+from pathlib import Path
+from tqdm import tqdm
+
+TRAJECTORY_LENGTH = 91
+
+
+def calculate_area(p1, p2, p3):
+ """Calculate the area of the triangle using the determinant method."""
+ return 0.5 * abs(
+ (p1["x"] - p3["x"]) * (p2["y"] - p1["y"])
+ - (p1["x"] - p2["x"]) * (p3["y"] - p1["y"])
+ )
+
+
+def dist(a, b):
+ dx = a["x"] - b["x"]
+ dy = a["y"] - b["y"]
+ return dx * dx + dy * dy
+
+
+def simplify_polyline(geometry, polyline_reduction_threshold, max_segment_length):
+ """Simplify the given polyline using a method inspired by Visvalingham-Whyatt."""
+ num_points = len(geometry)
+ if num_points < 3:
+ return geometry
+
+ skip = [False] * num_points
+ skip_changed = True
+
+ while skip_changed:
+ skip_changed = False
+ k = 0
+ while k < num_points - 1:
+ k_1 = k + 1
+ while k_1 < num_points - 1 and skip[k_1]:
+ k_1 += 1
+ if k_1 >= num_points - 1:
+ break
+
+ k_2 = k_1 + 1
+ while k_2 < num_points and skip[k_2]:
+ k_2 += 1
+ if k_2 >= num_points:
+ break
+
+ point1 = geometry[k]
+ point2 = geometry[k_1]
+ point3 = geometry[k_2]
+ area = calculate_area(point1, point2, point3)
+ if (
+ area < polyline_reduction_threshold
+ and dist(point1, point3) <= max_segment_length
+ ):
+ skip[k_1] = True
+ skip_changed = True
+ k = k_2
+ else:
+ k = k_1
+
+ return [geometry[i] for i in range(num_points) if not skip[i]]
+
+
+def save_map_binary(map_data, output_file, unique_map_id):
+ """Save map data in a binary format readable by C."""
+ with open(output_file, "wb") as f:
+ num_objects = len(map_data.get("objects", []))
+ num_roads = len(map_data.get("roads", []))
+ f.write(struct.pack("i", num_objects))
+ f.write(struct.pack("i", num_roads))
+
+ # Write objects
+ for obj in map_data.get("objects", []):
+ obj_type = obj.get("type", 1)
+ if obj_type == "vehicle":
+ obj_type = 1
+ elif obj_type == "pedestrian":
+ obj_type = 2
+ elif obj_type == "cyclist":
+ obj_type = 3
+ f.write(struct.pack("i", obj_type))
+ f.write(struct.pack("i", TRAJECTORY_LENGTH))
+
+ positions = obj.get("position", [])
+ for coord in ["x", "y", "z"]:
+ for i in range(TRAJECTORY_LENGTH):
+ pos = (
+ positions[i]
+ if i < len(positions)
+ else {"x": 0.0, "y": 0.0, "z": 0.0}
+ )
+ f.write(struct.pack("f", float(pos.get(coord, 0.0))))
+
+ velocities = obj.get("velocity", [])
+ for coord in ["x", "y", "z"]:
+ for i in range(TRAJECTORY_LENGTH):
+ vel = (
+ velocities[i]
+ if i < len(velocities)
+ else {"x": 0.0, "y": 0.0, "z": 0.0}
+ )
+ f.write(struct.pack("f", float(vel.get(coord, 0.0))))
+
+ headings = obj.get("heading", [])
+ f.write(
+ struct.pack(
+ f"{TRAJECTORY_LENGTH}f",
+ *[
+ float(headings[i]) if i < len(headings) else 0.0
+ for i in range(TRAJECTORY_LENGTH)
+ ],
+ )
+ )
+
+ valids = obj.get("valid", [])
+ f.write(
+ struct.pack(
+ f"{TRAJECTORY_LENGTH}i",
+ *[
+ int(valids[i]) if i < len(valids) else 0
+ for i in range(TRAJECTORY_LENGTH)
+ ],
+ )
+ )
+
+ f.write(struct.pack("f", float(obj.get("width", 0.0))))
+ f.write(struct.pack("f", float(obj.get("length", 0.0))))
+ f.write(struct.pack("f", float(obj.get("height", 0.0))))
+ goal_pos = obj.get("goalPosition", {"x": 0, "y": 0, "z": 0})
+ f.write(struct.pack("f", float(goal_pos.get("x", 0.0))))
+ f.write(struct.pack("f", float(goal_pos.get("y", 0.0))))
+ f.write(struct.pack("f", float(goal_pos.get("z", 0.0))))
+ f.write(struct.pack("i", obj.get("mark_as_expert", 0)))
+
+ # Write roads
+ for road in map_data.get("roads", []):
+ geometry = road.get("geometry", [])
+ road_type = road.get("map_element_id", 0)
+ road_type_word = road.get("type", 0)
+ if road_type_word == "lane":
+ road_type = 2
+ elif road_type_word == "road_edge":
+ road_type = 15
+
+ if len(geometry) > 10 and road_type <= 16:
+ geometry = simplify_polyline(geometry, 0.1, 250)
+ size = len(geometry)
+
+ if 0 <= road_type <= 3:
+ road_type = 4
+ elif 5 <= road_type <= 13:
+ road_type = 5
+ elif 14 <= road_type <= 16:
+ road_type = 6
+ elif road_type == 17:
+ road_type = 7
+ elif road_type == 18:
+ road_type = 8
+ elif road_type == 19:
+ road_type = 9
+ elif road_type == 20:
+ road_type = 10
+
+ f.write(struct.pack("i", road_type))
+ f.write(struct.pack("i", size))
+
+ for coord in ["x", "y", "z"]:
+ for point in geometry:
+ f.write(struct.pack("f", float(point.get(coord, 0.0))))
+
+ f.write(struct.pack("f", float(road.get("width", 0.0))))
+ f.write(struct.pack("f", float(road.get("length", 0.0))))
+ f.write(struct.pack("f", float(road.get("height", 0.0))))
+ goal_pos = road.get("goalPosition", {"x": 0, "y": 0, "z": 0})
+ f.write(struct.pack("f", float(goal_pos.get("x", 0.0))))
+ f.write(struct.pack("f", float(goal_pos.get("y", 0.0))))
+ f.write(struct.pack("f", float(goal_pos.get("z", 0.0))))
+ f.write(struct.pack("i", road.get("mark_as_expert", 0)))
+
+
+def _process_single_map(args):
+ """Worker function to process a single map file."""
+ i, map_path, binary_path = args
+ try:
+ with open(map_path, "r") as f:
+ map_data = json.load(f)
+ save_map_binary(map_data, str(binary_path), i)
+ return (i, map_path.name, True, None)
+ except Exception as e:
+ return (i, map_path.name, False, str(e))
+
+
+def process_all_maps(
+ data_folder,
+ output_dir,
+ max_maps=50_000,
+ num_workers=None,
+):
+ """Process JSON map files into binary format using multiprocessing.
+
+ Args:
+ data_folder: Path to the folder containing JSON map files.
+ output_dir: Path to the directory where binary files will be written.
+ max_maps: Maximum number of maps to process.
+ num_workers: Number of parallel workers (defaults to cpu_count()).
+ """
+ if num_workers is None:
+ num_workers = cpu_count()
+
+ data_dir = Path(data_folder)
+ binary_dir = Path(output_dir)
+ binary_dir.mkdir(parents=True, exist_ok=True)
+
+ json_files = sorted(data_dir.glob("*.json"))
+ if not json_files:
+ print(f"No JSON files found in {data_dir}")
+ return
+
+ tasks = []
+ for i, map_path in enumerate(json_files[:max_maps]):
+ binary_path = binary_dir / f"map_{i:03d}.bin"
+ tasks.append((i, map_path, binary_path))
+
+ with Pool(num_workers) as pool:
+ results = list(
+ tqdm(
+ pool.imap(_process_single_map, tasks),
+ total=len(tasks),
+ desc="Processing maps",
+ unit="map",
+ )
+ )
+
+ successful = sum(1 for _, _, success, _ in results if success)
+ failed = sum(1 for _, _, success, _ in results if not success)
+
+ print(f"\nProcessed {successful}/{len(results)} maps successfully.")
+ if failed > 0:
+ print(f"Failed {failed}/{len(results)} files:")
+ for i, name, success, error in results:
+ if not success:
+ print(f" {name}: {error}")
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(description="Convert JSON map files to binary format.")
+ parser.add_argument("--data_folder", type=str, required=True, help="Path to folder containing JSON map files.")
+ parser.add_argument("--output_dir", type=str, required=True, help="Path to output directory for binary files.")
+ parser.add_argument("--max_maps", type=int, default=50_000, help="Maximum number of maps to process.")
+ parser.add_argument("--num_workers", type=int, default=None, help="Number of parallel workers.")
+ args = parser.parse_args()
+
+ process_all_maps(
+ data_folder=args.data_folder,
+ output_dir=args.output_dir,
+ max_maps=args.max_maps,
+ num_workers=args.num_workers,
+ )
\ No newline at end of file
diff --git a/ocean/drive/drive.c b/ocean/drive/drive.c
new file mode 100644
index 0000000000..48a3ddf7b7
--- /dev/null
+++ b/ocean/drive/drive.c
@@ -0,0 +1,89 @@
+#include
+#include
+#include "drive.h"
+#include "puffernet.h"
+
+void demo() {
+ Drive env = {
+ .dynamics_model = CLASSIC,
+ .human_agent_idx = 0,
+ .reward_vehicle_collision = -0.1f,
+ .reward_offroad_collision = -0.1f,
+ .map_name = "resources/drive/map_010.bin",
+ };
+ allocate(&env);
+ c_reset(&env);
+ c_render(&env);
+ Weights* weights = load_weights("resources/drive/drive_weights.bin");
+ int logit_sizes[2] = {7, 13};
+ PufferNet* net = make_puffernet(weights, env.active_agent_count, OBS_SIZE, 256, 4, logit_sizes, 2);
+ int accel_delta = 2;
+ int steer_delta = 4;
+ while (!WindowShouldClose()) {
+ float (*actions)[2] = (float(*)[2])env.actions;
+ forward_puffernet(net, env.observations, env.actions);
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ actions[env.human_agent_idx][0] = 3;
+ actions[env.human_agent_idx][1] = 6;
+ if(IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)){
+ actions[env.human_agent_idx][0] += accel_delta;
+ if(actions[env.human_agent_idx][0] > 6) actions[env.human_agent_idx][0] = 6;
+ }
+ if(IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)){
+ actions[env.human_agent_idx][0] -= accel_delta;
+ if(actions[env.human_agent_idx][0] < 0) actions[env.human_agent_idx][0] = 0;
+ }
+ if(IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)){
+ actions[env.human_agent_idx][1] += steer_delta;
+ if(actions[env.human_agent_idx][1] < 0) actions[env.human_agent_idx][1] = 0;
+ }
+ if(IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)){
+ actions[env.human_agent_idx][1] -= steer_delta;
+ if(actions[env.human_agent_idx][1] > 12) actions[env.human_agent_idx][1] = 12;
+ }
+ if(IsKeyPressed(KEY_TAB)){
+ env.human_agent_idx = (env.human_agent_idx + 1) % env.active_agent_count;
+ }
+ }
+ c_step(&env);
+ c_render(&env);
+ }
+
+ close_client(env.client);
+ free_allocated(&env);
+ free_puffernet(net);
+ free(weights);
+}
+
+void performance_test() {
+ long test_time = 10;
+ Drive env = {
+ .dynamics_model = CLASSIC,
+ .human_agent_idx = 0,
+ .map_name = "resources/drive/map_942.bin",
+ };
+ allocate(&env);
+ c_reset(&env);
+
+ Weights* weights = load_weights("resources/drive/drive_weights.bin");
+ int logit_sizes[2] = {7, 13};
+ PufferNet* net = make_puffernet(weights, env.active_agent_count, OBS_SIZE, 256, 4, logit_sizes, 2);
+
+ long start = time(NULL);
+ int i = 0;
+ while (time(NULL) - start < test_time) {
+ forward_puffernet(net, env.observations, env.actions);
+ c_step(&env);
+ i++;
+ }
+ long end = time(NULL);
+ printf("SPS: %ld\n", (long)(i*env.active_agent_count) / (end - start));
+ free_allocated(&env);
+ free_puffernet(net);
+ free(weights);
+}
+
+int main() {
+ demo();
+ return 0;
+}
diff --git a/ocean/drive/drive.h b/ocean/drive/drive.h
new file mode 100644
index 0000000000..931e04d1fe
--- /dev/null
+++ b/ocean/drive/drive.h
@@ -0,0 +1,1590 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "raylib.h"
+#include "raymath.h"
+#include "rlgl.h"
+#include
+
+// Entity Types
+#define NONE 0
+#define VEHICLE 1
+#define PEDESTRIAN 2
+#define CYCLIST 3
+#define ROAD_LANE 4
+#define ROAD_LINE 5
+#define ROAD_EDGE 6
+#define STOP_SIGN 7
+#define CROSSWALK 8
+#define SPEED_BUMP 9
+#define DRIVEWAY 10
+
+#define INVALID_POSITION -10000.0f
+
+// Simulation constants
+#define TRAJECTORY_LENGTH 91 // Discretized Waymo scenarios
+#define SIM_DT 0.1f
+
+// Agent limits
+#ifndef MAX_AGENTS
+#define MAX_AGENTS 64
+#endif
+
+// Dynamics models
+#define CLASSIC 0
+
+// Collision State
+#define NO_COLLISION 0
+#define VEHICLE_COLLISION 1
+#define OFFROAD 2
+
+// Grid Map
+#define GRID_CELL_SIZE 5.0f
+#define MAX_ENTITIES_PER_CELL 10
+#define SLOTS_PER_CELL (MAX_ENTITIES_PER_CELL * 2 + 1)
+#define VISION_RANGE 21
+
+// Observation Space
+#define MAX_ROAD_SEGMENT_OBSERVATIONS 75
+
+#define PARTNER_FEATURES 7
+#define EGO_FEATURES 7
+#define ROAD_FEATURES 7
+
+#define OBS_SIZE (EGO_FEATURES + PARTNER_FEATURES * (MAX_AGENTS - 1) + ROAD_FEATURES * MAX_ROAD_SEGMENT_OBSERVATIONS)
+
+// Observation normalization
+#define MAX_SPEED 100.0f
+#define MAX_VEH_LEN 30.0f
+#define MAX_VEH_WIDTH 15.0f
+#define MAX_VEH_HEIGHT 10.0f
+#define MAX_ROAD_SCALE 100.0f
+#define MAX_ROAD_SEGMENT_LENGTH 100.0f
+
+// Observation scaling factors
+#define OBS_GOAL_SCALE 0.005f
+#define OBS_SPEED_SCALE 0.01f
+#define OBS_POSITION_SCALE 0.02f
+
+// Distance thresholds
+#define MIN_DISTANCE_TO_GOAL 5.0f
+#define COLLISION_DIST_SQ 225.0f
+#define OBS_DIST_SQ 2500.0f
+#define COLLISION_BOX_SCALE 0.7f
+
+// Action space
+#define NUM_ACCEL_BINS 7
+#define NUM_STEER_BINS 13
+
+static const float ACCELERATION_VALUES[NUM_ACCEL_BINS] = {
+ -4.0000f, -2.6670f, -1.3330f, -0.0000f, 1.3330f, 2.6670f, 4.0000f
+};
+
+static const float STEERING_VALUES[NUM_STEER_BINS] = {
+ -1.000f, -0.833f, -0.667f, -0.500f, -0.333f, -0.167f, 0.000f,
+ 0.167f, 0.333f, 0.500f, 0.667f, 0.833f, 1.000f
+};
+
+// Geometry helpers
+static const float offsets[4][2] = {
+ {-1, 1}, // top-left
+ { 1, 1}, // top-right
+ { 1, -1}, // bottom-right
+ {-1, -1} // bottom-left
+};
+
+static const int collision_offsets[25][2] = {
+ {-2, -2}, {-1, -2}, {0, -2}, {1, -2}, {2, -2},
+ {-2, -1}, {-1, -1}, {0, -1}, {1, -1}, {2, -1},
+ {-2, 0}, {-1, 0}, {0, 0}, {1, 0}, {2, 0},
+ {-2, 1}, {-1, 1}, {0, 1}, {1, 1}, {2, 1},
+ {-2, 2}, {-1, 2}, {0, 2}, {1, 2}, {2, 2}
+};
+
+// Rendering Colors
+const Color STONE_GRAY = (Color){80, 80, 80, 255};
+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_BACKGROUND2 = (Color){18, 72, 72, 255};
+const Color ROAD_COLOR = (Color){35, 35, 37, 255};
+
+// Forward declarations
+typedef struct Drive Drive;
+typedef struct Client Client;
+typedef struct Log Log;
+typedef struct Entity Entity;
+
+struct Log {
+ float episode_return;
+ float episode_length;
+ float perf;
+ float score;
+ float offroad_rate;
+ float collision_rate;
+ float clean_collision_rate;
+ float completion_rate;
+ float dnf_rate;
+ float n;
+};
+
+struct Entity {
+ int type;
+ int array_size;
+ float* traj_x;
+ float* traj_y;
+ float* traj_z;
+ float* traj_vx;
+ float* traj_vy;
+ float* traj_vz;
+ float* traj_heading;
+ int* traj_valid;
+ float width;
+ float length;
+ float height;
+ float goal_position_x;
+ float goal_position_y;
+ float goal_position_z;
+ int mark_as_expert;
+ int collision_state;
+ float x;
+ float y;
+ float z;
+ float vx;
+ float vy;
+ float vz;
+ float heading;
+ float heading_x;
+ float heading_y;
+ int valid;
+ int reached_goal;
+ int respawn_timestep;
+ int collided_before_goal;
+ int reached_goal_this_episode;
+ int active_agent;
+};
+
+void free_entity(Entity* entity) {
+ free(entity->traj_x);
+ free(entity->traj_y);
+ free(entity->traj_z);
+ free(entity->traj_vx);
+ free(entity->traj_vy);
+ free(entity->traj_vz);
+ free(entity->traj_heading);
+ free(entity->traj_valid);
+}
+
+// Utility
+float relative_distance_2d(float x1, float y1, float x2, float y2) {
+ float dx = x2 - x1;
+ float dy = y2 - y1;
+ return sqrtf(dx * dx + dy * dy);
+}
+
+float clipSpeed(float speed) {
+ if (speed > MAX_SPEED) return MAX_SPEED;
+ if (speed < -MAX_SPEED) return -MAX_SPEED;
+ return speed;
+}
+
+float normalize_heading(float heading) {
+ if (heading > M_PI) heading -= 2 * M_PI;
+ if (heading < -M_PI) heading += 2 * M_PI;
+ return heading;
+}
+
+struct Drive {
+ Client* client;
+ float* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ Log log;
+ Log* logs;
+ int num_agents;
+ int max_agents;
+ int active_agent_count;
+ int* active_agent_indices;
+ int human_agent_idx;
+ Entity* entities;
+ int num_entities;
+ int num_actors; // Total agents (active + static)
+ int num_objects;
+ int num_roads;
+ int static_agent_count;
+ int* static_agent_indices;
+ int expert_static_agent_count;
+ int* expert_static_agent_indices;
+ int timestep;
+ int dynamics_model;
+ float* map_corners;
+ int* grid_cells;
+ int grid_cols;
+ int grid_rows;
+ int vision_range;
+ int* neighbor_offsets;
+ int* neighbor_cache_entities;
+ int* neighbor_cache_indices;
+ float reward_vehicle_collision;
+ float reward_offroad_collision;
+ char* map_name;
+ float world_mean_x;
+ float world_mean_y;
+ float reward_goal_post_respawn;
+ float reward_vehicle_collision_post_respawn;
+ unsigned int rng;
+};
+
+void add_log(Drive* env) {
+ for (int i = 0; i < env->active_agent_count; i++) {
+ Entity* e = &env->entities[env->active_agent_indices[i]];
+ if (e->reached_goal_this_episode) {
+ env->log.completion_rate += 1.0f;
+ }
+ int offroad = env->logs[i].offroad_rate;
+ env->log.offroad_rate += env->logs[i].offroad_rate;
+ int collided = env->logs[i].collision_rate;
+ env->log.collision_rate += collided;
+ int clean_collided = env->logs[i].clean_collision_rate;
+ env->log.clean_collision_rate += clean_collided;
+ if (e->reached_goal_this_episode && !e->collided_before_goal) {
+ env->log.score += 1.0f;
+ env->log.perf += 1.0f;
+ }
+ if (!offroad && !collided && !e->reached_goal_this_episode) {
+ env->log.dnf_rate += 1.0f;
+ }
+ env->log.episode_length += env->logs[i].episode_length;
+ env->log.episode_return += env->logs[i].episode_return;
+ env->log.n += 1;
+ }
+}
+
+// Map loading
+Entity* load_map_binary(const char* filename, Drive* env) {
+ FILE* file = fopen(filename, "rb");
+ if (!file) return NULL;
+
+ fread(&env->num_objects, sizeof(int), 1, file);
+ fread(&env->num_roads, sizeof(int), 1, file);
+ env->num_entities = env->num_objects + env->num_roads;
+
+ Entity* entities = (Entity*)malloc(env->num_entities * sizeof(Entity));
+ for (int i = 0; i < env->num_entities; i++) {
+ // Read base entity data
+ fread(&entities[i].type, sizeof(int), 1, file);
+ fread(&entities[i].array_size, sizeof(int), 1, file);
+ // Allocate arrays based on type
+ int size = entities[i].array_size;
+ entities[i].traj_x = (float*)malloc(size * sizeof(float));
+ entities[i].traj_y = (float*)malloc(size * sizeof(float));
+ entities[i].traj_z = (float*)malloc(size * sizeof(float));
+
+ int is_actor = (entities[i].type == VEHICLE ||
+ entities[i].type == PEDESTRIAN ||
+ entities[i].type == CYCLIST);
+ if (is_actor) {
+ // Allocate arrays for object-specific data
+ entities[i].traj_vx = (float*)malloc(size * sizeof(float));
+ entities[i].traj_vy = (float*)malloc(size * sizeof(float));
+ entities[i].traj_vz = (float*)malloc(size * sizeof(float));
+ entities[i].traj_heading = (float*)malloc(size * sizeof(float));
+ entities[i].traj_valid = (int*)malloc(size * sizeof(int));
+ } else {
+ // Roads don't use these arrays
+ entities[i].traj_vx = NULL;
+ entities[i].traj_vy = NULL;
+ entities[i].traj_vz = NULL;
+ entities[i].traj_heading = NULL;
+ entities[i].traj_valid = NULL;
+ }
+ // Read array data
+ fread(entities[i].traj_x, sizeof(float), size, file);
+ fread(entities[i].traj_y, sizeof(float), size, file);
+ fread(entities[i].traj_z, sizeof(float), size, file);
+ if (is_actor) {
+ fread(entities[i].traj_vx, sizeof(float), size, file);
+ fread(entities[i].traj_vy, sizeof(float), size, file);
+ fread(entities[i].traj_vz, sizeof(float), size, file);
+ fread(entities[i].traj_heading, sizeof(float), size, file);
+ fread(entities[i].traj_valid, sizeof(int), size, file);
+ }
+ // Read remaining scalar fields
+ fread(&entities[i].width, sizeof(float), 1, file);
+ fread(&entities[i].length, sizeof(float), 1, file);
+ fread(&entities[i].height, sizeof(float), 1, file);
+ fread(&entities[i].goal_position_x, sizeof(float), 1, file);
+ fread(&entities[i].goal_position_y, sizeof(float), 1, file);
+ fread(&entities[i].goal_position_z, sizeof(float), 1, file);
+ fread(&entities[i].mark_as_expert, sizeof(int), 1, file);
+ }
+ fclose(file);
+ return entities;
+}
+
+// Position initialization
+void set_start_position(Drive* env) {
+ for (int i = 0; i < env->num_entities; i++) {
+ int is_active = 0;
+ for (int j = 0; j < env->active_agent_count; j++) {
+ if (env->active_agent_indices[j] == i) {
+ is_active = 1;
+ break;
+ }
+ }
+ Entity* e = &env->entities[i];
+ e->x = e->traj_x[0];
+ e->y = e->traj_y[0];
+ e->z = e->traj_z[0];
+
+ if (e->type > CYCLIST || e->type == NONE) {
+ continue;
+ }
+ if (!is_active) {
+ e->vx = 0;
+ e->vy = 0;
+ e->vz = 0;
+ e->reached_goal = 0;
+ e->collided_before_goal = 0;
+ } else {
+ e->vx = e->traj_vx[0];
+ e->vy = e->traj_vy[0];
+ e->vz = e->traj_vz[0];
+ }
+ e->heading = e->traj_heading[0];
+ e->heading_x = cosf(e->heading);
+ e->heading_y = sinf(e->heading);
+ e->valid = e->traj_valid[0];
+ e->collision_state = NO_COLLISION;
+ e->respawn_timestep = -1;
+ }
+}
+
+// Grid Map
+int getGridIndex(Drive* env, float x1, float y1) {
+ if (env->map_corners[0] >= env->map_corners[2] ||
+ env->map_corners[1] >= env->map_corners[3]) {
+ return -1;
+ }
+ float relativeX = x1 - env->map_corners[0];
+ float relativeY = y1 - env->map_corners[1];
+ int gridX = (int)(relativeX / GRID_CELL_SIZE);
+ int gridY = (int)(relativeY / GRID_CELL_SIZE);
+ if (gridX < 0 || gridX >= env->grid_cols || gridY < 0 || gridY >= env->grid_rows) {
+ return -1;
+ }
+ return (gridY * env->grid_cols) + gridX;
+}
+
+void add_entity_to_grid(Drive* env, int grid_index, int entity_idx, int geometry_idx) {
+ if (grid_index == -1) return;
+
+ int base_index = grid_index * SLOTS_PER_CELL;
+ int count = env->grid_cells[base_index];
+ if (count >= MAX_ENTITIES_PER_CELL) return;
+
+ env->grid_cells[base_index + count * 2 + 1] = entity_idx;
+ env->grid_cells[base_index + count * 2 + 2] = geometry_idx;
+ env->grid_cells[base_index] = count + 1;
+}
+
+void init_grid_map(Drive* env) {
+ float top_left_x, top_left_y, bottom_right_x, bottom_right_y;
+ int first_valid_point = 0;
+
+ for (int i = 0; i < env->num_entities; i++) {
+ if (env->entities[i].type >= ROAD_LANE && env->entities[i].type <= ROAD_EDGE) {
+ Entity* e = &env->entities[i];
+ for (int j = 0; j < e->array_size; j++) {
+ if (e->traj_x[j] == INVALID_POSITION) continue;
+ if (e->traj_y[j] == INVALID_POSITION) continue;
+ if (!first_valid_point) {
+ top_left_x = bottom_right_x = e->traj_x[j];
+ top_left_y = bottom_right_y = e->traj_y[j];
+ first_valid_point = 1;
+ continue;
+ }
+ if (e->traj_x[j] < top_left_x) top_left_x = e->traj_x[j];
+ if (e->traj_x[j] > bottom_right_x) bottom_right_x = e->traj_x[j];
+ if (e->traj_y[j] < top_left_y) top_left_y = e->traj_y[j];
+ if (e->traj_y[j] > bottom_right_y) bottom_right_y = e->traj_y[j];
+ }
+ }
+ }
+
+ env->map_corners = (float*)calloc(4, sizeof(float));
+ env->map_corners[0] = top_left_x;
+ env->map_corners[1] = top_left_y;
+ env->map_corners[2] = bottom_right_x;
+ env->map_corners[3] = bottom_right_y;
+
+ float grid_width = bottom_right_x - top_left_x;
+ float grid_height = bottom_right_y - top_left_y;
+ env->grid_cols = ceil(grid_width / GRID_CELL_SIZE);
+ env->grid_rows = ceil(grid_height / GRID_CELL_SIZE);
+ int grid_cell_count = env->grid_cols * env->grid_rows;
+ env->grid_cells = (int*)calloc(grid_cell_count * SLOTS_PER_CELL, sizeof(int));
+
+ for (int i = 0; i < env->num_entities; i++) {
+ if (env->entities[i].type >= ROAD_LANE && env->entities[i].type <= ROAD_EDGE) {
+ for (int j = 0; j < env->entities[i].array_size - 1; j++) {
+ float x_center = (env->entities[i].traj_x[j] + env->entities[i].traj_x[j + 1]) / 2;
+ float y_center = (env->entities[i].traj_y[j] + env->entities[i].traj_y[j + 1]) / 2;
+ int grid_index = getGridIndex(env, x_center, y_center);
+ add_entity_to_grid(env, grid_index, i, j);
+ }
+ }
+ }
+}
+
+void init_neighbor_offsets(Drive* env) {
+ int vr = env->vision_range;
+ env->neighbor_offsets = (int*)calloc(vr * vr * 2, sizeof(int));
+
+ int dx[] = {1, 0, -1, 0};
+ int dy[] = {0, 1, 0, -1};
+ int x = 0, y = 0, dir = 0;
+ int steps_to_take = 1, steps_taken = 0, segments_completed = 0;
+ int total = 0, max_offsets = vr * vr;
+ int curr_idx = 0;
+
+ env->neighbor_offsets[curr_idx++] = 0;
+ env->neighbor_offsets[curr_idx++] = 0;
+ total++;
+
+ while (total < max_offsets) {
+ x += dx[dir];
+ y += dy[dir];
+ if (abs(x) <= vr / 2 && abs(y) <= vr / 2) {
+ env->neighbor_offsets[curr_idx++] = x;
+ env->neighbor_offsets[curr_idx++] = y;
+ total++;
+ }
+ steps_taken++;
+ if (steps_taken != steps_to_take) continue;
+ steps_taken = 0;
+ dir = (dir + 1) % 4;
+ segments_completed++;
+ if (segments_completed % 2 == 0) {
+ steps_to_take++;
+ }
+ }
+}
+
+void cache_neighbor_offsets(Drive* env) {
+ int vr = env->vision_range;
+ int count = 0;
+ int cell_count = env->grid_cols * env->grid_rows;
+
+ for (int i = 0; i < cell_count; i++) {
+ int cell_x = i % env->grid_cols;
+ int cell_y = i / env->grid_cols;
+ env->neighbor_cache_indices[i] = count;
+ for (int j = 0; j < vr * vr; j++) {
+ int x = cell_x + env->neighbor_offsets[j * 2];
+ int y = cell_y + env->neighbor_offsets[j * 2 + 1];
+ if (x < 0 || x >= env->grid_cols || y < 0 || y >= env->grid_rows) continue;
+ int grid_index = env->grid_cols * y + x;
+ count += env->grid_cells[grid_index * SLOTS_PER_CELL] * 2;
+ }
+ }
+ env->neighbor_cache_indices[cell_count] = count;
+ env->neighbor_cache_entities = (int*)calloc(count, sizeof(int));
+
+ for (int i = 0; i < cell_count; i++) {
+ int neighbor_cache_base_index = 0;
+ int cell_x = i % env->grid_cols;
+ int cell_y = i / env->grid_cols;
+ for (int j = 0; j < vr * vr; j++) {
+ int x = cell_x + env->neighbor_offsets[j * 2];
+ int y = cell_y + env->neighbor_offsets[j * 2 + 1];
+ if (x < 0 || x >= env->grid_cols || y < 0 || y >= env->grid_rows) continue;
+ int grid_index = env->grid_cols * y + x;
+ int grid_count = env->grid_cells[grid_index * SLOTS_PER_CELL];
+ int base_index = env->neighbor_cache_indices[i];
+ int src_idx = grid_index * SLOTS_PER_CELL + 1;
+ int dst_idx = base_index + neighbor_cache_base_index;
+ memcpy(&env->neighbor_cache_entities[dst_idx],
+ &env->grid_cells[src_idx],
+ grid_count * 2 * sizeof(int));
+ neighbor_cache_base_index += grid_count * 2;
+ }
+ }
+}
+
+int get_neighbor_cache_entities(Drive* env, int cell_idx, int* entities, int max_entities) {
+ if (cell_idx < 0 || cell_idx >= (env->grid_cols * env->grid_rows)) {
+ return 0;
+ }
+ int base_index = env->neighbor_cache_indices[cell_idx];
+ int end_index = env->neighbor_cache_indices[cell_idx + 1];
+ int count = end_index - base_index;
+ int pairs = count / 2;
+ if (pairs > max_entities) {
+ pairs = max_entities;
+ count = pairs * 2;
+ }
+ memcpy(entities, env->neighbor_cache_entities + base_index, count * sizeof(int));
+ return pairs;
+}
+
+// Mean Centering
+void set_means(Drive* env) {
+ float mean_x = 0.0f;
+ float mean_y = 0.0f;
+ int64_t point_count = 0;
+
+ for (int i = 0; i < env->num_entities; i++) {
+ if (env->entities[i].type == VEHICLE) {
+ for (int j = 0; j < env->entities[i].array_size; j++) {
+ if (env->entities[i].traj_valid[j]) {
+ point_count++;
+ mean_x += (env->entities[i].traj_x[j] - mean_x) / point_count;
+ mean_y += (env->entities[i].traj_y[j] - mean_y) / point_count;
+ }
+ }
+ } else if (env->entities[i].type >= ROAD_LANE) {
+ for (int j = 0; j < env->entities[i].array_size; j++) {
+ point_count++;
+ mean_x += (env->entities[i].traj_x[j] - mean_x) / point_count;
+ mean_y += (env->entities[i].traj_y[j] - mean_y) / point_count;
+ }
+ }
+ }
+ env->world_mean_x = mean_x;
+ env->world_mean_y = mean_y;
+
+ for (int i = 0; i < env->num_entities; i++) {
+ if (env->entities[i].type == VEHICLE || env->entities[i].type >= ROAD_LANE) {
+ for (int j = 0; j < env->entities[i].array_size; j++) {
+ if (env->entities[i].traj_x[j] == INVALID_POSITION) continue;
+ env->entities[i].traj_x[j] -= mean_x;
+ env->entities[i].traj_y[j] -= mean_y;
+ }
+ env->entities[i].goal_position_x -= mean_x;
+ env->entities[i].goal_position_y -= mean_y;
+ }
+ }
+}
+
+// Expert Movement
+void move_expert(Drive* env, float* actions, int agent_idx) {
+ Entity* agent = &env->entities[agent_idx];
+ int t = env->timestep;
+ if (t < 0 || t >= agent->array_size) {
+ agent->x = INVALID_POSITION;
+ agent->y = INVALID_POSITION;
+ return;
+ }
+ agent->x = agent->traj_x[t];
+ agent->y = agent->traj_y[t];
+ agent->z = agent->traj_z[t];
+ agent->heading = agent->traj_heading[t];
+ agent->heading_x = cosf(agent->heading);
+ agent->heading_y = sinf(agent->heading);
+}
+
+// Collision Detection
+bool check_line_intersection(float p1[2], float p2[2], float q1[2], float q2[2]) {
+ if (fmax(p1[0], p2[0]) < fmin(q1[0], q2[0]) || fmin(p1[0], p2[0]) > fmax(q1[0], q2[0]) ||
+ fmax(p1[1], p2[1]) < fmin(q1[1], q2[1]) || fmin(p1[1], p2[1]) > fmax(q1[1], q2[1]))
+ return false;
+
+ float dx1 = p2[0] - p1[0];
+ float dy1 = p2[1] - p1[1];
+ float dx2 = q2[0] - q1[0];
+ float dy2 = q2[1] - q1[1];
+ float cross = dx1 * dy2 - dy1 * dx2;
+ if (cross == 0) return false;
+
+ float dx3 = p1[0] - q1[0];
+ float dy3 = p1[1] - q1[1];
+ float s = (dx1 * dy3 - dy1 * dx3) / cross;
+ float t = (dx2 * dy3 - dy2 * dx3) / cross;
+ return (s >= 0 && s <= 1 && t >= 0 && t <= 1);
+}
+
+int checkNeighbors(Drive* env, float x, float y, int* entity_list, int max_size,
+ const int (*local_offsets)[2], int offset_size) {
+ int index = getGridIndex(env, x, y);
+ if (index == -1) return 0;
+ int cellsX = env->grid_cols;
+ int gridX = index % cellsX;
+ int gridY = index / cellsX;
+ int entity_list_count = 0;
+
+ for (int i = 0; i < offset_size; i++) {
+ int nx = gridX + local_offsets[i][0];
+ int ny = gridY + local_offsets[i][1];
+ if (nx < 0 || nx >= env->grid_cols || ny < 0 || ny >= env->grid_rows) continue;
+ int neighborIndex = (ny * env->grid_cols + nx) * SLOTS_PER_CELL;
+ int count = env->grid_cells[neighborIndex];
+ for (int j = 0; j < count && entity_list_count < max_size; j++) {
+ entity_list[entity_list_count] = env->grid_cells[neighborIndex + 1 + j * 2];
+ entity_list[entity_list_count + 1] = env->grid_cells[neighborIndex + 2 + j * 2];
+ entity_list_count += 2;
+ }
+ }
+ return entity_list_count;
+}
+
+int check_aabb_collision(Entity* car1, Entity* car2) {
+ float cos1 = car1->heading_x, sin1 = car1->heading_y;
+ float cos2 = car2->heading_x, sin2 = car2->heading_y;
+ float hl1 = car1->length * 0.5f, hw1 = car1->width * 0.5f;
+ float hl2 = car2->length * 0.5f, hw2 = car2->width * 0.5f;
+
+ float car1_corners[4][2] = {
+ {car1->x + (hl1 * cos1 - hw1 * sin1), car1->y + (hl1 * sin1 + hw1 * cos1)},
+ {car1->x + (hl1 * cos1 + hw1 * sin1), car1->y + (hl1 * sin1 - hw1 * cos1)},
+ {car1->x + (-hl1 * cos1 - hw1 * sin1), car1->y + (-hl1 * sin1 + hw1 * cos1)},
+ {car1->x + (-hl1 * cos1 + hw1 * sin1), car1->y + (-hl1 * sin1 - hw1 * cos1)}
+ };
+
+ float car2_corners[4][2] = {
+ {car2->x + (hl2 * cos2 - hw2 * sin2), car2->y + (hl2 * sin2 + hw2 * cos2)},
+ {car2->x + (hl2 * cos2 + hw2 * sin2), car2->y + (hl2 * sin2 - hw2 * cos2)},
+ {car2->x + (-hl2 * cos2 - hw2 * sin2), car2->y + (-hl2 * sin2 + hw2 * cos2)},
+ {car2->x + (-hl2 * cos2 + hw2 * sin2), car2->y + (-hl2 * sin2 - hw2 * cos2)}
+ };
+
+ float axes[4][2] = {
+ {cos1, sin1}, {-sin1, cos1},
+ {cos2, sin2}, {-sin2, cos2}
+ };
+
+ for (int i = 0; i < 4; i++) {
+ float min1 = INFINITY, max1 = -INFINITY;
+ float min2 = INFINITY, max2 = -INFINITY;
+ for (int j = 0; j < 4; j++) {
+ float proj1 = car1_corners[j][0] * axes[i][0] + car1_corners[j][1] * axes[i][1];
+ min1 = fminf(min1, proj1);
+ max1 = fmaxf(max1, proj1);
+ float proj2 = car2_corners[j][0] * axes[i][0] + car2_corners[j][1] * axes[i][1];
+ min2 = fminf(min2, proj2);
+ max2 = fmaxf(max2, proj2);
+ }
+ if (max1 < min2 || min1 > max2) return 0;
+ }
+ return 1;
+}
+
+int collision_check(Drive* env, int agent_idx) {
+ Entity* agent = &env->entities[agent_idx];
+ if (agent->x == INVALID_POSITION) return -1;
+
+ float half_length = agent->length / 2.0f;
+ float half_width = agent->width / 2.0f;
+ float cos_heading = cosf(agent->heading);
+ float sin_heading = sinf(agent->heading);
+ float corners[4][2];
+ for (int i = 0; i < 4; i++) {
+ corners[i][0] = agent->x + (offsets[i][0] * half_length * cos_heading - offsets[i][1] * half_width * sin_heading);
+ corners[i][1] = agent->y + (offsets[i][0] * half_length * sin_heading + offsets[i][1] * half_width * cos_heading);
+ }
+
+ int collided = NO_COLLISION;
+ int car_collided_with_index = -1;
+
+ // Check road edge collisions via grid
+ int entity_list[MAX_ENTITIES_PER_CELL * 2 * 25];
+ int list_size = checkNeighbors(env, agent->x, agent->y, entity_list,
+ MAX_ENTITIES_PER_CELL * 2 * 25, collision_offsets, 25);
+ for (int i = 0; i < list_size; i += 2) {
+ if (entity_list[i] == -1 || entity_list[i] == agent_idx) continue;
+ Entity* entity = &env->entities[entity_list[i]];
+ if (entity->type != ROAD_EDGE) continue;
+ int geometry_idx = entity_list[i + 1];
+ float start[2] = {entity->traj_x[geometry_idx], entity->traj_y[geometry_idx]};
+ float end[2] = {entity->traj_x[geometry_idx + 1], entity->traj_y[geometry_idx + 1]};
+ for (int k = 0; k < 4; k++) {
+ int next = (k + 1) % 4;
+ if (check_line_intersection(corners[k], corners[next], start, end)) {
+ collided = OFFROAD;
+ break;
+ }
+ }
+ if (collided == OFFROAD) break;
+ }
+
+ // Check vehicle-vehicle collisions
+ for (int i = 0; i < MAX_AGENTS; i++) {
+ int index = -1;
+ if (i < env->active_agent_count) {
+ index = env->active_agent_indices[i];
+ } else if (i < env->num_actors) {
+ index = env->static_agent_indices[i - env->active_agent_count];
+ }
+ if (index == -1 || index == agent_idx) continue;
+ Entity* entity = &env->entities[index];
+ float dx = entity->x - agent->x;
+ float dy = entity->y - agent->y;
+ if ((dx * dx + dy * dy) > COLLISION_DIST_SQ) continue;
+ if (check_aabb_collision(agent, entity)) {
+ collided = VEHICLE_COLLISION;
+ car_collided_with_index = index;
+ break;
+ }
+ }
+
+ agent->collision_state = collided;
+
+ // Spawn immunity: agent just respawned
+ if (collided == VEHICLE_COLLISION && agent->active_agent == 1 &&
+ agent->respawn_timestep != -1) {
+ agent->collision_state = NO_COLLISION;
+ }
+
+ if (collided == OFFROAD) return -1;
+ if (car_collided_with_index == -1) return -1;
+
+ // Spawn immunity: collided-with agent just respawned
+ if (env->entities[car_collided_with_index].respawn_timestep != -1) {
+ agent->collision_state = NO_COLLISION;
+ }
+
+ return car_collided_with_index;
+}
+
+// Agent Selection
+int valid_active_agent(Drive* env, int agent_idx) {
+ if (agent_idx < 0 || agent_idx >= env->num_entities) return 0;
+ Entity* e = &env->entities[agent_idx];
+ if (e->type != VEHICLE || e->traj_valid[0] != 1) return 0;
+ float cos_heading = cosf(e->traj_heading[0]);
+ float sin_heading = sinf(e->traj_heading[0]);
+ float goal_x = e->goal_position_x - e->traj_x[0];
+ float goal_y = e->goal_position_y - e->traj_y[0];
+ float rel_goal_x = goal_x * cos_heading + goal_y * sin_heading;
+ float rel_goal_y = -goal_x * sin_heading + goal_y * cos_heading;
+ float distance_to_goal = relative_distance_2d(0, 0, rel_goal_x, rel_goal_y);
+
+ e->width *= COLLISION_BOX_SCALE;
+ e->length *= COLLISION_BOX_SCALE;
+
+ if (distance_to_goal >= MIN_DISTANCE_TO_GOAL &&
+ e->mark_as_expert == 0 &&
+ env->active_agent_count < env->max_agents) {
+ return distance_to_goal;
+ }
+ return 0;
+}
+
+void set_active_agents(Drive* env) {
+ env->active_agent_count = 0;
+ env->static_agent_count = 0;
+ env->num_actors = 1;
+ env->expert_static_agent_count = 0;
+
+ int active_agent_indices[MAX_AGENTS];
+ int static_agent_indices[MAX_AGENTS];
+ int expert_static_agent_indices[MAX_AGENTS];
+
+ if (env->max_agents == 0) {
+ env->max_agents = MAX_AGENTS;
+ }
+
+ // First agent: last object (SDC equivalent)
+ int first_agent_id = env->num_objects - 1;
+ float distance_to_goal = valid_active_agent(env, first_agent_id);
+ if (distance_to_goal) {
+ env->active_agent_count = 1;
+ active_agent_indices[0] = first_agent_id;
+ env->entities[first_agent_id].active_agent = 1;
+ env->num_actors = 1;
+ } else {
+ env->active_agent_count = 0;
+ env->num_actors = 0;
+ }
+
+ for (int i = 0; i < env->num_objects - 1 && env->num_actors < MAX_AGENTS; i++) {
+ if (env->entities[i].type != VEHICLE) continue;
+ if (env->entities[i].traj_valid[0] != 1) continue;
+ env->num_actors++;
+
+ float dist = valid_active_agent(env, i);
+ if (dist > 0) {
+ active_agent_indices[env->active_agent_count] = i;
+ env->active_agent_count++;
+ env->entities[i].active_agent = 1;
+ } else {
+ static_agent_indices[env->static_agent_count] = i;
+ env->static_agent_count++;
+ env->entities[i].active_agent = 0;
+ if (env->entities[i].mark_as_expert == 1 ||
+ (dist >= MIN_DISTANCE_TO_GOAL && env->active_agent_count == env->max_agents)) {
+ expert_static_agent_indices[env->expert_static_agent_count] = i;
+ env->expert_static_agent_count++;
+ env->entities[i].mark_as_expert = 1;
+ }
+ }
+ }
+
+ env->active_agent_indices = (int*)malloc(env->active_agent_count * sizeof(int));
+ env->static_agent_indices = (int*)malloc(env->static_agent_count * sizeof(int));
+ env->expert_static_agent_indices = (int*)malloc(env->expert_static_agent_count * sizeof(int));
+ memcpy(env->active_agent_indices, active_agent_indices, env->active_agent_count * sizeof(int));
+ memcpy(env->static_agent_indices, static_agent_indices, env->static_agent_count * sizeof(int));
+ memcpy(env->expert_static_agent_indices, expert_static_agent_indices, env->expert_static_agent_count * sizeof(int));
+}
+
+// Trajectory Validation
+void remove_bad_trajectories(Drive* env) {
+ set_start_position(env);
+ int collided_agents[env->active_agent_count];
+ int collided_with_indices[env->active_agent_count];
+ memset(collided_agents, 0, env->active_agent_count * sizeof(int));
+
+ for (int t = 0; t < TRAJECTORY_LENGTH; t++) {
+ for (int i = 0; i < env->active_agent_count; i++) {
+ move_expert(env, env->actions, env->active_agent_indices[i]);
+ }
+ for (int i = 0; i < env->expert_static_agent_count; i++) {
+ int expert_idx = env->expert_static_agent_indices[i];
+ if (env->entities[expert_idx].x == INVALID_POSITION) continue;
+ move_expert(env, env->actions, expert_idx);
+ }
+ for (int i = 0; i < env->active_agent_count; i++) {
+ int agent_idx = env->active_agent_indices[i];
+ env->entities[agent_idx].collision_state = NO_COLLISION;
+ int collided_with = collision_check(env, agent_idx);
+ if (env->entities[agent_idx].collision_state > NO_COLLISION && collided_agents[i] == 0) {
+ collided_agents[i] = 1;
+ collided_with_indices[i] = collided_with;
+ }
+ }
+ env->timestep++;
+ }
+
+ for (int i = 0; i < env->active_agent_count; i++) {
+ if (collided_with_indices[i] == -1) continue;
+ for (int j = 0; j < env->static_agent_count; j++) {
+ int static_idx = env->static_agent_indices[j];
+ if (static_idx != collided_with_indices[i]) continue;
+ env->entities[static_idx].traj_x[0] = INVALID_POSITION;
+ env->entities[static_idx].traj_y[0] = INVALID_POSITION;
+ }
+ }
+ env->timestep = 0;
+}
+
+// Initialization / Cleanup
+void init(Drive* env) {
+ env->human_agent_idx = 0;
+ env->timestep = 0;
+ env->entities = load_map_binary(env->map_name, env);
+ env->dynamics_model = CLASSIC;
+ set_means(env);
+ init_grid_map(env);
+ env->vision_range = VISION_RANGE;
+ init_neighbor_offsets(env);
+ env->neighbor_cache_indices = (int*)calloc((env->grid_cols * env->grid_rows) + 1, sizeof(int));
+ cache_neighbor_offsets(env);
+ set_active_agents(env);
+ remove_bad_trajectories(env);
+ set_start_position(env);
+ env->logs = (Log*)calloc(env->active_agent_count, sizeof(Log));
+}
+
+void c_close(Drive* env) {
+ for (int i = 0; i < env->num_entities; i++) {
+ free_entity(&env->entities[i]);
+ }
+ free(env->entities);
+ free(env->active_agent_indices);
+ free(env->logs);
+ free(env->map_corners);
+ free(env->grid_cells);
+ free(env->neighbor_offsets);
+ free(env->neighbor_cache_entities);
+ free(env->neighbor_cache_indices);
+ free(env->static_agent_indices);
+ free(env->expert_static_agent_indices);
+}
+
+void allocate(Drive* env) {
+ init(env);
+ env->observations = (float*)calloc(env->active_agent_count * OBS_SIZE, sizeof(float));
+ env->actions = (float*)calloc(env->active_agent_count * 2, sizeof(float));
+ env->rewards = (float*)calloc(env->active_agent_count, sizeof(float));
+ env->terminals = (float*)calloc(env->active_agent_count, sizeof(float));
+}
+
+void free_allocated(Drive* env) {
+ free(env->observations);
+ free(env->actions);
+ free(env->rewards);
+ free(env->terminals);
+ c_close(env);
+}
+
+// Dynamics
+void move_dynamics(Drive* env, int action_idx, int agent_idx) {
+ if (env->dynamics_model != CLASSIC) return;
+
+ Entity* agent = &env->entities[agent_idx];
+ float (*action_array)[2] = (float(*)[2])env->actions;
+ int acceleration_index = action_array[action_idx][0];
+ int steering_index = action_array[action_idx][1];
+ float acceleration = ACCELERATION_VALUES[acceleration_index];
+ float steering = STEERING_VALUES[steering_index];
+
+ float x = agent->x;
+ float y = agent->y;
+ float heading = agent->heading;
+ float speed = sqrtf(agent->vx * agent->vx + agent->vy * agent->vy);
+
+ speed = speed + 0.5f * acceleration * SIM_DT;
+ speed = clipSpeed(speed);
+
+ float beta = tanh(0.5 * tanf(steering));
+ float yaw_rate = (speed * cosf(beta) * tanf(steering)) / agent->length;
+ float new_vx = speed * cosf(heading + beta);
+ float new_vy = speed * sinf(heading + beta);
+
+ x += new_vx * SIM_DT;
+ y += new_vy * SIM_DT;
+ heading += yaw_rate * SIM_DT;
+
+ agent->x = x;
+ agent->y = y;
+ agent->heading = heading;
+ agent->heading_x = cosf(heading);
+ agent->heading_y = sinf(heading);
+ agent->vx = new_vx;
+ agent->vy = new_vy;
+}
+
+// Observations
+void compute_observations(Drive* env) {
+ memset(env->observations, 0, OBS_SIZE * env->active_agent_count * sizeof(float));
+ float (*observations)[OBS_SIZE] = (float(*)[OBS_SIZE])env->observations;
+
+ for (int i = 0; i < env->active_agent_count; i++) {
+ float* obs = &observations[i][0];
+ Entity* ego = &env->entities[env->active_agent_indices[i]];
+ if (ego->type > CYCLIST) break;
+
+ if (ego->respawn_timestep != -1) {
+ obs[6] = 1;
+ }
+
+ float cos_h = ego->heading_x;
+ float sin_h = ego->heading_y;
+ float ego_speed = sqrtf(ego->vx * ego->vx + ego->vy * ego->vy);
+
+ // Goal in ego frame
+ float goal_x = ego->goal_position_x - ego->x;
+ float goal_y = ego->goal_position_y - ego->y;
+ float rel_goal_x = goal_x * cos_h + goal_y * sin_h;
+ float rel_goal_y = -goal_x * sin_h + goal_y * cos_h;
+
+ // Ego features
+ obs[0] = rel_goal_x * OBS_GOAL_SCALE;
+ obs[1] = rel_goal_y * OBS_GOAL_SCALE;
+ obs[2] = ego_speed * OBS_SPEED_SCALE;
+ obs[3] = ego->width / MAX_VEH_WIDTH;
+ obs[4] = ego->length / MAX_VEH_LEN;
+ obs[5] = (ego->collision_state > NO_COLLISION) ? 1 : 0;
+
+ // Partner observations
+ int obs_idx = EGO_FEATURES;
+ int cars_seen = 0;
+ for (int j = 0; j < MAX_AGENTS; j++) {
+ int index = -1;
+ if (j < env->active_agent_count) {
+ index = env->active_agent_indices[j];
+ } else if (j < env->num_actors) {
+ index = env->static_agent_indices[j - env->active_agent_count];
+ }
+ if (index == -1) continue;
+ if (env->entities[index].type > CYCLIST) break;
+ if (index == env->active_agent_indices[i]) continue;
+
+ Entity* other = &env->entities[index];
+ if (ego->respawn_timestep != -1) continue;
+ if (other->respawn_timestep != -1) continue;
+
+ float dx = other->x - ego->x;
+ float dy = other->y - ego->y;
+ if ((dx * dx + dy * dy) > OBS_DIST_SQ) continue;
+
+ float rel_x = dx * cos_h + dy * sin_h;
+ float rel_y = -dx * sin_h + dy * cos_h;
+
+ obs[obs_idx + 0] = rel_x * OBS_POSITION_SCALE;
+ obs[obs_idx + 1] = rel_y * OBS_POSITION_SCALE;
+ obs[obs_idx + 2] = other->width / MAX_VEH_WIDTH;
+ obs[obs_idx + 3] = other->length / MAX_VEH_LEN;
+ obs[obs_idx + 4] = other->heading_x * ego->heading_x + other->heading_y * ego->heading_y;
+ obs[obs_idx + 5] = other->heading_y * ego->heading_x - other->heading_x * ego->heading_y;
+ float other_speed = sqrtf(other->vx * other->vx + other->vy * other->vy);
+ obs[obs_idx + 6] = other_speed / MAX_SPEED;
+ cars_seen++;
+ obs_idx += PARTNER_FEATURES;
+ }
+ int remaining_partner_obs = (MAX_AGENTS - 1 - cars_seen) * PARTNER_FEATURES;
+ memset(&obs[obs_idx], 0, remaining_partner_obs * sizeof(float));
+ obs_idx += remaining_partner_obs;
+
+ // Road observations
+ int entity_list[MAX_ROAD_SEGMENT_OBSERVATIONS * 2];
+ int grid_idx = getGridIndex(env, ego->x, ego->y);
+ int list_size = get_neighbor_cache_entities(env, grid_idx, entity_list, MAX_ROAD_SEGMENT_OBSERVATIONS);
+
+ for (int k = 0; k < list_size; k++) {
+ int entity_idx = entity_list[k * 2];
+ int geometry_idx = entity_list[k * 2 + 1];
+ Entity* entity = &env->entities[entity_idx];
+
+ float start_x = entity->traj_x[geometry_idx];
+ float start_y = entity->traj_y[geometry_idx];
+ float end_x = entity->traj_x[geometry_idx + 1];
+ float end_y = entity->traj_y[geometry_idx + 1];
+ float mid_x = (start_x + end_x) / 2.0f;
+ float mid_y = (start_y + end_y) / 2.0f;
+ float rel_x = mid_x - ego->x;
+ float rel_y = mid_y - ego->y;
+ float x_obs = rel_x * cos_h + rel_y * sin_h;
+ float y_obs = -rel_x * sin_h + rel_y * cos_h;
+ float length = relative_distance_2d(mid_x, mid_y, end_x, end_y);
+
+ float dx = end_x - mid_x;
+ float dy = end_y - mid_y;
+ float hypot = sqrtf(dx * dx + dy * dy);
+ float dx_norm = dx, dy_norm = dy;
+ if (hypot > 0) { dx_norm /= hypot; dy_norm /= hypot; }
+
+ float cos_angle = dx_norm * cos_h + dy_norm * sin_h;
+ float sin_angle = -dx_norm * sin_h + dy_norm * cos_h;
+
+ obs[obs_idx + 0] = x_obs * OBS_POSITION_SCALE;
+ obs[obs_idx + 1] = y_obs * OBS_POSITION_SCALE;
+ obs[obs_idx + 2] = length / MAX_ROAD_SEGMENT_LENGTH;
+ obs[obs_idx + 3] = 0.1f / MAX_ROAD_SCALE;
+ obs[obs_idx + 4] = cos_angle;
+ obs[obs_idx + 5] = sin_angle;
+ obs[obs_idx + 6] = entity->type - (float)ROAD_LANE;
+ obs_idx += ROAD_FEATURES;
+ }
+ int remaining_obs = (MAX_ROAD_SEGMENT_OBSERVATIONS - list_size) * ROAD_FEATURES;
+ memset(&obs[obs_idx], 0, remaining_obs * sizeof(float));
+ }
+}
+
+void c_reset(Drive* env) {
+ env->timestep = 0;
+ set_start_position(env);
+ for (int x = 0; x < env->active_agent_count; x++) {
+ env->logs[x] = (Log){0};
+ int agent_idx = env->active_agent_indices[x];
+ env->entities[agent_idx].respawn_timestep = -1;
+ env->entities[agent_idx].reached_goal = 0;
+ env->entities[agent_idx].collided_before_goal = 0;
+ env->entities[agent_idx].reached_goal_this_episode = 0;
+
+ collision_check(env, agent_idx);
+ }
+ compute_observations(env);
+}
+
+void respawn_agent(Drive* env, int agent_idx) {
+ Entity* e = &env->entities[agent_idx];
+ e->x = e->traj_x[0];
+ e->y = e->traj_y[0];
+ e->heading = e->traj_heading[0];
+ e->heading_x = cosf(e->heading);
+ e->heading_y = sinf(e->heading);
+ e->vx = e->traj_vx[0];
+ e->vy = e->traj_vy[0];
+ e->reached_goal = 0;
+ e->respawn_timestep = env->timestep;
+}
+
+void c_step(Drive* env) {
+ memset(env->rewards, 0, env->active_agent_count * sizeof(float));
+ memset(env->terminals, 0, env->active_agent_count * sizeof(float));
+ env->timestep++;
+
+ if (env->timestep == TRAJECTORY_LENGTH) {
+ add_log(env);
+ c_reset(env);
+ return;
+ }
+
+ // Move expert static agents
+ for (int i = 0; i < env->expert_static_agent_count; i++) {
+ int expert_idx = env->expert_static_agent_indices[i];
+ if (env->entities[expert_idx].x == INVALID_POSITION) continue;
+ move_expert(env, env->actions, expert_idx);
+ }
+
+ // Apply dynamics for active agents
+ for (int i = 0; i < env->active_agent_count; i++) {
+ env->logs[i].score = 0.0f;
+ env->logs[i].episode_length += 1;
+ int agent_idx = env->active_agent_indices[i];
+ env->entities[agent_idx].collision_state = NO_COLLISION;
+ move_dynamics(env, i, agent_idx);
+ }
+
+ // Collision detection and rewards
+ for (int i = 0; i < env->active_agent_count; i++) {
+ int agent_idx = env->active_agent_indices[i];
+ env->entities[agent_idx].collision_state = NO_COLLISION;
+ collision_check(env, agent_idx);
+ int collision_state = env->entities[agent_idx].collision_state;
+
+ if (collision_state > NO_COLLISION) {
+ if (collision_state == VEHICLE_COLLISION && env->entities[agent_idx].respawn_timestep == -1) {
+ env->rewards[i] = env->reward_vehicle_collision;
+ env->logs[i].episode_return += env->reward_vehicle_collision;
+ env->logs[i].clean_collision_rate = 1.0f;
+ env->logs[i].collision_rate = 1.0f;
+ } else if (collision_state == OFFROAD) {
+ env->rewards[i] = env->reward_offroad_collision;
+ env->logs[i].offroad_rate = 1.0f;
+ env->logs[i].episode_return += env->reward_offroad_collision;
+ }
+ if (!env->entities[agent_idx].reached_goal_this_episode) {
+ env->entities[agent_idx].collided_before_goal = 1;
+ }
+ }
+
+ float distance_to_goal = relative_distance_2d(
+ env->entities[agent_idx].x, env->entities[agent_idx].y,
+ env->entities[agent_idx].goal_position_x, env->entities[agent_idx].goal_position_y);
+
+ if (distance_to_goal < MIN_DISTANCE_TO_GOAL) {
+ if (env->entities[agent_idx].respawn_timestep != -1) {
+ env->rewards[i] += env->reward_goal_post_respawn;
+ env->logs[i].episode_return += env->reward_goal_post_respawn;
+ } else {
+ env->rewards[i] += 1.0f;
+ env->logs[i].episode_return += 1.0f;
+ }
+ env->entities[agent_idx].reached_goal = 1;
+ env->entities[agent_idx].reached_goal_this_episode = 1;
+ }
+ }
+
+ // Respawn agents that reached goal
+ for (int i = 0; i < env->active_agent_count; i++) {
+ int agent_idx = env->active_agent_indices[i];
+ if (env->entities[agent_idx].reached_goal) {
+ respawn_agent(env, agent_idx);
+ }
+ }
+
+ compute_observations(env);
+}
+
+struct Client {
+ float width;
+ float height;
+ Texture2D puffers;
+ Vector3 camera_target;
+ float camera_zoom;
+ Camera3D camera;
+ Model cars[6];
+ int car_assignments[MAX_AGENTS];
+ Vector3 default_camera_position;
+ Vector3 default_camera_target;
+};
+
+Client* make_client(Drive* env) {
+ Client* client = (Client*)calloc(1, sizeof(Client));
+ client->width = 1280;
+ client->height = 704;
+ SetConfigFlags(FLAG_MSAA_4X_HINT);
+ InitWindow(client->width, client->height, "PufferLib Ray GPU Drive");
+ SetTargetFPS(30);
+ client->puffers = LoadTexture("resources/puffers_128.png");
+ client->cars[0] = LoadModel("resources/drive/RedCar.glb");
+ client->cars[1] = LoadModel("resources/drive/WhiteCar.glb");
+ client->cars[2] = LoadModel("resources/drive/BlueCar.glb");
+ client->cars[3] = LoadModel("resources/drive/YellowCar.glb");
+ client->cars[4] = LoadModel("resources/drive/GreenCar.glb");
+ client->cars[5] = LoadModel("resources/drive/GreyCar.glb");
+ for (int i = 0; i < MAX_AGENTS; i++) {
+ client->car_assignments[i] = (rand_r(&env->rng) % 4) + 1;
+ }
+ // Get initial target position from first active agent
+ float map_center_x = (env->map_corners[0] + env->map_corners[2]) / 2.0f;
+ float map_center_y = (env->map_corners[1] + env->map_corners[3]) / 2.0f;
+ Vector3 target_pos = {
+ 0,
+ 0, // Y is up
+ 1 // Z is depth
+ };
+
+ // Set up camera to look at target from above and behind
+ client->default_camera_position = (Vector3){
+ 0, // Same X as target
+ 120.0f, // 20 units above target
+ 175.0f // 20 units behind target
+ };
+ client->default_camera_target = target_pos;
+ client->camera.position = client->default_camera_position;
+ client->camera.target = client->default_camera_target;
+ client->camera.up = (Vector3){ 0.0f, -1.0f, 0.0f }; // Y is up
+ client->camera.fovy = 45.0f;
+ client->camera.projection = CAMERA_PERSPECTIVE;
+ client->camera_zoom = 1.0f;
+ return client;
+}
+
+// Camera control functions
+void handle_camera_controls(Client* client) {
+ static Vector2 prev_mouse_pos = {0};
+ static bool is_dragging = false;
+ float camera_move_speed = 0.5f;
+
+ // Handle mouse drag for camera movement
+ if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+ prev_mouse_pos = GetMousePosition();
+ is_dragging = true;
+ }
+
+ if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
+ is_dragging = false;
+ }
+
+ if (is_dragging) {
+ Vector2 current_mouse_pos = GetMousePosition();
+ Vector2 delta = {
+ (current_mouse_pos.x - prev_mouse_pos.x) * camera_move_speed,
+ -(current_mouse_pos.y - prev_mouse_pos.y) * camera_move_speed
+ };
+
+ // Update camera position (only X and Y)
+ client->camera.position.x += delta.x;
+ client->camera.position.y += delta.y;
+
+ // Update camera target (only X and Y)
+ client->camera.target.x += delta.x;
+ client->camera.target.y += delta.y;
+
+ prev_mouse_pos = current_mouse_pos;
+ }
+
+ // Handle mouse wheel for zoom
+ float wheel = GetMouseWheelMove();
+ if (wheel != 0) {
+ float zoom_factor = 1.0f - (wheel * 0.1f);
+ // Calculate the current direction vector from target to position
+ Vector3 direction = {
+ client->camera.position.x - client->camera.target.x,
+ client->camera.position.y - client->camera.target.y,
+ client->camera.position.z - client->camera.target.z
+ };
+
+ // Scale the direction vector by the zoom factor
+ direction.x *= zoom_factor;
+ direction.y *= zoom_factor;
+ direction.z *= zoom_factor;
+ client->camera.position.x = client->camera.target.x + direction.x;
+ client->camera.position.y = client->camera.target.y + direction.y;
+ client->camera.position.z = client->camera.target.z + direction.z;
+ }
+}
+
+void draw_agent_obs(Drive* env, int agent_index) {
+ float diamond_height = 3.0f;
+ float diamond_width = 1.5f;
+ float diamond_z = 8.0f;
+
+ Vector3 top = {0, 0, diamond_z + diamond_height / 2};
+ Vector3 bot = {0, 0, diamond_z - diamond_height / 2};
+ Vector3 fwd = {0, diamond_width / 2, diamond_z};
+ Vector3 bck = {0, -diamond_width / 2, diamond_z};
+ Vector3 lft = {-diamond_width / 2, 0, diamond_z};
+ Vector3 rgt = {diamond_width / 2, 0, diamond_z};
+
+ DrawTriangle3D(top, fwd, rgt, PUFF_CYAN);
+ DrawTriangle3D(top, rgt, bck, PUFF_CYAN);
+ DrawTriangle3D(top, bck, lft, PUFF_CYAN);
+ DrawTriangle3D(top, lft, fwd, PUFF_CYAN);
+ DrawTriangle3D(bot, rgt, fwd, PUFF_CYAN);
+ DrawTriangle3D(bot, bck, rgt, PUFF_CYAN);
+ DrawTriangle3D(bot, lft, bck, PUFF_CYAN);
+ DrawTriangle3D(bot, fwd, lft, PUFF_CYAN);
+
+ if (!IsKeyDown(KEY_LEFT_CONTROL)) return;
+
+ float (*observations)[OBS_SIZE] = (float(*)[OBS_SIZE])env->observations;
+ float* agent_obs = &observations[agent_index][0];
+
+ // Draw goal
+ float goal_x = agent_obs[0] / OBS_GOAL_SCALE;
+ float goal_y = agent_obs[1] / OBS_GOAL_SCALE;
+ DrawSphere((Vector3){goal_x, goal_y, 1}, 0.5f, GREEN);
+
+ // Draw partner observations
+ int obs_idx = EGO_FEATURES;
+ for (int j = 0; j < MAX_AGENTS - 1; j++) {
+ if (agent_obs[obs_idx] == 0 || agent_obs[obs_idx + 1] == 0) {
+ obs_idx += PARTNER_FEATURES;
+ continue;
+ }
+ float x = agent_obs[obs_idx] / OBS_POSITION_SCALE;
+ float y = agent_obs[obs_idx + 1] / OBS_POSITION_SCALE;
+ DrawLine3D((Vector3){0, 0, 0}, (Vector3){x, y, 1}, ORANGE);
+
+ float theta_x = agent_obs[obs_idx + 4];
+ float theta_y = agent_obs[obs_idx + 5];
+ float angle = atan2f(theta_y, theta_x);
+ // Draw an arrow above the car pointing in the direction that the partner is going
+ float arrow_length = 7.5f;
+
+ float ax = x + arrow_length * cosf(angle);
+ float ay = y + arrow_length * sinf(angle);
+ DrawLine3D((Vector3){x, y, 1}, (Vector3){ax, ay, 1}, PUFF_WHITE);
+
+ float arrow_size = 2.0f;
+ float dx = ax - x, dy = ay - y;
+ float len = sqrtf(dx * dx + dy * dy);
+ if (len > 0) {
+ dx /= len; dy /= len;
+ float px = -dy * arrow_size, py = dx * arrow_size;
+ DrawLine3D((Vector3){ax, ay, 1}, (Vector3){ax - dx * arrow_size + px, ay - dy * arrow_size + py, 1}, PUFF_WHITE);
+ DrawLine3D((Vector3){ax, ay, 1}, (Vector3){ax - dx * arrow_size - px, ay - dy * arrow_size - py, 1}, PUFF_WHITE);
+ }
+ obs_idx += PARTNER_FEATURES;
+ }
+
+ // Draw road edge observations
+ int map_start_idx = EGO_FEATURES + PARTNER_FEATURES * (MAX_AGENTS - 1);
+ for (int k = 0; k < MAX_ROAD_SEGMENT_OBSERVATIONS; k++) {
+ int idx = map_start_idx + k * ROAD_FEATURES;
+ if (agent_obs[idx] == 0 && agent_obs[idx + 1] == 0) continue;
+ int entity_type = (int)agent_obs[idx + 6];
+ if (entity_type + ROAD_LANE != ROAD_EDGE) continue;
+
+ float x_mid = agent_obs[idx] / OBS_POSITION_SCALE;
+ float y_mid = agent_obs[idx + 1] / OBS_POSITION_SCALE;
+ float rel_angle = atan2f(agent_obs[idx + 5], agent_obs[idx + 4]);
+ float seg_len = agent_obs[idx + 2] * MAX_ROAD_SEGMENT_LENGTH;
+
+ float x_start = x_mid - seg_len * cosf(rel_angle);
+ float y_start = y_mid - seg_len * sinf(rel_angle);
+ float x_end = x_mid + seg_len * cosf(rel_angle);
+ float y_end = y_mid + seg_len * sinf(rel_angle);
+
+ DrawLine3D((Vector3){0, 0, 0}, (Vector3){x_mid, y_mid, 1}, PUFF_CYAN);
+ DrawCube((Vector3){x_mid, y_mid, 1}, 0.5f, 0.5f, 0.5f, PUFF_CYAN);
+ DrawLine3D((Vector3){x_start, y_start, 1}, (Vector3){x_end, y_end, 1}, BLUE);
+ }
+}
+
+void draw_road_edge(Drive* env, float start_x, float start_y, float end_x, float end_y) {
+ Color CURB_TOP = (Color){220, 220, 220, 255};
+ Color CURB_SIDE = (Color){180, 180, 180, 255};
+ Color CURB_BOTTOM = (Color){160, 160, 160, 255};
+ float curb_height = 0.5f;
+ float curb_width = 0.3f;
+
+ Vector3 direction = {end_x - start_x, end_y - start_y, 0};
+ float length = sqrtf(direction.x * direction.x + direction.y * direction.y);
+ Vector3 nd = {direction.x / length, direction.y / length, 0};
+ Vector3 perp = {-nd.y, nd.x, 0};
+
+ Vector3 b1 = {start_x - perp.x * curb_width / 2, start_y - perp.y * curb_width / 2, 1.0f};
+ Vector3 b2 = {start_x + perp.x * curb_width / 2, start_y + perp.y * curb_width / 2, 1.0f};
+ Vector3 b3 = {end_x + perp.x * curb_width / 2, end_y + perp.y * curb_width / 2, 1.0f};
+ Vector3 b4 = {end_x - perp.x * curb_width / 2, end_y - perp.y * curb_width / 2, 1.0f};
+
+ DrawTriangle3D(b1, b2, b3, CURB_BOTTOM);
+ DrawTriangle3D(b1, b3, b4, CURB_BOTTOM);
+
+ Vector3 t1 = {b1.x, b1.y, b1.z + curb_height};
+ Vector3 t2 = {b2.x, b2.y, b2.z + curb_height};
+ Vector3 t3 = {b3.x, b3.y, b3.z + curb_height};
+ Vector3 t4 = {b4.x, b4.y, b4.z + curb_height};
+ DrawTriangle3D(t1, t3, t2, CURB_TOP);
+ DrawTriangle3D(t1, t4, t3, CURB_TOP);
+
+ DrawTriangle3D(b1, t1, b2, CURB_SIDE); DrawTriangle3D(t1, t2, b2, CURB_SIDE);
+ DrawTriangle3D(b2, t2, b3, CURB_SIDE); DrawTriangle3D(t2, t3, b3, CURB_SIDE);
+ DrawTriangle3D(b3, t3, b4, CURB_SIDE); DrawTriangle3D(t3, t4, b4, CURB_SIDE);
+ DrawTriangle3D(b4, t4, b1, CURB_SIDE); DrawTriangle3D(t4, t1, b1, CURB_SIDE);
+}
+
+void c_render(Drive* env) {
+ if (env->client == NULL) {
+ env->client = make_client(env);
+ }
+ Client* client = env->client;
+ BeginDrawing();
+ ClearBackground(ROAD_COLOR);
+ BeginMode3D(client->camera);
+ handle_camera_controls(client);
+
+ // Map bounds
+ DrawLine3D((Vector3){env->map_corners[0], env->map_corners[1], 0}, (Vector3){env->map_corners[2], env->map_corners[1], 0}, PUFF_CYAN);
+ DrawLine3D((Vector3){env->map_corners[0], env->map_corners[1], 0}, (Vector3){env->map_corners[0], env->map_corners[3], 0}, PUFF_CYAN);
+ DrawLine3D((Vector3){env->map_corners[2], env->map_corners[1], 0}, (Vector3){env->map_corners[2], env->map_corners[3], 0}, PUFF_CYAN);
+ DrawLine3D((Vector3){env->map_corners[0], env->map_corners[3], 0}, (Vector3){env->map_corners[2], env->map_corners[3], 0}, PUFF_CYAN);
+
+ for (int i = 0; i < env->num_entities; i++) {
+ // Draw vehicles
+ if (env->entities[i].type == VEHICLE || env->entities[i].type == PEDESTRIAN) {
+ bool is_active_agent = false;
+ bool is_static_agent = false;
+ int agent_index = -1;
+ for (int j = 0; j < env->active_agent_count; j++) {
+ if (env->active_agent_indices[j] == i) {
+ is_active_agent = true;
+ agent_index = j;
+ break;
+ }
+ }
+ for (int j = 0; j < env->static_agent_count; j++) {
+ if (env->static_agent_indices[j] == i) {
+ is_static_agent = true;
+ break;
+ }
+ }
+
+ if ((!is_active_agent && !is_static_agent) || env->entities[i].respawn_timestep != -1) {
+ continue;
+ }
+
+ Vector3 position = {env->entities[i].x, env->entities[i].y, 1};
+ float heading = env->entities[i].heading;
+ Vector3 size = {env->entities[i].length, env->entities[i].width, env->entities[i].height};
+
+ rlPushMatrix();
+ rlTranslatef(position.x, position.y, position.z);
+ rlRotatef(heading * RAD2DEG, 0.0f, 0.0f, 1.0f);
+
+ Model car_model = client->cars[5];
+ if (is_active_agent) {
+ car_model = client->cars[client->car_assignments[i % MAX_AGENTS]];
+ }
+ if (agent_index == env->human_agent_idx) {
+ // Human-controlled agent uses default model
+ }
+ if (is_active_agent && env->entities[i].collision_state > NO_COLLISION) {
+ car_model = client->cars[0];
+ }
+
+ if (agent_index == env->human_agent_idx && !env->entities[agent_index].reached_goal) {
+ draw_agent_obs(env, agent_index);
+ }
+
+ BoundingBox bounds = GetModelBoundingBox(car_model);
+ Vector3 model_size = {
+ bounds.max.x - bounds.min.x,
+ bounds.max.y - bounds.min.y,
+ bounds.max.z - bounds.min.z
+ };
+ Vector3 scale = {size.x / model_size.x, size.y / model_size.y, size.z / model_size.z};
+ DrawModelEx(car_model, (Vector3){0, 0, 0}, (Vector3){1, 0, 0}, 90.0f, scale, WHITE);
+ rlPopMatrix();
+
+ // Draw collision box
+ float cos_h = env->entities[i].heading_x;
+ float sin_h = env->entities[i].heading_y;
+ float hl = env->entities[i].length * 0.5f;
+ float hw = env->entities[i].width * 0.5f;
+ Vector3 corners[4] = {
+ {position.x + (hl * cos_h - hw * sin_h), position.y + (hl * sin_h + hw * cos_h), position.z},
+ {position.x + (hl * cos_h + hw * sin_h), position.y + (hl * sin_h - hw * cos_h), position.z},
+ {position.x + (-hl * cos_h - hw * sin_h), position.y + (-hl * sin_h + hw * cos_h), position.z},
+ {position.x + (-hl * cos_h + hw * sin_h), position.y + (-hl * sin_h - hw * cos_h), position.z}
+ };
+ for (int j = 0; j < 4; j++) {
+ DrawLine3D(corners[j], corners[(j + 1) % 4], PURPLE);
+ }
+
+ // FPV camera
+ if (IsKeyDown(KEY_SPACE) && env->human_agent_idx == agent_index) {
+ if (env->entities[agent_index].reached_goal) {
+ env->human_agent_idx = rand_r(&env->rng) % env->active_agent_count;
+ }
+ client->camera.position = (Vector3){
+ position.x - 25.0f * cosf(heading),
+ position.y - 25.0f * sinf(heading),
+ position.z + 15
+ };
+ client->camera.target = (Vector3){
+ position.x + 40.0f * cosf(heading),
+ position.y + 40.0f * sinf(heading),
+ position.z - 5.0f
+ };
+ client->camera.up = (Vector3){0, 0, 1};
+ }
+ if (IsKeyReleased(KEY_SPACE)) {
+ client->camera.position = client->default_camera_position;
+ client->camera.target = client->default_camera_target;
+ client->camera.up = (Vector3){0, 0, 1};
+ }
+
+ if (!is_active_agent || env->entities[i].valid == 0) continue;
+ if (!IsKeyDown(KEY_LEFT_CONTROL)) {
+ DrawSphere((Vector3){env->entities[i].goal_position_x, env->entities[i].goal_position_y, 1}, 0.5f, DARKGREEN);
+ }
+ }
+
+ // Draw road elements
+ if (env->entities[i].type < ROAD_LANE || env->entities[i].type > ROAD_EDGE) {
+ continue;
+ }
+ for (int j = 0; j < env->entities[i].array_size - 1; j++) {
+ if (env->entities[i].type != ROAD_EDGE) continue;
+ if (!IsKeyDown(KEY_LEFT_CONTROL)) {
+ draw_road_edge(env,
+ env->entities[i].traj_x[j], env->entities[i].traj_y[j],
+ env->entities[i].traj_x[j + 1], env->entities[i].traj_y[j + 1]);
+ }
+ }
+ }
+
+ // Grid overlay
+ float grid_start_x = env->map_corners[0];
+ float grid_start_y = env->map_corners[1];
+ for (int i = 0; i < env->grid_cols; i++) {
+ for (int j = 0; j < env->grid_rows; j++) {
+ float x = grid_start_x + i * GRID_CELL_SIZE;
+ float y = grid_start_y + j * GRID_CELL_SIZE;
+ DrawCubeWires(
+ (Vector3){x + GRID_CELL_SIZE / 2, y + GRID_CELL_SIZE / 2, 1},
+ GRID_CELL_SIZE, GRID_CELL_SIZE, 0.1f, PUFF_BACKGROUND2);
+ }
+ }
+ EndMode3D();
+
+ // Draw debug info
+ DrawText(TextFormat("Camera Position: (%.2f, %.2f, %.2f)",
+ client->camera.position.x, client->camera.position.y, client->camera.position.z), 10, 10, 20, PUFF_WHITE);
+ DrawText(TextFormat("Camera Target: (%.2f, %.2f, %.2f)",
+ client->camera.target.x, client->camera.target.y, client->camera.target.z), 10, 30, 20, PUFF_WHITE);
+ DrawText(TextFormat("Timestep: %d", env->timestep), 10, 50, 20, PUFF_WHITE);
+ int human_idx = env->active_agent_indices[env->human_agent_idx];
+ DrawText(TextFormat("Controlling Agent: %d", env->human_agent_idx), 10, 70, 20, PUFF_WHITE);
+ DrawText(TextFormat("Agent Index: %d", human_idx), 10, 90, 20, PUFF_WHITE);
+ DrawText("Controls: W/S - Accelerate/Brake, A/D - Steer, 1-4 - Switch Agent",
+ 10, client->height - 30, 20, PUFF_WHITE);
+ DrawText(TextFormat("Acceleration: %d", env->actions[env->human_agent_idx * 2]), 10, 110, 20, PUFF_WHITE);
+ DrawText(TextFormat("Steering: %d", env->actions[env->human_agent_idx * 2 + 1]), 10, 130, 20, PUFF_WHITE);
+ DrawText(TextFormat("Grid Rows: %d", env->grid_rows), 10, 150, 20, PUFF_WHITE);
+ DrawText(TextFormat("Grid Cols: %d", env->grid_cols), 10, 170, 20, PUFF_WHITE);
+ EndDrawing();
+}
+
+void close_client(Client* client) {
+ for (int i = 0; i < 6; i++) {
+ UnloadModel(client->cars[i]);
+ }
+ UnloadTexture(client->puffers);
+ CloseWindow();
+ free(client);
+}
diff --git a/ocean/drmario/binding.c b/ocean/drmario/binding.c
new file mode 100644
index 0000000000..53ec633f5a
--- /dev/null
+++ b/ocean/drmario/binding.c
@@ -0,0 +1,25 @@
+#include "drmario.h"
+
+#define OBS_SIZE 133
+#define NUM_ATNS 1
+#define ACT_SIZES {7}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env DrMario
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents=1;
+ env->n_rows = dict_get(kwargs, "n_rows")->value;
+ env->n_cols = dict_get(kwargs, "n_cols")->value;
+ env->n_init_viruses = dict_get(kwargs, "n_init_viruses")->value;
+ c_init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "viruses_cleared", log->viruses_cleared);
+}
diff --git a/ocean/drmario/drmario.c b/ocean/drmario/drmario.c
new file mode 100644
index 0000000000..5a4fe797dd
--- /dev/null
+++ b/ocean/drmario/drmario.c
@@ -0,0 +1,50 @@
+#include "drmario.h"
+
+int main() {
+ DrMario env = {0};
+ env.n_rows = 16;
+ env.n_cols = 8;
+ env.n_init_viruses = 14;
+ env.rng = (unsigned int)time(NULL);
+
+ allocate(&env);
+ c_reset(&env);
+
+ env.actions[0] = 0;
+ int frame = 0;
+ bool processLogic;
+ while (1) {
+ frame += 1;
+ processLogic = true;
+
+ if(IsKeyDown(KEY_LEFT_SHIFT)){
+ processLogic = frame % 3 == 0;
+
+ if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) {
+ env.actions[0] = ACTION_LEFT;
+ } else if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) {
+ env.actions[0] = ACTION_RIGHT;
+ } else if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) {
+ env.actions[0] = ACTION_DOWN;
+ } else if (IsKeyPressed(KEY_Z)) {
+ env.actions[0] = ACTION_ROTATE_LEFT;
+ } else if (IsKeyPressed(KEY_X)) {
+ env.actions[0] = ACTION_ROTATE_RIGHT;
+ } else if (IsKeyPressed(KEY_SPACE)) {
+ env.actions[0] = ACTION_DROP;
+ }
+
+ if (IsKeyPressed(KEY_R)) c_reset(&env);
+ }
+
+ if(processLogic){
+ c_step(&env);
+ env.actions[0] = 0;
+ }
+ c_render(&env);
+
+ }
+
+ free_allocated(&env);
+ return 0;
+}
\ No newline at end of file
diff --git a/ocean/drmario/drmario.h b/ocean/drmario/drmario.h
new file mode 100644
index 0000000000..fe4ccf7b7e
--- /dev/null
+++ b/ocean/drmario/drmario.h
@@ -0,0 +1,759 @@
+#include
+#include
+#include "raylib.h"
+#include
+#include
+
+#define SQUARE_SIZE 32
+#define TICKS_PER_FALL 3
+
+#define SCORE_SOFT_DROP 0.0f
+#define SCORE_HARD_DROP 0.0f
+#define SCORE_ROTATE 0.0f
+#define SCORE_KILL_VIRUS 1000.0f
+#define SCORE_PLACE_NEXT_TO_SAME_COLOR 10.0f
+#define SCORE_NO_LINE_CLEARS -10.0f
+#define SCORE_CLEAR_LINE 500.0f
+
+#define REWARD_SOFT_DROP 0.0f
+#define REWARD_HARD_DROP 0.0f
+#define REWARD_ROTATE 0.0f
+#define REWARD_KILL_VIRUS 1.0f
+#define REWARD_PLACE_NEXT_TO_SAME_COLOR 0.01f
+#define REWARD_NO_LINE_CLEARS -0.01f
+#define REWARD_CLEAR_LINE 0.5f
+#define REWARD_HEIGHT -0.02f
+
+#define ROTATION_0 0
+#define ROTATION_90 1
+#define ROTATION_180 2
+#define ROTATION_270 3
+
+#define ACTION_NO_OP 0
+#define ACTION_LEFT 1
+#define ACTION_RIGHT 2
+#define ACTION_DOWN 3
+#define ACTION_ROTATE_LEFT 4
+#define ACTION_ROTATE_RIGHT 5
+#define ACTION_DROP 6
+
+#define N_SCALAR_OBS 12
+#define N_OBS_PLANES 3
+
+// Required struct. Only use floats!
+typedef struct {
+ float perf; // Recommended 0-1 normalized single real number perf metric
+ float score; // Recommended unnormalized single real number perf metric
+ float episode_return; // Recommended metric: sum of agent rewards over episode
+ float episode_length; // Recommended metric: number of steps of agent episode
+ // Any extra fields you add here may be exported in binding.c
+ float viruses_cleared;
+
+ float n; // Required as the last field
+} Log;
+
+// Required that you have some struct for your env
+typedef struct {
+ int total_rows;
+ int total_columns;
+} Client;
+
+typedef struct {
+ Client *client;
+ Log log;
+
+ float *observations;
+ float *actions;
+ float *rewards;
+ float *terminals;
+ int dim_obs;
+
+ int num_agents;
+
+ int n_rows;
+ int n_cols;
+ int *grid;
+
+ int cap_color_a;
+ int cap_color_b;
+ int cap_orient;
+ int cap_row_1;
+ int cap_col_1;
+ int cap_row_2;
+ int cap_col_2;
+
+ bool cap_colliding_left;
+ bool cap_colliding_right;
+ bool cap_colliding_down;
+ bool cap_colliding_up;
+
+ float cap_colliding_color_hor_1;
+ float cap_colliding_color_hor_2;
+ float cap_colliding_color_ver_1;
+ float cap_colliding_color_ver_2;
+
+ int tick;
+ int tick_fall;
+ int ticks_per_fall;
+
+ int score;
+ int stage;
+
+ int viruses_remaining;
+ int n_init_viruses;
+
+ float episode_return;
+ int viruses_cleared;
+
+ int viruses_cleared_step;
+ int lines_cleared_step;
+
+ int atn_count_soft_drop;
+ int atn_count_hard_drop;
+ int atn_count_rotate;
+
+ unsigned int rng;
+} DrMario;
+
+
+void c_init(DrMario *env) {
+ env->grid = (int*)calloc(env->n_rows*env->n_cols, sizeof(int));
+ if (env->grid == NULL) {
+ exit(1);
+ }
+}
+
+void allocate(DrMario *env) {
+ c_init(env);
+ env->dim_obs = env->n_rows*env->n_cols*N_OBS_PLANES + N_SCALAR_OBS;
+ env->observations = (float *)calloc(env->dim_obs, sizeof(float));
+ if (env->observations == NULL) {
+ exit(1);
+ }
+ env->actions = (float *)calloc(1, sizeof(float));
+ if (env->actions == NULL) {
+ exit(1);
+ }
+ env->rewards = (float *)calloc(1, sizeof(float));
+ if (env->rewards == NULL) {
+ exit(1);
+ }
+ env->terminals = (float *)calloc(1, sizeof(float));
+ if (env->terminals == NULL) {
+ exit(1);
+ }
+}
+
+void c_close(DrMario *env) {
+ free(env->grid);
+ if (IsWindowReady()) {
+ CloseWindow();
+ }
+}
+
+void free_allocated(DrMario *env) {
+ free(env->actions);
+ free(env->observations);
+ free(env->terminals);
+ free(env->rewards);
+ c_close(env);
+}
+
+
+void add_log(DrMario *env) {
+ env->log.perf += env->viruses_cleared / (float)env->n_init_viruses;
+ env->log.score += env->score;
+ env->log.episode_length += env->tick;
+ env->log.episode_return += env->episode_return;
+ env->log.viruses_cleared += env->viruses_cleared;
+ env->log.n++;
+}
+
+
+void compute_observations(DrMario *env) {
+ int cells = env->n_rows * env->n_cols;
+
+ float* plane_occupied = env->observations;
+ float* plane_viruses = env->observations + cells;
+ float* plane_colors = env->observations + 2*cells;
+
+ for (int i = 0; i < cells; i++) {
+ int cell = env->grid[i];
+ plane_occupied[i] = cell != 0 ? 1.0f : 0.0f;
+ plane_viruses[i] = cell < 0 ? 1.0f : 0.0f;
+ plane_colors[i] = cell != 0 ? abs(cell) / 3.0f : 0.0f;
+ }
+
+ int r1 = env->cap_row_1, c1 = env->cap_col_1;
+ int r2 = env->cap_row_2, c2 = env->cap_col_2;
+ if (r1 >= 0 && r1 < env->n_rows && c1 >= 0 && c1 < env->n_cols) {
+ int i = r1*env->n_cols + c1;
+ plane_occupied[i] = 1.0f;
+ plane_viruses[i] = 0.0f;
+ plane_colors[i] = env->cap_color_a / 3.0f;
+ }
+ if (r2 >= 0 && r2 < env->n_rows && c2 >= 0 && c2 < env->n_cols) {
+ int i = r2*env->n_cols + c2;
+ plane_occupied[i] = 1.0f;
+ plane_viruses[i] = 0.0f;
+ plane_colors[i] = env->cap_color_b / 3.0f;
+ }
+
+ int off = cells * N_OBS_PLANES;
+ float safe_r1 = (r1 < 0) ? 0.0f : r1 / (float)(env->n_rows - 1);
+ float safe_r2 = (r2 < 0) ? 0.0f : r2 / (float)(env->n_rows - 1);
+ env->observations[off + 0] = env->cap_color_a / 3.0f;
+ env->observations[off + 1] = env->cap_color_b / 3.0f;
+ env->observations[off + 2] = env->cap_orient / 3.0f;
+ env->observations[off + 3] = safe_r1;
+ env->observations[off + 4] = c1 / (float)(env->n_cols - 1);
+ env->observations[off + 5] = safe_r2;
+ env->observations[off + 6] = c2 / (float)(env->n_cols - 1);
+ env->observations[off + 7] = env->viruses_remaining / (float)env->n_init_viruses;
+ env->observations[off + 8] = env->viruses_cleared_step / (float)env->n_init_viruses;
+ env->observations[off + 9] = env->lines_cleared_step / 4.0f;
+ env->observations[off + 10] = env->score / 10000.0f;
+ env->observations[off + 11] = env->tick / 2000.0f;
+}
+
+void place_viruses(DrMario *env) {
+ env->viruses_remaining = 0;
+ int placed = 0;
+ int attempts = 0;
+
+ while (placed < env->n_init_viruses && attempts < 1000) {
+ attempts++;
+ int r = (rand_r(&env->rng) % 8) + 8;
+ int c = rand_r(&env->rng) % env->n_cols;
+ int idx = r*env->n_cols + c;
+
+ if (env->grid[idx] != 0) {
+ continue;
+ }
+
+ int color = (rand_r(&env->rng) % 3) + 1;
+ env->grid[idx] = -color;
+ placed++;
+ env->viruses_remaining++;
+ }
+}
+
+void spawn_capsule(DrMario *env) {
+ env->cap_color_a = rand_r(&env->rng) % 3 + 1;
+ env->cap_color_b = rand_r(&env->rng) % 3 + 1;
+ env->cap_orient = ROTATION_0;
+ env->cap_row_1 = -1;
+ env->cap_col_1 = env->n_cols / 2;
+ env->cap_row_2 = env->cap_row_1;
+ env->cap_col_2 = env->cap_col_1 + 1;
+ env->tick_fall = 0;
+}
+
+void c_reset(DrMario *env) {
+ memset(env->grid, 0, env->n_rows*env->n_cols*sizeof(int));
+ env->score = 0;
+ env->tick = 0;
+ env->tick_fall = 0;
+
+ env->ticks_per_fall = TICKS_PER_FALL;
+ env->viruses_remaining = env->n_init_viruses;
+
+ env->episode_return = 0;
+ env->viruses_cleared = 0;
+ env->atn_count_soft_drop = 0;
+ env->atn_count_hard_drop = 0;
+ env->atn_count_rotate = 0;
+ place_viruses(env);
+ spawn_capsule(env);
+ compute_observations(env);
+}
+
+void get_collisions(DrMario* env) {
+ env->cap_colliding_left = false;
+ env->cap_colliding_right = false;
+ env->cap_colliding_down = false;
+ env->cap_colliding_up = false;
+
+ if (env->cap_row_1 < 0 || env->cap_row_2 < 0) {
+ return;
+ }
+
+ int below1 = (env->cap_row_1+1)*env->n_cols + env->cap_col_1;
+ int below2 = (env->cap_row_2+1)*env->n_cols + env->cap_col_2;
+ bool blocked_down = env->grid[below1] != 0 || env->grid[below2] != 0;
+ bool at_bottom = env->cap_row_1 == env->n_rows-1 || env->cap_row_2 == env->n_rows-1;
+ if (blocked_down || at_bottom) {
+ env->cap_colliding_down = true;
+ }
+
+ int above1 = (env->cap_row_1-1)*env->n_cols + env->cap_col_1;
+ int above2 = (env->cap_row_2-1)*env->n_cols + env->cap_col_2;
+ bool blocked_up = env->grid[above1] != 0 || env->grid[above2] != 0;
+ if (blocked_up) {
+ env->cap_colliding_up = true;
+ }
+
+ int right1 = env->cap_row_1*env->n_cols + env->cap_col_1 + 1;
+ int right2 = env->cap_row_2*env->n_cols + env->cap_col_2 + 1;
+ bool blocked_right = env->grid[right1] != 0 || env->grid[right2] != 0;
+ bool at_right_wall = env->cap_col_1 == env->n_cols-1 || env->cap_col_2 == env->n_cols-1;
+ if (blocked_right || at_right_wall) {
+ env->cap_colliding_right = true;
+ }
+
+ int left1 = env->cap_row_1*env->n_cols + env->cap_col_1 - 1;
+ int left2 = env->cap_row_2*env->n_cols + env->cap_col_2 - 1;
+ bool blocked_left = env->grid[left1] != 0 || env->grid[left2] != 0;
+ bool at_left_wall = env->cap_col_1 == 0 || env->cap_col_2 == 0;
+ if (blocked_left || at_left_wall) {
+ env->cap_colliding_left = true;
+ }
+}
+
+void get_color_collisions(DrMario* env) {
+ env->cap_colliding_color_hor_1 = 0.0f;
+ env->cap_colliding_color_ver_1 = 0.0f;
+ env->cap_colliding_color_hor_2 = 0.0f;
+ env->cap_colliding_color_ver_2 = 0.0f;
+
+ if (env->cap_row_1 < 0 || env->cap_row_2 < 0) {
+ return;
+ }
+
+ int cap_color_1 = abs(env->cap_color_a);
+ int cap_color_2 = abs(env->cap_color_b);
+ int cols = env->n_cols;
+
+ if (env->cap_row_1+1 != env->cap_row_2) {
+ int i = 1;
+ int color_up = abs(env->grid[(env->cap_row_1+i)*cols + env->cap_col_1]);
+ while (color_up == cap_color_1 && i < 100) {
+ env->cap_colliding_color_ver_1++;
+ i++;
+ color_up = abs(env->grid[(env->cap_row_1+i)*cols + env->cap_col_1]);
+ }
+ }
+
+ if (env->cap_row_1 >= 1 && env->cap_row_1-1 != env->cap_row_2) {
+ int i = 1;
+ int color_down = abs(env->grid[(env->cap_row_1-i)*cols + env->cap_col_1]);
+ while (color_down == cap_color_1 && i <= 100) {
+ env->cap_colliding_color_ver_1++;
+ i++;
+ color_down = abs(env->grid[(env->cap_row_1-i)*cols + env->cap_col_1]);
+ }
+ }
+
+ if (env->cap_col_1+1 != env->cap_col_2) {
+ int i = 1;
+ int color_right = abs(env->grid[env->cap_row_1*cols + env->cap_col_1+i]);
+ while (color_right == cap_color_1 && i <= 100) {
+ env->cap_colliding_color_hor_1++;
+ i++;
+ color_right = abs(env->grid[env->cap_row_1*cols + env->cap_col_1+i]);
+ }
+ }
+
+ if (env->cap_col_1 >= 1 && env->cap_col_1-1 != env->cap_col_2) {
+ int i = 1;
+ int color_left = abs(env->grid[env->cap_row_1*cols + env->cap_col_1-i]);
+ while (color_left == cap_color_1 && i <= 100) {
+ env->cap_colliding_color_hor_1++;
+ i++;
+ color_left = abs(env->grid[env->cap_row_1*cols + env->cap_col_1-i]);
+ }
+ }
+
+ if (env->cap_row_2+1 != env->cap_row_1) {
+ int i = 1;
+ int color_up = abs(env->grid[(env->cap_row_2+i)*cols + env->cap_col_2]);
+ while (color_up == cap_color_2 && i < 100) {
+ env->cap_colliding_color_ver_2++;
+ i++;
+ color_up = abs(env->grid[(env->cap_row_2+i)*cols + env->cap_col_2]);
+ }
+ }
+
+ if (env->cap_row_2 >= 1 && env->cap_row_2-1 != env->cap_row_1) {
+ int i = 1;
+ int color_down = abs(env->grid[(env->cap_row_2-i)*cols + env->cap_col_2]);
+ while (color_down == cap_color_2 && i <= 100) {
+ env->cap_colliding_color_ver_2++;
+ i++;
+ color_down = abs(env->grid[(env->cap_row_2-i)*cols + env->cap_col_2]);
+ }
+ }
+
+ if (env->cap_col_2+1 != env->cap_col_1) {
+ int i = 1;
+ int color_right = abs(env->grid[env->cap_row_2*cols + env->cap_col_2+i]);
+ while (color_right == cap_color_2 && i <= 100) {
+ env->cap_colliding_color_hor_2++;
+ i++;
+ color_right = abs(env->grid[env->cap_row_2*cols + env->cap_col_2+i]);
+ }
+ }
+
+ if (env->cap_col_2 >= 1 && env->cap_col_2-1 != env->cap_col_1) {
+ int i = 1;
+ int color_left = abs(env->grid[env->cap_row_2*cols + env->cap_col_2-i]);
+ while (color_left == cap_color_2 && i <= 100) {
+ env->cap_colliding_color_hor_2++;
+ i++;
+ color_left = abs(env->grid[env->cap_row_2*cols + env->cap_col_2-i]);
+ }
+ }
+}
+
+void rotate_cap(DrMario* env) {
+ int old_orient = env->cap_orient;
+ int old_cap_row_2 = env->cap_row_2;
+ int old_cap_col_2 = env->cap_col_2;
+
+ if (env->actions[0] == ACTION_ROTATE_LEFT) {
+ env->cap_orient = (env->cap_orient + 1) % 4;
+ } else if (env->actions[0] == ACTION_ROTATE_RIGHT) {
+ env->cap_orient = (env->cap_orient + 3) % 4;
+ } else {
+ return;
+ }
+
+ env->cap_row_2 = env->cap_row_1;
+ if (env->cap_orient == ROTATION_90) {
+ env->cap_row_2 -= 1;
+ } else if (env->cap_orient == ROTATION_270) {
+ env->cap_row_2 += 1;
+ }
+
+ env->cap_col_2 = env->cap_col_1;
+ if (env->cap_orient == ROTATION_0) {
+ env->cap_col_2 += 1;
+ } else if (env->cap_orient == ROTATION_180) {
+ env->cap_col_2 -= 1;
+ }
+
+ int idx = env->cap_row_2*env->n_cols + env->cap_col_2;
+ bool out_of_bounds = env->cap_row_2 < 0 || env->cap_row_2 >= env->n_rows
+ || env->cap_col_2 < 0 || env->cap_col_2 >= env->n_cols;
+ if (env->grid[idx] != 0 || out_of_bounds) {
+ env->cap_orient = old_orient;
+ env->cap_row_2 = old_cap_row_2;
+ env->cap_col_2 = old_cap_col_2;
+ return;
+ }
+
+ env->score += SCORE_ROTATE;
+ env->rewards[0] += REWARD_ROTATE;
+}
+
+void move_cap(DrMario* env) {
+ env->tick_fall += 1;
+ if (env->tick_fall >= env->ticks_per_fall) {
+ env->tick_fall = 0;
+ if (!env->cap_colliding_down) {
+ env->cap_row_1 += 1;
+ env->cap_row_2 += 1;
+ }
+ return;
+ }
+
+ if (env->cap_row_1 < 0 || env->cap_row_2 < 0) {
+ return;
+ }
+
+ if (env->actions[0] == ACTION_LEFT && !env->cap_colliding_left) {
+ env->cap_col_1 -= 1;
+ env->cap_col_2 -= 1;
+ } else if (env->actions[0] == ACTION_RIGHT && !env->cap_colliding_right) {
+ env->cap_col_1 += 1;
+ env->cap_col_2 += 1;
+ } else if (env->actions[0] == ACTION_DOWN && !env->cap_colliding_down) {
+ env->cap_row_1 += 1;
+ env->cap_row_2 += 1;
+
+ env->atn_count_soft_drop += 1;
+ env->score += SCORE_SOFT_DROP;
+ env->rewards[0] += REWARD_SOFT_DROP;
+ } else if (env->actions[0] == ACTION_DROP) {
+ if (!env->cap_colliding_down) {
+ env->atn_count_hard_drop += 1;
+ env->score += SCORE_HARD_DROP;
+ env->rewards[0] += REWARD_HARD_DROP;
+ do {
+ env->cap_row_1 += 1;
+ env->cap_row_2 += 1;
+ get_collisions(env);
+ } while (!env->cap_colliding_down);
+ }
+ }
+}
+
+void clear_lines(DrMario* env) {
+ int n = 1000;
+ int i = 0;
+
+ env->lines_cleared_step = 0;
+ env->viruses_cleared_step = 0;
+
+ while (i < n) {
+ bool *to_clear = (bool*)calloc(env->n_rows*env->n_cols, sizeof(bool));
+ if (!to_clear) {
+ break;
+ }
+
+ for (int r = 0; r < env->n_rows; r++) {
+ for (int c = 0; c < env->n_cols; c++) {
+ int cell = env->grid[r*env->n_cols + c];
+ if (cell == 0) {
+ continue;
+ }
+ int color = abs(cell);
+ int c_end = c + 1;
+ while (c_end < env->n_cols && abs(env->grid[r*env->n_cols + c_end]) == color) {
+ c_end += 1;
+ }
+ if (c_end - c >= 4) {
+ env->lines_cleared_step++;
+ for (int k = c; k < c_end; k++) {
+ to_clear[r*env->n_cols + k] = true;
+ }
+ }
+ c = c_end-1;
+ }
+ }
+
+ for (int c = 0; c < env->n_cols; c++) {
+ for (int r = 0; r < env->n_rows; r++) {
+ int cell = env->grid[r*env->n_cols + c];
+ if (cell == 0) {
+ continue;
+ }
+ int color = abs(cell);
+ int r_end = r + 1;
+ while (r_end < env->n_rows && abs(env->grid[r_end*env->n_cols + c]) == color) {
+ r_end += 1;
+ }
+ if (r_end - r >= 4) {
+ env->lines_cleared_step++;
+ for (int k = r; k < r_end; k++) {
+ to_clear[k*env->n_cols + c] = true;
+ }
+ }
+ r = r_end-1;
+ }
+ }
+
+ bool any_cleared = false;
+ for (int k = 0; k < env->n_rows*env->n_cols; k++) {
+ if (to_clear[k]) {
+ any_cleared = true;
+ break;
+ }
+ }
+
+ if (!any_cleared) {
+ free(to_clear);
+ break;
+ }
+
+ for (int k = 0; k < env->n_rows*env->n_cols; k++) {
+ if (to_clear[k]) {
+ if (env->grid[k] < 0) {
+ env->viruses_remaining--;
+ env->viruses_cleared++;
+ env->viruses_cleared_step++;
+ }
+ env->grid[k] = 0;
+ }
+ }
+
+ int m = 1000;
+ int j = 0;
+ while (j < m) {
+ bool falling = false;
+ for (int r = env->n_rows-2; r >= 0; r--) {
+ for (int c = 0; c < env->n_cols; c++) {
+ int cell = env->grid[r*env->n_cols + c];
+ if (cell <= 0 || env->grid[(r+1)*env->n_cols + c] != 0) {
+ continue;
+ }
+
+ bool neighbor_cleared = (r < env->n_rows-2 && to_clear[(r+1)*env->n_cols + c])
+ || (r > 0 && to_clear[(r-1)*env->n_cols + c])
+ || (c < env->n_cols-1 && to_clear[r*env->n_cols + c+1])
+ || (c > 0 && to_clear[r*env->n_cols + c-1]);
+ if (neighbor_cleared) {
+ to_clear[r*env->n_cols + c] = true;
+ }
+
+ if (to_clear[r*env->n_cols + c]) {
+ env->grid[(r+1)*env->n_cols + c] = cell;
+ env->grid[r*env->n_cols + c] = 0;
+ falling = true;
+ }
+ }
+ }
+ if (!falling) {
+ break;
+ }
+ j++;
+ }
+
+ free(to_clear);
+ i++;
+ }
+}
+
+void spawn_new_cap(DrMario* env) {
+ if (!env->cap_colliding_down) {
+ return;
+ }
+
+ env->grid[env->cap_row_1*env->n_cols + env->cap_col_1] = env->cap_color_a;
+ env->grid[env->cap_row_2*env->n_cols + env->cap_col_2] = env->cap_color_b;
+
+ int row = env->cap_row_1 > env->cap_row_2 ? env->cap_row_1 : env->cap_row_2;
+ env->rewards[0] += row*REWARD_HEIGHT;
+
+ get_color_collisions(env);
+
+ int color_collisions = 0;
+ if (env->cap_colliding_color_hor_1 >= 2) {
+ color_collisions += env->cap_colliding_color_hor_1;
+ }
+ if (env->cap_colliding_color_hor_2 >= 2) {
+ color_collisions += env->cap_colliding_color_hor_2;
+ }
+ if (env->cap_colliding_color_ver_1 >= 2) {
+ color_collisions += env->cap_colliding_color_ver_1;
+ }
+ if (env->cap_colliding_color_ver_2 >= 2) {
+ color_collisions += env->cap_colliding_color_ver_2;
+ }
+
+ if (color_collisions > 0) {
+ env->score += color_collisions*SCORE_PLACE_NEXT_TO_SAME_COLOR;
+ env->rewards[0] += color_collisions*REWARD_PLACE_NEXT_TO_SAME_COLOR;
+ }
+
+ clear_lines(env);
+ if (env->viruses_cleared_step > 0) {
+ env->rewards[0] += env->viruses_cleared_step*REWARD_KILL_VIRUS;
+ env->score += env->viruses_cleared_step*SCORE_KILL_VIRUS;
+ }
+
+ if (env->lines_cleared_step > 0) {
+ env->rewards[0] += env->lines_cleared_step*REWARD_CLEAR_LINE;
+ env->score += env->lines_cleared_step*SCORE_CLEAR_LINE;
+ }
+
+ if (env->lines_cleared_step == 0 && env->viruses_cleared_step == 0) {
+ env->rewards[0] += REWARD_NO_LINE_CLEARS;
+ env->score += SCORE_NO_LINE_CLEARS;
+ }
+
+ spawn_capsule(env);
+}
+
+void end_game_check(DrMario* env) {
+ if (env->viruses_remaining <= 0) {
+ float speed_bonus = 1.0f / (1.0f + env->tick*0.001f);
+ env->rewards[0] += 1.0f + speed_bonus;
+ env->terminals[0] = 1;
+ add_log(env);
+ c_reset(env);
+ return;
+ }
+
+ bool cap_at_top = env->cap_colliding_down && (env->cap_row_1 <= 0 || env->cap_row_2 <= 0);
+ if (!cap_at_top) {
+ return;
+ }
+ float fraction_remaining = env->viruses_remaining / (float)env->n_init_viruses;
+ env->rewards[0] -= 1.0f + fraction_remaining*0.5f;
+ env->terminals[0] = 1;
+ add_log(env);
+ c_reset(env);
+}
+
+void c_step(DrMario *env) {
+ env->tick += 1;
+ env->terminals[0] = 0;
+ env->rewards[0] = 0;
+
+ env->lines_cleared_step = 0;
+ env->viruses_cleared_step = 0;
+
+ get_collisions(env);
+
+ if (!env->cap_colliding_down) {
+ rotate_cap(env);
+ get_collisions(env);
+ }
+
+ move_cap(env);
+ get_collisions(env);
+
+ end_game_check(env);
+ spawn_new_cap(env);
+
+ env->episode_return += env->rewards[0];
+
+ compute_observations(env);
+}
+
+void c_render(DrMario *env) {
+ if (!IsWindowReady()) {
+ InitWindow(SQUARE_SIZE*env->n_cols, SQUARE_SIZE*env->n_rows, "Dr Mario");
+ SetTargetFPS(30);
+ }
+ if (IsKeyDown(KEY_ESCAPE)) {
+ exit(0);
+ }
+
+ BeginDrawing();
+ ClearBackground(BLACK);
+
+ for (int r = 0; r < env->n_rows; r++) {
+ for (int c = 0; c < env->n_cols; c++) {
+ int cell = env->grid[r*env->n_cols + c];
+ int x = c*SQUARE_SIZE;
+ int y = r*SQUARE_SIZE;
+ if (cell == 0) {
+ continue;
+ }
+
+ Color color;
+ if (cell == 1 || cell == -1) {
+ color = RED;
+ } else if (cell == 2 || cell == -2) {
+ color = BLUE;
+ } else {
+ color = YELLOW;
+ }
+
+ if (cell < 0) {
+ DrawCircle(x + SQUARE_SIZE/2, y + SQUARE_SIZE/2,
+ SQUARE_SIZE/2 - 2, color);
+ } else {
+ DrawRectangle(x + 2, y + 2,
+ SQUARE_SIZE - 4, SQUARE_SIZE - 4, color);
+ }
+ }
+ }
+
+ int x1 = env->cap_col_1*SQUARE_SIZE;
+ int y1 = env->cap_row_1*SQUARE_SIZE;
+ int x2 = env->cap_col_2*SQUARE_SIZE;
+ int y2 = env->cap_row_2*SQUARE_SIZE;
+ Color ca = (env->cap_color_a == 1) ? RED : (env->cap_color_a == 2) ? BLUE : YELLOW;
+ Color cb = (env->cap_color_b == 1) ? RED : (env->cap_color_b == 2) ? BLUE : YELLOW;
+
+ DrawRectangle(x1 + 2, y1 + 2, SQUARE_SIZE - 4, SQUARE_SIZE - 4, ca);
+ DrawRectangle(x2 + 2, y2 + 2, SQUARE_SIZE - 4, SQUARE_SIZE - 4, cb);
+
+ DrawText(TextFormat("Viruses: %d", env->viruses_remaining), 4, 4, 14, WHITE);
+ DrawText(TextFormat("Score: %d", env->score), 4, 20, 14, WHITE);
+
+ EndDrawing();
+}
\ No newline at end of file
diff --git a/ocean/drone/binding.c b/ocean/drone/binding.c
new file mode 100644
index 0000000000..ac07e0845f
--- /dev/null
+++ b/ocean/drone/binding.c
@@ -0,0 +1,147 @@
+#include "drone.h"
+#include "render.h"
+
+#define OBS_SIZE DRONE_OBS_SIZE
+#define NUM_ATNS 4
+#define ACT_SIZES {1, 1, 1, 1}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env DroneEnv
+#include "vecenv.h"
+
+
+static float task_fracs[NUM_TASKS];
+
+static void hover_config(DroneEnv* env, Dict* kwargs) {
+ HoverConfig* cfg = (HoverConfig*)calloc(1, sizeof(HoverConfig));
+ cfg->target_dist = dict_get(kwargs, "hover_target_dist")->value;
+ cfg->alpha_hover = dict_get(kwargs, "alpha_hover")->value;
+ cfg->alpha_dist = dict_get(kwargs, "hover_alpha_dist")->value;
+ cfg->sphere_radius = dict_get(kwargs, "sphere_radius")->value;
+ cfg->horizon = (int)dict_get(kwargs, "hover_horizon")->value;
+ env->task_config = cfg;
+}
+
+static void race_config(DroneEnv* env, Dict* kwargs) {
+ RaceConfig* cfg = (RaceConfig*)calloc(1, sizeof(RaceConfig));
+ cfg->max_rings = (int)dict_get(kwargs, "max_rings")->value;
+ cfg->ring_reward = dict_get(kwargs, "ring_reward")->value;
+ cfg->alpha_dist = dict_get(kwargs, "race_alpha_dist")->value;
+ cfg->horizon = (int)dict_get(kwargs, "race_horizon")->value;
+ env->task_config = cfg;
+}
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = (int)dict_get(kwargs, "num_drones")->value;
+
+ env->alpha_vel = dict_get(kwargs, "alpha_vel")->value;
+ env->alpha_omega = dict_get(kwargs, "alpha_omega")->value;
+ env->alpha_action = dict_get(kwargs, "alpha_action")->value;
+ env->dr = dict_get(kwargs, "dr")->value;
+
+ env->integrator = (int)dict_get(kwargs, "use_rk2")->value;
+
+ task_fracs[TASK_HOVER] = dict_get(kwargs, "hover_frac")->value;
+ task_fracs[TASK_RACE] = dict_get(kwargs, "race_frac")->value;
+ task_fracs[TASK_SPHERE] = dict_get(kwargs, "sphere_frac")->value;
+ task_fracs[TASK_CUBE] = dict_get(kwargs, "cube_frac")->value;
+ task_fracs[TASK_FLAG] = dict_get(kwargs, "flag_frac")->value;
+
+ float total = 0.0f;
+ for (int t = 0; t < NUM_TASKS; t++) {
+ total += task_fracs[t];
+ }
+
+ int idx = (int)env->rng;
+ float cum = 0.0f;
+ env->task = TASK_HOVER;
+ for (int t = 0; t < NUM_TASKS; t++) {
+ cum += task_fracs[t] / total;
+ if ((int)floorf((idx + 1) * cum) > (int)floorf(idx * cum)) {
+ env->task = (TaskType)t;
+ break;
+ }
+ }
+
+ if (env->task == TASK_RACE) {
+ race_config(env, kwargs);
+ } else {
+ hover_config(env, kwargs);
+ }
+
+ task_init(env);
+ init(env);
+}
+
+static inline float task_avg(float sum, float n) { return n > 0.0f ? sum / n : 0.0f; }
+
+void my_log(Log* log, Dict* out) {
+ static int first = 1;
+
+ float perf = 0.0f, score = 0.0f;
+ int active = 0;
+ for (int t = 0; t < NUM_TASKS; t++) {
+ float n = log->task[t].n;
+ if (n <= 0.0f) continue;
+ perf += log->task[t].perf / n;
+ score += log->task[t].score / n;
+ active++;
+ }
+ dict_set(out, "perf", active > 0 ? perf / active : 0.0f);
+ dict_set(out, "score", active > 0 ? score / active : 0.0f);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+
+ if (log->task[TASK_HOVER].n > 0.0f || (first && task_fracs[TASK_HOVER] > 0.0f)) {
+ TaskLog* h = &log->task[TASK_HOVER];
+ dict_set(out, "hover/perf", task_avg(h->perf, h->n));
+ dict_set(out, "hover/score", task_avg(h->score, h->n));
+ dict_set(out, "hover/ema_dist", task_avg(h->keys[0], h->n));
+ dict_set(out, "hover/ema_vel", task_avg(h->keys[1], h->n));
+ dict_set(out, "hover/ema_omega", task_avg(h->keys[2], h->n));
+ dict_set(out, "hover/oob", task_avg(h->keys[3], h->n));
+ dict_set(out, "hover/episode_frac", h->n);
+ }
+ if (log->task[TASK_RACE].n > 0.0f || (first && task_fracs[TASK_RACE] > 0.0f)) {
+ TaskLog* r = &log->task[TASK_RACE];
+ dict_set(out, "race/perf", task_avg(r->perf, r->n));
+ dict_set(out, "race/score", task_avg(r->score, r->n));
+ dict_set(out, "race/rings_passed", task_avg(r->keys[0], r->n));
+ dict_set(out, "race/ring_collisions", task_avg(r->keys[1], r->n));
+ dict_set(out, "race/completed", task_avg(r->keys[2], r->n));
+ dict_set(out, "race/oob", task_avg(r->keys[3], r->n));
+ dict_set(out, "race/episode_frac", r->n);
+ }
+ if (log->task[TASK_SPHERE].n > 0.0f || (first && task_fracs[TASK_SPHERE] > 0.0f)) {
+ TaskLog* s = &log->task[TASK_SPHERE];
+ dict_set(out, "sphere/perf", task_avg(s->perf, s->n));
+ dict_set(out, "sphere/score", task_avg(s->score, s->n));
+ dict_set(out, "sphere/ema_dist", task_avg(s->keys[0], s->n));
+ dict_set(out, "sphere/ema_vel", task_avg(s->keys[1], s->n));
+ dict_set(out, "sphere/ema_omega", task_avg(s->keys[2], s->n));
+ dict_set(out, "sphere/oob", task_avg(s->keys[3], s->n));
+ dict_set(out, "sphere/episode_frac", s->n);
+ }
+ if (log->task[TASK_CUBE].n > 0.0f || (first && task_fracs[TASK_CUBE] > 0.0f)) {
+ TaskLog* c = &log->task[TASK_CUBE];
+ dict_set(out, "cube/perf", task_avg(c->perf, c->n));
+ dict_set(out, "cube/score", task_avg(c->score, c->n));
+ dict_set(out, "cube/ema_dist", task_avg(c->keys[0], c->n));
+ dict_set(out, "cube/ema_vel", task_avg(c->keys[1], c->n));
+ dict_set(out, "cube/ema_omega", task_avg(c->keys[2], c->n));
+ dict_set(out, "cube/oob", task_avg(c->keys[3], c->n));
+ dict_set(out, "cube/episode_frac", c->n);
+ }
+ if (log->task[TASK_FLAG].n > 0.0f || (first && task_fracs[TASK_FLAG] > 0.0f)) {
+ TaskLog* f = &log->task[TASK_FLAG];
+ dict_set(out, "flag/perf", task_avg(f->perf, f->n));
+ dict_set(out, "flag/score", task_avg(f->score, f->n));
+ dict_set(out, "flag/ema_dist", task_avg(f->keys[0], f->n));
+ dict_set(out, "flag/ema_vel", task_avg(f->keys[1], f->n));
+ dict_set(out, "flag/ema_omega", task_avg(f->keys[2], f->n));
+ dict_set(out, "flag/oob", task_avg(f->keys[3], f->n));
+ dict_set(out, "flag/episode_frac", f->n);
+ }
+
+ first = 0;
+}
\ No newline at end of file
diff --git a/ocean/drone/drone.c b/ocean/drone/drone.c
new file mode 100644
index 0000000000..aa3c19e500
--- /dev/null
+++ b/ocean/drone/drone.c
@@ -0,0 +1,110 @@
+#include "drone.h"
+#include "puffernet.h"
+#include "render.h"
+#include
+
+#ifdef __EMSCRIPTEN__
+#include
+#endif
+
+// demo config
+static void setup_task(DroneEnv* env, int task) {
+ task_close(env);
+ env->task = task;
+
+ if (task == TASK_RACE) {
+ RaceConfig* cfg = (RaceConfig*)calloc(1, sizeof(RaceConfig));
+ cfg->max_rings = 10;
+ cfg->horizon = 2048;
+ env->task_config = cfg;
+ } else {
+ HoverConfig* cfg = (HoverConfig*)calloc(1, sizeof(HoverConfig));
+ cfg->target_dist = 5.0f;
+ cfg->sphere_radius = 4.0f;
+ cfg->horizon = 1024;
+ env->task_config = cfg;
+ }
+ task_init(env);
+ c_reset(env);
+}
+
+// we render at 60Hz, but drone frames are 100Hz
+static void step_realtime(DroneEnv* env, PufferNet* net) {
+ static double accum = 0.0;
+ accum += GetFrameTime();
+ if (accum > 0.25) accum = 0.25;
+ while (accum >= ACTION_DT) {
+ forward_puffernet(net, env->observations, env->actions);
+ c_step(env);
+ accum -= ACTION_DT;
+ }
+}
+
+static bool tab_swap_pressed(void) {
+ static bool prev_down = false;
+ bool down = IsKeyDown(KEY_TAB);
+ bool edge = down && !prev_down;
+ prev_down = down;
+ return edge;
+}
+
+#ifdef __EMSCRIPTEN__
+typedef struct {
+ DroneEnv* env;
+ PufferNet* net;
+} WebRenderArgs;
+
+void emscriptenStep(void* e) {
+ WebRenderArgs* args = (WebRenderArgs*)e;
+ if (tab_swap_pressed()) setup_task(args->env, (args->env->task + 1) % NUM_TASKS);
+ step_realtime(args->env, args->net);
+ c_render(args->env);
+}
+#endif
+
+int main(int argc, char** argv) {
+ srand(time(NULL));
+
+ int task = argc > 1 ? atoi(argv[1]) : TASK_RACE;
+
+ DroneEnv* env = calloc(1, sizeof(DroneEnv));
+ env->num_agents = 64;
+ env->dr = 0.05f; // static 5% flat DR for the demo
+
+ env->observations = (float*)calloc(env->num_agents * DRONE_OBS_SIZE, sizeof(float));
+ env->actions = (float*)calloc(env->num_agents * 4, sizeof(float));
+ env->rewards = (float*)calloc(env->num_agents, sizeof(float));
+ env->terminals = (float*)calloc(env->num_agents, sizeof(float));
+
+ init(env);
+ setup_task(env, task);
+
+ Weights* weights = load_weights("resources/drone/drone_weights.bin");
+ int logit_sizes[4] = {1, 1, 1, 1};
+ PufferNet* net = make_puffernet(weights, env->num_agents, DRONE_OBS_SIZE, 64, 2, logit_sizes, 4);
+
+#ifdef __EMSCRIPTEN__
+ WebRenderArgs args = {.env = env, .net = net};
+ emscripten_set_main_loop_arg(emscriptenStep, &args, 0, true);
+#else
+ c_render(env);
+ SetTargetFPS(60);
+
+ while (!WindowShouldClose()) {
+ if (tab_swap_pressed()) setup_task(env, (env->task + 1) % NUM_TASKS);
+ step_realtime(env, net);
+ c_render(env);
+ }
+
+ c_close(env);
+ free_puffernet(net);
+ free(weights);
+ free(env->observations);
+ free(env->actions);
+ free(env->rewards);
+ free(env->terminals);
+ free(env);
+#endif
+
+ return 0;
+}
\ No newline at end of file
diff --git a/ocean/drone/drone.h b/ocean/drone/drone.h
new file mode 100644
index 0000000000..cb8ffaca05
--- /dev/null
+++ b/ocean/drone/drone.h
@@ -0,0 +1,188 @@
+// Originally made by Sam Turner and Finlay Sanders, 2025.
+// Included in pufferlib under the original project's MIT license.
+// https://github.com/tensaur/drone
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "dronelib.h"
+#include "physics.h"
+
+typedef enum {
+ TASK_HOVER = 0,
+ TASK_RACE = 1,
+ TASK_SPHERE = 2,
+ TASK_CUBE = 3,
+ TASK_FLAG = 4,
+} TaskType;
+
+#define NUM_TASKS 5
+
+typedef struct {
+ float dist;
+ float prev_dist;
+ float vel;
+ float omega;
+} StepCache;
+
+typedef struct {
+ float n;
+ float perf;
+ float score;
+ float keys[4];
+} TaskLog;
+
+typedef struct Log Log;
+struct Log {
+ float episode_return;
+ float episode_length;
+ float n;
+ TaskLog task[NUM_TASKS];
+};
+
+typedef struct DroneEnv DroneEnv;
+typedef struct Client Client;
+
+struct DroneEnv {
+ float* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ int num_agents;
+ unsigned int rng;
+
+ Drone* agents;
+ Physics physics;
+ Log log;
+
+ TaskType task;
+ void* task_config;
+ void* task_state;
+
+ // shared reward shaping
+ float alpha_vel;
+ float alpha_omega;
+ float alpha_action;
+
+ // domain randomisation
+ float dr;
+
+ // physics integrator
+ int integrator;
+
+ Client* client;
+};
+
+#include "tasklib.h"
+
+void init(DroneEnv* env) {
+ env->agents = (Drone*)calloc(env->num_agents, sizeof(Drone));
+ for (int i = 0; i < env->num_agents; i++)
+ env->agents[i].target = (Target*)calloc(1, sizeof(Target));
+
+ physics_init(&env->physics, env->num_agents, env->integrator);
+
+ env->log = (Log){0};
+}
+
+void add_log(DroneEnv* env, int idx, StepCache* cache) {
+ Drone* agent = &env->agents[idx];
+ env->log.episode_return += agent->episode_return;
+ env->log.episode_length += agent->episode_length;
+ env->log.n += 1.0f;
+
+ task_log(env, agent, idx, &env->log, cache);
+}
+
+void reset_agent(DroneEnv* env, int idx) {
+ Drone* agent = &env->agents[idx];
+ Target* target = agent->target;
+ memset(agent, 0, sizeof(Drone));
+ agent->target = target;
+
+ init_drone(agent, &env->rng, env->dr);
+ task_reset(env, agent, idx);
+ physics_set_drone(&env->physics, idx, &agent->params, &agent->state);
+ agent->prev_pos = agent->state.pos;
+}
+
+void compute_observations(DroneEnv* env) {
+ bool is_race = (env->task == TASK_RACE);
+ for (int i = 0; i < env->num_agents; i++)
+ compute_drone_observations(&env->agents[i], env->observations + i * DRONE_OBS_SIZE, is_race);
+}
+
+void c_reset(DroneEnv* env) {
+ task_env_reset(env);
+
+ for (int i = 0; i < env->num_agents; i++)
+ reset_agent(env, i);
+
+ compute_observations(env);
+}
+
+void c_step(DroneEnv* env) {
+ for (int i = 0; i < env->num_agents; i++)
+ env->agents[i].prev_pos = env->agents[i].state.pos;
+
+ physics_step(&env->physics, env->actions);
+
+ for (int i = 0; i < env->num_agents; i++) {
+ Drone* agent = &env->agents[i];
+ agent->episode_length++;
+
+ agent->state = physics_get_state(&env->physics, i);
+ StepCache cache = {
+ .prev_dist = norm3(sub3(agent->target->pos, agent->prev_pos)),
+ .dist = norm3(sub3(agent->target->pos, agent->state.pos)),
+ .vel = norm3(agent->state.vel),
+ .omega = norm3(agent->state.omega),
+ };
+
+ float reward = task_reward(env, agent, i, &cache);
+ reward -= env->alpha_vel * cache.vel;
+ reward -= env->alpha_omega * cache.omega;
+
+ float* action = &env->actions[4 * i];
+ if (agent->episode_length > 1) {
+ float da = 0.0f;
+ for (int k = 0; k < 4; k++) {
+ float d = action[k] - agent->prev_action[k];
+ da += d * d;
+ }
+ reward -= env->alpha_action * da;
+ }
+ for (int k = 0; k < 4; k++) agent->prev_action[k] = action[k];
+
+ bool done = task_done(env, agent, i, &cache);
+
+ agent->episode_return += reward;
+ env->rewards[i] = reward;
+ env->terminals[i] = done ? 1.0f : 0.0f;
+
+ if (done) {
+ add_log(env, i, &cache);
+ reset_agent(env, i);
+ }
+ }
+
+ compute_observations(env);
+}
+
+void c_close_client(Client* client);
+
+void c_close(DroneEnv* env) {
+ task_close(env);
+
+ for (int i = 0; i < env->num_agents; i++)
+ free(env->agents[i].target);
+ free(env->agents);
+ physics_close(&env->physics);
+
+ if (env->client != NULL) c_close_client(env->client);
+}
\ No newline at end of file
diff --git a/ocean/drone/dronelib.h b/ocean/drone/dronelib.h
new file mode 100644
index 0000000000..05b9c447ac
--- /dev/null
+++ b/ocean/drone/dronelib.h
@@ -0,0 +1,278 @@
+// Originally made by Sam Turner and Finlay Sanders, 2025.
+// Included in pufferlib under the original project's MIT license.
+// https://github.com/tensaur/drone
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+// Visualisation properties
+#define WIDTH 1080
+#define HEIGHT 720
+#define TRAIL_LENGTH 50
+
+// Crazyflie Physical Constants
+// https://github.com/arplaboratory/learning-to-fly
+#define BASE_MASS 0.027f // kg
+#define BASE_IXX 3.85e-6f // kgm²
+#define BASE_IYY 3.85e-6f // kgm²
+#define BASE_IZZ 5.9675e-6f // kgm²
+#define BASE_ARM_LEN 0.0396f // m
+#define BASE_K_THRUST 3.16e-10f // thrust coefficient
+#define BASE_K_DRAG 0.005964552f // yaw moment constant
+#define BASE_GRAVITY 9.81f // m/s^2
+#define BASE_MAX_RPM 21702.0f // RPM
+#define BASE_K_MOT 0.15f // s (RPM time constant)
+
+#define BASE_K_ANG_DAMP 0.0f // angular damping coefficient
+#define BASE_B_DRAG 0.0f // linear drag coefficient
+#define BASE_MAX_VEL 20.0f // m/s
+#define BASE_MAX_OMEGA 20.0f // rad/s
+
+// Simulation properties
+#define GRID_X 10.0f
+#define GRID_Y 10.0f
+#define GRID_Z 5.0f
+#define MARGIN_X (GRID_X - 1)
+#define MARGIN_Y (GRID_Y - 1)
+#define MARGIN_Z (GRID_Z - 1)
+#define RING_RADIUS 0.5f
+#define V_TARGET 0.05f
+
+#define DRONE_OBS_SIZE 21
+
+// Core Parameters
+#define DT 0.002f // 500 Hz
+#define ACTION_SUBSTEPS 5
+#define ACTION_DT (DT * (float)ACTION_SUBSTEPS) // 100 Hz
+
+#define DT_RNG 0.0f
+
+#define MAX_DIST \
+ sqrtf((2 * GRID_X) * (2 * GRID_X) + (2 * GRID_Y) * (2 * GRID_Y) + (2 * GRID_Z) * (2 * GRID_Z))
+
+typedef struct {
+ float w, x, y, z;
+} Quat;
+
+typedef struct {
+ float x, y, z;
+} Vec3;
+
+typedef struct {
+ Vec3 pos;
+ Vec3 vel;
+ Quat orientation;
+ Vec3 normal;
+ float radius;
+} Target;
+
+typedef struct {
+ Vec3 pos[TRAIL_LENGTH];
+ int index;
+ int count;
+} Trail;
+
+typedef struct {
+ Vec3 pos;
+ Vec3 vel;
+ Quat quat;
+ Vec3 omega;
+ float rpms[4];
+} State;
+
+typedef struct {
+ float mass;
+ float ixx;
+ float iyy;
+ float izz;
+ float arm_len;
+ float k_thrust;
+ float k_ang_damp;
+ float k_drag;
+ float b_drag;
+ float gravity;
+ float max_rpm;
+ float max_vel;
+ float max_omega;
+ float k_mot;
+
+ float inv_mass;
+ float inv_ixx;
+ float inv_iyy;
+ float inv_izz;
+ float inv_k_mot;
+} Params;
+
+typedef struct {
+ State state;
+ Params params;
+ Vec3 prev_pos;
+ float prev_action[4];
+ Target* target;
+
+ float episode_return;
+ int episode_length;
+} Drone;
+
+// math
+
+static inline float clampf(float v, float min, float max) {
+ if (v < min) return min;
+ if (v > max) return max;
+ return v;
+}
+
+static inline float rndf(float a, float b, unsigned int* rng) {
+ return a + ((float)rand_r(rng) / (float)RAND_MAX) * (b - a);
+}
+
+static inline Vec3 add3(Vec3 a, Vec3 b) { return (Vec3){a.x + b.x, a.y + b.y, a.z + b.z}; }
+static inline Vec3 sub3(Vec3 a, Vec3 b) { return (Vec3){a.x - b.x, a.y - b.y, a.z - b.z}; }
+static inline Vec3 scalmul3(Vec3 a, float b) { return (Vec3){a.x * b, a.y * b, a.z * b}; }
+
+static inline float dot3(Vec3 a, Vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
+static inline float norm3(Vec3 a) { return sqrtf(dot3(a, a)); }
+
+static inline void clamp4(float a[4], float min, float max) {
+ a[0] = clampf(a[0], min, max);
+ a[1] = clampf(a[1], min, max);
+ a[2] = clampf(a[2], min, max);
+ a[3] = clampf(a[3], min, max);
+}
+
+static inline Quat quat_mul(Quat q1, Quat q2) {
+ Quat out;
+ out.w = q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z;
+ out.x = q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y;
+ out.y = q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x;
+ out.z = q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w;
+ return out;
+}
+
+static inline Vec3 quat_rotate(Quat q, Vec3 v) {
+ Quat qv = (Quat){0.0f, v.x, v.y, v.z};
+ Quat tmp = quat_mul(q, qv);
+ Quat q_conj = (Quat){q.w, -q.x, -q.y, -q.z};
+ Quat res = quat_mul(tmp, q_conj);
+ return (Vec3){res.x, res.y, res.z};
+}
+
+static inline Quat quat_inverse(Quat q) { return (Quat){q.w, -q.x, -q.y, -q.z}; }
+
+static inline Quat rndquat(unsigned int* rng) {
+ float u1 = rndf(0.0f, 1.0f, rng);
+ float u2 = rndf(0.0f, 1.0f, rng);
+ float u3 = rndf(0.0f, 1.0f, rng);
+ float s1 = sqrtf(1.0f - u1), s2 = sqrtf(u1);
+ float a = 2.0f * (float)M_PI * u2, b = 2.0f * (float)M_PI * u3;
+ return (Quat){s1 * sinf(a), s1 * cosf(a), s2 * sinf(b), s2 * cosf(b)};
+}
+
+static inline Target rndring(unsigned int* rng, float radius) {
+ Target ring = (Target){0};
+ ring.pos.x = rndf(-GRID_X + 2 * radius, GRID_X - 2 * radius, rng);
+ ring.pos.y = rndf(-GRID_Y + 2 * radius, GRID_Y - 2 * radius, rng);
+ ring.pos.z = rndf(-GRID_Z + 2 * radius, GRID_Z - 2 * radius, rng);
+ ring.orientation = rndquat(rng);
+ ring.normal = quat_rotate(ring.orientation, (Vec3){0.0f, 0.0f, 1.0f});
+ ring.radius = radius;
+ return ring;
+}
+
+static inline Vec3 random_pos(unsigned int* rng) {
+ return (Vec3){
+ rndf(-MARGIN_X, MARGIN_X, rng),
+ rndf(-MARGIN_Y, MARGIN_Y, rng),
+ rndf(-MARGIN_Z, MARGIN_Z, rng),
+ };
+}
+
+static inline bool out_of_bounds(Vec3 p, float scale) {
+ return fabsf(p.x) > GRID_X * scale || fabsf(p.y) > GRID_Y * scale ||
+ fabsf(p.z) > GRID_Z * scale;
+}
+
+// params
+
+static inline float rpm_hover(const Params* p) {
+ return sqrtf((p->mass * p->gravity) / (4.0f * p->k_thrust));
+}
+
+static inline void init_drone(Drone* drone, unsigned int* rng, float dr) {
+ drone->params.arm_len = BASE_ARM_LEN * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.mass = BASE_MASS * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.ixx = BASE_IXX * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.iyy = BASE_IYY * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.izz = BASE_IZZ * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.k_thrust = BASE_K_THRUST * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.k_ang_damp = BASE_K_ANG_DAMP * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.k_drag = BASE_K_DRAG * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.b_drag = BASE_B_DRAG * rndf(1.0f - dr, 1.0f + dr, rng);
+ drone->params.gravity = BASE_GRAVITY * rndf(0.99f, 1.01f, rng);
+ drone->params.max_rpm = BASE_MAX_RPM;
+ drone->params.max_vel = BASE_MAX_VEL;
+ drone->params.max_omega = BASE_MAX_OMEGA;
+ drone->params.k_mot = BASE_K_MOT * rndf(1.0f - dr, 1.0f + dr, rng);
+
+ drone->params.inv_mass = 1.0f / drone->params.mass;
+ drone->params.inv_ixx = 1.0f / drone->params.ixx;
+ drone->params.inv_iyy = 1.0f / drone->params.iyy;
+ drone->params.inv_izz = 1.0f / drone->params.izz;
+ drone->params.inv_k_mot = 1.0f / drone->params.k_mot;
+
+ float hover = rpm_hover(&drone->params);
+ for (int i = 0; i < 4; i++)
+ drone->state.rpms[i] = hover;
+
+ drone->state.pos = (Vec3){0, 0, 0};
+ drone->prev_pos = drone->state.pos;
+ drone->state.vel = (Vec3){0, 0, 0};
+ drone->state.omega = (Vec3){0, 0, 0};
+ drone->state.quat = (Quat){1, 0, 0, 0};
+}
+
+// observations
+
+void compute_drone_observations(Drone* agent, float* observations, bool is_race) {
+ int idx = 0;
+ Quat q = agent->state.quat;
+ Quat q_inv = quat_inverse(q);
+ Vec3 vel_body = quat_rotate(q_inv, agent->state.vel);
+ Vec3 to_target = quat_rotate(q_inv, sub3(agent->target->pos, agent->state.pos));
+
+ float denom = agent->params.max_vel * 1.7320508f;
+ observations[idx++] = vel_body.x / denom;
+ observations[idx++] = vel_body.y / denom;
+ observations[idx++] = vel_body.z / denom;
+
+ observations[idx++] = agent->state.omega.x / agent->params.max_omega;
+ observations[idx++] = agent->state.omega.y / agent->params.max_omega;
+ observations[idx++] = agent->state.omega.z / agent->params.max_omega;
+
+ observations[idx++] = q.w;
+ observations[idx++] = q.x;
+ observations[idx++] = q.y;
+ observations[idx++] = q.z;
+
+ // this is body frame so we have to be careful about scaling
+ // because distances are relative to the drone orientation
+ observations[idx++] = tanhf(to_target.x * 0.1f);
+ observations[idx++] = tanhf(to_target.y * 0.1f);
+ observations[idx++] = tanhf(to_target.z * 0.1f);
+
+ observations[idx++] = tanhf(to_target.x * 10.0f);
+ observations[idx++] = tanhf(to_target.y * 10.0f);
+ observations[idx++] = tanhf(to_target.z * 10.0f);
+
+ Vec3 normal_body = quat_rotate(q_inv, agent->target->normal);
+ observations[idx++] = normal_body.x;
+ observations[idx++] = normal_body.y;
+ observations[idx++] = normal_body.z;
+
+ observations[idx++] = is_race ? 0.0f : 1.0f;
+ observations[idx++] = is_race ? 1.0f : 0.0f;
+}
\ No newline at end of file
diff --git a/ocean/drone/physics.h b/ocean/drone/physics.h
new file mode 100644
index 0000000000..9bfce61f4c
--- /dev/null
+++ b/ocean/drone/physics.h
@@ -0,0 +1,347 @@
+// SIMD physics kernel
+
+#pragma once
+
+#include
+
+#include "dronelib.h"
+
+#define DRONE_LANES 8
+
+// types
+
+typedef enum {
+ INTEGRATOR_RK4 = 0,
+ INTEGRATOR_RK2 = 1,
+} IntegratorType;
+
+typedef float vf __attribute__((vector_size(sizeof(float) * DRONE_LANES)));
+
+typedef struct {
+ vf x, y, z;
+} Vec3v;
+
+typedef struct {
+ vf w, x, y, z;
+} Quatv;
+
+typedef struct {
+ Vec3v pos;
+ Vec3v vel;
+ Quatv quat;
+ Vec3v omega;
+ vf rpms[4];
+} Statev;
+
+typedef struct {
+ vf mass;
+ vf ixx;
+ vf iyy;
+ vf izz;
+ vf arm_len;
+ vf k_thrust;
+ vf k_ang_damp;
+ vf k_drag;
+ vf b_drag;
+ vf gravity;
+ vf max_rpm;
+ vf max_vel;
+ vf max_omega;
+ vf k_mot;
+ vf inv_mass;
+ vf inv_ixx;
+ vf inv_iyy;
+ vf inv_izz;
+ vf inv_k_mot;
+} Paramsv;
+
+typedef struct {
+ Vec3v vel;
+ Vec3v v_dot;
+ Quatv q_dot;
+ Vec3v w_dot;
+ vf rpm_dot[4];
+} StateDerivativev;
+
+// math
+
+static inline Vec3v vadd3(Vec3v a, Vec3v b) { return (Vec3v){a.x + b.x, a.y + b.y, a.z + b.z}; }
+static inline Vec3v vscalmul3(Vec3v a, float b) { return (Vec3v){a.x * b, a.y * b, a.z * b}; }
+
+static inline Quatv vadd_quat(Quatv a, Quatv b) {
+ return (Quatv){a.w + b.w, a.x + b.x, a.y + b.y, a.z + b.z};
+}
+
+static inline Quatv vscalmul_quat(Quatv a, float b) {
+ return (Quatv){a.w * b, a.x * b, a.y * b, a.z * b};
+}
+
+static inline Quatv vquat_mul(Quatv q1, Quatv q2) {
+ return (Quatv){
+ q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z,
+ q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y,
+ q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x,
+ q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w,
+ };
+}
+
+static inline void vquat_normalize(Quatv* q) {
+ vf n = __builtin_elementwise_sqrt(q->w * q->w + q->x * q->x + q->y * q->y + q->z * q->z);
+ q->w /= n;
+ q->x /= n;
+ q->y /= n;
+ q->z /= n;
+}
+
+static inline vf vclampf(vf v, vf lo, vf hi) {
+ return __builtin_elementwise_min(__builtin_elementwise_max(v, lo), hi);
+}
+
+static inline void vclamp3(Vec3v* v, vf lo, vf hi) {
+ v->x = vclampf(v->x, lo, hi);
+ v->y = vclampf(v->y, lo, hi);
+ v->z = vclampf(v->z, lo, hi);
+}
+
+static inline vf vrpm_hover(const Paramsv* p) {
+ return __builtin_elementwise_sqrt((p->mass * p->gravity) / (4.0f * p->k_thrust));
+}
+
+static inline vf vrpm_min_for_centered_hover(const Paramsv* p) {
+ vf min_rpm = 2.0f * vrpm_hover(p) - p->max_rpm;
+ return __builtin_elementwise_min(__builtin_elementwise_max(min_rpm, (vf){0}), p->max_rpm);
+}
+
+// dynamics
+
+static inline void compute_derivatives(Statev* state, const Paramsv* params, const vf* target_rpms,
+ StateDerivativev* d) {
+ for (int i = 0; i < 4; i++)
+ d->rpm_dot[i] = params->inv_k_mot * (target_rpms[i] - state->rpms[i]);
+
+ vf T[4];
+ for (int i = 0; i < 4; i++) {
+ vf rpm = __builtin_elementwise_max(state->rpms[i], (vf){0});
+ T[i] = params->k_thrust * rpm * rpm;
+ }
+
+ vf Tsum = T[0] + T[1] + T[2] + T[3];
+ Quatv q = state->quat;
+ Vec3v F_prop = {
+ Tsum * 2.0f * (q.x * q.z + q.w * q.y),
+ Tsum * 2.0f * (q.y * q.z - q.w * q.x),
+ Tsum * (q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z),
+ };
+
+ d->vel = state->vel;
+ d->v_dot = (Vec3v){
+ (F_prop.x - params->b_drag * state->vel.x) * params->inv_mass,
+ (F_prop.y - params->b_drag * state->vel.y) * params->inv_mass,
+ ((F_prop.z - params->b_drag * state->vel.z) * params->inv_mass) - params->gravity,
+ };
+
+ Quatv omega_q = (Quatv){(vf){0}, state->omega.x, state->omega.y, state->omega.z};
+ d->q_dot = vscalmul_quat(vquat_mul(state->quat, omega_q), 0.5f);
+
+ vf af = params->arm_len * 0.70710678f; // 1/sqrt(2)
+ Vec3v tau_prop = {
+ af * ((T[2] + T[3]) - (T[0] + T[1])),
+ af * ((T[1] + T[2]) - (T[0] + T[3])),
+ params->k_drag * (-T[0] + T[1] - T[2] + T[3]),
+ };
+ Vec3v tau_aero = {
+ -params->k_ang_damp * state->omega.x,
+ -params->k_ang_damp * state->omega.y,
+ -params->k_ang_damp * state->omega.z,
+ };
+ Vec3v tau_iner = {
+ (params->iyy - params->izz) * state->omega.y * state->omega.z,
+ (params->izz - params->ixx) * state->omega.z * state->omega.x,
+ (params->ixx - params->iyy) * state->omega.x * state->omega.y,
+ };
+
+ d->w_dot = (Vec3v){
+ (tau_prop.x + tau_aero.x + tau_iner.x) * params->inv_ixx,
+ (tau_prop.y + tau_aero.y + tau_iner.y) * params->inv_iyy,
+ (tau_prop.z + tau_aero.z + tau_iner.z) * params->inv_izz,
+ };
+}
+
+static inline void step(Statev* s, StateDerivativev* d, float dt, Statev* out) {
+ out->pos = vadd3(s->pos, vscalmul3(d->vel, dt));
+ out->vel = vadd3(s->vel, vscalmul3(d->v_dot, dt));
+ out->quat = vadd_quat(s->quat, vscalmul_quat(d->q_dot, dt));
+ out->omega = vadd3(s->omega, vscalmul3(d->w_dot, dt));
+ for (int i = 0; i < 4; i++)
+ out->rpms[i] = s->rpms[i] + d->rpm_dot[i] * dt;
+ vquat_normalize(&out->quat);
+}
+
+static inline void rk2_step(Statev* state, const Paramsv* params, const vf* target_rpms, float dt) {
+ StateDerivativev k1, k2;
+ Statev mid;
+
+ compute_derivatives(state, params, target_rpms, &k1);
+ step(state, &k1, dt * 0.5f, &mid);
+ compute_derivatives(&mid, params, target_rpms, &k2);
+ step(state, &k2, dt, state);
+}
+
+static inline void rk4_step(Statev* state, const Paramsv* params, const vf* target_rpms, float dt) {
+ StateDerivativev k1, k2, k3, k4;
+ Statev tmp;
+
+ compute_derivatives(state, params, target_rpms, &k1);
+ step(state, &k1, dt * 0.5f, &tmp);
+ compute_derivatives(&tmp, params, target_rpms, &k2);
+ step(state, &k2, dt * 0.5f, &tmp);
+ compute_derivatives(&tmp, params, target_rpms, &k3);
+ step(state, &k3, dt, &tmp);
+ compute_derivatives(&tmp, params, target_rpms, &k4);
+
+ float dt6 = dt / 6.0f;
+
+ state->pos.x += (k1.vel.x + 2.0f * k2.vel.x + 2.0f * k3.vel.x + k4.vel.x) * dt6;
+ state->pos.y += (k1.vel.y + 2.0f * k2.vel.y + 2.0f * k3.vel.y + k4.vel.y) * dt6;
+ state->pos.z += (k1.vel.z + 2.0f * k2.vel.z + 2.0f * k3.vel.z + k4.vel.z) * dt6;
+
+ state->vel.x += (k1.v_dot.x + 2.0f * k2.v_dot.x + 2.0f * k3.v_dot.x + k4.v_dot.x) * dt6;
+ state->vel.y += (k1.v_dot.y + 2.0f * k2.v_dot.y + 2.0f * k3.v_dot.y + k4.v_dot.y) * dt6;
+ state->vel.z += (k1.v_dot.z + 2.0f * k2.v_dot.z + 2.0f * k3.v_dot.z + k4.v_dot.z) * dt6;
+
+ state->quat.w += (k1.q_dot.w + 2.0f * k2.q_dot.w + 2.0f * k3.q_dot.w + k4.q_dot.w) * dt6;
+ state->quat.x += (k1.q_dot.x + 2.0f * k2.q_dot.x + 2.0f * k3.q_dot.x + k4.q_dot.x) * dt6;
+ state->quat.y += (k1.q_dot.y + 2.0f * k2.q_dot.y + 2.0f * k3.q_dot.y + k4.q_dot.y) * dt6;
+ state->quat.z += (k1.q_dot.z + 2.0f * k2.q_dot.z + 2.0f * k3.q_dot.z + k4.q_dot.z) * dt6;
+
+ state->omega.x += (k1.w_dot.x + 2.0f * k2.w_dot.x + 2.0f * k3.w_dot.x + k4.w_dot.x) * dt6;
+ state->omega.y += (k1.w_dot.y + 2.0f * k2.w_dot.y + 2.0f * k3.w_dot.y + k4.w_dot.y) * dt6;
+ state->omega.z += (k1.w_dot.z + 2.0f * k2.w_dot.z + 2.0f * k3.w_dot.z + k4.w_dot.z) * dt6;
+
+ for (int i = 0; i < 4; i++) {
+ state->rpms[i] += (k1.rpm_dot[i] + 2.0f * k2.rpm_dot[i] + 2.0f * k3.rpm_dot[i] + k4.rpm_dot[i]) * dt6;
+ }
+
+ vquat_normalize(&state->quat);
+}
+
+// soa access
+
+static inline void set3(Vec3v* v, int l, Vec3 a) { v->x[l] = a.x; v->y[l] = a.y; v->z[l] = a.z; }
+static inline Vec3 get3(const Vec3v* v, int l) { return (Vec3){v->x[l], v->y[l], v->z[l]}; }
+static inline void setq(Quatv* q, int l, Quat a) { q->w[l] = a.w; q->x[l] = a.x; q->y[l] = a.y; q->z[l] = a.z; }
+static inline Quat getq(const Quatv* q, int l) { return (Quat){q->w[l], q->x[l], q->y[l], q->z[l]}; }
+
+// engine
+
+typedef struct {
+ int num_drones;
+ Statev* state;
+ Paramsv* params;
+ int integrator;
+} Physics;
+
+static inline void physics_init(Physics* phys, int num_drones, int integrator) {
+ int num_blocks = (num_drones + DRONE_LANES - 1) / DRONE_LANES;
+ phys->num_drones = num_drones;
+ phys->integrator = integrator;
+
+ phys->params = (Paramsv*)aligned_alloc(32, num_blocks * sizeof(Paramsv));
+ memset(phys->params, 0, num_blocks * sizeof(Paramsv));
+
+ phys->state = (Statev*)aligned_alloc(32, num_blocks * sizeof(Statev));
+ memset(phys->state, 0, num_blocks * sizeof(Statev));
+ for (int b = 0; b < num_blocks; b++)
+ for (int l = 0; l < DRONE_LANES; l++)
+ phys->state[b].quat.w[l] = 1.0f;
+}
+
+static inline void physics_close(Physics* phys) {
+ free(phys->params);
+ free(phys->state);
+}
+
+static inline void physics_set_drone(Physics* phys, int i, const Params* p, const State* st) {
+ int b = i / DRONE_LANES;
+ int l = i % DRONE_LANES;
+
+ Paramsv* pv = &phys->params[b];
+ pv->mass[l] = p->mass;
+ pv->ixx[l] = p->ixx;
+ pv->iyy[l] = p->iyy;
+ pv->izz[l] = p->izz;
+ pv->arm_len[l] = p->arm_len;
+ pv->k_thrust[l] = p->k_thrust;
+ pv->k_ang_damp[l] = p->k_ang_damp;
+ pv->k_drag[l] = p->k_drag;
+ pv->b_drag[l] = p->b_drag;
+ pv->gravity[l] = p->gravity;
+ pv->max_rpm[l] = p->max_rpm;
+ pv->max_vel[l] = p->max_vel;
+ pv->max_omega[l] = p->max_omega;
+ pv->k_mot[l] = p->k_mot;
+ pv->inv_mass[l] = p->inv_mass;
+ pv->inv_ixx[l] = p->inv_ixx;
+ pv->inv_iyy[l] = p->inv_iyy;
+ pv->inv_izz[l] = p->inv_izz;
+ pv->inv_k_mot[l] = p->inv_k_mot;
+
+ Statev* sv = &phys->state[b];
+ set3(&sv->pos, l, st->pos);
+ set3(&sv->vel, l, st->vel);
+ setq(&sv->quat, l, st->quat);
+ set3(&sv->omega, l, st->omega);
+ for (int k = 0; k < 4; k++) sv->rpms[k][l] = st->rpms[k];
+}
+
+static inline State physics_get_state(const Physics* phys, int i) {
+ const Statev* sv = &phys->state[i / DRONE_LANES];
+ int l = i % DRONE_LANES;
+ State st;
+ st.pos = get3(&sv->pos, l);
+ st.vel = get3(&sv->vel, l);
+ st.quat = getq(&sv->quat, l);
+ st.omega = get3(&sv->omega, l);
+ for (int k = 0; k < 4; k++) st.rpms[k] = sv->rpms[k][l];
+ return st;
+}
+
+static inline void physics_step(Physics* phys, float* actions) {
+ for (int base = 0; base < phys->num_drones; base += DRONE_LANES) {
+ int lanes = phys->num_drones - base;
+ if (lanes > DRONE_LANES) lanes = DRONE_LANES;
+
+ Statev* s = &phys->state[base / DRONE_LANES];
+ const Paramsv* p = &phys->params[base / DRONE_LANES];
+
+ vf act[4];
+ for (int l = 0; l < DRONE_LANES; l++) {
+ int idx = base + (l < lanes ? l : 0);
+ float* a = &actions[4 * idx];
+ clamp4(a, -1.0f, 1.0f);
+ for (int k = 0; k < 4; k++) act[k][l] = a[k];
+ }
+
+ vf min_rpm = vrpm_min_for_centered_hover(p);
+ vf target_rpms[4];
+ for (int k = 0; k < 4; k++) {
+ vf u = (act[k] + 1.0f) * 0.5f;
+ target_rpms[k] = min_rpm + u * (p->max_rpm - min_rpm);
+ }
+
+ for (int sub = 0; sub < ACTION_SUBSTEPS; sub++) {
+ if (phys->integrator == INTEGRATOR_RK2) {
+ rk2_step(s, p, target_rpms, DT);
+ } else {
+ rk4_step(s, p, target_rpms, DT);
+ }
+
+ vclamp3(&s->vel, -p->max_vel, p->max_vel);
+ vclamp3(&s->omega, -p->max_omega, p->max_omega);
+
+ for (int k = 0; k < 4; k++) {
+ s->rpms[k] = vclampf(s->rpms[k], (vf){0}, p->max_rpm);
+ }
+ }
+ }
+}
diff --git a/ocean/drone/render.h b/ocean/drone/render.h
new file mode 100644
index 0000000000..7b88320d68
--- /dev/null
+++ b/ocean/drone/render.h
@@ -0,0 +1,649 @@
+// Originally made by Sam Turner and Finlay Sanders, 2025.
+// Included in pufferlib under the original project's MIT license.
+// https://github.com/tensaur/drone
+
+#pragma once
+
+#include
+
+#include "drone.h"
+#include "dronelib.h"
+#include "raylib.h"
+#include "raymath.h"
+
+#define R (Color){255, 0, 0, 255}
+#define W (Color){255, 255, 255, 255}
+#define B (Color){0, 0, 255, 255}
+Color COLORS[64] = {B, B, B, R, R, R, R, R,
+ B, B, B, W, W, W, W, W,
+ B, B, B, R, R, R, R, R,
+ B, B, B, W, W, W, W, W,
+ R, R, R, R, R, R, R, R,
+ W, W, W, W, W, W, W, W,
+ R, R, R, R, R, R, R, R,
+ W, W, W, W, W, W, W, W};
+#undef R
+#undef W
+#undef B
+
+// 3D model config
+#define MODEL_SCALE_NORMAL 3.0f
+#define NUM_PROPELLERS 4
+static const int PROP_MESH_IDX[NUM_PROPELLERS] = {8, 6, 5, 7};
+static const float PROP_DIRS[NUM_PROPELLERS] = {1.0f, -1.0f, 1.0f, -1.0f};
+
+typedef struct Client Client;
+
+struct Client {
+ Camera3D camera;
+ float width;
+ float height;
+
+ float camera_distance;
+ float camera_azimuth;
+ float camera_elevation;
+ bool is_dragging;
+ Vector2 last_mouse_pos;
+
+ Trail* trails;
+
+ int selected_drone;
+ bool inspect_mode;
+ bool follow_mode;
+ int target_fps;
+
+ // Drone 3D model
+ Model drone_model;
+ bool model_loaded;
+ bool use_3d_model;
+ float* prop_angles;
+ Vec3 prop_centers[NUM_PROPELLERS];
+ float model_scale;
+};
+
+// Convert dronelib Quat to raylib Matrix
+static inline Matrix quat_to_matrix(Quat q) {
+ float xx = q.x * q.x, yy = q.y * q.y, zz = q.z * q.z;
+ float xy = q.x * q.y, xz = q.x * q.z, yz = q.y * q.z;
+ float wx = q.w * q.x, wy = q.w * q.y, wz = q.w * q.z;
+
+ Matrix m = {0};
+ m.m0 = 1.0f - 2.0f * (yy + zz);
+ m.m1 = 2.0f * (xy + wz);
+ m.m2 = 2.0f * (xz - wy);
+ m.m4 = 2.0f * (xy - wz);
+ m.m5 = 1.0f - 2.0f * (xx + zz);
+ m.m6 = 2.0f * (yz + wx);
+ m.m8 = 2.0f * (xz + wy);
+ m.m9 = 2.0f * (yz - wx);
+ m.m10 = 1.0f - 2.0f * (xx + yy);
+ m.m15 = 1.0f;
+ return m;
+}
+
+void c_close_client(Client* client) {
+ if (client->model_loaded) {
+ UnloadModel(client->drone_model);
+ }
+
+ if (client->prop_angles) {
+ free(client->prop_angles);
+ }
+
+ CloseWindow();
+ free(client->trails);
+ free(client);
+}
+
+static void update_camera_position(Client* c, Vec3 target_pos) {
+ float r = c->camera_distance;
+ float az = c->camera_azimuth;
+ float el = c->camera_elevation;
+
+ float x = r * cosf(el) * cosf(az);
+ float y = r * cosf(el) * sinf(az);
+ float z = r * sinf(el);
+
+ if (c->follow_mode) {
+ c->camera.target = (Vector3){target_pos.x, target_pos.y, target_pos.z};
+ c->camera.position = (Vector3){target_pos.x + x, target_pos.y + y, target_pos.z + z};
+ } else {
+ c->camera.target = (Vector3){0, 0, 0};
+ c->camera.position = (Vector3){x, y, z};
+ }
+}
+
+void handle_camera_controls(Client* client, Vec3 target_pos, float min_zoom) {
+ Vector2 mouse_pos = GetMousePosition();
+
+ if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
+ client->is_dragging = true;
+ client->last_mouse_pos = mouse_pos;
+ }
+
+ if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) {
+ client->is_dragging = false;
+ }
+
+ if (client->is_dragging && IsMouseButtonDown(MOUSE_BUTTON_LEFT)) {
+ Vector2 mouse_delta = {mouse_pos.x - client->last_mouse_pos.x,
+ mouse_pos.y - client->last_mouse_pos.y};
+
+ float sensitivity = 0.005f;
+
+ client->camera_azimuth -= mouse_delta.x * sensitivity;
+
+ client->camera_elevation += mouse_delta.y * sensitivity;
+ client->camera_elevation =
+ clampf(client->camera_elevation, -PI / 2.0f + 0.1f, PI / 2.0f - 0.1f);
+
+ client->last_mouse_pos = mouse_pos;
+
+ update_camera_position(client, target_pos);
+ }
+
+ float wheel = GetMouseWheelMove();
+ if (wheel != 0) {
+ client->camera_distance -= wheel * 2.0f;
+ client->camera_distance = clampf(client->camera_distance, min_zoom, 100.0f);
+ update_camera_position(client, target_pos);
+ }
+}
+
+void handle_drone_selection(Client* client, int num_agents, float dt) {
+ static float repeat_timer = 0;
+ static bool key_held = false;
+
+ if (IsKeyDown(KEY_D) || IsKeyDown(KEY_A)) {
+ if (!key_held || repeat_timer <= 0) {
+ if (IsKeyDown(KEY_D)) {
+ client->selected_drone = (client->selected_drone + 1) % num_agents;
+ }
+
+ if (IsKeyDown(KEY_A)) {
+ client->selected_drone = (client->selected_drone - 1 + num_agents) % num_agents;
+ }
+
+ repeat_timer = key_held ? 0.05f : 0.3f;
+ key_held = true;
+ }
+
+ repeat_timer -= dt;
+ } else {
+ key_held = false;
+ repeat_timer = 0;
+ }
+}
+
+void handle_fps_control(Client* client, float dt) {
+ static float repeat_timer = 0;
+ static bool key_held = false;
+
+ if (IsKeyDown(KEY_W) || IsKeyDown(KEY_S)) {
+ if (!key_held || repeat_timer <= 0) {
+ if (IsKeyDown(KEY_W)) {
+ client->target_fps += 10;
+ if (client->target_fps > 240) client->target_fps = 240;
+ }
+
+ if (IsKeyDown(KEY_S)) {
+ client->target_fps -= 10;
+ if (client->target_fps < 10) client->target_fps = 10;
+ }
+
+ SetTargetFPS(client->target_fps);
+ repeat_timer = key_held ? 0.05f : 0.3f;
+ key_held = true;
+ }
+
+ repeat_timer -= dt;
+ } else {
+ key_held = false;
+ repeat_timer = 0;
+ }
+}
+
+// Compute center of mesh from vertices
+static Vec3 compute_mesh_center(Mesh* mesh) {
+ Vec3 center = {0, 0, 0};
+
+ for (int v = 0; v < mesh->vertexCount; v++) {
+ center.x += mesh->vertices[v * 3 + 0];
+ center.y += mesh->vertices[v * 3 + 1];
+ center.z += mesh->vertices[v * 3 + 2];
+ }
+
+ return scalmul3(center, 1.0f / mesh->vertexCount);
+}
+
+Client* make_client(DroneEnv* env) {
+ Client* client = (Client*)calloc(1, sizeof(Client));
+
+ client->width = WIDTH;
+ client->height = HEIGHT;
+
+ SetConfigFlags(FLAG_MSAA_4X_HINT);
+ InitWindow(WIDTH, HEIGHT, "PufferLib Drone");
+
+#ifndef __EMSCRIPTEN__
+ SetTargetFPS(60);
+#endif
+
+ if (!IsWindowReady()) {
+ TraceLog(LOG_ERROR, "Window failed to initialize\n");
+ free(client);
+ return NULL;
+ }
+
+ client->camera_distance = 40.0f;
+ client->camera_azimuth = 0.0f;
+ client->camera_elevation = PI / 10.0f;
+ client->is_dragging = false;
+ client->last_mouse_pos = (Vector2){0.0f, 0.0f};
+
+ client->camera.up = (Vector3){0.0f, 0.0f, 1.0f};
+ client->camera.fovy = 45.0f;
+ client->camera.projection = CAMERA_PERSPECTIVE;
+
+ Vec3 origin = {0, 0, 0};
+ update_camera_position(client, origin);
+
+ // Initialize trail buffer
+ client->trails = (Trail*)calloc(env->num_agents, sizeof(Trail));
+ for (int i = 0; i < env->num_agents; i++) {
+ Trail* trail = &client->trails[i];
+ trail->index = 0;
+ trail->count = 0;
+ for (int j = 0; j < TRAIL_LENGTH; j++) {
+ trail->pos[j] = env->agents[i].state.pos;
+ }
+ }
+
+ client->selected_drone = 0;
+ client->inspect_mode = false;
+ client->follow_mode = false;
+ client->target_fps = 100;
+ client->model_loaded = false;
+ client->model_scale = MODEL_SCALE_NORMAL;
+
+ // Load 3D model
+ const char* model_paths[] = {"resources/crazyflie.glb", "resources/drone/crazyflie.glb",
+ "crazyflie.glb", NULL};
+
+ for (int i = 0; model_paths[i] != NULL; i++) {
+ if (FileExists(model_paths[i])) {
+ client->drone_model = LoadModel(model_paths[i]);
+
+ if (client->drone_model.meshCount > 0) {
+ client->model_loaded = true;
+ TraceLog(LOG_INFO, "Loaded drone model: %s", model_paths[i]);
+
+ // Cache propeller centers
+ for (int p = 0; p < NUM_PROPELLERS; p++) {
+ int idx = PROP_MESH_IDX[p];
+
+ if (idx < client->drone_model.meshCount) {
+ client->prop_centers[p] =
+ compute_mesh_center(&client->drone_model.meshes[idx]);
+ }
+ }
+
+ break;
+ }
+ }
+ }
+
+ client->use_3d_model = client->model_loaded;
+ client->prop_angles = (float*)calloc(env->num_agents * NUM_PROPELLERS, sizeof(float));
+
+ return client;
+}
+
+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_GREEN = (Color){0, 220, 80, 255};
+
+void DrawRing3D(Target ring, float thickness, Color entryColor, Color exitColor) {
+ float half_thick = thickness / 2.0f;
+
+ Vector3 center_pos = {ring.pos.x, ring.pos.y, ring.pos.z};
+
+ Vector3 entry_start_pos = {center_pos.x - half_thick * ring.normal.x,
+ center_pos.y - half_thick * ring.normal.y,
+ center_pos.z - half_thick * ring.normal.z};
+
+ DrawCylinderWiresEx(entry_start_pos, center_pos, ring.radius, ring.radius, 32, entryColor);
+
+ Vector3 exit_end_pos = {center_pos.x + half_thick * ring.normal.x,
+ center_pos.y + half_thick * ring.normal.y,
+ center_pos.z + half_thick * ring.normal.z};
+
+ DrawCylinderWiresEx(center_pos, exit_end_pos, ring.radius, ring.radius, 32, exitColor);
+}
+
+void DrawDroneModel(Client* client, Drone* agent, int drone_idx, float dt, Color body_color) {
+ if (!client->model_loaded) return;
+
+ Model* model = &client->drone_model;
+ float* angles = &client->prop_angles[drone_idx * NUM_PROPELLERS];
+
+ // Update propeller angles from RPM
+ for (int p = 0; p < NUM_PROPELLERS; p++) {
+ float rpm = agent->state.rpms[p];
+ angles[p] += rpm * (2.0f * PI / 60.0f) * dt * PROP_DIRS[p];
+
+ if (angles[p] > 2.0f * PI) angles[p] -= 2.0f * PI;
+ if (angles[p] < 0.0f) angles[p] += 2.0f * PI;
+ }
+
+ // Build world transform matrices using client's model_scale
+ float scale = client->model_scale;
+ Matrix mScale = MatrixScale(scale, scale, scale);
+ Matrix mRot = quat_to_matrix(agent->state.quat);
+ Matrix mTrans = MatrixTranslate(agent->state.pos.x, agent->state.pos.y, agent->state.pos.z);
+
+ Matrix droneWorld = MatrixMultiply(MatrixMultiply(mScale, mRot), mTrans);
+
+ // Draw each mesh
+ for (int m = 0; m < model->meshCount; m++) {
+ Matrix meshWorld = droneWorld;
+
+ // Check if this mesh is a propeller
+ bool is_prop = false;
+ for (int p = 0; p < NUM_PROPELLERS; p++) {
+ if (m == PROP_MESH_IDX[p]) {
+ is_prop = true;
+ Vec3 c = client->prop_centers[p];
+ Matrix toOrigin = MatrixTranslate(-c.x, -c.y, -c.z);
+ Matrix spin = MatrixRotateZ(angles[p]);
+ Matrix fromOrigin = MatrixTranslate(c.x, c.y, c.z);
+ Matrix propLocal = MatrixMultiply(MatrixMultiply(toOrigin, spin), fromOrigin);
+ meshWorld = MatrixMultiply(propLocal, droneWorld);
+ break;
+ }
+ }
+
+ Material mat = model->materials[model->meshMaterial[m]];
+
+ Color origColor = mat.maps[MATERIAL_MAP_DIFFUSE].color;
+ int brightness = (origColor.r + origColor.g + origColor.b) / 3;
+
+ if (is_prop || brightness > 64) {
+ mat.maps[MATERIAL_MAP_DIFFUSE].color = body_color;
+ } else {
+ mat.maps[MATERIAL_MAP_DIFFUSE].color = origColor;
+ }
+
+ DrawMesh(model->meshes[m], mat, meshWorld);
+ }
+}
+
+void DrawDronePrimitive(Client* client, Drone* agent, float* actions, Color body_color) {
+ const float scale = client->model_scale;
+
+ DrawSphere((Vector3){agent->state.pos.x, agent->state.pos.y, agent->state.pos.z}, 0.06f * scale,
+ body_color);
+
+ const float rotor_radius = 0.03f * scale;
+ const float arm_len = 0.15f * scale;
+ const float diag = arm_len * 0.7071f; // 1/sqrt(2)
+
+ Vec3 rotor_offsets[4] = {
+ {+diag, +diag, 0.0f}, {+diag, -diag, 0.0f}, {-diag, -diag, 0.0f}, {-diag, +diag, 0.0f}};
+
+ for (int j = 0; j < 4; j++) {
+ Vec3 world_off = quat_rotate(agent->state.quat, rotor_offsets[j]);
+
+ Vector3 rotor_pos = {agent->state.pos.x + world_off.x, agent->state.pos.y + world_off.y,
+ agent->state.pos.z + world_off.z};
+
+ float rpm = (actions[j] + 1.0f) * 0.5f * agent->params.max_rpm;
+ float intensity = 0.75f + 0.25f * (rpm / agent->params.max_rpm);
+
+ Color rotor_color = (Color){(unsigned char)(body_color.r * intensity),
+ (unsigned char)(body_color.g * intensity),
+ (unsigned char)(body_color.b * intensity), 255};
+
+ DrawSphere(rotor_pos, rotor_radius, rotor_color);
+ DrawCylinderEx((Vector3){agent->state.pos.x, agent->state.pos.y, agent->state.pos.z},
+ rotor_pos, 0.004f * scale, 0.004f * scale, 8, BLACK);
+ }
+}
+
+// Task-specific overlays
+static void render_task(DroneEnv* env, Client* client) {
+ (void)client;
+ if (env->task != TASK_RACE) return;
+ RaceConfig* cfg = (RaceConfig*)env->task_config;
+ RaceState* state = (RaceState*)env->task_state;
+ for (int i = 0; i < cfg->max_rings; i++)
+ DrawRing3D(state->ring_buffer[i], 0.1f, GREEN, BLUE);
+}
+
+void c_render(DroneEnv* env) {
+ if (env->client == NULL) {
+ env->client = make_client(env);
+
+ if (env->client == NULL) {
+ TraceLog(LOG_ERROR, "Failed to initialize client for rendering\n");
+ return;
+ }
+ }
+
+ if (WindowShouldClose() || IsKeyDown(KEY_ESCAPE)) {
+ c_close(env);
+ exit(0);
+ }
+
+ Client* client = env->client;
+ float dt = GetFrameTime();
+
+ // Get selected drone position for camera
+ Vec3 drone_pos = env->agents[client->selected_drone].state.pos;
+
+ handle_camera_controls(client, drone_pos, 1.0f);
+ handle_drone_selection(client, env->num_agents, dt);
+ handle_fps_control(client, dt);
+
+ if (IsKeyPressed(KEY_I)) {
+ client->inspect_mode = !client->inspect_mode;
+ // When entering inspect mode, turn on follow mode by default
+ if (client->inspect_mode) {
+ client->follow_mode = true;
+ update_camera_position(client, drone_pos);
+ } else {
+ // When exiting inspect mode, turn off follow mode
+ client->follow_mode = false;
+ update_camera_position(client, drone_pos);
+ }
+ }
+
+ if (IsKeyPressed(KEY_M) && client->model_loaded) {
+ client->use_3d_model = !client->use_3d_model;
+ }
+
+ if (IsKeyPressed(KEY_F)) {
+ client->follow_mode = !client->follow_mode;
+ }
+
+ // Update camera position every frame when in follow mode
+ if (client->follow_mode) {
+ update_camera_position(client, drone_pos);
+ }
+
+ bool inspect_mode = client->inspect_mode;
+
+ // Update trails
+ for (int i = 0; i < env->num_agents; i++) {
+ Drone* agent = &env->agents[i];
+ Trail* trail = &client->trails[i];
+ trail->pos[trail->index] = agent->state.pos;
+ trail->index = (trail->index + 1) % TRAIL_LENGTH;
+ if (trail->count < TRAIL_LENGTH) {
+ trail->count++;
+ }
+ if (env->terminals[i]) {
+ trail->index = 0;
+ trail->count = 0;
+ }
+ }
+
+ BeginDrawing();
+ ClearBackground(PUFF_BACKGROUND);
+ BeginMode3D(client->camera);
+
+ // Bounding cube
+ DrawCubeWires((Vector3){0, 0, 0}, GRID_X * 2.0f, GRID_Y * 2.0f, GRID_Z * 2.0f, WHITE);
+
+ // Draw drones
+ for (int i = 0; i < env->num_agents; i++) {
+ Drone* agent = &env->agents[i];
+ bool is_selected = (i == client->selected_drone);
+ Color body_color = (inspect_mode && is_selected) ? PUFF_GREEN : COLORS[i % 64];
+
+ if (client->use_3d_model && client->model_loaded) {
+ DrawDroneModel(client, agent, i, dt, body_color);
+ } else {
+ DrawDronePrimitive(client, agent, &env->actions[4 * i], body_color);
+ }
+
+ // Velocity vector
+ if (norm3(agent->state.vel) > 0.1f) {
+ Vec3 p = agent->state.pos;
+ Vec3 v = scalmul3(agent->state.vel, 0.1f);
+ DrawLine3D((Vector3){p.x, p.y, p.z}, (Vector3){p.x + v.x, p.y + v.y, p.z + v.z},
+ MAGENTA);
+ }
+
+ // Target line (shown in inspect mode)
+ if (inspect_mode && is_selected) {
+ Vec3 p = agent->state.pos;
+ Vec3 t = agent->target->pos;
+ DrawLine3D((Vector3){p.x, p.y, p.z}, (Vector3){t.x, t.y, t.z},
+ ColorAlpha(PUFF_GREEN, 0.5f));
+ }
+
+ // Draw trailing path for each drone
+ Trail* trail = &client->trails[i];
+ if (trail->count > 2) {
+ Color trail_color = (inspect_mode && is_selected) ? PUFF_GREEN : PUFF_CYAN;
+
+ for (int j = 0; j < trail->count - 1; j++) {
+ int idx0 = (trail->index - j - 1 + TRAIL_LENGTH) % TRAIL_LENGTH;
+ int idx1 = (trail->index - j - 2 + TRAIL_LENGTH) % TRAIL_LENGTH;
+
+ float alpha = (float)(TRAIL_LENGTH - j) / (float)trail->count * 0.8f;
+ Vec3 p0 = trail->pos[idx0], p1 = trail->pos[idx1];
+
+ DrawLine3D((Vector3){p0.x, p0.y, p0.z}, (Vector3){p1.x, p1.y, p1.z},
+ ColorAlpha(trail_color, alpha));
+ }
+ }
+ }
+
+ // Task-specific rendering
+ render_task(env, client);
+
+ // Targets (shown in inspect mode)
+ if (inspect_mode) {
+ float target_size = 0.1f;
+
+ for (int i = 0; i < env->num_agents; i++) {
+ Vec3 t = env->agents[i].target->pos;
+ bool is_selected = (i == client->selected_drone);
+ float size = is_selected ? target_size * 1.1f : target_size;
+ DrawSphere((Vector3){t.x, t.y, t.z}, size,
+ is_selected ? (Color){0, 255, 100, 180} : (Color){0, 255, 255, 100});
+ }
+ }
+
+ EndMode3D();
+
+ // Heads up display
+ int y = 10;
+ DrawText(TextFormat("Task: %s", task_name(env->task)), 10, y, 20, WHITE);
+ y += 25;
+ DrawText(TextFormat("Step: %d / %d", env->agents[client->selected_drone].episode_length,
+ task_horizon(env)), 10, y, 20, WHITE);
+ y += 25;
+ DrawText(TextFormat("FPS: %d (W/S to adjust)", client->target_fps), 10, y, 18, WHITE);
+ y += 22;
+ if (client->model_loaded) {
+ DrawText(TextFormat("Render: %s (M)", client->use_3d_model ? "3D Model" : "Primitive"), 10,
+ y, 18, client->use_3d_model ? PUFF_GREEN : LIGHTGRAY);
+ }
+ y += 22;
+ DrawText(TextFormat("Follow: %s (F)", client->follow_mode ? "ON" : "OFF"), 10, y, 18,
+ client->follow_mode ? PUFF_GREEN : LIGHTGRAY);
+ y += 30;
+
+ // Inspect mode stats
+ if (inspect_mode) {
+ int idx = client->selected_drone;
+ Drone* agent = &env->agents[idx];
+
+ DrawText(TextFormat("Drone: %d / %d (A/D to switch)", idx, env->num_agents - 1), 10, y, 20,
+ PUFF_GREEN);
+ y += 30;
+ DrawText(TextFormat("Pos: (%.1f, %.1f, %.1f)", agent->state.pos.x, agent->state.pos.y,
+ agent->state.pos.z),
+ 10, y, 18, WHITE);
+ y += 20;
+ DrawText(TextFormat("Vel: %.2f m/s", norm3(agent->state.vel)), 10, y, 18, WHITE);
+ y += 20;
+ DrawText(TextFormat("Omega: (%.1f, %.1f, %.1f)", agent->state.omega.x, agent->state.omega.y,
+ agent->state.omega.z),
+ 10, y, 18, WHITE);
+ y += 25;
+
+ // Motor RPM bars
+ DrawText("Motor RPMs:", 10, y, 18, WHITE);
+ y += 22;
+ int bar_w = 150, bar_h = 14;
+ Color motor_colors[4] = {ORANGE, PURPLE, LIME, SKYBLUE};
+ const char* motor_names[4] = {"M1", "M2", "M3", "M4"};
+
+ for (int m = 0; m < 4; m++) {
+ float pct = clampf(agent->state.rpms[m] / agent->params.max_rpm, 0.0f, 1.0f);
+ int fill_w = (int)(pct * bar_w);
+
+ // Label
+ DrawText(motor_names[m], 10, y, 16, motor_colors[m]);
+
+ // Bar background
+ DrawRectangle(35, y, bar_w, bar_h, (Color){40, 40, 40, 255});
+
+ // Bar fill
+ DrawRectangle(35, y, fill_w, bar_h, motor_colors[m]);
+
+ // Bar outline
+ DrawRectangleLines(35, y, bar_w, bar_h, LIGHTGRAY);
+
+ // RPM value text
+ DrawText(TextFormat("%.0f", agent->state.rpms[m]), 35 + bar_w + 5, y, 14, WHITE);
+
+ y += bar_h + 4;
+ }
+
+ y += 10;
+
+ DrawText(TextFormat("Episode Return: %.4f", agent->episode_return), 10, y, 18, WHITE);
+ y += 20;
+ DrawText(TextFormat("Episode Length: %d", agent->episode_length), 10, y, 18, WHITE);
+ y += 30;
+ }
+
+ // Controls (always visible)
+ DrawText("Left click + drag: Rotate camera", 10, y, 16, LIGHTGRAY);
+ y += 18;
+ DrawText("Mouse wheel: Zoom in/out", 10, y, 16, LIGHTGRAY);
+ y += 18;
+ DrawText("Tab: Change task", 10, y, 16, LIGHTGRAY);
+ y += 18;
+ DrawText(TextFormat("I: Inspect mode [%s]", inspect_mode ? "ON" : "OFF"), 10, y, 16,
+ inspect_mode ? PUFF_GREEN : LIGHTGRAY);
+
+ EndDrawing();
+}
\ No newline at end of file
diff --git a/ocean/drone/task_hover.h b/ocean/drone/task_hover.h
new file mode 100644
index 0000000000..578be27e3b
--- /dev/null
+++ b/ocean/drone/task_hover.h
@@ -0,0 +1,180 @@
+#pragma once
+
+#include "drone.h"
+
+// types
+
+#define HOVER_SCORE_DIST_SCALE 0.01f
+#define HOVER_SCORE_VEL_SCALE 0.01f
+#define HOVER_SCORE_OMEGA_SCALE 0.01f
+
+typedef struct {
+ float target_dist;
+ float alpha_hover;
+ float alpha_dist;
+ float sphere_radius;
+ int horizon;
+} HoverConfig;
+
+typedef struct {
+ float* score;
+ float* perf;
+ float* ema_dist;
+ float* ema_vel;
+ float* ema_omega;
+} HoverState;
+
+// lifecycle
+
+static void hover_init(DroneEnv* env) {
+ HoverState* state = (HoverState*)calloc(1, sizeof(HoverState));
+ state->score = (float*)calloc(env->num_agents, sizeof(float));
+ state->perf = (float*)calloc(env->num_agents, sizeof(float));
+ state->ema_dist = (float*)calloc(env->num_agents, sizeof(float));
+ state->ema_vel = (float*)calloc(env->num_agents, sizeof(float));
+ state->ema_omega = (float*)calloc(env->num_agents, sizeof(float));
+ env->task_state = state;
+}
+
+static void hover_close(DroneEnv* env) {
+ HoverState* state = (HoverState*)env->task_state;
+ if (state != NULL) {
+ free(state->score);
+ free(state->perf);
+ free(state->ema_dist);
+ free(state->ema_vel);
+ free(state->ema_omega);
+ free(state);
+ }
+ free(env->task_config);
+}
+
+// helpers
+
+static inline Vec3 random_ball_offset(unsigned int* rng, float radius) {
+ float u = rndf(0.0f, 1.0f, rng);
+ float v = rndf(0.0f, 1.0f, rng);
+ float z = 2.0f * v - 1.0f;
+ float a = 2.0f * (float)M_PI * u;
+ float r_xy = sqrtf(fmaxf(0.0f, 1.0f - z * z));
+ Vec3 dir = (Vec3){r_xy * cosf(a), r_xy * sinf(a), z};
+ return scalmul3(dir, radius * cbrtf(rndf(0.0f, 1.0f, rng)));
+}
+
+static inline float hover_score(float dist, float vel, float omega) {
+ float d = dist / HOVER_SCORE_DIST_SCALE;
+ float v = vel / HOVER_SCORE_VEL_SCALE;
+ float w = omega / HOVER_SCORE_OMEGA_SCALE;
+ float penalty = 0.7f * d + 0.15f * v + 0.15f * w;
+ return 1.0f / (1.0f + 0.05f * penalty);
+}
+
+static void hover_reset_to(DroneEnv* env, Drone* agent, int idx, Vec3 target, float spawn_dist) {
+ HoverState* state = (HoverState*)env->task_state;
+
+ agent->target->pos = target;
+ agent->target->vel = (Vec3){0.0f, 0.0f, 0.0f};
+ agent->target->normal = (Vec3){0.0f, 0.0f, 0.0f};
+
+ Vec3 p = add3(target, random_ball_offset(&env->rng, spawn_dist));
+ agent->state.pos = (Vec3){
+ clampf(p.x, -MARGIN_X, MARGIN_X),
+ clampf(p.y, -MARGIN_Y, MARGIN_Y),
+ clampf(p.z, -MARGIN_Z, MARGIN_Z),
+ };
+
+ float dist = norm3(sub3(agent->target->pos, agent->state.pos));
+ float vel = norm3(agent->state.vel);
+ float omega = norm3(agent->state.omega);
+ state->score[idx] = 0.0f;
+ state->perf[idx] = hover_score(dist, vel, omega);
+ state->ema_dist[idx] = dist;
+ state->ema_vel[idx] = vel;
+ state->ema_omega[idx] = omega;
+}
+
+static inline Vec3 sphere_slot(int idx, int num_agents, float radius) {
+ float phi = (float)M_PI * (sqrtf(5.0f) - 1.0f);
+ float y = 1.0f - 2.0f * ((float)idx / (float)num_agents);
+ float r = sqrtf(fmaxf(0.0f, 1.0f - y * y));
+ float theta = phi * (float)idx;
+ return (Vec3){radius * cosf(theta) * r, radius * sinf(theta) * r, radius * y};
+}
+
+static inline float cube_axis(int i, int side, float radius) {
+ if (side <= 1) return 0.0f;
+ return radius * (2.0f * (float)i / (float)(side - 1) - 1.0f);
+}
+
+static inline Vec3 cube_slot(int idx, int num_agents, float radius) {
+ float r = radius * 0.57735027f;
+ int side = (int)ceilf(cbrtf((float)num_agents));
+ int x = idx % side;
+ int y = (idx / side) % side;
+ int z = idx / (side * side);
+ return (Vec3){cube_axis(x, side, r), cube_axis(y, side, r), cube_axis(z, side, r)};
+}
+
+static inline Vec3 flag_slot(int idx) {
+ float y = (float)(idx % 8) - 3.5f;
+ float z = 2.5f - 0.75f * (float)(idx / 8);
+ return (Vec3){0.0f, y, z};
+}
+
+// callbacks
+
+static void hover_reset(DroneEnv* env, Drone* agent, int idx) {
+ HoverConfig* cfg = (HoverConfig*)env->task_config;
+ hover_reset_to(env, agent, idx, random_pos(&env->rng), cfg->target_dist);
+}
+
+static void sphere_reset(DroneEnv* env, Drone* agent, int idx) {
+ HoverConfig* cfg = (HoverConfig*)env->task_config;
+ Vec3 slot = sphere_slot(idx, env->num_agents, cfg->sphere_radius);
+ hover_reset_to(env, agent, idx, slot, cfg->target_dist);
+}
+
+static void cube_reset(DroneEnv* env, Drone* agent, int idx) {
+ HoverConfig* cfg = (HoverConfig*)env->task_config;
+ Vec3 slot = cube_slot(idx, env->num_agents, cfg->sphere_radius);
+ hover_reset_to(env, agent, idx, slot, cfg->target_dist);
+}
+
+static void flag_reset(DroneEnv* env, Drone* agent, int idx) {
+ HoverConfig* cfg = (HoverConfig*)env->task_config;
+ hover_reset_to(env, agent, idx, flag_slot(idx), cfg->target_dist);
+}
+
+static float hover_reward(DroneEnv* env, Drone* agent, int idx, StepCache* cache) {
+ HoverConfig* cfg = (HoverConfig*)env->task_config;
+ HoverState* state = (HoverState*)env->task_state;
+
+ float score = hover_score(cache->dist, cache->vel, cache->omega);
+ float reward = cfg->alpha_hover * score;
+ reward += cfg->alpha_dist * (cache->prev_dist - cache->dist);
+
+ state->score[idx] += score;
+ state->perf[idx] = 0.98f * state->perf[idx] + 0.02f * score;
+ state->ema_dist[idx] = 0.99f * state->ema_dist[idx] + 0.01f * cache->dist;
+ state->ema_vel[idx] = 0.99f * state->ema_vel[idx] + 0.01f * cache->vel;
+ state->ema_omega[idx] = 0.99f * state->ema_omega[idx] + 0.01f * cache->omega;
+ return reward;
+}
+
+static bool hover_done(DroneEnv* env, Drone* agent, int idx, StepCache* cache) {
+ HoverConfig* cfg = (HoverConfig*)env->task_config;
+ return cache->dist > (cfg->target_dist + 1.0f) || agent->episode_length >= cfg->horizon;
+}
+
+static void hover_log(DroneEnv* env, Drone* agent, int idx, Log* log, StepCache* cache) {
+ HoverConfig* cfg = (HoverConfig*)env->task_config;
+ HoverState* state = (HoverState*)env->task_state;
+ TaskLog* t = &log->task[env->task];
+ t->n += 1.0f;
+ t->perf += state->perf[idx];
+ t->score += state->score[idx];
+ t->keys[0] += state->ema_dist[idx];
+ t->keys[1] += state->ema_vel[idx];
+ t->keys[2] += state->ema_omega[idx];
+ t->keys[3] += cache->dist > (cfg->target_dist + 1.0f) ? 1.0f : 0.0f;
+}
diff --git a/ocean/drone/task_race.h b/ocean/drone/task_race.h
new file mode 100644
index 0000000000..b20ce82690
--- /dev/null
+++ b/ocean/drone/task_race.h
@@ -0,0 +1,200 @@
+#pragma once
+
+#include "drone.h"
+
+#define RACE_OOB_SCALE 2.0f
+
+#define RACE_RING_MIN_DIST (5.0f * RING_RADIUS)
+#define RACE_RING_MAX_DIST 8.0f
+#define RACE_RING_SEPARATION (3.0f * RING_RADIUS)
+#define RACE_MAX_PLACE_ATTEMPTS 100
+
+// types
+
+typedef struct {
+ int max_rings;
+ float ring_reward;
+ float alpha_dist;
+ int horizon;
+} RaceConfig;
+
+typedef struct {
+ Target* ring_buffer;
+ int* ring_idx;
+ int* rings_passed;
+ float* collisions;
+} RaceState;
+
+// lifecycle
+
+static void race_init(DroneEnv* env) {
+ RaceConfig* cfg = (RaceConfig*)env->task_config;
+ RaceState* state = (RaceState*)calloc(1, sizeof(RaceState));
+ state->ring_buffer = (Target*)calloc(cfg->max_rings, sizeof(Target));
+ state->ring_idx = (int*)calloc(env->num_agents, sizeof(int));
+ state->rings_passed = (int*)calloc(env->num_agents, sizeof(int));
+ state->collisions = (float*)calloc(env->num_agents, sizeof(float));
+ env->task_state = state;
+}
+
+static void race_close(DroneEnv* env) {
+ RaceState* state = (RaceState*)env->task_state;
+ if (state != NULL) {
+ free(state->ring_buffer);
+ free(state->ring_idx);
+ free(state->rings_passed);
+ free(state->collisions);
+ free(state);
+ }
+ free(env->task_config);
+}
+
+// helpers
+
+static inline bool ring_overlaps(const Target* rings, int count, Vec3 pos) {
+ for (int i = 0; i < count; i++)
+ if (norm3(sub3(rings[i].pos, pos)) < RACE_RING_SEPARATION) return true;
+ return false;
+}
+
+static inline bool in_gap_band(Vec3 a, Vec3 b) {
+ float d = norm3(sub3(a, b));
+ return d >= RACE_RING_MIN_DIST && d <= RACE_RING_MAX_DIST;
+}
+
+static inline Target gen_next_ring(unsigned int* rng, const Target* rings, int count,
+ const Target* close) {
+ const Target* prev = &rings[count - 1];
+ Target best = rndring(rng, RING_RADIUS);
+ bool have_fallback = false;
+ for (int attempt = 0; attempt < RACE_MAX_PLACE_ATTEMPTS; attempt++) {
+ Target ring = rndring(rng, RING_RADIUS);
+ if (!in_gap_band(ring.pos, prev->pos)) continue;
+ if (ring_overlaps(rings, count, ring.pos)) continue;
+ if (!have_fallback) { best = ring; have_fallback = true; }
+ if (close != NULL && !in_gap_band(ring.pos, close->pos)) continue;
+ return ring;
+ }
+ return best;
+}
+
+static inline Vec3 path_normal(const Target* rings, int n, int i) {
+ if (n < 2) return (Vec3){0.0f, 0.0f, 1.0f};
+ Vec3 dir = sub3(rings[(i + 1) % n].pos, rings[(i - 1 + n) % n].pos);
+ float len = norm3(dir);
+ return len > 1e-6f ? scalmul3(dir, 1.0f / len) : (Vec3){0.0f, 0.0f, 1.0f};
+}
+
+static inline void center_rings(Target* rings, int n) {
+ Vec3 lo = rings[0].pos, hi = rings[0].pos;
+ for (int i = 1; i < n; i++) {
+ lo.x = fminf(lo.x, rings[i].pos.x); hi.x = fmaxf(hi.x, rings[i].pos.x);
+ lo.y = fminf(lo.y, rings[i].pos.y); hi.y = fmaxf(hi.y, rings[i].pos.y);
+ lo.z = fminf(lo.z, rings[i].pos.z); hi.z = fmaxf(hi.z, rings[i].pos.z);
+ }
+ Vec3 mid = scalmul3(add3(lo, hi), 0.5f);
+ for (int i = 0; i < n; i++) rings[i].pos = sub3(rings[i].pos, mid);
+}
+
+static inline int check_ring(Vec3 pos, Vec3 prev_pos, Target* ring) {
+ float prev_dot = dot3(sub3(prev_pos, ring->pos), ring->normal);
+ float new_dot = dot3(sub3(pos, ring->pos), ring->normal);
+
+ bool valid_dir = (prev_dot < 0.0f && new_dot > 0.0f);
+ bool invalid_dir = (prev_dot > 0.0f && new_dot < 0.0f);
+
+ if (valid_dir || invalid_dir) {
+ Vec3 dir = sub3(pos, prev_pos);
+ float denom = dot3(ring->normal, dir);
+ if (fabsf(denom) < 1e-9f) return 0;
+
+ float t = -prev_dot / denom;
+ Vec3 intersection = add3(prev_pos, scalmul3(dir, t));
+ float d = norm3(sub3(intersection, ring->pos));
+
+ // margins scale with radius
+ float margin = 0.1f * ring->radius;
+ if (d < (ring->radius - margin) && valid_dir) return 1;
+ if (d < ring->radius + margin) return -1;
+ }
+ return 0;
+}
+
+// callbacks
+
+static void race_env_reset(DroneEnv* env) {
+ RaceConfig* cfg = (RaceConfig*)env->task_config;
+ RaceState* state = (RaceState*)env->task_state;
+
+ state->ring_buffer[0] = rndring(&env->rng, RING_RADIUS);
+ for (int i = 1; i < cfg->max_rings; i++) {
+ const Target* close = (i == cfg->max_rings - 1) ? &state->ring_buffer[0] : NULL;
+ state->ring_buffer[i] = gen_next_ring(&env->rng, state->ring_buffer, i, close);
+ }
+
+ center_rings(state->ring_buffer, cfg->max_rings);
+
+ for (int i = 0; i < cfg->max_rings; i++) {
+ state->ring_buffer[i].normal = path_normal(state->ring_buffer, cfg->max_rings, i);
+ }
+}
+
+static void race_reset(DroneEnv* env, Drone* agent, int idx) {
+ RaceConfig* cfg = (RaceConfig*)env->task_config;
+ RaceState* state = (RaceState*)env->task_state;
+
+ int g = (int)(rand_r(&env->rng) % cfg->max_rings);
+ Target* gate = &state->ring_buffer[g];
+
+ float back = rndf(1.0f, 3.0f, &env->rng);
+ Vec3 pos = sub3(gate->pos, scalmul3(gate->normal, back));
+ pos = add3(pos, (Vec3){rndf(-0.3f, 0.3f, &env->rng), rndf(-0.3f, 0.3f, &env->rng),
+ rndf(-0.3f, 0.3f, &env->rng)});
+ agent->state.pos = (Vec3){
+ clampf(pos.x, -MARGIN_X, MARGIN_X),
+ clampf(pos.y, -MARGIN_Y, MARGIN_Y),
+ clampf(pos.z, -MARGIN_Z, MARGIN_Z),
+ };
+
+ state->ring_idx[idx] = g;
+ state->rings_passed[idx] = 0;
+ state->collisions[idx] = 0.0f;
+ *agent->target = *gate;
+}
+
+static float race_reward(DroneEnv* env, Drone* agent, int idx, StepCache* cache) {
+ RaceConfig* cfg = (RaceConfig*)env->task_config;
+ RaceState* state = (RaceState*)env->task_state;
+
+ float reward = cfg->alpha_dist * (cache->prev_dist - cache->dist);
+
+ int result = check_ring(agent->state.pos, agent->prev_pos, &state->ring_buffer[state->ring_idx[idx]]);
+ if (result == 1) {
+ state->rings_passed[idx]++;
+ state->ring_idx[idx] = (state->ring_idx[idx] + 1) % cfg->max_rings;
+ *agent->target = state->ring_buffer[state->ring_idx[idx]];
+ reward += cfg->ring_reward;
+ } else if (result == -1) {
+ state->collisions[idx] += 1.0f;
+ }
+
+ return reward;
+}
+
+static bool race_done(DroneEnv* env, Drone* agent, int idx, StepCache* cache) {
+ RaceConfig* cfg = (RaceConfig*)env->task_config;
+ return out_of_bounds(agent->state.pos, RACE_OOB_SCALE) || agent->episode_length >= cfg->horizon;
+}
+
+static void race_log(DroneEnv* env, Drone* agent, int idx, Log* log, StepCache* cache) {
+ RaceConfig* cfg = (RaceConfig*)env->task_config;
+ RaceState* state = (RaceState*)env->task_state;
+ TaskLog* t = &log->task[TASK_RACE];
+ t->n += 1.0f;
+ t->perf += fminf((float)state->rings_passed[idx] / (float)cfg->max_rings, 1.0f);
+ t->score += (float)state->rings_passed[idx];
+ t->keys[0] += (float)state->rings_passed[idx];
+ t->keys[1] += state->collisions[idx];
+ t->keys[2] += state->rings_passed[idx] >= cfg->max_rings ? 1.0f : 0.0f;
+ t->keys[3] += out_of_bounds(agent->state.pos, RACE_OOB_SCALE) ? 1.0f : 0.0f;
+}
diff --git a/ocean/drone/tasklib.h b/ocean/drone/tasklib.h
new file mode 100644
index 0000000000..8eec90c62e
--- /dev/null
+++ b/ocean/drone/tasklib.h
@@ -0,0 +1,75 @@
+#pragma once
+
+#include "task_hover.h"
+#include "task_race.h"
+
+
+const char* task_name(TaskType task) {
+ switch (task) {
+ case TASK_HOVER: return "hover";
+ case TASK_RACE: return "race";
+ case TASK_SPHERE: return "sphere";
+ case TASK_CUBE: return "cube";
+ case TASK_FLAG: return "flag";
+ }
+ return "?";
+}
+
+int task_horizon(DroneEnv* env) {
+ switch (env->task) {
+ case TASK_RACE: return ((RaceConfig*)env->task_config)->horizon;
+ default: return ((HoverConfig*)env->task_config)->horizon;
+ }
+}
+
+void task_init(DroneEnv* env) {
+ switch (env->task) {
+ case TASK_RACE: race_init(env); break;
+ default: hover_init(env); break;
+ }
+}
+
+void task_close(DroneEnv* env) {
+ switch (env->task) {
+ case TASK_RACE: race_close(env); break;
+ default: hover_close(env); break;
+ }
+}
+
+void task_env_reset(DroneEnv* env) {
+ switch (env->task) {
+ case TASK_RACE: race_env_reset(env); break;
+ default: break;
+ }
+}
+
+void task_reset(DroneEnv* env, Drone* agent, int idx) {
+ switch (env->task) {
+ case TASK_HOVER: hover_reset(env, agent, idx); break;
+ case TASK_SPHERE: sphere_reset(env, agent, idx); break;
+ case TASK_CUBE: cube_reset(env, agent, idx); break;
+ case TASK_FLAG: flag_reset(env, agent, idx); break;
+ case TASK_RACE: race_reset(env, agent, idx); break;
+ }
+}
+
+float task_reward(DroneEnv* env, Drone* agent, int idx, StepCache* cache) {
+ switch (env->task) {
+ case TASK_RACE: return race_reward(env, agent, idx, cache);
+ default: return hover_reward(env, agent, idx, cache);
+ }
+}
+
+bool task_done(DroneEnv* env, Drone* agent, int idx, StepCache* cache) {
+ switch (env->task) {
+ case TASK_RACE: return race_done(env, agent, idx, cache);
+ default: return hover_done(env, agent, idx, cache);
+ }
+}
+
+void task_log(DroneEnv* env, Drone* agent, int idx, Log* log, StepCache* cache) {
+ switch (env->task) {
+ case TASK_RACE: race_log(env, agent, idx, log, cache); break;
+ default: hover_log(env, agent, idx, log, cache); break;
+ }
+}
diff --git a/ocean/enduro/binding.c b/ocean/enduro/binding.c
new file mode 100644
index 0000000000..e49da0b6b5
--- /dev/null
+++ b/ocean/enduro/binding.c
@@ -0,0 +1,36 @@
+#include "enduro.h"
+#define OBS_SIZE 68
+#define NUM_ATNS 1
+#define ACT_SIZES {9}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Enduro
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->car_width = dict_get(kwargs, "car_width")->value;
+ env->car_height = dict_get(kwargs, "car_height")->value;
+ env->max_enemies = dict_get(kwargs, "max_enemies")->value;
+ env->continuous = dict_get(kwargs, "continuous")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "reward", log->reward);
+ dict_set(out, "step_rew_car_passed_no_crash", log->step_rew_car_passed_no_crash);
+ dict_set(out, "crashed_penalty", log->crashed_penalty);
+ dict_set(out, "passed_cars", log->passed_cars);
+ dict_set(out, "passed_by_enemy", log->passed_by_enemy);
+ dict_set(out, "cars_to_pass", log->cars_to_pass);
+ dict_set(out, "days_completed", log->days_completed);
+ dict_set(out, "days_failed", log->days_failed);
+ dict_set(out, "collisions_player_vs_car", log->collisions_player_vs_car);
+ dict_set(out, "collisions_player_vs_road", log->collisions_player_vs_road);
+}
diff --git a/ocean/enduro/enduro.c b/ocean/enduro/enduro.c
new file mode 100644
index 0000000000..ec496febe0
--- /dev/null
+++ b/ocean/enduro/enduro.c
@@ -0,0 +1,72 @@
+// puffer_enduro.c
+
+#define MAX_ENEMIES 10
+
+#include
+#include
+#include
+#include
+#include "enduro.h"
+#include "raylib.h"
+#include "puffernet.h"
+
+void get_input(Enduro* env) {
+ 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)) || (IsKeyDown(KEY_S) && IsKeyDown(KEY_A))) {
+ env->actions[0] = ACTION_DOWNLEFT; // Decelerate and move left
+ } 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) || 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) || IsKeyDown(KEY_S)) {
+ env->actions[0] = ACTION_DOWN; // Decelerate
+ } else if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) {
+ env->actions[0] = ACTION_LEFT; // Move left
+ } else if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) {
+ env->actions[0] = ACTION_RIGHT; // Move right
+ } else {
+ env->actions[0] = ACTION_NOOP; // No action
+ }
+}
+
+int demo() {
+ Weights* weights = load_weights("resources/enduro/enduro_weights.bin");
+ int logit_sizes[1] = {9};
+ PufferNet* net = make_puffernet(weights, 1, 68, 128, 2, logit_sizes, 1);
+
+ Enduro env = {
+ .num_envs = 1,
+ .max_enemies = MAX_ENEMIES,
+ .obs_size = OBSERVATIONS_MAX_SIZE
+ };
+
+ allocate(&env);
+
+ init(&env);
+ c_reset(&env);
+ c_render(&env);
+
+ while (!WindowShouldClose()) {
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ get_input(&env);
+ } else {
+ forward_puffernet(net, env.observations, env.actions);
+ }
+
+ c_step(&env);
+ c_render(&env);
+ }
+
+ free_puffernet(net);
+ free(weights);
+ free_allocated(&env);
+ return 0;
+}
+
+int main() {
+ demo();
+ return 0;
+}
diff --git a/pufferlib/ocean/enduro/enduro.h b/ocean/enduro/enduro.h
similarity index 98%
rename from pufferlib/ocean/enduro/enduro.h
rename to ocean/enduro/enduro.h
index 0961caf3c4..1fa4759668 100644
--- a/pufferlib/ocean/enduro/enduro.h
+++ b/ocean/enduro/enduro.h
@@ -181,9 +181,10 @@ typedef struct Enduro {
Client* client;
Log log;
float* observations;
- int* actions;
+ float* actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
+ int num_agents;
size_t obs_size;
int num_envs;
float width;
@@ -271,6 +272,7 @@ typedef struct Enduro {
int currentDayTimeIndex;
int previousDayTimeIndex;
// RNG
+ unsigned int rng;
unsigned int rng_state;
int reset_count;
// Rewards
@@ -689,10 +691,7 @@ void init(Enduro* env) {
env->parallaxFactor = 1.0f;
env->dayCompleted = 0;
env->lane = 1;
- env->terminals[0] = 0;
- // Reset rewards and logs
- env->rewards[0] = 0.0f;
// Initialize tracking variables
env->tracking_episode_return = 0.0f;
env->tracking_episode_length = 0.0f;
@@ -707,28 +706,13 @@ void init(Enduro* env) {
env->tracking_days_failed = 0.0f;
env->tracking_collisions_player_vs_car = 0.0f;
env->tracking_collisions_player_vs_road = 0.0f;
-
- env->log.episode_return = 0.0f;
- env->log.episode_length = 0.0f;
- env->log.score = 0.0f;
- env->log.reward = 0.0f;
- env->log.step_rew_car_passed_no_crash = 0.0f;
- env->log.crashed_penalty = 0.0f;
- env->log.passed_cars = 0.0f;
- env->log.passed_by_enemy = 0.0f;
- env->log.cars_to_pass = INITIAL_CARS_TO_PASS;
- env->log.days_completed = 0;
- env->log.days_failed = 0;
- env->log.collisions_player_vs_car = 0.0f;
- env->log.collisions_player_vs_road = 0.0f;
- env->log.n = 0.0f;
}
void allocate(Enduro* env) {
env->observations = (float*)calloc(env->obs_size, sizeof(float));
- env->actions = (int*)calloc(1, sizeof(int));
+ env->actions = (float*)calloc(1, sizeof(float));
env->rewards = (float*)calloc(1, sizeof(float));
- env->terminals = (unsigned char*)calloc(1, sizeof(unsigned char));
+ env->terminals = (float*)calloc(1, sizeof(float));
}
void free_allocated(Enduro* env) {
@@ -929,9 +913,9 @@ void add_enemy_car(Enduro* env) {
}
// Randomly select a lane
- int lane = possible_lanes[rand() % num_possible_lanes];
+ int lane = possible_lanes[rand_r(&env->rng) % num_possible_lanes];
// Preferentially spawn in the last_spawned_lane 30% of the time
- if (rand() % 100 < 60 && env->last_spawned_lane != -1) {
+ if (rand_r(&env->rng) % 100 < 60 && env->last_spawned_lane != -1) {
lane = env->last_spawned_lane;
}
env->last_spawned_lane = lane;
@@ -943,14 +927,14 @@ void add_enemy_car(Enduro* env) {
.last_x = car_x_in_lane(env, lane, VANISHING_POINT_Y),
.last_y = VANISHING_POINT_Y,
.passed = false,
- .colorIndex = rand() % 6
+ .colorIndex = rand_r(&env->rng) % 6
};
// 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);
float scaled_car_length = CAR_HEIGHT * scale;
// Randomize min spacing between 1.0f and 6.0f car lengths
- float dynamic_spacing_factor = (rand() / (float)RAND_MAX) * 6.0f + 0.5f;
+ float dynamic_spacing_factor = (rand_r(&env->rng) / (float)RAND_MAX) * 6.0f + 0.5f;
float min_spacing = dynamic_spacing_factor * scaled_car_length;
for (int i = 0; i < env->numEnemies; i++) {
Car* existing_car = &env->enemyCars[i];
@@ -1374,8 +1358,8 @@ void c_step(Enduro* env) {
int num_to_spawn = 1;
// Randomly decide to spawn more cars in a clump
- if ((rand() / (float)RAND_MAX) < clump_probability) {
- num_to_spawn = 1 + rand() % 2; // Spawn 1 to 3 cars
+ if ((rand_r(&env->rng) / (float)RAND_MAX) < clump_probability) {
+ num_to_spawn = 1 + rand_r(&env->rng) % 2; // Spawn 1 to 3 cars
}
// Track occupied lanes to prevent over-blocking
@@ -1385,7 +1369,7 @@ void c_step(Enduro* env) {
// Find an unoccupied lane
int lane;
do {
- lane = rand() % NUM_LANES;
+ lane = rand_r(&env->rng) % NUM_LANES;
} while (occupied_lanes[lane]);
// Mark the lane as occupied
@@ -1611,14 +1595,14 @@ void update_road_curve(Enduro* env) {
for (int i = 0; i < 3; i++) {
// Generate random step thresholds
- step_thresholds[i] = 1500 + rand() % 3801; // Random value between 1500 and 3800
+ step_thresholds[i] = 1500 + rand_r(&env->rng) % 3801; // Random value between 1500 and 3800
// Generate a random curve direction (-1, 0, 1) with rules
int direction_choices[] = {-1, 0, 1};
int next_direction;
do {
- next_direction = direction_choices[rand() % 3];
+ next_direction = direction_choices[rand_r(&env->rng) % 3];
} while ((last_direction == -1 && next_direction == 1) || (last_direction == 1 && next_direction == -1));
curve_directions[i] = next_direction;
diff --git a/ocean/freeway/binding.c b/ocean/freeway/binding.c
new file mode 100644
index 0000000000..1f6b6e112e
--- /dev/null
+++ b/ocean/freeway/binding.c
@@ -0,0 +1,35 @@
+#include "freeway.h"
+#define OBS_SIZE 34
+#define NUM_ATNS 1
+#define ACT_SIZES {3}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Freeway
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->frameskip = dict_get(kwargs, "frameskip")->value;
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->player_width = dict_get(kwargs, "player_width")->value;
+ env->player_height = dict_get(kwargs, "player_height")->value;
+ env->car_width = dict_get(kwargs, "car_width")->value;
+ env->car_height = dict_get(kwargs, "car_height")->value;
+ env->lane_size = dict_get(kwargs, "lane_size")->value;
+ env->difficulty = dict_get(kwargs, "difficulty")->value;
+ env->level = dict_get(kwargs, "level")->value;
+ env->enable_human_player = dict_get(kwargs, "enable_human_player")->value;
+ env->env_randomization = dict_get(kwargs, "env_randomization")->value;
+ env->use_dense_rewards = dict_get(kwargs, "use_dense_rewards")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "up_action_frac", log->up_action_frac);
+ dict_set(out, "hits", log->hits);
+}
diff --git a/pufferlib/ocean/freeway/freeway.c b/ocean/freeway/freeway.c
similarity index 76%
rename from pufferlib/ocean/freeway/freeway.c
rename to ocean/freeway/freeway.c
index f36f2f9c8b..bb1c2ab0cf 100644
--- a/pufferlib/ocean/freeway/freeway.c
+++ b/ocean/freeway/freeway.c
@@ -1,12 +1,12 @@
#include
#include "freeway.h"
#include "puffernet.h"
-#include
+#include
-int main() {
- Weights* weights = load_weights("resources/freeway/freeway_weights.bin", 137092);
+void demo() {
+ Weights* weights = load_weights("resources/freeway/freeway_weights.bin");
int logit_sizes[1] = {3};
- LinearLSTM* net = make_linearlstm(weights, 1, 34, logit_sizes, 1);
+ PufferNet* net = make_puffernet(weights, 1, 34, 128, 7, logit_sizes, 1);
Freeway env = {
.frameskip=4,
@@ -28,15 +28,22 @@ int main() {
env.client = make_client(&env);
c_reset(&env);
+ SetTargetFPS(60);
while (!WindowShouldClose()) {
- forward_linearlstm(net, env.observations, env.actions);
+ forward_puffernet(net, env.observations, env.actions);
env.human_actions[0] = 0;
if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) env.human_actions[0] = 1;
if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) env.human_actions[0] = 2;
c_step(&env);
c_render(&env);
-
}
+ free_puffernet(net);
+ free(weights);
free_allocated(&env);
close_client(env.client);
}
+
+int main() {
+ demo();
+ return 0;
+}
diff --git a/pufferlib/ocean/freeway/freeway.h b/ocean/freeway/freeway.h
similarity index 97%
rename from pufferlib/ocean/freeway/freeway.h
rename to ocean/freeway/freeway.h
index c132fcab69..70b32aaa76 100644
--- a/pufferlib/ocean/freeway/freeway.h
+++ b/ocean/freeway/freeway.h
@@ -70,10 +70,11 @@ struct Freeway {
Client* client;
Log log;
float* observations;
- int* actions;
+ float* actions;
int* human_actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
+ int num_agents;
FreewayPlayer ai_player; // Player-Related
FreewayPlayer human_player;
@@ -101,6 +102,7 @@ struct Freeway {
int use_dense_rewards;
int env_randomization;
int enable_human_player;
+ unsigned int rng;
};
void load_level(Freeway* env, int level) {
@@ -152,7 +154,7 @@ void init(Freeway* env) {
env->enemies = (FreewayEnemy*)calloc(NUM_LANES*MAX_ENEMIES_PER_LANE, sizeof(FreewayEnemy));
env->human_actions = (int*)calloc(1, sizeof(int));
if ((env->level < 0) || (env->level >= NUM_LEVELS)) {
- env->level = rand() % NUM_LEVELS;
+ env->level = rand_r(&env->rng) % NUM_LEVELS;
}
load_level(env, env->level);
}
@@ -160,9 +162,9 @@ void init(Freeway* env) {
void allocate(Freeway* env) {
init(env);
env->observations = (float*)calloc(4 + NUM_LANES*MAX_ENEMIES_PER_LANE, sizeof(float));
- env->actions = (int*)calloc(1, sizeof(int));
+ env->actions = (float*)calloc(1, sizeof(float));
env->rewards = (float*)calloc(1, sizeof(float));
- env->terminals = (unsigned char*)calloc(1, sizeof(unsigned char));
+ env->terminals = (float*)calloc(1, sizeof(float));
}
void c_close(Freeway* env) {
@@ -213,7 +215,7 @@ void spawn_enemies(Freeway* env) {
float lane_offset_x;
FreewayEnemy* enemy;
for (int lane = 0; lane < NUM_LANES; lane++) {
- lane_offset_x = env->width * (rand() / (float) RAND_MAX);
+ lane_offset_x = env->width * (rand_r(&env->rng) / (float) RAND_MAX);
for (int i = 0; i < MAX_ENEMIES_PER_LANE; i++){
enemy = &env->enemies[lane * MAX_ENEMIES_PER_LANE + i];
if (enemy->is_enabled){
@@ -294,10 +296,10 @@ void clip_enemy_position(Freeway* env, FreewayEnemy* enemy){
void randomize_enemy_speed(Freeway* env) {
FreewayEnemy* enemy;
for (int lane = 0; lane < NUM_LANES; lane++) {
- int delta_speed = (rand() % 3) - 1; // Randomly increase or decrease speed
+ int delta_speed = (rand_r(&env->rng) % 3) - 1; // Randomly increase or decrease speed
for (int i = 0; i < MAX_ENEMIES_PER_LANE; i++) {
+ enemy = &env->enemies[lane*MAX_ENEMIES_PER_LANE + i];
if (enemy->speed_randomization) {
- enemy = &env->enemies[lane*MAX_ENEMIES_PER_LANE + i];
enemy->current_speed_idx = min(max(enemy->initial_speed_idx-2, enemy->current_speed_idx), enemy->initial_speed_idx+2);
enemy->current_speed_idx = min(max(0, enemy->current_speed_idx + delta_speed), 5);
enemy->enemy_vx = enemy->lane_idx < NUM_LANES/2 ? SPEED_VALUES[enemy->current_speed_idx] * TICK_RATE * env->width: -SPEED_VALUES[enemy->current_speed_idx] * TICK_RATE * env->width;
diff --git a/pufferlib/ocean/freeway/freeway_levels.h b/ocean/freeway/freeway_levels.h
similarity index 100%
rename from pufferlib/ocean/freeway/freeway_levels.h
rename to ocean/freeway/freeway_levels.h
diff --git a/ocean/g2048/binding.c b/ocean/g2048/binding.c
new file mode 100644
index 0000000000..e931ae7ee4
--- /dev/null
+++ b/ocean/g2048/binding.c
@@ -0,0 +1,27 @@
+#include "g2048.h"
+#define OBS_SIZE 16
+#define NUM_ATNS 1
+#define ACT_SIZES {4}
+#define OBS_TENSOR_T ByteTensor
+
+#define Env Game
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->scaffolding_ratio = dict_get(kwargs, "scaffolding_ratio")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "merge_score", log->merge_score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "lifetime_max_tile", log->lifetime_max_tile);
+ dict_set(out, "reached_16384", log->reached_16384);
+ dict_set(out, "reached_32768", log->reached_32768);
+ dict_set(out, "reached_65536", log->reached_65536);
+ dict_set(out, "reached_131072", log->reached_131072);
+}
diff --git a/ocean/g2048/g2048.c b/ocean/g2048/g2048.c
new file mode 100644
index 0000000000..e99eb13e61
--- /dev/null
+++ b/ocean/g2048/g2048.c
@@ -0,0 +1,57 @@
+#include "g2048.h"
+#include "puffernet.h"
+
+void demo() {
+ Weights* weights = load_weights("resources/g2048/g2048_weights.bin");
+ int logit_sizes[1] = {4};
+ PufferNet* net = make_puffernet(weights, 1, 16, 512, 4, logit_sizes, 1);
+
+ Game env = {
+ .scaffolding_ratio = 0.0,
+ };
+ init(&env);
+
+ unsigned char observations[16] = {0};
+ float actions[1] = {0};
+ float rewards[1] = {0};
+ float terminals[1] = {0};
+
+ env.observations = observations;
+ env.actions = actions;
+ env.rewards = rewards;
+ env.terminals = terminals;
+
+ c_reset(&env);
+ c_render(&env);
+
+ while (!WindowShouldClose()) {
+ // User can take control
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ bool pressed = false;
+ if (IsKeyPressed(KEY_UP) || IsKeyPressed(KEY_W)) { env.actions[0] = 0; pressed = true; }
+ else if (IsKeyPressed(KEY_DOWN) || IsKeyPressed(KEY_S)) { env.actions[0] = 1; pressed = true; }
+ else if (IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_A)) { env.actions[0] = 2; pressed = true; }
+ else if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_D)) { env.actions[0] = 3; pressed = true; }
+
+ if (pressed) {
+ c_step(&env);
+ }
+ } else {
+ float obs_f[16];
+ for (int i = 0; i < 16; i++) obs_f[i] = (float)env.observations[i];
+ forward_puffernet(net, obs_f, env.actions);
+ c_step(&env);
+ }
+
+ c_render(&env);
+ }
+
+ free_puffernet(net);
+ free(weights);
+ c_close(&env);
+}
+
+int main() {
+ demo();
+ return 0;
+}
diff --git a/ocean/g2048/g2048.h b/ocean/g2048/g2048.h
new file mode 100644
index 0000000000..e221188d54
--- /dev/null
+++ b/ocean/g2048/g2048.h
@@ -0,0 +1,502 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include "raylib.h"
+
+static inline int min(int a, int b) { return a < b ? a : b; }
+static inline int max(int a, int b) { return a > b ? a : b; }
+
+#define SIZE 4
+#define EMPTY 0
+#define UP 1
+#define DOWN 2
+#define LEFT 3
+#define RIGHT 4
+#define BASE_MAX_TICKS 1000
+
+// Reward constants
+#define MERGE_BASE_REWARD 0.05f
+#define MERGE_REWARD_SCALE 0.03f
+#define INVALID_MOVE_PENALTY -0.05f
+#define GAME_OVER_PENALTY -1.0f
+
+// Pow 1.5 lookup table for tiles 128+ (index = row[i] - 6)
+// Index: 1=128, 2=256, 3=512, 4=1024, 5=2048, 6=4096, 7=8192, 8=16384, 9=32768, 10=65536, 11=131k
+static const float pow15_table[12] = {
+ 0.0f, 1.0f, 2.83f, 5.20f, 8.0f, 11.18f, 14.70f, 18.52f, 22.63f, 27.0f, 31.62f, 36.48f,
+};
+
+static inline float calculate_perf(unsigned char max_tile) {
+ // Reaching 131k -> 1.0, 65k -> 0.8, 32k -> 0.4, 16k -> 0.2, 8k -> 0.1
+ float perf = 0.8f * (float)(1 << max_tile) / 65536.0f;
+ if (perf > 1.0f) perf = 1.0f;
+ return perf;
+}
+
+typedef struct Log {
+ float perf;
+ float score;
+ float merge_score;
+ float episode_return;
+ float episode_length;
+ float lifetime_max_tile;
+ float reached_16384;
+ float reached_32768;
+ float reached_65536;
+ float reached_131072;
+ float n;
+} Log;
+
+typedef struct Game {
+ Log log; // Required
+ unsigned char* observations; // Cheaper in memory if encoded in uint_8
+ float* actions; // Required
+ float* rewards; // Required
+ float* terminals; // Required
+ int num_agents; // Required for env_binding
+
+ float scaffolding_ratio; // The ratio for "scaffolding" runs, in which higher blocks are spawned
+ bool is_scaffolding_episode;
+
+ int score;
+ int tick;
+ unsigned char grid[SIZE][SIZE];
+ unsigned char lifetime_max_tile;
+ unsigned char max_tile; // Episode max tile
+ float episode_reward; // Accumulate episode reward
+ int moves_made;
+ int max_episode_ticks; // Dynamic max_ticks based on score
+
+ // Cached values to avoid recomputation
+ int empty_count;
+ bool game_over_cached;
+ bool grid_changed;
+ unsigned int rng;
+} Game;
+
+// Precomputed color table for rendering optimization
+const Color PUFF_BACKGROUND = (Color){6, 24, 24, 255};
+const Color PUFF_WHITE = (Color){241, 241, 241, 241};
+const Color PUFF_RED = (Color){187, 0, 0, 255};
+const Color PUFF_CYAN = (Color){0, 187, 187, 255};
+
+static Color tile_colors[17] = {
+ {6, 24, 24, 255}, // Empty/background
+ {187, 187, 187, 255}, // 2
+ {170, 187, 187, 255}, // 4
+ {150, 187, 187, 255}, // 8
+ {130, 187, 187, 255}, // 16
+ {110, 187, 187, 255}, // 32
+ {90, 187, 187, 255}, // 64 (Getting more cyan)
+ {70, 187, 187, 255}, // 128
+ {50, 187, 187, 255}, // 256
+ {30, 187, 187, 255}, // 512
+ {0, 187, 187, 255}, // 1024 (PUFF_CYAN)
+ {0, 150, 187, 255}, // 2048
+ {0, 110, 187, 255}, // 4096
+ {0, 70, 187, 255}, // 8192
+ {187, 0, 0, 255}, // 16384 (PUFF_RED)
+ {204, 173, 17, 255}, // 32768 (Gold)
+ {6, 24, 24, 255}, // 65536+ (Invisible)
+};
+
+// --- Logging ---
+void add_log(Game* game);
+
+// --- Required functions for env_binding.h ---
+void c_reset(Game* game);
+void c_step(Game* game);
+void c_render(Game* game);
+void c_close(Game* game);
+
+void init(Game* game) {
+ game->lifetime_max_tile = 0;
+ memset(game->grid, EMPTY, SIZE * SIZE);
+}
+
+void update_observations(Game* game) {
+ memcpy(game->observations, game->grid, SIZE * SIZE);
+}
+
+void add_log(Game* game) {
+ // Scaffolding runs will distort stats, so skip logging
+ if (game->is_scaffolding_episode) return;
+
+ // Update the lifetime best
+ if (game->max_tile > game->lifetime_max_tile) {
+ game->lifetime_max_tile = game->max_tile;
+ }
+
+ game->log.score += (float)(1 << game->max_tile);
+ game->log.perf += calculate_perf(game->max_tile);
+ game->log.merge_score += (float)game->score;
+ game->log.episode_length += game->tick;
+ game->log.episode_return += game->episode_reward;
+ game->log.lifetime_max_tile += (float)(1 << game->lifetime_max_tile);
+ game->log.reached_16384 += (game->max_tile >= 14);
+ game->log.reached_32768 += (game->max_tile >= 15);
+ game->log.reached_65536 += (game->max_tile >= 16);
+ game->log.reached_131072 += (game->max_tile >= 17);
+ game->log.n += 1;
+}
+
+static inline unsigned char get_new_tile(Game* game) {
+ // 10% chance of 2, 90% chance of 1
+ return (rand_r(&game->rng) % 10 == 0) ? 2 : 1;
+}
+
+static inline void place_tile_at_random_cell(Game* game, unsigned char tile) {
+ if (game->empty_count == 0) return;
+
+ int target = rand_r(&game->rng) % game->empty_count;
+ int pos = 0;
+ for (int i = 0; i < SIZE; i++) {
+ for (int j = 0; j < SIZE; j++) {
+ if (game->grid[i][j] == EMPTY) {
+ if (pos == target) {
+ game->grid[i][j] = tile;
+ game->empty_count--;
+ return;
+ }
+ pos++;
+ }
+ }
+ }
+}
+
+void set_scaffolding_curriculum(Game* game) {
+ if (game->lifetime_max_tile < 14) {
+ // Spawn one high tile from 8192 to 65536
+ int curriculum = rand_r(&game->rng) % 5;
+ unsigned char high_tile = max(12 + curriculum, game->lifetime_max_tile);
+ place_tile_at_random_cell(game, high_tile);
+
+ } else {
+ // base=14 until 65536 reached, then base=15 for 131072 practice
+ // All random placement, 1-2 tiles max
+ unsigned char base = (game->lifetime_max_tile >= 16) ? 15 : 14;
+ int curriculum = rand_r(&game->rng) % 4;
+
+ if (curriculum == 0) {
+ place_tile_at_random_cell(game, base);
+ } else if (curriculum == 1) {
+ place_tile_at_random_cell(game, base + 1);
+ } else if (curriculum == 2) {
+ place_tile_at_random_cell(game, base);
+ place_tile_at_random_cell(game, base - 1);
+ } else {
+ place_tile_at_random_cell(game, base + 1);
+ place_tile_at_random_cell(game, base);
+ }
+ }
+}
+
+void c_reset(Game* game) {
+ memset(game->grid, EMPTY, SIZE * SIZE);
+ game->score = 0;
+ game->tick = 0;
+ game->episode_reward = 0;
+ game->empty_count = SIZE * SIZE;
+ game->game_over_cached = false;
+ game->grid_changed = true;
+ game->moves_made = 0;
+ game->max_episode_ticks = BASE_MAX_TICKS;
+ game->max_tile = 0;
+
+ // Higher tiles are spawned in scaffolding episodes
+ // Having high tiles saves moves to get there, allowing agents to experience it faster
+ game->is_scaffolding_episode = (rand_r(&game->rng) / (float)RAND_MAX) < game->scaffolding_ratio;
+ if (game->is_scaffolding_episode) {
+ set_scaffolding_curriculum(game);
+
+ } else {
+ // Add two random tiles at the start
+ for (int i = 0; i < 2; i++) {
+ place_tile_at_random_cell(game, get_new_tile(game));
+ }
+ }
+
+ update_observations(game);
+}
+
+// Optimized slide and merge with fewer memory operations
+static inline bool slide_and_merge(Game* game, unsigned char* row, float* reward, float* score_increase) {
+ bool moved = false;
+ int write_pos = 0;
+
+ // Single pass: slide and identify merge candidates
+ for (int read_pos = 0; read_pos < SIZE; read_pos++) {
+ if (row[read_pos] != EMPTY) {
+ if (write_pos != read_pos) {
+ row[write_pos] = row[read_pos];
+ row[read_pos] = EMPTY;
+ moved = true;
+ }
+ write_pos++;
+ }
+ }
+
+ // Merge pass
+ for (int i = 0; i < SIZE - 1; i++) {
+ if (row[i] != EMPTY && row[i] == row[i + 1]) {
+ row[i]++;
+ // Tiles 2-64 (row[i] 1-6): base reward only
+ // Tiles 128+ (row[i] 7+): base + pow1.5 scaled bonus
+ if (row[i] <= 6) {
+ *reward += MERGE_BASE_REWARD;
+ } else {
+ *reward += MERGE_BASE_REWARD + pow15_table[row[i] - 6] * MERGE_REWARD_SCALE;
+ }
+ *score_increase += (float)(1 << (int)row[i]);
+ // Shift remaining elements left
+ for (int j = i + 1; j < SIZE - 1; j++) {
+ row[j] = row[j + 1];
+ }
+ row[SIZE - 1] = EMPTY;
+ moved = true;
+ }
+ }
+
+ return moved;
+}
+
+bool move(Game* game, int direction, float* reward, float* score_increase) {
+ bool moved = false;
+ unsigned char temp[SIZE];
+
+ if (direction == UP || direction == DOWN) {
+ for (int col = 0; col < SIZE; col++) {
+ // Extract column
+ for (int i = 0; i < SIZE; i++) {
+ int idx = (direction == UP) ? i : SIZE - 1 - i;
+ temp[i] = game->grid[idx][col];
+ }
+
+ if (slide_and_merge(game, temp, reward, score_increase)) {
+ moved = true;
+ // Write back column
+ for (int i = 0; i < SIZE; i++) {
+ int idx = (direction == UP) ? i : SIZE - 1 - i;
+ game->grid[idx][col] = temp[i];
+ }
+ }
+ }
+ } else {
+ for (int row = 0; row < SIZE; row++) {
+ // Extract row
+ for (int i = 0; i < SIZE; i++) {
+ int idx = (direction == LEFT) ? i : SIZE - 1 - i;
+ temp[i] = game->grid[row][idx];
+ }
+
+ if (slide_and_merge(game, temp, reward, score_increase)) {
+ moved = true;
+ // Write back row
+ for (int i = 0; i < SIZE; i++) {
+ int idx = (direction == LEFT) ? i : SIZE - 1 - i;
+ game->grid[row][idx] = temp[i];
+ }
+ }
+ }
+ }
+
+ if (moved) {
+ game->grid_changed = true;
+ game->game_over_cached = false; // Invalidate cache
+ }
+
+ return moved;
+}
+
+bool is_game_over(Game* game) {
+ // Use cached result if grid hasn't changed
+ if (!game->grid_changed) {
+ return game->game_over_cached;
+ }
+
+ // Quick check: if there are empty cells, game is not over
+ if (game->empty_count > 0) {
+ game->game_over_cached = false;
+ game->grid_changed = false;
+ return false;
+ }
+
+ // Check for possible merges
+ for (int i = 0; i < SIZE; i++) {
+ for (int j = 0; j < SIZE; j++) {
+ unsigned char current = game->grid[i][j];
+ if (i < SIZE - 1 && current == game->grid[i + 1][j]) {
+ game->game_over_cached = false;
+ game->grid_changed = false;
+ return false;
+ }
+ if (j < SIZE - 1 && current == game->grid[i][j + 1]) {
+ game->game_over_cached = false;
+ game->grid_changed = false;
+ return false;
+ }
+ }
+ }
+
+ game->game_over_cached = true;
+ game->grid_changed = false;
+ return true;
+}
+
+void update_stats(Game* game) {
+ int empty_count = 0;
+ unsigned char max_tile = 0;
+
+ for (int i = 0; i < SIZE; i++) {
+ for (int j = 0; j < SIZE; j++) {
+ unsigned char val = game->grid[i][j];
+ // Update empty count and max tile
+ if (val == EMPTY) empty_count++;
+ if (val > max_tile) {
+ max_tile = val;
+ }
+ }
+ }
+
+ game->empty_count = empty_count;
+ game->max_tile = max_tile;
+}
+
+void c_step(Game* game) {
+ float reward = 0.0f;
+ float score_add = 0.0f;
+ bool did_move = move(game, game->actions[0] + 1, &reward, &score_add);
+ game->tick++;
+
+ if (did_move) {
+ game->moves_made++;
+ // Refresh empty_count after merges so spawning uses the correct count.
+ update_stats(game);
+ place_tile_at_random_cell(game, get_new_tile(game));
+ game->score += score_add;
+
+ // Observations only change if the grid changes
+ update_observations(game);
+
+ // This is to limit infinite invalid moves during eval (happens for noob agents)
+ // Don't need to be tight. Don't need to show to human player.
+ int tick_multiplier = max(1, game->lifetime_max_tile - 8); // practically no limit for competent agent
+ game->max_episode_ticks = max(BASE_MAX_TICKS * tick_multiplier, game->score / 4);
+
+ } else {
+ reward = INVALID_MOVE_PENALTY;
+ // No need to update observations if the grid hasn't changed
+ }
+
+ bool game_over = is_game_over(game);
+ bool max_ticks_reached = game->tick >= game->max_episode_ticks;
+ game->terminals[0] = (game_over || max_ticks_reached) ? 1 : 0;
+
+ // Game over penalty overrides other rewards
+ if (game_over) {
+ reward += GAME_OVER_PENALTY;
+ }
+
+ game->rewards[0] = reward;
+ game->episode_reward += reward;
+
+ if (game->terminals[0]) {
+ add_log(game);
+ c_reset(game);
+ }
+}
+
+// Stepping for client/eval: no reward, no reset
+void step_without_reset(Game* game) {
+ float score_add = 0.0f;
+ float reward = 0.0f;
+ bool did_move = move(game, game->actions[0] + 1, &reward, &score_add);
+ game->tick++;
+
+ if (did_move) {
+ game->moves_made++;
+
+ // Refresh empty_count after merges so spawning uses the correct count.
+ update_stats(game);
+ place_tile_at_random_cell(game, get_new_tile(game));
+ game->score += score_add;
+
+ // Observations only change if the grid changes
+ update_observations(game);
+ }
+
+ bool game_over = is_game_over(game);
+ game->terminals[0] = (game_over) ? 1 : 0;
+}
+
+// Rendering optimizations
+void c_render(Game* game) {
+ static bool window_initialized = false;
+ static char score_text[32];
+ static const int px = 100;
+
+ if (!window_initialized) {
+ InitWindow(px * SIZE, px * SIZE + 50, "2048");
+ SetTargetFPS(30);
+ window_initialized = true;
+ }
+
+ if (IsKeyDown(KEY_ESCAPE)) {
+ CloseWindow();
+ exit(0);
+ }
+
+ BeginDrawing();
+ ClearBackground(PUFF_BACKGROUND);
+
+ // Draw grid
+ for (int i = 0; i < SIZE; i++) {
+ for (int j = 0; j < SIZE; j++) {
+ int val = game->grid[i][j];
+
+ // Use precomputed colors
+ int color_idx = min(val, 16); // Cap at the max index of our color array
+ Color color = tile_colors[color_idx];
+
+ DrawRectangle(j * px, i * px, px - 5, px - 5, color);
+
+ if (val > 0) {
+ int display_val = 1 << val; // Power of 2
+ // Pre-format text to avoid repeated formatting
+ snprintf(score_text, sizeof(score_text), "%d", display_val);
+
+ int font_size = 32;
+ int x_offset = 20; // Default for 4-digit numbers
+ if (display_val < 10) x_offset = 40; // 1-digit
+ else if (display_val < 100) x_offset = 35; // 2-digit
+ else if (display_val < 1000) x_offset = 25; // 3-digit
+ else if (display_val < 10000) x_offset = 15; // 4-digit
+ else if (display_val < 100000) x_offset = 2; // 5-digit
+ else {
+ font_size = 24;
+ x_offset = 5;
+ }
+
+ DrawText(score_text, j * px + x_offset, i * px + 34, font_size, PUFF_WHITE);
+ }
+ }
+ }
+
+ // Draw score (format once per frame)
+ snprintf(score_text, sizeof(score_text), "Score: %d", game->score);
+ DrawText(score_text, 10, px * SIZE + 10, 24, PUFF_WHITE);
+
+ snprintf(score_text, sizeof(score_text), "Moves: %d", game->moves_made);
+ DrawText(score_text, 210, px * SIZE + 10, 24, PUFF_WHITE);
+
+ EndDrawing();
+}
+
+void c_close(Game* game) {
+ if (IsWindowReady()) {
+ CloseWindow();
+ }
+}
diff --git a/ocean/go/binding.c b/ocean/go/binding.c
new file mode 100644
index 0000000000..c63516794f
--- /dev/null
+++ b/ocean/go/binding.c
@@ -0,0 +1,38 @@
+#include "go.h"
+// 9x9 - obs 326, act 82
+// 13x13 - obs 678, act 170
+// 19x19 - obs 1446, act 362
+#define OBS_SIZE 326
+#define NUM_ATNS 1
+#define ACT_SIZES {82}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env CGo
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->side = (rand_r(&env->rng) % 2) + 1;
+ env->selfplay = dict_get(kwargs, "selfplay")->value;
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->grid_size = dict_get(kwargs, "grid_size")->value;
+ env->board_width = dict_get(kwargs, "board_width")->value;
+ env->board_height = dict_get(kwargs, "board_height")->value;
+ env->grid_square_size = dict_get(kwargs, "grid_square_size")->value;
+ env->komi = dict_get(kwargs, "komi")->value;
+ env->reward_move_pass = dict_get(kwargs, "reward_move_pass")->value;
+ env->reward_move_invalid = dict_get(kwargs, "reward_move_invalid")->value;
+ env->reward_move_valid = dict_get(kwargs, "reward_move_valid")->value;
+ env->reward_player_capture = dict_get(kwargs, "reward_player_capture")->value;
+ env->reward_opponent_capture = dict_get(kwargs, "reward_opponent_capture")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "n", log->n);
+}
diff --git a/ocean/go/go.c b/ocean/go/go.c
new file mode 100644
index 0000000000..e4af73bb2b
--- /dev/null
+++ b/ocean/go/go.c
@@ -0,0 +1,91 @@
+#include
+#include "go.h"
+#include "puffernet.h"
+
+void demo(int grid_size) {
+
+ CGo env = {
+ .width = 950,
+ .height = 750,
+ .grid_size = grid_size,
+ .board_width = 600,
+ .board_height = 600,
+ .grid_square_size = 64,
+ .komi = 7.5,
+ .reward_move_pass = -0.518441,
+ .reward_move_valid = 0,
+ .reward_move_invalid = -0.0864746,
+ .reward_player_capture = 0.553628,
+ .reward_opponent_capture = -0.102283,
+ .selfplay = 0,
+ .side = 1,
+ };
+
+ Weights* weights = load_weights("resources/go/go_weights.bin");
+ int logit_sizes[1] = {grid_size * grid_size + 1};
+ int obs_size = grid_size * grid_size * 4 + 2;
+ PufferNet* net = make_puffernet(weights, 1, obs_size, 512, 1, logit_sizes, 1);
+ allocate(&env);
+ c_reset(&env);
+ c_render(&env);
+
+ int tick = 0;
+ while (!WindowShouldClose()) {
+ if(tick % 3 == 0) {
+ tick = 0;
+ int human_action = env.actions[0];
+ forward_puffernet(net, env.observations, env.actions);
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ env.actions[0] = human_action;
+ }
+ c_step(&env);
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ env.actions[0] = -1;
+ }
+ }
+ 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
+ }
+ }
+ }
+ c_render(&env);
+ }
+ free_puffernet(net);
+ free(weights);
+ free_allocated(&env);
+}
+
+int main() {
+ demo(9);
+ return 0;
+}
diff --git a/ocean/go/go.h b/ocean/go/go.h
new file mode 100644
index 0000000000..03c2224e0d
--- /dev/null
+++ b/ocean/go/go.h
@@ -0,0 +1,1013 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include "raylib.h"
+
+#define NOOP 0
+#define MOVE_MIN 1
+#define TICK_RATE 1.0f/60.0f
+#define NUM_DIRECTIONS 4
+#define ENV_WIN -1
+#define PLAYER_WIN 1
+#define MAX_CHANGED_PER_MOVE 362
+static const int DIRECTIONS[NUM_DIRECTIONS][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
+// LD_LIBRARY_PATH=raylib/lib ./go
+
+typedef struct Log Log;
+struct Log {
+ float perf;
+ float score;
+ float episode_return;
+ float episode_length;
+ float n;
+ float illegal_move_count;
+ float legal_move_count;
+ float pass_move_count;
+ float white_wins;
+ float black_wins;
+};
+
+typedef struct Group Group;
+struct Group {
+ int parent;
+ int rank;
+ int size;
+ int liberties;
+};
+
+int find(Group* groups, int x) {
+ if (groups[x].parent != x)
+ groups[x].parent = find(groups, groups[x].parent);
+ return groups[x].parent;
+}
+
+void union_groups(Group* groups, int pos1, int pos2) {
+ pos1 = find(groups, pos1);
+ pos2 = find(groups, pos2);
+
+ if (pos1 == pos2) return;
+
+ if (groups[pos1].rank < groups[pos2].rank) {
+ groups[pos1].parent = pos2;
+ groups[pos2].size += groups[pos1].size;
+ groups[pos2].liberties += groups[pos1].liberties;
+ } else if (groups[pos1].rank > groups[pos2].rank) {
+ groups[pos2].parent = pos1;
+ groups[pos1].size += groups[pos2].size;
+ groups[pos1].liberties += groups[pos2].liberties;
+ } else {
+ groups[pos2].parent = pos1;
+ groups[pos1].rank++;
+ groups[pos1].size += groups[pos2].size;
+ groups[pos1].liberties += groups[pos2].liberties;
+ }
+}
+
+typedef struct Client Client;
+typedef struct CGo CGo;
+struct CGo {
+ Client* client;
+ float* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ Log log;
+ float score;
+ int num_agents;
+ int width;
+ int height;
+ int* board_x;
+ int* board_y;
+ int board_width;
+ int board_height;
+ int grid_square_size;
+ int grid_size;
+ uint8_t* board_states;
+ uint8_t* previous_board_state;
+ int last_capture_position;
+ int moves_made;
+ int* capture_count;
+ float komi;
+ uint8_t* visited;
+ uint8_t current_version;
+ 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;
+ float tick;
+ int selfplay;
+ int turn;
+ int side;
+ int legal_move_count;
+ int illegal_move_count;
+ int pass_move_count;
+ int previous_move;
+ int human_play;
+ // undo stack
+ int changed_pos[MAX_CHANGED_PER_MOVE];
+ uint8_t old_board_values[MAX_CHANGED_PER_MOVE];
+ int changed_count;
+ int old_capture_count[2];
+ float old_reward;
+ float old_episode_return;
+ unsigned int rng;
+};
+
+void add_log(CGo* env) {
+ env->log.episode_length += env->tick;
+
+ // Calculate perf as a win rate (1.0 if win, 0.0 if loss)
+ float win_value = (env->score > 0) ? 1.0f : (env->score < 0) ? 0.0f : 0.5f;
+ float black_win = 0.0;
+ float white_win = 0.0;
+ if(env->score > 0){
+ if(env->side == 1){
+ black_win = 1.0;
+ }
+ else{
+ white_win = 1.0;
+ }
+ }
+ else if (env->score < 0){
+ if(env->side == 1){
+ white_win = 1.0;
+ }
+ else{
+ black_win = 1.0;
+ }
+ } else {
+ black_win = 0.5;
+ white_win = 0.5;
+ }
+ env->log.illegal_move_count += env->illegal_move_count;
+ env->log.legal_move_count += env->legal_move_count;
+ env->log.pass_move_count += env->pass_move_count;
+ env->log.perf += win_value;
+ env->log.black_wins += black_win;
+ env->log.white_wins += white_win;
+ env->log.score += env->score;
+ env->log.episode_return += env->rewards[0];
+ env->log.n += 1.0;
+}
+
+void generate_board_positions(CGo* env) {
+ for (int i = 0; i < (env->grid_size-1) * (env->grid_size-1); i++) {
+ int row = i / (env->grid_size-1);
+ int col = i % (env->grid_size-1);
+ env->board_x[i] = col * (env->grid_square_size-1);
+ env->board_y[i] = row * (env->grid_square_size-1);
+ }
+}
+
+void init_groups(CGo* env) {
+ for (int i = 0; i < (env->grid_size)*(env->grid_size); i++) {
+ env->groups[i].parent = i;
+ env->groups[i].rank = 0;
+ env->groups[i].size = 1;
+ env->groups[i].liberties = 0;
+ }
+}
+
+void init(CGo* env) {
+ int board_render_size = (env->grid_size-1)*(env->grid_size-1);
+ int grid_size = env->grid_size*env->grid_size;
+ env->board_x = (int*)calloc(board_render_size, sizeof(int));
+ env->board_y = (int*)calloc(board_render_size, sizeof(int));
+ env->board_states = (uint8_t*)calloc(grid_size, sizeof(uint8_t));
+ env->visited = (uint8_t*)calloc(grid_size, sizeof(uint8_t));
+ env->current_version = 1;
+ env->previous_board_state = (uint8_t*)calloc(grid_size, sizeof(uint8_t));
+ env->capture_count = (int*)calloc(2, sizeof(int));
+ env->groups = (Group*)calloc(grid_size, sizeof(Group));
+ env->temp_groups = (Group*)calloc(grid_size, sizeof(Group));
+ generate_board_positions(env);
+ init_groups(env);
+}
+
+void allocate(CGo* env) {
+ init(env);
+ if(env->selfplay){
+ env->observations = (float*)calloc(2*((env->grid_size)*(env->grid_size)*4 +2), sizeof(float));
+ env->actions = (float*)calloc(2, sizeof(float));
+ } else{
+ // +2 correct?
+ env->observations = (float*)calloc((env->grid_size)*(env->grid_size)*4 +2, sizeof(float));
+ env->actions = (float*)calloc(1, sizeof(float));
+ }
+ env->rewards = (float*)calloc(1, sizeof(float));
+ env->terminals = (float*)calloc(1, sizeof(float));
+}
+
+void c_close(CGo* env) {
+ free(env->board_x);
+ free(env->board_y);
+ free(env->board_states);
+ free(env->visited);
+ free(env->previous_board_state);
+ free(env->capture_count);
+ free(env->temp_groups);
+ free(env->groups);
+}
+
+void free_allocated(CGo* env) {
+ free(env->actions);
+ free(env->observations);
+ free(env->terminals);
+ free(env->rewards);
+ c_close(env);
+}
+
+static inline void increment_version(CGo* env) {
+ env->current_version++;
+ if (env->current_version == 0) {
+ memset(env->visited, 0, (env->grid_size) * (env->grid_size));
+ env->current_version = 1;
+ }
+}
+
+void compute_observations(CGo* env) {
+ int obs_len = env->grid_size * env->grid_size * 4 + 2;
+ int N = env->grid_size * env->grid_size;
+ int iterations = env->selfplay ? 2 : 1;
+
+ for(int i = 0; i < iterations; i++){
+ float* current_obs = env->observations + (i * obs_len);
+
+ int self, opp;
+ if (i == 0) {
+ self = env->side;
+ opp = 3 - self;
+ } else {
+ // Flip perspective for selfplay
+ self = 3 - env->side;
+ opp = env->side;
+ }
+ int turn = env->turn + 1 == self ? 1 : 0;
+
+ // Memory Layout: [Current Self][Current Opp][Prev Self][Prev Opp]
+ float* plane_self = current_obs;
+ float* plane_opp = current_obs + N;
+ float* plane_prev_self = current_obs + (2 * N);
+ float* plane_prev_opp = current_obs + (3 * N);
+
+ for (int idx = 0; idx < N; idx++) {
+ int val = env->board_states[idx];
+ int prev_val = env->previous_board_state[idx];
+
+ plane_self[idx] = (float)(val == self);
+ plane_opp[idx] = (float)(val == opp);
+
+ plane_prev_self[idx] = (float)(prev_val == self);
+ plane_prev_opp[idx] = (float)(prev_val == opp);
+ }
+
+ // Set the color bit at the very end
+ current_obs[4 * N] = (float)(self - 1);
+ current_obs[4 * N + 1] = (float)(turn);
+ }
+}
+
+int is_valid_position(CGo* env, int x, int y) {
+ return (x >= 0 && x < env->grid_size && y >= 0 && y < env->grid_size);
+}
+
+
+void flood_fill(CGo* env, int x, int y, int* territory, int player) {
+ if (!is_valid_position(env, x, y)) {
+ return;
+ }
+
+ int pos = y * (env->grid_size) + x;
+ if (env->visited[pos] == env->current_version || env->board_states[pos] != 0) {
+ return;
+ }
+ env->visited[pos] = env->current_version;
+ territory[player]++;
+ // Check adjacent positions
+ for (int i = 0; i < 4; i++) {
+ flood_fill(env, x + DIRECTIONS[i][0], y + DIRECTIONS[i][1], territory, player);
+ }
+}
+
+void compute_score_tromp_taylor(CGo* env) {
+ int player_score = 0;
+ int opponent_score = 0;
+ int player = env->side;
+ int opponent = 3 - player;
+ increment_version(env);
+ // 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] == player) {
+ player_score++;
+ } else if (env->board_states[i] == opponent) {
+ opponent_score++;
+ }
+ }
+
+ // 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] == env->current_version) {
+ continue;
+ }
+
+ // 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] = env->current_version;
+
+ // 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;
+ int neighbor_color = env->board_states[npos];
+ if (neighbor_color ==0) {
+ // Add unvisited empty points to queue
+ if(env->visited[npos] != env->current_version) {
+ queue[rear++] = npos;
+ env->visited[npos] = env->current_version;
+ }
+ } else if (bordering_player == 0) {
+ bordering_player = neighbor_color;
+ } else if (bordering_player != neighbor_color) {
+ bordering_player = 3; // Mixed territory
+ }
+ }
+ }
+
+ // Assign territory points
+ if (bordering_player == player) {
+ player_score += territory_size;
+ } else if (bordering_player == opponent) {
+ opponent_score += territory_size;
+ }
+ // Mixed territories (bordering_player == 3) are neutral and not counted
+ }
+ float komi = (env->side == 2) ? env->komi : -env->komi;
+ env->score = (float)player_score - (float)opponent_score + komi;
+ //printf("Score: %f\n", env->score);
+}
+
+int find_in_group(int* group, int group_size, int value) {
+ for (int i = 0; i < group_size; i++) {
+ if (group[i] == value) {
+ return 1; // Found
+ }
+ }
+ return 0; // Not found
+}
+
+
+void capture_group(CGo* env, uint8_t* board, int root, int* affected_groups, int* affected_count) {
+ increment_version(env);
+ // Use a queue for BFS
+ int queue_size = (env->grid_size) * (env->grid_size);
+ int queue[queue_size];
+ int front = 0, rear = 0;
+
+ int captured_player = board[root]; // Player whose stones are being captured
+ if (captured_player != 1 && captured_player !=2) return;
+ int capturing_player = 3 - captured_player; // Player who captures
+
+ queue[rear++] = root;
+ env->visited[root] = env->current_version;
+
+ while (front != rear) {
+ int pos = queue[front++];
+ env->old_board_values[env->changed_count] = board[pos]; // captured_player
+ env->changed_pos[env->changed_count] = pos;
+ env->changed_count++;
+ board[pos] = 0; // Remove stone
+ env->capture_count[capturing_player - 1]++; // Update capturing player's count
+ if(capturing_player == env->side){
+ 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);
+
+ for (int i = 0; i < 4; i++) {
+ int nx = x + DIRECTIONS[i][0];
+ int ny = y + DIRECTIONS[i][1];
+ int npos = ny * (env->grid_size) + nx;
+
+ if (!is_valid_position(env, nx, ny)) {
+ continue;
+ }
+
+ if (board[npos] == captured_player && env->visited[npos]!=env->current_version) {
+ env->visited[npos] = env->current_version;
+ queue[rear++] = npos;
+ }
+ else if (board[npos] == capturing_player) {
+ int adj_root = find(env->temp_groups, npos);
+ if (find_in_group(affected_groups, *affected_count, adj_root)) {
+ continue;
+ }
+ affected_groups[(*affected_count)] = adj_root;
+ (*affected_count)++;
+ }
+ }
+ }
+}
+
+
+int count_liberties(CGo* env, int root, int* queue, uint8_t* board) {
+ increment_version(env);
+ int liberties = 0;
+ int front = 0;
+ int rear = 0;
+
+ queue[rear++] = root;
+ env->visited[root] = env->current_version;
+ while (front < rear) {
+ int pos = queue[front++];
+ int x = pos % (env->grid_size);
+ int y = pos / (env->grid_size);
+
+ 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->visited[npos]== env->current_version) {
+ continue;
+ }
+
+ int temp_npos = board[npos];
+ if (temp_npos == 0) {
+ liberties++;
+ env->visited[npos] = env->current_version;
+ } else if (temp_npos == board[root]) {
+ queue[rear++] = npos;
+ env->visited[npos] = env->current_version;
+ }
+ }
+ }
+ return liberties;
+}
+
+int make_move(CGo* env, int pos, int player){
+ int x = pos % (env->grid_size);
+ int y = pos / (env->grid_size);
+ // cannot place stone on occupied tile
+ if (env->board_states[pos] != 0) {
+ if(player == env->side){
+ env->illegal_move_count+=1;
+ }
+ return 0 ;
+ }
+ env->old_capture_count[0] = env->capture_count[0];
+ env->old_capture_count[1] = env->capture_count[1];
+ env->old_reward = env->rewards[0];
+ env->old_episode_return = env->log.episode_return;
+
+ env->changed_count = 0;
+
+ env->old_board_values[env->changed_count] = env->board_states[pos];
+ env->changed_pos[env->changed_count++] = pos;
+ env->board_states[pos] = player;
+ // temp structures
+ memcpy(env->temp_groups, env->groups, sizeof(Group) * (env->grid_size) * (env->grid_size));
+ // create new group
+ env->temp_groups[pos].parent = pos;
+ env->temp_groups[pos].rank = 0;
+ env->temp_groups[pos].size = 1;
+ env->temp_groups[pos].liberties = 0;
+
+ int max_affected_groups = (env->grid_size) * (env->grid_size);
+ int affected_groups[max_affected_groups];
+ int affected_count = 0;
+ affected_groups[affected_count++] = pos;
+
+ int queue[(env->grid_size) * (env->grid_size)];
+
+ // Perform unions and track affected groups
+ for (int i = 0; i < 4; i++) {
+ int nx = x + DIRECTIONS[i][0];
+ int ny = y + DIRECTIONS[i][1];
+ int npos = ny * (env->grid_size) + nx;
+ if (!is_valid_position(env, nx, ny)) {
+ continue;
+ }
+ if (env->board_states[npos] == player) {
+ union_groups(env->temp_groups, pos, npos);
+ affected_groups[affected_count++] = npos;
+ } else if (env->board_states[npos] == 3 - player) {
+ affected_groups[affected_count++] = npos;
+ }
+ }
+
+ // Recalculate liberties only for affected groups
+ for (int i = 0; i < affected_count; i++) {
+ int root = find(env->temp_groups, affected_groups[i]);
+ env->temp_groups[root].liberties = count_liberties(env, root, queue, env->board_states);
+ }
+
+ // Check for captures
+ bool captured = false;
+ for (int i = 0; i < affected_count; i++) {
+ int root = find(env->temp_groups, affected_groups[i]);
+ if (env->board_states[root] == 3 - player && env->temp_groups[root].liberties == 0) {
+ capture_group(env, env->board_states, root, affected_groups, &affected_count);
+ captured = true;
+ }
+ }
+ // If captures occurred, recalculate liberties again
+ if (captured) {
+ for (int i = 0; i < affected_count; i++) {
+ int root = find(env->temp_groups, affected_groups[i]);
+ env->temp_groups[root].liberties = count_liberties(env, root, queue, env->board_states);
+ }
+ // Check for ko rule violation
+ }
+
+ // self capture
+ int root = find(env->temp_groups, pos);
+ if (env->temp_groups[root].liberties == 0) {
+ goto rollback;
+ }
+
+ if(captured && memcmp(env->board_states, env->previous_board_state, env->grid_size*env->grid_size*sizeof(uint8_t)) == 0){
+ goto rollback;
+ }
+ memcpy(env->previous_board_state, env->board_states, sizeof(uint8_t) * (env->grid_size) * (env->grid_size));
+ memcpy(env->groups, env->temp_groups, sizeof(Group) * (env->grid_size) * (env->grid_size));
+ for(int i = 0; i < env->changed_count; i++){
+ env->previous_board_state[env->changed_pos[i]] = env->old_board_values[i];
+ }
+ return 1;
+
+rollback:
+ for (int i = 0; i < env->changed_count; i++) {
+ env->board_states[env->changed_pos[i]] = env->old_board_values[i];
+ }
+ env->capture_count[0] = env->old_capture_count[0];
+ env->capture_count[1] = env->old_capture_count[1];
+ env->rewards[0] = env->old_reward;
+ env->log.episode_return = env->old_episode_return;
+
+ if (player == env->side) env->illegal_move_count++;
+
+ return 0;
+}
+
+
+void enemy_random_move(CGo* env, int side){
+ int num_positions = (env->grid_size)*(env->grid_size);
+ int positions[num_positions];
+ int count = 0;
+
+ // Collect all empty positions
+ for(int i = 0; i < num_positions; i++){
+ if(env->board_states[i] == 0){
+ positions[count++] = i;
+ }
+ }
+ // Shuffle the positions
+ for(int i = count - 1; i > 0; i--){
+ int j = rand_r(&env->rng) % (i + 1);
+ int temp = positions[i];
+ positions[i] = positions[j];
+ positions[j] = temp;
+ }
+ // Try to make a move in a random empty position
+ for(int i = 0; i < count; i++){
+ if(make_move(env, positions[i], side)){
+ env->previous_move = positions[i] + 1;
+ return;
+ }
+ }
+ // If no move is possible, pass or end the game
+ env->previous_move = 0;
+ env->terminals[0] = 1;
+}
+
+int find_group_liberty(CGo* env, int root){
+ increment_version(env);
+ int queue[(env->grid_size)*(env->grid_size)];
+ int front = 0, rear = 0;
+ queue[rear++] = root;
+ env->visited[root] = env->current_version;
+
+ while(front < rear){
+ int pos = queue[front++];
+ int x = pos % (env->grid_size);
+ int y = pos / (env->grid_size);
+
+ for(int i = 0; i < 4; i++){
+ int nx = x + DIRECTIONS[i][0];
+ int ny = y + DIRECTIONS[i][1];
+ int npos = ny * (env->grid_size) + nx;
+ if(!is_valid_position(env, nx, ny)){
+ continue;
+ }
+ if(env->board_states[npos] == 0){
+ return npos; // Found a liberty
+ } else if(env->board_states[npos] == env->board_states[root] && env->visited[npos] != env->current_version){
+ env->visited[npos] = env->current_version;
+ queue[rear++] = npos;
+ }
+ }
+ }
+ return -1; // Should not happen if liberties > 0
+}
+
+void enemy_greedy_hard(CGo* env, int side){
+
+ int opp = 3 - side;
+ // Attempt to capture opponent stones in atari
+ int liberties[4][(env->grid_size) * (env->grid_size)];
+ int liberty_counts[4] = {0};
+ for(int i = 0; i < (env->grid_size)*(env->grid_size); i++){
+ if(env->board_states[i]==0){
+ continue;
+ }
+ if (env->board_states[i]==opp){
+ int root = find(env->groups, i);
+ int group_liberties = env->groups[root].liberties;
+ if (group_liberties >= 1 && group_liberties <= 4) {
+ int liberty = find_group_liberty(env, root);
+ liberties[group_liberties - 1][liberty_counts[group_liberties - 1]++] = liberty;
+ }
+ } else if (env->board_states[i]==side){
+ int root = find(env->groups, i);
+ int group_liberties = env->groups[root].liberties;
+ if (group_liberties==1) {
+ int liberty = find_group_liberty(env, root);
+ liberties[group_liberties - 1][liberty_counts[group_liberties - 1]++] = liberty;
+ }
+ }
+ }
+ // make move to attack or defend
+ for (int priority = 0; priority < 4; priority++) {
+ for (int i = 0; i < liberty_counts[priority]; i++) {
+ if (make_move(env, liberties[priority][i], side)) {
+ env->previous_move = liberties[priority][i]+1;
+ return;
+ }
+ }
+ }
+
+ // random move
+ enemy_random_move(env, side);
+}
+
+void enemy_greedy_easy(CGo* env, int side){
+ // Attempt to capture opponent stones in atari
+ for(int i = 0; i < (env->grid_size)*(env->grid_size); i++){
+ if(env->board_states[i] != 1){
+ continue;
+ }
+ int root = find(env->groups, i);
+ if(env->groups[root].liberties == 1){
+ int liberty = find_group_liberty(env, root);
+ if(make_move(env, liberty, 2)){
+ return; // Successful capture
+ }
+ }
+ }
+ // Protect own stones in atari
+ for(int i = 0; i < (env->grid_size)*(env->grid_size); i++){
+ if(env->board_states[i] != 2){
+ continue;
+ }
+ // Enemy's own stones
+ int root = find(env->groups, i);
+ if(env->groups[root].liberties == 1){
+ int liberty = find_group_liberty(env, root);
+ if(make_move(env, liberty, 2)){
+ return; // Successful defense
+ }
+ }
+ }
+ // Play a random legal move
+ enemy_random_move(env, side);
+}
+
+void c_reset(CGo* env) {
+ env->tick = 0;
+ env->illegal_move_count = 0;
+ env->legal_move_count = 0;
+ env->pass_move_count = 0;
+ env->turn = 0;
+ env->previous_move = -1;
+ // We don't reset the log struct - leave it accumulating like in Pong
+ env->score = 0;
+ for (int i = 0; i < (env->grid_size)*(env->grid_size); i++) {
+ env->board_states[i] = 0;
+ env->visited[i] = 0;
+ env->previous_board_state[i] = 0;
+ env->groups[i].parent = i;
+ env->groups[i].rank = 0;
+ env->groups[i].size = 0;
+ env->groups[i].liberties = 0;
+ }
+ env->capture_count[0] = 0;
+ env->capture_count[1] = 0;
+ env->last_capture_position = -1;
+ env->moves_made = 0;
+ compute_observations(env);
+}
+
+void clip_rewards(CGo* env){
+ if(env->rewards[0] > 1){
+ env->rewards[0] = 1;
+ }
+ if(env->rewards[0] < -1){
+ env->rewards[0] = -1;
+ }
+}
+
+void end_game(CGo* env){
+ compute_score_tromp_taylor(env);
+ if (env->score > 0) {
+ env->rewards[0] = 1.0;
+ }
+ else if (env->score < 0) {
+ env->rewards[0] = -1.0;
+ }
+ else {
+ env->rewards[0] = 0.0;
+ }
+ //env->rewards[0] = env->score / 10.0f;
+ clip_rewards(env);
+ env->terminals[0] = 1;
+ add_log(env);
+ c_reset(env);
+}
+
+void human_play(CGo* env){
+ int indx=1;
+ if(!env->selfplay || !env->human_play){
+ return;
+ }
+ if(env->selfplay && env->turn + 1 != env->side){
+ env->actions[indx] = -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[indx] = (unsigned short)pointIndex;
+ }
+ // Check if pass button is clicked
+ int left = (env->grid_size + 1)*env->grid_square_size;
+ int top = env->grid_square_size;
+ int passButtonX = left;
+ int passButtonY = top + 90;
+ int passButtonWidth = 100;
+ int passButtonHeight = 50;
+
+ if (mousePos.x >= passButtonX && mousePos.x <= passButtonX + passButtonWidth &&
+ mousePos.y >= passButtonY && mousePos.y <= passButtonY + passButtonHeight) {
+ env->actions[indx] = 0; // Send action 0 for pass
+ }
+ }
+
+}
+
+void c_step(CGo* env) {
+ env->tick += 1;
+ env->rewards[0] = 0.0;
+ env->terminals[0] = 0;
+ int action = 0;
+ int bot_side = 3 - env->side;
+ int is_legal = 0;
+ if(env->human_play){
+ human_play(env);
+ }
+ if(env->selfplay){
+ action = (env->turn +1 == env->side) ? (int)env->actions[0] : (int)env->actions[1];
+ } else {
+ action = (int)env->actions[0];
+ }
+ if(action == -1){
+ compute_observations(env);
+ return;
+ }
+ // useful for training , can prob be a hyper param. Recommend to increase with larger board size
+ float max_moves = 3 * env->grid_size * env->grid_size;
+ if (env->tick > max_moves && !env->human_play) {
+ env->terminals[0] = 1;
+ end_game(env);
+ compute_observations(env);
+ return;
+ }
+ // play against bots
+ if(!env->selfplay && env->turn == (bot_side - 1)){
+ enemy_greedy_hard(env, bot_side);
+ if (env->terminals[0] == 1) {
+ end_game(env);
+ }
+ compute_observations(env);
+ clip_rewards(env);
+ env->turn = (env->turn + 1) % 2;
+ return;
+ }
+ // process action
+ if(action == NOOP){
+ if(env->turn + 1 == env->side){
+ //printf("Pass\n");
+ env->legal_move_count +=1;
+ env->rewards[0] = env->reward_move_pass;
+ env->log.episode_return += env->reward_move_pass;
+ env->pass_move_count += 1;
+ }
+ if (env->terminals[0] == 1 || env->previous_move == NOOP) {
+ end_game(env);
+ return;
+ }
+ env->previous_move = NOOP;
+ env->turn = (env->turn+1)%2;
+ compute_observations(env);
+ return;
+ }
+ if (action >= MOVE_MIN && action <= (env->grid_size)*(env->grid_size)) {
+ is_legal = make_move(env, action - 1, env->turn + 1);
+ if(is_legal) {
+ env->moves_made++;
+ if(env->turn + 1 == env->side){
+ env->legal_move_count +=1;
+ env->rewards[0] += env->reward_move_valid;
+ env->log.episode_return += env->reward_move_valid;
+ }
+ } else {
+ if(env->turn + 1 == env->side){
+ env->rewards[0] = env->reward_move_invalid;
+ env->log.episode_return += env->reward_move_invalid;
+ }
+ }
+ }
+ env->previous_move = action;
+
+ if (env->terminals[0] == 1) {
+ end_game(env);
+ return;
+ }
+ if(is_legal){
+ env->turn = (env->turn + 1) % 2;
+ }
+ compute_observations(env);
+}
+
+const Color STONE_GRAY = (Color){80, 80, 80, 255};
+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_BACKGROUND2 = (Color){18, 72, 72, 255};
+
+struct Client {
+ float width;
+ float height;
+};
+
+Client* make_client(int width, int height) {
+ Client* client = (Client*)calloc(1, sizeof(Client));
+ client->width = width;
+ client->height = height;
+ InitWindow(width, height, "PufferLib Ray Go");
+ SetTargetFPS(10);
+ return client;
+}
+
+
+void c_render(CGo* env) {
+ if (env->client == NULL) {
+ env->client = make_client(env->width, env->height);
+ }
+
+ if (IsKeyDown(KEY_ESCAPE)) {
+ exit(0);
+ }
+
+ BeginDrawing();
+ ClearBackground(PUFF_BACKGROUND);
+
+ int board_size = (env->grid_size + 1) * env->grid_square_size;
+ DrawRectangle(0, 0, board_size, board_size, PUFF_BACKGROUND);
+ DrawRectangle(
+ env->grid_square_size,
+ env->grid_square_size,
+ board_size - 2*env->grid_square_size,
+ board_size - 2*env->grid_square_size,
+ PUFF_BACKGROUND2
+ );
+ int start = env->grid_square_size;
+ int end = board_size - env->grid_square_size;
+ for (int i = 1; i <= env->grid_size; i++) {
+ DrawLineEx(
+ (Vector2){start, i*start},
+ (Vector2){end, i*start},
+ 4, PUFF_BACKGROUND
+ );
+ DrawLineEx(
+ (Vector2){i*start, start},
+ (Vector2){i*start, end},
+ 4, PUFF_BACKGROUND
+ );
+ }
+
+ for (int i = 0; i < (env->grid_size) * (env->grid_size); i++) {
+ int position_state = env->board_states[i];
+ int row = i / (env->grid_size);
+ int col = i % (env->grid_size);
+ int x = col * env->grid_square_size;
+ int y = row * env->grid_square_size;
+ // Calculate the circle position based on the grid
+ int circle_x = x + env->grid_square_size;
+ int circle_y = y + env->grid_square_size;
+ // if player draw circle tile for black
+ int inner = (env->grid_square_size / 2) - 4;
+ int outer = (env->grid_square_size / 2) - 2;
+ if (position_state == 1) {
+ DrawCircleGradient(circle_x, circle_y, outer, STONE_GRAY, BLACK);
+ }
+ // if enemy draw circle tile for white
+ if (position_state == 2) {
+ DrawCircleGradient(circle_x, circle_y, inner, WHITE, GRAY);
+ }
+ }
+ // design a pass button
+ int left = (env->grid_size + 1)*env->grid_square_size;
+ int top = env->grid_square_size;
+ DrawRectangle(left, top + 90, 100, 50, GRAY);
+ DrawText("Pass", left + 25, top + 105, 20, PUFF_WHITE);
+ DrawText(
+ TextFormat("Tick: %d", (int)env->tick),
+ left, top + 150, 20, PUFF_WHITE
+ );
+ if(env->side == 1){
+ DrawText(
+ TextFormat("Agent: black"),
+ left, top + 170, 20, PUFF_WHITE
+ );
+ }
+ else {
+ DrawText(
+ TextFormat("Agent: white"),
+ left, top + 170, 20, PUFF_WHITE
+ );
+ }
+ DrawText(
+ TextFormat("Black Capture Count: %d", env->capture_count[0]),
+ left, top, 20, PUFF_WHITE
+ );
+ DrawText(
+ TextFormat("White Capture Count: %d", env->capture_count[1]),
+ left, top + 40, 20, PUFF_WHITE
+ );
+ EndDrawing();
+}
+void close_client(Client* client) {
+ CloseWindow();
+ free(client);
+}
diff --git a/ocean/hex/binding.c b/ocean/hex/binding.c
new file mode 100644
index 0000000000..34bed40557
--- /dev/null
+++ b/ocean/hex/binding.c
@@ -0,0 +1,22 @@
+#include "hex.h"
+#define OBS_SIZE 2*TOTAL_CELLS
+#define NUM_ATNS 1
+#define ACT_SIZES {TOTAL_CELLS}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Hex
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->random_opponent=dict_get(kwargs, "random_opponent")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "n", log->n);
+}
diff --git a/ocean/hex/hex.c b/ocean/hex/hex.c
new file mode 100644
index 0000000000..f134d1b175
--- /dev/null
+++ b/ocean/hex/hex.c
@@ -0,0 +1,91 @@
+#include "hex.h"
+#include
+#include
+#include
+
+void allocate(Hex* env) {
+ env->observations = (float*)calloc(2 * TOTAL_CELLS, sizeof(float));
+ env->actions = (float*)calloc(1, sizeof(float));
+ env->terminals = (float*)calloc(1, sizeof(float));
+ env->rewards = (float*)calloc(1, sizeof(float));
+}
+
+void free_allocated(Hex* env) {
+ free(env->actions);
+ free(env->observations);
+ free(env->terminals);
+ free(env->rewards);
+}
+
+void demo() {
+ Hex env = {0};
+ allocate(&env);
+ c_reset(&env);
+ c_render(&env);
+ env.random_opponent = false;
+
+ while(!WindowShouldClose()) {
+ bool move_made = false;
+
+ if(IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+ Vector2 mouse = GetMousePosition();
+
+ int screen_width = GetScreenWidth();
+ int screen_height = GetScreenHeight();
+ float radius = 22.0f;
+ float sqrt3 = 1.73205f;
+ float hex_width = sqrt3 * radius;
+ float hex_height = 2.0f * radius;
+
+ float total_width = hex_width * BOARD_SIZE + hex_width * 0.5f * BOARD_SIZE;
+ float total_height = hex_height * 0.75f * BOARD_SIZE;
+
+ float start_x = screen_width / 2.0f - total_width / 2.0f + hex_width / 2.0f;
+ float start_y = screen_height / 2.0f - total_height / 2.0f + hex_height / 2.0f;
+
+ // Inverse map:
+ int r = (int)roundf((mouse.y - start_y) / (hex_height * 0.75f));
+ int c = (int)roundf((mouse.x - start_x) / hex_width - r * 0.5f);
+
+ if(r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE) {
+ env.actions[0] = r * BOARD_SIZE + c;
+ move_made = true;
+ }
+ }
+
+ if(move_made) {
+ c_step(&env);
+ }
+
+ c_render(&env);
+ }
+
+ free_allocated(&env);
+ c_close(&env);
+}
+
+void speed_test() {
+ Hex env = {0};
+ allocate(&env);
+ c_reset(&env);
+ clock_t start = clock();
+
+ int num_steps = 1000000;
+ for(int i = 0; i < num_steps; i++) {
+ env.actions[0] = compute_legal_move(&env);
+ c_step(&env);
+ }
+ clock_t end = clock();
+ double elapsed = (double)(end - start) / CLOCKS_PER_SEC;
+ printf("Time for %d steps: %.2f seconds\n", num_steps, elapsed);
+ printf("SPS: %.2fM\n", num_steps / elapsed / 1e6);
+
+ free_allocated(&env);
+ c_close(&env);
+}
+
+int main() {
+ demo();
+ // speed_test();
+ return 0;
+}
diff --git a/ocean/hex/hex.h b/ocean/hex/hex.h
new file mode 100644
index 0000000000..a7d16037e6
--- /dev/null
+++ b/ocean/hex/hex.h
@@ -0,0 +1,324 @@
+#include "raylib.h"
+#include
+#include
+#include
+#include
+#include
+
+#define BOARD_SIZE 11
+#define TOTAL_CELLS (BOARD_SIZE * BOARD_SIZE)
+#define PLAYER_COLOR 1
+#define ENV_COLOR -1
+
+// Virtual nodes for incremental connection tracking
+#define TOP_NODE TOTAL_CELLS
+#define BOTTOM_NODE (TOTAL_CELLS + 1)
+#define LEFT_NODE (TOTAL_CELLS + 2)
+#define RIGHT_NODE (TOTAL_CELLS + 3)
+#define TOTAL_NODES (TOTAL_CELLS + 4)
+
+const int dr[] = { -1, -1, 0, 0, 1, 1 };
+const int dc[] = { 0, 1, -1, 1, -1, 0 };
+
+typedef struct {
+ float perf;
+ float score;
+ float episode_return;
+ float episode_length;
+ float n;
+} Log;
+
+typedef struct {
+ Log log;
+ float* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ int num_agents;
+ int tick;
+ int current_player;
+ int8_t board[TOTAL_CELLS];
+ bool random_opponent;
+
+ // Disjoint Set Union (Union-Find) tracking arrays
+ int parent[TOTAL_NODES];
+ int size[TOTAL_NODES];
+
+ unsigned int rng;
+} Hex;
+
+void init(Hex* env) { env->tick = 0; }
+
+void add_log(Hex* env) {
+ env->log.perf += (env->rewards[0] > 0) ? 1 : 0;
+ env->log.score += env->rewards[0];
+ env->log.episode_length += env->tick;
+ env->log.episode_return += env->rewards[0];
+ env->log.n++;
+}
+
+// --- Union-Find (Disjoint Set) Logic ---
+void uf_init(Hex* env) {
+ for (int i = 0; i < TOTAL_NODES; i++) {
+ env->parent[i] = i;
+ env->size[i] = 1;
+ }
+}
+
+int uf_find(Hex* env, int i) {
+ int root = i;
+ while (root != env->parent[root]) {
+ root = env->parent[root];
+ }
+ // Path compression
+ int curr = i;
+ while (curr != root) {
+ int nxt = env->parent[curr];
+ env->parent[curr] = root;
+ curr = nxt;
+ }
+ return root;
+}
+
+void uf_union(Hex* env, int i, int j) {
+ int root_i = uf_find(env, i);
+ int root_j = uf_find(env, j);
+ if (root_i != root_j) {
+ // Union by size
+ if (env->size[root_i] < env->size[root_j]) {
+ env->parent[root_i] = root_j;
+ env->size[root_j] += env->size[root_i];
+ } else {
+ env->parent[root_j] = root_i;
+ env->size[root_i] += env->size[root_j];
+ }
+ }
+}
+// --- End Union-Find Logic ---
+
+void c_reset(Hex* env) {
+ // set board to empty board
+ memset(env->board, 0, sizeof(env->board));
+ env->current_player = 0;
+ env->tick = 0;
+ env->terminals[0] = 0;
+
+ uf_init(env);
+
+ for (int i = 0; i < 2 * TOTAL_CELLS; i++) {
+ env->observations[i] = 0;
+ }
+}
+
+bool invalid_move(int action, const int8_t* board) {
+ if (action < 0 || action >= TOTAL_CELLS) {
+ return true; // Out of bounds
+ }
+ if (board[action] != 0) {
+ return true; // Cell already occupied
+ }
+ return false;
+}
+
+int compute_legal_move(Hex* env) {
+ // Naive random move for the environment
+ int action;
+ do {
+ action = rand_r(&env->rng) % TOTAL_CELLS;
+ } while (invalid_move(action, env->board));
+
+ return action;
+}
+
+int compute_env_move(Hex* env, int player_last_action) {
+
+ // Get the coordinates of the player's last move.
+ int r = player_last_action / BOARD_SIZE;
+ int c = player_last_action % BOARD_SIZE;
+
+ int arr[6];
+ for (int i = 0; i < 6; i++) {
+ arr[i] = i;
+ }
+
+ for (int i = 6 - 1; i > 0; i--) {
+ int j = rand_r(&env->rng) % (i + 1);
+ int temp = arr[i];
+ arr[i] = arr[j];
+ arr[j] = temp;
+ }
+
+ int action = -1;
+ for (int j = 0; j < 6; j++) {
+ int i = arr[j];
+ int nr = r + dr[i];
+ int nc = c + dc[i];
+ int n_idx = nr * BOARD_SIZE + nc;
+ if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE) {
+ if (env->board[n_idx] == 0) {
+ action = n_idx;
+ break;
+ }
+ }
+ }
+ if (action == -1) {
+ action = compute_legal_move(env);
+ }
+ return action;
+}
+
+// Places a stone, merges components, and returns true if the player won
+bool place_stone_and_check_win(Hex* env, int action, int player) {
+ env->board[action] = player;
+ int offset = 0;
+ if (player == ENV_COLOR) {
+ offset = TOTAL_CELLS;
+ }
+ env->observations[action + offset] = 1;
+
+ int r = action / BOARD_SIZE;
+ int c = action % BOARD_SIZE;
+
+ // 1. Connect to adjacent stones of the same color
+ for (int i = 0; i < 6; i++) {
+ int nr = r + dr[i];
+ int nc = c + dc[i];
+
+ if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE) {
+ int n_idx = nr * BOARD_SIZE + nc;
+ if (env->board[n_idx] == player) {
+ uf_union(env, action, n_idx);
+ }
+ }
+ }
+
+ // 2. Connect to virtual edges and check for a winner
+ if (player == PLAYER_COLOR) {
+ if (r == 0)
+ uf_union(env, action, TOP_NODE);
+ if (r == BOARD_SIZE - 1)
+ uf_union(env, action, BOTTOM_NODE);
+
+ return uf_find(env, TOP_NODE) == uf_find(env, BOTTOM_NODE);
+ } else {
+ if (c == 0)
+ uf_union(env, action, LEFT_NODE);
+ if (c == BOARD_SIZE - 1)
+ uf_union(env, action, RIGHT_NODE);
+
+ return uf_find(env, LEFT_NODE) == uf_find(env, RIGHT_NODE);
+ }
+}
+
+void c_step(Hex* env) {
+ env->tick += 1;
+ int action = (int)env->actions[0];
+
+ if (invalid_move(action, env->board)) {
+ env->rewards[0] = -1;
+ env->terminals[0] = 1;
+ add_log(env);
+ c_reset(env);
+ return;
+ }
+
+ // Player move and incremental win check
+ if (place_stone_and_check_win(env, action, PLAYER_COLOR)) {
+ env->rewards[0] = 1;
+ env->terminals[0] = 1;
+
+ add_log(env);
+ c_reset(env);
+ return;
+ }
+ int env_action;
+
+ if (env->random_opponent) {
+ env_action = compute_legal_move(env);
+
+ } else {
+ env_action = compute_env_move(env, action);
+ }
+
+ if (place_stone_and_check_win(env, env_action, ENV_COLOR)) {
+ env->rewards[0] = -1;
+ env->terminals[0] = 1;
+
+ add_log(env);
+ c_reset(env);
+ return;
+ }
+}
+
+void c_render(Hex* env) {
+ int screen_width = 800;
+ int screen_height = 600;
+
+ if (!IsWindowReady()) {
+ InitWindow(screen_width, screen_height, "PufferLib Hex");
+ SetTargetFPS(60);
+ }
+
+ if (IsKeyDown(KEY_ESCAPE)) {
+ exit(0);
+ }
+
+ BeginDrawing();
+ ClearBackground((Color) { 6, 24, 24, 255 });
+
+ float radius = 22.0f;
+ float sqrt3 = 1.73205f;
+ float hex_width = sqrt3 * radius;
+ float hex_height = 2.0f * radius;
+
+ float total_width = hex_width * BOARD_SIZE + hex_width * 0.5f * BOARD_SIZE;
+ float total_height = hex_height * 0.75f * BOARD_SIZE;
+
+ float start_x = screen_width / 2.0f - total_width / 2.0f + hex_width / 2.0f;
+ float start_y = screen_height / 2.0f - total_height / 2.0f + hex_height / 2.0f;
+
+ // Draw borders to show player targets (Red connects Top/Bottom, Blue connects Left/Right)
+ for (int r = 0; r < BOARD_SIZE; r++) {
+ float left_x = start_x + (0 + r * 0.5f) * hex_width - hex_width * 0.8f;
+ float right_x = start_x + (BOARD_SIZE - 1 + r * 0.5f) * hex_width + hex_width * 0.8f;
+ float cy = start_y + r * hex_height * 0.75f;
+ DrawCircle(left_x, cy, radius * 0.3f, BLUE);
+ DrawCircle(right_x, cy, radius * 0.3f, BLUE);
+ }
+
+ for (int c = 0; c < BOARD_SIZE; c++) {
+ float cx_top = start_x + (c + 0 * 0.5f) * hex_width;
+ float cy_top = start_y + 0 * hex_height * 0.75f - hex_height * 0.6f;
+ float cx_bot = start_x + (c + (BOARD_SIZE - 1) * 0.5f) * hex_width;
+ float cy_bot = start_y + (BOARD_SIZE - 1) * hex_height * 0.75f + hex_height * 0.6f;
+ DrawCircle(cx_top, cy_top, radius * 0.3f, RED);
+ DrawCircle(cx_bot, cy_bot, radius * 0.3f, RED);
+ }
+
+ for (int r = 0; r < BOARD_SIZE; r++) {
+ for (int c = 0; c < BOARD_SIZE; c++) {
+ int idx = r * BOARD_SIZE + c;
+ int owner = env->board[idx];
+
+ Color color = DARKGRAY;
+ if (owner == PLAYER_COLOR)
+ color = RED;
+ else if (owner == ENV_COLOR)
+ color = BLUE;
+
+ float cx = start_x + (c + r * 0.5f) * hex_width;
+ float cy = start_y + r * hex_height * 0.75f;
+
+ DrawPoly((Vector2) { cx, cy }, 6, radius - 1.0f, 30.0f, color);
+ DrawPolyLines((Vector2) { cx, cy }, 6, radius - 1.0f, 30.0f, BLACK);
+ }
+ }
+
+ EndDrawing();
+}
+
+void c_close(Hex* env) {
+ if (IsWindowReady()) {
+ CloseWindow();
+ }
+}
diff --git a/pufferlib/ocean/impulse_wars/benchmark.c b/ocean/impulse_wars/benchmark.c
similarity index 100%
rename from pufferlib/ocean/impulse_wars/benchmark.c
rename to ocean/impulse_wars/benchmark.c
diff --git a/pufferlib/ocean/impulse_wars/binding.c b/ocean/impulse_wars/binding.c
similarity index 100%
rename from pufferlib/ocean/impulse_wars/binding.c
rename to ocean/impulse_wars/binding.c
diff --git a/pufferlib/ocean/impulse_wars/env.h b/ocean/impulse_wars/env.h
similarity index 100%
rename from pufferlib/ocean/impulse_wars/env.h
rename to ocean/impulse_wars/env.h
diff --git a/pufferlib/ocean/impulse_wars/game.h b/ocean/impulse_wars/game.h
similarity index 100%
rename from pufferlib/ocean/impulse_wars/game.h
rename to ocean/impulse_wars/game.h
diff --git a/pufferlib/ocean/impulse_wars/helpers.h b/ocean/impulse_wars/helpers.h
similarity index 100%
rename from pufferlib/ocean/impulse_wars/helpers.h
rename to ocean/impulse_wars/helpers.h
diff --git a/pufferlib/ocean/impulse_wars/impulse_wars.c b/ocean/impulse_wars/impulse_wars.c
similarity index 100%
rename from pufferlib/ocean/impulse_wars/impulse_wars.c
rename to ocean/impulse_wars/impulse_wars.c
diff --git a/pufferlib/ocean/impulse_wars/map.h b/ocean/impulse_wars/map.h
similarity index 100%
rename from pufferlib/ocean/impulse_wars/map.h
rename to ocean/impulse_wars/map.h
diff --git a/ocean/impulse_wars/render.h b/ocean/impulse_wars/render.h
new file mode 100644
index 0000000000..d4300d5538
--- /dev/null
+++ b/ocean/impulse_wars/render.h
@@ -0,0 +1,1778 @@
+#ifndef IMPULSE_WARS_RENDER_H
+#define IMPULSE_WARS_RENDER_H
+
+#include "raymath.h"
+#include "rlgl.h"
+
+#define RLIGHTS_IMPLEMENTATION
+#include "rlights.h"
+
+#include "helpers.h"
+
+#if defined(PLATFORM_DESKTOP)
+#define GLSL_VERSION 330
+#else // PLATFORM_ANDROID, PLATFORM_WEB
+#define GLSL_VERSION 100
+#endif
+
+#define LETTER_BOUNDRY_SIZE 0.25f
+#define TEXT_MAX_LAYERS 32
+#define LETTER_BOUNDRY_COLOR VIOLET
+
+bool SHOW_LETTER_BOUNDRY = false;
+
+const Color STONE_GRAY = (Color){80, 80, 80, 255};
+const Color PUFF_RED = RED;
+const Color PUFF_GREEN = GREEN;
+const Color PUFF_YELLOW = YELLOW;
+const Color PUFF_CYAN = BLUE;
+const Color PUFF_WHITE = RAYWHITE;
+const Color PUFF_BACKGROUND = BLACK;
+const Color PUFF_BACKGROUND2 = BLACK;
+
+void setEnvFrameRate(iwEnv *e);
+bool droneControlledByHuman(const iwEnv *e, uint8_t i);
+
+const float DEFAULT_SCALE = 11.0f;
+const uint16_t DEFAULT_WIDTH = 1280;
+const uint16_t DEFAULT_HEIGHT = 720;
+const uint16_t HEIGHT_LEEWAY = 75;
+
+const float START_READY_TIME = 1.5f;
+const float END_WAIT_TIME = 2.0f;
+
+const float EXPLOSION_TIME = 0.5f;
+
+const float DRONE_RESPAWN_GUIDE_SHRINK_TIME = 0.75f;
+const float DRONE_RESPAWN_GUIDE_HOLD_TIME = 0.75f;
+const float DRONE_RESPAWN_GUIDE_MAX_RADIUS = DRONE_RADIUS * 5.5f;
+const float DRONE_RESPAWN_GUIDE_MIN_RADIUS = DRONE_RADIUS * 2.5f;
+
+const float DRONE_PIECE_LIFETIME = 2.0f;
+
+const Color barolo = {.r = 165, .g = 37, .b = 8, .a = 255};
+const Color bambooBrown = {.r = 204, .g = 129, .b = 0, .a = 255};
+
+const float droneLightRadius = 0.1f;
+const float halfDroneRadius = DRONE_RADIUS / 2.0f;
+const float droneThrusterLength = 1.5f * DRONE_RADIUS;
+const float aimGuideLength = 0.3f * DRONE_RADIUS;
+const float chargedAimGuideLength = DRONE_RADIUS;
+
+static inline b2Vec2 rayVecToB2Vec(const iwEnv *e, const Vector2 v) {
+ return (b2Vec2){.x = (v.x - e->client->halfWidth) / e->renderScale, .y = ((v.y - e->client->halfHeight - (2 * e->renderScale)) / e->renderScale)};
+}
+
+void updateTrailPoints(trailPoints *tp, const uint8_t maxLen, const b2Vec2 pos) {
+ const Vector2 v = (Vector2){.x = pos.x, .y = pos.y};
+ if (tp->length < maxLen) {
+ tp->points[tp->length++] = v;
+ return;
+ }
+
+ for (uint8_t i = 0; i < maxLen - 1; i++) {
+ tp->points[i] = tp->points[i + 1];
+ }
+ tp->points[maxLen - 1] = v;
+}
+
+rayClient *createRayClient() {
+ SetConfigFlags(FLAG_MSAA_4X_HINT);
+ InitWindow(DEFAULT_WIDTH, DEFAULT_HEIGHT, "Impulse Wars");
+
+ rayClient *client = fastCalloc(1, sizeof(rayClient));
+
+ if (client->height == 0) {
+#ifndef __EMSCRIPTEN__
+ const int monitor = GetCurrentMonitor();
+ client->height = GetMonitorHeight(monitor) - HEIGHT_LEEWAY;
+ client->width = ((float)client->height * ((float)DEFAULT_WIDTH / (float)DEFAULT_HEIGHT));
+#else
+ client->width = DEFAULT_WIDTH;
+ client->height = DEFAULT_HEIGHT;
+#endif
+ }
+ client->scale = (float)client->height * (float)(DEFAULT_SCALE / DEFAULT_HEIGHT);
+
+ client->halfWidth = client->width / 2.0f;
+ client->halfHeight = client->height / 2.0f;
+
+ SetWindowSize(client->width, client->height);
+
+#ifndef __EMSCRIPTEN__
+ SetTargetFPS(EVAL_FRAME_RATE);
+#endif
+
+ client->camera = fastCalloc(1, sizeof(gameCamera));
+ client->camera->camera3D = (Camera3D){
+ .position = (Vector3){.x = 0.0f, .y = 100.0f, .z = 0.0f},
+ .target = (Vector3){.x = 0.0f, .y = 0.0f, .z = 0.0f},
+ .up = (Vector3){.x = 0.0f, .y = 0.0f, .z = -1.0f},
+ .fovy = 45.0f,
+ .projection = CAMERA_PERSPECTIVE,
+ };
+ client->camera->camera2D = (Camera2D){
+ .offset = (Vector2){.x = client->width / 2.0f, .y = client->height / 2.0f},
+ .target = (Vector2){.x = 0.0f, .y = 0.0f},
+ .zoom = 1.0f,
+ };
+ client->camera->orthographic = false;
+
+ client->wallTexture = LoadTexture("resources/impulse_wars/wall_texture_map.png");
+ client->blurSrcTexture = LoadRenderTexture(client->width, client->height);
+ client->blurDstTexture = LoadRenderTexture(client->width, client->height);
+ client->droneRawTex = LoadRenderTexture(client->width, client->height);
+ client->droneBloomTex = LoadRenderTexture(client->width, client->height);
+ client->projRawTex = LoadRenderTexture(client->width, client->height);
+ client->projBloomTex = LoadRenderTexture(client->width, client->height);
+
+ const char *gridVSPath = TextFormat("resources/impulse_wars/shaders/gls%i/shader.vs", GLSL_VERSION);
+ const char *gridFSPath = TextFormat("resources/impulse_wars/shaders/gls%i/grid.fs", GLSL_VERSION);
+ client->gridShader = LoadShader(gridVSPath, gridFSPath);
+ for (int i = 0; i < 4; i++) {
+ client->gridShaderPosLoc[i] = GetShaderLocation(client->gridShader, TextFormat("pos[%i]", i));
+ client->gridShaderColorLoc[i] = GetShaderLocation(client->gridShader, TextFormat("color[%i]", i));
+ }
+
+ const char *blurVSPath = TextFormat("resources/impulse_wars/shaders/gls%i/shader.vs", GLSL_VERSION);
+ const char *blurFSPath = TextFormat("resources/impulse_wars/shaders/gls%i/blur.fs", GLSL_VERSION);
+ client->blurShader = LoadShader(blurVSPath, blurFSPath);
+ client->blurShaderDirLoc = GetShaderLocation(client->blurShader, "uTexelDir");
+
+ const char *bloomVSPath = TextFormat("resources/impulse_wars/shaders/gls%i/shader.vs", GLSL_VERSION);
+ const char *bloomFSPath = TextFormat("resources/impulse_wars/shaders/gls%i/bloom.fs", GLSL_VERSION);
+ client->bloomShader = LoadShader(bloomVSPath, bloomFSPath);
+ int32_t bloomModeLoc = GetShaderLocation(client->bloomShader, "uBloomMode");
+ const int32_t bloomMode = 1;
+ SetShaderValue(client->bloomShader, bloomModeLoc, &bloomMode, SHADER_UNIFORM_INT);
+ client->bloomIntensityLoc = GetShaderLocation(client->bloomShader, "uBloomIntensity");
+ client->bloomTexColorLoc = GetShaderLocation(client->bloomShader, "uTexColor");
+ client->bloomTexBloomBlurLoc = GetShaderLocation(client->bloomShader, "uTexBloomBlur");
+
+ return client;
+}
+
+void setupRayClient(iwEnv *e) {
+ if (e->client != NULL) {
+ return;
+ }
+ // create a rendering client, change the env to eval mode and ensure
+ // it's reset so training only maps and behaviors aren't evaluated
+ e->client = createRayClient();
+ e->isTraining = false;
+ setEnvFrameRate(e);
+ e->needsReset = true;
+}
+
+void destroyRayClient(rayClient *client) {
+ UnloadTexture(client->wallTexture);
+ UnloadRenderTexture(client->blurSrcTexture);
+ UnloadRenderTexture(client->blurDstTexture);
+ UnloadRenderTexture(client->droneRawTex);
+ UnloadRenderTexture(client->droneBloomTex);
+ UnloadRenderTexture(client->projRawTex);
+ UnloadRenderTexture(client->projBloomTex);
+
+ UnloadShader(client->gridShader);
+ UnloadShader(client->blurShader);
+ UnloadShader(client->bloomShader);
+
+ CloseWindow();
+ fastFree(client->camera);
+ fastFree(client);
+}
+
+const float ZOOM_SPEED = 0.04f;
+const float PAN_SPEED = 0.04f;
+const float MAX_CAMERA_HEIGHT = 130.0f;
+const float MIN_CAMERA_HEIGHT = 60.0f;
+const float BOUNDS_PADDING = 4.0f;
+const float MAP_MAX_X_OFFSET = 25.0f;
+const float MAP_MAX_Y_OFFSET = 10.0f;
+
+void setCamera2DZoom(iwEnv *e) {
+ gameCamera *camera = e->client->camera;
+
+ float screenHeightInWorldUnits = 2.0f * tanf((camera->camera3D.fovy * DEG2RAD) / 2.0f) * camera->camera3D.position.y;
+ camera->camera2D.zoom = e->client->height / screenHeightInWorldUnits;
+}
+
+void setupEnvCamera(iwEnv *e) {
+ const float BASE_ROWS = 21.0f;
+ const float scale = e->client->scale * (BASE_ROWS / e->map->rows);
+ // TODO: remove this field?
+ e->renderScale = scale;
+
+ gameCamera *camera = e->client->camera;
+ if (camera->orthographic) {
+ camera->camera3D.projection = CAMERA_ORTHOGRAPHIC;
+ camera->camera3D.position.x = 0.0f;
+ camera->camera3D.position.y = 25.0f;
+ camera->camera3D.position.z = -2.0f;
+ camera->camera3D.target.x = 0.0f;
+ camera->camera3D.target.y = 0.0f;
+ camera->camera3D.target.z = -2.0f;
+ camera->camera3D.fovy = (5.0f * e->map->rows) - 15.0f;
+
+ camera->camera2D.target.x = 0.0f;
+ camera->camera2D.target.y = -2.0f;
+ camera->camera2D.zoom = (float)e->client->height / camera->camera3D.fovy;
+ } else {
+ camera->targetPos = Vector2Zero();
+
+ camera->camera3D.projection = CAMERA_PERSPECTIVE;
+ camera->maxZoom = (5 * e->map->rows) + 10;
+ const float startingZoom = max((camera->maxZoom + MIN_CAMERA_HEIGHT) / 2.0f, MIN_CAMERA_HEIGHT);
+ camera->camera3D.position = (Vector3){.x = 0.0f, .y = startingZoom, .z = 0.0f};
+ camera->camera3D.target = (Vector3){.x = 0.0f, .y = 0.0f, .z = 0.0f};
+ camera->camera3D.fovy = 45.0f;
+
+ camera->camera2D.target.x = 0.0f;
+ camera->camera2D.target.y = 0.0f;
+ setCamera2DZoom(e);
+ }
+}
+
+Rectangle calculatePlayersBoundingBox(const iwEnv *e) {
+ float minX = FLT_MAX;
+ float minY = FLT_MAX;
+ float maxX = -FLT_MAX;
+ float maxY = -FLT_MAX;
+
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ const droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead && drone->livesLeft == 0 && !drone->diedThisStep) {
+ continue;
+ }
+
+ minX = min(drone->pos.x, minX);
+ minY = min(drone->pos.y, minY);
+ maxX = max(drone->pos.x, maxX);
+ maxY = max(drone->pos.y, maxY);
+ }
+
+ Rectangle bounds = {
+ .x = (minX + maxX) / 2.0f,
+ .y = (minY + maxY) / 2.0f,
+ .width = (maxX - minX) + BOUNDS_PADDING,
+ .height = (maxY - minY) + BOUNDS_PADDING,
+ };
+ return bounds;
+}
+
+float calculateZoom(const iwEnv *e, Rectangle droneBounds) {
+ float boundsZoom = (droneBounds.width + droneBounds.height) * 0.5f;
+ boundsZoom += max(droneBounds.width, droneBounds.height) * 0.5f;
+
+ float fov = tanf((e->client->camera->camera3D.fovy * DEG2RAD) / 2.0f);
+ float zoom = boundsZoom / fov;
+ zoom = (MIN_CAMERA_HEIGHT + zoom) * 0.45f;
+ zoom = Clamp(zoom, MIN_CAMERA_HEIGHT, e->client->camera->maxZoom);
+
+ return zoom;
+}
+
+void updateCamera(iwEnv *e) {
+ gameCamera *camera = e->client->camera;
+
+ if (IsKeyPressed(KEY_TAB)) {
+ camera->orthographic = !camera->orthographic;
+ setupEnvCamera(e);
+ }
+ if (camera->orthographic) {
+ return;
+ }
+
+ const Rectangle droneBounds = calculatePlayersBoundingBox(e);
+ if (droneBounds.width == 0 && droneBounds.height == 0) {
+ return;
+ }
+
+ // smoothly move towards the center of players
+ const Vector2 centerPoint = {.x = droneBounds.x, .y = droneBounds.y};
+ camera->targetPos = Vector2Lerp(camera->targetPos, centerPoint, PAN_SPEED);
+ camera->targetPos.x = Clamp(camera->targetPos.x, e->map->bounds.min.x + MAP_MAX_X_OFFSET, e->map->bounds.max.x - MAP_MAX_X_OFFSET);
+ camera->targetPos.y = Clamp(camera->targetPos.y, e->map->bounds.min.y + MAP_MAX_Y_OFFSET, e->map->bounds.max.y - MAP_MAX_Y_OFFSET);
+
+ camera->camera3D.target.x = camera->targetPos.x;
+ camera->camera3D.target.z = camera->targetPos.y;
+
+ camera->camera3D.position.x = camera->targetPos.x;
+ camera->camera3D.position.z = camera->targetPos.y;
+
+ // smoothly zoom in or out
+ const float zoom = calculateZoom(e, droneBounds);
+ camera->camera3D.position.y = Lerp(camera->camera3D.position.y, zoom, ZOOM_SPEED);
+ setCamera2DZoom(e);
+
+ // update 2D camera
+ camera->camera2D.target = camera->targetPos;
+}
+
+Color getDroneColor(const uint8_t droneIdx) {
+ switch (droneIdx) {
+ case 0:
+ return barolo;
+ case 1:
+ return PUFF_GREEN;
+ case 2:
+ return PUFF_CYAN;
+ case 3:
+ return PUFF_YELLOW;
+ default:
+ ERRORF("unsupported number of drones %d", droneIdx + 1);
+ }
+}
+
+char *getWeaponAbreviation(const enum weaponType type) {
+ char *name = "";
+ switch (type) {
+ case MACHINEGUN_WEAPON:
+ name = "MCGN";
+ break;
+ case SNIPER_WEAPON:
+ // TODO: rename to railgun everywhere
+ name = "RAIL";
+ break;
+ case SHOTGUN_WEAPON:
+ name = "SHGN";
+ break;
+ case IMPLODER_WEAPON:
+ name = "IMPL";
+ break;
+ case ACCELERATOR_WEAPON:
+ name = "ACCL";
+ break;
+ case FLAK_CANNON_WEAPON:
+ name = "FLAK";
+ break;
+ case MINE_LAUNCHER_WEAPON:
+ name = "MINE";
+ break;
+ case BLACK_HOLE_WEAPON:
+ name = "BLKH";
+ break;
+ case NUKE_WEAPON:
+ name = "NUKE";
+ break;
+ default:
+ ERRORF("unknown weapon pickup type %d", type);
+ }
+ return name;
+}
+
+char *getWeaponName(const enum weaponType type) {
+ char *name = "";
+ switch (type) {
+ case STANDARD_WEAPON:
+ name = "Standard";
+ break;
+ case MACHINEGUN_WEAPON:
+ name = "Machine Gun";
+ break;
+ case SNIPER_WEAPON:
+ name = "Railgun";
+ break;
+ case SHOTGUN_WEAPON:
+ name = "Shotgun";
+ break;
+ case IMPLODER_WEAPON:
+ name = "Imploder";
+ break;
+ case ACCELERATOR_WEAPON:
+ name = "Accelerator";
+ break;
+ case FLAK_CANNON_WEAPON:
+ name = "Flak Cannon";
+ break;
+ case MINE_LAUNCHER_WEAPON:
+ name = "Mine Launcher";
+ break;
+ case BLACK_HOLE_WEAPON:
+ name = "Black Hole";
+ break;
+ case NUKE_WEAPON:
+ name = "Tactical Nuke";
+ break;
+ default:
+ ERRORF("unknown weapon pickup type %d", type);
+ }
+ return name;
+}
+
+float getWeaponAimGuideWidth(const enum weaponType type) {
+ switch (type) {
+ case STANDARD_WEAPON:
+ case IMPLODER_WEAPON:
+ case ACCELERATOR_WEAPON:
+ case NUKE_WEAPON:
+ return 5.0f;
+ case FLAK_CANNON_WEAPON:
+ case BLACK_HOLE_WEAPON:
+ return 7.5f;
+ case MACHINEGUN_WEAPON:
+ case MINE_LAUNCHER_WEAPON:
+ return 10.0f;
+ case SNIPER_WEAPON:
+ return 150.0f;
+ case SHOTGUN_WEAPON:
+ return 3.0f;
+ default:
+ ERRORF("unknown weapon when getting aim guide width %d", type);
+ }
+}
+
+Color getProjectileColor(const enum weaponType type) {
+ Color color;
+ switch (type) {
+ case STANDARD_WEAPON:
+ color = PURPLE;
+ break;
+ case IMPLODER_WEAPON:
+ case ACCELERATOR_WEAPON:
+ color = DARKBLUE;
+ break;
+ case FLAK_CANNON_WEAPON:
+ color = MAROON;
+ break;
+ case MACHINEGUN_WEAPON:
+ case SNIPER_WEAPON:
+ case SHOTGUN_WEAPON:
+ color = ORANGE;
+ break;
+ case MINE_LAUNCHER_WEAPON:
+ color = BROWN;
+ break;
+ case BLACK_HOLE_WEAPON:
+ color = DARKGRAY;
+ break;
+ case NUKE_WEAPON:
+ color = MAROON;
+ break;
+ default:
+ ERRORF("unknown weapon when getting projectile color %d", type);
+ }
+
+ color.r *= 0.5f;
+ color.g *= 0.5f;
+ color.b *= 0.5f;
+ return color;
+}
+
+void DrawCubeTexture(Texture2D texture, Vector3 position, float width, float height, float length, Color color) {
+ float x = position.x;
+ float y = position.y;
+ float z = position.z;
+
+ // Set desired texture to be enabled while drawing following vertex data
+ rlSetTexture(texture.id);
+
+ rlBegin(RL_QUADS);
+ rlColor4ub(color.r, color.g, color.b, color.a);
+ // Front Face
+ rlNormal3f(0.0f, 0.0f, 1.0f); // Normal Pointing Towards Viewer
+ rlTexCoord2f(0.0f, 0.0f);
+ rlVertex3f(x - width / 2, y - height / 2, z + length / 2); // Bottom Left Of The Texture and Quad
+ rlTexCoord2f(1.0f, 0.0f);
+ rlVertex3f(x + width / 2, y - height / 2, z + length / 2); // Bottom Right Of The Texture and Quad
+ rlTexCoord2f(1.0f, 1.0f);
+ rlVertex3f(x + width / 2, y + height / 2, z + length / 2); // Top Right Of The Texture and Quad
+ rlTexCoord2f(0.0f, 1.0f);
+ rlVertex3f(x - width / 2, y + height / 2, z + length / 2); // Top Left Of The Texture and Quad
+ // Back Face
+ rlNormal3f(0.0f, 0.0f, -1.0f); // Normal Pointing Away From Viewer
+ rlTexCoord2f(1.0f, 0.0f);
+ rlVertex3f(x - width / 2, y - height / 2, z - length / 2); // Bottom Right Of The Texture and Quad
+ rlTexCoord2f(1.0f, 1.0f);
+ rlVertex3f(x - width / 2, y + height / 2, z - length / 2); // Top Right Of The Texture and Quad
+ rlTexCoord2f(0.0f, 1.0f);
+ rlVertex3f(x + width / 2, y + height / 2, z - length / 2); // Top Left Of The Texture and Quad
+ rlTexCoord2f(0.0f, 0.0f);
+ rlVertex3f(x + width / 2, y - height / 2, z - length / 2); // Bottom Left Of The Texture and Quad
+ // Top Face
+ rlNormal3f(0.0f, 1.0f, 0.0f); // Normal Pointing Up
+ rlTexCoord2f(0.0f, 1.0f);
+ rlVertex3f(x - width / 2, y + height / 2, z - length / 2); // Top Left Of The Texture and Quad
+ rlTexCoord2f(0.0f, 0.0f);
+ rlVertex3f(x - width / 2, y + height / 2, z + length / 2); // Bottom Left Of The Texture and Quad
+ rlTexCoord2f(1.0f, 0.0f);
+ rlVertex3f(x + width / 2, y + height / 2, z + length / 2); // Bottom Right Of The Texture and Quad
+ rlTexCoord2f(1.0f, 1.0f);
+ rlVertex3f(x + width / 2, y + height / 2, z - length / 2); // Top Right Of The Texture and Quad
+ // Bottom Face
+ rlNormal3f(0.0f, -1.0f, 0.0f); // Normal Pointing Down
+ rlTexCoord2f(1.0f, 1.0f);
+ rlVertex3f(x - width / 2, y - height / 2, z - length / 2); // Top Right Of The Texture and Quad
+ rlTexCoord2f(0.0f, 1.0f);
+ rlVertex3f(x + width / 2, y - height / 2, z - length / 2); // Top Left Of The Texture and Quad
+ rlTexCoord2f(0.0f, 0.0f);
+ rlVertex3f(x + width / 2, y - height / 2, z + length / 2); // Bottom Left Of The Texture and Quad
+ rlTexCoord2f(1.0f, 0.0f);
+ rlVertex3f(x - width / 2, y - height / 2, z + length / 2); // Bottom Right Of The Texture and Quad
+ // Right face
+ rlNormal3f(1.0f, 0.0f, 0.0f); // Normal Pointing Right
+ rlTexCoord2f(1.0f, 0.0f);
+ rlVertex3f(x + width / 2, y - height / 2, z - length / 2); // Bottom Right Of The Texture and Quad
+ rlTexCoord2f(1.0f, 1.0f);
+ rlVertex3f(x + width / 2, y + height / 2, z - length / 2); // Top Right Of The Texture and Quad
+ rlTexCoord2f(0.0f, 1.0f);
+ rlVertex3f(x + width / 2, y + height / 2, z + length / 2); // Top Left Of The Texture and Quad
+ rlTexCoord2f(0.0f, 0.0f);
+ rlVertex3f(x + width / 2, y - height / 2, z + length / 2); // Bottom Left Of The Texture and Quad
+ // Left Face
+ rlNormal3f(-1.0f, 0.0f, 0.0f); // Normal Pointing Left
+ rlTexCoord2f(0.0f, 0.0f);
+ rlVertex3f(x - width / 2, y - height / 2, z - length / 2); // Bottom Left Of The Texture and Quad
+ rlTexCoord2f(1.0f, 0.0f);
+ rlVertex3f(x - width / 2, y - height / 2, z + length / 2); // Bottom Right Of The Texture and Quad
+ rlTexCoord2f(1.0f, 1.0f);
+ rlVertex3f(x - width / 2, y + height / 2, z + length / 2); // Top Right Of The Texture and Quad
+ rlTexCoord2f(0.0f, 1.0f);
+ rlVertex3f(x - width / 2, y + height / 2, z - length / 2); // Top Left Of The Texture and Quad
+ rlEnd();
+ // rlPopMatrix();
+
+ rlSetTexture(0);
+}
+
+// Draw cube with texture piece applied to all faces
+void DrawCubeTextureRec(Texture2D texture, Rectangle source, Vector3 position, float width, float height, float length, Color color) {
+ float x = position.x;
+ float y = position.y;
+ float z = position.z;
+ float texWidth = (float)texture.width;
+ float texHeight = (float)texture.height;
+
+ // Set desired texture to be enabled while drawing following vertex data
+ rlSetTexture(texture.id);
+
+ // We calculate the normalized texture coordinates for the desired texture-source-rectangle
+ // It means converting from (tex.width, tex.height) coordinates to [0.0f, 1.0f] equivalent
+ rlBegin(RL_QUADS);
+ rlColor4ub(color.r, color.g, color.b, color.a);
+
+ // Front face
+ rlNormal3f(0.0f, 0.0f, 1.0f);
+ rlTexCoord2f(source.x / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x - width / 2, y - height / 2, z + length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x + width / 2, y - height / 2, z + length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, source.y / texHeight);
+ rlVertex3f(x + width / 2, y + height / 2, z + length / 2);
+ rlTexCoord2f(source.x / texWidth, source.y / texHeight);
+ rlVertex3f(x - width / 2, y + height / 2, z + length / 2);
+
+ // Back face
+ rlNormal3f(0.0f, 0.0f, -1.0f);
+ rlTexCoord2f((source.x + source.width) / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x - width / 2, y - height / 2, z - length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, source.y / texHeight);
+ rlVertex3f(x - width / 2, y + height / 2, z - length / 2);
+ rlTexCoord2f(source.x / texWidth, source.y / texHeight);
+ rlVertex3f(x + width / 2, y + height / 2, z - length / 2);
+ rlTexCoord2f(source.x / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x + width / 2, y - height / 2, z - length / 2);
+
+ // Top face
+ rlNormal3f(0.0f, 1.0f, 0.0f);
+ rlTexCoord2f(source.x / texWidth, source.y / texHeight);
+ rlVertex3f(x - width / 2, y + height / 2, z - length / 2);
+ rlTexCoord2f(source.x / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x - width / 2, y + height / 2, z + length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x + width / 2, y + height / 2, z + length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, source.y / texHeight);
+ rlVertex3f(x + width / 2, y + height / 2, z - length / 2);
+
+ // Bottom face
+ rlNormal3f(0.0f, -1.0f, 0.0f);
+ rlTexCoord2f((source.x + source.width) / texWidth, source.y / texHeight);
+ rlVertex3f(x - width / 2, y - height / 2, z - length / 2);
+ rlTexCoord2f(source.x / texWidth, source.y / texHeight);
+ rlVertex3f(x + width / 2, y - height / 2, z - length / 2);
+ rlTexCoord2f(source.x / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x + width / 2, y - height / 2, z + length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x - width / 2, y - height / 2, z + length / 2);
+
+ // Right face
+ rlNormal3f(1.0f, 0.0f, 0.0f);
+ rlTexCoord2f((source.x + source.width) / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x + width / 2, y - height / 2, z - length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, source.y / texHeight);
+ rlVertex3f(x + width / 2, y + height / 2, z - length / 2);
+ rlTexCoord2f(source.x / texWidth, source.y / texHeight);
+ rlVertex3f(x + width / 2, y + height / 2, z + length / 2);
+ rlTexCoord2f(source.x / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x + width / 2, y - height / 2, z + length / 2);
+
+ // Left face
+ rlNormal3f(-1.0f, 0.0f, 0.0f);
+ rlTexCoord2f(source.x / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x - width / 2, y - height / 2, z - length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, (source.y + source.height) / texHeight);
+ rlVertex3f(x - width / 2, y - height / 2, z + length / 2);
+ rlTexCoord2f((source.x + source.width) / texWidth, source.y / texHeight);
+ rlVertex3f(x - width / 2, y + height / 2, z + length / 2);
+ rlTexCoord2f(source.x / texWidth, source.y / texHeight);
+ rlVertex3f(x - width / 2, y + height / 2, z - length / 2);
+
+ rlEnd();
+
+ rlSetTexture(0);
+}
+
+static void DrawTextCodepoint3D(Font font, int codepoint, Vector3 position, float fontSize, bool backface, Color tint) {
+ // Character index position in sprite font
+ // NOTE: In case a codepoint is not available in the font, index returned points to '?'
+ int index = GetGlyphIndex(font, codepoint);
+ float scale = fontSize / (float)font.baseSize;
+
+ // Character destination rectangle on screen
+ // NOTE: We consider charsPadding on drawing
+ position.x += (float)(font.glyphs[index].offsetX - font.glyphPadding) / (float)font.baseSize * scale;
+ position.z += (float)(font.glyphs[index].offsetY - font.glyphPadding) / (float)font.baseSize * scale;
+
+ // Character source rectangle from font texture atlas
+ // NOTE: We consider chars padding when drawing, it could be required for outline/glow shader effects
+ Rectangle srcRec = {
+ .x = font.recs[index].x - (float)font.glyphPadding,
+ .y = font.recs[index].y - (float)font.glyphPadding,
+ .width = font.recs[index].width + 2.0f * font.glyphPadding,
+ .height = font.recs[index].height + 2.0f * font.glyphPadding,
+ };
+
+ float width = (float)(font.recs[index].width + 2.0f * font.glyphPadding) / (float)font.baseSize * scale;
+ float height = (float)(font.recs[index].height + 2.0f * font.glyphPadding) / (float)font.baseSize * scale;
+
+ if (font.texture.id > 0) {
+ const float x = 0.0f;
+ const float y = 0.0f;
+ const float z = 0.0f;
+
+ // normalized texture coordinates of the glyph inside the font texture (0.0f -> 1.0f)
+ const float tx = srcRec.x / font.texture.width;
+ const float ty = srcRec.y / font.texture.height;
+ const float tw = (srcRec.x + srcRec.width) / font.texture.width;
+ const float th = (srcRec.y + srcRec.height) / font.texture.height;
+
+ if (SHOW_LETTER_BOUNDRY) {
+ DrawCubeWiresV((Vector3){position.x + width / 2, position.y, position.z + height / 2}, (Vector3){width, LETTER_BOUNDRY_SIZE, height}, LETTER_BOUNDRY_COLOR);
+ }
+
+ rlCheckRenderBatchLimit(4 + 4 * backface);
+ rlSetTexture(font.texture.id);
+
+ rlPushMatrix();
+ rlTranslatef(position.x, position.y, position.z);
+
+ rlBegin(RL_QUADS);
+ rlColor4ub(tint.r, tint.g, tint.b, tint.a);
+
+ // Front Face
+ rlNormal3f(0.0f, 1.0f, 0.0f); // Normal Pointing Up
+ rlTexCoord2f(tx, ty);
+ rlVertex3f(x, y, z); // Top Left Of The Texture and Quad
+ rlTexCoord2f(tx, th);
+ rlVertex3f(x, y, z + height); // Bottom Left Of The Texture and Quad
+ rlTexCoord2f(tw, th);
+ rlVertex3f(x + width, y, z + height); // Bottom Right Of The Texture and Quad
+ rlTexCoord2f(tw, ty);
+ rlVertex3f(x + width, y, z); // Top Right Of The Texture and Quad
+
+ if (backface) {
+ // Back Face
+ rlNormal3f(0.0f, -1.0f, 0.0f); // Normal Pointing Down
+ rlTexCoord2f(tx, ty);
+ rlVertex3f(x, y, z); // Top Right Of The Texture and Quad
+ rlTexCoord2f(tw, ty);
+ rlVertex3f(x + width, y, z); // Top Left Of The Texture and Quad
+ rlTexCoord2f(tw, th);
+ rlVertex3f(x + width, y, z + height); // Bottom Left Of The Texture and Quad
+ rlTexCoord2f(tx, th);
+ rlVertex3f(x, y, z + height); // Bottom Right Of The Texture and Quad
+ }
+ rlEnd();
+ rlPopMatrix();
+
+ rlSetTexture(0);
+ }
+}
+
+static void DrawText3D(Font font, const char *text, Vector3 position, float fontSize, float fontSpacing, float lineSpacing, bool backface, Color tint) {
+ int length = TextLength(text); // Total length in bytes of the text, scanned by codepoints in loop
+
+ float textOffsetY = 0.0f; // Offset between lines (on line break '\n')
+ float textOffsetX = 0.0f; // Offset X to next character to draw
+
+ const float scale = fontSize / (float)font.baseSize;
+
+ for (int i = 0; i < length;) {
+ // Get next codepoint from byte string and glyph index in font
+ int codepointByteCount = 0;
+ int codepoint = GetCodepoint(&text[i], &codepointByteCount);
+ int index = GetGlyphIndex(font, codepoint);
+
+ // NOTE: Normally we exit the decoding sequence as soon as a bad byte is found (and return 0x3f)
+ // but we need to draw all of the bad bytes using the '?' symbol moving one byte
+ if (codepoint == 0x3f) {
+ codepointByteCount = 1;
+ }
+
+ if (codepoint == '\n') {
+ // NOTE: Fixed line spacing of 1.5 line-height
+ // TODO: Support custom line spacing defined by user
+ textOffsetY += scale + lineSpacing / (float)font.baseSize * scale;
+ textOffsetX = 0.0f;
+ } else {
+ if ((codepoint != ' ') && (codepoint != '\t')) {
+ DrawTextCodepoint3D(font, codepoint, (Vector3){position.x + textOffsetX, position.y, position.z + textOffsetY}, fontSize, backface, tint);
+ }
+
+ if (font.glyphs[index].advanceX == 0) {
+ textOffsetX += (float)(font.recs[index].width + fontSpacing) / (float)font.baseSize * scale;
+ } else {
+ textOffsetX += (float)(font.glyphs[index].advanceX + fontSpacing) / (float)font.baseSize * scale;
+ }
+ }
+
+ i += codepointByteCount; // Move text bytes counter to next codepoint
+ }
+}
+
+void renderTimer(const iwEnv *e, const char *timerStr, const Color color) {
+ int fontSize = 2.5 * e->client->scale;
+ int textWidth = MeasureText(timerStr, fontSize);
+ int posX = (e->client->width - textWidth) / 2;
+ DrawText(timerStr, posX, e->client->scale, fontSize, color);
+}
+
+void renderUI(const iwEnv *e, const bool starting) {
+ // render drone info
+ const uint8_t fontSize = 2 * e->client->scale;
+ const uint8_t xMargin = 5 * e->client->scale;
+ const uint8_t yMargin = 12 * e->client->scale;
+
+ for (int i = 0; i < e->numDrones; i++) {
+ const droneEntity *drone = safe_array_get_at(e->drones, i);
+
+ const char *droneNum = TextFormat("Drone %d", drone->idx + 1);
+ const Vector2 textSize = MeasureTextEx(GetFontDefault(), droneNum, fontSize, fontSize / 10);
+ const uint16_t lineWidth = textSize.x + (3 * (e->client->scale * 2.5f));
+
+ uint16_t x = 0;
+ uint16_t y = 0;
+ switch (drone->idx) {
+ case 0:
+ x = xMargin;
+ y = yMargin;
+ break;
+ case 1:
+ x = e->client->width - lineWidth - xMargin;
+ y = yMargin;
+ break;
+ case 2:
+ x = xMargin;
+ y = e->client->height - yMargin - (6 * e->client->scale);
+ break;
+ case 3:
+ x = e->client->width - lineWidth - xMargin;
+ y = e->client->height - yMargin - (6 * e->client->scale);
+ break;
+ }
+
+ Color textColor = PUFF_WHITE;
+ if (drone->livesLeft == 0) {
+ textColor = Fade(PUFF_WHITE, 0.5f);
+ }
+ DrawText(droneNum, x, y, fontSize, textColor);
+
+ uint16_t lifeX = x + textSize.x;
+ uint16_t lifeY = y + (textSize.y / 2);
+ const Color droneColor = getDroneColor(drone->idx);
+ for (uint8_t i = 0; i < drone->livesLeft; i++) {
+ lifeX += e->client->scale * 2.5f;
+ DrawCircleLines(lifeX, lifeY, e->client->scale, droneColor);
+ }
+
+ y += textSize.y + e->client->scale;
+ DrawLine(x, y, x + textSize.x, y, droneColor);
+
+ y += e->client->scale;
+ const char *weaponName = getWeaponName(drone->weaponInfo->type);
+ DrawText(weaponName, x, y, fontSize, textColor);
+
+ y += textSize.y + e->client->scale;
+ DrawLine(x, y, x + MeasureText(weaponName, fontSize), y, droneColor);
+
+ y += e->client->scale;
+ char *playerType = "";
+ if (droneControlledByHuman(e, drone->idx)) {
+ playerType = "Human";
+ } else if (drone->idx < e->numAgents) {
+ playerType = "NN";
+ } else {
+ if (e->sittingDuck) {
+ playerType = "Sitting Duck";
+ } else {
+ playerType = "Scripted";
+ }
+ }
+ char *droneInfo;
+ if (e->teamsEnabled) {
+ droneInfo = (char *)TextFormat("%s | Team %d", playerType, drone->team + 1);
+ } else {
+ droneInfo = playerType;
+ }
+
+ DrawText(droneInfo, x, y, fontSize, textColor);
+
+ if (drone->killedBy != -1) {
+ y += textSize.y + e->client->scale;
+ const char *killedBy = TextFormat("Killed by Player %d", drone->killedBy + 1);
+
+ DrawText(killedBy, x, y, fontSize, getDroneColor(drone->killedBy));
+ }
+ }
+
+ // render timer
+ if (starting) {
+ renderTimer(e, "READY", PUFF_WHITE);
+ return;
+ } else if (e->stepsLeft > (ROUND_STEPS - 1) * e->frameRate) {
+ renderTimer(e, "GO!", PUFF_WHITE);
+ return;
+ } else if (e->stepsLeft == 0) {
+ renderTimer(e, "SUDDEN DEATH", PUFF_WHITE);
+ return;
+ }
+
+ char *timerStr;
+ if (e->stepsLeft >= 10 * e->frameRate) {
+ timerStr = (char *)TextFormat("%d", (uint16_t)(e->stepsLeft / e->frameRate));
+ } else {
+ timerStr = (char *)TextFormat("0%d", (uint16_t)(e->stepsLeft / e->frameRate));
+ }
+ renderTimer(e, timerStr, PUFF_WHITE);
+}
+
+// TODO: track when trails begine and end (ie when respawning)
+void renderBrakeTrails(iwEnv *e, const droneEntity *drone) {
+ const float maxLifetime = 3.0f * e->frameRate;
+ const float maxAlpha = 0.33f;
+ const float trailWidth = 0.33f;
+
+ // update lifetimes and prune expired points
+ CC_ArrayIter iter;
+ cc_array_iter_init(&iter, drone->brakeTrailPoints);
+ brakeTrailPoint *pt;
+ while (cc_array_iter_next(&iter, (void **)&pt) != CC_ITER_END) {
+ if (pt->lifetime == UINT16_MAX) {
+ pt->lifetime = maxLifetime;
+ } else if (pt->lifetime == 0) {
+ fastFree(pt);
+ cc_array_iter_remove(&iter, NULL);
+ continue;
+ } else {
+ pt->lifetime--;
+ }
+ }
+
+ size_t count = cc_array_size(drone->brakeTrailPoints);
+ if (count < 2) {
+ return;
+ }
+
+ for (size_t i = 0; i + 1 < count; i++) {
+ const brakeTrailPoint *trailPoint0 = safe_array_get_at(drone->brakeTrailPoints, i);
+ if (trailPoint0->isEnd) {
+ continue;
+ }
+ const brakeTrailPoint *trailPoint1 = safe_array_get_at(drone->brakeTrailPoints, i + 1);
+ const Vector2 p0 = (Vector2){.x = trailPoint0->pos.x, .y = trailPoint0->pos.y};
+ const Vector2 p1 = (Vector2){.x = trailPoint1->pos.x, .y = trailPoint1->pos.y};
+
+ // compute direction and a perpendicular vector
+ Vector2 segment = Vector2Subtract(p1, p0);
+ if (Vector2Length(segment) == 0) {
+ continue;
+ }
+ segment = Vector2Normalize(segment);
+ const Vector2 perp = {-segment.y, segment.x};
+
+ // compute four vertices for the quad segment
+ const Vector2 v0 = Vector2Add(p0, Vector2Scale(perp, trailWidth));
+ const Vector2 v1 = Vector2Subtract(p0, Vector2Scale(perp, trailWidth));
+ const Vector2 v2 = Vector2Add(p1, Vector2Scale(perp, trailWidth));
+ const Vector2 v3 = Vector2Subtract(p1, Vector2Scale(perp, trailWidth));
+
+ // draw the quad as two triangles
+ const float alpha0 = maxAlpha * (trailPoint0->lifetime / maxLifetime);
+ const float alpha1 = maxAlpha * (trailPoint1->lifetime / maxLifetime);
+ DrawTriangle3D(
+ (Vector3){.x = v0.x, .y = 0.0f, .z = v0.y},
+ (Vector3){.x = v2.x, .y = 0.0f, .z = v2.y},
+ (Vector3){.x = v1.x, .y = 0.0f, .z = v1.y},
+ Fade(GRAY, alpha0)
+ );
+ DrawTriangle3D(
+ (Vector3){.x = v1.x, .y = 0.0f, .z = v1.y},
+ (Vector3){.x = v2.x, .y = 0.0f, .z = v2.y},
+ (Vector3){.x = v3.x, .y = 0.0f, .z = v3.y},
+ Fade(GRAY, alpha1)
+ );
+ }
+}
+
+// TODO: make 2D circles
+void renderExplosions(const iwEnv *e) {
+ const uint16_t maxRenderSteps = EXPLOSION_TIME * e->frameRate;
+
+ CC_ArrayIter iter;
+ cc_array_iter_init(&iter, e->explosions);
+ explosionInfo *explosion;
+
+ while (cc_array_iter_next(&iter, (void **)&explosion) != CC_ITER_END) {
+ if (explosion->renderSteps == UINT16_MAX) {
+ explosion->renderSteps = maxRenderSteps;
+ } else if (explosion->renderSteps == 0) {
+ fastFree(explosion);
+ cc_array_iter_remove(&iter, NULL);
+ continue;
+ }
+
+ // color bursts with a bit of the parent drone's color
+ const float alpha = (float)explosion->renderSteps / maxRenderSteps;
+ BeginBlendMode(BLEND_ALPHA);
+ if (false && explosion->isBurst) {
+ const Color droneColor = Fade(getDroneColor(explosion->droneIdx), alpha);
+ DrawSphereEx(
+ (Vector3){.x = explosion->def.position.x, .y = 0.5f, .z = explosion->def.position.y},
+ explosion->def.radius + explosion->def.falloff,
+ 20,
+ 50,
+ DARKGRAY
+ );
+ DrawSphereEx(
+ (Vector3){.x = explosion->def.position.x, .y = 0.5f, .z = explosion->def.position.y},
+ explosion->def.radius,
+ 20,
+ 50,
+ droneColor
+ );
+ } else {
+ const Color falloffColor = Fade(GRAY, alpha);
+ const Color explosionColor = Fade(RAYWHITE, alpha);
+
+ DrawSphereEx(
+ (Vector3){.x = explosion->def.position.x, .y = 0.5f, .z = explosion->def.position.y},
+ explosion->def.radius + explosion->def.falloff,
+ 20,
+ 50,
+ falloffColor
+ );
+ DrawSphereEx(
+ (Vector3){.x = explosion->def.position.x, .y = 0.5f, .z = explosion->def.position.y},
+ explosion->def.radius,
+ 20,
+ 50,
+ explosionColor
+ );
+ }
+ EndBlendMode();
+
+ explosion->renderSteps = max(explosion->renderSteps - 1, 0);
+ }
+}
+
+// TODO: add bloom lines at drone level
+void renderWall(const iwEnv *e, const wallEntity *wall) {
+ Color color = {0};
+ Rectangle textureRec;
+ switch (wall->type) {
+ case STANDARD_WALL_ENTITY:
+ color = PUFF_CYAN;
+ textureRec = (Rectangle){
+ 0.0f,
+ 0.0f,
+ e->client->wallTexture.width / 2.0f,
+ e->client->wallTexture.height / 2.0f,
+ };
+ break;
+ case BOUNCY_WALL_ENTITY:
+ color = PUFF_YELLOW;
+ textureRec = (Rectangle){
+ 0.0f,
+ e->client->wallTexture.height / 2.0f,
+ e->client->wallTexture.width / 2.0f,
+ e->client->wallTexture.height / 2.0f,
+ };
+ break;
+ case DEATH_WALL_ENTITY:
+ color = PUFF_RED;
+ textureRec = (Rectangle){
+ e->client->wallTexture.width / 2.0f,
+ 0.0f,
+ e->client->wallTexture.width / 2.0f,
+ e->client->wallTexture.height / 2.0f,
+ };
+ break;
+ default:
+ ERRORF("unknown wall type %d", wall->type);
+ }
+
+ float angle = 0.0f;
+ if (wall->isFloating) {
+ angle = b2Rot_GetAngle(wall->rot);
+ angle *= RAD2DEG;
+ }
+
+ float y;
+ float y_size;
+ if (wall->isFloating) {
+ y = (FLOATING_WALL_THICKNESS / 2.0f) - 1.0f;
+ y_size = FLOATING_WALL_THICKNESS;
+ } else {
+ y = (WALL_THICKNESS / 2.0f) - 1.0f;
+ y_size = WALL_THICKNESS;
+ }
+
+ float x = 2.0f * wall->extent.x;
+ float z = 2.0f * wall->extent.y;
+
+ rlPushMatrix();
+ rlTranslatef(wall->pos.x, 0.0f, wall->pos.y);
+ rlRotatef(-angle, 0.0f, 1.0f, 0.0f);
+
+ DrawCubeTextureRec(
+ e->client->wallTexture,
+ textureRec,
+ (Vector3){.x = 0.0f, .y = y, .z = 0.0f},
+ x,
+ y_size,
+ z,
+ color
+ );
+
+ if (!wall->isFloating) {
+ for (uint8_t i = 0; i < 3; i++) {
+ y -= WALL_THICKNESS;
+ DrawCubeTextureRec(
+ e->client->wallTexture,
+ textureRec,
+ (Vector3){.x = 0.0f, .y = y, .z = 0.0f},
+ x,
+ y_size,
+ z,
+ color
+ );
+ }
+ }
+
+ rlPopMatrix();
+}
+
+void renderWeaponPickup(const iwEnv *e, const weaponPickupEntity *pickup) {
+ if (pickup->respawnWait != 0.0f || pickup->floatingWallsTouching != 0) {
+ return;
+ }
+ Rectangle textureRec = (Rectangle){
+ e->client->wallTexture.width / 2.0f,
+ e->client->wallTexture.height / 2.0f,
+ e->client->wallTexture.width / 2.0f,
+ e->client->wallTexture.height / 2.0f,
+ };
+
+ DrawCubeTextureRec(
+ e->client->wallTexture,
+ textureRec,
+ (Vector3){.x = pickup->pos.x, .y = 0.5f, .z = pickup->pos.y},
+ PICKUP_THICKNESS,
+ 0.0f,
+ PICKUP_THICKNESS,
+ WHITE
+ );
+
+ const char *weaponName = getWeaponAbreviation(pickup->weapon);
+ Vector3 textPos = (Vector3){
+ .x = pickup->pos.x - PICKUP_THICKNESS / 2.0f,
+ .y = 1.0f,
+ .z = pickup->pos.y - PICKUP_THICKNESS / 2.0f
+ };
+
+ DrawText3D(GetFontDefault(), weaponName, textPos, 12, 0.5f, -1.0f, false, PUFF_WHITE);
+}
+
+void renderDronePieces(iwEnv *e) {
+ const float maxLifetime = e->frameRate * DRONE_PIECE_LIFETIME;
+
+ CC_ArrayIter iter;
+ cc_array_iter_init(&iter, e->dronePieces);
+ dronePieceEntity *piece;
+
+ while (cc_array_iter_next(&iter, (void **)&piece) != CC_ITER_END) {
+ if (piece->lifetime == UINT16_MAX) {
+ piece->lifetime = maxLifetime;
+ }
+
+ float baseAlpha = 1.0f;
+ if (piece->isShieldPiece) {
+ baseAlpha = 0.5f;
+ }
+ const float alpha = 1.0f - (baseAlpha * ((float)piece->lifetime / maxLifetime));
+ const float finalAlpha = 1.0f - (SQUARED(alpha) * alpha);
+ const Color color = Fade(getDroneColor(piece->droneIdx), finalAlpha);
+ const float angle = RAD2DEG * b2Rot_GetAngle(piece->rot);
+
+ // Draw edges of the triangle
+ rlPushMatrix();
+ rlTranslatef(piece->pos.x, 0.0f, piece->pos.y);
+ rlRotatef(-angle, 0.0f, 1.0f, 0.0f);
+
+ rlBegin(RL_LINES);
+ rlColor4ub(color.r, color.g, color.b, color.a);
+
+ rlVertex3f(piece->vertices[0].x, 0.5f, piece->vertices[0].y);
+ rlVertex3f(piece->vertices[1].x, 0.5f, piece->vertices[1].y);
+
+ rlVertex3f(piece->vertices[1].x, 0.5f, piece->vertices[1].y);
+ rlVertex3f(piece->vertices[2].x, 0.5f, piece->vertices[2].y);
+
+ rlVertex3f(piece->vertices[2].x, 0.5f, piece->vertices[2].y);
+ rlVertex3f(piece->vertices[0].x, 0.5f, piece->vertices[0].y);
+
+ rlEnd();
+
+ rlPopMatrix();
+
+ piece->lifetime--;
+ if (piece->lifetime == 0) {
+ destroyDronePiece(e, piece);
+ cc_array_iter_remove_fast(&iter, NULL);
+ }
+ }
+}
+
+void renderDroneRespawnGuides(const iwEnv *e, droneEntity *drone) {
+ if (drone->respawnGuideLifetime == 0) {
+ return;
+ }
+
+ const float maxLifetime = e->frameRate * (DRONE_RESPAWN_GUIDE_SHRINK_TIME + DRONE_RESPAWN_GUIDE_HOLD_TIME);
+ const uint16_t shrinkTime = e->frameRate * DRONE_RESPAWN_GUIDE_SHRINK_TIME;
+ if (drone->respawnGuideLifetime == UINT16_MAX) {
+ drone->respawnGuideLifetime = maxLifetime;
+ }
+
+ float radius = DRONE_RESPAWN_GUIDE_MIN_RADIUS;
+ if (drone->respawnGuideLifetime >= maxLifetime - shrinkTime) {
+ radius += DRONE_RESPAWN_GUIDE_MAX_RADIUS * ((drone->respawnGuideLifetime - (e->frameRate * DRONE_RESPAWN_GUIDE_HOLD_TIME)) / shrinkTime);
+ }
+
+ Vector2 dronePos = (Vector2){.x = drone->pos.x, .y = drone->pos.y};
+ DrawCircleLinesV(dronePos, radius, getDroneColor(drone->idx));
+
+ drone->respawnGuideLifetime--;
+}
+
+b2RayResult droneAimingAt(const iwEnv *e, const droneEntity *drone) {
+ const b2Vec2 rayEnd = b2MulAdd(drone->pos, 150.0f, drone->lastAim);
+ const b2Vec2 translation = b2Sub(rayEnd, drone->pos);
+ const b2QueryFilter filter = {.categoryBits = PROJECTILE_SHAPE, .maskBits = WALL_SHAPE | FLOATING_WALL_SHAPE | DRONE_SHAPE};
+ return b2World_CastRayClosest(e->worldID, drone->pos, translation, filter);
+}
+
+void renderDroneAimGuide(const iwEnv *e, const droneEntity *drone) {
+ // find length of laser aiming guide by where it touches the nearest shape
+ const b2RayResult rayRes = droneAimingAt(e, drone);
+ ASSERT(b2Shape_IsValid(rayRes.shapeId));
+ const entity *ent = b2Shape_GetUserData(rayRes.shapeId);
+
+ const b2DistanceOutput output = closestPoint(drone->ent, ent);
+ float aimGuideWidth = getWeaponAimGuideWidth(drone->weaponInfo->type);
+ aimGuideWidth = min(aimGuideWidth, output.distance + 0.1f) + (DRONE_RADIUS * 2.0f);
+
+ // render laser aim guide
+ const b2Vec2 pos = b2MulAdd(drone->pos, aimGuideWidth / 2.0f, drone->lastAim);
+ const float aimAngle = RAD2DEG * b2Atan2(drone->lastAim.y, drone->lastAim.x);
+
+ rlPushMatrix();
+ rlTranslatef(pos.x, 0.0f, pos.y);
+ rlRotatef(-aimAngle, 0.0f, 1.0f, 0.0f);
+
+ const Color droneColor = getDroneColor(drone->idx);
+ DrawCube((Vector3){.x = 0.0f, .y = 0.0f, .z = 0.0f}, aimGuideWidth, 0.0f, aimGuideLength, droneColor);
+
+ rlPopMatrix();
+}
+
+void renderDroneGuides(iwEnv *e, const droneEntity *drone, const bool ending) {
+ // render thruster move guide
+ if (!b2VecEqual(drone->lastMove, b2Vec2_zero) && !ending) {
+ const float moveMagnitude = b2Length(drone->lastMove);
+ const float thrusterAngle = RAD2DEG * b2Atan2(-drone->lastMove.y, -drone->lastMove.x);
+ const float flickerWidth = randFloat(&e->randState, -0.05f, 0.05f);
+ const float thrusterWidth = 2.5f * ((halfDroneRadius * moveMagnitude) + halfDroneRadius + flickerWidth);
+ const b2Vec2 thrusterPos = b2MulAdd(drone->pos, -thrusterWidth / 2.0f, drone->lastMove);
+ const Color thrusterColor = Fade(getDroneColor(drone->idx), 0.9);
+
+ rlPushMatrix();
+ rlTranslatef(thrusterPos.x, 0.0f, thrusterPos.y);
+ rlRotatef(-thrusterAngle, 0.0f, 1.0f, 0.0f);
+
+ DrawCube((Vector3){.x = 0.0f, .y = 0.0f, .z = 0.0f}, thrusterWidth, 0.0f, droneThrusterLength, thrusterColor);
+
+ rlPopMatrix();
+ }
+
+ renderDroneAimGuide(e, drone);
+}
+
+void renderDroneTrail(const droneEntity *drone) {
+ if (drone->trailPoints.length < 2) {
+ return;
+ }
+
+ const float trailWidth = DRONE_RADIUS;
+ const float numPoints = drone->trailPoints.length;
+ const Color droneColor = getDroneColor(drone->idx);
+
+ for (uint8_t i = 0; i < drone->trailPoints.length - 1; i++) {
+ const Vector2 p0 = drone->trailPoints.points[i];
+ const Vector2 p1 = drone->trailPoints.points[i + 1];
+
+ // compute direction and a perpendicular vector
+ Vector2 segment = Vector2Subtract(p1, p0);
+ if (Vector2Length(segment) == 0) {
+ continue;
+ }
+ segment = Vector2Normalize(segment);
+ const Vector2 perp = {-segment.y, segment.x};
+
+ // compute four vertices for the quad segment
+ const Vector2 v0 = Vector2Add(p0, Vector2Scale(perp, trailWidth));
+ const Vector2 v1 = Vector2Subtract(p0, Vector2Scale(perp, trailWidth));
+ const Vector2 v2 = Vector2Add(p1, Vector2Scale(perp, trailWidth));
+ const Vector2 v3 = Vector2Subtract(p1, Vector2Scale(perp, trailWidth));
+
+ // draw the quad as two triangles
+ const float alpha0 = 0.7f * ((float)(i + 1) / numPoints);
+ const float alpha1 = 0.7f * ((float)(i + 2) / numPoints);
+ DrawTriangle3D(
+ (Vector3){.x = v0.x, .y = 0.0f, .z = v0.y},
+ (Vector3){.x = v2.x, .y = 0.0f, .z = v2.y},
+ (Vector3){.x = v1.x, .y = 0.0f, .z = v1.y},
+ Fade(droneColor, alpha0)
+ );
+ DrawTriangle3D(
+ (Vector3){.x = v1.x, .y = 0.0f, .z = v1.y},
+ (Vector3){.x = v2.x, .y = 0.0f, .z = v2.y},
+ (Vector3){.x = v3.x, .y = 0.0f, .z = v3.y},
+ Fade(droneColor, alpha1)
+ );
+ }
+}
+
+void renderDroneLight(const droneEntity *drone) {
+ const Color droneColor = getDroneColor(drone->idx);
+
+ DrawCylinder(
+ (Vector3){.x = drone->pos.x, .y = 0.0, .z = drone->pos.y},
+ DRONE_RADIUS,
+ DRONE_RADIUS,
+ 0.0f,
+ 32,
+ droneColor
+ );
+}
+
+void renderDrone(const droneEntity *drone) {
+ renderDroneLight(drone);
+
+ DrawSphere(
+ (Vector3){.x = drone->pos.x, .y = 0.0f, .z = drone->pos.y},
+ DRONE_RADIUS - droneLightRadius,
+ BLACK
+ );
+
+ if (drone->shield != NULL) {
+ DrawSphereWires(
+ (Vector3){.x = drone->shield->pos.x, .y = 0.0f, .z = drone->shield->pos.y},
+ DRONE_SHIELD_RADIUS,
+ 6,
+ 12,
+ Fade(getDroneColor(drone->idx), 0.5f)
+ );
+ }
+}
+
+void renderDroneAmmo(const iwEnv *e, const droneEntity *drone) {
+ Vector2 worldPos = {.x = drone->pos.x, .y = drone->pos.y};
+ Vector2 screenPos = GetWorldToScreen2D(worldPos, e->client->camera->camera2D);
+
+ // draw ammo count
+ const float fontSize = 1.5f * e->client->camera->camera2D.zoom;
+ const char *ammoStr = TextFormat("%d", drone->ammo);
+ const Vector2 textSize = MeasureTextEx(GetFontDefault(), ammoStr, fontSize, fontSize / 10.0f);
+ const Vector2 textOrigin = {.x = screenPos.x - (textSize.x / 2), .y = screenPos.y + (1.3f * textSize.y)};
+ DrawTextEx(GetFontDefault(), ammoStr, textOrigin, fontSize, fontSize / 10.f, RAYWHITE);
+}
+
+void renderDroneUI(const droneEntity *drone) {
+ // draw energy meter
+ const float energyMeterInnerRadius = 0.6f;
+ const float energyMeterOuterRadius = 0.3f;
+ const Vector2 energyMeterOrigin = {.x = drone->pos.x, .y = drone->pos.y};
+ float energyMeterEndAngle = 360.f * drone->energyLeft;
+ Color energyMeterColor = RAYWHITE;
+ if (drone->shield != NULL) {
+ energyMeterColor = bambooBrown;
+ } else if (drone->energyFullyDepleted && drone->energyRefillWait != 0.0f) {
+ energyMeterColor = bambooBrown;
+ energyMeterEndAngle = 360.0f * (1.0f - (drone->energyRefillWait / (DRONE_ENERGY_REFILL_EMPTY_WAIT)));
+ } else if (drone->energyFullyDepleted) {
+ energyMeterColor = GRAY;
+ }
+ DrawRing(energyMeterOrigin, energyMeterInnerRadius, energyMeterOuterRadius, 0.0f, energyMeterEndAngle, 32, energyMeterColor);
+
+ // draw burst charge indicator
+ if (drone->chargingBurst) {
+ const float alpha = min(drone->burstCharge + (50.0f / 255.0f), 1.0f);
+ const Color burstChargeColor = Fade(RAYWHITE, alpha);
+ const float burstChargeOuterRadius = (DRONE_BURST_RADIUS_BASE * drone->burstCharge) + DRONE_BURST_RADIUS_MIN;
+ const float burstChargeInnerRadius = burstChargeOuterRadius - 0.15f;
+ DrawRing(energyMeterOrigin, burstChargeInnerRadius, burstChargeOuterRadius, 0.0f, 360.0f, 50, burstChargeColor);
+ }
+
+ const float maxCharge = drone->weaponInfo->charge;
+ if (maxCharge == 0) {
+ return;
+ }
+
+ // draw charge meter
+ const Vector2 chargeMeterOrigin = {.x = drone->pos.x, .y = drone->pos.y + 3.3f};
+ const float chargeMeterInnerRadius = 1.0f;
+ const float chargeMeterOuterRadius = 0.5f;
+ const float chargeMeterStartAngle = 157.5f;
+ const float chargeMeterEndAngle = chargeMeterStartAngle - (135.0f * (drone->weaponCharge / drone->weaponInfo->charge));
+
+ rlPushMatrix();
+ rlTranslatef(chargeMeterOrigin.x, chargeMeterOrigin.y, 0.0f);
+ rlScalef(1.7f, 1.0f, 1.0f);
+
+ DrawRing(Vector2Zero(), chargeMeterInnerRadius, chargeMeterOuterRadius, chargeMeterStartAngle, chargeMeterEndAngle, 32, RAYWHITE);
+ DrawRingLines(Vector2Zero(), chargeMeterInnerRadius, chargeMeterOuterRadius, chargeMeterStartAngle, chargeMeterStartAngle - 135.0f, 10, RAYWHITE);
+
+ rlPopMatrix();
+}
+
+void renderProjectileTrail(const projectileEntity *proj) {
+ if (proj->trailPoints.length < 2) {
+ return; // need at least two points
+ }
+
+ const float maxWidth = proj->weaponInfo->radius;
+ const float numPoints = proj->trailPoints.length;
+
+ for (uint8_t i = 0; i < proj->trailPoints.length - 1; i++) {
+ const Vector2 p0 = proj->trailPoints.points[i];
+ const Vector2 p1 = proj->trailPoints.points[i + 1];
+
+ // Compute a perpendicular vector for the segment
+ Vector2 dir = Vector2Subtract(p1, p0);
+ if (Vector2Length(dir) == 0) {
+ continue; // Avoid division by zero
+ }
+ dir = Vector2Normalize(dir);
+ const Vector2 perp = {-dir.y, dir.x};
+
+ // Compute widths for the start and end of the segment.
+ // Taper so that older segments are narrower.
+ const float taper0 = (float)(i + 1) / numPoints;
+ const float taper1 = (float)(i + 2) / numPoints;
+ const float width0 = maxWidth * taper0;
+ const float width1 = maxWidth * taper1;
+
+ // Calculate two vertices on each side of the segment.
+ const Vector2 v0 = Vector2Add(p0, Vector2Scale(perp, width0));
+ const Vector2 v1 = Vector2Subtract(p0, Vector2Scale(perp, width0));
+ const Vector2 v2 = Vector2Add(p1, Vector2Scale(perp, width1));
+ const Vector2 v3 = Vector2Subtract(p1, Vector2Scale(perp, width1));
+
+ // Draw two triangles for the quad and fade the color with distance so older parts are more transparent.
+ const Color color = getProjectileColor(proj->weaponInfo->type);
+ DrawTriangle3D(
+ (Vector3){.x = v0.x, .y = 0.5f, .z = v0.y},
+ (Vector3){.x = v2.x, .y = 0.5f, .z = v2.y},
+ (Vector3){.x = v1.x, .y = 0.5f, .z = v1.y},
+ Fade(color, taper0)
+ );
+ DrawTriangle3D(
+ (Vector3){.x = v1.x, .y = 0.5f, .z = v1.y},
+ (Vector3){.x = v2.x, .y = 0.5f, .z = v2.y},
+ (Vector3){.x = v3.x, .y = 0.5f, .z = v3.y},
+ Fade(color, taper1)
+ );
+ }
+}
+
+void renderProjectile(const projectileEntity *projectile) {
+ DrawSphere(
+ (Vector3){.x = projectile->pos.x, .y = 0.5f, .z = projectile->pos.y},
+ projectile->weaponInfo->radius,
+ getProjectileColor(projectile->weaponInfo->type)
+ );
+}
+
+void renderBannerText(iwEnv *e, const bool starting, const int8_t winner, const int8_t winningTeam) {
+ const uint16_t fontSize = 5 * e->client->scale;
+
+ char *bannerStr = NULL;
+ Color winColor = PUFF_WHITE;
+
+ if (starting) {
+ bannerStr = "Ready?";
+ } else if (winner == -1 && winningTeam == -1) {
+ bannerStr = "Tie";
+ } else if (e->teamsEnabled) {
+ bannerStr = (char *)TextFormat("Team %d wins!", winningTeam + 1);
+ } else {
+ bannerStr = (char *)TextFormat("Player %d wins!", winner + 1);
+ winColor = getDroneColor(winner);
+ }
+
+ uint16_t textWidth = MeasureText(bannerStr, fontSize);
+ const uint16_t posX = (e->client->halfWidth - (textWidth / 2));
+ DrawText(bannerStr, posX, e->client->halfHeight, fontSize, winColor);
+}
+
+void applyBloom(const iwEnv *e, RenderTexture2D srcTex, RenderTexture2D dstTex, const float bloomIntensity) {
+ BeginTextureMode(e->client->blurSrcTexture);
+ ClearBackground(BLANK);
+ DrawTextureRec(srcTex.texture, (Rectangle){0.0f, 0.0f, e->client->width, -e->client->height}, Vector2Zero(), WHITE);
+ EndTextureMode();
+
+ // apply horizontal and vertical blurring
+ RenderTexture2D blurSrcTex = e->client->blurDstTexture;
+ RenderTexture2D blurDstTex = e->client->blurSrcTexture;
+ for (uint8_t i = 0, horizontal = true; i < 10; i++, horizontal = !horizontal) {
+ RenderTexture2D temp = blurSrcTex;
+ blurSrcTex = blurDstTex;
+ blurDstTex = temp;
+
+ Vector2 blurDir;
+ if (horizontal) {
+ blurDir = (Vector2){1.0f / e->client->width, 0.0f};
+ } else {
+ blurDir = (Vector2){0.0f, 1.0f / e->client->height};
+ }
+
+ BeginTextureMode(blurDstTex);
+ BeginShaderMode(e->client->blurShader);
+ SetShaderValue(e->client->blurShader, e->client->blurShaderDirLoc, &blurDir, SHADER_UNIFORM_VEC2);
+ DrawTextureRec(blurSrcTex.texture, (Rectangle){0.0f, 0.0f, e->client->width, -e->client->height}, Vector2Zero(), WHITE);
+ EndShaderMode();
+ EndTextureMode();
+ }
+
+ // bloom
+ BeginTextureMode(dstTex);
+ BeginShaderMode(e->client->bloomShader);
+
+ SetShaderValue(e->client->bloomShader, e->client->bloomIntensityLoc, &bloomIntensity, SHADER_UNIFORM_FLOAT);
+ SetShaderValueTexture(e->client->bloomShader, e->client->bloomTexColorLoc, srcTex.texture);
+ SetShaderValueTexture(e->client->bloomShader, e->client->bloomTexBloomBlurLoc, e->client->blurDstTexture.texture);
+ DrawTextureRec(e->client->blurDstTexture.texture, (Rectangle){0.0f, 0.0f, e->client->width, -e->client->height}, Vector2Zero(), WHITE);
+
+ EndShaderMode();
+ EndTextureMode();
+}
+
+void minimalStepEnv(iwEnv *e) {
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead || drone->shield == NULL) {
+ continue;
+ }
+
+ // update shield velocity if its active
+ b2Body_SetLinearVelocity(drone->shield->bodyID, b2Body_GetLinearVelocity(drone->bodyID));
+ };
+
+ b2World_Step(e->worldID, e->deltaTime, e->box2dSubSteps);
+
+ handleBodyMoveEvents(e);
+ handleContactEvents(e);
+ handleSensorEvents(e);
+
+ projectilesStep(e);
+
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead) {
+ continue;
+ }
+ droneStep(e, drone);
+ }
+}
+
+void _renderEnv(iwEnv *e, const bool starting, const bool ending, const int8_t winner, const int8_t winningTeam) {
+ if (ending) {
+ minimalStepEnv(e);
+ }
+
+ // UpdateCamera(&e->client->camera3D, CAMERA_ORBITAL);
+
+ updateCamera(e);
+
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ const droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead) {
+ // TODO: is there a better way to do this?
+ float gridPos[2] = {-1000, -1000};
+ SetShaderValue(e->client->gridShader, e->client->gridShaderPosLoc[drone->idx], gridPos, SHADER_UNIFORM_VEC2);
+ continue;
+ }
+
+ float gridPos[2] = {drone->pos.x, drone->pos.y};
+ SetShaderValue(e->client->gridShader, e->client->gridShaderPosLoc[drone->idx], gridPos, SHADER_UNIFORM_VEC2);
+ const Color droneColor = getDroneColor(drone->idx);
+ float gridColor[4] = {droneColor.r, droneColor.g, droneColor.b, droneColor.a};
+ SetShaderValue(e->client->gridShader, e->client->gridShaderColorLoc[drone->idx], gridColor, SHADER_UNIFORM_VEC4);
+ }
+
+ // apply bloom to parts of drones
+ BeginTextureMode(e->client->droneRawTex);
+ ClearBackground(BLACK);
+ BeginMode3D(e->client->camera->camera3D);
+
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ const droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead) {
+ continue;
+ }
+
+ // light up the laser aim guide if the drone's weapon is fully charged
+ if (drone->weaponInfo->charge != 0.0f && drone->weaponCharge == drone->weaponInfo->charge) {
+ renderDroneAimGuide(e, drone);
+ }
+
+ renderDroneLight(drone);
+ }
+
+ EndMode3D();
+ EndTextureMode();
+
+ applyBloom(e, e->client->droneRawTex, e->client->droneBloomTex, 2.0f);
+
+ // apply bloom to projectiles
+ BeginTextureMode(e->client->projRawTex);
+ ClearBackground(BLACK);
+ BeginMode3D(e->client->camera->camera3D);
+
+ BeginBlendMode(BLEND_ALPHA);
+ for (size_t i = 0; i < cc_array_size(e->projectiles); i++) {
+ const projectileEntity *projectile = safe_array_get_at(e->projectiles, i);
+ renderProjectileTrail(projectile);
+ }
+ EndBlendMode();
+
+ for (size_t i = 0; i < cc_array_size(e->projectiles); i++) {
+ const projectileEntity *projectile = safe_array_get_at(e->projectiles, i);
+ renderProjectile(projectile);
+ }
+
+ EndMode3D();
+ EndTextureMode();
+
+ applyBloom(e, e->client->projRawTex, e->client->projBloomTex, 3.0f);
+
+ BeginDrawing();
+ ClearBackground(BLACK);
+
+#ifndef __EMSCRIPTEN__
+ DrawFPS(e->client->scale, e->client->scale);
+#endif
+
+ BeginMode3D(e->client->camera->camera3D);
+
+ // TODO: fix for maps with different rows and columns
+ // draw a thicker grid below
+ float y = (-3.0f * WALL_THICKNESS) - 1.0f;
+ Color color = (Color){.r = 94, .g = 59, .b = 136, .a = 128};
+ for (int i = 0; i < e->map->columns; i++) {
+ const float d = WALL_THICKNESS * e->map->columns;
+ const Vector2 start = {.x = -d / 2.0f, .y = WALL_THICKNESS * i - d / 2.0f};
+ const Vector2 end = {.x = d / 2.0f, .y = WALL_THICKNESS * i - d / 2.0f};
+ const Vector3 pos = {.x = start.x, .y = y, .z = start.y};
+
+ rlPushMatrix();
+ rlTranslatef(pos.x, pos.y, pos.z);
+ rlRotatef(-90.0f, 0.0f, 0.0f, 1.0f);
+
+ DrawCylinder(Vector3Zero(), 0.1f, 0.1f, Vector2Distance(start, end), 1, color);
+
+ rlPopMatrix();
+ }
+ for (int i = 0; i < e->map->rows; i++) {
+ const float d = WALL_THICKNESS * e->map->rows;
+ const Vector2 start = {.x = WALL_THICKNESS * i - d / 2.0f, .y = -d / 2.0f};
+ const Vector2 end = {.x = WALL_THICKNESS * i - d / 2.0f, .y = d / 2.0f};
+ const Vector3 pos = {.x = start.x, .y = y, .z = start.y};
+
+ rlPushMatrix();
+ rlTranslatef(pos.x, pos.y, pos.z);
+ rlRotatef(90.0f, 1.0f, 0.0f, 0.0f);
+
+ DrawCylinder(Vector3Zero(), 0.1f, 0.1f, Vector2Distance(start, end), 1, color);
+
+ rlPopMatrix();
+ }
+
+ // render smaller higher grid
+ BeginBlendMode(BLEND_ALPHA);
+ BeginShaderMode(e->client->gridShader);
+ y = -1.0f;
+ color = PUFF_BACKGROUND;
+ for (int i = 0; i < 2 * e->map->columns; i++) {
+ const float d = WALL_THICKNESS * e->map->columns;
+ const Vector3 start = {.x = -d / 2.0f, .y = y, .z = WALL_THICKNESS / 2.0f * i - d / 2.0f};
+ const Vector3 end = {.x = (d - WALL_THICKNESS / 2.0f) / 2.0f, .y = y, .z = WALL_THICKNESS / 2.0f * i - d / 2.0f};
+
+ DrawLine3D(start, end, color);
+ }
+ for (int i = 0; i < 2 * e->map->rows; i++) {
+ float d = WALL_THICKNESS * e->map->rows;
+ const Vector3 start = {.x = WALL_THICKNESS * i / 2.0f - d / 2.0f, .y = y, .z = -d / 2.0f};
+ const Vector3 end = {.x = WALL_THICKNESS * i / 2.0f - d / 2.0f, .y = y, .z = (d - WALL_THICKNESS / 2.0f) / 2.0f};
+
+ DrawLine3D(start, end, color);
+ }
+ EndShaderMode();
+ EndBlendMode();
+
+ for (size_t i = 0; i < cc_array_size(e->pickups); i++) {
+ const weaponPickupEntity *pickup = safe_array_get_at(e->pickups, i);
+ renderWeaponPickup(e, pickup);
+ }
+
+ BeginBlendMode(BLEND_ALPHA);
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ const droneEntity *drone = safe_array_get_at(e->drones, i);
+ renderBrakeTrails(e, drone);
+ }
+ EndBlendMode();
+ renderDronePieces(e);
+
+ EndMode3D();
+
+ BeginBlendMode(BLEND_ADDITIVE);
+ DrawTextureRec(e->client->droneBloomTex.texture, (Rectangle){0.0f, 0.0f, e->client->width, -e->client->height}, Vector2Zero(), WHITE);
+ EndBlendMode();
+
+ BeginMode3D(e->client->camera->camera3D);
+
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead) {
+ continue;
+ }
+ renderDroneTrail(drone);
+ }
+
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead) {
+ continue;
+ }
+ renderDroneGuides(e, drone, ending);
+ }
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ const droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead) {
+ continue;
+ }
+ renderDrone(drone);
+ }
+
+ for (size_t i = 0; i < cc_array_size(e->walls); i++) {
+ const wallEntity *wall = safe_array_get_at(e->walls, i);
+ renderWall(e, wall);
+ }
+
+ for (size_t i = 0; i < cc_array_size(e->floatingWalls); i++) {
+ const wallEntity *wall = safe_array_get_at(e->floatingWalls, i);
+ renderWall(e, wall);
+ }
+
+ renderExplosions(e);
+ EndMode3D();
+
+ BeginBlendMode(BLEND_ADDITIVE);
+ DrawTextureRec(e->client->projRawTex.texture, (Rectangle){0.0f, 0.0f, e->client->width, -e->client->height}, Vector2Zero(), WHITE);
+ DrawTextureRec(e->client->projBloomTex.texture, (Rectangle){0.0f, 0.0f, e->client->width, -e->client->height}, Vector2Zero(), WHITE);
+ EndBlendMode();
+
+ BeginMode2D(e->client->camera->camera2D);
+
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead) {
+ continue;
+ }
+ renderDroneRespawnGuides(e, drone);
+ renderDroneUI(drone);
+ }
+
+#ifndef NDEBUG
+ for (uint8_t i = 0; i < cc_array_size(e->debugPoints); i++) {
+ debugPoint *point = safe_array_get_at(e->debugPoints, i);
+ const Vector2 pos = {.x = point->pos.x, .y = point->pos.y};
+ DrawCircleV(pos, point->size, point->color);
+ }
+#endif
+
+ EndMode2D();
+
+ for (uint8_t i = 0; i < cc_array_size(e->drones); i++) {
+ droneEntity *drone = safe_array_get_at(e->drones, i);
+ if (drone->dead) {
+ continue;
+ }
+ renderDroneAmmo(e, drone);
+ }
+
+ renderUI(e, starting);
+
+ if (starting || ending) {
+ renderBannerText(e, starting, winner, winningTeam);
+ }
+
+ EndDrawing();
+}
+
+void renderWait(iwEnv *e, const bool starting, const bool ending, const int8_t winner, const int8_t winningTeam, const float time) {
+#ifdef __EMSCRIPTEN__
+ const double startTime = emscripten_get_now();
+ while (time > (emscripten_get_now() - startTime) / 1000.0) {
+ _renderEnv(e, starting, ending, winner, winningTeam);
+ emscripten_sleep(e->deltaTime * 1000.0);
+ }
+#else
+ for (uint16_t i = 0; i < (uint16_t)(time * e->frameRate); i++) {
+ _renderEnv(e, starting, ending, winner, winningTeam);
+ }
+#endif
+}
+
+void renderEnv(iwEnv *e, const bool starting, const bool ending, const int8_t winner, const int8_t winningTeam) {
+ if (starting) {
+ renderWait(e, starting, ending, winner, winningTeam, START_READY_TIME);
+ } else if (ending) {
+ renderWait(e, starting, ending, winner, winningTeam, END_WAIT_TIME);
+ } else {
+ _renderEnv(e, starting, ending, winner, winningTeam);
+ }
+}
+
+#endif
diff --git a/pufferlib/ocean/impulse_wars/scripted_agent.h b/ocean/impulse_wars/scripted_agent.h
similarity index 100%
rename from pufferlib/ocean/impulse_wars/scripted_agent.h
rename to ocean/impulse_wars/scripted_agent.h
diff --git a/pufferlib/ocean/impulse_wars/settings.h b/ocean/impulse_wars/settings.h
similarity index 100%
rename from pufferlib/ocean/impulse_wars/settings.h
rename to ocean/impulse_wars/settings.h
diff --git a/pufferlib/ocean/impulse_wars/types.h b/ocean/impulse_wars/types.h
similarity index 100%
rename from pufferlib/ocean/impulse_wars/types.h
rename to ocean/impulse_wars/types.h
diff --git a/ocean/laser_puzzle/binding.c b/ocean/laser_puzzle/binding.c
new file mode 100644
index 0000000000..12eaaba238
--- /dev/null
+++ b/ocean/laser_puzzle/binding.c
@@ -0,0 +1,29 @@
+#include "laser_puzzle.h"
+
+#define OBS_SIZE (INIT_ROWS * INIT_COLS)
+#define NUM_ATNS 1
+#define ACT_SIZES {NUM_ACTIONS}
+#define OBS_TENSOR_T ByteTensor
+
+#define Env LaserPuzzle
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ // kwargs are passed in py the config .ini file, can set them here, will ignore for now
+ env->num_agents = 1;
+ env->ROWS = INIT_ROWS;
+ env->COLS = INIT_COLS;
+ env->max_steps = NUM_ACTIONS;
+ env->owns_buffers = 0;
+
+ // Only allocate env-owned state here. vecenv owns observations/actions/rewards/terminals and allocates it in the big buffer
+ env->board = (Cell*)calloc(env->ROWS * env->COLS, sizeof(Cell));
+ load_laser_puzzle_levels(env, LASER_PUZZLE_LEVELS_PATH);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+}
diff --git a/ocean/laser_puzzle/laser_puzzle.c b/ocean/laser_puzzle/laser_puzzle.c
new file mode 100644
index 0000000000..b902d019d3
--- /dev/null
+++ b/ocean/laser_puzzle/laser_puzzle.c
@@ -0,0 +1,72 @@
+#include "laser_puzzle.h"
+#include "puffernet.h"
+
+#define WEIGHTS_PATH "resources/laser_puzzle/laser_puzzle_weights.bin"
+
+static void copy_observations(float* out, const unsigned char* in) {
+ for (int i = 0; i < LASER_PUZZLE_OBS_SIZE; i++) {
+ out[i] = (float)in[i];
+ }
+}
+
+int demo() {
+ Weights* weights = load_weights(WEIGHTS_PATH);
+ if (weights == NULL) {
+ return 1;
+ }
+
+ int logit_sizes[1] = {NUM_ACTIONS};
+ PufferNet* net = make_puffernet(
+ weights, 1, LASER_PUZZLE_OBS_SIZE, 128, 2, logit_sizes, 1);
+ float observations[LASER_PUZZLE_OBS_SIZE] = {0};
+
+ LaserPuzzle env = {0};
+
+ // allocate memory, initialize the client
+ allocate(&env);
+ c_reset(&env);
+ env.client = make_client();
+
+ while (!WindowShouldClose()) {
+ if (IsKeyPressed(KEY_R)) {
+ c_reset(&env);
+ }
+
+ if (IsKeyDown(KEY_LEFT_SHIFT) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
+ int gridWidth = env.COLS * CELL_SIZE;
+ int gridHeight = env.ROWS * CELL_SIZE;
+ int offsetX = (GetScreenWidth() - gridWidth) / 2;
+ int offsetY = (GetScreenHeight() - gridHeight) / 2;
+
+ Vector2 mouse = GetMousePosition();
+ int c = (mouse.x - offsetX) / CELL_SIZE;
+ int r = (mouse.y - offsetY) / CELL_SIZE;
+
+ if (r >= 1 && r < env.ROWS - 1 && c >= 1 && c < env.COLS - 1) {
+ Cell* cell = &env.board[BOARD_IDX(env.COLS, r, c)];
+ int mirror_action = (cell->mirror + 1) % ACTIONS_PER_CELL;
+ int cell_idx = (r - 1) * INNER_COLS + (c - 1);
+ env.actions[0] = (float)(cell_idx * ACTIONS_PER_CELL + mirror_action);
+ c_step(&env);
+ }
+ } else {
+ copy_observations(observations, env.observations);
+ forward_puffernet(net, observations, env.actions);
+ c_step(&env);
+ }
+
+ c_render(&env);
+ }
+
+ free_puffernet(net);
+ free(weights);
+
+ // call closing procedures
+ c_close(&env);
+ return 0;
+}
+
+int main() {
+ demo();
+ return 0;
+}
diff --git a/ocean/laser_puzzle/laser_puzzle.h b/ocean/laser_puzzle/laser_puzzle.h
new file mode 100644
index 0000000000..15af98eb86
--- /dev/null
+++ b/ocean/laser_puzzle/laser_puzzle.h
@@ -0,0 +1,543 @@
+#include
+#include
+#include
+#include
+#include
+
+#include "raylib.h"
+#include "level_generation/puzzle_types.h"
+
+#define BOARD_IDX(cols, r, c) ((r) * (cols) + (c))
+#define LASER_PUZZLE_LEVELS_PATH "resources/laser_puzzle/laser_puzzle_levels.bin"
+
+// observations: 6*6 board, one byte per cell:
+// 0 empty, 1-8 laser ids 0-7, 9-16 sensor ids 0-7, 17 mirror /, 18 mirror \'
+#define LASER_PUZZLE_OBS_SIZE (INIT_ROWS * INIT_COLS)
+#define OBS_EMPTY 0
+#define OBS_LASER 1
+#define OBS_SENSOR (OBS_LASER + MAX_LASERS)
+#define OBS_MIRROR_RIGHT (OBS_SENSOR + MAX_LASERS)
+#define OBS_MIRROR_LEFT (OBS_MIRROR_RIGHT + 1)
+
+// actions: 4 * 4 * 3, set mirror to none, left or right for each interior cell. discrete actions
+#define ACTIONS_PER_CELL 3
+#define INNER_ROWS (INIT_ROWS - 2)
+#define INNER_COLS (INIT_COLS - 2)
+#define NUM_ACTIONS (ACTIONS_PER_CELL * INNER_ROWS * INNER_COLS)
+
+static const int CELL_SIZE = 80;
+static const Color LASER_COLORS[] = {SKYBLUE, RED, GREEN, YELLOW, BLUE, ORANGE, PURPLE, MAGENTA};
+
+// Required struct. Only use floats!
+typedef struct {
+ float perf; // Recommended 0-1 normalized single real number perf metric
+ float score; // Recommended unnormalized single real number perf metric
+ float episode_return; // Recommended metric: sum of agent rewards over episode
+ float episode_length; // Recommended metric: number of steps of agent episode
+ // Any extra fields you add here may be exported in binding.c
+ float n; // Required as the last field
+} Log;
+
+typedef struct {
+ Texture2D sprites;
+ Texture2D background;
+ Font font;
+ int assets_loaded;
+} Client;
+
+typedef struct {
+ int optimal_mirrors;
+ int sensor_count;
+ Cell puzzle[INIT_ROWS][INIT_COLS];
+} LaserPuzzleLevel;
+
+typedef struct {
+ Log log; // only stores results for completed episodes
+ Client* client;
+
+ unsigned char* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+
+ // vecenv uses num_agents and rng; owns_buffers prevents freeing vecenv-owned buffers.
+ int num_agents;
+ unsigned int rng;
+ int owns_buffers;
+
+ int episode_length;
+ int max_steps; // max actions allowed before the episode is over
+ float episode_return; // return for this episode
+
+ // env specific
+ int ROWS;
+ int COLS;
+ Cell *board;
+ int sinks_found;
+ int mirrors_placed;
+ int moves_made;
+ int total_sinks;
+ int sink_hit_before[MAX_LASERS];
+ int optimal_mirrors;
+ int num_levels;
+ LaserPuzzleLevel* levels;
+ int pending_reset;
+} LaserPuzzle;
+
+void load_laser_puzzle_levels(LaserPuzzle* env, const char* path) {
+ FILE* file = fopen(path, "rb");
+
+ uint32_t header[3] = {0};
+ fread(header, sizeof(uint32_t), 3, file);
+
+ int level_count = (int)header[2];
+ LaserPuzzleLevel* levels = (LaserPuzzleLevel*)calloc((size_t)level_count, sizeof(LaserPuzzleLevel));
+
+ for (int i = 0; i < level_count; i++) {
+ fread(&levels[i].optimal_mirrors, sizeof(int), 1, file);
+ fread(&levels[i].sensor_count, sizeof(int), 1, file);
+ for (int r = 0; r < INIT_ROWS; r++) {
+ for (int c = 0; c < INIT_COLS; c++) {
+ uint8_t raw[4] = {0};
+ fread(raw, sizeof(raw), 1, file);
+ levels[i].puzzle[r][c] = (Cell){
+ .type = (CellType)raw[0],
+ .mirror = (MirrorState)raw[1],
+ .id = (int8_t)raw[2],
+ };
+ }
+ }
+ }
+
+ fclose(file);
+ env->levels = levels;
+ env->num_levels = level_count;
+}
+
+
+// This allocate function only runs in the standalone demo since puffer vecenv already allocates memory.
+void allocate(LaserPuzzle* env) {
+ env->ROWS = INIT_ROWS;
+ env->COLS = INIT_COLS;
+ env->max_steps = NUM_ACTIONS;
+ env->num_agents = 1;
+ env->rng = 0;
+
+ env->board = (Cell*)calloc(env->ROWS * env->COLS, sizeof(Cell));
+ load_laser_puzzle_levels(env, LASER_PUZZLE_LEVELS_PATH);
+ if (env->observations == NULL) {
+ env->observations = (unsigned char*)calloc(env->ROWS * env->COLS, sizeof(unsigned char));
+ env->actions = (float*)calloc(1, sizeof(float));
+ env->rewards = (float*)calloc(1, sizeof(float));
+ env->terminals = (float*)calloc(1, sizeof(float));
+ env->owns_buffers = 1;
+ }
+}
+
+// Called from c_close in both standalone and vecenv modes.
+void deallocate(LaserPuzzle* env) {
+ free(env->board);
+ free(env->levels);
+
+ // check if we are in the standalone demo or puffer owns the buffers
+ if (env->owns_buffers) {
+ free(env->observations);
+ free(env->actions);
+ free(env->rewards);
+ free(env->terminals);
+ }
+
+ env->board = NULL;
+ env->levels = NULL;
+ env->observations = NULL;
+ env->actions = NULL;
+ env->rewards = NULL;
+ env->terminals = NULL;
+ env->num_levels = 0;
+
+ env->owns_buffers = 0;
+}
+
+Client* make_client() {
+ Client* client = (Client*)calloc(1, sizeof(Client));
+ InitWindow(800, 700, "laser puzzle");
+ SetTargetFPS(2);
+
+ client->sprites = LoadTexture("resources/shared/puffers.png");
+ client->font = LoadFontEx("resources/shared/JetBrainsMono-SemiBold.ttf", 32, NULL, 0);
+ client->assets_loaded = 1;
+ return client;
+}
+
+void close_client(Client* client) {
+ UnloadTexture(client->sprites);
+ UnloadTexture(client->background);
+ UnloadFont(client->font);
+ client->assets_loaded = 0;
+ if (IsWindowReady()) {
+ CloseWindow();
+ }
+ free(client);
+}
+
+// free alocated memory, unload raylib resources
+void c_close(LaserPuzzle* env) {
+ if (env->client != NULL) {
+ close_client(env->client);
+ env->client = NULL;
+ }
+
+ deallocate(env);
+}
+
+void add_log(LaserPuzzle* env) {
+ float perf = 0.0f; // takes into account sinks + mirros placed, normalized
+ if (env->mirrors_placed > 0) {
+ perf = ((float)env->sinks_found * (float)env->optimal_mirrors)
+ / ((float)env->total_sinks * (float)env->mirrors_placed);
+ }
+
+ float score = 0.0f; // takes into account sinks + mirros placed, unnormalized
+ if (env->mirrors_placed > 0) {
+ score = (float)env->sinks_found
+ * ((float)env->optimal_mirrors / (float)env->mirrors_placed);
+ }
+
+ env->log.perf += perf;
+ env->log.score += score;
+ env->log.episode_return += env->episode_return;
+ env->log.episode_length += env->episode_length;
+ env->log.n += 1.0f;
+}
+
+void apply_action(LaserPuzzle* env) {
+ int action = (int)env->actions[0];
+
+ int cell_idx = action / ACTIONS_PER_CELL; // 0..15 (for a 6x6 grid)
+ int mirror_action = action % ACTIONS_PER_CELL; // 0..2
+
+ int r = cell_idx / INNER_COLS; // 0..3 interior row
+ int c = cell_idx % INNER_COLS; // 0..3 interior col
+
+ // +1 to skip the borders, since the actions only correspond to the inner rows
+ Cell* cell = &env->board[BOARD_IDX(env->COLS, r + 1, c + 1)];
+ cell->mirror = (MirrorState)mirror_action;
+}
+
+
+void compute_observations(LaserPuzzle* env) {
+ for (int r = 0; r < env->ROWS; r++) {
+ for (int c = 0; c < env->COLS; c++) {
+ Cell cell = env->board[BOARD_IDX(env->COLS, r, c)];
+ unsigned char obs = OBS_EMPTY;
+
+ if (cell.type == LASER) {
+ obs = OBS_LASER + (unsigned char)cell.id;
+ } else if (cell.type == SENSOR) {
+ obs = OBS_SENSOR + (unsigned char)cell.id;
+ } else if (cell.mirror == MIRROR_RIGHT) {
+ obs = OBS_MIRROR_RIGHT;
+ } else if (cell.mirror == MIRROR_LEFT) {
+ obs = OBS_MIRROR_LEFT;
+ }
+
+ env->observations[BOARD_IDX(env->COLS, r, c)] = obs;
+ }
+ }
+}
+
+// reset the env state (ignore rewards, terminals --> handled by c_step)
+void c_reset(LaserPuzzle* env) {
+ env->sinks_found = 0;
+ env->mirrors_placed = 0;
+ env->moves_made = 0;
+ env->episode_length = 0;
+ env->episode_return = 0.0f;
+ env->pending_reset = 0;
+
+ memset(env->sink_hit_before, 0, sizeof(env->sink_hit_before));
+
+ int level_index = rand_r(&env->rng) % env->num_levels;
+ const LaserPuzzleLevel* level = &env->levels[level_index];
+ env->total_sinks = level->sensor_count;
+ env->optimal_mirrors = level->optimal_mirrors;
+
+ memcpy(env->board, level->puzzle, sizeof(level->puzzle));
+
+ compute_observations(env);
+}
+
+// advance state
+void c_step(LaserPuzzle* env) {
+ if (env->client && env->pending_reset) {
+ // When we have a client, since we deferred reset to display the terminal state, reset now. This also menas we are skipping an action given by puffernet. Not really an issue since this block only runs with puffer eval and standalone demo, not in training
+ c_reset(env);
+ return;
+ }
+
+ apply_action(env);
+ env->moves_made++;
+
+ // now we need to detect and update how many lasers are in thier sink and mirros are placed
+ env->sinks_found = 0;
+ env->mirrors_placed = 0;
+ int new_sinks_hit = 0;
+ for (int r = 0; r < env->ROWS; r++) {
+ for (int c = 0; c < env->COLS; c++) {
+ Cell boardCell = env->board[BOARD_IDX(env->COLS, r, c)];
+ if (boardCell.mirror != MIRROR_NONE) {
+ env->mirrors_placed++;
+ }
+
+ if (boardCell.type != LASER) {
+ continue;
+ }
+
+ int laserId = boardCell.id;
+ int curR = r;
+ int curC = c;
+ int dr = 0;
+ int dc = 0;
+
+ if (curR == 0) {
+ dr = 1;
+ } else if (curR == env->ROWS - 1) {
+ dr = -1;
+ } else if (curC == 0) {
+ dc = 1;
+ } else if (curC == env->COLS - 1) {
+ dc = -1;
+ }
+
+ while (curR + dr >= 0 && curR + dr < env->ROWS && curC + dc >= 0 && curC + dc < env->COLS) {
+ curR += dr;
+ curC += dc;
+
+ Cell hitCell = env->board[BOARD_IDX(env->COLS, curR, curC)];
+ if (hitCell.type == SENSOR && hitCell.id == laserId) {
+ env->sinks_found++;
+
+ if (!env->sink_hit_before[laserId]) {
+ env->sink_hit_before[laserId] = 1;
+ new_sinks_hit++;
+ }
+ } else if (hitCell.mirror == MIRROR_LEFT) {
+ int oldDr = dr;
+ dr = dc;
+ dc = oldDr;
+ } else if (hitCell.mirror == MIRROR_RIGHT) {
+ int oldDr = dr;
+ dr = -dc;
+ dc = -oldDr;
+ }
+ }
+ }
+ }
+
+ // handle the rewards, episode_length, terminal, episode_return
+ // rewards: +1 for ending the episode optimally (minimal mirrors), +0.6 for ending the episode suboptimally, -0.01 per move, +0.3 for first time laser hit
+ env->episode_length++;
+ env->rewards[0] = 0.3f * (float)new_sinks_hit;
+ env->terminals[0] = 0.0f;
+
+ if (env->sinks_found == env->total_sinks) {
+ env->terminals[0] = 1.0f;
+ if (env->mirrors_placed == env->optimal_mirrors) {
+ env->rewards[0] += 1.0f;
+ } else {
+ env->rewards[0] += 0.6f;
+ }
+ } else if (env->episode_length >= env->max_steps) {
+ env->terminals[0] = 1.0f;
+ }
+
+ env->episode_return += env->rewards[0];
+
+ if (env->terminals[0]) {
+ // we defer reset so that client can display the terminal state without it being immediately reset
+ add_log(env);
+ if (env->client) {
+ env->pending_reset = 1;
+ } else {
+ c_reset(env);
+ }
+ }
+
+ compute_observations(env);
+}
+
+void trace_laser(LaserPuzzle * env, int r, int c) {
+ Cell laser = env->board[BOARD_IDX(env->COLS, r, c)];
+ Color laserColor = LASER_COLORS[laser.id % 8];
+
+ int dr = 0;
+ int dc = 0;
+ if (r == 0) {
+ dr = 1;
+ } else if (r == env->ROWS - 1) {
+ dr = -1;
+ } else if (c == 0) {
+ dc = 1;
+ } else if (c == env->COLS - 1) {
+ dc = -1;
+ }
+
+ int gridWidth = env->COLS * CELL_SIZE;
+ int gridHeight = env->ROWS * CELL_SIZE;
+ int offsetX = (GetScreenWidth() - gridWidth) / 2;
+ int offsetY = (GetScreenHeight() - gridHeight) / 2;
+
+ int curR = r;
+ int curC = c;
+
+ while (curR + dr >= 0 && curR + dr < env->ROWS && curC + dc >= 0 && curC + dc < env->COLS) {
+ int nextR = curR + dr;
+ int nextC = curC + dc;
+
+ Vector2 start = {
+ offsetX + curC * CELL_SIZE + CELL_SIZE / 2.0f,
+ offsetY + curR * CELL_SIZE + CELL_SIZE / 2.0f
+ };
+ Vector2 end = {
+ offsetX + nextC * CELL_SIZE + CELL_SIZE / 2.0f,
+ offsetY + nextR * CELL_SIZE + CELL_SIZE / 2.0f
+ };
+
+ // offset so that the puffer fish mouth not blocked by lasers
+ if (env->board[BOARD_IDX(env->COLS, curR, curC)].type == LASER) {
+ start.x += dc * 27.0f;
+ start.y += dr * 27.0f;
+ }
+
+ DrawLineEx(start, end, 7, Fade(laserColor, 0.65f));
+ DrawLineEx(start, end, 3, Fade(WHITE, 0.75f));
+
+ // update current cell
+ curR = nextR;
+ curC = nextC;
+
+ // update direction
+ Cell cell = env->board[BOARD_IDX(env->COLS, curR, curC)];
+ if (cell.mirror == MIRROR_LEFT) {
+ int oldDr = dr;
+ dr = dc;
+ dc = oldDr;
+ } else if (cell.mirror == MIRROR_RIGHT) {
+ int oldDr = dr;
+ dr = -dc;
+ dc = -oldDr;
+ }
+ }
+}
+
+void draw_lasers(LaserPuzzle *env) {
+ for (int r = 0; r < env->ROWS; r++) {
+ for (int c = 0; c < env->COLS; c++) {
+ if (env->board[BOARD_IDX(env->COLS, r, c)].type == LASER) {
+ trace_laser(env, r, c);
+ }
+ }
+ }
+}
+
+void c_render(LaserPuzzle* env) {
+ // this client loading here and "escape key to shutdown" is Puffer convention, needs to be like this for
+ // puffer eval to work.
+ if (env->client == NULL) {
+ env->client = make_client();
+ }
+ Client* client = env->client;
+
+ // Standard across our envs so exiting is always the same
+ if (IsKeyDown(KEY_ESCAPE)) {
+ exit(0);
+ }
+
+ BeginDrawing();
+
+ ClearBackground((Color){10, 12, 24, 255});
+
+ // draw the centered grid
+ int gridWidth = env->COLS * CELL_SIZE;
+ int gridHeight = env->ROWS * CELL_SIZE;
+ int offsetX = (GetScreenWidth() - gridWidth) / 2;
+ int offsetY = (GetScreenHeight() - gridHeight) / 2;
+
+ for (int r = 0; r < env->ROWS; r++) {
+ for (int c = 0; c < env->COLS; c++) {
+ int x = offsetX + c * CELL_SIZE;
+ int y = offsetY + r * CELL_SIZE;
+
+ // draw the grey "X" for the mirrors (exclude border cells)
+ if (r > 0 && r < env->ROWS - 1 && c > 0 && c < env->COLS - 1) {
+ DrawLineEx((Vector2){x + 20, y + 20}, (Vector2){x + CELL_SIZE - 20, y + CELL_SIZE - 20}, 2, Fade(GRAY, 0.25f));
+ DrawLineEx((Vector2){x + CELL_SIZE - 20, y + 20}, (Vector2){x + 20, y + CELL_SIZE - 20}, 2, Fade(GRAY, 0.25f));
+ }
+
+ Cell cell = env->board[BOARD_IDX(env->COLS, r, c)];
+
+ if (cell.mirror == MIRROR_LEFT) {
+ DrawLineEx((Vector2){x + 10, y + 10}, (Vector2){x + CELL_SIZE - 10, y + CELL_SIZE - 10}, 12, Fade(VIOLET, 0.55f));
+ DrawLineEx((Vector2){x + 10, y + 10}, (Vector2){x + CELL_SIZE - 10, y + CELL_SIZE - 10}, 8, Fade(SKYBLUE, 0.9f));
+ DrawLineEx((Vector2){x + 10, y + 10}, (Vector2){x + CELL_SIZE - 10, y + CELL_SIZE - 10}, 4, BLACK);
+ } else if (cell.mirror == MIRROR_RIGHT) {
+ DrawLineEx((Vector2){x + CELL_SIZE - 10, y + 10}, (Vector2){x + 10, y + CELL_SIZE - 10}, 12, Fade(VIOLET, 0.55f));
+ DrawLineEx((Vector2){x + CELL_SIZE - 10, y + 10}, (Vector2){x + 10, y + CELL_SIZE - 10}, 8, Fade(SKYBLUE, 0.9f));
+ DrawLineEx((Vector2){x + CELL_SIZE - 10, y + 10}, (Vector2){x + 10, y + CELL_SIZE - 10}, 4, BLACK);
+ } else if (cell.type == LASER) {
+ int spriteIndex = cell.id % 8;
+ Rectangle source = {spriteIndex * 64.0f, 392.0f, 64.0f, 46.0f};
+ Rectangle dest = {x + CELL_SIZE / 2.0f, y + CELL_SIZE / 2.0f, 64.0f, 46.0f};
+
+ // need to make sure pufferfish are facing the right direction
+ Vector2 origin = {32.0f, 23.0f};
+ float rotation = 0.0f;
+
+ if (r == 0) {
+ rotation = 90.0f;
+ } else if (r == env->ROWS - 1) {
+ rotation = -90.0f;
+ } else if (c == env->COLS - 1) {
+ rotation = 180.0f;
+ source.height = -source.height;
+ }
+
+ DrawTexturePro(client->sprites, source, dest, origin, rotation, WHITE);
+ } else if (cell.type == SENSOR) {
+ int spriteIndex = cell.id % 8;
+ Rectangle source = {spriteIndex * 64.0f, 529.0f, 64.0f, 30.0f};
+ Rectangle dest = {x + 12.0f, y + 24.0f, 56.0f, 26.0f};
+ DrawTexturePro(client->sprites, source, dest, (Vector2){0}, 0.0f, WHITE);
+ }
+ }
+ }
+
+ // draw the lasers
+ draw_lasers(env);
+
+ // draw the sinks found and mirrors used
+ const float fontSize = 32.0f;
+ const float spacing = 1.0f;
+ const char* sinksText = TextFormat("Sinks: %i/%i", env->sinks_found, env->total_sinks);
+ const char* movesText = TextFormat("Moves: %i", env->moves_made);
+ const char* mirrorsText = TextFormat("Mirrors: %i/%i", env->mirrors_placed, env->optimal_mirrors);
+ Vector2 movesSize = MeasureTextEx(client->font, movesText, fontSize, spacing);
+ Vector2 mirrorsSize = MeasureTextEx(client->font, mirrorsText, fontSize, spacing);
+
+ DrawTextEx(client->font, sinksText, (Vector2){16, 14}, fontSize, spacing, RAYWHITE);
+ DrawTextEx(client->font, movesText, (Vector2){GetScreenWidth() - movesSize.x - 16, GetScreenHeight() - fontSize - 16}, fontSize, spacing, RAYWHITE);
+ DrawTextEx(client->font, mirrorsText, (Vector2){GetScreenWidth() - mirrorsSize.x - 16, 14}, fontSize, spacing, RAYWHITE);
+
+ if (env->sinks_found == env->total_sinks) {
+ const char* solvedText = "Puzzle solved! Can you do it with less mirrors?";
+ if (env->mirrors_placed == env->optimal_mirrors) {
+ solvedText = "Optimal solve! Press R for the next puzzle.";
+ }
+
+ const float solvedFontSize = 24.0f;
+ Vector2 solvedSize = MeasureTextEx(client->font, solvedText, solvedFontSize, spacing);
+ DrawTextEx(client->font, solvedText, (Vector2){(GetScreenWidth() - solvedSize.x) / 2.0f, 56}, solvedFontSize, spacing, RAYWHITE);
+ }
+
+ EndDrawing();
+}
diff --git a/ocean/laser_puzzle/level_generation/generate_level.py b/ocean/laser_puzzle/level_generation/generate_level.py
new file mode 100644
index 0000000000..6d2bbe41e1
--- /dev/null
+++ b/ocean/laser_puzzle/level_generation/generate_level.py
@@ -0,0 +1,164 @@
+import random
+
+DIRS_MAP = {
+ "down": (1, 0),
+ "up": (-1, 0),
+ "right": (0, 1),
+ "left": (0, -1),
+}
+MIRRORS = {"MR", "ML"}
+
+# returns a grid with one higher in each dimension (to store the border for the lasers sources and sinks)
+def generate_grid(MIN_ROWS, MAX_ROWS, MIN_COLS, MAX_COLS, MIN_LASERS, MAX_LASERS):
+ puzzle_rows = random.randint(MIN_ROWS, MAX_ROWS)
+ puzzle_cols = random.randint(MIN_COLS, MAX_COLS)
+ possible_lasers = random.randint(MIN_LASERS, MAX_LASERS)
+
+ # we will augment the grid by one to store the laser sources and sinks on the border
+ grid = [['*'] * (puzzle_cols + 2) for _ in range(puzzle_rows + 2)]
+
+ # choose where to put the lasers (inner cells only)
+ ROWS, COLS = len(grid), len(grid[0])
+ laser_choices = (
+ [(0, c) for c in range(1, COLS - 1)] +
+ [(ROWS - 1, c) for c in range(1, COLS - 1)] +
+ [(r, 0) for r in range(1, ROWS - 1)] +
+ [(r, COLS - 1) for r in range(1, ROWS - 1)]
+ )
+
+ laser_count = min(len(laser_choices), possible_lasers)
+ for idx, pos in enumerate(random.sample(laser_choices, laser_count)):
+ grid[pos[0]][pos[1]] = f"L{idx}"
+
+ return grid
+
+def on_border(pos, grid):
+ return pos[0] in (0, len(grid) - 1) or pos[1] in (0, len(grid[0]) - 1)
+
+def laser_direction(pos, grid):
+ rows = len(grid)
+ if pos[0] == 0:
+ return DIRS_MAP["down"]
+ if pos[0] == rows - 1:
+ return DIRS_MAP["up"]
+ if pos[1] == 0:
+ return DIRS_MAP["right"]
+ return DIRS_MAP["left"]
+
+# take a step with laser, give new pos of laser, give new direction of laser also (None if at border)
+def laser_step(pos, direction, grid):
+ nr = pos[0] + direction[0]
+ nc = pos[1] + direction[1]
+
+ # we have hit the border
+ if on_border((nr, nc), grid):
+ return (nr, nc), None
+
+ # check if we hit a mirror and reflect
+ if grid[nr][nc] == "ML":
+ return (nr, nc), (direction[1], direction[0])
+ elif grid[nr][nc] == "MR":
+ return (nr, nc), (-direction[1], -direction[0])
+
+ # no mirror, no border, just empty cell
+ return (nr, nc), direction
+
+# give one by one, the next position of a laser, taking into acount the board state
+def walk_laser(grid, start):
+ pos = start
+ direction = laser_direction(start, grid)
+ while direction is not None:
+ pos, direction = laser_step(pos, direction, grid)
+ yield pos, direction
+
+# Rules:
+# 1) laser paths cannot cycle
+# 2) laser paths of two colors can never be in the same position and same direction
+# 3) every mirror must be hit by at least one laser
+def ensure_rules(grid):
+ mirrors = {(r, c) for r in range(len(grid)) for c in range(len(grid[r])) if grid[r][c] in MIRRORS}
+
+ visited_lasers = set()
+
+ for laser_start in {(r, c) for r in range(len(grid)) for c in range(len(grid[r])) if grid[r][c].startswith("L")}:
+ start_state = (laser_start, laser_direction(laser_start, grid))
+ visited_lasers.add(start_state)
+
+ for pos, direction in walk_laser(grid, laser_start):
+ if direction is None:
+ break
+
+ state = (pos, direction)
+ if state in visited_lasers:
+ return False
+
+ mirrors.discard(pos)
+ visited_lasers.add(state)
+
+ # all mirrors must be used
+ return not mirrors
+
+
+def place_a_mirror(grid):
+ valid = []
+ for laser_start in {(r, c) for r in range(len(grid)) for c in range(len(grid[r])) if grid[r][c].startswith("L")}:
+ for pos, direction in walk_laser(grid, laser_start):
+ if direction is None:
+ break
+ if grid[pos[0]][pos[1]] == '*':
+ valid.append(pos)
+
+ # Try valid slots in random order. Duplicates bias toward cells hit by multiple lasers since using a list instead of a set, good for puzzle complexity
+ random.shuffle(valid)
+
+ for chosen in valid:
+ orientations = ["ML", "MR"]
+ random.shuffle(orientations)
+
+ for orientation in orientations:
+ grid[chosen[0]][chosen[1]] = orientation
+ if ensure_rules(grid):
+ return True
+ else:
+ # backtrack
+ grid[chosen[0]][chosen[1]] = '*'
+
+ return False
+
+
+def generate_puzzle(MIN_ROWS, MAX_ROWS, MIN_COLS, MAX_COLS, MIN_LASERS, MAX_LASERS, MIN_MIRRORS, MAX_MIRRORS, MAX_TRIES):
+
+ need_mirrors = random.randint(MIN_MIRRORS, MAX_MIRRORS)
+ for tries in range(MAX_TRIES):
+ # create a fresh grid for this full attempt
+ grid = generate_grid(MIN_ROWS, MAX_ROWS, MIN_COLS, MAX_COLS, MIN_LASERS, MAX_LASERS)
+
+ for _ in range(need_mirrors):
+ if not place_a_mirror(grid):
+ break
+ else:
+ break
+ else:
+ return None, None
+
+ lasers = {(r, c) for r in range(len(grid)) for c in range(len(grid[r])) if grid[r][c].startswith("L")}
+ sinks = set()
+ insert_sinks = []
+ for laser_start in lasers:
+ laser_pos = None
+ for laser_pos, direction in walk_laser(grid, laser_start):
+ if direction is None:
+ break
+
+ if laser_pos in sinks or laser_pos in lasers:
+ return None, None
+
+ laser_number = grid[laser_start[0]][laser_start[1]][1:]
+ sinks.add(laser_pos)
+ insert_sinks.append((laser_pos, laser_number))
+
+ # now make sure the source, sink pairs are paired and labelled correctly in the graph
+ for sink, laser_number in insert_sinks:
+ grid[sink[0]][sink[1]] = f"S{laser_number}"
+
+ return grid, tries + 1
diff --git a/ocean/laser_puzzle/level_generation/generate_levels_bin.py b/ocean/laser_puzzle/level_generation/generate_levels_bin.py
new file mode 100644
index 0000000000..92974efbbd
--- /dev/null
+++ b/ocean/laser_puzzle/level_generation/generate_levels_bin.py
@@ -0,0 +1,162 @@
+import os
+import random
+import struct
+from concurrent.futures import FIRST_COMPLETED, ProcessPoolExecutor, wait
+from pathlib import Path
+
+from generate_level import generate_puzzle
+from optimal_solver import iddfs
+
+
+OUTPUT_PATH = Path(__file__).resolve().parents[3] / "resources" / "laser_puzzle" / "laser_puzzle_levels.bin"
+MAGIC = b"LPZL"
+VERSION = 1
+WORKERS = max(1, (os.cpu_count() or 1) // 2)
+
+GROUPS = {
+ 7: {"count": 8, "sensor_counts": {4}},
+ 6: {"count": 12, "sensor_counts": {4}},
+ 5: {"count": 25, "sensor_counts": {3, 4}},
+ 4: {"count": 25, "sensor_counts": {2, 3}},
+ 2: {"count": 25, "sensor_counts": {1, 2}},
+ 1: {"count": 5, "sensor_counts": {1}},
+}
+
+CELL_TYPE_EMPTY = 0
+CELL_TYPE_LASER = 1
+CELL_TYPE_SENSOR = 2
+MIRROR_NONE = 0
+MIRROR_RIGHT = 1
+MIRROR_LEFT = 2
+
+def strip_mirrors(grid):
+ return [["*" if grid[r][c] in {"ML", "MR"} else grid[r][c] for c in range(len(grid[r]))] for r in range(len(grid))]
+
+def sensor_count(grid):
+ return sum(1 for r in range(len(grid)) for c in range(len(grid[r])) if grid[r][c].startswith("S"))
+
+def encode_cell(token):
+ if token == "*":
+ return CELL_TYPE_EMPTY, MIRROR_NONE, -1
+ if token == "MR":
+ return CELL_TYPE_EMPTY, MIRROR_RIGHT, -1
+ if token == "ML":
+ return CELL_TYPE_EMPTY, MIRROR_LEFT, -1
+ if token.startswith("L"):
+ return CELL_TYPE_LASER, MIRROR_NONE, int(token[1:])
+ if token.startswith("S"):
+ return CELL_TYPE_SENSOR, MIRROR_NONE, int(token[1:])
+ raise ValueError(f"Unknown cell token: {token}")
+
+
+def write_levels_bin(levels, output_path):
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ with output_path.open("wb") as file:
+ file.write(MAGIC)
+ file.write(struct.pack("= GROUPS[optimal_mirrors]["count"]:
+ continue
+
+ seen_puzzles.add(puzzle_key)
+ found[optimal_mirrors].append(candidate)
+ print(
+ "found",
+ optimal_mirrors,
+ f"{len(found[optimal_mirrors])}/{GROUPS[optimal_mirrors]['count']}",
+ "sensors",
+ sensors,
+ "attempts",
+ candidate["attempt"],
+ flush=True,
+ )
+
+ if not unfilled_groups():
+ for future in pending:
+ future.cancel()
+ break
+ while len(pending) < WORKERS and unfilled_groups():
+ submit_candidate(executor)
+
+ return [level for mirror_count in GROUPS for level in found[mirror_count]]
+
+
+def main():
+ levels = generate_verified_puzzles()
+ write_levels_bin(levels, OUTPUT_PATH)
+ print(f"wrote {len(levels)} levels to {OUTPUT_PATH}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ocean/laser_puzzle/level_generation/optimal_solver.py b/ocean/laser_puzzle/level_generation/optimal_solver.py
new file mode 100644
index 0000000000..0cc9f86cf9
--- /dev/null
+++ b/ocean/laser_puzzle/level_generation/optimal_solver.py
@@ -0,0 +1,87 @@
+"""
+Optimal Solver time complexity:
+
+branching factor: at most 16 * 2 = 32 initially
+depth: 7
+work per state: at most 16 * 4 = 64 (4 lasers)
+
+4x4 board, max 7 mirrors
+states: sum(C(16, k) * 2^k for k in 0..7) = 2,150,721
+
+Total work = states * work per state = 2,150,721 * 64 = 137,646,144 --> severly reduced though in practice due to laser path pruning
+
+Will implement idffs + pruning based on laser beams path. This means realistically, we should be finding the solution significantly quicker than the worst case
+"""
+from generate_level import laser_direction
+from generate_level import walk_laser
+
+def iddfs(grid, max_depth):
+ optimal = None
+ def dfs(grid, visited, cur_depth, goal_depth):
+ nonlocal optimal
+
+ if cur_depth > goal_depth:
+ return
+
+ # found the solution already, exit
+ if optimal is not None:
+ return
+
+ ROWS, COLS = len(grid), len(grid[0])
+
+ lasers = {(r, c, grid[r][c][1:]) for r in range(ROWS) for c in range(COLS) if grid[r][c][0] == 'L'}
+
+ valid = set()
+ need_sinks = len(lasers)
+
+ for laser in lasers:
+ r,c,idx = laser
+ cur_dir = laser_direction((r,c), grid)
+
+ visited_laser = set([((r,c), cur_dir)])
+ for next_pos, next_dir in walk_laser(grid, (r,c)):
+ if next_dir is None:
+ if grid[next_pos[0]][next_pos[1]] == f"S{idx}":
+ need_sinks -= 1
+ break
+
+ next_state = (next_pos, next_dir)
+
+ # see if laser forms a cycle
+ if next_state in visited_laser:
+ break
+
+ if grid[next_pos[0]][next_pos[1]] == '*':
+ valid.add(next_pos)
+
+ visited_laser.add(next_state)
+
+ # check if we have put the lasers all in thier place
+ if need_sinks == 0:
+ # store the optimal solution
+ optimal = [[[grid[r][c] for c in range(len(grid[r]))] for r in range(len(grid))], cur_depth]
+ return
+
+ # we now have all the valid spots, choose each valid spot with both configurations and branch out
+ for r,c in valid:
+ for orientation in ['ML', 'MR']:
+ grid[r][c] = orientation
+ state = tuple(tuple(grid[r][c] for c in range(len(grid[r]))) for r in range(len(grid)))
+ if state in visited:
+ # backtrack
+ grid[r][c] = '*'
+ continue
+
+ visited.add(state)
+ dfs(grid, visited, cur_depth + 1, goal_depth)
+
+ # backtrack
+ grid[r][c] = '*'
+
+ for depth in range(max_depth + 1):
+ visited = set([tuple(tuple(grid[r][c] for c in range(len(grid[r]))) for r in range(len(grid)))])
+ dfs(grid, visited, 0, depth)
+ if optimal is not None:
+ return optimal
+
+ return [None, None]
diff --git a/ocean/laser_puzzle/level_generation/puzzle_types.h b/ocean/laser_puzzle/level_generation/puzzle_types.h
new file mode 100644
index 0000000000..f3b4cca86d
--- /dev/null
+++ b/ocean/laser_puzzle/level_generation/puzzle_types.h
@@ -0,0 +1,26 @@
+#ifndef PUZZLE_TYPES_H
+#define PUZZLE_TYPES_H
+
+#define INIT_ROWS 6
+#define INIT_COLS 6
+#define MAX_LASERS 8
+
+typedef enum {
+ EMPTY,
+ LASER,
+ SENSOR
+} CellType;
+
+typedef enum {
+ MIRROR_NONE,
+ MIRROR_RIGHT,
+ MIRROR_LEFT
+} MirrorState;
+
+typedef struct {
+ CellType type;
+ MirrorState mirror;
+ int id;
+} Cell;
+
+#endif
diff --git a/ocean/lightsout/binding.c b/ocean/lightsout/binding.c
new file mode 100644
index 0000000000..68b7953b02
--- /dev/null
+++ b/ocean/lightsout/binding.c
@@ -0,0 +1,33 @@
+#include "lightsout.h"
+
+#define GRID_SIZE 5
+#define OBS_SIZE (GRID_SIZE * GRID_SIZE)
+#define NUM_ATNS 1
+#define ACT_SIZES {GRID_SIZE * GRID_SIZE}
+#define OBS_TENSOR_T ByteTensor
+
+#define Env LightsOut
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->grid_size = GRID_SIZE;
+ env->cell_size = 1280 / GRID_SIZE;
+ if (1280 % GRID_SIZE != 0) env->cell_size++; // ceil
+ env->max_steps = (int)dict_get(kwargs, "max_steps")->value;
+ env->observation_size = OBS_SIZE;
+ env->num_agents = 1;
+
+ env->ema = 0.5f;
+ env->score_ema = 0.0f;
+ env->scramble_prob = 0.15f;
+
+ init_lightsout(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "scramble_p", log->scramble_p);
+}
diff --git a/ocean/lightsout/lightsout.c b/ocean/lightsout/lightsout.c
new file mode 100644
index 0000000000..e27b0fc9ee
--- /dev/null
+++ b/ocean/lightsout/lightsout.c
@@ -0,0 +1,53 @@
+#include
+#include
+#include "lightsout.h"
+
+static LightsOut* g_env = NULL;
+
+static void demo_cleanup(void) {
+ if (g_env == NULL) {
+ return;
+ }
+ free(g_env->observations);
+ free(g_env->actions);
+ free(g_env->rewards);
+ free(g_env->terminals);
+ c_close(g_env);
+ g_env = NULL;
+}
+
+int demo(){
+ srand((unsigned)time(NULL));
+ LightsOut env = {.grid_size = 5, .cell_size = 100, .client = NULL};
+ g_env = &env;
+ atexit(demo_cleanup);
+ env.observations = (unsigned char*)calloc(env.grid_size * env.grid_size, sizeof(unsigned char));
+ env.actions = (float*)calloc(1, sizeof(float));
+ env.rewards = (float*)calloc(1, sizeof(float));
+ env.terminals = (float*)calloc(1, sizeof(float));
+
+ c_reset(&env);
+ env.client = make_client(env.cell_size, env.grid_size);
+
+ while (!WindowShouldClose()) {
+ if (IsKeyPressed(KEY_UP) || IsKeyPressed(KEY_W)) env.client->cursor_row = (env.client->cursor_row - 1 + env.grid_size) % env.grid_size;
+ if (IsKeyPressed(KEY_DOWN) || IsKeyPressed(KEY_S)) env.client->cursor_row = (env.client->cursor_row + 1) % env.grid_size;
+ if (IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_A)) env.client->cursor_col = (env.client->cursor_col - 1 + env.grid_size) % env.grid_size;
+ if (IsKeyPressed(KEY_RIGHT) || IsKeyPressed(KEY_D)) env.client->cursor_col = (env.client->cursor_col + 1) % env.grid_size;
+ if (IsKeyPressed(KEY_SPACE)) {
+ int idx = env.client->cursor_row * env.grid_size + env.client->cursor_col;
+ env.actions[0] = (float)idx;
+ c_step(&env);
+ } else if (IsKeyPressed(KEY_R)) {
+ c_reset(&env);
+ }
+ c_render(&env);
+ }
+
+ demo_cleanup();
+ return 0;
+}
+int main(void) {
+ demo();
+ return 0;
+}
diff --git a/ocean/lightsout/lightsout.h b/ocean/lightsout/lightsout.h
new file mode 100644
index 0000000000..508e12f5a6
--- /dev/null
+++ b/ocean/lightsout/lightsout.h
@@ -0,0 +1,231 @@
+#include
+#include
+#include
+#include "raylib.h"
+
+// Only use floats.
+typedef struct {
+ float perf;
+ float score;
+ float episode_return;
+ float episode_length;
+ float scramble_p;
+ float n; // Required as the last field.
+} Log;
+
+typedef struct Client {
+ int cell_size;
+ int cursor_row;
+ int cursor_col;
+} Client;
+
+typedef struct {
+ Log log; // Required field.
+ unsigned char* observations; // Required field. Ensure type matches in .py and .c.
+ float* actions; // Required field. Ensure type matches in .py and .c.
+ float* rewards; // Required field.
+ float* terminals; // Required field.
+ int grid_size;
+ int cell_size;
+ int max_steps;
+ int step_count;
+ int lights_on;
+ int prev_action;
+ int last_action;
+ float episode_return;
+ float ema;
+ float score_ema;
+ float scramble_prob;
+ unsigned char* grid;
+ Client* client;
+ int num_agents;
+ int observation_size;
+ unsigned int rng;
+} LightsOut;
+
+void step_grid(LightsOut* env, int idx) {
+ if (idx < 0 || idx >= env->grid_size * env->grid_size) return;
+ int row = idx/env->grid_size;
+ int col = idx%env->grid_size;
+
+ static const int dirs[5][2] = {{0,0}, {1,0}, {0,1}, {-1,0}, {0,-1}};
+ for (int i = 0; i < 5; i++) {
+ int dr = dirs[i][0];
+ int dc = dirs[i][1];
+ int r = row + dr;
+ int c = col + dc;
+ if (r >= 0 && r < env->grid_size && c >= 0 && c < env->grid_size) {
+ int offset = r*env->grid_size + c;
+ unsigned char old = env->grid[offset];
+ env->grid[offset] = (unsigned char)!old;
+ env->lights_on += old ? -1 : 1;
+ }
+ }
+}
+
+void init_lightsout(LightsOut* env) {
+ int n = env->grid_size * env->grid_size;
+ if (env->grid == NULL) {
+ env->grid = (unsigned char*)calloc(n, sizeof(unsigned char));
+ } else {
+ memset(env->grid, 0, n * sizeof(unsigned char));
+ }
+
+ if (env->ema > 0.7f && env->score_ema > 0.0f) {
+ env->scramble_prob = fminf(0.5f, env->scramble_prob + 0.01f); // Increase scramble prob if EMA is high
+ } else if (env->ema < 0.3f) {
+ env->scramble_prob = fmaxf(0.15f, env->scramble_prob - 0.01f); // Decrease scramble prob if EMA is low
+ }
+
+ env->step_count = 0;
+ env->lights_on = 0;
+ env->prev_action = -1;
+ env->last_action = -1;
+ env->episode_return = 0.0f;
+
+ for (int i = 0; i < n; i++) {
+ float u = (float)rand_r(&env->rng) / (float)RAND_MAX;
+ if (u < env->scramble_prob) {
+ step_grid(env, i);
+ }
+ }
+}
+
+void c_close(LightsOut* env) {
+ free(env->grid);
+ env->grid = NULL;
+ if (env->client != NULL) {
+ if (IsWindowReady()) {
+ CloseWindow();
+ }
+ free(env->client);
+ env->client = NULL;
+ }
+}
+
+void compute_observations(LightsOut* env) {
+ for (int i = 0; i < env->grid_size * env->grid_size; i++) {
+ env->observations[i] = env->grid[i];
+ }
+}
+
+void c_reset(LightsOut* env) {
+ env->rewards[0] = 0.0f;
+ env->terminals[0] = 0.0f;
+ init_lightsout(env);
+ compute_observations(env);
+}
+
+void c_step(LightsOut* env) {
+ int num_cells = env->grid_size * env->grid_size;
+ int atn = env->actions[0];
+ env->terminals[0] = 0.0f;
+
+ float reward = -0.02 * (36.0 / (env->grid_size * env->grid_size)); // Base step penalty.
+ int prev_on = env->lights_on;
+ if (atn < 0 || atn >= num_cells) {
+ reward -= 0.5f; // Invalid action penalty.
+ } else {
+ if (atn == env->last_action) {
+ reward -= 0.03f; // Penalty for pressing the same cell twice in a row.
+ } else if (atn == env->prev_action) {
+ reward -= 0.02f; // Penalty for 2-step loop (A,B,A).
+ }
+ if (env->client != NULL) {
+ env->client->cursor_row = atn / env->grid_size;
+ env->client->cursor_col = atn % env->grid_size;
+ }
+ step_grid(env, atn);
+ env->prev_action = env->last_action;
+ env->last_action = atn;
+ int next_on = env->lights_on;
+ reward += 0.005f * (float)(prev_on - next_on); // Dense shaping: improve when lights decrease.
+ }
+ env->step_count += 1;
+
+ if (env->lights_on == 0) {
+ reward = 2.0f; // Solved reward.
+ env->ema = 0.85f * env->ema + 0.15f; // Update EMA of steps to solve.
+ env->terminals[0] = 1.0f;
+ } else if (env->client == NULL && env->step_count >= env->max_steps) {
+ reward -= 0.5f; // Timeout penalty during training.
+ env->ema = 0.85f * env->ema; // Decay EMA since we failed to solve.
+ env->terminals[0] = 1.0f;
+ }
+
+ env->rewards[0] = reward;
+ env->episode_return += reward;
+
+ if (env->terminals[0] > 0.0f) {
+ env->log.episode_return += env->episode_return;
+ env->log.episode_length += (float)env->step_count;
+ env->log.n += 1.0f;
+ env->log.perf += (env->lights_on == 0) ? 1.0f : 0.0f;
+ env->log.score += env->episode_return;
+ env->log.scramble_p += env->scramble_prob;
+
+ env->score_ema = 0.9f * env->score_ema + 0.1f * env->episode_return;
+ init_lightsout(env);
+ }
+
+ compute_observations(env);
+}
+
+// Raylib client
+static const Color COLORS[] = {
+ (Color){6, 24, 24, 255},
+ (Color){0, 0, 255, 255},
+ (Color){255, 255, 255, 255}
+};
+
+Client* make_client(int cell_size, int grid_size) {
+ Client* client= (Client*)malloc(sizeof(Client));
+ client->cell_size = cell_size;
+ client->cursor_row = 0;
+ client->cursor_col = 0;
+ InitWindow(grid_size*cell_size, grid_size*cell_size, "PufferLib LightsOut");
+ SetTargetFPS(5);
+ return client;
+}
+
+void c_render(LightsOut* env) {
+ if (IsWindowReady() && (WindowShouldClose() || IsKeyPressed(KEY_ESCAPE))) {
+ c_close(env);
+ exit(0);
+ }
+
+ if (env->client == NULL) {
+ env->client = make_client(env->cell_size, env->grid_size);
+ }
+
+ Client* client = env->client;
+
+ BeginDrawing();
+ ClearBackground(COLORS[0]);
+ int sz = client->cell_size;
+ for (int y = 0; y < env->grid_size; y++) {
+ for (int x = 0; x < env->grid_size; x++){
+ int tile = env->grid[y*env->grid_size + x];
+ if (tile != 0)
+ DrawRectangle(x*sz, y*sz, sz, sz, COLORS[tile]);
+ }
+ }
+ DrawRectangleLinesEx(
+ (Rectangle){client->cursor_col * sz, client->cursor_row * sz, sz, sz},
+ 3.0f,
+ COLORS[2]
+ );
+
+ if (env->terminals[0] > 0.0f) {
+ const char* msg = "Solved";
+ int font_size = 48;
+ int text_w = MeasureText(msg, font_size);
+ int screen_w = env->grid_size * env->cell_size;
+ int screen_h = env->grid_size * env->cell_size;
+
+ DrawRectangle(0, 0, screen_w, screen_h, (Color){0, 0, 0, 120}); // dim overlay
+ DrawText(msg, (screen_w - text_w) / 2, (screen_h - font_size) / 2, font_size, RAYWHITE);
+ }
+
+ EndDrawing();
+}
diff --git a/pufferlib/ocean/matsci/binding.c b/ocean/matsci/binding.c
similarity index 100%
rename from pufferlib/ocean/matsci/binding.c
rename to ocean/matsci/binding.c
diff --git a/pufferlib/ocean/matsci/matsci.c b/ocean/matsci/matsci.c
similarity index 100%
rename from pufferlib/ocean/matsci/matsci.c
rename to ocean/matsci/matsci.c
diff --git a/pufferlib/ocean/matsci/matsci.h b/ocean/matsci/matsci.h
similarity index 100%
rename from pufferlib/ocean/matsci/matsci.h
rename to ocean/matsci/matsci.h
diff --git a/ocean/maze/binding.c b/ocean/maze/binding.c
new file mode 100644
index 0000000000..0abe8615db
--- /dev/null
+++ b/ocean/maze/binding.c
@@ -0,0 +1,94 @@
+#include "maze.h"
+#define OBS_SIZE 121
+#define NUM_ATNS 1
+#define ACT_SIZES {5}
+#define OBS_TENSOR_T ByteTensor
+
+#define MY_VEC_INIT
+#define MY_VEC_CLOSE
+#define Env Grid
+#include "vecenv.h"
+
+Env* my_vec_init(int* num_envs_out, int* buffer_env_starts, int* buffer_env_counts,
+ Dict* vec_kwargs, Dict* env_kwargs) {
+ int total_agents = (int)dict_get(vec_kwargs, "total_agents")->value;
+ int num_buffers = (int)dict_get(vec_kwargs, "num_buffers")->value;
+ int agents_per_buffer = total_agents / num_buffers;
+ int num_envs = total_agents;
+
+ int max_size = MAX_SIZE;
+ int num_maps = (int)dict_get(env_kwargs, "num_maps")->value;
+ int map_size = (int)dict_get(env_kwargs, "map_size")->value;
+
+ if (max_size <= 5) {
+ *num_envs_out = 0;
+ return NULL;
+ }
+
+ // Generate maze levels (shared across all envs)
+ State* levels = calloc(num_maps, sizeof(State));
+
+ unsigned int map_rng = 42;
+ for (int i = 0; i < num_maps; i++) {
+ int sz = map_size;
+ if (map_size == -1) {
+ sz = 5 + (rand_r(&map_rng) % (max_size - 5));
+ }
+
+ if (sz % 2 == 0) {
+ sz -= 1;
+ }
+
+ State* level = &levels[i];
+ level->width = sz;
+ level->height = sz;
+
+ float difficulty = (float)rand_r(&map_rng) / (float)(RAND_MAX);
+ create_maze_level(level, difficulty, i);
+ }
+
+ // Allocate all environments
+ Env* envs = (Env*)calloc(num_envs, sizeof(Env));
+
+ int buf = 0;
+ int buf_agents = 0;
+ buffer_env_starts[0] = 0;
+ buffer_env_counts[0] = 0;
+
+ unsigned int env_rng = 42;
+ for (int i = 0; i < num_envs; i++) {
+ Env* env = &envs[i];
+ env->num_levels = num_maps;
+ env->num_agents = 1;
+ env->levels = levels;
+ env->rng = rand_r(&env_rng);
+
+ buf_agents += env->num_agents;
+ buffer_env_counts[buf]++;
+ if (buf_agents >= agents_per_buffer && buf < num_buffers - 1) {
+ buf++;
+ buffer_env_starts[buf] = i + 1;
+ buffer_env_counts[buf] = 0;
+ buf_agents = 0;
+ }
+ }
+
+ *num_envs_out = num_envs;
+ return envs;
+}
+
+void my_vec_close(Env* envs) {
+ free(envs[0].levels);
+}
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_levels = (int)dict_get(kwargs, "num_maps")->value;
+ env->num_agents = 1;
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+}
diff --git a/ocean/maze/maze.c b/ocean/maze/maze.c
new file mode 100644
index 0000000000..923818a7c3
--- /dev/null
+++ b/ocean/maze/maze.c
@@ -0,0 +1,83 @@
+#include "maze.h"
+#include "puffernet.h"
+
+void demo() {
+ Weights* weights = load_weights("resources/maze/maze_weights.bin");
+ int logit_sizes[1] = {5};
+ PufferNet* net = make_puffernet(weights, 1, 121, 512, 5, logit_sizes, 1);
+
+ int num_maps = 64;
+ int horizon = 256;
+ float speed = 1;
+ int vision = 5;
+ bool discretize = true;
+
+ Grid* env = (Grid*)calloc(1, sizeof(Grid));
+ env->num_agents = 1;
+ env->rng = 73;
+ env->observations = calloc(WINDOW*WINDOW, sizeof(unsigned char));
+ env->actions = calloc(1, sizeof(float));
+ env->rewards = calloc(1, sizeof(float));
+ env->terminals = calloc(1, sizeof(float));
+
+ // Generate maps matching binding.c: random odd sizes, random difficulty
+ State* levels = calloc(num_maps, sizeof(State));
+ unsigned int map_rng = 42;
+ for (int i = 0; i < num_maps; i++) {
+ int sz = 5 + (rand_r(&map_rng) % (MAX_SIZE - 5));
+ if (sz % 2 == 0) sz -= 1;
+ float difficulty = (float)rand_r(&map_rng) / (float)(RAND_MAX);
+ State* level = &levels[i];
+ level->width = sz;
+ level->height = sz;
+ create_maze_level(level, difficulty, i);
+ }
+
+ env->num_levels = num_maps;
+ env->levels = levels;
+
+ c_reset(env);
+ c_render(env);
+ while (!WindowShouldClose()) {
+ env->actions[0] = ATN_PASS;
+ env->actions[0] = ATN_SOUTH;
+ State* s = &env->state;
+
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)){
+ env->actions[0] = ATN_NORTH;
+ } else if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) {
+ env->actions[0] = ATN_SOUTH;
+ } else if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) {
+ s->direction = PI;
+ env->actions[0] = ATN_WEST;
+ } else if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) {
+ s->direction = 0;
+ env->actions[0] = ATN_EAST;
+ } else {
+ env->actions[0] = ATN_PASS;
+ }
+ } else {
+ float obs[121];
+ for (int i = 0; i < 121; i++) obs[i] = env->observations[i];
+ forward_puffernet(net, obs, env->actions);
+ }
+
+ c_step(env);
+ c_render(env);
+ }
+
+ free_puffernet(net);
+ free(weights);
+ free(env->observations);
+ free(env->actions);
+ free(env->rewards);
+ free(env->terminals);
+ c_close(env);
+ free(levels);
+}
+
+int main() {
+ demo();
+ return 0;
+}
diff --git a/ocean/maze/maze.h b/ocean/maze/maze.h
new file mode 100644
index 0000000000..f61aaa004f
--- /dev/null
+++ b/ocean/maze/maze.h
@@ -0,0 +1,422 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include "raylib.h"
+
+#define TWO_PI 2.0*PI
+
+#define ATN_PASS 0
+#define ATN_EAST 1
+#define ATN_NORTH 2
+#define ATN_WEST 3
+#define ATN_SOUTH 4
+#define EMPTY 0
+#define WALL 1
+#define AGENT 2
+#define GOAL 4
+
+#define VISION 5
+#define WINDOW (2*VISION + 1)
+#define MAX_SIZE 47
+
+typedef struct Log Log;
+struct Log {
+ float perf;
+ float score;
+ float episode_return;
+ float episode_length;
+ float n;
+};
+
+typedef struct {
+ int cell_size;
+ int width;
+ int height;
+ Texture2D puffer;
+ float* overlay;
+} Renderer;
+
+typedef struct {
+ int width;
+ int height;
+ int spawn_x;
+ int spawn_y;
+ int x;
+ int y;
+ int direction;
+ unsigned char maze[MAX_SIZE*MAX_SIZE];
+} State;
+
+typedef struct {
+ Renderer* renderer;
+ State* levels;
+ State state;
+ Log log;
+ int num_levels;
+ int num_agents;
+ int tick;
+ unsigned char* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ unsigned int rng;
+} Grid;
+
+void c_close(Grid* env) {}
+
+bool in_bounds(State* s, int y, int c) {
+ return (y >= 0 && y <= s->height && c >= 0 && c <= s->width);
+}
+
+int maze_offset(int y, int x) {
+ return y*MAX_SIZE + x;
+}
+
+void add_log(Grid* env, int idx) {
+ env->log.perf += env->rewards[idx];
+ env->log.score += env->rewards[idx];
+ env->log.episode_return += env->rewards[idx];
+ env->log.episode_length += env->tick;
+ env->log.n += 1.0;
+}
+
+void compute_observations(Grid* env) {
+ memset(env->observations, 0, WINDOW*WINDOW*env->num_agents);
+ State* s = &env->state;
+ for (int agent_idx = 0; agent_idx < env->num_agents; agent_idx++) {
+ int x = s->x;
+ int y = s->y;
+ int start_r = y - VISION;
+ if (start_r < 0) {
+ start_r = 0;
+ }
+
+ int start_c = x - VISION;
+ if (start_c < 0) {
+ start_c = 0;
+ }
+
+ int end_r = y + VISION;
+ if (end_r >= MAX_SIZE) {
+ end_r = MAX_SIZE - 1;
+ }
+
+ int end_c = x + VISION;
+ if (end_c >= MAX_SIZE) {
+ end_c = MAX_SIZE - 1;
+ }
+
+ int obs_offset = agent_idx*WINDOW*WINDOW;
+ for (int r = start_r; r <= end_r; r++) {
+ for (int c = start_c; c <= end_c; c++) {
+ int r_idx = r - y + VISION;
+ int c_idx = c - x + VISION;
+ int obs_adr = obs_offset + r_idx*WINDOW + c_idx;
+ int adr = maze_offset(r, c);
+ env->observations[obs_adr] = s->maze[adr];
+ }
+ }
+ }
+}
+
+void c_reset(Grid* env) {
+ env->tick = 0;
+ int idx = rand_r(&env->rng) % env->num_levels;
+ env->state = env->levels[idx];
+ compute_observations(env);
+}
+
+int move_to(Grid* env, int agent_idx, float y, float x) {
+ if (!in_bounds(&env->state, y, x)) {
+ return 1;
+ }
+
+ State* s = &env->state;
+ int adr = maze_offset(round(y), round(x));
+ int dest = s->maze[adr];
+ if (dest == WALL) {
+ return 1;
+ } else if (dest == GOAL) {
+ env->rewards[agent_idx] = 1.0;
+ env->terminals[agent_idx] = 1.0f;
+ add_log(env, agent_idx);
+ }
+
+ int start_adr = maze_offset(s->y, s->x);
+ s->maze[start_adr] = EMPTY;
+ s->maze[adr] = AGENT;
+ s->y = y;
+ s->x = x;
+ return 0;
+}
+
+void c_step(Grid* env) {
+ env->terminals[0] = 0.0f;
+ env->rewards[0] = 0.0f;
+
+ State* s = &env->state;
+ env->tick++;
+
+ int atn = env->actions[0];
+ int direction = s->direction;
+ if (atn != ATN_PASS) {
+ direction = atn;
+ }
+
+ int x = s->x;
+ int y = s->y;
+ int dest_x = x;
+ int dest_y = y;
+ if (direction == ATN_EAST) {
+ dest_x = x + 1;
+ } else if (direction == ATN_NORTH) {
+ dest_y = y - 1;
+ } else if (direction == ATN_WEST) {
+ dest_x = x - 1;
+ } else if (direction == ATN_SOUTH) {
+ dest_y = y + 1;
+ }
+ if (in_bounds(&env->state, dest_y, dest_x)) {
+ int err = move_to(env, 0, dest_y, dest_x);
+ }
+
+ compute_observations(env);
+
+ if (env->tick >= 2*s->width*s->height) {
+ env->terminals[0] = 1.0f;
+ add_log(env, 0);
+ }
+
+ if (env->terminals[0]) {
+ c_reset(env);
+ int idx = rand_r(&env->rng) % env->num_levels;
+ env->state = env->levels[idx];
+ compute_observations(env);
+ }
+}
+
+Renderer* init_renderer(int cell_size, int width, int height) {
+ Renderer* renderer = (Renderer*)calloc(1, sizeof(Renderer));
+ renderer->cell_size = cell_size;
+ renderer->width = width;
+ renderer->height = height;
+
+ renderer->overlay = (float*)calloc(width*height, sizeof(float));
+
+ InitWindow(width*cell_size, height*cell_size, "PufferLib Grid");
+ SetTargetFPS(60);
+
+ renderer->puffer = LoadTexture("resources/shared/puffers_128.png");
+ return renderer;
+}
+
+void clear_overlay(Renderer* renderer) {
+ memset(renderer->overlay, 0, renderer->width*renderer->height*sizeof(float));
+}
+
+void close_renderer(Renderer* renderer) {
+ CloseWindow();
+ free(renderer->overlay);
+ free(renderer);
+}
+
+void c_render(Grid* env) {
+ float overlay = 0.0;
+ if (env->renderer == NULL) {
+ env->renderer = init_renderer(16, MAX_SIZE, MAX_SIZE);
+ }
+ Renderer* renderer = env->renderer;
+
+ if (IsKeyDown(KEY_ESCAPE)) {
+ exit(0);
+ }
+
+ State* s = &env->state;
+ int r = s->y;
+ int c = s->x;
+ int adr = maze_offset(r, c);
+
+ BeginDrawing();
+ ClearBackground((Color){6, 24, 24, 255});
+
+ int ts = renderer->cell_size;
+ for (int r = 0; r < s->height; r++) {
+ for (int c = 0; c < s->width; c++){
+ adr = maze_offset(r, c);
+ int tile = s->maze[adr];
+ if (tile == EMPTY) {
+ continue;
+ overlay = renderer->overlay[adr];
+ if (overlay == 0) {
+ continue;
+ }
+ Color color;
+ if (overlay < 0) {
+ overlay = -fmaxf(-1.0, overlay);
+ color = (Color){255.0*overlay, 0, 0, 255};
+ } else {
+ overlay = fminf(1.0, overlay);
+ color = (Color){0, 255.0*overlay, 0, 255};
+ }
+ DrawRectangle(c*ts, r*ts, ts, ts, color);
+ }
+
+ Color color;
+ if (tile == WALL) {
+ color = (Color){128, 128, 128, 255};
+ } else if (tile == GOAL) {
+ color = GREEN;
+ } else {
+ continue;
+ }
+
+ DrawRectangle(c*ts, r*ts, ts, ts, color);
+ }
+ }
+
+ float y = s->y;
+ float x = s->x;
+ Rectangle source_rect = (Rectangle){0, 0, 128, 128};
+ Rectangle dest_rect = (Rectangle){x*ts, y*ts, ts, ts};
+ DrawTexturePro(renderer->puffer, source_rect, dest_rect,
+ (Vector2){0, 0}, 0, WHITE);
+
+ EndDrawing();
+}
+
+void generate_growing_tree_maze(unsigned char* maze,
+ int width, int height, int max_size, float difficulty, int seed) {
+ unsigned int rng = seed;
+ int dx[4] = {-1, 0, 1, 0};
+ int dy[4] = {0, 1, 0, -1};
+ int dirs[4] = {0, 1, 2, 3};
+ int cells[2*width*height];
+ int num_cells = 1;
+
+ bool visited[width*height];
+ memset(visited, false, width*height);
+
+ memset(maze, WALL, max_size*height);
+ for (int r = 0; r < height; r++) {
+ for (int c = 0; c < width; c++) {
+ int adr = r*max_size + c;
+ if (r % 2 == 1 && c % 2 == 1) {
+ maze[adr] = EMPTY;
+ }
+ }
+ }
+
+ int x_init = rand_r(&rng) % (width - 1);
+ int y_init = rand_r(&rng) % (height - 1);
+
+ if (x_init % 2 == 0) {
+ x_init++;
+ }
+ if (y_init % 2 == 0) {
+ y_init++;
+ }
+
+ int adr = y_init*height + x_init;
+ visited[adr] = true;
+ cells[0] = x_init;
+ cells[1] = y_init;
+
+ while (num_cells > 0) {
+ if (rand_r(&rng) % 1000 > 1000*difficulty) {
+ int i = rand_r(&rng) % num_cells;
+ int tmp_x = cells[2*num_cells - 2];
+ int tmp_y = cells[2*num_cells - 1];
+ cells[2*num_cells - 2] = cells[2*i];
+ cells[2*num_cells - 1] = cells[2*i + 1];
+ cells[2*i] = tmp_x;
+ cells[2*i + 1] = tmp_y;
+
+ }
+
+ int x = cells[2*num_cells - 2];
+ int y = cells[2*num_cells - 1];
+
+ int nx, ny;
+
+ // In-place direction shuffle
+ for (int i = 0; i < 4; i++) {
+ int ii = i + rand_r(&rng) % (4 - i);
+ int tmp = dirs[i];
+ dirs[i] = dirs[ii];
+ dirs[ii] = tmp;
+ }
+
+ bool made_path = false;
+ for (int dir_i = 0; dir_i < 4; dir_i++) {
+ int dir = dirs[dir_i];
+ nx = x + 2*dx[dir];
+ ny = y + 2*dy[dir];
+
+ if (nx <= 0 || nx >= width-1 || ny <= 0 || ny >= height-1) {
+ continue;
+ }
+
+ int visit_adr = ny*width + nx;
+ if (visited[visit_adr]) {
+ continue;
+ }
+
+ visited[visit_adr] = true;
+ cells[2*num_cells] = nx;
+ cells[2*num_cells + 1] = ny;
+
+ nx = x + dx[dir];
+ ny = y + dy[dir];
+
+ int adr = ny*max_size + nx;
+ maze[adr] = EMPTY;
+ num_cells++;
+
+ made_path = true;
+ break;
+ }
+ if (!made_path) {
+ num_cells--;
+ }
+ }
+}
+
+void make_border(State* s) {
+ for (int r = 0; r < s->height; r++) {
+ int adr = maze_offset(r, 0);
+ s->maze[adr] = WALL;
+ adr = maze_offset(r, s->width-1);
+ s->maze[adr] = WALL;
+ }
+ for (int c = 0; c < s->width; c++) {
+ int adr = maze_offset(0, c);
+ s->maze[adr] = WALL;
+ adr = maze_offset(s->height-1, c);
+ s->maze[adr] = WALL;
+ }
+}
+
+void spawn_agent(State* s, int idx, int x, int y) {
+ int spawn_y = y;
+ int spawn_x = x;
+ assert(in_bounds(s, spawn_y, spawn_x));
+ int adr = maze_offset(spawn_y, spawn_x);
+ assert(s->maze[adr] == EMPTY);
+ s->spawn_y = spawn_y;
+ s->spawn_x = spawn_x;
+ s->y = spawn_y;
+ s->x = spawn_x;
+ s->maze[adr] = AGENT;
+ s->direction = 0;
+}
+
+void create_maze_level(State* s, float difficulty, int seed) {
+ generate_growing_tree_maze(s->maze, s->width, s->height, MAX_SIZE, difficulty, seed);
+ make_border(s);
+ spawn_agent(s, 0, 1, 1);
+ int goal_adr = maze_offset(s->height - 2, s->width - 2);
+ s->maze[goal_adr] = GOAL;
+}
diff --git a/pufferlib/ocean/memory/binding.c b/ocean/memory/binding.c
similarity index 100%
rename from pufferlib/ocean/memory/binding.c
rename to ocean/memory/binding.c
diff --git a/pufferlib/ocean/memory/memory.c b/ocean/memory/memory.c
similarity index 100%
rename from pufferlib/ocean/memory/memory.c
rename to ocean/memory/memory.c
diff --git a/pufferlib/ocean/memory/memory.h b/ocean/memory/memory.h
similarity index 100%
rename from pufferlib/ocean/memory/memory.h
rename to ocean/memory/memory.h
diff --git a/ocean/minimal/binding.c b/ocean/minimal/binding.c
new file mode 100644
index 0000000000..6054069686
--- /dev/null
+++ b/ocean/minimal/binding.c
@@ -0,0 +1,25 @@
+// Include your .h first
+#include "minimal.h"
+
+// Required metadata
+#define OBS_SIZE (2 + 4*(AGENTS+TARGETS))
+#define NUM_ATNS 2
+#define ACT_SIZES {9, 5}
+#define OBS_TENSOR_T FloatTensor
+
+// You can macro your struct and function names here
+#define Env Env
+
+// Include the vecenv.h here
+#include "vecenv.h"
+
+// Include any custom init logic here
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = AGENTS;
+}
+
+// Specify log fields to export during training
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+}
diff --git a/ocean/minimal/minimal.c b/ocean/minimal/minimal.c
new file mode 100644
index 0000000000..d279cfadc7
--- /dev/null
+++ b/ocean/minimal/minimal.c
@@ -0,0 +1,41 @@
+#include "minimal.h"
+#include "puffernet.h"
+
+int main() {
+ Env env = {.num_agents = AGENTS};
+ int num_obs = 2 + 4*(AGENTS + TARGETS);
+ env.observations = calloc(AGENTS*num_obs, sizeof(float));
+ env.actions = calloc(2*AGENTS, sizeof(float));
+ env.rewards = calloc(AGENTS, sizeof(float));
+ env.terminals = calloc(AGENTS, sizeof(float));
+
+ // Works directly with your .bin training checkpoints
+ Weights* weights = load_weights("resources/minimal/minimal_weights.bin");
+ int logit_sizes[2] = {9, 5};
+ PufferNet* net = make_puffernet(weights, env.num_agents, num_obs, 128, 4, logit_sizes, 2);
+
+ c_reset(&env);
+ c_render(&env);
+ while (!WindowShouldClose()) {
+ forward_puffernet(net, env.observations, env.actions);
+ // Always add a human control mode when possible
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ env.actions[0] = 4;
+ env.actions[1] = 2;
+ if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) env.actions[0] = 0;
+ if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) env.actions[0] = 8;
+ if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) env.actions[1] = 4;
+ if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) env.actions[1] = 0;
+ }
+ c_step(&env);
+ c_render(&env);
+ }
+
+ free_puffernet(net);
+ free(weights);
+ free(env.observations);
+ free(env.actions);
+ free(env.rewards);
+ free(env.terminals);
+ c_close(&env);
+}
diff --git a/ocean/minimal/minimal.h b/ocean/minimal/minimal.h
new file mode 100644
index 0000000000..9b01256083
--- /dev/null
+++ b/ocean/minimal/minimal.h
@@ -0,0 +1,112 @@
+// A sample multiagent coordination env. Star PufferLib on GitHub to support!
+// Don't one-line structs/fns/ifs/vars in PRs. This fits in a screenshot.
+#include
+#include
+#include "raylib.h"
+
+#define AGENTS 8
+#define TARGETS 8
+const int WIDTH = 1080, HEIGHT = 720, COOLDOWN = 30, TYPES = 4;
+const float SPEED = 20.0f, MIN_TICKS = COOLDOWN*AGENTS/(float)TARGETS;
+float clip(float val, float min, float max) { return fmaxf(fminf(val, max), min); }
+
+// Required struct. Floats only, n last
+typedef struct { float perf, score, n; } Log;
+typedef struct { float x, y, heading, speed, type, ticks, cooldown; } Entity;
+typedef struct {
+ Log log; int num_agents; unsigned int rng; // Required
+ float *observations, *actions, *rewards, *terminals; // Required
+ Entity entities[AGENTS + TARGETS]; Texture2D sprites;
+} Env; // Required: An env struct. You can macro the name in binding.c
+
+void compute_observations(Env* env) {
+ int idx = 0; float* obs = env->observations;
+ for (int a=0; aentities[a];
+ obs[idx++] = agent->heading / (2*PI);
+ obs[idx++] = agent->speed / SPEED;
+ for (int o=0; oentities[o];
+ obs[idx++] = (other->x - agent->x) / WIDTH;
+ obs[idx++] = (other->y - agent->y) / HEIGHT;
+ obs[idx++] = other->cooldown / COOLDOWN;
+ obs[idx++] = other->type == agent->type ? 1 : 0;
+ }
+ }
+}
+
+void c_reset(Env* env) {
+ for (int i=0; ientities[i];
+ entity->x = 16 + rand_r(&env->rng)%(WIDTH-16);
+ entity->y = 16 + rand_r(&env->rng)%(HEIGHT-16);
+ entity->type = i % TYPES;
+ entity->ticks = 0;
+ }
+ compute_observations(env);
+}
+
+void c_step(Env* env) {
+ for (int i=0; ientities[i];
+ agent->ticks += 1;
+ agent->heading += (env->actions[2*i] - 4.0f)/12.0f;
+ if (agent->heading < -PI) agent->heading += 2*PI;
+ if (agent->heading > PI) agent->heading -= 2*PI;
+ float speed = agent->speed;
+ agent->speed = clip(speed + (env->actions[2*i + 1] - 2.0f), 0.0f, SPEED);
+ agent->x = clip(agent->x + speed*cosf(agent->heading), 16, WIDTH-16);
+ agent->y = clip(agent->y + speed*sinf(agent->heading), 16, HEIGHT-16);
+ for (int t=0; tentities[AGENTS + t];
+ if (target->cooldown > 0 || target->type != agent->type
+ || fabsf(target->x - agent->x) > 32
+ || fabsf(target->y - agent->y) > 32) continue;
+ target->cooldown = COOLDOWN;
+ if (rand_r(&env->rng) % 10 == 0) {
+ target->x = 16 + rand_r(&env->rng)%(WIDTH-16);
+ target->y = 16 + rand_r(&env->rng)%(HEIGHT-16);
+ }
+ env->rewards[i] = 1.0f;
+ env->log.perf += clip(MIN_TICKS/agent->ticks, 0.0f, 1.0f);
+ env->log.score -= agent->ticks;
+ env->log.n++;
+ agent->type = ((int)agent->type + 1) % TYPES;
+ agent->ticks = 0;
+ break;
+ }
+ }
+ for (int t=0; tentities[AGENTS + t];
+ target->cooldown = fmaxf(target->cooldown - 1, 0);
+ }
+ compute_observations(env);
+}
+
+void c_render(Env* env) {
+ if (!IsWindowReady()) {
+ InitWindow(WIDTH, HEIGHT, "PufferLib Env"); SetTargetFPS(30);
+ env->sprites = LoadTexture("resources/shared/puffers.png");
+ }
+ if (IsKeyDown(KEY_ESCAPE)) exit(0);
+ BeginDrawing();
+ ClearBackground((Color){6, 24, 24, 255});
+ for (int i=0; ientities[i];
+ int sz = i < AGENTS ? 32 : 64, y = i < AGENTS ? 576 : 512;
+ if (i < AGENTS && (entity->heading < -PI/2 || entity->heading > PI/2)) y += 32;
+ DrawTexturePro(env->sprites,
+ (Rectangle){sz*entity->type, y, sz, sz},
+ (Rectangle){entity->x - sz/2, entity->y - sz/2, sz, sz},
+ (Vector2){0, 0}, 0, entity->cooldown > 0 ? DARKGRAY: WHITE
+ );
+ }
+ EndDrawing();
+}
+
+void c_close(Env* env) {
+ if (IsWindowReady()) {
+ UnloadTexture(env->sprites);
+ CloseWindow();
+ }
+}
diff --git a/ocean/moba/binding.c b/ocean/moba/binding.c
new file mode 100644
index 0000000000..6a73652fae
--- /dev/null
+++ b/ocean/moba/binding.c
@@ -0,0 +1,121 @@
+#include "moba.h"
+#define OBS_SIZE 510
+#define NUM_ATNS 6
+#define ACT_SIZES {7, 7, 3, 2, 2, 2}
+#define OBS_TENSOR_T ByteTensor
+
+#define MY_VEC_INIT
+#define MY_VEC_CLOSE
+#define Env MOBA
+#include "vecenv.h"
+
+void my_vec_close(Env* envs) {
+ free(envs[0].ai_paths);
+}
+
+Env* my_vec_init(int* num_envs_out, int* buffer_env_starts, int* buffer_env_counts,
+ Dict* vec_kwargs, Dict* env_kwargs) {
+ int num_envs = (int)dict_get(vec_kwargs, "total_agents")->value;
+ int num_buffers = (int)dict_get(vec_kwargs, "num_buffers")->value;
+
+ int vision_range = (int)dict_get(env_kwargs, "vision_range")->value;
+ float agent_speed = dict_get(env_kwargs, "agent_speed")->value;
+ float reward_death = dict_get(env_kwargs, "reward_death")->value;
+ float reward_xp = dict_get(env_kwargs, "reward_xp")->value;
+ float reward_distance = dict_get(env_kwargs, "reward_distance")->value;
+ float reward_tower = dict_get(env_kwargs, "reward_tower")->value;
+ int script_opponents = (int)dict_get(env_kwargs, "script_opponents")->value;
+
+
+
+ // ai_paths (256 MB) is shared — same map, so BFS results are identical across envs.
+ // ai_path_buffer (1.5 MB) must be per-env: bfs() uses it as a scratch queue
+ // starting from index 0 on every call, so concurrent BFS calls corrupt each other.
+ unsigned char* ai_paths = calloc(128*128*128*128, sizeof(unsigned char));
+ for (int i = 0; i < 128*128*128*128; i++) {
+ ai_paths[i] = 255;
+ }
+
+ // Calculate agents per env based on script_opponents
+ int agents_per_env = script_opponents ? 5 : 10;
+ int total_envs = num_envs / agents_per_env;
+
+ Env* envs = (Env*)calloc(total_envs, sizeof(Env));
+
+ for (int i = 0; i < total_envs; i++) {
+ Env* env = &envs[i];
+ env->num_agents = agents_per_env;
+ env->vision_range = vision_range;
+ env->agent_speed = agent_speed;
+ env->reward_death = reward_death;
+ env->reward_xp = reward_xp;
+ env->reward_distance = reward_distance;
+ env->reward_tower = reward_tower;
+ env->script_opponents = script_opponents;
+ env->ai_path_buffer = calloc(3*8*128*128, sizeof(int));
+ env->ai_paths = ai_paths;
+ init_moba(env, game_map_npy);
+ }
+
+ int agents_per_buffer = num_envs / num_buffers;
+ int buf = 0;
+ int buf_agents = 0;
+ buffer_env_starts[0] = 0;
+ buffer_env_counts[0] = 0;
+ for (int i = 0; i < total_envs; i++) {
+ buf_agents += envs[i].num_agents;
+ buffer_env_counts[buf]++;
+ if (buf_agents >= agents_per_buffer && buf < num_buffers - 1) {
+ buf++;
+ buffer_env_starts[buf] = i + 1;
+ buffer_env_counts[buf] = 0;
+ buf_agents = 0;
+ }
+ }
+
+ *num_envs_out = total_envs;
+ return envs;
+}
+
+void my_init(Env* env, Dict* kwargs) {
+ env->vision_range = dict_get(kwargs, "vision_range")->value;
+ env->agent_speed = dict_get(kwargs, "agent_speed")->value;
+ env->reward_death = dict_get(kwargs, "reward_death")->value;
+ env->reward_xp = dict_get(kwargs, "reward_xp")->value;
+ env->reward_distance = dict_get(kwargs, "reward_distance")->value;
+ env->reward_tower = dict_get(kwargs, "reward_tower")->value;
+ env->script_opponents = dict_get(kwargs, "script_opponents")->value;
+ env->num_agents = env->script_opponents ? 5 : 10;
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "radiant_victory", log->radiant_victory);
+ dict_set(out, "dire_victory", log->dire_victory);
+ dict_set(out, "radiant_level", log->radiant_level);
+ dict_set(out, "dire_level", log->dire_level);
+ dict_set(out, "radiant_towers_alive", log->radiant_towers_alive);
+ dict_set(out, "dire_towers_alive", log->dire_towers_alive);
+ dict_set(out, "radiant_support_episode_return", log->radiant_support_episode_return);
+ dict_set(out, "radiant_support_reward_death", log->radiant_support_reward_death);
+ dict_set(out, "radiant_support_reward_xp", log->radiant_support_reward_xp);
+ dict_set(out, "radiant_support_reward_distance", log->radiant_support_reward_distance);
+ dict_set(out, "radiant_support_reward_tower", log->radiant_support_reward_tower);
+ dict_set(out, "radiant_support_level", log->radiant_support_level);
+ dict_set(out, "radiant_support_kills", log->radiant_support_kills);
+ dict_set(out, "radiant_support_deaths", log->radiant_support_deaths);
+ dict_set(out, "radiant_support_damage_dealt", log->radiant_support_damage_dealt);
+ dict_set(out, "radiant_support_damage_received", log->radiant_support_damage_received);
+ dict_set(out, "radiant_support_healing_dealt", log->radiant_support_healing_dealt);
+ dict_set(out, "radiant_support_healing_received", log->radiant_support_healing_received);
+ dict_set(out, "radiant_support_creeps_killed", log->radiant_support_creeps_killed);
+ dict_set(out, "radiant_support_neutrals_killed", log->radiant_support_neutrals_killed);
+ dict_set(out, "radiant_support_towers_killed", log->radiant_support_towers_killed);
+ dict_set(out, "radiant_support_usage_auto", log->radiant_support_usage_auto);
+ dict_set(out, "radiant_support_usage_q", log->radiant_support_usage_q);
+ dict_set(out, "radiant_support_usage_w", log->radiant_support_usage_w);
+ dict_set(out, "radiant_support_usage_e", log->radiant_support_usage_e);
+}
diff --git a/pufferlib/ocean/moba/game_map.h b/ocean/moba/game_map.h
similarity index 100%
rename from pufferlib/ocean/moba/game_map.h
rename to ocean/moba/game_map.h
diff --git a/ocean/moba/moba.c b/ocean/moba/moba.c
new file mode 100644
index 0000000000..721d7eada5
--- /dev/null
+++ b/ocean/moba/moba.c
@@ -0,0 +1,43 @@
+#include "moba.h"
+#include "puffernet.h"
+
+void demo() {
+ // encoder(64x510=32640) + decoder(24x64=1536) + 5x mingru(192x64=12288) = 95616
+ Weights* weights = load_weights("resources/moba/moba_weights.bin");
+
+ int logit_sizes[6] = {7, 7, 3, 2, 2, 2};
+ PufferNet* net = make_puffernet(weights, 5, 510, 64, 5, logit_sizes, 6);
+
+ MOBA env = {
+ .vision_range = 5,
+ .agent_speed = 1.0,
+ .reward_death = -0.163764,
+ .reward_xp = 0.00665677,
+ .reward_distance = 0,
+ .reward_tower = 0.642119,
+ .script_opponents = true,
+ };
+ allocate_moba(&env);
+ c_reset(&env);
+
+ float obs_f[5 * 510];
+ c_render(&env);
+ int frame = 1;
+ while (!WindowShouldClose()) {
+ if (frame % 12 == 0) {
+ for (int i = 0; i < 5 * 510; i++)
+ obs_f[i] = (float)env.observations[i];
+ forward_puffernet(net, obs_f, env.actions);
+ c_step(&env);
+ }
+ c_render(&env);
+ frame = (frame + 1) % 12;
+ }
+ free_puffernet(net);
+ free(weights);
+ free_allocated_moba(&env);
+}
+
+int main() {
+ demo();
+}
diff --git a/pufferlib/ocean/moba/moba.h b/ocean/moba/moba.h
similarity index 96%
rename from pufferlib/ocean/moba/moba.h
rename to ocean/moba/moba.h
index bfe399f83c..532d08cc97 100644
--- a/pufferlib/ocean/moba/moba.h
+++ b/ocean/moba/moba.h
@@ -4,7 +4,8 @@
#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"
@@ -341,7 +342,6 @@ struct MOBA {
GameRenderer* client;
int vision_range;
float agent_speed;
- bool discretize;
bool script_opponents;
int obs_size;
int creep_idx;
@@ -353,10 +353,11 @@ struct MOBA {
unsigned char* ai_paths;
int* ai_path_buffer;
unsigned char* observations;
- int* actions;
+ float* actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
unsigned char* truncations;
+ int num_agents;
Entity* entities;
Reward* reward_components;
Log log;
@@ -381,6 +382,7 @@ struct MOBA {
void add_log(MOBA* env, int radiant_victory, int dire_victory) {
Log* log = &env->log;
+ int num_agents = env->script_opponents ? 5 : NUM_PLAYERS;
log->n += 1;
log->score += radiant_victory;
log->perf += radiant_victory;
@@ -402,6 +404,15 @@ void add_log(MOBA* env, int radiant_victory, int dire_victory) {
}
}
+ for (int i = 0; i < num_agents; i++) {
+ PlayerLog* pl = &env->player_logs[i];
+ log->episode_return += pl->episode_return;
+ log->reward_death += pl->reward_death;
+ log->reward_xp += pl->reward_xp;
+ log->reward_distance += pl->reward_distance;
+ log->reward_tower += pl->reward_tower;
+ }
+
PlayerLog* radiant_support = &env->player_logs[0];
log->radiant_support_episode_return = radiant_support->episode_return;
log->radiant_support_reward_death = radiant_support->reward_death;
@@ -425,24 +436,24 @@ void add_log(MOBA* env, int radiant_victory, int dire_victory) {
}
void c_close(MOBA* env) {
+ free(env->entities);
free(env->reward_components);
free(env->map->grid);
+ free(env->map->pids);
free(env->map);
free(env->orig_grid);
free(env->rng->rng);
free(env->rng);
+ free(env->ai_path_buffer);
}
void free_allocated_moba(MOBA* env) {
free(env->rewards);
- free(env->map->pids);
- free(env->ai_path_buffer);
free(env->ai_paths);
free(env->observations);
free(env->actions);
free(env->terminals);
free(env->truncations);
- free(env->entities);
c_close(env);
}
@@ -466,30 +477,30 @@ void compute_observations(MOBA* env) {
int x = player->x;
// TODO: Add bounds debug checks asserts
- obs_extra[0] = 2*x;
- obs_extra[1] = 2*y;
- obs_extra[2] = 255*player->level/30.0;
- obs_extra[3] = 255*player->health/player->max_health;
- obs_extra[4] = 255*player->mana/player->max_mana;
- obs_extra[5] = player->damage / 4.0;
- obs_extra[6] = 100*player->move_speed;
- obs_extra[7] = player->move_modifier*100;
- obs_extra[8] = 2*player->stun_timer;
- obs_extra[9] = 2*player->move_timer;
- obs_extra[10] = 2*player->q_timer;
- obs_extra[11] = 2*player->w_timer;
- obs_extra[12] = 2*player->e_timer;
- obs_extra[13] = 50*player->basic_attack_timer;
- obs_extra[14] = 50*player->basic_attack_cd;
- obs_extra[15] = 255*player->is_hit;
- obs_extra[16] = 255*player->team;
- obs_extra[17 + player->hero_type] = 255;
+ obs_extra[0] = x;
+ obs_extra[1] = y;
+ obs_extra[2] = player->level;
+ obs_extra[3] = 10*player->health/player->max_health;
+ obs_extra[4] = 10*player->mana/player->max_mana;
+ obs_extra[5] = player->damage / 50.0;
+ obs_extra[6] = player->move_speed;
+ obs_extra[7] = player->move_modifier;
+ obs_extra[8] = player->stun_timer;
+ obs_extra[9] = player->move_timer;
+ obs_extra[10] = player->q_timer;
+ obs_extra[11] = player->w_timer;
+ obs_extra[12] = player->e_timer;
+ obs_extra[13] = player->basic_attack_timer;
+ obs_extra[14] = player->basic_attack_cd;
+ obs_extra[15] = player->is_hit;
+ obs_extra[16] = player->team;
+ obs_extra[17 + player->hero_type] = 1;
// Assumes scaled between -1 and 1, else overflows
- obs_extra[22] = (reward->death == 0) ? 0 : 255;
- obs_extra[23] = (reward->xp == 0) ? 0 : 255;
- obs_extra[24] = (reward->distance == 0) ? 0 : 255;
- obs_extra[25] = (reward->tower == 0) ? 0 : 255;
+ obs_extra[22] = (reward->death == 0) ? 0 : 1;
+ obs_extra[23] = (reward->xp == 0) ? 0 : 1;
+ obs_extra[24] = (reward->distance == 0) ? 0 : 1;
+ obs_extra[25] = (reward->tower == 0) ? 0 : 1;
for (int dy = -vis; dy <= vis; dy++) {
for (int dx = -vis; dx <= vis; dx++) {
@@ -511,11 +522,11 @@ void compute_observations(MOBA* env) {
continue;
Entity* target = &env->entities[target_pid];
- obs_map[map_idx+1] = 255*target->health/target->max_health;
+ obs_map[map_idx+1] = 10*target->health/target->max_health;
if (target->max_mana > 0) { // Towers do not have mana
- obs_map[map_idx+2] = 255*target->mana/target->max_mana;
+ obs_map[map_idx+2] = 10*target->mana/target->max_mana;
}
- obs_map[map_idx+3] = target->level/30.0;
+ obs_map[map_idx+3] = target->level;
}
}
}
@@ -1489,21 +1500,21 @@ void step_players(MOBA* env) {
}
*/
} else {
- int (*actions)[6] = (int(*)[6])env->actions;
+ float (*actions)[6] = (float(*)[6])env->actions;
//float vel_y = (actions[pid][0] > 0) ? 1 : -1;
//float vel_x = (actions[pid][1] > 0) ? 1 : -1;
- float vel_y = actions[pid][0] / 300.0f;
- float vel_x = actions[pid][1] / 300.0f;
+ float vel_y = (actions[pid][0] - 3.0f) / 3.0f;
+ float vel_x = (actions[pid][1] - 3.0f) / 3.0f;
float mag = sqrtf(vel_y*vel_y + vel_x*vel_x);
if (mag > 1) {
vel_y /= mag;
vel_x /= mag;
}
- int attack_target = actions[pid][2];
- bool use_q = actions[pid][3];
- bool use_w = actions[pid][4];
- bool use_e = actions[pid][5];
+ int attack_target = (int)actions[pid][2];
+ bool use_q = (int)actions[pid][3];
+ bool use_w = (int)actions[pid][4];
+ bool use_e = (int)actions[pid][5];
if (attack_target == 1 || attack_target == 0) {
// Scan everything
@@ -1788,12 +1799,11 @@ MOBA* allocate_moba(MOBA* env) {
// TODO: Don't hardcode sizes
int agents = (env->script_opponents) ? NUM_PLAYERS/2 : NUM_PLAYERS;
env->observations = calloc(agents*(11*11*4 + 26), sizeof(unsigned char));
- env->actions = calloc(agents*6, sizeof(int));
+ env->actions = calloc(agents*6, sizeof(float));
env->rewards = calloc(agents, sizeof(float));
- env->terminals = calloc(agents, sizeof(unsigned char));
+ env->terminals = calloc(agents, sizeof(float));
env->truncations = calloc(agents, sizeof(unsigned char));
- unsigned char* game_map_npy = read_file("resources/moba/game_map.npy");
env->ai_path_buffer = calloc(3*8*128*128, sizeof(int));
env->ai_paths = calloc(128*128*128*128, sizeof(unsigned char));
for (int i = 0; i < 128*128*128*128; i++) {
@@ -1801,7 +1811,6 @@ MOBA* allocate_moba(MOBA* env) {
}
init_moba(env, game_map_npy);
- free(game_map_npy);
return env;
}
@@ -2115,9 +2124,7 @@ GameRenderer* init_game_renderer(int cell_size, int width, int height) {
return renderer;
}
-//def render(self, grid, pids, entities, obs_players, actions, discretize, frames):
#define FRAMES 12
-
void draw_bars(Entity* entity, int x, int y, int width, int height, bool draw_text) {
float health_bar = entity->health / entity->max_health;
float mana_bar = entity->mana / entity->max_mana;
@@ -2192,16 +2199,16 @@ int c_render(MOBA* env) {
int human = renderer->human_player;
bool HUMAN_CONTROL = IsKeyDown(KEY_LEFT_SHIFT);
- int (*actions)[6] = (int(*)[6])env->actions;
+ float (*actions)[6] = (float(*)[6])env->actions;
// Clears so as to not let the nn spam actions
if (HUMAN_CONTROL && frame % 12 == 0) {
- actions[human][0] = 0;
- actions[human][1] = 0;
- actions[human][2] = 0;
- actions[human][3] = 0;
- actions[human][4] = 0;
- actions[human][5] = 0;
+ actions[human][0] = 0.0;
+ actions[human][1] = 0.0;
+ actions[human][2] = 0.0;
+ actions[human][3] = 0.0;
+ actions[human][4] = 0.0;
+ actions[human][5] = 0.0;
}
// TODO: better way to null clicks?
@@ -2216,10 +2223,10 @@ int c_render(MOBA* env) {
renderer->last_click_x = -1;
renderer->last_click_y = -1;
}
-
+
if (HUMAN_CONTROL) {
- actions[human][0] = 300*dy;
- actions[human][1] = 300*dx;
+ actions[human][0] = 300.0*dy;
+ actions[human][1] = 300.0*dx;
}
}
if (IsKeyDown(KEY_ESCAPE)) {
@@ -2227,16 +2234,16 @@ int c_render(MOBA* env) {
}
if (HUMAN_CONTROL) {
if (IsKeyDown(KEY_Q) || IsKeyPressed(KEY_Q)) {
- actions[human][3] = 1;
+ actions[human][3] = 1.0;
}
if (IsKeyDown(KEY_W) || IsKeyPressed(KEY_W)) {
- actions[human][4] = 1;
+ actions[human][4] = 1.0;
}
if (IsKeyDown(KEY_E) || IsKeyPressed(KEY_E)) {
- actions[human][5] = 1;
+ actions[human][5] = 1.0;
}
if (IsKeyDown(KEY_LEFT_SHIFT)) {
- actions[human][2] = 2; // Target heroes
+ actions[human][2] = 2.0; // Target heroes
}
}
// Num keys toggle selected player
diff --git a/ocean/nethack/README.md b/ocean/nethack/README.md
new file mode 100644
index 0000000000..7a27c959ff
--- /dev/null
+++ b/ocean/nethack/README.md
@@ -0,0 +1,41 @@
+# NetHack
+
+PufferLib environment for NetHack 3.6.6 over
+[fast-nle](https://github.com/FinlaySanders/fast-nle): 22-verb factored
+action space (verb, item slot, direction), legality masking,
+decomposed-score reward, custom CUDA encoder/decoder (`src/nethack.cu`).
+
+## Setup
+
+```bash
+pip install -e .
+./build.sh nethack # clones + builds vendor/fast-nle, then the training backend
+```
+
+Run from the repo root — the engine finds its data at
+`vendor/fast-nle/build/dat` (override with `NETHACKDIR`).
+
+## Train
+
+```bash
+puffer train nethack
+```
+
+Reward coefficients and hypers live in `config/nethack.ini`.
+
+## Watch a policy
+
+```bash
+./build.sh nethack --fast # builds the ./nethack demo binary
+./nethack # plays resources/nethack/nethack_weights.bin
+NH_WEIGHTS=checkpoints/nethack//.bin ./nethack
+```
+
+`./nethack [steps] [ms_per_frame]` — `0` ms runs headless. Set `NH_SEED`
+to replay a run.
+
+## Test
+
+```bash
+python tests/test_nethack_encoder.py # encoder/decoder gradcheck vs torch
+```
diff --git a/ocean/nethack/binding.c b/ocean/nethack/binding.c
new file mode 100644
index 0000000000..e6b0d596a6
--- /dev/null
+++ b/ocean/nethack/binding.c
@@ -0,0 +1,94 @@
+#include "nethack.h"
+#define OBS_SIZE NETHACK_OBS_SIZE
+#define NUM_ATNS 14
+#define ACT_SIZES {NETHACK_NUM_ACTIONS, \
+ NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, \
+ NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, \
+ NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, \
+ NETHACK_NUM_DIRS}
+#define OBS_TENSOR_T ByteTensor
+#define MY_ACTION_MASK (NETHACK_NUM_ACTIONS + 12 * NETHACK_INV_SLOTS + NETHACK_NUM_DIRS)
+
+#define Env Nethack
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ init(env);
+ env->gold_coef = dict_get(kwargs, "gold_coef")->value;
+ env->exp_coef = dict_get(kwargs, "exp_coef")->value;
+ env->descent_coef = dict_get(kwargs, "descent_coef")->value;
+ env->scout_coef = dict_get(kwargs, "scout_coef")->value;
+ env->xp_coef = dict_get(kwargs, "xp_coef")->value;
+ env->hp_coef = dict_get(kwargs, "hp_coef")->value;
+ env->hunger_coef = dict_get(kwargs, "hunger_coef")->value;
+ env->illegal_penalty = dict_get(kwargs, "illegal_penalty")->value;
+ env->death_penalty = dict_get(kwargs, "death_penalty")->value;
+ env->ac_coef = dict_get(kwargs, "ac_coef")->value;
+ env->heal_coef = dict_get(kwargs, "heal_coef")->value;
+ env->status_coef = dict_get(kwargs, "status_coef")->value;
+}
+
+void my_log(Log* log, Dict* out) {
+ for (int v = 0; v < NETHACK_NUM_ACTIONS; v++)
+ if (NETHACK_VERB_STAT[v]) dict_set(out, NETHACK_VERB_STAT[v], log->verb_uses[v]);
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "valid_moves", log->valid_moves);
+ dict_set(out, "illegal_actions", log->illegal_actions);
+ dict_set(out, "new_tiles", log->new_tiles);
+ dict_set(out, "max_depth", log->max_depth);
+ dict_set(out, "enhances", log->enhances);
+ dict_set(out, "floor_eats", log->floor_eats);
+ dict_set(out, "prayers_low_hp", log->prayers_low_hp);
+ dict_set(out, "prayers_starving", log->prayers_starving);
+ dict_set(out, "burdened_frac", log->burdened_frac);
+ dict_set(out, "damage_taken", log->damage_taken);
+ dict_set(out, "ac", log->ac);
+ dict_set(out, "min_ac", log->min_ac);
+ dict_set(out, "armor_swaps", log->armor_swaps);
+ dict_set(out, "heal_hp", log->heal_hp);
+ dict_set(out, "cures", log->cures);
+ dict_set(out, "game_time", log->game_time);
+ dict_set(out, "max_xp_level", log->max_xp_level);
+ dict_set(out, "death_combat", log->death_combat);
+ dict_set(out, "death_starved", log->death_starved);
+ dict_set(out, "death_smited", log->death_smited);
+ dict_set(out, "death_other", log->death_other);
+ dict_set(out, "death_mon_level", log->death_mon_level);
+ dict_set(out, "death_adj_monsters", log->death_adj_monsters);
+ dict_set(out, "death_maxhp", log->death_maxhp);
+ dict_set(out, "truncated", log->truncated);
+ dict_set(out, "reach_mines", log->reach_mines);
+ dict_set(out, "reach_minetown", log->reach_minetown);
+ dict_set(out, "reach_deep_mines", log->reach_deep_mines);
+ dict_set(out, "reach_main_d5", log->reach_main_d5);
+ dict_set(out, "reach_sokoban", log->reach_sokoban);
+}
+
+// Per-(verb,head) consumption map for PPO consumed-head gating (weak symbol
+// read by src/pufferlib.cu). heads: [0]=verb, [1..12]=slot heads 0..11,
+// [13]=direction. A head is "consumed" iff the sampled verb actually uses it.
+const signed char* env_head_consume_map(int* n_verbs, int* n_atns) {
+ static signed char map[NETHACK_NUM_ACTIONS * NUM_ATNS];
+ static int built = 0;
+ if (!built) {
+ memset(map, 0, sizeof(map));
+ for (int v = 0; v < NETHACK_NUM_ACTIONS; v++) {
+ signed char* row = map + v * NUM_ATNS;
+ row[0] = 1; // verb head: always
+ int sh = NETHACK_VERBS[v].head; // slot head 0..11 or -1
+ if (sh >= 0) row[1 + sh] = 1;
+ if (v == NETHACK_ACT_MOVE || v == NETHACK_ACT_RUN
+ || v == NETHACK_ACT_KICK || v == NETHACK_ACT_THROW
+ || v == NETHACK_ACT_ZAP || v == NETHACK_ACT_APPLY)
+ row[NUM_ATNS - 1] = 1; // direction head
+ }
+ built = 1;
+ }
+ *n_verbs = NETHACK_NUM_ACTIONS;
+ *n_atns = NUM_ATNS;
+ return map;
+}
diff --git a/ocean/nethack/fs.h b/ocean/nethack/fs.h
new file mode 100644
index 0000000000..843ebfd211
--- /dev/null
+++ b/ocean/nethack/fs.h
@@ -0,0 +1,134 @@
+// Filesystem plumbing: one private vardir per env (NetHack wants nhdat +
+// writable files) under a per-process tmpfs parent, plus the shared options rc.
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// helpers
+
+static void nethack_touch(const char* path) {
+ int fd = open(path, O_CREAT | O_WRONLY, 0644);
+ if (fd >= 0) close(fd);
+}
+
+static void nethack_rm_rf(const char* path, int depth) {
+ if (depth > 3) return; // vardir trees are at most base/env/save/files
+ DIR* d = opendir(path);
+ if (d) {
+ struct dirent* e;
+ char p[2048];
+ while ((e = readdir(d)) != NULL) {
+ if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
+ snprintf(p, sizeof(p), "%s/%s", path, e->d_name);
+ if (unlink(p) != 0) nethack_rm_rf(p, depth + 1);
+ }
+ closedir(d);
+ }
+ rmdir(path);
+}
+
+// vardirs
+
+// per-process parent on tmpfs; first use sweeps parents whose owner pid died
+static const char* nethack_vardir_base(void) {
+ static char base[256] = "";
+ if (base[0]) return base;
+ const char* root = access("/dev/shm", W_OK) == 0 ? "/dev/shm" : "/tmp";
+ DIR* d = opendir(root);
+ if (d) {
+ struct dirent* e;
+ while ((e = readdir(d)) != NULL) {
+ long pid = 0;
+ if (sscanf(e->d_name, "nle-run-%ld", &pid) != 1 || pid <= 0) continue;
+ if (pid == (long)getpid()) continue;
+ if (kill((pid_t)pid, 0) == 0 || errno != ESRCH) continue; // owner alive
+ char dead[512];
+ snprintf(dead, sizeof(dead), "%s/%s", root, e->d_name);
+ nethack_rm_rf(dead, 0);
+ }
+ closedir(d);
+ }
+ snprintf(base, sizeof(base), "%s/nle-run-%ld", root, (long)getpid());
+ mkdir(base, 0755);
+ return base;
+}
+
+static int nethack_make_vardir(const char* source_hackdir, char* out_buf, size_t out_cap) {
+ char tmpl[512];
+ snprintf(tmpl, sizeof(tmpl), "%s/env-XXXXXX", nethack_vardir_base());
+ char* dir = mkdtemp(tmpl);
+ if (dir == NULL) return -1;
+ if ((size_t)snprintf(out_buf, out_cap, "%s", dir) >= out_cap) return -1;
+
+ char abs_source[1024];
+ if (source_hackdir[0] == '/') {
+ snprintf(abs_source, sizeof(abs_source), "%s", source_hackdir);
+ } else {
+ char cwd[768];
+ if (getcwd(cwd, sizeof(cwd)) == NULL) return -1;
+ snprintf(abs_source, sizeof(abs_source), "%s/%s", cwd, source_hackdir);
+ }
+
+ char src[1280], dst[1280];
+ snprintf(src, sizeof(src), "%s/nhdat", abs_source);
+ snprintf(dst, sizeof(dst), "%s/nhdat", dir);
+
+ // fail fast on a dangling nhdat symlink: symlink(2) would succeed and the
+ // error surface later as a cryptic init_dungeons panic
+ char resolved[4096]; // realpath(3) requires a PATH_MAX buffer
+ if (realpath(src, resolved) == NULL || access(resolved, R_OK) != 0) {
+ fprintf(stderr,
+ "nethack: NETHACKDIR misconfigured — no readable nhdat at %s (%s).\n"
+ "Set NETHACKDIR to an absolute dir containing nhdat, e.g. "
+ "/vendor/fast-nle/build/dat.\n", src, strerror(errno));
+ exit(1);
+ }
+ if (symlink(src, dst) != 0) return -1;
+ const char* touched[] = {"perm", "record", "logfile", "xlogfile"};
+ for (size_t i = 0; i < 4; i++) {
+ snprintf(dst, sizeof(dst), "%s/%s", dir, touched[i]);
+ nethack_touch(dst);
+ }
+ snprintf(dst, sizeof(dst), "%s/save", dir);
+ mkdir(dst, 0755);
+ return 0;
+}
+
+static void nethack_rm_vardir(const char* dir) {
+ if (dir == NULL || dir[0] == '\0') return;
+ // full tree: the game drops level/lock files beyond the fixed set
+ nethack_rm_rf(dir, 1);
+}
+
+// options rc
+
+// one rc per process: AUTOPICKUP_EXCEPTION is a config-file-only directive, so
+// the options string becomes "@"; written atomically (tmp + rename),
+// concurrent env inits write identical content
+static const char* nethack_rc_path(const char* default_options) {
+ static char path[512] = "";
+ if (path[0]) return path;
+ char p[512], tmp[560];
+ snprintf(p, sizeof(p), "%s/nhrc", nethack_vardir_base());
+ if (access(p, R_OK) != 0) {
+ snprintf(tmp, sizeof(tmp), "%s.%p", p, (void*)&tmp);
+ FILE* f = fopen(tmp, "w");
+ if (f) {
+ fprintf(f, "OPTIONS=%s\n", default_options);
+ // corpses are never AUTO-picked: acquiring one is a deliberate
+ // PICKUP, and eating carried corpses stays policy-learnable
+ fprintf(f, "AUTOPICKUP_EXCEPTION=\">corpse\"\n");
+ fclose(f);
+ rename(tmp, p);
+ }
+ }
+ strncpy(path, p, sizeof(path) - 1);
+ return path;
+}
diff --git a/ocean/nethack/glyph_map.h b/ocean/nethack/glyph_map.h
new file mode 100644
index 0000000000..13f1f81a39
--- /dev/null
+++ b/ocean/nethack/glyph_map.h
@@ -0,0 +1,19 @@
+// GENERATED by scratchpad gen_glyph_map.c from the vendored engine's
+// display.h macros (NetHack 3.6.6 glyph layout) — do not edit by hand.
+// Exhaustiveness + species-sharing asserted at generation time.
+// kind: mon pet invis detect body ridden obj cmap explode zap swallow warning statue pad
+// (kind, sub) is a function of the OBSERVED glyph id, which is already
+// post-shuffle (appearance space) — the factorization cannot re-leak
+// identities the shuffle hid.
+#ifndef NH_GM_QUAL
+#define NH_GM_QUAL static const
+#endif
+#define NH_GM_VOCAB 5977
+#define NH_GM_NKIND 14
+#define NH_GM_NSUB 944
+NH_GM_QUAL short nh_glyph_kind[5977] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13,};
+NH_GM_QUAL short nh_glyph_sub[5977] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,922,922,922,922,922,922,922,922,923,923,923,923,923,923,923,923,923,924,924,924,924,924,924,924,924,924,925,925,925,925,925,925,925,925,925,926,926,926,926,926,926,926,926,926,927,927,927,927,927,927,927,927,927,928,928,928,928,928,928,928,928,928,929,929,929,929,930,930,930,930,931,931,931,931,932,932,932,932,933,933,933,933,934,934,934,934,935,935,935,935,936,936,936,936,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,17,17,17,17,17,17,17,17,18,18,18,18,18,18,18,18,19,19,19,19,19,19,19,19,20,20,20,20,20,20,20,20,21,21,21,21,21,21,21,21,22,22,22,22,22,22,22,22,23,23,23,23,23,23,23,23,24,24,24,24,24,24,24,24,25,25,25,25,25,25,25,25,26,26,26,26,26,26,26,26,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,31,31,31,31,31,31,31,31,32,32,32,32,32,32,32,32,33,33,33,33,33,33,33,33,34,34,34,34,34,34,34,34,35,35,35,35,35,35,35,35,36,36,36,36,36,36,36,36,37,37,37,37,37,37,37,37,38,38,38,38,38,38,38,38,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,41,42,42,42,42,42,42,42,42,43,43,43,43,43,43,43,43,44,44,44,44,44,44,44,44,45,45,45,45,45,45,45,45,46,46,46,46,46,46,46,46,47,47,47,47,47,47,47,47,48,48,48,48,48,48,48,48,49,49,49,49,49,49,49,49,50,50,50,50,50,50,50,50,51,51,51,51,51,51,51,51,52,52,52,52,52,52,52,52,53,53,53,53,53,53,53,53,54,54,54,54,54,54,54,54,55,55,55,55,55,55,55,55,56,56,56,56,56,56,56,56,57,57,57,57,57,57,57,57,58,58,58,58,58,58,58,58,59,59,59,59,59,59,59,59,60,60,60,60,60,60,60,60,61,61,61,61,61,61,61,61,62,62,62,62,62,62,62,62,63,63,63,63,63,63,63,63,64,64,64,64,64,64,64,64,65,65,65,65,65,65,65,65,66,66,66,66,66,66,66,66,67,67,67,67,67,67,67,67,68,68,68,68,68,68,68,68,69,69,69,69,69,69,69,69,70,70,70,70,70,70,70,70,71,71,71,71,71,71,71,71,72,72,72,72,72,72,72,72,73,73,73,73,73,73,73,73,74,74,74,74,74,74,74,74,75,75,75,75,75,75,75,75,76,76,76,76,76,76,76,76,77,77,77,77,77,77,77,77,78,78,78,78,78,78,78,78,79,79,79,79,79,79,79,79,80,80,80,80,80,80,80,80,81,81,81,81,81,81,81,81,82,82,82,82,82,82,82,82,83,83,83,83,83,83,83,83,84,84,84,84,84,84,84,84,85,85,85,85,85,85,85,85,86,86,86,86,86,86,86,86,87,87,87,87,87,87,87,87,88,88,88,88,88,88,88,88,89,89,89,89,89,89,89,89,90,90,90,90,90,90,90,90,91,91,91,91,91,91,91,91,92,92,92,92,92,92,92,92,93,93,93,93,93,93,93,93,94,94,94,94,94,94,94,94,95,95,95,95,95,95,95,95,96,96,96,96,96,96,96,96,97,97,97,97,97,97,97,97,98,98,98,98,98,98,98,98,99,99,99,99,99,99,99,99,100,100,100,100,100,100,100,100,101,101,101,101,101,101,101,101,102,102,102,102,102,102,102,102,103,103,103,103,103,103,103,103,104,104,104,104,104,104,104,104,105,105,105,105,105,105,105,105,106,106,106,106,106,106,106,106,107,107,107,107,107,107,107,107,108,108,108,108,108,108,108,108,109,109,109,109,109,109,109,109,110,110,110,110,110,110,110,110,111,111,111,111,111,111,111,111,112,112,112,112,112,112,112,112,113,113,113,113,113,113,113,113,114,114,114,114,114,114,114,114,115,115,115,115,115,115,115,115,116,116,116,116,116,116,116,116,117,117,117,117,117,117,117,117,118,118,118,118,118,118,118,118,119,119,119,119,119,119,119,119,120,120,120,120,120,120,120,120,121,121,121,121,121,121,121,121,122,122,122,122,122,122,122,122,123,123,123,123,123,123,123,123,124,124,124,124,124,124,124,124,125,125,125,125,125,125,125,125,126,126,126,126,126,126,126,126,127,127,127,127,127,127,127,127,128,128,128,128,128,128,128,128,129,129,129,129,129,129,129,129,130,130,130,130,130,130,130,130,131,131,131,131,131,131,131,131,132,132,132,132,132,132,132,132,133,133,133,133,133,133,133,133,134,134,134,134,134,134,134,134,135,135,135,135,135,135,135,135,136,136,136,136,136,136,136,136,137,137,137,137,137,137,137,137,138,138,138,138,138,138,138,138,139,139,139,139,139,139,139,139,140,140,140,140,140,140,140,140,141,141,141,141,141,141,141,141,142,142,142,142,142,142,142,142,143,143,143,143,143,143,143,143,144,144,144,144,144,144,144,144,145,145,145,145,145,145,145,145,146,146,146,146,146,146,146,146,147,147,147,147,147,147,147,147,148,148,148,148,148,148,148,148,149,149,149,149,149,149,149,149,150,150,150,150,150,150,150,150,151,151,151,151,151,151,151,151,152,152,152,152,152,152,152,152,153,153,153,153,153,153,153,153,154,154,154,154,154,154,154,154,155,155,155,155,155,155,155,155,156,156,156,156,156,156,156,156,157,157,157,157,157,157,157,157,158,158,158,158,158,158,158,158,159,159,159,159,159,159,159,159,160,160,160,160,160,160,160,160,161,161,161,161,161,161,161,161,162,162,162,162,162,162,162,162,163,163,163,163,163,163,163,163,164,164,164,164,164,164,164,164,165,165,165,165,165,165,165,165,166,166,166,166,166,166,166,166,167,167,167,167,167,167,167,167,168,168,168,168,168,168,168,168,169,169,169,169,169,169,169,169,170,170,170,170,170,170,170,170,171,171,171,171,171,171,171,171,172,172,172,172,172,172,172,172,173,173,173,173,173,173,173,173,174,174,174,174,174,174,174,174,175,175,175,175,175,175,175,175,176,176,176,176,176,176,176,176,177,177,177,177,177,177,177,177,178,178,178,178,178,178,178,178,179,179,179,179,179,179,179,179,180,180,180,180,180,180,180,180,181,181,181,181,181,181,181,181,182,182,182,182,182,182,182,182,183,183,183,183,183,183,183,183,184,184,184,184,184,184,184,184,185,185,185,185,185,185,185,185,186,186,186,186,186,186,186,186,187,187,187,187,187,187,187,187,188,188,188,188,188,188,188,188,189,189,189,189,189,189,189,189,190,190,190,190,190,190,190,190,191,191,191,191,191,191,191,191,192,192,192,192,192,192,192,192,193,193,193,193,193,193,193,193,194,194,194,194,194,194,194,194,195,195,195,195,195,195,195,195,196,196,196,196,196,196,196,196,197,197,197,197,197,197,197,197,198,198,198,198,198,198,198,198,199,199,199,199,199,199,199,199,200,200,200,200,200,200,200,200,201,201,201,201,201,201,201,201,202,202,202,202,202,202,202,202,203,203,203,203,203,203,203,203,204,204,204,204,204,204,204,204,205,205,205,205,205,205,205,205,206,206,206,206,206,206,206,206,207,207,207,207,207,207,207,207,208,208,208,208,208,208,208,208,209,209,209,209,209,209,209,209,210,210,210,210,210,210,210,210,211,211,211,211,211,211,211,211,212,212,212,212,212,212,212,212,213,213,213,213,213,213,213,213,214,214,214,214,214,214,214,214,215,215,215,215,215,215,215,215,216,216,216,216,216,216,216,216,217,217,217,217,217,217,217,217,218,218,218,218,218,218,218,218,219,219,219,219,219,219,219,219,220,220,220,220,220,220,220,220,221,221,221,221,221,221,221,221,222,222,222,222,222,222,222,222,223,223,223,223,223,223,223,223,224,224,224,224,224,224,224,224,225,225,225,225,225,225,225,225,226,226,226,226,226,226,226,226,227,227,227,227,227,227,227,227,228,228,228,228,228,228,228,228,229,229,229,229,229,229,229,229,230,230,230,230,230,230,230,230,231,231,231,231,231,231,231,231,232,232,232,232,232,232,232,232,233,233,233,233,233,233,233,233,234,234,234,234,234,234,234,234,235,235,235,235,235,235,235,235,236,236,236,236,236,236,236,236,237,237,237,237,237,237,237,237,238,238,238,238,238,238,238,238,239,239,239,239,239,239,239,239,240,240,240,240,240,240,240,240,241,241,241,241,241,241,241,241,242,242,242,242,242,242,242,242,243,243,243,243,243,243,243,243,244,244,244,244,244,244,244,244,245,245,245,245,245,245,245,245,246,246,246,246,246,246,246,246,247,247,247,247,247,247,247,247,248,248,248,248,248,248,248,248,249,249,249,249,249,249,249,249,250,250,250,250,250,250,250,250,251,251,251,251,251,251,251,251,252,252,252,252,252,252,252,252,253,253,253,253,253,253,253,253,254,254,254,254,254,254,254,254,255,255,255,255,255,255,255,255,256,256,256,256,256,256,256,256,257,257,257,257,257,257,257,257,258,258,258,258,258,258,258,258,259,259,259,259,259,259,259,259,260,260,260,260,260,260,260,260,261,261,261,261,261,261,261,261,262,262,262,262,262,262,262,262,263,263,263,263,263,263,263,263,264,264,264,264,264,264,264,264,265,265,265,265,265,265,265,265,266,266,266,266,266,266,266,266,267,267,267,267,267,267,267,267,268,268,268,268,268,268,268,268,269,269,269,269,269,269,269,269,270,270,270,270,270,270,270,270,271,271,271,271,271,271,271,271,272,272,272,272,272,272,272,272,273,273,273,273,273,273,273,273,274,274,274,274,274,274,274,274,275,275,275,275,275,275,275,275,276,276,276,276,276,276,276,276,277,277,277,277,277,277,277,277,278,278,278,278,278,278,278,278,279,279,279,279,279,279,279,279,280,280,280,280,280,280,280,280,281,281,281,281,281,281,281,281,282,282,282,282,282,282,282,282,283,283,283,283,283,283,283,283,284,284,284,284,284,284,284,284,285,285,285,285,285,285,285,285,286,286,286,286,286,286,286,286,287,287,287,287,287,287,287,287,288,288,288,288,288,288,288,288,289,289,289,289,289,289,289,289,290,290,290,290,290,290,290,290,291,291,291,291,291,291,291,291,292,292,292,292,292,292,292,292,293,293,293,293,293,293,293,293,294,294,294,294,294,294,294,294,295,295,295,295,295,295,295,295,296,296,296,296,296,296,296,296,297,297,297,297,297,297,297,297,298,298,298,298,298,298,298,298,299,299,299,299,299,299,299,299,300,300,300,300,300,300,300,300,301,301,301,301,301,301,301,301,302,302,302,302,302,302,302,302,303,303,303,303,303,303,303,303,304,304,304,304,304,304,304,304,305,305,305,305,305,305,305,305,306,306,306,306,306,306,306,306,307,307,307,307,307,307,307,307,308,308,308,308,308,308,308,308,309,309,309,309,309,309,309,309,310,310,310,310,310,310,310,310,311,311,311,311,311,311,311,311,312,312,312,312,312,312,312,312,313,313,313,313,313,313,313,313,314,314,314,314,314,314,314,314,315,315,315,315,315,315,315,315,316,316,316,316,316,316,316,316,317,317,317,317,317,317,317,317,318,318,318,318,318,318,318,318,319,319,319,319,319,319,319,319,320,320,320,320,320,320,320,320,321,321,321,321,321,321,321,321,322,322,322,322,322,322,322,322,323,323,323,323,323,323,323,323,324,324,324,324,324,324,324,324,325,325,325,325,325,325,325,325,326,326,326,326,326,326,326,326,327,327,327,327,327,327,327,327,328,328,328,328,328,328,328,328,329,329,329,329,329,329,329,329,330,330,330,330,330,330,330,330,331,331,331,331,331,331,331,331,332,332,332,332,332,332,332,332,333,333,333,333,333,333,333,333,334,334,334,334,334,334,334,334,335,335,335,335,335,335,335,335,336,336,336,336,336,336,336,336,337,337,337,337,337,337,337,337,338,338,338,338,338,338,338,338,339,339,339,339,339,339,339,339,340,340,340,340,340,340,340,340,341,341,341,341,341,341,341,341,342,342,342,342,342,342,342,342,343,343,343,343,343,343,343,343,344,344,344,344,344,344,344,344,345,345,345,345,345,345,345,345,346,346,346,346,346,346,346,346,347,347,347,347,347,347,347,347,348,348,348,348,348,348,348,348,349,349,349,349,349,349,349,349,350,350,350,350,350,350,350,350,351,351,351,351,351,351,351,351,352,352,352,352,352,352,352,352,353,353,353,353,353,353,353,353,354,354,354,354,354,354,354,354,355,355,355,355,355,355,355,355,356,356,356,356,356,356,356,356,357,357,357,357,357,357,357,357,358,358,358,358,358,358,358,358,359,359,359,359,359,359,359,359,360,360,360,360,360,360,360,360,361,361,361,361,361,361,361,361,362,362,362,362,362,362,362,362,363,363,363,363,363,363,363,363,364,364,364,364,364,364,364,364,365,365,365,365,365,365,365,365,366,366,366,366,366,366,366,366,367,367,367,367,367,367,367,367,368,368,368,368,368,368,368,368,369,369,369,369,369,369,369,369,370,370,370,370,370,370,370,370,371,371,371,371,371,371,371,371,372,372,372,372,372,372,372,372,373,373,373,373,373,373,373,373,374,374,374,374,374,374,374,374,375,375,375,375,375,375,375,375,376,376,376,376,376,376,376,376,377,377,377,377,377,377,377,377,378,378,378,378,378,378,378,378,379,379,379,379,379,379,379,379,380,380,380,380,380,380,380,380,937,938,939,940,941,942,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,943,};
+NH_GM_QUAL int nh_kind_csr_off[15] = {0,381,762,763,1144,1525,1906,2359,2446,2509,2541,5589,5595,5976,5977,};
+NH_GM_QUAL short nh_kind_csr_glyph[5977] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2229,2230,2231,2232,2233,2234,2235,2236,2237,2238,2239,2240,2241,2242,2243,2244,2245,2246,2247,2248,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2274,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381,2382,2383,2384,2385,2386,2387,2388,2389,2390,2391,2392,2393,2394,2395,2396,2397,2398,2399,2400,2401,2402,2403,2404,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2416,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2433,2434,2435,2436,2437,2438,2439,2440,2441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2453,2454,2455,2456,2457,2458,2459,2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2471,2472,2473,2474,2475,2476,2477,2478,2479,2480,2481,2482,2483,2484,2485,2486,2487,2488,2489,2490,2491,2492,2493,2494,2495,2496,2497,2498,2499,2500,2501,2502,2503,2504,2505,2506,2507,2508,2509,2510,2511,2512,2513,2514,2515,2516,2517,2518,2519,2520,2521,2522,2523,2524,2525,2526,2527,2528,2529,2530,2531,2532,2533,2534,2535,2536,2537,2538,2539,2540,2541,2542,2543,2544,2545,2546,2547,2548,2549,2550,2551,2552,2553,2554,2555,2556,2557,2558,2559,2560,2561,2562,2563,2564,2565,2566,2567,2568,2569,2570,2571,2572,2573,2574,2575,2576,2577,2578,2579,2580,2581,2582,2583,2584,2585,2586,2587,2588,2589,2590,2591,2592,2593,2594,2595,2596,2597,2598,2599,2600,2601,2602,2603,2604,2605,2606,2607,2608,2609,2610,2611,2612,2613,2614,2615,2616,2617,2618,2619,2620,2621,2622,2623,2624,2625,2626,2627,2628,2629,2630,2631,2632,2633,2634,2635,2636,2637,2638,2639,2640,2641,2642,2643,2644,2645,2646,2647,2648,2649,2650,2651,2652,2653,2654,2655,2656,2657,2658,2659,2660,2661,2662,2663,2664,2665,2666,2667,2668,2669,2670,2671,2672,2673,2674,2675,2676,2677,2678,2679,2680,2681,2682,2683,2684,2685,2686,2687,2688,2689,2690,2691,2692,2693,2694,2695,2696,2697,2698,2699,2700,2701,2702,2703,2704,2705,2706,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2725,2726,2727,2728,2729,2730,2731,2732,2733,2734,2735,2736,2737,2738,2739,2740,2741,2742,2743,2744,2745,2746,2747,2748,2749,2750,2751,2752,2753,2754,2755,2756,2757,2758,2759,2760,2761,2762,2763,2764,2765,2766,2767,2768,2769,2770,2771,2772,2773,2774,2775,2776,2777,2778,2779,2780,2781,2782,2783,2784,2785,2786,2787,2788,2789,2790,2791,2792,2793,2794,2795,2796,2797,2798,2799,2800,2801,2802,2803,2804,2805,2806,2807,2808,2809,2810,2811,2812,2813,2814,2815,2816,2817,2818,2819,2820,2821,2822,2823,2824,2825,2826,2827,2828,2829,2830,2831,2832,2833,2834,2835,2836,2837,2838,2839,2840,2841,2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856,2857,2858,2859,2860,2861,2862,2863,2864,2865,2866,2867,2868,2869,2870,2871,2872,2873,2874,2875,2876,2877,2878,2879,2880,2881,2882,2883,2884,2885,2886,2887,2888,2889,2890,2891,2892,2893,2894,2895,2896,2897,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278,3279,3280,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299,3300,3301,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311,3312,3313,3314,3315,3316,3317,3318,3319,3320,3321,3322,3323,3324,3325,3326,3327,3328,3329,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343,3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,3388,3389,3390,3391,3392,3393,3394,3395,3396,3397,3398,3399,3400,3401,3402,3403,3404,3405,3406,3407,3408,3409,3410,3411,3412,3413,3414,3415,3416,3417,3418,3419,3420,3421,3422,3423,3424,3425,3426,3427,3428,3429,3430,3431,3432,3433,3434,3435,3436,3437,3438,3439,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510,3511,3512,3513,3514,3515,3516,3517,3518,3519,3520,3521,3522,3523,3524,3525,3526,3527,3528,3529,3530,3531,3532,3533,3534,3535,3536,3537,3538,3539,3540,3541,3542,3543,3544,3545,3546,3547,3548,3549,3550,3551,3552,3553,3554,3555,3556,3557,3558,3559,3560,3561,3562,3563,3564,3565,3566,3567,3568,3569,3570,3571,3572,3573,3574,3575,3576,3577,3578,3579,3580,3581,3582,3583,3584,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,3643,3644,3645,3646,3647,3648,3649,3650,3651,3652,3653,3654,3655,3656,3657,3658,3659,3660,3661,3662,3663,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3674,3675,3676,3677,3678,3679,3680,3681,3682,3683,3684,3685,3686,3687,3688,3689,3690,3691,3692,3693,3694,3695,3696,3697,3698,3699,3700,3701,3702,3703,3704,3705,3706,3707,3708,3709,3710,3711,3712,3713,3714,3715,3716,3717,3718,3719,3720,3721,3722,3723,3724,3725,3726,3727,3728,3729,3730,3731,3732,3733,3734,3735,3736,3737,3738,3739,3740,3741,3742,3743,3744,3745,3746,3747,3748,3749,3750,3751,3752,3753,3754,3755,3756,3757,3758,3759,3760,3761,3762,3763,3764,3765,3766,3767,3768,3769,3770,3771,3772,3773,3774,3775,3776,3777,3778,3779,3780,3781,3782,3783,3784,3785,3786,3787,3788,3789,3790,3791,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3802,3803,3804,3805,3806,3807,3808,3809,3810,3811,3812,3813,3814,3815,3816,3817,3818,3819,3820,3821,3822,3823,3824,3825,3826,3827,3828,3829,3830,3831,3832,3833,3834,3835,3836,3837,3838,3839,3840,3841,3842,3843,3844,3845,3846,3847,3848,3849,3850,3851,3852,3853,3854,3855,3856,3857,3858,3859,3860,3861,3862,3863,3864,3865,3866,3867,3868,3869,3870,3871,3872,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882,3883,3884,3885,3886,3887,3888,3889,3890,3891,3892,3893,3894,3895,3896,3897,3898,3899,3900,3901,3902,3903,3904,3905,3906,3907,3908,3909,3910,3911,3912,3913,3914,3915,3916,3917,3918,3919,3920,3921,3922,3923,3924,3925,3926,3927,3928,3929,3930,3931,3932,3933,3934,3935,3936,3937,3938,3939,3940,3941,3942,3943,3944,3945,3946,3947,3948,3949,3950,3951,3952,3953,3954,3955,3956,3957,3958,3959,3960,3961,3962,3963,3964,3965,3966,3967,3968,3969,3970,3971,3972,3973,3974,3975,3976,3977,3978,3979,3980,3981,3982,3983,3984,3985,3986,3987,3988,3989,3990,3991,3992,3993,3994,3995,3996,3997,3998,3999,4000,4001,4002,4003,4004,4005,4006,4007,4008,4009,4010,4011,4012,4013,4014,4015,4016,4017,4018,4019,4020,4021,4022,4023,4024,4025,4026,4027,4028,4029,4030,4031,4032,4033,4034,4035,4036,4037,4038,4039,4040,4041,4042,4043,4044,4045,4046,4047,4048,4049,4050,4051,4052,4053,4054,4055,4056,4057,4058,4059,4060,4061,4062,4063,4064,4065,4066,4067,4068,4069,4070,4071,4072,4073,4074,4075,4076,4077,4078,4079,4080,4081,4082,4083,4084,4085,4086,4087,4088,4089,4090,4091,4092,4093,4094,4095,4096,4097,4098,4099,4100,4101,4102,4103,4104,4105,4106,4107,4108,4109,4110,4111,4112,4113,4114,4115,4116,4117,4118,4119,4120,4121,4122,4123,4124,4125,4126,4127,4128,4129,4130,4131,4132,4133,4134,4135,4136,4137,4138,4139,4140,4141,4142,4143,4144,4145,4146,4147,4148,4149,4150,4151,4152,4153,4154,4155,4156,4157,4158,4159,4160,4161,4162,4163,4164,4165,4166,4167,4168,4169,4170,4171,4172,4173,4174,4175,4176,4177,4178,4179,4180,4181,4182,4183,4184,4185,4186,4187,4188,4189,4190,4191,4192,4193,4194,4195,4196,4197,4198,4199,4200,4201,4202,4203,4204,4205,4206,4207,4208,4209,4210,4211,4212,4213,4214,4215,4216,4217,4218,4219,4220,4221,4222,4223,4224,4225,4226,4227,4228,4229,4230,4231,4232,4233,4234,4235,4236,4237,4238,4239,4240,4241,4242,4243,4244,4245,4246,4247,4248,4249,4250,4251,4252,4253,4254,4255,4256,4257,4258,4259,4260,4261,4262,4263,4264,4265,4266,4267,4268,4269,4270,4271,4272,4273,4274,4275,4276,4277,4278,4279,4280,4281,4282,4283,4284,4285,4286,4287,4288,4289,4290,4291,4292,4293,4294,4295,4296,4297,4298,4299,4300,4301,4302,4303,4304,4305,4306,4307,4308,4309,4310,4311,4312,4313,4314,4315,4316,4317,4318,4319,4320,4321,4322,4323,4324,4325,4326,4327,4328,4329,4330,4331,4332,4333,4334,4335,4336,4337,4338,4339,4340,4341,4342,4343,4344,4345,4346,4347,4348,4349,4350,4351,4352,4353,4354,4355,4356,4357,4358,4359,4360,4361,4362,4363,4364,4365,4366,4367,4368,4369,4370,4371,4372,4373,4374,4375,4376,4377,4378,4379,4380,4381,4382,4383,4384,4385,4386,4387,4388,4389,4390,4391,4392,4393,4394,4395,4396,4397,4398,4399,4400,4401,4402,4403,4404,4405,4406,4407,4408,4409,4410,4411,4412,4413,4414,4415,4416,4417,4418,4419,4420,4421,4422,4423,4424,4425,4426,4427,4428,4429,4430,4431,4432,4433,4434,4435,4436,4437,4438,4439,4440,4441,4442,4443,4444,4445,4446,4447,4448,4449,4450,4451,4452,4453,4454,4455,4456,4457,4458,4459,4460,4461,4462,4463,4464,4465,4466,4467,4468,4469,4470,4471,4472,4473,4474,4475,4476,4477,4478,4479,4480,4481,4482,4483,4484,4485,4486,4487,4488,4489,4490,4491,4492,4493,4494,4495,4496,4497,4498,4499,4500,4501,4502,4503,4504,4505,4506,4507,4508,4509,4510,4511,4512,4513,4514,4515,4516,4517,4518,4519,4520,4521,4522,4523,4524,4525,4526,4527,4528,4529,4530,4531,4532,4533,4534,4535,4536,4537,4538,4539,4540,4541,4542,4543,4544,4545,4546,4547,4548,4549,4550,4551,4552,4553,4554,4555,4556,4557,4558,4559,4560,4561,4562,4563,4564,4565,4566,4567,4568,4569,4570,4571,4572,4573,4574,4575,4576,4577,4578,4579,4580,4581,4582,4583,4584,4585,4586,4587,4588,4589,4590,4591,4592,4593,4594,4595,4596,4597,4598,4599,4600,4601,4602,4603,4604,4605,4606,4607,4608,4609,4610,4611,4612,4613,4614,4615,4616,4617,4618,4619,4620,4621,4622,4623,4624,4625,4626,4627,4628,4629,4630,4631,4632,4633,4634,4635,4636,4637,4638,4639,4640,4641,4642,4643,4644,4645,4646,4647,4648,4649,4650,4651,4652,4653,4654,4655,4656,4657,4658,4659,4660,4661,4662,4663,4664,4665,4666,4667,4668,4669,4670,4671,4672,4673,4674,4675,4676,4677,4678,4679,4680,4681,4682,4683,4684,4685,4686,4687,4688,4689,4690,4691,4692,4693,4694,4695,4696,4697,4698,4699,4700,4701,4702,4703,4704,4705,4706,4707,4708,4709,4710,4711,4712,4713,4714,4715,4716,4717,4718,4719,4720,4721,4722,4723,4724,4725,4726,4727,4728,4729,4730,4731,4732,4733,4734,4735,4736,4737,4738,4739,4740,4741,4742,4743,4744,4745,4746,4747,4748,4749,4750,4751,4752,4753,4754,4755,4756,4757,4758,4759,4760,4761,4762,4763,4764,4765,4766,4767,4768,4769,4770,4771,4772,4773,4774,4775,4776,4777,4778,4779,4780,4781,4782,4783,4784,4785,4786,4787,4788,4789,4790,4791,4792,4793,4794,4795,4796,4797,4798,4799,4800,4801,4802,4803,4804,4805,4806,4807,4808,4809,4810,4811,4812,4813,4814,4815,4816,4817,4818,4819,4820,4821,4822,4823,4824,4825,4826,4827,4828,4829,4830,4831,4832,4833,4834,4835,4836,4837,4838,4839,4840,4841,4842,4843,4844,4845,4846,4847,4848,4849,4850,4851,4852,4853,4854,4855,4856,4857,4858,4859,4860,4861,4862,4863,4864,4865,4866,4867,4868,4869,4870,4871,4872,4873,4874,4875,4876,4877,4878,4879,4880,4881,4882,4883,4884,4885,4886,4887,4888,4889,4890,4891,4892,4893,4894,4895,4896,4897,4898,4899,4900,4901,4902,4903,4904,4905,4906,4907,4908,4909,4910,4911,4912,4913,4914,4915,4916,4917,4918,4919,4920,4921,4922,4923,4924,4925,4926,4927,4928,4929,4930,4931,4932,4933,4934,4935,4936,4937,4938,4939,4940,4941,4942,4943,4944,4945,4946,4947,4948,4949,4950,4951,4952,4953,4954,4955,4956,4957,4958,4959,4960,4961,4962,4963,4964,4965,4966,4967,4968,4969,4970,4971,4972,4973,4974,4975,4976,4977,4978,4979,4980,4981,4982,4983,4984,4985,4986,4987,4988,4989,4990,4991,4992,4993,4994,4995,4996,4997,4998,4999,5000,5001,5002,5003,5004,5005,5006,5007,5008,5009,5010,5011,5012,5013,5014,5015,5016,5017,5018,5019,5020,5021,5022,5023,5024,5025,5026,5027,5028,5029,5030,5031,5032,5033,5034,5035,5036,5037,5038,5039,5040,5041,5042,5043,5044,5045,5046,5047,5048,5049,5050,5051,5052,5053,5054,5055,5056,5057,5058,5059,5060,5061,5062,5063,5064,5065,5066,5067,5068,5069,5070,5071,5072,5073,5074,5075,5076,5077,5078,5079,5080,5081,5082,5083,5084,5085,5086,5087,5088,5089,5090,5091,5092,5093,5094,5095,5096,5097,5098,5099,5100,5101,5102,5103,5104,5105,5106,5107,5108,5109,5110,5111,5112,5113,5114,5115,5116,5117,5118,5119,5120,5121,5122,5123,5124,5125,5126,5127,5128,5129,5130,5131,5132,5133,5134,5135,5136,5137,5138,5139,5140,5141,5142,5143,5144,5145,5146,5147,5148,5149,5150,5151,5152,5153,5154,5155,5156,5157,5158,5159,5160,5161,5162,5163,5164,5165,5166,5167,5168,5169,5170,5171,5172,5173,5174,5175,5176,5177,5178,5179,5180,5181,5182,5183,5184,5185,5186,5187,5188,5189,5190,5191,5192,5193,5194,5195,5196,5197,5198,5199,5200,5201,5202,5203,5204,5205,5206,5207,5208,5209,5210,5211,5212,5213,5214,5215,5216,5217,5218,5219,5220,5221,5222,5223,5224,5225,5226,5227,5228,5229,5230,5231,5232,5233,5234,5235,5236,5237,5238,5239,5240,5241,5242,5243,5244,5245,5246,5247,5248,5249,5250,5251,5252,5253,5254,5255,5256,5257,5258,5259,5260,5261,5262,5263,5264,5265,5266,5267,5268,5269,5270,5271,5272,5273,5274,5275,5276,5277,5278,5279,5280,5281,5282,5283,5284,5285,5286,5287,5288,5289,5290,5291,5292,5293,5294,5295,5296,5297,5298,5299,5300,5301,5302,5303,5304,5305,5306,5307,5308,5309,5310,5311,5312,5313,5314,5315,5316,5317,5318,5319,5320,5321,5322,5323,5324,5325,5326,5327,5328,5329,5330,5331,5332,5333,5334,5335,5336,5337,5338,5339,5340,5341,5342,5343,5344,5345,5346,5347,5348,5349,5350,5351,5352,5353,5354,5355,5356,5357,5358,5359,5360,5361,5362,5363,5364,5365,5366,5367,5368,5369,5370,5371,5372,5373,5374,5375,5376,5377,5378,5379,5380,5381,5382,5383,5384,5385,5386,5387,5388,5389,5390,5391,5392,5393,5394,5395,5396,5397,5398,5399,5400,5401,5402,5403,5404,5405,5406,5407,5408,5409,5410,5411,5412,5413,5414,5415,5416,5417,5418,5419,5420,5421,5422,5423,5424,5425,5426,5427,5428,5429,5430,5431,5432,5433,5434,5435,5436,5437,5438,5439,5440,5441,5442,5443,5444,5445,5446,5447,5448,5449,5450,5451,5452,5453,5454,5455,5456,5457,5458,5459,5460,5461,5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472,5473,5474,5475,5476,5477,5478,5479,5480,5481,5482,5483,5484,5485,5486,5487,5488,5489,5490,5491,5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520,5521,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536,5537,5538,5539,5540,5541,5542,5543,5544,5545,5546,5547,5548,5549,5550,5551,5552,5553,5554,5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568,5569,5570,5571,5572,5573,5574,5575,5576,5577,5578,5579,5580,5581,5582,5583,5584,5585,5586,5587,5588,5589,5590,5591,5592,5593,5594,5595,5596,5597,5598,5599,5600,5601,5602,5603,5604,5605,5606,5607,5608,5609,5610,5611,5612,5613,5614,5615,5616,5617,5618,5619,5620,5621,5622,5623,5624,5625,5626,5627,5628,5629,5630,5631,5632,5633,5634,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646,5647,5648,5649,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660,5661,5662,5663,5664,5665,5666,5667,5668,5669,5670,5671,5672,5673,5674,5675,5676,5677,5678,5679,5680,5681,5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692,5693,5694,5695,5696,5697,5698,5699,5700,5701,5702,5703,5704,5705,5706,5707,5708,5709,5710,5711,5712,5713,5714,5715,5716,5717,5718,5719,5720,5721,5722,5723,5724,5725,5726,5727,5728,5729,5730,5731,5732,5733,5734,5735,5736,5737,5738,5739,5740,5741,5742,5743,5744,5745,5746,5747,5748,5749,5750,5751,5752,5753,5754,5755,5756,5757,5758,5759,5760,5761,5762,5763,5764,5765,5766,5767,5768,5769,5770,5771,5772,5773,5774,5775,5776,5777,5778,5779,5780,5781,5782,5783,5784,5785,5786,5787,5788,5789,5790,5791,5792,5793,5794,5795,5796,5797,5798,5799,5800,5801,5802,5803,5804,5805,5806,5807,5808,5809,5810,5811,5812,5813,5814,5815,5816,5817,5818,5819,5820,5821,5822,5823,5824,5825,5826,5827,5828,5829,5830,5831,5832,5833,5834,5835,5836,5837,5838,5839,5840,5841,5842,5843,5844,5845,5846,5847,5848,5849,5850,5851,5852,5853,5854,5855,5856,5857,5858,5859,5860,5861,5862,5863,5864,5865,5866,5867,5868,5869,5870,5871,5872,5873,5874,5875,5876,5877,5878,5879,5880,5881,5882,5883,5884,5885,5886,5887,5888,5889,5890,5891,5892,5893,5894,5895,5896,5897,5898,5899,5900,5901,5902,5903,5904,5905,5906,5907,5908,5909,5910,5911,5912,5913,5914,5915,5916,5917,5918,5919,5920,5921,5922,5923,5924,5925,5926,5927,5928,5929,5930,5931,5932,5933,5934,5935,5936,5937,5938,5939,5940,5941,5942,5943,5944,5945,5946,5947,5948,5949,5950,5951,5952,5953,5954,5955,5956,5957,5958,5959,5960,5961,5962,5963,5964,5965,5966,5967,5968,5969,5970,5971,5972,5973,5974,5975,5976,};
+NH_GM_QUAL int nh_sub_csr_off[945] = {0,14,28,42,56,70,84,98,112,126,140,154,168,182,196,210,224,238,252,266,280,294,308,322,336,350,364,378,392,406,420,434,448,462,476,490,504,518,532,546,560,574,588,602,616,630,644,658,672,686,700,714,728,742,756,770,784,798,812,826,840,854,868,882,896,910,924,938,952,966,980,994,1008,1022,1036,1050,1064,1078,1092,1106,1120,1134,1148,1162,1176,1190,1204,1218,1232,1246,1260,1274,1288,1302,1316,1330,1344,1358,1372,1386,1400,1414,1428,1442,1456,1470,1484,1498,1512,1526,1540,1554,1568,1582,1596,1610,1624,1638,1652,1666,1680,1694,1708,1722,1736,1750,1764,1778,1792,1806,1820,1834,1848,1862,1876,1890,1904,1918,1932,1946,1960,1974,1988,2002,2016,2030,2044,2058,2072,2086,2100,2114,2128,2142,2156,2170,2184,2198,2212,2226,2240,2254,2268,2282,2296,2310,2324,2338,2352,2366,2380,2394,2408,2422,2436,2450,2464,2478,2492,2506,2520,2534,2548,2562,2576,2590,2604,2618,2632,2646,2660,2674,2688,2702,2716,2730,2744,2758,2772,2786,2800,2814,2828,2842,2856,2870,2884,2898,2912,2926,2940,2954,2968,2982,2996,3010,3024,3038,3052,3066,3080,3094,3108,3122,3136,3150,3164,3178,3192,3206,3220,3234,3248,3262,3276,3290,3304,3318,3332,3346,3360,3374,3388,3402,3416,3430,3444,3458,3472,3486,3500,3514,3528,3542,3556,3570,3584,3598,3612,3626,3640,3654,3668,3682,3696,3710,3724,3738,3752,3766,3780,3794,3808,3822,3836,3850,3864,3878,3892,3906,3920,3934,3948,3962,3976,3990,4004,4018,4032,4046,4060,4074,4088,4102,4116,4130,4144,4158,4172,4186,4200,4214,4228,4242,4256,4270,4284,4298,4312,4326,4340,4354,4368,4382,4396,4410,4424,4438,4452,4466,4480,4494,4508,4522,4536,4550,4564,4578,4592,4606,4620,4634,4648,4662,4676,4690,4704,4718,4732,4746,4760,4774,4788,4802,4816,4830,4844,4858,4872,4886,4900,4914,4928,4942,4956,4970,4984,4998,5012,5026,5040,5054,5068,5082,5096,5110,5124,5138,5152,5166,5180,5194,5208,5222,5236,5250,5264,5278,5292,5306,5320,5334,5335,5336,5337,5338,5339,5340,5341,5342,5343,5344,5345,5346,5347,5348,5349,5350,5351,5352,5353,5354,5355,5356,5357,5358,5359,5360,5361,5362,5363,5364,5365,5366,5367,5368,5369,5370,5371,5372,5373,5374,5375,5376,5377,5378,5379,5380,5381,5382,5383,5384,5385,5386,5387,5388,5389,5390,5391,5392,5393,5394,5395,5396,5397,5398,5399,5400,5401,5402,5403,5404,5405,5406,5407,5408,5409,5410,5411,5412,5413,5414,5415,5416,5417,5418,5419,5420,5421,5422,5423,5424,5425,5426,5427,5428,5429,5430,5431,5432,5433,5434,5435,5436,5437,5438,5439,5440,5441,5442,5443,5444,5445,5446,5447,5448,5449,5450,5451,5452,5453,5454,5455,5456,5457,5458,5459,5460,5461,5462,5463,5464,5465,5466,5467,5468,5469,5470,5471,5472,5473,5474,5475,5476,5477,5478,5479,5480,5481,5482,5483,5484,5485,5486,5487,5488,5489,5490,5491,5492,5493,5494,5495,5496,5497,5498,5499,5500,5501,5502,5503,5504,5505,5506,5507,5508,5509,5510,5511,5512,5513,5514,5515,5516,5517,5518,5519,5520,5521,5522,5523,5524,5525,5526,5527,5528,5529,5530,5531,5532,5533,5534,5535,5536,5537,5538,5539,5540,5541,5542,5543,5544,5545,5546,5547,5548,5549,5550,5551,5552,5553,5554,5555,5556,5557,5558,5559,5560,5561,5562,5563,5564,5565,5566,5567,5568,5569,5570,5571,5572,5573,5574,5575,5576,5577,5578,5579,5580,5581,5582,5583,5584,5585,5586,5587,5588,5589,5590,5591,5592,5593,5594,5595,5596,5597,5598,5599,5600,5601,5602,5603,5604,5605,5606,5607,5608,5609,5610,5611,5612,5613,5614,5615,5616,5617,5618,5619,5620,5621,5622,5623,5624,5625,5626,5627,5628,5629,5630,5631,5632,5633,5634,5635,5636,5637,5638,5639,5640,5641,5642,5643,5644,5645,5646,5647,5648,5649,5650,5651,5652,5653,5654,5655,5656,5657,5658,5659,5660,5661,5662,5663,5664,5665,5666,5667,5668,5669,5670,5671,5672,5673,5674,5675,5676,5677,5678,5679,5680,5681,5682,5683,5684,5685,5686,5687,5688,5689,5690,5691,5692,5693,5694,5695,5696,5697,5698,5699,5700,5701,5702,5703,5704,5705,5706,5707,5708,5709,5710,5711,5712,5713,5714,5715,5716,5717,5718,5719,5720,5721,5722,5723,5724,5725,5726,5727,5728,5729,5730,5731,5732,5733,5734,5735,5736,5737,5738,5739,5740,5741,5742,5743,5744,5745,5746,5747,5748,5749,5750,5751,5752,5753,5754,5755,5756,5757,5758,5759,5760,5761,5762,5763,5764,5765,5766,5767,5768,5769,5770,5771,5772,5773,5774,5775,5776,5777,5778,5779,5780,5781,5782,5783,5784,5785,5786,5787,5788,5789,5790,5791,5792,5793,5794,5795,5796,5797,5798,5799,5800,5801,5802,5803,5804,5805,5806,5807,5808,5809,5810,5811,5812,5813,5814,5815,5816,5817,5818,5819,5820,5821,5822,5823,5824,5825,5826,5827,5828,5829,5830,5831,5832,5833,5834,5835,5836,5837,5838,5839,5840,5841,5842,5843,5844,5845,5846,5847,5848,5849,5850,5851,5852,5853,5854,5855,5856,5857,5858,5859,5860,5861,5862,5863,5864,5865,5866,5867,5868,5869,5870,5871,5872,5873,5874,5875,5884,5893,5902,5911,5920,5929,5938,5942,5946,5950,5954,5958,5962,5966,5970,5971,5972,5973,5974,5975,5976,5977,};
+NH_GM_QUAL short nh_sub_csr_glyph[5977] = {0,381,763,1144,1525,2541,2542,2543,2544,2545,2546,2547,2548,5595,1,382,764,1145,1526,2549,2550,2551,2552,2553,2554,2555,2556,5596,2,383,765,1146,1527,2557,2558,2559,2560,2561,2562,2563,2564,5597,3,384,766,1147,1528,2565,2566,2567,2568,2569,2570,2571,2572,5598,4,385,767,1148,1529,2573,2574,2575,2576,2577,2578,2579,2580,5599,5,386,768,1149,1530,2581,2582,2583,2584,2585,2586,2587,2588,5600,6,387,769,1150,1531,2589,2590,2591,2592,2593,2594,2595,2596,5601,7,388,770,1151,1532,2597,2598,2599,2600,2601,2602,2603,2604,5602,8,389,771,1152,1533,2605,2606,2607,2608,2609,2610,2611,2612,5603,9,390,772,1153,1534,2613,2614,2615,2616,2617,2618,2619,2620,5604,10,391,773,1154,1535,2621,2622,2623,2624,2625,2626,2627,2628,5605,11,392,774,1155,1536,2629,2630,2631,2632,2633,2634,2635,2636,5606,12,393,775,1156,1537,2637,2638,2639,2640,2641,2642,2643,2644,5607,13,394,776,1157,1538,2645,2646,2647,2648,2649,2650,2651,2652,5608,14,395,777,1158,1539,2653,2654,2655,2656,2657,2658,2659,2660,5609,15,396,778,1159,1540,2661,2662,2663,2664,2665,2666,2667,2668,5610,16,397,779,1160,1541,2669,2670,2671,2672,2673,2674,2675,2676,5611,17,398,780,1161,1542,2677,2678,2679,2680,2681,2682,2683,2684,5612,18,399,781,1162,1543,2685,2686,2687,2688,2689,2690,2691,2692,5613,19,400,782,1163,1544,2693,2694,2695,2696,2697,2698,2699,2700,5614,20,401,783,1164,1545,2701,2702,2703,2704,2705,2706,2707,2708,5615,21,402,784,1165,1546,2709,2710,2711,2712,2713,2714,2715,2716,5616,22,403,785,1166,1547,2717,2718,2719,2720,2721,2722,2723,2724,5617,23,404,786,1167,1548,2725,2726,2727,2728,2729,2730,2731,2732,5618,24,405,787,1168,1549,2733,2734,2735,2736,2737,2738,2739,2740,5619,25,406,788,1169,1550,2741,2742,2743,2744,2745,2746,2747,2748,5620,26,407,789,1170,1551,2749,2750,2751,2752,2753,2754,2755,2756,5621,27,408,790,1171,1552,2757,2758,2759,2760,2761,2762,2763,2764,5622,28,409,791,1172,1553,2765,2766,2767,2768,2769,2770,2771,2772,5623,29,410,792,1173,1554,2773,2774,2775,2776,2777,2778,2779,2780,5624,30,411,793,1174,1555,2781,2782,2783,2784,2785,2786,2787,2788,5625,31,412,794,1175,1556,2789,2790,2791,2792,2793,2794,2795,2796,5626,32,413,795,1176,1557,2797,2798,2799,2800,2801,2802,2803,2804,5627,33,414,796,1177,1558,2805,2806,2807,2808,2809,2810,2811,2812,5628,34,415,797,1178,1559,2813,2814,2815,2816,2817,2818,2819,2820,5629,35,416,798,1179,1560,2821,2822,2823,2824,2825,2826,2827,2828,5630,36,417,799,1180,1561,2829,2830,2831,2832,2833,2834,2835,2836,5631,37,418,800,1181,1562,2837,2838,2839,2840,2841,2842,2843,2844,5632,38,419,801,1182,1563,2845,2846,2847,2848,2849,2850,2851,2852,5633,39,420,802,1183,1564,2853,2854,2855,2856,2857,2858,2859,2860,5634,40,421,803,1184,1565,2861,2862,2863,2864,2865,2866,2867,2868,5635,41,422,804,1185,1566,2869,2870,2871,2872,2873,2874,2875,2876,5636,42,423,805,1186,1567,2877,2878,2879,2880,2881,2882,2883,2884,5637,43,424,806,1187,1568,2885,2886,2887,2888,2889,2890,2891,2892,5638,44,425,807,1188,1569,2893,2894,2895,2896,2897,2898,2899,2900,5639,45,426,808,1189,1570,2901,2902,2903,2904,2905,2906,2907,2908,5640,46,427,809,1190,1571,2909,2910,2911,2912,2913,2914,2915,2916,5641,47,428,810,1191,1572,2917,2918,2919,2920,2921,2922,2923,2924,5642,48,429,811,1192,1573,2925,2926,2927,2928,2929,2930,2931,2932,5643,49,430,812,1193,1574,2933,2934,2935,2936,2937,2938,2939,2940,5644,50,431,813,1194,1575,2941,2942,2943,2944,2945,2946,2947,2948,5645,51,432,814,1195,1576,2949,2950,2951,2952,2953,2954,2955,2956,5646,52,433,815,1196,1577,2957,2958,2959,2960,2961,2962,2963,2964,5647,53,434,816,1197,1578,2965,2966,2967,2968,2969,2970,2971,2972,5648,54,435,817,1198,1579,2973,2974,2975,2976,2977,2978,2979,2980,5649,55,436,818,1199,1580,2981,2982,2983,2984,2985,2986,2987,2988,5650,56,437,819,1200,1581,2989,2990,2991,2992,2993,2994,2995,2996,5651,57,438,820,1201,1582,2997,2998,2999,3000,3001,3002,3003,3004,5652,58,439,821,1202,1583,3005,3006,3007,3008,3009,3010,3011,3012,5653,59,440,822,1203,1584,3013,3014,3015,3016,3017,3018,3019,3020,5654,60,441,823,1204,1585,3021,3022,3023,3024,3025,3026,3027,3028,5655,61,442,824,1205,1586,3029,3030,3031,3032,3033,3034,3035,3036,5656,62,443,825,1206,1587,3037,3038,3039,3040,3041,3042,3043,3044,5657,63,444,826,1207,1588,3045,3046,3047,3048,3049,3050,3051,3052,5658,64,445,827,1208,1589,3053,3054,3055,3056,3057,3058,3059,3060,5659,65,446,828,1209,1590,3061,3062,3063,3064,3065,3066,3067,3068,5660,66,447,829,1210,1591,3069,3070,3071,3072,3073,3074,3075,3076,5661,67,448,830,1211,1592,3077,3078,3079,3080,3081,3082,3083,3084,5662,68,449,831,1212,1593,3085,3086,3087,3088,3089,3090,3091,3092,5663,69,450,832,1213,1594,3093,3094,3095,3096,3097,3098,3099,3100,5664,70,451,833,1214,1595,3101,3102,3103,3104,3105,3106,3107,3108,5665,71,452,834,1215,1596,3109,3110,3111,3112,3113,3114,3115,3116,5666,72,453,835,1216,1597,3117,3118,3119,3120,3121,3122,3123,3124,5667,73,454,836,1217,1598,3125,3126,3127,3128,3129,3130,3131,3132,5668,74,455,837,1218,1599,3133,3134,3135,3136,3137,3138,3139,3140,5669,75,456,838,1219,1600,3141,3142,3143,3144,3145,3146,3147,3148,5670,76,457,839,1220,1601,3149,3150,3151,3152,3153,3154,3155,3156,5671,77,458,840,1221,1602,3157,3158,3159,3160,3161,3162,3163,3164,5672,78,459,841,1222,1603,3165,3166,3167,3168,3169,3170,3171,3172,5673,79,460,842,1223,1604,3173,3174,3175,3176,3177,3178,3179,3180,5674,80,461,843,1224,1605,3181,3182,3183,3184,3185,3186,3187,3188,5675,81,462,844,1225,1606,3189,3190,3191,3192,3193,3194,3195,3196,5676,82,463,845,1226,1607,3197,3198,3199,3200,3201,3202,3203,3204,5677,83,464,846,1227,1608,3205,3206,3207,3208,3209,3210,3211,3212,5678,84,465,847,1228,1609,3213,3214,3215,3216,3217,3218,3219,3220,5679,85,466,848,1229,1610,3221,3222,3223,3224,3225,3226,3227,3228,5680,86,467,849,1230,1611,3229,3230,3231,3232,3233,3234,3235,3236,5681,87,468,850,1231,1612,3237,3238,3239,3240,3241,3242,3243,3244,5682,88,469,851,1232,1613,3245,3246,3247,3248,3249,3250,3251,3252,5683,89,470,852,1233,1614,3253,3254,3255,3256,3257,3258,3259,3260,5684,90,471,853,1234,1615,3261,3262,3263,3264,3265,3266,3267,3268,5685,91,472,854,1235,1616,3269,3270,3271,3272,3273,3274,3275,3276,5686,92,473,855,1236,1617,3277,3278,3279,3280,3281,3282,3283,3284,5687,93,474,856,1237,1618,3285,3286,3287,3288,3289,3290,3291,3292,5688,94,475,857,1238,1619,3293,3294,3295,3296,3297,3298,3299,3300,5689,95,476,858,1239,1620,3301,3302,3303,3304,3305,3306,3307,3308,5690,96,477,859,1240,1621,3309,3310,3311,3312,3313,3314,3315,3316,5691,97,478,860,1241,1622,3317,3318,3319,3320,3321,3322,3323,3324,5692,98,479,861,1242,1623,3325,3326,3327,3328,3329,3330,3331,3332,5693,99,480,862,1243,1624,3333,3334,3335,3336,3337,3338,3339,3340,5694,100,481,863,1244,1625,3341,3342,3343,3344,3345,3346,3347,3348,5695,101,482,864,1245,1626,3349,3350,3351,3352,3353,3354,3355,3356,5696,102,483,865,1246,1627,3357,3358,3359,3360,3361,3362,3363,3364,5697,103,484,866,1247,1628,3365,3366,3367,3368,3369,3370,3371,3372,5698,104,485,867,1248,1629,3373,3374,3375,3376,3377,3378,3379,3380,5699,105,486,868,1249,1630,3381,3382,3383,3384,3385,3386,3387,3388,5700,106,487,869,1250,1631,3389,3390,3391,3392,3393,3394,3395,3396,5701,107,488,870,1251,1632,3397,3398,3399,3400,3401,3402,3403,3404,5702,108,489,871,1252,1633,3405,3406,3407,3408,3409,3410,3411,3412,5703,109,490,872,1253,1634,3413,3414,3415,3416,3417,3418,3419,3420,5704,110,491,873,1254,1635,3421,3422,3423,3424,3425,3426,3427,3428,5705,111,492,874,1255,1636,3429,3430,3431,3432,3433,3434,3435,3436,5706,112,493,875,1256,1637,3437,3438,3439,3440,3441,3442,3443,3444,5707,113,494,876,1257,1638,3445,3446,3447,3448,3449,3450,3451,3452,5708,114,495,877,1258,1639,3453,3454,3455,3456,3457,3458,3459,3460,5709,115,496,878,1259,1640,3461,3462,3463,3464,3465,3466,3467,3468,5710,116,497,879,1260,1641,3469,3470,3471,3472,3473,3474,3475,3476,5711,117,498,880,1261,1642,3477,3478,3479,3480,3481,3482,3483,3484,5712,118,499,881,1262,1643,3485,3486,3487,3488,3489,3490,3491,3492,5713,119,500,882,1263,1644,3493,3494,3495,3496,3497,3498,3499,3500,5714,120,501,883,1264,1645,3501,3502,3503,3504,3505,3506,3507,3508,5715,121,502,884,1265,1646,3509,3510,3511,3512,3513,3514,3515,3516,5716,122,503,885,1266,1647,3517,3518,3519,3520,3521,3522,3523,3524,5717,123,504,886,1267,1648,3525,3526,3527,3528,3529,3530,3531,3532,5718,124,505,887,1268,1649,3533,3534,3535,3536,3537,3538,3539,3540,5719,125,506,888,1269,1650,3541,3542,3543,3544,3545,3546,3547,3548,5720,126,507,889,1270,1651,3549,3550,3551,3552,3553,3554,3555,3556,5721,127,508,890,1271,1652,3557,3558,3559,3560,3561,3562,3563,3564,5722,128,509,891,1272,1653,3565,3566,3567,3568,3569,3570,3571,3572,5723,129,510,892,1273,1654,3573,3574,3575,3576,3577,3578,3579,3580,5724,130,511,893,1274,1655,3581,3582,3583,3584,3585,3586,3587,3588,5725,131,512,894,1275,1656,3589,3590,3591,3592,3593,3594,3595,3596,5726,132,513,895,1276,1657,3597,3598,3599,3600,3601,3602,3603,3604,5727,133,514,896,1277,1658,3605,3606,3607,3608,3609,3610,3611,3612,5728,134,515,897,1278,1659,3613,3614,3615,3616,3617,3618,3619,3620,5729,135,516,898,1279,1660,3621,3622,3623,3624,3625,3626,3627,3628,5730,136,517,899,1280,1661,3629,3630,3631,3632,3633,3634,3635,3636,5731,137,518,900,1281,1662,3637,3638,3639,3640,3641,3642,3643,3644,5732,138,519,901,1282,1663,3645,3646,3647,3648,3649,3650,3651,3652,5733,139,520,902,1283,1664,3653,3654,3655,3656,3657,3658,3659,3660,5734,140,521,903,1284,1665,3661,3662,3663,3664,3665,3666,3667,3668,5735,141,522,904,1285,1666,3669,3670,3671,3672,3673,3674,3675,3676,5736,142,523,905,1286,1667,3677,3678,3679,3680,3681,3682,3683,3684,5737,143,524,906,1287,1668,3685,3686,3687,3688,3689,3690,3691,3692,5738,144,525,907,1288,1669,3693,3694,3695,3696,3697,3698,3699,3700,5739,145,526,908,1289,1670,3701,3702,3703,3704,3705,3706,3707,3708,5740,146,527,909,1290,1671,3709,3710,3711,3712,3713,3714,3715,3716,5741,147,528,910,1291,1672,3717,3718,3719,3720,3721,3722,3723,3724,5742,148,529,911,1292,1673,3725,3726,3727,3728,3729,3730,3731,3732,5743,149,530,912,1293,1674,3733,3734,3735,3736,3737,3738,3739,3740,5744,150,531,913,1294,1675,3741,3742,3743,3744,3745,3746,3747,3748,5745,151,532,914,1295,1676,3749,3750,3751,3752,3753,3754,3755,3756,5746,152,533,915,1296,1677,3757,3758,3759,3760,3761,3762,3763,3764,5747,153,534,916,1297,1678,3765,3766,3767,3768,3769,3770,3771,3772,5748,154,535,917,1298,1679,3773,3774,3775,3776,3777,3778,3779,3780,5749,155,536,918,1299,1680,3781,3782,3783,3784,3785,3786,3787,3788,5750,156,537,919,1300,1681,3789,3790,3791,3792,3793,3794,3795,3796,5751,157,538,920,1301,1682,3797,3798,3799,3800,3801,3802,3803,3804,5752,158,539,921,1302,1683,3805,3806,3807,3808,3809,3810,3811,3812,5753,159,540,922,1303,1684,3813,3814,3815,3816,3817,3818,3819,3820,5754,160,541,923,1304,1685,3821,3822,3823,3824,3825,3826,3827,3828,5755,161,542,924,1305,1686,3829,3830,3831,3832,3833,3834,3835,3836,5756,162,543,925,1306,1687,3837,3838,3839,3840,3841,3842,3843,3844,5757,163,544,926,1307,1688,3845,3846,3847,3848,3849,3850,3851,3852,5758,164,545,927,1308,1689,3853,3854,3855,3856,3857,3858,3859,3860,5759,165,546,928,1309,1690,3861,3862,3863,3864,3865,3866,3867,3868,5760,166,547,929,1310,1691,3869,3870,3871,3872,3873,3874,3875,3876,5761,167,548,930,1311,1692,3877,3878,3879,3880,3881,3882,3883,3884,5762,168,549,931,1312,1693,3885,3886,3887,3888,3889,3890,3891,3892,5763,169,550,932,1313,1694,3893,3894,3895,3896,3897,3898,3899,3900,5764,170,551,933,1314,1695,3901,3902,3903,3904,3905,3906,3907,3908,5765,171,552,934,1315,1696,3909,3910,3911,3912,3913,3914,3915,3916,5766,172,553,935,1316,1697,3917,3918,3919,3920,3921,3922,3923,3924,5767,173,554,936,1317,1698,3925,3926,3927,3928,3929,3930,3931,3932,5768,174,555,937,1318,1699,3933,3934,3935,3936,3937,3938,3939,3940,5769,175,556,938,1319,1700,3941,3942,3943,3944,3945,3946,3947,3948,5770,176,557,939,1320,1701,3949,3950,3951,3952,3953,3954,3955,3956,5771,177,558,940,1321,1702,3957,3958,3959,3960,3961,3962,3963,3964,5772,178,559,941,1322,1703,3965,3966,3967,3968,3969,3970,3971,3972,5773,179,560,942,1323,1704,3973,3974,3975,3976,3977,3978,3979,3980,5774,180,561,943,1324,1705,3981,3982,3983,3984,3985,3986,3987,3988,5775,181,562,944,1325,1706,3989,3990,3991,3992,3993,3994,3995,3996,5776,182,563,945,1326,1707,3997,3998,3999,4000,4001,4002,4003,4004,5777,183,564,946,1327,1708,4005,4006,4007,4008,4009,4010,4011,4012,5778,184,565,947,1328,1709,4013,4014,4015,4016,4017,4018,4019,4020,5779,185,566,948,1329,1710,4021,4022,4023,4024,4025,4026,4027,4028,5780,186,567,949,1330,1711,4029,4030,4031,4032,4033,4034,4035,4036,5781,187,568,950,1331,1712,4037,4038,4039,4040,4041,4042,4043,4044,5782,188,569,951,1332,1713,4045,4046,4047,4048,4049,4050,4051,4052,5783,189,570,952,1333,1714,4053,4054,4055,4056,4057,4058,4059,4060,5784,190,571,953,1334,1715,4061,4062,4063,4064,4065,4066,4067,4068,5785,191,572,954,1335,1716,4069,4070,4071,4072,4073,4074,4075,4076,5786,192,573,955,1336,1717,4077,4078,4079,4080,4081,4082,4083,4084,5787,193,574,956,1337,1718,4085,4086,4087,4088,4089,4090,4091,4092,5788,194,575,957,1338,1719,4093,4094,4095,4096,4097,4098,4099,4100,5789,195,576,958,1339,1720,4101,4102,4103,4104,4105,4106,4107,4108,5790,196,577,959,1340,1721,4109,4110,4111,4112,4113,4114,4115,4116,5791,197,578,960,1341,1722,4117,4118,4119,4120,4121,4122,4123,4124,5792,198,579,961,1342,1723,4125,4126,4127,4128,4129,4130,4131,4132,5793,199,580,962,1343,1724,4133,4134,4135,4136,4137,4138,4139,4140,5794,200,581,963,1344,1725,4141,4142,4143,4144,4145,4146,4147,4148,5795,201,582,964,1345,1726,4149,4150,4151,4152,4153,4154,4155,4156,5796,202,583,965,1346,1727,4157,4158,4159,4160,4161,4162,4163,4164,5797,203,584,966,1347,1728,4165,4166,4167,4168,4169,4170,4171,4172,5798,204,585,967,1348,1729,4173,4174,4175,4176,4177,4178,4179,4180,5799,205,586,968,1349,1730,4181,4182,4183,4184,4185,4186,4187,4188,5800,206,587,969,1350,1731,4189,4190,4191,4192,4193,4194,4195,4196,5801,207,588,970,1351,1732,4197,4198,4199,4200,4201,4202,4203,4204,5802,208,589,971,1352,1733,4205,4206,4207,4208,4209,4210,4211,4212,5803,209,590,972,1353,1734,4213,4214,4215,4216,4217,4218,4219,4220,5804,210,591,973,1354,1735,4221,4222,4223,4224,4225,4226,4227,4228,5805,211,592,974,1355,1736,4229,4230,4231,4232,4233,4234,4235,4236,5806,212,593,975,1356,1737,4237,4238,4239,4240,4241,4242,4243,4244,5807,213,594,976,1357,1738,4245,4246,4247,4248,4249,4250,4251,4252,5808,214,595,977,1358,1739,4253,4254,4255,4256,4257,4258,4259,4260,5809,215,596,978,1359,1740,4261,4262,4263,4264,4265,4266,4267,4268,5810,216,597,979,1360,1741,4269,4270,4271,4272,4273,4274,4275,4276,5811,217,598,980,1361,1742,4277,4278,4279,4280,4281,4282,4283,4284,5812,218,599,981,1362,1743,4285,4286,4287,4288,4289,4290,4291,4292,5813,219,600,982,1363,1744,4293,4294,4295,4296,4297,4298,4299,4300,5814,220,601,983,1364,1745,4301,4302,4303,4304,4305,4306,4307,4308,5815,221,602,984,1365,1746,4309,4310,4311,4312,4313,4314,4315,4316,5816,222,603,985,1366,1747,4317,4318,4319,4320,4321,4322,4323,4324,5817,223,604,986,1367,1748,4325,4326,4327,4328,4329,4330,4331,4332,5818,224,605,987,1368,1749,4333,4334,4335,4336,4337,4338,4339,4340,5819,225,606,988,1369,1750,4341,4342,4343,4344,4345,4346,4347,4348,5820,226,607,989,1370,1751,4349,4350,4351,4352,4353,4354,4355,4356,5821,227,608,990,1371,1752,4357,4358,4359,4360,4361,4362,4363,4364,5822,228,609,991,1372,1753,4365,4366,4367,4368,4369,4370,4371,4372,5823,229,610,992,1373,1754,4373,4374,4375,4376,4377,4378,4379,4380,5824,230,611,993,1374,1755,4381,4382,4383,4384,4385,4386,4387,4388,5825,231,612,994,1375,1756,4389,4390,4391,4392,4393,4394,4395,4396,5826,232,613,995,1376,1757,4397,4398,4399,4400,4401,4402,4403,4404,5827,233,614,996,1377,1758,4405,4406,4407,4408,4409,4410,4411,4412,5828,234,615,997,1378,1759,4413,4414,4415,4416,4417,4418,4419,4420,5829,235,616,998,1379,1760,4421,4422,4423,4424,4425,4426,4427,4428,5830,236,617,999,1380,1761,4429,4430,4431,4432,4433,4434,4435,4436,5831,237,618,1000,1381,1762,4437,4438,4439,4440,4441,4442,4443,4444,5832,238,619,1001,1382,1763,4445,4446,4447,4448,4449,4450,4451,4452,5833,239,620,1002,1383,1764,4453,4454,4455,4456,4457,4458,4459,4460,5834,240,621,1003,1384,1765,4461,4462,4463,4464,4465,4466,4467,4468,5835,241,622,1004,1385,1766,4469,4470,4471,4472,4473,4474,4475,4476,5836,242,623,1005,1386,1767,4477,4478,4479,4480,4481,4482,4483,4484,5837,243,624,1006,1387,1768,4485,4486,4487,4488,4489,4490,4491,4492,5838,244,625,1007,1388,1769,4493,4494,4495,4496,4497,4498,4499,4500,5839,245,626,1008,1389,1770,4501,4502,4503,4504,4505,4506,4507,4508,5840,246,627,1009,1390,1771,4509,4510,4511,4512,4513,4514,4515,4516,5841,247,628,1010,1391,1772,4517,4518,4519,4520,4521,4522,4523,4524,5842,248,629,1011,1392,1773,4525,4526,4527,4528,4529,4530,4531,4532,5843,249,630,1012,1393,1774,4533,4534,4535,4536,4537,4538,4539,4540,5844,250,631,1013,1394,1775,4541,4542,4543,4544,4545,4546,4547,4548,5845,251,632,1014,1395,1776,4549,4550,4551,4552,4553,4554,4555,4556,5846,252,633,1015,1396,1777,4557,4558,4559,4560,4561,4562,4563,4564,5847,253,634,1016,1397,1778,4565,4566,4567,4568,4569,4570,4571,4572,5848,254,635,1017,1398,1779,4573,4574,4575,4576,4577,4578,4579,4580,5849,255,636,1018,1399,1780,4581,4582,4583,4584,4585,4586,4587,4588,5850,256,637,1019,1400,1781,4589,4590,4591,4592,4593,4594,4595,4596,5851,257,638,1020,1401,1782,4597,4598,4599,4600,4601,4602,4603,4604,5852,258,639,1021,1402,1783,4605,4606,4607,4608,4609,4610,4611,4612,5853,259,640,1022,1403,1784,4613,4614,4615,4616,4617,4618,4619,4620,5854,260,641,1023,1404,1785,4621,4622,4623,4624,4625,4626,4627,4628,5855,261,642,1024,1405,1786,4629,4630,4631,4632,4633,4634,4635,4636,5856,262,643,1025,1406,1787,4637,4638,4639,4640,4641,4642,4643,4644,5857,263,644,1026,1407,1788,4645,4646,4647,4648,4649,4650,4651,4652,5858,264,645,1027,1408,1789,4653,4654,4655,4656,4657,4658,4659,4660,5859,265,646,1028,1409,1790,4661,4662,4663,4664,4665,4666,4667,4668,5860,266,647,1029,1410,1791,4669,4670,4671,4672,4673,4674,4675,4676,5861,267,648,1030,1411,1792,4677,4678,4679,4680,4681,4682,4683,4684,5862,268,649,1031,1412,1793,4685,4686,4687,4688,4689,4690,4691,4692,5863,269,650,1032,1413,1794,4693,4694,4695,4696,4697,4698,4699,4700,5864,270,651,1033,1414,1795,4701,4702,4703,4704,4705,4706,4707,4708,5865,271,652,1034,1415,1796,4709,4710,4711,4712,4713,4714,4715,4716,5866,272,653,1035,1416,1797,4717,4718,4719,4720,4721,4722,4723,4724,5867,273,654,1036,1417,1798,4725,4726,4727,4728,4729,4730,4731,4732,5868,274,655,1037,1418,1799,4733,4734,4735,4736,4737,4738,4739,4740,5869,275,656,1038,1419,1800,4741,4742,4743,4744,4745,4746,4747,4748,5870,276,657,1039,1420,1801,4749,4750,4751,4752,4753,4754,4755,4756,5871,277,658,1040,1421,1802,4757,4758,4759,4760,4761,4762,4763,4764,5872,278,659,1041,1422,1803,4765,4766,4767,4768,4769,4770,4771,4772,5873,279,660,1042,1423,1804,4773,4774,4775,4776,4777,4778,4779,4780,5874,280,661,1043,1424,1805,4781,4782,4783,4784,4785,4786,4787,4788,5875,281,662,1044,1425,1806,4789,4790,4791,4792,4793,4794,4795,4796,5876,282,663,1045,1426,1807,4797,4798,4799,4800,4801,4802,4803,4804,5877,283,664,1046,1427,1808,4805,4806,4807,4808,4809,4810,4811,4812,5878,284,665,1047,1428,1809,4813,4814,4815,4816,4817,4818,4819,4820,5879,285,666,1048,1429,1810,4821,4822,4823,4824,4825,4826,4827,4828,5880,286,667,1049,1430,1811,4829,4830,4831,4832,4833,4834,4835,4836,5881,287,668,1050,1431,1812,4837,4838,4839,4840,4841,4842,4843,4844,5882,288,669,1051,1432,1813,4845,4846,4847,4848,4849,4850,4851,4852,5883,289,670,1052,1433,1814,4853,4854,4855,4856,4857,4858,4859,4860,5884,290,671,1053,1434,1815,4861,4862,4863,4864,4865,4866,4867,4868,5885,291,672,1054,1435,1816,4869,4870,4871,4872,4873,4874,4875,4876,5886,292,673,1055,1436,1817,4877,4878,4879,4880,4881,4882,4883,4884,5887,293,674,1056,1437,1818,4885,4886,4887,4888,4889,4890,4891,4892,5888,294,675,1057,1438,1819,4893,4894,4895,4896,4897,4898,4899,4900,5889,295,676,1058,1439,1820,4901,4902,4903,4904,4905,4906,4907,4908,5890,296,677,1059,1440,1821,4909,4910,4911,4912,4913,4914,4915,4916,5891,297,678,1060,1441,1822,4917,4918,4919,4920,4921,4922,4923,4924,5892,298,679,1061,1442,1823,4925,4926,4927,4928,4929,4930,4931,4932,5893,299,680,1062,1443,1824,4933,4934,4935,4936,4937,4938,4939,4940,5894,300,681,1063,1444,1825,4941,4942,4943,4944,4945,4946,4947,4948,5895,301,682,1064,1445,1826,4949,4950,4951,4952,4953,4954,4955,4956,5896,302,683,1065,1446,1827,4957,4958,4959,4960,4961,4962,4963,4964,5897,303,684,1066,1447,1828,4965,4966,4967,4968,4969,4970,4971,4972,5898,304,685,1067,1448,1829,4973,4974,4975,4976,4977,4978,4979,4980,5899,305,686,1068,1449,1830,4981,4982,4983,4984,4985,4986,4987,4988,5900,306,687,1069,1450,1831,4989,4990,4991,4992,4993,4994,4995,4996,5901,307,688,1070,1451,1832,4997,4998,4999,5000,5001,5002,5003,5004,5902,308,689,1071,1452,1833,5005,5006,5007,5008,5009,5010,5011,5012,5903,309,690,1072,1453,1834,5013,5014,5015,5016,5017,5018,5019,5020,5904,310,691,1073,1454,1835,5021,5022,5023,5024,5025,5026,5027,5028,5905,311,692,1074,1455,1836,5029,5030,5031,5032,5033,5034,5035,5036,5906,312,693,1075,1456,1837,5037,5038,5039,5040,5041,5042,5043,5044,5907,313,694,1076,1457,1838,5045,5046,5047,5048,5049,5050,5051,5052,5908,314,695,1077,1458,1839,5053,5054,5055,5056,5057,5058,5059,5060,5909,315,696,1078,1459,1840,5061,5062,5063,5064,5065,5066,5067,5068,5910,316,697,1079,1460,1841,5069,5070,5071,5072,5073,5074,5075,5076,5911,317,698,1080,1461,1842,5077,5078,5079,5080,5081,5082,5083,5084,5912,318,699,1081,1462,1843,5085,5086,5087,5088,5089,5090,5091,5092,5913,319,700,1082,1463,1844,5093,5094,5095,5096,5097,5098,5099,5100,5914,320,701,1083,1464,1845,5101,5102,5103,5104,5105,5106,5107,5108,5915,321,702,1084,1465,1846,5109,5110,5111,5112,5113,5114,5115,5116,5916,322,703,1085,1466,1847,5117,5118,5119,5120,5121,5122,5123,5124,5917,323,704,1086,1467,1848,5125,5126,5127,5128,5129,5130,5131,5132,5918,324,705,1087,1468,1849,5133,5134,5135,5136,5137,5138,5139,5140,5919,325,706,1088,1469,1850,5141,5142,5143,5144,5145,5146,5147,5148,5920,326,707,1089,1470,1851,5149,5150,5151,5152,5153,5154,5155,5156,5921,327,708,1090,1471,1852,5157,5158,5159,5160,5161,5162,5163,5164,5922,328,709,1091,1472,1853,5165,5166,5167,5168,5169,5170,5171,5172,5923,329,710,1092,1473,1854,5173,5174,5175,5176,5177,5178,5179,5180,5924,330,711,1093,1474,1855,5181,5182,5183,5184,5185,5186,5187,5188,5925,331,712,1094,1475,1856,5189,5190,5191,5192,5193,5194,5195,5196,5926,332,713,1095,1476,1857,5197,5198,5199,5200,5201,5202,5203,5204,5927,333,714,1096,1477,1858,5205,5206,5207,5208,5209,5210,5211,5212,5928,334,715,1097,1478,1859,5213,5214,5215,5216,5217,5218,5219,5220,5929,335,716,1098,1479,1860,5221,5222,5223,5224,5225,5226,5227,5228,5930,336,717,1099,1480,1861,5229,5230,5231,5232,5233,5234,5235,5236,5931,337,718,1100,1481,1862,5237,5238,5239,5240,5241,5242,5243,5244,5932,338,719,1101,1482,1863,5245,5246,5247,5248,5249,5250,5251,5252,5933,339,720,1102,1483,1864,5253,5254,5255,5256,5257,5258,5259,5260,5934,340,721,1103,1484,1865,5261,5262,5263,5264,5265,5266,5267,5268,5935,341,722,1104,1485,1866,5269,5270,5271,5272,5273,5274,5275,5276,5936,342,723,1105,1486,1867,5277,5278,5279,5280,5281,5282,5283,5284,5937,343,724,1106,1487,1868,5285,5286,5287,5288,5289,5290,5291,5292,5938,344,725,1107,1488,1869,5293,5294,5295,5296,5297,5298,5299,5300,5939,345,726,1108,1489,1870,5301,5302,5303,5304,5305,5306,5307,5308,5940,346,727,1109,1490,1871,5309,5310,5311,5312,5313,5314,5315,5316,5941,347,728,1110,1491,1872,5317,5318,5319,5320,5321,5322,5323,5324,5942,348,729,1111,1492,1873,5325,5326,5327,5328,5329,5330,5331,5332,5943,349,730,1112,1493,1874,5333,5334,5335,5336,5337,5338,5339,5340,5944,350,731,1113,1494,1875,5341,5342,5343,5344,5345,5346,5347,5348,5945,351,732,1114,1495,1876,5349,5350,5351,5352,5353,5354,5355,5356,5946,352,733,1115,1496,1877,5357,5358,5359,5360,5361,5362,5363,5364,5947,353,734,1116,1497,1878,5365,5366,5367,5368,5369,5370,5371,5372,5948,354,735,1117,1498,1879,5373,5374,5375,5376,5377,5378,5379,5380,5949,355,736,1118,1499,1880,5381,5382,5383,5384,5385,5386,5387,5388,5950,356,737,1119,1500,1881,5389,5390,5391,5392,5393,5394,5395,5396,5951,357,738,1120,1501,1882,5397,5398,5399,5400,5401,5402,5403,5404,5952,358,739,1121,1502,1883,5405,5406,5407,5408,5409,5410,5411,5412,5953,359,740,1122,1503,1884,5413,5414,5415,5416,5417,5418,5419,5420,5954,360,741,1123,1504,1885,5421,5422,5423,5424,5425,5426,5427,5428,5955,361,742,1124,1505,1886,5429,5430,5431,5432,5433,5434,5435,5436,5956,362,743,1125,1506,1887,5437,5438,5439,5440,5441,5442,5443,5444,5957,363,744,1126,1507,1888,5445,5446,5447,5448,5449,5450,5451,5452,5958,364,745,1127,1508,1889,5453,5454,5455,5456,5457,5458,5459,5460,5959,365,746,1128,1509,1890,5461,5462,5463,5464,5465,5466,5467,5468,5960,366,747,1129,1510,1891,5469,5470,5471,5472,5473,5474,5475,5476,5961,367,748,1130,1511,1892,5477,5478,5479,5480,5481,5482,5483,5484,5962,368,749,1131,1512,1893,5485,5486,5487,5488,5489,5490,5491,5492,5963,369,750,1132,1513,1894,5493,5494,5495,5496,5497,5498,5499,5500,5964,370,751,1133,1514,1895,5501,5502,5503,5504,5505,5506,5507,5508,5965,371,752,1134,1515,1896,5509,5510,5511,5512,5513,5514,5515,5516,5966,372,753,1135,1516,1897,5517,5518,5519,5520,5521,5522,5523,5524,5967,373,754,1136,1517,1898,5525,5526,5527,5528,5529,5530,5531,5532,5968,374,755,1137,1518,1899,5533,5534,5535,5536,5537,5538,5539,5540,5969,375,756,1138,1519,1900,5541,5542,5543,5544,5545,5546,5547,5548,5970,376,757,1139,1520,1901,5549,5550,5551,5552,5553,5554,5555,5556,5971,377,758,1140,1521,1902,5557,5558,5559,5560,5561,5562,5563,5564,5972,378,759,1141,1522,1903,5565,5566,5567,5568,5569,5570,5571,5572,5973,379,760,1142,1523,1904,5573,5574,5575,5576,5577,5578,5579,5580,5974,380,761,1143,1524,1905,5581,5582,5583,5584,5585,5586,5587,5588,5975,762,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2229,2230,2231,2232,2233,2234,2235,2236,2237,2238,2239,2240,2241,2242,2243,2244,2245,2246,2247,2248,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2274,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381,2382,2383,2384,2385,2386,2387,2388,2389,2390,2391,2392,2393,2394,2395,2396,2397,2398,2399,2400,2401,2402,2403,2404,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2416,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2433,2434,2435,2436,2437,2438,2439,2440,2441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2453,2454,2455,2456,2457,2458,2459,2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2471,2472,2473,2474,2475,2476,2477,2478,2479,2480,2481,2482,2483,2484,2485,2486,2487,2488,2489,2490,2491,2492,2493,2494,2495,2496,2497,2498,2499,2500,2501,2502,2503,2504,2505,2506,2507,2508,2509,2510,2511,2512,2513,2514,2515,2516,2517,2518,2519,2520,2521,2522,2523,2524,2525,2526,2527,2528,2529,2530,2531,2532,2533,2534,2535,2536,2537,2538,2539,2540,5589,5590,5591,5592,5593,5594,5976,};
diff --git a/ocean/nethack/macros.h b/ocean/nethack/macros.h
new file mode 100644
index 0000000000..82db8e046e
--- /dev/null
+++ b/ocean/nethack/macros.h
@@ -0,0 +1,233 @@
+// The keystroke dialogue with NetHack: prompt predicates and auto-dismissal,
+// plus the per-verb key sequences. Included by nethack.h after the struct.
+#pragma once
+
+// helpers
+
+static void nethack_send_key(Nethack* env, int key) {
+ env->obs.action = key;
+ env->ctx = nle_step(env->ctx, &env->obs);
+}
+
+// message ends with '?': single-key prompts NLE doesn't expose via misc[]
+static int nethack_msg_is_prompt(const Nethack* env) {
+ const unsigned char* m = env->message;
+ if (!m[0]) return 0;
+ int e = 0;
+ while (e < NLE_MESSAGE_SIZE && m[e]) e++;
+ while (e > 0 && m[e-1] == ' ') e--;
+ return e > 0 && m[e-1] == '?';
+}
+
+static int nethack_msg_contains(const Nethack* env, const char* needle) {
+ char buf[NLE_MESSAGE_SIZE + 1];
+ memcpy(buf, env->message, NLE_MESSAGE_SIZE);
+ buf[NLE_MESSAGE_SIZE] = '\0';
+ return strstr(buf, needle) != NULL;
+}
+
+// parse a getobj bracket list ("[b-d f or ?*]") into cand[]; returns count
+static int nethack_parse_candidates(const Nethack* env, char* cand, int cap) {
+ const unsigned char* m = env->message;
+ int i = 0;
+ while (i < NLE_MESSAGE_SIZE && m[i] && m[i] != '[') i++;
+ int n = 0;
+ for (i++; i < NLE_MESSAGE_SIZE && m[i] && n < cap; i++) {
+ unsigned char c = m[i];
+ if (n == 0 && (c == '-' || c == ' ' || c == '$')) continue; // leading "- " (allownone) / "$" (gold)
+ if (c == '-' && i + 1 < NLE_MESSAGE_SIZE) { // compactified run
+ for (char x = cand[n-1] + 1; x <= (char)m[i+1] && n < cap; x++)
+ cand[n++] = x;
+ i++;
+ continue;
+ }
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) cand[n++] = (char)c;
+ else break; // ' ' before "or ?*", ']', '#', ...: end of the letter list
+ }
+ return n;
+}
+
+// prompts
+
+// dismiss passive prompts (welcome, --More--, getline) until the game is back
+// at the main command prompt
+static void nethack_drain_prompts(Nethack* env) {
+ for (int i = 0; i < NETHACK_AUTODISMISS_MAX && !env->obs.done; i++) {
+ // skill-advance notice can flash by mid-drain; flag the pending auto-claim
+ if (nethack_msg_contains(env, "more confident in your")) env->enh_ready = 1;
+ int yn = env->misc[NETHACK_MISC_YN];
+ if (!yn && !env->misc[NETHACK_MISC_GETLIN] && !env->misc[NETHACK_MISC_XWAIT]) break;
+ env->obs.action = yn ? 27 : '\r';
+ env->ctx = nle_step(env->ctx, &env->obs);
+ }
+}
+
+// Answer sub-prompts the agent can't: yn prompts commit 'y' EXCEPT the
+// no-return climb (ends the game as ESCAPED) and peaceful-attack confirms
+// (hostilizes Minetown); those and everything else get ESC. Returns 1 if a
+// sub-prompt fired (the illegal_penalty condition).
+static int nethack_handle_prompts(Nethack* env) {
+ // direction prompts stay live — the agent's next key answers them
+ if (env->misc[NETHACK_MISC_YN] && nethack_msg_contains(env, "n what direction"))
+ return 0;
+ // the pray confirm is a deliberate action's own prompt: commit, no penalty
+ int praying = env->misc[NETHACK_MISC_YN] && nethack_msg_contains(env, "to pray");
+ // ring PUTON asks "Which ring-finger, Right or Left?" ('y' is invalid and
+ // aborts): the action's own prompt — answer 'r', no penalty
+ int ringq = env->misc[NETHACK_MISC_YN] && nethack_msg_contains(env, "ight or Left");
+ int illegal = !praying && !ringq && (env->misc[NETHACK_MISC_YN] || env->misc[NETHACK_MISC_GETLIN]
+ || nethack_msg_is_prompt(env));
+ if (!illegal && !praying && !ringq) {
+ if (env->misc[NETHACK_MISC_XWAIT]) nethack_drain_prompts(env);
+ return 0;
+ }
+ for (int i = 0; i < NETHACK_AUTODISMISS_MAX && !env->obs.done; i++) {
+ if (nethack_msg_contains(env, "more confident in your")) env->enh_ready = 1;
+ int yn = env->misc[NETHACK_MISC_YN];
+ if (!yn && !env->misc[NETHACK_MISC_GETLIN] && !env->misc[NETHACK_MISC_XWAIT]
+ && !nethack_msg_is_prompt(env)) break;
+ int ring = yn && nethack_msg_contains(env, "ight or Left");
+ // commit 'y' ONLY to prompts rendering y/n choices ("[yn"): getobj also
+ // polls through yn_function but wants an item LETTER — an auto-'y'
+ // there reads as slot y and re-prompts forever (zero-turn loop)
+ int commit = yn && nethack_msg_contains(env, "[yn")
+ && !nethack_msg_contains(env, "no return")
+ && !nethack_msg_contains(env, "eally attack");
+ env->obs.action = ring ? 'r' : (commit ? 'y' : 27);
+ env->ctx = nle_step(env->ctx, &env->obs);
+ }
+ if (illegal) env->stats.illegal_actions++;
+ return illegal;
+}
+
+static void nethack_answer_direction(Nethack* env, int key) {
+ if (!env->obs.done && env->misc[NETHACK_MISC_YN]
+ && nethack_msg_contains(env, "n what direction"))
+ nethack_send_key(env, key);
+}
+
+// selection menus (pickup pile, identify) yield inside xwaitforspace: answer
+// select-all + RET; on a plain --More-- the '.' just bells and RET dismisses
+static void nethack_answer_menu(Nethack* env) {
+ for (int r = 0; r < 2 && !env->obs.done && env->misc[NETHACK_MISC_XWAIT]; r++) {
+ env->obs.action = '.';
+ env->ctx = nle_step(env->ctx, &env->obs);
+ if (env->obs.done || !env->misc[NETHACK_MISC_XWAIT]) break;
+ env->obs.action = '\r';
+ env->ctx = nle_step(env->ctx, &env->obs);
+ }
+}
+
+// verbs
+
+// item-verb flow: press the command, walk the engine's prompts, choose the
+// slot's letter at the getobj gate (ESC + bad_pick if the engine refuses it);
+// returns 1 on a successful use
+static int nethack_item_use(Nethack* env, int cmd, const char* gate,
+ const char* floor, int slot, long* stat, int* bad_pick) {
+ nethack_send_key(env, cmd);
+ for (int i = 0; i < NETHACK_AUTODISMISS_MAX && !env->obs.done; i++) {
+ if (env->misc[NETHACK_MISC_XWAIT]) env->obs.action = ' ';
+ else if (env->misc[NETHACK_MISC_YN] && floor && nethack_msg_contains(env, floor)) {
+ // quaff: decline fountain/sink offers so the potion prompt follows;
+ // eat: accept floor food except the cockatrice family
+ if (cmd == 'q' || nethack_msg_contains(env, "atrice")) env->obs.action = 'n';
+ else {
+ env->stats.floor_eats++;
+ nethack_send_key(env, 'y');
+ if (stat) (*stat)++;
+ return 1;
+ }
+ }
+ else if (env->misc[NETHACK_MISC_YN] && nethack_msg_contains(env, gate)) {
+ char cand[52];
+ int n = nethack_parse_candidates(env, cand, (int)sizeof(cand));
+ char want = (char)env->inv_letters[slot];
+ int ok = 0;
+ for (int j = 0; j < n; j++)
+ if (cand[j] == want) { ok = 1; break; }
+ nethack_send_key(env, ok ? want : 27);
+ if (!ok) { if (bad_pick) *bad_pick = 1; return 0; }
+ if (stat) (*stat)++;
+ return 1;
+ }
+ else return 0;
+ env->ctx = nle_step(env->ctx, &env->obs);
+ }
+ return 0;
+}
+
+// same-slot armor swap: take off the worn piece first so upgrading is atomic
+static void nethack_wear_takeoff_conflict(Nethack* env, int slot) {
+ int gn = env->inv_glyphs[slot] - NH_GLYPH_OBJ_OFF;
+ int cat_new = (gn >= 0 && gn < NH_NUM_OBJECTS) ? nh_obj_armcat[gn] : -1;
+ if (cat_new < 0) return;
+ for (int i = 0; i < NETHACK_INV_SLOTS && env->inv_letters[i]; i++) {
+ if (!(env->inv_state[i * NLE_INV_STATE_FIELDS + 5] & 1)) continue;
+ int gi = env->inv_glyphs[i] - NH_GLYPH_OBJ_OFF;
+ int cat_i = (gi >= 0 && gi < NH_NUM_OBJECTS) ? nh_obj_armcat[gi] : -1;
+ if (cat_i == cat_new && i != slot) {
+ nethack_item_use(env, 'T', "take off", NULL, i, NULL, NULL);
+ env->stats.armor_swaps++;
+ return;
+ }
+ }
+}
+
+static void nethack_verb_wield(Nethack* env, int slot, int* bad_pick) {
+ // re-selecting the wielded weapon unwields (keeps WIELD reversible)
+ if (env->inv_state[slot * NLE_INV_STATE_FIELDS + 5] & 2) {
+ nethack_send_key(env, 'w');
+ if (!env->obs.done && env->misc[NETHACK_MISC_YN]
+ && nethack_msg_contains(env, "wield"))
+ nethack_send_key(env, '-');
+ env->stats.verb_uses[NETHACK_ACT_WIELD]++;
+ return;
+ }
+ nethack_item_use(env, 'w', "wield", NULL, slot, &env->stats.verb_uses[NETHACK_ACT_WIELD], bad_pick);
+}
+
+// engrave Elbereth with a fingertip (E, '-', "Elbereth", RET) — the early
+// game's strongest panic button; each stage gated on its expected prompt,
+// aborts fall through to nethack_handle_prompts
+static void nethack_do_elbereth(Nethack* env) {
+ env->obs.action = 'E';
+ env->ctx = nle_step(env->ctx, &env->obs);
+ if (env->obs.done || !env->misc[NETHACK_MISC_YN]
+ || !nethack_msg_contains(env, "write with")) return;
+ env->obs.action = '-';
+ env->ctx = nle_step(env->ctx, &env->obs);
+ // the dust --More-- raises xwait ALONGSIDE the getlin — clear it before
+ // typing; decline "add to current engraving?" so the fresh text replaces it
+ const char* c = "Elbereth\r";
+ for (int i = 0; i < NETHACK_AUTODISMISS_MAX && !env->obs.done && *c; i++) {
+ if (env->misc[NETHACK_MISC_XWAIT]) env->obs.action = ' ';
+ else if (env->misc[NETHACK_MISC_YN]
+ && nethack_msg_contains(env, "current engraving")) env->obs.action = 'n';
+ else if (env->misc[NETHACK_MISC_GETLIN]) env->obs.action = (unsigned char)*c++;
+ else break;
+ env->ctx = nle_step(env->ctx, &env->obs);
+ }
+}
+
+// claim a banked skill advance the moment its notice appears (zero-turn,
+// unconditionally good): #enhance, then 'a' = the first advanceable skill
+static void nethack_auto_enhance(Nethack* env) {
+ if (env->obs.done) return;
+ if (nethack_msg_contains(env, "more confident in your")) env->enh_ready = 1;
+ if (!env->enh_ready) return;
+ env->enh_ready = 0;
+ env->stats.enhances++;
+ nethack_send_key(env, '#');
+ // the extcmd prompt is NOT getlin-flagged: it renders "# " on the topline
+ // and autocompletes; typing past the completion point is accepted
+ if (!env->obs.done && nethack_msg_contains(env, "# ")) {
+ for (const char* c = "enhance\r"; !env->obs.done && *c; c++)
+ nethack_send_key(env, (unsigned char)*c);
+ if (!env->obs.done && env->misc[NETHACK_MISC_XWAIT]) {
+ nethack_send_key(env, 'a');
+ }
+ }
+ nethack_drain_prompts(env);
+ if (!env->obs.done) nle_obs_refresh(env->ctx, &env->obs);
+}
diff --git a/ocean/nethack/nethack.c b/ocean/nethack/nethack.c
new file mode 100644
index 0000000000..755ed178d6
--- /dev/null
+++ b/ocean/nethack/nethack.c
@@ -0,0 +1,448 @@
+#include
+#include
+#include
+#include "nethack.h"
+#include "../../src/puffernet.h"
+#include "glyph_map.h"
+
+// single-agent env, reset immediately (training's c_reset is lazy)
+static void env_open(Nethack* env) {
+ memset(env, 0, sizeof(*env));
+ env->num_agents = 1;
+ env->observations = (unsigned char*)calloc(NETHACK_OBS_SIZE, 1);
+ env->actions = (float*)calloc(14, sizeof(float)); // {verb, 12 per-verb slots, direction}
+ env->action_mask = (unsigned char*)calloc(NETHACK_NUM_ACTIONS
+ + 12 * NETHACK_INV_SLOTS + NETHACK_NUM_DIRS, 1);
+ env->rewards = (float*)calloc(1, sizeof(float));
+ env->terminals = (float*)calloc(1, sizeof(float));
+ init(env);
+ nethack_do_reset(env);
+}
+
+static void env_close(Nethack* env) {
+ c_close(env);
+ free(env->observations); free(env->actions); free(env->rewards); free(env->terminals);
+ free(env->action_mask);
+}
+
+// CPU port of the CUDA encoder (src/nethack.cu) + puffernet MinGRU/decoder;
+// weight order matches param registration: encoder, decoder, mingru
+#define DEMO_VOCAB 5977
+#define DEMO_EMBED 32
+#define DEMO_BL_FEAT (25 + 7 + 13 + NETHACK_NUM_ACTIONS + NETHACK_NUM_OCLASSES + 2 + 8 + 2)
+#define DEMO_INV_HID 16 // 16-dim slot rep: pool bottleneck + decoder key (unified)
+#define DEMO_INV_FLAT (NETHACK_INV_SLOTS * DEMO_INV_HID)
+#define DEMO_INV_POOL 128
+#define DEMO_SFEAT 24 // buc4 + known+spe + quan + ero2 + flags7 + tk + armcat7
+#define DEMO_OD (NETHACK_NUM_ACTIONS + 12 * NETHACK_INV_SLOTS + NETHACK_NUM_DIRS)
+#define DEMO_NUM_HEADS 14
+#define DEMO_PTR_HEADS 12
+#define DEMO_QDIM (DEMO_PTR_HEADS * DEMO_INV_HID)
+#define DEMO_DEC_PAD 32
+#define DEMO_DEC_LIN (NETHACK_NUM_ACTIONS + NETHACK_NUM_DIRS + 1)
+#define DEMO_LOC_IN (NETHACK_CROP_GRID * DEMO_EMBED) // 9x9 crop, per-cell embeds
+#define DEMO_LOC_HID 256
+#define DEMO_PW 5
+#define DEMO_PH 5
+#define DEMO_PX 16
+#define DEMO_PY 5
+#define DEMO_TOK (DEMO_PX * DEMO_PY) // 5x5 patches over 79x21
+#define DEMO_PCELLS (DEMO_PW * DEMO_PH) // off-map cells read the pad glyph
+#define DEMO_P1 16
+#define DEMO_GLB_IN (DEMO_PCELLS * DEMO_EMBED) // per-patch flatten (glyph slice)
+#define DEMO_GLB_HID 128
+// trigram message branch, mirroring NH_MSG_* in src/nethack.cu
+#define DEMO_MSG_LEN NETHACK_MSG_LEN
+#define DEMO_MSG_VOCAB 4096
+#define DEMO_MSG_LOG2V 12
+#define DEMO_MSG_HID 32
+#define DEMO_MSG_CONCAT_OFF (DEMO_LOC_HID + DEMO_GLB_HID + DEMO_INV_POOL + 64 + DEMO_BL_FEAT)
+#define DEMO_CONCAT (DEMO_MSG_CONCAT_OFF + DEMO_MSG_HID)
+
+// per-blstat normalization, mirroring NH_BL_SCALE / NH_BL_ISLOG in src/nethack.cu
+static const float DEMO_BL_SCALE[27] = {
+ 1.f/79, 1.f/21,
+ 1.f/25, 1.f/125, 1.f/25, 1.f/25, 1.f/25, 1.f/25, 1.f/25,
+ 0.1f, 1.f/200, 1.f/200, 1.f/50, 0.1f,
+ 1.f/100, 1.f/100, 1.f/10, 1.f/10, 1.f/30,
+ 0.1f, 0.1f, 0.f, 1.f/4, 0.f, 1.f/50, 0.f, 1.f, // dnum one-hot (scale dead)
+};
+static const int DEMO_BL_ISLOG[27] =
+ {0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,1,0,0,0,0,0,0};
+
+typedef struct {
+ float *embed; // (5977, 32) E_res
+ float *ekind_w, *esub_w; // (14, 32), (944, 32) factor tables
+ float *e_eff; // materialized E_res + E_kind + E_sub
+ float *loc_w, *loc_b; // (256, 2592), (256)
+ float *g1_w, *g1_xy, *g1_b; // (16, 800), (16, 2), (16): per-patch embed+flatten + hero dx,dy -> 16
+ float *g2_w, *g2_b; // (128, 16), (128): 16 -> 128, maxed over tokens
+ float *inv1_w, *inv1_b; // (16, 32), (16): per-slot features (pointer keys)
+ float *inv1s_w; // (16, 24): gated item-state path into the slot MLP
+ float *inv2_w, *inv2_b; // (128, 16), (128): pooled trunk summary (max over slots)
+ float *bl_w, *bl_b; // (64, DEMO_BL_FEAT), (64)
+ float *proj_w, *proj_b; // (H, DEMO_CONCAT), (H)
+ float *msg_w; // (4096, 32) trigram embedding table
+ float *dec_lin; // (32, H) bias-free; rows [22 verb | 8 dir | value], 31 used
+ float *dec_q; // (192, H): twelve stacked 16-dim query projections
+ float *dec_k; // (16, 16): key projection over slot features
+ float *dec_tau; // (12,): per-head log cosine temperature
+ MinGRU* mingru;
+ Multidiscrete* md;
+ int hidden_size, num_layers, num_actions;
+ float x[DEMO_LOC_IN]; // crop cell embeds, flattened
+ float px[DEMO_GLB_IN]; // one patch's cell embeds, flattened
+ float t16[DEMO_P1];
+ float t128[DEMO_GLB_HID];
+ float slots[DEMO_INV_FLAT]; // per-slot post-relu features (decoder keys)
+ float concat[DEMO_CONCAT]; // [local hid | global hid | inv pool | bl hidden | bl feats | msg]
+ float logits[DEMO_OD + 1]; // assembled decoder output; last entry is value
+ float* hidden; // (hidden_size)
+} NethackNet;
+
+// (hidden, layers) from the checkpoint float count:
+// total = ENC_FIXED + H*(DEMO_CONCAT + 1) + H*(32 + 192) + DEC_FIXED + L * 3*H*H
+// All tensors land on 8-float boundaries; only tau (12) needs padding (+4).
+#define DEMO_ENC_FIXED (DEMO_VOCAB*DEMO_EMBED \
+ + NH_GM_NKIND*DEMO_EMBED + NH_GM_NSUB*DEMO_EMBED \
+ + DEMO_LOC_HID*DEMO_LOC_IN + DEMO_LOC_HID \
+ + DEMO_P1*DEMO_GLB_IN + DEMO_P1*2 + DEMO_P1 \
+ + DEMO_GLB_HID*DEMO_P1 + DEMO_GLB_HID \
+ + DEMO_INV_HID*DEMO_EMBED + DEMO_INV_HID \
+ + DEMO_INV_HID*DEMO_SFEAT \
+ + DEMO_INV_POOL*DEMO_INV_HID + DEMO_INV_POOL \
+ + 64*DEMO_BL_FEAT + 64 \
+ + DEMO_MSG_VOCAB*DEMO_MSG_HID)
+#define DEMO_DEC_FIXED (DEMO_INV_HID*DEMO_INV_HID + 16) // k_w + tau padded 12->16
+// ambiguities are possible; prefer the fewest layers (real configs have <= 8)
+static int demo_infer_arch(int total, int* hidden, int* layers, int* actions) {
+ int best_l = 1 << 30;
+ for (int H = 8; H <= 4096; H += 8) {
+ long rem = (long)total - DEMO_ENC_FIXED - DEMO_DEC_FIXED
+ - (long)H * (DEMO_CONCAT + 1 + DEMO_DEC_PAD + DEMO_QDIM);
+ long per_layer = 3L * H * H;
+ if (rem <= 0) break;
+ if (rem % per_layer) continue;
+ long L = rem / per_layer;
+ if (L >= 1 && L < best_l) { best_l = (int)L; *hidden = H; *layers = (int)L; *actions = NETHACK_NUM_ACTIONS; }
+ }
+ return best_l == 1 << 30 ? -1 : 0;
+}
+
+static NethackNet* make_nethack_net(Weights* w) {
+ NethackNet* net = (NethackNet*)calloc(1, sizeof(NethackNet));
+ if (demo_infer_arch(w->size - 7, &net->hidden_size, &net->num_layers, &net->num_actions) != 0) {
+ fprintf(stderr, "nethack demo: cannot infer arch from %d floats — "
+ "checkpoint is not a nethack policy with %d actions?\n",
+ w->size - 7, NETHACK_NUM_ACTIONS);
+ exit(1);
+ }
+ fprintf(stderr, "nethack demo: hidden=%d layers=%d actions=%d (%d floats)\n",
+ net->hidden_size, net->num_layers, net->num_actions, w->size - 7);
+ net->hidden = (float*)calloc(net->hidden_size, sizeof(float));
+ net->embed = get_weights_aligned(w, DEMO_VOCAB * DEMO_EMBED);
+ net->ekind_w = get_weights_aligned(w, NH_GM_NKIND * DEMO_EMBED);
+ net->esub_w = get_weights_aligned(w, NH_GM_NSUB * DEMO_EMBED);
+ net->loc_w = get_weights_aligned(w, DEMO_LOC_HID * DEMO_LOC_IN);
+ net->loc_b = get_weights_aligned(w, DEMO_LOC_HID);
+ net->g1_w = get_weights_aligned(w, DEMO_P1 * DEMO_GLB_IN);
+ net->g1_xy = get_weights_aligned(w, DEMO_P1 * 2);
+ net->g1_b = get_weights_aligned(w, DEMO_P1);
+ net->g2_w = get_weights_aligned(w, DEMO_GLB_HID * DEMO_P1);
+ net->g2_b = get_weights_aligned(w, DEMO_GLB_HID);
+ net->inv1_w = get_weights_aligned(w, DEMO_INV_HID * DEMO_EMBED);
+ net->inv1_b = get_weights_aligned(w, DEMO_INV_HID);
+ net->inv1s_w = get_weights_aligned(w, DEMO_INV_HID * DEMO_SFEAT);
+ net->inv2_w = get_weights_aligned(w, DEMO_INV_POOL * DEMO_INV_HID);
+ net->inv2_b = get_weights_aligned(w, DEMO_INV_POOL);
+ net->bl_w = get_weights_aligned(w, 64 * DEMO_BL_FEAT);
+ net->bl_b = get_weights_aligned(w, 64);
+ net->proj_w = get_weights_aligned(w, net->hidden_size * DEMO_CONCAT);
+ net->proj_b = get_weights_aligned(w, net->hidden_size);
+ net->msg_w = get_weights_aligned(w, DEMO_MSG_VOCAB * DEMO_MSG_HID);
+ net->dec_lin = get_weights_aligned(w, DEMO_DEC_PAD * net->hidden_size);
+ net->dec_q = get_weights_aligned(w, DEMO_QDIM * net->hidden_size);
+ net->dec_k = get_weights_aligned(w, DEMO_INV_HID * DEMO_INV_HID);
+ net->dec_tau = get_weights_aligned(w, DEMO_PTR_HEADS);
+ net->mingru = make_mingru(w, 1, net->hidden_size, net->num_layers);
+ static int logit_sizes[DEMO_NUM_HEADS] = {
+ NETHACK_NUM_ACTIONS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS,
+ NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS,
+ NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS, NETHACK_INV_SLOTS,
+ NETHACK_INV_SLOTS, NETHACK_NUM_DIRS};
+ net->md = make_multidiscrete(1, logit_sizes, DEMO_NUM_HEADS);
+ assert(w->idx == w->size - 7);
+ // materialize the residual-factorized embedding once (host, load time)
+ net->e_eff = (float*)malloc((size_t)DEMO_VOCAB * DEMO_EMBED * sizeof(float));
+ for (int g = 0; g < DEMO_VOCAB; g++)
+ for (int d = 0; d < DEMO_EMBED; d++)
+ net->e_eff[g * DEMO_EMBED + d] = net->embed[g * DEMO_EMBED + d]
+ + net->ekind_w[nh_glyph_kind[g] * DEMO_EMBED + d]
+ + net->esub_w[nh_glyph_sub[g] * DEMO_EMBED + d];
+ return net;
+}
+
+static inline int demo_msg_lc(int c) {
+ return (c >= 'A' && c <= 'Z') ? c + 32 : c; // lowercase; keep spaces/punct
+}
+static inline int demo_msg_hash(int c0, int c1, int c2) {
+ unsigned key = ((unsigned)c0 << 16) | ((unsigned)c1 << 8) | (unsigned)c2;
+ return (int)((key * 2654435761u) >> (32 - DEMO_MSG_LOG2V));
+}
+// normalized-sum trigram bag over the null-terminated topline; scaled by
+// 1/sqrt(count+1), no relu (raw signed summary)
+static void demo_msg_pool(NethackNet* net, const unsigned char* obs, float* out) {
+ const unsigned char* m = obs + NETHACK_OFF_MSG;
+ for (int d = 0; d < DEMO_MSG_HID; d++) out[d] = 0.0f;
+ int count = 0;
+ for (int t = 0; t <= DEMO_MSG_LEN - 3; t++) {
+ int c0 = m[t], c1 = m[t + 1], c2 = m[t + 2];
+ if (c0 == 0 || c1 == 0 || c2 == 0) break;
+ int id = demo_msg_hash(demo_msg_lc(c0), demo_msg_lc(c1), demo_msg_lc(c2));
+ count++;
+ for (int d = 0; d < DEMO_MSG_HID; d++)
+ out[d] += net->msg_w[(size_t)id * DEMO_MSG_HID + d];
+ }
+ float scale = 1.0f / sqrtf((float)count + 1.0f);
+ for (int d = 0; d < DEMO_MSG_HID; d++) out[d] *= scale;
+}
+
+// blstats/extra live at unaligned byte offsets: assemble, don't cast
+static int32_t demo_i32(const unsigned char* p) {
+ int32_t v;
+ memcpy(&v, p, 4);
+ return v;
+}
+
+static int demo_glyph_at(const int16_t* glyphs, int r, int c) {
+ if (r < 0 || r >= NH_ROWS || c < 0 || c >= NH_COLS) return NETHACK_PAD_GLYPH;
+ int g = glyphs[r * NH_COLS + c];
+ if (g < 0) g = 0;
+ if (g >= DEMO_VOCAB) g = DEMO_VOCAB - 1;
+ return g;
+}
+
+static int nethack_net_forward(NethackNet* net, const unsigned char* obs) { // fills decoder->output
+ const int16_t* glyphs = (const int16_t*)(obs + NETHACK_OFF_GLYPHS);
+ const unsigned char* bl = obs + NETHACK_OFF_BLSTATS;
+
+ // local view: per-cell embeds of the egocentric crop, flattened
+ int hx = demo_i32(bl), hy = demo_i32(bl + 4);
+ int half = NETHACK_CROP / 2;
+ for (int p = 0; p < NETHACK_CROP_GRID; p++) {
+ int g = demo_glyph_at(glyphs, hy - half + p / NETHACK_CROP,
+ hx - half + p % NETHACK_CROP);
+ memcpy(net->x + p * DEMO_EMBED, net->e_eff + g * DEMO_EMBED,
+ DEMO_EMBED * sizeof(float));
+ }
+ _linear(net->x, net->loc_w, net->loc_b, net->concat, 1, DEMO_LOC_IN, DEMO_LOC_HID);
+ _relu(net->concat, net->concat, DEMO_LOC_HID);
+
+ // global view: per patch embed+flatten + normalized hero (dx,dy) -> 16 ->
+ // 128, elementwise max over the 80 tokens (off-map cells of ragged edge
+ // patches read the pad glyph)
+ float* glb = net->concat + DEMO_LOC_HID;
+ for (int o = 0; o < DEMO_GLB_HID; o++) glb[o] = -1e30f;
+ for (int tk = 0; tk < DEMO_TOK; tk++) {
+ int r0 = (tk / DEMO_PX) * DEMO_PH, c0 = (tk % DEMO_PX) * DEMO_PW;
+ for (int pos = 0; pos < DEMO_PCELLS; pos++) {
+ int g = demo_glyph_at(glyphs, r0 + pos / DEMO_PW, c0 + pos % DEMO_PW);
+ memcpy(net->px + pos * DEMO_EMBED, net->e_eff + g * DEMO_EMBED,
+ DEMO_EMBED * sizeof(float));
+ }
+ float dx = (c0 + 0.5f * (DEMO_PW - 1) - hx) / (float)NH_COLS;
+ float dy = (r0 + 0.5f * (DEMO_PH - 1) - hy) / (float)NH_ROWS;
+ _linear(net->px, net->g1_w, net->g1_b, net->t16, 1, DEMO_GLB_IN, DEMO_P1);
+ for (int k = 0; k < DEMO_P1; k++) {
+ net->t16[k] += dx * net->g1_xy[k * 2] + dy * net->g1_xy[k * 2 + 1];
+ if (net->t16[k] < 0.f) net->t16[k] = 0.f;
+ }
+ _linear(net->t16, net->g2_w, net->g2_b, net->t128, 1, DEMO_P1, DEMO_GLB_HID);
+ for (int o = 0; o < DEMO_GLB_HID; o++)
+ if (net->t128[o] > glb[o]) glb[o] = net->t128[o];
+ }
+ _relu(glb, glb, DEMO_GLB_HID);
+
+ // inventory entities: per-slot embed -> shared 32->32 linear+relu (kept
+ // as the pointer decoder's keys), then 32 -> 128 with max over slots for
+ // the trunk (matches the CUDA fused pool)
+ const int16_t* inv = (const int16_t*)(obs + NETHACK_OFF_INV);
+ const signed char* invst = (const signed char*)(obs + NETHACK_OFF_INVST);
+ for (int slot = 0; slot < NETHACK_INV_SLOTS; slot++) {
+ int g = inv[slot];
+ if (g < 0) g = 0;
+ if (g >= DEMO_VOCAB) g = DEMO_VOCAB - 1;
+ const signed char* st = invst + slot * NLE_INV_STATE_FIELDS;
+ float sf[DEMO_SFEAT];
+ for (int c = 0; c < 4; c++) sf[c] = st[0] == c ? 1.0f : 0.0f;
+ int spe_known = st[1] != -128;
+ sf[4] = (float)spe_known;
+ sf[5] = spe_known ? (float)st[1] * 0.1f : 0.0f;
+ sf[6] = log1pf(fmaxf((float)st[2], 0.0f)) * 0.5f;
+ sf[7] = (float)st[3] * (1.0f / 3.0f);
+ sf[8] = (float)st[4] * (1.0f / 3.0f);
+ for (int c = 0; c < 7; c++) sf[9 + c] = (float)((st[5] >> c) & 1);
+ sf[16] = (float)st[6];
+ int ot = inv[slot] - NH_GLYPH_OBJ_OFF; // armor slot category one-hot
+ int cat = (ot >= 0 && ot < NH_NUM_OBJECTS) ? nh_obj_armcat[ot] : -1;
+ for (int c = 0; c < 7; c++) sf[17 + c] = cat == c ? 1.0f : 0.0f;
+ float* h32 = net->slots + slot * DEMO_INV_HID;
+ _linear(net->e_eff + g * DEMO_EMBED, net->inv1_w, net->inv1_b,
+ h32, 1, DEMO_EMBED, DEMO_INV_HID);
+ for (int k = 0; k < DEMO_INV_HID; k++)
+ for (int j = 0; j < DEMO_SFEAT; j++)
+ h32[k] += net->inv1s_w[k * DEMO_SFEAT + j] * sf[j];
+ _relu(h32, h32, DEMO_INV_HID);
+ }
+ float* invp = net->concat + DEMO_LOC_HID + DEMO_GLB_HID;
+ for (int o = 0; o < DEMO_INV_POOL; o++) {
+ float best = -1e30f;
+ for (int slot = 0; slot < NETHACK_INV_SLOTS; slot++) {
+ float v = 0.0f;
+ for (int k = 0; k < DEMO_INV_HID; k++)
+ v += net->inv2_w[o * DEMO_INV_HID + k] * net->slots[slot * DEMO_INV_HID + k];
+ if (v > best) best = v;
+ }
+ invp[o] = fmaxf(best + net->inv2_b[o], 0.0f);
+ }
+
+ // blstats+extra features (25 scalars, hunger 7, cond bits 13, prev verb
+ // one-hot, inv class counts, hp/ene frac, dnum one-hot, engraving bits)
+ float* f = net->concat + DEMO_LOC_HID + DEMO_GLB_HID + DEMO_INV_POOL + 64;
+ int j = 0;
+ for (int i = 0; i < 27; i++) {
+ if (i == 21 || i == 25) continue; // hunger, condition: expanded below
+ float v = (float)demo_i32(bl + 4*i);
+ f[j++] = DEMO_BL_ISLOG[i] ? log1pf(fmaxf(v, 0.f)) * DEMO_BL_SCALE[i]
+ : v * DEMO_BL_SCALE[i];
+ }
+ int h21 = demo_i32(bl + 4*21);
+ int hunger = h21 < 0 ? 0 : (h21 > 6 ? 6 : h21);
+ for (int h = 0; h < 7; h++) f[j++] = (h == hunger) ? 1.f : 0.f;
+ for (int k = 0; k < 13; k++) f[j++] = (float)(((uint32_t)demo_i32(bl + 4*25) >> k) & 1u);
+ const unsigned char* ex = obs + NETHACK_OFF_EXTRA;
+ for (int h = 0; h < NETHACK_NUM_ACTIONS; h++) f[j++] = (h == demo_i32(ex + 4)) ? 1.f : 0.f;
+ for (int k = 0; k < NETHACK_NUM_OCLASSES; k++) f[j++] = (float)demo_i32(ex + 4*(2 + k)) * 0.125f;
+ for (int p = 0; p < 2; p++) { // hp_frac, ene_frac
+ int cur = demo_i32(bl + 4*(p ? 14 : 10)), mx = demo_i32(bl + 4*(p ? 15 : 11));
+ f[j++] = fminf(fmaxf((float)cur / (float)(mx > 1 ? mx : 1), 0.f), 1.f);
+ }
+ int d23 = demo_i32(bl + 4*23);
+ int dnum = d23 < 0 ? 0 : (d23 > 7 ? 7 : d23);
+ for (int d = 0; d < 8; d++) f[j++] = (d == dnum) ? 1.f : 0.f;
+ int engr = demo_i32(ex);
+ f[j++] = engr >= 1 ? 1.f : 0.f; // any engraving underfoot
+ f[j++] = engr >= 2 ? 1.f : 0.f; // active Elbereth
+ for (int k = 0; k < DEMO_BL_FEAT; k++) f[k] = fminf(fmaxf(f[k], -1.f), 1.f);
+
+ float* blout = net->concat + DEMO_LOC_HID + DEMO_GLB_HID + DEMO_INV_POOL;
+ _linear(f, net->bl_w, net->bl_b, blout, 1, DEMO_BL_FEAT, 64);
+ _relu(blout, blout, 64);
+
+ demo_msg_pool(net, obs, net->concat + DEMO_MSG_CONCAT_OFF);
+
+ _linear(net->concat, net->proj_w, net->proj_b, net->hidden, 1, DEMO_CONCAT, net->hidden_size);
+ _relu(net->hidden, net->hidden, net->hidden_size);
+
+ mingru(net->mingru, net->hidden);
+
+ // pointer decoder: [22 verb | 12x55 slots | 8 dir | value]. verb/dir/value
+ // from one bias-free linear; slot logit i = tau_h * cos(q_h, k_i) with
+ // keys k_i projected from the per-slot features above.
+ float* hs = net->mingru->output;
+ int H = net->hidden_size;
+ float tmp[DEMO_DEC_LIN];
+ for (int r = 0; r < DEMO_DEC_LIN; r++) {
+ float acc = 0.0f;
+ for (int k = 0; k < H; k++) acc += net->dec_lin[r * H + k] * hs[k];
+ tmp[r] = acc;
+ }
+ float q[DEMO_QDIM];
+ for (int r = 0; r < DEMO_QDIM; r++) {
+ float acc = 0.0f;
+ for (int k = 0; k < H; k++) acc += net->dec_q[r * H + k] * hs[k];
+ q[r] = acc;
+ }
+ float kmat[DEMO_INV_FLAT], kn[NETHACK_INV_SLOTS];
+ for (int i = 0; i < NETHACK_INV_SLOTS; i++) {
+ float nk = 0.0f;
+ for (int r = 0; r < DEMO_INV_HID; r++) {
+ float acc = 0.0f;
+ for (int k = 0; k < DEMO_INV_HID; k++)
+ acc += net->dec_k[r * DEMO_INV_HID + k] * net->slots[i * DEMO_INV_HID + k];
+ kmat[i * DEMO_INV_HID + r] = acc;
+ nk += acc * acc;
+ }
+ kn[i] = sqrtf(nk) + 1e-6f;
+ }
+ for (int a = 0; a < NETHACK_NUM_ACTIONS; a++) net->logits[a] = tmp[a];
+ for (int h = 0; h < DEMO_PTR_HEADS; h++) {
+ const float* qh = q + h * DEMO_INV_HID;
+ float nq = 0.0f;
+ for (int k = 0; k < DEMO_INV_HID; k++) nq += qh[k] * qh[k];
+ nq = sqrtf(nq) + 1e-6f;
+ for (int i = 0; i < NETHACK_INV_SLOTS; i++) {
+ float dot = 0.0f;
+ for (int k = 0; k < DEMO_INV_HID; k++)
+ dot += qh[k] * kmat[i * DEMO_INV_HID + k];
+ net->logits[NETHACK_NUM_ACTIONS + h * NETHACK_INV_SLOTS + i] =
+ expf(net->dec_tau[h]) * dot / (nq * kn[i]);
+ }
+ }
+ for (int d = 0; d <= NETHACK_NUM_DIRS; d++) // 8 dirs + value
+ net->logits[NETHACK_NUM_ACTIONS + DEMO_PTR_HEADS * NETHACK_INV_SLOTS + d] =
+ tmp[NETHACK_NUM_ACTIONS + d];
+ return 0;
+}
+
+static void run_demo(long max_steps, int frame_ms) {
+ const char* wpath = getenv("NH_WEIGHTS");
+ if (!wpath) wpath = "resources/nethack/nethack_weights.bin";
+ Weights* w = load_weights((char*)wpath);
+ if (!w) {
+ fprintf(stderr, "nethack demo: %s missing (copy a checkpoint there)\n", wpath);
+ exit(1);
+ }
+ NethackNet* net = make_nethack_net(w);
+
+ Nethack env;
+ env_open(&env);
+ const char* seed_env = getenv("NH_SEED"); // fixed seed replays a run
+ srand(seed_env ? (unsigned)strtoul(seed_env, NULL, 10) : (unsigned)time(NULL));
+
+ float ep_score = 0, ep_len = 0; // log totals at last episode end
+ float acts_f[DEMO_NUM_HEADS];
+ for (long t = 0; t < max_steps; t++) {
+ nethack_net_forward(net, env.observations);
+ for (int i = 0; i < DEMO_OD; i++)
+ if (!env.action_mask[i]) net->logits[i] = -1e9f;
+ softmax_multidiscrete(net->md, net->logits, acts_f);
+ for (int h = 0; h < DEMO_NUM_HEADS; h++) env.actions[h] = acts_f[h];
+ c_step(&env);
+ if (frame_ms > 0) {
+ c_render(&env);
+ usleep(frame_ms * 1000);
+ }
+ if (env.terminals[0] > 0.5f) {
+ fprintf(stderr, "episode end: score=%.0f len=%.0f\n",
+ env.log.score - ep_score, env.log.episode_length - ep_len);
+ ep_score = env.log.score;
+ ep_len = env.log.episode_length;
+ memset(net->mingru->state, 0,
+ (size_t)net->num_layers * net->hidden_size * sizeof(float));
+ }
+ }
+ if (env.log.n > 0)
+ printf("episodes=%.0f avg_score=%.1f\n", env.log.n, env.log.score / env.log.n);
+ env_close(&env);
+ free_mingru(net->mingru);
+ free(net->md); free(net->hidden); free(net->e_eff); free(net);
+ free(w);
+}
+
+// ./nethack [N_STEPS] [MS_PER_FRAME (0 = headless)]
+int main(int argc, char** argv) {
+ run_demo((argc >= 2) ? atol(argv[1]) : 1000000,
+ (argc >= 3) ? atoi(argv[2]) : 50);
+ return 0;
+}
diff --git a/ocean/nethack/nethack.h b/ocean/nethack/nethack.h
new file mode 100644
index 0000000000..9467d3a835
--- /dev/null
+++ b/ocean/nethack/nethack.h
@@ -0,0 +1,617 @@
+// NetHack env, obs layout mirrored by src/nethack.cu
+// One env per agent, each owning an nle_ctx_t and a private vardir on tmpfs.
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "fs.h"
+
+// nletypes.h, not nle.h: nle.h's `settings` macro would rewrite env->settings
+#include "nletypes.h"
+
+extern nle_ctx_t* nle_start(nle_obs*, FILE*, nle_settings*);
+extern nle_ctx_t* nle_step(nle_ctx_t*, nle_obs*);
+extern nle_ctx_t* nle_obs_refresh(nle_ctx_t*, nle_obs*);
+extern void nle_end(nle_ctx_t*);
+
+#include "netlib.h"
+
+typedef struct Nethack {
+ Log log;
+ unsigned char* observations;
+ float* actions;
+ float* rewards;
+ float* terminals;
+ unsigned char* action_mask;
+ int num_agents;
+ int pending_reset; // NLE's coroutine must reset on a stepping thread
+
+ // engine handle
+ nle_ctx_t* ctx;
+ nle_obs obs;
+ nle_settings settings;
+ char vardir[1024];
+
+ // NLE-written buffers
+ short glyphs[NH_GRID];
+ long blstats[NLE_BLSTATS_SIZE];
+ unsigned char chars[NH_GRID];
+ unsigned char message[NLE_MESSAGE_SIZE];
+ int misc[NLE_MISC_SIZE];
+ int internal[NLE_INTERNAL_SIZE];
+ short inv_glyphs[NLE_INVENTORY_SIZE];
+ unsigned char inv_letters[NLE_INVENTORY_SIZE];
+ unsigned char inv_oclasses[NLE_INVENTORY_SIZE];
+ signed char inv_state[NLE_INVENTORY_SIZE * NLE_INV_STATE_FIELDS];
+
+ Stats stats;
+
+ // reward-delta trackers
+ int prev_action;
+ int enh_ready;
+ long prev_score;
+ long prev_exp;
+ long prev_gold;
+ long start_gold;
+ long prev_hp;
+ long prev_hunger;
+ long prev_time;
+ int prev_depth;
+ long prev_ac;
+ int prev_bad_cond;
+
+ // reward coefs
+ float gold_coef;
+ float exp_coef;
+ float descent_coef;
+ float xp_coef;
+ float scout_coef;
+ float hp_coef;
+ float hunger_coef;
+ float illegal_penalty;
+ float death_penalty;
+ float ac_coef;
+ float heal_coef;
+ float status_coef;
+
+ unsigned int rng; // required by vecenv.h
+ unsigned long seed; // advanced each reset
+} Nethack;
+
+#include "macros.h" // keystroke utils
+
+// init
+
+static void nethack_bind_obs(Nethack* env) {
+ nle_obs* o = &env->obs;
+ memset(o, 0, sizeof(*o));
+ o->glyphs = env->glyphs;
+ o->blstats = env->blstats;
+ o->chars = env->chars;
+ o->message = env->message;
+ o->misc = env->misc;
+ o->internal = env->internal;
+ o->inv_glyphs = env->inv_glyphs;
+ o->inv_letters = env->inv_letters;
+ o->inv_oclasses = env->inv_oclasses;
+ o->inv_state = env->inv_state;
+ // partial fills
+ o->partial = 1;
+}
+
+static void nethack_init_settings(Nethack* env) {
+ memset(&env->settings, 0, sizeof(env->settings));
+ const char* source = getenv("NETHACKDIR");
+ if (source == NULL) source = "./vendor/fast-nle/build/dat";
+
+ if (nethack_make_vardir(source, env->vardir, sizeof(env->vardir)) != 0) {
+ fprintf(stderr, "nethack: failed to create vardir from source=%s\n", source);
+ strncpy(env->settings.hackdir, source, sizeof(env->settings.hackdir) - 1);
+ } else {
+ strncpy(env->settings.hackdir, env->vardir, sizeof(env->settings.hackdir) - 1);
+ }
+ env->settings.spawn_monsters = 1;
+ env->settings.underfoot_glyphs = 1; // underfoot shows objects
+ snprintf(env->settings.options, sizeof(env->settings.options), "@%s",
+ nethack_rc_path(NETHACK_DEFAULT_OPTIONS));
+ env->settings.fix_moon_phase = true; // moon phase from seed
+}
+
+void init(Nethack* env) {
+ env->seed = 0xCAFEBEEFUL + (unsigned long)env->rng; // rng = env index
+ // nle_start deferred to first c_reset
+ nethack_init_settings(env);
+}
+
+// masking
+
+static int nethack_slot_usable(const Nethack* env, const Verb* verb, int i) {
+ if (!(verb->item_classes & (1u << env->inv_oclasses[i]))) return 0;
+ int worn = env->inv_state[i * NLE_INV_STATE_FIELDS + 5] & 1;
+ if (verb->wornreq == WORN_ONLY) return worn;
+ if (verb->wornreq == UNWORN_ONLY) return !worn;
+ return 1;
+}
+
+static void nethack_compute_mask(Nethack* env) {
+ unsigned char* mask = env->action_mask;
+ memset(mask, 1, NETHACK_NUM_ACTIONS);
+ if (env->blstats[NLE_BL_HUNGER] == 0) mask[NETHACK_ACT_EAT] = 0; // choke gate
+
+ // underfoot
+ long hero_x = env->blstats[NLE_BL_X], hero_y = env->blstats[NLE_BL_Y];
+ int underfoot = (hero_x >= 0 && hero_x < NH_COLS && hero_y >= 0 && hero_y < NH_ROWS)
+ ? env->glyphs[hero_y * NH_COLS + hero_x] : -1;
+ int on_object = (underfoot >= NETHACK_GLYPH_OBJ_LO && underfoot < NETHACK_GLYPH_OBJ_HI);
+ int on_corpse = (underfoot >= NETHACK_GLYPH_BODY_OFF
+ && underfoot < NETHACK_GLYPH_BODY_OFF + NETHACK_NUMMONS);
+
+ if (underfoot != NETHACK_GLYPH_DNSTAIR && underfoot != NETHACK_GLYPH_DNLADDER)
+ mask[NETHACK_ACT_DOWN] = 0;
+ if (underfoot != NETHACK_GLYPH_UPSTAIR && underfoot != NETHACK_GLYPH_UPLADDER)
+ mask[NETHACK_ACT_UP] = 0;
+ if (env->blstats[NLE_BL_DEPTH] <= 1) mask[NETHACK_ACT_UP] = 0; // declined exit
+ if (!on_object && !on_corpse) mask[NETHACK_ACT_PICKUP] = 0;
+
+ // item slot heads
+ for (int a = 0; a < NETHACK_NUM_ACTIONS; a++) {
+ const Verb* verb = &NETHACK_VERBS[a];
+ if (verb->head < 0) continue; // direct verb, no item argument
+ unsigned char* slots = mask + NETHACK_NUM_ACTIONS + verb->head * NETHACK_INV_SLOTS;
+ memset(slots, 0, NETHACK_INV_SLOTS);
+ int has_usable = 0;
+ for (int i = 0; i < NETHACK_INV_SLOTS && env->inv_letters[i]; i++) {
+ if (env->inv_oclasses[i] >= NETHACK_NUM_OCLASSES) break; // padded tail
+ if (nethack_slot_usable(env, verb, i)) { slots[i] = 1; has_usable = 1; }
+ }
+ if (has_usable) continue;
+ // no usable item
+ slots[0] = 1;
+ int floor_food = (a == NETHACK_ACT_EAT) && (on_object || on_corpse);
+ if (!floor_food) mask[a] = 0;
+ }
+
+ // directions
+ unsigned char* dirs = mask + NETHACK_NUM_ACTIONS + 12 * NETHACK_INV_SLOTS;
+ memset(dirs, 1, NETHACK_NUM_DIRS);
+ int legal_dirs = 0;
+ for (int d = 0; d < NETHACK_NUM_DIRS; d++) {
+ long col = hero_x + NETHACK_DIR_DX[d], row = hero_y + NETHACK_DIR_DY[d];
+ if (row < 0 || row >= NH_ROWS || col < 0 || col >= NH_COLS) { dirs[d] = 0; continue; }
+ int g = env->glyphs[row * NH_COLS + col];
+ if (g >= NETHACK_WALL_GLYPH_LO && g <= NETHACK_WALL_GLYPH_HI) dirs[d] = 0;
+ else legal_dirs++;
+ }
+ if (!legal_dirs) memset(dirs, 1, NETHACK_NUM_DIRS);
+}
+
+// observations
+
+static void nethack_pack_obs(Nethack* env) {
+ memcpy(env->observations + NETHACK_OFF_GLYPHS, env->glyphs, sizeof(env->glyphs));
+ unsigned char* bl = env->observations + NETHACK_OFF_BLSTATS;
+ for (int i = 0; i < NLE_BLSTATS_SIZE; i++) {
+ uint32_t v = (uint32_t)(int32_t)env->blstats[i];
+ bl[4*i + 0] = (unsigned char)(v & 0xffu);
+ bl[4*i + 1] = (unsigned char)((v >> 8) & 0xffu);
+ bl[4*i + 2] = (unsigned char)((v >> 16) & 0xffu);
+ bl[4*i + 3] = (unsigned char)((v >> 24) & 0xffu);
+ }
+ int32_t extra[NETHACK_EXTRA_INTS] = {0};
+ // engraving state 0/1/2
+ extra[0] = env->internal[6];
+ extra[1] = env->prev_action;
+ for (int i = 0; i < NLE_INVENTORY_SIZE; i++) {
+ int oc = env->inv_oclasses[i];
+ if (oc >= NETHACK_NUM_OCLASSES) break; // padded tail
+ extra[2 + oc]++;
+ }
+ unsigned char* ex = env->observations + NETHACK_OFF_EXTRA;
+ for (int i = 0; i < NETHACK_EXTRA_INTS; i++) {
+ uint32_t v = (uint32_t)extra[i];
+ ex[4*i + 0] = (unsigned char)(v & 0xffu);
+ ex[4*i + 1] = (unsigned char)((v >> 8) & 0xffu);
+ ex[4*i + 2] = (unsigned char)((v >> 16) & 0xffu);
+ ex[4*i + 3] = (unsigned char)((v >> 24) & 0xffu);
+ }
+ // slot glyphs
+ unsigned char* iv = env->observations + NETHACK_OFF_INV;
+ for (int i = 0; i < NETHACK_INV_SLOTS; i++) {
+ uint16_t g = env->inv_oclasses[i] < NETHACK_NUM_OCLASSES
+ ? (uint16_t)env->inv_glyphs[i] : (uint16_t)NETHACK_PAD_GLYPH;
+ iv[2*i + 0] = (unsigned char)(g & 0xffu);
+ iv[2*i + 1] = (unsigned char)((g >> 8) & 0xffu);
+ }
+ // item state
+ memcpy(env->observations + NETHACK_OFF_INVST, env->inv_state, sizeof(env->inv_state));
+ // topline
+ unsigned char* mv = env->observations + NETHACK_OFF_MSG;
+ size_t mlen = strnlen((const char*)env->message, NETHACK_MSG_LEN);
+ memcpy(mv, env->message, mlen);
+ if (mlen < (size_t)NETHACK_MSG_LEN) memset(mv + mlen, 0, NETHACK_MSG_LEN - mlen);
+
+ if (env->action_mask != NULL) nethack_compute_mask(env);
+}
+
+// logging
+
+static void nethack_add_log(Nethack* env, int how) { // how: nle how_done, -1 = truncated
+ for (int v = 0; v < NETHACK_NUM_ACTIONS; v++)
+ env->log.verb_uses[v] += (float)env->stats.verb_uses[v];
+ env->log.perf += (float)env->prev_score;
+ env->log.score += (float)env->prev_score;
+ env->log.valid_moves += (float)env->stats.valid_moves;
+ env->log.illegal_actions += (float)env->stats.illegal_actions;
+ env->log.new_tiles += (float)env->stats.new_tiles;
+ env->log.max_depth += (float)env->stats.max_depth;
+ env->log.enhances += (float)env->stats.enhances;
+ env->log.prayers_low_hp += (float)env->stats.prayers_low_hp;
+ env->log.prayers_starving += (float)env->stats.prayers_starving;
+ env->log.floor_eats += (float)env->stats.floor_eats;
+ env->log.damage_taken += (float)env->stats.damage;
+ env->log.ac += env->stats.length > 0
+ ? (float)env->stats.ac_sum / (float)env->stats.length : 0.0f;
+ env->log.min_ac += (float)env->stats.min_ac;
+ env->log.armor_swaps += (float)env->stats.armor_swaps;
+ env->log.heal_hp += (float)env->stats.heal_hp;
+ env->log.cures += (float)env->stats.cures;
+ env->log.burdened_frac += env->stats.length > 0
+ ? (float)env->stats.burdened_steps / (float)env->stats.length : 0.0f;
+ env->log.game_time += (float)env->prev_time;
+ env->log.max_xp_level += (float)env->stats.max_xp;
+ env->log.episode_return += env->stats.ret;
+ env->log.episode_length += env->stats.length;
+ if (how == -1) env->log.truncated += 1.0f;
+ else if (how == 0) env->log.death_combat += 1.0f;
+ else if (how == 3) env->log.death_starved += 1.0f;
+ else if (how == NLE_HOW_WRATH) env->log.death_smited += 1.0f;
+ else env->log.death_other += 1.0f;
+ // combat anatomy
+ if (how == 0) {
+ env->log.death_mon_level += (float)env->internal[NETHACK_INTERNAL_KILLER_MLEV];
+ env->log.death_adj_monsters += (float)env->stats.last_adj;
+ env->log.death_maxhp += (float)env->stats.last_maxhp;
+ }
+ env->log.reach_mines += (env->stats.areas & NETHACK_AREA_MINES) ? 1.0f : 0.0f;
+ env->log.reach_minetown += (env->stats.areas & NETHACK_AREA_MINETOWN) ? 1.0f : 0.0f;
+ env->log.reach_deep_mines += (env->stats.areas & NETHACK_AREA_DEEP_MINES) ? 1.0f : 0.0f;
+ env->log.reach_main_d5 += (env->stats.areas & NETHACK_AREA_MAIN_D5) ? 1.0f : 0.0f;
+ env->log.reach_sokoban += (env->stats.areas & NETHACK_AREA_SOKOBAN) ? 1.0f : 0.0f;
+ env->log.n += 1.0f;
+}
+
+// reset
+
+static void nethack_do_reset(Nethack* env) {
+ if (env->ctx != NULL) {
+ nle_end(env->ctx);
+ env->ctx = NULL;
+ }
+
+ nethack_bind_obs(env);
+ env->obs.how_done = -2; // only really_done() sets it
+
+ // seed advance
+ env->seed = env->seed * 6364136223846793005UL + 1442695040888963407UL;
+ env->settings.initial_seeds.seeds[0] = env->seed;
+ env->settings.initial_seeds.seeds[1] = env->seed ^ 0x9E3779B97F4A7C15UL;
+ env->settings.initial_seeds.use_init_seeds = true;
+ env->settings.time_seed = env->seed;
+ env->settings.time_seed_is_set = true;
+ env->ctx = nle_start(&env->obs, NULL, &env->settings);
+
+ nethack_drain_prompts(env);
+ nle_obs_refresh(env->ctx, &env->obs); // full fill: prev_* seeds read blstats
+
+ env->prev_score = 0;
+ env->prev_exp = env->blstats[NLE_BL_EXP];
+ env->start_gold = env->blstats[NLE_BL_GOLD];
+ env->prev_gold = 0; // clamped net gold
+ env->prev_hp = env->blstats[NLE_BL_HP];
+ env->prev_hunger = env->blstats[NLE_BL_HUNGER];
+ if (env->prev_hunger < 1) env->prev_hunger = 1;
+ else if (env->prev_hunger > 6) env->prev_hunger = 6;
+ env->prev_depth = (int)env->blstats[NLE_BL_DEPTH];
+ env->prev_ac = env->blstats[NLE_BL_AC];
+ env->prev_bad_cond = __builtin_popcount((unsigned)env->blstats[NLE_BL_CONDITION] & NETHACK_COND_BAD);
+ env->prev_time = env->blstats[NLE_BL_TIME];
+ env->prev_action = -1;
+ env->enh_ready = 0;
+ memset(&env->stats, 0, sizeof(env->stats));
+ env->stats.max_depth = env->prev_depth;
+ env->stats.max_xp = (int)env->blstats[NLE_BL_XP];
+ env->stats.min_ac = (int)env->blstats[NLE_BL_AC];
+ nethack_pack_obs(env);
+}
+
+void c_reset(Nethack* env) {
+ env->pending_reset = 1;
+}
+
+// reward
+
+static void nethack_update_stats(Nethack* env) {
+ int depth = (int)env->blstats[NLE_BL_DEPTH];
+ long dnum = env->blstats[NLE_BL_DNUM];
+ if (dnum == 2) {
+ env->stats.areas |= NETHACK_AREA_MINES;
+ long mlvl = env->blstats[NLE_BL_DLEVEL];
+ if (mlvl >= 3) env->stats.areas |= NETHACK_AREA_MINETOWN;
+ if (mlvl >= 5) env->stats.areas |= NETHACK_AREA_DEEP_MINES;
+ }
+ else if (dnum == 0 && depth >= 5) env->stats.areas |= NETHACK_AREA_MAIN_D5;
+ else if (dnum == 4) env->stats.areas |= NETHACK_AREA_SOKOBAN;
+
+ long hp = env->blstats[NLE_BL_HP];
+ if (hp < env->prev_hp) env->stats.damage += env->prev_hp - hp;
+ if (env->blstats[NLE_BL_CAP] > 0) env->stats.burdened_steps++;
+
+ // death anatomy, read back at death
+ env->stats.last_maxhp = env->blstats[NLE_BL_HPMAX];
+ long hx = env->blstats[NLE_BL_X], hy = env->blstats[NLE_BL_Y];
+ int adj = 0;
+ for (int dy = -1; dy <= 1; dy++)
+ for (int dx = -1; dx <= 1; dx++) {
+ if (!dx && !dy) continue;
+ long r = hy + dy, c = hx + dx;
+ if (r < 0 || r >= NH_ROWS || c < 0 || c >= NH_COLS) continue;
+ int g = env->glyphs[r * NH_COLS + c];
+ if (g >= 0 && g < NETHACK_NUMMONS) adj++;
+ }
+ env->stats.last_adj = adj;
+
+ env->prev_score = env->blstats[NLE_BL_SCORE];
+ env->prev_time = env->blstats[NLE_BL_TIME];
+ env->prev_depth = depth;
+}
+
+static int nethack_first_visit(Nethack* env, int depth, long px, long py) {
+ if (px < 0 || px >= NH_COLS || py < 0 || py >= NH_ROWS) return 0;
+ int d = depth < 1 ? 0 : (depth > NETHACK_MAX_DEPTH ? NETHACK_MAX_DEPTH - 1 : depth - 1);
+ int bit = (int)py * NH_COLS + (int)px;
+ unsigned char mask = (unsigned char)(1 << (bit & 7));
+ if (env->stats.visited[d][bit >> 3] & mask) return 0;
+ env->stats.visited[d][bit >> 3] |= mask;
+ return 1;
+}
+
+static float nethack_reward(Nethack* env, int illegal) {
+ // death payout
+ if (env->obs.done)
+ return env->death_penalty - env->hp_coef * (float)env->prev_hp;
+ nethack_update_stats(env);
+
+ int depth = (int)env->blstats[NLE_BL_DEPTH];
+
+ // exp, gains only
+ long exp = env->blstats[NLE_BL_EXP];
+ float r = exp > env->prev_exp ? env->exp_coef * (float)(exp - env->prev_exp) : 0.0f;
+ env->prev_exp = exp;
+
+ // gold, net of start
+ long g = env->blstats[NLE_BL_GOLD] - env->start_gold;
+ if (g < 0) g = 0;
+ r += env->gold_coef * (float)(g - env->prev_gold);
+ env->prev_gold = g;
+
+ // descent, max-depth only
+ if (depth > env->stats.max_depth) {
+ r += env->descent_coef * (float)(depth - env->stats.max_depth);
+ env->stats.max_depth = depth;
+ }
+
+ // hp potential
+ long hp = env->blstats[NLE_BL_HP];
+ long hp_delta = hp - env->prev_hp;
+ r += env->hp_coef * (float)hp_delta;
+ // gain-only heal credit
+ if (hp_delta > 0 && (env->prev_action == NETHACK_ACT_QUAFF
+ || env->prev_action == NETHACK_ACT_PRAY)) {
+ r += env->heal_coef * (float)hp_delta;
+ env->stats.heal_hp += hp_delta;
+ }
+ env->prev_hp = hp;
+
+ // ac potential
+ long ac = env->blstats[NLE_BL_AC];
+ r += env->ac_coef * (float)(env->prev_ac - ac);
+ env->prev_ac = ac;
+ env->stats.ac_sum += ac;
+ if ((int)ac < env->stats.min_ac) env->stats.min_ac = (int)ac;
+
+ // status potential
+ int bad_cond = __builtin_popcount((unsigned)env->blstats[NLE_BL_CONDITION] & NETHACK_COND_BAD);
+ r += env->status_coef * (float)(env->prev_bad_cond - bad_cond);
+ if (bad_cond < env->prev_bad_cond) env->stats.cures += env->prev_bad_cond - bad_cond;
+ env->prev_bad_cond = bad_cond;
+
+ // hunger potential
+ long hunger = env->blstats[NLE_BL_HUNGER];
+ if (hunger < 1) hunger = 1;
+ else if (hunger > 6) hunger = 6;
+ r += env->hunger_coef * (float)(env->prev_hunger - hunger);
+ env->prev_hunger = hunger;
+
+ // xp level, max only
+ int xp = (int)env->blstats[NLE_BL_XP];
+ if (xp > env->stats.max_xp) {
+ r += env->xp_coef * (float)(xp - env->stats.max_xp);
+ env->stats.max_xp = xp;
+ }
+
+ // scout
+ if (nethack_first_visit(env, depth, env->blstats[NLE_BL_X], env->blstats[NLE_BL_Y])) {
+ r += env->scout_coef;
+ env->stats.new_tiles++;
+ }
+
+ if (illegal) r += env->illegal_penalty;
+ return r;
+}
+
+// stepping
+
+static void nethack_execute(Nethack* env, int verb, int slot, int dirkey, int* bad_pick) {
+ Stats* st = &env->stats;
+ switch (verb) {
+ case NETHACK_ACT_MOVE:
+ nethack_send_key(env, dirkey);
+ break;
+ case NETHACK_ACT_RUN:
+ nethack_send_key(env, dirkey - 32); // uppercase = run
+ break;
+ case NETHACK_ACT_DOWN:
+ nethack_send_key(env, '>');
+ break;
+ case NETHACK_ACT_UP:
+ nethack_send_key(env, '<');
+ break;
+ case NETHACK_ACT_KICK:
+ nethack_send_key(env, 4); // ^D
+ nethack_answer_direction(env, dirkey);
+ break;
+ case NETHACK_ACT_SEARCH:
+ st->verb_uses[verb]++;
+ nethack_send_key(env, 's');
+ break;
+ case NETHACK_ACT_ELBERETH:
+ st->verb_uses[verb]++;
+ nethack_do_elbereth(env);
+ break;
+ case NETHACK_ACT_SEARCH20:
+ st->verb_uses[verb]++;
+ nethack_send_key(env, '2');
+ if (!env->obs.done) nethack_send_key(env, '0');
+ if (!env->obs.done) nethack_send_key(env, 's');
+ break;
+ case NETHACK_ACT_PICKUP:
+ st->verb_uses[verb]++;
+ nethack_send_key(env, ',');
+ nethack_answer_menu(env);
+ break;
+ case NETHACK_ACT_PRAY:
+ if (4 * env->blstats[NLE_BL_HP] <= env->blstats[NLE_BL_HPMAX]) st->prayers_low_hp++;
+ if (env->blstats[NLE_BL_HUNGER] >= NETHACK_HUNGER_WEAK) st->prayers_starving++;
+ st->verb_uses[verb]++;
+ nethack_send_key(env, 0x80 | 'p');
+ break;
+ case NETHACK_ACT_WEAR:
+ nethack_wear_takeoff_conflict(env, slot);
+ nethack_item_use(env, 'W', "want to wear", NULL, slot, &st->verb_uses[verb], bad_pick);
+ break;
+ case NETHACK_ACT_EAT:
+ nethack_item_use(env, 'e', "want to eat", "eat it", slot, &st->verb_uses[verb], bad_pick);
+ break;
+ case NETHACK_ACT_QUAFF:
+ nethack_item_use(env, 'q', "want to drink", "rink from the", slot, &st->verb_uses[verb], bad_pick);
+ break;
+ case NETHACK_ACT_THROW:
+ if (nethack_item_use(env, 't', "want to throw", NULL, slot, &st->verb_uses[verb], bad_pick))
+ nethack_answer_direction(env, dirkey);
+ break;
+ case NETHACK_ACT_ZAP:
+ if (nethack_item_use(env, 'z', "want to zap", NULL, slot, &st->verb_uses[verb], bad_pick))
+ nethack_answer_direction(env, dirkey);
+ break;
+ case NETHACK_ACT_TAKEOFF:
+ nethack_item_use(env, 'T', "take off", NULL, slot, &st->verb_uses[verb], bad_pick);
+ break;
+ case NETHACK_ACT_PUTON:
+ nethack_item_use(env, 'P', "put on", NULL, slot, &st->verb_uses[verb], bad_pick);
+ break;
+ case NETHACK_ACT_REMOVE:
+ nethack_item_use(env, 'R', "remove", NULL, slot, &st->verb_uses[verb], bad_pick);
+ break;
+ case NETHACK_ACT_WIELD:
+ nethack_verb_wield(env, slot, bad_pick);
+ break;
+ case NETHACK_ACT_APPLY:
+ if (nethack_item_use(env, 'a', "apply", NULL, slot, &st->verb_uses[verb], bad_pick)) {
+ // diggers dig down
+ int otyp = env->inv_glyphs[slot] - NH_GLYPH_OBJ_OFF;
+ nethack_answer_direction(env,
+ (otyp == 234 /* PICK_AXE */ || otyp == 50 /* MATTOCK */) ? '>' : dirkey);
+ }
+ break;
+ case NETHACK_ACT_READ:
+ if (nethack_item_use(env, 'r', "read", NULL, slot, &st->verb_uses[verb], bad_pick))
+ nethack_answer_menu(env);
+ break;
+ case NETHACK_ACT_DROP:
+ nethack_item_use(env, 'd', "drop", NULL, slot, &st->verb_uses[verb], bad_pick);
+ break;
+ }
+}
+
+void c_step(Nethack* env) {
+ if (env->pending_reset) {
+ env->pending_reset = 0;
+ nethack_do_reset(env);
+ }
+
+ int verb = (int)env->actions[0];
+ int head = NETHACK_VERBS[verb].head;
+ int slot = (head >= 0) ? (int)env->actions[1 + head] : 0;
+ int dirkey = NETHACK_DIR_KEYS[(int)env->actions[13]];
+
+ long time_before = env->blstats[NLE_BL_TIME];
+ int bad_pick = 0;
+ nethack_execute(env, verb, slot, dirkey, &bad_pick);
+
+ env->prev_action = verb;
+ int illegal = nethack_handle_prompts(env);
+ if (!env->obs.done) nle_obs_refresh(env->ctx, &env->obs);
+ nethack_auto_enhance(env);
+
+ if (bad_pick) { illegal = 1; env->stats.illegal_actions++; }
+ if (env->blstats[NLE_BL_TIME] > time_before) env->stats.valid_moves++;
+ env->stats.length++;
+
+ float reward = nethack_reward(env, illegal);
+ env->rewards[0] = reward;
+ env->stats.ret += reward;
+
+ int done = env->obs.done || env->stats.length >= NETHACK_MAX_EPISODE_STEPS;
+ env->terminals[0] = done ? 1.0f : 0.0f; // truncation reported as terminal too
+ if (done) {
+ nethack_add_log(env, env->obs.done ? env->obs.how_done : -1);
+ // eager same-thread reset: the terminal step returns the fresh obs
+ nethack_do_reset(env);
+ } else {
+ nethack_pack_obs(env);
+ }
+}
+
+void c_close(Nethack* env) {
+ if (env->ctx != NULL) {
+ nle_end(env->ctx);
+ env->ctx = NULL;
+ }
+ nethack_rm_vardir(env->vardir);
+ env->vardir[0] = '\0';
+}
+
+void c_render(Nethack* env) {
+ printf("\x1b[H\x1b[2J");
+ for (int r = 0; r < NH_ROWS; r++) {
+ for (int c = 0; c < NH_COLS; c++) {
+ unsigned char ch = env->chars[r * NH_COLS + c];
+ putchar(ch ? ch : ' ');
+ }
+ putchar('\n');
+ }
+ printf("HP %ld/%ld AC %ld Dlvl %ld Score %ld T %ld\n",
+ env->blstats[NLE_BL_HP], env->blstats[NLE_BL_HPMAX],
+ env->blstats[NLE_BL_AC], env->blstats[NLE_BL_DEPTH],
+ env->blstats[NLE_BL_SCORE], env->blstats[NLE_BL_TIME]);
+ printf("Msg: %.*s\n", NLE_MESSAGE_SIZE, env->message);
+ fflush(stdout);
+}
diff --git a/ocean/nethack/netlib.h b/ocean/nethack/netlib.h
new file mode 100644
index 0000000000..c8ea125616
--- /dev/null
+++ b/ocean/nethack/netlib.h
@@ -0,0 +1,258 @@
+// Static data for the NetHack env: layout and glyph constants, the action
+// space and verb table, engine options, and the telemetry structs.
+// Included by nethack.h after nletypes.h.
+#pragma once
+
+// object-type -> armor slot (ARM_SUIT=0..ARM_SHIRT=6, -1 = not armor), indexed
+// by otyp = glyph - NH_GLYPH_OBJ_OFF; generated from the engine's objects[]
+// (gen_obj_armcat, NetHack 3.6.6). Device copy inlined in src/nethack.cu.
+#define NH_NUM_OBJECTS 453
+#define NH_GLYPH_OBJ_OFF 1906
+static const signed char nh_obj_armcat[NH_NUM_OBJECTS] = {
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,2,2,2,2,2,2,2,2,2,
+ 2,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,6,6,5,5,5,
+ 5,5,5,5,5,5,5,5,5,1,1,1,1,1,1,1,3,3,3,3,
+ 4,4,4,4,4,4,4,4,4,4,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+};
+
+#define NH_ROWS 21
+#define NH_COLS 79
+#define NH_GRID (NH_ROWS * NH_COLS)
+
+// encoder views (GPU-side): egocentric crop + 5x5 patches over the grid
+#define NETHACK_CROP 9
+#define NETHACK_CROP_GRID (NETHACK_CROP * NETHACK_CROP)
+#define NETHACK_PAD_GLYPH 5976
+
+// corpse glyphs: [GLYPH_BODY_OFF, +NUMMONS), display.h
+#define NETHACK_GLYPH_BODY_OFF 1144
+#define NETHACK_NUMMONS 381
+
+// obs layout: glyphs | blstats | extra | inventory | item state | message
+#define NETHACK_NUM_OCLASSES 18 // MAXOCLASSES; inv_oclasses pads with 18
+#define NETHACK_OFF_GLYPHS 0
+#define NETHACK_OFF_BLSTATS (NH_GRID * 2)
+#define NETHACK_OFF_EXTRA (NETHACK_OFF_BLSTATS + NLE_BLSTATS_SIZE * 4)
+#define NETHACK_EXTRA_INTS (2 + NETHACK_NUM_OCLASSES)
+// inventory: 55 slot glyphs (slot heads index these), then 8 gated int8
+// state fields per slot [buc, spe, quan, ero1, ero2, flags, typeknown, rsvd]
+#define NETHACK_INV_SLOTS NLE_INVENTORY_SIZE
+#define NETHACK_OFF_INV (NETHACK_OFF_EXTRA + NETHACK_EXTRA_INTS * 4)
+#define NETHACK_OFF_INVST (NETHACK_OFF_INV + NETHACK_INV_SLOTS * 2)
+// raw topline chars, null-padded; must match NH_MSG_LEN in src/nethack.cu
+#define NETHACK_OFF_MSG (NETHACK_OFF_INVST + NETHACK_INV_SLOTS * NLE_INV_STATE_FIELDS)
+#define NETHACK_MSG_LEN 128
+#define NETHACK_OBS_SIZE (NETHACK_OFF_MSG + NETHACK_MSG_LEN)
+#define NETHACK_INTERNAL_KILLER_MNUM 9 // killer monster index + 1 (0 = not a monster), death only
+#define NETHACK_INTERNAL_KILLER_MLEV 10 // killer monster level, death only
+
+#define NETHACK_MAX_EPISODE_STEPS 10000
+#define NETHACK_AUTODISMISS_MAX 64 // cap on prompt-dismiss keystrokes per step
+#define NETHACK_MAX_DEPTH 64 // scout bitmaps tracked per episode
+
+// Stats.areas bits; logged as reach_* proportions
+#define NETHACK_AREA_MINES 1u // Gnomish Mines (dnum 2)
+#define NETHACK_AREA_MINETOWN 2u // Mines level 3+ (Minetown band)
+#define NETHACK_AREA_DEEP_MINES 4u // Mines level 5+ (past Minetown)
+#define NETHACK_AREA_MAIN_D5 8u // Dungeons of Doom depth 5+ (Oracle route)
+#define NETHACK_AREA_SOKOBAN 16u // Sokoban (dnum 4)
+
+// nle_obs.misc[] prompt-state flags
+enum { NETHACK_MISC_YN = 0, NETHACK_MISC_GETLIN = 1, NETHACK_MISC_XWAIT = 2 };
+
+// action space: verb head (22) + 12 item-slot heads (55) + direction head (8)
+#define NETHACK_NUM_ACTIONS 22
+#define NETHACK_NUM_DIRS 8
+static const int NETHACK_DIR_KEYS[NETHACK_NUM_DIRS] =
+ {'k','j','h','l','y','u','b','n'}; // N S W E NW NE SW SE
+static const int NETHACK_DIR_DX[NETHACK_NUM_DIRS] = { 0, 0,-1, 1,-1, 1,-1, 1};
+static const int NETHACK_DIR_DY[NETHACK_NUM_DIRS] = {-1, 1, 0, 0,-1,-1, 1, 1};
+// cmap wall glyphs S_vwall..S_trwall; S_stone excluded (= "unexplored")
+#define NETHACK_WALL_GLYPH_LO 2360
+#define NETHACK_WALL_GLYPH_HI 2370
+// hunger states (hack.h): SATIATED 0, NOT_HUNGRY 1, HUNGRY 2, WEAK 3, FAINTING 4
+#define NETHACK_HUNGER_WEAK 3
+// major-trouble condition bits (botl.h BL_MASK_): STONE|SLIME|STRNGL|FOODPOIS|TERMILL
+#define NETHACK_COND_MAJOR 0x1Fu
+#define NETHACK_COND_BAD 0x3FFu // all afflictions STONE..HALLU; excludes LEV/FLY/RIDE
+// stair/ladder cmap glyphs: GLYPH_CMAP_OFF(2359) + S_upstair(23)..S_dnladder(26)
+#define NETHACK_GLYPH_UPSTAIR 2382
+#define NETHACK_GLYPH_DNSTAIR 2383
+#define NETHACK_GLYPH_UPLADDER 2384
+#define NETHACK_GLYPH_DNLADDER 2385
+// object glyphs [GLYPH_OBJ_OFF, GLYPH_CMAP_OFF); underfoot objects win over terrain
+#define NETHACK_GLYPH_OBJ_LO 1906
+#define NETHACK_GLYPH_OBJ_HI 2359
+
+enum {
+ NETHACK_ACT_MOVE = 0,
+ NETHACK_ACT_RUN = 1,
+ NETHACK_ACT_DOWN = 2,
+ NETHACK_ACT_UP = 3,
+ NETHACK_ACT_KICK = 4,
+ NETHACK_ACT_SEARCH = 5,
+ NETHACK_ACT_ELBERETH = 6,
+ NETHACK_ACT_WEAR = 7,
+ NETHACK_ACT_EAT = 8,
+ NETHACK_ACT_QUAFF = 9,
+ NETHACK_ACT_PRAY = 10,
+ NETHACK_ACT_THROW = 11,
+ NETHACK_ACT_ZAP = 12,
+ NETHACK_ACT_SEARCH20 = 13, // count-prefixed search: ~20 turns of rest
+ NETHACK_ACT_PICKUP = 14, // no slot: grab the pile underfoot (beyond narrow autopickup)
+ NETHACK_ACT_TAKEOFF = 15,
+ NETHACK_ACT_PUTON = 16,
+ NETHACK_ACT_REMOVE = 17,
+ NETHACK_ACT_WIELD = 18,
+ NETHACK_ACT_APPLY = 19,
+ NETHACK_ACT_READ = 20,
+ NETHACK_ACT_DROP = 21,
+};
+
+// !status_updates skips the status renderer + recalc_mapseen (~25% of engine)
+#define NETHACK_DEFAULT_OPTIONS \
+ "name:Agent-mon-hum-neu-mal," \
+ "autopickup,color,disclose:+i +a +v +g +c +o," \
+ "mention_walls,nobones,nocmdassist,nolegacy,nosparkle," \
+ "pickup_burden:unencumbered,pickup_types:$[%!)/," \
+ "runmode:teleport,showexp,showscore,time," \
+ "!status_updates"
+
+// slot-head legality per verb (masking + decode; execution is nethack_execute)
+enum { WORN_ANY = 0, WORN_ONLY = 1, UNWORN_ONLY = 2 };
+
+typedef struct {
+ signed char head; // -1 direct verb, 0..11 = item slot head
+ unsigned int item_classes;
+ unsigned char wornreq;
+} Verb;
+
+static const Verb NETHACK_VERBS[NETHACK_NUM_ACTIONS] = {
+ [NETHACK_ACT_MOVE] = {-1}, [NETHACK_ACT_RUN] = {-1}, [NETHACK_ACT_DOWN] = {-1},
+ [NETHACK_ACT_UP] = {-1}, [NETHACK_ACT_KICK] = {-1}, [NETHACK_ACT_SEARCH] = {-1},
+ [NETHACK_ACT_ELBERETH] = {-1}, [NETHACK_ACT_SEARCH20] = {-1},
+ [NETHACK_ACT_PICKUP] = {-1}, [NETHACK_ACT_PRAY] = {-1},
+ [NETHACK_ACT_WEAR] = {0, 1u<<3, UNWORN_ONLY},
+ [NETHACK_ACT_EAT] = {1, 1u<<7, WORN_ANY},
+ [NETHACK_ACT_QUAFF] = {2, 1u<<8, WORN_ANY},
+ [NETHACK_ACT_THROW] = {3, 1u<<2, WORN_ANY},
+ [NETHACK_ACT_ZAP] = {4, 1u<<11, WORN_ANY},
+ [NETHACK_ACT_TAKEOFF] = {5, 1u<<3, WORN_ONLY},
+ [NETHACK_ACT_PUTON] = {6, (1u<<4)|(1u<<5), UNWORN_ONLY},
+ [NETHACK_ACT_REMOVE] = {7, (1u<<4)|(1u<<5), WORN_ONLY},
+ [NETHACK_ACT_WIELD] = {8, 1u<<2, WORN_ANY},
+ [NETHACK_ACT_APPLY] = {9, 1u<<6, WORN_ANY},
+ [NETHACK_ACT_READ] = {10, (1u<<9)|(1u<<10), WORN_ANY},
+ [NETHACK_ACT_DROP] = {11, 0x3FFFFu, UNWORN_ONLY},
+};
+
+// wandb key per verb success counter (NULL = not logged)
+static const char* NETHACK_VERB_STAT[NETHACK_NUM_ACTIONS] = {
+ [NETHACK_ACT_SEARCH] = "searches",
+ [NETHACK_ACT_ELBERETH] = "engraves",
+ [NETHACK_ACT_WEAR] = "wears",
+ [NETHACK_ACT_EAT] = "eats",
+ [NETHACK_ACT_QUAFF] = "quaffs",
+ [NETHACK_ACT_PRAY] = "prayers",
+ [NETHACK_ACT_THROW] = "throws",
+ [NETHACK_ACT_ZAP] = "zaps",
+ [NETHACK_ACT_SEARCH20] = "search20",
+ [NETHACK_ACT_PICKUP] = "pickups",
+ [NETHACK_ACT_TAKEOFF] = "takeoffs",
+ [NETHACK_ACT_PUTON] = "putons",
+ [NETHACK_ACT_REMOVE] = "removes",
+ [NETHACK_ACT_WIELD] = "wields",
+ [NETHACK_ACT_APPLY] = "applies",
+ [NETHACK_ACT_READ] = "reads",
+ [NETHACK_ACT_DROP] = "drops",
+};
+
+typedef struct Log {
+ float perf;
+ float verb_uses[NETHACK_NUM_ACTIONS]; // success counters, keys = NETHACK_VERB_STAT
+ float score;
+ float episode_return;
+ float episode_length;
+ float valid_moves; // steps that advanced NetHack's turn counter
+ float illegal_actions; // steps that hit a sub-prompt we ESC'd
+ float new_tiles;
+ float max_depth; // deepest level reached (depth under-reports at death)
+ float enhances; // #enhance presses (skill advancement claims)
+ float floor_eats; // eats that accepted a floor "eat it?" offer
+ float prayers_low_hp; // prayers at <=25% max HP (looser than real trouble)
+ float prayers_starving; // prayers at hunger >= Weak: TROUBLE_STARVING, prayer feeds you
+ // rest/retreat/burden diagnostics (log-only)
+ float burdened_frac; // steps with encumbrance > Unencumbered
+ float damage_taken;
+ float ac; // mean armor class over the episode (lower = better)
+ float min_ac; // best (lowest) AC reached this episode
+ float armor_swaps; // atomic WEAR swaps (auto-takeoff + wear in one step)
+ float heal_hp; // HP restored by heal actions (quaff/pray) this episode
+ float cures; // bad conditions cleared this episode
+ float game_time; // NetHack turns survived
+ float max_xp_level;
+ // episode end reason, one-hot (game_end_types in hack.h)
+ float death_combat;
+ float death_starved;
+ float death_smited; // god's wrath (NLE_HOW_WRATH), not a monster kill
+ float death_other;
+ // combat-death anatomy (0 for non-combat episodes; ~95% are combat)
+ float death_mon_level; // killer's monster level (vs max_xp_level = the mismatch)
+ float death_adj_monsters; // hostile monsters adjacent on the last obs before death
+ float death_maxhp; // max HP at death (progression measure)
+ float truncated; // hit NETHACK_MAX_EPISODE_STEPS
+ // 0/1 per episode; the logged mean is the proportion
+ float reach_mines;
+ float reach_minetown;
+ float reach_deep_mines;
+ float reach_main_d5;
+ float reach_sokoban;
+ float n;
+} Log;
+
+// per-episode stats; cleared with one memset per reset
+typedef struct Stats {
+ long verb_uses[NETHACK_NUM_ACTIONS];
+ long valid_moves;
+ long illegal_actions;
+ long new_tiles;
+ long enhances;
+ long armor_swaps; // atomic WEAR that auto-took-off an occupant
+ long burdened_steps;
+ long heal_hp; // HP restored by heal actions (quaff/pray)
+ long cures; // bad conditions cleared
+ int min_ac; // best (lowest) AC reached this episode
+ long last_maxhp;
+ int last_adj; // hostile monsters adjacent, last obs
+ long prayers_low_hp;
+ long prayers_starving;
+ long floor_eats;
+ long damage;
+ long ac_sum; // sum of AC over living steps; mean = ac_sum/length
+ int max_depth;
+ int max_xp;
+ unsigned areas; // NETHACK_AREA_* bits
+ float ret;
+ int length;
+ // per-level first-visit bitmaps; branch levels sharing a depth share one
+ unsigned char visited[NETHACK_MAX_DEPTH][(NH_GRID + 7) / 8];
+} Stats;
diff --git a/ocean/nmmo3/binding.c b/ocean/nmmo3/binding.c
new file mode 100644
index 0000000000..b2d5fb4100
--- /dev/null
+++ b/ocean/nmmo3/binding.c
@@ -0,0 +1,52 @@
+#include "nmmo3.h"
+#define OBS_SIZE 1707
+#define NUM_ATNS 1
+#define ACT_SIZES {26}
+#define OBS_TENSOR_T ByteTensor
+
+#define Env MMO
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->num_agents = dict_get(kwargs, "num_agents")->value;
+ env->num_enemies = dict_get(kwargs, "num_enemies")->value;
+ env->num_resources = dict_get(kwargs, "num_resources")->value;
+ env->num_weapons = dict_get(kwargs, "num_weapons")->value;
+ env->num_gems = dict_get(kwargs, "num_gems")->value;
+ env->tiers = dict_get(kwargs, "tiers")->value;
+ env->levels = dict_get(kwargs, "levels")->value;
+ env->teleportitis_prob = dict_get(kwargs, "teleportitis_prob")->value;
+ env->enemy_respawn_ticks = dict_get(kwargs, "enemy_respawn_ticks")->value;
+ env->item_respawn_ticks = dict_get(kwargs, "item_respawn_ticks")->value;
+ env->x_window = dict_get(kwargs, "x_window")->value;
+ env->y_window = dict_get(kwargs, "y_window")->value;
+ env->reward_combat_level = dict_get(kwargs, "reward_combat_level")->value;
+ env->reward_prof_level = dict_get(kwargs, "reward_prof_level")->value;
+ env->reward_item_level = dict_get(kwargs, "reward_item_level")->value;
+ env->reward_market = dict_get(kwargs, "reward_market")->value;
+ env->reward_death = dict_get(kwargs, "reward_death")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "return_comb_lvl", log->return_comb_lvl);
+ dict_set(out, "return_prof_lvl", log->return_prof_lvl);
+ dict_set(out, "return_item_atk_lvl", log->return_item_atk_lvl);
+ dict_set(out, "return_item_def_lvl", log->return_item_def_lvl);
+ dict_set(out, "return_market_buy", log->return_market_buy);
+ dict_set(out, "return_market_sell", log->return_market_sell);
+ dict_set(out, "return_death", log->return_death);
+ dict_set(out, "min_comb_prof", log->min_comb_prof);
+ dict_set(out, "purchases", log->purchases);
+ dict_set(out, "sales", log->sales);
+ dict_set(out, "equip_attack", log->equip_attack);
+ dict_set(out, "equip_defense", log->equip_defense);
+ dict_set(out, "r", log->r);
+ dict_set(out, "c", log->c);
+}
diff --git a/ocean/nmmo3/nmmo3.c b/ocean/nmmo3/nmmo3.c
new file mode 100644
index 0000000000..0bbff7d345
--- /dev/null
+++ b/ocean/nmmo3/nmmo3.c
@@ -0,0 +1,541 @@
+#include
+#include
+#include
+#include "puffernet.h"
+#include "nmmo3.h"
+
+// Only run 1 agent in the C version
+// You can run the full 1024 on GPU
+#define NUM_AGENTS 1
+
+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;
+ Affine* proj;
+ ReLU* proj_relu;
+ Linear* decoder;
+ MinGRU* mingru;
+ Multidiscrete* multidiscrete;
+};
+
+MMONet* init_mmonet(Weights* weights, int num_agents) {
+ MMONet* net = calloc(1, sizeof(MMONet));
+ int hidden = 512;
+ 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, 128, 5, 3);
+ net->map_relu = make_relu(num_agents, 128*3*4);
+ net->map_conv2 = make_conv2d(weights, num_agents, 4, 3, 128, 128, 3, 1);
+ net->player_embed = make_embedding(weights, num_agents*47, 128, 32);
+ net->proj_buffer = calloc(num_agents*1817, sizeof(float));
+ net->proj = make_affine(weights, num_agents, 1817, hidden);
+ net->proj_relu = make_relu(num_agents, hidden);
+ net->decoder = make_linear(weights, num_agents, hidden, 26 + 1);
+ net->mingru = make_mingru(weights, num_agents, hidden, 4);
+ 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->decoder);
+ free_mingru(net->mingru);
+ free(net->multidiscrete);
+ free(net);
+}
+
+void forward(MMONet* net, unsigned char* observations, float* terminals, float* actions) {
+ for (int b = 0; b < net->num_agents; b++) {
+ if (terminals[b] > 0.5f) {
+ for (int l = 0; l < net->mingru->num_layers; l++) {
+ memset(net->mingru->state + l * net->mingru->batch_size * net->mingru->hidden_size + b * net->mingru->hidden_size, 0, net->mingru->hidden_size * sizeof(float));
+ }
+ terminals[b] = 0.0f;
+ }
+ }
+ memset(net->ob_map, 0, net->num_agents*11*15*59*sizeof(float));
+
+ // DUMMY INPUT FOR TESTING
+ //for (int i = 0; i < 11*15*10 + 47 + 10; i++) {
+ // observations[i] = i % 4;
+ //}
+
+ // 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);
+
+ // Rewards
+ for (int b = 0; b < net->num_agents; b++) {
+ for (int i = 0; i < 10; i++) {
+ net->ob_reward[b*10 + i] = observations[b*(11*15*10 + 47 + 10) + 11*15*10 + 47 + i];
+ }
+ }
+
+ for (int b = 0; b < net->num_agents; b++) {
+ int b_offset = b*1817;
+ for (int i = 0; i < 256; i++) {
+ net->proj_buffer[b_offset + i] = net->map_conv2->output[b*256 + i];
+ }
+
+ b_offset += 256;
+ 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];
+ }
+ }
+
+ affine(net->proj, net->proj_buffer);
+ relu(net->proj_relu, net->proj->output);
+
+ mingru(net->mingru, net->proj_relu->output);
+ linear(net->decoder, net->mingru->output);
+
+ softmax_multidiscrete(net->multidiscrete, net->decoder->output, actions);
+}
+
+void demo(int num_players) {
+ Weights* weights = load_weights("resources/nmmo3/nmmo3_weights.bin");
+ MMONet* net = init_mmonet(weights, num_players);
+
+ MMO env = {
+ .client = NULL,
+ .width = 512,
+ .height = 512,
+ .num_agents = num_players,
+ .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 = 1.0,
+ .reward_market = 0.0,
+ .reward_death = -1.0,
+ };
+ allocate_mmo(&env);
+
+ c_reset(&env);
+ c_render(&env);
+
+ float human_action = ATN_NOOP;
+ bool human_mode = false;
+ int i = 1;
+ while (!WindowShouldClose()) {
+ if (IsKeyPressed(KEY_LEFT_CONTROL)) {
+ human_mode = !human_mode;
+ }
+ if (i % 36 == 0) {
+ forward(net, env.observations, env.terminals, env.actions);
+ if (human_mode) {
+ env.actions[0] = human_action;
+ }
+
+ c_step(&env);
+ human_action = ATN_NOOP;
+ }
+ int atn = c_render(&env);
+ 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");
+ MMONet* net = init_mmonet(weights, num_players);
+
+ MMO env = {
+ .width = 512,
+ .height = 512,
+ .num_agents = 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);
+ c_reset(&env);
+
+ int start = time(NULL);
+ int num_steps = 0;
+ while (time(NULL) - start < timeout) {
+ forward(net, env.observations, env.terminals, env.actions);
+ c_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 int rng = 42;
+ unsigned char unfilled[width][height];
+ memset(unfilled, 0, width*height);
+
+ // Draw some squares
+ for (int i = 0; i < 32; i++) {
+ int w = rand_r(&rng) % width/4;
+ int h = rand_r(&rng) % height/4;
+ int start_r = rand_r(&rng) % (3*height/4);
+ int start_c = rand_r(&rng) % (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, &rng);
+
+ // 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) {
+ unsigned int rng = 42;
+ 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_r(&rng) % width/4;
+ int h = rand_r(&rng) % height/4;
+ int start_r = rand_r(&rng) % (3*height/4);
+ int start_c = rand_r(&rng) % (3*width/4);
+ int end_r = start_r + h;
+ int end_c = start_c + w;
+ int color = rand_r(&rng) % 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, &rng);
+
+ // 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];
+ unsigned int rng = 42; generate_terrain((char*)terrain, (unsigned char*)rendered, width, height, x_border, y_border, &rng);
+
+
+ // 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_agents = 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);
+ c_reset(&env);
+
+ 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_r(&env.rng) % 23;
+ }
+ c_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);
+}
+
+void test_no_render_log(int num_players, int target_episodes) {
+ Weights* weights = load_weights("resources/nmmo3/nmmo3_weights.bin");
+ MMONet* net = init_mmonet(weights, num_players);
+
+ MMO env = {
+ .client = NULL,
+ .width = 512,
+ .height = 512,
+ .num_agents = num_players,
+ .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 = 1.0,
+ .reward_market = 0.0,
+ .reward_death = -1.0,
+ };
+ allocate_mmo(&env);
+ c_reset(&env);
+
+ int num_steps = 0;
+ int prev_n = 0;
+ float prev_mcp = 0.0f;
+ while ((int)env.log.n < target_episodes) {
+ forward(net, env.observations, env.terminals, env.actions);
+ c_step(&env);
+ num_steps++;
+
+ int curr_n = (int)env.log.n;
+ if (curr_n > prev_n) {
+ float ep_mcp = env.log.min_comb_prof - prev_mcp;
+ float running_mean = env.log.min_comb_prof / (float)curr_n;
+ printf("Episode %d: min_comb_prof=%.3f running_mean=%.4f (step %d)\n",
+ curr_n, ep_mcp, running_mean, num_steps);
+ prev_n = curr_n;
+ prev_mcp = env.log.min_comb_prof;
+ }
+ }
+
+ printf("\n--- C eval summary (%d episodes, %d steps) ---\n",
+ prev_n, num_steps);
+ printf("mean min_comb_prof = %.4f\n",
+ env.log.min_comb_prof / (float)prev_n);
+
+ free_allocated_mmo(&env);
+ free_mmonet(net);
+ free(weights);
+}
+
+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);
+ //test_no_render_log(1, 100);
+ demo(NUM_AGENTS);
+}
diff --git a/pufferlib/ocean/nmmo3/nmmo3.h b/ocean/nmmo3/nmmo3.h
similarity index 96%
rename from pufferlib/ocean/nmmo3/nmmo3.h
rename to ocean/nmmo3/nmmo3.h
index b1d5c22c46..c48c93e567 100644
--- a/pufferlib/ocean/nmmo3/nmmo3.h
+++ b/ocean/nmmo3/nmmo3.h
@@ -174,17 +174,17 @@ void range(int* array, int n) {
}
}
-void shuffle(int* array, int n) {
+void shuffle(int* array, int n, unsigned int* rng) {
for (int i = 0; i < n; i++) {
- int j = rand() % n;
+ int j = rand_r(rng) % 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)
+double sample_exponential(double halving_rate, unsigned int* rng) {
+ double u = (double)rand_r(rng) / RAND_MAX; // Random number u in [0, 1)
return 1 + halving_rate*(-log(1 - u) / log(2));
}
@@ -275,7 +275,7 @@ void perlin_noise(float* map, int width, int height,
}
void flood_fill(unsigned char* input, char* output,
- int width, int height, int n, int max_fill) {
+ int width, int height, int n, int max_fill, unsigned int* rng) {
for (int r = 0; r < height; r++) {
for (int c = 0; c < width; c++) {
@@ -285,7 +285,7 @@ void flood_fill(unsigned char* input, char* output,
int* pos = calloc(width*height, sizeof(int));
range((int*)pos, width*height);
- shuffle((int*)pos, width*height);
+ shuffle((int*)pos, width*height, rng);
short queue[2*max_fill];
for (int i = 0; i < 2*max_fill; i++) {
@@ -301,7 +301,7 @@ void flood_fill(unsigned char* input, char* output,
continue;
}
- int color = rand() % n;
+ int color = rand_r(rng) % n;
output[adr] = color;
queue[0] = r;
queue[1] = c;
@@ -366,7 +366,7 @@ void flood_fill(unsigned char* input, char* output,
}
void cellular_automata(char* grid,
- int width, int height, int colors, int max_fill) {
+ int width, int height, int colors, int max_fill, unsigned int* rng) {
int* pos = calloc(2*width*height, sizeof(int));
int pos_sz = 0;
@@ -389,7 +389,7 @@ void cellular_automata(char* grid,
for (int i = 0; i < pos_sz; i+=2) {
int r = pos[i];
int c = pos[i + 1];
- int adr = rand() % pos_sz;
+ int adr = rand_r(rng) % pos_sz;
if (adr % 2 == 1) {
adr--;
}
@@ -450,7 +450,7 @@ void cellular_automata(char* grid,
}
int idx = 0;
- int winner = rand() % num_ties;
+ int winner = rand_r(rng) % num_ties;
for (int j = 0; j < colors; j++) {
if (counts[j] == max_count) {
if (idx == winner) {
@@ -469,12 +469,12 @@ void cellular_automata(char* grid,
}
void generate_terrain(char* terrain, unsigned char* rendered,
- int R, int C, int x_border, int y_border) {
+ int R, int C, int x_border, int y_border, unsigned int* rng) {
// 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;
+ int offset_x = rand_r(rng) % 100000;
+ int offset_y = rand_r(rng) % 100000;
perlin_noise(perlin_map, C, R, 1.0/64.0, 2, offset_x, offset_y);
// Flood fill connected components to determine biomes
@@ -486,10 +486,10 @@ void generate_terrain(char* terrain, unsigned char* rendered,
}
}
char *biomes = calloc(R*C, sizeof(char));
- flood_fill(ridges, biomes, R, C, 4, 4000);
+ flood_fill(ridges, biomes, R, C, 4, 4000, rng);
// Cellular automata to cover unfilled ridges
- cellular_automata(biomes, R, C, 4, 4000);
+ cellular_automata(biomes, R, C, 4, 4000, rng);
unsigned char (*rendered_ary)[C][3] = (unsigned char(*)[C][3])rendered;
@@ -680,7 +680,7 @@ struct MMO {
Client* client;
int width;
int height;
- int num_players;
+ int num_agents;
int num_enemies;
int num_resources;
int num_weapons;
@@ -697,7 +697,7 @@ struct MMO {
float* terminals;
Reward* reward_struct;
Reward* returns;
- int* actions;
+ float* actions;
int tick;
int tiers;
int levels;
@@ -714,6 +714,7 @@ struct MMO {
RespawnBuffer* enemy_respawn_buffer;
RespawnBuffer* drop_respawn_buffer;
Log log;
+ unsigned int rng;
float reward_combat_level;
float reward_prof_level;
float reward_item_level;
@@ -722,10 +723,10 @@ struct MMO {
};
Entity* get_entity(MMO* env, int pid) {
- if (pid < env->num_players) {
+ if (pid < env->num_agents) {
return &env->players[pid];
} else {
- return &env->enemies[pid - env->num_players];
+ return &env->enemies[pid - env->num_agents];
}
}
@@ -758,6 +759,9 @@ void add_player_log(MMO* env, int pid) {
log->perf = log->min_comb_prof / (float)env->levels;
log->n++;
*ret = (Reward){0};
+ if (pid < env->num_agents) {
+ env->terminals[pid] = 1.0f;
+ }
}
void init(MMO* env) {
@@ -778,9 +782,9 @@ void init(MMO* env) {
env->num_enemies, env->enemy_respawn_ticks);
env->drop_respawn_buffer = make_respawn_buffer(2*env->num_enemies, 20);
- env->returns = calloc(env->num_players, sizeof(Reward));
- env->reward_struct = calloc(env->num_players, sizeof(Reward));
- env->players = calloc(env->num_players, sizeof(Entity));
+ env->returns = calloc(env->num_agents, sizeof(Reward));
+ env->reward_struct = calloc(env->num_agents, sizeof(Reward));
+ env->players = calloc(env->num_agents, sizeof(Entity));
env->enemies = calloc(env->num_enemies, sizeof(Entity));
// TODO: Figure out how to cast to array. Size is static
@@ -790,10 +794,10 @@ void init(MMO* env) {
void allocate_mmo(MMO* env) {
// TODO: Not hardcode
- env->observations = calloc(env->num_players*(11*15*10+47+10), sizeof(unsigned char));
- env->rewards = calloc(env->num_players, sizeof(float));
- env->terminals = calloc(env->num_players, sizeof(float));
- env->actions = calloc(env->num_players, sizeof(int));
+ env->observations = calloc(env->num_agents*(11*15*10+47+10), sizeof(unsigned char));
+ env->rewards = calloc(env->num_agents, sizeof(float));
+ env->terminals = calloc(env->num_agents, sizeof(float));
+ env->actions = calloc(env->num_agents, sizeof(float));
init(env);
}
@@ -920,7 +924,7 @@ float sell_price(int idx) {
}
void compute_all_obs(MMO* env) {
- for (int pid = 0; pid < env->num_players; pid++) {
+ for (int pid = 0; pid < env->num_agents; pid++) {
Entity* player = get_entity(env, pid);
int r = player->r;
int c = player->c;
@@ -1020,7 +1024,7 @@ int safe_tile(MMO* env, int delta) {
int idx;
while (!valid) {
valid = true;
- idx = rand() % (env->width * env->height);
+ idx = rand_r(&env->rng) % (env->width * env->height);
char tile = env->terrain[idx];
if (!is_grass(tile)) {
valid = false;
@@ -1089,7 +1093,7 @@ void spawn(MMO* env, Entity* entity) {
entity->is_equipped[idx] = 0;
}
- entity->goal = (rand() % 2) == 0;
+ entity->goal = (rand_r(&env->rng) % 2) == 0;
memset(entity->min_comb_prof, 0, sizeof(entity->min_comb_prof));
entity->min_comb_prof_idx = 0;
}
@@ -1099,8 +1103,8 @@ void give_starter_gear(MMO* env, int pid, int tier) {
assert(tier <= env->tiers);
Entity* player = &env->players[pid];
- int idx = (rand() % 6) + 1;
- tier = (rand() % tier) + 1;
+ int idx = (rand_r(&env->rng) % 6) + 1;
+ tier = (rand_r(&env->rng) % tier) + 1;
player->inventory[0] = item_index(idx, tier);
player->gold += 50;
}
@@ -1174,7 +1178,7 @@ void pickup_item(MMO* env, int pid) {
// Some items are different on the ground and in inventory
if (ground_type == I_ORE) {
- int armor_id = I_HELM + rand() % 3;
+ int armor_id = I_HELM + rand_r(&env->rng) % 3;
ground_id = item_index(armor_id, ground_tier);
} else if (ground_type == I_HILT) {
ground_id = item_index(I_SWORD, ground_tier);
@@ -1264,7 +1268,7 @@ void wander(MMO* env, int pid) {
}
// Move randomly
- int direction = rand() % 4;
+ int direction = rand_r(&env->rng) % 4;
if (direction == ATN_UP) {
end_r -= 1;
} else if (direction == ATN_DOWN) {
@@ -1574,7 +1578,7 @@ void enemy_ai(MMO* env, int pid) {
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) {
+ if (target_id == -1 || target_id >= env->num_agents) {
continue;
}
@@ -1643,7 +1647,7 @@ void c_reset(MMO* env) {
// TODO: Check width/height args!
generate_terrain(env->terrain, env->rendered, env->width, env->height,
- env->x_window, env->y_window);
+ env->x_window, env->y_window, &env->rng);
for (int i = 0; i < env->width*env->height; i++) {
env->pids[i] = -1;
@@ -1666,7 +1670,7 @@ void c_reset(MMO* env) {
// 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);
+ shuffle((int*)spawn_cands, env->width*env->height, &env->rng);
for (int cand_idx = 0; cand_idx < env->width*env->height; cand_idx++) {
int cand = spawn_cands[cand_idx];
@@ -1721,7 +1725,7 @@ void c_reset(MMO* env) {
//int tier = 1 + env->tiers*level/env->levels;
int tier = 0;
while (tier < 1 || tier > env->tiers) {
- tier = sample_exponential(1);
+ tier = sample_exponential(1, &env->rng);
}
if (spawned) {
@@ -1750,7 +1754,7 @@ void c_reset(MMO* env) {
}
if (
- player_count == env->num_players &&
+ player_count == env->num_agents &&
enemy_count == env->num_enemies &&
ore_count == env->num_resources &&
herb_count == env->num_resources &&
@@ -1776,7 +1780,7 @@ void c_reset(MMO* env) {
free(spawn_cands);
//int distance = abs(r - env->height/2);
- for (int player_count = 0; player_count < env->num_players; player_count++) {
+ for (int player_count = 0; player_count < env->num_agents; player_count++) {
int pid = player_count;
Entity* player = &env->players[pid];
player->type = ENTITY_PLAYER;
@@ -1797,9 +1801,9 @@ void c_reset(MMO* env) {
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);
+ level = sample_exponential(8, &env->rng);
}
- if (rand() % 8 == 0) {
+ if (rand_r(&env->rng) % 8 == 0) {
level = 1;
}
//if (distance > 8 && r < env->height/2 && enemy_count < env->num_enemies) {
@@ -1828,7 +1832,7 @@ void c_reset(MMO* env) {
enemy->element = element;
enemy->ranged = ranged;
- env->pids[adr] = env->num_players + enemy_count;
+ env->pids[adr] = env->num_agents + enemy_count;
enemy->comb_lvl = level;
}
@@ -1875,7 +1879,7 @@ void c_step(MMO* env) {
}
}
- for (int pid = 0; pid < env->num_players + env->num_enemies; pid++) {
+ for (int pid = 0; pid < env->num_agents + env->num_enemies; pid++) {
Entity* entity = get_entity(env, pid);
entity->time_alive += 1;
int entity_type = entity->type;
@@ -1901,7 +1905,7 @@ void c_step(MMO* env) {
// 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;
+ double prob = (double)rand_r(&env->rng) / RAND_MAX;
if (prob < env->teleportitis_prob) {
r = entity->r;
c = entity->c;
@@ -2117,7 +2121,7 @@ void c_step(MMO* env) {
}
}
compute_all_obs(env);
- for (int pid = 0; pid < env->num_players; pid++) {
+ for (int pid = 0; pid < env->num_agents; pid++) {
Reward* reward = &env->reward_struct[pid];
env->rewards[pid] = (
reward->death + reward->comb_lvl
@@ -2330,7 +2334,7 @@ Animation ANIMATIONS[7] = {
#define STONE_OFFSET OFF * 1
#define DIRT_OFFSET 0
-void render_conversion(char* flat_tiles, int* flat_converted, int R, int C) {
+void render_conversion(char* flat_tiles, int* flat_converted, int R, int C, unsigned int* rng) {
char* tex_codes = tile_atlas;
char (*tiles)[C] = (char(*)[C])flat_tiles;
int (*converted)[C] = (int(*)[C])flat_converted;
@@ -2374,11 +2378,11 @@ void render_conversion(char* flat_tiles, int* flat_converted, int R, int C) {
int idx = code;
if (code == TEX_FULL) {
if (is_dirt(tile)) {
- idx = DIRT_OFFSET + rand() % 5;
+ idx = DIRT_OFFSET + rand_r(rng) % 5;
} else if (is_stone(tile)) {
- idx = STONE_OFFSET + rand() % 5;
+ idx = STONE_OFFSET + rand_r(rng) % 5;
} else if (is_water(tile)) {
- idx = WATER_OFFSET + rand() % 5;
+ idx = WATER_OFFSET + rand_r(rng) % 5;
}
} else if (is_dirt(tile)) {
idx += DIRT_OFFSET + 5;
@@ -2426,13 +2430,13 @@ void render_conversion(char* flat_tiles, int* flat_converted, int R, int C) {
} else {
int lookup = (1000*num_spring + 100*num_summer
+ 10*num_autumn + num_winter);
- int offset = (rand() % 4) * 714; // num_lerps;
+ int offset = (rand_r(rng) % 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;
+ int variant = (rand_r(rng) % 5);
+ int anim = rand_r(rng) % 3;
idx = 240 + 3*4*4*variant + 4*4*anim;
if (tile == TILE_SPRING_WATER) {
idx += 0;
@@ -2459,7 +2463,7 @@ Client* make_client(MMO* env) {
client->command_len = 0;
client->terrain = calloc(env->height*env->width, sizeof(int));
- render_conversion(env->terrain, client->terrain, env->height, env->width);
+ render_conversion(env->terrain, client->terrain, env->height, env->width, &env->rng);
client->shader = LoadShader("", TextFormat("resources/nmmo3/map_shader_%i.fs", GLSL_VERSION));
@@ -2883,7 +2887,7 @@ void render_centered(Client* client, MMO* env, int pid, int action, float delta)
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++) {
+ for (int pid = 0; pid < env->num_agents+env->num_enemies; pid++) {
draw_entity(client, env, pid, delta);
}
@@ -3045,7 +3049,7 @@ void render_fixed(Client* client, MMO* env, float delta) {
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++) {
+ for (int pid = 0; pid < env->num_agents+env->num_enemies; pid++) {
draw_entity(client, env, pid, delta);
}
@@ -3095,7 +3099,7 @@ void process_command_input(Client* client, MMO* env) {
char* pid = command + 7;
pid = pid;
int pid = atoi(pid);
- if (pid < 0 || pid > env->num_players) {
+ if (pid < 0 || pid > env->num_agents) {
client->command = "Invalid player id";
}
client->my_player = pid;
diff --git a/pufferlib/ocean/nmmo3/simplex.h b/ocean/nmmo3/simplex.h
similarity index 100%
rename from pufferlib/ocean/nmmo3/simplex.h
rename to ocean/nmmo3/simplex.h
diff --git a/pufferlib/ocean/nmmo3/tile_atlas.h b/ocean/nmmo3/tile_atlas.h
similarity index 100%
rename from pufferlib/ocean/nmmo3/tile_atlas.h
rename to ocean/nmmo3/tile_atlas.h
diff --git a/pufferlib/ocean/onestateworld/binding.c b/ocean/onestateworld/binding.c
similarity index 100%
rename from pufferlib/ocean/onestateworld/binding.c
rename to ocean/onestateworld/binding.c
diff --git a/pufferlib/ocean/onestateworld/onestateworld.c b/ocean/onestateworld/onestateworld.c
similarity index 100%
rename from pufferlib/ocean/onestateworld/onestateworld.c
rename to ocean/onestateworld/onestateworld.c
diff --git a/pufferlib/ocean/onestateworld/onestateworld.h b/ocean/onestateworld/onestateworld.h
similarity index 100%
rename from pufferlib/ocean/onestateworld/onestateworld.h
rename to ocean/onestateworld/onestateworld.h
diff --git a/pufferlib/ocean/onlyfish/binding.c b/ocean/onlyfish/binding.c
similarity index 100%
rename from pufferlib/ocean/onlyfish/binding.c
rename to ocean/onlyfish/binding.c
diff --git a/pufferlib/ocean/onlyfish/onlyfish.c b/ocean/onlyfish/onlyfish.c
similarity index 97%
rename from pufferlib/ocean/onlyfish/onlyfish.c
rename to ocean/onlyfish/onlyfish.c
index 27d257a3ee..297d0a46b6 100644
--- a/pufferlib/ocean/onlyfish/onlyfish.c
+++ b/ocean/onlyfish/onlyfish.c
@@ -41,7 +41,7 @@ int main() {
if (len > 4 && strcmp(entry->d_name + len - 4, ".bin") == 0) {
char fullpath[256];
sprintf(fullpath, "%s%s", dirpath, entry->d_name);
- Weights* weights = load_weights(fullpath, 136847);
+ Weights* weights = load_weights(fullpath);
int logit_sizes[2] = {9, 5};
nets[idx] = make_linearlstm(weights, 1, 21, logit_sizes, 2);
diff --git a/pufferlib/ocean/onlyfish/onlyfish.h b/ocean/onlyfish/onlyfish.h
similarity index 100%
rename from pufferlib/ocean/onlyfish/onlyfish.h
rename to ocean/onlyfish/onlyfish.h
diff --git a/ocean/overcooked/README.md b/ocean/overcooked/README.md
new file mode 100644
index 0000000000..bcb146c56e
--- /dev/null
+++ b/ocean/overcooked/README.md
@@ -0,0 +1,247 @@
+# Overcooked Environment
+
+A multi-agent cooking coordination environment where agents cooperate to prepare and serve onion soup. Based on the popular Overcooked video game, this environment tests agents' ability to coordinate, divide labor, and work together efficiently.
+
+## File Structure
+
+```
+overcooked/
+├── overcooked.h # Main entry point (init, reset, step, close)
+├── overcooked_types.h # Constants, enums, and struct definitions
+├── overcooked_items.h # Item and cooking pot management
+├── overcooked_obs.h # Observation computation
+├── overcooked_logic.h # Game logic (interaction, movement, cooking)
+├── overcooked_render.h # Rendering and texture management
+├── binding.c # Python bindings
+└── overcooked.py # Python environment wrapper
+```
+
+## Observation Space
+
+**39-dimensional vector per agent** — *see [compute_observations](overcooked_obs.h#L81)*
+
+### Player Features (34 dims)
+- **Orientation** (4): One-hot encoding of facing direction — [overcooked_obs.h:101-103](overcooked_obs.h#L101-L103)
+- **Held Object** (4): One-hot encoding (onion, plated_soup, plate, empty) — [overcooked_obs.h:105-116](overcooked_obs.h#L105-L116)
+- **Proximity Features** (12): Normalized (dx, dy) to nearest — [overcooked_obs.h:118-167](overcooked_obs.h#L118-L167):
+ - Onion source (ingredient box)
+ - Dish source (plate box)
+ - Plated soup on counter
+ - Serving area
+ - Empty counter
+ - Pot (stove)
+- **Nearest Soup Ingredients** (2): Onion/tomato counts in nearest plated soup or held soup (normalized) — [overcooked_obs.h:169-179](overcooked_obs.h#L169-L179)
+- **Pot Soup Ingredients** (2): Onion/tomato counts in nearest pot (normalized) — [overcooked_obs.h:181-202](overcooked_obs.h#L181-L202)
+- **Pot Existence** (1): Binary flag for reachable pot — [overcooked_obs.h:205](overcooked_obs.h#L205)
+- **Pot State** (4): Binary flags (empty, full, cooking, ready) — [overcooked_obs.h:207-215](overcooked_obs.h#L207-L215)
+- **Cooking Time** (1): Remaining cook time (normalized) — [overcooked_obs.h:217-223](overcooked_obs.h#L217-L223)
+- **Wall Detection** (4): Binary flags for walls/obstacles (up, down, left, right) — [overcooked_obs.h:225-235](overcooked_obs.h#L225-L235)
+
+### Spatial Features (4 dims)
+- **Teammate Relative Position** (2): Normalized (dx, dy) to other agent — [overcooked_obs.h:237-248](overcooked_obs.h#L237-L248)
+- **Absolute Position** (2): Normalized (x, y) coordinates — [overcooked_obs.h:250-252](overcooked_obs.h#L250-L252)
+
+### Context (1 dim)
+- **Reward** (1): Current step reward — [overcooked_obs.h:255](overcooked_obs.h#L255)
+
+## Action Space
+
+**6 discrete actions** — *see [c_step](overcooked.h#L77)*
+- 0: No-op — [ACTION_NOOP](overcooked_types.h#L43)
+- 1: Move up — [ACTION_UP](overcooked_types.h#L44)
+- 2: Move down — [ACTION_DOWN](overcooked_types.h#L45)
+- 3: Move left — [ACTION_LEFT](overcooked_types.h#L46)
+- 4: Move right — [ACTION_RIGHT](overcooked_types.h#L47)
+- 5: Interact (pick up/place items, use equipment) — [ACTION_INTERACT](overcooked_types.h#L48)
+
+## Reward System
+
+*See [evaluate_dish_served](overcooked_logic.h#L229) and [handle_interaction](overcooked_logic.h#L106)*
+
+### Main Rewards
+- **Correct dish served** (3 onions): +1.0 (shared), +0.0 (server bonus) — [overcooked_logic.h:237-241](overcooked_logic.h#L237-L241)
+- **Wrong dish served** (incorrect recipe): +0.0 (shared) — [overcooked_logic.h:252-258](overcooked_logic.h#L252-L258)
+- **Step penalty**: 0.0 — [overcooked.h:80](overcooked.h#L80)
+
+### Intermediate Rewards
+- **Pick up ingredient**: +0.05 — [overcooked_logic.h:221](overcooked_logic.h#L221)
+- **Add onion to pot**: +0.15 — [overcooked_logic.h:133](overcooked_logic.h#L133)
+- **Start cooking** (3 onions in pot): +0.15 — [overcooked_logic.h:145-147](overcooked_logic.h#L145-L147)
+- **Plate cooked soup**: +0.20 — [overcooked_logic.h:159](overcooked_logic.h#L159)
+
+## Recipe
+
+The correct recipe requires **exactly 3 onions** in the soup. Agents must:
+1. Pick up onions from ingredient boxes
+2. Add 3 onions to a pot
+3. Start cooking (interact with pot when empty-handed)
+4. Wait for soup to cook (20 steps)
+5. Pick up a plate from plate box
+6. Plate the cooked soup (interact with pot while holding plate)
+7. Deliver plated soup to serving area
+
+## Configuration
+
+*See [Overcooked class](overcooked.py#L14)*
+
+```python
+env = Overcooked(
+ num_envs=1, # Number of parallel environments
+ layout="cramped_room", # Layout name (see Available Layouts)
+ num_agents=2, # Agents per environment
+ render_mode=None, # Set to enable rendering
+ log_interval=128, # Steps between log aggregation
+ grid_size=32, # Render tile size in pixels
+
+ # Reward configuration (from config/ocean/overcooked.ini)
+ reward_dish_served_whole_team=1.0, # Shared reward for correct dish
+ reward_dish_served_agent=0.0, # Bonus for serving agent
+ reward_pot_started=0.15, # Starting correct recipe
+ reward_ingredient_added=0.15, # Adding onion to pot
+ reward_ingredient_picked=0.05, # Picking up ingredient
+ reward_soup_plated=0.20, # Plating cooked soup
+ reward_wrong_dish_served=0.0, # Serving incorrect dish
+ reward_step_penalty=0.0, # Per-step penalty
+)
+```
+
+## Game Constants
+
+- **Cooking time**: 20 steps — [COOKING_TIME](overcooked_types.h#L39)
+- **Max ingredients per pot**: 3 — [MAX_INGREDIENTS](overcooked_types.h#L40)
+- **Max episode steps**: 400 (default)
+- **Max dynamic items**: 20 — [overcooked.h:19](overcooked.h#L19)
+
+## Available Layouts
+
+*See [LAYOUTS](overcooked_types.h#L244-L259)*
+
+### cramped_room (5x5)
+
+```
++---+---+---+---+---+
+| W | C | P | C | W | W = Wall
++---+---+---+---+---+ C = Counter
+| I | | | | I | P = Pot (Stove)
++---+---+---+---+---+ I = Ingredient Box (Onions)
+| C | | | | C | D = Dish/Plate Box
++---+---+---+---+---+ S = Serving Area
+| C | | | | C |
++---+---+---+---+---+
+| W | D | C | S | W |
++---+---+---+---+---+
+```
+Spawns: (1,2) and (3,2)
+
+### asymmetric_advantages (9x5)
+
+```
++---+---+---+---+---+---+---+---+---+
+| W | C | W | W | W | W | W | C | W |
++---+---+---+---+---+---+---+---+---+
+| I | | C | S | W | I | C | | S |
++---+---+---+---+---+---+---+---+---+
+| C | | | | P | | | | C |
++---+---+---+---+---+---+---+---+---+
+| C | | | | P | | | | C |
++---+---+---+---+---+---+---+---+---+
+| W | C | C | D | W | D | C | C | W |
++---+---+---+---+---+---+---+---+---+
+```
+Spawns: (1,2) and (7,2)
+
+### forced_coordination (5x5)
+
+```
++---+---+---+---+---+
+| W | C | W | P | W | W = Wall
++---+---+---+---+---+ C = Counter
+| I | | C | | P | P = Pot (Stove)
++---+---+---+---+---+ I = Ingredient Box (Onions)
+| I | | C | | C | D = Dish/Plate Box
++---+---+---+---+---+ S = Serving Area
+| D | | C | | C |
++---+---+---+---+---+
+| W | C | W | S | W |
++---+---+---+---+---+
+```
+Spawns: (1,2) and (3,2)
+
+A challenging layout with a center wall dividing the kitchen. Agents must coordinate through limited passage points.
+
+### coordination_ring (5x5)
+
+```
++---+---+---+---+---+
+| W | C | C | P | W | W = Wall
++---+---+---+---+---+ C = Counter
+| C | | | | P | P = Pot (Stove)
++---+---+---+---+---+ I = Ingredient Box (Onions)
+| D | | C | | C | D = Dish/Plate Box
++---+---+---+---+---+ S = Serving Area
+| I | | | | C |
++---+---+---+---+---+
+| W | I | S | C | W |
++---+---+---+---+---+
+```
+Spawns: (1,2) and (3,2)
+
+Ring-shaped layout with a center counter obstacle. Agents must navigate around the center to coordinate ingredient pickup and soup delivery.
+
+### counter_circuit (8x5)
+
+```
++---+---+---+---+---+---+---+---+
+| W | C | C | P | P | C | C | W |
++---+---+---+---+---+---+---+---+
+| C | | | | | | | C |
++---+---+---+---+---+---+---+---+
+| D | | C | C | C | C | | S |
++---+---+---+---+---+---+---+---+
+| C | | | | | | | C |
++---+---+---+---+---+---+---+---+
+| W | C | C | I | I | C | C | W |
++---+---+---+---+---+---+---+---+
+```
+Spawns: (1,1) and (6,3)
+
+Circuit-shaped layout with a center counter island. Agents must coordinate around the obstacle to efficiently transport ingredients and serve dishes. Features dual pots and dual ingredient boxes for parallel cooking.
+
+## Logging Metrics
+
+*See [Log struct](overcooked_types.h#L65-L78)*
+
+| Metric | Description |
+|--------|-------------|
+| perf | Normalized performance (correct dishes served) |
+| score | Raw score (correct dishes served) |
+| episode_return | Sum of rewards over episode |
+| episode_length | Number of steps in episode |
+| dishes_served | Total dishes served (correct + wrong) |
+| correct_dishes | Number of 3-onion dishes served |
+| wrong_dishes | Number of incorrect dishes served |
+| ingredients_picked | Total ingredients picked up |
+| pots_started | Number of cooking sessions started |
+| items_dropped | Number of items placed on counters |
+| agent_collisions | Number of agent collision attempts |
+
+## Agent Reset Mechanism
+
+If an agent goes 512 steps without receiving a reward, it is automatically reset to its starting position with no held item. This prevents agents from getting stuck — [c_step](overcooked.h#L114-L133)
+
+## Building
+
+```bash
+# Build the environment
+python setup.py build_overcooked --inplace
+
+# Run standalone test
+python pufferlib/ocean/overcooked/overcooked.py
+
+# Run standalone demo with specific layout
+./overcooked cramped_room
+./overcooked asymmetric_advantages
+./overcooked forced_coordination
+./overcooked coordination_ring
+./overcooked counter_circuit
+```
diff --git a/ocean/overcooked/binding.c b/ocean/overcooked/binding.c
new file mode 100644
index 0000000000..e809a81687
--- /dev/null
+++ b/ocean/overcooked/binding.c
@@ -0,0 +1,42 @@
+#include "overcooked.h"
+
+#define OBS_SIZE 43
+#define NUM_ATNS 1
+#define ACT_SIZES {6}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Overcooked
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->layout_id = (LayoutType)dict_get(kwargs, "layout")->value;
+ env->num_agents = (int)dict_get(kwargs, "num_agents")->value;
+ env->grid_size = (int)dict_get(kwargs, "grid_size")->value;
+ env->observation_size = OBS_SIZE;
+
+ env->rewards_config.dish_served_whole_team = dict_get(kwargs, "reward_dish_served_whole_team")->value;
+ env->rewards_config.dish_served_agent = dict_get(kwargs, "reward_dish_served_agent")->value;
+ env->rewards_config.pot_started = dict_get(kwargs, "reward_pot_started")->value;
+ env->rewards_config.ingredient_added = dict_get(kwargs, "reward_ingredient_added")->value;
+ env->rewards_config.ingredient_picked = dict_get(kwargs, "reward_ingredient_picked")->value;
+ env->rewards_config.plate_picked = dict_get(kwargs, "reward_plate_picked")->value;
+ env->rewards_config.soup_plated = dict_get(kwargs, "reward_soup_plated")->value;
+ env->rewards_config.wrong_dish_served = dict_get(kwargs, "reward_wrong_dish_served")->value;
+ env->rewards_config.step_penalty = dict_get(kwargs, "reward_step_penalty")->value;
+
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ dict_set(out, "dishes_served", log->dishes_served);
+ dict_set(out, "correct_dishes", log->correct_dishes);
+ dict_set(out, "wrong_dishes", log->wrong_dishes);
+ dict_set(out, "ingredients_picked", log->ingredients_picked);
+ dict_set(out, "pots_started", log->pots_started);
+ dict_set(out, "items_dropped", log->items_dropped);
+ dict_set(out, "agent_collisions", log->agent_collisions);
+}
diff --git a/ocean/overcooked/overcooked.c b/ocean/overcooked/overcooked.c
new file mode 100644
index 0000000000..5d6e875b00
--- /dev/null
+++ b/ocean/overcooked/overcooked.c
@@ -0,0 +1,84 @@
+#include
+#include "overcooked.h"
+#include "puffernet.h"
+
+int main(int argc, char** argv) {
+ LayoutType layout_id = LAYOUT_CRAMPED_ROOM;
+ if (argc > 1) {
+ layout_id = get_layout_by_name(argv[1]);
+ }
+
+ int num_agents = 2;
+ int num_obs = 43;
+
+ // Select weights file and size based on layout
+ const char* weights_file;
+ int weights_size;
+ if (layout_id == LAYOUT_ASYMMETRIC_ADVANTAGES) {
+ weights_file = "resources/overcooked/puffer_overcooked_weights_aa.bin";
+ weights_size = 138631;
+ } else if (layout_id == LAYOUT_FORCED_COORDINATION) {
+ weights_file = "resources/overcooked/puffer_overcooked_weights_fc.bin";
+ weights_size = 138631;
+ } else if (layout_id == LAYOUT_COORDINATION_RING) {
+ weights_file = "resources/overcooked/puffer_overcooked_weights_cor.bin";
+ weights_size = 138631;
+ } else if (layout_id == LAYOUT_COUNTER_CIRCUIT) {
+ weights_file = "resources/overcooked/puffer_overcooked_weights_cc.bin";
+ weights_size = 138631;
+ } else {
+ weights_file = "resources/overcooked/puffer_overcooked_weights_cr.bin";
+ weights_size = 138631;
+ }
+
+ // Weights* weights = load_weights(weights_file);
+ // int logit_sizes[] = {6};
+ // LinearLSTM* net = make_linearlstm(weights, num_agents, num_obs, logit_sizes, 1);
+
+ Overcooked env = {
+ .layout_id = layout_id,
+ .num_agents = num_agents,
+ .grid_size = 100,
+ .rewards_config = {
+ .dish_served_whole_team = 1.0f,
+ .dish_served_agent = 0.0f,
+ .pot_started = 0.15f,
+ .ingredient_added = 0.15f,
+ .ingredient_picked = 0.05f,
+ .plate_picked = 0.05f,
+ .soup_plated = 0.20f,
+ .wrong_dish_served = 0.0f,
+ .step_penalty = 0.0f
+ },
+ .observation_size = num_obs
+ };
+
+ env.observations = (float*)calloc(num_obs * num_agents, sizeof(float));
+ env.actions = (float*)calloc(num_agents, sizeof(float));
+ env.rewards = (float*)calloc(num_agents, sizeof(float));
+ env.terminals = (float*)calloc(num_agents, sizeof(float));
+
+ init(&env);
+ c_reset(&env);
+ c_render(&env);
+
+ srand(time(NULL));
+
+ while (!WindowShouldClose()) {
+ // forward_linearlstm(net, env.observations, env.actions);
+ for (int i = 0; i < num_agents; i++) {
+ env.actions[i] = rand() % 6;
+ }
+ c_step(&env);
+ c_render(&env);
+ }
+
+ // free_linearlstm(net);
+ free(env.observations);
+ free(env.actions);
+ free(env.rewards);
+ free(env.terminals);
+ c_close(&env);
+
+ return 0;
+}
diff --git a/ocean/overcooked/overcooked.h b/ocean/overcooked/overcooked.h
new file mode 100644
index 0000000000..6ff3cdf7f3
--- /dev/null
+++ b/ocean/overcooked/overcooked.h
@@ -0,0 +1,154 @@
+/* Overcooked: a multi-agent cooking coordination environment.
+ * Agents can walk around, pick up items, and put down items.
+ */
+
+#ifndef OVERCOOKED_H
+#define OVERCOOKED_H
+
+#include "overcooked_types.h"
+#include "overcooked_items.h"
+#include "overcooked_obs.h"
+#include "overcooked_logic.h"
+#include "overcooked_render.h"
+
+static void init(Overcooked* env) {
+ const LayoutInfo* layout = get_layout_info(env->layout_id);
+ env->width = layout->width;
+ env->height = layout->height;
+ env->grid = calloc(env->width * env->height, sizeof(char));
+ env->max_items = 20;
+ env->items = calloc(env->max_items, sizeof(Item));
+ env->num_items = 0;
+ env->agents = calloc(env->num_agents, sizeof(Agent));
+ parse_grid(env);
+ init_static_cache(env);
+ init_cooking_pots(env);
+ init_pot_indices(env);
+ init_item_grid(env);
+ env->client = NULL;
+
+ memset(&env->log, 0, sizeof(Log));
+}
+
+void c_reset(Overcooked* env) {
+ env->num_items = 0;
+ reset_item_grid(env);
+ parse_grid(env);
+
+ for (int i = 0; i < env->num_stoves; i++) {
+ CookingPot* pot = &env->cooking_pots[i];
+ pot->cooking_state = NOT_COOKING;
+ pot->cooking_progress = 0;
+ pot->ingredient_count = 0;
+ pot->num_onions = 0;
+ pot->num_tomatoes = 0;
+ for (int j = 0; j < MAX_INGREDIENTS; j++) {
+ pot->ingredient_types[j] = NO_ITEM;
+ }
+ }
+
+ const LayoutInfo* layout = get_layout_info(env->layout_id);
+ for (int i = 0; i < env->num_agents; i++) {
+ if (i < layout->num_spawns) {
+ env->agents[i].x = layout->spawn_positions[i * 2];
+ env->agents[i].y = layout->spawn_positions[i * 2 + 1];
+ } else {
+ env->agents[i].x = 1 + (i % (env->width - 2));
+ env->agents[i].y = 1 + (i / (env->width - 2));
+ }
+ env->agents[i].held_item = NO_ITEM;
+ env->agents[i].facing_direction = 0;
+ env->agents[i].held_soup_onions = 0;
+ env->agents[i].held_soup_tomatoes = 0;
+ env->agents[i].held_soup_total = 0;
+ env->agents[i].ticks_since_reward = 0;
+
+ env->rewards[i] = 0.0f;
+ env->terminals[i] = 0;
+ }
+
+ env->agent_position_mask = 0;
+ for (int i = 0; i < env->num_agents; i++) {
+ set_agent_position(env, env->agents[i].x, env->agents[i].y);
+ }
+
+ compute_observations(env);
+}
+
+void c_step(Overcooked* env) {
+ for (int i = 0; i < env->num_agents; i++) {
+ int action = env->actions[i];
+ env->rewards[i] = env->rewards_config.step_penalty;
+ env->agents[i].ticks_since_reward++;
+
+ Agent* agent = &env->agents[i];
+ int new_x = agent->x;
+ int new_y = agent->y;
+
+ switch (action) {
+ case ACTION_UP: new_y -= 1; agent->facing_direction = 0; break;
+ case ACTION_DOWN: new_y += 1; agent->facing_direction = 1; break;
+ case ACTION_LEFT: new_x -= 1; agent->facing_direction = 2; break;
+ case ACTION_RIGHT: new_x += 1; agent->facing_direction = 3; break;
+ case ACTION_INTERACT: handle_interaction(env, i); break;
+ }
+
+ if (action != ACTION_INTERACT && action != ACTION_NOOP) {
+ if (is_valid_position(env, new_x, new_y, i)) {
+ clear_agent_position(env, agent->x, agent->y);
+ agent->x = new_x;
+ agent->y = new_y;
+ set_agent_position(env, new_x, new_y);
+ } else {
+ for (int j = 0; j < env->num_agents; j++) {
+ if (j != i && (int)env->agents[j].x == new_x && (int)env->agents[j].y == new_y) {
+ env->log.agent_collisions++;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ update_cooking(env);
+
+ const LayoutInfo* layout = get_layout_info(env->layout_id);
+ for (int i = 0; i < env->num_agents; i++) {
+ if (env->agents[i].ticks_since_reward % 512 == 0 && env->agents[i].ticks_since_reward > 0) {
+ clear_agent_position(env, env->agents[i].x, env->agents[i].y);
+ if (i < layout->num_spawns) {
+ env->agents[i].x = layout->spawn_positions[i * 2];
+ env->agents[i].y = layout->spawn_positions[i * 2 + 1];
+ } else {
+ env->agents[i].x = 1 + (i % (env->width - 2));
+ env->agents[i].y = 1 + (i / (env->width - 2));
+ }
+ set_agent_position(env, env->agents[i].x, env->agents[i].y);
+ env->agents[i].held_item = NO_ITEM;
+ env->agents[i].held_soup_onions = 0;
+ env->agents[i].held_soup_tomatoes = 0;
+ env->agents[i].held_soup_total = 0;
+ }
+ }
+
+ for (int i = 0; i < env->num_agents; i++) {
+ env->log.episode_return += env->rewards[i];
+ }
+
+ compute_observations(env);
+}
+
+void c_close(Overcooked* env) {
+ free(env->grid);
+ free(env->items);
+ free(env->agents);
+ free(env->cooking_pots);
+ free(env->pot_index_grid);
+ free(env->item_grid);
+ if (env->client != NULL) {
+ unload_textures(env->client);
+ free(env->client);
+ }
+}
+
+#endif // OVERCOOKED_H
\ No newline at end of file
diff --git a/ocean/overcooked/overcooked_items.h b/ocean/overcooked/overcooked_items.h
new file mode 100644
index 0000000000..980bef2c8c
--- /dev/null
+++ b/ocean/overcooked/overcooked_items.h
@@ -0,0 +1,123 @@
+/* Overcooked Items: Item and cooking pot management functions.
+ */
+
+#ifndef OVERCOOKED_ITEMS_H
+#define OVERCOOKED_ITEMS_H
+
+#include "overcooked_types.h"
+
+static inline Item* get_item_at(Overcooked* env, int x, int y) {
+ int idx = env->item_grid[y * env->width + x];
+ return (idx >= 0) ? &env->items[idx] : NULL;
+}
+
+static void add_item(Overcooked* env, int type, int x, int y) {
+ if (env->num_items < env->max_items) {
+ int idx = env->num_items;
+ env->items[idx].type = type;
+ env->items[idx].x = x;
+ env->items[idx].y = y;
+ env->items[idx].state = 0;
+ env->items[idx].num_onions = 0;
+ env->items[idx].num_tomatoes = 0;
+ env->items[idx].total_ingredients = 0;
+ env->item_grid[y * env->width + x] = idx;
+ env->num_items++;
+ }
+}
+
+static void remove_item(Overcooked* env, int x, int y) {
+ int idx = env->item_grid[y * env->width + x];
+ if (idx < 0) return;
+
+ env->item_grid[y * env->width + x] = -1;
+
+ if (idx < env->num_items - 1) {
+ Item* last = &env->items[env->num_items - 1];
+ env->items[idx] = *last;
+ env->item_grid[last->y * env->width + last->x] = idx;
+ }
+ env->num_items--;
+}
+
+static void init_cooking_pots(Overcooked* env) {
+ env->num_stoves = 0;
+ for (int i = 0; i < env->width * env->height; i++) {
+ if (env->grid[i] == STOVE) {
+ env->num_stoves++;
+ }
+ }
+
+ env->cooking_pots = calloc(env->num_stoves, sizeof(CookingPot));
+
+ int pot_index = 0;
+ for (int y = 0; y < env->height; y++) {
+ for (int x = 0; x < env->width; x++) {
+ if (env->grid[y * env->width + x] == STOVE) {
+ CookingPot* pot = &env->cooking_pots[pot_index];
+ pot->cooking_state = NOT_COOKING;
+ pot->cooking_progress = 0;
+ pot->ingredient_count = 0;
+ pot->num_onions = 0;
+ pot->num_tomatoes = 0;
+ for (int i = 0; i < MAX_INGREDIENTS; i++) {
+ pot->ingredient_types[i] = NO_ITEM;
+ }
+ pot_index++;
+ }
+ }
+ }
+}
+
+static void init_pot_indices(Overcooked* env) {
+ // Allocate pot index grid (same size as main grid)
+ env->pot_index_grid = calloc(env->width * env->height, sizeof(int));
+
+ // Initialize all cells to -1 (not a stove)
+ for (int i = 0; i < env->width * env->height; i++) {
+ env->pot_index_grid[i] = -1;
+ }
+
+ // Map stove cells to their pot indices (same order as init_cooking_pots)
+ int pot_idx = 0;
+ for (int y = 0; y < env->height; y++) {
+ for (int x = 0; x < env->width; x++) {
+ if (env->grid[y * env->width + x] == STOVE) {
+ env->pot_index_grid[y * env->width + x] = pot_idx++;
+ }
+ }
+ }
+}
+
+// O(1) pot lookup using precomputed index grid
+static inline CookingPot* get_pot_at(Overcooked* env, int x, int y) {
+ int idx = env->pot_index_grid[y * env->width + x];
+ return (idx >= 0) ? &env->cooking_pots[idx] : NULL;
+}
+
+static void init_item_grid(Overcooked* env) {
+ env->item_grid = calloc(env->width * env->height, sizeof(int));
+ for (int i = 0; i < env->width * env->height; i++) {
+ env->item_grid[i] = -1;
+ }
+}
+
+static void reset_item_grid(Overcooked* env) {
+ for (int i = 0; i < env->width * env->height; i++) {
+ env->item_grid[i] = -1;
+ }
+}
+
+static void update_cooking(Overcooked* env) {
+ for (int i = 0; i < env->num_stoves; i++) {
+ CookingPot* pot = &env->cooking_pots[i];
+ if (pot->cooking_state == COOKING) {
+ pot->cooking_progress++;
+ if (pot->cooking_progress >= COOKING_TIME) {
+ pot->cooking_state = COOKED;
+ }
+ }
+ }
+}
+
+#endif // OVERCOOKED_ITEMS_H
diff --git a/ocean/overcooked/overcooked_logic.h b/ocean/overcooked/overcooked_logic.h
new file mode 100644
index 0000000000..a2f5a58a6e
--- /dev/null
+++ b/ocean/overcooked/overcooked_logic.h
@@ -0,0 +1,255 @@
+/* Overcooked Logic: Game logic functions (parsing, interaction, movement).
+ */
+
+#ifndef OVERCOOKED_LOGIC_H
+#define OVERCOOKED_LOGIC_H
+
+#include "overcooked_types.h"
+#include "overcooked_items.h"
+
+// Forward declaration for circular dependency
+static void evaluate_dish_served(Overcooked* env, Agent* agent, int agent_idx);
+
+static void parse_grid(Overcooked* env) {
+ const LayoutInfo* layout = get_layout_info(env->layout_id);
+ for (int y = 0; y < env->height; y++) {
+ for (int x = 0; x < env->width; x++) {
+ char tile = get_layout_tile(layout, x, y);
+ int idx = y * env->width + x;
+ switch (tile) {
+ case '#': env->grid[idx] = WALL; break;
+ case '1': env->grid[idx] = COUNTER; break;
+ case '2': env->grid[idx] = STOVE; break;
+ case '3': env->grid[idx] = CUTTING_BOARD; break;
+ case '4': env->grid[idx] = INGREDIENT_BOX; break;
+ case '5': env->grid[idx] = SERVING_AREA; break;
+ case '6': env->grid[idx] = WALL; break;
+ case '7': env->grid[idx] = PLATE_BOX; break;
+ default: env->grid[idx] = EMPTY; break;
+ }
+ }
+ }
+}
+
+static void init_static_cache(Overcooked* env) {
+ // Precompute normalization factors
+ env->cache.inv_width = 1.0f / env->width;
+ env->cache.inv_height = 1.0f / env->height;
+
+ // Reset counts
+ env->cache.ingredient_box_count = 0;
+ env->cache.plate_box_count = 0;
+ env->cache.serving_area_count = 0;
+ env->cache.stove_count = 0;
+ env->cache.counter_count = 0;
+
+ // Scan grid once and cache all static tile positions
+ for (int y = 0; y < env->height; y++) {
+ for (int x = 0; x < env->width; x++) {
+ int tile = env->grid[y * env->width + x];
+ switch (tile) {
+ case INGREDIENT_BOX:
+ env->cache.ingredient_box_positions[env->cache.ingredient_box_count * 2] = x;
+ env->cache.ingredient_box_positions[env->cache.ingredient_box_count * 2 + 1] = y;
+ env->cache.ingredient_box_count++;
+ break;
+ case PLATE_BOX:
+ env->cache.plate_box_positions[env->cache.plate_box_count * 2] = x;
+ env->cache.plate_box_positions[env->cache.plate_box_count * 2 + 1] = y;
+ env->cache.plate_box_count++;
+ break;
+ case SERVING_AREA:
+ env->cache.serving_area_positions[env->cache.serving_area_count * 2] = x;
+ env->cache.serving_area_positions[env->cache.serving_area_count * 2 + 1] = y;
+ env->cache.serving_area_count++;
+ break;
+ case STOVE:
+ env->cache.stove_positions[env->cache.stove_count * 2] = x;
+ env->cache.stove_positions[env->cache.stove_count * 2 + 1] = y;
+ env->cache.stove_count++;
+ break;
+ case COUNTER:
+ env->cache.counter_positions[env->cache.counter_count * 2] = x;
+ env->cache.counter_positions[env->cache.counter_count * 2 + 1] = y;
+ env->cache.counter_count++;
+ break;
+ }
+ }
+ }
+}
+
+static inline void set_agent_position(Overcooked* env, int x, int y) {
+ env->agent_position_mask |= (1ULL << (y * env->width + x));
+}
+
+static inline void clear_agent_position(Overcooked* env, int x, int y) {
+ env->agent_position_mask &= ~(1ULL << (y * env->width + x));
+}
+
+static inline int is_agent_at(Overcooked* env, int x, int y) {
+ return (env->agent_position_mask >> (y * env->width + x)) & 1;
+}
+
+static int is_valid_position(Overcooked* env, int x, int y, int excluding_agent) {
+ (void)excluding_agent;
+ if (x < 0 || x >= env->width || y < 0 || y >= env->height) {
+ return 0;
+ }
+ if (env->grid[y * env->width + x] != EMPTY) {
+ return 0;
+ }
+ if (is_agent_at(env, x, y)) {
+ return 0;
+ }
+ return 1;
+}
+
+static void handle_interaction(Overcooked* env, int agent_idx) {
+ Agent* agent = &env->agents[agent_idx];
+ int target_x = agent->x;
+ int target_y = agent->y;
+
+ switch (agent->facing_direction) {
+ case 0: target_y -= 1; break; // Up
+ case 1: target_y += 1; break; // Down
+ case 2: target_x -= 1; break; // Left
+ case 3: target_x += 1; break; // Right
+ }
+
+ if (target_x < 0 || target_x >= env->width || target_y < 0 || target_y >= env->height) {
+ return;
+ }
+
+ int tile = env->grid[target_y * env->width + target_x];
+ Item* item = get_item_at(env, target_x, target_y);
+ CookingPot* pot = get_pot_at(env, target_x, target_y);
+
+ if (tile == STOVE && pot != NULL) {
+ if (agent->held_item == ONION || agent->held_item == TOMATO) {
+ if (pot->cooking_state == NOT_COOKING && pot->ingredient_count < MAX_INGREDIENTS) {
+ pot->ingredient_types[pot->ingredient_count] = agent->held_item;
+ pot->ingredient_count++;
+ if (agent->held_item == ONION) {
+ pot->num_onions++;
+ env->rewards[agent_idx] += env->rewards_config.ingredient_added;
+ } else if (agent->held_item == TOMATO) {
+ pot->num_tomatoes++;
+ }
+ agent->held_item = NO_ITEM;
+ }
+ }
+ else if (agent->held_item == NO_ITEM && pot->ingredient_count > 0) {
+ if (pot->cooking_state == NOT_COOKING) {
+ pot->cooking_state = COOKING;
+ pot->cooking_progress = 0;
+ env->log.pots_started++;
+ if (pot->num_onions == 3) {
+ env->rewards[agent_idx] += env->rewards_config.pot_started;
+ }
+ }
+ else if (pot->cooking_state == COOKED) {
+ return;
+ }
+ }
+ else if (agent->held_item == PLATE && pot->cooking_state == COOKED) {
+ agent->held_item = PLATED_SOUP;
+ agent->held_soup_onions = pot->num_onions;
+ agent->held_soup_tomatoes = pot->num_tomatoes;
+ agent->held_soup_total = pot->ingredient_count;
+
+ env->rewards[agent_idx] += env->rewards_config.soup_plated;
+
+ pot->cooking_state = NOT_COOKING;
+ pot->cooking_progress = 0;
+ pot->ingredient_count = 0;
+ pot->num_onions = 0;
+ pot->num_tomatoes = 0;
+ for (int i = 0; i < MAX_INGREDIENTS; i++) {
+ pot->ingredient_types[i] = NO_ITEM;
+ }
+ }
+ return;
+ }
+
+ if (tile == SERVING_AREA && agent->held_item == PLATED_SOUP) {
+ evaluate_dish_served(env, agent, agent_idx);
+
+ agent->held_item = NO_ITEM;
+ agent->held_soup_onions = 0;
+ agent->held_soup_tomatoes = 0;
+ agent->held_soup_total = 0;
+ return;
+ }
+
+ if (agent->held_item != NO_ITEM) {
+ if ((tile == COUNTER || tile == CUTTING_BOARD) && item == NULL) {
+ if (agent->held_item == PLATED_SOUP) {
+ add_item(env, agent->held_item, target_x, target_y);
+ Item* placed_soup = get_item_at(env, target_x, target_y);
+ if (placed_soup) {
+ placed_soup->num_onions = agent->held_soup_onions;
+ placed_soup->num_tomatoes = agent->held_soup_tomatoes;
+ placed_soup->total_ingredients = agent->held_soup_total;
+ }
+ agent->held_soup_onions = 0;
+ agent->held_soup_tomatoes = 0;
+ agent->held_soup_total = 0;
+ } else {
+ add_item(env, agent->held_item, target_x, target_y);
+ }
+ agent->held_item = NO_ITEM;
+ env->log.items_dropped++;
+ } else if ((tile == EMPTY) && item == NULL) {
+ agent->held_item = NO_ITEM;
+ env->log.items_dropped++;
+ }
+ }
+ else {
+ if (item != NULL) {
+ if (item->type == PLATED_SOUP) {
+ agent->held_soup_onions = item->num_onions;
+ agent->held_soup_tomatoes = item->num_tomatoes;
+ agent->held_soup_total = item->total_ingredients;
+ }
+ agent->held_item = item->type;
+ remove_item(env, target_x, target_y);
+ }
+ else if (tile == INGREDIENT_BOX) {
+ // TODO @mmbajo: What if we have Tomatoes as well?
+ // Add logs for each ingredient type
+ agent->held_item = ONION; // Always gives onions for now
+ env->log.ingredients_picked++;
+ env->rewards[agent_idx] += env->rewards_config.ingredient_picked;
+ }
+ else if (tile == PLATE_BOX) {
+ agent->held_item = PLATE;
+ env->rewards[agent_idx] += env->rewards_config.plate_picked;
+ }
+ }
+}
+
+static void evaluate_dish_served(Overcooked* env, Agent* agent, int agent_idx) {
+ int is_correct_recipe = (agent->held_soup_onions == 3);
+
+ if (is_correct_recipe) {
+ env->rewards[agent_idx] += env->rewards_config.dish_served_agent;
+ for (int i = 0; i < env->num_agents; i++) {
+ env->rewards[i] += env->rewards_config.dish_served_whole_team;
+ }
+ env->log.episode_length += agent->ticks_since_reward;
+ env->log.score += 25.0 / agent->ticks_since_reward;
+ env->log.perf += 25.0 / agent->ticks_since_reward;
+ agent->ticks_since_reward = 0;
+ env->log.correct_dishes++;
+ env->log.n++;
+ } else {
+ env->rewards[agent_idx] += env->rewards_config.wrong_dish_served;
+ for (int i = 0; i < env->num_agents; i++) {
+ env->rewards[i] += env->rewards_config.wrong_dish_served;
+ }
+ env->log.wrong_dishes++;
+ }
+ env->log.dishes_served++;
+}
+
+#endif // OVERCOOKED_LOGIC_H
diff --git a/ocean/overcooked/overcooked_obs.h b/ocean/overcooked/overcooked_obs.h
new file mode 100644
index 0000000000..c349a71f1e
--- /dev/null
+++ b/ocean/overcooked/overcooked_obs.h
@@ -0,0 +1,292 @@
+/* Overcooked Observations: Observation computation functions.
+ */
+
+#ifndef OVERCOOKED_OBS_H
+#define OVERCOOKED_OBS_H
+
+#include "overcooked_types.h"
+#include "overcooked_items.h"
+
+static Item* find_nearest_plated_soup(Overcooked* env, Agent* agent, float* dx, float* dy) {
+ *dx = 0.0f;
+ *dy = 0.0f;
+
+ if (agent->held_item == PLATED_SOUP) return NULL;
+
+ Item* nearest = NULL;
+ float min_dist = 1000.0f;
+ for (int i = 0; i < env->num_items; i++) {
+ if (env->items[i].type == PLATED_SOUP) {
+ float dist = (float)(abs(env->items[i].x - (int)agent->x) + abs(env->items[i].y - (int)agent->y));
+ if (dist < min_dist) {
+ min_dist = dist;
+ nearest = &env->items[i];
+ *dx = (env->items[i].x - agent->x) * env->cache.inv_width;
+ *dy = (env->items[i].y - agent->y) * env->cache.inv_height;
+ }
+ }
+ }
+ return nearest;
+}
+
+static void find_nearest_item_by_type(Overcooked* env, Agent* agent,
+ int item_type, float* dx, float* dy) {
+ *dx = 0.0f;
+ *dy = 0.0f;
+
+ if (agent->held_item == item_type) return;
+
+ float min_dist = 1000.0f;
+ for (int i = 0; i < env->num_items; i++) {
+ if (env->items[i].type == item_type) {
+ float dist = (float)(abs(env->items[i].x - (int)agent->x) +
+ abs(env->items[i].y - (int)agent->y));
+ if (dist < min_dist) {
+ min_dist = dist;
+ *dx = (env->items[i].x - agent->x) * env->cache.inv_width;
+ *dy = (env->items[i].y - agent->y) * env->cache.inv_height;
+ }
+ }
+ }
+}
+
+// Cached version: iterate over precomputed tile positions instead of scanning grid
+static void compute_tile_proximity_cached(Overcooked* env, Agent* agent,
+ int* positions, int count,
+ float* dx, float* dy) {
+ *dx = 0.0f;
+ *dy = 0.0f;
+
+ int min_dist = 1000;
+ int best_x = 0, best_y = 0;
+
+ for (int i = 0; i < count; i++) {
+ int x = positions[i * 2];
+ int y = positions[i * 2 + 1];
+ int dist = abs(x - (int)agent->x) + abs(y - (int)agent->y);
+ if (dist < min_dist) {
+ min_dist = dist;
+ best_x = x;
+ best_y = y;
+ }
+ }
+
+ if (min_dist < 1000) {
+ *dx = (best_x - agent->x) * env->cache.inv_width;
+ *dy = (best_y - agent->y) * env->cache.inv_height;
+ }
+}
+
+
+static void find_nearest_empty_counter(Overcooked* env, int agent_x, int agent_y, float* dx, float* dy) {
+ *dx = 0.0f;
+ *dy = 0.0f;
+ int min_dist = 1000;
+
+ // Iterate cached counter positions instead of scanning entire grid
+ for (int i = 0; i < env->cache.counter_count; i++) {
+ int x = env->cache.counter_positions[i * 2];
+ int y = env->cache.counter_positions[i * 2 + 1];
+
+ if (env->item_grid[y * env->width + x] < 0) {
+ int dist = abs(x - agent_x) + abs(y - agent_y);
+ if (dist < min_dist) {
+ min_dist = dist;
+ *dx = (x - agent_x) * env->cache.inv_width;
+ *dy = (y - agent_y) * env->cache.inv_height;
+ }
+ }
+ }
+}
+
+static void compute_observations(Overcooked* env) {
+ // 43-dimensional observation vector for each agent
+ // Structure per agent:
+ // - Player features: 38 dims (4 orientation + 4 held + 16 proximity + 2 nearest soup ingredients + 2 pot soup ingredients + 1 pot exist + 4 pot state + 1 cook time + 4 walls)
+ // - Teammate relative position: 2 dims
+ // - Absolute position: 2 dims
+ // - Reward: 1 dim
+ // Total: 43 dims
+ // Proximity: onion box, plate box, plated soup, serving, empty counter, pot, pickable onion, pickable plate
+
+ for (int agent_idx = 0; agent_idx < env->num_agents; agent_idx++) {
+ Agent* agent = &env->agents[agent_idx];
+ float* obs = &env->observations[agent_idx * env->observation_size];
+ int obs_idx = 0;
+
+ memset(obs, 0, env->observation_size * sizeof(float));
+
+ // === PLAYER-SPECIFIC FEATURES (28 dims) ===
+
+ // 1. Orientation (one-hot, 4 dims)
+ obs[obs_idx + agent->facing_direction] = 1.0f;
+ obs_idx += 4;
+
+ // 2. Held object (one-hot: onion, soup, dish, tomato, empty - 5 dims but we use 4)
+ if (agent->held_item == NO_ITEM) {
+ obs[obs_idx + 3] = 1.0f; // Empty
+ } else if (agent->held_item == ONION) {
+ obs[obs_idx + 0] = 1.0f;
+ } else if (agent->held_item == PLATED_SOUP) {
+ obs[obs_idx + 1] = 1.0f; // Soup
+ } else if (agent->held_item == PLATE) {
+ obs[obs_idx + 2] = 1.0f; // Dish
+ }
+ // Note: We don't use tomatoes in this version, keeping slot for compatibility
+ obs_idx += 4;
+
+ // 3. Proximity to key objects (dx, dy for each, 16 dims total)
+ float dx, dy;
+
+ // Nearest onion source (ingredient box) - returns (0,0) if holding onion
+ if (agent->held_item == ONION) {
+ dx = 0.0f;
+ dy = 0.0f;
+ } else {
+ compute_tile_proximity_cached(env, agent,
+ env->cache.ingredient_box_positions, env->cache.ingredient_box_count,
+ &dx, &dy);
+ }
+ obs[obs_idx++] = dx;
+ obs[obs_idx++] = dy;
+
+ // Nearest dish (plate box) - returns (0,0) if holding plate
+ if (agent->held_item == PLATE) {
+ dx = 0.0f;
+ dy = 0.0f;
+ } else {
+ compute_tile_proximity_cached(env, agent,
+ env->cache.plate_box_positions, env->cache.plate_box_count,
+ &dx, &dy);
+ }
+ obs[obs_idx++] = dx;
+ obs[obs_idx++] = dy;
+
+ // Nearest soup (plated soup) - returns (0,0) if holding soup or none exists
+ Item* nearest_soup = find_nearest_plated_soup(env, agent, &dx, &dy);
+ obs[obs_idx++] = dx;
+ obs[obs_idx++] = dy;
+
+ // Nearest serving area
+ compute_tile_proximity_cached(env, agent,
+ env->cache.serving_area_positions, env->cache.serving_area_count,
+ &dx, &dy);
+ obs[obs_idx++] = dx;
+ obs[obs_idx++] = dy;
+
+ // Nearest empty counter - special case, needs custom handling
+ find_nearest_empty_counter(env, agent->x, agent->y, &dx, &dy);
+ obs[obs_idx++] = dx;
+ obs[obs_idx++] = dy;
+
+ // Nearest pot (stove)
+ compute_tile_proximity_cached(env, agent,
+ env->cache.stove_positions, env->cache.stove_count,
+ &dx, &dy);
+ obs[obs_idx++] = dx;
+ obs[obs_idx++] = dy;
+
+ // Nearest pickable onion on counter (not in box)
+ find_nearest_item_by_type(env, agent, ONION, &dx, &dy);
+ obs[obs_idx++] = dx;
+ obs[obs_idx++] = dy;
+
+ // Nearest pickable plate on counter (not in box)
+ find_nearest_item_by_type(env, agent, PLATE, &dx, &dy);
+ obs[obs_idx++] = dx;
+ obs[obs_idx++] = dy;
+
+ // 4. Nearest soup ingredients (2 dims: onions, tomatoes in nearest plated soup or held soup)
+ if (agent->held_item == PLATED_SOUP) {
+ obs[obs_idx++] = agent->held_soup_onions / (float)MAX_INGREDIENTS;
+ obs[obs_idx++] = agent->held_soup_tomatoes / (float)MAX_INGREDIENTS;
+ } else if (nearest_soup) {
+ obs[obs_idx++] = nearest_soup->num_onions / (float)MAX_INGREDIENTS;
+ obs[obs_idx++] = nearest_soup->num_tomatoes / (float)MAX_INGREDIENTS;
+ } else {
+ obs[obs_idx++] = 0.0f;
+ obs[obs_idx++] = 0.0f;
+ }
+
+ // 5. Pot soup ingredients (2 dims: onion count, always 0 for tomatoes in nearest pot)
+ // Find nearest pot using cached stove positions
+ int min_pot_dist = 1000;
+ CookingPot* nearest_pot = NULL;
+
+ for (int i = 0; i < env->cache.stove_count; i++) {
+ int x = env->cache.stove_positions[i * 2];
+ int y = env->cache.stove_positions[i * 2 + 1];
+ int dist = abs(x - (int)agent->x) + abs(y - (int)agent->y);
+ if (dist < min_pot_dist) {
+ min_pot_dist = dist;
+ nearest_pot = get_pot_at(env, x, y);
+ }
+ }
+
+ if (nearest_pot) {
+ obs[obs_idx++] = nearest_pot->num_onions / (float)MAX_INGREDIENTS;
+ obs[obs_idx++] = 0.0f; // No tomatoes in our version
+ } else {
+ obs[obs_idx++] = 0.0f;
+ obs[obs_idx++] = 0.0f;
+ }
+
+ // 6. Reachable pot existence (1 dim)
+ obs[obs_idx++] = (nearest_pot != NULL) ? 1.0f : 0.0f;
+
+ // 7. Pot state flags (4 dims: empty, full, cooking, ready)
+ if (nearest_pot) {
+ obs[obs_idx++] = (nearest_pot->ingredient_count == 0) ? 1.0f : 0.0f; // Empty
+ obs[obs_idx++] = (nearest_pot->ingredient_count == MAX_INGREDIENTS) ? 1.0f : 0.0f; // Full (exactly MAX_INGREDIENTS)
+ obs[obs_idx++] = (nearest_pot->cooking_state == COOKING) ? 1.0f : 0.0f; // Cooking
+ obs[obs_idx++] = (nearest_pot->cooking_state == COOKED) ? 1.0f : 0.0f; // Ready
+ } else {
+ obs_idx += 4; // Skip pot state if no pot found
+ }
+
+ // 8. Remaining cooking time (1 dim)
+ if (nearest_pot && nearest_pot->cooking_state == COOKING) {
+ float remaining = (COOKING_TIME - nearest_pot->cooking_progress) / (float)COOKING_TIME;
+ obs[obs_idx++] = remaining;
+ } else {
+ obs[obs_idx++] = 0.0f;
+ }
+
+ // 9. Wall detection (4 dims: up, down, left, right)
+ // Check each direction for any non-EMPTY tile (walls, stoves, counters, serving area, ingredient box, plate box, cutting board - all are non-walkable)
+ int wall_up = (agent->y > 0) ? env->grid[((int)agent->y - 1) * env->width + (int)agent->x] : WALL;
+ int wall_down = (agent->y < env->height - 1) ? env->grid[((int)agent->y + 1) * env->width + (int)agent->x] : WALL;
+ int wall_left = (agent->x > 0) ? env->grid[(int)agent->y * env->width + ((int)agent->x - 1)] : WALL;
+ int wall_right = (agent->x < env->width - 1) ? env->grid[(int)agent->y * env->width + ((int)agent->x + 1)] : WALL;
+
+ obs[obs_idx++] = (wall_up != EMPTY) ? 1.0f : 0.0f;
+ obs[obs_idx++] = (wall_down != EMPTY) ? 1.0f : 0.0f;
+ obs[obs_idx++] = (wall_left != EMPTY) ? 1.0f : 0.0f;
+ obs[obs_idx++] = (wall_right != EMPTY) ? 1.0f : 0.0f;
+
+ // === TEAMMATE RELATIVE POSITION (2 dims) ===
+ // Find teammate (other agent)
+ int teammate_idx = (agent_idx == 0) ? 1 : 0;
+ if (teammate_idx < env->num_agents) {
+ Agent* teammate = &env->agents[teammate_idx];
+ obs[obs_idx++] = (teammate->x - agent->x) / (float)env->width;
+ obs[obs_idx++] = (teammate->y - agent->y) / (float)env->height;
+ } else {
+ // No teammate, set relative position to 0
+ obs[obs_idx++] = 0.0f;
+ obs[obs_idx++] = 0.0f;
+ }
+
+ // === ABSOLUTE POSITION (2 dims) ===
+ obs[obs_idx++] = agent->x / (float)env->width;
+ obs[obs_idx++] = agent->y / (float)env->height;
+
+ // === REWARD (1 dim) ===
+ obs[obs_idx++] = env->rewards[agent_idx];
+
+ // Total should be 43 dims (38 player features + 2 teammate relative position + 2 absolute position + 1 reward)
+ // Debug check removed - was only useful on first step
+ }
+}
+
+#endif // OVERCOOKED_OBS_H
diff --git a/ocean/overcooked/overcooked_render.h b/ocean/overcooked/overcooked_render.h
new file mode 100644
index 0000000000..043c716e23
--- /dev/null
+++ b/ocean/overcooked/overcooked_render.h
@@ -0,0 +1,511 @@
+/* Overcooked Render: All rendering and texture management functions.
+ */
+
+ #ifndef OVERCOOKED_RENDER_H
+ #define OVERCOOKED_RENDER_H
+
+ #include "overcooked_types.h"
+ #include "overcooked_items.h"
+
+ static Color get_agent_color(int held_item) {
+ switch (held_item) {
+ case NO_ITEM:
+ return BLUE; // Blue when empty-handed
+ case TOMATO:
+ return (Color){200, 50, 50, 255}; // Dark red when holding tomato
+ case ONION:
+ return (Color){255, 200, 100, 255}; // Light orange when holding onion
+ case PLATE:
+ return (Color){200, 200, 220, 255}; // Light blue-gray when holding plate
+ case SOUP:
+ return (Color){255, 140, 0, 255}; // Orange when holding soup
+ case PLATED_SOUP:
+ return (Color){255, 165, 0, 255}; // Brighter orange when holding plated soup
+ default:
+ return BLUE; // Default to blue
+ }
+ }
+
+ static void unload_textures(Client* client) {
+ UnloadTexture(client->floor);
+ UnloadTexture(client->counter);
+ UnloadTexture(client->pot);
+ UnloadTexture(client->serve);
+ UnloadTexture(client->onions_box);
+ UnloadTexture(client->tomatoes_box);
+ UnloadTexture(client->dishes_box);
+ UnloadTexture(client->wall);
+
+ UnloadTexture(client->onion);
+ UnloadTexture(client->tomato);
+ UnloadTexture(client->dish);
+ UnloadTexture(client->soup_onion);
+ UnloadTexture(client->soup_tomato);
+ UnloadTexture(client->soup_onion_dish);
+ UnloadTexture(client->soup_tomato_dish);
+
+ UnloadTexture(client->soup_onion_cooking_1);
+ UnloadTexture(client->soup_onion_cooking_2);
+ UnloadTexture(client->soup_onion_cooking_3);
+ UnloadTexture(client->soup_onion_cooked);
+ UnloadTexture(client->soup_tomato_cooking_1);
+ UnloadTexture(client->soup_tomato_cooking_2);
+ UnloadTexture(client->soup_tomato_cooking_3);
+ UnloadTexture(client->soup_tomato_cooked);
+
+ UnloadTexture(client->chef_north);
+ UnloadTexture(client->chef_south);
+ UnloadTexture(client->chef_east);
+ UnloadTexture(client->chef_west);
+ UnloadTexture(client->chef_north_onion);
+ UnloadTexture(client->chef_south_onion);
+ UnloadTexture(client->chef_east_onion);
+ UnloadTexture(client->chef_west_onion);
+ UnloadTexture(client->chef_north_tomato);
+ UnloadTexture(client->chef_south_tomato);
+ UnloadTexture(client->chef_east_tomato);
+ UnloadTexture(client->chef_west_tomato);
+ UnloadTexture(client->chef_north_dish);
+ UnloadTexture(client->chef_south_dish);
+ UnloadTexture(client->chef_east_dish);
+ UnloadTexture(client->chef_west_dish);
+ UnloadTexture(client->chef_north_soup_onion);
+ UnloadTexture(client->chef_south_soup_onion);
+ UnloadTexture(client->chef_east_soup_onion);
+ UnloadTexture(client->chef_west_soup_onion);
+ UnloadTexture(client->chef_north_soup_tomato);
+ UnloadTexture(client->chef_south_soup_tomato);
+ UnloadTexture(client->chef_east_soup_tomato);
+ UnloadTexture(client->chef_west_soup_tomato);
+
+ CloseWindow();
+ }
+
+ void c_render(Overcooked* env) {
+ if (env->client == NULL) {
+ int window_width = env->width * env->grid_size + 350;
+ int window_height = env->height * env->grid_size + 80;
+ InitWindow(window_width, window_height, "PufferLib Overcooked");
+ SetTargetFPS(16);
+ env->client = (Client*)calloc(1, sizeof(Client));
+
+ env->client->floor = LoadTexture("resources/overcooked/terrain/floor.png");
+ env->client->counter = LoadTexture("resources/overcooked/terrain/counter.png");
+ env->client->pot = LoadTexture("resources/overcooked/terrain/pot.png");
+ env->client->serve = LoadTexture("resources/overcooked/terrain/serve.png");
+ env->client->onions_box = LoadTexture("resources/overcooked/terrain/onions.png");
+ env->client->tomatoes_box = LoadTexture("resources/overcooked/terrain/tomatoes.png");
+ env->client->dishes_box = LoadTexture("resources/overcooked/terrain/dishes.png");
+ env->client->wall = LoadTexture("resources/overcooked/terrain/counter.png");
+
+ env->client->onion = LoadTexture("resources/overcooked/objects/onion.png");
+ env->client->tomato = LoadTexture("resources/overcooked/objects/tomato.png");
+ env->client->dish = LoadTexture("resources/overcooked/objects/dish.png");
+ env->client->soup_onion = LoadTexture("resources/overcooked/objects/soup-onion-cooked.png");
+ env->client->soup_tomato = LoadTexture("resources/overcooked/objects/soup-tomato-cooked.png");
+ env->client->soup_onion_dish = LoadTexture("resources/overcooked/objects/soup-onion-dish.png");
+ env->client->soup_tomato_dish = LoadTexture("resources/overcooked/objects/soup-tomato-dish.png");
+
+ env->client->soup_onion_cooking_1 = LoadTexture("resources/overcooked/objects/soup-onion-1-cooking.png");
+ env->client->soup_onion_cooking_2 = LoadTexture("resources/overcooked/objects/soup-onion-2-cooking.png");
+ env->client->soup_onion_cooking_3 = LoadTexture("resources/overcooked/objects/soup-onion-3-cooking.png");
+ env->client->soup_onion_cooked = LoadTexture("resources/overcooked/objects/soup-onion-cooked.png");
+ env->client->soup_tomato_cooking_1 = LoadTexture("resources/overcooked/objects/soup-tomato-1-cooking.png");
+ env->client->soup_tomato_cooking_2 = LoadTexture("resources/overcooked/objects/soup-tomato-2-cooking.png");
+ env->client->soup_tomato_cooking_3 = LoadTexture("resources/overcooked/objects/soup-tomato-3-cooking.png");
+ env->client->soup_tomato_cooked = LoadTexture("resources/overcooked/objects/soup-tomato-cooked.png");
+
+ env->client->chef_north = LoadTexture("resources/overcooked/chefs/NORTH.png");
+ env->client->chef_south = LoadTexture("resources/overcooked/chefs/SOUTH.png");
+ env->client->chef_east = LoadTexture("resources/overcooked/chefs/EAST.png");
+ env->client->chef_west = LoadTexture("resources/overcooked/chefs/WEST.png");
+ env->client->chef_north_onion = LoadTexture("resources/overcooked/chefs/NORTH-onion.png");
+ env->client->chef_south_onion = LoadTexture("resources/overcooked/chefs/SOUTH-onion.png");
+ env->client->chef_east_onion = LoadTexture("resources/overcooked/chefs/EAST-onion.png");
+ env->client->chef_west_onion = LoadTexture("resources/overcooked/chefs/WEST-onion.png");
+ env->client->chef_north_tomato = LoadTexture("resources/overcooked/chefs/NORTH-tomato.png");
+ env->client->chef_south_tomato = LoadTexture("resources/overcooked/chefs/SOUTH-tomato.png");
+ env->client->chef_east_tomato = LoadTexture("resources/overcooked/chefs/EAST-tomato.png");
+ env->client->chef_west_tomato = LoadTexture("resources/overcooked/chefs/WEST-tomato.png");
+ env->client->chef_north_dish = LoadTexture("resources/overcooked/chefs/NORTH-dish.png");
+ env->client->chef_south_dish = LoadTexture("resources/overcooked/chefs/SOUTH-dish.png");
+ env->client->chef_east_dish = LoadTexture("resources/overcooked/chefs/EAST-dish.png");
+ env->client->chef_west_dish = LoadTexture("resources/overcooked/chefs/WEST-dish.png");
+
+ env->client->chef_north_soup_onion = LoadTexture("resources/overcooked/chefs/NORTH-soup-onion.png");
+ env->client->chef_south_soup_onion = LoadTexture("resources/overcooked/chefs/SOUTH-soup-onion.png");
+ env->client->chef_east_soup_onion = LoadTexture("resources/overcooked/chefs/EAST-soup-onion.png");
+ env->client->chef_west_soup_onion = LoadTexture("resources/overcooked/chefs/WEST-soup-onion.png");
+ env->client->chef_north_soup_tomato = LoadTexture("resources/overcooked/chefs/NORTH-soup-tomato.png");
+ env->client->chef_south_soup_tomato = LoadTexture("resources/overcooked/chefs/SOUTH-soup-tomato.png");
+ env->client->chef_east_soup_tomato = LoadTexture("resources/overcooked/chefs/EAST-soup-tomato.png");
+ env->client->chef_west_soup_tomato = LoadTexture("resources/overcooked/chefs/WEST-soup-tomato.png");
+ }
+
+ if (IsKeyDown(KEY_ESCAPE)) exit(0);
+
+ BeginDrawing();
+ ClearBackground((Color){240, 240, 240, 255});
+
+ DrawText(TextFormat("Correct Dishes: %d", (int)env->log.n), 10, 10, 20, BLACK);
+ DrawText(TextFormat("Total Dishes: %d", (int)env->log.dishes_served), 10, 35, 20, BLACK);
+ DrawText("Recipe: 3 Onions", 10, 60, 16, DARKGRAY);
+
+ int grid_offset_y = 80;
+ for (int y = 0; y < env->height; y++) {
+ for (int x = 0; x < env->width; x++) {
+ int idx = y * env->width + x;
+ Rectangle dest = {x * env->grid_size, y * env->grid_size + grid_offset_y, env->grid_size, env->grid_size};
+
+ if (env->client->floor.id != 0) {
+ DrawTexturePro(env->client->floor,
+ (Rectangle){0, 0, env->client->floor.width, env->client->floor.height},
+ dest, (Vector2){0, 0}, 0, WHITE);
+ }
+
+ Texture2D* texture = NULL;
+ switch (env->grid[idx]) {
+ case COUNTER:
+ texture = &env->client->counter;
+ break;
+ case STOVE:
+ texture = &env->client->pot;
+ break;
+ case CUTTING_BOARD:
+ texture = &env->client->counter;
+ break;
+ case INGREDIENT_BOX:
+ texture = &env->client->onions_box;
+ break;
+ case SERVING_AREA:
+ texture = &env->client->serve;
+ break;
+ case PLATE_BOX:
+ texture = &env->client->dishes_box;
+ break;
+ case WALL:
+ texture = &env->client->wall;
+ break;
+ }
+
+ if (texture && texture->id != 0) {
+ DrawTexturePro(*texture,
+ (Rectangle){0, 0, texture->width, texture->height},
+ dest, (Vector2){0, 0}, 0, WHITE);
+ }
+
+ if (env->grid[idx] == STOVE) {
+ CookingPot* pot = get_pot_at(env, x, y);
+ if (pot && pot->ingredient_count > 0) {
+ Texture2D* cooking_texture = NULL;
+
+ bool is_onion_soup = (pot->num_onions >= pot->num_tomatoes);
+ if (is_onion_soup) {
+ if (pot->ingredient_count <= 1) {
+ cooking_texture = &env->client->soup_onion_cooking_1;
+ } else if (pot->ingredient_count == 2) {
+ cooking_texture = &env->client->soup_onion_cooking_2;
+ } else {
+ cooking_texture = &env->client->soup_onion_cooking_3;
+ }
+ } else {
+ if (pot->ingredient_count <= 1) {
+ cooking_texture = &env->client->soup_tomato_cooking_1;
+ } else if (pot->ingredient_count == 2) {
+ cooking_texture = &env->client->soup_tomato_cooking_2;
+ } else {
+ cooking_texture = &env->client->soup_tomato_cooking_3;
+ }
+ }
+
+ if (pot->cooking_state == COOKING) {
+ float progress = (float)pot->cooking_progress / COOKING_TIME;
+
+ DrawRectangle(x * env->grid_size + 5,
+ y * env->grid_size + grid_offset_y + env->grid_size - 10,
+ (env->grid_size - 10) * progress, 3, GREEN);
+ DrawRectangleLines(x * env->grid_size + 5,
+ y * env->grid_size + grid_offset_y + env->grid_size - 10,
+ env->grid_size - 10, 3, BLACK);
+ }
+ else if (pot->cooking_state == COOKED) {
+ cooking_texture = is_onion_soup ? &env->client->soup_onion_cooked :
+ &env->client->soup_tomato_cooked;
+ DrawText("READY!", x * env->grid_size + 5,
+ y * env->grid_size + grid_offset_y + env->grid_size - 10,
+ 8, GREEN);
+ }
+
+ if (cooking_texture && cooking_texture->id != 0) {
+ Rectangle pot_dest = {
+ x * env->grid_size,
+ y * env->grid_size + grid_offset_y,
+ env->grid_size,
+ env->grid_size
+ };
+ DrawTexturePro(*cooking_texture,
+ (Rectangle){0, 0, cooking_texture->width, cooking_texture->height},
+ pot_dest, (Vector2){0, 0}, 0, WHITE);
+ }
+ }
+ }
+ }
+ }
+
+ for (int i = 0; i < env->num_items; i++) {
+ Texture2D* texture = NULL;
+ switch (env->items[i].type) {
+ case TOMATO:
+ texture = &env->client->tomato;
+ break;
+ case ONION:
+ texture = &env->client->onion;
+ break;
+ case PLATE:
+ texture = &env->client->dish;
+ break;
+ case SOUP:
+ texture = &env->client->soup_onion;
+ break;
+ case PLATED_SOUP:
+ if (env->items[i].num_onions >= env->items[i].num_tomatoes) {
+ texture = &env->client->soup_onion_dish;
+ } else {
+ texture = &env->client->soup_tomato_dish;
+ }
+ break;
+ }
+
+ if (texture && texture->id != 0) {
+ Rectangle dest = {
+ env->items[i].x * env->grid_size + env->grid_size/4,
+ env->items[i].y * env->grid_size + grid_offset_y + env->grid_size/4,
+ env->grid_size/2,
+ env->grid_size/2
+ };
+ DrawTexturePro(*texture,
+ (Rectangle){0, 0, texture->width, texture->height},
+ dest, (Vector2){0, 0}, 0, WHITE);
+ } else {
+ Color item_color = GRAY;
+ switch (env->items[i].type) {
+ case TOMATO: item_color = RED; break;
+ case ONION: item_color = YELLOW; break;
+ case PLATE: item_color = WHITE; break;
+ case SOUP: item_color = ORANGE; break;
+ case PLATED_SOUP: item_color = ORANGE; break;
+ }
+ DrawCircle(
+ env->items[i].x * env->grid_size + env->grid_size/2,
+ env->items[i].y * env->grid_size + grid_offset_y + env->grid_size/2,
+ env->grid_size/4,
+ item_color
+ );
+ }
+ }
+
+ for (int agent_idx = 0; agent_idx < env->num_agents; agent_idx++) {
+ Agent* agent = &env->agents[agent_idx];
+ Texture2D* chef_texture = NULL;
+
+ if (agent->held_item == NO_ITEM) {
+ switch (agent->facing_direction) {
+ case 0: chef_texture = &env->client->chef_north; break;
+ case 1: chef_texture = &env->client->chef_south; break;
+ case 2: chef_texture = &env->client->chef_west; break;
+ case 3: chef_texture = &env->client->chef_east; break;
+ }
+ } else if (agent->held_item == ONION) {
+ switch (agent->facing_direction) {
+ case 0: chef_texture = &env->client->chef_north_onion; break;
+ case 1: chef_texture = &env->client->chef_south_onion; break;
+ case 2: chef_texture = &env->client->chef_west_onion; break;
+ case 3: chef_texture = &env->client->chef_east_onion; break;
+ }
+ } else if (agent->held_item == TOMATO) {
+ switch (agent->facing_direction) {
+ case 0: chef_texture = &env->client->chef_north_tomato; break;
+ case 1: chef_texture = &env->client->chef_south_tomato; break;
+ case 2: chef_texture = &env->client->chef_west_tomato; break;
+ case 3: chef_texture = &env->client->chef_east_tomato; break;
+ }
+ } else if (agent->held_item == PLATE) {
+ switch (agent->facing_direction) {
+ case 0: chef_texture = &env->client->chef_north_dish; break;
+ case 1: chef_texture = &env->client->chef_south_dish; break;
+ case 2: chef_texture = &env->client->chef_west_dish; break;
+ case 3: chef_texture = &env->client->chef_east_dish; break;
+ }
+ } else if (agent->held_item == PLATED_SOUP) {
+ bool is_onion_soup = (agent->held_soup_onions >= agent->held_soup_tomatoes);
+ if (is_onion_soup) {
+ switch (agent->facing_direction) {
+ case 0: chef_texture = &env->client->chef_north_soup_onion; break;
+ case 1: chef_texture = &env->client->chef_south_soup_onion; break;
+ case 2: chef_texture = &env->client->chef_west_soup_onion; break;
+ case 3: chef_texture = &env->client->chef_east_soup_onion; break;
+ }
+ } else {
+ switch (agent->facing_direction) {
+ case 0: chef_texture = &env->client->chef_north_soup_tomato; break;
+ case 1: chef_texture = &env->client->chef_south_soup_tomato; break;
+ case 2: chef_texture = &env->client->chef_west_soup_tomato; break;
+ case 3: chef_texture = &env->client->chef_east_soup_tomato; break;
+ }
+ }
+ }
+
+ if (chef_texture && chef_texture->id != 0) {
+ Rectangle dest = {
+ agent->x * env->grid_size,
+ agent->y * env->grid_size + grid_offset_y,
+ env->grid_size,
+ env->grid_size
+ };
+ Color tint = WHITE;
+ if (agent_idx == 0) {
+ tint = (Color){255, 255, 255, 255}; // White for player 1
+ } else if (agent_idx == 1) {
+ tint = (Color){200, 200, 255, 255}; // Light blue tint for player 2
+ } else {
+ tint = (Color){255, 200, 200, 255}; // Light red tint for other players
+ }
+ DrawTexturePro(*chef_texture,
+ (Rectangle){0, 0, chef_texture->width, chef_texture->height},
+ dest, (Vector2){0, 0}, 0, tint);
+ } else {
+ Color agent_color = get_agent_color(agent->held_item);
+ if (agent_idx == 1) {
+ agent_color = (Color){agent_color.r * 0.8, agent_color.g * 0.8, agent_color.b, agent_color.a};
+ }
+ DrawRectangle(
+ agent->x * env->grid_size + env->grid_size/4,
+ agent->y * env->grid_size + grid_offset_y + env->grid_size/4,
+ env->grid_size/2,
+ env->grid_size/2,
+ agent_color
+ );
+
+ int dir_x = agent->x * env->grid_size + env->grid_size/2;
+ int dir_y = agent->y * env->grid_size + grid_offset_y + env->grid_size/2;
+ int end_x = dir_x, end_y = dir_y;
+ switch (agent->facing_direction) {
+ case 0: end_y -= env->grid_size/4; break; // Up
+ case 1: end_y += env->grid_size/4; break; // Down
+ case 2: end_x -= env->grid_size/4; break; // Left
+ case 3: end_x += env->grid_size/4; break; // Right
+ }
+ DrawLine(dir_x, dir_y, end_x, end_y, BLACK);
+
+ DrawText(TextFormat("%d", agent_idx + 1),
+ agent->x * env->grid_size + 2,
+ agent->y * env->grid_size + grid_offset_y + 2,
+ 10, BLACK);
+ }
+ }
+
+ int obs_panel_x = env->width * env->grid_size + 10;
+ int obs_panel_y = grid_offset_y;
+
+ if (env->num_agents > 0) {
+ float* obs = &env->observations[0];
+
+ DrawText("=== OBSERVATION ARRAY (43 dims) ===", obs_panel_x, obs_panel_y, 11, BLACK);
+ obs_panel_y += 18;
+
+ DrawText("-- PLAYER (0-33) --", obs_panel_x, obs_panel_y, 10, DARKGREEN);
+ obs_panel_y += 13;
+
+ DrawText(TextFormat("[0-3] Orient: %.0f %.0f %.0f %.0f",
+ obs[0], obs[1], obs[2], obs[3]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 11;
+
+ DrawText(TextFormat("[4-7] Held: %.0f %.0f %.0f %.0f",
+ obs[4], obs[5], obs[6], obs[7]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 11;
+
+ DrawText(TextFormat("[8-9] Onion: %.2f, %.2f", obs[8], obs[9]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+ DrawText(TextFormat("[10-11] Dish: %.2f, %.2f", obs[10], obs[11]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+ DrawText(TextFormat("[12-13] Soup: %.2f, %.2f", obs[12], obs[13]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+ DrawText(TextFormat("[14-15] Serve: %.2f, %.2f", obs[14], obs[15]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+ DrawText(TextFormat("[16-17] Empty: %.2f, %.2f", obs[16], obs[17]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+ DrawText(TextFormat("[18-19] Pot: %.2f, %.2f", obs[18], obs[19]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[20-21] PickOnion: %.2f, %.2f", obs[20], obs[21]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[22-23] PickPlate: %.2f, %.2f", obs[22], obs[23]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[24-25] SoupIngr: %.2f, %.2f", obs[24], obs[25]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[26-27] PotIngr: %.2f, %.2f", obs[26], obs[27]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[28] PotExists: %.0f", obs[28]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[29-32] PotState: %.0f %.0f %.0f %.0f",
+ obs[29], obs[30], obs[31], obs[32]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[33] CookTime: %.2f", obs[33]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[34-37] Walls: %.0f %.0f %.0f %.0f",
+ obs[34], obs[35], obs[36], obs[37]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 13;
+
+ DrawText("-- TEAMMATE (38-39) --", obs_panel_x, obs_panel_y, 10, DARKBLUE);
+ obs_panel_y += 13;
+
+ if (env->num_agents > 1) {
+ DrawText(TextFormat("[38-39] T.RelPos: %.2f, %.2f", obs[38], obs[39]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+ } else {
+ DrawText("No teammate", obs_panel_x, obs_panel_y, 9, GRAY);
+ obs_panel_y += 10;
+ }
+
+ obs_panel_y += 3;
+ DrawText("-- MISC (40-42) --", obs_panel_x, obs_panel_y, 10, DARKGRAY);
+ obs_panel_y += 13;
+
+ DrawText(TextFormat("[40-41] AbsPos: %.3f, %.3f", obs[40], obs[41]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+
+ DrawText(TextFormat("[42] Reward: %.2f", obs[42]),
+ obs_panel_x, obs_panel_y, 9, BLACK);
+ obs_panel_y += 10;
+ }
+
+ EndDrawing();
+ }
+
+ #endif // OVERCOOKED_RENDER_H
+
\ No newline at end of file
diff --git a/ocean/overcooked/overcooked_types.h b/ocean/overcooked/overcooked_types.h
new file mode 100644
index 0000000000..5e46abcec1
--- /dev/null
+++ b/ocean/overcooked/overcooked_types.h
@@ -0,0 +1,327 @@
+/* Overcooked Types: Constants, enums, and struct definitions.
+ */
+
+#ifndef OVERCOOKED_TYPES_H
+#define OVERCOOKED_TYPES_H
+
+#include
+#include
+#include
+#include
+#include
+#include "raylib.h"
+
+// Tile types
+#define EMPTY 0
+#define COUNTER 1
+#define STOVE 2
+#define CUTTING_BOARD 3
+#define INGREDIENT_BOX 4
+#define SERVING_AREA 5
+#define WALL 6
+#define PLATE_BOX 7
+#define AGENT 8
+
+// Item types
+#define NO_ITEM 10
+#define TOMATO 11
+#define ONION 12
+#define PLATE 13
+#define SOUP 14
+#define PLATED_SOUP 15
+
+// Cooking states
+#define NOT_COOKING 0
+#define COOKING 1
+#define COOKED 2
+
+// Cooking parameters
+#define COOKING_TIME 20
+#define MAX_INGREDIENTS 3
+
+// Actions
+#define ACTION_NOOP 0
+#define ACTION_UP 1
+#define ACTION_DOWN 2
+#define ACTION_LEFT 3
+#define ACTION_RIGHT 4
+#define ACTION_INTERACT 5
+
+// Agent states
+#define AGENT_EMPTY_HANDED 0
+#define AGENT_HOLDING_ITEM 1
+
+#define MAX_SPAWN_POSITIONS 8
+
+typedef enum {
+ LAYOUT_CRAMPED_ROOM = 0,
+ LAYOUT_ASYMMETRIC_ADVANTAGES = 1,
+ LAYOUT_FORCED_COORDINATION = 2,
+ LAYOUT_COORDINATION_RING = 3,
+ LAYOUT_COUNTER_CIRCUIT = 4,
+ LAYOUT_COUNT
+} LayoutType;
+
+typedef struct {
+ const char* name;
+ int width;
+ int height;
+ const char* grid;
+ int spawn_positions[MAX_SPAWN_POSITIONS];
+ int num_spawns;
+} LayoutInfo;
+
+typedef struct {
+ float dish_served_whole_team;
+ float dish_served_agent;
+ float pot_started;
+ float ingredient_added;
+ float ingredient_picked;
+ float plate_picked;
+ float soup_plated;
+ float wrong_dish_served;
+ float step_penalty;
+} RewardConfig;
+
+typedef struct {
+ float perf; // Recommended 0-1 normalized single real number perf metric
+ float score; // Recommended unnormalized single real number perf metric
+ float episode_return; // Recommended metric: sum of agent rewards over episode
+ float episode_length; // Recommended metric: number of steps of agent episode
+ float dishes_served; // Number of dishes successfully served
+ float correct_dishes; // Number of correct 3-onion dishes
+ float wrong_dishes; // Number of wrong dishes submitted
+ float ingredients_picked; // Total ingredients picked up
+ float pots_started; // Number of cooking sessions started
+ float items_dropped; // Number of items dropped/placed
+ float agent_collisions; // Number of times agents tried to move to same spot
+ float n; // Required as the last field
+} Log;
+
+typedef struct {
+ Texture2D floor;
+ Texture2D counter;
+ Texture2D pot;
+ Texture2D serve;
+ Texture2D onions_box;
+ Texture2D tomatoes_box;
+ Texture2D dishes_box;
+ Texture2D wall;
+
+ Texture2D onion;
+ Texture2D tomato;
+ Texture2D dish;
+ Texture2D soup_onion;
+ Texture2D soup_tomato;
+
+ Texture2D soup_onion_cooking_1;
+ Texture2D soup_onion_cooking_2;
+ Texture2D soup_onion_cooking_3;
+ Texture2D soup_onion_cooked;
+ Texture2D soup_tomato_cooking_1;
+ Texture2D soup_tomato_cooking_2;
+ Texture2D soup_tomato_cooking_3;
+ Texture2D soup_tomato_cooked;
+
+ Texture2D chef_north;
+ Texture2D chef_south;
+ Texture2D chef_east;
+ Texture2D chef_west;
+ Texture2D chef_north_onion;
+ Texture2D chef_south_onion;
+ Texture2D chef_east_onion;
+ Texture2D chef_west_onion;
+ Texture2D chef_north_tomato;
+ Texture2D chef_south_tomato;
+ Texture2D chef_east_tomato;
+ Texture2D chef_west_tomato;
+ Texture2D chef_north_dish;
+ Texture2D chef_south_dish;
+ Texture2D chef_east_dish;
+ Texture2D chef_west_dish;
+ Texture2D chef_north_soup_onion;
+ Texture2D chef_south_soup_onion;
+ Texture2D chef_east_soup_onion;
+ Texture2D chef_west_soup_onion;
+ Texture2D chef_north_soup_tomato;
+ Texture2D chef_south_soup_tomato;
+ Texture2D chef_east_soup_tomato;
+ Texture2D chef_west_soup_tomato;
+
+ Texture2D soup_onion_dish;
+ Texture2D soup_tomato_dish;
+} Client;
+
+typedef struct __attribute__((aligned(32))) {
+ float x;
+ float y;
+ int facing_direction;
+ int held_item;
+ int held_soup_onions;
+ int held_soup_tomatoes;
+ int held_soup_total;
+ int ticks_since_reward;
+} Agent;
+
+typedef struct __attribute__((aligned(32))) {
+ int x;
+ int y;
+ int type;
+ int state;
+ int num_onions;
+ int num_tomatoes;
+ int total_ingredients;
+} Item;
+
+typedef struct {
+ int cooking_state; // NOT_COOKING, COOKING, COOKED
+ int cooking_progress; // Steps since cooking started
+ int ingredient_types[MAX_INGREDIENTS]; // Types of ingredients added
+ int ingredient_count; // Number of ingredients in pot
+ int num_onions; // Count of onions
+ int num_tomatoes; // Count of tomatoes
+} CookingPot;
+
+// Cache for static tile positions (computed once at init, never changes)
+typedef struct {
+ // Static tile positions stored as x,y pairs: [x0, y0, x1, y1, ...]
+ int ingredient_box_positions[20]; // Max 10 ingredient boxes
+ int ingredient_box_count;
+ int plate_box_positions[20]; // Max 10 plate boxes
+ int plate_box_count;
+ int serving_area_positions[20]; // Max 10 serving areas
+ int serving_area_count;
+ int stove_positions[20]; // Max 10 stoves
+ int stove_count;
+ int counter_positions[100]; // Max 50 counters
+ int counter_count;
+
+ // Precomputed normalization factors
+ float inv_width; // 1.0f / width
+ float inv_height; // 1.0f / height
+} StaticCache;
+
+typedef struct {
+ Log log; // Required field. Env binding code uses this to aggregate logs
+ Client* client;
+ LayoutType layout_id;
+ char* grid;
+ Item* items; // Dynamic items in the kitchen
+ int num_items;
+ int max_items;
+ Agent* agents; // Array of agents
+ int num_agents;
+ uint64_t agent_position_mask; // Bit (y * width + x) set if agent present
+ CookingPot* cooking_pots; // Array of cooking pots (one per stove)
+ int num_stoves;
+ int* pot_index_grid; // Maps grid cell to pot index (-1 if not a stove)
+ int* item_grid; // Maps grid cell to item index (-1 if empty)
+ float* observations; // Required. You can use any obs type, but make sure it matches in Python!
+ float* actions; // Required. int* for discrete/multidiscrete, float* for box
+ float* rewards; // Required
+ float* terminals; // Required. We don't yet have truncations as standard yet
+ int width;
+ int height;
+ int grid_size;
+ RewardConfig rewards_config;
+ int observation_size;
+ StaticCache cache; // Cached static tile positions for O(1) lookup
+ unsigned int rng;
+} Overcooked;
+
+// Grid layout
+static const char CRAMPED_ROOM[5][5] = {
+ {'6', '1', '2', '1', '6'},
+ {'4', ' ', ' ', ' ', '4'},
+ {'1', ' ', ' ', ' ', '1'},
+ {'1', ' ', ' ', ' ', '1'},
+ {'6', '7', '1', '5', '6'}
+};
+
+static const char ASYMMETRIC_ADVANTAGES[5][9] = {
+ {'6','1','6','6','6','6','6','1','6'},
+ {'4',' ','1','5','6','4','1',' ','5'},
+ {'1',' ',' ',' ','2',' ',' ',' ','1'},
+ {'1',' ',' ',' ','2',' ',' ',' ','1'},
+ {'6','1','1','7','6','7','1','1','6'}
+};
+
+static const char COORDINATION_RING[5][5] = {
+ {'6', '1', '1', '2', '6'},
+ {'1', ' ', ' ', ' ', '2'},
+ {'7', ' ', '1', ' ', '1'},
+ {'4', ' ', ' ', ' ', '1'},
+ {'6', '4', '5', '1', '6'}
+};
+
+static const char FORCED_COORDINATION[5][5] = {
+ {'6', '1', '6', '2', '6'},
+ {'4', ' ', '1', ' ', '2'},
+ {'4', ' ', '1', ' ', '1'},
+ {'7', ' ', '1', ' ', '1'},
+ {'6', '1', '6', '5', '6'}
+};
+
+static const char COUNTER_CIRCUIT[5][8] = {
+ {'6','1','1','2','2','1','1','6'},
+ {'1',' ',' ',' ',' ',' ',' ','1'},
+ {'7',' ','1','1','1','1',' ','5'},
+ {'1',' ',' ',' ',' ',' ',' ','1'},
+ {'6','1','1','4','4','1','1','6'}
+};
+
+static const LayoutInfo LAYOUTS[LAYOUT_COUNT] = {
+ {
+ "cramped_room",
+ 5, 5,
+ (const char*)CRAMPED_ROOM,
+ {1, 2, 3, 2},
+ 2
+ },
+ {
+ "asymmetric_advantages",
+ 9, 5,
+ (const char*)ASYMMETRIC_ADVANTAGES,
+ {1, 2, 7, 2},
+ 2
+ },
+ {
+ "forced_coordination",
+ 5, 5,
+ (const char*)FORCED_COORDINATION,
+ {1, 2, 3, 2},
+ 2
+ },
+ {
+ "coordination_ring",
+ 5, 5,
+ (const char*)COORDINATION_RING,
+ {1, 2, 3, 2},
+ 2
+ },
+ {
+ "counter_circuit",
+ 8, 5,
+ (const char*)COUNTER_CIRCUIT,
+ {1, 1, 6, 3},
+ 2
+ }
+};
+
+static inline const LayoutInfo* get_layout_info(LayoutType id) {
+ if (id < 0 || id >= LAYOUT_COUNT) return &LAYOUTS[0];
+ return &LAYOUTS[id];
+}
+
+static inline char get_layout_tile(const LayoutInfo* info, int x, int y) {
+ return info->grid[y * info->width + x];
+}
+
+static inline LayoutType get_layout_by_name(const char* name) {
+ for (int i = 0; i < LAYOUT_COUNT; i++) {
+ if (strcmp(LAYOUTS[i].name, name) == 0) return (LayoutType)i;
+ }
+ return LAYOUT_CRAMPED_ROOM;
+}
+
+#endif // OVERCOOKED_TYPES_H
diff --git a/ocean/pacman/binding.c b/ocean/pacman/binding.c
new file mode 100644
index 0000000000..06e36ceb4d
--- /dev/null
+++ b/ocean/pacman/binding.c
@@ -0,0 +1,27 @@
+#include "pacman.h"
+#define OBS_SIZE 291
+#define NUM_ATNS 1
+#define ACT_SIZES {4}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env PacmanEnv
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->randomize_starting_position = dict_get(kwargs, "randomize_starting_position")->value;
+ env->min_start_timeout = dict_get(kwargs, "min_start_timeout")->value;
+ env->max_start_timeout = dict_get(kwargs, "max_start_timeout")->value;
+ env->frightened_time = dict_get(kwargs, "frightened_time")->value;
+ env->max_mode_changes = dict_get(kwargs, "max_mode_changes")->value;
+ env->scatter_mode_length = dict_get(kwargs, "scatter_mode_length")->value;
+ env->chase_mode_length = dict_get(kwargs, "chase_mode_length")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+}
diff --git a/pufferlib/ocean/pacman/helpers.h b/ocean/pacman/helpers.h
similarity index 100%
rename from pufferlib/ocean/pacman/helpers.h
rename to ocean/pacman/helpers.h
diff --git a/ocean/pacman/pacman.c b/ocean/pacman/pacman.c
new file mode 100644
index 0000000000..0a8acacbb6
--- /dev/null
+++ b/ocean/pacman/pacman.c
@@ -0,0 +1,59 @@
+#include
+#include "pacman.h"
+#include "puffernet.h"
+
+void demo() {
+ // printf("OBSERVATIONS_COUNT: %d\n", OBSERVATIONS_COUNT);
+ Weights* weights = load_weights("resources/pacman/pacman_weights.bin");
+ int logit_sizes[1] = {4};
+ PufferNet* net = make_puffernet(weights, 1, OBSERVATIONS_COUNT, 256, 6, logit_sizes, 1);
+
+ PacmanEnv env = {
+ .randomize_starting_position = false,
+ .min_start_timeout = 0, // randomized ghost delay range
+ .max_start_timeout = 49,
+ .frightened_time = 35, // ghost frighten time
+ .max_mode_changes = 6,
+ .scatter_mode_length = 700,
+ .chase_mode_length = 70,
+ };
+ allocate(&env);
+ c_reset(&env);
+
+ Client* client = make_client(&env);
+ bool human_control = false;
+
+ while (!WindowShouldClose()) {
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) env.actions[0] = DOWN;
+ if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) env.actions[0] = UP;
+ if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) env.actions[0] = LEFT;
+ if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) env.actions[0] = RIGHT;
+ human_control = true;
+ } else {
+ human_control = false;
+ }
+
+ if (!human_control) {
+ forward_puffernet(net, env.observations, env.actions);
+ }
+
+ c_step(&env);
+ if (env.terminals[0] > 0.5f) {
+ c_reset(&env);
+ }
+
+ for (int i = 0; i < FRAMES; i++) {
+ c_render(&env);
+ }
+ }
+ free_puffernet(net);
+ free(weights);
+ free_allocated(&env);
+ close_client(client);
+}
+
+int main() {
+ demo();
+ return 0;
+}
diff --git a/pufferlib/ocean/pacman/pacman.h b/ocean/pacman/pacman.h
similarity index 98%
rename from pufferlib/ocean/pacman/pacman.h
rename to ocean/pacman/pacman.h
index b00c7002c1..5ca0bf8f5e 100644
--- a/pufferlib/ocean/pacman/pacman.h
+++ b/ocean/pacman/pacman.h
@@ -32,6 +32,7 @@ struct Log {
float episode_return;
float episode_length;
float score;
+ float perf;
float n;
};
@@ -118,9 +119,10 @@ typedef struct PacmanEnv {
int chase_mode_length;
float *observations;
- int *actions;
+ float *actions;
float *rewards;
- char *terminals;
+ float *terminals;
+ int num_agents;
Log log;
int step_count;
@@ -149,10 +151,12 @@ typedef struct PacmanEnv {
bool player_caught;
Ghost ghosts[NUM_GHOSTS];
+ unsigned int rng;
} PacmanEnv;
void add_log(PacmanEnv *env) {
env->log.score += env->score;
+ env->log.perf += (float)env->score / NUM_DOTS;
env->log.episode_return += env->score;
env->log.episode_length = env->step_count;
env->log.n++;
@@ -227,9 +231,9 @@ void init(PacmanEnv *env) {
void allocate(PacmanEnv *env) {
init(env);
env->observations = (float *)calloc(OBSERVATIONS_COUNT, sizeof(float));
- env->actions = (int *)calloc(1, sizeof(int));
+ env->actions = (float *)calloc(1, sizeof(float));
env->rewards = (float *)calloc(1, sizeof(float));
- env->terminals = (char *)calloc(1, sizeof(char));
+ env->terminals = (float *)calloc(1, sizeof(float));
}
void c_close(PacmanEnv *env) {
@@ -327,7 +331,7 @@ static inline void reset_round(PacmanEnv *env) {
}
if (env->randomize_starting_position) {
- int player_randomizer = rand() % NUM_DOTS;
+ int player_randomizer = rand_r(&env->rng) % NUM_DOTS;
env->player_pos = env->possible_spawn_pos[player_randomizer];
} else {
env->player_pos = env->player_spawn_pos;
@@ -424,7 +428,7 @@ static inline int ghost_direction(PacmanEnv *env, Ghost *ghost) {
}
if (ghost->frightened) {
- int random_index = rand() % option_count;
+ int random_index = rand_r(&env->rng) % option_count;
return directions[random_index];
}
diff --git a/ocean/pong/binding.c b/ocean/pong/binding.c
new file mode 100644
index 0000000000..93eae1dd4c
--- /dev/null
+++ b/ocean/pong/binding.c
@@ -0,0 +1,34 @@
+#include "pong.h"
+#define OBS_SIZE 8
+#define NUM_ATNS 1
+#define ACT_SIZES {3}
+#define OBS_TENSOR_T FloatTensor
+
+#define Env Pong
+#include "vecenv.h"
+
+void my_init(Env* env, Dict* kwargs) {
+ env->num_agents = 1;
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->paddle_width = dict_get(kwargs, "paddle_width")->value;
+ env->paddle_height = dict_get(kwargs, "paddle_height")->value;
+ env->ball_width = dict_get(kwargs, "ball_width")->value;
+ env->ball_height = dict_get(kwargs, "ball_height")->value;
+ env->paddle_speed = dict_get(kwargs, "paddle_speed")->value;
+ env->ball_initial_speed_x = dict_get(kwargs, "ball_initial_speed_x")->value;
+ env->ball_initial_speed_y = dict_get(kwargs, "ball_initial_speed_y")->value;
+ env->ball_max_speed_y = dict_get(kwargs, "ball_max_speed_y")->value;
+ env->ball_speed_y_increment = dict_get(kwargs, "ball_speed_y_increment")->value;
+ env->max_score = dict_get(kwargs, "max_score")->value;
+ env->frameskip = dict_get(kwargs, "frameskip")->value;
+ env->continuous = dict_get(kwargs, "continuous")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+}
diff --git a/ocean/pong/pong.c b/ocean/pong/pong.c
new file mode 100644
index 0000000000..698b93efb7
--- /dev/null
+++ b/ocean/pong/pong.c
@@ -0,0 +1,66 @@
+#include
+#include "pong.h"
+#include "puffernet.h"
+
+void demo() {
+ // Weight count: encoder(32x8=256) + decoder(4x32=128) + 1x mingru(3x32x32=3072) = 3456
+ Weights* weights = load_weights("resources/pong/pong_weights.bin");
+
+ int logit_sizes[1] = {3};
+ PufferNet* net = make_puffernet(weights, 1, 8, 32, 1, logit_sizes, 1);
+
+ Pong env = {
+ .width = 500,
+ .height = 640,
+ .paddle_width = 20,
+ .paddle_height = 70,
+ .ball_width = 32,
+ .ball_height = 32,
+ .paddle_speed = 8,
+ .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,
+ .continuous = 0,
+ };
+
+ allocate(&env);
+ c_reset(&env);
+ c_render(&env);
+ SetTargetFPS(60);
+ int frame = 0;
+ while (!WindowShouldClose()) {
+ // User can take control of the paddle
+ if (IsKeyDown(KEY_LEFT_SHIFT)) {
+ if(env.continuous) {
+ float move = GetMouseWheelMove();
+ float clamped_wheel = fmaxf(-1.0f, fminf(1.0f, move));
+ env.actions[0] = clamped_wheel;
+ printf("Mouse wheel move: %f\n", env.actions[0]);
+ } else {
+ env.actions[0] = 0.0;
+ if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) env.actions[0] = 1.0;
+ if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) env.actions[0] = 2.0;
+ }
+ } else if (frame == 0) {
+ forward_puffernet(net, env.observations, env.actions);
+ }
+
+ frame = (frame + 1) % 8;
+ c_step(&env);
+ // Reset frame counter on score so policy fires immediately next
+ // iteration, matching training's early-return-from-frameskip behavior
+ if (env.rewards[0] != 0.0f) frame = 0;
+ c_render(&env);
+ }
+ free_puffernet(net);
+ free(weights);
+ free_allocated(&env);
+ close_client(env.client);
+}
+
+int main() {
+ demo();
+}
diff --git a/pufferlib/ocean/pong/pong.h b/ocean/pong/pong.h
similarity index 97%
rename from pufferlib/ocean/pong/pong.h
rename to ocean/pong/pong.h
index f4e718c2c0..ae6e3eb5a6 100644
--- a/pufferlib/ocean/pong/pong.h
+++ b/ocean/pong/pong.h
@@ -20,7 +20,8 @@ struct Pong {
float* observations;
float* actions;
float* rewards;
- unsigned char* terminals;
+ float* terminals;
+ int num_agents;
float paddle_yl;
float paddle_yr;
float ball_x;
@@ -49,6 +50,7 @@ struct Pong {
int win;
int frameskip;
int continuous;
+ unsigned int rng;
};
void init(Pong* env) {
@@ -69,7 +71,7 @@ void allocate(Pong* env) {
env->observations = (float*)calloc(8, sizeof(float));
env->actions = (float*)calloc(1, sizeof(float));
env->rewards = (float*)calloc(1, sizeof(float));
- env->terminals = (unsigned char*)calloc(1, sizeof(unsigned char));
+ env->terminals = (float*)calloc(1, sizeof(float));
}
void free_allocated(Pong* env) {
@@ -108,7 +110,7 @@ void reset_round(Pong* env) {
env->ball_x = env->width / 5;
env->ball_y = env->height / 2 - env->ball_height / 2;
env->ball_vx = env->ball_initial_speed_x;
- env->ball_vy = (rand() % 2 - 1) * env->ball_initial_speed_y;
+ env->ball_vy = (rand_r(&env->rng) % 2 - 1) * env->ball_initial_speed_y;
env->tick = 0;
env->n_bounces = 0;
}
@@ -181,6 +183,7 @@ void c_step(Pong* env) {
return;
} else {
reset_round(env);
+ compute_observations(env);
return;
}
}
@@ -193,7 +196,6 @@ void c_step(Pong* env) {
// collision with paddle
env->ball_vx = -env->ball_vx;
env->n_bounces += 1;
- env->rewards[0] = 0.1; // agent bounced the ball
// ball speed change
env->ball_vy += env->ball_speed_y_increment * env->paddle_dir;
env->ball_vy = fminf(fmaxf(env->ball_vy, -env->ball_max_speed_y), env->ball_max_speed_y);
@@ -212,6 +214,7 @@ void c_step(Pong* env) {
return;
} else {
reset_round(env);
+ compute_observations(env);
return;
}
}
diff --git a/ocean/robocode/binding.c b/ocean/robocode/binding.c
new file mode 100644
index 0000000000..ee7028799c
--- /dev/null
+++ b/ocean/robocode/binding.c
@@ -0,0 +1,58 @@
+#include "robocode.h"
+#define OBS_SIZE 16
+#define NUM_ATNS 5
+#define ACT_SIZES {4, 9, 11, 11, 6}
+#define OBS_TENSOR_T FloatTensor
+
+#define MY_USES_PERM
+#define MY_USES_TAGS
+#define Env Robocode
+#include "vecenv.h"
+
+// Selfplay-pool routing: write per-slot pointers into the global vec buffers,
+// respecting agent_perm if set (selfplay re-routes logical slots into specific
+// physical rows so banks own contiguous ranges). Identity perm = adjacent
+// slot_base + s layout — matches single-agent / bot-mode runs.
+void my_setup_perm(StaticVec* vec, Env* env, int slot_base) {
+ for (int s = 0; s < env->num_agents; s++) {
+ int phys = vec->agent_perm ? vec->agent_perm[slot_base + s] : (slot_base + s);
+ env->obs_ptr[s] = (float*)vec->observations + (size_t)phys * OBS_SIZE;
+ env->action_ptr[s] = vec->actions + (size_t)phys * NUM_ATNS;
+ env->reward_ptr[s] = vec->rewards + phys;
+ env->terminal_ptr[s] = vec->terminals + phys;
+ }
+}
+
+void my_init(Env* env, Dict* kwargs) {
+ env->width = dict_get(kwargs, "width")->value;
+ env->height = dict_get(kwargs, "height")->value;
+ env->num_agents = dict_get(kwargs, "num_agents")->value;
+ env->num_bots = dict_get(kwargs, "num_bots")->value;
+ env->max_ticks = (int)dict_get(kwargs, "max_ticks")->value;
+ env->reward_damage = dict_get(kwargs, "reward_damage")->value;
+ env->reward_spot = dict_get(kwargs, "reward_spot")->value;
+ env->bot_policy = dict_get(kwargs, "bot_policy")->value;
+ init(env);
+}
+
+void my_log(Log* log, Dict* out) {
+ dict_set(out, "perf", log->perf);
+ dict_set(out, "score", log->score);
+ dict_set(out, "damage_received", log->damage_received);
+ dict_set(out, "episode_return", log->episode_return);
+ dict_set(out, "episode_length", log->episode_length);
+ // Historical-pool stats. selfplay.py reads hist_score_bank_ /
+ // hist_n_bank_ per bank to drive swap decisions. Legacy aggregate
+ // hist_score / hist_n sum across all banks for backward-compat dashboards.
+ dict_set(out, "hist_score", log->hist_score);
+ dict_set(out, "hist_n", log->hist_n);
+ dict_set(out, "hist_score_bank_0", log->hist_score_bank[0]);
+ dict_set(out, "hist_score_bank_1", log->hist_score_bank[1]);
+ dict_set(out, "hist_n_bank_0", log->hist_n_bank[0]);
+ dict_set(out, "hist_n_bank_1", log->hist_n_bank[1]);
+ // Per-slot scores — match() reads slot_0_score / slot_1_score as A/B win rates.
+ dict_set(out, "slot_0_score", log->slot_0_score);
+ dict_set(out, "slot_1_score", log->slot_1_score);
+ dict_set(out, "draw_rate", log->draw_rate);
+ dict_set(out, "n", log->n);
+}
diff --git a/ocean/robocode/bots.h b/ocean/robocode/bots.h
new file mode 100644
index 0000000000..c996f3b2e0
--- /dev/null
+++ b/ocean/robocode/bots.h
@@ -0,0 +1,380 @@
+// CPU bot policies for robocode. Included by robocode.h AFTER the Robocode /
+// Robot / Bullet structs and the move/turn/fire helpers are defined.
+//
+// Information model is faithful to classic Robocode:
+// * Each tick the bot "scans" the target and reads: x, y, heading, v, energy.
+// It does NOT read gun_heading or bullet state directly.
+// * Fire events are inferred from target.energy drops in (0, 3].
+// * Training samples for the kNN danger model are only added when an enemy
+// bullet actually hits the bot — c_step calls bot_on_hit_by_bullet() and
+// we get (bullet.heading, bullet.power) from the impact event, matching
+// Robocode's onHitByBullet event.
+//
+// Faithful adaptation of BeepBoop's wave surfer
+// (https://robowiki.net/wiki/BeepBoop/Understanding_BeepBoop) but simplified:
+// * 5 hand-picked features instead of beep-boop's learned embedding.
+// * No bullet shielding, no flattening, no virtual gun-heat waves.
+// * 3-candidate direction search (-1, 0, +1) instead of full path simulation.
+
+#ifndef ROBOCODE_BOTS_H
+#define ROBOCODE_BOTS_H
+
+#include
+#include
+
+typedef enum {
+ BOT_STATIONARY = 0,
+ BOT_MINIMAL = 1,
+ BOT_SURFER = 2,
+ BOT_WAVE_SURFER = 3,
+} BotPolicy;
+
+#define WS_NUM_WAVES 8
+#define WS_KNN_CAP 256
+#define WS_KNN_K 5
+#define WS_NUM_FEATS 5
+#define WS_SCAN_WIDTH_DEG 5.625f // beep-boop's SCAN_WIDTH = π/32 in degrees
+#define WS_MAX_REVERSALS 3 // reversals before falling back to full sweep
+
+// Scale weights — bigger scale => feature contributes more to kNN distance.
+// Same trick as beep-boop's embedding (no normalization to [-1,1]).
+static const float WS_FEAT_W[WS_NUM_FEATS] = {
+ 0.01f, // distance (range ~0..1200 -> ~0..12)
+ 1.0f, // signed lateral velocity / 8 (~-1..1)
+ 0.5f, // signed advancing velocity / 8 (~-0.5..0.5)
+ 0.04f, // ticks since direction change
+ 2.0f, // wall proximity (0=at wall, 1=center)
+};
+
+typedef struct {
+ float ox, oy; // wave origin (target pos at fire time)
+ float head_on; // angle from origin to bot at fire time (deg)
+ float speed;
+ int fire_tick;
+ int lat_sign; // sign of bot's lateral velocity at fire time
+ float feats[WS_NUM_FEATS];
+ int active;
+} WSWave;
+
+struct BotMem {
+ int tick;
+ int orbit_dir; // -1, 0, +1
+ int last_dir_change_tick;
+ // Last-scanned target snapshot. Updated only when scan_area() returns the
+ // target this tick. Until first scan, last_scan_tick == 0 and decisions
+ // are skipped.
+ float last_x, last_y, last_heading, last_v;
+ int last_energy_seen;
+ int last_scan_tick;
+ int radar_dir; // ±1, the direction the radar is currently sweeping
+ int radar_reversals; // beep-boop's lost-lock reversal counter
+ int wave_head;
+ WSWave waves[WS_NUM_WAVES];
+ int knn_n;
+ int knn_head;
+ float knn_feats[WS_KNN_CAP][WS_NUM_FEATS];
+ float knn_gf[WS_KNN_CAP];
+};
+
+// ---- Lifetime ---------------------------------------------------------------
+static inline void bot_mems_alloc(Robocode* env) {
+ if (env->num_bots <= 0) { env->bot_mems = NULL; return; }
+ env->bot_mems = (BotMem*)calloc(env->num_bots, sizeof(BotMem));
+ for (int i = 0; i < env->num_bots; i++) env->bot_mems[i].orbit_dir = 1;
+}
+static inline void bot_mems_free(Robocode* env) {
+ if (env->bot_mems) free(env->bot_mems);
+}
+// Called from c_reset on episode boundaries. Clears scan/wave/radar state but
+// preserves the kNN model — long-term learning across episodes is the point.
+static inline void bot_mems_episode_reset(Robocode* env) {
+ if (env->bot_mems == NULL) return;
+ for (int i = 0; i < env->num_bots; i++) {
+ BotMem* m = &env->bot_mems[i];
+ m->last_x = m->last_y = m->last_heading = m->last_v = 0.0f;
+ m->last_energy_seen = 0;
+ m->last_scan_tick = 0;
+ m->radar_dir = 0;
+ m->radar_reversals = 0;
+ for (int wi = 0; wi < WS_NUM_WAVES; wi++) m->waves[wi].active = 0;
+ }
+}
+
+// ---- Feature extraction -----------------------------------------------------
+// tgt_x/tgt_y come from the bot's cached scan (BotMem.last_x / last_y), not
+// the live target struct — matches Robocode's information model.
+static void ws_features(Robot* bot, float tgt_x, float tgt_y, Robocode* env,
+ int tick, int last_change, float out[WS_NUM_FEATS]) {
+ float dx = bot->x - tgt_x, dy = bot->y - tgt_y;
+ float dist = sqrtf(dx*dx + dy*dy);
+ float ux = (dist > 1e-6f) ? dx/dist : 1.0f;
+ float uy = (dist > 1e-6f) ? dy/dist : 0.0f;
+ float bvx = cos_deg(bot->heading) * bot->v;
+ float bvy = sin_deg(bot->heading) * bot->v;
+ float adv_v = bvx*ux + bvy*uy; // along bot->target axis
+ float lat_v = -bvx*uy + bvy*ux; // perpendicular
+ float wall_min = fminf(fminf(bot->x, env->width - bot->x),
+ fminf(bot->y, env->height - bot->y));
+ float wall_half = fmaxf(fminf(env->width, env->height) * 0.5f, 1.0f);
+ out[0] = dist;
+ out[1] = lat_v / 8.0f;
+ out[2] = adv_v / 8.0f;
+ out[3] = (float)(tick - last_change);
+ out[4] = wall_min / wall_half;
+}
+
+// ---- kNN density estimate ---------------------------------------------------
+// danger(features, gf) = sum over top-K neighbors of w_i * N(gf - gf_i, sigma)
+// w_i = 1 / (1 + weighted_d2_i)
+// Higher value => enemy's aim more likely lands at `gf` => more dangerous.
+static float ws_danger(BotMem* m, const float feats[WS_NUM_FEATS], float gf) {
+ if (m->knn_n == 0) return 0.0f;
+ float best_d[WS_KNN_K];
+ int best_i[WS_KNN_K];
+ for (int k = 0; k < WS_KNN_K; k++) { best_d[k] = 1e18f; best_i[k] = -1; }
+ for (int n = 0; n < m->knn_n; n++) {
+ float d2 = 0.0f;
+ for (int f = 0; f < WS_NUM_FEATS; f++) {
+ float diff = (feats[f] - m->knn_feats[n][f]) * WS_FEAT_W[f];
+ d2 += diff * diff;
+ }
+ for (int k = 0; k < WS_KNN_K; k++) {
+ if (d2 < best_d[k]) {
+ for (int s = WS_KNN_K - 1; s > k; s--) {
+ best_d[s] = best_d[s-1]; best_i[s] = best_i[s-1];
+ }
+ best_d[k] = d2; best_i[k] = n;
+ break;
+ }
+ }
+ }
+ const float sigma = 0.15f;
+ const float two_s2 = 2.0f * sigma * sigma;
+ float danger = 0.0f;
+ for (int k = 0; k < WS_KNN_K; k++) {
+ if (best_i[k] < 0) break;
+ float w = 1.0f / (1.0f + best_d[k]);
+ float dgf = gf - m->knn_gf[best_i[k]];
+ danger += w * expf(-(dgf*dgf) / two_s2);
+ }
+ return danger;
+}
+
+static inline void ws_add_sample(BotMem* m, const float feats[WS_NUM_FEATS], float gf) {
+ int slot = m->knn_head;
+ memcpy(m->knn_feats[slot], feats, WS_NUM_FEATS * sizeof(float));
+ m->knn_gf[slot] = gf;
+ m->knn_head = (m->knn_head + 1) % WS_KNN_CAP;
+ if (m->knn_n < WS_KNN_CAP) m->knn_n++;
+}
+
+// ---- onHitByBullet ----------------------------------------------------------
+// Called from c_step when an enemy bullet hits a bot. The bullet's heading and
+// power are provided — same info Robocode's onHitByBullet event delivers.
+// We find the matching in-flight wave (by speed + age) and add a training
+// sample to the kNN.
+static void bot_on_hit_by_bullet(Robocode* env, int bot_idx,
+ float bullet_heading, float bullet_power) {
+ if (env->bot_policy != BOT_WAVE_SURFER) return;
+ if (env->bot_mems == NULL) return;
+ BotMem* m = &env->bot_mems[bot_idx - env->num_agents];
+ float speed = 20.0f - 3.0f * bullet_power;
+ WSWave* best = NULL;
+ int best_age = -1;
+ for (int wi = 0; wi < WS_NUM_WAVES; wi++) {
+ WSWave* w = &m->waves[wi];
+ if (!w->active) continue;
+ if (fabsf(w->speed - speed) > 0.5f) continue;
+ int age = m->tick - w->fire_tick;
+ if (age > best_age) { best_age = age; best = w; }
+ }
+ if (best == NULL) return;
+ float gf_raw = bullet_heading - best->head_on;
+ if (gf_raw > 180.0f) gf_raw -= 360.0f;
+ else if (gf_raw < -180.0f) gf_raw += 360.0f;
+ float mea_rad = asinf(fminf(8.0f / best->speed, 1.0f));
+ float mea_deg = fmaxf(mea_rad * (180.0f / 3.14159265358979f), 0.1f);
+ float gf = (gf_raw / mea_deg) * best->lat_sign;
+ if (gf > 1.5f) gf = 1.5f;
+ if (gf < -1.5f) gf = -1.5f;
+ ws_add_sample(m, best->feats, gf);
+ best->active = 0;
+}
+
+// ---- Main entry -------------------------------------------------------------
+static void bot_step(Robocode* env, int bot_idx) {
+ Robot* bot = &env->robots[bot_idx];
+ // Dead bots are skipped entirely; disabled bots (energy=0) are frozen
+ // but the env still ticks. Same as agent rule in c_step.
+ if (bot->energy < 0) return;
+ if (bot->energy == 0) { bot->v = 0; return; }
+ if (bot->gun_heat > 0) bot->gun_heat -= 0.1f;
+ if (env->bot_policy == BOT_STATIONARY) return;
+
+ BotMem* m = &env->bot_mems[bot_idx - env->num_agents];
+ m->tick++;
+ if (m->orbit_dir == 0) m->orbit_dir = 1;
+
+ // Pick a target index. In 1v1 there's only one agent; for melee we use
+ // true position to lock on — equivalent to "the only one we know about".
+ int t = -1; float best = 1e18f;
+ for (int j = 0; j < env->num_agents; j++) {
+ Robot* a = &env->robots[j];
+ if (a->energy < 0) continue; // dead only — disabled is still a valid target
+ float dx = a->x - bot->x, dy = a->y - bot->y;
+ float d2 = dx*dx + dy*dy;
+ if (d2 < best) { best = d2; t = j; }
+ }
+ if (t < 0) return;
+ const float R2D = 180.0f / 3.14159265358979f;
+
+ // ---- Radar control (faithful to beep-boop's Scanner) ------------------
+ // Cold-start direction: aim toward battlefield center, max rate.
+ if (m->radar_dir == 0) {
+ float cx = env->width * 0.5f, cy = env->height * 0.5f;
+ float center_bear = atan2f(cy - bot->y, cx - bot->x) * R2D;
+ if (center_bear < 0) center_bear += 360.0f;
+ float diff = center_bear - bot->radar_heading;
+ if (diff > 180) diff -= 360; else if (diff < -180) diff += 360;
+ m->radar_dir = (diff >= 0) ? 1 : -1;
+ }
+ float radar_delta;
+ bool just_scanned = (m->last_scan_tick != 0 && m->last_scan_tick == m->tick - 1);
+ if (just_scanned) {
+ // scan(): aim radar at last seen position, then push past by SCAN_WIDTH
+ // so next tick's wedge sweeps back across the target.
+ float dxr = m->last_x - bot->x, dyr = m->last_y - bot->y;
+ float bear = atan2f(dyr, dxr) * R2D;
+ if (bear < 0) bear += 360.0f;
+ float diff = bear - bot->radar_heading;
+ if (diff > 180) diff -= 360; else if (diff < -180) diff += 360;
+ float overshoot = (diff >= 0) ? WS_SCAN_WIDTH_DEG : -WS_SCAN_WIDTH_DEG;
+ radar_delta = diff + overshoot;
+ m->radar_dir = (radar_delta >= 0) ? 1 : -1;
+ m->radar_reversals = 0;
+ } else if (m->last_scan_tick != 0 && m->radar_reversals < WS_MAX_REVERSALS) {
+ // search(): if the bearing-to-last-seen flipped sign vs our current
+ // sweep direction, we overshot — reverse and bump the counter.
+ float dxr = m->last_x - bot->x, dyr = m->last_y - bot->y;
+ float bear = atan2f(dyr, dxr) * R2D;
+ if (bear < 0) bear += 360.0f;
+ float diff = bear - bot->radar_heading;
+ if (diff > 180) diff -= 360; else if (diff < -180) diff += 360;
+ int new_dir = (diff >= 0) ? 1 : -1;
+ if (new_dir != m->radar_dir) {
+ m->radar_dir = new_dir;
+ m->radar_reversals++;
+ }
+ radar_delta = m->radar_dir * 45.0f;
+ } else {
+ // Cold start or out of reversals: just keep spinning at max rate.
+ radar_delta = m->radar_dir * 45.0f;
+ }
+ bot->radar_heading_prev = bot->radar_heading;
+ turn(&bot->radar_heading, radar_delta, 45.0f, 0);
+
+ // ---- Scan: only refresh cache if the radar wedge actually crossed t ---
+ int scanned = scan_area(env, bot);
+ if (scanned == t) {
+ Robot* tgt = &env->robots[t];
+ // Detect target fire from energy drop BEFORE overwriting last_energy_seen.
+ int drop = m->last_scan_tick > 0 ? (m->last_energy_seen - tgt->energy) : 0;
+ bool fired = (drop > 0 && drop <= 3);
+ if (env->bot_policy == BOT_SURFER && fired) {
+ m->orbit_dir = -m->orbit_dir;
+ m->last_dir_change_tick = m->tick;
+ } else if (env->bot_policy == BOT_WAVE_SURFER && fired) {
+ // Wave origin = target's PREVIOUS scanned position (where they
+ // were the tick before they fired). speed inferred from drop.
+ WSWave* w = &m->waves[m->wave_head];
+ m->wave_head = (m->wave_head + 1) % WS_NUM_WAVES;
+ w->ox = m->last_x; w->oy = m->last_y;
+ w->speed = 20.0f - 3.0f * drop;
+ w->fire_tick = m->tick;
+ float dwx = bot->x - w->ox, dwy = bot->y - w->oy;
+ w->head_on = atan2f(dwy, dwx) * R2D;
+ ws_features(bot, w->ox, w->oy, env, m->tick, m->last_dir_change_tick, w->feats);
+ w->lat_sign = (w->feats[1] >= 0.0f) ? 1 : -1;
+ w->active = 1;
+ if (m->knn_n < WS_KNN_K) { // bootstrap while kNN is sparse
+ m->orbit_dir = -m->orbit_dir;
+ m->last_dir_change_tick = m->tick;
+ }
+ }
+ // Refresh cache.
+ m->last_x = tgt->x; m->last_y = tgt->y;
+ m->last_heading = tgt->heading; m->last_v = tgt->v;
+ m->last_energy_seen = tgt->energy;
+ m->last_scan_tick = m->tick;
+ }
+ if (m->last_scan_tick == 0) return; // still hunting for first contact
+
+ // ---- Wave-surfer: expire missed waves, then choose orbit direction ---
+ if (env->bot_policy == BOT_WAVE_SURFER) {
+ for (int wi = 0; wi < WS_NUM_WAVES; wi++) {
+ WSWave* w = &m->waves[wi];
+ if (!w->active) continue;
+ float radius = (m->tick - w->fire_tick) * w->speed;
+ float ddx = bot->x - w->ox, ddy = bot->y - w->oy;
+ float dist_now = sqrtf(ddx*ddx + ddy*ddy);
+ if (radius >= dist_now + 32.0f) w->active = 0;
+ else if (m->tick - w->fire_tick > 400) w->active = 0;
+ }
+ if (m->knn_n > 0) {
+ float feats[WS_NUM_FEATS];
+ ws_features(bot, m->last_x, m->last_y, env, m->tick, m->last_dir_change_tick, feats);
+ int cands[3] = {-1, 0, +1};
+ float danger[3] = {0, 0, 0};
+ for (int wi = 0; wi < WS_NUM_WAVES; wi++) {
+ WSWave* w = &m->waves[wi];
+ if (!w->active) continue;
+ float ddx = bot->x - w->ox, ddy = bot->y - w->oy;
+ float dist_now = fmaxf(sqrtf(ddx*ddx + ddy*ddy), 1.0f);
+ float radius = (m->tick - w->fire_tick) * w->speed;
+ float tti = (dist_now - radius) / w->speed;
+ if (tti <= 0) continue;
+ float mea_rad = asinf(fminf(8.0f / w->speed, 1.0f));
+ for (int c = 0; c < 3; c++) {
+ float lat_v = cands[c] * 8.0f;
+ float dtheta = lat_v * tti / dist_now;
+ float gf = (dtheta / mea_rad) * w->lat_sign;
+ danger[c] += ws_danger(m, feats, gf);
+ }
+ }
+ int best_c = 0;
+ for (int c = 1; c < 3; c++) if (danger[c] < danger[best_c]) best_c = c;
+ int new_dir = cands[best_c];
+ if (new_dir == 0) new_dir = m->orbit_dir;
+ if (new_dir != m->orbit_dir) {
+ m->orbit_dir = new_dir;
+ m->last_dir_change_tick = m->tick;
+ }
+ }
+ }
+
+ // ---- Shared aim/move/fire (linear-lead from cached scan values) ------
+ float dx = m->last_x - bot->x, dy = m->last_y - bot->y;
+ float dt = sqrtf(dx*dx + dy*dy) / 17.0f;
+ float tvx = cos_deg(m->last_heading) * m->last_v;
+ float tvy = sin_deg(m->last_heading) * m->last_v;
+ float aim = atan2f(dy + tvy*dt, dx + tvx*dt) * R2D;
+ float bear = atan2f(dy, dx) * R2D;
+ if (aim < 0) aim += 360.0f;
+ if (bear < 0) bear += 360.0f;
+ float orbit = bear + 90.0f * m->orbit_dir;
+ if (orbit >= 360.0f) orbit -= 360.0f;
+ else if (orbit < 0.0f) orbit += 360.0f;
+
+ float gun_d = aim - bot->gun_heading;
+ float body_d = orbit - bot->heading;
+ if (gun_d > 180) gun_d -= 360; else if (gun_d < -180) gun_d += 360;
+ if (body_d > 180) body_d -= 360; else if (body_d < -180) body_d += 360;
+ float body_turned = turn(&bot->heading, body_d, 10.0f - 0.75f*fabsf(bot->v), 0);
+ turn(&bot->gun_heading, gun_d, 20.0f, body_turned);
+ move(env, bot, 1.0f);
+ bot->x = fmaxf(16.0f, fminf(bot->x, env->width - 16.0f));
+ bot->y = fmaxf(16.0f, fminf(bot->y, env->height - 16.0f));
+ if (fabsf(gun_d) < 3.0f && bot->gun_heat <= 0.0f) fire(env, bot, bot_idx, 1.0f);
+}
+
+#endif // ROBOCODE_BOTS_H
diff --git a/ocean/robocode/robocode.c b/ocean/robocode/robocode.c
new file mode 100644
index 0000000000..0508c734dd
--- /dev/null
+++ b/ocean/robocode/robocode.c
@@ -0,0 +1,80 @@
+#include "robocode.h"
+#include
+
+void performance_test() {
+ long test_time = 10;
+ Robocode env = {
+ .num_agents = 2,
+ .num_bots = 0,
+ .width = 800,
+ .height = 600,
+ .reward_damage = 0.01f,
+ .reward_spot = 0.001f,
+ .bot_policy = 3, // BOT_WAVE_SURFER
+ .max_ticks = 3000,
+ .rng = 42,
+ };
+ allocate_env(&env);
+ c_reset(&env);
+
+ long start = time(NULL);
+ int i = 0;
+ while (time(NULL) - start < test_time) {
+ env.actions[0] = rand_r(&env.rng) % 4;
+ env.actions[1] = rand_r(&env.rng) % 9;
+ env.actions[2] = rand_r(&env.rng) % 11;
+ env.actions[3] = rand_r(&env.rng) % 11;
+ env.actions[4] = (rand_r(&env.rng) % 6) > 4 ? 1.0f : 0.0f;
+ c_step(&env);
+ i++;
+ }
+ long end = time(NULL);
+ printf("SPS: %ld\n", (long)i*env.num_agents / (end - start));
+ c_close(&env);
+}
+
+void demo(void) {
+ Robocode env = {
+ .num_agents = 1,
+ .num_bots = 1,
+ .reward_damage = 0.01,
+ .width = 800,
+ .height = 600,
+ .max_ticks = 512,
+ };
+ allocate_env(&env);
+ c_reset(&env);
+
+ env.client = make_client(&env);
+ c_render(&env);
+
+ while (!WindowShouldClose()) {
+ env.actions[0] = 2;
+ env.actions[1] = 4;
+ env.actions[2] = 5;
+ env.actions[3] = 5;
+ env.actions[4] = 0;
+
+ if (IsKeyPressed(KEY_ESCAPE)) break;
+ if (IsKeyDown(KEY_W)) env.actions[0] = 3.0f;
+ if (IsKeyDown(KEY_S)) env.actions[0] = 1.0f;
+ if (IsKeyDown(KEY_A)) env.actions[1] = 3.0f;
+ if (IsKeyDown(KEY_D)) env.actions[1] = 5.0f;
+ if (IsKeyDown(KEY_Q)) env.actions[2] = 4.0f;
+ if (IsKeyDown(KEY_E)) env.actions[2] = 6.0f;
+ if (IsKeyDown(KEY_LEFT)) env.actions[3] = 0.0f;
+ if (IsKeyDown(KEY_RIGHT)) env.actions[3] = 8.0f;
+ if (IsKeyDown(KEY_SPACE)) env.actions[4] = 1.0f;
+
+ c_step(&env);
+ c_render(&env);
+ }
+ c_close(&env);
+ CloseWindow();
+}
+
+int main() {
+ demo();
+ //performance_test();
+ return 0;
+}
diff --git a/ocean/robocode/robocode.h b/ocean/robocode/robocode.h
new file mode 100644
index 0000000000..33d45c7857
--- /dev/null
+++ b/ocean/robocode/robocode.h
@@ -0,0 +1,753 @@
+#include