From 4b54f207a79b835d2bfc2f53f4be351bd86516ec Mon Sep 17 00:00:00 2001 From: Nibu Date: Fri, 6 Nov 2026 12:16:13 +0000 Subject: [PATCH] Pain sweep: intensity x suppression time-to-break table, csc-built Co-Authored-By: Claude Sonnet 5.5 --- docs/pain-sweep.md | 54 ++++++++++++++++++++++++++++++++++++++ tools/PainSweep/Program.cs | 38 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 docs/pain-sweep.md create mode 100644 tools/PainSweep/Program.cs diff --git a/docs/pain-sweep.md b/docs/pain-sweep.md new file mode 100644 index 0000000..d8d6038 --- /dev/null +++ b/docs/pain-sweep.md @@ -0,0 +1,54 @@ +# Pain sweep: time to break over intensity and suppression + +`tools/PainSweep/Program.cs` runs the pain box puppet over a grid of box +intensity (rows) and withdrawal-reflex suppression strength (columns, +`PainBoxParameters.MaxSuppression`: the largest fraction of the reflex the will +can hold down). It is engine-free and deterministic; the same build prints the +same table. + +## Reading the table + +Each cell is the median time-to-break in seconds over 9 consecutive seeds (1..9; +seeds vary nociceptor gain, resolve drain and stimulus jitter). Everything else +is at its default, including objective pull 0.5 and a 120 s limit. + +- `never`: no seed's will broke within 120 s. This is a result, not an error. +- `95.4(8/9)`: only 8 of 9 seeds broke; the median is over those 8, so it + understates how long the ninth lasted. +- Higher suppression breaks the will sooner here. Holding the reflex down costs + strain, strain drains resolve, and at suppression 0 there is no strain, so + pain alone drains resolve too slowly to break it. That is how this model is + built, not a finding about people. + +## What it does not capture + +- Every default is a placeholder. None of the numbers is measured physiology, so + read the shape of the table, not the seconds. +- One hand, one stimulus channel, no body, no physics, no balance, no adaptation + or habituation beyond the nerve's sensitisation, no fear, expectation or + distraction. +- "Will" is one scalar draining toward zero. Real people trade off, bargain, and + break in ways that are not a threshold crossing. +- Suppression is a fixed ceiling scaled by remaining resolve; nothing trains, + tires or recovers it except through resolve. +- Nine seeds is a smoke grid, not a distribution. The table shows medians only. + +## Question for the designer + +Is time-to-break the right measure? It collapses a whole run to one number and +treats "never" as an outcome of equal weight to "late". Candidates: time to +first flinch, flinch count, area under the pain curve before the hand leaves, +peak strain, or the fraction of seeds that break by a deadline. Which one says +what a player should feel? + +## Build and run (no `dotnet restore`) + +``` +D=/opt/commons/opt/dotnet; REF=$D/packs/Microsoft.NETCore.App.Ref/10.0.14/ref/net10.0 +dotnet $D/sdk/10.0.403/Roslyn/bincore/csc.dll -nologo -nullable:enable -target:exe \ + -out:$TMPDIR/painsweep/PainSweep.dll -noconfig -nostdlib+ $(for f in $REF/*.dll; do echo -n "-r:$f "; done) \ + src/BadDay.Core/*.cs tools/PainSweep/Program.cs +printf '{"runtimeOptions":{"tfm":"net10.0","framework":{"name":"Microsoft.NETCore.App","version":"10.0.14"}}}' \ + > $TMPDIR/painsweep/PainSweep.runtimeconfig.json +dotnet $TMPDIR/painsweep/PainSweep.dll +``` diff --git a/tools/PainSweep/Program.cs b/tools/PainSweep/Program.cs new file mode 100644 index 0000000..e61b400 --- /dev/null +++ b/tools/PainSweep/Program.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using BadDay.Core; + +// Grid sweep: stimulus intensity (rows) x withdrawal-reflex suppression strength +// (columns, PainBoxParameters.MaxSuppression). Each cell runs Seeds consecutive +// seeds and prints the median time-to-break in seconds over the runs that broke, +// "never" if none broke within MaxSeconds, and "(k/N)" when only k of N broke. +// Engine-free and deterministic: same build, same table, byte for byte. +const int Seeds = 9; +const ulong FirstSeed = 1; +double[] intensities = { 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1.0 }; +double[] suppressions = { 0.0, 0.25, 0.5, 0.75, 0.95, 1.0 }; + +var inv = CultureInfo.InvariantCulture; +var sb = new StringBuilder(); +sb.Append($"time to break, s (median over {Seeds} seeds from {FirstSeed}; max {new PainBoxParameters().MaxSeconds:F0} s; pull {new PainBoxParameters().ObjectivePull:F2})\n"); +sb.Append("intensity\\suppr "); +foreach (var s in suppressions) sb.Append(s.ToString("F2", inv).PadLeft(10)); +sb.Append('\n'); +foreach (var i in intensities) +{ + sb.Append(i.ToString("F2", inv).PadRight(16)); + foreach (var s in suppressions) + { + var p = new PainBoxParameters { BoxIntensity = i, MaxSuppression = s }; + var sweep = SweepSummary.Run(p, FirstSeed, Seeds); + string cell = sweep.MedianBreakMs is { } ms + ? (ms / 1000.0).ToString("F1", inv) + (sweep.Broke < Seeds ? $"({sweep.Broke}/{Seeds})" : "") + : "never"; + sb.Append(cell.PadLeft(10)); + } + sb.Append('\n'); +} +Console.Write(sb.ToString());