Add "Skill level functionality
authorMarco Costalba <mcostalba@gmail.com>
Sun, 27 Mar 2011 10:23:29 +0000 (11:23 +0100)
committerMarco Costalba <mcostalba@gmail.com>
Sun, 27 Mar 2011 10:50:22 +0000 (11:50 +0100)
It is now possible to adjust skill level of Stockfish
from 10 (full strength) to 0.

Skill adjustment is done in such a way that is CPU speed and
time control largely independent, at least at low skills. It
means that given a skill we have same play level on a mobile
phone and on a super OCTAL CPU, at 1' per game or at 180'.

At skill 9 strength is that of an average engine, I have used
Crafty 20.14 to tune and we are more or less there. At skill 0
engine is pretty weak but still shows a realistic play.

When skill is not used we don't have any impact on the regular
code.

Idea to use MultiPV is from Heinz van Saanen, implementation and
formulas by me.

No functional change.

Signed-off-by: Marco Costalba <mcostalba@gmail.com>
src/search.cpp
src/ucioption.cpp

index c393b75b5e4a4ba75998b96c85c5b8316ca0d2fd..a2424ddc3ddb05886dee4af9a8c06c64972dddc6 100644 (file)
@@ -243,7 +243,7 @@ namespace {
   RootMoveList Rml;
 
   // MultiPV mode
-  int MultiPV;
+  int MultiPV, UCIMultiPV;
 
   // Time management variables
   int SearchStartTime, MaxNodes, MaxDepth, ExactMaxTime;
@@ -255,6 +255,10 @@ namespace {
   bool UseLogFile;
   std::ofstream LogFile;
 
+  // Skill level adjustment
+  int SkillLevel;
+  RKISS RK;
+
   // Multi-threads manager object
   ThreadsManager ThreadsMgr;
 
@@ -503,11 +507,16 @@ bool think(Position& pos, bool infinite, bool ponder, int time[], int increment[
   PawnEndgameExtension[0]   = Options["Pawn Endgame Extension (non-PV nodes)"].value<Depth>();
   MateThreatExtension[1]    = Options["Mate Threat Extension (PV nodes)"].value<Depth>();
   MateThreatExtension[0]    = Options["Mate Threat Extension (non-PV nodes)"].value<Depth>();
-  MultiPV                   = Options["MultiPV"].value<int>();
+  UCIMultiPV                = Options["MultiPV"].value<int>();
+  SkillLevel                = Options["Skill level"].value<int>();
   UseLogFile                = Options["Use Search Log"].value<bool>();
 
   read_evaluation_uci_options(pos.side_to_move());
 
+  // Do we have to play with skill handicap? In this case enable MultiPV that
+  // we will use behind the scenes to retrieve a set of possible moves.
+  MultiPV = (SkillLevel < 10 ? Max(UCIMultiPV, 4) : UCIMultiPV);
+
   // Set the number of active threads
   ThreadsMgr.read_uci_options();
   init_eval(ThreadsMgr.active_threads());
@@ -687,11 +696,12 @@ namespace {
 
         // Collect info about search result
         bestMove = Rml[0].pv[0];
+        *ponderMove = Rml[0].pv[1];
         bestValues[depth] = value;
         bestMoveChanges[depth] = Rml.bestMoveChanges;
 
         // Send PV line to GUI and to log file
-        for (int i = 0; i < Min(MultiPV, (int)Rml.size()); i++)
+        for (int i = 0; i < Min(UCIMultiPV, (int)Rml.size()); i++)
             cout << Rml[i].pv_info_to_uci(pos, depth, alpha, beta, i) << endl;
 
         if (UseLogFile)
@@ -746,7 +756,47 @@ namespace {
         }
     }
 
-    *ponderMove = Rml[0].pv[1];
+    // When playing with strength handicap choose best move among the MultiPV
+    // set using a statistical rule dependent on SkillLevel.
+    if (SkillLevel < 10)
+    {
+        assert(MultiPV > 1);
+
+        // Rml list is already sorted by pv_score in descending order
+        int s;
+        int max_s = -VALUE_INFINITE;
+        int size = Min(MultiPV, (int)Rml.size());
+        int max = Rml[0].pv_score;
+        int var = Min(max - Rml[size - 1].pv_score, PawnValueMidgame);
+        int wk = 128 - 8 * SkillLevel;
+
+        // PRNG sequence should be non deterministic
+        for (int i = abs(get_system_time() % 50); i > 0; i--)
+            RK.rand<unsigned>();
+
+        // Choose best move. For each move's score we add two terms both dependent
+        // on wk, one deterministic and bigger for weaker moves, and one random,
+        // then we choose the move with the resulting highest score.
+        for (int i = 0; i < size; i++)
+        {
+            s = Rml[i].pv_score;
+
+            // Don't allow crazy blunders even at very low skills
+            if (i > 0 && Rml[i-1].pv_score > s + EasyMoveMargin)
+                break;
+
+            // This is our magical formula
+            s += ((max - s) * wk + var * (RK.rand<unsigned>() % wk)) / 128;
+
+            if (s > max_s)
+            {
+                max_s = s;
+                bestMove = Rml[i].pv[0];
+                *ponderMove = Rml[i].pv[1];
+            }
+        }
+    }
+
     return bestMove;
   }
 
index 487778e5ad590b007dde1156f9a95eef6c09b559..cd7dc7396fe7b3edef283b509bc75ab1d5d2b7c1 100644 (file)
@@ -98,6 +98,7 @@ void init_uci_options() {
   Options["Ponder"] = Option(true);
   Options["OwnBook"] = Option(true);
   Options["MultiPV"] = Option(1, 1, 500);
+  Options["Skill level"] = Option(10, 0, 10);
   Options["Emergency Move Horizon"] = Option(40, 0, 50);
   Options["Emergency Base Time"] = Option(200, 0, 30000);
   Options["Emergency Move Time"] = Option(70, 0, 5000);