mirror of
git://git.savannah.nongnu.org/eliot.git
synced 2025-01-17 06:11:49 +01:00
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:
parent
771ba0c35e
commit
87e1d4795b
12 changed files with 399 additions and 77 deletions
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
217
game/results.cpp
217
game/results.cpp
|
@ -19,9 +19,11 @@
|
|||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*****************************************************************************/
|
||||
|
||||
#include <boost/foreach.hpp>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <cwctype>
|
||||
#include <cmath>
|
||||
|
||||
#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<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;
|
||||
}
|
||||
|
||||
|
|
102
game/results.h
102
game/results.h
|
@ -23,6 +23,7 @@
|
|||
#define _RESULTS_H_
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#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(); }
|
||||
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<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:
|
||||
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
|
||||
|
|
|
@ -119,6 +119,18 @@ namespace
|
|||
#endif
|
||||
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 ==============
|
||||
|
||||
// ============== 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<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 (...)
|
||||
{
|
||||
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
<ui version="4.0" >
|
||||
<class>PrefsDialog</class>
|
||||
<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" >
|
||||
<string>_("Preferences")</string>
|
||||
</property>
|
||||
|
@ -194,6 +202,45 @@
|
|||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
s i training.search-limit 1000
|
||||
e
|
||||
t QpiNZ?s
|
||||
a t
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue