Move Generation: Castling - micro-Max by H.G. Muller

Discussion of chess software programming and technical issues.

Moderator: Ras

Alexlaw1964
Posts: 45
Joined: Fri Jan 03, 2025 6:36 am
Full name: alex lobov

Re: Move Generation: Castling - micro-Max by H.G. Muller

Post by Alexlaw1964 »

"STM32 micro-Max firmware → Polyglot book → Windows Winboard port (and a fun E1-hunting story)".
Porting an STM32 micro-Max firmware to a Winboard engine — including a Polyglot book, and the bug it exposed
This is a report on a two-stage project: first I extended the classic micro-Max chess engine (by H.G. Muller — thanks for one of the most fascinating pieces of code in computer chess) running as firmware on an STM32, and then reverse-ported that firmware into a Windows console engine speaking the Winboard protocol. Along the way I hit a hash-key bug that took a diff-test to find, and I'll describe it in some detail, since the root cause may bite anyone porting micro-Max-like code with global game-state variables.

The starting point
The firmware is based on micro-Max 4.1, with these patches already applied:

v4.3 — king safety through a "magnetic, frozen king" in the middle game;
v4.5 — hash table and repetition-draw recognition kept at game level.
On top of that, the STM32 build got:

piece-square tables (1024-byte PST array indexed by ((x>>4)&7)*8+(x&7), i.e. rank-major), added as an increment to the material score i = 99*w[t&7] inside the move loop;
a small eval patch for the e-pawn / f8-bishop structure (bonus for opening the f8–a3 window, penalties for the locked bishop on e6, mirrored for White);
a Polyglot opening book, consulted only when the engine plays Black (i.e. after White's first move), exactly like the firmware logic.
The firmware itself runs on an STM32F103 ("Blue Pill") with an 8x8 LED matrix and a USART command interface; the engine is driven by a fixed node limit (level 60000), and the root warm-up loop N=-1; W(++N<121); is kept to reproduce the exact node-counter parity of the original.

Stage 2: the Windows port
For the reverse port I used H.G. Muller's own as the communication skeleton: a main() loop reading Winboard commands from stdin, with the engine replying move xxxx. Crucially, I did not restructure the engine: the obfuscated D() search, the deferred-move mechanism (global K/L receiving the move, executed inside D()), the O-based castling encoding — all preserved as in the firmware, so that port behavior stays comparable to the hardware. Winboard commands (new, force, go, undo, remove, post, time controls...) map onto the firmware's state machine.

Problem 1: rand() mismatch
The engine fills its 1035-byte T[] attack table with random bytes, drawn from rand(). The STM32 firmware uses newlib, whose LCG differs from Microsoft's Windows rand() — so the port would build a different table and play differently. Solution: an explicit LCG emulation with newlib's parameters:

Code: Select all

static uint64_t mm_seed = 1ULL;
static int mm_rand(void) {
    mm_seed = mm_seed*6364136223846793005ULL + 1ULL;
    return (int)((mm_seed>>32) & 0x7fffffff);
}
...
N=1035; W(N-->M) T[N] = mm_rand()>>9;
Problem 2: the Polyglot hash key that refused to change
The book lookup computes a 32-bit Polyglot key from the position via a standard compute_zobrist(): pieces through an 0x88→0x63 mapping, side-to-move XOR when Black is on move, castling-rights keys, and the e.p. file key at ZobristTable[773 + (ep_file)] whenever an e.p. square is set.

The first symptom: after e2e4, the key was consistently missing the e.p. component. The debugging went through several wrong hypotheses — at one point I even added a "pawn capturer" condition to the e.p. XOR, on the theory that the book generator implements the usual rule of Polyglot filtering. Diff-testing against an independent Python generator showed the opposite: the generator XORs the e.p. square unconditionally whenever it is set, capturer or not. So the conditional was my own error, and reverting to the plain XOR was correct.

But reverting still didn't fix the key — and here was the interesting part. Debug prints showed that at book-lookup time the e.p. global (E1, micro-Max's e.p. square variable) contained 128 = 0x80, i.e. an off-board value. Where did that come from? The sequence of events after White plays e2e4:

the move is applied, and E1 is correctly set to the jumped-over square ((from + to) >> 1 — e3);
then a mate/stalemate check runs, which internally calls the search D() for a null-move-style verification;
and D() starts every invocation with E1 = E; — its e.p. parameter — which at root level receives O = 128 ("no e.p.");
...so by the time the book code ran, the honest e3 had been clobbered with garbage. The firmware never noticed because it has no such intervening search call. The fix is a two-liner: keep the true e.p. square in a dedicated global that D() never touches, and restore it just before the book lookup (it also makes e.p. captures visible to the move-legality check that validates book moves):

Code: Select all

