diff --git a/docs/results/KlondikeDraw3_results_20261014_220504.csv b/docs/results/KlondikeDraw3_results_20261014_220504.csv new file mode 100644 index 0000000..c5a9b27 --- /dev/null +++ b/docs/results/KlondikeDraw3_results_20261014_220504.csv @@ -0,0 +1,512 @@ +# Klondike Draw 3 with the stock misalignment term in the IDA* heuristic (weights plan, phase 3), on top of the two weight refactors already committed (d4f1197: inert priorities and DEADLOCK_PENALTY removed, heuristic clamped at zero; 516656b: checkpoint adoption thresholds expressed by foundation count). Phase 3: BoardEval::from_board_fast counts the next foundation cards sitting in the remaining stock at a position the current pass skips (offset from the pile card not a multiple of the draw size, last card excluded), and heuristic_cost adds MISALIGNED_TARGET_COST = 2 per such card while hidden cards remain. Draw 1 is unaffected by construction (the term is zero). Seed not recorded in this CSV; not pairable game by game with earlier runs. +# Result: 500 games; 376 wins (75.20%); 124 stalled; 0 timeouts; average duration 7.49 s (stalled 10.48 s, won 6.50 s). Previous Draw 3 run (12 Oct, KlondikeDraw3_results_20261012_162902.csv): 70.00%, 5.70 s. +5.2 points is just outside the ~4-point noise between two unseeded runs of 500, and the shape matches a better heuristic: 251 games won without a checkpoint (211 before) and 0.35 checkpoints per won game (0.43 before). Caveat: duration rose 31% overall and ~47% in stalled games with checkpoints, which spend the full node budget, so the cost per node or the search dynamics changed; a seeded pair with MISALIGNED_TARGET_COST = 0 vs 2 on this build is needed to isolate the term and its time cost before accepting it. +configuration +game,search,simulations,parallel_games,auto_parallel,undos,timeout_s,max_depth +"Klondike Draw 3","IDA*",500,10,false,-1,0.00,150 + +summary +games,active_timeout,win_rate,avg_moves,avg_undos,avg_checkpoints,avg_duration,wins,stalled,timeouts +500,00:00,75.20%,95.62,0.17,0.46,7.49s,376,124,0 + +details +game_id,moves,undos,checkpoints,result,stop_reason,duration_sec,score +1,77,0,2,lost,stalled,14.282,535 +2,107,0,0,won,win,9.345,820 +3,96,0,0,won,win,0.797,740 +4,95,0,1,won,win,12.046,750 +5,108,0,1,won,win,13.145,810 +6,98,0,0,won,win,0.978,745 +7,73,0,1,lost,stalled,13.889,355 +8,74,0,2,lost,stalled,13.302,430 +9,118,0,1,won,win,13.016,810 +10,112,0,0,won,win,7.124,790 +11,107,0,0,won,win,0.432,785 +12,105,0,0,won,win,1.747,810 +13,113,0,1,won,win,19.794,805 +14,48,0,1,lost,stalled,26.430,185 +15,126,3,1,won,win,22.528,795 +16,98,0,0,won,win,3.382,760 +17,99,0,0,won,win,8.662,775 +18,98,0,1,won,win,19.837,760 +19,104,0,0,won,win,0.694,810 +20,129,0,0,won,win,5.014,780 +21,75,0,1,lost,stalled,15.390,270 +22,99,0,1,won,win,13.279,780 +23,104,0,0,won,win,2.563,780 +24,111,0,0,won,win,0.297,735 +25,59,0,1,lost,stalled,15.379,350 +26,111,0,0,won,win,1.744,795 +27,99,0,1,won,win,12.680,755 +28,124,1,1,won,win,14.140,785 +29,100,0,1,won,win,12.016,795 +30,110,0,1,won,win,11.705,800 +31,72,0,1,lost,stalled,16.870,410 +32,105,0,0,won,win,14.606,785 +33,114,1,1,won,win,14.892,800 +34,113,1,0,won,win,13.651,775 +35,106,0,1,won,win,14.711,790 +36,102,0,0,won,win,0.701,795 +37,61,0,1,lost,stalled,14.017,345 +38,110,1,1,won,win,12.807,775 +39,107,0,1,won,win,15.740,795 +40,110,0,1,won,win,13.628,845 +41,112,2,0,won,win,0.991,755 +42,61,0,1,lost,stalled,15.453,325 +43,83,0,1,lost,stalled,13.214,515 +44,107,0,0,won,win,1.181,790 +45,104,0,1,won,win,11.308,820 +46,108,0,0,won,win,0.513,775 +47,117,0,1,won,win,10.704,835 +48,138,0,0,won,win,5.576,810 +49,95,0,0,won,win,2.709,755 +50,111,0,0,won,win,6.300,770 +51,99,0,1,won,win,12.739,760 +52,123,0,1,won,win,13.643,865 +53,114,0,0,won,win,0.147,785 +54,107,2,1,won,win,16.452,755 +55,108,0,0,won,win,1.863,795 +56,100,0,1,won,win,13.278,770 +57,112,0,1,won,win,13.163,785 +58,69,0,1,lost,stalled,15.044,465 +59,106,0,0,won,win,0.330,780 +60,65,1,1,lost,stalled,17.862,290 +61,60,0,1,lost,stalled,15.751,335 +62,100,0,0,won,win,3.354,760 +63,105,0,0,won,win,9.639,800 +64,101,0,0,won,win,0.514,765 +65,116,0,0,won,win,0.539,760 +66,63,0,0,lost,stalled,0.121,155 +67,107,0,1,won,win,14.152,800 +68,100,0,0,won,win,3.703,785 +69,112,1,0,won,win,1.572,785 +70,64,0,2,lost,stalled,17.407,385 +71,98,0,0,won,win,0.145,775 +72,95,1,1,lost,stalled,20.132,425 +73,115,0,1,won,win,20.050,785 +74,49,0,0,lost,stalled,0.012,115 +75,10,0,0,lost,stalled,0.000,20 +76,153,0,0,won,win,2.532,840 +77,104,0,0,won,win,0.450,755 +78,99,1,0,won,win,1.500,760 +79,103,0,0,won,win,6.135,805 +80,113,0,0,won,win,1.113,810 +81,107,0,0,won,win,1.314,780 +82,106,0,0,won,win,0.348,780 +83,48,0,0,lost,stalled,0.031,120 +84,117,1,0,won,win,5.577,800 +85,104,0,0,won,win,0.037,785 +86,100,0,0,won,win,0.241,750 +87,94,0,0,won,win,0.303,795 +88,111,0,0,won,win,0.388,755 +89,66,0,2,lost,stalled,17.332,370 +90,103,0,0,won,win,0.366,765 +91,43,0,1,lost,stalled,14.015,270 +92,112,1,1,won,win,13.980,780 +93,110,0,0,won,win,1.806,810 +94,60,0,1,lost,stalled,13.563,350 +95,104,0,0,won,win,0.677,780 +96,106,0,1,won,win,13.009,800 +97,101,0,0,won,win,0.148,765 +98,106,0,0,won,win,1.166,780 +99,97,0,0,won,win,0.191,770 +100,99,0,0,won,win,12.533,750 +101,100,0,0,won,win,6.204,795 +102,116,3,1,won,win,12.663,815 +103,79,0,1,lost,stalled,15.094,360 +104,115,1,0,won,win,5.792,780 +105,106,0,1,won,win,11.915,785 +106,114,0,1,won,win,12.923,850 +107,101,0,0,won,win,2.180,775 +108,65,0,1,lost,stalled,14.908,280 +109,11,0,0,lost,stalled,0.000,20 +110,103,0,0,won,win,0.028,795 +111,130,2,1,won,win,13.623,820 +112,100,1,0,won,win,3.726,760 +113,94,0,0,won,win,0.083,750 +114,112,0,0,won,win,2.096,815 +115,131,0,1,won,win,13.986,805 +116,53,0,1,lost,stalled,13.824,260 +117,114,0,1,won,win,14.324,805 +118,77,0,1,lost,stalled,13.446,475 +119,107,0,1,won,win,10.949,765 +120,115,0,1,won,win,12.407,800 +121,102,0,0,won,win,1.469,720 +122,28,0,0,lost,stalled,0.202,105 +123,95,1,2,lost,stalled,15.294,460 +124,100,0,0,won,win,0.497,785 +125,7,0,0,lost,stalled,0.000,15 +126,129,0,1,won,win,13.720,810 +127,109,0,0,won,win,0.861,760 +128,102,0,0,won,win,4.051,810 +129,104,0,0,won,win,0.638,790 +130,104,0,0,won,win,5.849,780 +131,110,0,1,won,win,15.932,785 +132,124,0,1,won,win,14.416,805 +133,117,0,1,won,win,14.382,830 +134,63,0,0,lost,stalled,0.018,150 +135,112,1,1,won,win,15.203,775 +136,99,0,0,won,win,0.161,785 +137,101,0,0,won,win,3.536,800 +138,88,0,1,lost,stalled,12.278,550 +139,122,1,2,won,win,16.824,830 +140,110,0,0,won,win,0.714,770 +141,102,0,0,won,win,1.402,770 +142,107,0,0,won,win,0.169,790 +143,66,0,1,lost,stalled,12.873,400 +144,36,0,0,lost,stalled,0.047,90 +145,115,2,0,won,win,6.607,840 +146,103,0,0,won,win,3.517,800 +147,99,0,1,won,win,13.002,790 +148,3,0,0,lost,stalled,0.000,35 +149,93,0,0,won,win,0.046,720 +150,116,0,0,won,win,2.936,815 +151,117,0,0,won,win,5.094,790 +152,118,0,0,won,win,1.107,805 +153,95,0,0,won,win,5.461,780 +154,43,0,0,lost,stalled,1.097,110 +155,125,0,1,won,win,14.091,830 +156,112,0,0,won,win,0.124,815 +157,47,0,1,lost,stalled,13.322,230 +158,111,0,1,won,win,9.942,810 +159,104,0,1,won,win,10.269,745 +160,114,0,1,won,win,10.838,805 +161,81,1,1,lost,stalled,14.071,475 +162,105,0,0,won,win,3.833,770 +163,15,0,0,lost,stalled,0.000,50 +164,110,0,1,won,win,12.985,785 +165,72,0,1,lost,stalled,16.261,290 +166,114,1,1,won,win,13.249,825 +167,120,0,0,won,win,2.484,780 +168,94,0,1,won,win,11.112,745 +169,83,0,1,lost,stalled,16.654,290 +170,99,0,0,won,win,1.696,745 +171,118,0,0,won,win,12.258,830 +172,98,0,0,won,win,0.291,755 +173,120,0,0,won,win,2.490,780 +174,100,0,0,won,win,4.275,755 +175,52,0,1,lost,stalled,14.286,245 +176,110,0,1,won,win,13.662,810 +177,107,0,0,won,win,1.163,770 +178,68,0,1,lost,stalled,12.455,380 +179,67,1,1,lost,stalled,14.464,285 +180,102,0,1,won,win,12.451,795 +181,119,0,0,won,win,5.423,785 +182,24,0,0,lost,stalled,0.000,45 +183,111,0,0,won,win,3.228,785 +184,117,0,0,won,win,3.269,790 +185,99,0,0,won,win,1.457,775 +186,114,0,0,won,win,0.285,810 +187,122,0,1,won,win,13.490,850 +188,41,0,1,lost,stalled,14.674,115 +189,102,0,0,won,win,0.279,770 +190,107,0,0,won,win,5.442,755 +191,99,0,0,won,win,0.557,745 +192,104,0,0,won,win,4.262,760 +193,116,0,1,won,win,12.636,790 +194,106,2,0,won,win,1.543,750 +195,0,0,0,lost,stalled,0.000,0 +196,111,0,0,won,win,0.632,780 +197,122,0,1,won,win,14.037,815 +198,8,0,0,lost,stalled,0.000,65 +199,0,0,0,lost,stalled,0.000,0 +200,105,0,0,won,win,12.774,795 +201,101,0,0,won,win,9.458,775 +202,33,0,0,lost,stalled,0.070,100 +203,36,0,1,lost,stalled,15.266,145 +204,111,0,0,won,win,3.209,805 +205,105,0,0,won,win,8.947,780 +206,118,0,1,won,win,11.708,820 +207,77,0,1,lost,stalled,17.091,345 +208,130,0,1,won,win,18.818,815 +209,117,1,1,won,win,13.010,810 +210,99,0,0,won,win,0.115,775 +211,131,1,0,won,win,5.762,845 +212,80,1,2,lost,stalled,17.258,395 +213,106,0,1,won,win,11.999,780 +214,107,0,0,won,win,1.475,745 +215,107,0,1,won,win,14.038,805 +216,119,0,0,won,win,1.977,795 +217,66,1,1,lost,stalled,14.998,350 +218,106,0,1,won,win,13.687,805 +219,74,1,1,lost,stalled,16.055,345 +220,115,0,1,won,win,12.576,800 +221,105,1,0,won,win,2.622,805 +222,132,1,1,won,win,13.021,795 +223,75,0,2,lost,stalled,15.703,355 +224,98,0,0,won,win,1.558,780 +225,101,0,0,won,win,1.169,795 +226,105,0,0,won,win,11.193,795 +227,102,0,0,won,win,1.755,765 +228,102,0,0,won,win,4.772,765 +229,94,0,1,lost,stalled,12.403,685 +230,141,1,0,won,win,2.523,820 +231,41,0,0,lost,stalled,7.336,130 +232,128,0,1,won,win,14.620,865 +233,97,0,0,won,win,0.701,770 +234,89,0,1,lost,stalled,16.225,520 +235,102,0,0,won,win,0.955,810 +236,112,0,0,won,win,2.232,790 +237,122,0,0,won,win,0.680,830 +238,56,0,1,lost,stalled,14.062,300 +239,118,0,0,won,win,2.358,795 +240,116,1,1,won,win,11.807,835 +241,142,1,1,won,win,14.492,915 +242,98,0,0,won,win,0.080,750 +243,116,0,0,won,win,1.766,810 +244,100,0,0,won,win,2.737,770 +245,125,0,1,won,win,14.813,805 +246,113,0,0,won,win,2.589,790 +247,108,0,1,won,win,11.945,770 +248,115,0,0,won,win,0.965,800 +249,59,0,1,lost,stalled,12.527,410 +250,102,0,0,won,win,1.647,770 +251,108,0,1,won,win,13.824,785 +252,70,0,2,lost,stalled,16.678,370 +253,119,0,0,won,win,0.156,795 +254,100,0,0,won,win,6.381,755 +255,88,0,0,won,win,1.237,765 +256,101,0,1,won,win,12.544,795 +257,108,2,0,won,win,0.519,760 +258,108,0,1,won,win,11.957,755 +259,126,2,2,won,win,13.289,885 +260,0,0,0,lost,stalled,0.000,0 +261,119,0,1,won,win,13.657,810 +262,108,1,0,won,win,0.734,780 +263,91,0,0,won,win,0.014,755 +264,111,0,2,won,win,14.447,845 +265,103,0,0,won,win,10.254,760 +266,99,0,0,won,win,1.094,795 +267,120,1,0,won,win,3.191,775 +268,97,0,0,won,win,0.202,740 +269,99,0,1,lost,stalled,13.211,555 +270,101,0,0,won,win,0.705,815 +271,45,0,1,lost,stalled,14.125,200 +272,106,1,0,won,win,3.648,770 +273,39,0,1,lost,stalled,13.985,205 +274,101,0,0,won,win,4.414,760 +275,83,0,1,lost,stalled,13.575,355 +276,109,0,1,won,win,14.129,810 +277,97,0,0,won,win,0.008,780 +278,124,0,2,won,win,13.466,865 +279,127,0,1,won,win,13.760,830 +280,114,0,0,won,win,0.052,810 +281,0,0,0,lost,stalled,0.000,0 +282,108,1,1,won,win,14.622,765 +283,100,0,0,won,win,0.163,790 +284,99,0,1,won,win,10.916,790 +285,100,0,0,won,win,2.622,760 +286,106,0,0,won,win,8.297,795 +287,119,0,0,won,win,1.149,785 +288,46,0,0,lost,stalled,2.255,140 +289,114,0,0,won,win,0.265,780 +290,115,1,0,won,win,2.406,800 +291,53,0,1,lost,stalled,14.352,195 +292,62,0,1,lost,stalled,15.544,365 +293,114,0,0,won,win,10.366,760 +294,115,0,0,won,win,1.488,755 +295,96,0,0,won,win,0.057,780 +296,112,0,1,won,win,12.491,810 +297,100,0,0,won,win,3.092,800 +298,112,1,1,won,win,13.634,775 +299,137,1,0,won,win,13.865,870 +300,69,0,1,lost,stalled,15.710,360 +301,107,0,1,won,win,12.241,800 +302,106,0,0,won,win,1.050,795 +303,107,0,0,won,win,5.961,805 +304,135,1,1,won,win,11.406,820 +305,112,0,1,won,win,13.574,820 +306,94,1,3,lost,stalled,17.161,535 +307,109,1,0,won,win,1.072,760 +308,134,0,3,won,win,16.984,890 +309,42,0,0,lost,stalled,4.310,180 +310,105,0,0,won,win,3.462,745 +311,39,0,1,lost,stalled,13.252,145 +312,110,0,0,won,win,0.553,770 +313,118,0,0,won,win,1.612,800 +314,53,0,1,lost,stalled,13.964,165 +315,111,0,0,won,win,3.720,775 +316,109,0,1,won,win,10.557,765 +317,119,0,1,won,win,10.939,820 +318,99,0,0,won,win,0.545,760 +319,92,0,0,won,win,0.243,750 +320,52,0,1,lost,stalled,14.367,210 +321,111,1,0,won,win,11.127,805 +322,101,0,0,won,win,2.266,760 +323,107,0,1,won,win,12.721,795 +324,73,0,1,lost,stalled,17.718,175 +325,61,1,1,lost,stalled,15.663,355 +326,105,0,1,won,win,11.764,830 +327,106,0,0,won,win,1.252,770 +328,65,0,1,lost,stalled,14.698,390 +329,114,1,1,won,win,11.993,805 +330,64,0,1,lost,stalled,13.971,360 +331,103,0,0,won,win,2.292,810 +332,105,0,0,won,win,3.828,810 +333,111,0,0,won,win,7.127,785 +334,100,0,0,won,win,8.166,785 +335,101,0,0,won,win,2.730,805 +336,111,1,1,won,win,13.227,765 +337,102,0,1,won,win,9.948,780 +338,112,0,1,won,win,11.217,770 +339,94,0,0,won,win,0.267,735 +340,98,0,0,won,win,0.257,760 +341,99,0,0,won,win,1.323,760 +342,48,0,0,lost,stalled,1.834,175 +343,109,0,1,won,win,11.237,790 +344,103,0,1,won,win,10.229,780 +345,115,0,0,won,win,1.139,805 +346,99,0,0,won,win,2.654,785 +347,117,1,1,won,win,12.472,800 +348,101,0,0,won,win,0.007,755 +349,88,0,1,lost,stalled,15.051,355 +350,91,0,0,won,win,0.112,745 +351,108,0,0,won,win,0.664,780 +352,101,0,0,won,win,0.451,785 +353,121,0,1,won,win,13.928,835 +354,101,0,0,won,win,2.832,765 +355,120,0,0,won,win,0.836,810 +356,99,0,0,won,win,0.375,790 +357,75,0,1,lost,stalled,13.287,480 +358,97,0,0,won,win,3.175,780 +359,48,0,0,lost,stalled,0.170,125 +360,112,0,1,won,win,15.214,810 +361,98,0,0,won,win,8.701,780 +362,105,0,0,won,win,0.028,785 +363,109,0,0,won,win,0.082,800 +364,81,0,1,lost,stalled,13.920,365 +365,117,0,0,won,win,5.595,780 +366,117,0,0,won,win,6.305,830 +367,100,0,1,won,win,11.528,800 +368,67,0,1,lost,stalled,12.806,405 +369,108,0,1,won,win,12.069,810 +370,99,0,0,won,win,3.626,780 +371,31,0,0,lost,stalled,0.001,50 +372,98,0,0,won,win,1.383,790 +373,103,0,0,won,win,12.771,785 +374,96,0,0,won,win,9.833,745 +375,95,0,0,won,win,0.421,760 +376,59,0,1,lost,stalled,13.647,350 +377,100,0,0,won,win,0.119,765 +378,101,0,0,won,win,0.656,765 +379,109,0,0,won,win,2.675,760 +380,94,0,1,won,win,11.685,740 +381,106,0,0,won,win,8.905,785 +382,2,0,0,lost,stalled,0.000,15 +383,111,0,1,won,win,11.502,805 +384,116,0,0,won,win,6.385,790 +385,106,0,0,won,win,0.441,785 +386,110,0,0,won,win,1.379,735 +387,107,0,0,won,win,0.079,785 +388,100,0,0,won,win,2.986,755 +389,0,0,0,lost,stalled,0.000,0 +390,103,0,0,won,win,2.045,780 +391,61,0,1,lost,stalled,12.562,340 +392,120,1,0,won,win,3.950,830 +393,136,1,0,won,win,11.921,825 +394,25,0,0,lost,stalled,0.000,95 +395,69,0,1,lost,stalled,11.221,430 +396,74,0,1,lost,stalled,12.078,385 +397,76,0,2,lost,stalled,14.573,415 +398,107,0,0,won,win,1.490,765 +399,93,0,2,lost,stalled,14.102,385 +400,92,1,1,lost,stalled,13.588,525 +401,119,0,1,won,win,14.646,815 +402,105,0,0,won,win,0.028,775 +403,103,0,0,won,win,0.741,815 +404,142,0,0,won,win,12.609,775 +405,46,0,0,lost,stalled,0.016,110 +406,31,0,1,lost,stalled,13.485,80 +407,98,0,0,won,win,0.143,780 +408,111,0,0,won,win,5.862,775 +409,124,0,1,won,win,16.219,835 +410,118,0,0,won,win,7.291,800 +411,105,0,0,won,win,0.930,785 +412,126,1,1,won,win,13.715,805 +413,114,0,0,won,win,2.599,815 +414,102,0,0,won,win,4.126,765 +415,93,0,0,won,win,0.480,790 +416,133,0,0,won,win,5.138,840 +417,102,1,0,won,win,5.005,785 +418,125,0,1,won,win,14.123,830 +419,94,0,0,won,win,6.603,755 +420,135,1,1,won,win,12.162,835 +421,121,1,1,won,win,13.868,835 +422,100,0,0,won,win,12.426,755 +423,117,0,1,won,win,12.119,790 +424,105,0,0,won,win,14.419,790 +425,57,0,1,lost,stalled,14.648,300 +426,101,0,0,won,win,5.429,770 +427,122,0,1,won,win,15.598,855 +428,97,0,2,lost,stalled,15.875,540 +429,119,0,1,won,win,12.815,840 +430,101,0,1,won,win,12.940,780 +431,122,0,0,won,win,2.531,810 +432,69,0,0,lost,stalled,5.912,110 +433,110,0,1,won,win,12.452,810 +434,100,1,1,won,win,11.078,770 +435,99,0,0,won,win,4.831,755 +436,100,0,1,won,win,10.997,765 +437,41,0,0,lost,stalled,2.407,160 +438,61,2,1,lost,stalled,15.683,195 +439,119,0,0,won,win,2.700,785 +440,57,0,1,lost,stalled,15.490,275 +441,114,0,0,won,win,0.288,795 +442,115,0,0,won,win,1.433,790 +443,94,0,0,won,win,1.146,730 +444,126,0,1,won,win,13.391,850 +445,106,0,0,won,win,6.727,765 +446,100,0,0,won,win,0.472,770 +447,53,0,0,lost,stalled,8.780,185 +448,0,0,0,lost,stalled,0.000,0 +449,105,0,0,won,win,3.553,785 +450,106,0,0,won,win,0.376,790 +451,111,0,1,won,win,11.979,810 +452,104,0,0,won,win,0.195,740 +453,109,0,1,won,win,11.154,770 +454,107,0,1,won,win,12.950,815 +455,65,0,1,lost,stalled,15.299,290 +456,62,1,1,lost,stalled,15.250,320 +457,100,0,0,won,win,4.001,790 +458,113,3,1,won,win,11.921,805 +459,104,0,0,won,win,2.772,800 +460,114,1,1,won,win,14.560,800 +461,116,0,0,won,win,0.152,835 +462,50,0,1,lost,stalled,16.095,215 +463,120,0,0,won,win,2.785,815 +464,106,0,0,won,win,0.739,785 +465,98,0,0,won,win,0.079,750 +466,0,0,0,lost,stalled,0.000,0 +467,62,0,1,lost,stalled,15.656,275 +468,96,0,0,won,win,0.032,755 +469,103,0,0,won,win,0.231,790 +470,52,0,1,lost,stalled,14.742,315 +471,66,0,1,lost,stalled,14.981,375 +472,111,0,0,won,win,7.314,805 +473,36,0,0,lost,stalled,0.030,120 +474,96,0,0,won,win,6.509,730 +475,84,2,1,lost,stalled,15.647,390 +476,3,0,0,lost,stalled,0.001,5 +477,112,0,1,won,win,12.169,805 +478,107,0,0,won,win,1.416,785 +479,102,0,0,won,win,0.276,780 +480,0,0,0,lost,stalled,0.000,0 +481,119,0,1,won,win,13.499,820 +482,101,0,0,won,win,6.690,765 +483,62,1,2,lost,stalled,12.809,370 +484,128,0,0,won,win,9.298,795 +485,100,0,0,won,win,3.899,795 +486,116,0,1,won,win,12.495,805 +487,113,0,0,won,win,9.568,795 +488,103,0,0,won,win,13.921,785 +489,69,0,1,lost,stalled,15.193,380 +490,115,0,1,won,win,14.102,820 +491,112,0,1,won,win,11.841,800 +492,118,0,0,won,win,0.240,810 +493,124,0,0,won,win,1.579,785 +494,102,0,0,won,win,3.293,815 +495,120,0,0,won,win,3.778,790 +496,104,0,0,won,win,1.684,785 +497,65,0,1,lost,stalled,14.313,295 +498,118,2,0,won,win,0.044,785 +499,36,0,0,lost,stalled,0.258,105 +500,81,0,0,lost,stalled,0.405,190 diff --git a/solver-core-rs/src/klondike/board_eval.rs b/solver-core-rs/src/klondike/board_eval.rs index 9cdeea3..a35353b 100644 --- a/solver-core-rs/src/klondike/board_eval.rs +++ b/solver-core-rs/src/klondike/board_eval.rs @@ -26,6 +26,7 @@ pub struct BoardEval { pub foundation_imbalance: i64, // max_rank - min_rank (raw, no tolerance) pub depth_penalty: i64, // sum of fd + (fd-1)/DIVISOR per column pub stock_advance_cost: i64, // minimum advances needed to access the stock + pub misaligned_targets: i64, // next foundation cards the current stock pass skips (draw > 1) pub target_burial_penalty: i64, // penalty for face-down target cards pub deadlock_count: i64, // same-suit hidden dependency severity, not proven deadlocks pub reveal_bonus: i64, // bonus for exposing a key card @@ -98,6 +99,28 @@ impl BoardEval { 0 }; + // Misaligned targets: next foundation cards in the remaining stock that + // this pass skips. A pass exposes the cards at offsets da, 2da, ... from + // the current pile card, plus the last card of the stock. A skipped + // target stays out of reach until a card is played from the stock + // before it, which shifts the alignment. + let mut misaligned_targets = 0i64; + if da > 1 { + let da = da as usize; + let si = board.stock_index as usize; + let len = board.stock_len as usize; + for pos in si..len { + let card = board.stock[pos]; + if card.value != board.foundation[card.suit as usize] + 1 { + continue; + } + let offset = pos - si + 1; + if offset % da != 0 && pos + 1 != len { + misaligned_targets += 1; + } + } + } + Self { foundation_count, face_up, @@ -109,6 +132,7 @@ impl BoardEval { foundation_imbalance: max_rank - min_rank, depth_penalty, stock_advance_cost, + misaligned_targets, target_burial_penalty: 0, deadlock_count: 0, reveal_bonus: 0, @@ -247,13 +271,13 @@ impl BoardEval { // It is the current pile card: accessible now, no penalty. } else if pos < si { // In waste (already passed): requires recycle. - stock_target_penalty += STOCK_WASTE_TARGET_PENALTY as i64; + stock_target_penalty += STOCK_WASTE_TARGET_PENALTY; } else if da > 1 { // In remaining stock: check alignment with draw_advance. let offset = pos - si + 1; if offset % da != 0 { // Misaligned: not accessible in this pass. - stock_target_penalty += STOCK_MISALIGNED_TARGET_PENALTY as i64; + stock_target_penalty += STOCK_MISALIGNED_TARGET_PENALTY; } } break; @@ -266,8 +290,10 @@ impl BoardEval { } /// Heuristic cost for IDA* (scale ~0-80+, lower is better). - /// Maintains admissibility: never overestimates the real cost. - /// Thoughtful signals are NOT included here to preserve admissibility. + /// Informative, not admissible: the penalty terms overestimate the real + /// remaining move count, which trades optimality for search speed. + /// Thoughtful signals are NOT included here: they need the face-down + /// cards and are too expensive for the hot path. pub fn heuristic_cost(&self) -> i64 { // Auto-play detection: if there are no hidden cards and no stock, the game is won. if self.hidden == 0 && self.stock_remaining == 0 && self.waste_count == 0 { @@ -305,10 +331,15 @@ impl BoardEval { // Stock advance cost. cost += self.stock_advance_cost / STOCK_ADVANCE_DIVISOR; + // Draw 3: targets the current stock pass skips. + cost += self.misaligned_targets * MISALIGNED_TARGET_COST; + // Do NOT add target_burial_penalty or deadlock_count here. // Those signals belong ONLY in progress_score and successor_ordering. - cost + // Several empty columns next to an almost complete foundation can push + // the sum below zero; a negative bound has no meaning. + cost.max(0) } /// Progress score for checkpoints (higher is better). @@ -425,19 +456,20 @@ impl CheckpointPolicy { /// Decides whether a progress delta is adoptable given the current phase. /// `foundation_count`: real number of cards in foundations (not derived from score). pub fn is_adoptable(&self, start_score: i64, best_score: i64, foundation_count: usize) -> bool { - let delta = best_score - start_score; + best_score - start_score >= Self::adopt_threshold(foundation_count) + } + /// Progress required to adopt a checkpoint, by foundation count. + pub fn adopt_threshold(foundation_count: usize) -> i64 { let fc = foundation_count as i64; - let (clamp_min, clamp_max) = if fc < ADOPT_EARLY_THRESHOLD_FC { - ADOPT_EARLY_CLAMP + let scaled = fc * ADOPT_PER_FOUNDATION_CARD; + if fc < ADOPT_EARLY_THRESHOLD_FC { + scaled.max(ADOPT_EARLY_MIN) } else if fc < ADOPT_MID_THRESHOLD_FC { - ADOPT_MID_CLAMP + scaled.min(ADOPT_MID_MAX) } else { - ADOPT_LATE_CLAMP - }; - let relative_threshold = (start_score.saturating_abs() * ADOPT_RELATIVE_PCT) / 100; - let adopt_threshold = relative_threshold.clamp(clamp_min, clamp_max); - delta >= adopt_threshold + ADOPT_LATE + } } } @@ -529,6 +561,67 @@ mod tests { assert_eq!(draw_three.stock_advance_cost, 2); } + #[test] + fn misaligned_targets_count_skipped_foundation_cards_in_draw_three_only() { + // One hidden card keeps heuristic_cost on its main branch. + let mut board = KlondikeBoard::new(); + board.columns[0].push(card(Suit::Spade, 9), true); + board.columns[0].push(card(Suit::Heart, 8), false); + // Pile card at 0. With draw 3 the pass exposes positions 3 and the + // last card (5); positions 1, 2 and 4 are skipped. + set_stock( + &mut board, + &[ + card(Suit::Spade, 13), + card(Suit::Club, 1), // skipped + card(Suit::Spade, 10), // + card(Suit::Diamond, 1), // exposed + card(Suit::Heart, 10), // + card(Suit::Heart, 1), // last card: always exposed + ], + 1, + ); + + let draw_one = BoardEval::from_board_fast(&board, 1); + let mut draw_three = BoardEval::from_board_fast(&board, 3); + assert_eq!(draw_one.misaligned_targets, 0); + assert_eq!(draw_three.misaligned_targets, 1); + + let with_term = draw_three.heuristic_cost(); + draw_three.misaligned_targets = 0; + assert_eq!(with_term, draw_three.heuristic_cost() + MISALIGNED_TARGET_COST); + + // Playing the pile card shifts the alignment: A♣ becomes the pile + // card and A♦ (offset 2 from the new pile) is now the skipped one. + board.stock_index = 2; + board.compute_signature(); + assert_eq!(BoardEval::from_board_fast(&board, 3).misaligned_targets, 1); + + // A foundation that no longer needs the card stops counting it. + board.stock_index = 1; + board.foundation[Suit::Club as usize] = 1; + board.compute_signature(); + assert_eq!(BoardEval::from_board_fast(&board, 3).misaligned_targets, 0); + } + + #[test] + fn misaligned_targets_do_not_affect_the_endgame_heuristic() { + let mut board = KlondikeBoard::new(); + board.columns[0].push(card(Suit::Heart, 8), false); + set_stock( + &mut board, + &[card(Suit::Spade, 13), card(Suit::Club, 1), card(Suit::Spade, 10)], + 1, + ); + + let mut eval = BoardEval::from_board_fast(&board, 3); + assert_eq!(eval.hidden, 0); + assert_eq!(eval.misaligned_targets, 1); + let cost = eval.heuristic_cost(); + eval.misaligned_targets = 0; + assert_eq!(cost, eval.heuristic_cost()); + } + #[test] fn blocked_kings_only_count_when_no_empty_columns() { let mut board = board_with_hidden_counts([1, 0, 0, 0, 0, 0, 0]); @@ -722,17 +815,37 @@ mod tests { assert_eq!(draw_three.base_limit_for(&hidden_21), 9_000_000); } + #[test] + fn checkpoint_adopt_threshold_grows_with_foundation_count_within_phase_bounds() { + // Early: floor until the slope passes it. + assert_eq!(CheckpointPolicy::adopt_threshold(0), 150); + assert_eq!(CheckpointPolicy::adopt_threshold(3), 150); + assert_eq!(CheckpointPolicy::adopt_threshold(4), 156); + assert_eq!(CheckpointPolicy::adopt_threshold(9), 351); + // Mid: slope until the cap. + assert_eq!(CheckpointPolicy::adopt_threshold(10), 390); + assert_eq!(CheckpointPolicy::adopt_threshold(25), 975); + assert_eq!(CheckpointPolicy::adopt_threshold(26), 1000); + assert_eq!(CheckpointPolicy::adopt_threshold(29), 1000); + // Late: fixed. + assert_eq!(CheckpointPolicy::adopt_threshold(30), 500); + assert_eq!(CheckpointPolicy::adopt_threshold(51), 500); + } + #[test] fn checkpoint_policy_is_adoptable_uses_phase_thresholds() { let policy = CheckpointPolicy::for_draw(1); - assert!(!policy.is_adoptable(1_000, 1_100, 0)); + // The start score itself does not matter, only the delta. + assert!(!policy.is_adoptable(1_000, 1_149, 0)); assert!(policy.is_adoptable(1_000, 1_150, 0)); + assert!(!policy.is_adoptable(-7_000, -6_851, 0)); + assert!(policy.is_adoptable(-7_000, -6_850, 0)); - assert!(!policy.is_adoptable(1_000, 1_090, 10)); - assert!(policy.is_adoptable(1_000, 1_100, 10)); + assert!(!policy.is_adoptable(13_000, 13_389, 10)); + assert!(policy.is_adoptable(13_000, 13_390, 10)); - assert!(!policy.is_adoptable(100, 119, 30)); - assert!(policy.is_adoptable(100, 120, 30)); + assert!(!policy.is_adoptable(40_000, 40_499, 30)); + assert!(policy.is_adoptable(40_000, 40_500, 30)); } } diff --git a/solver-core-rs/src/klondike/solver.rs b/solver-core-rs/src/klondike/solver.rs index 6519a58..7627760 100644 --- a/solver-core-rs/src/klondike/solver.rs +++ b/solver-core-rs/src/klondike/solver.rs @@ -899,12 +899,8 @@ fn successor_order_key( let crit_bonus = critical_path_bonus(transition, ordering); let safe = is_safe_foundation_move(transition, from_board); let bucket = successor_bucket(transition, from_board, revealed_delta, safe); - let secondary = local_secondary_priority( - transition, - from_board, - exposed_bonus + thoughtful_bonus + crit_bonus, - safe, - ); + let secondary = + local_secondary_priority(transition, exposed_bonus + thoughtful_bonus + crit_bonus, safe); (bucket, std::cmp::Reverse(secondary)) } @@ -964,7 +960,6 @@ fn successor_bucket( fn local_secondary_priority( transition: &KlondikeTransition, - from_board: &KlondikeBoard, exposed_bonus: i64, safe_foundation: bool, ) -> i64 { @@ -980,19 +975,16 @@ fn local_secondary_priority( if m.is_from_foundation() { return weights::PRIORITY_FROM_FOUNDATION; } - if m.is_stock_advance() { - return weights::PRIORITY_STOCK_ADVANCE + exposed_bonus; - } - if m.is_stock_recycle() { - return weights::PRIORITY_STOCK_RECYCLE - draw_cost; + // Stock advances and recycles each own a bucket, so only the draw cost + // can order them; a base priority would be a shared constant. + if m.is_stock_advance() || m.is_stock_recycle() { + return -draw_cost; } + // Moves that empty a column also own a bucket: no bonus for it here. let mut score = weights::PRIORITY_COLUMN_BASE + exposed_bonus - draw_cost; if let KlondikeMove::ColumnToColumn { count, .. } = m { score += (*count as i64).saturating_sub(1) * weights::PRIORITY_STACK_BONUS; - if creates_empty_column(transition, from_board) { - score += weights::PRIORITY_EMPTY_COL_BONUS; - } } score } @@ -1403,6 +1395,46 @@ mod tests { } } + #[test] + fn single_kind_buckets_carry_no_base_secondary_priority() { + // Column 0 holds a lone 5♥ that can leave for the 6♣ in column 1, + // emptying its column. The stock has one unreachable card past the + // pile, so the only stock macro is a one-advance recycle. + let mut board = KlondikeBoard::new(); + board.columns[0].push(card(Suit::Heart, 5), false); + board.columns[1].push(card(Suit::Club, 6), false); + board.stock[0] = card(Suit::Diamond, 2); + board.stock[1] = card(Suit::Spade, 9); + board.stock_len = 2; + board.stock_index = 1; + board.compute_signature(); + + let empties_column = KlondikeMove::ColumnToColumn { + source: 0, + destination: 1, + count: 1, + }; + for draw in [1, 3] { + let recycle = KlondikeMove::StockDrawRecycle { + advances: 1, + increment: draw as u8, + }; + let transitions = KlondikeSolver::new(draw).successors(&board, None, 0); + let ordering = OrderingContext::new(&board); + let key = |m| { + let transition = transitions.iter().find(|t| t.the_move == m).unwrap(); + let (bucket, std::cmp::Reverse(secondary)) = + successor_order_key(transition, &board, &ordering); + (bucket, secondary) + }; + + // Emptying a column: its own bucket, plain column priority. + assert_eq!(key(empties_column), (2, weights::PRIORITY_COLUMN_BASE)); + // Recycle: its own bucket, ordered by the folded advances alone. + assert_eq!(key(recycle), (6, -weights::PRIORITY_DRAW_COST)); + } + } + #[test] fn column_moves_without_hidden_cards_are_not_reveals() { let mut board = KlondikeBoard::new(); diff --git a/solver-core-rs/src/klondike/weights.rs b/solver-core-rs/src/klondike/weights.rs index d60aaa6..aee0798 100644 --- a/solver-core-rs/src/klondike/weights.rs +++ b/solver-core-rs/src/klondike/weights.rs @@ -2,8 +2,12 @@ //! Centralized to simplify tuning and avoid scattered literals. // ── Heuristic Cost ────────────────────────────── +// Informative heuristic, NOT admissible: the depth, blocked-King, waste and +// imbalance terms all overestimate the real remaining move count. The solver +// does not look for optimal paths (checkpoints and node limits already give +// that up), so these weights may be tuned freely for search speed. -/// Divisor for burial depth bonus, formula: cost += fd + (fd - 1) * MULTIPLIER. +/// Divisor for the burial depth term, per column: cost += fd + (fd - 1) / DIVISOR. pub const HIDDEN_DEPTH_DIVISOR: i64 = 2; /// Empty-column bonus with diminishing returns (index = min(empty_cols, 4)). @@ -30,22 +34,24 @@ pub const IMBALANCE_TOLERANCE: i64 = 2; /// Multiplier weight for foundation imbalance. pub const IMBALANCE_WEIGHT: i64 = 4; -/// Divisor for stock advance cost in heuristic_cost (endgame path: hidden==0). -/// Conservative (/2) to avoid overestimation (real cost = stock_remaining). +/// Divisor for the stock advance term in heuristic_cost: half of the minimum +/// number of advances needed to reach every remaining stock card. Adjustable. pub const STOCK_ADVANCE_DIVISOR: i64 = 2; +/// Cost per next-foundation card sitting in the remaining stock at a position +/// the current pass cannot reach. Only Draw 3 (and any draw > 1) skips stock +/// positions; Draw 1 reaches every card, so the term is always zero there. +pub const MISALIGNED_TARGET_COST: i64 = 2; + // ── Thoughtful Heuristics ────────────────────── -// These signals are NOT used in heuristic_cost (admissibility), -// only in progress_score and successor ordering. +// These signals use knowledge of face-down cards and are NOT used in +// heuristic_cost (the IDA* hot path), only in progress_score and successor +// ordering. /// Multiplier penalty if a "target" card is face-down. /// Only used in progress_score via PROGRESS_BURIAL_WEIGHT. pub const BURIED_TARGET_PENALTY: i64 = 2; -/// Penalty for a logical face-down deadlock, weighted by severity. -/// Only used in progress_score via PROGRESS_DEADLOCK_WEIGHT. -pub const DEADLOCK_PENALTY: i64 = 1; - /// Ordering bonus when a move reveals the exact target card. /// Only affects exploration order, not IDA* admissibility. pub const THOUGHTFUL_REVEAL_TARGET: i64 = 400; @@ -124,8 +130,14 @@ pub const AUTOPLAY_PROXIMITY_THRESHOLD: i64 = 7; /// Rewards uncovering cards; remaining stock can still prevent a win at hidden == 0. pub const PROGRESS_AUTOPLAY_PROXIMITY: i64 = 100; -/// Secondary priority for safe foundation moves. -pub const PRIORITY_SAFE_FOUNDATION: i64 = 600; // Restored. +// Secondary priorities only order moves inside the same bucket of +// `successor_bucket`. Move kinds that have a bucket of their own (stock +// recycles, moves that empty a column) carry no base priority: a constant +// offset shared by every move of the bucket never changes the order. + +/// Secondary priority for safe foundation moves. Competes with other reveals +/// in the reveal bucket. +pub const PRIORITY_SAFE_FOUNDATION: i64 = 600; /// Secondary priority for moves to foundation (not safe). pub const PRIORITY_TO_FOUNDATION: i64 = 500; @@ -133,36 +145,34 @@ pub const PRIORITY_TO_FOUNDATION: i64 = 500; /// Secondary priority for moves from foundation. pub const PRIORITY_FROM_FOUNDATION: i64 = 100; -/// Base secondary priority for stock advance. -pub const PRIORITY_STOCK_ADVANCE: i64 = 120; - -/// Secondary priority for stock recycle. -pub const PRIORITY_STOCK_RECYCLE: i64 = 10; - /// Base secondary priority for column moves. pub const PRIORITY_COLUMN_BASE: i64 = 100; /// Secondary bonus for each additional card in the moved stack. pub const PRIORITY_STACK_BONUS: i64 = 20; -/// Secondary bonus for creating an empty column in C2C. -pub const PRIORITY_EMPTY_COL_BONUS: i64 = 300; - /// Secondary penalty per stock advance folded into a stock macro move: /// nearer waste cards are explored first. pub const PRIORITY_DRAW_COST: i64 = 15; -/// ── Checkpoint Adoption ───────────────────────── -/// Defines the progress required to adopt a checkpoint. +// ── Checkpoint Adoption ───────────────────────── +// Progress (in progress_score points) a chunk must gain for its best board to +// be adopted as the next checkpoint. The threshold grows with the foundation +// count and is bounded per phase. For scale: a reveal is worth about 410 +// points and a foundation move about 1190. -/// Early phase (fc < 10): adoption-threshold clamps. -pub const ADOPT_EARLY_CLAMP: (i64, i64) = (150, 1500); +/// Threshold slope: points of required progress per foundation card. +pub const ADOPT_PER_FOUNDATION_CARD: i64 = 39; -/// Mid phase (10 <= fc < 30): adoption-threshold clamps. -pub const ADOPT_MID_CLAMP: (i64, i64) = (100, 1000); +/// Early phase (fc < ADOPT_EARLY_THRESHOLD_FC): floor of the threshold. +pub const ADOPT_EARLY_MIN: i64 = 150; -/// Late phase (fc >= 30): adoption-threshold clamps. -pub const ADOPT_LATE_CLAMP: (i64, i64) = (20, 500); +/// Mid phase (ADOPT_EARLY_THRESHOLD_FC <= fc < ADOPT_MID_THRESHOLD_FC): cap +/// of the threshold. +pub const ADOPT_MID_MAX: i64 = 1000; + +/// Late phase (fc >= ADOPT_MID_THRESHOLD_FC): fixed threshold. +pub const ADOPT_LATE: i64 = 500; /// fc threshold for considering the position earlygame. pub const ADOPT_EARLY_THRESHOLD_FC: i64 = 10; @@ -170,9 +180,6 @@ pub const ADOPT_EARLY_THRESHOLD_FC: i64 = 10; /// fc threshold for considering the position midgame. pub const ADOPT_MID_THRESHOLD_FC: i64 = 30; -/// Percentage relative to the initial score for calculating the base threshold. -pub const ADOPT_RELATIVE_PCT: i64 = 3; - /// ── Checkpoint Policy / Limits ────────────────── /// Defines the node limit for each board difficulty.