worldbox-observer-mod/scripts/soak-audit-player-log.sh
DazedAnon 7d23f590db Enhance event handling and introduce soft duel mechanics.
- Update event catalog to include new decision intents for civilization founding
- Implement soft duel yield mechanics in InterestDirector for combat interactions
- Adjust scoring weights for soft duels and variety valve timings
- Refactor event reasoning to improve spell casting labels and decision handling
- Increment version to 0.28.33 in mod.json
2026-07-17 18:58:45 -05:00

250 lines
7.8 KiB
Bash
Executable file

#!/usr/bin/env bash
# Audit IdleSpectator Watching / DROP / BAD / STATE from Player.log.
#
# Usage:
# ./scripts/soak-audit-player-log.sh # last 120s wall wait, then audit new bytes
# ./scripts/soak-audit-player-log.sh 60 # wait 60s then audit
# ./scripts/soak-audit-player-log.sh --since-bytes N # audit from byte offset (no wait)
# ./scripts/soak-audit-player-log.sh --since-bytes N 60 # wait 60s, then audit from N
# ./scripts/soak-audit-player-log.sh --no-wait # audit empty window from current EOF
#
# Env:
# PLAYER_LOG - override Player.log path
set -euo pipefail
LOG="${PLAYER_LOG:-$HOME/.config/unity3d/mkarpenko/WorldBox/Player.log}"
SECONDS_WAIT=0
SINCE_BYTES=""
while [[ $# -gt 0 ]]; do
case "$1" in
--since-bytes)
SINCE_BYTES="${2:-}"
shift 2
;;
--no-wait)
SECONDS_WAIT=0
shift
;;
-h|--help)
sed -n '2,12p' "$0"
exit 0
;;
*)
if [[ "$1" =~ ^[0-9]+$ ]]; then
SECONDS_WAIT="$1"
else
echo "Unknown arg: $1" >&2
exit 2
fi
shift
;;
esac
done
if [[ ! -f "$LOG" ]]; then
echo "Player.log not found: $LOG" >&2
exit 1
fi
MARKED_FROM_EOF=0
if [[ -z "$SINCE_BYTES" ]]; then
SINCE_BYTES=$(wc -c < "$LOG" | tr -d ' ')
MARKED_FROM_EOF=1
fi
if [[ "$SECONDS_WAIT" -gt 0 ]]; then
echo "Marked offset $SINCE_BYTES; waiting ${SECONDS_WAIT}s (leave Idle Spectator running) ..."
sleep "$SECONDS_WAIT"
elif [[ "$MARKED_FROM_EOF" -eq 1 ]]; then
echo "No wait and no --since-bytes: auditing from current EOF (empty window)."
echo "Tip: ./scripts/soak-audit-player-log.sh 60"
fi
python3 - "$LOG" "$SINCE_BYTES" <<'PY'
import re, sys
from collections import Counter
from pathlib import Path
log_path = Path(sys.argv[1])
armed = int(sys.argv[2])
raw = log_path.read_bytes()[armed:].decode("utf-8", errors="replace")
lines = raw.splitlines()
def harness_noise(ln: str) -> bool:
if "[HARNESS]" not in ln and "[ASSERT]" not in ln:
return False
return "action=" in ln or "batch done" in ln or "inbox reset" in ln
watching = []
pairs = []
drops = []
bads = []
scenes = []
states = []
last = None
for ln in lines:
if harness_noise(ln):
continue
if "IdleSpectator" not in ln:
continue
if "Watching:" in ln:
tip = ln.split("Watching:", 1)[1].strip()
tip = re.sub(r"^\[\d+\]\s*", "", tip)
watching.append(tip)
last = {"tip": tip, "unit": None, "caption": None}
pairs.append(last)
elif "[CAPTION]" in ln and last is not None:
last["caption"] = ln.split("[CAPTION]", 1)[1].strip()
elif "[DROP]" in ln:
drops.append(ln.split("[DROP]", 1)[1].strip())
elif "[BAD]" in ln:
bads.append(ln.split("[BAD]", 1)[1].strip())
elif "Scene end" in ln:
scenes.append(ln.split("Scene end", 1)[1].strip())
elif "[STATE]" in ln:
states.append(ln)
if last is not None and last.get("unit") is None and "unit=" in ln:
last["unit"] = ln.split("unit=", 1)[1].strip()
drop_kinds = Counter()
for d in drops:
drop_kinds[d.split(":", 1)[0].strip()] += 1
focus_true = focus_false = 0
for s in states:
m = re.search(r"focus=(True|False)", s)
if not m:
continue
if m.group(1) == "False":
focus_false += 1
else:
focus_true += 1
def bucket(t: str) -> str:
tl = t.lower()
# Combat scale first (before generic fight / vs).
if tl.startswith("duel") or " duel " in f" {tl} " or tl.startswith("duel -"):
return "combat_duel"
if any(tl.startswith(p) for p in ("skirmish", "battle", "mass")) or " mass -" in f" {tl} ":
return "combat_multi"
if any(x in tl for x in ("fighting", "in a fight", " vs ")) and "war -" not in tl and "plot -" not in tl:
return "combat_other"
if "decides to start a new civilization" in tl or "decides to found" in tl:
return "decision_genesis_intent"
if any(x in tl for x in ("new city", "new kingdom", "founds a", "founded")):
return "meta_genesis"
if any(x in tl for x in ("war -", "kingdom", "rebellion")):
return "politics"
if any(x in tl for x in ("love", "smitten", "child", "parenthood", "mourns", "lover")):
return "relationship"
if any(x in tl for x in ("curs", "possess", "dream", "nightmare", "enchant", "stun")):
return "status"
if "new species" in tl or "appear" in tl or "hatch" in tl:
return "discovery"
if "plot" in tl or "decides to" in tl:
return "plot_or_decision"
if "fall" in tl and any(x in tl for x in ("tree", "plant", "bush", "flower", "maple")):
return "object_fall"
return "other"
buckets = Counter(bucket(t) for t in watching)
scene_reasons = Counter()
for s in scenes:
m = re.match(r"\(([^)]+)\)", s)
if m:
scene_reasons[m.group(1)] += 1
identity = sum(1 for d in drops if "identity_filter" in d)
unpresentable = sum(1 for d in drops if "unpresentable" in d)
placeholder = sum(1 for t in watching if "something interesting" in t.lower())
veg = [t for t in watching if bucket(t) == "object_fall"]
no_focus_bads = sum(1 for b in bads if "no focus" in b.lower())
mismatches = []
sleep_lag = []
for p in pairs:
tip = p["tip"]
unit = (p.get("unit") or "").strip()
cap = p.get("caption") or ""
if "fighting" in tip.lower() and "Sleeping" in cap:
sleep_lag.append((tip, cap))
if not tip or not unit:
continue
if "fighting" not in tip.lower() and "fight of" not in tip.lower():
continue
subj = tip.split(" is ", 1)[0].split(" · ", 1)[0].strip()
u0 = unit.split()[0]
if u0 and u0.lower() not in subj.lower():
mismatches.append((tip, unit, cap[:72]))
print(f"Player.log soak audit")
print(f" log={log_path}")
print(f" since_bytes={armed} chunk_lines={len(lines)}")
print()
print(f"Watching cuts: {len(watching)}")
print(f"Focus BADs: {no_focus_bads} (all BADs={len(bads)})")
print(f"focus True/False: {focus_true}/{focus_false}")
print(f"identity_filter: {identity}")
print(f"unpresentable: {unpresentable}")
print(f"placeholder tips: {placeholder}")
print(f"object falls: {len(veg)}")
print(f"fight tip/focus mismatches: {len(mismatches)}")
print(f"fighting+Sleeping lag: {len(sleep_lag)}")
print()
if buckets:
print("Buckets:")
for k, v in buckets.most_common():
print(f" {k:16} {v}")
print()
if drop_kinds:
print("Drop kinds:")
for k, v in drop_kinds.most_common(12):
print(f" {k:24} {v}")
print()
if scene_reasons:
print("Scene ends:")
for k, v in scene_reasons.most_common():
print(f" {k:16} {v}")
print()
if watching:
print("Tips:")
for t in watching:
print(f" - {t}")
print()
if mismatches:
print("Fight tip/focus mismatches:")
for tip, unit, cap in mismatches:
print(f" tip={tip!r} focus={unit!r} caption={cap!r}")
print()
if sleep_lag:
print("Fight tip + Sleeping caption:")
for tip, cap in sleep_lag:
print(f" tip={tip!r} caption={cap!r}")
print()
genesis_intent = buckets.get("decision_genesis_intent", 0)
duel_n = buckets.get("combat_duel", 0)
multi_n = buckets.get("combat_multi", 0)
combat_total = duel_n + multi_n + buckets.get("combat_other", 0)
print(f"Variety: duel={duel_n} multi={multi_n} combat_total={combat_total} genesis_intent={genesis_intent}")
if genesis_intent:
print(" NOTE: decision_genesis_intent tips should be 0 after 0.28.28 (meta outcomes only).")
if combat_total >= 4 and multi_n == 0 and duel_n >= 3:
print(" NOTE: combat tips are Duel-heavy; multi scrap may be scarce in this world.")
print()
ok = (
no_focus_bads == 0
and focus_false == 0
and identity == 0
and placeholder == 0
and len(veg) == 0
and len(mismatches) == 0
and len(sleep_lag) == 0
and genesis_intent == 0
)
print("VERDICT:", "PASS" if ok else "REVIEW")
sys.exit(0 if ok else 1)
PY