/* in the move-apply routine, right after the board update: */
if ((b[to]&7) < 3 && ((from-to == 32) || (to-from == 32)))
    ep_ext = (from + to) >> 1;        /* jumped-over square: e4->e3, e5->e6 */
else
    ep_ext = 0;

/* in the book branch of think(): */
if (book_pending) {
    book_pending = 0;
    E1 = ep_ext;                       /* honest e.p. instead of D()'s garbage */
    book_key = (uint32_t)(compute_zobrist(k) & 0xFFFFFFFFU);
    ...
}
The lesson: in micro-Max's code, globals like E1, K, L, O are workspace, not state — anything that needs to survive a search invocation must be kept elsewhere. Something to keep in mind for anyone adding book/repetition logic around this engine.

Problem 3: validating book moves
A Polyglot move is decoded to from/to in 0x88 coordinates, then converted to the engine's K/L encoding and checked for legality by generating the full move list with the firmware's is_move() (which also handles e.p. and promotion flags). Only if the move passes is it applied through the same code path as a user move — no separate board-update logic, which avoids a whole class of state desync bugs.

The engine answers as Black by default (like the firmware). Time-control commands are accepted and ignored: the node budget is fixed, matching the hardware behavior.
Verification
The book path was validated against an independent Python Polyglot generator, position by position:
White's move computed key expected key book reply
1.e4 66FF886D 66FF886D c7c5
1.d4 D75F281A D75F281A d7d5

Both roots of the (small) book now hash correctly, including the e.p. component on different files, and the port answers with book moves identical to the STM32 firmware. With that fixed, the Windows engine is a behavioral twin of the hardware build.

Code: Select all

/* umax_714_w.c — Windows-порт прошивки STM32 ver7.14 (шаг 1 из 4) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include "polyglot.h"      /* подключение книги — после #include <stdint.h> */

int k=8;                   /* сторона, делающая ход (8=белые, 16=чёрные) */
int jg=0;                  /* счётчик корневых вызовов (как j в main прошивки) */
int HistPtr=0, StartKey=0, Post=0, Computer=0;
int UndoHistory[512];      /* сделанные ходы для undo/remove */
int UndoPtr=0;


#define WHITE 8
#define BLACK 16
#define GH 96

uint16_t GameHistory[GH];
char GamePtr;
int side;        /* сторона, за которую играл движок на предыдущем ходу */
int hh;          /* --- TT --- попадания в хэш за текущий поиск */
int level=60000; /* фиксированный узловой лимит, как в прошивке */

#define W while
#define K(A,B) *(int*)(T+A+(B&8)+S*(B&7))
#define J(A) K(y+A,b[y])-K(x+A,u)-K(H+A,t)
#define U (1<<8)
struct _ {int K,V;char X,Y,D;} A[U];

int M=136,S=128,I=8e3,C=799,Q,O,K,N,R,J,Z,E1;
uint16_t pg_move;
uint32_t book_key;
int from88, to88;
int book_pending = 0, book_move_ready = 0;
int ep_ext=0;   /* ep-квадрат текущей позиции: переживает вызовы D() */
char L,
w[]={0,1,1,3,-1,3,5,9},
o[]={-16,-15,-17,0,1,16,0,1,16,15,17,0,14,18,31,33,0,
 7,-1,11,6,8,3,6,
 6,3,5,7,4,5,3,6},
b[129],
T[1035],
n[]=".?+nkbrq?*?NKBRQ";

/* --- генератор rand(), побитово совпадающий с newlib (STM32):
   seed=1, next = next*6364136223846793005 + 1, старшие 31 бита.
   T[] в порту заполняется той же последовательностью, что в прошивке. */
static uint64_t mm_seed = 1ULL;
static int mm_rand(void)
{
    mm_seed = mm_seed*6364136223846793005ULL + 1ULL;
    return (int)((mm_seed>>32)&0x7fffffff);
}

/* --- PST --- piece-square tables, 1024 bytes (const -> .rodata).
   Индекс поля: ((x>>4)&7)*8+(x&7) = rank*8 + column.
   Строки для u&15 = 0,1,8,10 не используются. */
