Ir al contenido
wannacode

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:

class Planet {
constructor(name, rich, mines, yard) {
this.name = name;
this.rich = rich;
this.mines = mines;
this.lab = false;
this.yard = yard;
}
}
class Empire {
constructor() {
this.metal = 100;
this.power = 10;
this.turn = 5;
this.owned = [];
this.free = [];
}
}
const MAX_MINE = 3;
const LAB_COST = 40;
const SHIP_COST = 8;
const SHIP_POWER = 5;
function newEmpire() {
const empire = new Empire();
empire.owned.push(new Planet("Kora I", 4, 0, true));
empire.owned.push(new Planet("Kora II", 3, 1, false));
empire.free.push(new Planet("Vesta I", 3, 0, false));
empire.free.push(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.
function options(empire, p) {
// Cada acción que el imperio puede pagar recibe una puntuación.
const list = [];
let labs = 0;
for (const planet of empire.owned) {
if (planet.lab) labs++;
}
for (const planet of empire.owned) {
if (planet.mines < MAX_MINE && empire.metal >= mineCost(planet)) {
list.push({ kind: "mine", target: planet.name, utility: Math.floor((p.build * planet.rich) / (4 * (planet.mines + 1))) });
}
if (!planet.lab && empire.metal >= LAB_COST) {
list.push({ kind: "lab", target: planet.name, utility: Math.floor((p.research * 6) / 5) - 40 * labs });
}
}
const cost = colonyCost(empire);
if (empire.metal >= cost) {
for (const planet of empire.free) {
const value = planet.rich + 6;
list.push({ kind: "colony", target: planet.name, utility: Math.floor((p.expand * value * 50) / (cost * 6)) });
}
}
const want = wantedPower(empire, p);
for (const planet of empire.owned) {
if (planet.yard && empire.power < want && shipCount(empire) > 0) {
const urge = Math.min(100, Math.floor((want - empire.power) / 2));
list.push({ kind: "ships", target: planet.name, utility: 60 + Math.floor(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 objeto simple con tres campos. JavaScript no tiene división entera, por eso cada división va dentro de Math.floor(…): los números son positivos y el resultado es como en los otros lenguajes.
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).

function best(list, floor) {
// Solo gana una puntuación estrictamente mayor, así que en un empate se queda la primera.
let pick = null;
let bestU = floor;
for (const option of list) {
if (option.utility > bestU) {
pick = option;
bestU = option.utility;
}
}
return pick;
}
function label(option) {
if (option.kind === "mine") return "mina " + option.target;
if (option.kind === "lab") return "laboratorio " + option.target;
if (option.kind === "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 (const p of PERSONALITIES) {
const list = options(newEmpire(), p);
console.log(p.name);
for (const option of list) {
console.log(" " + pad(label(option), 21) + padLeft(String(option.utility), 4));
}
const pick = best(list, 0);
console.log(" → " + label(pick) + " (" + pick.utility + ")");
}
Salida
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 Contigo
A Imperio Terrano Constructor 1 109 0 20
C Unión de Lira Constructor 1 109 0 20 paz
D Academia de Vega Erudito 1 109 0 20 paz
B Hegemonía de Orión Señor de la 1 109 0 20 paz

Mí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.

function options(empire, p) {
// Cada acción que el imperio puede pagar recibe una puntuación.
const list = [];
let labs = 0;
for (const planet of empire.owned) {
if (planet.lab) labs++;
}
for (const planet of empire.owned) {
if (planet.mines < MAX_MINE && empire.metal >= mineCost(planet)) {
list.push({ kind: "mine", target: planet.name, utility: Math.floor((p.build * planet.rich) / (4 * (planet.mines + 1))) });
}
if (!planet.lab && empire.metal >= LAB_COST) {
list.push({ kind: "lab", target: planet.name, utility: Math.floor((p.research * 6) / 5) - 40 * labs });
}
}
const cost = colonyCost(empire);
if (empire.metal >= cost) {
for (const planet of empire.free) {
const value = planet.rich + 6;
list.push({ kind: "colony", target: planet.name, utility: Math.floor((p.expand * value * 50) / (cost * 6)) });
}
}
const want = wantedPower(empire, p);
for (const planet of empire.owned) {
if (planet.yard && empire.power < want && shipCount(empire) > 0) {
const urge = Math.min(100, Math.floor((want - empire.power) / 2));
list.push({ kind: "ships", target: planet.name, utility: 60 + Math.floor(p.attack / 2) + urge });
break;
}
}
return list;
}
function best(list, floor) {
// Solo gana una puntuación estrictamente mayor, así que en un empate se queda la primera.
let pick = null;
let bestU = floor;
for (const option of list) {
if (option.utility > bestU) {
pick = option;
bestU = option.utility;
}
}
return pick;
}
Paso a paso, en palabras
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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).
  7. El constructor elige la colonia en Vesta I (230). El señor de la guerra, con exactamente los mismos números, eligió naves.
  8. El erudito (70/100/60/110) valora la ciencia: un laboratorio puntúa 132 en los dos planetas, una mina en Kora I 100.
  9. Las colonias puntúan 100 y 78, las naves 99: el poder deseado es 29.
  10. 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.
  11. 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.
  12. 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.
  13. 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.

function find(planets, name) {
return planets.find((planet) => planet.name === name);
}
function apply(empire, option) {
if (option.kind === "mine") {
const planet = find(empire.owned, option.target);
empire.metal -= mineCost(planet);
planet.mines++;
} else if (option.kind === "lab") {
empire.metal -= LAB_COST;
find(empire.owned, option.target).lab = true;
} else if (option.kind === "colony") {
const planet = find(empire.free, option.target);
empire.metal -= colonyCost(empire);
empire.free.splice(empire.free.indexOf(planet), 1);
empire.owned.push(planet);
} else {
const count = shipCount(empire);
empire.metal -= count * SHIP_COST;
empire.power += count * SHIP_POWER;
}
}
function endTurn(empire) {
let income = 4;
for (const planet of empire.owned) {
income += Math.floor((planet.rich * (2 + planet.mines)) / 2);
}
empire.metal += income;
empire.turn++;
}
function playTurn(empire, p, log) {
// Pagar, puntuar de nuevo y repetir mientras haya algo que hacer (como máximo 6 veces).
for (let step = 1; step <= 6; step++) {
const option = best(options(empire, p), 0);
if (option === null) {
if (log) console.log(" nada más que hacer: metal " + empire.metal);
return;
}
const before = empire.metal;
apply(empire, option);
if (log) {
console.log(" " + step + ". " + label(option) + " (" + option.utility + "): metal " + before + " → " + empire.metal);
}
}
}
function summarize(p) {
const empire = newEmpire();
for (let turn = 0; turn < 6; turn++) {
playTurn(empire, p, false);
endTurn(empire);
}
let mines = 0;
let labs = 0;
for (const planet of empire.owned) {
mines += planet.mines;
if (planet.lab) labs++;
}
console.log(p.name + ": planetas " + empire.owned.length + ", 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:

const builder = PERSONALITIES[1];
const empire = newEmpire();
console.log(builder.name + ", turno " + empire.turn + ":");
playTurn(empire, builder, true);
console.log();
console.log("Después de 6 turnos:");
for (const p of PERSONALITIES) summarize(p);
Salida
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 19
Constructor: planetas 3, minas 3, labs 1, poder 40, metal 38
Erudito: planetas 2, minas 4, labs 1, poder 45, metal 21

La 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.

const FLEET_POWER = 40;
const STAY = 20;
const SPOTS = [
{ name: "Brisa", distance: 1, freeValue: 12, enemyValue: 0, enemyPower: 0 },
{ name: "Altan", distance: 5, freeValue: 0, enemyValue: 12, enemyPower: 10 },
{ name: "Cygra", distance: 3, freeValue: 0, enemyValue: 18, enemyPower: 60 },
];
function targetOptions(power, p) {
const list = [];
for (const spot of SPOTS) {
const steps = spot.distance + 1;
if (spot.freeValue > 0 && spot.enemyPower === 0 && spot.distance <= 5) {
list.push({ kind: "settle", target: spot.name, utility: Math.floor((p.expand * spot.freeValue) / (6 * steps)) });
}
const ratio = Math.floor((power * 100 * 100) / (spot.enemyPower * 100 + 100));
if (spot.enemyValue > 0 && ratio >= 130 && spot.distance <= 6) {
const sure = Math.min(ratio, 300);
list.push({ kind: "attack", target: spot.name, utility: Math.floor((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 (const p of PERSONALITIES) {
const list = targetOptions(FLEET_POWER, p);
console.log(p.name);
for (const option of list) {
console.log(" " + pad(label(option), 18) + padLeft(String(option.utility), 4));
}
const pick = best(list, STAY);
console.log(" → " + (pick === null ? "se queda" : label(pick) + " (" + pick.utility + ")"));
}
Salida
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: la mejor por utilidad, o null.
function aiBestAction(world, empire) {
const p = empire.personality;
let best = null;
let bestU = 0;
const consider = (u, action) => {
if (u > bestU) {
bestU = u;
best = action;
}
};
const mine = ownedPlanets(world, empire.id);
let labs = 0;
let yards = 0;
for (const planet of mine) {
labs += planet.lab;
yards += planet.yard;
}
const mods = techMods(empire);
for (const planet of mine) {
if (planet.mines < MAX_MINE && canPay(empire, buildCost('mine', planet))) {
consider(Math.floor((p.build * richSum(planet)) / (4 * (planet.mines + 1))), { kind: 'mine', planetId: planet.id });
}
if (planet.lab === 0 && canPay(empire, buildCost('lab', planet))) {
consider(Math.floor((p.research * 6) / 5) - 40 * labs, { kind: 'lab', planetId: planet.id });
}
if (planet.yard === 0 && canPay(empire, buildCost('yard', planet))) {
consider(yards === 0 ? 250 : mine.length >= 5 && yards < 2 ? 60 : 0, { kind: 'yard', planetId: planet.id });
}
if (mods.fort > 0 && planet.fort < MAX_FORT && canPay(empire, buildCost('fort', planet))) {
let threat = enemyPowerAt(world, empire, planet.systemId);
for (const next of world.systems[planet.systemId].links) threat += enemyPowerAt(world, empire, next);
if (threat > 0) consider(40 + minOf(160, threat) + (planet.id === empire.capital ? 30 : 0), { kind: 'fort', planetId: planet.id });
}
}
const cost = colonyCost(world, empire);
if (canPay(empire, cost)) {
for (const planet of world.planets) {
if (planet.owner >= 0 || !hasPresence(world, empire.id, planet.systemId) || hostileIn(world, empire.id, planet.systemId)) continue;
consider(Math.floor((p.expand * (richSum(planet) + 6) * 50) / (cost[0] * 6)), { kind: 'col', planetId: planet.id });
}
}
// Naves: mientras la fuerza esté por debajo de la deseada para este turno.
let want = Math.floor(((20 + 3 * world.turn) * (140 + p.attack)) / 240);
for (const other of world.empires) {
if (other.alive && other.id !== empire.id && empire.war[other.id] === 1) want = Math.floor((want * 3) / 2);
}
const have = empirePower(world, empire.id) + queuedPower(world, empire.id);
if (have < want) {
for (const planet of mine) {
if (planet.yard === 0) continue;
let kind = counterKind(world, empire);
if (shipAllowed(empire, 3) && canPay(empire, SHIPS[3].cost) && have >= 30) kind = 3;
let count = 4;
while (count > 0 && !canPay(empire, scaleCost(SHIPS[kind].cost, count))) count--;
if (count > 0) consider(60 + Math.floor(p.attack / 2) + minOf(100, Math.floor((want - have) / 2)), { kind: 'ship', planetId: planet.id, shipKind: kind, count: count });
break;
}
}
return best;
}

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

function aiEconomy(world, empire) {
for (let step = 0; step < 6; step++) {
const action = aiBestAction(world, empire);
if (action === null) return;
const planet = world.planets[action.planetId];
let error = null;
if (action.kind === 'col') error = colonize(world, empire, planet);
else if (action.kind === 'ship') error = queueShips(world, empire, planet, action.shipKind, action.count);
else error = buildStructure(world, empire, action.kind, planet);
if (error !== null) return;
}
}
function aiPlay(world, empire) {
// La inteligencia artificial vuelve a planificar en cada turno.
empire.orders = [];
aiResearch(world, empire);
aiEconomy(world, empire);
aiFleets(world, empire);
}

Las diferencias con nuestro programa son pocas:

  • Cuatro recursos en vez de uno. canPay compara cuatro números, y la riqueza de un planeta son cuatro números (richSum los suma y cuenta doble el cristal y el uranio).
  • Las mismas funciones que el jugador. El rival llama a colonize, queueShips y buildStructure, que el jugador llama con las órdenes col, ship y b. 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:

function aiFleets(world, empire) {
const p = empire.personality;
const snapshot = [];
for (const fleet of world.fleets) {
if (fleet.owner === empire.id) snapshot.push(fleet);
}
for (const fleet of snapshot) {
const busy = orderedShips(empire, fleet.systemId);
const free = [0, 0, 0, 0];
for (let k = 0; k < SHIP_KIND_COUNT; k++) free[k] = fleet.ships[k] - busy[k];
const myPower = countsPower(free);
if (myPower === 0) continue;
const here = fleet.systemId;
const dist = bfs(world.systems, here).dist;
// Mantener: si estamos en casa bajo amenaza o asediando a un enemigo.
let bestU = 20;
let target = -1;
let split = false;
let siege = false;
for (const system of world.systems) {
const foe = enemyPowerAt(world, empire, system.id);
let freeValue = 0;
let enemyValue = 0;
let ownHere = false;
for (const pid of system.planets) {
const planet = world.planets[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;
}
const d = dist[system.id];
if (d < 0) continue;
const ratio = percent(myPower * 100, foe * 100 + 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) {
const u = Math.floor((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) {
const u = Math.floor((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) {
const u = 150 - 10 * d;
if (u > bestU) {
bestU = u;
target = system.id;
split = false;
siege = false;
}
}
}
if (target < 0 || siege) continue;
const 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.)

const list = [
{ kind: "mine", target: "Kora I", utility: 90 },
{ kind: "lab", target: "Kora I", utility: 132 },
{ kind: "lab", target: "Kora II", utility: 132 },
{ kind: "colony", target: "Vesta I", utility: 100 },
];
const pick = best(list, 0);
console.log(label(pick) + " (" + pick.utility + ")");
Respuesta

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?

function best(list, floor) {
  // Solo gana una puntuación estrictamente mayor, así que en un empate se queda la primera.
  let pick = null;
  let bestU = ;
  for (const option of list) {
    if (option.utility > bestU) {
      pick = option;
      bestU = option.utility;
    }
  }
  return pick;
}

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 (const p of PERSONALITIES) {
const list = options(newEmpire(), p);
console.log(p.name);
for (const option of list) {
console.log(" " + pad(label(option), 21) + padLeft(String(option.utility), 4));
}
const pick = best(list, 0);
console.log(" → " + label(pick) + " (" + pick.utility + ")");
}
Salida
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.

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

Salida
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 apply la 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

1. El señor de la guerra y el constructor puntúan el mismo mundo: los mismos planetas, el mismo dinero, las mismas fórmulas. El señor de la guerra elige naves, el constructor una colonia. ¿De dónde viene la diferencia?
2. El erudito tiene dos laboratorios con la puntuación 132, y es la más alta. ¿Cuál se construye?
3. Tras la colonia el constructor tiene 48 de metal y en la lista no hay ninguna colonia, aunque Vesta II sigue libre. ¿Por qué?
4. Un compañero escribe «expansión × valor × 50 / coste × 6» en vez de «… / (coste × 6)». ¿Cuál es la puntuación de la colonia en Vesta I para el constructor?
5. La flota tiene poder 40. En el sistema Cygra el enemigo tiene poder 60 y ningún carácter lo ataca, ni siquiera el señor de la guerra. ¿Por qué?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·