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

Discussion of chess software programming and technical issues.

Moderator: Ras

Alexlaw1964
Posts: 34
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 »

Something went wrong. The pawn moves on two squares have disappeared.
Alexlaw1964
Posts: 34
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 »

Все работает, спасибо thomasahle.

Code: Select all

#include <stdio.h>
#define W while
#define WHITE 8
#define BLACK 16
//char *FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk";
char *FEN = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1 ";//48
//char *FEN = "rnbqkb1r/ppppp1pp/7n/4Pp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3";//31
//char *FEN = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -";
//char *FEN = "8/2p5/3p3k/1P5r/1R3pP1/1K6/4P3/8 b - g3";
char o[]={-16,-15,-17,0,1,16,0,1,16,15,17,0,14,18,31,33,0, /* step-vector lists */
 7,-1,11,6,8,3,6, /* 1st dir. in o[] per piece*/
 6,3,5,7,4,5,3,6}; /* initial piece setup */
char n[]=".?+nkbrq?*?NKBRQ"; /* piece symbols on printout*/
char b[129],t,x,y,u,p,H;
int M=136,r;
int K,N,cnt;
int E,S=128,V=112; /*E=e.p. sqr.*/
int ReadFEN(char *FEN){
 int row, file, i, col, nr, cc;
 char c, *p, epSqr[2];
 p = FEN;
 for(i=0; i<128; i++) b[i&0x77]=0;
 nr=0;
 cc=0;
 E=0;
 for(row=7; row>=0; row--)
    {   /* read one row of the 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;
            //default: return -15;
            }
          file++;
          }
          }while(file < 8);
        if(file >  8) return (-10); /* bad format */
        if(file == 8)
        {   c = *p++;
            nr+=8;
            if(row > 0 && c != '/') return(-10); /* bad format */
            if(row==0  && c != ' ') return (-10); /* bad format */
        }
    }
        while(c = *p++)
    {
        if(c>='0' && c<='9') continue; /* ignore move counts */
        if(c>='a' && c<='h') /* might be e.p. square */
        {    if(*p == '3' || *p == '6')
             {
                 epSqr[0]=c;
                 epSqr[1]=*p;
                 E=((8 - (epSqr[1] - '0')) * 16) + (epSqr[0] - 'a');
                 p++;
                 continue;
             }
             //else if(c != 'b') continue;
        }
        switch(c)
        {
        case 'K':
        case 'Q':
        case 'k':
        case 'g':b[7*(c&34^32)/2] |= c&64;break;
        case 'w': col = WHITE; break;
        case 'b': col = BLACK; break;
        case ' ':
        case '-': break;
        //default: return -10;
        }
    }

return col;
}
int main()
{
 int j,k=8;//k=8; белые k=16; черные

 K=8;W(K--)
 {
     b[K]=(b[K+112]=o[K+24]+8)+8;
     b[K+16]=18;
     b[K+96]=9;  /* initial board setup*/

 }
 k=ReadFEN(FEN);
   W(1)
 {
   N=-1;W(++N<121)
   printf(" %c",N&8&&(N+=7)?10:n[b[N]&15]);          /* print board        */
 W((getchar())>10); /* read input line */

 x=0;cnt=0;
 do{
         u=b[x]; /* scan board looking for */
         if(u&k) /* own piece (inefficient!)*/
 {r=p=u&7; /* p = piece type (set r>0) */
 j=o[p+16]; /* first step vector f.piece*/
 while(r=p>2&r<0?-r:-o[++j]) /* loop over directions o[] */
 {
    y=x;
    do{
      H=y+=r; /* y traverses ray */
      if(y&M)break; /* board edge hit */
      if(p<3&y==E)H=y^16; /* shift capt.sqr. H if e.p.*/
      t=b[H];
      if(t&k|p<3&!(r&7)!=!t)break; /* capt. own, bad pawn mode */
      cnt++;
      printf("%c%c%c%c\n",
       'a' + (x % 16),
       '0' + (8 - (x / 16)),
       'a' + (y % 16),
       '0' + (8 - (y / 16))
      );
      t += p<5
   ^ p<3 & 6*k+(y&V)==S
   ^ j<8 & y
     & b[H=x^3^r>>1&7]==u+66
     & !b[H^1] & !b[H^2];

    }while(!t); /* if not capt. continue ray*/
 }
 }}W(x=x+9&~M);/* next sqr. of board, wrap */
 printf("cnt=%d\n",cnt);
  }
}

User avatar
hgm
Posts: 28540
Joined: Fri Mar 10, 2006 10:06 am
Location: Amsterdam
Full name: H G Muller

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

Post by hgm »

Well, the code by Thomas is not what there originally was in the micro-Max code, and he might have simplified it too much, or just made a typo in posting it here.
Alexlaw1964
Posts: 34
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 »

hgm wrote: ↑Sun Aug 23, 2026 12:31 pm Well, the code by Thomas is not what there originally was in the micro-Max code, and he might have simplified it too much, or just made a typo in posting it here.
Alexlaw1964 wrote: ↑Sun Aug 23, 2026 5:10 am Все работает, спасибо thomasahle.

Code: Select all

#include <stdio.h>
#define W while
#define WHITE 8
#define BLACK 16
//char *FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w QKqk";
char *FEN = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1 ";//48
//char *FEN = "rnbqkb1r/ppppp1pp/7n/4Pp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3";//31
//char *FEN = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -";
//char *FEN = "8/2p5/3p3k/1P5r/1R3pP1/1K6/4P3/8 b - g3";
char o[]={-16,-15,-17,0,1,16,0,1,16,15,17,0,14,18,31,33,0, /* step-vector lists */
 7,-1,11,6,8,3,6, /* 1st dir. in o[] per piece*/
 6,3,5,7,4,5,3,6}; /* initial piece setup */
char n[]=".?+nkbrq?*?NKBRQ"; /* piece symbols on printout*/
char b[129],t,x,y,u,p,H;
int M=136,r;
int K,N,cnt;
int E,S=128,V=112; /*E=e.p. sqr.*/
int ReadFEN(char *FEN){
 int row, file, i, col, nr, cc;
 char c, *p, epSqr[2];
 p = FEN;
 for(i=0; i<128; i++) b[i&0x77]=0;
 nr=0;
 cc=0;
 E=0;
 for(row=7; row>=0; row--)
    {   /* read one row of the 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;
            //default: return -15;
            }
          file++;
          }
          }while(file < 8);
        if(file >  8) return (-10); /* bad format */
        if(file == 8)
        {   c = *p++;
            nr+=8;
            if(row > 0 && c != '/') return(-10); /* bad format */
            if(row==0  && c != ' ') return (-10); /* bad format */
        }
    }
        while(c = *p++)
    {
        if(c>='0' && c<='9') continue; /* ignore move counts */
        if(c>='a' && c<='h') /* might be e.p. square */
        {    if(*p == '3' || *p == '6')
             {
                 epSqr[0]=c;
                 epSqr[1]=*p;
                 E=((8 - (epSqr[1] - '0')) * 16) + (epSqr[0] - 'a');
                 p++;
                 continue;
             }
             //else if(c != 'b') continue;
        }
        switch(c)
        {
        case 'K':
        case 'Q':
        case 'k':
        case 'g':b[7*(c&34^32)/2] |= c&64;break;
        case 'w': col = WHITE; break;
        case 'b': col = BLACK; break;
        case ' ':
        case '-': break;
        //default: return -10;
        }
    }

return col;
}
int main()
{
 int j,k=8;//k=8; белые k=16; черные

 K=8;W(K--)
 {
     b[K]=(b[K+112]=o[K+24]+8)+8;
     b[K+16]=18;
     b[K+96]=9;  /* initial board setup*/

 }
 k=ReadFEN(FEN);
   W(1)
 {
   N=-1;W(++N<121)
   printf(" %c",N&8&&(N+=7)?10:n[b[N]&15]);          /* print board        */
 W((getchar())>10); /* read input line */

 x=0;cnt=0;
 do{
         u=b[x]; /* scan board looking for */
         if(u&k) /* own piece (inefficient!)*/
 {r=p=u&7; /* p = piece type (set r>0) */
 j=o[p+16]; /* first step vector f.piece*/
 while(r=p>2&r<0?-r:-o[++j]) /* loop over directions o[] */
 {
    y=x;
    do{
      H=y+=r; /* y traverses ray */
      if(y&M)break; /* board edge hit */
      if(p<3&y==E)H=y^16; /* shift capt.sqr. H if e.p.*/
      t=b[H];
      if(t&k|p<3&!(r&7)!=!t)break; /* capt. own, bad pawn mode */
      cnt++;
      printf("%c%c%c%c\n",
       'a' + (x % 16),
       '0' + (8 - (x / 16)),
       'a' + (y % 16),
       '0' + (8 - (y / 16))
      );
      t += p<5
   ^ p<3 & 6*k+(y&V)==S
   ^ j<8 & y
     & b[H=x^3^r>>1&7]==u+66
     & !b[H^1] & !b[H^2];

    }while(!t); /* if not capt. continue ray*/
 }
 }}W(x=x+9&~M);/* next sqr. of board, wrap */
 printf("cnt=%d\n",cnt);
  }
}

The code written by Thomas works correctly.
What’s surprising is something else.
If I use this code, it doesn’t fit in the Attiny4313.
If I use my own version, it even fits in the Attiny2313.
Alexlaw1964
Posts: 34
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 »