const signed char PST[16][64] = {
    /* u&15= 0,1: unused */
    {0},
    {0},
    /* u&15= 2: black pawn */
    {   0,   2,   4,   8,   8,   4,   2,   0,
        0,   2,   4,   8,   8,   4,   2,   0,
        6,   8,  10,  14,  14,  10,   8,   6,
       12,  14,  16,  20,  20,  16,  14,  12,
       22,  24,  26,  30,  30,  26,  24,  22,
       35,  37,  39,  43,  43,  39,  37,  35,
       50,  52,  54,  58,  58,  54,  52,  50,
       30,  31,  32,  34,  34,  32,  31,  30 },
    /* u&15= 3: black knight */
    { -20, -10, -10,   0,   0, -10, -10, -20,
      -10,   0,   5,  10,  10,   5,   0, -10,
       -5,   5,  10,  15,  15,  10,   5,  -5,
       -5,   5,  15,  20,  20,  15,   5,  -5,
       -5,   5,  15,  20,  20,  15,   5,  -5,
       -5,   5,  10,  15,  15,  10,   5,  -5,
      -10,   0,   0,   0,   0,   0,   0, -10,
      -20, -10, -10, -10, -10, -10, -10, -20 },
    /* u&15= 4: black king */
    { -28, -21, -16, -13, -12, -13, -12, -21,   /* <- g8: -16 -> -12 */
      -22, -15, -10,  -7,  -6,  -7, -10, -15,
      -18, -11,  -6,  -3,  -2,  -3,  -6, -11,
      -16,  -9,  -4,  -1,   0,  -1,  -4,  -9,
      -16,  -9,  -4,  -1,   0,  -1,  -4,  -9,
      -18, -11,  -6,  -3,  -2,  -3,  -6, -11,
      -22, -15, -10,  -7,  -6,  -7, -10, -15,
      -28, -21, -16, -13, -12, -13, -16, -21 },
    /* u&15= 5: black bishop */
    { -10, -10, -10, -10, -10, -10, -10, -10,
      -10,   0,   0,   0,   0,   0,   0, -10,
      -10,   5,   8,  10,  10,   8,   5, -10,
      -10,   5,  10,  12,  12,  10,   5, -10,
      -10,   5,  10,  12,  12,  10,   5, -10,
      -10,   5,   8,  10,  10,   8,   5, -10,
      -10,   0,   0,   0,   0,   0,   0, -10,
      -10, -10, -10, -10, -10, -10, -10, -10 },
    /* u&15= 6: black rook */
    {   0,   0,   0,   5,   5,   0,   0,   0,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       15,  15,  15,  15,  15,  15,  15,  15,
        0,   0,   0,   0,   0,   0,   0,   0 },
    /* u&15= 7: black queen */
    { -10,  -5,  -5,  -5,  -5,  -5,  -5, -10,
       -5,   0,   5,   0,   0,   0,   0,  -5,
       -5,   5,   5,   5,   5,   5,   5,  -5,
        0,   0,   5,   5,   5,   5,   0,   0,
       -5,   0,   5,   5,   5,   5,   0,  -5,
       -5,   0,   5,   5,   5,   5,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
      -10,  -5,  -5,  -5,  -5,  -5,  -5, -10 },
    /* u&15= 8: unused */
    {0},
    /* u&15= 9: white pawn */
    {  30,  31,  32,  34,  34,  32,  31,  30,
       50,  52,  54,  58,  58,  54,  52,  50,
       35,  37,  39,  43,  43,  39,  37,  35,
       22,  24,  26,  30,  30,  26,  24,  22,
       12,  14,  16,  20,  20,  16,  14,  12,
        6,   8,  10,  14,  14,  10,   8,   6,
        0,   2,   4,   8,   8,   4,   2,   0,
        0,   2,   4,   8,   8,   4,   2,   0 },
    /* u&15=10: unused */
    {0},
    /* u&15=11: white knight */
    { -20, -10, -10, -10, -10, -10, -10, -20,
      -10,   0,   0,   0,   0,   0,   0, -10,
       -5,   5,  10,  15,  15,  10,   5,  -5,
       -5,   5,  15,  20,  20,  15,   5,  -5,
       -5,   5,  15,  20,  20,  15,   5,  -5,
       -5,   5,  10,  15,  15,  10,   5,  -5,
      -10,   0,   5,  10,  10,   5,   0, -10,
      -20, -10, -10,   0,   0, -10, -10, -20 },
    /* u&15=12: white king */
    { -28, -21, -12, -13, -12, -13, -12, -21,   /* <- g1: -16 -> -12 */
      -22, -15, -10,  -7,  -6,  -7, -10, -15,
      -18, -11,  -6,  -3,  -2,  -3,  -6, -11,
      -16,  -9,  -4,  -1,   0,  -1,  -4,  -9,
      -16,  -9,  -4,  -1,   0,  -1,  -4,  -9,
      -18, -11,  -6,  -3,  -2,  -3,  -6, -11,
      -22, -15, -10,  -7,  -6,  -7, -10, -15,
      -28, -21, -16, -13, -12, -13, -16, -21 },
    /* u&15=13: white bishop */
    { -10, -10, -10, -10, -10, -10, -10, -10,
      -10,   0,   0,   0,   0,   0,   0, -10,
      -10,   5,   8,  10,  10,   8,   5, -10,
      -10,   5,  10,  12,  12,  10,   5, -10,
      -10,   5,  10,  12,  12,  10,   5, -10,
      -10,   5,   8,  10,  10,   8,   5, -10,
      -10,   0,   0,   0,   0,   0,   0, -10,
      -10, -10, -10, -10, -10, -10, -10, -10 },
    /* u&15=14: white rook */
    {   0,   0,   0,   0,   0,   0,   0,   0,
       15,  15,  15,  15,  15,  15,  15,  15,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   0,   0,   0,   0,   0,  -5,
        0,   0,   0,   5,   5,   0,   0,   0 },
    /* u&15=15: white queen */
    { -10,  -5,  -5,  -5,  -5,  -5,  -5, -10,
       -5,   0,   0,   0,   0,   0,   0,  -5,
       -5,   0,   5,   5,   5,   5,   0,  -5,
       -5,   0,   5,   5,   5,   5,   0,  -5,
        0,   0,   5,   5,   5,   5,   0,   0,
       -5,   5,   5,   5,   5,   5,   5,  -5,
       -5,   0,   5,   0,   0,   0,   0,  -5,
      -10,  -5,  -5,  -5,  -5,  -5,  -5, -10 },
};

