pons 0.10.0

Rust package for contract bridge
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
//! The instinct bidder: a keyless floor for off-book auctions
//!
//! Competitive auctions cannot be enumerated — interference multiplies
//! sequences combinatorially, and a book that stops mid-auction leaves the
//! driver to pass by default.  The worst of those defaults is passing
//! partner's takeout double on a worthless hand, turning a routine advance
//! into a doubled partscore for the opponents.
//!
//! [`instinct()`] is the floor under the book: one context-driven [`Rules`]
//! ladder that answers *every* auction with a sane natural action.  Attach it
//! as a root [`Always`][super::fallback::Always] fallback — as
//! [`american()`][crate::bidding::american::american] does for its
//! competitive and defensive books — and the system never falls off the book.
//! By [`Trie::resolve`][super::Trie::resolve] precedence the root is reached
//! last, so instinct can never override an authored rule, only catch what
//! falls past all of them.
//!
//! # Everything is natural
//!
//! Instinct fires precisely where the book has no agreement, so partner's
//! continuation is usually off-book too — decoded by *partner's* instinct.
//! The two halves stay coherent because every instinct call is natural:
//! bids show the bid suit, raises show support, doubles are takeout.  No
//! conventional calls (in particular no strength-showing cue-bids) belong
//! here until both sides of the convention are authored.
//!
//! # Advancing partner's double
//!
//! Partner's live takeout double — the auction ends `… (bid) X (Pass)` with
//! their suit bid at the three level or below doubled by partner — calls for an
//! advance, but a takeout double is *not 100% forcing*.  Pass means *play the
//! top bid*: with length behind their doubled suit the better action is to
//! **defend** (pass plays their doubled contract), so the floor passes; only a
//! hand that cannot beat their contract advances — a penalty pass on a trump
//! stack, a major-suit game jump or 3NT with values, the longest unbid suit at
//! the cheapest level, and a notrump escape so *some* action is always
//! available.  A four-level new suit is a *free bid* (you could defend instead),
//! so it shows values.  The interpretation of the double is deliberately
//! mechanical: a classifier may know its system, and instinct's system is plain
//! standard.  (The defend-or-advance reading is the "settle floor", default on;
//! [`set_settle_floor`] recovers the old always-advance behavior.)
//!
//! # Observability
//!
//! Instinct activations are visible in the
//! [`Provenance`][super::trie::Provenance] returned by
//! [`Trie::resolve`][super::Trie::resolve]: `depth == 0` with
//! `fallback == Some(_)` is the floor firing.  In simulation, count these —
//! the most-hit auctions are the next nodes worth authoring properly.

use super::Rules;
use super::constraint::{
    Cons, Constraint, balanced, described, hcp, len, min_level_is, partner_shown_len,
    partner_suit_is, point_count, points, pred, short_in_their_suits, stopper_in_their_suits,
    support, takeout_double_shape_ok, they_bid, top_honors,
};
use super::context::Context;
use super::inference::Inferences;
use super::rules::Alert;
use contract_bridge::auction::Call;
use contract_bridge::eval::hcp as holding_hcp;
use contract_bridge::{Bid, Hand, Penalty, Rank, Strain, Suit};
use core::cell::Cell;

/// The per-call alert for responder's gambling 3NT over a double of our 1NT: a
/// long minor run, *not* a natural balanced 3NT.  Marks the call artificial so
/// the inference reader suppresses the natural notrump reading — without it the
/// sampler would deal responder balanced and mis-score the gamble.
const GAMBLING_3NT: Alert = Alert("1ntx:gambling-3nt");

/// What responder's `2NT` shows in the doubled-1NT runout (A/B knob)
///
/// This governs only the weak, no-five-card-suit responder's both-minor action;
/// a hand with a five-card suit always escapes naturally, in every mode.
#[doc(hidden)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Unusual2nt {
    /// `2NT` = both minors, four-four (the scramble); opener picks the better
    /// minor.  The historic behavior, now an opt-in.
    FourFour,
    /// `FourFour` plus a five-five-minors hand bids `2NT` too (above the natural
    /// escape) so opener picks the better fit rather than guess a minor.  A/B'd a
    /// *loss* vs the default, so opt-in only.
    FiveFiveAdd,
    /// No `2NT` relay: a four-four bust bids its longer minor directly at the two
    /// level — one double-exposure instead of the relay's two.  The default: A/B'd
    /// a win over the relay (the extra exposure and higher landing level cost more
    /// than the better minor the relay finds).
    #[default]
    Direct,
}

/// What a *latched* later double means after our natural penalty double of their
/// 1NT — the `(1NT)−X−(2Y)−X` second double (A/B knob, see [`set_latch_style`])
///
/// The mirror of [`DoubleStyle`][super::american::DoubleStyle] on the defensive
/// side: the same penalty-vs-optional question the we-open `1NT−(2X)−X` faced.
#[doc(hidden)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum LatchStyle {
    /// Pure penalty: the latched double needs a trump stack (4+ with two top
    /// honors) and partner *sits*.  The default — the human "once penalty, always
    /// penalty" rule.
    #[default]
    Penalty,
    /// Cooperative / optional: the latched double shows only 2-3 cards in their
    /// suit with values, and partner *cooperates* (sit on a fit, run when short)
    /// via the general advance-a-double machinery instead of being forced to sit.
    Optional,
}

std::thread_local! {
    /// Whether the floor consults the auction interpretation for known fits
    static INFERENCE_AWARE: Cell<bool> = const { Cell::new(true) };

    /// Whether a weak responder runs from our doubled 1NT (default on)
    static ONE_NT_RUNOUT: Cell<bool> = const { Cell::new(true) };

    /// HCP floor at which responder redoubles a doubled 1NT to play (A/B knob)
    static RUNOUT_XX_MIN: Cell<u8> = const { Cell::new(7) };

    /// Whether the runout is universal: opener also escapes / SOS-redoubles in
    /// the balancing seat, not just the weak responder direct (default on)
    static ONE_NT_RUNOUT_UNIVERSAL: Cell<bool> = const { Cell::new(true) };

    /// What responder's `2NT` shows in the runout (see [`set_unusual_2nt`]);
    /// `Direct` (no relay) by default, A/B'd a win over the `FourFour` relay
    static UNUSUAL_2NT: Cell<Unusual2nt> = const { Cell::new(Unusual2nt::Direct) };

    /// Whether we double the opponents' escape from our doubled 1NT on a trump
    /// stack in their suit (default on; A/B'd +5..+7 IMPs/divergent)
    static PENALIZE_ESCAPE_STACK: Cell<bool> = const { Cell::new(true) };

    /// Whether we double their escape from our 1NT-XX on values, once
    /// responder's business redouble has shown them (default on; A/B'd a win)
    static PENALIZE_ESCAPE_VALUES: Cell<bool> = const { Cell::new(true) };

    /// Whether we encircle (penalty-double) the opponents' escape from our
    /// `1NT-(2NT)-X` — the Unusual-vs-Unusual penalty chase. Default on (it only
    /// fires after our own UvU `X`, so it is dormant unless [`set_uvu`] is on).
    static UVU_ENCIRCLE: Cell<bool> = const { Cell::new(true) };

    /// Whether the "settle" view of Pass is in force (**default on** — see
    /// [`set_settle_floor`]): partner's takeout double is not 100% forcing, so a
    /// hand may pass to *play the top bid* (defend) instead of always advancing
    static SETTLE_FLOOR: Cell<bool> = const { Cell::new(true) };

    /// Whether the "once penalty, always penalty" latch is in force (**on by
    /// default** — DD-measured a penalty-X-bucket win with no regression, see
    /// [`set_penalty_latch`]): after our natural penalty double of their 1NT, our
    /// later doubles read as penalty (sit / leave in) rather than the takeout default
    static PENALTY_LATCH: Cell<bool> = const { Cell::new(true) };

    /// What a latched later double means: [`Penalty`] (stack + sit, **the
    /// default**) or [`Optional`] (2-3 + cooperate). See [`set_latch_style`].
    ///
    /// [`Penalty`]: LatchStyle::Penalty
    /// [`Optional`]: LatchStyle::Optional
    static LATCH_STYLE: Cell<LatchStyle> = const { Cell::new(LatchStyle::Penalty) };

    /// Whether to suppress the doubler's *constructive pulls* of its own penalty
    /// double of their 1NT (**on by default** — DD-measured a clear penalty-X-bucket
    /// win; see [`set_penalty_no_pull`]).  While [`penalty_latched`], the natural
    /// suit and notrump overcall rules still fire for the doubler (a double is not a
    /// bid), so a 15+ balanced doubler "competes" to 2NT/3NT/a major opposite a
    /// likely-broke partner — the dominant defense leak.  On, those pulls step aside
    /// and the doubler defends (Pass) or latch-doubles their escape.
    static PENALTY_NO_PULL: Cell<bool> = const { Cell::new(true) };

    /// Whether a weak advancer runs from their *redoubled* penalty double
    /// (`[1NT, X, XX]`, **on by default** — see [`set_advancer_xx_runout`]).  Their
    /// XX is business (BBA and our own system both: "we make 1NT redoubled"), so a
    /// broke advancer escapes to its long suit rather than sit for the doom.
    static ADVANCER_XX_RUNOUT: Cell<bool> = const { Cell::new(true) };

    /// Whether the *doubler* runs after `[1NT, X, XX, P, P]` comes back around
    /// (**on by default** — see [`set_doubler_xx_runout`]).  Construction-gated:
    /// read once in [`instinct`] so the escape rule lands only in the on book.
    static DOUBLER_XX_RUNOUT: Cell<bool> = const { Cell::new(true) };

    /// Whether advances of partner's simple overcall are Rubens transfers /
    /// the cue-raise (**on by default** — today's behavior; see
    /// [`set_rubens_advances`]).  Off recovers a natural-advances baseline for
    /// A/B: raises stay the natural ladder and a knob-off natural new-suit
    /// advance covers the hands the transfers covered.
    static RUBENS_ADVANCES: Cell<bool> = const { Cell::new(true) };

    /// HCP floor at which a strong-1NT responder forces game off the floor *in an
    /// undisturbed auction* (A/B knob; see [`set_nt_responder_game_floor`]).  The
    /// authored direct-3NT game force is already 9, but a 9-count *five-card-major*
    /// hand can't bid it (it must transfer) and matches no authored game-forcing
    /// transfer rebid, so it lands here; default **9** (an A/B win: plain +0.0048
    /// IMPs/board vs BBA, PD wash).  Only undisturbed: forcing a thin 9 over a suit
    /// overcall measured a DD loss (the enemy lead/shape beats the thin 3NT), and
    /// over a double the business XX governs ([`SUPPRESS_NT_GF_OVER_DOUBLE`]).
    static NT_RESPONDER_GAME_FLOOR: Cell<u8> = const { Cell::new(9) };

    /// Whether to suppress the strong-1NT responder's natural-3NT game force at
    /// responder's first turn over a *double* of our 1NT (**on by default**; see
    /// [`set_suppress_nt_game_force_over_double`]).  The business redouble is
    /// unlimited — over the double we defend `1NT` redoubled (or escape a long
    /// suit) rather than pull to 3NT.  Isolated A/B win +5.6 IMPs/fired in both
    /// plain and PD (rare, ~0.03%).
    static SUPPRESS_NT_GF_OVER_DOUBLE: Cell<bool> = const { Cell::new(true) };

    /// Whether opener corrects partner's choice-of-games `3NT` to `4M` holding a
    /// *known* eight-card major fit — **undisturbed and with a ruffing doubleton**
    /// (see [`set_correct_3nt_to_major`]).  The 5-3 ruffing edge is single-dummy lore
    /// that double-dummy shares only when the trump-short hand can ruff: a flat
    /// 4-3-3-3 has no ruff (`3NT` keeps its ninth trick against `4M`'s tenth), and a
    /// contested pull walks into a penalty double.  Ungated the correction lost
    /// −0.037 IMPs/board; gated on both (`undisturbed`, `has_ruffing_shortness`) it
    /// wins **+0.0062 plain / +0.0068 PD** (CI ±0.0005, two seeds).  Default **on**.
    static CORRECT_3NT_TO_MAJOR: Cell<bool> = const { Cell::new(true) };

    /// Whether responder's 3NT over a *double* of our 1NT is the **gambling**
    /// long-minor game — six-plus clubs or diamonds, semi-solid, optionally an
    /// outside ace — instead of the suppressed game-force / business-XX baseline.
    /// Off by default (opt-in A/B knob; see [`set_gambling_3nt_over_double`]).  The
    /// minor length floor is fixed at six (it must be a build-time `len` to project
    /// the suit for the reader); the quality and ace gates are runtime knobs.
    static GAMBLING_3NT_OVER_DOUBLE: Cell<bool> = const { Cell::new(false) };

    /// Top-honor floor (count of A/K/Q) the gambling 3NT's long minor must hold —
    /// the "semi-solid" gate.  `0` disables it (length only).  Default `2`.
    static GAMBLING_3NT_TOP_HONORS: Cell<u8> = const { Cell::new(2) };

    /// Whether the gambling 3NT requires the *suit* ace — the ace of the long
    /// minor itself, so the suit runs from the top and buffs total tricks.  On by
    /// default when the package is armed.
    static GAMBLING_3NT_REQUIRE_ACE: Cell<bool> = const { Cell::new(true) };

    /// Whether responder's 4M over a *double* of our 1NT is loosened to a
    /// **preemptive** long-major game — six-plus major plus a modest HCP floor —
    /// instead of needing full game values.  Off by default (opt-in A/B knob; see
    /// [`set_preempt_4m_over_double`]).  The undisturbed / over-an-overcall 4M is
    /// unchanged: this only adds a rule in the doubled-1NT runout.
    static PREEMPT_4M_OVER_DOUBLE: Cell<bool> = const { Cell::new(false) };

    /// The HCP floor for the preemptive 4M long-major game (see
    /// [`PREEMPT_4M_OVER_DOUBLE`]).  Default `5` — a source of tricks, not a bust.
    static PREEMPT_4M_FLOOR: Cell<u8> = const { Cell::new(5) };

    /// Top-honor floor (count of A/K/Q) the preemptive 4M's long major must hold —
    /// the same "semi-solid" gate the gambling 3NT uses, so 4M is a *quality* long
    /// major, not any six-bagger (`0` = length only).  Default `2`: a ragged six-card
    /// major jumping to game fails double-dummy exactly as a ragged minor 3NT does.
    static PREEMPT_4M_TOP_HONORS: Cell<u8> = const { Cell::new(2) };

    /// Whether the preemptive 4M requires the *trump* ace — the ace of the long
    /// major, a sure trump trick and control that buffs total tricks.  On by default
    /// when the package is armed.
    static PREEMPT_4M_REQUIRE_ACE: Cell<bool> = const { Cell::new(true) };

    /// A hand that already bid a suit rebids it in competition rather than being
    /// forced to a takeout double (**shipped default-on**; see
    /// [`set_competitive_rebid`]).
    static COMPETITIVE_REBID: Cell<bool> = const { Cell::new(true) };
}

/// Responder runs from a doubled 1NT below this many HCP; with more, 1NT-X
/// rates to make opposite a 15–17 opener, so sit (or redouble — see
/// [`set_runout_xx_min`]).  A named knob for A/B tuning.
const RUNOUT_MAX_HCP: u8 = 8;

/// Enable or disable inference-aware instinct rules on the current thread
///
/// For A/B measurement only (see the `inference-floor` example): with it
/// disabled the floor ignores partner's shown shape, falling back to the
/// shape-blind 3NT / six-card-major game selection.  The flag is read at
/// classification time and is per-thread; classify on the thread that set it.
#[doc(hidden)]
pub fn set_inference_aware(enabled: bool) {
    INFERENCE_AWARE.with(|flag| flag.set(enabled));
}

/// The floor is consulting the auction interpretation (see [`set_inference_aware`])
fn inference_aware() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| INFERENCE_AWARE.with(Cell::get))
}

/// Enable or disable the doubled-1NT runout on the current thread
///
/// On by default: when our 1NT is doubled, a weak responder escapes to its
/// longest five-plus-card suit instead of sitting for the penalty (and opener
/// passes that escape).  Disable to fall back to the natural floor — Pass.  For
/// A/B measurement (see the `ab-one-nt-runout` example); read at classification
/// time, per-thread.
#[doc(hidden)]
pub fn set_one_nt_runout(enabled: bool) {
    ONE_NT_RUNOUT.with(|flag| flag.set(enabled));
}

/// The doubled-1NT runout is enabled (see [`set_one_nt_runout`])
fn one_nt_runout_enabled() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| ONE_NT_RUNOUT.with(Cell::get))
}

/// Enable or disable the "settle" view of Pass on the current thread
///
/// **On by default** (A/B'd a clear win — +0.26 IMPs/board vul none, +0.37 vul
/// both, on `ab-settle-floor`'s perfect-defense measure).  The floor treats Pass
/// as *playing the top bid*: partner's takeout double is not 100% forcing, so a
/// hand with a good penalty (length behind their doubled suit) defends instead of
/// advancing, and a four-level advance becomes a *free bid* requiring values.
/// Disable to recover the old always-advance floor.  For A/B measurement (see the
/// `ab-settle-floor` example); read at classification time, per-thread.
#[doc(hidden)]
pub fn set_settle_floor(enabled: bool) {
    SETTLE_FLOOR.with(|flag| flag.set(enabled));
}

/// The "settle" view of Pass is enabled (see [`set_settle_floor`])
fn settle_floor() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| SETTLE_FLOOR.with(Cell::get))
}

/// Enable opener's/overcaller's competitive rebid of a long suit (**shipped default-on**)
///
/// Once our side has bid, the deterministic floor's only competitive actions are
/// raising partner and the takeout double — so a self-sufficient one-suiter
/// (e.g. `1♦ (1♥) P (2♥)` holding `AKJT984`) can only double, misdescribing a
/// takeout shape it does not have.  With this on, a suit we already bid and hold
/// six-plus in is rebid at the cheapest legal level, outranking that double; the
/// existing raise ladder then carries responder to game.  The two-level rebid is
/// unconditional; the more committal three-level rebid demands a real source of
/// tricks (seven cards, or a good six — two of the top three honors).
///
/// A/B (SEED_BASE 1783316036, 102.4k bd/arm/vul): plain **+0.047/+0.037** IMPs/bd
/// NV/vul, PD **+0.040/+0.023**, all four cells' CIs exclude 0.  Disable with
/// `bba-gen --no-ns-competitive-rebid`.  Read at book construction.
#[doc(hidden)]
pub fn set_competitive_rebid(enabled: bool) {
    COMPETITIVE_REBID.with(|flag| flag.set(enabled));
}

/// The competitive long-suit rebid is enabled (see [`set_competitive_rebid`])
fn competitive_rebid_enabled() -> bool {
    COMPETITIVE_REBID.with(Cell::get)
}

/// Enable or disable Rubens advances of partner's simple overcall
///
/// **On by default** (today's behavior): over a one-level overcall the calls
/// from the cue up to just below partner's suit are transfers, and over a
/// two-level overcall the cue is the limit-plus raise.  Disable to recover a
/// natural-advances baseline — raises stay the natural ladder (the limit
/// distinction is lost, the honest natural price) and a natural two-level
/// new-suit advance replaces the new-suit transfer.  For A/B measurement (see
/// `bba-gen --no-ns-rubens`); read at classification time, per-thread.  The
/// [`Inferences`] reading shares the knob: off, an advance in the band is a
/// genuine suit.
///
/// [`Inferences`]: super::inference::Inferences
#[doc(hidden)]
pub fn set_rubens_advances(enabled: bool) {
    RUBENS_ADVANCES.with(|flag| flag.set(enabled));
}

/// Rubens advances are enabled (see [`set_rubens_advances`]); shared with the
/// [`Inferences`](super::inference::Inferences) reading so the bidder and the
/// reader flip together
pub(super) fn rubens_advances_enabled() -> bool {
    RUBENS_ADVANCES.with(Cell::get)
}

/// The floor's 4NT keycard ask and its 1430 answers are artificial (M6.4);
/// the alert suppresses their natural reading — without it partner would read
/// a 5♦ answer as a diamond suit and the sampler would deal the phantom.
const RKCB_FLOOR: Alert = Alert("floor:rkcb");

std::thread_local! {
    /// Whether the floor asks and answers RKCB 1430 once a fit and small-slam
    /// values are known (**on by default**, M6.4).  See [`set_floor_rkcb`].
    static FLOOR_RKCB: Cell<bool> = const { Cell::new(true) };
}

/// Enable or disable the floor's RKCB 1430 (M6.4)
///
/// **On by default**: with a known eight-card fit and combined small-slam
/// values the floor asks 4NT before committing — the milestone 6-of-the-fit
/// only fires directly at the grand-zone 37 or when the ask has no room.  The
/// answers reuse the book's 1430 ladder ([`american`](super::american)'s
/// keycard counting), so instinct decodes instinct on both sides.  Disable to
/// recover the direct-milestone floor (the A/B off arm, `bba-gen
/// --no-ns-floor-rkcb`); read at classification time, per-thread.
#[doc(hidden)]
pub fn set_floor_rkcb(enabled: bool) {
    FLOOR_RKCB.with(|flag| flag.set(enabled));
}

/// The floor RKCB is enabled (see [`set_floor_rkcb`])
fn floor_rkcb_now() -> bool {
    FLOOR_RKCB.with(Cell::get)
}

/// Set the HCP floor at which responder redoubles a doubled 1NT to play
///
/// For A/B measurement (see `ab-one-nt-runout --xx-min`).  XX shows values and
/// suggests defending 1NT redoubled; keyed on raw HCP (defensive strength), not
/// the shape-upgraded point count — a shapely weak hand should run, not sit.
#[doc(hidden)]
pub fn set_runout_xx_min(floor: u8) {
    RUNOUT_XX_MIN.with(|cell| cell.set(floor));
}

/// Set the HCP floor at which a strong-1NT responder forces game off the floor
///
/// For A/B measurement.  Default 10; lowering to 9 closes the post-transfer seam
/// where a 9-count five-card-major hand transfers, finds no authored game-forcing
/// rebid, and stalls below the floor's trigger.  The authored direct-3NT force is
/// already 9, so 9 here is symmetric.
#[doc(hidden)]
pub fn set_nt_responder_game_floor(floor: u8) {
    NT_RESPONDER_GAME_FLOOR.with(|cell| cell.set(floor));
}

/// The current strong-1NT responder game-force floor (see
/// [`set_nt_responder_game_floor`])
fn nt_responder_game_floor() -> u8 {
    NT_RESPONDER_GAME_FLOOR.with(Cell::get)
}

