diff --git a/config/craftax.ini b/config/craftax.ini index b5db458141..7bef9ad07c 100644 --- a/config/craftax.ini +++ b/config/craftax.ini @@ -2,28 +2,37 @@ env_name = craftax [vec] -total_agents = 16384 -num_buffers = 16 -num_threads = 16 +total_agents = 8192 +num_buffers = 4 +num_threads = 8 [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 +action_mask = 1 + +[policy] +hidden_size = 1024 +num_layers = 3.95689535 [train] -total_timesteps = 20_000_000_000 -learning_rate = 0.008 -ent_coef = 0.02 -gamma = 0.997 -gae_lambda = 0.95 -horizon = 128 -minibatch_size = 32768 +gpus = 1 +total_timesteps = 200_000_000 +learning_rate = 0.00212870375 +anneal_lr = 1 +min_lr_ratio = 0.0 +gamma = 0.980109215 +gae_lambda = 0.741049647 +replay_ratio = 1.59645867 +clip_coef = 0.361835271 +vf_coef = 3.47198796 +vf_clip_coef = 3.29685092 +max_grad_norm = 1.08277392 +ent_coef = 8.97720674e-05 +anneal_ent_coef = 0 +min_ent_coef_ratio = 0.1 +momentum = 0.965376794 +minibatch_size = 4096 +horizon = 32 -[policy] -hidden_size = 32 -num_layers = 3 +[sweep] +max_suggestion_cost = 7200 diff --git a/config/craftax_clean.ini b/config/craftax_clean.ini deleted file mode 100644 index f01ae7ebdf..0000000000 --- a/config/craftax_clean.ini +++ /dev/null @@ -1,38 +0,0 @@ -[base] -env_name = craftax_clean - -[vec] -total_agents = 8192 -num_buffers = 4 -num_threads = 8 - -[env] -reset_pool_size = 1024 -action_mask = 1 - -[policy] -hidden_size = 1024 -num_layers = 3.95689535 - -[train] -gpus = 1 -total_timesteps = 200_000_000 -learning_rate = 0.00212870375 -anneal_lr = 1 -min_lr_ratio = 0.0 -gamma = 0.980109215 -gae_lambda = 0.741049647 -replay_ratio = 1.59645867 -clip_coef = 0.361835271 -vf_coef = 3.47198796 -vf_clip_coef = 3.29685092 -max_grad_norm = 1.08277392 -ent_coef = 8.97720674e-05 -anneal_ent_coef = 0 -min_ent_coef_ratio = 0.1 -momentum = 0.965376794 -minibatch_size = 4096 -horizon = 32 - -[sweep] -max_suggestion_cost = 7200 diff --git a/craftax_clean b/craftax_clean deleted file mode 100755 index 5bc0dbeed7..0000000000 Binary files a/craftax_clean and /dev/null differ diff --git a/ocean/craftax/PORT_NOTES.md b/ocean/craftax/PORT_NOTES.md deleted file mode 100644 index 4542b1dcb8..0000000000 --- a/ocean/craftax/PORT_NOTES.md +++ /dev/null @@ -1,543 +0,0 @@ -# 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_clean/constants.h b/ocean/craftax/constants.h similarity index 98% rename from ocean/craftax_clean/constants.h rename to ocean/craftax/constants.h index 47ffa4a36c..7b9697f506 100644 --- a/ocean/craftax_clean/constants.h +++ b/ocean/craftax/constants.h @@ -29,6 +29,7 @@ // Environment parameters #define DEFAULT_MAX_TIMESTEPS 100000 #define DAY_LENGTH 300 +#define VISIBLE_LIGHT_THRESHOLD 12 #define MAX_ATTRIBUTE 5 #define MOB_DESPAWN_DISTANCE 14 #define MONSTERS_KILLED_TO_CLEAR_LEVEL 8 @@ -447,11 +448,13 @@ static const DungeonConfig DUNGEON_LEVEL_CONFIGS[3] = { }; -// Rendering parameters. tiles.png is 16x16 RGBA tiles, row-major, 16 columns: +// Rendering parameters. textures.png is 16x16 RGBA tiles, row-major, 16 columns: // [0..36] blocks [37..41] player [42..46] items [47..49] generic mobs // [50..53] arrows [54..61] armour [62..70] tools [71..76] potions // [77..79] HUD [80..87] melee [88..90] passive [91..98] ranged // [99..102] projectiles +// [103..104] sword enchant [105..106] arrow enchant +// [107..110] armour fire overlay [111..114] armour ice overlay #define TEX_TILE_PX 16 #define TEX_SHEET_COLS 16 #define TEX_SCALE 3 diff --git a/ocean/craftax/craftax.c b/ocean/craftax/craftax.c index ca1dccf476..f6b37dbb1d 100644 --- a/ocean/craftax/craftax.c +++ b/ocean/craftax/craftax.c @@ -1,40 +1,30 @@ -// Standalone viewer for Craftax (random-action policy). -// -// Build: -// ./build.sh craftax --cpu # optimized -// ./build.sh craftax --debug # 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 - -int main(int argc, char** argv) { - uint64_t seed = (argc > 1) ? strtoull(argv[1], NULL, 10) : (uint64_t)time(NULL); +int main(void) { 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.agents[0].observations = calloc(CRAFTAX_OBS_SIZE, sizeof(float)); - env.agents[0].actions = calloc(1, sizeof(float)); - env.agents[0].rewards = calloc(1, sizeof(float)); - env.agents[0].terminals = calloc(1, sizeof(float)); - - c_init(&env); + env.rng = 1; + env.seed = 1; + env.use_action_mask = 1; + + env.agents[0].observations = (obs_t*)calloc(OBS_SIZE, sizeof(obs_t)); + env.agents[0].actions = (float*)calloc(1, sizeof(float)); + env.agents[0].rewards = (float*)calloc(1, sizeof(float)); + env.agents[0].terminals = (float*)calloc(1, sizeof(float)); + env.agents[0].action_mask = (unsigned char*)calloc(ATN_DIM, 1); puf_reset(&env); + env.agents[0].actions[0] = -1.0f; + puf_render(&env); while (!WindowShouldClose()) { + int action = key_to_action(); + if (action < 0) { + env.agents[0].actions[0] = -1.0f; + puf_render(&env); + continue; + } + env.agents[0].actions[0] = (float)action; puf_step(&env); puf_render(&env); } @@ -44,5 +34,6 @@ int main(int argc, char** argv) { free(env.agents[0].actions); free(env.agents[0].rewards); free(env.agents[0].terminals); + free(env.agents[0].action_mask); return 0; } diff --git a/ocean/craftax/craftax.h b/ocean/craftax/craftax.h index 6f20e13b6b..28011fb391 100644 --- a/ocean/craftax/craftax.h +++ b/ocean/craftax/craftax.h @@ -1,591 +1,146 @@ -// Full native Craftax environment for PufferLib Ocean. - +// Full native Craftax port. #pragma once #include #include #include #include +#include -#include "worldgen.h" +#include "constants.h" #include "raylib.h" typedef float obs_t; #include "pufferenv.h" - -// Train/eval builds need full step implementations (forward-declared below). -#ifndef CRAFTAX_ENABLE_ENV_IMPL -#define CRAFTAX_ENABLE_ENV_IMPL -#endif #include #include -#include - -// ============================================================ -// Optional step profiling (compile with -DCRAFTAX_PROFILE) -// ============================================================ -#ifdef CRAFTAX_PROFILE - -#define CRAFTAX_NUM_PROFILE_ZONES 18 - -struct Log { - 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 ACT_SIZES {CRAFTAX_NUM_ACTIONS} -#define OBS_SIZE CRAFTAX_OBS_SIZE +#define ACT_SIZES {ATN_DIM} #define NUM_ATNS 1 - +#ifdef PUFFERCPU_EVAL_MAIN +#define PUF_CRAFTAX_NET 1 +#endif +#ifdef PUF_CRAFTAX_NET +#include "craftax_net.h" +#endif +#define MY_VEC_INIT +#define MY_VEC_CLOSE typedef Env Craftax; -#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; +// Data structures +typedef struct { + int wood; + int stone; + int coal; + int iron; + int diamond; + int sapling; + int pickaxe; + int sword; + int bow; + int arrows; + int armour[4]; + int torches; + int ruby; + int sapphire; + int potions[NUM_POTIONS]; + int books; +} Inventory; + +typedef struct { + int position[3][2]; + float health[3]; + bool mask[3]; + int attack_cooldown[3]; + int type_id[3]; +} Mobs; + +typedef struct { + uint8_t map[NUM_LEVELS][MAP_SIZE][MAP_SIZE]; + uint8_t item_map[NUM_LEVELS][MAP_SIZE][MAP_SIZE]; + uint8_t light_map[NUM_LEVELS][MAP_SIZE][MAP_SIZE]; + uint64_t mob_bits[NUM_LEVELS][MAP_SIZE]; + uint64_t spawn_land[NUM_LEVELS][MAP_SIZE]; + uint64_t spawn_grave[NUM_LEVELS][MAP_SIZE]; + uint64_t spawn_water[NUM_LEVELS][MAP_SIZE]; + int down_ladders[NUM_LEVELS][2]; + int up_ladders[NUM_LEVELS][2]; + int chests_opened[NUM_LEVELS]; + int monsters_killed[NUM_LEVELS]; + + int player_position[2]; + int player_level; + int player_direction; + + // Intrinsics 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; - + int player_food; + int player_drink; + int player_energy; + int player_mana; + int is_sleeping; + int is_resting; + + // Second order intrinsics 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; - + // Attributes + int player_xp; + int player_dexterity; + int player_strength; + int player_intelligence; + + Inventory inventory; + + Mobs melee_mobs[NUM_LEVELS]; + Mobs passive_mobs[NUM_LEVELS]; + Mobs ranged_mobs[NUM_LEVELS]; + Mobs mob_projectiles[NUM_LEVELS]; + + int mob_projectile_dirs[NUM_LEVELS][MAX_MOB_PROJECTILES][2]; + Mobs player_projectiles[NUM_LEVELS]; + int player_projectile_directions[NUM_LEVELS][MAX_PLAYER_PROJECTILES][2]; + int growing_plants_pos[MAX_GROWING_PLANTS][2]; + int growing_plants_age[MAX_GROWING_PLANTS]; + int growing_plants_mask[MAX_GROWING_PLANTS]; + int potion_mapping[NUM_POTIONS]; + int learned_spells[2]; + int sword_enchantment; + int bow_enchantment; + int armour_enchantments[4]; + int boss_progress; + int boss_timestep_to_spawn_this_round; float light_level; - bool achievements[CRAFTAX_NUM_ACHIEVEMENTS]; + int achievements[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 + int timestep; +} State; struct Log { float perf; + float achievement_rate; float score; float episode_return; float episode_length; - float achievements[CRAFTAX_NUM_ACHIEVEMENTS]; + float floors[NUM_LEVELS]; + float achievements[NUM_ACHIEVEMENTS]; float n; }; -typedef struct Client { - int unused; +// Rendering +typedef struct { + int cell_size; + int screen_width; + int screen_height; + bool window_ready; } Client; +// Random number generation +typedef uint64_t Rng; + struct Env { Client* client; Log log; @@ -593,692 +148,3267 @@ struct Env { int num_agents; int tag; int boundary_reached; - + State state; + int timestep; 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]; + Rng env_rng; float episode_return_accum; - int32_t episode_length_accum; - // EVAL_MAIN (web/--cpu) calls puf_step once per rAF. Craftax is a - // grid game: hold the world for TICK_FRAMES so len/timestep match - // visible actions instead of 60Hz no-op crafts. - int tick_frames_left; + int episode_length_accum; + int max_floor_accum; + int achievements[NUM_ACHIEVEMENTS]; + State* reset_pool; + int reset_pool_size; + int use_action_mask; + float predicted_value; }; -#ifdef PUFFERCPU_EVAL_MAIN -#define CRAFTAX_TICK_FRAMES 6 -#define PUF_EVAL_SHOULD_FORWARD -#endif - -// ============================================================ -// 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 uint32_t rng_rotl32(uint32_t x, uint32_t k) { + return (uint32_t)((x << k) | (x >> (32u - k))); +} -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; +static void rng_threefry2x32(Rng key, uint32_t count0, uint32_t count1, uint32_t out[2]) { + static const uint32_t rotations[2][4] = { + {13u, 15u, 26u, 6u}, + {17u, 29u, 16u, 24u}, + }; + uint32_t k0 = (uint32_t)key; + uint32_t k1 = (uint32_t)(key >> 32); + uint32_t ks[3] = { + k0, + k1, + k0 ^ k1 ^ 0x1BD11BDAu, + }; + uint32_t x0 = count0 + ks[0]; + uint32_t x1 = count1 + ks[1]; + for (uint32_t block = 0; block < 5u; block++) { + const uint32_t* rs = rotations[block & 1u]; + for (int i = 0; i < 4; i++) { + x0 += x1; + x1 = rng_rotl32(x1, rs[i]); + x1 ^= x0; + } + x0 += ks[(block + 1u) % 3u]; + x1 += ks[(block + 2u) % 3u] + block + 1u; } + out[0] = x0; + out[1] = x1; +} - 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; +static Rng rng_counter_key(Rng key, uint32_t count0, uint32_t count1) { + uint32_t out[2]; + rng_threefry2x32(key, count0, count1, out); + return (uint64_t)out[0] | ((uint64_t)out[1] << 32); +} - env->arena = arena; - env->state = arena->states; - env->packet_id = 0; - env->lane_id = 0; - env->owns_state_storage = true; +Rng rng_seed(uint32_t seed) { + return (uint64_t)seed << 32; } -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 puf_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); +void rng_split(Rng key, Rng* left, Rng* right) { + *left = rng_counter_key(key, 0u, 0u); + *right = rng_counter_key(key, 0u, 1u); } -static inline void craftax_encode_native_observation( - const CraftaxState* state, - float* obs -) { - if (obs == NULL) { - return; +void rng_split_n(Rng key, Rng* out, int n) { + for (int i = 0; i < n; i++) { + out[i] = rng_counter_key(key, 0u, (uint32_t)i); } - 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; +Rng rng_key(Rng* rng) { + Rng draw; + rng_split(*rng, rng, &draw); + return draw; } -// ============================================================ -// 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); +uint32_t rng_u32(Rng key, uint64_t i) { + uint32_t out[2]; + rng_threefry2x32(key, (uint32_t)(i >> 32), (uint32_t)i, out); + return out[0] ^ out[1]; } -void puf_reset(Craftax* env) { - float* obs = env->agents[0].observations; - if (env->agents[0].rewards != NULL) { - env->agents[0].rewards[0] = 0.0f; +float rng_f32(Rng key, uint64_t i) { + uint32_t bits = (rng_u32(key, i) >> 9u) | 0x3F800000u; + float v; + memcpy(&v, &bits, sizeof(v)); + return v - 1.0f; +} + +int randint(Rng key, uint64_t i, int lo, int hi) { + Rng k1; + Rng k2; + rng_split(key, &k1, &k2); + uint32_t higher_bits = rng_u32(k1, i); + uint32_t lower_bits = rng_u32(k2, i); + uint32_t span = (uint32_t)hi > (uint32_t)lo ? (uint32_t)(hi - lo) : 1u; + uint32_t multiplier = 65536u % span; + multiplier = (uint32_t)(((uint64_t)multiplier * (uint64_t)multiplier) + % (uint64_t)span); + uint32_t random_offset = (uint32_t)( + (((uint64_t)(higher_bits % span) * (uint64_t)multiplier) + + (uint64_t)(lower_bits % span)) + % (uint64_t)span + ); + return lo + (int)random_offset; +} + +void store_rng(State* state, Rng rng) { + state->state_rng[0] = (uint32_t)rng; + state->state_rng[1] = (uint32_t)(rng >> 32); +} + +int choice_valid(Rng 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 (env->agents[0].terminals != NULL) { - env->agents[0].terminals[0] = 0.0f; + if (valid_count == 0) { + return 0; } - env->episode_return_accum = 0.0f; - env->episode_length_accum = 0; - memset(env->achievements, 0, sizeof(env->achievements)); + float draw = valid_count * (1.0f - rng_f32(key, 0)); + 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; +} - craftax_reset_state_from_seed(env); - craftax_encode_native_observation(env->state, obs); +void refresh_spawn_cell(State* state, int level, int row, int col) { + int block = state->map[level][row][col]; + uint64_t bit = 1ull << col; + uint64_t* land = &state->spawn_land[level][row]; + uint64_t* grave = &state->spawn_grave[level][row]; + uint64_t* water = &state->spawn_water[level][row]; + *land = (*land & ~bit) | ((block == BLOCK_GRASS || block == BLOCK_PATH + || block == BLOCK_FIRE_GRASS || block == BLOCK_ICE_GRASS) ? bit : 0); + *grave = (*grave & ~bit) | ((block == BLOCK_GRAVE || block == BLOCK_GRAVE2 + || block == BLOCK_GRAVE3) ? bit : 0); + *water = (*water & ~bit) | (block == BLOCK_WATER ? bit : 0); } -#ifdef CRAFTAX_PROFILE -static void c_step_native(Craftax* env) { - float* obs = env->agents[0].observations; - CRAFTAX_PROFILE_START(); - env->agents[0].rewards[0] = 0.0f; - env->agents[0].terminals[0] = 0.0f; +void set_block(State* state, int level, int row, int col, int block) { + state->map[level][row][col] = block; + refresh_spawn_cell(state, level, row, col); +} - int action = (int)env->agents[0].actions[0]; - if (action < 0) { - action = CRAFTAX_ACTION_NOOP; +void generate_fractal(Rng rng, int rows, int cols, int res_rows, int res_cols, + int octaves, float persistence, int lacunarity, float* out) { + // Perlin noise for world generation + int size = rows * cols; + memset(out, 0, size * sizeof(float)); + int frequency = 1; + float amplitude = 1.0f; + for (int octave = 0; octave < octaves; octave++) { + Rng next_rng; + Rng noise_key; + rng_split(rng, &next_rng, &noise_key); + rng = next_rng; + + Rng unused; + Rng angle_key; + rng_split(noise_key, &unused, &angle_key); + int cell_rows = rows / (frequency * res_rows); + int cell_cols = cols / (frequency * res_cols); + int width = frequency * res_cols + 1; + + for (int row = 0; row < rows; row++) { + int grad_row = row / cell_rows; + float local_row = (row - grad_row * cell_rows) / (float)cell_rows; + float interp_row = local_row * local_row * local_row + * (local_row * (local_row * 6.0f - 15.0f) + 10.0f); + for (int col = 0; col < cols; col++) { + int grad_col = col / cell_cols; + float local_col = (col - grad_col * cell_cols) / (float)cell_cols; + float interp_col = local_col * local_col * local_col + * (local_col * (local_col * 6.0f - 15.0f) + 10.0f); + float gx[2][2]; + float gy[2][2]; + for (int dr = 0; dr < 2; dr++) { + for (int dc = 0; dc < 2; dc++) { + uint64_t index = (grad_row + dr) * width + (grad_col + dc); + float angle = NOISE_PI2 * rng_f32(angle_key, index); + gx[dr][dc] = cosf(angle); + gy[dr][dc] = sinf(angle); + } + } + float n00 = local_row * gx[0][0] + local_col * gy[0][0]; + float n10 = (local_row - 1.0f) * gx[1][0] + local_col * gy[1][0]; + float n01 = local_row * gx[0][1] + (local_col - 1.0f) * gy[0][1]; + float n11 = (local_row - 1.0f) * gx[1][1] + (local_col - 1.0f) * gy[1][1]; + float n0 = n00 * (1.0f - interp_row) + interp_row * n10; + float n1 = n01 * (1.0f - interp_row) + interp_row * n11; + out[row * cols + col] += amplitude + * NOISE_SQRT2 * ((1.0f - interp_col) * n0 + interp_col * n1); + } + } + + frequency *= lacunarity; + amplitude *= persistence; + } + float min_value = out[0]; + float max_value = out[0]; + for (int i = 1; i < size; i++) { + if (out[i] < min_value) { + min_value = out[i]; + } + if (out[i] > max_value) { + max_value = out[i]; + } } - if (action >= CRAFTAX_NUM_ACTIONS) { - action = CRAFTAX_NUM_ACTIONS - 1; + float scale = max_value - min_value; + for (int i = 0; i < size; i++) { + out[i] = (out[i] - min_value) / scale; } +} - 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); +int cell_index(int row, int col) { + return row * MAP_SIZE + col; +} - float reward = craftax_gameplay_step_native(env->state, action, step_rng); +void generate_world_from_key(State* state, Rng rng) { + memset(state, 0, sizeof(*state)); + Rng smooth_split[7]; + rng_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]; + Rng level_rng = smooth_split[i + 1]; + const SmoothGenConfig* config = &SMOOTH_LEVEL_CONFIGS[i]; + const int player_row = MAP_SIZE / 2; + const int player_col = MAP_SIZE / 2; + float water[MAP_CELLS]; + float mountain[MAP_CELLS]; + float path_x[MAP_CELLS]; + float tree_noise[MAP_CELLS]; + bool lava_map[MAP_SIZE][MAP_SIZE]; + float light_acc[MAP_SIZE][MAP_SIZE]; + Rng subkey; + + rng_split(level_rng, &level_rng, &subkey); + generate_fractal(subkey, MAP_SIZE, MAP_SIZE, 3, 3, 1, 0.5f, 2, water); + rng_split(level_rng, &level_rng, &subkey); + rng_split(level_rng, &level_rng, &subkey); + generate_fractal(subkey, MAP_SIZE, MAP_SIZE, 3, 3, 1, 0.5f, 2, mountain); + rng_split(level_rng, &level_rng, &subkey); + generate_fractal(subkey, MAP_SIZE, MAP_SIZE, 6, 24, 1, 0.5f, 2, path_x); + rng_split(level_rng, &level_rng, &subkey); + rng_split(level_rng, &level_rng, &subkey); + Rng tree_uniform_key = level_rng; + generate_fractal(subkey, MAP_SIZE, MAP_SIZE, 12, 12, 1, 0.5f, 2, tree_noise); + + for (int row = 0; row < MAP_SIZE; row++) { + int dr = row > player_row ? row - player_row : player_row - row; + for (int col = 0; col < MAP_SIZE; col++) { + int dc = col > player_col ? col - player_col : player_col - col; + float distance = sqrtf(dr * dr + dc * dc); + float proximity_water = distance / config->water_strength; + if (proximity_water < 0.0f) { + proximity_water = 0.0f; + } + if (proximity_water > config->water_max) { + proximity_water = config->water_max; + } + float proximity_mountain = distance / config->mountain_strength; + if (proximity_mountain < 0.0f) { + proximity_mountain = 0.0f; + } + if (proximity_mountain > config->mountain_max) { + proximity_mountain = config->mountain_max; + } + int idx = cell_index(row, col); + + water[idx] = water[idx] + proximity_water - 1.0f; + int block = water[idx] > config->water_threshold + ? config->sea_block + : config->default_block; + if (water[idx] > config->sand_threshold && block != config->sea_block) { + block = config->coast_block; + } - CRAFTAX_PROFILE_ZONE(13); - bool done = craftax_is_game_over_native(env->state); - CRAFTAX_PROFILE_END(13); + mountain[idx] = mountain[idx] + 0.05f + proximity_mountain - 1.0f; + if (mountain[idx] > 0.7f) { + block = config->mountain_block; + } + if (mountain[idx] > 0.7f && path_x[idx] > 0.8f) { + block = config->path_block; + } + if (mountain[idx] > 0.7f && path_x[cell_index(col, row)] > 0.8f) { + block = config->path_block; + } + if (mountain[idx] > 0.85f && water[idx] > 0.4f) { + block = config->inner_mountain_block; + } + if (tree_noise[idx] > config->tree_threshold_perlin + && rng_f32(tree_uniform_key, idx) > config->tree_threshold_uniform + && block == config->tree_requirement_block) { + block = config->tree; + } - CRAFTAX_PROFILE_ZONE(15); - craftax_copy_achievements_to_env(env, env->state); - CRAFTAX_PROFILE_END(15); + state->map[level][row][col] = block; + state->item_map[level][row][col] = ITEM_NONE; + light_acc[row][col] = config->default_light; + } + } - CRAFTAX_PROFILE_ZONE(16); - env->agents[0].rewards[0] = reward; - env->agents[0].terminals[0] = done ? 1.0f : 0.0f; - env->episode_return_accum += reward; - env->episode_length_accum += 1; - CRAFTAX_PROFILE_END(16); + Rng ore_rng; + rng_split(level_rng, &level_rng, &ore_rng); + for (int ore_index = 0; ore_index < 5; ore_index++) { + Rng ore_key; + rng_split(ore_rng, &ore_rng, &ore_key); + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + int idx = cell_index(row, col); + if (state->map[level][row][col] == config->ore_requirement_blocks[ore_index] + && rng_f32(ore_key, idx) < config->ore_chances[ore_index]) { + state->map[level][row][col] = config->ores[ore_index]; + } + } + } + } - 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); - } + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + int idx = cell_index(row, col); + lava_map[row][col] = mountain[idx] > 0.85f && tree_noise[idx] > 0.7f; + if (lava_map[row][col]) { + state->map[level][row][col] = config->lava; + } + } + } - CRAFTAX_PROFILE_ZONE(11); - craftax_encode_native_observation(env->state, obs); - CRAFTAX_PROFILE_END(11); + rng_split(level_rng, &level_rng, &subkey); + bool valid_diamond[MAP_CELLS]; + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + valid_diamond[cell_index(row, col)] = state->map[level][row][col] == BLOCK_STONE; + } + } + int diamond_index = choice_valid(subkey, valid_diamond, MAP_CELLS); + state->map[level][diamond_index / MAP_SIZE][diamond_index % MAP_SIZE] = BLOCK_STONE; + state->map[level][player_row][player_col] = config->player_spawn; + + bool valid_ladder[MAP_CELLS]; + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + valid_ladder[cell_index(row, col)] = + state->map[level][row][col] == config->valid_ladder; + } + } - // Record unprofiled time - CRAFTAX_PROFILE_ZONE(17); - CRAFTAX_PROFILE_END(17); + rng_split(level_rng, &level_rng, &subkey); + int ladder_down_index = choice_valid(subkey, valid_ladder, MAP_CELLS); + state->down_ladders[level][0] = ladder_down_index / MAP_SIZE; + state->down_ladders[level][1] = ladder_down_index % MAP_SIZE; + if (config->ladder_down) { + state->item_map[level][state->down_ladders[level][0]][state->down_ladders[level][1]] = + ITEM_LADDER_DOWN; + } -#ifdef CRAFTAX_PROFILE - static int profile_step_count = 0; - profile_step_count++; - if (profile_step_count >= 100000) { - craftax_profile_report(); - profile_step_count = 0; + rng_split(level_rng, &level_rng, &subkey); + int ladder_up_index = choice_valid(subkey, valid_ladder, MAP_CELLS); + int r = ladder_up_index / MAP_SIZE; + int c = ladder_up_index % MAP_SIZE; + state->up_ladders[level][0] = r; + state->up_ladders[level][1] = c; + int light_row = r - 4; + int light_col = c - 4; + if (light_row < 0) { + light_row += MAP_SIZE; + } + if (light_col < 0) { + light_col += MAP_SIZE; + } + if (light_row > MAP_SIZE - 9) { + light_row = MAP_SIZE - 9; + } + if (light_col > MAP_SIZE - 9) { + light_col = MAP_SIZE - 9; + } + if (light_row < 0) { + light_row = 0; + } + if (light_col < 0) { + light_col = 0; + } + for (int lr = 0; lr < 9; lr++) { + for (int lc = 0; lc < 9; lc++) { + float torch = 1.0f - sqrtf((lr - 4) * (lr - 4) + (lc - 4) * (lc - 4)) / 5.0f; + if (torch < 0.0f) { + torch = 0.0f; + } + float light = torch * (1.0f - config->default_light) + config->default_light; + light_acc[light_row + lr][light_col + lc] = light; + } + } + if (config->lava == BLOCK_LAVA) { + 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 < MAP_SIZE; row++) { + for (int col = 0; col < 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 >= MAP_SIZE) { + continue; + } + for (int kc = 0; kc < 3; kc++) { + int src_col = col + kc - 1; + if (src_col < 0 || src_col >= MAP_SIZE) { + continue; + } + if (lava_map[src_row][src_col]) { + add += kernel[kr][kc]; + } + } + } + float light = light_acc[row][col] + add; + if (light > 1.0f) { + light = 1.0f; + } + light_acc[row][col] = light; + } + } + } + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + float light = light_acc[row][col]; + if (light < 0.0f) { + light = 0.0f; + } + if (light > 1.0f) { + light = 1.0f; + } + state->light_map[level][row][col] = (unsigned char)(light * 255.0f); + } + } + if (config->ladder_up) { + state->item_map[level][r][c] = ITEM_LADDER_UP; + } } -#endif -} + Rng dungeon_split[4]; + rng_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]; + Rng level_rng = dungeon_split[i + 1]; + const DungeonConfig* config = &DUNGEON_LEVEL_CONFIGS[i]; + const int chunk_size = DUNGEON_CHUNK_SIZE; + const int world_chunk_height = MAP_SIZE / chunk_size; + const int num_rooms = DUNGEON_ROOM_COUNT; + const int min_room_size = DUNGEON_MIN_ROOM_SIZE; + const int max_room_size = DUNGEON_MAX_ROOM_SIZE; + const int padded_size = MAP_SIZE + 2 * max_room_size; + + int padded_map[68][68]; + int padded_item[68][68]; + bool room_occupancy[9]; + int room_sizes[8][2]; + int 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 + MAP_SIZE + && col >= max_room_size + && col < max_room_size + MAP_SIZE; + padded_map[row][col] = inner ? BLOCK_WALL : 0; + padded_item[row][col] = ITEM_NONE; + } + } + for (int i = 0; i < 9; i++) { + room_occupancy[i] = true; + } -#endif + Rng keys3[3]; + rng_split_n(level_rng, keys3, 3); + level_rng = keys3[0]; + Rng room_size_key = keys3[2]; + for (int room = 0; room < num_rooms; room++) { + room_sizes[room][0] = randint(room_size_key, room * 2u, min_room_size, max_room_size); + room_sizes[room][1] = randint(room_size_key, room * 2u + 1u, min_room_size, max_room_size); + } -static void c_step_gameplay(Craftax* env) { - env->agents[0].rewards[0] = 0.0f; - env->agents[0].terminals[0] = 0.0f; + Rng room_rng; + rng_split(level_rng, &level_rng, &room_rng); + for (int room_index = 0; room_index < num_rooms; room_index++) { + Rng choice_key; + rng_split(room_rng, &room_rng, &choice_key); + int room_chunk = choice_valid(choice_key, room_occupancy, 9); + room_occupancy[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; + Rng position_key; + rng_split(room_rng, &room_rng, &position_key); + room_row += randint(position_key, 0, 0, chunk_size - min_room_size); + room_col += randint(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] = BLOCK_PATH; + } + } + } - int action = (int)env->agents[0].actions[0]; - if (action < 0) action = CRAFTAX_ACTION_NOOP; - if (action >= CRAFTAX_NUM_ACTIONS) action = CRAFTAX_NUM_ACTIONS - 1; + padded_item[room_row][room_col] = ITEM_TORCH; + padded_item[room_row + room_sizes[room_index][0] - 1][room_col] = ITEM_TORCH; + padded_item[room_row][room_col + room_sizes[room_index][1] - 1] = ITEM_TORCH; + padded_item[room_row + room_sizes[room_index][0] - 1][room_col + room_sizes[room_index][1] - 1] = ITEM_TORCH; + + Rng chest_key; + rng_split(room_rng, &room_rng, &chest_key); + int chest_row = randint(chest_key, 0, 1, room_sizes[room_index][0] - 1); + int chest_col = randint(chest_key, 1, 1, room_sizes[room_index][1] - 1); + padded_map[room_row + chest_row][room_col + chest_col] = BLOCK_CHEST; + + Rng fountain_keys[3]; + rng_split_n(room_rng, fountain_keys, 3); + room_rng = fountain_keys[0]; + int fountain_row = randint(fountain_keys[1], 0, 1, room_sizes[room_index][0] - 1); + int fountain_col = randint(fountain_keys[1], 1, 1, room_sizes[room_index][1] - 1); + if (rng_f32(fountain_keys[2], 0) > 0.5f) { + padded_map[room_row + fountain_row][room_col + fountain_col] = config->fountain_block; + } + } - 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); + Rng path_rng; + rng_split(level_rng, &level_rng, &path_rng); + bool included_rooms[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]; + Rng sink_key; + rng_split(path_rng, &path_rng, &sink_key); + int sink_index = choice_valid(sink_key, included_rooms, 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; + if (path_index_col >= 0 && path_index_col <= abs_distance + && padded_map[source_row][col] == BLOCK_WALL) { + padded_map[source_row][col] = 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; + if (path_index_row >= 0 && path_index_row <= abs_distance + && padded_map[row][sink_col] == BLOCK_WALL) { + padded_map[row][sink_col] = BLOCK_PATH; + } + } + } - 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); + Rng unused_left; + Rng next_path_rng; + rng_split(path_rng, &unused_left, &next_path_rng); + path_rng = next_path_rng; + included_rooms[path_index] = true; + } - env->agents[0].rewards[0] = reward; - env->agents[0].terminals[0] = done ? 1.0f : 0.0f; - env->episode_return_accum += reward; - env->episode_length_accum += 1; + padded_map[room_positions[0][0] + 2][room_positions[0][1] + 2] = config->special_block; - 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); - } -} + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + state->map[level][row][col] = + padded_map[row + max_room_size][col + max_room_size]; + state->item_map[level][row][col] = + padded_item[row + max_room_size][col + max_room_size]; + } + } -static void c_step_encode(Craftax* env) { - float* obs = env->agents[0].observations; - craftax_encode_native_observation(env->state, obs); -} + bool adjacent_path[MAP_SIZE][MAP_SIZE]; + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + bool adjacent = state->map[level][row][col] != BLOCK_WALL; + adjacent = adjacent || (row > 0 && state->map[level][row - 1][col] != BLOCK_WALL); + adjacent = adjacent || (row + 1 < MAP_SIZE && state->map[level][row + 1][col] != BLOCK_WALL); + adjacent = adjacent || (col > 0 && state->map[level][row][col - 1] != BLOCK_WALL); + adjacent = adjacent || (col + 1 < MAP_SIZE && state->map[level][row][col + 1] != BLOCK_WALL); + adjacent_path[row][col] = adjacent; + } + } -// Hold Left Shift + WASD/arrows/space/Z. Period steps a noop. -static int craftax_human_controls(Craftax *env) { - if (!IsWindowReady() || !IsKeyDown(KEY_LEFT_SHIFT)) { - return 0; - } - int action = CRAFTAX_ACTION_NOOP; - if (IsKeyPressed(KEY_A) || IsKeyPressed(KEY_LEFT)) { - action = CRAFTAX_ACTION_LEFT; - } - if (IsKeyPressed(KEY_D) || IsKeyPressed(KEY_RIGHT)) { - action = CRAFTAX_ACTION_RIGHT; + Rng rare_key; + rng_split(level_rng, &level_rng, &rare_key); + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + int idx = cell_index(row, col); + bool rare = (1.0f - rng_f32(rare_key, idx)) > 0.9f; + int wall_map = rare ? BLOCK_WALL_MOSS : BLOCK_WALL; + bool rare_path = rare + && state->map[level][row][col] == BLOCK_PATH + && state->item_map[level][row][col] == ITEM_NONE; + int path_map = rare_path ? config->rare_path_replacement_block : state->map[level][row][col]; + bool is_wall_map = state->map[level][row][col] == BLOCK_WALL && adjacent_path[row][col]; + if (!adjacent_path[row][col]) { + state->map[level][row][col] = BLOCK_DARKNESS; + } else if (is_wall_map) { + state->map[level][row][col] = wall_map; + } else { + state->map[level][row][col] = path_map; + } + state->light_map[level][row][col] = 255; + } + } + + bool valid_ladder[MAP_CELLS]; + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + valid_ladder[cell_index(row, col)] = state->map[level][row][col] == BLOCK_PATH; + } + } + Rng ladder_down_key; + rng_split(level_rng, &level_rng, &ladder_down_key); + int ladder_down_index = choice_valid(ladder_down_key, valid_ladder, MAP_CELLS); + int r = ladder_down_index / MAP_SIZE; + int c = ladder_down_index % MAP_SIZE; + state->down_ladders[level][0] = r; + state->down_ladders[level][1] = c; + state->item_map[level][r][c] = ITEM_LADDER_DOWN; + + Rng ladder_up_key; + rng_split(level_rng, &level_rng, &ladder_up_key); + int ladder_up_index = choice_valid(ladder_up_key, valid_ladder, MAP_CELLS); + r = ladder_up_index / MAP_SIZE; + c = ladder_up_index % MAP_SIZE; + state->up_ladders[level][0] = r; + state->up_ladders[level][1] = c; + state->item_map[level][r][c] = ITEM_LADDER_UP; } - if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) { - action = CRAFTAX_ACTION_UP; + + for (int level = 0; level < NUM_LEVELS; level++) { + for (int i = 0; i < MAX_MELEE_MOBS; i++) { + state->melee_mobs[level].health[i] = 1.0f; + state->passive_mobs[level].health[i] = 1.0f; + state->mob_projectiles[level].health[i] = 1.0f; + state->player_projectiles[level].health[i] = 1.0f; + } + for (int i = 0; i < MAX_RANGED_MOBS; i++) { + state->ranged_mobs[level].health[i] = 1.0f; + } + for (int projectile = 0; projectile < MAX_MOB_PROJECTILES; projectile++) { + state->mob_projectile_dirs[level][projectile][0] = 1; + state->mob_projectile_dirs[level][projectile][1] = 1; + } + for (int projectile = 0; projectile < MAX_PLAYER_PROJECTILES; projectile++) { + state->player_projectile_directions[level][projectile][0] = 1; + state->player_projectile_directions[level][projectile][1] = 1; + } } - if (IsKeyPressed(KEY_S) || IsKeyPressed(KEY_DOWN)) { - action = CRAFTAX_ACTION_DOWN; + + Rng potion_key; + rng_split(rng, &rng, &potion_key); + Rng potion_carry; + Rng sort_key; + rng_split(potion_key, &potion_carry, &sort_key); + uint32_t potion_keys[6]; + for (int i = 0; i < 6; i++) { + potion_keys[i] = rng_u32(sort_key, i); + state->potion_mapping[i] = i; } - if (IsKeyPressed(KEY_SPACE)) { - action = CRAFTAX_ACTION_DO; + for (int i = 1; i < 6; i++) { + uint32_t key_value = potion_keys[i]; + int value = state->potion_mapping[i]; + int j = i - 1; + while (j >= 0 && potion_keys[j] > key_value) { + potion_keys[j + 1] = potion_keys[j]; + state->potion_mapping[j + 1] = state->potion_mapping[j]; + j--; + } + potion_keys[j + 1] = key_value; + state->potion_mapping[j + 1] = value; } - if (IsKeyPressed(KEY_Z)) { - action = CRAFTAX_ACTION_SLEEP; + + Rng state_key; + rng_split(rng, &rng, &state_key); + store_rng(state, state_key); + + state->monsters_killed[0] = 10; + state->player_position[0] = MAP_SIZE / 2; + state->player_position[1] = MAP_SIZE / 2; + state->player_level = 0; + state->player_direction = ACTION_UP; + state->player_health = 9.0f; + state->player_food = 9; + state->player_drink = 9; + state->player_energy = 9; + state->player_mana = 9; + state->player_dexterity = 1; + state->player_strength = 1; + state->player_intelligence = 1; + state->boss_timestep_to_spawn_this_round = BOSS_SPAWN_TURNS; + float cosine = cosf(3.14159265358979323846f * 0.3f); + state->light_level = 1.0f - powf(fabsf(cosine), 3.0f); + memset(state->spawn_land, 0, sizeof(state->spawn_land)); + memset(state->spawn_grave, 0, sizeof(state->spawn_grave)); + memset(state->spawn_water, 0, sizeof(state->spawn_water)); + for (int level = 0; level < NUM_LEVELS; level++) { + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + refresh_spawn_cell(state, level, row, col); + } + } } - if (action != CRAFTAX_ACTION_NOOP || IsKeyPressed(KEY_PERIOD)) { - env->agents[0].actions[0] = (float)action; - return 1; +} + +void write_mob_obs(float* obs, const State* state, const Mobs* mobs, int slots, + int channel) { + int level = state->player_level; + int half_r = OBS_ROWS / 2; + int half_c = OBS_COLS / 2; + for (int i = 0; i < slots; i++) { + int local_row = mobs->position[i][0] - state->player_position[0] + half_r; + int local_col = mobs->position[i][1] - state->player_position[1] + half_c; + int on_screen = mobs->mask[i] + && local_row >= 0 && local_row < OBS_ROWS + && local_col >= 0 && local_col < OBS_COLS; + if (local_row >= OBS_ROWS || local_row < -OBS_ROWS + || local_col >= OBS_COLS || local_col < -OBS_COLS) { + continue; + } + if (local_row < 0) { + local_row += OBS_ROWS; + } + if (local_col < 0) { + local_col += OBS_COLS; + } + int dest_row = state->player_position[0] + local_row - half_r; + int dest_col = state->player_position[1] + local_col - half_c; + int dest_visible = dest_row >= 0 && dest_row < MAP_SIZE + && dest_col >= 0 && dest_col < MAP_SIZE + && state->light_map[level][dest_row][dest_col] > VISIBLE_LIGHT_THRESHOLD; + float value = 0.0f; + if (on_screen && dest_visible) { + value = (float)(mobs->type_id[i] + 1); + } + int base = (local_row * OBS_COLS + local_col) * OBS_TILE_CHANNELS; + obs[base + 3 + channel] = value; } - return -1; } -void puf_step(Craftax* env) { - if (craftax_human_controls(env) < 0) { - return; +int clampi(int value, int low, int high) { + if (value < low) { + return low; } -#ifdef CRAFTAX_TICK_FRAMES - if (env->tick_frames_left > 0) { - env->tick_frames_left--; - return; + if (value > high) { + return high; } - env->tick_frames_left = CRAFTAX_TICK_FRAMES - 1; -#endif - c_step_gameplay(env); - c_step_encode(env); + return value; } -void puf_close(Craftax* env) { - if (!env->owns_state_storage || env->arena == NULL) { - return; +float clampf(float value, float low, float high) { + if (value < low) { + return low; } - 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); + if (value > high) { + return high; } - free(buf); - fclose(f); - craftax_textures_loaded = true; + return value; } -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; - } +int max_health(const State* state) { + return 8 + state->player_strength; } -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); +int equipped_armour(const State* state) { + return state->inventory.armour[0] + state->inventory.armour[1] + + state->inventory.armour[2] + state->inventory.armour[3]; } -void puf_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; +int max_food(const State* state) { + return 7 + 2 * state->player_dexterity; +} - 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); - craftax_human_controls(env); +int max_drink(const State* state) { + return 7 + 2 * state->player_dexterity; +} - 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; +int max_energy(const State* state) { + return 7 + 2 * state->player_dexterity; +} - BeginDrawing(); - ClearBackground(BLACK); +int max_mana(const State* state) { + return 6 + 3 * state->player_intelligence; +} - 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); - } - } +bool fighting_boss(const State* state) { + return state->player_level == NUM_LEVELS - 1; +} + +bool boss_vulnerable(const State* state) { + if (state->boss_timestep_to_spawn_this_round > 0) { + return false; + } + int level = state->player_level; + for (int i = 0; i < MAX_MELEE_MOBS; i++) { + if (state->melee_mobs[level].mask[i]) { + return false; + } + } + for (int i = 0; i < MAX_RANGED_MOBS; i++) { + if (state->ranged_mobs[level].mask[i]) { + return false; } } + return true; +} - // 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); +void action_to_direction(int action, int direction[2]) { + direction[0] = 0; + direction[1] = 0; + + if (action == ACTION_LEFT) { + direction[1] = -1; + } else if (action == ACTION_RIGHT) { + direction[1] = 1; + } else if (action == ACTION_UP) { + direction[0] = -1; + } else if (action == ACTION_DOWN) { + direction[0] = 1; + } +} - // 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}); +bool is_solid_block(int block) { + switch (block) { + case BLOCK_STONE: + case BLOCK_TREE: + case BLOCK_COAL: + case BLOCK_IRON: + case BLOCK_DIAMOND: + case BLOCK_CRAFTING_TABLE: + case BLOCK_FURNACE: + case BLOCK_PLANT: + case BLOCK_RIPE_PLANT: + case BLOCK_WALL: + case BLOCK_WALL_MOSS: + case BLOCK_STALAGMITE: + case BLOCK_RUBY: + case BLOCK_SAPPHIRE: + case BLOCK_CHEST: + case BLOCK_FOUNTAIN: + case BLOCK_FIRE_TREE: + case BLOCK_ENCHANTMENT_TABLE_FIRE: + case BLOCK_ENCHANTMENT_TABLE_ICE: + case BLOCK_GRAVE: + case BLOCK_GRAVE2: + case BLOCK_GRAVE3: + case BLOCK_NECROMANCER: + return true; + default: + return false; } +} - // 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}); +bool mob_at(const State* state, int level, int row, int col) { + if ((unsigned)row >= MAP_SIZE || (unsigned)col >= MAP_SIZE) { + return false; + } + return (state->mob_bits[level][row] >> col) & 1ull; +} - EndDrawing(); - puf_web_vsync(); +void set_mob_bit(State* state, int level, int row, int col, bool on) { + if ((unsigned)row >= MAP_SIZE || (unsigned)col >= MAP_SIZE) { + return; + } + uint64_t bit = 1ull << col; + if (on) { + state->mob_bits[level][row] |= bit; + } else { + state->mob_bits[level][row] &= ~bit; + } } -// --- Native trainer (pufferl) API --- -void puf_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); - 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]); +void move_mob_occupancy(State* state, int level, int old_row, int old_col, + int new_row, int new_col, bool keep) { + set_mob_bit(state, level, old_row, old_col, false); + if (keep) { + set_mob_bit(state, level, new_row, new_col, true); } - dict_set(out, "n", log->n); +} + +bool mobs_at(const Mobs* mobs, int slots, int row, int col, int* slot) { + for (int i = 0; i < slots; i++) { + if (mobs->mask[i] + && mobs->position[i][0] == row + && mobs->position[i][1] == col) { + *slot = i; + return true; + } + } + return false; +} + +Mobs* mobs_for_class(State* state, int level, int mob_class) { + if (mob_class == MOB_PASSIVE) { + return &state->passive_mobs[level]; + } + if (mob_class == MOB_RANGED) { + return &state->ranged_mobs[level]; + } + return &state->melee_mobs[level]; +} + +bool find_mob_at(const State* state, int level, int row, int col, int* mob_class, + int* slot) { + if (mobs_at(&state->melee_mobs[level], MAX_MELEE_MOBS, row, col, slot)) { + *mob_class = MOB_MELEE; + return true; + } + if (mobs_at(&state->passive_mobs[level], MAX_PASSIVE_MOBS, row, col, slot)) { + *mob_class = MOB_PASSIVE; + return true; + } + if (mobs_at(&state->ranged_mobs[level], MAX_RANGED_MOBS, row, col, slot)) { + *mob_class = MOB_RANGED; + return true; + } + return false; +} + +bool valid_typed_mob_position(const State* state, int level, int mob_class, + int type_id, int row, int col, int old_row, int old_col) { + if (row < 0 || row >= MAP_SIZE || col < 0 || col >= MAP_SIZE) { + return false; + } + if (row == state->player_position[0] && col == state->player_position[1]) { + return false; + } + int block = state->map[level][row][col]; + if (is_solid_block(block)) { + return false; + } + static const bool blocked[NUM_MOB_TYPES][3][3] = { + {{0,1,1},{0,1,1},{0,1,1}}, {{0,0,0},{0,1,1},{0,1,1}}, + {{0,1,1},{0,1,1},{0,1,1}}, {{0,1,1},{0,0,1},{0,1,1}}, + {{0,1,1},{0,1,1},{0,1,1}}, {{0,1,1},{0,1,1},{1,0,1}}, + {{0,1,1},{0,1,1},{0,0,0}}, {{0,1,1},{0,1,1},{0,0,0}}, + }; + int terrain = block == BLOCK_WATER ? 1 : (block == BLOCK_LAVA ? 2 : 0); + if (blocked[clampi(type_id, 0, 7)][clampi(mob_class, 0, 2)][terrain]) { + return false; + } + return !mob_at(state, level, row, col) || (row == old_row && col == old_col); +} + +typedef struct { float physical, fire, ice; } Damage; + +Damage mob_damage_vector(int type, int mob_class) { + static const float damage[NUM_MOB_TYPES][4][3] = { + {{0,0,0},{2,0,0},{0,0,0},{2,0,0}}, {{0,0,0},{4,0,0},{0,0,0},{4,0,0}}, + {{0,0,0},{3,0,0},{0,0,0},{0,3,0}}, {{0,0,0},{5,0,0},{0,0,0},{0,0,3}}, + {{0,0,0},{6,0,0},{0,0,0},{5,0,0}}, {{0,0,0},{6,1,1},{0,0,0},{4,3,3}}, + {{0,0,0},{3,5,0},{0,0,0},{3,5,0}}, {{0,0,0},{4,0,5},{0,0,0},{4,0,5}}, + }; + const float* d = damage[clampi(type, 0, 7)][clampi(mob_class, 0, 3)]; + return (Damage){d[0], d[1], d[2]}; +} + +float damage_to_mob(Damage damage, int type, int mob_class) { + static const float defense[NUM_MOB_TYPES][4][3] = { + {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}, + {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}, + {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}, + {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}, + {{0,0,0},{.5f,0,0},{.5f,0,0},{0,0,0}}, + {{0,0,0},{.2f,0,0},{0,0,0},{0,0,0}}, + {{0,0,0},{.9f,1,0},{.9f,1,0},{0,0,0}}, + {{0,0,0},{.9f,0,1},{.9f,0,1},{0,0,0}}, + }; + const float* d = defense[clampi(type, 0, 7)][clampi(mob_class, 0, 3)]; + return damage.physical * (1-d[0]) + damage.fire * (1-d[1]) + damage.ice * (1-d[2]); +} + +float damage_to_player(const State* state, Damage damage) { + float physical_defense = 0, fire_defense = 0, ice_defense = 0; + for (int i = 0; i < 4; i++) { + physical_defense += 0.1f * state->inventory.armour[i]; + fire_defense += 0.2f * (state->armour_enchantments[i] == 1); + ice_defense += 0.2f * (state->armour_enchantments[i] == 2); + } + float coeff = fighting_boss(state) ? 1.5f : 1.0f; + return coeff * (damage.physical * (1 - physical_defense) + + damage.fire * (1 - fire_defense) + damage.ice * (1 - ice_defense)); +} + +bool damage_mob_at(State* state, int level, int row, int col, float damage, + bool can_eat, bool can_get_achievement) { + int mob_class; + int slot; + if (!find_mob_at(state, level, row, col, &mob_class, &slot)) { + return false; + } + Mobs* mobs = mobs_for_class(state, level, mob_class); + if (!mobs->mask[slot]) { + return false; + } + + mobs->health[slot] -= damage; + if (mobs->health[slot] > 0.0f) { + return true; + } + + int type_id = mobs->type_id[slot]; + mobs->mask[slot] = false; + set_mob_bit(state, level, row, col, false); + state->monsters_killed[level] += mob_class == MOB_PASSIVE ? 0 : 1; + if (can_get_achievement) { + static const int achievements[3][8] = { + {ACH_EAT_COW, ACH_EAT_BAT, ACH_EAT_SNAIL, 0, 0, 0, 0, 0}, + {ACH_DEFEAT_ZOMBIE, ACH_DEFEAT_GNOME_WARRIOR, ACH_DEFEAT_ORC_SOLIDER, + ACH_DEFEAT_LIZARD, ACH_DEFEAT_KNIGHT, ACH_DEFEAT_TROLL, + ACH_DEFEAT_PIGMAN, ACH_DEFEAT_FROST_TROLL}, + {ACH_DEFEAT_SKELETON, ACH_DEFEAT_GNOME_ARCHER, ACH_DEFEAT_ORC_MAGE, + ACH_DEFEAT_KOBOLD, ACH_DEFEAT_ARCHER, ACH_DEFEAT_DEEP_THING, + ACH_DEFEAT_FIRE_ELEMENTAL, ACH_DEFEAT_ICE_ELEMENTAL}, + }; + state->achievements[achievements[clampi(mob_class, 0, 2)][clampi(type_id, 0, 7)]] = 1; + } + + if (mob_class == MOB_PASSIVE && can_eat) { + state->player_food = clampi(state->player_food + 6, 0, max_food(state)); + state->player_hunger = 0.0f; + } + return true; +} + +bool spawn_projectile(State* state, bool from_player, int projectile_type, + int row, int col, int dir_row, int dir_col) { + int level = state->player_level; + Mobs* projectiles = from_player ? &state->player_projectiles[level] : &state->mob_projectiles[level]; + int (*directions)[MAX_PLAYER_PROJECTILES][2] = + from_player ? state->player_projectile_directions : state->mob_projectile_dirs; + for (int i = 0; i < MAX_PLAYER_PROJECTILES; i++) { + if (projectiles->mask[i]) { + continue; + } + projectiles->position[i][0] = row; + projectiles->position[i][1] = col; + Damage d = mob_damage_vector(projectile_type, MOB_PROJECTILE); + projectiles->health[i] = d.physical + d.fire + d.ice; + projectiles->attack_cooldown[i] = 0; + projectiles->type_id[i] = projectile_type; + projectiles->mask[i] = true; + directions[level][i][0] = dir_row; + directions[level][i][1] = dir_col; + return true; + } + return false; +} + +void update_projectile_set(State* state, bool from_player) { + int level = state->player_level; + for (int i = 0; i < MAX_PLAYER_PROJECTILES; i++) { + if (from_player) { + Mobs* projectiles = &state->player_projectiles[level]; + if (!projectiles->mask[i]) { + continue; + } + int old_row = projectiles->position[i][0]; + int old_col = projectiles->position[i][1]; + int proposed_row = old_row + state->player_projectile_directions[level][i][0]; + int proposed_col = old_col + state->player_projectile_directions[level][i][1]; + int ptype = projectiles->type_id[i]; + Damage vector = mob_damage_vector(ptype, MOB_PROJECTILE); + bool arrow = ptype == PROJECTILE_ARROW || ptype == PROJECTILE_ARROW2; + if (arrow && state->bow_enchantment == 1) { + vector.fire += vector.physical * 0.5f; + } + if (arrow && state->bow_enchantment == 2) { + vector.ice += vector.physical * 0.5f; + } + float coeff = 1.0f; + if (arrow) { + coeff = 1.0f + 0.2f * (state->player_dexterity - 1); + } else if (ptype == PROJECTILE_FIREBALL || ptype == PROJECTILE_ICEBALL) { + coeff = 1.0f + 0.5f * (state->player_intelligence - 1); + } + vector.physical *= coeff; + vector.fire *= coeff; + vector.ice *= coeff; + + bool hit_old = false; + int mob_class; + int mob_slot; + if (find_mob_at(state, level, old_row, old_col, &mob_class, &mob_slot)) { + Mobs* target = mobs_for_class(state, level, mob_class); + hit_old = damage_mob_at( + state, level, old_row, old_col, + damage_to_mob(vector, target->type_id[mob_slot], mob_class), + false, true + ); + } + + Damage second = vector; + if (hit_old) { + second.physical = 0.0f; + second.fire = 0.0f; + second.ice = 0.0f; + } + bool hit_new = false; + if (find_mob_at(state, level, proposed_row, proposed_col, &mob_class, &mob_slot)) { + Mobs* target = mobs_for_class(state, level, mob_class); + hit_new = damage_mob_at( + state, level, proposed_row, proposed_col, + damage_to_mob(second, target->type_id[mob_slot], mob_class), + false, true + ); + } + + bool proposed_in_bounds = proposed_row >= 0 && proposed_row < MAP_SIZE + && proposed_col >= 0 && proposed_col < MAP_SIZE; + int proposed_block = proposed_in_bounds ? state->map[level][proposed_row][proposed_col] : 0; + bool in_wall = is_solid_block(proposed_block) && proposed_block != BLOCK_WATER; + bool keep = proposed_in_bounds && !in_wall && !hit_old && !hit_new; + projectiles->position[i][0] = proposed_row; + projectiles->position[i][1] = proposed_col; + projectiles->mask[i] = keep; + } else { + Mobs* projectiles = &state->mob_projectiles[level]; + if (!projectiles->mask[i]) { + continue; + } + int old_row = projectiles->position[i][0]; + int old_col = projectiles->position[i][1]; + int proposed_row = old_row + state->mob_projectile_dirs[level][i][0]; + int proposed_col = old_col + state->mob_projectile_dirs[level][i][1]; + bool proposed_in_player = proposed_row == state->player_position[0] + && proposed_col == state->player_position[1]; + bool proposed_in_bounds = proposed_row >= 0 && proposed_row < MAP_SIZE + && proposed_col >= 0 && proposed_col < MAP_SIZE; + int proposed_block = proposed_in_bounds ? state->map[level][proposed_row][proposed_col] : 0; + bool in_wall = is_solid_block(proposed_block) && proposed_block != BLOCK_WATER; + bool in_mob = mob_at(state, level, proposed_row, proposed_col) + || (state->player_position[0] == proposed_row + && state->player_position[1] == proposed_col); + bool keep_moving = proposed_in_bounds && !in_wall && !in_mob; + bool hit_player = ( + (old_row == state->player_position[0] && old_col == state->player_position[1]) + || proposed_in_player + ); + keep_moving = keep_moving && !hit_player; + bool hit_bench = proposed_block == BLOCK_FURNACE + || proposed_block == BLOCK_CRAFTING_TABLE; + int new_block = hit_bench ? BLOCK_PATH : proposed_block; + + projectiles->position[i][0] = proposed_row; + projectiles->position[i][1] = proposed_col; + projectiles->mask[i] = keep_moving; + if (hit_player) { + state->player_health -= damage_to_player( + state, mob_damage_vector(projectiles->type_id[i], MOB_PROJECTILE)); + state->is_sleeping = false; + state->is_resting = false; + } + if ((unsigned)proposed_row < MAP_SIZE && (unsigned)proposed_col < MAP_SIZE) { + set_block(state, level, proposed_row, proposed_col, new_block); + } + } + } +} + +int floor_mob_type(int level, int mob_class) { + static const int types[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}, + }; + return types[clampi(level, 0, NUM_LEVELS - 1)][clampi(mob_class, 0, 2)]; +} + +int collect_spawn_cells(const State* state, int level, int min_exclusive, + int max_exclusive, bool boss, bool water_only, int* rows, int* cols) { + const uint64_t* terrain = boss + ? state->spawn_grave[level] + : (water_only ? state->spawn_water[level] : state->spawn_land[level]); + + int pr = state->player_position[0]; + int pc = state->player_position[1]; + int limit = MOB_DESPAWN_DISTANCE - 1; + int r0 = pr - limit; + int r1 = pr + limit; + int c0 = pc - limit; + int c1 = pc + limit; + if (r0 < 0) { + r0 = 0; + } + if (r1 > MAP_SIZE - 1) { + r1 = MAP_SIZE - 1; + } + if (c0 < 0) { + c0 = 0; + } + if (c1 > MAP_SIZE - 1) { + c1 = MAP_SIZE - 1; + } + uint64_t col_mask = (~0ull << c0) & ((1ull << (c1 + 1)) - 1); + int count = 0; + for (int row = r0; row <= r1; row++) { + int dr = row - pr; + int dr2 = dr * dr; + uint64_t bits = terrain[row] & ~state->mob_bits[level][row] & col_mask; + while (bits) { + int col = __builtin_ctzll(bits); + bits &= bits - 1; + int dc = col - pc; + int distance2 = dr2 + dc * dc; + if (distance2 > min_exclusive && distance2 < max_exclusive) { + rows[count] = row; + cols[count] = col; + count++; + } + } + } + return count; +} + +bool pick_spawn_cell(const int* rows, const int* cols, int count, Rng key, + int* out_row, int* out_col) { + if (count <= 0) { + return false; + } + float draw = count * (1.0f - rng_f32(key, 0)); + int chosen = (int)ceilf(draw) - 1; + if (chosen < 0) { + chosen = 0; + } + if (chosen >= count) { + chosen = count - 1; + } + *out_row = rows[chosen]; + *out_col = cols[chosen]; + return true; +} + +void spawn_into_slot(State* state, int level, Mobs* mobs, int slot, int mob_class, + int type_id, int row, int col) { + static const float passive_health[NUM_MOB_TYPES] = {3, 4, 6, 8, 0, 0, 0, 0}; + static const float melee_health[NUM_MOB_TYPES] = {5, 7, 9, 11, 12, 20, 20, 24}; + static const float ranged_health[NUM_MOB_TYPES] = {3, 5, 6, 8, 12, 4, 14, 16}; + int idx = clampi(type_id, 0, NUM_MOB_TYPES - 1); + float health = melee_health[idx]; + if (mob_class == MOB_PASSIVE) { + health = passive_health[idx]; + } else if (mob_class == MOB_RANGED) { + health = ranged_health[idx]; + } + mobs->position[slot][0] = row; + mobs->position[slot][1] = col; + mobs->health[slot] = health; + mobs->mask[slot] = true; + set_mob_bit(state, level, row, col, true); +} + +void count_and_empty(const Mobs* mobs, int slots, int* count, int* empty) { + int n = 0; + int first = 0; + bool found = false; + for (int i = 0; i < slots; i++) { + n += mobs->mask[i] ? 1 : 0; + if (!mobs->mask[i] && !found) { + first = i; + found = true; + } + } + *count = n; + *empty = first; +} + +void choose_direction(Rng key, int count, int direction[2]) { + int choice = randint(key, 0u, 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; + } +} + +int choose_player_axis(Rng key, int distance_row, int distance_col) { + int total = distance_row + distance_col; + if (total == 0) { + return 1; + } + int maximum = distance_row > distance_col ? distance_row : distance_col; + float weights[2] = { + distance_row == maximum ? 1.0f / total : 0.0f, + distance_col == maximum ? 1.0f / total : 0.0f, + }; + float sum = weights[0] + weights[1]; + float draw = sum * (1.0f - rng_f32(key, 0)); + return (weights[0] >= draw || sum == 0.0f) ? 0 : 1; +} + +int signi(int value) { + if (value < 0) { + return -1; + } + return value > 0 ? 1 : 0; +} + +void move_melee_slot(State* state, int level, int slot, Rng* rng) { + Mobs* mobs = &state->melee_mobs[level]; + bool alive = mobs->mask[slot]; + int old_row = mobs->position[slot][0]; + int old_col = mobs->position[slot][1]; + int type_id = mobs->type_id[slot]; + int cooldown = mobs->attack_cooldown[slot]; + + int random_dir[2]; + choose_direction(rng_key(rng), 4, random_dir); + int distance_row = abs(state->player_position[0] - old_row); + int distance_col = abs(state->player_position[1] - old_col); + int axis = choose_player_axis(rng_key(rng), distance_row, distance_col); + int player_dir[2] = {0, 0}; + if (axis == 0) { + player_dir[0] = signi(state->player_position[0] - old_row); + } else { + player_dir[1] = signi(state->player_position[1] - old_col); + } + int dist = distance_row + distance_col; + float chase_roll = rng_f32(rng_key(rng), 0); + bool chase = (dist < 10 || fighting_boss(state)) && chase_roll < 0.75f; + int proposed_row = chase ? old_row + player_dir[0] : old_row + random_dir[0]; + int proposed_col = chase ? old_col + player_dir[1] : old_col + random_dir[1]; + bool attacking = dist == 1 && cooldown <= 0 && alive; + if (attacking) { + proposed_row = old_row; + proposed_col = old_col; + Damage damage = mob_damage_vector(type_id, MOB_MELEE); + float sleep = 1.0f + 2.5f * state->is_sleeping; + damage.physical *= sleep; + damage.fire *= sleep; + damage.ice *= sleep; + state->player_health -= damage_to_player(state, damage); + state->achievements[ACH_WAKE_UP] = state->achievements[ACH_WAKE_UP] || state->is_sleeping; + state->is_sleeping = false; + state->is_resting = false; + } + int new_cooldown = attacking ? 5 : cooldown - 1; + bool valid = valid_typed_mob_position(state, level, MOB_MELEE, type_id, + proposed_row, proposed_col, old_row, old_col); + int new_row = valid ? proposed_row : old_row; + int new_col = valid ? proposed_col : old_col; + bool keep = alive && (dist < MOB_DESPAWN_DISTANCE || fighting_boss(state)); + Rng unused; + rng_split(*rng, &unused, rng); + + move_mob_occupancy(state, level, old_row, old_col, new_row, new_col, keep); + mobs->position[slot][0] = new_row; + mobs->position[slot][1] = new_col; + mobs->attack_cooldown[slot] = new_cooldown; + mobs->mask[slot] = keep; +} + +void move_passive_slot(State* state, int level, int slot, Rng* rng) { + Mobs* mobs = &state->passive_mobs[level]; + bool alive = mobs->mask[slot]; + int old_row = mobs->position[slot][0]; + int old_col = mobs->position[slot][1]; + int type_id = mobs->type_id[slot]; + int direction[2]; + choose_direction(rng_key(rng), 8, direction); + int proposed_row = old_row + direction[0]; + int proposed_col = old_col + direction[1]; + bool valid = valid_typed_mob_position(state, level, MOB_PASSIVE, type_id, + proposed_row, proposed_col, old_row, old_col); + int new_row = valid ? proposed_row : old_row; + int new_col = valid ? proposed_col : old_col; + int dist = abs(state->player_position[0] - old_row) + abs(state->player_position[1] - old_col); + bool keep = alive && dist < MOB_DESPAWN_DISTANCE; + move_mob_occupancy(state, level, old_row, old_col, new_row, new_col, keep); + mobs->position[slot][0] = new_row; + mobs->position[slot][1] = new_col; + mobs->mask[slot] = keep; +} + +void move_ranged_slot(State* state, int level, int slot, Rng* rng) { + Mobs* mobs = &state->ranged_mobs[level]; + bool alive = mobs->mask[slot]; + int old_row = mobs->position[slot][0]; + int old_col = mobs->position[slot][1]; + int type_id = mobs->type_id[slot]; + int cooldown = mobs->attack_cooldown[slot]; + + int random_dir[2]; + choose_direction(rng_key(rng), 4, random_dir); + int distance_row = abs(state->player_position[0] - old_row); + int distance_col = abs(state->player_position[1] - old_col); + int axis = choose_player_axis(rng_key(rng), distance_row, distance_col); + int player_dir[2] = {0, 0}; + if (axis == 0) { + player_dir[0] = signi(state->player_position[0] - old_row); + } else { + player_dir[1] = signi(state->player_position[1] - old_col); + } + int dist = distance_row + distance_col; + int proposed_row = dist >= 6 ? old_row + player_dir[0] : old_row + random_dir[0]; + int proposed_col = dist >= 6 ? old_col + player_dir[1] : old_col + random_dir[1]; + if (dist <= 3) { + proposed_row = old_row - player_dir[0]; + proposed_col = old_col - player_dir[1]; + } + if (rng_f32(rng_key(rng), 0) <= 0.85f) { + proposed_row = old_row + random_dir[0]; + proposed_col = old_col + random_dir[1]; + } + bool valid = valid_typed_mob_position(state, level, MOB_RANGED, type_id, + proposed_row, proposed_col, old_row, old_col); + bool attacking = ((dist >= 4 && dist <= 5) || (dist <= 3 && !valid)) && cooldown <= 0 && alive; + if (attacking) { + static const int projectile[8] = { + PROJECTILE_ARROW, PROJECTILE_ARROW, PROJECTILE_FIREBALL, PROJECTILE_DAGGER, + PROJECTILE_ARROW2, PROJECTILE_SLIMEBALL, PROJECTILE_FIREBALL2, PROJECTILE_ICEBALL2 + }; + spawn_projectile(state, false, projectile[clampi(type_id, 0, 7)], + old_row, old_col, player_dir[0], player_dir[1]); + proposed_row = old_row; + proposed_col = old_col; + } + int new_cooldown = attacking ? 4 : cooldown - 1; + valid = valid_typed_mob_position(state, level, MOB_RANGED, type_id, + proposed_row, proposed_col, old_row, old_col); + int new_row = valid ? proposed_row : old_row; + int new_col = valid ? proposed_col : old_col; + bool keep = alive && (dist < MOB_DESPAWN_DISTANCE || fighting_boss(state)); + move_mob_occupancy(state, level, old_row, old_col, new_row, new_col, keep); + mobs->position[slot][0] = new_row; + mobs->position[slot][1] = new_col; + mobs->attack_cooldown[slot] = new_cooldown; + mobs->mask[slot] = keep; +} + +int choose_weighted_key(Rng key, const float* weights, int count) { + float total = 0.0f; + for (int i = 0; i < count; i++) { + total += weights[i]; + } + float draw = total * (1.0f - rng_f32(key, 0)); + float cumulative = 0.0f; + for (int i = 0; i < count; i++) { + cumulative += weights[i]; + if (cumulative >= draw) { + return i; + } + } + return count - 1; +} + +void compute_action_mask(Craftax* env) { + unsigned char* m = env->agents[0].action_mask; + if (m == NULL) { + return; + } + if (!env->use_action_mask) { + memset(m, 1, ATN_DIM); + return; + } + const State* s = &env->state; + const Inventory* inv = &s->inventory; + memset(m, 0, ATN_DIM); + m[ACTION_NOOP] = 1; + if (s->is_sleeping || s->is_resting) { + return; + } + m[ACTION_LEFT] = m[ACTION_RIGHT] = m[ACTION_UP] = m[ACTION_DOWN] = m[ACTION_DO] = 1; + m[ACTION_SLEEP] = s->player_energy < max_energy(s); + m[ACTION_REST] = s->player_health < max_health(s); + m[ACTION_PLACE_STONE] = m[ACTION_PLACE_FURNACE] = inv->stone > 0; + m[ACTION_PLACE_TABLE] = inv->wood >= 2; + m[ACTION_PLACE_PLANT] = inv->sapling > 0; + m[ACTION_PLACE_TORCH] = inv->torches > 0; + m[ACTION_MAKE_WOOD_PICKAXE] = inv->wood > 0 && inv->pickaxe < 1; + m[ACTION_MAKE_STONE_PICKAXE] = inv->wood > 0 && inv->stone > 0 && inv->pickaxe < 2; + m[ACTION_MAKE_IRON_PICKAXE] = inv->wood > 0 && inv->stone > 0 && inv->iron > 0 + && inv->coal > 0 && inv->pickaxe < 3; + m[ACTION_MAKE_DIAMOND_PICKAXE] = inv->wood > 0 && inv->diamond >= 3 && inv->pickaxe < 4; + m[ACTION_MAKE_WOOD_SWORD] = inv->wood > 0 && inv->sword < 1; + m[ACTION_MAKE_STONE_SWORD] = inv->wood > 0 && inv->stone > 0 && inv->sword < 2; + m[ACTION_MAKE_IRON_SWORD] = inv->wood > 0 && inv->stone > 0 && inv->iron > 0 + && inv->coal > 0 && inv->sword < 3; + m[ACTION_MAKE_DIAMOND_SWORD] = inv->wood > 0 && inv->diamond >= 2 && inv->sword < 4; + m[ACTION_MAKE_ARROW] = inv->wood > 0 && inv->stone > 0 && inv->arrows < 99; + m[ACTION_MAKE_TORCH] = inv->wood > 0 && inv->coal > 0 && inv->torches < 99; + int missing_iron = 0; + int missing_diamond = 0; + int armour = 0; + for (int k = 0; k < 4; k++) { + missing_iron += inv->armour[k] < 1; + missing_diamond += inv->armour[k] < 2; + armour += inv->armour[k]; + } + m[ACTION_MAKE_IRON_ARMOUR] = missing_iron && inv->iron >= 3 && inv->coal >= 3; + m[ACTION_MAKE_DIAMOND_ARMOUR] = missing_diamond && inv->diamond >= 3; + int item = s->item_map[s->player_level][s->player_position[0]][s->player_position[1]]; + m[ACTION_DESCEND] = item == ITEM_LADDER_DOWN + && s->monsters_killed[s->player_level] >= MONSTERS_KILLED_TO_CLEAR_LEVEL + && s->player_level < NUM_LEVELS - 1; + m[ACTION_ASCEND] = item == ITEM_LADDER_UP && s->player_level > 0; + m[ACTION_SHOOT_ARROW] = inv->bow > 0 && inv->arrows > 0; + m[ACTION_CAST_FIREBALL] = s->learned_spells[0] && s->player_mana >= 2; + m[ACTION_CAST_ICEBALL] = s->learned_spells[1] && s->player_mana >= 2; + for (int k = 0; k < NUM_POTIONS; k++) { + m[ACTION_DRINK_POTION_RED + k] = inv->potions[k] > 0; + } + m[ACTION_READ_BOOK] = inv->books > 0; + int enchant = s->player_mana >= 9 && (inv->ruby > 0 || inv->sapphire > 0); + m[ACTION_ENCHANT_SWORD] = enchant && inv->sword > 0; + m[ACTION_ENCHANT_ARMOUR] = enchant && armour > 0; + m[ACTION_ENCHANT_BOW] = enchant && inv->bow > 0; + m[ACTION_LEVEL_UP_DEXTERITY] = s->player_xp > 0 && s->player_dexterity < MAX_ATTRIBUTE; + m[ACTION_LEVEL_UP_STRENGTH] = s->player_xp > 0 && s->player_strength < MAX_ATTRIBUTE; + m[ACTION_LEVEL_UP_INTELLIGENCE] = s->player_xp > 0 && s->player_intelligence < MAX_ATTRIBUTE; +} + +void compute_observations(Craftax* env) { + State* state = &env->state; + float* obs = env->agents[0].observations; + const int map_obs = OBS_ROWS * OBS_COLS * OBS_TILE_CHANNELS; + memset(obs, 0, map_obs * sizeof(float)); + + int level = state->player_level; + int row = state->player_position[0]; + int col = state->player_position[1]; + int row_radius = OBS_ROWS / 2; + int col_radius = OBS_COLS / 2; + int r0 = clampi(-row, -row_radius, row_radius); + int r1 = clampi(MAP_SIZE - 1 - row, -row_radius, row_radius); + int c0 = clampi(-col, -col_radius, col_radius); + int c1 = clampi(MAP_SIZE - 1 - col, -col_radius, col_radius); + + // Add map information + for (int r = r0; r <= r1; r++) { + int obs_row = row + r; + uint8_t* map_row = state->map[level][obs_row]; + uint8_t* item_row = state->item_map[level][obs_row]; + uint8_t* light_row = state->light_map[level][obs_row]; + float* tile = obs + ((r + row_radius) * OBS_COLS + (c0 + col_radius)) * OBS_TILE_CHANNELS; + for (int c = c0; c <= c1; c++) { + int obs_col = col + c; + if (light_row[obs_col] > VISIBLE_LIGHT_THRESHOLD) { + tile[0] = map_row[obs_col]; + tile[1] = item_row[obs_col] + 1; + tile[2] = 1.0f; + } + tile += OBS_TILE_CHANNELS; + } + } + // Add mob information + write_mob_obs(obs, state, &state->melee_mobs[level], MAX_MELEE_MOBS, 0); + write_mob_obs(obs, state, &state->passive_mobs[level], MAX_PASSIVE_MOBS, 1); + write_mob_obs(obs, state, &state->ranged_mobs[level], MAX_RANGED_MOBS, 2); + write_mob_obs(obs, state, &state->mob_projectiles[level], MAX_MOB_PROJECTILES, 3); + write_mob_obs(obs, state, &state->player_projectiles[level], MAX_PLAYER_PROJECTILES, 4); + + int obs_idx = map_obs; + + // Inventory and player stats + obs[obs_idx++] = sqrtf(state->inventory.wood) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.stone) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.coal) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.iron) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.diamond) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.sapphire) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.ruby) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.sapling) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.torches) / 10.0f; + obs[obs_idx++] = sqrtf(state->inventory.arrows) / 10.0f; + obs[obs_idx++] = state->inventory.books / 2.0f; + obs[obs_idx++] = state->inventory.pickaxe / 4.0f; + obs[obs_idx++] = state->inventory.sword / 4.0f; + obs[obs_idx++] = state->sword_enchantment; + obs[obs_idx++] = state->bow_enchantment; + obs[obs_idx++] = state->inventory.bow; + for (int i = 0; i < NUM_POTIONS; i++) { + obs[obs_idx++] = sqrtf(state->inventory.potions[i]) / 10.0f; + } + + obs[obs_idx++] = state->player_health / 10.0f; + obs[obs_idx++] = state->player_food / 10.0f; + obs[obs_idx++] = state->player_drink / 10.0f; + obs[obs_idx++] = state->player_energy / 10.0f; + obs[obs_idx++] = state->player_mana / 10.0f; + obs[obs_idx++] = state->player_xp / 10.0f; + obs[obs_idx++] = state->player_dexterity / 10.0f; + obs[obs_idx++] = state->player_strength / 10.0f; + obs[obs_idx++] = state->player_intelligence / 10.0f; + + int direction_index = state->player_direction - ACTION_LEFT; + for (int i = 0; i < 4; i++) { + obs[obs_idx++] = i == direction_index ? 1.0f : 0.0f; + } + for (int i = 0; i < 4; i++) { + obs[obs_idx++] = state->inventory.armour[i] / 2.0f; + } + for (int i = 0; i < 4; i++) { + obs[obs_idx++] = state->armour_enchantments[i]; + } + + obs[obs_idx++] = state->light_level; + obs[obs_idx++] = state->is_sleeping ? 1.0f : 0.0f; + obs[obs_idx++] = state->is_resting ? 1.0f : 0.0f; + obs[obs_idx++] = state->learned_spells[0] ? 1.0f : 0.0f; + obs[obs_idx++] = state->learned_spells[1] ? 1.0f : 0.0f; + obs[obs_idx++] = state->player_level / 10.0f; + obs[obs_idx++] = state->monsters_killed[level] >= MONSTERS_KILLED_TO_CLEAR_LEVEL ? 1.0f : 0.0f; + obs[obs_idx++] = boss_vulnerable(state) ? 1.0f : 0.0f; + + compute_action_mask(env); +} + +void update_log_state(Craftax* env) { + if (env->state.player_level > env->max_floor_accum) { + env->max_floor_accum = env->state.player_level; + } +} + +static int key_to_action(void) { + static const int map[][2] = { + {KEY_Q, ACTION_NOOP}, + {KEY_W, ACTION_UP}, + {KEY_UP, ACTION_UP}, + {KEY_D, ACTION_RIGHT}, + {KEY_RIGHT, ACTION_RIGHT}, + {KEY_S, ACTION_DOWN}, + {KEY_DOWN, ACTION_DOWN}, + {KEY_A, ACTION_LEFT}, + {KEY_LEFT, ACTION_LEFT}, + {KEY_SPACE, ACTION_DO}, + {KEY_ONE, ACTION_MAKE_WOOD_PICKAXE}, + {KEY_TWO, ACTION_MAKE_STONE_PICKAXE}, + {KEY_THREE, ACTION_MAKE_IRON_PICKAXE}, + {KEY_FOUR, ACTION_MAKE_DIAMOND_PICKAXE}, + {KEY_FIVE, ACTION_MAKE_WOOD_SWORD}, + {KEY_SIX, ACTION_MAKE_STONE_SWORD}, + {KEY_SEVEN, ACTION_MAKE_IRON_SWORD}, + {KEY_EIGHT, ACTION_MAKE_DIAMOND_SWORD}, + {KEY_T, ACTION_PLACE_TABLE}, + {KEY_TAB, ACTION_SLEEP}, + {KEY_R, ACTION_PLACE_STONE}, + {KEY_F, ACTION_PLACE_FURNACE}, + {KEY_P, ACTION_PLACE_PLANT}, + {KEY_E, ACTION_REST}, + {KEY_COMMA, ACTION_ASCEND}, + {KEY_PERIOD, ACTION_DESCEND}, + {KEY_Y, ACTION_MAKE_IRON_ARMOUR}, + {KEY_U, ACTION_MAKE_DIAMOND_ARMOUR}, + {KEY_I, ACTION_SHOOT_ARROW}, + {KEY_O, ACTION_MAKE_ARROW}, + {KEY_G, ACTION_CAST_FIREBALL}, + {KEY_H, ACTION_CAST_ICEBALL}, + {KEY_J, ACTION_PLACE_TORCH}, + {KEY_Z, ACTION_DRINK_POTION_RED}, + {KEY_X, ACTION_DRINK_POTION_GREEN}, + {KEY_C, ACTION_DRINK_POTION_BLUE}, + {KEY_V, ACTION_DRINK_POTION_PINK}, + {KEY_B, ACTION_DRINK_POTION_CYAN}, + {KEY_N, ACTION_DRINK_POTION_YELLOW}, + {KEY_M, ACTION_READ_BOOK}, + {KEY_K, ACTION_ENCHANT_SWORD}, + {KEY_L, ACTION_ENCHANT_ARMOUR}, + {KEY_LEFT_BRACKET, ACTION_MAKE_TORCH}, + {KEY_RIGHT_BRACKET, ACTION_LEVEL_UP_DEXTERITY}, + {KEY_MINUS, ACTION_LEVEL_UP_STRENGTH}, + {KEY_EQUAL, ACTION_LEVEL_UP_INTELLIGENCE}, + {KEY_SEMICOLON, ACTION_ENCHANT_BOW}, + }; + for (int i = 0; i < (int)(sizeof(map) / sizeof(map[0])); i++) { + if (IsKeyPressed(map[i][0])) { + return map[i][1]; + } + } + return -1; +} + +// Shift + action-panel key. 1 = applied, 0 = policy, -1 = skip tick. +static int got_human_input(Craftax* env) { + int shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); + if (!IsWindowReady() || !shift + || env->state.is_sleeping || env->state.is_resting) { + return 0; + } + int action = key_to_action(); + if (action < 0) { + return -1; + } + env->agents[0].actions[0] = (float)action; + return 1; +} + +void puf_reset(Craftax* env) { + env->agents[0].rewards[0] = 0.0f; + env->agents[0].terminals[0] = 0.0f; + env->episode_return_accum = 0.0f; + env->episode_length_accum = 0; + env->max_floor_accum = 0; + memset(env->achievements, 0, sizeof(env->achievements)); + + Rng initial = rng_seed((uint32_t)env->seed); + if (env->reset_pool_size > 0) { + Rng discard; + rng_split(initial, &env->env_rng, &discard); + int idx = env->seed % env->reset_pool_size; + memcpy(&env->state, &env->reset_pool[idx], sizeof(State)); + } else { + Rng reset_key; + rng_split(initial, &env->env_rng, &reset_key); + Rng unused; + Rng world_key; + rng_split(reset_key, &unused, &world_key); + generate_world_from_key(&env->state, world_key); + } + compute_observations(env); + update_log_state(env); +} + +void puf_step(Craftax* env) { + if (got_human_input(env) < 0) { + return; + } + env->agents[0].rewards[0] = 0.0f; + env->agents[0].terminals[0] = 0.0f; + int action = env->agents[0].actions[0]; + + State* state = &env->state; + int initial_achievements[NUM_ACHIEVEMENTS]; + memcpy(initial_achievements, state->achievements, sizeof(initial_achievements)); + + int initial_armour = equipped_armour(state); + // float initial_health = state->player_health; + + // Sleep/rest used to return control every tick as forced NOOPs (~100 + // agent steps). Collapse those ticks into this one puf_step so credit + // assignment sees a single action that ends on wake, hit, or death. + Rng reset_key = 0; + bool done = false; + do { + Rng step_key; + rng_split(env->env_rng, &env->env_rng, &step_key); + Rng step_rng; + rng_split(step_key, &step_rng, &reset_key); + + if (state->is_sleeping || state->is_resting) { + action = ACTION_NOOP; + } + + int level = state->player_level; + int row = state->player_position[0]; + int col = state->player_position[1]; + + // Ascend/descend ladders if possible, otherwise stay on the same level. + bool on_down_ladder = state->item_map[level][row][col] == ITEM_LADDER_DOWN; + bool on_up_ladder = state->item_map[level][row][col] == ITEM_LADDER_UP; + + bool can_move_down = action == ACTION_DESCEND + && on_down_ladder + && state->monsters_killed[level] >= MONSTERS_KILLED_TO_CLEAR_LEVEL + && level < NUM_LEVELS - 1; + + + bool can_move_up = action == ACTION_ASCEND && on_up_ladder && level > 0; + + if (can_move_down || can_move_up) { + int new_level = level + (can_move_down ? 1 : -1); + if (can_move_down) { + state->player_position[0] = state->up_ladders[new_level][0]; + state->player_position[1] = state->up_ladders[new_level][1]; + } else { + state->player_position[0] = state->down_ladders[new_level][0]; + state->player_position[1] = state->down_ladders[new_level][1]; + } + state->player_level = new_level; + static const int floor_ach[NUM_LEVELS] = { + -1, ACH_ENTER_DUNGEON, ACH_ENTER_GNOMISH_MINES, ACH_ENTER_SEWERS, + ACH_ENTER_VAULT, ACH_ENTER_TROLL_MINES, ACH_ENTER_FIRE_REALM, + ACH_ENTER_ICE_REALM, ACH_ENTER_GRAVEYARD, + }; + int achievement = floor_ach[new_level]; + if (achievement >= 0 && !state->achievements[achievement]) { + state->achievements[achievement] = 1; + state->player_xp += 1; + } + } + + // Crafting actions require a crafting table and/or furnace nearby. + static const int nearby_tiles[8][2] = { + {0, -1}, {0, 1}, {-1, 0}, {1, 0}, + {-1, -1}, {-1, 1}, {1, -1}, {1, 1}, + }; + level = state->player_level; + bool at_table = false; + bool at_furnace = false; + for (int i = 0; i < 8; i++) { + int row = state->player_position[0] + nearby_tiles[i][0]; + int col = state->player_position[1] + nearby_tiles[i][1]; + if (row < 0 || row >= MAP_SIZE || col < 0 || col >= MAP_SIZE) { + continue; + } + at_table = at_table || state->map[level][row][col] == BLOCK_CRAFTING_TABLE; + at_furnace = at_furnace || state->map[level][row][col] == BLOCK_FURNACE; + } + + Inventory* inv = &state->inventory; + + if (action == ACTION_MAKE_WOOD_PICKAXE + && at_table && inv->wood >= 1 + && inv->pickaxe < 1 + ) { + inv->wood -= 1; + inv->pickaxe = 1; + } else if (action == ACTION_MAKE_STONE_PICKAXE + && at_table && inv->wood >= 1 + && inv->stone >= 1 + && inv->pickaxe < 2) { + inv->wood -= 1; + inv->stone -= 1; + inv->pickaxe = 2; + } else if (action == ACTION_MAKE_IRON_PICKAXE + && at_table + && at_furnace + && inv->wood >= 1 + && inv->stone >= 1 + && inv->iron >= 1 + && inv->coal >= 1 + && inv->pickaxe < 3 + ) { + inv->wood -= 1; + inv->stone -= 1; + inv->iron -= 1; + inv->coal -= 1; + inv->pickaxe = 3; + } else if (action == ACTION_MAKE_DIAMOND_PICKAXE + && at_table && inv->wood >= 1 + && inv->diamond >= 3 + && inv->pickaxe < 4 + ) { + inv->wood -= 1; + inv->diamond -= 3; + inv->pickaxe = 4; + } else if (action == ACTION_MAKE_WOOD_SWORD + && at_table && inv->wood >= 1 + && inv->sword < 1 + ) { + inv->wood -= 1; + inv->sword = 1; + } else if (action == ACTION_MAKE_STONE_SWORD + && at_table && inv->wood >= 1 + && inv->stone >= 1 + && inv->sword < 2 + ) { + inv->wood -= 1; + inv->stone -= 1; + inv->sword = 2; + } else if (action == ACTION_MAKE_IRON_SWORD + && at_table && at_furnace + && inv->wood >= 1 && inv->stone >= 1 && inv->iron >= 1 + && inv->coal >= 1 && inv->sword < 3 + ) { + inv->wood -= 1; + inv->stone -= 1; + inv->iron -= 1; + inv->coal -= 1; + inv->sword = 3; + } else if (action == ACTION_MAKE_DIAMOND_SWORD + && at_table && inv->wood >= 1 + && inv->diamond >= 2 && inv->sword < 4 + ) { + inv->wood -= 1; + inv->diamond -= 2; + inv->sword = 4; + } else if (action == ACTION_MAKE_ARROW + && at_table && inv->wood >= 1 + && inv->stone >= 1 && inv->arrows < 99 + ) { + inv->wood -= 1; + inv->stone -= 1; + inv->arrows += 2; + } else if (action == ACTION_MAKE_TORCH + && at_table && inv->wood >= 1 + && inv->coal >= 1 && inv->torches < 99 + ) { + inv->wood -= 1; + inv->coal -= 1; + inv->torches += 4; + } else if (action == ACTION_MAKE_IRON_ARMOUR + && at_table + && at_furnace + && inv->iron >= 3 + && inv->coal >= 3 + ) { + for (int i = 0; i < 4; i++) { + if (inv->armour[i] < 1) { + inv->iron -= 3; + inv->coal -= 3; + inv->armour[i] = 1; + state->achievements[ACH_MAKE_IRON_ARMOUR] = 1; + break; + } + } + } else if (action == ACTION_MAKE_DIAMOND_ARMOUR + && at_table + && inv->diamond >= 3 + ) { + for (int i = 0; i < 4; i++) { + if (inv->armour[i] < 2) { + inv->diamond -= 3; + inv->armour[i] = 2; + state->achievements[ACH_MAKE_DIAMOND_ARMOUR] = 1; + break; + } + } + } + + Rng interact_rng = rng_key(&step_rng); + int direction[2]; + action_to_direction(state->player_direction, direction); + row = state->player_position[0] + direction[0]; + col = state->player_position[1] + direction[1]; + bool in_bounds = (unsigned)row < MAP_SIZE && (unsigned)col < MAP_SIZE; + level = state->player_level; + inv = &state->inventory; + + if (action == ACTION_DO) { + bool did_attack = false; + int attack_class; + int attack_slot; + if (find_mob_at(state, level, row, col, &attack_class, &attack_slot)) { + // Compute damage based on sword level and player stats. + Mobs* attack_mobs = mobs_for_class(state, level, attack_class); + static const float base_damage[5] = {1, 2, 3, 5, 8}; + float base = base_damage[clampi(inv->sword, 0, 4)]; + float physical = base * (1.0f + 0.25f * (state->player_strength - 1)); + float magic = base * 0.5f * (1.0f + 0.05f * (state->player_intelligence - 1)); + Damage vector = { + physical, + state->sword_enchantment == 1 ? magic : 0, + state->sword_enchantment == 2 ? magic : 0, + }; + did_attack = damage_mob_at( + state, level, row, col, + damage_to_mob(vector, attack_mobs->type_id[attack_slot], attack_class), true, true); + } + Rng sapling_key = rng_key(&interact_rng); + Rng chest_key = rng_key(&interact_rng); + if (!did_attack && in_bounds) { + int block = state->map[level][row][col]; + + if (block == BLOCK_TREE || block == BLOCK_FIRE_TREE || block == BLOCK_ICE_SHRUB) { + int ground = BLOCK_GRASS; + if (block == BLOCK_FIRE_TREE) { + ground = BLOCK_FIRE_GRASS; + } else if (block == BLOCK_ICE_SHRUB) { + ground = BLOCK_ICE_GRASS; + } + set_block(state, level, row, col, ground); + inv->wood += 1; + } else if (block == BLOCK_STONE && inv->pickaxe >= 1) { + set_block(state, level, row, col, BLOCK_PATH); + inv->stone += 1; + } else if (block == BLOCK_COAL && inv->pickaxe >= 1) { + set_block(state, level, row, col, BLOCK_PATH); + inv->coal += 1; + } else if (block == BLOCK_IRON && inv->pickaxe >= 2) { + set_block(state, level, row, col, BLOCK_PATH); + inv->iron += 1; + } else if (block == BLOCK_DIAMOND && inv->pickaxe >= 3) { + set_block(state, level, row, col, BLOCK_PATH); + inv->diamond += 1; + } else if (block == BLOCK_SAPPHIRE && inv->pickaxe >= 4) { + set_block(state, level, row, col, BLOCK_PATH); + inv->sapphire += 1; + } else if (block == BLOCK_RUBY && inv->pickaxe >= 4) { + set_block(state, level, row, col, BLOCK_PATH); + inv->ruby += 1; + } else if (block == BLOCK_STALAGMITE && inv->pickaxe >= 1) { + set_block(state, level, row, col, BLOCK_PATH); + inv->stone += 1; + } else if (block == BLOCK_CRAFTING_TABLE || block == BLOCK_FURNACE) { + set_block(state, level, row, col, BLOCK_PATH); + } else if (block == BLOCK_WATER || block == BLOCK_FOUNTAIN) { + state->player_drink = clampi(state->player_drink + 1, 0, max_drink(state)); + state->player_thirst = 0.0f; + state->achievements[ACH_COLLECT_DRINK] = 1; + } else if (block == BLOCK_RIPE_PLANT) { + set_block(state, level, row, col, BLOCK_PLANT); + for (int i = 0; i < MAX_GROWING_PLANTS; i++) { + if (state->growing_plants_pos[i][0] == row && state->growing_plants_pos[i][1] == col) { + state->growing_plants_age[i] = 0; + break; + } + } + state->player_food = clampi(state->player_food + 4, 0, max_food(state)); + state->player_hunger = 0.0f; + state->achievements[ACH_EAT_PLANT] = 1; + } else if (block == BLOCK_CHEST) { + set_block(state, level, row, col, BLOCK_PATH); + Rng chest_rng = chest_key; + rng_key(&chest_rng); + randint(rng_key(&chest_rng), 0u, 1, 6); + bool torch = rng_f32(rng_key(&chest_rng), 0) < 0.6f; + int torches = randint(rng_key(&chest_rng), 0u, 4, 8); + bool ore = rng_f32(rng_key(&chest_rng), 0) < 0.6f; + float ore_weights[5] = {0.3f, 0.3f, 0.15f, 0.125f, 0.125f}; + int ore_id = choose_weighted_key(rng_key(&chest_rng), ore_weights, 5); + Rng amount_key = rng_key(&chest_rng); + int ore_amt[5] = { + randint(amount_key, 0u, 1, 4), + randint(amount_key, 0u, 1, 3), + randint(amount_key, 0u, 1, 2), + randint(amount_key, 0u, 1, 2), + randint(amount_key, 0u, 1, 2), + }; + bool potion = rng_f32(rng_key(&chest_rng), 0) < 0.5f; + int potion_id = randint(rng_key(&chest_rng), 0u, 0, 6); + int potion_amount = randint(rng_key(&chest_rng), 0u, 1, 3); + bool arrows = rng_f32(rng_key(&chest_rng), 0) < 0.25f; + int arrow_amount = randint(rng_key(&chest_rng), 0u, 1, 5); + bool tool = rng_f32(rng_key(&chest_rng), 0) < 0.2f; + int tool_id = randint(rng_key(&chest_rng), 0u, 0, 2); + float tool_weights[4] = {0.4f, 0.3f, 0.2f, 0.1f}; + int pickaxe = choose_weighted_key(rng_key(&chest_rng), tool_weights, 4) + 1; + int sword = choose_weighted_key(rng_key(&chest_rng), tool_weights, 4) + 1; + int* ore_inv[5] = { + &inv->coal, &inv->iron, &inv->diamond, &inv->sapphire, &inv->ruby, + }; + inv->torches += torch * torches; + if (ore) { + *ore_inv[ore_id] += ore_amt[ore_id]; + } + inv->potions[potion_id] += potion * potion_amount; + inv->arrows += arrows * arrow_amount; + if (tool && tool_id == 0 && pickaxe > inv->pickaxe) { + inv->pickaxe = pickaxe; + } + if (tool && tool_id == 1 && sword > inv->sword) { + inv->sword = sword; + } + if (!state->chests_opened[level]) { + if (level == 1) { + inv->bow = 1; + } + if (level == 3 || level == 4) { + inv->books += 1; + } + } + state->achievements[ACH_OPEN_CHEST] = 1; + } else if (block == BLOCK_NECROMANCER && boss_vulnerable(state) && fighting_boss(state)) { + state->boss_progress += 1; + state->boss_timestep_to_spawn_this_round = BOSS_SPAWN_TURNS; + state->achievements[ACH_DAMAGE_NECROMANCER] = 1; + } + if (block == BLOCK_GRASS && rng_f32(sapling_key, 0) < 0.1f) { + inv->sapling += 1; + } + state->chests_opened[level] |= block == BLOCK_CHEST; + } + } + + if (in_bounds) { + int block = state->map[level][row][col]; + bool occupied = is_solid_block(block) || state->item_map[level][row][col] != ITEM_NONE + || mob_at(state, level, row, col); + + if (action == ACTION_PLACE_TABLE && !occupied && inv->wood >= 2) { + set_block(state, level, row, col, BLOCK_CRAFTING_TABLE); + inv->wood -= 2; + state->achievements[ACH_PLACE_TABLE] = 1; + } else if (action == ACTION_PLACE_FURNACE && !occupied && inv->stone >= 1) { + set_block(state, level, row, col, BLOCK_FURNACE); + inv->stone -= 1; + state->achievements[ACH_PLACE_FURNACE] = 1; + } else if (action == ACTION_PLACE_STONE + && (block == BLOCK_WATER || !occupied) && inv->stone >= 1) { + set_block(state, level, row, col, BLOCK_STONE); + inv->stone -= 1; + state->achievements[ACH_PLACE_STONE] = 1; + } else if (action == ACTION_PLACE_TORCH + && (block == BLOCK_GRASS || block == BLOCK_SAND || block == BLOCK_PATH + || block == BLOCK_FIRE_GRASS || block == BLOCK_ICE_GRASS) + && state->item_map[level][row][col] == ITEM_NONE + && inv->torches >= 1) { + state->item_map[level][row][col] = ITEM_TORCH; + for (int dr = -4; dr <= 4; dr++) { + int light_row = row + dr; + if ((unsigned)light_row >= MAP_SIZE) { + continue; + } + for (int dc = -4; dc <= 4; dc++) { + int light_col = col + dc; + if ((unsigned)light_col >= MAP_SIZE) { + continue; + } + float torch = 1.0f - sqrtf(dr * dr + dc * dc) / 5.0f; + if (torch < 0.0f) { + torch = 0.0f; + } + float light = state->light_map[level][light_row][light_col] / 255.0f + torch; + if (light > 1.0f) { + light = 1.0f; + } + state->light_map[level][light_row][light_col] = (unsigned char)(light * 255.0f); + } + } + inv->torches -= 1; + state->achievements[ACH_PLACE_TORCH] = 1; + } else if (action == ACTION_PLACE_PLANT && block == BLOCK_GRASS + && state->item_map[level][row][col] == ITEM_NONE + && inv->sapling >= 1) { + set_block(state, level, row, col, BLOCK_PLANT); + inv->sapling -= 1; + for (int i = 0; i < MAX_GROWING_PLANTS; i++) { + if (!state->growing_plants_mask[i]) { + state->growing_plants_pos[i][0] = row; + state->growing_plants_pos[i][1] = col; + state->growing_plants_age[i] = 0; + state->growing_plants_mask[i] = 1; + break; + } + } + state->achievements[ACH_PLACE_PLANT] = 1; + } + } + + int fire_row = direction[0]; + int fire_col = direction[1]; + if (fire_row == 0 && fire_col == 0) { + fire_row = 1; + } + int prow = state->player_position[0]; + int pcol = state->player_position[1]; + if (action == ACTION_SHOOT_ARROW && inv->bow > 0 && inv->arrows > 0) { + bool fired = spawn_projectile(state, true, PROJECTILE_ARROW2, + prow, pcol, fire_row, fire_col); + if (fired) { + inv->arrows -= 1; + state->achievements[ACH_FIRE_BOW] = 1; + } + } else if (action == ACTION_CAST_FIREBALL && state->learned_spells[0] + && state->player_mana >= 2) { + bool cast = spawn_projectile(state, true, PROJECTILE_FIREBALL, + prow, pcol, fire_row, fire_col); + if (cast) { + state->player_mana -= 2; + state->achievements[ACH_CAST_FIREBALL] = 1; + } + } else if (action == ACTION_CAST_ICEBALL && state->learned_spells[1] + && state->player_mana >= 2) { + bool cast = spawn_projectile(state, true, PROJECTILE_ICEBALL, + prow, pcol, fire_row, fire_col); + if (cast) { + state->player_mana -= 2; + state->achievements[ACH_CAST_ICEBALL] = 1; + } + } + + int potion = action - ACTION_DRINK_POTION_RED; + if (potion >= 0 && potion < NUM_POTIONS && inv->potions[potion] > 0) { + int effect = state->potion_mapping[potion]; + inv->potions[potion] -= 1; + if (effect == 0) { + state->player_health += 8.0f; + } else if (effect == 1) { + state->player_health -= 3.0f; + } else if (effect == 2) { + state->player_mana += 8; + } else if (effect == 3) { + state->player_mana -= 3; + } else if (effect == 4) { + state->player_energy += 8; + } else { + state->player_energy -= 3; + } + state->achievements[ACH_DRINK_POTION] = 1; + } + + Rng book_rng = rng_key(&step_rng); + + bool reading = action == ACTION_READ_BOOK && inv->books > 0; + Rng unused; + Rng choice_key; + rng_split(book_rng, &unused, &choice_key); + float p0 = state->learned_spells[0] ? 0.0f : 1.0f; + float p1 = state->learned_spells[1] ? 0.0f : 1.0f; + int spell = 0; + if (p0 + p1 != 0.0f) { + float r = 1.0f - rng_f32(choice_key, 0); + spell = r <= (p0 / (p0 + p1)) ? 0 : 1; + } + if (reading) { + inv->books -= 1; + state->learned_spells[spell] = 1; + state->achievements[spell == 0 ? ACH_LEARN_FIREBALL : ACH_LEARN_ICEBALL] = 1; + } + Rng enchant_rng = rng_key(&step_rng); + + int eblock = 0; + if (in_bounds) { + eblock = state->map[level][row][col]; + } + int enchant = eblock == BLOCK_ENCHANTMENT_TABLE_FIRE ? 1 : + (eblock == BLOCK_ENCHANTMENT_TABLE_ICE ? 2 : 0); + int gems = enchant == 1 ? inv->ruby : inv->sapphire; + bool could = state->player_mana >= 9 && enchant != 0 && gems >= 1; + bool enchanting_sword = could && action == ACTION_ENCHANT_SWORD && inv->sword > 0; + bool enchanting_bow = could && action == ACTION_ENCHANT_BOW && inv->bow > 0; + bool enchanting_armour = could && action == ACTION_ENCHANT_ARMOUR + && equipped_armour(state) > 0; + Rng armour_key = rng_key(&enchant_rng); + int unenchanted = 0; + for (int i = 0; i < 4; i++) { + unenchanted += state->armour_enchantments[i] == 0; + } + float candidates[4]; + for (int i = 0; i < 4; i++) { + bool opposite = state->armour_enchantments[i] != 0 && state->armour_enchantments[i] != enchant; + candidates[i] = (state->armour_enchantments[i] == 0 || (unenchanted == 0 && opposite)) ? 1.0f : 0.0f; + } + int armour_target = choose_weighted_key(armour_key, candidates, 4); + if (enchanting_sword) { + state->sword_enchantment = enchant; + state->achievements[ACH_ENCHANT_SWORD] = 1; + } + if (enchanting_bow) { + state->bow_enchantment = enchant; + } + if (enchanting_armour) { + state->armour_enchantments[armour_target] = enchant; + state->achievements[ACH_ENCHANT_ARMOUR] = 1; + } + bool enchanting = enchanting_sword || enchanting_bow || enchanting_armour; + if (enchanting) { + if (enchant == 1) { + inv->ruby -= 1; + } else { + inv->sapphire -= 1; + } + state->player_mana -= 9; + } + state->achievements[ACH_DEFEAT_NECROMANCER] |= state->boss_progress >= NUM_LEVELS - 1; + if (fighting_boss(state)) { + state->boss_timestep_to_spawn_this_round -= 1; + } + + if (state->player_xp >= 1) { + bool leveled = false; + if (action == ACTION_LEVEL_UP_DEXTERITY && state->player_dexterity < MAX_ATTRIBUTE) { + state->player_dexterity += 1; + leveled = true; + } else if (action == ACTION_LEVEL_UP_STRENGTH && state->player_strength < MAX_ATTRIBUTE) { + state->player_strength += 1; + leveled = true; + } else if (action == ACTION_LEVEL_UP_INTELLIGENCE && state->player_intelligence < MAX_ATTRIBUTE) { + state->player_intelligence += 1; + leveled = true; + } + if (leveled) { + state->player_xp -= 1; + } + } + + action_to_direction(action, direction); + int proposed_row = state->player_position[0] + direction[0]; + int proposed_col = state->player_position[1] + direction[1]; + bool valid = (unsigned)proposed_row < MAP_SIZE + && (unsigned)proposed_col < MAP_SIZE; + if (valid) { + int pblock = state->map[level][proposed_row][proposed_col]; + valid = !is_solid_block(pblock) && pblock != BLOCK_WATER && pblock != BLOCK_LAVA + && !mob_at(state, level, proposed_row, proposed_col); + } + if (valid) { + state->player_position[0] = proposed_row; + state->player_position[1] = proposed_col; + } + if (direction[0] != 0 || direction[1] != 0) { + state->player_direction = action; + } + + Rng mobs_rng = rng_key(&step_rng); + rng_key(&mobs_rng); + move_melee_slot(state, level, 0, &mobs_rng); + move_melee_slot(state, level, 1, &mobs_rng); + move_melee_slot(state, level, 2, &mobs_rng); + rng_key(&mobs_rng); + move_passive_slot(state, level, 0, &mobs_rng); + move_passive_slot(state, level, 1, &mobs_rng); + move_passive_slot(state, level, 2, &mobs_rng); + rng_key(&mobs_rng); + move_ranged_slot(state, level, 0, &mobs_rng); + move_ranged_slot(state, level, 1, &mobs_rng); + rng_key(&mobs_rng); + update_projectile_set(state, false); + rng_key(&mobs_rng); + update_projectile_set(state, true); + + Rng spawn_rng = rng_key(&step_rng); + bool boss = fighting_boss(state); + int coeff = 1 + (state->monsters_killed[level] < MONSTERS_KILLED_TO_CLEAR_LEVEL ? 2 : 0); + if (boss) { + coeff *= (state->boss_timestep_to_spawn_this_round >= 1) ? 1000 : 0; + } + + static const float chances[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}, + }; + + // Mobs despawn if they are too far from the player. This is to prevent mobs from + // accumulating in the distance and slowing down the game. + int despawn_radius = MOB_DESPAWN_DISTANCE * MOB_DESPAWN_DISTANCE; + float night = 1.0f - state->light_level; + float melee_chance = chances[level][1] + chances[level][3] * night * night; + int hostile = boss ? state->boss_progress : level; + + int passive_count; + int passive_slot; + count_and_empty(&state->passive_mobs[level], MAX_PASSIVE_MOBS, + &passive_count, &passive_slot); + Rng passive_prob = rng_key(&spawn_rng); + Rng passive_pos = rng_key(&spawn_rng); + int passive_type = floor_mob_type(level, MOB_PASSIVE); + state->passive_mobs[level].type_id[passive_slot] = passive_type; + + int melee_count; + int melee_slot; + count_and_empty(&state->melee_mobs[level], MAX_MELEE_MOBS, + &melee_count, &melee_slot); + Rng melee_prob = rng_key(&spawn_rng); + Rng melee_pos = rng_key(&spawn_rng); + int melee_type = floor_mob_type(hostile, MOB_MELEE); + state->melee_mobs[level].type_id[melee_slot] = melee_type; + + int ranged_count; + int ranged_slot; + count_and_empty(&state->ranged_mobs[level], MAX_RANGED_MOBS, &ranged_count, &ranged_slot); + Rng ranged_prob = rng_key(&spawn_rng); + Rng ranged_pos = rng_key(&spawn_rng); + int ranged_type = floor_mob_type(hostile, MOB_RANGED); + state->ranged_mobs[level].type_id[ranged_slot] = ranged_type; + + bool try_passive = !boss && passive_count < MAX_PASSIVE_MOBS + && rng_f32(passive_prob, 0) < chances[level][0]; + bool try_melee = melee_count < MAX_MELEE_MOBS + && rng_f32(melee_prob, 0) < melee_chance * coeff; + bool try_ranged = ranged_count < MAX_RANGED_MOBS + && rng_f32(ranged_prob, 0) < chances[level][2] * coeff; + if (try_passive || try_melee || try_ranged) { + int min_hostile = boss ? -1 : 81; + int max_hostile = boss ? 37 : despawn_radius; + int spawn_rows[729]; + int spawn_cols[729]; + int row; + int col; + if (try_passive) { + int n = collect_spawn_cells( + state, level, 9, despawn_radius, + false, false, spawn_rows, spawn_cols); + if (pick_spawn_cell(spawn_rows, spawn_cols, n, passive_pos, &row, &col)) { + spawn_into_slot(state, level, &state->passive_mobs[level], + passive_slot, MOB_PASSIVE, passive_type, row, col); + } + } + if (try_melee) { + int n = collect_spawn_cells( + state, level, min_hostile, max_hostile, + boss, false, spawn_rows, spawn_cols); + if (pick_spawn_cell(spawn_rows, spawn_cols, n, melee_pos, &row, &col)) { + spawn_into_slot(state, level, &state->melee_mobs[level], + melee_slot, MOB_MELEE, melee_type, row, col); + } + } + if (try_ranged) { + int n = collect_spawn_cells( + state, level, min_hostile, max_hostile, + boss, ranged_type == 5, spawn_rows, spawn_cols); + if (pick_spawn_cell(spawn_rows, spawn_cols, n, ranged_pos, &row, &col)) { + spawn_into_slot(state, level, &state->ranged_mobs[level], + ranged_slot, MOB_RANGED, ranged_type, row, col); + } + } + } + + for (int plant = 0; plant < MAX_GROWING_PLANTS; plant++) { + if (!state->growing_plants_mask[plant]) { + continue; + } + + state->growing_plants_age[plant] += 1; + if (state->growing_plants_age[plant] >= 600) { + set_block(state, 0, state->growing_plants_pos[plant][0], + state->growing_plants_pos[plant][1], BLOCK_RIPE_PLANT); + } + } + + bool start_sleep = action == ACTION_SLEEP && state->player_energy < max_energy(state); + state->is_sleeping = state->is_sleeping || start_sleep; + + bool wake_from_sleep = state->is_sleeping && state->player_energy >= max_energy(state); + state->is_sleeping = state->is_sleeping && !wake_from_sleep; + state->achievements[ACH_WAKE_UP] = state->achievements[ACH_WAKE_UP] || wake_from_sleep; + + bool start_rest = action == ACTION_REST && state->player_health < max_health(state); + state->is_resting = state->is_resting || start_rest; + + bool wake_from_rest = state->is_resting && ( + state->player_health >= max_health(state) + || state->player_food <= 0 + || state->player_drink <= 0 + ); + state->is_resting = state->is_resting && !wake_from_rest; + + bool not_boss = !fighting_boss(state); + float decay = 1.0f - 0.125f * (state->player_dexterity - 1); + + state->player_hunger += (state->is_sleeping ? 0.5f : 1.0f) * decay; + if (state->player_hunger > 25.0f) { + state->player_hunger = 0.0f; + state->player_food = clampi(state->player_food - (not_boss ? 1 : 0), 0, max_food(state)); + } + + state->player_thirst += (state->is_sleeping ? 0.5f : 1.0f) * decay; + if (state->player_thirst > 20.0f) { + state->player_thirst = 0.0f; + state->player_drink = clampi(state->player_drink - (not_boss ? 1 : 0), 0, max_drink(state)); + } + + if (state->is_sleeping) { + state->player_fatigue = state->player_fatigue - 1.0f; + if (state->player_fatigue > 0.0f) { + state->player_fatigue = 0.0f; + } + } else { + state->player_fatigue += decay; + } + if (state->player_fatigue > 30.0f) { + state->player_fatigue = 0.0f; + state->player_energy = clampi(state->player_energy - (not_boss ? 1 : 0), 0, max_energy(state)); + } else if (state->player_fatigue < -10.0f) { + state->player_fatigue = 0.0f; + state->player_energy = clampi(state->player_energy + 1, 0, max_energy(state)); + } + + bool all_necessities = state->player_food > 0 + && state->player_drink > 0 + && (state->player_energy > 0 || state->is_sleeping); + state->player_recover += all_necessities + ? (state->is_sleeping ? 2.0f : 1.0f) + : (state->is_sleeping ? -0.5f : -1.0f) * (not_boss ? 1.0f : 0.0f); + + if (state->player_recover > 25.0f) { + state->player_recover = 0.0f; + state->player_health = clampf(state->player_health + 1.0f, 0.0f, max_health(state)); + } else if (state->player_recover < -15.0f) { + state->player_recover = 0.0f; + state->player_health -= 1.0f; + } + + float mana_gain = state->is_sleeping ? 2.0f : 1.0f; + float mana_coeff = 1.0f + 0.25f * (state->player_intelligence - 1); + state->player_recover_mana = (state->player_recover_mana + mana_gain) * mana_coeff; + if (state->player_recover_mana > 30.0f) { + state->player_recover_mana = 0.0f; + state->player_mana = clampi(state->player_mana + 1, 0, max_mana(state)); + } + + state->inventory.wood = clampi(state->inventory.wood, 0, 99); + state->inventory.stone = clampi(state->inventory.stone, 0, 99); + state->inventory.coal = clampi(state->inventory.coal, 0, 99); + state->inventory.iron = clampi(state->inventory.iron, 0, 99); + state->inventory.diamond = clampi(state->inventory.diamond, 0, 99); + state->inventory.sapling = clampi(state->inventory.sapling, 0, 99); + state->inventory.pickaxe = clampi(state->inventory.pickaxe, 0, 99); + state->inventory.sword = clampi(state->inventory.sword, 0, 99); + state->inventory.bow = clampi(state->inventory.bow, 0, 99); + state->inventory.arrows = clampi(state->inventory.arrows, 0, 99); + state->inventory.torches = clampi(state->inventory.torches, 0, 99); + state->inventory.ruby = clampi(state->inventory.ruby, 0, 99); + state->inventory.sapphire = clampi(state->inventory.sapphire, 0, 99); + state->inventory.books = clampi(state->inventory.books, 0, 99); + for (int i = 0; i < 4; i++) { + state->inventory.armour[i] = clampi(state->inventory.armour[i], 0, 99); + } + for (int i = 0; i < NUM_POTIONS; i++) { + state->inventory.potions[i] = clampi(state->inventory.potions[i], 0, 99); + } + + state->player_health = clampf(state->player_health, 0.0f, max_health(state)); + state->player_food = clampi(state->player_food, 0, max_food(state)); + state->player_drink = clampi(state->player_drink, 0, max_drink(state)); + state->player_energy = clampi(state->player_energy, 0, max_energy(state)); + state->player_mana = clampi(state->player_mana, 0, max_mana(state)); + + state->achievements[ACH_COLLECT_WOOD] |= state->inventory.wood > 0; + state->achievements[ACH_COLLECT_STONE] |= state->inventory.stone > 0; + state->achievements[ACH_COLLECT_COAL] |= state->inventory.coal > 0; + state->achievements[ACH_COLLECT_IRON] |= state->inventory.iron > 0; + state->achievements[ACH_COLLECT_DIAMOND] |= state->inventory.diamond > 0; + state->achievements[ACH_COLLECT_SAPPHIRE] |= state->inventory.sapphire > 0; + state->achievements[ACH_COLLECT_RUBY] |= state->inventory.ruby > 0; + state->achievements[ACH_COLLECT_SAPLING] |= state->inventory.sapling > 0; + state->achievements[ACH_FIND_BOW] |= state->inventory.bow > 0; + state->achievements[ACH_MAKE_ARROW] |= state->inventory.arrows > 0; + state->achievements[ACH_MAKE_TORCH] |= state->inventory.torches > 0; + state->achievements[ACH_MAKE_WOOD_PICKAXE] |= state->inventory.pickaxe >= 1; + state->achievements[ACH_MAKE_STONE_PICKAXE] |= state->inventory.pickaxe >= 2; + state->achievements[ACH_MAKE_IRON_PICKAXE] |= state->inventory.pickaxe >= 3; + state->achievements[ACH_MAKE_DIAMOND_PICKAXE] |= state->inventory.pickaxe >= 4; + state->achievements[ACH_MAKE_WOOD_SWORD] |= state->inventory.sword >= 1; + state->achievements[ACH_MAKE_STONE_SWORD] |= state->inventory.sword >= 2; + state->achievements[ACH_MAKE_IRON_SWORD] |= state->inventory.sword >= 3; + state->achievements[ACH_MAKE_DIAMOND_SWORD] |= state->inventory.sword >= 4; + update_log_state(env); + + store_rng(state, rng_key(&step_rng)); + state->timestep += 1; + float day_progress = fmodf(state->timestep / (float)DAY_LENGTH, 1.0f) + 0.3f; + state->light_level = 1.0f - powf(fabsf(cosf(3.14159265358979323846f * day_progress)), 3.0f); + + done = state->player_health <= 0.0f || state->timestep >= DEFAULT_MAX_TIMESTEPS; + } while (!done && (state->is_sleeping || state->is_resting)); + + float achievement_reward = 0.0f; + for (int i = 0; i < NUM_ACHIEVEMENTS; i++) { + int delta = state->achievements[i] - initial_achievements[i]; + achievement_reward += delta * ACHIEVEMENT_REWARD_MAP[i]; + } + float reward = achievement_reward + (equipped_armour(state) - initial_armour); + // reward += (state->player_health - initial_health) * 0.1f; + if (state->player_health <= 0.0f) { + reward = -1.0f; // Dead + } + + memcpy(env->achievements, env->state.achievements, sizeof(env->achievements)); + + env->agents[0].rewards[0] = reward; + env->agents[0].terminals[0] = done ? 1.0f : 0.0f; + env->episode_return_accum += achievement_reward; + env->episode_length_accum += 1; + + if (done) { + int unlocked = 0; + float achievement_return = 0.0f; + for (int i = 0; i < NUM_ACHIEVEMENTS; i++) { + if (env->achievements[i]) { + unlocked++; + achievement_return += ACHIEVEMENT_REWARD_MAP[i]; + env->log.achievements[i] += 1.0f; + } + } + env->log.achievement_rate += unlocked / (float)NUM_ACHIEVEMENTS; + env->log.perf += achievement_return / max_achievement_return(); + env->log.score += env->episode_return_accum; + env->log.episode_return += env->episode_return_accum; + env->log.episode_length += env->episode_length_accum; + for (int floor = 0; floor <= env->max_floor_accum; floor++) { + env->log.floors[floor] += 1.0f; + } + env->log.n += 1.0f; + + env->episode_return_accum = 0.0f; + env->episode_length_accum = 0; + env->max_floor_accum = 0; + memset(env->achievements, 0, sizeof(env->achievements)); + if (env->reset_pool_size > 0) { + uint32_t idx = (uint32_t)reset_key % (uint32_t)env->reset_pool_size; + memcpy(&env->state, &env->reset_pool[idx], sizeof(State)); + } else { + Rng done_unused; + Rng world_key; + rng_split(reset_key, &done_unused, &world_key); + generate_world_from_key(&env->state, world_key); + } + } + + compute_observations(env); } void puf_init(Env* env, Dict* kwargs) { env->num_agents = 1; env->agents[0].policy = 0; - env->agents[0].action_mask = NULL; + env->agents[0].action_mask = NULL; // trainer wires mask after puf_init + env->use_action_mask = 0; uint64_t seed_offset = 0; - // optional keys for (int i = 0; i < kwargs->size; i++) { if (strcmp(kwargs->items[i].key, "seed_offset") == 0) { seed_offset = (uint64_t)kwargs->items[i].value; + } else if (strcmp(kwargs->items[i].key, "action_mask") == 0) { + env->use_action_mask = kwargs->items[i].value != 0.0; + } + } + env->seed = seed_offset + env->rng; + memset(&env->state, 0, sizeof(State)); + env->episode_return_accum = 0.0f; + env->episode_length_accum = 0; + env->max_floor_accum = 0; + memset(env->achievements, 0, sizeof(env->achievements)); + memset(&env->log, 0, sizeof(Log)); + env->client = NULL; +} + +Env* my_vec_init(int* num_envs_out, int* env_starts, int* env_counts, + Dict* vec_kwargs, Dict* env_kwargs) { + int total_agents = dict_get(vec_kwargs, "total_agents"); + int num_buffers = dict_get(vec_kwargs, "num_buffers"); + int agents_per_buf = total_agents / num_buffers; + int num_envs = total_agents; + int reset_pool_size = 0; + DictItem* item = dict_find(env_kwargs, "reset_pool_size"); + if (item) { + reset_pool_size = item->value; + } + State* pool = NULL; + if (reset_pool_size > 0) { + pool = (State*)calloc(reset_pool_size, sizeof(State)); + for (int i = 0; i < reset_pool_size; i++) { + Rng init_key = rng_seed(i); + Rng discard; + Rng reset_key; + rng_split(init_key, &discard, &reset_key); + Rng unused; + Rng world_key; + rng_split(reset_key, &unused, &world_key); + generate_world_from_key(&pool[i], world_key); } - if (strcmp(kwargs->items[i].key, "reset_pool_size") == 0) { - craftax_set_reset_pool_size((int)kwargs->items[i].value); + } + + Env* envs = (Env*)calloc(num_envs, sizeof(Env)); + int buf = 0; + int buf_agents = 0; + env_starts[0] = 0; + env_counts[0] = 0; + for (int i = 0; i < num_envs; i++) { + Env* env = &envs[i]; + env->rng = i; + env->reset_pool = pool; + env->reset_pool_size = reset_pool_size; + puf_init(env, env_kwargs); + buf_agents += env->num_agents; + env_counts[buf]++; + if (buf_agents >= agents_per_buf && buf < num_buffers - 1) { + buf++; + env_starts[buf] = i + 1; + env_counts[buf] = 0; + buf_agents = 0; } } - env->seed = seed_offset + (uint64_t)env->rng; - c_init(env); + *num_envs_out = num_envs; + return envs; +} + +void my_vec_close(Env* envs) { + free(envs[0].reset_pool); } -// Step subsystem implementations (after Craftax types / forward decls). -#include "step_crafting.h" -#include "step_update_mobs.h" -#include "step_spawn_mobs.h" +void puf_log(Log* log, Dict* out) { + dict_set(out, "perf", log->perf); + dict_set(out, "achievement_rate", log->achievement_rate); + 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, "floor_0_overworld", log->floors[0]); + dict_set(out, "floor_1_dungeon", log->floors[1]); + dict_set(out, "floor_2_gnomish_mines", log->floors[2]); + dict_set(out, "floor_3_sewers", log->floors[3]); + dict_set(out, "floor_4_vault", log->floors[4]); + dict_set(out, "floor_5_troll_mines", log->floors[5]); + dict_set(out, "floor_6_fire_realm", log->floors[6]); + dict_set(out, "floor_7_ice_realm", log->floors[7]); + dict_set(out, "floor_8_graveyard", log->floors[8]); + dict_set(out, "n", log->n); +} + +static Texture2D textures; +static int textures_loaded; + +static void draw_tile(int tex_id, int x, int y, int px) { + Rectangle src = { + (float)((tex_id % TEX_SHEET_COLS) * TEX_TILE_PX), + (float)((tex_id / TEX_SHEET_COLS) * TEX_TILE_PX), + (float)TEX_TILE_PX, + (float)TEX_TILE_PX, + }; + Rectangle dst = {(float)x, (float)y, (float)px, (float)px}; + DrawTexturePro(textures, src, dst, (Vector2){0, 0}, 0.0f, WHITE); +} + +static int projectile_tex(int ptype, int dr, int dc) { + if (ptype == PROJECTILE_DAGGER) { + return TEX_PROJ_DAGGER; + } + if (ptype == PROJECTILE_FIREBALL || ptype == PROJECTILE_FIREBALL2) { + return TEX_PROJ_FIREBALL; + } + if (ptype == PROJECTILE_ICEBALL || ptype == PROJECTILE_ICEBALL2) { + return TEX_PROJ_ICEBALL; + } + if (ptype == PROJECTILE_SLIMEBALL) { + return TEX_PROJ_SLIMEBALL; + } + if (dr < 0) { + return TEX_ARROW_UP; + } + if (dr > 0) { + return TEX_ARROW_DOWN; + } + if (dc < 0) { + return TEX_ARROW_LEFT; + } + return TEX_ARROW_RIGHT; +} + +static int mob_tex_base[] = {TEX_PASSIVE, TEX_MELEE, TEX_RANGED}; + +static void draw_agent_obs(Craftax* env, int panel_x, int panel_y, + int panel_w, int panel_h) { + State* state = &env->state; + int pad = 10; + int px = (panel_w - pad * 2) / OBS_COLS; + int grid_x = panel_x + (panel_w - OBS_COLS * px) / 2; + int grid_y = panel_y + 36 + pad; + int level = clampi(state->player_level, 0, NUM_LEVELS - 1); + int pr = state->player_position[0]; + int pc = state->player_position[1]; + int rr = OBS_ROWS / 2; + int rc = OBS_COLS / 2; + + DrawRectangle(panel_x, panel_y, panel_w, panel_h, (Color){8, 10, 14, 255}); + DrawRectangleLines(panel_x, panel_y, panel_w, panel_h, (Color){0, 210, 220, 255}); + DrawText("agent obs", panel_x + pad, panel_y + 8, 18, WHITE); + DrawText("9x11 light>12", panel_x + pad, panel_y + 28, 12, + (Color){140, 160, 166, 255}); + + for (int vr = 0; vr < OBS_ROWS; vr++) { + for (int vc = 0; vc < OBS_COLS; vc++) { + int wr = pr + (vr - rr); + int wc = pc + (vc - rc); + int dst_x = grid_x + vc * px; + int dst_y = grid_y + vr * px; + int lit = (unsigned)wr < MAP_SIZE && (unsigned)wc < MAP_SIZE + && state->light_map[level][wr][wc] > VISIBLE_LIGHT_THRESHOLD; + if (!lit) { + DrawRectangle(dst_x, dst_y, px, px, BLACK); + continue; + } + int block = state->map[level][wr][wc]; + if (block < 0 || block >= NUM_BLOCK_TYPES) { + block = BLOCK_INVALID; + } + draw_tile(block, dst_x, dst_y, px); + int item = state->item_map[level][wr][wc]; + if (item > ITEM_NONE) { + draw_tile(TEX_ITEM_BASE + item, dst_x, dst_y, px); + } + int mob_class; + int slot; + if (find_mob_at(state, level, wr, wc, &mob_class, &slot)) { + int type_id = mobs_for_class(state, level, mob_class)->type_id[slot]; + draw_tile(mob_tex_base[mob_class] + type_id, dst_x, dst_y, px); + } + } + } + + for (int from_player = 0; from_player < 2; from_player++) { + Mobs* projectiles = from_player + ? &state->player_projectiles[level] : &state->mob_projectiles[level]; + int (*directions)[MAX_PLAYER_PROJECTILES][2] = from_player + ? state->player_projectile_directions : state->mob_projectile_dirs; + for (int i = 0; i < MAX_PLAYER_PROJECTILES; i++) { + if (!projectiles->mask[i]) { + continue; + } + int row = projectiles->position[i][0]; + int col = projectiles->position[i][1]; + int vr = row - pr + rr; + int vc = col - pc + rc; + int lit = (unsigned)row < MAP_SIZE && (unsigned)col < MAP_SIZE + && state->light_map[level][row][col] > VISIBLE_LIGHT_THRESHOLD; + if (vr < 0 || vr >= OBS_ROWS || vc < 0 || vc >= OBS_COLS || !lit) { + continue; + } + int dr = directions[level][i][0]; + int dc = directions[level][i][1]; + draw_tile(projectile_tex(projectiles->type_id[i], dr, dc), + grid_x + vc * px, grid_y + vr * px, px); + } + } + + int player_tex = TEX_PLAYER_DOWN; + if (state->is_sleeping) { + player_tex = TEX_PLAYER_SLEEP; + } else if (state->player_direction == ACTION_LEFT) { + player_tex = TEX_PLAYER_LEFT; + } else if (state->player_direction == ACTION_RIGHT) { + player_tex = TEX_PLAYER_RIGHT; + } else if (state->player_direction == ACTION_UP) { + player_tex = TEX_PLAYER_UP; + } + draw_tile(player_tex, grid_x + rc * px, grid_y + rr * px, px); + + float v = env->predicted_value; + float t = clampf(v, -1.0f, 1.0f); + unsigned char fade = (unsigned char)(255.0f * (1.0f - fabsf(t))); + Color vc = WHITE; + if (t < 0.0f) { + vc = (Color){255, fade, fade, 255}; + } else if (t > 0.0f) { + vc = (Color){fade, 255, fade, 255}; + } + char* vlabel = TextFormat("V(o, h) = %.2f", v); + DrawText(vlabel, panel_x + (panel_w - MeasureText(vlabel, 18)) / 2, + grid_y + OBS_ROWS * px + 10, 18, vc); +} + +static void draw_icon_count(int tex_id, int value, int x, int y) { + draw_tile(tex_id, x, y, 20); + DrawText(TextFormat("%d", value), x + 23, y + 4, 14, RAYWHITE); +} + +static void draw_inv_slot(int tex_id, int overlay, int x, int y) { + DrawRectangle(x, y, 24, 24, (Color){32, 32, 32, 255}); + DrawRectangleLines(x, y, 24, 24, (Color){80, 80, 80, 255}); + if (tex_id >= 0) { + draw_tile(tex_id, x, y, 24); + } + if (overlay >= 0) { + draw_tile(overlay, x, y, 24); + } +} + +static const char* action_names[ATN_DIM] = { + "NOOP", "LEFT", "RIGHT", "UP", "DOWN", "DO", "SLEEP", + "PLACE_STONE", "PLACE_TABLE", "PLACE_FURNACE", "PLACE_PLANT", + "MAKE_WOOD_PICKAXE", "MAKE_STONE_PICKAXE", "MAKE_IRON_PICKAXE", + "MAKE_WOOD_SWORD", "MAKE_STONE_SWORD", "MAKE_IRON_SWORD", + "REST", "DESCEND", "ASCEND", + "MAKE_DIAMOND_PICKAXE", "MAKE_DIAMOND_SWORD", + "MAKE_IRON_ARMOUR", "MAKE_DIAMOND_ARMOUR", + "SHOOT_ARROW", "MAKE_ARROW", "CAST_FIREBALL", "CAST_ICEBALL", "PLACE_TORCH", + "DRINK_POTION_RED", "DRINK_POTION_GREEN", "DRINK_POTION_BLUE", + "DRINK_POTION_PINK", "DRINK_POTION_CYAN", "DRINK_POTION_YELLOW", + "READ_BOOK", "ENCHANT_SWORD", "ENCHANT_ARMOUR", "MAKE_TORCH", + "LEVEL_UP_DEXTERITY", "LEVEL_UP_STRENGTH", "LEVEL_UP_INTELLIGENCE", + "ENCHANT_BOW", +}; + +static const char* action_keys[ATN_DIM] = { + "Q", "A", "D", "W", "S", "Space", "Tab", + "R", "T", "F", "P", + "1", "2", "3", + "5", "6", "7", + "E", ".", ",", + "4", "8", + "Y", "U", + "I", "O", "G", "H", "J", + "Z", "X", "C", "V", "B", "N", + "M", "K", "L", "[", + "]", "-", "=", + ";", +}; + +static const char* ach_names[NUM_ACHIEVEMENTS] = { + "Collect Wood", "Place Table", "Eat Cow", "Collect Sapling", "Collect Drink", + "Make Wood Pickaxe", "Make Wood Sword", "Place Plant", "Defeat Zombie", + "Collect Stone", "Place Stone", "Eat Plant", "Defeat Skeleton", + "Make Stone Pickaxe", "Make Stone Sword", "Wake Up", "Place Furnace", + "Collect Coal", "Collect Iron", "Collect Diamond", "Make Iron Pickaxe", + "Make Iron Sword", "Make Arrow", "Make Torch", "Place Torch", + "Make Diamond Sword", "Make Iron Armour", "Make Diamond Armour", + "Enter Gnomish Mines", "Enter Dungeon", "Enter Sewers", "Enter Vault", + "Enter Troll Mines", "Enter Fire Realm", "Enter Ice Realm", "Enter Graveyard", + "Defeat Gnome Warrior", "Defeat Gnome Archer", "Defeat Orc Soldier", + "Defeat Orc Mage", "Defeat Lizard", "Defeat Kobold", "Defeat Troll", + "Defeat Deep Thing", "Defeat Pigman", "Defeat Fire Elemental", + "Defeat Frost Troll", "Defeat Ice Elemental", "Damage Necromancer", + "Defeat Necromancer", "Eat Bat", "Eat Snail", "Find Bow", "Fire Bow", + "Collect Sapphire", "Learn Fireball", "Cast Fireball", "Learn Iceball", + "Cast Iceball", "Collect Ruby", "Make Diamond Pickaxe", "Open Chest", + "Drink Potion", "Enchant Sword", "Enchant Armour", "Defeat Knight", + "Defeat Archer", +}; + +void puf_render(Craftax* env) { + const int view_w = RENDER_COLS * TEX_DRAW_PX; + const int view_h = RENDER_ROWS * TEX_DRAW_PX; + const int hud_h = 142; + const int origin_x = ACH_PANEL_W; + const int window_w = origin_x + view_w + OBS_PANEL_W + ACTION_PANEL_W; + + if (env->client == NULL) { + env->client = (Client*)calloc(1, sizeof(Client)); + env->client->cell_size = TEX_DRAW_PX; + env->client->screen_width = window_w; + env->client->screen_height = view_h + hud_h; + } + + Client* client = env->client; + if (!client->window_ready) { + InitWindow(client->screen_width, client->screen_height, "Craftax"); + SetTargetFPS(30); + client->window_ready = true; + } + if (!textures_loaded) { + const char* candidates[] = { + "resources/craftax/textures.png", + "../resources/craftax/textures.png", + "../../resources/craftax/textures.png", + }; + for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { + if (FileExists(candidates[i])) { + textures = LoadTexture(candidates[i]); + break; + } + } + if (textures.id == 0) { + fprintf(stderr, "craftax textures.png not found in resources/craftax\n"); + exit(1); + } + SetTextureFilter(textures, TEXTURE_FILTER_POINT); + textures_loaded = 1; + } + if (IsKeyDown(KEY_ESCAPE)) { + exit(0); + } + got_human_input(env); + + int level = clampi(env->state.player_level, 0, NUM_LEVELS - 1); + int player_row = clampi(env->state.player_position[0], 0, MAP_SIZE - 1); + int player_col = clampi(env->state.player_position[1], 0, MAP_SIZE - 1); + int half_r = RENDER_ROWS / 2; + int half_c = RENDER_COLS / 2; + int top_row = player_row - half_r; + int left_col = player_col - half_c; + + BeginDrawing(); + ClearBackground(BLACK); + + for (int vr = 0; vr < RENDER_ROWS; vr++) { + for (int vc = 0; vc < RENDER_COLS; vc++) { + int wr = top_row + vr; + int wc = left_col + vc; + int dst_x = origin_x + vc * TEX_DRAW_PX; + int dst_y = vr * TEX_DRAW_PX; + + int in_map = wr >= 0 && wr < MAP_SIZE && wc >= 0 && wc < MAP_SIZE; + int block = BLOCK_OUT_OF_BOUNDS; + if (in_map) { + block = env->state.map[level][wr][wc]; + } + if (block < 0 || block >= NUM_BLOCK_TYPES) { + block = BLOCK_INVALID; + } + draw_tile(block, dst_x, dst_y, TEX_DRAW_PX); + if (!in_map) { + continue; + } + int item = env->state.item_map[level][wr][wc]; + if (item > ITEM_NONE) { + draw_tile(TEX_ITEM_BASE + item, dst_x, dst_y, TEX_DRAW_PX); + } + int mob_class; + int slot; + if (find_mob_at(&env->state, level, wr, wc, &mob_class, &slot)) { + int type_id = mobs_for_class(&env->state, level, mob_class) + ->type_id[slot]; + draw_tile(mob_tex_base[mob_class] + type_id, dst_x, dst_y, TEX_DRAW_PX); + } + } + } + + for (int from_player = 0; from_player < 2; from_player++) { + Mobs* projectiles = from_player + ? &env->state.player_projectiles[level] + : &env->state.mob_projectiles[level]; + int (*directions)[MAX_PLAYER_PROJECTILES][2] = from_player + ? env->state.player_projectile_directions + : env->state.mob_projectile_dirs; + for (int i = 0; i < MAX_PLAYER_PROJECTILES; i++) { + if (!projectiles->mask[i]) { + continue; + } + int vr = projectiles->position[i][0] - top_row; + int vc = projectiles->position[i][1] - left_col; + if (vr < 0 || vr >= RENDER_ROWS || vc < 0 || vc >= RENDER_COLS) { + continue; + } + int dr = directions[level][i][0]; + int dc = directions[level][i][1]; + draw_tile(projectile_tex(projectiles->type_id[i], dr, dc), + origin_x + vc * TEX_DRAW_PX, vr * TEX_DRAW_PX, TEX_DRAW_PX); + } + } + + int player_tex = TEX_PLAYER_DOWN; + if (env->state.is_sleeping) { + player_tex = TEX_PLAYER_SLEEP; + } else if (env->state.player_direction == ACTION_LEFT) { + player_tex = TEX_PLAYER_LEFT; + } else if (env->state.player_direction == ACTION_RIGHT) { + player_tex = TEX_PLAYER_RIGHT; + } else if (env->state.player_direction == ACTION_UP) { + player_tex = TEX_PLAYER_UP; + } + draw_tile(player_tex, origin_x + half_c * TEX_DRAW_PX, + half_r * TEX_DRAW_PX, TEX_DRAW_PX); + + if (env->state.light_level < 1.0f) { + unsigned char alpha = (unsigned char)((1.0f - env->state.light_level) * 140.0f); + DrawRectangle(origin_x, 0, view_w, view_h, (Color){0, 0, 40, alpha}); + } + + int floor_bar_h = 16; + int cell_w = view_w / NUM_LEVELS; + DrawRectangle(origin_x, 0, view_w, floor_bar_h, (Color){18, 18, 18, 230}); + for (int f = 0; f < NUM_LEVELS; f++) { + int x = origin_x + f * cell_w; + int w = (f == NUM_LEVELS - 1) ? (origin_x + view_w - x) : cell_w; + bool reached = f <= env->max_floor_accum; + bool here = f == env->state.player_level; + Color fill = reached ? (Color){255, 210, 40, 255} : (Color){45, 45, 45, 255}; + if (here) { + fill = (Color){255, 235, 80, 255}; + } + DrawRectangle(x + 1, 1, w - 2, floor_bar_h - 2, fill); + if (here) { + DrawRectangleLines(x + 1, 1, w - 2, floor_bar_h - 2, WHITE); + } + DrawText(TextFormat("%d", f), x + 4, 2, 10, reached ? BLACK : (Color){140, 140, 140, 255}); + } + + draw_agent_obs(env, origin_x + view_w, 0, OBS_PANEL_W, view_h + hud_h); + + int hud_y = view_h; + Inventory* inv = &env->state.inventory; + DrawRectangle(origin_x, hud_y, view_w, hud_h, (Color){20, 20, 20, 255}); + + int health_max = max_health(&env->state); + float health_frac = clampf(env->state.player_health / health_max, 0.0f, 1.0f); + int bar_x = origin_x + 4; + int bar_y = hud_y + 4; + int bar_w = view_w - 8; + int bar_h = 18; + DrawRectangle(bar_x, bar_y, bar_w, bar_h, (Color){115, 25, 25, 255}); + DrawRectangle(bar_x, bar_y, (int)(bar_w * health_frac), bar_h, (Color){35, 190, 75, 255}); + DrawRectangleLines(bar_x, bar_y, bar_w, bar_h, (Color){220, 220, 220, 255}); + DrawText( + TextFormat("HP %.0f / %d", env->state.player_health, health_max), + bar_x + 8, + bar_y + 2, + 14, + WHITE + ); + + DrawText( + TextFormat( + "Food:%d/%d Drink:%d/%d Energy:%d/%d Mana:%d/%d L:%d t:%d", + env->state.player_food, + max_food(&env->state), + env->state.player_drink, + max_drink(&env->state), + env->state.player_energy, + max_energy(&env->state), + env->state.player_mana, + max_mana(&env->state), + env->state.player_level, + env->state.timestep + ), + origin_x + 4, + hud_y + 26, + 14, + WHITE + ); + DrawText( + TextFormat( + "XP:%d DEX:%d STR:%d INT:%d light:%.2f sleep:%d rest:%d", + env->state.player_xp, + env->state.player_dexterity, + env->state.player_strength, + env->state.player_intelligence, + env->state.light_level, + env->state.is_sleeping, + env->state.is_resting + ), + origin_x + 4, + hud_y + 44, + 14, + (Color){200, 200, 200, 255} + ); + int achievements = 0; + for (int i = 0; i < NUM_ACHIEVEMENTS; i++) { + achievements += env->state.achievements[i] ? 1 : 0; + } + int inv_y = hud_y + 62; + int inv_x = origin_x + 4; + int inv_ids[] = { + BLOCK_WOOD, BLOCK_STONE, BLOCK_COAL, BLOCK_IRON, BLOCK_DIAMOND, + TEX_SAPLING, TEX_TORCH_INV, BLOCK_RUBY, BLOCK_SAPPHIRE, TEX_BOOK, + }; + int inv_counts[] = { + inv->wood, inv->stone, inv->coal, inv->iron, inv->diamond, + inv->sapling, inv->torches, inv->ruby, inv->sapphire, inv->books, + }; + for (int i = 0; i < 10; i++) { + draw_icon_count(inv_ids[i], inv_counts[i], inv_x + 52 * i, inv_y); + } + int armour_x = inv_x + 52 * 10 + 8; + for (int slot = 0; slot < 4; slot++) { + int alvl = inv->armour[slot]; + int tex = -1; + if (alvl > 0) { + tex = (alvl >= 2 ? TEX_ARMOUR_DIAMOND : TEX_ARMOUR_IRON) + slot; + } + int overlay = -1; + int ench = env->state.armour_enchantments[slot]; + if (ench == 1) { + overlay = TEX_ARMOUR_ENCHANT_FIRE + slot; + } else if (ench == 2) { + overlay = TEX_ARMOUR_ENCHANT_ICE + slot; + } + draw_inv_slot(tex, overlay, armour_x + slot * 30, inv_y); + } + int weap_y = hud_y + 90; + int gear[] = { + inv->pickaxe > 0 ? TEX_PICKAXE_WOOD + inv->pickaxe - 1 : -1, + inv->sword > 0 ? TEX_SWORD_WOOD + inv->sword - 1 : -1, + inv->bow > 0 ? TEX_BOW : -1, + inv->arrows > 0 ? TEX_ARROW_UP : -1, + }; + int overlays[4] = {-1, -1, -1, -1}; + if (env->state.sword_enchantment == 1) { + overlays[1] = TEX_SWORD_ENCHANT_FIRE; + } else if (env->state.sword_enchantment == 2) { + overlays[1] = TEX_SWORD_ENCHANT_ICE; + } + if (inv->arrows > 0) { + if (env->state.bow_enchantment == 1) { + overlays[3] = TEX_ARROW_ENCHANT_FIRE; + } else if (env->state.bow_enchantment == 2) { + overlays[3] = TEX_ARROW_ENCHANT_ICE; + } + } + for (int i = 0; i < 4; i++) { + draw_inv_slot(gear[i], overlays[i], armour_x + 30 * i, weap_y); + } + DrawText(TextFormat("%d", inv->arrows), armour_x + 117, weap_y + 6, 14, RAYWHITE); + for (int p = 0; p < NUM_POTIONS; p++) { + draw_icon_count(TEX_POTION + p, inv->potions[p], inv_x + 52 * p, weap_y); + } + draw_inv_slot(env->state.learned_spells[0] ? TEX_PROJ_FIREBALL : -1, + -1, inv_x + 52 * 6, weap_y); + draw_inv_slot(env->state.learned_spells[1] ? TEX_PROJ_ICEBALL : -1, + -1, inv_x + 52 * 7, weap_y); + int human = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); + DrawText( + TextFormat( + "ach:%d/%d ret:%.2f len:%d %s", + achievements, + NUM_ACHIEVEMENTS, + env->episode_return_accum, + env->episode_length_accum, + human ? "HUMAN" : "Hold SHIFT to take control" + ), + origin_x + 4, + hud_y + 118, + 14, + human ? (Color){255, 210, 40, 255} : (Color){200, 200, 140, 255} + ); + + int panel_x = origin_x + view_w + OBS_PANEL_W; + int panel_h = view_h + hud_h; + int taken_action = env->agents[0].actions[0]; + DrawRectangle(panel_x, 0, ACTION_PANEL_W, panel_h, (Color){12, 18, 22, 255}); + DrawRectangleLines(panel_x, 0, ACTION_PANEL_W, panel_h, (Color){55, 70, 76, 255}); + DrawText("Actions", panel_x + 10, 8, 18, RAYWHITE); + DrawText("key", panel_x + 12, 32, 11, (Color){140, 160, 166, 255}); + DrawText("action", panel_x + 78, 32, 11, (Color){140, 160, 166, 255}); + for (int action = 0; action < ATN_DIM; action++) { + int y = 48 + action * 15; + bool selected = action == taken_action; + bool legal = env->agents[0].action_mask == NULL + || env->agents[0].action_mask[action]; + if (selected) { + DrawRectangle(panel_x + 6, y - 2, ACTION_PANEL_W - 12, 15, (Color){0, 210, 220, 255}); + } + Color text_color = selected ? BLACK + : (legal ? (Color){220, 230, 230, 255} : (Color){80, 90, 90, 255}); + DrawText(action_keys[action], panel_x + 12, y, 10, text_color); + DrawText(TextFormat("%02d %s", action, action_names[action]), + panel_x + 78, y, 10, text_color); + } + + int ach_h = view_h + hud_h; + DrawRectangle(0, 0, ACH_PANEL_W, ach_h, WHITE); + DrawText("Achievements", 8, 6, 16, BLACK); + int ach_top = 26; + int ach_row = (ach_h - ach_top) / NUM_ACHIEVEMENTS; + if (ach_row < 10) { + ach_row = 10; + } + for (int i = 0; i < NUM_ACHIEVEMENTS; i++) { + int y = ach_top + i * ach_row; + bool done = env->state.achievements[i] != 0; + if (done) { + DrawRectangle(0, y, ACH_PANEL_W, ach_row, (Color){46, 180, 80, 255}); + } + DrawText( + ach_names[i], + 6, + y + (ach_row > 10 ? 1 : 0), + 10, + done ? WHITE : (Color){50, 50, 50, 255} + ); + } + + EndDrawing(); + puf_web_vsync(); +} + +void puf_close(Craftax* env) { + if (env->client == NULL) { + return; + } + if (env->client->window_ready) { + CloseWindow(); + } + free(env->client); + env->client = NULL; +} diff --git a/ocean/craftax/noise.h b/ocean/craftax/noise.h deleted file mode 100644 index e81e398509..0000000000 --- a/ocean/craftax/noise.h +++ /dev/null @@ -1,206 +0,0 @@ -// 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..58a8f748b5 --- /dev/null +++ b/ocean/craftax/pack_textures.py @@ -0,0 +1,281 @@ +"""Pack Craftax upstream 16x16 PNG assets into resources/craftax/textures.png. + +Consumed by ocean/craftax (and craftax_classic). Layout is a 16-column +row-major RGBA atlas of 16x16 tiles. Order must match the TEX_* enums. + + [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 + [54..61] armour: iron then diamond, each helmet/chest/pants/boots + [62..65] pickaxes: wood, stone, iron, diamond + [66..69] swords: wood, stone, iron, diamond + [70] bow + [71..76] potions: red, green, blue, pink, cyan, yellow + [77..79] HUD-only: sapling, torch_in_inventory, book + [80..87] melee types: zombie, gnome_warrior, orc_soldier, lizard, knight, troll, pigman, frost_troll + [88..90] passive types: cow, bat, snail + [91..98] ranged types: skeleton, gnome_archer, orc_mage, kobold, knight_archer, deep_thing, fire_elemental, ice_elemental + [99..102] projectiles: dagger, fireball, iceball, slimeball + [103..104] sword enchant overlays: fire, ice + [105..106] arrow enchant overlays: fire, ice + [107..110] armour fire overlays: helmet, chest, pants, boots + [111..114] armour ice overlays: helmet, chest, pants, boots +""" + +import os +from pathlib import Path + +import numpy as np +from PIL import Image + +ROOT = Path(__file__).resolve().parents[2] +OUT_DIR = ROOT / "resources" / "craftax" +OUT_PNG = OUT_DIR / "textures.png" +SHEET_COLS = 16 + + +def find_assets() -> Path: + env = os.environ.get("CRAFTAX_ASSETS") + if env: + p = Path(env) + if (p / "iron_helmet.png").exists(): + return p + try: + import craftax + pkg = Path(craftax.__file__).resolve().parent + for cand in (pkg / "craftax" / "assets", pkg / "assets"): + if (cand / "iron_helmet.png").exists(): + return cand + except ImportError: + pass + candidates = [ + ROOT / ".venv/lib/python3.12/site-packages/craftax/craftax/assets", + ROOT / ".venv/lib/python3.10/site-packages/craftax/craftax/assets", + Path.home() / "github/multitask_preplay/.venv/lib/python3.10/site-packages/craftax/craftax/assets", + ] + for cand in candidates: + if (cand / "iron_helmet.png").exists(): + return cand + raise FileNotFoundError( + "craftax assets not found (need iron_helmet.png). " + "Set CRAFTAX_ASSETS or install the craftax package." + ) + + +ASSETS = find_assets() + +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", +] + +ARMOUR_FILES = [ + "iron_helmet.png", + "iron_chestplate.png", + "iron_pants.png", + "iron_boots.png", + "diamond_helmet.png", + "diamond_chestplate.png", + "diamond_pants.png", + "diamond_boots.png", +] + +WEAPON_FILES = [ + "wood_pickaxe.png", + "stone_pickaxe.png", + "iron_pickaxe.png", + "diamond_pickaxe.png", + "wood_sword.png", + "stone_sword.png", + "iron_sword.png", + "diamond_sword.png", + "bow.png", +] + +POTION_FILES = [ + "potion_red.png", + "potion_green.png", + "potion_blue.png", + "potion_pink.png", + "potion_cyan.png", + "potion_yellow.png", +] + +HUD_ITEM_FILES = [ + "sapling.png", + "torch_in_inventory.png", + "book.png", +] + +MELEE_TYPE_FILES = [ + "zombie.png", + "gnome_warrior.png", + "orc_soldier.png", + "lizard.png", + "knight.png", + "troll.png", + "pigman.png", + "frost_troll.png", +] +PASSIVE_TYPE_FILES = [ + "cow.png", + "bat.png", + "snail.png", +] +RANGED_TYPE_FILES = [ + "skeleton.png", + "gnome_archer.png", + "orc_mage.png", + "kobold.png", + "knight_archer.png", + "deep_thing.png", + "fire_elemental.png", + "ice_elemental.png", +] +PROJECTILE_TYPE_FILES = [ + "dagger.png", + "fireball.png", + "iceball.png", + "slimeball.png", +] +ENCHANT_FILES = [ + "sword_fire_enchantment.png", + "sword_ice_enchantment.png", + "arrow_fire_enchantment.png", + "arrow_ice_enchantment.png", + "helmet_fire_enchantment.png", + "chestplate_fire_enchantment.png", + "pants_fire_enchantment.png", + "boots_fire_enchantment.png", + "helmet_ice_enchantment.png", + "chestplate_ice_enchantment.png", + "pants_ice_enchantment.png", + "boots_ice_enchantment.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 tiles_to_sheet(tiles: list[np.ndarray]) -> Image.Image: + n = len(tiles) + rows = (n + SHEET_COLS - 1) // SHEET_COLS + sheet = Image.new("RGBA", (SHEET_COLS * TILE, rows * TILE), (0, 0, 0, 0)) + for i, tile in enumerate(tiles): + x = (i % SHEET_COLS) * TILE + y = (i // SHEET_COLS) * TILE + sheet.paste(Image.fromarray(tile, mode="RGBA"), (x, y)) + return sheet + + +def main() -> None: + print(f"craftax assets: {ASSETS}") + tiles: list[np.ndarray] = [] + for f in BLOCK_FILES: + tiles.append(load_tile(f)) + + tiles[1] = np.full((TILE, TILE, 4), 128, dtype=np.uint8) + tiles[1][..., 3] = 255 + tiles[18] = np.zeros((TILE, TILE, 4), dtype=np.uint8) + tiles[18][..., 3] = 255 + + for f in PLAYER_FILES: + tiles.append(load_tile(f)) + + 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 + ARMOUR_FILES + WEAPON_FILES + + POTION_FILES + HUD_ITEM_FILES + + MELEE_TYPE_FILES + PASSIVE_TYPE_FILES + RANGED_TYPE_FILES + + PROJECTILE_TYPE_FILES + ENCHANT_FILES + ): + tiles.append(load_tile(f)) + + assert len(tiles) == 115, len(tiles) + OUT_DIR.mkdir(parents=True, exist_ok=True) + sheet = tiles_to_sheet(tiles) + sheet.save(OUT_PNG) + print(f"wrote {OUT_PNG} — {len(tiles)} tiles, {sheet.size[0]}x{sheet.size[1]}") + + +if __name__ == "__main__": + main() diff --git a/ocean/craftax/step_crafting.h b/ocean/craftax/step_crafting.h deleted file mode 100644 index 60779364b7..0000000000 --- a/ocean/craftax/step_crafting.h +++ /dev/null @@ -1,424 +0,0 @@ -// 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 deleted file mode 100644 index 7aaab44b64..0000000000 --- a/ocean/craftax/step_do_action.h +++ /dev/null @@ -1,610 +0,0 @@ -// 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 deleted file mode 100644 index 9f5ac1aae1..0000000000 --- a/ocean/craftax/step_medium.h +++ /dev/null @@ -1,459 +0,0 @@ -// 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 deleted file mode 100644 index c643160f4b..0000000000 --- a/ocean/craftax/step_simple.h +++ /dev/null @@ -1,556 +0,0 @@ -// 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 deleted file mode 100644 index f323724ac8..0000000000 --- a/ocean/craftax/step_spawn_mobs.h +++ /dev/null @@ -1,837 +0,0 @@ -// 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) { - int32_t idx = craftax_step_jax_index(block, CRAFTAX_NUM_BLOCK_TYPES); - return idx == CRAFTAX_BLOCK_GRASS - || idx == CRAFTAX_BLOCK_PATH - || idx == CRAFTAX_BLOCK_FIRE_GRASS - || idx == CRAFTAX_BLOCK_ICE_GRASS; -} - -static inline bool craftax_spawn_is_grave_block(int32_t block) { - int32_t idx = craftax_step_jax_index(block, CRAFTAX_NUM_BLOCK_TYPES); - return idx == CRAFTAX_BLOCK_GRAVE - || idx == CRAFTAX_BLOCK_GRAVE2 - || idx == CRAFTAX_BLOCK_GRAVE3; -} - -static inline bool craftax_spawn_is_water_block(int32_t block) { - int32_t idx = craftax_step_jax_index(block, CRAFTAX_NUM_BLOCK_TYPES); - return idx == CRAFTAX_BLOCK_WATER; -} - -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 deleted file mode 100644 index 2b81681d77..0000000000 --- a/ocean/craftax/step_update_mobs.h +++ /dev/null @@ -1,1119 +0,0 @@ -// 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 deleted file mode 100644 index 4d4004e9bd..0000000000 --- a/ocean/craftax/threefry.h +++ /dev/null @@ -1,126 +0,0 @@ -// 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 deleted file mode 100644 index 2712128d96..0000000000 --- a/ocean/craftax/worldgen.h +++ /dev/null @@ -1,1861 +0,0 @@ -// 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); - 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_clean/craftax_clean.c b/ocean/craftax_clean/craftax_clean.c deleted file mode 100644 index cd2ef230ac..0000000000 --- a/ocean/craftax_clean/craftax_clean.c +++ /dev/null @@ -1,118 +0,0 @@ -#ifdef CRAFTAX_CLEAN_IN_HEADER - -static int key_to_action(void) { - static const int map[][2] = { - {KEY_Q, ACTION_NOOP}, - {KEY_W, ACTION_UP}, - {KEY_UP, ACTION_UP}, - {KEY_D, ACTION_RIGHT}, - {KEY_RIGHT, ACTION_RIGHT}, - {KEY_S, ACTION_DOWN}, - {KEY_DOWN, ACTION_DOWN}, - {KEY_A, ACTION_LEFT}, - {KEY_LEFT, ACTION_LEFT}, - {KEY_SPACE, ACTION_DO}, - {KEY_ONE, ACTION_MAKE_WOOD_PICKAXE}, - {KEY_TWO, ACTION_MAKE_STONE_PICKAXE}, - {KEY_THREE, ACTION_MAKE_IRON_PICKAXE}, - {KEY_FOUR, ACTION_MAKE_DIAMOND_PICKAXE}, - {KEY_FIVE, ACTION_MAKE_WOOD_SWORD}, - {KEY_SIX, ACTION_MAKE_STONE_SWORD}, - {KEY_SEVEN, ACTION_MAKE_IRON_SWORD}, - {KEY_EIGHT, ACTION_MAKE_DIAMOND_SWORD}, - {KEY_T, ACTION_PLACE_TABLE}, - {KEY_TAB, ACTION_SLEEP}, - {KEY_R, ACTION_PLACE_STONE}, - {KEY_F, ACTION_PLACE_FURNACE}, - {KEY_P, ACTION_PLACE_PLANT}, - {KEY_E, ACTION_REST}, - {KEY_COMMA, ACTION_ASCEND}, - {KEY_PERIOD, ACTION_DESCEND}, - {KEY_Y, ACTION_MAKE_IRON_ARMOUR}, - {KEY_U, ACTION_MAKE_DIAMOND_ARMOUR}, - {KEY_I, ACTION_SHOOT_ARROW}, - {KEY_O, ACTION_MAKE_ARROW}, - {KEY_G, ACTION_CAST_FIREBALL}, - {KEY_H, ACTION_CAST_ICEBALL}, - {KEY_J, ACTION_PLACE_TORCH}, - {KEY_Z, ACTION_DRINK_POTION_RED}, - {KEY_X, ACTION_DRINK_POTION_GREEN}, - {KEY_C, ACTION_DRINK_POTION_BLUE}, - {KEY_V, ACTION_DRINK_POTION_PINK}, - {KEY_B, ACTION_DRINK_POTION_CYAN}, - {KEY_N, ACTION_DRINK_POTION_YELLOW}, - {KEY_M, ACTION_READ_BOOK}, - {KEY_K, ACTION_ENCHANT_SWORD}, - {KEY_L, ACTION_ENCHANT_ARMOUR}, - {KEY_LEFT_BRACKET, ACTION_MAKE_TORCH}, - {KEY_RIGHT_BRACKET, ACTION_LEVEL_UP_DEXTERITY}, - {KEY_MINUS, ACTION_LEVEL_UP_STRENGTH}, - {KEY_EQUAL, ACTION_LEVEL_UP_INTELLIGENCE}, - {KEY_SEMICOLON, ACTION_ENCHANT_BOW}, - }; - for (int i = 0; i < (int)(sizeof(map) / sizeof(map[0])); i++) { - if (IsKeyPressed(map[i][0])) { - return map[i][1]; - } - } - return -1; -} - -// Shift + action-panel key. 1 = applied, 0 = policy, -1 = skip tick. -static int craftax_clean_human_controls(Craftax* env) { - int shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); - if (!IsWindowReady() || !shift - || env->state.is_sleeping || env->state.is_resting) { - return 0; - } - int action = key_to_action(); - if (action < 0) { - return -1; - } - env->agents[0].actions[0] = (float)action; - return 1; -} - -#else - -#include "craftax_clean.h" - -int main(void) { - Craftax env; - memset(&env, 0, sizeof(env)); - env.num_agents = 1; - env.rng = 1; - env.seed = 1; - env.use_action_mask = 1; - - env.agents[0].observations = (obs_t*)calloc(OBS_SIZE, sizeof(obs_t)); - env.agents[0].actions = (float*)calloc(1, sizeof(float)); - env.agents[0].rewards = (float*)calloc(1, sizeof(float)); - env.agents[0].terminals = (float*)calloc(1, sizeof(float)); - env.agents[0].action_mask = (unsigned char*)calloc(ATN_DIM, 1); - puf_reset(&env); - env.agents[0].actions[0] = -1.0f; - - puf_render(&env); - while (!WindowShouldClose()) { - int action = key_to_action(); - if (action < 0) { - env.agents[0].actions[0] = -1.0f; - puf_render(&env); - continue; - } - env.agents[0].actions[0] = (float)action; - puf_step(&env); - puf_render(&env); - } - - puf_close(&env); - free(env.agents[0].observations); - free(env.agents[0].actions); - free(env.agents[0].rewards); - free(env.agents[0].terminals); - free(env.agents[0].action_mask); - return 0; -} - -#endif diff --git a/ocean/craftax_clean/craftax_clean.cu b/ocean/craftax_clean/craftax_clean.cu deleted file mode 100644 index 3780505522..0000000000 --- a/ocean/craftax_clean/craftax_clean.cu +++ /dev/null @@ -1,2 +0,0 @@ -// Same packed 9x11x8+51 obs as ocean/craftax, so the same category embedding encoder. -#include "../craftax/craftax.cu" diff --git a/ocean/craftax_clean/craftax_clean.h b/ocean/craftax_clean/craftax_clean.h deleted file mode 100644 index c1f421e208..0000000000 --- a/ocean/craftax_clean/craftax_clean.h +++ /dev/null @@ -1,3381 +0,0 @@ -// Full native Craftax port. -#pragma once - -#include -#include -#include -#include -#include - -#include "constants.h" -#include "raylib.h" -typedef float obs_t; -#include "pufferenv.h" -#include -#include - -#ifdef CRAFTAX_CLEAN_PROFILE -#define CLEAN_PROF_ZONES 9 -static const char* clean_prof_names[CLEAN_PROF_ZONES] = { - "floor+craft", - "do", - "place+shoot+potion", - "book+enchant+move", - "update_mobs", - "spawn", - "plants+intrinsics", - "reward+reset", - "obs", -}; -static uint64_t clean_prof_ticks[CLEAN_PROF_ZONES]; -static uint64_t clean_prof_count[CLEAN_PROF_ZONES]; - -static inline uint64_t clean_prof_now(void) { - return __builtin_ia32_rdtsc(); -} - -static inline void clean_prof_add(int zone, uint64_t t0) { - clean_prof_ticks[zone] += clean_prof_now() - t0; - clean_prof_count[zone]++; -} - -void clean_prof_report(void) { - uint64_t total = 0; - for (int i = 0; i < CLEAN_PROF_ZONES; i++) { - total += clean_prof_ticks[i]; - } - fprintf(stderr, "\n=== puf_step zones ===\n"); - for (int i = 0; i < CLEAN_PROF_ZONES; i++) { - double pct = total ? 100.0 * (double)clean_prof_ticks[i] / (double)total : 0.0; - fprintf(stderr, "%-22s %6.2f%% %lu calls ticks=%lu\n", - clean_prof_names[i], pct, (unsigned long)clean_prof_count[i], - (unsigned long)clean_prof_ticks[i]); - } -} - -#define CLEAN_PROF_START() uint64_t _z0 = 0 -#define CLEAN_ZONE(n) do { _z0 = clean_prof_now(); } while (0) -#define CLEAN_ZONE_END(n) clean_prof_add((n), _z0) -#else -#define CLEAN_PROF_START() -#define CLEAN_ZONE(n) -#define CLEAN_ZONE_END(n) -#define clean_prof_report() -#endif - -#define ACT_SIZES {ATN_DIM} -#define NUM_ATNS 1 -#ifdef PUFFERCPU_EVAL_MAIN -#define PUF_CRAFTAX_NET 1 -#endif -#ifdef PUF_CRAFTAX_NET -#include "../craftax/craftax_net.h" -#endif -#define MY_VEC_INIT -#define MY_VEC_CLOSE -typedef Env Craftax; - -// Data structures -typedef struct { - int wood; - int stone; - int coal; - int iron; - int diamond; - int sapling; - int pickaxe; - int sword; - int bow; - int arrows; - int armour[4]; - int torches; - int ruby; - int sapphire; - int potions[NUM_POTIONS]; - int books; -} Inventory; - -typedef struct { - int position[3][2]; - float health[3]; - bool mask[3]; - int attack_cooldown[3]; - int type_id[3]; -} Mobs; - -typedef struct { - uint8_t map[NUM_LEVELS][MAP_SIZE][MAP_SIZE]; - uint8_t item_map[NUM_LEVELS][MAP_SIZE][MAP_SIZE]; - uint8_t light_map[NUM_LEVELS][MAP_SIZE][MAP_SIZE]; - uint64_t mob_bits[NUM_LEVELS][MAP_SIZE]; - uint64_t spawn_land[NUM_LEVELS][MAP_SIZE]; - uint64_t spawn_grave[NUM_LEVELS][MAP_SIZE]; - uint64_t spawn_water[NUM_LEVELS][MAP_SIZE]; - int down_ladders[NUM_LEVELS][2]; - int up_ladders[NUM_LEVELS][2]; - int chests_opened[NUM_LEVELS]; - int monsters_killed[NUM_LEVELS]; - - int player_position[2]; - int player_level; - int player_direction; - - // Intrinsics - float player_health; - int player_food; - int player_drink; - int player_energy; - int player_mana; - int is_sleeping; - int is_resting; - - // Second order intrinsics - float player_recover; - float player_hunger; - float player_thirst; - float player_fatigue; - float player_recover_mana; - - // Attributes - int player_xp; - int player_dexterity; - int player_strength; - int player_intelligence; - - Inventory inventory; - - Mobs melee_mobs[NUM_LEVELS]; - Mobs passive_mobs[NUM_LEVELS]; - Mobs ranged_mobs[NUM_LEVELS]; - Mobs mob_projectiles[NUM_LEVELS]; - - int mob_projectile_directions[NUM_LEVELS][MAX_MOB_PROJECTILES][2]; - Mobs player_projectiles[NUM_LEVELS]; - int player_projectile_directions[NUM_LEVELS][MAX_PLAYER_PROJECTILES][2]; - int growing_plants_positions[MAX_GROWING_PLANTS][2]; - int growing_plants_age[MAX_GROWING_PLANTS]; - int growing_plants_mask[MAX_GROWING_PLANTS]; - int potion_mapping[NUM_POTIONS]; - int learned_spells[2]; - int sword_enchantment; - int bow_enchantment; - int armour_enchantments[4]; - int boss_progress; - int boss_timestep_to_spawn_this_round; - float light_level; - int achievements[NUM_ACHIEVEMENTS]; - uint32_t state_rng[2]; - int timestep; -} State; - -struct Log { - float perf; - float achievement_rate; - float score; - float episode_return; - float episode_length; - float floors[NUM_LEVELS]; - float achievements[NUM_ACHIEVEMENTS]; - float n; -}; - -// Rendering -typedef struct { - int cell_size; - int screen_width; - int screen_height; - bool window_ready; -} Client; - -// Random number generation -typedef uint64_t Rng; - -struct Env { - Client* client; - Log log; - Agent agents[1]; - int num_agents; - int tag; - int boundary_reached; - State state; - int timestep; - unsigned int rng; - uint64_t seed; - Rng env_rng; - float episode_return_accum; - int episode_length_accum; - int max_floor_accum; - int achievements[NUM_ACHIEVEMENTS]; - State* reset_pool; - int reset_pool_size; - int use_action_mask; - float predicted_value; -}; - -Rng rng_seed(uint32_t seed) { - return (uint64_t)seed | ((uint64_t)(seed ^ 0x9E3779B9u) << 32); -} - -void rng_split(Rng key, Rng* left, Rng* right) { - *left = key * 6364136223846793005ULL + 1; - *right = *left * 6364136223846793005ULL + 1; -} - -void rng_split_n(Rng key, Rng* out, int n) { - for (int i = 0; i < n; i++) { - key = key * 6364136223846793005ULL + 1; - out[i] = key; - } -} - -Rng rng_key(Rng* rng) { - Rng draw; - rng_split(*rng, rng, &draw); - return draw; -} - -uint64_t rng_hash(Rng key, uint64_t i) { - uint64_t x = key ^ i; - x ^= x >> 33; - x *= 0xff51afd7ed558ccdULL; - x ^= x >> 33; - x *= 0xc4ceb9fe1a85ec53ULL; - x ^= x >> 33; - return x; -} - -uint32_t rng_u32(Rng key, uint64_t i) { - uint64_t h = rng_hash(key, i); - return (uint32_t)h ^ (uint32_t)(h >> 32); -} - -float rng_f32(Rng key, uint64_t i) { - uint32_t bits = (rng_u32(key, i) >> 9u) | 0x3F800000u; - float v; - memcpy(&v, &bits, sizeof(v)); - return v - 1.0f; -} - -int randint(Rng key, uint64_t i, int lo, int hi) { - uint32_t span = (uint32_t)hi > (uint32_t)lo ? (uint32_t)(hi - lo) : 1u; - if ((span & (span - 1)) == 0) { - return lo + (int)(rng_u32(key, i) & (span - 1)); - } - return lo + (int)((rng_hash(key, i) >> 32) * (uint64_t)span >> 32); -} - -void store_rng(State* state, Rng rng) { - state->state_rng[0] = (uint32_t)rng; - state->state_rng[1] = (uint32_t)(rng >> 32); -} - -int choice_valid(Rng 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 = valid_count * (1.0f - rng_f32(key, 0)); - 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; -} - -void refresh_spawn_cell(State* state, int level, int row, int col) { - int block = state->map[level][row][col]; - uint64_t bit = 1ull << col; - uint64_t* land = &state->spawn_land[level][row]; - uint64_t* grave = &state->spawn_grave[level][row]; - uint64_t* water = &state->spawn_water[level][row]; - *land = (*land & ~bit) | ((block == BLOCK_GRASS || block == BLOCK_PATH - || block == BLOCK_FIRE_GRASS || block == BLOCK_ICE_GRASS) ? bit : 0); - *grave = (*grave & ~bit) | ((block == BLOCK_GRAVE || block == BLOCK_GRAVE2 - || block == BLOCK_GRAVE3) ? bit : 0); - *water = (*water & ~bit) | (block == BLOCK_WATER ? bit : 0); -} - -void set_block(State* state, int level, int row, int col, int block) { - state->map[level][row][col] = block; - refresh_spawn_cell(state, level, row, col); -} - -void generate_fractal(Rng rng, int rows, int cols, int res_rows, int res_cols, - int octaves, float persistence, int lacunarity, float* out) { - // Perlin noise for world generation - int size = rows * cols; - memset(out, 0, size * sizeof(float)); - int frequency = 1; - float amplitude = 1.0f; - for (int octave = 0; octave < octaves; octave++) { - Rng next_rng; - Rng noise_key; - rng_split(rng, &next_rng, &noise_key); - rng = next_rng; - - Rng unused; - Rng angle_key; - rng_split(noise_key, &unused, &angle_key); - int cell_rows = rows / (frequency * res_rows); - int cell_cols = cols / (frequency * res_cols); - int width = frequency * res_cols + 1; - - for (int row = 0; row < rows; row++) { - int grad_row = row / cell_rows; - float local_row = (row - grad_row * cell_rows) / (float)cell_rows; - float interp_row = local_row * local_row * local_row - * (local_row * (local_row * 6.0f - 15.0f) + 10.0f); - for (int col = 0; col < cols; col++) { - int grad_col = col / cell_cols; - float local_col = (col - grad_col * cell_cols) / (float)cell_cols; - float interp_col = local_col * local_col * local_col - * (local_col * (local_col * 6.0f - 15.0f) + 10.0f); - float gx[2][2]; - float gy[2][2]; - for (int dr = 0; dr < 2; dr++) { - for (int dc = 0; dc < 2; dc++) { - uint64_t index = (grad_row + dr) * width + (grad_col + dc); - float angle = NOISE_PI2 * rng_f32(angle_key, index); - gx[dr][dc] = cosf(angle); - gy[dr][dc] = sinf(angle); - } - } - float n00 = local_row * gx[0][0] + local_col * gy[0][0]; - float n10 = (local_row - 1.0f) * gx[1][0] + local_col * gy[1][0]; - float n01 = local_row * gx[0][1] + (local_col - 1.0f) * gy[0][1]; - float n11 = (local_row - 1.0f) * gx[1][1] + (local_col - 1.0f) * gy[1][1]; - float n0 = n00 * (1.0f - interp_row) + interp_row * n10; - float n1 = n01 * (1.0f - interp_row) + interp_row * n11; - out[row * cols + col] += amplitude - * NOISE_SQRT2 * ((1.0f - interp_col) * n0 + interp_col * n1); - } - } - - frequency *= lacunarity; - amplitude *= persistence; - } - float min_value = out[0]; - float max_value = out[0]; - for (int 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 (int i = 0; i < size; i++) { - out[i] = (out[i] - min_value) / scale; - } -} - -int cell_index(int row, int col) { - return row * MAP_SIZE + col; -} - -void generate_world_from_key(State* state, Rng rng) { - memset(state, 0, sizeof(*state)); - Rng smooth_split[7]; - rng_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]; - Rng level_rng = smooth_split[i + 1]; - const SmoothGenConfig* config = &SMOOTH_LEVEL_CONFIGS[i]; - const int player_row = MAP_SIZE / 2; - const int player_col = MAP_SIZE / 2; - float water[MAP_CELLS]; - float mountain[MAP_CELLS]; - float path_x[MAP_CELLS]; - float tree_noise[MAP_CELLS]; - bool lava_map[MAP_SIZE][MAP_SIZE]; - Rng subkey; - - rng_split(level_rng, &level_rng, &subkey); - generate_fractal(subkey, MAP_SIZE, MAP_SIZE, 3, 3, 1, 0.5f, 2, water); - rng_split(level_rng, &level_rng, &subkey); - rng_split(level_rng, &level_rng, &subkey); - generate_fractal(subkey, MAP_SIZE, MAP_SIZE, 3, 3, 1, 0.5f, 2, mountain); - rng_split(level_rng, &level_rng, &subkey); - generate_fractal(subkey, MAP_SIZE, MAP_SIZE, 6, 24, 1, 0.5f, 2, path_x); - rng_split(level_rng, &level_rng, &subkey); - rng_split(level_rng, &level_rng, &subkey); - Rng tree_uniform_key = level_rng; - generate_fractal(subkey, MAP_SIZE, MAP_SIZE, 12, 12, 1, 0.5f, 2, tree_noise); - - for (int row = 0; row < MAP_SIZE; row++) { - int dr = row > player_row ? row - player_row : player_row - row; - for (int col = 0; col < MAP_SIZE; col++) { - int dc = col > player_col ? col - player_col : player_col - col; - float distance = sqrtf(dr * dr + dc * dc); - float proximity_water = distance / config->water_strength; - if (proximity_water < 0.0f) { - proximity_water = 0.0f; - } - if (proximity_water > config->water_max) { - proximity_water = config->water_max; - } - float proximity_mountain = distance / config->mountain_strength; - if (proximity_mountain < 0.0f) { - proximity_mountain = 0.0f; - } - if (proximity_mountain > config->mountain_max) { - proximity_mountain = config->mountain_max; - } - int idx = cell_index(row, col); - - water[idx] = water[idx] + proximity_water - 1.0f; - int block = water[idx] > config->water_threshold - ? config->sea_block - : config->default_block; - if (water[idx] > config->sand_threshold && block != config->sea_block) { - block = config->coast_block; - } - - mountain[idx] = mountain[idx] + 0.05f + proximity_mountain - 1.0f; - if (mountain[idx] > 0.7f) { - block = config->mountain_block; - } - if (mountain[idx] > 0.7f && path_x[idx] > 0.8f) { - block = config->path_block; - } - if (mountain[idx] > 0.7f && path_x[cell_index(col, row)] > 0.8f) { - block = config->path_block; - } - if (mountain[idx] > 0.85f && water[idx] > 0.4f) { - block = config->inner_mountain_block; - } - if (tree_noise[idx] > config->tree_threshold_perlin - && rng_f32(tree_uniform_key, idx) > config->tree_threshold_uniform - && block == config->tree_requirement_block) { - block = config->tree; - } - - state->map[level][row][col] = block; - state->item_map[level][row][col] = ITEM_NONE; - state->light_map[level][row][col] = (unsigned char)(config->default_light * 255.0f); - } - } - - Rng ore_rng; - rng_split(level_rng, &level_rng, &ore_rng); - for (int ore_index = 0; ore_index < 5; ore_index++) { - Rng ore_key; - rng_split(ore_rng, &ore_rng, &ore_key); - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - int idx = cell_index(row, col); - if (state->map[level][row][col] == config->ore_requirement_blocks[ore_index] - && rng_f32(ore_key, idx) < config->ore_chances[ore_index]) { - state->map[level][row][col] = config->ores[ore_index]; - } - } - } - } - - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - int idx = cell_index(row, col); - lava_map[row][col] = mountain[idx] > 0.85f && tree_noise[idx] > 0.7f; - if (lava_map[row][col]) { - state->map[level][row][col] = config->lava; - } - } - } - - rng_split(level_rng, &level_rng, &subkey); - bool valid_diamond[MAP_CELLS]; - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - valid_diamond[cell_index(row, col)] = state->map[level][row][col] == BLOCK_STONE; - } - } - int diamond_index = choice_valid(subkey, valid_diamond, MAP_CELLS); - state->map[level][diamond_index / MAP_SIZE][diamond_index % MAP_SIZE] = BLOCK_STONE; - state->map[level][player_row][player_col] = config->player_spawn; - - bool valid_ladder[MAP_CELLS]; - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - valid_ladder[cell_index(row, col)] = - state->map[level][row][col] == config->valid_ladder; - } - } - - rng_split(level_rng, &level_rng, &subkey); - int ladder_down_index = choice_valid(subkey, valid_ladder, MAP_CELLS); - state->down_ladders[level][0] = ladder_down_index / MAP_SIZE; - state->down_ladders[level][1] = ladder_down_index % MAP_SIZE; - if (config->ladder_down) { - state->item_map[level][state->down_ladders[level][0]][state->down_ladders[level][1]] = - ITEM_LADDER_DOWN; - } - - rng_split(level_rng, &level_rng, &subkey); - int ladder_up_index = choice_valid(subkey, valid_ladder, MAP_CELLS); - int r = ladder_up_index / MAP_SIZE; - int c = ladder_up_index % MAP_SIZE; - state->up_ladders[level][0] = r; - state->up_ladders[level][1] = c; - int light_row = r - 4; - int light_col = c - 4; - if (light_row < 0) { - light_row += MAP_SIZE; - } - if (light_col < 0) { - light_col += MAP_SIZE; - } - if (light_row > MAP_SIZE - 9) { - light_row = MAP_SIZE - 9; - } - if (light_col > MAP_SIZE - 9) { - light_col = MAP_SIZE - 9; - } - if (light_row < 0) { - light_row = 0; - } - if (light_col < 0) { - light_col = 0; - } - for (int lr = 0; lr < 9; lr++) { - for (int lc = 0; lc < 9; lc++) { - float torch = 1.0f - sqrtf((lr - 4) * (lr - 4) + (lc - 4) * (lc - 4)) / 5.0f; - if (torch < 0.0f) { - torch = 0.0f; - } - float light = torch * (1.0f - config->default_light) + config->default_light; - state->light_map[level][light_row + lr][light_col + lc] = - (unsigned char)(light * 255.0f); - } - } - if (config->lava == BLOCK_LAVA) { - 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 < MAP_SIZE; row++) { - for (int col = 0; col < 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 >= MAP_SIZE) { - continue; - } - for (int kc = 0; kc < 3; kc++) { - int src_col = col + kc - 1; - if (src_col < 0 || src_col >= MAP_SIZE) { - continue; - } - if (lava_map[src_row][src_col]) { - add += kernel[kr][kc]; - } - } - } - float light = state->light_map[level][row][col] / 255.0f + add; - if (light > 1.0f) { - light = 1.0f; - } - state->light_map[level][row][col] = (unsigned char)(light * 255.0f); - } - } - } - if (config->ladder_up) { - state->item_map[level][r][c] = ITEM_LADDER_UP; - } - } - - Rng dungeon_split[4]; - rng_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]; - Rng level_rng = dungeon_split[i + 1]; - const DungeonConfig* config = &DUNGEON_LEVEL_CONFIGS[i]; - const int chunk_size = DUNGEON_CHUNK_SIZE; - const int world_chunk_height = MAP_SIZE / chunk_size; - const int num_rooms = DUNGEON_ROOM_COUNT; - const int min_room_size = DUNGEON_MIN_ROOM_SIZE; - const int max_room_size = DUNGEON_MAX_ROOM_SIZE; - const int padded_size = MAP_SIZE + 2 * max_room_size; - - int padded_map[68][68]; - int padded_item[68][68]; - bool room_occupancy[9]; - int room_sizes[8][2]; - int 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 + MAP_SIZE - && col >= max_room_size - && col < max_room_size + MAP_SIZE; - padded_map[row][col] = inner ? BLOCK_WALL : 0; - padded_item[row][col] = ITEM_NONE; - } - } - for (int i = 0; i < 9; i++) { - room_occupancy[i] = true; - } - - Rng keys3[3]; - rng_split_n(level_rng, keys3, 3); - level_rng = keys3[0]; - Rng room_size_key = keys3[2]; - for (int room = 0; room < num_rooms; room++) { - room_sizes[room][0] = randint(room_size_key, room * 2u, min_room_size, max_room_size); - room_sizes[room][1] = randint(room_size_key, room * 2u + 1u, min_room_size, max_room_size); - } - - Rng room_rng; - rng_split(level_rng, &level_rng, &room_rng); - for (int room_index = 0; room_index < num_rooms; room_index++) { - Rng choice_key; - rng_split(room_rng, &room_rng, &choice_key); - int room_chunk = choice_valid(choice_key, room_occupancy, 9); - room_occupancy[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; - Rng position_key; - rng_split(room_rng, &room_rng, &position_key); - room_row += randint(position_key, 0, 0, chunk_size - min_room_size); - room_col += randint(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] = BLOCK_PATH; - } - } - } - - padded_item[room_row][room_col] = ITEM_TORCH; - padded_item[room_row + room_sizes[room_index][0] - 1][room_col] = ITEM_TORCH; - padded_item[room_row][room_col + room_sizes[room_index][1] - 1] = ITEM_TORCH; - padded_item[room_row + room_sizes[room_index][0] - 1][room_col + room_sizes[room_index][1] - 1] = ITEM_TORCH; - - Rng chest_key; - rng_split(room_rng, &room_rng, &chest_key); - int chest_row = randint(chest_key, 0, 1, room_sizes[room_index][0] - 1); - int chest_col = randint(chest_key, 1, 1, room_sizes[room_index][1] - 1); - padded_map[room_row + chest_row][room_col + chest_col] = BLOCK_CHEST; - - Rng fountain_keys[3]; - rng_split_n(room_rng, fountain_keys, 3); - room_rng = fountain_keys[0]; - int fountain_row = randint(fountain_keys[1], 0, 1, room_sizes[room_index][0] - 1); - int fountain_col = randint(fountain_keys[1], 1, 1, room_sizes[room_index][1] - 1); - if (rng_f32(fountain_keys[2], 0) > 0.5f) { - padded_map[room_row + fountain_row][room_col + fountain_col] = config->fountain_block; - } - } - - Rng path_rng; - rng_split(level_rng, &level_rng, &path_rng); - bool included_rooms[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]; - Rng sink_key; - rng_split(path_rng, &path_rng, &sink_key); - int sink_index = choice_valid(sink_key, included_rooms, 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; - if (path_index_col >= 0 && path_index_col <= abs_distance - && padded_map[source_row][col] == BLOCK_WALL) { - padded_map[source_row][col] = 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; - if (path_index_row >= 0 && path_index_row <= abs_distance - && padded_map[row][sink_col] == BLOCK_WALL) { - padded_map[row][sink_col] = BLOCK_PATH; - } - } - } - - Rng unused_left; - Rng next_path_rng; - rng_split(path_rng, &unused_left, &next_path_rng); - path_rng = next_path_rng; - included_rooms[path_index] = true; - } - - padded_map[room_positions[0][0] + 2][room_positions[0][1] + 2] = config->special_block; - - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - state->map[level][row][col] = - padded_map[row + max_room_size][col + max_room_size]; - state->item_map[level][row][col] = - padded_item[row + max_room_size][col + max_room_size]; - } - } - - bool adjacent_path[MAP_SIZE][MAP_SIZE]; - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - bool adjacent = state->map[level][row][col] != BLOCK_WALL; - adjacent = adjacent || (row > 0 && state->map[level][row - 1][col] != BLOCK_WALL); - adjacent = adjacent || (row + 1 < MAP_SIZE && state->map[level][row + 1][col] != BLOCK_WALL); - adjacent = adjacent || (col > 0 && state->map[level][row][col - 1] != BLOCK_WALL); - adjacent = adjacent || (col + 1 < MAP_SIZE && state->map[level][row][col + 1] != BLOCK_WALL); - adjacent_path[row][col] = adjacent; - } - } - - Rng rare_key; - rng_split(level_rng, &level_rng, &rare_key); - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - int idx = cell_index(row, col); - bool rare = (1.0f - rng_f32(rare_key, idx)) > 0.9f; - int wall_map = rare ? BLOCK_WALL_MOSS : BLOCK_WALL; - bool rare_path = rare - && state->map[level][row][col] == BLOCK_PATH - && state->item_map[level][row][col] == ITEM_NONE; - int path_map = rare_path ? config->rare_path_replacement_block : state->map[level][row][col]; - bool is_wall_map = state->map[level][row][col] == BLOCK_WALL && adjacent_path[row][col]; - if (!adjacent_path[row][col]) { - state->map[level][row][col] = BLOCK_DARKNESS; - } else if (is_wall_map) { - state->map[level][row][col] = wall_map; - } else { - state->map[level][row][col] = path_map; - } - state->light_map[level][row][col] = 255; - } - } - - bool valid_ladder[MAP_CELLS]; - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - valid_ladder[cell_index(row, col)] = state->map[level][row][col] == BLOCK_PATH; - } - } - Rng ladder_down_key; - rng_split(level_rng, &level_rng, &ladder_down_key); - int ladder_down_index = choice_valid(ladder_down_key, valid_ladder, MAP_CELLS); - int r = ladder_down_index / MAP_SIZE; - int c = ladder_down_index % MAP_SIZE; - state->down_ladders[level][0] = r; - state->down_ladders[level][1] = c; - state->item_map[level][r][c] = ITEM_LADDER_DOWN; - - Rng ladder_up_key; - rng_split(level_rng, &level_rng, &ladder_up_key); - int ladder_up_index = choice_valid(ladder_up_key, valid_ladder, MAP_CELLS); - r = ladder_up_index / MAP_SIZE; - c = ladder_up_index % MAP_SIZE; - state->up_ladders[level][0] = r; - state->up_ladders[level][1] = c; - state->item_map[level][r][c] = ITEM_LADDER_UP; - } - - for (int level = 0; level < NUM_LEVELS; level++) { - for (int i = 0; i < MAX_MELEE_MOBS; i++) { - state->melee_mobs[level].health[i] = 1.0f; - state->passive_mobs[level].health[i] = 1.0f; - state->mob_projectiles[level].health[i] = 1.0f; - state->player_projectiles[level].health[i] = 1.0f; - } - for (int i = 0; i < MAX_RANGED_MOBS; i++) { - state->ranged_mobs[level].health[i] = 1.0f; - } - for (int projectile = 0; projectile < MAX_MOB_PROJECTILES; projectile++) { - state->mob_projectile_directions[level][projectile][0] = 1; - state->mob_projectile_directions[level][projectile][1] = 1; - } - for (int projectile = 0; projectile < MAX_PLAYER_PROJECTILES; projectile++) { - state->player_projectile_directions[level][projectile][0] = 1; - state->player_projectile_directions[level][projectile][1] = 1; - } - } - - Rng potion_key; - rng_split(rng, &rng, &potion_key); - Rng potion_carry; - Rng sort_key; - rng_split(potion_key, &potion_carry, &sort_key); - uint32_t potion_keys[6]; - for (int i = 0; i < 6; i++) { - potion_keys[i] = rng_u32(sort_key, i); - state->potion_mapping[i] = i; - } - for (int i = 1; i < 6; i++) { - uint32_t key_value = potion_keys[i]; - int value = state->potion_mapping[i]; - int j = i - 1; - while (j >= 0 && potion_keys[j] > key_value) { - potion_keys[j + 1] = potion_keys[j]; - state->potion_mapping[j + 1] = state->potion_mapping[j]; - j--; - } - potion_keys[j + 1] = key_value; - state->potion_mapping[j + 1] = value; - } - - Rng state_key; - rng_split(rng, &rng, &state_key); - store_rng(state, state_key); - - state->monsters_killed[0] = 10; - state->player_position[0] = MAP_SIZE / 2; - state->player_position[1] = MAP_SIZE / 2; - state->player_level = 0; - state->player_direction = ACTION_UP; - state->player_health = 9.0f; - state->player_food = 9; - state->player_drink = 9; - state->player_energy = 9; - state->player_mana = 9; - state->player_dexterity = 1; - state->player_strength = 1; - state->player_intelligence = 1; - state->boss_timestep_to_spawn_this_round = BOSS_SPAWN_TURNS; - float cosine = cosf(3.14159265358979323846f * 0.3f); - state->light_level = 1.0f - powf(fabsf(cosine), 3.0f); - memset(state->spawn_land, 0, sizeof(state->spawn_land)); - memset(state->spawn_grave, 0, sizeof(state->spawn_grave)); - memset(state->spawn_water, 0, sizeof(state->spawn_water)); - for (int level = 0; level < NUM_LEVELS; level++) { - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - refresh_spawn_cell(state, level, row, col); - } - } - } -} - -void write_mob_obs(float* obs, const State* state, const Mobs* mobs, int slots, - int channel) { - int level = state->player_level; - for (int i = 0; i < slots; i++) { - if (!mobs->mask[i]) { - continue; - } - int type_id = mobs->type_id[i]; - int world_row = mobs->position[i][0]; - int world_col = mobs->position[i][1]; - int local_row = world_row - state->player_position[0] + OBS_ROWS / 2; - int local_col = world_col - state->player_position[1] + OBS_COLS / 2; - if ((unsigned)local_row >= OBS_ROWS - || (unsigned)local_col >= OBS_COLS) { - continue; - } - if ((unsigned)world_row >= MAP_SIZE - || (unsigned)world_col >= MAP_SIZE - || state->light_map[level][world_row][world_col] <= 12) { - continue; - } - int base = (local_row * OBS_COLS + local_col) * OBS_TILE_CHANNELS; - obs[base + 3 + channel] = type_id + 1; - } -} - -int clampi(int value, int low, int high) { - if (value < low) { - return low; - } - if (value > high) { - return high; - } - return value; -} - -float clampf(float value, float low, float high) { - if (value < low) { - return low; - } - if (value > high) { - return high; - } - return value; -} - -int max_health(const State* state) { - return 8 + state->player_strength; -} - -int equipped_armour(const State* state) { - return state->inventory.armour[0] + state->inventory.armour[1] - + state->inventory.armour[2] + state->inventory.armour[3]; -} - -int max_food(const State* state) { - return 7 + 2 * state->player_dexterity; -} - -int max_drink(const State* state) { - return 7 + 2 * state->player_dexterity; -} - -int max_energy(const State* state) { - return 7 + 2 * state->player_dexterity; -} - -int max_mana(const State* state) { - return 6 + 3 * state->player_intelligence; -} - -bool fighting_boss(const State* state) { - return state->player_level == NUM_LEVELS - 1; -} - -bool boss_vulnerable(const State* state) { - if (state->boss_timestep_to_spawn_this_round > 0) { - return false; - } - int level = state->player_level; - for (int i = 0; i < MAX_MELEE_MOBS; i++) { - if (state->melee_mobs[level].mask[i]) { - return false; - } - } - for (int i = 0; i < MAX_RANGED_MOBS; i++) { - if (state->ranged_mobs[level].mask[i]) { - return false; - } - } - return true; -} - -void action_to_direction(int action, int direction[2]) { - direction[0] = 0; - direction[1] = 0; - - if (action == ACTION_LEFT) { - direction[1] = -1; - } else if (action == ACTION_RIGHT) { - direction[1] = 1; - } else if (action == ACTION_UP) { - direction[0] = -1; - } else if (action == ACTION_DOWN) { - direction[0] = 1; - } -} - -bool is_solid_block(int block) { - switch (block) { - case BLOCK_STONE: - case BLOCK_TREE: - case BLOCK_COAL: - case BLOCK_IRON: - case BLOCK_DIAMOND: - case BLOCK_CRAFTING_TABLE: - case BLOCK_FURNACE: - case BLOCK_PLANT: - case BLOCK_RIPE_PLANT: - case BLOCK_WALL: - case BLOCK_WALL_MOSS: - case BLOCK_STALAGMITE: - case BLOCK_RUBY: - case BLOCK_SAPPHIRE: - case BLOCK_CHEST: - case BLOCK_FOUNTAIN: - case BLOCK_FIRE_TREE: - case BLOCK_ENCHANTMENT_TABLE_FIRE: - case BLOCK_ENCHANTMENT_TABLE_ICE: - case BLOCK_GRAVE: - case BLOCK_GRAVE2: - case BLOCK_GRAVE3: - case BLOCK_NECROMANCER: - return true; - default: - return false; - } -} - -bool mob_at(const State* state, int level, int row, int col) { - if ((unsigned)row >= MAP_SIZE || (unsigned)col >= MAP_SIZE) { - return false; - } - return (state->mob_bits[level][row] >> col) & 1ull; -} - -void set_mob_bit(State* state, int level, int row, int col, bool on) { - if ((unsigned)row >= MAP_SIZE || (unsigned)col >= MAP_SIZE) { - return; - } - uint64_t bit = 1ull << col; - if (on) { - state->mob_bits[level][row] |= bit; - } else { - state->mob_bits[level][row] &= ~bit; - } -} - -void move_mob_occupancy(State* state, int level, int old_row, int old_col, - int new_row, int new_col, bool keep) { - set_mob_bit(state, level, old_row, old_col, false); - if (keep) { - set_mob_bit(state, level, new_row, new_col, true); - } -} - -bool mobs_at(const Mobs* mobs, int slots, int row, int col, int* slot) { - for (int i = 0; i < slots; i++) { - if (mobs->mask[i] - && mobs->position[i][0] == row - && mobs->position[i][1] == col) { - *slot = i; - return true; - } - } - return false; -} - -Mobs* mobs_for_class(State* state, int level, int mob_class) { - if (mob_class == MOB_PASSIVE) { - return &state->passive_mobs[level]; - } - if (mob_class == MOB_RANGED) { - return &state->ranged_mobs[level]; - } - return &state->melee_mobs[level]; -} - -bool find_mob_at(const State* state, int level, int row, int col, int* mob_class, - int* slot) { - if (mobs_at(&state->melee_mobs[level], MAX_MELEE_MOBS, row, col, slot)) { - *mob_class = MOB_MELEE; - return true; - } - if (mobs_at(&state->passive_mobs[level], MAX_PASSIVE_MOBS, row, col, slot)) { - *mob_class = MOB_PASSIVE; - return true; - } - if (mobs_at(&state->ranged_mobs[level], MAX_RANGED_MOBS, row, col, slot)) { - *mob_class = MOB_RANGED; - return true; - } - return false; -} - -bool valid_typed_mob_position(const State* state, int level, int mob_class, - int type_id, int row, int col, int old_row, int old_col) { - if (row < 0 || row >= MAP_SIZE || col < 0 || col >= MAP_SIZE) { - return false; - } - if (row == state->player_position[0] && col == state->player_position[1]) { - return false; - } - int block = state->map[level][row][col]; - if (is_solid_block(block)) { - return false; - } - static const bool blocked[NUM_MOB_TYPES][3][3] = { - {{0,1,1},{0,1,1},{0,1,1}}, {{0,0,0},{0,1,1},{0,1,1}}, - {{0,1,1},{0,1,1},{0,1,1}}, {{0,1,1},{0,0,1},{0,1,1}}, - {{0,1,1},{0,1,1},{0,1,1}}, {{0,1,1},{0,1,1},{1,0,1}}, - {{0,1,1},{0,1,1},{0,0,0}}, {{0,1,1},{0,1,1},{0,0,0}}, - }; - int terrain = block == BLOCK_WATER ? 1 : (block == BLOCK_LAVA ? 2 : 0); - if (blocked[clampi(type_id, 0, 7)][clampi(mob_class, 0, 2)][terrain]) { - return false; - } - return !mob_at(state, level, row, col) || (row == old_row && col == old_col); -} - -typedef struct { float physical, fire, ice; } Damage; - -Damage mob_damage_vector(int type, int mob_class) { - static const float damage[NUM_MOB_TYPES][4][3] = { - {{0,0,0},{2,0,0},{0,0,0},{2,0,0}}, {{0,0,0},{4,0,0},{0,0,0},{4,0,0}}, - {{0,0,0},{3,0,0},{0,0,0},{0,3,0}}, {{0,0,0},{5,0,0},{0,0,0},{0,0,3}}, - {{0,0,0},{6,0,0},{0,0,0},{5,0,0}}, {{0,0,0},{6,1,1},{0,0,0},{4,3,3}}, - {{0,0,0},{3,5,0},{0,0,0},{3,5,0}}, {{0,0,0},{4,0,5},{0,0,0},{4,0,5}}, - }; - const float* d = damage[clampi(type, 0, 7)][clampi(mob_class, 0, 3)]; - return (Damage){d[0], d[1], d[2]}; -} - -float damage_to_mob(Damage damage, int type, int mob_class) { - static const float defense[NUM_MOB_TYPES][4][3] = { - {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}, - {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}, - {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}, - {{0,0,0},{0,0,0},{0,0,0},{0,0,0}}, - {{0,0,0},{.5f,0,0},{.5f,0,0},{0,0,0}}, - {{0,0,0},{.2f,0,0},{0,0,0},{0,0,0}}, - {{0,0,0},{.9f,1,0},{.9f,1,0},{0,0,0}}, - {{0,0,0},{.9f,0,1},{.9f,0,1},{0,0,0}}, - }; - const float* d = defense[clampi(type, 0, 7)][clampi(mob_class, 0, 3)]; - return damage.physical * (1-d[0]) + damage.fire * (1-d[1]) + damage.ice * (1-d[2]); -} - -float damage_to_player(const State* state, Damage damage) { - float physical_defense = 0, fire_defense = 0, ice_defense = 0; - for (int i = 0; i < 4; i++) { - physical_defense += 0.1f * state->inventory.armour[i]; - fire_defense += 0.2f * (state->armour_enchantments[i] == 1); - ice_defense += 0.2f * (state->armour_enchantments[i] == 2); - } - float coeff = fighting_boss(state) ? 1.5f : 1.0f; - return coeff * (damage.physical * (1 - physical_defense) - + damage.fire * (1 - fire_defense) + damage.ice * (1 - ice_defense)); -} - -bool damage_mob_at(State* state, int level, int row, int col, float damage, - bool can_eat, bool can_get_achievement) { - int mob_class; - int slot; - if (!find_mob_at(state, level, row, col, &mob_class, &slot)) { - return false; - } - Mobs* mobs = mobs_for_class(state, level, mob_class); - if (!mobs->mask[slot]) { - return false; - } - - mobs->health[slot] -= damage; - if (mobs->health[slot] > 0.0f) { - return true; - } - - int type_id = mobs->type_id[slot]; - mobs->mask[slot] = false; - set_mob_bit(state, level, row, col, false); - state->monsters_killed[level] += mob_class == MOB_PASSIVE ? 0 : 1; - if (can_get_achievement) { - static const int achievements[3][8] = { - {ACH_EAT_COW, ACH_EAT_BAT, ACH_EAT_SNAIL, 0, 0, 0, 0, 0}, - {ACH_DEFEAT_ZOMBIE, ACH_DEFEAT_GNOME_WARRIOR, ACH_DEFEAT_ORC_SOLIDER, - ACH_DEFEAT_LIZARD, ACH_DEFEAT_KNIGHT, ACH_DEFEAT_TROLL, - ACH_DEFEAT_PIGMAN, ACH_DEFEAT_FROST_TROLL}, - {ACH_DEFEAT_SKELETON, ACH_DEFEAT_GNOME_ARCHER, ACH_DEFEAT_ORC_MAGE, - ACH_DEFEAT_KOBOLD, ACH_DEFEAT_ARCHER, ACH_DEFEAT_DEEP_THING, - ACH_DEFEAT_FIRE_ELEMENTAL, ACH_DEFEAT_ICE_ELEMENTAL}, - }; - state->achievements[achievements[clampi(mob_class, 0, 2)][clampi(type_id, 0, 7)]] = 1; - } - - if (mob_class == MOB_PASSIVE && can_eat) { - state->player_food = clampi(state->player_food + 6, 0, max_food(state)); - state->player_hunger = 0.0f; - } - return true; -} - -bool spawn_projectile(State* state, bool from_player, int projectile_type, - int row, int col, int dir_row, int dir_col) { - int level = state->player_level; - Mobs* projectiles = from_player ? &state->player_projectiles[level] : &state->mob_projectiles[level]; - int (*directions)[MAX_PLAYER_PROJECTILES][2] = - from_player ? state->player_projectile_directions : state->mob_projectile_directions; - for (int i = 0; i < MAX_PLAYER_PROJECTILES; i++) { - if (projectiles->mask[i]) { - continue; - } - projectiles->position[i][0] = row; - projectiles->position[i][1] = col; - Damage d = mob_damage_vector(projectile_type, MOB_PROJECTILE); - projectiles->health[i] = d.physical + d.fire + d.ice; - projectiles->attack_cooldown[i] = 0; - projectiles->type_id[i] = projectile_type; - projectiles->mask[i] = true; - directions[level][i][0] = dir_row; - directions[level][i][1] = dir_col; - return true; - } - return false; -} - -void update_projectile_set(State* state, bool from_player) { - int level = state->player_level; - for (int i = 0; i < MAX_PLAYER_PROJECTILES; i++) { - if (from_player) { - Mobs* projectiles = &state->player_projectiles[level]; - if (!projectiles->mask[i]) { - continue; - } - int old_row = projectiles->position[i][0]; - int old_col = projectiles->position[i][1]; - int proposed_row = old_row + state->player_projectile_directions[level][i][0]; - int proposed_col = old_col + state->player_projectile_directions[level][i][1]; - int ptype = projectiles->type_id[i]; - Damage vector = mob_damage_vector(ptype, MOB_PROJECTILE); - bool arrow = ptype == PROJECTILE_ARROW || ptype == PROJECTILE_ARROW2; - if (arrow && state->bow_enchantment == 1) { - vector.fire += vector.physical * 0.5f; - } - if (arrow && state->bow_enchantment == 2) { - vector.ice += vector.physical * 0.5f; - } - float coeff = 1.0f; - if (arrow) { - coeff = 1.0f + 0.2f * (state->player_dexterity - 1); - } else if (ptype == PROJECTILE_FIREBALL || ptype == PROJECTILE_ICEBALL) { - coeff = 1.0f + 0.5f * (state->player_intelligence - 1); - } - vector.physical *= coeff; - vector.fire *= coeff; - vector.ice *= coeff; - - bool hit_old = false; - int mob_class; - int mob_slot; - if (find_mob_at(state, level, old_row, old_col, &mob_class, &mob_slot)) { - Mobs* target = mobs_for_class(state, level, mob_class); - hit_old = damage_mob_at( - state, level, old_row, old_col, - damage_to_mob(vector, target->type_id[mob_slot], mob_class), - false, true - ); - } - - Damage second = vector; - if (hit_old) { - second.physical = 0.0f; - second.fire = 0.0f; - second.ice = 0.0f; - } - bool hit_new = false; - if (find_mob_at(state, level, proposed_row, proposed_col, &mob_class, &mob_slot)) { - Mobs* target = mobs_for_class(state, level, mob_class); - hit_new = damage_mob_at( - state, level, proposed_row, proposed_col, - damage_to_mob(second, target->type_id[mob_slot], mob_class), - false, true - ); - } - - bool proposed_in_bounds = proposed_row >= 0 && proposed_row < MAP_SIZE - && proposed_col >= 0 && proposed_col < MAP_SIZE; - int proposed_block = proposed_in_bounds ? state->map[level][proposed_row][proposed_col] : 0; - bool in_wall = is_solid_block(proposed_block) && proposed_block != BLOCK_WATER; - bool keep = proposed_in_bounds && !in_wall && !hit_old && !hit_new; - projectiles->position[i][0] = proposed_row; - projectiles->position[i][1] = proposed_col; - projectiles->mask[i] = keep; - } else { - Mobs* projectiles = &state->mob_projectiles[level]; - if (!projectiles->mask[i]) { - continue; - } - int old_row = projectiles->position[i][0]; - int old_col = projectiles->position[i][1]; - int proposed_row = old_row + state->mob_projectile_directions[level][i][0]; - int proposed_col = old_col + state->mob_projectile_directions[level][i][1]; - bool proposed_in_player = proposed_row == state->player_position[0] - && proposed_col == state->player_position[1]; - bool proposed_in_bounds = proposed_row >= 0 && proposed_row < MAP_SIZE - && proposed_col >= 0 && proposed_col < MAP_SIZE; - int proposed_block = proposed_in_bounds ? state->map[level][proposed_row][proposed_col] : 0; - bool in_wall = is_solid_block(proposed_block) && proposed_block != BLOCK_WATER; - bool in_mob = mob_at(state, level, proposed_row, proposed_col) - || (state->player_position[0] == proposed_row - && state->player_position[1] == proposed_col); - bool keep_moving = proposed_in_bounds && !in_wall && !in_mob; - bool hit_player = ( - (old_row == state->player_position[0] && old_col == state->player_position[1]) - || proposed_in_player - ); - keep_moving = keep_moving && !hit_player; - bool hit_bench = proposed_block == BLOCK_FURNACE - || proposed_block == BLOCK_CRAFTING_TABLE; - int new_block = hit_bench ? BLOCK_PATH : proposed_block; - - projectiles->position[i][0] = proposed_row; - projectiles->position[i][1] = proposed_col; - projectiles->mask[i] = keep_moving; - if (hit_player) { - state->player_health -= damage_to_player( - state, mob_damage_vector(projectiles->type_id[i], MOB_PROJECTILE)); - state->is_sleeping = false; - state->is_resting = false; - } - if ((unsigned)proposed_row < MAP_SIZE && (unsigned)proposed_col < MAP_SIZE) { - set_block(state, level, proposed_row, proposed_col, new_block); - } - } - } -} - -int floor_mob_type(int level, int mob_class) { - static const int types[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}, - }; - return types[clampi(level, 0, NUM_LEVELS - 1)][clampi(mob_class, 0, 2)]; -} - -int collect_spawn_cells(const State* state, int level, int min_exclusive, - int max_exclusive, bool boss, bool water_only, int* rows, int* cols) { - const uint64_t* terrain = boss - ? state->spawn_grave[level] - : (water_only ? state->spawn_water[level] : state->spawn_land[level]); - - int pr = state->player_position[0]; - int pc = state->player_position[1]; - int limit = MOB_DESPAWN_DISTANCE - 1; - int r0 = pr - limit; - int r1 = pr + limit; - int c0 = pc - limit; - int c1 = pc + limit; - if (r0 < 0) { - r0 = 0; - } - if (r1 > MAP_SIZE - 1) { - r1 = MAP_SIZE - 1; - } - if (c0 < 0) { - c0 = 0; - } - if (c1 > MAP_SIZE - 1) { - c1 = MAP_SIZE - 1; - } - uint64_t col_mask = (~0ull << c0) & ((1ull << (c1 + 1)) - 1); - int count = 0; - for (int row = r0; row <= r1; row++) { - int dr = row - pr; - int dr2 = dr * dr; - uint64_t bits = terrain[row] & ~state->mob_bits[level][row] & col_mask; - while (bits) { - int col = __builtin_ctzll(bits); - bits &= bits - 1; - int dc = col - pc; - int distance2 = dr2 + dc * dc; - if (distance2 > min_exclusive && distance2 < max_exclusive) { - rows[count] = row; - cols[count] = col; - count++; - } - } - } - return count; -} - -bool pick_spawn_cell(const int* rows, const int* cols, int count, Rng key, - int* out_row, int* out_col) { - if (count <= 0) { - return false; - } - float draw = count * (1.0f - rng_f32(key, 0)); - int chosen = (int)ceilf(draw) - 1; - if (chosen < 0) { - chosen = 0; - } - if (chosen >= count) { - chosen = count - 1; - } - *out_row = rows[chosen]; - *out_col = cols[chosen]; - return true; -} - -void spawn_into_slot(State* state, int level, Mobs* mobs, int slot, int mob_class, - int type_id, int row, int col) { - static const float passive_health[NUM_MOB_TYPES] = {3, 4, 6, 8, 0, 0, 0, 0}; - static const float melee_health[NUM_MOB_TYPES] = {5, 7, 9, 11, 12, 20, 20, 24}; - static const float ranged_health[NUM_MOB_TYPES] = {3, 5, 6, 8, 12, 4, 14, 16}; - int idx = clampi(type_id, 0, NUM_MOB_TYPES - 1); - float health = melee_health[idx]; - if (mob_class == MOB_PASSIVE) { - health = passive_health[idx]; - } else if (mob_class == MOB_RANGED) { - health = ranged_health[idx]; - } - mobs->position[slot][0] = row; - mobs->position[slot][1] = col; - mobs->health[slot] = health; - mobs->mask[slot] = true; - set_mob_bit(state, level, row, col, true); -} - -void count_and_empty(const Mobs* mobs, int slots, int* count, int* empty) { - int n = 0; - int first = 0; - bool found = false; - for (int i = 0; i < slots; i++) { - n += mobs->mask[i] ? 1 : 0; - if (!mobs->mask[i] && !found) { - first = i; - found = true; - } - } - *count = n; - *empty = first; -} - -void choose_direction(Rng key, int count, int direction[2]) { - int choice = randint(key, 0u, 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; - } -} - -int choose_player_axis(Rng key, int distance_row, int distance_col) { - int total = distance_row + distance_col; - if (total == 0) { - return 1; - } - int maximum = distance_row > distance_col ? distance_row : distance_col; - float weights[2] = { - distance_row == maximum ? 1.0f / total : 0.0f, - distance_col == maximum ? 1.0f / total : 0.0f, - }; - float sum = weights[0] + weights[1]; - float draw = sum * (1.0f - rng_f32(key, 0)); - return (weights[0] >= draw || sum == 0.0f) ? 0 : 1; -} - -int signi(int value) { - if (value < 0) { - return -1; - } - return value > 0 ? 1 : 0; -} - -void move_melee_slot(State* state, int level, int slot, Rng* rng) { - Mobs* mobs = &state->melee_mobs[level]; - bool alive = mobs->mask[slot]; - if (!alive) { - return; - } - int old_row = mobs->position[slot][0]; - int old_col = mobs->position[slot][1]; - int type_id = mobs->type_id[slot]; - int cooldown = mobs->attack_cooldown[slot]; - - int random_dir[2]; - choose_direction(rng_key(rng), 4, random_dir); - int distance_row = abs(state->player_position[0] - old_row); - int distance_col = abs(state->player_position[1] - old_col); - int axis = choose_player_axis(rng_key(rng), distance_row, distance_col); - int player_dir[2] = {0, 0}; - if (axis == 0) { - player_dir[0] = signi(state->player_position[0] - old_row); - } else { - player_dir[1] = signi(state->player_position[1] - old_col); - } - int dist = distance_row + distance_col; - float chase_roll = rng_f32(rng_key(rng), 0); - bool chase = (dist < 10 || fighting_boss(state)) && chase_roll < 0.75f; - int proposed_row = chase ? old_row + player_dir[0] : old_row + random_dir[0]; - int proposed_col = chase ? old_col + player_dir[1] : old_col + random_dir[1]; - bool attacking = dist == 1 && cooldown <= 0 && alive; - if (attacking) { - proposed_row = old_row; - proposed_col = old_col; - Damage damage = mob_damage_vector(type_id, MOB_MELEE); - float sleep = 1.0f + 2.5f * state->is_sleeping; - damage.physical *= sleep; - damage.fire *= sleep; - damage.ice *= sleep; - state->player_health -= damage_to_player(state, damage); - state->achievements[ACH_WAKE_UP] = state->achievements[ACH_WAKE_UP] || state->is_sleeping; - state->is_sleeping = false; - state->is_resting = false; - } - int new_cooldown = attacking ? 5 : cooldown - 1; - bool valid = valid_typed_mob_position(state, level, MOB_MELEE, type_id, - proposed_row, proposed_col, old_row, old_col); - int new_row = valid ? proposed_row : old_row; - int new_col = valid ? proposed_col : old_col; - bool keep = alive && (dist < MOB_DESPAWN_DISTANCE || fighting_boss(state)); - Rng unused; - rng_split(*rng, &unused, rng); - - move_mob_occupancy(state, level, old_row, old_col, new_row, new_col, keep); - mobs->position[slot][0] = new_row; - mobs->position[slot][1] = new_col; - mobs->attack_cooldown[slot] = new_cooldown; - mobs->mask[slot] = keep; -} - -void move_passive_slot(State* state, int level, int slot, Rng* rng) { - Mobs* mobs = &state->passive_mobs[level]; - bool alive = mobs->mask[slot]; - if (!alive) { - return; - } - int old_row = mobs->position[slot][0]; - int old_col = mobs->position[slot][1]; - int type_id = mobs->type_id[slot]; - int direction[2]; - choose_direction(rng_key(rng), 8, direction); - int proposed_row = old_row + direction[0]; - int proposed_col = old_col + direction[1]; - bool valid = valid_typed_mob_position(state, level, MOB_PASSIVE, type_id, - proposed_row, proposed_col, old_row, old_col); - int new_row = valid ? proposed_row : old_row; - int new_col = valid ? proposed_col : old_col; - int dist = abs(state->player_position[0] - old_row) + abs(state->player_position[1] - old_col); - bool keep = alive && dist < MOB_DESPAWN_DISTANCE; - move_mob_occupancy(state, level, old_row, old_col, new_row, new_col, keep); - mobs->position[slot][0] = new_row; - mobs->position[slot][1] = new_col; - mobs->mask[slot] = keep; -} - -void move_ranged_slot(State* state, int level, int slot, Rng* rng) { - Mobs* mobs = &state->ranged_mobs[level]; - bool alive = mobs->mask[slot]; - if (!alive) { - return; - } - int old_row = mobs->position[slot][0]; - int old_col = mobs->position[slot][1]; - int type_id = mobs->type_id[slot]; - int cooldown = mobs->attack_cooldown[slot]; - - int random_dir[2]; - choose_direction(rng_key(rng), 4, random_dir); - int distance_row = abs(state->player_position[0] - old_row); - int distance_col = abs(state->player_position[1] - old_col); - int axis = choose_player_axis(rng_key(rng), distance_row, distance_col); - int player_dir[2] = {0, 0}; - if (axis == 0) { - player_dir[0] = signi(state->player_position[0] - old_row); - } else { - player_dir[1] = signi(state->player_position[1] - old_col); - } - int dist = distance_row + distance_col; - int proposed_row = dist >= 6 ? old_row + player_dir[0] : old_row + random_dir[0]; - int proposed_col = dist >= 6 ? old_col + player_dir[1] : old_col + random_dir[1]; - if (dist <= 3) { - proposed_row = old_row - player_dir[0]; - proposed_col = old_col - player_dir[1]; - } - if (rng_f32(rng_key(rng), 0) <= 0.85f) { - proposed_row = old_row + random_dir[0]; - proposed_col = old_col + random_dir[1]; - } - bool valid = valid_typed_mob_position(state, level, MOB_RANGED, type_id, - proposed_row, proposed_col, old_row, old_col); - bool attacking = ((dist >= 4 && dist <= 5) || (dist <= 3 && !valid)) && cooldown <= 0 && alive; - if (attacking) { - static const int projectile[8] = { - PROJECTILE_ARROW, PROJECTILE_ARROW, PROJECTILE_FIREBALL, PROJECTILE_DAGGER, - PROJECTILE_ARROW2, PROJECTILE_SLIMEBALL, PROJECTILE_FIREBALL2, PROJECTILE_ICEBALL2 - }; - spawn_projectile(state, false, projectile[clampi(type_id, 0, 7)], - old_row, old_col, player_dir[0], player_dir[1]); - proposed_row = old_row; - proposed_col = old_col; - } - int new_cooldown = attacking ? 4 : cooldown - 1; - valid = valid_typed_mob_position(state, level, MOB_RANGED, type_id, - proposed_row, proposed_col, old_row, old_col); - int new_row = valid ? proposed_row : old_row; - int new_col = valid ? proposed_col : old_col; - bool keep = alive && (dist < MOB_DESPAWN_DISTANCE || fighting_boss(state)); - move_mob_occupancy(state, level, old_row, old_col, new_row, new_col, keep); - mobs->position[slot][0] = new_row; - mobs->position[slot][1] = new_col; - mobs->attack_cooldown[slot] = new_cooldown; - mobs->mask[slot] = keep; -} - -int choose_weighted_key(Rng key, const float* weights, int count) { - float total = 0.0f; - for (int i = 0; i < count; i++) { - total += weights[i]; - } - float draw = total * (1.0f - rng_f32(key, 0)); - float cumulative = 0.0f; - for (int i = 0; i < count; i++) { - cumulative += weights[i]; - if (cumulative >= draw) { - return i; - } - } - return count - 1; -} - -void compute_action_mask(Craftax* env) { - unsigned char* m = env->agents[0].action_mask; - if (m == NULL) { - return; - } - if (!env->use_action_mask) { - memset(m, 1, ATN_DIM); - return; - } - const State* s = &env->state; - const Inventory* inv = &s->inventory; - memset(m, 0, ATN_DIM); - m[ACTION_NOOP] = 1; - if (s->is_sleeping || s->is_resting) { - return; - } - m[ACTION_LEFT] = m[ACTION_RIGHT] = m[ACTION_UP] = m[ACTION_DOWN] = m[ACTION_DO] = 1; - m[ACTION_SLEEP] = s->player_energy < max_energy(s); - m[ACTION_REST] = s->player_health < max_health(s); - m[ACTION_PLACE_STONE] = m[ACTION_PLACE_FURNACE] = inv->stone > 0; - m[ACTION_PLACE_TABLE] = inv->wood >= 2; - m[ACTION_PLACE_PLANT] = inv->sapling > 0; - m[ACTION_PLACE_TORCH] = inv->torches > 0; - m[ACTION_MAKE_WOOD_PICKAXE] = inv->wood > 0 && inv->pickaxe < 1; - m[ACTION_MAKE_STONE_PICKAXE] = inv->wood > 0 && inv->stone > 0 && inv->pickaxe < 2; - m[ACTION_MAKE_IRON_PICKAXE] = inv->wood > 0 && inv->stone > 0 && inv->iron > 0 - && inv->coal > 0 && inv->pickaxe < 3; - m[ACTION_MAKE_DIAMOND_PICKAXE] = inv->wood > 0 && inv->diamond >= 3 && inv->pickaxe < 4; - m[ACTION_MAKE_WOOD_SWORD] = inv->wood > 0 && inv->sword < 1; - m[ACTION_MAKE_STONE_SWORD] = inv->wood > 0 && inv->stone > 0 && inv->sword < 2; - m[ACTION_MAKE_IRON_SWORD] = inv->wood > 0 && inv->stone > 0 && inv->iron > 0 - && inv->coal > 0 && inv->sword < 3; - m[ACTION_MAKE_DIAMOND_SWORD] = inv->wood > 0 && inv->diamond >= 2 && inv->sword < 4; - m[ACTION_MAKE_ARROW] = inv->wood > 0 && inv->stone > 0 && inv->arrows < 99; - m[ACTION_MAKE_TORCH] = inv->wood > 0 && inv->coal > 0 && inv->torches < 99; - int missing_iron = 0; - int missing_diamond = 0; - int armour = 0; - for (int k = 0; k < 4; k++) { - missing_iron += inv->armour[k] < 1; - missing_diamond += inv->armour[k] < 2; - armour += inv->armour[k]; - } - m[ACTION_MAKE_IRON_ARMOUR] = missing_iron && inv->iron >= 3 && inv->coal >= 3; - m[ACTION_MAKE_DIAMOND_ARMOUR] = missing_diamond && inv->diamond >= 3; - int item = s->item_map[s->player_level][s->player_position[0]][s->player_position[1]]; - m[ACTION_DESCEND] = item == ITEM_LADDER_DOWN - && s->monsters_killed[s->player_level] >= MONSTERS_KILLED_TO_CLEAR_LEVEL - && s->player_level < NUM_LEVELS - 1; - m[ACTION_ASCEND] = item == ITEM_LADDER_UP && s->player_level > 0; - m[ACTION_SHOOT_ARROW] = inv->bow > 0 && inv->arrows > 0; - m[ACTION_CAST_FIREBALL] = s->learned_spells[0] && s->player_mana >= 2; - m[ACTION_CAST_ICEBALL] = s->learned_spells[1] && s->player_mana >= 2; - for (int k = 0; k < NUM_POTIONS; k++) { - m[ACTION_DRINK_POTION_RED + k] = inv->potions[k] > 0; - } - m[ACTION_READ_BOOK] = inv->books > 0; - int enchant = s->player_mana >= 9 && (inv->ruby > 0 || inv->sapphire > 0); - m[ACTION_ENCHANT_SWORD] = enchant && inv->sword > 0; - m[ACTION_ENCHANT_ARMOUR] = enchant && armour > 0; - m[ACTION_ENCHANT_BOW] = enchant && inv->bow > 0; - m[ACTION_LEVEL_UP_DEXTERITY] = s->player_xp > 0 && s->player_dexterity < MAX_ATTRIBUTE; - m[ACTION_LEVEL_UP_STRENGTH] = s->player_xp > 0 && s->player_strength < MAX_ATTRIBUTE; - m[ACTION_LEVEL_UP_INTELLIGENCE] = s->player_xp > 0 && s->player_intelligence < MAX_ATTRIBUTE; -} - -void compute_observations(Craftax* env) { - State* state = &env->state; - float* obs = env->agents[0].observations; - const int map_obs = OBS_ROWS * OBS_COLS * OBS_TILE_CHANNELS; - memset(obs, 0, map_obs * sizeof(float)); - - int level = state->player_level; - int row = state->player_position[0]; - int col = state->player_position[1]; - const int row_radius = OBS_ROWS / 2; - const int col_radius = OBS_COLS / 2; - int r0 = -row_radius; - int r1 = row_radius; - int c0 = -col_radius; - int c1 = col_radius; - if (row + r0 < 0) { - r0 = -row; - } - if (row + r1 >= MAP_SIZE) { - r1 = MAP_SIZE - 1 - row; - } - if (col + c0 < 0) { - c0 = -col; - } - if (col + c1 >= MAP_SIZE) { - c1 = MAP_SIZE - 1 - col; - } - - for (int r = r0; r <= r1; r++) { - int obs_row = row + r; - uint8_t* map_row = state->map[level][obs_row]; - uint8_t* item_row = state->item_map[level][obs_row]; - uint8_t* light_row = state->light_map[level][obs_row]; - float* tile = obs + ((r + row_radius) * OBS_COLS + (c0 + col_radius)) - * OBS_TILE_CHANNELS; - for (int c = c0; c <= c1; c++) { - int obs_col = col + c; - if (light_row[obs_col] > 12) { - tile[0] = map_row[obs_col]; - tile[1] = item_row[obs_col] + 1; - tile[2] = 1.0f; - } - tile += OBS_TILE_CHANNELS; - } - } - - write_mob_obs(obs, state, &state->melee_mobs[level], MAX_MELEE_MOBS, 0); - write_mob_obs(obs, state, &state->passive_mobs[level], MAX_PASSIVE_MOBS, 1); - write_mob_obs(obs, state, &state->ranged_mobs[level], MAX_RANGED_MOBS, 2); - write_mob_obs(obs, state, &state->mob_projectiles[level], MAX_MOB_PROJECTILES, 3); - write_mob_obs(obs, state, &state->player_projectiles[level], MAX_PLAYER_PROJECTILES, 4); - - int obs_idx = map_obs; - - obs[obs_idx++] = sqrtf(state->inventory.wood) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.stone) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.coal) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.iron) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.diamond) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.sapphire) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.ruby) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.sapling) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.torches) / 10.0f; - obs[obs_idx++] = sqrtf(state->inventory.arrows) / 10.0f; - obs[obs_idx++] = state->inventory.books / 2.0f; - obs[obs_idx++] = state->inventory.pickaxe / 4.0f; - obs[obs_idx++] = state->inventory.sword / 4.0f; - obs[obs_idx++] = state->sword_enchantment; - obs[obs_idx++] = state->bow_enchantment; - obs[obs_idx++] = state->inventory.bow; - for (int i = 0; i < NUM_POTIONS; i++) { - obs[obs_idx++] = sqrtf(state->inventory.potions[i]) / 10.0f; - } - - obs[obs_idx++] = state->player_health / 10.0f; - obs[obs_idx++] = state->player_food / 10.0f; - obs[obs_idx++] = state->player_drink / 10.0f; - obs[obs_idx++] = state->player_energy / 10.0f; - obs[obs_idx++] = state->player_mana / 10.0f; - obs[obs_idx++] = state->player_xp / 10.0f; - obs[obs_idx++] = state->player_dexterity / 10.0f; - obs[obs_idx++] = state->player_strength / 10.0f; - obs[obs_idx++] = state->player_intelligence / 10.0f; - - int direction_index = state->player_direction - ACTION_LEFT; - for (int i = 0; i < 4; i++) { - obs[obs_idx++] = i == direction_index ? 1.0f : 0.0f; - } - for (int i = 0; i < 4; i++) { - obs[obs_idx++] = state->inventory.armour[i] / 2.0f; - } - for (int i = 0; i < 4; i++) { - obs[obs_idx++] = state->armour_enchantments[i]; - } - - obs[obs_idx++] = state->light_level; - obs[obs_idx++] = state->is_sleeping ? 1.0f : 0.0f; - obs[obs_idx++] = state->is_resting ? 1.0f : 0.0f; - obs[obs_idx++] = state->learned_spells[0] ? 1.0f : 0.0f; - obs[obs_idx++] = state->learned_spells[1] ? 1.0f : 0.0f; - obs[obs_idx++] = state->player_level / 10.0f; - obs[obs_idx++] = state->monsters_killed[level] >= MONSTERS_KILLED_TO_CLEAR_LEVEL ? 1.0f : 0.0f; - obs[obs_idx++] = boss_vulnerable(state) ? 1.0f : 0.0f; - - compute_action_mask(env); -} - -void update_log_state(Craftax* env) { - if (env->state.player_level > env->max_floor_accum) { - env->max_floor_accum = env->state.player_level; - } -} - -#define CRAFTAX_CLEAN_IN_HEADER -#include "craftax_clean.c" - -void puf_reset(Craftax* env) { - env->agents[0].rewards[0] = 0.0f; - env->agents[0].terminals[0] = 0.0f; - env->episode_return_accum = 0.0f; - env->episode_length_accum = 0; - env->max_floor_accum = 0; - memset(env->achievements, 0, sizeof(env->achievements)); - - Rng initial = rng_seed((uint32_t)env->seed); - if (env->reset_pool_size > 0) { - Rng discard; - rng_split(initial, &env->env_rng, &discard); - int idx = env->seed % env->reset_pool_size; - memcpy(&env->state, &env->reset_pool[idx], sizeof(State)); - } else { - Rng reset_key; - rng_split(initial, &env->env_rng, &reset_key); - Rng unused; - Rng world_key; - rng_split(reset_key, &unused, &world_key); - generate_world_from_key(&env->state, world_key); - } - compute_observations(env); - update_log_state(env); -} - -void puf_step(Craftax* env) { - if (craftax_clean_human_controls(env) < 0) { - return; - } - CLEAN_PROF_START(); - env->agents[0].rewards[0] = 0.0f; - env->agents[0].terminals[0] = 0.0f; - int action = env->agents[0].actions[0]; - - State* state = &env->state; - int initial_achievements[NUM_ACHIEVEMENTS]; - memcpy(initial_achievements, state->achievements, sizeof(initial_achievements)); - float initial_health = state->player_health; - int initial_armour = equipped_armour(state); - - // Sleep/rest used to return control every tick as forced NOOPs (~100 - // agent steps). Collapse those ticks into this one puf_step so credit - // assignment sees a single action that ends on wake, hit, or death. - Rng reset_key = 0; - bool done = false; - do { - Rng step_key; - rng_split(env->env_rng, &env->env_rng, &step_key); - Rng step_rng; - rng_split(step_key, &step_rng, &reset_key); - - if (state->is_sleeping || state->is_resting) { - action = ACTION_NOOP; - } - - CLEAN_ZONE(0); - int level = state->player_level; - int row = state->player_position[0]; - int col = state->player_position[1]; - - bool on_down_ladder = state->item_map[level][row][col] == ITEM_LADDER_DOWN; - bool can_move_down = action == ACTION_DESCEND - && on_down_ladder - && state->monsters_killed[level] >= MONSTERS_KILLED_TO_CLEAR_LEVEL - && level < NUM_LEVELS - 1; - - bool on_up_ladder = state->item_map[level][row][col] == ITEM_LADDER_UP; - bool can_move_up = action == ACTION_ASCEND - && on_up_ladder - && level > 0; - - if (can_move_down || can_move_up) { - int new_level = level + (can_move_down ? 1 : -1); - if (can_move_down) { - state->player_position[0] = state->up_ladders[new_level][0]; - state->player_position[1] = state->up_ladders[new_level][1]; - } else { - state->player_position[0] = state->down_ladders[new_level][0]; - state->player_position[1] = state->down_ladders[new_level][1]; - } - state->player_level = new_level; - static const int floor_ach[NUM_LEVELS] = { - -1, ACH_ENTER_DUNGEON, ACH_ENTER_GNOMISH_MINES, ACH_ENTER_SEWERS, - ACH_ENTER_VAULT, ACH_ENTER_TROLL_MINES, ACH_ENTER_FIRE_REALM, - ACH_ENTER_ICE_REALM, ACH_ENTER_GRAVEYARD, - }; - int achievement = floor_ach[new_level]; - if (achievement >= 0 && !state->achievements[achievement]) { - state->achievements[achievement] = 1; - state->player_xp += 1; - } - } - - static const int offsets[8][2] = { - {0, -1}, {0, 1}, {-1, 0}, {1, 0}, - {-1, -1}, {-1, 1}, {1, -1}, {1, 1}, - }; - level = state->player_level; - bool at_table = false; - bool at_furnace = false; - for (int i = 0; i < 8; i++) { - int row = state->player_position[0] + offsets[i][0]; - int col = state->player_position[1] + offsets[i][1]; - if (row < 0 || row >= MAP_SIZE || col < 0 || col >= MAP_SIZE) { - continue; - } - int nblock = state->map[level][row][col]; - at_table = at_table || nblock == BLOCK_CRAFTING_TABLE; - at_furnace = at_furnace || nblock == BLOCK_FURNACE; - } - Inventory* inv = &state->inventory; - - if (action == ACTION_MAKE_WOOD_PICKAXE && at_table && inv->wood >= 1 - && inv->pickaxe < 1) { - inv->wood -= 1; - inv->pickaxe = 1; - } else if (action == ACTION_MAKE_STONE_PICKAXE && at_table && inv->wood >= 1 - && inv->stone >= 1 && inv->pickaxe < 2) { - inv->wood -= 1; - inv->stone -= 1; - inv->pickaxe = 2; - } else if (action == ACTION_MAKE_IRON_PICKAXE && at_table && at_furnace - && inv->wood >= 1 && inv->stone >= 1 && inv->iron >= 1 - && inv->coal >= 1 && inv->pickaxe < 3) { - inv->wood -= 1; - inv->stone -= 1; - inv->iron -= 1; - inv->coal -= 1; - inv->pickaxe = 3; - } else if (action == ACTION_MAKE_DIAMOND_PICKAXE && at_table && inv->wood >= 1 - && inv->diamond >= 3 && inv->pickaxe < 4) { - inv->wood -= 1; - inv->diamond -= 3; - inv->pickaxe = 4; - } else if (action == ACTION_MAKE_WOOD_SWORD && at_table && inv->wood >= 1 - && inv->sword < 1) { - inv->wood -= 1; - inv->sword = 1; - } else if (action == ACTION_MAKE_STONE_SWORD && at_table && inv->wood >= 1 - && inv->stone >= 1 && inv->sword < 2) { - inv->wood -= 1; - inv->stone -= 1; - inv->sword = 2; - } else if (action == ACTION_MAKE_IRON_SWORD && at_table && at_furnace - && inv->wood >= 1 && inv->stone >= 1 && inv->iron >= 1 - && inv->coal >= 1 && inv->sword < 3) { - inv->wood -= 1; - inv->stone -= 1; - inv->iron -= 1; - inv->coal -= 1; - inv->sword = 3; - } else if (action == ACTION_MAKE_DIAMOND_SWORD && at_table && inv->wood >= 1 - && inv->diamond >= 2 && inv->sword < 4) { - inv->wood -= 1; - inv->diamond -= 2; - inv->sword = 4; - } else if (action == ACTION_MAKE_ARROW && at_table && inv->wood >= 1 - && inv->stone >= 1 && inv->arrows < 99) { - inv->wood -= 1; - inv->stone -= 1; - inv->arrows += 2; - } else if (action == ACTION_MAKE_TORCH && at_table && inv->wood >= 1 - && inv->coal >= 1 && inv->torches < 99) { - inv->wood -= 1; - inv->coal -= 1; - inv->torches += 4; - } - - int count = 0; - int idx = 0; - for (int i = 0; i < 4; i++) { - bool below = inv->armour[i] < 1; - if (count == 0 && below) { - idx = i; - } - count += below ? 1 : 0; - } - if (action == ACTION_MAKE_IRON_ARMOUR && at_table && at_furnace && count > 0 - && inv->iron >= 3 && inv->coal >= 3) { - inv->iron -= 3; - inv->coal -= 3; - inv->armour[idx] = 1; - state->achievements[ACH_MAKE_IRON_ARMOUR] = 1; - } - - count = 0; - idx = 0; - for (int i = 0; i < 4; i++) { - bool below = inv->armour[i] < 2; - if (count == 0 && below) { - idx = i; - } - count += below ? 1 : 0; - } - if (action == ACTION_MAKE_DIAMOND_ARMOUR && at_table && count > 0 && inv->diamond >= 3) { - inv->diamond -= 3; - inv->armour[idx] = 2; - state->achievements[ACH_MAKE_DIAMOND_ARMOUR] = 1; - } - CLEAN_ZONE_END(0); - - CLEAN_ZONE(1); - Rng interact_rng = rng_key(&step_rng); - int direction[2]; - - if (action == ACTION_DO) { - action_to_direction(state->player_direction, direction); - row = state->player_position[0] + direction[0]; - col = state->player_position[1] + direction[1]; - bool in_bounds = row >= 0 && row < MAP_SIZE && col >= 0 && col < MAP_SIZE; - level = state->player_level; - inv = &state->inventory; - - bool did_attack = false; - int attack_class; - int attack_slot; - if (find_mob_at(state, level, row, col, &attack_class, &attack_slot)) { - Mobs* attack_mobs = mobs_for_class(state, level, attack_class); - static const float base_damage[5] = {1, 2, 3, 5, 8}; - float base = base_damage[clampi(state->inventory.sword, 0, 4)]; - float physical = base * (1.0f + 0.25f * (state->player_strength - 1)); - float magic = base * 0.5f * (1.0f + 0.05f * (state->player_intelligence - 1)); - Damage vector = { - physical, - state->sword_enchantment == 1 ? magic : 0, - state->sword_enchantment == 2 ? magic : 0, - }; - did_attack = damage_mob_at( - state, level, row, col, - damage_to_mob(vector, attack_mobs->type_id[attack_slot], attack_class), - true, true - ); - } - Rng sapling_key = rng_key(&interact_rng); - Rng chest_key = rng_key(&interact_rng); - if (!did_attack && in_bounds) { - int block = state->map[level][row][col]; - - if (block == BLOCK_TREE || block == BLOCK_FIRE_TREE || block == BLOCK_ICE_SHRUB) { - set_block(state, level, row, col, - block == BLOCK_TREE ? BLOCK_GRASS - : (block == BLOCK_FIRE_TREE ? BLOCK_FIRE_GRASS : BLOCK_ICE_GRASS)); - inv->wood += 1; - } else if (block == BLOCK_STONE && inv->pickaxe >= 1) { - set_block(state, level, row, col, BLOCK_PATH); - inv->stone += 1; - } else if (block == BLOCK_COAL && inv->pickaxe >= 1) { - set_block(state, level, row, col, BLOCK_PATH); - inv->coal += 1; - } else if (block == BLOCK_IRON && inv->pickaxe >= 2) { - set_block(state, level, row, col, BLOCK_PATH); - inv->iron += 1; - } else if (block == BLOCK_DIAMOND && inv->pickaxe >= 3) { - set_block(state, level, row, col, BLOCK_PATH); - inv->diamond += 1; - } else if (block == BLOCK_SAPPHIRE && inv->pickaxe >= 4) { - set_block(state, level, row, col, BLOCK_PATH); - inv->sapphire += 1; - } else if (block == BLOCK_RUBY && inv->pickaxe >= 4) { - set_block(state, level, row, col, BLOCK_PATH); - inv->ruby += 1; - } else if (block == BLOCK_STALAGMITE && inv->pickaxe >= 1) { - set_block(state, level, row, col, BLOCK_PATH); - inv->stone += 1; - } else if (block == BLOCK_CRAFTING_TABLE || block == BLOCK_FURNACE) { - set_block(state, level, row, col, BLOCK_PATH); - } else if (block == BLOCK_WATER || block == BLOCK_FOUNTAIN) { - state->player_drink = clampi(state->player_drink + 1, 0, max_drink(state)); - state->player_thirst = 0.0f; - state->achievements[ACH_COLLECT_DRINK] = 1; - } else if (block == BLOCK_RIPE_PLANT) { - set_block(state, level, row, col, BLOCK_PLANT); - for (int i = 0; i < MAX_GROWING_PLANTS; i++) { - if (state->growing_plants_positions[i][0] == row - && state->growing_plants_positions[i][1] == col) { - state->growing_plants_age[i] = 0; - break; - } - } - state->player_food = clampi(state->player_food + 4, 0, max_food(state)); - state->player_hunger = 0.0f; - state->achievements[ACH_EAT_PLANT] = 1; - } else if (block == BLOCK_CHEST) { - set_block(state, level, row, col, BLOCK_PATH); - Rng chest_rng = chest_key; - - inv = &state->inventory; - rng_key(&chest_rng); - randint(rng_key(&chest_rng), 0u, 1, 6); - bool torch = rng_f32(rng_key(&chest_rng), 0) < 0.6f; - int torches = randint(rng_key(&chest_rng), 0u, 4, 8); - bool ore = rng_f32(rng_key(&chest_rng), 0) < 0.6f; - const float ore_weights[5] = {0.3f, 0.3f, 0.15f, 0.125f, 0.125f}; - int ore_id = choose_weighted_key(rng_key(&chest_rng), ore_weights, 5); - Rng amount_key = rng_key(&chest_rng); - int coal = randint(amount_key, 0u, 1, 4); - int iron = randint(amount_key, 0u, 1, 3); - int diamond = randint(amount_key, 0u, 1, 2); - int sapphire = randint(amount_key, 0u, 1, 2); - int ruby = randint(amount_key, 0u, 1, 2); - bool potion = rng_f32(rng_key(&chest_rng), 0) < 0.5f; - int potion_id = randint(rng_key(&chest_rng), 0u, 0, 6); - int potion_amount = randint(rng_key(&chest_rng), 0u, 1, 3); - bool arrows = rng_f32(rng_key(&chest_rng), 0) < 0.25f; - int arrow_amount = randint(rng_key(&chest_rng), 0u, 1, 5); - bool tool = rng_f32(rng_key(&chest_rng), 0) < 0.2f; - int tool_id = randint(rng_key(&chest_rng), 0u, 0, 2); - const float tool_weights[4] = {0.4f, 0.3f, 0.2f, 0.1f}; - int pickaxe = choose_weighted_key(rng_key(&chest_rng), tool_weights, 4) + 1; - int sword = choose_weighted_key(rng_key(&chest_rng), tool_weights, 4) + 1; - level = state->player_level; - - if (torch) { - inv->torches += torches; - } - if (ore && ore_id == 0) { - inv->coal += coal; - } - if (ore && ore_id == 1) { - inv->iron += iron; - } - if (ore && ore_id == 2) { - inv->diamond += diamond; - } - if (ore && ore_id == 3) { - inv->sapphire += sapphire; - } - if (ore && ore_id == 4) { - inv->ruby += ruby; - } - if (potion) { - inv->potions[potion_id] += potion_amount; - } - if (arrows) { - inv->arrows += arrow_amount; - } - if (tool && tool_id == 0 && pickaxe > inv->pickaxe) { - inv->pickaxe = pickaxe; - } - if (tool && tool_id == 1 && sword > inv->sword) { - inv->sword = sword; - } - if (state->player_level == 1 && !state->chests_opened[level]) { - inv->bow = 1; - } - if (!state->chests_opened[level] && (state->player_level == 3 || state->player_level == 4)) { - inv->books += 1; - } - state->achievements[ACH_OPEN_CHEST] = 1; - } else if (block == BLOCK_NECROMANCER && boss_vulnerable(state) && fighting_boss(state)) { - state->boss_progress += 1; - state->boss_timestep_to_spawn_this_round = BOSS_SPAWN_TURNS; - state->achievements[ACH_DAMAGE_NECROMANCER] = 1; - } - if (block == BLOCK_GRASS && rng_f32(sapling_key, 0) < 0.1f) { - inv->sapling += 1; - } - state->chests_opened[level] |= block == BLOCK_CHEST; - } - } - CLEAN_ZONE_END(1); - - CLEAN_ZONE(2); - action_to_direction(state->player_direction, direction); - row = state->player_position[0] + direction[0]; - col = state->player_position[1] + direction[1]; - if (row >= 0 && row < MAP_SIZE && col >= 0 && col < MAP_SIZE) { - level = state->player_level; - int block = state->map[level][row][col]; - bool occupied = is_solid_block(block) || state->item_map[level][row][col] != ITEM_NONE - || mob_at(state, level, row, col); - inv = &state->inventory; - - if (action == ACTION_PLACE_TABLE && !occupied && inv->wood >= 2) { - set_block(state, level, row, col, BLOCK_CRAFTING_TABLE); - inv->wood -= 2; - state->achievements[ACH_PLACE_TABLE] = 1; - } else if (action == ACTION_PLACE_FURNACE && !occupied && inv->stone >= 1) { - set_block(state, level, row, col, BLOCK_FURNACE); - inv->stone -= 1; - state->achievements[ACH_PLACE_FURNACE] = 1; - } else if (action == ACTION_PLACE_STONE - && (block == BLOCK_WATER || !occupied) && inv->stone >= 1) { - set_block(state, level, row, col, BLOCK_STONE); - inv->stone -= 1; - state->achievements[ACH_PLACE_STONE] = 1; - } else if (action == ACTION_PLACE_TORCH - && (block == BLOCK_GRASS || block == BLOCK_SAND || block == BLOCK_PATH - || block == BLOCK_FIRE_GRASS || block == BLOCK_ICE_GRASS) - && state->item_map[level][row][col] == ITEM_NONE - && inv->torches >= 1) { - state->item_map[level][row][col] = ITEM_TORCH; - for (int dr = -4; dr <= 4; dr++) { - int light_row = row + dr; - if (light_row < 0 || light_row >= MAP_SIZE) { - continue; - } - for (int dc = -4; dc <= 4; dc++) { - int light_col = col + dc; - if (light_col < 0 || light_col >= MAP_SIZE) { - continue; - } - float torch = 1.0f - sqrtf(dr * dr + dc * dc) / 5.0f; - if (torch < 0.0f) { - torch = 0.0f; - } - float light = state->light_map[level][light_row][light_col] / 255.0f + torch; - if (light > 1.0f) { - light = 1.0f; - } - state->light_map[level][light_row][light_col] = (unsigned char)(light * 255.0f); - } - } - inv->torches -= 1; - state->achievements[ACH_PLACE_TORCH] = 1; - } else if (action == ACTION_PLACE_PLANT && block == BLOCK_GRASS - && state->item_map[level][row][col] == ITEM_NONE - && inv->sapling >= 1) { - set_block(state, level, row, col, BLOCK_PLANT); - inv->sapling -= 1; - for (int i = 0; i < MAX_GROWING_PLANTS; i++) { - if (!state->growing_plants_mask[i]) { - state->growing_plants_positions[i][0] = row; - state->growing_plants_positions[i][1] = col; - state->growing_plants_age[i] = 0; - state->growing_plants_mask[i] = 1; - break; - } - } - state->achievements[ACH_PLACE_PLANT] = 1; - } - } - - action_to_direction(state->player_direction, direction); - if (direction[0] == 0 && direction[1] == 0) { - direction[0] = 1; - } - - if (action == ACTION_SHOOT_ARROW && state->inventory.bow > 0 - && state->inventory.arrows > 0) { - bool fired = spawn_projectile(state, true, PROJECTILE_ARROW2, - state->player_position[0], state->player_position[1], - direction[0], direction[1]); - if (fired) { - state->inventory.arrows -= 1; - state->achievements[ACH_FIRE_BOW] = 1; - } - } else if (action == ACTION_CAST_FIREBALL && state->learned_spells[0] - && state->player_mana >= 2) { - bool cast = spawn_projectile(state, true, PROJECTILE_FIREBALL, - state->player_position[0], state->player_position[1], - direction[0], direction[1]); - if (cast) { - state->player_mana -= 2; - state->achievements[ACH_CAST_FIREBALL] = 1; - } - } else if (action == ACTION_CAST_ICEBALL && state->learned_spells[1] - && state->player_mana >= 2) { - bool cast = spawn_projectile(state, true, PROJECTILE_ICEBALL, - state->player_position[0], state->player_position[1], - direction[0], direction[1]); - if (cast) { - state->player_mana -= 2; - state->achievements[ACH_CAST_ICEBALL] = 1; - } - } - - int potion = action - ACTION_DRINK_POTION_RED; - if (potion >= 0 && potion < NUM_POTIONS && state->inventory.potions[potion] > 0) { - int effect = state->potion_mapping[potion]; - state->inventory.potions[potion] -= 1; - if (effect == 0) { - state->player_health += 8.0f; - } else if (effect == 1) { - state->player_health -= 3.0f; - } else if (effect == 2) { - state->player_mana += 8; - } else if (effect == 3) { - state->player_mana -= 3; - } else if (effect == 4) { - state->player_energy += 8; - } else { - state->player_energy -= 3; - } - state->achievements[ACH_DRINK_POTION] = 1; - } - CLEAN_ZONE_END(2); - - CLEAN_ZONE(3); - Rng book_rng = rng_key(&step_rng); - - bool reading = action == ACTION_READ_BOOK && state->inventory.books > 0; - Rng unused; - Rng choice_key; - rng_split(book_rng, &unused, &choice_key); - float p0 = state->learned_spells[0] ? 0.0f : 1.0f; - float p1 = state->learned_spells[1] ? 0.0f : 1.0f; - int spell = 0; - if (p0 + p1 != 0.0f) { - float r = 1.0f - rng_f32(choice_key, 0); - spell = r <= (p0 / (p0 + p1)) ? 0 : 1; - } - if (reading) { - state->inventory.books -= 1; - state->learned_spells[spell] = 1; - state->achievements[spell == 0 ? ACH_LEARN_FIREBALL : ACH_LEARN_ICEBALL] = 1; - } - Rng enchant_rng = rng_key(&step_rng); - - action_to_direction(state->player_direction, direction); - level = state->player_level; - int erow = state->player_position[0] + direction[0]; - int ecol = state->player_position[1] + direction[1]; - int eblock = 0; - if (erow >= 0 && erow < MAP_SIZE && ecol >= 0 && ecol < MAP_SIZE) { - eblock = state->map[level][erow][ecol]; - } - int enchant = eblock == BLOCK_ENCHANTMENT_TABLE_FIRE ? 1 : - (eblock == BLOCK_ENCHANTMENT_TABLE_ICE ? 2 : 0); - int gems = enchant == 1 ? state->inventory.ruby : state->inventory.sapphire; - bool could = state->player_mana >= 9 && enchant != 0 && gems >= 1; - bool enchanting_sword = could && action == ACTION_ENCHANT_SWORD && state->inventory.sword > 0; - bool enchanting_bow = could && action == ACTION_ENCHANT_BOW && state->inventory.bow > 0; - int armour_count = 0; - for (int i = 0; i < 4; i++) { - armour_count += state->inventory.armour[i]; - } - bool enchanting_armour = could && action == ACTION_ENCHANT_ARMOUR && armour_count > 0; - Rng armour_key = rng_key(&enchant_rng); - int unenchanted = 0; - for (int i = 0; i < 4; i++) { - unenchanted += state->armour_enchantments[i] == 0; - } - float candidates[4]; - for (int i = 0; i < 4; i++) { - bool opposite = state->armour_enchantments[i] != 0 && state->armour_enchantments[i] != enchant; - candidates[i] = (state->armour_enchantments[i] == 0 || (unenchanted == 0 && opposite)) ? 1.0f : 0.0f; - } - int armour_target = choose_weighted_key(armour_key, candidates, 4); - if (enchanting_sword) { - state->sword_enchantment = enchant; - state->achievements[ACH_ENCHANT_SWORD] = 1; - } - if (enchanting_bow) { - state->bow_enchantment = enchant; - } - if (enchanting_armour) { - state->armour_enchantments[armour_target] = enchant; - state->achievements[ACH_ENCHANT_ARMOUR] = 1; - } - bool enchanting = enchanting_sword || enchanting_bow || enchanting_armour; - if (enchanting) { - if (enchant == 1) { - state->inventory.ruby -= 1; - } else { - state->inventory.sapphire -= 1; - } - state->player_mana -= 9; - } - state->achievements[ACH_DEFEAT_NECROMANCER] |= state->boss_progress >= NUM_LEVELS - 1; - if (fighting_boss(state)) { - state->boss_timestep_to_spawn_this_round -= 1; - } - - if (state->player_xp >= 1) { - bool leveled = false; - if (action == ACTION_LEVEL_UP_DEXTERITY && state->player_dexterity < MAX_ATTRIBUTE) { - state->player_dexterity += 1; - leveled = true; - } else if (action == ACTION_LEVEL_UP_STRENGTH && state->player_strength < MAX_ATTRIBUTE) { - state->player_strength += 1; - leveled = true; - } else if (action == ACTION_LEVEL_UP_INTELLIGENCE && state->player_intelligence < MAX_ATTRIBUTE) { - state->player_intelligence += 1; - leveled = true; - } - if (leveled) { - state->player_xp -= 1; - } - } - - action_to_direction(action, direction); - int proposed_row = state->player_position[0] + direction[0]; - int proposed_col = state->player_position[1] + direction[1]; - level = state->player_level; - bool valid = proposed_row >= 0 && proposed_row < MAP_SIZE - && proposed_col >= 0 && proposed_col < MAP_SIZE; - if (valid) { - int pblock = state->map[level][proposed_row][proposed_col]; - valid = !is_solid_block(pblock) && pblock != BLOCK_WATER && pblock != BLOCK_LAVA - && !mob_at(state, level, proposed_row, proposed_col); - } - if (valid) { - state->player_position[0] = proposed_row; - state->player_position[1] = proposed_col; - } - if (direction[0] != 0 || direction[1] != 0) { - state->player_direction = action; - } - CLEAN_ZONE_END(3); - - CLEAN_ZONE(4); - Rng mobs_rng = rng_key(&step_rng); - - level = state->player_level; - rng_key(&mobs_rng); - move_melee_slot(state, level, 0, &mobs_rng); - move_melee_slot(state, level, 1, &mobs_rng); - move_melee_slot(state, level, 2, &mobs_rng); - rng_key(&mobs_rng); - move_passive_slot(state, level, 0, &mobs_rng); - move_passive_slot(state, level, 1, &mobs_rng); - move_passive_slot(state, level, 2, &mobs_rng); - rng_key(&mobs_rng); - move_ranged_slot(state, level, 0, &mobs_rng); - move_ranged_slot(state, level, 1, &mobs_rng); - rng_key(&mobs_rng); - update_projectile_set(state, false); - rng_key(&mobs_rng); - update_projectile_set(state, true); - CLEAN_ZONE_END(4); - - CLEAN_ZONE(5); - Rng spawn_rng = rng_key(&step_rng); - - level = state->player_level; - bool boss = fighting_boss(state); - int coeff = 1 + (state->monsters_killed[level] < MONSTERS_KILLED_TO_CLEAR_LEVEL ? 2 : 0); - if (boss) { - coeff *= (state->boss_timestep_to_spawn_this_round >= 1) ? 1000 : 0; - } - - static const float chances[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}, - }; - - int passive_count; - int passive_slot; - count_and_empty(&state->passive_mobs[level], MAX_PASSIVE_MOBS, &passive_count, &passive_slot); - Rng passive_prob = rng_key(&spawn_rng); - Rng passive_pos = rng_key(&spawn_rng); - int passive_type = floor_mob_type(level, MOB_PASSIVE); - state->passive_mobs[level].type_id[passive_slot] = passive_type; - - int melee_count; - int melee_slot; - count_and_empty(&state->melee_mobs[level], MAX_MELEE_MOBS, &melee_count, &melee_slot); - int melee_type = floor_mob_type(boss ? state->boss_progress : level, MOB_MELEE); - Rng melee_prob = rng_key(&spawn_rng); - float night = 1.0f - state->light_level; - float melee_chance = chances[level][1] + chances[level][3] * night * night; - Rng melee_pos = rng_key(&spawn_rng); - state->melee_mobs[level].type_id[melee_slot] = melee_type; - - int ranged_count; - int ranged_slot; - count_and_empty(&state->ranged_mobs[level], MAX_RANGED_MOBS, &ranged_count, &ranged_slot); - int ranged_type = floor_mob_type(boss ? state->boss_progress : level, MOB_RANGED); - Rng ranged_prob = rng_key(&spawn_rng); - Rng ranged_pos = rng_key(&spawn_rng); - state->ranged_mobs[level].type_id[ranged_slot] = ranged_type; - - bool try_passive = !boss && passive_count < MAX_PASSIVE_MOBS - && rng_f32(passive_prob, 0) < chances[level][0]; - bool try_melee = melee_count < MAX_MELEE_MOBS - && rng_f32(melee_prob, 0) < melee_chance * coeff; - bool try_ranged = ranged_count < MAX_RANGED_MOBS - && rng_f32(ranged_prob, 0) < chances[level][2] * coeff; - if (try_passive || try_melee || try_ranged) { - int min_hostile = boss ? -1 : 81; - int max_hostile = boss ? 37 : MOB_DESPAWN_DISTANCE * MOB_DESPAWN_DISTANCE; - int spawn_rows[729]; - int spawn_cols[729]; - int row; - int col; - if (try_passive) { - int n = collect_spawn_cells( - state, level, 9, MOB_DESPAWN_DISTANCE * MOB_DESPAWN_DISTANCE, - false, false, spawn_rows, spawn_cols); - if (pick_spawn_cell(spawn_rows, spawn_cols, n, passive_pos, &row, &col)) { - spawn_into_slot(state, level, &state->passive_mobs[level], - passive_slot, MOB_PASSIVE, passive_type, row, col); - } - } - if (try_melee) { - int n = collect_spawn_cells( - state, level, min_hostile, max_hostile, - boss, false, spawn_rows, spawn_cols); - if (pick_spawn_cell(spawn_rows, spawn_cols, n, melee_pos, &row, &col)) { - spawn_into_slot(state, level, &state->melee_mobs[level], - melee_slot, MOB_MELEE, melee_type, row, col); - } - } - if (try_ranged) { - int n = collect_spawn_cells( - state, level, min_hostile, max_hostile, - boss, ranged_type == 5, spawn_rows, spawn_cols); - if (pick_spawn_cell(spawn_rows, spawn_cols, n, ranged_pos, &row, &col)) { - spawn_into_slot(state, level, &state->ranged_mobs[level], - ranged_slot, MOB_RANGED, ranged_type, row, col); - } - } - } - CLEAN_ZONE_END(5); - - CLEAN_ZONE(6); - for (int plant = 0; plant < MAX_GROWING_PLANTS; plant++) { - if (!state->growing_plants_mask[plant]) { - continue; - } - - state->growing_plants_age[plant] += 1; - if (state->growing_plants_age[plant] < 600) { - continue; - } - - int row = state->growing_plants_positions[plant][0]; - int col = state->growing_plants_positions[plant][1]; - if (state->growing_plants_age[plant] >= 600) { - set_block(state, 0, row, col, BLOCK_RIPE_PLANT); - } - } - - bool start_sleep = action == ACTION_SLEEP && state->player_energy < max_energy(state); - state->is_sleeping = state->is_sleeping || start_sleep; - - bool wake_from_sleep = state->is_sleeping && state->player_energy >= max_energy(state); - state->is_sleeping = state->is_sleeping && !wake_from_sleep; - state->achievements[ACH_WAKE_UP] = state->achievements[ACH_WAKE_UP] || wake_from_sleep; - - bool start_rest = action == ACTION_REST && state->player_health < max_health(state); - state->is_resting = state->is_resting || start_rest; - - bool wake_from_rest = state->is_resting && ( - state->player_health >= max_health(state) - || state->player_food <= 0 - || state->player_drink <= 0 - ); - state->is_resting = state->is_resting && !wake_from_rest; - - bool not_boss = !fighting_boss(state); - float decay = 1.0f - 0.125f * (state->player_dexterity - 1); - - state->player_hunger += (state->is_sleeping ? 0.5f : 1.0f) * decay; - if (state->player_hunger > 25.0f) { - state->player_hunger = 0.0f; - state->player_food = clampi(state->player_food - (not_boss ? 1 : 0), 0, max_food(state)); - } - - state->player_thirst += (state->is_sleeping ? 0.5f : 1.0f) * decay; - if (state->player_thirst > 20.0f) { - state->player_thirst = 0.0f; - state->player_drink = clampi(state->player_drink - (not_boss ? 1 : 0), 0, max_drink(state)); - } - - if (state->is_sleeping) { - state->player_fatigue = state->player_fatigue - 1.0f; - if (state->player_fatigue > 0.0f) { - state->player_fatigue = 0.0f; - } - } else { - state->player_fatigue += decay; - } - if (state->player_fatigue > 30.0f) { - state->player_fatigue = 0.0f; - state->player_energy = clampi(state->player_energy - (not_boss ? 1 : 0), 0, max_energy(state)); - } else if (state->player_fatigue < -10.0f) { - state->player_fatigue = 0.0f; - state->player_energy = clampi(state->player_energy + 1, 0, max_energy(state)); - } - - bool all_necessities = state->player_food > 0 - && state->player_drink > 0 - && (state->player_energy > 0 || state->is_sleeping); - state->player_recover += all_necessities - ? (state->is_sleeping ? 2.0f : 1.0f) - : (state->is_sleeping ? -0.5f : -1.0f) * (not_boss ? 1.0f : 0.0f); - - if (state->player_recover > 25.0f) { - state->player_recover = 0.0f; - state->player_health = clampf(state->player_health + 1.0f, 0.0f, max_health(state)); - } else if (state->player_recover < -15.0f) { - state->player_recover = 0.0f; - state->player_health -= 1.0f; - } - - float mana_gain = state->is_sleeping ? 2.0f : 1.0f; - float mana_coeff = 1.0f + 0.25f * (state->player_intelligence - 1); - state->player_recover_mana = (state->player_recover_mana + mana_gain) * mana_coeff; - if (state->player_recover_mana > 30.0f) { - state->player_recover_mana = 0.0f; - state->player_mana = clampi(state->player_mana + 1, 0, max_mana(state)); - } - - state->inventory.wood = clampi(state->inventory.wood, 0, 99); - state->inventory.stone = clampi(state->inventory.stone, 0, 99); - state->inventory.coal = clampi(state->inventory.coal, 0, 99); - state->inventory.iron = clampi(state->inventory.iron, 0, 99); - state->inventory.diamond = clampi(state->inventory.diamond, 0, 99); - state->inventory.sapling = clampi(state->inventory.sapling, 0, 99); - state->inventory.pickaxe = clampi(state->inventory.pickaxe, 0, 99); - state->inventory.sword = clampi(state->inventory.sword, 0, 99); - state->inventory.bow = clampi(state->inventory.bow, 0, 99); - state->inventory.arrows = clampi(state->inventory.arrows, 0, 99); - state->inventory.torches = clampi(state->inventory.torches, 0, 99); - state->inventory.ruby = clampi(state->inventory.ruby, 0, 99); - state->inventory.sapphire = clampi(state->inventory.sapphire, 0, 99); - state->inventory.books = clampi(state->inventory.books, 0, 99); - for (int i = 0; i < 4; i++) { - state->inventory.armour[i] = clampi(state->inventory.armour[i], 0, 99); - } - for (int i = 0; i < NUM_POTIONS; i++) { - state->inventory.potions[i] = clampi(state->inventory.potions[i], 0, 99); - } - - state->player_health = clampf(state->player_health, 0.0f, max_health(state)); - state->player_food = clampi(state->player_food, 0, max_food(state)); - state->player_drink = clampi(state->player_drink, 0, max_drink(state)); - state->player_energy = clampi(state->player_energy, 0, max_energy(state)); - state->player_mana = clampi(state->player_mana, 0, max_mana(state)); - - state->achievements[ACH_COLLECT_WOOD] |= state->inventory.wood > 0; - state->achievements[ACH_COLLECT_STONE] |= state->inventory.stone > 0; - state->achievements[ACH_COLLECT_COAL] |= state->inventory.coal > 0; - state->achievements[ACH_COLLECT_IRON] |= state->inventory.iron > 0; - state->achievements[ACH_COLLECT_DIAMOND] |= state->inventory.diamond > 0; - state->achievements[ACH_COLLECT_SAPPHIRE] |= state->inventory.sapphire > 0; - state->achievements[ACH_COLLECT_RUBY] |= state->inventory.ruby > 0; - state->achievements[ACH_COLLECT_SAPLING] |= state->inventory.sapling > 0; - state->achievements[ACH_FIND_BOW] |= state->inventory.bow > 0; - state->achievements[ACH_MAKE_ARROW] |= state->inventory.arrows > 0; - state->achievements[ACH_MAKE_TORCH] |= state->inventory.torches > 0; - state->achievements[ACH_MAKE_WOOD_PICKAXE] |= state->inventory.pickaxe >= 1; - state->achievements[ACH_MAKE_STONE_PICKAXE] |= state->inventory.pickaxe >= 2; - state->achievements[ACH_MAKE_IRON_PICKAXE] |= state->inventory.pickaxe >= 3; - state->achievements[ACH_MAKE_DIAMOND_PICKAXE] |= state->inventory.pickaxe >= 4; - state->achievements[ACH_MAKE_WOOD_SWORD] |= state->inventory.sword >= 1; - state->achievements[ACH_MAKE_STONE_SWORD] |= state->inventory.sword >= 2; - state->achievements[ACH_MAKE_IRON_SWORD] |= state->inventory.sword >= 3; - state->achievements[ACH_MAKE_DIAMOND_SWORD] |= state->inventory.sword >= 4; - update_log_state(env); - CLEAN_ZONE_END(6); - - store_rng(state, rng_key(&step_rng)); - state->timestep += 1; - float day_progress = fmodf(state->timestep / (float)DAY_LENGTH, 1.0f) + 0.3f; - state->light_level = 1.0f - powf(fabsf(cosf(3.14159265358979323846f * day_progress)), 3.0f); - - done = state->player_health <= 0.0f || state->timestep >= DEFAULT_MAX_TIMESTEPS; - } while (!done && (state->is_sleeping || state->is_resting)); - - CLEAN_ZONE(7); - float reward = 0.0f; - for (int i = 0; i < NUM_ACHIEVEMENTS; i++) { - int delta = state->achievements[i] - initial_achievements[i]; - reward += delta * ACHIEVEMENT_REWARD_MAP[i]; - } - //reward += (state->player_health - initial_health) * 0.1f; - reward += (equipped_armour(state) - initial_armour); - if (state->player_health <= 0.0f) { - reward = -1.0f; // dead - } - - memcpy(env->achievements, env->state.achievements, sizeof(env->achievements)); - - env->agents[0].rewards[0] = reward; - env->agents[0].terminals[0] = done ? 1.0f : 0.0f; - env->episode_return_accum += reward; - env->episode_length_accum += 1; - - if (done) { - int unlocked = 0; - float achievement_return = 0.0f; - for (int i = 0; i < NUM_ACHIEVEMENTS; i++) { - if (env->achievements[i]) { - unlocked++; - achievement_return += ACHIEVEMENT_REWARD_MAP[i]; - env->log.achievements[i] += 1.0f; - } - } - env->log.achievement_rate += unlocked / (float)NUM_ACHIEVEMENTS; - env->log.perf += achievement_return / max_achievement_return(); - env->log.score += env->episode_return_accum; - env->log.episode_return += env->episode_return_accum; - env->log.episode_length += env->episode_length_accum; - for (int floor = 0; floor <= env->max_floor_accum; floor++) { - env->log.floors[floor] += 1.0f; - } - env->log.n += 1.0f; - - env->episode_return_accum = 0.0f; - env->episode_length_accum = 0; - env->max_floor_accum = 0; - memset(env->achievements, 0, sizeof(env->achievements)); - if (env->reset_pool_size > 0) { - uint32_t idx = (uint32_t)reset_key % (uint32_t)env->reset_pool_size; - memcpy(&env->state, &env->reset_pool[idx], sizeof(State)); - } else { - Rng done_unused; - Rng world_key; - rng_split(reset_key, &done_unused, &world_key); - generate_world_from_key(&env->state, world_key); - } - } - CLEAN_ZONE_END(7); - - CLEAN_ZONE(8); - compute_observations(env); - CLEAN_ZONE_END(8); -} - -void puf_init(Env* env, Dict* kwargs) { - env->num_agents = 1; - env->agents[0].policy = 0; - env->agents[0].action_mask = NULL; // trainer wires mask after puf_init - env->use_action_mask = 0; - uint64_t seed_offset = 0; - for (int i = 0; i < kwargs->size; i++) { - if (strcmp(kwargs->items[i].key, "seed_offset") == 0) { - seed_offset = (uint64_t)kwargs->items[i].value; - } else if (strcmp(kwargs->items[i].key, "action_mask") == 0) { - env->use_action_mask = kwargs->items[i].value != 0.0; - } - } - env->seed = seed_offset + env->rng; - memset(&env->state, 0, sizeof(State)); - env->episode_return_accum = 0.0f; - env->episode_length_accum = 0; - env->max_floor_accum = 0; - memset(env->achievements, 0, sizeof(env->achievements)); - memset(&env->log, 0, sizeof(Log)); - env->client = NULL; -} - -Env* my_vec_init(int* num_envs_out, int* env_starts, int* env_counts, - Dict* vec_kwargs, Dict* env_kwargs) { - int total_agents = dict_get(vec_kwargs, "total_agents"); - int num_buffers = dict_get(vec_kwargs, "num_buffers"); - int agents_per_buf = total_agents / num_buffers; - int num_envs = total_agents; - int reset_pool_size = 0; - DictItem* item = dict_find(env_kwargs, "reset_pool_size"); - if (item) { - reset_pool_size = item->value; - } - State* pool = NULL; - if (reset_pool_size > 0) { - pool = (State*)calloc(reset_pool_size, sizeof(State)); - for (int i = 0; i < reset_pool_size; i++) { - Rng init_key = rng_seed(i); - Rng discard; - Rng reset_key; - rng_split(init_key, &discard, &reset_key); - Rng unused; - Rng world_key; - rng_split(reset_key, &unused, &world_key); - generate_world_from_key(&pool[i], world_key); - } - } - - Env* envs = (Env*)calloc(num_envs, sizeof(Env)); - int buf = 0; - int buf_agents = 0; - env_starts[0] = 0; - env_counts[0] = 0; - for (int i = 0; i < num_envs; i++) { - Env* env = &envs[i]; - env->rng = i; - env->reset_pool = pool; - env->reset_pool_size = reset_pool_size; - puf_init(env, env_kwargs); - buf_agents += env->num_agents; - env_counts[buf]++; - if (buf_agents >= agents_per_buf && buf < num_buffers - 1) { - buf++; - env_starts[buf] = i + 1; - env_counts[buf] = 0; - buf_agents = 0; - } - } - *num_envs_out = num_envs; - return envs; -} - -void my_vec_close(Env* envs) { - free(envs[0].reset_pool); -} - -void puf_log(Log* log, Dict* out) { - dict_set(out, "perf", log->perf); - dict_set(out, "achievement_rate", log->achievement_rate); - 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, "floor_0_overworld", log->floors[0]); - dict_set(out, "floor_1_dungeon", log->floors[1]); - dict_set(out, "floor_2_gnomish_mines", log->floors[2]); - dict_set(out, "floor_3_sewers", log->floors[3]); - dict_set(out, "floor_4_vault", log->floors[4]); - dict_set(out, "floor_5_troll_mines", log->floors[5]); - dict_set(out, "floor_6_fire_realm", log->floors[6]); - dict_set(out, "floor_7_ice_realm", log->floors[7]); - dict_set(out, "floor_8_graveyard", log->floors[8]); - dict_set(out, "n", log->n); -} - -static Texture2D textures; -static int textures_loaded; - -static void draw_tile(int tex_id, int x, int y, int px) { - Rectangle src = { - (float)((tex_id % TEX_SHEET_COLS) * TEX_TILE_PX), - (float)((tex_id / TEX_SHEET_COLS) * TEX_TILE_PX), - (float)TEX_TILE_PX, - (float)TEX_TILE_PX, - }; - Rectangle dst = {(float)x, (float)y, (float)px, (float)px}; - DrawTexturePro(textures, src, dst, (Vector2){0, 0}, 0.0f, WHITE); -} - -static int projectile_tex(int ptype, int dr, int dc) { - if (ptype == PROJECTILE_DAGGER) { - return TEX_PROJ_DAGGER; - } - if (ptype == PROJECTILE_FIREBALL || ptype == PROJECTILE_FIREBALL2) { - return TEX_PROJ_FIREBALL; - } - if (ptype == PROJECTILE_ICEBALL || ptype == PROJECTILE_ICEBALL2) { - return TEX_PROJ_ICEBALL; - } - if (ptype == PROJECTILE_SLIMEBALL) { - return TEX_PROJ_SLIMEBALL; - } - if (dr < 0) { - return TEX_ARROW_UP; - } - if (dr > 0) { - return TEX_ARROW_DOWN; - } - if (dc < 0) { - return TEX_ARROW_LEFT; - } - return TEX_ARROW_RIGHT; -} - -static int mob_tex_base[] = {TEX_PASSIVE, TEX_MELEE, TEX_RANGED}; - -static void draw_agent_obs(Craftax* env, int panel_x, int panel_y, - int panel_w, int panel_h) { - State* state = &env->state; - int pad = 10; - int px = (panel_w - pad * 2) / OBS_COLS; - int grid_x = panel_x + (panel_w - OBS_COLS * px) / 2; - int grid_y = panel_y + 36 + pad; - int level = clampi(state->player_level, 0, NUM_LEVELS - 1); - int pr = state->player_position[0]; - int pc = state->player_position[1]; - int rr = OBS_ROWS / 2; - int rc = OBS_COLS / 2; - - DrawRectangle(panel_x, panel_y, panel_w, panel_h, (Color){8, 10, 14, 255}); - DrawRectangleLines(panel_x, panel_y, panel_w, panel_h, (Color){0, 210, 220, 255}); - DrawText("agent obs", panel_x + pad, panel_y + 8, 18, WHITE); - DrawText("9x11 light>12", panel_x + pad, panel_y + 28, 12, - (Color){140, 160, 166, 255}); - - for (int vr = 0; vr < OBS_ROWS; vr++) { - for (int vc = 0; vc < OBS_COLS; vc++) { - int wr = pr + (vr - rr); - int wc = pc + (vc - rc); - int dst_x = grid_x + vc * px; - int dst_y = grid_y + vr * px; - int lit = (unsigned)wr < MAP_SIZE && (unsigned)wc < MAP_SIZE - && state->light_map[level][wr][wc] > 12; - if (!lit) { - DrawRectangle(dst_x, dst_y, px, px, BLACK); - continue; - } - int block = state->map[level][wr][wc]; - if (block < 0 || block >= NUM_BLOCK_TYPES) { - block = BLOCK_INVALID; - } - draw_tile(block, dst_x, dst_y, px); - int item = state->item_map[level][wr][wc]; - if (item > ITEM_NONE) { - draw_tile(TEX_ITEM_BASE + item, dst_x, dst_y, px); - } - int mob_class; - int slot; - if (find_mob_at(state, level, wr, wc, &mob_class, &slot)) { - int type_id = mobs_for_class(state, level, mob_class)->type_id[slot]; - draw_tile(mob_tex_base[mob_class] + type_id, dst_x, dst_y, px); - } - } - } - - for (int from_player = 0; from_player < 2; from_player++) { - Mobs* projectiles = from_player - ? &state->player_projectiles[level] : &state->mob_projectiles[level]; - int (*directions)[MAX_PLAYER_PROJECTILES][2] = from_player - ? state->player_projectile_directions : state->mob_projectile_directions; - for (int i = 0; i < MAX_PLAYER_PROJECTILES; i++) { - if (!projectiles->mask[i]) { - continue; - } - int row = projectiles->position[i][0]; - int col = projectiles->position[i][1]; - int vr = row - pr + rr; - int vc = col - pc + rc; - int lit = (unsigned)row < MAP_SIZE && (unsigned)col < MAP_SIZE - && state->light_map[level][row][col] > 12; - if (vr < 0 || vr >= OBS_ROWS || vc < 0 || vc >= OBS_COLS || !lit) { - continue; - } - int dr = directions[level][i][0]; - int dc = directions[level][i][1]; - draw_tile(projectile_tex(projectiles->type_id[i], dr, dc), - grid_x + vc * px, grid_y + vr * px, px); - } - } - - int player_tex = TEX_PLAYER_DOWN; - if (state->is_sleeping) { - player_tex = TEX_PLAYER_SLEEP; - } else if (state->player_direction == ACTION_LEFT) { - player_tex = TEX_PLAYER_LEFT; - } else if (state->player_direction == ACTION_RIGHT) { - player_tex = TEX_PLAYER_RIGHT; - } else if (state->player_direction == ACTION_UP) { - player_tex = TEX_PLAYER_UP; - } - draw_tile(player_tex, grid_x + rc * px, grid_y + rr * px, px); - - float v = env->predicted_value; - float t = clampf(v, -1.0f, 1.0f); - unsigned char fade = (unsigned char)(255.0f * (1.0f - fabsf(t))); - Color vc = WHITE; - if (t < 0.0f) { - vc = (Color){255, fade, fade, 255}; - } else if (t > 0.0f) { - vc = (Color){fade, 255, fade, 255}; - } - const char* vlabel = TextFormat("V(o, h) = %.2f", v); - int tw = MeasureText(vlabel, 18); - DrawText(vlabel, panel_x + (panel_w - tw) / 2, - grid_y + OBS_ROWS * px + 10, 18, vc); -} - -static void draw_icon_count(int tex_id, int value, int x, int y) { - draw_tile(tex_id, x, y, 20); - DrawText(TextFormat("%d", value), x + 23, y + 4, 14, RAYWHITE); -} - -static void draw_inv_slot(int tex_id, int overlay, int x, int y) { - DrawRectangle(x, y, 24, 24, (Color){32, 32, 32, 255}); - DrawRectangleLines(x, y, 24, 24, (Color){80, 80, 80, 255}); - if (tex_id >= 0) { - draw_tile(tex_id, x, y, 24); - } - if (overlay >= 0) { - draw_tile(overlay, x, y, 24); - } -} - -static const char* craftax_clean_action_names[ATN_DIM] = { - "NOOP", "LEFT", "RIGHT", "UP", "DOWN", "DO", "SLEEP", - "PLACE_STONE", "PLACE_TABLE", "PLACE_FURNACE", "PLACE_PLANT", - "MAKE_WOOD_PICKAXE", "MAKE_STONE_PICKAXE", "MAKE_IRON_PICKAXE", - "MAKE_WOOD_SWORD", "MAKE_STONE_SWORD", "MAKE_IRON_SWORD", - "REST", "DESCEND", "ASCEND", - "MAKE_DIAMOND_PICKAXE", "MAKE_DIAMOND_SWORD", - "MAKE_IRON_ARMOUR", "MAKE_DIAMOND_ARMOUR", - "SHOOT_ARROW", "MAKE_ARROW", "CAST_FIREBALL", "CAST_ICEBALL", "PLACE_TORCH", - "DRINK_POTION_RED", "DRINK_POTION_GREEN", "DRINK_POTION_BLUE", - "DRINK_POTION_PINK", "DRINK_POTION_CYAN", "DRINK_POTION_YELLOW", - "READ_BOOK", "ENCHANT_SWORD", "ENCHANT_ARMOUR", "MAKE_TORCH", - "LEVEL_UP_DEXTERITY", "LEVEL_UP_STRENGTH", "LEVEL_UP_INTELLIGENCE", - "ENCHANT_BOW", -}; - -static const char* craftax_clean_action_keys[ATN_DIM] = { - "Q", "A", "D", "W", "S", "Space", "Tab", - "R", "T", "F", "P", - "1", "2", "3", - "5", "6", "7", - "E", ".", ",", - "4", "8", - "Y", "U", - "I", "O", "G", "H", "J", - "Z", "X", "C", "V", "B", "N", - "M", "K", "L", "[", - "]", "-", "=", - ";", -}; - -static const char* craftax_clean_ach_names[NUM_ACHIEVEMENTS] = { - "Collect Wood", "Place Table", "Eat Cow", "Collect Sapling", "Collect Drink", - "Make Wood Pickaxe", "Make Wood Sword", "Place Plant", "Defeat Zombie", - "Collect Stone", "Place Stone", "Eat Plant", "Defeat Skeleton", - "Make Stone Pickaxe", "Make Stone Sword", "Wake Up", "Place Furnace", - "Collect Coal", "Collect Iron", "Collect Diamond", "Make Iron Pickaxe", - "Make Iron Sword", "Make Arrow", "Make Torch", "Place Torch", - "Make Diamond Sword", "Make Iron Armour", "Make Diamond Armour", - "Enter Gnomish Mines", "Enter Dungeon", "Enter Sewers", "Enter Vault", - "Enter Troll Mines", "Enter Fire Realm", "Enter Ice Realm", "Enter Graveyard", - "Defeat Gnome Warrior", "Defeat Gnome Archer", "Defeat Orc Soldier", - "Defeat Orc Mage", "Defeat Lizard", "Defeat Kobold", "Defeat Troll", - "Defeat Deep Thing", "Defeat Pigman", "Defeat Fire Elemental", - "Defeat Frost Troll", "Defeat Ice Elemental", "Damage Necromancer", - "Defeat Necromancer", "Eat Bat", "Eat Snail", "Find Bow", "Fire Bow", - "Collect Sapphire", "Learn Fireball", "Cast Fireball", "Learn Iceball", - "Cast Iceball", "Collect Ruby", "Make Diamond Pickaxe", "Open Chest", - "Drink Potion", "Enchant Sword", "Enchant Armour", "Defeat Knight", - "Defeat Archer", -}; - -void puf_render(Craftax* env) { - const int view_w = RENDER_COLS * TEX_DRAW_PX; - const int view_h = RENDER_ROWS * TEX_DRAW_PX; - const int hud_h = 142; - const int origin_x = ACH_PANEL_W; - const int window_w = origin_x + view_w + OBS_PANEL_W + ACTION_PANEL_W; - - if (env->client == NULL) { - env->client = (Client*)calloc(1, sizeof(Client)); - env->client->cell_size = TEX_DRAW_PX; - env->client->screen_width = window_w; - env->client->screen_height = view_h + hud_h; - } - - Client* client = env->client; - if (!client->window_ready) { - InitWindow(client->screen_width, client->screen_height, "Craftax Clean"); - SetTargetFPS(30); - client->window_ready = true; - } - if (!textures_loaded) { - const char* candidates[] = { - "resources/craftax/textures.png", - "../resources/craftax/textures.png", - "../../resources/craftax/textures.png", - }; - for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) { - if (FileExists(candidates[i])) { - textures = LoadTexture(candidates[i]); - break; - } - } - if (textures.id == 0) { - fprintf(stderr, "craftax_clean: textures.png not found in resources/craftax\n"); - exit(1); - } - SetTextureFilter(textures, TEXTURE_FILTER_POINT); - textures_loaded = 1; - } - if (IsKeyDown(KEY_ESCAPE)) { - exit(0); - } - craftax_clean_human_controls(env); - - int level = clampi(env->state.player_level, 0, NUM_LEVELS - 1); - int player_row = clampi(env->state.player_position[0], 0, MAP_SIZE - 1); - int player_col = clampi(env->state.player_position[1], 0, MAP_SIZE - 1); - int half_r = RENDER_ROWS / 2; - int half_c = RENDER_COLS / 2; - int top_row = player_row - half_r; - int left_col = player_col - half_c; - - BeginDrawing(); - ClearBackground(BLACK); - - for (int vr = 0; vr < RENDER_ROWS; vr++) { - for (int vc = 0; vc < RENDER_COLS; vc++) { - int wr = top_row + vr; - int wc = left_col + vc; - int dst_x = origin_x + vc * TEX_DRAW_PX; - int dst_y = vr * TEX_DRAW_PX; - - int in_map = wr >= 0 && wr < MAP_SIZE && wc >= 0 && wc < MAP_SIZE; - int block = BLOCK_OUT_OF_BOUNDS; - if (in_map) { - block = env->state.map[level][wr][wc]; - } - if (block < 0 || block >= NUM_BLOCK_TYPES) { - block = BLOCK_INVALID; - } - draw_tile(block, dst_x, dst_y, TEX_DRAW_PX); - if (!in_map) { - continue; - } - int item = env->state.item_map[level][wr][wc]; - if (item > ITEM_NONE) { - draw_tile(TEX_ITEM_BASE + item, dst_x, dst_y, TEX_DRAW_PX); - } - int mob_class; - int slot; - if (find_mob_at(&env->state, level, wr, wc, &mob_class, &slot)) { - int type_id = mobs_for_class(&env->state, level, mob_class) - ->type_id[slot]; - draw_tile(mob_tex_base[mob_class] + type_id, dst_x, dst_y, TEX_DRAW_PX); - } - } - } - - for (int from_player = 0; from_player < 2; from_player++) { - Mobs* projectiles = from_player - ? &env->state.player_projectiles[level] - : &env->state.mob_projectiles[level]; - int (*directions)[MAX_PLAYER_PROJECTILES][2] = from_player - ? env->state.player_projectile_directions - : env->state.mob_projectile_directions; - for (int i = 0; i < MAX_PLAYER_PROJECTILES; i++) { - if (!projectiles->mask[i]) { - continue; - } - int vr = projectiles->position[i][0] - top_row; - int vc = projectiles->position[i][1] - left_col; - if (vr < 0 || vr >= RENDER_ROWS || vc < 0 || vc >= RENDER_COLS) { - continue; - } - int dr = directions[level][i][0]; - int dc = directions[level][i][1]; - draw_tile(projectile_tex(projectiles->type_id[i], dr, dc), - origin_x + vc * TEX_DRAW_PX, vr * TEX_DRAW_PX, TEX_DRAW_PX); - } - } - - int player_tex = TEX_PLAYER_DOWN; - if (env->state.is_sleeping) { - player_tex = TEX_PLAYER_SLEEP; - } else if (env->state.player_direction == ACTION_LEFT) { - player_tex = TEX_PLAYER_LEFT; - } else if (env->state.player_direction == ACTION_RIGHT) { - player_tex = TEX_PLAYER_RIGHT; - } else if (env->state.player_direction == ACTION_UP) { - player_tex = TEX_PLAYER_UP; - } - draw_tile(player_tex, origin_x + half_c * TEX_DRAW_PX, - half_r * TEX_DRAW_PX, TEX_DRAW_PX); - - if (env->state.light_level < 1.0f) { - unsigned char alpha = (unsigned char)((1.0f - env->state.light_level) * 140.0f); - DrawRectangle(origin_x, 0, view_w, view_h, (Color){0, 0, 40, alpha}); - } - - int floor_bar_h = 16; - int cell_w = view_w / NUM_LEVELS; - DrawRectangle(origin_x, 0, view_w, floor_bar_h, (Color){18, 18, 18, 230}); - for (int f = 0; f < NUM_LEVELS; f++) { - int x = origin_x + f * cell_w; - int w = (f == NUM_LEVELS - 1) ? (origin_x + view_w - x) : cell_w; - bool reached = f <= env->max_floor_accum; - bool here = f == env->state.player_level; - Color fill = reached ? (Color){255, 210, 40, 255} : (Color){45, 45, 45, 255}; - if (here) { - fill = (Color){255, 235, 80, 255}; - } - DrawRectangle(x + 1, 1, w - 2, floor_bar_h - 2, fill); - if (here) { - DrawRectangleLines(x + 1, 1, w - 2, floor_bar_h - 2, WHITE); - } - DrawText(TextFormat("%d", f), x + 4, 2, 10, reached ? BLACK : (Color){140, 140, 140, 255}); - } - - draw_agent_obs(env, origin_x + view_w, 0, OBS_PANEL_W, view_h + hud_h); - - int hud_y = view_h; - Inventory* inv = &env->state.inventory; - DrawRectangle(origin_x, hud_y, view_w, hud_h, (Color){20, 20, 20, 255}); - - int health_max = max_health(&env->state); - float health_frac = clampf(env->state.player_health / health_max, 0.0f, 1.0f); - int bar_x = origin_x + 4; - int bar_y = hud_y + 4; - int bar_w = view_w - 8; - int bar_h = 18; - DrawRectangle(bar_x, bar_y, bar_w, bar_h, (Color){115, 25, 25, 255}); - DrawRectangle(bar_x, bar_y, (int)(bar_w * health_frac), bar_h, (Color){35, 190, 75, 255}); - DrawRectangleLines(bar_x, bar_y, bar_w, bar_h, (Color){220, 220, 220, 255}); - DrawText( - TextFormat("HP %.0f / %d", env->state.player_health, health_max), - bar_x + 8, - bar_y + 2, - 14, - WHITE - ); - - DrawText( - TextFormat( - "Food:%d/%d Drink:%d/%d Energy:%d/%d Mana:%d/%d L:%d t:%d", - env->state.player_food, - max_food(&env->state), - env->state.player_drink, - max_drink(&env->state), - env->state.player_energy, - max_energy(&env->state), - env->state.player_mana, - max_mana(&env->state), - env->state.player_level, - env->state.timestep - ), - origin_x + 4, - hud_y + 26, - 14, - WHITE - ); - DrawText( - TextFormat( - "XP:%d DEX:%d STR:%d INT:%d light:%.2f sleep:%d rest:%d", - env->state.player_xp, - env->state.player_dexterity, - env->state.player_strength, - env->state.player_intelligence, - env->state.light_level, - env->state.is_sleeping, - env->state.is_resting - ), - origin_x + 4, - hud_y + 44, - 14, - (Color){200, 200, 200, 255} - ); - int achievements = 0; - for (int i = 0; i < NUM_ACHIEVEMENTS; i++) { - achievements += env->state.achievements[i] ? 1 : 0; - } - int inv_y = hud_y + 62; - int inv_x = origin_x + 4; - int inv_ids[] = { - BLOCK_WOOD, BLOCK_STONE, BLOCK_COAL, BLOCK_IRON, BLOCK_DIAMOND, - TEX_SAPLING, TEX_TORCH_INV, BLOCK_RUBY, BLOCK_SAPPHIRE, TEX_BOOK, - }; - int inv_counts[] = { - inv->wood, inv->stone, inv->coal, inv->iron, inv->diamond, - inv->sapling, inv->torches, inv->ruby, inv->sapphire, inv->books, - }; - for (int i = 0; i < 10; i++) { - draw_icon_count(inv_ids[i], inv_counts[i], inv_x + 52 * i, inv_y); - } - int armour_x = inv_x + 52 * 10 + 8; - for (int slot = 0; slot < 4; slot++) { - int alvl = inv->armour[slot]; - int tex = -1; - if (alvl > 0) { - tex = (alvl >= 2 ? TEX_ARMOUR_DIAMOND : TEX_ARMOUR_IRON) + slot; - } - int overlay = -1; - int ench = env->state.armour_enchantments[slot]; - if (ench == 1) { - overlay = TEX_ARMOUR_ENCHANT_FIRE + slot; - } else if (ench == 2) { - overlay = TEX_ARMOUR_ENCHANT_ICE + slot; - } - draw_inv_slot(tex, overlay, armour_x + slot * 30, inv_y); - } - int weap_y = hud_y + 90; - int gear[] = { - inv->pickaxe > 0 ? TEX_PICKAXE_WOOD + inv->pickaxe - 1 : -1, - inv->sword > 0 ? TEX_SWORD_WOOD + inv->sword - 1 : -1, - inv->bow > 0 ? TEX_BOW : -1, - inv->arrows > 0 ? TEX_ARROW_UP : -1, - }; - int overlays[4] = {-1, -1, -1, -1}; - if (env->state.sword_enchantment == 1) { - overlays[1] = TEX_SWORD_ENCHANT_FIRE; - } else if (env->state.sword_enchantment == 2) { - overlays[1] = TEX_SWORD_ENCHANT_ICE; - } - if (inv->arrows > 0) { - if (env->state.bow_enchantment == 1) { - overlays[3] = TEX_ARROW_ENCHANT_FIRE; - } else if (env->state.bow_enchantment == 2) { - overlays[3] = TEX_ARROW_ENCHANT_ICE; - } - } - for (int i = 0; i < 4; i++) { - draw_inv_slot(gear[i], overlays[i], armour_x + 30 * i, weap_y); - } - DrawText(TextFormat("%d", inv->arrows), armour_x + 117, weap_y + 6, 14, RAYWHITE); - for (int p = 0; p < NUM_POTIONS; p++) { - draw_icon_count(TEX_POTION + p, inv->potions[p], inv_x + 52 * p, weap_y); - } - draw_inv_slot(env->state.learned_spells[0] ? TEX_PROJ_FIREBALL : -1, - -1, inv_x + 52 * 6, weap_y); - draw_inv_slot(env->state.learned_spells[1] ? TEX_PROJ_ICEBALL : -1, - -1, inv_x + 52 * 7, weap_y); - int human = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); - DrawText( - TextFormat( - "ach:%d/%d ret:%.2f len:%d %s", - achievements, - NUM_ACHIEVEMENTS, - env->episode_return_accum, - env->episode_length_accum, - human ? "HUMAN" : "Hold SHIFT to take control" - ), - origin_x + 4, - hud_y + 118, - 14, - human ? (Color){255, 210, 40, 255} : (Color){200, 200, 140, 255} - ); - - int panel_x = origin_x + view_w + OBS_PANEL_W; - int panel_h = view_h + hud_h; - int taken_action = env->agents[0].actions[0]; - DrawRectangle(panel_x, 0, ACTION_PANEL_W, panel_h, (Color){12, 18, 22, 255}); - DrawRectangleLines(panel_x, 0, ACTION_PANEL_W, panel_h, (Color){55, 70, 76, 255}); - DrawText("Actions", panel_x + 10, 8, 18, RAYWHITE); - DrawText("key", panel_x + 12, 32, 11, (Color){140, 160, 166, 255}); - DrawText("action", panel_x + 78, 32, 11, (Color){140, 160, 166, 255}); - for (int action = 0; action < ATN_DIM; action++) { - int y = 48 + action * 15; - bool selected = action == taken_action; - bool legal = env->agents[0].action_mask == NULL - || env->agents[0].action_mask[action]; - if (selected) { - DrawRectangle(panel_x + 6, y - 2, ACTION_PANEL_W - 12, 15, (Color){0, 210, 220, 255}); - } - Color text_color = selected ? BLACK - : (legal ? (Color){220, 230, 230, 255} : (Color){80, 90, 90, 255}); - DrawText(craftax_clean_action_keys[action], panel_x + 12, y, 10, text_color); - DrawText(TextFormat("%02d %s", action, craftax_clean_action_names[action]), - panel_x + 78, y, 10, text_color); - } - - int ach_h = view_h + hud_h; - DrawRectangle(0, 0, ACH_PANEL_W, ach_h, WHITE); - DrawText("Achievements", 8, 6, 16, BLACK); - int ach_top = 26; - int ach_row = (ach_h - ach_top) / NUM_ACHIEVEMENTS; - if (ach_row < 10) { - ach_row = 10; - } - for (int i = 0; i < NUM_ACHIEVEMENTS; i++) { - int y = ach_top + i * ach_row; - bool done = env->state.achievements[i] != 0; - if (done) { - DrawRectangle(0, y, ACH_PANEL_W, ach_row, (Color){46, 180, 80, 255}); - } - DrawText( - craftax_clean_ach_names[i], - 6, - y + (ach_row > 10 ? 1 : 0), - 10, - done ? WHITE : (Color){50, 50, 50, 255} - ); - } - - EndDrawing(); - puf_web_vsync(); -} - -void puf_close(Craftax* env) { - if (env->client == NULL) { - return; - } - if (env->client->window_ready) { - CloseWindow(); - } - free(env->client); - env->client = NULL; -} - diff --git a/resources/craftax/textures.png b/resources/craftax/textures.png new file mode 100644 index 0000000000..36fd651300 Binary files /dev/null and b/resources/craftax/textures.png differ diff --git a/resources/craftax_clean/craftax_clean_weights.bin b/resources/craftax_clean/craftax_clean_weights.bin deleted file mode 100644 index c9167c8450..0000000000 Binary files a/resources/craftax_clean/craftax_clean_weights.bin and /dev/null differ diff --git a/scripts/bench_craftax_sps.c b/scripts/bench_craftax_sps.c index 98e35291d6..266dd70bc0 100644 --- a/scripts/bench_craftax_sps.c +++ b/scripts/bench_craftax_sps.c @@ -5,13 +5,7 @@ // scripts/bench_craftax_sps.c raylib-5.5_linux_amd64/lib/libraylib.a \ // -lm -lpthread -lGL -ldl -o bench_craftax_sps // -// clang -O2 -mavx2 -mfma -fopenmp -std=c11 -D_POSIX_C_SOURCE=200809L -DPLATFORM_DESKTOP \ -// -DBENCH_CLEAN -I. -Isrc -Ivendor -Iocean/craftax_clean -Iraylib-5.5_linux_amd64/include \ -// scripts/bench_craftax_sps.c raylib-5.5_linux_amd64/lib/libraylib.a \ -// -lm -lpthread -lGL -ldl -o bench_craftax_clean_sps -// // ./bench_craftax_sps [n_envs=8192] [n_threads=1] -// ./bench_craftax_clean_sps 8192 16 #define _POSIX_C_SOURCE 200809L #include @@ -21,21 +15,10 @@ #include #include -#ifdef BENCH_CLEAN -#include "ocean/craftax_clean/craftax_clean.h" -#define BENCH_OBS OBS_SIZE -#define BENCH_ACTIONS NUM_ACTIONS -#define BENCH_ENV_NAME "craftax_clean" -#else -#define CRAFTAX_ENABLE_ENV_IMPL #include "ocean/craftax/craftax.h" -#include "ocean/craftax/step_crafting.h" -#include "ocean/craftax/step_update_mobs.h" -#include "ocean/craftax/step_spawn_mobs.h" -#define BENCH_OBS CRAFTAX_OBS_SIZE -#define BENCH_ACTIONS CRAFTAX_NUM_ACTIONS +#define BENCH_OBS OBS_SIZE +#define BENCH_ACTIONS ATN_DIM #define BENCH_ENV_NAME "craftax" -#endif static double now_s(void) { struct timespec ts; diff --git a/src/ocean.cu b/src/ocean.cu index 23ac57fbd6..4cc62e53f8 100644 --- a/src/ocean.cu +++ b/src/ocean.cu @@ -52,9 +52,6 @@ __device__ static const float OSRS_ITEM_OBS_TABLE_DEV #ifdef PUFFER_CRAFTAX #include "../ocean/craftax/craftax.cu" #endif -#ifdef PUFFER_CRAFTAX_CLEAN -#include "../ocean/craftax_clean/craftax_clean.cu" -#endif // Override encoder vtable for known ocean environments. No-op for unknown envs. static void create_custom_encoder(const char* env_name, Encoder* enc) { @@ -72,12 +69,6 @@ static void create_custom_encoder(const char* env_name, Encoder* enc) { return; } #endif -#ifdef PUFFER_CRAFTAX_CLEAN - if (strcmp(env_name, "craftax_clean") == 0) { - create_craftax_encoder(enc); - return; - } -#endif #ifdef PUFFER_NMMO3 if (strcmp(env_name, "nmmo3") == 0) { #ifdef N3_ATTN diff --git a/tests/craftax_clean_parity.py b/tests/craftax_clean_parity.py deleted file mode 100644 index 84f6c0e33a..0000000000 --- a/tests/craftax_clean_parity.py +++ /dev/null @@ -1,653 +0,0 @@ -#!/usr/bin/env python3 -"""Compare craftax_clean against ocean/craftax. - -Checks: -1. Reset worldgen (maps, items, lights, ladders, starter stats) -2. Packed observations after reset and after a shared action sequence -""" - -from __future__ import annotations - -import argparse -import ctypes -import random -import subprocess -import sys -import tempfile -from pathlib import Path - - -MAP_SIZE = 48 -NUM_LEVELS = 9 -NUM_POTIONS = 6 -MAP_CELLS = NUM_LEVELS * MAP_SIZE * MAP_SIZE -OBS_ROWS = 9 -OBS_COLS = 11 -NUM_MOB_CLASSES = 5 -INVENTORY_OBS_SIZE = 51 -OBS_TILE_CHANNELS = 3 + NUM_MOB_CLASSES -OBS_SIZE = OBS_ROWS * OBS_COLS * OBS_TILE_CHANNELS + INVENTORY_OBS_SIZE -NUM_ACTIONS = 43 - -DUNGEON_FLOORS = (1, 3, 4) -SMOOTH_FLOORS = (0, 2, 5, 6, 7, 8) -BOSS_FLOOR = 8 -FIRE_FLOOR = 6 - -BLOCK_WATER = 3 -BLOCK_STONE = 4 -BLOCK_TREE = 5 -BLOCK_PATH = 7 -BLOCK_LAVA = 14 -BLOCK_DARKNESS = 18 -BLOCK_CHEST = 23 -BLOCK_FOUNTAIN = 24 -BLOCK_FIRE_GRASS = 25 -BLOCK_FIRE_TREE = 28 -BLOCK_ENCHANTMENT_TABLE_FIRE = 30 -BLOCK_ENCHANTMENT_TABLE_ICE = 31 -BLOCK_NECROMANCER = 32 -BLOCK_GRAVE = 33 - -ITEM_TORCH = 1 -ITEM_LADDER_DOWN = 2 -ITEM_LADDER_UP = 3 - - -class WorldDump(ctypes.Structure): - _fields_ = [ - ("map", ctypes.c_int32 * MAP_CELLS), - ("item_map", ctypes.c_int32 * MAP_CELLS), - ("light_map", ctypes.c_uint8 * MAP_CELLS), - ("down_ladders", ctypes.c_int32 * (NUM_LEVELS * 2)), - ("up_ladders", ctypes.c_int32 * (NUM_LEVELS * 2)), - ("monsters_killed", ctypes.c_int32 * NUM_LEVELS), - ("potion_mapping", ctypes.c_int32 * NUM_POTIONS), - ("player_position", ctypes.c_int32 * 2), - ("player_level", ctypes.c_int32), - ("player_direction", ctypes.c_int32), - ("player_health", ctypes.c_float), - ("player_food", ctypes.c_int32), - ("player_drink", ctypes.c_int32), - ("player_energy", ctypes.c_int32), - ("player_mana", ctypes.c_int32), - ("player_dexterity", ctypes.c_int32), - ("player_strength", ctypes.c_int32), - ("player_intelligence", ctypes.c_int32), - ("light_level", ctypes.c_float), - ("boss_timesteps", ctypes.c_int32), - ] - - -SOURCE = r""" -#include -#include -#include "ocean/craftax/worldgen.h" -#include "ocean/craftax_clean/craftax_clean.h" - -static void flatten_clean(const State* state, WorldDump* out) { - int cell = 0; - for (int level = 0; level < NUM_LEVELS; level++) { - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - out->map[cell] = state->map[level][row][col]; - out->item_map[cell] = state->item_map[level][row][col]; - out->light_map[cell] = state->light_map[level][row][col]; - cell++; - } - } - out->down_ladders[level * 2 + 0] = state->down_ladders[level][0]; - out->down_ladders[level * 2 + 1] = state->down_ladders[level][1]; - out->up_ladders[level * 2 + 0] = state->up_ladders[level][0]; - out->up_ladders[level * 2 + 1] = state->up_ladders[level][1]; - out->monsters_killed[level] = state->monsters_killed[level]; - } - memcpy(out->potion_mapping, state->potion_mapping, sizeof(out->potion_mapping)); - out->player_position[0] = state->player_position[0]; - out->player_position[1] = state->player_position[1]; - out->player_level = state->player_level; - out->player_direction = state->player_direction; - out->player_health = state->player_health; - out->player_food = state->player_food; - out->player_drink = state->player_drink; - out->player_energy = state->player_energy; - out->player_mana = state->player_mana; - out->player_dexterity = state->player_dexterity; - out->player_strength = state->player_strength; - out->player_intelligence = state->player_intelligence; - out->light_level = state->light_level; - out->boss_timesteps = state->boss_timestep_to_spawn_this_round; -} - -static void flatten_full(const CraftaxWorldState* world, WorldDump* out) { - int cell = 0; - for (int level = 0; level < NUM_LEVELS; level++) { - for (int row = 0; row < MAP_SIZE; row++) { - for (int col = 0; col < MAP_SIZE; col++) { - out->map[cell] = world->map[level][row][col]; - out->item_map[cell] = world->item_map[level][row][col]; - out->light_map[cell] = world->light_map[level][row][col]; - cell++; - } - } - out->down_ladders[level * 2 + 0] = world->down_ladders[level][0]; - out->down_ladders[level * 2 + 1] = world->down_ladders[level][1]; - out->up_ladders[level * 2 + 0] = world->up_ladders[level][0]; - out->up_ladders[level * 2 + 1] = world->up_ladders[level][1]; - out->monsters_killed[level] = world->monsters_killed[level]; - } - memcpy(out->potion_mapping, world->potion_mapping, sizeof(out->potion_mapping)); - out->player_position[0] = world->player_position[0]; - out->player_position[1] = world->player_position[1]; - out->player_level = world->player_level; - out->player_direction = world->player_direction; - out->player_health = world->player_health; - out->player_food = world->player_food; - out->player_drink = world->player_drink; - out->player_energy = world->player_energy; - out->player_mana = world->player_mana; - out->player_dexterity = world->player_dexterity; - out->player_strength = world->player_strength; - out->player_intelligence = world->player_intelligence; - out->light_level = world->light_level; - out->boss_timesteps = world->boss_timesteps_to_spawn_this_round; -} - -void generate_clean_world(int32_t seed, WorldDump* out) { - State state; - generate_world(&state, seed); - flatten_clean(&state, out); -} - -void generate_full_world(int32_t seed, WorldDump* out) { - CraftaxWorldState world; - craftax_generate_world_from_seed((uint32_t)seed, &world); - flatten_full(&world, out); -} -""" - -CLEAN_REPLAY = r""" -#include -#include -#include "ocean/craftax_clean/craftax_clean.h" - -void replay_clean( - int32_t seed, - const int32_t* actions, - int32_t num_actions, - float* obs_out, - float* rewards_out, - int32_t* terminal_step -) { - Craftax env; - float action_value = 0.0f; - float reward_value = 0.0f; - float terminal_value = 0.0f; - float live_obs[OBS_SIZE]; - memset(&env, 0, sizeof(env)); - env.num_agents = 1; - env.rng = (unsigned int)seed; - env.seed = (uint64_t)(uint32_t)seed; - env.agents[0].actions = &action_value; - env.agents[0].rewards = &reward_value; - env.agents[0].terminals = &terminal_value; - env.agents[0].observations = live_obs; - - craftax_clean_set_reset_pool_size(0); - puf_reset(&env); - memcpy(obs_out, live_obs, OBS_SIZE * sizeof(float)); - *terminal_step = -1; - for (int32_t i = 0; i < num_actions; i++) { - action_value = (float)actions[i]; - puf_step(&env); - memcpy(obs_out + (size_t)(i + 1) * OBS_SIZE, live_obs, OBS_SIZE * sizeof(float)); - rewards_out[i] = env.agents[0].rewards[0]; - if (env.agents[0].terminals[0] > 0.5f) { - *terminal_step = i; - break; - } - } -} -""" - -FULL_REPLAY = r""" -#include -#include -#include "ocean/craftax/craftax.h" -#include "ocean/craftax/step_crafting.h" -#include "ocean/craftax/step_update_mobs.h" -#include "ocean/craftax/step_spawn_mobs.h" - -void replay_full( - int32_t seed, - const int32_t* actions, - int32_t num_actions, - float* obs_out, - float* rewards_out, - int32_t* terminal_step -) { - Craftax env; - float action_value = 0.0f; - float reward_value = 0.0f; - float terminal_value = 0.0f; - float live_obs[OBS_SIZE]; - memset(&env, 0, sizeof(env)); - env.num_agents = 1; - env.seed = (uint64_t)(uint32_t)seed; - env.rng = (unsigned int)seed; - env.agents[0].actions = &action_value; - env.agents[0].rewards = &reward_value; - env.agents[0].terminals = &terminal_value; - env.agents[0].observations = live_obs; - - craftax_set_reset_pool_size(0); - c_init(&env); - puf_reset(&env); - memcpy(obs_out, live_obs, OBS_SIZE * sizeof(float)); - *terminal_step = -1; - for (int32_t i = 0; i < num_actions; i++) { - action_value = (float)actions[i]; - // puf_step also polls raylib human controls; skip that so the - // harness stays deterministic when multiple raylib .so copies load. - c_step_gameplay(&env); - c_step_encode(&env); - memcpy(obs_out + (size_t)(i + 1) * OBS_SIZE, live_obs, OBS_SIZE * sizeof(float)); - rewards_out[i] = env.agents[0].rewards[0]; - if (env.agents[0].terminals[0] > 0.5f) { - *terminal_step = i; - break; - } - } - puf_close(&env); -} -""" - - -def compile_lib(root: Path) -> ctypes.CDLL: - tmp = tempfile.TemporaryDirectory(prefix="craftax_clean_parity_") - src = Path(tmp.name) / "parity.c" - so = Path(tmp.name) / "parity.so" - # Keep the tempdir alive for the process lifetime. - compile_lib._tmp = tmp # type: ignore[attr-defined] - c_struct = f""" -#include -typedef struct WorldDump {{ - int32_t map[{MAP_CELLS}]; - int32_t item_map[{MAP_CELLS}]; - uint8_t light_map[{MAP_CELLS}]; - int32_t down_ladders[{NUM_LEVELS * 2}]; - int32_t up_ladders[{NUM_LEVELS * 2}]; - int32_t monsters_killed[{NUM_LEVELS}]; - int32_t potion_mapping[{NUM_POTIONS}]; - 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; - int32_t player_dexterity; - int32_t player_strength; - int32_t player_intelligence; - float light_level; - int32_t boss_timesteps; -}} WorldDump; -""" - src.write_text(c_struct + SOURCE) - subprocess.run( - [ - "cc", - "-std=c99", - "-O2", - "-shared", - "-fPIC", - "-I", - str(root), - "-I", - str(root / "src"), - "-I", - str(root / "ocean" / "craftax_clean"), - "-I", - str(root / "raylib-5.5_linux_amd64/include"), - str(src), - str(root / "raylib-5.5_linux_amd64/lib/libraylib.a"), - "-lm", - "-lpthread", - "-lGL", - "-ldl", - "-o", - str(so), - ], - check=True, - cwd=root, - ) - lib = ctypes.CDLL(str(so)) - lib.generate_clean_world.argtypes = [ctypes.c_int32, ctypes.POINTER(WorldDump)] - lib.generate_clean_world.restype = None - lib.generate_full_world.argtypes = [ctypes.c_int32, ctypes.POINTER(WorldDump)] - lib.generate_full_world.restype = None - return lib - - -def _cc(root: Path, src: Path, so: Path) -> None: - subprocess.run( - [ - "cc", - "-std=c99", - "-O2", - "-shared", - "-fPIC", - "-I", - str(root), - "-I", - str(root / "src"), - "-I", - str(root / "ocean" / "craftax_clean"), - "-I", - str(root / "raylib-5.5_linux_amd64/include"), - str(src), - str(root / "raylib-5.5_linux_amd64/lib/libraylib.a"), - "-lm", - "-lpthread", - "-lGL", - "-ldl", - "-o", - str(so), - ], - check=True, - cwd=root, - ) - - -def compile_replay_libs(root: Path) -> tuple[ctypes.CDLL, ctypes.CDLL]: - tmp = tempfile.TemporaryDirectory(prefix="craftax_clean_replay_") - compile_replay_libs._tmp = tmp # type: ignore[attr-defined] - tmp_path = Path(tmp.name) - - clean_src = tmp_path / "replay_clean.c" - clean_so = tmp_path / "replay_clean.so" - clean_src.write_text(CLEAN_REPLAY) - _cc(root, clean_src, clean_so) - clean = ctypes.CDLL(str(clean_so)) - clean.replay_clean.argtypes = [ - ctypes.c_int32, - ctypes.POINTER(ctypes.c_int32), - ctypes.c_int32, - ctypes.POINTER(ctypes.c_float), - ctypes.POINTER(ctypes.c_float), - ctypes.POINTER(ctypes.c_int32), - ] - clean.replay_clean.restype = None - - full_src = tmp_path / "replay_full.c" - full_so = tmp_path / "replay_full.so" - full_src.write_text(FULL_REPLAY) - _cc(root, full_src, full_so) - full = ctypes.CDLL(str(full_so)) - full.replay_full.argtypes = [ - ctypes.c_int32, - ctypes.POINTER(ctypes.c_int32), - ctypes.c_int32, - ctypes.POINTER(ctypes.c_float), - ctypes.POINTER(ctypes.c_float), - ctypes.POINTER(ctypes.c_int32), - ] - full.replay_full.restype = None - return clean, full - - -def obs_section(index: int) -> str: - map_size = OBS_ROWS * OBS_COLS * OBS_TILE_CHANNELS - if index < map_size: - cell = index // OBS_TILE_CHANNELS - channel = index % OBS_TILE_CHANNELS - names = ["block", "item", "visible", "melee", "passive", "ranged", "mob_proj", "player_proj"] - name = names[channel] if channel < len(names) else str(channel) - return f"map cell={cell} channel={name}" - return f"scalar[{index - map_size}]" - - -def first_obs_mismatch(clean, full, atol: float): - best_index = None - best_diff = 0.0 - for index, (left, right) in enumerate(zip(clean, full)): - diff = abs(float(left) - float(right)) - if diff > atol and diff > best_diff: - best_index = index - best_diff = diff - if best_index is None: - return None - return best_index, best_diff, float(clean[best_index]), float(full[best_index]) - - -def first_mismatch(left, right, name: str): - for index, (a, b) in enumerate(zip(left, right)): - if a != b: - return f"{name}[{index}] clean={a} full={b}" - return None - - -def compare_dumps(clean: WorldDump, full: WorldDump) -> list[str]: - diffs = [] - for name, _ctype in WorldDump._fields_: - left = getattr(clean, name) - right = getattr(full, name) - if hasattr(left, "__len__"): - mismatch = first_mismatch(left, right, name) - if mismatch: - diffs.append(mismatch) - elif left != right: - diffs.append(f"{name} clean={left} full={right}") - return diffs - - -def count_block(dump: WorldDump, level: int, block: int) -> int: - start = level * MAP_SIZE * MAP_SIZE - end = start + MAP_SIZE * MAP_SIZE - return sum(1 for value in dump.map[start:end] if value == block) - - -def count_item(dump: WorldDump, level: int, item: int) -> int: - start = level * MAP_SIZE * MAP_SIZE - end = start + MAP_SIZE * MAP_SIZE - return sum(1 for value in dump.item_map[start:end] if value == item) - - -def min_light(dump: WorldDump, level: int) -> int: - start = level * MAP_SIZE * MAP_SIZE - end = start + MAP_SIZE * MAP_SIZE - return min(dump.light_map[start:end]) - - -def mean_light(dump: WorldDump, level: int) -> float: - start = level * MAP_SIZE * MAP_SIZE - end = start + MAP_SIZE * MAP_SIZE - values = dump.light_map[start:end] - return sum(values) / float(len(values)) - - -def check_structure(dump: WorldDump) -> list[str]: - errors = [] - if dump.player_position[0] != MAP_SIZE // 2 or dump.player_position[1] != MAP_SIZE // 2: - errors.append("player does not spawn at map center") - if dump.monsters_killed[0] != 10: - errors.append("overworld is not pre-cleared") - if dump.player_health != 9.0 or dump.player_food != 9: - errors.append("starter vitals are not 9") - - for level in DUNGEON_FLOORS: - if count_block(dump, level, BLOCK_DARKNESS) == 0: - errors.append(f"floor {level} is missing BLOCK_DARKNESS") - if count_block(dump, level, BLOCK_CHEST) < 6: - errors.append( - f"floor {level} has {count_block(dump, level, BLOCK_CHEST)} chests, expected at least 6" - ) - if count_item(dump, level, ITEM_TORCH) < 16: - errors.append(f"floor {level} is missing room-corner torches") - if min_light(dump, level) != 255: - errors.append(f"floor {level} is not fully lit") - if count_item(dump, level, ITEM_LADDER_UP) != 1: - errors.append(f"floor {level} is missing an up ladder") - if count_item(dump, level, ITEM_LADDER_DOWN) != 1: - errors.append(f"floor {level} is missing a down ladder") - - if count_block(dump, 1, BLOCK_FOUNTAIN) == 0: - errors.append("dungeon is missing a fountain") - if count_block(dump, 3, BLOCK_ENCHANTMENT_TABLE_ICE) == 0: - errors.append("sewers are missing the ice enchantment table") - if count_block(dump, 4, BLOCK_ENCHANTMENT_TABLE_FIRE) == 0: - errors.append("vault is missing the fire enchantment table") - - if mean_light(dump, 0) < 250: - errors.append("overworld is not fully lit") - if mean_light(dump, FIRE_FLOOR) < 250: - errors.append("fire realm is not bright") - if mean_light(dump, BOSS_FLOOR) > 80: - errors.append("boss floor is not dark") - if count_block(dump, BOSS_FLOOR, BLOCK_NECROMANCER) == 0: - errors.append("boss floor is missing the necromancer") - if count_item(dump, BOSS_FLOOR, ITEM_LADDER_UP) != 0: - errors.append("boss floor should not have an up ladder") - if count_item(dump, BOSS_FLOOR, ITEM_LADDER_DOWN) != 0: - errors.append("boss floor should not have a down ladder") - if count_block(dump, FIRE_FLOOR, BLOCK_FIRE_GRASS) == 0: - errors.append("fire realm is missing fire grass") - return errors - - -def replay_seed(clean_lib, full_lib, seed: int, actions, atol: float): - num_actions = len(actions) - action_array = (ctypes.c_int32 * num_actions)(*actions) - obs_count = (num_actions + 1) * OBS_SIZE - clean_obs = (ctypes.c_float * obs_count)() - full_obs = (ctypes.c_float * obs_count)() - clean_rewards = (ctypes.c_float * num_actions)() - full_rewards = (ctypes.c_float * num_actions)() - clean_done = ctypes.c_int32(-1) - full_done = ctypes.c_int32(-1) - - clean_lib.replay_clean( - seed, - action_array, - num_actions, - clean_obs, - clean_rewards, - ctypes.byref(clean_done), - ) - full_lib.replay_full( - seed, - action_array, - num_actions, - full_obs, - full_rewards, - ctypes.byref(full_done), - ) - - compare_steps = num_actions + 1 - if clean_done.value >= 0 or full_done.value >= 0: - compare_steps = min( - clean_done.value if clean_done.value >= 0 else num_actions, - full_done.value if full_done.value >= 0 else num_actions, - ) + 2 - compare_steps = min(compare_steps, num_actions + 1) - - for step in range(compare_steps): - start = step * OBS_SIZE - end = start + OBS_SIZE - mismatch = first_obs_mismatch(clean_obs[start:end], full_obs[start:end], atol) - if mismatch is not None: - index, abs_diff, clean_value, full_value = mismatch - return ( - f"obs mismatch seed={seed} step={step} " - f"index={index} section={obs_section(index)} " - f"abs_diff={abs_diff:.8g} clean={clean_value:.8g} full={full_value:.8g}" - ) - if step > 0: - reward_index = step - 1 - if abs(float(clean_rewards[reward_index]) - float(full_rewards[reward_index])) > atol: - return ( - f"reward mismatch seed={seed} step={step} " - f"clean={clean_rewards[reward_index]:.8g} " - f"full={full_rewards[reward_index]:.8g}" - ) - if clean_done.value != full_done.value: - return ( - f"terminal mismatch seed={seed} " - f"clean_done={clean_done.value} full_done={full_done.value}" - ) - return None - - -def run(args: argparse.Namespace) -> int: - root = Path(__file__).resolve().parents[1] - print(f"Compiling craftax_clean vs craftax worldgen harness in {root}") - lib = compile_lib(root) - print("Compiling step/observation replay harnesses") - clean_lib, full_lib = compile_replay_libs(root) - - failures = 0 - for seed in range(args.seeds): - clean = WorldDump() - full = WorldDump() - lib.generate_clean_world(seed, ctypes.byref(clean)) - lib.generate_full_world(seed, ctypes.byref(full)) - - diffs = compare_dumps(clean, full) - struct_errors = check_structure(clean) - rng = random.Random(args.action_seed + seed) - actions = [rng.randrange(NUM_ACTIONS) for _ in range(args.steps)] - obs_error = replay_seed(clean_lib, full_lib, seed, actions, args.atol) - - if diffs or struct_errors or obs_error: - failures += 1 - print(f"FAIL seed={seed}") - for diff in diffs[:8]: - print(f" {diff}") - for error in struct_errors: - print(f" {error}") - if obs_error: - print(f" {obs_error}") - if not args.keep_going: - return 1 - elif args.verbose: - print( - f"PASS seed={seed} " - f"dungeon_dark={count_block(clean, 1, BLOCK_DARKNESS)} " - f"dungeon_chests={count_block(clean, 1, BLOCK_CHEST)} " - f"boss_light={mean_light(clean, BOSS_FLOOR):.1f} " - f"steps={args.steps}" - ) - - if failures: - print(f"FAIL craftax_clean parity: {failures}/{args.seeds} seeds diverged") - return 1 - - sample = WorldDump() - lib.generate_clean_world(0, ctypes.byref(sample)) - print( - "PASS craftax_clean parity: " - f"seeds={args.seeds} steps={args.steps} atol={args.atol} " - f"dungeon_chests={count_block(sample, 1, BLOCK_CHEST)} " - f"dungeon_darkness={count_block(sample, 1, BLOCK_DARKNESS)} " - f"sewer_ice_table={count_block(sample, 3, BLOCK_ENCHANTMENT_TABLE_ICE)} " - f"vault_fire_table={count_block(sample, 4, BLOCK_ENCHANTMENT_TABLE_FIRE)} " - f"fire_light={mean_light(sample, FIRE_FLOOR):.1f} " - f"boss_light={mean_light(sample, BOSS_FLOOR):.1f}" - ) - return 0 - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--seeds", type=int, default=16) - parser.add_argument("--steps", type=int, default=200) - parser.add_argument("--action-seed", type=int, default=0) - parser.add_argument("--atol", type=float, default=1e-5) - parser.add_argument("--keep-going", action="store_true") - parser.add_argument("--verbose", action="store_true") - return run(parser.parse_args()) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/craftax_convergence_bench.py b/tests/craftax_convergence_bench.py deleted file mode 100644 index b0aac95390..0000000000 --- a/tests/craftax_convergence_bench.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Compare convergence of Craftax Classic vs Full on overlapping achievements. - -Runs both envs through `uv run puffer train` back-to-back (default 10M env -steps each), then parses pufferlib's per-run JSON log and plots: - - mean episode score over env steps - - per-achievement unlock rate (for the 22 Classic-compatible achievements) - - wall-clock time to reach each score threshold - -The envs share the first 22 achievement IDs (Classic's entire set). Full -has 67 achievements total; the extra 45 are plotted separately so Full -isn't rewarded twice for reaching the same tier. - -Usage: - uv run python tests/craftax_convergence_bench.py --timesteps 10_000_000 - uv run python tests/craftax_convergence_bench.py --skip-train --plot-only -""" -import argparse -import json -import os -import subprocess -import sys -from pathlib import Path - -import numpy as np - - -REPO = Path(__file__).resolve().parent.parent -LOG_DIR = REPO / "logs" - -CLASSIC_ACHIEVEMENTS = [ - "collect_wood", "place_table", "eat_cow", "collect_sapling", "collect_drink", - "make_wood_pickaxe", "make_wood_sword", "place_plant", "defeat_zombie", - "collect_stone", "place_stone", "eat_plant", "defeat_skeleton", - "make_stone_pickaxe", "make_stone_sword", "wake_up", "place_furnace", - "collect_coal", "collect_iron", "collect_diamond", "make_iron_pickaxe", - "make_iron_sword", -] - -SCORE_THRESHOLDS = [1, 3, 5, 7, 10, 15] - - -def train(env_name, timesteps): - env_log_dir = LOG_DIR / env_name - env_log_dir.mkdir(parents=True, exist_ok=True) - before = {p.name for p in env_log_dir.glob("*.json")} - - # pufferlib._C is compiled for one env at a time; rebuild before each run. - build_cmd = [ - "uv", "run", "--with", "pybind11", "--with", "rich_argparse", - "./build.sh", env_name, - ] - print(f"\n=== rebuilding pufferlib._C for {env_name} ===") - subprocess.check_call(build_cmd, cwd=REPO) - - cmd = [ - "uv", "run", "--with", "pybind11", "--with", "rich_argparse", - "puffer", "train", env_name, - "--train.total-timesteps", str(int(timesteps)), - ] - print(f"\n=== training {env_name} for {timesteps:,} steps ===") - print(" ".join(cmd)) - subprocess.check_call(cmd, cwd=REPO) - after = {p.name for p in env_log_dir.glob("*.json")} - new = sorted(after - before) - if not new: - raise RuntimeError(f"no new log file under {env_log_dir}") - return env_log_dir / new[-1] - - -def load_run(path): - with open(path) as f: - raw = json.load(f) - m = raw["metrics"] - steps = np.array(m["agent_steps"], dtype=np.float64) - uptime = np.array(m["uptime"], dtype=np.float64) - score = np.array(m.get("env/score", [np.nan] * len(steps)), dtype=np.float64) - ach = {} - for name in CLASSIC_ACHIEVEMENTS: - key = f"env/{name}" - if key in m: - ach[name] = np.array(m[key], dtype=np.float64) - return {"steps": steps, "uptime": uptime, "score": score, "ach": ach, "path": str(path)} - - -def time_to_threshold(steps, score, threshold): - above = np.nonzero(score >= threshold)[0] - if len(above) == 0: - return None - return float(steps[above[0]]) - - -def print_summary(label, run): - print(f"\n--- {label} ({run['path']}) ---") - total_steps = int(run["steps"][-1]) - wall = run["uptime"][-1] - peak = float(np.nanmax(run["score"])) if run["score"].size else float("nan") - final = float(run["score"][-1]) if run["score"].size else float("nan") - print(f"total env steps: {total_steps:,} wall: {wall/60:.1f}min " - f"final score: {final:.2f} peak: {peak:.2f}") - print(f"time to score threshold (env steps):") - for t in SCORE_THRESHOLDS: - s = time_to_threshold(run["steps"], run["score"], t) - if s is None: - print(f" >={t:>2}: NOT REACHED") - else: - wall_at = run["uptime"][np.nonzero(run["score"] >= t)[0][0]] - print(f" >={t:>2}: {int(s):>12,} steps ({wall_at/60:5.1f} min)") - if run["ach"]: - print("final per-achievement unlock rate (mean over eval episodes):") - for name in CLASSIC_ACHIEVEMENTS: - if name in run["ach"]: - print(f" {name:<22s} {run['ach'][name][-1]:.3f}") - - -def plot(runs, out_path): - try: - import matplotlib.pyplot as plt - except Exception as exc: - print(f"matplotlib unavailable ({exc}); skipping plot.") - return - fig, axes = plt.subplots(1, 2, figsize=(14, 5)) - for label, run in runs.items(): - axes[0].plot(run["steps"] / 1e6, run["score"], label=label) - axes[0].set_xlabel("env steps (M)") - axes[0].set_ylabel("mean episode score (achievements)") - axes[0].set_title("Convergence: score vs env steps") - axes[0].legend() - axes[0].grid(True, alpha=0.3) - - for label, run in runs.items(): - axes[1].plot(run["uptime"] / 60, run["score"], label=label) - axes[1].set_xlabel("wall time (min)") - axes[1].set_ylabel("mean episode score") - axes[1].set_title("Convergence: score vs wall time") - axes[1].legend() - axes[1].grid(True, alpha=0.3) - - fig.tight_layout() - fig.savefig(out_path, dpi=120) - print(f"\nwrote plot to {out_path}") - - -def main(): - ap = argparse.ArgumentParser() - ap.add_argument("--timesteps", type=float, default=10_000_000, - help="env steps per training run") - ap.add_argument("--skip-train", action="store_true", - help="skip training; use most recent log in logs/{env}") - ap.add_argument("--classic-log", type=str, default=None, - help="explicit path to craftax_classic log json") - ap.add_argument("--full-log", type=str, default=None, - help="explicit path to craftax log json") - ap.add_argument("--out", type=str, default="craftax_convergence.png") - args = ap.parse_args() - - runs = {} - for label, env_name, override in [ - ("Classic", "craftax_classic", args.classic_log), - ("Full", "craftax", args.full_log), - ]: - if override: - path = Path(override) - elif args.skip_train: - candidates = sorted((LOG_DIR / env_name).glob("*.json")) - if not candidates: - print(f"no logs for {env_name} under {LOG_DIR/env_name}; skipping.") - continue - path = candidates[-1] - else: - path = train(env_name, args.timesteps) - runs[label] = load_run(path) - print_summary(label, runs[label]) - - if len(runs) >= 1: - plot(runs, args.out) - - -if __name__ == "__main__": - main() diff --git a/tests/craftax_parity.py b/tests/craftax_parity.py index bdf9939c1f..4e15cec454 100644 --- a/tests/craftax_parity.py +++ b/tests/craftax_parity.py @@ -1,9 +1,30 @@ +#!/usr/bin/env python3 +"""Compare ocean/craftax against original JAX Craftax-Symbolic-v1. + +https://github.com/MichaelTMatthews/Craftax + +The reference is the symbolic env (8268-d one-hot obs from +render_craftax_symbolic), not the pixels env. Those observations are packed +into the 9x11x8+51 layout used by ocean/craftax. + +Checks: +1. Reset worldgen (maps, items, lights, ladders, starter stats) +2. Packed symbolic observations after reset and after a shared action sequence + +JAX sleep/rest is collapsed into one agent step to match puf_step. Rewards are +not compared: clean dropped health shaping, adds armour delta, and uses -1 on +death. +""" + +from __future__ import annotations + import argparse import ctypes import os +import random import subprocess +import sys import tempfile -from collections import deque from pathlib import Path os.environ.setdefault("JAX_PLATFORM_NAME", "cpu") @@ -12,1336 +33,641 @@ import jax import jax.numpy as jnp import numpy as np - +from craftax.craftax.envs.craftax_symbolic_env import CraftaxSymbolicEnvNoAutoReset from craftax.craftax_env import make_craftax_env_from_name -try: - from craftax_state_fixtures import ( - CraftaxState, - craftax_state_to_jax, - flatten_env_state, - ) -except ModuleNotFoundError: - from tests.craftax_state_fixtures import ( - CraftaxState, - craftax_state_to_jax, - flatten_env_state, - ) -OBS_SIZE = 8268 -NUM_ACTIONS = 43 - +MAP_SIZE = 48 +NUM_LEVELS = 9 +NUM_POTIONS = 6 +MAP_CELLS = NUM_LEVELS * MAP_SIZE * MAP_SIZE OBS_ROWS = 9 OBS_COLS = 11 -NUM_BLOCK_TYPES = 37 -NUM_ITEM_TYPES = 5 NUM_MOB_CLASSES = 5 NUM_MOB_TYPES = 8 -NUM_TILE_CHANNELS = NUM_BLOCK_TYPES + NUM_ITEM_TYPES + NUM_MOB_CLASSES * NUM_MOB_TYPES + 1 -MAP_OBS_SIZE = OBS_ROWS * OBS_COLS * NUM_TILE_CHANNELS -MAP_SIZE = 48 -NUM_LEVELS = 9 -MONSTERS_KILLED_TO_CLEAR_LEVEL = 8 - -NOOP = 0 -LEFT = 1 -RIGHT = 2 -UP = 3 -DOWN = 4 -DO = 5 -PLACE_STONE = 7 -PLACE_TABLE = 8 -PLACE_FURNACE = 9 -MAKE_WOOD_PICKAXE = 11 -MAKE_STONE_PICKAXE = 12 -MAKE_IRON_PICKAXE = 13 -MAKE_WOOD_SWORD = 14 -MAKE_STONE_SWORD = 15 -MAKE_IRON_SWORD = 16 -DESCEND = 18 -MAKE_DIAMOND_PICKAXE = 20 -MAKE_DIAMOND_SWORD = 21 -MAKE_IRON_ARMOUR = 22 -MAKE_DIAMOND_ARMOUR = 23 -SHOOT_ARROW = 24 -MAKE_ARROW = 25 -CAST_FIREBALL = 26 -CAST_ICEBALL = 27 -PLACE_TORCH = 28 -MAKE_TORCH = 38 +NUM_BLOCK_TYPES = 37 +NUM_ITEM_TYPES = 5 +INVENTORY_OBS_SIZE = 51 +OBS_TILE_CHANNELS = 3 + NUM_MOB_CLASSES +OBS_SIZE = OBS_ROWS * OBS_COLS * OBS_TILE_CHANNELS + INVENTORY_OBS_SIZE +JAX_TILE_CHANNELS = ( + NUM_BLOCK_TYPES + NUM_ITEM_TYPES + NUM_MOB_CLASSES * NUM_MOB_TYPES + 1 +) +JAX_MAP_OBS = OBS_ROWS * OBS_COLS * JAX_TILE_CHANNELS +JAX_OBS_SIZE = JAX_MAP_OBS + INVENTORY_OBS_SIZE +NUM_ACTIONS = 43 + +DUNGEON_FLOORS = (1, 3, 4) +SMOOTH_FLOORS = (0, 2, 5, 6, 7, 8) +BOSS_FLOOR = 8 +FIRE_FLOOR = 6 BLOCK_WATER = 3 +BLOCK_STONE = 4 +BLOCK_TREE = 5 +BLOCK_PATH = 7 BLOCK_LAVA = 14 +BLOCK_DARKNESS = 18 +BLOCK_CHEST = 23 +BLOCK_FOUNTAIN = 24 +BLOCK_FIRE_GRASS = 25 +BLOCK_FIRE_TREE = 28 +BLOCK_ENCHANTMENT_TABLE_FIRE = 30 +BLOCK_ENCHANTMENT_TABLE_ICE = 31 +BLOCK_NECROMANCER = 32 +BLOCK_GRAVE = 33 + +ITEM_TORCH = 1 ITEM_LADDER_DOWN = 2 - -MOVE_ACTIONS = np.asarray([LEFT, RIGHT, UP, DOWN], dtype=np.int32) -DIRS = { - LEFT: (0, -1), - RIGHT: (0, 1), - UP: (-1, 0), - DOWN: (1, 0), -} - -SOLID_BLOCKS = frozenset( - [ - 4, - 5, - 8, - 9, - 10, - 11, - 12, - 15, - 16, - 17, - 19, - 20, - 21, - 22, - 23, - 24, - 28, - 30, - 31, - 32, - 33, - 34, - 35, +ITEM_LADDER_UP = 3 + + +class WorldDump(ctypes.Structure): + _fields_ = [ + ("map", ctypes.c_int32 * MAP_CELLS), + ("item_map", ctypes.c_int32 * MAP_CELLS), + ("light_map", ctypes.c_uint8 * MAP_CELLS), + ("down_ladders", ctypes.c_int32 * (NUM_LEVELS * 2)), + ("up_ladders", ctypes.c_int32 * (NUM_LEVELS * 2)), + ("monsters_killed", ctypes.c_int32 * NUM_LEVELS), + ("potion_mapping", ctypes.c_int32 * NUM_POTIONS), + ("player_position", ctypes.c_int32 * 2), + ("player_level", ctypes.c_int32), + ("player_direction", ctypes.c_int32), + ("player_health", ctypes.c_float), + ("player_food", ctypes.c_int32), + ("player_drink", ctypes.c_int32), + ("player_energy", ctypes.c_int32), + ("player_mana", ctypes.c_int32), + ("player_dexterity", ctypes.c_int32), + ("player_strength", ctypes.c_int32), + ("player_intelligence", ctypes.c_int32), + ("light_level", ctypes.c_float), + ("boss_timesteps", ctypes.c_int32), ] -) - -INVENTORY_OBS_NAMES = [ - "inventory.wood", - "inventory.stone", - "inventory.coal", - "inventory.iron", - "inventory.diamond", - "inventory.sapphire", - "inventory.ruby", - "inventory.sapling", - "inventory.torches", - "inventory.arrows", - "inventory.books", - "inventory.pickaxe", - "inventory.sword", - "sword_enchantment", - "bow_enchantment", - "inventory.bow", - "inventory.potions.red", - "inventory.potions.green", - "inventory.potions.blue", - "inventory.potions.pink", - "inventory.potions.cyan", - "inventory.potions.yellow", - "player_health", - "player_food", - "player_drink", - "player_energy", - "player_mana", - "player_xp", - "player_dexterity", - "player_strength", - "player_intelligence", - "direction.left", - "direction.right", - "direction.up", - "direction.down", - "inventory.armour.0", - "inventory.armour.1", - "inventory.armour.2", - "inventory.armour.3", - "armour_enchantments.0", - "armour_enchantments.1", - "armour_enchantments.2", - "armour_enchantments.3", - "light_level", - "is_sleeping", - "is_resting", - "learned_spells.fireball", - "learned_spells.iceball", - "player_level", - "ladder_down_open", - "boss_vulnerable", -] - -MOB_CLASS_NAMES = [ - "melee_mobs", - "passive_mobs", - "ranged_mobs", - "mob_projectiles", - "player_projectiles", -] - -POLICIES = ("uniform", "combat", "descend", "suicide", "boss", "mixed") -MIXED_ORDER = ("uniform", "combat", "descend", "suicide", "boss") - - -def _preload_nccl(): - root = Path(__file__).resolve().parents[1] - nccl = root / ".venv/lib/python3.12/site-packages/nvidia/nccl/lib/libnccl.so.2" - if nccl.exists(): - ctypes.CDLL(str(nccl), mode=ctypes.RTLD_GLOBAL) - - -def import_c_env(): - _preload_nccl() - import pufferlib._C as cmod - - env_name = getattr(cmod, "env_name", None) - if env_name != "craftax": - raise RuntimeError( - f"pufferlib._C is compiled for {env_name!r}, expected 'craftax'. " - "Run: uv run --with pybind11 --with rich_argparse ./build.sh craftax" - ) - return cmod - - -def float_view(ptr, count): - array_t = ctypes.c_float * count - return np.ctypeslib.as_array(array_t.from_address(ptr)) -def _stack_states(states): - return jax.tree_util.tree_map(lambda *xs: jnp.stack(xs), *states) - +SOURCE = r""" +#include +#include +#include "ocean/craftax/craftax.h" + +static void flatten_clean(const State* state, WorldDump* out) { + int cell = 0; + for (int level = 0; level < NUM_LEVELS; level++) { + for (int row = 0; row < MAP_SIZE; row++) { + for (int col = 0; col < MAP_SIZE; col++) { + out->map[cell] = state->map[level][row][col]; + out->item_map[cell] = state->item_map[level][row][col]; + out->light_map[cell] = state->light_map[level][row][col]; + cell++; + } + } + out->down_ladders[level * 2 + 0] = state->down_ladders[level][0]; + out->down_ladders[level * 2 + 1] = state->down_ladders[level][1]; + out->up_ladders[level * 2 + 0] = state->up_ladders[level][0]; + out->up_ladders[level * 2 + 1] = state->up_ladders[level][1]; + out->monsters_killed[level] = state->monsters_killed[level]; + } + memcpy(out->potion_mapping, state->potion_mapping, sizeof(out->potion_mapping)); + out->player_position[0] = state->player_position[0]; + out->player_position[1] = state->player_position[1]; + out->player_level = state->player_level; + out->player_direction = state->player_direction; + out->player_health = state->player_health; + out->player_food = state->player_food; + out->player_drink = state->player_drink; + out->player_energy = state->player_energy; + out->player_mana = state->player_mana; + out->player_dexterity = state->player_dexterity; + out->player_strength = state->player_strength; + out->player_intelligence = state->player_intelligence; + out->light_level = state->light_level; + out->boss_timesteps = state->boss_timestep_to_spawn_this_round; +} -class JaxCraftaxBatch: - def __init__(self, seeds, resetter=None): +void generate_clean_world(int32_t seed, WorldDump* out) { + State state; + Rng initial = rng_seed((uint32_t)seed); + Rng env_rng; + Rng reset_key; + rng_split(initial, &env_rng, &reset_key); + Rng unused; + Rng world_key; + rng_split(reset_key, &unused, &world_key); + generate_world_from_key(&state, world_key); + flatten_clean(&state, out); +} +""" + +CLEAN_REPLAY = r""" +#include +#include +#include "ocean/craftax/craftax.h" + +void replay_clean( + int32_t seed, + const int32_t* actions, + int32_t num_actions, + float* obs_out, + float* rewards_out, + int32_t* terminal_step +) { + Craftax env; + float action_value = 0.0f; + float reward_value = 0.0f; + float terminal_value = 0.0f; + float live_obs[OBS_SIZE]; + memset(&env, 0, sizeof(env)); + env.num_agents = 1; + env.rng = (unsigned int)seed; + env.seed = (uint64_t)(uint32_t)seed; + env.agents[0].actions = &action_value; + env.agents[0].rewards = &reward_value; + env.agents[0].terminals = &terminal_value; + env.agents[0].observations = live_obs; + + puf_reset(&env); + memcpy(obs_out, live_obs, OBS_SIZE * sizeof(float)); + *terminal_step = -1; + for (int32_t i = 0; i < num_actions; i++) { + action_value = (float)actions[i]; + puf_step(&env); + memcpy(obs_out + (size_t)(i + 1) * OBS_SIZE, live_obs, OBS_SIZE * sizeof(float)); + rewards_out[i] = env.agents[0].rewards[0]; + if (env.agents[0].terminals[0] > 0.5f) { + *terminal_step = i; + break; + } + } +} +""" + + +def _fill_c_array(c_arr, values, dtype): + flat = np.ascontiguousarray(values, dtype=dtype).reshape(-1) + if flat.size != len(c_arr): + raise ValueError(f"size mismatch: {flat.size} vs {len(c_arr)}") + ctypes.memmove(ctypes.addressof(c_arr), flat.ctypes.data, flat.nbytes) + + +def jax_state_to_dump(state) -> WorldDump: + dump = WorldDump() + _fill_c_array(dump.map, state.map, np.int32) + _fill_c_array(dump.item_map, state.item_map, np.int32) + lights = np.clip(np.asarray(state.light_map) * 255.0, 0, 255).astype(np.uint8) + _fill_c_array(dump.light_map, lights, np.uint8) + _fill_c_array(dump.down_ladders, state.down_ladders, np.int32) + _fill_c_array(dump.up_ladders, state.up_ladders, np.int32) + _fill_c_array(dump.monsters_killed, state.monsters_killed, np.int32) + _fill_c_array(dump.potion_mapping, state.potion_mapping, np.int32) + pos = np.asarray(state.player_position, dtype=np.int32).reshape(-1) + dump.player_position[0] = int(pos[0]) + dump.player_position[1] = int(pos[1]) + dump.player_level = int(np.asarray(state.player_level)) + dump.player_direction = int(np.asarray(state.player_direction)) + dump.player_health = float(np.asarray(state.player_health)) + dump.player_food = int(np.asarray(state.player_food)) + dump.player_drink = int(np.asarray(state.player_drink)) + dump.player_energy = int(np.asarray(state.player_energy)) + dump.player_mana = int(np.asarray(state.player_mana)) + dump.player_dexterity = int(np.asarray(state.player_dexterity)) + dump.player_strength = int(np.asarray(state.player_strength)) + dump.player_intelligence = int(np.asarray(state.player_intelligence)) + dump.light_level = float(np.asarray(state.light_level)) + dump.boss_timesteps = int(np.asarray(state.boss_timesteps_to_spawn_this_round)) + return dump + + +def pack_symbolic_obs(obs) -> np.ndarray: + """Convert JAX 8268-d one-hot symbolic obs to packed 9x11x8+51 obs.""" + obs = np.asarray(obs, dtype=np.float32).reshape(-1) + if obs.size != JAX_OBS_SIZE: + raise ValueError(f"JAX obs size {obs.size}, expected {JAX_OBS_SIZE}") + tiled = obs[:JAX_MAP_OBS].reshape(OBS_ROWS, OBS_COLS, JAX_TILE_CHANNELS) + blocks = tiled[..., :NUM_BLOCK_TYPES] + items = tiled[..., NUM_BLOCK_TYPES : NUM_BLOCK_TYPES + NUM_ITEM_TYPES] + mobs = tiled[ + ..., + NUM_BLOCK_TYPES + NUM_ITEM_TYPES : NUM_BLOCK_TYPES + + NUM_ITEM_TYPES + + NUM_MOB_CLASSES * NUM_MOB_TYPES, + ].reshape(OBS_ROWS, OBS_COLS, NUM_MOB_CLASSES, NUM_MOB_TYPES) + visible = tiled[..., -1] + packed = np.zeros((OBS_ROWS, OBS_COLS, OBS_TILE_CHANNELS), dtype=np.float32) + packed[..., 0] = np.argmax(blocks, axis=-1).astype(np.float32) * visible + packed[..., 1] = (np.argmax(items, axis=-1).astype(np.float32) + 1.0) * visible + packed[..., 2] = visible + present = mobs.max(axis=-1) + packed[..., 3:] = (np.argmax(mobs, axis=-1).astype(np.float32) + 1.0) * present + return np.concatenate([packed.reshape(-1), obs[JAX_MAP_OBS:]], axis=0) + + +class JaxCraftax: + def __init__(self): self.env = make_craftax_env_from_name("Craftax-Symbolic-v1", auto_reset=False) - self.params = self.env.default_params - self.num_envs = len(seeds) - self.resetter = resetter - self.reset_keys = [] - rngs = [] - states = [] - obs = [] - for seed in seeds: - rng = jax.random.PRNGKey(int(seed)) - rng, reset_key = jax.random.split(rng) - env_obs, state = self.env.reset(reset_key, self.params) - rngs.append(rng) - self.reset_keys.append(np.asarray(reset_key, dtype=np.uint32)) - states.append(state) - obs.append(np.asarray(env_obs, dtype=np.float32).reshape(-1)) - - self.rngs = jnp.stack(rngs) - self.states = _stack_states(states) - self.obs = np.stack(obs, axis=0) - self._step_batch = self._make_step_batch() - - def _make_step_batch(self): - env = self.env - params = self.params - - def step_one(key, state, action): - step_rng, reset_key = jax.random.split(key, 2) - obs, next_state, reward, done, _info = env.step( - step_rng, - state, - action, - params, - ) - return obs, next_state, reward, done, reset_key - - def step_batch(rngs, states, actions): - split_keys = jax.vmap(lambda key: jax.random.split(key, 2))(rngs) - next_rngs = split_keys[:, 0] - step_keys = split_keys[:, 1] - obs, next_states, rewards, dones, reset_keys = jax.vmap(step_one)( - step_keys, states, actions + if not isinstance(self.env, CraftaxSymbolicEnvNoAutoReset): + raise TypeError( + f"expected Craftax-Symbolic-v1, got {type(self.env).__name__}" ) - return next_rngs, next_states, obs, rewards, dones, reset_keys - - return jax.jit(step_batch) - - def step(self, actions): - actions = jnp.asarray(actions, dtype=jnp.int32) - ( - self.rngs, - self.states, - obs, - rewards, - dones, - reset_keys, - ) = self._step_batch(self.rngs, self.states, actions) - self.obs = np.asarray(obs, dtype=np.float32).reshape(self.num_envs, -1).copy() - dones_np = np.asarray(dones, dtype=np.bool_) - reset_keys_np = np.asarray(reset_keys, dtype=np.uint32) - if self.resetter is not None and np.any(dones_np): - for env_i, done in enumerate(dones_np): - if not bool(done): - continue - reset_state, reset_obs = self.resetter.reset( - reset_keys_np[env_i], - self.state_at(env_i), + self.params = self.env.default_params + self._step = self.env.step + + def reset(self, seed: int): + rng = jax.random.PRNGKey(int(seed)) + rng, reset_key = jax.random.split(rng) + obs, state = self.env.reset(reset_key, self.params) + return rng, obs, state + + def replay(self, seed: int, actions): + rng, obs, state = self.reset(seed) + packed = [pack_symbolic_obs(obs)] + terminal_step = -1 + for i, action in enumerate(actions): + current = int(action) + while True: + rng, step_key = jax.random.split(rng) + step_rng, reset_key = jax.random.split(step_key) + obs, state, _reward, done, _info = self._step( + step_rng, state, jnp.int32(current), self.params ) - self.states = jax.tree_util.tree_map( - lambda batched, value: batched.at[env_i].set(value), - self.states, - reset_state, + done = bool(np.asarray(done)) + sleeping = bool(np.asarray(state.is_sleeping)) or bool( + np.asarray(state.is_resting) ) - self.obs[env_i] = reset_obs - return ( - self.obs, - np.asarray(rewards, dtype=np.float32), - dones_np, - reset_keys_np, - ) - - def state_at(self, env_i): - return jax.tree_util.tree_map(lambda leaf: leaf[env_i], self.states) - - -class PolicySnapshot: - def __init__(self, states): - self.level = np.asarray(states.player_level, dtype=np.int32) - self.position = np.asarray(states.player_position, dtype=np.int32) - self.direction = np.asarray(states.player_direction, dtype=np.int32) - self.health = np.asarray(states.player_health, dtype=np.float32) - self.mana = np.asarray(states.player_mana, dtype=np.int32) - self.learned_spells = np.asarray(states.learned_spells, dtype=np.bool_) - - self.inventory = states.inventory - self.wood = np.asarray(self.inventory.wood, dtype=np.int32) - self.stone = np.asarray(self.inventory.stone, dtype=np.int32) - self.coal = np.asarray(self.inventory.coal, dtype=np.int32) - self.iron = np.asarray(self.inventory.iron, dtype=np.int32) - self.diamond = np.asarray(self.inventory.diamond, dtype=np.int32) - self.bow = np.asarray(self.inventory.bow, dtype=np.int32) - self.arrows = np.asarray(self.inventory.arrows, dtype=np.int32) - self.torches = np.asarray(self.inventory.torches, dtype=np.int32) - - num_envs = int(self.level.shape[0]) - env_idx = np.arange(num_envs) - - full_map = np.asarray(states.map, dtype=np.int32) - full_item_map = np.asarray(states.item_map, dtype=np.int32) - full_mob_map = np.asarray(states.mob_map, dtype=np.bool_) - full_monsters_killed = np.asarray(states.monsters_killed, dtype=np.int32) - full_down_ladders = np.asarray(states.down_ladders, dtype=np.int32) - - self.map = full_map[env_idx, self.level] - self.item_map = full_item_map[env_idx, self.level] - self.mob_map = full_mob_map[env_idx, self.level] - self.monsters_killed = full_monsters_killed[env_idx, self.level] - self.down_ladders = full_down_ladders[env_idx, self.level] - - self.melee_pos, self.melee_mask, self.melee_type = self._take_mobs( - states.melee_mobs, env_idx - ) - self.passive_pos, self.passive_mask, self.passive_type = self._take_mobs( - states.passive_mobs, env_idx - ) - self.ranged_pos, self.ranged_mask, self.ranged_type = self._take_mobs( - states.ranged_mobs, env_idx - ) - ( - self.mob_projectile_pos, - self.mob_projectile_mask, - self.mob_projectile_type, - ) = self._take_mobs(states.mob_projectiles, env_idx) - ( - self.player_projectile_pos, - self.player_projectile_mask, - self.player_projectile_type, - ) = self._take_mobs(states.player_projectiles, env_idx) - - def _take_mobs(self, mobs, env_idx): - pos = np.asarray(mobs.position, dtype=np.int32)[env_idx, self.level] - mask = np.asarray(mobs.mask, dtype=np.bool_)[env_idx, self.level] - type_id = np.asarray(mobs.type_id, dtype=np.int32)[env_idx, self.level] - return pos, mask, type_id - - -class ResetVerifier: - def __init__(self): - root = Path(__file__).resolve().parents[1] - source = r""" - #include - #include - #define CRAFTAX_ENABLE_ENV_IMPL - #include "ocean/craftax/craftax.h" - #include "ocean/craftax/step_crafting.h" - #include "ocean/craftax/step_update_mobs.h" - #include "ocean/craftax/step_spawn_mobs.h" - - void reset_from_key( - uint32_t key0, - uint32_t key1, - CraftaxState* out, - float* obs - ) { - CraftaxThreefryKey reset_key = {{key0, key1}}; - craftax_reset_state_from_reset_key(out, reset_key); - craftax_encode_native_observation(out, obs); - } - """ - self._tmp = tempfile.TemporaryDirectory() - tmp_path = Path(self._tmp.name) - src = tmp_path / "craftax_reset_verify.c" - so = tmp_path / "craftax_reset_verify.so" - src.write_text(source) - subprocess.run( - [ - "cc", - "-std=c99", - "-O2", - "-shared", - "-fPIC", - "-I", - str(root), - "-I", - str(root / "raylib-5.5_linux_amd64/include"), - str(src), - "-lm", - "-o", - str(so), - ], - check=True, - cwd=root, - ) - self.lib = ctypes.CDLL(str(so)) - self.lib.reset_from_key.argtypes = [ - ctypes.c_uint32, - ctypes.c_uint32, - ctypes.POINTER(CraftaxState), - ctypes.POINTER(ctypes.c_float), + if done or not sleeping: + break + current = 0 + if done: + # puf_step generates a new world from this tick's reset_key. + obs, state = self.env.reset(reset_key, self.params) + packed.append(pack_symbolic_obs(obs)) + terminal_step = i + break + packed.append(pack_symbolic_obs(obs)) + return packed, terminal_step + + +def compile_lib(root: Path) -> ctypes.CDLL: + tmp = tempfile.TemporaryDirectory(prefix="craftax_parity_") + src = Path(tmp.name) / "parity.c" + so = Path(tmp.name) / "parity.so" + compile_lib._tmp = tmp # type: ignore[attr-defined] + c_struct = f""" +#include +typedef struct WorldDump {{ + int32_t map[{MAP_CELLS}]; + int32_t item_map[{MAP_CELLS}]; + uint8_t light_map[{MAP_CELLS}]; + int32_t down_ladders[{NUM_LEVELS * 2}]; + int32_t up_ladders[{NUM_LEVELS * 2}]; + int32_t monsters_killed[{NUM_LEVELS}]; + int32_t potion_mapping[{NUM_POTIONS}]; + 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; + int32_t player_dexterity; + int32_t player_strength; + int32_t player_intelligence; + float light_level; + int32_t boss_timesteps; +}} WorldDump; +""" + src.write_text(c_struct + SOURCE) + subprocess.run( + [ + "cc", + "-std=c99", + "-O2", + "-shared", + "-fPIC", + "-I", + str(root), + "-I", + str(root / "src"), + "-I", + str(root / "ocean" / "craftax"), + "-I", + str(root / "raylib-5.5_linux_amd64/include"), + str(src), + str(root / "raylib-5.5_linux_amd64/lib/libraylib.a"), + "-lm", + "-lpthread", + "-lGL", + "-ldl", + "-o", + str(so), + ], + check=True, + cwd=root, + ) + lib = ctypes.CDLL(str(so)) + lib.generate_clean_world.argtypes = [ctypes.c_int32, ctypes.POINTER(WorldDump)] + lib.generate_clean_world.restype = None + return lib + + +def compile_replay_lib(root: Path) -> ctypes.CDLL: + tmp = tempfile.TemporaryDirectory(prefix="craftax_replay_") + compile_replay_lib._tmp = tmp # type: ignore[attr-defined] + src = Path(tmp.name) / "replay_clean.c" + so = Path(tmp.name) / "replay_clean.so" + src.write_text(CLEAN_REPLAY) + subprocess.run( + [ + "cc", + "-std=c99", + "-O2", + "-shared", + "-fPIC", + "-I", + str(root), + "-I", + str(root / "src"), + "-I", + str(root / "ocean" / "craftax"), + "-I", + str(root / "raylib-5.5_linux_amd64/include"), + str(src), + str(root / "raylib-5.5_linux_amd64/lib/libraylib.a"), + "-lm", + "-lpthread", + "-lGL", + "-ldl", + "-o", + str(so), + ], + check=True, + cwd=root, + ) + clean = ctypes.CDLL(str(so)) + clean.replay_clean.argtypes = [ + ctypes.c_int32, + ctypes.POINTER(ctypes.c_int32), + ctypes.c_int32, + ctypes.POINTER(ctypes.c_float), + ctypes.POINTER(ctypes.c_float), + ctypes.POINTER(ctypes.c_int32), + ] + clean.replay_clean.restype = None + return clean + + +def obs_section(index: int) -> str: + map_size = OBS_ROWS * OBS_COLS * OBS_TILE_CHANNELS + if index < map_size: + cell = index // OBS_TILE_CHANNELS + channel = index % OBS_TILE_CHANNELS + names = [ + "block", + "item", + "visible", + "melee", + "passive", + "ranged", + "mob_proj", + "player_proj", ] - self.lib.reset_from_key.restype = None - - def reset(self, reset_key, template): - c_state = CraftaxState() - c_obs = np.empty(OBS_SIZE, dtype=np.float32) - key = np.asarray(reset_key, dtype=np.uint32) - self.lib.reset_from_key( - ctypes.c_uint32(int(key[0])), - ctypes.c_uint32(int(key[1])), - ctypes.byref(c_state), - c_obs.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), - ) - return craftax_state_to_jax(c_state, template=template), c_obs - - def compare(self, jax_state, jax_obs, reset_key, seed, step, policy, atol): - c_jax_state, c_obs = self.reset(reset_key, jax_state) - - obs_diff = first_obs_diff(jax_obs, c_obs, atol) - state_diff = first_state_diff(jax_state, c_jax_state, atol) - if obs_diff is not None: - idx, max_diff, jax_value, c_value = obs_diff - key = np.asarray(reset_key, dtype=np.uint32) - print( - "RESET DIVERGENCE " - f"seed={seed} step={step} policy={policy} " - f"reset_key=[{int(key[0])},{int(key[1])}] " - f"obs_index={idx} section={section_for_index(idx)} " - f"subsystem={subsystem_for_section(section_for_index(idx))} " - f"abs_diff={max_diff:.8g} jax={jax_value:.8g} c={c_value:.8g}" - ) - if state_diff is not None: - name, index, state_max_diff, state_jax_value, state_c_value = state_diff - print( - "reset_state_first_diff: " - f"field={name} index={index} " - f"abs_diff={state_max_diff:.8g} " - f"jax={state_jax_value} c={state_c_value}" - ) - return False - - if state_diff is not None: - name, index, max_diff, jax_value, c_value = state_diff - key = np.asarray(reset_key, dtype=np.uint32) - print( - "RESET STATE DIVERGENCE " - f"seed={seed} step={step} policy={policy} " - f"reset_key=[{int(key[0])},{int(key[1])}] " - f"field={name} index={index} abs_diff={max_diff:.8g} " - f"jax={jax_value} c={c_value}" - ) - return False - return True + name = names[channel] if channel < len(names) else str(channel) + return f"map cell={cell} channel={name}" + return f"scalar[{index - map_size}]" -_RESET_VERIFIER = None - - -def get_reset_verifier(enabled): - global _RESET_VERIFIER - if not enabled: - return None - if _RESET_VERIFIER is None: - _RESET_VERIFIER = ResetVerifier() - return _RESET_VERIFIER - - -def make_c_vec(cmod, num_envs, seed_offset, num_threads=1): - args = { - "vec": { - "total_agents": num_envs, - "num_buffers": 1, - "num_threads": num_threads, - }, - "env": { - "seed_offset": seed_offset, - }, - } - vec = cmod.create_vec(args, 0) - if vec.obs_size != OBS_SIZE: - raise RuntimeError(f"C obs_size={vec.obs_size}, expected {OBS_SIZE}") - if vec.num_atns != 1: - raise RuntimeError(f"C num_atns={vec.num_atns}, expected 1") - if list(vec.act_sizes) != [NUM_ACTIONS]: - raise RuntimeError(f"C act_sizes={vec.act_sizes}, expected [{NUM_ACTIONS}]") - vec.reset() - obs = float_view(vec.obs_ptr, num_envs * OBS_SIZE).reshape(num_envs, OBS_SIZE) - rewards = float_view(vec.rewards_ptr, num_envs) - terminals = float_view(vec.terminals_ptr, num_envs) - return vec, obs, rewards, terminals - - -def action_plan(seeds, steps, action_seed): - rng = np.random.default_rng(action_seed) - return rng.integers(0, NUM_ACTIONS, size=(steps, len(seeds)), dtype=np.int32) - - -def first_obs_diff(ref, got, atol): - diff = np.abs(ref - got) - idx = int(np.argmax(diff)) - max_diff = float(diff[idx]) +def first_obs_mismatch(clean, jax_obs, atol: float): + clean = np.asarray(clean, dtype=np.float32).reshape(-1) + jax_obs = np.asarray(jax_obs, dtype=np.float32).reshape(-1) + diff = np.abs(clean - jax_obs) + index = int(np.argmax(diff)) + max_diff = float(diff[index]) if max_diff <= atol: return None - return idx, max_diff, float(ref[idx]), float(got[idx]) - - -def _format_index(index): - index = np.asarray(index) - if index.ndim == 0: - return "scalar" - return ",".join(str(int(i)) for i in index) - - -def first_state_diff(jax_state, c_state, atol): - jax_flat = flatten_env_state(jax_state) - c_flat = flatten_env_state(c_state) - if jax_flat.keys() != c_flat.keys(): - missing = sorted(jax_flat.keys() - c_flat.keys()) - extra = sorted(c_flat.keys() - jax_flat.keys()) - return "state_keys", "scalar", 1.0, f"missing_c={missing}", f"extra_c={extra}" - - for name, jax_value in jax_flat.items(): - c_value = c_flat[name] - if np.asarray(jax_value).dtype.kind == "f": - diff = np.abs(np.asarray(jax_value) - np.asarray(c_value)) - if diff.size == 0: - continue - idx = np.unravel_index(int(np.argmax(diff)), diff.shape) - max_diff = float(diff[idx]) - if max_diff > atol: - return ( - name, - _format_index(np.asarray(idx)), - max_diff, - float(np.asarray(jax_value)[idx]), - float(np.asarray(c_value)[idx]), - ) - else: - neq = np.asarray(jax_value) != np.asarray(c_value) - if np.any(neq): - idx = np.argwhere(neq)[0] if np.asarray(neq).ndim else np.asarray(()) - idx_tuple = tuple(int(i) for i in np.asarray(idx).reshape(-1)) - return ( - name, - _format_index(idx), - 1.0, - np.asarray(jax_value)[idx_tuple].item() - if idx_tuple - else np.asarray(jax_value).item(), - np.asarray(c_value)[idx_tuple].item() - if idx_tuple - else np.asarray(c_value).item(), - ) - return None - - -def section_for_index(idx): - if idx < MAP_OBS_SIZE: - tile = idx // NUM_TILE_CHANNELS - channel = idx % NUM_TILE_CHANNELS - row = tile // OBS_COLS - col = tile % OBS_COLS - if channel < NUM_BLOCK_TYPES: - return f"map_one_hot[row={row},col={col},block={channel}]" - channel -= NUM_BLOCK_TYPES - if channel < NUM_ITEM_TYPES: - return f"item_one_hot[row={row},col={col},item={channel}]" - channel -= NUM_ITEM_TYPES - if channel < NUM_MOB_CLASSES * NUM_MOB_TYPES: - mob_class = channel // NUM_MOB_TYPES - mob_type = channel % NUM_MOB_TYPES - return ( - f"{MOB_CLASS_NAMES[mob_class]}_type_{mob_type}" - f"[row={row},col={col}]" - ) - return f"light[row={row},col={col}]" - - inv_idx = idx - MAP_OBS_SIZE - if 0 <= inv_idx < len(INVENTORY_OBS_NAMES): - return INVENTORY_OBS_NAMES[inv_idx] - return f"inventory_or_special[{inv_idx}]" - - -def subsystem_for_section(section): - if section.startswith("map_one_hot"): - return "symbolic_observation.map" - if section.startswith("item_one_hot"): - return "symbolic_observation.item_or_ladder" - if section.startswith("melee_mobs") or section.startswith("passive_mobs"): - return "mobs.update_or_observation" - if section.startswith("ranged_mobs") or section.startswith("mob_projectiles"): - return "projectiles_or_ranged_mobs" - if section.startswith("player_projectiles"): - return "player_projectiles" - if section.startswith("light[") or section == "light_level": - return "light" - if section.startswith("inventory."): - return "inventory" - if section.startswith("player_"): - return "player_intrinsics" - if section.startswith("direction."): - return "movement" - if section in {"ladder_down_open", "player_level"}: - return "floor_change" - if section == "boss_vulnerable": - return "boss_logic" - return "state_or_observation" - - -def compare_reset(ref_obs, c_obs, seeds, atol): - for env_i, seed in enumerate(seeds): - diff = first_obs_diff(ref_obs[env_i], c_obs[env_i], atol) - if diff is not None: - idx, max_diff, ref_value, c_value = diff - section = section_for_index(idx) - print( - "RESET DIVERGENCE " - f"seed={seed} obs_index={idx} section={section} " - f"subsystem={subsystem_for_section(section)} " - f"abs_diff={max_diff:.8g} jax={ref_value:.8g} c={c_value:.8g}" - ) - return False - return True - - -def _in_bounds(pos): - return 0 <= int(pos[0]) < MAP_SIZE and 0 <= int(pos[1]) < MAP_SIZE - + return index, max_diff, float(clean[index]), float(jax_obs[index]) -def _action_toward_delta(delta): - dr, dc = int(delta[0]), int(delta[1]) - if abs(dr) > abs(dc): - return DOWN if dr > 0 else UP - if dc != 0: - return RIGHT if dc > 0 else LEFT - if dr != 0: - return DOWN if dr > 0 else UP - return NOOP - -def _action_to_neighbor(start, target): - delta = np.asarray(target, dtype=np.int32) - np.asarray(start, dtype=np.int32) - if abs(int(delta[0])) + abs(int(delta[1])) != 1: - return None - return _action_toward_delta(delta) - - -def _passable_map(snapshot, env_i, allow_danger=False, allow_mobs=False): - level_map = snapshot.map[env_i] - passable = np.ones((MAP_SIZE, MAP_SIZE), dtype=np.bool_) - for block in SOLID_BLOCKS: - passable &= level_map != block - if not allow_danger: - passable &= level_map != BLOCK_WATER - passable &= level_map != BLOCK_LAVA - if not allow_mobs: - passable &= ~snapshot.mob_map[env_i] - return passable - - -def _valid_move_actions(snapshot, env_i, allow_danger=False): - pos = snapshot.position[env_i] - passable = _passable_map(snapshot, env_i, allow_danger=allow_danger) - actions = [] - for action, delta in DIRS.items(): - target = pos + np.asarray(delta, dtype=np.int32) - if _in_bounds(target) and passable[int(target[0]), int(target[1])]: - actions.append(action) - return actions - - -def _random_move(snapshot, env_i, rng, allow_danger=False): - actions = _valid_move_actions(snapshot, env_i, allow_danger=allow_danger) - if actions: - return int(rng.choice(actions)) - return int(rng.choice(MOVE_ACTIONS)) - - -def _bfs_first_action(snapshot, env_i, target, rng, allow_danger=False): - start = tuple(int(x) for x in snapshot.position[env_i]) - target = tuple(int(x) for x in np.asarray(target, dtype=np.int32)) - if start == target: - return NOOP - - passable = _passable_map(snapshot, env_i, allow_danger=allow_danger) - passable[start] = True - if not _in_bounds(target) or not passable[target]: - return _greedy_action(snapshot, env_i, np.asarray(target), rng, allow_danger) - - visited = np.zeros((MAP_SIZE, MAP_SIZE), dtype=np.bool_) - visited[start] = True - queue = deque() - for action in rng.permutation(MOVE_ACTIONS): - delta = DIRS[int(action)] - row = start[0] + delta[0] - col = start[1] + delta[1] - if not (0 <= row < MAP_SIZE and 0 <= col < MAP_SIZE): - continue - if visited[row, col] or not passable[row, col]: - continue - if (row, col) == target: - return int(action) - visited[row, col] = True - queue.append((row, col, int(action))) - - while queue: - row, col, first_action = queue.popleft() - for action in MOVE_ACTIONS: - delta = DIRS[int(action)] - next_row = row + delta[0] - next_col = col + delta[1] - if not (0 <= next_row < MAP_SIZE and 0 <= next_col < MAP_SIZE): - continue - if visited[next_row, next_col] or not passable[next_row, next_col]: - continue - if (next_row, next_col) == target: - return int(first_action) - visited[next_row, next_col] = True - queue.append((next_row, next_col, first_action)) - - return _greedy_action(snapshot, env_i, np.asarray(target), rng, allow_danger) - - -def _greedy_action(snapshot, env_i, target, rng, allow_danger=False): - pos = snapshot.position[env_i] - actions = _valid_move_actions(snapshot, env_i, allow_danger=allow_danger) - if not actions: - return int(rng.choice(MOVE_ACTIONS)) - scored = [] - for action in actions: - delta = np.asarray(DIRS[action], dtype=np.int32) - next_pos = pos + delta - dist = int(np.abs(next_pos - target).sum()) - scored.append((dist, action)) - best_dist = min(dist for dist, _action in scored) - best = [action for dist, action in scored if dist == best_dist] - return int(rng.choice(best)) - - -def _nearest_target(snapshot, env_i, positions): - if len(positions) == 0: - return None - pos = snapshot.position[env_i] - positions = np.asarray(positions, dtype=np.int32) - distances = np.abs(positions - pos).sum(axis=1) - return positions[int(np.argmin(distances))] +def first_mismatch(left, right, name: str): + for index, (a, b) in enumerate(zip(left, right)): + if a != b: + return f"{name}[{index}] clean={a} jax={b}" + return None -def _live_mobs(snapshot, env_i, include_passive=True, include_projectiles=False): - groups = [ - (0, snapshot.melee_pos[env_i], snapshot.melee_mask[env_i], snapshot.melee_type[env_i]), - (2, snapshot.ranged_pos[env_i], snapshot.ranged_mask[env_i], snapshot.ranged_type[env_i]), - ] - if include_passive: - groups.append( - ( - 1, - snapshot.passive_pos[env_i], - snapshot.passive_mask[env_i], - snapshot.passive_type[env_i], +def compare_dumps(clean: WorldDump, jax_dump: WorldDump) -> list[str]: + diffs = [] + for name, _ctype in WorldDump._fields_: + left = getattr(clean, name) + right = getattr(jax_dump, name) + if hasattr(left, "__len__"): + mismatch = first_mismatch(left, right, name) + if mismatch: + diffs.append(mismatch) + elif left != right: + diffs.append(f"{name} clean={left} jax={right}") + return diffs + + +def count_block(dump: WorldDump, level: int, block: int) -> int: + start = level * MAP_SIZE * MAP_SIZE + end = start + MAP_SIZE * MAP_SIZE + return sum(1 for value in dump.map[start:end] if value == block) + + +def count_item(dump: WorldDump, level: int, item: int) -> int: + start = level * MAP_SIZE * MAP_SIZE + end = start + MAP_SIZE * MAP_SIZE + return sum(1 for value in dump.item_map[start:end] if value == item) + + +def min_light(dump: WorldDump, level: int) -> int: + start = level * MAP_SIZE * MAP_SIZE + end = start + MAP_SIZE * MAP_SIZE + return min(dump.light_map[start:end]) + + +def mean_light(dump: WorldDump, level: int) -> float: + start = level * MAP_SIZE * MAP_SIZE + end = start + MAP_SIZE * MAP_SIZE + values = dump.light_map[start:end] + return sum(values) / float(len(values)) + + +def check_structure(dump: WorldDump) -> list[str]: + errors = [] + if dump.player_position[0] != MAP_SIZE // 2 or dump.player_position[1] != MAP_SIZE // 2: + errors.append("player does not spawn at map center") + if dump.monsters_killed[0] != 10: + errors.append("overworld is not pre-cleared") + if dump.player_health != 9.0 or dump.player_food != 9: + errors.append("starter vitals are not 9") + + for level in DUNGEON_FLOORS: + if count_block(dump, level, BLOCK_DARKNESS) == 0: + errors.append(f"floor {level} is missing BLOCK_DARKNESS") + if count_block(dump, level, BLOCK_CHEST) < 6: + errors.append( + f"floor {level} has {count_block(dump, level, BLOCK_CHEST)} chests, expected at least 6" ) - ) - if include_projectiles: - groups.append( - ( - 3, - snapshot.mob_projectile_pos[env_i], - snapshot.mob_projectile_mask[env_i], - snapshot.mob_projectile_type[env_i], + if count_item(dump, level, ITEM_TORCH) < 16: + errors.append(f"floor {level} is missing room-corner torches") + if min_light(dump, level) != 255: + errors.append(f"floor {level} is not fully lit") + if count_item(dump, level, ITEM_LADDER_UP) != 1: + errors.append(f"floor {level} is missing an up ladder") + if count_item(dump, level, ITEM_LADDER_DOWN) != 1: + errors.append(f"floor {level} is missing a down ladder") + + if count_block(dump, 1, BLOCK_FOUNTAIN) == 0: + errors.append("dungeon is missing a fountain") + if count_block(dump, 3, BLOCK_ENCHANTMENT_TABLE_ICE) == 0: + errors.append("sewers are missing the ice enchantment table") + if count_block(dump, 4, BLOCK_ENCHANTMENT_TABLE_FIRE) == 0: + errors.append("vault is missing the fire enchantment table") + + if mean_light(dump, 0) < 250: + errors.append("overworld is not fully lit") + if mean_light(dump, FIRE_FLOOR) < 250: + errors.append("fire realm is not bright") + if mean_light(dump, BOSS_FLOOR) > 80: + errors.append("boss floor is not dark") + if count_block(dump, BOSS_FLOOR, BLOCK_NECROMANCER) == 0: + errors.append("boss floor is missing the necromancer") + if count_item(dump, BOSS_FLOOR, ITEM_LADDER_UP) != 0: + errors.append("boss floor should not have an up ladder") + if count_item(dump, BOSS_FLOOR, ITEM_LADDER_DOWN) != 0: + errors.append("boss floor should not have a down ladder") + if count_block(dump, FIRE_FLOOR, BLOCK_FIRE_GRASS) == 0: + errors.append("fire realm is missing fire grass") + return errors + + +def replay_seed(clean_lib, jax_env: JaxCraftax, seed: int, actions, atol: float): + num_actions = len(actions) + action_array = (ctypes.c_int32 * num_actions)(*actions) + obs_count = (num_actions + 1) * OBS_SIZE + clean_obs = (ctypes.c_float * obs_count)() + clean_rewards = (ctypes.c_float * num_actions)() + clean_done = ctypes.c_int32(-1) + + clean_lib.replay_clean( + seed, + action_array, + num_actions, + clean_obs, + clean_rewards, + ctypes.byref(clean_done), + ) + jax_obs, jax_done = jax_env.replay(seed, actions) + + compare_steps = min(len(jax_obs), num_actions + 1) + if clean_done.value >= 0 or jax_done >= 0: + compare_steps = min( + clean_done.value if clean_done.value >= 0 else num_actions, + jax_done if jax_done >= 0 else num_actions, + ) + 2 + compare_steps = min(compare_steps, len(jax_obs), num_actions + 1) + + for step in range(compare_steps): + start = step * OBS_SIZE + end = start + OBS_SIZE + mismatch = first_obs_mismatch(clean_obs[start:end], jax_obs[step], atol) + if mismatch is not None: + index, abs_diff, clean_value, jax_value = mismatch + return ( + f"obs mismatch seed={seed} step={step} " + f"index={index} section={obs_section(index)} " + f"abs_diff={abs_diff:.8g} clean={clean_value:.8g} jax={jax_value:.8g}" ) + if clean_done.value != jax_done: + return ( + f"terminal mismatch seed={seed} " + f"clean_done={clean_done.value} jax_done={jax_done}" ) - - mobs = [] - for mob_class, positions, masks, type_ids in groups: - for index, mask in enumerate(masks): - if bool(mask): - mobs.append((mob_class, index, positions[index], int(type_ids[index]))) - return mobs - - -def _mob_positions(snapshot, env_i, include_passive=True, include_projectiles=False): - return [ - np.asarray(position, dtype=np.int32) - for _cls, _idx, position, _type_id in _live_mobs( - snapshot, - env_i, - include_passive=include_passive, - include_projectiles=include_projectiles, - ) - ] - - -def _projectile_slot_available(snapshot, env_i): - return int(np.count_nonzero(snapshot.player_projectile_mask[env_i])) < 3 - - -def _target_in_current_line(snapshot, env_i, target): - pos = snapshot.position[env_i] - direction = int(snapshot.direction[env_i]) - delta = np.asarray(target, dtype=np.int32) - pos - if direction == LEFT: - return int(delta[0]) == 0 and int(delta[1]) < 0 - if direction == RIGHT: - return int(delta[0]) == 0 and int(delta[1]) > 0 - if direction == UP: - return int(delta[1]) == 0 and int(delta[0]) < 0 - if direction == DOWN: - return int(delta[1]) == 0 and int(delta[0]) > 0 - return False - - -def _combat_action(snapshot, env_i, rng): - pos = snapshot.position[env_i] - mobs = _live_mobs(snapshot, env_i, include_passive=True) - mob_positions = [mob[2] for mob in mobs] - adjacent = [ - np.asarray(position, dtype=np.int32) - for position in mob_positions - if int(np.abs(np.asarray(position) - pos).sum()) == 1 - ] - - for target in adjacent: - action = _action_to_neighbor(pos, target) - if action == int(snapshot.direction[env_i]) and rng.random() < 0.75: - return DO - if adjacent: - target = adjacent[int(rng.integers(0, len(adjacent)))] - return int(_action_to_neighbor(pos, target)) - - has_projectile_slot = _projectile_slot_available(snapshot, env_i) - projectile_actions = [] - if has_projectile_slot and int(snapshot.bow[env_i]) >= 1 and int(snapshot.arrows[env_i]) >= 1: - projectile_actions.append(SHOOT_ARROW) - if has_projectile_slot and int(snapshot.mana[env_i]) >= 2: - if bool(snapshot.learned_spells[env_i, 0]): - projectile_actions.append(CAST_FIREBALL) - if bool(snapshot.learned_spells[env_i, 1]): - projectile_actions.append(CAST_ICEBALL) - - if projectile_actions and mob_positions: - line_targets = [ - target - for target in mob_positions - if _target_in_current_line(snapshot, env_i, target) - ] - if line_targets and rng.random() < 0.8: - return int(rng.choice(projectile_actions)) - - axis_targets = [ - target - for target in mob_positions - if int(target[0]) == int(pos[0]) or int(target[1]) == int(pos[1]) - ] - if axis_targets: - target = _nearest_target(snapshot, env_i, axis_targets) - return _action_toward_delta(target - pos) - - if mob_positions: - target = _nearest_target(snapshot, env_i, mob_positions) - return _bfs_first_action(snapshot, env_i, target, rng) - - return _random_move(snapshot, env_i, rng) - - -def _craft_or_place_action(snapshot, env_i, rng): - options = [] - if int(snapshot.wood[env_i]) > 0: - options.extend([PLACE_TABLE, MAKE_WOOD_PICKAXE, MAKE_WOOD_SWORD]) - if int(snapshot.stone[env_i]) > 0: - options.append(PLACE_STONE) - if int(snapshot.stone[env_i]) >= 4: - options.append(PLACE_FURNACE) - if int(snapshot.stone[env_i]) > 0 and int(snapshot.wood[env_i]) > 0: - options.extend([MAKE_STONE_PICKAXE, MAKE_STONE_SWORD]) - if int(snapshot.iron[env_i]) > 0 and int(snapshot.wood[env_i]) > 0: - options.extend([MAKE_IRON_PICKAXE, MAKE_IRON_SWORD, MAKE_IRON_ARMOUR]) - if int(snapshot.diamond[env_i]) > 0 and int(snapshot.wood[env_i]) > 0: - options.extend([MAKE_DIAMOND_PICKAXE, MAKE_DIAMOND_SWORD, MAKE_DIAMOND_ARMOUR]) - if int(snapshot.wood[env_i]) > 0 and int(snapshot.stone[env_i]) > 0: - options.append(MAKE_ARROW) - if int(snapshot.coal[env_i]) > 0 and int(snapshot.wood[env_i]) > 0: - options.append(MAKE_TORCH) - if int(snapshot.torches[env_i]) > 0: - options.append(PLACE_TORCH) - if not options: - return None - return int(rng.choice(options)) - - -def _descend_action(snapshot, env_i, rng): - level = int(snapshot.level[env_i]) - pos = snapshot.position[env_i] - if level >= NUM_LEVELS - 1: - return _combat_action(snapshot, env_i, rng) - - row, col = int(pos[0]), int(pos[1]) - on_down_ladder = int(snapshot.item_map[env_i, row, col]) == ITEM_LADDER_DOWN - ladder_open = int(snapshot.monsters_killed[env_i]) >= MONSTERS_KILLED_TO_CLEAR_LEVEL - if on_down_ladder and ladder_open: - return DESCEND - - mobs = _mob_positions(snapshot, env_i, include_passive=False) - if not ladder_open and mobs: - return _combat_action(snapshot, env_i, rng) - - if rng.random() < 0.12: - craft_action = _craft_or_place_action(snapshot, env_i, rng) - if craft_action is not None: - return craft_action - - ladder = snapshot.down_ladders[env_i] - if ladder_open: - return _bfs_first_action(snapshot, env_i, ladder, rng) - - if mobs: - return _combat_action(snapshot, env_i, rng) - return _random_move(snapshot, env_i, rng) - - -def _danger_adjacent_action(snapshot, env_i, rng): - pos = snapshot.position[env_i] - level_map = snapshot.map[env_i] - dangerous_actions = [] - for action, delta in DIRS.items(): - target = pos + np.asarray(delta, dtype=np.int32) - if not _in_bounds(target): - continue - block = int(level_map[int(target[0]), int(target[1])]) - if block in (BLOCK_WATER, BLOCK_LAVA) or bool( - snapshot.mob_map[env_i, int(target[0]), int(target[1])] - ): - dangerous_actions.append(action) - if dangerous_actions: - return int(rng.choice(dangerous_actions)) return None -def _suicide_action(snapshot, env_i, rng): - adjacent = _danger_adjacent_action(snapshot, env_i, rng) - if adjacent is not None: - return adjacent - - hostile_positions = _mob_positions( - snapshot, env_i, include_passive=False, include_projectiles=True - ) - danger_blocks = np.argwhere( - (snapshot.map[env_i] == BLOCK_LAVA) | (snapshot.map[env_i] == BLOCK_WATER) - ) - - targets = [] - targets.extend(hostile_positions) - if danger_blocks.size: - targets.extend([danger_blocks[i] for i in range(danger_blocks.shape[0])]) - - target = _nearest_target(snapshot, env_i, targets) - if target is None: - return _random_move(snapshot, env_i, rng, allow_danger=True) - - if int(np.abs(target - snapshot.position[env_i]).sum()) == 1: - return _action_toward_delta(target - snapshot.position[env_i]) - - passable = _passable_map(snapshot, env_i, allow_danger=False) - adjacent_cells = [] - for delta in DIRS.values(): - cell = target + np.asarray(delta, dtype=np.int32) - if _in_bounds(cell) and passable[int(cell[0]), int(cell[1])]: - adjacent_cells.append(cell) - adjacent_target = _nearest_target(snapshot, env_i, adjacent_cells) - if adjacent_target is not None: - return _bfs_first_action(snapshot, env_i, adjacent_target, rng) - return _greedy_action(snapshot, env_i, target, rng, allow_danger=True) - - -def _boss_action(snapshot, env_i, rng, step): - if step < 1000: - return _descend_action(snapshot, env_i, rng) - level = int(snapshot.level[env_i]) - if level >= NUM_LEVELS - 1: - return _combat_action(snapshot, env_i, rng) - pos = snapshot.position[env_i] - on_down_ladder = int(snapshot.item_map[env_i, int(pos[0]), int(pos[1])]) == ITEM_LADDER_DOWN - ladder_open = int(snapshot.monsters_killed[env_i]) >= MONSTERS_KILLED_TO_CLEAR_LEVEL - if on_down_ladder and ladder_open: - return DESCEND - if rng.random() < 0.25: - return DESCEND - return _descend_action(snapshot, env_i, rng) - - -class ActionPolicy: - def __init__(self, policy, action_seed, num_envs): - if policy not in POLICIES: - raise ValueError(f"unknown policy {policy!r}") - self.policy = policy - self.rng = np.random.default_rng(action_seed) - self.num_envs = num_envs - - def effective_policy(self, step): - if self.policy != "mixed": - return self.policy - return MIXED_ORDER[(step // 500) % len(MIXED_ORDER)] - - def actions(self, step, ref): - policy = self.effective_policy(step) - if policy == "uniform": - return ( - self.rng.integers(0, NUM_ACTIONS, size=self.num_envs, dtype=np.int32), - policy, +def run(args: argparse.Namespace) -> int: + root = Path(__file__).resolve().parents[1] + print(f"Compiling craftax worldgen harness in {root}") + lib = compile_lib(root) + print("Compiling craftax replay harness") + clean_lib = compile_replay_lib(root) + print("Loading JAX Craftax-Symbolic-v1") + jax_env = JaxCraftax() + print(f"Reference env: {type(jax_env.env).__name__}") + + failures = 0 + for seed in range(args.seeds): + clean = WorldDump() + lib.generate_clean_world(seed, ctypes.byref(clean)) + _rng, _obs, jax_state = jax_env.reset(seed) + jax_dump = jax_state_to_dump(jax_state) + + diffs = compare_dumps(clean, jax_dump) + struct_errors = check_structure(clean) + jax_struct_errors = [f"jax {error}" for error in check_structure(jax_dump)] + rng = random.Random(args.action_seed + seed) + actions = [rng.randrange(NUM_ACTIONS) for _ in range(args.steps)] + obs_error = replay_seed(clean_lib, jax_env, seed, actions, args.atol) + + if diffs or struct_errors or jax_struct_errors or obs_error: + failures += 1 + print(f"FAIL seed={seed}") + for diff in diffs[:8]: + print(f" {diff}") + for error in struct_errors: + print(f" {error}") + for error in jax_struct_errors: + print(f" {error}") + if obs_error: + print(f" {obs_error}") + if not args.keep_going: + return 1 + else: + print( + f"PASS seed={seed} " + f"dungeon_dark={count_block(clean, 1, BLOCK_DARKNESS)} " + f"dungeon_chests={count_block(clean, 1, BLOCK_CHEST)} " + f"boss_light={mean_light(clean, BOSS_FLOOR):.1f} " + f"steps={args.steps}" ) - snapshot = PolicySnapshot(ref.states) - out = np.empty(self.num_envs, dtype=np.int32) - for env_i in range(self.num_envs): - if policy == "combat": - out[env_i] = _combat_action(snapshot, env_i, self.rng) - elif policy == "descend": - out[env_i] = _descend_action(snapshot, env_i, self.rng) - elif policy == "suicide": - out[env_i] = _suicide_action(snapshot, env_i, self.rng) - elif policy == "boss": - out[env_i] = _boss_action(snapshot, env_i, self.rng, step) - else: - raise AssertionError(policy) - return out, policy - - -def _print_step_divergence( - seed, - step, - action, - policy_name, - reward_diff, - ref_reward, - c_reward, - ref_done, - c_done, - obs_diff, - history, -): - terminal_delta = int(bool(c_done)) - int(bool(ref_done)) - print( - "STEP DIVERGENCE " - f"seed={seed} step={step} action={int(action)} policy={policy_name}" - ) - print( - f"reward_delta={reward_diff:.8g} " - f"reward: jax={float(ref_reward):.8g} c={float(c_reward):.8g}" - ) - print( - f"terminal_delta={terminal_delta} " - f"done: jax={bool(ref_done)} c={bool(c_done)}" - ) - if obs_diff is None: - print("obs: ok") - else: - idx, max_diff, ref_value, c_value = obs_diff - section = section_for_index(idx) + passed = args.seeds - failures + if failures: print( - "obs: " - f"index={idx} section={section} " - f"subsystem={subsystem_for_section(section)} " - f"abs_diff={max_diff:.8g} " - f"jax={ref_value:.8g} c={c_value:.8g}" + f"FAIL craftax vs JAX parity: " + f"{passed}/{args.seeds} passed, {failures}/{args.seeds} diverged" ) - print(f"last_10_actions={list(history)}") - - -def _print_terminal_reset_check( - reset_verifier, - ref, - ref_obs, - reset_key, - env_i, - seed, - step, - policy_name, - atol, -): - if reset_verifier is None: - return True - key = np.asarray(reset_key, dtype=np.uint32) - ok = reset_verifier.compare( - ref.state_at(env_i), - ref_obs[env_i], - reset_key, - int(seed), - step, - policy_name, - atol, - ) - if ok: - print( - "terminal_reset_reference: ok " - f"reset_key=[{int(key[0])},{int(key[1])}]" - ) - return ok - + return 1 -def _terminal_summary(seeds, terminal_counts, episode_length_sums): - total_terminals = int(np.sum(terminal_counts)) - per_seed = [] - for seed, count, length_sum in zip(seeds, terminal_counts, episode_length_sums): - if int(count) > 0: - mean_len = float(length_sum) / float(count) - per_seed.append(f"{int(seed)}:{int(count)}@{mean_len:.1f}") - else: - per_seed.append(f"{int(seed)}:0") - return total_terminals, " ".join(per_seed) - - -def _diagnose_isolated_replay(cmod, seed, actions, atol, num_threads, reset_verifier): + sample = WorldDump() + lib.generate_clean_world(0, ctypes.byref(sample)) print( - "isolated_replay: start " - f"seed={int(seed)} steps={len(actions)}" - ) - trace_path = Path("build") / f"craftax_repro_seed_{int(seed)}_steps_{len(actions)}.txt" - trace_path.parent.mkdir(exist_ok=True) - trace_path.write_text("\n".join(str(int(action)) for action in actions) + "\n") - print(f"isolated_replay_actions={trace_path}") - ref = JaxCraftaxBatch(np.asarray([seed], dtype=np.int64), resetter=reset_verifier) - vec, c_obs, c_rewards, c_terminals = make_c_vec( - cmod, - 1, - int(seed), - num_threads=num_threads, + "PASS craftax vs JAX Craftax: " + f"seeds={args.seeds} steps={args.steps} atol={args.atol} " + f"dungeon_chests={count_block(sample, 1, BLOCK_CHEST)} " + f"dungeon_darkness={count_block(sample, 1, BLOCK_DARKNESS)} " + f"sewer_ice_table={count_block(sample, 3, BLOCK_ENCHANTMENT_TABLE_ICE)} " + f"vault_fire_table={count_block(sample, 4, BLOCK_ENCHANTMENT_TABLE_FIRE)} " + f"fire_light={mean_light(sample, FIRE_FLOOR):.1f} " + f"boss_light={mean_light(sample, BOSS_FLOOR):.1f}" ) - try: - if not compare_reset(ref.obs, c_obs.copy(), np.asarray([seed]), atol): - print("isolated_replay: initial reset diverged") - return - action_buf = np.zeros((1, 1), dtype=np.float32) - for step, action in enumerate(actions): - action_buf[0, 0] = float(action) - ref_obs, ref_rewards, ref_dones, reset_keys = ref.step( - np.asarray([action], dtype=np.int32) - ) - vec.cpu_step(action_buf.ctypes.data) - c_obs_snapshot = c_obs.copy() - c_rewards_snapshot = c_rewards.copy() - c_dones_snapshot = c_terminals.copy().astype(bool) - reward_diff = abs(float(ref_rewards[0]) - float(c_rewards_snapshot[0])) - done_match = bool(ref_dones[0]) == bool(c_dones_snapshot[0]) - obs_diff = first_obs_diff(ref_obs[0], c_obs_snapshot[0], atol) - if reward_diff > atol or not done_match or obs_diff is not None: - print( - "isolated_replay: divergence " - f"step={step} action={int(action)} " - f"reward_delta={reward_diff:.8g} " - f"done_jax={bool(ref_dones[0])} " - f"done_c={bool(c_dones_snapshot[0])}" - ) - if obs_diff is not None: - idx, max_diff, ref_value, c_value = obs_diff - section = section_for_index(idx) - print( - "isolated_replay_obs: " - f"index={idx} section={section} " - f"subsystem={subsystem_for_section(section)} " - f"abs_diff={max_diff:.8g} " - f"jax={ref_value:.8g} c={c_value:.8g}" - ) - if bool(ref_dones[0]) and bool(c_dones_snapshot[0]): - _print_terminal_reset_check( - reset_verifier, - ref, - ref_obs, - reset_keys[0], - 0, - seed, - step, - "isolated_replay", - atol, - ) - return - print("isolated_replay: no divergence") - finally: - vec.close() - - -def run(args): - if args.seeds <= 0: - raise ValueError("--seeds must be positive") - if args.steps < 0: - raise ValueError("--steps must be non-negative") - - policy_name = getattr(args, "policy", "uniform") - if policy_name not in POLICIES: - raise ValueError(f"--policy must be one of {POLICIES}") - - num_threads = int(getattr(args, "num_threads", 1)) - if num_threads <= 0: - raise ValueError("--num-threads must be positive") - os.environ.setdefault("OMP_NUM_THREADS", str(num_threads)) - - reset_on_done = bool(getattr(args, "reset_on_done", True)) - seeds = np.arange(args.seed_start, args.seed_start + args.seeds, dtype=np.int64) - - cmod = import_c_env() - reset_verifier = get_reset_verifier(True) - ref = JaxCraftaxBatch(seeds, resetter=reset_verifier) - ref_obs = ref.obs - - vec, c_obs, c_rewards, c_terminals = make_c_vec( - cmod, len(seeds), int(seeds[0]), num_threads=num_threads - ) - try: - if not compare_reset(ref_obs, c_obs.copy(), seeds, args.atol): - return 1 - - if reset_verifier is not None: - for env_i, seed in enumerate(seeds): - if not reset_verifier.compare( - ref.state_at(env_i), - ref_obs[env_i], - ref.reset_keys[env_i], - int(seed), - "initial", - policy_name, - args.atol, - ): - return 1 - - policy = ActionPolicy(policy_name, args.action_seed, len(seeds)) - action_buf = np.zeros((len(seeds), 1), dtype=np.float32) - histories = [deque(maxlen=10) for _seed in seeds] - full_histories = [[] for _seed in seeds] - terminal_counts = np.zeros(len(seeds), dtype=np.int64) - episode_lengths = np.zeros(len(seeds), dtype=np.int64) - episode_length_sums = np.zeros(len(seeds), dtype=np.int64) - - for step in range(args.steps): - step_actions, effective_policy = policy.actions(step, ref) - action_buf[:, 0] = step_actions.astype(np.float32) - for env_i, action in enumerate(step_actions): - histories[env_i].append(int(action)) - full_histories[env_i].append(int(action)) - - ref_obs, ref_rewards, ref_dones, reset_keys = ref.step(step_actions) - vec.cpu_step(action_buf.ctypes.data) - - c_obs_snapshot = c_obs.copy() - c_rewards_snapshot = c_rewards.copy() - c_dones_snapshot = c_terminals.copy().astype(bool) - - for env_i, seed in enumerate(seeds): - reward_diff = abs(float(ref_rewards[env_i]) - float(c_rewards_snapshot[env_i])) - done_match = bool(ref_dones[env_i]) == bool(c_dones_snapshot[env_i]) - obs_diff = first_obs_diff(ref_obs[env_i], c_obs_snapshot[env_i], args.atol) - if reward_diff > args.atol or not done_match or obs_diff is not None: - _print_step_divergence( - seed=seed, - step=step, - action=step_actions[env_i], - policy_name=effective_policy, - reward_diff=reward_diff, - ref_reward=ref_rewards[env_i], - c_reward=c_rewards_snapshot[env_i], - ref_done=ref_dones[env_i], - c_done=c_dones_snapshot[env_i], - obs_diff=obs_diff, - history=histories[env_i], - ) - if bool(ref_dones[env_i]) and bool(c_dones_snapshot[env_i]): - _print_terminal_reset_check( - reset_verifier, - ref, - ref_obs, - reset_keys[env_i], - env_i, - seed, - step, - effective_policy, - args.atol, - ) - _diagnose_isolated_replay( - cmod, - int(seed), - full_histories[env_i], - args.atol, - num_threads, - reset_verifier, - ) - return 1 - - episode_lengths += 1 - done_any = np.logical_or(ref_dones, c_dones_snapshot) - if reset_on_done and np.any(done_any): - for env_i, is_done in enumerate(done_any): - if not bool(is_done): - continue - terminal_counts[env_i] += 1 - episode_length_sums[env_i] += episode_lengths[env_i] - if reset_verifier is not None: - if not reset_verifier.compare( - ref.state_at(env_i), - ref_obs[env_i], - reset_keys[env_i], - int(seeds[env_i]), - step, - effective_policy, - args.atol, - ): - return 1 - episode_lengths[env_i] = 0 - - total_terminals, per_seed_summary = _terminal_summary( - seeds, terminal_counts, episode_length_sums - ) - print( - f"PASS craftax parity: seeds={args.seeds} steps={args.steps} " - f"atol={args.atol:g} action_seed={args.action_seed}" - ) - print( - f"policy={policy_name} reset_on_done={reset_on_done} " - f"terminal_count={total_terminals} " - f"mean_episode_length_by_seed={per_seed_summary}" - ) - return 0 - finally: - vec.close() + return 0 -def main(): - parser = argparse.ArgumentParser() +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--seeds", type=int, default=16) - parser.add_argument("--seed-start", type=int, default=0) - parser.add_argument("--steps", type=int, default=1000) + parser.add_argument("--steps", type=int, default=200) parser.add_argument("--action-seed", type=int, default=0) parser.add_argument("--atol", type=float, default=1e-5) - parser.add_argument("--policy", choices=POLICIES, default="uniform") - parser.add_argument("--num-threads", type=int, default=1) - parser.set_defaults(reset_on_done=True) - parser.add_argument("--reset-on-done", dest="reset_on_done", action="store_true") - parser.add_argument("--no-reset-on-done", dest="reset_on_done", action="store_false") - raise SystemExit(run(parser.parse_args())) + parser.add_argument("--keep-going", action="store_true") + parser.add_argument("--verbose", action="store_true") + return run(parser.parse_args()) if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/tests/craftax_parity_stress.py b/tests/craftax_parity_stress.py deleted file mode 100644 index 52edfb17b2..0000000000 --- a/tests/craftax_parity_stress.py +++ /dev/null @@ -1,96 +0,0 @@ -import argparse -import os -import time -from types import SimpleNamespace - -from craftax_parity import run - - -STRESS_CASES = [ - { - "name": "mixed-wide", - "seeds": 64, - "steps": 10000, - "policy": "mixed", - "action_seed": 0, - }, - { - "name": "descend-boss-target", - "seeds": 16, - "steps": 30000, - "policy": "descend", - "action_seed": 1, - }, - { - "name": "suicide-terminal-target", - "seeds": 32, - "steps": 5000, - "policy": "suicide", - "action_seed": 2, - }, - { - "name": "combat-projectile-xp", - "seeds": 16, - "steps": 5000, - "policy": "combat", - "action_seed": 3, - }, -] - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--atol", type=float, default=1e-5) - parser.add_argument("--seed-start", type=int, default=0) - parser.add_argument( - "--num-threads", - type=int, - default=max(1, min(16, os.cpu_count() or 1)), - ) - args = parser.parse_args() - - started = time.monotonic() - for case in STRESS_CASES: - case_started = time.monotonic() - print( - "RUN craftax parity stress " - f"name={case['name']} seeds={case['seeds']} steps={case['steps']} " - f"policy={case['policy']} action_seed={case['action_seed']} " - f"atol={args.atol:g}", - flush=True, - ) - status = run( - SimpleNamespace( - seeds=case["seeds"], - seed_start=args.seed_start, - steps=case["steps"], - action_seed=case["action_seed"], - atol=args.atol, - policy=case["policy"], - reset_on_done=True, - num_threads=args.num_threads, - ) - ) - elapsed = time.monotonic() - case_started - if status != 0: - print( - "FAIL craftax parity stress " - f"name={case['name']} elapsed={elapsed:.1f}s", - flush=True, - ) - raise SystemExit(status) - print( - "PASS craftax parity stress case " - f"name={case['name']} elapsed={elapsed:.1f}s", - flush=True, - ) - - elapsed = time.monotonic() - started - print( - f"PASS craftax parity stress: cases={len(STRESS_CASES)} elapsed={elapsed:.1f}s", - flush=True, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/craftax_state_fixtures.py b/tests/craftax_state_fixtures.py deleted file mode 100644 index 3639965c8a..0000000000 --- a/tests/craftax_state_fixtures.py +++ /dev/null @@ -1,620 +0,0 @@ -import ctypes -import os -import pickle - -os.environ.setdefault("JAX_PLATFORM_NAME", "cpu") -os.environ.setdefault("XLA_PYTHON_CLIENT_PREALLOCATE", "false") - -import jax.numpy as jnp -import numpy as np - -from craftax.craftax.craftax_state import EnvState, Inventory, Mobs - - -LEVELS = 9 -MAP_SIZE = 48 -ACHIEVEMENTS = 67 -MAX_MELEE_MOBS = 3 -MAX_PASSIVE_MOBS = 3 -MAX_RANGED_MOBS = 2 -MAX_MOB_PROJECTILES = 3 -MAX_PLAYER_PROJECTILES = 3 -MAX_GROWING_PLANTS = 10 - - -def _c_array(ctype, *shape): - array_type = ctype - for size in reversed(shape): - array_type = array_type * size - return array_type - - -class CraftaxInventory(ctypes.Structure): - _fields_ = [ - ("wood", ctypes.c_int32), - ("stone", ctypes.c_int32), - ("coal", ctypes.c_int32), - ("iron", ctypes.c_int32), - ("diamond", ctypes.c_int32), - ("sapling", ctypes.c_int32), - ("pickaxe", ctypes.c_int32), - ("sword", ctypes.c_int32), - ("bow", ctypes.c_int32), - ("arrows", ctypes.c_int32), - ("armour", _c_array(ctypes.c_int32, 4)), - ("torches", ctypes.c_int32), - ("ruby", ctypes.c_int32), - ("sapphire", ctypes.c_int32), - ("potions", _c_array(ctypes.c_int32, 6)), - ("books", ctypes.c_int32), - ] - - -class CraftaxMobs3(ctypes.Structure): - _fields_ = [ - ("position", _c_array(ctypes.c_int32, LEVELS, 3, 2)), - ("health", _c_array(ctypes.c_float, LEVELS, 3)), - ("mask", _c_array(ctypes.c_bool, LEVELS, 3)), - ("attack_cooldown", _c_array(ctypes.c_int32, LEVELS, 3)), - ("type_id", _c_array(ctypes.c_int32, LEVELS, 3)), - ] - - -class CraftaxMobs2(ctypes.Structure): - _fields_ = [ - ("position", _c_array(ctypes.c_int32, LEVELS, 2, 2)), - ("health", _c_array(ctypes.c_float, LEVELS, 2)), - ("mask", _c_array(ctypes.c_bool, LEVELS, 2)), - ("attack_cooldown", _c_array(ctypes.c_int32, LEVELS, 2)), - ("type_id", _c_array(ctypes.c_int32, LEVELS, 2)), - ] - - -class CraftaxState(ctypes.Structure): - _fields_ = [ - ("map", _c_array(ctypes.c_int32, LEVELS, MAP_SIZE, MAP_SIZE)), - ("item_map", _c_array(ctypes.c_int32, LEVELS, MAP_SIZE, MAP_SIZE)), - ("mob_map", _c_array(ctypes.c_bool, LEVELS, MAP_SIZE, MAP_SIZE)), - ("light_map", _c_array(ctypes.c_float, LEVELS, MAP_SIZE, MAP_SIZE)), - ("down_ladders", _c_array(ctypes.c_int32, LEVELS, 2)), - ("up_ladders", _c_array(ctypes.c_int32, LEVELS, 2)), - ("chests_opened", _c_array(ctypes.c_bool, LEVELS)), - ("monsters_killed", _c_array(ctypes.c_int32, LEVELS)), - ("player_position", _c_array(ctypes.c_int32, 2)), - ("player_level", ctypes.c_int32), - ("player_direction", ctypes.c_int32), - ("player_health", ctypes.c_float), - ("player_food", ctypes.c_int32), - ("player_drink", ctypes.c_int32), - ("player_energy", ctypes.c_int32), - ("player_mana", ctypes.c_int32), - ("is_sleeping", ctypes.c_bool), - ("is_resting", ctypes.c_bool), - ("player_recover", ctypes.c_float), - ("player_hunger", ctypes.c_float), - ("player_thirst", ctypes.c_float), - ("player_fatigue", ctypes.c_float), - ("player_recover_mana", ctypes.c_float), - ("player_xp", ctypes.c_int32), - ("player_dexterity", ctypes.c_int32), - ("player_strength", ctypes.c_int32), - ("player_intelligence", ctypes.c_int32), - ("inventory", CraftaxInventory), - ("melee_mobs", CraftaxMobs3), - ("passive_mobs", CraftaxMobs3), - ("ranged_mobs", CraftaxMobs2), - ("mob_projectiles", CraftaxMobs3), - ( - "mob_projectile_directions", - _c_array(ctypes.c_int32, LEVELS, MAX_MOB_PROJECTILES, 2), - ), - ("player_projectiles", CraftaxMobs3), - ( - "player_projectile_directions", - _c_array(ctypes.c_int32, LEVELS, MAX_PLAYER_PROJECTILES, 2), - ), - ( - "growing_plants_positions", - _c_array(ctypes.c_int32, MAX_GROWING_PLANTS, 2), - ), - ("growing_plants_age", _c_array(ctypes.c_int32, MAX_GROWING_PLANTS)), - ("growing_plants_mask", _c_array(ctypes.c_bool, MAX_GROWING_PLANTS)), - ("potion_mapping", _c_array(ctypes.c_int32, 6)), - ("learned_spells", _c_array(ctypes.c_bool, 2)), - ("sword_enchantment", ctypes.c_int32), - ("bow_enchantment", ctypes.c_int32), - ("armour_enchantments", _c_array(ctypes.c_int32, 4)), - ("boss_progress", ctypes.c_int32), - ("boss_timesteps_to_spawn_this_round", ctypes.c_int32), - ("light_level", ctypes.c_float), - ("achievements", _c_array(ctypes.c_bool, ACHIEVEMENTS)), - ("state_rng", _c_array(ctypes.c_uint32, 2)), - ("timestep", ctypes.c_int32), - ("fractal_noise_angles", _c_array(ctypes.c_int32, 4)), - ] - - -def _np_array(value, dtype): - return np.ascontiguousarray(np.asarray(value, dtype=dtype)) - - -def _copy_to_c(c_array, value, dtype, shape): - array = _np_array(value, dtype) - if array.shape != shape: - raise ValueError(f"shape mismatch: got {array.shape}, expected {shape}") - ctypes.memmove(ctypes.addressof(c_array), array.ctypes.data, array.nbytes) - - -def _copy_from_c(c_array, dtype): - return np.asarray(np.ctypeslib.as_array(c_array), dtype=dtype).copy() - - -def _mobs_payload(mobs): - return { - "position": _np_array(mobs.position, np.int32), - "health": _np_array(mobs.health, np.float32), - "mask": _np_array(mobs.mask, np.bool_), - "attack_cooldown": _np_array(mobs.attack_cooldown, np.int32), - "type_id": _np_array(mobs.type_id, np.int32), - } - - -def _inventory_payload(inventory): - return { - "wood": int(inventory.wood), - "stone": int(inventory.stone), - "coal": int(inventory.coal), - "iron": int(inventory.iron), - "diamond": int(inventory.diamond), - "sapling": int(inventory.sapling), - "pickaxe": int(inventory.pickaxe), - "sword": int(inventory.sword), - "bow": int(inventory.bow), - "arrows": int(inventory.arrows), - "armour": _np_array(inventory.armour, np.int32), - "torches": int(inventory.torches), - "ruby": int(inventory.ruby), - "sapphire": int(inventory.sapphire), - "potions": _np_array(inventory.potions, np.int32), - "books": int(inventory.books), - } - - -def _fractal_payload(state): - values = [] - for value in state.fractal_noise_angles: - values.append(0 if value is None else int(value)) - return np.asarray(values, dtype=np.int32) - - -def serialize_jax_state(state: EnvState) -> bytes: - payload = { - "map": _np_array(state.map, np.int32), - "item_map": _np_array(state.item_map, np.int32), - "mob_map": _np_array(state.mob_map, np.bool_), - "light_map": _np_array(state.light_map, np.float32), - "down_ladders": _np_array(state.down_ladders, np.int32), - "up_ladders": _np_array(state.up_ladders, np.int32), - "chests_opened": _np_array(state.chests_opened, np.bool_), - "monsters_killed": _np_array(state.monsters_killed, np.int32), - "player_position": _np_array(state.player_position, np.int32), - "player_level": int(state.player_level), - "player_direction": int(state.player_direction), - "player_health": float(state.player_health), - "player_food": int(state.player_food), - "player_drink": int(state.player_drink), - "player_energy": int(state.player_energy), - "player_mana": int(state.player_mana), - "is_sleeping": bool(state.is_sleeping), - "is_resting": bool(state.is_resting), - "player_recover": float(state.player_recover), - "player_hunger": float(state.player_hunger), - "player_thirst": float(state.player_thirst), - "player_fatigue": float(state.player_fatigue), - "player_recover_mana": float(state.player_recover_mana), - "player_xp": int(state.player_xp), - "player_dexterity": int(state.player_dexterity), - "player_strength": int(state.player_strength), - "player_intelligence": int(state.player_intelligence), - "inventory": _inventory_payload(state.inventory), - "melee_mobs": _mobs_payload(state.melee_mobs), - "passive_mobs": _mobs_payload(state.passive_mobs), - "ranged_mobs": _mobs_payload(state.ranged_mobs), - "mob_projectiles": _mobs_payload(state.mob_projectiles), - "mob_projectile_directions": _np_array( - state.mob_projectile_directions, np.int32 - ), - "player_projectiles": _mobs_payload(state.player_projectiles), - "player_projectile_directions": _np_array( - state.player_projectile_directions, np.int32 - ), - "growing_plants_positions": _np_array( - state.growing_plants_positions, np.int32 - ), - "growing_plants_age": _np_array(state.growing_plants_age, np.int32), - "growing_plants_mask": _np_array(state.growing_plants_mask, np.bool_), - "potion_mapping": _np_array(state.potion_mapping, np.int32), - "learned_spells": _np_array(state.learned_spells, np.bool_), - "sword_enchantment": int(state.sword_enchantment), - "bow_enchantment": int(state.bow_enchantment), - "armour_enchantments": _np_array(state.armour_enchantments, np.int32), - "boss_progress": int(state.boss_progress), - "boss_timesteps_to_spawn_this_round": int( - state.boss_timesteps_to_spawn_this_round - ), - "light_level": float(state.light_level), - "achievements": _np_array(state.achievements, np.bool_), - "state_rng": _np_array(state.state_rng, np.uint32), - "timestep": int(state.timestep), - "fractal_noise_angles": _fractal_payload(state), - } - return pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL) - - -def _copy_inventory_to_c(c_inventory, payload): - for name in [ - "wood", - "stone", - "coal", - "iron", - "diamond", - "sapling", - "pickaxe", - "sword", - "bow", - "arrows", - "torches", - "ruby", - "sapphire", - "books", - ]: - setattr(c_inventory, name, int(payload[name])) - _copy_to_c(c_inventory.armour, payload["armour"], np.int32, (4,)) - _copy_to_c(c_inventory.potions, payload["potions"], np.int32, (6,)) - - -def _copy_mobs_to_c(c_mobs, payload, max_mobs): - _copy_to_c(c_mobs.position, payload["position"], np.int32, (LEVELS, max_mobs, 2)) - _copy_to_c(c_mobs.health, payload["health"], np.float32, (LEVELS, max_mobs)) - _copy_to_c(c_mobs.mask, payload["mask"], np.bool_, (LEVELS, max_mobs)) - _copy_to_c( - c_mobs.attack_cooldown, - payload["attack_cooldown"], - np.int32, - (LEVELS, max_mobs), - ) - _copy_to_c(c_mobs.type_id, payload["type_id"], np.int32, (LEVELS, max_mobs)) - - -def deserialize_jax_state_to_c(buffer: bytes) -> CraftaxState: - payload = pickle.loads(buffer) - state = CraftaxState() - - _copy_to_c(state.map, payload["map"], np.int32, (LEVELS, MAP_SIZE, MAP_SIZE)) - _copy_to_c( - state.item_map, payload["item_map"], np.int32, (LEVELS, MAP_SIZE, MAP_SIZE) - ) - _copy_to_c( - state.mob_map, payload["mob_map"], np.bool_, (LEVELS, MAP_SIZE, MAP_SIZE) - ) - _copy_to_c( - state.light_map, payload["light_map"], np.float32, (LEVELS, MAP_SIZE, MAP_SIZE) - ) - _copy_to_c(state.down_ladders, payload["down_ladders"], np.int32, (LEVELS, 2)) - _copy_to_c(state.up_ladders, payload["up_ladders"], np.int32, (LEVELS, 2)) - _copy_to_c(state.chests_opened, payload["chests_opened"], np.bool_, (LEVELS,)) - _copy_to_c(state.monsters_killed, payload["monsters_killed"], np.int32, (LEVELS,)) - - _copy_to_c(state.player_position, payload["player_position"], np.int32, (2,)) - state.player_level = int(payload["player_level"]) - state.player_direction = int(payload["player_direction"]) - state.player_health = float(payload["player_health"]) - state.player_food = int(payload["player_food"]) - state.player_drink = int(payload["player_drink"]) - state.player_energy = int(payload["player_energy"]) - state.player_mana = int(payload["player_mana"]) - state.is_sleeping = bool(payload["is_sleeping"]) - state.is_resting = bool(payload["is_resting"]) - state.player_recover = float(payload["player_recover"]) - state.player_hunger = float(payload["player_hunger"]) - state.player_thirst = float(payload["player_thirst"]) - state.player_fatigue = float(payload["player_fatigue"]) - state.player_recover_mana = float(payload["player_recover_mana"]) - state.player_xp = int(payload["player_xp"]) - state.player_dexterity = int(payload["player_dexterity"]) - state.player_strength = int(payload["player_strength"]) - state.player_intelligence = int(payload["player_intelligence"]) - - _copy_inventory_to_c(state.inventory, payload["inventory"]) - _copy_mobs_to_c(state.melee_mobs, payload["melee_mobs"], MAX_MELEE_MOBS) - _copy_mobs_to_c(state.passive_mobs, payload["passive_mobs"], MAX_PASSIVE_MOBS) - _copy_mobs_to_c(state.ranged_mobs, payload["ranged_mobs"], MAX_RANGED_MOBS) - _copy_mobs_to_c( - state.mob_projectiles, payload["mob_projectiles"], MAX_MOB_PROJECTILES - ) - _copy_to_c( - state.mob_projectile_directions, - payload["mob_projectile_directions"], - np.int32, - (LEVELS, MAX_MOB_PROJECTILES, 2), - ) - _copy_mobs_to_c( - state.player_projectiles, - payload["player_projectiles"], - MAX_PLAYER_PROJECTILES, - ) - _copy_to_c( - state.player_projectile_directions, - payload["player_projectile_directions"], - np.int32, - (LEVELS, MAX_PLAYER_PROJECTILES, 2), - ) - _copy_to_c( - state.growing_plants_positions, - payload["growing_plants_positions"], - np.int32, - (MAX_GROWING_PLANTS, 2), - ) - _copy_to_c( - state.growing_plants_age, - payload["growing_plants_age"], - np.int32, - (MAX_GROWING_PLANTS,), - ) - _copy_to_c( - state.growing_plants_mask, - payload["growing_plants_mask"], - np.bool_, - (MAX_GROWING_PLANTS,), - ) - _copy_to_c(state.potion_mapping, payload["potion_mapping"], np.int32, (6,)) - _copy_to_c(state.learned_spells, payload["learned_spells"], np.bool_, (2,)) - state.sword_enchantment = int(payload["sword_enchantment"]) - state.bow_enchantment = int(payload["bow_enchantment"]) - _copy_to_c( - state.armour_enchantments, payload["armour_enchantments"], np.int32, (4,) - ) - state.boss_progress = int(payload["boss_progress"]) - state.boss_timesteps_to_spawn_this_round = int( - payload["boss_timesteps_to_spawn_this_round"] - ) - state.light_level = float(payload["light_level"]) - _copy_to_c(state.achievements, payload["achievements"], np.bool_, (ACHIEVEMENTS,)) - _copy_to_c(state.state_rng, payload["state_rng"], np.uint32, (2,)) - state.timestep = int(payload["timestep"]) - _copy_to_c( - state.fractal_noise_angles, - payload["fractal_noise_angles"], - np.int32, - (4,), - ) - return state - - -def jax_state_to_c_state(state: EnvState) -> CraftaxState: - return deserialize_jax_state_to_c(serialize_jax_state(state)) - - -def _inventory_from_c(inventory): - return Inventory( - wood=int(inventory.wood), - stone=int(inventory.stone), - coal=int(inventory.coal), - iron=int(inventory.iron), - diamond=int(inventory.diamond), - sapling=int(inventory.sapling), - pickaxe=int(inventory.pickaxe), - sword=int(inventory.sword), - bow=int(inventory.bow), - arrows=int(inventory.arrows), - armour=jnp.asarray(_copy_from_c(inventory.armour, np.int32)), - torches=int(inventory.torches), - ruby=int(inventory.ruby), - sapphire=int(inventory.sapphire), - potions=jnp.asarray(_copy_from_c(inventory.potions, np.int32)), - books=int(inventory.books), - ) - - -def _mobs_from_c(mobs): - return Mobs( - position=jnp.asarray(_copy_from_c(mobs.position, np.int32)), - health=jnp.asarray(_copy_from_c(mobs.health, np.float32)), - mask=jnp.asarray(_copy_from_c(mobs.mask, np.bool_)), - attack_cooldown=jnp.asarray(_copy_from_c(mobs.attack_cooldown, np.int32)), - type_id=jnp.asarray(_copy_from_c(mobs.type_id, np.int32)), - ) - - -def _fractal_from_template(template): - if template is None: - return (None, None, None, None) - return template.fractal_noise_angles - - -def craftax_state_to_jax(state: CraftaxState, template: EnvState | None = None) -> EnvState: - return EnvState( - map=jnp.asarray(_copy_from_c(state.map, np.int32)), - item_map=jnp.asarray(_copy_from_c(state.item_map, np.int32)), - mob_map=jnp.asarray(_copy_from_c(state.mob_map, np.bool_)), - light_map=jnp.asarray(_copy_from_c(state.light_map, np.float32)), - down_ladders=jnp.asarray(_copy_from_c(state.down_ladders, np.int32)), - up_ladders=jnp.asarray(_copy_from_c(state.up_ladders, np.int32)), - chests_opened=jnp.asarray(_copy_from_c(state.chests_opened, np.bool_)), - monsters_killed=jnp.asarray(_copy_from_c(state.monsters_killed, np.int32)), - player_position=jnp.asarray(_copy_from_c(state.player_position, np.int32)), - player_level=int(state.player_level), - player_direction=int(state.player_direction), - player_health=float(state.player_health), - player_food=int(state.player_food), - player_drink=int(state.player_drink), - player_energy=int(state.player_energy), - player_mana=int(state.player_mana), - is_sleeping=bool(state.is_sleeping), - is_resting=bool(state.is_resting), - player_recover=float(state.player_recover), - player_hunger=float(state.player_hunger), - player_thirst=float(state.player_thirst), - player_fatigue=float(state.player_fatigue), - player_recover_mana=float(state.player_recover_mana), - player_xp=int(state.player_xp), - player_dexterity=int(state.player_dexterity), - player_strength=int(state.player_strength), - player_intelligence=int(state.player_intelligence), - inventory=_inventory_from_c(state.inventory), - melee_mobs=_mobs_from_c(state.melee_mobs), - passive_mobs=_mobs_from_c(state.passive_mobs), - ranged_mobs=_mobs_from_c(state.ranged_mobs), - mob_projectiles=_mobs_from_c(state.mob_projectiles), - mob_projectile_directions=jnp.asarray( - _copy_from_c(state.mob_projectile_directions, np.int32) - ), - player_projectiles=_mobs_from_c(state.player_projectiles), - player_projectile_directions=jnp.asarray( - _copy_from_c(state.player_projectile_directions, np.int32) - ), - growing_plants_positions=jnp.asarray( - _copy_from_c(state.growing_plants_positions, np.int32) - ), - growing_plants_age=jnp.asarray( - _copy_from_c(state.growing_plants_age, np.int32) - ), - growing_plants_mask=jnp.asarray( - _copy_from_c(state.growing_plants_mask, np.bool_) - ), - potion_mapping=jnp.asarray(_copy_from_c(state.potion_mapping, np.int32)), - learned_spells=jnp.asarray(_copy_from_c(state.learned_spells, np.bool_)), - sword_enchantment=int(state.sword_enchantment), - bow_enchantment=int(state.bow_enchantment), - armour_enchantments=jnp.asarray( - _copy_from_c(state.armour_enchantments, np.int32) - ), - boss_progress=int(state.boss_progress), - boss_timesteps_to_spawn_this_round=int( - state.boss_timesteps_to_spawn_this_round - ), - light_level=float(state.light_level), - achievements=jnp.asarray(_copy_from_c(state.achievements, np.bool_)), - state_rng=jnp.asarray(_copy_from_c(state.state_rng, np.uint32)), - timestep=int(state.timestep), - fractal_noise_angles=_fractal_from_template(template), - ) - - -def _flatten_mobs(prefix, mobs): - return { - f"{prefix}.position": np.asarray(mobs.position), - f"{prefix}.health": np.asarray(mobs.health), - f"{prefix}.mask": np.asarray(mobs.mask), - f"{prefix}.attack_cooldown": np.asarray(mobs.attack_cooldown), - f"{prefix}.type_id": np.asarray(mobs.type_id), - } - - -def _flatten_inventory(inventory): - return { - "inventory.wood": np.asarray(inventory.wood), - "inventory.stone": np.asarray(inventory.stone), - "inventory.coal": np.asarray(inventory.coal), - "inventory.iron": np.asarray(inventory.iron), - "inventory.diamond": np.asarray(inventory.diamond), - "inventory.sapling": np.asarray(inventory.sapling), - "inventory.pickaxe": np.asarray(inventory.pickaxe), - "inventory.sword": np.asarray(inventory.sword), - "inventory.bow": np.asarray(inventory.bow), - "inventory.arrows": np.asarray(inventory.arrows), - "inventory.armour": np.asarray(inventory.armour), - "inventory.torches": np.asarray(inventory.torches), - "inventory.ruby": np.asarray(inventory.ruby), - "inventory.sapphire": np.asarray(inventory.sapphire), - "inventory.potions": np.asarray(inventory.potions), - "inventory.books": np.asarray(inventory.books), - } - - -def flatten_env_state(state: EnvState): - flat = { - "map": np.asarray(state.map), - "item_map": np.asarray(state.item_map), - "mob_map": np.asarray(state.mob_map), - "light_map": np.asarray(state.light_map), - "down_ladders": np.asarray(state.down_ladders), - "up_ladders": np.asarray(state.up_ladders), - "chests_opened": np.asarray(state.chests_opened), - "monsters_killed": np.asarray(state.monsters_killed), - "player_position": np.asarray(state.player_position), - "player_level": np.asarray(state.player_level), - "player_direction": np.asarray(state.player_direction), - "player_health": np.asarray(state.player_health, dtype=np.float32), - "player_food": np.asarray(state.player_food), - "player_drink": np.asarray(state.player_drink), - "player_energy": np.asarray(state.player_energy), - "player_mana": np.asarray(state.player_mana), - "is_sleeping": np.asarray(state.is_sleeping), - "is_resting": np.asarray(state.is_resting), - "player_recover": np.asarray(state.player_recover, dtype=np.float32), - "player_hunger": np.asarray(state.player_hunger, dtype=np.float32), - "player_thirst": np.asarray(state.player_thirst, dtype=np.float32), - "player_fatigue": np.asarray(state.player_fatigue, dtype=np.float32), - "player_recover_mana": np.asarray( - state.player_recover_mana, dtype=np.float32 - ), - "player_xp": np.asarray(state.player_xp), - "player_dexterity": np.asarray(state.player_dexterity), - "player_strength": np.asarray(state.player_strength), - "player_intelligence": np.asarray(state.player_intelligence), - "mob_projectile_directions": np.asarray(state.mob_projectile_directions), - "player_projectile_directions": np.asarray( - state.player_projectile_directions - ), - "growing_plants_positions": np.asarray(state.growing_plants_positions), - "growing_plants_age": np.asarray(state.growing_plants_age), - "growing_plants_mask": np.asarray(state.growing_plants_mask), - "potion_mapping": np.asarray(state.potion_mapping), - "learned_spells": np.asarray(state.learned_spells), - "sword_enchantment": np.asarray(state.sword_enchantment), - "bow_enchantment": np.asarray(state.bow_enchantment), - "armour_enchantments": np.asarray(state.armour_enchantments), - "boss_progress": np.asarray(state.boss_progress), - "boss_timesteps_to_spawn_this_round": np.asarray( - state.boss_timesteps_to_spawn_this_round - ), - "light_level": np.asarray(state.light_level, dtype=np.float32), - "achievements": np.asarray(state.achievements), - "state_rng": np.asarray(state.state_rng, dtype=np.uint32), - "timestep": np.asarray(state.timestep), - "fractal_noise_angles": np.asarray( - [0 if value is None else int(value) for value in state.fractal_noise_angles], - dtype=np.int32, - ), - } - flat.update(_flatten_inventory(state.inventory)) - flat.update(_flatten_mobs("melee_mobs", state.melee_mobs)) - flat.update(_flatten_mobs("passive_mobs", state.passive_mobs)) - flat.update(_flatten_mobs("ranged_mobs", state.ranged_mobs)) - flat.update(_flatten_mobs("mob_projectiles", state.mob_projectiles)) - flat.update(_flatten_mobs("player_projectiles", state.player_projectiles)) - return flat - - -def assert_env_states_equal(actual: EnvState, expected: EnvState, context: str): - actual_flat = flatten_env_state(actual) - expected_flat = flatten_env_state(expected) - if actual_flat.keys() != expected_flat.keys(): - missing = expected_flat.keys() - actual_flat.keys() - extra = actual_flat.keys() - expected_flat.keys() - raise AssertionError(f"{context}: state keys differ missing={missing} extra={extra}") - - for name, expected_value in expected_flat.items(): - actual_value = actual_flat[name] - err_msg = f"{context}: field {name}" - if expected_value.dtype.kind == "f": - np.testing.assert_allclose( - actual_value, - expected_value, - atol=1e-6, - rtol=0.0, - err_msg=err_msg, - ) - else: - np.testing.assert_array_equal(actual_value, expected_value, err_msg=err_msg) diff --git a/tests/craftax_step_full_test.py b/tests/craftax_step_full_test.py deleted file mode 100644 index 05bb930389..0000000000 --- a/tests/craftax_step_full_test.py +++ /dev/null @@ -1,14 +0,0 @@ -from types import SimpleNamespace - -from tests import craftax_parity - - -def test_craftax_full_native_step_parity(): - args = SimpleNamespace( - seeds=16, - seed_start=0, - steps=2000, - action_seed=0, - atol=1e-5, - ) - assert craftax_parity.run(args) == 0