Ходът на галактиката
Накратко: Ходът на галактиката е редица от фази в твърд ред; флотите се движат едновременно (първо тръгват всички, после пристигат), събитията се теглят по тегла, мирът и войната имат правило за най-малко MIN_PEACE хода, а новините и озлобването слушат шината от събития, без публикуващият да знае за тях.
Проблем
Всички играят наведнъж, а редът решава
В стратегията по ходове играчът пише end (или wait 3 за три хода подред) и цялата галактика се мърда: съперниците решават какво да правят, флотите тръгват, стават битки, планетите произвеждат, понякога дойдат пирати. Това, което за играча е едно натискане на клавиш, в кода е ход: поредица от фази в твърд ред.
Редът не е украса. Вземи най-простия пример: три флота във верига, A→B, B→C, C→D, и всичките 3 кораба са в A. Ако обработваш поръчките една след друга и местиш корабите в същия масив, корабите стигат от A до D за един ход: първата поръчка ги слага в B, втората ги вижда там и ги праща в C, третата в D. Играч, който види това, разбира веднага, че флотите са телепорти.
Този урок строи хода на малка галактика от три империи (A, B, C) и четири системи по права линия, със същите четири идеи като в канона:
- фазите на хода и едновременното движение;
- случайните събития: колко често идват и кои по-често;
- мирът и войната: правило, което не позволява да се обявява и сключва мир всеки ход;
- шината от събития: как новините, озлобването и сигналите научават какво се е случило, без никой да вика никого.
В края има цял ход от фази, който печата какво става, и сцена, която го показва.
Идеята
Фази и едновременно движение
Ходът е функция, която вика фази една след друга. В канона те са: дипломация на изкуствения интелект, неговите поръчки, движение на всички флотове, битки, завладяване, производство и наука, събитие, край на хода. В нашата малка галактика са пет: дипломация, движение, битки, производство, събитие.
Движението се разбива на две: първо всички флоти напускат системите си, чак тогава пристигат. Ето двата начина върху веригата от по-горе. Поръчката е [от, към, кораби]; корабите са 3 в системата 0:
// [от, към, кораби]const ORDERS = [[0, 1, 3], [1, 2, 3], [2, 3, 3]];
function moveOneByOne(start, orders) { const ships = start.slice(); for (const [source, target, count] of orders) { const take = Math.min(count, ships[source]); ships[source] -= take; ships[target] += take; } return ships;}
function moveTogether(start, orders) { const ships = start.slice(); const takes = []; // Първо всички флоти напускат системите си... for (const [source, target, count] of orders) { const take = Math.min(count, ships[source]); ships[source] -= take; takes.push(take); } // ...и чак тогава пристигат. orders.forEach(([, target], i) => { ships[target] += takes[i]; }); return ships;}const start = [3, 0, 0, 0];console.log("последователно: " + show(moveOneByOne(start, ORDERS)));console.log("едновременно: " + show(moveTogether(start, ORDERS)));последователно: 0 0 0 3едновременно: 0 3 0 0Последователният вариант е тихо грешен: програмата не се чупи, флотите просто стават бързи. Едновременният взима за всяка поръчка min(кораби от поръчката, кораби в системата в началото) и ги маха (напускане), а после ги слага (пристигане). Втората поръчка (B→C) вижда в B нула кораби, защото първите още не са пристигнали, и не мести нищо. Затова веригата е 0 3 0 0: корабите са стигнали само до B.
start.slice() е копие на масива: без него функцията би променила масива на викащия.По-точно, за любопитните
Същото правило важи за всичко, което се случва „наведнъж“: две империи, които си разменят системи, два флота, които се разминават. Ако движиш ги един по един, вторият вижда света, който първият вече е променил, и резултатът зависи от реда на играчите. Правилото на двете стъпки (вземи всичко, после върни всичко) е същото като при размяна на две променливи с помощна: нужна е памет за „старото“.
В канона движението е още по-близо до нашето, но с една добавка: дългият път продължава сам. Поръчката помни крайната цел (dest), а след пристигане се записва нова поръчка за следващата стъпка. Затова флот, пратен през пет системи, върви пет хода, по една система на ход.
Идеята
Случайни събития по тегла
На всеки ход с шанс 40% се случва едно събитие на случайна империя. Кое точно? Ако всички видове имат равен шанс, пиратите ще идват на първия ход, когато империята няма и един кораб. Затова всеки вид има тегло (колко често е) и най-ранен ход (от кога е позволен). Ето таблицата на канона и избора:
// id, тегло, най-ранен ходconst EVENT_DEFS = [ { id: "meteor", weight: 20, minTurn: 1 }, { id: "vein", weight: 15, minTurn: 1 }, { id: "pirates", weight: 20, minTurn: 6 }, { id: "breakthrough", weight: 15, minTurn: 1 }, { id: "unrest", weight: 10, minTurn: 5 }, { id: "boom", weight: 15, minTurn: 1 }, { id: "flare", weight: 10, minTurn: 1 }, { id: "derelict", weight: 8, minTurn: 3 }];
function pickWeighted(rng, weights) { let total = 0; for (const weight of weights) total += weight; let roll = rng.nextInt(total); for (let i = 0; i < weights.length; i++) { if (roll < weights[i]) return i; roll -= weights[i]; } return weights.length - 1;}
function rollEvent(rng, turn) { if (!rng.chance(EVENT_CHANCE)) return null; const empire = rng.nextInt(NAMES.length); const weights = EVENT_DEFS.map((def) => (turn >= def.minTurn ? def.weight : 0)); return { kind: pickWeighted(rng, weights), empire: empire };}Изборът по тегла е „рулетка“: хвърля се число от 0 до сбора на теглата и се върви по видовете, като се изважда теглото на всеки, докато числото стане по-малко от текущото тегло. Вид, който още не е позволен, получава тегло 0 и никога не излиза, а сборът намалява, така че другите стават по-вероятни. Програмата пуска дванайсет хода и после още хиляда, за да се види колко пъти излиза всеки вид:
const rng = new Rng(7);for (let turn = 1; turn <= 12; turn++) { const roll = rollEvent(rng, turn); if (roll !== null) console.log("ход " + turn + ": " + EVENT_DEFS[roll.kind].id + " → " + NAMES[roll.empire]);}
const counts = new Array(EVENT_DEFS.length).fill(0);for (let i = 0; i < 1000; i++) { const roll = rollEvent(rng, 99); if (roll !== null) counts[roll.kind]++;}console.log("Събития за 1000 хода:");for (let i = 0; i < counts.length; i++) { console.log(EVENT_DEFS[i].id + " " + EVENT_DEFS[i].weight + ": " + counts[i]);}ход 1: boom → Съюз Лираход 2: breakthrough → Академия Вегаход 3: vein → Академия Вегаход 4: boom → Академия Вегаход 8: pirates → Съюз Лираход 9: breakthrough → Земна империяСъбития за 1000 хода:meteor 20: 70vein 15: 53pirates 20: 78breakthrough 15: 58unrest 10: 30boom 15: 50flare 10: 28derelict 8: 28Пиратите (най-ранен ход 6) се виждат чак на ход 8. В хилядата хода най-често излизат пиратите (78) и метеоритите (70) с тегло 20, най-рядко изригването (28) и изоставените кораби (28) с тегла 10 и 8. Бройките не са равни на теглата, защото е случайно, но редът им е същият. Шансът за събитие е 40% (EVENT_CHANCE), а генераторът е със семе: един и същ вход дава един и същи изход на четирите езика, затова записът на играта може да се повтори.
По-точно, за любопитните
В канона всяко събитие прави нещо различно: метеоритите отнемат метал, жилата вдига богатството на една планета, пиратите се бият с флота на империята (и се печелят или губят ресурси), пробивът в науката дава точки, размириците свалят ниво на мина, бумът дава газ и кристал, изригването спира лабораториите за един ход, изоставените кораби дават две фрегати. Ето всичките (регион events от канона). Обърни внимание на края на rollEvent: резултатът не се пише на екрана, а се публикува. Какво значи това, е в картата за шината.
// Случайните събития: на всеки ход с 40% шанс се случва едно на случайна империя. Видът е по// тегла (само от най-ранния му ход нататък). Всичко се публикува в шината; новините, статистиката// и озлобването са абонати, а събитията не знаят за тях.const EVENT_CHANCE = 40;
function pickEmpirePlanet(world, empire) { const list = ownedPlanets(world, empire.id); if (list.length === 0) return null; return list[world.rng.nextInt(list.length)];}
function pirateFleet(world) { const strength = Math.floor(world.turn / 6); return [2 + strength, Math.floor(strength / 2), Math.floor(strength / 3), 0, 0];}
function applyEvent(world, kind, empire) { const rng = world.rng; const vars = { empire: empireName(empire) }; if (kind === 'meteor') { const planet = pickEmpirePlanet(world, empire); if (planet === null) return null; const loss = minOf(empire.res[0], 8 + rng.nextInt(8)); empire.res[0] -= loss; vars.planet = planetName(world, planet); vars.n = loss; } else if (kind === 'vein') { const planet = pickEmpirePlanet(world, empire); if (planet === null) return null; let r = rng.nextInt(RES_COUNT); let tries = 0; while (planet.rich[r] >= 5 && tries < RES_COUNT) { r = (r + 1) % RES_COUNT; tries++; } if (planet.rich[r] >= 5) return null; planet.rich[r]++; vars.planet = planetName(world, planet); vars.res = t('res.' + r); } else if (kind === 'pirates') { const planet = pickEmpirePlanet(world, empire); if (planet === null) return null; const systemId = planet.systemId; const mine = emptyCounts(); const fleet = fleetAt(world, empire.id, systemId); for (let k = 0; k < SHIP_KIND_COUNT; k++) mine[k] = fleet === null ? 0 : fleet.ships[k]; mine[FORT_KIND] = fortsIn(world, empire.id, systemId); const result = runBattle(sideStats(empire), mine, sideStats(null), pirateFleet(world)); for (let k = 0; k < SHIP_KIND_COUNT; k++) { if (fleet !== null) fleet.ships[k] = result.a[k]; } vars.system = world.systems[systemId].name; vars.fleet = countsText(pirateFleet(world)); if (result.winner === 'a') { empire.res[0] += 20; empire.res[1] += 10; vars.outcome = t('event.pirates.win'); } else { const m = Math.floor(empire.res[0] / 5); const g = Math.floor(empire.res[1] / 5); empire.res[0] -= m; empire.res[1] -= g; vars.outcome = t('event.pirates.lose', { m: m, g: g }); } if (!world.quiet) world.last.battle = battleLines(vars.system, empireName(empire), t('pirates'), result); world.stats.battles++; world.stats.lost += totalOf(mine) - totalOf(result.a) + totalOf(pirateFleet(world)) - totalOf(result.b); } else if (kind === 'breakthrough') { const n = 6 + rng.nextInt(7); empire.progress += n; vars.n = n; } else if (kind === 'unrest') { const planet = pickEmpirePlanet(world, empire); if (planet === null || planet.mines === 0) return null; planet.mines--; vars.planet = planetName(world, planet); } else if (kind === 'boom') { empire.res[1] += 20; empire.res[2] += 10; } else if (kind === 'flare') { empire.flare = 1; } else if (kind === 'derelict') { const planet = pickEmpirePlanet(world, empire); if (planet === null) return null; addShips(world, empire.id, planet.systemId, [0, 0, 2, 0]); vars.system = world.systems[planet.systemId].name; } return vars;}
function rollEvent(world) { const rng = world.rng; if (!rng.chance(EVENT_CHANCE)) return; const living = []; for (const e of world.empires) { if (e.alive) living.push(e); } if (living.length === 0) return; const empire = living[rng.nextInt(living.length)]; const allowed = []; const weights = []; for (const def of EVENT_DEFS) { if (world.turn >= def.minTurn) { allowed.push(def); weights.push(def.weight); } } const def = allowed[pickWeighted(rng, weights)]; const vars = applyEvent(world, def.id, empire); if (vars === null) return; world.stats.events++; world.bus.publish({ type: 'event', kind: def.id, empire: empire.id, vars: vars });}
// Абонатите: новините за играча, озлобването след битки и завладяване.function addNews(world, text) { world.news.push(world.turn + ': ' + text); while (world.news.length > 60) world.news.shift(); world.turnLines.push(text);}
function installSubscribers(world, quiet) { const bus = world.bus; const involvesHuman = (a, b) => world.empires[a].human || world.empires[b].human; bus.subscribe('battle', (e) => { if (e.lostA > 0) world.empires[e.b].grudge[e.a]++; if (e.lostB > 0) world.empires[e.a].grudge[e.b]++; }); bus.subscribe('conquest', (e) => { if (e.from >= 0) world.empires[e.from].grudge[e.empire] += 3; }); // Сигнал към играча: нещо се случи, при което „wait“ спира. bus.subscribe('battle', (e) => { if (involvesHuman(e.a, e.b)) world.alert++; }); bus.subscribe('conquest', (e) => { if (world.empires[e.empire].human || (e.from >= 0 && world.empires[e.from].human)) world.alert++; }); bus.subscribe('war', (e) => { if (involvesHuman(e.a, e.b)) world.alert++; }); bus.subscribe('siege', (e) => { if (world.empires[e.from].human) world.alert++; }); bus.subscribe('offer', (e) => { if (world.empires[e.to].human) world.alert++; }); if (quiet) return; bus.subscribe('battle', (e) => { if (involvesHuman(e.a, e.b)) { for (const line of e.lines) addNews(world, line); } else { addNews(world, t('news.battle', { system: world.systems[e.systemId].name, a: empireName(world.empires[e.a]), b: empireName(world.empires[e.b]), la: e.lostA, lb: e.lostB, })); } }); bus.subscribe('colony', (e) => { addNews(world, t('news.colony', { empire: empireName(world.empires[e.empire]), planet: planetName(world, world.planets[e.planetId]) })); }); bus.subscribe('tech', (e) => { if (world.empires[e.empire].human) addNews(world, t('news.tech', { tech: nm(TECHS.get(e.techId)) })); }); bus.subscribe('war', (e) => { addNews(world, t('news.war', { a: empireName(world.empires[e.a]), b: empireName(world.empires[e.b]) })); }); bus.subscribe('peace', (e) => { addNews(world, t('news.peace', { a: empireName(world.empires[e.a]), b: empireName(world.empires[e.b]) })); }); bus.subscribe('offer', (e) => { addNews(world, t('news.offer', { empire: empireName(world.empires[e.from]), letter: world.empires[e.from].def.letter })); }); bus.subscribe('siege', (e) => { addNews(world, t('news.siege', { empire: empireName(world.empires[e.empire]), planet: planetName(world, world.planets[e.planetId]) })); }); bus.subscribe('conquest', (e) => { addNews(world, t('news.conquest', { empire: empireName(world.empires[e.empire]), planet: planetName(world, world.planets[e.planetId]) })); }); bus.subscribe('eliminated', (e) => { addNews(world, t('news.eliminated', { empire: empireName(world.empires[e.empire]) })); }); bus.subscribe('event', (e) => { const empire = world.empires[e.empire]; addNews(world, t('event.' + e.kind, e.vars)); });}Идеята
Мир и война: правило срещу бъркотията
Дипломацията в играта е два обекта: двойка империи е във война или в мир (war[a][b]). Кое е трудното? Без правило империя би обявявала война на един ход, сключвала мир на следващия и пак война след него. Затова:
- мирът важи най-малко MIN_PEACE =
4хода преди нова война (turn − since < MIN_PEACE; в началото на играта правилото не важи, за да може някой да започне); - предложение за мир е позволено най-рано два хода след промяната;
- изкуственият интелект приема мир по желание: колко е по-слаб от другия, какъв е характерът му, колко други войни води и колко е озлобен. От 90 нагоре приема.
В програмата желанието е опростено: характер + (150 − съотношение на корабите) / 2 + други войни × 15 − озлобление × 3, където характерът е 40 за Орион (воин) и 120 за двете други (строители). Отказът увеличава озлоблението, затова следващият опит е по-труден. Сценарият е от историята на сцената: Орион (B) обявява война на Земната империя (A) на ход 1, Лира (C) на Орион на ход 3, а A предлага мир:
class World { constructor() { this.turn = 1; this.ships = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; this.war = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; this.since = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; this.grudge = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; }}
function atWar(world, a, b) { return world.war[a][b] === 1;}
function power(world, empire) { let total = 0; for (const n of world.ships[empire]) total += n; return total;}
function setStance(world, a, b, war) { world.war[a][b] = war; world.war[b][a] = war; world.since[a][b] = world.turn; world.since[b][a] = world.turn;}
function declareWar(world, a, b) { if (atWar(world, a, b)) return "alreadyWar"; // Мирът важи най-малко MIN_PEACE хода. if (world.turn - world.since[a][b] < MIN_PEACE && world.turn > MIN_PEACE) return "tooSoon"; setStance(world, a, b, 1); return null;}
function makePeace(world, a, b) { setStance(world, a, b, 0); world.grudge[a][b] = 0; world.grudge[b][a] = 0;}
function peaceWish(world, me, other) { // Колко по-слаба е империята от другата. const ratio = Math.min(150, Math.floor((power(world, me) * 100) / (power(world, other) + 1))); let otherWars = 0; for (let x = 0; x < 3; x++) { if (x !== me && x !== other && atWar(world, me, x)) otherWars++; } return Math.max(0, PEACE[me] + Math.floor((150 - ratio) / 2) + otherWars * 15 - world.grudge[me][other] * 3);}
function proposePeace(world, a, b) { if (!atWar(world, a, b)) return "noWar"; if (world.turn - world.since[a][b] < 2) return "tooSoon"; // От 90 нагоре империята приема мир. if (peaceWish(world, b, a) >= 90) { makePeace(world, a, b); return null; } // Отказът озлобява: следващият мир е по-труден. world.grudge[b][a]++; return "refused";}function step(world, turn, kind, a, b) { world.turn = turn; const wish = kind === "peace" ? peaceWish(world, b, a) : 0; const error = kind === "war" ? declareWar(world, a, b) : proposePeace(world, a, b); const shown = kind === "peace" && (error === null || error === "refused") ? " (желание " + wish + ")" : ""; console.log("ход " + turn + ": " + kind + " " + NAMES[a] + " → " + NAMES[b] + ": " + (error ?? "ok") + shown);}
const world = new World();world.ships[1][1] = 1;world.grudge[1][0] = 1;step(world, 1, "war", 1, 0);step(world, 2, "war", 1, 0);step(world, 2, "peace", 0, 1);step(world, 3, "war", 2, 1);step(world, 3, "peace", 0, 1);world.ships[1][1] = 0;step(world, 4, "peace", 0, 1);step(world, 5, "war", 1, 0);step(world, 9, "war", 1, 0);ход 1: war Хегемония Орион → Земна империя: okход 2: war Хегемония Орион → Земна империя: alreadyWarход 2: peace Земна империя → Хегемония Орион: tooSoonход 3: war Съюз Лира → Хегемония Орион: okход 3: peace Земна империя → Хегемония Орион: refused (желание 77)ход 4: peace Земна империя → Хегемония Орион: ok (желание 124)ход 5: war Хегемония Орион → Земна империя: tooSoonход 9: war Хегемония Орион → Земна империя: okХод 2: Орион пак обявява война на същия противник (alreadyWar), а A предлага мир едва един ход след войната (tooSoon). На ход 3 A предлага мир, но желанието на Орион е 40 + 25 + 15 − 3 = 77 (характер, един кораб срещу нула, още една война с Лира, озлобление 1) и е под 90: отказ, а озлоблението става 2. На ход 4 Орион няма кораби и желанието му е 40 + 75 + 15 − 6 = 124: приема. Ход 5: Орион иска нова война, но мирът е от ход 4 и 5 − 4 = 1 е по-малко от 4: tooSoon. На ход 9 вече е позволено (9 − 4 = 5).
function atWar(world, a, b) {
return world.war[a][b] === 1;
}
function power(world, empire) {
let total = 0;
for (const n of world.ships[empire]) total += n;
return total;
}
function setStance(world, a, b, war) {
world.war[a][b] = war;
world.war[b][a] = war;
world.since[a][b] = world.turn;
world.since[b][a] = world.turn;
}
function declareWar(world, a, b) {
if (atWar(world, a, b)) return "alreadyWar";
// Мирът важи най-малко MIN_PEACE хода.
if (world.turn - world.since[a][b] < MIN_PEACE && world.turn > MIN_PEACE) return "tooSoon";
setStance(world, a, b, 1);
return null;
}
function makePeace(world, a, b) {
setStance(world, a, b, 0);
world.grudge[a][b] = 0;
world.grudge[b][a] = 0;
}
function peaceWish(world, me, other) {
// Колко по-слаба е империята от другата.
const ratio = Math.min(150, Math.floor((power(world, me) * 100) / (power(world, other) + 1)));
let otherWars = 0;
for (let x = 0; x < 3; x++) {
if (x !== me && x !== other && atWar(world, me, x)) otherWars++;
}
return Math.max(0, PEACE[me] + Math.floor((150 - ratio) / 2) + otherWars * 15 - world.grudge[me][other] * 3);
}
function proposePeace(world, a, b) {
if (!atWar(world, a, b)) return "noWar";
if (world.turn - world.since[a][b] < 2) return "tooSoon";
// От 90 нагоре империята приема мир.
if (peaceWish(world, b, a) 90) {
makePeace(world, a, b);
return null;
}
// Отказът озлобява: следващият мир е по-труден.
world.grudge[b][a]++;
return "refused";
}Прагът е „от 90 нагоре“, т.е. 90 е достатъчно: >=. С > желание точно 90 би било отказ, а правилото в играта казва обратното.
По-точно, за любопитните
Резултатът от функциите е низ или нищо: null, ако промяната е станала, и ключ на грешка (alreadyWar, tooSoon, refused, noWar), ако не. Грешките са ключове, а не готови изречения, за да ги превежда екранът (играта е на четири езика). Така същата функция служи и на играча (команда peace B), и на изкуствения интелект.
Канонът има и предложения към играча: изкуственият интелект не може да сключи мир с човека сам, затова записва предложение (offers), което важи три хода, и публикува събитие „offer“. Ето целия регион diplomacy:
// Дипломацията е мир или война между две империи. Мирът важи най-малко MIN_PEACE хода преди// нова война. Изкуственият интелект приема мир по мерило: колко е по-слаб, какъв е характерът// му и колко е озлобен. Предложение към играча се записва и важи три хода.function atWar(world, a, b) { return world.empires[a].war[b] === 1;}
function setStance(world, a, b, war) { world.empires[a].war[b] = war; world.empires[b].war[a] = war; world.empires[a].since[b] = world.turn; world.empires[b].since[a] = world.turn;}
function declareWar(world, a, b) { if (a === b) return 'dip.self'; if (!world.empires[b].alive) return 'dip.dead'; if (atWar(world, a, b)) return 'dip.alreadyWar'; if (world.turn - world.empires[a].since[b] < MIN_PEACE && world.turn > MIN_PEACE) return 'dip.tooSoon'; setStance(world, a, b, 1); world.bus.publish({ type: 'war', a: a, b: b }); return null;}
function makePeace(world, a, b) { setStance(world, a, b, 0); world.empires[a].grudge[b] = 0; world.empires[b].grudge[a] = 0; world.bus.publish({ type: 'peace', a: a, b: b });}
// Най-малкото разстояние в пътища между системи с планети на двете империи.function empireDistance(world, a, b) { let best = 99; for (const pa of world.planets) { if (pa.owner !== a) continue; const dist = bfs(world.systems, pa.systemId).dist; for (const pb of world.planets) { if (pb.owner === b && dist[pb.systemId] >= 0 && dist[pb.systemId] < best) best = dist[pb.systemId]; } } return best;}
function powerRatio(world, a, b, top) { return clamp(percent(empirePower(world, a) * 100, empirePower(world, b) * 100 + 100), 0, top);}
// Колко иска империята мир с другата (0 и нагоре; от 90 нагоре приема).function peaceWish(world, me, other) { const e = world.empires[me]; const weaker = 150 - powerRatio(world, me, other, 150); let otherWars = 0; for (const x of world.empires) { if (x.alive && x.id !== me && x.id !== other && e.war[x.id] === 1) otherWars++; } const wish = e.personality.peace + Math.floor(weaker / 2) + otherWars * 15 - e.grudge[other] * 3; return maxOf(0, wish);}
// Колко иска империята война с другата.function warWish(world, me, other) { const e = world.empires[me]; const ratio = powerRatio(world, me, other, 300); let wish = Math.floor(e.personality.attack / 2); if (ratio > 100) wish += Math.floor((ratio - 100) / 4); else wish -= Math.floor((100 - ratio) / 2); if (empireDistance(world, me, other) <= 3) wish += 25; for (const x of world.empires) { if (x.alive && x.id !== me && e.war[x.id] === 1) wish -= 30; } return wish;}
function hasOffer(world, from, to) { for (const offer of world.offers) { if (offer.from === from && offer.to === to && world.turn - offer.turn <= 3) return true; } return false;}
// Предложение за мир от a към b. Връща ключ на грешка или null (мирът е сключен).function proposePeace(world, a, b) { if (a === b) return 'dip.self'; if (!world.empires[b].alive) return 'dip.dead'; if (!atWar(world, a, b)) return 'dip.noWar'; if (world.turn - world.empires[a].since[b] < 2) return 'dip.tooSoon'; if (world.empires[b].human) { if (hasOffer(world, b, a)) { makePeace(world, a, b); return null; } if (!hasOffer(world, a, b)) world.offers.push({ from: a, to: b, turn: world.turn }); return 'dip.sent'; } if (hasOffer(world, b, a) || peaceWish(world, b, a) >= 90) { makePeace(world, a, b); return null; } world.empires[b].grudge[a]++; return 'dip.refused';}
// Дипломацията на изкуствения интелект в края на хода: мир, когато е по-слаб, война, когато е по-силен.function aiDiplomacy(world, empire) { for (const other of world.empires) { if (!other.alive || other.id === empire.id) continue; if (atWar(world, empire.id, other.id)) { if (world.turn - empire.since[other.id] >= 3 && peaceWish(world, empire.id, other.id) >= 90) { if (other.human) { if (!hasOffer(world, empire.id, other.id)) { world.offers.push({ from: empire.id, to: other.id, turn: world.turn }); world.bus.publish({ type: 'offer', from: empire.id, to: other.id }); } } else { proposePeace(world, empire.id, other.id); } } } else if (world.turn - empire.since[other.id] >= MIN_PEACE + 2 && world.turn > MIN_PEACE + 2) { if (warWish(world, empire.id, other.id) >= 95 && world.rng.chance(40)) declareWar(world, empire.id, other.id); } }}Идеята
Шина от събития: който публикува, не знае кой слуша
Когато се обяви война, трябва да стане много: да се напише новина, да порасне озлобването на двете империи, да светне сигнал на играча, да се преброи в статистиката. Най-простото е declareWar да вика всичко това. Тогава declareWar знае за новините, за озлобването, за сигнала и за статистиката, а всяка нова възможност е още един ред в нея и още една причина да се счупи. Шината обръща посоката: declareWar казва „обявена е война между A и B“ и свършва. Който се интересува, се е абонирал за „war“.
function event(type, a, b, place = 0, n = 0) { return { type: type, a: a, b: b, place: place, n: n };}
class EventBus { constructor() { this.handlers = new Map(); }
subscribe(type, handler) { if (!this.handlers.has(type)) this.handlers.set(type, []); this.handlers.get(type).push(handler); }
publish(e) { if (!this.handlers.has(e.type)) return; for (const handler of this.handlers.get(e.type).slice()) handler(e); }}subscribe записва обработчик за вид събитие, publish вика всички обработчици на този вид. Вид без абонати не е грешка: събитието отива в празното. Ето публикуването в двете места, където става (обява на война и сключване на мир). Единственото ново е редът с publish:
function declareWar(world, a, b) { if (atWar(world, a, b)) return "alreadyWar"; // Мирът важи най-малко MIN_PEACE хода. if (world.turn - world.since[a][b] < MIN_PEACE && world.turn > MIN_PEACE) return "tooSoon"; setStance(world, a, b, 1); world.bus.publish(event("war", a, b)); return null;}
function makePeace(world, a, b) { setStance(world, a, b, 0); world.grudge[a][b] = 0; world.grudge[b][a] = 0; world.bus.publish(event("peace", a, b));}А ето програмата. Новините слушат всички видове, озлоблението само битките, а report публикува и неуспешните опити (refused, tooSoon) като събития:
const world = new World();world.ships[1][1] = 1;world.grudge[1][0] = 1;// Новините слушат всички видове събития, озлобването само битките.for (const type of NEWS_TYPES) { world.bus.subscribe(type, (e) => console.log(newsText(e)));}world.bus.subscribe("battle", (e) => { world.grudge[e.a][e.b]++; world.grudge[e.b][e.a]++;});
world.turn = 1;report(world, declareWar(world, 1, 0), 1, 0);world.bus.publish(event("battle", 1, 0, 1, 2));world.turn = 3;report(world, proposePeace(world, 0, 1), 0, 1);report(world, declareWar(world, 1, 0), 1, 0);console.log("озлобление " + NAMES[0] + " → " + NAMES[1] + ": " + world.grudge[0][1]);console.log("озлобление " + NAMES[1] + " → " + NAMES[0] + ": " + world.grudge[1][0]);Хегемония Орион обявява война на Земна империя.Битка при Vesta: Хегемония Орион и Земна империя губят кораби (по 2).Хегемония Орион отказва мир с Земна империя.Хегемония Орион вече е във война с Земна империя.озлобление Земна империя → Хегемония Орион: 1озлобление Хегемония Орион → Земна империя: 3Опитът на ход 1 е успешен, битката е публикувана на ръка, отказът на ход 3 е събитие (refused), а повторната война е alreadyWar. Озлоблението на A към B е 1 (от битката), а на B към A е 3: едно старо, едно от битката и едно от отказа (отказът вдига озлоблението на този, който отказва, към предложилия).
(e) => ...); EventBus не знае нищо за нея освен че може да я вика.По-точно, за любопитните
Без шина всяко ново нещо, което трябва да научи за война, иска промяна в declareWar. С шината нов абонат е само нов ред при създаването на света, а функцията за война не се пипа. Това е шаблонът наблюдател (observer) от модула за шаблони за дизайн.
Малка, но важна подробност: publish върти по копие на списъка с абонати. Обработчик може да се абонира за друго събитие, докато го викат; ако списъкът се променя по време на обхождането, абонатите се прескачат или се повтарят. Копието прави правилото ясно: новият абонат чува от следващото публикуване.
Редът на абонатите е редът на subscribe. Тук няма значение (новините и озлоблението не си пречат), но ако два абоната зависят един от друг, редът ще решава изхода, и това е причина да ги държиш независими.
Идеята
Целият ход
Сега всичко заедно. Светът е World: ход, фаза, генератор, шина, флотите на трите империи (по системи), метал и таблиците за война (war), от кой ход (since) и озлобление (grudge):
class World { constructor(seed) { this.turn = 1; this.phase = ""; this.rng = new Rng(seed); this.bus = new EventBus(); this.ships = [[3, 0, 0, 0], [0, 0, 0, 4], [0, 2, 0, 0]]; this.metal = [50, 50, 50]; this.war = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; this.since = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; this.grudge = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; }}Поръчките и дипломацията на хода са сценарий (в играта ги решава изкуственият интелект или играчът): поръчката е [ход, империя, от, към, кораби], а дипломацията [ход, вид, от, към].
// [ход, империя, от, към, кораби]const ORDERS = [[1, 1, 3, 2, 4], [1, 0, 0, 1, 3], [2, 1, 2, 1, 4]];// [ход, вид (0 война, 1 мир), от, към]const DIPLO = [[1, 0, 1, 0], [3, 0, 2, 1], [3, 1, 0, 1], [4, 1, 0, 1], [5, 0, 1, 0]];Фазите са функции. Дипломацията изпълнява сценария за хода и публикува грешките. Движението е точно двете стъпки от първата карта. Битките обхождат системите: където две империи във война имат кораби, всяка губи толкова, колкото е по-малката флота, и се публикува „battle“ (озлоблението се абонира за него):
function scriptedDiplomacy(world) { world.phase = "дипломация"; for (const [turn, kind, a, b] of DIPLO) { if (turn !== world.turn) continue; const error = kind === 0 ? declareWar(world, a, b) : proposePeace(world, a, b); report(world, error, a, b); }}function resolveMovement(world) { world.phase = "движение"; const moving = []; // Първо всички флоти напускат системите си... for (const [turn, empire, source, target, count] of ORDERS) { if (turn !== world.turn) continue; const take = Math.min(count, world.ships[empire][source]); world.ships[empire][source] -= take; if (take > 0) moving.push({ empire: empire, source: source, target: target, take: take }); } // ...и чак тогава пристигат. for (const move of moving) { world.ships[move.empire][move.target] += move.take; world.bus.publish(event("move", move.empire, move.source, move.target, move.take)); }}function resolveBattles(world) { world.phase = "битки"; for (let system = 0; system < 4; system++) { for (let a = 0; a < 3; a++) { for (let b = a + 1; b < 3; b++) { if (!atWar(world, a, b) || world.ships[a][system] === 0 || world.ships[b][system] === 0) continue; const lost = Math.min(world.ships[a][system], world.ships[b][system]); world.ships[a][system] -= lost; world.ships[b][system] -= lost; world.bus.publish(event("battle", a, b, system, lost)); } } }}
function produce(world) { world.phase = "производство"; for (let empire = 0; empire < 3; empire++) world.metal[empire] += 10;}След тях е събитието: теглене по теглата от втората карта, с тези ефекти: метеор отнема метал, бум дава метал, изоставените кораби дават два кораба в столицата. Накрая runTurn ги вика в ред:
function rollEvent(world) { world.phase = "събитие"; if (!world.rng.chance(EVENT_CHANCE)) return; const empire = world.rng.nextInt(3); const weights = EVENT_DEFS.map((def) => (world.turn >= def.minTurn ? def.weight : 0)); const kind = EVENT_DEFS[pickWeighted(world.rng, weights)].id; if (kind === "meteor") { const loss = Math.min(world.metal[empire], 8 + world.rng.nextInt(8)); world.metal[empire] -= loss; world.bus.publish(event("meteor", empire, 0, 0, loss)); } else if (kind === "boom") { world.metal[empire] += 20; world.bus.publish(event("boom", empire, 0, 0, 20)); } else { world.ships[empire][HOME[empire]] += 2; world.bus.publish(event("derelict", empire, 0, HOME[empire], 2)); }}function runTurn(world) { // Фазите вървят в този ред и редът е важен. scriptedDiplomacy(world); resolveMovement(world); resolveBattles(world); produce(world); rollEvent(world); world.turn++;}Програмата абонира новините (те слагат пред всеки ред фазата, в която е станало събитието) и озлоблението, пуска пет хода и печата резултата. Редовете са точно новините на играта:
const world = new World(7);world.bus.subscribe("battle", (e) => { world.grudge[e.a][e.b]++; world.grudge[e.b][e.a]++;});for (const type of NEWS_TYPES) { world.bus.subscribe(type, (e) => console.log(" [" + world.phase + "] " + newsText(e)));}
for (let i = 0; i < 5; i++) { console.log(fill("-- ход {0} --", world.turn)); runTurn(world);}console.log("-- край --");for (let empire = 0; empire < 3; empire++) { console.log(fill("{0}: кораби {1}, метал {2}", NAMES[empire], power(world, empire), world.metal[empire]));}-- ход 1 -- [дипломация] Хегемония Орион обявява война на Земна империя. [движение] Хегемония Орион праща флот от Brisa към Altan (кораби: 4). [движение] Земна империя праща флот от Kora към Vesta (кораби: 3). [събитие] Метеоритен дъжд: Съюз Лира губи 14 метал.-- ход 2 -- [движение] Хегемония Орион праща флот от Altan към Vesta (кораби: 4). [битки] Битка при Vesta: Земна империя и Хегемония Орион губят кораби (по 3).-- ход 3 -- [дипломация] Съюз Лира обявява война на Хегемония Орион. [дипломация] Хегемония Орион отказва мир с Земна империя. [битки] Битка при Vesta: Хегемония Орион и Съюз Лира губят кораби (по 1).-- ход 4 -- [дипломация] Мир между Земна империя и Хегемония Орион. [събитие] Метеоритен дъжд: Съюз Лира губи 8 метал.-- ход 5 -- [дипломация] Хегемония Орион още не може да смени отношенията си с Земна империя: прекалено скоро. [събитие] Хегемония Орион намира изоставени кораби при Brisa (кораби: 2).-- край --Земна империя: кораби 0, метал 100Хегемония Орион: кораби 2, метал 100Съюз Лира: кораби 1, метал 78Прочети изхода като история. Ход 1: Орион обявява война, двата флота (на Орион и на Земната империя) тръгват едновременно, а Лира губи метал от метеор. Ход 2: флотът на Орион пристига във Vesta, където вече е Земната империя, и двамата губят по 3 кораба. Ход 3: Лира се включва във войната и в боя във Vesta губят по 1; преди това A предлага мир и получава отказ. Ход 4: Орион няма кораби, желанието му е 124 и мирът е сключен. Ход 5: нова война е „прекалено скоро“, а Орион намира два изоставени кораба в столицата си.
Виж го
Един ход на галактиката
Ето същото по ходове. Горе светва фазата, в която е станало нещо. По средата са системите и флотите на трите империи: при движението всички флотове излитат наведнъж и чак тогава кацат. Под тях се вижда дали всяка двойка е във война, а най-долу шината: публикуваното събитие светва абоната „новини“, а ако е битка, и „озлобление“.
function runTurn(world) { // Фазите вървят в този ред и редът е важен. scriptedDiplomacy(world); resolveMovement(world); resolveBattles(world); produce(world); rollEvent(world); world.turn++;}
class EventBus { constructor() { this.handlers = new Map(); }
subscribe(type, handler) { if (!this.handlers.has(type)) this.handlers.set(type, []); this.handlers.get(type).push(handler); }
publish(e) { if (!this.handlers.has(e.type)) return; for (const handler of this.handlers.get(e.type).slice()) handler(e); }}Стъпка по стъпка, с думи
- A = Земна империя, B = Хегемония Орион, C = Съюз Лира. Четири системи по права линия; ходът минава през фазите отгоре надолу.
- ход 1 · дипломация: Хегемония Орион обявява война на Земна империя.
- ход 1 · движение: Хегемония Орион праща флот от Brisa към Altan (кораби: 4). Земна империя праща флот от Kora към Vesta (кораби: 3).
- ход 1 · събитие: Метеоритен дъжд: Съюз Лира губи 14 метал.
- ход 2 · движение: Хегемония Орион праща флот от Altan към Vesta (кораби: 4).
- ход 2 · битки: Битка при Vesta: Земна империя и Хегемония Орион губят кораби (по 3).
- ход 3 · дипломация: Съюз Лира обявява война на Хегемония Орион. Хегемония Орион отказва мир с Земна империя.
- ход 3 · битки: Битка при Vesta: Хегемония Орион и Съюз Лира губят кораби (по 1).
- ход 4 · дипломация: Мир между Земна империя и Хегемония Орион.
- ход 4 · събитие: Метеоритен дъжд: Съюз Лира губи 8 метал.
- ход 5 · дипломация: Хегемония Орион още не може да смени отношенията си с Земна империя: прекалено скоро.
- ход 5 · събитие: Хегемония Орион намира изоставени кораби при Brisa (кораби: 2).
- След 5 хода: Земна империя: кораби 0, метал 100 · Хегемония Орион: кораби 2, метал 100 · Съюз Лира: кораби 1, метал 78
Кодът до сцената е runTurn и EventBus от програмата. Светят редовете, които работят в момента.
Идеята
В играта
Това, което построихме, е същото като в канона, само по-малко. Ето истинския ход (регион turn). Над runTurn са движението, което прави същите две стъпки (напускане, пристигане), битките и завладяването на планети; runTurn вика фазите в твърд ред, а в края на хода озлоблението остарява (на всеки пети ход намалява с 1) и се изхвърлят остарелите предложения за мир.
// Един ход на галактиката има фази: дипломация на изкуствения интелект, поръчките му, движение на// всички флотове наведнъж, битки, завладяване, производство и наука, събитие, край на хода.// Движението е едновременно: първо всички кораби напускат системите си, чак тогава пристигат.function resolveMovement(world) { const moving = []; for (const empire of world.empires) { if (!empire.alive) continue; for (const order of empire.orders) { const fleet = fleetAt(world, empire.id, order.from); if (fleet === null) continue; const ships = [0, 0, 0, 0]; for (let k = 0; k < SHIP_KIND_COUNT; k++) { ships[k] = minOf(order.ships[k], fleet.ships[k]); fleet.ships[k] -= ships[k]; } if (totalOf(ships) > 0) moving.push({ owner: empire.id, to: order.to, dest: order.dest, ships: ships }); } empire.orders = []; } for (const move of moving) { addShips(world, move.owner, move.to, move.ships); // дългият път продължава сам: нова поръчка от мястото, където стигнаха if (move.to !== move.dest) { world.empires[move.owner].orders.push({ from: move.to, to: nextStep(world.systems, move.to, move.dest), dest: move.dest, ships: move.ships.slice() }); } } removeEmptyFleets(world);}
function hasUnits(world, owner, systemId) { const fleet = fleetAt(world, owner, systemId); return (fleet !== null && totalOf(fleet.ships) > 0) || fortsNow(world, owner, systemId) > 0;}
function resolveBattles(world) { for (const system of world.systems) { for (let a = 0; a < world.empires.length; a++) { for (let b = a + 1; b < world.empires.length; b++) { if (!world.empires[a].alive || !world.empires[b].alive || !atWar(world, a, b)) continue; if (hasUnits(world, a, system.id) && hasUnits(world, b, system.id)) fightAt(world, system.id, a, b); } } } removeEmptyFleets(world);}
// Планета пада, когато враг е във война с владетеля ѝ, има кораби в системата, а владетелят няма// нито кораби, нито оцелели крепости там, и така SIEGE_TURNS хода подред: първите ходове са обсада,// последният е превземане. Защитникът има време да доведе флот. Постройките се разрушават.function resolveConquest(world) { for (const system of world.systems) { for (const pid of system.planets) { const planet = world.planets[pid]; if (planet.owner < 0) continue; const owner = planet.owner; let attacker = -1; if (!hasUnits(world, owner, system.id)) { for (const e of world.empires) { if (!e.alive || e.id === owner || !atWar(world, e.id, owner)) continue; const fleet = fleetAt(world, e.id, system.id); if (fleet !== null && totalOf(fleet.ships) > 0) { attacker = e.id; break; } } } if (attacker < 0) { planet.siege = 0; continue; } planet.siege++; if (planet.siege < SIEGE_TURNS) { world.bus.publish({ type: 'siege', empire: attacker, from: owner, planetId: planet.id }); continue; } planet.siege = 0; planet.owner = attacker; planet.mines = maxOf(0, planet.mines - 1); planet.lab = 0; planet.yard = 0; planet.fort = 0; world.bus.publish({ type: 'conquest', empire: attacker, from: owner, planetId: planet.id }); } } for (const empire of world.empires) { if (!empire.alive || ownedPlanets(world, empire.id).length > 0) continue; empire.alive = false; const kept = []; for (const fleet of world.fleets) { if (fleet.owner !== empire.id) kept.push(fleet); } world.fleets = kept; empire.orders = []; world.bus.publish({ type: 'eliminated', empire: empire.id }); }}
function runTurn(world) { world.turnLines = []; world.fortLeft = new Map(); for (const empire of world.empires) { if (empire.alive && !empire.human) aiDiplomacy(world, empire); } for (const empire of world.empires) { if (empire.alive && !empire.human) aiPlay(world, empire); } resolveMovement(world); resolveBattles(world); resolveConquest(world); for (const empire of world.empires) { if (!empire.alive) continue; produce(world, empire); completeResearch(world, empire); } finishQueue(world); rollEvent(world); checkVictory(world); world.turn++; if (world.turn % 5 === 0) { for (const empire of world.empires) { for (let i = 0; i < empire.grudge.length; i++) empire.grudge[i] = maxOf(0, empire.grudge[i] - 1); } } const fresh = []; for (const offer of world.offers) { if (world.turn - offer.turn <= 3) fresh.push(offer); } world.offers = fresh;}А ето шината, която слушат новините, озлоблението и сигналът към играча. Събитията са един клас с повече полета (в JavaScript — обект), а шината е същата:
// Шината от събития: който публикува, не знае кой слуша. Новините, злопаметността на империите и// статистиката се абонират сами, а битката само казва „флот се би при тази система“.class EventBus { constructor() { this.handlers = new Map(); }
subscribe(type, handler) { if (!this.handlers.has(type)) this.handlers.set(type, []); this.handlers.get(type).push(handler); }
publish(event) { if (!this.handlers.has(event.type)) return; for (const handler of this.handlers.get(event.type).slice()) handler(event); }}Разликите от нашата програма са много, и затова са важни:
- Няма сценарий. Поръчките и дипломацията идват от
aiPlayиaiDiplomacy(урокът за съперниците) и от командите на играча. - Галактиката е граф (урокът за генерирането ѝ), а не права линия. Поръчката помни крайната цел и продължава сама.
- Битката (
fightAt, урокът за флотите) е с роли на корабите и по шест рунда, а не „всеки губи по по-малкото“. Има и крепости, и завладяване на планета (две обсади). - Желанието за мир ползва силата на империята (не броя кораби), характера ѝ и нейното желание за война (
warWish). Предложение към играча се записва за три хода. - Производството тук е плоски 10 метал на империя; в канона зависи от планетите, мините и технологиите.
- Събитията са осем вида, с по-различен ефект (пиратите се бият със същия модел на битка).
- Шината носи и други видове: завладяване, обсада, откриване на технология, колония, унищожена империя. Новините и сигналът към играча ги слушат, а
waitспира, щом сигналът порасне.
Пробвай
Предвиди изхода
Два флота: първата поръчка праща 3-те кораба от системата 0 към 1, втората — 2 кораба от системата 1 към 2. Поръчките се обработват едновременно. Какво печата програмата?
function moveTogether(start, orders) { const ships = start.slice(); const takes = []; // Първо всички флоти напускат системите си... for (const [source, target, count] of orders) { const take = Math.min(count, ships[source]); ships[source] -= take; takes.push(take); } // ...и чак тогава пристигат. orders.forEach(([, target], i) => { ships[target] += takes[i]; }); return ships;}
function show(ships) { return ships.join(" ");}
const orders = [[0, 1, 3], [1, 2, 2]];console.log(show(moveTogether([3, 0, 0, 0], orders)));Първо всички тръгват: първата поръчка взима 3 кораба от системата 0, а втората вижда в системата 1 нула кораби (първите още не са пристигнали) и взима 0. После пристигат: 3 кораба в системата 1, 0 в системата 2. Втората възможност е резултатът при поръчки една след друга: 2 от пристигналите кораби тръгват пак същия ход; третата е началното състояние, без движение.
Пробвай
Намери грешката
Колега „оправи“ едновременното движение, но при него корабите пак минават по няколко системи на ход:
const start = [3, 0, 0, 0];console.log("едновременно: " + show(moveTogether(start, ORDERS)));едновременно: 0 0 0 3Резултатът е като при последователното движение. Кой ред праща корабите на местоназначението твърде рано?
Избери реда с грешката.
Корабите пристигат (+= в местоназначението) още в цикъла за напускане. Когато дойде втората поръчка, корабите на първата вече са там и тя ги взима. Правилно: в първия цикъл само се вземат (-=) и се помнят в takes; пристигането е втори цикъл, след като всички са тръгнали.
По-точно
По-точно: ред, справедливост и случайност
Редът на играчите. При едновременно движение редът на поръчките не променя резултата, защото първо всички вземат от началото на хода. При битките обаче има ред: система по система, двойка по двойка (a < b). Ако на една система са три империи във война, първата двойка в реда се бие първа и оцелелите стигат до следващата. Редът е твърд, затова играта остава повторяема.
Фазите са договор. Дипломацията е преди движението, защото тогава ново обявената война може да започне битка в същия ход. Производството е след завладяването: планета, взета този ход, не произвежда за стария си господар. Събитието е почти накрая и работи със света такъв, какъвто е станал (метеорът взима от метала, който вече е изкаран). Размени две фази и играта остава „същата“, но изходът е друг. Затова редът е на едно място (runTurn) и се чете като списък.
Случайност и повторяемост. Всичко случайно (шанс за събитие, коя империя, кой вид, колко метал) минава през един генератор със семе. Ако две фази си разменят местата и двете теглят числа, поредицата е друга. В нашата програма само rollEvent ползва генератора, затова изходът е стабилен; в канона дипломацията и изкуственият интелект също теглят числа, и редът на фазите решава кой взема кое число.
Какво не е в шината. Шината е за новини: „стана това“. Тя не е за команди („направи това“): публикуващият не знае дали някой е чул и не чака отговор. Публикувай факти, не заповеди.
Упражнение
Сигнал за играча
Реши задачата в редактора си. Тръгни от програмата за цял ход.
Сигнал. В играта wait спира, щом нещо се случи на играча. Добави абонат „сигнал“, който брои събитията war, peace и battle, в които участва империя 0 (играчът): като a или като b. След обобщението програмата печата още един ред със сигналите (сигнали: …). Не пипай declareWar, makePeace и resolveBattles: абонатът е само нов ред при създаването на света и една функция.
Изходът е (последният ред е новият):
-- ход 1 -- [дипломация] Хегемония Орион обявява война на Земна империя. [движение] Хегемония Орион праща флот от Brisa към Altan (кораби: 4). [движение] Земна империя праща флот от Kora към Vesta (кораби: 3). [събитие] Метеоритен дъжд: Съюз Лира губи 14 метал.-- ход 2 -- [движение] Хегемония Орион праща флот от Altan към Vesta (кораби: 4). [битки] Битка при Vesta: Земна империя и Хегемония Орион губят кораби (по 3).-- ход 3 -- [дипломация] Съюз Лира обявява война на Хегемония Орион. [дипломация] Хегемония Орион отказва мир с Земна империя. [битки] Битка при Vesta: Хегемония Орион и Съюз Лира губят кораби (по 1).-- ход 4 -- [дипломация] Мир между Земна империя и Хегемония Орион. [събитие] Метеоритен дъжд: Съюз Лира губи 8 метал.-- ход 5 -- [дипломация] Хегемония Орион още не може да смени отношенията си с Земна империя: прекалено скоро. [събитие] Хегемония Орион намира изоставени кораби при Brisa (кораби: 2).-- край --Земна империя: кораби 0, метал 100Хегемония Орион: кораби 2, метал 100Съюз Лира: кораби 1, метал 78сигнали: 3Още задачи
- Статистика. Добави още един абонат, който брои битките и сбора на загубените кораби (всяка битка губи
nна всяка от двете страни). Печата ги след сигналите. - Примирие. Намали
MIN_PEACEна 2 и пусни програмата. Кои редове от изхода се менят и защо ход 5 вече не е „прекалено скоро“? - Нов вид събитие. Добави
plagueс най-ранен ход 4 и тегло 10: империята губи 5 метал. Кои места променяш: таблицата,rollEvent,newsText, списъка с видовете за новините? А шината?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Масиви: push, splice и sort · Map и Set · Стрелкови функции · Данни, switch и разглобяване
- Задачи по темата: Epic Cosmos: империята