/*
===============================================================================
DETERMINISTIC BLACKJACK DECISION ENGINE RESEARCH
PHASE 3.55 - TIER ENTRY, RETENTION AND ATTRITION

CODE VERSION: Deterministic Blackjack Phase 3.55
CODE BUILD TAG: DBDE-3.55-TIER-RETENTION-ATTRITION

RESEARCH DIRECTOR: Amit Amlani
PUBLICATION AUTHOR: ChatGPT (OpenAI)

STATUS
Exploratory bankroll-risk / player-behaviour simulation. This phase does not
claim that any next hand is more likely to win.

QUESTION
Phase 3.38 found that cooling earlier than <=100 creates many more rebuilds
without improving 2x target attainment. Phase 3.39 therefore freezes the
strongest cooling point at <=100 and changes only how far the bankroll must
recover before the player leaves stable 10-unit rebuilding and resumes the
profit-spending/aggressive ladder: >=120, >=130 or >=140.
===============================================================================
*/
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.MessageDigest;
import java.util.*;

public final class DeterministicBlackjackPhase3100 {
    static final int SESSIONS = 5000;
    static final int HANDS = 30;
    static final int SHOE_SIZE = 312;
    static final double START_BANK = 100.0;
    static final double BASE_WAGER = 1.0;
    static final long PRIMARY_SEED = 202201L;
    static final long HOLDOUT_SEED = 202301L;

    enum Action { HIT, STAND, DOUBLE, SPLIT, NONE }
    enum Profile { TEXTBOOK, MAXIMUM_AGGRESSION, SELECTIVE_AGGRESSION }

    static final class Hand {
        final ArrayList<Integer> c = new ArrayList<>();
        int total(){int t=0,a=0;for(int v:c){t+=v;if(v==11)a++;}while(t>21&&a-->0)t-=10;return t;}
        boolean soft(){int raw=0;for(int v:c)raw+=v;return c.contains(11)&&raw==total();}
        boolean bust(){return total()>21;}
        boolean blackjack(){return c.size()==2&&total()==21;}
        int cards(){return c.size();}
    }

    static final class ResolvedHandRecord {
        String cohort;
        int session, round, child;
        boolean splitChild;
        int startTotal;
        boolean startSoft;
        int dealerUp;
        String visibleClass;
        Action textbookAction;
        Action aggressiveAction;
        int startCards;
        int endCards;
        int dealerEndCards;
        boolean playerNaturalBlackjack;
        boolean dealerNaturalBlackjack;
        double baseWager;
        boolean selectiveWagerTrigger;
        double stake;
        boolean thirdCardTaken;
        boolean thirdCardBust;
        boolean bust;
        String outcome;
        double net;
        int startPointer, endPointer;

        String csv(){
            return String.join(",",
                cohort,Integer.toString(session),Integer.toString(round),Integer.toString(child),Boolean.toString(splitChild),
                Integer.toString(startTotal),Boolean.toString(startSoft),Integer.toString(dealerUp),visibleClass,
                textbookAction.toString(),aggressiveAction.toString(),Integer.toString(startCards),Integer.toString(endCards),Integer.toString(dealerEndCards),Boolean.toString(playerNaturalBlackjack),Boolean.toString(dealerNaturalBlackjack),String.format(Locale.ROOT,"%.1f",baseWager),Boolean.toString(selectiveWagerTrigger),
                String.format(Locale.ROOT,"%.1f",stake),Boolean.toString(thirdCardTaken),Boolean.toString(thirdCardBust),
                Boolean.toString(bust),outcome,String.format(Locale.ROOT,"%.1f",net),Integer.toString(startPointer),Integer.toString(endPointer));
        }
    }

    static final class Round {
        double net;
        int endPointer;
        int doubles, splits;
        boolean win, loss, push;
        final ArrayList<ResolvedHandRecord> records = new ArrayList<>();
    }

    static final class PairStats {
        long sessions,tPositive,tNegative,tEven,aPositive,aNegative,aEven,tHigher,aHigher,tie;
        long t2x,t3x,t5x,t10x,a2x,a3x,a5x,a10x;
        double tEnd,aEnd;
        long tWins,tLosses,tPushes,aWins,aLosses,aPushes,tDoubles,aDoubles,tSplits,aSplits;
        long diverged,firstDivSum,sameStart,totalStarts,finalSame,tAhead,aAhead,absDiff,tCards,aCards;
        int maxT,maxA;
        double maxBankT=START_BANK,maxBankA=START_BANK;
        int maxBankTHand=0,maxBankAHand=0,maxBankTSession=0,maxBankASession=0;
        final double[] aBankAfterHand=new double[HANDS+1];
        double sumSessionPeakA,sumMaxDrawdownA;
        long aEver2x,aEver3x,aEver5x,aEver10x,aEverZero,aBankruptcyStops,aHandsSkippedAfterBankruptcy;
        long affordabilityBaseStops,affordabilitySplitFallbacks,affordabilityDoubleFallbacks;
        long selective2xTriggers,selective2xWins,selective2xLosses,selective2xPushes;
        double selective2xNet;
        final long[] peakHandCountsA=new long[HANDS+1];
    }

    static final class StateKey implements Comparable<StateKey> {
        final int total, up;
        final boolean soft, splitChild;
        final Action textbook;
        StateKey(int total,boolean soft,int up,boolean splitChild,Action textbook){this.total=total;this.soft=soft;this.up=up;this.splitChild=splitChild;this.textbook=textbook;}
        public int compareTo(StateKey o){int c=Integer.compare(total,o.total);if(c!=0)return c;c=Boolean.compare(soft,o.soft);if(c!=0)return c;c=Integer.compare(up,o.up);if(c!=0)return c;c=Boolean.compare(splitChild,o.splitChild);if(c!=0)return c;return textbook.compareTo(o.textbook);}
        public boolean equals(Object x){if(!(x instanceof StateKey))return false;StateKey o=(StateKey)x;return total==o.total&&soft==o.soft&&up==o.up&&splitChild==o.splitChild&&textbook==o.textbook;}
        public int hashCode(){return Objects.hash(total,soft,up,splitChild,textbook);}
        String label(){return (soft?"SOFT ":"HARD ")+total+" vs "+up+(splitChild?" [split-child]":"")+" | textbook="+textbook;}
    }

    static final class StateAgg {
        long hands,wins,losses,pushes,busts,thirdTaken,thirdBust;
        double net,stake;
        void add(ResolvedHandRecord r){hands++;net+=r.net;stake+=r.stake;if(r.outcome.equals("WIN"))wins++;else if(r.outcome.equals("LOSS"))losses++;else pushes++;if(r.bust)busts++;if(r.thirdCardTaken)thirdTaken++;if(r.thirdCardBust)thirdBust++;}
    }

    static long sessionSeed(long cohort,int s){return cohort+s*1_000_003L;}

    static int[] sixDeckShoe(long seed){
        int[] a=new int[SHOE_SIZE];int p=0;
        for(int deck=0;deck<6;deck++)for(int suit=0;suit<4;suit++)for(int rank=1;rank<=13;rank++)a[p++]=rank==1?11:Math.min(rank,10);
        Random r=new Random(seed);
        for(int i=a.length-1;i>0;i--){int j=r.nextInt(i+1);int t=a[i];a[i]=a[j];a[j]=t;}
        return a;
    }

    static boolean auditShoe(int[] s){
        if(s.length!=312)return false;int ace=0,ten=0;int[] n=new int[10];
        for(int v:s){if(v==11)ace++;else if(v==10)ten++;else if(v>=2&&v<=9)n[v]++;else return false;}
        if(ace!=24||ten!=96)return false;for(int v=2;v<=9;v++)if(n[v]!=24)return false;return true;
    }

    static int draw(int[] shoe,int[] ptr){if(ptr[0]>=shoe.length)throw new IllegalStateException("Shoe exhausted at pointer "+ptr[0]);return shoe[ptr[0]++];}

    static Action textbook(Hand h,int up,boolean first){
        int t=h.total();
        if(h.soft()){
            if(t>=19)return Action.STAND;
            if(t==18)return first&&up>=3&&up<=6?Action.DOUBLE:(up>=9?Action.HIT:Action.STAND);
            if(t==17)return first&&up>=3&&up<=6?Action.DOUBLE:Action.HIT;
            if(t==15||t==16)return first&&up>=4&&up<=6?Action.DOUBLE:Action.HIT;
            if(t==13||t==14)return first&&up>=5&&up<=6?Action.DOUBLE:Action.HIT;
            return Action.HIT;
        }
        if(t>=17)return Action.STAND;
        if(t>=13)return up>=2&&up<=6?Action.STAND:Action.HIT;
        if(t==12)return up>=4&&up<=6?Action.STAND:Action.HIT;
        if(t==11)return first&&up!=11?Action.DOUBLE:Action.HIT;
        if(t==10)return first&&up<=9?Action.DOUBLE:Action.HIT;
        if(t==9)return first&&up>=3&&up<=6?Action.DOUBLE:Action.HIT;
        return Action.HIT;
    }

    static boolean textbookSplit(int pair,int up){
        if(pair==11||pair==8)return true;if(pair==10||pair==5)return false;
        if(pair==9)return up!=7&&up!=10&&up!=11;if(pair==7)return up>=2&&up<=7;
        if(pair==6)return up>=2&&up<=6;if(pair==4)return up==5||up==6;
        if(pair==3||pair==2)return up>=2&&up<=7;return false;
    }

    // Rank normalisation collapses 10/J/Q/K to value 10. Therefore K+J,
    // Q+10, J+K, etc. arrive here as 10+10 and are split-eligible.
    static boolean splitEligibleByStoredValue(int firstCard,int secondCard){
        return firstCard==secondCard;
    }

    static String tenValueSplitAudit(){
        int ten=10,jack=10,queen=10,king=10;
        int[][] cases={{ten,jack},{ten,queen},{ten,king},{jack,queen},{jack,king},{queen,king},{ten,ten},{jack,jack},{queen,queen},{king,king}};
        String[] names={"10+J","10+Q","10+K","J+Q","J+K","Q+K","10+10","J+J","Q+Q","K+K"};
        StringBuilder b=new StringBuilder("PHASE 3.02 — TEN-VALUE SPLIT PRECEDENCE AUDIT\n\n");
        b.append("Stored-card rule: 10, J, Q and K are all represented as integer value 10.\n");
        b.append("Maximum Aggression precedence: SPLIT eligible initial pair first; only otherwise consider first-decision DOUBLE.\n\n");
        for(int i=0;i<cases.length;i++){
            boolean ok=splitEligibleByStoredValue(cases[i][0],cases[i][1]);
            b.append(String.format(Locale.ROOT,"%-5s -> stored %d+%d -> split eligible = %s -> first aggressive action = %s%n",
                    names[i],cases[i][0],cases[i][1],ok,ok?"SPLIT":"DOUBLE-CANDIDATE"));
            if(!ok)throw new IllegalStateException("Ten-value split audit failed for "+names[i]);
        }
        b.append("\nRESULT: PASS — every two-card ten-value combination is routed to SPLIT before DOUBLE.\n");
        return b.toString();
    }

    static String visibleClass(Hand h){return h.soft()?"SOFT":"HARD";}

    static Round playRound(int[] shoe,int pointer,Profile profile,String cohort,int session,int round,boolean capture,double availableBank,PairStats stats,double wager,boolean selectiveWagerTrigger){
        int[] ptr={pointer};Hand initial=new Hand(),dealer=new Hand();
        initial.c.add(draw(shoe,ptr));dealer.c.add(draw(shoe,ptr));initial.c.add(draw(shoe,ptr));dealer.c.add(draw(shoe,ptr));
        boolean pair=splitEligibleByStoredValue(initial.c.get(0),initial.c.get(1));
        boolean aggressive=profile==Profile.MAXIMUM_AGGRESSION||profile==Profile.SELECTIVE_AGGRESSION;
        boolean selective=profile==Profile.SELECTIVE_AGGRESSION;
        boolean wantsSplit=pair&&(aggressive||textbookSplit(initial.c.get(0),dealer.c.get(0)));
        if(capture && wantsSplit && availableBank < 2.0*wager){
            wantsSplit=false;
            if(stats!=null)stats.affordabilitySplitFallbacks++;
        }
        ArrayList<Hand> hands=new ArrayList<>();ArrayList<Double> stakes=new ArrayList<>();ArrayList<Boolean> natural=new ArrayList<>();ArrayList<Boolean> splitChild=new ArrayList<>();
        Round out=new Round();
        if(wantsSplit){
            out.splits=1;Hand a=new Hand(),b=new Hand();a.c.add(initial.c.get(0));b.c.add(initial.c.get(1));a.c.add(draw(shoe,ptr));b.c.add(draw(shoe,ptr));
            hands.add(a);hands.add(b);stakes.add(wager);stakes.add(wager);natural.add(false);natural.add(false);splitChild.add(true);splitChild.add(true);
        } else {hands.add(initial);stakes.add(wager);natural.add(true);splitChild.add(false);}

        double committedCapital=hands.size()*wager;
        for(int i=0;i<hands.size();i++){
            Hand h=hands.get(i);boolean first=true;
            ResolvedHandRecord rec=null;
            if(capture){
                rec=new ResolvedHandRecord();rec.cohort=cohort;rec.session=session;rec.round=round;rec.child=i+1;rec.splitChild=splitChild.get(i);
                rec.startTotal=h.total();rec.startSoft=h.soft();rec.dealerUp=dealer.c.get(0);rec.visibleClass=visibleClass(h);rec.startCards=h.cards();rec.baseWager=wager;rec.selectiveWagerTrigger=selectiveWagerTrigger;rec.playerNaturalBlackjack=(!splitChild.get(i)&&h.blackjack());rec.dealerNaturalBlackjack=dealer.blackjack();rec.startPointer=ptr[0];
                if(h.blackjack()||dealer.blackjack()){rec.textbookAction=Action.NONE;rec.aggressiveAction=Action.NONE;}
                else {rec.textbookAction=textbook(h,dealer.c.get(0),true);rec.aggressiveAction=aggressive?((selective&&h.soft()&&h.total()>=18&&h.total()<=19&&dealer.c.get(0)>=9)?textbook(h,dealer.c.get(0),true):(selective&&!h.soft()&&h.total()>=5&&h.total()<=8&&dealer.c.get(0)>=2&&dealer.c.get(0)<=6)?Action.HIT:(selective&&!h.soft()&&h.total()==9&&dealer.c.get(0)>=9)?Action.HIT:(selective&&!h.soft()&&h.total()==12&&dealer.c.get(0)>=2&&dealer.c.get(0)<=6)?textbook(h,dealer.c.get(0),true):(selective&&h.soft()&&h.total()>=13&&h.total()<=17&&dealer.c.get(0)>=7)?Action.HIT:(selective&&!h.soft()&&h.total()==13&&dealer.c.get(0)>=2&&dealer.c.get(0)<=6)?Action.STAND:(selective&&!h.soft()&&h.total()==8&&dealer.c.get(0)>=7)?Action.HIT:(selective&&!h.soft()&&h.total()>=12&&h.total()<=13&&dealer.c.get(0)>=7)?Action.HIT:(selective&&!h.soft()&&h.total()>=5&&h.total()<=7&&dealer.c.get(0)>=7)?Action.HIT:(selective&&!h.soft()&&h.total()==14)?textbook(h,dealer.c.get(0),true):(selective&&!h.soft()&&h.total()==15)?textbook(h,dealer.c.get(0),true):(selective&&!h.soft()&&h.total()==16)?textbook(h,dealer.c.get(0),true):(selective&&!h.soft()&&splitChild.get(i)&&h.total()==16&&dealer.c.get(0)==11)?Action.HIT:((selective&&!h.soft()&&(h.total()==19||h.total()==18||h.total()==17||(splitChild.get(i)&&h.total()==20)))?Action.STAND:Action.DOUBLE)):rec.textbookAction;}
            }
            if(!h.blackjack()&&!dealer.blackjack())while(!h.bust()){
                Action a=(aggressive&&first)?((selective&&h.soft()&&h.total()>=18&&h.total()<=19&&dealer.c.get(0)>=9)?textbook(h,dealer.c.get(0),true):(selective&&!h.soft()&&h.total()>=5&&h.total()<=8&&dealer.c.get(0)>=2&&dealer.c.get(0)<=6)?Action.HIT:(selective&&!h.soft()&&h.total()==9&&dealer.c.get(0)>=9)?Action.HIT:(selective&&!h.soft()&&h.total()==12&&dealer.c.get(0)>=2&&dealer.c.get(0)<=6)?textbook(h,dealer.c.get(0),true):(selective&&h.soft()&&h.total()>=13&&h.total()<=17&&dealer.c.get(0)>=7)?Action.HIT:(selective&&!h.soft()&&h.total()==13&&dealer.c.get(0)>=2&&dealer.c.get(0)<=6)?Action.STAND:(selective&&!h.soft()&&h.total()==8&&dealer.c.get(0)>=7)?Action.HIT:(selective&&!h.soft()&&h.total()>=12&&h.total()<=13&&dealer.c.get(0)>=7)?Action.HIT:(selective&&!h.soft()&&h.total()>=5&&h.total()<=7&&dealer.c.get(0)>=7)?Action.HIT:(selective&&!h.soft()&&h.total()==14)?textbook(h,dealer.c.get(0),true):(selective&&!h.soft()&&h.total()==15)?textbook(h,dealer.c.get(0),true):(selective&&!h.soft()&&h.total()==16)?textbook(h,dealer.c.get(0),true):(selective&&!h.soft()&&splitChild.get(i)&&h.total()==16&&dealer.c.get(0)==11)?Action.HIT:((selective&&!h.soft()&&(h.total()==19||h.total()==18||h.total()==17||(splitChild.get(i)&&h.total()==20)))?Action.STAND:Action.DOUBLE)):textbook(h,dealer.c.get(0),first);
                if(a==Action.STAND)break;
                if(a==Action.DOUBLE&&first){
                    if(capture && availableBank < committedCapital + wager){
                        if(stats!=null)stats.affordabilityDoubleFallbacks++;
                        Action fb=textbook(h,dealer.c.get(0),true);
                        if(fb==Action.DOUBLE)fb=Action.HIT;
                        if(fb==Action.STAND)break;
                        h.c.add(draw(shoe,ptr));first=false;
                        continue;
                    }
                    stakes.set(i,stakes.get(i)*2.0);committedCapital+=wager;out.doubles++;h.c.add(draw(shoe,ptr));break;
                }
                h.c.add(draw(shoe,ptr));first=false;
            }
            if(capture){rec.endCards=h.cards();rec.stake=stakes.get(i);rec.thirdCardTaken=rec.startCards==2&&rec.endCards>=3;rec.thirdCardBust=rec.thirdCardTaken&&h.bust()&&rec.endCards==3;rec.bust=h.bust();rec.endPointer=ptr[0];out.records.add(rec);}
        }

        boolean live=false;for(Hand h:hands)if(!h.bust())live=true;
        if(live&&!dealer.blackjack())while(dealer.total()<17)dealer.c.add(draw(shoe,ptr));
        for(int i=0;i<hands.size();i++){
            Hand p=hands.get(i);double st=stakes.get(i),net;
            if(natural.get(i)&&p.blackjack()&&!dealer.blackjack())net=1.5*st;
            else if(natural.get(i)&&dealer.blackjack()&&!p.blackjack())net=-st;
            else if(natural.get(i)&&p.blackjack()&&dealer.blackjack())net=0;
            else if(p.bust())net=-st;
            else if(dealer.bust()||p.total()>dealer.total())net=st;
            else if(p.total()<dealer.total())net=-st;
            else net=0;
            out.net+=net;
            if(capture){ResolvedHandRecord rec=out.records.get(i);rec.net=net;rec.outcome=net>0?"WIN":net<0?"LOSS":"PUSH";rec.dealerEndCards=dealer.cards();rec.endPointer=ptr[0];}
        }
        out.win=out.net>0;out.loss=out.net<0;out.push=out.net==0;out.endPointer=ptr[0];return out;
    }

