#82 FINAL LAYER: the peer body-channel trn-lock -- a circling limper skated because the peer's turn-arm had no walk-demand yield and the trn speed exit read a dead replicant mapper cell

The master limped clean (218x gimp-cycle records, zero trn) -- a _ReturnAddress
trap on SetAnimationState(4) proved ALL peer trn arming came from the port's own
mech4 turn-step block, not the type-3 reader.  Two dead ends, one root:
- mech4 peer arm: gated on !wantsWalk (standSpeed < bodyTargetSpeed), exits trn
  when walking demand appears -- the leg twin's authentic precedence
  (part_012.c:12013, the Standing speed test outranks the turn test).
- mech2 body case 4: bspd reads bodyTargetSpeed on ReplicantInstance; the local
  mapper's speedDemand is a dead cell on a peer (reads 0 forever -> no exit).
Verified 2-node timeline: arena circle replicant 218x state-24 / 0x state-4
(was 116x trn churn), grass circle 235-clean, knockdowns bounded, 0 deaths.
Diagnostics kept: BT_TRNTRAP ra-trap, BT_ANIMIND_CAP, [gimpfeed], [replgimp]
extension.  KB: locomotion.md trn-lock entry + the dead-mapper-cell lesson.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QRjrTQpJd6u9XyfnUbTB3v
This commit is contained in:
Joe DiPrima
2026-07-30 18:26:15 -05:00
co-authored by Claude Fable 5
parent 14ff3519ac
commit e91d447405
8 changed files with 231 additions and 15 deletions
+20
View File
@@ -247,6 +247,26 @@ figure, printed under a misleading label since renamed `ggCapL/R`); field @0x52c
has NO binary consumer. Advance caps, loader fills, crunch path and crash branch are all verified has NO binary consumer. Advance caps, loader fills, crunch path and crash branch are all verified
byte-faithful — the self-damage dispatch was the only gap. Replication detail in [[multiplayer]]. byte-faithful — the self-damage dispatch was the only gap. Replication detail in [[multiplayer]].
**The LAST #82 layer: the peer body-channel TRN-LOCK (root-caused + fixed 2026-07-30) [T2].**
After the storm fix, a CIRCLING limper still skated on peers (user repro): post-knockdown recovery
lands the peer at body-stand; the port's peer turn-step arming block (mech4.cpp `PerformAndWatch`,
needed because the peer no longer runs the leg channel that cross-arms trn) saw
`replMppr->turnDemand` (circling → always turning) and armed body state 4 — and NOTHING could exit:
(a) body case 4's speed exit read the LOCAL mapper's `speedDemand`, a **dead cell on a replicant**
(nothing writes it → 0 forever); (b) turning never stops for a circler. Peer pinned in trn
(advance-only churn = leg-lifty skating) while `bodyTargetSpeed` held the replicated demand
(~24 u/s full). Measured via `[replgimp]` (`bts=24.6 standSpeed=6.83 bodyState=4` in 168/171
samples) and a `_ReturnAddress` trap on `SetAnimationState(4)` (`[trntrap]`, BT_TRNTRAP → linker
map: ALL 60 hits from PerformAndWatch = the port's own arm, NOT the type-3 reader — the master
broadcasts clean gimp states). Fix pair, mirroring the leg twin's authentic precedence
(part_012.c:12013 — the Standing speed test outranks the turn test): (1) mech4 peer arm gated on
`!wantsWalk` (`standSpeed < bodyTargetSpeed || bts < 0`), exit trn when wantsWalk; (2) mech2 body
case 4 `bspd` reads `bodyTargetSpeed` on ReplicantInstance (the replicated demand — the same
number the peer Standing case walks on). Verified: arena circle replicant 218×state-24/0×state-4
(was 116×trn churn), grass circle 235-clean, knockdowns bounded, 0 deaths. **Lesson: on a
replicant, every `MechControlsMapper` demand cell except those explicitly re-derived
(`turnDemand`) is DEAD — any peer-side state machine judging a local mapper read is judging 0.**
## Controls (`BT_REAL_CONTROLS`, default-on) ## Controls (`BT_REAL_CONTROLS`, default-on)
`MechControlsMapper` (mechmppr.cpp @004afbe0; btl4mppr.cpp mappers) interprets input → `speedDemand` `MechControlsMapper` (mechmppr.cpp @004afbe0; btl4mppr.cpp mappers) interprets input → `speedDemand`
/ `turnDemand`. ⚠ **WndProc NEVER receives WM_KEYUP** (the engine's per-frame reader `GetMessage`s / `turnDemand`. ⚠ **WndProc NEVER receives WM_KEYUP** (the engine's per-frame reader `GetMessage`s
+88 -4
View File
@@ -90,6 +90,7 @@
// //
#include <bt.hpp> #include <bt.hpp>
#include <intrin.h> // _ReturnAddress -- the #82 trn-armer trap
#include <AUDCMP.hpp> // AudioComponent -- the foot-plant step-intensity broadcast #include <AUDCMP.hpp> // AudioComponent -- the foot-plant step-intensity broadcast
#include <AUDSRC.hpp> // AudioSource -- footstep-source identification #include <AUDSRC.hpp> // AudioSource -- footstep-source identification
#include <AUDLVL.hpp> // AudioResource::GetAudioLevelOfDetail #include <AUDLVL.hpp> // AudioResource::GetAudioLevelOfDetail
@@ -250,11 +251,33 @@ void
// StateIndicator::SetState's Verify(state<stateCount). [T2] // StateIndicator::SetState's Verify(state<stateCount). [T2]
if (state >= 0 && state < 0x21) if (state >= 0 && state < 0x21)
{ {
// DIAG (BT_TRNTRAP): who arms state 4 on a REPLICANT? Module-relative
// ra (symbolize: tools/symcrash.py) -- the #82 trn-lock armer hunt.
if (state == 4 && getenv("BT_TRNTRAP")
&& GetInstance() == ReplicantInstance)
{
static int s_tt = 0;
if (s_tt++ < 60)
{
char ttbuf[96];
sprintf(ttbuf, "[trntrap] mech=%d ra=btl4+0x%lx",
(int)GetEntityID(),
(unsigned long)_ReturnAddress()
- (unsigned long)GetModuleHandleA(0));
DEBUG_STREAM << ttbuf << std::endl << std::flush;
}
}
// #78 audio-flake diag: print the LIVE indicator address once per mech // #78 audio-flake diag: print the LIVE indicator address once per mech
// so a session's [attrbind] ptr can be checked against it (stale-bind // so a session's [attrbind] ptr can be checked against it (stale-bind
// hypothesis: watchers bound to a recreated mech's dead indicator). // hypothesis: watchers bound to a recreated mech's dead indicator).
if (getenv("BT_AUDIO_SPATIAL")) { static int s_ai=0; if (getenv("BT_AUDIO_SPATIAL")) { static int s_ai=0;
if (s_ai++ < 6 || (state >= 22 && state <= 27 && s_ai < 200)) // BT_ANIMIND_CAP: the 200-print budget hid every post-1-minute state
// (two #82 investigations mis-read the silence as "never entered") --
// raise it for state-timeline diagnosis.
static int s_aiCap = -1;
if (s_aiCap < 0) { const char *c = getenv("BT_ANIMIND_CAP"); s_aiCap = (c && *c) ? atoi(c) : 200; }
if (s_ai++ < 6 || (state >= 22 && state <= 27 && s_ai < s_aiCap)
|| (s_aiCap > 200 && s_ai < s_aiCap))
DEBUG_STREAM << "[animind] mech=" << (int)GetEntityID() DEBUG_STREAM << "[animind] mech=" << (int)GetEntityID()
<< " &animationState=" << (void*)&animationState << " &animationState=" << (void*)&animationState
<< " inst=" << (int)(GetInstance() == ReplicantInstance) << " inst=" << (int)(GetInstance() == ReplicantInstance)
@@ -833,8 +856,35 @@ Scalar
// alias (an AV); controlsMapper is the typed mirror of roster slot 0. A mech // alias (an AV); controlsMapper is the typed mirror of roster slot 0. A mech
// with no mapper reads demand 0 (idles) -- matching a zeroed binary roster. // with no mapper reads demand 0 (idles) -- matching a zeroed binary roster.
MechControlsMapper *mppr = MappingMapper(); // roster slot 0 (task #7) MechControlsMapper *mppr = MappingMapper(); // roster slot 0 (task #7)
// REPLICANT DEMAND FEED (#82 final root, 2026-07-30): a replicant's LOCAL
// mapper cell is a dead 0 (input never drives it), so every demand
// threshold in this machine failed on peers -- from STAND the walk-begin
// could never fire and the trn state churned on the turn signal forever
// ("lifting legs in an alternating turning fashion"). It never mattered
// while a peer was mid-cycle (clip-end chains keep cycles going, which is
// why straight-line/grass limpers replicated fine) -- it bit the moment a
// KNOCKDOWN recovery dropped the replicant to STAND. The binary replicant
// reads a LIVE demand here (its mapper cell replicates with the subsystem
// records); the port's replicated equivalent is bodyTargetSpeed (@0x6b4 --
// every update record's speedDemand writes it on RX). [gimpfeed]-probe
// proof: AdvanceLegAnimationGimp never even runs on replicants, so THIS
// driver is the peer's one and only leg machine.
Scalar commandedSpeed = Scalar commandedSpeed =
(mppr != 0) ? mppr->speedDemand : 0.0f; (GetInstance() == ReplicantInstance) ? bodyTargetSpeed
: (mppr != 0) ? mppr->speedDemand : 0.0f;
// DIAG (BT_GIMPFEED): 1 Hz -- the NORMAL driver is the only leg machine a
// replicant runs; print every threshold operand it sees.
if (getenv("BT_GIMPFEED") && GetInstance() == ReplicantInstance)
{
static Scalar s_gfAcc = 0.0f; s_gfAcc += time_slice;
if (s_gfAcc >= 1.0f) { s_gfAcc = 0.0f;
DEBUG_STREAM << "[gimpfeed] cmd=" << commandedSpeed
<< " bts=" << bodyTargetSpeed
<< " standSpeed=" << standSpeed
<< " state=" << (int)legStateAlarm.GetLevel()
<< " gl=" << ([](Mech *m){ extern int BTMechGimpLevel(void*); return BTMechGimpLevel(m); })(this)
<< std::endl << std::flush; }
}
Scalar distance = 0.0f; Scalar distance = 0.0f;
// binary: legAnimationState@0x3b0 IS legStateAlarm's level (one field; the // binary: legAnimationState@0x3b0 IS legStateAlarm's level (one field; the
@@ -1251,7 +1301,15 @@ Scalar
// re-enter walk on the same frame. // re-enter walk on the same frame.
{ {
MechControlsMapper *bm = MappingMapper(); MechControlsMapper *bm = MappingMapper();
const Scalar bspd = (bm != 0) ? bm->speedDemand : 0.0f; // REPLICANT: the local mapper's speedDemand is a dead cell (nothing
// writes it on a peer -- reads 0 forever, so a peer that entered trn
// could NEVER take the speed exit = the #82 trn-lock). The peer's
// commanded speed is the REPLICATED demand (bodyTargetSpeed@0x6b4,
// stamped by every record RX) -- same source the peer Standing case
// walks on, so entry and exit judge the same number.
const Scalar bspd = (GetInstance() == ReplicantInstance)
? bodyTargetSpeed
: (bm != 0) ? bm->speedDemand : 0.0f;
if (standSpeed < bspd || bspd < ZeroSpeed) // walk / reverse (leg-symmetric) if (standSpeed < bspd || bspd < ZeroSpeed) // walk / reverse (leg-symmetric)
{ {
bodyStateAlarm.SetLevel(0); bodyStateAlarm.SetLevel(0);
@@ -1613,12 +1671,38 @@ Scalar
// straight at the speedDemand cell. // straight at the speedDemand cell.
MechControlsMapper *gimpMppr = MappingMapper(); MechControlsMapper *gimpMppr = MappingMapper();
static Scalar s_nullDemand = 0.0f; // no mapper -> demand 0, writes inert static Scalar s_nullDemand = 0.0f; // no mapper -> demand 0, writes inert
ReconMotionSource *motionSource = gimpMppr // REPLICANT DEMAND FEED (#82 final root, 2026-07-30): a replicant's LOCAL
// mapper demand is a dead 0 (no input ever drives it), so every speed
// threshold in this machine read 0 on peers -- in particular the trn exit
// (standSpeed < commandedSpeed) could NEVER fire, and a gimped replicant
// that staggered through stand->trn was LOCKED in turn-in-place forever
// (the observed "lifting legs in an alternating turning fashion" skate;
// unblinded [animind] timeline: SIX transitions in 3 minutes, ending at
// state 4). The binary's replicant reads a LIVE demand here because the
// mapper's speedDemand cell replicates with the subsystem records; the
// port's replicated equivalent of that same value is bodyTargetSpeed
// (@0x6b4 -- every update record's speedDemand writes it on RX). Feed
// the machine from it on replicants; the gimp caps that write back
// through the source are transient there (the next record rewrites it),
// and masters keep the authentic live mapper cell.
ReconMotionSource *motionSource =
(GetInstance() == ReplicantInstance)
? (ReconMotionSource *)&bodyTargetSpeed
: gimpMppr
? (ReconMotionSource *)&gimpMppr->speedDemand ? (ReconMotionSource *)&gimpMppr->speedDemand
: (ReconMotionSource *)&s_nullDemand; : (ReconMotionSource *)&s_nullDemand;
Scalar distance = 0.0f; Scalar distance = 0.0f;
extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23) extern int BTMechGimpLevel(void *mech_v); // mechdmg.cpp (gotcha #23)
int mode = BTMechGimpLevel(this); // binary this+0x40 = gimp level int mode = BTMechGimpLevel(this); // binary this+0x40 = gimp level
// DIAG (BT_GIMPFEED): 1 Hz -- what demand does this machine actually see?
if (getenv("BT_GIMPFEED") && GetInstance() == ReplicantInstance)
{
static Scalar s_gfAcc = 0.0f; s_gfAcc += time_slice;
if (s_gfAcc >= 1.0f) { s_gfAcc = 0.0f;
DEBUG_STREAM << "[gimpfeed] repl cmd=" << motionSource->commandedSpeed
<< " bts=" << bodyTargetSpeed << " state=" << legAnimationState
<< " standSpeed=" << standSpeed << " mode=" << mode << std::endl << std::flush; }
}
// RE-SYNC alarm -> state member (see AdvanceBodyAnimationGimp note). // RE-SYNC alarm -> state member (see AdvanceBodyAnimationGimp note).
legAnimationState = (int)legStateAlarm.GetLevel(); legAnimationState = (int)legStateAlarm.GetLevel();
int state = legAnimationState; // this+0x3b0 int state = legAnimationState; // this+0x3b0
+15 -3
View File
@@ -2436,10 +2436,19 @@ void
const int bs = (int)bodyStateAlarm.GetLevel(); const int bs = (int)bodyStateAlarm.GetLevel();
const bool turning = (replMppr->turnDemand > 0.05f const bool turning = (replMppr->turnDemand > 0.05f
|| replMppr->turnDemand < -0.05f); || replMppr->turnDemand < -0.05f);
if (turning && bs == 0) // standing + turning -> enter turn // YIELD TO WALK DEMAND (#82 trn-lock): the master only sits in
// trn when its commanded speed is at stand (the leg Standing
// case's speed test wins first, part_012.c:12013). A circling
// LIMPER keeps full demand (bts~24) while its yaw rate reads as
// turning here -- ungated, this block pinned the peer in body
// case 4 (advance-only, no exit) forever = the skating. Mirror
// the leg twin's precedence: walking demand outranks the pivot.
const bool wantsWalk = (standSpeed < bodyTargetSpeed
|| bodyTargetSpeed < 0.0f /*reverse*/);
if (turning && bs == 0 && !wantsWalk) // standing pivot only
SetBodyAnimation(4); SetBodyAnimation(4);
else if (!turning && bs == 4) // turn stopped -> back to stand else if (bs == 4 && (!turning || wantsWalk))
SetBodyAnimation(0); SetBodyAnimation(0); // stand; case 0 walk-begins next tick
} }
// GIMP routing (#78): a limping PEER runs the gimp machines too // GIMP routing (#78): a limping PEER runs the gimp machines too
// (the graphicAlarm level replicates with the damage state). // (the graphicAlarm level replicates with the damage state).
@@ -2456,6 +2465,9 @@ void
s_ga = 0.0f; s_ga = 0.0f;
DEBUG_STREAM << "[replgimp] ent=" << (int)GetEntityID() DEBUG_STREAM << "[replgimp] ent=" << (int)GetEntityID()
<< " gl=" << replGl << " gl=" << replGl
<< " bts=" << bodyTargetSpeed
<< " standSpeed=" << standSpeed
<< " bodyState=" << (int)bodyStateAlarm.GetLevel()
<< " sim=" << (int)GetSimulationState() << " sim=" << (int)GetSimulationState()
<< " hasClips=" << hasGimpClips << " zones:"; << " hasClips=" << hasGimpClips << " zones:";
for (int zi = 0; zi < damageZoneCount; ++zi) for (int zi = 0; zi < damageZoneCount; ++zi)
+1 -1
View File
@@ -51,6 +51,6 @@ PC=$!
sleep 480 sleep 480
kill $PC 2>/dev/null kill $PC 2>/dev/null
taskkill //F //IM btl4.exe 2>/dev/null bt_kill_ours
rm -f MP4X.EGG rm -f MP4X.EGG
echo "=== DONE ===" echo "=== DONE ==="
+10 -4
View File
@@ -26,7 +26,13 @@ A=/c/git/bt411/scratchpad/night6/runs/$(date +%H%M%S); mkdir -p "$A"; mv matchlo
bt_novice_egg MP4L.EGG MP4LN.EGG bt_novice_egg MP4L.EGG MP4LN.EGG
observer () { # $1=log $2=affinity $3=port observer () { # $1=log $2=affinity $3=port
BT_MP_LOG=1 BT_SYNC_LOG=1 BT_REPL_LOG=1 BT_MATCHLOG=1 BT_AUDIO_SPATIAL=1 \ # STATIC + fully controllable from frame one. NO autopilot: BT_GOTO's
# handover is broken (after BT_GOTO_RELEASE the mech reads throttle 0 yet
# keeps advancing ~40u/s -- the control-mapper demand freezes instead of
# following the keyboard), so a released mech walks straight past whatever
# it was sent to watch. Map is arena1, the tightest-spawning map measured
# (pairwise 41-1055u vs grass ~1200u); the limper is the WHITE Black Hawk.
BT_MP_LOG=1 BT_SYNC_LOG=1 BT_REPL_LOG=1 BT_MATCHLOG=1 BT_AUDIO_SPATIAL=1 BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_TLOC_LOG=1 \
bt_launch "$1" MP4LN.EGG "$2" -net "$3" bt_launch "$1" MP4LN.EGG "$2" -net "$3"
} }
observer mp4l_d.log 0xC0 1801 observer mp4l_d.log 0xC0 1801
@@ -37,7 +43,7 @@ observer mp4l_b.log 0x0C 1601
sleep 1 sleep 1
# the LIMPER: symbolic leg zone, 2 ticks of 60 -> gimp (no kill) ~2s after # the LIMPER: symbolic leg zone, 2 ticks of 60 -> gimp (no kill) ~2s after
# alive; slow tight circle from t0 so it never leaves its spawn area. # alive; slow tight circle from t0 so it never leaves its spawn area.
BT_MP_LOG=1 BT_DMG_LOG=1 BT_MATCHLOG=1 BT_AUDIO_SPATIAL=1 \ BT_MP_LOG=1 BT_DMG_LOG=1 BT_MATCHLOG=1 BT_AUDIO_SPATIAL=1 BT_SCORE_LOG=1 BT_DEATH_LOG=1 BT_TLOC_LOG=1 \
BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \ BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \
BT_AUTODRIVE=0.4 BT_FORCE_TURN=0.45 \ BT_AUTODRIVE=0.4 BT_FORCE_TURN=0.45 \
bt_launch mp4l_a.log MP4LN.EGG 0x03 -net 1501 bt_launch mp4l_a.log MP4LN.EGG 0x03 -net 1501
@@ -46,8 +52,8 @@ python ../tools/btconsole.py MP4LN.EGG 127.0.0.1:1501 127.0.0.1:1601 \
127.0.0.1:1701 127.0.0.1:1801 & 127.0.0.1:1701 127.0.0.1:1801 &
PC=$! PC=$!
sleep 420 sleep 660
kill $PC 2>/dev/null kill $PC 2>/dev/null
taskkill //F //IM btl4.exe 2>/dev/null bt_kill_ours
rm -f MP4LN.EGG rm -f MP4LN.EGG
echo "=== DONE ===" echo "=== DONE ==="
+1 -1
View File
@@ -43,6 +43,6 @@ PC=$!
sleep 150 sleep 150
kill $PC 2>/dev/null kill $PC 2>/dev/null
taskkill //F //IM btl4.exe 2>/dev/null bt_kill_ours
rm -f MP4X.EGG rm -f MP4X.EGG
echo "=== DONE ===" echo "=== DONE ==="
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# g_circle regression only (extracted from mp_gimpAB.sh)
set -x
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^experience=.*/experience=novice/" MP.EGG > AB.EGG
rm -f ab_g_circle_obs.log ab_g_circle_limp.log
BT_MP_LOG=1 BT_REPL_LOG=1 BT_AUDIO_SPATIAL=1 BT_SYNC_LOG=1 BT_ANIMIND_CAP=100000 \
bt_launch ab_g_circle_obs.log AB.EGG 0x0C -net 1601
sleep 2
BT_FORCE_TURN=0.45 \
BT_MP_LOG=1 BT_DMG_LOG=1 BT_AUDIO_SPATIAL=1 BT_GROUND_LOG=1 BT_ANIMIND_CAP=100000 \
BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \
BT_AUTODRIVE=0.4 \
bt_launch ab_g_circle_limp.log AB.EGG 0x03 -net 1501
sleep 4
python ../tools/btconsole.py AB.EGG 127.0.0.1:1501 127.0.0.1:1601 &
PC=$!
sleep 180
kill $PC 2>/dev/null
bt_kill_ours
rm -f AB.EGG
echo "=== DONE ==="
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# #82 REPLICANT-LIMP A/B MATRIX: which factor breaks the peer's limp view --
# TURNING (replicant trn-arming outprioritizes the gimp cycle at gimp speeds)
# or TERRAIN (arena1 ground staggers -> type-5 knockdown broadcasts)?
#
# 4 configs x 2 nodes x ~3min, sequential:
# g_straight grass, limper walks straight (the old mp_skate baseline)
# g_circle grass, limper circles (run-6 shape)
# a_straight arena1, limper walks straight
# a_circle arena1, limper circles (run-13 shape = the user's repro)
#
# VERDICT per config, from logs alone (no eyes needed):
# observer [animind] inst=1 states 22-27 -> replicant LIMPS (good)
# observer trn(4)/knockdown(32) churn -> skating (bad)
# observer type=5 rx count + limper [knock] rows -> the stagger factor
set -x
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
run_config () { # $1=tag $2=map $3=circle(0|1)
local tag=$1 map=$2 circle=$3
# headless rig only: a leftover node from the previous config holds the -net
# port and null-runs this one -- sweep before every config
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
sed "s/^map=.*/map=$map/; s/^time=.*/time=day/; s/^experience=.*/experience=novice/" MP.EGG > AB.EGG
rm -f ab_${tag}_obs.log ab_${tag}_limp.log
# observer: static, replicant-diagnostics on
BT_MP_LOG=1 BT_REPL_LOG=1 BT_AUDIO_SPATIAL=1 BT_SYNC_LOG=1 BT_ANIMIND_CAP=100000 BT_GIMPFEED=1 BT_TRNTRAP=1 \
bt_launch ab_${tag}_obs.log AB.EGG 0x0C -net 1601
sleep 2
# limper: symbolic leg gimp ~2s after alive; straight or circling
if [ "$circle" = "1" ]; then
BT_FORCE_TURN=0.45 \
BT_MP_LOG=1 BT_DMG_LOG=1 BT_AUDIO_SPATIAL=1 BT_GROUND_LOG=1 BT_ANIMIND_CAP=100000 \
BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \
BT_AUTODRIVE=0.4 \
bt_launch ab_${tag}_limp.log AB.EGG 0x03 -net 1501
else
BT_MP_LOG=1 BT_DMG_LOG=1 BT_AUDIO_SPATIAL=1 BT_GROUND_LOG=1 BT_ANIMIND_CAP=100000 \
BT_SELF_DAMAGE=60 BT_SELF_DAMAGE_ZONE=leg BT_SELF_DAMAGE_TICKS=2 BT_SELF_DAMAGE_DELAY=2 \
BT_AUTODRIVE=0.4 \
bt_launch ab_${tag}_limp.log AB.EGG 0x03 -net 1501
fi
sleep 4
python ../tools/btconsole.py AB.EGG 127.0.0.1:1501 127.0.0.1:1601 &
local PC=$!
sleep 180
kill $PC 2>/dev/null
bt_kill_ours
sleep 3
}
run_config a_circle arena1 1
rm -f AB.EGG
echo "=== A/B VERDICTS ==="
for tag in a_circle; do
obs=ab_${tag}_obs.log
gimp=$(grep -a '\[animind\]' $obs 2>/dev/null | grep 'inst=1' | grep -c 'state=2[2-7] ')
trn=$(grep -a '\[animind\]' $obs 2>/dev/null | grep 'inst=1' | grep -c 'state=4 ')
kd=$(grep -a '\[animind\]' $obs 2>/dev/null | grep 'inst=1' | grep -c 'state=32 ')
t5=$(grep -a '\[mrec-rx\]' $obs 2>/dev/null | grep -c 'type=5 ')
knocks=$(grep -ac '\[knock\]' ab_${tag}_limp.log 2>/dev/null)
gsim=$(grep -a '\[gimp-sim\]' ab_${tag}_limp.log 2>/dev/null | head -1)
echo "VERDICT $tag: replicantGimp=$gimp trn=$trn knockdown=$kd type5rx=$t5 limperKnocks=$knocks ($gsim)"
done
echo "=== DONE ==="