app.c (34186B)
1 /* app.c - el ajedrez en la PicoCalc (portable: corre en la PicoCalc y en el simulador). 2 * 3 * Modos: partida local (personas y/o maquina), puzzles, y cable 1v1 entre dos 4 * PicoCalc. En el cable, la "A" es la autoridad (valida y guarda) y la "B" es 5 * cliente; hablan un protocolo parecido al del servidor, con checksum por linea 6 * (comun/pico/link.c). La partida se guarda en AJEDREZ.SAV de la SD. */ 7 #include <string.h> 8 #include "hal.h" 9 #include "link.h" 10 #include "form.h" 11 #include "../core/chess.h" 12 #include "../core/ctrl.h" 13 #include "../gfx/chess_gfx.h" 14 15 #define MAX_ACTS 1400 16 #define SAVE_MAX 32768 17 18 enum { ROLE_LOCAL, ROLE_A, ROLE_B }; 19 enum { PL_HUMAN, PL_ENGINE }; 20 enum { LINK_IDLE, LINK_HELLO, LINK_JOIN, LINK_SYNCED }; 21 22 static struct { 23 Game g; 24 int role; 25 uint8_t kind[2]; 26 int level[2]; 27 char names[2][20]; 28 Act log[MAX_ACTS]; /* todas las acciones (para guardar y para el cable) */ 29 int nlog; 30 uint32_t turn_start; 31 int eval; 32 bool eval_valid; 33 bool dirty, save_due; 34 int theme; 35 uint32_t engine_at; 36 int engine_delay; 37 /* cable */ 38 int my_color; /* B: el color de esta pantalla */ 39 int wseat; /* A: 0 si la A juega blancas */ 40 int link; 41 uint32_t link_t, ping_t, pong_t; 42 int evcount; 43 bool peer; 44 int last_err; 45 bool err_pending; 46 } S; 47 48 static TTEntry tt[2048]; 49 static Engine eng; 50 static char savebuf[SAVE_MAX]; 51 52 typedef struct { char *b; int n, pos; } Sb; 53 static void sb_s(Sb *s, const char *t) { int p = str_put(s->b, s->n, s->pos, t); if (p >= 0) s->pos = p; } 54 static void sb_i(Sb *s, long v) { int p = str_int(s->b, s->n, s->pos, v); if (p >= 0) s->pos = p; } 55 static void cpy(char *d, int n, const char *s) { str_put(d, n, 0, s); } 56 57 static uint32_t now_ms(void) { return hal_ms(); } 58 59 /* ------------------------------------------------------------ partida */ 60 61 static void record(const Act *a) 62 { 63 if (S.nlog < MAX_ACTS) S.log[S.nlog++] = *a; 64 S.save_due = true; 65 } 66 67 static void apply(const Act *a) 68 { 69 int before = S.g.nply; 70 game_apply(&S.g, a); 71 record(a); 72 if (S.g.nply != before) { 73 S.turn_start = hal_ms(); 74 S.engine_at = S.turn_start + (uint32_t)S.engine_delay; 75 S.eval_valid = false; 76 } 77 S.dirty = true; 78 if (S.role == ROLE_A) { 79 char line[64] = "EV "; 80 act_fmt(a, line + 3, (int)sizeof line - 3); 81 link_send(line); 82 } 83 } 84 85 static int32_t clock_of(int color) 86 { 87 int32_t t = game_clock(&S.g, color); 88 if (game_has_clock(&S.g) && S.g.result == R_NONE && S.g.pos.side == color) t -= (int32_t)(hal_ms() - S.turn_start); 89 return t; 90 } 91 92 static uint8_t local_mask(void) 93 { 94 if (S.role == ROLE_B) return (uint8_t)(1 << S.my_color); 95 if (S.role == ROLE_A) return (uint8_t)(1 << (S.wseat == 0 ? WHITE : BLACK)); 96 uint8_t m = 0; 97 for (int c = 0; c < 2; c++) if (S.kind[c] == PL_HUMAN) m |= (uint8_t)(1 << c); 98 return m; 99 } 100 101 /* respuestas automaticas: la maquina (o dos personas en la misma PicoCalc) */ 102 static void auto_answer(void) 103 { 104 Game *g = &S.g; 105 if (S.role != ROLE_LOCAL) return; 106 if (g->draw_offer && g->result == R_NONE) { 107 int to = (g->draw_offer - 1) ^ 1; 108 if (S.kind[to] == PL_ENGINE) { 109 int ev = evaluate(&g->pos); 110 if (g->pos.side != to) ev = -ev; 111 Act a = { (uint8_t)(ev < -150 || pos_insufficient(&g->pos) ? ACT_DRAWOK : ACT_DRAWNO), (int8_t)to, 0, 0 }; 112 apply(&a); 113 } 114 } 115 if (g->undo_req) { 116 Act a = { ACT_UNDOOK, (int8_t)((g->undo_req - 1) ^ 1), 0, 0 }; 117 apply(&a); 118 } 119 } 120 121 /* ------------------------------------------------------------ guardado */ 122 123 static void save_game(void) 124 { 125 if (S.role == ROLE_B) return; 126 Sb s = { savebuf, SAVE_MAX, 0 }; 127 savebuf[0] = 0; 128 char hdr[160]; 129 game_hdr_fmt(&S.g, hdr, sizeof hdr); 130 /* la cabecera describe el inicio: se regenera desde la posicion inicial */ 131 sb_s(&s, "AJEDREZ-SAVE 1\nROLE "); sb_s(&s, S.role == ROLE_A ? "A" : "L"); 132 sb_s(&s, "\nWSEAT "); sb_i(&s, S.wseat); 133 sb_s(&s, "\nHDR "); sb_s(&s, hdr); sb_s(&s, "\n"); 134 for (int c = 0; c < 2; c++) { 135 sb_s(&s, "PLAYER "); sb_i(&s, c); sb_s(&s, S.kind[c] == PL_ENGINE ? " E " : " H "); 136 sb_i(&s, S.level[c]); sb_s(&s, " "); sb_s(&s, S.names[c]); sb_s(&s, "\n"); 137 } 138 for (int i = 0; i < S.nlog; i++) { 139 char line[64]; 140 act_fmt(&S.log[i], line, sizeof line); 141 sb_s(&s, "A "); sb_s(&s, line); sb_s(&s, "\n"); 142 } 143 hal_store_save(savebuf, s.pos + 1); 144 } 145 146 static void start_game(int role, const Pos *start, const TimeCtl *tc, bool undo) 147 { 148 memset(&S.g, 0, sizeof S.g); 149 game_init(&S.g, start, tc, undo); 150 S.role = role; 151 S.nlog = 0; 152 S.turn_start = hal_ms(); 153 S.engine_at = S.turn_start + 300; 154 S.eval_valid = false; 155 S.dirty = true; 156 S.save_due = true; 157 } 158 159 static bool load_game(void) 160 { 161 int n = hal_store_load(savebuf, SAVE_MAX - 1); 162 if (n <= 0) return false; 163 savebuf[n] = 0; 164 if (strncmp(savebuf, "AJEDREZ-SAVE 1", 14)) return false; 165 bool hdr = false, acts = false; 166 int role = ROLE_LOCAL; 167 char *p = savebuf; 168 while (p && *p) { 169 char *e = strchr(p, '\n'); 170 if (e) *e = 0; 171 long v; 172 if (!strncmp(p, "ROLE ", 5)) role = p[5] == 'A' ? ROLE_A : ROLE_LOCAL; 173 else if (!strncmp(p, "WSEAT ", 6) && str_scan_int(p + 6, &v)) S.wseat = (int)v; 174 else if (!strncmp(p, "HDR ", 4)) { 175 Game tmp; 176 if (!game_hdr_parse(p + 4, &tmp)) return false; 177 start_game(role, &tmp.start, &tmp.tc, tmp.allow_undo); 178 hdr = true; 179 } else if (!strncmp(p, "PLAYER ", 7)) { 180 const char *q = str_scan_int(p + 7, &v); 181 int c = (int)v; 182 if (q && (c == 0 || c == 1)) { 183 while (*q == ' ') q++; 184 S.kind[c] = *q == 'E' ? PL_ENGINE : PL_HUMAN; 185 q++; 186 q = str_scan_int(q, &v); 187 if (q) { 188 S.level[c] = (int)v; 189 while (*q == ' ') q++; 190 cpy(S.names[c], sizeof S.names[c], q); 191 } 192 } 193 } else if (hdr && p[0] == 'A' && p[1] == ' ') { 194 Act a; 195 if (act_parse(p + 2, &a) && act_resolve(&S.g, &a) && game_check(&S.g, &a) == E_OK) { 196 game_apply(&S.g, &a); 197 record(&a); 198 } 199 acts = true; 200 } 201 p = e ? e + 1 : 0; 202 } 203 (void)acts; 204 S.turn_start = hal_ms(); 205 S.save_due = false; 206 return hdr; 207 } 208 209 /* ------------------------------------------------------------ cable */ 210 211 static void a_attach(void) 212 { 213 char line[200]; 214 link_send("GAME CABLE 1 -"); 215 char hdr[160]; 216 game_hdr_fmt(&S.g, hdr, sizeof hdr); 217 Sb s = { line, sizeof line, 0 }; 218 line[0] = 0; 219 sb_s(&s, "HDR S "); sb_i(&s, S.wseat); sb_s(&s, " "); sb_i(&s, (long)(hal_ms() - S.turn_start)); sb_s(&s, " "); sb_s(&s, hdr); 220 link_send(line); 221 for (int seat = 0; seat < 2; seat++) { 222 int color = seat == S.wseat ? WHITE : BLACK; 223 s.pos = 0; 224 line[0] = 0; 225 sb_s(&s, "SEAT "); sb_i(&s, seat); sb_s(&s, " P 1 "); sb_s(&s, S.names[color]); 226 link_send(line); 227 } 228 for (int i = 0; i < S.nlog; i++) { 229 str_put(line, sizeof line, 0, "EV "); 230 act_fmt(&S.log[i], line + 3, (int)sizeof line - 3); 231 link_send(line); 232 } 233 link_send("SYNC"); 234 S.peer = true; 235 } 236 237 static void a_handle(char *line) 238 { 239 if (!strncmp(line, "HELLO", 5)) { 240 const char *nm = strchr(line + 6, ' '); 241 int bcolor = S.wseat == 0 ? BLACK : WHITE; 242 if (nm && nm[1]) cpy(S.names[bcolor], sizeof S.names[bcolor], nm + 1); 243 link_send("WELCOME 1"); 244 } else if (!strncmp(line, "JOIN", 4)) { 245 a_attach(); 246 } else if (!strncmp(line, "MOVE ", 5)) { 247 Act a; 248 int bcolor = S.wseat == 0 ? BLACK : WHITE; 249 int e = E_ARGS; 250 if (act_parse(line + 5, &a) && a.c == bcolor && a.type != ACT_FLAG) { 251 if (a.type == ACT_MOVE && a.c != S.g.pos.side) e = E_TURN; 252 else if (!act_resolve(&S.g, &a)) e = E_ILLEGAL; 253 else { 254 if (a.type == ACT_MOVE) a.ms = (int32_t)(hal_ms() - S.turn_start); 255 e = game_check(&S.g, &a); 256 if (!e) apply(&a); 257 } 258 } 259 if (e) { 260 char buf[24]; 261 Sb s = { buf, sizeof buf, 0 }; 262 buf[0] = 0; 263 sb_s(&s, "ERR "); sb_i(&s, e); 264 link_send(buf); 265 } 266 } else if (!strcmp(line, "PING")) { 267 char buf[24] = "PONG "; 268 str_int(buf, sizeof buf, 5, S.nlog); 269 link_send(buf); 270 } 271 } 272 273 static void b_restart(void) 274 { 275 S.link = LINK_HELLO; 276 S.link_t = 0; 277 } 278 279 static void b_handle(char *line) 280 { 281 if (!strncmp(line, "WELCOME", 7)) { 282 link_send("JOIN -"); 283 S.link = LINK_JOIN; 284 S.link_t = hal_ms(); 285 } else if (!strncmp(line, "HDR S ", 6)) { 286 long ws, el; 287 const char *q = str_scan_int(line + 6, &ws); 288 if (q) q = str_scan_int(q, &el); 289 if (!q) return; 290 while (*q == ' ') q++; 291 Game tmp; 292 if (!game_hdr_parse(q, &tmp)) return; 293 S.wseat = (int)ws; 294 S.my_color = ws == 1 ? WHITE : BLACK; /* la B es el asiento 1 */ 295 start_game(ROLE_B, &tmp.start, &tmp.tc, tmp.allow_undo); 296 S.turn_start = hal_ms() - (uint32_t)el; 297 S.evcount = 0; 298 } else if (!strncmp(line, "SEAT ", 5)) { 299 long i, on; 300 const char *p = str_scan_int(line + 5, &i); 301 if (!p || i < 0 || i > 1) return; 302 while (*p == ' ') p++; 303 p++; 304 p = str_scan_int(p, &on); 305 if (!p) return; 306 while (*p == ' ') p++; 307 int color = i == S.wseat ? WHITE : BLACK; 308 cpy(S.names[color], sizeof S.names[color], p); 309 } else if (!strncmp(line, "EV ", 3)) { 310 Act a; 311 if (act_parse(line + 3, &a) && act_resolve(&S.g, &a) && game_check(&S.g, &a) == E_OK) { 312 int before = S.g.nply; 313 game_apply(&S.g, &a); 314 if (S.g.nply != before) S.turn_start = hal_ms(); 315 } 316 S.evcount++; 317 } else if (!strncmp(line, "PONG", 4)) { 318 long n; 319 S.pong_t = hal_ms(); 320 /* si la A tiene mas acciones de las que recibimos, se perdio algo: resincronizar */ 321 if (S.link == LINK_SYNCED && str_scan_int(line + 4, &n) && n != S.evcount) b_restart(); 322 } else if (!strcmp(line, "SYNC")) { 323 S.link = LINK_SYNCED; 324 S.pong_t = hal_ms(); 325 } else if (!strncmp(line, "ERR ", 4)) { 326 long e; 327 if (str_scan_int(line + 4, &e)) { S.last_err = (int)e; S.err_pending = true; } 328 } 329 S.dirty = true; 330 } 331 332 static void link_poll(void) 333 { 334 if (S.role == ROLE_LOCAL) return; 335 static char line[600]; 336 int r; 337 while ((r = link_line(line, sizeof line)) != 0) { 338 if (r < 0) { /* linea corrupta */ 339 if (S.role == ROLE_B) b_restart(); 340 continue; 341 } 342 if (S.role == ROLE_A) a_handle(line); else b_handle(line); 343 } 344 if (S.role == ROLE_B) { 345 uint32_t now = hal_ms(); 346 if (S.link == LINK_HELLO && now - S.link_t > 1500) { 347 link_send2("HELLO 1 ", S.names[S.my_color][0] ? S.names[S.my_color] : "B"); 348 S.link_t = now; 349 } else if (S.link == LINK_JOIN && now - S.link_t > 8000) b_restart(); 350 else if (S.link == LINK_SYNCED) { 351 if (now - S.ping_t > 2000) { link_send("PING"); S.ping_t = now; } 352 if (now - S.pong_t > 7000) b_restart(); /* cable desenchufado */ 353 } 354 } 355 } 356 357 /* ------------------------------------------------------------ motor */ 358 359 static bool engine_poll(void *ctx) 360 { 361 (void)ctx; 362 return hal_key() == K_BACK; /* Esc: que juegue lo que tiene */ 363 } 364 365 static void engine_move(void) 366 { 367 int side = S.g.pos.side; 368 SearchLimits lim; 369 level_limits(S.level[side], &lim); 370 if (game_has_clock(&S.g)) { 371 int32_t left = clock_of(side); 372 int32_t budget = S.g.tc.per_move > 0 ? left / 4 : left / 30 + S.g.tc.inc / 2; 373 if (budget < 50) budget = 50; 374 if (!lim.movetime || lim.movetime > budget) lim.movetime = budget; 375 } 376 SearchResult r; 377 engine_search(&eng, &S.g, &lim, &r); 378 if (r.depth > 0) { S.eval = side == WHITE ? r.score : -r.score; S.eval_valid = true; } 379 Act a = { ACT_MOVE, (int8_t)side, r.best, (int32_t)(hal_ms() - S.turn_start) }; 380 if (r.best && game_check(&S.g, &a) == E_OK) { apply(&a); auto_answer(); } 381 } 382 383 /* ------------------------------------------------------------ dibujo */ 384 385 typedef struct { const View *v; const Form *f; } Paint; 386 387 static void paint(Surface *s, void *ctx) 388 { 389 Paint *p = ctx; 390 if (p->f) gfx_form(s, p->f, 0); 391 else chess_screen(s, p->v, 0); 392 } 393 394 static void draw_form(const Form *f) 395 { 396 Paint pc = { 0, f }; 397 hal_draw(paint, &pc); 398 } 399 400 static int form_loop(Form *f, void (*on_change)(Form *, void *), void *ctx) 401 { 402 if (on_change) on_change(f, ctx); 403 bool dirty = true; 404 for (;;) { 405 if (dirty) { draw_form(f); dirty = false; } 406 int k = hal_key(); 407 if (k < 0) { hal_sleep(10); continue; } 408 int r = form_key(f, k); 409 if (r != FORM_NONE) return r; 410 if (on_change) on_change(f, ctx); 411 dirty = true; 412 } 413 } 414 415 static void notice(const char *title, const char *text) 416 { 417 Form f; 418 form_begin(&f, title); 419 form_label(&f, text); 420 form_button(&f, 1, "OK"); 421 f.cur = 1; 422 form_loop(&f, 0, 0); 423 } 424 425 static void build_view(View *v, Ctrl *c, char *banner, int bn, bool thinking) 426 { 427 memset(v, 0, sizeof *v); 428 v->g = &S.g; 429 v->c = c; 430 v->names[0] = S.names[0]; 431 v->names[1] = S.names[1]; 432 v->has_clock = game_has_clock(&S.g); 433 v->clock[0] = clock_of(WHITE); 434 v->clock[1] = clock_of(BLACK); 435 v->clock_running = S.g.result == R_NONE ? S.g.pos.side : -1; 436 v->theme = S.theme; 437 v->layout = LAYOUT_COMPACT; 438 v->show_eval = S.eval_valid; 439 v->eval = S.eval; 440 v->eval_valid = S.eval_valid; 441 v->view_ply = -1; 442 if (thinking) v->status = tr(S_P_THINK); 443 else if (S.role != ROLE_LOCAL) v->status = S.role == ROLE_A ? (S.peer ? "Cable A" : (g_lang ? "Cable A: waiting for B" : "Cable A: esperando a B")) : "Cable B"; 444 static char op[80]; 445 const char *eco = 0; 446 const char *name = opening_name(S.g.mv, S.g.nply, &S.g.start, &eco); 447 if (name) { str_put(op, sizeof op, 0, name); v->opening = op; } 448 uint8_t local = local_mask(); 449 const Game *g = &S.g; 450 banner[0] = 0; 451 if (g->result == R_NONE) { 452 Sb s = { banner, bn, 0 }; 453 if (g->draw_offer && ((local >> ((g->draw_offer - 1) ^ 1)) & 1)) { sb_s(&s, color_name(g->draw_offer - 1)); sb_s(&s, " "); sb_s(&s, tr(S_DRAW_OFFER)); } 454 else if (g->undo_req && ((local >> ((g->undo_req - 1) ^ 1)) & 1)) { sb_s(&s, color_name(g->undo_req - 1)); sb_s(&s, " "); sb_s(&s, tr(S_UNDO_REQ)); } 455 else if (game_can_claim(g) && ((local >> g->pos.side) & 1)) sb_s(&s, tr(S_CAN_CLAIM)); 456 } 457 if (banner[0]) v->banner = banner; 458 } 459 460 /* ------------------------------------------------------------ bucle de juego */ 461 462 static int submit(Act *a) 463 { 464 if (S.role == ROLE_B) { 465 if (S.link != LINK_SYNCED) return E_TURN; 466 int e = game_check(&S.g, a); 467 if (e) return e; 468 char line[64] = "MOVE "; 469 Act b = *a; 470 b.ms = 0; 471 act_fmt(&b, line + 5, (int)sizeof line - 5); 472 link_send(line); 473 return E_OK; 474 } 475 if (a->type == ACT_MOVE) a->ms = (int32_t)(hal_ms() - S.turn_start); 476 int e = game_check(&S.g, a); 477 if (e) return e; 478 apply(a); 479 auto_answer(); 480 return E_OK; 481 } 482 483 static int acting_color(bool for_undo) 484 { 485 uint8_t m = local_mask(); 486 if (m == 3) return for_undo ? S.g.pos.side ^ 1 : S.g.pos.side; 487 return (m & 1) ? WHITE : BLACK; 488 } 489 490 static void do_act(Ctrl *c, int type, int color) 491 { 492 Act a = { (uint8_t)type, (int8_t)color, 0, 0 }; 493 int e = submit(&a); 494 if (e) ctrl_error(c, e); 495 } 496 497 enum { PM_CONTINUE = 1, PM_DRAW, PM_CLAIM, PM_UNDO, PM_RESIGN, PM_FLIP, PM_THEME, PM_QUIT }; 498 static const char *const THEME_NAMES[NTHEMES] = { "classic", "ocean", "night", "contrast", "colorblind", "sunset" }; 499 500 static bool pause_menu(Ctrl *c) 501 { 502 bool en = g_lang; 503 Form f; 504 form_begin(&f, en ? "Game menu" : "Menú de la partida"); 505 bool over = S.g.result != R_NONE; 506 form_button(&f, PM_CONTINUE, en ? "Continue" : "Seguir"); 507 if (!over) { 508 form_button(&f, PM_DRAW, en ? "Offer a draw" : "Ofrecer tablas"); 509 if (game_can_claim(&S.g)) form_button(&f, PM_CLAIM, en ? "Claim a draw" : "Reclamar tablas"); 510 if (S.g.allow_undo) form_button(&f, PM_UNDO, en ? "Take back" : "Deshacer"); 511 form_button(&f, PM_RESIGN, en ? "Resign" : "Abandonar"); 512 } else if (S.g.allow_undo && S.g.nply) { 513 Act u = { ACT_UNDO, (int8_t)acting_color(true), 0, 0 }; 514 if (game_check(&S.g, &u) == E_OK) form_button(&f, PM_UNDO, en ? "Take back" : "Deshacer"); 515 } 516 form_button(&f, PM_FLIP, en ? "Flip the board" : "Girar el tablero"); 517 form_choice(&f, PM_THEME, en ? "Theme" : "Tema", THEME_NAMES, NTHEMES, S.theme); 518 form_button(&f, PM_QUIT, en ? "Back to the menu (saved)" : "Volver al menú (queda guardada)"); 519 int r = form_loop(&f, 0, 0); 520 S.theme = form_get(&f, PM_THEME)->value; 521 S.dirty = true; 522 switch (r) { 523 case PM_DRAW: do_act(c, ACT_DRAW, acting_color(false)); break; 524 case PM_CLAIM: do_act(c, ACT_CLAIM, S.g.pos.side); break; 525 case PM_UNDO: do_act(c, ACT_UNDO, acting_color(true)); break; 526 case PM_RESIGN: do_act(c, ACT_RESIGN, acting_color(false)); break; 527 case PM_FLIP: c->flipped = !c->flipped; break; 528 case PM_QUIT: return true; 529 } 530 return false; 531 } 532 533 static void play(void) 534 { 535 static Ctrl c; 536 uint8_t mask = local_mask(); 537 ctrl_init(&c, mask, mask == 2); 538 bool help = false; 539 int modal = 0; 540 char banner[128]; 541 uint32_t last_second = 0; 542 S.dirty = true; 543 for (;;) { 544 link_poll(); 545 if (local_mask() != mask) { mask = local_mask(); ctrl_init(&c, mask, mask == 2); } 546 if (S.save_due) { S.save_due = false; save_game(); } 547 if (S.role == ROLE_B && S.link != LINK_SYNCED) { 548 static Form f; 549 form_begin(&f, g_lang ? "Cable: connecting..." : "Cable: conectando..."); 550 form_label(&f, g_lang ? "Waiting for PicoCalc A (Esc: cancel)" : "Esperando a la PicoCalc A (Esc: cancelar)"); 551 if (S.dirty) { draw_form(&f); S.dirty = false; } 552 int k = hal_key(); 553 if (k == K_BACK) return; 554 if (k < 0) hal_sleep(10); 555 continue; 556 } 557 Game *g = &S.g; 558 /* bandera (solo la autoridad) */ 559 if (S.role != ROLE_B && g->result == R_NONE && game_has_clock(g) && clock_of(g->pos.side) <= 0) { 560 Act a = { ACT_FLAG, (int8_t)g->pos.side, 0, (int32_t)(hal_ms() - S.turn_start) }; 561 apply(&a); 562 } 563 ctrl_sync(&c, g); 564 if (S.err_pending) { ctrl_error(&c, S.last_err); S.err_pending = false; S.dirty = true; } 565 /* el reloj se redibuja una vez por segundo */ 566 if (game_has_clock(g) && g->result == R_NONE && hal_ms() / 1000 != last_second) { last_second = hal_ms() / 1000; S.dirty = true; } 567 bool engine_turn = S.role == ROLE_LOCAL && g->result == R_NONE && S.kind[g->pos.side] == PL_ENGINE && hal_ms() >= S.engine_at; 568 if (S.dirty || engine_turn) { 569 View v; 570 build_view(&v, &c, banner, sizeof banner, engine_turn); 571 v.show_help = help; 572 if (modal == 1) { v.modal = g_lang ? "Back to the menu? (saved)" : "¿Volver al menú? (queda guardada)"; v.modal2 = g_lang ? "y / n" : "s / n"; } 573 if (modal == 2) { v.modal = g_lang ? "Resign the game?" : "¿Abandonar la partida?"; v.modal2 = g_lang ? "y / n" : "s / n"; } 574 Paint pc = { &v, 0 }; 575 hal_draw(paint, &pc); 576 S.dirty = false; 577 } 578 if (engine_turn && !modal && !help) { engine_move(); continue; } 579 int k = hal_key(); 580 if (k < 0) { hal_sleep(10); continue; } 581 S.dirty = true; 582 if (k == 0) continue; /* solo redibujar */ 583 if (modal) { 584 if (k == 's' || k == 'S' || k == 'y' || k == 'Y' || k == K_OK) { 585 if (modal == 1) { save_game(); return; } 586 do_act(&c, ACT_RESIGN, acting_color(false)); 587 } 588 modal = 0; 589 continue; 590 } 591 if (help) { help = false; continue; } 592 Act a; 593 if (c.mode == CM_ENTRY || c.mode == CM_PROMO) { 594 if (ctrl_key(&c, k, &a)) { int e = submit(&a); if (e) ctrl_error(&c, e); } 595 continue; 596 } 597 uint8_t local = local_mask(); 598 bool offer_to_me = g->draw_offer && ((local >> ((g->draw_offer - 1) ^ 1)) & 1) && g->result == R_NONE; 599 bool undo_to_me = g->undo_req && ((local >> ((g->undo_req - 1) ^ 1)) & 1) && g->result == R_NONE; 600 switch (k) { 601 case '?': help = true; continue; 602 case 'L': g_lang ^= 1; continue; 603 case 'f': c.flipped = !c.flipped; continue; 604 case 't': S.theme = (S.theme + 1) % NTHEMES; continue; 605 case 'q': case 'Q': modal = 1; continue; 606 case 'm': if (pause_menu(&c)) { save_game(); return; } continue; 607 case 'a': case 'n': 608 if (offer_to_me) do_act(&c, k == 'a' ? ACT_DRAWOK : ACT_DRAWNO, (g->draw_offer - 1) ^ 1); 609 else if (undo_to_me) do_act(&c, k == 'a' ? ACT_UNDOOK : ACT_UNDONO, (g->undo_req - 1) ^ 1); 610 continue; 611 case 'u': do_act(&c, ACT_UNDO, acting_color(true)); continue; 612 case 'd': do_act(&c, ACT_DRAW, acting_color(false)); continue; 613 case 'c': do_act(&c, ACT_CLAIM, g->pos.side); continue; 614 case 'r': if (g->result == R_NONE) modal = 2; continue; 615 case K_BACK: 616 if (c.sel < 0) { if (pause_menu(&c)) { save_game(); return; } continue; } 617 break; 618 } 619 if (ctrl_key(&c, k, &a)) { int e = submit(&a); if (e) ctrl_error(&c, e); } 620 } 621 } 622 623 /* ------------------------------------------------------------ puzzles */ 624 625 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)); } 626 627 static void play_puzzle(const Puzzle *pz) 628 { 629 char sol[32][8]; 630 int ns = 0; 631 const char *q = pz->solution; 632 while (*q && ns < 32) { 633 while (*q == ' ') q++; 634 int k = 0; 635 while (*q && *q != ' ' && k < 7) sol[ns][k++] = *q++; 636 sol[ns][k] = 0; 637 if (k) ns++; 638 } 639 Pos p; 640 pos_from_fen(&p, pz->fen); 641 int me = p.side; 642 static Ctrl c; 643 for (;;) { 644 start_game(ROLE_LOCAL, &p, 0, false); 645 S.save_due = false; 646 cpy(S.names[me], 20, g_lang ? "You" : "Vos"); 647 cpy(S.names[me ^ 1], 20, pz->name[g_lang]); 648 ctrl_init(&c, (uint8_t)(1 << me), me == BLACK); 649 int step = 0, state = 0; /* 0 jugando, 1 resuelto, 2 fallo */ 650 uint32_t reply_at = 0; 651 char banner[160]; 652 bool dirty = true; 653 for (;;) { 654 if (state == 0 && step < ns && step % 2 == 1 && hal_ms() >= reply_at) { 655 Move m = move_from_uci(&S.g.pos, sol[step]); 656 Act a = { ACT_MOVE, (int8_t)S.g.pos.side, m, 0 }; 657 if (m) game_apply(&S.g, &a); 658 step++; 659 if (step >= ns) state = 1; 660 dirty = true; 661 } 662 ctrl_sync(&c, &S.g); 663 if (dirty) { 664 View v; 665 memset(&v, 0, sizeof v); 666 v.g = &S.g; 667 v.c = &c; 668 v.names[0] = S.names[0]; 669 v.names[1] = S.names[1]; 670 v.theme = S.theme; 671 v.layout = LAYOUT_COMPACT; 672 v.view_ply = -1; 673 v.clock_running = -1; 674 Sb s = { banner, sizeof banner, 0 }; 675 banner[0] = 0; 676 if (state == 1) sb_s(&s, g_lang ? "Solved! Enter: next try Esc: back" : "¡Resuelto! Enter: de nuevo Esc: volver"); 677 else if (state == 2) { 678 sb_s(&s, g_lang ? "Not quite: " : "No es esa: "); 679 for (int i = step; i < ns; i++) { sb_s(&s, sol[i]); sb_s(&s, " "); } 680 } else { sb_s(&s, pz->name[g_lang]); sb_s(&s, g_lang ? ": find the best move" : ": buscá la mejor jugada"); } 681 v.banner = banner; 682 Paint pc = { &v, 0 }; 683 hal_draw(paint, &pc); 684 dirty = false; 685 } 686 int k = hal_key(); 687 if (k < 0) { hal_sleep(10); continue; } 688 dirty = true; 689 if (k == 0) continue; 690 if (state != 0) { if (k == K_BACK || k == 'q') return; break; } 691 if (k == 'q' || (k == K_BACK && c.sel < 0 && c.mode == CM_BOARD)) return; 692 if (k == 'f') { c.flipped = !c.flipped; continue; } 693 Act a; 694 if (!ctrl_key(&c, k, &a) || step % 2 == 1) continue; 695 Move want = move_from_uci(&S.g.pos, sol[step]); 696 Game after = S.g; 697 game_apply(&after, &a); 698 bool ok = same_move(a.m, want) || (step == ns - 1 && after.reason == RS_MATE); 699 if (ok) { 700 S.g = after; 701 step++; 702 reply_at = hal_ms() + 600; 703 if (step >= ns) state = 1; 704 } else state = 2; 705 } 706 } 707 } 708 709 static void puzzles(void) 710 { 711 for (;;) { 712 Form f; 713 form_begin(&f, "Puzzles"); 714 for (int i = 0; i < NPUZZLES && i < 18; i++) form_button(&f, 100 + i, PUZZLES[i].name[g_lang]); 715 form_button(&f, 1, g_lang ? "Back" : "Volver"); 716 int r = form_loop(&f, 0, 0); 717 if (r < 100) return; 718 play_puzzle(&PUZZLES[r - 100]); 719 } 720 } 721 722 /* ------------------------------------------------------------ menus */ 723 724 static const char *YN[2][2] = { { "No", "Sí" }, { "No", "Yes" } }; 725 static const char *LEVELS[2][LEVEL_COUNT] = { 726 { "Fácil", "Principiante", "Medio", "Difícil", "Máximo" }, 727 { "Easy", "Beginner", "Medium", "Hard", "Maximum" }, 728 }; 729 static const char *COLORS[2][3] = { { "Blancas", "Negras", "Al azar" }, { "White", "Black", "Random" } }; 730 static const char *TCL[] = { "-", "3+2", "5+0", "5+5", "10+5", "15+10" }; 731 static const int32_t TCV[][2] = { { 0, 0 }, { 180000, 2000 }, { 300000, 0 }, { 300000, 5000 }, { 600000, 5000 }, { 900000, 10000 } }; 732 static int opt_color, opt_level = 2, opt_tc, opt_undo = 1; 733 734 enum { B_BACK = 0, B_GO = 1, F_COLOR = 10, F_LEVEL, F_TC, F_UNDO, F_WLEVEL, F_BLEVEL }; 735 enum { MODE_AI, MODE_HOTSEAT, MODE_WATCH, MODE_CABLE }; 736 737 static void new_game(int mode) 738 { 739 bool en = g_lang; 740 Form f; 741 form_begin(&f, mode == MODE_AI ? (en ? "Play the computer" : "Contra la máquina") 742 : mode == MODE_HOTSEAT ? (en ? "Two players" : "Dos jugadores") 743 : mode == MODE_WATCH ? (en ? "Computer vs computer" : "Máquina contra máquina") : (en ? "Cable 1v1: create (A)" : "Cable 1v1: crear (A)")); 744 if (mode == MODE_AI || mode == MODE_CABLE) form_choice(&f, F_COLOR, en ? "Your color" : "Tu color", COLORS[en], 3, opt_color); 745 if (mode == MODE_AI) form_choice(&f, F_LEVEL, en ? "Level" : "Nivel", LEVELS[en], LEVEL_COUNT, opt_level); 746 if (mode == MODE_WATCH) { 747 form_choice(&f, F_WLEVEL, en ? "White" : "Blancas", LEVELS[en], LEVEL_COUNT, opt_level); 748 form_choice(&f, F_BLEVEL, en ? "Black" : "Negras", LEVELS[en], LEVEL_COUNT, opt_level); 749 } else { 750 form_choice(&f, F_TC, en ? "Clock" : "Reloj", TCL, 6, opt_tc); 751 form_choice(&f, F_UNDO, en ? "Takebacks" : "Deshacer", YN[en], 2, opt_undo); 752 } 753 form_button(&f, B_GO, en ? "Start" : "Empezar"); 754 form_button(&f, B_BACK, en ? "Back" : "Volver"); 755 f.cur = f.n - 2; 756 if (form_loop(&f, 0, 0) != B_GO) return; 757 TimeCtl tc = { 0, 0, 0 }; 758 bool undo = true; 759 if (mode != MODE_WATCH) { 760 opt_tc = form_get(&f, F_TC)->value; 761 opt_undo = form_get(&f, F_UNDO)->value; 762 tc.base = TCV[opt_tc][0]; 763 tc.inc = TCV[opt_tc][1]; 764 undo = opt_undo; 765 } 766 int me = WHITE; 767 if (mode == MODE_AI || mode == MODE_CABLE) { 768 opt_color = form_get(&f, F_COLOR)->value; 769 me = opt_color == 2 ? (int)(hal_random() & 1) : opt_color; 770 } 771 start_game(mode == MODE_CABLE ? ROLE_A : ROLE_LOCAL, 0, &tc, undo); 772 S.kind[0] = S.kind[1] = PL_HUMAN; 773 S.engine_delay = 300; 774 if (mode == MODE_AI) { 775 opt_level = form_get(&f, F_LEVEL)->value; 776 S.kind[me ^ 1] = PL_ENGINE; 777 S.level[me ^ 1] = opt_level; 778 cpy(S.names[me], 20, en ? "You" : "Vos"); 779 cpy(S.names[me ^ 1], 20, LEVELS[en][opt_level]); 780 } else if (mode == MODE_WATCH) { 781 S.kind[0] = S.kind[1] = PL_ENGINE; 782 S.level[0] = form_get(&f, F_WLEVEL)->value; 783 S.level[1] = form_get(&f, F_BLEVEL)->value; 784 S.engine_delay = 800; 785 cpy(S.names[0], 20, LEVELS[en][S.level[0]]); 786 cpy(S.names[1], 20, LEVELS[en][S.level[1]]); 787 } else if (mode == MODE_CABLE) { 788 S.wseat = me == WHITE ? 0 : 1; 789 cpy(S.names[me], 20, "A"); 790 cpy(S.names[me ^ 1], 20, "B"); 791 S.peer = false; 792 link_reset(); 793 } else { 794 cpy(S.names[0], 20, color_name(WHITE)); 795 cpy(S.names[1], 20, color_name(BLACK)); 796 } 797 play(); 798 } 799 800 static void join_cable(void) 801 { 802 memset(&S.g, 0, sizeof S.g); 803 game_init(&S.g, 0, 0, false); 804 S.role = ROLE_B; 805 S.my_color = BLACK; 806 S.nlog = 0; 807 cpy(S.names[BLACK], 20, "B"); 808 link_reset(); 809 b_restart(); 810 play(); 811 } 812 813 /* ------------------------------------------------------------ prueba */ 814 815 void boot_log(const char *msg); 816 #ifdef CHESS_DEVICE 817 #define APPLOG(s) boot_log(s) 818 #else 819 #define APPLOG(s) hal_log(s) 820 #endif 821 822 static void self_test(void) 823 { 824 char lines[8][54]; 825 int nl = 0; 826 Form f; 827 form_begin(&f, g_lang ? "Test" : "Prueba"); 828 form_label(&f, g_lang ? "Running... (takes about a minute)" : "Probando... (tarda un minuto)"); 829 draw_form(&f); 830 /* perft: generacion de jugadas */ 831 Pos p; 832 pos_start(&p); 833 uint32_t t0 = hal_ms(); 834 uint64_t n = perft(&p, 3); 835 Sb s = { lines[nl], 54, 0 }; 836 lines[nl][0] = 0; 837 sb_s(&s, "perft 3: "); sb_i(&s, (long)n); sb_s(&s, n == 8902 ? " ok " : " MAL "); sb_i(&s, (long)(hal_ms() - t0)); sb_s(&s, " ms"); 838 APPLOG(lines[nl]); 839 nl++; 840 /* velocidad del motor */ 841 Game g; 842 game_init(&g, 0, 0, false); 843 SearchLimits lim; 844 memset(&lim, 0, sizeof lim); 845 lim.movetime = 3000; 846 SearchResult r; 847 engine_clear(&eng); 848 engine_search(&eng, &g, &lim, &r); 849 s = (Sb){ lines[nl], 54, 0 }; 850 lines[nl][0] = 0; 851 sb_s(&s, g_lang ? "engine: " : "motor: "); sb_i(&s, (long)(r.ms ? r.nodes / r.ms : 0)); sb_s(&s, " knps, prof "); sb_i(&s, r.depth); 852 APPLOG(lines[nl]); 853 nl++; 854 /* puzzles */ 855 int solved = 0; 856 for (int i = 0; i < NPUZZLES; i++) { 857 Pos pp; 858 pos_from_fen(&pp, PUZZLES[i].fen); 859 game_init(&g, &pp, 0, false); 860 lim.movetime = 2000; 861 engine_clear(&eng); 862 engine_search(&eng, &g, &lim, &r); 863 char u[8]; 864 move_uci(r.best, u, sizeof u); 865 bool mate_in_one = !strchr(PUZZLES[i].solution, ' ') && r.score >= MATE - 1; 866 if (!strncmp(u, PUZZLES[i].solution, strlen(u)) || mate_in_one) solved++; 867 } 868 s = (Sb){ lines[nl], 54, 0 }; 869 lines[nl][0] = 0; 870 sb_s(&s, "puzzles: "); sb_i(&s, solved); sb_s(&s, "/"); sb_i(&s, NPUZZLES); 871 APPLOG(lines[nl]); 872 nl++; 873 form_begin(&f, g_lang ? "Test" : "Prueba"); 874 for (int i = 0; i < nl; i++) form_label(&f, lines[i]); 875 form_button(&f, 1, "OK"); 876 f.cur = f.n - 1; 877 form_loop(&f, 0, 0); 878 } 879 880 /* ------------------------------------------------------------ menu principal */ 881 882 enum { M_AI = 1, M_HOTSEAT, M_CONT, M_PUZZLES, M_WATCH, M_CABLE_A, M_CABLE_B, M_TEST, M_LANG = 50, M_THEME }; 883 static const char *LANGS[] = { "Español", "English" }; 884 885 void app_main(void) 886 { 887 hal_init(); 888 gfx_form_backdrop = chess_form_backdrop; 889 engine_init(&eng, tt, sizeof tt / sizeof tt[0], now_ms); 890 eng.poll = engine_poll; 891 APPLOG("8 menu principal"); 892 for (;;) { 893 static Form f; 894 bool en = g_lang; 895 int n = hal_store_load(savebuf, 16); 896 bool have = n >= 14 && !strncmp(savebuf, "AJEDREZ-SAVE 1", 14); 897 form_begin(&f, en ? "PicoCalc Chess" : "Ajedrez PicoCalc"); 898 form_button(&f, M_AI, en ? "Play the computer" : "Contra la máquina"); 899 form_button(&f, M_HOTSEAT, en ? "Two players" : "Dos jugadores"); 900 form_button(&f, M_CONT, en ? "Continue saved game" : "Continuar partida guardada")->disabled = !have; 901 form_button(&f, M_PUZZLES, "Puzzles"); 902 form_button(&f, M_WATCH, en ? "Computer vs computer" : "Máquina contra máquina"); 903 form_button(&f, M_CABLE_A, en ? "Cable 1v1: create (A)" : "Cable 1v1: crear (A)"); 904 form_button(&f, M_CABLE_B, en ? "Cable 1v1: join (B)" : "Cable 1v1: unirse (B)"); 905 form_choice(&f, M_LANG, en ? "Language" : "Idioma", LANGS, 2, g_lang); 906 form_choice(&f, M_THEME, en ? "Theme" : "Tema", THEME_NAMES, NTHEMES, S.theme); 907 form_button(&f, M_TEST, en ? "Test: speed and puzzles" : "Prueba: velocidad y puzzles"); 908 f.cur = have ? 2 : 0; 909 int r = form_loop(&f, 0, 0); 910 g_lang = form_get(&f, M_LANG)->value; 911 S.theme = form_get(&f, M_THEME)->value; 912 switch (r) { 913 case M_AI: new_game(MODE_AI); break; 914 case M_HOTSEAT: new_game(MODE_HOTSEAT); break; 915 case M_WATCH: new_game(MODE_WATCH); break; 916 case M_CABLE_A: new_game(MODE_CABLE); break; 917 case M_CABLE_B: join_cable(); break; 918 case M_PUZZLES: puzzles(); break; 919 case M_TEST: self_test(); break; 920 case M_CONT: 921 if (load_game()) { if (S.role == ROLE_A) link_reset(); play(); } 922 else notice("Error", en ? "Could not read the saved game" : "No se pudo leer la partida guardada"); 923 break; 924 } 925 } 926 }