#pragma mode( separator(.,;) integer(h32) )

// ============================================================
// CICATCHER - beta 2c
// Confidence intervals: "95 % of WHAT?"
// ============================================================
// Author: Roger Metcalf, 2026.  Written with AI assistance
//   (Anthropic Claude); concept, direction and all hardware
//   testing by the author.
//
// FREE FOR EDUCATIONAL AND PERSONAL USE.  Keep this header with
//   the file and credit the author if you pass it on.  Not for
//   sale or commercial redistribution.  Provided as is, with no
//   warranty: check any result you intend to rely on.
// DISCLAIMER: a teaching and demonstration aid. Not validated for
//   forensic, clinical or legal use; do not rely on its results for
//   legal purposes.
//
// Firmware: written for the 2026-09-09 firmware (2.4.2).  This
//   version (beta 2c) has run on the PC harness only (ppl_run_v3e,
//   verify_CICatcher_beta2c.py): NOT on the emulator, NOT on
//   hardware.  No version has run on hardware yet.  On the HP Prime
//   Virtual Calculator (2.4.2, build 2026-09-09, the calculators'
//   firmware): beta 1 passed round 1 (17 Oct 2026); beta 2a ran in
//   round 2 (17 Oct 2026), and its setup menu cut "Proportion:
//   Exact (Clopper-Pearson)" at the right edge; beta 2b passed the
//   retest that afternoon, its setup menu whole.  The five interval
//   formulas and every coverage figure on the help pages were
//   checked against scipy and statsmodels, and again by the fleet
//   review of 19 Oct 2026 (check_CICatcher.py: no wrong number).
//   The CAS's fallback (the "decimal" badge) has never been needed
//   on the emulator.
//
// Companion files: CICatcher_beta2c_CHANGES.md (beta 2c),
//   CICatcher_beta2b_CHANGES.md (beta 2b), CICatcher_beta2a_CHANGES.md
//   (beta 2a), CICatcher_beta2_NOTES.md (what changed in beta 2, the
//   device-test checklist, what is unverified) and
//   cicatcher-guide_beta2.md (the user's guide; two menu lines have
//   changed since it was written: the setup menu's "Proportion:
//   Exact (Clopper-Pearson)" is now "Proportion: Exact (C-P)", and
//   the main menu's "New setup: interval, population, n" is now
//   "New: interval, population, n"; nothing else a user sees
//   changed, apart from help page 4's last line).
//
// beta 2c (19 Oct 2026): two lines a character or two too wide for
//   their boxes, C-C1 and C-C2 (fleet review 19 Oct 2026,
//   findings_C).  Line numbers are beta 2b's.  Nothing else changed.
//   - C-C1, the main menu (cqMenu, 1854).  "New setup: interval,
//     population, n" is 34 characters; the same list-form CHOOSE box
//     cut "Proportion: Exact (Clopper-Pearson)" (35) after about 31 on
//     the emulator, so a student could see "New setup: interval,
//     populatio".  It is now "New: interval, population, n" (28, the
//     length every other CHOOSE line here keeps to).  The three
//     things the item sets up keep their names: the review's
//     "interval, data, n" was not used, because the form asks for the
//     POPULATION's true values (its title is "Population and
//     sample"), not for data.
//   - C-C2, help page 4's last line (cqXEngS, 1723).  "exact ends:
//     bisection on BINOMIAL_CDF (checked)" is 47 characters, one over
//     the 46 that cqPage's 296-px clip holds at about 6.5 px a
//     character, so the closing ")" could be cut.  It is now "exact
//     ends: bisection on BINOMIAL_CDF, checked" (46).
//   - The DISCLAIMER line under the distribution terms (Roger, 19 Oct
//     2026).  No page of the program states the terms, so no screen
//     changed.
//
// beta 2b (17 Oct 2026): the round-2 emulator fixes.  Line numbers
//   are beta 2a's.  Nothing else changed.
//   - cqCas (505-528).  Typed in Home on the emulator,
//     INSTRING("", "a") is "Error: Invalid input", and cR is "" after
//     any failed CAS call, so the fallback itself would have stopped
//     the program (the emulator's CAS never failed, so it did not
//     show).  The undef / error / infinity checks now run only on a
//     non-empty answer.  The file has no other INSTRING; its one LEFT
//     (517) takes 1 character, and every MID starts at 1 or later.
//   - The setup menu (cqSetup, 1562): "Proportion: Exact (Clopper-
//     Pearson)" was cut at the right edge of the CHOOSE box on the
//     emulator ("...Clopper-Pear").  It is now "Proportion: Exact
//     (C-P)": the word "Exact" stays, because the header line
//     ("Exact, p = 0.10, n = 20"), the start-up message, help page 4
//     and the guide all call it Exact, and help page 4 spells out
//     Clopper-Pearson.
//   - Fixed decimals.  STRING(x, 2, d) prints SCIENTIFIC when
//     0 < |x| < 10^-d (STRING(0.096, 2, 1) is "9.6E-2" with a small-
//     capital E, emulator, round 2).  cqF (264-267), which prints the
//     estimates, the interval ends, "missed by" and the closed form's
//     decimal, now rounds first: STRING(ROUND(x, d) + 0, 2, d).  An
//     Exact interval for 1 "yes" in 100 starts at 0.00025 (beta 2a:
//     "2.531E-4"), and a Wilson end for 0 "yes" answers can land a
//     hair below 0 ("-5.55E-17" in PC arithmetic); "+ 0" makes a
//     rounded -0 read 0.00.  cqPct (291-295), the caught-
//     percentages, rounds first too (a share under 0.05 % takes an
//     unlucky run, but the guard costs nothing).  cqNumS
//     (271-280) shows a typed mu or sigma below 0.0001 with the
//     decimals it needs (two significant digits) instead of
//     "1.0000E-5".  Left as they were: the level (50 to 99.9 %), the
//     counts, and the axis labels (whole multiples of the step).
//
// beta 2a (17 Oct 2026): the CAS call form only.  CASProbe beta 1b
//   found that CAS(s) with s a variable hands the text back
//   UNEVALUATED, so beta 2's cqCas (484-508) returned its own input:
//   the panel would have shown "1-(50/2000)^(1/20)", unreduced, with
//   the badge "exact (CAS)".  Its IFERR line is now
//     STRING(EXPR("CAS(" + CHAR(34) + "string(" + s + ")" + CHAR(34) + ")"))
//   (BRIEF_CAS_v2), the quotes STRING() adds are dropped, and an
//   answer holding the infinity sign is refused like undef.  No
//   reader changed: the answer is only shown (cqTimes, cqCFDraw), and
//   it prints alike on the Prime ("1-(1/40)^(1/20)"; for n = 2 the
//   root sign, "1-√(1/40)").  Nothing else changed.
//
// beta 2 (16 Oct 2026): the EXACT interval, built on beta 1 (every
//   beta 1 feature kept).  NOT yet run on hardware.
//   - A fifth interval, "Proportion: Exact (Clopper-Pearson)", beside
//     Wald and Wilson.  Its ends come from cqCPB / cqCPInt, the
//     fleet's one exact-interval routine (the same lines as EpiStats
//     v1.7's es_CPB / es_CPInt): bisection on BINOMIAL_CDF with a
//     Newton step when it lands inside the bracket.
//   - Its coverage is kept exactly as the others' is: each interval
//     stores its catch level, here 1 - 2 min(P(X >= k), P(X <= k))
//     under the true p -- a table of n + 1 values worked out once per
//     run (cqXBuild), so +50 costs no searches at all.  The ends drawn
//     at the current level are cached per count (cqXGet), and a drag
//     of the level re-solves only the counts on screen, each from its
//     last end.
//   - Help page 4 (new): why exact intervals are conservative (never
//     below the level), with the numbers at p = 0.10, n = 20: exact
//     98.9 %, Wilson 95.7 %, Wald 87.6 %; worst case over p 95.8 %.
//   - The CAS, behind the fleet's fence (one call routine, cqCas;
//     whole numbers only in CAS text; events only, cached; a decimal
//     fallback and a badge).  Its one job here: when the picked line
//     is an Exact interval for 0 (or n) "yes" answers, the bottom panel
//     shows the closed form of its end, e.g. upper = 1-(1/40)^(1/20),
//     which the CAS writes from the level and n.
//   - A one-time check at start (cqBinCheck): BINOMIAL_CDF and
//     BINOMIAL must give two known values, or Exact is refused with a
//     message instead of drawing wrong intervals.
//
// beta 1 (16 Oct 2026): first version, from the fleet review of
//   16 Oct 2026 (section 7, item 4).  Built from the house touch
//   kit -- ChiTable beta 1k and GaltonBoard beta 1b: file-level
//   LOCAL state, palette constants set in cqInit, the capped
//   finger-lift drain, the key-or-tap wait, DRAWMENU soft keys
//   read as taps at y >= 218, every frame composed in G1 and
//   blitted once.  Normal samples come from NormExplorer beta
//   40's Marsaglia polar sampler (ne_SampBins), copied under
//   this program's own tag.  Pure PPL.
//
// Run CICatcher() from the Program Catalog.
// ============================================================
//
// THE IDEA
//   A confidence level is a promise about a METHOD.  This program
//   draws sample after sample from a population whose true mean
//   (or proportion) it knows, turns each sample into an interval
//   by the textbook recipe, and keeps count of how many intervals
//   catch the truth.  At 95 % about 95 of every 100 do -- and any
//   ONE interval either caught it or did not.  That is the answer
//   to "95 % of WHAT?": 95 % of the intervals the method makes.
//
// WHAT YOU SEE
//   Green vertical line ...... the truth (mu, or p), which the
//                              method never sees
//   Horizontal lines ......... the 50 most recent intervals, the
//                              newest on top, 3 px apart; the dot
//                              is the sample's estimate.  BLUE =
//                              caught the truth, RED = missed it.
//   Header ................... "caught 47 of 50 (94.0 %), all 237:
//                              95.4 %" and the target level
//   Right edge (x >= 300) .... the level slider, 50 % to 99.9 %,
//                              with its magenta knob
//   Grey zones (proportions) . values below 0 or above 1, which a
//                              proportion cannot take
//   Bottom panel ............. the sample behind the line you
//                              tapped: its values, its estimate,
//                              its interval, caught or missed
//
// KEYS AND TOUCH
//   soft keys: +1 (one sample, animated), +10, +50, Conf (80, 90,
//     95, 99 %), n (sample size: a fresh run), Menu (new setup,
//     reset the counts, help, quit)
//   drag the right edge ...... the level, live: every visible line
//                              is recomputed as the finger moves
//   Up / Down ................ the level, 1 % at a time
//   tap or slide on the lines  that sample in the bottom panel
//   Left / Right ............. the next line up / down the stack
//   - = +1, + = +10, Enter = menu, Esc = quit
//
// THE FIVE INTERVALS (z* = NORMALD_ICDF(0, 1, 1 - alpha/2))
//   1 mean, sigma known:   xbar +/- z* sigma / sqrt(n)
//   2 mean, sigma unknown: xbar +/- t* s / sqrt(n),  df = n - 1
//       t* = STUDENT_ICDF(df, 1 - alpha/2), inside IFERR and checked
//       against STUDENT_CDF; if that fails, bisection on
//       STUDENT_CDF.  The help page names the one that ran.
//   3 proportion, Wald:    phat +/- z* sqrt(phat (1 - phat) / n)
//       (phat = 0 or 1 gives an interval of zero width)
//   4 proportion, Wilson:  (phat + z*^2/2n
//                           +/- z* sqrt(phat(1 - phat)/n + z*^2/4n^2))
//                          / (1 + z*^2/n)
//   5 proportion, Exact (Clopper-Pearson, beta 2): every p under which
//       the count k is in neither alpha/2 tail of Binomial(n, p):
//       lower: P(X >= k | p) = alpha/2, upper: P(X <= k | p) = alpha/2
//       (k = 0: from 0 to 1 - (alpha/2)^(1/n); k = n: from
//       (alpha/2)^(1/n) to 1).  No critical value, no normal curve.
//
// HOW THE COUNTS ARE KEPT (so the level can move without
//   resampling)
//   Each interval is "caught" exactly when one number, its pivot,
//   is at most the critical value:
//     z:      |xbar - mu| / (sigma / sqrt(n)) <= z*
//     t:      |xbar - mu| / (s / sqrt(n))     <= t*
//     Wald:   |phat - p| / sqrt(phat (1 - phat) / n) <= z*
//     Wilson: |phat - p| / sqrt(p (1 - p) / n)       <= z*
//     Exact:  min(P(X >= k), P(X <= k)) under p    >= alpha/2
//   (the Wilson interval is by construction the set of p that
//   pass that test; the exact one the set that pass the exact
//   binomial test with each tail at alpha/2).  So for each interval the program stores the
//   LOWEST level, in steps of 0.1 %, at which it catches the
//   truth -- its "catch level", from 2 * CDF(pivot) - 1 (Exact:
//   1 - 2 min(P(X >= k), P(X <= k)), the same for every sample with
//   the same count k, so it is a table of n + 1 numbers per run).  An
//   interval is blue at level L exactly when its catch level is
//   at most L.  A tally of catch levels over every interval ever
//   drawn gives the "all" count at any level by adding, so the
//   long-run rate follows the slider exactly, for all intervals,
//   not just the 50 on screen.
//
// HOME EXPORTS (after the program ends)
//   CQ_TOTAL  intervals drawn in the last run
//   CQ_HITS   how many caught the truth at the final level
//   CQ_COVER  CQ_HITS / CQ_TOTAL, the long-run catch rate
//
// LAYOUT (y, pixels)
//   header 1-25 | intervals 29-176 (row r at 29 + 3(r - 1)) |
//   axis 179, tick labels 183 | bottom panel 195-217 |
//   soft keys 218-239 (painted by DRAWMENU).  Slider x 298-319.
//   Font 1 is about 6.5 px a character: text pages keep a line to
//   46 characters and pass a width so nothing runs off the edge.
// ============================================================


