app.c (16042B)
1 /* app.c - la vibora (snake) para la PicoCalc. 2 * 3 * Flechas cambian la direccion (se guardan hasta 2 giros para las vueltas en U, 4 * el giro de 180 grados se ignora), Esc pausa. La partida arranca con la primera 5 * flecha. Cada manzana suma puntos, alarga la vibora y la acelera un poco. 6 * Con paredes, chocar el borde mata; sin paredes se sale por el lado opuesto. 7 * 8 * Dibujo: la cancha tiene un patron fijo y cada paso cambia pocas celdas (cabeza, 9 * cola, manzana), asi en el aparato solo se retransmiten 2-4 franjas. */ 10 #include <string.h> 11 #include "mini.h" 12 13 enum { W = 20, H = 18, CELL = 15, OX = 10, OY = 32, MAXN = W * H }; 14 15 enum { UP, RIGHT, DOWN, LEFT }; 16 static const int8_t DX[4] = { 0, 1, 0, -1 }, DY[4] = { -1, 0, 1, 0 }; 17 18 static const char *const SPEED_NAMES[] = { "Lenta", "Normal", "Rápida" }; 19 static const char *const WALL_NAMES[] = { "Sí", "No" }; 20 static const int SPEED_MS[] = { 190, 140, 95 }; /* intervalo inicial por paso */ 21 static const int SPEED_MIN[] = { 90, 70, 50 }; /* tope de aceleracion */ 22 enum { NSPEEDS = 3, POINTS = 10 }; 23 24 enum { READY, PLAYING, DEAD, FULL }; 25 26 typedef struct { 27 int speed, walls; /* opciones (walls 0: si hay paredes) */ 28 uint16_t pos[MAXN]; /* anillo de celdas y*W+x, de la cola a la cabeza */ 29 uint8_t dir[MAXN]; /* direccion con la que se entro a esa celda */ 30 int tail, len; /* indice de la cola en el anillo y largo */ 31 uint8_t occ[H][W]; 32 int grow; /* segmentos que faltan crecer */ 33 int food; /* celda de la manzana, -1 si no hay */ 34 int d; /* direccion actual */ 35 uint8_t q[2]; int nq; /* giros pendientes */ 36 int state, score, eaten; 37 int crash; /* celda contra la que choco (-1: ninguna) */ 38 int interval; /* ms por paso */ 39 uint32_t next; /* hal_ms del proximo paso */ 40 long best; 41 bool record; 42 } Game; 43 44 static Game G; 45 46 /* ------------------------------------------------------------ reglas */ 47 48 static int seg(int i) { return (G.tail + i) % MAXN; } /* i-esimo desde la cola */ 49 static int head_idx(void) { return seg(G.len - 1); } 50 51 static void rec_key(char *k, int speed, int walls) 52 { 53 k[0] = 'r'; k[1] = (char)('0' + speed); k[2] = (char)('0' + walls); k[3] = 0; 54 } 55 56 static void place_food(void) 57 { 58 int nfree = W * H - G.len; 59 G.food = -1; 60 if (nfree <= 0) return; 61 int n = mini_rand(nfree); 62 for (int c = 0; c < W * H; c++) 63 if (!G.occ[c / W][c % W] && n-- == 0) { G.food = c; return; } 64 } 65 66 static void new_game(int speed, int walls) 67 { 68 memset(&G, 0, sizeof G); 69 G.speed = speed; 70 G.walls = walls; 71 G.len = 4; 72 for (int i = 0; i < G.len; i++) { 73 int x = W / 2 - 3 + i, y = H / 2; 74 G.pos[i] = (uint16_t)(y * W + x); 75 G.dir[i] = RIGHT; 76 G.occ[y][x] = 1; 77 } 78 G.d = RIGHT; 79 G.crash = -1; 80 G.interval = SPEED_MS[speed]; 81 char k[4]; 82 rec_key(k, speed, walls); 83 G.best = mini_get(k, 0); 84 place_food(); 85 } 86 87 static void end_game(int state) 88 { 89 G.state = state; 90 char k[8]; 91 rec_key(k, G.speed, G.walls); 92 if (G.score > mini_get(k, 0)) { mini_set(k, G.score); G.best = G.score; G.record = true; } 93 k[0] = 'j'; 94 mini_set(k, mini_get(k, 0) + 1); 95 mini_save(); 96 } 97 98 /* Flecha: se encola si no repite ni invierte el ultimo giro pendiente. */ 99 static void turn(int nd) 100 { 101 int last = G.nq ? G.q[G.nq - 1] : G.d; 102 if (nd == last || nd == (last + 2) % 4 || G.nq >= 2) return; 103 G.q[G.nq++] = (uint8_t)nd; 104 } 105 106 static void step(void) 107 { 108 if (G.nq) { 109 G.d = G.q[0]; 110 G.q[0] = G.q[1]; 111 G.nq--; 112 } 113 int h = G.pos[head_idx()], x = h % W + DX[G.d], y = h / W + DY[G.d]; 114 if (x < 0 || y < 0 || x >= W || y >= H) { 115 if (!G.walls) { end_game(DEAD); return; } 116 x = (x + W) % W; 117 y = (y + H) % H; 118 } 119 int nc = y * W + x; 120 bool eat = nc == G.food; 121 if (eat) G.grow++; 122 /* la cola se corre en este mismo paso, asi que se puede entrar donde estaba */ 123 if (G.occ[y][x] && (G.grow || nc != G.pos[G.tail])) { G.crash = nc; end_game(DEAD); return; } 124 if (G.grow) G.grow--; 125 else { 126 int t = G.pos[G.tail]; 127 G.occ[t / W][t % W] = 0; 128 G.tail = (G.tail + 1) % MAXN; 129 G.len--; 130 } 131 int i = seg(G.len); 132 G.pos[i] = (uint16_t)nc; 133 G.dir[i] = (uint8_t)G.d; 134 G.occ[y][x] = 1; 135 G.len++; 136 if (eat) { 137 G.eaten++; 138 G.score += POINTS + G.speed * 5; 139 G.interval -= 3; 140 if (G.interval < SPEED_MIN[G.speed]) G.interval = SPEED_MIN[G.speed]; 141 place_food(); 142 if (G.food < 0) end_game(FULL); 143 } 144 } 145 146 /* ------------------------------------------------------------ dibujo */ 147 148 #define C_BG RGB(16, 20, 30) 149 #define C_FIELD1 RGB(26, 40, 32) 150 #define C_FIELD2 RGB(30, 46, 37) 151 #define C_SKIN RGB(90, 200, 80) 152 #define C_SKIN2 RGB(70, 176, 64) 153 #define C_EDGE RGB(20, 70, 30) 154 #define C_HEAD RGB(120, 220, 96) 155 156 static void cell_xy(int c, int *px, int *py) { *px = OX + (c % W) * CELL; *py = OY + (c / W) * CELL; } 157 158 static void draw_apple(Surface *s, int px, int py) 159 { 160 int cx = px + 7, cy = py + 8; 161 gfx_disc(s, cx, cy, 5, RGB(210, 36, 40)); 162 gfx_disc(s, cx - 2, cy - 2, 1, RGB(255, 150, 140)); 163 gfx_rect(s, cx, py + 1, 1, 3, RGB(110, 70, 30)); 164 int32_t xs[3] = { (cx + 1) * 16, (cx + 5) * 16, (cx + 2) * 16 }; 165 int32_t ys[3] = { (py + 3) * 16, (py + 1) * 16, (py + 5) * 16 }; 166 gfx_poly16(s, xs, ys, 3, RGB(80, 190, 70)); 167 } 168 169 /* Tramo del cuerpo de media celda que sale del centro (cx, cy) hacia d. */ 170 static void bridge(Surface *s, int cx, int cy, int d, int r, Color c) 171 { 172 int h = CELL / 2; 173 switch (d) { 174 case UP: gfx_rect(s, cx - r, cy - h, 2 * r + 1, h, c); break; 175 case DOWN: gfx_rect(s, cx - r, cy + 1, 2 * r + 1, h, c); break; 176 case LEFT: gfx_rect(s, cx - h, cy - r, h, 2 * r + 1, c); break; 177 case RIGHT: gfx_rect(s, cx + 1, cy - r, h, 2 * r + 1, c); break; 178 } 179 } 180 181 /* Radio del segmento i (desde la cola): la cola se afina. */ 182 static int seg_r(int i) 183 { 184 if (i == G.len - 1) return 6; 185 return i == 0 ? 4 : i == 1 ? 5 : 6; 186 } 187 188 /* Un segmento: disco en el centro y puentes hacia sus vecinos. pass 0: borde oscuro. */ 189 static void draw_seg(Surface *s, int i, int pass) 190 { 191 int k = seg(i), px, py; 192 cell_xy(G.pos[k], &px, &py); 193 int cx = px + 7, cy = py + 7, r = seg_r(i) + (pass ? 0 : 1); 194 /* puentes un poco mas finos y oscuros que los discos: se ven los segmentos */ 195 int rb = pass ? r - 1 : r; 196 Color cb = pass ? C_SKIN2 : C_EDGE; 197 if (i < G.len - 1) bridge(s, cx, cy, G.dir[seg(i + 1)], rb, cb); /* hacia la cabeza */ 198 if (i > 0) bridge(s, cx, cy, (G.dir[k] + 2) % 4, rb, cb); /* hacia la cola */ 199 Color c = pass == 0 ? C_EDGE : C_SKIN; 200 if (i == G.len - 1 && pass) c = G.state == DEAD ? RGB(230, 100, 70) : C_HEAD; 201 gfx_disc(s, cx, cy, r, c); 202 if (pass && r >= 5 && i < G.len - 1) gfx_disc(s, cx - 2, cy - 2, 1, RGB(140, 230, 120)); 203 } 204 205 static void draw_eyes(Surface *s) 206 { 207 int px, py; 208 cell_xy(G.pos[head_idx()], &px, &py); 209 int cx = px + 7, cy = py + 7, d = G.d; 210 /* ojos adelante y a los costados de la direccion */ 211 int fx = DX[d] * 2, fy = DY[d] * 2, sx = -DY[d] * 3, sy = DX[d] * 3; 212 for (int e = -1; e <= 1; e += 2) { 213 int ex = cx + fx + e * sx, ey = cy + fy + e * sy; 214 if (G.state == DEAD) { 215 gfx_line(s, ex - 1, ey - 1, ex + 1, ey + 1, 1, C_BLACK); 216 gfx_line(s, ex + 1, ey - 1, ex - 1, ey + 1, 1, C_BLACK); 217 } else { 218 gfx_disc(s, ex, ey, 2, C_WHITE); 219 gfx_rect(s, ex + DX[d] - (DX[d] < 0), ey + DY[d] - (DY[d] < 0), 1 + !DX[d], 1 + !DY[d], C_BLACK); 220 } 221 } 222 } 223 224 static bool rows_hit(Surface *s, int c) 225 { 226 int py = OY + (c / W) * CELL; 227 return py + CELL + 1 >= s->hy0 && py - 1 < s->hy1; 228 } 229 230 static void paint_field(Surface *s) 231 { 232 Color frame = G.walls ? RGB(70, 80, 100) : RGB(150, 110, 60); 233 gfx_rect(s, OX - 3, OY - 3, W * CELL + 6, H * CELL + 6, frame); 234 if (G.walls) { /* sin paredes: borde punteado (se sale por el otro lado) */ 235 for (int x = OX; x < OX + W * CELL; x += 10) { 236 gfx_rect(s, x + 5, OY - 3, 5, 3, C_BG); 237 gfx_rect(s, x + 5, OY + H * CELL, 5, 3, C_BG); 238 } 239 for (int y = OY; y < OY + H * CELL; y += 10) { 240 gfx_rect(s, OX - 3, y + 5, 3, 5, C_BG); 241 gfx_rect(s, OX + W * CELL, y + 5, 3, 5, C_BG); 242 } 243 } 244 int y0 = (s->hy0 - OY) / CELL, y1 = (s->hy1 - OY) / CELL; 245 if (y0 < 0) y0 = 0; 246 if (y1 > H - 1) y1 = H - 1; 247 for (int y = y0; y <= y1; y++) 248 for (int x = 0; x < W; x++) 249 gfx_rect(s, OX + x * CELL, OY + y * CELL, CELL, CELL, (x + y) & 1 ? C_FIELD2 : C_FIELD1); 250 } 251 252 static void paint(Surface *s, void *ctx) 253 { 254 char buf[40]; 255 gfx_rect(s, 0, 0, 320, 320, C_BG); 256 if (s->hy0 < 24) { 257 gfx_rect(s, 0, 0, 320, 24, C_PANEL); 258 draw_apple(s, 6, 4); 259 str_int(buf, sizeof buf, 0, G.score); 260 gfx_text(s, &font_bold, 26, 6, buf, C_TEXT); 261 int p = str_put(buf, sizeof buf, 0, SPEED_NAMES[G.speed]); 262 str_put(buf, sizeof buf, p, G.walls ? ", sin paredes" : ", con paredes"); 263 gfx_text_center(s, &font_normal, 70, 6, 180, buf, C_ACCENT); 264 p = str_put(buf, sizeof buf, 0, "Récord "); 265 str_int(buf, sizeof buf, p, G.best > G.score ? G.best : G.score); 266 gfx_text(s, &font_normal, 314 - gfx_text_w(&font_normal, buf), 6, buf, C_DIM); 267 } 268 paint_field(s); 269 270 if (G.food >= 0 && rows_hit(s, G.food)) { 271 int px, py; 272 cell_xy(G.food, &px, &py); 273 draw_apple(s, px, py); 274 } 275 if (G.crash >= 0 && rows_hit(s, G.crash)) { 276 int px, py; 277 cell_xy(G.crash, &px, &py); 278 gfx_rect(s, px, py, CELL, CELL, RGB(150, 40, 36)); 279 } 280 for (int pass = 0; pass < 2; pass++) 281 for (int i = 0; i < G.len; i++) 282 if (rows_hit(s, G.pos[seg(i)])) draw_seg(s, i, pass); 283 draw_eyes(s); 284 285 if (G.state == READY) { 286 gfx_box(s, 70, 196, 180, 22, C_PANEL, C_ACCENT); 287 gfx_text_center(s, &font_normal, 70, 201, 180, "Apretá una flecha", C_TEXT); 288 } 289 if (G.state == READY || G.state == PLAYING) { 290 gfx_text_center(s, &font_small, 0, 311, 320, "Flechas girar Esc pausa", C_DIM); 291 return; 292 } 293 /* cartel de fin */ 294 bool won = G.state == FULL; 295 Color bc = won || G.record ? C_OK : C_ERR; 296 int bx = 40, by = 130, w = 240, h = 60; 297 gfx_box(s, bx, by, w, h, C_PANEL, bc); 298 gfx_box(s, bx + 1, by + 1, w - 2, h - 2, C_PANEL, bc); 299 gfx_text_center(s, &font_bold, bx, by + 8, w, won ? "¡Llenaste la cancha!" : "¡Chocaste!", bc); 300 int p = str_put(buf, sizeof buf, 0, "Puntos "); 301 p = str_int(buf, sizeof buf, p, G.score); 302 if (G.record) str_put(buf, sizeof buf, p, " ¡récord!"); 303 else { 304 p = str_put(buf, sizeof buf, p, " récord "); 305 str_int(buf, sizeof buf, p, G.best); 306 } 307 gfx_text_center(s, &font_normal, bx, by + 26, w, buf, G.record ? C_ACCENT : C_TEXT); 308 gfx_text_center(s, &font_small, bx, by + 45, w, "Enter otra Esc menú", C_DIM); 309 } 310 311 /* ------------------------------------------------------------ menus y bucle */ 312 313 static void art(Surface *s) 314 { 315 /* una viborita yendo hacia una manzana */ 316 static const uint8_t body[] = { 0, 1, 2, 3, 4, 5 }; 317 int y = 64, x0 = 110; 318 for (int pass = 0; pass < 2; pass++) 319 for (unsigned i = 0; i < sizeof body; i++) { 320 int cx = x0 + body[i] * CELL + 7, cy = y + 7 + (i == 1 || i == 2 ? -3 : i == 4 ? 3 : 0); 321 int r = (i == 0 ? 4 : i == 1 ? 5 : 6) + (pass ? 0 : 1); 322 Color c = pass == 0 ? C_EDGE : i == sizeof body - 1 ? C_HEAD : (i & 1) ? C_SKIN : C_SKIN2; 323 gfx_disc(s, cx, cy, r, c); 324 if (i + 1 < sizeof body) { 325 int ny = y + 7 + (i + 1 == 1 || i + 1 == 2 ? -3 : i + 1 == 4 ? 3 : 0); 326 gfx_line(s, cx, cy, cx + CELL, ny, 2 * r, c); 327 } 328 } 329 int hx = x0 + 5 * CELL + 7, hy = y + 7; 330 for (int e = -1; e <= 1; e += 2) { 331 gfx_disc(s, hx + 2, hy + e * 3, 2, C_WHITE); 332 gfx_rect(s, hx + 3, hy + e * 3, 1, 1, C_BLACK); 333 } 334 draw_apple(s, x0 + 7 * CELL, y); 335 } 336 337 /* Tabla de records: una fila por paredes, una columna por velocidad (letra fija de 6 px). */ 338 static void menu_labels(Form *f) 339 { 340 static char lines[3][48]; 341 str_put(lines[0], sizeof lines[0], 0, "Récords Lenta Normal Rápida"); 342 for (int w = 0; w < 2; w++) { 343 char *l = lines[w + 1]; 344 int p = str_put(l, sizeof lines[0], 0, w ? "Sin paredes" : "Con paredes"); 345 for (int i = 0; i < NSPEEDS; i++) { 346 char k[4], n[12]; 347 rec_key(k, i, w); 348 int nl = str_int(n, sizeof n, 0, mini_get(k, 0)); 349 for (int pad = (i ? 7 : 8) - nl; pad > 0; pad--) p = str_put(l, sizeof lines[0], p, " "); 350 p = str_put(l, sizeof lines[0], p, n); 351 } 352 } 353 for (int i = 0; i < 3; i++) form_label(f, lines[i]); 354 } 355 356 static void main_menu(int *speed, int *walls) 357 { 358 Form f; 359 form_begin(&f, "Víbora"); 360 form_button(&f, 1, "Jugar"); 361 form_choice(&f, 2, "Velocidad", SPEED_NAMES, NSPEEDS, *speed); 362 form_choice(&f, 3, "Paredes", WALL_NAMES, 2, *walls); 363 form_label(&f, ""); 364 menu_labels(&f); 365 while (mini_form(&f) != 1) {} 366 *speed = form_get(&f, 2)->value; 367 *walls = form_get(&f, 3)->value; 368 } 369 370 /* Devuelve true si hay que volver al menu principal. */ 371 static bool pause_menu(void) 372 { 373 Form f; 374 form_begin(&f, "Pausa"); 375 form_button(&f, 1, "Seguir"); 376 form_button(&f, 2, "Empezar de nuevo"); 377 form_button(&f, 3, "Menú principal"); 378 int r = mini_form(&f); 379 G.next = hal_ms() + (uint32_t)G.interval; /* la pausa no cuenta */ 380 if (r == 2) new_game(G.speed, G.walls); 381 return r == 3; 382 } 383 384 void app_main(void) 385 { 386 mini_start("Víbora", RGB(110, 220, 90)); 387 mini_art = art; 388 int speed = (int)mini_get("vel", 1), walls = (int)mini_get("par", 0); 389 if (speed < 0 || speed >= NSPEEDS) speed = 1; 390 if (walls < 0 || walls > 1) walls = 0; 391 for (;;) { 392 int os = speed, ow = walls; 393 main_menu(&speed, &walls); 394 if (os != speed || ow != walls) { mini_set("vel", speed); mini_set("par", walls); mini_save(); } 395 new_game(speed, walls); 396 bool dirty = true; 397 for (;;) { 398 int k = hal_key(); 399 if (k == 0) dirty = true; 400 else if (k > 0) { 401 if (G.state == DEAD || G.state == FULL) { 402 if (k == K_OK || k == ' ') { new_game(speed, walls); dirty = true; } 403 else if (k == K_BACK) break; 404 continue; 405 } 406 int nd = k == K_UP ? UP : k == K_RIGHT ? RIGHT : k == K_DOWN ? DOWN : k == K_LEFT ? LEFT : -1; 407 if (nd >= 0) { 408 if (G.state == READY) { /* la primera flecha arranca */ 409 if (nd == (G.d + 2) % 4) nd = G.d; 410 G.state = PLAYING; 411 G.next = hal_ms(); 412 dirty = true; 413 } 414 turn(nd); 415 } else if (k == K_BACK) { 416 if (pause_menu()) break; 417 dirty = true; 418 } 419 } 420 if (G.state == PLAYING) { 421 uint32_t now = hal_ms(); 422 if ((int32_t)(now - G.next) >= 0) { 423 step(); 424 dirty = true; 425 G.next += (uint32_t)G.interval; 426 /* si el dibujo nos atraso mas de un paso, no acumular deuda */ 427 if ((int32_t)(now - G.next) > G.interval) G.next = now + (uint32_t)G.interval; 428 } 429 } 430 if (dirty) { hal_draw(paint, 0); dirty = false; } 431 else hal_sleep(5); 432 } 433 } 434 }