/// Suppress (or not) the strong-1NT responder's 3NT game force over a double of
/// our 1NT — see [`SUPPRESS_NT_GF_OVER_DOUBLE`].  For A/B measurement.
#[doc(hidden)]
pub fn set_suppress_nt_game_force_over_double(suppress: bool) {
    SUPPRESS_NT_GF_OVER_DOUBLE.with(|cell| cell.set(suppress));
}

/// Author whether opener corrects partner's choice-of-games `3NT` to `4M` with a
/// known eight-card major fit, undisturbed and holding a ruffing doubleton (see
/// [`CORRECT_3NT_TO_MAJOR`]).  Default on; disable for the off arm of an A/B.
#[doc(hidden)]
pub fn set_correct_3nt_to_major(correct: bool) {
    CORRECT_3NT_TO_MAJOR.with(|cell| cell.set(correct));
}

/// Whether the strong-1NT responder's 3NT game force is allowed in the current
/// auction.  It steps aside only at responder's first turn over a double of our
/// 1NT (when [`SUPPRESS_NT_GF_OVER_DOUBLE`] is set) — the business-XX / escape
/// runout governs instead.  Over a suit overcall it bids as usual (no XX there,
/// the opponents are not penalizing).
fn nt_game_force_3nt_allowed() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        !(SUPPRESS_NT_GF_OVER_DOUBLE.with(Cell::get) && responder_one_nt_runout_now(context))
    })
}

/// Responder holds redouble values: raw HCP at or above the [`RUNOUT_XX_MIN`]
/// floor (see [`set_runout_xx_min`])
fn responder_has_xx_values() -> Cons<impl Constraint + Clone> {
    pred(|hand: Hand, _: &Context<'_>| {
        let hcp: u8 = Suit::ASC
            .iter()
            .map(|&suit| holding_hcp::<u8>(hand[suit]))
            .sum();
        hcp >= RUNOUT_XX_MIN.with(Cell::get)
    })
}

/// Author whether responder's 3NT over a double of our 1NT is the gambling
/// long-minor game (see [`GAMBLING_3NT_OVER_DOUBLE`]).  For A/B measurement.
#[doc(hidden)]
pub fn set_gambling_3nt_over_double(on: bool) {
    GAMBLING_3NT_OVER_DOUBLE.with(|cell| cell.set(on));
}

/// Set the gambling 3NT's "semi-solid" top-honor floor (see
/// [`GAMBLING_3NT_TOP_HONORS`]; `0` = length only).  For A/B measurement.
#[doc(hidden)]
pub fn set_gambling_3nt_top_honors(floor: u8) {
    GAMBLING_3NT_TOP_HONORS.with(|cell| cell.set(floor));
}

/// Author whether the gambling 3NT requires an outside ace (see
/// [`GAMBLING_3NT_REQUIRE_ACE`]).  For A/B measurement.
#[doc(hidden)]
pub fn set_gambling_3nt_require_ace(on: bool) {
    GAMBLING_3NT_REQUIRE_ACE.with(|cell| cell.set(on));
}

/// Author whether responder's 4M over a double of our 1NT is the preemptive
/// long-major game (see [`PREEMPT_4M_OVER_DOUBLE`]).  For A/B measurement.
#[doc(hidden)]
pub fn set_preempt_4m_over_double(on: bool) {
    PREEMPT_4M_OVER_DOUBLE.with(|cell| cell.set(on));
}

/// Set the HCP floor for the preemptive 4M long-major game (see
/// [`PREEMPT_4M_FLOOR`]).  For A/B measurement.
#[doc(hidden)]
pub fn set_preempt_4m_floor(floor: u8) {
    PREEMPT_4M_FLOOR.with(|cell| cell.set(floor));
}

/// Set the preemptive 4M's "semi-solid" top-honor floor (see
/// [`PREEMPT_4M_TOP_HONORS`]; `0` = length only).  For A/B measurement.
#[doc(hidden)]
pub fn set_preempt_4m_top_honors(floor: u8) {
    PREEMPT_4M_TOP_HONORS.with(|cell| cell.set(floor));
}

/// Author whether the preemptive 4M requires the trump ace (see
/// [`PREEMPT_4M_REQUIRE_ACE`]).  For A/B measurement.
#[doc(hidden)]
pub fn set_preempt_4m_require_ace(on: bool) {
    PREEMPT_4M_REQUIRE_ACE.with(|cell| cell.set(on));
}

/// The gambling long-minor 3NT is armed (see [`set_gambling_3nt_over_double`])
fn gambling_3nt_authored() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| GAMBLING_3NT_OVER_DOUBLE.with(Cell::get))
}

/// The gambling 3NT's long minor is semi-solid: it holds at least
/// [`GAMBLING_3NT_TOP_HONORS`] of the top three honors (A/K/Q).  An eval-time
/// knob (not the build-time [`top_honors`][super::constraint::top_honors]) so the
/// A/B can flip length-only vs semi-solid per board without rebuilding.
fn gambling_3nt_semisolid(minor: Suit) -> Cons<impl Constraint + Clone> {
    described("a semi-solid suit", move |hand: Hand, _: &Context<'_>| {
        let top = [Rank::A, Rank::K, Rank::Q]
            .into_iter()
            .filter(|&rank| hand[minor].contains(rank))
            .count() as u8;
        top >= GAMBLING_3NT_TOP_HONORS.with(Cell::get)
    })
}

/// The gambling 3NT's long minor is headed by its own ace — the suit ace cashes
/// and buffs total tricks (the running suit loses no top trick to a missing ace).
/// Vacuously satisfied when the ace requirement is off.
fn gambling_3nt_suit_ace(minor: Suit) -> Cons<impl Constraint + Clone> {
    described("the suit ace", move |hand: Hand, _: &Context<'_>| {
        !GAMBLING_3NT_REQUIRE_ACE.with(Cell::get) || hand[minor].contains(Rank::A)
    })
}

/// The preemptive long-major 4M is armed (see [`set_preempt_4m_over_double`])
fn preempt_4m_authored() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| PREEMPT_4M_OVER_DOUBLE.with(Cell::get))
}

/// Responder holds at least the preemptive-4M HCP floor (see [`PREEMPT_4M_FLOOR`])
fn preempt_4m_values() -> Cons<impl Constraint + Clone> {
    described("a modest opening", |hand: Hand, _: &Context<'_>| {
        let hcp: u8 = Suit::ASC
            .iter()
            .map(|&suit| holding_hcp::<u8>(hand[suit]))
            .sum();
        hcp >= PREEMPT_4M_FLOOR.with(Cell::get)
    })
}

/// The preemptive 4M's long major is semi-solid: it holds at least
/// [`PREEMPT_4M_TOP_HONORS`] of the top three honors (A/K/Q).  The major's mirror
/// of [`gambling_3nt_semisolid`].
fn preempt_4m_semisolid(major: Suit) -> Cons<impl Constraint + Clone> {
    described("a semi-solid major", move |hand: Hand, _: &Context<'_>| {
        let top = [Rank::A, Rank::K, Rank::Q]
            .into_iter()
            .filter(|&rank| hand[major].contains(rank))
            .count() as u8;
        top >= PREEMPT_4M_TOP_HONORS.with(Cell::get)
    })
}

/// The preemptive 4M's long major is headed by the trump ace — a sure trump trick
/// and control that buffs total tricks.  Vacuously satisfied when off.
fn preempt_4m_trump_ace(major: Suit) -> Cons<impl Constraint + Clone> {
    described("the trump ace", move |hand: Hand, _: &Context<'_>| {
        !PREEMPT_4M_REQUIRE_ACE.with(Cell::get) || hand[major].contains(Rank::A)
    })
}

/// Enable or disable the *universal* doubled-1NT runout on the current thread
///
/// On by default: opener too escapes its own five-plus-card suit, and SOS-
/// redoubles (the balancing redouble) when it has none, in the seat where the
/// double comes back to it with a weak partner.  Off restricts the runout to the
/// weak responder's direct seat.  For A/B measurement (see
/// `ab-one-nt-runout --universal`); read at classification time, per-thread.
#[doc(hidden)]
pub fn set_one_nt_runout_universal(enabled: bool) {
    ONE_NT_RUNOUT_UNIVERSAL.with(|flag| flag.set(enabled));
}

/// The universal runout is enabled (see [`set_one_nt_runout_universal`])
fn one_nt_runout_universal() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| ONE_NT_RUNOUT_UNIVERSAL.with(Cell::get))
}

/// Set what responder's `2NT` shows in the doubled-1NT runout
///
/// For A/B measurement (see `ab-one-nt-runout --compare minors5|direct`); read
/// at classification time, per-thread.  [`Unusual2nt::Direct`] is the default.
#[doc(hidden)]
pub fn set_unusual_2nt(mode: Unusual2nt) {
    UNUSUAL_2NT.with(|cell| cell.set(mode));
}

/// Responder's `2NT` is configured to `mode` (see [`set_unusual_2nt`])
fn unusual_2nt_is(mode: Unusual2nt) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, _: &Context<'_>| UNUSUAL_2NT.with(Cell::get) == mode)
}

/// Enable or disable the trump-stack penalty double of the opponents' escape
///
/// For A/B measurement (see `ab-one-nt-runout --compare escape-stack`); read at
/// classification time, per-thread.  On by default.
#[doc(hidden)]
pub fn set_penalize_escape_stack(enabled: bool) {
    PENALIZE_ESCAPE_STACK.with(|flag| flag.set(enabled));
}

/// The trump-stack escape penalty is enabled (see [`set_penalize_escape_stack`])
fn penalize_escape_stack_enabled() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| PENALIZE_ESCAPE_STACK.with(Cell::get))
}

/// Enable or disable the values penalty double of their escape from our 1NT-XX
///
/// For A/B measurement (see `ab-one-nt-runout --compare escape-values`); read at
/// classification time, per-thread.  On by default.
#[doc(hidden)]
pub fn set_penalize_escape_values(enabled: bool) {
    PENALIZE_ESCAPE_VALUES.with(|flag| flag.set(enabled));
}

/// The values escape penalty is enabled (see [`set_penalize_escape_values`])
fn penalize_escape_values_enabled() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| PENALIZE_ESCAPE_VALUES.with(Cell::get))
}

/// Enable or disable the Unusual-vs-Unusual penalty chase after `1NT-(2NT)-X`
///
/// "All our doubles are penalty from the first X on"; a pass conveys inability
/// to punish *this* contract.  The responder `X` itself lives in the american
/// book ([`set_uvu`][crate::bidding::american::set_uvu]); this only adds the
/// follow-up chase of the opponents' escape.  Read at classification time,
/// per-thread.  On by default — but dormant unless our UvU `X` was bid.
#[doc(hidden)]
pub fn set_uvu_encircle(enabled: bool) {
    UVU_ENCIRCLE.with(|flag| flag.set(enabled));
}

/// The UvU penalty chase is enabled (see [`set_uvu_encircle`])
fn uvu_encircle_enabled() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| UVU_ENCIRCLE.with(Cell::get))
}

/// Partner opened a strong 1NT, RHO doubled it, and it is responder's first
/// turn — the runout situation.  The double need not be penalty: left in, any
/// double of 1NT plays for the penalty, so a weak responder escapes regardless.
fn responder_one_nt_runout_now(context: &Context<'_>) -> bool {
    let auction = context.auction();
    let n = auction.len();
    our_strong_notrump(context, 1, true)
        && auction.last() == Some(&Call::Double)
        && n >= 2
        && matches!(auction[n - 2], Call::Bid(bid) if bid == Bid::new(1, Strain::Notrump))
}

/// [`responder_one_nt_runout_now`] as a hand-ignoring [`Constraint`]
fn responder_one_nt_runout() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| responder_one_nt_runout_now(context))
}

/// We opened a strong 1NT, LHO doubled, partner ran out to a suit, and it is
/// our turn again.  Responder ran because it is weak, so it captains the
/// auction: opener passes rather than read the escape as a natural new suit.
fn opener_after_one_nt_runout_now(context: &Context<'_>) -> bool {
    let auction = context.auction();
    if !our_strong_notrump(context, 1, false) {
        return false;
    }
    let Some((index, _)) = opening_bid(auction) else {
        return false;
    };
    auction.get(index + 1) == Some(&Call::Double)
        && matches!(
            auction.get(index + 2),
            Some(&Call::Bid(bid)) if bid.strain.suit().is_some()
        )
}

/// [`opener_after_one_nt_runout_now`] as a hand-ignoring [`Constraint`]
fn opener_after_one_nt_runout() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| opener_after_one_nt_runout_now(context))
}

/// We opened a strong 1NT, LHO doubled, partner scrambled `2NT` (both minors),
/// and it is our turn — name the better minor at the three level.
fn opener_after_one_nt_minors_now(context: &Context<'_>) -> bool {
    let auction = context.auction();
    if !our_strong_notrump(context, 1, false) {
        return false;
    }
    let Some((index, _)) = opening_bid(auction) else {
        return false;
    };
    auction.get(index + 1) == Some(&Call::Double)
        && auction.get(index + 2) == Some(&Call::Bid(Bid::new(2, Strain::Notrump)))
}

/// [`opener_after_one_nt_minors_now`] as a hand-ignoring [`Constraint`]
fn opener_after_one_nt_minors() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| opener_after_one_nt_minors_now(context))
}

/// Diamonds are at least as long as clubs (the minor to name over a scramble)
fn longer_diamonds() -> Cons<impl Constraint + Clone> {
    pred(|hand: Hand, _: &Context<'_>| hand[Suit::Diamonds].len() >= hand[Suit::Clubs].len())
}

/// We opened a strong 1NT, LHO doubled, partner and the doubler's partner both
/// passed, and it is our turn — the balancing seat.  Partner had no escape, so
/// it is weak: opener may run its own suit or SOS-redouble rather than sit.
fn opener_balancing_runout_now(context: &Context<'_>) -> bool {
    if !our_strong_notrump(context, 1, false) {
        return false;
    }
    let auction = context.auction();
    let Some((index, _)) = opening_bid(auction) else {
        return false;
    };
    auction.len() == index + 4
        && auction.get(index + 1) == Some(&Call::Double)
        && auction[index + 2] == Call::Pass
        && auction[index + 3] == Call::Pass
}

/// [`opener_balancing_runout_now`] as a hand-ignoring [`Constraint`]
fn opener_balancing_runout() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| opener_balancing_runout_now(context))
}

/// Opener SOS-redoubled (the balancing redouble) and it is back to responder:
/// pick a suit, four-card suits included — opener has none of its own.
fn responder_after_opener_sos_now(context: &Context<'_>) -> bool {
    if !our_strong_notrump(context, 1, true) {
        return false;
    }
    let auction = context.auction();
    let Some((index, _)) = opening_bid(auction) else {
        return false;
    };
    auction.len() >= index + 6
        && auction.get(index + 1) == Some(&Call::Double)
        && auction[index + 2] == Call::Pass
        && auction[index + 3] == Call::Pass
        && auction[index + 4] == Call::Redouble
        && auction[index + 5..].iter().all(|&call| call == Call::Pass)
}

/// [`responder_after_opener_sos_now`] as a hand-ignoring [`Constraint`]
fn responder_after_opener_sos() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| responder_after_opener_sos_now(context))
}

/// Responder answered our SOS redouble with a suit; pass it (responder captains
/// the rescue) rather than read it as a natural new suit and raise.
fn opener_after_responder_sos_now(context: &Context<'_>) -> bool {
    if !our_strong_notrump(context, 1, false) {
        return false;
    }
    let auction = context.auction();
    let Some((index, _)) = opening_bid(auction) else {
        return false;
    };
    auction.len() >= index + 8
        && auction.get(index + 1) == Some(&Call::Double)
        && auction[index + 2] == Call::Pass
        && auction[index + 3] == Call::Pass
        && auction[index + 4] == Call::Redouble
        && auction[index + 5] == Call::Pass
        && matches!(auction[index + 6], Call::Bid(bid) if bid.strain.suit().is_some())
        && auction[index + 7..].iter().all(|&call| call == Call::Pass)
}

/// [`opener_after_responder_sos_now`] as a hand-ignoring [`Constraint`]
fn opener_after_responder_sos() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| opener_after_responder_sos_now(context))
}

/// The opponents have escaped our doubled (or redoubled) 1NT and it is our turn
///
/// Our side opened 1NT, LHO doubled, and since then we have only passed or
/// (re)doubled — never made a contract bid — so the live suit contract is
/// *theirs* (their escape), not a suit of ours.  Returns the index of our
/// opening 1NT when the pattern holds.  Counting our own doubles as "no contract
/// bid" is what lets the penalty chase recurse as they keep running.
fn our_doubled_one_nt_escape(context: &Context<'_>) -> Option<usize> {
    let auction = context.auction();
    let (index, bid) = opening_bid(auction)?;
    // Our side is to act, and opened a 1NT that LHO doubled.
    if index % 2 != auction.len() % 2 || bid != Bid::new(1, Strain::Notrump) {
        return None;
    }
    if auction.get(index + 1) != Some(&Call::Double) {
        return None;
    }
    // We made no contract bid since the opening: the live suit contract is the
    // opponents' escape, not a suit of ours that they doubled.
    let we_only_doubled = auction
        .iter()
        .enumerate()
        .skip(index + 1)
        .filter(|(i, _)| i % 2 == index % 2)
        .all(|(_, &call)| !matches!(call, Call::Bid(_)));
    if !we_only_doubled {
        return None;
    }
    // The live contract is a suit at the three level or below.
    context
        .last_bid()
        .filter(|bid| bid.strain.suit().is_some() && bid.level.get() <= 3)?;
    Some(index)
}

/// Their escape from our (re)doubled 1NT is live and undoubled — we may double
/// it for penalty (see [`our_doubled_one_nt_escape`])
fn opp_escaped_our_nt_undoubled() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        our_doubled_one_nt_escape(context).is_some() && context.penalty() == Penalty::Undoubled
    })
}

/// Their escape is undoubled *and* responder's business redouble (1NT-X-XX) has
/// already shown the values — combined we hold the balance, so a values double
/// is sound without a personal stack (see [`our_doubled_one_nt_escape`])
fn opp_escaped_our_business_xx() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        our_doubled_one_nt_escape(context).is_some_and(|index| {
            context.penalty() == Penalty::Undoubled
                && context.auction().get(index + 2) == Some(&Call::Redouble)
        })
    })
}

/// We doubled their escape for penalty; partner leaves it in rather than read it
/// as the takeout the `advancing_a_double` default would advance (see
/// [`our_doubled_one_nt_escape`])
fn leave_in_escape_penalty() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        our_doubled_one_nt_escape(context).is_some() && context.penalty() == Penalty::Doubled
    })
}

/// The opponents have escaped our `1NT-(2NT)-X` penalty double and it is our turn
///
/// Our side opened 1NT, RHO overcalled a (both-minors) 2NT, our side doubled it
/// for penalty, and since then we have only passed or doubled — so the live suit
/// contract is *theirs* (their escape from the X).  Returns the index of our
/// opening 1NT when the pattern holds; mirrors [`our_doubled_one_nt_escape`] for
/// the Unusual-vs-Unusual chase ([`set_uvu_encircle`]).
fn our_uvu_penalty_escape(context: &Context<'_>) -> Option<usize> {
    let auction = context.auction();
    let (index, bid) = opening_bid(auction)?;
    // Our side is to act, opened 1NT, RHO overcalled 2NT, our side doubled it.
    if index % 2 != auction.len() % 2 || bid != Bid::new(1, Strain::Notrump) {
        return None;
    }
    if auction.get(index + 1) != Some(&Call::Bid(Bid::new(2, Strain::Notrump)))
        || auction.get(index + 2) != Some(&Call::Double)
    {
        return None;
    }
    // We made no contract bid since the opening: the live suit is their escape.
    let we_only_doubled = auction
        .iter()
        .enumerate()
        .skip(index + 1)
        .filter(|(i, _)| i % 2 == index % 2)
        .all(|(_, &call)| !matches!(call, Call::Bid(_)));
    if !we_only_doubled {
        return None;
    }
    // The live contract is a suit at the three level or below.
    context
        .last_bid()
        .filter(|bid| bid.strain.suit().is_some() && bid.level.get() <= 3)?;
    Some(index)
}

/// Their escape from our UvU penalty `X` is live and undoubled — we may double
/// it for penalty (see [`our_uvu_penalty_escape`])
fn opp_escaped_our_uvu_undoubled() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        our_uvu_penalty_escape(context).is_some() && context.penalty() == Penalty::Undoubled
    })
}

/// We doubled their UvU escape for penalty; partner leaves it in (see
/// [`our_uvu_penalty_escape`])
fn leave_in_uvu_penalty() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        our_uvu_penalty_escape(context).is_some() && context.penalty() == Penalty::Doubled
    })
}

/// Partner's takeout double is live: the auction ends `… (bid) X (Pass)`
///
/// Mechanically: the last two calls are partner's double and RHO's pass, and
/// the doubled contract is their suit bid at the three level or below —
/// doubles of notrump or of game-level contracts read as penalty, not as a
/// request to act.
fn advancing_a_double_now(context: &Context<'_>) -> bool {
    let auction = context.auction();
    let n = auction.len();
    n >= 2
        && auction[n - 1] == Call::Pass
        && auction[n - 2] == Call::Double
        && context
            .last_bid()
            .is_some_and(|bid| bid.strain.suit().is_some() && bid.level.get() <= 3)
}

/// [`advancing_a_double_now`] as a hand-ignoring [`Constraint`] for the ladder
fn advancing_a_double() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| advancing_a_double_now(context))
}

/// We already hold an eight-card fit in some suit: our length there plus the
/// minimum partner has shown ([`Inferences`]) reaches eight.
///
/// Reads the shown *minimum* (length partner cannot lack), so it fires only on a
/// fit the calls have promised.  Used by the free-bid gate to stop inventing a
/// new suit once a trump suit is already found.
fn has_fit(hand: Hand, context: &Context<'_>) -> bool {
    let inferences = Inferences::read(context);
    let partner = inferences.partner();
    Suit::ASC
        .iter()
        .any(|&suit| hand[suit].len() + usize::from(partner.length(suit).min) >= 8)
}

/// The free-bid gate on an advance of partner's double into a new suit at `level` (see
/// [`set_settle_floor`])
///
/// A no-op unless the settle floor is on, and only ever gates the **four level**:
/// a new suit there is a *free bid* — partner's takeout double does not force us to
/// the four level, so voluntarily climbing must show values (~11+ points) and not
/// invent a suit once we already [`has_fit`].  Without them the hand stays lower (a
/// three-level take-out, the notrump escape) or defends (see [`doubled_suit_length`]).
/// One- to three-level advances are untouched: the leak was the captive `4♣x`, not
/// the cheap take-out a bust owes partner's double.
fn free_bid_gate(level: u8) -> Cons<impl Constraint + Clone> {
    pred(move |hand: Hand, context: &Context<'_>| {
        level < 4
            || !SETTLE_FLOOR.with(Cell::get)
            || (point_count(hand) >= 11 && !has_fit(hand, context))
    })
}

