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