app.c (17168B)
1 /* app.c - 2048 para la PicoCalc. 2 * 3 * Flechas deslizan todas las fichas; dos iguales que chocan se funden (una vez por 4 * jugada). Despues de cada jugada aparece un 2 (90%) o un 4 (10%) en un hueco. 5 * U o Retroceso deshace la ultima jugada (una sola vez seguida). Esc pausa. 6 * La partida en curso se guarda en la SD despues de cada jugada. */ 7 #include <string.h> 8 #include "mini.h" 9 10 enum { MAXN = 5, TOP = 28, FLASH_MS = 90 }; 11 12 typedef struct { const char *name; int n, cell, gap; } Size; 13 static const Size SIZES[] = { 14 { "4x4", 4, 64, 4 }, 15 { "5x5", 5, 50, 4 }, 16 }; 17 enum { NSIZES = 2 }; 18 19 enum { PLAYING, WINBOX, LOST }; 20 21 typedef struct { 22 int sz, n; 23 uint8_t b[MAXN][MAXN]; /* exponente: 0 vacio, 1 = 2, 2 = 4, ... */ 24 uint8_t ub[MAXN][MAXN]; /* tablero antes de la ultima jugada */ 25 uint8_t merged[MAXN][MAXN]; /* fusionadas en la ultima jugada (destello) */ 26 long score, uscore; 27 bool can_undo; 28 bool won; /* ya se mostro el cartel de 2048 */ 29 int state, sel; /* sel: boton del cartel de 2048 */ 30 int new_x, new_y; /* ficha recien aparecida (-1 ninguna) */ 31 bool flash; /* dibujar el destello */ 32 bool record; /* supero el record en esta partida */ 33 } Game; 34 35 static Game G; 36 static bool g_flash_on = true; /* opcion del menu */ 37 38 /* ------------------------------------------------------------ records y guardado */ 39 40 static void key_for(char *k, int n, const char *pre, int sz) 41 { 42 int p = str_put(k, n, 0, pre); 43 str_int(k, n, p, SIZES[sz].n); 44 } 45 46 static long best_score(int sz) { char k[8]; key_for(k, sizeof k, "r", sz); return mini_get(k, 0); } 47 static long best_tile(int sz) { char k[8]; key_for(k, sizeof k, "t", sz); return mini_get(k, 0); } 48 49 /* Partida en curso: "p <n><fichas a..><:puntos:ganada>", o "-" si no hay. */ 50 static void save_game(void) 51 { 52 char v[64]; 53 int p = 0; 54 v[p++] = (char)('0' + G.n); 55 for (int y = 0; y < G.n; y++) 56 for (int x = 0; x < G.n; x++) v[p++] = (char)('a' + G.b[y][x]); 57 v[p] = 0; 58 p = str_put(v, sizeof v, p, ":"); 59 p = str_int(v, sizeof v, p, G.score); 60 str_put(v, sizeof v, p, G.won ? ":1" : ":0"); 61 mini_set_str("p", v); 62 } 63 64 static bool has_saved(void) 65 { 66 const char *v = mini_get_str("p"); 67 return v && (v[0] == '4' || v[0] == '5'); 68 } 69 70 static bool load_game(void) 71 { 72 const char *v = mini_get_str("p"); 73 if (!v) return false; 74 int n = v[0] - '0', sz = -1; 75 for (int i = 0; i < NSIZES; i++) if (SIZES[i].n == n) sz = i; 76 if (sz < 0 || (int)strlen(v) < 1 + n * n + 2) return false; 77 memset(&G, 0, sizeof G); 78 G.sz = sz; G.n = n; 79 for (int i = 0; i < n * n; i++) { 80 int e = v[1 + i] - 'a'; 81 if (e < 0 || e > 20) return false; 82 G.b[i / n][i % n] = (uint8_t)e; 83 } 84 const char *s = v + 1 + n * n; 85 if (*s++ != ':' || !(s = str_scan_int(s, &G.score))) return false; 86 G.won = s[0] == ':' && s[1] == '1'; 87 G.new_x = G.new_y = -1; 88 return true; 89 } 90 91 static void clear_saved(void) { mini_set_str("p", "-"); } 92 93 /* ------------------------------------------------------------ reglas */ 94 95 static void spawn(void) 96 { 97 int empty[MAXN * MAXN], ne = 0; 98 for (int y = 0; y < G.n; y++) 99 for (int x = 0; x < G.n; x++) 100 if (!G.b[y][x]) empty[ne++] = y * MAXN + x; 101 if (!ne) { G.new_x = G.new_y = -1; return; } 102 int c = empty[mini_rand(ne)]; 103 G.new_x = c % MAXN; G.new_y = c / MAXN; 104 G.b[G.new_y][G.new_x] = mini_rand(10) ? 1 : 2; 105 } 106 107 static void new_game(int sz) 108 { 109 memset(&G, 0, sizeof G); 110 G.sz = sz; 111 G.n = SIZES[sz].n; 112 spawn(); 113 spawn(); 114 G.new_x = G.new_y = -1; 115 } 116 117 static bool can_move(void) 118 { 119 for (int y = 0; y < G.n; y++) 120 for (int x = 0; x < G.n; x++) { 121 if (!G.b[y][x]) return true; 122 if (x + 1 < G.n && G.b[y][x + 1] == G.b[y][x]) return true; 123 if (y + 1 < G.n && G.b[y + 1][x] == G.b[y][x]) return true; 124 } 125 return false; 126 } 127 128 static int max_exp(void) 129 { 130 int m = 0; 131 for (int y = 0; y < G.n; y++) 132 for (int x = 0; x < G.n; x++) 133 if (G.b[y][x] > m) m = G.b[y][x]; 134 return m; 135 } 136 137 /* Desliza hacia (dx, dy). Devuelve true si algo se movio. */ 138 static bool slide(int dx, int dy) 139 { 140 uint8_t old[MAXN][MAXN]; 141 long old_score = G.score; 142 bool moved = false; 143 memcpy(old, G.b, sizeof old); 144 memset(G.merged, 0, sizeof G.merged); 145 for (int line = 0; line < G.n; line++) { 146 /* celdas de la linea, empezando por la del borde hacia donde se desliza */ 147 int cx[MAXN], cy[MAXN]; 148 for (int i = 0; i < G.n; i++) { 149 int j = (dx > 0 || dy > 0) ? G.n - 1 - i : i; 150 if (dx) { cx[i] = j; cy[i] = line; } 151 else { cx[i] = line; cy[i] = j; } 152 } 153 int out = 0; 154 bool last_merged = false; 155 for (int i = 0; i < G.n; i++) { 156 int e = G.b[cy[i]][cx[i]]; 157 if (!e) continue; 158 G.b[cy[i]][cx[i]] = 0; 159 if (out > 0 && !last_merged && G.b[cy[out - 1]][cx[out - 1]] == e) { 160 G.b[cy[out - 1]][cx[out - 1]] = (uint8_t)(e + 1); 161 G.merged[cy[out - 1]][cx[out - 1]] = 1; 162 G.score += 1L << (e + 1); 163 last_merged = true; 164 } else { 165 G.b[cy[out]][cx[out]] = (uint8_t)e; 166 out++; 167 last_merged = false; 168 } 169 } 170 } 171 for (int y = 0; y < G.n && !moved; y++) 172 for (int x = 0; x < G.n; x++) 173 if (G.b[y][x] != old[y][x]) { moved = true; break; } 174 if (moved) { 175 memcpy(G.ub, old, sizeof old); 176 G.uscore = old_score; 177 G.can_undo = true; 178 } 179 return moved; 180 } 181 182 static void update_records(void) 183 { 184 char k[8]; 185 key_for(k, sizeof k, "r", G.sz); 186 if (G.score > mini_get(k, 0)) { mini_set(k, G.score); G.record = G.score > 0; } 187 key_for(k, sizeof k, "t", G.sz); 188 long tile = 1L << max_exp(); 189 if (tile > mini_get(k, 0)) mini_set(k, tile); 190 } 191 192 /* Si no quedan jugadas termina la partida; si no, la guarda. No escribe la SD. */ 193 static void check_lost(void) 194 { 195 if (can_move()) { save_game(); return; } 196 G.state = LOST; 197 char k[8]; 198 key_for(k, sizeof k, "j", G.sz); 199 mini_set(k, mini_get(k, 0) + 1); 200 clear_saved(); 201 } 202 203 static void act_move(int dx, int dy) 204 { 205 if (!slide(dx, dy)) return; 206 spawn(); 207 G.flash = g_flash_on; 208 update_records(); 209 if (!G.won && max_exp() >= 11) { G.won = true; G.state = WINBOX; G.sel = 0; save_game(); } 210 else check_lost(); 211 mini_save(); 212 } 213 214 static void act_undo(void) 215 { 216 if (!G.can_undo) return; 217 memcpy(G.b, G.ub, sizeof G.b); 218 G.score = G.uscore; 219 G.can_undo = false; 220 G.flash = false; 221 G.new_x = G.new_y = -1; 222 memset(G.merged, 0, sizeof G.merged); 223 if (G.state == LOST) { /* deshacer la jugada perdedora */ 224 char k[8]; 225 key_for(k, sizeof k, "j", G.sz); 226 mini_set(k, mini_get(k, 0) - 1); 227 } 228 G.state = PLAYING; 229 save_game(); 230 mini_save(); 231 } 232 233 /* ------------------------------------------------------------ dibujo */ 234 235 static Color tile_bg(int e) 236 { 237 static const Color C[] = { 238 RGB(44, 50, 66), /* vacia */ 239 RGB(238, 228, 218), RGB(237, 224, 200), RGB(242, 177, 121), RGB(245, 149, 99), 240 RGB(246, 124, 95), RGB(246, 94, 59), RGB(237, 207, 114), RGB(237, 204, 97), 241 RGB(237, 200, 80), RGB(237, 197, 63), RGB(237, 194, 46), /* 2048 */ 242 RGB(178, 110, 240), RGB(140, 90, 230), RGB(96, 110, 235), RGB(60, 150, 230), 243 RGB(40, 180, 190), RGB(40, 180, 120), 244 }; 245 return C[e < (int)(sizeof C / sizeof C[0]) ? e : (int)(sizeof C / sizeof C[0]) - 1]; 246 } 247 248 static Color tile_fg(int e) { return e <= 2 ? RGB(110, 100, 90) : RGB(250, 248, 242); } 249 250 /* Rectangulo con las esquinas recortadas (r = 1 o 2). */ 251 static void round_rect(Surface *s, int x, int y, int w, int h, Color c) 252 { 253 gfx_rect(s, x + 2, y, w - 4, h, c); 254 gfx_rect(s, x, y + 2, w, h - 4, c); 255 gfx_rect(s, x + 1, y + 1, w - 2, h - 2, c); 256 } 257 258 static void num_text(char *t, int n, int e) 259 { 260 str_int(t, n, 0, 1L << e); 261 } 262 263 /* Numero lo mas grande posible que entre en la ficha. */ 264 static void draw_num(Surface *s, int x, int y, int c, const char *t, Color col) 265 { 266 static const struct { const Font *f; int sc; } OPT[] = { 267 { &font_bold, 3 }, { &font_bold, 2 }, { &font_small, 3 }, { &font_small, 2 }, { &font_bold, 1 }, 268 }; 269 int nopt = (int)(sizeof OPT / sizeof OPT[0]); 270 for (int i = 0; i < nopt; i++) { 271 const Font *f = OPT[i].f; 272 int sc = OPT[i].sc, w = mini_big_w(f, sc, t), h = f->h * sc; 273 if (i < nopt - 1 && (w > c - 8 || h > c - 8)) continue; 274 /* los digitos de font_bold ocupan las filas 2..11: centrar eso */ 275 int gy = f == &font_bold ? (c - 10 * sc) / 2 - 2 * sc : (c - h) / 2; 276 mini_big_text(s, f, x + (c - w) / 2 + 1, y + gy, sc, t, col); 277 return; 278 } 279 } 280 281 static void draw_tile(Surface *s, int px, int py, int c, int e, bool is_new, bool merged) 282 { 283 if (!e) { round_rect(s, px, py, c, c, tile_bg(0)); return; } 284 if (is_new) { /* aparece: mas chica y palida */ 285 int in = c / 6; 286 round_rect(s, px, py, c, c, tile_bg(0)); 287 round_rect(s, px + in, py + in, c - 2 * in, c - 2 * in, tile_bg(e)); 288 return; 289 } 290 round_rect(s, px, py, c, c, tile_bg(e)); 291 if (merged) { /* destello: borde blanco */ 292 gfx_frame(s, px + 1, py + 1, c - 2, c - 2, C_WHITE); 293 gfx_frame(s, px + 2, py + 2, c - 4, c - 4, C_WHITE); 294 } 295 char t[12]; 296 num_text(t, sizeof t, e); 297 draw_num(s, px, py, c, t, tile_fg(e)); 298 } 299 300 static void draw_banner(Surface *s, int bx, int by, int w, int h, Color border) 301 { 302 gfx_box(s, bx, by, w, h, C_PANEL, border); 303 gfx_box(s, bx + 1, by + 1, w - 2, h - 2, C_PANEL, border); 304 } 305 306 static void paint(Surface *s, void *ctx) 307 { 308 const Size *sz = &SIZES[G.sz]; 309 int n = G.n, c = sz->cell, g = sz->gap, bw = n * c + (n + 1) * g; 310 int ox = (320 - bw) / 2, oy = TOP + (278 - bw) / 2; 311 char buf[48]; 312 int p; 313 314 gfx_rect(s, 0, 0, 320, 320, RGB(16, 20, 30)); 315 gfx_rect(s, 0, 0, 320, 24, C_PANEL); 316 p = str_put(buf, sizeof buf, 0, "Puntos "); 317 str_int(buf, sizeof buf, p, G.score); 318 gfx_text(s, &font_bold, 6, 6, buf, C_TEXT); 319 p = str_put(buf, sizeof buf, 0, "Récord "); 320 long best = best_score(G.sz); 321 str_int(buf, sizeof buf, p, best > G.score ? best : G.score); 322 gfx_text(s, &font_bold, 314 - gfx_text_w(&font_bold, buf), 6, buf, G.record ? C_ACCENT : C_TEXT); 323 gfx_text_center(s, &font_normal, 130, 6, 60, sz->name, C_ACCENT); 324 325 round_rect(s, ox, oy, bw, bw, RGB(30, 35, 48)); 326 for (int y = 0; y < n; y++) 327 for (int x = 0; x < n; x++) 328 draw_tile(s, ox + g + x * (c + g), oy + g + y * (c + g), c, G.b[y][x], 329 G.flash && x == G.new_x && y == G.new_y, G.flash && G.merged[y][x]); 330 331 if (G.state == PLAYING) { 332 gfx_text_center(s, &font_small, 0, 311, 320, 333 G.can_undo ? "Flechas mover U deshacer Esc menú" : "Flechas mover Esc menú", C_DIM); 334 return; 335 } 336 int bx = 40, w = 240; 337 if (G.state == WINBOX) { 338 int by = 120, h = 80; 339 draw_banner(s, bx, by, w, h, C_ACCENT); 340 gfx_text_center(s, &font_bold, bx, by + 10, w, "¡Llegaste a 2048!", C_ACCENT); 341 p = str_put(buf, sizeof buf, 0, "Puntos "); 342 str_int(buf, sizeof buf, p, G.score); 343 gfx_text_center(s, &font_normal, bx, by + 28, w, buf, C_TEXT); 344 static const char *const BTN[2] = { "Seguir jugando", "Menú" }; 345 int bxs[2] = { bx + 16, bx + 148 }, bws[2] = { 120, 76 }; 346 for (int i = 0; i < 2; i++) { 347 bool on = G.sel == i; 348 gfx_box(s, bxs[i], by + 50, bws[i], 20, on ? C_PANEL2 : C_PANEL, on ? C_CURSOR : C_DIM); 349 gfx_text_center(s, &font_normal, bxs[i], by + 54, bws[i], BTN[i], on ? C_CURSOR : C_TEXT); 350 } 351 return; 352 } 353 /* sin movimientos */ 354 int by = 125, h = 70; 355 draw_banner(s, bx, by, w, h, C_ERR); 356 gfx_text_center(s, &font_bold, bx, by + 8, w, "Sin movimientos", C_ERR); 357 p = str_put(buf, sizeof buf, 0, "Puntos "); 358 p = str_int(buf, sizeof buf, p, G.score); 359 if (G.record) str_put(buf, sizeof buf, p, " ¡récord!"); 360 gfx_text_center(s, &font_normal, bx, by + 26, w, buf, G.record ? C_ACCENT : C_TEXT); 361 gfx_text_center(s, &font_small, bx, by + 46, w, 362 G.can_undo ? "Enter otra U deshacer Esc menú" : "Enter otra Esc menú", C_DIM); 363 } 364 365 /* ------------------------------------------------------------ menus y bucle */ 366 367 static void art(Surface *s) 368 { 369 /* una tirita de fichas de adorno: 2 4 8 ... 128 */ 370 int c = 22, gap = 3, nt = 7, x0 = (320 - nt * c - (nt - 1) * gap) / 2, y = 56; 371 for (int i = 0; i < nt; i++) { 372 int x = x0 + i * (c + gap), e = i + 1; 373 char t[8]; 374 round_rect(s, x, y, c, c, tile_bg(e)); 375 num_text(t, sizeof t, e); 376 const Font *f = e < 4 ? &font_bold : &font_small; 377 gfx_text(s, f, x + (c - gfx_text_w(f, t)) / 2 + 1, y + (c - f->h) / 2 + 1, t, tile_fg(e)); 378 } 379 } 380 381 static const char *const SIZE_NAMES[] = { "4x4", "5x5" }; 382 static const char *const YESNO[] = { "No", "Sí" }; 383 384 /* Devuelve 1 jugar, 2 continuar; cambia *sz y la opcion de destello. */ 385 static int main_menu(int *sz) 386 { 387 static char lines[NSIZES][48]; 388 Form f; 389 form_begin(&f, "2048"); 390 if (has_saved()) form_button(&f, 2, mini_get_str("p")[0] == '5' ? "Continuar (5x5)" : "Continuar (4x4)"); 391 form_button(&f, 1, "Jugar"); 392 form_choice(&f, 3, "Tamaño", SIZE_NAMES, NSIZES, *sz); 393 form_choice(&f, 4, "Destello", YESNO, 2, g_flash_on); 394 form_label(&f, ""); 395 for (int i = 0; i < NSIZES; i++) { 396 int p = str_put(lines[i], sizeof lines[i], 0, SIZE_NAMES[i]); 397 p = str_put(lines[i], sizeof lines[i], p, ": récord "); 398 p = str_int(lines[i], sizeof lines[i], p, best_score(i)); 399 long t = best_tile(i); 400 if (t) { 401 p = str_put(lines[i], sizeof lines[i], p, ", ficha "); 402 str_int(lines[i], sizeof lines[i], p, t); 403 } 404 form_label(&f, lines[i]); 405 } 406 for (;;) { 407 int r = mini_form(&f); 408 if (r == 1 || r == 2) { 409 int nsz = form_get(&f, 3)->value; 410 bool fl = form_get(&f, 4)->value != 0; 411 if (nsz != *sz || fl != g_flash_on) { 412 *sz = nsz; g_flash_on = fl; 413 mini_set("tam", nsz); mini_set("fl", fl); mini_save(); 414 } 415 return r; 416 } 417 } 418 } 419 420 /* Devuelve true si hay que volver al menu principal. */ 421 static bool pause_menu(void) 422 { 423 Form f; 424 form_begin(&f, "Pausa"); 425 form_button(&f, 1, "Seguir"); 426 form_button(&f, 2, "Empezar de nuevo"); 427 form_button(&f, 3, "Menú principal"); 428 int r = mini_form(&f); 429 if (r == 2) { 430 new_game(G.sz); 431 save_game(); 432 mini_save(); 433 } 434 return r == 3; 435 } 436 437 void app_main(void) 438 { 439 mini_start("2048", RGB(237, 194, 46)); 440 mini_art = art; 441 int sz = (int)mini_get("tam", 0); 442 if (sz < 0 || sz >= NSIZES) sz = 0; 443 g_flash_on = mini_get("fl", 1) != 0; 444 for (;;) { 445 int r = main_menu(&sz); 446 if (r != 2 || !load_game()) { 447 new_game(sz); 448 save_game(); 449 mini_save(); 450 } 451 bool dirty = true; 452 uint32_t flash_t = 0; 453 for (;;) { 454 int k = hal_key(); 455 if (k == 0) dirty = true; 456 else if (k > 0) { 457 dirty = true; 458 G.flash = false; 459 if (G.state == WINBOX) { 460 if (k == K_LEFT || k == K_RIGHT || k == K_TAB) G.sel ^= 1; 461 else if (k == K_OK || k == ' ') { 462 if (G.sel == 1) break; 463 G.state = PLAYING; 464 check_lost(); 465 mini_save(); 466 } else if (k == K_BACK) break; 467 continue; 468 } 469 if (G.state == LOST) { 470 if (k == 'u' || k == 'U' || k == K_BKSP) act_undo(); 471 else if (k == K_OK || k == ' ') { new_game(G.sz); save_game(); mini_save(); } 472 else if (k == K_BACK) break; 473 continue; 474 } 475 switch (k) { 476 case K_LEFT: act_move(-1, 0); break; 477 case K_RIGHT: act_move(1, 0); break; 478 case K_UP: act_move(0, -1); break; 479 case K_DOWN: act_move(0, 1); break; 480 case 'u': case 'U': case K_BKSP: act_undo(); break; 481 case K_BACK: if (pause_menu()) goto menu; break; 482 } 483 if (G.flash) flash_t = hal_ms(); 484 } 485 if (G.flash && hal_ms() - flash_t >= FLASH_MS) { G.flash = false; dirty = true; } 486 if (dirty) { hal_draw(paint, 0); dirty = false; } 487 else hal_sleep(10); 488 } 489 menu:; 490 } 491 }