import java.io.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.time.*;
import java.time.format.*;
import java.util.*;

public class LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger {
  enum A { HIT, STAND, DOUBLE, SPLIT }
  enum State { NORMAL, COOLING, CAUTIOUS, BRAKE }
  static class Card {
    final String rank, suit;
    final int value;
    Card(String rank,String suit,int value){this.rank=rank;this.suit=suit;this.value=value;}
    public String toString(){return rank+(suit.isEmpty()?"":suit);}
  }
  static final Scanner sc=new Scanner(System.in);
  static final List<String> log=new ArrayList<>();
  static final double PLATFORM_OFFSET=2400.0;
  static final double PLATFORM_START=2500.0;
  static final double PLATFORM_DEPLETED=2400.0;
  static double bank=PLATFORM_START, peak=PLATFORM_START, trough=PLATFORM_START, maxDD=0, exposure=0;
  static boolean reached130=false;
  static State state=State.NORMAL;
  static int w=0,l=0,p=0, wagerMatch=0,wagerAbove=0,wagerBelow=0, actionMatch=0,actionDiff=0;
  static int validationWarnings=0, shuffleEvents=0, cardsSinceShuffle=0, totalCardsObserved=0, amendmentsMade=0;
  static int textbookAlignedDecisions=0, frozenResearchOverrideDecisions=0, frozenPrimarySpecialDecisions=0;
  static final List<String> shuffleLog=new ArrayList<>();
  static final List<String> chronologyLog=new ArrayList<>();
  static final List<Card> currentPlayerPostDealCards=new ArrayList<>();
  static final List<Card> currentDealerCards=new ArrayList<>();
  static int chronologicalCardNumber=0;
  static boolean handWasSplit=false;
  static double handCommittedStake=0.0;
  static boolean personalMode=false;
  static int lowSinceShuffle=0, neutralSinceShuffle=0, highSinceShuffle=0;
  static final List<String> handSignatureLog=new ArrayList<>();
  static final Path CUMULATIVE_OUTPUT=Path.of("output.txt");
  static String currentInitialAction="NONE";
  static String lastSavedSessionId=null;
  static Path lastSavedReport=null;
  static final int DEFAULT_SHUFFLE_REPLICATES=5000;
  static final long SHUFFLE_SEED_BASE=202609050001L;

  static class HandSnapshot {
    double bank,peak,trough,maxDD,exposure; boolean reached130; State state;
    int w,l,p,wagerMatch,wagerAbove,wagerBelow,actionMatch,actionDiff;
    int validationWarnings,shuffleEvents,cardsSinceShuffle,totalCardsObserved;
    int textbookAlignedDecisions,frozenResearchOverrideDecisions,frozenPrimarySpecialDecisions;
    int logSize,shuffleLogSize,chronologySize,handSignatureLogSize,chronologicalCardNumber; int lowSinceShuffle,neutralSinceShuffle,highSinceShuffle; Map<String,Integer> counts;
    HandSnapshot(){
      this.bank=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.bank;
      this.peak=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.peak;
      this.trough=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.trough;
      this.maxDD=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.maxDD;
      this.exposure=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.exposure;
      this.reached130=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.reached130;
      this.state=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.state;
      this.w=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.w;
      this.l=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.l;
      this.p=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.p;
      this.wagerMatch=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerMatch;
      this.wagerAbove=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerAbove;
      this.wagerBelow=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerBelow;
      this.actionMatch=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionMatch;
      this.actionDiff=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionDiff;
      this.validationWarnings=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.validationWarnings;
      this.shuffleEvents=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.shuffleEvents;
      this.cardsSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.cardsSinceShuffle;
      this.totalCardsObserved=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.totalCardsObserved;
      this.textbookAlignedDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.textbookAlignedDecisions;
      this.frozenResearchOverrideDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenResearchOverrideDecisions;
      this.frozenPrimarySpecialDecisions=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenPrimarySpecialDecisions;
      this.logSize=log.size(); this.shuffleLogSize=shuffleLog.size(); this.chronologySize=chronologyLog.size(); this.handSignatureLogSize=handSignatureLog.size(); this.lowSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.lowSinceShuffle; this.neutralSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.neutralSinceShuffle; this.highSinceShuffle=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.highSinceShuffle; this.chronologicalCardNumber=LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.chronologicalCardNumber; this.counts=new LinkedHashMap<>(observedPhysicalCounts);
    }
    void restore(){
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.bank=bank; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.peak=peak; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.trough=trough; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.maxDD=maxDD; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.exposure=exposure;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.reached130=reached130; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.state=state;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.w=w; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.l=l; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.p=p;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerMatch=wagerMatch; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerAbove=wagerAbove; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.wagerBelow=wagerBelow; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionMatch=actionMatch; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.actionDiff=actionDiff;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.validationWarnings=validationWarnings; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.shuffleEvents=shuffleEvents; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.cardsSinceShuffle=cardsSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.totalCardsObserved=totalCardsObserved;
      LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.textbookAlignedDecisions=textbookAlignedDecisions; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenResearchOverrideDecisions=frozenResearchOverrideDecisions; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.frozenPrimarySpecialDecisions=frozenPrimarySpecialDecisions;
      while(log.size()>logSize) log.remove(log.size()-1); while(shuffleLog.size()>shuffleLogSize) shuffleLog.remove(shuffleLog.size()-1); while(chronologyLog.size()>chronologySize) chronologyLog.remove(chronologyLog.size()-1); while(handSignatureLog.size()>handSignatureLogSize) handSignatureLog.remove(handSignatureLog.size()-1); LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.lowSinceShuffle=lowSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.neutralSinceShuffle=neutralSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.highSinceShuffle=highSinceShuffle; LiveBlackjackCompanion_PersonalOrFrozen_OptionalHistory_Ledger.chronologicalCardNumber=chronologicalCardNumber;
      observedPhysicalCounts.clear(); observedPhysicalCounts.putAll(counts);
    }
  }

  public static void main(String[] z)throws Exception{
    System.out.println("DETERMINISTIC BLACKJACK - LIVE + POST-SESSION ANALYSIS");
    System.out.println("Self-contained workflow: L = live capture, S = local shuffle robustness, Q = exit.");
    while(true){
      String operation=choice("\nMAIN MENU: [L] LIVE SESSION / [S] RUN SHUFFLE ROBUSTNESS / [Q] QUIT: ","LSQ");
      if(operation.equals("Q")){ System.out.println("Program closed."); return; }
      if(operation.equals("S")){ runAutomaticShuffleAnalysis(); continue; }
      if(hasPendingShuffleAnalysis()){
        System.out.println("\n*** POST-SESSION ANALYSIS PENDING ***");
        System.out.println("The most recently completed live session has not yet completed S analysis.");
        if(choice("Run S before starting another live session? [Y/N]: ","YN").equals("Y")){runAutomaticShuffleAnalysis();continue;}
        if(!choice("Defer the previous S analysis and start another live session anyway? [Y/N]: ","YN").equals("Y"))continue;
        appendAnalysisDeferral();
      }
      runLiveSession();
    }
  }

  static void resetLiveSessionState(){
    log.clear();shuffleLog.clear();chronologyLog.clear();currentPlayerPostDealCards.clear();currentDealerCards.clear();handSignatureLog.clear();observedPhysicalCounts.clear();
    bank=PLATFORM_START;peak=PLATFORM_START;trough=PLATFORM_START;maxDD=0;exposure=0;reached130=false;state=State.NORMAL;w=l=p=wagerMatch=wagerAbove=wagerBelow=actionMatch=actionDiff=0;validationWarnings=shuffleEvents=cardsSinceShuffle=totalCardsObserved=amendmentsMade=0;textbookAlignedDecisions=frozenResearchOverrideDecisions=frozenPrimarySpecialDecisions=0;chronologicalCardNumber=0;handWasSplit=false;handCommittedStake=0;personalMode=false;lowSinceShuffle=neutralSinceShuffle=highSinceShuffle=0;currentInitialAction="NONE";
  }