Reverse conversion from a position to FEN.
I’m a bit confused.
How does the color of “w” and “b” change when you enter “enter” and when you enter a move?
And also, please help with the castling again.

Code: Select all

//n[]=".?+nkbrq?*?NKBRQ";
//char fen[128];
//int M=136,S=128,I=8e3,C=799,Q,O,K,N,R,J,Z; /* M=0x88 */
void board_to_fen() {
    char *ptr = fen;
    char piece;
    int emptyCount = 0;
 N=-1;W(++N<121) {
 piece = N&8&&(N+=7)?10:n[b[N]&15];
if (ptr >= fen + 86){*ptr = '\0';return;}
if (piece == '?')continue;
        switch (piece) {
            case 10:  // Moving to the next horizontal
                if (emptyCount > 0) {
                    *ptr++ = emptyCount + '0';
                    emptyCount = 0;
                }
                 if (N < 112) {
                    *ptr++ = '/';
                }
                break;

            case '.':  // Empty field
                emptyCount++;
                break;

            case '+':  // Black pawn
                if (emptyCount > 0) {
                    *ptr++ = emptyCount + '0';
                    emptyCount = 0;
                }
                *ptr++ = 'p';
                break;

            case '*':  // White pawn
                if (emptyCount > 0) {
                    *ptr++ = emptyCount + '0';
                    emptyCount = 0;
                }
                *ptr++ = 'P';
                break;

            default:  // The other figures
                if (emptyCount > 0) {
                    *ptr++ = emptyCount + '0';
                    emptyCount = 0;
                }
                *ptr++ = piece;
                break;
        }
    }
    if (k==8)ptr += sprintf(ptr, " w "); else ptr +=sprintf(ptr, " b ");
    ptr += sprintf(ptr, "- ");
    if (O!=128){
       *ptr++ = 'a'+(O&7);
       *ptr++ = '8'-(O>>4);
       *ptr++ =  ' ';}
     else ptr +=sprintf(ptr, "- ");
    // Adding the remaining FEN fields
    ptr += sprintf(ptr, "0 1");

    // End the line with a zero
    *ptr = '\0';
printf("%s",fen);
}
Alexlaw1964
Posts: 34
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 »

Using AI, I added a book of openings.
https://brewpage.app/public/cyzEXQVI4b?p=BwDG
Alexlaw1964
Posts: 34
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 »

I have compiled the book. The first 5 moves in accordance with the book.
https://brewpage.app/public/5LXuLk7i5E
stockfish_windowsVSmicro_Max_STM32
[pgn][Event "Computer Schach Partie"]
[Site "LENOVO-PC-LAW"]
[Date "????.??.??"]
[Round "?"]
[White "stockfish 15.1"]
[Black "stm32"]
[Result "1-0"]
[WhiteElo "?"]
[BlackElo "?"]
[Variant "Standard"]
[TimeControl "-"]
[ECO "B50"]
[Opening "Sicilian Defense: Modern Variations"]
[Termination "Normal"]
[Annotator "lichess.org"]

1. e4 c5 2. Nf3 d6 { B50 Sicilian Defense: Modern Variations } 3. Nc3 e5 4. a4 Be7 5. Bc4 Nf6 6. Ng5 O-O 7. a5 Nc6 8. a6 bxa6 9. Nd5 Nxd5 10. Bxd5 Qb6 11. h4 Bxg5 12. hxg5 g6 13. b3 Be6 14. Bxc6 Qxc6 15. d3 f5 16. gxf6 Rxf6 17. Bh6 Rf7 18. Qd2 Rb8 19. Qe3 Ra8 20. Qd2 Rb8 21. Qc3 Rbb7 22. Ke2 Bg4+ 23. Ke3 Be6 24. Qa5 Rb6 25. f3 d5 26. Qd2 d4+ 27. Kf2 Qd6 28. Qg5 Re7 29. Ra5 Re8 30. Qf6 Rb7 31. Rha1 Rf7 32. Rxa6 Qxa6 33. Qxf7+ Bxf7 34. Rxa6 Re7 35. Bg5 Rd7 36. Bf6 Kf8 37. Rc6 Bxb3 38. cxb3 Rb7 39. Rxc5 Rxb3 40. Ra5 Rxd3 41. Ra2 Kf7 42. Bxe5 Ke6 43. Bf4 Rb3 44. Rxa7 Rb2+ 45. Kg1 h5 46. Ra4 Rb1+ 47. Kh2 Rd1 48. Ra2 Rb1 49. Ra7 Rd1 50. Ra8 Rb1 51. Ra5 Rd1 52. Rb5 Ra1 53. Rd5 Rd1 54. Rd6+ Kf7 55. Kg3 d3 56. Kf2 Kg7 57. Bg5 Kf7 58. e5 Kg7 59. e6 Kf8 60. Bf6 Rd2+ 61. Kg3 h4+ 62. Kf4 g5+ 63. Kf5 Kg8 64. Kg6 Rxg2 65. e7 Re2 66. Rd8# { White wins by checkmate. } 1-0[/pgn]
Alexlaw1964
Posts: 34
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 »

chess_book_builder.py

Code: Select all

# chess_book_builder.py v3.9
# Генератор дебютной книги для Micro-Max на STM32
# v3.9: depth 14 (black) / 12 (white), multipv 5, таймер с прогнозом
import chess
import chess.engine
import random
import re
import os
import json
import glob
import sys
import time

STOCKFISH_PATH = r"C:\telnet_server\stockfish-windows-2022-x86-64-modern.exe"
DEPTH_BLACK = 14
DEPTH_WHITE = 12
BOOK_SIZE = 1500
SEED = 42
ZOBRIST_HEADER = "zobrist_table.h"
THRESHOLD_CP = 40

random.seed(SEED)

def find_zobrist_file():
    script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
    search_dirs = [script_dir, os.getcwd()]
    for d in search_dirs:
        p = os.path.join(d, ZOBRIST_HEADER)
        if os.path.exists(p):
            return p
        for sub in glob.glob(os.path.join(d, "**", ZOBRIST_HEADER), recursive=True):
            if os.path.exists(sub):
                return sub
    return None

def find_stockfish():
    if os.path.exists(STOCKFISH_PATH):
        return STOCKFISH_PATH
    script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
    search_dirs = [script_dir, os.getcwd(), os.path.dirname(script_dir)]
    for d in search_dirs:
        for pattern in ["stockfish*.exe", "Stockfish*.exe"]:
            matches = glob.glob(os.path.join(d, pattern))
            if matches:
                return matches[0]
            for sub in glob.glob(os.path.join(d, "**", pattern), recursive=True):
                if os.path.exists(sub):
                    return sub
    return None

def load_zobrist_from_header(filename):
    if not os.path.exists(filename):
        print(f"ОШИБКА: файл {filename} не найден!")
        sys.exit(1)
    with open(filename, "r") as f:
        content = f.read()
    numbers = re.findall(r'0x[0-9A-Fa-f]+ULL', content)
    return [int(n.replace('ULL', ''), 16) for n in numbers]

MM_TO_STD = [0, 1, 1, 2, 6, 3, 4, 5]

def get_mm_type(piece):
    if piece.piece_type == chess.PAWN:
        return 2 if piece.color else 1
    elif piece.piece_type == chess.KNIGHT:
        return 3
    elif piece.piece_type == chess.BISHOP:
        return 5
    elif piece.piece_type == chess.ROOK:
        return 6
    elif piece.piece_type == chess.QUEEN:
        return 7
    elif piece.piece_type == chess.KING:
        return 4
    return 0

def zobrist_key_stm32(board, ZT):
    key = 0
    for sq in chess.SQUARES:
        piece = board.piece_at(sq)
        if piece is None:
            continue
        mm_type = get_mm_type(piece)
        std_type = MM_TO_STD[mm_type]
        color = 0 if piece.color else 1
        file_ = chess.square_file(sq)
        rank_ = chess.square_rank(sq)
        sq063 = rank_ * 8 + file_
        z_index = color * 384 + (std_type - 1) * 64 + sq063
        key ^= ZT[z_index]
    if not board.turn:
        key ^= ZT[768]
    if board.castling_rights & chess.BB_H1:
        key ^= ZT[769]
    if board.castling_rights & chess.BB_A1:
        key ^= ZT[770]
    if board.castling_rights & chess.BB_H8:
        key ^= ZT[771]
    if board.castling_rights & chess.BB_A8:
        key ^= ZT[772]
    if board.ep_square is not None:
        key ^= ZT[773 + chess.square_file(board.ep_square)]
    return key & 0xFFFFFFFF

def move_to_polyglot(move):
    file_from = chess.square_file(move.from_square)
    rank_from = chess.square_rank(move.from_square)
    file_to = chess.square_file(move.to_square)
    rank_to = chess.square_rank(move.to_square)
    sq063_from = rank_from * 8 + file_from
    sq063_to = rank_to * 8 + file_to
    return (sq063_from & 0x3F) | ((sq063_to & 0x3F) << 6)

def build_board(moves):
    b = chess.Board()
    for m in moves:
        if isinstance(m, str):
            b.push(chess.Move.from_uci(m))
        else:
            b.push(m)
    return b

def best_black_move(board, engine):
    """Один лучший ход чёрных через engine.play()"""
    result = engine.play(board, chess.engine.Limit(depth=DEPTH_BLACK))
    return result.move

