diff --git a/C/opening_learner/Makefile b/C/opening_learner/Makefile new file mode 100644 index 0000000..aa0546e --- /dev/null +++ b/C/opening_learner/Makefile @@ -0,0 +1,30 @@ +CC := gcc +CFLAGS := -O2 -Wall -Wextra -std=c11 + +# SDL2 flags: require sdl2-config (no fallback) +SDL2CONF := $(shell command -v sdl2-config 2>/dev/null) +ifeq ($(SDL2CONF),) +$(error sdl2-config not found. Please install SDL2 development package.) +endif +SDL_CFLAGS := $(shell sdl2-config --cflags) +SDL_LDFLAGS := $(shell sdl2-config --libs) + +SRC := main.c gui.c engine.c chess.c mistakes.c +OBJ := $(SRC:.c=.o) +BIN := opening_learner + +.PHONY: all clean run + +all: $(BIN) + +$(BIN): $(OBJ) + $(CC) $(CFLAGS) -o $@ $^ $(SDL_LDFLAGS) + +%.o: %.c + $(CC) $(CFLAGS) $(SDL_CFLAGS) -c -o $@ $< + +run: $(BIN) + ./$(BIN) + +clean: + rm -f $(OBJ) $(BIN) diff --git a/C/opening_learner/README.md b/C/opening_learner/README.md new file mode 100644 index 0000000..31176c2 --- /dev/null +++ b/C/opening_learner/README.md @@ -0,0 +1,26 @@ +# Opening Learner (C + SDL2) + +- Click a piece, then click a destination to move. +- Thick board outline, board uses non-pure colors. +- Uses local Stockfish or asmfish via UCI. +- Logs mistakes to `mistakes.txt` and lets you revisit them with the `m` key. + +Build and check: + +```sh +./check_build.sh +``` + +Run: + +```sh +./opening_learner +``` + +Tips: +- ESC clears selection. +- Press `m` to cycle to a stored mistake position and practice the best move there. +- If you play Black, the board flips so Black is at the bottom. + +Notes: +- Rendering avoids TTF dependency; pieces are clear, high-contrast geometric glyphs. diff --git a/C/opening_learner/check_build.sh b/C/opening_learner/check_build.sh new file mode 100755 index 0000000..e6ab5b2 --- /dev/null +++ b/C/opening_learner/check_build.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +echo "Checking for engine (stockfish or asmfish)" +if command -v stockfish >/dev/null 2>&1; then + echo "Found stockfish" +elif command -v asmfish >/dev/null 2>&1; then + echo "Found asmfish" +else + echo "Error: Neither stockfish nor asmfish found in PATH." >&2 + exit 1 +fi + +echo "Checking for SDL2 dev (sdl2-config)" +if command -v sdl2-config >/dev/null 2>&1; then + echo "Found sdl2-config" +else + echo "Error: sdl2-config not found. Install SDL2 dev (e.g., libsdl2-dev)." >&2 + exit 2 +fi + +echo "Building project" +make clean +make -j + +echo "Build OK" diff --git a/C/opening_learner/chess.c b/C/opening_learner/chess.c new file mode 100644 index 0000000..f5e0ed2 --- /dev/null +++ b/C/opening_learner/chess.c @@ -0,0 +1,296 @@ +#include "chess.h" +#include +#include +#include +#include + +static const int knight_offsets[8] = {15, 17, -15, -17, 10, -10, 6, -6}; +static const int bishop_dirs[4] = {9, 7, -9, -7}; +static const int rook_dirs[4] = {8, -8, 1, -1}; +static const int king_dirs[8] = {8,-8,1,-1,9,7,-9,-7}; + +static inline int file_of(int sq){return sq%8;} +static inline int rank_of(int sq){return sq/8;} +static inline bool on_board(int sq){return sq>=0 && sq<64;} +static inline bool same_color(char a, char b){return (isupper(a)&&isupper(b))||(islower(a)&&islower(b));} +static inline bool is_white(char p){return isupper((unsigned char)p);} + +void chess_init_start(Position *pos){ + // a1..h1 (0..7) are white back rank; rank 8 at indexes 56..63 are black back rank + const char *start = "RNBQKBNRPPPPPPPP................................pppppppprnbqkbnr"; + for (int i=0;i<64;i++) pos->board[i] = start[i]; + pos->white_to_move = true; + pos->castle_wk = pos->castle_wq = pos->castle_bk = pos->castle_bq = true; + pos->ep_square = -1; pos->halfmove_clock = 0; pos->fullmove_number = 1; +} + +void chess_copy(Position *dst, const Position *src){ *dst = *src; } + +static bool is_empty(const Position *p, int sq){ return p->board[sq]=='.'; } + +bool chess_square_attacked(const Position *pos, int sq, bool by_white){ + // pawns + int r = rank_of(sq), f = file_of(sq); + if (by_white){ + int s1 = (r-1)*8 + (f-1); if (f>0 && r>0 && on_board(s1) && pos->board[s1]=='P') return true; + int s2 = (r-1)*8 + (f+1); if (f<7 && r>0 && on_board(s2) && pos->board[s2]=='P') return true; + } else { + int s1 = (r+1)*8 + (f-1); if (f>0 && r<7 && on_board(s1) && pos->board[s1]=='p') return true; + int s2 = (r+1)*8 + (f+1); if (f<7 && r<7 && on_board(s2) && pos->board[s2]=='p') return true; + } + // knights + for (int i=0;i<8;i++){ + int t = sq + knight_offsets[i]; + if (!on_board(t)) continue; + int df = file_of(t)-f; int dr = rank_of(t)-r; if (df< -2||df>2||dr<-2||dr>2) continue; // edge wrap guard + char pc = pos->board[t]; + if (by_white && pc=='N') return true; + if (!by_white && pc=='n') return true; + } + // bishops/queens + for (int d=0; d<4; d++){ + int off = bishop_dirs[d]; int t = sq + off; + while (on_board(t) && abs(file_of(t)-f)==abs(rank_of(t)-r)){ + char pc = pos->board[t]; + if (pc!='.') { if (by_white && (pc=='B'||pc=='Q')) return true; if (!by_white && (pc=='b'||pc=='q')) return true; break; } + t += off; + } + } + // rooks/queens + for (int d=0; d<4; d++){ + int off = rook_dirs[d]; int t = sq + off; + while (on_board(t) && (file_of(t)==f || rank_of(t)==r)){ + char pc = pos->board[t]; + if (pc!='.') { if (by_white && (pc=='R'||pc=='Q')) return true; if (!by_white && (pc=='r'||pc=='q')) return true; break; } + t += off; + } + } + // king + for (int i=0;i<8;i++){ + int t=sq+king_dirs[i]; if (!on_board(t)) continue; if (abs(file_of(t)-f)>1||abs(rank_of(t)-r)>1) continue; + char pc = pos->board[t]; if (by_white && pc=='K') return true; if (!by_white && pc=='k') return true; + } + return false; +} + +bool chess_is_in_check(const Position *pos, bool white){ + int ks = -1; char k = white? 'K' : 'k'; + for (int i=0;i<64;i++) if (pos->board[i]==k) { ks = i; break; } + if (ks==-1) return false; // malformed + return chess_square_attacked(pos, ks, !white); +} + +static void add_move_if_legal(const Position *pos, int from, int to, char promo, Move *out, size_t *n, size_t max){ + if (*n >= max) return; + Position tmp; chess_copy(&tmp, pos); + Move m = {0}; m.from=from; m.to=to; m.promo=promo; m.moved=pos->board[from]; m.captured=pos->board[to]; + int prev_ep = tmp.ep_square; m.prev_ep = prev_ep; m.prev_wk=tmp.castle_wk; m.prev_wq=tmp.castle_wq; m.prev_bk=tmp.castle_bk; m.prev_bq=tmp.castle_bq; m.prev_halfmove=tmp.halfmove_clock; + if (!chess_make_move(&tmp, &m)) return; + if (chess_is_in_check(&tmp, !tmp.white_to_move)) return; // after make, side switched + out[(*n)++] = m; // store pseudo move with added flags from make_move +} + +size_t chess_generate_legal_moves(const Position *pos, Move *out, size_t max){ + size_t n=0; + bool white = pos->white_to_move; + for (int sq=0; sq<64; sq++){ + char p = pos->board[sq]; if (p=='.') continue; if (white != is_white(p)) continue; + int f=file_of(sq), r=rank_of(sq); + switch (tolower(p)){ + case 'p': { + int dir = white? 8 : -8; int start_rank = white? 1 : 6; int prom_rank = white? 6 : 1; + int one = sq + dir; + if (on_board(one) && is_empty(pos, one)){ + if (r==prom_rank){ + const char *pr = white? "QRBN" : "qrbn"; + for (int i=0;i<4;i++) add_move_if_legal(pos, sq, one, pr[i], out, &n, max); + } else add_move_if_legal(pos, sq, one, 0, out, &n, max); + // two + int two = sq + 2*dir; if (r==start_rank && is_empty(pos, two)) add_move_if_legal(pos, sq, two, 0, out, &n, max); + } + // captures + int caps[2] = { dir+1, dir-1 }; + for (int i=0;i<2;i++){ + int t = sq + caps[i]; if (!on_board(t)) continue; if (abs(file_of(t)-f)!=1) continue; + if (!is_empty(pos, t) && !same_color(pos->board[sq], pos->board[t])){ + if (r==prom_rank){ const char *pr = white? "QRBN" : "qrbn"; for (int j=0;j<4;j++) add_move_if_legal(pos, sq, t, pr[j], out, &n, max); } + else add_move_if_legal(pos, sq, t, 0, out, &n, max); + } + } + // en passant + if (pos->ep_square!=-1){ + int ep = pos->ep_square; if (abs(file_of(ep)-f)==1 && (ep == sq+dir+1 || ep==sq+dir-1)) add_move_if_legal(pos, sq, ep, 0, out, &n, max); + } + } break; + case 'n': { + for (int i=0;i<8;i++){ + int t = sq + knight_offsets[i]; if (!on_board(t)) continue; if (abs(file_of(t)-f)>2 || abs(rank_of(t)-r)>2) continue; if (!is_empty(pos, t) && same_color(p, pos->board[t])) continue; add_move_if_legal(pos, sq, t, 0, out, &n, max); + } + } break; + case 'b': { + for (int d=0; d<4; d++){ + int off = bishop_dirs[d]; int t = sq + off; while (on_board(t) && abs(file_of(t)-f)==abs(rank_of(t)-r)){ + if (!is_empty(pos, t)){ if (!same_color(p, pos->board[t])) add_move_if_legal(pos, sq, t, 0, out, &n, max); break; } + add_move_if_legal(pos, sq, t, 0, out, &n, max); t += off; + } + } + } break; + case 'r': { + for (int d=0; d<4; d++){ + int off = rook_dirs[d]; int t = sq + off; while (on_board(t) && (file_of(t)==f || rank_of(t)==r)){ + if (!is_empty(pos, t)){ if (!same_color(p, pos->board[t])) add_move_if_legal(pos, sq, t, 0, out, &n, max); break; } + add_move_if_legal(pos, sq, t, 0, out, &n, max); t += off; + } + } + } break; + case 'q': { + for (int d=0; d<4; d++){ + int off = bishop_dirs[d]; int t = sq + off; while (on_board(t) && abs(file_of(t)-f)==abs(rank_of(t)-r)){ + if (!is_empty(pos, t)){ if (!same_color(p, pos->board[t])) add_move_if_legal(pos, sq, t, 0, out, &n, max); break; } + add_move_if_legal(pos, sq, t, 0, out, &n, max); t += off; + } + } + for (int d=0; d<4; d++){ + int off = rook_dirs[d]; int t = sq + off; while (on_board(t) && (file_of(t)==f || rank_of(t)==r)){ + if (!is_empty(pos, t)){ if (!same_color(p, pos->board[t])) add_move_if_legal(pos, sq, t, 0, out, &n, max); break; } + add_move_if_legal(pos, sq, t, 0, out, &n, max); t += off; + } + } + } break; + case 'k': { + for (int i=0;i<8;i++){ + int t = sq + king_dirs[i]; if (!on_board(t)) continue; if (abs(file_of(t)-f)>1||abs(rank_of(t)-r)>1) continue; if (!is_empty(pos, t) && same_color(p, pos->board[t])) continue; add_move_if_legal(pos, sq, t, 0, out, &n, max); + } + // castling + if (white){ + if (pos->castle_wk && pos->board[5]=='.' && pos->board[6]=='.' && !chess_square_attacked(pos,4,false) && !chess_square_attacked(pos,5,false) && !chess_square_attacked(pos,6,false)) add_move_if_legal(pos, 4, 6, 0, out, &n, max); + if (pos->castle_wq && pos->board[3]=='.' && pos->board[2]=='.' && pos->board[1]=='.' && !chess_square_attacked(pos,4,false) && !chess_square_attacked(pos,3,false) && !chess_square_attacked(pos,2,false)) add_move_if_legal(pos, 4, 2, 0, out, &n, max); + } else { + if (pos->castle_bk && pos->board[61]=='.' && pos->board[62]=='.' && !chess_square_attacked(pos,60,true) && !chess_square_attacked(pos,61,true) && !chess_square_attacked(pos,62,true)) add_move_if_legal(pos, 60, 62, 0, out, &n, max); + if (pos->castle_bq && pos->board[59]=='.' && pos->board[58]=='.' && pos->board[57]=='.' && !chess_square_attacked(pos,60,true) && !chess_square_attacked(pos,59,true) && !chess_square_attacked(pos,58,true)) add_move_if_legal(pos, 60, 58, 0, out, &n, max); + } + } break; + } + } + return n; +} + +bool chess_make_move(Position *pos, Move *m){ + m->is_castle=false; m->is_enpassant=false; + char p = pos->board[m->from]; char tgt = pos->board[m->to]; + if (p=='.') return false; + // handle special: en passant capture + if (tolower(p)=='p' && m->to == pos->ep_square && file_of(m->to)!=file_of(m->from) && tgt=='.'){ + m->is_enpassant = true; + int cap_sq = pos->white_to_move ? (m->to - 8) : (m->to + 8); + m->captured = pos->board[cap_sq]; pos->board[cap_sq]='.'; + } + + // move piece + pos->board[m->to] = p; + pos->board[m->from] = '.'; + + // promotion + if (tolower(p)=='p' && m->promo){ pos->board[m->to] = m->promo; } + + // castling rook move + if (tolower(p)=='k'){ + int from = m->from, to = m->to; + if (from==4 && to==6){ pos->board[5]='R'; pos->board[7]='.'; m->is_castle=true; } + else if (from==4 && to==2){ pos->board[3]='R'; pos->board[0]='.'; m->is_castle=true; } + else if (from==60 && to==62){ pos->board[61]='r'; pos->board[63]='.'; m->is_castle=true; } + else if (from==60 && to==58){ pos->board[59]='r'; pos->board[56]='.'; m->is_castle=true; } + } + + // update castling rights + if (m->from==0||m->to==0) pos->castle_wq=false; + if (m->from==7||m->to==7) pos->castle_wk=false; + if (m->from==56||m->to==56) pos->castle_bq=false; + if (m->from==63||m->to==63) pos->castle_bk=false; + if (tolower(p)=='k'){ if (is_white(p)) { pos->castle_wk=pos->castle_wq=false;} else {pos->castle_bk=pos->castle_bq=false;} } + + // update ep square + pos->ep_square = -1; + if (tolower(p)=='p'){ + int df = rank_of(m->to) - rank_of(m->from); + if (df==2 || df==-2){ pos->ep_square = (m->from + m->to)/2; } + } + + // halfmove clock + if (tolower(p)=='p' || tgt!='.') pos->halfmove_clock = 0; else pos->halfmove_clock++; + + // side to move + pos->white_to_move = !pos->white_to_move; if (pos->white_to_move) pos->fullmove_number++; + + return true; +} + +void chess_unmake_move(Position *pos, const Move *m){ + pos->white_to_move = !pos->white_to_move; if (!pos->white_to_move) pos->fullmove_number--; + // restore halfmove/flags + pos->ep_square = m->prev_ep; + pos->castle_wk = m->prev_wk; pos->castle_wq = m->prev_wq; pos->castle_bk = m->prev_bk; pos->castle_bq = m->prev_bq; + pos->halfmove_clock = m->prev_halfmove; + + char p = m->moved; + // undo promotions + if (tolower(p)=='p' && m->promo){ p = is_white(p)? 'P':'p'; } + + pos->board[m->from] = p; + pos->board[m->to] = m->captured? m->captured : '.'; + if (m->is_enpassant){ int cap_sq = pos->white_to_move ? (m->to - 8) : (m->to + 8); pos->board[m->to]='.'; pos->board[cap_sq]= m->captured; } + if (m->is_castle){ + if (m->from==4 && m->to==6){ pos->board[7]='R'; pos->board[5]='.'; } + else if (m->from==4 && m->to==2){ pos->board[0]='R'; pos->board[3]='.'; } + else if (m->from==60 && m->to==62){ pos->board[63]='r'; pos->board[61]='.'; } + else if (m->from==60 && m->to==58){ pos->board[56]='r'; pos->board[59]='.'; } + } +} + +void sq_to_coord(int sq, int *file, int *rank){ if (file) *file=file_of(sq); if (rank) *rank=rank_of(sq); } +int coord_to_sq(int file, int rank){ return rank*8 + file; } + +void move_to_uci(const Move *m, char buf[8]){ + int f1=file_of(m->from), r1=rank_of(m->from), f2=file_of(m->to), r2=rank_of(m->to); + buf[0]='a'+f1; buf[1]='1'+r1; buf[2]='a'+f2; buf[3]='1'+r2; int i=4; + if (m->promo){ buf[i++]=tolower(m->promo); } + buf[i]='\0'; +} + +bool parse_uci_move(const char *s, const Position *pos, Move *out){ + if (!s || strlen(s)<4) return false; + int f1=s[0]-'a', r1=s[1]-'1', f2=s[2]-'a', r2=s[3]-'1'; if (f1<0||f1>7||f2<0||f2>7||r1<0||r1>7||r2<0||r2>7) return false; + int from = r1*8+f1, to=r2*8+f2; char promo = s[4]? s[4]:0; if (promo) promo = pos->white_to_move? toupper((unsigned char)promo):tolower((unsigned char)promo); + Move list[MAX_MOVES]; size_t n = chess_generate_legal_moves(pos, list, MAX_MOVES); + for (size_t i=0;ipromo = promo? promo : list[i].promo; return true; } } + return false; +} + +bool chess_to_fen(const Position *pos, char *out, size_t outsz){ + char buf[256]; int idx=0; + for (int r=7;r>=0;r--){ + int empty=0; + for (int f=0;f<8;f++){ + char p = pos->board[r*8+f]; + if (p=='.') empty++; else { if (empty){ buf[idx++]= '0'+empty; empty=0; } buf[idx++]=p; } + } + if (empty) buf[idx++]= '0'+empty; + if (r) buf[idx++]='/'; + } + buf[idx++]=' '; + buf[idx++]= pos->white_to_move ? 'w':'b'; + buf[idx++]=' '; + int start=idx; + if (pos->castle_wk) buf[idx++]='K'; + if (pos->castle_wq) buf[idx++]='Q'; + if (pos->castle_bk) buf[idx++]='k'; + if (pos->castle_bq) buf[idx++]='q'; + if (idx==start) buf[idx++]='-'; + buf[idx++]=' '; + if (pos->ep_square==-1){ buf[idx++]='-'; } + else { int f=file_of(pos->ep_square), r=rank_of(pos->ep_square); buf[idx++]='a'+f; buf[idx++]='1'+r; } + idx += snprintf(buf+idx, sizeof(buf)-idx, " %d %d", pos->halfmove_clock, pos->fullmove_number); + buf[idx]='\0'; + snprintf(out, outsz, "%s", buf); + return true; +} diff --git a/C/opening_learner/chess.h b/C/opening_learner/chess.h new file mode 100644 index 0000000..9c2c9f6 --- /dev/null +++ b/C/opening_learner/chess.h @@ -0,0 +1,56 @@ +#ifndef CHESS_H +#define CHESS_H + +#include +#include + +// Board is 64 chars, a1=0, b1=1, ..., h8=63 +// Pieces: 'P','N','B','R','Q','K' for white, lowercase for black, '.' empty + +typedef struct { + char board[64]; + bool white_to_move; + bool castle_wk, castle_wq, castle_bk, castle_bq; + int ep_square; // -1 if none + int halfmove_clock; + int fullmove_number; +} Position; + +typedef struct { + int from, to; + char promo; // 0 or 'q','r','b','n' (lowercase for black) + char captured; // piece captured or 0 + char moved; // piece moved + bool is_castle; + bool is_enpassant; + int prev_ep; + bool prev_wk, prev_wq, prev_bk, prev_bq; + int prev_halfmove; +} Move; + +void chess_init_start(Position *pos); +void chess_copy(Position *dst, const Position *src); + +// Move gen and make/unmake +size_t chess_generate_legal_moves(const Position *pos, Move *out, size_t max); +bool chess_make_move(Position *pos, Move *m); +void chess_unmake_move(Position *pos, const Move *m); + +// Utility +bool chess_is_in_check(const Position *pos, bool white); +bool chess_square_attacked(const Position *pos, int sq, bool by_white); + +// Conversions +void sq_to_coord(int sq, int *file, int *rank); +int coord_to_sq(int file, int rank); + +// UCI strings like e2e4, with optional promotion char +void move_to_uci(const Move *m, char buf[8]); +bool parse_uci_move(const char *s, const Position *pos, Move *out); + +// FEN +bool chess_to_fen(const Position *pos, char *out, size_t outsz); + +#define MAX_MOVES 256 + +#endif diff --git a/C/opening_learner/chess.o b/C/opening_learner/chess.o new file mode 100644 index 0000000..ee51fb8 Binary files /dev/null and b/C/opening_learner/chess.o differ diff --git a/C/opening_learner/engine.c b/C/opening_learner/engine.c new file mode 100644 index 0000000..c4ce1b0 --- /dev/null +++ b/C/opening_learner/engine.c @@ -0,0 +1,141 @@ +#include "engine.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static bool spawn_process(const char *path, Engine *e){ + int inpipe[2], outpipe[2]; + if (pipe(inpipe)<0 || pipe(outpipe)<0) return false; + pid_t pid = fork(); + if (pid < 0) return false; + if (pid == 0){ + dup2(inpipe[0], STDIN_FILENO); // child stdin from inpipe[0] + dup2(outpipe[1], STDOUT_FILENO); // child stdout to outpipe[1] + dup2(outpipe[1], STDERR_FILENO); + close(inpipe[0]); close(inpipe[1]); close(outpipe[0]); close(outpipe[1]); + execlp(path, path, (char*)NULL); + _exit(127); + } + // parent + close(inpipe[0]); close(outpipe[1]); + e->pid = pid; e->in_fd = inpipe[1]; e->out_fd = outpipe[0]; e->ready=false; + // make out_fd non-blocking for reads with polling + int flags = fcntl(e->out_fd, F_GETFL, 0); fcntl(e->out_fd, F_SETFL, flags | O_NONBLOCK); + return true; +} + +static bool try_start(Engine *e, const char *name){ + if (!spawn_process(name, e)) return false; + // send UCI init + engine_cmd(e, "uci\n"); + char buf[4096]; int attempts=50; // ~5s total + while (attempts--){ + usleep(100000); + ssize_t n = read(e->out_fd, buf, sizeof(buf)-1); + if (n>0){ + buf[n]='\0'; + if (strstr(buf, "uciok")) { e->ready=true; break; } + } + } + if (!e->ready){ engine_stop(e); return false; } + engine_cmd(e, "isready\n"); + attempts=50; + while (attempts--){ + usleep(100000); + ssize_t n = read(e->out_fd, buf, sizeof(buf)-1); + if (n>0){ buf[n]='\0'; if (strstr(buf, "readyok")) break; } + } + return e->ready; +} + +bool engine_start(Engine *e){ + memset(e, 0, sizeof(*e)); e->pid=-1; e->in_fd=-1; e->out_fd=-1; e->ready=false; + if (try_start(e, "stockfish")) return true; + if (try_start(e, "asmfish")) return true; + return false; +} + +void engine_stop(Engine *e){ + if (e->in_fd!=-1){ write(e->in_fd, "quit\n", 5); close(e->in_fd); } + if (e->out_fd!=-1) close(e->out_fd); + if (e->pid>0){ int status; waitpid(e->pid, &status, 0); } + e->pid=-1; e->in_fd=e->out_fd=-1; e->ready=false; +} + +bool engine_cmd(Engine *e, const char *cmd){ + if (e->in_fd==-1) return false; + size_t len = strlen(cmd); + ssize_t n = write(e->in_fd, cmd, len); + if (n < 0) { + return false; + } + return n == (ssize_t)len; +} + +static void position_to_uci(const Position *pos, char *out, size_t outsz){ + // Send starting position and moves list by comparing with startpos; for simplicity, use FEN always. + char fen[256]; chess_to_fen(pos, fen, sizeof(fen)); + snprintf(out, outsz, "position fen %s\n", fen); +} + +size_t engine_get_top_moves(Engine *e, const Position *pos, EngineMove *out, size_t max){ + if (!e->ready) return 0; + char cmd[512]; position_to_uci(pos, cmd, sizeof(cmd)); engine_cmd(e, cmd); + // ask multiPV up to max (cap at 5 as requested) + size_t req = max; if (req>5) req=5; char go[128]; snprintf(go, sizeof(go), "setoption name MultiPV value %zu\n", req); engine_cmd(e, go); + engine_cmd(e, "go movetime 400\n"); + char buf[8192]; size_t count=0; int attempts=50; + while (attempts--){ + usleep(100000); + ssize_t n = read(e->out_fd, buf, sizeof(buf)-1); + if (n<=0) continue; + buf[n]='\0'; + char *line = strtok(buf, "\n"); + while (line){ + if (strncmp(line, "info ", 5)==0){ + // parse "info ... multipv X score cp Y ... pv " + char *mpv = strstr(line, " multipv "); char *score = strstr(line, " score "); char *pv = strstr(line, " pv "); + if (mpv && score && pv){ + int idx = atoi(mpv+9); if (idx>=1 && (size_t)idx<=req){ + int cp=0; if (strstr(score, "cp ")) cp = atoi(strstr(score, "cp ")+3); + char mv[8]={0}; + sscanf(pv+4, "%7s", mv); + size_t i = (size_t)idx-1; if (icount) count=i+1; } + } + } + } else if (strncmp(line, "bestmove ", 9)==0){ attempts=0; break; } + line = strtok(NULL, "\n"); + } + } + // simple sort by score descending (best to worst), keeping empties at end + for (size_t i=0;i out[i].score_cp){ EngineMove tmp=out[i]; out[i]=out[j]; out[j]=tmp; } + } + } + return count; +} + +bool engine_get_best_move(Engine *e, const Position *pos, char out_uci[8]){ + if (!e->ready) return false; + char cmd[512]; position_to_uci(pos, cmd, sizeof(cmd)); engine_cmd(e, cmd); + engine_cmd(e, "go movetime 300\n"); + char buf[4096]; int attempts=50; + while (attempts--){ + usleep(100000); + ssize_t n = read(e->out_fd, buf, sizeof(buf)-1); + if (n<=0) continue; + buf[n]='\0'; + char *line = strtok(buf, "\n"); + while (line){ + if (strncmp(line, "bestmove ", 9)==0){ sscanf(line+9, "%7s", out_uci); return true; } + line = strtok(NULL, "\n"); + } + } + return false; +} diff --git a/C/opening_learner/engine.h b/C/opening_learner/engine.h new file mode 100644 index 0000000..092918f --- /dev/null +++ b/C/opening_learner/engine.h @@ -0,0 +1,34 @@ +#ifndef ENGINE_H +#define ENGINE_H + +#include +#include + +#include "chess.h" + +typedef struct { + int score_cp; // centipawns relative to side to move + char uci[8]; +} EngineMove; + +typedef struct { + int pid; + int in_fd; // write to engine stdin + int out_fd; // read from engine stdout + bool ready; +} Engine; + +// Start engine: tries stockfish, then asmfish. Returns false if none. +bool engine_start(Engine *e); +void engine_stop(Engine *e); + +// Synchronous send command +bool engine_cmd(Engine *e, const char *cmd); + +// Ask for top N moves from a position (short fixed time). Returns count. +size_t engine_get_top_moves(Engine *e, const Position *pos, EngineMove *out, size_t max); + +// Ask for best move only. +bool engine_get_best_move(Engine *e, const Position *pos, char out_uci[8]); + +#endif diff --git a/C/opening_learner/engine.o b/C/opening_learner/engine.o new file mode 100644 index 0000000..e400c89 Binary files /dev/null and b/C/opening_learner/engine.o differ diff --git a/C/opening_learner/gui.c b/C/opening_learner/gui.c new file mode 100644 index 0000000..428315d --- /dev/null +++ b/C/opening_learner/gui.c @@ -0,0 +1,158 @@ +#include "gui.h" +#include +#include + +const SDL_Color COLOR_LIGHT = { 238, 238, 210, 255 }; // light square (not pure white) +const SDL_Color COLOR_DARK = { 118, 150, 86, 255 }; // dark square (not pure black) +const SDL_Color COLOR_GRID = { 20, 20, 20, 255 }; // thick outline +const SDL_Color COLOR_SEL = { 200, 50, 50, 200 }; // selection highlight +const SDL_Color COLOR_TEXT = { 10, 10, 10, 255 }; + +static void set_color(SDL_Renderer *r, SDL_Color c) { + SDL_SetRenderDrawColor(r, c.r, c.g, c.b, c.a); +} + +bool gui_init(Gui *g, int w, int h, const char *title) { + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + fprintf(stderr, "SDL_Init error: %s\n", SDL_GetError()); + return false; + } + g->window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, w, h, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + if (!g->window) { + fprintf(stderr, "SDL_CreateWindow error: %s\n", SDL_GetError()); + return false; + } + g->renderer = SDL_CreateRenderer(g->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!g->renderer) { + fprintf(stderr, "SDL_CreateRenderer error: %s\n", SDL_GetError()); + return false; + } + g->win_w = w; g->win_h = h; g->flipped = false; + return true; +} + +void gui_destroy(Gui *g) { + if (g->renderer) SDL_DestroyRenderer(g->renderer); + if (g->window) SDL_DestroyWindow(g->window); + SDL_Quit(); +} + +void gui_set_flipped(Gui *g, bool flipped) { g->flipped = flipped; } + +static void draw_rect(SDL_Renderer *r, int x, int y, int w, int h, SDL_Color c) { + set_color(r, c); + SDL_Rect rc = { x, y, w, h }; + SDL_RenderFillRect(r, &rc); +} + +static void draw_outline(SDL_Renderer *r, int x, int y, int w, int h, int thickness, SDL_Color c) { + set_color(r, c); + for (int i=0;i= 'A' && p <= 'Z') ? (SDL_Color){250, 250, 250, 255} : (SDL_Color){30, 30, 30, 255}; + SDL_Color glyph = (p >= 'A' && p <= 'Z') ? (SDL_Color){30, 30, 30, 255} : (SDL_Color){240, 240, 240, 255}; + // Base disk + draw_rect(r, x+size*0.15, y+size*0.15, (int)(size*0.7), (int)(size*0.7), fill); + // Glyph: draw a simple letter-like mark + set_color(r, glyph); + // vertical bar + SDL_Rect bar1 = { x + size/2 - size/16, y + size/3, size/8, size/3 }; + SDL_RenderFillRect(r, &bar1); + // top bar + SDL_Rect bar2 = { x + size/3, y + size/3 - size/10, size/3, size/10 }; + SDL_RenderFillRect(r, &bar2); +} + +void gui_draw(Gui *g, const char board[64], const GuiSelection *sel, const char *status_line) { + SDL_GetWindowSize(g->window, &g->win_w, &g->win_h); + + set_color(g->renderer, (SDL_Color){ 35, 35, 35, 255 }); + SDL_RenderClear(g->renderer); + + int size = (g->win_w < g->win_h ? g->win_w : g->win_h) - 40; // margins + if (size < 200) size = 200; + int cell = size / 8; + size = cell * 8; + int ox = (g->win_w - size)/2; + int oy = (g->win_h - size)/2; + + // Board outline (thick) + draw_outline(g->renderer, ox-6, oy-6, size+12, size+12, 6, COLOR_GRID); + + // Squares + for (int r=0;r<8;r++) { + for (int f=0;f<8;f++) { + int idx = g->flipped ? (63 - (r*8+f)) : (r*8+f); + SDL_Color c = ((r+f)&1) ? COLOR_DARK : COLOR_LIGHT; + draw_rect(g->renderer, ox + f*cell, oy + r*cell, cell, cell, c); + + char p = board[idx]; + if (p != '.' && p != '\0') { + draw_piece_letter(g->renderer, ox + f*cell, oy + r*cell, cell, p); + } + } + } + + // Selection overlay + if (sel && sel->clicked && sel->from_sq >= 0) { + int s = sel->from_sq; + int rr = g->flipped ? 7 - (s/8) : (s/8); + int ff = g->flipped ? 7 - (s%8) : (s%8); + draw_outline(g->renderer, ox + ff*cell+2, oy + rr*cell+2, cell-4, cell-4, 3, COLOR_SEL); + } + + // Status strip + draw_outline(g->renderer, 10, g->win_h - 40, g->win_w - 20, 30, 2, COLOR_GRID); + // Without TTF, we can't render text; draw a minimal indicator bar to signal state. + // If status_line indicates success/failure, alter color. + SDL_Color bar = { 80, 120, 200, 255 }; + if (status_line && status_line[0]) { + if (SDL_strstr(status_line, "Correct")) bar = (SDL_Color){80, 200, 120, 255}; + else if (SDL_strstr(status_line, "Wrong")) bar = (SDL_Color){200, 80, 80, 255}; + } + draw_rect(g->renderer, 12, g->win_h - 38, g->win_w - 24, 26, bar); + + SDL_RenderPresent(g->renderer); +} + +int gui_coord_to_sq(Gui *g, int x, int y) { + int w, h; SDL_GetWindowSize(g->window, &w, &h); + int size = (w < h ? w : h) - 40; if (size < 200) size = 200; + int cell = size / 8; size = cell * 8; + int ox = (w - size)/2; int oy = (h - size)/2; + if (x < ox || y < oy || x >= ox+size || y >= oy+size) return -1; + int f = (x - ox) / cell; + int r = (y - oy) / cell; + int sq = r*8 + f; + if (g->flipped) sq = 63 - sq; + return sq; +} + +bool gui_poll_move(Gui *g, GuiSelection *sel, bool *quit_requested, int *key_out) { + SDL_Event e; + bool updated = false; + if (key_out) *key_out = 0; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT) { if (quit_requested) *quit_requested = true; } + else if (e.type == SDL_WINDOWEVENT && e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + updated = true; + } else if (e.type == SDL_MOUSEBUTTONDOWN && e.button.button == SDL_BUTTON_LEFT) { + int sq = gui_coord_to_sq(g, e.button.x, e.button.y); + if (sq >= 0) { + if (!sel->clicked) { sel->from_sq = sq; sel->to_sq = -1; sel->clicked = true; } + else { sel->to_sq = sq; updated = true; } + } + } else if (e.type == SDL_KEYDOWN) { + if (e.key.keysym.sym == SDLK_ESCAPE) { sel->clicked = false; sel->from_sq = sel->to_sq = -1; updated = true; } + if (key_out) *key_out = e.key.keysym.sym; + } + } + return updated; +} diff --git a/C/opening_learner/gui.h b/C/opening_learner/gui.h new file mode 100644 index 0000000..c409657 --- /dev/null +++ b/C/opening_learner/gui.h @@ -0,0 +1,36 @@ +#ifndef GUI_H +#define GUI_H + +#include +#include + +typedef struct { + SDL_Window *window; + SDL_Renderer *renderer; + int win_w, win_h; + bool flipped; // true if black at bottom +} Gui; + +typedef struct { + int from_sq; // 0..63 or -1 + int to_sq; // 0..63 or -1 + char promo; // 'q','r','b','n' or 0 + bool clicked; +} GuiSelection; + +bool gui_init(Gui *g, int w, int h, const char *title); +void gui_destroy(Gui *g); +void gui_set_flipped(Gui *g, bool flipped); +void gui_draw(Gui *g, const char board[64], const GuiSelection *sel, const char *status_line); +// Returns true if something changed. If a key was pressed, key_out receives SDL_Keycode else 0. +bool gui_poll_move(Gui *g, GuiSelection *sel, bool *quit_requested, int *key_out); +int gui_coord_to_sq(Gui *g, int x, int y); + +// colors +extern const SDL_Color COLOR_LIGHT; +extern const SDL_Color COLOR_DARK; +extern const SDL_Color COLOR_GRID; +extern const SDL_Color COLOR_SEL; +extern const SDL_Color COLOR_TEXT; + +#endif diff --git a/C/opening_learner/gui.o b/C/opening_learner/gui.o new file mode 100644 index 0000000..46d7ba4 Binary files /dev/null and b/C/opening_learner/gui.o differ diff --git a/C/opening_learner/main.c b/C/opening_learner/main.c new file mode 100644 index 0000000..993f89e --- /dev/null +++ b/C/opening_learner/main.c @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include +#include + +#include "gui.h" +#include "chess.h" +#include "engine.h" +#include "mistakes.h" + +typedef struct { + Position pos; + Engine engine; + Gui gui; + MistakeList mistakes; + bool replay_mode; // allow revisiting mistakes + size_t replay_index; +} App; + +static void append_uci(char *line, size_t sz, const char *mv){ + if (line[0]) strncat(line, " ", sz-1); + strncat(line, mv, sz-1); +} + +static void position_push_move_text(Position *pos, const Move *m, char *line, size_t lsz){ + (void)pos; + char u[8]; move_to_uci(m, u); + append_uci(line, lsz, u); +} + +static void collect_all_legal_uci(const Position *pos, char list[][8], size_t *n, size_t max){ + Move mv[MAX_MOVES]; size_t k = chess_generate_legal_moves(pos, mv, MAX_MOVES); + *n = 0; + for (size_t i=0;i0)? (rand()%wsum) : 0; + size_t pick=0; for (size_t i=0;i0 && parse_uci_move(props[0].uci, &app.pos, &m)) pick=0; else { snprintf(status, sizeof(status), "No engine move"); quit=true; continue; } + } + chess_make_move(&app.pos, &m); + position_push_move_text(&app.pos, &m, line_uci, sizeof(line_uci)); + awaiting_player = true; + // 6. Ask engine for optimal response from player + engine_get_best_move(&app.engine, &app.pos, expected_player_move); + snprintf(status, sizeof(status), "Your turn"); + continue; + } + + // Player move input + int key=0; + bool updated = gui_poll_move(&app.gui, &sel, &quit, &key); + if (quit) break; + if (key=='m' || key=='M'){ + // enter simple replay: load a mistake line and restart from it to the position where best move is required + if (app.mistakes.count>0){ + if (app.replay_index >= app.mistakes.count) app.replay_index = 0; + Mistake *mk = &app.mistakes.items[app.replay_index++]; + // reset and play the line moves to reach the mistake position + chess_init_start(&app.pos); + char tmp[512]; snprintf(tmp, sizeof(tmp), "%s", mk->line); + char *tok = strtok(tmp, " "); + while (tok){ Move m; if (parse_uci_move(tok, &app.pos, &m)) chess_make_move(&app.pos, &m); tok = strtok(NULL, " "); } + snprintf(status, sizeof(status), "Practice: best is %s", mk->best_move); + strncpy(expected_player_move, mk->best_move, sizeof(expected_player_move)); + awaiting_player = true; + gui_set_flipped(&app.gui, !app.pos.white_to_move); // if it's black to move, flip so black bottom + } + } + if (updated && sel.clicked && sel.to_sq>=0){ + Move list[MAX_MOVES]; size_t n = chess_generate_legal_moves(&app.pos, list, MAX_MOVES); + bool moved=false; Move chosen={0}; + for (size_t i=0;i +#include +#include + +void mistakes_init(MistakeList *ml) { + ml->items = NULL; ml->count = 0; ml->cap = 0; +} + +void mistakes_free(MistakeList *ml) { + free(ml->items); ml->items = NULL; ml->count = ml->cap = 0; +} + +static void ensure_cap(MistakeList *ml, size_t need) { + if (need <= ml->cap) return; + size_t ncap = ml->cap ? ml->cap*2 : 16; + while (ncap < need) ncap *= 2; + Mistake *ni = realloc(ml->items, ncap * sizeof(Mistake)); + if (!ni) return; // OOM silently ignored + ml->items = ni; ml->cap = ncap; +} + +void mistakes_add(MistakeList *ml, const char *fen, const char *best_move, const char *line) { + ensure_cap(ml, ml->count+1); + Mistake *m = &ml->items[ml->count++]; + snprintf(m->fen, sizeof(m->fen), "%s", fen); + snprintf(m->best_move, sizeof(m->best_move), "%s", best_move); + snprintf(m->line, sizeof(m->line), "%s", line); +} + +bool mistakes_save(const MistakeList *ml, const char *path) { + FILE *f = fopen(path, "w"); + if (!f) return false; + for (size_t i=0;icount;i++) { + const Mistake *m = &ml->items[i]; + fprintf(f, "FEN:%s\nBEST:%s\nLINE:%s\n.\n", m->fen, m->best_move, m->line); + } + fclose(f); return true; +} + +bool mistakes_load(MistakeList *ml, const char *path) { + FILE *f = fopen(path, "r"); + if (!f) return false; + char buf[1024]; char fen[128] = ""; char best[16] = ""; char line[512] = ""; + while (fgets(buf, sizeof(buf), f)) { + if (strncmp(buf, "FEN:", 4) == 0) { + // copy up to 127 chars, strip newline + size_t l = strnlen(buf+4, sizeof(fen)-1); + memcpy(fen, buf+4, l); fen[l]='\0'; + if (l && fen[l-1]=='\n') fen[l-1]='\0'; + } else if (strncmp(buf, "BEST:", 5) == 0) { + size_t l = strnlen(buf+5, sizeof(best)-1); + memcpy(best, buf+5, l); best[l]='\0'; + if (l && best[l-1]=='\n') best[l-1]='\0'; + } else if (strncmp(buf, "LINE:", 5) == 0) { + size_t l = strnlen(buf+5, sizeof(line)-1); + memcpy(line, buf+5, l); line[l]='\0'; + if (l && line[l-1]=='\n') line[l-1]='\0'; + } else if (buf[0]=='.') { + mistakes_add(ml, fen, best, line); + fen[0]=best[0]=line[0]='\0'; + } + } + fclose(f); return true; +} diff --git a/C/opening_learner/mistakes.h b/C/opening_learner/mistakes.h new file mode 100644 index 0000000..7f77ce5 --- /dev/null +++ b/C/opening_learner/mistakes.h @@ -0,0 +1,28 @@ +#ifndef MISTAKES_H +#define MISTAKES_H + +#include +#include + +// A lightweight mistake store in memory + file persistence. + +typedef struct { + char fen[128]; + char best_move[8]; + // PGN-like ply list in UCI for context + char line[512]; +} Mistake; + +typedef struct { + Mistake *items; + size_t count; + size_t cap; +} MistakeList; + +void mistakes_init(MistakeList *ml); +void mistakes_free(MistakeList *ml); +void mistakes_add(MistakeList *ml, const char *fen, const char *best_move, const char *line); +bool mistakes_save(const MistakeList *ml, const char *path); +bool mistakes_load(MistakeList *ml, const char *path); + +#endif diff --git a/C/opening_learner/mistakes.o b/C/opening_learner/mistakes.o new file mode 100644 index 0000000..e8e7eba Binary files /dev/null and b/C/opening_learner/mistakes.o differ diff --git a/C/opening_learner/mistakes.txt b/C/opening_learner/mistakes.txt new file mode 100644 index 0000000..3512742 --- /dev/null +++ b/C/opening_learner/mistakes.txt @@ -0,0 +1,12 @@ +FEN:rnbqkbnr/pppp1ppp/4p3/8/8/N4N2/PPPPPPPP/R1BQKB1R b KQkq - 1 2 +BEST:f8a3 +LINE:g1f3 e7e6 b1a3 +. +FEN:rnbqkbnr/pppppppp/8/8/8/7P/PPPPPPP1/RNBQKBNR b KQkq - 0 1 +BEST:e7e5 +LINE:h2h3 +. +FEN:rnbqkbnr/pppp1ppp/8/4p3/8/4P3/PPPPKPPP/RNBQ1BNR b kq - 1 2 +BEST:d7d5 +LINE:e2e3 e7e5 e1e2 +. diff --git a/C/opening_learner/opening_learner b/C/opening_learner/opening_learner new file mode 100755 index 0000000..578843f Binary files /dev/null and b/C/opening_learner/opening_learner differ diff --git a/C/opening_learner/run.sh b/C/opening_learner/run.sh new file mode 100755 index 0000000..e88c54c --- /dev/null +++ b/C/opening_learner/run.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +# Install SDL2 dev if sdl2-config is missing; otherwise build and run. + +if ! command -v sdl2-config >/dev/null 2>&1; then + echo "sdl2-config not found. Attempting to install SDL2 dev..." + if [ -f /etc/os-release ]; then + . /etc/os-release + case "$ID" in + ubuntu|debian|linuxmint|neon|pop) + sudo apt-get update + sudo apt-get install -y libsdl2-dev + ;; + arch|manjaro|endeavouros) + sudo pacman -Syu --noconfirm sdl2 + ;; + fedora) + sudo dnf install -y SDL2-devel + ;; + opensuse*|sles) + sudo zypper install -y libSDL2-devel + ;; + void) + sudo xbps-install -Sy SDL2-devel + ;; + alpine) + sudo apk add sdl2-dev + ;; + *) + echo "Unsupported distro ($ID). Please install SDL2 dev manually and rerun." >&2 + exit 3 + ;; + esac + else + echo "/etc/os-release not found; cannot auto-detect distro. Install SDL2 dev manually." >&2 + exit 3 + fi +fi + +./check_build.sh +./opening_learner