// ------------------------------------------------------------
// RESULTS VISIBLE FROM HOME
// ------------------------------------------------------------
EXPORT CQ_TOTAL, CQ_HITS, CQ_COVER;

// ------------------------------------------------------------
// STATE SHARED BY EVERY ROUTINE (private to this program).
// All of it is set in cqInit or cqReset.
// ------------------------------------------------------------
// ---- the population and the method ----
LOCAL cqType, cqMu, cqSg, cqP, cqN, cqDF;
//  cqType 1 mean z, 2 mean t, 3 proportion Wald, 4 Wilson
//  cqMu, cqSg the true mean and SD; cqP the true proportion
//  cqN the sample size; cqDF = n - 1 (for t)
LOCAL cqL, cqCV, cqEng, cqNm, cqNp, cqSel;
//  cqL the confidence level in thousandths (950 = 95.0 %),
//      always a whole number from 500 to 999
//  cqCV the critical value (z* or t*) at level cqL
//  cqEng which code produced t*: 0 none yet, 1 STUDENT_ICDF,
//      2 bisection on STUDENT_CDF
//  cqNm, cqNp the n last used for a mean / for a proportion
//  cqSel the ring slot of the picked interval (0 = none)

// ---- the 50 visible samples: a ring buffer of slots 1..50 ----
LOCAL cqEst, cqSd, cqCat, cqRaw, cqHead, cqVis;
//  cqEst(s) the estimate (xbar or phat) of slot s
//  cqSd(s)  its sample SD (used by t; 0 for proportions)
//  cqCat(s) its catch level, 500..1000 (1000 = never caught
//           at or below 99.9 %)
//  cqRaw    the raw values of all 50 samples in ONE flat list:
//           slot s, value i at index (s - 1) * n + i
//  cqHead   the slot of the newest sample; cqVis how many
//           slots are in use (0..50)

// ---- the long-run tally, every interval ever drawn ----
LOCAL cqTot, cqHit, cqHist, cqCVc;
//  cqTot intervals drawn; cqHit how many catch at level cqL
//  cqHist(c - 499) how many intervals have catch level c
//  cqCVc(L - 499) cached critical value at level L (-1 = not yet)

// ---- the x window, frozen for a run ----
LOCAL cqWL, cqWR, cqSX, cqStp, cqTDec, cqVDec;
//  window cqWL..cqWR; cqSX pixels per unit; cqStp tick step;
//  cqTDec tick-label decimals; cqVDec decimals for the panel

// ---- animation and touch ----
LOCAL cqOff, cqGrow, cqTX, cqTY;
//  cqOff  y offset of rows 2..50 while the stack slides down
//  cqGrow -1 normal; 0 newest row hidden; 0 < g <= 1 the newest
//         interval drawn grey at g times its full half-width
//  cqTX, cqTY the last tap seen by cqWaitAny (-1 = none)

// ---- geometry, shared by the drawing and the touch code ----
LOCAL cqPL, cqPR, cqPT, cqPB, cqRY0, cqAY;
LOCAL cqPY, cqSLX, cqST, cqSB, cqZlo, cqZhi;
LOCAL cqSYt, cqSLt;
//  plot box cqPL..cqPR x cqPT..cqPB; row 1 at y = cqRY0;
//  axis at cqAY; bottom panel from cqPY; slider zone x >= cqSLX,
//  track from cqST (99.9 %) down to cqSB (50 %), spaced by z:
//  cqZlo = z at 50 %, cqZhi = z at 99.9 %.  cqSYt, cqSLt: the
//  slider's tick positions and labels.

// ---- house colours ----
LOCAL cqBLUE, cqRED, cqGRN, cqMAG, cqORNG, cqGRAY;
LOCAL cqFOOT, cqGRID, cqLBLU, cqLRED, cqLORN, cqLGRY;

// ---- beta 2: the exact interval and the CAS ----
LOCAL cqXCat, cqXLo, cqXHi, cqXLv, cqBinOK, cqCasOK;
LOCAL cqCasT, cqCasK;
//  cqXCat(k + 1) the catch level of an Exact interval for k "yes"
//         answers (500..1000), built once per run by cqXBuild
//  cqXLo, cqXHi (k + 1) its ends at level cqXLv(k + 1) (-1 = none
//         worked out yet), kept by cqXGet
//  cqBinOK 1 = BINOMIAL_CDF and BINOMIAL passed cqBinCheck at start
//  cqCasOK 1 = the last CAS call answered, 0 = it fell back
//  cqCasT  the CAS's closed form for the picked line (cqCasPanel),
//          for the key cqCasK (which end, n and the level)

// ---- display glyphs, built with CHAR() so the code stays ASCII ----
LOCAL cqXb, cqPh, cqMuS, cqSgS, cqUD, cqLR;
LOCAL cqTms;
//  x-bar, p-hat, mu, sigma, the up-down and left-right arrows,
//  the multiplication sign


// ------------------------------------------------------------
// SMALL HELPERS
// ------------------------------------------------------------

// A number as text with a FIXED number of decimals.  Mode 2 keeps
// trailing zeros; mode 1 would ignore the digits argument.
// beta 2b: rounded first.  On the Prime STRING(x, 2, d) turns
// SCIENTIFIC when 0 < |x| < 10^-d ("3.000E-4" for an interval end of
// 0.0003 at 3 decimals; emulator, 17 Oct 2026); ROUND makes x either
// 0 or at least 10^-d, so the fixed form always holds.  "+ 0" turns a
// rounded -0 into 0: a Wilson end for 0 "yes" answers can come out a
// hair below 0 (-5.6E-17 on the PC) and should read 0.00, not -0.00.
cqF(fmV, fmD)
BEGIN
  RETURN STRING(ROUND(fmV, fmD) + 0, 2, fmD);
END;

// A parameter as typed: 100 stays "100", 0.1 becomes "0.10",
// anything finer gets four decimals.
// beta 2b: a value below 0.0001 (a typed mu or sigma; p is 0.01 or
// more) gets two significant digits -- four decimals would print it
// in scientific form ("1.0000E-5") -- and the value is rounded before
// STRING, so the fixed form always holds.
cqNumS(nsV)
BEGIN
  LOCAL nsD;
  nsD := 0;
  IF ABS(nsV - ROUND(nsV, 0)) > 0.0000001 THEN
    nsD := 2;
    IF ABS(nsV - ROUND(nsV, 2)) > 0.0000001 THEN nsD := 4; END;
  END;
  IF nsD == 4 AND ABS(nsV) < 0.0001 THEN nsD := MIN(11, 1 - FLOOR(LOG(ABS(nsV)))); END;
  RETURN STRING(ROUND(nsV, nsD), 2, nsD);
END;

// A level in thousandths as a percentage: 950 -> "95",
// 953 -> "95.3", 999 -> "99.9".
cqLvS(lvL)
BEGIN
  IF lvL MOD 10 == 0 THEN RETURN STRING(lvL / 10, 2, 0); END;
  RETURN STRING(lvL / 10, 2, 1);
END;

// a out of b as a percentage with one decimal ("--" when b = 0)
// beta 2b: rounded first (1 of 2,000 is 0.05 %, which STRING(x, 2, 1)
// would print in scientific form)
cqPct(pcA, pcB)
BEGIN
  IF pcB <= 0 THEN RETURN "--"; END;
  RETURN STRING(ROUND(100 * pcA / pcB, 1), 2, 1);
END;

// A "nice" step (1, 2 or 5 times a power of ten) at least as big
// as the value handed in -- Heckbert's rule, for axis ticks.
cqNice(nnV)
BEGIN
  LOCAL nnE, nnF;
  IF nnV <= 0 THEN RETURN 1; END;
  nnE := FLOOR(LOG(nnV));
  nnF := nnV / 10^nnE;
  IF nnF <= 1 THEN RETURN 10^nnE; END;
  IF nnF <= 2 THEN RETURN 2 * 10^nnE; END;
  IF nnF <= 5 THEN RETURN 5 * 10^nnE; END;
  RETURN 10^(nnE + 1);
END;

// Wait for the finger to lift -- but never for more than a second.
// MOUSE(1) is -1 while nothing touches the screen.  Without the
// time limit a stuck or phantom touch would hold the program here
// for ever, and the only way out would be a reset.
cqDrain()
BEGIN
  LOCAL drT;
  drT := TICKS;                                  // milliseconds
  REPEAT UNTIL MOUSE(1) == -1 OR TICKS - drT > 1000;
END;

// Drain, wait for a key OR a tap, drain.  Returns the key code,
// or -1 for a tap, whose x and y are left in cqTX, cqTY.
cqWaitAny()
BEGIN
  LOCAL wk, wm, wm1;
  REPEAT wk := GETKEY; UNTIL wk == -1;
  cqDrain();
  REPEAT
    wk := GETKEY;
    wm := MOUSE;
    wm1 := wm(1);
  UNTIL wk > -1 OR SIZE(wm1) > 0;
  cqTX := -1; cqTY := -1;
  IF SIZE(wm1) > 0 THEN cqTX := wm1(1); cqTY := wm1(2); wk := -1; END;
  cqDrain();
  REPEAT UNTIL GETKEY == -1;
  RETURN wk;
END;

// A short pause for the animation, timed with TICKS (the same
// clock the drain uses).
cqPause(paMs)
BEGIN
  LOCAL paT;
  paT := TICKS;
  REPEAT UNTIL TICKS - paT >= paMs;
END;

// The truth the method is trying to catch.
cqTruth()
BEGIN
  IF cqType <= 2 THEN RETURN cqMu; END;
  RETURN cqP;
END;

// Pixel column of value v on the frozen window (not clamped:
// callers clip to the plot box themselves).
cqPXi(pxV)
BEGIN
  RETURN IP(cqPL + (pxV - cqWL) * cqSX + 0.5);
END;

// y of row r (row 1 = the newest, at the top).
cqRowY(ryR)
BEGIN
  RETURN cqRY0 + 3 * (ryR - 1);
END;

// The ring slot shown in row r: row 1 is the newest (cqHead),
// row 2 the one before, wrapping from slot 1 back to slot 50.
cqSlot(slR)
BEGIN
  LOCAL slS;
  slS := cqHead - slR + 1;
  IF slS < 1 THEN slS := slS + 50; END;
  RETURN slS;
END;

// The row a slot is shown in, or 0 if it is not on screen.
cqRowOf(roS)
BEGIN
  LOCAL roR;
  IF roS < 1 OR cqVis == 0 THEN RETURN 0; END;
  roR := cqHead - roS + 1;
  IF roR < 1 THEN roR := roR + 50; END;
  IF roR > cqVis THEN RETURN 0; END;
  RETURN roR;
END;

