4

The Stockfish chess engine needs to store, for its evaluation, both an endgame score and a middlegame score.

Instead of storing them separately, it packs them into one int. The middlegame score is stored in the lower 16 bits. The endgame score is stored in the higher 16 bits, as-is if the middlegame score is positive or minus one if it is negative.

This has the advantage that operations (addition, subtraction, negation and multiplication) can be done for both numbers in parallel.

Here is the code:

/// Score enum stores a middlegame and an endgame value in a single integer (enum).
/// The least significant 16 bits are used to store the middlegame value and the
/// upper 16 bits are used to store the endgame value. We have to take care to
/// avoid left-shifting a signed int to avoid undefined behavior.
enum Score : int { SCORE_ZERO };

constexpr Score make_score(int mg, int eg) {
  return Score((int)((unsigned int)eg << 16) + mg);
}

/// Extracting the signed lower and upper 16 bits is not so trivial because
/// according to the standard a simple cast to short is implementation defined
/// and so is a right shift of a signed integer.
inline Value eg_value(Score s) {
  union { uint16_t u; int16_t s; } eg = { uint16_t(unsigned(s + 0x8000) >> 16) };
  return Value(eg.s);
}

inline Value mg_value(Score s) {
  union { uint16_t u; int16_t s; } mg = { uint16_t(unsigned(s)) };
  return Value(mg.s);
}

#define ENABLE_BASE_OPERATORS_ON(T)                                \
constexpr T operator+(T d1, int d2) { return T(int(d1) + d2); }    \
constexpr T operator-(T d1, int d2) { return T(int(d1) - d2); }    \
constexpr T operator-(T d) { return T(-int(d)); }                  \
inline T& operator+=(T& d1, int d2) { return d1 = d1 + d2; }       \
inline T& operator-=(T& d1, int d2) { return d1 = d1 - d2; }

ENABLE_BASE_OPERATORS_ON(Score)

/// Only declared but not defined. We don't want to multiply two scores due to
/// a very high risk of overflow. So user should explicitly convert to integer.
Score operator*(Score, Score) = delete;

/// Division of a Score must be handled separately for each term
inline Score operator/(Score s, int i) {
  return make_score(mg_value(s) / i, eg_value(s) / i);
}

/// Multiplication of a Score by an integer. We check for overflow in debug mode.
inline Score operator*(Score s, int i) {

  Score result = Score(int(s) * i);

  assert(eg_value(result) == (i * eg_value(s)));
  assert(mg_value(result) == (i * mg_value(s)));
  assert((i == 0) || (result / i) == s);

  return result;
}

I understand how addition, subtraction and negation work, but what I have trouble understanding is multiplication. How does multiplying the integer multiplies both the endgame and the middlegame scores together correctly?

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
  • I'm not sure it works that way. You'd have to do some bit shuffling in, perhaps, a `uint64_t`? – tadman Nov 22 '22 at 02:33
  • @tadman Well, it is in a pretty popular chess engine, used for a lot of years and functioning well. In addition, it also works in the few cases I tested. – Chayim Friedman Nov 22 '22 at 02:34
  • I just mean mathematically if you multiply them you get the right bits at the ends, but the middle has cross-multiplication between high and low values, which presumably you don't want/need. The code here only seems to describe multiplying by a constant, which I'd expect to see duplicated. – tadman Nov 22 '22 at 02:35
  • All this seems like a really primitive version of what's now done trivially and reliably with SIMD. – tadman Nov 22 '22 at 02:36
  • 3
    It doesn't : `Score operator*(Score, Score) = delete;` – Jeffrey Nov 22 '22 at 02:36
  • @Jeffrey I'm talking about `Score operator*(Score s, int i)`. And I _think_ multiplying two scores also works, just deleted because of a high risk of overflow, but I'm not sure about that. – Chayim Friedman Nov 22 '22 at 02:36
  • 2
    Why wouldn't it work? The encoding is _m + 2^16 e_. If that's multiplied by k, then you have _k*m + 2^16 * (k * e)_. Both _m_ and _e_ are multiplied by _k_ because multiplication distributes over addition. As the assertions say, however, if k is too big, then _k*m_ or _k*e_ can overflow 16 bits, which means the result is invalid. Multiplying two scores will not work. That's easy to show by expanding the multiplication. – Gene Nov 22 '22 at 02:38
  • @Gene But then why division doesn't work if `(m+2^16e)/d=m/d+2^16(e/d)`? – Chayim Friedman Dec 05 '22 at 02:21

4 Answers4

3

The reason that multiplication works here, even for negative middlegame score, is, despite the document says:

The least significant 16 bits are used to store the middlegame value and the upper 16 bits are used to store the endgame value.

This statement isn't entirely true.


As an example, if you have mg = -10, ed = 3, if we simply take the description in the document, we should get something like:

