Флотите и космическата битка
Накратко: Флотът е масив с брой кораби по роли. Ролите са камък-ножица-хартия (множител в проценти), всяка група стреля по най-изгодната цел, щетата, която не стига за цял кораб, остава като рана, а двата залпа на рунда се смятат от броя в началото му и удрят едновременно. Същото е и при ходовете: първо всички тръгват, после всички пристигат.
Проблем
Кой стреля пръв?
Два флота се срещат в една система. Първият отговор е на ход: стреля първият, после вторият. Но така първата страна печели само заради реда в кода: корабите, които тя унищожи, вече не отвръщат. Играта иска друго: в един рунд и двете страни стрелят едновременно, а флотовете се местят между системите също наведнъж.
Ето кадър от играта (командата battle, от минаването, с което се проверява играта): при Измар четири прехващача срещу два бомбардировача и две фрегати. Празните редове на екрана са съкратени.
Ход 28/150 Земна империя (A) точки 147M3+27 G203+36 C11+24 U52+5 наука +16Проучава: Торпеда 28/31Битка при Ismar: Земна империя срещу Хегемония ОрионРунд 1: 4I | 2B 2F загуби 2 / 2Рунд 2: 2I | 2F загуби 2 / 0Победа за Хегемония Орион. Остават: 2F.
Enter или map: към картата help: командиИграта сгъва интервалите на редовете (battle ги пренарежда по ширината на екрана), затова „Рунд 1: 4I | 2B 2F“ няма колони. Програмите в урока печатат същите думи.
За този кадър трябват пет неща: флот (брой кораби по роли), роли (камък-ножица-хартия), залп (всяка група стреля по най-изгодната цел), рана (щетата, която не стига за цял кораб, не се губи) и едновременност (на залповете и на ходовете). Урокът ги строи едно по едно с малки програми, а накрая ги събира в същата битка. В урока за бойната система ударите са един по един и със зарче. Тук няма зарче: битката е чиста функция, същите два флота дават същия изход.
Идеята
Флотът е броят
Флотът не е списък от кораби, а пет числа: колко прехващача (I), бомбардировача (B), фрегати (F), дредноута (D) и крепости (#) има една империя в една система. Всички кораби от една роля са еднакви, затова обект за всеки кораб не трябва: битка с 500 кораба е същите пет числа. Крепостите на планетите там се бият като пета роля.
Показателите на ролите са две таблици с индекс по роля: живот и атака. Третата таблица е множителят, за който е следващата стъпка:
const KINDS = 5;const ROUNDS = 6;const LETTERS = "IBFD#";const HP = [10, 16, 30, 90, 25];const ATK = [5, 7, 6, 18, 5];const MULT = [ [100, 175, 50, 50, 60], [50, 100, 175, 80, 150], [175, 50, 100, 80, 100], [110, 110, 110, 100, 110], [100, 100, 100, 100, 100],];Две малки функции са навсякъде: сборът на корабите и текстът „3I 2B 2F“, който играта пише за един флот (празният е „-“). Силата на роля е живот × атака / 10; на флота е сборът по роли. Играта я ползва, за да сравнява флотове (изкуственият интелект, следващите уроци).
function total(counts) { let sum = 0; for (const n of counts) sum += n; return sum;}
function countsText(counts) { const parts = []; for (let k = 0; k < KINDS; k++) { if (counts[k] > 0) parts.push(counts[k] + LETTERS[k]); } return parts.length === 0 ? "-" : parts.join(" ");}function unitPower(kind) { return Math.floor((HP[kind] * ATK[kind]) / 10);}
function power(counts) { let sum = 0; for (let k = 0; k < KINDS; k++) sum += counts[k] * unitPower(k); return sum;}Програмата показва флота 3I 2B 2F: текст, брой, сила и празен флот.
const fleet = [3, 2, 2, 0, 0];console.log("Флот: " + countsText(fleet));console.log("Кораби: " + total(fleet));console.log("Сила: " + power(fleet));console.log("Празен флот: " + countsText(new Array(KINDS).fill(0)));Флот: 3I 2B 2FКораби: 7Сила: 73Празен флот: -Силата е 3 × 5 + 2 × 11 + 2 × 18 = 73. Прехващачът е 5, бомбардировачът 11, фрегатата 18, а дредноутът 162.
Math.floor(a / b) е целочислено деление: в JavaScript / е винаги дробно.В играта флотовете са в списък world.fleets, подреден по империя и после по система. fleetAt търси флота на империята в системата, а addShips го създава (на мястото му в реда) или добавя кораби към съществуващия:
function fleetAt(world, owner, systemId) { for (const fleet of world.fleets) { if (fleet.owner === owner && fleet.systemId === systemId) return fleet; } return null;}// Добавя кораби към флота на империята в системата; нов флот влиза на мястото си в реда.function addShips(world, owner, systemId, counts) { let fleet = fleetAt(world, owner, systemId); if (fleet === null) { fleet = { owner: owner, systemId: systemId, ships: [0, 0, 0, 0] }; let at = world.fleets.length; while (at > 0) { const before = world.fleets[at - 1]; if (before.owner < owner || (before.owner === owner && before.systemId < systemId)) break; at--; } world.fleets.splice(at, 0, fleet); } for (let k = 0; k < SHIP_KIND_COUNT; k++) fleet.ships[k] += counts[k];}Идеята
Камък, ножица, хартия
Ако силата беше единственото, винаги би се изплащало да строиш най-силния кораб. Затова ролите се бият помежду си по-различно. Множителят е процент на щетата: ред е нападателят, колона е целта. Прехващачът бие бомбардировача (175%), бомбардировачът бие фрегатата (175%), фрегатата бие прехващача (175%), срещу ролята, която го бие, всеки удря наполовина (50%), а срещу своята роля със 100%. Дредноутът удря всички със 100–110%, а срещу него всеки удря слабо (50–80%): той е силен сам по себе си, не по роля.
// Множителят на щетата в проценти: ред е нападателят, колона е целта (прехващач, бомбардировач,// фрегата, дредноут, крепост).const MULT = [ [100, 175, 50, 50, 60], [50, 100, 175, 80, 150], [175, 50, 100, 80, 100], [110, 110, 110, 100, 110], [100, 100, 100, 100, 100],];Всяка група (всички кораби от една роля) стреля по една цел: ролята сред живите врагове, срещу която множителят е най-голям. При равенство печели първата в реда: сравнението е строго „по-голямо“, затова по-късна роля с равен множител не заменя вече избраната.
function chooseTarget(kind, enemy) { let best = -1; for (let k = 0; k < KINDS; k++) { if (enemy[k] > 0 && (best < 0 || MULT[kind][k] > MULT[kind][best])) best = k; } return best;}function chooseTarget(kind, enemy) {
let best = -1;
for (let k = 0; k < KINDS; k++) {
if (enemy[k] > 0 && (best < 0 || MULT[kind][k] MULT[kind][best])) best = k;
}
return best;
}Сравнението е „по-голямо“: >. С >= при равенство би печелила последната роля: програмата пак би работила, но битките щяха да са други. С > целта зависи само от реда на ролите, затова изходът е един и същ на четирите езика.
Програмата показва целите на петте роли срещу пълен флот и един случай, в който най-изгодната роля липсва:
const all = [1, 1, 1, 1, 1];for (let kind = 0; kind < KINDS; kind++) { const target = chooseTarget(kind, all); console.log(NAMES[kind] + " -> " + NAMES[target] + " " + MULT[kind][target] + "%");}const noFrigates = [1, 1, 0, 1, 1];const alt = chooseTarget(1, noFrigates);console.log("Без фрегати: " + NAMES[1] + " -> " + NAMES[alt] + " " + MULT[1][alt] + "%");прехващач -> бомбардировач 175%бомбардировач -> фрегата 175%фрегата -> прехващач 175%дредноут -> прехващач 110%крепост -> прехващач 100%Без фрегати: бомбардировач -> крепост 150%Първите три реда са триъгълникът. Дредноутът и крепостта нямат роля, срещу която да са по-силни: най-големият им множител (110% и 100%) е на първото място в реда, прехващача. Последният ред е най-интересен: бомбардировачът иска фрегати, а ги няма, затова избира следващия по изгода, крепостта (150%), не прехващача (50%) и не дредноута (80%).
Ето функцията в играта:
function chooseTarget(kind, enemy) { let best = -1; for (let k = 0; k < KIND_COUNT; k++) { if (enemy[k] > 0 && (best < 0 || MULT[kind][k] > MULT[kind][best])) best = k; } return best;}Идеята
Залп и рана
Групата, която е избрала цел, нанася щета = броят × атака × множител / 100. Деленето е целочислено и се отрязва: 6 бомбардировача по 7 срещу фрегати са 6 × 7 × 175 / 100 = 73 (не 73,5). Щетата на всички групи на една страна се събира по роля на целта.
function volley(mine, enemy) { const damage = new Array(KINDS).fill(0); for (let k = 0; k < KINDS; k++) { if (mine[k] === 0) continue; const target = chooseTarget(k, enemy); if (target < 0) continue; damage[target] += Math.floor((mine[k] * ATK[k] * MULT[k][target]) / 100); } return damage;}После щетата се прилага. Щетата по роля се дели на живота на кораба: колкото цели кораба се побират, толкова падат. Ако падат всички, ролята изчезва. Иначе остатъкът (щетата, която не стига за още един кораб) се записва като рана и се добавя към следващия залп. Без раните бавният огън никога нямаше да убива кораб: 9 щета по кораб с живот 10 са нищо, а втори път са 18 и един кораб пада.
function applyDamage(counts, wound, damage) { for (let k = 0; k < KINDS; k++) { if (damage[k] === 0 || counts[k] === 0) continue; const sum = wound[k] + damage[k]; const killed = Math.floor(sum / HP[k]); if (killed >= counts[k]) { counts[k] = 0; wound[k] = 0; } else { counts[k] -= killed; wound[k] = sum - killed * HP[k]; } }}Програмата прави първия рунд на битката при Измар: двата залпа (колоните са I, B, F, D, #), после прилагането. Малката функция numbers печата масив с интервали:
function numbers(values) { return values.join(" ");}Програмата:
const a = [4, 0, 0, 0, 0];const b = [0, 2, 2, 0, 0];const hitB = volley(a, b);const hitA = volley(b, a);console.log("a -> b: " + numbers(hitB));console.log("b -> a: " + numbers(hitA));const woundA = new Array(KINDS).fill(0);const woundB = new Array(KINDS).fill(0);applyDamage(b, woundB, hitB);applyDamage(a, woundA, hitA);console.log("a: " + countsText(a) + ", рани " + numbers(woundA));console.log("b: " + countsText(b) + ", рани " + numbers(woundB));a -> b: 0 35 0 0 0b -> a: 28 0 0 0 0a: 2I, рани 8 0 0 0 0b: 2F, рани 0 0 0 0 0Страната a удря само бомбардировачите: 4 × 5 × 175 / 100 = 35, а два бомбардировача са 32 живот, затова и двата падат (35 / 16 = 2). Страната b удря само прехващачите: бомбардировачите с 50% (2 × 7 × 50 / 100 = 7), фрегатите със 175% (2 × 6 × 175 / 100 = 21), общо 28. 28 / 10 = 2 прехващача падат, а 8 остават като рана на третия. Фрегатите в първия рунд не са ударени: прехващачите стрелят само по бомбардировачите.
Ето двете функции в играта. Освен броя кораби и щетата те получават stats (бонусите от технологиите); тук ги няма, затова числата на програмата са на империя без технологии. В Java щетата е long, защото броят кораби от запис може да е голям и int би препълнил:
// Щетата, която една страна нанася на другата, по роля на целта.function volley(stats, mine, enemy) { const damage = emptyCounts(); for (let k = 0; k < KIND_COUNT; k++) { if (mine[k] === 0) continue; const target = chooseTarget(k, enemy); if (target < 0) continue; damage[target] += Math.floor((mine[k] * stats.atk[k] * MULT[k][target]) / 100); } return damage;}// Щетата се трупа: единиците губят живот, но първият оцелял пази раната си за следващия рунд.function applyDamage(stats, counts, wound, damage) { for (let k = 0; k < KIND_COUNT; k++) { if (damage[k] === 0 || counts[k] === 0) continue; const total = wound[k] + damage[k]; const killed = Math.floor(total / stats.hp[k]); if (killed >= counts[k]) { counts[k] = 0; wound[k] = 0; } else { counts[k] -= killed; wound[k] = total - killed * stats.hp[k]; } }}Виж го
Целият рунд, едновременно
Сега всичко се събира. Цикълът на битката прави за всеки рунд следното:
- Ако някоя страна няма кораби, край.
- Копира броя на двете страни: това е „снимката“ на началото на рунда.
- Смята и двата залпа от снимките: залпа на a по b и залпа на b по a.
- Прилага щетата по b, после по a. Редът няма значение, защото залповете вече са изчислени.
- Записва реда на рунда: колко кораба са имали двете страни в началото и колко са загубили.
След шест рунда, ако още има кораби и от двете страни, няма победител. Ето цялата функция и програмата, която прави битката при Измар:
function runBattle(a, b, nameA, nameB, log) { const woundA = new Array(KINDS).fill(0); const woundB = new Array(KINDS).fill(0); for (let round = 1; round <= ROUNDS; round++) { if (total(a) === 0 || total(b) === 0) break; const startA = [...a]; const startB = [...b]; const hitB = volley(startA, startB); const hitA = volley(startB, startA); applyDamage(b, woundB, hitB); applyDamage(a, woundA, hitA); log.push("Рунд " + round + ": " + countsText(startA) + " | " + countsText(startB) + " загуби " + (total(startA) - total(a)) + " / " + (total(startB) - total(b))); } if (total(a) > 0 && total(b) === 0) log.push("Победа за " + nameA + ". Остават: " + countsText(a) + "."); else if (total(b) > 0 && total(a) === 0) log.push("Победа за " + nameB + ". Остават: " + countsText(b) + "."); else if (total(a) === 0 && total(b) === 0) log.push("Двата флота са унищожени."); else log.push("Няма победител след шест рунда: " + countsText(a) + " | " + countsText(b) + ".");}const a = [4, 0, 0, 0, 0];const b = [0, 2, 2, 0, 0];const where = "Ismar";const nameA = "Земна империя";const nameB = "Хегемония Орион";const log = [];runBattle(a, b, nameA, nameB, log);console.log("Битка при " + where + ": " + nameA + " срещу " + nameB);for (const line of log) console.log(line);Битка при Ismar: Земна империя срещу Хегемония ОрионРунд 1: 4I | 2B 2F загуби 2 / 2Рунд 2: 2I | 2F загуби 2 / 0Победа за Хегемония Орион. Остават: 2F.Това са точно редовете на играта. Вторият рунд е кратък: от четирите прехващача са останали два (с рана 8), а от бомбардировачите няма следа. Двата прехващача бият фрегатите само с 50% (2 × 5 × 50 / 100 = 5 щета, по-малко от живота на един кораб), а фрегатите бият прехващачите със 175%: 2 × 6 × 175 / 100 = 21, плюс раната 8 дава 29. Двата прехващача падат. Фрегатите печелят, без да загубят кораб. Следи рундовете в сцената: стрелките и числата са едновременни.
function runBattle(a, b, nameA, nameB, log) { const woundA = new Array(KINDS).fill(0); const woundB = new Array(KINDS).fill(0); for (let round = 1; round <= ROUNDS; round++) { if (total(a) === 0 || total(b) === 0) break; const startA = [...a]; const startB = [...b]; const hitB = volley(startA, startB); const hitA = volley(startB, startA); applyDamage(b, woundB, hitB); applyDamage(a, woundA, hitA); log.push("Рунд " + round + ": " + countsText(startA) + " | " + countsText(startB) + " загуби " + (total(startA) - total(a)) + " / " + (total(startB) - total(b))); } if (total(a) > 0 && total(b) === 0) log.push("Победа за " + nameA + ". Остават: " + countsText(a) + "."); else if (total(b) > 0 && total(a) === 0) log.push("Победа за " + nameB + ". Остават: " + countsText(b) + "."); else if (total(a) === 0 && total(b) === 0) log.push("Двата флота са унищожени."); else log.push("Няма победител след шест рунда: " + countsText(a) + " | " + countsText(b) + ".");}Стъпка по стъпка, с думи
- Два флота при Измар: a има 4I, b има 2B 2F. Флотът е само броят кораби по роли, а битката е функция на два такива масива.
- Рунд 1. Всяка страна копира броя си в началото на рунда. И двата залпа ще се смятат от тези копия.
- Всяка роля избира цел с най-голям множител. a: 4I → B (175%). b: 2B → I (50%), 2F → I (175%). Камък-ножица-хартия.
- Залповете: a: 4 × 5 × 175 / 100 = 35 по бомбардировачите. b: 2 × 7 × 50 / 100 = 7; 2 × 6 × 175 / 100 = 21 по прехващачите, общо 28. Удрят едновременно.
- Щетата се прилага: 35 / 16 = 2 бомбардировача падат. 28 / 10 = 2 прехващача падат, остатъкът 8 е рана. Дневникът: загуби 2 / 2.
- Рунд 2: 2I срещу 2F. Бомбардировачите ги няма, затова целите се избират наново.
- Прехващачите бият фрегатите само с 50%: 2 × 5 × 50 / 100 = 5. Фрегатите бият прехващачите със 175%: 2 × 6 × 175 / 100 = 21.
- Щетата по прехващачите е 21 плюс раната 8 = 29: падат и двата. Фрегатите получават 5, по-малко от живота 30: рана 5, никоя не пада.
- a няма кораби, b има 2F: Победа за Хегемония Орион. Остават: 2F. Така е и в играта. Битката е чиста функция.
В играта битката е същата функция, само с повече параметри (stats за технологиите), и връща списък от рундове, а текстът се пише от друга функция. Ето я:
function runBattle(statsA, countsA, statsB, countsB) { const a = countsA.slice(); const b = countsB.slice(); const woundA = emptyCounts(); const woundB = emptyCounts(); const rounds = []; for (let round = 1; round <= BATTLE_ROUNDS; round++) { if (totalOf(a) === 0 || totalOf(b) === 0) break; const startA = a.slice(); const startB = b.slice(); const hitB = volley(statsA, startA, startB); const hitA = volley(statsB, startB, startA); applyDamage(statsB, b, woundB, hitB); applyDamage(statsA, a, woundA, hitA); const lostA = emptyCounts(); const lostB = emptyCounts(); for (let k = 0; k < KIND_COUNT; k++) { lostA[k] = startA[k] - a[k]; lostB[k] = startB[k] - b[k]; } rounds.push({ a: startA, b: startB, lostA: lostA, lostB: lostB }); } let winner = 'draw'; if (totalOf(a) > 0 && totalOf(b) === 0) winner = 'a'; else if (totalOf(b) > 0 && totalOf(a) === 0) winner = 'b'; else if (totalOf(a) === 0 && totalOf(b) === 0) winner = 'none'; return { rounds: rounds, a: a, b: b, winner: winner };}По-точно, за любопитните
Битката не е случайна, затова е чиста функция: същите два флота дават същия изход, винаги и на четирите езика. Не се хвърля зарче (за разлика от урока за бойната система, където всеки удар е с шанс). Така битката се тества с едно извикване и проверка на резултата. Със същата функция се бият и пиратите от събитията (против флот без технологии), а решение като „да нападна ли“ би могло да я изиграе наум върху копие на флотовете. Изкуственият интелект на играта е по-прост: сравнява силите на флотовете (както по-горе), а как решава, е темата на урока за съперниците.
Рундовете са най-много шест (BATTLE_ROUNDS): ако двата флота не могат да се унищожат, например малък флот срещу голяма крепост, боят свършва без победител и играта пише „няма победител след шест рунда“. Така битка не виси вечно.
Идеята
Едновременно и при ходовете
Същият принцип важи за движението. В играта играчът (и изкуственият интелект) дава заповеди, например go 1 2 3 2 2 0 („от система 1 към 2: три прехващача, два бомбардировача, две фрегати“), а флотовете тръгват чак във фазата на движението. Те минават една връзка на ход по най-краткия път и заповедта се подновява сама, докато стигнат целта.
Движението е в две фази: първо всички напускат системите си, чак тогава всички пристигат. Ето защо, например, два вражески флота, които си разменят системите, не се срещат: докато единият напуска, другият още не е пристигнал. Битките са после, само там, където след движението има и двата флота в една система.
Програмата движи три флота (X, Y и Z) по верига от системи 0, 1, 2, 3 (заповеди към 3, 0 и 1) за два хода и печата къде са и кои се срещат. После прави същото грешно: мести флот по флот и проверява среща веднага след всяко преместване.
function stepToward(pos, dest) { if (pos < dest) return pos + 1; if (pos > dest) return pos - 1; return pos;}function report(turn, names, pos) { const parts = pos.map((p, i) => names[i] + " в " + p); console.log("Ход " + turn + ": " + parts.join(", ")); for (let s = 0; s < 4; s++) { const here = names.filter((_, i) => pos[i] === s); if (here.length > 1) console.log("среща в " + s + ": " + here.join(", ")); }}const names = ["X", "Y", "Z"];const dest = [3, 0, 1];let pos = [0, 1, 2];for (let turn = 1; turn <= 2; turn++) { const next = pos.map((p, i) => stepToward(p, dest[i])); pos = next; report(turn, names, pos);}const live = [0, 1, 2];for (let i = 0; i < live.length; i++) { live[i] = stepToward(live[i], dest[i]); for (let j = 0; j < live.length; j++) { if (j !== i && live[j] === live[i]) console.log("Един по един: " + names[i] + " среща " + names[j]); }}Ход 1: X в 1, Y в 0, Z в 1среща в 1: X, ZХод 2: X в 2, Y в 0, Z в 1Един по един: X среща YЕдин по един: Z среща XВ първия ход X (от 0 към 3) и Y (от 1 към 0) си разменят системите и не се срещат, но X и Z (от 2 към 1) са едновременно в система 1: среща. Грешният начин вижда „среща“ и между X и Y: X пристига в 1, докато Y още е там и тъкмо тръгва. Проверката вътре в цикъла вижда половин ход. Правилният начин първо смята къде ще бъде всеки (next) от стария ход и чак тогава ги слага (pos = next).
Ето движението в играта. Списъкът moving е „във въздуха“: корабите вече са напуснали флота си, но още не са пристигнали:
// Един ход на галактиката има фази: дипломация на изкуствения интелект, поръчките му, движение на// всички флотове наведнъж, битки, завладяване, производство и наука, събитие, край на хода.// Движението е едновременно: първо всички кораби напускат системите си, чак тогава пристигат.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);}Идеята
Симулация на битка
Битката е чиста функция, затова не е нужно да я гледаш, за да я разбереш: можеш да я пускаш върху много флотове и да четеш таблица. Така се настройва балансът на играта. Програмата сравнява еднакви флоти от по 6 кораба на няколко двойки роли:
const pairs = [[0, 1], [1, 2], [0, 2], [0, 3]];for (const [first, second] of pairs) { const a = new Array(KINDS).fill(0); const b = new Array(KINDS).fill(0); a[first] = 6; b[second] = 6; const head = countsText(a) + " срещу " + countsText(b) + ": "; runBattle(a, b, "a", "b", []); if (total(a) > 0 && total(b) === 0) console.log(head + "печели " + LETTERS[first] + ", остават " + countsText(a)); else if (total(b) > 0 && total(a) === 0) console.log(head + "печели " + LETTERS[second] + ", остават " + countsText(b)); else console.log(head + "без победител");}6I срещу 6B: печели I, остават 3I6B срещу 6F: печели B, остават 4B6I срещу 6F: печели F, остават 6F6I срещу 6D: печели D, остават 6DТриъгълникът се вижда в числата. Шест прехващача побеждават шест бомбардировача и губят три. Шест бомбардировача побеждават шест фрегати и губят два. Шест фрегати унищожават шест прехващача без нито една загуба: прехващачите падат още в първия рунд. Дредноутът (90 живот, атака 18) е друг случай: шест дредноута унищожават шест прехващача, също без загуба, защото всеки от тях издържа много повече (в играта той иска технология и повече ресурси). „Кой е по-силен“ няма един отговор: зависи от ролята на врага. Затова и силата на флота (сборът от силите на ролите) е само груба оценка.
Същото се прави в играта с цели игри: --simulate=200 пуска двеста игри без екран и печата кой печели. Това е темата на последния урок. Битките в тях са тази функция, пускана хиляди пъти.
Пробвай
Предвиди изхода
Три бомбардировача срещу три прехващача. Кой печели и колко рунда трае битката? Не пускай програмата: пресметни рундовете на ръка с таблиците от урока (живот 10 и атака 5 за прехващача, 16 и 7 за бомбардировача) и не забравяй раните.
const a = [0, 3, 0, 0, 0];const b = [3, 0, 0, 0, 0];const log = [];runBattle(a, b, "Земна империя", "Хегемония Орион", log);for (const line of log) console.log(line);Рунд 1: прехващачите бият бомбардировачите със 175%: 3 × 5 × 175 / 100 = 26, а 26 / 16 = 1 пада, остава рана 10. Бомбардировачите бият прехващачите с 50%: 3 × 7 × 50 / 100 = 10, един прехващач пада без рана. Рунд 2 (2 срещу 2): 2 × 5 × 175 / 100 = 17 плюс раната 10 е 27, един бомбардировач пада (остава рана 11); бомбардировачите дават 7, никой прехващач не пада (рана 7). Рунд 3 (1 бомбардировач с рана 11 срещу 2 прехващача с рана 7): 17 + 11 = 28 убива последния бомбардировач, а 3 + 7 = 10 убива един прехващач. Остава един прехващач. Изходът с четири рунда е, ако вторият залп се смята след първия (бомбардировачите, които са стреляли пръв, биха ударили първи): рундовете стават четири, а остатъкът е същият. Изходът с два оцелели прехващача е без пренасяне на раните: бавният огън не убива.
Пробвай
Намери грешката
Колега реши да „пооправи“ рунда: щом залпът на a е изчислен, веднага го прилага, и чак след това смята залпа на b. Така не му трябват копията за залповете. Двубой от два прехващача срещу два бомбардировача печата:
const a = [2, 0, 0, 0, 0];const b = [0, 2, 0, 0, 0];const log = [];runBattle(a, b, "a", "b", log);for (const line of log) console.log(line);Рунд 1: 2I | 2B загуби 0 / 1Рунд 2: 2I | 1B загуби 0 / 1Победа за a. Остават: 2I.Вторият рунд свършва с „загуби 0 / 1“, а победата оставя два прехващача. Преди промяната вторият рунд е „загуби 1 / 1“ и остава един прехващач (Рунд 2: 2I | 1B загуби 1 / 1). Кой ред се изпълнява твърде рано?
Избери реда с грешката.
Прилагането на щетата към b стои преди залпа на b: когато b стреля, броят му вече е намален и бомбардировачите, които падат в този рунд, мълчат. Във втория рунд последният бомбардировач е убит от залпа на a и не успява да удари. Правилното е и двата залпа да се изчислят от копията в началото на рунда, а чак тогава да се приложат, както прави функцията на битката в урока.
По-точно
Какво не е в тази система
Битката в урока е ядрото; в играта има още няколко неща, които не променят принципа.
Технологиите дават бонуси на атаката и на живота (и допълнителни за бомбардировачите и за фрегатите). Те влизат през stats на volley и applyDamage: един и същ масив с брой кораби се бие различно за две империи. Затова параметърът го има в канона, а тук не.
Крепостите са пета роля (FORT_KIND): планетите на една империя в системата им дават станции, които стрелят и падат като кораби. След битката оцелелите станции се пазят до края на хода, а в началото на следващия са пълни.
Повече от две империи в една система се бият по двойки: за всяка двойка във война и с кораби или станции там се вика една битка. Редът на двойките е фиксиран, затова изходът е определен.
Няма отстъпление по средата на битката и няма избор на цел. Отстъплението е заповед за движение, която се изпълнява преди битката. Групата винаги стреля по най-изгодната роля; по-богата версия би дала на играча команда „фокус върху една роля“, но тогава битката вече не би била функция само на двата флота.
Загубите са цели кораби. Раната се пази само в рамките на битката: след нея ранен кораб е здрав, защото флотът е само броят. Игра с ремонт би пазела рана на флота между ходовете.
Упражнение
Битка по входа
Реши задачата в редактора си.
Битка по входа. Програмата чете от входа два реда по пет числа: броя на I, B, F, D и # за флота a, после същото за b. Печата редовете на битката като в урока: един ред на рунд и последния ред с победата (с имена „a“ и „b“; няма заглавие).
С вход 5 0 4 0 0 | 0 6 2 0 0 (по един флот на ред) изходът е:
Рунд 1: 5I 4F | 6B 2F загуби 4 / 2Рунд 2: 3I 2F | 4B 2F загуби 4 / 3Рунд 3: 1I | 2B 1F загуби 1 / 0Победа за b. Остават: 2B 1F.Внимавай на третия рунд: a е останал с един прехващач срещу два бомбардировача и една фрегата, и на победата, която е на b, не на a, въпреки че a започва с повече кораби.
Още задачи
- Колко прехващача трябват? За колко прехващача (от 1 до 30) побеждават 4 фрегати? Печатай първото число, при което a печели. Пробвай и с бомбардировачи срещу 4 фрегати.
- Размяна на реда. Пусни една и съща битка като (a, b) и като (b, a). Едно и също ли е? Защо?
- Дредноут. Добави на a дредноут. Колко прехващача на b трябват, за да го унищожат?
- Празен флот. Какво прави програмата, ако едната страна няма кораби? Не трябва да се чупи.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Функции · Параметри и return · static: общото за всички · Масиви: push, splice и sort
- Задачи по темата: Epic Cosmos: империята