Let's take a look at the important Code-Snippet (which I commented for easier reading purposes):
inline Score operator*(Score s, int i) {
// Multiply the 'Score' value by the integer and store the result in 'result'
Score result = Score(int(s) * i);
// Assert that the endgame and middlegame scores in 'result' are correct
assert(eg_value(result) == (i * eg_value(s)));
assert(mg_value(result) == (i * mg_value(s)));
// Assert that the 'result' value can be correctly divided by 'i' to obtain the original 'Score' value
assert((i == 0) || (result / i) == s);
// Return the 'result' value
return result;
}
In the following, I'll go through how to code works exactly for the Score s
being 0x00001234
and multiplying by an Integer of i = 2
:
- The
Score
value 0x00001234
is multiplied by 2, resulting in the value 0x00002468
. This value is stored in the result
variable.
- The
eg_value()
function is called to extract the endgame score from the result
value. In this case, the endgame score is 0x0024
, which is then multiplied by 2. The result is compared to the expected value of 0x0048
using the assert()
function. If the values do not match, an assertion failure occurs.
- The
mg_value()
function is called to extract the middlegame score from the result
value. In this case, the middlegame score is 0x0068
, which is then multiplied by 2. The result is compared to the expected value of 0x00D0
using the assert()
function. If the values do not match, an assertion failure occurs.
- The
result
value is divided by 2 using the result / i expression. The result of this division should be the original Score
value of 0x00001234
. This result is compared to the expected value using the assert()
function. If the values do not match, an assertion failure occurs.
- If all of the
assert()
checks pass, the result value is returned. In this case, the returned value would be 0x00002468
.
All in all, Stockfish correctly multiplies the Score
value by 2 by shifting the bits representing the middlegame and endgame scores to the left by one position. This effectively multiplies both scores by 2, resulting in the correct result
value!
Example 1: A second more in-depth explanation with the help of each assertion step: In the following let's consider s = make_score(4, 8)
and i = 2
being called with operator*(s, i)
.
First, the result
will be calculated as follows:
Score result = Score(int(s) * i);
// result = Score(int(make_score(4, 8)) * 2);
// result = Score(int(0x00080004) * 2);
// result = Score(0x0010 * 2);
// result = Score(0x0020);
// result = make_score(0, 32);
Next, we will assert()
- as explained above - to prevent e.g. an Overflow:
assert(eg_value(result) == (i * eg_value(s)));
// assert(eg_value(make_score(0, 32)) == (2 * eg_value(make_score(4, 8))));
// assert(32 == (2 * 8));
// assert(true);
assert(mg_value(result) == (i * mg_value(s)));
// assert(mg_value(make_score(0, 32)) == (2 * mg_value(make_score(4, 8))));
// assert(0 == (2 * 4));
// assert(true);
assert((i == 0) || (result / i) == s);
// assert((2 == 0) || (make_score(0, 32) / 2) == make_score(4, 8));
// assert((false) || (make_score(0, 16) == make_score(4, 8)));
// assert(true);
As all of those assert()
statements evaluated to true
, the function will return the result
.
Example 2: As you mentioned in another reply that you struggled with understanding a negative middlegame score and a positive endgame score, here is a visualization of this situation:
Same play as above - e.g. going through the code with annotations to visualize each step (including the needed assertions to verify the code). In this example I just flipped four to being negative: s = make_score(-4, 8)
!
Again, start with calculating result
:
Score result = Score(int(s) * i);
// result = Score(int(make_score(-4, 8)) * 2);
// result = Score(int(0x000800FB) * 2); // special treatment for negative mg value
// result = Score(0x0010 * 2);
// result = Score(0x0020);
// result = make_score(0, 32);
Note that in this case, the middlegame score is negative, so the make_score() function stores the endgame score as -1 instead of the actual value in order to correctly handle negation. This means that the multiplication applied to the underlying integer value of the Score does not affect the endgame score, and only affects the middlegame score, which is stored in the lower 16 bits.
And for the sake of completeness here are the assert()
s:
assert(eg_value(result) == (i * eg_value(s)));
// assert(eg_value(make_score(0, 32)) == (2 * eg_value(make_score(-4, 8))));
// assert(32 == (2 * 8));
// assert(true);
assert(mg_value(result) == (i * mg_value(s)));
// assert(mg_value(make_score(0, 32)) == (2 * mg_value(make_score(-4, 8))));
// assert(0 == (2 * -4));
// assert(true);
assert((i == 0) || (result / i) == s);
// assert((2 == 0) || (make_score(0, 32) / 2) == make_score(-4, 8));
// assert((false) || (make_score(0, 16) == make_score(-4, 8)));
// assert(true);
In order to tackle a mathematical "proof" we have to consider the representation of the Score enum as a single integer value with the lower 16 bits representing the middlegame value and the upper 16 bits representing the endgame value. Let's assume that the original Score value s is represented as an integer with the following binary representation:
s = a[31]a[30]...a[16]b[15]...b[0]
where a[31]a[30]...a[16] is the binary representation of the endgame value, and b[15]...b[0] is the binary representation of the middlegame value.
If we now multiply this value by an integer i, the result will be a new integer with the following binary representation:
s * i = c[31]c[30]...c[16]d[15]...d[0]
where c[31]c[30]...c[16] is the binary representation of the endgame value multiplied by i, and d[15]...d[0] is the binary representation of the middlegame value multiplied by i.
In order to check that the multiplication is correct, the implementation asserts that the eg_value
and mg_value
of the result match the expected values. This can be proven by considering the following:
The eg_value
of the result is calculated by first converting the result to an unsigned integer and then right shifting it by 16 bits. This effectively discards the lower 16 bits of the result and only keeps the upper 16 bits, which are the binary representation of the endgame value multiplied by i.
The mg_value
of the result is calculated by converting the result to an unsigned integer and then discarding the upper 16 bits, which leaves only the lower 16 bits, which are the binary representation of the middlegame value multiplied by i.
Since the eg_value
and mg_value
of the result are calculated in this way, it is guaranteed that they will match the expected values, as long as the multiplication does not overflow the integer representation of the Score enum. This is why the implementation also asserts that the result divided by the original integer is equal to the original Score value, as this is a way to check that the multiplication did not overflow.
Therefore, we can conclude that the operator*
implementation for the Score enum is correct and will always produce the expected result, as long as the multiplication does not overflow the integer representation of the Score.
Let's consider the "Overflow":
The middlegame and endgame values are represented by the lower and upper 16 bits of the Score value, respectively. Therefore, the maximum possible value for the middlegame and endgame values is 2^15 - 1 = 32767
, and the minimum possible value is -32768
.
The multiplication of the middlegame and endgame values by the integer i will not overflow if the result is within the range of -2^31
to 2^31 - 1
, as this is the range of values that can be represented by the Score enum.
Since the maximum possible value for the middlegame and endgame values is 32767
, the maximum possible result of the multiplication is 32767 * i
. Therefore, the multiplication will not overflow if 32767 * i
is within the range of -2^31
to 2^31 - 1
.
We can prove that 32767 * i
will always be within the range of -2^31
to 2^31 - 1
by considering the following cases:
If i > 0
, then 32767 * i
will be within the range of 0
to 2^31 - 1
. This is because the maximum possible value of i is 2^31 - 1
, and therefore 32767 * i
will be at most (2^31 - 1) * (2^31 - 1) = 2^62 - 2^31 + 1
, which is less than 2^31 - 1
.
If i < 0
, then 32767 * i
will be within the range of -2^31
to 0
. This is because the minimum possible value of i is -(2^31 - 1)
, and therefore 32767 * i
will be at least -(2^31 - 1) * (2^31 - 1) = -(2^62 - 2^31 + 1)
, which is greater than -(2^31 - 1)
.
Small addition to your comment:
When the middlegame and endgame values of the Score value are extracted by the mg_value
and eg_value
functions, they are not being multiplied by the integer value. Instead, the functions are simply extracting the lower and upper 16 bits of the Score value, respectively, and then converting them to the corresponding middlegame and endgame values.
In the operator*
implementation, the middlegame and endgame values are multiplied by the integer value before they are passed to the make_score
function. This means that the resulting Score value will have middlegame and endgame values that are the product of the original values and the integer value.
Regarding the case where the endgame value is stored minus one, this does not affect the multiplication of the endgame value by the integer value. The reason is that the endgame value is first converted to an unsigned integer before it is multiplied by the integer value, which effectively removes the minus one stored in the endgame value. Therefore, the endgame value will be multiplied by the integer value in the same way as if it was stored as a regular positive value.
To illustrate this, let's consider an example where the original Score value has a middlegame value of 5 and an endgame value of -6 (stored as -7 in the Score value). If we multiply this value by 2, the result will be as follows:
s = make_score(5, -7)
s * 2 = make_score(5 * 2, (-7 * 2) + 2^16)
= make_score(10, 2^16 - 14)
As we can see, the endgame value of the result is calculated as (-7 * 2) + 2^16
, which is equivalent to (-7 * 2) + 65536
. This is because the endgame value is first converted to an unsigned integer (65529) before it is multiplied by 2, and then the resulting value is added to 2^16 to restore the minus one that was stored in the original endgame value. Therefore, the endgame value of the result is 2^16 - 14, which is the correct value that is the product of the original endgame value and the integer value.
EDIT:
The question in the chat-room was why (eg*2^16+mg)/n=(eg*2^16)/n+mg/n=(eg/n)*2^16+mg/n
doesn't hold for the division then (in comparison to the unified approach for multiplication). You can write it as (eg2^16)/n+mg/n
which yields the same as operator/
does: mg_value(s) / i, eg_value(s) / i
. The rest violates PEMDAS due to the order of multiplication and division (in the first two terms you perform multiplication before division and in the third vice-versa)!
So multiplying endgame by 2^16
and then dividing the result by n
is in this context not allowed and therefore we solved the issue why operator/
calls it with split parameters (not treating it independently <-> independently treatment of multiplication)!