  static void runLiveSession()throws Exception{
    resetLiveSessionState();
    say("\nDETERMINISTIC BLACKJACK - LIVE 30-HAND COMPANION");
    say("Manual external-session entry | Platform start £2500 | £2400 = depleted | 30-hand reference");
    say("SOURCE: 247blackjack.com | PLAY-MONEY | configured 6-deck game");
    say("VALIDATION ON: suit required, wager checked, balance arithmetic checked where determinable.");
    say("HAND REVIEW ON: rejected/amended current hands roll back before re-entry.");
    personalMode=choice("Mode: [P] PERSONAL (no Frozen prompts) / [F] FROZEN architecture: ","PF").equals("P");
    say(personalMode ? "PERSONAL MODE: play entirely by your own judgement. Frozen wager/action prompts are suppressed." : "FROZEN MODE: existing Frozen wager/action prompts remain active.");
    say("Cumulative project file: "+CUMULATIVE_OUTPUT.toAbsolutePath()+"\n");
    for(int h=1;h<=30 && bank>PLATFORM_DEPLETED;h++){
      while(true){
      HandSnapshot snapshot=new HandSnapshot();
      updateState(); double ref=wager(h);
      say("\n========== HAND "+h+" / 30 ==========");
      String sh=choice("Visible shuffle immediately BEFORE this hand? [Y/N]: ","YN");
      if(sh.equals("Y")){
        shuffleEvents++;
        shuffleLog.add("Shuffle observed before hand "+h+" after "+cardsSinceShuffle+" recorded cards since previous start/shuffle.");
        say("SHUFFLE RECORDED before hand "+h+" | previous segment cards recorded: "+cardsSinceShuffle);
        cardsSinceShuffle=0; lowSinceShuffle=neutralSinceShuffle=highSinceShuffle=0;
      }
      double researchBank=researchBankroll();
      if(personalMode){
        say("Bankroll £"+m(bank)+" | PERSONAL MODE");
        showPreWagerRecentHistory();
      } else {
        say("Bankroll £"+m(bank)+" | Research-equivalent available £"+m(researchBank)+" | State "+state+" | FROZEN WAGER £"+m(ref));
      }
      double actual=personalMode ? validatedPersonalWager() : validatedWager(ref);
      exposure+=actual;
      if(!personalMode) compareWager(actual,ref);
      double before=bank;
      handWasSplit=false; handCommittedStake=actual; currentInitialAction="NONE"; currentPlayerPostDealCards.clear(); currentDealerCards.clear();
      say("Initial visible deal entry: PLAYER first card -> DEALER upcard -> PLAYER second card.");
      Card p1=card("Player card 1");
      Card up=card("Dealer upcard");
      Card p2=card("Player card 2");
      List<Card> player=new ArrayList<>(); player.add(p1); player.add(p2);
      say("Player "+ct(player)+" = "+tt(player)+" | Dealer "+rt(up));
      showObservedHandContext(p1,p2,up);
      if(player.size()==2 && total(player)==21){ currentInitialAction="NATURAL"; say("Player 21/natural: no further player decision entered here."); }
      else play("MAIN",player,up,actual,true,true);
      String dealer=dealerSequence("Dealer hidden/draw sequence IN ORDER after upcard (e.g. 6H,10D,5C; or ?): ");
      showDealerTotal(up);

      // Show the reconstructed physical deal order BEFORE result/bankroll entry so the
      // operator can compare our interpretation with the game screen while the hand is fresh.
      showChronologyPreview(h,p1,up,p2);
      if(choice("Does this reconstructed card order correctly represent the hand? [Y/N]: ","YN").equals("N")){
        amendmentsMade++;
        snapshot.restore();
        say("CARD-STREAM REVIEW: hand "+h+" rejected before result/bankroll entry; re-entering the hand.");
        log.add("  CARD-STREAM REVIEW | Hand "+h+" rejected by operator; replacement entry follows.");
        continue;
      }

      String r=validatedResult(player,up); if(r.equals("W"))w++; else if(r.equals("L"))l++; else p++;
      boolean natural=(player.size()==2 && total(player)==21);
      say("BANKROLL CHECK: enter the updated platform balance shown immediately after the on-screen Win/Lose/Push verdict.");
      bank=validatedEndingBankroll(before,actual,handCommittedStake,r,natural,handWasSplit);

      String amend=choice("Hand "+h+" captured. Amend any details before moving on? [Y/N]: ","YN");
      if(amend.equals("Y")){
        amendmentsMade++;
        snapshot.restore();
        say("AMENDMENT: hand "+h+" superseded before commit and will now be re-entered from the beginning.");
        say("Previous hands remain committed. Current-hand counters/cards/exposure were rolled back.");
        log.add("  AMENDMENT AUDIT | Hand "+h+" superseded before commit; replacement entry follows.");
        continue;
      }

      appendChronology(h,p1,up,p2);
      String hist="HAND_HISTORY | "+signature(p1,p2,up)+" | mode "+(personalMode?"PERSONAL":"FROZEN")+" | action "+currentInitialAction+" | result "+r+" | bank "+m(before)+"->"+m(bank)+" | wager "+m(actual);
      handSignatureLog.add(hist);
      log.add("  "+hist);
      peak=Math.max(peak,bank); trough=Math.min(trough,bank); maxDD=Math.max(maxDD,peak-bank); if(bank>=2530) reached130=true;
      log.add(String.format(Locale.ROOT,"HAND %02d | %.2f->%.2f | ref %.2f actual %.2f | committed %.2f | %s | P %s | D-up %s | D-hidden/draw %s | %s",h,before,bank,ref,actual,handCommittedStake,state,ct(player),rt(up),dealer,r));
      say("Recorded | Peak £"+m(peak)+" | Trough £"+m(trough)+" | MaxDD £"+m(maxDD)+" | Exposure £"+m(exposure));
      if(bank<=PLATFORM_DEPLETED){
        say("DEPLETION THRESHOLD REACHED: platform balance £"+m(bank)+" <= £2400.00.");
        say("Research-equivalent bankroll: £"+m(bank-PLATFORM_OFFSET)+". Session stops here.");
      }
      break; // current hand committed
      }
      if(h<30 && line("ENTER next hand, or Q to finish: ").equalsIgnoreCase("Q")) break;
    }
    save();
  }

  static void updateState(){
    if(state==State.BRAKE)return;
    if(state==State.COOLING){ if(bank>=2520)state=State.CAUTIOUS; return; }
    if(state==State.CAUTIOUS){ if(bank>=2540)state=State.NORMAL; return; }
    if(reached130 && bank<=2500)state=State.COOLING;
  }
  static double wager(int h){
    if(state!=State.NORMAL)return 15;
    if(h<=8 || bank<2530)return 15;
    if(bank<2550)return 20;
    if(bank<2570)return 30;
    return 15;
  }
  static void compareWager(double a,double r){
    if(Math.abs(a-r)<.001){wagerMatch++;say("Wager: MATCH reference");}
    else if(a>r){wagerAbove++;say("Wager: £"+m(a-r)+" ABOVE reference");}
    else {wagerBelow++;say("Wager: £"+m(r-a)+" BELOW reference");}
  }

  static double researchBankroll(){
    return Math.max(0.0, bank-PLATFORM_OFFSET);
  }

  static double uncommittedResearchBankroll(){
    return Math.max(0.0, researchBankroll()-handCommittedStake);
  }

  static boolean canCommitAdditional(double amount){
    return uncommittedResearchBankroll()+0.001 >= amount;
  }

  static void play(String label,List<Card> c,Card upCard,double stake,boolean first,boolean allowSplit){
    int up=upCard.value;
    while(true){
      if(total(c)>21){say(label+" BUST at "+total(c));return;}
      boolean pair=allowSplit && first && c.size()==2 && c.get(0).value==c.get(1).value;
      boolean canD=first && canCommitAdditional(stake);
      boolean canP=pair && canCommitAdditional(stake);

      A rec=null;
      if(!personalMode){
        rec=recommend(c,up,first,pair,canD,canP);
        String provenance=policyProvenance(c,up,first,pair,rec);
        say(label+" "+ct(c)+" = "+tt(c)+" | FROZEN POLICY: "+rec+" ["+provenance+"]");
        log.add("  POLICY SOURCE | "+label+" | "+ct(c)+" vs "+rt(upCard)+" | "+rec+" | "+provenance);
      } else {
        say(label+" "+ct(c)+" = "+tt(c)+" | PERSONAL DECISION");
      }

      if(first && !canD){
        say("DOUBLE unavailable: research-equivalent bankroll £"+m(researchBankroll())+
            " | already committed £"+m(handCommittedStake)+
            " | additional £"+m(stake)+" required.");
      }
      if(pair && !canP){
        say("SPLIT unavailable: research-equivalent bankroll £"+m(researchBankroll())+
            " | already committed £"+m(handCommittedStake)+
            " | additional £"+m(stake)+" required.");
      }

      String opts="HS"+(canD?"D":"")+(canP?"P":"");
      A act=parse(choice("Actual action ["+opts+"]: ",opts));
      if(label.equals("MAIN") && first && currentInitialAction.equals("NONE")) currentInitialAction=act.toString();
      if(!personalMode){
        if(act==rec)actionMatch++; else actionDiff++;
        log.add("  "+label+" | "+ct(c)+" | frozen "+rec+" | actual "+act);
      } else {
        log.add("  "+label+" | "+ct(c)+" | personal actual "+act);
      }

      if(act==A.STAND)return;
      if(act==A.DOUBLE){
        // Defensive guard as well as removing D from the allowed options.
        if(!canCommitAdditional(stake)){
          validationWarnings++;
          say("DOUBLE REJECTED: insufficient research-equivalent bankroll after the £2400 platform offset.");
          continue;
        }
        exposure+=stake; handCommittedStake+=stake;
        c.add(card(label+" double card")); say(label+" after DOUBLE: "+ct(c)+" = "+tt(c)); return;
      }
      if(act==A.SPLIT){
        if(!canCommitAdditional(stake)){
          validationWarnings++;
          say("SPLIT REJECTED: insufficient research-equivalent bankroll after the £2400 platform offset.");
          continue;
        }
        handWasSplit=true; exposure+=stake; handCommittedStake+=stake; Card x=c.get(0),y=c.get(1);
        List<Card>a=new ArrayList<>(List.of(x,card("Split A added card")));
        List<Card>b=new ArrayList<>(List.of(y,card("Split B added card")));
        say("Split A "+ct(a)+" = "+tt(a)); say("Split B "+ct(b)+" = "+tt(b));
        if(x.value==11&&y.value==11){say("Split Aces: one additional card each; stop.");return;}
        play("SPLIT A",a,upCard,stake,true,false); play("SPLIT B",b,upCard,stake,true,false); return;
      }
      c.add(card(label+" hit card")); first=false; allowSplit=false;
    }
  }

