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 (14912B)


      1 /* tui.c - interfaz de terminal (ncurses, sobre comun/linux/tuikit).
      2  *
      3  * El tablero son celdas de texto con el fondo del tema y las piezas como glifos
      4  * Unicode rellenos, coloreados por bando (como el ajedrez en bash). El tamano
      5  * de las casillas se adapta a la terminal. */
      6 #include "tuikit.h"
      7 #include <stdlib.h>
      8 #include <string.h>
      9 #include "frontend.h"
     10 
     11 /* ♙ para los peones de los dos bandos: ♟ (U+265F) muchas terminales lo muestran
     12  * como emoji y no respetan el color (el ajedrez en bash tenia el mismo problema) */
     13 static const char *const GLYPH[7] = { " ", "♙", "♞", "♝", "♜", "♛", "♚" };
     14 
     15 #define FG_WHITE 231
     16 #define FG_BLACK 16
     17 
     18 static void cat(char *b, int n, const char *s) { size_t l = strlen(b); snprintf(b + l, (size_t)n - l, "%s", s); }
     19 
     20 /* casilla (fila/columna de pantalla) de una casilla del tablero */
     21 static void cell_of(const View *v, int sq, int *col, int *row)
     22 {
     23     int f = FILE_OF(sq), r = RANK_OF(sq);
     24     if (v->c && v->c->flipped) { f = 7 - f; r = 7 - r; }
     25     *col = f;
     26     *row = 7 - r;
     27 }
     28 
     29 static void draw_board(Frontend *fe, const View *v, int y0, int x0, int sw, int sh)
     30 {
     31     const Theme *th = &THEMES[v->theme % NTHEMES];
     32     const Game *g = v->g;
     33     const Ctrl *c = v->c;
     34     int lf = -1, lt = -1;
     35     if (g->nply) { lf = MFROM(g->mv[g->nply - 1]); lt = MTO(g->mv[g->nply - 1]); }
     36     int check_sq = -1;
     37     if ((g->result == R_NONE && pos_in_check(&g->pos)) || g->reason == RS_MATE) check_sq = g->pos.king[g->pos.side];
     38     for (int sq = 0; sq < 64; sq++) {
     39         int col, row;
     40         cell_of(v, sq, &col, &row);
     41         int x = x0 + col * sw, y = y0 + row * sh;
     42         bool light = (FILE_OF(sq) + RANK_OF(sq)) & 1;
     43         short bg = light ? th->xterm[TH_LIGHT] : th->xterm[TH_DARK];
     44         if (sq == lf || sq == lt) bg = th->xterm[TH_PATH];
     45         if (c && sq == c->sel) bg = th->xterm[TH_FROM];
     46         if (sq == check_sq) bg = th->xterm[TH_CAPTURE];
     47         bool target = c && ctrl_is_target(c, sq);
     48         bool cursor = c && sq == c->cursor && g->result == R_NONE;
     49         for (int j = 0; j < sh; j++) {
     50             tk_att(0, bg, 0);
     51             tk_fill(y + j, x, sw, " ");
     52         }
     53         int cy = y + sh / 2, cx = x + sw / 2;
     54         uint8_t pc = g->pos.sq[sq];
     55         if (pc) {
     56             tk_att(PCOLOR(pc) == WHITE ? FG_WHITE : FG_BLACK, bg, A_BOLD);
     57             mvaddstr(cy, cx, GLYPH[PTYPE(pc)]);
     58         }
     59         if (target) {
     60             short tc = pc ? th->xterm[TH_CAPTURE] : th->xterm[TH_TARGET];
     61             if (pc) {
     62                 /* captura: el borde de la casilla en el color de captura */
     63                 tk_att(0, tc, 0);
     64                 for (int j = 0; j < sh; j++) { mvaddstr(y + j, x, " "); mvaddstr(y + j, x + sw - 1, " "); }
     65             } else {
     66                 tk_att(tc, bg, A_BOLD);
     67                 mvaddstr(cy, cx, "●");
     68             }
     69         }
     70         if (cursor) {
     71             tk_att(0, tk_xterm(C_CURSOR), A_BOLD);
     72             if (sw >= 5) {
     73                 mvaddstr(y, x, "▛"); mvaddstr(y, x + sw - 1, "▜");
     74                 if (sh >= 2) { mvaddstr(y + sh - 1, x, "▙"); mvaddstr(y + sh - 1, x + sw - 1, "▟"); }
     75                 tk_att(tk_xterm(C_CURSOR), bg, A_BOLD);
     76                 mvaddstr(y, x, "▛"); mvaddstr(y, x + sw - 1, "▜");
     77                 if (sh >= 2) { mvaddstr(y + sh - 1, x, "▙"); mvaddstr(y + sh - 1, x + sw - 1, "▟"); }
     78             } else {
     79                 tk_att(tk_xterm(C_CURSOR), bg, A_BOLD);
     80                 mvaddstr(cy, x, "[");
     81                 mvaddstr(cy, x + sw - 1, "]");
     82             }
     83         }
     84         hit_add(&fe->hits, x, y, sw, sh, HIT_SQUARE, sq);
     85     }
     86     /* coordenadas */
     87     bool fl = c && c->flipped;
     88     tk_att(244, TK_DEF, 0);
     89     for (int i = 0; i < 8; i++) {
     90         char fc[2] = { (char)('a' + (fl ? 7 - i : i)), 0 }, rc[2] = { (char)('8' - (fl ? 7 - i : i)), 0 };
     91         mvaddstr(y0 + 8 * sh, x0 + i * sw + sw / 2, fc);
     92         mvaddstr(y0 + i * sh + sh / 2, x0 - 2, rc);
     93     }
     94     /* coronacion: las cuatro piezas en una cajita */
     95     if (c && c->mode == CM_PROMO) {
     96         int bw = 4 * 4 + 3, bx = x0 + 4 * sw - bw / 2, by = y0 + 4 * sh - 2;
     97         tk_box(by, bx, 3, bw, "", C_ACCENT);
     98         for (int k = 0; k < 4; k++) {
     99             int px = bx + 2 + k * 4;
    100             bool on = k == c->promo_cur;
    101             tk_att(g->pos.side == WHITE ? FG_WHITE : FG_BLACK, on ? tk_xterm(C_ACCENT) : 238, A_BOLD);
    102             mvaddstr(by + 1, px, " ");
    103             mvaddstr(by + 1, px + 1, GLYPH[MPROMO(c->promo[k])]);
    104             mvaddstr(by + 1, px + 2, " ");
    105             hit_add(&fe->hits, px, by + 1, 3, 1, HIT_PROMO, k);
    106         }
    107     }
    108 }
    109 
    110 static void status_text(const View *v, char *buf, int n)
    111 {
    112     const Game *g = v->g;
    113     buf[0] = 0;
    114     if (g->result == R_DRAW) { cat(buf, n, tr(S_DRAW)); cat(buf, n, " · "); cat(buf, n, reason_text(g->reason)); }
    115     else if (g->result != R_NONE) {
    116         cat(buf, n, color_name(g->result == R_WHITE ? WHITE : BLACK));
    117         cat(buf, n, " ");
    118         cat(buf, n, tr(S_WINS));
    119         cat(buf, n, " · ");
    120         cat(buf, n, reason_text(g->reason));
    121     } else {
    122         const char *nm = v->names[g->pos.side] && *v->names[g->pos.side] ? v->names[g->pos.side] : color_name(g->pos.side);
    123         if (g_lang) { cat(buf, n, nm); cat(buf, n, " to move"); }
    124         else { cat(buf, n, "Juegan "); cat(buf, n, nm); }
    125         if (pos_in_check(&g->pos)) { cat(buf, n, " · "); cat(buf, n, tr(S_CHECK)); }
    126     }
    127 }
    128 
    129 static void player_line(const View *v, int color, int y, int x, int w)
    130 {
    131     const Game *g = v->g;
    132     bool active = g->result == R_NONE && g->pos.side == color;
    133     tk_att(active ? 255 : 250, active ? 238 : 236, active ? A_BOLD : 0);
    134     tk_fill(y, x, w, " ");
    135     tk_att(color == WHITE ? FG_WHITE : FG_BLACK, active ? 238 : 236, A_BOLD);
    136     mvaddstr(y, x + 1, "♚");
    137     tk_att(active ? 255 : 250, active ? 238 : 236, active ? A_BOLD : 0);
    138     const char *nm = v->names[color] && *v->names[color] ? v->names[color] : color_name(color);
    139     int cw = 0;
    140     if (v->has_clock) {
    141         char clk[16];
    142         fmt_clock(v->clock[color], clk, sizeof clk);
    143         cw = tk_ulen(clk) + 1;
    144         if (active && v->clock[color] < 20000) tk_att(tk_xterm(C_ERR), 238, A_BOLD);
    145         else if (active) tk_att(tk_xterm(C_ACCENT), 238, A_BOLD);
    146         mvaddstr(y, x + w - cw, clk);
    147         tk_att(active ? 255 : 250, active ? 238 : 236, active ? A_BOLD : 0);
    148     }
    149     tk_put(y, x + 3, nm, w - 4 - cw);
    150 }
    151 
    152 static void captured_line(const View *v, int who, int y, int x, int w)
    153 {
    154     int cap[2][7];
    155     captured_pieces(v->g, cap);
    156     char buf[128] = "";
    157     for (int t = QUEEN; t >= PAWN; t--)
    158         for (int k = 0; k < cap[who][t]; k++) cat(buf, sizeof buf, GLYPH[t]);
    159     tk_att(who == WHITE ? FG_WHITE : 245, TK_DEF, 0);
    160     tk_put(y, x + 1, buf, w - 2);
    161 }
    162 
    163 static void draw_panel(const View *v, int y, int x, int w, int h)
    164 {
    165     const Game *g = v->g;
    166     bool fl = v->c && v->c->flipped;
    167     int top = fl ? WHITE : BLACK, bot = top ^ 1;
    168     int ybot = y + h - 1;
    169     player_line(v, top, y, x, w);
    170     captured_line(v, bot, y + 1, x, w);
    171     char buf[200];
    172     int ly = y + 3;
    173     status_text(v, buf, sizeof buf);
    174     tk_att(g->result != R_NONE ? tk_xterm(C_ACCENT) : 252, TK_DEF, A_BOLD);
    175     tk_put(ly++, x + 1, buf, w - 2);
    176     if (v->status && *v->status) { tk_att(245, TK_DEF, 0); tk_put(ly++, x + 1, v->status, w - 2); }
    177     if (v->opening) { tk_att(tk_xterm(C_OK), TK_DEF, 0); tk_put(ly++, x + 1, v->opening, w - 2); }
    178     if (v->show_eval && v->eval_valid) {
    179         char e[24];
    180         int ev = v->eval;
    181         if (MATE_IN(ev)) snprintf(e, sizeof e, "%sM%d", ev > 0 ? "+" : "-", (MATE - abs(ev) + 1) / 2);
    182         else snprintf(e, sizeof e, "%+.2f", ev / 100.0);
    183         int bw = w - 10;
    184         int cp = ev > 800 ? 800 : ev < -800 ? -800 : ev;
    185         int wx = MATE_IN(ev) ? (ev > 0 ? bw : 0) : bw / 2 + cp * bw / 1600;
    186         tk_att(250, TK_DEF, 0);
    187         mvaddstr(ly, x + 1, e);
    188         for (int i = 0; i < bw; i++) { tk_att(0, i < wx ? 255 : 234, 0); mvaddstr(ly, x + 9 + i, " "); }
    189         ly++;
    190     }
    191     if (v->banner) {
    192         ly++;
    193         tk_att(0, tk_xterm(C_ACCENT), A_BOLD);
    194         tk_fill(ly, x, w, " ");
    195         tk_put(ly++, x + 1, v->banner, w - 2);
    196     }
    197     ly++;
    198     tk_section(ly++, x, w, tr(S_MOVES));
    199     /* jugadas: las ultimas que entran */
    200     int rows = ybot - 2 - ly;
    201     if (rows < 1) rows = 1;
    202     int first_side = g->start.side;
    203     int total = (g->nply + first_side + 1) / 2;
    204     int first_row = total > rows ? total - rows : 0;
    205     Pos p = g->start;
    206     int ply = 0, target = first_row * 2 - first_side;
    207     if (target < 0) target = 0;
    208     for (; ply < target; ply++) { Undo u; pos_make(&p, g->mv[ply], &u); }
    209     for (int r = first_row; r < total && ply < g->nply; r++) {
    210         char num[12];
    211         snprintf(num, sizeof num, "%d.", g->start.fullmove + r);
    212         tk_att(244, TK_DEF, 0);
    213         mvaddstr(ly, x + 1, num);
    214         for (int col = 0; col < 2 && ply < g->nply; col++) {
    215             if (r == 0 && col == 0 && first_side == BLACK) { mvaddstr(ly, x + 6, "..."); continue; }
    216             char san[16];
    217             move_san(&p, g->mv[ply], san, sizeof san);
    218             bool last = ply == g->nply - 1;
    219             tk_att(last ? tk_xterm(C_ACCENT) : 252, TK_DEF, last ? A_BOLD : 0);
    220             tk_put(ly, x + 6 + col * 10, san, 9);
    221             Undo u;
    222             pos_make(&p, g->mv[ply], &u);
    223             ply++;
    224         }
    225         ly++;
    226     }
    227     captured_line(v, top, ybot - 1, x, w);
    228     player_line(v, bot, ybot, x, w);
    229 }
    230 
    231 static void draw_help(int y, int x, int w)
    232 {
    233     const char *t = tr(S_HELP_KEYS);
    234     char lines[24][96];
    235     int n = 0;
    236     while (*t && n < 24) {
    237         int k = 0;
    238         while (*t && !(t[0] == ' ' && t[1] == '\xc2')) { if (k < 95) lines[n][k++] = *t; t++; }
    239         lines[n++][k] = 0;
    240         if (*t) t += 4;
    241         while (*t == ' ') t++;
    242     }
    243     tk_box(y, x, n + 2, w, g_lang ? "Keys" : "Teclas", C_ACCENT);
    244     tk_att(252, 236, 0);
    245     for (int i = 0; i < n; i++) tk_put(y + 1 + i, x + 2, lines[i], w - 4);
    246 }
    247 
    248 static void tui_draw_game(Frontend *f, const View *v)
    249 {
    250     f->hits.n = 0;
    251     tk_clear();
    252     int H = LINES, W = COLS;
    253     if (W < 60 || H < 20) {
    254         tk_att(tk_xterm(C_ERR), TK_DEF, A_BOLD);
    255         mvaddstr(0, 0, g_lang ? "Make the terminal bigger (60x20 min)" : "Agrandá la terminal (mínimo 60x20)");
    256         refresh();
    257         return;
    258     }
    259     /* tamano de casilla: el mas grande que deja lugar al panel */
    260     int sh = (H - 4) / 8;
    261     if (sh > 2) sh = 2;     /* casillas chicas: la pieza (un solo caracter) tiene que llenar la casilla */
    262     int sw = sh * 2 + 1;
    263     while (sh > 1 && 8 * sw + 3 + 28 > W) { sh--; sw = sh * 2 + 1; }
    264     if (8 * sw + 3 + 28 > W) sw = 3;
    265     int bx = 3, by = 1;
    266     tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), A_BOLD);
    267     tk_fill(0, 0, W, " ");
    268     mvaddstr(0, 1, tr(S_TITLE));
    269     if (v->status && *v->status) { tk_att(tk_xterm(C_BLACK), tk_xterm(C_ACCENT), 0); tk_put(0, 10, v->status, W - 12); }
    270     draw_board(f, v, by, bx, sw, sh);
    271     int px = bx + 8 * sw + 2, pw = W - px - 1;
    272     if (pw > 48) pw = 48;
    273     draw_panel(v, by, px, pw, 8 * sh + 1);
    274     /* instruccion y mensajes */
    275     const Ctrl *c = v->hide_prompt ? 0 : v->c;
    276     tk_att(tk_xterm(C_ACCENT), TK_DEF, A_BOLD);
    277     tk_fill(H - 2, 0, W, " ");
    278     char buf[160] = "";
    279     if (c && c->mode == CM_ENTRY) snprintf(buf, sizeof buf, "▶ %s %s█", tr(S_P_ENTRY), c->entry);
    280     else if (c && c->g && c->g->result == R_NONE && !ctrl_my_turn(c)) {
    281         int side = c->g->pos.side;
    282         snprintf(buf, sizeof buf, "▶ %s %s", ctrl_prompt(c), v->names[side] && *v->names[side] ? v->names[side] : color_name(side));
    283     } else if (c) snprintf(buf, sizeof buf, "▶ %s", ctrl_prompt(c));
    284     tk_put(H - 2, 1, buf, W - 2);
    285     tk_fill(H - 1, 0, W, " ");
    286     if (c && 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); }
    287     else if (c) {
    288         tk_att(244, TK_DEF, 0);
    289         tk_put(H - 1, 1, g_lang ? "Enter pick · arrows move · Tab next · : type move · m menu · ? help · q quit"
    290                                 : "Enter elegir · flechas mover · Tab siguiente · : escribir jugada · m menú · ? ayuda · q salir", W - 2);
    291     }
    292     if (v->show_help) draw_help(by + 1, bx + 1, 8 * sw > 60 ? 60 : 8 * sw);
    293     if (v->modal) {
    294         int w = 8 * sw - 4 > 56 ? 56 : 8 * sw - 4, h = v->modal2 ? 5 : 4;
    295         int x = bx + (8 * sw - w) / 2, y = by + (8 * sh - h) / 2;
    296         tk_box(y, x, h, w, "", C_ACCENT);
    297         tk_att(tk_xterm(C_ACCENT), 236, A_BOLD);
    298         tk_put(y + 1, x + (w - tk_ulen(v->modal)) / 2, v->modal, w - 4);
    299         if (v->modal2) { tk_att(250, 236, 0); tk_put(y + 3, x + (w - tk_ulen(v->modal2)) / 2, v->modal2, w - 4); }
    300     }
    301     refresh();
    302 }
    303 
    304 static void tui_draw_form(Frontend *f, const Form *form) { tk_draw_form(form, &f->hits); }
    305 
    306 /* logo de los menus: una fila de piezas */
    307 static void form_logo(int cols)
    308 {
    309     static const int ROW[] = { ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK };
    310     int lx = cols / 2 - 12;
    311     for (int i = 0; i < 8; i++) {
    312         tk_att(i & 1 ? FG_BLACK : FG_WHITE, (i & 1) ? THEMES[0].xterm[TH_LIGHT] : THEMES[0].xterm[TH_DARK], A_BOLD);
    313         mvaddstr(1, lx + i * 3, " ");
    314         mvaddstr(1, lx + i * 3 + 1, GLYPH[ROW[i]]);
    315         mvaddstr(1, lx + i * 3 + 2, " ");
    316     }
    317 }
    318 
    319 static int tui_wait(Frontend *f, int timeout_ms, const int *fds, int nfds, InEvent *ev)
    320 {
    321     (void)f;
    322     return tk_wait(timeout_ms, fds, nfds, ev);
    323 }
    324 
    325 /* Para pegar texto largo se suelta la pantalla un momento y se lee la entrada
    326  * comun hasta Ctrl-D, como en el ajedrez en bash. */
    327 static char *tui_paste(Frontend *f)
    328 {
    329     (void)f;
    330     def_prog_mode();
    331     endwin();
    332     printf("\n%s\n\n", g_lang ? "Paste the game (PGN) or a position (FEN), then press Ctrl-D on an empty line:"
    333                               : "Pegá la partida (PGN) o una posición (FEN) y después Ctrl-D en una línea vacía:");
    334     fflush(stdout);
    335     size_t cap = 4096, n = 0;
    336     char *buf = malloc(cap), line[1024];
    337     while (buf && fgets(line, sizeof line, stdin)) {
    338         size_t l = strlen(line);
    339         if (n + l + 1 > cap) {
    340             if (cap > (4u << 20)) break;
    341             char *nb = realloc(buf, cap * 2);
    342             if (!nb) break;
    343             buf = nb;
    344             cap *= 2;
    345         }
    346         memcpy(buf + n, line, l);
    347         n += l;
    348     }
    349     if (buf) buf[n] = 0;
    350     clearerr(stdin);
    351     reset_prog_mode();
    352     flushinp();
    353     refresh();
    354     if (buf && !n) { free(buf); buf = 0; }
    355     return buf;
    356 }
    357 
    358 static void tui_close(Frontend *f)
    359 {
    360     (void)f;
    361     tk_close();
    362 }
    363 
    364 Frontend *tui_open(void)
    365 {
    366     static Frontend fe;
    367     tk_open();
    368     tk_form_logo = form_logo;
    369     memset(&fe, 0, sizeof fe);
    370     fe.draw_game = tui_draw_game;
    371     fe.draw_form = tui_draw_form;
    372     fe.wait = tui_wait;
    373     fe.close = tui_close;
    374     fe.paste = tui_paste;
    375     return &fe;
    376 }