    static PairStats paired(long cohort,String name,List<ResolvedHandRecord> records){
        PairStats s=new PairStats();
        for(int session=0;session<SESSIONS;session++){
            int[] shoe=sixDeckShoe(sessionSeed(cohort,session));if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
            int pt=0,pa=0;double bt=START_BANK,ba=START_BANK;boolean div=false;int first=0;long tw=0,tl=0,tp=0,aw=0,al=0,ap=0,td=0,ad=0,ts=0,as=0;
            double sessionPeakA=START_BANK,maxDrawdownA=0.0;int sessionPeakHandA=0;
            boolean ever2xA=false,ever3xA=false,ever5xA=false,ever10xA=false,everZeroA=false;
            boolean selectiveStopped=false;
            ArrayDeque<Double> last3UnitNet=new ArrayDeque<>();
            for(int h=1;h<=HANDS;h++){
                Round rt=playRound(shoe,pt,Profile.TEXTBOOK,name,session+1,h,false,Double.POSITIVE_INFINITY,null,BASE_WAGER,false);
                pt=rt.endPointer;bt+=rt.net;
                if(rt.win)tw++;else if(rt.loss)tl++;else tp++;td+=rt.doubles;ts+=rt.splits;
                if(bt>s.maxBankT){s.maxBankT=bt;s.maxBankTHand=h;s.maxBankTSession=session+1;}

                double recent3=0.0;
                for(double v:last3UnitNet)recent3+=v;
                double drawdownBefore=Math.max(0.0,sessionPeakA-ba);
                boolean wagerTrigger=false;
                double currentWager=BASE_WAGER;
                if(!selectiveStopped){
                    if(ba < currentWager){
                        selectiveStopped=true;s.affordabilityBaseStops++;
                        s.aHandsSkippedAfterBankruptcy+=(HANDS-h+1);
                    }
                }
                if(!selectiveStopped){
                    s.totalStarts++;if(pt==pa)s.sameStart++;
                    Round ra=playRound(shoe,pa,Profile.SELECTIVE_AGGRESSION,name,session+1,h,true,ba,s,currentWager,wagerTrigger);
                    records.addAll(ra.records);
                    pa=ra.endPointer;ba+=ra.net;
                    if(wagerTrigger){
                        s.selective2xTriggers++;s.selective2xNet+=ra.net;
                        if(ra.win)s.selective2xWins++;else if(ra.loss)s.selective2xLosses++;else s.selective2xPushes++;
                    }
                    double unitEquivalentNet=ra.net/currentWager;
                    last3UnitNet.addLast(unitEquivalentNet);
                    if(last3UnitNet.size()>3)last3UnitNet.removeFirst();
                    if(ra.win)aw++;else if(ra.loss)al++;else ap++;ad+=ra.doubles;as+=ra.splits;
                    if(ba>sessionPeakA){sessionPeakA=ba;sessionPeakHandA=h;}
                    maxDrawdownA=Math.max(maxDrawdownA,sessionPeakA-ba);
                    if(ba>=200)ever2xA=true;if(ba>=300)ever3xA=true;if(ba>=500)ever5xA=true;if(ba>=1000)ever10xA=true;
                    if(ba<=0){
                        everZeroA=true;selectiveStopped=true;s.aBankruptcyStops++;
                        s.aHandsSkippedAfterBankruptcy+=(HANDS-h);
                        ba=0.0;
                    }
                    if(ba>s.maxBankA){s.maxBankA=ba;s.maxBankAHand=h;s.maxBankASession=session+1;}
                    if(!div&&pt!=pa){div=true;first=h;}
                }
                s.aBankAfterHand[h]+=ba;
            }
            if(pt>SHOE_SIZE||pa>SHOE_SIZE)throw new IllegalStateException("Shoe exhausted");
            s.sessions++;s.tEnd+=bt;s.aEnd+=ba;s.tWins+=tw;s.tLosses+=tl;s.tPushes+=tp;s.aWins+=aw;s.aLosses+=al;s.aPushes+=ap;s.tDoubles+=td;s.aDoubles+=ad;s.tSplits+=ts;s.aSplits+=as;
            if(bt>START_BANK)s.tPositive++;else if(bt<START_BANK)s.tNegative++;else s.tEven++;if(ba>START_BANK)s.aPositive++;else if(ba<START_BANK)s.aNegative++;else s.aEven++;
            if(bt>=200)s.t2x++;if(bt>=300)s.t3x++;if(bt>=500)s.t5x++;if(bt>=1000)s.t10x++;
            if(ba>=200)s.a2x++;if(ba>=300)s.a3x++;if(ba>=500)s.a5x++;if(ba>=1000)s.a10x++;
            if(bt>ba)s.tHigher++;else if(ba>bt)s.aHigher++;else s.tie++;
            s.sumSessionPeakA+=sessionPeakA;s.sumMaxDrawdownA+=maxDrawdownA;s.peakHandCountsA[sessionPeakHandA]++;
            if(ever2xA)s.aEver2x++;if(ever3xA)s.aEver3x++;if(ever5xA)s.aEver5x++;if(ever10xA)s.aEver10x++;if(everZeroA)s.aEverZero++;
            if(div){s.diverged++;s.firstDivSum+=first;}if(pt==pa)s.finalSame++;else if(pt>pa)s.tAhead++;else s.aAhead++;s.absDiff+=Math.abs(pt-pa);s.tCards+=pt;s.aCards+=pa;s.maxT=Math.max(s.maxT,pt);s.maxA=Math.max(s.maxA,pa);
        }
        return s;
    }

    static String pct(long a,long b){return String.format(Locale.ROOT,"%.2f%%",b==0?0:100.0*a/b);}
    static String avg(double a,long b){return String.format(Locale.ROOT,"%.3f",b==0?0:a/b);}

    static String baseline(PairStats p,PairStats h){
        StringBuilder r=new StringBuilder();
        r.append("PHASE 3.35 SECOND INTERVENTION RESULTS\n\n");
        for(Object[] z:new Object[][]{{"PRIMARY",p},{"HOLDOUT",h}}){String n=(String)z[0];PairStats s=(PairStats)z[1];
            r.append(n).append("\n");
            r.append("Average ending bankroll Textbook/Selective: ").append(avg(s.tEnd,s.sessions)).append(" / ").append(avg(s.aEnd,s.sessions)).append("\n");
            r.append(String.format(Locale.ROOT,"Maximum bankroll observed Textbook/Selective: %.1f at hand %d (session %d) / %.1f at hand %d (session %d)%n",
                s.maxBankT,s.maxBankTHand,s.maxBankTSession,s.maxBankA,s.maxBankAHand,s.maxBankASession));
            r.append("Positive sessions Textbook/Selective: ").append(pct(s.tPositive,s.sessions)).append(" / ").append(pct(s.aPositive,s.sessions)).append("\n");
            r.append("Affordability base stops: ").append(s.affordabilityBaseStops).append("\n");
            r.append("Affordability split fallbacks: ").append(s.affordabilitySplitFallbacks).append("\n");
            r.append("Affordability double fallbacks: ").append(s.affordabilityDoubleFallbacks).append("\n");
            r.append("Selective multiplier trigger wagers: ").append(s.selective2xTriggers).append("\n");
            r.append("Selective multiplier trigger net: ").append(String.format(Locale.ROOT,"%.1f",s.selective2xNet)).append("\n");
            r.append("Selective multiplier trigger win/loss/push: ").append(s.selective2xWins).append(" / ").append(s.selective2xLosses).append(" / ").append(s.selective2xPushes).append("\n");
            r.append("Paired winner Textbook/Selective/tie: ").append(pct(s.tHigher,s.sessions)).append(" / ").append(pct(s.aHigher,s.sessions)).append(" / ").append(pct(s.tie,s.sessions)).append("\n");
            r.append("Doubles Textbook/Selective: ").append(s.tDoubles).append(" / ").append(s.aDoubles).append("; splits Textbook/Selective: ").append(s.tSplits).append(" / ").append(s.aSplits).append("\n");
            r.append("Mean cards consumed Textbook/Selective: ").append(avg(s.tCards,s.sessions)).append(" / ").append(avg(s.aCards,s.sessions)).append("\n");
            r.append("Ending bankroll >=2x (200) Textbook/Selective: ").append(pct(s.t2x,s.sessions)).append(" / ").append(pct(s.a2x,s.sessions)).append("\n");
            r.append("Ending bankroll >=3x (300) Textbook/Selective: ").append(pct(s.t3x,s.sessions)).append(" / ").append(pct(s.a3x,s.sessions)).append("\n");
            r.append("Ending bankroll >=5x (500) Textbook/Selective: ").append(pct(s.t5x,s.sessions)).append(" / ").append(pct(s.a5x,s.sessions)).append("\n");
            r.append("Ending bankroll >=10x (1000) Textbook/Selective: ").append(pct(s.t10x,s.sessions)).append(" / ").append(pct(s.a10x,s.sessions)).append("\n\n");
        }
        return r.toString();
    }

    static String multiplierExperiment(PairStats p,PairStats h){
        StringBuilder r=new StringBuilder();
        r.append("PHASE 3.35 BANKRUPTCY-STOP CALIBRATION — CONSTANT 5x\n\n");
        r.append("SCIENTIFIC BOUNDARY\n");
        r.append("Decision policy: frozen Phase 3.20 checkpoint, validated Phase 3.21.\n");
        r.append("Wager architecture: constant 1-unit observational base wager (5x the Phase 3.22 baseline).\n");
        r.append("One multiplier intervention is active: the base wager is five times the Phase 3.22 baseline (5 units).\n\n");
        for(Object[] z:new Object[][]{{"PRIMARY",p},{"HOLDOUT",h}}){
            String n=(String)z[0];PairStats x=(PairStats)z[1];
            r.append(n).append("\n");
            r.append("Mean ending bankroll: ").append(avg(x.aEnd,x.sessions)).append("\n");
            r.append(String.format(Locale.ROOT,"Maximum bankroll observed anywhere: %.1f at Hand %d (session %d)%n",x.maxBankA,x.maxBankAHand,x.maxBankASession));
            r.append(String.format(Locale.ROOT,"Mean per-session peak bankroll: %.3f%n",x.sumSessionPeakA/x.sessions));
            r.append(String.format(Locale.ROOT,"Mean per-session maximum drawdown: %.3f units%n",x.sumMaxDrawdownA/x.sessions));
            r.append("Sessions ever reaching 2x / 3x / 5x / 10x: ")
             .append(pct(x.aEver2x,x.sessions)).append(" / ").append(pct(x.aEver3x,x.sessions)).append(" / ")
             .append(pct(x.aEver5x,x.sessions)).append(" / ").append(pct(x.aEver10x,x.sessions)).append("\n");
            r.append("Sessions ever reaching bankroll <=0: ").append(pct(x.aEverZero,x.sessions)).append("\n");
            int mode=1;for(int i=2;i<=HANDS;i++)if(x.peakHandCountsA[i]>x.peakHandCountsA[mode])mode=i;
            r.append("Sessions whose highest bankroll never exceeded the 100-unit start: ").append(x.peakHandCountsA[0]).append("\n");
            r.append("Most common positive hand-of-peak among sessions that exceeded start: Hand ").append(mode)
             .append(" [").append(x.peakHandCountsA[mode]).append(" sessions]\n\n");
        }
        r.append("MEAN SELECTIVE BANKROLL TRAJECTORY AFTER EACH HAND\n");
        r.append("Hand | Primary | Holdout\n");
        for(int i=1;i<=HANDS;i++)r.append(String.format(Locale.ROOT,"%2d | %8.3f | %8.3f%n",i,p.aBankAfterHand[i]/p.sessions,h.aBankAfterHand[i]/h.sessions));
        return r.toString();
    }

    static Map<StateKey,StateAgg> aggregate(List<ResolvedHandRecord> rr){
        TreeMap<StateKey,StateAgg> m=new TreeMap<>();
        for(ResolvedHandRecord r:rr){if(r.aggressiveAction==Action.NONE)continue;StateKey k=new StateKey(r.startTotal,r.startSoft,r.dealerUp,r.splitChild,r.textbookAction);m.computeIfAbsent(k,x->new StateAgg()).add(r);}return m;
    }

    static Map<StateKey,StateAgg> aggregateDivergent(List<ResolvedHandRecord> rr){
        TreeMap<StateKey,StateAgg> m=new TreeMap<>();
        for(ResolvedHandRecord r:rr){
            if(r.aggressiveAction==Action.NONE || r.aggressiveAction==r.textbookAction)continue;
            StateKey k=new StateKey(r.startTotal,r.startSoft,r.dealerUp,r.splitChild,r.textbookAction);
            m.computeIfAbsent(k,x->new StateAgg()).add(r);
        }
        return m;
    }

    static String stateReport(String cohort,List<ResolvedHandRecord> rr){
        Map<StateKey,StateAgg> m=aggregate(rr);
        ArrayList<Map.Entry<StateKey,StateAgg>> rows=new ArrayList<>(m.entrySet());
        rows.sort((a,b)->{
            double da=a.getValue().net/a.getValue().hands,db=b.getValue().net/b.getValue().hands;
            int c=Double.compare(da,db);if(c!=0)return c;return Long.compare(b.getValue().hands,a.getValue().hands);
        });
        StringBuilder r=new StringBuilder();
        r.append(cohort).append(" — WORST SELECTIVE-AGGRESSION VISIBLE STATES BY MEAN NET/HAND\n");
        r.append("Rank | Visible state | Hands | Mean net | Loss% | Bust% | 3rd-card bust% of 3rd-card draws | Mean stake\n");
        int rank=0;for(Map.Entry<StateKey,StateAgg> e:rows){if(++rank>25)break;StateAgg a=e.getValue();
            r.append(String.format(Locale.ROOT,"%2d | %-45s | %6d | %+7.3f | %6.2f%% | %6.2f%% | %6.2f%% | %.3f%n",
                rank,e.getKey().label(),a.hands,a.net/a.hands,100.0*a.losses/a.hands,100.0*a.busts/a.hands,a.thirdTaken==0?0:100.0*a.thirdBust/a.thirdTaken,a.stake/a.hands));
        }
        r.append("\n");return r.toString();
    }