  static double validatedPersonalWager(){
    while(true){
      double a=money("Actual wager £: ");
      if(a<=0){validationWarnings++;say("VALIDATION FAILED: wager must be positive.");continue;}
      double available=researchBankroll();
      if(a>available+0.001){
        validationWarnings++;
        say("WAGER REJECTED: research-equivalent bankroll is only £"+m(available)+
            ". The £2400 platform offset is protected and cannot be wagered in the research session.");
        continue;
      }
      return a;
    }
  }

  static String rankOnly(Card q){ return q.rank; }
  static int bucket(Card q){ return (q.value>=2&&q.value<=6)?-1:(q.value>=10?1:0); }
  static String signature(Card p1,Card p2,Card up){
    String a=rankOnly(p1), b=rankOnly(p2);
    if(a.compareTo(b)>0){String t=a;a=b;b=t;}
    return a+","+b+" vs "+rankOnly(up);
  }
  static int previousOccurrences(String sig){
    if(!Files.exists(CUMULATIVE_OUTPUT)) return 0;
    try{
      int n=0; String needle="HAND_SIGNATURE | "+sig;
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)) if(x.trim().equals(needle)) n++;
      return n;
    }catch(Exception e){ validationWarnings++; return 0; }
  }
  static List<String> previousOutcomeLines(String sig){
    // HAND_HISTORY is intentionally present in more than one report section.
    // De-duplicate exact history records so one historical occurrence is shown once.
    LinkedHashSet<String> unique=new LinkedHashSet<>();
    if(!Files.exists(CUMULATIVE_OUTPUT)) return new ArrayList<>();
    try{
      String needle="HAND_HISTORY | "+sig+" |";
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(t.startsWith(needle)) unique.add(t);
      }
    }catch(Exception e){ validationWarnings++; }
    return new ArrayList<>(unique);
  }

  static void showPreWagerRecentHistory(){
    boolean recentHandsRequested=choice("Before wagering, show previous hands played on this table/session? [Y/N]: ","YN").equals("Y");
    if(recentHandsRequested){
      int recentCount=Integer.parseInt(choice("How many recent hands? [1/2/3]: ","123"));
      log.add("  BEHAVIOURAL INFO REQUEST | recent table hands | REQUESTED BEFORE WAGER | last "+recentCount);
      handSignatureLog.add("  HISTORY_REQUESTED | RECENT_TABLE_HANDS | Y | BEFORE_WAGER | requested "+recentCount);
      showRecentTableHands(recentCount);
    } else {
      log.add("  BEHAVIOURAL INFO REQUEST | recent table hands | NOT REQUESTED BEFORE WAGER");
      handSignatureLog.add("  HISTORY_REQUESTED | RECENT_TABLE_HANDS | N | BEFORE_WAGER");
      say("RECENT TABLE-HAND HISTORY: not shown before wager.");
    }
  }

  static void showObservedHandContext(Card p1,Card p2,Card up){
    String sig=signature(p1,p2,up);
    int prev=previousOccurrences(sig);
    int cl=0,cn=0,ch=0;
    for(Card q:List.of(p1,p2,up)){int b=bucket(q);if(b<0)cl++;else if(b>0)ch++;else cn++;}
    int bl=lowSinceShuffle-cl, bn=neutralSinceShuffle-cn, bh=highSinceShuffle-ch;
    say("OBSERVED HAND (suits excluded): "+sig+" | appeared before in project output: "+prev+" time(s)");

    boolean sameHandRequested=false;

    if(personalMode){
      sameHandRequested=choice("View previous outcomes for this starting hand? [Y/N]: ","YN").equals("Y");
      log.add("  BEHAVIOURAL INFO REQUEST | matching starting-hand history | "+(sameHandRequested?"REQUESTED":"NOT REQUESTED")+" | signature "+sig);
      handSignatureLog.add("  HISTORY_REQUESTED | MATCHING_START_HAND | "+(sameHandRequested?"Y":"N")+" | signature "+sig);
      if(sameHandRequested) showMatchingHandHistory(sig);
      else say("MATCHING-HAND HISTORY: not shown.");
    } else {
      // Preserve the established Frozen-mode display. It is observational only and
      // never enters the deterministic policy calculation.
      showMatchingHandHistory(sig);
    }

    say("CARD MIX before current visible hand since last recorded shuffle: Low "+Math.max(0,bl)+" | Neutral "+Math.max(0,bn)+" | High "+Math.max(0,bh));
    say("CARD MIX current visible hand (P1 + P2 + dealer up): Low "+cl+" | Neutral "+cn+" | High "+ch);
    say("Low=2-6 | Neutral=7-9 | High=10/J/Q/K/A | observational only");
    handSignatureLog.add("HAND_SIGNATURE | "+sig);
    handSignatureLog.add("  PRIOR_OCCURRENCES | "+prev+" | BEFORE_LNH "+Math.max(0,bl)+"/"+Math.max(0,bn)+"/"+Math.max(0,bh)+" | CURRENT_VISIBLE_LNH "+cl+"/"+cn+"/"+ch);
  }

  static void showMatchingHandHistory(String sig){
    List<String> priorOutcomes=previousOutcomeLines(sig);
    if(priorOutcomes.isEmpty()){
      say("PREVIOUS OUTCOMES: none yet in the project output for this starting hand.");
      handSignatureLog.add("  HISTORY_MATCHES_SHOWN | 0 | signature "+sig);
      return;
    }
    say("PREVIOUS OUTCOMES FOR THIS STARTING HAND (all recorded occurrences):");
    int i=1;
    for(String x:priorOutcomes){
      String detail=x.substring(("HAND_HISTORY | "+sig+" | ").length());
      say("  #"+(i++)+" | "+detail);
    }
    handSignatureLog.add("  HISTORY_MATCHES_SHOWN | "+priorOutcomes.size()+" | signature "+sig);
  }

  static void showRecentTableHands(int requested){
    List<String> prior=new ArrayList<>();
    for(String x:handSignatureLog) if(x.startsWith("HAND_HISTORY | ")) prior.add(x);
    int n=Math.min(requested,prior.size());
    if(n==0){
      say("RECENT TABLE HANDS: none yet in this session.");
      handSignatureLog.add("  RECENT_TABLE_HANDS_SHOWN | 0");
      return;
    }
    say("RECENT TABLE HANDS (current session only; most recent last):");
    for(int i=prior.size()-n;i<prior.size();i++) say("  "+prior.get(i));
    handSignatureLog.add("  RECENT_TABLE_HANDS_SHOWN | "+n+" | requested "+requested);
  }

  static String policyProvenance(List<Card> c,int up,boolean first,boolean pair,A rec){
    if(first && pair && c.size()==2 && c.get(0).value==10 && c.get(1).value==10 && up==10 && rec==A.STAND){
      frozenResearchOverrideDecisions++;
      return "FROZEN RESEARCH OVERRIDE: prior SPLIT -> STAND; also TEXTBOOK";
    }
    if(first && total(c)==11 && !soft(c) && up==10 && rec==A.DOUBLE){
      frozenPrimarySpecialDecisions++;
      return "FROZEN PRIMARY: DOUBLE; TEXTBOOK-aligned";
    }
    textbookAlignedDecisions++;
    return "TEXTBOOK / FROZEN-ALIGNED";
  }

  static A recommend(List<Card>c,int up,boolean first,boolean pair,boolean canD,boolean canP){
    int t=total(c); if(t>=21)return A.STAND;
    if(pair){int q=c.get(0).value;
      if(q==10)return A.STAND;
      if(canP){
        if(q==11||q==8)return A.SPLIT;
        if(q==9&&in(up,2,3,4,5,6,8,9))return A.SPLIT;
        if(q==7&&between(up,2,7))return A.SPLIT;
        if(q==6&&between(up,2,6))return A.SPLIT;
        if(q==4&&(up==5||up==6))return A.SPLIT;
        if((q==3||q==2)&&between(up,2,7))return A.SPLIT;
      }
      // If a split is unaffordable (or not called for), continue into the ordinary
      // hard/soft policy for the same cards instead of inventing a forced HIT.
    }
    if(soft(c)){
      if(t>=19)return A.STAND;
      if(t==18){if(first&&canD&&between(up,3,6))return A.DOUBLE; if(up==2||up==7||up==8)return A.STAND; return A.HIT;}
      if(t==17){if(first&&canD&&between(up,3,6))return A.DOUBLE;return A.HIT;}
      if(t==15||t==16){if(first&&canD&&between(up,4,6))return A.DOUBLE;return A.HIT;}
      if(t==13||t==14){if(first&&canD&&(up==5||up==6))return A.DOUBLE;return A.HIT;}
      return A.HIT;
    }
    if(t>=17)return A.STAND; if(t>=13)return between(up,2,6)?A.STAND:A.HIT; if(t==12)return between(up,4,6)?A.STAND:A.HIT;
    if(t==11)return first&&canD&&between(up,2,10)?A.DOUBLE:A.HIT;
    if(t==10)return first&&canD&&between(up,2,9)?A.DOUBLE:A.HIT;
    if(t==9)return first&&canD&&between(up,3,6)?A.DOUBLE:A.HIT;
    return A.HIT;
  }

  static int total(List<Card>c){int t=0,a=0;for(Card q:c){t+=q.value;if(q.value==11)a++;}while(t>21&&a-->0)t-=10;return t;}
  static boolean soft(List<Card>c){int t=0,a=0;for(Card q:c){t+=q.value;if(q.value==11)a++;}while(t>21&&a>0){t-=10;a--;}return a>0;}
  static String tt(List<Card>c){return (soft(c)?"soft ":"")+total(c);}
  static String ct(List<Card>c){List<String>x=new ArrayList<>();for(Card q:c)x.add(q.toString());return String.join(",",x);}
  static String rt(Card q){return q.toString();}
  // Stable ASCII rendering for chronology/card-stream review.
  // Avoids console/font substitution of suit glyphs (e.g. ♠ appearing as ?).
  static String streamCard(Card q){
    String s=q.suit;
    String a=s.equals("♠")?"S":s.equals("♥")?"H":s.equals("♦")?"D":s.equals("♣")?"C":s.equals("?")?"U":s;
    return q.rank+a;
  }
  static boolean between(int x,int a,int b){return x>=a&&x<=b;} static boolean in(int x,int...v){for(int q:v)if(x==q)return true;return false;}
  static A parse(String s){return switch(s){case"H"->A.HIT;case"S"->A.STAND;case"D"->A.DOUBLE;default->A.SPLIT;};}
  static final Map<String,Integer> observedPhysicalCounts=new LinkedHashMap<>();

  static Card card(String p){
    while(true){
      String raw=line(p+": ").trim().toUpperCase(Locale.ROOT);
      Card q=parseCard(raw);
      if(q==null){ say("INVALID CARD. Use rank+suit such as 8H, KS, A♦, 10C, or 4U if suit genuinely unknown."); continue; }
      if(q.suit.isEmpty()){
        validationWarnings++;
        say("SUIT MISSING - entry rejected. Re-enter with S/H/D/C, ♠/♥/♦/♣, or U for explicitly unknown.");
        continue;
      }
      trackPhysicalCard(q);
      String lp=p.toLowerCase(Locale.ROOT);
      if(lp.contains("hit card") || lp.contains("double card") || lp.contains("split a added card") || lp.contains("split b added card")) currentPlayerPostDealCards.add(q);
      return q;
    }
  }
  static Card parseCard(String raw){
    String x=raw.trim().toUpperCase(Locale.ROOT).replace(" ",""), suit="";
    if(x.endsWith("♠")||x.endsWith("S")){suit="♠";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♥")||x.endsWith("H")){suit="♥";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♦")||x.endsWith("D")){suit="♦";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("♣")||x.endsWith("C")){suit="♣";x=x.substring(0,x.length()-1);}
    else if(x.endsWith("U")){suit="?";x=x.substring(0,x.length()-1);}
    int v;
    if(x.equals("A"))v=11; else if(x.matches("10|J|Q|K"))v=10;
    else {try{v=Integer.parseInt(x);}catch(Exception e){return null;}if(v<2||v>9)return null;}
    return new Card(x,suit,v);
  }
  static void trackPhysicalCard(Card q){
    if(q.suit.isEmpty()) return;
    totalCardsObserved++;
    cardsSinceShuffle++;
    if(!q.suit.equals("?")) observedPhysicalCounts.merge(q.rank+q.suit,1,Integer::sum);
  }
  static String dealerSequence(String prompt){
    currentDealerCards.clear();
    while(true){
      String raw=line(prompt).trim(); 
      if(raw.equals("?")){validationWarnings++; say("Dealer sequence explicitly recorded as UNKNOWN."); return "?";}
      String[] a=raw.split(","); List<String> out=new ArrayList<>(); boolean ok=true;
      for(String x:a){
        Card q=parseCard(x);
        if(q==null || q.suit.isEmpty()){ok=false;break;}
        trackPhysicalCard(q); currentDealerCards.add(q); out.add(q.toString());
      }
      if(ok&&!out.isEmpty())return String.join(",",out);
      validationWarnings++;
      say("VALIDATION FAILED. Every dealer card needs a suit (or U), e.g. 6H,10D,5C.");
    }
  }


  static void showDealerTotal(Card up){
    if(currentDealerCards.isEmpty()){
      say("Dealer total: UNKNOWN (dealer hidden/draw sequence not fully recorded)");
      return;
    }
    List<Card> full=new ArrayList<>();
    full.add(up);
    full.addAll(currentDealerCards);
    int t=total(full);
    String status=t>21 ? " BUST" : (t==21 ? " 21" : "");
    say("Dealer "+ct(full)+" = "+tt(full)+status);
  }

  static String validatedResult(List<Card> player, Card up){
    while(true){
      String entered=choice("Result W/L/P: ","WLP");

      // Split rounds can contain mixed child outcomes (for example WIN + PUSH).
      // The present round-level W/L/P field cannot prove a single expected result.
      if(handWasSplit){
        say("RESULT CHECK: split hand - automatic single-result validation is not forced because child outcomes may differ.");
        if(choice("Confirm the entered round result "+entered+" matches the website/your chosen round-level convention? [Y/N]: ","YN").equals("Y")) return entered;
        validationWarnings++;
        continue;
      }

      if(currentDealerCards.isEmpty()){
        say("RESULT CHECK: dealer sequence is UNKNOWN, so card-based result validation cannot be completed.");
        if(choice("Confirm entered result "+entered+" from the website? [Y/N]: ","YN").equals("Y")) return entered;
        validationWarnings++;
        continue;
      }

      List<Card> dealer=new ArrayList<>();
      dealer.add(up); dealer.addAll(currentDealerCards);
      int pt=total(player), dt=total(dealer);
      boolean playerNatural=(player.size()==2 && pt==21);
      boolean dealerNatural=(dealer.size()==2 && dt==21);
      String expected;
      if(pt>21) expected="L";
      else if(dealerNatural) expected=playerNatural?"P":"L";
      else if(playerNatural) expected="W";
      else if(dt>21) expected="W";
      else if(pt>dt) expected="W";
      else if(pt<dt) expected="L";
      else expected="P";

      if(entered.equals(expected)){
        say("RESULT VALIDATED ✓ cards imply "+resultWord(expected)+" | Player "+pt+" vs Dealer "+dt);
        return entered;
      }

      validationWarnings++;
      say("*** RESULT VALIDATION WARNING ***");
      say("Player "+ct(player)+" = "+pt+" | Dealer "+ct(dealer)+" = "+dt);
      say("Cards imply "+resultWord(expected)+" ("+expected+") but entered result is "+resultWord(entered)+" ("+entered+").");
      String c=choice("[R] re-enter result / [A] accept discrepancy with explanation: ","RA");
      if(c.equals("R")) continue;
      String note=line("Reason/explanation (check the game screen first): ");
      log.add("  RESULT VALIDATION OVERRIDE | cards imply "+expected+" entered "+entered+" | "+note);
      return entered;
    }
  }

  static String resultWord(String r){
    return r.equals("W")?"PLAYER WIN":r.equals("L")?"PLAYER LOSS":"PUSH";
  }

  static double validatedWager(double ref){
    while(true){
      double a=money("Actual wager £: ");
      if(a<=0){validationWarnings++;say("VALIDATION FAILED: wager must be positive.");continue;}
      double available=researchBankroll();
      if(a>available+0.001){
        validationWarnings++;
        say("WAGER REJECTED: research-equivalent bankroll is only £"+m(available)+
            ". The £2400 platform offset is protected and cannot be wagered in the research session.");
        continue;
      }
      if(Math.abs(a-ref)>.001){
        validationWarnings++;
        say("CHECK: frozen reference is £"+m(ref)+" but entered wager is £"+m(a)+".");
        String c=choice("Is £"+m(a)+" definitely the wager placed? [Y/N]: ","YN");
        if(c.equals("N"))continue;

        // Preserve the actual wager as evidence, but classify why it differed.
        String reason=choice("Reason for wager deviation: [A] accidental entry/website amount / [I] intentional deviation / [O] other: ","AIO");
        String reasonText=reason.equals("A") ? "ACCIDENTAL" : reason.equals("I") ? "INTENTIONAL" : "OTHER";
        String note="";
        if(reason.equals("O")) note=line("Brief reason/note: ");
        log.add("  WAGER DEVIATION | frozen_reference "+m(ref)+" | actual "+m(a)+" | difference "+m(a-ref)+" | reason "+reasonText+(note.isEmpty()?"":" | note "+note));
      }
      return a;
    }
  }

  static double validatedEndingBankroll(double before,double base,double committed,String result,boolean natural,boolean split){
    while(true){
      double entered=money("Bankroll AFTER hand £: ");
      if(split){
        say("Split hand: automatic exact balance calculation is intentionally not forced because child outcomes may differ.");
        if(choice("Confirm website balance £"+m(entered)+" is correct? [Y/N]: ","YN").equals("Y")) return entered;
        validationWarnings++; continue;
      }
      double expected;
      if(result.equals("P")) expected=before;
      else if(result.equals("L")) expected=before-committed;
      else expected=before+(natural ? 1.5*base : committed);

      if(Math.abs(entered-expected)<0.011){
        say("BALANCE VALIDATED ✓ expected £"+m(expected));
        return entered;
      }

      validationWarnings++;
      say("*** BALANCE VALIDATION WARNING ***");
      say("Start £"+m(before)+" | committed stake £"+m(committed)+" | result "+result+(natural?" | natural blackjack":""));
      say("Expected £"+m(expected)+" | entered £"+m(entered)+" | difference £"+m(entered-expected));
      String c=choice("[R] re-enter balance / [A] accept discrepancy with note: ","RA");
      if(c.equals("R")) continue;
      String note=line("Reason/note (may be brief): ");
      log.add("  VALIDATION OVERRIDE | expected "+m(expected)+" entered "+m(entered)+" | "+note);
      return entered;
    }
  }


  static void showChronologyPreview(int hand,Card p1,Card up,Card p2){
    say("\n--- CARD STREAM REVIEW: HAND "+hand+" ---");
    say("Reconstructed physical deal order (not the order you typed hidden cards):");
    say("Suit key: S=Spades, H=Hearts, D=Diamonds, C=Clubs, U=Unknown. Suits are shown in ASCII so they cannot disappear in the console.");
    int n=chronologicalCardNumber;
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER card 1",streamCard(p1)));
    if(currentDealerCards.isEmpty()){
      say("[UNKNOWN] DEALER hole card / dealer sequence not fully recorded");
    } else {
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER hole card",streamCard(currentDealerCards.get(0))));
    }
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER card 2",streamCard(p2)));
    say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER upcard",streamCard(up)));
    for(Card q:currentPlayerPostDealCards)
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"PLAYER post-deal card",streamCard(q)));
    for(int i=1;i<currentDealerCards.size();i++)
      say(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",++n,hand,"DEALER draw "+i,streamCard(currentDealerCards.get(i))));
    if(handWasSplit)
      say("SPLIT NOTE: post-deal split cards are shown in operator-entry order; verify this sub-order against the screen.");
    say("--- END CARD STREAM REVIEW ---\n");
  }

  static void appendChronology(int hand,Card p1,Card up,Card p2){
    chronologyLog.add("HAND "+hand+" DEAL-ORDER RECONSTRUCTION");
    addChron(hand,"PLAYER card 1",p1);
    if(currentDealerCards.isEmpty()){
      chronologyLog.add("  [UNKNOWN] DEALER hole card / dealer sequence not fully recorded");
    } else {
      addChron(hand,"DEALER hole card",currentDealerCards.get(0));
    }
    addChron(hand,"PLAYER card 2",p2);
    addChron(hand,"DEALER upcard",up);
    for(Card q:currentPlayerPostDealCards) addChron(hand,"PLAYER post-deal card",q);
    for(int i=1;i<currentDealerCards.size();i++) addChron(hand,"DEALER draw "+i,currentDealerCards.get(i));
    chronologyLog.add("");
  }
  static void addChron(int hand,String role,Card q){
    chronologicalCardNumber++;
    chronologyLog.add(String.format(Locale.ROOT,"#%03d | H%02d | %-22s | %s",chronologicalCardNumber,hand,role,q));
  }

  static double money(String p){while(true)try{return Double.parseDouble(line(p).replace("£","").trim());}catch(Exception e){say("Enter numeric amount.");}}
  static String choice(String p,String allowed){while(true){String s=line(p).toUpperCase();if(s.length()==1&&allowed.contains(s))return s;say("Allowed: "+allowed);}}
  static String line(String p){System.out.print(p);return sc.nextLine().trim();}
  static void say(String s){System.out.println(s);log.add(s);}
  static String m(double x){return String.format(Locale.UK,"%.2f",x);}

  static class LedgerTotals { int sessions=0, hands=0; double cumulativePL=0.0; }

  static LedgerTotals readLedgerTotals(){
    LedgerTotals t=new LedgerTotals();
    if(!Files.exists(CUMULATIVE_OUTPUT)) return t;
    try{
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String q=x.trim();
        if(!q.startsWith("LEDGER_ENTRY |")) continue;
        t.sessions++;
        for(String part:q.split("\\|")){
          String z=part.trim();
          if(z.startsWith("hands ")) t.hands+=Integer.parseInt(z.substring(6).trim());
          else if(z.startsWith("session_pl ")) t.cumulativePL+=Double.parseDouble(z.substring(11).trim());
        }
      }
    }catch(Exception e){ validationWarnings++; }
    return t;
  }

  static int committedHands(){ return w+l+p; }

  // Explicitly distinguishes player decision events from hands played.
  static int totalDecisionEvents(){
    return textbookAlignedDecisions+frozenResearchOverrideDecisions+frozenPrimarySpecialDecisions;
  }

  static String ledgerEntry(String stamp){
    double researchFinal=bank-PLATFORM_OFFSET;
    double sessionPL=researchFinal-100.0;
    return "LEDGER_ENTRY | session "+stamp+" | mode "+(personalMode?"PERSONAL":"FROZEN")+
      " | hands "+committedHands()+" | start 100.00 | final "+m(researchFinal)+" | session_pl "+m(sessionPL)+
      " | total_decision_events "+totalDecisionEvents()+
      " | wager_match "+wagerMatch+" | wager_above "+wagerAbove+" | wager_below "+wagerBelow+
      " | action_match "+actionMatch+" | action_different "+actionDiff;
  }

  static List<String> ledgerSummaryLines(LedgerTotals prior,double thisPL,int thisHands){
    int sessions=prior.sessions+1, hands=prior.hands+thisHands;
    double cumulative=prior.cumulativePL+thisPL;
    double cumulativePct=100.0*cumulative/(100.0*sessions);
    double runningProjectBalance=100.0+cumulative;
    List<String> x=new ArrayList<>();
    x.add("PROJECT LEDGER SUMMARY");
    x.add("Completed sessions: "+sessions+" | Total hands: "+hands);
    x.add("Cumulative project P/L: £"+m(cumulative));
    x.add("Cumulative P/L vs £100 per-session starting capital: "+m(cumulativePct)+"%");
    x.add("Project-equivalent running balance (initial £100 + cumulative P/L): £"+m(runningProjectBalance));
    x.add("Ledger basis: each live session independently starts at research-equivalent £100; cumulative P/L sums session profits/losses.");
    return x;
  }


  static void save()throws Exception{
    String stamp=LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); Path f=Path.of("blackjack_live_session_"+stamp+".txt");
    List<String>r=new ArrayList<>(); r.add("DETERMINISTIC BLACKJACK - LIVE SESSION REPORT");
    r.add("PLATFORM BALANCE BASIS: Start £2500.00 | £2400.00 = research-equivalent £0/depleted");
    r.add("Platform Final £"+m(bank)+" | Platform Peak £"+m(peak)+" | Platform Trough £"+m(trough)+" | MaxDD £"+m(maxDD)+" | Exposure £"+m(exposure));
    r.add("OFFSET RESEARCH-EQUIVALENT: Start £100.00 | Final £"+m(bank-PLATFORM_OFFSET)+" | Peak £"+m(peak-PLATFORM_OFFSET)+" | Trough £"+m(trough-PLATFORM_OFFSET));
    r.add("OFFSET RULE: research-equivalent bankroll = platform balance - £2400.00");
    r.add("AFFORDABILITY RULE: £2400 platform offset is protected; wager/double/split affordability uses research-equivalent bankroll only.");
    r.add("MODE: "+(personalMode?"PERSONAL - no Frozen wager/action prompts":"FROZEN ARCHITECTURE")); r.add("W/L/P "+w+"/"+l+"/"+p); if(!personalMode){r.add("Wager match/above/below "+wagerMatch+"/"+wagerAbove+"/"+wagerBelow);r.add("Action match/different "+actionMatch+"/"+actionDiff);}
    r.add("SOURCE: 247blackjack.com");
    r.add("SOURCE TYPE: Online play-money blackjack, configured 6-deck game");
    r.add("CARD GENERATION: computational/RNG implementation; visible six-deck shuffle events recorded from interface");
    r.add("Shuffle events explicitly observed: "+shuffleEvents);
    r.add("Total cards recorded: "+totalCardsObserved);
    r.add("Cards since most recent observed shuffle/start: "+cardsSinceShuffle);
    r.add("Exact known rank+suit identities observed: "+observedPhysicalCounts.size());
    r.add("Validation warnings/overrides encountered: "+validationWarnings);
    r.add("Hands re-entered through amendment review: "+amendmentsMade);
    r.add("Decision provenance counts:");
    r.add("  TOTAL DECISION EVENTS: "+totalDecisionEvents()+" (decision events, not hands)");
    r.add("  TEXTBOOK / FROZEN-ALIGNED: "+textbookAlignedDecisions);
    r.add("  FROZEN RESEARCH OVERRIDE events: "+frozenResearchOverrideDecisions);
    r.add("  FROZEN PRIMARY HARD11-vs-10 events: "+frozenPrimarySpecialDecisions);
    r.add("Policy note: Pair20-vs-10 STAND is a research override of prior aggressive SPLIT and also agrees with textbook.");
    r.add("Policy note: HARD11-vs-10 primary remains DOUBLE; Cooling->HIT is a capital-protection alternate, not the primary action used here.");
    r.add("Shuffle log: "+(shuffleLog.isEmpty()?"none":String.join(" | ",shuffleLog)));
    r.add("Penetration is not inferred unless a visible shuffle boundary is recorded; card counts are observational.");
    r.add("Boundary: frozen wagers are historical reference wagers, not proven optimal stakes.");r.add("Controller note: exact mature BRAKE predicate is not invented; only explicit handover transitions are used.");
    r.add(""); r.add("PERSONAL-MODE HISTORY ACCESS: recent-table-hand history is hidden by default and, if requested, shown BEFORE the wager; matching-hand history is available only after the current starting cards are known and before the player action.");
    r.add("RECENT TABLE-HAND WINDOW: user-selectable 1-3 previously committed hands from the current session only; the request itself is retained as behavioural evidence and occurs before capital is committed.");
    r.add(""); r.add("OBSERVED INITIAL-HAND INDEX + HISTORICAL OUTCOMES (SUITS EXCLUDED)"); r.addAll(handSignatureLog); r.add(""); r.add("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)");
    r.add("Order rule observed on 247 animation: Player1 -> Dealer hole -> Player2 -> Dealer up -> subsequent player cards -> subsequent dealer draws.");
    r.add("Dealer hole cards are entered later when revealed but are retrospectively placed as the dealer's first dealt card, matching the observed 247 animation.");
    r.add("Split/post-deal cards are retained in operator-entry order; if platform split dealing order differs, that sub-order must be reviewed before deterministic replay.");
    r.addAll(chronologyLog);
    r.add("");r.add("AUDIT LOG");r.addAll(log);Files.write(f,r);
     LedgerTotals priorLedger=readLedgerTotals();
     double thisPL=(bank-PLATFORM_OFFSET)-100.0; int thisHands=committedHands();
     List<String> cumulative=new ArrayList<>();
     cumulative.add("\n================ SESSION "+stamp+" ================"); cumulative.addAll(r);
     cumulative.add(ledgerEntry(stamp)); cumulative.addAll(ledgerSummaryLines(priorLedger,thisPL,thisHands));
     cumulative.add("================ END SESSION "+stamp+" ================\n");
     cumulative.add("SHUFFLE_ANALYSIS_STATUS | session "+stamp+" | PENDING");
     Files.write(CUMULATIVE_OUTPUT,cumulative,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);
     lastSavedSessionId=stamp; lastSavedReport=f;
     System.out.println("\nReport written: "+f.toAbsolutePath());
     System.out.println("Cumulative project output updated: "+CUMULATIVE_OUTPUT.toAbsolutePath());
     System.out.println("Ledger updated: cumulative project P/L £"+m(priorLedger.cumulativePL+thisPL)+" | project-equivalent running balance £"+m(100.0+priorLedger.cumulativePL+thisPL));
  }

  // ==================== SELF-CONTAINED POST-SESSION SHUFFLE ENGINE ====================
  // Replay rules: 247 physical initial deal order P1 -> dealer hole -> P2 -> dealer up; clean peek; dealer stands on all 17s; blackjack 3:2; no insurance;
  // one split level; split aces receive one card; double receives one card.
  // Fixed Casual profile: £15 flat wager; conventional Hit/Stand; double hard 10/11
  // versus dealer 2-9 when affordable; split A-A and 8-8 when affordable.

  enum ShuffleMethod { A, B, C1, C2, C3, C4, D, E }
  static class ReplayDecision { final String key; final A action; ReplayDecision(String k,A a){key=k;action=a;} }
  static class ReplayResult {
    double bank=100,peak=100,trough=100,maxDD=0,exposure=0;
    int hands=0,w=0,l=0,p=0,cards=0; boolean reached200=false,sourceExhausted=false,reached130Replay=false;
    State replayState=State.NORMAL; String reason="COMPLETED"; final List<ReplayDecision> decisions=new ArrayList<>();
  }
  static class PairDiff { int comparable=0,different=0; }
  static class MethodAggregate {
    final ShuffleMethod method; final int reps; final List<Double> frozenFinal=new ArrayList<>(),casualFinal=new ArrayList<>();
    double frozenExposure=0,casualExposure=0,frozenDD=0,casualDD=0,pairedDelta=0; int frozenReach=0,casualReach=0,frozenComplete=0,casualComplete=0,bothComplete=0;
    long comparableActions=0,differentActions=0; MethodAggregate(ShuffleMethod m,int n){method=m;reps=n;}
  }
  static class ReplayShoe {
    final ShuffleMethod method; final Random rng; final List<Card> original; List<Card> cards; int pos=0,segmentStart=0;
    ReplayShoe(ShuffleMethod m,List<Card> reservoir,long seed){method=m;rng=new Random(seed);original=copyCards(reservoir);cards=(m==ShuffleMethod.B)?fullSixDeck():copyCards(reservoir);if(m==ShuffleMethod.D)physicalShuffle(cards,rng);else Collections.shuffle(cards,rng);}
    void beforeHand(){
      if(method==ShuffleMethod.C1||method==ShuffleMethod.C2||method==ShuffleMethod.C3||method==ShuffleMethod.C4){
        double t=switch(method){case C1->.50;case C2->.65;case C3->.75;default->.80;};
        if(pos-segmentStart>=Math.ceil(original.size()*t)&&pos<cards.size()){Collections.shuffle(cards.subList(pos,cards.size()),rng);segmentStart=pos;}
      }
    }
    Card draw()throws EOFException{if(pos>=cards.size())throw new EOFException("source exhausted");return cards.get(pos++);}
    void afterHand(){if(method==ShuffleMethod.E){cards=copyCards(original);Collections.shuffle(cards,rng);pos=0;segmentStart=0;}}
  }
  static class HandCtx { double committed; HandCtx(double x){committed=x;} }
  static class PlayedHand { List<Card> cards; double stake; PlayedHand(List<Card> c,double s){cards=c;stake=s;} }

  static List<Card> copyCards(List<Card> src){List<Card>x=new ArrayList<>();for(Card c:src)x.add(new Card(c.rank,c.suit,c.value));return x;}
  static List<Card> fullSixDeck(){List<Card>x=new ArrayList<>();String[]su={"S","H","D","C"},ra={"A","2","3","4","5","6","7","8","9","10","J","Q","K"};for(int d=0;d<6;d++)for(String s:su)for(String r:ra){int v=r.equals("A")?11:(r.matches("10|J|Q|K")?10:Integer.parseInt(r));x.add(new Card(r,s,v));}return x;}
  static Card replayDraw(ReplayResult r,ReplayShoe s)throws EOFException{Card c=s.draw();r.cards++;return c;}

  // Declared physical-style mathematical proxy; not a proprietary casino-shuffler claim.
  static void physicalShuffle(List<Card> deck,Random rng){
    for(int cycle=0;cycle<3;cycle++){
      int cut=Math.max(1,Math.min(deck.size()-1,deck.size()/2+rng.nextInt(11)-5));List<Card>L=new ArrayList<>(deck.subList(0,cut)),R=new ArrayList<>(deck.subList(cut,deck.size())),mix=new ArrayList<>();int i=0,j=0;
      while(i<L.size()||j<R.size()){boolean left=i<L.size()&&(j>=R.size()||rng.nextBoolean());int packet=1+rng.nextInt(3);for(int k=0;k<packet;k++){if(left&&i<L.size())mix.add(L.get(i++));else if(!left&&j<R.size())mix.add(R.get(j++));else if(i<L.size())mix.add(L.get(i++));else if(j<R.size())mix.add(R.get(j++));}}
      deck.clear();deck.addAll(mix);List<Card>strip=new ArrayList<>();int at=0;while(at<deck.size()){int n=Math.min(deck.size()-at,2+rng.nextInt(5));strip.addAll(0,new ArrayList<>(deck.subList(at,at+n)));at+=n;}deck.clear();deck.addAll(strip);if(deck.size()>1)Collections.rotate(deck,-(1+rng.nextInt(deck.size()-1)));
    }
  }

  static A frozenReplayAction(List<Card>c,int up,boolean first,boolean pair,boolean canD){
    int t=total(c);if(t>=21)return A.STAND;if(pair){int q=c.get(0).value;if(q==10)return A.STAND;if(canD){if(q==11||q==8)return A.SPLIT;if(q==9&&in(up,2,3,4,5,6,8,9))return A.SPLIT;if(q==7&&between(up,2,7))return A.SPLIT;if(q==6&&between(up,2,6))return A.SPLIT;if(q==4&&(up==5||up==6))return A.SPLIT;if((q==3||q==2)&&between(up,2,7))return A.SPLIT;}}
    if(soft(c)){if(t>=19)return A.STAND;if(t==18){if(first&&canD&&between(up,3,6))return A.DOUBLE;if(up==2||up==7||up==8)return A.STAND;return A.HIT;}if(t==17){if(first&&canD&&between(up,3,6))return A.DOUBLE;return A.HIT;}if(t==15||t==16){if(first&&canD&&between(up,4,6))return A.DOUBLE;return A.HIT;}if(t==13||t==14){if(first&&canD&&(up==5||up==6))return A.DOUBLE;return A.HIT;}return A.HIT;}
    if(t>=17)return A.STAND;if(t>=13)return between(up,2,6)?A.STAND:A.HIT;if(t==12)return between(up,4,6)?A.STAND:A.HIT;if(t==11)return first&&canD&&between(up,2,10)?A.DOUBLE:A.HIT;if(t==10)return first&&canD&&between(up,2,9)?A.DOUBLE:A.HIT;if(t==9)return first&&canD&&between(up,3,6)?A.DOUBLE:A.HIT;return A.HIT;
  }
  static A casualReplayAction(List<Card>c,int up,boolean first,boolean pair,boolean canExtra){
    int t=total(c);if(t>=21)return A.STAND;if(first&&pair&&canExtra&&(c.get(0).value==11||c.get(0).value==8))return A.SPLIT;if(!soft(c)&&first&&canExtra&&(t==10||t==11)&&between(up,2,9))return A.DOUBLE;
    if(soft(c)){if(t>=19)return A.STAND;if(t==18&&(up==2||up==7||up==8))return A.STAND;return A.HIT;}if(t>=17)return A.STAND;if(t>=13)return between(up,2,6)?A.STAND:A.HIT;if(t==12)return between(up,4,6)?A.STAND:A.HIT;return A.HIT;
  }
  static String decisionKey(int hand,String branch,int ord,List<Card>c,int up){List<String>r=new ArrayList<>();for(Card q:c)r.add(q.rank);Collections.sort(r);return hand+"|"+branch+"|"+ord+"|"+String.join(",",r)+"|UP"+up;}

  static void playReplayBranch(ReplayResult rr,ReplayShoe shoe,List<PlayedHand> out,List<Card> cards,int up,double stake,boolean frozen,int hand,String branch,boolean allowSplit,HandCtx hc)throws EOFException{
    boolean first=true;int ord=0;
    while(true){
      if(total(cards)>21){out.add(new PlayedHand(cards,stake));return;}
      boolean pair=allowSplit&&first&&cards.size()==2&&cards.get(0).value==cards.get(1).value;boolean canExtra=rr.bank-hc.committed+0.001>=stake;
      A act=frozen?frozenReplayAction(cards,up,first,pair,canExtra):casualReplayAction(cards,up,first,pair,canExtra);
      rr.decisions.add(new ReplayDecision(decisionKey(hand,branch,ord++,cards,up),act));
      if(act==A.STAND){out.add(new PlayedHand(cards,stake));return;}
      if(act==A.DOUBLE&&canExtra){hc.committed+=stake;List<Card>d=new ArrayList<>(cards);d.add(replayDraw(rr,shoe));out.add(new PlayedHand(d,stake*2));return;}
      if(act==A.SPLIT&&pair&&canExtra){hc.committed+=stake;Card a=cards.get(0),b=cards.get(1);List<Card>x=new ArrayList<>(List.of(a,replayDraw(rr,shoe))),y=new ArrayList<>(List.of(b,replayDraw(rr,shoe)));if(a.value==11&&b.value==11){out.add(new PlayedHand(x,stake));out.add(new PlayedHand(y,stake));return;}playReplayBranch(rr,shoe,out,x,up,stake,frozen,hand,branch+"A",false,hc);playReplayBranch(rr,shoe,out,y,up,stake,frozen,hand,branch+"B",false,hc);return;}
      cards.add(replayDraw(rr,shoe));first=false;allowSplit=false;
    }
  }

  static void updateReplayState(ReplayResult r){if(r.replayState==State.BRAKE)return;if(r.replayState==State.COOLING){if(r.bank>=120)r.replayState=State.CAUTIOUS;return;}if(r.replayState==State.CAUTIOUS){if(r.bank>=140)r.replayState=State.NORMAL;return;}if(r.reached130Replay&&r.bank<=100)r.replayState=State.COOLING;}
  static double frozenReplayWager(ReplayResult r,int h){updateReplayState(r);if(r.replayState!=State.NORMAL)return 15;if(h<=8||r.bank<130)return 15;if(r.bank<150)return 20;if(r.bank<170)return 30;return 15;}

  static ReplayResult runReplay(List<Card> reservoir,ShuffleMethod method,long seed,boolean frozen){
    ReplayResult rr=new ReplayResult();ReplayShoe shoe=new ReplayShoe(method,reservoir,seed);
    try{
      for(int h=1;h<=30;h++){
        double base=frozen?frozenReplayWager(rr,h):15.0;if(rr.bank+0.001<15||rr.bank+0.001<base){rr.reason="BANKROLL";break;}shoe.beforeHand();
        Card p1=replayDraw(rr,shoe),hole=replayDraw(rr,shoe),p2=replayDraw(rr,shoe),up=replayDraw(rr,shoe);List<Card>player=new ArrayList<>(List.of(p1,p2)),dealer=new ArrayList<>(List.of(up,hole));double before=rr.bank;HandCtx hc=new HandCtx(base);List<PlayedHand>ph=new ArrayList<>();
        boolean pn=total(player)==21,dn=total(dealer)==21;double net=0;
        if(dn){net=pn?0:-base;ph.add(new PlayedHand(player,base));}
        else if(pn){net=1.5*base;ph.add(new PlayedHand(player,base));}
        else{
          playReplayBranch(rr,shoe,ph,player,up.value,base,frozen,h,"MAIN",true,hc);
          boolean anyLive=false;for(PlayedHand x:ph)if(total(x.cards)<=21)anyLive=true;
          if(anyLive)while(total(dealer)<17)dealer.add(replayDraw(rr,shoe));int dt=total(dealer);
          for(PlayedHand x:ph){int pt=total(x.cards);if(pt>21)net-=x.stake;else if(dt>21||pt>dt)net+=x.stake;else if(pt<dt)net-=x.stake;}
        }
        rr.exposure+=hc.committed;rr.bank+=net;rr.hands++;if(net>0)rr.w++;else if(net<0)rr.l++;else rr.p++;rr.peak=Math.max(rr.peak,rr.bank);rr.trough=Math.min(rr.trough,rr.bank);rr.maxDD=Math.max(rr.maxDD,rr.peak-rr.bank);if(rr.bank>=130)rr.reached130Replay=true;if(rr.bank>=200)rr.reached200=true;shoe.afterHand();
      }
      if(rr.hands==30)rr.reason="COMPLETED";
    }catch(EOFException e){rr.sourceExhausted=true;rr.reason="SOURCE_EXHAUSTED";}
    return rr;
  }

  static PairDiff compareReplayDecisions(ReplayResult f,ReplayResult c){
    Map<String,A> fm=new LinkedHashMap<>();for(ReplayDecision d:f.decisions)fm.putIfAbsent(d.key,d.action);PairDiff pd=new PairDiff();for(ReplayDecision d:c.decisions){A x=fm.get(d.key);if(x!=null){pd.comparable++;if(x!=d.action)pd.different++;}}return pd;
  }
  static String methodDescription(ShuffleMethod m){return switch(m){case A->"Observed-card permutation: shuffle only the captured session-card reservoir; no cards added.";case B->"Full six-deck RNG: generate and uniformly shuffle a fresh 312-card six-deck shoe.";case C1->"Penetration 50%: captured reservoir; reshuffle only remaining cards between hands at 50%.";case C2->"Penetration 65%: captured reservoir; reshuffle only remaining cards between hands at 65%.";case C3->"Penetration 75%: captured reservoir; reshuffle only remaining cards between hands at 75%.";case C4->"Penetration 80%: captured reservoir; reshuffle only remaining cards between hands at 80%.";case D->"Physical-style proxy: captured reservoir with deterministic riffle/strip/cut-style mixing.";case E->"Automatic/continuous proxy: captured reservoir; after each complete hand, used cards return and the full reservoir is remixed.";};}
  static double mean(List<Double>x){double s=0;for(double v:x)s+=v;return x.isEmpty()?Double.NaN:s/x.size();}
  static double median(List<Double>x){if(x.isEmpty())return Double.NaN;List<Double>y=new ArrayList<>(x);Collections.sort(y);int n=y.size();return n%2==1?y.get(n/2):(y.get(n/2-1)+y.get(n/2))/2;}

  static MethodAggregate runMethod(List<Card> reservoir,ShuffleMethod method,int reps){
    MethodAggregate a=new MethodAggregate(method,reps);System.out.println("\n"+method+" - "+methodDescription(method));
    for(int i=0;i<reps;i++){
      long seed=SHUFFLE_SEED_BASE+1000003L*method.ordinal()+i;ReplayResult f=runReplay(reservoir,method,seed,true),c=runReplay(reservoir,method,seed,false);PairDiff d=compareReplayDecisions(f,c);
      a.frozenFinal.add(f.bank);a.casualFinal.add(c.bank);a.frozenExposure+=f.exposure;a.casualExposure+=c.exposure;a.frozenDD+=f.maxDD;a.casualDD+=c.maxDD;if(f.reached200)a.frozenReach++;if(c.reached200)a.casualReach++;if(f.hands==30)a.frozenComplete++;if(c.hands==30)a.casualComplete++;if(f.hands==30&&c.hands==30){a.bothComplete++;a.pairedDelta+=f.bank-c.bank;}a.comparableActions+=d.comparable;a.differentActions+=d.different;
    }
    System.out.println("Completed "+reps+" paired replays | mean comparable-action differences "+String.format(Locale.ROOT,"%.4f",(double)a.differentActions/reps));return a;
  }

  static String latestPendingSessionId(){
    if(!Files.exists(CUMULATIVE_OUTPUT))return null;
    try{
      LinkedHashMap<String,String> status=new LinkedHashMap<>();
      for(String x:Files.readAllLines(CUMULATIVE_OUTPUT)){
        String t=x.trim();
        if(!t.startsWith("SHUFFLE_ANALYSIS_STATUS | session "))continue;
        String[]p=t.split("\\|");
        if(p.length<3)continue;
        String sid=p[1].trim().replaceFirst("^session\\s+","");
        String st=p[2].trim();
        status.put(sid,st);
      }
      // Process the oldest unresolved session first so a deferred analysis cannot
      // become hidden by a newer live session.
      for(Map.Entry<String,String> e:status.entrySet())
        if(e.getValue().equals("PENDING")||e.getValue().startsWith("DEFERRED"))return e.getKey();
      return null;
    }catch(Exception e){return null;}
  }
  static boolean hasPendingShuffleAnalysis(){return latestPendingSessionId()!=null;}
  static void appendAnalysisDeferral()throws Exception{String sid=latestPendingSessionId();if(sid!=null)Files.writeString(CUMULATIVE_OUTPUT,"SHUFFLE_ANALYSIS_STATUS | session "+sid+" | DEFERRED_BY_OPERATOR\n",StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);}

  static Path reportForSession(String sid){
    Path direct=Path.of("blackjack_live_session_"+sid+".txt");
    if(Files.exists(direct))return direct;
    // lastSavedReport is safe only when it actually belongs to the requested SID.
    // Otherwise fall back to the matching session block in output.txt.
    if(lastSavedReport!=null&&Files.exists(lastSavedReport)&&
       lastSavedReport.getFileName().toString().equals("blackjack_live_session_"+sid+".txt"))return lastSavedReport;
    return CUMULATIVE_OUTPUT;
  }
  static List<Card> loadCapturedReservoir(Path source,String sid)throws Exception{
    List<String> lines=Files.readAllLines(source);List<Card> cards=new ArrayList<>();boolean in=false;String sessionMarker="================ SESSION "+sid+" ================";
    if(source.equals(CUMULATIVE_OUTPUT)){boolean found=false;for(String line:lines){if(line.trim().equals(sessionMarker)){found=true;in=false;}else if(found&&line.startsWith("================ END SESSION "))break;else if(found&&line.trim().equals("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)"))in=true;else if(found&&in&&line.trim().equals("AUDIT LOG"))break;else if(found&&in)parseChronCard(line,cards);} }
    else {for(String line:lines){if(line.trim().equals("CHRONOLOGICAL CARD DATASET (RECONSTRUCTED DEAL ORDER)")){in=true;continue;}if(in&&line.trim().equals("AUDIT LOG"))break;if(in)parseChronCard(line,cards);}}
    if(cards.isEmpty())throw new IOException("No chronological captured-card dataset found for session "+sid);return cards;
  }
  static void parseChronCard(String line,List<Card> out){String t=line.trim();if(!t.matches("#\\d+.*\\|.*"))return;String raw=t.substring(t.lastIndexOf('|')+1).trim();Card c=parseCard(raw);if(c!=null&&!c.suit.isEmpty()&&!c.suit.equals("?"))out.add(c);}

  static void runAutomaticShuffleAnalysis()throws Exception{
    String sid=latestPendingSessionId();if(sid==null){System.out.println("\nNo PENDING live-session shuffle analysis was found in output.txt.");return;}Path source=reportForSession(sid);List<Card> reservoir=loadCapturedReservoir(source,sid);
    System.out.println("\nPOST-SESSION SHUFFLE ROBUSTNESS - SESSION "+sid);System.out.println("Captured reservoir: "+reservoir.size()+" cards | paired replays per method: "+DEFAULT_SHUFFLE_REPLICATES);System.out.println("Frozen and fixed Casual receive the same seeded starting source per replicate and then consume independently.");
    List<MethodAggregate> all=new ArrayList<>();for(ShuffleMethod m:ShuffleMethod.values())all.add(runMethod(reservoir,m,DEFAULT_SHUFFLE_REPLICATES));
    List<String> out=new ArrayList<>();out.add("");out.add("================ AUTOMATIC SHUFFLE ROBUSTNESS | SESSION "+sid+" ================");out.add("ENGINE VERSION: integrated-post-session-v3 | paired_replays_per_method "+DEFAULT_SHUFFLE_REPLICATES+" | seed_base "+SHUFFLE_SEED_BASE);out.add("Captured-card reservoir: "+reservoir.size()+" cards");out.add("Comparable-action definition: same replay hand, branch, decision ordinal, player-rank state and dealer upcard in both independently consumed paths.");
    for(MethodAggregate a:all){double md=(double)a.differentActions/a.reps;out.add(a.method+" | "+methodDescription(a.method));out.add(String.format(Locale.ROOT,"  Frozen median %.2f | Casual median %.2f | Frozen mean %.2f | Casual mean %.2f",median(a.frozenFinal),median(a.casualFinal),mean(a.frozenFinal),mean(a.casualFinal)));out.add(String.format(Locale.ROOT,"  £200 reach Frozen %.2f%% | Casual %.2f%% | Mean exposure Frozen %.2f | Casual %.2f | Mean maxDD Frozen %.2f | Casual %.2f",100.0*a.frozenReach/a.reps,100.0*a.casualReach/a.reps,a.frozenExposure/a.reps,a.casualExposure/a.reps,a.frozenDD/a.reps,a.casualDD/a.reps));out.add("  30-hand completion Frozen "+a.frozenComplete+" | Casual "+a.casualComplete+" | Both "+a.bothComplete);out.add(String.format(Locale.ROOT,"  Mean paired final-bankroll difference Frozen-Casual (both completed): %s",a.bothComplete==0?"N/A":String.format(Locale.ROOT,"%.2f",a.pairedDelta/a.bothComplete)));out.add("  Comparable action decisions total: "+a.comparableActions+" | Different actions total: "+a.differentActions+" | Mean Comparable Action Differences: "+String.format(Locale.ROOT,"%.4f",md));}
    out.add("Method E boundary: after each COMPLETE hand, consumed cards return to the captured reservoir and the entire reservoir is remixed; no captured card is added, removed or substituted.");out.add("Method D boundary: declared mathematical riffle/strip/cut proxy; not a proprietary casino-shuffler reproduction.");out.add("SHUFFLE_ANALYSIS_STATUS | session "+sid+" | COMPLETE");out.add("================ END AUTOMATIC SHUFFLE ROBUSTNESS | SESSION "+sid+" ================");Files.write(CUMULATIVE_OUTPUT,out,StandardCharsets.UTF_8,StandardOpenOption.CREATE,StandardOpenOption.APPEND);Path rf=Path.of("blackjack_shuffle_analysis_"+sid+".txt");Files.write(rf,out,StandardCharsets.UTF_8);System.out.println("\nShuffle analysis complete. Results appended to "+CUMULATIVE_OUTPUT.toAbsolutePath());System.out.println("Standalone analysis report: "+rf.toAbsolutePath());
  }

}
