juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

commit d1227d473999bf4f03aa0ab913b097f9c6862e93
parent 3528f3271236ebbbf4fce5f5f2612df62d8579b6
Author: Francisco Armando Rosmino <franciscorosmino@rosmino.com.ar>
Date:   Sat,  3 Oct 2026 13:30:06 -0300

Cartas explicadas en la ayuda y 'jugar otra' tambien online (AGAIN)

- Ayuda (GUI y TUI) con que hace cada carta de desarrollo, y la descripcion al
  lado de cada carta en el menu de jugar carta (pedido de felox).
- AGAIN en el servidor: otra partida como la actual (mismas opciones y bots,
  la misma gente en los mismos asientos con su llave, los libres siguen libres,
  otro reparto); al que la pide le llega como un CREATE y a los demas les
  aparece en 'Mis partidas'. GameModule.again (Catan: bseed en 0 para que
  cambie el tablero). El cliente online manda AGAIN al elegir 'jugar otra'.
- CLAUDE.md: orden nuevo de ADMIN.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
MCLAUDE.md | 2+-
Mcatan/core/ctrl.c | 4+++-
Mcatan/core/strings.h | 6++++++
Mcatan/gfx/screen.c | 12+++++++++++-
Mcatan/linux/app.c | 2+-
Mcatan/linux/online.c | 8++++++--
Mcatan/linux/tui.c | 14+++++++++++++-
Mcatan/server/catan_game.c | 12++++++++++++
Mcomun/server/srv.c | 64++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mcomun/server/srv.h | 6++++++
10 files changed, 111 insertions(+), 19 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md @@ -70,7 +70,7 @@ Al correr pruebas o el binario, aislar `XDG_CONFIG_HOME`/`XDG_DATA_HOME` en un d - **GUI ancha**: lienzo lógico de 640x360 (16:9) con escala **entera** (`gk_open_fluid` en `comun/linux/guikit.c`: 2x en 720p, 3x en 1080p; un factor no entero deja la letra despareja). Pensada para 720p/1080p (los amigos de Pancho juegan en Windows con `catan.exe`). Fuentes en `comun/gfx/font.c`, generadas por `comun/tools/mkfont.py` desde `comun/tools/fonts/`: Terminus 6x12 (normal, y engrosada a 7x12 como negrita) y Spleen 5x8 (chica, con los acentos del castellano agregados). - **`linux/`**: cliente. `frontend.h` define la interfaz TUI (`tui.c`, sobre `comun/linux/tuikit`; colores opacos propios — el usuario no quiere depender de la transparencia de su terminal) / GUI (`gui_sdl.c`, sobre `guikit`). `app.c` menús y bucle, `session.c` guardado, `net.c`/`online.c` red (sobre `netline`). - **Web/PWA** (`make web`, `web/`): el mismo cliente Linux (sin TUI ni LAN, `#ifdef __EMSCRIPTEN__`) compilado con `-sASYNCIFY`: los bucles bloqueantes y el `poll()` de red ceden al navegador solos; los sockets BSD van por un WebSocket cuya URL fija pone `web/shell.html` (`/ws` de la misma página, o `?ws=ws://localhost:7373` para probar). `comun/server/ws.c` hace que `catan-server` acepte WebSocket en el mismo puerto (detecta `GET ` en el primer paquete; `-DSRV_NO_WS` lo saca); Caddy en el VPS hace TLS y proxy de `/ws`. Datos en `/data/catan` sobre IDBFS (`web/pre.js`; `paths_flush()` espera el sync tras guardar). SDL3 en emscripten no muestra el teclado táctil: `gk_text_input` (overlay HTML) via `Frontend.text_input` para chat y campos de formulario. Pantalla táctil (`Frontend.touch`: en la web según `navigator.maxTouchPoints`, así en una PC con navegador no hay barra táctil; `APP_TOUCH=1` en la GUI nativa para probar): un "≡" en la barra de arriba (id `PK_APP` 12 → `ui.modal = 2`: Ayuda/Salir) y un "Volver" (id 7 = Esc) que aparece en `bottom_lines` solo cuando hay algo que cancelar; 8 = sí del diálogo de salir, 9/10 = volver/juego libre al terminar; `modal_menu` lleva una "x" (id 7) salvo los menús obligatorios; `gfx_touch` (global de `comun/gfx`) agranda filas de formularios, menús modales, acciones y +/- de cartas, y la tolerancia de toque del tablero. Arrastrar con el botón apretado (o el dedo) sobre el registro = rueda (`guikit.c`: cada 33 px una muesca, `EV_WHEEL` en el punto donde empezó). Layout **vertical** (`screen_tall`, elegido cuando la superficie es más alta que ancha): tablero arriba del ancho entero, panel apilado abajo. `gk_open_fluid` calcula la escala entera con "lado largo x lado corto" para que sirva en las dos orientaciones. -- **Blanquear contraseñas**: `ADMIN <secreto> RESET <código> <nombre> <llave>` en `srv.c` (secreto en `<datos>/admin.key`); `comun/deploy/admin.sh` → `catan-admin reset|partidas` en el VPS (`install-vps.sh` lo instala). +- **Blanquear contraseñas**: `ADMIN <secreto> RESET <código> <llave> <nombre>` en `srv.c` (el nombre al final, puede tener espacios) (secreto en `<datos>/admin.key`); `comun/deploy/admin.sh` → `catan-admin reset|partidas` en el VPS (`install-vps.sh` lo instala). - **Online sin cuentas**: el cliente deriva una clave de nombre + contraseña (`seat_key` en `linux/online.c`, SHA-256 de `comun/base/sha256.c`) y la manda con `AUTH`; esa clave es el token del asiento. `JOIN` con el mismo nombre y clave vuelve al asiento; `MINE` lista tus partidas desde cualquier compu. Sin contraseña sigue la lista local de tokens (LAN, partidas viejas). Ausencia: 8º campo de OPTS en minutos; `catan_game.c` lleva `since[]` (reloj de pared, se reinicia al cargar) y el bot (búsqueda) juega por el asiento vencido hasta que deja de tocarle. - **Chat**: `SAY <para|-1> <texto>` → `CHAT <de> <para> <texto>` (en `comun/server`, persistido en `<partida>/chat`; al entrar se reenvían las últimas 150 que ese asiento puede ver). En el cliente las líneas de chat van al registro con marca `\1` (todos) / `\2` (privado); la caja de chat está siempre visible al pie del panel (click o F2 para escribir; Enter manda, Esc suelta), el chat va del color de quien habla (la marca lleva el asiento), `/w nombre texto` (o /msg, /whisper) es privado (el nombre puede tener espacios: gana el nombre completo más largo). Registro con barra de scroll (click arriba/abajo de la barrita), RePág/AvPág/Fin (también escribiendo) y rueda (`EV_WHEEL` en `comun/base/input.h`). - **Cursor del tablero**: las flechas caminan por vecinos (vértice→vértice, arista→arista que comparte vértice, hex→hex) aunque no sean válidos (`Ctrl.pos`, `ctrl_cursor_pos`; gris si no se puede); Tab salta al próximo válido. diff --git a/catan/core/ctrl.c b/catan/core/ctrl.c @@ -475,7 +475,9 @@ static bool do_action(Ctrl *c, int a, int arg, Move *out) for (int d = 0; d < D_VP; d++) if (g->p[p].dev[d] || g->p[p].dev_new[d]) { char key = (char)('1' + d); - menu_add(c, d, d, dev_playable(g, p, d), key, dev_name(d), 0); + char what[64] = "("; + { Sb b = { what, sizeof what, 1 }; sb_s(&b, tr(S_DD_KNIGHT + d)); sb_s(&b, ")"); } + menu_add(c, d, d, dev_playable(g, p, d), key, dev_name(d), what); /* que hace cada carta */ } break; case A_BANK: diff --git a/catan/core/strings.h b/catan/core/strings.h @@ -27,6 +27,12 @@ X(S_PLENTY, "Descubrimiento", "Year of Plenty") \ X(S_MONO, "Monopolio", "Monopoly") \ X(S_VPCARD, "Punto de victoria", "Victory Point") \ + X(S_DD_KNIGHT, "movés el ladrón y robás 1 carta", "move the robber and steal 1 card") \ + X(S_DD_ROADS, "2 rutas gratis", "2 free roads") \ + X(S_DD_PLENTY, "2 recursos del banco a elección", "2 resources of your choice from the bank") \ + X(S_DD_MONO, "todos te dan el recurso que elijas", "everyone gives you the resource you name") \ + X(S_DD_VP, "+1 punto, oculto hasta que ganás", "+1 point, hidden until you win") \ + X(S_DEV_CARDS, "Cartas de desarrollo:", "Development cards:") \ X(S_E_OK, "OK", "OK") \ X(S_E_PHASE, "No es el momento para eso", "Not allowed right now") \ X(S_E_TURN, "No es tu turno", "Not your turn") \ diff --git a/catan/gfx/screen.c b/catan/gfx/screen.c @@ -302,11 +302,21 @@ static void modal_help(Surface *s, int bx, int by, int bw, int bh) const char **L = g_lang ? EN : ES; int n = 14; if (gfx_touch) { L = g_lang ? TEN : TES; n = 8; } - int w = bw * 4 / 5 < 260 ? bw * 4 / 5 : 260, h = 26 + n * 12; + bool cards = bh >= 26 + (n + 1 + NDEV) * 12; /* que hace cada carta, si hay lugar (la Pico no) */ + int w = bw * 4 / 5 < 260 ? bw * 4 / 5 : 260, h = 26 + (n + (cards ? 1 + NDEV : 0)) * 12; int x = bx + (bw - w) / 2, y = by + (bh - h) / 2; box(s, x, y, w, h, C_PANEL, C_ACCENT); gfx_text(s, &font_bold, x + 6, y + 4, g_lang ? "Help" : "Ayuda", C_ACCENT); for (int i = 0; i < n; i++) gfx_text_fit(s, &font_normal, x + 8, y + 22 + i * 12, w - 12, L[i], C_TEXT); + if (cards) { + int ly = y + 22 + n * 12; + gfx_text(s, &font_bold, x + 8, ly, tr(S_DEV_CARDS), C_ACCENT); + for (int d = 0; d < NDEV; d++) { + char t[96] = ""; + cat(t, sizeof t, dev_name(d)); cat(t, sizeof t, ": "); cat(t, sizeof t, tr(S_DD_KNIGHT + d)); + gfx_text_fit(s, &font_normal, x + 8, ly + 12 * (d + 1), w - 12, t, C_TEXT); + } + } } static void modals(Surface *s, const Game *g, const Ctrl *c, const UiInfo *ui, int bx, int by, int bw, int bh, HitList *hits) diff --git a/catan/linux/app.c b/catan/linux/app.c @@ -319,7 +319,7 @@ int app_play(Frontend *fe, Session *s, Settings *st) ui.show_help = help; ui.touch = fe->touch; ui.can_free = s->local && !s->net && s->save_path[0] && s->g->winner >= 0; - ui.can_again = s->local && !s->net && s->g->phase == PH_OVER; + ui.can_again = s->g->phase == PH_OVER; /* local: app_play_again; online: AGAIN al servidor */ fe->draw_game(fe, s->g, &c, &ui); int fds[16]; diff --git a/catan/linux/online.c b/catan/linux/online.c @@ -105,9 +105,13 @@ static bool play_remote(Frontend *fe, Settings *st, Net *n, Form *f, const char sess_add_log(&s, line); if (hint) sess_add_log(&s, hint); s.handoff = false; - app_play(fe, &s, st); - net_send(n, "LEAVE"); + int again = app_play(fe, &s, st); s.net = 0; + if (again == 1) { /* otra como esta: el servidor la crea y nos sienta en el asiento 0 */ + net_send(n, "AGAIN"); + return play_remote(fe, st, n, f, hint); + } + net_send(n, "LEAVE"); return true; } diff --git a/catan/linux/tui.c b/catan/linux/tui.c @@ -474,11 +474,23 @@ static void draw_help(int bx, int by, int bw, int bh) "q quit (the game is saved)", "Mouse click on the board and menus" }; const char **L = g_lang ? EN : ES; - int n = 18, w = 52, h = n + 4; + int n = 18, w = 56; + bool cards = bh >= n + 1 + NDEV + 4; /* que hace cada carta, si la terminal es alta */ + int h = n + (cards ? 1 + NDEV : 0) + 4; int x = bx + (bw - w) / 2, y = by + (bh - h) / 2; tk_box(y, x, h, w, g_lang ? "Help" : "Ayuda", C_ACCENT); tk_att(255, 236, 0); for (int i = 0; i < n; i++) tk_put(y + 2 + i, x + 3, L[i], w - 6); + if (cards) { + tk_att(tk_xterm(C_ACCENT), 236, A_BOLD); + tk_put(y + 2 + n, x + 3, tr(S_DEV_CARDS), w - 6); + tk_att(255, 236, 0); + for (int d = 0; d < NDEV; d++) { + char t[120]; + snprintf(t, sizeof t, "%-18s %s", dev_name(d), tr(S_DD_KNIGHT + d)); + tk_put(y + 3 + n + d, x + 3, t, w - 6); + } + } } static void draw_confirm(const UiInfo *ui, int bx, int by, int bw, int bh) diff --git a/catan/server/catan_game.c b/catan/server/catan_game.c @@ -62,6 +62,17 @@ static void fmt_opts(const Opts *o, int away_min, int setup, char *out, int n) static uint32_t clock32(void) { return (uint32_t)srv_now_ms(); } +/* AGAIN: las mismas opciones sin la semilla del tablero (0: sale de la semilla nueva de la + * partida), asi el reparto es otro; el tablero de principiante queda como es. */ +static void cg_again(const char *opts, char *out, int n) +{ + int v[11] = { 0 }; + unsigned long bseed = 0; + int k = sscanf(opts, "%d %d %d %d %d %d %d %d %d %d %d %lu", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7], &v[8], &v[9], &v[10], &bseed); + if (k < 7) { snprintf(out, (size_t)n, "%s", opts); return; } + snprintf(out, (size_t)n, "%d %d %d %d %d %d %d %d %d %d %d 0", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10]); +} + static int cg_create(const char *args, char *opts, int optsn, char *seats) { int np, v2p, vp, beg, away = 0, setup = 0, scen = 0, map = 0; @@ -243,4 +254,5 @@ const GameModule catan_module = { .wake = cg_wake, .summary = cg_summary, .export = cg_export, + .again = cg_again, }; diff --git a/comun/server/srv.c b/comun/server/srv.c @@ -504,14 +504,12 @@ static void clean_name(const char *in, char *out, int n) if (!k) snprintf(out, (size_t)n, "?"); } -static void cmd_create(Server *s, Client *c, const char *args) +/* Arma la partida (opciones y tipo de asiento ya validados) y sienta al creador en el 0. + * Con like (AGAIN): la gente de esa partida conserva su asiento y su llave (les aparece en + * "mis partidas"), y el que pidio queda en el asiento myseat. */ +static void create_game(Server *s, Client *c, const char *opts, const char *seats, int np, const SGame *like, int myseat) { const GameModule *gm = s->gm; - char opts[SRV_LINE], seats[SRV_MAX_SEATS + 1] = ""; - int np = gm->create(args, opts, sizeof opts, seats); - if (np < 1 || np > SRV_MAX_SEATS || seats[0] != 'H') { sendf(s, c, "ERR %d bad create", gm->err_args); return; } - for (int i = 0; i < np; i++) - if (seats[i] != 'H' && seats[i] != 'B') { sendf(s, c, "ERR %d bad seats", gm->err_args); return; } long long now = srv_now_ms(); if (now - s->create_window > 60000) { s->create_window = now; s->creates_in_window = 0; } if (s->ngames >= SRV_MAX_GAMES || c->creates >= SRV_CREATES_CONN || s->creates_in_window >= SRV_CREATES_MIN) { @@ -522,6 +520,7 @@ static void cmd_create(Server *s, Client *c, const char *args) s->creates_in_window++; if (c->game >= 0) detach(s, c); SGame *g = add_game(s); + if (like) like = s->games[find_game(s, like->code)]; /* add_game puede mover el arreglo */ if (!g) { sendf(s, c, "ERR %d out of memory", gm->err_args); return; } static const char AL[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; do { @@ -538,10 +537,22 @@ static void cmd_create(Server *s, Client *c, const char *args) g->kind[i] = seats[i]; if (seats[i] == 'B') snprintf(g->name[i], sizeof g->name[i], "Bot %d", i + 1); } - g->kind[0] = 'P'; - if (c->key[0]) memcpy(g->token[0], c->key, 33); - else new_token(g->token[0]); - snprintf(g->name[0], sizeof g->name[0], "%s", c->name); + int seat = 0; + if (like) { + for (int i = 0; i < np; i++) + if (like->kind[i] == 'P') { + g->kind[i] = 'P'; + memcpy(g->token[i], like->token[i], 33); + snprintf(g->name[i], sizeof g->name[i], "%s", like->name[i]); + } + seat = myseat >= 0 && myseat < np && g->kind[myseat] == 'P' ? myseat : 0; + } + if (g->kind[seat] != 'P' || !like) { + g->kind[seat] = 'P'; + if (c->key[0]) memcpy(g->token[seat], c->key, 33); + else new_token(g->token[seat]); + snprintf(g->name[seat], sizeof g->name[seat], "%s", c->name); + } if (!start_game(s, g)) { s->ngames--; free_game(g); @@ -554,8 +565,36 @@ static void cmd_create(Server *s, Client *c, const char *args) write_export(s, g); char sum[128] = ""; if (gm->summary) gm->summary(g->st, sum, sizeof sum); - logf_(s, "partida %s creada por %s (%s)", g->code, c->name, sum); - attach(s, c, s->ngames - 1, 0); + logf_(s, "partida %s creada por %s (%s)%s", g->code, c->name, sum, like ? " [otra como la anterior]" : ""); + attach(s, c, s->ngames - 1, seat); +} + +static void cmd_create(Server *s, Client *c, const char *args) +{ + const GameModule *gm = s->gm; + char opts[SRV_LINE], seats[SRV_MAX_SEATS + 1] = ""; + int np = gm->create(args, opts, sizeof opts, seats); + if (np < 1 || np > SRV_MAX_SEATS || seats[0] != 'H') { sendf(s, c, "ERR %d bad create", gm->err_args); return; } + for (int i = 0; i < np; i++) + if (seats[i] != 'H' && seats[i] != 'B') { sendf(s, c, "ERR %d bad seats", gm->err_args); return; } + create_game(s, c, opts, seats, np, 0, 0); +} + +/* AGAIN: otra partida como la que estoy jugando: mismas opciones, bots donde habia bots, + * la misma gente en los mismos asientos (con su llave: les aparece en "mis partidas"), + * los asientos que estaban libres siguen libres, y otro reparto. */ +static void cmd_again(Server *s, Client *c) +{ + const GameModule *gm = s->gm; + if (c->game < 0) { sendf(s, c, "ERR %d not in a game", gm->err_args); return; } + SGame *old = s->games[c->game]; + char opts[SRV_LINE], seats[SRV_MAX_SEATS + 1] = ""; + if (gm->again) gm->again(old->opts, opts, sizeof opts); + else snprintf(opts, sizeof opts, "%s", old->opts); + int np = old->np, myseat = c->seat; + for (int i = 0; i < np; i++) seats[i] = old->kind[i] == 'B' ? 'B' : old->kind[i] == 'N' ? 'N' : 'H'; + seats[np] = 0; + create_game(s, c, opts, seats, np, old, myseat); } static void cmd_join(Server *s, Client *c, const char *code) @@ -743,6 +782,7 @@ static void handle(Server *s, Client *c, char *line) return; } if (!strcmp(line, "CREATE")) cmd_create(s, c, args); + else if (!strcmp(line, "AGAIN")) cmd_again(s, c); else if (!strcmp(line, "JOIN")) cmd_join(s, c, args); else if (!strcmp(line, "OPEN")) cmd_open(s, c, args); else if (!strcmp(line, "STATUS")) cmd_status(s, c, args); diff --git a/comun/server/srv.h b/comun/server/srv.h @@ -15,6 +15,9 @@ * OPEN <codigo> <token> volver a entrar a una partida propia * STATUS <codigo> <token> [...] estado de varias partidas (para "mis partidas") * MINE estado de las partidas donde tu clave (AUTH) tiene asiento + * AGAIN otra partida como la actual (mismas opciones y bots, la misma + * gente en los mismos asientos, otro reparto): siguen GAME/HDR/... + * como en CREATE; a los demas les aparece en MINE * MOVE <movida> movida en el formato del juego * SAY <para> <texto> chat de la partida; para = asiento (privado) o -1 (todos) * LEAVE | PING @@ -72,6 +75,9 @@ typedef struct GameModule { * orden). El servidor la mantiene al dia en <dir>/<codigo>/partida.txt, con el chat. */ void (*export)(const void *st, const char *code, const char *const *moves, int nmoves, char (*names)[20], const char *kinds, void (*put)(void *ctx, const char *line), void *ctx); + /* Opcional (AGAIN): la linea de opciones de la partida nueva a partir de la vieja (por + * ejemplo, sin la semilla del tablero para que salga otro reparto). NULL: la misma. */ + void (*again)(const char *opts, char *out, int n); } GameModule; typedef struct Server Server;