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 fff2df2bd2570c7ecc4f7b8f63c2087fe4118bf3
parent 894b693dbe3c10a573b26f215ea7b01386dd1fc6
Author: Francisco Armando Rosmino <franciscorosmino@rosmino.com.ar>
Date:   Sat,  3 Oct 2026 12:35:01 -0300

Jugabilidad: confirmar el ladron, trueques explicitos, puertos visibles

Lo que salio de la partida XCFWRQ (Pancho y felox):
- El ladron se confirma antes de robar: hexagono -> (victima) -> 'Si, mover y
  robarle a X' / 'Elegir otro lugar'; Esc vuelve al tablero. Un toque de mas
  no se podia deshacer porque el robo es al azar. Solo en ctrl (TUI, GUI, web
  y PicoCalc), sin cambios de protocolo. Test t_ctrl_robber.
- Trueques: 'ofrece: da 2 madera, pide 1 trigo' en el registro, 'te da ...,
  te pide ...' al responder y en las contraofertas (la flecha no decia quien
  daba que).
- Puertos: el disco va perpendicular a la costa, del lado del agua (desde el
  centro del tablero caia sobre un vertice o adentro de la casilla) y se
  dibuja encima de rutas y pueblos; los muelles quedan abajo.

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

Diffstat:
Mcatan/core/ctrl.c | 45+++++++++++++++++++++++++++++++++++----------
Mcatan/core/ctrl.h | 2++
Mcatan/core/strings.h | 8++++++++
Mcatan/gfx/board.c | 29++++++++++++++++++++---------
Mcatan/linux/selftest.c | 39+++++++++++++++++++++++++++++++++++++++
5 files changed, 104 insertions(+), 19 deletions(-)

