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


      1 /* app.c - menus de la app, ajustes y el loop de juego (comun a TUI y GUI). */
      2 #include <ctype.h>
      3 #include <stdio.h>
      4 #include <stdlib.h>
      5 #include <string.h>
      6 #include <time.h>
      7 #include <unistd.h>
      8 #include "app.h"
      9 #include "net.h"
     10 #include "../server/export.h"
     11 #include "paths.h"
     12 #include "version.h"
     13 #ifdef __EMSCRIPTEN__
     14 #include <emscripten.h>
     15 #endif
     16 
     17 /* ------------------------------------------------------------------ ajustes */
     18 
     19 void settings_load(Settings *st)
     20 {
     21     memset(st, 0, sizeof *st);
     22     st->handoff = 1;
     23     st->bot_delay = 450;
     24     st->bot_level = 2;
     25     st->layout = LAYOUT_WIDE;
     26     st->scale = 2;
     27 #ifdef __EMSCRIPTEN__
     28     const char *u = 0;                          /* emscripten inventa USER=web_user */
     29 #else
     30     const char *u = getenv("USER");
     31     if (!u || !*u) u = getenv("USERNAME");     /* Windows */
     32 #endif
     33     snprintf(st->name, sizeof st->name, "%s", u && *u ? u : "Yo");
     34     FILE *f = fopen(config_path(), "r");
     35     if (!f) return;
     36     char line[256];
     37     while (fgets(line, sizeof line, f)) {
     38         line[strcspn(line, "\r\n")] = 0;
     39         char *eq = strchr(line, '=');
     40         if (!eq) continue;
     41         *eq = 0;
     42         const char *k = line, *v = eq + 1;
     43         if (!strcmp(k, "lang")) st->lang = !strcmp(v, "en");
     44         else if (!strcmp(k, "handoff")) st->handoff = atoi(v);
     45         else if (!strcmp(k, "bot_delay")) st->bot_delay = atoi(v);
     46         else if (!strcmp(k, "bot_level")) st->bot_level = atoi(v);
     47         else if (!strcmp(k, "bot_hard")) st->bot_level = atoi(v) ? 2 : 0;    /* config vieja */
     48         else if (!strcmp(k, "layout")) st->layout = !strcmp(v, "compact") ? LAYOUT_COMPACT : LAYOUT_WIDE;
     49         else if (!strcmp(k, "scale")) st->scale = atoi(v);
     50         else if (!strcmp(k, "name")) snprintf(st->name, sizeof st->name, "%s", v);
     51         else if (!strcmp(k, "server")) snprintf(st->server, sizeof st->server, "%s", v);
     52         else if (!strncmp(k, "color", 5) && k[5] >= '1' && k[5] < '1' + MAXP && !k[6]) snprintf(st->color[k[5] - '1'], 8, "%.7s", v);
     53     }
     54     fclose(f);
     55     if (st->scale < 1 || st->scale > 6) st->scale = 2;
     56     if (!strcmp(st->server, "localhost:7373") || !strcmp(st->server, "lu3dhn.xyz:7373"))
     57         st->server[0] = 0;                   /* defaults viejos: ahora es catan.lu3dhn.xyz */
     58 }
     59 
     60 /* "#rrggbb" o "rrggbb" -> color; false si no es un hex valido */
     61 static bool parse_hex(const char *t, Color *out)
     62 {
     63     if (*t == '#') t++;
     64     unsigned v = 0;
     65     for (int i = 0; i < 6; i++) {
     66         char ch = (char)tolower((unsigned char)t[i]);
     67         if (ch >= '0' && ch <= '9') v = v * 16 + (unsigned)(ch - '0');
     68         else if (ch >= 'a' && ch <= 'f') v = v * 16 + (unsigned)(ch - 'a' + 10);
     69         else return false;
     70     }
     71     if (t[6]) return false;
     72     *out = RGB((v >> 16) & 255, (v >> 8) & 255, v & 255);
     73     return true;
     74 }
     75 
     76 static void hex_of(Color c, char out[8])
     77 {
     78     snprintf(out, 8, "#%02x%02x%02x", (c >> 11) << 3, ((c >> 5) & 63) << 2, (c & 31) << 3);
     79 }
     80 
     81 void settings_apply_colors(const Settings *st)
     82 {
     83     for (int i = 0; i < MAXP; i++) {
     84         Color c;
     85         PLAYER_COLOR[i] = st->color[i][0] && parse_hex(st->color[i], &c) ? c : PLAYER_COLOR_DEF[i];
     86     }
     87 }
     88 
     89 void settings_save(const Settings *st)
     90 {
     91     FILE *f = fopen(config_path(), "w");
     92     if (!f) return;
     93     fprintf(f, "lang=%s\nhandoff=%d\nbot_delay=%d\nbot_level=%d\nlayout=%s\nscale=%d\nname=%s\nserver=%s\n",
     94             st->lang ? "en" : "es", st->handoff, st->bot_delay, st->bot_level, st->layout == LAYOUT_COMPACT ? "compact" : "wide",
     95             st->scale, st->name, st->server);
     96     for (int i = 0; i < MAXP; i++) if (st->color[i][0]) fprintf(f, "color%d=%s\n", i + 1, st->color[i]);
     97     fclose(f);
     98     paths_flush();
     99 }
    100 
    101 uint64_t fresh_seed(void)
    102 {
    103     return ((uint64_t)time(0) << 20) ^ (uint64_t)now_ms() ^ ((uint64_t)getpid() << 40);
    104 }
    105 
    106 /* ------------------------------------------------------------ mapas */
    107 
    108 const char *const SCEN_CH_ES[SCEN__COUNT] = { "Clásico", "Navegantes: Nuevas costas", "Navegantes: Cuatro islas",
    109                                               "Navegantes: Archipiélago" };
    110 const char *const SCEN_CH_EN[SCEN__COUNT] = { "Classic", "Seafarers: New shores", "Seafarers: Four islands",
    111                                               "Seafarers: Archipelago" };
    112 
    113 const char *const *map_choices(int family)
    114 {
    115     static char lbl[NMAPS + 1][48];
    116     static const char *ch[NMAPS + 1];
    117     snprintf(lbl[0], sizeof lbl[0], "%s", g_lang ? "Random" : "Al azar");
    118     for (int k = 0; k < NMAPS; k++) snprintf(lbl[k + 1], sizeof lbl[k + 1], "%d · %s", k + 1, board_name(family * NMAPS + k));
    119     for (int k = 0; k <= NMAPS; k++) ch[k] = lbl[k];
    120     return ch;
    121 }
    122 
    123 static const char *family_name(int family)
    124 {
    125     static const char *const ES[BF__COUNT] = { "Clásico", "Tablero 5-6", "Navegantes: Nuevas costas",
    126                                                "Navegantes: Cuatro islas", "Navegantes: Archipiélago" };
    127     static const char *const EN[BF__COUNT] = { "Classic", "5-6 board", "Seafarers: New shores",
    128                                                "Seafarers: Four islands", "Seafarers: Archipelago" };
    129     return (g_lang ? EN : ES)[family < BF__COUNT ? family : 0];
    130 }
    131 
    132 bool map_preview(Frontend *fe, int layout, Opts *o, bool random_map, const char *const names[MAXP])
    133 {
    134     static Game g;
    135     Rng r;
    136     rng_seed(&r, fresh_seed(), 0x3A9);
    137     if (random_map) o->map = (uint8_t)rng_below(&r, NMAPS);
    138     o->bseed = 1 + rng_below(&r, 0x7FFFFFFE);
    139     o->beginner = 0;
    140     int family = opts_family(o);
    141     char title[160];
    142     const char *help[4];
    143     for (;;) {
    144         Rng unused;
    145         rng_seed(&unused, 1, 1);
    146         board_make(&g, o, &unused);
    147         snprintf(title, sizeof title, "%s · %s %d/%d: %s", family_name(family), g_lang ? "map" : "mapa", o->map + 1, NMAPS,
    148                  board_name(opts_board(o)));
    149         help[0] = g_lang ? "This is the board. Before playing you can:" : "Así queda el tablero. Antes de jugar podés:";
    150         help[1] = g_lang ? "  R: reshuffle (same map, other terrains and numbers)" : "  R: otro reparto (mismo mapa, otros terrenos y números)";
    151         help[2] = g_lang ? "  left/right arrows: change map (all play-tested to be fair)" : "  flechas izq/der: cambiar de mapa (todos probados para que sean parejos)";
    152         help[3] = g_lang ? "  Enter: play this one · Esc: back" : "  Enter: jugar este · Esc: volver";
    153         Ctrl c;
    154         ctrl_init(&c, 0, false);
    155         for (int i = 0; i < MAXP; i++) c.names[i] = names[i];
    156         ctrl_sync(&c, &g);
    157         UiInfo ui;
    158         memset(&ui, 0, sizeof ui);
    159         for (int i = 0; i < MAXP; i++) ui.names[i] = names[i];
    160         ui.log = help;
    161         ui.nlog = 4;
    162         int log_max = 0;
    163         ui.log_max = &log_max;
    164         ui.view_seat = -1;
    165         ui.layout = layout;
    166         ui.touch = fe->touch;
    167         ui.preview = title;
    168         fe->draw_game(fe, &g, &c, &ui);
    169         InEvent ev;
    170         if (!fe->wait(fe, 1000, 0, 0, &ev)) continue;
    171         if (ev.type == EV_QUIT) return false;
    172         int k = ev.type == EV_KEY ? ev.key : 0;
    173         if (ev.type == EV_CLICK) {
    174             const Hit *h = hit_find(&fe->hits, ev.x, ev.y);
    175             if (h && h->kind == PK_APP && h->id >= 20 && h->id <= 24) {
    176                 static const int K[5] = { K_LEFT, 'r', K_RIGHT, K_OK, K_BACK };
    177                 k = K[h->id - 20];
    178             } else if (h && h->kind == PK_APP && (h->id == 7 || h->id == 0)) k = K_BACK;
    179         }
    180         if (k == K_OK) return true;
    181         if (k == K_BACK || k == 'q' || k == 'Q') return false;
    182         if (k == 'r' || k == 'R') o->bseed = 1 + rng_below(&r, 0x7FFFFFFE);
    183         if (k == K_LEFT) o->map = (uint8_t)((o->map + NMAPS - 1) % NMAPS);
    184         if (k == K_RIGHT) o->map = (uint8_t)((o->map + 1) % NMAPS);
    185     }
    186 }
    187 
    188 /* ------------------------------------------------------------ formularios */
    189 
    190 int run_form(Frontend *fe, Form *f, void (*on_change)(Form *, void *), void *ctx)
    191 {
    192     if (on_change) on_change(f, ctx);
    193     for (;;) {
    194         fe->draw_form(fe, f);
    195         InEvent ev;
    196         if (!fe->wait(fe, 1000, 0, 0, &ev)) continue;
    197         int r = FORM_NONE;
    198         if (ev.type == EV_QUIT) return FORM_CANCEL;
    199         if (ev.type == EV_KEY) r = form_key(f, ev.key);
    200         else if (ev.type == EV_CLICK) {
    201             const Hit *h = hit_find(&fe->hits, ev.x, ev.y);
    202             if (h && h->kind == PK_APP) {
    203                 r = form_click(f, h->id);
    204                 Field *x = h->id >= 0 && h->id < f->n ? &f->f[h->id] : 0;
    205                 if (r == FORM_NONE && x && x->type == F_TEXT && !x->disabled && fe->text_input) {
    206                     char tmp[sizeof x->text];           /* la web: cuadro de texto del navegador */
    207                     snprintf(tmp, sizeof tmp, "%s", x->text);
    208                     /* el tope en bytes va al cuadro, que corta en borde de caracter utf-8 */
    209                     int cap = x->maxlen && x->maxlen + 1 < (int)sizeof tmp ? x->maxlen + 1 : (int)sizeof tmp;
    210                     if (fe->text_input(fe, x->label, tmp, cap) > 0) snprintf(x->text, sizeof x->text, "%s", tmp);
    211                 }
    212             }
    213         }
    214         if (r != FORM_NONE) return r;
    215         f->msg[0] = 0;
    216         if (on_change) on_change(f, ctx);
    217     }
    218 }
    219 
    220 /* ------------------------------------------------------------ loop de juego */
    221 
    222 /* F5: la partida como texto (cabecera, tablero y el registro tal como lo viste) en
    223  * data_dir()/exportes. La version completa, con todo, la guarda el servidor. */
    224 static void put_file(void *ctx, const char *line) { fputs(line, ctx); fputc('\n', ctx); }
    225 
    226 static bool export_now(Session *s, char *out, int n)
    227 {
    228     char dir[600], path[700], when[32];
    229     snprintf(dir, sizeof dir, "%s/exportes", data_dir());
    230     mkdirs(dir);
    231     time_t now = time(0);
    232     strftime(when, sizeof when, "%Y%m%d-%H%M%S", localtime(&now));
    233     const char *code = s->net ? net_code(s->net) : 0;
    234     snprintf(path, sizeof path, "%s/%s%s%s.txt", dir, code ? code : "local", code ? "-" : "-", when);
    235     FILE *f = fopen(path, "w");
    236     if (!f) return false;
    237     const char *names[MAXP];
    238     char kinds[MAXP + 1] = "";
    239     for (int i = 0; i < MAXP; i++) {
    240         names[i] = s->names[i];
    241         kinds[i] = s->local && s->host.kind[i] == SEAT_BOT ? 'B' : 'P';
    242     }
    243     export_header(s->g, code, names, kinds, put_file, f);
    244     fprintf(f, "\n## %s\n", g_lang ? "Log (as you saw it)" : "Registro (como lo viste)");
    245     for (int i = 0; i < s->nlog; i++) {
    246         const char *t = s->log[i];
    247         if (*t >= '\1' && *t <= '\4' && t[1]) t += 2;      /* sin las marcas de color */
    248         fprintf(f, "%s\n", t);
    249     }
    250     fclose(f);
    251     paths_flush();
    252     snprintf(out, (size_t)n, "%s", path);
    253     return true;
    254 }
    255 
    256 static void submit(Session *s, Ctrl *c, Move *m)
    257 {
    258     int e = sess_submit(s, m);
    259     Move q;
    260     while (ctrl_next(c, &q)) {          /* lo confirmado junto (colocacion: pueblo + ruta) */
    261         if (!e) e = sess_submit(s, &q);
    262     }
    263     if (e) ctrl_error(c, e);
    264 }
    265 
    266 static bool picking(const Ctrl *c)
    267 {
    268     return c->mode == CM_VERT || c->mode == CM_EDGE || c->mode == CM_HEX || c->mode == CM_MENU || c->mode == CM_CARDS;
    269 }
    270 
    271 /* "Jugar otra": las mismas opciones, asientos y nombres de la partida local que
    272  * termino, con otro reparto del tablero (y otros dados). */
    273 void app_play_again(Session *s, Settings *st)
    274 {
    275     Opts o = s->g->o;
    276     uint8_t kinds[MAXP];
    277     char names[MAXP][20];
    278     for (int i = 0; i < MAXP; i++) {
    279         kinds[i] = s->host.kind[i] == SEAT_REMOTE ? SEAT_LOCAL : s->host.kind[i];
    280         snprintf(names[i], sizeof names[i], "%s", s->names[i] ? s->names[i] : "");
    281     }
    282     Rng r;
    283     rng_seed(&r, fresh_seed(), 0x3A9);
    284     if (!o.beginner) o.bseed = 1 + rng_below(&r, 0x7FFFFFFE);     /* otro reparto del mismo mapa */
    285     o.first = OPTS_FIRST_RANDOM;                                   /* y otro que empieza */
    286     sess_close(s);
    287     sess_new_local(s, &o, fresh_seed(), kinds, names);
    288     s->handoff = st->handoff;
    289     sess_set_bots(s, st->bot_level);
    290 }
    291 
    292 int app_play(Frontend *fe, Session *s, Settings *st)
    293 {
    294     Ctrl c;
    295     ctrl_init(&c, s->local_mask, s->handoff);
    296     UiInfo ui;
    297     memset(&ui, 0, sizeof ui);
    298     int modal = 0;
    299     bool help = false;
    300     s->bot_delay_ms = st->bot_delay;
    301     bool typing = false;              /* escribiendo en el chat */
    302     char chat[201] = "";
    303     int log_max = 0;
    304     ui.log_max = &log_max;
    305 
    306     for (;;) {
    307         sess_poll(s);
    308         for (int i = 0; i < MAXP; i++) { c.names[i] = s->names[i]; ui.names[i] = s->names[i]; }
    309         ctrl_sync(&c, s->g);
    310         if (s->err_pending) { ctrl_error(&c, s->last_err); s->err_pending = false; }
    311         ui.nlog = s->nlog;
    312         ui.log = (const char *const *)s->log;
    313         ui.chat_input = chat;              /* la linea de chat siempre se ve */
    314         ui.chat_focus = typing;
    315         ui.status = s->status;
    316         ui.view_seat = c.shown;
    317         ui.layout = st->layout;
    318         ui.modal = modal;
    319         ui.modal_text = g_lang ? "Quit the game? (it is saved)" : "¿Salir de la partida? (queda guardada)";
    320         if (!s->local) ui.modal_text = g_lang ? "Leave the game? (you can rejoin)" : "¿Salir? (podés volver a entrar)";
    321         ui.show_help = help;
    322         ui.touch = fe->touch;
    323         ui.can_free = s->local && !s->net && s->save_path[0] && s->g->winner >= 0;
    324         ui.can_again = s->g->phase == PH_OVER;      /* local: app_play_again; online: AGAIN al servidor */
    325         fe->draw_game(fe, s->g, &c, &ui);
    326 
    327         int fds[16];
    328         int nf = sess_fds(s, fds, 16);
    329         InEvent ev;
    330         if (!fe->wait(fe, sess_timeout(s), fds, nf, &ev)) continue;
    331         if (ev.type == EV_QUIT) return 0;
    332         Move m;
    333         if (ev.type == EV_WHEEL) {
    334             const Hit *h = hit_find(&fe->hits, ev.x, ev.y);
    335             if (!fe->is_gui || (h && h->kind == PK_APP && h->id == 3)) {
    336                 ui.log_scroll += ev.key * 3;
    337                 if (ui.log_scroll > log_max) ui.log_scroll = log_max;
    338                 if (ui.log_scroll < 0) ui.log_scroll = 0;
    339             }
    340             continue;
    341         }
    342         if (ev.type == EV_KEY && (ev.key == K_PGUP || ev.key == K_PGDN || ev.key == K_END)) {
    343             int k = ev.key;                 /* el registro se mueve tambien escribiendo */
    344             ui.log_scroll = k == K_END ? 0 : ui.log_scroll + (k == K_PGUP ? 8 : -8);
    345             if (ui.log_scroll > log_max) ui.log_scroll = log_max;
    346             if (ui.log_scroll < 0) ui.log_scroll = 0;
    347             continue;
    348         }
    349         if (ev.type == EV_CLICK && !modal && !help) {           /* botones de la pantalla tactil: valen como teclas */
    350             const Hit *h = hit_find(&fe->hits, ev.x, ev.y);
    351             if (h && h->kind == PK_APP) {
    352                 int k = h->id == 7 ? K_BACK : h->id == 9 ? K_OK : h->id == 10 ? 'c' : h->id == 14 ? 'a' : 0;
    353                 if (k) { ev.type = EV_KEY; ev.key = k; typing = false; }
    354             }
    355         }
    356         if (ev.type == EV_KEY && ev.key == K_F1 + 4 && !modal) {      /* F5: exportar */
    357             char path[700];
    358             bool ok = export_now(s, path, sizeof path);
    359             snprintf(c.msg, sizeof c.msg, "%s %s", ok ? (g_lang ? "Exported:" : "Exportada:") : (g_lang ? "Could not export" : "No se pudo exportar"),
    360                      ok ? path : "");
    361             c.msg_err = !ok;
    362             continue;
    363         }
    364         if (ev.type == EV_KEY && ev.key == K_F1 + 1 && !modal) {      /* F2: chat */
    365             typing = !typing;
    366             help = false;
    367             continue;
    368         }
    369         if (ev.type == EV_KEY && typing) {
    370             int k = ev.key;
    371             size_t len = strlen(chat);
    372             c.msg[0] = 0;
    373             if (k == K_BACK) typing = false;          /* lo escrito queda en la caja */
    374             else if (k == K_OK) {
    375                 const char *err = sess_say(s, chat);
    376                 if (err) { snprintf(c.msg, sizeof c.msg, "%s", err); c.msg_err = true; }
    377                 else { typing = false; chat[0] = 0; ui.log_scroll = 0; }
    378             } else if (k == K_BKSP) {
    379                 while (len > 0 && ((unsigned char)chat[len - 1] & 0xC0) == 0x80) len--;   /* utf-8 */
    380                 if (len > 0) len--;
    381                 chat[len] = 0;
    382             } else if (k >= 32 && k < 256 && k != 127 && len + 2 < sizeof chat) {
    383                 if (k < 128) chat[len++] = (char)k;
    384                 else { chat[len++] = (char)(0xC0 | (k >> 6)); chat[len++] = (char)(0x80 | (k & 0x3F)); }
    385                 chat[len] = 0;
    386             }
    387             continue;
    388         }
    389         if (ev.type == EV_KEY) {
    390             int k = ev.key;
    391             if (modal) {
    392                 if (modal == 1 && (k == 's' || k == 'S' || k == 'y' || k == 'Y' || k == K_OK)) return 0;
    393                 modal = 0;
    394                 continue;
    395             }
    396             if (help) { help = false; continue; }
    397             if (k == 'q' || k == 'Q') {
    398                 if (s->g->phase == PH_OVER) return 0;
    399                 modal = 1;
    400                 continue;
    401             }
    402             if (s->g->phase == PH_OVER && (k == 'c' || k == 'C') && ui.can_free) {
    403                 char err[160];
    404                 if (!sess_go_free(s, err, sizeof err)) { sess_add_log(s, err); continue; }
    405                 sess_add_log(s, g_lang ? "— free play: keep going —" : "— juego libre: a seguir —");
    406                 ctrl_init(&c, s->local_mask, s->handoff);
    407                 continue;
    408             }
    409             if (s->g->phase == PH_OVER && (k == 'a' || k == 'A') && ui.can_again) return 1;   /* jugar otra */
    410             if (s->g->phase == PH_OVER && (k == K_OK || k == K_BACK)) return 0;
    411             if (k == '?') { help = true; continue; }
    412 
    413             if (k == 'L') {
    414                 g_lang ^= 1;
    415                 st->lang = g_lang;
    416                 settings_save(st);
    417                 c.ctx_sig = 0xFFFFFFFFu;
    418                 continue;
    419             }
    420             if (k == K_TAB && c.mode == CM_IDLE) { ui.show_info = !ui.show_info; continue; }
    421             if (picking(&c)) {
    422                 if (k == 'h') k = K_LEFT;
    423                 else if (k == 'l') k = K_RIGHT;
    424                 else if (k == 'j') k = K_DOWN;
    425                 else if (k == 'k') k = K_UP;
    426             }
    427             if (ctrl_key(&c, k, &m)) submit(s, &c, &m);
    428         } else if (ev.type == EV_CLICK) {
    429             if (modal) {
    430                 const Hit *h = hit_find(&fe->hits, ev.x, ev.y);
    431                 int id = h && h->kind == PK_APP ? h->id : -1;
    432                 if (modal == 1 && id == 8) return 0;                     /* "si" del dialogo de salir */
    433                 if (modal == 2 && id == 1) { modal = 0; help = true; continue; }   /* menu de la app (tactil) */
    434                 if (modal == 2 && id == 0) { modal = 1; continue; }
    435                 modal = 0;
    436                 continue;
    437             }
    438             if (help) { help = false; continue; }
    439             const Hit *h = hit_find(&fe->hits, ev.x, ev.y);
    440             bool app = h && h->kind == PK_APP;
    441             if (app && h->id == 6) {                                      /* caja de chat */
    442                 char tmp[sizeof chat];
    443                 snprintf(tmp, sizeof tmp, "%s", chat);
    444                 int ti = fe->text_input ? fe->text_input(fe, "Chat", tmp, sizeof tmp) : -1;   /* la web: cuadro propio */
    445                 if (ti > 0) {
    446                     c.msg[0] = 0;
    447                     const char *err = tmp[0] ? sess_say(s, tmp) : 0;
    448                     if (err) { snprintf(c.msg, sizeof c.msg, "%s", err); c.msg_err = true; }
    449                     else { chat[0] = 0; ui.log_scroll = 0; }
    450                 } else if (ti < 0) typing = true;       /* sin cuadro: se escribe con el teclado; cancelado: nada */
    451                 continue;
    452             }
    453             if (app && (h->id == 4 || h->id == 5)) {                      /* barra de scroll */
    454                 ui.log_scroll += h->id == 4 ? 8 : -8;
    455                 if (ui.log_scroll > log_max) ui.log_scroll = log_max;
    456                 if (ui.log_scroll < 0) ui.log_scroll = 0;
    457                 continue;
    458             }
    459             typing = false;                 /* click en otro lado: el teclado vuelve al juego */
    460             if (!h) continue;
    461             if (h->kind == PK_BOARD) {
    462                 int px = ev.x - fe->board_ox;
    463                 int py = (ev.y - fe->board_oy) * fe->board_sy + fe->board_sy / 2;
    464                 int id = gfx_board_hit(&fe->geom, &c, px, py);
    465                 if (id < 0) continue;
    466                 int kind = c.mode == CM_VERT ? PK_VERT : c.mode == CM_EDGE ? PK_EDGE : PK_HEX;
    467                 if (ctrl_pick(&c, kind, id, &m)) submit(s, &c, &m);
    468             } else if (h->kind == PK_APP) {
    469                 if (h->id == 0) modal = 1;
    470                 else if (h->id == 1) help = true;
    471                 else if (h->id == 2) ui.show_info = !ui.show_info;
    472                 else if (h->id == 12) modal = 2;                             /* "menu" de la barra (tactil) */
    473             } else if (ctrl_pick(&c, h->kind, h->id, &m)) {
    474                 submit(s, &c, &m);
    475             }
    476         }
    477     }
    478 }
    479 
    480 /* ------------------------------------------------------------ nueva partida */
    481 
    482 enum { B_BACK = 0, B_START = 1, F_PLAYERS = 10, F_VARIANT, F_BOARD, F_HANDOFF, F_FIRST, F_FREE, F_LEVEL, F_SCEN, F_MAP,
    483        F_KIND = 20, F_NAME = 30 };
    484 
    485 static const char *CH_NUM[] = { "2", "3", "4", "5", "6" };   /* 5-6: tablero grande, en pareja */
    486 static const char *CH_YESNO_ES[] = { "No", "Sí" }, *CH_YESNO_EN[] = { "No", "Yes" };
    487 static const char *CH_KIND_ES[] = { "Humano", "Bot" }, *CH_KIND_EN[] = { "Human", "Bot" };
    488 static const char *CH_LEVEL_ES[] = { "Fáciles", "Medios", "Difíciles" }, *CH_LEVEL_EN[] = { "Easy", "Medium", "Hard" };
    489 static const char *CH_BOARD_ES[] = { "Aleatorio", "Principiante" }, *CH_BOARD_EN[] = { "Random", "Beginner" };
    490 static const char *CH_FREE_ES[] = { "A 10 puntos", "Libre (seguir al ganar)" }, *CH_FREE_EN[] = { "First to 10", "Free play (keep going)" };
    491 static const char *CH_FIRST_ES[] = { "Por turnos", "Humanos primero" }, *CH_FIRST_EN[] = { "Snake order", "Humans first" };
    492 
    493 static void newgame_change(Form *f, void *ctx)
    494 {
    495     (void)ctx;
    496     int players = form_get(f, F_PLAYERS)->value + 2;
    497     Field *var = form_get(f, F_VARIANT);
    498     var->hidden = players != 2;
    499     bool human = false, bot = false;
    500     for (int i = 0; i < MAXP; i++) {
    501         bool b = form_get(f, F_KIND + i)->value == 1;
    502         if (i < players) { human |= !b; bot |= b; }
    503         form_get(f, F_KIND + i)->hidden = i >= players;
    504         form_get(f, F_NAME + i)->hidden = i >= players || b;   /* los bots se llaman "Bot N" */
    505         if (form_get(f, F_KIND + i)->value == 1 && !form_get(f, F_NAME + i)->text[0])
    506             snprintf(form_get(f, F_NAME + i)->text, 20, "Bot %d", i + 1);
    507     }
    508     form_get(f, F_FIRST)->hidden = !(human && bot);   /* solo tiene sentido contra bots */
    509     Field *scen = form_get(f, F_SCEN);                  /* Navegantes: 3-4 jugadores, sin la variante */
    510     scen->hidden = players < 3 || players > 4;
    511     if (scen->hidden) scen->value = 0;
    512     form_get(f, F_BOARD)->hidden = players > 4 || scen->value;   /* el de principiante es solo el clasico */
    513     Opts o;                                             /* el campo de mapa, con los de esa familia */
    514     opts_default(&o, players, false);
    515     opts_scenario(&o, scen->value);
    516     Field *map = form_get(f, F_MAP);
    517     map->choices = map_choices(opts_family(&o));
    518     map->hidden = !form_get(f, F_BOARD)->hidden && form_get(f, F_BOARD)->value == 1;   /* principiante: fijo */
    519     form_get(f, F_LEVEL)->hidden = !bot;
    520 }
    521 
    522 static bool new_game(Frontend *fe, Settings *st, Session *s)
    523 {
    524     Form f;
    525     bool en = g_lang;
    526     form_begin(&f, en ? "New local game" : "Nueva partida local");
    527     form_choice(&f, F_PLAYERS, en ? "Players" : "Jugadores", CH_NUM, MAXP - 1, 2);
    528     form_choice(&f, F_VARIANT, en ? "2-player variant" : "Variante para 2", en ? CH_YESNO_EN : CH_YESNO_ES, 2, 1);
    529     for (int i = 0; i < MAXP; i++) {
    530         char lbl[32];
    531         snprintf(lbl, sizeof lbl, "%s %d", en ? "Seat" : "Asiento", i + 1);
    532         form_choice(&f, F_KIND + i, lbl, en ? CH_KIND_EN : CH_KIND_ES, 2, i == 0 ? 0 : 1);
    533         char nm[20];
    534         if (i == 0) snprintf(nm, sizeof nm, "%s", st->name);
    535         else snprintf(nm, sizeof nm, "Bot %d", i + 1);
    536         form_text(&f, F_NAME + i, en ? "  name" : "  nombre", nm, 16);
    537     }
    538     form_choice(&f, F_LEVEL, "Bots", en ? CH_LEVEL_EN : CH_LEVEL_ES, 3, st->bot_level < 0 ? 0 : st->bot_level > 2 ? 2 : st->bot_level);
    539     form_choice(&f, F_SCEN, en ? "Scenario" : "Escenario", en ? SCEN_CH_EN : SCEN_CH_ES, SCEN__COUNT, 0);
    540     form_choice(&f, F_BOARD, en ? "Board" : "Tablero", en ? CH_BOARD_EN : CH_BOARD_ES, 2, 0);
    541     form_choice(&f, F_MAP, en ? "Map" : "Mapa", map_choices(BF_CLASSIC), NMAPS + 1, 1);
    542     form_choice(&f, F_FIRST, en ? "Initial placement" : "Colocación inicial", en ? CH_FIRST_EN : CH_FIRST_ES, 2, 1);
    543     form_choice(&f, F_FREE, en ? "End" : "Fin", en ? CH_FREE_EN : CH_FREE_ES, 2, 0);
    544     form_choice(&f, F_HANDOFF, en ? "Hand-off screen" : "Pantalla de pase", en ? CH_YESNO_EN : CH_YESNO_ES, 2, st->handoff);
    545     form_button(&f, B_START, en ? "Start" : "Empezar");
    546     form_button(&f, B_BACK, en ? "Back" : "Volver");
    547     f.cur = f.n - 2;
    548 
    549     for (;;) {
    550         int r = run_form(fe, &f, newgame_change, 0);
    551         if (r != B_START) return false;
    552         int players = form_get(&f, F_PLAYERS)->value + 2;
    553         bool v2p = players == 2 && form_get(&f, F_VARIANT)->value;
    554         Opts o;
    555         opts_default(&o, players, v2p);
    556         o.beginner = (uint8_t)(!form_get(&f, F_BOARD)->hidden && form_get(&f, F_BOARD)->value);
    557         o.freeplay = (uint8_t)form_get(&f, F_FREE)->value;
    558         if (!form_get(&f, F_SCEN)->hidden && form_get(&f, F_SCEN)->value) opts_scenario(&o, form_get(&f, F_SCEN)->value);
    559         uint8_t kinds[MAXP] = { 0 };
    560         char names[MAXP][20];
    561         memset(names, 0, sizeof names);
    562         for (int i = 0; i < players; i++) {
    563             kinds[i] = form_get(&f, F_KIND + i)->value ? SEAT_BOT : SEAT_LOCAL;
    564             snprintf(names[i], sizeof names[i], "%s", form_get(&f, F_NAME + i)->text);
    565         }
    566         if (v2p) { kinds[2] = kinds[3] = SEAT_NEUTRAL; }
    567         if (!o.beginner) {                   /* mirar el tablero, cambiarlo si no gusta */
    568             int mv = form_get(&f, F_MAP)->value;
    569             o.map = (uint8_t)(mv > 0 ? mv - 1 : 0);
    570             const char *nm[MAXP];
    571             for (int i = 0; i < MAXP; i++) nm[i] = names[i];
    572             if (!map_preview(fe, st->layout, &o, mv == 0, nm)) continue;
    573         }
    574         /* humanos primero: ponen sus dos pueblos y despues eligen los bots */
    575         if (!form_get(&f, F_FIRST)->hidden && form_get(&f, F_FIRST)->value)
    576             for (int i = 0; i < players; i++)
    577                 if (kinds[i] == SEAT_LOCAL) o.setup_first |= (uint8_t)(1 << i);
    578         if (kinds[0] == SEAT_LOCAL && names[0][0]) { snprintf(st->name, sizeof st->name, "%s", names[0]); }
    579         st->handoff = form_get(&f, F_HANDOFF)->value;
    580         st->bot_level = form_get(&f, F_LEVEL)->value;
    581         settings_save(st);
    582         o.first = OPTS_FIRST_RANDOM;         /* quien empieza se sortea */
    583         sess_new_local(s, &o, fresh_seed(), kinds, names);
    584         s->handoff = st->handoff;
    585         sess_set_bots(s, st->bot_level);
    586         return true;
    587     }
    588 }
    589 
    590 /* ------------------------------------------------------------ menu principal */
    591 
    592 enum { M_QUIT = 0, M_NEW = 1, M_CONTINUE, M_ONLINE, M_LAN_HOST, M_LAN_JOIN, M_COLORS, M_DL_WIN, M_DL_LINUX, M_HOWTO,
    593        M_LANG = 50, M_NAME, M_SERVER };
    594 
    595 /* ------------------------------------------------------------ colores */
    596 
    597 enum { K_BACKC = 0, K_SAVE = 1, K_RESET = 2, K_COL = 10 };
    598 
    599 static void colors_form(Frontend *fe, Settings *st)
    600 {
    601     static const char *NAME_ES[] = { "rojo", "azul", "verde", "naranja", "blanco", "marrón" },
    602                       *NAME_EN[] = { "red", "blue", "green", "orange", "white", "brown" };
    603     bool en = g_lang;
    604     Form f;
    605     form_begin(&f, en ? "Player colors" : "Colores de los jugadores");
    606     for (int i = 0; i < MAXP; i++) {
    607         char lbl[48], cur[8];
    608         snprintf(lbl, sizeof lbl, "%s %d (%s)", en ? "Seat" : "Asiento", i + 1, en ? NAME_EN[i] : NAME_ES[i]);
    609         hex_of(PLAYER_COLOR[i], cur);
    610         form_text(&f, K_COL + i, lbl, cur, 7);
    611     }
    612     form_label(&f, en ? "Hex code like #d62828. Only on this computer." : "Código hex, por ejemplo #d62828. Vale solo en esta compu.");
    613     form_button(&f, K_SAVE, en ? "Save" : "Guardar");
    614     form_button(&f, K_RESET, en ? "Back to Catan's colors" : "Volver a los colores de Catan");
    615     form_button(&f, K_BACKC, en ? "Back" : "Volver");
    616     for (;;) {
    617         int r = run_form(fe, &f, 0, 0);
    618         if (r == K_RESET) {
    619             for (int i = 0; i < MAXP; i++) {
    620                 st->color[i][0] = 0;
    621                 hex_of(PLAYER_COLOR_DEF[i], form_get(&f, K_COL + i)->text);
    622             }
    623             settings_save(st);
    624             settings_apply_colors(st);
    625             form_msg(&f, en ? "Original colors restored" : "Volvieron los colores originales", false);
    626             continue;
    627         }
    628         if (r != K_SAVE) return;
    629         bool ok = true;
    630         for (int i = 0; i < MAXP && ok; i++) {
    631             Color c;
    632             if (!parse_hex(form_get(&f, K_COL + i)->text, &c)) {
    633                 char m[96];
    634                 snprintf(m, sizeof m, en ? "Seat %d: not a hex color (#rrggbb)" : "Asiento %d: no es un color hex (#rrggbb)", i + 1);
    635                 form_msg(&f, m, true);
    636                 ok = false;
    637             }
    638         }
    639         if (!ok) continue;
    640         for (int i = 0; i < MAXP; i++) {
    641             Color c;
    642             parse_hex(form_get(&f, K_COL + i)->text, &c);
    643             if (c == PLAYER_COLOR_DEF[i]) st->color[i][0] = 0;
    644             else hex_of(c, st->color[i]);
    645         }
    646         settings_save(st);
    647         settings_apply_colors(st);
    648         return;
    649     }
    650 }
    651 
    652 static void menu_change(Form *f, void *ctx)
    653 {
    654     Settings *st = ctx;
    655     int lang = form_get(f, M_LANG)->value;
    656     if (lang != g_lang) {
    657         g_lang = lang;
    658         st->lang = lang;
    659         settings_save(st);
    660     }
    661 }
    662 
    663 static bool lan_host_form(Frontend *fe, Settings *st)
    664 {
    665     Form f;
    666     bool en = g_lang;
    667     form_begin(&f, en ? "Host a LAN game" : "Crear partida en red local");
    668     form_text(&f, 1, en ? "Port" : "Puerto", "7373", 6);
    669     form_label(&f, en ? "Others join with: catan join <your-ip> <port>" : "Los demás entran con: catan join <tu-ip> <puerto>");
    670     form_button(&f, 10, en ? "Create" : "Crear");
    671     form_button(&f, 0, en ? "Back" : "Volver");
    672     if (run_form(fe, &f, 0, 0) != 10) return false;
    673     return lan_host(fe, st, atoi(form_get(&f, 1)->text));
    674 }
    675 
    676 static bool lan_join_form(Frontend *fe, Settings *st)
    677 {
    678     Form f;
    679     bool en = g_lang;
    680     form_begin(&f, en ? "Join a LAN game" : "Unirse a partida en red local");
    681     form_text(&f, 1, en ? "Host" : "Host / IP", "", 60);
    682     form_text(&f, 2, en ? "Port" : "Puerto", "7373", 6);
    683     form_button(&f, 10, en ? "Join" : "Unirse");
    684     form_button(&f, 0, en ? "Back" : "Volver");
    685     if (run_form(fe, &f, 0, 0) != 10) return false;
    686     return lan_join(fe, st, form_get(&f, 1)->text, atoi(form_get(&f, 2)->text));
    687 }
    688 
    689 #ifdef __EMSCRIPTEN__
    690 /* el "leeme" de las descargas */
    691 static void howto_form(Frontend *fe)
    692 {
    693     Form f;
    694     bool en = g_lang;
    695     char t[64];
    696     snprintf(t, sizeof t, "%s %s", en ? "Catan for your computer, version" : "Catan para la compu, versión", APP_VERSION);
    697     form_begin(&f, t);
    698     if (en) {
    699         form_label(&f, "Windows: download catan.exe and double-click it.");
    700         form_label(&f, "If Windows warns: More info > Run anyway.");
    701         form_label(&f, "Linux: download catan-linux, then in a terminal:");
    702         form_label(&f, "  chmod +x catan-linux");
    703         form_label(&f, "  ./catan-linux --gui   (window; without --gui: terminal)");
    704         form_label(&f, "Online uses the same server as this page:");
    705         form_label(&f, "web, Windows and Linux play together. Use the");
    706         form_label(&f, "same version (shown in the main menu title).");
    707     } else {
    708         form_label(&f, "Windows: bajá catan.exe y abrilo con doble click.");
    709         form_label(&f, "Si Windows avisa: Más información > Ejecutar.");
    710         form_label(&f, "Linux: bajá catan-linux y en una terminal:");
    711         form_label(&f, "  chmod +x catan-linux");
    712         form_label(&f, "  ./catan-linux --gui   (ventana; sin --gui: terminal)");
    713         form_label(&f, "Online es el mismo servidor que esta página:");
    714         form_label(&f, "web, Windows y Linux juegan juntos. Conviene");
    715         form_label(&f, "la misma versión (está en el título del menú).");
    716     }
    717     form_button(&f, 0, en ? "Back" : "Volver");
    718     run_form(fe, &f, 0, 0);
    719 }
    720 #endif
    721 
    722 void app_menu(Frontend *fe, Settings *st)
    723 {
    724     static const char *LANGS[] = { "Español", "English" };
    725     for (;;) {
    726         Form f;
    727         bool en = g_lang;
    728         char last[512];
    729         bool have_save = latest_save(last, sizeof last);
    730         char title[64];
    731         snprintf(title, sizeof title, "%s · %s %s", en ? "Main menu" : "Menú principal", en ? "version" : "versión", APP_VERSION);
    732         form_begin(&f, title);
    733         form_button(&f, M_NEW, en ? "New local game (hotseat / bots)" : "Nueva partida local (misma compu / bots)");
    734         form_button(&f, M_CONTINUE, en ? "Continue last game" : "Continuar la última partida")->disabled = !have_save;
    735         form_button(&f, M_ONLINE, en ? "Play online (server)" : "Jugar online (servidor)");
    736 #ifndef __EMSCRIPTEN__                   /* la web no abre sockets de red local */
    737         form_button(&f, M_LAN_HOST, en ? "LAN: host a game" : "Red local: crear partida");
    738         form_button(&f, M_LAN_JOIN, en ? "LAN: join a game" : "Red local: unirse");
    739 #endif
    740         form_button(&f, M_COLORS, en ? "Player colors" : "Colores de los jugadores");
    741         form_choice(&f, M_LANG, en ? "Language" : "Idioma", LANGS, 2, g_lang);
    742 #ifndef __EMSCRIPTEN__                   /* en la web "salir" es cerrar la pestana */
    743         form_button(&f, M_QUIT, en ? "Quit" : "Salir");
    744 #else                                    /* la web ofrece bajar el juego para la compu */
    745         form_button(&f, M_DL_WIN, en ? "Download for Windows (catan.exe)" : "Descargar para Windows (catan.exe)");
    746         form_button(&f, M_DL_LINUX, en ? "Download for Linux" : "Descargar para Linux");
    747         form_button(&f, M_HOWTO, en ? "How to install (read me)" : "Cómo instalarlo (leeme)");
    748 #endif
    749         int r = run_form(fe, &f, menu_change, st);
    750         static Session s;
    751         switch (r) {
    752         case M_NEW:
    753             if (new_game(fe, st, &s)) {
    754                 while (app_play(fe, &s, st) == 1) app_play_again(&s, st);
    755                 sess_close(&s);
    756             }
    757             break;
    758         case M_CONTINUE: {
    759             char err[128] = "";
    760             if (have_save && sess_resume(&s, last, err, sizeof err)) {
    761                 s.handoff = st->handoff;
    762                 sess_set_bots(&s, st->bot_level);
    763                 while (app_play(fe, &s, st) == 1) app_play_again(&s, st);
    764                 sess_close(&s);
    765             }
    766             break;
    767         }
    768         case M_ONLINE: online_menu(fe, st, st->server); break;
    769         case M_COLORS: colors_form(fe, st); break;
    770         case M_LAN_HOST: lan_host_form(fe, st); break;
    771         case M_LAN_JOIN: lan_join_form(fe, st); break;
    772         case M_QUIT: return;
    773 #ifdef __EMSCRIPTEN__
    774         case M_DL_WIN: emscripten_run_script("window.open('descargas/catan.exe', '_blank')"); break;
    775         case M_DL_LINUX: emscripten_run_script("window.open('descargas/catan-linux', '_blank')"); break;
    776         case M_HOWTO: howto_form(fe); break;
    777 #endif
    778         case FORM_CANCEL:
    779 #ifndef __EMSCRIPTEN__
    780             return;
    781 #endif
    782             break;
    783         }
    784     }
    785 }