#include #include #include #include #include #include #include "chess.h" #include "engine.h" #include "gui.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; i < k && *n < max; i++) { char u[8]; move_to_uci(&mv[i], u); strncpy(list[(*n)++], u, 8); } } static bool uci_in_list(const char *u, char list[][8], size_t n) { for (size_t i = 0; i < n; i++) { if (strncmp(u, list[i], 8) == 0) return true; } return false; } static const char *mistake_file_path(void) { return "mistakes.txt"; } int main(void) { srand((unsigned)time(NULL)); App app; memset(&app, 0, sizeof(app)); mistakes_init(&app.mistakes); mistakes_load(&app.mistakes, mistake_file_path()); // Avoid SIGPIPE crashes when engine pipe closes (void)signal(SIGPIPE, SIG_IGN); if (!gui_init(&app.gui, 720, 760, "Opening Learner")) { (void)fprintf(stderr, "GUI init failed.\n"); return 1; } if (!engine_start(&app.engine)) { (void)fprintf(stderr, "Error: Neither stockfish nor asmfish found locally. Please install one.\n"); gui_destroy(&app.gui); return 1; } // Initialize position chess_init_start(&app.pos); // Randomly pick side bool player_is_white = rand() % 2 == 0; gui_set_flipped(&app.gui, !player_is_white); // gameplay state char status[128] = ""; GuiSelection sel = {.from_sq = -1, .to_sq = -1, .promo = 0, .clicked = false}; char line_uci[512] = ""; // history of moves uci // If white, player moves first bool awaiting_player = player_is_white; char expected_player_move[8] = ""; // best move suggested by engine for player bool quit = false; while (!quit) { // Show board gui_draw(&app.gui, app.pos.board, &sel, status); // Engine to act when it's engine turn if (!awaiting_player) { // 2. Ask engine for proposed responses (5) EngineMove props[5]; size_t n = engine_get_top_moves(&app.engine, &app.pos, props, 5); // 3. Sort is done in engine; also collect legal ones not proposed char legal[256][8]; size_t lcnt = 0; collect_all_legal_uci(&app.pos, legal, &lcnt, 256); // 4. pick response with decreasing probability // add non-proposed at the end with minimal priority char pool[300][8]; int weights[300]; size_t pcnt = 0; for (size_t i = 0; i < n; i++) { strncpy(pool[pcnt], props[i].uci, 8); weights[pcnt++] = (int)(n - i); } for (size_t i = 0; i < lcnt; i++) { if (!uci_in_list(legal[i], pool, pcnt)) { memcpy(pool[pcnt], legal[i], 8); pool[pcnt][7] = '\0'; weights[pcnt++] = 1; } } // weighted pick int wsum = 0; for (size_t i = 0; i < pcnt; i++) wsum += weights[i]; int r = (wsum > 0) ? (rand() % wsum) : 0; size_t pick = 0; for (size_t i = 0; i < pcnt; i++) { if (r < weights[i]) { pick = i; break; } r -= weights[i]; } // 5. play response Move m; if (!parse_uci_move(pool[pick], &app.pos, &m)) { // fallback: best if (!(n > 0 && parse_uci_move(props[0].uci, &app.pos, &m))) { (void)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); (void)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]; (void)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, " "); } (void)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 < n; i++) { if (list[i].from == sel.from_sq && list[i].to == sel.to_sq) { chosen = list[i]; moved = true; break; } } sel.clicked = false; sel.from_sq = sel.to_sq = -1; sel.promo = 0; if (!moved) continue; // 7. Compare with expected move char uci[8]; move_to_uci(&chosen, uci); bool correct = (expected_player_move[0] && strncmp(uci, expected_player_move, 8) == 0); if (correct) { chess_make_move(&app.pos, &chosen); position_push_move_text(&app.pos, &chosen, line_uci, sizeof(line_uci)); (void)snprintf(status, sizeof(status), "Correct"); awaiting_player = false; // engine move next } else { // log mistake: 1) save all moves that lead to mistake, 2) allow revisit later, 3) // reset position char fen[256]; chess_to_fen(&app.pos, fen, sizeof(fen)); MistakeEntry entry = { .fen = fen, .best_move = expected_player_move, .line = line_uci, }; mistakes_add(&app.mistakes, &entry); (void)mistakes_save(&app.mistakes, mistake_file_path()); (void)snprintf(status, sizeof(status), "Wrong, best was %s", expected_player_move); // redo best move to show Move best; if (parse_uci_move(expected_player_move, &app.pos, &best)) { chess_make_move(&app.pos, &best); position_push_move_text(&app.pos, &best, line_uci, sizeof(line_uci)); } gui_draw(&app.gui, app.pos.board, &sel, status); SDL_Delay(600); // reset to start chess_init_start(&app.pos); line_uci[0] = '\0'; // randomize side again player_is_white = rand() % 2 == 0; gui_set_flipped(&app.gui, !player_is_white); awaiting_player = player_is_white; expected_player_move[0] = '\0'; } } SDL_Delay(10); } mistakes_save(&app.mistakes, mistake_file_path()); gui_destroy(&app.gui); engine_stop(&app.engine); mistakes_free(&app.mistakes); return 0; }