testsAndMisc/C/opening_learner/main.c

284 lines
9.3 KiB
C

#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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;
}