diff --git a/game/ai_percent.cpp b/game/ai_percent.cpp index af414d8..d4e52ab 100644 --- a/game/ai_percent.cpp +++ b/game/ai_percent.cpp @@ -29,29 +29,39 @@ AIPercent::AIPercent(float iPercent) - : m_percent(iPercent) { - // Ensure the decimal value of the percentage is between 0 and 1 - if (m_percent < 0) - m_percent = 0; - if (m_percent > 1) - m_percent = 1; + if (iPercent < 0) + iPercent = 0; + if (iPercent > 1) + iPercent = 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) { - m_results.clear(); + m_results->clear(); Rack rack; getCurrentRack().getRack(rack); - m_results.search(iDic, iBoard, rack, iFirstWord); + m_results->search(iDic, iBoard, rack, iFirstWord); } Move AIPercent::getMove() const { - if (m_results.size() == 0) + if (m_results->size() == 0) { // If there is no result, pass the turn. // XXX: it is forbidden in duplicate mode (even passing is forbidden), @@ -60,25 +70,7 @@ Move AIPercent::getMove() const } else { - // If there are results, apply the algorithm - 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)); + return Move(m_results->get(0)); } } diff --git a/game/ai_percent.h b/game/ai_percent.h index 0d21cbc..ab12f7f 100644 --- a/game/ai_percent.h +++ b/game/ai_percent.h @@ -40,7 +40,7 @@ class AIPercent: public AIPlayer public: /// Constructor, taking the percentage (0.0 <= iPercent <= 1.0) AIPercent(float iPercent); - virtual ~AIPercent() {} + virtual ~AIPercent(); /** * This method does the actual computation. It will be called before any @@ -52,11 +52,8 @@ public: virtual Move getMove() const; private: - /// Percentage used for this player - float m_percent; - /// Container for all the found solutions - Results m_results; + Results *m_results; }; #endif diff --git a/game/game.cpp b/game/game.cpp index 6461d56..6fd908e 100644 --- a/game/game.cpp +++ b/game/game.cpp @@ -331,7 +331,7 @@ PlayedRack Game::helperSetRackRandom(const PlayedRack &iPld, Rack rack; pld.getRack(rack); - Results res; + BestResults res; res.search(getDic(), getBoard(), rack, getHistory().beforeFirstRound()); if (res.size()) { diff --git a/game/results.cpp b/game/results.cpp index 7005315..8264241 100644 --- a/game/results.cpp +++ b/game/results.cpp @@ -19,9 +19,11 @@ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************/ +#include #include #include #include +#include #include "tile.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, - const Rack &iRack, bool iFirstWord) -{ - clear(); - - if (iFirstWord) - { - iBoard.searchFirst(iDic, iRack, *this); - } - else - { - iBoard.search(iDic, iRack, *this); - } - - sortByPoints(); -} - - -void Results::sortByPoints() +void Results::sort() { less_points 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::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; +} + diff --git a/game/results.h b/game/results.h index 123a979..7ca9f8c 100644 --- a/game/results.h +++ b/game/results.h @@ -23,6 +23,7 @@ #define _RESULTS_H_ #include +#include #include "round.h" using namespace std; @@ -33,31 +34,104 @@ class Rack; /** - * This class allows to perform a search on the board for a given rack, - * and it offers accessors to the resulting rounds. - * The rounds are sorted by decreasing number of points, then by alphabetical - * order (case insensitive), then by coordinates, then by alphabetical orderi - * again (case sensitive this time). + * This abstract class defines the interface to perform a search on the board + * for a given rack, and it offers accessors to the resulting rounds. + * Not all the rounds found by the search are necessarily kept, it depends + * on the implementation (see below in the file for the various + * 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 { public: - unsigned int size() const { return m_rounds.size(); } - void clear() { m_rounds.clear(); } + unsigned int size() const { return m_rounds.size(); } const Round & get(unsigned int) const; - /// Perform a search on the board - void search(const Dictionary &iDic, const Board &iBoard, - const Rack &iRack, bool iFirstWord); + /** + * Perform a search on the board. Every time a word is found, + * 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, - // but it should not be part of the public interface - void add(const Round &iRound) { m_rounds.push_back(iRound); } + /** Add a round */ + virtual void add(const Round &iRound) = 0; + + /** Clear the stored rounds, and get ready for a new search */ + virtual void clear() = 0; + +protected: + vector 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: - vector 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 m_scoresCount; + int m_total; + int m_minScore; }; #endif diff --git a/game/settings.cpp b/game/settings.cpp index 97ffac8..d59e672 100644 --- a/game/settings.cpp +++ b/game/settings.cpp @@ -119,6 +119,18 @@ namespace #endif return fileName; } + + + template + 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 ============== // ============== 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 ============== 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 { - 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(tmpConf, *m_conf, "training.search-limit"); + copySetting(tmpConf, *m_conf, "duplicate.solo-players"); + copySetting(tmpConf, *m_conf, "duplicate.solo-value"); + copySetting(tmpConf, *m_conf, "duplicate.reject-invalid"); + copySetting(tmpConf, *m_conf, "freegame.reject-invalid"); } catch (...) { @@ -236,9 +261,11 @@ int Settings::getInt(const string &iName) const } #else // Dummy implementation - if (iName == "duplicate.solo-players") + if (iName == "training.search-limit") + return 100; + else if (iName == "duplicate.solo-players") return 16; - else if (iName == "duplicate.solo-bonus") + else if (iName == "duplicate.solo-value") return 10; return 0; #endif diff --git a/game/training.cpp b/game/training.cpp index 040fe49..ec2bbf2 100644 --- a/game/training.cpp +++ b/game/training.cpp @@ -28,8 +28,10 @@ # define _(String) String #endif +#include "training.h" #include "dic.h" #include "tile.h" +#include "settings.h" #include "rack.h" #include "round.h" #include "move.h" @@ -38,14 +40,13 @@ #include "player_move_cmd.h" #include "player_rack_cmd.h" #include "game_move_cmd.h" -#include "training.h" #include "encoding.h" #include "debug.h" Training::Training(const Dictionary &iDic) - : Game(iDic) + : Game(iDic), m_results(1000) { // Training mode implicitly uses 1 human player Game::addPlayer(new HumanPlayer); @@ -147,6 +148,8 @@ void Training::search() // Search for the current player Rack 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()); } diff --git a/game/training.h b/game/training.h index fc99182..674bce2 100644 --- a/game/training.h +++ b/game/training.h @@ -97,8 +97,8 @@ private: void endTurn(); - /// Search results, with all the possible rounds - Results m_results; + /// Search results, with all the possible rounds up to a predefined limit + LimitResults m_results; /// Round corresponding to the last test play (if any) Round m_testRound; diff --git a/qt/prefs_dialog.cpp b/qt/prefs_dialog.cpp index 546727a..fd30ee2 100644 --- a/qt/prefs_dialog.cpp +++ b/qt/prefs_dialog.cpp @@ -64,7 +64,7 @@ PrefsDialog::PrefsDialog(QWidget *iParent) checkBoxFreeRefuseInvalid->setChecked(Settings::Instance().getBool("freegame.reject-invalid")); // Training settings - + spinBoxTrainSearchLimit->setValue(Settings::Instance().getInt("training.search-limit")); } catch (GameException &e) { @@ -133,7 +133,8 @@ void PrefsDialog::updateSettings() checkBoxFreeRefuseInvalid->isChecked()); // Training settings - + Settings::Instance().setInt("training.search-limit", + spinBoxTrainSearchLimit->value()); } catch (GameException &e) { diff --git a/qt/ui/prefs_dialog.ui b/qt/ui/prefs_dialog.ui index 707714f..679d325 100644 --- a/qt/ui/prefs_dialog.ui +++ b/qt/ui/prefs_dialog.ui @@ -1,6 +1,14 @@ PrefsDialog + + + 0 + 0 + 414 + 526 + + _("Preferences") @@ -194,6 +202,45 @@ + + + + + + _("Search results limit:") + + + + + + + _("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!).") + + + 100000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/test/training_search.input b/test/training_search.input index 39c91c1..41cf32e 100644 --- a/test/training_search.input +++ b/test/training_search.input @@ -1,3 +1,4 @@ +s i training.search-limit 1000 e t QpiNZ?s a t diff --git a/test/training_search.ref b/test/training_search.ref index 7112903..88daf1e 100644 --- a/test/training_search.ref +++ b/test/training_search.ref @@ -1,5 +1,6 @@ Using seed: 0 [?] pour l'aide +commande> s i training.search-limit 1000 commande> e mode entraînement [?] pour l'aide