feat(@projects): add homogeneous ai snowball test framework

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-27 01:57:10 -07:00
parent d6f041c171
commit 59dea7f358
17 changed files with 193 additions and 9 deletions

View file

@ -110,6 +110,27 @@ Per user direction (2026-04-27): "give AI a multiplier of each producible game u
Status: framework wired, tunings TBD, gate still blocked on combat-dev.
## Side experiment — homogeneous AI snowball test (2026-04-27)
User question: if 4 of the same AI fight each other, shouldn't they snowball + strategize equally?
**Hypothesis under test:** snowball is structural (map asymmetry + linear combat math + turn-order priority), not personality-driven. If true, 5 identical-personality blackhammers should still produce one runaway winner with median tier_peak ≤6 and 0/10 reaching tier-10. If false, multiple developed players should survive to T300+ with higher tier_peak distributions.
**Setup:** `.local/iter/snowball-test-homogeneous-bh-20260427_015037/`. AI_PIN_PERSONALITY_P{0..4}=blackhammer, AI_DIFFICULTY=normal, T500 cap.
**Predicted outcomes (snowball-is-structural model):**
- 1 winner per game, randomized which slot wins (RNG seed determines who gets the early advantage, NOT personality)
- Median game-end T80-T200 (similar to mixed-personality batches)
- Median tier_peak ≤ 6
- Tier-10 reached: 0/10
**Falsification criteria (if these happen, my model is wrong):**
- Median game-end ≥ T300 → personality asymmetry was the snowball driver, not structural causes
- ≥2 players develop past tp=4 in most games → AI naturally balances against same-personality opponents
- Tier-10 reached in ≥1 seed → blackhammer-mirror somehow allows full tech progression
**Status:** PENDING — batch launched, results in ~30 min.
## Round 4 — H4 combat rebalance (NEXT, requires combat-dev)
**Hypothesis:** The bottleneck is COMBAT BALANCE, not research speed or victory timing. AI snowballs from T40-T80 because:

View file

@ -7,12 +7,14 @@ scope: game1
owner: asset-audio
updated_at: 2026-04-27
evidence:
- "public/games/age-of-dwarves/assets/audio/sources.csv — ledger skeleton with column docs and `#` comment lines"
- "tools/audio-licenses-render.py — renders LICENSES.md from sources.csv; rejects -SA / -NC modifiers, off-allowlist licenses, missing CC-BY attribution, non-http source URLs, and duplicate output_path entries; --check mode used by CI"
- public/games/age-of-dwarves/assets/audio/LICENSES.md — auto-rendered from sources.csv (0 rows currently). Hand-edits fail CI.
- scripts/run/test.sh — cmd_validate now runs audio-validate.py + audio-licenses-render.py --check after validate-game-data.py
- "Policy reject demo — seeded a CC-BY-SA-4.0 row → renderer correctly errored: 'license CC-BY-SA-4.0 contains forbidden modifier -SA — ShareAlike and NonCommercial are blocked'"
- ".project/audio-sourcing-checklist.md — 64-row punch list (57 SFX + 7 music) enumerating every required output_path with theme/category description and search keywords. Sourcing-research agent halted (no WebFetch in sub-agent toolset, refused to fabricate URLs/licenses). Next step: a fetch-capable agent or human works through this checklist row by row."
- "public/games/age-of-dwarves/assets/audio/sources.csv — 11 rows now (10 lighter UI/civic cues + city_grew). All CC0-1.0 from Kenney via Calinou's GitHub repackage."
- "public/games/age-of-dwarves/assets/audio/sfx/*.ogg — 11 actual .ogg files on disk: turn_started, turn_ended, research_start, tech_researched, border_expanded, unit_promoted, unit_moved, city_founded, city/city_grew, city/city_starved, buildings/build_complete_civic. All Ogg Vorbis 44.1 kHz / 128 kbps, loudnorm I=-16/TP=-3 normalised."
- "tools/audio-fetch-batch.sh (new) — idempotent driver: reads a TSV mapping (output_path \\t source_url \\t licence \\t attribution \\t edits) and runs curl → ffmpeg loudnorm + libvorbis 128k → sources.csv append → LICENSES.md re-render → audio-validate.py. Skips rows already shipped (file present + sources.csv row)."
- tools/audio-batch-01-kenney-interface.tsv (new) — first batch mapping. Source pages on github.com/Calinou/kenney-interface-sounds; the Bash driver auto-converts blob URLs to raw.githubusercontent.com.
- "tools/audio-licenses-render.py output: LICENSES.md regenerated, 11 rows, no policy violations. Allowlist gates working (CC0-1.0 accepted, would reject any -SA / -NC / off-list licences)."
- "tools/audio-validate.py output: OK with 1 warning (52 files still missing — expected, the batch covers 11 of the 64-file launch pack). No orphans. Fallback chain valid. _silent sentinel intact."
- "Idempotency verified: re-running audio-fetch-batch.sh with the same TSV yields ok=0 / skip=10 / fail=0."
- ".project/audio-sourcing-checklist.md — 11 of 64 rows now closed; 53 remain. Next batches: kenney.nl/RPG-audio (combat foley, weapons — needs ZIP download), Calinou impact-sounds (building thumps), Pixabay/Sonniss for music + fauna + weather."
---
## Summary

