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

tui.c (26833B)


      1 /* tui.c - interfaz de terminal con ncurses.
      2  *
      3  * El tablero se dibuja con el renderer de pixeles (gfx) sobre un buffer donde
      4  * cada celda de la terminal son 2 pixeles verticales (caracter '▀' con color de
      5  * frente arriba y de fondo abajo). Encima se escriben como texto los numeros,
      6  * edificios, ladron y puertos. Los paneles son texto comun. */
      7 #include "tuikit.h"
      8 #include <stdlib.h>
      9 #include <string.h>
     10 #include "frontend.h"
     11 
     12 /* ------------------------------------------------------------------ tablero */
     13 
     14 static void draw_board(Frontend *f, const Game *g, const Ctrl *c, int x0, int y0, int w, int h)
     15 {
     16     Surface s = *tk_pix_begin(w, h);
     17     gfx_rect(&s, 0, 0, s.w, s.h, C_SEA);
     18     BoardGeom geo;
     19     geom_fit(&geo, g, 0, 0, s.w, s.h);
     20     int flags = GB_ROADS | GB_PORTS | (geo.R >= 18 ? GB_DETAIL : 0);
     21     gfx_board(&s, &geo, g, 0, flags);
     22 
     23     const Topo *t = game_topo(g);
     24     int cur = ctrl_cursor_pos(c);
     25     Color ccol = ctrl_cursor_target(c) >= 0 ? C_CURSOR : C_DIM;
     26     int br = geo.R / 4 > 2 ? geo.R / 4 : 2;
     27     if (c->mode == CM_EDGE) {
     28         for (int i = 0; i < c->ntargets; i++) {
     29             int x, y;
     30             if (c->targets[i] == cur) continue;
     31             geom_edge(&geo, c->targets[i], &x, &y);
     32             gfx_disc(&s, x, y, 2, C_OUTLINE);
     33             gfx_disc(&s, x, y, 1, C_WHITE);
     34         }
     35         if (cur >= 0) {
     36             int ax, ay, bx, by;
     37             geom_vert(&geo, t->edge_v[cur][0], &ax, &ay);
     38             geom_vert(&geo, t->edge_v[cur][1], &bx, &by);
     39             gfx_line(&s, ax, ay, bx, by, geo.R / 4 > 3 ? geo.R / 4 : 3, ccol);
     40         }
     41     }
     42     for (int v = 0; v < t->nvert; v++) {
     43         if (g->vown[v] < 0) continue;
     44         int x, y;
     45         geom_vert(&geo, v, &x, &y);
     46         int r = g->vlev[v] == 2 ? br + 1 : br;
     47         gfx_disc(&s, x, y, r + 1, C_OUTLINE);
     48         gfx_disc(&s, x, y, r, PLAYER_COLOR[g->vown[v]]);
     49     }
     50     {                                    /* colocacion en borrador */
     51         int dv, de;
     52         ctrl_draft(c, &dv, &de);
     53         Color pc = c->actor >= 0 ? PLAYER_COLOR[c->actor] : C_WHITE;
     54         if (de >= 0) {
     55             int ax, ay, bx, by;
     56             geom_vert(&geo, t->edge_v[de][0], &ax, &ay);
     57             geom_vert(&geo, t->edge_v[de][1], &bx, &by);
     58             gfx_line(&s, ax, ay, bx, by, geo.R / 5 > 2 ? geo.R / 5 : 2, pc);
     59         }
     60         if (dv >= 0) {
     61             int x, y;
     62             geom_vert(&geo, dv, &x, &y);
     63             gfx_disc(&s, x, y, br + 1, C_OUTLINE);
     64             gfx_disc(&s, x, y, br, pc);
     65         }
     66     }
     67     if (c->mode == CM_VERT) {
     68         for (int i = 0; i < c->ntargets; i++) {
     69             int x, y;
     70             if (c->targets[i] == cur) continue;
     71             geom_vert(&geo, c->targets[i], &x, &y);
     72             gfx_disc(&s, x, y, 2, C_OUTLINE);
     73             gfx_disc(&s, x, y, 1, C_WHITE);
     74         }
     75         if (cur >= 0) {
     76             int x, y;
     77             geom_vert(&geo, cur, &x, &y);
     78             gfx_disc(&s, x, y, br + 2, C_OUTLINE);
     79             gfx_disc(&s, x, y, br + 1, ccol);
     80         }
     81     }
     82 
     83     /* pixeles -> medios bloques */
     84     tk_pix_blit(y0, x0);
     85 
     86     /* puertos */
     87     for (int i = 0; i < t->nport; i++) {
     88         int px, py;
     89         geom_port(&geo, i, &px, &py);
     90         int k = g->port[i];
     91         Color bg = k == PORT_ANY ? C_WHITE : TERRAIN_COLOR[k];
     92         tk_pix_text(y0, x0, px - 1, py, k == PORT_ANY ? "3:1" : "2:1", C_BLACK, bg, A_BOLD);
     93         if (k != PORT_ANY) {
     94             char sh[8];
     95             snprintf(sh, sizeof sh, "%.3s", res_short(k));
     96             tk_pix_text(y0, x0, px - 1, py + 2, res_short(k), C_BLACK, bg, 0);
     97         }
     98     }
     99 
    100     /* fichas numeradas */
    101     for (int hx = 0; hx < t->nhex; hx++) {
    102         int x, y;
    103         geom_hex(&geo, hx, &x, &y);
    104         bool target = c->mode == CM_HEX && ctrl_is_target(c, hx);
    105         bool iscur = c->mode == CM_HEX && cur == hx;
    106         Color bg = iscur ? ccol : target ? C_WHITE : C_TOKEN;
    107         char txt[8];
    108         if (g->num[hx]) snprintf(txt, sizeof txt, "%2d ", g->num[hx]);
    109         else if (target || iscur) snprintf(txt, sizeof txt, " · ");
    110         else continue;
    111         Color fg = (g->num[hx] == 6 || g->num[hx] == 8) ? C_RED : C_BLACK;
    112         tk_pix_text(y0, x0, x - 1, y, txt, fg, bg, A_BOLD);
    113         if (geo.R >= 11 && g->num[hx]) {
    114             static const int PIPS[13] = { 0, 0, 1, 2, 3, 4, 5, 0, 5, 4, 3, 2, 1 };
    115             char pips[16] = { 0 };
    116             int n = PIPS[g->num[hx]];
    117             for (int i = 0; i < n; i++) strcat(pips, "·");
    118             tk_pix_text(y0, x0, x - n / 2, y + 2, pips, fg, TERRAIN_COLOR[g->terrain[hx]], A_BOLD);
    119         }
    120     }
    121 
    122     /* ladron */
    123     {
    124         int x, y;
    125         geom_hex(&geo, g->robber, &x, &y);
    126         int rx = g->terrain[g->robber] == T_DESERT ? x : x - geo.R * 55 / 100 - 1;
    127         tk_pix_text(y0, x0, rx, y, "♟", C_WHITE, RGB(30, 30, 34), A_BOLD);
    128     }
    129 
    130     /* edificios */
    131     for (int v = 0; v < t->nvert; v++) {
    132         int x, y;
    133         geom_vert(&geo, v, &x, &y);
    134         if (g->vown[v] >= 0)
    135             tk_pix_text(y0, x0, x, y, g->vlev[v] == 2 ? "♜" : "⌂", C_BLACK, PLAYER_COLOR[g->vown[v]], A_BOLD);
    136     }
    137     if (c->mode == CM_VERT && cur >= 0) {
    138         int x, y;
    139         geom_vert(&geo, cur, &x, &y);
    140         tk_pix_text(y0, x0, x, y, "◆", C_BLACK, C_CURSOR, A_BOLD);
    141     }
    142 
    143     hit_add(&f->hits, x0, y0, w, h, PK_BOARD, 0);
    144     f->geom = geo;
    145     f->board_ox = x0;
    146     f->board_oy = y0;
    147     f->board_sy = 2;
    148 }
    149 
    150 /* ------------------------------------------------------------------ paneles */
    151 
    152 static void res_chip(int y, int x, int r, int n, bool dim)
    153 {
    154     char buf[16];
    155     snprintf(buf, sizeof buf, " %s %d ", res_short(r), n);
    156     tk_attc(C_BLACK, TERRAIN_COLOR[r], dim ? 0 : A_BOLD);
    157     mvaddstr(y, x, buf);
    158 }
    159 
    160 static void draw_panel(Frontend *f, const Game *g, const Ctrl *c, const UiInfo *ui, int x, int y, int w, int h)
    161 {
    162     int line = y, bottom = y + h;
    163     tk_section(line++, x, w, tr(S_PLAYERS));
    164     for (int p = 0; p < g->o.np && line < bottom; p++) {
    165         const Player *pl = &g->p[p];
    166         char buf[96];
    167         tk_att(pl->neutral ? 244 : TK_DEF, TK_DEF, 0);
    168         mvaddstr(line, x, p == g->cur && g->phase != PH_OVER ? "▶" : " ");
    169         tk_attc(PLAYER_COLOR[p], C_BLACK, 0);
    170         mvaddstr(line, x + 1, "██");
    171         tk_att(pl->neutral ? 244 : TK_DEF, TK_DEF, p == ui->view_seat ? A_BOLD : 0);
    172         tk_put(line, x + 4, ui->names[p], 14);
    173         if (pl->neutral) {
    174             snprintf(buf, sizeof buf, "%s · %s %d", tr(S_NEUTRAL), tr(S_ROAD), pl->road_len);
    175             tk_att(244, TK_DEF, 0);
    176             tk_put(line, x + 19, buf, w - 20);
    177             line++;
    178             continue;
    179         }
    180         int vp = game_vp_public(g, p);
    181         int hidden = (p == ui->view_seat || g->phase == PH_OVER) ? pl->dev[D_VP] + pl->dev_new[D_VP] : 0;
    182         if (hidden) snprintf(buf, sizeof buf, "%2d+%d %s", vp, hidden, tr(S_VP));
    183         else snprintf(buf, sizeof buf, "%2d %s", vp, tr(S_VP));
    184         tk_att(tk_xterm(C_ACCENT), TK_DEF, A_BOLD);
    185         mvaddstr(line, x + 19, buf);
    186         /* fichas que le quedan: pueblos, ciudades, rutas */
    187         if (game_has_sea(g)) snprintf(buf, sizeof buf, "⌂%d ♜%d ═%d ≈%d", pl->settles, pl->cities, pl->roads, pl->ships);
    188         else snprintf(buf, sizeof buf, "⌂%d ♜%d ═%d", pl->settles, pl->cities, pl->roads);
    189         if (x + 27 + tk_ulen(buf) <= x + w) {
    190             tk_attc(PLAYER_COLOR[p], C_BLACK, 0);
    191             tk_put(line, x + w - tk_ulen(buf) - 1, buf, w);
    192         }
    193         line++;
    194         if (line >= bottom) break;
    195         int devs = pl->dev_hidden;
    196         for (int d = 0; d < NDEV; d++) devs += pl->dev[d] + pl->dev_new[d];
    197         int nc = hand_size(g, p);
    198         snprintf(buf, sizeof buf, "   %d %s · %d %s · %d %s · %s %d", nc, tr(nc == 1 ? S_CARD1 : S_CARDS), devs, g_lang ? "dev" : "des",
    199                  pl->knights, g_lang ? "kn" : "cab", g_lang ? "road" : "ruta", pl->road_len);
    200         if (g->o.variant2p) snprintf(buf + strlen(buf), sizeof buf - strlen(buf), " · %d %s", pl->tokens, tr(S_TOKENS));
    201         tk_att(248, TK_DEF, 0);
    202         tk_put(line, x, buf, w);
    203         int bx = x + tk_ulen(buf) + 1;
    204         if (g->longest == p && bx + 3 < x + w) { tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD); mvaddstr(line, bx, "LR"); bx += 3; }
    205         if (g->army == p && bx + 3 < x + w) { tk_att(tk_xterm(C_BLACK), tk_xterm(C_ERR), A_BOLD); mvaddstr(line, bx, "EJ"); }
    206         line++;
    207     }
    208 
    209     /* mano */
    210     int vs = ui->view_seat;
    211     if (line + 3 < bottom && vs >= 0 && c->mode != CM_HANDOFF && !g->p[vs].neutral) {
    212         char title[64];
    213         snprintf(title, sizeof title, "%s — %s", tr(S_HAND), ui->names[vs]);
    214         tk_section(line++, x, w, title);
    215         const Player *pl = &g->p[vs];
    216         int cx = x + 1;
    217         for (int r = 0; r < NRES; r++) {
    218             res_chip(line, cx, r, pl->res[r], pl->res[r] == 0);
    219             cx += 8;
    220         }
    221         line++;
    222         char buf[128] = "";
    223         for (int d = 0; d < NDEV; d++) {
    224             if (!pl->dev[d] && !pl->dev_new[d]) continue;
    225             char one[48];
    226             snprintf(one, sizeof one, "%s%s×%d%s", buf[0] ? " · " : "", dev_name(d), pl->dev[d] + pl->dev_new[d], pl->dev_new[d] ? "*" : "");
    227             strncat(buf, one, sizeof buf - strlen(buf) - 1);
    228         }
    229         if (buf[0] && line < bottom) { tk_att(tk_xterm(C_TEXT), TK_DEF, 0); tk_put(line++, x + 1, buf, w - 2); }
    230     }
    231 
    232     /* acciones */
    233     if (line + 2 < bottom) {
    234         tk_section(line++, x, w, tr(S_ACTIONS));
    235         if (c->nactions && c->mode == CM_IDLE) {
    236             for (int i = 0; i < c->nactions && line < bottom; i++) {
    237                 const MenuItem *it = &c->actions[i];
    238                 char buf[80];
    239                 snprintf(buf, sizeof buf, " %c  %s", it->key, it->label);
    240                 bool sel = i == c->icursor;
    241                 if (sel) tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD);
    242                 else tk_att(it->enabled ? TK_DEF : 244, TK_DEF, 0);
    243                 tk_fill(line, x, w, " ");
    244                 tk_put(line, x, buf, w);
    245                 hit_add(&f->hits, x, line, w, 1, PK_ACTION, i);
    246                 line++;
    247             }
    248         } else {
    249             static const struct { int name; const uint8_t *cost; } COSTS[] = {
    250                 { S_ROAD, COST_ROAD }, { S_SETTLE, COST_SETTLE }, { S_CITY, COST_CITY }, { S_DEV_SHORT, COST_DEV } };
    251             for (int i = 0; i < 4 && line < bottom; i++) {
    252                 tk_att(246, TK_DEF, 0);
    253                 tk_put(line, x + 1, tr(COSTS[i].name), 11);
    254                 int cx = x + 13;
    255                 for (int r = 0; r < NRES; r++)
    256                     for (int k = 0; k < COSTS[i].cost[r] && cx + 4 < x + w; k++) {
    257                         char chip[8];
    258                         snprintf(chip, sizeof chip, "%.3s", res_short(r));
    259                         tk_attc(C_BLACK, TERRAIN_COLOR[r], 0);
    260                         mvaddstr(line, cx, chip);
    261                         cx += 4;
    262                     }
    263                 line++;
    264             }
    265         }
    266     }
    267 
    268     /* registro */
    269     if (line + 2 < bottom) {
    270         tk_section(line++, x, w, tr(S_LOG));
    271         if (ui->chat_input) {               /* linea de chat siempre visible, abajo de todo */
    272             bottom--;
    273             char cb[260];
    274             const char *t = ui->chat_input;
    275             int room = w - 6, cl = tk_ulen(t);
    276             while (cl > room) { utf8_next(&t); cl--; }
    277             if (!ui->chat_focus && !*t) {
    278                 tk_att(244, 237, 0);
    279                 snprintf(cb, sizeof cb, "%s", g_lang ? "> type here to chat (F2)" : "> escribí acá para chatear (F2)");
    280             } else {
    281                 tk_att(117, 237, ui->chat_focus ? A_BOLD : 0);
    282                 snprintf(cb, sizeof cb, "> %s", t);
    283             }
    284             tk_fill(bottom, x, w, " ");
    285             tk_put(bottom, x + 1, cb, w - 2);
    286             if (ui->chat_focus) { tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), 0); mvaddstr(bottom, x + 1 + tk_ulen(cb), " "); }
    287             hit_add(&f->hits, x, bottom, w, 1, PK_APP, 6);
    288         }
    289         int rows = bottom - line;
    290         int max = ui->nlog > rows ? ui->nlog - rows : 0, scroll = ui->log_scroll;
    291         if (ui->log_max) *ui->log_max = max;
    292         if (scroll > max) scroll = max;
    293         int start = ui->nlog - rows - scroll;
    294         if (start < 0) start = 0;
    295         for (int i = start; i < ui->nlog && line < bottom; i++) {
    296             const char *t = ui->log[i];
    297             int base = i == ui->nlog - 1 ? TK_DEF : 248, seat = -1;
    298             char kind = *t >= '\1' && *t <= '\4' && t[1] ? *t : 0;
    299             if (kind) { seat = t[1] - '0'; if (seat < 0 || seat >= MAXP) seat = -1; t += 2; }
    300             if (kind == '\1' || kind == '\2') {                     /* chat: todo del color de quien habla */
    301                 if (seat >= 0) tk_att(tk_xterm(PLAYER_COLOR[seat]), TK_DEF, A_BOLD);
    302                 else tk_att(kind == '\1' ? 117 : 183, TK_DEF, 0);
    303             } else if (kind == '\4') tk_att(tk_xterm(C_ACCENT), TK_DEF, A_BOLD);   /* te paso a vos */
    304             else tk_att(base, TK_DEF, 0);
    305             size_t nl = kind == '\3' && seat >= 0 ? strlen(ui->names[seat]) : 0;
    306             if (nl && !strncmp(t, ui->names[seat], nl)) {             /* movida: el nombre de su color */
    307                 char who[64];
    308                 snprintf(who, sizeof who, "%.*s", (int)nl, t);
    309                 tk_att(tk_xterm(PLAYER_COLOR[seat]), TK_DEF, A_BOLD);
    310                 tk_put(line, x + 1, who, w - 2);
    311                 tk_att(base, TK_DEF, 0);
    312                 int wn = tk_ulen(who);
    313                 tk_put(line++, x + 1 + wn, t + nl, w - 2 - wn);
    314             } else tk_put(line++, x + 1, t, w - 2);
    315         }
    316         if (scroll > 0) {
    317             tk_att(tk_xterm(C_ACCENT), TK_DEF, 0);
    318             tk_put(bottom - 1, x + w - 12, g_lang ? "▼ End" : "▼ Fin", 10);
    319         }
    320     }
    321 }
    322 
    323 /* ------------------------------------------------------------------ modales */
    324 
    325 static void draw_menu_modal(Frontend *f, const Ctrl *c, int bx, int by, int bw, int bh)
    326 {
    327     int w = 44, h = c->nitems + 4;
    328     const char *title = tr(c->menu_title);
    329     if (c->intent == I_RESPOND) h += 2;
    330     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    331     tk_box(y, x, h, w, title, C_ACCENT);
    332     int line = y + 2;
    333     if (c->intent == I_RESPOND) {
    334         tk_att(tk_xterm(C_TEXT), 236, 0);
    335         tk_put(line, x + 2, ctrl_prompt(c), w - 4);
    336         line += 2;
    337     }
    338     for (int i = 0; i < c->nitems; i++) {
    339         const MenuItem *it = &c->items[i];
    340         char buf[80];
    341         snprintf(buf, sizeof buf, " %c  %s", it->key, it->label);
    342         if (i == c->icursor) tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD);
    343         else tk_att(it->enabled ? 255 : 242, 236, 0);
    344         tk_fill(line, x + 2, w - 4, " ");
    345         tk_put(line, x + 2, buf, w - 4);
    346         hit_add(&f->hits, x + 2, line, w - 4, 1, PK_ITEM, i);
    347         line++;
    348     }
    349 }
    350 
    351 static void draw_cards_modal(Frontend *f, const Game *g, const Ctrl *c, int bx, int by, int bw, int bh)
    352 {
    353     bool offer = c->intent == I_OFFER || c->intent == I_COUNTER;
    354     int w = 52, h = offer ? 12 : 10;
    355     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    356     tk_box(y, x, h, w, ctrl_prompt(c), C_ACCENT);
    357     int cx0 = x + 10;
    358     for (int r = 0; r < NRES; r++) {
    359         tk_attc(C_BLACK, TERRAIN_COLOR[r], A_BOLD);
    360         char buf[12];
    361         snprintf(buf, sizeof buf, " %-5.5s", res_short(r));
    362         mvaddstr(y + 2, cx0 + r * 8, buf);
    363     }
    364     int rows = offer ? 2 : 1;
    365     for (int row = 0; row < rows; row++) {
    366         int ly = y + 3 + row;
    367         tk_att(255, 236, row == c->row && offer ? A_BOLD : 0);
    368         tk_put(ly, x + 2, offer ? (row ? tr(S_GET) : tr(S_GIVE)) : "", 7);
    369         const uint8_t *sel = row ? c->sel2 : c->sel;
    370         for (int r = 0; r < NRES; r++) {
    371             bool curcell = row == c->row && r == c->rescur;
    372             int cx = cx0 + r * 8;
    373             tk_att(250, 236, 0);
    374             mvaddstr(ly, cx, "-");
    375             hit_add(&f->hits, cx, ly, 1, 1, PK_CARD, row * NRES * 2 + r * 2 + 1);
    376             char num[8];
    377             snprintf(num, sizeof num, "%2d", sel[r]);
    378             if (curcell) tk_att(tk_xterm(C_BLACK), tk_xterm(C_CURSOR), A_BOLD);
    379             else tk_att(sel[r] ? 255 : 244, 236, sel[r] ? A_BOLD : 0);
    380             mvaddstr(ly, cx + 1, num);
    381             tk_att(250, 236, 0);
    382             mvaddstr(ly, cx + 3, "+");
    383             hit_add(&f->hits, cx + 3, ly, 1, 1, PK_CARD, row * NRES * 2 + r * 2);
    384         }
    385     }
    386     int ly = y + 3 + rows;
    387     tk_att(246, 236, 0);
    388     tk_put(ly, x + 2, g->p[c->actor].neutral ? "" : (g_lang ? "Have" : "Tenés"), 7);
    389     for (int r = 0; r < NRES; r++) {
    390         char num[8];
    391         snprintf(num, sizeof num, "%2d", (c->intent == I_PLENTY || c->intent == I_GOLD) ? g->bank[r] : g->p[c->actor].res[r]);
    392         mvaddstr(ly, cx0 + r * 8 + 1, num);
    393     }
    394     tk_att(244, 236, 0);
    395     tk_put(y + h - 4, x + 2, g_lang ? "←→ resource  ↑↓/+- amount  1-5 add" : "←→ recurso  ↑↓/+- cantidad  1-5 suma", w - 4);
    396     if (offer) tk_put(y + h - 3, x + 2, g_lang ? "Tab: switch give/get row" : "Tab: cambiar fila das/pedís", w - 4);
    397     tk_att(tk_xterm(C_BLACK), tk_xterm(C_OK), A_BOLD);
    398     mvaddstr(y + h - 2, x + w - 22, " OK ");
    399     hit_add(&f->hits, x + w - 22, y + h - 2, 4, 1, PK_CARD, 100);
    400     tk_att(tk_xterm(C_BLACK), 250, 0);
    401     mvaddstr(y + h - 2, x + w - 16, g_lang ? " Cancel " : " Cancelar ");
    402     hit_add(&f->hits, x + w - 16, y + h - 2, 10, 1, PK_CARD, 101);
    403 }
    404 
    405 static void draw_over(const Game *g, const UiInfo *ui, int bx, int by, int bw, int bh)
    406 {
    407     int w = 48, h = g->o.np + 6 + (ui->can_free ? 2 : 0) + (ui->can_again ? 1 : 0);
    408     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    409     tk_box(y, x, h, w, tr(S_P_OVER), C_ACCENT);
    410     char buf[96];
    411     if (g->winner >= 0) {
    412         snprintf(buf, sizeof buf, "★ %s %s ★", ui->names[g->winner], tr(S_WINS));
    413         tk_att(tk_xterm(C_ACCENT), 236, A_BOLD);
    414         tk_put(y + 2, x + (w - tk_ulen(buf)) / 2, buf, w - 4);
    415     }
    416     for (int p = 0; p < g->o.np; p++) {
    417         if (g->p[p].neutral) continue;
    418         snprintf(buf, sizeof buf, "%-16s %2d %s", ui->names[p], game_vp(g, p), tr(S_VP));
    419         tk_attc(PLAYER_COLOR[p], RGB(48, 48, 48), 0);
    420         mvaddstr(y + 4 + p, x + 3, "██");
    421         tk_att(255, 236, p == g->winner ? A_BOLD : 0);
    422         tk_put(y + 4 + p, x + 6, buf, w - 8);
    423     }
    424     int ly = y + 5 + g->o.np;
    425     if (ui->can_free) {
    426         tk_att(tk_xterm(C_OK), 236, 0);
    427         tk_put(ly, x + (w - tk_ulen(tr(S_GO_FREE))) / 2, tr(S_GO_FREE), w - 4);
    428         ly++;
    429     }
    430     if (ui->can_again) {
    431         tk_att(tk_xterm(C_ACCENT), 236, 0);
    432         tk_put(ly, x + (w - tk_ulen(tr(S_AGAIN))) / 2, tr(S_AGAIN), w - 4);
    433     }
    434 }
    435 
    436 static void draw_help(int bx, int by, int bw, int bh)
    437 {
    438     static const char *ES[] = {
    439         "Enter / Espacio   confirmar, abrir acción",
    440         "↑↓←→ / hjkl       mover el cursor (Tab: próximo válido)",
    441         "Esc               volver / cancelar",
    442         "r                 tirar los dados",
    443         "1 2 3 4           ruta, pueblo, ciudad, carta",
    444         "d                 jugar carta de desarrollo",
    445         "b                 comerciar con banco/puerto",
    446         "t                 ofrecer trueque",
    447         "o                 contraofertar (al responder)",
    448         "F2 / click abajo  chat (/w nombre: privado)",
    449         "RePág AvPág Fin   recorrer el registro",
    450         "F5                exportar la partida (texto)",
    451         "e                 terminar el turno",
    452         "u                 deshacer (construcciones y banco)",
    453         "Tab               siguiente objetivo",
    454         "L                 cambiar idioma",
    455         "q                 salir (la partida se guarda)",
    456         "Mouse             click en el tablero y menús" };
    457     static const char *EN[] = {
    458         "Enter / Space     confirm, open action",
    459         "↑↓←→ / hjkl       move the cursor",
    460         "Esc               back / cancel",
    461         "r                 roll the dice",
    462         "1 2 3 4           road, settlement, city, card",
    463         "d                 play a development card",
    464         "b                 trade with bank/port",
    465         "t                 offer a trade",
    466         "o                 counter-offer (when answering)",
    467         "F2 / click below  chat (/w name: private)",
    468         "PgUp PgDn End     scroll the log",
    469         "F5                export the game (text)",
    470         "e                 end the turn",
    471         "u                 undo (builds and bank trades)",
    472         "Tab               next target",
    473         "L                 switch language",
    474         "q                 quit (the game is saved)",
    475         "Mouse             click on the board and menus" };
    476     const char **L = g_lang ? EN : ES;
    477     int n = 18, w = 56;
    478     bool cards = bh >= n + 1 + NDEV + 4;     /* que hace cada carta, si la terminal es alta */
    479     int h = n + (cards ? 1 + NDEV : 0) + 4;
    480     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    481     tk_box(y, x, h, w, g_lang ? "Help" : "Ayuda", C_ACCENT);
    482     tk_att(255, 236, 0);
    483     for (int i = 0; i < n; i++) tk_put(y + 2 + i, x + 3, L[i], w - 6);
    484     if (cards) {
    485         tk_att(tk_xterm(C_ACCENT), 236, A_BOLD);
    486         tk_put(y + 2 + n, x + 3, tr(S_DEV_CARDS), w - 6);
    487         tk_att(255, 236, 0);
    488         for (int d = 0; d < NDEV; d++) {
    489             char t[120];
    490             snprintf(t, sizeof t, "%-18s %s", dev_name(d), tr(S_DD_KNIGHT + d));
    491             tk_put(y + 3 + n + d, x + 3, t, w - 6);
    492         }
    493     }
    494 }
    495 
    496 static void draw_confirm(const UiInfo *ui, int bx, int by, int bw, int bh)
    497 {
    498     int w = 46, h = 6;
    499     int x = bx + (bw - w) / 2, y = by + (bh - h) / 2;
    500     tk_box(y, x, h, w, "?", C_ERR);
    501     tk_att(255, 236, A_BOLD);
    502     tk_put(y + 2, x + 3, ui->modal_text ? ui->modal_text : "", w - 6);
    503     tk_att(250, 236, 0);
    504     tk_put(y + 3, x + 3, g_lang ? "[y] yes   [n] no" : "[s] sí   [n] no", w - 6);
    505 }
    506 
    507 /* ------------------------------------------------------------------ pantalla */
    508 
    509 static void tui_draw_game(Frontend *f, const Game *g, const Ctrl *c, const UiInfo *ui)
    510 {
    511     f->hits.n = 0;
    512     tk_clear();
    513     int H = LINES, W = COLS;
    514     if (W < 80 || H < 24) {
    515         tk_att(tk_xterm(C_ERR), TK_DEF, A_BOLD);
    516         mvaddstr(0, 0, g_lang ? "Make the terminal bigger (80x24 min)" : "Agrandá la terminal (mínimo 80x24)");
    517         refresh();
    518         return;
    519     }
    520     int PW = W >= 140 ? 48 : W >= 110 ? 42 : 36;
    521     int BW = W - PW - 1, BH = H - 3;
    522 
    523     /* barra de titulo */
    524     char buf[160];
    525     tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD);
    526     tk_fill(0, 0, W, " ");
    527     mvaddstr(0, 1, "CATAN");
    528     int n = snprintf(buf, sizeof buf, "  %s %d", g_lang ? "Turn" : "Turno", g->turn + 1);
    529     if (g->dice[0]) n += snprintf(buf + n, sizeof buf - (size_t)n, "  ·  %s %d+%d", tr(S_DICE), g->dice[0], g->dice[1]);
    530     if (g->dice[2]) n += snprintf(buf + n, sizeof buf - (size_t)n, " / %d+%d", g->dice[2], g->dice[3]);
    531     if (g->winner >= 0 && g->phase != PH_OVER)
    532         n += snprintf(buf + n, sizeof buf - (size_t)n, "  ·  ★ %s %s", ui->names[g->winner], tr(S_WON_FREE));
    533     tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), 0);
    534     tk_put(0, 7, buf, W - 8);
    535     snprintf(buf, sizeof buf, "%s %d · %s", tr(S_DECK), g->deck_n, tr(S_BANK));
    536     int bx = W - 1 - tk_ulen(buf) - NRES * 4;
    537     tk_put(0, bx, buf, W);
    538     bx += tk_ulen(buf) + 1;
    539     for (int r = 0; r < NRES; r++) {
    540         char nb[8];
    541         snprintf(nb, sizeof nb, "%2d", g->bank[r]);
    542         tk_attc(C_BLACK, TERRAIN_COLOR[r], A_BOLD);
    543         mvaddstr(0, bx, nb);
    544         bx += 3;
    545     }
    546     if (ui->status && *ui->status) {
    547         tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), 0);
    548         tk_put(0, 40, ui->status, bx - 40 - NRES * 3 - 12);
    549     }
    550 
    551     draw_board(f, g, c, 0, 1, BW, BH);
    552     draw_panel(f, g, c, ui, BW + 1, 1, PW, BH);
    553 
    554     /* instruccion y mensajes */
    555     tk_att(tk_xterm(C_ACCENT), TK_DEF, A_BOLD);
    556     tk_fill(H - 2, 0, W, " ");
    557     mvaddstr(H - 2, 1, "▶ ");
    558     tk_put(H - 2, 3, ui->preview ? ui->preview : ctrl_prompt(c), W - 4);
    559     tk_fill(H - 1, 0, W, " ");
    560     if (ui->preview) {
    561         tk_att(244, TK_DEF, 0);
    562         tk_put(H - 1, 1, g_lang ? "R reshuffle · ← → change map · Enter play · Esc back"
    563                                 : "R otro reparto · ← → cambiar de mapa · Enter jugar · Esc volver", W - 2);
    564     } else if (c->msg[0]) { tk_att(tk_xterm(c->msg_err ? C_ERR : C_OK), TK_DEF, A_BOLD); tk_put(H - 1, 1, c->msg, W - 2); }
    565     else if (ui->chat_focus) {
    566         tk_att(244, TK_DEF, 0);
    567         tk_put(H - 1, 1, g_lang ? "Enter send · Esc cancel · /w name text: private" : "Enter manda · Esc cancela · /w nombre texto: privado", W - 2);
    568     } else {
    569         tk_att(244, TK_DEF, 0);
    570         tk_put(H - 1, 1, g_lang ? "Enter action · arrows move · Esc back · ? help · F2 chat · PgUp log · L language · q quit"
    571                              : "Enter acción · flechas mover · Esc volver · ? ayuda · F2 chat · RePág registro · L idioma · q salir", W - 2);
    572     }
    573 
    574     if (c->mode == CM_MENU) draw_menu_modal(f, c, 0, 1, BW, BH);
    575     else if (c->mode == CM_CARDS) draw_cards_modal(f, g, c, 0, 1, BW, BH);
    576     else if (c->mode == CM_HANDOFF) {
    577         int w = 50, h = 7, x = (BW - w) / 2, y = 1 + (BH - h) / 2;
    578         tk_box(y, x, h, w, "", C_ACCENT);
    579         snprintf(buf, sizeof buf, "%s %s", tr(S_P_HANDOFF), ui->names[c->actor]);
    580         tk_attc(PLAYER_COLOR[c->actor], RGB(48, 48, 48), A_BOLD);
    581         tk_put(y + 2, x + (w - tk_ulen(buf)) / 2, buf, w - 4);
    582         tk_att(250, 236, 0);
    583         tk_put(y + 4, x + (w - tk_ulen(tr(S_P_ENTER))) / 2, tr(S_P_ENTER), w - 4);
    584         hit_add(&f->hits, x, y, w, h, PK_ITEM, 0);
    585     } else if (c->mode == CM_OVER || g->phase == PH_OVER) draw_over(g, ui, 0, 1, BW, BH);
    586     if (ui->show_help) draw_help(0, 1, BW, BH);
    587     if (ui->modal) draw_confirm(ui, 0, 1, BW, BH);
    588     refresh();
    589 }
    590 
    591 static void tui_draw_form(Frontend *f, const Form *form) { tk_draw_form(form, &f->hits); }
    592 
    593 /* logo de los menus: cinco fichas con las letras */
    594 static void form_logo(int cols)
    595 {
    596     static const char *LOGO[] = { "C", "A", "T", "A", "N" };
    597     int lx = cols / 2 - 9;
    598     for (int i = 0; i < 5; i++) {
    599         tk_attc(C_BLACK, TERRAIN_COLOR[i], A_BOLD);
    600         char b[8];
    601         snprintf(b, sizeof b, " %s ", LOGO[i]);
    602         mvaddstr(1, lx + i * 4, b);
    603     }
    604 }
    605 
    606 static int tui_wait(Frontend *f, int timeout_ms, const int *fds, int nfds, InEvent *ev)
    607 {
    608     (void)f;
    609     return tk_wait(timeout_ms, fds, nfds, ev);
    610 }
    611 
    612 static void tui_close(Frontend *f)
    613 {
    614     (void)f;
    615     tk_close();
    616 }
    617 
    618 Frontend *tui_open(void)
    619 {
    620     static Frontend fe;
    621     tk_open();
    622     tk_form_logo = form_logo;
    623     memset(&fe, 0, sizeof fe);
    624     fe.draw_game = tui_draw_game;
    625     fe.draw_form = tui_draw_form;
    626     fe.wait = tui_wait;
    627     fe.close = tui_close;
    628     return &fe;
    629 }