Sorting the search results happens to be quite slow, often much slower than the search itself.

But we don't need to sort them all the time, and in general we don't even need to keep all the rounds.

This commit greatly improves the search performance by filtering the results in 3 different ways, depending on the context:
 - A limit to the number of results can be given (useful for the training mode). The kept results are the best ones, not the first ones found by the search.
 - When only the best round is needed (when the AI is playing with level 100, or when preparing the rack for an explosive game), we don't need to keep rounds with a lower score
 - When the AI has a level lower than 100, it is still possible to skip many rounds

The search limit in training mode is configurable (defaulting to 100) and can be deactivated.
This commit is contained in:
Olivier Teulière 2009-01-22 18:30:22 +00:00
parent 771ba0c35e
commit 87e1d4795b
12 changed files with 399 additions and 77 deletions

View file

@ -29,29 +29,39 @@
AIPercent::AIPercent(float iPercent) AIPercent::AIPercent(float iPercent)
: m_percent(iPercent)
{ {
// Ensure the decimal value of the percentage is between 0 and 1 if (iPercent < 0)
if (m_percent < 0) iPercent = 0;
m_percent = 0; if (iPercent > 1)
if (m_percent > 1) iPercent = 1;
m_percent = 1;
// Use BestResults to be slightly faster when the percentage is 100%
if (iPercent == 1)
m_results = new BestResults;
else
m_results = new PercentResults(iPercent);
}
AIPercent::~AIPercent()
{
delete m_results;
} }
void AIPercent::compute(const Dictionary &iDic, const Board &iBoard, bool iFirstWord) void AIPercent::compute(const Dictionary &iDic, const Board &iBoard, bool iFirstWord)
{ {
m_results.clear(); m_results->clear();
Rack rack; Rack rack;
getCurrentRack().getRack(rack); getCurrentRack().getRack(rack);
m_results.search(iDic, iBoard, rack, iFirstWord); m_results->search(iDic, iBoard, rack, iFirstWord);
} }
Move AIPercent::getMove() const Move AIPercent::getMove() const
{ {
if (m_results.size() == 0) if (m_results->size() == 0)
{ {
// If there is no result, pass the turn. // If there is no result, pass the turn.
// XXX: it is forbidden in duplicate mode (even passing is forbidden), // XXX: it is forbidden in duplicate mode (even passing is forbidden),
@ -60,25 +70,7 @@ Move AIPercent::getMove() const
} }
else else
{ {
// If there are results, apply the algorithm return Move(m_results->get(0));
double wantedScore = m_percent * m_results.get(0).getPoints();
// Look for the first round giving at least 'wantedScore' points
// Browse the results 10 by 10 (a dichotomy would be better, but this
// is not performance critical)
unsigned int index = 0;
while (index < m_results.size() &&
m_results.get(index).getPoints() > wantedScore)
{
index += 10;
}
// Now the wanted round is in the last 10 indices
if (index >= m_results.size())
index = m_results.size() - 1;
while (m_results.get(index).getPoints() < wantedScore)
{
--index;
}
return Move(m_results.get(index));
} }
} }

View file

@ -40,7 +40,7 @@ class AIPercent: public AIPlayer
public: public:
/// Constructor, taking the percentage (0.0 <= iPercent <= 1.0) /// Constructor, taking the percentage (0.0 <= iPercent <= 1.0)
AIPercent(float iPercent); AIPercent(float iPercent);
virtual ~AIPercent() {} virtual ~AIPercent();
/** /**
* This method does the actual computation. It will be called before any * This method does the actual computation. It will be called before any
@ -52,11 +52,8 @@ public:
virtual Move getMove() const; virtual Move getMove() const;
private: private:
/// Percentage used for this player
float m_percent;
/// Container for all the found solutions /// Container for all the found solutions
Results m_results; Results *m_results;
}; };
#endif #endif

View file

@ -331,7 +331,7 @@ PlayedRack Game::helperSetRackRandom(const PlayedRack &iPld,
Rack rack; Rack rack;
pld.getRack(rack); pld.getRack(rack);
Results res; BestResults res;
res.search(getDic(), getBoard(), rack, getHistory().beforeFirstRound()); res.search(getDic(), getBoard(), rack, getHistory().beforeFirstRound());
if (res.size()) if (res.size())
{ {

View file

@ -19,9 +19,11 @@
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*****************************************************************************/ *****************************************************************************/
#include <boost/foreach.hpp>
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
#include <cwctype> #include <cwctype>
#include <cmath>
#include "tile.h" #include "tile.h"
#include "round.h" #include "round.h"
@ -98,27 +100,204 @@ const Round & Results::get(unsigned int i) const
} }
void Results::search(const Dictionary &iDic, const Board &iBoard, void Results::sort()
const Rack &iRack, bool iFirstWord)
{
clear();
if (iFirstWord)
{
iBoard.searchFirst(iDic, iRack, *this);
}
else
{
iBoard.search(iDic, iRack, *this);
}
sortByPoints();
}
void Results::sortByPoints()
{ {
less_points lp; less_points lp;
std::sort(m_rounds.begin(), m_rounds.end(), lp); std::sort(m_rounds.begin(), m_rounds.end(), lp);
} }
BestResults::BestResults()
: m_bestScore(0)
{
}
void BestResults::search(const Dictionary &iDic, const Board &iBoard,
const Rack &iRack, bool iFirstWord)
{
clear();
if (iFirstWord)
iBoard.searchFirst(iDic, iRack, *this);
else
iBoard.search(iDic, iRack, *this);
sort();
}
void BestResults::add(const Round &iRound)
{
// Ignore too low scores
if (m_bestScore > iRound.getPoints())
return;
if (m_bestScore < iRound.getPoints())
{
// New best score: clear the stored results
m_bestScore = iRound.getPoints();
m_rounds.clear();
}
m_rounds.push_back(iRound);
}
void BestResults::clear()
{
m_rounds.clear();
m_bestScore = 0;
}
PercentResults::PercentResults(float iPercent)
: m_percent(iPercent)
{
}
class Predicate
{
public:
Predicate(int iPoints) : m_chosenPoints(iPoints) {}
bool operator()(const Round &iRound) const
{
return iRound.getPoints() != m_chosenPoints;
}
private:
const int m_chosenPoints;
};
void PercentResults::search(const Dictionary &iDic, const Board &iBoard,
const Rack &iRack, bool iFirstWord)
{
clear();
if (iFirstWord)
iBoard.searchFirst(iDic, iRack, *this);
else
iBoard.search(iDic, iRack, *this);
if (m_rounds.empty())
return;
// At this point, add() has been called, so the best score is valid
// Find the lowest score at least equal to the min_score
int chosenPoints = m_bestScore;
BOOST_FOREACH(const Round &iRound, m_rounds)
{
int points = iRound.getPoints();
if (points >= m_minScore && points < chosenPoints)
{
chosenPoints = points;
}
}
// Keep only the rounds with the "chosenPoints" score
std::remove_if(m_rounds.begin(), m_rounds.end(), Predicate(chosenPoints));
ASSERT(!m_rounds.empty(), "Bug in PercentResults");
// Sort the remaining rounds
sort();
}
void PercentResults::add(const Round &iRound)
{
// Ignore too low scores
if (m_minScore > iRound.getPoints())
return;
if (m_bestScore < iRound.getPoints())
{
m_bestScore = iRound.getPoints();
m_minScore = (int)ceil(m_bestScore * m_percent);
}
m_rounds.push_back(iRound);
}
void PercentResults::clear()
{
m_rounds.clear();
m_bestScore = 0;
m_minScore = 0;
}
LimitResults::LimitResults(int iLimit)
: m_limit(iLimit), m_total(0), m_minScore(-1)
{
}
void LimitResults::search(const Dictionary &iDic, const Board &iBoard,
const Rack &iRack, bool iFirstWord)
{
clear();
if (iFirstWord)
iBoard.searchFirst(iDic, iRack, *this);
else
iBoard.search(iDic, iRack, *this);
if (m_rounds.empty())
return;
// Sort the rounds
sort();
// Truncate the results to respect the limit
if (m_limit != 0 && m_rounds.size() > (unsigned int) m_limit)
m_rounds.resize(m_limit);
}
void LimitResults::add(const Round &iRound)
{
// If we ignore the limit, simply add the round
if (m_limit == 0)
{
m_rounds.push_back(iRound);
return;
}
// Ignore too low scores
if (m_minScore >= iRound.getPoints())
return;
// Add the round
m_rounds.push_back(iRound);
++m_total;
++m_scoresCount[iRound.getPoints()];
// Can we increase the minimum score required?
if (m_total - m_scoresCount[m_minScore] >= m_limit)
{
// Yes! "Forget" the rounds of score m_minScore
// They are still present in m_rounds, but they will be removed
// for real later in the search() method
m_total -= m_scoresCount[m_minScore];
m_scoresCount.erase(m_minScore);
// Find the new min score
map<int, int>::const_iterator it =
m_scoresCount.lower_bound(m_minScore);
ASSERT(it != m_scoresCount.end(), "Bug in LimitResults::add())");
m_minScore = it->first;
}
}
void LimitResults::clear()
{
m_rounds.clear();
m_scoresCount.clear();
m_minScore = -1;
m_total = 0;
}