mg =                     | 1111 1111 1111 0110
ed = 0000 0000 0000 0011 | 
-------------------------+--------------------
     0000 0000 0000 0011 | 1111 1111 1111 0110

However, if we look at the logic in make_score:

Score((int)((unsigned int)eg << 16) + mg)

Note that we are actually promoting the number to int, so what we actually get is:

     1111 1111 1111 1111 | 1111 1111 1111 0110
   + 0000 0000 0000 0011 | 0000 0000 0000 0000
-------------------------+--------------------
 (1) 0000 0000 0000 0010 | 1111 1111 1111 0110
                       ^
             this bit is different

Now go back to the math part, what we have is essentially: 3⋅216-10.

Now say we multiply it with 37, we will get (3⋅216⋅37)-(10⋅37), or 111⋅216-370, or in binary:

     0000 0000 0110 1111 | 0000 0000 0000 0000
   - 0000 0000 0000 0000 | 0000 0001 0111 0010
-------------------------+--------------------

Since the lowest 16 bits of 111⋅216 are all 0s, this operation is basically having the higher 16 bits -1, and the lower 16 bits will be the bitwise negation of 370 then +1, or:

     0000 0000 0110 1110 | 1111 1110 1000 1110

In fact, as long (unsigned)(mg⋅k) is less than 216, then doing multiplication in the lower bits will never affect the higher bits.

Ranoiaetep
  • 5,872
  • 1
  • 14
  • 39
  • I'm not looking for cases. I can do it myself. What I'm after is a mathematical proof or an explanation of why this code is correct in the general case, both for positive and negative middlegame score. – Chayim Friedman Dec 05 '22 at 13:27
2

Multiplying two packed integers doesn't work. But multiplying by a constant works, as long as you have enough padding bits.

Imagine it in base ten, it becomes easier:

23 and 31 packed with a thousand factor: 230031

Let's multiply by 3, we would get 69 and 93

And 230031 * 3 is 690093. So, you can do both multiplication at once, as long as the number in the lowest bits doesn't overflow over the highest-bits number.

The same magic happens if you look at your number in hexadecimal. But, to me, it is easier to understand in decimal.

Jeffrey
  • 11,063
  • 1
  • 21
  • 42
  • The positive case is easy. But what when the middlegame score is negative? – Chayim Friedman Nov 22 '22 at 02:42
  • @ChayimFriedman: In two's complement, signed arithmetic is the same as unsigned arithmetic, in terms of the actual operations on the bit-pattern. It's only the interpretation of the result that changes. – Nicol Bolas Nov 22 '22 at 03:50
  • @NicolBolas Yes, this is the case with a single number, but how this is the case with two packed numbers? – Chayim Friedman Dec 05 '22 at 13:27
  • @ChayimFriedman: It is the case because that's how positional numbering systems work. As long as the math operations don't overflow into the upper bits, everything works as normal. Again, two's complement signed math uses the same math operation as unsigned math. – Nicol Bolas Dec 05 '22 at 14:45
  • @NicolBolas Can you elaborate more? – Chayim Friedman Dec 05 '22 at 14:51
2

Here's a proof, assuming addition works which you already understand.

It involves some algebra, so here's some handy notation for that. All variables are signed 16-bit values, 2's complement representation, unless stated otherwise.

S(eg,mg) = [eg - neg(mg), mg]

Here eg is an endgame score, and mg is a midgame score and [,] shows the bits of values packed into a 32-bit 2's complement signed integer. Most significant bits are to the left. S(eg,mg) is the representation of those scores. Here neg(x) is 1 if x is negative and 0 otherwise, so that the requirement that the representation of eg in the upper bits has one subtracted if mg is negative is satisfied.

This is what make_score should do (bug — for example it is not consistent with eg_value for negative mg).

We want to show that for any 32-bit signed integer i, multiplying S(eg,mg) by i just models multiplying eg and mg by i, which can be precisely stated as

(T) i*S(eg,mg) = S(i*eg, i*mg) where 16-bit multiplications are used in the arguments.

Proof: If i=0 this is obvious. Suppose that i>0 and that addition works, i.e. adding representations adds their endgame scores and adds their midgame scores. i.e. suppose

(A) S(eg1,mg1) + S(eg2,mg2) = S(eg1+eg2,mg1+mg2) for any arguments.

Then repeated use of (A) establishes (T) as follows.

i*S(eg,mg) 
        // sum of i copies of S(eq,mq)
    = S(eg,mg) + S(eg,mg) + S(eg,mg) + ... + S(eg,mg)
        // (A) on first two terms
    = S(eg+eg,mg+mg) + S(eg,mg) + ... + S(eg,mg)
    = S(2*eg,2*mg) + S(eg,mg) + ... + S(eg,mg)
        // (A) on first two remaining terms
    = S(2*eg+eg,2*mg+mg) + ... + S(eg,mg)
    = S(3*eg,3*mg) + ... + S(eg,mg)
        // ...
    = S(i*eg,i*mg)

