Pain sweep: intensity x suppression time-to-break table, csc-built

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nibu 2026-11-06 12:16:13 +00:00
commit 4b54f207a7
2 changed files with 92 additions and 0 deletions

54
docs/pain-sweep.md Normal file
View file

@ -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
```

View file

@ -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());