/// Four-plus cards in the doubled suit: a *good penalty* behind their suit
///
/// The milder sibling of [`doubled_suit_stack`] — length without the two top
/// honors.  Opposite partner's takeout double (partner is short their suit, with
/// values), sitting with four trumps behind declarer beats taking out: pass and
/// play their doubled contract.  Drives the settle floor's defend pass.
fn doubled_suit_length() -> Cons<impl Constraint + Clone> {
    pred(|hand: Hand, context: &Context<'_>| {
        context
            .last_bid()
            .and_then(|bid| bid.strain.suit())
            .is_some_and(|suit| hand[suit].len() >= 4)
    })
}

/// The doubled suit's length is within `range` — the cooperative-double gate
///
/// The 2-3-card holding behind their suit that makes the latched double *optional*
/// (see [`LatchStyle::Optional`]): some length and values, but partner decides.
fn doubled_suit_len(range: core::ops::RangeInclusive<usize>) -> Cons<impl Constraint + Clone> {
    pred(move |hand: Hand, context: &Context<'_>| {
        context
            .last_bid()
            .and_then(|bid| bid.strain.suit())
            .is_some_and(|suit| range.contains(&hand[suit].len()))
    })
}

/// A trump stack in the doubled suit: four-plus cards with two top honors
///
/// The one holding that converts partner's takeout double into penalties.
fn doubled_suit_stack() -> Cons<impl Constraint + Clone> {
    pred(|hand: Hand, context: &Context<'_>| {
        context
            .last_bid()
            .and_then(|bid| bid.strain.suit())
            .is_some_and(|suit| {
                let holding = hand[suit];
                let honors = [Rank::A, Rank::K, Rank::Q]
                    .into_iter()
                    .filter(|&rank| holding.contains(rank))
                    .count();
                holding.len() >= 4 && honors >= 2
            })
    })
}

/// Our side has not bid yet (doubles and passes do not count)
///
/// The anchor for overcall-shaped actions: once we have shown a suit or
/// notrump, instinct competes by raising or doubling instead.
fn we_have_not_bid() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        !Suit::ASC
            .into_iter()
            .map(Strain::from)
            .chain([Strain::Notrump])
            .any(|strain| context.we_bid(strain))
    })
}

/// The player to act has *personally* bid `suit` — the anchor for rebidding our
/// own long suit in competition (see [`set_competitive_rebid`]).
///
/// Seat-scoped, not side-scoped (`context.we_bid` is the union of both seats):
/// partner's artificial bid — a Jacoby transfer names our short major — must not
/// license a phantom natural "rebid" of a suit we never held.  Our own past
/// turns sit at the indices congruent to the acting seat, `(len - index) % 4 == 0`
/// (the same arithmetic `Context` uses for partner at `== 2`).
fn i_bid_suit(suit: Suit) -> Cons<impl Constraint + Clone> {
    let strain = Strain::from(suit);
    pred(move |_: Hand, context: &Context<'_>| {
        let auction = context.auction();
        let len = auction.len();
        auction.iter().enumerate().any(|(index, &call)| {
            (len - index).is_multiple_of(4)
                && matches!(call, Call::Bid(bid) if bid.strain == strain)
        })
    })
}

/// The opponents' undoubled suit bid at most `level` is the call to beat
///
/// This is the legality *and* sanity anchor for instinct doubles: the last
/// non-pass call is an opposing suit bid, not yet doubled, low enough that a
/// double still reads as takeout.
fn their_live_bid_at_most(level: u8) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        context.penalty() == Penalty::Undoubled
            && context
                .last_bid()
                .is_some_and(|bid| bid.strain.suit().is_some() && bid.level.get() <= level)
            && context
                .auction()
                .iter()
                .rposition(|&call| call != Call::Pass)
                .is_some_and(|index| (context.auction().len() - index) % 2 == 1)
    })
}

/// The strain is still biddable at or below the given level
fn level_available(level: u8, strain: Strain) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        context
            .min_level(strain)
            .is_some_and(|min| min.get() <= level)
    })
}

/// The opening bid (first non-pass call) and its index, if it is a bid
fn opening_bid(auction: &[Call]) -> Option<(usize, Bid)> {
    let index = auction.iter().position(|&call| call != Call::Pass)?;
    match auction[index] {
        Call::Bid(bid) => Some((index, bid)),
        _ => None,
    }
}

/// Our side opened a strong notrump of `level`, and the player to act is its
/// opener (`partner == false`) or its responder (`partner == true`)
///
/// This is one of the two conventions instinct reads (the other is the strong
/// 2♣ — see [`forcing_two_clubs_response`]): a strong notrump opening is the
/// anchor for completing transfers and refusing to pass below a forced game,
/// the deep conventional structures the book may not author.
fn our_strong_notrump(context: &Context<'_>, level: u8, partner: bool) -> bool {
    let auction = context.auction();
    let Some((index, bid)) = opening_bid(auction) else {
        return false;
    };
    // Our side owns the indices sharing the player-to-act's parity.
    if index % 2 != auction.len() % 2 {
        return false;
    }
    if bid.strain != Strain::Notrump || bid.level.get() != level {
        return false;
    }
    // Seats four apart are the same player; two apart are partners.
    match (auction.len() - index) % 4 {
        0 => !partner,
        2 => partner,
        _ => false,
    }
}

/// Partner's call immediately before ours, if it was a bid
fn partner_last_call(auction: &[Call]) -> Option<Bid> {
    match auction.len().checked_sub(2).map(|i| auction[i]) {
        Some(Call::Bid(bid)) => Some(bid),
        _ => None,
    }
}

/// The current contract is below game: no bid, or a partscore-level suit bid
fn below_game() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| below_game_now(context))
}

/// Partner's last call was a choice-of-games `3NT` we may correct to `4M`, and
/// the correction is enabled (see [`CORRECT_3NT_TO_MAJOR`])
///
/// Pair with a known eight-card major fit: a responder who transferred (showing
/// five) then bid `3NT` offers the choice, and opposite three-card support the
/// 5-3 fit out-scores notrump (`answer_transfer_spade_single`).  Keyed only on
/// the `3NT`, so it fires in contested auctions too (`1NT–(2♦)–…–3NT`).
fn correct_3nt_to_major_now() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        CORRECT_3NT_TO_MAJOR.with(Cell::get)
            && context.last_bid() == Some(Bid::new(3, Strain::Notrump))
    })
}

/// A ruffing doubleton — any suit of two cards or fewer.  For the balanced 1NT
/// opener this is exactly *not* a flat 4-3-3-3, the one shape with no ruffing
/// value: the 3NT→4M correction gains its extra trick only when the trump-short
/// hand can ruff, so opposite responder's balanced transferred five it stands
/// down on the flat hand and leaves the better game (3NT) in place.
fn has_ruffing_shortness() -> Cons<impl Constraint + Clone> {
    pred(|hand: Hand, _: &Context<'_>| Suit::ASC.iter().any(|&suit| hand[suit].len() <= 2))
}

/// The current contract is below game (the predicate body of [`below_game`])
fn below_game_now(context: &Context<'_>) -> bool {
    context.last_bid().is_none_or(|bid| {
        let level = bid.level.get();
        level <= 2 || (level == 3 && bid.strain != Strain::Notrump)
    })
}

/// The current contract is below slam: nothing above the five level yet
fn below_slam() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| context.last_bid().is_none_or(|bid| bid.level.get() <= 5))
}

// ---------------------------------------------------------------------------
// M6.4: slam machinery on the floor — RKCB 1430 and control-bid signoffs
// ---------------------------------------------------------------------------

/// The agreed keycard trump: the **major** maximizing our actual length plus
/// partner's shown floor, provided the total reaches an eight-card fit and we
/// hold three-plus ourselves — BWS's "an agreed suit makes 4NT keycard",
/// derived instead of installed.  Ties prefer spades.
///
/// Majors with a real (3+) holding only: at combined 33 the milestone 6NT
/// power-blast out-scores minor and thin 6-2 suit slams on double-dummy
/// (round 4 of the A/B lost 14 IMPs a board rerouting those), so the floor
/// only asks where the suit slam is the right strain anyway.
fn keycard_trump(hand: Hand, context: &Context<'_>) -> Option<Suit> {
    let inferences = Inferences::read(context);
    let partner = inferences.partner();
    #[allow(clippy::cast_possible_truncation)]
    [Suit::Hearts, Suit::Spades]
        .into_iter()
        .map(|suit| (hand[suit].len() as u8 + partner.length(suit).min, suit))
        .filter(|&(total, suit)| total >= 8 && hand[suit].len() >= 3)
        .max_by_key(|&(total, suit)| (total, suit as u8))
        .map(|(_, suit)| suit)
}

/// The trump the 4NT *answerer* counts against: the known fit if our hand
/// sees one, else the partnership's genuinely shown five-plus suit (either
/// seat's — BWS's "the only shown suit") — the asker knew the fit from their
/// side (a sixth card opposite our shown doubleton, or a fit for the six our
/// own rebid showed) even when we cannot count to eight
fn answer_trump(hand: Hand, context: &Context<'_>) -> Option<Suit> {
    keycard_trump(hand, context).or_else(|| {
        let inferences = Inferences::read(context);
        let shown = |suit: Suit| {
            inferences
                .partner()
                .length(suit)
                .min
                .max(inferences.me().length(suit).min)
        };
        [Suit::Hearts, Suit::Spades]
            .into_iter()
            .filter(|&suit| shown(suit) >= 5)
            .max_by_key(|&suit| (shown(suit), suit as u8))
    })
}

/// Partner's off-book 4NT asks keycards: undisturbed, not an opening, not
/// over our own notrump bid (that 4NT is quantitative — BWS reads keycard
/// only with an agreed suit), and a fit is known from our seat.  Returns the
/// agreed trump the 1430 answer counts against.
fn keycard_asked(hand: Hand, context: &Context<'_>) -> Option<Suit> {
    if !floor_rkcb_now() || !context.undisturbed() {
        return None;
    }
    let auction = context.auction();
    let n = auction.len();
    if partner_last_call(auction) != Some(Bid::new(4, Strain::Notrump)) {
        return None;
    }
    let (opening_index, _) = opening_bid(auction)?;
    if opening_index == n - 2 {
        return None;
    }
    if n >= 4 && matches!(auction[n - 4], Call::Bid(bid) if bid.strain == Strain::Notrump) {
        return None;
    }
    answer_trump(hand, context)
}

/// A 1430 answer to partner's 4NT: our keycard count is one of `counts`, and
/// — for the queen-splitting five-of-a-major answers — our trump-queen
/// holding matches
fn keycard_answer(counts: &'static [usize], queen: Option<bool>) -> Cons<impl Constraint + Clone> {
    use super::american::slam::count_keycards;
    described(
        "the matching 1430 keycard answer",
        move |hand: Hand, context: &Context<'_>| {
            keycard_asked(hand, context).is_some_and(|trump| {
                counts.contains(&count_keycards(hand, trump))
                    && queen.is_none_or(|wanted| hand[trump].contains(Rank::Q) == wanted)
            })
        },
    )
}

/// We asked 4NT two calls ago and partner answered 1430: decode the answer,
/// returning the trump and the partnership's combined keycard count
///
/// The ambiguous 5♣/5♦ answers resolve arithmetically first (five keycards
/// exist, so a high reading inconsistent with our own count means the low
/// one) and optimistically otherwise — the book's policy.
fn keycard_answered(hand: Hand, context: &Context<'_>) -> Option<(Suit, usize)> {
    use super::american::slam::count_keycards;
    if !floor_rkcb_now() || !context.undisturbed() {
        return None;
    }
    let auction = context.auction();
    let n = auction.len();
    if n < 4 || auction[n - 4] != Call::Bid(Bid::new(4, Strain::Notrump)) {
        return None;
    }
    let Some(&Call::Bid(answer)) = auction.get(n - 2) else {
        return None;
    };
    if answer.level.get() != 5 {
        return None;
    }
    let answer_suit = answer.strain.suit()?;
    let trump = keycard_trump(hand, context)?;
    let mine = count_keycards(hand, trump);
    let (low, high) = match answer_suit {
        Suit::Clubs => (1, 4),
        Suit::Diamonds => (0, 3),
        Suit::Hearts | Suit::Spades => (2, 2),
    };
    let partners = if mine + high > 5 { low } else { high };
    Some((trump, mine + partners))
}

/// The combined keycard count after partner's 1430 answer is within `range`,
/// with `trump` the agreed suit
fn keycard_total(
    trump: Suit,
    range: impl core::ops::RangeBounds<usize> + Clone + Send + Sync + 'static,
) -> Cons<impl Constraint + Clone> {
    pred(move |hand: Hand, context: &Context<'_>| {
        keycard_answered(hand, context)
            .is_some_and(|(t, total)| t == trump && range.contains(&total))
    })
}

/// Partner's 1430 answer was five of the agreed trump itself, so passing
/// plays it — the cramped-minor signoff
fn answer_is_five_of(trump: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        context.last_bid() == Some(Bid::new(5, Strain::from(trump)))
    })
}

/// We answered partner's 4NT keycard ask and partner has since placed the
/// contract — respect the placement (the asker holds the count); never
/// milestone past it
///
/// The exception: holding **two or more** keycards ourselves, the milestone
/// drive stays live — the asker may have taken an ambiguous answer's low
/// reading, and the book's asker tables sign off pessimistically (after a 5♥
/// answer with two own keycards the total is four, one missing, yet they
/// stop); the answerer corrects with a maximum, the standard agreement.
/// Rounds 2–3 of the A/B lost 11 IMPs a board suppressing exactly those
/// corrections.  With at most one keycard the total genuinely cannot be
/// slam-safe, so the placement stands.
fn respect_keycard_signoff() -> Cons<impl Constraint + Clone> {
    use super::american::slam::count_keycards;
    pred(|hand: Hand, context: &Context<'_>| {
        if !floor_rkcb_now() || !context.undisturbed() {
            return false;
        }
        let auction = context.auction();
        let n = auction.len();
        if n < 6 || auction[n - 6] != Call::Bid(Bid::new(4, Strain::Notrump)) {
            return false;
        }
        let (Call::Bid(answer), Call::Bid(signoff)) = (auction[n - 4], auction[n - 2]) else {
            return false;
        };
        if answer.level.get() != 5 || !answer.strain.is_suit() {
            return false;
        }
        let Some(trump) = signoff.strain.suit() else {
            return false;
        };
        count_keycards(hand, trump) <= 1
    })
}

/// Partner's last call reads as a control bid agreeing `trump` — the M6.4
/// classifier's own witness carried on [`Inferences`] (a to-play four-level
/// bid is also unread, so "the named suit floors nothing" cannot tell them
/// apart).  A slam try agreeing a suit is never a place to play: it must not
/// be passed out.
///
/// [`Inferences`]: super::inference::Inferences
fn partner_control_bid(trump: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        if !super::inference::control_bid_reading() || !context.undisturbed() {
            return false;
        }
        let n = context.auction().len();
        n >= 2
            && Inferences::read(context)
                .control_bid()
                .is_some_and(|(index, agreed)| usize::from(index) == n - 2 && agreed == trump)
    })
}

/// A known eight-card fit in `suit`: our actual length meets partner's shown
/// floor (the milestone rules' disjunction, generalized to any suit)
fn known_eight_card_fit(suit: Suit) -> Cons<impl Constraint + Clone> {
    (len(suit, 5..) & partner_shown_len(suit, 3..))
        | (len(suit, 3..) & partner_shown_len(suit, 5..))
        | (len(suit, 2..) & partner_shown_len(suit, 6..))
}

/// Our side holds at least `threshold` combined points: our exact count plus the
/// *sound floor* of partner's shown points ([`Inferences`]), so the true total
/// is never less than the test admits
///
/// This is the general game/slam trigger.  Where the special-cased forces (a
/// strong-notrump responder, a strong 2♣) encode a single auction, this fires on
/// *any* auction whose shown strength reaches a milestone — the inference floor
/// makes it sound, never an overbid on a hand that could be weaker than counted.
///
/// [`Inferences`]: super::inference::Inferences
fn combined_points(threshold: u8) -> Cons<impl Constraint + Clone> {
    pred(move |hand: Hand, context: &Context<'_>| {
        let partner_min = Inferences::read(context).partner().points.min;
        u16::from(point_count(hand)) + u16::from(partner_min) >= u16::from(threshold)
    })
}

/// Partner opened a strong notrump of `level` (we are the responder)
fn partner_strong_notrump(level: u8) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| our_strong_notrump(context, level, true))
}

/// We opened a strong notrump and partner forced past invitation with a
/// three-level suit bid — so passing below game is wrong, whatever our hand
fn opener_forced_past_invitation(context: &Context<'_>) -> bool {
    (our_strong_notrump(context, 1, false) || our_strong_notrump(context, 2, false))
        && partner_last_call(context.auction())
            .is_some_and(|bid| bid.level.get() == 3 && bid.strain != Strain::Notrump)
}

/// Our side opened a strong 2♣ and responder answered past the double negative
///
/// The artificial `2♣` promises 22+ and is forcing — but for one round only.
/// Responder's *answer* settles the game force: the 0–3 HCP double negative
/// (`2♥`) keeps open the option to stop short, while every other response — the
/// waiting `2♦` or a natural positive — commits *both* partners to at least
/// game.  So the force is read off responder's call, not off the 2♣ opening.
/// (Interference, where responder's seat holds a pass or double rather than a
/// response, is out of scope and reads as not forced.)
fn forcing_two_clubs_response(context: &Context<'_>) -> bool {
    let auction = context.auction();
    let Some((index, bid)) = opening_bid(auction) else {
        return false;
    };
    // The player to act must be on the opening side — opener or responder.
    if index % 2 != auction.len() % 2 {
        return false;
    }
    if bid != Bid::new(2, Strain::Clubs) {
        return false;
    }
    // Responder sits two seats past the opening; the force is on once that
    // answer is in and is any bid other than the double-negative 2♥.
    matches!(
        auction.get(index + 2),
        Some(&Call::Bid(response)) if response != Bid::new(2, Strain::Hearts)
    )
}

/// We are sitting for a penalty: the live contract is the opponents' bid
/// doubled (or redoubled) by our side
///
/// Since a side may only double the other, a doubled contract whose last bid is
/// theirs was doubled by us — passing it out is the intended penalty action.
fn penalizing(context: &Context<'_>) -> bool {
    let auction = context.auction();
    context.penalty() != Penalty::Undoubled
        && auction
            .iter()
            .rposition(|call| matches!(call, Call::Bid(_)))
            .is_some_and(|index| (auction.len() - index) % 2 == 1)
}

/// Instinct's reading of an auction: the system intent the laws-only [`Context`]
/// deliberately omits, reconstructed from the immutable auction on demand
///
/// There is no per-classification scratchpad to cache this in, so each flag is
/// recovered by a short walk of the auction whenever the floor consults it.
/// Every flag here is *hand-independent* — it follows from the calls alone — so
/// hand-conditioned forces (a strong-notrump responder who holds game values)
/// stay as ordinary [`Constraint`]s rather than living here.
#[derive(Clone, Copy, Debug)]
struct Interpretation {
    /// Our side is committed to at least game by a prior call: a strong 2♣
    /// whose response cleared the double negative, or an opener forced past
    /// invitation opposite our strong notrump.
    forced_to_game: bool,
    /// We are sitting for our own penalty double, so passing below game is the
    /// intended action rather than a missed game.
    penalizing: bool,
}

impl Interpretation {
    /// Read the auction's intent from its [`Context`]
    fn read(context: &Context<'_>) -> Self {
        Self {
            forced_to_game: forcing_two_clubs_response(context)
                || opener_forced_past_invitation(context),
            penalizing: penalizing(context),
        }
    }
}

/// A prior call has committed our side to game (see [`Interpretation`])
fn auction_forces_game() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| Interpretation::read(context).forced_to_game)
}

/// We are not sitting for a penalty double of our own (see [`Interpretation`])
///
/// A game force forbids passing below game — *unless* we are penalizing the
/// opponents, where passing their doubled contract out is the whole point.
/// There the forced-to-game rules step aside and let the natural defense —
/// including the [advance][advancing_a_double] of partner's penalty double —
/// govern.
fn not_penalizing() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| !Interpretation::read(context).penalizing)
}

/// The opponents have made nothing but passes (see [`Context::undisturbed`])
fn undisturbed() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| context.undisturbed())
}

/// Enable or disable the penalty-double latch on the current thread
///
/// **On by default** (DD-measured a clear penalty-X-bucket win with no regression:
/// self-play X bucket −0.621 → −0.464 IMPs/action-board, vs BBA −2.716 → −2.329
/// IMPs/X-board).  The human "once penalty, always penalty" rule: after our side's
/// natural penalty double of their 1NT ([`penalty_x_reading`]), our later doubles
/// read as **penalty** — we double their runout on a trump stack rather than for
/// takeout on shortness, and partner leaves our double in rather than advancing it.
/// Keyed off the one penalty double the floor classifies today, so it is a no-op
/// unless the natural defense is on.  Disable for the off arm of the A/B.  Read at
/// classification time, per-thread.
///
/// [`penalty_x_reading`]: super::inference::penalty_x_reading
#[doc(hidden)]
pub fn set_penalty_latch(enabled: bool) {
    PENALTY_LATCH.with(|flag| flag.set(enabled));
}

/// Our side has latched into a penalty stance: we made the natural penalty double
/// of their 1NT earlier this auction and have bid no contract since
///
/// Hand-independent — it follows from the calls alone.  Same-side only (the
/// opponents' penalty doubles do not latch us).  Once we penalty-double their 1NT
/// the penalty stance holds for the rest of the auction — "once penalty, always
/// penalty" — even after our side bids a suit of its own.  Gated on
/// [`set_penalty_latch`], so it is dormant by default.
fn penalty_latched(context: &Context<'_>) -> bool {
    if !PENALTY_LATCH.with(Cell::get) {
        return false;
    }
    let auction = context.auction();
    let Some(double_index) = super::inference::penalty_x_reading(auction) else {
        return false;
    };
    // The doubler shares the player-to-act's parity (our side).
    double_index % 2 == auction.len() % 2
}