    static String summary(List<ResolvedHandRecord> p,List<ResolvedHandRecord> h){
        long pHands=p.size(),hHands=h.size();long pThird=0,pThirdBust=0,hThird=0,hThirdBust=0,pBust=0,hBust=0;double pNet=0,hNet=0,pStake=0,hStake=0;
        EnumMap<Action,Long> pTextbook=new EnumMap<>(Action.class),hTextbook=new EnumMap<>(Action.class);for(Action a:Action.values()){pTextbook.put(a,0L);hTextbook.put(a,0L);}
        for(ResolvedHandRecord r:p){if(r.thirdCardTaken)pThird++;if(r.thirdCardBust)pThirdBust++;if(r.bust)pBust++;pNet+=r.net;pStake+=r.stake;pTextbook.put(r.textbookAction,pTextbook.get(r.textbookAction)+1);}
        for(ResolvedHandRecord r:h){if(r.thirdCardTaken)hThird++;if(r.thirdCardBust)hThirdBust++;if(r.bust)hBust++;hNet+=r.net;hStake+=r.stake;hTextbook.put(r.textbookAction,hTextbook.get(r.textbookAction)+1);}
        return "PHASE 3.35 — SELECTIVE-AGGRESSION INSTRUMENTATION SUMMARY\n\n"+
            "PRIMARY resolved selective-aggression hands: "+pHands+"; mean net/hand "+String.format(Locale.ROOT,"%+.4f",pNet/pHands)+"; mean committed stake "+String.format(Locale.ROOT,"%.4f",pStake/pHands)+"; bust rate "+pct(pBust,pHands)+"; third-card bust rate when a third card was taken "+pct(pThirdBust,pThird)+".\n"+
            "Textbook would have chosen on those same visible first-decision states: HIT "+pTextbook.get(Action.HIT)+", STAND "+pTextbook.get(Action.STAND)+", DOUBLE "+pTextbook.get(Action.DOUBLE)+".\n\n"+
            "HOLDOUT resolved selective-aggression hands: "+hHands+"; mean net/hand "+String.format(Locale.ROOT,"%+.4f",hNet/hHands)+"; mean committed stake "+String.format(Locale.ROOT,"%.4f",hStake/hHands)+"; bust rate "+pct(hBust,hHands)+"; third-card bust rate when a third card was taken "+pct(hThirdBust,hThird)+".\n"+
            "Textbook would have chosen on those same visible first-decision states: HIT "+hTextbook.get(Action.HIT)+", STAND "+hTextbook.get(Action.STAND)+", DOUBLE "+hTextbook.get(Action.DOUBLE)+".\n\n"+
            "INTERPRETATION\nPhase 3.35 carries forward split-child HARD 20 STAND, HARD 19 STAND, and split-child HARD 18 STAND. It adds one new family only: NO POLICY OR WAGER INTERVENTION; frozen policy carried forward at 1x. All other Maximum Aggression behaviour remains frozen.\n";
    }

    static String replicatedCandidates(List<ResolvedHandRecord> primary,List<ResolvedHandRecord> holdout){
        Map<StateKey,StateAgg> p=aggregateDivergent(primary),h=aggregateDivergent(holdout);
        final class C {StateKey k;StateAgg a,b;double score;C(StateKey k,StateAgg a,StateAgg b){this.k=k;this.a=a;this.b=b;this.score=(a.net/a.hands+b.net/b.hands)/2.0;}}
        ArrayList<C> c=new ArrayList<>();
        for(StateKey k:p.keySet())if(h.containsKey(k)){StateAgg a=p.get(k),b=h.get(k);if(a.hands>=100&&b.hands>=100&&a.net/a.hands<0&&b.net/b.hands<0)c.add(new C(k,a,b));}
        c.sort(Comparator.comparingDouble(x->x.score));
        StringBuilder r=new StringBuilder("PHASE 3.35 — REMAINING TRUE POLICY-DIVERGENCE STATES UNDER FROZEN 3.20 POLICY\n\n");
        r.append("Filter: aggregate ONLY hands where Selective Aggression's recorded first action differs from the frozen Textbook first action; then require the same visible state >=100 times in BOTH cohorts and negative mean net/hand in BOTH cohorts. This prevents already-protected states with negative realised outcomes from being mislabelled as remaining policy damage.\n\n");
        r.append("Rank | State | Primary n / net | Holdout n / net | P bust% / H bust% | Textbook first action\n");
        int rank=0;for(C x:c){if(++rank>20)break;r.append(String.format(Locale.ROOT,"%2d | %-34s | %5d / %+6.3f | %5d / %+6.3f | %6.2f%% / %6.2f%% | %s%n",rank,(x.k.soft?"SOFT ":"HARD ")+x.k.total+" vs "+x.k.up+(x.k.splitChild?" [split-child]":""),x.a.hands,x.a.net/x.a.hands,x.b.hands,x.b.net/x.b.hands,100.0*x.a.busts/x.a.hands,100.0*x.b.busts/x.b.hands,x.k.textbook));}
        r.append("\nDESIGN USE\nPhase 3.35 carries forward the validated split-child HARD 20, HARD 19 and split-child HARD 18 protections and adds no intervention; frozen decision policy is measured at the unchanged 1x wager. Remaining replicated damage states are retained for later one-rule-at-a-time interventions.\n");
        return r.toString();
    }

    static String methodology(){
        return "PHASE 3.35 — RUNNING TXT AUDIT RECORD\n\n"+
            "OBJECTIVE\nMake the fourth controlled stabilisation intervention while preserving the frozen architecture. Carry forward the three validated protections, then add one new policy family: HARD 16 follows its corresponding Textbook action rather than taking Maximum Aggression's automatic DOUBLE.\n\n"+
            "EVIDENCE HIERARCHY\n1. Primary design authority: finite six-deck Phase 2.xx evidence and new Phase 3.xx data.\n2. Earlier non-depleting/infinite-generator behaviour-profile data: historical corroboration and hypothesis generation only.\n3. Old behaviour-profile evidence must not directly define a new policy unless finite-shoe evidence independently supports the same conclusion.\n\n"+
            "FROZEN VARIABLES\nSix decks, deterministic session shuffle, 30 hands, 100-unit start, 1-unit base wager, dealer rules, blackjack payout, split limit, no in-session reshuffle, textbook control and Maximum Aggression policy are unchanged from Phase 2.02.\n\n"+
            "POLICY INFORMATION BOUNDARY\nThe future optimiser may use player hand composition/total, pair status, dealer upcard and legal actions. Phase 3.35 deliberately uses no previous-card-history, running count, remaining-rank inventory, hidden card or future-card feature. The accumulated policy uses only current visible states: split-child HARD 20, HARD 19, HARD 18, split-child HARD 16 vs Ace, and HARD 17. No card-history or composition feature is introduced.\n\n"+
            "PHASE 3.35 DECISION RULE\nCarry forward split-child HARD 20 STAND, HARD 19 STAND and split-child HARD 18 STAND. ONE additional Phase 3.35 intervention: if an ordinary first-decision hand is hard (not soft) and totals 18, Selective Aggression STANDS. Otherwise the frozen Maximum Aggression policy is unchanged. Textbook remains the control.\n";
    }

    static String hash(String s)throws Exception{MessageDigest md=MessageDigest.getInstance("SHA-256");byte[] d=md.digest(s.getBytes(StandardCharsets.UTF_8));StringBuilder b=new StringBuilder();for(byte x:d)b.append(String.format("%02x",x));return b.toString();}
    static void write(String n,String s)throws Exception{Files.writeString(Path.of(n),s,StandardCharsets.UTF_8);}
    static void writeCsv(String n,List<ResolvedHandRecord> rr)throws Exception{StringBuilder b=new StringBuilder("cohort,session,round,child,split_child,start_total,start_soft,dealer_up,visible_class,textbook_first_action,aggressive_first_action,start_cards,end_cards,dealer_end_cards,player_natural_blackjack,dealer_natural_blackjack,base_wager,selective_wager_trigger,stake,third_card_taken,third_card_bust,bust,outcome,net,start_pointer,end_pointer\n");for(ResolvedHandRecord r:rr)b.append(r.csv()).append('\n');write(n,b.toString());}
















    static int bal355(int[] h,int count){
        int n=Math.min(3,count),s=0;
        for(int i=0;i<n;i++)s+=h[(count-1-i+h.length)%h.length];
        return s;
    }
    static double normal355(int hand,double bank){
        if(hand<=8)return 10.0;
        if(bank<115)return 10.0;
        if(bank<130)return 15.0;
        if(bank<150)return 20.0;
        return 30.0;
    }
    static double cautious355(int[] hist,int count){
        int b=bal355(hist,count);
        return b<0?10.0:(b==0?15.0:17.5);
    }

    static final int[] TIERS356={100,110,120,130,140,150,160,170,180,190};

    static final class Recovery356 {
        int startHand, endHand;
        double startBank, trough, exposure;
        boolean recovered;
    }

    static final class Session356 {
        String cohort;
        int session, handsPlayed, targetHand, bankruptcyHand;
        double finalBank, peakBank, troughBank, maxDrawdown, cumulativeWager;
        double wagerBelow100, targetExposure, peakGiveBack;
        int handsBelow100, recoveryEpisodes, successfulRecoveries, failedRecoveries;
        long cardsConsumed, splits, doubles;
        long[] tierHands=new long[TIERS356.length];
        long[][] transitions=new long[TIERS356.length][TIERS356.length+2]; // + target / below100
        ArrayList<Recovery356> recoveries=new ArrayList<>();
    }

    static final class Cohort356 {
        String name;
        ArrayList<Session356> sessions=new ArrayList<>();
        Cohort356(String n){name=n;}
    }

    static int tier356(double bank){
        if(bank<100)return -1;
        if(bank>=200)return 10;
        int x=((int)Math.floor(bank)-100)/10;
        return Math.max(0,Math.min(9,x));
    }

    static double roundStake356(Round rr){
        double s=0.0;
        for(ResolvedHandRecord x:rr.records)s+=x.stake;
        return s;
    }