View file

@ -23,6 +23,7 @@
#define _RESULTS_H_ #define _RESULTS_H_
#include <vector> #include <vector>
#include <map>
#include "round.h" #include "round.h"
using namespace std; using namespace std;
@ -33,31 +34,104 @@ class Rack;
/** /**
* This class allows to perform a search on the board for a given rack, * This abstract class defines the interface to perform a search on the board
* and it offers accessors to the resulting rounds. * for a given rack, and it offers accessors to the resulting rounds.
* The rounds are sorted by decreasing number of points, then by alphabetical * Not all the rounds found by the search are necessarily kept, it depends
* order (case insensitive), then by coordinates, then by alphabetical orderi * on the implementation (see below in the file for the various
* again (case sensitive this time). * implementations).
*
* After the search, the rounds are sorted by decreasing number of points,
* then by alphabetical order (case insensitive), then by coordinates,
* then by alphabetical order again (case sensitive this time).
*/ */
class Results class Results
{ {
public: public:
unsigned int size() const { return m_rounds.size(); } unsigned int size() const { return m_rounds.size(); }
void clear() { m_rounds.clear(); }
const Round & get(unsigned int) const; const Round & get(unsigned int) const;
/// Perform a search on the board /**
void search(const Dictionary &iDic, const Board &iBoard, * Perform a search on the board. Every time a word is found,
const Rack &iRack, bool iFirstWord); * the add() method will be called. At the end of the search,
* results are sorted.
*/
virtual void search(const Dictionary &iDic, const Board &iBoard,
const Rack &iRack, bool iFirstWord) = 0;
// FIXME: This method is used to fill the container with the rounds, /** Add a round */
// but it should not be part of the public interface virtual void add(const Round &iRound) = 0;
void add(const Round &iRound) { m_rounds.push_back(iRound); }
/** Clear the stored rounds, and get ready for a new search */
virtual void clear() = 0;
protected:
vector<Round> m_rounds;
void sort();
};
/**
* This implementation keeps only the rounds corresponding to the best score.
* If there are several rounds with the same score, they are all kept.
* All other rounds are ignored.
*/
class BestResults: public Results
{
public:
BestResults();
virtual void search(const Dictionary &iDic, const Board &iBoard,
const Rack &iRack, bool iFirstWord);
virtual void clear();
virtual void add(const Round &iRound);
private: private:
vector<Round> m_rounds; int m_bestScore;
};
void sortByPoints(); /**
* This implementation finds the best score possible, and keeps only
* the rounds whose score is closest to (but not lower than) the given
* percentage of the best score.
* All the rounds with this closest score are kept, rounds with a different
* score are ignored.
*/
class PercentResults: public Results
{
public:
/** The percentage is given as a float between 0 (0%) and 1 (100%) */
PercentResults(float iPercent);
virtual void search(const Dictionary &iDic, const Board &iBoard,
const Rack &iRack, bool iFirstWord);
virtual void clear();
virtual void add(const Round &iRound);
private:
const float m_percent;
int m_bestScore;
int m_minScore;
};
/**
* This implementation keeps the N best rounds, N being the given limit.
* All other rounds are ignored.
* In the special case where the limit is 0, all rounds are kept (but you can
* expect the sorting of the rounds to be much slower...)
*/
class LimitResults: public Results
{
public:
LimitResults(int iLimit);
virtual void search(const Dictionary &iDic, const Board &iBoard,
const Rack &iRack, bool iFirstWord);
virtual void clear();
virtual void add(const Round &iRound);
void setLimit(int iNewLimit) { m_limit = iNewLimit; }
private:
int m_limit;
map<int, int> m_scoresCount;
int m_total;
int m_minScore;
}; };
#endif #endif