/* <<< PATCH: e-пешка и ферзевый слон --------------------------------------
   Black:  пешка e6 + пустая e7 + слон f8 -> +10 (открылось окно f8-a3)
           пешка e7 + слон f8            ->  -5 (пешка бездельничает, слон заперт)
           слон e6 + пешка e7            -> -15 (слон замурован: e7, f8 и рокировка)
   White: зеркально с обратным знаком (E_LOCK>0 всегда в пользу чёрных).
   Индексы 0x88: e7=20 e6=36 f8=5 e2=100 e3=84 f1=117
   Коды: чёрная пешка=2, чёрный слон=5, белая пешка=9, белый слон=13     */
#define E_LOCK ( \
 ((b[36]&15)==2 && !b[20] && (b[5]&15)==5 ? 10:0) \
+((b[20]&15)==2 && (b[5]&15)==5 ? -5:0) \
+((b[36]&15)==5 && (b[20]&15)==2 ? -15:0) \
-((b[84]&15)==9 && !b[100] && (b[117]&15)==13 ? 10:0) \
-((b[100]&15)==9 && (b[117]&15)==13 ? -5:0) \
-((b[84]&15)==13 && (b[100]&15)==9 ? -15:0))
/* --- шаг 2 из 4: ядро поиска D() ---------------------------------------- */
char deb2[64];   /* строка поиска (глубина, узлы, оценка, TT-хиты) — как в прошивке,
                    только не на LED-матрицу, а в глобальный буфер */

int D(k,q,l,e,J,Z,E,z,n)
int k,q,l,e,J,Z,E,z,n;
{
 int j,r,m,v,d,h,i,F,G,s,r0,f=J,g=Z;   /* <<< PATCH: добавлен r0 */
 char t,p,u,x,y,X,Y,H,B;
 struct _*a=A+(J+k*E&U-1);
 q--;
 d=a->D;m=a->V;X=a->X;Y=a->Y;
 if(a->K-Z|z|
 !(m<=q|X&8&&m>=l|X&S))
 d=Y=0;else hh++;                      /* ключ совпал, окно совместимо */
 X&=~M;
 E1=E;
 W(d++<n||
 z==8&K==I&&(N<level&d<98||
 (K=X,L=Y&~M,d=2)))
 {x=B=X;
 h=Y&S;
 m=d>1?-I:e;
 N++;
 do{u=b[x];
 if(u&k)
 {r=p=u&7;
 j=o[p+16];
 W(r=p>2&r<0?-r:-o[++j])
 {A:
 y=x;F=G=S;
 do{
 H=y=h?Y^h:y+r;
 if(y&M)break;
 m=E-S&b[E]&&y-E<2&E-y<2?I:m;
 if(p<3&y==E)H^=16;
 t=b[H];if(t&k|p<3&!(y-x&7)-!t)break;
 //i=99*w[t&7];
 i=99*w[t&7]+((t&7)==4?0:PST[t&15][((H>>4)&7)*8+(H&7)]);
 m=i<0?I:m;
 if(m>=l)goto C;
 if(s=d-(y!=z))
 //{v=p<6?b[x+8]-b[y+8]:0;
  {v=PST[u&15][((y>>4)&7)*8+(y&7)]-PST[u&15][((x>>4)&7)*8+(x&7)];
  r0=E_LOCK;                                  /* <<< PATCH: бонус до хода */
  b[G]=b[H]=b[x]=0;b[y]=u|32;
  //if(!(G&M))b[F]=k+6,v+=50;
  if(!(G&M))b[F]=(u&24)+6,v+=y>x?50:20;      /* <- было k+6 */
  v-=p-4|R>29?0:20;
  if(p<3)
  //{v-=9*((x-2&M||b[x-2]-u)+
  {v-=20*((x-2&M||b[x-2]-u)+
    (x+2&M||b[x+2]-u)-1);
    if(b[x^16]==k+36)v-=10+10*!((y-x)&15);
    if(y+r+1&S)b[y]|=7,i+=C;
  }
  J+=J(0);Z+=J(8)+G-S;
  v+=e+i;
  v+=u&16?E_LOCK-r0:r0-E_LOCK;                /* <<< PATCH: дельта после хода */
  f=m>q?m:q;
  v=s>1|v>f?-D(24-k,-l,-f,-v,
  J+J(0),Z+J(8)+G-S,F,y,s):v;
  if(z&8&&K-I)
  {if(v+I&&x==K&y==L)
  {Q=-e-i;O=F;
      R+=i>>7;return l;}
  v=m;
  }
  J=f;Z=g;
  b[G]=(u&24)+6;b[F]=b[y]=0;b[x]=u;b[H]=t;   /* <- было k+6 */
  if(v>m)
  m=v,X=x,Y=y|S&F;
  if(h){h=0;goto A;}
 }
 if(x+r-y|u&32|
 p>2&(p-4|j-7||
 b[G=x+3^r>>1&7]-(u&24)-6     /* <- было k-6 */
 ||b[G^1]|b[G^2])
 )t+=p<5;
 else F=y;
 }W(!t);
 }}}W((x=x+9&~M)-B);
C:if(m>I-M|m<M-I)d=99;
 m=m+I?m:-D(24-k,-I,I,0,J,Z,S,S,1);
 a->K=Z;a->V=m;a->D=d;
 a->X=X|8*(m>q)|S*(m<l);a->Y=Y;
 if(z==S+1)K=X,L=Y&~M;   /* извлечение хода для PrintResult */

  if(z==8)
 {
   /* вместо GPIOB->ODR и write_reg(): строка статистики — в буфер */
   sprintf(deb2," d=%d uz=%d oc=%d tt=%d", d-1, N-S, m, hh);
 }
 }
 return m+=m<e;
}
/* --- шаг 3 из 4: ReadFEN, is_move, история позиций ---------------------- */

#define STATE 64
char HistoryBoards[GH][STATE];   /* снимки позиций для правила троекратного повтора */

void CopyBoard(int s)
{
 int j;
 /* снимаем доску; не забываем права рокировки и e.p. поле */
 for(j=0; j<64; j++)
   HistoryBoards[s][j] = b[j+(j&0x38)];
 if(!(O&M)) HistoryBoards[s][O+(O&7)>>1] |= 64; /* пометить e.p. квадрат */
}

int PrintResult(int s)
{
 int j, k2, cnt=0;

 /* ищем в последних позициях третий повтор */
 for(j=2; j<=50; j+=2)
 {
   for(k2=0; k2<STATE; k2++)
     if(HistoryBoards[HistPtr][k2] !=
        HistoryBoards[HistPtr-j&GH-1][k2] )
       goto differs;
   if(++cnt == 2) /* третий повтор */
   {
     printf("1/2-1/2 {Draw by repetition}\n");
     return 1;
   }
   differs: ;
 }
 K=I;
 cnt = D(s,-I,I,Q,1,StartKey++,O,S+1,2);
 if(cnt==0 && K==0 && L==0) {
   printf("1/2-1/2 {Stalemate}\n");
   return 2;
 }
 if(cnt==-I+1) {
   if (s == WHITE) printf("0-1 {Black mates}\n");
   else            printf("1-0 {White mates}\n");
   return 3;
 }
 return 0;
}


int ReadFEN(const char *FEN)
{
 int row, file, i, col, nr, cc, rights;
 char c, epSqr[2];
 const char *p;

 p = FEN;
 rights = 0;
 for(i=0; i<128; i++) b[i&0x77]=0;
 nr=0;
 cc=0;
 E1=0;
 for(row=7; row>=0; row--)
 {   /* читаем один ряд FEN */
     file = 0;
     do{
       c = *p++;
       if(c>='1' && c<='8') { file += c - '0'; }
       else
       {
         col = WHITE;
         cc=8*(7-row)+file+nr;
         if(c >= 'a') { c += 'A'-'a'; col = BLACK; }
         switch(c)
         {
         //{1,2,3,4,5,6,7} = {P+,P-,N,K,B,R,Q}
         case 'K': b[cc]=4|col; break;
         case 'R': b[cc]=6|col; break;
         case 'Q': b[cc]=7|col; break;
         case 'B': b[cc]=5|col; break;
         case 'P': b[cc]=9<<(1&(col>>4)); break;
         case 'N': b[cc]=3|col; break;
         }
         file++;
       }
     }while(file < 8);
     if(file >  8) return (-10); /* плохой формат */
     if(file == 8)
     {   c = *p++;
         nr+=8;
         if(row > 0 && c != '/') return(-10);
         if(row==0  && c != ' ') return (-10);
     }
 }
 while(c = *p++)
 {
     if(c>='0' && c<='9') continue; /* игнорируем счётчики ходов */
     if(c>='a' && c<='h') /* возможно, поле e.p. */
     {    if(*p == '3' || *p == '6')
          {
              epSqr[0]=c;
              epSqr[1]=*p;
              E1=((8 - (epSqr[1] - '0')) * 16) + (epSqr[0] - 'a');
              p++;
              continue;
          }
     }
     switch(c)
     {
     case 'K': rights |= 1; break;
     case 'Q': rights |= 2; break;
     case 'k': rights |= 4; break;
     case 'q': rights |= 8; break;
     case 'w': col = WHITE; break;
     case 'b': col = BLACK; break;
     case ' ':
     case '-': break;
     }
 }

 /* права рокировки сняты -> короля и ладью помечаем "уже двигал" (|32) */
 if(!(rights&1)){ if(b[116]&7) b[116]|=32; if(b[119]&7) b[119]|=32; } /* нет K: e1,h1 */
 if(!(rights&2)){ if(b[112]&7) b[112]|=32; if(b[116]&7) b[116]|=32; } /* нет Q: a1,e1 */
 if(!(rights&4)){ if(b[4]&7)   b[4] |=32;  if(b[7]&7)   b[7] |=32;  } /* нет k: e8,h8 */
 if(!(rights&8)){ if(b[4]&7)   b[4] |=32;  if(b[0]&7)   b[0] |=32;  } /* нет q: a8,e8 */

 return col;
}

int is_move()
{
 int j,r,cnt=0;
 char t,x,y,u,p,H1;
 x=0;
 do{
     u=b[x];
     if(u&k)
     { r=p=u&7;
       j=o[p+16];
       while(r=p>2&r<0?-r:-o[++j])
       {
         y=x;
         do{
           H1=y+=r;
           if(y&M)break;
           if(p<3&y==E1)H1=y^16;
           t=b[H1];
           if(t&k|p<3&!(r&7)!=!t)break;
           cnt++;
           if(GamePtr<GH)GameHistory[GamePtr++] = 256*x + y;
           t += p<5
             ^ p<3 & 6*k+(y&112)==S
             ^ j<8 & y & !(u&32)
               & b[H1=x^3^r>>1&7]==(u&24)+6
               & !b[H1^1] & !b[H1^2];
         }while(!t);
       }
     }
 }W(x=x+9&~M);
 return cnt;
}
/* --- шаг 4 из 4: main() — цикл Winboard + книга полиоглот за чёрных ----- */

const char FEN[] = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";

/* применить ход, уже записанный в K,L — ровно как в прошивке:
   D() сама исполняет ход на доске (механизм отложенного хода)
   и возвращает l, если ход легален */
void apply_KL(void)
{
    side=k;
    hh=0;
    k^=D(k,-I,I,Q,1,jg++,O,8,2)-I?0:24;
    if ((b[L]&7)<3 && ((K-L==32)||(L-K==32)))
        ep_ext = (K+L)>>1;                /* перепрыгнутое поле: e4->e3, e5->e6 */
    else
        ep_ext = 0;
    if(UndoPtr<512) UndoHistory[UndoPtr++] = 256*K+L;
    HistPtr=(HistPtr+1)%GH;               /* кольцо снимков для повторов */
    CopyBoard(HistPtr);
    /* книга просыпается только после хода белых (ход чёрных) — как в прошивке */
    if(side!=k) book_pending = (side==8);
    else        book_pending = 0;
}

void new_game(void)
{
    k = ReadFEN(FEN);
    Q=0; O=S; E1=0; jg=0; StartKey=0;ep_ext = 0;
    side=k;
    GamePtr=0; UndoPtr=0; HistPtr=0;
    memset(HistoryBoards,0,sizeof(HistoryBoards));
    CopyBoard(HistPtr);
    book_pending = (k==BLACK);
}

void think(void)
{
    int i;
    char bc[4];

    /* --- книга: только за чёрных, как в прошивке --- */
    if(book_pending){
        book_pending=0;
        E1 = ep_ext;                      /* вернуть честный ep-квадрат вместо мусора от D() */
        book_key = (uint32_t)(compute_zobrist(k) & 0xFFFFFFFFU);
        if(polyglot_find_move(book_key,&pg_move)){
            polyglot_to_mm(pg_move,&from88,&to88);
            bc[0]='a'+(from88&7); bc[1]='8'-(from88>>4);
            bc[2]='a'+(to88&7);   bc[3]='8'-(to88>>4);
            K=bc[0]-16*bc[1]+C;
            L=bc[2]-16*bc[3]+C;
            /* проверка легальности книжного хода — как в прошивке */
            GamePtr=0; is_move();
            for(i=0;i<GamePtr;i++)
                if(GameHistory[i]==(256*K+L)) break;
            if(i<GamePtr){
                apply_KL();
                printf("move %c%c%c%c\n",bc[0],bc[1],bc[2],bc[3]);
                fflush(stdout);
                if(PrintResult(k)) Computer=0;
                return;
            }
        }
        /* книжного хода нет — играем поиском */
    }

    /* --- поиск: корневой вызов с N=121, как в прошивке --- */
    N=-1; W(++N<121);        /* N доходит до 121 до входа в D() — тот же узловой старт */
    hh=0;
    side=k;
    K=I;                     /* отложенного хода нет: движок ищет сам */
    k^=D(k,-I,I,Q,1,jg++,O,8,2)-I?0:24;
    if ((b[L]&7)<3 && ((K-L==32)||(L-K==32)))
        E1 = (K+L)>>1;
    else
        E1 = 0;
    if(UndoPtr<512) UndoHistory[UndoPtr++] = 256*K+L;
    HistPtr=(HistPtr+1)%GH;
    CopyBoard(HistPtr);
    printf("move %c%c%c%c\n",'a'+(K&7),'8'-(K>>4),'a'+(L&7),'8'-(L>>4&7));
    fflush(stdout);
    if(side!=k) book_pending = (side==8);
    else        book_pending = 0;
    if(PrintResult(k)) Computer=0;
}

int main(void)
{
    char line[256], command[256];
    int i, bad;

    /* InitEngine: T[] — той же последовательностью, что на STM32 */
    N=1035; W(N-->M) T[N]=mm_rand()>>9;

    Computer=BLACK;          /* по умолчанию движок играет чёрными, как в прошивке */
    new_game();

    for(;;){
        fflush(stdout);
        if(Computer && k==Computer){
            think();
            continue;
        }
        if(!fgets(line,256,stdin)) return 0;
        if(line[0]=='\n') continue;
        sscanf(line,"%s",command);

        if(!strcmp(command,"xboard"))    continue;
        if(!strcmp(command,"protover")){ printf("feature done=1\n"); continue; }
        if(!strcmp(command,"new"))     { Computer=BLACK; new_game(); continue; }
        if(!strcmp(command,"quit"))      return 0;
        if(!strcmp(command,"force"))   { Computer=0; book_pending=0; continue; }
        if(!strcmp(command,"white"))   { k=WHITE; side=k; UndoPtr=0; HistPtr=0;
                                         memset(HistoryBoards,0,sizeof(HistoryBoards));
                                         CopyBoard(HistPtr); Computer=BLACK;
                                         book_pending=0; continue; }
        if(!strcmp(command,"black"))   { k=BLACK; side=k; UndoPtr=0; HistPtr=0;
                                         memset(HistoryBoards,0,sizeof(HistoryBoards));
                                         CopyBoard(HistPtr); Computer=WHITE;
                                         book_pending=1; continue; }
        if(!strcmp(command,"go"))      { Computer=k; book_pending=(k==BLACK); continue; }
        if(!strcmp(command,"post"))    { Post=1;  continue; }
        if(!strcmp(command,"nopost"))  { Post=0;  continue; }
        if(!strcmp(command,"undo") || !strcmp(command,"remove")){
            int nr = (command[0]=='u') ? 1 : 2;
            if(UndoPtr<nr) continue;
            UndoPtr-=nr;
            new_game();          /* откат — перепроигрыванием с нуля */
            for(i=0;i<UndoPtr;i++){
                K=UndoHistory[i]>>8 & 255;
                L=UndoHistory[i] & 255;
                apply_KL();
            }
            continue;
        }
        /* команды управления временем игнорируем: лимит фиксированный,
           level=60000 узлов, как в прошивке */
        if(!strcmp(command,"level")||!strcmp(command,"time")||
           !strcmp(command,"otim") ||!strcmp(command,"st")  ||
           !strcmp(command,"sd")   ||!strcmp(command,"memory")||
           !strcmp(command,"cores")||!strcmp(command,"random")||
           !strcmp(command,"computer")||!strcmp(command,"hard")||
           !strcmp(command,"easy")) continue;

        /* иначе — попытка разобрать ход */
        bad = line[0]<'a'|line[0]>'h'|line[1]<'1'|line[1]>'8'|
              line[2]<'a'|line[2]>'h'|line[3]<'1'|line[3]>'8';
        if(bad){ printf("Error (unknown command): %s\n",command); continue; }
        K=line[0]-16*line[1]+C;
        L=line[2]-16*line[3]+C;
        /* проверка легальности по списку is_move() — как в прошивке */
        GamePtr=0; is_move();
        for(i=0;i<GamePtr;i++)
            if(GameHistory[i]==(256*K+L)) break;
        if(i>=GamePtr){ printf("Illegal move: %s\n",line); continue; }
        apply_KL();
        if(PrintResult(k)) Computer=0;
    }
}
Alexlaw1964
Posts: 45
Joined: Fri Jan 03, 2025 6:36 am
Full name: alex lobov