// The row under a finger at height y (nearest row, kept in range).
cqRowAt(raY)
BEGIN
  LOCAL raR;
  raR := ROUND((raY - cqRY0) / 3, 0) + 1;
  RETURN MAX(1, MIN(cqVis, raR));
END;


// ------------------------------------------------------------
// CRITICAL VALUES
// ------------------------------------------------------------

// t* with df = cqDF: the point where the t CDF reaches tcQ.
// STUDENT_ICDF is a documented built-in (and hp-prime-kit saw it
// on the emulator) but it is NOT yet on our device-verified list,
// so: try it inside IFERR, USE the answer before trusting it (the
// t CDF there must give tcQ back), and otherwise bisect on
// STUDENT_CDF, which is verified.  cqEng records which one ran;
// the help page shows it.
cqTCrit(tcQ)
BEGIN
  LOCAL tcV, tcOK, tcLo, tcHi, tcMid, tcI;
  tcOK := 0;
  tcV := 0;
  IFERR
    tcV := STUDENT_ICDF(cqDF, tcQ);
    IF tcV > 0 THEN
      IF ABS(STUDENT_CDF(cqDF, tcV) - tcQ) < 0.000001 THEN tcOK := 1; END;
    END;
  THEN
    tcOK := 0;
  END;
  IF tcOK == 1 THEN
    cqEng := 1;
    RETURN tcV;
  END;
  // ---- fallback: bisection on STUDENT_CDF ----
  // Bracket first: double the upper end until the CDF passes tcQ
  // (df = 1 at 99.9 % needs t* = 636.6).
  tcLo := 0;
  tcHi := 4;
  WHILE STUDENT_CDF(cqDF, tcHi) < tcQ AND tcHi < 1000000 DO
    tcLo := tcHi;
    tcHi := 2 * tcHi;
  END;
  // 60 halvings shrink the bracket below 1E-12 of its width
  FOR tcI FROM 1 TO 60 DO
    tcMid := (tcLo + tcHi) / 2;
    IF STUDENT_CDF(cqDF, tcMid) < tcQ THEN
      tcLo := tcMid;
    ELSE
      tcHi := tcMid;
    END;
  END;
  cqEng := 2;
  RETURN (tcLo + tcHi) / 2;
END;

// The critical value at level crL (thousandths, 500..999), from
// the cache when it is there.  Two-sided: the upper point of the
// middle crL/1000, i.e. the CDF reaches 0.5 + crL/2000.
cqCrit(crL)
BEGIN
  LOCAL crI, crV;
  crI := crL - 499;
  IF cqCVc(crI) > 0 THEN RETURN cqCVc(crI); END;
  IF cqType == 2 THEN
    crV := cqTCrit(0.5 + crL / 2000);
  ELSE
    crV := NORMALD_ICDF(0, 1, 0.5 + crL / 2000);
  END;
  cqCVc(crI) := crV;
  RETURN crV;
END;


// ------------------------------------------------------------
// THE CAS KIT (beta 2) -- copied from BRIEF_CAS.md with xx
// replaced by this program's tag (the kit's parameter xxN is
// cqIN here: cqN is already the sample size).  beta 2a: cqCas in
// the call form CASProbe beta 1b found on the emulator
// (BRIEF_CAS_v2), not yet run on a calculator.  The fence: CAS text is built from whole numbers
// only (cqIStr); the CAS answers events, never frames; its answer
// is text for display, cached; a decimal fallback and a badge.
// ------------------------------------------------------------

// A whole number as plain digits: no digit-grouping marks, no
// decimal point, no exponent.  Home's STRING() follows the Home
// number format (PyProbe saw grouping marks and a special minus
// sign), which the CAS cannot read, so CAS text is built only from
// this routine.
cqIStr(cqIN)
BEGIN
  LOCAL dS, dV, dD;
  dV := ROUND(ABS(cqIN), 0);
  IF dV == 0 THEN RETURN "0"; END;
  dS := "";
  WHILE dV > 0 DO
    dD := dV MOD 10;
    dS := CHAR(48 + dD) + dS;
    dV := (dV - dD) / 10;
  END;
  IF cqIN < 0 THEN dS := "-" + dS; END;
  RETURN dS;
END;

// The ONE place this program calls the CAS.  It returns the CAS answer as text,
// or "" if the CAS failed, gave undef, an error or an infinity.
// The call form is the one CASProbe beta 1b found on the HP Prime Virtual
// Calculator (build 2026-09-09, 17 Oct 2026): CAS(s) with s a variable hands
// the text back UNEVALUATED, so the text is built into a literal CAS("...")
// that EXPR reads at run time, and the CAS is asked for string(...) of the
// answer, so a big integer crosses into PPL whole, not as a 12-digit real.
cqCas(cqCS)
BEGIN
  LOCAL cR;
  cR := "";
  IFERR
    cR := STRING(EXPR("CAS(" + CHAR(34) + "string(" + cqCS + ")" + CHAR(34) + ")"));
  THEN
    cR := "";
  END;
  // The answer is a PPL string, and STRING() of it adds one pair of quotes
  // (CASProbe beta 1b T12, emulator): drop that pair.
  IF DIM(cR) >= 2 THEN
    IF LEFT(cR, 1) == CHAR(34) AND MID(cR, DIM(cR), 1) == CHAR(34) THEN
      cR := MID(cR, 2, DIM(cR) - 2);
    END;
  END;
  // undef, an error, or an infinity (CHAR(8734) is the infinity sign; it
  // also catches the Prime's plus-minus infinity, 1/0): no usable answer
  // beta 2b: an empty answer is already a failure; INSTRING("", ...) is
  // "Invalid input" on the Prime (emulator, 17 Oct 2026), so the checks
  // run only on a non-empty answer
  IF DIM(cR) > 0 THEN
    IF INSTRING(cR, "undef") > 0 OR INSTRING(cR, "rror") > 0 OR INSTRING(cR, CHAR(8734)) > 0 THEN cR := ""; END;
  END;
  // (one line added to the kit: the badge flag, set where the answer is judged)
  cqCasOK := 0;
  IF DIM(cR) > 0 THEN cqCasOK := 1; END;
  RETURN cR;
END;

// Every * in a CAS answer shown as the multiplication sign
// (fence rule 7).
cqTimes(tmS)
BEGIN
  LOCAL tmI, tmR, tmC;
  tmR := "";
  FOR tmI FROM 1 TO DIM(tmS) DO
    tmC := MID(tmS, tmI, 1);
    IF tmC == "*" THEN tmC := cqTms; END;
    tmR := tmR + tmC;
  END;
  RETURN tmR;
END;


// ------------------------------------------------------------
// THE EXACT INTERVAL (beta 2)
// cqBinCheck, cqCPB and cqCPInt are the fleet's ONE exact-interval
// algorithm, the same lines as EpiStats v1.7's es_BinCheck, es_CPB
// and es_CPInt (verify_CICatcher_beta2.py compares them).  Checked
// against statsmodels proportion_confint(method='beta').  A numeric
// answer, so PPL computes it; the CAS only writes the closed form
// for 0 or n "yes" answers (BRIEF_CAS rule 8).
// ------------------------------------------------------------

cqBinCheck()
BEGIN
  LOCAL bcV, bcW;
  bcV := -1; bcW := -1;
  IFERR
    bcV := BINOMIAL_CDF(20, 0.1, 2);
    bcW := BINOMIAL(19, 0.1, 2);
  THEN
    bcV := -1;
  END;
  IF ABS(bcV - 0.676926805) < 0.000001 AND ABS(bcW - 0.285179807) < 0.000001 THEN RETURN 1; END;
  RETURN 0;
END;

cqCPB(cbK, cbN, cbA, cbS, cbM, cbG)
BEGIN
  LOCAL cbLo, cbHi, cbP, cbH, cbD, cbQ;
  LOCAL cbC0, cbC1, cbGo, cbI;
  IF cbS == 1 AND cbK <= 0 THEN RETURN 0; END;
  IF cbS == 2 AND cbK >= cbN THEN RETURN 1; END;
  IF cbS == 1 AND cbK >= cbN THEN RETURN ((1 + cbM) * cbA)^(1 / cbN); END;
  IF cbS == 2 AND cbK <= 0 THEN RETURN 1 - ((1 + cbM) * cbA)^(1 / cbN); END;
  cbLo := 0; cbHi := 1;
  cbP := cbG;
  IF cbP <= 0 OR cbP >= 1 THEN cbP := cbK / cbN; END;
  cbGo := 1; cbI := 0;
  WHILE cbGo == 1 DO
    cbI := cbI + 1;
    // the two CDF values this end needs
    cbC0 := 0; cbC1 := 0;
    IF cbS == 2 OR cbM == 1 THEN cbC0 := BINOMIAL_CDF(cbN, cbP, cbK); END;
    IF cbS == 1 OR cbM == 1 THEN cbC1 := BINOMIAL_CDF(cbN, cbP, cbK - 1); END;
    // h(p), rising with p, and its slope cbD
    IF cbM == 0 THEN
      IF cbS == 1 THEN
        cbH := (1 - cbC1) - cbA;                     // P(X >= k) - alpha/2
        cbD := cbN * BINOMIAL(cbN - 1, cbP, cbK - 1);
      ELSE
        cbH := cbA - cbC0;                           // alpha/2 - P(X <= k)
        cbD := cbN * BINOMIAL(cbN - 1, cbP, cbK);
      END;
    ELSE
      cbD := cbN * (BINOMIAL(cbN - 1, cbP, cbK) + BINOMIAL(cbN - 1, cbP, cbK - 1)) / 2;
      IF cbS == 1 THEN
        cbH := (1 - (cbC0 + cbC1) / 2) - cbA;         // P(X > k) + P(X = k)/2 - alpha/2
      ELSE
        cbH := cbA - (cbC0 + cbC1) / 2;               // alpha/2 - P(X < k) - P(X = k)/2
      END;
    END;
    IF cbH == 0 THEN
      cbGo := 0;                                     // exactly on it
    ELSE
      // the end wanted lies between cbLo and cbHi
      IF cbH < 0 THEN cbLo := cbP; ELSE cbHi := cbP; END;
      cbQ := -1;
      IF cbD > 0 THEN cbQ := cbP - cbH / cbD; END;   // Newton's step
      IF cbD > 0 AND ABS(cbQ - cbP) <= 0.000000001 * cbP THEN
        cbGo := 0;                                   // the step is tiny: done
      ELSE
        // a Newton step outside the bracket is replaced by halving it
        IF cbQ <= cbLo OR cbQ >= cbHi THEN cbQ := (cbLo + cbHi) / 2; END;
        IF cbHi - cbLo <= 0.000000001 * cbP THEN cbGo := 0; END;
      END;
      IF cbI >= 100 THEN cbGo := 0; END;             // a guard; never reached in tests
      cbP := cbQ;
    END;
  END;
  RETURN cbP;
END;

cqCPInt(xpK, xpN, xpA, xpM)
BEGIN
  LOCAL wiZ, wiP, wiD, wiC, wiH, wiR;
  IF xpN <= 0 THEN RETURN {0, 1}; END;
  wiZ := NORMALD_ICDF(0, 1, 1 - xpA);
  wiP := xpK / xpN;
  wiD := 1 + wiZ^2 / xpN;
  wiC := (wiP + wiZ^2 / (2 * xpN)) / wiD;
  wiH := wiZ * (wiP * (1 - wiP) / xpN + wiZ^2 / (4 * xpN^2))^0.5 / wiD;
  wiR := {cqCPB(xpK, xpN, xpA, 1, xpM, wiC - wiH), cqCPB(xpK, xpN, xpA, 2, xpM, wiC + wiH)};
  RETURN wiR;
END;


// The catch level of an Exact interval for every possible count
// k = 0..n, once per run (n + 1 values, two BINOMIAL_CDF calls each).
// The interval for k contains the true p at level L exactly when
// both tails under p hold at least alpha/2 = (1000 - L)/2000:
//   min(P(X >= k), P(X <= k)) >= (1000 - L) / 2000,
// i.e. L >= 1000 (1 - 2 min(...)) -- so the catch level is the
// smallest whole thousandth at or above that, kept to 500..1000 as
// for the other intervals.  P(X >= 0) and P(X <= n) are 1.
cqXBuild()
BEGIN
  LOCAL xbK, xbS, xbT, xbM, xbC;
  cqXCat := MAKELIST(1000, K, 1, cqN + 1, 1);
  FOR xbK FROM 0 TO cqN DO
    xbS := 1; xbT := 1;
    IF xbK > 0 THEN xbS := 1 - BINOMIAL_CDF(cqN, cqP, xbK - 1); END;
    IF xbK < cqN THEN xbT := BINOMIAL_CDF(cqN, cqP, xbK); END;
    xbM := MIN(xbS, xbT);
    xbC := CEILING(1000 * (1 - 2 * xbM));
    cqXCat(xbK + 1) := MAX(500, MIN(1000, xbC));
  END;
