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


      1 /* app.c - Palabra: el Wordle en castellano para la PicoCalc.
      2  *
      3  * Adivinar una palabra de 5 letras en 6 intentos. Letras del teclado, ';' = Ñ,
      4  * Retroceso borra, Enter prueba, Esc pausa. Las tildes no cuentan (se muestran al
      5  * final). En modo difícil las pistas reveladas se tienen que usar.
      6  * La partida en curso se guarda despues de cada intento ("Continuar"). */
      7 #include <string.h>
      8 #include "mini.h"
      9 #include "palabras.h"
     10 
     11 /* 1: muestra la solucion en la barra y la fija en PALABRA_WORD (para probar). */
     12 #ifndef PALABRA_DEBUG
     13 #define PALABRA_DEBUG 0
     14 #endif
     15 #ifndef PALABRA_WORD
     16 #define PALABRA_WORD "perro"
     17 #endif
     18 
     19 enum { ROWS = 6, LEN = 5, NLET = 27, ENE = 26 };
     20 enum { PLAYING, WON, LOST };
     21 enum { UNK, ABSENT, PRESENT, CORRECT };             /* estado de letra / casilla */
     22 
     23 /* tablero y teclado en pantalla */
     24 enum { CELL = 30, GAP = 3, GX = (320 - (LEN * CELL + (LEN - 1) * GAP)) / 2, GY = 30,
     25        KW = 28, KH = 19, KGAP = 3, KY = 243 };
     26 enum { STEP_MS = 180, SHAKE_MS = 40, MSG_MS = 1800 };
     27 
     28 typedef struct {
     29     int sol;                      /* indice en SOLUCIONES */
     30     uint8_t ans[LEN];
     31     bool hard;
     32     uint8_t guess[ROWS][LEN], res[ROWS][LEN];
     33     int rows;                     /* intentos hechos */
     34     uint8_t cur[LEN];
     35     int ncur;
     36     uint8_t keys[NLET];           /* mejor estado conocido de cada letra */
     37     int state;
     38     char msg[48];
     39     uint32_t msg_t, shake_t, reveal_t;
     40     bool shaking, revealing;
     41     bool card;                    /* cartel de fin a la vista (Tab lo esconde) */
     42 } Game;
     43 
     44 static Game G;
     45 
     46 /* ------------------------------------------------------------ palabras */
     47 
     48 static void unpack(uint32_t w, uint8_t *l)
     49 {
     50     for (int i = LEN - 1; i >= 0; i--) { l[i] = (uint8_t)(w & 31); w >>= 5; }
     51 }
     52 
     53 static uint32_t pack(const uint8_t *l)
     54 {
     55     uint32_t w = 0;
     56     for (int i = 0; i < LEN; i++) w = w << 5 | l[i];
     57     return w;
     58 }
     59 
     60 static bool is_valid(const uint8_t *l)
     61 {
     62     uint32_t w = pack(l);
     63     int lo = 0, hi = NVALIDAS - 1;
     64     while (lo <= hi) {
     65         int m = (lo + hi) / 2;
     66         if (VALIDAS[m] == w) return true;
     67         if (VALIDAS[m] < w) lo = m + 1; else hi = m - 1;
     68     }
     69     return false;
     70 }
     71 
     72 /* Letra en UTF-8 (mayuscula). */
     73 static int put_letter(char *b, int n, int p, int l)
     74 {
     75     char t[3];
     76     if (l == ENE) { t[0] = (char)0xC3; t[1] = (char)0x91; t[2] = 0; }
     77     else { t[0] = (char)('A' + l); t[1] = 0; }
     78     return str_put(b, n, p, t);
     79 }
     80 
     81 /* La solucion con su tilde, en mayusculas. */
     82 static int put_solution(char *b, int n, int p)
     83 {
     84     uint32_t w = SOLUCIONES[G.sol];
     85     int acc = (int)(w >> 25 & 7), dier = (int)(w >> 28 & 1);
     86     for (int i = 0; i < LEN; i++) {
     87         int l = G.ans[i];
     88         if (acc == i + 1 && l < 26) {
     89             static const char V[] = "AEIOU";
     90             static const uint8_t TILDE[] = { 0xC1, 0xC9, 0xCD, 0xD3, 0xDA };   /* Latin-1 */
     91             const char *v = strchr(V, 'A' + l);
     92             if (v) {
     93                 int c = dier ? 0xDC : TILDE[v - V];                          /* Ü */
     94                 char t[3] = { (char)0xC3, (char)(c - 0x40), 0 };
     95                 p = str_put(b, n, p, t);
     96                 continue;
     97             }
     98         }
     99         p = put_letter(b, n, p, l);
    100     }
    101     return p;
    102 }
    103 
    104 /* Colores con la regla de las letras repetidas: primero las exactas, despues las
    105  * que estan en otro lado mientras queden sin usar en la solucion. */
    106 static void score(const uint8_t *g, uint8_t *r)
    107 {
    108     int left[NLET] = { 0 };
    109     for (int i = 0; i < LEN; i++) {
    110         if (g[i] == G.ans[i]) r[i] = CORRECT;
    111         else { r[i] = ABSENT; left[G.ans[i]]++; }
    112     }
    113     for (int i = 0; i < LEN; i++)
    114         if (r[i] != CORRECT && left[g[i]] > 0) { r[i] = PRESENT; left[g[i]]--; }
    115 }
    116 
    117 /* Modo dificil: los verdes en su lugar y las letras reveladas, al menos tantas veces. */
    118 static bool hard_ok(const uint8_t *g, char *why, int n)
    119 {
    120     for (int r = 0; r < G.rows; r++) {
    121         for (int i = 0; i < LEN; i++)
    122             if (G.res[r][i] == CORRECT && g[i] != G.guess[r][i]) {
    123                 int p = str_put(why, n, 0, "La ");
    124                 p = str_int(why, n, p, i + 1);
    125                 p = str_put(why, n, p, "ª letra tiene que ser ");
    126                 put_letter(why, n, p, G.guess[r][i]);
    127                 return false;
    128             }
    129         for (int i = 0; i < LEN; i++) {
    130             if (G.res[r][i] != PRESENT) continue;
    131             int l = G.guess[r][i], need = 0, have = 0;
    132             for (int j = 0; j < LEN; j++) {
    133                 if (G.guess[r][j] == l && G.res[r][j] != ABSENT) need++;
    134                 if (g[j] == l) have++;
    135             }
    136             if (have < need) {
    137                 int p = str_put(why, n, 0, "Tiene que tener la ");
    138                 put_letter(why, n, p, l);
    139                 return false;
    140             }
    141         }
    142     }
    143     return true;
    144 }
    145 
    146 static void update_keys(int r)
    147 {
    148     for (int i = 0; i < LEN; i++) {
    149         int l = G.guess[r][i];
    150         if (G.res[r][i] > G.keys[l]) G.keys[l] = G.res[r][i];
    151     }
    152 }
    153 
    154 /* ------------------------------------------------------------ partida y guardado */
    155 
    156 static void new_game(bool hard)
    157 {
    158     memset(&G, 0, sizeof G);
    159     G.hard = hard;
    160     G.sol = mini_rand(NSOLUCIONES);
    161 #if PALABRA_DEBUG
    162     uint8_t dw[LEN];
    163     for (int i = 0; i < LEN; i++) dw[i] = (uint8_t)(PALABRA_WORD[i] - 'a');
    164     for (int i = 0; i < NSOLUCIONES; i++)
    165         if ((SOLUCIONES[i] & PAL_MASK) == pack(dw)) G.sol = i;
    166 #endif
    167     unpack(SOLUCIONES[G.sol] & PAL_MASK, G.ans);
    168 }
    169 
    170 /* "p": modo (0/1), indice de la solucion, '.', los intentos (a..z, Ñ = '_'). */
    171 static void save_progress(void)
    172 {
    173     char b[64];
    174     int p = str_put(b, sizeof b, 0, G.hard ? "1" : "0");
    175     p = str_int(b, sizeof b, p, G.sol);
    176     p = str_put(b, sizeof b, p, ".");
    177     for (int r = 0; r < G.rows; r++)
    178         for (int i = 0; i < LEN; i++) {
    179             char t[2] = { G.guess[r][i] == ENE ? '_' : (char)('a' + G.guess[r][i]), 0 };
    180             p = str_put(b, sizeof b, p, t);
    181         }
    182     mini_set_str("p", b);
    183     mini_save();
    184 }
    185 
    186 static bool has_progress(void)
    187 {
    188     const char *s = mini_get_str("p");
    189     return s && s[0];
    190 }
    191 
    192 static bool load_progress(void)
    193 {
    194     const char *s = mini_get_str("p");
    195     if (!s || (s[0] != '0' && s[0] != '1')) return false;
    196     long idx = 0;
    197     int i = 1;
    198     while (s[i] >= '0' && s[i] <= '9') idx = idx * 10 + (s[i++] - '0');
    199     if (s[i] != '.' || idx >= NSOLUCIONES) return false;
    200     memset(&G, 0, sizeof G);
    201     G.hard = s[0] == '1';
    202     G.sol = (int)idx;
    203     unpack(SOLUCIONES[G.sol] & PAL_MASK, G.ans);
    204     const char *g = s + i + 1;
    205     while (G.rows < ROWS - 1 && strlen(g) >= LEN) {
    206         for (int k = 0; k < LEN; k++) {
    207             if (g[k] == '_') G.guess[G.rows][k] = ENE;
    208             else if (g[k] >= 'a' && g[k] <= 'z') G.guess[G.rows][k] = (uint8_t)(g[k] - 'a');
    209             else return false;
    210         }
    211         score(G.guess[G.rows], G.res[G.rows]);
    212         update_keys(G.rows);
    213         G.rows++;
    214         g += LEN;
    215     }
    216     return true;
    217 }
    218 
    219 static void add_stat(const char *k, long d) { mini_set(k, mini_get(k, 0) + d); }
    220 
    221 static void record(bool won)
    222 {
    223     add_stat("jug", 1);
    224     if (won) {
    225         add_stat("gan", 1);
    226         add_stat("rac", 1);
    227         if (mini_get("rac", 0) > mini_get("max", 0)) mini_set("max", mini_get("rac", 0));
    228         char k[4] = { 'd', (char)('0' + G.rows), 0 };
    229         add_stat(k, 1);
    230     } else mini_set("rac", 0);
    231     mini_set_str("p", "");
    232     mini_save();
    233 }
    234 
    235 /* Dejar una partida empezada cuenta como perdida (si no, se podria sortear la palabra). */
    236 static void abandon(void)
    237 {
    238     if (has_progress()) {
    239         const char *s = mini_get_str("p");
    240         const char *dot = strchr(s, '.');
    241         if (dot && dot[1]) record(false);
    242         else { mini_set_str("p", ""); mini_save(); }
    243     }
    244 }
    245 
    246 static void set_msg(const char *m)
    247 {
    248     str_put(G.msg, sizeof G.msg, 0, m);
    249     G.msg_t = hal_ms();
    250 }
    251 
    252 static void submit(void)
    253 {
    254     if (G.ncur < LEN) { set_msg("Faltan letras"); G.shaking = true; G.shake_t = hal_ms(); return; }
    255     if (!is_valid(G.cur)) { set_msg("No está en la lista"); G.shaking = true; G.shake_t = hal_ms(); return; }
    256     char why[48];
    257     if (G.hard && !hard_ok(G.cur, why, sizeof why)) { set_msg(why); G.shaking = true; G.shake_t = hal_ms(); return; }
    258     memcpy(G.guess[G.rows], G.cur, LEN);
    259     score(G.cur, G.res[G.rows]);
    260     G.rows++;
    261     G.ncur = 0;
    262     G.msg[0] = 0;
    263     G.revealing = true;
    264     G.reveal_t = hal_ms();
    265     bool won = !memcmp(G.guess[G.rows - 1], G.ans, LEN);
    266     if (won || G.rows == ROWS) {
    267         G.state = won ? WON : LOST;
    268         G.card = true;
    269         record(won);
    270     } else save_progress();
    271 }
    272 
    273 /* Termina la animacion de revelado (el teclado se actualiza recien ahi). */
    274 static void finish_reveal(void)
    275 {
    276     if (!G.revealing) return;
    277     G.revealing = false;
    278     update_keys(G.rows - 1);
    279 }
    280 
    281 static void type_letter(int l)
    282 {
    283     if (G.ncur < LEN) G.cur[G.ncur++] = (uint8_t)l;
    284 }
    285 
    286 /* ------------------------------------------------------------ dibujo */
    287 
    288 #define BG       RGB(16, 20, 30)
    289 #define C_GREEN  RGB(70, 150, 70)
    290 #define C_YELLOW RGB(196, 164, 48)
    291 #define C_GRAY   RGB(62, 66, 76)
    292 #define C_EMPTY  RGB(50, 56, 72)
    293 #define C_TYPED  RGB(130, 140, 162)
    294 #define C_KEY    RGB(104, 114, 136)
    295 
    296 static Color tile_color(int st)
    297 {
    298     return st == CORRECT ? C_GREEN : st == PRESENT ? C_YELLOW : C_GRAY;
    299 }
    300 
    301 static void draw_tile(Surface *s, int x, int y, int l, int st, bool typed)
    302 {
    303     char t[4];
    304     if (st == UNK) {
    305         gfx_rect(s, x, y, CELL, CELL, BG);
    306         gfx_frame(s, x, y, CELL, CELL, typed ? C_TYPED : C_EMPTY);
    307         gfx_frame(s, x + 1, y + 1, CELL - 2, CELL - 2, typed ? C_TYPED : C_EMPTY);
    308     } else gfx_rect(s, x, y, CELL, CELL, tile_color(st));
    309     if (l < 0) return;
    310     put_letter(t, sizeof t, 0, l);
    311     mini_big_text(s, &font_bold, x + (CELL - 12) / 2, y + 3, 2, t, C_WHITE);
    312 }
    313 
    314 /* Cuantas casillas de la ultima fila ya se revelaron. */
    315 static int revealed(void)
    316 {
    317     if (!G.revealing) return LEN;
    318     int n = (int)((hal_ms() - G.reveal_t) / STEP_MS);
    319     return n > LEN ? LEN : n;
    320 }
    321 
    322 static int shake_dx(void)
    323 {
    324     static const int8_t D[] = { -6, 6, -5, 5, -3, 3, -1, 0 };
    325     if (!G.shaking) return 0;
    326     uint32_t i = (hal_ms() - G.shake_t) / SHAKE_MS;
    327     return i < sizeof D ? D[i] : 0;
    328 }
    329 
    330 static void draw_board(Surface *s)
    331 {
    332     int rv = revealed();
    333     for (int r = 0; r < ROWS; r++) {
    334         int y = GY + r * (CELL + GAP), dx = r == G.rows && G.state == PLAYING ? shake_dx() : 0;
    335         for (int i = 0; i < LEN; i++) {
    336             int x = GX + dx + i * (CELL + GAP);
    337             if (r < G.rows) {
    338                 bool shown = r < G.rows - 1 || i < rv;
    339                 draw_tile(s, x, y, G.guess[r][i], shown ? G.res[r][i] : UNK, true);
    340             } else if (r == G.rows && G.state == PLAYING)
    341                 draw_tile(s, x, y, i < G.ncur ? G.cur[i] : -1, UNK, i < G.ncur);
    342             else draw_tile(s, x, y, -1, UNK, false);
    343         }
    344     }
    345 }
    346 
    347 static void draw_key(Surface *s, int x, int y, int w, const char *t, Color bg)
    348 {
    349     gfx_rect(s, x, y, w, KH, bg);
    350     gfx_text_center(s, &font_bold, x, y + 3, w, t, C_WHITE);
    351 }
    352 
    353 static const char *const KROWS[3] = { "QWERTYUIOP", "ASDFGHJKL;", "ZXCVBNM" };
    354 
    355 static void draw_keyboard(Surface *s)
    356 {
    357     for (int r = 0; r < 3; r++) {
    358         const char *k = KROWS[r];
    359         int n = (int)strlen(k), side = r == 2 ? 44 : 0;
    360         int w = n * KW + (n - 1) * KGAP + (side ? 2 * (side + KGAP) : 0);
    361         int x = (320 - w) / 2, y = KY + r * (KH + KGAP);
    362         if (side) {
    363             draw_key(s, x, y, side, "", C_KEY);
    364             gfx_text_center(s, &font_small, x, y + 6, side, "ENTER", C_WHITE);
    365             x += side + KGAP;
    366         }
    367         for (int i = 0; i < n; i++, x += KW + KGAP) {
    368             int l = k[i] == ';' ? ENE : k[i] - 'A';
    369             char t[3];
    370             put_letter(t, sizeof t, 0, l);
    371             Color bg = G.keys[l] == UNK ? C_KEY : G.keys[l] == ABSENT ? RGB(36, 40, 50) : tile_color(G.keys[l]);
    372             draw_key(s, x, y, KW, t, bg);
    373             if (G.keys[l] == ABSENT) gfx_text_center(s, &font_bold, x, y + 3, KW, t, RGB(90, 96, 110));
    374         }
    375         if (side) {                               /* borrar: una flecha */
    376             draw_key(s, x, y, side, "", C_KEY);
    377             int cx = x + side / 2, cy = y + KH / 2;
    378             int32_t xs[3] = { (cx - 10) * 16, (cx - 3) * 16, (cx - 3) * 16 };
    379             int32_t ys[3] = { cy * 16, (cy - 6) * 16, (cy + 6) * 16 };
    380             gfx_poly16(s, xs, ys, 3, C_WHITE);
    381             gfx_rect(s, cx - 3, cy - 2, 12, 5, C_WHITE);
    382         }
    383     }
    384 }
    385 
    386 static void stat_col(Surface *s, int x, int y, long v, const char *label)
    387 {
    388     char b[12];
    389     str_int(b, sizeof b, 0, v);
    390     int w = mini_big_w(&font_bold, 2, b);
    391     mini_big_text(s, &font_bold, x + (60 - w) / 2, y, 2, b, C_TEXT);
    392     gfx_text_center(s, &font_small, x, y + 28, 60, label, C_DIM);
    393 }
    394 
    395 static void draw_card(Surface *s)
    396 {
    397     int bx = 30, by = 34, w = 260, h = 232;
    398     Color edge = G.state == WON ? C_OK : C_ERR;
    399     char b[48];
    400     gfx_box(s, bx, by, w, h, C_PANEL, edge);
    401     gfx_box(s, bx + 1, by + 1, w - 2, h - 2, C_PANEL, edge);
    402     const char *title = G.state == WON ? "¡Ganaste!" : "Perdiste";
    403     mini_big_text(s, &font_bold, bx + (w - mini_big_w(&font_bold, 2, title)) / 2, by + 8, 2, title, edge);
    404     int p = str_put(b, sizeof b, 0, "La palabra era ");
    405     put_solution(b, sizeof b, p);
    406     gfx_text_center(s, &font_normal, bx, by + 38, w, b, C_TEXT);
    407 
    408     long jug = mini_get("jug", 0), gan = mini_get("gan", 0);
    409     int y = by + 58;
    410     stat_col(s, bx + 10, y, jug, "Jugadas");
    411     stat_col(s, bx + 70, y, jug ? (gan * 100 + jug / 2) / jug : 0, "% ganadas");
    412     stat_col(s, bx + 130, y, mini_get("rac", 0), "Racha");
    413     stat_col(s, bx + 190, y, mini_get("max", 0), "Máxima");
    414 
    415     y += 44;
    416     gfx_text_center(s, &font_bold, bx, y, w, "Intentos", C_ACCENT);
    417     y += 17;
    418     long d[ROWS], dmax = 1;
    419     for (int i = 0; i < ROWS; i++) {
    420         char k[4] = { 'd', (char)('1' + i), 0 };
    421         d[i] = mini_get(k, 0);
    422         if (d[i] > dmax) dmax = d[i];
    423     }
    424     for (int i = 0; i < ROWS; i++, y += 15) {
    425         char n[2] = { (char)('1' + i), 0 };
    426         gfx_text(s, &font_bold, bx + 16, y, n, C_TEXT);
    427         str_int(b, sizeof b, 0, d[i]);
    428         int tw = gfx_text_w(&font_bold, b), full = w - 44;
    429         int bw = (int)(d[i] * full / dmax);
    430         if (bw < tw + 8) bw = tw + 8;
    431         bool mine = G.state == WON && G.rows == i + 1;
    432         gfx_rect(s, bx + 28, y - 1, bw, 13, mine ? C_GREEN : C_GRAY);
    433         gfx_text(s, &font_bold, bx + 28 + bw - tw - 4, y, b, C_WHITE);
    434     }
    435     gfx_text_center(s, &font_small, bx, by + h - 14, w, "Enter otra   Tab tablero   Esc menú", C_DIM);
    436 }
    437 
    438 static void paint(Surface *s, void *ctx)
    439 {
    440     char b[40];
    441     gfx_rect(s, 0, 0, 320, 320, BG);
    442     gfx_rect(s, 0, 0, 320, 24, C_PANEL);
    443     int p = str_put(b, sizeof b, 0, "Intento ");
    444     p = str_int(b, sizeof b, p, G.rows + (G.state == PLAYING ? 1 : 0));
    445     str_put(b, sizeof b, p, "/6");
    446     gfx_text(s, &font_bold, 6, 6, b, C_TEXT);
    447     gfx_text_center(s, &font_normal, 100, 6, 120, G.hard ? "Difícil" : "Normal", C_ACCENT);
    448 #if PALABRA_DEBUG
    449     put_solution(b, sizeof b, 0);
    450 #else
    451     p = str_put(b, sizeof b, 0, "Racha ");
    452     str_int(b, sizeof b, p, mini_get("rac", 0));
    453 #endif
    454     gfx_text(s, &font_bold, 314 - gfx_text_w(&font_bold, b), 6, b, C_TEXT);
    455 
    456     draw_board(s);
    457     draw_keyboard(s);
    458     if (G.msg[0] && hal_ms() - G.msg_t < MSG_MS) {
    459         int w = gfx_text_w(&font_bold, G.msg) + 16;
    460         gfx_box(s, (320 - w) / 2, 226, w, 16, C_TEXT, C_TEXT);
    461         gfx_text_center(s, &font_bold, 0, 228, 320, G.msg, BG);
    462     }
    463     if (G.state != PLAYING && !G.revealing && G.card) draw_card(s);
    464     else if (G.state != PLAYING && !G.revealing)
    465         gfx_text_center(s, &font_small, 0, 311, 320, "Enter otra   Tab estadísticas   Esc menú", C_DIM);
    466     else gfx_text_center(s, &font_small, 0, 311, 320, "Enter probar   Retroceso borrar   ; = Ñ   Esc menú", C_DIM);
    467 }
    468 
    469 /* ------------------------------------------------------------ menus y bucle */
    470 
    471 static void art(Surface *s)
    472 {
    473     /* PALABRA en fichas de colores */
    474     static const char W[] = "PALABRA";
    475     static const uint8_t ST[] = { CORRECT, ABSENT, PRESENT, ABSENT, CORRECT, PRESENT, CORRECT };
    476     int c = 22, g = 3, x0 = (320 - 7 * c - 6 * g) / 2, y = 58;
    477     for (int i = 0; i < 7; i++) {
    478         int x = x0 + i * (c + g);
    479         char t[2] = { W[i], 0 };
    480         gfx_rect(s, x, y, c, c, tile_color(ST[i]));
    481         gfx_text_center(s, &font_bold, x, y + 5, c, t, C_WHITE);
    482     }
    483 }
    484 
    485 static const char *const MODES[] = { "Normal", "Difícil" };
    486 
    487 static void how_to(void)
    488 {
    489     Form f;
    490     form_begin(&f, "Cómo se juega");
    491     form_label(&f, "Adiviná la palabra de 5 letras en 6 intentos.");
    492     form_label(&f, "Verde: la letra está en ese lugar.");
    493     form_label(&f, "Amarillo: está en la palabra, en otro lugar.");
    494     form_label(&f, "Gris: no está (o no tantas veces).");
    495     form_label(&f, "Las tildes no cuentan. ; = Ñ");
    496     form_label(&f, "Difícil: las pistas se tienen que usar.");
    497     form_button(&f, 1, "OK");
    498     f.cur = f.n - 1;
    499     mini_form(&f);
    500 }
    501 
    502 /* 1 jugar nueva, 2 continuar */
    503 static int main_menu(bool *hard)
    504 {
    505     static char l1[64], l2[64];
    506     for (;;) {
    507         bool cont = has_progress();
    508         Form f;
    509         form_begin(&f, "Palabra");
    510         if (cont) form_button(&f, 2, "Continuar");
    511         form_button(&f, 1, "Palabra al azar");
    512         form_choice(&f, 3, "Modo", MODES, 2, *hard ? 1 : 0);
    513         form_button(&f, 4, "Cómo se juega");
    514         form_label(&f, "");
    515         long jug = mini_get("jug", 0), gan = mini_get("gan", 0);
    516         int p = str_put(l1, sizeof l1, 0, "Jugadas ");
    517         p = str_int(l1, sizeof l1, p, jug);
    518         p = str_put(l1, sizeof l1, p, ", ganadas ");
    519         p = str_int(l1, sizeof l1, p, jug ? (gan * 100 + jug / 2) / jug : 0);
    520         str_put(l1, sizeof l1, p, "%");
    521         p = str_put(l2, sizeof l2, 0, "Racha ");
    522         p = str_int(l2, sizeof l2, p, mini_get("rac", 0));
    523         p = str_put(l2, sizeof l2, p, ", máxima ");
    524         str_int(l2, sizeof l2, p, mini_get("max", 0));
    525         form_label(&f, l1);
    526         form_label(&f, l2);
    527         int r = mini_form(&f);
    528         bool h = form_get(&f, 3)->value == 1;
    529         if (h != *hard) { *hard = h; mini_set("modo", h); mini_save(); }
    530         if (r == 4) how_to();
    531         else if (r == 1 || r == 2) return r;
    532     }
    533 }
    534 
    535 /* Devuelve true si hay que volver al menu principal. */
    536 static bool pause_menu(void)
    537 {
    538     Form f;
    539     form_begin(&f, "Pausa");
    540     form_button(&f, 1, "Seguir");
    541     form_button(&f, 2, "Empezar de nuevo");
    542     form_button(&f, 3, "Menú principal");
    543     form_label(&f, "");
    544     form_label(&f, "La partida queda guardada.");
    545     form_label(&f, "Empezar de nuevo cuenta como perdida.");
    546     int r = mini_form(&f);
    547     if (r == 2) {
    548         if (G.rows) record(false);
    549         new_game(G.hard);
    550     }
    551     return r == 3;
    552 }
    553 
    554 static int to_letter(int k)
    555 {
    556     if (k >= 'a' && k <= 'z') return k - 'a';
    557     if (k >= 'A' && k <= 'Z') return k - 'A';
    558     if (k == ';' || k == 0xF1 || k == 0xD1) return ENE;       /* ñ Ñ en Latin-1 */
    559     return -1;
    560 }
    561 
    562 void app_main(void)
    563 {
    564     mini_start("Palabra", RGB(110, 200, 110));
    565     mini_art = art;
    566     bool hard = mini_get("modo", 0) == 1;
    567     for (;;) {
    568         int m = main_menu(&hard);
    569         if (m == 2 && load_progress()) {
    570             hard = G.hard;
    571         } else {
    572             abandon();
    573             new_game(hard);
    574         }
    575         bool dirty = true;
    576         int shown_rv = -1, shown_dx = 0;
    577         bool shown_msg = false;
    578         for (;;) {
    579             int k = hal_key();
    580             if (k == 0) dirty = true;
    581             else if (k > 0) {
    582                 dirty = true;
    583                 if (G.revealing) finish_reveal();
    584                 if (G.state != PLAYING) {
    585                     if (k == K_OK || k == ' ') { new_game(G.hard); }
    586                     else if (k == K_TAB) G.card = !G.card;
    587                     else if (k == K_BACK) break;
    588                     continue;
    589                 }
    590                 int l = to_letter(k);
    591                 if (l >= 0) type_letter(l);
    592                 else if (k == K_BKSP || k == K_LEFT) { if (G.ncur) G.ncur--; }
    593                 else if (k == K_OK) submit();
    594                 else if (k == K_BACK && pause_menu()) break;
    595             }
    596             /* animaciones: revelado, sacudon y mensaje */
    597             if (G.revealing) {
    598                 int rv = revealed();
    599                 if (rv != shown_rv) { shown_rv = rv; dirty = true; }
    600                 if (hal_ms() - G.reveal_t >= (uint32_t)(LEN * STEP_MS + 100)) { finish_reveal(); dirty = true; }
    601             }
    602             if (G.shaking) {
    603                 int dx = shake_dx();
    604                 if (dx != shown_dx) { shown_dx = dx; dirty = true; }
    605                 if (hal_ms() - G.shake_t > 8 * SHAKE_MS) G.shaking = false;
    606             }
    607             bool msg = G.msg[0] && hal_ms() - G.msg_t < MSG_MS;
    608             if (msg != shown_msg) { shown_msg = msg; dirty = true; }
    609             if (dirty) { hal_draw(paint, 0); dirty = false; }
    610             else hal_sleep(10);
    611         }
    612     }
    613 }