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

Tactil: nada fuera de la pantalla, Volver solo cuando sirve

- screen_tall: se calcula lo que necesita el panel y se reparten las filas de
  acciones (13..22) y si hace falta el tablero (hasta W/2): en 360x640 con 6
  jugadores la caja de chat tapaba 'Terminar turno'. La caja de chat nunca
  encima del registro.
- screen_wide: filas de acciones adaptables y registro con al menos una fila
  (en 780x360 daba filas negativas); log_scrollbar sin division por cero.
- ctrl_can_back(): Esc hace algo (cancelar, cerrar menu, deshacer borrador);
  'Volver' y la 'x' de los menus solo en esos casos (antes aparecian en modos
  obligatorios como elegir victima o descartar).
- Vista previa del mapa: botones altos en tactil, etiquetas cortas si no
  entran, sin el boton de menu que no hacia nada.
- gfxform: si el formulario no entra, filas mas bajas y despues una ventana que
  sigue al cursor (en 780x360 tactil quedaban filas arriba de la pantalla).
- Puntos de probabilidad un pixel mas adentro de la ficha.
- linux-release.sh: valida argumentos, avisa si falta sudo-askpass, borra el
  temporal con trap.
- README: pendientes conocidos de la web.

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

Diffstat:
Mcatan/README.md | 5+++++
Mcatan/core/ctrl.c | 12++++++++++++
Mcatan/core/ctrl.h | 1+
Mcatan/gfx/board.c | 2+-
Mcatan/gfx/screen.c | 76+++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcatan/tools/linux-release.sh | 14++++++++++++--
Mcomun/gfx/gfxform.c | 23++++++++++++++++++++---
7 files changed, 100 insertions(+), 33 deletions(-)

