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 5815b3a21937a54aaa2aa532c9cb926ba1ed25ad
parent 2c19c1f08abb3a6b3d91ac8b203fd315583ef770
Author: Francisco Armando Rosmino <franciscorosmino@rosmino.com.ar>
Date:   Sat,  3 Oct 2026 15:37:20 -0300

Bots: banco para rutas y barcos, nivel elegible online; aviso de ofertas

- search.c: las candidatas de cambio con el banco tambien apuntan a ruta y
  barco (con una pila de ladrillos y sin madera el bot dificil pasaba el
  turno sin cambiar).
- Crear partida online: 'Bots: Faciles/Dificiles' (11o argumento de CREATE,
  13o campo de OPTS; falta = dificiles, asi las partidas viejas no cambian).
- Oferta: cuando respondieron todos, la instruccion dice 'cerra el trato o
  cancela', y si aceptaron varios, 'elegi con quien cerrar'.

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

Diffstat:
MCLAUDE.md | 2+-
Mcatan/README.md | 4++--
Mcatan/core/ctrl.c | 9++++++++-
Mcatan/core/search.c | 8+++++---
Mcatan/core/strings.h | 2++
Mcatan/linux/online.c | 8+++++---
Mcatan/server/catan_game.c | 26+++++++++++++-------------
Mcatan/server/server.h | 2+-
8 files changed, 37 insertions(+), 24 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md @@ -76,7 +76,7 @@ Al correr pruebas o el binario, aislar `XDG_CONFIG_HOME`/`XDG_DATA_HOME` en un d - **Cursor del tablero**: las flechas caminan por vecinos (vértice→vértice, arista→arista que comparte vértice, hex→hex) aunque no sean válidos (`Ctrl.pos`, `ctrl_cursor_pos`; gris si no se puede); Tab salta al próximo válido. - **Colores**: `PLAYER_COLOR` es modificable (`PLAYER_COLOR_DEF`: rojo, azul, verde, naranja); "Colores de los jugadores" en el menú guarda `color1..4=#rrggbb` en la config (`settings_apply_colors`). Servidor online por defecto: `APP_SERVER` en `app_config.h` (`lu3dhn.xyz:7373`, el VPS de Oracle `ssh vps`; `catan-server` por systemd con datos en /var/lib/catan; puerto 7373 abierto en iptables y en la Security List de Oracle — se toca con `~/.oci/venv/bin/oci` o su SDK de Python). Actualizar: `make catan-server-static`, copiar a `/usr/local/bin/catan-server`, `systemctl restart catan-server`. - **Vista previa del mapa**: al darle Empezar/Crear (local, online y LAN) se muestra el tablero (`map_preview` en `linux/app.c`, `UiInfo.preview`; botones `PK_APP` 20-24): R otro reparto (nueva `bseed`), flechas izq/der otro mapa, Enter jugar, Esc vuelve al formulario. El formulario tiene "Mapa" (al azar o 1-5 con nombre, `map_choices`); con el tablero de principiante no hay vista previa. -- **Colocación inicial**: el pueblo y la ruta quedan en borrador en `Ctrl` (`draft_v/draft_e`, se dibujan en el color del jugador) hasta confirmar (`I_SETUP_OK`); `u`/Esc deshacen un paso; al confirmar salen SETTLE y la ROAD encolada (`ctrl_next`, que los bucles de `app.c`/`pico/app.c` vacían). Online, el host elige la colocación (9º campo de OPTS en `catan_game.c`: 0 por turnos, >0 plazo propio en minutos, -1 automática: el bot coloca por todos). +- **Colocación inicial**: el pueblo y la ruta quedan en borrador en `Ctrl` (`draft_v/draft_e`, se dibujan en el color del jugador) hasta confirmar (`I_SETUP_OK`); `u`/Esc deshacen un paso; al confirmar salen SETTLE y la ROAD encolada (`ctrl_next`, que los bucles de `app.c`/`pico/app.c` vacían). Online, el host elige la colocación (9º campo de OPTS en `catan_game.c`: 0 por turnos, >0 plazo propio en minutos, -1 automática: el bot coloca por todos) y el nivel de los bots (13º campo de OPTS / 11º de CREATE: 1 difíciles con búsqueda, 0 heurísticos; falta = difíciles). - **Registro**: completo (crece sin límite en `Session.log`). Marca + asiento al principio de cada línea: `\1` chat, `\2` privado (enteros del color de quien habla), `\3` movida (solo el nombre en color), `\4` algo que te pasó a vos (te robaron: resaltado). Robos: la carta la ven solo ladrón y víctima (`move_public`, `move_describe_for`). Panel: cada jugador muestra las fichas que le quedan (`gfx_piece`). - **Exportar**: `server/export.c` (compartido): cabecera, tablero, turno por turno (con dónde se construyó y qué produjo cada tirada) y movidas de máquina. El servidor mantiene `<partida>/partida.txt` al día con todo + chat (gancho `export` del `GameModule`); F5 en el cliente exporta lo que viste a `data_dir()/exportes/`. - **Contraofertas**: `M_COUNTER r=doy r2=pido` de quien responde (`offer.resp = 2`, términos en `offer.cgive/cget`); `M_CONFIRM` cierra con la aceptación o con la contraoferta de ese asiento. Los bots contraofertan (`bot_counter`), ofrecen 1 por 1 una vez por turno (`bot_offer`, `Game.offers`) y cierran con la mejor respuesta (`bot_close_offer`); mientras faltan respuestas `bot_decide` no juega y `host.c` espera `HOST_OFFER_WAIT` pasos (~15 s) antes de cerrar igual. diff --git a/catan/README.md b/catan/README.md @@ -99,8 +99,8 @@ Cómo se usa: 1. En `./catan` → *Jugar online*, poné `mivps.com:7373`, tu **nombre** y una **contraseña**. No hay cuentas: nombre + contraseña son tu llave (el cliente manda solo un hash). -2. *Crear partida*: elegís jugadores, qué asientos son bots y **"Bot juega si no hay"** - (nunca, 1, 4, 12, 24 o 48 horas). Te da un **código** de 6 letras; pasáselo a tus amigos. +2. *Crear partida*: elegís jugadores, qué asientos son bots, si los bots son fáciles o + difíciles, y **"Bot juega si no hay"** (nunca, 1, 4, 12, 24 o 48 horas). Te da un **código** de 6 letras; pasáselo a tus amigos. 3. Tus amigos: *Jugar online* → su nombre y contraseña → el código → *Unirse con el código*. 4. Se juega igual estén todos o no: los conectados ven todo en vivo y la partida da la vuelta hasta llegar a alguien que no está. Ahí espera; si pasa el tiempo elegido, el diff --git a/catan/core/ctrl.c b/catan/core/ctrl.c @@ -1022,8 +1022,15 @@ const char *ctrl_prompt(const Ctrl *c) default: if (g->phase == PH_ROLL) sb_s(&s, tr(S_P_ROLL)); else if (g->offer.active && g->offer.from == c->actor) { + int acc = 0, pending = 0; + for (int q = 0; q < g->o.np; q++) { + if (q == g->offer.from || !((g->offer.tomask >> q) & 1)) continue; + if (g->offer.resp[q] == 1) acc++; + else if (g->offer.resp[q] == 0) pending++; + } 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)); + sb_s(&s, tr(S_WANTS)); sb_s(&s, " "); sb_res(&s, g->offer.get); sb_s(&s, " — "); + sb_s(&s, tr(acc >= 2 ? S_PICK_DEAL : pending ? S_WAITING_RESP : S_ANSWERED)); } else sb_s(&s, tr(g->phase == PH_PAIRED ? S_P_PAIRED : S_P_MAIN)); break; } diff --git a/catan/core/search.c b/catan/core/search.c @@ -275,11 +275,13 @@ static void add_main(Cands *c, const Game *g, int p) } } - /* banco: por cada recurso que falta para algo, dar el que mas sobra */ - static const uint8_t *const COSTS[3] = { COST_SETTLE, COST_CITY, COST_DEV }; + /* banco: por cada recurso que falta para algo (tambien rutas y barcos: con una pila de + * ladrillos y sin madera, antes no se le ocurria cambiar), dar el que mas sobra */ + static const uint8_t *const COSTS[5] = { COST_SETTLE, COST_CITY, COST_DEV, COST_ROAD, COST_SHIP }; + int ncosts = game_has_sea(g) && pl->ships ? 5 : 4; for (int want = 0; want < NRES; want++) { bool need = false; - for (int k = 0; k < 3; k++) if (pl->res[want] < COSTS[k][want]) need = true; + for (int k = 0; k < ncosts; k++) if (pl->res[want] < COSTS[k][want]) need = true; if (!need || !g->bank[want]) continue; int give = -1, spare = 0; for (int r = 0; r < NRES; r++) { diff --git a/catan/core/strings.h b/catan/core/strings.h @@ -167,6 +167,8 @@ X(S_GET, "Pedís", "Get") \ X(S_NO_TARGETS, "No hay lugar disponible", "No valid spot") \ X(S_WAITING_RESP, "esperando respuestas", "waiting for answers") \ + X(S_PICK_DEAL, "aceptaron varios: elegí con quién cerrar", "several accepted: choose whom to deal with") \ + X(S_ANSWERED, "respondieron: cerrá el trato o cancelá", "all answered: close the deal or cancel") \ X(S_AND, "y", "and") \ X(S_BANK, "Banco", "Bank") \ X(S_DECK, "Mazo", "Deck") \ diff --git a/catan/linux/online.c b/catan/linux/online.c @@ -117,7 +117,8 @@ static bool play_remote(Frontend *fe, Settings *st, Net *n, Form *f, const char /* ------------------------------------------------------------ crear */ -enum { C_BACK = 0, C_CREATE = 1, C_PLAYERS = 10, C_VARIANT, C_BOARD, C_AWAY, C_SETUP, C_SCEN, C_MAP, C_SEAT = 20 }; +enum { C_BACK = 0, C_CREATE = 1, C_PLAYERS = 10, C_VARIANT, C_BOARD, C_AWAY, C_SETUP, C_SCEN, C_MAP, C_LEVEL, C_SEAT = 20 }; +static const char *LV_ES[] = { "Fáciles", "Difíciles" }, *LV_EN[] = { "Easy", "Hard" }; static const char *CH_NUM[] = { "2", "3", "4", "5", "6" }; static const char *YN_ES[] = { "No", "Sí" }, *YN_EN[] = { "No", "Yes" }; @@ -165,6 +166,7 @@ static bool create_form(Frontend *fe, int layout, char *cmd, int n) form_choice(&f, C_SCEN, en ? "Scenario" : "Escenario", en ? SCEN_CH_EN : SCEN_CH_ES, SCEN__COUNT, 0); form_choice(&f, C_BOARD, en ? "Board" : "Tablero", en ? BO_EN : BO_ES, 2, 0); form_choice(&f, C_MAP, en ? "Map" : "Mapa", map_choices(BF_CLASSIC), NMAPS + 1, 1); + form_choice(&f, C_LEVEL, en ? "Bots" : "Bots", en ? LV_EN : LV_ES, 2, 1); form_choice(&f, C_AWAY, en ? "Bot plays if away" : "Bot juega si no hay", en ? AW_EN : AW_ES, 6, 4); form_choice(&f, C_SETUP, en ? "Initial placement" : "Colocación inicial", en ? SU_EN : SU_ES, 5, 2); form_label(&f, en ? "Seat 1 is yours; 'Person' seats wait for players." : "El asiento 1 es tuyo; los de 'Persona' esperan gente."); @@ -195,9 +197,9 @@ static bool create_form(Frontend *fe, int layout, char *cmd, int n) } if (!map_preview(fe, layout, &o, mv == 0, nm)) continue; } - snprintf(cmd, (size_t)n, "CREATE %d %d %d %d %s %d %d %d %d %lu", np, v2p, scen ? 13 : 10, beg, seats, + snprintf(cmd, (size_t)n, "CREATE %d %d %d %d %s %d %d %d %d %lu %d", np, v2p, scen ? 13 : 10, beg, seats, AWAY_MIN[form_get(&f, C_AWAY)->value], SETUP_V[form_get(&f, C_SETUP)->value], scen, beg ? 0 : o.map, - beg ? 0ul : (unsigned long)o.bseed); + beg ? 0ul : (unsigned long)o.bseed, form_get(&f, C_LEVEL)->value); return true; } } diff --git a/catan/server/catan_game.c b/catan/server/catan_game.c @@ -54,10 +54,10 @@ static void on_move(void *ctx, const Move *m) c->emit(c->ctx, line, move_is_auto(m)); } -static void fmt_opts(const Opts *o, int away_min, int setup, char *out, int n) +static void fmt_opts(const Opts *o, int away_min, int setup, int hard, char *out, int n) { - snprintf(out, (size_t)n, "%d %d %d %d %d %d %d %d %d %d %d %lu", o->np, o->variant2p, o->vp_target, o->friendly_robber, - o->tokens_start, o->neutral_setup, o->beginner, away_min, setup, o->scenario, o->map, (unsigned long)o->bseed); + snprintf(out, (size_t)n, "%d %d %d %d %d %d %d %d %d %d %d %lu %d", o->np, o->variant2p, o->vp_target, o->friendly_robber, + o->tokens_start, o->neutral_setup, o->beginner, away_min, setup, o->scenario, o->map, (unsigned long)o->bseed, hard); } static uint32_t clock32(void) { return (uint32_t)srv_now_ms(); } @@ -66,19 +66,19 @@ static uint32_t clock32(void) { return (uint32_t)srv_now_ms(); } * partida), asi el reparto es otro; el tablero de principiante queda como es. */ static void cg_again(const char *opts, char *out, int n) { - int v[11] = { 0 }; + int v[11] = { 0 }, hard = 1; unsigned long bseed = 0; - int k = sscanf(opts, "%d %d %d %d %d %d %d %d %d %d %d %lu", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7], &v[8], &v[9], &v[10], &bseed); + int k = sscanf(opts, "%d %d %d %d %d %d %d %d %d %d %d %lu %d", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7], &v[8], &v[9], &v[10], &bseed, &hard); if (k < 7) { snprintf(out, (size_t)n, "%s", opts); return; } - snprintf(out, (size_t)n, "%d %d %d %d %d %d %d %d %d %d %d 0", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10]); + snprintf(out, (size_t)n, "%d %d %d %d %d %d %d %d %d %d %d 0 %d", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10], hard); } static int cg_create(const char *args, char *opts, int optsn, char *seats) { - int np, v2p, vp, beg, away = 0, setup = 0, scen = 0, map = 0; + int np, v2p, vp, beg, away = 0, setup = 0, scen = 0, map = 0, hard = 1; /* hard: bots dificiles (busqueda) o faciles */ unsigned long bseed = 0; char st[8]; - if (sscanf(args, "%d %d %d %d %7s %d %d %d %d %lu", &np, &v2p, &vp, &beg, st, &away, &setup, &scen, &map, &bseed) < 5 || + if (sscanf(args, "%d %d %d %d %7s %d %d %d %d %lu %d", &np, &v2p, &vp, &beg, st, &away, &setup, &scen, &map, &bseed, &hard) < 5 || np < 2 || np > MAXP || map < 0 || map >= NMAPS || bseed > 0xFFFFFFFFul || scen < 0 || scen >= SCEN__COUNT || (scen && (np > 4 || v2p)) || vp < 3 || vp > 20 || (int)strlen(st) != np || away < 0 || away > 30 * 24 * 60 || setup < -1 || setup > 24 * 60) @@ -91,7 +91,7 @@ static int cg_create(const char *args, char *opts, int optsn, char *seats) o.map = (uint8_t)map; /* el mapa y el tablero que eligio quien crea */ o.bseed = (uint32_t)bseed; if (o.map || o.bseed) o.beginner = 0; - fmt_opts(&o, away, setup, opts, optsn); + fmt_opts(&o, away, setup, hard != 0, opts, optsn); memcpy(seats, st, (size_t)np + 1); return np; } @@ -99,10 +99,10 @@ static int cg_create(const char *args, char *opts, int optsn, char *seats) static bool cg_start(void *vst, const char *opts, uint64_t seed, char *kinds, char (*names)[20], SrvEmit emit, void *ctx) { CatanSt *c = vst; - int v[11] = { 0 }; /* ausencia, colocacion, escenario y mapa no estan en partidas viejas */ + int v[11] = { 0 }, hard = 1; /* ausencia, colocacion, escenario, mapa y nivel no estan en partidas viejas */ unsigned long bseed = 0; - if (sscanf(opts, "%d %d %d %d %d %d %d %d %d %d %d %lu", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7], &v[8], &v[9], - &v[10], &bseed) < 7) + if (sscanf(opts, "%d %d %d %d %d %d %d %d %d %d %d %lu %d", &v[0], &v[1], &v[2], &v[3], &v[4], &v[5], &v[6], &v[7], &v[8], &v[9], + &v[10], &bseed, &hard) < 7) return false; Opts o; memset(&o, 0, sizeof o); @@ -120,7 +120,7 @@ static bool cg_start(void *vst, const char *opts, uint64_t seed, char *kinds, ch for (int i = 0; i < MAXP; i++) k[i] = kinds[i] == 'B' ? SEAT_BOT : i < o.np ? SEAT_REMOTE : SEAT_OPEN; host_init(&c->host, &o, seed, k, on_move, c); /* bots con busqueda, pero cortos: el servidor atiende todo en un solo hilo */ - SearchCfg b = { .iters = 20000, .horizon = 8, .c_ucb = 0.5f, .now_ms = clock32, .max_ms = 150 }; + SearchCfg b = { .iters = hard ? 20000 : 0, .horizon = 8, .c_ucb = 0.5f, .now_ms = clock32, .max_ms = 150 }; /* iters 0: heuristico */ c->host.bot = b; for (int i = 0; i < o.np; i++) if (c->host.a.g.p[i].neutral) { diff --git a/catan/server/server.h b/catan/server/server.h @@ -1,7 +1,7 @@ /* server.h - Catan en el servidor de partidas de comun/ (ver comun/server/srv.h * para el protocolo general). Lo propio de Catan: * - * CREATE <np> <v2p> <vp> <principiante> <asientos> [ausencia] [colocacion] [escenario] [mapa] [semilla del tablero] + * CREATE <np> <v2p> <vp> <principiante> <asientos> [ausencia] [colocacion] [escenario] [mapa] [semilla del tablero] [bots: 1 dificiles, 0 faciles] * asientos: una letra por asiento, H humano / B bot * ausencia: minutos; si a un humano le toca y no juega en ese * tiempo, el bot juega por el hasta pasar el turno (0 = nunca)