END;

// The Exact interval {lower, upper} for xgK "yes" answers at the
// current level, from the cache when this count was last worked out
// at this level.  At a new level each end is solved again starting
// from where it was, so a drag of the level costs a few BINOMIAL
// calls per count on screen (about 6 different counts at p = 0.10,
// n = 20), not a full search per line.
cqXGet(xgK)
BEGIN
  LOCAL xgI, xgA, xgW;
  xgI := xgK + 1;
  IF cqXLv(xgI) <> cqL THEN
    xgA := (1000 - cqL) / 2000;
    IF cqXLv(xgI) > 0 THEN
      cqXLo(xgI) := cqCPB(xgK, cqN, xgA, 1, 0, cqXLo(xgI));
      cqXHi(xgI) := cqCPB(xgK, cqN, xgA, 2, 0, cqXHi(xgI));
    ELSE
      xgW := cqCPInt(xgK, cqN, xgA, 0);
      cqXLo(xgI) := xgW(1);
      cqXHi(xgI) := xgW(2);
    END;
    cqXLv(xgI) := cqL;
  END;
  RETURN {cqXLo(xgI), cqXHi(xgI)};
END;

// Which closed form a picked line with 0 or n "yes" answers needs,
// as one number: the end (2 = the upper end, for 0 yeses; 1 = the
// lower end, for n), n and the level.  The CAS text is cached under it.
cqCFKey(ckK)
BEGIN
  LOCAL ckS;
  ckS := 1;
  IF ckK == 0 THEN ckS := 2; END;
  RETURN ckS * 1000000 + cqN * 1000 + cqL;
END;

// The CAS's one job in CICatcher.  Called by the main loop before each
// frame it draws -- an EVENT (a tap, a key, a finger lifted), never
// inside a drag or the +1 animation (fence rule 2).  When the picked
// line is an Exact interval for 0 or n "yes" answers, it asks the CAS
// for the closed form of its end at this n and level, e.g.
//   1-(50/2000)^(1/20)  ->  1-(1/40)^(1/20)
// (the CAS reduces alpha/2 = (1000 - L)/2000 to lowest terms).  The
// plain expression is sent: Giac's simplify() turns many of these
// roots into "Root of ..." or an error.
cqCasPanel()
BEGIN
  LOCAL cpK, cpKey, cpS;
  IF cqType <> 5 OR cqRowOf(cqSel) == 0 THEN RETURN 0; END;
  IF cqSel == cqHead AND cqGrow >= 0 THEN RETURN 0; END;
  cpK := ROUND(cqEst(cqSel) * cqN, 0);
  IF cpK <> 0 AND cpK <> cqN THEN RETURN 0; END;
  cpKey := cqCFKey(cpK);
  IF cpKey == cqCasK THEN RETURN 0; END;
  cpS := "(" + cqIStr(1000 - cqL) + "/2000)^(1/" + cqIStr(cqN) + ")";
  IF cpK == 0 THEN cpS := "1-" + cpS; END;
  cqCasT := cqTimes(cqCas(cpS));
  cqCasK := cpKey;
  RETURN 1;
END;

