juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

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 }