#!/usr/bin/env bash # Checks the simulation is deterministic across builds and platforms, which desktop/web crossplay depends on. # # Fails if any source calls the maths library's transcendentals (src/dmath.h replaces them), then builds the game # several ways and plays the same seeded bot matches in each (--balance with RPG_SEED), comparing the determinism hash # they print: # gcc GCC, optimised (the usual build) # gcc-fma GCC for this CPU (with FMA if it has it, standing in for arm64's fused multiply-adds) # clang-O0 Clang, unoptimised # wasm Emscripten, run under node (needs EMSDK, or emsdk_env.sh sourced) # # Usage: tools/determinism_check.sh [matches per seed, default 20] [seeds, default "1 2 3"] set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" OUT="$ROOT/build-check" MATCHES="${1:-20}" SEEDS="${2:-1 2 3}" banned=$(grep -nE "std::(sin|cos|tan|atan2|atan|asin|acos|exp|exp2|expm1|log|log2|log10|log1p|pow|hypot|cbrt|sinh|cosh|tanh|lerp)\(|\b(sinf|cosf|tanf|atan2f|powf|expf|logf|log2f)\(|SDL_(sin|cos|tan|atan2|atan|pow|exp|log)f?\(" \ "$ROOT"/src/*.cpp "$ROOT"/src/*.h | grep -v "src/dmath.h" || true) if [ -n "$banned" ]; then echo "Maths library transcendentals in the source (use math_sin and friends from src/dmath.h):" echo "$banned" exit 1 fi configure() # name, cmake arguments... { local name="$1" shift if [ ! -f "$OUT/$name/build.ninja" ]; then cmake -S "$ROOT" -B "$OUT/$name" -G Ninja "$@" > "$OUT/$name.log" 2>&1 fi cmake --build "$OUT/$name" >> "$OUT/$name.log" 2>&1 || { echo "$name failed to build, see $OUT/$name.log" exit 1 } } mkdir -p "$OUT" names=(gcc gcc-fma clang-O0) configure gcc -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ configure gcc-fma -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ \ -DCMAKE_CXX_FLAGS=-march=native -DCMAKE_C_FLAGS=-march=native configure clang-O0 -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ if [ -n "${EMSDK:-}" ] || command -v emcmake > /dev/null; then [ -n "${EMSDK:-}" ] && source "$EMSDK/emsdk_env.sh" > /dev/null 2>&1 if [ ! -f "$OUT/wasm/build.ninja" ]; then emcmake cmake -S "$ROOT" -B "$OUT/wasm" -G Ninja -DCMAKE_BUILD_TYPE=Release -DRPG_NODE=ON > "$OUT/wasm.log" 2>&1 fi cmake --build "$OUT/wasm" >> "$OUT/wasm.log" 2>&1 || { echo "wasm failed to build, see $OUT/wasm.log" exit 1 } names+=(wasm) else echo "Emscripten not found (set EMSDK): skipping the wasm build." fi hash_get() # name, seed { local run=("$OUT/$1/smack-a-brother") [ "$1" = wasm ] && run=(node "$OUT/wasm/smack-a-brother.js") (cd "$OUT/$1" && RPG_SEED="$2" "${run[@]}" --balance "$MATCHES" 2>&1 | grep -oE "hash: [0-9a-f]+" | cut -d' ' -f2) } failed=0 for seed in $SEEDS; do line="seed $seed:" first="" for name in "${names[@]}"; do hash=$(hash_get "$name" "$seed") line+=" $name $hash" [ -z "$first" ] && first="$hash" [ "$hash" != "$first" ] && failed=1 done echo "$line" done if [ "$failed" = 1 ]; then echo "DESYNC: the builds disagree." exit 1 fi echo "Deterministic: every build agrees."