app.c (35692B)
1 /* app.c - menus de la app, ajustes y el loop de juego (comun a TUI y GUI). */ 2 #include <ctype.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include <time.h> 7 #include <unistd.h> 8 #include "app.h" 9 #include "net.h" 10 #include "../server/export.h" 11 #include "paths.h" 12 #include "version.h" 13 #ifdef __EMSCRIPTEN__ 14 #include <emscripten.h> 15 #endif 16 17 /* ------------------------------------------------------------------ ajustes */ 18 19 void settings_load(Settings *st) 20 { 21 memset(st, 0, sizeof *st); 22 st->handoff = 1; 23 st->bot_delay = 450; 24 st->bot_level = 2; 25 st->layout = LAYOUT_WIDE; 26 st->scale = 2; 27 #ifdef __EMSCRIPTEN__ 28 const char *u = 0; /* emscripten inventa USER=web_user */ 29 #else 30 const char *u = getenv("USER"); 31 if (!u || !*u) u = getenv("USERNAME"); /* Windows */ 32 #endif 33 snprintf(st->name, sizeof st->name, "%s", u && *u ? u : "Yo"); 34 FILE *f = fopen(config_path(), "r"); 35 if (!f) return; 36 char line[256]; 37 while (fgets(line, sizeof line, f)) { 38 line[strcspn(line, "\r\n")] = 0; 39 char *eq = strchr(line, '='); 40 if (!eq) continue; 41 *eq = 0; 42 const char *k = line, *v = eq + 1; 43 if (!strcmp(k, "lang")) st->lang = !strcmp(v, "en"); 44 else if (!strcmp(k, "handoff")) st->handoff = atoi(v); 45 else if (!strcmp(k, "bot_delay")) st->bot_delay = atoi(v); 46 else if (!strcmp(k, "bot_level")) st->bot_level = atoi(v); 47 else if (!strcmp(k, "bot_hard")) st->bot_level = atoi(v) ? 2 : 0; /* config vieja */ 48 else if (!strcmp(k, "layout")) st->layout = !strcmp(v, "compact") ? LAYOUT_COMPACT : LAYOUT_WIDE; 49 else if (!strcmp(k, "scale")) st->scale = atoi(v); 50 else if (!strcmp(k, "name")) snprintf(st->name, sizeof st->name, "%s", v); 51 else if (!strcmp(k, "server")) snprintf(st->server, sizeof st->server, "%s", v); 52 else if (!strncmp(k, "color", 5) && k[5] >= '1' && k[5] < '1' + MAXP && !k[6]) snprintf(st->color[k[5] - '1'], 8, "%.7s", v); 53 } 54 fclose(f); 55 if (st->scale < 1 || st->scale > 6) st->scale = 2; 56 if (!strcmp(st->server, "localhost:7373") || !strcmp(st->server, "lu3dhn.xyz:7373")) 57 st->server[0] = 0; /* defaults viejos: ahora es catan.lu3dhn.xyz */ 58 } 59 60 /* "#rrggbb" o "rrggbb" -> color; false si no es un hex valido */ 61 static bool parse_hex(const char *t, Color *out) 62 { 63 if (*t == '#') t++; 64 unsigned v = 0; 65 for (int i = 0; i < 6; i++) { 66 char ch = (char)tolower((unsigned char)t[i]); 67 if (ch >= '0' && ch <= '9') v = v * 16 + (unsigned)(ch - '0'); 68 else if (ch >= 'a' && ch <= 'f') v = v * 16 + (unsigned)(ch - 'a' + 10); 69 else return false; 70 } 71 if (t[6]) return false; 72 *out = RGB((v >> 16) & 255, (v >> 8) & 255, v & 255); 73 return true; 74 } 75 76 static void hex_of(Color c, char out[8]) 77 { 78 snprintf(out, 8, "#%02x%02x%02x", (c >> 11) << 3, ((c >> 5) & 63) << 2, (c & 31) << 3); 79 } 80 81 void settings_apply_colors(const Settings *st) 82 { 83 for (int i = 0; i < MAXP; i++) { 84 Color c; 85 PLAYER_COLOR[i] = st->color[i][0] && parse_hex(st->color[i], &c) ? c : PLAYER_COLOR_DEF[i]; 86 } 87 } 88 89 void settings_save(const Settings *st) 90 { 91 FILE *f = fopen(config_path(), "w"); 92 if (!f) return; 93 fprintf(f, "lang=%s\nhandoff=%d\nbot_delay=%d\nbot_level=%d\nlayout=%s\nscale=%d\nname=%s\nserver=%s\n", 94 st->lang ? "en" : "es", st->handoff, st->bot_delay, st->bot_level, st->layout == LAYOUT_COMPACT ? "compact" : "wide", 95 st->scale, st->name, st->server); 96 for (int i = 0; i < MAXP; i++) if (st->color[i][0]) fprintf(f, "color%d=%s\n", i + 1, st->color[i]); 97 fclose(f); 98 paths_flush(); 99 } 100 101 uint64_t fresh_seed(void) 102 { 103 return ((uint64_t)time(0) << 20) ^ (uint64_t)now_ms() ^ ((uint64_t)getpid() << 40); 104 } 105 106 /* ------------------------------------------------------------ mapas */ 107 108 const char *const SCEN_CH_ES[SCEN__COUNT] = { "Clásico", "Navegantes: Nuevas costas", "Navegantes: Cuatro islas", 109 "Navegantes: Archipiélago" }; 110 const char *const SCEN_CH_EN[SCEN__COUNT] = { "Classic", "Seafarers: New shores", "Seafarers: Four islands", 111 "Seafarers: Archipelago" }; 112 113 const char *const *map_choices(int family) 114 { 115 static char lbl[NMAPS + 1][48]; 116 static const char *ch[NMAPS + 1]; 117 snprintf(lbl[0], sizeof lbl[0], "%s", g_lang ? "Random" : "Al azar"); 118 for (int k = 0; k < NMAPS; k++) snprintf(lbl[k + 1], sizeof lbl[k + 1], "%d · %s", k + 1, board_name(family * NMAPS + k)); 119 for (int k = 0; k <= NMAPS; k++) ch[k] = lbl[k]; 120 return ch; 121 } 122 123 static const char *family_name(int family) 124 { 125 static const char *const ES[BF__COUNT] = { "Clásico", "Tablero 5-6", "Navegantes: Nuevas costas", 126 "Navegantes: Cuatro islas", "Navegantes: Archipiélago" }; 127 static const char *const EN[BF__COUNT] = { "Classic", "5-6 board", "Seafarers: New shores", 128 "Seafarers: Four islands", "Seafarers: Archipelago" }; 129 return (g_lang ? EN : ES)[family < BF__COUNT ? family : 0]; 130 } 131 132 bool map_preview(Frontend *fe, int layout, Opts *o, bool random_map, const char *const names[MAXP]) 133 { 134 static Game g; 135 Rng r; 136 rng_seed(&r, fresh_seed(), 0x3A9); 137 if (random_map) o->map = (uint8_t)rng_below(&r, NMAPS); 138 o->bseed = 1 + rng_below(&r, 0x7FFFFFFE); 139 o->beginner = 0; 140 int family = opts_family(o); 141 char title[160]; 142 const char *help[4]; 143 for (;;) { 144 Rng unused; 145 rng_seed(&unused, 1, 1); 146 board_make(&g, o, &unused); 147 snprintf(title, sizeof title, "%s · %s %d/%d: %s", family_name(family), g_lang ? "map" : "mapa", o->map + 1, NMAPS, 148 board_name(opts_board(o))); 149 help[0] = g_lang ? "This is the board. Before playing you can:" : "Así queda el tablero. Antes de jugar podés:"; 150 help[1] = g_lang ? " R: reshuffle (same map, other terrains and numbers)" : " R: otro reparto (mismo mapa, otros terrenos y números)"; 151 help[2] = g_lang ? " left/right arrows: change map (all play-tested to be fair)" : " flechas izq/der: cambiar de mapa (todos probados para que sean parejos)"; 152 help[3] = g_lang ? " Enter: play this one · Esc: back" : " Enter: jugar este · Esc: volver"; 153 Ctrl c; 154 ctrl_init(&c, 0, false); 155 for (int i = 0; i < MAXP; i++) c.names[i] = names[i]; 156 ctrl_sync(&c, &g); 157 UiInfo ui; 158 memset(&ui, 0, sizeof ui); 159 for (int i = 0; i < MAXP; i++) ui.names[i] = names[i]; 160 ui.log = help; 161 ui.nlog = 4; 162 int log_max = 0; 163 ui.log_max = &log_max; 164 ui.view_seat = -1; 165 ui.layout = layout; 166 ui.touch = fe->touch; 167 ui.preview = title; 168 fe->draw_game(fe, &g, &c, &ui); 169 InEvent ev; 170 if (!fe->wait(fe, 1000, 0, 0, &ev)) continue; 171 if (ev.type == EV_QUIT) return false; 172 int k = ev.type == EV_KEY ? ev.key : 0; 173 if (ev.type == EV_CLICK) { 174 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 175 if (h && h->kind == PK_APP && h->id >= 20 && h->id <= 24) { 176 static const int K[5] = { K_LEFT, 'r', K_RIGHT, K_OK, K_BACK }; 177 k = K[h->id - 20]; 178 } else if (h && h->kind == PK_APP && (h->id == 7 || h->id == 0)) k = K_BACK; 179 } 180 if (k == K_OK) return true; 181 if (k == K_BACK || k == 'q' || k == 'Q') return false; 182 if (k == 'r' || k == 'R') o->bseed = 1 + rng_below(&r, 0x7FFFFFFE); 183 if (k == K_LEFT) o->map = (uint8_t)((o->map + NMAPS - 1) % NMAPS); 184 if (k == K_RIGHT) o->map = (uint8_t)((o->map + 1) % NMAPS); 185 } 186 } 187 188 /* ------------------------------------------------------------ formularios */ 189 190 int run_form(Frontend *fe, Form *f, void (*on_change)(Form *, void *), void *ctx) 191 { 192 if (on_change) on_change(f, ctx); 193 for (;;) { 194 fe->draw_form(fe, f); 195 InEvent ev; 196 if (!fe->wait(fe, 1000, 0, 0, &ev)) continue; 197 int r = FORM_NONE; 198 if (ev.type == EV_QUIT) return FORM_CANCEL; 199 if (ev.type == EV_KEY) r = form_key(f, ev.key); 200 else if (ev.type == EV_CLICK) { 201 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 202 if (h && h->kind == PK_APP) { 203 r = form_click(f, h->id); 204 Field *x = h->id >= 0 && h->id < f->n ? &f->f[h->id] : 0; 205 if (r == FORM_NONE && x && x->type == F_TEXT && !x->disabled && fe->text_input) { 206 char tmp[sizeof x->text]; /* la web: cuadro de texto del navegador */ 207 snprintf(tmp, sizeof tmp, "%s", x->text); 208 /* el tope en bytes va al cuadro, que corta en borde de caracter utf-8 */ 209 int cap = x->maxlen && x->maxlen + 1 < (int)sizeof tmp ? x->maxlen + 1 : (int)sizeof tmp; 210 if (fe->text_input(fe, x->label, tmp, cap) > 0) snprintf(x->text, sizeof x->text, "%s", tmp); 211 } 212 } 213 } 214 if (r != FORM_NONE) return r; 215 f->msg[0] = 0; 216 if (on_change) on_change(f, ctx); 217 } 218 } 219 220 /* ------------------------------------------------------------ loop de juego */ 221 222 /* F5: la partida como texto (cabecera, tablero y el registro tal como lo viste) en 223 * data_dir()/exportes. La version completa, con todo, la guarda el servidor. */ 224 static void put_file(void *ctx, const char *line) { fputs(line, ctx); fputc('\n', ctx); } 225 226 static bool export_now(Session *s, char *out, int n) 227 { 228 char dir[600], path[700], when[32]; 229 snprintf(dir, sizeof dir, "%s/exportes", data_dir()); 230 mkdirs(dir); 231 time_t now = time(0); 232 strftime(when, sizeof when, "%Y%m%d-%H%M%S", localtime(&now)); 233 const char *code = s->net ? net_code(s->net) : 0; 234 snprintf(path, sizeof path, "%s/%s%s%s.txt", dir, code ? code : "local", code ? "-" : "-", when); 235 FILE *f = fopen(path, "w"); 236 if (!f) return false; 237 const char *names[MAXP]; 238 char kinds[MAXP + 1] = ""; 239 for (int i = 0; i < MAXP; i++) { 240 names[i] = s->names[i]; 241 kinds[i] = s->local && s->host.kind[i] == SEAT_BOT ? 'B' : 'P'; 242 } 243 export_header(s->g, code, names, kinds, put_file, f); 244 fprintf(f, "\n## %s\n", g_lang ? "Log (as you saw it)" : "Registro (como lo viste)"); 245 for (int i = 0; i < s->nlog; i++) { 246 const char *t = s->log[i]; 247 if (*t >= '\1' && *t <= '\4' && t[1]) t += 2; /* sin las marcas de color */ 248 fprintf(f, "%s\n", t); 249 } 250 fclose(f); 251 paths_flush(); 252 snprintf(out, (size_t)n, "%s", path); 253 return true; 254 } 255 256 static void submit(Session *s, Ctrl *c, Move *m) 257 { 258 int e = sess_submit(s, m); 259 Move q; 260 while (ctrl_next(c, &q)) { /* lo confirmado junto (colocacion: pueblo + ruta) */ 261 if (!e) e = sess_submit(s, &q); 262 } 263 if (e) ctrl_error(c, e); 264 } 265 266 static bool picking(const Ctrl *c) 267 { 268 return c->mode == CM_VERT || c->mode == CM_EDGE || c->mode == CM_HEX || c->mode == CM_MENU || c->mode == CM_CARDS; 269 } 270 271 /* "Jugar otra": las mismas opciones, asientos y nombres de la partida local que 272 * termino, con otro reparto del tablero (y otros dados). */ 273 void app_play_again(Session *s, Settings *st) 274 { 275 Opts o = s->g->o; 276 uint8_t kinds[MAXP]; 277 char names[MAXP][20]; 278 for (int i = 0; i < MAXP; i++) { 279 kinds[i] = s->host.kind[i] == SEAT_REMOTE ? SEAT_LOCAL : s->host.kind[i]; 280 snprintf(names[i], sizeof names[i], "%s", s->names[i] ? s->names[i] : ""); 281 } 282 Rng r; 283 rng_seed(&r, fresh_seed(), 0x3A9); 284 if (!o.beginner) o.bseed = 1 + rng_below(&r, 0x7FFFFFFE); /* otro reparto del mismo mapa */ 285 o.first = OPTS_FIRST_RANDOM; /* y otro que empieza */ 286 sess_close(s); 287 sess_new_local(s, &o, fresh_seed(), kinds, names); 288 s->handoff = st->handoff; 289 sess_set_bots(s, st->bot_level); 290 } 291 292 int app_play(Frontend *fe, Session *s, Settings *st) 293 { 294 Ctrl c; 295 ctrl_init(&c, s->local_mask, s->handoff); 296 UiInfo ui; 297 memset(&ui, 0, sizeof ui); 298 int modal = 0; 299 bool help = false; 300 s->bot_delay_ms = st->bot_delay; 301 bool typing = false; /* escribiendo en el chat */ 302 char chat[201] = ""; 303 int log_max = 0; 304 ui.log_max = &log_max; 305 306 for (;;) { 307 sess_poll(s); 308 for (int i = 0; i < MAXP; i++) { c.names[i] = s->names[i]; ui.names[i] = s->names[i]; } 309 ctrl_sync(&c, s->g); 310 if (s->err_pending) { ctrl_error(&c, s->last_err); s->err_pending = false; } 311 ui.nlog = s->nlog; 312 ui.log = (const char *const *)s->log; 313 ui.chat_input = chat; /* la linea de chat siempre se ve */ 314 ui.chat_focus = typing; 315 ui.status = s->status; 316 ui.view_seat = c.shown; 317 ui.layout = st->layout; 318 ui.modal = modal; 319 ui.modal_text = g_lang ? "Quit the game? (it is saved)" : "¿Salir de la partida? (queda guardada)"; 320 if (!s->local) ui.modal_text = g_lang ? "Leave the game? (you can rejoin)" : "¿Salir? (podés volver a entrar)"; 321 ui.show_help = help; 322 ui.touch = fe->touch; 323 ui.can_free = s->local && !s->net && s->save_path[0] && s->g->winner >= 0; 324 ui.can_again = s->g->phase == PH_OVER; /* local: app_play_again; online: AGAIN al servidor */ 325 fe->draw_game(fe, s->g, &c, &ui); 326 327 int fds[16]; 328 int nf = sess_fds(s, fds, 16); 329 InEvent ev; 330 if (!fe->wait(fe, sess_timeout(s), fds, nf, &ev)) continue; 331 if (ev.type == EV_QUIT) return 0; 332 Move m; 333 if (ev.type == EV_WHEEL) { 334 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 335 if (!fe->is_gui || (h && h->kind == PK_APP && h->id == 3)) { 336 ui.log_scroll += ev.key * 3; 337 if (ui.log_scroll > log_max) ui.log_scroll = log_max; 338 if (ui.log_scroll < 0) ui.log_scroll = 0; 339 } 340 continue; 341 } 342 if (ev.type == EV_KEY && (ev.key == K_PGUP || ev.key == K_PGDN || ev.key == K_END)) { 343 int k = ev.key; /* el registro se mueve tambien escribiendo */ 344 ui.log_scroll = k == K_END ? 0 : ui.log_scroll + (k == K_PGUP ? 8 : -8); 345 if (ui.log_scroll > log_max) ui.log_scroll = log_max; 346 if (ui.log_scroll < 0) ui.log_scroll = 0; 347 continue; 348 } 349 if (ev.type == EV_CLICK && !modal && !help) { /* botones de la pantalla tactil: valen como teclas */ 350 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 351 if (h && h->kind == PK_APP) { 352 int k = h->id == 7 ? K_BACK : h->id == 9 ? K_OK : h->id == 10 ? 'c' : h->id == 14 ? 'a' : 0; 353 if (k) { ev.type = EV_KEY; ev.key = k; typing = false; } 354 } 355 } 356 if (ev.type == EV_KEY && ev.key == K_F1 + 4 && !modal) { /* F5: exportar */ 357 char path[700]; 358 bool ok = export_now(s, path, sizeof path); 359 snprintf(c.msg, sizeof c.msg, "%s %s", ok ? (g_lang ? "Exported:" : "Exportada:") : (g_lang ? "Could not export" : "No se pudo exportar"), 360 ok ? path : ""); 361 c.msg_err = !ok; 362 continue; 363 } 364 if (ev.type == EV_KEY && ev.key == K_F1 + 1 && !modal) { /* F2: chat */ 365 typing = !typing; 366 help = false; 367 continue; 368 } 369 if (ev.type == EV_KEY && typing) { 370 int k = ev.key; 371 size_t len = strlen(chat); 372 c.msg[0] = 0; 373 if (k == K_BACK) typing = false; /* lo escrito queda en la caja */ 374 else if (k == K_OK) { 375 const char *err = sess_say(s, chat); 376 if (err) { snprintf(c.msg, sizeof c.msg, "%s", err); c.msg_err = true; } 377 else { typing = false; chat[0] = 0; ui.log_scroll = 0; } 378 } else if (k == K_BKSP) { 379 while (len > 0 && ((unsigned char)chat[len - 1] & 0xC0) == 0x80) len--; /* utf-8 */ 380 if (len > 0) len--; 381 chat[len] = 0; 382 } else if (k >= 32 && k < 256 && k != 127 && len + 2 < sizeof chat) { 383 if (k < 128) chat[len++] = (char)k; 384 else { chat[len++] = (char)(0xC0 | (k >> 6)); chat[len++] = (char)(0x80 | (k & 0x3F)); } 385 chat[len] = 0; 386 } 387 continue; 388 } 389 if (ev.type == EV_KEY) { 390 int k = ev.key; 391 if (modal) { 392 if (modal == 1 && (k == 's' || k == 'S' || k == 'y' || k == 'Y' || k == K_OK)) return 0; 393 modal = 0; 394 continue; 395 } 396 if (help) { help = false; continue; } 397 if (k == 'q' || k == 'Q') { 398 if (s->g->phase == PH_OVER) return 0; 399 modal = 1; 400 continue; 401 } 402 if (s->g->phase == PH_OVER && (k == 'c' || k == 'C') && ui.can_free) { 403 char err[160]; 404 if (!sess_go_free(s, err, sizeof err)) { sess_add_log(s, err); continue; } 405 sess_add_log(s, g_lang ? "— free play: keep going —" : "— juego libre: a seguir —"); 406 ctrl_init(&c, s->local_mask, s->handoff); 407 continue; 408 } 409 if (s->g->phase == PH_OVER && (k == 'a' || k == 'A') && ui.can_again) return 1; /* jugar otra */ 410 if (s->g->phase == PH_OVER && (k == K_OK || k == K_BACK)) return 0; 411 if (k == '?') { help = true; continue; } 412 413 if (k == 'L') { 414 g_lang ^= 1; 415 st->lang = g_lang; 416 settings_save(st); 417 c.ctx_sig = 0xFFFFFFFFu; 418 continue; 419 } 420 if (k == K_TAB && c.mode == CM_IDLE) { ui.show_info = !ui.show_info; continue; } 421 if (picking(&c)) { 422 if (k == 'h') k = K_LEFT; 423 else if (k == 'l') k = K_RIGHT; 424 else if (k == 'j') k = K_DOWN; 425 else if (k == 'k') k = K_UP; 426 } 427 if (ctrl_key(&c, k, &m)) submit(s, &c, &m); 428 } else if (ev.type == EV_CLICK) { 429 if (modal) { 430 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 431 int id = h && h->kind == PK_APP ? h->id : -1; 432 if (modal == 1 && id == 8) return 0; /* "si" del dialogo de salir */ 433 if (modal == 2 && id == 1) { modal = 0; help = true; continue; } /* menu de la app (tactil) */ 434 if (modal == 2 && id == 0) { modal = 1; continue; } 435 modal = 0; 436 continue; 437 } 438 if (help) { help = false; continue; } 439 const Hit *h = hit_find(&fe->hits, ev.x, ev.y); 440 bool app = h && h->kind == PK_APP; 441 if (app && h->id == 6) { /* caja de chat */ 442 char tmp[sizeof chat]; 443 snprintf(tmp, sizeof tmp, "%s", chat); 444 int ti = fe->text_input ? fe->text_input(fe, "Chat", tmp, sizeof tmp) : -1; /* la web: cuadro propio */ 445 if (ti > 0) { 446 c.msg[0] = 0; 447 const char *err = tmp[0] ? sess_say(s, tmp) : 0; 448 if (err) { snprintf(c.msg, sizeof c.msg, "%s", err); c.msg_err = true; } 449 else { chat[0] = 0; ui.log_scroll = 0; } 450 } else if (ti < 0) typing = true; /* sin cuadro: se escribe con el teclado; cancelado: nada */ 451 continue; 452 } 453 if (app && (h->id == 4 || h->id == 5)) { /* barra de scroll */ 454 ui.log_scroll += h->id == 4 ? 8 : -8; 455 if (ui.log_scroll > log_max) ui.log_scroll = log_max; 456 if (ui.log_scroll < 0) ui.log_scroll = 0; 457 continue; 458 } 459 typing = false; /* click en otro lado: el teclado vuelve al juego */ 460 if (!h) continue; 461 if (h->kind == PK_BOARD) { 462 int px = ev.x - fe->board_ox; 463 int py = (ev.y - fe->board_oy) * fe->board_sy + fe->board_sy / 2; 464 int id = gfx_board_hit(&fe->geom, &c, px, py); 465 if (id < 0) continue; 466 int kind = c.mode == CM_VERT ? PK_VERT : c.mode == CM_EDGE ? PK_EDGE : PK_HEX; 467 if (ctrl_pick(&c, kind, id, &m)) submit(s, &c, &m); 468 } else if (h->kind == PK_APP) { 469 if (h->id == 0) modal = 1; 470 else if (h->id == 1) help = true; 471 else if (h->id == 2) ui.show_info = !ui.show_info; 472 else if (h->id == 12) modal = 2; /* "menu" de la barra (tactil) */ 473 } else if (ctrl_pick(&c, h->kind, h->id, &m)) { 474 submit(s, &c, &m); 475 } 476 } 477 } 478 } 479 480 /* ------------------------------------------------------------ nueva partida */ 481 482 enum { B_BACK = 0, B_START = 1, F_PLAYERS = 10, F_VARIANT, F_BOARD, F_HANDOFF, F_FIRST, F_FREE, F_LEVEL, F_SCEN, F_MAP, 483 F_KIND = 20, F_NAME = 30 }; 484 485 static const char *CH_NUM[] = { "2", "3", "4", "5", "6" }; /* 5-6: tablero grande, en pareja */ 486 static const char *CH_YESNO_ES[] = { "No", "Sí" }, *CH_YESNO_EN[] = { "No", "Yes" }; 487 static const char *CH_KIND_ES[] = { "Humano", "Bot" }, *CH_KIND_EN[] = { "Human", "Bot" }; 488 static const char *CH_LEVEL_ES[] = { "Fáciles", "Medios", "Difíciles" }, *CH_LEVEL_EN[] = { "Easy", "Medium", "Hard" }; 489 static const char *CH_BOARD_ES[] = { "Aleatorio", "Principiante" }, *CH_BOARD_EN[] = { "Random", "Beginner" }; 490 static const char *CH_FREE_ES[] = { "A 10 puntos", "Libre (seguir al ganar)" }, *CH_FREE_EN[] = { "First to 10", "Free play (keep going)" }; 491 static const char *CH_FIRST_ES[] = { "Por turnos", "Humanos primero" }, *CH_FIRST_EN[] = { "Snake order", "Humans first" }; 492 493 static void newgame_change(Form *f, void *ctx) 494 { 495 (void)ctx; 496 int players = form_get(f, F_PLAYERS)->value + 2; 497 Field *var = form_get(f, F_VARIANT); 498 var->hidden = players != 2; 499 bool human = false, bot = false; 500 for (int i = 0; i < MAXP; i++) { 501 bool b = form_get(f, F_KIND + i)->value == 1; 502 if (i < players) { human |= !b; bot |= b; } 503 form_get(f, F_KIND + i)->hidden = i >= players; 504 form_get(f, F_NAME + i)->hidden = i >= players || b; /* los bots se llaman "Bot N" */ 505 if (form_get(f, F_KIND + i)->value == 1 && !form_get(f, F_NAME + i)->text[0]) 506 snprintf(form_get(f, F_NAME + i)->text, 20, "Bot %d", i + 1); 507 } 508 form_get(f, F_FIRST)->hidden = !(human && bot); /* solo tiene sentido contra bots */ 509 Field *scen = form_get(f, F_SCEN); /* Navegantes: 3-4 jugadores, sin la variante */ 510 scen->hidden = players < 3 || players > 4; 511 if (scen->hidden) scen->value = 0; 512 form_get(f, F_BOARD)->hidden = players > 4 || scen->value; /* el de principiante es solo el clasico */ 513 Opts o; /* el campo de mapa, con los de esa familia */ 514 opts_default(&o, players, false); 515 opts_scenario(&o, scen->value); 516 Field *map = form_get(f, F_MAP); 517 map->choices = map_choices(opts_family(&o)); 518 map->hidden = !form_get(f, F_BOARD)->hidden && form_get(f, F_BOARD)->value == 1; /* principiante: fijo */ 519 form_get(f, F_LEVEL)->hidden = !bot; 520 } 521 522 static bool new_game(Frontend *fe, Settings *st, Session *s) 523 { 524 Form f; 525 bool en = g_lang; 526 form_begin(&f, en ? "New local game" : "Nueva partida local"); 527 form_choice(&f, F_PLAYERS, en ? "Players" : "Jugadores", CH_NUM, MAXP - 1, 2); 528 form_choice(&f, F_VARIANT, en ? "2-player variant" : "Variante para 2", en ? CH_YESNO_EN : CH_YESNO_ES, 2, 1); 529 for (int i = 0; i < MAXP; i++) { 530 char lbl[32]; 531 snprintf(lbl, sizeof lbl, "%s %d", en ? "Seat" : "Asiento", i + 1); 532 form_choice(&f, F_KIND + i, lbl, en ? CH_KIND_EN : CH_KIND_ES, 2, i == 0 ? 0 : 1); 533 char nm[20]; 534 if (i == 0) snprintf(nm, sizeof nm, "%s", st->name); 535 else snprintf(nm, sizeof nm, "Bot %d", i + 1); 536 form_text(&f, F_NAME + i, en ? " name" : " nombre", nm, 16); 537 } 538 form_choice(&f, F_LEVEL, "Bots", en ? CH_LEVEL_EN : CH_LEVEL_ES, 3, st->bot_level < 0 ? 0 : st->bot_level > 2 ? 2 : st->bot_level); 539 form_choice(&f, F_SCEN, en ? "Scenario" : "Escenario", en ? SCEN_CH_EN : SCEN_CH_ES, SCEN__COUNT, 0); 540 form_choice(&f, F_BOARD, en ? "Board" : "Tablero", en ? CH_BOARD_EN : CH_BOARD_ES, 2, 0); 541 form_choice(&f, F_MAP, en ? "Map" : "Mapa", map_choices(BF_CLASSIC), NMAPS + 1, 1); 542 form_choice(&f, F_FIRST, en ? "Initial placement" : "Colocación inicial", en ? CH_FIRST_EN : CH_FIRST_ES, 2, 1); 543 form_choice(&f, F_FREE, en ? "End" : "Fin", en ? CH_FREE_EN : CH_FREE_ES, 2, 0); 544 form_choice(&f, F_HANDOFF, en ? "Hand-off screen" : "Pantalla de pase", en ? CH_YESNO_EN : CH_YESNO_ES, 2, st->handoff); 545 form_button(&f, B_START, en ? "Start" : "Empezar"); 546 form_button(&f, B_BACK, en ? "Back" : "Volver"); 547 f.cur = f.n - 2; 548 549 for (;;) { 550 int r = run_form(fe, &f, newgame_change, 0); 551 if (r != B_START) return false; 552 int players = form_get(&f, F_PLAYERS)->value + 2; 553 bool v2p = players == 2 && form_get(&f, F_VARIANT)->value; 554 Opts o; 555 opts_default(&o, players, v2p); 556 o.beginner = (uint8_t)(!form_get(&f, F_BOARD)->hidden && form_get(&f, F_BOARD)->value); 557 o.freeplay = (uint8_t)form_get(&f, F_FREE)->value; 558 if (!form_get(&f, F_SCEN)->hidden && form_get(&f, F_SCEN)->value) opts_scenario(&o, form_get(&f, F_SCEN)->value); 559 uint8_t kinds[MAXP] = { 0 }; 560 char names[MAXP][20]; 561 memset(names, 0, sizeof names); 562 for (int i = 0; i < players; i++) { 563 kinds[i] = form_get(&f, F_KIND + i)->value ? SEAT_BOT : SEAT_LOCAL; 564 snprintf(names[i], sizeof names[i], "%s", form_get(&f, F_NAME + i)->text); 565 } 566 if (v2p) { kinds[2] = kinds[3] = SEAT_NEUTRAL; } 567 if (!o.beginner) { /* mirar el tablero, cambiarlo si no gusta */ 568 int mv = form_get(&f, F_MAP)->value; 569 o.map = (uint8_t)(mv > 0 ? mv - 1 : 0); 570 const char *nm[MAXP]; 571 for (int i = 0; i < MAXP; i++) nm[i] = names[i]; 572 if (!map_preview(fe, st->layout, &o, mv == 0, nm)) continue; 573 } 574 /* humanos primero: ponen sus dos pueblos y despues eligen los bots */ 575 if (!form_get(&f, F_FIRST)->hidden && form_get(&f, F_FIRST)->value) 576 for (int i = 0; i < players; i++) 577 if (kinds[i] == SEAT_LOCAL) o.setup_first |= (uint8_t)(1 << i); 578 if (kinds[0] == SEAT_LOCAL && names[0][0]) { snprintf(st->name, sizeof st->name, "%s", names[0]); } 579 st->handoff = form_get(&f, F_HANDOFF)->value; 580 st->bot_level = form_get(&f, F_LEVEL)->value; 581 settings_save(st); 582 o.first = OPTS_FIRST_RANDOM; /* quien empieza se sortea */ 583 sess_new_local(s, &o, fresh_seed(), kinds, names); 584 s->handoff = st->handoff; 585 sess_set_bots(s, st->bot_level); 586 return true; 587 } 588 } 589 590 /* ------------------------------------------------------------ menu principal */ 591 592 enum { M_QUIT = 0, M_NEW = 1, M_CONTINUE, M_ONLINE, M_LAN_HOST, M_LAN_JOIN, M_COLORS, M_DL_WIN, M_DL_LINUX, M_HOWTO, 593 M_LANG = 50, M_NAME, M_SERVER }; 594 595 /* ------------------------------------------------------------ colores */ 596 597 enum { K_BACKC = 0, K_SAVE = 1, K_RESET = 2, K_COL = 10 }; 598 599 static void colors_form(Frontend *fe, Settings *st) 600 { 601 static const char *NAME_ES[] = { "rojo", "azul", "verde", "naranja", "blanco", "marrón" }, 602 *NAME_EN[] = { "red", "blue", "green", "orange", "white", "brown" }; 603 bool en = g_lang; 604 Form f; 605 form_begin(&f, en ? "Player colors" : "Colores de los jugadores"); 606 for (int i = 0; i < MAXP; i++) { 607 char lbl[48], cur[8]; 608 snprintf(lbl, sizeof lbl, "%s %d (%s)", en ? "Seat" : "Asiento", i + 1, en ? NAME_EN[i] : NAME_ES[i]); 609 hex_of(PLAYER_COLOR[i], cur); 610 form_text(&f, K_COL + i, lbl, cur, 7); 611 } 612 form_label(&f, en ? "Hex code like #d62828. Only on this computer." : "Código hex, por ejemplo #d62828. Vale solo en esta compu."); 613 form_button(&f, K_SAVE, en ? "Save" : "Guardar"); 614 form_button(&f, K_RESET, en ? "Back to Catan's colors" : "Volver a los colores de Catan"); 615 form_button(&f, K_BACKC, en ? "Back" : "Volver"); 616 for (;;) { 617 int r = run_form(fe, &f, 0, 0); 618 if (r == K_RESET) { 619 for (int i = 0; i < MAXP; i++) { 620 st->color[i][0] = 0; 621 hex_of(PLAYER_COLOR_DEF[i], form_get(&f, K_COL + i)->text); 622 } 623 settings_save(st); 624 settings_apply_colors(st); 625 form_msg(&f, en ? "Original colors restored" : "Volvieron los colores originales", false); 626 continue; 627 } 628 if (r != K_SAVE) return; 629 bool ok = true; 630 for (int i = 0; i < MAXP && ok; i++) { 631 Color c; 632 if (!parse_hex(form_get(&f, K_COL + i)->text, &c)) { 633 char m[96]; 634 snprintf(m, sizeof m, en ? "Seat %d: not a hex color (#rrggbb)" : "Asiento %d: no es un color hex (#rrggbb)", i + 1); 635 form_msg(&f, m, true); 636 ok = false; 637 } 638 } 639 if (!ok) continue; 640 for (int i = 0; i < MAXP; i++) { 641 Color c; 642 parse_hex(form_get(&f, K_COL + i)->text, &c); 643 if (c == PLAYER_COLOR_DEF[i]) st->color[i][0] = 0; 644 else hex_of(c, st->color[i]); 645 } 646 settings_save(st); 647 settings_apply_colors(st); 648 return; 649 } 650 } 651 652 static void menu_change(Form *f, void *ctx) 653 { 654 Settings *st = ctx; 655 int lang = form_get(f, M_LANG)->value; 656 if (lang != g_lang) { 657 g_lang = lang; 658 st->lang = lang; 659 settings_save(st); 660 } 661 } 662 663 static bool lan_host_form(Frontend *fe, Settings *st) 664 { 665 Form f; 666 bool en = g_lang; 667 form_begin(&f, en ? "Host a LAN game" : "Crear partida en red local"); 668 form_text(&f, 1, en ? "Port" : "Puerto", "7373", 6); 669 form_label(&f, en ? "Others join with: catan join <your-ip> <port>" : "Los demás entran con: catan join <tu-ip> <puerto>"); 670 form_button(&f, 10, en ? "Create" : "Crear"); 671 form_button(&f, 0, en ? "Back" : "Volver"); 672 if (run_form(fe, &f, 0, 0) != 10) return false; 673 return lan_host(fe, st, atoi(form_get(&f, 1)->text)); 674 } 675 676 static bool lan_join_form(Frontend *fe, Settings *st) 677 { 678 Form f; 679 bool en = g_lang; 680 form_begin(&f, en ? "Join a LAN game" : "Unirse a partida en red local"); 681 form_text(&f, 1, en ? "Host" : "Host / IP", "", 60); 682 form_text(&f, 2, en ? "Port" : "Puerto", "7373", 6); 683 form_button(&f, 10, en ? "Join" : "Unirse"); 684 form_button(&f, 0, en ? "Back" : "Volver"); 685 if (run_form(fe, &f, 0, 0) != 10) return false; 686 return lan_join(fe, st, form_get(&f, 1)->text, atoi(form_get(&f, 2)->text)); 687 } 688 689 #ifdef __EMSCRIPTEN__ 690 /* el "leeme" de las descargas */ 691 static void howto_form(Frontend *fe) 692 { 693 Form f; 694 bool en = g_lang; 695 char t[64]; 696 snprintf(t, sizeof t, "%s %s", en ? "Catan for your computer, version" : "Catan para la compu, versión", APP_VERSION); 697 form_begin(&f, t); 698 if (en) { 699 form_label(&f, "Windows: download catan.exe and double-click it."); 700 form_label(&f, "If Windows warns: More info > Run anyway."); 701 form_label(&f, "Linux: download catan-linux, then in a terminal:"); 702 form_label(&f, " chmod +x catan-linux"); 703 form_label(&f, " ./catan-linux --gui (window; without --gui: terminal)"); 704 form_label(&f, "Online uses the same server as this page:"); 705 form_label(&f, "web, Windows and Linux play together. Use the"); 706 form_label(&f, "same version (shown in the main menu title)."); 707 } else { 708 form_label(&f, "Windows: bajá catan.exe y abrilo con doble click."); 709 form_label(&f, "Si Windows avisa: Más información > Ejecutar."); 710 form_label(&f, "Linux: bajá catan-linux y en una terminal:"); 711 form_label(&f, " chmod +x catan-linux"); 712 form_label(&f, " ./catan-linux --gui (ventana; sin --gui: terminal)"); 713 form_label(&f, "Online es el mismo servidor que esta página:"); 714 form_label(&f, "web, Windows y Linux juegan juntos. Conviene"); 715 form_label(&f, "la misma versión (está en el título del menú)."); 716 } 717 form_button(&f, 0, en ? "Back" : "Volver"); 718 run_form(fe, &f, 0, 0); 719 } 720 #endif 721 722 void app_menu(Frontend *fe, Settings *st) 723 { 724 static const char *LANGS[] = { "Español", "English" }; 725 for (;;) { 726 Form f; 727 bool en = g_lang; 728 char last[512]; 729 bool have_save = latest_save(last, sizeof last); 730 char title[64]; 731 snprintf(title, sizeof title, "%s · %s %s", en ? "Main menu" : "Menú principal", en ? "version" : "versión", APP_VERSION); 732 form_begin(&f, title); 733 form_button(&f, M_NEW, en ? "New local game (hotseat / bots)" : "Nueva partida local (misma compu / bots)"); 734 form_button(&f, M_CONTINUE, en ? "Continue last game" : "Continuar la última partida")->disabled = !have_save; 735 form_button(&f, M_ONLINE, en ? "Play online (server)" : "Jugar online (servidor)"); 736 #ifndef __EMSCRIPTEN__ /* la web no abre sockets de red local */ 737 form_button(&f, M_LAN_HOST, en ? "LAN: host a game" : "Red local: crear partida"); 738 form_button(&f, M_LAN_JOIN, en ? "LAN: join a game" : "Red local: unirse"); 739 #endif 740 form_button(&f, M_COLORS, en ? "Player colors" : "Colores de los jugadores"); 741 form_choice(&f, M_LANG, en ? "Language" : "Idioma", LANGS, 2, g_lang); 742 #ifndef __EMSCRIPTEN__ /* en la web "salir" es cerrar la pestana */ 743 form_button(&f, M_QUIT, en ? "Quit" : "Salir"); 744 #else /* la web ofrece bajar el juego para la compu */ 745 form_button(&f, M_DL_WIN, en ? "Download for Windows (catan.exe)" : "Descargar para Windows (catan.exe)"); 746 form_button(&f, M_DL_LINUX, en ? "Download for Linux" : "Descargar para Linux"); 747 form_button(&f, M_HOWTO, en ? "How to install (read me)" : "Cómo instalarlo (leeme)"); 748 #endif 749 int r = run_form(fe, &f, menu_change, st); 750 static Session s; 751 switch (r) { 752 case M_NEW: 753 if (new_game(fe, st, &s)) { 754 while (app_play(fe, &s, st) == 1) app_play_again(&s, st); 755 sess_close(&s); 756 } 757 break; 758 case M_CONTINUE: { 759 char err[128] = ""; 760 if (have_save && sess_resume(&s, last, err, sizeof err)) { 761 s.handoff = st->handoff; 762 sess_set_bots(&s, st->bot_level); 763 while (app_play(fe, &s, st) == 1) app_play_again(&s, st); 764 sess_close(&s); 765 } 766 break; 767 } 768 case M_ONLINE: online_menu(fe, st, st->server); break; 769 case M_COLORS: colors_form(fe, st); break; 770 case M_LAN_HOST: lan_host_form(fe, st); break; 771 case M_LAN_JOIN: lan_join_form(fe, st); break; 772 case M_QUIT: return; 773 #ifdef __EMSCRIPTEN__ 774 case M_DL_WIN: emscripten_run_script("window.open('descargas/catan.exe', '_blank')"); break; 775 case M_DL_LINUX: emscripten_run_script("window.open('descargas/catan-linux', '_blank')"); break; 776 case M_HOWTO: howto_form(fe); break; 777 #endif 778 case FORM_CANCEL: 779 #ifndef __EMSCRIPTEN__ 780 return; 781 #endif 782 break; 783 } 784 } 785 }