/// Whether the penalty-double latch is enabled (see [`set_penalty_latch`])
///
/// Exposed for the inference walk's matching reading
/// ([`penalty_latch_double_reading`][super::inference]), which must agree with the
/// floor on when a later double is penalty rather than takeout.
pub(super) fn penalty_latch_enabled() -> bool {
    PENALTY_LATCH.with(Cell::get)
}

/// [`penalty_latched`] as a hand-ignoring [`Constraint`] for the ladder
fn penalty_latched_c() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| penalty_latched(context))
}

/// Suppress the doubler's constructive pulls of its own penalty double of their
/// 1NT (**on by default**)
///
/// Independent of the latch's double-handling: this only stops the *bids* (the
/// natural suit / notrump overcalls), so the doubler defends or latch-doubles
/// instead of "competing" to 2NT/3NT/a major opposite a likely-broke partner.
/// A no-op unless the latch is on (the penalty stance it keys off, see
/// [`penalty_latched`]).  Read at classification time, per-thread.
///
/// DD-measured against BBA's 2/1 on the isolated 1NT-defense match (8000 we-defend
/// boards/seed): the penalty-X bucket goes −2.312 → −1.013 IMPs/X-board vulnerable
/// (paired +0.058 IMPs/board overall, 95% CI [+0.030, +0.085]) and is neutral
/// non-vulnerable (+0.007, CI straddles 0); the swing is isolated to the X bucket.
/// Disable for the off arm of the A/B.
#[doc(hidden)]
pub fn set_penalty_no_pull(enabled: bool) {
    PENALTY_NO_PULL.with(|flag| flag.set(enabled));
}

/// The doubler may make a constructive overcall: either the no-pull knob is off,
/// or we are not in the penalty stance ([`penalty_latched`]).  Gates the
/// overcall-shaped rules that fire off [`we_have_not_bid`] (a double is not a bid).
fn may_pull_penalty() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| {
        !(PENALTY_NO_PULL.with(Cell::get) && penalty_latched(context))
    })
}

/// The penalty latch is *not* in force (the takeout-double default applies)
fn not_penalty_latched() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| !penalty_latched(context))
}

/// Select what a latched later double means for the current thread (live-read)
///
/// [`LatchStyle::Penalty`] (the **default**) is the stack-and-sit penalty double;
/// [`LatchStyle::Optional`] is the 2-3-card cooperative double.  A live-read
/// instinct flag (like [`set_penalty_latch`]), so the A/B harness sets it per
/// worker thread.
#[doc(hidden)]
pub fn set_latch_style(style: LatchStyle) {
    LATCH_STYLE.with(|cell| cell.set(style));
}

/// The latched double is the cooperative *optional* style (see [`LatchStyle`])
fn latch_optional_c() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| LATCH_STYLE.with(Cell::get) == LatchStyle::Optional)
}

/// The latched double is the pure *penalty* style (the default; see [`LatchStyle`])
fn latch_penalty_c() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, _: &Context<'_>| LATCH_STYLE.with(Cell::get) == LatchStyle::Penalty)
}

/// Enable or disable the advancer's runout from their redoubled penalty double
///
/// **On by default.**  After our natural penalty double of their 1NT, their
/// business redouble (`[1NT, X, XX]`) marks their side with the values, so a weak
/// advancer escapes to its long suit rather than sit for a making `1NTxx`.  The
/// mirror of the [responder runout][`set_one_nt_runout`] on the defensive side.
/// Disable for the off arm of the A/B; read at classification time, per-thread.
#[doc(hidden)]
pub fn set_advancer_xx_runout(enabled: bool) {
    ADVANCER_XX_RUNOUT.with(|flag| flag.set(enabled));
}

/// Their redoubled penalty double is back to a weak advancer (`[1NT, X, XX]`) and
/// the runout is enabled — the defensive mirror of [`responder_one_nt_runout_now`]
///
/// Keyed off [`penalty_x_reading`][super::inference::penalty_x_reading]: our side
/// penalty-doubled their 1NT, their next call was the redouble, and it is now the
/// doubler's partner (the advancer) to act for the first time.
fn advancer_xx_runout_now(context: &Context<'_>) -> bool {
    if !ADVANCER_XX_RUNOUT.with(Cell::get) {
        return false;
    }
    let auction = context.auction();
    let Some(x_index) = super::inference::penalty_x_reading(auction) else {
        return false;
    };
    auction.len() == x_index + 2
        && auction.last() == Some(&Call::Redouble)
        && x_index % 2 == auction.len() % 2
}

/// [`advancer_xx_runout_now`] as a hand-ignoring [`Constraint`] for the ladder
fn advancer_xx_runout() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| advancer_xx_runout_now(context))
}

/// Enable or disable the *doubler's* runout from their redoubled penalty double
///
/// **On by default.**  After `[1NT, X, XX]` the opponents' business redouble runs
/// back around — advancer passes, opener passes (`[1NT, X, XX, P, P]`) — to the 15+
/// doubler.  On, a doubler holding a five-plus-card suit escapes to it rather than
/// defend a likely-making `1NTxx`; off, it sits.  Read once at book construction
/// (the escape rule is added only when on) so a duplicate A/B isolates cleanly.
#[doc(hidden)]
pub fn set_doubler_xx_runout(enabled: bool) {
    DOUBLER_XX_RUNOUT.with(|flag| flag.set(enabled));
}

/// Whether the doubler's runout rule is authored into the current book
fn doubler_xx_runout_enabled() -> bool {
    DOUBLER_XX_RUNOUT.with(Cell::get)
}

/// Their redoubled penalty double has run back to the doubler (`[1NT, X, XX, P, P]`)
///
/// Keyed off [`penalty_x_reading`][super::inference::penalty_x_reading] like
/// [`advancer_xx_runout_now`], but two calls later: the business redouble, then the
/// advancer's and opener's passes, leaving the doubler to act for the first time
/// since the double.  Pure on the auction (the flag gates the rule at construction).
fn doubler_xx_runout_now(context: &Context<'_>) -> bool {
    let auction = context.auction();
    let Some(x_index) = super::inference::penalty_x_reading(auction) else {
        return false;
    };
    auction.len() == x_index + 4
        && auction[x_index + 1] == Call::Redouble
        && auction[x_index + 2] == Call::Pass
        && auction[x_index + 3] == Call::Pass
}

/// [`doubler_xx_runout_now`] as a hand-ignoring [`Constraint`] for the ladder
fn doubler_xx_runout() -> Cons<impl Constraint + Clone> {
    pred(|_: Hand, context: &Context<'_>| doubler_xx_runout_now(context))
}

/// We opened the strong notrump of `nt_level` and partner just transferred with
/// the call `from` — the cue to complete the transfer
fn partner_transferred_now(context: &Context<'_>, from: Bid, nt_level: u8) -> bool {
    our_strong_notrump(context, nt_level, false)
        && partner_last_call(context.auction()) == Some(from)
}

/// [`partner_transferred_now`] as a hand-ignoring [`Constraint`] for the ladder
fn partner_transferred(from: Bid, nt_level: u8) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| partner_transferred_now(context, from, nt_level))
}

/// The transfers instinct completes opposite our own strong notrump, each
/// `(nt_level, partner's artificial call, completion)`
///
/// Standard Jacoby (2♦/2♥ over 1NT, 3♦/3♥ over 2NT) and South African Texas
/// (4♣/4♦).  Shared by the ladder's completion rules and the [`forced`] rail
/// predicate so the two never disagree on which transfers are in force.
const TRANSFERS: [(u8, Bid, Bid); 6] = [
    (
        1,
        Bid::new(2, Strain::Diamonds),
        Bid::new(2, Strain::Hearts),
    ),
    (1, Bid::new(2, Strain::Hearts), Bid::new(2, Strain::Spades)),
    (1, Bid::new(4, Strain::Clubs), Bid::new(4, Strain::Hearts)),
    (
        1,
        Bid::new(4, Strain::Diamonds),
        Bid::new(4, Strain::Spades),
    ),
    (
        2,
        Bid::new(3, Strain::Diamonds),
        Bid::new(3, Strain::Hearts),
    ),
    (2, Bid::new(3, Strain::Hearts), Bid::new(3, Strain::Spades)),
];

/// An auction-determined forced situation: partner's live takeout double, a
/// prior call committing our side to game, or partner's just-made transfer over
/// our strong notrump
///
/// Hand-independent — it follows from the calls alone.  The neural safety shell
/// consults it to decide when to delegate to the deterministic [`instinct()`]
/// ladder instead of trusting the learned net: the net handles the judgement
/// middle, but never these forced rails.  Hand-conditioned forces (a
/// strong-notrump responder who holds game values) are deliberately excluded —
/// they are judgement the net is trusted with, measured on the harness.
#[cfg(feature = "neural-floor")]
pub(crate) fn forced(context: &Context<'_>) -> bool {
    advancing_a_double_now(context)
        || Interpretation::read(context).forced_to_game
        || TRANSFERS
            .iter()
            .any(|&(nt_level, from, _)| partner_transferred_now(context, from, nt_level))
}

/// The opponents opened a one-level suit `X`, and our side answered with a
/// *simple* (non-jump) suit overcall `Y` — the setting for Rubens advances
///
/// Returns `(X, Y, overcall index, overcall level)`.  Only a one-level opening
/// and a non-jump overcall qualify: a jump overcall is preemptive (advance it
/// naturally, like over a preempt), and a preemptive opening leaves no room.  A
/// cue-bid (`Y == X`) is not a natural overcall.  Shared with [`Inferences`] so
/// the bidding and the reading agree on which calls are Rubens transfers.
///
/// [`Inferences`]: super::inference::Inferences
pub(crate) fn overcall_shape(auction: &[Call]) -> Option<(Suit, Suit, usize, u8)> {
    let (open_index, opening) = opening_bid(auction)?;
    let x = opening.strain.suit()?;
    if opening.level.get() != 1 {
        return None;
    }
    let opening_side = open_index % 2;
    let overcall_index =
        (open_index + 1..auction.len()).find(|&i| matches!(auction[i], Call::Bid(_)))?;
    // The first bid after the opening must be the *other* side's — an overcall,
    // not the opening side bidding on.
    if overcall_index % 2 == opening_side {
        return None;
    }
    // ... and the side's FIRST action: a double before the bid makes this an
    // advance-of-double structure, not an overcall advance — the doubler's
    // later cue of the opening is a strong raise, never a Rubens transfer
    // (A/B'd: transfer-detecting these tails died in unauthored passouts).
    if auction[open_index + 1..overcall_index].contains(&Call::Double) {
        return None;
    }
    let Call::Bid(overcall) = auction[overcall_index] else {
        return None;
    };
    let y = overcall.strain.suit()?;
    if y == x {
        return None;
    }
    // A simple overcall sits at the cheapest level: one when above the opening,
    // two when below it.  Anything higher is a (preemptive) jump.
    let simple = if (y as u8) > (x as u8) { 1 } else { 2 };
    (overcall.level.get() == simple).then_some((x, y, overcall_index, simple))
}

/// We are advancing partner's simple overcall, RHO having passed: the auction is
/// `(1X) Y (Pass)` to us
///
/// Returns `(X = the cue suit, Y = partner's overcall, overcall level)`.
fn advance_of_overcall(context: &Context<'_>) -> Option<(Suit, Suit, u8)> {
    let auction = context.auction();
    let (x, y, overcall_index, level) = overcall_shape(auction)?;
    (overcall_index + 2 == auction.len() && auction[auction.len() - 1] == Call::Pass)
        .then_some((x, y, level))
}

/// `2 source` is a Rubens transfer over partner's one-level overcall: the band
/// `X ≤ source < Y`, transferring to the next suit up
///
/// `into_partner` selects the transfer that lands in partner's suit `Y` (a
/// limit-plus raise) over a new-suit transfer (advancer's own five-card suit).
fn rubens_transfer(source: Suit, into_partner: bool) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        rubens_advances_enabled()
            && advance_of_overcall(context).is_some_and(|(x, y, level)| {
                level == 1
                    && (x as u8) <= (source as u8)
                    && (source as u8) < (y as u8)
                    && (source as u8 + 1 == y as u8) == into_partner
            })
    })
}

/// `2 cue` is the Rubens cue-raise over partner's simple *two-level* overcall —
/// a limit-plus raise, the cue being the opponents' suit `X`
fn rubens_cue_raise(cue: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        rubens_advances_enabled()
            && advance_of_overcall(context)
                .is_some_and(|(x, _, level)| level == 2 && x as u8 == cue as u8)
    })
}

/// Partner answered our simple one-level overcall with a Rubens transfer, RHO
/// passing — the cue to complete it
///
/// Returns the suit to complete into: the suit just above partner's transfer.
/// Mechanical (hand-independent), like completing a transfer over our own
/// notrump — see [`TRANSFERS`].
fn rubens_completion(context: &Context<'_>) -> Option<Suit> {
    if !rubens_advances_enabled() {
        return None;
    }
    let auction = context.auction();
    let len = auction.len();
    let (x, y, overcall_index, level) = overcall_shape(auction)?;
    // Only a one-level overcall carries the transfer ladder; the sequence is
    // overcall, (pass), transfer, (pass or opener's lead-directing double), us.
    // Completing through the double matters: without it the relay dies and the
    // advancer plays the phantom suit doubled (A/B'd −14 IMPs a board).
    if level != 1
        || overcall_index + 4 != len
        || auction[overcall_index + 1] != Call::Pass
        || !matches!(auction[len - 1], Call::Pass | Call::Double)
    {
        return None;
    }
    let Call::Bid(transfer) = auction[overcall_index + 2] else {
        return None;
    };
    let source = transfer.strain.suit()?;
    (transfer.level.get() == 2 && (x as u8) <= (source as u8) && (source as u8) < (y as u8))
        .then(|| Suit::ASC[(source as u8 + 1) as usize])
}

/// [`rubens_completion`] as a [`Constraint`]: complete into `target`
fn rubens_completes(target: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| rubens_completion(context) == Some(target))
}

/// Partner answered our simple two-level overcall with the Rubens cue-raise,
/// opener passing or doubling — we must place the contract
///
/// The cue names the *opponents'* suit, so passing it out plays their suit at
/// the two level (A/B'd the dominant Rubens leak: a quarter of the divergent
/// boards died in this passout).  Returns `(X = their suit, Y = our suit)`.
fn rubens_cue_answer(context: &Context<'_>) -> Option<(Suit, Suit)> {
    if !rubens_advances_enabled() {
        return None;
    }
    let auction = context.auction();
    let len = auction.len();
    let (x, y, overcall_index, level) = overcall_shape(auction)?;
    // The sequence is overcall, (pass), cue, (pass or double), us.
    if level != 2
        || overcall_index + 4 != len
        || auction[overcall_index + 1] != Call::Pass
        || !matches!(auction[len - 1], Call::Pass | Call::Double)
    {
        return None;
    }
    (auction[overcall_index + 2] == Call::Bid(Bid::new(2, Strain::from(x)))).then_some((x, y))
}

/// [`rubens_cue_answer`] as a [`Constraint`]: our overcall suit is `y`
fn rubens_cue_answers(y: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        rubens_cue_answer(context).is_some_and(|(_, own)| own == y)
    })
}

/// Partner's transfer lands in our own overcall suit `y` — the limit-plus
/// raise — and we are on completion duty ([`rubens_completion`])
///
/// The seat that grades the overcall against the shown ten-plus: complete
/// `2Y` with a minimum, super-accept `3Y` in between, break to game with a
/// maximum — the separation Rubens buys over a flat natural raise is cashed
/// here or nowhere.
fn rubens_into_partner(y: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        rubens_completion(context) == Some(y)
            && overcall_shape(context.auction()).is_some_and(|(_, own, _, _)| own == y)
    })
}

/// Partner mechanically completed our transfer into its own suit `y` — the
/// limit-plus raise rests at `2Y` unless we hold extras
///
/// The auction is `(1X) Y (P) 2S (P|X) 2Y (P|X)` back to us: the completion
/// limited partner to no super-accept, so only extras beyond our shown
/// ten-plus move again.
fn rubens_raiser_rebid(context: &Context<'_>) -> Option<Suit> {
    if !rubens_advances_enabled() {
        return None;
    }
    let auction = context.auction();
    let len = auction.len();
    let (x, y, overcall_index, level) = overcall_shape(auction)?;
    if level != 1 || overcall_index + 6 != len || auction[overcall_index + 1] != Call::Pass {
        return None;
    }
    // Our transfer into partner's suit…
    let Call::Bid(transfer) = auction[overcall_index + 2] else {
        return None;
    };
    let source = transfer.strain.suit()?;
    if transfer.level.get() != 2 || (source as u8) < (x as u8) || source as u8 + 1 != y as u8 {
        return None;
    }
    // …mechanically completed (opener may have doubled either turn).
    (matches!(auction[overcall_index + 3], Call::Pass | Call::Double)
        && auction[overcall_index + 4] == Call::Bid(Bid::new(2, Strain::from(y)))
        && matches!(auction[len - 1], Call::Pass | Call::Double))
    .then_some(y)
}

/// [`rubens_raiser_rebid`] as a [`Constraint`]: partner's suit is `y`
fn rubens_raiser_rebids(y: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| rubens_raiser_rebid(context) == Some(y))
}

/// Partner mechanically completed our *new-suit* transfer into `target` — the
/// wide-yet-unlimited hand clarifies now
///
/// The auction is `(1X) Y (P) 2S (P|X) 2(S+1) (P|X)` back to us with
/// `S+1 ≠ Y`: the transfer showed the suit cheaply without settling forcing
/// questions, so a mild hand passes the completion and extras move again.
fn rubens_transferee_rebid(context: &Context<'_>) -> Option<Suit> {
    if !rubens_advances_enabled() {
        return None;
    }
    let auction = context.auction();
    let len = auction.len();
    let (x, y, overcall_index, level) = overcall_shape(auction)?;
    if level != 1 || overcall_index + 6 != len || auction[overcall_index + 1] != Call::Pass {
        return None;
    }
    let Call::Bid(transfer) = auction[overcall_index + 2] else {
        return None;
    };
    let source = transfer.strain.suit()?;
    // Band first: `source < y` also keeps the `source + 1` index in range.
    if transfer.level.get() != 2 || (source as u8) < (x as u8) || (source as u8) >= (y as u8) {
        return None;
    }
    let target = Suit::ASC[(source as u8 + 1) as usize];
    if target == y {
        return None;
    }
    (matches!(auction[overcall_index + 3], Call::Pass | Call::Double)
        && auction[overcall_index + 4] == Call::Bid(Bid::new(2, Strain::from(target)))
        && matches!(auction[len - 1], Call::Pass | Call::Double))
    .then_some(target)
}

/// [`rubens_transferee_rebid`] as a [`Constraint`]: our shown suit is `target`
fn rubens_transferee_rebids(target: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| rubens_transferee_rebid(context) == Some(target))
}

/// Partner's transfer names a *new* suit `target` and we are on completion
/// duty ([`rubens_completion`])
///
/// The mechanical completion covers exactly the hands that would have passed a
/// natural non-forcing `2 target`; a hand good enough to bid over that makes
/// the same descriptive bid here instead of completing.
fn rubens_new_suit_completion(target: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        rubens_completion(context) == Some(target)
            && overcall_shape(context.auction()).is_some_and(|(_, own, _, _)| own != target)
    })
}

/// `2 target` is a *natural* new-suit advance of partner's one-level overcall,
/// live only while Rubens advances are off ([`set_rubens_advances`])
///
/// The knob-off baseline: the natural five-card-suit overcall rule is anchored
/// on [`we_have_not_bid`], dead for the advancer, so without this the band
/// hands (own five-card suit between their suit and partner's) would have *no*
/// call and the A/B would measure Rubens against a pass, not against natural
/// advances.  Covers exactly the hand class of the new-suit
/// [`rubens_transfer`], at the same weight.
fn natural_new_suit_advance(target: Suit) -> Cons<impl Constraint + Clone> {
    pred(move |_: Hand, context: &Context<'_>| {
        !rubens_advances_enabled()
            && advance_of_overcall(context).is_some_and(|(x, y, level)| {
                level == 1 && (x as u8) < (target as u8) && (target as u8) < (y as u8)
            })
    })
}