// The panel's closed-form line for a picked Exact line with 0 or n
// "yes" answers, at y = cqPY: the CAS's form when it answered for this
// n and level, else the formula in words; the PPL's own decimal after
// it, and the badge.
cqCFDraw(cfK)
BEGIN
  LOCAL cfLim, cfT, cfV, cfS;
  cfLim := cqXGet(cfK);
  IF cfK == 0 THEN cfT := "upper = "; cfV := cfLim(2); ELSE cfT := "lower = "; cfV := cfLim(1); END;
  IF cqCFKey(cfK) == cqCasK AND DIM(cqCasT) > 0 AND DIM(cqCasT) <= 28 THEN
    cfS := cfT + cqCasT;
    IF DIM(cqCasT) <= 20 THEN cfS := cfS + " = " + cqF(cfV, 4); END;
    TEXTOUT_P(cfS, G1, 2, cqPY, 1, #000000h, 240);
    TEXTOUT_P("exact (CAS)", G1, 246, cqPY, 1, cqFOOT, 72);
  ELSE
    IF cfK == 0 THEN cfS := "1 - (" + CHAR(945) + "/2)^(1/n)"; ELSE cfS := "(" + CHAR(945) + "/2)^(1/n)"; END;
    TEXTOUT_P(cfT + cfS + " = " + cqF(cfV, 4), G1, 2, cqPY, 1, #000000h, 240);
    TEXTOUT_P("decimal", G1, 246, cqPY, 1, cqFOOT, 72);
  END;
END;

// ------------------------------------------------------------
// THE FIVE INTERVALS
// ------------------------------------------------------------

// The interval from one sample at the current level: returns
// {lower, upper}.  lmE is the estimate (xbar or phat); lmS is the
// sample SD (used only by t).  Uses the CURRENT critical value
// cqCV, so a new level redraws every interval without resampling.
cqLimits(lmE, lmS)
BEGIN
  LOCAL lmH, lmC, lmZ2, lmDen;
  lmC := lmE;
  CASE
    IF cqType == 1 THEN
      // z: the TRUE sigma, so every z interval has the same width
      lmH := cqCV * cqSg / cqN^0.5;
    END;
    IF cqType == 2 THEN
      // t: the sample's own s, so t intervals vary in width
      lmH := cqCV * lmS / cqN^0.5;
    END;
    IF cqType == 3 THEN
      // Wald: centre and width both from phat
      lmH := cqCV * (lmE * (1 - lmE) / cqN)^0.5;
    END;
    IF cqType == 4 THEN
      // Wilson (score): the centre is pulled toward 1/2
      lmZ2 := cqCV * cqCV;
      lmDen := 1 + lmZ2 / cqN;
      lmC := (lmE + lmZ2 / (2 * cqN)) / lmDen;
      lmH := cqCV * (lmE * (1 - lmE) / cqN + lmZ2 / (4 * cqN * cqN))^0.5 / lmDen;
    END;
    DEFAULT
      // beta 2, Exact (Clopper-Pearson): not centre +/- half-width,
      // and no critical value -- the two ends for this count at this
      // level, from the cache (cqXGet)
      RETURN cqXGet(ROUND(lmE * cqN, 0));
  END;
  RETURN {lmC - lmH, lmC + lmH};
END;

// The catch level of one interval: the lowest level, in steps of
// 0.1 % from 50.0 % (500) to 99.9 % (999), at which it contains
// the truth; 1000 if it misses even at 99.9 %.  The interval
// contains the truth at level L exactly when its pivot q is at
// most the critical value, i.e. when 2 CDF(q) - 1 <= L / 1000.
cqCatch(ctE, ctS)
BEGIN
  LOCAL ctQ, ctLv, ctC;
  ctLv := 0;
  CASE
    IF cqType == 1 THEN
      ctQ := ABS(ctE - cqMu) / (cqSg / cqN^0.5);
      ctLv := 2 * NORMALD_CDF(0, 1, ctQ) - 1;
    END;
    IF cqType == 2 THEN
      // s = 0 would need n identical values: not possible with
      // real-valued sampling, but guarded all the same
      IF ctS <= 0 THEN
        ctLv := 2;
        IF ctE == cqMu THEN ctLv := 0; END;
      ELSE
        ctQ := ABS(ctE - cqMu) / (ctS / cqN^0.5);
        ctLv := 2 * STUDENT_CDF(cqDF, ctQ) - 1;
      END;
    END;
    IF cqType == 3 THEN
      // phat = 0 or 1: a Wald interval of zero width, which can
      // never contain a p strictly between 0 and 1
      IF ctE * (1 - ctE) <= 0 THEN
        ctLv := 2;
      ELSE
        ctQ := ABS(ctE - cqP) / (ctE * (1 - ctE) / cqN)^0.5;
        ctLv := 2 * NORMALD_CDF(0, 1, ctQ) - 1;
      END;
    END;
    IF cqType == 4 THEN
      ctQ := ABS(ctE - cqP) / (cqP * (1 - cqP) / cqN)^0.5;
      ctLv := 2 * NORMALD_CDF(0, 1, ctQ) - 1;
    END;
    DEFAULT
      // beta 2, Exact: the same for every sample with this count, so
      // it comes from the table cqXBuild made for this run
      RETURN cqXCat(ROUND(ctE * cqN, 0) + 1);
  END;
  ctC := CEILING(1000 * ctLv);
  RETURN MAX(500, MIN(1000, ctC));
END;

// Move to level seL (thousandths).  The long-run count follows
// exactly: intervals whose catch level lies between the old and
// the new level change colour; the tally says how many there are.
cqSetLevel(seL)
BEGIN
  LOCAL seI;
  IF seL > cqL THEN
    FOR seI FROM cqL + 1 TO seL DO cqHit := cqHit + cqHist(seI - 499); END;
  END;
  IF seL < cqL THEN
    FOR seI FROM seL + 1 TO cqL DO cqHit := cqHit - cqHist(seI - 499); END;
  END;
  cqL := seL;
  cqCV := cqCrit(cqL);
END;

// How many of the visible intervals catch the truth now.  The
// visible slots are simply slots 1..cqVis (the ring fills from
// slot 1 and only then wraps), so no row arithmetic is needed.
cqVisHits()
BEGIN
  LOCAL vhS, vhN;
  vhN := 0;
  FOR vhS FROM 1 TO cqVis DO
    IF cqCat(vhS) <= cqL THEN vhN := vhN + 1; END;
  END;
  RETURN vhN;
END;


// ------------------------------------------------------------
// THE WINDOW, THE RUN, THE SAMPLES
// ------------------------------------------------------------

// Freeze the x window for this run: the truth +/- 6.5 standard
// errors.  It does not move with the level, so a higher level
// visibly LENGTHENS the lines (a t line at 99.9 % with a small n
// can still run off the edge: it is cut there and notched).
// A proportion's window may reach below 0 or above 1 (to -0.2
// and 1.2 at most); those zones are shaded as impossible values.
cqWindow()
BEGIN
  LOCAL wnSE, wnC, wnW;
  IF cqType <= 2 THEN
    wnSE := cqSg / cqN^0.5;
    wnC := cqMu;
  ELSE
    wnSE := (cqP * (1 - cqP) / cqN)^0.5;
    wnC := cqP;
  END;
  wnW := 6.5 * wnSE;
  cqWL := wnC - wnW;
  cqWR := wnC + wnW;
  IF cqType >= 3 THEN
    cqWL := MAX(-0.2, cqWL);
    cqWR := MIN(1.2, cqWR);
  END;
  cqSX := (cqPR - cqPL) / (cqWR - cqWL);
  cqStp := cqNice((cqWR - cqWL) / 8);
  cqTDec := MAX(0, -FLOOR(LOG(cqStp)));
  // panel decimals from the standard error: SE 4.7 -> 1 decimal,
  // a proportion's SE 0.067 -> 2, SE 0.007 -> 3
  IF cqType <= 2 THEN
    cqVDec := MAX(0, MIN(4, 1 - FLOOR(LOG(wnSE))));
  ELSE
    cqVDec := MAX(2, MIN(4, -FLOOR(LOG(wnSE))));
  END;
END;

// Copy the long-run tally into the Home exports.
cqExport()
BEGIN
  CQ_TOTAL := cqTot;
  CQ_HITS := cqHit;
  IF cqTot > 0 THEN
    CQ_COVER := cqHit / cqTot;
  ELSE
    CQ_COVER := 0;
  END;
END;

// A fresh run: no samples, no tally, new caches, a new window.
// n at most 100 keeps the raw-value list at 50 x 100 = 5,000
// values, well inside the Prime's list size.
cqReset()
BEGIN
  cqDF := cqN - 1;
  cqEst := MAKELIST(0, K, 1, 50, 1);
  cqSd := MAKELIST(0, K, 1, 50, 1);
  cqCat := MAKELIST(1000, K, 1, 50, 1);
  cqRaw := MAKELIST(0, K, 1, 50 * cqN, 1);
  cqHist := MAKELIST(0, K, 1, 501, 1);
  cqCVc := MAKELIST(-1, K, 1, 500, 1);
  // beta 2: the Exact interval's tables, one entry per count 0..n
  cqXLo := MAKELIST(-1, K, 1, cqN + 1, 1);
  cqXHi := MAKELIST(-1, K, 1, cqN + 1, 1);
  cqXLv := MAKELIST(-1, K, 1, cqN + 1, 1);
  cqXCat := MAKELIST(1000, K, 1, cqN + 1, 1);
  IF cqType == 5 THEN cqXBuild(); END;
  cqCasK := -1;
  cqHead := 0; cqVis := 0; cqSel := 0;
  cqTot := 0; cqHit := 0;
  cqOff := 0; cqGrow := -1;
  cqCV := cqCrit(cqL);
  cqWindow();
  cqExport();
END;

// Draw one sample of n into slot spS, keep its raw values, its
// estimate and SD and its catch level, and add it to the tally.
// Normal values: Marsaglia's polar method (as NormExplorer beta
// 40's ne_SampBins) -- two normals per accepted point, no sine or
// cosine, so the angle mode cannot spoil it.
cqSample(spS)
BEGIN
  LOCAL spB, spMade, spV1, spV2, spW, spF;
  LOCAL spVal, spSum, spM, spSS, spD, spI;
  LOCAL spCnt, spEst, spSD, spC;
  spB := (spS - 1) * cqN;              // where this slot's values start
  IF cqType <= 2 THEN
    spSum := 0;
    spMade := 0;
    WHILE spMade < cqN DO
      // a random point in the unit disc (not the centre)
      REPEAT
        spV1 := 2 * RANDOM - 1;
        spV2 := 2 * RANDOM - 1;
        spW := spV1 * spV1 + spV2 * spV2;
      UNTIL spW > 0 AND spW < 1;
      spF := (-2 * LN(spW) / spW)^0.5;
      // first of the pair
      spMade := spMade + 1;
      spVal := cqMu + cqSg * spV1 * spF;
      cqRaw(spB + spMade) := spVal;
      spSum := spSum + spVal;
      // second of the pair, only if it is still needed (odd n)
      IF spMade < cqN THEN
        spMade := spMade + 1;
        spVal := cqMu + cqSg * spV2 * spF;
        cqRaw(spB + spMade) := spVal;
        spSum := spSum + spVal;
      END;
    END;
    spM := spSum / cqN;
    // the sample SD by two passes over the stored values (no
    // cancellation trouble), with the n - 1 divisor
    spSS := 0;
    FOR spI FROM 1 TO cqN DO
      spD := cqRaw(spB + spI) - spM;
      spSS := spSS + spD * spD;
    END;
    spEst := spM;
    spSD := (spSS / (cqN - 1))^0.5;
  ELSE
    // n yes/no answers: 1 with chance p
    spCnt := 0;
    FOR spI FROM 1 TO cqN DO
      IF RANDOM < cqP THEN
        cqRaw(spB + spI) := 1;
        spCnt := spCnt + 1;
      ELSE
        cqRaw(spB + spI) := 0;
      END;
    END;
    spEst := spCnt / cqN;
    spSD := 0;
  END;
  cqEst(spS) := spEst;
  cqSd(spS) := spSD;
  spC := cqCatch(spEst, spSD);
  cqCat(spS) := spC;
  // the long-run tally
  cqTot := cqTot + 1;
  cqHist(spC - 499) := cqHist(spC - 499) + 1;
  IF spC <= cqL THEN cqHit := cqHit + 1; END;
END;

// One more sample into the ring: the newest goes on top, and once
// 50 are showing it replaces the oldest.
cqAdd1()
BEGIN
  cqHead := cqHead + 1;
  IF cqHead > 50 THEN cqHead := 1; END;
  IF cqVis < 50 THEN cqVis := cqVis + 1; END;
  IF cqSel == cqHead THEN cqSel := 0; END;   // its line is being replaced
  cqSample(cqHead);
END;


// ------------------------------------------------------------
// DRAWING (every frame composed in G1, then one blit)
// ------------------------------------------------------------

// What is being sampled, for the header and the summary.
cqDesc()
BEGIN
  IF cqType == 1 THEN
    RETURN "z, " + cqMuS + " = " + cqNumS(cqMu) + ", " + cqSgS + " = " + cqNumS(cqSg) + ", n = " + STRING(cqN, 2, 0);
  END;
  IF cqType == 2 THEN
    RETURN "t, " + cqMuS + " = " + cqNumS(cqMu) + ", " + cqSgS + " = " + cqNumS(cqSg) + ", n = " + STRING(cqN, 2, 0);
  END;
  IF cqType == 3 THEN
    RETURN "Wald, p = " + cqNumS(cqP) + ", n = " + STRING(cqN, 2, 0);
  END;
  IF cqType == 5 THEN
    RETURN "Exact, p = " + cqNumS(cqP) + ", n = " + STRING(cqN, 2, 0);
  END;
  RETURN "Wilson, p = " + cqNumS(cqP) + ", n = " + STRING(cqN, 2, 0);
END;

// y on the slider track of level syL.  The track is spaced by the
// critical value z*, not by the percentage, so equal finger moves
// make equal changes in interval length (50 % at the bottom,
// 80 % about a quarter of the way up, 99.9 % at the top).
cqSliderY(syL)
BEGIN
  LOCAL syZ;
  syZ := NORMALD_ICDF(0, 1, 0.5 + syL / 2000);
  RETURN IP(cqSB - (syZ - cqZlo) / (cqZhi - cqZlo) * (cqSB - cqST) + 0.5);
END;

// The level (thousandths) under a finger at height slY.  Within
// 0.3 % of 80, 90, 95 or 99 % it snaps to that level, so a finger
// can land on the usual levels exactly (a pixel is worth about
// 0.2 % near 95 %).
cqSliderL(slY)
BEGIN
  LOCAL slF, slZ, slL, slI, slD;
  slF := (cqSB - slY) / (cqSB - cqST);
  slF := MAX(0, MIN(1, slF));
  slZ := cqZlo + slF * (cqZhi - cqZlo);
  slL := ROUND(1000 * (2 * NORMALD_CDF(0, 1, slZ) - 1), 0);
  slD := {800, 900, 950, 990};
  FOR slI FROM 1 TO 4 DO
    IF ABS(slL - slD(slI)) <= 3 THEN slL := slD(slI); END;
  END;
  RETURN MAX(500, MIN(999, slL));
END;

// The two header lines.  While the newest interval is still being
// drawn (cqGrow >= 0) it is left out of the counts, so the header
// does not give its colour away before the line does.
cqDrawHead()
BEGIN
  LOCAL dhV, dhH, dhT, dhA, dhS;
  dhV := cqVis;
  dhH := cqVisHits();
  dhT := cqTot;
  dhA := cqHit;
  IF cqGrow >= 0 AND cqVis > 0 THEN
    dhV := dhV - 1;
    dhT := dhT - 1;
    IF cqCat(cqHead) <= cqL THEN dhH := dhH - 1; dhA := dhA - 1; END;
  END;
  IF dhT <= 0 THEN
    IF cqGrow >= 0 THEN
      dhS := "the first sample ...";
    ELSE
      dhS := "no intervals yet: tap +1, +10 or +50";
    END;
    TEXTOUT_P(dhS, G1, 2, 1, 1, cqBLUE, 294);
  ELSE
    dhS := "caught " + STRING(dhH, 2, 0) + " of " + STRING(dhV, 2, 0) + " (" + cqPct(dhH, dhV) + " %),";
    TEXTOUT_P(dhS, G1, 2, 1, 1, cqBLUE, 166);
    dhS := "all " + STRING(dhT, 2, 0) + ": " + cqPct(dhA, dhT) + " %";
    TEXTOUT_P(dhS, G1, 172, 1, 1, #000000h, 124);
  END;
  // line 2: the promise (magenta, like the slider knob) and the
  // truth with the method (green, like the truth line)
  TEXTOUT_P("target " + cqLvS(cqL) + " %", G1, 2, 14, 1, cqMAG, 86);
  TEXTOUT_P(cqDesc(), G1, 92, 14, 1, cqGRN, 204);
END;

// Grey impossible zones (proportions), grid lines, tick marks and
// labels, the axis, and the green truth line.
cqDrawAxis()
BEGIN
  LOCAL daK, daK0, daK1, daTV, daX, daS;
  IF cqType >= 3 THEN
    IF cqWL < 0 THEN
      daX := cqPXi(0);
      IF daX - 1 >= cqPL THEN RECT_P(G1, cqPL, cqPT, daX - 1, cqPB, cqLGRY, cqLGRY); END;
    END;
    IF cqWR > 1 THEN
      daX := cqPXi(1);
      IF daX + 1 <= cqPR THEN RECT_P(G1, daX + 1, cqPT, cqPR, cqPB, cqLGRY, cqLGRY); END;
    END;
  END;
  // ticks at whole multiples of the step: k * step, so 0 is 0
  daK0 := CEILING(cqWL / cqStp - 0.000001);
  daK1 := FLOOR(cqWR / cqStp + 0.000001);
  FOR daK FROM daK0 TO daK1 DO
    daTV := daK * cqStp;
    daX := cqPXi(daTV);
    IF daX >= cqPL AND daX <= cqPR THEN
      LINE_P(G1, daX, cqPT, daX, cqPB, cqGRID);
      LINE_P(G1, daX, cqAY, daX, cqAY + 3, cqGRAY);
      daS := STRING(daTV, 2, cqTDec);
      TEXTOUT_P(daS, G1, MAX(0, daX - 3 * DIM(daS)), cqAY + 4, 1, cqGRAY);
    END;
  END;
  LINE_P(G1, cqPL, cqAY, cqPR, cqAY, #000000h);
  // the truth: two pixels wide, from the top of the stack to the axis
  daX := cqPXi(cqTruth());
  LINE_P(G1, daX, cqPT, daX, cqAY - 1, cqGRN);
  LINE_P(G1, daX + 1, cqPT, daX + 1, cqAY - 1, cqGRN);
END;

// The stack of intervals, newest on top.  The picked one sits on
// a light orange band and is drawn three pixels thick.
cqDrawRows()
BEGIN
  LOCAL drR, drS, drY, drLim, drLo, drHi;
  LOCAL drE, drXL, drXH, drXE, drCol, drA;
  LOCAL drB, drSR;
  // the band behind the picked line (drawn first, under the rest)
  drSR := cqRowOf(cqSel);
  IF drSR > 0 AND (drSR > 1 OR cqGrow <> 0) THEN
    drY := cqRowY(drSR);
    IF drSR > 1 THEN drY := drY + cqOff; END;
    RECT_P(G1, cqPL, drY - 1, cqPR, drY + 1, cqLORN, cqLORN);
  END;
  // (cqSlot, cqRowY and cqPXi are written out inline in this loop:
  // it runs 50 times a frame during a drag, and a routine call
  // costs more than the arithmetic on a G1)
  FOR drR FROM 1 TO cqVis DO
    // row 1 is hidden while the stack slides down (cqGrow = 0)
    IF drR > 1 OR cqGrow <> 0 THEN
      drS := cqHead - drR + 1;
      IF drS < 1 THEN drS := drS + 50; END;
      drY := cqRY0 + 3 * (drR - 1);
      IF drR > 1 THEN drY := drY + cqOff; END;
      drE := cqEst(drS);
      drLim := cqLimits(drE, cqSd(drS));
      drLo := drLim(1);
      drHi := drLim(2);
      IF cqCat(drS) <= cqL THEN drCol := cqBLUE; ELSE drCol := cqRED; END;
      IF drR == 1 AND cqGrow > 0 THEN
        // the newest interval growing out of its estimate, in grey
        drLo := drE - cqGrow * (drE - drLo);
        drHi := drE + cqGrow * (drHi - drE);
        drCol := cqGRAY;
      END;
      drXL := IP(cqPL + (drLo - cqWL) * cqSX + 0.5);
      drXH := IP(cqPL + (drHi - cqWL) * cqSX + 0.5);
      // the line, cut to the plot box
      drA := MAX(cqPL, drXL);
      drB := MIN(cqPR, drXH);
      IF drB >= drA THEN
        LINE_P(G1, drA, drY, drB, drY, drCol);
        IF drS == cqSel THEN
          LINE_P(G1, drA, drY - 1, drB, drY - 1, drCol);
          LINE_P(G1, drA, drY + 1, drB, drY + 1, drCol);
        END;
      END;
      // a notch where a line runs off the window
      IF drXL < cqPL THEN LINE_P(G1, cqPL, drY - 1, cqPL, drY + 1, drCol); END;
      IF drXH > cqPR THEN LINE_P(G1, cqPR, drY - 1, cqPR, drY + 1, drCol); END;
      // the estimate: a 3 x 3 dot, kept inside the box
      drXE := MAX(cqPL + 1, MIN(cqPR - 1, IP(cqPL + (drE - cqWL) * cqSX + 0.5)));
      RECT_P(G1, drXE - 1, drY - 1, drXE + 1, drY + 1, drCol, drCol);
    END;
  END;
END;

// The level slider at the right edge: a grey track spaced by z*,
// ticks at 50, 80, 90, 95 and 99 %, and the magenta knob on the
// track (narrow, so the tick labels beside it stay readable).
cqDrawSlider()
BEGIN
  LOCAL dsI, dsY, dsK;
  LINE_P(G1, 315, cqST, 315, cqSB, cqGRAY);
  FOR dsI FROM 1 TO 5 DO
    dsY := cqSYt(dsI);
    LINE_P(G1, 311, dsY, 319, dsY, cqGRAY);
    TEXTOUT_P(cqSLt(dsI), G1, 297, dsY - 5, 1, cqGRAY, 13);
  END;
  dsK := cqSliderY(cqL);
  RECT_P(G1, 310, dsK - 3, 319, dsK + 3, cqMAG, cqMAG);
END;

// The bottom panel.  With nothing picked it holds the key legend.
// With a line picked: that sample's n values as dots on the same
// axis (a mean), or as n yes/no tiles (a proportion); the truth
// and the interval under the dots; and one line of numbers.
cqDrawPanel()
BEGIN
  LOCAL pnS, pnB, pnI, pnX, pnCol, pnFill;
  LOCAL pnLim, pnTxt, pnGrw, pnTP, pnTW, pnD;
  LOCAL pnE, pnA, pnZ, pnDD, pnK, pnCF;
  IF cqRowOf(cqSel) == 0 THEN
    TEXTOUT_P("tap line = its sample, drag edge = level", G1, 2, cqPY, 1, cqFOOT, 316);
    TEXTOUT_P(cqUD + " = level, " + cqLR + " = pick line, Ent = menu, Esc = quit", G1, 2, cqPY + 12, 1, cqFOOT, 316);
    RETURN;
  END;
  pnS := cqSel;
  pnB := (pnS - 1) * cqN;
  pnE := cqEst(pnS);
  // still being drawn by the +1 animation: no verdict yet
  pnGrw := 0;
  IF pnS == cqHead AND cqGrow >= 0 THEN pnGrw := 1; END;
  // beta 2: an Exact interval for 0 or n "yes" answers gets its closed
  // form on the panel's first line, in place of the tiles (which would
  // all be one colour anyway)
  pnCF := 0;
  IF cqType == 5 AND pnGrw == 0 THEN
    pnK := ROUND(pnE * cqN, 0);
    IF pnK == 0 OR pnK == cqN THEN pnCF := 1; END;
  END;
  IF pnGrw == 1 THEN
    pnCol := cqGRAY; pnFill := #F0F0F0h;
  ELSE
    IF cqCat(pnS) <= cqL THEN
      pnCol := cqBLUE; pnFill := cqLBLU;
    ELSE
      pnCol := cqRED; pnFill := cqLRED;
    END;
  END;
  RECT_P(G1, 0, cqPY - 1, 319, 217, pnCol, pnFill);
  pnLim := cqLimits(pnE, cqSd(pnS));
  IF cqType <= 2 THEN
    // ---- a mean: the n values as black dots on the plot's axis;
    // a value off the window is pulled to the edge and greyed ----
    FOR pnI FROM 1 TO cqN DO
      pnX := cqPXi(cqRaw(pnB + pnI));
      IF pnX < cqPL OR pnX > cqPR THEN
        pnX := MAX(cqPL, MIN(cqPR, pnX));
        RECT_P(G1, pnX - 1, cqPY + 2, pnX + 1, cqPY + 4, cqGRAY, cqGRAY);
      ELSE
        RECT_P(G1, pnX - 1, cqPY + 2, pnX + 1, cqPY + 4, #000000h, #000000h);
      END;
    END;
    // the truth (green) and this interval with its xbar under them
    pnX := cqPXi(cqMu);
    LINE_P(G1, pnX, cqPY, pnX, cqPY + 8, cqGRN);
    LINE_P(G1, pnX + 1, cqPY, pnX + 1, cqPY + 8, cqGRN);
    pnA := pnLim(1);
    pnZ := pnLim(2);
    IF pnGrw == 1 THEN
      pnA := pnE - MAX(0, cqGrow) * (pnE - pnA);
      pnZ := pnE + MAX(0, cqGrow) * (pnZ - pnE);
    END;
    pnA := MAX(cqPL, cqPXi(pnA));
    pnZ := MIN(cqPR, cqPXi(pnZ));
    IF pnZ >= pnA THEN LINE_P(G1, pnA, cqPY + 6, pnZ, cqPY + 6, pnCol); END;
    pnX := MAX(cqPL + 1, MIN(cqPR - 1, cqPXi(pnE)));
    RECT_P(G1, pnX - 1, cqPY + 4, pnX + 1, cqPY + 8, pnCol, pnCol);
  ELSE
    IF pnCF == 1 THEN
      cqCFDraw(pnK);                   // beta 2: the closed form
    ELSE
      // ---- a proportion: one tile per answer, black = yes ----
      pnTP := MAX(1, MIN(8, FLOOR(288 / cqN)));
      pnTW := MAX(1, pnTP - 1);
      FOR pnI FROM 1 TO cqN DO
        pnX := 6 + (pnI - 1) * pnTP;
        IF cqRaw(pnB + pnI) == 1 THEN
          RECT_P(G1, pnX, cqPY + 2, pnX + pnTW - 1, cqPY + 7, #000000h, #000000h);
        ELSE
          RECT_P(G1, pnX, cqPY + 2, pnX + pnTW - 1, cqPY + 7, cqGRAY, #FFFFFFh);
        END;
      END;
    END;
  END;
  // ---- the numbers ----
  IF cqType <= 2 THEN
    pnTxt := cqXb + " = " + cqF(pnE, cqVDec);
    IF cqType == 2 THEN pnTxt := pnTxt + ", s = " + cqF(cqSd(pnS), cqVDec); END;
  ELSE
    pnK := ROUND(pnE * cqN, 0);                 // how many said yes
    pnTxt := cqPh + " = " + STRING(pnK, 2, 0) + "/" + STRING(cqN, 2, 0) + " = " + cqF(pnE, cqVDec);
  END;
  pnTxt := pnTxt + ": " + cqF(pnLim(1), cqVDec) + " to " + cqF(pnLim(2), cqVDec);
  IF pnGrw == 0 THEN
    IF cqCat(pnS) <= cqL THEN
      pnTxt := pnTxt + ", caught";
    ELSE
      // how far the nearer end is from the truth; more decimals if
      // it would round to zero
      pnD := MAX(pnLim(1) - cqTruth(), cqTruth() - pnLim(2));
      pnDD := cqVDec;
      WHILE ROUND(pnD, pnDD) == 0 AND pnDD < 6 DO pnDD := pnDD + 1; END;
      pnTxt := pnTxt + ", missed by " + cqF(pnD, pnDD);
    END;
  END;
  IF pnCF == 1 THEN
    TEXTOUT_P(pnTxt, G1, 2, cqPY + 12, 1, pnCol, 316);   // under the closed form
  ELSE
    TEXTOUT_P(pnTxt, G1, 2, cqPY + 10, 1, pnCol, 316);
  END;
END;

// One whole frame: compose in G1, blit once, then the soft keys
// (DRAWMENU paints the screen, so it comes after the blit, every
// frame).
cqDraw()
BEGIN
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  cqDrawHead();
  cqDrawAxis();
  cqDrawRows();
  cqDrawSlider();
  cqDrawPanel();
  BLIT_P(G0, 0, 0, G1);
  DRAWMENU({"+1", "+10", "+50", "Conf", "n", "Menu"});
END;


// ------------------------------------------------------------
// ADDING SAMPLES
// ------------------------------------------------------------

// afN samples at once, no animation.  A box says what is going on
// (50 samples of 100 take a few seconds on a G1).
cqAddFast(afN)
BEGIN
  LOCAL afI;
  cqSel := 0;
  FOR afI FROM 1 TO afN DO
    IF afI MOD 10 == 1 THEN
      RECT_P(G1, 60, 96, 260, 136, cqORNG, cqLORN);
      TEXTOUT_P("Sampling " + STRING(afI - 1, 2, 0) + " of " + STRING(afN, 2, 0) + " ...", G1, 76, 108, 2, cqORNG, 176);
      BLIT_P(G0, 0, 0, G1);
      DRAWMENU({"+1", "+10", "+50", "Conf", "n", "Menu"});
    END;
    cqAdd1();
  END;
  cqExport();
END;

// One sample, animated: the stack slides down one place, the new
// sample's values appear in the panel, its interval grows grey out
// of its estimate, and only then does it turn blue or red.
cqAddAnim()
BEGIN
  LOCAL anF;
  cqAdd1();
  cqSel := cqHead;                    // the panel shows the new sample
  FOR anF FROM 1 TO 3 DO
    cqOff := anF - 3;                  // -2, -1, 0 pixels
    cqGrow := 0;
    cqDraw();
    cqPause(40);
  END;
  FOR anF FROM 1 TO 5 DO
    cqGrow := anF / 5;
    cqDraw();
    cqPause(40);
  END;
  cqOff := 0;
  cqGrow := -1;                       // the next frame shows the verdict
  cqExport();
END;


// ------------------------------------------------------------
// THE LEVEL AND THE PICKED LINE
// ------------------------------------------------------------

// Up / Down: the next whole percent, and 99 -> 99.5 -> 99.9 at
// the top.
cqStepLevel(stD)
BEGIN
  LOCAL stL;
  stL := cqL;
  IF stD > 0 THEN
    IF stL >= 995 THEN
      stL := 999;
    ELSE
      IF stL >= 990 THEN stL := 995; ELSE stL := (FLOOR(stL / 10) + 1) * 10; END;
    END;
  ELSE
    IF stL > 995 THEN
      stL := 995;
    ELSE
      IF stL > 990 THEN stL := 990; ELSE stL := (CEILING(stL / 10) - 1) * 10; END;
    END;
  END;
  cqSetLevel(MAX(500, MIN(999, stL)));
END;

// Left / Right: pick the next line up (newer) or down (older).
cqStepSel(ssD)
BEGIN
  LOCAL ssR;
  IF cqVis == 0 THEN RETURN; END;
  ssR := cqRowOf(cqSel);
  IF ssR == 0 THEN
    ssR := 1;
  ELSE
    ssR := MAX(1, MIN(cqVis, ssR + ssD));
  END;
  cqSel := cqSlot(ssR);
END;

// THE SHOWCASE: drag the level.  The finger went down on the
// slider at height dlY.  While it stays down, every MOUSE sample
// sets the level; every visible interval is recomputed from its
// stored estimate and SD (no resampling) and the frame redrawn,
// so the lines visibly lengthen or shrink, red turns blue, and
// both counts in the header follow.  The window stays frozen.
// A key press, a lift, or a reading off the screen (a phantom
// touch) ends the drag.
cqDragLevel(dlY)
BEGIN
  LOCAL dlK, dlM, dlM1, dlGo, dlL;
  cqSetLevel(cqSliderL(dlY));
  cqDraw();
  dlGo := 1;
  WHILE dlGo == 1 DO
    dlK := GETKEY;
    dlM := MOUSE;
    dlM1 := dlM(1);
    IF dlK > -1 OR SIZE(dlM1) == 0 THEN
      dlGo := 0;
    ELSE
      IF dlM1(1) < 0 OR dlM1(1) > 319 OR dlM1(2) < 0 OR dlM1(2) > 239 THEN
        dlGo := 0;
      ELSE
        dlL := cqSliderL(dlM1(2));
        IF dlL <> cqL THEN
          cqSetLevel(dlL);
          cqDraw();
        END;
      END;
    END;
  END;
  cqDrain();
  REPEAT dlK := GETKEY; UNTIL dlK == -1;
END;

// Touch on the stack at height rwY: pick the line under the
// finger; slide up and down to browse the samples one by one.
// A tap (no slide of more than 8 px) on the line that was already
// picked puts the panel away.
cqDragRows(rwY)
BEGIN
  LOCAL rwK, rwM, rwM1, rwGo, rwOld, rwMv;
  LOCAL rwS;
  IF cqVis == 0 THEN
    cqDrain();
    RETURN;
  END;
  rwOld := cqSel;
  rwMv := 0;
  cqSel := cqSlot(cqRowAt(rwY));
  rwS := cqSel;
  cqDraw();
  rwGo := 1;
  WHILE rwGo == 1 DO
    rwK := GETKEY;
    rwM := MOUSE;
    rwM1 := rwM(1);
    IF rwK > -1 OR SIZE(rwM1) == 0 THEN
      rwGo := 0;
    ELSE
      IF rwM1(1) < 0 OR rwM1(1) > 319 OR rwM1(2) < 0 OR rwM1(2) > 239 THEN
        rwGo := 0;
      ELSE
        IF ABS(rwM1(2) - rwY) > 8 THEN rwMv := 1; END;
        IF cqSlot(cqRowAt(rwM1(2))) <> cqSel THEN
          cqSel := cqSlot(cqRowAt(rwM1(2)));
          cqDraw();
        END;
      END;
    END;
  END;
  IF rwMv == 0 AND rwS == rwOld THEN cqSel := 0; END;
  cqDrain();
  REPEAT rwK := GETKEY; UNTIL rwK == -1;
END;


// ------------------------------------------------------------
// ASKING THE USER
// ------------------------------------------------------------

// Conf: the four usual levels.
cqAskConf()
BEGIN
  LOCAL acI, acL;
  acL := {800, 900, 950, 990};
  acI := 0;
  CHOOSE(acI, "Confidence level", {
    "80 % confidence",
    "90 % confidence",
    "95 % confidence (the usual)",
    "99 % confidence"});
  IF acI > 0 THEN cqSetLevel(acL(acI)); END;
END;

// n: a new sample size starts a fresh run.  A different n gives a
// different standard error, so the long-run count would otherwise
// mix two methods; one run = one method.
cqAskN()
BEGIN
  LOCAL nkN, nkLo, nkH, nkOK;
  nkN := cqN;
  IF cqType <= 2 THEN nkLo := 2; ELSE nkLo := 5; END;
  nkH := "Whole number, " + STRING(nkLo, 2, 0) + " to 100; starts a fresh run";
  nkOK := INPUT({nkN}, "Sample size", {"n:"}, {nkH});
  IF nkOK == 1 THEN
    IF FP(nkN) <> 0 OR nkN < nkLo OR nkN > 100 THEN
      MSGBOX("n must be a whole number from " + STRING(nkLo, 2, 0) + " to 100.");
      RETURN;
    END;
    IF nkN <> cqN THEN
      cqN := nkN;
      IF cqType <= 2 THEN cqNm := nkN; ELSE cqNp := nkN; END;
      cqReset();
    END;
  END;
END;

// The setup: which interval (CHOOSE), then the population, n and
// the level (INPUT).  Returns 1 when a new run was set up, 0 if
// the user cancelled (the old run, if any, is kept).  A bad value
// gets a message saying why, and the form comes back.
cqSetup()
BEGIN
  LOCAL suTy, suTr, suSD, suNn, suLv, suOK;
  LOCAL suL1, suL2, suL3, suL4, suMsg, suLL;
  LOCAL suH1, suH2, suH3, suH4, suGo;
  suTy := 0;
  // beta 2b: "Proportion: Exact (Clopper-Pearson)" became "Proportion:
  // Exact (C-P)" -- CHOOSE cut it at the right edge of its box on the
  // emulator.  (Comments go here, never inside the list: a comment
  // inside an open bracket is a Syntax Error on the Prime.)
  CHOOSE(suTy, "Which interval?", {
    "Mean, " + cqSgS + " known (z)",
    "Mean, " + cqSgS + " unknown (t)",
    "Proportion: Wald (textbook)",
    "Proportion: Wilson (score)",
    "Proportion: Exact (C-P)"});
  IF suTy == 0 THEN RETURN 0; END;
  // beta 2: Exact needs BINOMIAL_CDF and BINOMIAL to have passed the
  // check at start; otherwise it would draw wrong intervals
  IF suTy == 5 AND cqBinOK <> 1 THEN
    MSGBOX("Exact needs BINOMIAL_CDF, which failed its check at start: Exact is off.");
    RETURN 0;
  END;
  suLv := cqL / 1000;
  suSD := cqSg;
  suL2 := cqSgS + ":";
  suL3 := "n:";
  suL4 := "Level:";
  suH2 := "The true SD, greater than 0";
  suH4 := "0.95 (or 95 for 95 %), from 0.50 to 0.999";
  IF suTy <= 2 THEN
    suTr := cqMu; suNn := cqNm;
    suL1 := cqMuS + ":";
    suH1 := "The true mean: the green line";
    suH3 := "Sample size, a whole number 2 to 100";
  ELSE
    suTr := cqP; suNn := cqNp;
    suL1 := "p:";
    suH1 := "The true proportion, 0.01 to 0.99";
    suH3 := "Sample size, a whole number 5 to 100";
  END;
  suGo := 1;
  WHILE suGo == 1 DO
    IF suTy <= 2 THEN
      suOK := INPUT({suTr, suSD, suNn, suLv}, "Population and sample", {suL1, suL2, suL3, suL4}, {suH1, suH2, suH3, suH4});
    ELSE
      suOK := INPUT({suTr, suNn, suLv}, "Population and sample", {suL1, suL3, suL4}, {suH1, suH3, suH4});
    END;
    IF suOK <> 1 THEN RETURN 0; END;
    suMsg := "";
    IF suLv > 1 THEN suLv := suLv / 100; END;       // typed as a percent
    suLL := ROUND(suLv * 1000, 0);
    IF suLL < 500 OR suLL > 999 THEN
      suMsg := "The level must be from 0.50 to 0.999 (50 % to 99.9 %).";
    END;
    IF suTy <= 2 THEN
      IF suSD <= 0 THEN
        suMsg := cqSgS + " must be greater than 0: it is the spread of the population.";
      END;
      IF FP(suNn) <> 0 OR suNn < 2 OR suNn > 100 THEN
        suMsg := "n must be a whole number from 2 to 100 (a t interval needs 2 or more).";
      END;
    ELSE
      IF suTr < 0.01 OR suTr > 0.99 THEN
        suMsg := "p must be from 0.01 to 0.99.";
      END;
      IF FP(suNn) <> 0 OR suNn < 5 OR suNn > 100 THEN
        suMsg := "n must be a whole number from 5 to 100.";
      END;
    END;
    IF suMsg == "" THEN suGo := 0; ELSE MSGBOX(suMsg); END;
  END;
  cqType := suTy;
  IF suTy <= 2 THEN
    cqMu := suTr; cqSg := suSD; cqNm := suNn;
  ELSE
    cqP := suTr; cqNp := suNn;
  END;
  cqN := suNn;
  cqL := suLL;
  cqReset();
  RETURN 1;
END;


// ------------------------------------------------------------
// HELP PAGES (drawn: a MSGBOX shows only about five lines)
// ------------------------------------------------------------

// One page: a title, up to 14 lines of font 1 (46 characters at
// most) in the colours given, and the footer.  Returns the key
// that closed it (-1 for a tap).
cqPage(pgT, pgL, pgC, pgF)
BEGIN
  LOCAL pgI;
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, cqBLUE, cqLBLU);
  TEXTOUT_P(pgT, G1, 12, 8, 2, cqBLUE, 296);
  FOR pgI FROM 1 TO SIZE(pgL) DO
    TEXTOUT_P(pgL(pgI), G1, 12, 28 + 13 * (pgI - 1), 1, pgC(pgI), 296);
  END;
  TEXTOUT_P(pgF, G1, 12, 220, 1, cqFOOT, 296);
  BLIT_P(G0, 0, 0, G1);
  RETURN cqWaitAny();
END;

// Which code produced t* (the engine note).
cqEngS()
BEGIN
  IF cqEng == 1 THEN RETURN "t critical value: STUDENT_ICDF (built-in)"; END;
  IF cqEng == 2 THEN RETURN "t critical value: bisection on STUDENT_CDF"; END;
  RETURN "t critical value: not needed yet (no t run)";
END;

// beta 2: what the exact interval runs on (the help page's last line).
// beta 2c: "..., checked" instead of "... (checked)" -- the old line
// was 47 characters, one more than a help page's 296-px width holds
// at about 6.5 px a character (fleet review 19 Oct 2026, C-C2).  The
// meaning is the same: BINOMIAL_CDF passed cqBinCheck at start.
cqXEngS()
BEGIN
  IF cqBinOK == 1 THEN RETURN "exact ends: bisection on BINOMIAL_CDF, checked"; END;
  RETURN "exact: OFF (BINOMIAL_CDF failed its check)";
END;

// Six pages (beta 2; five in beta 1); any key or tap turns the page,
// Esc leaves.
cqHelp()
BEGIN
  LOCAL hpK, hpN, hpF, hpB, hpG, hpM;
  LOCAL hpR, hpY;
  hpN := #000000h; hpB := cqBLUE; hpG := cqGRAY;
  hpM := cqMAG; hpR := cqRED; hpY := cqGRN;
  hpF := "any key or tap = next page, Esc = done";

  hpK := cqPage("95 % of WHAT?", {
    "Each line is the interval from ONE sample.",
    "The green line is the truth (" + cqMuS + " or p): the",
    "method never sees it. Blue lines caught it,",
    "red lines missed it.",
    "",
    "The 95 % is the METHOD'S long-run catch rate,",
    "not a property of any one interval. Keep",
    "adding samples: the 'all' rate in the header",
    "settles near the target.",
    "",
    "Any one interval either caught " + cqMuS + " or missed it.",
    "So say 'we are 95 % confident' -- meaning the",
    "method catches the truth 95 % of the time --",
    "not 'a 95 % chance " + cqMuS + " is in THIS interval'."},
    {hpN, hpY, hpN, hpN, hpN, hpM, hpM, hpM, hpM, hpN, hpR, hpR, hpR, hpR}, hpF);
  IF hpK == 4 THEN RETURN; END;

  hpK := cqPage("Level and width; z and t", {
    "Drag the right edge (or " + cqUD + "): a higher level",
    "uses a bigger critical value, so EVERY line",
    "grows, red lines turn blue and the catch rate",
    "rises. 99 % catches more but says less.",
    "",
    "z: " + cqXb + " +/- z " + cqTms + " " + cqSgS + "/sqrt(n) uses the TRUE " + cqSgS + ",",
    "so every z line has the same length.",
    "t: " + cqXb + " +/- t " + cqTms + " s/sqrt(n) uses the sample's s.",
    "s changes from sample to sample, so t lines",
    "differ in length, and t* is bigger than z* to",
    "pay for it: t(9) = 2.262 against z = 1.960.",
    "z with s at n = 10 would catch only 91.8 %.",
    "",
    cqEngS()},
    {hpM, hpM, hpM, hpM, hpN, hpB, hpB, hpB, hpB, hpB, hpB, hpR, hpN, hpG}, hpF);
  IF hpK == 4 THEN RETURN; END;

  hpK := cqPage("Proportions: Wald and Wilson", {
    "Wald: " + cqPh + " +/- z " + cqTms + " sqrt(" + cqPh + "(1 - " + cqPh + ")/n), the",
    "textbook interval. Centre and width both",
    "come from " + cqPh + ", which fails when n " + cqTms + " p is small:",
    "at p = 0.10, n = 20 Wald catches only 87.6 %",
    "(exact binomial; it claims 95 %).",
    "12.2 % of those samples have " + cqPh + " = 0: the",
    "Wald interval has ZERO width and must miss.",
    "Wilson (score) takes its SE from the p being",
    "tested, not from " + cqPh + ": never zero width, centre",
    "pulled toward 1/2. Same p and n: 95.7 %.",
    "Wald wants n " + cqTms + " p and n " + cqTms + " (1 - p) of 10 or",
    "more; at p = 0.10, n = 100 it reaches 93.2 %.",
    "Grey on the axis: impossible values (below 0,",
    "above 1). Only Wald lines can reach them."},
    {hpN, hpN, hpN, hpR, hpR, hpR, hpR, hpB, hpB, hpB, hpN, hpN, hpG, hpG}, hpF);
  IF hpK == 4 THEN RETURN; END;

  // beta 2: why the exact interval is conservative (every number
  // checked by exact binomial sums in verify_CICatcher_beta2.py)
  hpK := cqPage("Exact intervals: never below", {
    "Exact (Clopper-Pearson) uses no normal curve:",
    "it keeps every p under which your count is in",
    "neither tail of Binomial(n, p) holding " + CHAR(945) + "/2.",
    "A count has only n + 1 possible values, so no",
    "interval can catch exactly 95 % at every p.",
    "Exact makes sure it is NEVER LESS: at n = 20",
    "its worst case over every p is 95.8 %.",
    "At p = 0.10, n = 20 it catches 98.9 % (Wilson",
    "95.7 %, Wald 87.6 %); at 80 % or 90 % it still",
    "catches 95.7 %. The price: longer lines.",
    "0 yeses: from 0 to 1 - (" + CHAR(945) + "/2)^(1/n), 0.168 at",
    "n = 20 and 95 %. Tap such a line: the panel",
    "shows that end as the CAS writes it.",
    cqXEngS()},
    {hpN, hpN, hpN, hpN, hpN, hpM, hpM, hpB, hpB, hpB, hpN, hpN, hpN, hpG}, hpF);
  IF hpK == 4 THEN RETURN; END;

  hpK := cqPage("A criminal-justice example", {
    "A city asks n = 100 residents: 'Are you afraid",
    "to walk alone at night?' 38 say yes, so",
    cqPh + " = 0.38 and the 95 % Wald interval is",
    "0.38 +/- 1.96 " + cqTms + " sqrt(0.38 " + cqTms + " 0.62 / 100)",
    "= 0.38 +/- 0.095, from 28.5 % to 47.5 %.",
    "",
    "RIGHT: 'We are 95 % confident the true share",
    "is between 28.5 % and 47.5 %': the method",
    "catches the truth 95 % of the time.",
    "WRONG: 'There is a 95 % chance the true share",
    "is in this interval.' The share is fixed;",
    "this interval caught it or it did not.",
    "",
    "Try it: Wilson, p = 0.38, n = 100, then +50."},
    {hpN, hpN, hpB, hpB, hpB, hpN, hpY, hpY, hpY, hpR, hpR, hpR, hpN, hpM}, hpF);
  IF hpK == 4 THEN RETURN; END;

  hpK := cqPage("Keys and touch", {
    "+1 = one new sample, animated: its values,",
    "its estimate, then its interval (key: -)",
    "+10, +50 = more samples at once (+10 key: +)",
    "Conf = 80, 90, 95 or 99 %",
    "drag the right edge = any level, 50 to 99.9 %",
    cqUD + " = the level up or down, 1 % at a time",
    "tap a line = its sample in the bottom panel;",
    "slide up and down the stack to browse,",
    cqLR + " = the next line; tap it again to close",
    "n = sample size (starts a fresh run)",
    "Menu = new setup, reset counts, help, quit",
    "Ent = menu, Esc = quit",
    "Home after quitting: CQ_TOTAL, CQ_HITS and",
    "CQ_COVER (the 'all' catch rate)."},
    {hpN, hpN, hpN, hpN, hpM, hpM, hpB, hpB, hpB, hpN, hpN, hpN, hpG, hpG},
    "any key or tap = back");
END;

// The Enter / Menu menu.  Returns 1 to keep going, 0 to quit.
// beta 2c: the first item was "New setup: interval, population, n"
// (34 characters); the list-form CHOOSE box cut a 35-character line
// after about 31 on the emulator, so it is now 28 characters, like
// every other menu line in this program (fleet review 19 Oct 2026,
// C-C1).  It still names what a new setup asks for: which interval,
// the population's true values, and n.  (Comments go here, never
// inside the list: a comment inside an open bracket is a Syntax
// Error on the Prime.)
cqMenu()
BEGIN
  LOCAL mnI;
  mnI := 0;
  CHOOSE(mnI, "CICatcher", {
    "New: interval, population, n",
    "Reset the counts",
    "Help",
    "Quit"});
  IF mnI == 1 THEN cqSetup(); END;
  IF mnI == 2 THEN cqReset(); END;
  IF mnI == 3 THEN cqHelp(); END;
  IF mnI == 4 THEN RETURN 0; END;
  RETURN 1;
END;

// The opening page.
cqWelcome()
BEGIN
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, cqORNG, cqLORN);
  TEXTOUT_P("CICatcher", G1, 14, 10, 4, cqBLUE);
  TEXTOUT_P("95 % confidence: 95 % of WHAT?", G1, 14, 44, 2, cqMAG, 296);
  TEXTOUT_P("Take many samples from a population", G1, 14, 66, 2, #000000h, 296);
  TEXTOUT_P("you know.  Each sample gives one", G1, 14, 82, 2, #000000h, 296);
  TEXTOUT_P("interval: blue if it catches the", G1, 14, 98, 2, cqBLUE, 296);
  TEXTOUT_P("truth, red if it misses.", G1, 14, 114, 2, cqRED, 296);
  TEXTOUT_P("The level promises a long-run catch", G1, 14, 136, 2, cqGRN, 296);
  TEXTOUT_P("rate.  Count, and see if it is kept.", G1, 14, 152, 2, cqGRN, 296);
  TEXTOUT_P("Drag the right edge to change the level;", G1, 14, 176, 1, cqGRAY, 296);
  TEXTOUT_P("tap a line to see the sample behind it.", G1, 14, 189, 1, cqGRAY, 296);
  TEXTOUT_P("Any key or tap to set up", G1, 14, 212, 2, cqFOOT, 296);
  BLIT_P(G0, 0, 0, G1);
  cqWaitAny();
END;


// ------------------------------------------------------------
// START-UP AND THE MAIN LOOP
// ------------------------------------------------------------
cqInit()
BEGIN
  // house colours (Lessons Learned section 13)
  cqBLUE := #003CC8h; cqRED := #FF0000h; cqGRN := #008C3Ch;
  cqMAG := #C800C8h; cqORNG := #FF4500h; cqGRAY := #5A5A5Ah;
  cqFOOT := #1E90FFh; cqGRID := #E1E4EBh; cqLBLU := #EBF2FFh;
  cqLRED := #FFEBEBh; cqLORN := #FFF0DCh; cqLGRY := #E6E6E6h;
  // glyphs: x-bar, p-hat (HP's private-use pair), mu, sigma, the
  // up-down and left-right arrows, the multiplication sign
  cqXb := CHAR(57344); cqPh := CHAR(57345);
  cqMuS := CHAR(956); cqSgS := CHAR(963);
  cqUD := CHAR(8645); cqLR := CHAR(8646);
  cqTms := CHAR(215);
  // geometry (see LAYOUT in the header)
  cqPL := 6; cqPR := 294; cqPT := 27; cqPB := 178;
  cqRY0 := 29; cqAY := 179; cqPY := 195;
  cqSLX := 298; cqST := 30; cqSB := 176;
  cqZlo := NORMALD_ICDF(0, 1, 0.75);        // 0.674, the z* of 50 %
  cqZhi := NORMALD_ICDF(0, 1, 0.9995);      // 3.291, the z* of 99.9 %
  cqSYt := {0, 0, 0, 0, 0};
  cqSYt(1) := cqSliderY(500);
  cqSYt(2) := cqSliderY(800);
  cqSYt(3) := cqSliderY(900);
  cqSYt(4) := cqSliderY(950);
  cqSYt(5) := cqSliderY(990);
  cqSLt := {"50", "80", "90", "95", "99"};
  // the defaults are pedagogy: IQ-like scores for a mean (n = 10),
  // and p = 0.10 with n = 20 for a proportion, where the Wald
  // interval visibly falls short of its promise
  cqType := 1;
  cqMu := 100; cqSg := 15; cqNm := 10;
  cqP := 0.1; cqNp := 20;
  cqN := cqNm;
  cqL := 950;
  cqEng := 0;
  cqTX := -1; cqTY := -1;
  // beta 2: the exact interval's built-ins, checked once; the CAS cache
  cqBinOK := cqBinCheck();
  cqCasOK := 0; cqCasT := ""; cqCasK := -1;
  cqReset();
END;

// The live screen: draw, wait for a key or a touch, act.
cqLoop()
BEGIN
  LOCAL lpK, lpM, lpM1, lpGo, lpX, lpY;
  LOCAL lpA;
  lpGo := 1;
  REPEAT lpK := GETKEY; UNTIL lpK == -1;
  cqDrain();
  WHILE lpGo == 1 DO
    cqCasPanel();          // beta 2: the CAS, on events only (see there)
    cqDraw();
    REPEAT
      lpK := GETKEY;
      lpM := MOUSE;
      lpM1 := lpM(1);
    UNTIL lpK > -1 OR SIZE(lpM1) > 0;
    // lpA: 1 +1, 2 +10, 3 +50, 4 Conf, 5 n, 6 Menu; 0 nothing
    lpA := 0;
    IF SIZE(lpM1) > 0 THEN
      lpX := lpM1(1);
      lpY := lpM1(2);
      IF lpY >= 218 THEN
        // the soft-key strip: six 53-px slots, left to right
        cqDrain();
        lpA := MIN(6, IP(lpX / 53.4) + 1);
      ELSE
        IF lpX >= cqSLX AND lpY >= cqST - 8 AND lpY <= cqSB + 8 THEN
          cqDragLevel(lpY);                 // the level slider
        ELSE
          IF lpX < cqSLX AND lpY >= cqPT AND lpY <= cqPB THEN
            cqDragRows(lpY);                // pick / browse the lines
          ELSE
            IF lpY >= cqPY - 1 THEN cqSel := 0; END;   // a tap on the panel closes it
            cqDrain();
          END;
        END;
      END;
    ELSE
      // the keys remain as shortcuts for the soft keys
      IF lpK == 4 THEN lpGo := 0; END;                 // Esc   = quit
      IF lpK == 30 THEN lpA := 6; END;                 // Enter = Menu
      IF lpK == 45 THEN lpA := 1; END;                 // -     = +1
      IF lpK == 50 THEN lpA := 2; END;                 // +     = +10
      IF lpK == 2 THEN cqStepLevel(1); END;            // Up    = level up
      IF lpK == 12 THEN cqStepLevel(-1); END;          // Down  = level down
      IF lpK == 7 THEN cqStepSel(-1); END;             // Left  = newer line
      IF lpK == 8 THEN cqStepSel(1); END;              // Right = older line
    END;
    IF lpA == 1 THEN cqAddAnim(); END;
    IF lpA == 2 THEN cqAddFast(10); END;
    IF lpA == 3 THEN cqAddFast(50); END;
    IF lpA == 4 THEN cqAskConf(); END;
    IF lpA == 5 THEN cqAskN(); END;
    IF lpA == 6 THEN lpGo := cqMenu(); END;
    // a dialog's closing tap or key can arrive again on the screen
    // underneath, so drain both before the next frame
    IF lpA >= 4 THEN
      REPEAT lpK := GETKEY; UNTIL lpK == -1;
      cqDrain();
    END;
  END;
END;

EXPORT CICatcher()
BEGIN
  cqInit();
  cqWelcome();
  IF cqSetup() == 0 THEN
    RETURN "CICatcher: no interval chosen";
  END;
  cqLoop();
  cqExport();
  // Hand the summary back as the program's value: Home shows it in
  // the history and no Terminal screen opens.
  IF cqTot == 0 THEN
    RETURN "CICatcher: no intervals drawn (" + cqDesc() + ")";
  END;
  RETURN "CICatcher: " + STRING(cqHit, 2, 0) + " of " + STRING(cqTot, 2, 0) + " caught the truth (" + cqPct(cqHit, cqTot) + " %) at " + cqLvS(cqL) + " %, " + cqDesc() + "; see CQ_COVER";
END;
