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