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


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