From c3ada5f8c9c42a6170bc9ac7dea2d8df297c8a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Tue, 6 Oct 2026 01:48:10 -0300 Subject: [PATCH 1/6] fix(klondike): prioritize hidden-card reveals and preserve bonuses Add ordering regression tests and document the draw-1 trial results for P1a and P1b. --- .../KlondikeDraw1_results_20261006_014657.csv | 512 ++++++++++++++++++ solver-core-rs/src/klondike/solver.rs | 226 +++++++- 2 files changed, 718 insertions(+), 20 deletions(-) create mode 100644 docs/results/KlondikeDraw1_results_20261006_014657.csv diff --git a/docs/results/KlondikeDraw1_results_20261006_014657.csv b/docs/results/KlondikeDraw1_results_20261006_014657.csv new file mode 100644 index 0000000..7ac0ecd --- /dev/null +++ b/docs/results/KlondikeDraw1_results_20261006_014657.csv @@ -0,0 +1,512 @@ +# Testing del cambio de prioridades de Klondike (P1a + P1b): revelaciones medidas por la reducción de cartas ocultas y conservación de bonos al ordenar subidas a fundación. + +configuration +game,search,simulations,parallel_games,auto_parallel,undos,timeout_s,max_depth +"Klondike Draw 1","IDA*",500,8,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,79.20%,116.82,0.44,1.37,8.08s,396,104,0 + +details +game_id,moves,undos,checkpoints,result,stop_reason,duration_sec,score +1,145,0,2,won,win,8.970,810 +2,93,1,2,lost,stalled,9.146,410 +3,153,0,1,won,win,8.512,825 +4,69,0,2,lost,stalled,9.769,275 +5,123,0,0,won,win,0.480,770 +6,132,1,2,won,win,8.639,840 +7,125,0,1,won,win,7.844,755 +8,136,0,2,won,win,8.152,825 +9,117,0,0,won,win,5.306,735 +10,72,0,1,lost,stalled,7.818,330 +11,57,0,1,lost,stalled,8.540,275 +12,132,1,1,won,win,8.028,775 +13,114,0,1,won,win,9.019,765 +14,133,0,1,won,win,8.303,825 +15,112,0,1,won,win,7.186,730 +16,124,0,2,won,win,9.617,720 +17,99,1,1,lost,stalled,9.215,490 +18,173,3,2,won,win,8.358,765 +19,133,0,1,won,win,7.791,760 +20,121,0,1,won,win,8.997,780 +21,69,0,2,lost,stalled,10.527,320 +22,108,0,2,lost,stalled,9.578,510 +23,130,0,1,won,win,8.859,825 +24,155,1,1,won,win,8.326,820 +25,122,0,1,won,win,7.956,770 +26,114,0,1,won,win,7.765,745 +27,105,0,1,won,win,7.048,770 +28,123,0,1,won,win,7.504,750 +29,114,1,1,won,win,8.035,750 +30,102,0,2,lost,stalled,9.127,485 +31,114,0,1,won,win,7.754,775 +32,130,0,1,won,win,7.611,755 +33,135,2,1,won,win,8.494,785 +34,120,0,1,won,win,7.906,810 +35,137,2,2,won,win,8.907,795 +36,108,0,1,won,win,6.252,785 +37,131,1,1,won,win,7.180,805 +38,149,0,1,won,win,7.338,835 +39,127,0,1,won,win,7.496,755 +40,77,1,2,lost,stalled,10.618,205 +41,55,1,1,lost,stalled,9.720,190 +42,99,1,3,lost,stalled,9.861,400 +43,149,0,1,won,win,7.706,820 +44,85,0,2,lost,stalled,9.102,355 +45,111,1,0,won,win,3.673,785 +46,89,0,2,lost,stalled,8.077,405 +47,80,2,2,lost,stalled,11.573,230 +48,129,0,2,won,win,9.150,795 +49,116,0,1,won,win,7.153,775 +50,127,0,2,won,win,8.243,820 +51,142,3,2,won,win,8.289,775 +52,130,0,1,won,win,7.468,835 +53,149,2,2,won,win,9.036,760 +54,135,0,1,won,win,8.026,770 +55,124,0,2,won,win,8.130,795 +56,138,1,2,won,win,8.398,800 +57,122,1,0,won,win,7.077,770 +58,135,3,2,won,win,8.562,740 +59,71,1,2,lost,stalled,10.016,355 +60,93,0,2,lost,stalled,9.449,385 +61,105,0,4,lost,stalled,11.116,380 +62,126,0,1,won,win,8.533,770 +63,119,0,1,won,win,7.843,770 +64,67,0,1,lost,stalled,8.727,205 +65,132,0,1,won,win,6.944,810 +66,126,0,1,won,win,8.120,755 +67,148,3,2,won,win,8.853,860 +68,83,2,3,lost,stalled,10.487,300 +69,121,0,1,won,win,7.633,795 +70,134,4,1,won,win,8.284,795 +71,130,0,1,won,win,8.027,795 +72,121,0,1,won,win,7.295,800 +73,117,0,1,won,win,7.289,750 +74,132,3,2,won,win,8.811,775 +75,127,0,1,won,win,7.151,800 +76,125,0,1,won,win,7.516,780 +77,142,1,2,won,win,8.649,825 +78,129,0,1,won,win,8.174,760 +79,109,0,1,won,win,7.201,725 +80,72,0,2,lost,stalled,12.364,270 +81,110,0,1,won,win,8.858,795 +82,117,0,1,won,win,8.094,780 +83,123,0,2,won,win,9.115,790 +84,140,0,4,won,win,9.469,825 +85,121,0,1,won,win,8.323,780 +86,110,0,1,won,win,6.580,730 +87,128,0,1,won,win,7.451,800 +88,84,0,1,lost,stalled,8.946,310 +89,82,0,2,lost,stalled,10.483,420 +90,115,0,1,won,win,8.116,755 +91,125,0,1,won,win,7.123,800 +92,126,0,1,won,win,9.650,810 +93,111,0,1,won,win,7.904,800 +94,128,1,1,won,win,7.433,815 +95,117,0,1,won,win,6.858,745 +96,125,1,2,won,win,8.698,805 +97,115,0,1,won,win,8.391,775 +98,64,0,2,lost,stalled,12.831,160 +99,98,0,1,won,win,7.190,735 +100,141,1,2,won,win,8.703,830 +101,125,0,1,won,win,7.632,785 +102,93,0,2,lost,stalled,8.998,400 +103,124,1,1,won,win,7.494,825 +104,110,0,1,won,win,7.827,795 +105,120,0,0,won,win,1.845,795 +106,127,2,2,won,win,8.758,780 +107,101,0,4,lost,stalled,11.470,460 +108,120,0,1,won,win,9.157,775 +109,122,0,1,won,win,6.826,780 +110,128,1,5,lost,stalled,11.224,365 +111,135,0,2,won,win,9.295,760 +112,122,0,1,won,win,7.749,740 +113,121,0,1,won,win,8.047,750 +114,131,3,2,won,win,9.476,755 +115,119,0,1,won,win,7.617,790 +116,127,0,1,won,win,8.351,765 +117,115,0,0,won,win,3.234,760 +118,106,0,1,won,win,8.657,775 +119,63,1,1,lost,stalled,10.408,185 +120,104,0,1,won,win,7.188,765 +121,66,0,2,lost,stalled,9.001,240 +122,110,0,1,won,win,7.398,795 +123,101,0,2,lost,stalled,8.537,315 +124,150,1,2,won,win,8.247,845 +125,116,0,1,won,win,7.694,775 +126,104,0,1,won,win,7.902,765 +127,75,1,1,lost,stalled,8.294,255 +128,128,0,1,won,win,7.970,770 +129,126,0,1,won,win,8.058,805 +130,99,1,3,lost,stalled,10.307,500 +131,110,0,1,won,win,7.608,795 +132,112,0,1,won,win,8.165,765 +133,119,0,0,won,win,4.846,745 +134,132,2,0,won,win,3.122,790 +135,114,0,1,won,win,7.540,755 +136,113,0,1,won,win,8.211,775 +137,114,0,1,won,win,7.387,765 +138,154,1,2,won,win,7.970,785 +139,83,0,2,lost,stalled,10.058,365 +140,145,3,2,won,win,9.266,775 +141,118,1,1,won,win,7.248,755 +142,120,1,1,won,win,7.398,745 +143,120,0,1,won,win,7.757,790 +144,118,0,0,won,win,2.352,770 +145,129,0,1,won,win,7.324,800 +146,121,0,1,won,win,6.873,785 +147,143,1,2,won,win,8.172,815 +148,145,0,1,won,win,9.056,860 +149,115,0,1,won,win,8.235,775 +150,132,4,2,won,win,8.934,770 +151,135,0,1,won,win,7.755,775 +152,122,0,2,won,win,9.731,810 +153,121,0,1,won,win,7.730,785 +154,109,0,1,won,win,7.544,745 +155,113,0,1,won,win,7.606,755 +156,129,0,2,won,win,8.946,825 +157,160,1,2,won,win,7.025,865 +158,110,0,1,won,win,8.029,795 +159,152,2,4,won,win,12.680,860 +160,137,2,2,won,win,9.004,835 +161,119,0,2,won,win,8.490,780 +162,120,0,1,won,win,8.261,770 +163,107,0,1,won,win,7.723,780 +164,125,0,2,won,win,8.459,800 +165,119,1,1,won,win,7.138,795 +166,129,0,1,won,win,7.599,815 +167,134,0,2,won,win,8.037,785 +168,120,0,1,won,win,7.042,770 +169,134,0,1,won,win,7.506,795 +170,131,0,2,won,win,8.809,770 +171,103,0,1,won,win,7.448,760 +172,102,1,3,lost,stalled,9.557,445 +173,80,0,2,lost,stalled,10.238,325 +174,125,0,1,won,win,7.094,790 +175,117,0,1,won,win,8.179,800 +176,127,0,2,won,win,8.856,785 +177,102,0,0,won,win,5.818,755 +178,113,0,1,won,win,8.838,750 +179,64,0,2,lost,stalled,10.649,265 +180,137,0,2,won,win,10.306,795 +181,127,0,2,won,win,8.736,825 +182,114,0,1,won,win,7.167,745 +183,102,0,3,lost,stalled,13.014,300 +184,135,1,2,won,win,10.134,750 +185,179,2,5,won,win,10.622,825 +186,127,0,2,won,win,9.980,810 +187,85,1,2,lost,stalled,9.385,335 +188,101,0,1,won,win,7.679,750 +189,81,0,1,lost,stalled,10.391,205 +190,116,1,1,won,win,7.355,745 +191,129,0,1,won,win,8.551,800 +192,114,0,1,won,win,7.963,770 +193,114,0,1,won,win,7.709,770 +194,49,0,1,lost,stalled,7.461,180 +195,130,1,1,won,win,8.060,810 +196,73,1,1,lost,stalled,8.227,185 +197,77,0,2,lost,stalled,8.587,305 +198,154,1,4,won,win,10.210,805 +199,120,0,1,won,win,8.902,785 +200,114,1,0,won,win,3.300,740 +201,112,0,1,won,win,8.341,765 +202,125,1,1,won,win,7.125,765 +203,142,0,2,won,win,8.856,850 +204,125,0,1,won,win,8.407,780 +205,121,0,1,won,win,7.901,785 +206,108,1,1,won,win,7.233,770 +207,128,1,1,won,win,7.866,745 +208,118,0,1,won,win,7.428,725 +209,102,0,3,lost,stalled,8.672,495 +210,140,0,1,won,win,7.662,825 +211,90,0,2,lost,stalled,10.797,265 +212,140,6,1,won,win,7.427,700 +213,120,0,1,won,win,7.699,780 +214,127,0,2,won,win,8.224,775 +215,127,0,1,won,win,7.050,740 +216,120,0,1,won,win,7.259,775 +217,116,0,1,won,win,7.146,765 +218,116,0,2,won,win,8.569,760 +219,122,0,1,won,win,8.096,755 +220,119,0,1,won,win,8.613,780 +221,114,1,1,won,win,7.184,745 +222,132,1,1,won,win,7.329,755 +223,128,1,1,won,win,7.368,800 +224,73,1,2,lost,stalled,11.584,250 +225,130,2,2,won,win,8.976,805 +226,126,2,1,won,win,9.021,765 +227,118,1,2,won,win,8.473,775 +228,86,0,2,lost,stalled,10.483,310 +229,119,0,1,won,win,8.170,800 +230,107,1,1,won,win,7.408,765 +231,93,0,2,lost,stalled,8.705,360 +232,101,2,2,lost,stalled,8.662,330 +233,131,2,2,won,win,8.823,765 +234,136,2,1,won,win,8.371,740 +235,135,0,1,won,win,7.944,785 +236,52,1,1,lost,stalled,7.966,165 +237,124,0,1,won,win,6.948,815 +238,137,0,2,won,win,9.468,770 +239,113,0,1,won,win,8.182,740 +240,112,0,1,won,win,6.963,735 +241,124,0,2,won,win,9.493,780 +242,79,1,1,lost,stalled,8.017,175 +243,96,2,2,lost,stalled,10.282,280 +244,139,0,2,won,win,8.206,810 +245,125,0,1,won,win,8.527,760 +246,76,0,2,lost,stalled,9.541,255 +247,127,1,1,won,win,8.396,830 +248,155,1,2,won,win,9.134,820 +249,120,0,1,won,win,10.423,760 +250,130,1,1,won,win,9.142,775 +251,129,0,1,won,win,8.548,815 +252,135,0,1,won,win,7.836,775 +253,131,0,2,won,win,7.921,780 +254,94,2,3,lost,stalled,13.256,385 +255,140,0,1,won,win,8.653,810 +256,138,0,2,won,win,10.132,775 +257,122,1,1,won,win,7.666,800 +258,155,5,2,won,win,9.416,815 +259,133,0,2,won,win,9.279,800 +260,136,1,3,won,win,9.933,745 +261,110,0,0,won,win,1.265,730 +262,127,0,1,won,win,8.243,820 +263,125,0,1,won,win,7.453,785 +264,123,0,2,won,win,8.951,775 +265,145,2,1,won,win,10.065,795 +266,108,0,0,won,win,1.409,720 +267,138,0,2,won,win,8.432,780 +268,124,0,1,won,win,7.904,775 +269,68,0,2,lost,stalled,9.250,290 +270,130,0,0,won,win,6.788,760 +271,109,0,0,won,win,1.960,745 +272,69,1,1,lost,stalled,9.057,375 +273,123,0,1,won,win,7.539,740 +274,120,0,2,won,win,7.918,775 +275,130,0,1,won,win,7.357,815 +276,135,2,1,won,win,8.265,810 +277,141,1,1,won,win,7.513,790 +278,135,0,2,won,win,9.358,795 +279,122,2,3,lost,stalled,8.798,365 +280,105,0,3,lost,stalled,14.338,325 +281,118,0,1,won,win,7.473,785 +282,126,0,1,won,win,7.274,735 +283,102,0,0,won,win,1.004,755 +284,80,0,2,lost,stalled,9.710,380 +285,123,0,1,won,win,6.944,745 +286,130,0,1,won,win,7.733,825 +287,111,0,1,won,win,8.094,730 +288,121,0,1,won,win,8.062,785 +289,114,3,2,lost,stalled,9.869,485 +290,129,2,2,won,win,9.619,785 +291,137,0,2,won,win,7.524,765 +292,126,0,1,won,win,7.770,755 +293,91,0,3,lost,stalled,8.873,435 +294,117,1,1,won,win,7.294,760 +295,117,0,1,won,win,7.950,760 +296,92,0,2,lost,stalled,8.475,380 +297,130,0,1,won,win,7.082,755 +298,114,0,1,won,win,7.116,730 +299,106,0,0,won,win,3.983,710 +300,108,0,0,won,win,3.852,785 +301,127,0,1,won,win,6.742,765 +302,120,0,1,won,win,7.056,760 +303,117,0,1,won,win,8.796,770 +304,99,0,2,lost,stalled,9.213,465 +305,81,0,1,lost,stalled,9.163,320 +306,119,0,1,won,win,7.761,755 +307,121,0,1,won,win,8.196,790 +308,81,0,2,lost,stalled,9.664,350 +309,131,0,2,won,win,9.498,805 +310,162,5,3,won,win,9.081,915 +311,118,0,1,won,win,8.630,755 +312,136,1,0,won,win,2.490,830 +313,143,3,3,won,win,11.353,855 +314,102,0,0,won,win,7.804,755 +315,83,1,2,lost,stalled,10.266,405 +316,136,0,1,won,win,7.893,765 +317,97,0,2,lost,stalled,7.704,390 +318,105,0,1,won,win,6.923,770 +319,119,0,1,won,win,8.274,755 +320,111,2,1,won,win,7.528,770 +321,139,0,1,won,win,7.073,785 +322,115,0,1,won,win,8.111,770 +323,114,1,1,won,win,7.156,720 +324,114,0,1,won,win,8.156,775 +325,134,0,1,won,win,8.092,795 +326,82,0,1,lost,stalled,8.429,335 +327,150,0,2,won,win,8.323,815 +328,63,0,0,lost,stalled,3.519,115 +329,134,1,1,won,win,7.777,780 +330,133,0,3,won,win,9.192,840 +331,127,0,1,won,win,8.885,800 +332,124,0,1,won,win,7.336,795 +333,125,0,2,won,win,8.061,800 +334,88,0,2,lost,stalled,8.655,405 +335,131,0,2,won,win,8.306,845 +336,58,0,1,lost,stalled,8.519,190 +337,76,2,1,lost,stalled,7.822,160 +338,120,0,1,won,win,7.767,800 +339,117,0,0,won,win,2.238,735 +340,133,2,2,won,win,9.053,785 +341,99,0,2,lost,stalled,9.039,350 +342,110,0,3,lost,stalled,10.703,380 +343,128,1,1,won,win,8.248,795 +344,129,0,2,won,win,9.210,820 +345,120,0,1,won,win,7.346,775 +346,118,0,2,won,win,7.609,795 +347,93,1,2,lost,stalled,8.526,390 +348,137,0,2,won,win,8.615,795 +349,73,0,2,lost,stalled,10.007,365 +350,116,0,1,won,win,6.909,750 +351,123,2,2,won,win,8.616,785 +352,129,2,2,won,win,7.814,780 +353,113,0,0,won,win,3.976,755 +354,120,1,0,won,win,2.485,745 +355,145,0,2,won,win,8.355,825 +356,120,0,0,won,win,4.565,765 +357,139,0,2,won,win,9.012,805 +358,127,0,2,won,win,8.457,835 +359,121,0,1,won,win,7.409,800 +360,76,0,2,lost,stalled,8.598,340 +361,121,0,1,won,win,7.316,760 +362,112,0,1,won,win,8.071,805 +363,138,0,1,won,win,7.889,775 +364,121,0,1,won,win,7.714,770 +365,112,0,0,won,win,5.587,740 +366,95,0,2,lost,stalled,7.570,490 +367,120,2,4,lost,stalled,9.530,485 +368,121,0,2,won,win,8.167,765 +369,121,0,2,won,win,7.842,790 +370,124,1,1,won,win,8.131,760 +371,77,0,2,lost,stalled,9.109,235 +372,134,2,2,won,win,8.150,810 +373,97,0,2,lost,stalled,10.030,320 +374,91,0,2,lost,stalled,7.872,475 +375,76,0,1,lost,stalled,8.534,295 +376,120,0,1,won,win,8.488,805 +377,140,0,2,won,win,8.130,860 +378,119,0,1,won,win,7.862,775 +379,127,0,1,won,win,7.779,815 +380,113,0,1,won,win,8.176,735 +381,133,1,1,won,win,7.739,795 +382,116,0,1,won,win,7.250,745 +383,62,1,1,lost,stalled,8.329,190 +384,126,0,1,won,win,7.229,795 +385,52,0,1,lost,stalled,8.706,165 +386,118,1,1,won,win,7.484,795 +387,135,3,1,won,win,7.700,800 +388,134,0,1,won,win,8.244,760 +389,117,0,1,won,win,7.796,780 +390,128,1,1,won,win,7.720,785 +391,143,0,2,won,win,7.586,805 +392,99,0,2,lost,stalled,7.648,585 +393,110,0,1,won,win,7.640,795 +394,155,0,2,won,win,9.106,770 +395,79,1,2,lost,stalled,10.234,375 +396,76,0,2,lost,stalled,10.429,305 +397,122,1,1,won,win,7.492,755 +398,122,0,0,won,win,5.596,775 +399,143,2,2,won,win,8.386,810 +400,117,0,1,won,win,8.192,780 +401,127,0,1,won,win,6.829,775 +402,116,0,1,won,win,7.049,765 +403,119,1,0,won,win,2.839,740 +404,142,2,2,won,win,10.570,815 +405,123,0,1,won,win,8.039,780 +406,123,0,1,won,win,7.150,770 +407,116,0,1,won,win,7.390,750 +408,123,0,0,won,win,0.149,775 +409,111,1,3,lost,stalled,11.499,375 +410,115,0,0,won,win,1.605,745 +411,135,1,2,won,win,9.438,840 +412,136,1,2,won,win,8.701,815 +413,122,0,1,won,win,7.893,765 +414,114,1,1,won,win,8.236,745 +415,118,1,2,won,win,7.408,760 +416,79,0,2,lost,stalled,10.790,355 +417,119,0,1,won,win,7.085,790 +418,132,1,1,won,win,8.373,750 +419,117,0,1,won,win,7.018,750 +420,83,0,2,lost,stalled,11.173,240 +421,115,0,1,won,win,7.796,755 +422,64,0,1,lost,stalled,7.910,155 +423,106,0,1,won,win,8.163,720 +424,138,1,3,won,win,9.973,845 +425,118,0,1,won,win,7.569,760 +426,145,1,2,won,win,8.015,840 +427,75,0,2,lost,stalled,9.006,345 +428,123,0,1,won,win,8.125,785 +429,92,1,2,lost,stalled,9.812,380 +430,107,0,0,won,win,3.094,780 +431,115,0,1,won,win,8.100,745 +432,122,0,1,won,win,8.413,785 +433,157,2,1,won,win,8.717,760 +434,122,0,1,won,win,7.551,780 +435,132,0,1,won,win,7.780,820 +436,79,0,1,lost,stalled,8.060,290 +437,132,0,1,won,win,7.292,775 +438,121,0,1,won,win,7.272,765 +439,127,0,2,won,win,8.144,795 +440,132,0,1,won,win,7.547,805 +441,130,0,1,won,win,8.377,760 +442,120,0,0,won,win,4.481,760 +443,137,0,2,won,win,8.625,815 +444,151,1,3,won,win,9.271,845 +445,125,1,3,won,win,8.954,805 +446,121,0,1,won,win,8.803,795 +447,103,1,1,lost,stalled,7.550,275 +448,132,0,1,won,win,7.284,820 +449,125,0,1,won,win,8.354,795 +450,126,0,1,won,win,7.702,820 +451,124,0,1,won,win,10.058,830 +452,130,0,1,won,win,7.919,830 +453,64,0,2,lost,stalled,12.223,285 +454,156,5,3,won,win,10.946,770 +455,138,2,2,won,win,9.390,800 +456,143,1,2,won,win,8.195,840 +457,130,1,1,won,win,8.566,790 +458,113,0,1,won,win,9.003,810 +459,126,0,2,won,win,8.459,805 +460,129,0,1,won,win,6.595,780 +461,113,0,1,won,win,7.511,740 +462,118,0,2,won,win,8.715,815 +463,111,1,0,won,win,4.912,705 +464,94,0,1,lost,stalled,7.970,200 +465,108,1,0,won,win,0.626,770 +466,72,0,1,lost,stalled,7.478,245 +467,123,0,1,won,win,7.439,775 +468,143,0,2,won,win,11.377,815 +469,119,1,1,won,win,8.842,740 +470,115,0,1,won,win,8.263,750 +471,117,0,1,won,win,7.495,760 +472,71,0,1,lost,stalled,8.192,250 +473,120,1,4,lost,stalled,10.776,395 +474,112,1,1,won,win,7.975,735 +475,118,0,0,won,win,7.754,750 +476,70,0,2,lost,stalled,10.365,340 +477,122,0,1,won,win,8.029,795 +478,111,0,0,won,win,2.778,725 +479,122,0,1,won,win,7.737,770 +480,103,0,1,won,win,7.569,760 +481,147,0,1,won,win,8.092,785 +482,129,0,1,won,win,7.268,770 +483,108,0,1,won,win,8.363,730 +484,117,1,1,won,win,8.022,750 +485,124,0,1,won,win,8.466,790 +486,114,0,1,won,win,8.208,745 +487,124,0,1,won,win,7.722,770 +488,116,1,4,lost,stalled,12.195,435 +489,100,2,2,lost,stalled,8.623,415 +490,80,0,2,lost,stalled,9.053,295 +491,116,1,0,won,win,1.737,755 +492,109,0,0,won,win,1.476,750 +493,153,0,3,won,win,9.376,830 +494,115,1,1,won,win,7.749,745 +495,116,0,1,won,win,8.124,780 +496,118,1,1,won,win,7.567,700 +497,126,0,1,won,win,6.979,785 +498,79,1,1,lost,stalled,8.378,310 +499,118,0,1,won,win,8.382,775 +500,129,0,2,won,win,7.937,785 diff --git a/solver-core-rs/src/klondike/solver.rs b/solver-core-rs/src/klondike/solver.rs index edde492..dddf6b1 100644 --- a/solver-core-rs/src/klondike/solver.rs +++ b/solver-core-rs/src/klondike/solver.rs @@ -609,11 +609,11 @@ fn successor_order_key( from_board: &KlondikeBoard, ) -> (u8, std::cmp::Reverse) { // Lexicographic order: group first (move type), then local priority. - let exposed_delta = exposed_face_up_delta(transition, from_board); - let exposed_bonus = (exposed_delta.max(0) as i64) * weights::EXPOSED_BONUS_MULTIPLIER; + let revealed_delta = revealed_hidden_delta(transition, from_board); + let exposed_bonus = revealed_delta as i64 * weights::EXPOSED_BONUS_MULTIPLIER; let thoughtful_bonus = thoughtful_reveal_value(transition, from_board); let crit_bonus = critical_path_bonus(transition, from_board); - let bucket = successor_bucket(transition, from_board, exposed_delta); + let bucket = successor_bucket(transition, from_board, revealed_delta); let secondary = local_secondary_priority( transition, from_board, @@ -625,10 +625,10 @@ fn successor_order_key( fn successor_bucket( transition: &KlondikeTransition, from_board: &KlondikeBoard, - exposed_delta: isize, + revealed_delta: isize, ) -> u8 { // Groups with lower indexes are explored first. - if exposed_delta > 0 { + if revealed_delta > 0 { return 0; } if is_safe_foundation_move(transition, from_board) { @@ -659,10 +659,10 @@ fn local_secondary_priority( ) -> i64 { let m = &transition.the_move; if is_safe_foundation_move(transition, from_board) { - return weights::PRIORITY_SAFE_FOUNDATION; + return weights::PRIORITY_SAFE_FOUNDATION + exposed_bonus; } if m.is_to_foundation() { - return weights::PRIORITY_TO_FOUNDATION; + return weights::PRIORITY_TO_FOUNDATION + exposed_bonus; } if m.is_from_foundation() { return weights::PRIORITY_FROM_FOUNDATION; @@ -684,19 +684,16 @@ fn local_secondary_priority( score } -fn exposed_face_up_delta(transition: &KlondikeTransition, from_board: &KlondikeBoard) -> isize { - let before_fu: isize = from_board - .columns - .iter() - .map(|c| c.num_face_up() as isize) - .sum(); - let after_fu: isize = transition - .to_board - .columns - .iter() - .map(|c| c.num_face_up() as isize) - .sum(); - after_fu - before_fu +/// Counts newly revealed cards, excluding transfers into the tableau. +fn revealed_hidden_delta(transition: &KlondikeTransition, from_board: &KlondikeBoard) -> isize { + let source = match transition.the_move { + KlondikeMove::ColumnToColumn { source, .. } + | KlondikeMove::ColumnToFoundation { source, .. } => source as usize, + _ => return 0, + }; + let before = from_board.columns[source].face_down_len; + let after = transition.to_board.columns[source].face_down_len; + before.saturating_sub(after) as isize } fn thoughtful_reveal_value(transition: &KlondikeTransition, from_board: &KlondikeBoard) -> i64 { @@ -823,3 +820,192 @@ pub fn deal(deck: &[crate::common::card::Card]) -> Option { // solver-centered API. super::board::deal(deck) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::card::Card; + + // Small legal move fixtures exercise ordering without running a full search. + fn card(suit: Suit, value: u8) -> Card { + Card::new(suit, value) + } + + #[test] + fn successors_prioritize_reveals_over_stock_and_foundation_transfers() { + let mut board = KlondikeBoard::new(); + board.foundation[Suit::Heart as usize] = 4; + board.columns[0].push(card(Suit::Diamond, 9), true); + board.columns[0].push(card(Suit::Heart, 5), false); + board.columns[1].push(card(Suit::Club, 6), false); + board.columns[2].push(card(Suit::Spade, 5), false); + board.stock[0] = card(Suit::Diamond, 5); + board.stock[1] = card(Suit::Spade, 9); + board.stock_len = 2; + board.stock_index = 1; + board.compute_signature(); + + let foundation_reveal = KlondikeMove::ColumnToFoundation { + source: 0, + card: card(Suit::Heart, 5), + }; + let column_reveal = KlondikeMove::ColumnToColumn { + source: 0, + destination: 1, + count: 1, + }; + let stock_transfer = KlondikeMove::StockPileToColumn { + destination: 1, + card: card(Suit::Diamond, 5), + }; + let foundation_transfer = KlondikeMove::FoundationToColumn { + destination: 2, + card: card(Suit::Heart, 4), + }; + + for draw in [1, 3] { + let solver = KlondikeSolver::new(draw); + let transitions = solver.successors(&board, None, 0); + let position = |m| transitions.iter().position(|t| t.the_move == m).unwrap(); + let advance = KlondikeMove::StockPileAdvance { + beginning_index: 1, + increment: draw as u8, + }; + + assert!(position(foundation_reveal) < position(advance)); + assert!(position(column_reveal) < position(advance)); + assert!(position(advance) < position(stock_transfer)); + assert!(position(advance) < position(foundation_transfer)); + + for m in [foundation_reveal, column_reveal] { + assert_eq!(revealed_hidden_delta(&transitions[position(m)], &board), 1); + } + for m in [stock_transfer, foundation_transfer, advance] { + assert_eq!(revealed_hidden_delta(&transitions[position(m)], &board), 0); + } + + // None of this fixture's candidates is pruned: ordering preserves them all. + let candidates = KlondikeMove::find_candidate_moves(&board, draw as u8); + assert_eq!(transitions.len(), candidates.len()); + for m in candidates { + assert!(transitions.iter().any(|t| t.the_move == m)); + } + } + } + + #[test] + fn column_moves_without_hidden_cards_are_not_reveals() { + let mut board = KlondikeBoard::new(); + board.foundation[Suit::Heart as usize] = 4; + board.columns[0].push(card(Suit::Club, 6), false); + board.columns[0].push(card(Suit::Heart, 5), false); + board.columns[1].push(card(Suit::Spade, 6), false); + board.compute_signature(); + + let transitions = KlondikeSolver::new(1).successors(&board, None, 0); + for m in [ + KlondikeMove::ColumnToFoundation { + source: 0, + card: card(Suit::Heart, 5), + }, + KlondikeMove::ColumnToColumn { + source: 0, + destination: 1, + count: 1, + }, + ] { + let transition = transitions.iter().find(|t| t.the_move == m).unwrap(); + assert_eq!(revealed_hidden_delta(transition, &board), 0); + assert_ne!(successor_order_key(transition, &board).0, 0); + } + } + + #[test] + fn foundation_moves_preserve_critical_path_priority_without_a_reveal() { + let mut board = KlondikeBoard::new(); + board.foundation[Suit::Diamond as usize] = 4; + board.foundation[Suit::Heart as usize] = 4; + board.columns[0].push(card(Suit::Club, 9), true); + board.columns[0].push(card(Suit::Club, 6), false); + board.columns[0].push(card(Suit::Diamond, 5), false); + board.columns[1].push(card(Suit::Club, 1), true); + board.columns[1].push(card(Suit::Spade, 6), false); + board.columns[1].push(card(Suit::Heart, 5), false); + board.compute_signature(); + + for draw in [1, 3] { + let transitions = KlondikeSolver::new(draw).successors(&board, None, 0); + assert_eq!(transitions.len(), 2); + assert_eq!( + transitions[0].the_move, + KlondikeMove::ColumnToFoundation { + source: 1, + card: card(Suit::Heart, 5), + } + ); + for transition in &transitions { + assert!(!is_safe_foundation_move(transition, &board)); + assert_eq!(revealed_hidden_delta(transition, &board), 0); + assert_eq!(thoughtful_reveal_value(transition, &board), 0); + } + } + } + + #[test] + fn foundation_moves_prioritize_revealing_the_next_foundation_card() { + let mut board = KlondikeBoard::new(); + board.foundation[Suit::Diamond as usize] = 4; + board.foundation[Suit::Heart as usize] = 4; + board.columns[0].push(card(Suit::Club, 9), true); + board.columns[0].push(card(Suit::Diamond, 5), false); + board.columns[1].push(card(Suit::Heart, 6), true); + board.columns[1].push(card(Suit::Heart, 5), false); + board.compute_signature(); + + for draw in [1, 3] { + let transitions = KlondikeSolver::new(draw).successors(&board, None, 0); + assert_eq!(transitions.len(), 2); + assert_eq!( + transitions[0].the_move, + KlondikeMove::ColumnToFoundation { + source: 1, + card: card(Suit::Heart, 5), + } + ); + for transition in &transitions { + assert!(!is_safe_foundation_move(transition, &board)); + assert_eq!(revealed_hidden_delta(transition, &board), 1); + assert_eq!(critical_path_bonus(transition, &board), 0); + } + } + } + + #[test] + fn safe_foundation_fast_path_preserves_its_order_and_candidates() { + let mut board = KlondikeBoard::new(); + board.columns[0].push(card(Suit::Spade, 1), false); + board.columns[1].push(card(Suit::Heart, 9), true); + board.columns[1].push(card(Suit::Club, 1), false); + board.stock[0] = card(Suit::Diamond, 13); + board.stock_len = 1; + board.compute_signature(); + + for draw in [1, 3] { + let transitions = KlondikeSolver::new(draw).successors(&board, None, 0); + let moves: Vec<_> = transitions.iter().map(|t| t.the_move).collect(); + assert_eq!( + moves, + vec![ + KlondikeMove::ColumnToFoundation { + source: 0, + card: card(Suit::Spade, 1), + }, + KlondikeMove::ColumnToFoundation { + source: 1, + card: card(Suit::Club, 1), + }, + ] + ); + } + } +} From 06b0cd2222e47c307888b4573ea4d8422ee97fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Tue, 6 Oct 2026 02:10:25 -0300 Subject: [PATCH 2/6] fix(klondike): correct checkpoint progress evaluation Keep the proximity bonus at zero hidden cards and penalize lower cards buried beneath higher cards of the same suit. Add regression tests and record the P2a and P2c draw-1 trial. --- .../KlondikeDraw1_results_20261006_020927.csv | 512 ++++++++++++++++++ solver-core-rs/src/klondike/board_eval.rs | 137 ++++- solver-core-rs/src/klondike/weights.rs | 8 +- 3 files changed, 636 insertions(+), 21 deletions(-) create mode 100644 docs/results/KlondikeDraw1_results_20261006_020927.csv diff --git a/docs/results/KlondikeDraw1_results_20261006_020927.csv b/docs/results/KlondikeDraw1_results_20261006_020927.csv new file mode 100644 index 0000000..0c4378c --- /dev/null +++ b/docs/results/KlondikeDraw1_results_20261006_020927.csv @@ -0,0 +1,512 @@ +# Testing del puntaje de checkpoints de Klondike (P2a + P2c): bono de 6400 al llegar a cero cartas ocultas y penalización de cartas menores enterradas bajo mayores del mismo palo con peso 160. Mantiene P1a + P1b; P2b no aplicado. + +configuration +game,search,simulations,parallel_games,auto_parallel,undos,timeout_s,max_depth +"Klondike Draw 1","IDA*",500,8,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,78.40%,116.26,0.50,1.31,7.88s,392,108,0 + +details +game_id,moves,undos,checkpoints,result,stop_reason,duration_sec,score +1,124,1,1,won,win,7.976,770 +2,135,3,1,won,win,7.880,785 +3,119,0,1,won,win,8.627,795 +4,45,0,1,lost,stalled,7.598,125 +5,90,2,2,lost,stalled,10.032,360 +6,119,0,1,won,win,8.203,780 +7,52,0,1,lost,stalled,8.686,245 +8,143,1,3,won,win,9.703,825 +9,54,1,1,lost,stalled,11.688,190 +10,143,0,1,won,win,8.107,755 +11,129,0,1,won,win,7.723,765 +12,130,1,1,won,win,8.148,805 +13,125,0,0,won,win,1.101,775 +14,52,0,1,lost,stalled,9.391,220 +15,133,0,4,won,win,11.080,835 +16,127,5,1,won,win,8.685,740 +17,119,0,1,won,win,8.324,765 +18,115,0,2,won,win,8.801,780 +19,111,0,1,won,win,7.414,745 +20,115,0,1,won,win,7.396,750 +21,115,1,1,won,win,6.811,745 +22,100,0,2,lost,stalled,10.727,475 +23,116,0,1,won,win,7.201,765 +24,82,1,2,lost,stalled,10.985,290 +25,122,2,1,won,win,8.250,780 +26,111,0,1,won,win,8.069,775 +27,123,0,1,won,win,7.159,740 +28,92,1,2,lost,stalled,12.083,315 +29,114,0,1,won,win,7.784,740 +30,132,0,1,won,win,7.283,815 +31,132,2,2,won,win,9.087,805 +32,125,0,1,won,win,7.648,760 +33,129,0,1,won,win,7.923,790 +34,127,0,1,won,win,7.844,775 +35,104,1,2,lost,stalled,9.381,395 +36,121,0,1,won,win,8.336,795 +37,126,0,1,won,win,7.574,740 +38,89,0,2,lost,stalled,9.659,275 +39,116,0,0,won,win,4.559,735 +40,121,0,1,won,win,7.591,780 +41,125,1,2,won,win,8.056,800 +42,95,0,2,lost,stalled,10.745,400 +43,129,0,1,won,win,7.087,755 +44,92,0,1,lost,stalled,8.028,415 +45,81,0,2,lost,stalled,9.622,445 +46,114,0,1,won,win,6.553,735 +47,129,0,2,won,win,8.918,765 +48,128,2,1,won,win,7.805,745 +49,116,0,1,won,win,7.773,735 +50,138,2,1,won,win,9.511,810 +51,115,0,1,won,win,7.255,740 +52,179,2,2,won,win,11.906,825 +53,123,0,1,won,win,7.021,785 +54,125,0,1,won,win,7.641,745 +55,76,0,2,lost,stalled,10.756,280 +56,109,1,0,won,win,0.179,775 +57,97,0,2,lost,stalled,9.229,505 +58,92,0,2,lost,stalled,9.514,375 +59,126,0,1,won,win,9.712,815 +60,122,0,2,won,win,7.831,730 +61,111,0,1,won,win,8.394,800 +62,113,1,0,won,win,4.370,795 +63,115,0,0,won,win,0.349,820 +64,83,0,2,lost,stalled,10.359,335 +65,124,0,1,won,win,7.113,795 +66,113,1,0,won,win,0.123,725 +67,118,0,1,won,win,8.473,775 +68,137,0,2,won,win,8.252,805 +69,136,3,2,won,win,8.422,775 +70,134,0,1,won,win,7.514,830 +71,134,0,1,won,win,7.248,740 +72,144,1,2,won,win,9.414,815 +73,124,0,1,won,win,8.097,755 +74,96,1,2,lost,stalled,9.702,430 +75,105,0,1,won,win,8.153,770 +76,121,0,1,won,win,7.968,740 +77,125,0,1,won,win,8.005,745 +78,136,0,4,won,win,9.851,820 +79,96,1,3,lost,stalled,10.732,520 +80,59,0,0,lost,stalled,4.889,110 +81,127,0,1,won,win,6.977,750 +82,132,0,0,won,win,5.498,830 +83,126,0,1,won,win,7.674,805 +84,38,0,0,lost,stalled,0.913,60 +85,100,1,3,lost,stalled,9.876,515 +86,118,0,1,won,win,7.669,750 +87,144,0,1,won,win,8.335,795 +88,130,0,1,won,win,7.904,745 +89,123,1,1,won,win,7.731,760 +90,92,2,2,lost,stalled,10.909,400 +91,121,0,0,won,win,2.252,765 +92,99,0,2,lost,stalled,9.177,495 +93,130,0,1,won,win,7.472,790 +94,121,1,0,won,win,3.290,750 +95,93,0,2,lost,stalled,8.561,480 +96,123,0,1,won,win,7.408,770 +97,101,1,3,lost,stalled,11.123,475 +98,87,2,1,lost,stalled,7.564,160 +99,123,0,1,won,win,9.684,785 +100,126,0,1,won,win,7.126,760 +101,111,0,1,won,win,7.401,765 +102,110,0,1,won,win,7.266,740 +103,134,0,2,won,win,9.533,815 +104,117,0,1,won,win,7.359,770 +105,119,0,0,won,win,3.807,740 +106,91,0,2,lost,stalled,11.035,210 +107,123,0,1,won,win,7.621,765 +108,121,0,1,won,win,7.914,785 +109,121,1,1,won,win,8.083,780 +110,121,0,1,won,win,7.195,745 +111,113,0,1,won,win,7.702,725 +112,92,1,2,lost,stalled,9.615,260 +113,107,0,1,won,win,7.315,750 +114,62,2,2,lost,stalled,8.838,215 +115,73,0,2,lost,stalled,9.567,280 +116,125,0,1,won,win,7.770,795 +117,77,0,2,lost,stalled,10.084,300 +118,136,1,1,won,win,7.898,775 +119,125,0,1,won,win,8.297,790 +120,145,2,1,won,win,7.601,775 +121,109,0,1,won,win,7.175,755 +122,133,0,5,won,win,10.039,865 +123,118,0,1,won,win,8.506,780 +124,123,1,1,won,win,8.348,780 +125,129,1,1,won,win,7.611,815 +126,115,0,2,won,win,7.844,750 +127,137,1,2,won,win,8.928,770 +128,136,0,2,won,win,8.608,815 +129,111,0,0,won,win,2.702,800 +130,133,1,1,won,win,7.392,750 +131,132,1,1,won,win,7.503,750 +132,113,0,1,won,win,7.531,745 +133,117,0,1,won,win,8.044,765 +134,107,0,1,won,win,7.497,730 +135,139,0,2,won,win,7.656,800 +136,118,0,1,won,win,7.154,810 +137,81,0,1,lost,stalled,9.961,235 +138,110,1,1,won,win,7.890,710 +139,119,0,1,won,win,7.410,765 +140,128,0,1,won,win,7.725,795 +141,127,0,0,won,win,1.972,785 +142,136,0,1,won,win,8.447,800 +143,124,0,1,won,win,8.250,805 +144,101,0,0,won,win,0.145,750 +145,124,1,1,won,win,7.074,785 +146,125,2,1,won,win,7.233,780 +147,137,1,2,won,win,8.331,805 +148,117,0,1,won,win,7.263,785 +149,133,0,1,won,win,7.752,780 +150,96,0,3,lost,stalled,10.259,500 +151,133,0,1,won,win,7.768,775 +152,122,0,1,won,win,7.388,790 +153,125,0,1,won,win,7.089,780 +154,124,0,1,won,win,7.501,800 +155,153,8,2,won,win,8.327,785 +156,127,2,1,won,win,8.091,790 +157,94,0,3,lost,stalled,10.583,430 +158,118,0,1,won,win,7.986,775 +159,85,0,2,lost,stalled,9.649,315 +160,173,8,3,won,win,12.577,790 +161,144,1,1,won,win,7.543,775 +162,108,0,3,lost,stalled,10.814,425 +163,141,0,2,lost,stalled,11.333,535 +164,121,1,0,won,win,6.510,755 +165,127,0,1,won,win,7.159,755 +166,125,0,1,won,win,6.986,790 +167,134,0,1,won,win,7.526,785 +168,128,0,2,won,win,8.285,830 +169,118,1,1,won,win,6.833,730 +170,115,1,1,won,win,9.145,770 +171,119,0,1,won,win,7.488,755 +172,113,0,1,won,win,7.031,765 +173,95,0,3,lost,stalled,10.340,350 +174,117,1,1,won,win,7.788,755 +175,132,0,1,won,win,6.886,800 +176,131,3,1,won,win,7.863,770 +177,112,0,1,won,win,8.466,750 +178,117,0,1,won,win,7.989,790 +179,92,1,2,lost,stalled,8.677,430 +180,132,1,1,won,win,7.489,795 +181,108,0,0,won,win,3.926,785 +182,116,0,2,won,win,8.218,760 +183,136,0,1,won,win,8.008,825 +184,99,0,2,lost,stalled,9.306,355 +185,76,2,1,lost,stalled,7.837,200 +186,53,0,1,lost,stalled,7.707,150 +187,86,2,2,lost,stalled,9.940,395 +188,135,1,1,won,win,7.207,785 +189,121,1,2,won,win,8.501,770 +190,132,1,2,won,win,8.381,800 +191,64,0,0,lost,stalled,1.060,110 +192,59,1,1,lost,stalled,8.551,240 +193,96,0,2,lost,stalled,9.399,380 +194,125,0,0,won,win,8.053,805 +195,133,0,2,won,win,8.060,850 +196,131,1,1,won,win,8.712,820 +197,123,2,3,lost,stalled,12.284,340 +198,119,0,1,won,win,6.849,775 +199,107,0,1,won,win,7.577,780 +200,125,1,2,won,win,9.347,805 +201,131,0,1,won,win,7.932,765 +202,114,0,1,won,win,7.919,760 +203,129,0,2,won,win,8.812,760 +204,62,2,1,lost,stalled,6.585,155 +205,131,1,1,won,win,7.940,795 +206,80,0,2,lost,stalled,9.386,415 +207,130,1,1,won,win,7.247,755 +208,153,0,2,won,win,7.785,870 +209,138,3,2,won,win,9.112,770 +210,51,0,1,lost,stalled,8.086,105 +211,114,0,1,won,win,7.890,785 +212,118,0,1,won,win,7.618,785 +213,97,0,2,lost,stalled,9.490,415 +214,120,0,1,won,win,8.187,760 +215,94,0,1,lost,stalled,9.896,250 +216,123,1,1,won,win,7.587,795 +217,113,1,0,won,win,2.173,750 +218,139,4,2,won,win,9.632,770 +219,144,0,0,won,win,2.379,800 +220,125,1,1,won,win,8.071,790 +221,152,1,3,won,win,10.021,895 +222,130,1,1,won,win,8.833,815 +223,140,1,1,won,win,7.643,785 +224,141,2,2,won,win,7.999,775 +225,111,0,0,won,win,3.396,800 +226,135,0,3,won,win,9.792,815 +227,102,0,0,won,win,0.913,755 +228,127,1,2,won,win,8.933,800 +229,126,0,0,won,win,1.190,775 +230,121,0,1,won,win,8.165,755 +231,116,0,1,won,win,8.104,765 +232,99,0,0,won,win,7.090,740 +233,79,0,1,lost,stalled,10.423,255 +234,142,2,2,won,win,9.761,810 +235,130,0,3,won,win,9.596,820 +236,131,1,2,won,win,8.971,780 +237,87,1,2,lost,stalled,7.936,365 +238,125,0,1,won,win,7.662,800 +239,145,2,1,won,win,7.703,780 +240,124,1,0,won,win,5.245,775 +241,132,0,0,won,win,0.612,775 +242,83,0,2,lost,stalled,8.447,440 +243,119,0,2,won,win,9.018,790 +244,117,0,1,won,win,7.373,775 +245,136,4,2,won,win,9.917,740 +246,113,0,1,won,win,6.898,730 +247,154,3,3,won,win,10.613,825 +248,132,0,1,won,win,7.522,845 +249,119,0,1,won,win,8.587,775 +250,133,0,2,won,win,9.108,850 +251,130,1,1,won,win,8.637,790 +252,113,0,1,won,win,7.610,735 +253,99,2,2,lost,stalled,9.902,210 +254,122,0,1,won,win,9.436,775 +255,115,0,1,won,win,7.416,730 +256,103,0,0,won,win,0.121,760 +257,88,1,2,lost,stalled,8.145,435 +258,138,1,1,won,win,7.416,800 +259,127,0,1,won,win,7.879,745 +260,144,1,1,won,win,7.503,780 +261,113,0,1,won,win,8.124,745 +262,113,0,1,won,win,7.016,750 +263,100,1,2,lost,stalled,10.365,320 +264,114,0,1,won,win,7.887,775 +265,70,0,2,lost,stalled,10.423,275 +266,112,0,0,won,win,0.218,730 +267,127,0,2,won,win,9.235,825 +268,122,1,2,won,win,8.227,765 +269,82,0,2,lost,stalled,9.882,285 +270,120,0,0,won,win,6.034,760 +271,113,0,0,won,win,4.351,725 +272,127,0,1,won,win,7.516,790 +273,139,0,2,won,win,8.848,810 +274,110,0,0,won,win,0.874,795 +275,109,0,0,won,win,3.045,790 +276,168,0,2,won,win,8.854,845 +277,134,2,3,won,win,9.503,780 +278,115,0,1,won,win,7.275,760 +279,113,0,2,lost,stalled,9.738,480 +280,107,0,0,won,win,6.578,780 +281,139,0,2,won,win,9.079,815 +282,140,0,1,won,win,7.133,830 +283,85,0,3,lost,stalled,10.857,375 +284,52,1,1,lost,stalled,7.600,150 +285,126,4,2,won,win,8.090,745 +286,120,1,0,won,win,3.796,750 +287,106,1,2,lost,stalled,9.503,365 +288,106,1,3,lost,stalled,10.689,425 +289,90,0,2,lost,stalled,10.383,295 +290,130,0,1,won,win,7.210,810 +291,60,1,1,lost,stalled,7.001,105 +292,130,0,1,won,win,8.228,790 +293,124,1,1,won,win,8.190,780 +294,110,1,1,won,win,7.254,780 +295,112,0,1,won,win,6.301,725 +296,119,0,1,won,win,7.706,730 +297,81,0,2,lost,stalled,9.344,375 +298,139,0,2,won,win,7.849,850 +299,114,0,1,won,win,7.387,770 +300,121,0,2,won,win,7.953,790 +301,121,0,1,won,win,7.727,775 +302,120,0,1,won,win,7.228,775 +303,130,0,1,won,win,6.606,770 +304,130,0,2,won,win,8.512,775 +305,113,0,1,won,win,9.583,810 +306,139,0,1,won,win,8.276,780 +307,116,0,1,won,win,8.344,765 +308,136,0,1,won,win,7.464,785 +309,134,1,2,won,win,8.046,780 +310,113,0,1,won,win,6.232,715 +311,121,0,1,won,win,8.666,790 +312,137,0,1,won,win,7.708,775 +313,96,0,2,lost,stalled,8.254,410 +314,137,0,1,won,win,8.245,790 +315,116,0,2,won,win,8.265,760 +316,117,0,1,won,win,7.924,775 +317,91,1,2,lost,stalled,8.342,405 +318,114,0,1,won,win,8.290,755 +319,83,0,2,lost,stalled,9.303,325 +320,102,1,3,lost,stalled,10.673,415 +321,100,0,1,won,win,7.393,745 +322,120,0,1,won,win,7.288,785 +323,139,0,2,won,win,9.020,805 +324,112,1,2,lost,stalled,9.348,415 +325,111,0,1,lost,stalled,9.470,295 +326,82,0,1,lost,stalled,8.006,395 +327,114,0,1,won,win,7.326,765 +328,112,0,1,won,win,7.526,755 +329,116,0,1,won,win,8.650,765 +330,115,1,1,won,win,7.849,805 +331,130,1,1,won,win,7.398,780 +332,140,1,2,won,win,8.758,825 +333,141,1,2,won,win,8.990,780 +334,139,4,2,won,win,8.486,800 +335,140,0,2,won,win,9.147,795 +336,110,0,0,won,win,2.618,795 +337,137,0,1,won,win,8.489,830 +338,122,1,1,won,win,8.007,780 +339,110,0,1,won,win,7.770,745 +340,144,1,4,won,win,12.250,805 +341,147,0,1,won,win,9.494,830 +342,112,0,0,won,win,3.266,740 +343,163,3,5,won,win,11.729,835 +344,106,0,3,lost,stalled,9.618,495 +345,119,1,1,won,win,7.830,780 +346,129,1,2,won,win,8.685,805 +347,107,0,1,won,win,7.452,780 +348,140,0,2,won,win,9.500,795 +349,103,0,1,won,win,7.572,760 +350,129,1,3,won,win,8.764,785 +351,119,0,1,won,win,8.258,780 +352,122,0,1,won,win,7.400,790 +353,145,0,2,won,win,8.786,745 +354,127,1,2,won,win,8.671,765 +355,61,0,1,lost,stalled,8.552,280 +356,112,0,1,won,win,7.640,740 +357,94,0,1,lost,stalled,8.322,380 +358,144,0,2,won,win,8.214,785 +359,74,1,1,lost,stalled,8.506,275 +360,125,0,0,won,win,3.808,765 +361,140,1,2,won,win,8.834,820 +362,120,0,1,won,win,7.584,775 +363,95,0,2,lost,stalled,8.007,385 +364,109,0,0,won,win,3.112,745 +365,117,1,0,won,win,7.406,750 +366,124,1,1,won,win,8.169,780 +367,129,0,1,won,win,8.077,825 +368,110,0,1,won,win,6.940,795 +369,122,0,1,won,win,8.273,790 +370,107,0,0,won,win,2.282,710 +371,89,0,2,lost,stalled,8.519,435 +372,135,0,1,won,win,7.108,805 +373,127,1,2,won,win,8.597,755 +374,114,0,1,won,win,7.871,815 +375,116,1,2,won,win,8.293,755 +376,128,1,2,won,win,8.711,810 +377,110,0,1,won,win,7.905,750 +378,110,0,1,won,win,7.936,740 +379,117,0,2,won,win,7.744,775 +380,95,0,2,lost,stalled,9.485,490 +381,100,0,0,won,win,1.682,745 +382,104,1,3,lost,stalled,10.796,380 +383,118,0,1,won,win,7.659,785 +384,127,0,1,won,win,6.581,785 +385,124,0,1,won,win,8.567,790 +386,117,0,1,won,win,7.519,765 +387,75,1,1,lost,stalled,10.111,260 +388,105,0,1,won,win,7.494,690 +389,129,0,1,won,win,7.159,730 +390,141,0,3,won,win,9.584,840 +391,131,0,1,won,win,6.946,775 +392,125,0,1,won,win,6.970,755 +393,91,1,2,lost,stalled,10.250,410 +394,130,1,2,won,win,8.940,825 +395,127,1,1,won,win,8.151,865 +396,122,0,1,won,win,7.376,765 +397,117,0,0,won,win,6.202,775 +398,129,5,1,won,win,7.050,755 +399,119,0,1,won,win,8.379,760 +400,137,0,2,won,win,7.637,845 +401,127,1,2,won,win,7.844,795 +402,119,0,2,won,win,7.928,765 +403,109,1,2,lost,stalled,9.930,420 +404,118,0,0,won,win,2.089,750 +405,125,0,1,won,win,6.976,755 +406,101,1,3,lost,stalled,9.031,485 +407,111,0,0,won,win,4.245,750 +408,120,0,2,won,win,8.616,740 +409,121,1,0,won,win,4.157,725 +410,76,1,2,lost,stalled,10.054,270 +411,151,4,2,won,win,8.037,805 +412,122,1,1,won,win,7.793,775 +413,151,1,2,won,win,8.028,830 +414,119,0,1,won,win,8.132,775 +415,79,2,2,lost,stalled,17.165,175 +416,123,0,1,won,win,7.431,790 +417,122,1,1,won,win,8.873,765 +418,119,0,1,won,win,7.763,790 +419,124,1,1,won,win,8.406,795 +420,117,1,1,won,win,8.231,785 +421,104,0,1,won,win,8.278,765 +422,143,1,1,won,win,7.300,820 +423,139,0,2,won,win,9.053,810 +424,106,2,1,won,win,7.878,745 +425,116,0,1,won,win,7.684,740 +426,134,0,1,won,win,8.016,800 +427,126,1,1,won,win,8.612,825 +428,113,0,1,won,win,7.785,810 +429,112,0,1,won,win,9.434,740 +430,144,3,1,won,win,8.200,735 +431,155,1,2,won,win,8.203,875 +432,108,0,1,won,win,8.048,720 +433,114,0,1,won,win,7.948,765 +434,124,1,1,won,win,8.940,790 +435,119,0,1,won,win,8.224,790 +436,133,0,2,won,win,8.454,835 +437,139,1,1,won,win,8.069,800 +438,65,1,1,lost,stalled,8.092,195 +439,124,0,1,won,win,8.711,805 +440,113,0,1,won,win,6.442,730 +441,134,1,2,won,win,8.675,775 +442,128,1,1,won,win,7.885,800 +443,135,0,3,won,win,8.930,800 +444,137,0,1,won,win,8.209,770 +445,89,1,2,lost,stalled,11.111,370 +446,115,0,1,won,win,8.229,755 +447,118,0,1,won,win,7.782,750 +448,73,0,2,lost,stalled,9.861,305 +449,119,0,1,won,win,8.355,795 +450,159,0,2,won,win,8.160,845 +451,121,2,1,won,win,7.057,820 +452,117,0,0,won,win,3.895,750 +453,121,0,1,won,win,7.113,755 +454,143,0,1,won,win,8.895,845 +455,89,0,2,lost,stalled,8.302,365 +456,118,0,0,won,win,2.330,765 +457,130,0,1,won,win,8.293,825 +458,133,2,1,won,win,7.552,775 +459,123,0,2,won,win,9.408,740 +460,134,0,1,won,win,7.316,775 +461,113,0,1,won,win,6.709,775 +462,75,0,1,lost,stalled,7.283,420 +463,103,0,0,won,win,4.814,760 +464,108,0,0,won,win,1.397,785 +465,92,0,1,lost,stalled,9.962,435 +466,93,0,2,lost,stalled,7.990,415 +467,109,0,1,won,win,6.754,790 +468,111,0,0,won,win,6.201,800 +469,118,1,1,won,win,7.211,750 +470,79,2,1,lost,stalled,8.349,230 +471,78,2,1,lost,stalled,9.638,235 +472,106,0,0,won,win,4.796,710 +473,114,0,1,won,win,8.250,750 +474,126,0,1,won,win,7.679,790 +475,122,2,1,won,win,7.499,785 +476,121,0,1,won,win,7.714,780 +477,119,0,1,won,win,8.159,760 +478,135,0,1,won,win,7.770,795 +479,86,2,2,lost,stalled,10.520,370 +480,154,1,2,won,win,8.292,835 +481,98,1,3,lost,stalled,10.256,340 +482,105,2,2,lost,stalled,8.861,440 +483,48,1,1,lost,stalled,6.954,100 +484,119,0,1,won,win,7.375,770 +485,76,3,2,lost,stalled,9.238,325 +486,123,0,1,won,win,7.970,805 +487,120,0,2,won,win,8.609,780 +488,149,0,3,won,win,10.569,865 +489,84,1,2,lost,stalled,11.276,215 +490,114,2,1,won,win,7.288,785 +491,53,0,1,lost,stalled,7.583,275 +492,132,0,1,won,win,7.948,770 +493,106,0,2,lost,stalled,9.625,385 +494,134,1,1,won,win,7.756,790 +495,118,0,2,won,win,8.673,785 +496,126,0,2,won,win,9.181,735 +497,74,0,2,lost,stalled,8.733,265 +498,123,0,1,won,win,7.960,800 +499,131,0,1,won,win,7.370,755 +500,119,0,1,won,win,7.356,775 diff --git a/solver-core-rs/src/klondike/board_eval.rs b/solver-core-rs/src/klondike/board_eval.rs index d0ad250..60d025b 100644 --- a/solver-core-rs/src/klondike/board_eval.rs +++ b/solver-core-rs/src/klondike/board_eval.rs @@ -28,7 +28,7 @@ pub struct BoardEval { 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 target_burial_penalty: i64, // penalty for face-down target cards - pub deadlock_count: i64, // number of detected logical deadlocks + pub deadlock_count: i64, // same-suit hidden dependency severity, not proven deadlocks pub reveal_bonus: i64, // bonus for exposing a key card pub stranded_blockers: i64, // blockers above targets with no destination pub stock_target_penalty: i64, // penalty for targets trapped in stock @@ -148,9 +148,9 @@ impl BoardEval { continue; } - // For deadlock detection: the lowest value seen for each suit, - // iterating from surface to depth. - let mut lowest_ranks = [255i32; 4]; + // Track the highest hidden rank above each card, per suit, + // while iterating from surface to depth. + let mut highest_ranks = [0u8; 4]; for i in (0..c.face_down_len).rev() { let card = c.cards[i as usize]; @@ -163,14 +163,14 @@ impl BoardEval { eval.target_burial_penalty += obstacle_count * BURIED_TARGET_PENALTY; } - // Deadlock detection: rank inversion in the same suit. - if lowest_ranks[card_suit] < card_val as i32 { - let severity = (card_val as i32 - lowest_ranks[card_suit]).min(3) as i64; - eval.deadlock_count += severity; - } - if (card_val as i32) < lowest_ranks[card_suit] { - lowest_ranks[card_suit] = card_val as i32; + // A higher same-suit card above a lower one must move out of + // the way before the lower card can reach its foundation. + // This is a progress penalty, not proof of an unsolvable state. + let blocker_rank = highest_ranks[card_suit]; + if blocker_rank > card_val { + eval.deadlock_count += (blocker_rank - card_val).min(3) as i64; } + highest_ranks[card_suit] = blocker_rank.max(card_val); } // Reveal value reward (thoughtful: the face-down card is known). @@ -325,7 +325,7 @@ impl BoardEval { /// Progress score for checkpoints (higher is better). /// Incorporates thoughtful signals: the solver penalizes checkpoints that - /// keep targets buried or deadlocks active. + /// keep targets buried or same-suit dependencies blocked. pub fn progress_score(&self) -> i64 { let mut score = self.foundation_count * PROGRESS_FOUNDATION_WEIGHT + self.face_up * PROGRESS_FACE_UP_WEIGHT @@ -339,10 +339,9 @@ impl BoardEval { - self.stranded_blockers * PROGRESS_STRANDED_BLOCKER_PENALTY - self.stock_target_penalty; - // Near-autoplay bonus: states with few hidden cards are close to - // auto-play (hidden==0 -> essentially won). - // Quadratic bonus that grows quickly as hidden approaches 0. - if self.hidden > 0 && self.hidden <= AUTOPLAY_PROXIMITY_THRESHOLD { + // Reward uncovering cards all the way to zero hidden cards. Remaining + // stock can still prevent a win, so this only influences progress. + if (0..=AUTOPLAY_PROXIMITY_THRESHOLD).contains(&self.hidden) { let proximity = (AUTOPLAY_PROXIMITY_THRESHOLD + 1 - self.hidden) as i64; score += proximity * proximity * PROGRESS_AUTOPLAY_PROXIMITY; } @@ -454,6 +453,8 @@ impl CheckpointPolicy { mod tests { use super::*; use crate::common::card::Suit; + use crate::klondike::moves::KlondikeMove; + use crate::klondike::solver::KlondikeSolver; fn card(suit: Suit, value: u8) -> Card { Card::new(suit, value) @@ -570,12 +571,114 @@ mod tests { let eval = BoardEval::from_board(&board, 3); assert!(eval.target_burial_penalty > 0); - assert!(eval.deadlock_count > 0); + assert_eq!(eval.deadlock_count, 0); assert_eq!(eval.reveal_bonus, REVEALED_FOUNDATION_CARD_BONUS); assert!(eval.stranded_blockers > 0); assert!(eval.stock_target_penalty >= STOCK_MISALIGNED_TARGET_PENALTY); } + #[test] + fn revealing_last_hidden_card_improves_adoptable_progress_with_stock_remaining() { + let mut board = KlondikeBoard::new(); + board.foundation = [4; 4]; + board.columns[0].push(card(Suit::Club, 6), false); + board.columns[1].push(card(Suit::Spade, 9), true); + board.columns[1].push(card(Suit::Heart, 5), false); + for c in [ + card(Suit::Club, 13), + card(Suit::Heart, 12), + card(Suit::Club, 11), + card(Suit::Heart, 10), + ] { + board.columns[2].push(c, false); + } + for c in [ + card(Suit::Spade, 13), + card(Suit::Diamond, 12), + card(Suit::Spade, 11), + ] { + board.columns[3].push(c, false); + } + board.columns[4].push(card(Suit::Diamond, 13), false); + board.columns[4].push(card(Suit::Spade, 12), false); + + // Complete a valid 52-card position with the remaining 24 cards in stock. + let stock: Vec<_> = Card::standard_deck() + .into_iter() + .filter(|c| { + c.value > 4 + && !board + .columns + .iter() + .any(|column| column.cards[..column.len as usize].contains(c)) + }) + .collect(); + assert_eq!(stock.len(), 24); + set_stock(&mut board, &stock, 0); + + let reveal = KlondikeMove::ColumnToColumn { + source: 1, + destination: 0, + count: 1, + }; + for draw in [1, 3] { + assert!(KlondikeMove::find_candidate_moves(&board, draw).contains(&reveal)); + let after = reveal.apply(board).expect("legal reveal move"); + let before_eval = BoardEval::from_board(&board, draw); + let after_eval = BoardEval::from_board(&after, draw); + + assert_eq!(before_eval.hidden, 1); + assert_eq!(after_eval.hidden, 0); + assert_eq!(after.stock_len, 24); + assert!(!KlondikeSolver::is_win(&after)); + assert!(after_eval.progress_score() > before_eval.progress_score()); + assert!(CheckpointPolicy::default_policy().is_adoptable( + before_eval.progress_score(), + after_eval.progress_score(), + board.total_foundation_count(), + )); + } + } + + #[test] + fn hidden_dependencies_penalize_lower_cards_below_higher_cards_of_the_same_suit() { + for (buried, above, expected_severity) in [ + (card(Suit::Club, 1), card(Suit::Club, 4), 3), + (card(Suit::Club, 4), card(Suit::Club, 1), 0), + (card(Suit::Club, 1), card(Suit::Club, 2), 1), + (card(Suit::Club, 1), card(Suit::Club, 13), 3), + (card(Suit::Club, 1), card(Suit::Spade, 4), 0), + ] { + let mut board = KlondikeBoard::new(); + board.columns[2].push(buried, true); + board.columns[2].push(above, true); + board.columns[2].push(card(Suit::Heart, 9), false); + board.compute_signature(); + + for draw in [1, 3] { + assert_eq!( + BoardEval::from_board(&board, draw).deadlock_count, + expected_severity, + "hidden cards from base to surface: {buried:?}, {above:?}", + ); + } + } + } + + #[test] + fn hidden_dependency_tracking_is_local_to_each_column() { + let mut board = KlondikeBoard::new(); + board.columns[1].push(card(Suit::Club, 13), true); + board.columns[1].push(card(Suit::Heart, 9), false); + board.columns[2].push(card(Suit::Club, 1), true); + board.columns[2].push(card(Suit::Diamond, 9), false); + board.compute_signature(); + + for draw in [1, 3] { + assert_eq!(BoardEval::from_board(&board, draw).deadlock_count, 0); + } + } + #[test] fn checkpoint_policy_base_limit_for_hidden_and_endgame_ranges() { let policy = CheckpointPolicy { diff --git a/solver-core-rs/src/klondike/weights.rs b/solver-core-rs/src/klondike/weights.rs index 326dc72..ba2420d 100644 --- a/solver-core-rs/src/klondike/weights.rs +++ b/solver-core-rs/src/klondike/weights.rs @@ -112,16 +112,16 @@ pub const EXPOSED_BONUS_MULTIPLIER: i64 = 300; /// No admissibility constraints: penalizes checkpoints that leave targets blocked. pub const PROGRESS_BURIAL_WEIGHT: i64 = 110; -/// Thoughtful weight for deadlocks in progress_score. -/// No admissibility constraints: penalizes checkpoints that keep active suit inversions. +/// Weight of same-suit hidden dependencies in progress_score. +/// Penalizes blocked lower ranks without treating them as proven deadlocks. pub const PROGRESS_DEADLOCK_WEIGHT: i64 = 160; /// Hidden-card threshold for enabling the near-autoplay bonus in progress_score. -/// When hidden <= this value, an increasing quadratic bonus is applied. +/// For hidden counts from zero through this value, an increasing quadratic bonus is applied. pub const AUTOPLAY_PROXIMITY_THRESHOLD: i64 = 7; /// Base weight of the near-autoplay bonus: proximity^2 * this value. -/// States with few hidden cards are almost auto-won, so this is a strong incentive. +/// 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. From 546c2ee384f3d95c3cb1e15ac2c9648b84ac116b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Tue, 6 Oct 2026 02:32:30 -0300 Subject: [PATCH 3/6] fix(klondike): preserve reveals across immediate inverse moves Allow immediate inverses when the previous source may have revealed a hidden card, while keeping path-signature cycle rejection. Add regression coverage and record the P3 draw-1 trial. --- .../KlondikeDraw1_results_20261006_023146.csv | 512 ++++++++++++++++++ solver-core-rs/src/klondike/rules.rs | 68 ++- solver-core-rs/src/klondike/solver.rs | 186 +++++++ 3 files changed, 762 insertions(+), 4 deletions(-) create mode 100644 docs/results/KlondikeDraw1_results_20261006_023146.csv diff --git a/docs/results/KlondikeDraw1_results_20261006_023146.csv b/docs/results/KlondikeDraw1_results_20261006_023146.csv new file mode 100644 index 0000000..e47701a --- /dev/null +++ b/docs/results/KlondikeDraw1_results_20261006_023146.csv @@ -0,0 +1,512 @@ +# Testing del cambio P3 de Klondike: permite inversas columna-columna y columna-fundación-columna cuando el origen anterior queda con una sola carta visible, conservando posibles revelaciones; los ciclos reales siguen descartándose mediante firmas del camino. Mantiene P1a + P1b y P2a + P2c, sin modificar los límites de búsqueda. + +configuration +game,search,simulations,parallel_games,auto_parallel,undos,timeout_s,max_depth +"Klondike Draw 1","IDA*",500,8,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,78.60%,116.22,0.39,1.33,7.99s,393,107,0 + +details +game_id,moves,undos,checkpoints,result,stop_reason,duration_sec,score +1,120,0,1,won,win,6.954,760 +2,153,0,0,won,win,1.573,875 +3,139,3,4,won,win,11.443,825 +4,115,0,1,won,win,6.841,750 +5,123,0,1,won,win,7.640,805 +6,129,1,1,won,win,8.175,745 +7,134,0,1,won,win,7.130,775 +8,83,1,2,lost,stalled,9.458,295 +9,137,0,1,won,win,7.905,825 +10,118,0,1,won,win,8.188,770 +11,77,1,2,lost,stalled,9.241,335 +12,112,0,1,won,win,7.748,745 +13,76,1,2,lost,stalled,9.556,365 +14,103,0,1,won,win,7.711,720 +15,121,0,1,won,win,7.916,770 +16,106,0,2,lost,stalled,11.844,360 +17,104,0,1,won,win,7.661,765 +18,161,1,1,won,win,8.629,860 +19,106,0,0,won,win,1.777,735 +20,147,1,3,won,win,9.623,840 +21,127,0,1,won,win,7.874,790 +22,139,1,3,won,win,9.454,785 +23,113,1,1,won,win,8.246,720 +24,121,0,1,won,win,8.115,775 +25,115,0,0,won,win,1.141,730 +26,69,0,2,lost,stalled,11.298,280 +27,115,0,1,won,win,7.873,770 +28,130,0,1,won,win,7.169,790 +29,122,0,1,won,win,7.501,775 +30,123,0,1,won,win,7.276,765 +31,131,1,1,won,win,8.176,795 +32,115,0,1,won,win,7.063,705 +33,118,0,1,won,win,9.458,780 +34,87,0,2,lost,stalled,9.607,310 +35,113,0,1,won,win,7.683,735 +36,125,0,1,won,win,7.713,810 +37,120,0,0,won,win,1.668,755 +38,118,0,1,won,win,7.912,765 +39,137,0,1,won,win,7.693,865 +40,60,0,1,lost,stalled,9.113,120 +41,120,0,1,won,win,7.117,755 +42,122,2,1,won,win,8.090,755 +43,122,0,1,won,win,6.858,775 +44,142,0,1,won,win,9.320,825 +45,115,0,1,won,win,7.573,765 +46,106,0,0,won,win,4.385,775 +47,130,0,1,won,win,8.218,785 +48,124,0,1,won,win,8.184,790 +49,130,0,2,won,win,9.030,790 +50,121,0,1,won,win,8.237,770 +51,120,0,1,won,win,7.454,800 +52,114,0,1,won,win,9.356,760 +53,123,0,1,won,win,8.395,780 +54,118,0,1,won,win,7.782,775 +55,82,1,2,lost,stalled,8.405,355 +56,143,2,1,won,win,7.893,820 +57,147,6,3,won,win,11.646,775 +58,126,0,2,won,win,12.123,805 +59,137,0,2,won,win,7.505,805 +60,71,2,2,lost,stalled,12.419,255 +61,45,0,1,lost,stalled,8.304,150 +62,120,0,1,won,win,8.038,765 +63,110,0,0,won,win,4.997,735 +64,121,0,1,won,win,7.109,770 +65,124,0,0,won,win,4.201,775 +66,138,2,1,won,win,8.408,800 +67,67,1,2,lost,stalled,11.421,220 +68,100,0,0,won,win,3.497,720 +69,145,0,1,won,win,9.110,810 +70,128,0,2,won,win,10.027,800 +71,110,0,1,won,win,8.357,735 +72,116,0,1,won,win,8.637,825 +73,92,0,3,lost,stalled,9.812,405 +74,134,0,2,won,win,9.250,795 +75,111,0,1,won,win,8.628,800 +76,103,0,3,lost,stalled,11.567,425 +77,64,0,2,lost,stalled,11.163,225 +78,120,1,2,won,win,8.045,780 +79,127,2,1,won,win,8.248,800 +80,119,0,1,won,win,8.113,785 +81,67,0,1,lost,stalled,7.551,160 +82,135,0,2,won,win,7.745,775 +83,86,0,3,lost,stalled,11.347,390 +84,89,0,1,lost,stalled,9.065,200 +85,116,0,1,won,win,8.946,755 +86,126,0,1,won,win,7.227,775 +87,86,2,2,lost,stalled,8.585,345 +88,131,0,1,won,win,8.114,800 +89,122,0,2,won,win,9.400,790 +90,124,0,1,won,win,8.083,755 +91,106,0,0,won,win,1.515,775 +92,136,1,1,won,win,8.249,820 +93,123,2,2,won,win,8.270,765 +94,120,0,2,won,win,7.590,785 +95,145,1,1,won,win,7.631,810 +96,94,1,2,lost,stalled,10.699,370 +97,113,0,0,won,win,3.240,810 +98,130,0,1,won,win,8.366,815 +99,124,0,1,won,win,7.455,780 +100,89,0,3,lost,stalled,10.544,385 +101,134,0,3,won,win,9.036,805 +102,144,1,2,won,win,11.596,870 +103,121,0,1,won,win,7.846,795 +104,148,0,1,won,win,8.303,850 +105,74,2,1,lost,stalled,7.962,180 +106,85,1,2,lost,stalled,10.855,300 +107,82,0,2,lost,stalled,9.665,260 +108,114,0,1,won,win,7.269,755 +109,119,0,1,won,win,7.757,785 +110,125,1,1,won,win,8.641,790 +111,106,0,0,won,win,0.186,730 +112,124,0,1,won,win,8.547,820 +113,132,0,2,won,win,9.014,795 +114,123,0,2,won,win,8.118,800 +115,154,0,2,won,win,8.320,820 +116,124,1,1,won,win,7.570,805 +117,138,0,1,won,win,8.734,835 +118,136,0,0,won,win,1.691,755 +119,108,0,1,won,win,8.936,785 +120,120,0,2,won,win,8.356,775 +121,126,0,1,won,win,7.374,780 +122,168,1,4,won,win,10.635,865 +123,113,0,1,won,win,7.840,760 +124,120,1,1,won,win,8.452,795 +125,122,2,1,won,win,7.581,765 +126,129,0,1,won,win,7.591,805 +127,67,0,1,lost,stalled,8.275,250 +128,142,0,0,won,win,4.174,795 +129,123,0,0,won,win,1.598,745 +130,84,0,2,lost,stalled,10.942,430 +131,140,0,3,won,win,10.369,845 +132,113,0,1,won,win,7.480,775 +133,125,0,1,won,win,7.474,755 +134,131,0,1,won,win,7.088,790 +135,122,0,0,won,win,0.666,770 +136,114,0,0,won,win,5.616,765 +137,126,1,1,won,win,7.518,780 +138,156,0,1,won,win,9.572,825 +139,118,0,1,won,win,8.003,790 +140,79,0,1,lost,stalled,8.824,180 +141,115,0,1,won,win,7.861,790 +142,127,0,1,won,win,9.433,760 +143,150,0,1,won,win,8.683,785 +144,118,0,1,won,win,7.945,755 +145,96,1,2,lost,stalled,11.060,420 +146,65,0,1,lost,stalled,9.282,235 +147,136,0,1,won,win,9.255,830 +148,123,0,0,won,win,1.881,760 +149,124,1,1,won,win,7.732,785 +150,126,0,2,won,win,10.209,755 +151,134,0,1,won,win,8.758,795 +152,68,1,1,lost,stalled,7.595,165 +153,52,0,1,lost,stalled,8.520,105 +154,107,0,0,won,win,4.430,750 +155,121,0,1,won,win,7.724,795 +156,113,1,0,won,win,6.960,750 +157,86,2,1,lost,stalled,7.826,250 +158,96,0,2,lost,stalled,8.405,420 +159,113,0,1,won,win,8.344,755 +160,125,1,2,won,win,9.147,790 +161,122,0,1,won,win,7.281,740 +162,65,0,2,lost,stalled,10.408,215 +163,111,0,1,won,win,8.509,800 +164,138,0,1,won,win,8.903,775 +165,116,0,0,won,win,2.196,750 +166,112,0,0,won,win,4.863,735 +167,115,0,1,won,win,6.253,730 +168,110,0,1,won,win,7.164,730 +169,153,0,2,won,win,8.656,795 +170,128,0,1,won,win,7.860,760 +171,119,0,1,won,win,6.881,760 +172,128,1,1,won,win,7.123,770 +173,112,2,1,won,win,7.956,775 +174,125,0,0,won,win,3.572,800 +175,130,0,1,won,win,7.986,765 +176,110,0,1,won,win,7.931,780 +177,128,0,1,won,win,8.092,810 +178,128,0,1,won,win,7.763,760 +179,131,0,2,won,win,9.036,825 +180,119,0,1,won,win,7.682,790 +181,161,0,2,won,win,9.221,810 +182,154,3,4,won,win,11.206,835 +183,103,0,3,lost,stalled,8.693,555 +184,127,0,1,won,win,7.278,800 +185,91,2,3,lost,stalled,9.349,355 +186,146,0,2,won,win,11.184,800 +187,82,0,2,lost,stalled,10.219,320 +188,121,0,1,won,win,8.377,770 +189,127,0,2,won,win,9.493,780 +190,124,0,4,won,win,9.902,820 +191,123,0,1,won,win,7.714,755 +192,113,1,1,won,win,7.039,795 +193,115,0,1,won,win,6.930,745 +194,82,0,2,lost,stalled,10.153,275 +195,117,0,1,won,win,7.671,785 +196,117,0,1,won,win,8.184,780 +197,114,0,1,won,win,7.853,815 +198,141,3,2,won,win,8.606,790 +199,100,2,3,lost,stalled,11.036,285 +200,103,0,0,won,win,3.244,760 +201,84,1,1,lost,stalled,7.989,160 +202,120,0,1,won,win,9.094,770 +203,123,0,2,won,win,8.937,820 +204,115,0,1,won,win,7.904,750 +205,111,0,1,won,win,8.137,800 +206,131,1,1,won,win,8.134,775 +207,120,0,1,won,win,7.756,765 +208,124,0,1,won,win,8.410,805 +209,125,0,1,won,win,7.611,750 +210,106,1,2,lost,stalled,8.381,375 +211,115,0,1,won,win,7.634,775 +212,69,0,2,lost,stalled,10.250,340 +213,84,1,2,lost,stalled,8.818,280 +214,151,2,1,won,win,11.120,845 +215,112,0,0,won,win,3.341,805 +216,95,0,1,lost,stalled,9.075,405 +217,140,1,3,won,win,8.834,800 +218,115,1,1,won,win,7.108,750 +219,112,0,1,won,win,7.236,745 +220,116,0,1,won,win,7.335,785 +221,122,0,1,won,win,8.204,775 +222,125,0,1,won,win,7.474,770 +223,114,0,1,won,win,9.144,740 +224,117,1,1,won,win,8.167,775 +225,94,1,0,lost,stalled,2.084,140 +226,118,0,2,won,win,7.965,835 +227,79,0,1,lost,stalled,8.447,300 +228,119,3,1,won,win,8.106,795 +229,66,1,2,lost,stalled,9.375,235 +230,74,1,1,lost,stalled,7.367,160 +231,126,0,1,won,win,7.134,760 +232,125,0,1,won,win,7.074,750 +233,130,4,1,won,win,9.963,765 +234,124,1,1,won,win,7.504,740 +235,132,0,1,won,win,7.491,780 +236,93,0,3,lost,stalled,11.004,450 +237,155,1,2,won,win,10.242,910 +238,111,0,1,won,win,7.322,765 +239,132,2,4,won,win,10.509,835 +240,81,0,2,lost,stalled,8.181,345 +241,114,0,1,won,win,7.384,700 +242,117,0,1,won,win,7.798,780 +243,99,1,2,lost,stalled,9.594,455 +244,115,0,0,won,win,6.796,780 +245,151,2,4,won,win,10.303,815 +246,84,1,1,lost,stalled,7.832,290 +247,133,1,1,won,win,8.021,820 +248,84,0,2,lost,stalled,10.879,325 +249,65,0,2,lost,stalled,11.385,300 +250,87,1,2,lost,stalled,9.622,265 +251,118,0,1,won,win,8.155,785 +252,124,0,2,won,win,7.886,795 +253,178,0,2,won,win,8.365,815 +254,122,0,2,won,win,8.244,800 +255,119,0,2,won,win,8.429,765 +256,121,0,1,won,win,7.697,800 +257,109,0,1,won,win,8.219,760 +258,94,1,2,lost,stalled,9.360,370 +259,135,0,4,won,win,10.267,810 +260,114,2,3,lost,stalled,10.060,355 +261,137,1,2,won,win,8.694,810 +262,124,0,1,won,win,8.527,810 +263,123,0,1,won,win,8.234,780 +264,107,0,1,won,win,7.599,745 +265,133,1,1,won,win,7.418,780 +266,111,0,1,won,win,7.193,750 +267,119,0,1,won,win,7.813,775 +268,116,0,2,won,win,8.047,755 +269,138,0,2,won,win,8.260,775 +270,137,2,2,won,win,8.115,765 +271,129,0,1,won,win,7.170,765 +272,81,0,2,lost,stalled,8.665,425 +273,90,0,2,lost,stalled,9.020,345 +274,120,1,1,won,win,6.739,760 +275,118,0,0,won,win,3.624,775 +276,65,0,1,lost,stalled,8.163,240 +277,130,0,2,won,win,8.994,780 +278,124,1,1,won,win,7.987,755 +279,108,1,0,won,win,0.640,770 +280,107,2,2,lost,stalled,8.673,540 +281,123,0,1,won,win,8.310,785 +282,139,1,2,won,win,9.831,815 +283,79,1,2,lost,stalled,9.329,385 +284,170,1,2,won,win,9.946,820 +285,71,2,1,lost,stalled,9.687,190 +286,116,0,1,won,win,7.373,765 +287,113,0,1,won,win,6.663,720 +288,113,0,1,won,win,8.011,745 +289,136,0,1,won,win,8.208,790 +290,101,2,2,lost,stalled,9.471,385 +291,111,4,3,lost,stalled,11.015,425 +292,144,2,2,won,win,9.075,760 +293,117,0,1,won,win,7.191,745 +294,132,0,1,won,win,7.407,780 +295,144,1,2,won,win,10.064,815 +296,127,0,1,won,win,7.306,800 +297,137,0,1,won,win,6.932,805 +298,126,0,1,won,win,7.755,785 +299,108,0,0,won,win,0.593,720 +300,115,1,4,lost,stalled,9.567,470 +301,142,1,1,won,win,9.066,820 +302,113,0,1,won,win,6.766,740 +303,47,1,1,lost,stalled,9.977,115 +304,130,0,0,won,win,4.149,815 +305,157,1,3,won,win,10.041,835 +306,138,0,2,won,win,10.229,800 +307,137,1,1,won,win,8.608,830 +308,108,0,1,won,win,7.489,760 +309,140,0,1,won,win,8.334,845 +310,115,0,1,won,win,8.306,745 +311,118,0,1,won,win,8.725,755 +312,121,0,2,won,win,9.977,775 +313,127,0,1,won,win,7.846,790 +314,118,0,1,won,win,7.586,765 +315,54,0,0,lost,stalled,6.970,130 +316,118,0,1,won,win,8.278,790 +317,119,1,1,won,win,8.316,785 +318,148,1,5,won,win,11.713,865 +319,103,0,1,lost,stalled,9.321,360 +320,117,0,1,won,win,8.226,760 +321,124,0,2,won,win,7.495,800 +322,119,0,0,won,win,3.700,745 +323,126,0,1,won,win,7.288,750 +324,115,0,0,won,win,2.247,755 +325,125,1,3,won,win,9.400,795 +326,82,0,2,lost,stalled,8.967,375 +327,110,0,1,won,win,7.546,745 +328,128,0,2,won,win,8.839,810 +329,115,0,0,won,win,1.367,730 +330,120,1,2,won,win,7.954,770 +331,106,0,0,won,win,2.496,775 +332,123,0,1,won,win,6.871,775 +333,117,0,1,won,win,7.963,780 +334,132,0,2,won,win,9.003,765 +335,111,0,0,won,win,2.668,755 +336,103,1,2,lost,stalled,8.315,500 +337,118,0,1,won,win,7.515,760 +338,111,0,1,won,win,7.995,755 +339,116,0,1,won,win,7.910,770 +340,88,0,2,lost,stalled,9.544,360 +341,140,1,2,won,win,8.113,805 +342,100,0,3,lost,stalled,9.699,455 +343,123,2,2,won,win,8.401,770 +344,112,0,1,won,win,8.076,805 +345,115,0,1,won,win,7.717,735 +346,115,0,0,won,win,7.306,770 +347,113,0,0,won,win,0.907,810 +348,87,0,2,lost,stalled,10.552,320 +349,126,1,1,won,win,9.275,810 +350,151,1,1,won,win,7.086,815 +351,103,5,3,lost,stalled,10.399,340 +352,134,0,2,won,win,9.773,765 +353,117,0,1,won,win,7.713,740 +354,91,0,2,lost,stalled,10.021,275 +355,133,0,2,won,win,8.372,785 +356,129,0,1,won,win,7.999,775 +357,116,0,1,won,win,7.628,750 +358,112,0,1,won,win,7.301,805 +359,109,0,0,won,win,4.459,705 +360,98,0,3,lost,stalled,9.572,480 +361,122,0,1,won,win,7.676,775 +362,105,0,1,won,win,6.890,770 +363,150,1,2,won,win,9.862,780 +364,101,0,0,won,win,2.448,750 +365,121,1,2,won,win,8.732,780 +366,118,0,1,won,win,7.918,770 +367,153,1,5,won,win,10.547,860 +368,132,0,1,won,win,7.922,790 +369,117,0,0,won,win,6.650,750 +370,73,0,2,lost,stalled,16.543,155 +371,127,0,1,won,win,7.565,745 +372,165,0,2,won,win,9.265,840 +373,125,1,1,won,win,7.214,800 +374,124,1,1,won,win,8.485,770 +375,140,0,1,won,win,8.043,730 +376,114,0,1,won,win,7.943,740 +377,113,0,1,won,win,7.340,725 +378,142,1,3,won,win,8.958,825 +379,124,0,1,won,win,7.979,790 +380,134,0,2,won,win,8.073,780 +381,103,0,0,won,win,6.795,760 +382,119,0,0,won,win,0.383,745 +383,106,0,0,won,win,0.724,775 +384,129,0,2,won,win,9.891,775 +385,131,0,1,won,win,7.628,740 +386,115,0,1,won,win,7.403,755 +387,86,0,2,lost,stalled,10.099,465 +388,127,0,2,won,win,9.762,815 +389,125,0,1,won,win,7.078,780 +390,134,0,1,won,win,8.132,785 +391,118,0,1,won,win,8.473,835 +392,90,1,2,lost,stalled,9.601,440 +393,137,0,2,won,win,8.390,835 +394,86,0,2,lost,stalled,10.488,330 +395,142,0,1,won,win,7.292,795 +396,117,0,1,won,win,7.661,755 +397,110,0,1,won,win,8.077,750 +398,117,1,1,won,win,8.368,775 +399,79,0,1,lost,stalled,9.057,345 +400,112,0,1,won,win,7.317,745 +401,85,2,2,lost,stalled,10.516,270 +402,137,1,1,won,win,9.006,845 +403,125,0,1,won,win,7.753,780 +404,144,0,2,won,win,8.951,790 +405,162,1,2,won,win,8.638,815 +406,135,0,2,won,win,9.153,800 +407,118,0,2,won,win,8.057,770 +408,64,0,0,lost,stalled,5.142,100 +409,133,0,2,won,win,8.503,795 +410,147,1,2,won,win,8.505,865 +411,84,1,1,lost,stalled,8.041,350 +412,143,1,2,won,win,8.781,795 +413,120,2,2,won,win,8.985,760 +414,118,0,1,won,win,8.076,795 +415,104,0,1,won,win,8.559,715 +416,93,1,1,lost,stalled,9.798,330 +417,108,0,0,won,win,2.394,785 +418,109,0,0,won,win,1.726,725 +419,105,0,0,won,win,1.704,770 +420,115,0,1,won,win,7.688,820 +421,117,0,1,won,win,7.933,760 +422,75,2,1,lost,stalled,7.812,130 +423,129,0,1,won,win,7.945,755 +424,146,2,2,won,win,9.943,810 +425,116,0,1,won,win,7.717,730 +426,91,1,2,lost,stalled,9.858,410 +427,115,0,1,won,win,7.631,760 +428,150,2,2,won,win,9.064,810 +429,143,3,2,won,win,8.251,790 +430,120,1,1,won,win,7.803,830 +431,85,1,2,lost,stalled,9.038,510 +432,120,0,1,won,win,7.318,765 +433,112,0,1,won,win,7.305,740 +434,77,0,1,lost,stalled,9.160,265 +435,121,0,1,won,win,7.535,765 +436,116,0,1,won,win,8.367,760 +437,108,0,1,won,win,7.980,740 +438,126,0,2,won,win,10.235,795 +439,131,0,3,won,win,8.951,840 +440,118,0,2,won,win,8.399,775 +441,119,0,1,won,win,8.220,775 +442,105,0,0,won,win,2.352,770 +443,104,0,2,lost,stalled,8.776,380 +444,73,1,1,lost,stalled,11.495,260 +445,106,0,1,won,win,8.262,740 +446,117,0,0,won,win,6.704,760 +447,113,0,1,won,win,7.322,750 +448,110,0,0,won,win,4.900,720 +449,127,0,2,won,win,7.952,805 +450,108,0,2,lost,stalled,10.525,415 +451,125,0,1,won,win,8.178,795 +452,112,0,1,won,win,7.642,760 +453,107,0,1,won,win,8.614,780 +454,126,0,1,won,win,8.274,790 +455,81,0,2,lost,stalled,8.361,340 +456,84,0,1,lost,stalled,8.826,260 +457,122,0,2,won,win,8.174,740 +458,140,4,2,won,win,10.414,820 +459,108,0,1,won,win,8.233,730 +460,97,0,2,lost,stalled,9.970,440 +461,100,3,3,lost,stalled,10.047,435 +462,139,1,2,won,win,8.522,830 +463,78,1,2,lost,stalled,11.402,245 +464,138,0,3,won,win,10.401,835 +465,120,0,1,won,win,8.444,790 +466,141,0,2,won,win,8.682,820 +467,128,0,2,won,win,8.800,805 +468,97,0,3,lost,stalled,9.074,465 +469,102,0,2,lost,stalled,9.199,410 +470,90,0,2,lost,stalled,8.888,465 +471,70,0,1,lost,stalled,10.235,155 +472,114,0,1,won,win,7.346,735 +473,100,1,2,lost,stalled,10.106,305 +474,134,0,1,won,win,7.623,770 +475,112,1,1,won,win,7.636,735 +476,175,1,0,won,win,5.002,820 +477,114,1,2,won,win,9.328,740 +478,130,1,1,won,win,8.092,835 +479,137,0,1,won,win,8.093,820 +480,129,0,1,won,win,7.210,740 +481,152,2,2,won,win,8.131,840 +482,153,1,3,won,win,9.063,830 +483,123,0,1,won,win,7.831,815 +484,147,0,1,won,win,8.353,830 +485,65,0,1,lost,stalled,9.016,175 +486,137,0,1,won,win,8.507,765 +487,115,0,1,won,win,7.811,760 +488,118,0,1,won,win,7.640,765 +489,58,0,0,lost,stalled,0.766,125 +490,122,0,1,won,win,7.737,770 +491,143,0,1,won,win,8.291,755 +492,104,0,0,won,win,2.306,765 +493,90,0,2,lost,stalled,8.642,380 +494,135,0,2,won,win,8.483,825 +495,75,1,1,lost,stalled,7.521,225 +496,113,0,1,won,win,7.544,775 +497,121,0,1,won,win,6.993,795 +498,81,0,1,lost,stalled,9.408,435 +499,123,0,1,won,win,6.760,755 +500,130,0,2,won,win,8.936,770 diff --git a/solver-core-rs/src/klondike/rules.rs b/solver-core-rs/src/klondike/rules.rs index fd2ef7a..8cea8b2 100644 --- a/solver-core-rs/src/klondike/rules.rs +++ b/solver-core-rs/src/klondike/rules.rs @@ -29,8 +29,8 @@ pub enum KlondikeRule { /// Discards moves that exceed the maximum allowed depth. DepthLimit { max_depth: usize }, - /// Discards moves that exactly undo the previous move (immediate undo). - /// Prevents short cycles with no progress. + /// Discards immediate inverses unless the previous move may have revealed a card. + /// Ambiguous cases are left to the search's path-signature cycle check. NoImmediateUndo, /// Discards moves where the resulting board is identical to the previous one. @@ -80,14 +80,23 @@ impl KlondikeRule { destination: d, count: c2, }, - ) => a == d && b == c && c1 == c2, + ) => { + // A reveal leaves exactly one face-up card in the source. + // One visible card can also remain without a reveal; path + // signatures reject those genuine cycles after apply(). + a == d && b == c && c1 == c2 && board.columns[a as usize].num_face_up() != 1 + } ( KlondikeMove::ColumnToFoundation { source, card }, KlondikeMove::FoundationToColumn { destination, card: same_card, }, - ) => source == destination && card == same_card, + ) => { + source == destination + && card == same_card + && board.columns[source as usize].num_face_up() != 1 + } ( KlondikeMove::FoundationToColumn { destination, card }, KlondikeMove::ColumnToFoundation { @@ -216,6 +225,57 @@ mod tests { .any(|r| matches!(r, KlondikeRule::KingToEmptyMustExpose)) } + #[test] + fn immediate_inverse_is_still_pruned_when_the_source_cannot_have_revealed() { + let rule = KlondikeRule::NoImmediateUndo; + for remaining_face_up in [0, 2] { + let moved = Card::new(Suit::Heart, if remaining_face_up == 0 { 13 } else { 7 }); + let mut board = KlondikeBoard::new(); + if remaining_face_up == 2 { + board.columns[0].push(Card::new(Suit::Diamond, 9), false); + board.columns[0].push(Card::new(Suit::Club, 8), false); + } + board.columns[0].push(moved, false); + if remaining_face_up == 2 { + board.columns[1].push(Card::new(Suit::Spade, 8), false); + } + board.foundation[Suit::Heart as usize] = moved.value - 1; + board.compute_signature(); + + for (forward, inverse) in [ + ( + KlondikeMove::ColumnToColumn { + source: 0, + destination: 1, + count: 1, + }, + KlondikeMove::ColumnToColumn { + source: 1, + destination: 0, + count: 1, + }, + ), + ( + KlondikeMove::ColumnToFoundation { + source: 0, + card: moved, + }, + KlondikeMove::FoundationToColumn { + destination: 0, + card: moved, + }, + ), + ] { + let after = forward.apply(board).unwrap(); + assert_eq!(after.columns[0].num_face_up(), remaining_face_up); + assert!(after.columns[0].can_add_run(moved)); + assert!(rule.can_prune_early(&after, inverse, Some(forward), 2)); + assert!(!rule.can_prune_early(&after, inverse, None, 2)); + assert_eq!(inverse.apply(after).unwrap().signature, board.signature); + } + } + } + /// Verifies that Fast includes the heuristic rules and Strict does not. #[test] fn fast_vs_strict_rule_sets_differ_on_heuristic_pruning() { diff --git a/solver-core-rs/src/klondike/solver.rs b/solver-core-rs/src/klondike/solver.rs index dddf6b1..6da4811 100644 --- a/solver-core-rs/src/klondike/solver.rs +++ b/solver-core-rs/src/klondike/solver.rs @@ -831,6 +831,192 @@ mod tests { Card::new(suit, value) } + // Complete the inverse-move fixtures with all 52 cards. Low foundations and + // a King atop column 6 avoid introducing forced safe foundation moves. + fn complete_inverse_fixture(board: &mut KlondikeBoard) { + for rank in &mut board.foundation { + *rank = (*rank).max(4); + } + let remaining: Vec<_> = Card::standard_deck() + .into_iter() + .filter(|card| { + card.value > board.foundation[card.suit as usize] + && !board + .columns + .iter() + .any(|column| column.cards[..column.len as usize].contains(card)) + }) + .collect(); + board.stock[..24].copy_from_slice(&remaining[..24]); + board.stock_len = 24; + for (index, &card) in remaining[24..].iter().enumerate() { + board.columns[6].push(card, index + 25 < remaining.len()); + } + board.compute_signature(); + assert_full_deck(board); + } + + fn assert_full_deck(board: &KlondikeBoard) { + let mut cards = board.stock[..board.stock_len as usize].to_vec(); + for column in &board.columns { + cards.extend_from_slice(&column.cards[..column.len as usize]); + } + for suit in Suit::ALL { + cards.extend((1..=board.foundation[suit as usize]).map(|rank| card(suit, rank))); + } + cards.sort_by_key(|card| card.encode()); + assert_eq!(cards, Card::standard_deck()); + } + + #[test] + fn immediate_column_inverse_preserves_a_reveal_for_single_cards_and_runs() { + for count in [1, 2] { + let mut board = KlondikeBoard::new(); + board.columns[0].push(card(Suit::Diamond, 9), true); + board.columns[0].push(card(Suit::Club, 8), true); + board.columns[0].push(card(Suit::Heart, 7), false); + if count == 2 { + board.columns[0].push(card(Suit::Club, 6), false); + } + board.columns[1].push(card(Suit::Spade, 8), false); + complete_inverse_fixture(&mut board); + + let forward = KlondikeMove::ColumnToColumn { + source: 0, + destination: 1, + count, + }; + let inverse = KlondikeMove::ColumnToColumn { + source: 1, + destination: 0, + count, + }; + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + let solver = KlondikeSolver::new_with_mode(draw, mode); + let after = solver + .successors(&board, None, 0) + .into_iter() + .find(|t| t.the_move == forward) + .expect("the reveal must be a legal successor") + .to_board; + assert_full_deck(&after); + assert_eq!(after.columns[0].face_down_len, 1); + let restored = solver + .successors(&after, Some(forward), 1) + .into_iter() + .find(|t| t.the_move == inverse) + .expect("returning the run must preserve the reveal") + .to_board; + assert_full_deck(&restored); + assert_eq!(restored.columns[0].face_down_len, 1); + assert_eq!(restored.columns[0].len, board.columns[0].len); + assert_eq!(restored.columns[1].len, board.columns[1].len); + assert_ne!(restored.signature, board.signature); + } + } + } + } + + #[test] + fn immediate_foundation_inverse_preserves_a_reveal_or_returns_an_ancestor() { + for hidden in [false, true] { + let mut board = KlondikeBoard::new(); + board.foundation[Suit::Heart as usize] = 6; + board.columns[0].push(card(Suit::Club, 8), hidden); + board.columns[0].push(card(Suit::Heart, 7), false); + complete_inverse_fixture(&mut board); + let forward = KlondikeMove::ColumnToFoundation { + source: 0, + card: card(Suit::Heart, 7), + }; + let inverse = KlondikeMove::FoundationToColumn { + destination: 0, + card: card(Suit::Heart, 7), + }; + // The opposite-color foundations lag behind: this is not a forced safe move. + assert!(!is_safe_move_pre_check(&forward, &board)); + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + let solver = KlondikeSolver::new_with_mode(draw, mode); + let after = solver + .successors(&board, None, 0) + .into_iter() + .find(|t| t.the_move == forward) + .expect("the non-safe foundation move must be available") + .to_board; + assert_full_deck(&after); + let restored = solver + .successors(&after, Some(forward), 1) + .into_iter() + .find(|t| t.the_move == inverse) + .expect("one face-up card may be the result of a reveal") + .to_board; + assert_full_deck(&restored); + assert_eq!(restored.columns[0].face_down_len, 0); + assert_eq!(restored.foundation, board.foundation); + assert_eq!(restored.signature != board.signature, hidden); + // Foundation -> column never reveals a hidden tableau card. + assert!(!solver + .successors(&restored, Some(inverse), 2) + .iter() + .any(|t| t.the_move == forward)); + } + } + } + } + + #[test] + fn immediate_inverse_without_a_reveal_is_rejected_by_search_as_a_cycle() { + let mut board = KlondikeBoard::new(); + board.columns[0].push(card(Suit::Club, 8), false); + board.columns[0].push(card(Suit::Heart, 7), false); + board.columns[1].push(card(Suit::Spade, 8), false); + board.compute_signature(); + let forward = KlondikeMove::ColumnToColumn { + source: 0, + destination: 1, + count: 1, + }; + let after = forward.apply(board).unwrap(); + let inverse = KlondikeMove::ColumnToColumn { + source: 1, + destination: 0, + count: 1, + }; + + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + let solver = KlondikeSolver::new_with_mode(draw, mode); + let transitions = solver.successors(&after, Some(forward), 1); + assert_eq!(transitions.len(), 1); + assert_eq!(transitions[0].the_move, inverse); + assert_eq!(transitions[0].to_board.signature, board.signature); + + let mut path = vec![forward]; + let mut signatures = AHashSet::from_iter([board.signature, after.signature]); + let mut tt = AHashMap::new(); + let mut context = IdaContext::new(Instant::now(), None, 0, draw as u8); + let result = solver.search_with_bound( + &after, + 1, + 100, + &mut path, + 0, + &mut signatures, + &mut tt, + &mut context, + ); + + assert!(matches!(result, IdaSearchResult::NextBound(i64::MAX))); + assert_eq!(context.checked_nodes, 1, "the cycle must not be explored"); + assert!(tt.is_empty()); + assert_eq!(path, vec![forward]); + assert_eq!(signatures.len(), 2); + } + } + } + #[test] fn successors_prioritize_reveals_over_stock_and_foundation_transfers() { let mut board = KlondikeBoard::new(); From 0d475223fd9eddaa42c68b9b6a9f23a82d324413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Tue, 6 Oct 2026 13:19:49 -0300 Subject: [PATCH 4/6] perf(klondike): streamline endgames and safe foundation chains Finish visible endgames directly, use one equivalent empty destination, and iterate unique safe foundation chains while preserving primitive accounting. Enforce depth limits on safe foundation moves, add regression tests, and record the P4 draw-1 trial. --- .../KlondikeDraw1_results_20261006_131859.csv | 512 ++++++++++++++++++ solver-core-rs/src/klondike/moves.rs | 114 +++- solver-core-rs/src/klondike/solver.rs | 300 +++++++--- .../klondike/solver_optimizations_tests.rs | 511 +++++++++++++++++ 4 files changed, 1366 insertions(+), 71 deletions(-) create mode 100644 docs/results/KlondikeDraw1_results_20261006_131859.csv create mode 100644 solver-core-rs/src/klondike/solver_optimizations_tests.rs diff --git a/docs/results/KlondikeDraw1_results_20261006_131859.csv b/docs/results/KlondikeDraw1_results_20261006_131859.csv new file mode 100644 index 0000000..a7a1c22 --- /dev/null +++ b/docs/results/KlondikeDraw1_results_20261006_131859.csv @@ -0,0 +1,512 @@ +# Testing de Klondike P4a + P4b + P4c: cierre directo con todas las cartas visibles y sin stock/waste; un solo destino entre columnas vacías equivalentes; recorrido iterativo de cadenas de subidas seguras sin bifurcaciones. La ruta rápida de subidas seguras ahora respeta DepthLimit. Mantiene P1a + P1b, P2a + P2c y P3, sin aumentar los límites configurados de búsqueda. + +configuration +game,search,simulations,parallel_games,auto_parallel,undos,timeout_s,max_depth +"Klondike Draw 1","IDA*",500,8,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,80.40%,116.95,0.49,1.26,7.94s,402,98,0 + +details +game_id,moves,undos,checkpoints,result,stop_reason,duration_sec,score +1,115,0,1,won,win,7.103,760 +2,120,0,0,won,win,4.816,750 +3,139,2,1,won,win,7.984,765 +4,129,0,1,won,win,7.766,770 +5,118,0,1,won,win,7.549,790 +6,88,0,1,lost,stalled,8.056,410 +7,109,0,1,won,win,8.011,790 +8,136,2,1,won,win,8.786,810 +9,143,0,2,won,win,9.196,840 +10,126,0,2,won,win,10.005,815 +11,120,3,3,lost,stalled,9.904,495 +12,148,1,1,won,win,8.847,800 +13,66,1,1,lost,stalled,11.066,255 +14,106,0,0,won,win,4.281,775 +15,121,0,1,won,win,7.854,755 +16,87,0,2,lost,stalled,11.061,395 +17,129,0,1,won,win,8.270,830 +18,129,3,2,won,win,9.248,790 +19,121,0,1,won,win,7.929,780 +20,119,0,1,won,win,9.082,800 +21,85,0,2,lost,stalled,8.775,455 +22,157,0,0,won,win,6.904,760 +23,138,2,1,won,win,7.372,770 +24,118,0,1,won,win,8.367,760 +25,101,0,2,lost,stalled,9.284,550 +26,112,0,1,won,win,7.819,740 +27,145,1,2,won,win,8.804,815 +28,110,0,0,won,win,4.049,795 +29,79,0,2,lost,stalled,11.220,330 +30,110,0,1,won,win,9.039,795 +31,141,2,1,won,win,7.452,825 +32,115,0,1,won,win,7.650,750 +33,161,4,3,won,win,8.501,880 +34,142,4,2,won,win,8.549,775 +35,116,0,1,won,win,8.042,770 +36,130,0,2,won,win,8.588,790 +37,108,1,2,lost,stalled,8.847,405 +38,152,1,2,won,win,9.868,810 +39,125,1,1,won,win,8.088,780 +40,102,0,0,won,win,0.038,755 +41,112,0,0,won,win,1.101,750 +42,117,0,1,won,win,8.232,740 +43,133,1,1,won,win,7.874,775 +44,89,2,2,lost,stalled,10.333,380 +45,117,0,1,won,win,8.431,775 +46,34,0,0,lost,stalled,0.030,75 +47,131,0,1,won,win,7.685,810 +48,134,1,1,won,win,8.056,755 +49,89,0,2,lost,stalled,10.609,280 +50,119,0,1,won,win,8.792,790 +51,85,0,1,lost,stalled,9.600,335 +52,116,0,1,won,win,7.243,785 +53,133,0,1,won,win,8.023,820 +54,148,1,1,won,win,7.369,835 +55,102,0,1,won,win,7.562,755 +56,139,0,1,won,win,7.548,805 +57,72,0,1,lost,stalled,7.935,280 +58,119,0,1,won,win,7.621,780 +59,114,0,0,won,win,6.106,745 +60,136,1,2,won,win,8.000,780 +61,152,1,2,won,win,8.041,805 +62,116,0,0,won,win,0.181,735 +63,132,0,2,won,win,8.451,805 +64,105,0,1,won,win,7.988,770 +65,75,1,2,lost,stalled,9.516,235 +66,53,1,1,lost,stalled,9.597,110 +67,129,3,2,won,win,8.593,780 +68,120,0,1,won,win,8.085,770 +69,117,0,1,won,win,7.802,795 +70,118,0,1,won,win,7.546,750 +71,117,0,0,won,win,2.445,750 +72,118,0,1,won,win,8.577,780 +73,149,4,1,won,win,9.540,800 +74,126,1,1,won,win,8.739,790 +75,107,0,0,won,win,5.130,720 +76,131,1,1,won,win,6.891,805 +77,140,0,1,won,win,8.509,815 +78,125,0,1,won,win,7.529,765 +79,133,1,2,won,win,9.073,785 +80,137,0,1,won,win,8.183,805 +81,120,0,0,won,win,6.042,760 +82,58,0,1,lost,stalled,7.638,140 +83,110,0,0,won,win,5.727,740 +84,123,1,1,won,win,8.369,790 +85,107,0,0,won,win,6.400,745 +86,111,0,0,won,win,6.293,720 +87,114,0,1,won,win,7.715,750 +88,82,2,2,lost,stalled,10.570,315 +89,120,0,1,won,win,8.801,760 +90,129,2,1,won,win,7.838,770 +91,136,1,2,won,win,8.747,810 +92,54,0,1,lost,stalled,7.321,215 +93,131,0,1,won,win,7.390,775 +94,130,0,1,won,win,8.205,760 +95,110,0,0,won,win,5.520,795 +96,149,0,2,won,win,7.844,830 +97,121,0,1,won,win,8.173,805 +98,113,0,1,won,win,8.059,780 +99,136,1,2,won,win,8.020,775 +100,121,0,1,won,win,8.599,800 +101,117,1,0,won,win,7.779,755 +102,129,0,1,won,win,7.551,795 +103,138,0,2,won,win,7.468,790 +104,153,0,2,won,win,8.974,820 +105,183,6,3,won,win,11.481,840 +106,105,2,2,lost,stalled,9.633,295 +107,101,0,2,lost,stalled,8.847,430 +108,119,0,1,won,win,8.823,790 +109,118,0,1,won,win,8.674,785 +110,95,0,3,lost,stalled,10.146,340 +111,104,0,1,won,win,7.571,765 +112,121,0,2,won,win,9.152,785 +113,161,0,1,won,win,7.821,835 +114,180,2,5,won,win,11.141,875 +115,72,0,2,lost,stalled,9.585,315 +116,103,0,1,won,win,8.387,735 +117,110,0,1,won,win,7.988,760 +118,133,1,1,won,win,8.298,795 +119,111,2,1,won,win,8.033,730 +120,116,0,1,won,win,7.728,730 +121,94,3,3,lost,stalled,12.425,305 +122,113,0,1,won,win,8.237,810 +123,137,0,1,won,win,8.050,760 +124,120,0,1,won,win,8.513,805 +125,120,0,0,won,win,0.258,770 +126,148,0,3,won,win,11.673,830 +127,116,0,1,won,win,8.996,800 +128,65,0,1,lost,stalled,8.572,270 +129,126,0,1,won,win,7.210,760 +130,106,3,3,lost,stalled,11.136,365 +131,124,0,1,won,win,7.612,815 +132,129,0,1,won,win,7.507,820 +133,115,1,2,won,win,7.746,755 +134,53,0,1,lost,stalled,7.884,120 +135,118,0,0,won,win,4.750,760 +136,111,0,1,won,win,7.078,750 +137,111,0,1,won,win,8.494,755 +138,126,3,2,won,win,8.906,785 +139,103,0,1,won,win,8.513,760 +140,134,1,1,won,win,7.719,765 +141,117,0,2,won,win,8.498,815 +142,105,0,0,won,win,2.618,770 +143,138,1,4,won,win,11.417,865 +144,62,0,1,lost,stalled,7.846,195 +145,111,0,1,won,win,8.391,735 +146,108,0,1,won,win,7.502,785 +147,120,0,0,won,win,6.387,775 +148,108,0,0,won,win,2.707,785 +149,108,0,1,won,win,7.287,745 +150,129,0,1,won,win,8.658,785 +151,89,0,2,lost,stalled,8.531,505 +152,125,0,1,won,win,8.094,800 +153,125,0,1,won,win,6.864,775 +154,110,0,1,won,win,7.722,795 +155,113,1,1,won,win,8.462,770 +156,100,0,0,won,win,3.143,745 +157,127,0,2,won,win,9.029,780 +158,107,2,3,lost,stalled,10.671,370 +159,124,1,1,won,win,7.844,785 +160,85,0,2,lost,stalled,8.214,420 +161,135,1,2,won,win,8.162,840 +162,118,0,4,lost,stalled,11.518,510 +163,117,1,1,won,win,7.424,750 +164,148,1,2,won,win,8.700,810 +165,133,0,1,won,win,7.820,810 +166,118,0,1,won,win,8.493,785 +167,115,0,1,won,win,7.524,755 +168,109,0,1,won,win,7.579,790 +169,125,0,1,won,win,8.639,740 +170,158,1,2,won,win,10.074,830 +171,81,1,2,lost,stalled,9.041,385 +172,82,0,3,lost,stalled,10.839,425 +173,110,0,0,won,win,1.391,735 +174,132,2,2,won,win,8.101,815 +175,116,0,0,won,win,4.404,750 +176,82,0,2,lost,stalled,11.341,260 +177,123,0,2,won,win,8.595,745 +178,141,0,2,won,win,10.981,810 +179,120,0,0,won,win,2.160,750 +180,116,0,1,won,win,7.891,775 +181,116,0,1,won,win,8.924,775 +182,66,0,1,lost,stalled,8.678,315 +183,115,0,1,won,win,7.687,820 +184,136,0,2,won,win,9.285,830 +185,116,0,1,won,win,9.223,765 +186,112,0,1,won,win,8.550,745 +187,118,1,1,won,win,8.390,765 +188,119,0,1,won,win,7.845,790 +189,109,0,0,won,win,2.399,790 +190,69,0,2,lost,stalled,10.140,290 +191,148,0,1,won,win,8.002,800 +192,109,0,0,won,win,2.960,790 +193,135,1,1,won,win,8.141,760 +194,120,0,1,won,win,8.203,780 +195,97,2,1,lost,stalled,7.723,200 +196,119,0,1,won,win,8.029,755 +197,127,0,1,won,win,7.775,815 +198,125,0,1,won,win,6.842,760 +199,122,0,1,won,win,7.845,790 +200,122,1,1,won,win,7.529,795 +201,130,2,1,won,win,9.318,775 +202,147,2,2,won,win,8.444,815 +203,145,0,1,won,win,7.260,820 +204,121,1,1,won,win,8.311,785 +205,130,3,3,lost,stalled,10.632,455 +206,109,2,2,lost,stalled,8.764,375 +207,144,1,2,won,win,9.085,880 +208,122,0,0,won,win,5.940,765 +209,99,2,2,lost,stalled,9.709,255 +210,133,0,1,won,win,7.867,790 +211,26,0,1,lost,stalled,8.896,75 +212,117,0,1,won,win,7.248,785 +213,105,1,3,lost,stalled,11.855,350 +214,129,1,1,won,win,7.855,795 +215,130,0,1,won,win,7.400,785 +216,162,3,3,won,win,9.677,840 +217,121,0,0,won,win,0.979,765 +218,84,0,1,lost,stalled,8.079,300 +219,122,0,0,won,win,3.539,770 +220,147,0,2,won,win,8.395,840 +221,124,1,0,won,win,1.397,760 +222,120,0,1,won,win,8.611,770 +223,157,3,2,won,win,9.694,805 +224,135,0,1,won,win,7.833,780 +225,132,1,1,won,win,7.556,805 +226,123,0,1,won,win,8.477,790 +227,98,1,1,lost,stalled,9.193,485 +228,104,1,2,lost,stalled,8.749,440 +229,121,3,1,won,win,9.373,805 +230,117,0,1,won,win,7.048,770 +231,78,0,1,lost,stalled,9.992,350 +232,115,0,0,won,win,0.734,775 +233,128,0,1,won,win,7.921,760 +234,130,1,2,won,win,8.764,765 +235,133,0,2,won,win,9.810,810 +236,141,0,2,won,win,9.116,805 +237,116,0,1,won,win,8.712,765 +238,111,2,1,won,win,8.266,770 +239,117,1,1,won,win,7.200,750 +240,81,0,2,lost,stalled,10.362,370 +241,83,1,2,lost,stalled,9.894,300 +242,121,0,1,won,win,8.236,805 +243,123,0,1,won,win,6.629,735 +244,113,0,1,won,win,8.608,750 +245,127,0,1,won,win,7.398,745 +246,122,1,1,won,win,8.445,790 +247,112,0,1,won,win,8.273,745 +248,121,0,1,won,win,7.594,775 +249,77,0,2,lost,stalled,10.978,250 +250,140,0,2,won,win,8.412,805 +251,118,4,1,won,win,8.006,775 +252,111,0,1,won,win,7.907,725 +253,158,0,2,won,win,9.449,845 +254,132,2,0,won,win,2.504,765 +255,107,0,1,won,win,7.777,780 +256,129,1,2,won,win,8.732,825 +257,64,0,1,lost,stalled,9.369,270 +258,125,0,0,won,win,4.329,790 +259,75,1,2,lost,stalled,12.492,310 +260,126,1,1,won,win,8.071,790 +261,131,0,1,won,win,7.408,830 +262,122,0,1,won,win,8.197,775 +263,125,2,1,won,win,7.383,760 +264,158,2,2,won,win,11.149,790 +265,101,0,0,won,win,0.141,750 +266,109,1,0,won,win,7.280,775 +267,118,0,2,won,win,8.780,785 +268,129,0,1,won,win,8.486,780 +269,130,0,1,won,win,7.642,775 +270,116,1,1,won,win,6.818,725 +271,115,0,0,won,win,1.809,755 +272,131,0,1,won,win,7.471,775 +273,105,0,1,won,win,8.295,770 +274,168,3,3,won,win,8.961,840 +275,116,0,1,won,win,8.284,825 +276,122,1,1,won,win,7.751,785 +277,77,0,2,lost,stalled,11.168,200 +278,128,0,1,won,win,8.041,790 +279,112,0,1,won,win,7.133,755 +280,137,0,1,won,win,8.626,810 +281,91,0,2,lost,stalled,9.402,305 +282,122,0,1,won,win,7.700,800 +283,151,2,4,won,win,10.170,805 +284,112,0,1,won,win,7.882,770 +285,121,0,1,won,win,8.059,780 +286,21,1,0,lost,stalled,0.031,30 +287,94,0,3,lost,stalled,10.837,465 +288,127,1,1,won,win,8.770,770 +289,120,1,1,won,win,7.322,770 +290,122,0,1,won,win,8.460,790 +291,159,0,1,won,win,8.400,830 +292,120,1,1,won,win,7.725,755 +293,104,0,0,won,win,7.284,765 +294,51,0,1,lost,stalled,9.166,155 +295,123,0,1,won,win,7.464,770 +296,113,0,2,won,win,9.659,810 +297,134,0,1,won,win,7.728,765 +298,105,0,1,won,win,7.815,770 +299,114,0,1,won,win,6.857,770 +300,105,0,0,won,win,0.349,770 +301,115,1,1,won,win,8.140,755 +302,171,1,3,won,win,10.828,850 +303,129,2,1,won,win,7.876,770 +304,142,4,3,won,win,10.782,820 +305,65,0,1,lost,stalled,9.756,295 +306,118,0,2,won,win,8.079,775 +307,83,0,2,lost,stalled,11.330,410 +308,113,0,1,won,win,7.732,745 +309,123,0,1,won,win,9.274,780 +310,127,0,1,won,win,9.816,745 +311,105,0,2,lost,stalled,8.671,455 +312,106,0,1,won,win,7.352,705 +313,128,1,1,won,win,10.637,790 +314,119,1,1,won,win,7.671,775 +315,116,0,0,won,win,2.768,760 +316,121,0,0,won,win,1.469,755 +317,134,1,2,won,win,8.330,775 +318,133,0,2,won,win,8.138,795 +319,107,0,0,won,win,1.090,735 +320,129,2,2,won,win,7.957,800 +321,163,3,3,won,win,9.515,825 +322,147,0,1,won,win,8.202,865 +323,124,1,1,won,win,7.709,795 +324,140,0,2,won,win,7.738,810 +325,103,0,0,won,win,4.519,760 +326,117,0,1,won,win,7.927,775 +327,147,2,2,won,win,8.470,820 +328,133,0,1,won,win,8.701,770 +329,80,0,2,lost,stalled,9.169,325 +330,143,1,1,won,win,7.920,795 +331,86,0,2,lost,stalled,10.543,370 +332,105,0,1,won,win,8.484,770 +333,120,0,1,won,win,7.369,775 +334,90,1,2,lost,stalled,10.090,325 +335,127,2,2,won,win,8.989,800 +336,129,0,2,won,win,8.176,765 +337,131,0,2,won,win,9.515,790 +338,136,0,1,won,win,8.957,790 +339,130,2,1,won,win,7.653,725 +340,79,1,2,lost,stalled,9.440,320 +341,61,0,1,lost,stalled,11.416,160 +342,125,0,1,won,win,8.639,820 +343,157,0,2,won,win,9.598,825 +344,74,1,2,lost,stalled,9.364,395 +345,118,0,1,won,win,6.991,750 +346,155,0,0,won,win,6.907,800 +347,133,0,2,won,win,8.597,785 +348,111,0,0,won,win,0.284,800 +349,111,0,0,won,win,0.525,800 +350,105,0,2,lost,stalled,8.847,495 +351,88,0,1,lost,stalled,8.962,400 +352,142,1,2,won,win,9.424,825 +353,136,0,3,won,win,9.179,795 +354,54,0,1,lost,stalled,7.957,160 +355,101,2,2,lost,stalled,10.565,355 +356,151,4,2,won,win,8.460,830 +357,129,0,1,won,win,8.012,805 +358,118,0,1,won,win,7.728,745 +359,121,0,1,won,win,7.148,795 +360,160,1,5,won,win,13.413,910 +361,121,0,1,won,win,9.486,790 +362,125,0,1,won,win,7.966,750 +363,142,1,3,won,win,10.123,845 +364,125,0,1,won,win,8.113,775 +365,105,0,1,won,win,7.451,770 +366,147,2,2,won,win,8.158,815 +367,118,0,1,won,win,7.538,790 +368,142,2,2,won,win,9.772,805 +369,131,0,1,won,win,7.631,785 +370,130,0,1,won,win,8.017,750 +371,136,0,1,won,win,7.922,785 +372,147,2,2,won,win,9.232,845 +373,114,0,1,won,win,8.055,760 +374,115,1,1,won,win,8.442,750 +375,121,0,1,won,win,7.585,745 +376,115,0,2,won,win,8.813,780 +377,124,0,1,won,win,7.648,810 +378,117,0,1,won,win,7.637,780 +379,117,0,0,won,win,2.320,770 +380,115,1,1,won,win,8.327,770 +381,130,0,1,won,win,8.170,820 +382,122,0,1,won,win,8.287,785 +383,119,0,1,won,win,7.590,780 +384,84,2,2,lost,stalled,10.300,280 +385,154,0,1,won,win,10.877,785 +386,139,0,2,won,win,8.613,805 +387,109,1,0,won,win,2.405,715 +388,90,0,2,lost,stalled,9.938,430 +389,119,0,1,won,win,8.425,765 +390,116,0,1,won,win,8.457,795 +391,118,0,1,won,win,8.701,760 +392,119,1,1,won,win,7.582,750 +393,122,0,1,won,win,7.246,770 +394,73,1,2,lost,stalled,9.826,205 +395,118,0,1,won,win,7.044,750 +396,71,0,1,lost,stalled,7.767,325 +397,122,0,1,won,win,8.060,755 +398,129,2,0,won,win,0.762,705 +399,138,0,1,won,win,8.429,775 +400,30,0,0,lost,stalled,0.699,85 +401,123,0,2,won,win,9.863,815 +402,99,0,2,lost,stalled,8.143,390 +403,85,2,1,lost,stalled,7.761,230 +404,119,0,1,won,win,8.704,775 +405,121,0,1,won,win,7.591,770 +406,107,0,1,won,win,8.573,780 +407,131,0,3,won,win,9.551,785 +408,89,0,2,lost,stalled,9.758,405 +409,126,0,1,won,win,8.057,805 +410,107,3,1,won,win,7.304,735 +411,85,0,2,lost,stalled,8.059,315 +412,90,1,1,lost,stalled,8.277,330 +413,128,1,0,won,win,4.439,780 +414,135,0,1,won,win,8.102,805 +415,97,1,2,lost,stalled,9.138,335 +416,121,0,1,won,win,8.122,780 +417,121,0,1,won,win,7.014,760 +418,109,0,1,won,win,7.168,745 +419,113,0,0,won,win,6.402,725 +420,122,0,1,won,win,7.264,785 +421,122,0,0,won,win,0.315,775 +422,128,0,1,won,win,8.021,750 +423,117,0,1,won,win,7.765,760 +424,107,1,1,won,win,8.065,765 +425,117,0,1,won,win,8.504,760 +426,81,0,1,lost,stalled,9.280,290 +427,132,2,4,won,win,9.871,810 +428,59,0,1,lost,stalled,8.106,220 +429,122,1,1,won,win,9.041,785 +430,121,0,1,won,win,7.201,785 +431,135,1,2,won,win,9.368,805 +432,93,1,2,lost,stalled,11.190,305 +433,140,1,2,won,win,8.741,795 +434,100,3,1,lost,stalled,9.535,175 +435,134,1,1,won,win,7.664,770 +436,111,0,1,won,win,7.656,730 +437,134,3,1,won,win,8.250,755 +438,95,2,2,lost,stalled,8.569,410 +439,124,0,1,won,win,7.798,805 +440,87,0,2,lost,stalled,9.641,420 +441,91,1,2,lost,stalled,10.944,305 +442,106,0,0,won,win,4.709,695 +443,25,0,1,lost,stalled,8.915,75 +444,120,0,2,won,win,7.914,785 +445,79,0,1,lost,stalled,8.224,275 +446,126,0,1,won,win,9.629,795 +447,127,0,1,won,win,8.394,765 +448,136,0,1,won,win,7.325,770 +449,106,0,1,won,win,7.777,720 +450,138,0,0,won,win,2.427,780 +451,121,0,1,won,win,8.071,795 +452,147,2,2,won,win,8.036,815 +453,129,1,3,won,win,9.488,800 +454,120,1,1,won,win,8.598,775 +455,118,0,1,won,win,7.638,760 +456,111,0,1,won,win,7.668,755 +457,121,0,1,won,win,8.041,775 +458,156,0,3,won,win,11.834,850 +459,114,0,1,won,win,7.495,735 +460,131,0,1,won,win,7.986,765 +461,122,0,1,won,win,7.924,730 +462,113,0,0,won,win,6.425,755 +463,108,0,1,won,win,8.579,720 +464,84,1,1,lost,stalled,10.447,365 +465,127,0,2,won,win,9.466,810 +466,128,1,2,won,win,8.993,800 +467,112,0,1,won,win,7.119,760 +468,120,0,1,won,win,7.759,715 +469,61,0,0,lost,stalled,0.088,100 +470,141,0,1,won,win,7.196,805 +471,123,0,2,won,win,9.349,770 +472,37,1,0,lost,stalled,4.304,65 +473,119,0,1,won,win,7.594,760 +474,125,0,1,won,win,7.579,785 +475,120,0,2,won,win,8.878,780 +476,118,0,1,won,win,7.525,740 +477,164,2,1,won,win,8.497,840 +478,149,0,3,won,win,10.768,805 +479,71,1,1,lost,stalled,7.945,195 +480,146,0,2,won,win,8.689,815 +481,141,1,2,won,win,9.186,775 +482,153,0,1,won,win,8.383,820 +483,89,1,1,lost,stalled,7.888,170 +484,43,0,1,lost,stalled,7.445,110 +485,81,0,2,lost,stalled,10.792,360 +486,139,0,1,won,win,7.538,770 +487,135,0,1,won,win,7.336,780 +488,130,0,1,won,win,8.035,790 +489,137,1,2,won,win,8.870,785 +490,121,0,1,won,win,7.447,775 +491,113,0,1,won,win,6.916,745 +492,91,0,2,lost,stalled,9.304,425 +493,131,0,1,won,win,9.332,780 +494,142,1,2,won,win,9.022,780 +495,126,0,1,won,win,10.222,750 +496,100,1,4,lost,stalled,12.284,465 +497,127,0,1,won,win,7.093,755 +498,115,0,1,won,win,8.849,760 +499,132,0,1,won,win,7.286,760 +500,132,0,1,won,win,8.655,785 diff --git a/solver-core-rs/src/klondike/moves.rs b/solver-core-rs/src/klondike/moves.rs index 2e5dcbd..e044fec 100644 --- a/solver-core-rs/src/klondike/moves.rs +++ b/solver-core-rs/src/klondike/moves.rs @@ -34,9 +34,10 @@ pub enum KlondikeMove { } impl KlondikeMove { - // Generates all valid primitive moves. + // Generates legal primitive moves, using one representative empty destination. pub fn find_candidate_moves(board: &KlondikeBoard, draw_advance: u8) -> Vec { let mut moves = Vec::with_capacity(32); + let first_empty = board.columns.iter().position(|column| column.len == 0); // 1. Column to foundation. for c in 0..7 { @@ -77,6 +78,9 @@ impl KlondikeMove { if src == dest { continue; } + if board.columns[dest].len == 0 && Some(dest) != first_empty { + continue; + } if board.columns[dest].can_add_run(run_base) { moves.push(KlondikeMove::ColumnToColumn { source: src as u8, @@ -91,6 +95,9 @@ impl KlondikeMove { // 4. Stock to column. if let Some(card) = board.stock_pile_card() { for dest in 0..7 { + if board.columns[dest].len == 0 && Some(dest) != first_empty { + continue; + } if board.columns[dest].can_add_run(card) { moves.push(KlondikeMove::StockPileToColumn { destination: dest as u8, @@ -104,6 +111,9 @@ impl KlondikeMove { for &suit in crate::common::card::Suit::ALL.iter() { if let Some(card) = board.top_of_foundation(suit) { for dest in 0..7 { + if board.columns[dest].len == 0 && Some(dest) != first_empty { + continue; + } if board.columns[dest].can_add_run(card) { moves.push(KlondikeMove::FoundationToColumn { destination: dest as u8, @@ -290,3 +300,105 @@ pub fn find_stock_to_foundation_moves(board: &KlondikeBoard) -> Vec = moves + .iter() + .filter_map(|m| match (origin, m) { + ( + 0, + KlondikeMove::ColumnToColumn { + source: 0, + destination, + count: 1, + }, + ) + | (1, KlondikeMove::StockPileToColumn { destination, .. }) + | (2, KlondikeMove::FoundationToColumn { destination, .. }) => { + Some(*destination as usize) + } + _ => None, + }) + .collect(); + assert_eq!( + destinations, + empty_columns + .first() + .copied() + .into_iter() + .collect::>() + ); + } + if let Some(&destination) = empty_columns.first() { + let m = KlondikeMove::ColumnToColumn { + source: 0, + destination: destination as u8, + count: 1, + }; + assert_eq!(m.apply(board).unwrap().columns[0].face_down_len, 0); + } + } + } + } + + #[test] + fn nonempty_destinations_keep_all_legal_tableau_stock_and_foundation_moves() { + let mut board = KlondikeBoard::new(); + board.columns[0].push(Card::new(Suit::Heart, 7), false); + for (column, suit, rank) in [ + (1, Suit::Club, 8), + (2, Suit::Spade, 8), + (3, Suit::Club, 7), + (4, Suit::Spade, 7), + ] { + board.columns[column].push(Card::new(suit, rank), false); + } + board.stock[0] = Card::new(Suit::Diamond, 7); + board.stock_len = 1; + board.stock_index = 1; + board.foundation[Suit::Heart as usize] = 6; + for draw in [1, 3] { + let moves = KlondikeMove::find_candidate_moves(&board, draw); + for destination in [1, 2] { + assert!(moves.contains(&KlondikeMove::ColumnToColumn { + source: 0, + destination, + count: 1 + })); + assert!(moves.contains(&KlondikeMove::StockPileToColumn { + destination, + card: Card::new(Suit::Diamond, 7) + })); + } + for destination in [3, 4] { + assert!(moves.contains(&KlondikeMove::FoundationToColumn { + destination, + card: Card::new(Suit::Heart, 6) + })); + } + } + } +} diff --git a/solver-core-rs/src/klondike/solver.rs b/solver-core-rs/src/klondike/solver.rs index 6da4811..7f85bf0 100644 --- a/solver-core-rs/src/klondike/solver.rs +++ b/solver-core-rs/src/klondike/solver.rs @@ -121,6 +121,12 @@ impl KlondikeSolver { .iter() .any(|m| is_safe_move_pre_check(m, board)) { + // Safe moves still cost one primitive step, including inside a chain. + if self.rules.iter().any(|rule| { + matches!(rule, KlondikeRule::DepthLimit { max_depth } if next_depth > *max_depth) + }) { + return Vec::new(); + } let mut transitions = Vec::new(); for the_move in &candidate_moves { if !is_safe_move_pre_check(the_move, board) { @@ -225,6 +231,33 @@ impl KlondikeSolver { break; } + // The chunk root has no incoming move and is not charged as a node. + // The closure charges each primitive foundation move it performs. + if let Some(result) = self.try_finish_visible_without_stock( + ¤t_board, + 0, + bound, + &[], + &mut context, + ) { + match result { + IdaSearchResult::Found => { + chunk_solution = context.solution_path.take(); + break; + } + IdaSearchResult::Timeout => break, + IdaSearchResult::CheckpointTriggered => { + triggered_checkpoint = true; + break; + } + IdaSearchResult::NextBound(i64::MAX) => break, + IdaSearchResult::NextBound(next_bound) => { + bound = next_bound; + continue; + } + } + } + // Restart the IDA* frontier: generate successors from the current root. let root_transitions = self.successors(¤t_board, None, 0); if root_transitions.is_empty() { @@ -378,6 +411,79 @@ impl KlondikeSolver { } } + /// Finish a valid, fully visible tableau without stock or waste by legal + /// foundation moves. Return the exact required bound before building a tail. + fn try_finish_visible_without_stock( + &self, + board: &KlondikeBoard, + depth: usize, + bound: i64, + prefix: &[KlondikeMove], + context: &mut IdaContext, + ) -> Option { + if board.stock_len != 0 || board.columns.iter().any(|c| c.face_down_len != 0) { + return None; + } + if context.timeout_reached() { + return Some(IdaSearchResult::Timeout); + } + + let max_depth = self + .rules + .iter() + .filter_map(|rule| match rule { + KlondikeRule::DepthLimit { max_depth } => Some(*max_depth), + _ => None, + }) + .min() + .unwrap_or(usize::MAX); + let remaining = 52usize.saturating_sub(board.total_foundation_count()); + let finish_depth = depth.saturating_add(remaining); + if finish_depth > max_depth { + return Some(IdaSearchResult::NextBound(i64::MAX)); + } + if finish_depth as i64 > bound { + return Some(IdaSearchResult::NextBound(finish_depth as i64)); + } + + let mut current = *board; + let mut moves = prefix.to_vec(); + while !Self::is_win(¤t) { + if context.timeout_reached() { + return Some(IdaSearchResult::Timeout); + } + let next_move = (0..7).find_map(|source| { + let card = current.columns[source].top_face_up()?; + current + .can_add_to_foundation(card) + .then_some(KlondikeMove::ColumnToFoundation { + source: source as u8, + card, + }) + }); + let Some(next_move) = next_move else { + return Some(IdaSearchResult::NextBound(i64::MAX)); + }; + let Some(next_board) = next_move.apply(current) else { + return Some(IdaSearchResult::NextBound(i64::MAX)); + }; + current = next_board; + moves.push(next_move); + context.consider_progress(¤t, &moves); + + // Same accounting order as recursion: charge the primitive child, + // check budgets, then recognize a win (including on the last move). + if context.should_timeout() { + return Some(IdaSearchResult::Timeout); + } + if context.should_checkpoint() { + return Some(IdaSearchResult::CheckpointTriggered); + } + } + context.solution_path = Some(moves); + Some(IdaSearchResult::Found) + } + // ── Main IDA* recursion ───────────── fn search_with_bound( @@ -391,91 +497,136 @@ impl KlondikeSolver { tt: &mut AHashMap, context: &mut IdaContext, ) -> IdaSearchResult { - if context.should_timeout() { - return IdaSearchResult::Timeout; - } - if context.should_checkpoint() { - return IdaSearchResult::CheckpointTriggered; - } + let restore_path_len = path.len(); + let mut chain_signatures = Vec::new(); + let mut chain_board; + let mut board = board; + let mut depth = depth; + let result = 'nodes: loop { + if context.should_timeout() { + break IdaSearchResult::Timeout; + } + if context.should_checkpoint() { + break IdaSearchResult::CheckpointTriggered; + } - // Limit memory: clear the TT when it exceeds the configured threshold. - if tt.len() >= weights::TT_MAX_ENTRIES { - tt.clear(); - } + // Limit memory: clear the TT when it exceeds the configured threshold. + if tt.len() >= weights::TT_MAX_ENTRIES { + tt.clear(); + } - let h_cost = BoardEval::from_board_fast(board, self.draw_advance).heuristic_cost(); - let f_score = depth as i64 + h_cost; - // Typical IDA* pruning: the node exceeds the f = g + h bound. - if f_score > bound { - return IdaSearchResult::NextBound(f_score); - } + let h_cost = BoardEval::from_board_fast(board, self.draw_advance).heuristic_cost(); + let f_score = depth as i64 + h_cost; + // Typical IDA* pruning: the node exceeds the f = g + h bound. + if f_score > bound { + break IdaSearchResult::NextBound(f_score); + } - if Self::is_win(board) { - context.solution_path = Some(path.clone()); - return IdaSearchResult::Found; - } + if Self::is_win(board) { + context.solution_path = Some(path.clone()); + break IdaSearchResult::Found; + } - let prev_move = path.last().copied(); - let transitions = self.successors(board, prev_move, depth); - let mut min_next_bound = i64::MAX; - - for transition in transitions { - let next_board = transition.to_board; - let next_signature = next_board.signature; - let next_depth = depth + 1; - let next_remaining_budget = bound - next_depth as i64; - let next_undo_count = if transition.the_move.is_from_foundation() { - undo_count + 1 - } else { - undo_count - }; + if let Some(result) = + self.try_finish_visible_without_stock(board, depth, bound, path, context) + { + break result; + } - if let Some(max) = self.max_undos { - if next_undo_count > max { - continue; + let prev_move = path.last().copied(); + let mut transitions = self.successors(board, prev_move, depth); + if transitions.len() == 1 && is_safe_foundation_move(&transitions[0], board) { + let transition = transitions.pop().unwrap(); + let next_signature = transition.to_board.signature; + let next_depth = depth + 1; + let next_remaining_budget = bound - next_depth as i64; + // A safe foundation step never consumes an undo. Check every + // intermediate signature and TT entry before extending the path. + let key = self.tt_key(next_signature, undo_count); + if path_signatures.contains(&next_signature) + || Self::should_prune_by_tt(tt, key, next_remaining_budget) + { + break IdaSearchResult::NextBound(i64::MAX); } - } - // Avoid cycles in the current path (depth-first search). - if path_signatures.contains(&next_signature) { + Self::upsert_remaining_budget(tt, key, next_remaining_budget); + path_signatures.insert(next_signature); + chain_signatures.push(next_signature); + path.push(transition.the_move); + context.consider_progress(&transition.to_board, path); + chain_board = transition.to_board; + board = &chain_board; + depth = next_depth; + // The next iteration charges this primitive node exactly once and + // checks its f-score, timeout, checkpoint and victory before expansion. continue; } + let mut min_next_bound = i64::MAX; + + for transition in transitions { + let next_board = transition.to_board; + let next_signature = next_board.signature; + let next_depth = depth + 1; + let next_remaining_budget = bound - next_depth as i64; + let next_undo_count = if transition.the_move.is_from_foundation() { + undo_count + 1 + } else { + undo_count + }; - let next_tt_key = self.tt_key(next_signature, next_undo_count); - if Self::should_prune_by_tt(tt, next_tt_key, next_remaining_budget) { - continue; - } + if let Some(max) = self.max_undos { + if next_undo_count > max { + continue; + } + } + // Avoid cycles in the current path (depth-first search). + if path_signatures.contains(&next_signature) { + continue; + } - Self::upsert_remaining_budget(tt, next_tt_key, next_remaining_budget); - path_signatures.insert(next_signature); - path.push(transition.the_move); - context.consider_progress(&next_board, path); - - match self.search_with_bound( - &next_board, - next_depth, - bound, - path, - next_undo_count, - path_signatures, - tt, - context, - ) { - IdaSearchResult::Found => return IdaSearchResult::Found, - IdaSearchResult::Timeout => return IdaSearchResult::Timeout, - IdaSearchResult::CheckpointTriggered => { - return IdaSearchResult::CheckpointTriggered + let next_tt_key = self.tt_key(next_signature, next_undo_count); + if Self::should_prune_by_tt(tt, next_tt_key, next_remaining_budget) { + continue; } - IdaSearchResult::NextBound(nb) => { - if nb < min_next_bound { - min_next_bound = nb; + + Self::upsert_remaining_budget(tt, next_tt_key, next_remaining_budget); + path_signatures.insert(next_signature); + path.push(transition.the_move); + context.consider_progress(&next_board, path); + + match self.search_with_bound( + &next_board, + next_depth, + bound, + path, + next_undo_count, + path_signatures, + tt, + context, + ) { + IdaSearchResult::Found => break 'nodes IdaSearchResult::Found, + IdaSearchResult::Timeout => break 'nodes IdaSearchResult::Timeout, + IdaSearchResult::CheckpointTriggered => { + break 'nodes IdaSearchResult::CheckpointTriggered + } + IdaSearchResult::NextBound(nb) => { + if nb < min_next_bound { + min_next_bound = nb; + } } } - } - path.pop(); - path_signatures.remove(&next_signature); + path.pop(); + path_signatures.remove(&next_signature); + } + break IdaSearchResult::NextBound(min_next_bound); + }; + if matches!(result, IdaSearchResult::NextBound(_)) { + path.truncate(restore_path_len); + for signature in chain_signatures { + path_signatures.remove(&signature); + } } - IdaSearchResult::NextBound(min_next_bound) + result } } @@ -483,6 +634,7 @@ impl KlondikeSolver { // IDA* result and context // ═══════════════════════════════════════════ +#[derive(Debug, PartialEq, Eq)] pub enum IdaSearchResult { Found, NextBound(i64), @@ -821,6 +973,10 @@ pub fn deal(deck: &[crate::common::card::Card]) -> Option { super::board::deal(deck) } +#[cfg(test)] +#[path = "solver_optimizations_tests.rs"] +mod optimizations_tests; + #[cfg(test)] mod tests { use super::*; @@ -972,6 +1128,10 @@ mod tests { board.columns[0].push(card(Suit::Club, 8), false); board.columns[0].push(card(Suit::Heart, 7), false); board.columns[1].push(card(Suit::Spade, 8), false); + // Keep this fixture out of the fully visible endgame closure so it + // continues to exercise the recursive path-signature cycle check. + board.columns[6].push(card(Suit::Diamond, 9), true); + board.columns[6].push(card(Suit::Spade, 10), false); board.compute_signature(); let forward = KlondikeMove::ColumnToColumn { source: 0, diff --git a/solver-core-rs/src/klondike/solver_optimizations_tests.rs b/solver-core-rs/src/klondike/solver_optimizations_tests.rs new file mode 100644 index 0000000..ba9f00d --- /dev/null +++ b/solver-core-rs/src/klondike/solver_optimizations_tests.rs @@ -0,0 +1,511 @@ +use super::*; +use crate::common::card::Card; + +fn visible_endgame() -> KlondikeBoard { + let mut board = KlondikeBoard::new(); + board.foundation = [10; 4]; + for (column, suits) in [ + [Suit::Club, Suit::Heart, Suit::Club], + [Suit::Diamond, Suit::Spade, Suit::Heart], + [Suit::Heart, Suit::Club, Suit::Diamond], + [Suit::Spade, Suit::Diamond, Suit::Spade], + ] + .into_iter() + .enumerate() + { + for (suit, rank) in suits.into_iter().zip([13, 12, 11]) { + board.columns[column].push(Card::new(suit, rank), false); + } + } + board.compute_signature(); + board +} + +fn assert_full_deck(board: &KlondikeBoard) { + let mut cards = board.stock[..board.stock_len as usize].to_vec(); + for column in &board.columns { + cards.extend_from_slice(&column.cards[..column.len as usize]); + } + for suit in Suit::ALL { + cards.extend((1..=board.foundation[suit as usize]).map(|rank| Card::new(suit, rank))); + } + cards.sort_by_key(|card| card.encode()); + assert_eq!(cards, Card::standard_deck()); +} + +fn replay(mut board: KlondikeBoard, moves: &[KlondikeMove], draw: u8) -> KlondikeBoard { + assert_full_deck(&board); + for &step in moves { + assert!(KlondikeMove::find_candidate_moves(&board, draw).contains(&step)); + board = step.apply(board).expect("legal primitive replay"); + assert_full_deck(&board); + } + board +} + +fn context(draw: u8) -> IdaContext { + IdaContext::new(Instant::now(), None, i64::MIN, draw) +} + +#[test] +fn visible_closure_replays_all_cards_and_keeps_the_existing_prefix_once() { + let initial = visible_endgame(); + let first = KlondikeMove::ColumnToFoundation { + source: 0, + card: Card::new(Suit::Club, 11), + }; + let after = first.apply(initial).unwrap(); + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + let solver = KlondikeSolver::new_with_mode(draw, mode); + let mut ctx = context(draw as u8); + assert_eq!( + solver.try_finish_visible_without_stock(&after, 1, 12, &[first], &mut ctx,), + Some(IdaSearchResult::Found) + ); + assert_eq!(ctx.checked_nodes, 11); + assert_eq!(ctx.checkpoint_nodes, 11); + assert_eq!(ctx.progress_sample_counter, 11); + let moves = ctx.solution_path.unwrap(); + assert_eq!(moves.len(), 12); + assert_eq!(moves[0], first); + assert!(KlondikeSolver::is_win(&replay(initial, &moves, draw as u8))); + // Exercise the public root path, including its initial zero heuristic. + let stats = solver.solve_with_stats(&initial, false); + let moves = stats.moves.expect("direct closure from root"); + assert_eq!(moves.len(), 12); + assert_eq!(stats.checkpoints_adopted, 0); + let won = replay(initial, &moves, draw as u8); + assert!(KlondikeSolver::is_win(&won)); + assert_eq!(solver.solve(&won, false), Some(vec![])); + let mut won_ctx = context(draw as u8); + assert_eq!( + solver.try_finish_visible_without_stock(&won, 0, 0, &[], &mut won_ctx), + Some(IdaSearchResult::Found) + ); + assert_eq!(won_ctx.checked_nodes, 0); + } + } +} + +#[test] +fn visible_closure_returns_exact_bound_and_obeys_the_smallest_depth_limit() { + let board = visible_endgame(); + let mut solver = KlondikeSolver::new(1); + let mut ctx = context(1); + assert_eq!( + solver.try_finish_visible_without_stock(&board, 0, 11, &[], &mut ctx), + Some(IdaSearchResult::NextBound(12)) + ); + assert_eq!(ctx.checked_nodes, 0); + assert_eq!(ctx.progress_sample_counter, 0); + solver + .rules + .push(KlondikeRule::DepthLimit { max_depth: 11 }); + assert_eq!( + solver.try_finish_visible_without_stock(&board, 0, 100, &[], &mut ctx), + Some(IdaSearchResult::NextBound(i64::MAX)) + ); + assert_eq!(solver.solve(&board, false), None); + assert_eq!(ctx.checked_nodes, 0); +} + +#[test] +fn visible_closure_checks_budgets_before_accepting_the_last_move_as_a_win() { + let board = visible_endgame(); + let solver = KlondikeSolver::new(3); + for limit in [1, 4, 12] { + let mut ctx = context(3); + ctx.checkpoint_node_limit = limit; + assert_eq!( + solver.try_finish_visible_without_stock(&board, 0, 12, &[], &mut ctx), + Some(IdaSearchResult::CheckpointTriggered) + ); + assert_eq!(ctx.checked_nodes, limit); + assert_eq!(ctx.checkpoint_nodes, limit); + assert_eq!(ctx.progress_sample_counter, limit); + assert!(ctx.solution_path.is_none()); + assert_eq!(ctx.best_progress_path.len(), (limit / 4 * 4) as usize); + replay(board, &ctx.best_progress_path, 3); + } + let mut expired = IdaContext::new( + Instant::now() - Duration::from_secs(1), + Some(Duration::ZERO), + 0, + 3, + ); + assert_eq!( + solver.try_finish_visible_without_stock(&board, 0, 12, &[], &mut expired), + Some(IdaSearchResult::Timeout) + ); + assert_eq!(expired.checked_nodes, 0); + assert!(expired.solution_path.is_none()); +} + +#[test] +fn visible_closure_declines_stock_waste_and_hidden_cards_and_rejects_dead_ends() { + let solver = KlondikeSolver::new(1); + for stock_index in [0, 1] { + let mut board = visible_endgame(); + board.stock[0] = board.columns[0].pop().unwrap(); + board.stock_len = 1; + board.stock_index = stock_index; + board.compute_signature(); + assert_full_deck(&board); + assert_eq!( + solver.try_finish_visible_without_stock(&board, 0, 100, &[], &mut context(1)), + None + ); + } + let mut hidden = visible_endgame(); + hidden.columns[0].face_down_len = 1; + assert_eq!( + solver.try_finish_visible_without_stock(&hidden, 0, 100, &[], &mut context(1)), + None + ); + let mut ctx = context(1); + assert_eq!( + solver.try_finish_visible_without_stock(&KlondikeBoard::new(), 0, 52, &[], &mut ctx), + Some(IdaSearchResult::NextBound(i64::MAX)) + ); + assert!(ctx.solution_path.is_none()); +} + +// A deliberately primitive DFS oracle: one recursive call per move, with no +// terminal closure or chain compression. Used only on bounded fixtures that +// retain hidden cards or stock, to isolate P4c's accounting and backtracking. +impl KlondikeSolver { + fn search_primitive_reference( + &self, + board: &KlondikeBoard, + depth: usize, + bound: i64, + path: &mut Vec, + undo_count: usize, + path_signatures: &mut AHashSet, + tt: &mut AHashMap, + context: &mut IdaContext, + ) -> IdaSearchResult { + if context.should_timeout() { + return IdaSearchResult::Timeout; + } + if context.should_checkpoint() { + return IdaSearchResult::CheckpointTriggered; + } + + // Limit memory: clear the TT when it exceeds the configured threshold. + if tt.len() >= weights::TT_MAX_ENTRIES { + tt.clear(); + } + + let h_cost = BoardEval::from_board_fast(board, self.draw_advance).heuristic_cost(); + let f_score = depth as i64 + h_cost; + // Typical IDA* pruning: the node exceeds the f = g + h bound. + if f_score > bound { + return IdaSearchResult::NextBound(f_score); + } + + if Self::is_win(board) { + context.solution_path = Some(path.clone()); + return IdaSearchResult::Found; + } + + let prev_move = path.last().copied(); + let transitions = self.successors(board, prev_move, depth); + let mut min_next_bound = i64::MAX; + + for transition in transitions { + let next_board = transition.to_board; + let next_signature = next_board.signature; + let next_depth = depth + 1; + let next_remaining_budget = bound - next_depth as i64; + let next_undo_count = if transition.the_move.is_from_foundation() { + undo_count + 1 + } else { + undo_count + }; + + if let Some(max) = self.max_undos { + if next_undo_count > max { + continue; + } + } + // Avoid cycles in the current path (depth-first search). + if path_signatures.contains(&next_signature) { + continue; + } + + let next_tt_key = self.tt_key(next_signature, next_undo_count); + if Self::should_prune_by_tt(tt, next_tt_key, next_remaining_budget) { + continue; + } + + Self::upsert_remaining_budget(tt, next_tt_key, next_remaining_budget); + path_signatures.insert(next_signature); + path.push(transition.the_move); + context.consider_progress(&next_board, path); + + match self.search_primitive_reference( + &next_board, + next_depth, + bound, + path, + next_undo_count, + path_signatures, + tt, + context, + ) { + IdaSearchResult::Found => return IdaSearchResult::Found, + IdaSearchResult::Timeout => return IdaSearchResult::Timeout, + IdaSearchResult::CheckpointTriggered => { + return IdaSearchResult::CheckpointTriggered + } + IdaSearchResult::NextBound(nb) => { + if nb < min_next_bound { + min_next_bound = nb; + } + } + } + + path.pop(); + path_signatures.remove(&next_signature); + } + IdaSearchResult::NextBound(min_next_bound) + } +} + +fn forced_chain() -> KlondikeBoard { + let mut board = KlondikeBoard::new(); + board.foundation = [0, 2, 2, 2]; + for rank in (1..=5).rev() { + board.columns[0].push(Card::new(Suit::Club, rank), rank != 1); + } + board.stock[0] = Card::new(Suit::Spade, 9); + board.stock_len = 1; + board.compute_signature(); + board +} + +#[derive(Debug, PartialEq, Eq)] +struct SearchSnapshot { + result: IdaSearchResult, + path: Vec, + signatures: Vec, + tt: Vec<(u64, u16, i64)>, + checked: u64, + checkpoint: u64, + samples: u64, + progress: i64, + progress_path: Vec, + solution: Option>, +} + +#[test] +fn safe_chains_match_primitive_search_at_every_intermediate_cut_and_branch() { + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + for scenario in 0..10 { + let mut board = forced_chain(); + if scenario == 8 { + // Two safe candidates: preserve both branches instead of choosing one. + board.foundation[Suit::Spade as usize] = 0; + board.columns[1].push(Card::new(Suit::Spade, 1), false); + board.compute_signature(); + } + let mut solver = KlondikeSolver::new_with_mode(draw, mode); + solver.rules.push(KlondikeRule::DepthLimit { + max_depth: if scenario == 5 { 2 } else { 5 }, + }); + solver.max_undos = Some(0); + let first = solver.successors(&board, None, 0).remove(0); + let after = first.to_board; + let initial_path = vec![first.the_move]; + let bound = if scenario == 9 { 1 } else { 100 }; + let run = |compressed: bool| { + let mut ctx = context(draw as u8); + ctx.checkpoint_node_limit = match scenario { + 1..=4 => scenario, + _ => 1000, + }; + if scenario == 7 { + ctx.start = Instant::now() - Duration::from_secs(1); + ctx.timeout = Some(Duration::ZERO); + ctx.checked_nodes = 2046; + } + let mut signatures = AHashSet::from_iter([board.signature, after.signature]); + let mut tt = AHashMap::new(); + if scenario == 0 || scenario == 6 { + let child = solver + .successors(&after, initial_path.last().copied(), 1) + .remove(0); + if scenario == 0 { + signatures.insert(child.to_board.signature); + } else { + KlondikeSolver::upsert_remaining_budget( + &mut tt, + solver.tt_key(child.to_board.signature, 0), + 98, + ); + } + } + let mut path = initial_path.clone(); + let result = if compressed { + solver.search_with_bound( + &after, + 1, + bound, + &mut path, + 0, + &mut signatures, + &mut tt, + &mut ctx, + ) + } else { + solver.search_primitive_reference( + &after, + 1, + bound, + &mut path, + 0, + &mut signatures, + &mut tt, + &mut ctx, + ) + }; + let mut signatures: Vec<_> = signatures.into_iter().collect(); + signatures.sort_unstable(); + let mut tt: Vec<_> = tt + .into_iter() + .map(|(key, value)| (key.signature, key.undo_bucket, value)) + .collect(); + tt.sort_unstable(); + SearchSnapshot { + result, + path, + signatures, + tt, + checked: ctx.checked_nodes, + checkpoint: ctx.checkpoint_nodes, + samples: ctx.progress_sample_counter, + progress: ctx.best_progress, + progress_path: ctx.best_progress_path, + solution: ctx.solution_path, + } + }; + let actual = run(true); + let expected = run(false); + assert_eq!( + actual, expected, + "draw {draw}, mode {mode:?}, scenario {scenario}" + ); + if matches!(actual.result, IdaSearchResult::NextBound(_)) { + assert_eq!(actual.path, initial_path, "restore the caller's prefix"); + } + if scenario == 7 { + assert_eq!(actual.result, IdaSearchResult::Timeout); + assert_eq!(actual.checked, 2048); + } + } + } + } +} + +#[test] +fn safe_chain_checks_the_intermediate_f_score_and_restores_signatures() { + let mut board = KlondikeBoard::new(); + board.foundation = [2, 2, 2, 0]; + board.columns[0].push(Card::new(Suit::Club, 4), true); + board.columns[0].push(Card::new(Suit::Club, 3), false); + board.columns[1].push(Card::new(Suit::Spade, 10), true); + board.columns[1].push(Card::new(Suit::Diamond, 11), false); + board.stock[0] = Card::new(Suit::Spade, 9); + board.stock_len = 1; + board.compute_signature(); + for draw in [1, 3] { + let solver = KlondikeSolver::new(draw); + let transitions = solver.successors(&board, None, 0); + assert_eq!(transitions.len(), 1); + assert!(is_safe_foundation_move(&transitions[0], &board)); + let bound = BoardEval::from_board_fast(&board, draw as u8).heuristic_cost(); + let next_f = + 1 + BoardEval::from_board_fast(&transitions[0].to_board, draw as u8).heuristic_cost(); + assert!(next_f > bound); + let mut ctx = context(draw as u8); + let mut path = vec![]; + let mut signatures = AHashSet::from_iter([board.signature]); + let result = solver.search_with_bound( + &board, + 0, + bound, + &mut path, + 0, + &mut signatures, + &mut AHashMap::new(), + &mut ctx, + ); + assert_eq!(result, IdaSearchResult::NextBound(next_f)); + assert_eq!(ctx.checked_nodes, 2); + assert_eq!(ctx.progress_sample_counter, 1); + assert!(path.is_empty()); + assert_eq!(signatures, AHashSet::from_iter([board.signature])); + } +} + +#[test] +fn safe_foundation_fast_path_respects_depth_even_at_the_root() { + let board = forced_chain(); + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + let mut solver = KlondikeSolver::new_with_mode(draw, mode); + solver.rules.push(KlondikeRule::DepthLimit { max_depth: 0 }); + assert!(solver.successors(&board, None, 0).is_empty()); + assert!(solver.solve(&board, false).is_none()); + } + } +} + +#[test] +fn safe_stock_chain_replays_primitive_moves_and_checks_the_budget_before_victory() { + let mut board = KlondikeBoard::new(); + board.foundation = [10, 13, 13, 13]; + for (i, rank) in [13, 12, 11].into_iter().enumerate() { + board.stock[i] = Card::new(Suit::Club, rank); + } + board.stock_len = 3; + board.stock_index = 3; + board.compute_signature(); + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + let mut solver = KlondikeSolver::new_with_mode(draw, mode); + solver.max_undos = Some(0); + for limit in [4, 5] { + let mut ctx = context(draw as u8); + ctx.checkpoint_node_limit = limit; + let mut path = vec![]; + let mut signatures = AHashSet::from_iter([board.signature]); + let result = solver.search_with_bound( + &board, + 0, + 3, + &mut path, + 0, + &mut signatures, + &mut AHashMap::new(), + &mut ctx, + ); + assert_eq!(ctx.checked_nodes, 4); + assert_eq!(ctx.checkpoint_nodes, 4); + assert_eq!(ctx.progress_sample_counter, 3); + assert_eq!(path.len(), 3); + assert!(KlondikeSolver::is_win(&replay(board, &path, draw as u8))); + if limit == 4 { + assert_eq!(result, IdaSearchResult::CheckpointTriggered); + assert!(ctx.solution_path.is_none()); + } else { + assert_eq!(result, IdaSearchResult::Found); + assert_eq!(ctx.solution_path, Some(path)); + } + } + } + } +} From ef495d4721086c098aaf7b8add795e48999da216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Tue, 6 Oct 2026 14:39:45 -0300 Subject: [PATCH 5/6] docs: record latest Klondike Draw 1 test results --- .../KlondikeDraw1_results_20261006_143625.csv | 512 ++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 docs/results/KlondikeDraw1_results_20261006_143625.csv diff --git a/docs/results/KlondikeDraw1_results_20261006_143625.csv b/docs/results/KlondikeDraw1_results_20261006_143625.csv new file mode 100644 index 0000000..b6fc6ce --- /dev/null +++ b/docs/results/KlondikeDraw1_results_20261006_143625.csv @@ -0,0 +1,512 @@ +# Testing de Klondike Draw 1: 500 partidas, 399 victorias (79.80%), 101 stalled, 0 timeouts y 8.17 s de duración media. La revisión ejecutada no está registrada en el CSV; al guardar este resultado, el código del worktree coincide con 0d47522 (P1 + P2a/P2c + P3 + P4), sin P6. + +configuration +game,search,simulations,parallel_games,auto_parallel,undos,timeout_s,max_depth +"Klondike Draw 1","IDA*",500,8,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,79.80%,115.89,0.48,1.24,8.17s,399,101,0 + +details +game_id,moves,undos,checkpoints,result,stop_reason,duration_sec,score +1,106,0,0,won,win,6.711,750 +2,118,3,3,lost,stalled,11.202,490 +3,114,0,0,won,win,3.576,740 +4,118,1,1,won,win,7.758,785 +5,123,0,2,won,win,9.026,765 +6,124,1,1,won,win,8.731,800 +7,100,2,3,lost,stalled,11.005,470 +8,144,0,2,won,win,9.505,820 +9,72,1,1,lost,stalled,7.510,290 +10,141,4,3,won,win,9.963,755 +11,119,0,0,won,win,2.557,755 +12,126,0,1,won,win,7.621,725 +13,117,1,2,won,win,11.152,750 +14,113,1,1,won,win,8.089,735 +15,110,0,0,won,win,1.031,750 +16,133,1,1,won,win,8.272,795 +17,110,0,1,won,win,7.358,720 +18,120,0,1,won,win,8.406,780 +19,123,2,1,won,win,8.377,790 +20,140,0,3,won,win,8.648,805 +21,117,0,0,won,win,1.150,755 +22,164,1,1,won,win,7.768,830 +23,93,0,0,won,win,0.107,710 +24,125,1,1,won,win,8.018,785 +25,110,0,1,won,win,7.362,795 +26,120,0,1,won,win,8.196,755 +27,113,0,0,won,win,3.854,740 +28,113,0,0,won,win,0.573,745 +29,153,1,1,won,win,7.398,820 +30,118,0,1,won,win,7.826,765 +31,88,0,1,lost,stalled,9.647,240 +32,44,2,1,lost,stalled,10.330,90 +33,116,0,1,won,win,7.767,770 +34,84,0,2,lost,stalled,8.637,360 +35,126,0,1,won,win,8.115,800 +36,80,0,2,lost,stalled,9.732,425 +37,138,1,2,won,win,9.307,840 +38,104,0,0,won,win,1.290,765 +39,117,0,1,won,win,7.730,745 +40,124,0,1,won,win,9.217,810 +41,114,0,1,won,win,7.224,725 +42,138,0,2,won,win,8.637,775 +43,127,0,0,won,win,3.295,795 +44,140,1,1,won,win,7.416,795 +45,112,0,1,won,win,8.050,760 +46,120,0,1,won,win,7.722,750 +47,114,0,1,won,win,9.622,745 +48,144,1,2,won,win,9.009,860 +49,123,0,1,won,win,8.274,805 +50,131,0,2,won,win,9.685,770 +51,120,0,1,won,win,8.302,740 +52,126,0,2,won,win,9.788,835 +53,92,0,2,lost,stalled,9.838,355 +54,114,0,1,won,win,9.051,740 +55,124,0,1,won,win,7.963,790 +56,93,0,2,lost,stalled,9.928,290 +57,110,0,1,won,win,8.105,730 +58,105,3,3,lost,stalled,12.895,330 +59,134,0,1,won,win,10.591,790 +60,119,0,1,won,win,7.381,750 +61,122,1,1,won,win,10.152,760 +62,110,0,1,won,win,7.667,765 +63,132,0,3,won,win,9.616,805 +64,114,0,1,won,win,7.605,735 +65,118,0,1,won,win,8.144,765 +66,120,0,1,won,win,7.971,765 +67,110,0,1,won,win,8.331,730 +68,115,0,1,won,win,8.794,770 +69,132,0,1,won,win,8.116,805 +70,112,0,1,won,win,7.312,745 +71,136,1,1,won,win,7.863,815 +72,94,2,2,lost,stalled,10.416,225 +73,121,2,1,won,win,7.355,765 +74,116,0,1,won,win,8.057,760 +75,116,0,0,won,win,5.669,735 +76,103,2,3,lost,stalled,11.532,385 +77,117,0,0,won,win,2.150,760 +78,146,2,1,won,win,7.507,820 +79,116,0,1,won,win,7.662,765 +80,129,1,2,won,win,9.798,805 +81,128,0,1,won,win,9.284,785 +82,116,0,0,won,win,2.924,755 +83,93,0,2,lost,stalled,10.014,375 +84,118,0,1,won,win,7.690,775 +85,137,1,1,won,win,8.524,840 +86,103,0,0,won,win,0.240,760 +87,119,0,1,won,win,8.072,770 +88,103,0,1,won,win,7.794,760 +89,109,0,1,won,win,8.479,790 +90,123,0,1,won,win,8.433,790 +91,19,0,0,lost,stalled,0.732,25 +92,136,0,2,won,win,10.075,805 +93,136,2,2,won,win,8.774,780 +94,113,0,1,won,win,8.599,745 +95,93,0,2,lost,stalled,10.808,355 +96,110,0,1,won,win,7.519,755 +97,133,1,1,won,win,8.133,765 +98,161,1,4,won,win,13.230,845 +99,84,0,1,lost,stalled,8.176,310 +100,121,0,1,won,win,7.999,785 +101,103,0,3,lost,stalled,9.019,495 +102,130,0,1,won,win,7.644,795 +103,112,0,1,won,win,7.595,755 +104,61,2,2,lost,stalled,10.678,190 +105,124,0,1,won,win,8.231,760 +106,109,0,0,won,win,4.909,790 +107,132,1,1,won,win,7.706,820 +108,118,0,1,won,win,7.429,800 +109,117,1,1,won,win,8.178,780 +110,133,2,2,won,win,9.466,815 +111,141,3,2,won,win,9.486,805 +112,84,2,2,lost,stalled,9.911,300 +113,121,0,2,won,win,10.629,765 +114,109,1,0,won,win,1.695,725 +115,123,0,1,won,win,8.409,800 +116,132,0,1,won,win,8.782,815 +117,152,0,2,won,win,9.641,850 +118,125,0,2,won,win,9.200,820 +119,129,0,1,won,win,7.702,805 +120,126,0,1,won,win,7.653,775 +121,114,1,1,won,win,8.480,800 +122,96,1,2,lost,stalled,9.052,325 +123,116,1,2,won,win,8.394,730 +124,128,1,1,won,win,8.691,825 +125,130,1,2,won,win,8.469,790 +126,103,0,0,won,win,1.472,760 +127,74,0,2,lost,stalled,10.064,360 +128,146,2,3,won,win,10.260,840 +129,116,0,0,won,win,3.652,780 +130,124,1,1,won,win,9.187,795 +131,104,1,0,won,win,6.446,710 +132,111,1,1,won,win,7.621,745 +133,120,0,0,won,win,4.724,760 +134,88,0,1,lost,stalled,8.777,280 +135,134,1,2,won,win,8.046,775 +136,134,2,2,won,win,9.047,820 +137,108,0,1,won,win,7.398,715 +138,148,3,2,won,win,8.782,795 +139,136,0,0,won,win,7.374,750 +140,105,4,2,lost,stalled,8.620,480 +141,112,1,1,won,win,7.572,740 +142,118,0,1,won,win,9.140,780 +143,118,0,1,won,win,8.205,760 +144,120,0,0,won,win,7.369,780 +145,106,0,0,won,win,0.889,775 +146,78,2,1,lost,stalled,8.708,240 +147,121,2,0,won,win,5.006,750 +148,91,1,2,lost,stalled,11.221,310 +149,123,0,1,won,win,7.340,765 +150,87,0,3,lost,stalled,11.253,405 +151,118,0,1,won,win,7.794,770 +152,94,1,2,lost,stalled,9.430,410 +153,127,0,1,won,win,8.936,775 +154,124,0,1,won,win,7.802,805 +155,115,1,1,won,win,7.656,725 +156,96,0,0,won,win,7.160,725 +157,113,0,1,won,win,8.693,810 +158,116,3,3,lost,stalled,9.823,420 +159,96,0,0,won,win,0.397,725 +160,160,0,2,won,win,10.213,860 +161,127,0,1,won,win,10.401,810 +162,60,0,1,lost,stalled,9.250,145 +163,78,2,2,lost,stalled,9.601,285 +164,116,0,1,won,win,7.632,770 +165,134,0,1,won,win,8.009,795 +166,114,0,0,won,win,3.093,750 +167,111,0,1,won,win,8.280,745 +168,117,1,4,lost,stalled,12.883,595 +169,129,0,1,won,win,7.914,805 +170,122,0,1,won,win,9.349,735 +171,143,0,1,won,win,7.339,820 +172,112,0,0,won,win,5.241,805 +173,125,1,1,won,win,7.364,780 +174,38,1,1,lost,stalled,9.022,80 +175,109,0,1,won,win,7.960,790 +176,123,0,1,won,win,8.517,750 +177,130,0,1,won,win,8.325,830 +178,126,0,0,won,win,2.405,720 +179,74,1,1,lost,stalled,8.513,215 +180,123,0,1,won,win,7.904,790 +181,113,0,1,won,win,7.804,745 +182,114,2,1,won,win,7.455,785 +183,104,0,0,won,win,4.319,765 +184,123,0,0,won,win,7.188,785 +185,130,0,1,won,win,8.748,800 +186,140,0,1,won,win,8.475,750 +187,112,0,0,won,win,0.831,755 +188,110,0,0,won,win,1.214,730 +189,132,0,1,won,win,7.515,750 +190,101,1,3,lost,stalled,13.789,295 +191,118,0,2,won,win,8.515,770 +192,122,0,1,won,win,8.596,785 +193,131,0,1,won,win,7.981,800 +194,121,3,1,won,win,8.116,755 +195,122,1,1,won,win,7.381,805 +196,121,0,1,won,win,8.645,795 +197,117,0,1,won,win,7.910,775 +198,141,0,1,won,win,9.959,815 +199,120,0,1,won,win,8.743,785 +200,131,0,2,won,win,8.936,830 +201,70,0,1,lost,stalled,8.703,180 +202,134,0,1,won,win,8.558,765 +203,109,0,1,won,win,7.880,790 +204,77,0,1,lost,stalled,8.825,145 +205,144,3,1,won,win,8.065,810 +206,117,1,0,won,win,5.612,760 +207,126,0,2,won,win,9.446,750 +208,75,0,1,lost,stalled,9.650,270 +209,112,0,0,won,win,4.081,750 +210,125,0,1,won,win,7.844,780 +211,114,0,1,won,win,8.010,755 +212,103,0,2,lost,stalled,10.913,450 +213,125,0,2,won,win,9.080,805 +214,115,0,1,won,win,8.629,770 +215,101,0,1,lost,stalled,8.833,245 +216,41,0,1,lost,stalled,10.807,145 +217,117,0,1,won,win,7.895,765 +218,103,0,0,won,win,3.607,760 +219,122,0,0,won,win,2.917,785 +220,86,1,1,lost,stalled,7.521,230 +221,81,0,1,lost,stalled,9.642,255 +222,125,1,1,won,win,8.227,735 +223,138,1,0,won,win,8.108,775 +224,141,2,2,won,win,8.376,800 +225,75,0,1,lost,stalled,10.604,185 +226,115,1,1,won,win,8.554,735 +227,119,0,0,won,win,4.478,765 +228,116,0,1,won,win,8.497,780 +229,81,0,3,lost,stalled,10.849,335 +230,127,0,1,won,win,7.347,750 +231,101,1,2,lost,stalled,10.594,415 +232,116,0,1,won,win,7.825,750 +233,147,1,1,won,win,11.104,830 +234,125,0,1,won,win,9.870,800 +235,119,0,1,won,win,10.037,785 +236,105,0,0,won,win,0.418,770 +237,134,2,1,won,win,9.936,805 +238,129,0,2,won,win,8.972,815 +239,119,0,1,won,win,8.924,750 +240,162,2,2,won,win,10.167,815 +241,119,0,0,won,win,4.467,795 +242,130,0,1,won,win,8.054,765 +243,116,0,1,won,win,8.746,770 +244,120,0,1,won,win,8.886,815 +245,115,0,1,won,win,8.080,750 +246,126,0,1,won,win,8.854,770 +247,109,0,0,won,win,5.878,705 +248,134,1,3,won,win,10.083,805 +249,146,0,1,won,win,9.513,765 +250,161,3,2,won,win,8.801,800 +251,118,2,0,won,win,1.065,805 +252,125,1,1,won,win,8.528,815 +253,135,1,2,won,win,9.666,795 +254,133,0,2,won,win,8.217,805 +255,115,0,1,won,win,9.008,760 +256,82,0,2,lost,stalled,12.666,425 +257,85,0,2,lost,stalled,12.173,280 +258,161,2,4,won,win,10.366,905 +259,117,0,2,won,win,9.547,790 +260,111,1,1,won,win,8.175,785 +261,98,0,2,lost,stalled,10.081,425 +262,122,0,1,won,win,9.253,805 +263,135,1,2,won,win,9.125,825 +264,130,1,1,won,win,7.724,820 +265,123,0,2,won,win,14.434,770 +266,122,0,1,won,win,11.948,760 +267,112,0,0,won,win,3.081,730 +268,83,0,2,lost,stalled,13.885,405 +269,108,0,1,won,win,12.026,720 +270,113,0,1,won,win,11.930,730 +271,134,0,2,won,win,13.587,790 +272,141,1,2,won,win,13.426,835 +273,102,0,2,lost,stalled,8.784,450 +274,134,4,2,won,win,9.437,770 +275,113,0,1,won,win,7.951,765 +276,149,2,4,won,win,12.200,845 +277,131,0,2,won,win,8.771,775 +278,86,0,1,lost,stalled,9.976,290 +279,92,0,2,lost,stalled,10.168,475 +280,130,0,1,won,win,8.772,815 +281,138,0,1,won,win,9.523,830 +282,130,1,1,won,win,8.302,735 +283,130,5,2,won,win,9.641,800 +284,109,0,1,won,win,8.317,740 +285,134,0,2,won,win,10.828,800 +286,96,0,3,lost,stalled,10.825,390 +287,111,0,0,won,win,0.975,725 +288,116,2,1,won,win,8.204,750 +289,126,0,1,won,win,8.198,750 +290,147,5,2,won,win,10.010,800 +291,115,0,1,won,win,8.487,745 +292,147,2,2,won,win,9.922,820 +293,133,1,1,won,win,8.365,765 +294,140,1,1,won,win,9.311,850 +295,132,0,1,won,win,8.040,830 +296,105,1,2,lost,stalled,9.984,415 +297,134,0,1,won,win,7.675,765 +298,62,0,1,lost,stalled,10.591,130 +299,103,1,1,won,win,7.767,745 +300,127,0,0,won,win,4.273,785 +301,123,0,1,won,win,8.528,825 +302,130,0,2,won,win,10.454,780 +303,125,1,1,won,win,7.656,780 +304,114,0,0,won,win,0.452,750 +305,118,0,1,won,win,10.604,770 +306,58,0,1,lost,stalled,10.702,160 +307,119,0,2,won,win,10.033,780 +308,125,1,1,won,win,9.829,780 +309,122,0,2,won,win,11.640,810 +310,121,1,2,won,win,11.294,775 +311,131,0,0,won,win,6.765,755 +312,92,1,3,lost,stalled,14.289,435 +313,124,0,1,won,win,7.680,800 +314,123,2,1,won,win,7.899,760 +315,105,0,4,lost,stalled,12.060,425 +316,82,1,1,lost,stalled,8.491,275 +317,121,0,1,won,win,7.834,760 +318,132,1,0,won,win,0.068,800 +319,131,0,2,won,win,11.791,830 +320,124,0,1,won,win,8.224,785 +321,120,0,1,won,win,7.661,800 +322,132,1,2,won,win,8.554,830 +323,132,1,2,won,win,9.944,815 +324,105,2,2,lost,stalled,9.787,370 +325,118,0,0,won,win,4.870,745 +326,132,0,1,won,win,7.914,760 +327,20,1,0,lost,stalled,0.506,10 +328,135,2,2,won,win,8.552,755 +329,151,0,3,won,win,10.452,850 +330,130,2,1,won,win,9.761,805 +331,116,0,1,won,win,7.846,735 +332,133,0,1,won,win,8.616,775 +333,104,0,1,won,win,8.317,765 +334,136,1,2,won,win,9.197,855 +335,66,0,1,lost,stalled,7.829,265 +336,124,0,1,won,win,7.757,755 +337,115,0,0,won,win,8.524,820 +338,114,0,1,won,win,8.471,760 +339,136,1,1,won,win,8.549,815 +340,105,0,1,won,win,7.980,725 +341,137,1,1,won,win,8.625,800 +342,140,0,3,won,win,9.762,850 +343,111,1,3,lost,stalled,9.285,475 +344,92,0,1,lost,stalled,8.654,455 +345,122,2,1,won,win,10.449,755 +346,115,0,1,won,win,8.056,780 +347,126,0,1,won,win,8.352,740 +348,124,0,2,won,win,9.560,780 +349,116,0,1,won,win,7.777,745 +350,98,0,3,lost,stalled,10.310,370 +351,134,1,2,won,win,10.312,800 +352,131,0,1,won,win,8.561,780 +353,117,0,1,won,win,7.633,770 +354,110,0,0,won,win,0.699,745 +355,136,0,1,won,win,8.126,775 +356,116,0,1,won,win,7.914,750 +357,131,0,1,won,win,8.236,825 +358,90,0,3,lost,stalled,12.163,370 +359,42,1,0,lost,stalled,0.519,90 +360,109,2,1,lost,stalled,8.971,210 +361,131,0,0,won,win,3.913,780 +362,107,1,2,lost,stalled,9.359,335 +363,140,0,2,won,win,9.601,785 +364,128,1,2,won,win,9.084,785 +365,127,0,1,won,win,7.327,790 +366,121,0,1,won,win,7.730,730 +367,126,0,1,won,win,8.505,810 +368,84,3,2,lost,stalled,11.328,270 +369,116,0,1,won,win,8.028,790 +370,119,0,1,won,win,8.365,750 +371,121,2,1,won,win,8.019,755 +372,115,0,1,won,win,8.107,790 +373,88,2,2,lost,stalled,11.585,285 +374,91,0,2,lost,stalled,10.058,385 +375,136,0,4,won,win,10.350,855 +376,112,0,1,won,win,7.782,750 +377,121,0,1,won,win,8.968,785 +378,121,0,1,won,win,7.739,785 +379,109,0,1,won,win,7.888,755 +380,123,0,1,won,win,8.615,780 +381,86,1,2,lost,stalled,11.175,235 +382,124,0,1,won,win,8.656,810 +383,150,1,4,won,win,11.436,855 +384,112,0,1,won,win,8.418,785 +385,114,0,1,won,win,8.186,775 +386,124,0,1,won,win,7.671,750 +387,92,0,3,lost,stalled,9.915,425 +388,99,2,3,lost,stalled,12.680,365 +389,142,0,2,won,win,9.171,835 +390,110,0,1,won,win,8.773,795 +391,122,0,0,won,win,4.232,780 +392,129,0,1,won,win,8.266,795 +393,103,0,1,won,win,7.029,715 +394,139,0,2,won,win,9.010,790 +395,115,0,1,won,win,8.879,750 +396,91,0,3,lost,stalled,10.881,440 +397,117,0,1,won,win,7.559,780 +398,38,1,0,lost,stalled,8.218,80 +399,110,0,4,lost,stalled,10.873,510 +400,128,1,1,won,win,9.095,740 +401,79,0,2,lost,stalled,9.768,415 +402,138,0,2,won,win,8.831,830 +403,122,0,1,won,win,7.404,770 +404,122,0,1,won,win,7.334,770 +405,109,0,0,won,win,1.051,755 +406,120,0,1,won,win,6.925,770 +407,112,0,2,lost,stalled,9.434,495 +408,47,0,1,lost,stalled,7.750,120 +409,105,4,3,lost,stalled,10.639,345 +410,116,0,1,won,win,7.774,780 +411,80,0,2,lost,stalled,9.475,495 +412,47,0,1,lost,stalled,17.728,90 +413,128,2,2,won,win,9.014,820 +414,152,0,2,won,win,8.786,780 +415,131,1,1,won,win,9.160,835 +416,120,0,1,won,win,9.562,770 +417,123,1,1,won,win,8.339,820 +418,130,0,1,won,win,8.288,760 +419,89,1,2,lost,stalled,9.903,415 +420,120,0,1,won,win,8.413,770 +421,115,0,0,won,win,3.915,735 +422,129,0,1,won,win,8.682,795 +423,99,1,2,lost,stalled,8.821,495 +424,119,0,1,won,win,8.421,765 +425,101,0,2,lost,stalled,9.809,340 +426,121,0,1,won,win,7.678,755 +427,110,0,1,lost,stalled,8.260,410 +428,109,0,1,won,win,9.002,745 +429,118,0,1,won,win,8.884,835 +430,120,0,0,won,win,3.889,790 +431,170,6,4,won,win,11.149,860 +432,126,0,1,won,win,8.413,795 +433,76,0,1,lost,stalled,8.398,310 +434,126,0,1,won,win,8.174,790 +435,87,2,1,lost,stalled,8.586,215 +436,120,0,1,won,win,8.507,785 +437,111,0,1,won,win,7.611,755 +438,148,1,2,won,win,9.126,830 +439,120,0,1,won,win,7.493,780 +440,113,0,1,won,win,7.664,760 +441,122,0,1,won,win,7.301,765 +442,113,1,4,lost,stalled,10.357,595 +443,93,1,2,lost,stalled,9.470,350 +444,125,0,1,won,win,7.992,790 +445,72,0,2,lost,stalled,11.020,330 +446,163,3,2,won,win,9.634,835 +447,117,1,1,won,win,7.736,740 +448,129,0,1,won,win,7.690,760 +449,132,0,2,won,win,9.673,840 +450,43,0,1,lost,stalled,10.880,220 +451,71,0,2,lost,stalled,12.331,220 +452,111,0,1,won,win,8.722,800 +453,115,0,1,won,win,7.201,735 +454,87,0,2,lost,stalled,9.225,390 +455,91,0,1,lost,stalled,8.432,330 +456,18,0,0,lost,stalled,0.049,40 +457,108,0,1,won,win,7.729,730 +458,113,0,0,won,win,6.259,735 +459,130,1,1,won,win,8.187,795 +460,154,1,3,won,win,11.561,835 +461,134,0,0,won,win,7.074,770 +462,121,0,1,won,win,8.423,785 +463,119,0,0,won,win,4.855,770 +464,127,0,2,won,win,8.582,785 +465,126,0,1,won,win,8.665,775 +466,121,0,1,won,win,8.968,750 +467,113,0,1,won,win,7.542,810 +468,123,1,1,won,win,9.075,780 +469,109,0,0,won,win,0.873,745 +470,159,1,2,won,win,8.259,820 +471,90,3,3,lost,stalled,10.792,330 +472,109,0,1,won,win,8.014,700 +473,117,0,1,won,win,8.659,790 +474,108,0,0,won,win,0.495,725 +475,145,0,2,won,win,9.804,765 +476,125,0,1,won,win,7.708,745 +477,148,0,2,won,win,7.959,790 +478,138,1,2,won,win,8.954,820 +479,113,0,0,won,win,3.142,745 +480,131,1,1,won,win,10.253,770 +481,109,0,0,won,win,1.461,740 +482,129,0,1,won,win,8.186,770 +483,133,0,1,won,win,7.718,780 +484,122,0,1,won,win,8.613,740 +485,125,3,2,won,win,8.076,785 +486,111,3,3,lost,stalled,8.674,400 +487,101,0,0,won,win,6.450,750 +488,131,1,0,won,win,1.270,800 +489,121,2,1,won,win,8.482,740 +490,122,1,1,won,win,8.772,780 +491,121,0,0,won,win,2.412,755 +492,132,0,1,won,win,7.831,780 +493,109,1,0,won,win,1.984,775 +494,114,0,1,won,win,8.573,730 +495,123,0,1,won,win,8.093,805 +496,123,0,0,won,win,4.587,790 +497,123,0,1,won,win,7.361,760 +498,45,0,1,lost,stalled,10.073,190 +499,124,0,1,won,win,7.216,785 +500,115,0,1,won,win,7.097,745 From eb68d941d3bc0f023cf318b478288137e7cb8343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Gurruchaga?= Date: Tue, 6 Oct 2026 15:28:09 -0300 Subject: [PATCH 6/6] fix(klondike): preserve partial progress in visible endgames --- .../KlondikeDraw1_results_20261006_152709.csv | 513 ++++++++++++++++++ solver-core-rs/src/klondike/solver.rs | 44 +- .../klondike/solver_optimizations_tests.rs | 129 +++++ 3 files changed, 674 insertions(+), 12 deletions(-) create mode 100644 docs/results/KlondikeDraw1_results_20261006_152709.csv diff --git a/docs/results/KlondikeDraw1_results_20261006_152709.csv b/docs/results/KlondikeDraw1_results_20261006_152709.csv new file mode 100644 index 0000000..561b205 --- /dev/null +++ b/docs/results/KlondikeDraw1_results_20261006_152709.csv @@ -0,0 +1,513 @@ +# Prueba de la corrección del cierre directo de Klondike (P4a): con allow_partial=true conserva un prefijo legal cuando la profundidad impide completar la partida y registra el progreso final aunque no coincida con el muestreo cada cuatro movimientos. Respeta los límites existentes de profundidad, nodos y tiempo; con allow_partial=false mantiene la poda sin construir un parcial. Incluye P1 + P2a/P2c + P3 + P4, sin P5 ni P6. +# Resultado: 500 partidas; 408 victorias (81.60%); 92 stalled; 0 timeouts; duración media 7.94 s. + +configuration +game,search,simulations,parallel_games,auto_parallel,undos,timeout_s,max_depth +"Klondike Draw 1","IDA*",500,8,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,81.60%,117.59,0.47,1.26,7.94s,408,92,0 + +details +game_id,moves,undos,checkpoints,result,stop_reason,duration_sec,score +1,107,0,0,won,win,6.444,780 +2,125,0,1,won,win,7.874,745 +3,82,0,1,lost,stalled,8.665,320 +4,64,0,2,lost,stalled,12.418,155 +5,126,2,2,won,win,9.030,790 +6,152,1,1,won,win,8.297,795 +7,122,0,0,won,win,6.197,780 +8,93,0,2,lost,stalled,9.557,590 +9,111,2,1,won,win,8.034,770 +10,106,0,1,won,win,8.663,730 +11,99,0,3,lost,stalled,10.268,490 +12,127,0,2,won,win,8.691,780 +13,125,0,0,won,win,0.813,775 +14,115,0,0,won,win,0.854,735 +15,110,0,1,won,win,7.571,745 +16,63,0,1,lost,stalled,10.757,210 +17,131,0,1,won,win,6.783,820 +18,136,0,1,won,win,8.503,790 +19,99,0,3,lost,stalled,10.848,425 +20,129,1,1,won,win,7.661,780 +21,124,0,1,won,win,7.513,805 +22,113,0,0,won,win,5.600,790 +23,120,0,1,won,win,7.935,775 +24,122,0,0,won,win,7.302,755 +25,135,0,2,won,win,8.410,845 +26,79,1,2,lost,stalled,9.718,260 +27,122,0,0,won,win,6.136,755 +28,117,0,1,won,win,7.350,775 +29,91,1,2,lost,stalled,9.188,455 +30,142,1,3,won,win,10.744,805 +31,130,0,1,won,win,7.934,790 +32,113,0,1,won,win,7.494,740 +33,137,0,1,won,win,7.861,820 +34,134,0,1,won,win,8.548,765 +35,135,2,2,won,win,8.634,810 +36,95,2,3,lost,stalled,11.771,405 +37,116,0,1,won,win,7.986,770 +38,118,0,0,won,win,7.709,770 +39,164,0,3,won,win,8.604,855 +40,130,0,1,won,win,8.924,810 +41,122,0,1,won,win,7.656,800 +42,103,0,0,won,win,2.417,760 +43,119,0,1,won,win,7.789,775 +44,120,1,1,won,win,7.706,765 +45,152,0,0,won,win,5.510,815 +46,70,0,1,lost,stalled,8.860,195 +47,169,1,0,won,win,5.302,825 +48,117,0,1,won,win,7.831,810 +49,113,0,1,won,win,7.699,810 +50,116,1,1,lost,stalled,8.771,390 +51,103,1,3,lost,stalled,11.206,330 +52,130,0,1,won,win,7.966,765 +53,88,1,2,lost,stalled,8.644,325 +54,134,0,1,won,win,8.795,775 +55,121,0,1,won,win,7.938,730 +56,61,1,1,lost,stalled,9.745,110 +57,125,0,1,won,win,8.043,800 +58,117,0,1,won,win,8.065,745 +59,134,0,1,won,win,7.695,760 +60,88,0,2,lost,stalled,10.515,210 +61,128,0,1,won,win,7.696,805 +62,109,0,0,won,win,0.107,790 +63,135,0,1,won,win,8.319,820 +64,130,0,2,won,win,9.194,795 +65,52,0,1,lost,stalled,7.581,165 +66,121,0,1,won,win,7.898,800 +67,155,1,2,won,win,8.836,825 +68,121,1,1,won,win,7.331,790 +69,103,2,3,lost,stalled,11.016,505 +70,104,0,1,won,win,7.585,765 +71,116,0,1,won,win,7.807,775 +72,125,0,1,won,win,7.199,790 +73,105,0,0,won,win,0.127,770 +74,119,0,1,won,win,7.990,755 +75,135,0,1,won,win,7.108,860 +76,133,0,2,won,win,9.526,845 +77,51,0,1,lost,stalled,7.595,135 +78,139,0,2,won,win,8.754,840 +79,145,0,3,won,win,10.943,800 +80,135,0,1,won,win,8.929,760 +81,139,0,2,won,win,8.872,830 +82,130,0,1,won,win,7.565,825 +83,120,0,1,won,win,7.652,770 +84,166,6,4,won,win,11.322,920 +85,111,0,0,won,win,3.986,800 +86,132,0,2,won,win,7.426,775 +87,118,0,1,won,win,7.778,780 +88,120,0,0,won,win,5.501,750 +89,147,0,2,won,win,8.501,780 +90,128,1,2,won,win,9.804,805 +91,121,0,1,won,win,8.327,850 +92,122,0,1,won,win,8.604,765 +93,117,0,2,won,win,9.164,795 +94,118,0,1,won,win,7.250,820 +95,138,0,1,won,win,7.995,770 +96,75,1,2,lost,stalled,10.386,300 +97,85,2,1,lost,stalled,8.284,320 +98,127,1,2,won,win,8.305,785 +99,116,0,1,won,win,8.501,765 +100,69,0,1,lost,stalled,8.094,200 +101,125,2,2,won,win,8.573,790 +102,122,0,1,won,win,7.707,755 +103,108,0,0,won,win,0.264,785 +104,122,0,1,won,win,8.143,735 +105,117,0,1,won,win,8.478,755 +106,116,0,1,won,win,7.688,750 +107,81,1,3,lost,stalled,11.475,420 +108,116,0,1,won,win,9.273,765 +109,116,0,1,won,win,7.364,765 +110,102,0,3,lost,stalled,10.660,465 +111,76,2,1,lost,stalled,7.633,205 +112,116,0,1,won,win,9.267,760 +113,119,0,1,won,win,7.790,775 +114,150,0,1,won,win,7.321,805 +115,134,1,2,won,win,9.289,775 +116,115,0,1,won,win,7.255,775 +117,125,0,1,won,win,8.732,805 +118,131,1,1,won,win,9.378,825 +119,113,0,1,won,win,8.160,735 +120,128,0,1,won,win,8.912,835 +121,116,0,0,won,win,2.141,745 +122,84,0,2,lost,stalled,8.393,375 +123,128,0,1,won,win,7.888,800 +124,116,0,1,won,win,7.914,755 +125,140,0,2,won,win,9.709,840 +126,160,0,2,won,win,9.870,865 +127,130,0,1,won,win,7.967,770 +128,100,0,3,lost,stalled,10.196,415 +129,146,2,2,won,win,9.181,860 +130,119,1,1,won,win,9.914,770 +131,129,1,2,won,win,8.977,815 +132,120,0,1,won,win,8.876,760 +133,114,0,0,won,win,1.935,770 +134,97,1,2,lost,stalled,8.020,440 +135,80,1,2,lost,stalled,9.042,330 +136,133,0,1,won,win,7.670,775 +137,136,1,2,won,win,9.989,850 +138,101,0,0,won,win,2.841,750 +139,124,1,1,won,win,9.144,790 +140,112,0,0,won,win,7.934,740 +141,121,0,1,won,win,7.029,730 +142,113,0,3,lost,stalled,10.522,480 +143,138,1,2,won,win,10.718,790 +144,123,0,1,won,win,8.159,735 +145,110,0,1,won,win,7.293,750 +146,64,1,1,lost,stalled,7.741,120 +147,126,0,1,won,win,8.253,785 +148,108,0,1,won,win,7.276,750 +149,126,1,1,won,win,9.180,825 +150,125,1,0,won,win,7.264,790 +151,124,0,1,won,win,7.320,775 +152,134,2,3,won,win,10.336,845 +153,75,0,2,lost,stalled,12.163,235 +154,96,2,2,lost,stalled,10.417,400 +155,123,0,2,won,win,8.949,795 +156,128,0,1,won,win,8.237,820 +157,83,0,2,lost,stalled,11.360,350 +158,61,1,2,lost,stalled,10.302,280 +159,117,1,1,won,win,8.180,750 +160,139,0,1,won,win,9.038,800 +161,135,1,2,won,win,7.860,810 +162,129,2,2,won,win,8.553,790 +163,121,0,1,won,win,8.196,800 +164,126,0,1,won,win,8.096,750 +165,130,4,1,won,win,7.587,770 +166,140,0,3,won,win,9.020,860 +167,129,0,1,won,win,8.265,775 +168,82,1,2,lost,stalled,9.305,340 +169,130,0,1,won,win,9.135,800 +170,123,2,1,won,win,8.855,775 +171,118,0,1,won,win,7.570,795 +172,170,5,4,won,win,10.310,830 +173,115,0,1,won,win,7.644,780 +174,70,1,1,lost,stalled,11.753,175 +175,127,1,1,won,win,7.605,795 +176,127,0,1,won,win,8.674,775 +177,111,0,1,won,win,7.676,730 +178,131,2,1,won,win,10.532,810 +179,132,1,1,won,win,8.690,805 +180,123,0,1,won,win,8.350,805 +181,141,3,2,won,win,9.504,790 +182,100,1,2,lost,stalled,9.878,450 +183,137,0,4,won,win,10.240,820 +184,87,0,2,lost,stalled,10.783,335 +185,106,0,0,won,win,1.366,775 +186,119,0,1,won,win,7.780,785 +187,118,0,1,won,win,7.996,795 +188,134,0,0,won,win,8.446,750 +189,144,1,4,won,win,9.643,835 +190,142,1,2,won,win,8.922,805 +191,139,0,1,won,win,8.427,780 +192,121,0,1,won,win,7.818,785 +193,123,0,1,won,win,7.588,800 +194,104,1,1,won,win,7.213,750 +195,118,1,1,won,win,7.019,750 +196,128,0,1,won,win,8.195,775 +197,126,0,2,won,win,8.536,820 +198,140,0,2,won,win,9.518,790 +199,110,0,0,won,win,1.138,795 +200,113,0,1,won,win,7.803,775 +201,116,0,1,won,win,7.408,730 +202,127,2,2,won,win,9.394,775 +203,122,0,1,won,win,7.318,795 +204,122,0,1,won,win,8.003,770 +205,90,0,2,lost,stalled,10.695,330 +206,131,0,1,won,win,7.646,770 +207,120,0,0,won,win,3.336,770 +208,163,2,5,won,win,11.449,930 +209,124,0,1,won,win,7.843,805 +210,135,6,2,won,win,9.176,780 +211,143,0,1,won,win,7.929,790 +212,129,0,1,won,win,7.547,780 +213,132,0,2,won,win,11.085,770 +214,98,2,2,lost,stalled,11.151,350 +215,129,0,1,won,win,8.392,770 +216,112,0,1,won,win,7.304,725 +217,124,0,1,won,win,7.968,795 +218,132,0,2,won,win,9.066,780 +219,119,0,1,won,win,9.442,840 +220,109,3,3,lost,stalled,11.373,495 +221,123,2,1,won,win,7.435,770 +222,122,0,0,won,win,1.179,760 +223,121,0,1,won,win,8.342,795 +224,129,0,1,won,win,7.760,765 +225,51,0,1,lost,stalled,7.889,125 +226,115,0,0,won,win,3.139,765 +227,141,3,3,won,win,8.980,820 +228,120,0,0,won,win,4.220,730 +229,108,0,1,won,win,7.893,730 +230,40,1,0,lost,stalled,4.203,35 +231,124,0,0,won,win,4.300,780 +232,138,1,2,won,win,9.224,820 +233,123,0,0,won,win,5.101,760 +234,133,1,1,won,win,7.832,755 +235,86,2,2,lost,stalled,9.503,325 +236,112,0,0,won,win,0.630,735 +237,123,0,1,won,win,6.451,775 +238,126,1,1,won,win,7.149,755 +239,116,0,1,won,win,7.491,760 +240,118,0,0,won,win,2.058,740 +241,116,0,0,won,win,3.762,785 +242,113,0,1,won,win,7.304,755 +243,120,0,1,won,win,7.814,765 +244,95,1,2,lost,stalled,8.694,400 +245,114,0,1,won,win,7.321,740 +246,119,0,1,won,win,7.869,765 +247,120,0,1,won,win,9.047,770 +248,123,0,1,won,win,7.567,785 +249,124,0,1,won,win,7.707,790 +250,103,1,2,lost,stalled,10.096,520 +251,129,0,1,won,win,8.595,770 +252,122,0,1,won,win,8.132,790 +253,112,0,1,won,win,7.836,805 +254,142,2,1,won,win,7.808,765 +255,126,0,1,won,win,7.777,795 +256,122,0,1,won,win,8.627,805 +257,123,0,1,won,win,8.168,775 +258,132,1,1,won,win,7.747,820 +259,120,0,2,won,win,8.887,755 +260,144,1,2,won,win,10.008,825 +261,120,2,3,lost,stalled,9.950,560 +262,153,1,2,won,win,9.645,855 +263,128,0,1,won,win,7.615,775 +264,103,4,4,lost,stalled,9.954,405 +265,114,0,1,won,win,8.342,755 +266,104,0,1,won,win,7.743,765 +267,142,2,1,won,win,8.567,805 +268,134,2,1,won,win,7.510,760 +269,118,0,0,won,win,5.408,745 +270,113,0,1,won,win,8.007,765 +271,59,0,1,lost,stalled,10.345,220 +272,120,1,0,won,win,2.993,745 +273,149,3,2,won,win,10.231,800 +274,111,0,1,won,win,8.838,735 +275,112,0,0,won,win,3.392,770 +276,169,1,4,won,win,12.449,835 +277,107,0,1,won,win,7.601,735 +278,89,2,3,lost,stalled,10.690,335 +279,145,1,2,won,win,9.993,805 +280,104,0,0,won,win,2.445,765 +281,125,0,1,won,win,8.301,795 +282,98,0,2,lost,stalled,8.491,375 +283,125,1,1,won,win,10.964,780 +284,122,0,1,won,win,8.807,795 +285,129,0,1,won,win,7.482,750 +286,111,1,3,lost,stalled,10.399,335 +287,139,0,2,won,win,8.410,835 +288,121,0,0,won,win,2.751,775 +289,97,1,2,lost,stalled,8.539,480 +290,144,1,3,won,win,10.865,830 +291,116,0,1,won,win,8.461,825 +292,126,1,1,won,win,8.520,745 +293,82,2,2,lost,stalled,9.598,220 +294,135,0,1,won,win,9.670,790 +295,126,1,1,won,win,7.822,760 +296,126,0,0,won,win,1.890,805 +297,120,0,1,won,win,7.758,800 +298,117,0,1,won,win,8.558,750 +299,104,0,1,won,win,9.068,765 +300,115,0,1,won,win,8.963,755 +301,142,0,2,won,win,8.365,775 +302,109,0,0,won,win,0.357,790 +303,85,0,4,lost,stalled,11.251,380 +304,65,1,1,lost,stalled,9.103,295 +305,97,0,3,lost,stalled,11.149,400 +306,76,0,1,lost,stalled,8.252,190 +307,102,0,0,won,win,0.068,755 +308,115,1,2,won,win,7.784,760 +309,156,1,1,won,win,8.169,790 +310,114,0,1,won,win,7.919,750 +311,123,0,2,won,win,9.073,795 +312,126,2,1,won,win,7.300,765 +313,113,0,0,won,win,5.952,750 +314,122,0,1,won,win,8.510,795 +315,124,0,1,won,win,8.336,815 +316,68,0,1,lost,stalled,8.560,315 +317,80,0,2,lost,stalled,10.155,355 +318,108,1,1,lost,stalled,8.352,200 +319,140,0,1,won,win,9.144,825 +320,154,2,3,won,win,10.925,845 +321,111,0,1,won,win,7.990,770 +322,145,1,2,won,win,8.380,790 +323,114,0,2,won,win,8.844,815 +324,77,0,1,lost,stalled,9.463,375 +325,113,0,1,won,win,9.231,765 +326,138,0,2,won,win,9.133,785 +327,113,0,0,won,win,4.476,745 +328,84,0,2,lost,stalled,8.719,305 +329,134,2,2,won,win,8.320,775 +330,142,1,2,won,win,9.652,765 +331,123,1,1,won,win,8.144,800 +332,128,0,1,won,win,7.737,765 +333,100,0,1,lost,stalled,8.962,270 +334,106,0,0,won,win,1.637,720 +335,118,0,0,won,win,0.865,740 +336,118,0,1,won,win,7.957,775 +337,82,1,1,lost,stalled,7.628,205 +338,65,0,2,lost,stalled,10.699,320 +339,113,0,1,won,win,7.705,810 +340,117,0,1,won,win,7.782,775 +341,135,1,2,won,win,8.613,765 +342,111,0,0,won,win,6.429,715 +343,116,0,1,won,win,7.633,795 +344,67,0,1,lost,stalled,8.590,235 +345,116,1,1,won,win,7.853,775 +346,69,2,2,lost,stalled,9.799,265 +347,137,0,2,won,win,8.312,770 +348,126,1,1,won,win,7.458,770 +349,119,0,1,won,win,7.396,735 +350,115,0,1,won,win,7.753,820 +351,135,0,1,won,win,8.231,805 +352,153,0,1,won,win,8.793,775 +353,107,0,0,won,win,1.304,780 +354,114,0,1,won,win,7.920,780 +355,127,0,1,won,win,7.680,780 +356,121,0,1,won,win,8.301,785 +357,148,3,2,won,win,8.075,805 +358,113,0,1,won,win,7.485,735 +359,128,0,2,won,win,8.044,830 +360,135,0,1,won,win,7.457,805 +361,139,1,1,won,win,7.353,805 +362,135,0,1,won,win,7.810,780 +363,93,2,2,lost,stalled,10.866,370 +364,88,0,2,lost,stalled,8.057,365 +365,116,0,0,won,win,5.575,760 +366,97,0,2,lost,stalled,8.264,415 +367,117,0,0,won,win,3.669,750 +368,122,0,2,won,win,8.885,760 +369,123,0,1,won,win,8.990,785 +370,134,0,0,won,win,5.424,775 +371,127,0,1,won,win,8.113,785 +372,122,4,3,lost,stalled,10.498,350 +373,121,0,1,won,win,8.529,790 +374,122,1,1,won,win,7.833,790 +375,133,1,4,won,win,10.232,835 +376,128,0,1,won,win,7.859,765 +377,123,0,1,won,win,7.170,770 +378,134,1,1,won,win,9.322,825 +379,136,0,1,won,win,7.996,800 +380,129,0,1,won,win,7.949,760 +381,122,0,1,won,win,8.542,795 +382,122,0,1,won,win,7.796,765 +383,127,0,1,won,win,7.406,790 +384,126,0,2,won,win,9.094,795 +385,108,1,2,lost,stalled,10.459,245 +386,32,0,0,lost,stalled,0.365,50 +387,116,0,1,won,win,7.442,745 +388,107,1,3,lost,stalled,11.292,325 +389,106,0,2,lost,stalled,10.286,395 +390,110,0,0,won,win,3.405,795 +391,129,0,2,won,win,8.171,750 +392,133,0,0,won,win,3.123,750 +393,127,1,1,won,win,8.970,800 +394,69,0,1,lost,stalled,9.050,185 +395,67,0,1,lost,stalled,8.799,165 +396,130,0,1,won,win,8.608,800 +397,139,0,1,won,win,7.502,795 +398,126,0,0,won,win,3.923,770 +399,129,1,2,won,win,8.777,815 +400,104,0,0,won,win,2.933,765 +401,122,0,1,won,win,8.547,765 +402,162,0,0,won,win,5.046,775 +403,118,0,1,won,win,8.015,760 +404,110,0,1,won,win,8.162,765 +405,77,0,1,lost,stalled,11.886,310 +406,128,0,1,won,win,8.037,805 +407,113,0,1,won,win,7.142,750 +408,56,0,1,lost,stalled,10.153,205 +409,133,0,1,won,win,7.610,835 +410,128,1,1,won,win,7.290,740 +411,112,0,1,won,win,8.736,730 +412,120,0,1,won,win,7.434,750 +413,102,0,0,won,win,0.851,755 +414,125,1,1,won,win,8.079,780 +415,107,0,0,won,win,4.295,780 +416,107,0,0,won,win,0.606,780 +417,108,0,1,won,win,7.384,785 +418,141,0,1,won,win,8.190,805 +419,110,0,1,won,win,8.548,795 +420,118,1,1,won,win,7.328,755 +421,126,0,1,won,win,7.467,805 +422,123,0,0,won,win,2.818,780 +423,145,0,1,won,win,7.756,795 +424,134,1,2,won,win,8.986,805 +425,142,4,3,won,win,8.840,855 +426,135,2,2,won,win,8.143,840 +427,116,0,1,won,win,7.887,755 +428,108,0,1,won,win,8.385,740 +429,146,9,3,won,win,9.364,780 +430,130,1,1,won,win,8.101,810 +431,137,1,2,won,win,8.969,795 +432,87,3,3,lost,stalled,10.822,330 +433,118,0,2,won,win,9.304,835 +434,72,0,1,lost,stalled,7.909,170 +435,161,7,2,won,win,10.493,795 +436,121,0,1,won,win,7.666,785 +437,121,0,1,won,win,7.689,780 +438,113,0,1,won,win,8.928,760 +439,118,0,1,won,win,7.710,745 +440,120,0,1,won,win,7.533,755 +441,134,0,1,won,win,9.249,825 +442,71,0,2,lost,stalled,11.349,310 +443,123,1,1,won,win,8.243,795 +444,114,0,1,won,win,7.514,760 +445,101,1,2,lost,stalled,9.747,345 +446,118,0,1,won,win,7.500,755 +447,124,0,1,won,win,7.585,810 +448,125,0,1,won,win,7.967,790 +449,42,1,0,lost,stalled,1.822,95 +450,142,1,2,won,win,8.549,810 +451,176,3,2,won,win,8.811,820 +452,121,0,1,won,win,9.311,815 +453,126,0,2,won,win,8.500,765 +454,129,0,1,won,win,9.383,805 +455,132,0,2,won,win,8.919,840 +456,126,0,1,won,win,8.353,800 +457,60,0,1,lost,stalled,9.336,115 +458,128,0,1,won,win,7.395,755 +459,55,0,1,lost,stalled,7.885,265 +460,86,0,1,lost,stalled,7.953,275 +461,140,0,2,won,win,8.716,770 +462,121,1,2,won,win,9.268,775 +463,101,0,0,won,win,1.725,750 +464,124,0,1,won,win,8.375,740 +465,115,1,0,won,win,1.041,735 +466,140,0,3,won,win,9.552,835 +467,115,0,0,won,win,0.703,820 +468,124,1,2,won,win,9.103,780 +469,108,2,3,lost,stalled,14.028,405 +470,146,0,0,won,win,2.868,760 +471,129,0,2,won,win,8.466,770 +472,131,0,3,won,win,9.766,850 +473,120,0,1,won,win,8.333,760 +474,111,0,1,won,win,7.988,735 +475,160,0,2,won,win,10.476,820 +476,114,0,1,won,win,8.235,735 +477,127,1,1,won,win,8.804,775 +478,128,0,1,won,win,8.389,770 +479,98,1,3,lost,stalled,9.110,400 +480,132,0,1,won,win,8.649,790 +481,68,0,2,lost,stalled,10.803,230 +482,121,0,1,won,win,7.723,770 +483,118,0,1,won,win,9.395,775 +484,114,0,1,won,win,7.196,735 +485,146,2,2,won,win,10.061,730 +486,122,0,1,won,win,9.190,775 +487,116,0,1,won,win,8.457,775 +488,116,0,1,won,win,8.053,760 +489,113,0,1,won,win,7.328,810 +490,122,0,1,won,win,7.522,770 +491,111,0,0,won,win,5.314,740 +492,113,0,1,won,win,8.231,810 +493,130,0,1,won,win,8.553,815 +494,45,1,1,lost,stalled,6.913,120 +495,124,0,1,won,win,8.779,810 +496,121,0,1,won,win,8.793,775 +497,116,1,1,won,win,7.944,770 +498,106,0,1,won,win,7.647,775 +499,134,2,2,won,win,9.877,830 +500,105,0,0,won,win,4.823,770 diff --git a/solver-core-rs/src/klondike/solver.rs b/solver-core-rs/src/klondike/solver.rs index 7f85bf0..dfb5abe 100644 --- a/solver-core-rs/src/klondike/solver.rs +++ b/solver-core-rs/src/klondike/solver.rs @@ -208,6 +208,7 @@ impl KlondikeSolver { current_board.compute_signature(); let eval = BoardEval::from_board(¤t_board, self.draw_advance); let mut context = IdaContext::new(start, timeout, eval.progress_score(), self.draw_advance); + context.allow_partial = allow_partial; loop { if context.timeout_reached() { @@ -412,7 +413,8 @@ impl KlondikeSolver { } /// Finish a valid, fully visible tableau without stock or waste by legal - /// foundation moves. Return the exact required bound before building a tail. + /// foundation moves. Bound the tail by the depth limit when partial results + /// are allowed, retaining its final progress even between sampling points. fn try_finish_visible_without_stock( &self, board: &KlondikeBoard, @@ -439,18 +441,27 @@ impl KlondikeSolver { .unwrap_or(usize::MAX); let remaining = 52usize.saturating_sub(board.total_foundation_count()); let finish_depth = depth.saturating_add(remaining); - if finish_depth > max_depth { + if finish_depth > max_depth && !context.allow_partial { return Some(IdaSearchResult::NextBound(i64::MAX)); } - if finish_depth as i64 > bound { - return Some(IdaSearchResult::NextBound(finish_depth as i64)); + let target_depth = finish_depth.min(max_depth); + if target_depth as i64 > bound { + return Some(IdaSearchResult::NextBound(target_depth as i64)); } let mut current = *board; + let mut current_depth = depth; let mut moves = prefix.to_vec(); - while !Self::is_win(¤t) { + let result = loop { + if Self::is_win(¤t) { + context.solution_path = Some(moves); + return Some(IdaSearchResult::Found); + } + if current_depth >= max_depth { + break IdaSearchResult::NextBound(i64::MAX); + } if context.timeout_reached() { - return Some(IdaSearchResult::Timeout); + break IdaSearchResult::Timeout; } let next_move = (0..7).find_map(|source| { let card = current.columns[source].top_face_up()?; @@ -462,26 +473,29 @@ impl KlondikeSolver { }) }); let Some(next_move) = next_move else { - return Some(IdaSearchResult::NextBound(i64::MAX)); + break IdaSearchResult::NextBound(i64::MAX); }; let Some(next_board) = next_move.apply(current) else { - return Some(IdaSearchResult::NextBound(i64::MAX)); + break IdaSearchResult::NextBound(i64::MAX); }; current = next_board; + current_depth += 1; moves.push(next_move); context.consider_progress(¤t, &moves); // Same accounting order as recursion: charge the primitive child, // check budgets, then recognize a win (including on the last move). if context.should_timeout() { - return Some(IdaSearchResult::Timeout); + break IdaSearchResult::Timeout; } if context.should_checkpoint() { - return Some(IdaSearchResult::CheckpointTriggered); + break IdaSearchResult::CheckpointTriggered; } + }; + if context.allow_partial && !moves.is_empty() { + context.record_progress(¤t, &moves); } - context.solution_path = Some(moves); - Some(IdaSearchResult::Found) + Some(result) } // ── Main IDA* recursion ───────────── @@ -647,6 +661,7 @@ pub struct IdaContext { timeout: Option, checked_nodes: u64, progress_sample_counter: u64, + allow_partial: bool, pub best_progress: i64, pub best_progress_path: Vec, pub solution_path: Option>, @@ -670,6 +685,7 @@ impl IdaContext { timeout, checked_nodes: 0, progress_sample_counter: 0, + allow_partial: false, checkpoint_nodes: 0, checkpoint_node_limit: weights::CHECKPOINT_FALLBACK_LIMIT, best_progress: initial_progress, @@ -723,7 +739,11 @@ impl IdaContext { if self.progress_sample_counter & 3 != 0 { return; } + self.record_progress(board, path); + } + // Terminal partial paths must not depend on the four-node sampling phase. + fn record_progress(&mut self, board: &KlondikeBoard, path: &[KlondikeMove]) { let score = BoardEval::from_board(board, self.draw_advance).progress_score(); if score > self.best_progress { diff --git a/solver-core-rs/src/klondike/solver_optimizations_tests.rs b/solver-core-rs/src/klondike/solver_optimizations_tests.rs index ba9f00d..00c6cbb 100644 --- a/solver-core-rs/src/klondike/solver_optimizations_tests.rs +++ b/solver-core-rs/src/klondike/solver_optimizations_tests.rs @@ -110,6 +110,135 @@ fn visible_closure_returns_exact_bound_and_obeys_the_smallest_depth_limit() { assert_eq!(ctx.checked_nodes, 0); } +#[test] +fn visible_closure_preserves_partial_progress_at_the_depth_limit() { + let board = visible_endgame(); + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + for max_depth in [0, 1, 3, 7, 8, 11, 12] { + let mut solver = KlondikeSolver::new_with_mode(draw, mode); + solver.rules.push(KlondikeRule::DepthLimit { max_depth }); + let stats = solver.solve_with_stats(&board, true); + assert_eq!(stats.checkpoints_adopted, 0); + if max_depth == 0 { + assert!(stats.moves.is_none()); + } else { + let moves = stats.moves.expect("legal progress within the depth limit"); + assert_eq!(moves.len(), max_depth, "draw={draw}, mode={mode:?}"); + let after = replay(board, &moves, draw as u8); + assert_eq!(after.total_foundation_count(), 40 + max_depth); + assert_eq!(KlondikeSolver::is_win(&after), max_depth == 12); + } + if max_depth < 12 { + assert!(solver.solve(&board, false).is_none()); + } + } + } + } +} + +#[test] +fn visible_partial_closure_keeps_the_prefix_and_respects_bound_and_sampling() { + let initial = visible_endgame(); + let first = KlondikeMove::ColumnToFoundation { + source: 0, + card: Card::new(Suit::Club, 11), + }; + let after = first.apply(initial).unwrap(); + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + for max_depth in [7, 8] { + let mut solver = KlondikeSolver::new_with_mode(draw, mode); + solver.rules.push(KlondikeRule::DepthLimit { max_depth }); + for phase in 0..4 { + let mut ctx = context(draw as u8); + ctx.allow_partial = true; + ctx.progress_sample_counter = phase; + assert_eq!( + solver.try_finish_visible_without_stock( + &after, + 1, + max_depth as i64 - 1, + &[first], + &mut ctx, + ), + Some(IdaSearchResult::NextBound(max_depth as i64)) + ); + assert_eq!(ctx.checked_nodes, 0); + assert_eq!(ctx.progress_sample_counter, phase); + assert!(ctx.best_progress_path.is_empty()); + assert_eq!( + solver.try_finish_visible_without_stock( + &after, + 1, + max_depth as i64, + &[first], + &mut ctx, + ), + Some(IdaSearchResult::NextBound(i64::MAX)) + ); + assert!(ctx.solution_path.is_none()); + assert_eq!(ctx.best_progress_path.len(), max_depth); + assert_eq!(ctx.best_progress_path[0], first); + assert_eq!(ctx.checked_nodes, (max_depth - 1) as u64); + assert_eq!(ctx.checkpoint_nodes, ctx.checked_nodes); + assert_eq!(ctx.progress_sample_counter, phase + ctx.checked_nodes); + let end = replay(initial, &ctx.best_progress_path, draw as u8); + assert_eq!(end.total_foundation_count(), 40 + max_depth); + assert_eq!( + ctx.best_progress, + BoardEval::from_board(&end, draw as u8).progress_score() + ); + } + } + } + } +} + +#[test] +fn visible_partial_closure_obeys_node_and_time_budgets() { + let board = visible_endgame(); + for draw in [1, 3] { + for mode in [KlondikeSolveMode::Fast, KlondikeSolveMode::Strict] { + let mut solver = KlondikeSolver::new_with_mode(draw, mode); + solver.rules.push(KlondikeRule::DepthLimit { max_depth: 8 }); + for limit in [1, 3, 7, 8] { + let mut ctx = context(draw as u8); + ctx.allow_partial = true; + ctx.checkpoint_node_limit = limit; + assert_eq!( + solver.try_finish_visible_without_stock(&board, 0, 8, &[], &mut ctx), + Some(IdaSearchResult::CheckpointTriggered) + ); + assert_eq!(ctx.checked_nodes, limit); + assert_eq!(ctx.checkpoint_nodes, limit); + assert_eq!(ctx.progress_sample_counter, limit); + assert!(ctx.solution_path.is_none()); + assert_eq!(ctx.best_progress_path.len(), limit as usize); + assert_eq!( + replay(board, &ctx.best_progress_path, draw as u8).total_foundation_count(), + 40 + limit as usize + ); + } + let mut expired = IdaContext::new( + Instant::now() - Duration::from_secs(1), + Some(Duration::ZERO), + 0, + draw as u8, + ); + expired.allow_partial = true; + assert_eq!( + solver.try_finish_visible_without_stock(&board, 0, 8, &[], &mut expired), + Some(IdaSearchResult::Timeout) + ); + assert_eq!(expired.checked_nodes, 0); + assert_eq!(expired.checkpoint_nodes, 0); + assert!(expired.best_progress_path.is_empty()); + assert!(expired.solution_path.is_none()); + } + } +} + #[test] fn visible_closure_checks_budgets_before_accepting_the_last_move_as_a_win() { let board = visible_endgame();