So now (T) is established for all but negative i values.

Suppose i < 0, and let i = -j. Then

i*S(eg,mg) 
    = (-1)*j*S(eg,mg)
        // j is positive
    = (-1)*S(j*eg,j*mg)
       // if (T) works for (-1)
    = S((-1)*j*eg,(-1)*j*mg)
    = S((-j)*eg,(-j)*mg)
    = S(i*eg,i*mg)

so (T) is true for negative i provided it is true for i = -1, which is proved below.

This is the crux of the matter: working for scaling by (-1), i.e. proving that -S(eg,mg) = S(-eg,-mg). The proof below completes the proof of (T), solving the problem. Here's the argument:

-S(eq,mq) = -[eg - neg(mg), mg]

and we want to show this equal to

S(-eg,-mg) = [(-eg) - neg(-mg), (-mg)]

and here's some algebra that does just that. It makes liberal use of the well known 2's complement identity that -a = ~a + 1, i.e. the arithmetic negation is one more than the bitwise negation.

-[eg - neg(mg), mg]
            // `-a = ~a + 1`
        = ~[eg - neg(mg), mg] + 1
            // ~ is bitwise
        = [~(eg - neg(mg)), ~mg] + 1
            // if mg=0 the carry on adding 1 will propagate to the upper bits
case1: mg=0
[~(eg - neg(mg)), ~mg] + 1
           // neg(mg) = 0
        = [~eg, ~mg] + 1
            // ~mg is a bit pattern of all ones
        = [~eg + 1, ~mg + 1]
            // `~a = -a - 1`
        = [-eg, -mg]
            // neg(-mg)=0 because mg=0
        = [(-eg) - neg(-mg), (-mg)] 
        = S(-eg,-mg) as desired.
case2: mg≠0
[~(eg - neg(mg)), ~mg] + 1
            // carry does not propagate to upper bits
        = [~(eg - neg(mg)), ~mg + 1]
            // `~a = -a - 1`
        = [-(eg - neg(mg)) - 1, -mg]
        = [-eg - (1 - neg(mg)), -mg]
            // neg(-mg) = 1 - neg(mg) for mg≠0
        = [(-eg) - neg(-mg), (-mg)]
        = S(-eg,-mg) as desired.
Sturt
  • 204
  • 7
  • TL;DR? Addition works, and multiplication by positive i is just repeated addition, so multiplying by positive i works. i=0 also clearly works: mg=0 means no subtraction of one in the upper bits, so mg=0,eg=0 is represented by 32-bit zero. Multiplying any 32 bit score by 0 then leads to 0 which is mg=0,eg=0 as if they were multiplied individually by 0. Multiplying by a negative i can be interpreted as multiplying by its magnitude which works for the reasons above, followed by multiplying by -1. So proving it works when multiplying by -1 proves it always works. That algebra is given. – Sturt Dec 11 '22 at 02:46
  • Perfect! I was looking for something like this. Didn't think that I can prove multiplication via addition and negation. Thanks! – Chayim Friedman Dec 11 '22 at 12:52
0

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:

  1. The Score value 0x00001234 is multiplied by 2, resulting in the value 0x00002468. This value is stored in the result variable.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

    1. 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.

    2. 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)!

