-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Standard C++ 17 API #3
base: development
Are you sure you want to change the base?
Conversation
Add MalomAPI_cpp following C++17 standards with no external dependencies. Examples of API usage can be found in perfect_test.cpp file. sec_val_path can be passed via argv[1].
Thank you very much for the PR! We will be able to review it a bit later, because we are a bit busy at the moment. |
Thank you for letting me know! I understand that you're busy. If it's possible, could the review be completed by November 9th? I'm planning to release a new version of Mill app by New Year's Day 2024. I appreciate your time and effort. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are sorry for not getting back to you sooner! We have started reviewing the pull request, and have found a few minor issues. Overall it looks good! We'll continue reviewing soon.
double val; | ||
}; | ||
|
||
// const double WRGMInf = 2; // Is this good? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(The WRGM stuff is not needed in the C++ API. This was part of some old experimental code.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WRGMInf: Based on your feedback and upon reviewing the code, I agree that WRGMInf
seems to be part of old experimental code and is not required in the current C++ API. I will proceed to remove it to make the code cleaner and more maintainable.
BTW, Struct MoveValuePair: Regarding the struct MoveValuePair
, it is currently not in use, and I am considering its removal as well. Would you recommend deleting it for the sake of code cleanliness, or might there be potential debugging or development scenarios where it could be useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, you can remove MoveValuePair
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking care of removing WRGM in calcitem/Sanmill@b91a8df. You could also push this commit to this pull request (i.e., to the calcitem:cpp-api
branch).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proceeding with modifications at https://github.com/calcitem/Sanmill/commits/master/src/perfect for ease of testing. Expect to port these changes to calcitem:cpp-api within a month. Your input on these modifications, including the commit changing Hungarian to English, is welcome. Thank you.
|
||
// const double WRGMInf = 2; // Is this good? | ||
|
||
std::mutex evalLock; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is also an evalLock
in perfect_player.cpp
, which seems to be a different variable. We recommend keeping only the other one (which is a global variable), because this lock should be shared between instances of this class. This is because this code calls some other code that is not thread-safe, for example Wrappers::WSector::hash
manipulates loaded_hashes
, which is a static variable. (Note that EvalLock
is Shared
in the original VB code.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for highlighting the locking mechanisms in PerfectPlayer::eval
and Wrappers::WSector::hash
. After reviewing the code, I will remove the evalLock
from PerfectPlayer::eval
and keep the global evalLock
.
However, I'm considering how to handle locking in Wrappers::WSector::hash
. The function manipulates loaded_hashes
, a static variable that necessitates some thread safety.
-
Option 1: Use the same global
evalLock
for both functions. This would ensure thread safety but might introduce performance bottlenecks if these functions are frequently accessed concurrently. -
Option 2: Use separate locks for each function. This would improve performance but could introduce complexity and the risk of deadlocks if not carefully managed.
-
Option 3: Avoid locking altogether in
Wrappers::WSector::hash
. This would be risky unless I can be sure this function is never accessed concurrently, or the logic permits eventual consistency.
Since I'm not fully aware of the logical structure and how these functions might interact in a multi-threaded environment, I would appreciate your input on the most suitable locking strategy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Option 3 seems ok to us, because Wrappers::WSector::hash
is only called from PerfectPlayer::eval
, which has a lock. If you would like to, you can add a comment on Wrappers::WSector::hash
that it should never be called by anything else than PerfectPlayer::eval
.
Alternatively, Option 2 seems also fine. Deadlocks can only occur if there are code paths that acquire two locks in different orders, but this can't happen here, since Wrappers::WSector::hash
is not going to call into PerfectPlayer
, so the two locks would always be called in the order of first evalLock
and then hashLock
. Note that Option 2 has some performance cost, which might be important if you will later want to extract evaluations en masse.
|
||
auto iter = secs.find(id_val); | ||
if (iter == secs.end()) { | ||
throw std::runtime_error("Key not found in secs"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This happens when there is no corresponding database file. However, in other parts of the code, you are printing an error msg that indicates that a database file was not found when catching std::out_of_range
, so you might want to throw that here instead of std::runtime_error
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your code review and suggestions.
You recommended using std::out_of_range
for throwing exceptions when no corresponding database file is found, aiming for consistency with other parts of the code. This is a reasonable suggestion, especially when considering readability and consistency.
However, I believe it is more appropriate to keep the current std::runtime_error
for this specific scenario for the following reasons:
-
Semantic Accuracy:
std::out_of_range
is generally used to indicate that an access was made outside the bounds of a container. In this case, not finding a key is more an issue of database integrity or data availability rather than an "out of range" issue. -
Maintainability and Extensibility: Using
std::runtime_error
allows us to handle this type of exception in the future. For example, we could introduce new exception types as subtypes ofstd::runtime_error
. -
Error Diagnosis:
std::runtime_error
can offer broader context information, which is more useful for unpredictable runtime errors.
For these reasons, I'm inclined to retain std::runtime_error
in this scenario. Thank you again for your thoughtful suggestions, and I'm open to further talks if you have additional points or rebuttals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The important thing to consider here is that you need to throw the same type of exception that you are catching where you want to handle this error. More specifically, MalomSolutionAccess::getBestMove
is catching std::out_of_range
, so the thrown exception should match that catch block.
Also note that PerfectPlayer::getSec
already throws std::out_of_range
in the same situation.
- Semantic Accuracy:
std::out_of_range
is generally used to indicate that an access was made outside the bounds of a container. In this case, not finding a key is more an issue of database integrity or data availability rather than an "out of range" issue.
MalomSolutionAccess::getBestMove
catches the exception thrown here and converts it to an exception that is semantically meaningful to an external caller of the API. This means that the exception thrown here in perfect_player.cpp
is just an internal exception, so it seems ok if it is meaningful only internally to the API.
As you also mentioned, it might make sense to introduce a custom exception for this error, and throw it at perfect_api.cpp:95
. This could make it easier for callers of the API to differentiate this error from other, unpredictable errors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the detailed feedback. I'd like to propose moving away from exception handling in this case and switching to a return-value-based mechanism. Exception handling, while useful for catching errors in a structured manner, can impact performance for several reasons:
-
Code Size: Exception handling can increase the binary size due to the extra code required to implement the try-catch blocks.
-
Stack Unwinding: When an exception is thrown, the stack must be unwound, which can be a computationally expensive operation.
-
Branch Prediction: Modern CPUs optimize for predictable branching. Exception handling can disrupt this, leading to performance penalties.
-
Cache Efficiency: The extra code and data structures involved in exception handling may lead to less efficient cache usage.
-
Debugging Overhead: While not a runtime performance concern, the complexity introduced by exceptions can make debugging more difficult.
Given the performance-sensitive nature of our applications, a return-value-based approach could offer a more efficient way to handle errors. This would make it easier for API callers to handle errors without the overhead associated with exceptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We’d like to avoid switching from exception handling to a return-value-based mechanism, because we believe that the required time investment would not be worth the performance gains. Note that each read from our database involves several random reads from files, which takes time on the order of milliseconds or tens of milliseconds. Compared to these file reads, exception handling has negligible overhead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@calcitem, I find it curious that you previously argued for enabling the future use of new exception subtypes for better maintainability in your previous comment in this thread.
Consider filtering out the irrelevant parts of the output of ChatGPT.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved in calcitem/Sanmill@52cb036. Synchronization with this repository is required.
The mutex evalLock, previously used in the PerfectPlayer class for thread safety, has been removed. The global evalLock will be used instead to handle thread safety across multiple functions, ensuring a unified locking mechanism. Removed WRGMInf variable from the C++ API as it was part of old experimental code and is no longer needed. Related discussion can be found in ggevay/malom#3 (review) Change-Id: Ibf81593e2410184412d61f44728234c1f858a03c
"from the starting position."); | ||
} | ||
|
||
deinitializeIfNeeded(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the performance-sensitive nature of our applications, we'd recommend against calling deinitializeIfNeeded()
in every call of getBestMove
, because in this case the initializeIfNeeded()
call at the beginning of the function would have to actually perform the initialization every time, which would have a non-trivial performance cost.
|
||
if (pp == nullptr) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Rules::cleanup();
line should be after this if
statement to avoid double deletions if deinitializeIfNeeded
is called multiple times. (The "ifNeeded" part suggests that the function is idempotent, i.e., it should be ok to call it redundantly.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in calcitem/Sanmill@c24e536
|
||
void MalomSolutionAccess::setVariantStripped() | ||
{ | ||
// copy-paste from Rules.cpp, but references to Main stripped |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We recommend deleting this comment, as it's not really true for the C++ version. (There is no Rules.cpp
, it's not really a copy-paste in the C++ version, and the other function also doesn't have references to Main in the C++ version.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in calcitem/Sanmill@7ad0cc5
T PerfectPlayer::chooseRandom(const std::vector<T> &l) | ||
{ | ||
std::random_device rd; | ||
std::mt19937 gen(rd()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the performance-sensitive nature of our applications, we'd recommend making both the std::random_device rd
and the std::mt19937 gen
static variables, so that they are created only once. Creating these is expensive, see https://stackoverflow.com/a/72577343
Note that in the original Visual Basic code, the variable for the random generator is static (Shared
in Visual Basic).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've implemented your suggestion to make std::random_device and std::mt19937 static in the chooseRandom function. This change resulted in a performance improvement of approximately 0.02%. Thank you for your valuable input.
(We will be quite busy in the next 2-3 weeks, so we'll continue the review later. Thank you for your patience!) |
Dear Gabor, I am writing to share some observations and seek your guidance regarding a specific aspect of the AI behavior in your application. I've been thoroughly analyzing the code and encountered an interesting scenario that I believe merits your expert opinion. I am referring to the std::vector<AdvancedMove> PerfectPlayer::goodMoves(const GameState &s)
{
return allMaxBy(std::function<Wrappers::gui_eval_elem2(AdvancedMove)>(
[this, &s](AdvancedMove m) { return moveValue(s, m); }),
getMoveList(s),
Wrappers::gui_eval_elem2::min_value(getSec(s)));
} Further investigation led me to the template <typename T, typename K>
std::vector<T> PerfectPlayer::allMaxBy(std::function<K(T)> f,
const std::vector<T> &l, K minValue)
{
std::vector<T> r;
K ma = minValue;
for (auto &m : l) {
K e = f(m);
if (e > ma) {
ma = e;
r.clear();
r.push_back(m);
} else if (e == ma) {
r.push_back(m);
}
}
return r;
} My attention then turned to the bool operator>(const gui_eval_elem2 &b) const
{
return this->compare(b) > 0;
}
int compare(const gui_eval_elem2 &o) const
{
assert(s == o.s);
if (!ignore_DD) {
if (key1 != o.key1)
return key1 < o.key1 ? -1 : 1;
else if (key1 < 0)
return key2 < o.key2 ? -1 : 1;
else if (key1 > 0)
return key2 > o.key2 ? -1 : 1;
else
return 0;
} else {
auto a1 = to_eval_elem2().corr(s ? s->sval : virt_unique_sec_val());
auto a2 = o.to_eval_elem2().corr(o.s ? o.s->sval :
virt_unique_sec_val());
drop_DD(a1);
drop_DD(a2);
if (a1.key1 != a2.key1)
return a1.key1 < a2.key1 ? -1 : 1;
else if (a1.key1 < 0)
return a1.key2 < a2.key2 ? -1 : 1;
else if (a1.key1 > 0)
return a2.key2 < a1.key2 ? -1 : 1;
else
return 0;
}
} Given the intricate nature of the database's internal design, I find myself unable to conclusively determine if the behavior is as expected. To further elucidate the issue, I modified the std::vector<AdvancedMove> PerfectPlayer::goodMoves(const GameState &s)
{
auto moveList = getMoveList(s);
std::cout << "Move list size: " << moveList.size()
<< std::endl;
std::function<Wrappers::gui_eval_elem2(AdvancedMove)> evalFunction =
[this, &s](AdvancedMove m) {
auto value = moveValue(s, m);
std::cout << "Evaluating move from " << m.from << " to " << m.to
<< " with score: " << value.toString()
<< std::endl;
return value;
};
auto bestMoves = allMaxBy(evalFunction, moveList,
Wrappers::gui_eval_elem2::min_value(getSec(s)));
std::cout << "Number of best moves: " << bestMoves.size() << std::endl;
return bestMoves;
} In a test scenario where the white player's first move is at b4, the AI (black player) consistently moves to b2. Interestingly, when using your VB.net GUI application, the AI seems capable of making random moves. The debug output for the moves evaluated is as follows:
From my understanding, I was expecting that the "best moves" would include all moves with a score not equal to Could you kindly provide some insights into whether this behavior aligns with the intended design of the AI? Your expertise would be greatly appreciated in clarifying this matter. My current temporary solution is to modify the following function to look like this: template <typename T, typename K>
std::vector<T> PerfectPlayer::allMaxBy(std::function<K(T)> f,
const std::vector<T> &l, K minValue)
{
std::vector<T> r;
if (gameOptions.getShufflingEnabled()) {
bool foundW = false;
bool foundD = false;
for (auto &m : l) {
K e = f(m);
std::string eStr = e.toString();
if (eStr[0] == 'W') {
if (!foundW) {
r.clear();
foundW = true;
}
r.push_back(m);
} else if (!foundW && eStr[0] != 'L') {
if (!foundD) {
r.clear();
foundD = true;
}
r.push_back(m);
} else if (!foundW && !foundD && eStr[0] == 'L') {
r.push_back(m);
}
}
} else {
K ma = minValue;
for (auto &m : l) {
K e = f(m);
if (e > ma) {
ma = e;
r.clear();
r.push_back(m);
} else if (e == ma) {
r.push_back(m);
}
}
}
return r;
} Thank you very much for your time and assistance. I look forward to your valuable feedback. Merry Christmas! |
Dear Calcitem, The problem is originating from this and similar lines in the translated
In the original code, this line is as follows:
This is different from the translation: .net’s Merry Christmas! |
int GetHashCode() { return (W << 0) | (B << 4) | (WF << 8) | (BF << 12); } | ||
|
||
private: | ||
static int64_t factorial(int n) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, int64_t
won't be large enough for this. nCr(n,r)
calls factorial(n)
, and size()
calls nCr(24 - W, B)
, where W
can be as small as 3. So the factorial function needs to be able to compute factorial(21)
, which is 51090942171709440000
, which doesn't fit in int64_t
, or even uint64_t
, since 2^64 = 18446744073709551616
. (Note that the original code's BigInteger
is an arbitrary-precision integer type in .net.)
You might be able to simply delete these functions (factorial
, nCr
, size
) from the C++ code (and the sector_sizes
variable). If I remember correctly, size()
is used only by the Controller
project, but not the GUI or the API. (I can't easily verify this right now, because I don't have that computer with me where I have my development environment installed.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've confirmed that the API does not use the mentioned functions and variables. They will be removed in the next update.
struct gui_eval_elem2 | ||
{ | ||
private: | ||
// azert cannot be valid, because it cannot contain a count (as asserted by |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"azert cannot be valid, because it cannot contain a count"
-->
"could not be simply val instead of sec_val, because val cannot contain a count"
eval_elem2 to_eval_elem2() const { return eval_elem2 {key1, key2}; } | ||
|
||
public: | ||
// The nezo point of key1 is s. However, if s is null, then |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"nezo point"
-->
"viewpoint"
I would greatly appreciate your insights and guidance on a few aspects that are unclear to me. 1. Code Modifications: I have made the following changes to a function in C++: Before Modification: int compare(const gui_eval_elem2 &o) const
{
assert(s == o.s);
if (!ignore_DD) {
if (key1 != o.key1)
return key1 < o.key1 ? -1 : 1;
else if (key1 < 0)
return key2 < o.key2 ? -1 : 1;
else if (key1 > 0)
return key2 > o.key2 ? -1 : 1;
else
return 0;
} else {
auto a1 = to_eval_elem2().corr(s ? s->sval : virt_unique_sec_val());
auto a2 = o.to_eval_elem2().corr(o.s ? o.s->sval :
virt_unique_sec_val());
drop_DD(a1);
drop_DD(a2);
if (a1.key1 != a2.key1)
return a1.key1 < a2.key1 ? -1 : 1;
else if (a1.key1 < 0)
return a1.key2 < a2.key2 ? -1 : 1;
else if (a1.key1 > 0)
return a2.key2 < a1.key2 ? -1 : 1;
else
return 0;
}
} After Modification: int compare(const gui_eval_elem2 &o) const
{
assert(s == o.s);
if (!ignore_DD) { // What's ignore_DD?
if (key1 != o.key1)
return key1 < o.key1 ? -1 : 1;
else if (key1 < 0)
return key2 == o.key2 ? 0 : (key2 < o.key2 ? -1 : 1);
else if (key1 > 0)
return key2 == o.key2 ? 0 : (key2 > o.key2 ? -1 : 1);
else
return 0;
} else {
auto a1 = to_eval_elem2().corr(s ? s->sval : virt_unique_sec_val());
auto a2 = o.to_eval_elem2().corr(o.s ? o.s->sval :
virt_unique_sec_val());
drop_DD(a1);
drop_DD(a2);
if (a1.key1 != a2.key1)
return a1.key1 < a2.key1 ? -1 : 1;
else if (a1.key1 < 0)
return a1.key2 == a2.key2 ? 0 : (a1.key2 < a2.key2 ? -1 : 1);
else if (a1.key1 > 0)
return a2.key2 == a1.key2 ? 0 : (a2.key2 < a1.key2 ? -1 : 1);
else
return 0;
}
} 2. Evaluation of Moves: When White's first move is b4 and playing as Black, the evaluation results are as follows:
I am particularly puzzled as to why only the scores labeled 3. Interpretation of the Evaluation Results: I have encountered some difficulties in understanding the meaning of
4. Clarification Request on The original text of
Could you please provide a detailed explanation, especially regarding the evaluation results like Thank you very much for your time and assistance. BTW. In the coming days, the Beta channel on Play Store will be updating the Sanmill App. This update will enable users to download and import the perfect database you developed, by following the guide at https://github.com/calcitem/Sanmill/wiki/Perfect-Database. When it's available, I would greatly appreciate it if you could check to see if the behavior meets expectations. Thank you very much for your time. |
If the skill level is greater than 15, then the range of move choices for the perfect database is narrower, aiming to end the game with an equal number of pieces for both players. If the skill level is less than 15, although the AI's move choices also result in a draw or a win, it does not pursue having more remaining pieces than the opponent. The final effect should be based on the conclusions discussed in ggevay/malom#3. Change-Id: I96c93015f9d823cb1a76d4657cf9fffe585ab094
Modified the `chooseRandom` function to declare `std::random_device` and `std::mt19937` as static variables. This change ensures that these objects are initialized only once, reducing the overhead of repeatedly creating them on each function call. This optimization is expected to improve performance by approximately 0.02%. This aligns with practices in ggevay's original Visual Basic code, where the random generator was a static (Shared) variable. Reference: ggevay/malom#3 (comment) Change-Id: Ic3b461caf52f533d4bbd5dc29ada65172d5c4450
…:eval In the PerfectPlayer::eval method, the exception thrown when a key is not found in the 'secs' map has been changed from std::runtime_error to std::out_of_range. This aligns with the handling of similar exceptions elsewhere in the code, providing consistency and clearer error messaging about missing database files. Reference: ggevay/malom#3 (comment) Change-Id: Ic860087ebf2d7e714b58f0c2ad78c05250434152
This commit adjusts the deinitializeIfNeeded method in MalomSolutionAccess to enhance performance and ensure safety. The check for a null pointer (pp == nullptr) is moved to the beginning of the method. This change prevents unnecessary cleanup calls and potential double deletion issues, improving both the performance and robustness of the code. By returning early when pp is null, we avoid redundant cleanup operations and align with the intended idempotent nature of the function. Reference: ggevay/malom#3 (comment) Change-Id: I6b77af22f99ad500217f1a70145144b33c1f20fb
This commit updates the comment preceding the deinitializeIfNeeded() call in the getBestMove function. The revised comment more clearly articulates the performance implications of frequent initialization and deinitialization in a performance-sensitive environment. It emphasizes the need for a more efficient approach to managing initialization cycles, thereby guiding future optimizations and code maintenance efforts. Reference: ggevay/malom#3 (comment) Change-Id: I0f8ed192d5a40f2485d7e7819c42e553c265909f
We recommend deleting this comment, as it's not really true for the C++ version. (There is no Rules.cpp, it's not really a copy-paste in the C++ version, and the other function also doesn't have references to Main in the C++ version.) https: //github.com/ggevay/malom/pull/3#discussion_r1349747293 Change-Id: I7ab924fc96cd67cc3e4b9417510094a5f46d4898
"DD" means "distinguishing draws", which refers to our heuristic way to achieve an ultra-strong solution (as opposed to a strong solution): our retrograde analysis is based on the assumption that there are no turn limits, so a draw is actually a cycle or a combination of cycles in the graph of game states; we differentiate draws based on how many stones each player has in such a cycle. For the meaning of the phrases "ultra-strong solution" and "strong solution", see our website, or our paper:
The
To make the program’s move selection take into account how many steps it takes to win or lose (in the paper, DTW refers to this number of steps), you can simply use our comparison function. The “number of steps it takes to draw” is an ambiguous term; the definition we used in our program is a bit tricky, see our definition in subsection IV-B-2 of our paper. For displaying the DTW to your users, you could look at (If the The code in the commit calcitem/Sanmill@b906d31 has a bug in the
The second one should be negated. (Note that the corresponding code in your GitHub comment is correct.) Note that the commit message of calcitem/Sanmill@b906d31 seems to suggest that the ultra-strong solution strives to win by as large stone difference as possible, but this is not the case. The ultra-strong solution differs from the strong solution only for draw positions. For wins and losses, the strong solution makes the same moves as the ultra-strong solution.
Yes, this is correct for the considered state, because stone advantage cannot be achieved in that state. |
Morabaraba occasional drawsCalcitem:
GG:
Regarding the issue of occasional draws in self-play mode, I have successfully replicated the issue, and the details are as follows: Reproduction steps: Configure as follows: General Settings:
Rule Settings:
Switch to AI Vs AI mode and start a self-play game. The game resulted in a draw:
A simpler way to reproduce the draw is: Copy the following content to the clipboard.
Then click: Game -> Import game Click "Move -> Move now" We will see a draw occur. In https://github.com/calcitem/Sanmill/blob/master/src/perfect/perfect_common.h, we can see on Line 44: #define DD // distinguish draws (ultra) //comment or uncomment With the DD switch turned on, it should recognize a draw. Open Settings -> AI -> Ignore draw distinguishing info in the databases: OFF Players -> First: Human Players -> Second: Human It is found that in the 12th round, the move returned by Perfect Database is This indicates there might be a bug in the code adaptation, but I am unsure which part of the code should be checked first or how to debug it. Could you please provide some guidance? Thank you! |
Add MalomAPI_cpp following C++17 standards with no external dependencies.
Examples of API usage can be found in
perfect_test.cpp
file.sec_val_path can be passed via argv[1].