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


      1 /* app.c - Bloques: piezas que caen, para la PicoCalc.
      2  *
      3  * Flechas izq/der mueven, arriba o X rota horario, Z antihorario, abajo baja una fila
      4  * (el teclado solo manda "apretada": cada toque es una fila), espacio/Enter la tira
      5  * hasta el fondo, C guarda la pieza, Esc pausa.
      6  * Bolsa de 7, rotacion SRS con wall kicks, lock delay de 500 ms con tope de reinicios,
      7  * gravedad y puntaje segun la guideline. Los tiempos se miden con hal_ms. */
      8 #include <string.h>
      9 #include "mini.h"
     10 
     11 enum {
     12     W = 10, H = 22, HID = 2,               /* 2 filas ocultas arriba del pozo */
     13     CELL = 14, TOP = 26,
     14     OX = 14, OY = TOP,                     /* esquina del pozo visible */
     15     PX = 166, PW = 148,                    /* panel lateral */
     16     LOCK_MS = 500, LOCK_RESETS = 15, FLASH_MS = 150,
     17     NLEVELS = 15, MAXLEVEL = 20, NQUEUE = 3,
     18 };
     19 
     20 enum { PLAYING, FLASH, OVER };
     21 
     22 /* ------------------------------------------------------------ piezas */
     23 
     24 enum { P_I, P_O, P_T, P_S, P_Z, P_J, P_L };
     25 
     26 /* estado 0 de cada pieza (x, y con y hacia abajo) y lado de su caja de rotacion */
     27 static const int8_t SHAPE0[7][4][2] = {
     28     { {0,1}, {1,1}, {2,1}, {3,1} },        /* I */
     29     { {1,0}, {2,0}, {1,1}, {2,1} },        /* O */
     30     { {1,0}, {0,1}, {1,1}, {2,1} },        /* T */
     31     { {1,0}, {2,0}, {0,1}, {1,1} },        /* S */
     32     { {0,0}, {1,0}, {1,1}, {2,1} },        /* Z */
     33     { {0,0}, {0,1}, {1,1}, {2,1} },        /* J */
     34     { {2,0}, {0,1}, {1,1}, {2,1} },        /* L */
     35 };
     36 static const int8_t BOX[7] = { 4, 4, 3, 3, 3, 3, 3 };
     37 
     38 static int8_t shape[7][4][4][2];           /* [pieza][rotacion][celda][x,y] */
     39 
     40 /* Wall kicks SRS (y hacia arriba, como en la tabla oficial). Indice: 0->R, R->0,
     41  * R->2, 2->R, 2->L, L->2, L->0, 0->L; horario desde s = 2s, antihorario = (2s+7)%8. */
     42 static const int8_t KICK_JLSTZ[8][5][2] = {
     43     { {0,0}, {-1,0}, {-1, 1}, {0,-2}, {-1,-2} },
     44     { {0,0}, { 1,0}, { 1,-1}, {0, 2}, { 1, 2} },
     45     { {0,0}, { 1,0}, { 1,-1}, {0, 2}, { 1, 2} },
     46     { {0,0}, {-1,0}, {-1, 1}, {0,-2}, {-1,-2} },
     47     { {0,0}, { 1,0}, { 1, 1}, {0,-2}, { 1,-2} },
     48     { {0,0}, {-1,0}, {-1,-1}, {0, 2}, {-1, 2} },
     49     { {0,0}, {-1,0}, {-1,-1}, {0, 2}, {-1, 2} },
     50     { {0,0}, { 1,0}, { 1, 1}, {0,-2}, { 1,-2} },
     51 };
     52 static const int8_t KICK_I[8][5][2] = {
     53     { {0,0}, {-2,0}, { 1,0}, {-2,-1}, { 1, 2} },
     54     { {0,0}, { 2,0}, {-1,0}, { 2, 1}, {-1,-2} },
     55     { {0,0}, {-1,0}, { 2,0}, {-1, 2}, { 2,-1} },
     56     { {0,0}, { 1,0}, {-2,0}, { 1,-2}, {-2, 1} },
     57     { {0,0}, { 2,0}, {-1,0}, { 2, 1}, {-1,-2} },
     58     { {0,0}, {-2,0}, { 1,0}, {-2,-1}, { 1, 2} },
     59     { {0,0}, { 1,0}, {-2,0}, { 1,-2}, {-2, 1} },
     60     { {0,0}, {-1,0}, { 2,0}, {-1, 2}, { 2,-1} },
     61 };
     62 
     63 /* ms por fila segun nivel (guideline: (0.8 - (n-1)*0.007)^(n-1) s) */
     64 static const uint16_t GRAVITY[MAXLEVEL] = {
     65     1000, 793, 618, 473, 355, 262, 190, 135, 94, 64, 43, 28, 18, 11, 7, 5, 4, 3, 2, 1
     66 };
     67 
     68 static const Color COL[7]   = { RGB(40, 200, 230), RGB(240, 210, 40), RGB(170, 80, 220), RGB(80, 200, 80),
     69                                 RGB(230, 60, 60), RGB(60, 110, 235), RGB(240, 140, 40) };
     70 static const Color LIGHT[7] = { RGB(150, 235, 250), RGB(255, 240, 150), RGB(210, 160, 245), RGB(160, 235, 160),
     71                                 RGB(250, 150, 150), RGB(150, 180, 250), RGB(255, 195, 130) };
     72 static const Color DARK[7]  = { RGB(20, 110, 130), RGB(150, 125, 20), RGB(95, 40, 130), RGB(40, 115, 40),
     73                                 RGB(135, 30, 30), RGB(30, 55, 140), RGB(145, 80, 20) };
     74 
     75 static void init_shapes(void)
     76 {
     77     for (int p = 0; p < 7; p++) {
     78         int n = BOX[p];
     79         for (int c = 0; c < 4; c++) { shape[p][0][c][0] = SHAPE0[p][c][0]; shape[p][0][c][1] = SHAPE0[p][c][1]; }
     80         for (int r = 1; r < 4; r++)
     81             for (int c = 0; c < 4; c++) {
     82                 int x = shape[p][r - 1][c][0], y = shape[p][r - 1][c][1];
     83                 if (p == P_O) { shape[p][r][c][0] = (int8_t)x; shape[p][r][c][1] = (int8_t)y; }
     84                 else { shape[p][r][c][0] = (int8_t)(n - 1 - y); shape[p][r][c][1] = (int8_t)x; }
     85             }
     86     }
     87 }
     88 
     89 /* ------------------------------------------------------------ estado */
     90 
     91 typedef struct {
     92     uint8_t well[H][W];                    /* 0 vacio, 1..7 pieza+1 */
     93     int cur, rot, x, y;                    /* pieza en juego */
     94     int hold;                              /* -1 nada */
     95     bool held;                             /* ya se guardo con esta pieza */
     96     uint8_t bag[7]; int bag_n;
     97     uint8_t queue[NQUEUE];
     98     int state;
     99     int start_level, level;
    100     long score, lines, best;
    101     bool b2b;                              /* el ultimo borrado fue un tetris */
    102     uint32_t fall_t, lock_t, flash_t, t0, elapsed, over_t;
    103     bool locking;
    104     int resets, lowest;
    105     uint8_t full[H];                       /* filas que destellan */
    106     int queued[4], nq;                     /* teclas apretadas durante el destello */
    107     bool record;
    108     const char *msg;                       /* "¡Tetris!", "Doble"... */
    109     uint32_t msg_t;
    110 } Game;
    111 
    112 static Game G;
    113 
    114 /* ------------------------------------------------------------ reglas */
    115 
    116 static bool fits(int p, int r, int x, int y)
    117 {
    118     for (int c = 0; c < 4; c++) {
    119         int cx = x + shape[p][r][c][0], cy = y + shape[p][r][c][1];
    120         if (cx < 0 || cx >= W || cy < 0 || cy >= H || G.well[cy][cx]) return false;
    121     }
    122     return true;
    123 }
    124 
    125 static int next_from_bag(void)
    126 {
    127     if (G.bag_n == 0) {
    128         for (int i = 0; i < 7; i++) G.bag[i] = (uint8_t)i;
    129         mini_shuffle(G.bag, 7);
    130         G.bag_n = 7;
    131     }
    132     return G.bag[--G.bag_n];
    133 }
    134 
    135 static int take_next(void)
    136 {
    137     int p = G.queue[0];
    138     for (int i = 0; i < NQUEUE - 1; i++) G.queue[i] = G.queue[i + 1];
    139     G.queue[NQUEUE - 1] = (uint8_t)next_from_bag();
    140     return p;
    141 }
    142 
    143 static void end_game(void);
    144 
    145 static void spawn(int p)
    146 {
    147     G.cur = p; G.rot = 0;
    148     G.x = 3; G.y = 0;
    149     G.locking = false; G.resets = 0;
    150     G.fall_t = hal_ms();
    151     if (!fits(p, 0, G.x, G.y)) { end_game(); return; }
    152     if (fits(p, 0, G.x, G.y + 1)) G.y++;
    153     G.lowest = G.y;
    154 }
    155 
    156 static int level_now(void)
    157 {
    158     int l = G.start_level + (int)(G.lines / 10);
    159     return l > MAXLEVEL ? MAXLEVEL : l;
    160 }
    161 
    162 static void new_game(int start_level)
    163 {
    164     memset(&G, 0, sizeof G);
    165     G.start_level = G.level = start_level;
    166     G.hold = -1;
    167     G.best = mini_get("rec", 0);
    168     for (int i = 0; i < NQUEUE; i++) G.queue[i] = (uint8_t)next_from_bag();
    169     G.t0 = hal_ms();
    170     spawn(take_next());
    171 }
    172 
    173 /* Records y estadisticas; tambien al abandonar desde la pausa. */
    174 static void save_stats(void)
    175 {
    176     mini_set("jug", mini_get("jug", 0) + 1);
    177     if (G.score > mini_get("rec", 0)) { mini_set("rec", G.score); G.record = true; }
    178     if (G.lines > mini_get("lin", 0)) mini_set("lin", G.lines);
    179     mini_save();
    180 }
    181 
    182 static void end_game(void)
    183 {
    184     G.state = OVER;
    185     G.elapsed = hal_ms() - G.t0;
    186     G.over_t = hal_ms();
    187     save_stats();
    188 }
    189 
    190 /* Despues de un movimiento o giro que salio bien: reinicia el lock delay (con tope). */
    191 static void moved(void)
    192 {
    193     if (G.y > G.lowest) { G.lowest = G.y; G.resets = 0; }
    194     if (G.locking && G.resets < LOCK_RESETS) { G.resets++; G.lock_t = hal_ms(); }
    195     if (fits(G.cur, G.rot, G.x, G.y + 1)) G.locking = false;
    196 }
    197 
    198 static bool shift(int dx)
    199 {
    200     if (!fits(G.cur, G.rot, G.x + dx, G.y)) return false;
    201     G.x += dx;
    202     moved();
    203     return true;
    204 }
    205 
    206 static bool rotate(int dir)                /* 1 horario, -1 antihorario */
    207 {
    208     if (G.cur == P_O) return false;
    209     int to = (G.rot + (dir > 0 ? 1 : 3)) & 3;
    210     int k = dir > 0 ? 2 * G.rot : (2 * G.rot + 7) % 8;
    211     const int8_t (*kick)[2] = G.cur == P_I ? KICK_I[k] : KICK_JLSTZ[k];
    212     for (int t = 0; t < 5; t++) {
    213         int nx = G.x + kick[t][0], ny = G.y - kick[t][1];
    214         if (fits(G.cur, to, nx, ny)) {
    215             G.x = nx; G.y = ny; G.rot = to;
    216             moved();
    217             return true;
    218         }
    219     }
    220     return false;
    221 }
    222 
    223 static void lock_piece(void)
    224 {
    225     bool visible = false;
    226     for (int c = 0; c < 4; c++) {
    227         int cy = G.y + shape[G.cur][G.rot][c][1];
    228         G.well[cy][G.x + shape[G.cur][G.rot][c][0]] = (uint8_t)(G.cur + 1);
    229         if (cy >= HID) visible = true;
    230     }
    231     int n = 0;
    232     for (int y = 0; y < H; y++) {
    233         int k = 0;
    234         for (int x = 0; x < W; x++) k += G.well[y][x] != 0;
    235         G.full[y] = k == W;
    236         n += G.full[y];
    237     }
    238     G.held = false;
    239     if (n) {
    240         static const int PTS[5] = { 0, 100, 300, 500, 800 };
    241         static const char *const MSG[5] = { 0, "Simple", "Doble", "Triple", "¡Tetris!" };
    242         long pts = (long)PTS[n] * G.level;
    243         if (n == 4 && G.b2b) pts += pts / 2;
    244         G.b2b = n == 4;
    245         G.score += pts;
    246         G.lines += n;
    247         G.level = level_now();
    248         G.msg = MSG[n]; G.msg_t = hal_ms();
    249         G.state = FLASH;
    250         G.flash_t = hal_ms();
    251         G.cur = -1;
    252         return;
    253     }
    254     if (!visible) { G.cur = -1; end_game(); return; }   /* se trabo entera arriba del pozo */
    255     spawn(take_next());
    256 }
    257 
    258 static void finish_flash(void)
    259 {
    260     int dst = H - 1;
    261     for (int y = H - 1; y >= 0; y--)
    262         if (!G.full[y]) { if (dst != y) memcpy(G.well[dst], G.well[y], W); dst--; }
    263     for (; dst >= 0; dst--) memset(G.well[dst], 0, W);
    264     memset(G.full, 0, sizeof G.full);
    265     G.state = PLAYING;
    266     spawn(take_next());
    267 }
    268 
    269 static int drop_dist(void)
    270 {
    271     int d = 0;
    272     while (fits(G.cur, G.rot, G.x, G.y + d + 1)) d++;
    273     return d;
    274 }
    275 
    276 static void hard_drop(void)
    277 {
    278     int d = drop_dist();
    279     G.y += d;
    280     G.score += 2 * d;
    281     lock_piece();
    282 }
    283 
    284 static void soft_drop(void)
    285 {
    286     if (!fits(G.cur, G.rot, G.x, G.y + 1)) return;
    287     G.y++;
    288     G.score++;
    289     G.fall_t = hal_ms();
    290     moved();
    291 }
    292 
    293 static void do_hold(void)
    294 {
    295     if (G.held) return;
    296     int p = G.cur;
    297     if (G.hold < 0) { G.hold = p; spawn(take_next()); }
    298     else { int h = G.hold; G.hold = p; spawn(h); }
    299     G.held = true;
    300 }
    301 
    302 /* Gravedad y lock delay; devuelve true si algo cambio. */
    303 static bool tick(uint32_t now)
    304 {
    305     if (G.state == FLASH) {
    306         if (now - G.flash_t < FLASH_MS) return false;
    307         finish_flash();
    308         return true;
    309     }
    310     if (G.state != PLAYING) return false;
    311     bool ch = false;
    312     uint32_t g = GRAVITY[G.level - 1];
    313     int steps = 0;
    314     while (now - G.fall_t >= g && steps < H) {
    315         if (!fits(G.cur, G.rot, G.x, G.y + 1)) { G.fall_t = now; break; }
    316         G.y++; G.fall_t += g; steps++; ch = true;
    317         if (G.y > G.lowest) { G.lowest = G.y; G.resets = 0; }
    318     }
    319     if (!fits(G.cur, G.rot, G.x, G.y + 1)) {
    320         if (!G.locking) { G.locking = true; G.lock_t = now; }
    321         else if (now - G.lock_t >= LOCK_MS) { lock_piece(); ch = true; }
    322     } else G.locking = false;
    323     return ch;
    324 }
    325 
    326 static void game_key(int k)
    327 {
    328     switch (k) {
    329     case K_LEFT:  shift(-1); break;
    330     case K_RIGHT: shift(1); break;
    331     case K_UP: case 'x': case 'X': rotate(1); break;
    332     case 'z': case 'Z': rotate(-1); break;
    333     case K_DOWN: soft_drop(); break;
    334     case ' ': case K_OK: hard_drop(); break;
    335     case 'c': case 'C': do_hold(); break;
    336     }
    337 }
    338 
    339 /* ------------------------------------------------------------ dibujo */
    340 
    341 static void draw_block(Surface *s, int x, int y, int c, int p)
    342 {
    343     gfx_rect(s, x, y, c, c, COL[p]);
    344     gfx_rect(s, x, y, c, 2, LIGHT[p]);
    345     gfx_rect(s, x, y, 2, c, LIGHT[p]);
    346     gfx_rect(s, x, y + c - 2, c, 2, DARK[p]);
    347     gfx_rect(s, x + c - 2, y + 2, 2, c - 2, DARK[p]);
    348 }
    349 
    350 /* pieza suelta centrada en la caja (x, y, w, h) */
    351 static void draw_mini(Surface *s, int p, int x, int y, int w, int h, int c, bool dim)
    352 {
    353     int minx = 9, maxx = -1, miny = 9, maxy = -1;
    354     for (int i = 0; i < 4; i++) {
    355         int cx = shape[p][0][i][0], cy = shape[p][0][i][1];
    356         if (cx < minx) minx = cx;
    357         if (cx > maxx) maxx = cx;
    358         if (cy < miny) miny = cy;
    359         if (cy > maxy) maxy = cy;
    360     }
    361     int ox = x + (w - (maxx - minx + 1) * c) / 2, oy = y + (h - (maxy - miny + 1) * c) / 2;
    362     for (int i = 0; i < 4; i++) {
    363         int bx = ox + (shape[p][0][i][0] - minx) * c, by = oy + (shape[p][0][i][1] - miny) * c;
    364         if (dim) { gfx_rect(s, bx, by, c, c, RGB(70, 76, 92)); gfx_frame(s, bx, by, c, c, RGB(50, 55, 68)); }
    365         else draw_block(s, bx, by, c, p);
    366     }
    367 }
    368 
    369 static uint32_t elapsed(void) { return G.state == OVER ? G.elapsed : hal_ms() - G.t0; }
    370 
    371 static void stat_row(Surface *s, int y, const char *label, long v, Color c)
    372 {
    373     char buf[16];
    374     str_int(buf, sizeof buf, 0, v);
    375     gfx_text(s, &font_normal, PX + 8, y, label, C_DIM);
    376     gfx_text(s, &font_bold, PX + PW - 8 - gfx_text_w(&font_bold, buf), y, buf, c);
    377 }
    378 
    379 static void paint(Surface *s, void *ctx)
    380 {
    381     char buf[40];
    382     gfx_rect(s, 0, 0, 320, 320, RGB(16, 20, 30));
    383 
    384     /* barra de arriba */
    385     gfx_rect(s, 0, 0, 320, 24, C_PANEL);
    386     gfx_text(s, &font_bold, 8, 6, "Bloques", C_ACCENT);
    387     int p = str_put(buf, sizeof buf, 0, "Nivel ");
    388     str_int(buf, sizeof buf, p, G.level);
    389     gfx_text_center(s, &font_normal, 80, 6, 160, buf, C_TEXT);
    390     str_time(buf, sizeof buf, 0, elapsed());
    391     gfx_text(s, &font_bold, 312 - gfx_text_w(&font_bold, buf), 6, buf, C_TEXT);
    392 
    393     /* pozo: fondo con grilla fija */
    394     int bw = W * CELL, bh = (H - HID) * CELL;
    395     gfx_rect(s, OX - 3, OY - 3, bw + 6, bh + 6, RGB(60, 68, 88));
    396     gfx_rect(s, OX - 1, OY - 1, bw + 2, bh + 2, RGB(30, 34, 46));
    397     gfx_rect(s, OX, OY, bw, bh, RGB(8, 10, 16));
    398     for (int i = 1; i < W; i++) gfx_rect(s, OX + i * CELL, OY, 1, bh, RGB(20, 24, 34));
    399     for (int i = 1; i < H - HID; i++) gfx_rect(s, OX, OY + i * CELL, bw, 1, RGB(20, 24, 34));
    400 
    401     for (int y = HID; y < H; y++) {
    402         int py = OY + (y - HID) * CELL;
    403         if (G.full[y]) {
    404             gfx_rect(s, OX, py, bw, CELL, RGB(250, 250, 250));
    405             continue;
    406         }
    407         for (int x = 0; x < W; x++)
    408             if (G.well[y][x]) draw_block(s, OX + x * CELL, py, CELL, G.well[y][x] - 1);
    409     }
    410     if (G.state == PLAYING && G.cur >= 0) {
    411         int gy = G.y + drop_dist();
    412         for (int c = 0; c < 4; c++) {                       /* fantasma */
    413             int cy = gy + shape[G.cur][G.rot][c][1];
    414             if (cy < HID) continue;
    415             int bx = OX + (G.x + shape[G.cur][G.rot][c][0]) * CELL, by = OY + (cy - HID) * CELL;
    416             gfx_frame(s, bx + 1, by + 1, CELL - 2, CELL - 2, DARK[G.cur]);
    417             gfx_frame(s, bx + 2, by + 2, CELL - 4, CELL - 4, gfx_dim(DARK[G.cur]));
    418         }
    419         for (int c = 0; c < 4; c++) {
    420             int cy = G.y + shape[G.cur][G.rot][c][1];
    421             if (cy < HID) continue;
    422             draw_block(s, OX + (G.x + shape[G.cur][G.rot][c][0]) * CELL, OY + (cy - HID) * CELL, CELL, G.cur);
    423         }
    424     }
    425 
    426     /* panel lateral */
    427     gfx_text(s, &font_small, PX + 2, 30, "GUARDADA (C)", C_DIM);
    428     gfx_box(s, PX, 40, PW, 40, C_PANEL, C_PANEL2);
    429     if (G.hold >= 0) draw_mini(s, G.hold, PX, 40, PW, 40, 12, G.held);
    430 
    431     gfx_text(s, &font_small, PX + 2, 88, "SIGUIENTES", C_DIM);
    432     gfx_box(s, PX, 98, PW, 96, C_PANEL, C_PANEL2);
    433     draw_mini(s, G.queue[0], PX, 100, PW, 36, 12, false);
    434     draw_mini(s, G.queue[1], PX, 138, PW, 26, 9, false);
    435     draw_mini(s, G.queue[2], PX, 165, PW, 26, 9, false);
    436 
    437     gfx_box(s, PX, 202, PW, 84, C_PANEL, C_PANEL2);
    438     long best = G.score > G.best ? G.score : G.best;
    439     stat_row(s, 208, "Puntos", G.score, C_TEXT);
    440     stat_row(s, 227, "Líneas", G.lines, C_TEXT);
    441     stat_row(s, 246, "Nivel", G.level, C_TEXT);
    442     stat_row(s, 265, "Récord", best, G.score > G.best && G.best ? C_ACCENT : C_TEXT);
    443 
    444     if (G.msg) gfx_text_center(s, &font_bold, PX, 292, PW, G.msg, G.msg[0] == '\xc2' ? C_ACCENT : C_OK);
    445 
    446     if (G.state != OVER) {
    447         gfx_text_center(s, &font_small, 0, 311, 320, "X/Z gira  Abajo baja  Espacio tira  C guarda  Esc pausa", C_DIM);
    448         return;
    449     }
    450     /* cartel de fin */
    451     int bx = 40, by = 110, w = 240, h = 86;
    452     gfx_box(s, bx, by, w, h, C_PANEL, C_ERR);
    453     gfx_box(s, bx + 1, by + 1, w - 2, h - 2, C_PANEL, C_ERR);
    454     gfx_text_center(s, &font_bold, bx, by + 8, w, "Fin de la partida", C_ERR);
    455     p = str_put(buf, sizeof buf, 0, "Puntos ");
    456     p = str_int(buf, sizeof buf, p, G.score);
    457     p = str_put(buf, sizeof buf, p, "   Líneas ");
    458     str_int(buf, sizeof buf, p, G.lines);
    459     gfx_text_center(s, &font_normal, bx, by + 28, w, buf, C_TEXT);
    460     if (G.record) p = str_put(buf, sizeof buf, 0, "¡Récord nuevo!");
    461     else {
    462         p = str_put(buf, sizeof buf, 0, "Récord ");
    463         str_int(buf, sizeof buf, p, G.best);
    464     }
    465     gfx_text_center(s, &font_normal, bx, by + 46, w, buf, G.record ? C_ACCENT : C_DIM);
    466     gfx_text_center(s, &font_small, bx, by + 70, w, "Enter otra   Esc menú", C_DIM);
    467 }
    468 
    469 /* ------------------------------------------------------------ menus y bucle */
    470 
    471 static void art(Surface *s)
    472 {
    473     /* las siete piezas en fila */
    474     static const int8_t order[7] = { P_T, P_S, P_I, P_O, P_L, P_Z, P_J };
    475     int c = 8, x = 40;          /* 168 px de piezas + 6 huecos de 12 */
    476     for (int i = 0; i < 7; i++) {
    477         int p = order[i], w = (p == P_I ? 4 : p == P_O ? 2 : 3) * c;
    478         draw_mini(s, p, x, 58, w, 20, c, false);
    479         x += w + 12;
    480     }
    481 }
    482 
    483 static const char *const LEVEL_NAMES[NLEVELS] = {
    484     "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"
    485 };
    486 
    487 static int main_menu(int lvl)
    488 {
    489     static char lines[3][48];
    490     Form f;
    491     form_begin(&f, "Bloques");
    492     form_button(&f, 1, "Jugar");
    493     form_choice(&f, 2, "Nivel inicial", LEVEL_NAMES, NLEVELS, lvl - 1);
    494     form_label(&f, "");
    495     int p = str_put(lines[0], sizeof lines[0], 0, "Récord: ");
    496     str_int(lines[0], sizeof lines[0], p, mini_get("rec", 0));
    497     p = str_put(lines[1], sizeof lines[1], 0, "Más líneas: ");
    498     str_int(lines[1], sizeof lines[1], p, mini_get("lin", 0));
    499     p = str_put(lines[2], sizeof lines[2], 0, "Partidas: ");
    500     str_int(lines[2], sizeof lines[2], p, mini_get("jug", 0));
    501     for (int i = 0; i < 3; i++) form_label(&f, lines[i]);
    502     for (;;) {
    503         int r = mini_form(&f);
    504         if (r == 1) return form_get(&f, 2)->value + 1;
    505     }
    506 }
    507 
    508 /* Devuelve true si hay que volver al menu principal. */
    509 static bool pause_menu(void)
    510 {
    511     uint32_t t = hal_ms();
    512     Form f;
    513     form_begin(&f, "Pausa");
    514     form_button(&f, 1, "Seguir");
    515     form_button(&f, 2, "Empezar de nuevo");
    516     form_button(&f, 3, "Menú principal");
    517     int r = mini_form(&f);
    518     uint32_t d = hal_ms() - t;                  /* la pausa no cuenta */
    519     G.t0 += d; G.fall_t += d; G.lock_t += d; G.flash_t += d; G.msg_t += d;
    520     if (r == 2 || r == 3) save_stats();
    521     if (r == 2) new_game(G.start_level);
    522     return r == 3;
    523 }
    524 
    525 void app_main(void)
    526 {
    527     mini_start("Bloques", RGB(240, 140, 40));
    528     mini_art = art;
    529     init_shapes();
    530     int lvl = (int)mini_get("nivel", 1);
    531     if (lvl < 1 || lvl > NLEVELS) lvl = 1;
    532     for (;;) {
    533         int nl = main_menu(lvl);
    534         if (nl != lvl) { lvl = nl; mini_set("nivel", lvl); mini_save(); }
    535         new_game(lvl);
    536         bool dirty = true;
    537         uint32_t shown_sec = 0;
    538         for (;;) {
    539             int k = hal_key();
    540             if (k == 0) dirty = true;
    541             else if (k > 0) {
    542                 dirty = true;
    543                 if (G.state == OVER) {
    544                     if (hal_ms() - G.over_t < 400) continue;    /* que un espacio de mas no reinicie */
    545                     if (k == K_OK || k == ' ') new_game(lvl);
    546                     else if (k == K_BACK) break;
    547                     continue;
    548                 }
    549                 if (k == K_BACK) {
    550                     if (pause_menu()) break;
    551                     continue;
    552                 }
    553                 if (G.state == FLASH) {                         /* se aplican al salir la pieza */
    554                     if (G.nq < 4) G.queued[G.nq++] = k;
    555                     continue;
    556                 }
    557                 game_key(k);
    558             }
    559             uint32_t now = hal_ms();
    560             bool was_flash = G.state == FLASH;
    561             if (tick(now)) dirty = true;
    562             if (was_flash && G.state == PLAYING) {
    563                 for (int i = 0; i < G.nq && G.state == PLAYING; i++) game_key(G.queued[i]);
    564                 G.nq = 0;
    565             }
    566             if (G.msg && now - G.msg_t > 1500) { G.msg = 0; dirty = true; }
    567             uint32_t sec = elapsed() / 1000;
    568             if (sec != shown_sec) { shown_sec = sec; dirty = true; }
    569             if (dirty) { hal_draw(paint, 0); dirty = false; }
    570             else hal_sleep(5);
    571         }
    572     }
    573 }