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 b3fe0cc2d8a0082da393cc0f272487f54bf5c384
parent 8920f9890b79b7e70180d6827caf4c4f0bce9e78
Author: Francisco Armando Rosmino <franciscorosmino@rosmino.com.ar>
Date:   Mon,  5 Oct 2026 12:29:44 -0300

Quien empieza se sortea; tres niveles de bots con la misma busqueda

- Opts.first: el asiento que empieza. OPTS_FIRST_RANDOM lo resuelve host_init
  con la semilla de la partida (igual en todos los nodos); viaja en la cabecera
  como prefijo 'F <asiento>', en el guardado local (13o campo) y en el servidor
  (14o). Las partidas y cabeceras viejas sin el campo arrancan por el 0 como
  siempre. game_init da la vuelta a la mesa desde ese asiento. Test t_first.
- Bots: facil / medio / dificil = ISMCTS con 25 / 300 / 30000 iteraciones (el
  bot de reglas deja de ser un nivel; queda para playouts y fallback). Config
  bot_level (bot_hard viejo se convierte). Online: CREATE 11o = 0 facil,
  1 dificil, 2 medio (compatible con lo creado hasta ahora).
- Makefile: search_old.c (de catan-duel) fuera del build normal.

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

Diffstat:
MCLAUDE.md | 4++--
Mcatan/Makefile | 2+-
Mcatan/README.md | 5+++--
Mcatan/core/catan.h | 4++++
Mcatan/core/game.c | 6++++--
Mcatan/core/host.c | 6++++++
Mcatan/core/move.c | 16++++++++++++----
Mcatan/linux/app.c | 23+++++++++++++----------
Mcatan/linux/app.h | 2+-
Mcatan/linux/main.c | 5+++--
Mcatan/linux/online.c | 7++++---
Mcatan/linux/selftest.c | 31+++++++++++++++++++++++++++++++
Mcatan/linux/session.c | 23++++++++++++++---------
Mcatan/linux/session.h | 3++-
Mcatan/server/catan_game.c | 18+++++++++++-------
Mcatan/server/server.h | 2+-
16 files changed, 112 insertions(+), 45 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md @@ -66,7 +66,7 @@ Al correr pruebas o el binario, aislar `XDG_CONFIG_HOME`/`XDG_DATA_HOME` en un d - `ctrl.c`: máquina de estados de interacción **compartida** por TUI, GUI y PicoCalc (teclas abstractas / clicks resueltos → `Move`). - i18n: `strings.h` es una X-macro `X(id, es, en)`; todo texto visible va ahí y se lee con `tr()`. - Reglas configurables en `Opts` (variante 2p, ladrón amable, fichas, etc.). - - Bots: `bot.c` heurístico ("fáciles"); `search.c` ISMCTS ("difíciles", `search_decide` + `SearchCfg` en `Host.bot`; iters 0 = heurístico). Determiniza solo lo oculto para ese asiento, candidatas podadas con `bot_int.h`, playouts con `bot_decide`, horizonte 8 + evaluación. Árbol estático (`SEARCH_NODES`, 1024 en la Pico): una búsqueda a la vez. Corta por tiempo: 300 ms Linux (`sess_set_bots`), 150 ms servidor, 1200 ms Pico. **Todo cambio de bots se mide nueva contra vieja**: `make catan-duel` (lleva además la `search.c` de `DUEL_BASE`, por defecto el último tag, con el símbolo renombrado a `search_old_decide`) y `./catan-duel --duel 80 4 100 8 - <semilla>` enfrenta la búsqueda nueva contra la vieja en la misma partida rotando asientos (base 25 % con 4 jugadores; correr dos semillas, el ruido con 80 partidas es ±8 puntos). `--duel` en `./catan` sigue siendo contra los heurísticos y solo sirve de sanidad. + - Bots: tres niveles, todos ISMCTS (`search.c`, `search_decide` + `SearchCfg` en `Host.bot`) con distinta cantidad de iteraciones: fácil 25 (empata con el bot de reglas), medio 300 (le gana ~57 %), difícil 30000 en la PC / 20000 en el servidor (`bot_iters` en `linux/session.c`; config `bot_level`). `bot.c` es el bot de reglas: ya no es un nivel, queda para los playouts, las ausencias y el fallback (iters 0). Determiniza solo lo oculto para ese asiento, candidatas podadas con `bot_int.h`, playouts con `bot_decide`, horizonte 8 + evaluación. Árbol estático (`SEARCH_NODES`, 1024 en la Pico): una búsqueda a la vez. Corta por tiempo: 300 ms Linux (`sess_set_bots`), 150 ms servidor, 1200 ms Pico. **Todo cambio de bots se mide nueva contra vieja**: `make catan-duel` (lleva además la `search.c` de `DUEL_BASE`, por defecto el último tag, con el símbolo renombrado a `search_old_decide`) y `./catan-duel --duel 80 4 100 8 - <semilla>` enfrenta la búsqueda nueva contra la vieja en la misma partida rotando asientos (base 25 % con 4 jugadores; correr dos semillas, el ruido con 80 partidas es ±8 puntos). `--duel` en `./catan` sigue siendo contra los heurísticos y solo sirve de sanidad. - **`gfx/`** (`catan_gfx.h`): tablero y pantallas de Catan en píxeles (layouts compact 320x320 y wide) sobre `comun/gfx`. `geom_fit(geo, game, ...)` encuadra cualquier tablero por los límites de sus vértices. Panel ancho: con más de 4 jugadores las filas son más bajas; acciones y costos van en dos columnas. - **GUI ancha**: lienzo lógico de 640x360 (16:9) con escala **entera** (`gk_open_fluid` en `comun/linux/guikit.c`: 2x en 720p, 3x en 1080p; un factor no entero deja la letra despareja). Pensada para 720p/1080p (los amigos de Pancho juegan en Windows con `catan.exe`). Fuentes en `comun/gfx/font.c`, generadas por `comun/tools/mkfont.py` desde `comun/tools/fonts/`: Terminus 6x12 (normal, y engrosada a 7x12 como negrita) y Spleen 5x8 (chica, con los acentos del castellano agregados). - **`linux/`**: cliente. `frontend.h` define la interfaz TUI (`tui.c`, sobre `comun/linux/tuikit`; colores opacos propios — el usuario no quiere depender de la transparencia de su terminal) / GUI (`gui_sdl.c`, sobre `guikit`). `app.c` menús y bucle, `session.c` guardado, `net.c`/`online.c` red (sobre `netline`). @@ -77,7 +77,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) 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). +- **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, 0 fáciles, 2 medios; falta = difíciles). **Quién empieza se sortea**: `Opts.first` (`OPTS_FIRST_RANDOM` = lo resuelve `host_init` con la semilla, igual en todos los nodos; 14º campo de OPTS del servidor, 13º del guardado local), la cabecera lleva el prefijo `F <asiento>` (como `M`) si no es el 0, y `game_init` da la vuelta a la mesa desde ahí; cabeceras y partidas viejas sin el campo arrancan por el 0. - **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/Makefile b/catan/Makefile @@ -10,7 +10,7 @@ CPPFLAGS += -I. $(COMUN_INC) -DFORM_MAX_FIELDS=32 VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) $(shell printf '#define APP_VERSION "%s"\n' '$(VERSION)' > linux/version.h.tmp; \ cmp -s linux/version.h.tmp linux/version.h || mv linux/version.h.tmp linux/version.h; rm -f linux/version.h.tmp) -CORE := $(wildcard core/*.c) +CORE := $(filter-out core/search_old.c,$(wildcard core/*.c)) # search_old.c es solo de catan-duel GFX := $(wildcard gfx/*.c) LINUX := $(wildcard linux/*.c) SERVER := server/catan_game.c server/export.c diff --git a/catan/README.md b/catan/README.md @@ -99,8 +99,9 @@ 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, 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. +2. *Crear partida*: elegís jugadores, qué asientos son bots, el nivel de los bots (fáciles, + medios o difíciles: la misma búsqueda con más o menos iteraciones) y **"Bot juega si no + hay"** (nunca, 1, 4, 12, 24 o 48 horas). Quién empieza se sortea. 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/catan.h b/catan/core/catan.h @@ -83,6 +83,8 @@ const Topo *topo_of(int board); /* ------------------------------------------------------------------ opciones */ +#define OPTS_FIRST_RANDOM 255 + typedef struct { uint8_t np; /* asientos totales (incluye neutrales), 2..MAXP */ uint8_t variant2p; /* variante oficial de 2 (asientos 2 y 3 neutrales) */ @@ -97,6 +99,8 @@ typedef struct { uint8_t scenario; /* SCEN_*: Navegantes usa su mapa, barcos, pirata y oro */ uint8_t map; /* mapa de la familia (0..NMAPS-1; 0 = el de siempre) */ uint32_t bseed; /* semilla del tablero (0: sale de la de la partida); la elige quien crea */ + uint8_t first; /* asiento que empieza (setup y primer turno); OPTS_FIRST_RANDOM: lo sortea host_init + con la semilla de la partida. Las partidas viejas, sin el campo, arrancan por el 0. */ } Opts; void opts_default(Opts *o, int humans, bool variant2p); diff --git a/catan/core/game.c b/catan/core/game.c @@ -121,11 +121,13 @@ void game_init(Game *g, const Opts *o) g->phase = PH_SETUP; /* serpiente (1 2 3 4 4 3 2 1); con setup_first, primero la serpiente de esos * asientos y despues la del resto (ej. humano contra bots: 1 1 2 3 4 4 3 2) */ - int n = 0; + int n = 0, first = o->first < o->np ? o->first : 0; /* desde que asiento se da la vuelta a la mesa */ for (int grp = 0; grp < 2; grp++) { int real[MAXP], nr = 0; - for (int i = 0; i < o->np; i++) + for (int k = 0; k < o->np; k++) { + int i = (first + k) % o->np; if (!g->p[i].neutral && ((o->setup_first >> i & 1) == (grp == 0))) real[nr++] = i; + } for (int i = 0; i < nr; i++) { g->setup_order[n + i] = (uint8_t)real[i]; g->setup_order[n + 2 * nr - 1 - i] = (uint8_t)real[i]; diff --git a/catan/core/host.c b/catan/core/host.c @@ -17,6 +17,12 @@ void host_init(Host *h, const Opts *o, uint64_t seed, const uint8_t kinds[MAXP], memset(h, 0, sizeof *h); h->on_move = on_move; h->ctx = ctx; + Opts oo = *o; + if (oo.first == OPTS_FIRST_RANDOM) { /* quien empieza se sortea con la semilla: igual en todos los nodos */ + int nreal = oo.variant2p ? 2 : oo.np; + oo.first = (uint8_t)(seed % (uint64_t)nreal); + } + o = &oo; for (int s = 0; s < MAXP; s++) h->kind[s] = kinds[s]; for (int s = 0; s < MAXP; s++) { char def[2] = { (char)('1' + s), 0 }; diff --git a/catan/core/move.c b/catan/core/move.c @@ -127,6 +127,9 @@ int hdr_fmt(const Game *g, char *buf, int n) * con un mapa que no es el de siempre, antes "M <mapa> " */ int pos = 0; if (o->map) { pos = str_put(buf, n, 0, "M"); pos = put_sp_int(buf, n, pos, o->map); pos = str_put(buf, n, pos, " "); } + if (o->first && o->first < o->np) { /* quien empieza, si no es el asiento 0: "F <asiento> " */ + pos = str_put(buf, n, pos, "F"); pos = put_sp_int(buf, n, pos, o->first); pos = str_put(buf, n, pos, " "); + } pos = str_put(buf, n, pos, o->scenario ? "S" : "G"); if (o->scenario) pos = put_sp_int(buf, n, pos, o->scenario); long ov[7] = { o->np, o->variant2p, o->vp_target, o->friendly_robber, o->tokens_start, o->neutral_setup, o->beginner }; @@ -143,11 +146,12 @@ int hdr_fmt(const Game *g, char *buf, int n) bool hdr_parse(const char *s, Game *g) { - long v, ov[7], scen = 0, map = 0; + long v, ov[7], scen = 0, map = 0, first = 0; static const long HI[7] = { MAXP, 1, 30, 1, 20, 4, 1 }; while (*s == ' ') s++; - if (*s == 'M') { - if (!(s = scan_small(s + 1, 0, NMAPS - 1, &map))) return false; + while (*s == 'M' || *s == 'F') { /* prefijos opcionales: mapa y quien empieza */ + if (*s == 'M') { if (!(s = scan_small(s + 1, 0, NMAPS - 1, &map))) return false; } + else if (!(s = scan_small(s + 1, 0, MAXP - 1, &first))) return false; while (*s == ' ') s++; } char tag = *s++; @@ -156,9 +160,13 @@ bool hdr_parse(const char *s, Game *g) for (int i = 0; i < 7; i++) if (!(s = scan_small(s, 0, HI[i], &ov[i]))) return false; if (ov[0] < 2) return false; - Opts o = { (uint8_t)ov[0], (uint8_t)ov[1], (uint8_t)ov[2], (uint8_t)ov[3], (uint8_t)ov[4], (uint8_t)ov[5], (uint8_t)ov[6], 0, 0, 0, 0, 0, 0 }; + Opts o; + memset(&o, 0, sizeof o); /* tambien el relleno: Game se compara byte a byte en los tests */ + o.np = (uint8_t)ov[0]; o.variant2p = (uint8_t)ov[1]; o.vp_target = (uint8_t)ov[2]; o.friendly_robber = (uint8_t)ov[3]; + o.tokens_start = (uint8_t)ov[4]; o.neutral_setup = (uint8_t)ov[5]; o.beginner = (uint8_t)ov[6]; o.scenario = (uint8_t)scen; o.map = (uint8_t)map; + o.first = (uint8_t)first; const Topo *tp = topo_of(opts_board(&o)); /* el tablero sale de la cantidad de jugadores */ memset(g, 0, sizeof *g); for (int h = 0; h < tp->nhex; h++) { if (!(s = scan_small(s, 0, T__COUNT - 1, &v))) return false; g->terrain[h] = (uint8_t)v; } diff --git a/catan/linux/app.c b/catan/linux/app.c @@ -21,7 +21,7 @@ void settings_load(Settings *st) memset(st, 0, sizeof *st); st->handoff = 1; st->bot_delay = 450; - st->bot_hard = 1; + st->bot_level = 2; st->layout = LAYOUT_WIDE; st->scale = 2; #ifdef __EMSCRIPTEN__ @@ -43,7 +43,8 @@ void settings_load(Settings *st) if (!strcmp(k, "lang")) st->lang = !strcmp(v, "en"); else if (!strcmp(k, "handoff")) st->handoff = atoi(v); else if (!strcmp(k, "bot_delay")) st->bot_delay = atoi(v); - else if (!strcmp(k, "bot_hard")) st->bot_hard = atoi(v); + else if (!strcmp(k, "bot_level")) st->bot_level = atoi(v); + else if (!strcmp(k, "bot_hard")) st->bot_level = atoi(v) ? 2 : 0; /* config vieja */ else if (!strcmp(k, "layout")) st->layout = !strcmp(v, "compact") ? LAYOUT_COMPACT : LAYOUT_WIDE; else if (!strcmp(k, "scale")) st->scale = atoi(v); else if (!strcmp(k, "name")) snprintf(st->name, sizeof st->name, "%s", v); @@ -89,8 +90,8 @@ void settings_save(const Settings *st) { FILE *f = fopen(config_path(), "w"); if (!f) return; - fprintf(f, "lang=%s\nhandoff=%d\nbot_delay=%d\nbot_hard=%d\nlayout=%s\nscale=%d\nname=%s\nserver=%s\n", - st->lang ? "en" : "es", st->handoff, st->bot_delay, st->bot_hard, st->layout == LAYOUT_COMPACT ? "compact" : "wide", + fprintf(f, "lang=%s\nhandoff=%d\nbot_delay=%d\nbot_level=%d\nlayout=%s\nscale=%d\nname=%s\nserver=%s\n", + st->lang ? "en" : "es", st->handoff, st->bot_delay, st->bot_level, st->layout == LAYOUT_COMPACT ? "compact" : "wide", st->scale, st->name, st->server); for (int i = 0; i < MAXP; i++) if (st->color[i][0]) fprintf(f, "color%d=%s\n", i + 1, st->color[i]); fclose(f); @@ -281,10 +282,11 @@ void app_play_again(Session *s, Settings *st) Rng r; rng_seed(&r, fresh_seed(), 0x3A9); if (!o.beginner) o.bseed = 1 + rng_below(&r, 0x7FFFFFFE); /* otro reparto del mismo mapa */ + o.first = OPTS_FIRST_RANDOM; /* y otro que empieza */ sess_close(s); sess_new_local(s, &o, fresh_seed(), kinds, names); s->handoff = st->handoff; - sess_set_bots(s, st->bot_hard); + sess_set_bots(s, st->bot_level); } int app_play(Frontend *fe, Session *s, Settings *st) @@ -483,7 +485,7 @@ enum { B_BACK = 0, B_START = 1, F_PLAYERS = 10, F_VARIANT, F_BOARD, F_HANDOFF, F static const char *CH_NUM[] = { "2", "3", "4", "5", "6" }; /* 5-6: tablero grande, en pareja */ static const char *CH_YESNO_ES[] = { "No", "Sí" }, *CH_YESNO_EN[] = { "No", "Yes" }; static const char *CH_KIND_ES[] = { "Humano", "Bot" }, *CH_KIND_EN[] = { "Human", "Bot" }; -static const char *CH_LEVEL_ES[] = { "Fáciles", "Difíciles" }, *CH_LEVEL_EN[] = { "Easy", "Hard" }; +static const char *CH_LEVEL_ES[] = { "Fáciles", "Medios", "Difíciles" }, *CH_LEVEL_EN[] = { "Easy", "Medium", "Hard" }; static const char *CH_BOARD_ES[] = { "Aleatorio", "Principiante" }, *CH_BOARD_EN[] = { "Random", "Beginner" }; static const char *CH_FREE_ES[] = { "A 10 puntos", "Libre (seguir al ganar)" }, *CH_FREE_EN[] = { "First to 10", "Free play (keep going)" }; static const char *CH_FIRST_ES[] = { "Por turnos", "Humanos primero" }, *CH_FIRST_EN[] = { "Snake order", "Humans first" }; @@ -533,7 +535,7 @@ static bool new_game(Frontend *fe, Settings *st, Session *s) else snprintf(nm, sizeof nm, "Bot %d", i + 1); form_text(&f, F_NAME + i, en ? " name" : " nombre", nm, 16); } - form_choice(&f, F_LEVEL, "Bots", en ? CH_LEVEL_EN : CH_LEVEL_ES, 2, st->bot_hard ? 1 : 0); + form_choice(&f, F_LEVEL, "Bots", en ? CH_LEVEL_EN : CH_LEVEL_ES, 3, st->bot_level < 0 ? 0 : st->bot_level > 2 ? 2 : st->bot_level); form_choice(&f, F_SCEN, en ? "Scenario" : "Escenario", en ? SCEN_CH_EN : SCEN_CH_ES, SCEN__COUNT, 0); form_choice(&f, F_BOARD, en ? "Board" : "Tablero", en ? CH_BOARD_EN : CH_BOARD_ES, 2, 0); form_choice(&f, F_MAP, en ? "Map" : "Mapa", map_choices(BF_CLASSIC), NMAPS + 1, 1); @@ -575,11 +577,12 @@ static bool new_game(Frontend *fe, Settings *st, Session *s) if (kinds[i] == SEAT_LOCAL) o.setup_first |= (uint8_t)(1 << i); if (kinds[0] == SEAT_LOCAL && names[0][0]) { snprintf(st->name, sizeof st->name, "%s", names[0]); } st->handoff = form_get(&f, F_HANDOFF)->value; - st->bot_hard = form_get(&f, F_LEVEL)->value; + st->bot_level = form_get(&f, F_LEVEL)->value; settings_save(st); + o.first = OPTS_FIRST_RANDOM; /* quien empieza se sortea */ sess_new_local(s, &o, fresh_seed(), kinds, names); s->handoff = st->handoff; - sess_set_bots(s, st->bot_hard); + sess_set_bots(s, st->bot_level); return true; } } @@ -756,7 +759,7 @@ void app_menu(Frontend *fe, Settings *st) char err[128] = ""; if (have_save && sess_resume(&s, last, err, sizeof err)) { s.handoff = st->handoff; - sess_set_bots(&s, st->bot_hard); + sess_set_bots(&s, st->bot_level); while (app_play(fe, &s, st) == 1) app_play_again(&s, st); sess_close(&s); } diff --git a/catan/linux/app.h b/catan/linux/app.h @@ -10,7 +10,7 @@ typedef struct { int lang; int handoff; int bot_delay; - int bot_hard; /* bots con busqueda (ISMCTS) */ + int bot_level; /* 0 facil, 1 medio, 2 dificil (ISMCTS con 25 / 300 / 30000 iteraciones) */ int layout; int scale; char name[20]; diff --git a/catan/linux/main.c b/catan/linux/main.c @@ -114,8 +114,9 @@ int main(int argc, char **argv) if (bot) snprintf(names[i], sizeof names[i], "Bot %d", i + 1); else if (i == 0) snprintf(names[i], sizeof names[i], "%s", st.name); } + o.first = OPTS_FIRST_RANDOM; sess_new_local(&s, &o, fresh_seed(), kinds, names); - sess_set_bots(&s, st.bot_hard); + sess_set_bots(&s, st.bot_level); s.handoff = st.handoff; while (app_play(fe, &s, &st) == 1) app_play_again(&s, &st); sess_close(&s); @@ -124,7 +125,7 @@ int main(int argc, char **argv) if (nargs) snprintf(path, sizeof path, "%s", args[0]); else if (!latest_save(path, sizeof path)) { fe->close(fe); fprintf(stderr, "no hay partidas guardadas\n"); return 1; } if (!sess_resume(&s, path, err, sizeof err)) { fe->close(fe); fprintf(stderr, "%s: %s\n", path, err); return 1; } - sess_set_bots(&s, st.bot_hard); + sess_set_bots(&s, st.bot_level); s.handoff = st.handoff; while (app_play(fe, &s, &st) == 1) app_play_again(&s, &st); sess_close(&s); diff --git a/catan/linux/online.c b/catan/linux/online.c @@ -118,7 +118,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_LEVEL, C_SEAT = 20 }; -static const char *LV_ES[] = { "Fáciles", "Difíciles" }, *LV_EN[] = { "Easy", "Hard" }; +static const char *LV_ES[] = { "Fáciles", "Medios", "Difíciles" }, *LV_EN[] = { "Easy", "Medium", "Hard" }; +static const int LV_WIRE[3] = { 0, 2, 1 }; /* CREATE/OPTS: 0 facil, 1 dificil (como antes), 2 medio */ static const char *CH_NUM[] = { "2", "3", "4", "5", "6" }; static const char *YN_ES[] = { "No", "Sí" }, *YN_EN[] = { "No", "Yes" }; @@ -166,7 +167,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_LEVEL, en ? "Bots" : "Bots", en ? LV_EN : LV_ES, 3, 2); 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."); @@ -199,7 +200,7 @@ static bool create_form(Frontend *fe, int layout, char *cmd, int n) } 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, form_get(&f, C_LEVEL)->value); + beg ? 0ul : (unsigned long)o.bseed, LV_WIRE[form_get(&f, C_LEVEL)->value]); return true; } } diff --git a/catan/linux/selftest.c b/catan/linux/selftest.c @@ -1,4 +1,5 @@ /* selftest.c - pruebas del nucleo (--selftest), simulacion bot contra bot (--sim) y duelo busqueda contra heuristica (--duel). */ +#include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <time.h> @@ -1387,6 +1388,35 @@ static void t_ctrl_gold(void) CHECK(ctrl_key(&c, K_OK, &m) && m.type == M_GOLD && m.p == 0); } +/* Quien empieza se sortea con la semilla (igual en todos los nodos), viaja en la cabecera + * como "F <asiento>" y las cabeceras viejas sin F arrancan por el asiento 0. */ +static void t_first(void) +{ + Opts o; + opts_default(&o, 4, false); + o.beginner = 1; + o.first = OPTS_FIRST_RANDOM; + uint8_t kinds[MAXP] = { SEAT_BOT, SEAT_BOT, SEAT_BOT, SEAT_BOT }; + static Host h; + int seen = 0; + for (uint64_t seed = 1; seed <= 8; seed++) { + host_init(&h, &o, seed, kinds, NULL, NULL); + Game *g = &h.a.g; + int f = (int)(seed % 4); + CHECK(g->o.first == f && g->cur == f && g->setup_order[0] == f && g->setup_order[1] == (f + 1) % 4); + CHECK(g->setup_order[7] == f); /* la serpiente vuelve al que empezo */ + seen |= 1 << f; + char hdr[512]; + Game g2; + hdr_fmt(g, hdr, sizeof hdr); + CHECK((f == 0) != (strncmp(hdr, "F ", 2) == 0)); /* el prefijo solo si no es el 0 */ + CHECK(hdr_parse(hdr, &g2) && g2.o.first == f && g2.cur == f && !memcmp(&g2, g, sizeof g2)); + } + CHECK(seen == 15); + Game g3; + CHECK(hdr_parse("G 4 0 10 0 2 2 1 " "4 1 0 3 1 2 4 3 1 0 2 4 2 3 0 4 3 2 1 " "5 2 6 3 8 10 9 12 11 4 8 10 9 4 5 6 3 11 0 " "4 5 5 5 5 5 1 2 3", &g3) && g3.cur == 0); +} + int app_selftest(void) { printf("selftest del nucleo\n"); @@ -1414,6 +1444,7 @@ int app_selftest(void) t_ctrl_setup(); t_ctrl_robber(); t_ctrl_gold(); + t_first(); t_serialize(); t_replay(); t_freeplay(); diff --git a/catan/linux/session.c b/catan/linux/session.c @@ -160,9 +160,9 @@ static void write_header(Session *s, uint64_t seed) { if (!s->save) return; const Opts *o = &s->host.a.g.o; - fprintf(s->save, "CATAN-SAVE 1\nSEED %llu\nOPTS %d %d %d %d %d %d %d %d %d %d %d %lu\n", (unsigned long long)seed, + fprintf(s->save, "CATAN-SAVE 1\nSEED %llu\nOPTS %d %d %d %d %d %d %d %d %d %d %d %lu %d\n", (unsigned long long)seed, o->np, o->variant2p, o->vp_target, o->friendly_robber, o->tokens_start, o->neutral_setup, o->beginner, - o->setup_first, o->freeplay, o->scenario, o->map, (unsigned long)o->bseed); + o->setup_first, o->freeplay, o->scenario, o->map, (unsigned long)o->bseed, o->first); for (int i = 0; i < o->np; i++) { int k = s->host.kind[i] == SEAT_REMOTE ? SEAT_LOCAL : s->host.kind[i]; fprintf(s->save, "SEAT %d %s %s\n", i, KIND_NAME[k], s->names[i]); @@ -217,10 +217,10 @@ bool sess_resume(Session *s, const char *path, char *err, int errlen) if (!strncmp(line, "CATAN-SAVE", 10)) hdr = true; else if (!strncmp(line, "SEED ", 5)) seed = strtoull(line + 5, 0, 10); else if (!strncmp(line, "OPTS ", 5)) { - int v[11] = { 0 }; /* setup_first, freeplay, escenario y mapa pueden faltar en partidas viejas */ + int v[11] = { 0 }, first = 0; /* setup_first, freeplay, escenario, mapa y quien empieza pueden faltar en partidas viejas */ unsigned long bseed = 0; - if (sscanf(line + 5, "%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(line + 5, "%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, &first) >= 7) { o.np = (uint8_t)v[0]; o.variant2p = (uint8_t)v[1]; o.vp_target = (uint8_t)v[2]; o.friendly_robber = (uint8_t)v[3]; o.tokens_start = (uint8_t)v[4]; o.neutral_setup = (uint8_t)v[5]; o.beginner = (uint8_t)v[6]; @@ -229,6 +229,7 @@ bool sess_resume(Session *s, const char *path, char *err, int errlen) o.scenario = (uint8_t)(v[9] > 0 && v[9] < SCEN__COUNT ? v[9] : 0); o.map = (uint8_t)(v[10] > 0 && v[10] < NMAPS ? v[10] : 0); o.bseed = (uint32_t)bseed; + o.first = (uint8_t)(first == OPTS_FIRST_RANDOM || (first >= 0 && first < MAXP) ? first : 0); opts = o.np >= 2 && o.np <= MAXP; } } else if (!strncmp(line, "SEAT ", 5)) { @@ -295,8 +296,8 @@ bool sess_go_free(Session *s, char *err, int errlen) f = fopen(path, "w"); if (!f) { free(buf); snprintf(err, (size_t)errlen, "%s", strerror(errno)); return false; } fwrite(buf, 1, (size_t)(op - buf), f); - fprintf(f, "\nOPTS %d %d %d %d %d %d %d %d 1 %d %d %lu", o->np, o->variant2p, o->vp_target, o->friendly_robber, - o->tokens_start, o->neutral_setup, o->beginner, o->setup_first, o->scenario, o->map, (unsigned long)o->bseed); + fprintf(f, "\nOPTS %d %d %d %d %d %d %d %d 1 %d %d %lu %d", o->np, o->variant2p, o->vp_target, o->friendly_robber, + o->tokens_start, o->neutral_setup, o->beginner, o->setup_first, o->scenario, o->map, (unsigned long)o->bseed, o->first); fputs(eol, f); fclose(f); paths_flush(); @@ -314,9 +315,13 @@ bool sess_go_free(Session *s, char *err, int errlen) static uint32_t clock32(void) { return (uint32_t)now_ms(); } /* Dificiles: ISMCTS con hasta 300 ms por decision (la ventana espera). */ -void sess_set_bots(Session *s, bool hard) +/* Los tres niveles son la misma busqueda con mas o menos iteraciones (calibrado contra el + * bot de reglas con --duel: 25 iter ~ parejo, 300 ~ 45 %, el dificil muy por encima). */ +int bot_iters(int level) { return level <= 0 ? 25 : level == 1 ? 300 : 30000; } + +void sess_set_bots(Session *s, int level) { - SearchCfg c = { .iters = hard ? 30000 : 0, .horizon = 8, .c_ucb = 0.5f, .now_ms = clock32, .max_ms = 300 }; + SearchCfg c = { .iters = bot_iters(level), .horizon = 8, .c_ucb = 0.5f, .now_ms = clock32, .max_ms = 300 }; s->host.bot = c; } diff --git a/catan/linux/session.h b/catan/linux/session.h @@ -41,7 +41,8 @@ typedef struct Session { /* Partida local nueva (hotseat, bots, o host LAN si despues se abre la red). */ void sess_new_local(Session *s, const Opts *o, uint64_t seed, const uint8_t kinds[MAXP], char names[MAXP][20]); bool sess_resume(Session *s, const char *path, char *err, int errlen); -void sess_set_bots(Session *s, bool hard); /* bots heuristicos o con busqueda */ +void sess_set_bots(Session *s, int level); /* 0 facil, 1 medio, 2 dificil: ISMCTS con mas iteraciones */ +int bot_iters(int level); /* bots heuristicos o con busqueda */ /* partida local terminada: la pasa a juego libre (reescribe OPTS y la recarga) */ bool sess_go_free(Session *s, char *err, int errlen); int sess_submit(Session *s, Move *m); diff --git a/catan/server/catan_game.c b/catan/server/catan_game.c @@ -56,8 +56,9 @@ static void on_move(void *ctx, const Move *m) 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 %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); + snprintf(out, (size_t)n, "%d %d %d %d %d %d %d %d %d %d %d %lu %d %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, + OPTS_FIRST_RANDOM); /* quien empieza: se sortea con la semilla de la partida */ } static uint32_t clock32(void) { return (uint32_t)srv_now_ms(); } @@ -70,7 +71,8 @@ static void cg_again(const char *opts, char *out, int n) unsigned long bseed = 0; 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 %d", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10], hard); + snprintf(out, (size_t)n, "%d %d %d %d %d %d %d %d %d %d %d 0 %d %d", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10], hard, + OPTS_FIRST_RANDOM); } static int cg_create(const char *args, char *opts, int optsn, char *seats) @@ -99,10 +101,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 }, hard = 1; /* ausencia, colocacion, escenario, mapa y nivel no estan en partidas viejas */ + int v[11] = { 0 }, hard = 1, first = 0; /* ausencia, colocacion, escenario, mapa, nivel y quien empieza no estan en partidas viejas */ unsigned long bseed = 0; - 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) + if (sscanf(opts, "%d %d %d %d %d %d %d %d %d %d %d %lu %d %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, &first) < 7) return false; Opts o; memset(&o, 0, sizeof o); @@ -111,6 +113,7 @@ static bool cg_start(void *vst, const char *opts, uint64_t seed, char *kinds, ch o.scenario = (uint8_t)(v[9] > 0 && v[9] < SCEN__COUNT ? v[9] : 0); o.map = (uint8_t)(v[10] > 0 && v[10] < NMAPS ? v[10] : 0); o.bseed = (uint32_t)bseed; + o.first = (uint8_t)(first == OPTS_FIRST_RANDOM || (first >= 0 && first < MAXP) ? first : 0); if (o.np < 2 || o.np > MAXP) return false; c->emit = emit; c->ctx = ctx; @@ -120,7 +123,8 @@ 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 = hard ? 20000 : 0, .horizon = 8, .c_ucb = 0.5f, .now_ms = clock32, .max_ms = 150 }; /* iters 0: heuristico */ + /* nivel: 0 facil, 1 dificil (el valor de siempre), 2 medio; misma busqueda con mas o menos iteraciones */ + SearchCfg b = { .iters = hard == 0 ? 25 : hard == 2 ? 300 : 20000, .horizon = 8, .c_ucb = 0.5f, .now_ms = clock32, .max_ms = 150 }; 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] [bots: 1 dificiles, 0 faciles] + * CREATE <np> <v2p> <vp> <principiante> <asientos> [ausencia] [colocacion] [escenario] [mapa] [semilla del tablero] [bots: 1 dificiles, 0 faciles, 2 medios] * 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)