diff --git a/catan/README.md b/catan/README.md @@ -166,6 +166,11 @@ catan.lu3dhn.xyz { `make deploy-web` sube `web/dist/` al VPS (host `vps` de ssh) a `/var/www/catan`. +Pendientes conocidos de la web: en iPhone el cuadro de texto no abre el teclado solo +(hay que tocar el campo); una partida local vive en el navegador de ese teléfono (si se +borran los datos del sitio se pierde: no hay exportar/importar todavía); una sola +instancia a la vez (la segunda pestaña avisa y no arranca). + ## PicoCalc ### Compilar y cargar desde la tarjeta SD (sin cable USB) diff --git a/catan/core/ctrl.c b/catan/core/ctrl.c @@ -807,6 +807,18 @@ static bool mandatory(const Ctrl *c) return false; } +/* Si Esc (K_BACK) hace algo en este estado: cancelar lo que se estaba eligiendo, cerrar + * un menu, deshacer el borrador de la colocacion inicial. Lo usan las interfaces tactiles + * para mostrar "Volver" solo cuando sirve. */ +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_CARDS && mandatory(c)) return false; /* solo borra la seleccion */ + return !mandatory(c); +} + bool ctrl_key(Ctrl *c, int key, Move *out) { c->msg[0] = 0; diff --git a/catan/core/ctrl.h b/catan/core/ctrl.h @@ -100,6 +100,7 @@ typedef struct { void ctrl_init(Ctrl *c, uint8_t local_mask, bool handoff); void ctrl_sync(Ctrl *c, const Game *g); /* despues de cada movida */ bool ctrl_key(Ctrl *c, int key, Move *out); /* true: hay movida para mandar */ +bool ctrl_can_back(const Ctrl *c); /* Esc hace algo ahora (cancelar, deshacer el borrador) */ bool ctrl_pick(Ctrl *c, int kind, int id, Move *out); bool ctrl_next(Ctrl *c, Move *out); /* otra movida para mandar detras de la anterior */ void ctrl_draft(const Ctrl *c, int *v, int *e); /* borrador de colocacion (-1 si no hay) */ diff --git a/catan/gfx/board.c b/catan/gfx/board.c @@ -220,7 +220,7 @@ static void token(Surface *s, const BoardGeom *g, int h, int num) int ty = cy - f->h / 2 - (pips ? 2 : 0); gfx_text(s, f, cx - tw / 2, ty, txt, c); if (pips) { - int n = PIPS[num], py = cy + r / 2 + 1; + int n = PIPS[num], py = cy + r / 2; /* dentro del disco tambien con r = 9 */ for (int i = 0; i < n; i++) gfx_rect(s, cx - n * 3 / 2 + i * 3 + 1, py, 2, 2, c); } } diff --git a/catan/gfx/screen.c b/catan/gfx/screen.c @@ -45,7 +45,7 @@ static void top_bar(Surface *s, const Game *g, const UiInfo *ui, int w, int h, b { gfx_rect(s, 0, 0, w, h, C_ACCENT); int menu = 0; - if (ui->touch && !compact) { /* tactil: "menu" (ayuda, salir) a la derecha */ + if (ui->touch && !compact && !ui->preview) { /* tactil: "menu" (ayuda, salir) a la derecha */ menu = 22; int mx = w - 19; gfx_rect(s, mx, 1, 17, h - 2, C_OUTLINE); @@ -156,12 +156,6 @@ static void dev_list(Surface *s, const Game *g, int p, int x, int y, int w) /* ------------------------------------------------------------ modales */ -/* un menu que no se puede cerrar sin elegir (responder una oferta, descartar, etc.) */ -static bool mandatory_menu(const Ctrl *c) -{ - return c->intent == I_RESPOND || c->intent == I_SETUP_OK; -} - static void modal_menu(Surface *s, const Ctrl *c, int bx, int by, int bw, int bh, HitList *hits) { int rowh = gfx_touch ? 26 : 14, pad = (rowh - 14) / 2, w = bw * 3 / 4 < 260 ? bw * 3 / 4 : 260; @@ -172,7 +166,7 @@ static void modal_menu(Surface *s, const Ctrl *c, int bx, int by, int bw, int bh box(s, x, y, w, h, C_PANEL, C_ACCENT); gfx_rect(s, x, y, w, 15, C_ACCENT); gfx_text_fit(s, &font_bold, x + 4, y + 1, w - (gfx_touch ? 30 : 8), tr(c->menu_title), C_OUTLINE); - if (gfx_touch && !mandatory_menu(c)) { /* cerrar (Esc) con el dedo */ + if (gfx_touch && ctrl_can_back(c) && c->intent != I_SETUP_OK) { /* cerrar (Esc) con el dedo; el de confirmar ya tiene "Deshacer" */ gfx_text(s, &font_bold, x + w - 14, y + 1, "x", C_OUTLINE); hit_add(hits, x + w - 26, y, 26, 15, PK_APP, 7); } @@ -362,13 +356,18 @@ static void preview_lines(Surface *s, const UiInfo *ui, int x, int y, int w, Hit gfx_text_fit(s, &font_bold, x, y, w, ui->preview, C_ACCENT); const char *L[5] = { g_lang ? "< Map" : "< Mapa", g_lang ? "Reshuffle (R)" : "Otro reparto (R)", g_lang ? "Map >" : "Mapa >", g_lang ? "Play (Enter)" : "Jugar (Enter)", g_lang ? "Back (Esc)" : "Volver (Esc)" }; - int bx = x, by = y + 13; + const char *S[5] = { "<", "R", ">", g_lang ? "Play" : "Jugar", g_lang ? "Back" : "Volver" }; /* si no entran */ + const Font *f = ui->touch ? &font_normal : &font_small; + int bh = ui->touch ? 26 : 11, by = y + 13, total = 0; + for (int i = 0; i < 5; i++) total += gfx_text_w(f, L[i]) + 8 + 3; + const char **T = total <= w ? L : S; + int bx = x; for (int i = 0; i < 5; i++) { - int bw = gfx_text_w(&font_small, L[i]) + 8; + int bw = gfx_text_w(f, T[i]) + 8; if (bx + bw > x + w) break; - box(s, bx, by, bw, 11, i == 3 ? C_ACCENT : C_PANEL2, C_DIM); - gfx_text(s, &font_small, bx + 4, by + 2, L[i], i == 3 ? C_OUTLINE : C_TEXT); - hit_add(hits, bx, by, bw, 11, PK_APP, 20 + i); + box(s, bx, by, bw, bh, i == 3 ? C_ACCENT : C_PANEL2, C_DIM); + gfx_text(s, f, bx + 4, by + (bh - f->h) / 2 + 1, T[i], i == 3 ? C_OUTLINE : C_TEXT); + hit_add(hits, bx, by, bw, bh, PK_APP, 20 + i); bx += bw + 3; } } @@ -376,7 +375,7 @@ static void preview_lines(Surface *s, const UiInfo *ui, int x, int y, int w, Hit static void bottom_lines(Surface *s, const Ctrl *c, const UiInfo *ui, int x, int y, int w, bool compact, HitList *hits) { if (ui->preview) { preview_lines(s, ui, x, y, w, hits); return; } - if (ui->touch && c->mode != CM_IDLE && c->mode != CM_OVER && c->mode != CM_HANDOFF) { + if (ui->touch && ctrl_can_back(c)) { /* tactil: "Volver" (Esc) solo cuando hay algo que cancelar, a la derecha de la instruccion */ int bw = 66; box(s, x + w - bw, y - 2, bw, 26, C_PANEL2, C_DIM); @@ -517,6 +516,7 @@ static void chat_box(Surface *s, const UiInfo *ui, int x, int y, int w, int h, H * abajo de ella pasa de pagina (PK_APP 4 / 5). */ static void log_scrollbar(Surface *s, const UiInfo *ui, int x, int y, int h, int rows, int total, HitList *hits) { + if (h <= 0 || rows <= 0) return; gfx_rect(s, x, y, 4, h, RGB(62, 68, 86)); if (total <= rows) return; int max = total - rows, sc = ui->log_scroll > max ? max : ui->log_scroll; @@ -573,8 +573,12 @@ static void screen_wide(Surface *s, const Game *g, const Ctrl *c, const UiInfo * gfx_text(s, &font_bold, x, y, tr(S_ACTIONS), C_ACCENT); y += 14; int colw = (w + 2) / 2; /* acciones y costos en dos columnas */ + int box = ui->chat_input ? (ui->touch ? 24 : 16) : 0; if (c->nactions && c->mode == CM_IDLE) { - int rows = (c->nactions + 1) / 2, ah = ui->touch ? 22 : 13; /* filas altas para el dedo */ + /* filas altas para el dedo, pero que dejen lugar a 3 filas de registro y el chat */ + int rows = (c->nactions + 1) / 2, ah = ui->touch ? 22 : 13; + int avail = (H - 4 - box - 6 - 14 - 3 * 11 - y) / rows; + if (ah > avail) ah = avail < 13 ? 13 : avail; for (int i = 0; i < c->nactions; i++) { const MenuItem *it = &c->actions[i]; int ax = x - 2 + (i / rows) * colw, ay = y + (i % rows) * ah, ty = ay + (ah - 13) / 2; /* por columnas */ @@ -606,7 +610,8 @@ static void screen_wide(Surface *s, const Game *g, const Ctrl *c, const UiInfo * } hit_add(hits, x, y, w, H - y, PK_APP, 3); /* rueda del mouse sobre el registro */ y += 14; - int box = ui->chat_input ? (ui->touch ? 24 : 16) : 0, rows = (H - 4 - box - y) / 11; + int rows = (H - 4 - box - y) / 11; + if (rows < 1) rows = 1; int total = log_lines(s, ui, x, y, w - 7, rows, 11); log_scrollbar(s, ui, x + w - 4, y, rows * 11, rows, total, hits); if (ui->chat_input) chat_box(s, ui, x - 2, H - box - 2, w + 4, box, hits); @@ -722,6 +727,25 @@ static void screen_tall(Surface *s, const Game *g, const Ctrl *c, const UiInfo * int W = s->w, H = s->h, top = 16; int bh = W * 9 / 10; /* el tablero es un poco mas ancho que alto */ if (bh > H / 2) bh = H / 2; + /* lo que necesita el panel de abajo; si no entra, primero se achican las filas de + * acciones y despues el tablero (hasta W/2), para que nada quede fuera de la pantalla + * ni la caja de chat tape a "Terminar" */ + int rowh = 21, ah_max = ui->touch ? 22 : 14, rh = ui->touch ? 30 : 26; + int box_h = ui->chat_input ? (ui->touch ? 26 : 18) : 0; + int vs = ui->view_seat; + bool hand = vs >= 0 && c->mode != CM_HANDOFF && !g->p[vs].neutral; + int arows = c->nactions && c->mode == CM_IDLE ? (c->nactions + 1) / 2 : 0; + int fixed = 3 + 30 + 2 + 4 + 14 + 3 * 11 + box_h + 4; /* instruccion, titulo del registro, 3 filas, chat */ + for (int p = 0; p < g->o.np; p++) fixed += g->p[p].neutral ? 14 : rowh; + if (hand) fixed += 2 + rh + 3 + 10; + int avail = H - top - bh - fixed, ah = ah_max; + if (arows) { + ah = avail / arows; + if (ah > ah_max) ah = ah_max; + if (ah < 13) ah = 13; + int need = arows * ah - avail; + if (need > 0) { bh -= need; if (bh < W / 2) bh = W / 2; } + } gfx_rect(s, 0, 0, W, H, C_PANEL); top_bar(s, g, ui, W, top, false, hits); gfx_rect(s, 0, top, W, bh, C_SEA); @@ -733,18 +757,15 @@ static void screen_tall(Surface *s, const Game *g, const Ctrl *c, const UiInfo * int x = 4, w = W - 8, y = top + bh + 3; bottom_lines(s, c, ui, x, y, w, false, hits); y += 30; - int ah = ui->touch ? 22 : 14; /* alto de las filas de acciones */ - int rowh = 21; for (int p = 0; p < g->o.np; p++) { bool det = !g->p[p].neutral; player_line(s, g, ui, p, x, y, w, det, rowh); y += det ? rowh : 14; } - int vs = ui->view_seat; - if (vs >= 0 && c->mode != CM_HANDOFF && !g->p[vs].neutral) { + if (hand) { y += 2; - int cw = (w - 4 * 3) / NRES, rh = ui->touch ? 30 : 26; + int cw = (w - 4 * 3) / NRES; for (int r = 0; r < NRES; r++) res_card(s, x + r * (cw + 3), y, cw, rh, r, g->p[vs].res[r], hits); y += rh + 3; dev_list(s, g, vs, x, y, w); @@ -752,11 +773,10 @@ static void screen_tall(Surface *s, const Game *g, const Ctrl *c, const UiInfo * } y += 2; int colw = (w + 2) / 2; - if (c->nactions && c->mode == CM_IDLE) { - int rows = (c->nactions + 1) / 2; + if (arows) { for (int i = 0; i < c->nactions; i++) { const MenuItem *it = &c->actions[i]; - int ax = x - 2 + (i / rows) * colw, ay = y + (i % rows) * ah, ty = ay + (ah - 14) / 2; + int ax = x - 2 + (i / arows) * colw, ay = y + (i % arows) * ah, ty = ay + (ah - 14) / 2; bool sel = i == c->icursor; box(s, ax, ay - 1, colw - 2, ah, sel ? C_ACCENT : C_PANEL2, sel ? C_ACCENT : C_DIM); char k[2] = { it->key, 0 }; @@ -764,7 +784,7 @@ static void screen_tall(Surface *s, const Game *g, const Ctrl *c, const UiInfo * gfx_text_fit(s, &font_normal, ax + 16, ty, colw - 20, it->label, sel ? C_OUTLINE : it->enabled ? C_TEXT : C_DIM); hit_add(hits, ax, ay - 1, colw - 2, ah, PK_ACTION, i); } - y += rows * ah; + y += arows * ah; } y += 4; gfx_text(s, &font_bold, x, y, tr(S_LOG), C_ACCENT); @@ -774,11 +794,13 @@ static void screen_tall(Surface *s, const Game *g, const Ctrl *c, const UiInfo * } hit_add(hits, x, y, w, H - y, PK_APP, 3); y += 14; - int box_h = ui->chat_input ? (ui->touch ? 26 : 18) : 0, rows = (H - 4 - box_h - y) / 11; + int chat_y = H - box_h - 2; + if (chat_y < y + 11) chat_y = y + 11; /* nunca encima de lo de arriba */ + int rows = (chat_y - 2 - y) / 11; if (rows < 1) rows = 1; int total = log_lines(s, ui, x, y, w - 7, rows, 11); log_scrollbar(s, ui, x + w - 4, y, rows * 11, rows, total, hits); - if (ui->chat_input) chat_box(s, ui, x - 2, H - box_h - 2, w + 4, box_h, hits); + if (ui->chat_input) chat_box(s, ui, x - 2, chat_y, w + 4, box_h, hits); modals(s, g, c, ui, 0, top, W, bh, hits); } diff --git a/catan/tools/linux-release.sh b/catan/tools/linux-release.sh @@ -11,10 +11,18 @@ # Uso: tools/linux-release.sh <version> <salida> (lo llama make release) set -e VERSION=$1; OUT=$2 +if [ -z "$VERSION" ] || [ -z "$OUT" ]; then + echo "uso: $0 <version> <salida>" >&2 + exit 1 +fi HOST=${RELEASE_HOST:-local} DOCKER=docker if [ "$HOST" = local ] && ! docker info >/dev/null 2>&1; then - export SUDO_ASKPASS=${SUDO_ASKPASS:-$(command -v sudo-askpass)} + export SUDO_ASKPASS=${SUDO_ASKPASS:-$(command -v sudo-askpass || true)} + if [ -z "$SUDO_ASKPASS" ]; then + echo "docker necesita sudo y no hay sudo-askpass (o pone SUDO_ASKPASS, o RELEASE_HOST=...)" >&2 + exit 1 + fi DOCKER="sudo -A docker" fi SDL_SRC=${WIN_SDL_SRC:-$HOME/.local/opt/src/SDL3-3.4.16} @@ -42,15 +50,17 @@ ldd /out/catan | sed "s/^/ /"' if [ "$HOST" = local ]; then mkdir -p "$(dirname "$OUT")" T=$(mktemp -d) + trap 'rm -rf "$T"' EXIT $DOCKER run --rm -v "$ROOT":/src:ro -v "$SDL_SRC":/sdl:ro -v "$T":/out -e VERSION="$VERSION" \ ubuntu:22.04 sh -c "$BUILD" - cp "$T/catan" "$OUT"; rm -rf "$T" + cp "$T/catan" "$OUT" exit 0 fi # remoto: se manda el codigo (lo que esta en git, tal como esta ahora) y la fuente de SDL3 R=/tmp/catan-release ssh "$HOST" "rm -rf $R && mkdir -p $R/src $R/sdl $R/out" +# (sin pipefail en sh: si tar falla por un archivo versionado borrado, lo acusa el build) (cd "$ROOT" && git ls-files -z | tar --null -T - -cz) | ssh "$HOST" "tar -xz -C $R/src" tar -C "$SDL_SRC" --exclude='build*' -cz . | ssh "$HOST" "tar -xz -C $R/sdl" ssh "$HOST" "docker run --rm -v $R/src:/src:ro -v $R/sdl:/sdl:ro -v $R/out:/out -e VERSION='$VERSION' \ diff --git a/comun/gfx/gfxform.c b/comun/gfx/gfxform.c @@ -35,19 +35,36 @@ void gfx_form(Surface *s, const struct Form *f, HitList *hits) int top; if (gfx_form_backdrop) top = gfx_form_backdrop(s, compact); else { gfx_rect(s, 0, 0, W, H, RGB(20, 24, 32)); top = compact ? 8 : 24; } - int rowh = gfx_touch ? 26 : compact ? 13 : 16, pad = gfx_touch ? 6 : 0; /* pad: el texto centrado en la fila alta */ + int rowh = gfx_touch ? 26 : compact ? 13 : 16; int shown = 0; for (int i = 0; i < f->n; i++) if (!f->f[i].hidden) shown++; - int w = compact ? W - 16 : 420, h = 28 + shown * rowh + 22; + /* si no entra en la pantalla, primero filas mas bajas y despues una ventana de + * filas que siga al cursor (nunca filas fuera de la pantalla) */ + int maxh = H - 12 - top; + while (rowh > 13 && 28 + shown * rowh + 22 > maxh) rowh--; + int pad = rowh >= 20 ? (rowh - 14) / 2 : 0; /* el texto centrado en la fila alta */ + int vis = shown, first = 0; + if (28 + shown * rowh + 22 > maxh) { + vis = (maxh - 50) / rowh; + if (vis < 1) vis = 1; + int ci = 0, k = 0; + for (int i = 0; i < f->n; i++) { if (f->f[i].hidden) continue; if (i == f->cur) ci = k; k++; } + first = ci - vis + 1 > 0 ? ci - vis + 1 : 0; + } + int w = compact ? W - 16 : 420, h = 28 + vis * rowh + 22; int x = (W - w) / 2, y = top; if (y + h > H - 12) y = H - 12 - h; + if (y < 0) y = 0; gfx_box(s, x, y, w, h, C_PANEL, C_ACCENT); gfx_rect(s, x, y, w, 16, C_ACCENT); gfx_text_fit(s, &font_bold, x + 6, y + 2, w - 12, f->title, C_OUTLINE); - int ly = y + 24, lw = compact ? 120 : 170; + if (first > 0) gfx_text(s, &font_small, x + w - 12, y + 4, "^", C_OUTLINE); + if (first + vis < shown) gfx_text(s, &font_small, x + w - 12, y + h - 10, "v", C_DIM); + int ly = y + 24, lw = compact ? 120 : 170, row = 0; for (int i = 0; i < f->n; i++) { const Field *fd = &f->f[i]; if (fd->hidden) continue; + if (row++ < first || row > first + vis) continue; bool sel = i == f->cur; switch (fd->type) { case F_LABEL: