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

Discussion of chess software programming and technical issues.

Moderator: Ras

Alexlaw1964
Posts: 44
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;
    }
}