diff --git a/catan/core/ctrl.c b/catan/core/ctrl.c @@ -2,6 +2,8 @@ #include <string.h> #include "ctrl.h" +static void robber_confirm(Ctrl *c, int hex, int victim); + /* ------------------------------------------------------------ texto simple */ typedef struct { char *b; int n, pos; } Sb; @@ -278,7 +280,8 @@ static void build_actions(Ctrl *c) char t[48]; Sb b = { t, sizeof t, 0 }; t[0] = 0; - sb_s(&b, c->names[s]); sb_s(&b, ": "); sb_res(&b, g->offer.cget[s]); sb_s(&b, " → "); sb_res(&b, g->offer.cgive[s]); + sb_s(&b, c->names[s]); sb_s(&b, ": "); sb_s(&b, tr(S_GIVES)); sb_s(&b, " "); sb_res(&b, g->offer.cgive[s]); + sb_s(&b, ", "); sb_s(&b, tr(S_WANTS)); sb_s(&b, " "); sb_res(&b, g->offer.cget[s]); add_action(c, A_CONFIRM, s, game_can_afford(g, p, g->offer.cget[s]), (char)('5' + s), t, 0); } add_action(c, A_CANCEL, 0, true, 'x', tr(S_A_CANCEL), 0); @@ -573,8 +576,8 @@ static bool commit_target(Ctrl *c, int id, Move *out) m.a = (int16_t)id; m.b = -1; m.c = -1; - if (!robber_victims(c, id, v)) return finish(c, out, m); - if (v[1] < 0) { m.b = v[0]; return finish(c, out, m); } + if (!robber_victims(c, id, v)) { robber_confirm(c, id, -1); return false; } + if (v[1] < 0) { robber_confirm(c, id, v[0]); return false; } c->pending_hex = id; menu_begin(c, I_VICTIM, S_P_VICTIM); for (int i = 0; i < MAXP && v[i] >= 0; i++) @@ -588,6 +591,18 @@ static bool commit_target(Ctrl *c, int id, Move *out) static void offer_to_menu(Ctrl *c); static Move offer_move(const Ctrl *c, int mask); +/* Antes de que el ladron robe se confirma (un toque de mas no puede deshacerse despues: + * el robo es al azar). Esc o "elegir otro lugar" vuelven a elegir el hexagono. */ +static void robber_confirm(Ctrl *c, int hex, int victim) +{ + c->pending_hex = hex; + c->pending_victim = victim; + menu_begin(c, I_ROBBER_OK, S_P_ROBBER_OK); + if (victim >= 0) menu_add(c, 1, 0, true, 'e', tr(S_ROBBER_OK), c->names[victim]); + else menu_add(c, 1, 0, true, 'e', tr(S_ROBBER_OK_NO), 0); + menu_add(c, 0, 0, true, 'v', tr(S_ROBBER_ELSE), 0); +} + static bool select_item(Ctrl *c, int idx, Move *out) { if (idx < 0 || idx >= c->nitems) return false; @@ -596,7 +611,11 @@ static bool select_item(Ctrl *c, int idx, Move *out) Move m; switch (c->intent) { case I_VICTIM: - m = base_move(c, M_ROBBER); m.a = (int16_t)c->pending_hex; m.b = it->arg; m.c = -1; + robber_confirm(c, c->pending_hex, it->arg); + return false; + case I_ROBBER_OK: + if (!it->action) { enter_pick(c, CM_HEX, I_ROBBER); return false; } + m = base_move(c, M_ROBBER); m.a = (int16_t)c->pending_hex; m.b = (int16_t)c->pending_victim; m.c = -1; return finish(c, out, m); case I_RESPOND: if (it->action == 2) { /* contraoferta: arranca de la oferta al reves */ @@ -801,7 +820,7 @@ static bool mandatory(const Ctrl *c) { switch (c->intent) { case I_SETUP_S: case I_SETUP_R: case I_ROBBER: case I_NEUTRAL: case I_DISCARD: case I_GOLD: - case I_GIVEBACK: case I_RESPOND: case I_VICTIM: + case I_GIVEBACK: case I_RESPOND: case I_VICTIM: case I_ROBBER_OK: return true; } return false; @@ -815,6 +834,7 @@ bool ctrl_can_back(const Ctrl *c) if (c->mode == CM_IDLE || c->mode == CM_OVER || c->mode == CM_HANDOFF) return false; if (c->g && c->g->phase == PH_SETUP && !c->g->setup_sub && c->draft_v >= 0) return true; /* deshacer un paso */ if (c->mode == CM_MENU && c->intent == I_OFFER_TO) return true; /* vuelve a las cartas */ + if (c->mode == CM_MENU && (c->intent == I_VICTIM || c->intent == I_ROBBER_OK)) return true; /* otro lugar para el ladron */ if (c->mode == CM_CARDS && mandatory(c)) return false; /* solo borra la seleccion */ return !mandatory(c); } @@ -875,6 +895,7 @@ bool ctrl_key(Ctrl *c, int key, Move *out) else if (key == K_OK || (key == ' ' && c->intent != I_OFFER_TO)) return select_item(c, c->icursor, out); else if (key == K_BACK) { if (c->intent == I_OFFER_TO) { c->mode = CM_CARDS; c->intent = I_OFFER; } + else if (c->intent == I_VICTIM || c->intent == I_ROBBER_OK) enter_pick(c, CM_HEX, I_ROBBER); /* otro lugar */ else if (!mandatory(c)) enter_default(c); } else if (key == ' ' && c->intent == I_OFFER_TO) return select_item(c, c->icursor, out); else @@ -975,6 +996,7 @@ const char *ctrl_prompt(const Ctrl *c) case I_NEUTRAL: sb_s(&s, tr(c->mode == CM_VERT ? S_P_NEUTRAL_S : S_P_NEUTRAL_R)); break; case I_ROBBER: sb_s(&s, tr(S_P_ROBBER)); break; case I_VICTIM: sb_s(&s, tr(S_P_VICTIM)); break; + case I_ROBBER_OK: sb_s(&s, tr(S_P_ROBBER_OK)); break; case I_DISCARD: sb_s(&s, tr(S_P_DISCARD)); sb_s(&s, " "); sb_i(&s, res_total(c->sel)); sb_s(&s, "/"); sb_i(&s, c->need); break; case I_GIVEBACK: sb_s(&s, tr(S_P_GIVEBACK)); sb_s(&s, " "); sb_i(&s, res_total(c->sel)); sb_s(&s, "/"); sb_i(&s, c->need); break; case I_PLENTY: sb_s(&s, tr(S_P_PLENTY)); break; @@ -991,13 +1013,14 @@ const char *ctrl_prompt(const Ctrl *c) case I_DEV: sb_s(&s, tr(S_P_DEV)); break; case I_RESPOND: sb_s(&s, c->names[g->offer.from]); sb_s(&s, " "); sb_s(&s, tr(S_P_RESPOND)); sb_s(&s, ": "); - sb_res(&s, g->offer.give); sb_s(&s, " → "); sb_res(&s, g->offer.get); + sb_s(&s, tr(S_GIVES_YOU)); sb_s(&s, " "); sb_res(&s, g->offer.give); sb_s(&s, ", "); + sb_s(&s, tr(S_WANTS_YOU)); sb_s(&s, " "); sb_res(&s, g->offer.get); break; default: if (g->phase == PH_ROLL) sb_s(&s, tr(S_P_ROLL)); else if (g->offer.active && g->offer.from == c->actor) { - sb_s(&s, tr(S_OFFERS)); sb_s(&s, " "); sb_res(&s, g->offer.give); sb_s(&s, " "); sb_s(&s, tr(S_FOR)); sb_s(&s, " "); - sb_res(&s, g->offer.get); sb_s(&s, " — "); sb_s(&s, tr(S_WAITING_RESP)); + sb_s(&s, tr(S_OFFERS)); sb_s(&s, ": "); sb_s(&s, tr(S_GIVES)); sb_s(&s, " "); sb_res(&s, g->offer.give); sb_s(&s, ", "); + sb_s(&s, tr(S_WANTS)); sb_s(&s, " "); sb_res(&s, g->offer.get); sb_s(&s, " — "); sb_s(&s, tr(S_WAITING_RESP)); } else sb_s(&s, tr(g->phase == PH_PAIRED ? S_P_PAIRED : S_P_MAIN)); break; } @@ -1063,7 +1086,8 @@ int move_describe(const Game *g, const Move *m, const char *const names[MAXP], c sb_s(&s, " → "); sb_i(&s, m->c); sb_s(&s, " "); sb_s(&s, res_name(m->b)); break; case M_OFFER: - sb_s(&s, tr(S_OFFERS)); sb_s(&s, " "); sb_res(&s, m->r); sb_s(&s, " "); sb_s(&s, tr(S_FOR)); sb_s(&s, " "); sb_res(&s, m->r2); + sb_s(&s, tr(S_OFFERS)); sb_s(&s, ": "); sb_s(&s, tr(S_GIVES)); sb_s(&s, " "); sb_res(&s, m->r); sb_s(&s, ", "); + sb_s(&s, tr(S_WANTS)); sb_s(&s, " "); sb_res(&s, m->r2); if (m->a != -1 && m->a != game_offer_candidates(g, m->p)) { sb_s(&s, " ("); sb_s(&s, tr(S_TO)); bool first = true; @@ -1074,7 +1098,8 @@ int move_describe(const Game *g, const Move *m, const char *const names[MAXP], c break; case M_ACCEPT: sb_s(&s, tr(S_ACCEPTS)); break; case M_COUNTER: - sb_s(&s, tr(S_COUNTERS)); sb_s(&s, " "); sb_res(&s, m->r); sb_s(&s, " "); sb_s(&s, tr(S_FOR)); sb_s(&s, " "); sb_res(&s, m->r2); + sb_s(&s, tr(S_COUNTERS)); sb_s(&s, ": "); sb_s(&s, tr(S_GIVES)); sb_s(&s, " "); sb_res(&s, m->r); sb_s(&s, ", "); + sb_s(&s, tr(S_WANTS)); sb_s(&s, " "); sb_res(&s, m->r2); break; case M_REJECT: sb_s(&s, tr(S_REJECTS)); break; case M_CONFIRM: sb_s(&s, tr(S_TRADED_WITH)); sb_s(&s, " "); sb_s(&s, names[m->a]); break; diff --git a/catan/core/ctrl.h b/catan/core/ctrl.h @@ -15,6 +15,7 @@ enum Intent { I_NONE, I_SETUP_S, I_SETUP_R, I_SETTLE, I_ROAD, I_CITY, I_FREE_ROAD, I_NEUTRAL, I_ROBBER, I_VICTIM, I_DISCARD, I_GIVEBACK, I_ACTIONS, I_DEV, I_PLENTY, I_MONO, I_BANK_GIVE, I_BANK_GET, I_OFFER, I_RESPOND, I_OFFER_TO, I_CONFIRM_END, I_COUNTER, I_SETUP_OK, + I_ROBBER_OK, /* confirmar adonde va el ladron (y a quien roba) antes de que robe */ I_SHIP, I_MOVESHIP_FROM, I_MOVESHIP_TO, I_GOLD }; @@ -73,6 +74,7 @@ typedef struct { int pos; /* donde esta el cursor (vertice/arista/hex), valido o no */ int last_x, last_y; /* posicion del cursor (coords de topologia x2) */ int pending_hex; + int pending_victim; /* a quien se le roba (o -1) mientras se confirma el ladron */ MenuItem items[24]; int nitems, icursor; diff --git a/catan/core/strings.h b/catan/core/strings.h @@ -131,6 +131,14 @@ X(S_P_ROBBER, "Elegí adónde mover el ladrón", "Choose where to move the robber") \ X(S_NOT_HERE, "Ahí no se puede (Tab: próximo lugar válido)", "Not allowed there (Tab: next valid spot)") \ X(S_P_VICTIM, "¿A quién le robás?", "Steal from whom?") \ + X(S_P_ROBBER_OK, "¿Mover el ladrón ahí?", "Move the robber there?") \ + X(S_ROBBER_OK, "Sí, mover y robarle a", "Yes, move and steal from") \ + X(S_ROBBER_OK_NO, "Sí, mover (no hay a quién robar)", "Yes, move (nobody to steal from)") \ + X(S_ROBBER_ELSE, "Elegir otro lugar", "Choose another place") \ + X(S_GIVES, "da", "gives") \ + X(S_WANTS, "pide", "wants") \ + X(S_GIVES_YOU, "te da", "gives you") \ + X(S_WANTS_YOU, "te pide", "wants from you") \ X(S_P_DISCARD, "Descartá cartas:", "Discard cards:") \ X(S_P_GIVEBACK, "Devolvé cartas:", "Give back cards:") \ X(S_P_PLENTY, "Elegí 2 recursos del banco", "Pick 2 resources from the bank") \ diff --git a/catan/gfx/board.c b/catan/gfx/board.c @@ -79,14 +79,16 @@ void geom_port(const BoardGeom *g, int i, int *x, int *y) { int mx, my, e = g->t->port_edge[i]; geom_edge(g, e, &mx, &my); + /* el disco va del lado del agua, perpendicular a la arista: con mar, hacia el hexagono + * de agua; sin mar, alejandose del hexagono de tierra (la direccion desde el centro + * del tablero caia a veces sobre un vertice o adentro de la casilla) */ int dx = mx - g->cx, dy = my - g->cy; - for (int s = 0; s < 2; s++) { /* con mar: hacia el hexagono de agua de ese lado */ - int h = g->t->edge_hex[e][s]; - if (h >= 0 && g->t->region[h] < 0) { - int hx, hy; - geom_hex(g, h, &hx, &hy); - dx = hx - mx; dy = hy - my; - } + for (int s = 0; s < 2; s++) { + int h = g->t->edge_hex[e][s], hx, hy; + if (h < 0) continue; + geom_hex(g, h, &hx, &hy); + if (g->t->region[h] < 0) { dx = hx - mx; dy = hy - my; break; } + dx = mx - hx; dy = my - hy; } int len = isqrt(dx * dx + dy * dy); if (!len) len = 1; @@ -225,7 +227,8 @@ static void token(Surface *s, const BoardGeom *g, int h, int num) } } -static void port(Surface *s, const BoardGeom *g, const Game *gm, int i, int flags) +/* los muelles van debajo de rutas y pueblos; el disco con el tipo, encima (port_disc) */ +static void port(Surface *s, const BoardGeom *g, int i) { const Topo *t = g->t; int px, py, x, y; @@ -234,6 +237,12 @@ static void port(Surface *s, const BoardGeom *g, const Game *gm, int i, int flag geom_vert(g, t->edge_v[t->port_edge[i]][k], &x, &y); gfx_line(s, x, y, px, py, g->R / 10 + 1, RGB(120, 84, 44)); } +} + +static void port_disc(Surface *s, const BoardGeom *g, const Game *gm, int i, int flags) +{ + int px, py; + geom_port(g, i, &px, &py); int r = g->R * 28 / 100; if (r < 2) r = 2; int kind = gm->port[i]; @@ -260,7 +269,7 @@ void gfx_board(Surface *s, const BoardGeom *g, const Game *gm, const Ctrl *c, in if ((flags & GB_DETAIL) && R >= 16) for (int h = 0; h < g->t->nhex; h++) detail(s, g, h, gm->terrain[h]); if (flags & GB_PORTS) - for (int i = 0; i < g->t->nport; i++) port(s, g, gm, i, flags); + for (int i = 0; i < g->t->nport; i++) port(s, g, i); if (flags & GB_TOKENS) for (int h = 0; h < g->t->nhex; h++) if (gm->num[h]) token(s, g, h, gm->num[h]); @@ -380,6 +389,8 @@ void gfx_board(Surface *s, const BoardGeom *g, const Game *gm, const Ctrl *c, in gfx_ring(s, x, y, R * 50 / 100 + 1, 4, C_OUTLINE); gfx_ring(s, x, y, R * 50 / 100, 3, ccol); } + if (flags & GB_PORTS) /* el tipo de puerto siempre visible, encima de todo */ + for (int i = 0; i < g->t->nport; i++) port_disc(s, g, gm, i, flags); } int gfx_board_hit(const BoardGeom *g, const Ctrl *c, int x, int y) diff --git a/catan/linux/selftest.c b/catan/linux/selftest.c @@ -1327,6 +1327,44 @@ static void t_websocket(void) CHECK(unwrap(buf, 1014, out, sizeof out, &got, &ctl) == -1); } +/* El ladron se confirma antes de robar (un toque de mas no se puede deshacer: el robo es + * al azar): hexagono -> (victima) -> confirmar; Esc o "otro lugar" vuelven al tablero. */ +static void t_ctrl_robber(void) +{ + static Auth a; + Opts o; + opts_default(&o, 3, false); + o.beginner = 1; + auth_new(&a, &o, 11, NULL, NULL); + Game *g = &a.g; + const Topo *t = game_topo(g); + static const char *nm[MAXP] = { "A", "B", "C", "D" }; + /* un hex sin nadie (h0) y otro con un pueblo de B con cartas (h1), ninguno con el ladron */ + int h0 = -1, h1 = -1; + for (int h = 0; h < t->nhex && (h0 < 0 || h1 < 0); h++) { + if (h == g->robber || !ter_land(g->terrain[h])) continue; + if (h0 < 0) { h0 = h; continue; } + h1 = h; + } + place(g, 1, t->hex_vert[h1][0], 1); + g->p[1].res[GRAIN] += 2; g->bank[GRAIN] -= 2; + g->phase = PH_ROBBER; g->cur = 0; g->rolled = 1; + static Ctrl c; + ctrl_init(&c, 1, false); + for (int i = 0; i < MAXP; i++) c.names[i] = nm[i]; + ctrl_sync(&c, g); + CHECK(c.mode == CM_HEX && c.intent == I_ROBBER); + Move m; + CHECK(!ctrl_pick(&c, PK_HEX, h0, &m) && c.mode == CM_MENU && c.intent == I_ROBBER_OK && c.pending_victim < 0); + CHECK(ctrl_can_back(&c)); + CHECK(!ctrl_key(&c, K_BACK, &m) && c.mode == CM_HEX && c.intent == I_ROBBER); /* otro lugar */ + CHECK(!ctrl_pick(&c, PK_HEX, h1, &m) && c.mode == CM_MENU && c.intent == I_ROBBER_OK && c.pending_victim == 1); + CHECK(!ctrl_key(&c, 'v', &m) && c.mode == CM_HEX); /* "elegir otro lugar" */ + CHECK(!ctrl_pick(&c, PK_HEX, h1, &m) && c.mode == CM_MENU); + CHECK(ctrl_key(&c, 'e', &m) && m.type == M_ROBBER && m.a == h1 && m.b == 1); /* confirma: recien ahi sale la movida */ + CHECK(auth_submit(&a, &m) == E_OK && g->robber == h1 && g->p[1].res[GRAIN] == 1 && g->p[0].res[GRAIN] == 1); +} + int app_selftest(void) { printf("selftest del nucleo\n"); @@ -1352,6 +1390,7 @@ int app_selftest(void) t_ctrl_counter(); t_ctrl_nav(); t_ctrl_setup(); + t_ctrl_robber(); t_serialize(); t_replay(); t_freeplay();