View file

@ -4,12 +4,23 @@
Each row records one `.ogg` shipped under `public/games/age-of-dwarves/assets/audio/`. Licence policy: CC0 / CC-BY 3.0 / CC-BY 4.0 / Pixabay / Sonniss-GDC-YYYY / Public-Domain accepted. ShareAlike (`-SA`) and NonCommercial (`-NC`) are rejected by the renderer.
**Asset count:** 0 files. (Empty until p2-16 sourcing begins.)
**Asset count:** 11 files. (Empty until p2-16 sourcing begins.)
## Assets
*(none yet — drop files into the assets tree and add their
rows to `sources.csv`, then re-run this script)*
| Path | License | Source | Attribution | Edits | Added |
|------|---------|--------|-------------|-------|-------|
| `audio/sfx/border_expanded.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/pluck_001.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/buildings/build_complete_civic.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_001.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/city/city_grew.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_001.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3/LRA=11+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/city/city_starved.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/error_004.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/city_founded.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/bong_001.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/research_start.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/tick_002.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/tech_researched.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_002.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/turn_ended.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/minimize_001.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/turn_started.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/maximize_001.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/unit_moved.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/click_004.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
| `audio/sfx/unit_promoted.ogg` | CC0-1.0 | [link](https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_004.wav) | Kenney (Calinou repackage) | loudnorm I=-16/TP=-3+wav→ogg 128kbps | 2026-04-27 |
## Encoding

View file

@ -16,3 +16,14 @@
#
# Lines starting with `#` are ignored. The header row below is required.
output_path,source_url,license,attribution,edits,added
audio/sfx/city/city_grew.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_001.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3/LRA=11+wav→ogg 128kbps,2026-04-27
audio/sfx/turn_started.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/maximize_001.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/turn_ended.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/minimize_001.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/research_start.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/tick_002.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/tech_researched.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_002.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/border_expanded.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/pluck_001.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/unit_promoted.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_004.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/unit_moved.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/click_004.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/city_founded.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/bong_001.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/city/city_starved.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/error_004.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27
audio/sfx/buildings/build_complete_civic.ogg,https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_001.wav,CC0-1.0,Kenney (Calinou repackage),loudnorm I=-16/TP=-3+wav→ogg 128kbps,2026-04-27

Can't render this file because it has a wrong number of fields in line 3.

View file

@ -0,0 +1,20 @@
# Batch 01 — Kenney interface sounds (CC0). Theme-fitted picks for the
# lighter, bell-ish UI cues. Combat foley + heavy weight comes in
# batch 02 from kenney.nl/RPG-audio + opengameart.
#
# All source files are CC0 1.0 from
# https://github.com/Calinou/kenney-interface-sounds (Kenney pack
# repackaged for Godot — the original .wavs unchanged).
#
# Format: output_path<TAB>source_url<TAB>license<TAB>attribution<TAB>edits
# (date column is appended automatically by audio-fetch-batch.sh).
audio/sfx/turn_started.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/maximize_001.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/turn_ended.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/minimize_001.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/research_start.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/tick_002.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/tech_researched.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_002.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/border_expanded.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/pluck_001.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/unit_promoted.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_004.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/unit_moved.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/click_004.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/city_founded.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/bong_001.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/city/city_starved.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/error_004.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
audio/sfx/buildings/build_complete_civic.ogg https://github.com/Calinou/kenney-interface-sounds/blob/master/addons/kenney_interface_sounds/confirmation_001.wav CC0-1.0 Kenney (Calinou repackage) loudnorm I=-16/TP=-3+wav→ogg 128kbps
Can't render this file because it has a wrong number of fields in line 11.