def filtered_white_moves(board, engine):
    """Ходы белых в пределах THRESHOLD_CP от лучшего, multipv=5"""
    results = engine.analyse(board, chess.engine.Limit(depth=DEPTH_WHITE), multipv=5)
    if not results:
        return []
    best_score = results[0]["score"].white().score()
    if best_score is None:
        best_score = 0
    good = []
    for r in results:
        s = r["score"].white().score()
        if s is None:
            s = 0
        if abs(s - best_score) <= THRESHOLD_CP:
            good.append(r["pv"][0])
    return good

def san_line(moves):
    b = chess.Board()
    parts = []
    for i, m in enumerate(moves):
        if isinstance(m, str):
            m = chess.Move.from_uci(m)
        if i % 2 == 0:
            parts.append(f"{i // 2 + 1}.")
        parts.append(b.san(m))
        b.push(m)
    return " ".join(parts)

def fmt_time(sec):
    if sec < 60:
        return f"{sec:.0f}с"
    m = int(sec // 60)
    s = sec % 60
    return f"{m}мин {s:.0f}с"

def generate_book(engine, ZT):
    book = []
    lines = []
    seen_keys = set()
    t0 = time.time()

    print("=" * 60)
    print(f"  chess_book_builder.py v3.9")
    print(f"  Размер: {BOOK_SIZE} позиций")
    print(f"  Глубина: чёрные={DEPTH_BLACK}, белые={DEPTH_WHITE}")
    print(f"  Фильтр белых: multipv=5, порог {THRESHOLD_CP}cp")
    print(f"  Логика: белые расширяют дерево, чёрные = 1 лучший ход")
    print(f"  Дебюты: e4, d4")
    print("=" * 60)
    print()

    def add_position(board, move_list, ply_label):
        if board.is_game_over():
            return False
        if len(book) >= BOOK_SIZE:
            return False
        key = zobrist_key_stm32(board, ZT)
        if key in seen_keys:
            return False
        seen_keys.add(key)
        # Один лучший ход чёрных
        best_move = best_black_move(board, engine)
        uci = best_move.uci()
        san = board.san(best_move)
        pg_move = move_to_polyglot(best_move)
        book.append((key, uci, san, pg_move, 1))
        full_line = move_list + [uci]
        line_name = san_line(full_line) + f" [0x{key:08X}]"
        lines.append((line_name, full_line, key))
        if len(book) % 50 == 0:
            elapsed = time.time() - t0
            print(f"  [{len(book):4d}/{BOOK_SIZE}] {ply_label} | {san} -> 0x{pg_move:04X} | {fmt_time(elapsed)}")
        return True

    # Очередь позиций для обработки: (board, move_list, ply)
    # Позиция = после хода белых, очередь хода чёрных
    from collections import deque
    queue = deque()

    # === ЭТАП 1: ply 1 — стартовые ходы белых ===
    print("Этап 1: стартовые ходы белых (ply 1)...")
    main_openings = ["e2e4", "d2d4"]
    for m1_uci in main_openings:
        b = chess.Board()
        b.push(chess.Move.from_uci(m1_uci))
        queue.append((b, [m1_uci], 1))

    # Обрабатываем очередь: для каждой позиции — лучший ход чёрных в книгу,
    # затем ходы белых для следующего этапа
    stage = 1
    max_ply = 11

    while queue and len(book) < BOOK_SIZE:
        stage_positions = len(queue)
        stage_start = time.time()
        next_queue = deque()

        while queue and len(book) < BOOK_SIZE:
            board, move_list, ply = queue.popleft()
            added = add_position(board, move_list, f"ply {ply}")
            if not added:
                continue
            # Делаем лучший ход чёрных
            key = zobrist_key_stm32(board, ZT)
            # Находим запись в книге
            entry = None
            for e in book:
                if e[0] == key:
                    entry = e
                    break
            if entry is None:
                continue
            black_uci = entry[1]
            b2 = build_board(move_list + [black_uci])
            # Получаем ходы белых для расширения
            if ply < max_ply:
                w_moves = filtered_white_moves(b2, engine)
                for wm in w_moves:
                    wm_uci = wm.uci()
                    b3 = build_board(move_list + [black_uci, wm_uci])
                    next_queue.append((b3, move_list + [black_uci, wm_uci], ply + 2))

        stage_time = time.time() - stage_start
        total_time = time.time() - t0
        done = len(book)
        remaining = BOOK_SIZE - done
        if stage_positions > 0 and done > 0:
            sec_per_pos = total_time / done
            forecast = sec_per_pos * remaining
            print(f"  После этапа {stage} (ply {2*stage-1}): {done} поз. | {fmt_time(stage_time)} | всего {fmt_time(total_time)} | прогноз: ~{fmt_time(forecast)} осталось")
        else:
            print(f"  После этапа {stage} (ply {2*stage-1}): {done} поз. | {fmt_time(stage_time)}")
        print()

        queue = next_queue
        stage += 1

    engine.quit()

    # Статистика
    print("=" * 60)
    print(f"  Готово! Позиций: {len(book)}")
    print(f"  Время: {fmt_time(time.time() - t0)}")
    print(f"  Размер во Flash: ~{len(book) * 7} байт ({len(book) * 7 / 1024:.1f} КБ)")
    ply_counts = {}
    for ln in lines:
        num_moves = len(ln[1])
        ply = num_moves - 1
        ply_counts[ply] = ply_counts.get(ply, 0) + 1
    print(f"\n  Статистика по этапам:")
    for ply in sorted(ply_counts.keys()):
        print(f"    ply {ply}: {ply_counts[ply]} позиций")
    print("=" * 60)

    return book, lines

def write_debut_book_c(book, filename="debut_book.c"):
    script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
    filepath = os.path.join(script_dir, filename)
    book.sort(key=lambda x: x[0])
    with open(filepath, "w") as f:
        f.write('#include "polyglot.h"\n\n')
        f.write("const polyglot_entry_t debut_book[] = {\n")
        for entry in book:
            key = entry[0]
            pg = entry[3]
            weight = entry[4]
            f.write(f"\t{{0x{key:08X}U, 0x{pg:04X}U, {weight}}},\n")
        f.write("};\n\n")
        f.write(f"const int debut_book_size = {len(book)};\n")
    print(f"\nФайл {filepath} записан ({len(book)} записей, отсортировано)")

def write_json_session(book, lines, filename="book_session.json"):
    script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
    filepath = os.path.join(script_dir, filename)
    book_entries = []
    for entry in book:
        book_entries.append([int(entry[0]), entry[1], entry[2], int(entry[3]), int(entry[4])])
    json_lines = []
    for ln in lines:
        json_lines.append([ln[0], ln[1], int(ln[2])])
    session = {
        "version": "3.9",
        "book": book_entries,
        "lines": json_lines
    }
    with open(filepath, "w", encoding="utf-8") as f:
        json.dump(session, f, ensure_ascii=False, indent=2)
    if book_entries:
        first_key = book_entries[0][0]
        print(f"[DEBUG] Первый ключ в JSON: 0x{first_key:08X} (int={first_key})")
    if lines and book_entries:
        line_key = int(lines[0][2])
        found = any(be[0] == line_key for be in book_entries)
        if found:
            print(f"[OK] Хэш из первой линии 0x{line_key:08X} найден в книге")
        else:
            print(f"[WARN] Хэш из первой линии 0x{line_key:08X} НЕ найден в книге")
    print(f"Файл {filepath} записан ({len(book_entries)} записей, {len(json_lines)} линий)")

def main():
    ZT_path = find_zobrist_file()
    if ZT_path is None:
        print("ОШИБКА: файл zobrist_table.h не найден!")
        script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
        print(f"  Искали в: {script_dir}")
        print(f"  Искали в: {os.getcwd()}")
        sys.exit(1)
    ZT = load_zobrist_from_header(ZT_path)
    print(f"Загружено {len(ZT)} Zobrist-чисел из {ZT_path}")

    sf_path = find_stockfish()
    if sf_path is None:
        print("ОШИБКА: Stockfish не найден!")
        script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
        print(f"  Искали в: {script_dir}")
        print(f"  Искали в: {os.getcwd()}")
        print(f"  Жёсткий путь: {STOCKFISH_PATH}")
        sys.exit(1)
    print(f"Stockfish: {sf_path}")

    engine = chess.engine.SimpleEngine.popen_uci(sf_path)
    print(f"Stockfish запущен\n")

    book, lines = generate_book(engine, ZT)
    write_debut_book_c(book)
    write_json_session(book, lines)

if __name__ == "__main__":
    main()
Alexlaw1964
Posts: 34
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 »

chess_book_editor.py

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
chess_book_editor.py — Ручной редактор дебютной книги для STM32
Версия: 4.9
GUI: tkinter, доска с шрифтом gc2004d, тёмная тема
Локализация: chess_book_editor_lang.py
"""

import os
import sys
import json
import re
import tkinter as tk
from tkinter import font as tkfont
from tkinter import filedialog, messagebox, ttk
import chess
import chess.pgn

try:
    from chess_book_editor_lang import LANG, tr, set_lang, get_lang
except ImportError:
    LANG = {}
    def tr(key, default=None):
        return default or key
    def set_lang(l):
        pass
    def get_lang():
        return 'ru'

# ====================== ZOBRIST ======================

ZOBRIST_NUMBERS = []
ZOBRIST_TABLE_H = ""

def load_zobrist_table(path):
    global ZOBRIST_NUMBERS, ZOBRIST_TABLE_H
    numbers = []
    try:
        with open(path, "r", encoding="utf-8") as f:
            content = f.read()
        ZOBRIST_TABLE_H = content
        hex_vals = re.findall(r'0x[0-9A-Fa-f]{8}', content)
        for h in hex_vals:
            numbers.append(int(h, 16))
        if len(numbers) < 100:
            print(f"[WARN] Only {len(numbers)} Zobrist numbers found")
    except Exception as e:
        print(f"[ERROR] Cannot load zobrist_table.h: {e}")
        numbers = []
    ZOBRIST_NUMBERS = numbers
    return numbers

def find_zobrist_file():
    # Search in current dir
    cwd = os.getcwd()
    for name in ["zobrist_table.h", "zobrist.h"]:
        p = os.path.join(cwd, name)
        if os.path.exists(p):
            return p
    # Search in subdirs of current dir
    try:
        for d in os.listdir(cwd):
            full = os.path.join(cwd, d)
            if os.path.isdir(full):
                for name in ["zobrist_table.h", "zobrist.h"]:
                    p = os.path.join(full, name)
                    if os.path.exists(p):
                        return p
    except:
        pass
    # Search in script dir
    script_dir = os.path.dirname(os.path.abspath(__file__))
    for name in ["zobrist_table.h", "zobrist.h"]:
        p = os.path.join(script_dir, name)
        if os.path.exists(p):
            return p
    # Search in subdirs of script dir
    try:
        for d in os.listdir(script_dir):
            full = os.path.join(script_dir, d)
            if os.path.isdir(full):
                for name in ["zobrist_table.h", "zobrist.h"]:
                    p = os.path.join(full, name)
                    if os.path.exists(p):
                        return p
    except:
        pass
    return None

def compute_zobrist(board):
    if not ZOBRIST_NUMBERS:
        return 0
    key = 0
    for sq in range(64):
        piece = board.piece_at(sq)
        if piece is not None:
            piece_type = piece.piece_type
            color = piece.color
            piece_idx = (piece_type - 1) * 2 + (0 if color else 1)
            zob_idx = piece_idx * 64 + sq
            if zob_idx < len(ZOBRIST_NUMBERS):
                key ^= ZOBRIST_NUMBERS[zob_idx]
    if board.turn == chess.BLACK:
        key ^= ZOBRIST_NUMBERS[-1] if len(ZOBRIST_NUMBERS) > 0 else 0
    castling_idx = len(ZOBRIST_NUMBERS) - 5
    if castling_idx + 3 < len(ZOBRIST_NUMBERS):
        if board.has_kingside_castling_rights(chess.WHITE):
            key ^= ZOBRIST_NUMBERS[castling_idx]
        if board.has_queenside_castling_rights(chess.WHITE):
            key ^= ZOBRIST_NUMBERS[castling_idx + 1]
        if board.has_kingside_castling_rights(chess.BLACK):
            key ^= ZOBRIST_NUMBERS[castling_idx + 2]
        if board.has_queenside_castling_rights(chess.BLACK):
            key ^= ZOBRIST_NUMBERS[castling_idx + 3]
    return key & 0xFFFFFFFF

def move_to_polyglot(move, board):
    from_sq = move.from_square
    to_sq = move.to_square
    ff = chess.square_file(from_sq)
    fr = chess.square_rank(from_sq)
    tf = chess.square_file(to_sq)
    tr = chess.square_rank(to_sq)
    pg = (fr << 9) | (ff << 6) | (tr << 3) | tf
    if move.promotion:
        promo_map = {chess.QUEEN: 0, chess.ROOK: 1, chess.BISHOP: 2, chess.KNIGHT: 3}
        pg |= (promo_map.get(move.promotion, 0) << 12)
    return pg & 0xFFFF

# ====================== HELPERS ======================

def _safe_key(val):
    """Convert any key format to int."""
    if isinstance(val, int):
        return val
    if isinstance(val, float):
        return int(val)
    if isinstance(val, str):
        s = val.strip()
        if s.startswith("0x") or s.startswith("0X"):
            return int(s, 16)
        return int(s)
    return 0

def _safe_int(val):
    """Convert any int-like value to int."""
    if isinstance(val, int):
        return val
    if isinstance(val, float):
        return int(val)
    if isinstance(val, str):
        s = val.strip()
        if s.startswith("0x") or s.startswith("0X"):
            return int(s, 16)
        return int(s)
    return 0

# ====================== ВИЗУАЛИЗАЦИЯ ДОСКИ (gc2004d) ======================

def board_to_visual_chars(board):
    f = {0: 163, 1: 48, 2: 71, 3: 87, 4: 109, 5: 169, 6: 185}
    piece_map = {'K': 1, 'Q': 2, 'R': 3, 'B': 4, 'N': 5, 'P': 6}
    chars = []
    n = 0
    for rank in range(7, -1, -1):
        for file_idx in range(8):
            piece = board.piece_at(chess.square(file_idx, rank))
            if piece is None:
                chars.append(chr(f[0] + n))
            elif piece.symbol().islower():
                chars.append(chr(f[piece_map[piece.symbol().upper()]] + n + 2))
            else:
                chars.append(chr(f[piece_map[piece.symbol().upper()]] + n))
            n = 1 - n
        n = 1 - n
    return chars

# ====================== GUI ======================

CELL_SIZE = 40
LIGHT_COLOR = "#f0d9b5"
DARK_COLOR = "#b58863"
BG_COLOR = "#2b2b2b"
PANEL_COLOR = "#3c3f41"
ACCENT_COLOR = "#4a9eff"
TEXT_COLOR = "#e0e0e0"

class ChessBookEditor:
    def __init__(self, root):
        self.root = root
        root.title(tr('title', 'Chess Book Editor v4.9 - STM32'))
        root.geometry("1200x720")
        root.configure(bg=BG_COLOR)

        self.board = chess.Board()
        self.book = []
        self.lines = []
        self.current_line_moves = []
        self.selected_square = None
        self.last_move = None
        self.auto_add = tk.BooleanVar(value=True)

        self.search_var = tk.StringVar()
        self.search_results = []
        self.search_idx = 0

        self.lines_search_var = tk.StringVar()
        self.lines_search_results = []
        self.lines_search_idx = 0

        # PGN playback state
        self.pgn_moves = []
        self.pgn_index = 0
        self.pgn_game = None

        # Create font once
        try:
            self.piece_font = tkfont.Font(family="gc2004d", size=28)
        except tk.TclError:
            try:
                self.piece_font = tkfont.Font(family="DejaVu Sans", size=28)
            except tk.TclError:
                self.piece_font = tkfont.Font(family="Arial", size=24)

        # Widget references for language switching
        self.widgets = {}

        style = ttk.Style()
        style.theme_use('clam')
        style.configure('Dark.TFrame', background=BG_COLOR)
        style.configure('Dark.TLabel', background=BG_COLOR, foreground=TEXT_COLOR, font=('Arial', 10))
        style.configure("Treeview", background=PANEL_COLOR, foreground=TEXT_COLOR, fieldbackground=PANEL_COLOR)
        style.configure("Treeview.Heading", background=PANEL_COLOR, foreground=TEXT_COLOR)

        self._build_ui()
        self._draw_board()

    def _build_ui(self):
        # Destroy existing widgets
        for w in self.root.winfo_children():
            w.destroy()

        LEFT_WIDTH = 340

        left = tk.Frame(self.root, bg=BG_COLOR, width=LEFT_WIDTH)
        left.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)
        left.pack_propagate(False)

        # Language selector
        lang_frame = tk.Frame(left, bg=BG_COLOR)
        lang_frame.pack(fill=tk.X, pady=(0, 5))
        tk.Label(lang_frame, text=tr('language', 'Language:'), bg=BG_COLOR, fg=TEXT_COLOR,
                 font=('Arial', 9)).pack(side=tk.LEFT)
        self.lang_var = tk.StringVar(value=get_lang())
        lang_cb = tk.OptionMenu(lang_frame, self.lang_var, 'ru', 'en',
                                command=self._switch_lang)
        lang_cb.config(bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT)
        lang_cb.pack(side=tk.LEFT, padx=5)

        board_frame = tk.Frame(left, bg=BG_COLOR)
        board_frame.pack()

        coords_left = tk.Canvas(board_frame, width=15, height=CELL_SIZE * 8,
                                bg=BG_COLOR, highlightthickness=0)
        coords_left.pack(side=tk.LEFT)
        for i in range(8):
            y = i * CELL_SIZE + CELL_SIZE // 2
            coords_left.create_text(7, y, text=str(8 - i), fill=TEXT_COLOR, font=('Arial', 9))

        self.canvas = tk.Canvas(board_frame, width=CELL_SIZE * 8, height=CELL_SIZE * 8,
                                 bg=LIGHT_COLOR, highlightthickness=0)
        self.canvas.pack(side=tk.LEFT)
        self.canvas.bind("<Button-1>", self._on_click)

        coords_bottom = tk.Canvas(left, width=CELL_SIZE * 8, height=15,
                                   bg=BG_COLOR, highlightthickness=0)
        coords_bottom.pack()
        for i in range(8):
            x = i * CELL_SIZE + CELL_SIZE // 2
            coords_bottom.create_text(x, 7, text=chr(ord('a') + i),
                                      fill=TEXT_COLOR, font=('Arial', 9))

        btn_frame = tk.Frame(left, bg=BG_COLOR)
        btn_frame.pack(fill=tk.X, pady=5)

        self.widgets['btn_new_line'] = tk.Button(btn_frame, text=tr('new_line', 'New line'),
            command=self._new_line, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9, 'bold'), relief=tk.FLAT)
        self.widgets['btn_new_line'].pack(fill=tk.X, pady=2)

        self.widgets['btn_undo'] = tk.Button(btn_frame, text=tr('undo', 'Undo move'),
            command=self._undo_move, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT)
        self.widgets['btn_undo'].pack(fill=tk.X, pady=2)

        self.widgets['btn_add'] = tk.Button(btn_frame, text=tr('add_to_book', '+ Add to book'),
            command=self._manual_add, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT)
        self.widgets['btn_add'].pack(fill=tk.X, pady=2)

        self.widgets['chk_auto'] = tk.Checkbutton(btn_frame, text=tr('auto_add', 'Auto-add'),
            variable=self.auto_add, bg=BG_COLOR, fg=TEXT_COLOR, selectcolor=PANEL_COLOR, font=('Arial', 9))
        self.widgets['chk_auto'].pack(fill=tk.X, pady=2)

        self.widgets['btn_pgn'] = tk.Button(btn_frame, text=tr('import_pgn', 'Import PGN'),
            command=self._import_pgn, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT)
        self.widgets['btn_pgn'].pack(fill=tk.X, pady=2)

        self.widgets['btn_reset'] = tk.Button(btn_frame, text=tr('reset_board', 'Reset board'),
            command=self._reset_board, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT)
        self.widgets['btn_reset'].pack(fill=tk.X, pady=2)

        # PGN navigation
        pgn_nav = tk.Frame(left, bg=BG_COLOR)
        pgn_nav.pack(fill=tk.X, pady=2)
        self.widgets['btn_pgn_first'] = tk.Button(pgn_nav, text="<<",
            command=self._pgn_first, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT, width=3)
        self.widgets['btn_pgn_first'].pack(side=tk.LEFT, padx=1)
        self.widgets['btn_pgn_prev'] = tk.Button(pgn_nav, text="<",
            command=self._pgn_prev, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT, width=3)
        self.widgets['btn_pgn_prev'].pack(side=tk.LEFT, padx=1)
        self.widgets['btn_pgn_next'] = tk.Button(pgn_nav, text=">",
            command=self._pgn_next, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT, width=3)
        self.widgets['btn_pgn_next'].pack(side=tk.LEFT, padx=1)
        self.widgets['btn_pgn_last'] = tk.Button(pgn_nav, text=">>",
            command=self._pgn_last, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT, width=3)
        self.widgets['btn_pgn_last'].pack(side=tk.LEFT, padx=1)
        self.widgets['btn_pgn_import'] = tk.Button(pgn_nav, text=tr('pgn_import_all', '>>>'),
            command=self._pgn_import_all, bg=ACCENT_COLOR, fg="white", font=('Arial', 9, 'bold'), relief=tk.FLAT, width=3)
        self.widgets['btn_pgn_import'].pack(side=tk.LEFT, padx=1)

        self.pgn_label = tk.Label(left, text="", bg=BG_COLOR, fg=TEXT_COLOR, font=('Arial', 8))
        self.pgn_label.pack(fill=tk.X)

        save_frame = tk.Frame(left, bg=BG_COLOR)
        save_frame.pack(fill=tk.X, pady=5)

        self.widgets['btn_save_c'] = tk.Button(save_frame, text=tr('save_c', 'Save debut_book.c'),
            command=self._save_c, bg=ACCENT_COLOR, fg="white", font=('Arial', 9, 'bold'), relief=tk.FLAT)
        self.widgets['btn_save_c'].pack(fill=tk.X, pady=2)

        self.widgets['btn_save_json'] = tk.Button(save_frame, text=tr('save_json', 'Save session (JSON)'),
            command=self._save_session, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT)
        self.widgets['btn_save_json'].pack(fill=tk.X, pady=2)

        self.widgets['btn_load_json'] = tk.Button(save_frame, text=tr('load_json', 'Load session (JSON)'),
            command=self._load_session, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 9), relief=tk.FLAT)
        self.widgets['btn_load_json'].pack(fill=tk.X, pady=2)

        # Fixed-width info label
        self.info_label = tk.Label(left, text="", bg=BG_COLOR, fg=TEXT_COLOR,
                                    font=('Consolas', 9), justify=tk.LEFT,
                                    anchor="nw", width=42, height=8)
        self.info_label.pack(fill=tk.X, pady=5)
        self._update_info()

        # Right panel
        right = tk.Frame(self.root, bg=BG_COLOR)
        right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5)

        search_frame = tk.Frame(right, bg=BG_COLOR)
        search_frame.pack(fill=tk.X, pady=(0, 5))

        self.widgets['lbl_search'] = tk.Label(search_frame, text=tr('search_key', 'Search by key:'),
            bg=BG_COLOR, fg=TEXT_COLOR, font=('Arial', 9))
        self.widgets['lbl_search'].pack(side=tk.LEFT)

        self.search_entry = tk.Entry(search_frame, textvariable=self.search_var,
                                      bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Consolas', 9),
                                      relief=tk.FLAT, width=20)
        self.search_entry.pack(side=tk.LEFT, padx=5)
        self.search_entry.bind('<Return>', self._do_search)

        self.widgets['btn_search'] = tk.Button(search_frame, text=tr('lines_search', 'Search'),
            command=self._do_search, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 8), relief=tk.FLAT)
        self.widgets['btn_search'].pack(side=tk.LEFT, padx=2)

        self.search_status = tk.Label(search_frame, text="", bg=BG_COLOR, fg=TEXT_COLOR,
                                       font=('Arial', 8))
        self.search_status.pack(side=tk.LEFT, padx=5)

        self.widgets['btn_search_prev'] = tk.Button(search_frame, text=tr('search_prev', '< Back'),
            command=self._search_prev, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 8), relief=tk.FLAT)
        self.widgets['btn_search_prev'].pack(side=tk.LEFT, padx=2)

        self.widgets['btn_search_next'] = tk.Button(search_frame, text=tr('search_next', 'Fwd >'),
            command=self._search_next, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 8), relief=tk.FLAT)
        self.widgets['btn_search_next'].pack(side=tk.LEFT, padx=2)

        # Book table
        table_frame = tk.Frame(right, bg=BG_COLOR)
        table_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))

        self.widgets['lbl_book'] = tk.Label(table_frame, text=tr('book_entries', 'Book entries'),
            bg=BG_COLOR, fg=ACCENT_COLOR, font=('Arial', 10, 'bold'))
        self.widgets['lbl_book'].pack(anchor=tk.W)

        self.tree = ttk.Treeview(table_frame, columns=("key", "move", "san", "pg"),
                                 show="headings", height=12)
        self.tree.heading("key", text=tr('col_key', 'Zobrist Key'))
        self.tree.heading("move", text=tr('col_move', 'UCI'))
        self.tree.heading("san", text=tr('col_san', 'SAN'))
        self.tree.heading("pg", text=tr('col_pg', 'Polyglot'))
        self.tree.column("key", width=120)
        self.tree.column("move", width=70)
        self.tree.column("san", width=70)
        self.tree.column("pg", width=80)
        self.tree.pack(fill=tk.BOTH, expand=True)
        self.tree.bind("<Double-1>", self._on_tree_double)
        self.tree.bind("<Delete>", self._on_tree_delete)

        # Lines
        lines_frame = tk.Frame(right, bg=BG_COLOR)
        lines_frame.pack(fill=tk.BOTH, expand=True, pady=(5, 0))

        self.widgets['lbl_lines'] = tk.Label(lines_frame, text=tr('lines', 'Lines'),
            bg=BG_COLOR, fg=ACCENT_COLOR, font=('Arial', 10, 'bold'))
        self.widgets['lbl_lines'].pack(anchor=tk.W)

        # Lines search
        lines_search_frame = tk.Frame(lines_frame, bg=BG_COLOR)
        lines_search_frame.pack(fill=tk.X, pady=(2, 5))

        self.widgets['lbl_lines_search'] = tk.Label(lines_search_frame, text=tr('lines_search_key', 'Search in lines:'),
            bg=BG_COLOR, fg=TEXT_COLOR, font=('Arial', 9))
        self.widgets['lbl_lines_search'].pack(side=tk.LEFT)

        self.lines_search_entry = tk.Entry(lines_search_frame, textvariable=self.lines_search_var,
                                      bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Consolas', 9),
                                      relief=tk.FLAT, width=20)
        self.lines_search_entry.pack(side=tk.LEFT, padx=5)
        self.lines_search_entry.bind('<Return>', self._do_lines_search)

        self.widgets['btn_lines_search'] = tk.Button(lines_search_frame, text=tr('lines_search', 'Search'),
            command=self._do_lines_search, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 8), relief=tk.FLAT)
        self.widgets['btn_lines_search'].pack(side=tk.LEFT, padx=2)

        self.lines_search_status = tk.Label(lines_search_frame, text="", bg=BG_COLOR, fg=TEXT_COLOR,
                                       font=('Arial', 8))
        self.lines_search_status.pack(side=tk.LEFT, padx=5)

        self.widgets['btn_lines_search_prev'] = tk.Button(lines_search_frame, text=tr('lines_search_prev', '< Back'),
            command=self._lines_search_prev, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 8), relief=tk.FLAT)
        self.widgets['btn_lines_search_prev'].pack(side=tk.LEFT, padx=2)

        self.widgets['btn_lines_search_next'] = tk.Button(lines_search_frame, text=tr('lines_search_next', 'Fwd >'),
            command=self._lines_search_next, bg=PANEL_COLOR, fg=TEXT_COLOR, font=('Arial', 8), relief=tk.FLAT)
        self.widgets['btn_lines_search_next'].pack(side=tk.LEFT, padx=2)

        self.lines_tree = ttk.Treeview(lines_frame, columns=("name", "moves"),
                                       show="tree", height=6)
        self.lines_tree.heading("#0", text=tr('lines', 'Lines'))
        self.lines_tree.pack(fill=tk.BOTH, expand=True)
        self.lines_tree.bind("<Double-1>", self._on_line_double)

        self._update_tree()
        self._update_lines_tree()

    def _switch_lang(self, lang):
        set_lang(lang)
        self.root.title(tr('title', 'Chess Book Editor v4.9 - STM32'))
        self._build_ui()
        self._draw_board()
        self._update_info()

    def _draw_board(self, highlight_sq=None):
        chars = board_to_visual_chars(self.board)
        self.canvas.delete("all")

        for r in range(8):
            for c in range(8):
                color = LIGHT_COLOR if (r + c) % 2 == 0 else DARK_COLOR
                x1, y1 = c * CELL_SIZE, r * CELL_SIZE
                x2, y2 = x1 + CELL_SIZE, y1 + CELL_SIZE
                self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline="")

        if self.last_move:
            for sq in [self.last_move.from_square, self.last_move.to_square]:
                f = chess.square_file(sq)
                r_sq = chess.square_rank(sq)
                col = f
                row = 7 - r_sq
                x1, y1 = col * CELL_SIZE, row * CELL_SIZE
                x2, y2 = x1 + CELL_SIZE, y1 + CELL_SIZE
                self.canvas.create_rectangle(x1, y1, x2, y2, outline="#ffcc00", width=3)

        if highlight_sq is not None:
            f = chess.square_file(highlight_sq)
            r_sq = chess.square_rank(highlight_sq)
            col = f
            row = 7 - r_sq
            x1, y1 = col * CELL_SIZE, row * CELL_SIZE
            x2, y2 = x1 + CELL_SIZE, y1 + CELL_SIZE
            self.canvas.create_rectangle(x1, y1, x2, y2, outline="#00ff00", width=3)

        for r in range(8):
            for c in range(8):
                ch = chars[r * 8 + c]
                cx, cy = c * CELL_SIZE + CELL_SIZE // 2, r * CELL_SIZE + CELL_SIZE // 2
                self.canvas.create_text(cx, cy, text=ch, font=self.piece_font, fill="#000000")

        if highlight_sq is not None:
            piece = self.board.piece_at(highlight_sq)
            if piece:
                for move in self.board.legal_moves:
                    if move.from_square == highlight_sq:
                        to_sq = move.to_square
                        f = chess.square_file(to_sq)
                        r_sq = chess.square_rank(to_sq)
                        col = f
                        row = 7 - r_sq
                        cx, cy = col * CELL_SIZE + CELL_SIZE // 2, row * CELL_SIZE + CELL_SIZE // 2
                        target_piece = self.board.piece_at(to_sq)
                        if target_piece:
                            self.canvas.create_oval(cx - 16, cy - 16, cx + 16, cy + 16,
                                                    outline="#00cc00", width=2)
                        else:
                            self.canvas.create_oval(cx - 5, cy - 5, cx + 5, cy + 5,
                                                    fill="#00cc00", outline="")

    def _on_click(self, event):
        col = event.x // CELL_SIZE
        row = event.y // CELL_SIZE
        if col < 0 or col > 7 or row < 0 or row > 7:
            return
        file_idx = col
        rank = 7 - row
        square = chess.square(file_idx, rank)

        if self.selected_square is None:
            piece = self.board.piece_at(square)
            if piece is not None and piece.color == self.board.turn:
                self.selected_square = square
                self._draw_board(square)
        else:
            if square == self.selected_square:
                self.selected_square = None
                self._draw_board()
                return
            from_sq = self.selected_square
            to_sq = square
            move_candidate = chess.Move(from_sq, to_sq)
            piece = self.board.piece_at(from_sq)
            if piece and piece.piece_type == chess.PAWN:
                if (piece.color == chess.WHITE and chess.square_rank(to_sq) == 7) or \
                   (piece.color == chess.BLACK and chess.square_rank(to_sq) == 0):
                    move_candidate = chess.Move(from_sq, to_sq, promotion=chess.QUEEN)
            if move_candidate in self.board.legal_moves:
                self._make_move(move_candidate)
                self.selected_square = None
            else:
                piece = self.board.piece_at(square)
                if piece is not None and piece.color == self.board.turn:
                    self.selected_square = square
                    self._draw_board(square)
                else:
                    self.selected_square = None
                    self._draw_board()

    def _make_move(self, move):
        was_black_turn = (self.board.turn == chess.BLACK)
        key = compute_zobrist(self.board)
        pg = move_to_polyglot(move, self.board)
        san = self.board.san(move)
        uci = move.uci()
        self.board.push(move)
        self.last_move = move
        self.current_line_moves.append(uci)
        self._draw_board()
        self._update_info()

        if was_black_turn and self.auto_add.get():
            self._add_to_book_precomputed(move, san, uci, key, pg)

    def _add_to_book_precomputed(self, move, san, uci, key, pg):
        for existing in self.book:
            if existing[0] == key and existing[1] == uci:
                return
        self.book.append((key, uci, san, pg))
        uci_short = uci[:4]
        print(f"  + Book: key=0x{key:08X} move={uci_short} ({san}) pg=0x{pg:04X}")
        self._update_tree()

    def _add_to_book(self, move, san, uci):
        key = compute_zobrist(self.board)
        pg = move_to_polyglot(move, self.board)
        for existing in self.book:
            if existing[0] == key and existing[1] == uci:
                return
        self.book.append((key, uci, san, pg))
        uci_short = uci[:4]
        print(f"  + Book: key=0x{key:08X} move={uci_short} ({san}) pg=0x{pg:04X}")
        self._update_tree()

    def _manual_add(self):
        if self.last_move is None:
            messagebox.showinfo(tr('info', 'Info'), tr('make_move_first', 'Make a move first'),
                               parent=self.root)
            return
        board_copy = self.board.copy()
        last_move = board_copy.pop()
        if board_copy.turn == chess.WHITE:
            messagebox.showinfo(tr('info', 'Info'), tr('only_black', 'Only black moves added to book'),
                               parent=self.root)
            return
        uci = last_move.uci()
        san = board_copy.san(last_move)
        key = compute_zobrist(board_copy)
        pg = move_to_polyglot(last_move, board_copy)
        for existing in self.book:
            if existing[0] == key and existing[1] == uci:
                return
        self.book.append((key, uci, san, pg))
        print(f"  + Book: key=0x{key:08X} move={uci[:4]} ({san}) pg=0x{pg:04X}")
        self._update_tree()

    def _undo_move(self):
        if len(self.current_line_moves) == 0:
            return
        self.current_line_moves.pop()
        self.board = chess.Board()
        for uci in self.current_line_moves:
            self.board.push_uci(uci)
        self.last_move = chess.Move.from_uci(self.current_line_moves[-1]) if self.current_line_moves else None
        self.selected_square = None
        self._draw_board()
        self._update_info()

    def _new_line(self):
        if self.current_line_moves:
            name = f"{tr('line', 'Line')} {len(self.lines) + 1}"
            self.lines.append((name, list(self.current_line_moves)))
            self._update_lines_tree()
        self.current_line_moves = []
        self.board = chess.Board()
        self.last_move = None
        self.selected_square = None
        self._draw_board()
        self._update_info()

    def _reset_board(self):
        self.current_line_moves = []
        self.board = chess.Board()
        self.last_move = None
        self.selected_square = None
        self._draw_board()
        self._update_info()

    def _on_line_double(self, event):
        sel = self.lines_tree.selection()
        if not sel:
            return
        moves_uci = self.lines_tree.set(sel[0], "moves")
        if not moves_uci:
            return
        moves_list = moves_uci.split()
        self.current_line_moves = list(moves_list)
        self.board = chess.Board()
        for uci in moves_list:
            self.board.push_uci(uci)
        self.last_move = chess.Move.from_uci(moves_list[-1]) if moves_list else None
        self._draw_board()
        self._update_info()

    def _import_pgn(self):
        path = filedialog.askopenfilename(
            title=tr('select_pgn', 'Select PGN file'),
            filetypes=[("PGN files", "*.pgn"), ("All files", "*.*")],
            parent=self.root)
        if not path:
            return
        try:
            with open(path, "r", encoding="utf-8", errors="ignore") as f:
                game = chess.pgn.read_game(f)
            if game is None:
                messagebox.showerror(tr('error', 'Error'), tr('no_games', 'No games found'),
                                    parent=self.root)
                return
            self.pgn_game = game
            self.pgn_moves = list(game.mainline_moves())
            self.pgn_index = 0
            self.board = chess.Board()
            self.last_move = None
            self.current_line_moves = []
            self._draw_board()
            self._update_info()
            self.pgn_label.config(text=f"PGN: {len(self.pgn_moves)} {tr('moves', 'moves')}")
            print(f"  + PGN loaded: {len(self.pgn_moves)} moves")
        except Exception as e:
            messagebox.showerror(tr('error', 'Error'), str(e), parent=self.root)

    def _pgn_first(self):
        if not self.pgn_moves:
            return
        self.pgn_index = 0
        self.board = chess.Board()
        self.last_move = None
        self.current_line_moves = []
        self._draw_board()
        self._update_info()
        self.pgn_label.config(text=f"PGN: 0 / {len(self.pgn_moves)}")

    def _pgn_prev(self):
        if not self.pgn_moves or self.pgn_index == 0:
            return
        self.pgn_index -= 1
        self.board = chess.Board()
        self.current_line_moves = []
        for i in range(self.pgn_index):
            mv = self.pgn_moves[i]
            self.board.push(mv)
            self.current_line_moves.append(mv.uci())
        self.last_move = self.pgn_moves[self.pgn_index - 1] if self.pgn_index > 0 else None
        self._draw_board()
        self._update_info()
        self.pgn_label.config(text=f"PGN: {self.pgn_index} / {len(self.pgn_moves)}")

    def _pgn_next(self):
        if not self.pgn_moves or self.pgn_index >= len(self.pgn_moves):
            return
        move = self.pgn_moves[self.pgn_index]
        was_black = (self.board.turn == chess.BLACK)
        key = compute_zobrist(self.board)
        pg = move_to_polyglot(move, self.board)
        san = self.board.san(move)
        uci = move.uci()
        self.board.push(move)
        self.last_move = move
        self.current_line_moves.append(uci)
        self.pgn_index += 1
        self._draw_board()
        self._update_info()
        self.pgn_label.config(text=f"PGN: {self.pgn_index} / {len(self.pgn_moves)}")
        if was_black and self.auto_add.get():
            self._add_to_book_precomputed(move, san, uci, key, pg)

    def _pgn_last(self):
        if not self.pgn_moves:
            return
        while self.pgn_index < len(self.pgn_moves):
            move = self.pgn_moves[self.pgn_index]
            was_black = (self.board.turn == chess.BLACK)
            key = compute_zobrist(self.board)
            pg = move_to_polyglot(move, self.board)
            san = self.board.san(move)
            uci = move.uci()
            self.board.push(move)
            self.current_line_moves.append(uci)
            self.pgn_index += 1
            if was_black and self.auto_add.get():
                self._add_to_book_precomputed(move, san, uci, key, pg)
        self.last_move = self.pgn_moves[-1] if self.pgn_moves else None
        self._draw_board()
        self._update_info()
        self.pgn_label.config(text=f"PGN: {self.pgn_index} / {len(self.pgn_moves)}")

    def _pgn_import_all(self):
        if not self.pgn_game:
            messagebox.showinfo(tr('info', 'Info'), tr('load_pgn_first', 'Load PGN first'),
                               parent=self.root)
            return
        count = 0
        board = self.pgn_game.board()
        for move in self.pgn_game.mainline_moves():
            was_black = (board.turn == chess.BLACK)
            if was_black:
                key = compute_zobrist(board)
                pg = move_to_polyglot(move, board)
                uci = move.uci()
                san = board.san(move)
                dup = False
                for existing in self.book:
                    if existing[0] == key and existing[1] == uci:
                        dup = True
                        break
                if not dup:
                    self.book.append((key, uci, san, pg))
                    count += 1
            board.push(move)
        print(f"  + PGN import: {count} entries added")
        self._update_tree()
        messagebox.showinfo(tr('pgn_import', 'PGN import'),
                          f"{count} {tr('entries_added', 'entries added')}",
                          parent=self.root)

    def _save_c(self):
        if not self.book:
            messagebox.showinfo(tr('info', 'Info'), tr('book_empty', 'Book is empty'),
                               parent=self.root)
            return
        path = filedialog.asksaveasfilename(
            title=tr('save_c', 'Save debut_book.c'),
            defaultextension=".c", initialfile="debut_book.c",
            filetypes=[("C files", "*.c"), ("All files", "*.*")],
            parent=self.root)
        if not path:
            return
        sorted_book = sorted(self.book, key=lambda x: x[0])
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write("// Auto-generated by chess_book_editor.py v4.9\n")
                f.write("// Total entries: %d\n" % len(sorted_book))
                f.write("// Sorted by Zobrist key for binary search\n\n")
                f.write("#include <stdint.h>\n\n")
                f.write("typedef struct {\n")
                f.write("    uint32_t key;   // Zobrist key\n")
                f.write("    uint16_t move;  // Polyglot move\n")
                f.write("    uint16_t weight;\n")
                f.write("} book_entry_t;\n\n")
                f.write("const book_entry_t debut_book[%d] = {\n" % len(sorted_book))
                for key, uci, san, pg in sorted_book:
                    uci_short = uci[:4]
                    k = _safe_key(key)
                    p = _safe_int(pg)
                    f.write("    { 0x%08X, 0x%04X, 1 },  // %s (%s)\n" % (k, p, uci_short, san))
                f.write("};\n\n")
                f.write("const uint16_t debut_book_size = %d;\n" % len(sorted_book))
            print(f"  + Saved: {path} ({len(sorted_book)} entries, sorted)")
            messagebox.showinfo(tr('save', 'Save'),
                              f"{len(sorted_book)} {tr('entries_saved_sorted', 'entries saved, sorted by key')}",
                              parent=self.root)
        except Exception as e:
            messagebox.showerror(tr('error', 'Error'), str(e), parent=self.root)

    def _save_session(self):
        path = filedialog.asksaveasfilename(
            title=tr('save_session', 'Save session'),
            defaultextension=".json", initialfile="book_session.json",
            filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
            parent=self.root)
        if not path:
            return
        if self.current_line_moves:
            self.lines.append((f"{tr('line', 'Line')} {len(self.lines) + 1}",
                            list(self.current_line_moves)))
            self._update_lines_tree()
        data = {
            "version": "4.9",
            "book": [[int(k), u, s, int(p)] for k, u, s, p in self.book],
            "lines": [[name, moves] for name, moves in self.lines],
        }
        try:
            with open(path, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
            print(f"  + Session saved: {path}")
            messagebox.showinfo(tr('save', 'Save'),
                              f"{len(self.book)} {tr('entries', 'entries')}, {len(self.lines)} {tr('lines_saved', 'lines')}",
                              parent=self.root)
        except Exception as e:
            messagebox.showerror(tr('error', 'Error'), str(e), parent=self.root)

    def _load_session(self):
        path = filedialog.askopenfilename(
            title=tr('load_session', 'Load session'),
            filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
            parent=self.root)
        if not path:
            return
        try:
            with open(path, "r", encoding="utf-8") as f:
                data = json.load(f)
            self.book = [(_safe_key(e[0]), e[1], e[2], _safe_int(e[3])) for e in data.get("book", [])]
            self.lines = [(l[0], l[1]) for l in data.get("lines", [])]
            self.current_line_moves = []
            self.board = chess.Board()
            self.last_move = None
            self._update_tree()
            self._update_lines_tree()
            self._draw_board()
            self._update_info()
            print(f"  + Loaded: {len(self.book)} entries, {len(self.lines)} lines")
            if self.book:
                k0 = _safe_key(self.book[0][0])
                p0 = _safe_int(self.book[0][3])
                print(f"  [DEBUG] First key: 0x{k0:08X} (type={type(self.book[0][0]).__name__})")
                print(f"  [DEBUG] First pg: 0x{p0:04X} (type={type(self.book[0][3]).__name__})")
            children = self.tree.get_children()
            print(f"  [DEBUG] Tree items: {len(children)}")
            messagebox.showinfo(tr('load', 'Load'),
                              f"{len(self.book)} {tr('entries', 'entries')}, {len(self.lines)} {tr('lines', 'lines')}",
                              parent=self.root)
        except Exception as e:
            messagebox.showerror(tr('error', 'Error'), str(e), parent=self.root)

    def _update_tree(self):
        for item in self.tree.get_children():
            self.tree.delete(item)
        for key, uci, san, pg in self.book:
            k = _safe_key(key)
            p = _safe_int(pg)
            self.tree.insert("", tk.END, values=(f"0x{k:08X}", uci[:4], san, f"0x{p:04X}"))

    def _update_lines_tree(self):
        for item in self.lines_tree.get_children():
            self.lines_tree.delete(item)
        for name, moves in self.lines:
            self.lines_tree.insert("", tk.END, text=name, values=("", " ".join(moves)))

    def _on_tree_double(self, event):
        sel = self.tree.selection()
        if not sel:
            return
        values = self.tree.item(sel[0], "values")
        messagebox.showinfo(tr('entry', 'Entry'),
                          f"Key: {values[0]}\nUCI: {values[1]}\nSAN: {values[2]}\nPG: {values[3]}",
                          parent=self.root)

    def _on_tree_delete(self, event):
        sel = self.tree.selection()
        if not sel:
            return
        idx = self.tree.index(sel[0])
        if idx < len(self.book):
            del self.book[idx]
            self._update_tree()
            self._update_info()

    def _do_search(self, event=None):
        query = self.search_var.get().strip().lower()
        if not query:
            self.search_status.config(text="", fg=TEXT_COLOR)
            return
        if query.startswith("0x"):
            query = query[2:]
        print(f"  [SEARCH] query='{query}', book size={len(self.book)}")
        self.search_results = []
        try:
            for i, (key, uci, san, pg) in enumerate(self.book):
                k = _safe_key(key)
                key_hex = f"{k:08x}"
                if query in key_hex:
                    self.search_results.append(i)
        except Exception as e:
            print(f"  [SEARCH ERROR] {e}")
            self.search_status.config(text=tr('not_found', 'not found'), fg="#ff6666")
            return
        print(f"  [SEARCH] found {len(self.search_results)} matches")
        if self.search_results:
            self.search_idx = 0
            self._goto_search_result()
            self.search_status.config(text=f"1/{len(self.search_results)}", fg="#00ff00")
        else:
            self.search_status.config(text=tr('not_found', 'not found'), fg="#ff6666")

    def _goto_search_result(self):
        if not self.search_results:
            return
        idx = self.search_results[self.search_idx]
        children = self.tree.get_children()
        print(f"  [SEARCH] goto idx={idx}, tree items={len(children)}")
        if idx < len(children):
            self.tree.selection_set(children[idx])
            self.tree.see(children[idx])

    def _search_next(self):
        if not self.search_results:
            self._do_search()
            return
        self.search_idx = (self.search_idx + 1) % len(self.search_results)
        self._goto_search_result()
        self.search_status.config(text=f"{self.search_idx + 1}/{len(self.search_results)}", fg="#00ff00")

    def _search_prev(self):
        if not self.search_results:
            self._do_search()
            return
        self.search_idx = (self.search_idx - 1) % len(self.search_results)
        self._goto_search_result()
        self.search_status.config(text=f"{self.search_idx + 1}/{len(self.search_results)}", fg="#00ff00")

    def _do_lines_search(self, event=None):
        query = self.lines_search_var.get().strip().lower()
        if not query:
            self.lines_search_status.config(text="", fg=TEXT_COLOR)
            return
        if query.startswith("0x"):
            query = query[2:]
        print(f"  [LINES SEARCH] query='{query}', lines count={len(self.lines)}")
        self.lines_search_results = []
        try:
            for i, (name, moves) in enumerate(self.lines):
                # Search in line name (hash is usually in the name)
                name_lower = name.lower()
                if query in name_lower:
                    self.lines_search_results.append(i)
                    continue
                # Also compute key from moves if name doesn't contain it
                if ZOBRIST_NUMBERS and moves:
                    b = chess.Board()
                    for uci in moves:
                        try:
                            b.push_uci(uci)
                        except:
                            break
                    key = compute_zobrist(b)
                    key_hex = f"{key:08x}"
                    if query in key_hex:
                        self.lines_search_results.append(i)
        except Exception as e:
            print(f"  [LINES SEARCH ERROR] {e}")
            self.lines_search_status.config(text=tr('not_found', 'not found'), fg="#ff6666")
            return
        print(f"  [LINES SEARCH] found {len(self.lines_search_results)} matches")
        if self.lines_search_results:
            self.lines_search_idx = 0
            self._goto_lines_search_result()
            self.lines_search_status.config(text=f"1/{len(self.lines_search_results)}", fg="#00ff00")
        else:
            self.lines_search_status.config(text=tr('not_found', 'not found'), fg="#ff6666")

    def _goto_lines_search_result(self):
        if not self.lines_search_results:
            return
        idx = self.lines_search_results[self.lines_search_idx]
        children = self.lines_tree.get_children()
        print(f"  [LINES SEARCH] goto idx={idx}, tree items={len(children)}")
        if idx < len(children):
            self.lines_tree.selection_set(children[idx])
            self.lines_tree.see(children[idx])

    def _lines_search_next(self):
        if not self.lines_search_results:
            self._do_lines_search()
            return
        self.lines_search_idx = (self.lines_search_idx + 1) % len(self.lines_search_results)
        self._goto_lines_search_result()
        self.lines_search_status.config(text=f"{self.lines_search_idx + 1}/{len(self.lines_search_results)}", fg="#00ff00")

    def _lines_search_prev(self):
        if not self.lines_search_results:
            self._do_lines_search()
            return
        self.lines_search_idx = (self.lines_search_idx - 1) % len(self.lines_search_results)
        self._goto_lines_search_result()
        self.lines_search_status.config(text=f"{self.lines_search_idx + 1}/{len(self.lines_search_results)}", fg="#00ff00")

    def _update_info(self):
        key = compute_zobrist(self.board)
        in_book = any(_safe_key(e[0]) == key for e in self.book)
        info = f"Zobrist: 0x{key:08X}\n"
        info += f"{tr('in_book', 'In book')}: {tr('yes', 'yes') if in_book else tr('no', 'no')}\n"
        info += f"{tr('entries', 'Entries')}: {len(self.book)}\n"
        info += f"{tr('line_moves', 'Moves in line')}: {len(self.current_line_moves)}\n"
        info += f"FEN: {self.board.fen()}"
        self.info_label.config(text=info)

# ====================== MAIN ======================

def main():
    print("=== Chess Book Editor v4.9 ===")

    zobrist_path = find_zobrist_file()
    if zobrist_path is None:
        print("[Zobrist] File not found. Select manually.")
        root_temp = tk.Tk()
        root_temp.withdraw()
        zobrist_path = filedialog.askopenfilename(
            title="Select zobrist_table.h",
            filetypes=[("Header files", "*.h"), ("All files", "*.*")])
        root_temp.destroy()
        if not zobrist_path:
            print("[ERROR] zobrist_table.h not selected. Exit.")
            sys.exit(1)

    numbers = load_zobrist_table(zobrist_path)
    print(f"Loaded {len(numbers)} Zobrist numbers from {os.path.basename(zobrist_path)}")

    root = tk.Tk()
    app = ChessBookEditor(root)
    root.mainloop()

if __name__ == "__main__":
    main()
Alexlaw1964
Posts: 34
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 »

chess_book_editor_lang.py

Code: Select all

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
chess_book_editor_lang.py — Локализация редактора дебютной книги
Версия: 4.9
"""

LANG = {
    'ru': {
        'title': 'Редактор дебютной книги v4.9 — STM32',
        'language': 'Язык:',
        'new_line': 'Новая линия',
        'undo': 'Отменить ход',
        'add_to_book': '+ В книгу',
        'auto_add': 'Авто-добавление',
        'import_pgn': 'Импорт PGN',
        'reset_board': 'Сброс доски',
        'pgn_import_all': '>>>',
        'save_c': 'Сохранить debut_book.c',
        'save_json': 'Сохранить сессию (JSON)',
        'load_json': 'Загрузить сессию (JSON)',
        'search_key': 'Поиск по ключу:',
        'search_prev': '< Назад',
        'search_next': 'Вперёд >',
        'lines_search_key': 'Поиск в линиях:',
        'lines_search': 'Поиск',
        'lines_search_prev': '< Назад',
        'lines_search_next': 'Вперёд >',
        'book_entries': 'Записи книги',
        'col_key': 'Zobrist-ключ',
        'col_move': 'UCI',
        'col_san': 'SAN',
        'col_pg': 'Polyglot',
        'lines': 'Линии',
        'info': 'Инфо',
        'make_move_first': 'Сначала сделайте ход',
        'only_black': 'В книгу добавляются только ходы чёрных',
        'line': 'Линия',
        'select_pgn': 'Выберите PGN-файл',
        'error': 'Ошибка',
        'no_games': 'Игры не найдены',
        'moves': 'ходов',
        'load_pgn_first': 'Сначала загрузите PGN',
        'entries_added': 'записей добавлено',
        'book_empty': 'Книга пуста',
        'entries_saved_sorted': 'записей сохранено, отсортировано по ключу',
        'save': 'Сохранение',
        'entries': 'записей',
        'lines_saved': 'линий',
        'load': 'Загрузка',
        'entry': 'Запись',
        'not_found': 'не найдено',
        'in_book': 'В книге',
        'yes': 'да',
        'no': 'нет',
        'line_moves': 'Ходов в линии',
    },
    'en': {
        'title': 'Chess Book Editor v4.9 — STM32',
        'language': 'Language:',
        'new_line': 'New line',
        'undo': 'Undo move',
        'add_to_book': '+ Add to book',
        'auto_add': 'Auto-add',
        'import_pgn': 'Import PGN',
        'reset_board': 'Reset board',
        'pgn_import_all': '>>>',
        'save_c': 'Save debut_book.c',
        'save_json': 'Save session (JSON)',
        'load_json': 'Load session (JSON)',
        'search_key': 'Search by key:',
        'search_prev': '< Back',
        'search_next': 'Fwd >',
        'lines_search_key': 'Search in lines:',
        'lines_search': 'Search',
        'lines_search_prev': '< Back',
        'lines_search_next': 'Fwd >',
        'book_entries': 'Book entries',
        'col_key': 'Zobrist Key',
        'col_move': 'UCI',
        'col_san': 'SAN',
        'col_pg': 'Polyglot',
        'lines': 'Lines',
        'info': 'Info',
        'make_move_first': 'Make a move first',
        'only_black': 'Only black moves added to book',
        'line': 'Line',
        'select_pgn': 'Select PGN file',
        'error': 'Error',
        'no_games': 'No games found',
        'moves': 'moves',
        'load_pgn_first': 'Load PGN first',
        'entries_added': 'entries added',
        'book_empty': 'Book is empty',
        'entries_saved_sorted': 'entries saved, sorted by key',
        'save': 'Save',
        'entries': 'entries',
        'lines_saved': 'lines',
        'load': 'Load',
        'entry': 'Entry',
        'not_found': 'not found',
        'in_book': 'In book',
        'yes': 'yes',
        'no': 'no',
        'line_moves': 'Moves in line',
    }
}

_current_lang = 'ru'

def set_lang(l):
    global _current_lang
    if l in LANG:
        _current_lang = l

def get_lang():
    return _current_lang

def tr(key, default=None):
    d = LANG.get(_current_lang, LANG.get('ru', {}))
    return d.get(key, default or key)