J. M. Arnold
  • 6,261
  • 3
  • 20
  • 38
  • Thought about visualizing the whole process with some sample values. Do you have a concrete situation for an endgame and middlegame score you want me to explain? :) Furthermore, should I explain it via tables (similar to a previous reply) or do you have any other preference? – J. M. Arnold Dec 05 '22 at 12:58
  • I'm not looking for cases. I can do it myself. What I'm after is a mathematical proof or an explanation of why this code is correct in the general case, both for positive and negative middlegame score. – Chayim Friedman Dec 05 '22 at 13:23
  • Don't the assertions suffice? – J. M. Arnold Dec 05 '22 at 13:24
  • I surely know this algorithm is correct. The fact that it appears in Stockfish is enough. But I want to know why. – Chayim Friedman Dec 05 '22 at 13:25
  • @ChayimFriedman There isn't much to mathematically proof, in my opinion - you are storing the two values in one Integer and perform a multiplication. But I'll try and you can give me feedback whether that is what you were looking for! :) – J. M. Arnold Dec 05 '22 at 13:30
  • But why when you extract them the result is each being multiplied by the value? Anyway, I can also accept an explanation that explains how the multiplication affects the bits. It is easy in the positive case (as they are multiplied independently), but I can't come with one for the negative case where the endgame is stored minus one. I can provide such explanation for addition, subtraction or negation. – Chayim Friedman Dec 05 '22 at 13:35
  • @ChayimFriedman Included a small explanation. – J. M. Arnold Dec 05 '22 at 13:58
  • You understood me wrong. The endgame score is stored minus one when **the middlegame score** is negative. Now we have two considerations: we need to keep it minus one (and not more) for if multiplied by a positive number (because then the middlegame score stays negative, unless there's an overflow) and change it to be intact when the multiplicand is negative (because then the middlegame score becomes -x-=+), and we need to make sure the high set bits of the middlegame (because it is stored negatively, bitwise negated as two's complement) do not render the resulting endgame score incorrect. – Chayim Friedman Dec 05 '22 at 14:05
  • I guess these factors effectively cancel each other, bringing in the correct answer, but I have no idea how. And I didn't understand what you mean by "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". – Chayim Friedman Dec 05 '22 at 14:06
  • Sorry, you are correct that the endgame score is stored minus one when the middlegame score is negative in order to preserve the sign of the middlegame score when it is multiplied by a positive integer value. This is because when the middlegame score is negative, the multiplication by a positive integer will result in a negative middlegame score, unless there is an overflow. But still: When the multiplicand is negative, the middlegame score will become positive and therefore the endgame score needs to be changed to be intact (not minus one) – J. M. Arnold Dec 05 '22 at 14:08
  • in order to preserve the correct sign of the middlegame score. This can be achieved by checking the sign of the multiplicand and adjusting the endgame score accordingly before it is multiplied by the multiplicand. or did I understand something completely wrong? – J. M. Arnold Dec 05 '22 at 14:09
  • 1
    Fine, the problem is that this code _doesn't do that_. It just magically adjusts everything. And also, contrary to what I would expect, the multiplication `(endgame-1)*v` doesn't expand to `endgame*v-v`, but rather to `endgame*v-1`, probably because the high bits of the middlegame score as I said. – Chayim Friedman Dec 05 '22 at 14:10
  • Maybe we're talking in different directions, but even the official commit (https://github.com/official-stockfish/Stockfish/commit/d2971f3fca929d9085ed17b71aa4ec8e96499d99) is in-line with the previous agenda, isn't it? – J. M. Arnold Dec 05 '22 at 14:16
  • What do you see in this commit? – Chayim Friedman Dec 05 '22 at 14:23
  • Regarding your last reply: You are correct that the multiplication `(endgame-1)*v` does not expand to `endgamev-v`, but rather to `endgame*v-1`. This is because the endgame score is stored minus one when the middlegame score is negative, in order to preserve the sign of the middlegame score when it is multiplied by a positive integer value. When the middlegame score is negative, its sign bit is set (so the endgame score must be stored -1 to preserve the sign of the middlegame score). Are we on the same page at this point? – J. M. Arnold Dec 05 '22 at 14:26
  • Yes, but how does this happen? – Chayim Friedman Dec 05 '22 at 14:41
  • In `make_score(int mg, int eg)`? After the unsigned int cast it left-shifts it by 16 bits and then adds the middlegamev to the endgamev.. – J. M. Arnold Dec 05 '22 at 14:47
  • No, where it happens that the multiplication becomes `endgame*v-1` and not `endgame*v-v`? – Chayim Friedman Dec 05 '22 at 14:50
  • First off, it's not explicitly in the `operator*` implementation. It's when extracting the values from the Score (using the _value functions). But this comment section becomes very lengthy. Do you want to open a chat and we go through it one-by-one? :) – J. M. Arnold Dec 05 '22 at 15:02
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250165/discussion-between-j-m-arnold-and-chayim-friedman). – J. M. Arnold Dec 05 '22 at 15:04
  • @ChayimFriedman I edited it to answer your formula. – J. M. Arnold Dec 05 '22 at 16:16
  • But `(a*b)/c=a*b*(1/c)=a*(1/c)*b=(a/c)*b`, so your argument is wrong. – Chayim Friedman Dec 05 '22 at 16:30
  • No, these are completely different situations: multiplication of `eg` by `2^16` should be performed before the division by `n`! – J. M. Arnold Dec 05 '22 at 16:39
  • Your original formula would be correct (but completely different meaning) if it were for those parentheses: `((eg * 2^16) / n) + (mg / n) = ((eg / n) * 2^16) + (mg / n)`. In this case you would need to multiply `eg` by `2^16` first and then add the final division parentheses. Without the parentheses the result you're getting (`(eg / n) * 2^16 + mg / n`) is mathematically not correct and therefore the order is different (and not relevant to our case). This is the reason why `operator/` is implemented non-independently (in comparison to the `operator*`. – J. M. Arnold Dec 05 '22 at 16:47
  • But `(a*b)/c`, which I proved is equal to `(a/c)*b`, is exactly the same as `(eg*2^16)/n`, so it is the same as `(eg/n)*2^16`. – Chayim Friedman Dec 06 '22 at 06:24