app.c (39871B)
1 /* app.c - el cliente: ajustes, menus, bucle de juego, puzzles, visor y analisis. */ 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <string.h> 5 #include <time.h> 6 #include <unistd.h> 7 #include "app.h" 8 9 const TcPreset TC_PRESETS[] = { 10 { "-", 0, 0, 0 }, 11 { "1+0", 60000, 0, 0 }, { "3+0", 180000, 0, 0 }, { "5+0", 300000, 0, 0 }, 12 { "10+0", 600000, 0, 0 }, { "15+0", 900000, 0, 0 }, { "3+2", 180000, 2000, 0 }, 13 { "5+5", 300000, 5000, 0 }, { "10+5", 600000, 5000, 0 }, { "15+10", 900000, 10000, 0 }, 14 }; 15 const int NTC_PRESETS = (int)(sizeof TC_PRESETS / sizeof TC_PRESETS[0]); 16 17 static const char *const YESNO[2][2] = { { "No", "Sí" }, { "No", "Yes" } }; 18 static const char *const LANGS[] = { "Español", "English" }; 19 static const char *const LEVELS[2][LEVEL_COUNT + 1] = { 20 { "Fácil", "Principiante", "Medio", "Difícil", "Máximo", "Stockfish" }, 21 { "Easy", "Beginner", "Medium", "Hard", "Maximum", "Stockfish" }, 22 }; 23 24 /* niveles del menu: Stockfish aparece solo si esta instalado */ 25 static int nlevels(void) { return stockfish_path() ? LEVEL_COUNT + 1 : LEVEL_COUNT; } 26 static int level_ok(int lv) { return lv < nlevels() ? lv : LEVEL_COUNT - 1; } 27 static const char *const COLORS[2][3] = { { "Blancas", "Negras", "Al azar" }, { "White", "Black", "Random" } }; 28 static const char *const SPEEDS[2][3] = { { "Rápido", "Normal", "Lento" }, { "Fast", "Normal", "Slow" } }; 29 30 /* ------------------------------------------------------------ ajustes */ 31 32 void settings_load(Settings *st) 33 { 34 memset(st, 0, sizeof *st); 35 st->scale = 2; 36 st->layout = LAYOUT_WIDE; 37 st->level = 2; 38 st->color = 0; 39 st->tc = 0; 40 st->undo = 1; 41 st->show_eval = 1; 42 st->watch_speed = 1; 43 const char *u = getenv("USER"); 44 snprintf(st->name, sizeof st->name, "%s", u && *u ? u : "Jugador"); 45 FILE *f = fopen(config_path(), "r"); 46 if (!f) return; 47 char line[256], key[32], val[200]; 48 while (fgets(line, sizeof line, f)) { 49 if (sscanf(line, "%31[^=]=%199[^\n]", key, val) != 2) continue; 50 int v = atoi(val); 51 if (!strcmp(key, "lang")) st->lang = v != 0; 52 else if (!strcmp(key, "theme")) st->theme = v >= 0 && v < NTHEMES ? v : 0; 53 else if (!strcmp(key, "layout")) st->layout = v ? LAYOUT_WIDE : LAYOUT_COMPACT; 54 else if (!strcmp(key, "scale")) st->scale = v > 0 && v < 8 ? v : 2; 55 else if (!strcmp(key, "level")) st->level = v >= 0 && v <= LEVEL_STOCKFISH ? v : 2; 56 else if (!strcmp(key, "color")) st->color = v >= 0 && v < 3 ? v : 0; 57 else if (!strcmp(key, "tc")) st->tc = v >= 0 && v < NTC_PRESETS ? v : 0; 58 else if (!strcmp(key, "undo")) st->undo = v != 0; 59 else if (!strcmp(key, "eval")) st->show_eval = v != 0; 60 else if (!strcmp(key, "watch")) st->watch_speed = v >= 0 && v < 3 ? v : 1; 61 else if (!strcmp(key, "name")) snprintf(st->name, sizeof st->name, "%.19s", val); 62 else if (!strcmp(key, "server")) snprintf(st->server, sizeof st->server, "%.127s", val); 63 } 64 fclose(f); 65 } 66 67 void settings_save(const Settings *st) 68 { 69 FILE *f = fopen(config_path(), "w"); 70 if (!f) return; 71 fprintf(f, "lang=%d\ntheme=%d\nlayout=%d\nscale=%d\nlevel=%d\ncolor=%d\ntc=%d\nundo=%d\neval=%d\nwatch=%d\nname=%s\nserver=%s\n", 72 st->lang, st->theme, st->layout == LAYOUT_WIDE, st->scale, st->level, st->color, st->tc, st->undo, 73 st->show_eval, st->watch_speed, st->name, st->server); 74 fclose(f); 75 } 76 77 char *read_file(const char *path, long *len) 78 { 79 FILE *f = fopen(path, "rb"); 80 if (!f) return 0; 81 fseek(f, 0, SEEK_END); 82 long n = ftell(f); 83 fseek(f, 0, SEEK_SET); 84 if (n < 0 || n > 4 << 20) { fclose(f); return 0; } 85 char *b = malloc((size_t)n + 1); 86 if (b && fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); b = 0; } 87 fclose(f); 88 if (b) b[n] = 0; 89 if (len) *len = n; 90 return b; 91 } 92 93 /* ------------------------------------------------------------ formularios */ 94 95 int run_form(Frontend *fe, Form *f, void (*on_change)(Form *, void *), void *ctx) 96 { 97 if (on_change) on_change(f, ctx); 98 for (;;) { 99 fe->draw_form(fe, f); 100 InEvent ev; 101 if (!fe->wait(fe, 1000, 0, 0, &ev)) continue; 102 int r = FORM_NONE; 103 if (ev.type == EV_QUIT) return FORM_CANCEL; 104 if (ev.type == EV_KEY) r = form_key(f, ev.key); 105 else if (ev.type == EV_CLICK) { 106 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 107 if (h && h->kind == HIT_APP) r = form_click(f, h->id); 108 } 109 if (r != FORM_NONE) return r; 110 f->msg[0] = 0; 111 if (on_change) on_change(f, ctx); 112 } 113 } 114 115 /* mensaje simple con un boton */ 116 static void notice(Frontend *fe, const char *title, const char *text) 117 { 118 Form f; 119 form_begin(&f, title); 120 form_label(&f, text); 121 form_button(&f, 1, "OK"); 122 f.cur = 1; 123 run_form(fe, &f, 0, 0); 124 } 125 126 /* ------------------------------------------------------------ exportar */ 127 128 static void stamp(char *out, int n, const char *ext) 129 { 130 time_t t = time(0); 131 struct tm tm; 132 localtime_r(&t, &tm); 133 snprintf(out, (size_t)n, "%s/saves/%04d%02d%02d-%02d%02d%02d.%s", data_dir(), tm.tm_year + 1900, tm.tm_mon + 1, 134 tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, ext); 135 } 136 137 static void pgn_tags(const Session *s, PgnTags *t) 138 { 139 memset(t, 0, sizeof *t); 140 snprintf(t->white, sizeof t->white, "%s", s->names[WHITE]); 141 snprintf(t->black, sizeof t->black, "%s", s->names[BLACK]); 142 snprintf(t->event, sizeof t->event, "%s", g_lang ? "Casual game" : "Partida amistosa"); 143 time_t now = time(0); 144 struct tm tm; 145 localtime_r(&now, &tm); 146 snprintf(t->date, sizeof t->date, "%04u.%02u.%02u", (unsigned)(tm.tm_year + 1900) % 10000u, (unsigned)(tm.tm_mon + 1) % 100u, (unsigned)tm.tm_mday % 100u); 147 } 148 149 static bool export_pgn(const Session *s, char *path, int n) 150 { 151 static char buf[1 << 16]; 152 PgnTags t; 153 pgn_tags(s, &t); 154 if (game_pgn(&s->g, &t, buf, sizeof buf) < 0) return false; 155 stamp(path, n, "pgn"); 156 FILE *f = fopen(path, "w"); 157 if (!f) return false; 158 fputs(buf, f); 159 fclose(f); 160 return true; 161 } 162 163 static bool export_fen(const Session *s, char *path, int n) 164 { 165 char fen[100]; 166 pos_fen(&s->g.pos, fen, sizeof fen); 167 stamp(path, n, "fen"); 168 FILE *f = fopen(path, "w"); 169 if (!f) return false; 170 fprintf(f, "%s\n", fen); 171 fclose(f); 172 return true; 173 } 174 175 /* ------------------------------------------------------------ analisis */ 176 177 static uint32_t mono32(void) { return (uint32_t)now_ms(); } 178 179 static void analyze(Frontend *fe, const Game *g) 180 { 181 if (!g->nply) return; 182 TTEntry *tt = calloc(1u << 18, sizeof(TTEntry)); 183 Engine e; 184 engine_init(&e, tt, tt ? 1u << 18 : 0, mono32); 185 int *score = calloc((size_t)g->nply + 1, sizeof(int)); /* desde blancas */ 186 Move *best = calloc((size_t)g->nply + 1, sizeof(Move)); 187 if (!score || !best) { free(tt); free(score); free(best); return; } 188 Game h; 189 game_init(&h, &g->start, 0, false); 190 /* con Stockfish el analisis es mucho mas confiable */ 191 Uci sf; 192 bool use_sf = uci_open(&sf, stockfish_path()); 193 volatile int never = 0; 194 Form prog; 195 for (int i = 0; i <= g->nply; i++) { 196 char t[64]; 197 snprintf(t, sizeof t, g_lang ? "Analyzing move %d of %d..." : "Analizando jugada %d de %d...", i, g->nply); 198 form_begin(&prog, g_lang ? "Analysis" : "Análisis"); 199 form_label(&prog, t); 200 fe->draw_form(fe, &prog); 201 SearchLimits lim; 202 memset(&lim, 0, sizeof lim); 203 lim.movetime = 250; 204 SearchResult r; 205 MoveList l; 206 pos_legal(&h.pos, &l); 207 if (l.n) { 208 if (!use_sf || !uci_search(&sf, &h, lim.movetime, &never, &r)) { 209 if (use_sf) { uci_close(&sf); use_sf = false; } 210 engine_search(&e, &h, &lim, &r); 211 } 212 score[i] = h.pos.side == WHITE ? r.score : -r.score; 213 best[i] = r.best; 214 } else score[i] = pos_in_check(&h.pos) ? (h.pos.side == WHITE ? -MATE : MATE) : 0; 215 if (i < g->nply) { 216 Act a = { ACT_MOVE, (int8_t)h.pos.side, g->mv[i], 0 }; 217 game_apply(&h, &a); 218 } 219 } 220 /* lineas del informe y totales por bando */ 221 static char lines[MAX_PLY][80]; 222 int nl = 0, count[2][3] = { { 0 } }; 223 Pos p = g->start; 224 for (int i = 0; i < g->nply; i++) { 225 int side = p.side; 226 int before = side == WHITE ? score[i] : -score[i], after = side == WHITE ? score[i + 1] : -score[i + 1]; 227 if (before > 2000) before = 2000; 228 if (before < -2000) before = -2000; 229 if (after > 2000) after = 2000; 230 if (after < -2000) after = -2000; 231 int loss = before - after; 232 const char *mark = ""; 233 if (loss >= 200) { mark = "??"; count[side][2]++; } 234 else if (loss >= 100) { mark = "?"; count[side][1]++; } 235 else if (loss >= 50) { mark = "?!"; count[side][0]++; } 236 char san[16], bs[16] = ""; 237 move_san(&p, g->mv[i], san, sizeof san); 238 if (loss >= 50 && best[i]) move_san(&p, best[i], bs, sizeof bs); 239 if (*mark) { 240 snprintf(lines[nl++], sizeof lines[0], "%d%s %s%s (%s: %s)", p.fullmove, side == WHITE ? "." : "...", san, mark, 241 g_lang ? "best" : "mejor", bs); 242 } 243 Undo u; 244 pos_make(&p, g->mv[i], &u); 245 } 246 if (use_sf) uci_close(&sf); 247 free(tt); 248 free(score); 249 free(best); 250 /* paginas de 12 renglones */ 251 int page = 0, per = 12, pages = nl ? (nl + per - 1) / per : 1; 252 for (;;) { 253 Form f; 254 form_begin(&f, g_lang ? "Game analysis" : "Análisis de la partida"); 255 char t[96]; 256 for (int c = 0; c < 2; c++) { 257 snprintf(t, sizeof t, g_lang ? "%s: %d inaccuracies, %d mistakes, %d blunders" : "%s: %d imprecisiones, %d errores, %d errores graves", 258 color_name(c), count[c][0], count[c][1], count[c][2]); 259 form_label(&f, t); 260 } 261 if (!nl) form_label(&f, g_lang ? "No bad moves. Well played!" : "Ninguna mala jugada. ¡Bien jugado!"); 262 for (int i = page * per; i < nl && i < (page + 1) * per; i++) form_label(&f, lines[i]); 263 if (page + 1 < pages) form_button(&f, 2, g_lang ? "Next page" : "Página siguiente"); 264 if (page > 0) form_button(&f, 3, g_lang ? "Previous page" : "Página anterior"); 265 form_button(&f, 1, g_lang ? "Back" : "Volver"); 266 f.cur = f.n - 1; 267 for (int i = 0; i < f.n; i++) if (f.f[i].type == F_BUTTON) { f.cur = i; break; } 268 int r = run_form(fe, &f, 0, 0); 269 if (r == 2) page++; 270 else if (r == 3) page--; 271 else return; 272 } 273 } 274 275 /* ------------------------------------------------------------ bucle de juego */ 276 277 /* color que "actua" desde esta pantalla para ofrecer tablas, deshacer, abandonar */ 278 static int acting_color(const Session *s, bool for_undo) 279 { 280 uint8_t m = sess_local_mask(s); 281 if (m == 3) return for_undo ? s->g.pos.side ^ 1 : s->g.pos.side; /* dos en la misma compu */ 282 if (m & 1) return WHITE; 283 if (m & 2) return BLACK; 284 return -1; 285 } 286 287 static int submit(Session *s, Ctrl *c, int type, int color) 288 { 289 if (color < 0) return E_TURN; 290 Act a = { (uint8_t)type, (int8_t)color, 0, 0 }; 291 int e = sess_submit(s, &a); 292 if (e) ctrl_error(c, e); 293 return e; 294 } 295 296 enum { PM_CONTINUE = 1, PM_DRAW, PM_CLAIM, PM_UNDO, PM_RESIGN, PM_FLIP, PM_THEME, PM_EVAL, PM_PGN, PM_FEN, PM_ANALYZE, PM_LANG, PM_QUIT }; 297 298 static const char *const THEME_NAMES[NTHEMES] = { "classic", "ocean", "night", "contrast", "colorblind", "sunset" }; 299 300 /* menu de pausa; devuelve true si hay que salir de la partida */ 301 static bool pause_menu(Frontend *fe, Session *s, Ctrl *c, Settings *st) 302 { 303 for (;;) { 304 bool en = g_lang; 305 Form f; 306 form_begin(&f, en ? "Game menu" : "Menú de la partida"); 307 bool over = s->g.result != R_NONE; 308 form_button(&f, PM_CONTINUE, en ? "Continue" : "Seguir jugando"); 309 if (!over) { 310 form_button(&f, PM_DRAW, en ? "Offer a draw" : "Ofrecer tablas"); 311 if (game_can_claim(&s->g)) form_button(&f, PM_CLAIM, en ? "Claim a draw" : "Reclamar tablas"); 312 if (s->g.allow_undo) form_button(&f, PM_UNDO, en ? "Take back" : "Deshacer"); 313 form_button(&f, PM_RESIGN, en ? "Resign" : "Abandonar"); 314 } else { 315 Act u = { ACT_UNDO, (int8_t)acting_color(s, true), 0, 0 }; 316 if (s->g.allow_undo && game_check(&s->g, &u) == E_OK) form_button(&f, PM_UNDO, en ? "Take back" : "Deshacer"); 317 form_button(&f, PM_ANALYZE, en ? "Analyze the game" : "Analizar la partida"); 318 } 319 form_button(&f, PM_FLIP, en ? "Flip the board" : "Girar el tablero"); 320 form_choice(&f, PM_THEME, en ? "Theme" : "Tema", THEME_NAMES, NTHEMES, st->theme); 321 form_choice(&f, PM_EVAL, en ? "Evaluation bar" : "Barra de evaluación", YESNO[en], 2, st->show_eval); 322 form_choice(&f, PM_LANG, en ? "Language" : "Idioma", LANGS, 2, g_lang); 323 form_button(&f, PM_PGN, en ? "Export PGN" : "Exportar PGN"); 324 form_button(&f, PM_FEN, en ? "Export FEN" : "Exportar FEN"); 325 form_button(&f, PM_QUIT, en ? "Back to the main menu (it's saved)" : "Volver al menú (queda guardada)"); 326 if (s->pgn_path[0]) { 327 form_label(&f, en ? "PGN of this game (updated after each move):" : "PGN de esta partida (se actualiza en cada jugada):"); 328 form_label(&f, s->pgn_path); 329 form_label(&f, last_pgn_path()); 330 } 331 int r = run_form(fe, &f, 0, 0); 332 st->theme = form_get(&f, PM_THEME)->value; 333 st->show_eval = form_get(&f, PM_EVAL)->value; 334 g_lang = form_get(&f, PM_LANG)->value; 335 st->lang = g_lang; 336 s->want_eval = st->show_eval; 337 settings_save(st); 338 char path[600]; 339 switch (r) { 340 case PM_DRAW: submit(s, c, ACT_DRAW, acting_color(s, false)); return false; 341 case PM_CLAIM: submit(s, c, ACT_CLAIM, s->g.pos.side); return false; 342 case PM_UNDO: submit(s, c, ACT_UNDO, acting_color(s, true)); return false; 343 case PM_RESIGN: submit(s, c, ACT_RESIGN, acting_color(s, false)); return false; 344 case PM_FLIP: c->flipped = !c->flipped; return false; 345 case PM_ANALYZE: analyze(fe, &s->g); continue; 346 case PM_PGN: case PM_FEN: { 347 bool ok = r == PM_PGN ? export_pgn(s, path, sizeof path) : export_fen(s, path, sizeof path); 348 notice(fe, r == PM_PGN ? "PGN" : "FEN", ok ? path : (en ? "Could not write the file" : "No se pudo escribir el archivo")); 349 continue; 350 } 351 case PM_QUIT: return true; 352 default: return false; 353 } 354 } 355 } 356 357 static void build_view(View *v, Session *s, Ctrl *c, Settings *st, char *banner, int bn) 358 { 359 memset(v, 0, sizeof *v); 360 v->g = &s->g; 361 v->c = c; 362 v->names[0] = s->names[0]; 363 v->names[1] = s->names[1]; 364 v->has_clock = game_has_clock(&s->g); 365 v->clock[0] = sess_clock(s, WHITE); 366 v->clock[1] = sess_clock(s, BLACK); 367 v->clock_running = s->g.result == R_NONE ? s->g.pos.side : -1; 368 v->theme = st->theme; 369 v->layout = st->layout; 370 v->show_eval = st->show_eval; 371 v->eval = s->eval; 372 v->eval_valid = s->eval_valid; 373 v->view_ply = -1; 374 v->status = sess_thinking(s) ? tr(S_P_THINK) : s->status[0] ? s->status : 0; 375 const char *eco = 0; 376 static char op[96]; 377 const char *name = opening_name(s->g.mv, s->g.nply, &s->g.start, &eco); 378 if (name) { snprintf(op, sizeof op, "%s %s", eco, name); v->opening = op; } 379 uint8_t local = sess_local_mask(s); 380 banner[0] = 0; 381 const Game *g = &s->g; 382 if (g->result == R_NONE) { 383 if (g->draw_offer && ((local >> ((g->draw_offer - 1) ^ 1)) & 1)) 384 snprintf(banner, (size_t)bn, "%s %s", color_name(g->draw_offer - 1), tr(S_DRAW_OFFER)); 385 else if (g->undo_req && ((local >> ((g->undo_req - 1) ^ 1)) & 1)) 386 snprintf(banner, (size_t)bn, "%s %s", color_name(g->undo_req - 1), tr(S_UNDO_REQ)); 387 else if (g->draw_offer && ((local >> (g->draw_offer - 1)) & 1)) snprintf(banner, (size_t)bn, "%s", tr(S_DRAW_SENT)); 388 else if (g->undo_req && ((local >> (g->undo_req - 1)) & 1)) snprintf(banner, (size_t)bn, "%s", tr(S_UNDO_SENT)); 389 else if (game_can_claim(g) && ((local >> g->pos.side) & 1)) snprintf(banner, (size_t)bn, "%s", tr(S_CAN_CLAIM)); 390 } 391 if (banner[0]) v->banner = banner; 392 } 393 394 void app_play(Frontend *fe, Session *s, Settings *st) 395 { 396 Ctrl c; 397 uint8_t local = sess_local_mask(s); 398 ctrl_init(&c, local, local == 2); 399 s->want_eval = st->show_eval; 400 s->engine_delay = s->kind[0] == PL_ENGINE && s->kind[1] == PL_ENGINE ? (st->watch_speed == 0 ? 100 : st->watch_speed == 1 ? 700 : 1800) : 250; 401 bool help = false; 402 int modal = 0; /* 1 salir, 2 abandonar */ 403 int last_nply = -1, last_result = -1; 404 char banner[160]; 405 for (;;) { 406 sess_poll(s); 407 if (s->err_pending) { ctrl_error(&c, s->last_err); s->err_pending = false; } 408 ctrl_sync(&c, &s->g); 409 if (s->g.nply != last_nply || s->g.result != last_result) { 410 if (last_nply >= 0 && s->g.result != R_NONE && last_result == R_NONE) c.msg[0] = 0; 411 last_nply = s->g.nply; 412 last_result = s->g.result; 413 } 414 View v; 415 build_view(&v, s, &c, st, banner, sizeof banner); 416 v.show_help = help; 417 if (modal == 1) { v.modal = g_lang ? "Quit? (the game is saved)" : "¿Salir? (la partida queda guardada)"; v.modal2 = g_lang ? "y / n" : "s / n"; } 418 if (modal == 2) { v.modal = g_lang ? "Resign the game?" : "¿Abandonar la partida?"; v.modal2 = g_lang ? "y / n" : "s / n"; } 419 fe->draw_game(fe, &v); 420 if (s->ended) return; 421 int fds[16]; 422 int nf = sess_fds(s, fds, 16); 423 InEvent ev; 424 if (!fe->wait(fe, sess_timeout(s), fds, nf, &ev)) continue; 425 if (ev.type == EV_QUIT) { 426 if (modal == 1) return; 427 modal = 1; 428 continue; 429 } 430 if (ev.type == EV_KEY) { 431 int k = ev.key; 432 if (modal) { 433 if (k == 's' || k == 'S' || k == 'y' || k == 'Y' || k == K_OK) { 434 if (modal == 1) return; 435 submit(s, &c, ACT_RESIGN, acting_color(s, false)); 436 } 437 modal = 0; 438 continue; 439 } 440 if (help) { help = false; continue; } 441 Act a; 442 if (c.mode == CM_ENTRY || c.mode == CM_PROMO) { 443 if (ctrl_key(&c, k, &a)) { int e = sess_submit(s, &a); if (e) ctrl_error(&c, e); } 444 continue; 445 } 446 const Game *g = &s->g; 447 bool offer_to_me = g->draw_offer && ((local >> ((g->draw_offer - 1) ^ 1)) & 1) && g->result == R_NONE; 448 bool undo_to_me = g->undo_req && ((local >> ((g->undo_req - 1) ^ 1)) & 1) && g->result == R_NONE; 449 switch (k) { 450 case '?': help = true; continue; 451 case 'L': g_lang = !g_lang; st->lang = g_lang; settings_save(st); continue; 452 case 'f': c.flipped = !c.flipped; continue; 453 case 'e': st->show_eval = !st->show_eval; s->want_eval = st->show_eval; settings_save(st); continue; 454 case 't': st->theme = (st->theme + 1) % NTHEMES; settings_save(st); continue; 455 case 'q': modal = 1; continue; 456 case 'm': 457 if (pause_menu(fe, s, &c, st)) return; 458 continue; 459 case 'a': case 'n': 460 if (offer_to_me) submit(s, &c, k == 'a' ? ACT_DRAWOK : ACT_DRAWNO, (g->draw_offer - 1) ^ 1); 461 else if (undo_to_me) submit(s, &c, k == 'a' ? ACT_UNDOOK : ACT_UNDONO, (g->undo_req - 1) ^ 1); 462 continue; 463 case 'u': submit(s, &c, ACT_UNDO, acting_color(s, true)); continue; 464 case 'd': submit(s, &c, ACT_DRAW, acting_color(s, false)); continue; 465 case 'c': submit(s, &c, ACT_CLAIM, g->pos.side); continue; 466 case 'r': if (g->result == R_NONE) modal = 2; continue; 467 case K_BACK: 468 if (c.sel < 0) { if (pause_menu(fe, s, &c, st)) return; continue; } 469 break; 470 } 471 if (ctrl_key(&c, k, &a)) { int e = sess_submit(s, &a); if (e) ctrl_error(&c, e); } 472 } else if (ev.type == EV_CLICK) { 473 if (help) { help = false; continue; } 474 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 475 Act a; 476 if (h && ctrl_click(&c, h->kind, h->id, &a)) { int e = sess_submit(s, &a); if (e) ctrl_error(&c, e); } 477 } 478 } 479 } 480 481 /* ------------------------------------------------------------ partidas nuevas */ 482 483 enum { NG_GO = 1, NG_BACK, NG_COLOR = 10, NG_LEVEL, NG_TC, NG_UNDO, NG_WNAME, NG_BNAME, NG_WLEVEL, NG_BLEVEL, NG_SPEED }; 484 485 bool app_new_game(Frontend *fe, Settings *st, int mode, const Pos *start) 486 { 487 bool en = g_lang; 488 static const char *TCL[16]; 489 for (int i = 0; i < NTC_PRESETS; i++) TCL[i] = i ? TC_PRESETS[i].label : (en ? "No clock" : "Sin reloj"); 490 Form f; 491 form_begin(&f, mode == MODE_AI ? (en ? "Play the computer" : "Contra la máquina") 492 : mode == MODE_HOTSEAT ? (en ? "Two players" : "Dos jugadores") : (en ? "Computer vs computer" : "Máquina contra máquina")); 493 if (mode == MODE_AI) { 494 form_choice(&f, NG_COLOR, en ? "Your color" : "Tu color", COLORS[en], 3, st->color); 495 form_choice(&f, NG_LEVEL, en ? "Level" : "Nivel", LEVELS[en], nlevels(), level_ok(st->level)); 496 } else if (mode == MODE_HOTSEAT) { 497 form_text(&f, NG_WNAME, en ? "White" : "Blancas", st->name, 19); 498 form_text(&f, NG_BNAME, en ? "Black" : "Negras", en ? "Guest" : "Invitado", 19); 499 } else { 500 form_choice(&f, NG_WLEVEL, en ? "White level" : "Nivel blancas", LEVELS[en], nlevels(), level_ok(st->level)); 501 form_choice(&f, NG_BLEVEL, en ? "Black level" : "Nivel negras", LEVELS[en], nlevels(), level_ok(st->level)); 502 form_choice(&f, NG_SPEED, en ? "Speed" : "Velocidad", SPEEDS[en], 3, st->watch_speed); 503 } 504 if (mode != MODE_WATCH) { 505 form_choice(&f, NG_TC, en ? "Clock" : "Reloj", TCL, NTC_PRESETS, st->tc); 506 form_choice(&f, NG_UNDO, en ? "Takebacks" : "Deshacer", YESNO[en], 2, st->undo); 507 } 508 form_button(&f, NG_GO, en ? "Start" : "Empezar"); 509 form_button(&f, NG_BACK, en ? "Back" : "Volver"); 510 f.cur = f.n - 2; 511 if (run_form(fe, &f, 0, 0) != NG_GO) return false; 512 513 uint8_t kinds[2] = { PL_HUMAN, PL_HUMAN }; 514 int levels[2] = { st->level, st->level }; 515 char names[2][24]; 516 TimeCtl tc = { 0, 0, 0 }; 517 bool undo = true; 518 if (mode != MODE_WATCH) { 519 st->tc = form_get(&f, NG_TC)->value; 520 st->undo = form_get(&f, NG_UNDO)->value; 521 tc.base = TC_PRESETS[st->tc].base; 522 tc.inc = TC_PRESETS[st->tc].inc; 523 undo = st->undo; 524 } 525 if (mode == MODE_AI) { 526 st->color = form_get(&f, NG_COLOR)->value; 527 st->level = form_get(&f, NG_LEVEL)->value; 528 int me = st->color == 2 ? (int)(now_ms() & 1) : st->color; 529 kinds[me ^ 1] = PL_ENGINE; 530 levels[me ^ 1] = st->level; 531 snprintf(names[me], sizeof names[me], "%s", st->name); 532 snprintf(names[me ^ 1], sizeof names[me ^ 1], "%s (%s)", en ? "Computer" : "Máquina", LEVELS[en][st->level]); 533 } else if (mode == MODE_HOTSEAT) { 534 snprintf(names[0], sizeof names[0], "%.23s", form_get(&f, NG_WNAME)->text); 535 snprintf(names[1], sizeof names[1], "%.23s", form_get(&f, NG_BNAME)->text); 536 } else { 537 kinds[0] = kinds[1] = PL_ENGINE; 538 levels[0] = form_get(&f, NG_WLEVEL)->value; 539 levels[1] = form_get(&f, NG_BLEVEL)->value; 540 st->watch_speed = form_get(&f, NG_SPEED)->value; 541 for (int c = 0; c < 2; c++) snprintf(names[c], sizeof names[c], "%s (%s)", en ? "Computer" : "Máquina", LEVELS[en][levels[c]]); 542 } 543 settings_save(st); 544 static Session s; 545 sess_new_local(&s, start, &tc, undo, kinds, levels, names); 546 app_play(fe, &s, st); 547 sess_close(&s); 548 return true; 549 } 550 551 /* ------------------------------------------------------------ puzzles */ 552 553 static bool same_move(Move a, Move b) { return MFROM(a) == MFROM(b) && MTO(a) == MTO(b) && MIS_PROMO(a) == MIS_PROMO(b) && (!MIS_PROMO(a) || MPROMO(a) == MPROMO(b)); } 554 555 static void play_puzzle(Frontend *fe, Settings *st, const Puzzle *pz) 556 { 557 char sol[128][8]; 558 int ns = 0; 559 const char *q = pz->solution; 560 while (*q && ns < 128) { 561 while (*q == ' ') q++; 562 int k = 0; 563 while (*q && *q != ' ' && k < 7) sol[ns][k++] = *q++; 564 sol[ns][k] = 0; 565 if (k) ns++; 566 } 567 static Session s; 568 memset(&s, 0, sizeof s); 569 s.local = true; 570 Pos p; 571 pos_from_fen(&p, pz->fen); 572 game_init(&s.g, &p, 0, false); 573 int me = p.side; 574 s.kind[me] = PL_HUMAN; 575 s.kind[me ^ 1] = PL_REMOTE; /* las respuestas las pone el puzzle */ 576 snprintf(s.names[me], sizeof s.names[me], "%s", st->name); 577 snprintf(s.names[me ^ 1], sizeof s.names[me ^ 1], "%s", pz->name[g_lang]); 578 Ctrl c; 579 ctrl_init(&c, (uint8_t)(1 << me), me == BLACK); 580 int step = 0, state = 0; /* 0 jugando, 1 resuelto, 2 fallo */ 581 long long reply_at = 0; 582 char banner[200] = ""; 583 for (;;) { 584 ctrl_sync(&c, &s.g); 585 if (state == 0 && step < ns && step % 2 == 1 && now_ms() >= reply_at) { 586 Move m = move_from_uci(&s.g.pos, sol[step]); 587 Act a = { ACT_MOVE, (int8_t)s.g.pos.side, m, 0 }; 588 if (m) game_apply(&s.g, &a); 589 step++; 590 if (step >= ns) state = 1; 591 } 592 if (state == 0 && step >= ns) state = 1; 593 View v; 594 memset(&v, 0, sizeof v); 595 v.g = &s.g; 596 v.c = &c; 597 v.names[0] = s.names[0]; 598 v.names[1] = s.names[1]; 599 v.theme = st->theme; 600 v.layout = st->layout; 601 v.view_ply = -1; 602 v.clock_running = -1; 603 if (state == 1) snprintf(banner, sizeof banner, "%s", g_lang ? "Solved! r retry · Esc back" : "¡Resuelto! r reintentar · Esc volver"); 604 else if (state == 0) snprintf(banner, sizeof banner, "%s · %s", pz->name[g_lang], g_lang ? "find the best move" : "encontrá la mejor jugada"); 605 v.banner = banner; 606 fe->draw_game(fe, &v); 607 InEvent ev; 608 if (!fe->wait(fe, state == 0 && step % 2 == 1 ? 50 : 1000, 0, 0, &ev)) continue; 609 if (ev.type == EV_QUIT) return; 610 Act a; 611 bool got = false; 612 if (ev.type == EV_KEY) { 613 if (ev.key == K_BACK && c.sel < 0 && c.mode == CM_BOARD) return; 614 if (ev.key == 'q') return; 615 if (ev.key == 'r' || (state == 2 && ev.key == K_OK)) { 616 game_init(&s.g, &p, 0, false); 617 ctrl_init(&c, (uint8_t)(1 << me), me == BLACK); 618 step = 0; 619 state = 0; 620 continue; 621 } 622 if (ev.key == 'f') { c.flipped = !c.flipped; continue; } 623 if (state == 0) got = ctrl_key(&c, ev.key, &a); 624 } else if (ev.type == EV_CLICK && state == 0) { 625 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 626 if (h) got = ctrl_click(&c, h->kind, h->id, &a); 627 } 628 if (!got || state != 0 || step % 2 == 1) continue; 629 Move want = move_from_uci(&s.g.pos, sol[step]); 630 Game after = s.g; 631 game_apply(&after, &a); 632 /* vale la jugada de la solucion, o cualquier otra que de mate si la solucion daba mate */ 633 bool ok = same_move(a.m, want); 634 if (!ok && step == ns - 1 && after.reason == RS_MATE) ok = true; 635 if (ok) { 636 s.g = after; 637 step++; 638 reply_at = now_ms() + 600; 639 if (step >= ns) state = 1; 640 } else { 641 char full[160] = ""; 642 for (int i = step; i < ns; i++) { strcat(full, sol[i]); strcat(full, " "); } 643 snprintf(banner, sizeof banner, "%s %s · Enter %s", g_lang ? "Not quite. Solution:" : "No es esa. Solución:", full, 644 g_lang ? "retry" : "reintentar"); 645 state = 2; 646 View w = v; 647 w.banner = banner; 648 fe->draw_game(fe, &w); 649 for (;;) { 650 if (!fe->wait(fe, 1000, 0, 0, &ev)) continue; 651 if (ev.type == EV_QUIT || (ev.type == EV_KEY && (ev.key == K_BACK || ev.key == 'q'))) return; 652 if (ev.type == EV_KEY || ev.type == EV_CLICK) break; 653 } 654 game_init(&s.g, &p, 0, false); 655 ctrl_init(&c, (uint8_t)(1 << me), me == BLACK); 656 step = 0; 657 state = 0; 658 } 659 } 660 } 661 662 void app_puzzles(Frontend *fe, Settings *st) 663 { 664 for (;;) { 665 Form f; 666 form_begin(&f, g_lang ? "Puzzles" : "Puzzles"); 667 for (int i = 0; i < NPUZZLES && i < 18; i++) form_button(&f, 100 + i, PUZZLES[i].name[g_lang]); 668 form_button(&f, 1, g_lang ? "Back" : "Volver"); 669 int r = run_form(fe, &f, 0, 0); 670 if (r < 100) return; 671 play_puzzle(fe, st, &PUZZLES[r - 100]); 672 } 673 } 674 675 /* ------------------------------------------------------------ visor de PGN */ 676 677 bool app_viewer(Frontend *fe, Settings *st, const char *text, char *err, int errlen) 678 { 679 static Game full; 680 PgnTags tags; 681 if (!game_from_pgn(&full, text, &tags, err, errlen)) return false; 682 int ply = 0; 683 Ctrl c; 684 ctrl_init(&c, 0, false); 685 static Game shown; 686 for (;;) { 687 shown = full; 688 /* reconstruir hasta la jugada que se mira */ 689 game_init(&shown, &full.start, 0, false); 690 for (int i = 0; i < ply; i++) { Act a = { ACT_MOVE, (int8_t)shown.pos.side, full.mv[i], 0 }; game_apply(&shown, &a); } 691 if (ply == full.nply) { shown.result = full.result; shown.reason = full.reason; } 692 ctrl_sync(&c, &shown); 693 c.cursor = -1; 694 View v; 695 memset(&v, 0, sizeof v); 696 v.g = &shown; 697 v.c = &c; 698 v.names[0] = tags.white[0] ? tags.white : color_name(WHITE); 699 v.names[1] = tags.black[0] ? tags.black : color_name(BLACK); 700 v.theme = st->theme; 701 v.layout = st->layout; 702 v.view_ply = ply; 703 v.hide_prompt = true; 704 v.clock_running = -1; 705 const char *eco = 0; 706 static char op[96]; 707 const char *name = opening_name(shown.mv, shown.nply, &shown.start, &eco); 708 if (name) { snprintf(op, sizeof op, "%s %s", eco, name); v.opening = op; } 709 char banner[128]; 710 snprintf(banner, sizeof banner, g_lang ? "Move %d/%d · ←→ step · ↑↓ start/end · p play from here · Esc back" 711 : "Jugada %d/%d · ←→ avanzar · ↑↓ inicio/fin · p jugar desde acá · Esc volver", ply, full.nply); 712 v.banner = banner; 713 fe->draw_game(fe, &v); 714 InEvent ev; 715 if (!fe->wait(fe, 1000, 0, 0, &ev)) continue; 716 if (ev.type == EV_QUIT) return true; 717 if (ev.type != EV_KEY) continue; 718 switch (ev.key) { 719 case K_RIGHT: case 'l': case ' ': if (ply < full.nply) ply++; break; 720 case K_LEFT: case 'h': if (ply > 0) ply--; break; 721 case K_UP: case 'k': ply = 0; break; 722 case K_DOWN: case 'j': ply = full.nply; break; 723 case 'f': c.flipped = !c.flipped; break; 724 case 'p': 725 if (shown.result == R_NONE) { app_new_game(fe, st, MODE_HOTSEAT, &shown.pos); return true; } 726 break; 727 case K_BACK: case 'q': return true; 728 } 729 } 730 } 731 732 /* ------------------------------------------------------------ importar */ 733 734 enum { IM_BACK = 0, IM_PASTE = 1, IM_FILE, IM_PATH = 10 }; 735 enum { IP_BACK = 0, IP_PLAY = 1, IP_VIEW, IP_MODE = 10, IP_COLOR, IP_LEVEL }; 736 737 /* Importa un PGN o un FEN (pegado o de un archivo) y sigue la partida desde ahi, 738 * de a dos o contra la maquina, o la abre en el visor. */ 739 void app_import(Frontend *fe, Settings *st, const char *given) 740 { 741 char *text = given ? strdup(given) : 0; 742 char msg[128] = ""; 743 for (;;) { 744 bool en = g_lang; 745 if (!text) { 746 Form f; 747 form_begin(&f, en ? "Import a game" : "Importar partida"); 748 form_label(&f, en ? "A game in PGN (or just its moves) or a position in FEN." : "Una partida en PGN (o solo sus jugadas) o una posición en FEN."); 749 form_button(&f, IM_PASTE, fe->is_gui ? (en ? "Paste from the clipboard" : "Pegar del portapapeles") : (en ? "Paste it here" : "Pegarla acá")); 750 form_text(&f, IM_PATH, en ? "File" : "Archivo", "", 60); 751 form_button(&f, IM_FILE, en ? "Open the file" : "Abrir el archivo"); 752 form_button(&f, IM_BACK, en ? "Back" : "Volver"); 753 if (msg[0]) form_msg(&f, msg, true); 754 f.cur = 1; 755 int r = run_form(fe, &f, 0, 0); 756 if (r == IM_PASTE) text = fe->paste ? fe->paste(fe) : 0; 757 else if (r == IM_FILE) text = read_file(form_get(&f, IM_PATH)->text, 0); 758 else return; 759 if (!text) { snprintf(msg, sizeof msg, "%s", r == IM_FILE ? (en ? "Cannot read that file" : "No se puede leer ese archivo") : (en ? "Nothing was pasted" : "No se pegó nada")); continue; } 760 } 761 /* FEN o PGN */ 762 static Game g; 763 PgnTags tags; 764 memset(&tags, 0, sizeof tags); 765 char *t = text; 766 while (*t == ' ' || *t == '\n' || *t == '\r' || *t == '\t') t++; 767 char line[128]; 768 snprintf(line, sizeof line, "%.*s", (int)strcspn(t, "\r\n"), t); 769 Pos p; 770 char err[128] = ""; 771 bool is_fen = pos_from_fen(&p, line); 772 if (is_fen) game_init(&g, &p, 0, true); 773 else if (!game_from_pgn(&g, text, &tags, err, sizeof err)) { 774 snprintf(msg, sizeof msg, "%s", err[0] ? err : (en ? "Not a PGN or FEN" : "No es un PGN ni un FEN")); 775 free(text); 776 text = 0; 777 if (given) { notice(fe, en ? "Import" : "Importar", msg); return; } 778 continue; 779 } 780 /* que hacer con ella */ 781 char info[128]; 782 const char *eco = 0, *op = opening_name(g.mv, g.nply, &g.start, &eco); 783 if (is_fen) snprintf(info, sizeof info, "%s · %s %s", en ? "Position" : "Posición", g_lang ? "to move:" : "juegan", color_name(g.pos.side)); 784 else snprintf(info, sizeof info, "%d %s%s%s", g.nply, en ? "moves" : "jugadas", op ? " · " : "", op ? op : ""); 785 static const char *MODES[2][2] = { { "Dos jugadores", "Contra la máquina" }, { "Two players", "Against the computer" } }; 786 static const char *SIDES[2][2] = { { "Blancas", "Negras" }, { "White", "Black" } }; 787 Form f; 788 form_begin(&f, en ? "Imported game" : "Partida importada"); 789 form_label(&f, info); 790 if (g.result != R_NONE) form_label(&f, en ? "The game is already over." : "La partida ya terminó."); 791 form_choice(&f, IP_MODE, en ? "Continue as" : "Seguir como", MODES[en], 2, 1); 792 form_choice(&f, IP_COLOR, en ? "Your color" : "Tu color", SIDES[en], 2, g.pos.side); 793 form_choice(&f, IP_LEVEL, en ? "Level" : "Nivel", LEVELS[en], nlevels(), level_ok(st->level)); 794 if (g.result == R_NONE) form_button(&f, IP_PLAY, en ? "Continue playing" : "Seguir jugando"); 795 if (g.nply) form_button(&f, IP_VIEW, en ? "Open in the viewer" : "Ver en el visor"); 796 form_button(&f, IP_BACK, en ? "Back" : "Volver"); 797 for (int i = 0; i < f.n; i++) if (f.f[i].type == F_BUTTON) { f.cur = i; break; } 798 int r = run_form(fe, &f, 0, 0); 799 if (r == IP_VIEW) { 800 char e2[128]; 801 if (!app_viewer(fe, st, text, e2, sizeof e2)) notice(fe, "PGN", e2); 802 } else if (r == IP_PLAY) { 803 bool ai = form_get(&f, IP_MODE)->value == 1; 804 int me = form_get(&f, IP_COLOR)->value; 805 uint8_t kinds[2] = { PL_HUMAN, PL_HUMAN }; 806 int levels[2] = { st->level, st->level }; 807 char names[2][24]; 808 snprintf(names[0], sizeof names[0], "%.23s", tags.white[0] && strcmp(tags.white, "?") ? tags.white : color_name(WHITE)); 809 snprintf(names[1], sizeof names[1], "%.23s", tags.black[0] && strcmp(tags.black, "?") ? tags.black : color_name(BLACK)); 810 if (ai) { 811 st->level = form_get(&f, IP_LEVEL)->value; 812 settings_save(st); 813 kinds[me ^ 1] = PL_ENGINE; 814 levels[me ^ 1] = st->level; 815 snprintf(names[me], sizeof names[me], "%s", st->name); 816 snprintf(names[me ^ 1], sizeof names[me ^ 1], "%s (%s)", en ? "Computer" : "Máquina", LEVELS[en][st->level]); 817 } 818 static Session s; 819 sess_new_imported(&s, &g, kinds, levels, names); 820 app_play(fe, &s, st); 821 sess_close(&s); 822 } 823 free(text); 824 return; 825 } 826 } 827 828 /* ------------------------------------------------------------ menu principal */ 829 830 enum { M_QUIT = 0, M_AI = 1, M_HOTSEAT, M_WATCH, M_PUZZLES, M_CONTINUE, M_OPEN, M_ONLINE, M_LAN_HOST, M_LAN_JOIN, 831 M_LANG = 50, M_THEME, M_NAME, M_PATH, M_HOST }; 832 833 void app_menu(Frontend *fe, Settings *st) 834 { 835 for (;;) { 836 bool en = g_lang; 837 Form f; 838 form_begin(&f, en ? "Main menu" : "Menú principal"); 839 char last[600]; 840 bool have = latest_save(last, sizeof last); 841 form_button(&f, M_AI, en ? "Play the computer" : "Jugar contra la máquina"); 842 form_button(&f, M_HOTSEAT, en ? "Two players (same computer)" : "Dos jugadores (misma compu)"); 843 form_button(&f, M_CONTINUE, en ? "Continue the last game" : "Continuar la última partida")->disabled = !have; 844 form_button(&f, M_PUZZLES, "Puzzles"); 845 form_button(&f, M_WATCH, en ? "Computer vs computer" : "Máquina contra máquina"); 846 form_button(&f, M_ONLINE, en ? "Play online (server)" : "Jugar online (servidor)"); 847 form_button(&f, M_LAN_HOST, en ? "LAN: create game" : "Red local: crear partida"); 848 form_text(&f, M_HOST, en ? "LAN host" : "Compu de la red", "", 40); 849 form_button(&f, M_LAN_JOIN, en ? "LAN: join that host" : "Red local: unirse a esa compu"); 850 form_button(&f, M_OPEN, en ? "Import a game (PGN or FEN)" : "Importar partida (PGN o FEN)"); 851 form_choice(&f, M_LANG, en ? "Language" : "Idioma", LANGS, 2, g_lang); 852 form_choice(&f, M_THEME, en ? "Theme" : "Tema", THEME_NAMES, NTHEMES, st->theme); 853 form_text(&f, M_NAME, en ? "Your name" : "Tu nombre", st->name, 19); 854 form_button(&f, M_QUIT, en ? "Quit" : "Salir"); 855 int r = run_form(fe, &f, 0, 0); 856 g_lang = form_get(&f, M_LANG)->value; 857 st->lang = g_lang; 858 st->theme = form_get(&f, M_THEME)->value; 859 if (form_get(&f, M_NAME)->text[0]) snprintf(st->name, sizeof st->name, "%.19s", form_get(&f, M_NAME)->text); 860 settings_save(st); 861 switch (r) { 862 case M_AI: app_new_game(fe, st, MODE_AI, 0); break; 863 case M_HOTSEAT: app_new_game(fe, st, MODE_HOTSEAT, 0); break; 864 case M_WATCH: app_new_game(fe, st, MODE_WATCH, 0); break; 865 case M_PUZZLES: app_puzzles(fe, st); break; 866 case M_ONLINE: online_menu(fe, st, 0); break; 867 case M_LAN_HOST: lan_host(fe, st, 0); break; 868 case M_LAN_JOIN: { 869 char host[128]; 870 int port; 871 if (!parse_hostport(form_get(&f, M_HOST)->text, host, sizeof host, &port)) { 872 notice(fe, "Error", en ? "Write the host (IP or name) first" : "Primero escribí la compu (IP o nombre)"); 873 break; 874 } 875 lan_join(fe, st, host, port); 876 break; 877 } 878 case M_CONTINUE: { 879 static Session s; 880 char err[128]; 881 if (!sess_resume(&s, last, err, sizeof err)) { notice(fe, "Error", err); break; } 882 app_play(fe, &s, st); 883 sess_close(&s); 884 break; 885 } 886 case M_OPEN: app_import(fe, st, 0); break; 887 case M_QUIT: case FORM_CANCEL: return; 888 } 889 } 890 }