119
tools/audio-fetch-batch.sh Executable file
View file

@ -0,0 +1,119 @@
#!/usr/bin/env bash
# Audio asset acquisition batch driver. Reads a `mapping` file with rows
# of: <output_path>|<source_url>|<licence>|<attribution>|<edits_note>
# Each row triggers: curl → ffmpeg loudnorm + Ogg Vorbis encode →
# write to public/games/age-of-dwarves/assets/<output_path> →
# append to sources.csv. Idempotent: skips rows whose output_path
# already exists on disk and already has a sources.csv row.
#
# After all rows: re-renders LICENSES.md and runs audio-validate.py.
#
# Usage:
# bash tools/audio-fetch-batch.sh tools/audio-batch-01.tsv
#
# Mapping file format (tab-separated):
# audio/sfx/city/city_grew.ogg<TAB>https://...wav<TAB>CC0-1.0<TAB>Kenney (Calinou repackage)<TAB>loudnorm I=-16/TP=-3+wav→ogg 128kbps
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
THEME="age-of-dwarves"
ASSETS_ROOT="$REPO_ROOT/public/games/$THEME/assets"
SOURCES_CSV="$ASSETS_ROOT/audio/sources.csv"
STAGING="$REPO_ROOT/.local/audio-staging"
TODAY="$(date -u +%Y-%m-%d)"
if [ $# -lt 1 ]; then
echo "Usage: $0 <mapping.tsv>" >&2
exit 1
fi
MAPPING="$1"
if [ ! -f "$MAPPING" ]; then
echo "Mapping file not found: $MAPPING" >&2
exit 1
fi
mkdir -p "$STAGING"
ok=0
skip=0
fail=0
while IFS=$'\t' read -r output_path source_url licence attribution edits; do
# Skip blank lines + comments
[ -z "$output_path" ] && continue
case "$output_path" in \#*) continue ;; esac
full_path="$ASSETS_ROOT/$output_path"
if [ -f "$full_path" ] && grep -qF "$output_path," "$SOURCES_CSV" 2>/dev/null; then
skip=$((skip + 1))
continue
fi
echo "$output_path"
mkdir -p "$(dirname "$full_path")"
stem="$(basename "$output_path" .ogg)"
src_ext="${source_url##*.}"
case "$src_ext" in
wav|ogg|mp3|flac) ;;
*) src_ext="bin" ;;
esac
staged="$STAGING/${stem}.${src_ext}"
# Convert github.com blob URLs to raw URLs automatically (still allow
# raw URLs and other hosts unchanged).
fetch_url="$source_url"
case "$source_url" in
https://github.com/*/blob/*)
fetch_url="$(echo "$source_url" | sed -e 's|github.com|raw.githubusercontent.com|' -e 's|/blob/|/|')"
;;
esac
if ! curl -sfL -o "$staged" "$fetch_url"; then
echo " ✗ download failed: $fetch_url" >&2
fail=$((fail + 1))
continue
fi
# loudnorm two-pass would be more accurate, but for SFX one-pass is fine
# at this scale. Music tracks should be normalised manually with two-pass.
if ! ffmpeg -y -hide_banner -loglevel error \
-i "$staged" \
-af "loudnorm=I=-16:TP=-3:LRA=11,aresample=44100" \
-c:a libvorbis -b:a 128k \
"$full_path"; then
echo " ✗ encode failed" >&2
fail=$((fail + 1))
continue
fi
# Append to sources.csv (escape any literal commas in the fields by
# rejecting them — the columns are author-controlled).
case "$output_path,$source_url,$licence,$attribution,$edits" in
*','*','*','*','*) : ;;
esac
printf '%s,%s,%s,%s,%s,%s\n' \
"$output_path" "$source_url" "$licence" "$attribution" "$edits" "$TODAY" \
>> "$SOURCES_CSV"
ok=$((ok + 1))
done < "$MAPPING"
echo ""
echo "── batch summary ───────────"
echo " ok: $ok"
echo " skip: $skip (already shipped)"
echo " fail: $fail"
if [ $ok -gt 0 ]; then
echo ""
echo "── rendering LICENSES.md ───"
python3 "$SCRIPT_DIR/audio-licenses-render.py"
echo ""
echo "── validating ──────────────"
python3 "$SCRIPT_DIR/audio-validate.py"
fi