View file

@ -119,6 +119,18 @@ namespace
#endif #endif
return fileName; return fileName;
} }
template<typename T>
void copySetting(const Config &srcConf, Config &dstConf, const char *path)
{
if (srcConf.exists(path))
{
T t;
srcConf.lookupValue(path, t);
dstConf.lookup(path) = t;
}
}
} }
@ -131,6 +143,10 @@ Settings::Settings()
// ============== General options ============== // ============== General options ==============
// ============== Training mode options ============== // ============== Training mode options ==============
Setting &training = m_conf->getRoot().add("training", Setting::TypeGroup);
// Number of search results kept in a search
training.add("search-limit", Setting::TypeInt) = 100;
// ============== Duplicate mode options ============== // ============== Duplicate mode options ==============
Setting &dupli = m_conf->getRoot().add("duplicate", Setting::TypeGroup); Setting &dupli = m_conf->getRoot().add("duplicate", Setting::TypeGroup);
@ -159,7 +175,16 @@ Settings::Settings()
// Try to read the values from the configuration file // Try to read the values from the configuration file
try try
{ {
m_conf->readFile(m_fileName.c_str()); // We cannot call readFile() on m_conf, as it removes the previous
// settings. So we create a temporary config, and copy the settings
// one by one...
Config tmpConf;
tmpConf.readFile(m_fileName.c_str());
copySetting<int>(tmpConf, *m_conf, "training.search-limit");
copySetting<int>(tmpConf, *m_conf, "duplicate.solo-players");
copySetting<int>(tmpConf, *m_conf, "duplicate.solo-value");
copySetting<bool>(tmpConf, *m_conf, "duplicate.reject-invalid");
copySetting<bool>(tmpConf, *m_conf, "freegame.reject-invalid");
} }
catch (...) catch (...)
{ {
@ -236,9 +261,11 @@ int Settings::getInt(const string &iName) const
} }
#else #else
// Dummy implementation // Dummy implementation
if (iName == "duplicate.solo-players") if (iName == "training.search-limit")
return 100;
else if (iName == "duplicate.solo-players")
return 16; return 16;
else if (iName == "duplicate.solo-bonus") else if (iName == "duplicate.solo-value")
return 10; return 10;
return 0; return 0;
#endif #endif

View file