/// Build the instinct ladder: a sane natural action for any auction
///
/// Forced (partner's live takeout double — see the [module docs][self]):
/// penalty pass on a trump stack, a major-suit game jump or 3NT with values,
/// the longest unbid suit at the cheapest level (majors and five-card suits
/// preferred), and a cheapest-notrump escape as the guaranteed action.
///
/// Otherwise: raise partner's suit with three-card support and rising
/// strength per level, overcall notrump (15–18 balanced with stoppers) or a
/// five-card suit if we have not bid, double their low suit bid for takeout
/// on shape (or any 17+), and pass.
///
/// The unconditioned pass at weight `-5` is the absolute last resort: it
/// keeps the logits finite when every action is illegal, while sitting far
/// enough below every forced action (≥ 3 nats) that sampling drivers never
/// pass a forced auction by accident.
#[must_use]
pub fn instinct() -> Rules {
    let mut rules = Rules::new()
        // Forced: a trump stack sits for partner's takeout double.
        .rule(Call::Pass, 1.5, advancing_a_double() & doubled_suit_stack())
        // Settle floor (opt-in): a takeout double is not 100% forcing.  With four
        // cards behind their doubled suit, *defend* — pass plays their doubled
        // contract.  Above the advance ladder (new suit ~1.0, raises 1.2) and the
        // 0.3 notrump escape, below the trump stack 1.5 and the game jumps 1.45.
        .rule(
            Call::Pass,
            1.35,
            settle_floor() & advancing_a_double() & doubled_suit_length(),
        )
        // Forced: 3NT to play with game values and their suits stopped.
        .rule(
            Bid::new(3, Strain::Notrump),
            1.3,
            advancing_a_double()
                & hcp(13..)
                & stopper_in_their_suits()
                & level_available(3, Strain::Notrump),
        )
        // Default unforced pass.  Under the settle floor it is also available in a
        // advance of partner's double (pass plays the top bid) — it still loses to every advance
        // rule, so a bust with no penalty advances as before.
        .rule(Call::Pass, 0.0, !advancing_a_double() | settle_floor())
        // The absolute last resort, keeping logits finite when all else is illegal.
        .rule(Call::Pass, -5.0, hcp(0..));

    // Forced: jump to a major-suit game with four-plus cards and values —
    // in an unbid major, never in the suit partner asked us to take out of.
    for major in [Suit::Hearts, Suit::Spades] {
        let strain = Strain::from(major);
        rules = rules.rule(
            Bid::new(4, strain),
            1.45,
            advancing_a_double()
                & len(major, 4..)
                & points(11..)
                & level_available(4, strain)
                & !they_bid(strain),
        );
    }

    for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
        let strain = Strain::from(suit);
        let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
            0.05
        } else {
            0.0
        };

        // Forced: a new suit at the cheapest level; longer suits and majors
        // are preferred.  Bidding their suit would be a cue-bid — excluded.  The
        // settle floor's `free_bid_gate` (off by default) makes the four-level new
        // suit a free bid — values and no existing fit.
        for level in 1u8..=4 {
            rules = rules
                .rule(
                    Bid::new(level, strain),
                    1.0 + major_bonus,
                    advancing_a_double()
                        & min_level_is(level, strain)
                        & len(suit, 4..)
                        & !they_bid(strain)
                        & free_bid_gate(level),
                )
                .rule(
                    Bid::new(level, strain),
                    1.1 + major_bonus,
                    advancing_a_double()
                        & min_level_is(level, strain)
                        & len(suit, 5..)
                        & !they_bid(strain)
                        & free_bid_gate(level),
                );
        }

        // Raise partner's suit with three-card support; each level up asks
        // for more strength, so competitive raises terminate by themselves.
        // `partner_shown_len(suit, 3..)` makes the raise trust the *reading*, not the
        // bid suit: an artificial overcall (Woolsey 2♣ = both majors, 2♦ = a major)
        // shows its named minor short, so the floor never raises the phantom suit
        // into a doubled disaster.  A natural overcall is shown 5+, so it is
        // unaffected.  See `inference::multi_reading`.
        for (level, threshold) in [(2u8, 6u8), (3, 10), (4, 13)] {
            rules = rules.rule(
                Bid::new(level, strain),
                1.2,
                partner_suit_is(suit)
                    & partner_shown_len(suit, 3..)
                    & min_level_is(level, strain)
                    & support(3..)
                    & points(threshold..),
            );
        }

        // Preemptive jump to game: five-card support but too weak to invite —
        // the weak distributional raise, distinct from the point-showing raises
        // above.  Now that the floor owns advances of an overcall, this is the
        // weak end the book's `advances` used to cover.
        rules = rules.rule(
            Bid::new(4, strain),
            1.3,
            partner_suit_is(suit)
                & partner_shown_len(suit, 3..)
                & support(5..)
                & hcp(..6)
                & level_available(4, strain),
        );

        // Overcall a five-card suit if we have not bid; the strength floor
        // rises with the level and stronger hands double first.
        for (level, floor) in [(1u8, 8u8), (2, 10), (3, 13)] {
            rules = rules.rule(
                Bid::new(level, strain),
                1.0 + major_bonus,
                we_have_not_bid()
                    & may_pull_penalty()
                    & min_level_is(level, strain)
                    & len(suit, 5..)
                    & points(floor..=16)
                    & !they_bid(strain),
            );
        }
    }

    // Runout after our 1NT is doubled (default on; `set_one_nt_runout`).  A weak
    // responder escapes to its longest five-plus-card suit rather than sit for
    // the (effectively penalty) double; the values end redoubles and opener
    // passes the escape — both rules below.  The run/XX boundary is the
    // `set_runout_xx_min` knob (raw HCP), measured best near 7.
    //
    // The both-minor 2NT action (`set_unusual_2nt`) and the penalty double of
    // the opponents' escape (`set_penalize_escape_stack` / `_values`) are
    // authored below as A/B knobs; see the `ab-one-nt-runout --compare` axes.
    for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
        let strain = Strain::from(suit);
        let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
            0.05
        } else {
            0.0
        };
        rules = rules
            .rule(
                Bid::new(2, strain),
                1.0 + major_bonus,
                one_nt_runout_enabled()
                    & responder_one_nt_runout()
                    & len(suit, 5..)
                    & hcp(..RUNOUT_MAX_HCP),
            )
            .rule(
                Bid::new(2, strain),
                1.1 + major_bonus,
                one_nt_runout_enabled()
                    & responder_one_nt_runout()
                    & len(suit, 6..)
                    & hcp(..RUNOUT_MAX_HCP),
            );
    }

    // Advancer's runout from their redoubled penalty double (`[1NT, X, XX]`,
    // default on; `set_advancer_xx_runout`).  Their XX is business, so a weak
    // advancer escapes to its longest five-plus-card suit instead of sitting for a
    // making `1NTxx` — the defensive mirror of the responder runout above.  A
    // values advancer (>= `RUNOUT_MAX_HCP`) passes to defend `1NTxx` instead.
    // ponytail: five-plus suits only; a 4-4 bust still sits — add the both-minors
    // escape (cf. the `2NT` rule below) if the A/B asks for it.
    for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
        let strain = Strain::from(suit);
        let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
            0.05
        } else {
            0.0
        };
        rules = rules
            .rule(
                Bid::new(2, strain),
                1.0 + major_bonus,
                advancer_xx_runout() & len(suit, 5..) & hcp(..RUNOUT_MAX_HCP),
            )
            .rule(
                Bid::new(2, strain),
                1.1 + major_bonus,
                advancer_xx_runout() & len(suit, 6..) & hcp(..RUNOUT_MAX_HCP),
            );
    }

    // Doubler's runout once the redouble runs back around (`[1NT, X, XX, P, P]`,
    // on by default; `set_doubler_xx_runout`).  Unlike the advancer, the doubler is
    // the 15+ penalty hand, so there is *no* HCP cap — a doubler holding a five-plus
    // suit (a 5332 under the default balanced gate) escapes the redoubled `1NTxx`
    // rather than defend it; a 4-3-3-3/4-4-3-2 bust has nowhere to run and sits.
    // Construction-gated so the off arm of a duplicate A/B never carries the rule.
    if doubler_xx_runout_enabled() {
        for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
            let strain = Strain::from(suit);
            let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
                0.05
            } else {
                0.0
            };
            rules = rules
                .rule(
                    Bid::new(2, strain),
                    1.0 + major_bonus,
                    doubler_xx_runout() & len(suit, 5..),
                )
                .rule(
                    Bid::new(2, strain),
                    1.1 + major_bonus,
                    doubler_xx_runout() & len(suit, 6..),
                );
        }
    }

    // Runout, the values end: responder redoubles to play 1NT-XX rather than
    // run.  Outranks the escape so a values hand with a long suit still sits for
    // the (re)double; stays below the 1.40 game milestone so a game-going hand
    // bids it instead.  Opener then passes (1NT-XX) or bids game off the floor.
    rules = rules.rule(
        Call::Redouble,
        1.2,
        one_nt_runout_enabled() & responder_one_nt_runout() & responder_has_xx_values(),
    );

    // Runout, the both-minors end: 2NT = unusual, four-four in the minors with
    // no five-card suit to run to (a 5+ suit prefers the natural escape, which
    // outweighs this; a five-card major is excluded).  Opener picks the better
    // minor.  Weight below the suit escape, above Pass — a 4-4 minor bust escapes
    // 1NT-X to a known eight-card fit rather than sit.  Opt-in only: the default
    // `Direct` mode runs straight to a minor (below); A/B'd the relay a loser.
    rules = rules.rule(
        Bid::new(2, Strain::Notrump),
        0.5,
        one_nt_runout_enabled()
            & responder_one_nt_runout()
            & !unusual_2nt_is(Unusual2nt::Direct)
            & hcp(..RUNOUT_MAX_HCP)
            & len(Suit::Clubs, 4..)
            & len(Suit::Diamonds, 4..)
            & len(Suit::Hearts, ..5)
            & len(Suit::Spades, ..5),
    );

    // Runout, 2NT extended (`set_unusual_2nt(FiveFiveAdd)`): a five-five-minors
    // hand bids 2NT too, above the natural minor escape (1.0/1.1), so opener
    // picks the better fit instead of responder guessing a minor.  A five-five
    // hand cannot hold a five-card major, so no major guard is needed.
    rules = rules.rule(
        Bid::new(2, Strain::Notrump),
        1.15,
        one_nt_runout_enabled()
            & responder_one_nt_runout()
            & unusual_2nt_is(Unusual2nt::FiveFiveAdd)
            & hcp(..RUNOUT_MAX_HCP)
            & len(Suit::Clubs, 5..)
            & len(Suit::Diamonds, 5..),
    );

    // Runout, the direct escape (`set_unusual_2nt(Direct)`, the default): no 2NT
    // relay — a weak four-four-minors bust bids its longer minor (ties to
    // diamonds) at the two level, one double-exposure instead of the relay's two.
    // Opener passes it like any escape (`opener_after_one_nt_runout`, above).
    let direct_bust = one_nt_runout_enabled()
        & responder_one_nt_runout()
        & unusual_2nt_is(Unusual2nt::Direct)
        & hcp(..RUNOUT_MAX_HCP)
        & len(Suit::Clubs, 4..)
        & len(Suit::Diamonds, 4..)
        & len(Suit::Hearts, ..5)
        & len(Suit::Spades, ..5);
    rules = rules
        .rule(
            Bid::new(2, Strain::Diamonds),
            1.0,
            direct_bust.clone() & longer_diamonds(),
        )
        .rule(
            Bid::new(2, Strain::Clubs),
            1.0,
            direct_bust & !longer_diamonds(),
        );

    // Opener passes partner's runout: responder ran because it is weak, so it
    // captains the auction.  Weight outranks the natural raise *and* the 1.5
    // transfer completion — without it a 2♦/2♥ escape is misread as a Jacoby
    // transfer and opener "completes" it into responder's short suit.
    // ponytail: always pass; pulling to a better suit on a misfit is deferred.
    rules = rules.rule(
        Call::Pass,
        1.55,
        one_nt_runout_enabled() & opener_after_one_nt_runout(),
    );

    // Opener answers partner's 2NT minors-scramble with the better minor (longer,
    // ties to diamonds).  Weight outranks the 1.5 transfer completion — the floor
    // reads 2NT as a diamond transfer, which would force a club-longer hand to 3♦.
    rules = rules
        .rule(
            Bid::new(3, Strain::Diamonds),
            1.6,
            one_nt_runout_enabled() & opener_after_one_nt_minors() & longer_diamonds(),
        )
        .rule(
            Bid::new(3, Strain::Clubs),
            1.6,
            one_nt_runout_enabled() & opener_after_one_nt_minors() & !longer_diamonds(),
        );

    // Universal runout, opener's balancing seat (`set_one_nt_runout_universal`).
    // The double came back to opener with a weak partner (it had no escape), so
    // 1NT-X rates to fail: opener runs its own five-plus-card suit rather than
    // sit — but only minimum-ish, since a maximum still rates to make 1NT-X.
    for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
        let strain = Strain::from(suit);
        let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
            0.05
        } else {
            0.0
        };
        rules = rules
            .rule(
                Bid::new(2, strain),
                1.0 + major_bonus,
                one_nt_runout_enabled()
                    & one_nt_runout_universal()
                    & opener_balancing_runout()
                    & len(suit, 5..)
                    & hcp(..17),
            )
            .rule(
                Bid::new(2, strain),
                1.1 + major_bonus,
                one_nt_runout_enabled()
                    & one_nt_runout_universal()
                    & opener_balancing_runout()
                    & len(suit, 6..)
                    & hcp(..17),
            );
    }

    // Balancing redouble = SOS: no five-card suit to run to and not a maximum —
    // ask partner to pick a suit, four-card suits included.
    rules = rules.rule(
        Call::Redouble,
        1.0,
        one_nt_runout_enabled()
            & one_nt_runout_universal()
            & opener_balancing_runout()
            & hcp(..17)
            & len(Suit::Clubs, ..5)
            & len(Suit::Diamonds, ..5)
            & len(Suit::Hearts, ..5)
            & len(Suit::Spades, ..5),
    );

    // Responder answers the SOS redouble with its longest suit (four-card OK).
    for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
        let strain = Strain::from(suit);
        let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
            0.05
        } else {
            0.0
        };
        for (length, weight) in [(4usize, 1.0f32), (5, 1.1), (6, 1.2)] {
            rules = rules.rule(
                Bid::new(2, strain),
                weight + major_bonus,
                one_nt_runout_enabled()
                    & one_nt_runout_universal()
                    & responder_after_opener_sos()
                    & len(suit, length..),
            );
        }
    }

    // Opener passes responder's SOS answer — responder captains the rescue.
    // Outranks the natural raise and the transfer completion, as elsewhere.
    rules = rules.rule(
        Call::Pass,
        1.55,
        one_nt_runout_enabled() & one_nt_runout_universal() & opener_after_responder_sos(),
    );

    // Encircling: the opponents ran from our doubled (or redoubled) 1NT.  We
    // hold the balance, so double their escape for penalty — and keep doubling
    // as they keep running — rather than let them buy it cheaply.  Two arms,
    // each an A/B knob: a trump stack in their suit (sound in any seat), or
    // general values once responder's business redouble has shown them.  Weight
    // outranks the floor's takeout double of the same suit (<=0.9).
    rules = rules
        .rule(
            Call::Double,
            1.6,
            one_nt_runout_enabled()
                & penalize_escape_stack_enabled()
                & opp_escaped_our_nt_undoubled()
                & doubled_suit_stack(),
        )
        .rule(
            Call::Double,
            1.6,
            one_nt_runout_enabled()
                & penalize_escape_values_enabled()
                & opp_escaped_our_business_xx()
                & hcp(7..),
        );

    // Partner leaves in our penalty double of their escape: it is penalty by
    // agreement, not the takeout the `advancing_a_double` default would advance.
    // Outranks every advance action (<=1.5).
    rules = rules.rule(
        Call::Pass,
        1.55,
        one_nt_runout_enabled() & leave_in_escape_penalty(),
    );

    // Penalty latch: once our side has penalty-doubled their 1NT, leave in any
    // later double of ours rather than advance it — "once penalty, always
    // penalty".  Outranks every advance action (<=1.5); the mirror of the runout
    // leave-in above, gated on its own A/B knob ([`set_penalty_latch`]).  The
    // optional latch style suppresses this forced sit: partner instead cooperates
    // (sit on a fit, run when short) via the general advance-a-double machinery.
    rules = rules.rule(
        Call::Pass,
        1.55,
        penalty_latched_c() & latch_penalty_c() & advancing_a_double(),
    );

    // UvU encircling: the opponents ran from our 1NT-(2NT)-X.  Double their
    // escape with a trump stack — and keep doubling as they keep running — by
    // agreement; partner leaves in.  Mirrors the doubled-1NT escape chase above,
    // gated on its own A/B knob ([`set_uvu_encircle`]), independent of the runout.
    rules = rules
        .rule(
            Call::Double,
            1.6,
            uvu_encircle_enabled() & opp_escaped_our_uvu_undoubled() & doubled_suit_stack(),
        )
        .rule(
            Call::Pass,
            1.55,
            uvu_encircle_enabled() & leave_in_uvu_penalty(),
        );

    for level in 1u8..=4 {
        // Forced: the notrump escape guarantees an action — no fit, no
        // stopper, no four-card suit outside theirs still has a call.
        rules = rules.rule(
            Bid::new(level, Strain::Notrump),
            0.3,
            advancing_a_double() & min_level_is(level, Strain::Notrump),
        );
    }

    for level in 1u8..=3 {
        // Notrump overcall: 15–18 balanced with their suits stopped.
        rules = rules.rule(
            Bid::new(level, Strain::Notrump),
            1.05,
            we_have_not_bid()
                & may_pull_penalty()
                & min_level_is(level, Strain::Notrump)
                & balanced()
                & hcp(15..=18)
                & stopper_in_their_suits(),
        );
    }

    // Opposite our own strong notrump: complete partner's transfer.  Standard
    // Jacoby (2♦/2♥, 3♦/3♥ over 2NT) and South African Texas (4♣/4♦); the book
    // authors these where it can, so this only catches off-book and competitive
    // continuations.  Bid the suit just above partner's artificial call.
    for (nt_level, from, to) in TRANSFERS {
        rules = rules.rule(
            to,
            1.5,
            partner_transferred(from, nt_level) & level_available(to.level.get(), to.strain),
        );
    }

    // Game values.  Three strands force game regardless of the point estimate:
    // the hand-conditioned strong-notrump responder forces (10+ opposite a 15–17
    // 1NT, 5+ opposite a 20–21 2NT), and the hand-independent forces from the
    // auction interpretation — a strong 2♣ past the double negative, or an opener
    // forced past invitation.  A fourth strand is *general*: our own count plus
    // the sound floor of partner's shown points reaching 25 (the inference makes
    // it sound, never an overbid).  Below game we take the cheapest milestone — a
    // known major fit, else 3NT with their suits stopped, dropping to the minor
    // game only when their suit is unstopped — but step aside when penalizing the
    // opponents.  The 3NT stopper guard is vacuous uncontested (no suit of theirs
    // to stop), so it changes only competitive auctions: never a notrump game bid
    // into an unstopped enemy suit.
    let game_values = ((partner_strong_notrump(1)
        & (hcp(10..) | (hcp(nt_responder_game_floor()..) & undisturbed())))
        | (partner_strong_notrump(2) & hcp(5..))
        | auction_forces_game()
        | combined_points(25))
        & not_penalizing();
    rules = rules.rule(
        Bid::new(3, Strain::Notrump),
        1.40,
        game_values.clone()
            & below_game()
            & stopper_in_their_suits()
            & nt_game_force_3nt_allowed()
            & level_available(3, Strain::Notrump),
    );
    // Gambling 3NT over a double of our 1NT (opt-in; `set_gambling_3nt_over_double`).
    // A long (6+) minor, semi-solid, with an outside ace by default — responder runs
    // its suit opposite the 15–17 opener rather than defend the redouble or escape.
    // Split per minor so the build-time `len(minor, 6..)` floors the *named* suit in
    // the projection; `.alert(GAMBLING_3NT)` marks the call artificial so the reader
    // suppresses the natural balanced-3NT reading and the sampler stops dealing
    // responder flat.  Weight 1.45 outranks the business XX (1.2) and the escapes
    // (≤1.1); a balanced strong hand holds no 6-card minor and still redoubles.
    for minor in [Suit::Clubs, Suit::Diamonds] {
        rules = rules
            .rule(
                Bid::new(3, Strain::Notrump),
                1.45,
                one_nt_runout_enabled()
                    & responder_one_nt_runout()
                    & gambling_3nt_authored()
                    & len(minor, 6..)
                    & gambling_3nt_semisolid(minor)
                    & gambling_3nt_suit_ace(minor)
                    & level_available(3, Strain::Notrump),
            )
            .alert(GAMBLING_3NT);
    }
    for minor in [Suit::Clubs, Suit::Diamonds] {
        let strain = Strain::from(minor);
        // 3NT is the milestone of choice; reach for the minor game only when
        // notrump is unsafe (a suit they bid is unstopped) and we hold a known
        // eight-card fit.  Uncontested, their suits are vacuously stopped, so
        // this never fires and 3NT plays.
        let known_minor_fit = (len(minor, 5..) & partner_shown_len(minor, 3..))
            | (len(minor, 3..) & partner_shown_len(minor, 5..));
        rules = rules.rule(
            Bid::new(5, strain),
            1.42,
            game_values.clone()
                & below_game()
                & inference_aware()
                & known_minor_fit
                & !stopper_in_their_suits()
                & level_available(5, strain),
        );
    }
    for major in [Suit::Hearts, Suit::Spades] {
        let strain = Strain::from(major);
        // A *known* eight-card major fit outranks 3NT: our five-card suit meets
        // partner's shown three-card support, our three meet partner's shown
        // five, or our doubleton meets partner's shown six (a transferred suit
        // jumped or raised to game — see [`Inferences`]).  The shown lengths come
        // from the auction interpretation, so this fires only on a fit the calls
        // have promised.
        //
        // [`Inferences`]: super::inference::Inferences
        let known_major_fit = (len(major, 5..) & partner_shown_len(major, 3..))
            | (len(major, 3..) & partner_shown_len(major, 5..))
            | (len(major, 2..) & partner_shown_len(major, 6..));
        rules = rules.rule(
            Bid::new(4, strain),
            1.45,
            game_values.clone() & below_game() & len(major, 6..) & level_available(4, strain),
        );
        // Preemptive 4M over a double of our 1NT (opt-in; `set_preempt_4m_over_double`).
        // The major's mirror of the gambling 3NT: a *quality* long (6+) major —
        // semi-solid and headed by the trump ace (a sure trump trick that buffs total
        // tricks) — on a modest hand, partly preemptive and partly to make opposite the
        // strong notrump.  Natural (the bid major reads as 6+), so unalerted; the
        // game-values arm above still governs undisturbed and over an overcall.
        rules = rules.rule(
            Bid::new(4, strain),
            1.45,
            one_nt_runout_enabled()
                & responder_one_nt_runout()
                & preempt_4m_authored()
                & len(major, 6..)
                & preempt_4m_semisolid(major)
                & preempt_4m_trump_ace(major)
                & preempt_4m_values()
                & below_game()
                & level_available(4, strain),
        );
        rules = rules.rule(
            Bid::new(4, strain),
            1.50,
            game_values.clone()
                & below_game()
                & inference_aware()
                & known_major_fit.clone()
                & level_available(4, strain),
        );
        // Correct partner's choice-of-games 3NT to a known eight-card major fit —
        // but only undisturbed and with a ruffing doubleton.  Game is already
        // agreed, so this is a pure strain choice (no strength gate); opposite
        // responder's *balanced* transferred five the 5-3 fit out-scores notrump
        // only when the trump-short hand can ruff, so a flat 4-3-3-3 opener leaves
        // it in 3NT (`has_ruffing_shortness`).  `undisturbed` keeps it off contested
        // auctions, where the pull to the four level walks into a penalty double.
        rules = rules.rule(
            Bid::new(4, strain),
            1.50,
            correct_3nt_to_major_now()
                & undisturbed()
                & inference_aware()
                & known_major_fit.clone()
                & has_ruffing_shortness()
                & level_available(4, strain),
        );
        // Slam is a milestone too: with a known major fit and the combined
        // minimum in the small- (33) or grand- (37) slam zone, bid it.
        rules = rules.rule(
            Bid::new(6, strain),
            1.65,
            combined_points(33)
                & not_penalizing()
                & below_slam()
                & inference_aware()
                & known_major_fit.clone()
                & level_available(6, strain),
        );
        rules = rules.rule(
            Bid::new(7, strain),
            1.75,
            combined_points(37)
                & not_penalizing()
                & below_slam()
                & inference_aware()
                & known_major_fit
                & level_available(7, strain),
        );
    }
    // Notrump slam when no major fit is known: small at 33, grand at 37, with
    // their suits stopped (vacuous when uncontested).
    rules = rules
        .rule(
            Bid::new(6, Strain::Notrump),
            1.60,
            combined_points(33)
                & not_penalizing()
                & below_slam()
                & stopper_in_their_suits()
                & level_available(6, Strain::Notrump),
        )
        .rule(
            Bid::new(7, Strain::Notrump),
            1.70,
            combined_points(37)
                & not_penalizing()
                & below_slam()
                & stopper_in_their_suits()
                & level_available(7, Strain::Notrump),
        );

    // ------------------------------------------------------------------
    // M6.4: slam machinery on the floor — RKCB 1430 (instinct decoding
    // instinct on both sides) and control-bid signoffs.
    // ------------------------------------------------------------------
    // The ask: with a known eight-card fit and combined small-slam values,
    // ask for keycards before committing — outweighing the direct milestone
    // slams (1.65), because RKCB's point is staying *out* of the
    // two-keycards-missing slam.  Not over partner's notrump bid (partner
    // would read that 4NT quantitative), mirroring the answerer's gate; the
    // grand-zone 37 keeps bidding sevens directly (1.75 outweighs the ask).
    // The ask must also be *decodable*: the trump has to be a suit someone
    // genuinely showed five-plus of, or partner cannot derive it and the ask
    // gets passed out (an 8-count fit against a four-card Puppet answer is
    // known only to us — round 2 lost 11 IMPs a board on exactly that).
    rules = rules
        .rule(
            Bid::new(4, Strain::Notrump),
            1.68,
            pred(|hand: Hand, context: &Context<'_>| {
                floor_rkcb_now()
                    && context.undisturbed()
                    && keycard_trump(hand, context).is_some_and(|trump| {
                        let inferences = Inferences::read(context);
                        inferences
                            .me()
                            .length(trump)
                            .min
                            .max(inferences.partner().length(trump).min)
                            >= 5
                    })
                    && partner_last_call(context.auction())
                        .is_none_or(|bid| bid.strain != Strain::Notrump)
            }) & inference_aware()
                & combined_points(33)
                & not_penalizing()
                & below_slam()
                & level_available(4, Strain::Notrump),
        )
        .alert(RKCB_FLOOR)
        // The 1430 answers — forcing, so they outweigh every milestone.  5♣
        // also covers all five keycards (a 2♣ rock answering its raiser's
        // ask; the book ladder's {1,4} left that hand with *no* answer and
        // round 3 passed the 4NT out).
        .rule(
            Bid::new(5, Strain::Clubs),
            1.9,
            keycard_answer(&[1, 4, 5], None),
        )
        .alert(RKCB_FLOOR)
        .rule(
            Bid::new(5, Strain::Diamonds),
            1.9,
            keycard_answer(&[0, 3], None),
        )
        .alert(RKCB_FLOOR)
        .rule(
            Bid::new(5, Strain::Hearts),
            1.9,
            keycard_answer(&[2], Some(false)),
        )
        .alert(RKCB_FLOOR)
        .rule(
            Bid::new(5, Strain::Spades),
            1.9,
            keycard_answer(&[2], Some(true)),
        )
        .alert(RKCB_FLOOR)
        // After our answer the asker holds the count: respect the placement.
        .rule(Call::Pass, 1.88, respect_keycard_signoff());
    // The asker's continuations, per possible trump.
    for trump in Suit::ASC {
        let strain = Strain::from(trump);
        rules = rules
            // All five keycards and grand-zone strength: bid seven.
            .rule(
                Bid::new(7, strain),
                1.86,
                keycard_total(trump, 5..) & combined_points(37) & level_available(7, strain),
            )
            // At most one keycard missing: small slam.
            .rule(
                Bid::new(6, strain),
                1.84,
                keycard_total(trump, 4..) & level_available(6, strain),
            )
            // Two missing: sign off at five while it is still available...
            .rule(
                Bid::new(5, strain),
                1.82,
                keycard_total(trump, ..=3) & level_available(5, strain),
            )
            // ...pass when partner's answer already is five of the trump...
            .rule(
                Call::Pass,
                1.80,
                keycard_total(trump, ..=3) & answer_is_five_of(trump),
            )
            // ...and with no room below slam (a cramped minor, or a 5♠ answer
            // over hearts) accept six rather than strand the phantom answer —
            // the book's `no_room_six` policy.
            .rule(
                Bid::new(6, strain),
                0.3,
                keycard_total(trump, ..) & level_available(6, strain),
            );
    }
    // Never pass out partner's control bid — it agrees a suit and forces.
    // Return to the agreed suit at the cheapest level; with slam-zone values
    // the ask above (or the direct milestones) outweighs this signoff, and
    // the control bidder keeps captaining over it.
    for trump in Suit::ASC {
        let strain = Strain::from(trump);
        for level in 4..=6 {
            rules = rules.rule(
                Bid::new(level, strain),
                1.55,
                partner_control_bid(trump)
                    & inference_aware()
                    & known_eight_card_fit(trump)
                    & min_level_is(level, strain),
            );
        }
    }

    // Rubens advances of partner's simple overcall.  Over a one-level overcall
    // the calls from the cue up to just below a two-level raise are transfers to
    // the next suit: a new-suit transfer shows a five-card suit and 10+ upgraded
    // points — a *good* 9 and all 10+, since the transfer commits partner to the
    // two-level — and the transfer into partner's suit is a limit-plus raise.
    // Over a two-level overcall the cue itself is the limit-plus raise.  Both halves are read in [`Inferences`]
    // (the transfer/cue suit is a relay, not a holding), so partner's instinct
    // completes the transfer and the milestone never misreads it as natural.
    //
    // [`Inferences`]: super::inference::Inferences
    for source in [Suit::Clubs, Suit::Diamonds, Suit::Hearts] {
        let source_strain = Strain::from(source);
        let target = Suit::ASC[(source as u8 + 1) as usize];
        rules = rules
            .rule(
                Bid::new(2, source_strain),
                1.35,
                rubens_transfer(source, false)
                    & len(target, 5..)
                    & points(10..)
                    & min_level_is(2, source_strain),
            )
            .rule(
                Bid::new(2, source_strain),
                1.45,
                rubens_transfer(source, true)
                    & support(3..)
                    & points(10..)
                    & min_level_is(2, source_strain),
            );
    }
    for cue in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
        let cue_strain = Strain::from(cue);
        rules = rules.rule(
            Bid::new(2, cue_strain),
            1.45,
            rubens_cue_raise(cue) & support(3..) & points(10..) & min_level_is(2, cue_strain),
        );
    }
    // Complete partner's transfer into the suit just above it — mechanical, like
    // completing a transfer over our own notrump.
    for target in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
        rules = rules.rule(
            Bid::new(2, Strain::from(target)),
            1.55,
            rubens_completes(target),
        );
    }
    // Grade the into-partner completion: the transfer showed a limit-plus
    // raise (10+ with support), so the completion `2Y` denies extras, a 13–14
    // hand super-accepts `3Y` (an invite the raiser carries on with 13+, via
    // the raise ladder), and a 15+ maximum places the game outright — the
    // acceptance that cashes the transfer's separation over a flat natural
    // raise (cf. the 1NT invite-acceptance win).  The minor maximum is `3NT`
    // behind a stopper; only diamonds can be an into-partner target at the one
    // level (nothing transfers into clubs).
    // ponytail: help-suit trials, a minor super-accept, and rebids after a
    // NEW-SUIT transfer are the named ceiling — author them if the A/B still trails.
    for y in [Suit::Hearts, Suit::Spades] {
        rules = rules
            .rule(
                Bid::new(4, Strain::from(y)),
                1.6,
                rubens_into_partner(y) & points(15..),
            )
            .rule(
                Bid::new(3, Strain::from(y)),
                1.58,
                rubens_into_partner(y) & points(13..15),
            );
    }
    rules = rules.rule(
        Bid::new(3, Strain::Notrump),
        1.6,
        rubens_into_partner(Suit::Diamonds) & points(15..) & stopper_in_their_suits(),
    );
    // The overcaller breaks a NEW-SUIT completion, too, exactly when it would
    // have bid over a natural non-forcing `2 target`: the fit raise with
    // values, graded like the into-partner break (invite 13–14, game 15+; the
    // diamond maximum is `3NT` behind a stopper).
    for target in [Suit::Diamonds, Suit::Hearts] {
        rules = rules.rule(
            Bid::new(3, Strain::from(target)),
            1.58,
            rubens_new_suit_completion(target) & len(target, 3..) & points(13..15),
        );
    }
    rules = rules
        .rule(
            Bid::new(4, Strain::from(Suit::Hearts)),
            1.6,
            rubens_new_suit_completion(Suit::Hearts) & len(Suit::Hearts, 3..) & points(15..),
        )
        .rule(
            Bid::new(3, Strain::Notrump),
            1.6,
            rubens_new_suit_completion(Suit::Diamonds) & points(15..) & stopper_in_their_suits(),
        );

    // The raiser's rebid over the minimum completion: extras beyond the shown
    // ten drive to game opposite even a minimum overcall.
    for y in [Suit::Hearts, Suit::Spades] {
        rules = rules.rule(
            Bid::new(4, Strain::from(y)),
            1.5,
            rubens_raiser_rebids(y) & points(14..),
        );
    }
    rules = rules.rule(
        Bid::new(3, Strain::Notrump),
        1.5,
        rubens_raiser_rebids(Suit::Diamonds) & points(14..) & stopper_in_their_suits(),
    );
    // The new-suit transferee's rebid: the transfer was wide yet unlimited —
    // it subsumes both the non-forcing and the forcing natural treatments,
    // because the rest/continue split happens *after* the cheap completion.  A
    // mild hand has already rested; 12–13 re-raises the suit as the invite;
    // 14+ clarifies to game — the six-card major, or `3NT` behind a stopper.
    rules = rules.rule(
        Bid::new(4, Strain::from(Suit::Hearts)),
        1.52,
        rubens_transferee_rebids(Suit::Hearts) & len(Suit::Hearts, 6..) & points(14..),
    );
    for target in [Suit::Diamonds, Suit::Hearts] {
        rules = rules
            .rule(
                Bid::new(3, Strain::Notrump),
                1.5,
                rubens_transferee_rebids(target) & points(14..) & stopper_in_their_suits(),
            )
            .rule(
                Bid::new(3, Strain::from(target)),
                1.5,
                rubens_transferee_rebids(target) & len(target, 6..) & points(12..14),
            );
    }

    // Answer partner's two-level cue-raise — the cue must never play their
    // suit.  Retreat to our overcall suit as the guaranteed action; with a
    // maximum (14+, opposite the cue's 10+) place the game instead: `4♥` on
    // the heart fit, `3NT` over a minor with their suit stopped.
    for y in [Suit::Clubs, Suit::Diamonds, Suit::Hearts] {
        rules = rules.rule(Bid::new(3, Strain::from(y)), 1.5, rubens_cue_answers(y));
    }
    rules = rules.rule(
        Bid::new(4, Strain::from(Suit::Hearts)),
        1.55,
        rubens_cue_answers(Suit::Hearts) & points(14..),
    );
    for y in [Suit::Clubs, Suit::Diamonds] {
        rules = rules.rule(
            Bid::new(3, Strain::Notrump),
            1.55,
            rubens_cue_answers(y) & points(14..) & stopper_in_their_suits(),
        );
    }
    // Knob-off natural new-suit advance — the fair baseline for the Rubens A/B
    // (see `natural_new_suit_advance`); dormant while the transfers are on.
    for target in [Suit::Diamonds, Suit::Hearts] {
        let target_strain = Strain::from(target);
        rules = rules.rule(
            Bid::new(2, target_strain),
            1.35,
            natural_new_suit_advance(target)
                & len(target, 5..)
                & points(10..)
                & min_level_is(2, target_strain),
        );
    }

    // Competitive long-suit rebid (opt-in; see `set_competitive_rebid`).  Once
    // `we_have_not_bid` is false the floor competes only by raising partner or
    // doubling, so a hand with a suit of its own — the opener's rebiddable
    // six-bagger, an overcaller's — is stuck doubling.  Rebid that suit at the
    // cheapest legal level, outranking the 0.9 takeout double below, and the
    // existing raise ladder carries responder to game.  The natural reading
    // already reads a repeated suit as 6+ (`inference.rs`).
    //
    // Capped at the three level: over their three-level bid the cheapest rebid of
    // a lower-ranking suit is game (4♣ over 3♦, 4♥ over 3♠), and this rule carries
    // no strength — a minimum must not blast game unilaterally.
    //
    // The A/B split the two live levels sharply: the 2-level rebid (balancing
    // seat, low overcaller rebids) is a clean win at both vulnerabilities on both
    // scorers, so it stays unconditional; the blanket 3-level rebid lost —
    // marginal non-vul, clearly negative vulnerable under perfect defense — so it
    // now demands a genuine source of tricks: seven cards, or a good six (two of
    // the top three honors).  A ragged six-bagger competing to the three level
    // stays home (double or pass).
    if competitive_rebid_enabled() {
        for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
            let strain = Strain::from(suit);
            rules = rules.rule(
                Bid::new(2, strain),
                1.0,
                their_live_bid_at_most(3)
                    & i_bid_suit(suit)
                    & min_level_is(2, strain)
                    & len(suit, 6..)
                    & !they_bid(strain)
                    & not_penalty_latched(),
            );
            rules = rules.rule(
                Bid::new(3, strain),
                1.0,
                their_live_bid_at_most(3)
                    & i_bid_suit(suit)
                    & min_level_is(3, strain)
                    & len(suit, 6..)
                    & (len(suit, 7..) | top_honors(suit, 2..))
                    & !they_bid(strain)
                    & not_penalty_latched(),
            );
        }
    }

    // Takeout double of their low suit bid: shape with opening values, or
    // any strong hand planning to bid again.  The penalty latch steps these
    // aside — once we own the auction for penalty, a double is not takeout.
    rules
        .rule(
            Call::Double,
            0.9,
            their_live_bid_at_most(3)
                & short_in_their_suits()
                & hcp(12..)
                & not_penalty_latched()
                & takeout_double_shape_ok(),
        )
        .rule(
            Call::Double,
            0.8,
            their_live_bid_at_most(3) & points(17..) & not_penalty_latched(),
        )
        // Penalty latch: double their runout for penalty on a trump stack instead
        // of takeout on shortness.  Weight matches the runout penalty doubles.
        .rule(
            Call::Double,
            1.6,
            their_live_bid_at_most(3)
                & penalty_latched_c()
                & latch_penalty_c()
                & doubled_suit_stack(),
        )
        // Optional latch: double their runout cooperatively on 2-3 cards and
        // values — partner decides (sit on a fit, run when short).  The defensive
        // mirror of the we-open optional double; same weight as the penalty stack.
        .rule(
            Call::Double,
            1.6,
            their_live_bid_at_most(3)
                & penalty_latched_c()
                & latch_optional_c()
                & doubled_suit_len(2..=3)
                & hcp(6..),
        )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::bidding::trie::Classifier;
    use contract_bridge::auction::RelativeVulnerability;

    const fn call(level: u8, strain: Strain) -> Call {
        Call::Bid(Bid::new(level, strain))
    }

    /// The highest-logit instinct call for a hand in an auction
    fn best(auction: &[Call], hand: &str) -> Call {
        let hand: Hand = hand.parse().expect("valid test hand");
        let context = Context::new(RelativeVulnerability::NONE, auction);
        let logits = instinct().classify(hand, &context);
        (&logits.0)
            .into_iter()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
            .map(|(call, _)| call)
            .expect("array is never empty")
    }

    /// The full-`american()` call for a hand and whether the floor produced it
    ///
    /// `depth == 0` with `fallback == Some(_)` is the instinct floor firing — so
    /// the second tuple field tells a test the node is off-book (floor territory),
    /// guarding against a floor rule that is silently shadowed by a book node.
    fn american_floored(auction: &[Call], hand: &str) -> (Call, bool) {
        use crate::bidding::Family;
        use crate::bidding::american::american;
        let hand: Hand = hand.parse().expect("valid test hand");
        let (logits, provenance) = american()
            .against(Family::NATURAL)
            .classify_with_provenance(hand, RelativeVulnerability::NONE, auction)
            .expect("a legal auction classifies");
        let call = (&logits.0)
            .into_iter()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
            .map(|(call, _)| call)
            .expect("array is never empty");
        (call, provenance.depth == 0 && provenance.fallback.is_some())
    }

    #[test]
    fn advancing_a_double_advances_a_bust_but_defends_with_length() {
        // Partner doubled their 3♣ for takeout, RHO passed.
        let auction = [call(3, Strain::Clubs), Call::Double, Call::Pass];
        // A worthless hand with a five-card suit outside theirs still advances —
        // it cannot beat 3♣ doubled, so it bids rather than pass into it.
        assert_eq!(best(&auction, "96432.J85.9742.2"), call(3, Strain::Spades));
        // But four cards sitting behind their suit defend: pass plays 3♣ doubled,
        // a better penalty than escaping (the settle floor, default on).
        assert_eq!(best(&auction, "964.J85.974.9632"), Call::Pass);
    }

    #[test]
    fn trump_stack_converts_to_penalties() {
        // KQ92 behind the 2♠ bidder sits for partner's takeout double.
        let auction = [call(2, Strain::Spades), Call::Double, Call::Pass];
        assert_eq!(best(&auction, "KQ92.A532.J42.96"), Call::Pass);
    }

    #[test]
    fn competitive_rebid_shows_the_long_suit() {
        // 1♦ (1♥) P (2♥): opener holds a self-sufficient seven-card diamond suit
        // and a stiff in their hearts.  The floor's only competitive actions once
        // it has bid are raise-partner and takeout-double — and partner passed.
        let raised = [
            call(1, Strain::Diamonds),
            call(1, Strain::Hearts),
            Call::Pass,
            call(2, Strain::Hearts),
        ];
        let one_suiter = "765.A.AKJT984.63";

        // Off (default): the floor can only double, misdescribing a takeout shape.
        set_competitive_rebid(false);
        assert_eq!(best(&raised, one_suiter), Call::Double);

        // On: rebid the suit instead — and it is the floor that produces it, not a
        // book node shadowing the position.
        set_competitive_rebid(true);
        assert_eq!(best(&raised, one_suiter), call(3, Strain::Diamonds));
        assert_eq!(
            american_floored(&raised, one_suiter),
            (call(3, Strain::Diamonds), true)
        );

        // Balancing seat (they did not raise): the cheapest rebid is 2♦.
        let balancing = [
            call(1, Strain::Diamonds),
            call(1, Strain::Hearts),
            Call::Pass,
            Call::Pass,
        ];
        assert_eq!(best(&balancing, one_suiter), call(2, Strain::Diamonds));

        // General across suits: opener's six-card major over their overcall + raise.
        let major = [
            call(1, Strain::Spades),
            call(2, Strain::Hearts),
            Call::Pass,
            call(3, Strain::Hearts),
        ];
        assert_eq!(best(&major, "AKJ982.K5.A54.63"), call(3, Strain::Spades));

        // A five-card suit is not enough: the takeout double stands.
        assert_eq!(best(&raised, "765.A.AKJT9.6432"), Call::Double);

        // 3-level quality gate (over their raise, cheapest rebid is 3♦): a good
        // six (two of the top three honors) or seven cards rebids; a ragged six
        // does not.  The seven-card `one_suiter` above already covers the length
        // path.
        assert_eq!(best(&raised, "765.A.AKJ864.632"), call(3, Strain::Diamonds));
        assert_ne!(best(&raised, "KQ5.A.T98764.632"), call(3, Strain::Diamonds));
        // …but that same ragged six still competes at the cheaper two level.
        assert_eq!(
            best(&balancing, "KQ5.A.T98764.632"),
            call(2, Strain::Diamonds)
        );

        // Capped at the three level: over their three-level bid the cheapest
        // diamond rebid is game (4♦), and a minimum must not blast it — the rule
        // stays home rather than jump.
        let over_three = [
            call(1, Strain::Diamonds),
            call(3, Strain::Hearts),
            Call::Pass,
            Call::Pass,
        ];
        assert_ne!(
            best(&over_three, "K5.54.AQJ982.J43"),
            call(4, Strain::Diamonds)
        );

        // Seat-scoped: partner's Jacoby transfer names our short major, but we
        // never bid it — no phantom natural rebid of the transfer strain.
        let transfer = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(2, Strain::Hearts),
            call(3, Strain::Clubs),
        ];
        assert_ne!(best(&transfer, "K5.AQJ982.A5.K43"), call(3, Strain::Hearts));

        // The overcaller's rebid path fires too — we personally bid the suit.
        let overcall = [
            call(1, Strain::Hearts),
            call(2, Strain::Diamonds),
            call(2, Strain::Hearts),
            Call::Pass,
            Call::Pass,
        ];
        assert_eq!(
            best(&overcall, "K5.A54.AKJT98.63"),
            call(3, Strain::Diamonds)
        );

        set_competitive_rebid(false);
    }

    #[test]
    fn penalty_latch_doubles_the_runout_for_penalty() {
        // (1NT) X — our penalty double — (2♦) runout; we hold a diamond stack.
        let auction = [
            call(1, Strain::Notrump),
            Call::Double,
            call(2, Strain::Diamonds),
        ];
        // A pure diamond stack (9 HCP, all in their suit): combined with partner's
        // shown 15+ this is below game, so the floor neither bids nor advances.
        // Latch off — defend by passing, no penalty double offered.
        set_penalty_latch(false);
        assert_eq!(best(&auction, "T98.964.AKQ7.853"), Call::Pass);
        // Latch on (the default): "once penalty, always penalty" — double for penalty.
        set_penalty_latch(true);
        assert_eq!(best(&auction, "T98.964.AKQ7.853"), Call::Double);
        // The latch keys off the 1NT penalty double only: a plain takeout auction
        // is untouched — short in clubs with opening values still doubles 2♣ takeout.
        let takeout = [call(2, Strain::Clubs)];
        assert_eq!(best(&takeout, "AQ95.KJ73.K842.6"), Call::Double);
    }

    #[test]
    fn penalty_latch_leaves_partner_s_double_in() {
        // (1NT) X (2♦) X (Pass): partner doubled the runout for penalty, back to us.
        let auction = [
            call(1, Strain::Notrump),
            Call::Double,
            call(2, Strain::Diamonds),
            Call::Double,
            Call::Pass,
        ];
        // A flat 16-count with no diamond stopper: latch off, the takeout-advance
        // jumps to a dubious 4♠ on a four-card suit.
        set_penalty_latch(false);
        assert_eq!(best(&auction, "AQ74.AQ5.82.A632"), call(4, Strain::Spades));
        // Latched (the default), partner's double is penalty — leave it in (defend 2♦x).
        set_penalty_latch(true);
        assert_eq!(best(&auction, "AQ74.AQ5.82.A632"), Call::Pass);
    }

    #[test]
    fn advancer_runs_from_redoubled_penalty_double() {
        // (1NT) X (XX): their business redouble, back to the broke advancer.
        let auction = [call(1, Strain::Notrump), Call::Double, Call::Redouble];
        // Weak with a five-card major: escape to it rather than sit for 1NTxx.
        assert_eq!(best(&auction, "J9763.852.764.43"), call(2, Strain::Spades));
        // Weak with a six-card minor: run to it.
        assert_eq!(best(&auction, "82.43.765.QJ8765"), call(2, Strain::Clubs));
        // Values (9 HCP): sit and defend 1NTxx — our side beats it.
        assert_eq!(best(&auction, "KQ7.K83.J642.643"), Call::Pass);
        // Off-switch: the runout disabled, the broke hand sits.
        set_advancer_xx_runout(false);
        assert_eq!(best(&auction, "J9763.852.764.43"), Call::Pass);
        set_advancer_xx_runout(true);
    }

    #[test]
    fn doubler_runs_from_redoubled_penalty_double() {
        // (1NT) X (XX) P P: the redouble ran back to the 15+ doubler.
        let auction = [
            call(1, Strain::Notrump),
            Call::Double,
            Call::Redouble,
            Call::Pass,
            Call::Pass,
        ];
        // On by default: a 15+ 5332 escapes to its five-card suit rather than defend
        // the redouble.
        assert_eq!(best(&auction, "AQ765.KQ4.A82.K3"), call(2, Strain::Spades));
        assert_eq!(best(&auction, "AQ4.K82.K3.AQ765"), call(2, Strain::Clubs));
        // No five-card suit (4-4-3-2): nowhere to run, so sit.
        assert_eq!(best(&auction, "AQ74.KQ32.A82.K3"), Call::Pass);
        // Off-switch: the strong doubler sits and defends 1NTxx.
        set_doubler_xx_runout(false);
        assert_eq!(best(&auction, "AQ765.KQ4.A82.K3"), Call::Pass);
        set_doubler_xx_runout(true);
    }

    #[test]
    fn optional_latch_doubles_short_and_partner_cooperates() {
        // (1NT) X (2♦): our latched double of their runout.
        let runout = [
            call(1, Strain::Notrump),
            Call::Double,
            call(2, Strain::Diamonds),
        ];
        // Three small diamonds (no stack), 13 HCP, no four-card suit worth bidding:
        // the PENALTY latch needs a stack, so it does not double…
        let cooperative = "KQ5.KQ5.642.QJ93";
        set_latch_style(LatchStyle::Penalty);
        assert_ne!(best(&runout, cooperative), Call::Double);
        // …but the OPTIONAL latch doubles on the 2-3 holding and values.
        set_latch_style(LatchStyle::Optional);
        assert_eq!(best(&runout, cooperative), Call::Double);

        // (1NT) X (2♦) X (Pass): partner's latched double, back to the 15+ doubler.
        let advance = [
            call(1, Strain::Notrump),
            Call::Double,
            call(2, Strain::Diamonds),
            Call::Double,
            Call::Pass,
        ];
        // Penalty: forced sit — leave the penalty double in (defend 2♦x).
        set_latch_style(LatchStyle::Penalty);
        assert_eq!(best(&advance, "AQ74.AQ5.82.A632"), Call::Pass);
        // Optional: cooperate (not forced to sit) — short in their suit with a
        // four-card major and values, run to the major game.
        set_latch_style(LatchStyle::Optional);
        assert_eq!(best(&advance, "AQ74.AQ5.82.A632"), call(4, Strain::Spades));

        set_latch_style(LatchStyle::default());
    }

    #[test]
    fn advancing_a_double_bids_game_with_values() {
        let auction = [call(2, Strain::Spades), Call::Double, Call::Pass];
        // 13 HCP with their suit stopped and no length behind it: 3NT to play.
        assert_eq!(best(&auction, "AQ3.K65.J64.QJ96"), call(3, Strain::Notrump));
        // 11 HCP with four hearts: jump to the major-suit game.
        assert_eq!(best(&auction, "92.AQ53.KQ42.962"), call(4, Strain::Hearts));
    }

    #[test]
    fn unforced_raise_with_fit() {
        // Partner opened 1♠ and they overcalled 2♥: raise with three-card
        // support and 8 HCP.
        let auction = [call(1, Strain::Spades), call(2, Strain::Hearts)];
        assert_eq!(best(&auction, "Q32.953.A964.Q92"), call(2, Strain::Spades));
    }

    #[test]
    fn unforced_takeout_double_on_shape() {
        // Their 3♦ preempt: 13 HCP, short in diamonds, no five-card suit.
        let auction = [call(3, Strain::Diamonds)];
        assert_eq!(best(&auction, "KQ32.AJ53.2.A942"), Call::Double);
    }

    #[test]
    fn unforced_pass_without_values() {
        // Nothing to say over their 3♦: too weak to act at the three level.
        let auction = [call(3, Strain::Diamonds)];
        assert_eq!(best(&auction, "Q5432.J53.942.92"), Call::Pass);
    }

    #[test]
    fn doubles_only_their_live_bids() {
        // The call to beat is our own 2♠ (partner raised our overcall):
        // doubling our side is never on the table.
        let auction = [
            call(1, Strain::Hearts),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Spades),
            Call::Pass,
        ];
        let hand: Hand = "92.K53.AQJ42.962".parse().expect("valid test hand");
        let context = Context::new(RelativeVulnerability::NONE, &auction);
        let logits = instinct().classify(hand, &context);
        assert_eq!(*logits.0.get(Call::Double), f32::NEG_INFINITY);
    }

    #[test]
    fn settle_floor_defends_with_length_behind_their_suit() {
        // Their 3♠, partner doubles (takeout), RHO passes → advancing a double.
        let auction = [call(3, Strain::Spades), Call::Double, Call::Pass];
        // 6 HCP, five clubs but four cards sitting behind their spades.
        let weak_with_defense = "9543.74.K2.QJ876";

        // Off (default): the floor over-advances to the captive 4♣.
        set_settle_floor(false);
        assert_eq!(best(&auction, weak_with_defense), call(4, Strain::Clubs));

        // On: the four-level new suit is a free bid we lack the values for, and we
        // hold four behind their suit, so we defend — pass plays 3♠ doubled.
        set_settle_floor(true);
        assert_eq!(best(&auction, weak_with_defense), Call::Pass);

        // On, with real values: the free bid is earned — we still advance to 4♣
        // (a hand short in their suit cannot defend anyway).
        let strong = "2.853.K42.AKQ876";
        assert_eq!(best(&auction, strong), call(4, Strain::Clubs));

        set_settle_floor(true); // restore the default (on) for the rest of the suite
    }

    #[test]
    fn completes_partners_transfer_over_notrump() {
        // We opened 1NT and partner transferred 2♦ (hearts): complete with 2♥,
        // even off-book, rather than passing or raising the artificial diamonds.
        let auction = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "AQ32.KJ5.KQ4.Q92"), call(2, Strain::Hearts));
    }

    #[test]
    fn forced_to_game_opposite_strong_notrump() {
        // Partner opened 1NT; after an artificial 2NT super-accept of our heart
        // transfer a game-forced 12-count bids 3NT, never passing below game.
        let auction = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Notrump),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "KQ52.AQ984.J6.32"), call(3, Strain::Notrump));
    }

    #[test]
    fn forced_to_game_picks_the_known_major_fit() {
        // We opened 1NT; partner's off-book, forcing 3♥ shows five-plus hearts.
        // With three-card support that is a known eight-card fit, so bid 4♥
        // rather than the stopperless-agnostic 3NT.
        let auction = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(3, Strain::Hearts),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "AQ52.K53.KQ4.32"), call(4, Strain::Hearts));
    }

    #[test]
    fn transfer_invite_reaches_the_floor_over_a_possible_five_two() {
        // 1NT–2♦–2♥–3♥: partner transferred to hearts and raised.  With the six-card
        // invite on (the default) this node is authored — so turn it off to exercise
        // the floor path this test guards: the projection reads the 2♦ transfer's
        // five-card floor (M6.1's core), but M6.2c dropped the old reader's six-card
        // upgrade off the 3♥ raise (soundness over tightness — projecting a
        // natural-suit raise is out of the overlay's artificial-only scope).  With
        // only a five-card major shown and our own doubleton, the floor prefers 3NT
        // over a possible 5-2 game.
        crate::bidding::american::set_sixcard_invite_floor(14);
        let invite = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Hearts),
            Call::Pass,
            call(3, Strain::Hearts),
            Call::Pass,
        ];
        let (bid, from_floor) = american_floored(&invite, "AKQ2.J5.AQ52.K42");
        assert!(
            from_floor,
            "the transfer invite is off-book, the floor decides"
        );
        assert_eq!(bid, call(3, Strain::Notrump));
        crate::bidding::american::set_sixcard_invite_floor(13); // restore default
    }

    // -----------------------------------------------------------------------
    // M6.4: floor RKCB + control-bid signoffs
    // -----------------------------------------------------------------------

    /// With a known fit and combined small-slam values the floor asks 4NT
    /// (M6.4) instead of blasting the direct milestone 6♠.
    #[test]
    fn floor_asks_keycards_with_slam_values_and_a_known_fit() {
        // 1♠–3♠: the jump raise shows three spades and 10+, so a 23-point
        // opener counts combined 33 with a decodable trump (its own shown
        // five-card spades).
        let auction = [
            call(1, Strain::Spades),
            Call::Pass,
            call(3, Strain::Spades),
            Call::Pass,
        ];
        assert_eq!(
            best(&auction, "AKQJ7.AKQ.A32.32"),
            call(4, Strain::Notrump),
            "slam values + known fit → ask keycards, not 6♠ direct"
        );
    }

    /// The 1430 answers to partner's off-book 4NT, counted against the
    /// derived trump (spades, raised)
    #[test]
    fn floor_answers_keycards_1430() {
        let auction = [
            call(1, Strain::Spades),
            Call::Pass,
            call(3, Strain::Spades),
            Call::Pass,
            call(4, Strain::Notrump),
            Call::Pass,
        ];
        // 1 keycard (trump K) → 5♣
        assert_eq!(
            best(&auction, "KQ732.K53.Q42.92"),
            call(5, Strain::Clubs),
            "1 keycard → 5♣"
        );
        // 0 keycards (the heart K is not one) → 5♦
        assert_eq!(
            best(&auction, "QJ732.K53.Q42.Q2"),
            call(5, Strain::Diamonds),
            "0 keycards → 5♦"
        );
        // 2 aces + trump K = 3 keycards → 5♦
        assert_eq!(
            best(&auction, "AK732.A53.842.92"),
            call(5, Strain::Diamonds),
            "3 keycards → 5♦"
        );
        // 2 keycards with the trump queen → 5♠
        assert_eq!(
            best(&auction, "AQ732.A53.842.92"),
            call(5, Strain::Spades),
            "2 keycards + trump Q → 5♠"
        );
        // 2 keycards without the queen → 5♥
        assert_eq!(
            best(&auction, "A8732.A53.842.92"),
            call(5, Strain::Hearts),
            "2 keycards, no trump Q → 5♥"
        );
        // All five keycards (four aces + trump K) → 5♣, the hole the book
        // ladder's {1,4} left open (round 3 passed a 4NT out on it)
        assert_eq!(
            best(&auction, "AK732.A53.A42.A2"),
            call(5, Strain::Clubs),
            "5 keycards → 5♣"
        );
    }

    /// The asker decodes the answer: two keycards missing signs off at five,
    /// one missing bids the small slam
    #[test]
    fn floor_asker_continues_after_the_answer() {
        let auction = [
            call(1, Strain::Spades),
            Call::Pass,
            call(3, Strain::Spades),
            Call::Pass,
            call(4, Strain::Notrump),
            Call::Pass,
            call(5, Strain::Diamonds),
            Call::Pass,
        ];
        // 3 keycards: 5♦ arithmetically means 0 with us — two missing → 5♠.
        assert_eq!(
            best(&auction, "AQ752.A76.72.A93"),
            call(5, Strain::Spades),
            "two keycards missing → sign off"
        );
        // 4 keycards: 5♦ means 0 — one missing → 6♠.
        assert_eq!(
            best(&auction, "AK752.A76.72.A93"),
            call(6, Strain::Spades),
            "one keycard missing → small slam"
        );
    }

    /// The answerer respects the asker's placement — holding at most one
    /// keycard the total cannot be slam-safe, so no milestone past it (with
    /// two-plus the correction stays live: the asker may have read an
    /// ambiguous answer low, or a book table signed off pessimistically)
    #[test]
    fn floor_answerer_respects_the_signoff() {
        let auction = [
            call(1, Strain::Spades),
            Call::Pass,
            call(3, Strain::Spades),
            Call::Pass,
            call(4, Strain::Notrump),
            Call::Pass,
            call(5, Strain::Clubs),
            Call::Pass,
            call(5, Strain::Spades),
            Call::Pass,
        ];
        // One keycard (the trump K), yet milestone-worthy values (33+
        // combined would bid 6♠): the asker held the count — pass.
        assert_eq!(
            best(&auction, "KQ543.KQJ.KQJ4.K"),
            Call::Pass,
            "the answerer never overrides the asker's signoff short a keycard"
        );
    }

    /// Partner's post-transfer 4♠ is a control bid agreeing hearts (the M6.4
    /// reading): opener returns to the agreed suit instead of passing out the
    /// phantom spade contract
    #[test]
    fn control_bid_is_never_passed_out() {
        // 1♦–1♠–2♦–4♥: responder bypassed the cheaper 1♥, so 4♥ cannot be
        // long — a control bid agreeing diamonds (the M6.4 reading) — and the
        // floor returns to the agreed suit instead of passing out the phantom
        // heart contract.
        let auction = [
            call(1, Strain::Diamonds),
            Call::Pass,
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(4, Strain::Hearts),
            Call::Pass,
        ];
        let (bid, from_floor) = american_floored(&auction, "A4.K85.KQJ62.Q75");
        assert!(from_floor, "the 4♥ jump is off-book");
        assert_eq!(
            bid,
            call(5, Strain::Diamonds),
            "return to the agreed suit over partner's control bid"
        );
    }

    #[test]
    fn transfer_jump_to_game_reaches_the_floor_and_passes() {
        // 1NT–2♦–2♥–4♥: the jump past 3NT is off-book too.  Game is already
        // reached and the floor has no slam machinery yet (M6.2), so it passes —
        // M6.1 derives the six-card major (length only) without over-reaching.
        let game = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Hearts),
            Call::Pass,
            call(4, Strain::Hearts),
            Call::Pass,
        ];
        let (bid, from_floor) = american_floored(&game, "AKQ2.J5.AQ52.K42");
        assert!(from_floor, "the 4♥ jump is off-book, the floor decides");
        assert_eq!(bid, Call::Pass);
    }

    #[test]
    fn nine_count_five_card_major_forces_game_after_a_transfer() {
        // 1NT–2♥–2♠: a 9-count with a single five-card spade suit transferred (it
        // cannot bid the direct 3NT, which denies a five-card major) and now forces
        // game off the floor — the authored rebid table stops at the exactly-8
        // invite, so the floor (default 9) carries the 9.
        let auction = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(2, Strain::Hearts),
            Call::Pass,
            call(2, Strain::Spades),
            Call::Pass,
        ];
        let (bid, from_floor) = american_floored(&auction, "AK543.82.Q76.542");
        assert!(from_floor, "the game force is off-book, the floor decides");
        assert_eq!(bid, call(3, Strain::Notrump));
    }

    #[test]
    fn opener_corrects_choice_of_games_3nt_to_the_known_major_fit() {
        // 1NT–2♥–2♠–3NT: responder transferred (showing five spades) then offered
        // the choice with 3NT.  Opposite three-card support the 5-3 fit out-scores
        // notrump single-dummy *only with a ruffing doubleton*, so opener corrects
        // to 4♠ on a doubleton, but a flat 4-3-3-3 (no ruff) leaves the better game
        // in 3NT.  Default on; the guard sets it explicitly to stay isolated.
        set_correct_3nt_to_major(true);
        let auction = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(2, Strain::Hearts),
            Call::Pass,
            call(2, Strain::Spades),
            Call::Pass,
            call(3, Strain::Notrump),
            Call::Pass,
        ];
        // Three-card support with a ruffing doubleton (3-2-4-4): correct to 4♠.
        let (fit, _) = american_floored(&auction, "AQ4.K8.KJ72.Q832");
        assert_eq!(
            fit,
            call(4, Strain::Spades),
            "3-card support with a doubleton corrects to 4♠"
        );
        // Three-card support but a flat 4-3-3-3 (no ruffing value): stay in 3NT.
        let (flat, _) = american_floored(&auction, "AQ4.K83.KJ72.Q83");
        assert_eq!(
            flat,
            Call::Pass,
            "flat 4333 has no ruff — 3NT is the better game"
        );
        // Only a doubleton spade — no eight-card fit — also stays in 3NT.
        let (two, _) = american_floored(&auction, "AQ.K842.KJ73.Q82");
        assert_eq!(two, Call::Pass, "no eight-card fit leaves it in 3NT");
        set_correct_3nt_to_major(true); // restore the default
    }

    #[test]
    fn strong_balanced_redoubles_a_double_of_our_1nt_not_3nt() {
        // 1NT–(X): a strong balanced responder defends the unlimited business
        // redouble rather than pulling to 3NT (the floor suppresses the game-force
        // 3NT over a double of our 1NT).
        let auction = [call(1, Strain::Notrump), Call::Double];
        let (bid, from_floor) = american_floored(&auction, "KQ4.KJ43.AQ62.Q5");
        assert!(from_floor, "the response is off-book, the floor decides");
        assert_eq!(bid, Call::Redouble);
    }

    #[test]
    fn keeps_passing_with_a_weak_responder() {
        // Partner opened 1NT but we are too weak to force game: still pass when
        // off-book (the forced-to-game floor must not fire on invitational-or-less).
        let auction = [
            call(1, Strain::Notrump),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Notrump),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "8632.J9842.96.42"), Call::Pass);
    }

    #[test]
    fn forced_to_game_after_strong_two_clubs() {
        // 2♣ (strong) – 2♦ (game-forcing waiting) – 2NT (22–24 balanced): the
        // auction is game forcing, so a flat 7-count bids 3NT, never passing.
        // 2♣–2♥ is the double negative, so 2♦ commits the partnership to game.
        let auction = [
            call(2, Strain::Clubs),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Notrump),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "QJ52.K43.T62.J32"), call(3, Strain::Notrump));
    }

    #[test]
    fn forced_two_clubs_bids_major_game() {
        // The same forcing 2♣–2♦–2NT auction, but holding six hearts: jump to
        // the major-suit game in preference to 3NT.
        let auction = [
            call(2, Strain::Clubs),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Notrump),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "3.QJ9854.K32.J32"), call(4, Strain::Hearts));
    }

    #[test]
    fn double_negative_two_clubs_may_pass() {
        // 2♣ – 2♥ is the double negative (0–3 HCP); after opener's 2NT the
        // partnership may still stop, so a yarborough passes off-book — the
        // forcing-2♣ floor must not fire once responder has shown the bust.
        let auction = [
            call(2, Strain::Clubs),
            Call::Pass,
            call(2, Strain::Hearts),
            Call::Pass,
            call(2, Strain::Notrump),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "8632.J9842.96.42"), Call::Pass);
    }

    #[test]
    fn forced_game_steps_aside_when_penalizing() {
        // 2♣ – 2♦ (game forcing) – 2NT, then they sacrifice in 3♦ and partner
        // doubles for penalty.  Passing the double out is the game-forcing
        // action, so the floor must not pull it to a stopperless 3NT; with six
        // clubs and no diamond guard, show the suit instead.
        let auction = [
            call(2, Strain::Clubs),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Notrump),
            call(3, Strain::Diamonds),
            Call::Double,
            Call::Pass,
        ];
        assert_eq!(best(&auction, "K3.KQ4.65.QJ8765"), call(4, Strain::Clubs));
    }

    #[test]
    fn milestone_game_opposite_a_limited_rebid() {
        // 1♦–1♥–1NT: opposite the 12–16 rebid a balanced 16 has 28+ combined,
        // a cold 3NT the constructive book never reached (the board that started
        // this).  The floor reads the rebid's strength and bids the game.
        let auction = [
            call(1, Strain::Diamonds),
            Call::Pass,
            call(1, Strain::Hearts),
            Call::Pass,
            call(1, Strain::Notrump),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "J9.AKJ7.K94.A852"), call(3, Strain::Notrump));
        // A 10-count is only invitational (22–24 combined): the floor uses the
        // *guaranteed* minimum, so it stays sound and passes rather than overbid.
        assert_eq!(best(&auction, "KJ9.QJ73.K94.852"), Call::Pass);
    }

    #[test]
    fn milestone_slam_opposite_a_strong_rebid() {
        // 1♦–1♥–2NT is the 18–19 jump rebid; a balanced 16 lifts the combined
        // minimum to 34, the small-slam zone, so bid 6NT instead of stranding in
        // game.  No known major fit, so notrump is the strain.
        let auction = [
            call(1, Strain::Diamonds),
            Call::Pass,
            call(1, Strain::Hearts),
            Call::Pass,
            call(2, Strain::Notrump),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "KQ.AKJ7.K94.8542"), call(6, Strain::Notrump));
    }

    #[test]
    fn milestone_game_opposite_a_competitive_overcall() {
        // LHO opened 3♦, partner overcalled 3♠ (the overcall reading: 5+ ♠,
        // 8+ points), RHO passed.  A 21-count with three-card support lifts the
        // combined minimum to 29 with a known eight-card spade fit, so the floor
        // bids the game it would otherwise miss off-book.
        let auction = [
            call(3, Strain::Diamonds),
            call(3, Strain::Spades),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "K32.AKJ.AQ4.KJ32"), call(4, Strain::Spades));
        // A flat 12-count is only 20 combined: below the milestone, and no raise
        // fits below game, so the floor stays sound and passes rather than overbid.
        assert_eq!(best(&auction, "K32.KJ4.KQ4.5432"), Call::Pass);
    }

    #[test]
    fn milestone_notrump_game_needs_a_stopper_in_competition() {
        // LHO opened 3♣, partner overcalled 3♦, RHO passed.  Game values opposite
        // the overcall, but no major fit and no diamond fit — the strain is 3NT,
        // and the floor must hold a club guard to bid it.
        let auction = [
            call(3, Strain::Clubs),
            call(3, Strain::Diamonds),
            Call::Pass,
        ];
        // A club stopper (K432): 3NT is the milestone game.
        assert_eq!(best(&auction, "AKQ.AQJ.32.K432"), call(3, Strain::Notrump));
        // No club guard and no fit: pass rather than bid into an unstopped suit.
        assert_eq!(best(&auction, "AKQ4.AKQ4.32.432"), Call::Pass);
    }

    #[test]
    fn rubens_new_suit_transfer() {
        // (1♣) 1♠ (P): advancing partner's spade overcall with our own five-card
        // diamond suit, we transfer — 2♣ shows diamonds (the next suit up).  The
        // floor is 10 upgraded points (a *good* 9 and all 10+), since the
        // transfer commits partner to the two-level.
        let auction = [call(1, Strain::Clubs), call(1, Strain::Spades), Call::Pass];
        // A good 9: working K/KQ in a five-card suit upgrades over the floor.
        assert_eq!(best(&auction, "2.K32.KQT54.J432"), call(2, Strain::Clubs));
        // A bare 8 does not reach it: too weak to introduce the suit, pass.
        assert_eq!(best(&auction, "2.Q32.KQT54.J432"), Call::Pass);
    }

    #[test]
    fn rubens_limit_raise_transfer() {
        // (1♣) 1♠ (P): a limit raise of partner's spades goes through the
        // transfer that lands in their suit — 2♥ (the bid just below 2♠).
        let auction = [call(1, Strain::Clubs), call(1, Strain::Spades), Call::Pass];
        assert_eq!(best(&auction, "K54.K32.K43.Q432"), call(2, Strain::Hearts));
    }

    #[test]
    fn rubens_completion_is_mechanical() {
        // (1♣) 1♠ (P) 2♣ (P): partner transferred to diamonds; the overcaller
        // completes into 2♦ regardless of hand.
        let auction = [
            call(1, Strain::Clubs),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Clubs),
            Call::Pass,
        ];
        assert_eq!(
            best(&auction, "AKJ52.K3.952.J32"),
            call(2, Strain::Diamonds)
        );
    }

    #[test]
    fn rubens_two_level_cue_raise() {
        // (1♠) 2♣ (P): partner overcalled at the two level, so the cue (2♠) is
        // the limit-plus raise of clubs — no transfer ladder where there is no room.
        let auction = [call(1, Strain::Spades), call(2, Strain::Clubs), Call::Pass];
        assert_eq!(best(&auction, "432.K32.K2.KQJ54"), call(2, Strain::Spades));
    }

    #[test]
    fn rubens_skips_jump_overcalls() {
        // (1♣) 2♠ (P): partner's 2♠ is a jump (1♠ was available), a preemptive
        // weak jump overcall — not a simple overcall, so no Rubens.  A limit hand
        // with support raises spades naturally rather than transferring.
        let auction = [call(1, Strain::Clubs), call(2, Strain::Spades), Call::Pass];
        assert_eq!(best(&auction, "K54.K32.K43.Q432"), call(3, Strain::Spades));
    }

    #[test]
    fn rubens_completes_through_the_double() {
        // (1♣) 1♠ (P) 2♣ (X): opener lead-directs against the transfer; the
        // completion still fires — otherwise the relay dies and partner plays
        // the phantom suit doubled.
        let auction = [
            call(1, Strain::Clubs),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Clubs),
            Call::Double,
        ];
        assert_eq!(
            best(&auction, "AKJ52.K3.952.J32"),
            call(2, Strain::Diamonds)
        );
    }

    #[test]
    fn rubens_max_breaks_the_completion_to_game() {
        // (1♣) 1♠ (P) 2♥ (P): partner's transfer into our spades showed 10+
        // with support, so a maximum places the game instead of completing.
        let auction = [
            call(1, Strain::Clubs),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Hearts),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "AKJ52.K3.K52.J32"), call(4, Strain::Spades));
        // In between, the overcaller super-accepts — the invite `3♠`.
        assert_eq!(best(&auction, "AKJ52.K3.Q52.432"), call(3, Strain::Spades));
        // A minimum still completes mechanically.
        assert_eq!(best(&auction, "AKJ52.K3.952.J32"), call(2, Strain::Spades));
        // (1♣) 1♦ (P) 2♣ (P): the diamond break is 3NT behind a club stopper…
        let minor = [
            call(1, Strain::Clubs),
            call(1, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Clubs),
            Call::Pass,
        ];
        assert_eq!(best(&minor, "A32.K32.AQJ54.K2"), call(3, Strain::Notrump));
        // …and completes without one, whatever the strength.
        assert_eq!(best(&minor, "AQ2.K32.AQJ54.32"), call(2, Strain::Diamonds));
    }

    #[test]
    fn rubens_new_suit_break_bids_what_it_would_over_natural() {
        // (1♣) 1♠ (P) 2♦ (P): partner shows hearts.  The completion covers the
        // would-pass-a-natural-2♥ hands; with a fit and values the overcaller
        // bids what it would have bid over that natural 2♥.
        let auction = [
            call(1, Strain::Clubs),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
        ];
        // Fit + 13: the invite raise.
        assert_eq!(best(&auction, "AKJ52.Q32.K52.32"), call(3, Strain::Hearts));
        // Fit + maximum: the game.
        assert_eq!(best(&auction, "AKJ52.Q32.K52.A2"), call(4, Strain::Hearts));
        // No fit, minimum: the mechanical completion.
        assert_eq!(best(&auction, "AKJ52.32.Q952.J2"), call(2, Strain::Hearts));
    }

    #[test]
    fn rubens_transferee_rebid_survives_an_out_of_band_two_spades() {
        // (1♣) 1♦ (P) 2♠ (P) 3♣ (P): the 2♠ sits above partner's suit — no
        // transfer.  The detector must reject it by the band, not index past
        // the spade suit (this exact shape panicked a 204k-board run).
        let auction = [
            call(1, Strain::Clubs),
            call(1, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Spades),
            Call::Pass,
            call(3, Strain::Clubs),
            Call::Pass,
        ];
        let _ = best(&auction, "K54.A32.KQ32.Q43");
    }

    #[test]
    fn rubens_transferee_clarifies_with_extras() {
        // (1♣) 1♠ (P) 2♦ (P) 2♥ (P): the heart transfer was wide yet
        // unlimited — a six-card maximum now bids the game.
        let hearts = [
            call(1, Strain::Clubs),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
            call(2, Strain::Hearts),
            Call::Pass,
        ];
        assert_eq!(best(&hearts, "2.AKQT54.K32.A32"), call(4, Strain::Hearts));
        // 12–13 re-raises the suit: the invite the natural NF 2♥ never had.
        assert_eq!(best(&hearts, "2.AKJT54.Q32.Q32"), call(3, Strain::Hearts));
        // (1♣) 1♠ (P) 2♣ (P) 2♦ (P): the diamond hand's game is 3NT behind a
        // club stopper.
        let diamonds = [
            call(1, Strain::Clubs),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Clubs),
            Call::Pass,
            call(2, Strain::Diamonds),
            Call::Pass,
        ];
        assert_eq!(
            best(&diamonds, "2.K32.AKQT54.A32"),
            call(3, Strain::Notrump)
        );
    }

    #[test]
    fn rubens_raiser_moves_with_extras_over_the_completion() {
        // (1♣) 1♠ (P) 2♥ (P) 2♠ (P): the mechanical completion denied extras,
        // so the raiser drives to game with 14+ and rests below it otherwise.
        let auction = [
            call(1, Strain::Clubs),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Hearts),
            Call::Pass,
            call(2, Strain::Spades),
            Call::Pass,
        ];
        assert_eq!(best(&auction, "K54.A32.KQ32.Q43"), call(4, Strain::Spades));
        assert_ne!(best(&auction, "K54.K32.K43.Q432"), call(4, Strain::Spades));
    }

    #[test]
    fn rubens_cue_answer_places_the_contract() {
        // (1♠) 2♣ (P) 2♠ (P): partner's cue-raise must never play their suit.
        let auction = [
            call(1, Strain::Spades),
            call(2, Strain::Clubs),
            Call::Pass,
            call(2, Strain::Spades),
            Call::Pass,
        ];
        // A minimum retreats to our suit.
        assert_eq!(best(&auction, "32.K32.Q32.AQJT54"), call(3, Strain::Clubs));
        // A maximum with their suit stopped places the notrump game.
        assert_eq!(best(&auction, "A2.K32.Q2.AKQJ54"), call(3, Strain::Notrump));
        // (1♠) 2♥ (P) 2♠ (P): a maximum with hearts places the major game.
        let hearts = [
            call(1, Strain::Spades),
            call(2, Strain::Hearts),
            Call::Pass,
            call(2, Strain::Spades),
            Call::Pass,
        ];
        assert_eq!(best(&hearts, "2.AKQJ54.K32.Q32"), call(4, Strain::Hearts));
        assert_eq!(best(&hearts, "Q2.AQJT54.432.32"), call(3, Strain::Hearts));
    }

    #[test]
    fn rubens_cue_answer_fires_through_the_system() {
        // The same node reached through `american()`: the floor rule must not
        // be shadowed by a book node (`project_floor_shadowed_by_book_nodes`),
        // or the cue keeps passing out at the real table.
        let auction = [
            call(1, Strain::Spades),
            call(2, Strain::Clubs),
            Call::Pass,
            call(2, Strain::Spades),
            Call::Pass,
        ];
        let (call_made, floored) = american_floored(&auction, "32.K32.Q32.AQJT54");
        assert!(floored, "the overcaller's cue answer is floor territory");
        assert_eq!(call_made, call(3, Strain::Clubs));
    }

    #[test]
    fn rubens_skips_advances_of_a_double() {
        // (1♥) X (P) 1♠: the side's first action was a double, so 1♠ advances
        // the double — the doubler's later cue is not a Rubens structure.
        let auction = [
            call(1, Strain::Hearts),
            Call::Double,
            Call::Pass,
            call(1, Strain::Spades),
        ];
        assert_eq!(overcall_shape(&auction), None);
    }

    #[test]
    fn rubens_disabled_reverts_to_natural_advances() {
        // Knob off (`set_rubens_advances`): the same hands advance naturally.
        set_rubens_advances(false);
        let auction = [call(1, Strain::Clubs), call(1, Strain::Spades), Call::Pass];
        // The limit raise is a direct natural raise, not the 2♥ transfer.
        assert_eq!(best(&auction, "K54.K32.K43.Q432"), call(2, Strain::Spades));
        // A five-card-diamond good 9 bids its suit naturally (the knob-off
        // fallback rule), not the 2♣ transfer.
        assert_eq!(
            best(&auction, "2.K32.KQT54.J432"),
            call(2, Strain::Diamonds)
        );
        // No mechanical completion: partner's 2♣ is a genuine club suit.
        let after = [
            call(1, Strain::Clubs),
            call(1, Strain::Spades),
            Call::Pass,
            call(2, Strain::Clubs),
            Call::Pass,
        ];
        assert_ne!(best(&after, "AKJ52.K3.952.J32"), call(2, Strain::Diamonds));
        set_rubens_advances(true);
    }

    #[test]
    fn one_nt_runout_disabled_passes() {
        // Disabled, responder has no runout and falls to the natural floor —
        // Pass — even broke with a five-card suit.
        set_one_nt_runout(false);
        let doubled = [call(1, Strain::Notrump), Call::Double];
        assert_eq!(best(&doubled, "32.QJ763.9742.83"), Call::Pass);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_escapes_to_the_long_suit() {
        set_one_nt_runout(true);
        let doubled = [call(1, Strain::Notrump), Call::Double];
        // A broke hand with five hearts runs to 2♥ rather than sit for it.
        assert_eq!(best(&doubled, "32.QJ763.9742.83"), call(2, Strain::Hearts));
        // Length beats the major preference: six clubs over five spades.
        assert_eq!(best(&doubled, "T9842.3.7.QJ9632"), call(2, Strain::Clubs));
        // A balanced bust has nowhere to run: it sits.
        assert_eq!(best(&doubled, "432.J85.K74.9632"), Call::Pass);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_redoubles_with_values() {
        set_one_nt_runout(true);
        set_runout_xx_min(8);
        let doubled = [call(1, Strain::Notrump), Call::Double];
        // 8 balanced HCP — too good to run, not enough to force game opposite a
        // 15–17 opener (23 combined): redouble to play 1NT-XX.
        assert_eq!(best(&doubled, "K43.KQ5.8642.972"), Call::Redouble);
        // A shapely bust at the same boundary still runs, never redoubles.
        assert_eq!(best(&doubled, "3.QJ763.97642.83"), call(2, Strain::Hearts));
        set_runout_xx_min(7);
        set_one_nt_runout(true);
    }

    #[test]
    fn gambling_3nt_over_double_routes_long_minors() {
        set_one_nt_runout(true);
        set_gambling_3nt_over_double(true);
        set_gambling_3nt_top_honors(2);
        set_gambling_3nt_require_ace(true);
        let doubled = [call(1, Strain::Notrump), Call::Double];

        // A six-card minor headed by its own ace (semi-solid, suit ace) runs to the
        // gambling 3NT — opposite the 15–17 opener the suit cashes — not XX, not an
        // escape.
        assert_eq!(best(&doubled, "32.43.654.AKJ987"), call(3, Strain::Notrump));
        assert_eq!(best(&doubled, "32.43.AKJ987.654"), call(3, Strain::Notrump));

        // A strong balanced hand holds no six-card minor, so the gamble can never
        // steal it: it still defends the business redouble.
        assert_eq!(best(&doubled, "KQ4.KJ43.AQ62.Q5"), Call::Redouble);

        // The suit-ace gate (default on): a semi-solid six-bagger missing its own ace
        // cannot gamble — it escapes.  Drop the requirement and it gambles.
        assert_eq!(best(&doubled, "32.43.654.KQJ987"), call(2, Strain::Clubs));
        set_gambling_3nt_require_ace(false);
        assert_eq!(best(&doubled, "32.43.654.KQJ987"), call(3, Strain::Notrump));
        set_gambling_3nt_require_ace(true);

        // The semi-solid gate: an ace-headed but ragged six-bagger (one top honor)
        // escapes; length-only (top-honors 0) lets it gamble instead.
        assert_eq!(best(&doubled, "32.43.654.AJ9876"), call(2, Strain::Clubs));
        set_gambling_3nt_top_honors(0);
        assert_eq!(best(&doubled, "32.43.654.AJ9876"), call(3, Strain::Notrump));

        set_gambling_3nt_top_honors(2);
        set_gambling_3nt_over_double(false);
        set_one_nt_runout(true);
    }

    #[test]
    fn preempt_4m_over_double_jumps_the_long_major() {
        set_one_nt_runout(true);
        set_preempt_4m_over_double(true);
        set_preempt_4m_top_honors(2);
        set_preempt_4m_require_ace(true);
        let doubled = [call(1, Strain::Notrump), Call::Double];

        // A semi-solid six-card major headed by the trump ace jumps to its game.
        assert_eq!(best(&doubled, "432.AKJ987.65.32"), call(4, Strain::Hearts));
        assert_eq!(best(&doubled, "AKJ987.432.65.32"), call(4, Strain::Spades));

        // The trump-ace gate (default on): a KQ-headed six-bagger lacking the trump
        // ace does not preempt to game (a 6-count escapes); drop it and it jumps.
        assert_eq!(best(&doubled, "432.KQJ987.65.32"), call(2, Strain::Hearts));
        set_preempt_4m_require_ace(false);
        assert_eq!(best(&doubled, "432.KQJ987.65.32"), call(4, Strain::Hearts));
        set_preempt_4m_require_ace(true);

        // The semi-solid gate: an ace-headed but ragged six-bagger escapes;
        // length-only (top-honors 0) lets it preempt.
        assert_eq!(best(&doubled, "432.AJ9876.65.32"), call(2, Strain::Hearts));
        set_preempt_4m_top_honors(0);
        assert_eq!(best(&doubled, "432.AJ9876.65.32"), call(4, Strain::Hearts));

        set_preempt_4m_top_honors(2);
        set_preempt_4m_over_double(false);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_2nt_scrambles_the_minors() {
        set_one_nt_runout(true);
        // The 2NT relay is the opt-in `FourFour` mode (the default is `Direct`).
        set_unusual_2nt(Unusual2nt::FourFour);
        // 4-4 in the minors, no five-card suit, broke: 2NT asks opener to pick.
        let doubled = [call(1, Strain::Notrump), Call::Double];
        assert_eq!(best(&doubled, "K3.842.Q642.J642"), call(2, Strain::Notrump));
        // Opener names the longer minor: clubs here, diamonds when reversed —
        // never blindly "completing" 2NT as a diamond transfer.
        let after = [
            call(1, Strain::Notrump),
            Call::Double,
            call(2, Strain::Notrump),
            Call::Pass,
        ];
        assert_eq!(best(&after, "AQ5.KQ4.32.AK842"), call(3, Strain::Clubs));
        assert_eq!(best(&after, "AQ5.KQ4.AK842.32"), call(3, Strain::Diamonds));
        set_unusual_2nt(Unusual2nt::Direct);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_2nt_shape_modes() {
        set_one_nt_runout(true);
        let doubled = [call(1, Strain::Notrump), Call::Double];
        // A weak 5-5 in the minors.  In `FourFour` it escapes naturally to a
        // five-card minor; the 2NT scramble is only the no-five-card-suit action.
        set_unusual_2nt(Unusual2nt::FourFour);
        assert_ne!(best(&doubled, "3.42.KQ876.J8765"), call(2, Strain::Notrump));
        // FiveFiveAdd routes the 5-5 hand through 2NT so opener picks the better
        // minor instead of responder guessing.
        set_unusual_2nt(Unusual2nt::FiveFiveAdd);
        assert_eq!(best(&doubled, "3.42.KQ876.J8765"), call(2, Strain::Notrump));
        // Direct suppresses 2NT: the 4-4 bust runs straight to its longer minor
        // (ties to diamonds) at the two level.
        set_unusual_2nt(Unusual2nt::Direct);
        assert_eq!(
            best(&doubled, "K3.842.Q642.J642"),
            call(2, Strain::Diamonds)
        );
        set_unusual_2nt(Unusual2nt::Direct);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_penalizes_escape_on_stack() {
        set_one_nt_runout(true);
        set_penalize_escape_values(false);
        // 1NT-(X)-XX (business redouble); RHO runs to 2♣.  A club stack (and not
        // short in their suit, so the floor would not take out) doubles the run
        // for penalty.  Toggling the arm off withdraws the double.
        let run = [
            call(1, Strain::Notrump),
            Call::Double,
            Call::Redouble,
            call(2, Strain::Clubs),
        ];
        set_penalize_escape_stack(true);
        assert_eq!(best(&run, "Q52.K43.Q43.AKJ4"), Call::Double);
        set_penalize_escape_stack(false);
        assert_ne!(best(&run, "Q52.K43.Q43.AKJ4"), Call::Double);
        set_penalize_escape_stack(true);
        set_penalize_escape_values(true);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_leaves_in_escape_penalty() {
        set_one_nt_runout(true);
        set_penalize_escape_stack(true);
        // 1NT-(X)-XX-(2♣)-X-(P): partner doubled their run for penalty.  We pass
        // to leave it in, never advancing it as if it were a takeout double.
        let doubled_run = [
            call(1, Strain::Notrump),
            Call::Double,
            Call::Redouble,
            call(2, Strain::Clubs),
            Call::Double,
            Call::Pass,
        ];
        assert_eq!(best(&doubled_run, "KQ3.K54.J632.987"), Call::Pass);
        set_penalize_escape_stack(true);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_penalizes_escape_on_values() {
        set_one_nt_runout(true);
        set_penalize_escape_stack(false);
        set_penalize_escape_values(true);
        // After responder's business redouble shows values, opener doubles their
        // run on general strength — no personal trump stack, and not short in
        // their suit, so the double is ours and not the floor's takeout.
        let run = [
            call(1, Strain::Notrump),
            Call::Double,
            Call::Redouble,
            call(2, Strain::Clubs),
        ];
        assert_eq!(best(&run, "AQ5.KQ43.K3.6432"), Call::Double);
        // The chase recurses: they run on to 2♦, the values hand doubles again.
        let again = [
            call(1, Strain::Notrump),
            Call::Double,
            Call::Redouble,
            call(2, Strain::Clubs),
            Call::Double,
            call(2, Strain::Diamonds),
        ];
        assert_eq!(best(&again, "KQ3.K54.J632.987"), Call::Double);
        // But opener's *SOS* redouble shows no values, so the values arm stays
        // silent there: 1NT-(X)-P-P-XX(SOS)-(2♣) is not a values double.
        let sos = [
            call(1, Strain::Notrump),
            Call::Double,
            Call::Pass,
            Call::Pass,
            Call::Redouble,
            call(2, Strain::Clubs),
        ];
        assert_ne!(best(&sos, "J32.Q54.J632.987"), Call::Double);
        set_penalize_escape_stack(true);
        set_penalize_escape_values(true);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_universal_opener_escapes_and_sos() {
        set_one_nt_runout(true);
        set_one_nt_runout_universal(true);
        // Balancing seat (1NT-X-P-P): partner is broke, opener acts rather than
        // sit 1NT-X.  A minimum with five spades runs to 2♠.
        let balancing = [
            call(1, Strain::Notrump),
            Call::Double,
            Call::Pass,
            Call::Pass,
        ];
        assert_eq!(
            best(&balancing, "AQ542.KJ.K43.Q32"),
            call(2, Strain::Spades)
        );
        // A minimum with no five-card suit SOS-redoubles instead.
        assert_eq!(best(&balancing, "AQ4.KJ2.K432.Q32"), Call::Redouble);
        // Responder answers the SOS with its longest suit, a four-carder.
        let after_sos = [
            call(1, Strain::Notrump),
            Call::Double,
            Call::Pass,
            Call::Pass,
            Call::Redouble,
            Call::Pass,
        ];
        assert_eq!(
            best(&after_sos, "QJ32.842.642.J32"),
            call(2, Strain::Spades)
        );
        set_one_nt_runout_universal(true);
        set_one_nt_runout(true);
    }

    #[test]
    fn one_nt_runout_opener_passes_not_completes_phantom_transfer() {
        set_one_nt_runout(true);
        // 1NT–(X)–2♥ is partner's *runout*, not a Jacoby transfer: opener passes
        // rather than "complete" it to 2♠ (responder's short suit).
        let after_runout = [
            call(1, Strain::Notrump),
            Call::Double,
            call(2, Strain::Hearts),
            Call::Pass,
        ];
        assert_eq!(best(&after_runout, "AQ4.KJ3.KQ52.432"), Call::Pass);
        set_one_nt_runout(true);
    }
}