Los rivales
En resumen: Un rival da un número a cada acción que puede pagar, toma el más alto, paga y puntúa de nuevo. El carácter del imperio son los pesos de las mismas fórmulas.
Problema
Un rival que no piensa
En Epic Cosmos juegas contra tres imperios. Mientras tú escribes órdenes, cada uno decide qué hacer en este turno: poner una mina, construir un laboratorio, colonizar un planeta nuevo, encargar naves, mandar una flota. ¿Cómo se hace un rival así?
El primer intento es un guion: «turno 1 mina, turno 2 mina, turno 3 colonia…». El jugador lo descubre en tres partidas y después lo vence siempre. El segundo intento es la cadena «si … si no»: si hay dinero para una colonia, coloniza; si no, si hay peligro, construye naves; si no… Cada acción nueva necesita una línea nueva y excepciones nuevas respecto a las demás, y el rival sigue siendo predecible y hace tonterías, porque nadie previó justo esta situación.
Hay una forma mejor. Da a cada acción posible un número: lo buena que es para este imperio ahora. Haz la acción con el número más alto. «¿Colonia o naves?» pasa a ser la comparación de dos números, no una regla para este par. Un tipo nuevo de acción es una línea nueva en la lista de puntuaciones, y no se toca nada más.
Y algo más que hace interesante el juego: el carácter. El señor de la guerra, el constructor y el erudito no son tres programas, sino el mismo código con distintos pesos en las fórmulas.
La idea
Un número para cada acción
Esta es la situación con la que trabaja toda la lección. Turno 5, un imperio con dos planetas (Kora I con un astillero y Kora II con una mina), dos planetas libres más en el mismo sistema (Vesta I y II), 100 de metal y un poder de flota de 10:
record Option(String kind, String target, int utility) {}
class Planet { final String name; final int rich; int mines; boolean lab; final boolean yard;
Planet(String name, int rich, int mines, boolean yard) { this.name = name; this.rich = rich; this.mines = mines; this.yard = yard; }}
class Empire { int metal = 100; int power = 10; int turn = 5; final List<Planet> owned = new ArrayList<>(); final List<Planet> free = new ArrayList<>();}static final int MAX_MINE = 3;static final int LAB_COST = 40;static final int SHIP_COST = 8;static final int SHIP_POWER = 5;
static Empire newEmpire() { Empire empire = new Empire(); empire.owned.add(new Planet("Kora I", 4, 0, true)); empire.owned.add(new Planet("Kora II", 3, 1, false)); empire.free.add(new Planet("Vesta I", 3, 0, false)); empire.free.add(new Planet("Vesta II", 1, 0, false)); return empire;}¿Qué puede hacer este imperio? Construir una mina en un planeta (cuesta 15 × (minas + 1)), construir un laboratorio (40), fundar una colonia en un planeta libre (cuesta más con cada planeta: ahora 52) o encargar naves. Cada una recibe una puntuación, y las puntuaciones son fórmulas. Primero los dos edificios:
- mina =
construcción × riqueza / (4 × (minas + 1)): un planeta rico vale más, y cada mina nueva en el mismo planeta vale menos (el divisor crece); - laboratorio =
ciencia × 6 / 5 − 40 × laboratorios: el segundo laboratorio vale 40 menos que el primero.
Luego las colonias y las naves:
- colonia =
expansión × (riqueza + 6) × 50 / (coste × 6): un planeta libre y rico tienta, y una colonia cara tienta menos; - naves =
60 + ataque / 2 + (poder deseado − poder) / 2(el último término es como máximo 100), pero solo mientras el poder esté por debajo del deseado. El poder deseado crece con el turno:(20 + 3 × turno) × (140 + ataque) / 240.
static List<Option> options(Empire empire, Personality p) { // Cada acción que el imperio puede pagar recibe una puntuación. List<Option> list = new ArrayList<>(); int labs = 0; for (Planet planet : empire.owned) { if (planet.lab) labs++; } for (Planet planet : empire.owned) { if (planet.mines < MAX_MINE && empire.metal >= mineCost(planet)) { list.add(new Option("mine", planet.name, p.build() * planet.rich / (4 * (planet.mines + 1)))); } if (!planet.lab && empire.metal >= LAB_COST) { list.add(new Option("lab", planet.name, p.research() * 6 / 5 - 40 * labs)); } } int cost = colonyCost(empire); if (empire.metal >= cost) { for (Planet planet : empire.free) { int value = planet.rich + 6; list.add(new Option("colony", planet.name, p.expand() * value * 50 / (cost * 6))); } } int want = wantedPower(empire, p); for (Planet planet : empire.owned) { if (planet.yard && empire.power < want && shipCount(empire) > 0) { int urge = Math.min(100, (want - empire.power) / 2); list.add(new Option("ships", planet.name, 60 + p.attack() / 2 + urge)); break; } } return list;}Esa es toda la función. No elige: solo reúne en una lista todo lo que el imperio puede pagar, con sus puntuaciones. La condición «puede pagar» va antes de la puntuación: una acción que no se puede pagar no entra en la lista.
Option es un record: tres campos y nada más, mientras que Planet y Empire son clases con campos mutables. La división entera corta el resto, justo lo que queremos aquí: todos los números son positivos.Más precisamente, para curiosos
Las fórmulas están ajustadas, no deducidas: los coeficientes 4, 50, 6 y 60 se eligen para que las puntuaciones de mina, colonia y naves sean comparables (centenas) y una no pese mil veces más que otra. Por eso la última lección lanza cientos de partidas de IA contra IA y mide si algún carácter está desequilibrado.
En el juego hay cuatro recursos (metal, gas, cristal, uranio) y la riqueza de un planeta son cuatro números. Aquí hay metal y una sola riqueza, para que se vea cada número.
La idea
Gana el número más alto: estrictamente
Cuando la lista está lista, la elección es un bucle sencillo. Una puntuación estrictamente mayor sustituye a la elegida. Así, cuando las puntuaciones más altas son iguales, gana la que se miró primero. El valor inicial es el umbral: una puntuación que no lo supera no vale la pena. Para la economía es 0 (una acción con puntuación 0 es «absurda»), y para la flota será 20 (es mejor quedarse).
static Option best(List<Option> list, int floor) { // Solo gana una puntuación estrictamente mayor, así que en un empate se queda la primera. Option pick = null; int bestU = floor; for (Option option : list) { if (option.utility() > bestU) { pick = option; bestU = option.utility(); } } return pick;}
static String label(Option option) { String kind = option.kind(); if (kind.equals("mine")) return "mina " + option.target(); if (kind.equals("lab")) return "laboratorio " + option.target(); if (kind.equals("colony")) return "colonia " + option.target(); return "naves " + option.target();}La situación de arriba, ejecutada para los tres caracteres (cada uno recibe un imperio nuevo con los mismos planetas y el mismo dinero). Para cada uno se listan las puntuaciones en el orden de la lista y al final la elección:
for (Personality p : PERSONALITIES) { List<Option> list = options(newEmpire(), p); System.out.println(p.name()); for (Option option : list) { System.out.println(" " + pad(label(option), 21) + padLeft(String.valueOf(option.utility()), 4)); } Option pick = best(list, 0); System.out.println(" → " + label(pick) + " (" + pick.utility() + ")");}Señor de la guerra mina Kora I 90 laboratorio Kora I 72 mina Kora II 33 laboratorio Kora II 72 colonia Vesta I 115 colonia Vesta II 89 naves Kora I 162 → naves Kora I (162)Constructor mina Kora I 140 laboratorio Kora I 96 mina Kora II 52 laboratorio Kora II 96 colonia Vesta I 230 colonia Vesta II 179 naves Kora I 93 → colonia Vesta I (230)Erudito mina Kora I 100 laboratorio Kora I 132 mina Kora II 37 laboratorio Kora II 132 colonia Vesta I 100 colonia Vesta II 78 naves Kora I 99 → laboratorio Kora I (132)Una situación, tres elecciones distintas. El señor de la guerra elige las naves (162): su poder deseado es 45 y la flota tiene solo 10. El constructor elige la colonia en Vesta I (230), porque su expansión pesa 160. El erudito elige el laboratorio de Kora I (132): el laboratorio de Kora II tiene la misma puntuación y queda segundo.
La idea
El carácter es una tabla de pesos
¿Qué distingue a los tres imperios en el código? Solo cuatro números. Aquí están, del canon del juego, PERSONALITIES_DATA:
| Carácter | expansión | construcción | ataque | ciencia | paz |
|---|---|---|---|---|---|
| Señor de la guerra | 80 | 90 | 170 | 60 | 40 |
| Constructor | 160 | 140 | 50 | 80 | 120 |
| Erudito | 70 | 100 | 60 | 110 | 110 |
La columna «paz» es para la diplomacia (lección 5). Las otras cuatro se multiplican en las puntuaciones: expansión en las colonias, construcción en las minas, ataque en las naves y en el ataque de la flota, ciencia en los laboratorios. El señor de la guerra no está programado para atacar: su ataque simplemente pesa 170, y el del constructor 50. Un carácter nuevo es una fila nueva en la tabla, sin código nuevo.
En el juego los rivales se ven con la orden emp (el carácter es la segunda columna):
Imperio Carácter Pl Poder TecPuntos ContigoA Imperio Terrano Constructor 1 109 0 20C Unión de Lira Constructor 1 109 0 20 pazD Academia de Vega Erudito 1 109 0 20 pazB Hegemonía de Orión Señor de la 1 109 0 20 pazMíralo
La puntuación en acción
La escena pasa por cuatro etapas. Las tres primeras son el mismo imperio en el turno 5, puntuado por el señor de la guerra, el constructor y el erudito. Las filas son las acciones en el orden de la lista, las barras son las puntuaciones y la línea naranja es «la mejor hasta ahora»: solo se mueve con un número estrictamente mayor. En la cuarta etapa el constructor ya ha pagado la colonia y puntúa de nuevo.
static List<Option> options(Empire empire, Personality p) { // Cada acción que el imperio puede pagar recibe una puntuación. List<Option> list = new ArrayList<>(); int labs = 0; for (Planet planet : empire.owned) { if (planet.lab) labs++; } for (Planet planet : empire.owned) { if (planet.mines < MAX_MINE && empire.metal >= mineCost(planet)) { list.add(new Option("mine", planet.name, p.build() * planet.rich / (4 * (planet.mines + 1)))); } if (!planet.lab && empire.metal >= LAB_COST) { list.add(new Option("lab", planet.name, p.research() * 6 / 5 - 40 * labs)); } } int cost = colonyCost(empire); if (empire.metal >= cost) { for (Planet planet : empire.free) { int value = planet.rich + 6; list.add(new Option("colony", planet.name, p.expand() * value * 50 / (cost * 6))); } } int want = wantedPower(empire, p); for (Planet planet : empire.owned) { if (planet.yard && empire.power < want && shipCount(empire) > 0) { int urge = Math.min(100, (want - empire.power) / 2); list.add(new Option("ships", planet.name, 60 + p.attack() / 2 + urge)); break; } } return list;}
static Option best(List<Option> list, int floor) { // Solo gana una puntuación estrictamente mayor, así que en un empate se queda la primera. Option pick = null; int bestU = floor; for (Option option : list) { if (option.utility() > bestU) { pick = option; bestU = option.utility(); } } return pick;}Paso a paso, en palabras
- Turno 5: 100 de metal, poder 10. El imperio puntúa cada acción que puede pagar. Primero viene el señor de la guerra con los pesos 80/90/170/60: construcción 90, ataque 170.
- Señor de la guerra: una mina en Kora I = construcción × riqueza / (4 × (minas + 1)) = 90; en Kora II 33, porque ya tiene una mina. Un laboratorio es ciencia × 6 / 5 = 72, igual en los dos planetas.
- Una colonia en Vesta I = expansión × (riqueza + 6) × 50 / (coste × 6) = 115 con coste 52. Naves: el poder deseado es 45 y tiene 10, así que 60 + ataque / 2 + (45 − 10) / 2 = 162.
- La puntuación más alta es 162: el señor de la guerra construye naves. La línea «la mejor hasta ahora» se movió tres veces (mina 90, colonia 115, naves 162), siempre con una puntuación estrictamente mayor.
- El mismo mundo, otros pesos. El constructor (160/140/50/80) valora construir: mina en Kora I 140, laboratorio 96. Los planetas, el dinero y las fórmulas son los mismos.
- Las colonias puntúan 230 y 179: la expansión pesa 160. Las naves solo 93, porque el poder deseado del constructor es 27 (su ataque es 50).
- El constructor elige la colonia en Vesta I (230). El señor de la guerra, con exactamente los mismos números, eligió naves.
- El erudito (70/100/60/110) valora la ciencia: un laboratorio puntúa 132 en los dos planetas, una mina en Kora I 100.
- Las colonias puntúan 100 y 78, las naves 99: el poder deseado es 29.
- Dos laboratorios con la misma puntuación 132. El segundo no es estrictamente mayor, así que la línea se queda en el primero: gana el laboratorio de Kora I.
- El constructor pagó la colonia: quedan 48 de metal. La siguiente colonia cuesta ahora 64 (cada planeta la encarece), no se puede pagar y no recibe puntuación: su fila está vacía. Se puntúa todo de nuevo.
- La mina de Kora I vuelve a ser 140, las naves 93. Ahora 48 de metal alcanzan para una mina, un laboratorio o naves, pero no para una colonia.
- El segundo paso del mismo turno: el constructor construye una mina en Kora I (140). El ciclo es: puntuar todo, hacer lo mejor, pagar y volver a puntuar.
El código junto a la escena es options y best del programa. En cada paso se iluminan las líneas que trabajan en ese momento.
La idea
Pagar y volver a calcular
Una puntuación vale para la situación de ahora. En cuanto el imperio hace la mejor acción, la situación cambia: baja el dinero, sube el precio de la siguiente colonia, una mina nueva hace que la siguiente valga menos, una colonia nueva abre acciones nuevas. Por eso el rival no toma la lista una vez, sino que repite: puntuar todo, hacer lo mejor, pagar, puntuar de nuevo. Como máximo 6 veces por turno (el canon hace lo mismo) o hasta que no quede nada que hacer.
static Planet find(List<Planet> planets, String name) { for (Planet planet : planets) { if (planet.name.equals(name)) return planet; } return null;}
static void apply(Empire empire, Option option) { String kind = option.kind(); if (kind.equals("mine")) { Planet planet = find(empire.owned, option.target()); empire.metal -= mineCost(planet); planet.mines++; } else if (kind.equals("lab")) { empire.metal -= LAB_COST; find(empire.owned, option.target()).lab = true; } else if (kind.equals("colony")) { Planet planet = find(empire.free, option.target()); empire.metal -= colonyCost(empire); empire.free.remove(planet); empire.owned.add(planet); } else { int count = shipCount(empire); empire.metal -= count * SHIP_COST; empire.power += count * SHIP_POWER; }}
static void endTurn(Empire empire) { int income = 4; for (Planet planet : empire.owned) { income += planet.rich * (2 + planet.mines) / 2; } empire.metal += income; empire.turn++;}
static void playTurn(Empire empire, Personality p, boolean log) { // Pagar, puntuar de nuevo y repetir mientras haya algo que hacer (como máximo 6 veces). for (int step = 1; step <= 6; step++) { Option option = best(options(empire, p), 0); if (option == null) { if (log) System.out.println(" nada más que hacer: metal " + empire.metal); return; } int before = empire.metal; apply(empire, option); if (log) { System.out.println(" " + step + ". " + label(option) + " (" + option.utility() + "): metal " + before + " → " + empire.metal); } }}
static void summarize(Personality p) { Empire empire = newEmpire(); for (int turn = 0; turn < 6; turn++) { playTurn(empire, p, false); endTurn(empire); } int mines = 0; int labs = 0; for (Planet planet : empire.owned) { mines += planet.mines; if (planet.lab) labs++; } System.out.println(p.name() + ": planetas " + empire.owned.size() + ", minas " + mines + ", labs " + labs + ", poder " + empire.power + ", metal " + empire.metal);}Aquí está un turno del constructor, acción por acción, y luego lo que han conseguido los tres caracteres en seis turnos:
Personality builder = PERSONALITIES.get(1);Empire empire = newEmpire();System.out.println(builder.name() + ", turno " + empire.turn + ":");playTurn(empire, builder, true);System.out.println();System.out.println("Después de 6 turnos:");for (Personality p : PERSONALITIES) summarize(p);Constructor, turno 5: 1. colonia Vesta I (230): metal 100 → 48 2. mina Kora I (140): metal 48 → 33 3. mina Vesta I (105): metal 33 → 18 4. naves Kora I (93): metal 18 → 2 nada más que hacer: metal 2
Después de 6 turnos:Señor de la guerra: planetas 2, minas 4, labs 0, poder 75, metal 19Constructor: planetas 3, minas 3, labs 1, poder 40, metal 38Erudito: planetas 2, minas 4, labs 1, poder 45, metal 21La primera acción es la colonia (230), que se lleva 52 de metal. Después las puntuaciones son otras: la siguiente colonia ya no se puede pagar y lo mejor es la mina de Kora I (140). Tras 4 acciones el dinero no alcanza para nada y el turno termina. Tras seis turnos las diferencias son reales, pero no dramáticas: el señor de la guerra tiene el mayor poder (75) y ningún laboratorio, el constructor tiene más planetas (3), el erudito tiene un laboratorio y 4 minas. Los pesos no excluyen nada: el constructor también construye un laboratorio cuando es lo mejor que le queda.
También hay un límite que debes conocer: la elección es codiciosa. El rival no piensa «ahorraré para la colonia», sino que toma lo mejor ahora: el constructor compra dos naves pequeñas (puntuación 93) en vez de esperar.
La idea
La flota: colonizar o atacar
Construir es la mitad. La otra mitad son las flotas: ¿adónde va la flota? La misma técnica, otra lista. Los candidatos son los sistemas de alrededor y hay dos tipos de acciones: colonizar (el sistema tiene planetas libres y ningún enemigo) y atacar (tiene planetas enemigos y somos lo bastante fuertes). Antes de la puntuación hay filtros: un ataque cuya proporción de fuerzas es inferior a 130% o cuyo objetivo está a más de 6 ni siquiera se considera.
static final int FLEET_POWER = 40;static final int STAY = 20;static final List<Spot> SPOTS = List.of( new Spot("Brisa", 1, 12, 0, 0), new Spot("Altan", 5, 0, 12, 10), new Spot("Cygra", 3, 0, 18, 60));
static List<Option> targetOptions(int power, Personality p) { List<Option> list = new ArrayList<>(); for (Spot spot : SPOTS) { int steps = spot.distance() + 1; if (spot.freeValue() > 0 && spot.enemyPower() == 0 && spot.distance() <= 5) { list.add(new Option("settle", spot.name(), p.expand() * spot.freeValue() / (6 * steps))); } int ratio = power * 100 * 100 / (spot.enemyPower() * 100 + 100); if (spot.enemyValue() > 0 && ratio >= 130 && spot.distance() <= 6) { int sure = Math.min(ratio, 300); list.add(new Option("attack", spot.name(), p.attack() * spot.enemyValue() * sure / (4 * steps * 150))); } } return list;}La flota tiene poder 40. El sistema Brisa está libre y cerca (distancia 1), Altan es enemigo y lejano (5, pero el enemigo es débil), y Cygra es enemigo con poder 60. La proporción de fuerzas contra Altan es 363%, pero en la puntuación se sube como máximo a 300%. Contra Cygra es 65%, menos de 130%: allí el ataque no se puntúa. Esta vez el umbral es STAY, 20: si nada lo supera, la flota se queda.
for (Personality p : PERSONALITIES) { List<Option> list = targetOptions(FLEET_POWER, p); System.out.println(p.name()); for (Option option : list) { System.out.println(" " + pad(label(option), 18) + padLeft(String.valueOf(option.utility()), 4)); } Option pick = best(list, STAY); System.out.println(" → " + (pick == null ? "se queda" : label(pick) + " (" + pick.utility() + ")"));}Señor de la guerra colonizar Brisa 80 atacar Altan 170 → atacar Altan (170)Constructor colonizar Brisa 160 atacar Altan 50 → colonizar Brisa (160)Erudito colonizar Brisa 70 atacar Altan 60 → colonizar Brisa (70)El señor de la guerra ataca Altan (170), a pesar de la distancia. El constructor coloniza Brisa (160). El erudito también coloniza Brisa, pero por poco: 70 frente a 60. Las fórmulas son las mismas, solo cambian los pesos.
La idea
En el juego
Lo que hemos construido es lo mismo que en el canon de Epic Cosmos, solo que más pequeño. Aquí está la puntuación real de la economía. Se reconocen la mina, el laboratorio, la colonia y las naves de nuestro programa. Además hay dos acciones más: un astillero (yard, puntuación 250 para el primero, porque sin él no hay flota) y una fortaleza (fort, solo cuando hay poder enemigo cerca). La elección es la misma idea, pero con consider(puntuación, acción) en lugar de una lista:
// Una acción en la economía: una construcción, una colonia o un pedido de naves.static class Action { String kind; int planetId; int shipKind = 0; int count = 0;
Action(String kind, int planetId) { this.kind = kind; this.planetId = planetId; }}
// La mejor acción hasta ahora: gana estrictamente la mayor puntuación, por eso en caso de empate la primera.static class Best { Action action = null; int u = 0;
void consider(int u, Action action) { if (u > this.u) { this.u = u; this.action = action; } }}
// Una acción en la economía: la mejor por utilidad, o null.static Action aiBestAction(World world, Empire empire) { Personality p = empire.personality; Best pick = new Best(); List<Planet> mine = ownedPlanets(world, empire.id); int labs = 0; int yards = 0; for (Planet planet : mine) { labs += planet.lab; yards += planet.yard; } TechMods mods = techMods(empire); for (Planet planet : mine) { if (planet.mines < MAX_MINE && canPay(empire, buildCost("mine", planet))) { pick.consider((p.build * richSum(planet)) / (4 * (planet.mines + 1)), new Action("mine", planet.id)); } if (planet.lab == 0 && canPay(empire, buildCost("lab", planet))) { pick.consider((p.research * 6) / 5 - 40 * labs, new Action("lab", planet.id)); } if (planet.yard == 0 && canPay(empire, buildCost("yard", planet))) { pick.consider(yards == 0 ? 250 : mine.size() >= 5 && yards < 2 ? 60 : 0, new Action("yard", planet.id)); } if (mods.fort > 0 && planet.fort < MAX_FORT && canPay(empire, buildCost("fort", planet))) { int threat = enemyPowerAt(world, empire, planet.systemId); for (int next : world.systems.get(planet.systemId).links) threat += enemyPowerAt(world, empire, next); if (threat > 0) pick.consider(40 + minOf(160, threat) + (planet.id == empire.capital ? 30 : 0), new Action("fort", planet.id)); } } int[] cost = colonyCost(world, empire); if (canPay(empire, cost)) { for (Planet planet : world.planets) { if (planet.owner >= 0 || !hasPresence(world, empire.id, planet.systemId) || hostileIn(world, empire.id, planet.systemId)) continue; pick.consider((p.expand * (richSum(planet) + 6) * 50) / (cost[0] * 6), new Action("col", planet.id)); } } // Naves: mientras la fuerza esté por debajo de la deseada para este turno. int want = ((20 + 3 * world.turn) * (140 + p.attack)) / 240; for (Empire other : world.empires) { if (other.alive && other.id != empire.id && empire.war[other.id] == 1) want = (want * 3) / 2; } int have = empirePower(world, empire.id) + queuedPower(world, empire.id); if (have < want) { for (Planet planet : mine) { if (planet.yard == 0) continue; int kind = counterKind(world, empire); if (shipAllowed(empire, 3) && canPay(empire, SHIPS.get(3).cost) && have >= 30) kind = 3; int count = 4; while (count > 0 && !canPay(empire, scaleCost(SHIPS.get(kind).cost, count))) count--; if (count > 0) { Action action = new Action("ship", planet.id); action.shipKind = kind; action.count = count; pick.consider(60 + p.attack / 2 + minOf(100, (want - have) / 2), action); } break; } } return pick.action;}Y aquí el bucle «puntuar, hacer, repetir» (aiEconomy) y el turno completo de un rival (aiPlay): ciencia, economía, flotas. La ciencia es la misma técnica en pequeño: aiResearch puntúa cada tecnología disponible con ciencia × (100 + bonus del carácter) × 10 / (coste + 5) y toma la más alta (el árbol de tecnologías está en la lección 2).
static void aiEconomy(World world, Empire empire) { for (int step = 0; step < 6; step++) { Action action = aiBestAction(world, empire); if (action == null) return; Planet planet = world.planets.get(action.planetId); String error = null; if (action.kind.equals("col")) error = colonize(world, empire, planet); else if (action.kind.equals("ship")) error = queueShips(world, empire, planet, action.shipKind, action.count); else error = buildStructure(world, empire, action.kind, planet); if (error != null) return; }}
static void aiPlay(World world, Empire empire) { // La inteligencia artificial vuelve a planificar en cada turno. empire.orders = new ArrayList<>(); aiResearch(world, empire); aiEconomy(world, empire); aiFleets(world, empire);}Las diferencias con nuestro programa son pocas:
- Cuatro recursos en vez de uno.
canPaycompara cuatro números, y la riqueza de un planeta son cuatro números (richSumlos suma y cuenta doble el cristal y el uranio). - Las mismas funciones que el jugador. El rival llama a
colonize,queueShipsybuildStructure, que el jugador llama con las órdenescol,shipyb. Paga los mismos precios con las mismas reglas. - La guerra cambia el poder deseado. Por cada imperio con el que estamos en guerra, el poder deseado se multiplica por 3/2, y el tipo de naves se elige según el enemigo (
counterKind: el interceptor vence al bombardero, el bombardero vence a la fragata, la fragata vence al interceptor; los roles son de la lección 3). - La defensa está en las puntuaciones. Se construye una fortaleza si hay un enemigo cerca, y la flota vuelve a defender su propio sistema (
ownHere). - Sin azar. Ni un solo dado en estas decisiones: la misma entrada da el mismo movimiento en Java, C#, Python y JavaScript.
Y aquí la elección del objetivo de la flota:
static void aiFleets(World world, Empire empire) { Personality p = empire.personality; List<Fleet> snapshot = new ArrayList<>(); for (Fleet fleet : world.fleets) { if (fleet.owner == empire.id) snapshot.add(fleet); } for (Fleet fleet : snapshot) { int[] busy = orderedShips(empire, fleet.systemId); int[] free = {0, 0, 0, 0}; for (int k = 0; k < SHIP_KIND_COUNT; k++) free[k] = fleet.ships[k] - busy[k]; int myPower = countsPower(free); if (myPower == 0) continue; int here = fleet.systemId; int[] dist = bfs(world.systems, here).dist; // Mantener: si estamos en casa bajo amenaza o asediando a un enemigo. int bestU = 20; int target = -1; boolean split = false; boolean siege = false; for (StarSystem system : world.systems) { int foe = enemyPowerAt(world, empire, system.id); int freeValue = 0; int enemyValue = 0; boolean ownHere = false; for (int pid : system.planets) { Planet planet = world.planets.get(pid); if (planet.owner < 0) freeValue += richSum(planet) + 6; else if (planet.owner == empire.id) ownHere = true; else if (empire.war[planet.owner] == 1) enemyValue += richSum(planet) + 10; } int d = dist[system.id]; if (d < 0) continue; int ratio = percent(100L * myPower, 100L * foe + 100); if (system.id == here) { if (enemyValue > 0 && ratio >= 80) { bestU = 250; siege = true; } else if (ownHere && foe > 0 && ratio >= 60) { bestU = 200; siege = true; } continue; } if (freeValue > 0 && foe == 0 && d <= 5) { int u = (p.expand * freeValue) / (6 * (d + 1)); if (u > bestU) { bestU = u; target = system.id; split = totalOf(free) >= 6; siege = false; } } if (enemyValue > 0 && ratio >= 130 && d <= 6) { int u = (p.attack * enemyValue * minOf(ratio, 300)) / (4 * (d + 1) * 150); if (u > bestU) { bestU = u; target = system.id; split = false; siege = false; } } if (ownHere && foe > 0 && d <= 4 && ratio >= 60) { int u = 150 - 10 * d; if (u > bestU) { bestU = u; target = system.id; split = false; siege = false; } } } if (target < 0 || siege) continue; int step = nextStep(world.systems, here, target); if (step < 0) continue; orderMove(world, empire, here, target, split ? halfOf(free) : null); }}Se parece a nuestro programa: las mismas dos fórmulas (colonizar y atacar), el filtro 130%, el umbral 20. Además hay el «mantener» (en un sistema con enemigos y buena proporción la flota se queda y asedia: 250), la defensa del propio sistema (150 − 10 × distancia) y dividir la flota en dos cuando es grande.
Pruébalo
Predice la salida
Cuatro acciones, dos de ellas con la puntuación más alta 132. ¿Qué imprime el programa? (Las funciones best y label son de la lección.)
List<Option> list = List.of( new Option("mine", "Kora I", 90), new Option("lab", "Kora I", 132), new Option("lab", "Kora II", 132), new Option("colony", "Vesta I", 100));Option pick = best(list, 0);System.out.println(label(pick) + " (" + pick.utility() + ")");El laboratorio de Kora I es el primero con la puntuación 132. El laboratorio de Kora II tiene la misma puntuación, pero no es estrictamente mayor y no cambia la elección. La colonia (100) es menor. La segunda respuesta saldría con >=: entonces cada puntuación igual cambia la elección y gana la última.
Pruébalo
Rellena el hueco
El valor inicial de la mejor es el parámetro de la función: 0 para la economía, 20 para la flota. ¿Con qué empieza bestU?
static Option best(List<Option> list, int floor) {
// Solo gana una puntuación estrictamente mayor, así que en un empate se queda la primera.
Option pick = null;
int bestU = ;
for (Option option : list) {
if (option.utility() > bestU) {
pick = option;
bestU = option.utility();
}
}
return pick;
}Empieza por el umbral: floor. Así la misma función sirve para la economía (0) y para la flota (20), y una puntuación que no lo supera no gana nada: el resultado es null. Si todo empezara en 0, la flota saldría a un ataque con puntuación 5 en vez de quedarse.
Pruébalo
Encuentra el error
Un compañero calcula las colonias sin paréntesis, y ahora todos los imperios, hasta el señor de la guerra, solo colonizan. Estas son las puntuaciones del constructor:
for (Personality p : PERSONALITIES) { List<Option> list = options(newEmpire(), p); System.out.println(p.name()); for (Option option : list) { System.out.println(" " + pad(label(option), 21) + padLeft(String.valueOf(option.utility()), 4)); } Option pick = best(list, 0); System.out.println(" → " + label(pick) + " (" + pick.utility() + ")");}Señor de la guerra mina Kora I 90 laboratorio Kora I 72 mina Kora II 33 laboratorio Kora II 72 colonia Vesta I 4152 colonia Vesta II 3228 naves Kora I 162 → colonia Vesta I (4152)Constructor mina Kora I 140 laboratorio Kora I 96 mina Kora II 52 laboratorio Kora II 96 colonia Vesta I 8304 colonia Vesta II 6456 naves Kora I 93 → colonia Vesta I (8304)Erudito mina Kora I 100 laboratorio Kora I 132 mina Kora II 37 laboratorio Kora II 132 colonia Vesta I 3630 colonia Vesta II 2826 naves Kora I 99 → colonia Vesta I (3630)La colonia en Vesta I es 8304 en vez de 230. ¿Qué línea calcula mal?
Elige la línea con el error.
En … * 50 / cost * 6 primero se divide por el coste (entera, con el resto cortado) y luego se multiplica por 6: 72000 / 52 = 1384, 1384 × 6 = 8304. La fórmula es … × 50 / (coste × 6): el divisor es todo el producto. Sin los paréntesis la colonia es unas 36 veces mayor de lo debido y vence a todo. Lección: la división entera no es asociativa, y la puntuación de una fórmula necesita sus paréntesis.
Más precisamente
Con más precisión: lo que una puntuación no hace
No es una probabilidad y no tiene unidad. 162 no significa «162 puntos de beneficio». El número solo tiene sentido para comparar dentro de una lista de un rival. Por eso las fórmulas están ajustadas (4, 50, 6, 60), no deducidas. Los pesos del carácter son números enteros «en porcentaje», como dice la tabla del canon, para que no haya fracciones.
Es codiciosa. El rival mira solo la siguiente acción y no planifica: no ahorra para una colonia y no sabe que dentro de tres turnos será más débil. Ese es el precio de la sencillez. Los sistemas más serios miran hacia delante: planificación de objetivos (GOAP), árboles de búsqueda o Monte Carlo (MCTS, como en el Go). Pero en una estrategia por turnos una elección codiciosa con buenas fórmulas suele ser lo bastante lista para ser divertida. En los combates de Arcanum es la misma técnica: Enemigos que piensan.
Las puntuaciones se ajustan, no se escriben. ¿De dónde salen los números 80, 90, 170? Primero una estimación, luego equilibrio con simulación: partidas de IA contra IA que cuentan qué carácter gana cuánto. Esa herramienta está en la última lección.
El rival no hace trampas, más bien al contrario. Paga los mismos precios y llama a las mismas funciones que tú, pero en una partida con un humano produce solo el 80% de lo normal (RIVAL_PACE, el nivel fácil); en la simulación es el 100%. La línea está en empireIncome y está a la vista.
Las puntuaciones iguales no se dejan a la suerte. El > estricto y el orden fijo de generación hacen la elección igual en los cuatro lenguajes y en cada repetición: para cada planeta mina, laboratorio, astillero, fortaleza, luego las colonias y por último las naves. Por eso el orden es parte de las reglas, no un detalle.
Ejercicio
La fortaleza
Resuelve la tarea en tu editor. Parte del programa de los tres caracteres.
Fortaleza. Añade una acción nueva fort: una fortaleza orbital en un planeta. Cuesta 25 de metal (FORT_COST) y se puntúa según el peligro: 40 + min(160, peligro) + 30 si el planeta es la capital (el primero de la lista), si no sin el +30. El peligro es un valor nuevo threat del imperio y en esta tarea es 120. La acción solo es elegible si el peligro es positivo y hay 25 de metal. El orden en la lista es: para cada planeta mina, laboratorio, fortaleza.
La salida esperada (añade fort también a label):
Señor de la guerra mina Kora I 90 laboratorio Kora I 72 fortaleza Kora I 190 mina Kora II 33 laboratorio Kora II 72 fortaleza Kora II 160 colonia Vesta I 115 colonia Vesta II 89 naves Kora I 162 → fortaleza Kora I (190)Constructor mina Kora I 140 laboratorio Kora I 96 fortaleza Kora I 190 mina Kora II 52 laboratorio Kora II 96 fortaleza Kora II 160 colonia Vesta I 230 colonia Vesta II 179 naves Kora I 93 → colonia Vesta I (230)Erudito mina Kora I 100 laboratorio Kora I 132 fortaleza Kora I 190 mina Kora II 37 laboratorio Kora II 132 fortaleza Kora II 160 colonia Vesta I 100 colonia Vesta II 78 naves Kora I 99 → fortaleza Kora I (190)¿Qué cambia en las elecciones? Con peligro 120 la fortaleza de la capital vale 190, y vence a las naves del señor de la guerra (162) y al laboratorio del erudito (132). El constructor sigue colonizando: 230 es más que 190. El peligro no es un carácter: la fortaleza tiene la misma puntuación para todos, por eso no está en la tabla de pesos.
Más tareas
- Construir la fortaleza. Añade a
applyla construcción de una fortaleza y ejecuta un turno entero del señor de la guerra con peligro 120: ¿cuántas acciones hace antes de quedarse sin dinero? - Un carácter nuevo. Añade una cuarta fila (
trader: expansión 120, construcción 120, ataque 60, ciencia 70). ¿Qué elige en la situación inicial? ¿Cambia algo en el código además de la tabla? - El tope. El ataque de la flota usa
min(proporción, 300). ¿Qué pasa sin el tope si el enemigo tiene poder 0 (la proporción es 4000)? ¿Cómo cambia la elección del señor de la guerra y del constructor?
Test
Compruébate
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Excepciones · Lanzar excepciones y crear las tuyas · Listas · record, switch y patrones
- Ejercicios sobre el tema: Epic Cosmos: el imperio