@ -28,8 +28,10 @@
# define _(String) String # define _(String) String
#endif #endif
#include "training.h"
#include "dic.h" #include "dic.h"
#include "tile.h" #include "tile.h"
#include "settings.h"
#include "rack.h" #include "rack.h"
#include "round.h" #include "round.h"
#include "move.h" #include "move.h"
@ -38,14 +40,13 @@
#include "player_move_cmd.h" #include "player_move_cmd.h"
#include "player_rack_cmd.h" #include "player_rack_cmd.h"
#include "game_move_cmd.h" #include "game_move_cmd.h"
#include "training.h"
#include "encoding.h" #include "encoding.h"
#include "debug.h" #include "debug.h"
Training::Training(const Dictionary &iDic) Training::Training(const Dictionary &iDic)
: Game(iDic) : Game(iDic), m_results(1000)
{ {
// Training mode implicitly uses 1 human player // Training mode implicitly uses 1 human player
Game::addPlayer(new HumanPlayer); Game::addPlayer(new HumanPlayer);
@ -147,6 +148,8 @@ void Training::search()
// Search for the current player // Search for the current player
Rack r; Rack r;
m_players[m_currPlayer]->getCurrentRack().getRack(r); m_players[m_currPlayer]->getCurrentRack().getRack(r);
int limit = Settings::Instance().getInt("training.search-limit");
m_results.setLimit(limit);
m_results.search(getDic(), getBoard(), r, getHistory().beforeFirstRound()); m_results.search(getDic(), getBoard(), r, getHistory().beforeFirstRound());
} }

View file

@ -97,8 +97,8 @@ private:
void endTurn(); void endTurn();
/// Search results, with all the possible rounds /// Search results, with all the possible rounds up to a predefined limit
Results m_results; LimitResults m_results;
/// Round corresponding to the last test play (if any) /// Round corresponding to the last test play (if any)
Round m_testRound; Round m_testRound;

View file

@ -64,7 +64,7 @@ PrefsDialog::PrefsDialog(QWidget *iParent)
checkBoxFreeRefuseInvalid->setChecked(Settings::Instance().getBool("freegame.reject-invalid")); checkBoxFreeRefuseInvalid->setChecked(Settings::Instance().getBool("freegame.reject-invalid"));
// Training settings // Training settings
spinBoxTrainSearchLimit->setValue(Settings::Instance().getInt("training.search-limit"));
} }
catch (GameException &e) catch (GameException &e)
{ {
@ -133,7 +133,8 @@ void PrefsDialog::updateSettings()
checkBoxFreeRefuseInvalid->isChecked()); checkBoxFreeRefuseInvalid->isChecked());
// Training settings // Training settings
Settings::Instance().setInt("training.search-limit",
spinBoxTrainSearchLimit->value());
} }
catch (GameException &e) catch (GameException &e)
{ {

View file

@ -1,6 +1,14 @@
<ui version="4.0" > <ui version="4.0" >
<class>PrefsDialog</class> <class>PrefsDialog</class>
<widget class="QDialog" name="PrefsDialog" > <widget class="QDialog" name="PrefsDialog" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>414</width>
<height>526</height>
</rect>
</property>
<property name="windowTitle" > <property name="windowTitle" >
<string>_("Preferences")</string> <string>_("Preferences")</string>
</property> </property>
@ -194,6 +202,45 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<widget class="QLabel" name="label" >
<property name="text" >
<string>_("Search results limit:")</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBoxTrainSearchLimit" >
<property name="toolTip" >
<string>_("Maximum number of results returned by a search. The returned
results will always be the best ones. Use 0 to disable the limit (warning:
searches yielding many results can be extremely slow in this case!).")</string>
</property>
<property name="maximum" >
<number>100000</number>
</property>
<property name="value" >
<number>100</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0" >
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View file

@ -1,3 +1,4 @@
s i training.search-limit 1000
e e
t QpiNZ?s t QpiNZ?s
a t a t

View file

@ -1,5 +1,6 @@
Using seed: 0 Using seed: 0
[?] pour l'aide [?] pour l'aide
commande> s i training.search-limit 1000
commande> e commande> e
mode entraînement mode entraînement
[?] pour l'aide [?] pour l'aide