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:
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;