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