app.c (29713B)
1 /* app.c - la aplicacion de la PicoCalc (portable: corre en la PicoCalc y en el simulador). 2 * 3 * Modos: partida local (una PicoCalc, personas y/o bots) y cable 1v1 entre dos 4 * PicoCalc. En el cable, la "A" corre la autoridad y la "B" es cliente; hablan 5 * el mismo protocolo de lineas que el servidor (ver server/server.h), con un 6 * checksum al final de cada linea (" *XX") porque el UART no es confiable. */ 7 #include <string.h> 8 #include "hal.h" 9 #include "link.h" 10 #include "../core/host.h" 11 #include "form.h" 12 #include "../gfx/catan_gfx.h" 13 14 #define MAXMOVES 1500 15 #define LOGN 40 16 #define SAVE_MAX 32768 17 18 enum { ROLE_LOCAL, ROLE_A, ROLE_B }; 19 enum { LINK_IDLE, LINK_HELLO, LINK_JOIN, LINK_SYNCED }; 20 21 static struct { 22 int role; 23 Host host; 24 Game view; 25 Undo view_undo; 26 const Game *g; 27 uint8_t local_mask; 28 bool handoff; 29 char names[MAXP][20]; 30 Move moves[MAXMOVES]; 31 int nmoves; 32 uint32_t seed; 33 Opts opts; 34 uint8_t kinds[MAXP]; 35 char log[LOGN][80]; 36 int nlog; 37 uint32_t next_bot; 38 int link; /* B: estado de conexion */ 39 bool peer; /* A: la B esta conectada */ 40 int my_seat; 41 uint32_t link_t; 42 uint32_t ping_t, pong_t; 43 int evcount; /* B: movidas recibidas desde el HDR */ 44 int last_err; 45 bool err_pending; 46 bool dirty; 47 bool save_due; 48 bool replaying; 49 } S; 50 51 static char savebuf[SAVE_MAX]; 52 53 /* ------------------------------------------------------------ texto */ 54 55 typedef struct { char *b; int n, pos; } Sb; 56 static void sb_s(Sb *s, const char *t) { int p = str_put(s->b, s->n, s->pos, t); if (p >= 0) s->pos = p; } 57 static void sb_i(Sb *s, long v) { int p = str_int(s->b, s->n, s->pos, v); if (p >= 0) s->pos = p; } 58 static void cpy(char *d, int n, const char *s) { str_put(d, n, 0, s); } 59 60 static void add_log(const char *line) 61 { 62 if (S.nlog == LOGN) { memmove(S.log[0], S.log[1], sizeof S.log[0] * (LOGN - 1)); S.nlog--; } 63 cpy(S.log[S.nlog++], sizeof S.log[0], line); 64 S.dirty = true; 65 } 66 67 static void describe(const Game *g, const Move *m) 68 { 69 const char *names[MAXP]; 70 for (int i = 0; i < MAXP; i++) names[i] = S.names[i]; 71 if (m->type == M_REJECT) return; 72 unsigned lm = S.local_mask, viewers = lm && !(lm & (lm - 1)) ? lm : 0; /* como en la mesa */ 73 char buf[120]; 74 move_describe_for(g, m, names, viewers, buf, sizeof buf); 75 add_log(buf); 76 } 77 78 /* ------------------------------------------------------------ cable */ 79 80 /* A: mandar todo el estado a la B */ 81 static void a_attach(void) 82 { 83 char line[256]; 84 link_send("GAME CABLE 1 -"); 85 str_put(line, sizeof line, 0, "HDR "); 86 hdr_fmt(&S.host.a.g, line + 4, (int)sizeof line - 4); 87 link_send(line); 88 for (int i = 0; i < S.host.a.g.o.np; i++) { 89 Sb s = { line, sizeof line, 0 }; 90 line[0] = 0; 91 char k = S.kinds[i] == SEAT_BOT ? 'B' : S.host.a.g.p[i].neutral ? 'N' : 'P'; 92 char ks[2] = { k, 0 }; 93 sb_s(&s, "SEAT "); sb_i(&s, i); sb_s(&s, " "); sb_s(&s, ks); sb_s(&s, " 1 "); sb_s(&s, S.names[i]); 94 link_send(line); 95 } 96 for (int i = 0; i < S.nmoves; i++) { 97 Move r; 98 move_redact(&S.moves[i], 1, &r); 99 str_put(line, sizeof line, 0, "EV "); 100 move_fmt(&r, line + 3, (int)sizeof line - 3); 101 link_send(line); 102 } 103 link_send("SYNC"); 104 if (!S.peer) add_log(g_lang ? "-- other PicoCalc connected --" : "-- la otra PicoCalc se conectó --"); 105 S.peer = true; 106 } 107 108 static void a_handle(char *line) 109 { 110 if (!strncmp(line, "HELLO", 5)) { 111 const char *nm = strchr(line + 6, ' '); 112 if (nm && nm[1]) { 113 cpy(S.names[1], sizeof S.names[1], nm + 1); 114 cpy(S.host.name[1], sizeof S.host.name[1], nm + 1); 115 } 116 link_send("WELCOME 1"); 117 } else if (!strncmp(line, "JOIN", 4) || !strncmp(line, "OPEN", 4)) { 118 a_attach(); 119 } else if (!strncmp(line, "MOVE ", 5)) { 120 Move m; 121 if (!move_parse(line + 5, &m) || m.p != 1) { link_send("ERR 3 bad move"); return; } 122 int e = host_submit(&S.host, 1, &m); 123 if (e) { 124 char buf[32]; 125 Sb s = { buf, sizeof buf, 0 }; 126 buf[0] = 0; 127 sb_s(&s, "ERR "); sb_i(&s, e); sb_s(&s, " rejected"); 128 link_send(buf); 129 } else S.next_bot = hal_ms() + 600; 130 } else if (!strcmp(line, "PING")) { 131 char buf[24] = "PONG "; 132 str_int(buf, sizeof buf, 5, S.nmoves); 133 link_send(buf); 134 } 135 } 136 137 static void b_restart(void) 138 { 139 S.link = LINK_HELLO; 140 S.link_t = 0; 141 } 142 143 static void b_handle(char *line) 144 { 145 if (!strncmp(line, "WELCOME", 7)) { 146 link_send("JOIN -"); 147 S.link = LINK_JOIN; 148 S.link_t = hal_ms(); 149 } else if (!strncmp(line, "GAME ", 5)) { 150 long seat; 151 const char *p = strchr(line + 5, ' '); 152 if (p && str_scan_int(p, &seat)) S.my_seat = (int)seat; 153 } else if (!strncmp(line, "HDR ", 4)) { 154 hdr_parse(line + 4, &S.view); 155 S.view_undo.n = 0; 156 S.nlog = 0; 157 S.evcount = 0; 158 S.local_mask = (uint8_t)(1 << S.my_seat); 159 } else if (!strncmp(line, "SEAT ", 5)) { 160 long i, on; 161 const char *p = str_scan_int(line + 5, &i); 162 if (!p || i < 0 || i >= MAXP) return; 163 while (*p == ' ') p++; 164 p++; 165 p = str_scan_int(p, &on); 166 if (!p) return; 167 while (*p == ' ') p++; 168 cpy(S.names[i], sizeof S.names[i], p); 169 } else if (!strncmp(line, "EV ", 3)) { 170 Move m; 171 if (move_parse(line + 3, &m)) { game_apply_u(&S.view, &S.view_undo, &m); describe(&S.view, &m); S.evcount++; } 172 } else if (!strncmp(line, "PONG", 4)) { 173 long n; 174 S.pong_t = hal_ms(); 175 /* si la A tiene mas movidas de las que recibimos, se perdio algo: resincronizar */ 176 if (S.link == LINK_SYNCED && str_scan_int(line + 4, &n) && n != S.evcount) b_restart(); 177 } else if (!strcmp(line, "SYNC")) { 178 S.link = LINK_SYNCED; 179 S.pong_t = hal_ms(); 180 add_log(g_lang ? "-- connected by cable --" : "-- conectada por cable --"); 181 } else if (!strncmp(line, "ERR ", 4)) { 182 long e; 183 if (str_scan_int(line + 4, &e)) { S.last_err = (int)e; S.err_pending = true; } 184 } 185 S.dirty = true; 186 } 187 188 static void link_poll(void) 189 { 190 if (S.role == ROLE_LOCAL) return; 191 static char line[600]; 192 int r; 193 while ((r = link_line(line, sizeof line)) != 0) { 194 if (r < 0) { /* linea corrupta */ 195 if (S.role == ROLE_B) { S.link = LINK_HELLO; S.link_t = 0; } 196 continue; 197 } 198 if (S.role == ROLE_A) a_handle(line); else b_handle(line); 199 } 200 if (S.role == ROLE_B) { 201 uint32_t now = hal_ms(); 202 if (S.link == LINK_HELLO && now - S.link_t > 1500) { 203 link_send2("HELLO 1 ", S.names[S.my_seat][0] ? S.names[S.my_seat] : "B"); 204 S.link_t = now; 205 } else if (S.link == LINK_JOIN && now - S.link_t > 8000) b_restart(); 206 else if (S.link == LINK_SYNCED) { 207 if (now - S.ping_t > 2000) { link_send("PING"); S.ping_t = now; } 208 if (now - S.pong_t > 7000) b_restart(); /* cable desenchufado */ 209 } 210 } 211 } 212 213 /* ------------------------------------------------------------ guardado */ 214 215 static void save_game(void) 216 { 217 if (S.role == ROLE_B) return; 218 Sb s = { savebuf, SAVE_MAX, 0 }; 219 savebuf[0] = 0; 220 const Opts *o = &S.opts; 221 sb_s(&s, "CATAN-SAVE 1\nROLE "); sb_s(&s, S.role == ROLE_A ? "A" : "L"); 222 sb_s(&s, "\nSEED "); sb_i(&s, (long)S.seed); 223 sb_s(&s, "\nOPTS "); 224 long ov[7] = { o->np, o->variant2p, o->vp_target, o->friendly_robber, o->tokens_start, o->neutral_setup, o->beginner }; 225 for (int i = 0; i < 7; i++) { sb_i(&s, ov[i]); sb_s(&s, " "); } 226 sb_s(&s, "\n"); 227 for (int i = 0; i < o->np; i++) { 228 sb_s(&s, "SEAT "); sb_i(&s, i); sb_s(&s, " "); sb_i(&s, S.kinds[i]); sb_s(&s, " "); sb_s(&s, S.names[i]); sb_s(&s, "\n"); 229 } 230 char line[96]; 231 for (int i = 0; i < S.nmoves && s.pos < SAVE_MAX - 100; i++) { 232 if (move_is_auto(&S.moves[i])) continue; 233 move_fmt(&S.moves[i], line, sizeof line); 234 sb_s(&s, "M "); sb_s(&s, line); sb_s(&s, "\n"); 235 } 236 hal_store_save(savebuf, s.pos + 1); 237 } 238 239 /* ------------------------------------------------------------ partida */ 240 241 static void on_move(void *ctx, const Move *m) 242 { 243 (void)ctx; 244 if (S.nmoves < MAXMOVES) S.moves[S.nmoves++] = *m; 245 describe(&S.host.a.g, m); 246 if (S.replaying) return; 247 if (S.role == ROLE_A && S.peer) { 248 char line[128] = "EV "; 249 Move r; 250 move_redact(m, 1, &r); 251 move_fmt(&r, line + 3, (int)sizeof line - 3); 252 link_send(line); 253 } 254 if (m->type == M_END || m->type == M_WIN) S.save_due = true; 255 } 256 257 static void start_host(int role, const Opts *o, uint32_t seed, const uint8_t kinds[MAXP]) 258 { 259 char names[MAXP][20]; 260 memcpy(names, S.names, sizeof names); 261 memset(&S, 0, sizeof S); 262 link_reset(); 263 memcpy(S.names, names, sizeof names); 264 S.role = role; 265 S.seed = seed; 266 S.opts = *o; 267 memcpy(S.kinds, kinds, MAXP); 268 S.replaying = true; 269 host_init(&S.host, o, seed, kinds, on_move, 0); 270 S.replaying = false; 271 /* bots con busqueda: el RP2040 es mucho mas lento, se corta por tiempo */ 272 SearchCfg b = { .iters = 3000, .horizon = 8, .c_ucb = 0.5f, .now_ms = hal_ms, .max_ms = 1200 }; 273 S.host.bot = b; 274 for (int i = 0; i < o->np; i++) { 275 if (S.host.a.g.p[i].neutral) { 276 Sb s = { S.names[i], 20, 0 }; 277 S.names[i][0] = 0; 278 sb_s(&s, tr(S_NEUTRAL)); sb_s(&s, " "); sb_i(&s, i - 1); 279 } 280 if (S.host.kind[i] == SEAT_LOCAL) S.local_mask |= (uint8_t)(1 << i); 281 cpy(S.host.name[i], sizeof S.host.name[i], S.names[i]); 282 } 283 S.g = &S.host.a.g; 284 } 285 286 static bool load_game(void) 287 { 288 int n = hal_store_load(savebuf, SAVE_MAX - 1); 289 if (n <= 0) return false; 290 savebuf[n] = 0; 291 if (strncmp(savebuf, "CATAN-SAVE 1", 12)) return false; 292 Opts o; 293 memset(&o, 0, sizeof o); 294 uint8_t kinds[MAXP] = { 0 }; 295 long seed = 0; 296 int role = ROLE_LOCAL; 297 char *p = savebuf, *moves = 0; 298 while (*p && !moves) { 299 char *e = strchr(p, '\n'); 300 if (!e) break; 301 *e = 0; 302 long v; 303 if (!strncmp(p, "ROLE ", 5)) role = p[5] == 'A' ? ROLE_A : ROLE_LOCAL; 304 else if (!strncmp(p, "SEED ", 5)) { if (str_scan_int(p + 5, &v)) seed = v; } 305 else if (!strncmp(p, "OPTS ", 5)) { 306 const char *q = p + 5; 307 long ov[7]; 308 for (int i = 0; i < 7 && q; i++) q = str_scan_int(q, &ov[i]); 309 if (!q) return false; 310 o.np = (uint8_t)ov[0]; o.variant2p = (uint8_t)ov[1]; o.vp_target = (uint8_t)ov[2]; o.friendly_robber = (uint8_t)ov[3]; 311 o.tokens_start = (uint8_t)ov[4]; o.neutral_setup = (uint8_t)ov[5]; o.beginner = (uint8_t)ov[6]; 312 } else if (!strncmp(p, "SEAT ", 5)) { 313 long i, k; 314 const char *q = str_scan_int(p + 5, &i); 315 if (q) q = str_scan_int(q, &k); 316 if (q && i >= 0 && i < MAXP) { 317 kinds[i] = (uint8_t)k; 318 while (*q == ' ') q++; 319 cpy(S.names[i], sizeof S.names[i], q); 320 } 321 } else if (p[0] == 'M') { *e = '\n'; moves = p; break; } 322 p = e + 1; 323 } 324 if (o.np < 2 || o.np > MAXP) return false; 325 start_host(role, &o, (uint32_t)seed, kinds); 326 S.replaying = true; 327 p = moves; 328 while (p && *p) { 329 char *e = strchr(p, '\n'); 330 if (e) *e = 0; 331 Move m; 332 if (p[0] == 'M' && move_parse(p + 2, &m)) host_replay(&S.host, &m); 333 p = e ? e + 1 : 0; 334 } 335 S.replaying = false; 336 add_log(g_lang ? "-- game resumed --" : "-- partida retomada --"); 337 return true; 338 } 339 340 /* ------------------------------------------------------------ dibujo */ 341 342 typedef struct { const Game *g; const Ctrl *c; const UiInfo *ui; const Form *f; } Paint; 343 344 static void paint(Surface *s, void *ctx) 345 { 346 Paint *p = ctx; 347 if (p->f) gfx_form(s, p->f, 0); 348 else { 349 BoardGeom geo; 350 gfx_screen(s, p->g, p->c, p->ui, 0, &geo); 351 } 352 } 353 354 static int form_loop(Form *f, void (*on_change)(Form *, void *), void *ctx, bool (*tick)(void)) 355 { 356 Paint pc = { 0, 0, 0, f }; 357 if (on_change) on_change(f, ctx); 358 bool dirty = true; 359 for (;;) { 360 if (tick && tick()) return FORM_NONE; 361 if (dirty) { hal_draw(paint, &pc); dirty = false; } 362 int k = hal_key(); 363 if (k < 0) { hal_sleep(10); continue; } 364 int r = form_key(f, k); 365 if (r != FORM_NONE) return r; 366 if (on_change) on_change(f, ctx); 367 dirty = true; 368 } 369 } 370 371 /* ------------------------------------------------------------ loop de juego */ 372 373 static int submit(Move *m) 374 { 375 if (m->p < 0 || m->p >= MAXP || !((S.local_mask >> m->p) & 1)) return E_TURN; 376 if (S.role == ROLE_B) { 377 if (S.link != LINK_SYNCED) return E_TURN; 378 bool setup_road = m->type == M_ROAD && S.view.phase == PH_SETUP && !S.view.setup_sub; 379 int e = setup_road ? E_OK : game_check(&S.view, m); /* va detras del pueblo */ 380 if (e) return e; 381 char line[128] = "MOVE "; 382 move_fmt(m, line + 5, (int)sizeof line - 5); 383 link_send(line); 384 return E_OK; 385 } 386 int e = host_submit(&S.host, m->p, m); 387 if (!e) S.next_bot = hal_ms() + 600; 388 return e; 389 } 390 391 static void play(void) 392 { 393 static Ctrl c; 394 static UiInfo ui; 395 memset(&ui, 0, sizeof ui); 396 ui.layout = LAYOUT_COMPACT; 397 ctrl_init(&c, S.local_mask, S.handoff); 398 int modal = 0; 399 bool help = false; 400 uint8_t mask = S.local_mask; 401 S.dirty = true; 402 for (;;) { 403 link_poll(); 404 if (S.role == ROLE_B) S.g = &S.view; 405 if (S.local_mask != mask) { mask = S.local_mask; ctrl_init(&c, mask, S.handoff); } 406 if (S.role != ROLE_B && host_bot_pending(&S.host) && hal_ms() >= S.next_bot) { 407 host_bot_step(&S.host); 408 S.next_bot = hal_ms() + 600; 409 S.dirty = true; 410 } 411 if (S.save_due) { S.save_due = false; save_game(); } 412 for (int i = 0; i < MAXP; i++) { c.names[i] = S.names[i]; ui.names[i] = S.names[i]; } 413 if (S.role == ROLE_B && S.link != LINK_SYNCED) { 414 static Form f; 415 form_begin(&f, g_lang ? "Cable: connecting..." : "Cable: conectando..."); 416 form_label(&f, g_lang ? "Waiting for PicoCalc A (Esc: cancel)" : "Esperando a la PicoCalc A (Esc: cancelar)"); 417 Paint pc = { 0, 0, 0, &f }; 418 if (S.dirty) { hal_draw(paint, &pc); S.dirty = false; } 419 int k = hal_key(); 420 if (k == K_BACK) return; 421 if (k < 0) hal_sleep(10); 422 continue; 423 } 424 ctrl_sync(&c, S.g); 425 if (S.err_pending) { ctrl_error(&c, S.last_err); S.err_pending = false; S.dirty = true; } 426 if (S.dirty) { 427 static const char *lines[LOGN]; 428 ui.nlog = S.nlog; 429 for (int i = 0; i < S.nlog; i++) lines[i] = S.log[i]; 430 ui.log = lines; 431 ui.view_seat = c.shown; 432 ui.modal = modal; 433 ui.modal_text = g_lang ? "Back to the menu? (saved)" : "¿Volver al menú? (queda guardada)"; 434 ui.show_help = help; 435 Paint pc = { S.g, &c, &ui, 0 }; 436 hal_draw(paint, &pc); 437 S.dirty = false; 438 } 439 int k = hal_key(); 440 if (k < 0) { hal_sleep(10); continue; } 441 S.dirty = true; 442 if (k == 0) continue; /* solo redibujar */ 443 if (modal) { 444 if (k == 's' || k == 'S' || k == 'y' || k == 'Y' || k == K_OK) { save_game(); return; } 445 modal = 0; 446 continue; 447 } 448 if (help) { help = false; continue; } 449 if (k == 'q' || k == 'Q') { if (S.g->phase == PH_OVER) return; modal = 1; continue; } 450 if (S.g->phase == PH_OVER && k == K_OK) return; 451 if (k == '?') { help = true; continue; } 452 if (k == 'L') { g_lang ^= 1; c.ctx_sig = 0xFFFFFFFFu; continue; } 453 if (k == K_TAB && c.mode == CM_IDLE) { ui.show_info = !ui.show_info; continue; } 454 if (ui.show_info && k == K_BACK) { ui.show_info = false; continue; } 455 Move m; 456 if (ctrl_key(&c, k, &m)) { 457 int e = submit(&m); 458 while (ctrl_next(&c, &m)) if (!e) e = submit(&m); /* colocacion: pueblo + ruta */ 459 if (e) ctrl_error(&c, e); 460 } 461 } 462 } 463 464 /* ------------------------------------------------------------ menus */ 465 466 enum { B_BACK = 0, B_GO = 1, F_PLAYERS = 10, F_VARIANT, F_BOARD, F_HANDOFF, F_KIND = 20, F_NAME = 30, F_BOTS = 40 }; 467 static const char *CH_NUM[] = { "2", "3", "4" }; 468 static const char *YN_ES[] = { "No", "Sí" }, *YN_EN[] = { "No", "Yes" }; 469 static const char *KI_ES[] = { "Persona", "Bot" }, *KI_EN[] = { "Person", "Bot" }; 470 static const char *BO_ES[] = { "Aleatorio", "Principiante" }, *BO_EN[] = { "Random", "Beginner" }; 471 static const char *NB[] = { "0", "1", "2" }; 472 473 static void local_change(Form *f, void *ctx) 474 { 475 (void)ctx; 476 int np = form_get(f, F_PLAYERS)->value + 2; 477 form_get(f, F_VARIANT)->hidden = np != 2; 478 for (int i = 0; i < MAXP; i++) { 479 form_get(f, F_KIND + i)->hidden = i >= np; 480 form_get(f, F_NAME + i)->hidden = i >= np || form_get(f, F_KIND + i)->value == 1; 481 } 482 } 483 484 static void new_local(void) 485 { 486 static Form f; 487 bool en = g_lang; 488 form_begin(&f, en ? "Local game" : "Partida local"); 489 form_choice(&f, F_PLAYERS, en ? "Players" : "Jugadores", CH_NUM, 3, 0); 490 form_choice(&f, F_VARIANT, en ? "2p variant" : "Variante 2", en ? YN_EN : YN_ES, 2, 1); 491 for (int i = 0; i < MAXP; i++) { 492 char l[16] = ""; 493 Sb s = { l, sizeof l, 0 }; 494 sb_s(&s, en ? "Seat " : "Asiento "); sb_i(&s, i + 1); 495 form_choice(&f, F_KIND + i, l, en ? KI_EN : KI_ES, 2, i < 2 ? 0 : 1); 496 char nm[20] = ""; 497 Sb t = { nm, sizeof nm, 0 }; 498 sb_s(&t, tr(S_PLAYER)); sb_s(&t, " "); sb_i(&t, i + 1); 499 form_text(&f, F_NAME + i, en ? " name" : " nombre", nm, 12); 500 } 501 form_choice(&f, F_BOARD, en ? "Board" : "Tablero", en ? BO_EN : BO_ES, 2, 0); 502 form_choice(&f, F_HANDOFF, en ? "Hand-off" : "Pantalla de pase", en ? YN_EN : YN_ES, 2, 1); 503 form_button(&f, B_GO, en ? "Start" : "Empezar"); 504 form_button(&f, B_BACK, en ? "Back" : "Volver"); 505 f.cur = f.n - 2; 506 if (form_loop(&f, local_change, 0, 0) != B_GO) return; 507 int np = form_get(&f, F_PLAYERS)->value + 2; 508 bool v2p = np == 2 && form_get(&f, F_VARIANT)->value; 509 Opts o; 510 opts_default(&o, np, v2p); 511 o.beginner = (uint8_t)form_get(&f, F_BOARD)->value; 512 uint8_t kinds[MAXP] = { 0 }; 513 for (int i = 0; i < np; i++) { 514 bool bot = form_get(&f, F_KIND + i)->value == 1; 515 kinds[i] = bot ? SEAT_BOT : SEAT_LOCAL; 516 if (bot) { 517 Sb s = { S.names[i], 20, 0 }; 518 S.names[i][0] = 0; 519 sb_s(&s, "Bot "); sb_i(&s, i + 1); 520 } else cpy(S.names[i], 20, form_get(&f, F_NAME + i)->text); 521 } 522 start_host(ROLE_LOCAL, &o, hal_random(), kinds); 523 S.handoff = form_get(&f, F_HANDOFF)->value; 524 play(); 525 } 526 527 static void cable_change(Form *f, void *ctx) 528 { 529 (void)ctx; 530 form_get(f, F_BOTS)->hidden = form_get(f, F_VARIANT)->value == 1; 531 } 532 533 static void cable_a(void) 534 { 535 static Form f; 536 bool en = g_lang; 537 form_begin(&f, en ? "Cable: create game (A)" : "Cable: crear partida (A)"); 538 form_text(&f, F_NAME, en ? "Your name" : "Tu nombre", S.names[0][0] ? S.names[0] : "A", 12); 539 form_choice(&f, F_VARIANT, en ? "2p variant" : "Variante 2", en ? YN_EN : YN_ES, 2, 1); 540 form_choice(&f, F_BOTS, en ? "Extra bots" : "Bots extra", NB, 3, 0); 541 form_choice(&f, F_BOARD, en ? "Board" : "Tablero", en ? BO_EN : BO_ES, 2, 0); 542 form_label(&f, en ? "Cable on J703: 4-5, 5-4, 8-8 (GND)" : "Cable en J703: 4-5, 5-4, 8-8 (GND)"); 543 form_button(&f, B_GO, en ? "Create" : "Crear"); 544 form_button(&f, B_BACK, en ? "Back" : "Volver"); 545 f.cur = f.n - 2; 546 if (form_loop(&f, cable_change, 0, 0) != B_GO) return; 547 bool v2p = form_get(&f, F_VARIANT)->value; 548 int bots = v2p ? 0 : form_get(&f, F_BOTS)->value; 549 Opts o; 550 opts_default(&o, 2 + bots, v2p); 551 o.beginner = (uint8_t)form_get(&f, F_BOARD)->value; 552 uint8_t kinds[MAXP] = { SEAT_LOCAL, SEAT_REMOTE, SEAT_BOT, SEAT_BOT }; 553 cpy(S.names[0], 20, form_get(&f, F_NAME)->text); 554 cpy(S.names[1], 20, "B"); 555 for (int i = 2; i < MAXP; i++) { 556 Sb s = { S.names[i], 20, 0 }; 557 S.names[i][0] = 0; 558 sb_s(&s, "Bot "); sb_i(&s, i + 1); 559 } 560 start_host(ROLE_A, &o, hal_random(), kinds); 561 add_log(en ? "-- waiting for PicoCalc B on the cable --" : "-- esperando a la PicoCalc B por el cable --"); 562 play(); 563 } 564 565 static void cable_b(void) 566 { 567 static Form f; 568 bool en = g_lang; 569 form_begin(&f, en ? "Cable: join (B)" : "Cable: unirse (B)"); 570 form_text(&f, F_NAME, en ? "Your name" : "Tu nombre", S.names[1][0] ? S.names[1] : "B", 12); 571 form_label(&f, en ? "The other PicoCalc must create the game (A)" : "La otra PicoCalc tiene que crear la partida (A)"); 572 form_button(&f, B_GO, en ? "Connect" : "Conectar"); 573 form_button(&f, B_BACK, en ? "Back" : "Volver"); 574 f.cur = 2; 575 if (form_loop(&f, 0, 0, 0) != B_GO) return; 576 char name[20]; 577 cpy(name, sizeof name, form_get(&f, F_NAME)->text); 578 memset(&S, 0, sizeof S); 579 link_reset(); 580 S.role = ROLE_B; 581 S.my_seat = 1; 582 cpy(S.names[1], 20, name); 583 S.g = &S.view; 584 b_restart(); 585 play(); 586 } 587 588 /* ------------------------------------------------------------ prueba de bots */ 589 590 static uint32_t sim_moves; 591 static void sim_on_move(void *ctx, const Move *m) { (void)ctx; (void)m; sim_moves++; } 592 593 static const char *sim_check(const Game *g) 594 { 595 for (int r = 0; r < NRES; r++) { 596 int n = g->bank[r]; 597 for (int p = 0; p < g->o.np; p++) n += g->p[p].res[r]; 598 if (n != bank_start(&g->o)) return "recursos no conservados"; 599 } 600 int onboard[MAXP] = { 0 }, devs = g->deck_n; 601 for (int v = 0; v < game_topo(g)->nvert; v++) if (g->vown[v] >= 0) onboard[g->vown[v]]++; 602 for (int p = 0; p < g->o.np; p++) { 603 const Player *pl = &g->p[p]; 604 if (pl->roads > MAX_ROADS || pl->settles > MAX_SETTLES || pl->cities > MAX_CITIES) return "piezas fuera de rango"; 605 if (onboard[p] != (MAX_SETTLES - pl->settles) + (MAX_CITIES - pl->cities)) return "edificios inconsistentes"; 606 for (int d = 0; d < NDEV; d++) devs += pl->dev[d] + pl->dev_new[d]; 607 devs += pl->knights; 608 } 609 uint8_t cnt[NDEV]; 610 if (devs > dev_counts(&g->o, cnt)) return "mazo inconsistente"; 611 return 0; 612 } 613 614 static void line_add(char *buf, int n, const char *a, long v, const char *b) 615 { 616 int p = (int)strlen(buf); 617 p = str_put(buf, n, p, a); 618 if (v >= 0) p = str_int(buf, n, p, v); 619 if (b) str_put(buf, n, p, b); 620 } 621 622 static void hexs(char *buf, int n, uint32_t v) 623 { 624 static const char HX[] = "0123456789abcdef"; 625 int p = (int)strlen(buf); 626 for (int i = 7; i >= 0 && p < n - 1; i--) buf[p++] = HX[(v >> (4 * i)) & 15]; 627 buf[p] = 0; 628 } 629 630 static void bot_test(int games) 631 { 632 bool verbose = games <= 10; 633 hal_log_clear(); 634 char buf[96] = ""; 635 line_add(buf, sizeof buf, "=== prueba: ", games, " partidas bot contra bot ==="); 636 hal_log(buf); 637 uint32_t t_all = hal_ms(), base = hal_random(); 638 uint32_t ok[2] = { 0, 0 }, fail = 0, turns_sum[2] = { 0, 0 }, moves_sum = 0; 639 uint32_t tmin[2] = { 9999, 9999 }, tmax[2] = { 0, 0 }, wins[2][MAXP]; 640 memset(wins, 0, sizeof wins); 641 for (int i = 0; i < games; i++) { 642 int mode = i % 2; /* 0: 4 jugadores, 1: variante 2p (alternados) */ 643 if (verbose) mode = i >= games / 2; 644 Opts o; 645 opts_default(&o, mode ? 2 : 4, mode == 1); 646 uint8_t kinds[MAXP] = { SEAT_BOT, SEAT_BOT, SEAT_BOT, SEAT_BOT }; 647 uint32_t seed = base + (uint32_t)i * 2654435761u; 648 sim_moves = 0; 649 uint32_t t0 = hal_ms(); 650 host_init(&S.host, &o, seed, kinds, sim_on_move, 0); 651 Game *g = &S.host.a.g; 652 const char *err = 0; 653 int steps = 0; 654 while (g->phase != PH_OVER && g->turn < 600 && steps < 20000) { 655 if (!host_bot_step(&S.host)) { err = "nadie puede jugar"; break; } 656 steps++; 657 if ((err = sim_check(g))) break; 658 if ((steps & 255) == 0 && hal_key() == K_BACK) { hal_log("cancelado"); games = i; goto done; } 659 } 660 if (!err && g->phase != PH_OVER) err = "sin ganador (600 turnos)"; 661 if (err) { 662 fail++; 663 buf[0] = 0; 664 line_add(buf, sizeof buf, "#", i + 1, mode ? " 2p" : " 4j"); 665 line_add(buf, sizeof buf, " semilla 0x", -1, 0); 666 hexs(buf, sizeof buf, seed); 667 line_add(buf, sizeof buf, " turno ", g->turn, " ERROR: "); 668 line_add(buf, sizeof buf, "", -1, err); 669 hal_log(buf); 670 } else { 671 ok[mode]++; 672 turns_sum[mode] += g->turn; 673 moves_sum += sim_moves; 674 if (g->turn < tmin[mode]) tmin[mode] = g->turn; 675 if (g->turn > tmax[mode]) tmax[mode] = g->turn; 676 if (g->winner >= 0) wins[mode][g->winner]++; 677 } 678 if (verbose) { 679 buf[0] = 0; 680 line_add(buf, sizeof buf, "#", i + 1, mode ? " 2p" : " 4j"); 681 line_add(buf, sizeof buf, " t", g->turn, 0); 682 line_add(buf, sizeof buf, " m", (long)sim_moves, 0); 683 if (!err) line_add(buf, sizeof buf, " gana ", g->winner + 1, 0); 684 line_add(buf, sizeof buf, " ", (long)(hal_ms() - t0), err ? " ms ERROR" : " ms ok"); 685 hal_log(buf); 686 } else if ((i + 1) % 500 == 0) { 687 buf[0] = 0; 688 line_add(buf, sizeof buf, "", i + 1, " partidas, "); 689 line_add(buf, sizeof buf, "", (long)fail, " errores, "); 690 line_add(buf, sizeof buf, "", (long)((hal_ms() - t_all) / 1000), " s"); 691 hal_log(buf); 692 } 693 } 694 done: 695 buf[0] = 0; 696 line_add(buf, sizeof buf, "RESULTADO: ", (long)(ok[0] + ok[1]), " ok, "); 697 line_add(buf, sizeof buf, "", (long)fail, " con error"); 698 hal_log(buf); 699 uint32_t total = hal_ms() - t_all; 700 buf[0] = 0; 701 line_add(buf, sizeof buf, "tiempo ", (long)(total / 1000), " s, "); 702 line_add(buf, sizeof buf, "", games ? (long)(total / (uint32_t)games) : 0, " ms/partida"); 703 hal_log(buf); 704 for (int m = 0; m < 2; m++) { 705 if (!ok[m]) continue; 706 buf[0] = 0; 707 line_add(buf, sizeof buf, m ? "2p: " : "4j: ", (long)ok[m], " ok, turnos "); 708 line_add(buf, sizeof buf, "prom ", (long)(turns_sum[m] / ok[m]), 0); 709 line_add(buf, sizeof buf, " min ", (long)tmin[m], 0); 710 line_add(buf, sizeof buf, " max ", (long)tmax[m], 0); 711 hal_log(buf); 712 buf[0] = 0; 713 line_add(buf, sizeof buf, " victorias por asiento:", -1, 0); 714 for (int p = 0; p < (m ? 2 : 4); p++) line_add(buf, sizeof buf, " ", (long)wins[m][p], 0); 715 hal_log(buf); 716 } 717 if (ok[0] + ok[1]) { 718 buf[0] = 0; 719 line_add(buf, sizeof buf, "movidas por partida (prom) ", (long)(moves_sum / (ok[0] + ok[1])), 0); 720 hal_log(buf); 721 } 722 hal_log("Enter para volver"); 723 int k; 724 while ((k = hal_key()) != K_OK && k != K_BACK) hal_sleep(20); 725 memset(&S, 0, sizeof S); 726 link_reset(); 727 } 728 729 static void bot_test_menu(void) 730 { 731 static const char *N[] = { "10", "100", "1000", "10000" }; 732 static const int NV[] = { 10, 100, 1000, 10000 }; 733 static Form f; 734 bool en = g_lang; 735 form_begin(&f, en ? "Test: bot games" : "Prueba: partidas de bots"); 736 form_choice(&f, 10, en ? "Games" : "Partidas", N, 4, 0); 737 form_label(&f, en ? "~70 ms per game. Esc cancels." : "~70 ms por partida. Esc cancela."); 738 form_label(&f, en ? "Results on screen and in catan.log" : "Resultados en pantalla y en catan.log"); 739 form_button(&f, 1, en ? "Start" : "Empezar"); 740 form_button(&f, 0, en ? "Back" : "Volver"); 741 f.cur = 3; 742 if (form_loop(&f, 0, 0, 0) == 1) bot_test(NV[form_get(&f, 10)->value]); 743 } 744 745 enum { M_CONT = 1, M_LOCAL, M_CABLE_A, M_CABLE_B, M_TEST, M_LANG = 50 }; 746 static const char *LANGS[] = { "Español", "English" }; 747 748 static void lang_change(Form *f, void *ctx) 749 { 750 (void)ctx; 751 g_lang = form_get(f, M_LANG)->value; 752 } 753 754 void boot_log(const char *msg); 755 #ifdef CATAN_DEVICE 756 #define APPLOG(s) boot_log(s) 757 #else 758 #define APPLOG(s) ((void)0) 759 #endif 760 761 void app_main(void) 762 { 763 hal_init(); 764 gfx_form_backdrop = catan_form_backdrop; 765 APPLOG("8 menu principal"); 766 for (;;) { 767 static Form f; 768 bool en = g_lang; 769 int n = hal_store_load(savebuf, 16); 770 bool have = n >= 12 && !strncmp(savebuf, "CATAN-SAVE 1", 12); 771 form_begin(&f, en ? "PicoCalc Catan" : "Catan PicoCalc"); 772 form_button(&f, M_CONT, en ? "Continue saved game" : "Continuar partida guardada")->disabled = !have; 773 form_button(&f, M_LOCAL, en ? "Local game (people / bots)" : "Partida local (personas / bots)"); 774 form_button(&f, M_CABLE_A, en ? "Cable 1v1: create (A)" : "Cable 1v1: crear (A)"); 775 form_button(&f, M_CABLE_B, en ? "Cable 1v1: join (B)" : "Cable 1v1: unirse (B)"); 776 form_button(&f, M_TEST, en ? "Test: bot games" : "Prueba: partidas de bots"); 777 form_choice(&f, M_LANG, en ? "Language" : "Idioma", LANGS, 2, g_lang); 778 f.cur = have ? 0 : 1; 779 int r = form_loop(&f, lang_change, 0, 0); 780 if (r == M_CONT && load_game()) play(); 781 else if (r == M_LOCAL) new_local(); 782 else if (r == M_CABLE_A) cable_a(); 783 else if (r == M_CABLE_B) cable_b(); 784 else if (r == M_TEST) bot_test_menu(); 785 } 786 }