    static Cohort356 runCohort356(long seed,String cohort){
        Cohort356 out=new Cohort356(cohort);
        for(int session=0;session<SESSIONS;session++){
            int[] shoe=sixDeckShoe(sessionSeed(seed,session));
            if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");

            int ptr=0,hc=0; int[] hist=new int[5];
            double bank=START_BANK, peak=START_BANK, trough=START_BANK, maxDD=0.0;
            boolean everPlunge=false,cooling=false,cautious=false;
            boolean negativeEpisode=false,brakeArmed=false;
            Recovery356 activeRecovery=null;

            Session356 s=new Session356();
            s.cohort=cohort;s.session=session+1;

            for(int hand=1;hand<=HANDS;hand++){
                if(bank>=200||bank<=0)break;

                if(cooling&&bank>=120){cooling=false;cautious=true;}
                if(cautious&&bank>=140)cautious=false;

                double wager;
                if(cooling){
                    wager=10.0;negativeEpisode=false;brakeArmed=false;
                }else if(cautious){
                    wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;
                }else if(hand>8&&bank>=140){
                    int b=bal355(hist,hc);boolean neg=b<0;
                    if(neg&&!negativeEpisode)brakeArmed=true;
                    negativeEpisode=neg;
                    double base=bank>=170.0?15.0:normal355(hand,bank);
                    if(brakeArmed){wager=Math.min(base,15.0);brakeArmed=false;}else wager=base;
                }else{
                    wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;
                }
                wager=Math.min(wager,bank);
                if(wager<=0)break;
                if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;

                double before=bank;
                int beforeTier=tier356(before);
                if(beforeTier>=0&&beforeTier<10)s.tierHands[beforeTier]++;

                PairStats ps=new PairStats();
                int ptrBeforeRound=ptr;
                Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
                ptr=rr.endPointer;

                double committed=roundStake356(rr);
                s.cumulativeWager+=committed;
                s.cardsConsumed+=(rr.endPointer-ptrBeforeRound);
                s.splits+=rr.splits;s.doubles+=rr.doubles;

                bank+=rr.net;if(bank<0)bank=0;
                s.handsPlayed++;
                if(bank<100){s.handsBelow100++;s.wagerBelow100+=committed;}

                peak=Math.max(peak,bank);
                trough=Math.min(trough,bank);
                maxDD=Math.max(maxDD,peak-bank);

                int afterTier=tier356(bank);
                if(beforeTier>=0&&beforeTier<10){
                    if(afterTier==10)s.transitions[beforeTier][10]++;
                    else if(afterTier<0)s.transitions[beforeTier][11]++;
                    else s.transitions[beforeTier][afterTier]++;
                }

                // Recovery episode = first crossing from >=£100 to <£100,
                // ending only when £100 is regained or the session terminates.
                if(activeRecovery==null && before>=100 && bank<100){
                    activeRecovery=new Recovery356();
                    activeRecovery.startHand=hand;
                    activeRecovery.startBank=before;
                    activeRecovery.trough=bank;
                    activeRecovery.exposure=committed;
                    s.recoveryEpisodes++;
                }else if(activeRecovery!=null){
                    activeRecovery.trough=Math.min(activeRecovery.trough,bank);
                    activeRecovery.exposure+=committed;
                    if(bank>=100){
                        activeRecovery.recovered=true;
                        activeRecovery.endHand=hand;
                        s.successfulRecoveries++;
                        s.recoveries.add(activeRecovery);
                        activeRecovery=null;
                    }
                }

                hist[hc%hist.length]=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);hc++;

                if(bank>=200){
                    s.targetHand=hand;
                    s.targetExposure=s.cumulativeWager;
                    break;
                }
                if(bank<=0){
                    s.bankruptcyHand=hand;
                    break;
                }
                if(everPlunge&&bank<=100&&!cooling){
                    cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;
                }
            }

            if(activeRecovery!=null){
                activeRecovery.endHand=s.handsPlayed;
                activeRecovery.recovered=false;
                s.failedRecoveries++;
                s.recoveries.add(activeRecovery);
            }

            s.finalBank=bank;s.peakBank=peak;s.troughBank=trough;s.maxDrawdown=maxDD;
            s.peakGiveBack=Math.max(0.0,peak-bank);
            out.sessions.add(s);
        }
        return out;
    }

    static String f3(double x){return String.format(Locale.ROOT,"%.3f",x);}
    static String p356(long n,long d){return d==0?"0.00%":String.format(Locale.ROOT,"%.2f%%",100.0*n/d);}

    static void appendCohort356(StringBuilder r,Cohort356 c){
        long n=c.sessions.size(),prof=0,loss=0,be=0,target=0,bust=0;
        long episodes=0,success=0,failed=0,handsBelow=0,cards=0,splits=0,doubles=0;
        double finalSum=0,wager=0,wagerBelow=0,dd=0,give=0,targetExp=0;
        long targetExpN=0; double recoveryHands=0,recoveryExp=0,recoveryDepth=0; long recoveryN=0;

        for(Session356 s:c.sessions){
            finalSum+=s.finalBank;wager+=s.cumulativeWager;wagerBelow+=s.wagerBelow100;
            dd+=s.maxDrawdown;give+=s.peakGiveBack;handsBelow+=s.handsBelow100;
            cards+=s.cardsConsumed;splits+=s.splits;doubles+=s.doubles;
            if(s.finalBank>100)prof++;else if(s.finalBank<100)loss++;else be++;
            if(s.targetHand>0){target++;targetExp+=s.targetExposure;targetExpN++;}
            if(s.finalBank<=0)bust++;
            episodes+=s.recoveryEpisodes;success+=s.successfulRecoveries;failed+=s.failedRecoveries;
            for(Recovery356 x:s.recoveries)if(x.recovered){
                recoveryN++;recoveryHands+=(x.endHand-x.startHand);
                recoveryExp+=x.exposure;recoveryDepth+=(100.0-x.trough);
            }
        }

        r.append("\n").append(c.name).append(" — EXPANDED MEASUREMENT BASELINE\n");
        r.append("Sessions: ").append(n).append("\n");
        r.append("Average ending bankroll: £").append(f3(finalSum/n)).append("\n");
        r.append("Profitable / Losing / Break-even: ").append(p356(prof,n)).append(" / ")
         .append(p356(loss,n)).append(" / ").append(p356(be,n)).append("\n");
        r.append("Reached £200 / Bankroll exhausted: ").append(p356(target,n)).append(" / ").append(p356(bust,n)).append("\n");
        r.append("Average cumulative wager per session: £").append(f3(wager/n)).append("\n");
        r.append("Average cumulative wager below £100 per session: £").append(f3(wagerBelow/n)).append("\n");
        r.append("Average maximum drawdown: £").append(f3(dd/n)).append("\n");
        r.append("Average peak-to-finish give-back: £").append(f3(give/n)).append("\n");
        r.append("Average hands below £100: ").append(f3((double)handsBelow/n)).append("\n");
        r.append("Recovery episodes / successful / failed: ").append(episodes).append(" / ").append(success).append(" / ").append(failed).append("\n");
        r.append("Successful recovery rate by episode: ").append(p356(success,episodes)).append("\n");
        r.append("Average successful recovery duration (hands after crossing below £100): ")
         .append(recoveryN==0?"0.000":f3(recoveryHands/recoveryN)).append("\n");
        r.append("Average successful recovery exposure: £").append(recoveryN==0?"0.000":f3(recoveryExp/recoveryN)).append("\n");
        r.append("Average successful recovery depth below £100: £").append(recoveryN==0?"0.000":f3(recoveryDepth/recoveryN)).append("\n");
        r.append("Average exposure at first £200 reach: £").append(targetExpN==0?"0.000":f3(targetExp/targetExpN)).append("\n");
        r.append("Cards consumed / splits / doubles: ").append(cards).append(" / ").append(splits).append(" / ").append(doubles).append("\n");
    }

    static String sessionCsv356(Cohort356... cs){
        StringBuilder b=new StringBuilder("cohort,session,hands_played,final_bank,peak_bank,trough_bank,max_drawdown,cumulative_wager,wager_below_100,hands_below_100,recovery_episodes,successful_recoveries,failed_recoveries,target_hand,target_exposure,bankruptcy_hand,peak_give_back,cards_consumed,splits,doubles\n");
        for(Cohort356 c:cs)for(Session356 s:c.sessions)
            b.append(String.format(Locale.ROOT,"%s,%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d,%.2f,%d,%.2f,%d,%d,%d%n",
                s.cohort,s.session,s.handsPlayed,s.finalBank,s.peakBank,s.troughBank,s.maxDrawdown,
                s.cumulativeWager,s.wagerBelow100,s.handsBelow100,s.recoveryEpisodes,s.successfulRecoveries,
                s.failedRecoveries,s.targetHand,s.targetExposure,s.bankruptcyHand,s.peakGiveBack,
                s.cardsConsumed,s.splits,s.doubles));
        return b.toString();
    }

    static String recoveryCsv356(Cohort356... cs){
        StringBuilder b=new StringBuilder("cohort,session,episode,start_hand,end_hand,start_bank,trough_bank,depth_below_100,exposure,recovered\n");
        for(Cohort356 c:cs)for(Session356 s:c.sessions){
            int e=0;for(Recovery356 x:s.recoveries){
                e++;b.append(String.format(Locale.ROOT,"%s,%d,%d,%d,%d,%.2f,%.2f,%.2f,%.2f,%s%n",
                    s.cohort,s.session,e,x.startHand,x.endHand,x.startBank,x.trough,
                    Math.max(0,100-x.trough),x.exposure,Boolean.toString(x.recovered)));
            }
        }
        return b.toString();
    }

    static String tierCsv356(Cohort356... cs){
        StringBuilder b=new StringBuilder("cohort,from_tier,to_state,count\n");
        for(Cohort356 c:cs){
            long[][] z=new long[10][12];
            for(Session356 s:c.sessions)for(int i=0;i<10;i++)for(int j=0;j<12;j++)z[i][j]+=s.transitions[i][j];
            for(int i=0;i<10;i++)for(int j=0;j<12;j++)if(z[i][j]>0){
                String to=j==10?"TARGET_200":(j==11?"BELOW_100":"£"+TIERS356[j]+"-"+(TIERS356[j]+9));
                b.append(c.name).append(",£").append(TIERS356[i]).append("-").append(TIERS356[i]+9)
                 .append(",").append(to).append(",").append(z[i][j]).append("\n");
            }
        }
        return b.toString();
    }


    static String depthBand357(double depth){
        if(depth<10)return "£0-<10";
        if(depth<20)return "£10-<20";
        if(depth<30)return "£20-<30";
        if(depth<40)return "£30-<40";
        if(depth<50)return "£40-<50";
        return "£50+";
    }
    static String exposureBand357(double x){
        if(x<25)return "£0-<25";
        if(x<50)return "£25-<50";
        if(x<75)return "£50-<75";
        if(x<100)return "£75-<100";
        if(x<150)return "£100-<150";
        return "£150+";
    }
    static String startHandBand357(int h){
        if(h<=5)return "Hands 1-5";
        if(h<=10)return "Hands 6-10";
        if(h<=15)return "Hands 11-15";
        if(h<=20)return "Hands 16-20";
        if(h<=25)return "Hands 21-25";
        return "Hands 26-30";
    }

    static final class Band357{
        long total,success;
        double exposure,depth,duration;
        void add(Recovery356 r){
            total++;
            if(r.recovered)success++;
            exposure+=r.exposure;
            depth+=Math.max(0,100-r.trough);
            duration+=(r.endHand-r.startHand);
        }
    }

    static void addBand357(Map<String,Band357> m,String key,Recovery356 r){
        Band357 b=m.get(key);
        if(b==null){b=new Band357();m.put(key,b);}
        b.add(r);
    }

    static String bandCsv357(String dimension,Map<String,Band357> m){
        StringBuilder b=new StringBuilder("dimension,band,episodes,successful,failed,success_rate,avg_duration_hands,avg_exposure,avg_depth_below_100\n");
        for(Map.Entry<String,Band357> e:m.entrySet()){
            Band357 x=e.getValue();
            b.append(String.format(Locale.ROOT,"%s,%s,%d,%d,%d,%.4f,%.4f,%.4f,%.4f%n",
                dimension,e.getKey(),x.total,x.success,x.total-x.success,
                x.total==0?0:(100.0*x.success/x.total),
                x.total==0?0:(x.duration/x.total),
                x.total==0?0:(x.exposure/x.total),
                x.total==0?0:(x.depth/x.total)));
        }
        return b.toString();
    }

    static void appendRecoveryAnatomy357(StringBuilder r,Cohort356 c)throws Exception{
        long episodes=0,success=0,failed=0;
        double successDur=0,successExp=0,successDepth=0;
        double failDur=0,failExp=0,failDepth=0;
        long sN=0,fN=0;

        LinkedHashMap<String,Band357> depth=new LinkedHashMap<>();
        LinkedHashMap<String,Band357> exposure=new LinkedHashMap<>();
        LinkedHashMap<String,Band357> start=new LinkedHashMap<>();
        String[] depthOrder={"£0-<10","£10-<20","£20-<30","£30-<40","£40-<50","£50+"};
        String[] expOrder={"£0-<25","£25-<50","£50-<75","£75-<100","£100-<150","£150+"};
        String[] startOrder={"Hands 1-5","Hands 6-10","Hands 11-15","Hands 16-20","Hands 21-25","Hands 26-30"};
        for(String x:depthOrder)depth.put(x,new Band357());
        for(String x:expOrder)exposure.put(x,new Band357());
        for(String x:startOrder)start.put(x,new Band357());

        for(Session356 s:c.sessions){
            for(Recovery356 x:s.recoveries){
                episodes++;
                double d=x.endHand-x.startHand;
                double dep=Math.max(0,100-x.trough);
                if(x.recovered){
                    success++;sN++;successDur+=d;successExp+=x.exposure;successDepth+=dep;
                }else{
                    failed++;fN++;failDur+=d;failExp+=x.exposure;failDepth+=dep;
                }
                addBand357(depth,depthBand357(dep),x);
                addBand357(exposure,exposureBand357(x.exposure),x);
                addBand357(start,startHandBand357(x.startHand),x);
            }
        }

        r.append("\n").append(c.name).append(" — RECOVERY EFFICIENCY ANATOMY\n");
        r.append("Recovery episodes: ").append(episodes).append("\n");
        r.append("Successful / Failed: ").append(success).append(" / ").append(failed)
         .append(" (").append(p356(success,episodes)).append(" successful)\n");
        r.append("Successful episodes — avg duration / exposure / depth: ")
         .append(f3(successDur/sN)).append(" hands / £").append(f3(successExp/sN))
         .append(" / £").append(f3(successDepth/sN)).append("\n");
        r.append("Failed episodes — avg observed duration / exposure / depth: ")
         .append(f3(failDur/fN)).append(" hands / £").append(f3(failExp/fN))
         .append(" / £").append(f3(failDepth/fN)).append("\n");
        r.append("Exposure ratio failed/successful: ").append(f3((failExp/fN)/(successExp/sN))).append("x\n");
        r.append("Depth ratio failed/successful: ").append(f3((failDepth/fN)/(successDepth/sN))).append("x\n");

        r.append("\nSUCCESS RATE BY RECOVERY DEPTH\n");
        for(Map.Entry<String,Band357> e:depth.entrySet()){
            Band357 x=e.getValue();
            r.append(String.format(Locale.ROOT,"%-9s  episodes=%5d  success=%6.2f%%  avg exposure=£%7.2f  avg duration=%5.2f%n",
                e.getKey(),x.total,x.total==0?0:100.0*x.success/x.total,x.total==0?0:x.exposure/x.total,x.total==0?0:x.duration/x.total));
        }
        r.append("\nSUCCESS RATE BY RECOVERY EXPOSURE\n");
        for(Map.Entry<String,Band357> e:exposure.entrySet()){
            Band357 x=e.getValue();
            r.append(String.format(Locale.ROOT,"%-11s episodes=%5d  success=%6.2f%%  avg depth=£%6.2f  avg duration=%5.2f%n",
                e.getKey(),x.total,x.total==0?0:100.0*x.success/x.total,x.total==0?0:x.depth/x.total,x.total==0?0:x.duration/x.total));
        }
        r.append("\nSUCCESS RATE BY EPISODE START HAND\n");
        for(Map.Entry<String,Band357> e:start.entrySet()){
            Band357 x=e.getValue();
            r.append(String.format(Locale.ROOT,"%-12s episodes=%5d  success=%6.2f%%  avg exposure=£%7.2f  avg depth=£%6.2f%n",
                e.getKey(),x.total,x.total==0?0:100.0*x.success/x.total,x.total==0?0:x.exposure/x.total,x.total==0?0:x.depth/x.total));
        }

        write("Phase_3_57_"+c.name+"_Recovery_Depth_Bands.csv",bandCsv357("depth",depth));
        write("Phase_3_57_"+c.name+"_Recovery_Exposure_Bands.csv",bandCsv357("exposure",exposure));
        write("Phase_3_57_"+c.name+"_Recovery_StartHand_Bands.csv",bandCsv357("start_hand",start));
    }


    static String depthGroup358(double depth){
        if(depth<20)return "SHALLOW_<20";
        if(depth<40)return "MODERATE_20_<40";
        if(depth<50)return "DEEP_40_<50";
        return "SEVERE_50_PLUS";
    }

    static String timeGroup358(int startHand){
        int remaining=HANDS-startHand;
        if(remaining>=20)return "20_PLUS_HANDS_LEFT";
        if(remaining>=10)return "10_19_HANDS_LEFT";
        if(remaining>=5)return "5_9_HANDS_LEFT";
        return "0_4_HANDS_LEFT";
    }

    static final class Cell358{
        ArrayList<Recovery356> episodes=new ArrayList<>();
    }

    static double median358(ArrayList<Double> a){
        if(a.isEmpty())return 0.0;
        Collections.sort(a);
        int n=a.size();
        return n%2==1?a.get(n/2):(a.get(n/2-1)+a.get(n/2))/2.0;
    }

    static String decompose358(Cohort356 c,StringBuilder report){
        LinkedHashMap<String,Cell358> cells=new LinkedHashMap<>();
        String[] dg={"SHALLOW_<20","MODERATE_20_<40","DEEP_40_<50","SEVERE_50_PLUS"};
        String[] tg={"20_PLUS_HANDS_LEFT","10_19_HANDS_LEFT","5_9_HANDS_LEFT","0_4_HANDS_LEFT"};
        for(String d:dg)for(String t:tg)cells.put(d+"|"+t,new Cell358());

        for(Session356 s:c.sessions)for(Recovery356 x:s.recoveries){
            double dep=Math.max(0,100-x.trough);
            String key=depthGroup358(dep)+"|"+timeGroup358(x.startHand);
            cells.get(key).episodes.add(x);
        }

        StringBuilder csv=new StringBuilder(
            "cohort,depth_group,time_remaining_group,episodes,median_exposure,low_exposure_n,low_success_n,low_success_rate,high_exposure_n,high_success_n,high_success_rate,high_minus_low_pp,low_avg_duration,high_avg_duration,low_avg_depth,high_avg_depth\n");

        report.append("\n").append(c.name).append(" — DEPTH × TIME-REMAINING × EXPOSURE DECOMPOSITION\n");
        report.append("Within each depth/time cell, episodes are divided at that cell's median observed recovery exposure.\n");
        report.append("This is descriptive, not causal: exposure accumulates as an episode continues, so outcome and exposure are temporally entangled.\n\n");

        for(Map.Entry<String,Cell358> e:cells.entrySet()){
            String[] parts=e.getKey().split("\\|");
            ArrayList<Recovery356> eps=e.getValue().episodes;
            if(eps.isEmpty())continue;

            ArrayList<Double> vals=new ArrayList<>();
            for(Recovery356 x:eps)vals.add(x.exposure);
            double med=median358(vals);

            long ln=0,ls=0,hn=0,hs=0;
            double ldur=0,hdur=0,ldep=0,hdep=0;
            for(Recovery356 x:eps){
                boolean low=x.exposure<=med;
                double dur=x.endHand-x.startHand;
                double dep=Math.max(0,100-x.trough);
                if(low){ln++;if(x.recovered)ls++;ldur+=dur;ldep+=dep;}
                else{hn++;if(x.recovered)hs++;hdur+=dur;hdep+=dep;}
            }
            double lr=ln==0?0:100.0*ls/ln;
            double hr=hn==0?0:100.0*hs/hn;
            double diff=hr-lr;

            report.append(String.format(Locale.ROOT,
                "%-16s | %-18s | n=%4d | median exposure £%6.2f | lower exposure success %6.2f%% | higher exposure success %6.2f%% | difference %+6.2f pp%n",
                parts[0],parts[1],eps.size(),med,lr,hr,diff));

            csv.append(String.format(Locale.ROOT,
                "%s,%s,%s,%d,%.2f,%d,%d,%.4f,%d,%d,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f%n",
                c.name,parts[0],parts[1],eps.size(),med,ln,ls,lr,hn,hs,hr,diff,
                ln==0?0:ldur/ln,hn==0?0:hdur/hn,ln==0?0:ldep/ln,hn==0?0:hdep/hn));
        }
        return csv.toString();
    }























    /* PHASE 3.80 — SAME-SEED PAIRED DISTRIBUTIONAL TEST */
    enum Policy380 { CONTROL, HIT_LE_15, CTX14_16 }

    static final class S380{
        String cohort,policy; int session,hands;
        double finalBank,cumWager,maxDrawdown,peakBank,giveBack;
        long playerBusts,hits,contextHits;
        boolean reached200,exhausted;
    }
    static final class H380{
        ArrayList<Integer> c=new ArrayList<>();
        int total(){int t=0,a=0;for(int v:c){t+=v;if(v==11)a++;}while(t>21&&a-->0)t-=10;return t;}
        boolean bust(){return total()>21;}
    }
    static int draw380(int[]shoe,int ptr,H380 h){h.c.add(shoe[ptr]);return ptr+1;}
    static boolean hit380(Policy380 p,int total,int up){
        if(p==Policy380.HIT_LE_15)return total<=15;
        if(p==Policy380.CTX14_16)return total<=13||(total>=14&&total<=16&&up>=7);
        return false;
    }
    static S380 runOne380(long seed,String cohort,int session,Policy380 policy){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];double bank=START_BANK,peak=START_BANK,maxDD=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        S380 s=new S380();s.cohort=cohort;s.policy=policy.name();s.session=session+1;
        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}if(cautious&&bank>=140)cautious=false;
            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){int b=bal355(hist,hc);boolean neg=b<0;if(neg&&!negativeEpisode)brakeArmed=true;negativeEpisode=neg;double base=bank>=170?15:normal355(hand,bank);if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;}
            else{wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;}
            wager=Math.min(wager,bank);if(wager<=0)break;if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;
            double net,stake=wager;
            if(policy==Policy380.CONTROL){
                PairStats ps=new PairStats();Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
                ptr=rr.endPointer;net=rr.net;stake=roundStake356(rr);
            }else{
                H380 p=new H380(),d=new H380();
                ptr=draw380(shoe,ptr,p);ptr=draw380(shoe,ptr,d);ptr=draw380(shoe,ptr,p);ptr=draw380(shoe,ptr,d);
                int visibleUpcard=d.c.get(0); // sole dealer information available to player
                while(!p.bust()&&hit380(policy,p.total(),visibleUpcard)){
                    if(policy==Policy380.CTX14_16&&p.total()>=14&&p.total()<=16&&visibleUpcard>=7)s.contextHits++;
                    ptr=draw380(shoe,ptr,p);s.hits++;
                }
                if(p.bust()){s.playerBusts++;net=-wager;}
                else{while(d.total()<17)ptr=draw380(shoe,ptr,d);if(d.bust()||p.total()>d.total())net=wager;else if(p.total()<d.total())net=-wager;else net=0;}
            }
            s.cumWager+=stake;bank+=net;if(bank<0)bank=0;s.hands++;peak=Math.max(peak,bank);maxDD=Math.max(maxDD,peak-bank);
            hist[hc%hist.length]=net>1e-9?1:(net<-1e-9?-1:0);hc++;
            if(bank>=200){s.reached200=true;break;}if(bank<=0){s.exhausted=true;break;}
            if(everPlunge&&bank<=100&&!cooling){cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;}
        }
        s.finalBank=bank;s.maxDrawdown=maxDD;s.peakBank=peak;s.giveBack=Math.max(0,peak-bank);
        if(bank>=200)s.reached200=true;if(bank<=0)s.exhausted=true;return s;
    }
    static ArrayList<S380> run380(long seed,String cohort,Policy380 p){ArrayList<S380>a=new ArrayList<>();for(int i=0;i<SESSIONS;i++)a.add(runOne380(seed,cohort,i,p));return a;}
    static String pct380(long x,long n){return String.format(Locale.ROOT,"%.2f%%",100.0*x/n);}
    static int bandIndex380(double x){if(x<=1e-9)return 0;if(x<50)return 1;if(x<100)return 2;if(x<150)return 3;if(x<200)return 4;return 5;}
    static String bandName380(int k){return new String[]{"£0","£1-49","£50-99","£100-149","£150-199","£200+"}[k];}

    static void population380(StringBuilder r,String cohort,ArrayList<S380>[]g){
        r.append("\n").append(cohort).append(" — POPULATION SUMMARY\n");
        r.append(String.format(Locale.ROOT,"%-11s %9s %9s %9s %9s %9s%n","Policy","Final","£200","£0","MaxDD","Wager"));
        for(ArrayList<S380>a:g){long n=a.size(),t=0,z=0;double f=0,d=0,w=0;for(S380 s:a){f+=s.finalBank;d+=s.maxDrawdown;w+=s.cumWager;if(s.reached200)t++;if(s.exhausted)z++;}
            r.append(String.format(Locale.ROOT,"%-11s £%8.2f %8s %8s £%8.2f £%8.2f%n",a.get(0).policy,f/n,pct380(t,n),pct380(z,n),d/n,w/n));}
    }
    static void paired380(StringBuilder r,String cohort,ArrayList<S380>c,ArrayList<S380>s,ArrayList<S380>x){
        long n=c.size(),xwc=0,tc=0,cwx=0,xws=0,ts=0,swx=0;double dc=0,ds=0,abs=0,ddDelta=0,wDelta=0;
        long[][]bands=new long[3][6];
        for(int i=0;i<n;i++){
            S380 a=c.get(i),b=s.get(i),e=x.get(i);double d=e.finalBank-a.finalBank,q=e.finalBank-b.finalBank;
            dc+=d;ds+=q;abs+=Math.abs(d);ddDelta+=e.maxDrawdown-a.maxDrawdown;wDelta+=e.cumWager-a.cumWager;
            if(d>1e-9)xwc++;else if(d<-1e-9)cwx++;else tc++;
            if(q>1e-9)xws++;else if(q<-1e-9)swx++;else ts++;
            bands[0][bandIndex380(a.finalBank)]++;bands[1][bandIndex380(b.finalBank)]++;bands[2][bandIndex380(e.finalBank)]++;
        }
        r.append("\n").append(cohort).append(" — SAME-SEED PAIRED EVIDENCE\n");
        r.append("CTX14_16 versus CONTROL\n");
        r.append("CTX higher final: ").append(pct380(xwc,n)).append("\nCONTROL higher final: ").append(pct380(cwx,n)).append("\nExact final tie: ").append(pct380(tc,n)).append("\n");
        r.append(String.format(Locale.ROOT,"Mean paired final difference (CTX - CONTROL): £%.3f%nMean absolute paired final difference: £%.3f%nMean paired max-drawdown difference: £%.3f%nMean paired cumulative-wager difference: £%.3f%n",dc/n,abs/n,ddDelta/n,wDelta/n));
        r.append("\nCTX14_16 versus HIT_LE_15\n");
        r.append("CTX higher final: ").append(pct380(xws,n)).append("\nHIT_LE_15 higher final: ").append(pct380(swx,n)).append("\nExact final tie: ").append(pct380(ts,n)).append("\n");
        r.append(String.format(Locale.ROOT,"Mean paired final difference (CTX - HIT_LE_15): £%.3f%n",ds/n));
        r.append("\nFINAL BANKROLL BANDS\n");
        r.append(String.format(Locale.ROOT,"%-11s %10s %12s %12s%n","Band","CONTROL","HIT_LE_15","CTX14_16"));
        for(int k=0;k<6;k++)r.append(String.format(Locale.ROOT,"%-11s %10s %12s %12s%n",bandName380(k),pct380(bands[0][k],n),pct380(bands[1][k],n),pct380(bands[2][k],n)));
    }
    static String csv380(String cohort,ArrayList<S380>c,ArrayList<S380>s,ArrayList<S380>x){
        StringBuilder b=new StringBuilder("cohort,session,control_final,hit15_final,ctx_final,ctx_minus_control,ctx_minus_hit15,control_drawdown,hit15_drawdown,ctx_drawdown,control_wager,hit15_wager,ctx_wager,control_band,hit15_band,ctx_band\n");
        for(int i=0;i<c.size();i++){S380 a=c.get(i),d=s.get(i),e=x.get(i);b.append(String.format(Locale.ROOT,"%s,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%s,%s,%s%n",cohort,i+1,a.finalBank,d.finalBank,e.finalBank,e.finalBank-a.finalBank,e.finalBank-d.finalBank,a.maxDrawdown,d.maxDrawdown,e.maxDrawdown,a.cumWager,d.cumWager,e.cumWager,bandName380(bandIndex380(a.finalBank)),bandName380(bandIndex380(d.finalBank)),bandName380(bandIndex380(e.finalBank))));}return b.toString();
    }

    /* PHASE 3.91 — EXISTING MAXIMUM-EXPOSURE REGION MAPPING
       No wager is increased. This instruments the frozen Phase 3.55 controller.
       The controller's highest nominal original-round wager is £30.
       This £30 state is an opportunity-mapping region; it is NOT itself re-labelled
       as a new >2x rule and does not reopen the closed 3.22–3.35 history trigger. */

    static final class R391 {
        String cohort; int session,hand;
        double bankBefore,baseWager,committedStake,net,bankAfter,peakBefore,drawdownAfter;
        boolean maxRegion,targetOnHand,bankruptOnHand;
    }
    static final class S391 {
        String cohort; int session,hands,maxRounds;
        double finalBank,peak,maxDD,cumWager,maxRegionExposure,maxRegionNet;
        boolean reached200,exhausted,everMaxRegion;
        ArrayList<R391> rounds=new ArrayList<>();
    }

    static S391 runOne391(long seed,String cohort,int session){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));
        if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];
        double bank=START_BANK,peak=START_BANK,maxDD=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        S391 s=new S391();s.cohort=cohort;s.session=session+1;
        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}
            if(cautious&&bank>=140)cautious=false;

            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){
                int b=bal355(hist,hc);boolean neg=b<0;
                if(neg&&!negativeEpisode)brakeArmed=true;
                negativeEpisode=neg;
                double base=bank>=170?15:normal355(hand,bank);
                if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;
            } else {wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;}

            wager=Math.min(wager,bank);if(wager<=0)break;
            if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;

            R391 q=new R391();q.cohort=cohort;q.session=session+1;q.hand=hand;
            q.bankBefore=bank;q.baseWager=wager;q.peakBefore=peak;
            q.maxRegion=Math.abs(wager-30.0)<1e-9;

            PairStats ps=new PairStats();
            Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
            ptr=rr.endPointer;
            q.committedStake=roundStake356(rr);q.net=rr.net;
            s.cumWager+=q.committedStake;
            if(q.maxRegion){s.maxRounds++;s.maxRegionExposure+=q.committedStake;s.maxRegionNet+=q.net;s.everMaxRegion=true;}

            bank+=rr.net;if(bank<0)bank=0;s.hands++;
            peak=Math.max(peak,bank);maxDD=Math.max(maxDD,peak-bank);
            q.bankAfter=bank;q.drawdownAfter=peak-bank;
            q.targetOnHand=bank>=200;q.bankruptOnHand=bank<=0;
            s.rounds.add(q);

            hist[hc%hist.length]=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);hc++;
            if(bank>=200){s.reached200=true;break;}
            if(bank<=0){s.exhausted=true;break;}
            if(everPlunge&&bank<=100&&!cooling){cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;}
        }
        s.finalBank=bank;s.peak=peak;s.maxDD=maxDD;
        return s;
    }

    static ArrayList<S391> run391(long seed,String cohort){
        ArrayList<S391>a=new ArrayList<>();
        for(int i=0;i<SESSIONS;i++)a.add(runOne391(seed,cohort,i));
        return a;
    }

    static void summary391(StringBuilder r,String cohort,ArrayList<S391>a){
        long n=a.size(),ever=0,target=0,bust=0,targetEver=0,bustEver=0,targetNever=0,bustNever=0;
        long rounds=0,maxRounds=0,maxWins=0,maxLosses=0,maxPushes=0,maxTargetHands=0,maxBustHands=0;
        double finalAll=0,ddAll=0,wAll=0,finalEver=0,ddEver=0,wEver=0,exp=0,net=0,bankBefore=0,ddAfter=0;
        for(S391 s:a){
            rounds+=s.hands;maxRounds+=s.maxRounds;finalAll+=s.finalBank;ddAll+=s.maxDD;wAll+=s.cumWager;
            if(s.reached200)target++;if(s.exhausted)bust++;
            if(s.everMaxRegion){
                ever++;finalEver+=s.finalBank;ddEver+=s.maxDD;wEver+=s.cumWager;
                if(s.reached200)targetEver++;if(s.exhausted)bustEver++;
            } else {
                if(s.reached200)targetNever++;if(s.exhausted)bustNever++;
            }
            for(R391 q:s.rounds)if(q.maxRegion){
                exp+=q.committedStake;net+=q.net;bankBefore+=q.bankBefore;ddAfter+=q.drawdownAfter;
                if(q.net>1e-9)maxWins++;else if(q.net<-1e-9)maxLosses++;else maxPushes++;
                if(q.targetOnHand)maxTargetHands++;if(q.bankruptOnHand)maxBustHands++;
            }
        }
        long never=n-ever;
        r.append("\n").append(cohort).append(" — MAXIMUM-EXPOSURE REGION\n");
        r.append("Sessions: ").append(n).append("\n");
        r.append("Total played rounds: ").append(rounds).append("\n");
        r.append("£30 maximum-region rounds: ").append(maxRounds).append(" (").append(pct380(maxRounds,rounds)).append(" of played rounds)\n");
        r.append("Sessions ever entering £30 region: ").append(ever).append(" (").append(pct380(ever,n)).append(")\n");
        r.append(String.format(Locale.ROOT,"Average bankroll immediately before £30 round: £%.3f%n",maxRounds==0?0:bankBefore/maxRounds));
        r.append(String.format(Locale.ROOT,"Total committed exposure on £30 rounds incl. branches: £%.2f%n",exp));
        r.append(String.format(Locale.ROOT,"Average committed exposure per £30 round: £%.3f%n",maxRounds==0?0:exp/maxRounds));
        r.append(String.format(Locale.ROOT,"Net bankroll change on £30 rounds: £%+.2f%n",net));
        r.append(String.format(Locale.ROOT,"Return on committed £30-region exposure: %+.4f%%%n",exp==0?0:100.0*net/exp));
        r.append("£30-region W/L/P: ").append(maxWins).append(" / ").append(maxLosses).append(" / ").append(maxPushes).append("\n");
        r.append("£200 reached ON a £30-region round: ").append(maxTargetHands).append("\n");
        r.append("£0 reached ON a £30-region round: ").append(maxBustHands).append("\n");
        r.append(String.format(Locale.ROOT,"Average drawdown immediately after £30 round: £%.3f%n",maxRounds==0?0:ddAfter/maxRounds));

        r.append("\nSESSION ASSOCIATION — OBSERVATIONAL, NOT CAUSAL\n");
        r.append(String.format(Locale.ROOT,"Ever-£30 sessions average final / maxDD / cumulative wager: £%.3f / £%.3f / £%.3f%n",
            ever==0?0:finalEver/ever,ever==0?0:ddEver/ever,ever==0?0:wEver/ever));
        r.append("Ever-£30 sessions reaching £200 / £0: ").append(pct380(targetEver,ever)).append(" / ").append(pct380(bustEver,ever)).append("\n");
        r.append("Never-£30 sessions reaching £200 / £0: ").append(pct380(targetNever,never)).append(" / ").append(pct380(bustNever,never)).append("\n");
        r.append(String.format(Locale.ROOT,"Whole cohort average final / maxDD / cumulative wager: £%.3f / £%.3f / £%.3f%n",finalAll/n,ddAll/n,wAll/n));
        r.append("Whole cohort £200 / £0: ").append(pct380(target,n)).append(" / ").append(pct380(bust,n)).append("\n");
    }

    static String csv391(ArrayList<S391>...groups){
        StringBuilder b=new StringBuilder("cohort,session,hand,bank_before,base_wager,committed_stake,net,bank_after,peak_before,drawdown_after,max_region_30,target_on_hand,bankrupt_on_hand\n");
        for(ArrayList<S391>g:groups)for(S391 s:g)for(R391 q:s.rounds)
            b.append(String.format(Locale.ROOT,"%s,%d,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%s,%s,%s%n",
                q.cohort,q.session,q.hand,q.bankBefore,q.baseWager,q.committedStake,q.net,q.bankAfter,q.peakBefore,q.drawdownAfter,
                Boolean.toString(q.maxRegion),Boolean.toString(q.targetOnHand),Boolean.toString(q.bankruptOnHand)));
        return b.toString();
    }


    /* PHASE 3.92 — PROFIT RETENTION / GIVE-BACK
       Measurement only. Frozen Phase 3.55 controller. No wager change.
       Peak-profit give-back is measured relative to accumulated profit above
       the £100 starting bankroll. */

    static final class S392 {
        String cohort; int session,hands,peakHand;
        double finalBank,peak,maxDD,cumWager,postPeakExposure;
        boolean reached200,exhausted;
    }

    static S392 runOne392(long seed,String cohort,int session){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));
        if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];
        double bank=START_BANK,peak=START_BANK,maxDD=0,cum=0;
        int peakHand=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        ArrayList<Double> stakes=new ArrayList<>();
        ArrayList<Double> banks=new ArrayList<>();
        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}
            if(cautious&&bank>=140)cautious=false;
            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){
                int b=bal355(hist,hc);boolean neg=b<0;
                if(neg&&!negativeEpisode)brakeArmed=true;negativeEpisode=neg;
                double base=bank>=170?15:normal355(hand,bank);
                if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;
            } else {wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;}
            wager=Math.min(wager,bank);if(wager<=0)break;
            if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;
            PairStats ps=new PairStats();
            Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
            ptr=rr.endPointer;
            double st=roundStake356(rr);cum+=st;stakes.add(st);
            bank+=rr.net;if(bank<0)bank=0;banks.add(bank);
            if(bank>peak+1e-9){peak=bank;peakHand=hand;}
            maxDD=Math.max(maxDD,peak-bank);
            hist[hc%hist.length]=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);hc++;
            if(bank>=200||bank<=0)break;
            if(everPlunge&&bank<=100&&!cooling){cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;}
        }
        double post=0;
        for(int i=peakHand;i<stakes.size();i++)post+=stakes.get(i); // rounds after peak hand
        S392 x=new S392();x.cohort=cohort;x.session=session+1;x.hands=stakes.size();
        x.peak=peak;x.peakHand=peakHand;x.finalBank=bank;x.maxDD=maxDD;x.cumWager=cum;x.postPeakExposure=post;
        x.reached200=bank>=200;x.exhausted=bank<=0;
        return x;
    }

    static ArrayList<S392> run392(long seed,String cohort){
        ArrayList<S392>a=new ArrayList<>();for(int i=0;i<SESSIONS;i++)a.add(runOne392(seed,cohort,i));return a;
    }

    static void summary392(StringBuilder r,String cohort,ArrayList<S392>a){
        long n=a.size(),profitPeak=0,g25=0,g50=0,g75=0,g100=0,finishProfit=0,target=0,bust=0;
        double peakSum=0,finalSum=0,giveSum=0,givePctSum=0,postExp=0,cum=0;
        ArrayList<Double> givePcts=new ArrayList<>();
        for(S392 s:a){
            finalSum+=s.finalBank;cum+=s.cumWager;if(s.reached200)target++;if(s.exhausted)bust++;
            if(s.peak>100+1e-9){
                profitPeak++;double pp=s.peak-100,gb=Math.max(0,s.peak-s.finalBank),gp=100*gb/pp;
                peakSum+=s.peak;giveSum+=gb;givePctSum+=gp;givePcts.add(gp);postExp+=s.postPeakExposure;
                if(gp>=25-1e-9)g25++;if(gp>=50-1e-9)g50++;if(gp>=75-1e-9)g75++;if(gp>=100-1e-9)g100++;
                if(s.finalBank>100+1e-9)finishProfit++;
            }
        }
        Collections.sort(givePcts);
        double med=givePcts.isEmpty()?0:givePcts.get(givePcts.size()/2);
        r.append("\n").append(cohort).append("\n");
        r.append("Sessions: ").append(n).append("\n");
        r.append("Sessions ever above £100: ").append(profitPeak).append(" (").append(pct380(profitPeak,n)).append(")\n");
        r.append(String.format(Locale.ROOT,"Average peak among profit-peaking sessions: £%.3f%n",profitPeak==0?0:peakSum/profitPeak));
        r.append(String.format(Locale.ROOT,"Average final bankroll, whole cohort: £%.3f%n",finalSum/n));
        r.append(String.format(Locale.ROOT,"Average peak-to-finish give-back among profit-peaking sessions: £%.3f%n",profitPeak==0?0:giveSum/profitPeak));
        r.append(String.format(Locale.ROOT,"Average / median give-back as %% of accumulated peak profit: %.3f%% / %.3f%%%n",profitPeak==0?0:givePctSum/profitPeak,med));
        r.append("Surrendered >=25% of peak profit: ").append(pct380(g25,profitPeak)).append("\n");
        r.append("Surrendered >=50%: ").append(pct380(g50,profitPeak)).append("\n");
        r.append("Surrendered >=75%: ").append(pct380(g75,profitPeak)).append("\n");
        r.append("Surrendered >=100% (all peak profit or more): ").append(pct380(g100,profitPeak)).append("\n");
        r.append("Profit-peaking sessions still finishing >£100: ").append(pct380(finishProfit,profitPeak)).append("\n");
        r.append(String.format(Locale.ROOT,"Average exposure AFTER the eventual peak: £%.3f%n",profitPeak==0?0:postExp/profitPeak));
        r.append(String.format(Locale.ROOT,"Whole-cohort average cumulative wager: £%.3f%n",cum/n));
        r.append("Whole-cohort £200 / £0: ").append(pct380(target,n)).append(" / ").append(pct380(bust,n)).append("\n");
    }

    static String csv392(ArrayList<S392>...groups){
        StringBuilder b=new StringBuilder("cohort,session,hands,peak_bankroll,peak_hand,final_bankroll,peak_profit,giveback_amount,giveback_pct_of_peak_profit,post_peak_exposure,max_drawdown,cumulative_wager,reached_200,exhausted\n");
        for(ArrayList<S392>g:groups)for(S392 s:g){
            double pp=Math.max(0,s.peak-100),gb=pp>0?Math.max(0,s.peak-s.finalBank):0,gp=pp>0?100*gb/pp:0;
            b.append(String.format(Locale.ROOT,"%s,%d,%d,%.2f,%d,%.2f,%.2f,%.2f,%.4f,%.2f,%.2f,%.2f,%s,%s%n",
                s.cohort,s.session,s.hands,s.peak,s.peakHand,s.finalBank,pp,gb,gp,s.postPeakExposure,s.maxDD,s.cumWager,
                Boolean.toString(s.reached200),Boolean.toString(s.exhausted)));
        }return b.toString();
    }


    /* PHASE 3.93 — OBSERVABLE PROFIT-TIER RETENTION MAP
       Measurement only. Frozen Phase 3.55 controller.
       Milestones are prospectively observable bankroll states:
       £110, £120, ..., £190.
       We measure what happens AFTER FIRST ATTAINMENT. No stopping rule. */

    static final int[] TIERS393={110,120,130,140,150,160,170,180,190};

    static final class S393 {
        String cohort; int session,hands;
        double finalBank,maxDD,cumWager;
        boolean reached200,exhausted;
        int[] firstHand=new int[TIERS393.length];
        double[] postExposure=new double[TIERS393.length];
        double[] maxAfter=new double[TIERS393.length];
        boolean[] below100After=new boolean[TIERS393.length];
        boolean[] exhaustedAfter=new boolean[TIERS393.length];
        boolean[] reached200After=new boolean[TIERS393.length];
    }

    static S393 runOne393(long seed,String cohort,int session){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));
        if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];
        double bank=START_BANK,peak=START_BANK,maxDD=0,cum=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        S393 x=new S393();x.cohort=cohort;x.session=session+1;
        Arrays.fill(x.firstHand,-1);
        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}
            if(cautious&&bank>=140)cautious=false;
            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){
                int b=bal355(hist,hc);boolean neg=b<0;
                if(neg&&!negativeEpisode)brakeArmed=true;negativeEpisode=neg;
                double base=bank>=170?15:normal355(hand,bank);
                if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;
            } else {wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;}
            wager=Math.min(wager,bank);if(wager<=0)break;
            if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;

            PairStats ps=new PairStats();
            Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
            ptr=rr.endPointer;double stake=roundStake356(rr);cum+=stake;
            for(int j=0;j<TIERS393.length;j++)if(x.firstHand[j]>=0)x.postExposure[j]+=stake;

            bank+=rr.net;if(bank<0)bank=0;x.hands++;
            peak=Math.max(peak,bank);maxDD=Math.max(maxDD,peak-bank);

            for(int j=0;j<TIERS393.length;j++){
                if(x.firstHand[j]<0 && bank>=TIERS393[j]-1e-9){
                    x.firstHand[j]=hand;x.maxAfter[j]=bank;
                }
                if(x.firstHand[j]>=0){
                    x.maxAfter[j]=Math.max(x.maxAfter[j],bank);
                    if(hand>x.firstHand[j] && bank<100-1e-9)x.below100After[j]=true;
                    if(bank>=200-1e-9)x.reached200After[j]=true;
                    if(bank<=1e-9)x.exhaustedAfter[j]=true;
                }
            }

            hist[hc%hist.length]=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);hc++;
            if(bank>=200){x.reached200=true;break;}
            if(bank<=0){x.exhausted=true;break;}
            if(everPlunge&&bank<=100&&!cooling){cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;}
        }
        x.finalBank=bank;x.maxDD=maxDD;x.cumWager=cum;return x;
    }

    static ArrayList<S393> run393(long seed,String cohort){
        ArrayList<S393>a=new ArrayList<>();for(int i=0;i<SESSIONS;i++)a.add(runOne393(seed,cohort,i));return a;
    }

    static void tiers393(StringBuilder r,String cohort,ArrayList<S393>a){
        r.append("\n").append(cohort).append("\n");
        r.append("Tier | Reached | Avg first hand | Finish >=tier | Finish >£100 | Later £200 | Later <£100 | Later £0 | Avg post-tier exposure | Avg give-back from max-after-tier\n");
        for(int j=0;j<TIERS393.length;j++){
            long n=0,ret=0,profit=0,t200=0,below=0,bust=0;double fh=0,pex=0,gb=0;
            for(S393 s:a)if(s.firstHand[j]>=0){
                n++;fh+=s.firstHand[j];pex+=s.postExposure[j];
                if(s.finalBank>=TIERS393[j]-1e-9)ret++;
                if(s.finalBank>100+1e-9)profit++;
                if(s.reached200After[j])t200++;
                if(s.below100After[j])below++;
                if(s.exhaustedAfter[j])bust++;
                gb+=Math.max(0,s.maxAfter[j]-s.finalBank);
            }
            r.append(String.format(Locale.ROOT,"£%d | %d (%.2f%%) | %.2f | %.2f%% | %.2f%% | %.2f%% | %.2f%% | %.2f%% | £%.2f | £%.2f%n",
                TIERS393[j],n,100.0*n/a.size(),n==0?0:fh/n,n==0?0:100.0*ret/n,n==0?0:100.0*profit/n,
                n==0?0:100.0*t200/n,n==0?0:100.0*below/n,n==0?0:100.0*bust/n,n==0?0:pex/n,n==0?0:gb/n));
        }
    }

    static String csv393(ArrayList<S393>...groups){
        StringBuilder b=new StringBuilder("cohort,session,tier,first_reach_hand,final_bankroll,finish_ge_tier,finish_gt_100,later_reach_200,later_below_100,later_exhausted,post_tier_exposure,max_after_tier,giveback_from_max_after_tier\n");
        for(ArrayList<S393>g:groups)for(S393 s:g)for(int j=0;j<TIERS393.length;j++)if(s.firstHand[j]>=0)
            b.append(String.format(Locale.ROOT,"%s,%d,%d,%d,%.2f,%s,%s,%s,%s,%s,%.2f,%.2f,%.2f%n",
                s.cohort,s.session,TIERS393[j],s.firstHand[j],s.finalBank,
                Boolean.toString(s.finalBank>=TIERS393[j]-1e-9),Boolean.toString(s.finalBank>100+1e-9),
                Boolean.toString(s.reached200After[j]),Boolean.toString(s.below100After[j]),Boolean.toString(s.exhaustedAfter[j]),
                s.postExposure[j],s.maxAfter[j],Math.max(0,s.maxAfter[j]-s.finalBank)));
        return b.toString();
    }


    /* PHASE 3.94 — PREDECLARED PROFIT-EXIT INTERVENTION SCREEN
       Frozen BEFORE execution from Phase 3.93:
       REFERENCE (unchanged 30-hand architecture), EXIT_140, EXIT_150, EXIT_160.
       Exit policies stop immediately after the first completed round on which
       bankroll reaches/exceeds the threshold. No wager-controller change. */

    static final int[] EXITS394={0,140,150,160};
    static final String[] NAMES394={"REFERENCE_30_HAND","EXIT_140","EXIT_150","EXIT_160"};

    static final class S394 {
        String cohort,policy; int session,hands,exitHand;
        double finalBank,peak,maxDD,cumWager,exitBank;
        boolean reached200,exhausted,exited;
        boolean everBelow100,recovered100;
        double maxProfitGiveback;
    }

    static S394 runOne394(long seed,String cohort,int session,int exitThreshold,String policy){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));
        if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];
        double bank=START_BANK,peak=START_BANK,maxDD=0,cum=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        boolean everBelow=false,recovered=false;
        S394 x=new S394();x.cohort=cohort;x.policy=policy;x.session=session+1;
        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}
            if(cautious&&bank>=140)cautious=false;
            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){
                int b=bal355(hist,hc);boolean neg=b<0;
                if(neg&&!negativeEpisode)brakeArmed=true;negativeEpisode=neg;
                double base=bank>=170?15:normal355(hand,bank);
                if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;
            } else {wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;}
            wager=Math.min(wager,bank);if(wager<=0)break;
            if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;

            PairStats ps=new PairStats();
            Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
            ptr=rr.endPointer;cum+=roundStake356(rr);
            bank+=rr.net;if(bank<0)bank=0;x.hands++;
            peak=Math.max(peak,bank);maxDD=Math.max(maxDD,peak-bank);

            if(bank<100-1e-9)everBelow=true;
            if(everBelow&&bank>=100-1e-9)recovered=true;

            hist[hc%hist.length]=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);hc++;

            if(bank>=200-1e-9){x.reached200=true;break;}
            if(bank<=1e-9){x.exhausted=true;break;}

            if(exitThreshold>0 && bank>=exitThreshold-1e-9){
                x.exited=true;x.exitHand=hand;x.exitBank=bank;break;
            }

            if(everPlunge&&bank<=100&&!cooling){cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;}
        }
        x.finalBank=bank;x.peak=peak;x.maxDD=maxDD;x.cumWager=cum;
        x.everBelow100=everBelow;x.recovered100=recovered;
        x.maxProfitGiveback=Math.max(0,peak-bank);
        return x;
    }

    static ArrayList<S394> run394(long seed,String cohort,int threshold,String policy){
        ArrayList<S394>a=new ArrayList<>();
        for(int i=0;i<SESSIONS;i++)a.add(runOne394(seed,cohort,i,threshold,policy));
        return a;
    }

    static void summary394(StringBuilder r,String cohort,String policy,ArrayList<S394>a){
        long n=a.size(),pos=0,be=0,loss=0,t200=0,bust=0,exit=0,below=0,rec=0;
        double fin=0,dd=0,w=0,gb=0,exitHand=0,exitBank=0;
        for(S394 s:a){
            fin+=s.finalBank;dd+=s.maxDD;w+=s.cumWager;gb+=s.maxProfitGiveback;
            if(s.finalBank>100+1e-9)pos++;else if(Math.abs(s.finalBank-100)<1e-9)be++;else loss++;
            if(s.reached200)t200++;if(s.exhausted)bust++;
            if(s.exited){exit++;exitHand+=s.exitHand;exitBank+=s.exitBank;}
            if(s.everBelow100){below++;if(s.recovered100)rec++;}
        }
        double net=fin-100.0*n;
        r.append("\n").append(cohort).append(" / ").append(policy).append("\n");
        r.append(String.format(Locale.ROOT,"Average ending bankroll: £%.3f%n",fin/n));
        r.append(String.format(Locale.ROOT,"Population net change: £%+.2f%n",net));
        r.append(String.format(Locale.ROOT,"Population return on cumulative wager: %+.4f%%%n",w==0?0:100*net/w));
        r.append("Positive / break-even / losing sessions: ").append(pct380(pos,n)).append(" / ").append(pct380(be,n)).append(" / ").append(pct380(loss,n)).append("\n");
        r.append("£200 attainment: ").append(pct380(t200,n)).append("\n");
        r.append("£0 exhaustion: ").append(pct380(bust,n)).append("\n");
        r.append(String.format(Locale.ROOT,"Average maximum drawdown: £%.3f%n",dd/n));
        r.append(String.format(Locale.ROOT,"Average cumulative wager: £%.3f%n",w/n));
        r.append("Sessions ever below £100: ").append(pct380(below,n)).append("\n");
        r.append("Recovery to >=£100 among below-£100 sessions: ").append(pct380(rec,below)).append("\n");
        r.append(String.format(Locale.ROOT,"Average peak-to-finish give-back: £%.3f%n",gb/n));
        r.append(String.format(Locale.ROOT,"Bankroll change per £100 wagered: £%+.3f%n",w==0?0:100*net/w));
        if(exit>0){
            r.append("Policy exits triggered: ").append(pct380(exit,n)).append("\n");
            r.append(String.format(Locale.ROOT,"Average exit hand / retained bankroll when triggered: %.2f / £%.3f%n",exitHand/exit,exitBank/exit));
        } else r.append("Policy exits triggered: N/A (reference)\n");
    }

    static String csv394(ArrayList<S394>...groups){
        StringBuilder b=new StringBuilder("cohort,policy,session,hands,final_bankroll,peak_bankroll,max_drawdown,cumulative_wager,reached_200,exhausted,positive,ever_below_100,recovered_100,peak_to_finish_giveback,exit_triggered,exit_hand,exit_bankroll\n");
        for(ArrayList<S394>g:groups)for(S394 s:g)
            b.append(String.format(Locale.ROOT,"%s,%s,%d,%d,%.2f,%.2f,%.2f,%.2f,%s,%s,%s,%s,%s,%.2f,%s,%d,%.2f%n",
                s.cohort,s.policy,s.session,s.hands,s.finalBank,s.peak,s.maxDD,s.cumWager,
                Boolean.toString(s.reached200),Boolean.toString(s.exhausted),Boolean.toString(s.finalBank>100+1e-9),
                Boolean.toString(s.everBelow100),Boolean.toString(s.recovered100),s.maxProfitGiveback,
                Boolean.toString(s.exited),s.exitHand,s.exitBank));
        return b.toString();
    }


    /* PHASE 3.95 — TRAILING / HIGH-WATER PROFIT-PROTECTION MAP
       MEASUREMENT ONLY.
       No session is stopped and no wager is changed.
       We retrospectively mark the FIRST observable occasion after a session has
       reached a qualifying high-water profit state where bankroll gives back
       25%, 50%, 75%, or 100% of the accumulated profit above £100.
       Activation floors £140/£150/£160 are mapped separately.
       This is candidate discovery, not an intervention. */

    static final int[] FLOORS395={140,150,160};
    static final int[] BANDS395={25,50,75,100};

    static final class S395 {
        String cohort; int session,hands;
        double finalBank,peak,maxDD,cumWager;
        boolean reached200,exhausted;
        int[][] trigHand=new int[FLOORS395.length][BANDS395.length];
        double[][] trigBank=new double[FLOORS395.length][BANDS395.length];
        double[][] trigPeak=new double[FLOORS395.length][BANDS395.length];
        double[][] postTrigExposure=new double[FLOORS395.length][BANDS395.length];
        boolean[][] later200=new boolean[FLOORS395.length][BANDS395.length];
        boolean[][] later0=new boolean[FLOORS395.length][BANDS395.length];
        boolean[][] laterBelow100=new boolean[FLOORS395.length][BANDS395.length];
    }

    static S395 runOne395(long seed,String cohort,int session){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));
        if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];
        double bank=START_BANK,peak=START_BANK,maxDD=0,cum=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        boolean[] activated=new boolean[FLOORS395.length];
        S395 x=new S395();x.cohort=cohort;x.session=session+1;
        for(int i=0;i<FLOORS395.length;i++)Arrays.fill(x.trigHand[i],-1);

        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}
            if(cautious&&bank>=140)cautious=false;
            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){
                int b=bal355(hist,hc);boolean neg=b<0;
                if(neg&&!negativeEpisode)brakeArmed=true;negativeEpisode=neg;
                double base=bank>=170?15:normal355(hand,bank);
                if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;
            } else {wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;}
            wager=Math.min(wager,bank);if(wager<=0)break;
            if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;

            PairStats ps=new PairStats();
            Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
            ptr=rr.endPointer;double stake=roundStake356(rr);cum+=stake;

            // Any already-triggered candidate continues to accumulate actual exposure.
            for(int i=0;i<FLOORS395.length;i++)for(int j=0;j<BANDS395.length;j++)
                if(x.trigHand[i][j]>=0)x.postTrigExposure[i][j]+=stake;

            bank+=rr.net;if(bank<0)bank=0;x.hands++;
            peak=Math.max(peak,bank);maxDD=Math.max(maxDD,peak-bank);

            for(int i=0;i<FLOORS395.length;i++){
                if(bank>=FLOORS395[i]-1e-9)activated[i]=true;
                if(activated[i] && peak>100+1e-9){
                    double profit=peak-100;
                    double give=Math.max(0,peak-bank);
                    double pct=100*give/profit;
                    for(int j=0;j<BANDS395.length;j++)if(x.trigHand[i][j]<0 && pct>=BANDS395[j]-1e-9){
                        x.trigHand[i][j]=hand;x.trigBank[i][j]=bank;x.trigPeak[i][j]=peak;
                    }
                }
                for(int j=0;j<BANDS395.length;j++)if(x.trigHand[i][j]>=0 && hand>x.trigHand[i][j]){
                    if(bank>=200-1e-9)x.later200[i][j]=true;
                    if(bank<=1e-9)x.later0[i][j]=true;
                    if(bank<100-1e-9)x.laterBelow100[i][j]=true;
                }
            }

            hist[hc%hist.length]=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);hc++;
            if(bank>=200){x.reached200=true;break;}
            if(bank<=0){x.exhausted=true;break;}
            if(everPlunge&&bank<=100&&!cooling){cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;}
        }
        x.finalBank=bank;x.peak=peak;x.maxDD=maxDD;x.cumWager=cum;return x;
    }

    static ArrayList<S395> run395(long seed,String cohort){
        ArrayList<S395>a=new ArrayList<>();for(int i=0;i<SESSIONS;i++)a.add(runOne395(seed,cohort,i));return a;
    }

    static void map395(StringBuilder r,String cohort,ArrayList<S395>a){
        r.append("\n").append(cohort).append("\n");
        r.append("Activation | Give-back trigger | Triggered sessions | Avg trigger hand | Avg peak at trigger | Avg bankroll at trigger | Later £200 | Later <£100 | Later £0 | Avg exposure after trigger\n");
        for(int i=0;i<FLOORS395.length;i++)for(int j=0;j<BANDS395.length;j++){
            long n=0,t200=0,below=0,bust=0;double h=0,p=0,b=0,e=0;
            for(S395 s:a)if(s.trigHand[i][j]>=0){
                n++;h+=s.trigHand[i][j];p+=s.trigPeak[i][j];b+=s.trigBank[i][j];e+=s.postTrigExposure[i][j];
                if(s.later200[i][j])t200++;if(s.laterBelow100[i][j])below++;if(s.later0[i][j])bust++;
            }
            r.append(String.format(Locale.ROOT,"£%d | %d%% | %d (%.2f%%) | %.2f | £%.2f | £%.2f | %.2f%% | %.2f%% | %.2f%% | £%.2f%n",
                FLOORS395[i],BANDS395[j],n,100.0*n/a.size(),n==0?0:h/n,n==0?0:p/n,n==0?0:b/n,
                n==0?0:100.0*t200/n,n==0?0:100.0*below/n,n==0?0:100.0*bust/n,n==0?0:e/n));
        }
    }

    static String csv395(ArrayList<S395>...groups){
        StringBuilder b=new StringBuilder("cohort,session,activation_floor,giveback_trigger_pct,triggered,trigger_hand,peak_at_trigger,bankroll_at_trigger,post_trigger_exposure,later_200,later_below_100,later_0,final_bankroll\n");
        for(ArrayList<S395>g:groups)for(S395 s:g)for(int i=0;i<FLOORS395.length;i++)for(int j=0;j<BANDS395.length;j++)
            b.append(String.format(Locale.ROOT,"%s,%d,%d,%d,%s,%d,%.2f,%.2f,%.2f,%s,%s,%s,%.2f%n",
                s.cohort,s.session,FLOORS395[i],BANDS395[j],Boolean.toString(s.trigHand[i][j]>=0),s.trigHand[i][j],
                s.trigPeak[i][j],s.trigBank[i][j],s.postTrigExposure[i][j],
                Boolean.toString(s.later200[i][j]),Boolean.toString(s.laterBelow100[i][j]),Boolean.toString(s.later0[i][j]),s.finalBank));
        return b.toString();
    }


    /* PHASE 3.96 — PREDECLARED TRAILING PROFIT-STOP INTERVENTION
       Frozen before execution from Phase 3.95:
       activation floor £160;
       trigger = >=25% surrender of running accumulated profit above £100;
       action = terminate immediately after the completed round producing trigger.
       Comparators:
       unchanged 30-hand reference;
       hard EXIT160 contextual benchmark.
       No wager or multiplier rule is changed. */

    static final String REF396="REFERENCE_30_HAND";
    static final String HARD396="HARD_EXIT_160";
    static final String TRAIL396="TRAIL_160_GIVEBACK_25";

    static final class S396 {
        String cohort,policy; int session,hands,stopHand;
        double finalBank,peak,maxDD,cumWager,stopBank;
        boolean reached200,exhausted,stopped,everBelow100,recovered100;
        double giveback;
    }

    static S396 runOne396(long seed,String cohort,int session,String policy){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));
        if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];
        double bank=START_BANK,peak=START_BANK,maxDD=0,cum=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        boolean activated160=false,everBelow=false,recovered=false;
        S396 x=new S396();x.cohort=cohort;x.policy=policy;x.session=session+1;

        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}
            if(cautious&&bank>=140)cautious=false;

            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){
                int b=bal355(hist,hc);boolean neg=b<0;
                if(neg&&!negativeEpisode)brakeArmed=true;
                negativeEpisode=neg;
                double base=bank>=170?15:normal355(hand,bank);
                if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;
            } else {
                wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;
            }

            wager=Math.min(wager,bank);if(wager<=0)break;
            if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;

            PairStats ps=new PairStats();
            Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
            ptr=rr.endPointer;
            cum+=roundStake356(rr);
            bank+=rr.net;if(bank<0)bank=0;x.hands++;

            peak=Math.max(peak,bank);
            maxDD=Math.max(maxDD,peak-bank);

            if(bank<100-1e-9)everBelow=true;
            if(everBelow&&bank>=100-1e-9)recovered=true;

            hist[hc%hist.length]=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);hc++;

            // Existing target/exhaustion termination retains precedence.
            if(bank>=200-1e-9){x.reached200=true;break;}
            if(bank<=1e-9){x.exhausted=true;break;}

            if(bank>=160-1e-9)activated160=true;

            if(policy.equals(HARD396) && activated160){
                x.stopped=true;x.stopHand=hand;x.stopBank=bank;break;
            }

            if(policy.equals(TRAIL396) && activated160 && peak>100+1e-9){
                double accumulatedProfit=peak-100.0;
                double surrendered=Math.max(0,peak-bank);
                double givePct=100.0*surrendered/accumulatedProfit;
                if(givePct>=25.0-1e-9){
                    x.stopped=true;x.stopHand=hand;x.stopBank=bank;break;
                }
            }

            if(everPlunge&&bank<=100&&!cooling){
                cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;
            }
        }

        x.finalBank=bank;x.peak=peak;x.maxDD=maxDD;x.cumWager=cum;
        x.everBelow100=everBelow;x.recovered100=recovered;
        x.giveback=Math.max(0,peak-bank);
        return x;
    }

    static ArrayList<S396> run396(long seed,String cohort,String policy){
        ArrayList<S396>a=new ArrayList<>();
        for(int i=0;i<SESSIONS;i++)a.add(runOne396(seed,cohort,i,policy));
        return a;
    }

    static void summary396(StringBuilder r,String cohort,String policy,ArrayList<S396>a){
        long n=a.size(),pos=0,be=0,loss=0,t200=0,bust=0,stop=0,below=0,rec=0;
        double fin=0,dd=0,w=0,gb=0,sh=0,sb=0;
        for(S396 s:a){
            fin+=s.finalBank;dd+=s.maxDD;w+=s.cumWager;gb+=s.giveback;
            if(s.finalBank>100+1e-9)pos++;else if(Math.abs(s.finalBank-100)<1e-9)be++;else loss++;
            if(s.reached200)t200++;if(s.exhausted)bust++;
            if(s.stopped){stop++;sh+=s.stopHand;sb+=s.stopBank;}
            if(s.everBelow100){below++;if(s.recovered100)rec++;}
        }
        double net=fin-100.0*n;
        r.append("\n").append(cohort).append(" / ").append(policy).append("\n");
        r.append(String.format(Locale.ROOT,"Average ending bankroll: £%.3f%n",fin/n));
        r.append(String.format(Locale.ROOT,"Population net change: £%+.2f%n",net));
        r.append(String.format(Locale.ROOT,"Population return on cumulative wager: %+.4f%%%n",w==0?0:100.0*net/w));
        r.append("Positive / break-even / losing sessions: ").append(pct380(pos,n)).append(" / ").append(pct380(be,n)).append(" / ").append(pct380(loss,n)).append("\n");
        r.append("£200 attainment: ").append(pct380(t200,n)).append("\n");
        r.append("£0 exhaustion: ").append(pct380(bust,n)).append("\n");
        r.append(String.format(Locale.ROOT,"Average maximum drawdown: £%.3f%n",dd/n));
        r.append(String.format(Locale.ROOT,"Average cumulative wager: £%.3f%n",w/n));
        r.append("Sessions ever below £100: ").append(pct380(below,n)).append("\n");
        r.append("Recovery to >=£100 among below-£100 sessions: ").append(pct380(rec,below)).append("\n");
        r.append(String.format(Locale.ROOT,"Average peak-to-finish give-back: £%.3f%n",gb/n));
        r.append(String.format(Locale.ROOT,"Bankroll change per £100 wagered: £%+.3f%n",w==0?0:100.0*net/w));
        if(stop>0){
            r.append("Policy stops triggered: ").append(pct380(stop,n)).append("\n");
            r.append(String.format(Locale.ROOT,"Average stop hand / retained bankroll: %.2f / £%.3f%n",sh/stop,sb/stop));
        } else r.append("Policy stops triggered: N/A / none\n");
    }

    static String csv396(ArrayList<S396>...groups){
        StringBuilder b=new StringBuilder("cohort,policy,session,hands,final_bankroll,peak_bankroll,max_drawdown,cumulative_wager,reached_200,exhausted,positive,ever_below_100,recovered_100,peak_to_finish_giveback,stop_triggered,stop_hand,stop_bankroll\n");
        for(ArrayList<S396>g:groups)for(S396 s:g)
            b.append(String.format(Locale.ROOT,"%s,%s,%d,%d,%.2f,%.2f,%.2f,%.2f,%s,%s,%s,%s,%s,%.2f,%s,%d,%.2f%n",
                s.cohort,s.policy,s.session,s.hands,s.finalBank,s.peak,s.maxDD,s.cumWager,
                Boolean.toString(s.reached200),Boolean.toString(s.exhausted),
                Boolean.toString(s.finalBank>100+1e-9),Boolean.toString(s.everBelow100),
                Boolean.toString(s.recovered100),s.giveback,Boolean.toString(s.stopped),s.stopHand,s.stopBank));
        return b.toString();
    }


    /* PHASE 3.97 — PAIRED CAUSAL DECOMPOSITION
       No new policy and no threshold tuning.
       Pair the frozen Phase 3.96 TRAIL160/25 outcome with the unchanged
       reference outcome for the identical cohort/session seed.
       Classification concerns only sessions in which the trailing stop fires. */

    static final class P397 {
        String cohort; int session;
        S396 ref,trail;
        String className;
        double finalDelta,wagerRemoved,drawdownReduced,profitPreserved;
    }

    static String class397(S396 r,S396 t){
        if(!t.stopped)return "UNCHANGED_NOT_TRIGGERED";
        if(r.reached200)return "PREMATURE_EXIT_SACRIFICED_200";
        if(r.exhausted)return "GOOD_SAVE_PREVENTED_0";
        if(r.finalBank<100-1e-9 && t.finalBank>=100-1e-9)return "GOOD_SAVE_PREVENTED_LOSS_FINISH";
        if(t.finalBank>r.finalBank+1e-9)return "GOOD_SAVE_HIGHER_FINAL";
        if(Math.abs(t.finalBank-r.finalBank)<=1e-9)return "NEUTRAL_SAME_FINAL";
        return "PREMATURE_EXIT_LOWER_FINAL";
    }

    static ArrayList<P397> pair397(long seed,String cohort){
        ArrayList<P397>a=new ArrayList<>();
        for(int i=0;i<SESSIONS;i++){
            S396 r=runOne396(seed,cohort,i,REF396);
            S396 t=runOne396(seed,cohort,i,TRAIL396);
            P397 p=new P397();p.cohort=cohort;p.session=i+1;p.ref=r;p.trail=t;
            p.className=class397(r,t);
            p.finalDelta=t.finalBank-r.finalBank;
            p.wagerRemoved=r.cumWager-t.cumWager;
            p.drawdownReduced=r.maxDD-t.maxDD;
            p.profitPreserved=t.finalBank-r.finalBank;
            a.add(p);
        }return a;
    }

    static void summary397(StringBuilder r,String cohort,ArrayList<P397>a){
        long trig=0,ref200=0,ref0=0,refLoss=0,refPos=0;
        long higher=0,lower=0,same=0,good=0,prem=0;
        double dFinal=0,wRemoved=0,ddReduced=0,stopBank=0,stopHand=0;
        TreeMap<String,Integer> cls=new TreeMap<>();
        for(P397 p:a){
            if(!p.trail.stopped)continue;
            trig++;
            cls.put(p.className,cls.getOrDefault(p.className,0)+1);
            if(p.ref.reached200)ref200++;
            if(p.ref.exhausted)ref0++;
            if(p.ref.finalBank<100-1e-9)refLoss++;
            if(p.ref.finalBank>100+1e-9)refPos++;
            if(p.finalDelta>1e-9)higher++;else if(p.finalDelta<-1e-9)lower++;else same++;
            if(p.className.startsWith("GOOD_SAVE"))good++;
            if(p.className.startsWith("PREMATURE_EXIT"))prem++;
            dFinal+=p.finalDelta;wRemoved+=p.wagerRemoved;ddReduced+=p.drawdownReduced;
            stopBank+=p.trail.stopBank;stopHand+=p.trail.stopHand;
        }
        r.append("\n").append(cohort).append("\n");
        r.append("Trailing-triggered paired sessions: ").append(trig).append(" (").append(pct380(trig,a.size())).append(")\n");
        r.append("REFERENCE fate of those same sessions:\n");
        r.append("  Would later reach £200: ").append(pct380(ref200,trig)).append("\n");
        r.append("  Would later exhaust £0: ").append(pct380(ref0,trig)).append("\n");
        r.append("  Would finish below £100: ").append(pct380(refLoss,trig)).append("\n");
        r.append("  Would finish above £100: ").append(pct380(refPos,trig)).append("\n");
        r.append("TRAILING versus paired REFERENCE final bankroll:\n");
        r.append("  Higher under trailing: ").append(pct380(higher,trig)).append("\n");
        r.append("  Lower under trailing: ").append(pct380(lower,trig)).append("\n");
        r.append("  Same: ").append(pct380(same,trig)).append("\n");
        r.append(String.format(Locale.ROOT,"  Average paired final-bankroll change: £%+.3f%n",trig==0?0:dFinal/trig));
        r.append(String.format(Locale.ROOT,"Average wager removed per triggered session: £%.3f%n",trig==0?0:wRemoved/trig));
        r.append(String.format(Locale.ROOT,"Average max-drawdown reduction per triggered session: £%+.3f%n",trig==0?0:ddReduced/trig));
        r.append(String.format(Locale.ROOT,"Average trigger hand / retained bankroll: %.2f / £%.3f%n",trig==0?0:stopHand/trig,trig==0?0:stopBank/trig));
        r.append("CLASSIFICATION (mutually exclusive):\n");
        for(Map.Entry<String,Integer>e:cls.entrySet())
            r.append("  ").append(e.getKey()).append(": ").append(e.getValue()).append(" (").append(pct380(e.getValue(),trig)).append(")\n");
        r.append("  Broad GOOD_SAVE classes: ").append(pct380(good,trig)).append("\n");
        r.append("  Broad PREMATURE_EXIT classes: ").append(pct380(prem,trig)).append("\n");
    }

    static String csv397(ArrayList<P397>...groups){
        StringBuilder b=new StringBuilder("cohort,session,trail_triggered,class,trigger_hand,trigger_bankroll,reference_final,trailing_final,final_delta,reference_reached_200,reference_exhausted,reference_cumulative_wager,trailing_cumulative_wager,wager_removed,reference_max_drawdown,trailing_max_drawdown,drawdown_reduced\n");
        for(ArrayList<P397>g:groups)for(P397 p:g)
            b.append(String.format(Locale.ROOT,"%s,%d,%s,%s,%d,%.2f,%.2f,%.2f,%.2f,%s,%s,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f%n",
                p.cohort,p.session,Boolean.toString(p.trail.stopped),p.className,p.trail.stopHand,p.trail.stopBank,
                p.ref.finalBank,p.trail.finalBank,p.finalDelta,Boolean.toString(p.ref.reached200),Boolean.toString(p.ref.exhausted),
                p.ref.cumWager,p.trail.cumWager,p.wagerRemoved,p.ref.maxDD,p.trail.maxDD,p.drawdownReduced));
        return b.toString();
    }


    /* PHASE 3.98 — FROZEN TRIGGER-STATE SEPARATION MAP
       No selective intervention.
       Freeze the TRAIL160/25 trigger from Phases 3.95–3.97 and record only
       information legitimately observable at that trigger.
       Then label the same session by its unchanged REFERENCE future:
       LATER_200, LATER_COLLAPSE (<£100 final, including £0), or MIDDLE.
       This is descriptive separation evidence, not future-card prediction. */

    static final class T398 {
        String cohort; int session,triggerHand;
        double triggerBank,highWater,absGiveback,givebackPct,cumExposure;
        int recentBalance,recentWins,recentLosses,recentPushes;
        boolean priorBelow100,cooling,cautious,brakeArmed;
        String futureClass;
        double refFinal;
        boolean ref200,ref0;
    }

    static T398 observe398(long seed,String cohort,int session){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));
        if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];
        double bank=START_BANK,peak=START_BANK,cum=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        boolean activated160=false,priorBelow=false;

        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}
            if(cautious&&bank>=140)cautious=false;

            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){
                int b=bal355(hist,hc);boolean neg=b<0;
                if(neg&&!negativeEpisode)brakeArmed=true;
                negativeEpisode=neg;
                double base=bank>=170?15:normal355(hand,bank);
                if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;
            } else {wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;}

            wager=Math.min(wager,bank);if(wager<=0)break;
            if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;

            PairStats ps=new PairStats();
            Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
            ptr=rr.endPointer;cum+=roundStake356(rr);
            bank+=rr.net;if(bank<0)bank=0;
            peak=Math.max(peak,bank);

            int outcome=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);
            hist[hc%hist.length]=outcome;hc++;

            if(bank<100-1e-9)priorBelow=true;
            if(bank>=200-1e-9||bank<=1e-9)break;
            if(bank>=160-1e-9)activated160=true;

            if(activated160 && peak>100+1e-9){
                double abs=peak-bank;
                double pct=100.0*abs/(peak-100.0);
                if(pct>=25.0-1e-9){
                    T398 t=new T398();t.cohort=cohort;t.session=session+1;t.triggerHand=hand;
                    t.triggerBank=bank;t.highWater=peak;t.absGiveback=abs;t.givebackPct=pct;t.cumExposure=cum;
                    t.priorBelow100=priorBelow;t.cooling=cooling;t.cautious=cautious;t.brakeArmed=brakeArmed;
                    int lim=Math.min(hc,hist.length);
                    for(int k=0;k<lim;k++){
                        int v=hist[(hc-1-k+hist.length)%hist.length];
                        if(v>0)t.recentWins++;else if(v<0)t.recentLosses++;else t.recentPushes++;
                    }
                    t.recentBalance=t.recentWins-t.recentLosses;
                    return t;
                }
            }
            if(everPlunge&&bank<=100&&!cooling){
                cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;
            }
        }
        return null;
    }

    static ArrayList<T398> run398(long seed,String cohort){
        ArrayList<T398>a=new ArrayList<>();
        for(int i=0;i<SESSIONS;i++){
            T398 t=observe398(seed,cohort,i);
            if(t==null)continue;
            S396 ref=runOne396(seed,cohort,i,REF396);
            t.refFinal=ref.finalBank;t.ref200=ref.reached200;t.ref0=ref.exhausted;
            if(ref.reached200)t.futureClass="LATER_200";
            else if(ref.finalBank<100-1e-9)t.futureClass="LATER_COLLAPSE";
            else t.futureClass="MIDDLE";
            a.add(t);
        }return a;
    }

    static void classSummary398(StringBuilder r,String cohort,ArrayList<T398>a){
        r.append("\n").append(cohort).append("\n");
        r.append("Future class | N | Trigger hand | Trigger bank | High-water | Abs give-back | Give-back % | Exposure so far | Recent W-L balance | Prior <£100\n");
        for(String c:new String[]{"LATER_200","LATER_COLLAPSE","MIDDLE"}){
            long n=0,prior=0;double hand=0,bank=0,peak=0,abs=0,pct=0,exp=0,bal=0;
            for(T398 t:a)if(t.futureClass.equals(c)){
                n++;hand+=t.triggerHand;bank+=t.triggerBank;peak+=t.highWater;abs+=t.absGiveback;
                pct+=t.givebackPct;exp+=t.cumExposure;bal+=t.recentBalance;if(t.priorBelow100)prior++;
            }
            r.append(String.format(Locale.ROOT,"%s | %d (%.2f%%) | %.2f | £%.2f | £%.2f | £%.2f | %.2f%% | £%.2f | %.3f | %.2f%%%n",
                c,n,100.0*n/a.size(),n==0?0:hand/n,n==0?0:bank/n,n==0?0:peak/n,n==0?0:abs/n,
                n==0?0:pct/n,n==0?0:exp/n,n==0?0:bal/n,n==0?0:100.0*prior/n));
        }

        // Predeclared coarse bins for descriptive separation only.
        r.append("\nCOARSE OBSERVABLE BINS — later £200 rate / later collapse rate\n");
        String[] hn={"hand <=12","hand 13-16","hand >=17"};
        for(int q=0;q<3;q++){
            long n=0,y=0,c=0;
            for(T398 t:a){
                boolean ok=q==0?t.triggerHand<=12:q==1?(t.triggerHand>=13&&t.triggerHand<=16):t.triggerHand>=17;
                if(ok){n++;if(t.ref200)y++;if(t.futureClass.equals("LATER_COLLAPSE"))c++;}
            }
            r.append(String.format(Locale.ROOT,"%s: N=%d, £200 %.2f%%, collapse %.2f%%%n",hn[q],n,n==0?0:100.0*y/n,n==0?0:100.0*c/n));
        }
        String[] bn={"trigger bank <£130","£130-£139.99","trigger bank >=£140"};
        for(int q=0;q<3;q++){
            long n=0,y=0,c=0;
            for(T398 t:a){
                boolean ok=q==0?t.triggerBank<130:q==1?(t.triggerBank>=130&&t.triggerBank<140):t.triggerBank>=140;
                if(ok){n++;if(t.ref200)y++;if(t.futureClass.equals("LATER_COLLAPSE"))c++;}
            }
            r.append(String.format(Locale.ROOT,"%s: N=%d, £200 %.2f%%, collapse %.2f%%%n",bn[q],n,n==0?0:100.0*y/n,n==0?0:100.0*c/n));
        }
        String[] rb={"recent balance <=-2","recent balance -1 to 0","recent balance >=1"};
        for(int q=0;q<3;q++){
            long n=0,y=0,c=0;
            for(T398 t:a){
                boolean ok=q==0?t.recentBalance<=-2:q==1?(t.recentBalance>=-1&&t.recentBalance<=0):t.recentBalance>=1;
                if(ok){n++;if(t.ref200)y++;if(t.futureClass.equals("LATER_COLLAPSE"))c++;}
            }
            r.append(String.format(Locale.ROOT,"%s: N=%d, £200 %.2f%%, collapse %.2f%%%n",rb[q],n,n==0?0:100.0*y/n,n==0?0:100.0*c/n));
        }
    }

    static String csv398(ArrayList<T398>...groups){
        StringBuilder b=new StringBuilder("cohort,session,future_class,trigger_hand,trigger_bankroll,high_water,absolute_giveback,giveback_pct,cumulative_exposure,recent_wins,recent_losses,recent_pushes,recent_balance,prior_below_100,cooling,cautious,brake_armed,reference_final,reference_reached_200,reference_exhausted\n");
        for(ArrayList<T398>g:groups)for(T398 t:g)
            b.append(String.format(Locale.ROOT,"%s,%d,%s,%d,%.2f,%.2f,%.2f,%.4f,%.2f,%d,%d,%d,%d,%s,%s,%s,%s,%.2f,%s,%s%n",
                t.cohort,t.session,t.futureClass,t.triggerHand,t.triggerBank,t.highWater,t.absGiveback,t.givebackPct,
                t.cumExposure,t.recentWins,t.recentLosses,t.recentPushes,t.recentBalance,
                Boolean.toString(t.priorBelow100),Boolean.toString(t.cooling),Boolean.toString(t.cautious),
                Boolean.toString(t.brakeArmed),t.refFinal,Boolean.toString(t.ref200),Boolean.toString(t.ref0)));
        return b.toString();
    }


    /* PHASE 3.99 — PREDECLARED SELECTIVE TRAILING-SEVERITY GATE
       Frozen before execution in Phase 3.98:
       existing activation >=£160;
       existing >=25% high-water-profit give-back trigger;
       STOP only when completed trigger round leaves bankroll <£140;
       otherwise continue normally.
       No threshold tuning. No wager/multiplier change. */

    static final String SELECT399="SELECTIVE_TRAIL_160_25_BANK140";

    static S396 runOne399(long seed,String cohort,int session,String policy){
        int[]shoe=sixDeckShoe(sessionSeed(seed,session));
        if(!auditShoe(shoe))throw new IllegalStateException("Inventory audit failed");
        int ptr=0,hc=0;int[]hist=new int[5];
        double bank=START_BANK,peak=START_BANK,maxDD=0,cum=0;
        boolean everPlunge=false,cooling=false,cautious=false,negativeEpisode=false,brakeArmed=false;
        boolean activated160=false,everBelow=false,recovered=false;
        S396 x=new S396();x.cohort=cohort;x.policy=policy;x.session=session+1;

        for(int hand=1;hand<=HANDS;hand++){
            if(bank>=200||bank<=0)break;
            if(cooling&&bank>=120){cooling=false;cautious=true;}
            if(cautious&&bank>=140)cautious=false;

            double wager;
            if(cooling){wager=10;negativeEpisode=false;brakeArmed=false;}
            else if(cautious){wager=cautious355(hist,hc);negativeEpisode=false;brakeArmed=false;}
            else if(hand>8&&bank>=140){
                int b=bal355(hist,hc);boolean neg=b<0;
                if(neg&&!negativeEpisode)brakeArmed=true;
                negativeEpisode=neg;
                double base=bank>=170?15:normal355(hand,bank);
                if(brakeArmed){wager=Math.min(base,15);brakeArmed=false;}else wager=base;
            } else {wager=normal355(hand,bank);negativeEpisode=false;brakeArmed=false;}

            wager=Math.min(wager,bank);if(wager<=0)break;
            if(!cooling&&!cautious&&hand>8&&bank>=130)everPlunge=true;

            PairStats ps=new PairStats();
            Round rr=playRound(shoe,ptr,Profile.SELECTIVE_AGGRESSION,cohort,session+1,hand,true,bank,ps,wager,false);
            ptr=rr.endPointer;cum+=roundStake356(rr);
            bank+=rr.net;if(bank<0)bank=0;x.hands++;
            peak=Math.max(peak,bank);maxDD=Math.max(maxDD,peak-bank);

            if(bank<100-1e-9)everBelow=true;
            if(everBelow&&bank>=100-1e-9)recovered=true;

            hist[hc%hist.length]=rr.net>1e-9?1:(rr.net<-1e-9?-1:0);hc++;

            if(bank>=200-1e-9){x.reached200=true;break;}
            if(bank<=1e-9){x.exhausted=true;break;}
            if(bank>=160-1e-9)activated160=true;

            if(policy.equals(HARD396) && activated160){
                x.stopped=true;x.stopHand=hand;x.stopBank=bank;break;
            }

            if((policy.equals(TRAIL396)||policy.equals(SELECT399)) && activated160 && peak>100+1e-9){
                double accumulatedProfit=peak-100.0;
                double surrendered=Math.max(0,peak-bank);
                double givePct=100.0*surrendered/accumulatedProfit;
                if(givePct>=25.0-1e-9){
                    boolean stop=policy.equals(TRAIL396) || bank<140.0-1e-9;
                    if(stop){x.stopped=true;x.stopHand=hand;x.stopBank=bank;break;}
                }
            }

            if(everPlunge&&bank<=100&&!cooling){
                cooling=true;cautious=false;negativeEpisode=false;brakeArmed=false;
            }
        }

        x.finalBank=bank;x.peak=peak;x.maxDD=maxDD;x.cumWager=cum;
        x.everBelow100=everBelow;x.recovered100=recovered;
        x.giveback=Math.max(0,peak-bank);
        return x;
    }

    static ArrayList<S396> run399(long seed,String cohort,String policy){
        ArrayList<S396>a=new ArrayList<>();
        for(int i=0;i<SESSIONS;i++)a.add(runOne399(seed,cohort,i,policy));
        return a;
    }

    static String csv399(ArrayList<S396>...groups){
        StringBuilder b=new StringBuilder("cohort,policy,session,hands,final_bankroll,peak_bankroll,max_drawdown,cumulative_wager,reached_200,exhausted,positive,ever_below_100,recovered_100,peak_to_finish_giveback,stop_triggered,stop_hand,stop_bankroll\n");
        for(ArrayList<S396>g:groups)for(S396 x:g)
            b.append(String.format(Locale.ROOT,"%s,%s,%d,%d,%.2f,%.2f,%.2f,%.2f,%s,%s,%s,%s,%s,%.2f,%s,%d,%.2f%n",
                x.cohort,x.policy,x.session,x.hands,x.finalBank,x.peak,x.maxDD,x.cumWager,
                Boolean.toString(x.reached200),Boolean.toString(x.exhausted),Boolean.toString(x.finalBank>100+1e-9),
                Boolean.toString(x.everBelow100),Boolean.toString(x.recovered100),x.giveback,
                Boolean.toString(x.stopped),x.stopHand,x.stopBank));
        return b.toString();
    }


    /* PHASE 3.100 — RELEASED-SESSION PAIRED CAUSAL DECOMPOSITION
       No new intervention and no threshold tuning.
       A RELEASED session is one that unconditional TRAIL160/25 stops but the
       frozen selective £140 gate allows to continue.
       Pair identical deterministic sessions under:
       REFERENCE, unconditional TRAIL160/25, and SELECTIVE £140 gate. */

    static final class P3100 {
        String cohort; int session;
        S396 ref,uncond,select;
        boolean released;
        double selectVsUncondFinal,selectVsUncondWager,selectVsUncondDD;
        String releaseOutcome;
    }

    static ArrayList<P3100> pair3100(long seed,String cohort){
        ArrayList<P3100>a=new ArrayList<>();
        for(int i=0;i<SESSIONS;i++){
            S396 r=runOne399(seed,cohort,i,REF396);
            S396 u=runOne399(seed,cohort,i,TRAIL396);
            S396 q=runOne399(seed,cohort,i,SELECT399);
            P3100 p=new P3100();p.cohort=cohort;p.session=i+1;p.ref=r;p.uncond=u;p.select=q;
            p.released=u.stopped && !q.stopped;
            p.selectVsUncondFinal=q.finalBank-u.finalBank;
            p.selectVsUncondWager=q.cumWager-u.cumWager;
            p.selectVsUncondDD=q.maxDD-u.maxDD;
            if(!p.released)p.releaseOutcome="NOT_RELEASED";
            else if(q.reached200)p.releaseOutcome="RELEASED_TO_200";
            else if(q.exhausted)p.releaseOutcome="RELEASED_TO_0";
            else if(q.finalBank>u.finalBank+1e-9)p.releaseOutcome="RELEASED_HIGHER_FINAL";
            else if(q.finalBank<u.finalBank-1e-9)p.releaseOutcome="RELEASED_LOWER_FINAL";
            else p.releaseOutcome="RELEASED_SAME_FINAL";
            a.add(p);
        }return a;
    }

    static void sum3100(StringBuilder r,String cohort,ArrayList<P3100>a){
        long rel=0,to200=0,to0=0,higher=0,lower=0,same=0,below100=0;
        double df=0,dw=0,ddd=0,rf=0,uf=0,sf=0;
        TreeMap<String,Integer> cls=new TreeMap<>();
        for(P3100 p:a)if(p.released){
            rel++;cls.put(p.releaseOutcome,cls.getOrDefault(p.releaseOutcome,0)+1);
            if(p.select.reached200)to200++;
            if(p.select.exhausted)to0++;
            if(p.select.finalBank<100-1e-9)below100++;
            if(p.selectVsUncondFinal>1e-9)higher++;else if(p.selectVsUncondFinal<-1e-9)lower++;else same++;
            df+=p.selectVsUncondFinal;dw+=p.selectVsUncondWager;ddd+=p.selectVsUncondDD;
            rf+=p.ref.finalBank;uf+=p.uncond.finalBank;sf+=p.select.finalBank;
        }
        r.append("\n").append(cohort).append("\n");
        r.append("Sessions released by £140 gate: ").append(rel).append(" (").append(pct380(rel,a.size())).append(" of all sessions)\n");
        r.append("Released-session selective outcomes:\n");
        r.append("  Reached £200: ").append(pct380(to200,rel)).append("\n");
        r.append("  Exhausted £0: ").append(pct380(to0,rel)).append("\n");
        r.append("  Finished below £100: ").append(pct380(below100,rel)).append("\n");
        r.append("Selective final vs unconditional retained bankroll:\n");
        r.append("  Higher: ").append(pct380(higher,rel)).append("\n");
        r.append("  Lower: ").append(pct380(lower,rel)).append("\n");
        r.append("  Same: ").append(pct380(same,rel)).append("\n");
        r.append(String.format(Locale.ROOT,"Average final bankroll — Reference / Unconditional / Selective: £%.3f / £%.3f / £%.3f%n",
            rel==0?0:rf/rel,rel==0?0:uf/rel,rel==0?0:sf/rel));
        r.append(String.format(Locale.ROOT,"Average selective-minus-unconditional final delta: £%+.3f%n",rel==0?0:df/rel));
        r.append(String.format(Locale.ROOT,"Average extra wager reintroduced by release: £%.3f%n",rel==0?0:dw/rel));
        r.append(String.format(Locale.ROOT,"Average extra maximum drawdown from release: £%+.3f%n",rel==0?0:ddd/rel));
        r.append("Mutually exclusive release outcomes:\n");
        for(Map.Entry<String,Integer>e:cls.entrySet())
            r.append("  ").append(e.getKey()).append(": ").append(e.getValue()).append(" (").append(pct380(e.getValue(),rel)).append(")\n");
    }

    static String csv3100(ArrayList<P3100>...groups){
        StringBuilder b=new StringBuilder("cohort,session,released_by_gate,release_outcome,reference_final,unconditional_final,selective_final,selective_minus_unconditional_final,reference_200,selective_200,selective_0,unconditional_stop_hand,unconditional_stop_bankroll,reference_wager,unconditional_wager,selective_wager,extra_wager_from_release,reference_drawdown,unconditional_drawdown,selective_drawdown,extra_drawdown_from_release\n");
        for(ArrayList<P3100>g:groups)for(P3100 p:g)
            b.append(String.format(Locale.ROOT,"%s,%d,%s,%s,%.2f,%.2f,%.2f,%.2f,%s,%s,%s,%d,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f%n",
                p.cohort,p.session,Boolean.toString(p.released),p.releaseOutcome,p.ref.finalBank,p.uncond.finalBank,
                p.select.finalBank,p.selectVsUncondFinal,Boolean.toString(p.ref.reached200),Boolean.toString(p.select.reached200),
                Boolean.toString(p.select.exhausted),p.uncond.stopHand,p.uncond.stopBank,p.ref.cumWager,p.uncond.cumWager,
                p.select.cumWager,p.selectVsUncondWager,p.ref.maxDD,p.uncond.maxDD,p.select.maxDD,p.selectVsUncondDD));
        return b.toString();
    }

    public static void main(String[]args)throws Exception{
        ArrayList<P3100>p=pair3100(PRIMARY_SEED,"PRIMARY");
        ArrayList<P3100>h=pair3100(HOLDOUT_SEED,"HOLDOUT");
        StringBuilder r=new StringBuilder();
        r.append("DETERMINISTIC BLACKJACK DECISION ENGINE RESEARCH\n");
        r.append("CODE VERSION: Deterministic Blackjack Phase 3.100\n");
        r.append("CODE BUILD TAG: DBDE-3.100-RELEASED-SESSION-PAIRED-DECOMPOSITION\n\n");
        r.append("PHASE 3.100 — SELECTIVE-GATE RELEASED-SESSION PAIRED CAUSAL DECOMPOSITION\n\n");
        r.append("DESIGN\nNo new intervention. No threshold tuning. Identify sessions stopped by unconditional TRAIL160/25 but released by the frozen £140 selective gate. Pair identical deterministic sessions across Reference, Unconditional, and Selective policies.\n");
        sum3100(r,"PRIMARY",p);sum3100(r,"HOLDOUT",h);
        r.append("\nINTERPRETATION GUARDRAIL\nThis phase decomposes the causal price and benefit of releasing stronger trigger states. It does not search for a better gate. A released session reaching £200 is an upper-target benefit; a lower final bankroll, extra exposure, or extra drawdown is a measurable cost. Both must remain visible.\n");
        String csv=csv3100(p,h),fp=hash(r.toString()+csv);
        write("Phase_3_100_Results.txt",r.toString());
        write("Phase_3_100_Released_Session_Decomposition.csv",csv);
        write("Phase_3_100_Replay_Verification.txt",
            "PHASE 3.100 REPLAY\nPrimary seed: "+PRIMARY_SEED+"\nHoldout seed: "+HOLDOUT_SEED+
            "\nPaired policies: reference / unconditional TRAIL160/25 / selective £140 gate"+
            "\nNo new intervention / no threshold tuning\nSHA-256: "+fp+"\n");
        System.out.print(r+"\nPHASE 3.100 REPLAY FINGERPRINT: "+fp+"\n");
    }
}
