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 (29713B)


      1 /* app.c - la aplicacion de la PicoCalc (portable: corre en la PicoCalc y en el simulador).
      2  *
      3  * Modos: partida local (una PicoCalc, personas y/o bots) y cable 1v1 entre dos
      4  * PicoCalc. En el cable, la "A" corre la autoridad y la "B" es cliente; hablan
      5  * el mismo protocolo de lineas que el servidor (ver server/server.h), con un
      6  * checksum al final de cada linea (" *XX") porque el UART no es confiable. */
      7 #include <string.h>
      8 #include "hal.h"
      9 #include "link.h"
     10 #include "../core/host.h"
     11 #include "form.h"
     12 #include "../gfx/catan_gfx.h"
     13 
     14 #define MAXMOVES 1500
     15 #define LOGN 40
     16 #define SAVE_MAX 32768
     17 
     18 enum { ROLE_LOCAL, ROLE_A, ROLE_B };
     19 enum { LINK_IDLE, LINK_HELLO, LINK_JOIN, LINK_SYNCED };
     20 
     21 static struct {
     22     int      role;
     23     Host     host;
     24     Game     view;
     25     Undo     view_undo;
     26     const Game *g;
     27     uint8_t  local_mask;
     28     bool     handoff;
     29     char     names[MAXP][20];
     30     Move     moves[MAXMOVES];
     31     int      nmoves;
     32     uint32_t seed;
     33     Opts     opts;
     34     uint8_t  kinds[MAXP];
     35     char     log[LOGN][80];
     36     int      nlog;
     37     uint32_t next_bot;
     38     int      link;              /* B: estado de conexion */
     39     bool     peer;              /* A: la B esta conectada */
     40     int      my_seat;
     41     uint32_t link_t;
     42     uint32_t ping_t, pong_t;
     43     int      evcount;           /* B: movidas recibidas desde el HDR */
     44     int      last_err;
     45     bool     err_pending;
     46     bool     dirty;
     47     bool     save_due;
     48     bool     replaying;
     49 } S;
     50 
     51 static char savebuf[SAVE_MAX];
     52 
     53 /* ------------------------------------------------------------ texto */
     54 
     55 typedef struct { char *b; int n, pos; } Sb;
     56 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; }
     57 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; }
     58 static void cpy(char *d, int n, const char *s) { str_put(d, n, 0, s); }
     59 
     60 static void add_log(const char *line)
     61 {
     62     if (S.nlog == LOGN) { memmove(S.log[0], S.log[1], sizeof S.log[0] * (LOGN - 1)); S.nlog--; }
     63     cpy(S.log[S.nlog++], sizeof S.log[0], line);
     64     S.dirty = true;
     65 }
     66 
     67 static void describe(const Game *g, const Move *m)
     68 {
     69     const char *names[MAXP];
     70     for (int i = 0; i < MAXP; i++) names[i] = S.names[i];
     71     if (m->type == M_REJECT) return;
     72     unsigned lm = S.local_mask, viewers = lm && !(lm & (lm - 1)) ? lm : 0;   /* como en la mesa */
     73     char buf[120];
     74     move_describe_for(g, m, names, viewers, buf, sizeof buf);
     75     add_log(buf);
     76 }
     77 
     78 /* ------------------------------------------------------------ cable */
     79 
     80 /* A: mandar todo el estado a la B */
     81 static void a_attach(void)
     82 {
     83     char line[256];
     84     link_send("GAME CABLE 1 -");
     85     str_put(line, sizeof line, 0, "HDR ");
     86     hdr_fmt(&S.host.a.g, line + 4, (int)sizeof line - 4);
     87     link_send(line);
     88     for (int i = 0; i < S.host.a.g.o.np; i++) {
     89         Sb s = { line, sizeof line, 0 };
     90         line[0] = 0;
     91         char k = S.kinds[i] == SEAT_BOT ? 'B' : S.host.a.g.p[i].neutral ? 'N' : 'P';
     92         char ks[2] = { k, 0 };
     93         sb_s(&s, "SEAT "); sb_i(&s, i); sb_s(&s, " "); sb_s(&s, ks); sb_s(&s, " 1 "); sb_s(&s, S.names[i]);
     94         link_send(line);
     95     }
     96     for (int i = 0; i < S.nmoves; i++) {
     97         Move r;
     98         move_redact(&S.moves[i], 1, &r);
     99         str_put(line, sizeof line, 0, "EV ");
    100         move_fmt(&r, line + 3, (int)sizeof line - 3);
    101         link_send(line);
    102     }
    103     link_send("SYNC");
    104     if (!S.peer) add_log(g_lang ? "-- other PicoCalc connected --" : "-- la otra PicoCalc se conectó --");
    105     S.peer = true;
    106 }
    107 
    108 static void a_handle(char *line)
    109 {
    110     if (!strncmp(line, "HELLO", 5)) {
    111         const char *nm = strchr(line + 6, ' ');
    112         if (nm && nm[1]) {
    113             cpy(S.names[1], sizeof S.names[1], nm + 1);
    114             cpy(S.host.name[1], sizeof S.host.name[1], nm + 1);
    115         }
    116         link_send("WELCOME 1");
    117     } else if (!strncmp(line, "JOIN", 4) || !strncmp(line, "OPEN", 4)) {
    118         a_attach();
    119     } else if (!strncmp(line, "MOVE ", 5)) {
    120         Move m;
    121         if (!move_parse(line + 5, &m) || m.p != 1) { link_send("ERR 3 bad move"); return; }
    122         int e = host_submit(&S.host, 1, &m);
    123         if (e) {
    124             char buf[32];
    125             Sb s = { buf, sizeof buf, 0 };
    126             buf[0] = 0;
    127             sb_s(&s, "ERR "); sb_i(&s, e); sb_s(&s, " rejected");
    128             link_send(buf);
    129         } else S.next_bot = hal_ms() + 600;
    130     } else if (!strcmp(line, "PING")) {
    131         char buf[24] = "PONG ";
    132         str_int(buf, sizeof buf, 5, S.nmoves);
    133         link_send(buf);
    134     }
    135 }
    136 
    137 static void b_restart(void)
    138 {
    139     S.link = LINK_HELLO;
    140     S.link_t = 0;
    141 }
    142 
    143 static void b_handle(char *line)
    144 {
    145     if (!strncmp(line, "WELCOME", 7)) {
    146         link_send("JOIN -");
    147         S.link = LINK_JOIN;
    148         S.link_t = hal_ms();
    149     } else if (!strncmp(line, "GAME ", 5)) {
    150         long seat;
    151         const char *p = strchr(line + 5, ' ');
    152         if (p && str_scan_int(p, &seat)) S.my_seat = (int)seat;
    153     } else if (!strncmp(line, "HDR ", 4)) {
    154         hdr_parse(line + 4, &S.view);
    155         S.view_undo.n = 0;
    156         S.nlog = 0;
    157         S.evcount = 0;
    158         S.local_mask = (uint8_t)(1 << S.my_seat);
    159     } else if (!strncmp(line, "SEAT ", 5)) {
    160         long i, on;
    161         const char *p = str_scan_int(line + 5, &i);
    162         if (!p || i < 0 || i >= MAXP) return;
    163         while (*p == ' ') p++;
    164         p++;
    165         p = str_scan_int(p, &on);
    166         if (!p) return;
    167         while (*p == ' ') p++;
    168         cpy(S.names[i], sizeof S.names[i], p);
    169     } else if (!strncmp(line, "EV ", 3)) {
    170         Move m;
    171         if (move_parse(line + 3, &m)) { game_apply_u(&S.view, &S.view_undo, &m); describe(&S.view, &m); S.evcount++; }
    172     } else if (!strncmp(line, "PONG", 4)) {
    173         long n;
    174         S.pong_t = hal_ms();
    175         /* si la A tiene mas movidas de las que recibimos, se perdio algo: resincronizar */
    176         if (S.link == LINK_SYNCED && str_scan_int(line + 4, &n) && n != S.evcount) b_restart();
    177     } else if (!strcmp(line, "SYNC")) {
    178         S.link = LINK_SYNCED;
    179         S.pong_t = hal_ms();
    180         add_log(g_lang ? "-- connected by cable --" : "-- conectada por cable --");
    181     } else if (!strncmp(line, "ERR ", 4)) {
    182         long e;
    183         if (str_scan_int(line + 4, &e)) { S.last_err = (int)e; S.err_pending = true; }
    184     }
    185     S.dirty = true;
    186 }
    187 
    188 static void link_poll(void)
    189 {
    190     if (S.role == ROLE_LOCAL) return;
    191     static char line[600];
    192     int r;
    193     while ((r = link_line(line, sizeof line)) != 0) {
    194         if (r < 0) {                     /* linea corrupta */
    195             if (S.role == ROLE_B) { S.link = LINK_HELLO; S.link_t = 0; }
    196             continue;
    197         }
    198         if (S.role == ROLE_A) a_handle(line); else b_handle(line);
    199     }
    200     if (S.role == ROLE_B) {
    201         uint32_t now = hal_ms();
    202         if (S.link == LINK_HELLO && now - S.link_t > 1500) {
    203             link_send2("HELLO 1 ", S.names[S.my_seat][0] ? S.names[S.my_seat] : "B");
    204             S.link_t = now;
    205         } else if (S.link == LINK_JOIN && now - S.link_t > 8000) b_restart();
    206         else if (S.link == LINK_SYNCED) {
    207             if (now - S.ping_t > 2000) { link_send("PING"); S.ping_t = now; }
    208             if (now - S.pong_t > 7000) b_restart();     /* cable desenchufado */
    209         }
    210     }
    211 }
    212 
    213 /* ------------------------------------------------------------ guardado */
    214 
    215 static void save_game(void)
    216 {
    217     if (S.role == ROLE_B) return;
    218     Sb s = { savebuf, SAVE_MAX, 0 };
    219     savebuf[0] = 0;
    220     const Opts *o = &S.opts;
    221     sb_s(&s, "CATAN-SAVE 1\nROLE "); sb_s(&s, S.role == ROLE_A ? "A" : "L");
    222     sb_s(&s, "\nSEED "); sb_i(&s, (long)S.seed);
    223     sb_s(&s, "\nOPTS ");
    224     long ov[7] = { o->np, o->variant2p, o->vp_target, o->friendly_robber, o->tokens_start, o->neutral_setup, o->beginner };
    225     for (int i = 0; i < 7; i++) { sb_i(&s, ov[i]); sb_s(&s, " "); }
    226     sb_s(&s, "\n");
    227     for (int i = 0; i < o->np; i++) {
    228         sb_s(&s, "SEAT "); sb_i(&s, i); sb_s(&s, " "); sb_i(&s, S.kinds[i]); sb_s(&s, " "); sb_s(&s, S.names[i]); sb_s(&s, "\n");
    229     }
    230     char line[96];
    231     for (int i = 0; i < S.nmoves && s.pos < SAVE_MAX - 100; i++) {
    232         if (move_is_auto(&S.moves[i])) continue;
    233         move_fmt(&S.moves[i], line, sizeof line);
    234         sb_s(&s, "M "); sb_s(&s, line); sb_s(&s, "\n");
    235     }
    236     hal_store_save(savebuf, s.pos + 1);
    237 }
    238 
    239 /* ------------------------------------------------------------ partida */
    240 
    241 static void on_move(void *ctx, const Move *m)
    242 {
    243     (void)ctx;
    244     if (S.nmoves < MAXMOVES) S.moves[S.nmoves++] = *m;
    245     describe(&S.host.a.g, m);
    246     if (S.replaying) return;
    247     if (S.role == ROLE_A && S.peer) {
    248         char line[128] = "EV ";
    249         Move r;
    250         move_redact(m, 1, &r);
    251         move_fmt(&r, line + 3, (int)sizeof line - 3);
    252         link_send(line);
    253     }
    254     if (m->type == M_END || m->type == M_WIN) S.save_due = true;
    255 }
    256 
    257 static void start_host(int role, const Opts *o, uint32_t seed, const uint8_t kinds[MAXP])
    258 {
    259     char names[MAXP][20];
    260     memcpy(names, S.names, sizeof names);
    261     memset(&S, 0, sizeof S);
    262     link_reset();
    263     memcpy(S.names, names, sizeof names);
    264     S.role = role;
    265     S.seed = seed;
    266     S.opts = *o;
    267     memcpy(S.kinds, kinds, MAXP);
    268     S.replaying = true;
    269     host_init(&S.host, o, seed, kinds, on_move, 0);
    270     S.replaying = false;
    271     /* bots con busqueda: el RP2040 es mucho mas lento, se corta por tiempo */
    272     SearchCfg b = { .iters = 3000, .horizon = 8, .c_ucb = 0.5f, .now_ms = hal_ms, .max_ms = 1200 };
    273     S.host.bot = b;
    274     for (int i = 0; i < o->np; i++) {
    275         if (S.host.a.g.p[i].neutral) {
    276             Sb s = { S.names[i], 20, 0 };
    277             S.names[i][0] = 0;
    278             sb_s(&s, tr(S_NEUTRAL)); sb_s(&s, " "); sb_i(&s, i - 1);
    279         }
    280         if (S.host.kind[i] == SEAT_LOCAL) S.local_mask |= (uint8_t)(1 << i);
    281         cpy(S.host.name[i], sizeof S.host.name[i], S.names[i]);
    282     }
    283     S.g = &S.host.a.g;
    284 }
    285 
    286 static bool load_game(void)
    287 {
    288     int n = hal_store_load(savebuf, SAVE_MAX - 1);
    289     if (n <= 0) return false;
    290     savebuf[n] = 0;
    291     if (strncmp(savebuf, "CATAN-SAVE 1", 12)) return false;
    292     Opts o;
    293     memset(&o, 0, sizeof o);
    294     uint8_t kinds[MAXP] = { 0 };
    295     long seed = 0;
    296     int role = ROLE_LOCAL;
    297     char *p = savebuf, *moves = 0;
    298     while (*p && !moves) {
    299         char *e = strchr(p, '\n');
    300         if (!e) break;
    301         *e = 0;
    302         long v;
    303         if (!strncmp(p, "ROLE ", 5)) role = p[5] == 'A' ? ROLE_A : ROLE_LOCAL;
    304         else if (!strncmp(p, "SEED ", 5)) { if (str_scan_int(p + 5, &v)) seed = v; }
    305         else if (!strncmp(p, "OPTS ", 5)) {
    306             const char *q = p + 5;
    307             long ov[7];
    308             for (int i = 0; i < 7 && q; i++) q = str_scan_int(q, &ov[i]);
    309             if (!q) return false;
    310             o.np = (uint8_t)ov[0]; o.variant2p = (uint8_t)ov[1]; o.vp_target = (uint8_t)ov[2]; o.friendly_robber = (uint8_t)ov[3];
    311             o.tokens_start = (uint8_t)ov[4]; o.neutral_setup = (uint8_t)ov[5]; o.beginner = (uint8_t)ov[6];
    312         } else if (!strncmp(p, "SEAT ", 5)) {
    313             long i, k;
    314             const char *q = str_scan_int(p + 5, &i);
    315             if (q) q = str_scan_int(q, &k);
    316             if (q && i >= 0 && i < MAXP) {
    317                 kinds[i] = (uint8_t)k;
    318                 while (*q == ' ') q++;
    319                 cpy(S.names[i], sizeof S.names[i], q);
    320             }
    321         } else if (p[0] == 'M') { *e = '\n'; moves = p; break; }
    322         p = e + 1;
    323     }
    324     if (o.np < 2 || o.np > MAXP) return false;
    325     start_host(role, &o, (uint32_t)seed, kinds);
    326     S.replaying = true;
    327     p = moves;
    328     while (p && *p) {
    329         char *e = strchr(p, '\n');
    330         if (e) *e = 0;
    331         Move m;
    332         if (p[0] == 'M' && move_parse(p + 2, &m)) host_replay(&S.host, &m);
    333         p = e ? e + 1 : 0;
    334     }
    335     S.replaying = false;
    336     add_log(g_lang ? "-- game resumed --" : "-- partida retomada --");
    337     return true;
    338 }
    339 
    340 /* ------------------------------------------------------------ dibujo */
    341 
    342 typedef struct { const Game *g; const Ctrl *c; const UiInfo *ui; const Form *f; } Paint;
    343 
    344 static void paint(Surface *s, void *ctx)
    345 {
    346     Paint *p = ctx;
    347     if (p->f) gfx_form(s, p->f, 0);
    348     else {
    349         BoardGeom geo;
    350         gfx_screen(s, p->g, p->c, p->ui, 0, &geo);
    351     }
    352 }
    353 
    354 static int form_loop(Form *f, void (*on_change)(Form *, void *), void *ctx, bool (*tick)(void))
    355 {
    356     Paint pc = { 0, 0, 0, f };
    357     if (on_change) on_change(f, ctx);
    358     bool dirty = true;
    359     for (;;) {
    360         if (tick && tick()) return FORM_NONE;
    361         if (dirty) { hal_draw(paint, &pc); dirty = false; }
    362         int k = hal_key();
    363         if (k < 0) { hal_sleep(10); continue; }
    364         int r = form_key(f, k);
    365         if (r != FORM_NONE) return r;
    366         if (on_change) on_change(f, ctx);
    367         dirty = true;
    368     }
    369 }
    370 
    371 /* ------------------------------------------------------------ loop de juego */
    372 
    373 static int submit(Move *m)
    374 {
    375     if (m->p < 0 || m->p >= MAXP || !((S.local_mask >> m->p) & 1)) return E_TURN;
    376     if (S.role == ROLE_B) {
    377         if (S.link != LINK_SYNCED) return E_TURN;
    378         bool setup_road = m->type == M_ROAD && S.view.phase == PH_SETUP && !S.view.setup_sub;
    379         int e = setup_road ? E_OK : game_check(&S.view, m);   /* va detras del pueblo */
    380         if (e) return e;
    381         char line[128] = "MOVE ";
    382         move_fmt(m, line + 5, (int)sizeof line - 5);
    383         link_send(line);
    384         return E_OK;
    385     }
    386     int e = host_submit(&S.host, m->p, m);
    387     if (!e) S.next_bot = hal_ms() + 600;
    388     return e;
    389 }
    390 
    391 static void play(void)
    392 {
    393     static Ctrl c;
    394     static UiInfo ui;
    395     memset(&ui, 0, sizeof ui);
    396     ui.layout = LAYOUT_COMPACT;
    397     ctrl_init(&c, S.local_mask, S.handoff);
    398     int modal = 0;
    399     bool help = false;
    400     uint8_t mask = S.local_mask;
    401     S.dirty = true;
    402     for (;;) {
    403         link_poll();
    404         if (S.role == ROLE_B) S.g = &S.view;
    405         if (S.local_mask != mask) { mask = S.local_mask; ctrl_init(&c, mask, S.handoff); }
    406         if (S.role != ROLE_B && host_bot_pending(&S.host) && hal_ms() >= S.next_bot) {
    407             host_bot_step(&S.host);
    408             S.next_bot = hal_ms() + 600;
    409             S.dirty = true;
    410         }
    411         if (S.save_due) { S.save_due = false; save_game(); }
    412         for (int i = 0; i < MAXP; i++) { c.names[i] = S.names[i]; ui.names[i] = S.names[i]; }
    413         if (S.role == ROLE_B && S.link != LINK_SYNCED) {
    414             static Form f;
    415             form_begin(&f, g_lang ? "Cable: connecting..." : "Cable: conectando...");
    416             form_label(&f, g_lang ? "Waiting for PicoCalc A (Esc: cancel)" : "Esperando a la PicoCalc A (Esc: cancelar)");
    417             Paint pc = { 0, 0, 0, &f };
    418             if (S.dirty) { hal_draw(paint, &pc); S.dirty = false; }
    419             int k = hal_key();
    420             if (k == K_BACK) return;
    421             if (k < 0) hal_sleep(10);
    422             continue;
    423         }
    424         ctrl_sync(&c, S.g);
    425         if (S.err_pending) { ctrl_error(&c, S.last_err); S.err_pending = false; S.dirty = true; }
    426         if (S.dirty) {
    427             static const char *lines[LOGN];
    428             ui.nlog = S.nlog;
    429             for (int i = 0; i < S.nlog; i++) lines[i] = S.log[i];
    430             ui.log = lines;
    431             ui.view_seat = c.shown;
    432             ui.modal = modal;
    433             ui.modal_text = g_lang ? "Back to the menu? (saved)" : "¿Volver al menú? (queda guardada)";
    434             ui.show_help = help;
    435             Paint pc = { S.g, &c, &ui, 0 };
    436             hal_draw(paint, &pc);
    437             S.dirty = false;
    438         }
    439         int k = hal_key();
    440         if (k < 0) { hal_sleep(10); continue; }
    441         S.dirty = true;
    442         if (k == 0) continue;            /* solo redibujar */
    443         if (modal) {
    444             if (k == 's' || k == 'S' || k == 'y' || k == 'Y' || k == K_OK) { save_game(); return; }
    445             modal = 0;
    446             continue;
    447         }
    448         if (help) { help = false; continue; }
    449         if (k == 'q' || k == 'Q') { if (S.g->phase == PH_OVER) return; modal = 1; continue; }
    450         if (S.g->phase == PH_OVER && k == K_OK) return;
    451         if (k == '?') { help = true; continue; }
    452         if (k == 'L') { g_lang ^= 1; c.ctx_sig = 0xFFFFFFFFu; continue; }
    453         if (k == K_TAB && c.mode == CM_IDLE) { ui.show_info = !ui.show_info; continue; }
    454         if (ui.show_info && k == K_BACK) { ui.show_info = false; continue; }
    455         Move m;
    456         if (ctrl_key(&c, k, &m)) {
    457             int e = submit(&m);
    458             while (ctrl_next(&c, &m)) if (!e) e = submit(&m);   /* colocacion: pueblo + ruta */
    459             if (e) ctrl_error(&c, e);
    460         }
    461     }
    462 }
    463 
    464 /* ------------------------------------------------------------ menus */
    465 
    466 enum { B_BACK = 0, B_GO = 1, F_PLAYERS = 10, F_VARIANT, F_BOARD, F_HANDOFF, F_KIND = 20, F_NAME = 30, F_BOTS = 40 };
    467 static const char *CH_NUM[] = { "2", "3", "4" };
    468 static const char *YN_ES[] = { "No", "Sí" }, *YN_EN[] = { "No", "Yes" };
    469 static const char *KI_ES[] = { "Persona", "Bot" }, *KI_EN[] = { "Person", "Bot" };
    470 static const char *BO_ES[] = { "Aleatorio", "Principiante" }, *BO_EN[] = { "Random", "Beginner" };
    471 static const char *NB[] = { "0", "1", "2" };
    472 
    473 static void local_change(Form *f, void *ctx)
    474 {
    475     (void)ctx;
    476     int np = form_get(f, F_PLAYERS)->value + 2;
    477     form_get(f, F_VARIANT)->hidden = np != 2;
    478     for (int i = 0; i < MAXP; i++) {
    479         form_get(f, F_KIND + i)->hidden = i >= np;
    480         form_get(f, F_NAME + i)->hidden = i >= np || form_get(f, F_KIND + i)->value == 1;
    481     }
    482 }
    483 
    484 static void new_local(void)
    485 {
    486     static Form f;
    487     bool en = g_lang;
    488     form_begin(&f, en ? "Local game" : "Partida local");
    489     form_choice(&f, F_PLAYERS, en ? "Players" : "Jugadores", CH_NUM, 3, 0);
    490     form_choice(&f, F_VARIANT, en ? "2p variant" : "Variante 2", en ? YN_EN : YN_ES, 2, 1);
    491     for (int i = 0; i < MAXP; i++) {
    492         char l[16] = "";
    493         Sb s = { l, sizeof l, 0 };
    494         sb_s(&s, en ? "Seat " : "Asiento "); sb_i(&s, i + 1);
    495         form_choice(&f, F_KIND + i, l, en ? KI_EN : KI_ES, 2, i < 2 ? 0 : 1);
    496         char nm[20] = "";
    497         Sb t = { nm, sizeof nm, 0 };
    498         sb_s(&t, tr(S_PLAYER)); sb_s(&t, " "); sb_i(&t, i + 1);
    499         form_text(&f, F_NAME + i, en ? " name" : " nombre", nm, 12);
    500     }
    501     form_choice(&f, F_BOARD, en ? "Board" : "Tablero", en ? BO_EN : BO_ES, 2, 0);
    502     form_choice(&f, F_HANDOFF, en ? "Hand-off" : "Pantalla de pase", en ? YN_EN : YN_ES, 2, 1);
    503     form_button(&f, B_GO, en ? "Start" : "Empezar");
    504     form_button(&f, B_BACK, en ? "Back" : "Volver");
    505     f.cur = f.n - 2;
    506     if (form_loop(&f, local_change, 0, 0) != B_GO) return;
    507     int np = form_get(&f, F_PLAYERS)->value + 2;
    508     bool v2p = np == 2 && form_get(&f, F_VARIANT)->value;
    509     Opts o;
    510     opts_default(&o, np, v2p);
    511     o.beginner = (uint8_t)form_get(&f, F_BOARD)->value;
    512     uint8_t kinds[MAXP] = { 0 };
    513     for (int i = 0; i < np; i++) {
    514         bool bot = form_get(&f, F_KIND + i)->value == 1;
    515         kinds[i] = bot ? SEAT_BOT : SEAT_LOCAL;
    516         if (bot) {
    517             Sb s = { S.names[i], 20, 0 };
    518             S.names[i][0] = 0;
    519             sb_s(&s, "Bot "); sb_i(&s, i + 1);
    520         } else cpy(S.names[i], 20, form_get(&f, F_NAME + i)->text);
    521     }
    522     start_host(ROLE_LOCAL, &o, hal_random(), kinds);
    523     S.handoff = form_get(&f, F_HANDOFF)->value;
    524     play();
    525 }
    526 
    527 static void cable_change(Form *f, void *ctx)
    528 {
    529     (void)ctx;
    530     form_get(f, F_BOTS)->hidden = form_get(f, F_VARIANT)->value == 1;
    531 }
    532 
    533 static void cable_a(void)
    534 {
    535     static Form f;
    536     bool en = g_lang;
    537     form_begin(&f, en ? "Cable: create game (A)" : "Cable: crear partida (A)");
    538     form_text(&f, F_NAME, en ? "Your name" : "Tu nombre", S.names[0][0] ? S.names[0] : "A", 12);
    539     form_choice(&f, F_VARIANT, en ? "2p variant" : "Variante 2", en ? YN_EN : YN_ES, 2, 1);
    540     form_choice(&f, F_BOTS, en ? "Extra bots" : "Bots extra", NB, 3, 0);
    541     form_choice(&f, F_BOARD, en ? "Board" : "Tablero", en ? BO_EN : BO_ES, 2, 0);
    542     form_label(&f, en ? "Cable on J703: 4-5, 5-4, 8-8 (GND)" : "Cable en J703: 4-5, 5-4, 8-8 (GND)");
    543     form_button(&f, B_GO, en ? "Create" : "Crear");
    544     form_button(&f, B_BACK, en ? "Back" : "Volver");
    545     f.cur = f.n - 2;
    546     if (form_loop(&f, cable_change, 0, 0) != B_GO) return;
    547     bool v2p = form_get(&f, F_VARIANT)->value;
    548     int bots = v2p ? 0 : form_get(&f, F_BOTS)->value;
    549     Opts o;
    550     opts_default(&o, 2 + bots, v2p);
    551     o.beginner = (uint8_t)form_get(&f, F_BOARD)->value;
    552     uint8_t kinds[MAXP] = { SEAT_LOCAL, SEAT_REMOTE, SEAT_BOT, SEAT_BOT };
    553     cpy(S.names[0], 20, form_get(&f, F_NAME)->text);
    554     cpy(S.names[1], 20, "B");
    555     for (int i = 2; i < MAXP; i++) {
    556         Sb s = { S.names[i], 20, 0 };
    557         S.names[i][0] = 0;
    558         sb_s(&s, "Bot "); sb_i(&s, i + 1);
    559     }
    560     start_host(ROLE_A, &o, hal_random(), kinds);
    561     add_log(en ? "-- waiting for PicoCalc B on the cable --" : "-- esperando a la PicoCalc B por el cable --");
    562     play();
    563 }
    564 
    565 static void cable_b(void)
    566 {
    567     static Form f;
    568     bool en = g_lang;
    569     form_begin(&f, en ? "Cable: join (B)" : "Cable: unirse (B)");
    570     form_text(&f, F_NAME, en ? "Your name" : "Tu nombre", S.names[1][0] ? S.names[1] : "B", 12);
    571     form_label(&f, en ? "The other PicoCalc must create the game (A)" : "La otra PicoCalc tiene que crear la partida (A)");
    572     form_button(&f, B_GO, en ? "Connect" : "Conectar");
    573     form_button(&f, B_BACK, en ? "Back" : "Volver");
    574     f.cur = 2;
    575     if (form_loop(&f, 0, 0, 0) != B_GO) return;
    576     char name[20];
    577     cpy(name, sizeof name, form_get(&f, F_NAME)->text);
    578     memset(&S, 0, sizeof S);
    579     link_reset();
    580     S.role = ROLE_B;
    581     S.my_seat = 1;
    582     cpy(S.names[1], 20, name);
    583     S.g = &S.view;
    584     b_restart();
    585     play();
    586 }
    587 
    588 /* ------------------------------------------------------------ prueba de bots */
    589 
    590 static uint32_t sim_moves;
    591 static void sim_on_move(void *ctx, const Move *m) { (void)ctx; (void)m; sim_moves++; }
    592 
    593 static const char *sim_check(const Game *g)
    594 {
    595     for (int r = 0; r < NRES; r++) {
    596         int n = g->bank[r];
    597         for (int p = 0; p < g->o.np; p++) n += g->p[p].res[r];
    598         if (n != bank_start(&g->o)) return "recursos no conservados";
    599     }
    600     int onboard[MAXP] = { 0 }, devs = g->deck_n;
    601     for (int v = 0; v < game_topo(g)->nvert; v++) if (g->vown[v] >= 0) onboard[g->vown[v]]++;
    602     for (int p = 0; p < g->o.np; p++) {
    603         const Player *pl = &g->p[p];
    604         if (pl->roads > MAX_ROADS || pl->settles > MAX_SETTLES || pl->cities > MAX_CITIES) return "piezas fuera de rango";
    605         if (onboard[p] != (MAX_SETTLES - pl->settles) + (MAX_CITIES - pl->cities)) return "edificios inconsistentes";
    606         for (int d = 0; d < NDEV; d++) devs += pl->dev[d] + pl->dev_new[d];
    607         devs += pl->knights;
    608     }
    609     uint8_t cnt[NDEV];
    610     if (devs > dev_counts(&g->o, cnt)) return "mazo inconsistente";
    611     return 0;
    612 }
    613 
    614 static void line_add(char *buf, int n, const char *a, long v, const char *b)
    615 {
    616     int p = (int)strlen(buf);
    617     p = str_put(buf, n, p, a);
    618     if (v >= 0) p = str_int(buf, n, p, v);
    619     if (b) str_put(buf, n, p, b);
    620 }
    621 
    622 static void hexs(char *buf, int n, uint32_t v)
    623 {
    624     static const char HX[] = "0123456789abcdef";
    625     int p = (int)strlen(buf);
    626     for (int i = 7; i >= 0 && p < n - 1; i--) buf[p++] = HX[(v >> (4 * i)) & 15];
    627     buf[p] = 0;
    628 }
    629 
    630 static void bot_test(int games)
    631 {
    632     bool verbose = games <= 10;
    633     hal_log_clear();
    634     char buf[96] = "";
    635     line_add(buf, sizeof buf, "=== prueba: ", games, " partidas bot contra bot ===");
    636     hal_log(buf);
    637     uint32_t t_all = hal_ms(), base = hal_random();
    638     uint32_t ok[2] = { 0, 0 }, fail = 0, turns_sum[2] = { 0, 0 }, moves_sum = 0;
    639     uint32_t tmin[2] = { 9999, 9999 }, tmax[2] = { 0, 0 }, wins[2][MAXP];
    640     memset(wins, 0, sizeof wins);
    641     for (int i = 0; i < games; i++) {
    642         int mode = i % 2;                       /* 0: 4 jugadores, 1: variante 2p (alternados) */
    643         if (verbose) mode = i >= games / 2;
    644         Opts o;
    645         opts_default(&o, mode ? 2 : 4, mode == 1);
    646         uint8_t kinds[MAXP] = { SEAT_BOT, SEAT_BOT, SEAT_BOT, SEAT_BOT };
    647         uint32_t seed = base + (uint32_t)i * 2654435761u;
    648         sim_moves = 0;
    649         uint32_t t0 = hal_ms();
    650         host_init(&S.host, &o, seed, kinds, sim_on_move, 0);
    651         Game *g = &S.host.a.g;
    652         const char *err = 0;
    653         int steps = 0;
    654         while (g->phase != PH_OVER && g->turn < 600 && steps < 20000) {
    655             if (!host_bot_step(&S.host)) { err = "nadie puede jugar"; break; }
    656             steps++;
    657             if ((err = sim_check(g))) break;
    658             if ((steps & 255) == 0 && hal_key() == K_BACK) { hal_log("cancelado"); games = i; goto done; }
    659         }
    660         if (!err && g->phase != PH_OVER) err = "sin ganador (600 turnos)";
    661         if (err) {
    662             fail++;
    663             buf[0] = 0;
    664             line_add(buf, sizeof buf, "#", i + 1, mode ? " 2p" : " 4j");
    665             line_add(buf, sizeof buf, " semilla 0x", -1, 0);
    666             hexs(buf, sizeof buf, seed);
    667             line_add(buf, sizeof buf, " turno ", g->turn, " ERROR: ");
    668             line_add(buf, sizeof buf, "", -1, err);
    669             hal_log(buf);
    670         } else {
    671             ok[mode]++;
    672             turns_sum[mode] += g->turn;
    673             moves_sum += sim_moves;
    674             if (g->turn < tmin[mode]) tmin[mode] = g->turn;
    675             if (g->turn > tmax[mode]) tmax[mode] = g->turn;
    676             if (g->winner >= 0) wins[mode][g->winner]++;
    677         }
    678         if (verbose) {
    679             buf[0] = 0;
    680             line_add(buf, sizeof buf, "#", i + 1, mode ? " 2p" : " 4j");
    681             line_add(buf, sizeof buf, " t", g->turn, 0);
    682             line_add(buf, sizeof buf, " m", (long)sim_moves, 0);
    683             if (!err) line_add(buf, sizeof buf, " gana ", g->winner + 1, 0);
    684             line_add(buf, sizeof buf, " ", (long)(hal_ms() - t0), err ? " ms ERROR" : " ms ok");
    685             hal_log(buf);
    686         } else if ((i + 1) % 500 == 0) {
    687             buf[0] = 0;
    688             line_add(buf, sizeof buf, "", i + 1, " partidas, ");
    689             line_add(buf, sizeof buf, "", (long)fail, " errores, ");
    690             line_add(buf, sizeof buf, "", (long)((hal_ms() - t_all) / 1000), " s");
    691             hal_log(buf);
    692         }
    693     }
    694 done:
    695     buf[0] = 0;
    696     line_add(buf, sizeof buf, "RESULTADO: ", (long)(ok[0] + ok[1]), " ok, ");
    697     line_add(buf, sizeof buf, "", (long)fail, " con error");
    698     hal_log(buf);
    699     uint32_t total = hal_ms() - t_all;
    700     buf[0] = 0;
    701     line_add(buf, sizeof buf, "tiempo ", (long)(total / 1000), " s, ");
    702     line_add(buf, sizeof buf, "", games ? (long)(total / (uint32_t)games) : 0, " ms/partida");
    703     hal_log(buf);
    704     for (int m = 0; m < 2; m++) {
    705         if (!ok[m]) continue;
    706         buf[0] = 0;
    707         line_add(buf, sizeof buf, m ? "2p: " : "4j: ", (long)ok[m], " ok, turnos ");
    708         line_add(buf, sizeof buf, "prom ", (long)(turns_sum[m] / ok[m]), 0);
    709         line_add(buf, sizeof buf, " min ", (long)tmin[m], 0);
    710         line_add(buf, sizeof buf, " max ", (long)tmax[m], 0);
    711         hal_log(buf);
    712         buf[0] = 0;
    713         line_add(buf, sizeof buf, "   victorias por asiento:", -1, 0);
    714         for (int p = 0; p < (m ? 2 : 4); p++) line_add(buf, sizeof buf, " ", (long)wins[m][p], 0);
    715         hal_log(buf);
    716     }
    717     if (ok[0] + ok[1]) {
    718         buf[0] = 0;
    719         line_add(buf, sizeof buf, "movidas por partida (prom) ", (long)(moves_sum / (ok[0] + ok[1])), 0);
    720         hal_log(buf);
    721     }
    722     hal_log("Enter para volver");
    723     int k;
    724     while ((k = hal_key()) != K_OK && k != K_BACK) hal_sleep(20);
    725     memset(&S, 0, sizeof S);
    726     link_reset();
    727 }
    728 
    729 static void bot_test_menu(void)
    730 {
    731     static const char *N[] = { "10", "100", "1000", "10000" };
    732     static const int NV[] = { 10, 100, 1000, 10000 };
    733     static Form f;
    734     bool en = g_lang;
    735     form_begin(&f, en ? "Test: bot games" : "Prueba: partidas de bots");
    736     form_choice(&f, 10, en ? "Games" : "Partidas", N, 4, 0);
    737     form_label(&f, en ? "~70 ms per game. Esc cancels." : "~70 ms por partida. Esc cancela.");
    738     form_label(&f, en ? "Results on screen and in catan.log" : "Resultados en pantalla y en catan.log");
    739     form_button(&f, 1, en ? "Start" : "Empezar");
    740     form_button(&f, 0, en ? "Back" : "Volver");
    741     f.cur = 3;
    742     if (form_loop(&f, 0, 0, 0) == 1) bot_test(NV[form_get(&f, 10)->value]);
    743 }
    744 
    745 enum { M_CONT = 1, M_LOCAL, M_CABLE_A, M_CABLE_B, M_TEST, M_LANG = 50 };
    746 static const char *LANGS[] = { "Español", "English" };
    747 
    748 static void lang_change(Form *f, void *ctx)
    749 {
    750     (void)ctx;
    751     g_lang = form_get(f, M_LANG)->value;
    752 }
    753 
    754 void boot_log(const char *msg);
    755 #ifdef CATAN_DEVICE
    756 #define APPLOG(s) boot_log(s)
    757 #else
    758 #define APPLOG(s) ((void)0)
    759 #endif
    760 
    761 void app_main(void)
    762 {
    763     hal_init();
    764     gfx_form_backdrop = catan_form_backdrop;
    765     APPLOG("8 menu principal");
    766     for (;;) {
    767         static Form f;
    768         bool en = g_lang;
    769         int n = hal_store_load(savebuf, 16);
    770         bool have = n >= 12 && !strncmp(savebuf, "CATAN-SAVE 1", 12);
    771         form_begin(&f, en ? "PicoCalc Catan" : "Catan PicoCalc");
    772         form_button(&f, M_CONT, en ? "Continue saved game" : "Continuar partida guardada")->disabled = !have;
    773         form_button(&f, M_LOCAL, en ? "Local game (people / bots)" : "Partida local (personas / bots)");
    774         form_button(&f, M_CABLE_A, en ? "Cable 1v1: create (A)" : "Cable 1v1: crear (A)");
    775         form_button(&f, M_CABLE_B, en ? "Cable 1v1: join (B)" : "Cable 1v1: unirse (B)");
    776         form_button(&f, M_TEST, en ? "Test: bot games" : "Prueba: partidas de bots");
    777         form_choice(&f, M_LANG, en ? "Language" : "Idioma", LANGS, 2, g_lang);
    778         f.cur = have ? 0 : 1;
    779         int r = form_loop(&f, lang_change, 0, 0);
    780         if (r == M_CONT && load_game()) play();
    781         else if (r == M_LOCAL) new_local();
    782         else if (r == M_CABLE_A) cable_a();
    783         else if (r == M_CABLE_B) cable_b();
    784         else if (r == M_TEST) bot_test_menu();
    785     }
    786 }