Re: Move Generation: Castling - micro-Max by H.G. Muller

Post by Alexlaw1964 »

UPDATE (next day).
Known remaining issue: promotion moves and the GUIs
One unsolved problem surfaced during the tournaments, and it is inherited straight from H.G. Muller's original port: SCID and Arena expect the promotion piece to be spelled out explicitly — the move must be sent as e7e8q, not as plain e7e8. Both GUIs treat the bare form as an illegal/incomplete move, so promotion positions effectively break the game flow.

The reason sits deep in micro-Max's design: the engine auto-promotes internally, and its deferred-move encoding (K/L receiving from/to inside D()) simply has no field for the promoted piece — the queen appears on the last rank as a side effect of the pawn's move, with the piece substitution happening in the board-update logic. Winboard's protocol, on the other hand, is designed around explicit promotion moves: the GUI sends e7e8q (and may also offer e7e8n etc. through the promotion dialog), and it expects the engine's own move replies to carry the same suffix.

So the mismatch is two-sided:

outbound — when the engine promotes, it prints move e7e8, and SCID/Arena reject it;
inbound — when the GUI sends move e7e8q, the plain micro-Max move parser has no code to strip and interpret the suffix.
The clean fix is a thin layer in the port only, without touching the engine: in the move-output routine, detect pawn-to-last-rank moves (same condition p<3 & (y&112)==S that the engine already uses for promotion) and append the piece letter — q, since micro-Max auto-queens; in the command handler, parse a trailing q/n/r/b and, for anything but q, either translate it into the engine's promotion encoding or politely reject/convert. I have not implemented this yet, so for now the practical workaround in tournaments is to avoid promotion positions or play them in a GUI that tolerates the bare form. Since the same limitation exists in Muller's original umax4_0w.c, this looks like a known family trait of micro-Max ports rather than a bug of my own — but it is the top item on my TODO before the reed-switch board, because a physical board with a human promoting a pawn will hit the same parsing issue from the firmware side.
After posting, I continued testing and found a second path of the very same E1-clobbering bug — this time on the inbound side, when validating the opponent's move.

Problem 3: the e.p. bug strikes move validation
In one of the games, after 13.f4 the engine rejected the reply e4f3 with "Illegal move" — an en-passant capture, our old acquaintance in a new disguise.

The mechanism:

the engine's own reply f2f4 is produced inside think(), which sets E1 = 85 (f3) directly — but ep_ext was only written in the move-apply routine used for opponent's moves, so it stayed 0;
PrintResult() runs its mate/stalemate check, which internally calls D() — and E1 is clobbered to 128 again, exactly as in Problem 2;
the opponent plays e4f3; the command handler restores E1 = ep_ext = 0 → the e.p. condition in is_move() fails → "Illegal move".
So there were two independent holes of the same disease: ep_ext not updated after the engine's own moves, and E1 not restored before validating inbound moves. Both fixed:

Code: Select all

/* at the bottom of think(), right after E1 is set from the search result: */
    if ((b[L]&7) < 3 && ((K-L == 32) || (L-K == 32)))
        E1 = (K+L)>>1;
    else
        E1 = 0;
    ep_ext = E1;                       /* own double pawn moves are remembered too */

/* in the Winboard command handler, before validating the incoming move: */
    E1 = ep_ext;
    GamePtr = 0;
    is_move();
After both patches, the position from the lost game was replayed (FEN r1bqr1k1/p1p2pbp/6p1/2ppN2n/P2Pp3/2N1P2P/1PP2PP1/R1BQR1K1 w - - 2 13), the engine answered f2f4, the reply e4f3 was accepted, and the game continued normally.

The lesson from Problem 2 now has two proofs: in micro-Max's code, globals like E1, K, L, O are workspace, not state — every place that reads E1 outside the search (book lookup, move validation, repetition logic) needs its own honest restore from a dedicated variable that D() never touches.

One terminology note
Worth clarifying for anyone following along: is_move() produces a pseudo-legal move list, not a legal one — it enumerates all geometrically possible moves for the side to move (including e.p. captures and promotions), but does not itself verify that the king is not left in check. The final king-safety decision is made later, by the search's own machinery. In practice this is sufficient for validating book moves, since Polyglot books generated from real games never contain moves that hang the king — but the distinction is worth keeping in mind when reading micro-Max-derived code.

And the honest scoreboard
I ran some tournaments with the port — and lost every game to real engines. But winning was never the point: this whole exercise is one of the debugging stages for a physical chess board built on reed switches, where the STM32 firmware has to recognize moves made by hand on the real board. A PC port that behaves identically to the hardware is the cheapest way to shake out protocol, book, and state-tracking bugs before they show up as misreads on physical hardware. The real opponent here was never another engine — it is the reed-switch matrix waiting for the port to be trusted.