Флотите и космическата битка
Накратко: Флотът е масив с брой кораби по роли. Ролите са камък-ножица-хартия (множител в проценти), всяка група стреля по най-изгодната цел, щетата, която не стига за цял кораб, остава като рана, а двата залпа на рунда се смятат от броя в началото му и удрят едновременно. Същото е и при ходовете: първо всички тръгват, после всички пристигат.
Проблем
Кой стреля пръв?
Два флота се срещат в една система. Първият отговор е на ход: стреля първият, после вторият. Но така първата страна печели само заради реда в кода: корабите, които тя унищожи, вече не отвръщат. Играта иска друго: в един рунд и двете страни стрелят едновременно, а флотовете се местят между системите също наведнъж.
Ето кадър от играта (командата 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 кораба е същите пет числа. Крепостите на планетите там се бият като пета роля.
Показателите на ролите са две таблици с индекс по роля: живот и атака. Третата таблица е множителят, за който е следващата стъпка:
static final int KINDS = 5;static final int ROUNDS = 6;static final String LETTERS = "IBFD#";static final int[] HP = {10, 16, 30, 90, 25};static final int[] ATK = {5, 7, 6, 18, 5};static final int[][] 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; на флота е сборът по роли. Играта я ползва, за да сравнява флотове (изкуственият интелект, следващите уроци).
static int total(int[] counts) { int sum = 0; for (int n : counts) sum += n; return sum;}
static String countsText(int[] counts) { String text = ""; for (int k = 0; k < KINDS; k++) { if (counts[k] > 0) text += (text.isEmpty() ? "" : " ") + counts[k] + LETTERS.charAt(k); } return text.isEmpty() ? "-" : text;}static int unitPower(int kind) { return HP[kind] * ATK[kind] / 10;}
static int power(int[] counts) { int sum = 0; for (int k = 0; k < KINDS; k++) sum += counts[k] * unitPower(k); return sum;}Програмата показва флота 3I 2B 2F: текст, брой, сила и празен флот.
int[] fleet = {3, 2, 2, 0, 0};System.out.println("Флот: " + countsText(fleet));System.out.println("Кораби: " + total(fleet));System.out.println("Сила: " + power(fleet));System.out.println("Празен флот: " + countsText(new int[KINDS]));Флот: 3I 2B 2FКораби: 7Сила: 73Празен флот: -Силата е 3 × 5 + 2 × 11 + 2 × 18 = 73. Прехващачът е 5, бомбардировачът 11, фрегатата 18, а дредноутът 162.
HP[kind] * ATK[kind] / 10 е целочислено, защото и трите са int.В играта флотовете са в списък world.fleets, подреден по империя и после по система. fleetAt търси флота на империята в системата, а addShips го създава (на мястото му в реда) или добавя кораби към съществуващия:
static Fleet fleetAt(World world, int owner, int systemId) { for (Fleet fleet : world.fleets) { if (fleet.owner == owner && fleet.systemId == systemId) return fleet; } return null;}// Добавя кораби към флота на империята в системата; нов флот влиза на мястото си в реда.static void addShips(World world, int owner, int systemId, int[] counts) { Fleet fleet = fleetAt(world, owner, systemId); if (fleet == null) { fleet = new Fleet(owner, systemId, new int[] {0, 0, 0, 0}); int at = world.fleets.size(); while (at > 0) { Fleet before = world.fleets.get(at - 1); if (before.owner < owner || (before.owner == owner && before.systemId < systemId)) break; at--; } world.fleets.add(at, fleet); } for (int k = 0; k < SHIP_KIND_COUNT; k++) fleet.ships[k] += counts[k];}Идеята
Камък, ножица, хартия
Ако силата беше единственото, винаги би се изплащало да строиш най-силния кораб. Затова ролите се бият помежду си по-различно. Множителят е процент на щетата: ред е нападателят, колона е целта. Прехващачът бие бомбардировача (175%), бомбардировачът бие фрегатата (175%), фрегатата бие прехващача (175%), срещу ролята, която го бие, всеки удря наполовина (50%), а срещу своята роля със 100%. Дредноутът удря всички със 100–110%, а срещу него всеки удря слабо (50–80%): той е силен сам по себе си, не по роля.
// Множителят на щетата в проценти: ред е нападателят, колона е целта (прехващач, бомбардировач,// фрегата, дредноут, крепост).static final int[][] 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},};Всяка група (всички кораби от една роля) стреля по една цел: ролята сред живите врагове, срещу която множителят е най-голям. При равенство печели първата в реда: сравнението е строго „по-голямо“, затова по-късна роля с равен множител не заменя вече избраната.
static int chooseTarget(int kind, int[] enemy) { int best = -1; for (int k = 0; k < KINDS; k++) { if (enemy[k] > 0 && (best < 0 || MULT[kind][k] > MULT[kind][best])) best = k; } return best;}static int chooseTarget(int kind, int[] enemy) {
int best = -1;
for (int k = 0; k < KINDS; k++) {
if (enemy[k] > 0 && (best < 0 || MULT[kind][k] MULT[kind][best])) best = k;
}
return best;
}Сравнението е „по-голямо“: >. С >= при равенство би печелила последната роля: програмата пак би работила, но битките щяха да са други. С > целта зависи само от реда на ролите, затова изходът е един и същ на четирите езика.
Програмата показва целите на петте роли срещу пълен флот и един случай, в който най-изгодната роля липсва:
int[] all = {1, 1, 1, 1, 1};for (int kind = 0; kind < KINDS; kind++) { int target = chooseTarget(kind, all); System.out.println(NAMES[kind] + " -> " + NAMES[target] + " " + MULT[kind][target] + "%");}int[] noFrigates = {1, 1, 0, 1, 1};int alt = chooseTarget(1, noFrigates);System.out.println("Без фрегати: " + NAMES[1] + " -> " + NAMES[alt] + " " + MULT[1][alt] + "%");прехващач -> бомбардировач 175%бомбардировач -> фрегата 175%фрегата -> прехващач 175%дредноут -> прехващач 110%крепост -> прехващач 100%Без фрегати: бомбардировач -> крепост 150%Първите три реда са триъгълникът. Дредноутът и крепостта нямат роля, срещу която да са по-силни: най-големият им множител (110% и 100%) е на първото място в реда, прехващача. Последният ред е най-интересен: бомбардировачът иска фрегати, а ги няма, затова избира следващия по изгода, крепостта (150%), не прехващача (50%) и не дредноута (80%).
Ето функцията в играта:
static int chooseTarget(int kind, int[] enemy) { int best = -1; for (int 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). Щетата на всички групи на една страна се събира по роля на целта.
static int[] volley(int[] mine, int[] enemy) { int[] damage = new int[KINDS]; for (int k = 0; k < KINDS; k++) { if (mine[k] == 0) continue; int target = chooseTarget(k, enemy); if (target < 0) continue; damage[target] += mine[k] * ATK[k] * MULT[k][target] / 100; } return damage;}После щетата се прилага. Щетата по роля се дели на живота на кораба: колкото цели кораба се побират, толкова падат. Ако падат всички, ролята изчезва. Иначе остатъкът (щетата, която не стига за още един кораб) се записва като рана и се добавя към следващия залп. Без раните бавният огън никога нямаше да убива кораб: 9 щета по кораб с живот 10 са нищо, а втори път са 18 и един кораб пада.
static void applyDamage(int[] counts, int[] wound, int[] damage) { for (int k = 0; k < KINDS; k++) { if (damage[k] == 0 || counts[k] == 0) continue; int sum = wound[k] + damage[k]; int killed = 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 печата масив с интервали:
static String numbers(int[] values) { String text = ""; for (int v : values) text += (text.isEmpty() ? "" : " ") + v; return text;}Програмата:
int[] a = {4, 0, 0, 0, 0};int[] b = {0, 2, 2, 0, 0};int[] hitB = volley(a, b);int[] hitA = volley(b, a);System.out.println("a -> b: " + numbers(hitB));System.out.println("b -> a: " + numbers(hitA));int[] woundA = new int[KINDS];int[] woundB = new int[KINDS];applyDamage(b, woundB, hitB);applyDamage(a, woundA, hitA);System.out.println("a: " + countsText(a) + ", рани " + numbers(woundA));System.out.println("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 би препълнил:
// Щетата, която една страна нанася на другата, по роля на целта. Сметката е в long: броят на// корабите от запис може да е голям, а JavaScript не прелива.static long[] volley(SideStats stats, int[] mine, int[] enemy) { long[] damage = new long[KIND_COUNT]; for (int k = 0; k < KIND_COUNT; k++) { if (mine[k] == 0) continue; int target = chooseTarget(k, enemy); if (target < 0) continue; damage[target] += ((long) mine[k] * stats.atk[k] * MULT[k][target]) / 100; } return damage;}// Щетата се трупа: единиците губят живот, но първият оцелял пази раната си за следващия рунд.static void applyDamage(SideStats stats, int[] counts, int[] wound, long[] damage) { for (int k = 0; k < KIND_COUNT; k++) { if (damage[k] == 0 || counts[k] == 0) continue; long total = wound[k] + damage[k]; long killed = total / stats.hp[k]; if (killed >= counts[k]) { counts[k] = 0; wound[k] = 0; } else { counts[k] -= (int) killed; wound[k] = (int) (total - killed * stats.hp[k]); } }}Виж го
Целият рунд, едновременно
Сега всичко се събира. Цикълът на битката прави за всеки рунд следното:
- Ако някоя страна няма кораби, край.
- Копира броя на двете страни: това е „снимката“ на началото на рунда.
- Смята и двата залпа от снимките: залпа на a по b и залпа на b по a.
- Прилага щетата по b, после по a. Редът няма значение, защото залповете вече са изчислени.
- Записва реда на рунда: колко кораба са имали двете страни в началото и колко са загубили.
След шест рунда, ако още има кораби и от двете страни, няма победител. Ето цялата функция и програмата, която прави битката при Измар:
static void runBattle(int[] a, int[] b, String nameA, String nameB, List<String> log) { int[] woundA = new int[KINDS]; int[] woundB = new int[KINDS]; for (int round = 1; round <= ROUNDS; round++) { if (total(a) == 0 || total(b) == 0) break; int[] startA = a.clone(); int[] startB = b.clone(); int[] hitB = volley(startA, startB); int[] hitA = volley(startB, startA); applyDamage(b, woundB, hitB); applyDamage(a, woundA, hitA); log.add("Рунд " + round + ": " + countsText(startA) + " | " + countsText(startB) + " загуби " + (total(startA) - total(a)) + " / " + (total(startB) - total(b))); } if (total(a) > 0 && total(b) == 0) log.add("Победа за " + nameA + ". Остават: " + countsText(a) + "."); else if (total(b) > 0 && total(a) == 0) log.add("Победа за " + nameB + ". Остават: " + countsText(b) + "."); else if (total(a) == 0 && total(b) == 0) log.add("Двата флота са унищожени."); else log.add("Няма победител след шест рунда: " + countsText(a) + " | " + countsText(b) + ".");}int[] a = {4, 0, 0, 0, 0};int[] b = {0, 2, 2, 0, 0};String where = "Ismar";String nameA = "Земна империя";String nameB = "Хегемония Орион";List<String> log = new ArrayList<>();runBattle(a, b, nameA, nameB, log);System.out.println("Битка при " + where + ": " + nameA + " срещу " + nameB);for (String line : log) System.out.println(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. Двата прехващача падат. Фрегатите печелят, без да загубят кораб. Следи рундовете в сцената: стрелките и числата са едновременни.
static void runBattle(int[] a, int[] b, String nameA, String nameB, List<String> log) { int[] woundA = new int[KINDS]; int[] woundB = new int[KINDS]; for (int round = 1; round <= ROUNDS; round++) { if (total(a) == 0 || total(b) == 0) break; int[] startA = a.clone(); int[] startB = b.clone(); int[] hitB = volley(startA, startB); int[] hitA = volley(startB, startA); applyDamage(b, woundB, hitB); applyDamage(a, woundA, hitA); log.add("Рунд " + round + ": " + countsText(startA) + " | " + countsText(startB) + " загуби " + (total(startA) - total(a)) + " / " + (total(startB) - total(b))); } if (total(a) > 0 && total(b) == 0) log.add("Победа за " + nameA + ". Остават: " + countsText(a) + "."); else if (total(b) > 0 && total(a) == 0) log.add("Победа за " + nameB + ". Остават: " + countsText(b) + "."); else if (total(a) == 0 && total(b) == 0) log.add("Двата флота са унищожени."); else log.add("Няма победител след шест рунда: " + 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 за технологиите), и връща списък от рундове, а текстът се пише от друга функция. Ето я:
static BattleResult runBattle(SideStats statsA, int[] countsA, SideStats statsB, int[] countsB) { int[] a = countsA.clone(); int[] b = countsB.clone(); int[] woundA = emptyCounts(); int[] woundB = emptyCounts(); List<Round> rounds = new ArrayList<>(); for (int round = 1; round <= BATTLE_ROUNDS; round++) { if (totalOf(a) == 0 || totalOf(b) == 0) break; int[] startA = a.clone(); int[] startB = b.clone(); long[] hitB = volley(statsA, startA, startB); long[] hitA = volley(statsB, startB, startA); applyDamage(statsB, b, woundB, hitB); applyDamage(statsA, a, woundA, hitA); int[] lostA = emptyCounts(); int[] lostB = emptyCounts(); for (int k = 0; k < KIND_COUNT; k++) { lostA[k] = startA[k] - a[k]; lostB[k] = startB[k] - b[k]; } rounds.add(new Round(startA, startB, lostA, lostB)); } String 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 new BattleResult(rounds, a, b, winner);}По-точно, за любопитните
Битката не е случайна, затова е чиста функция: същите два флота дават същия изход, винаги и на четирите езика. Не се хвърля зарче (за разлика от урока за бойната система, където всеки удар е с шанс). Така битката се тества с едно извикване и проверка на резултата. Със същата функция се бият и пиратите от събитията (против флот без технологии), а решение като „да нападна ли“ би могло да я изиграе наум върху копие на флотовете. Изкуственият интелект на играта е по-прост: сравнява силите на флотовете (както по-горе), а как решава, е темата на урока за съперниците.
Рундовете са най-много шест (BATTLE_ROUNDS): ако двата флота не могат да се унищожат, например малък флот срещу голяма крепост, боят свършва без победител и играта пише „няма победител след шест рунда“. Така битка не виси вечно.
Идеята
Едновременно и при ходовете
Същият принцип важи за движението. В играта играчът (и изкуственият интелект) дава заповеди, например go 1 2 3 2 2 0 („от система 1 към 2: три прехващача, два бомбардировача, две фрегати“), а флотовете тръгват чак във фазата на движението. Те минават една връзка на ход по най-краткия път и заповедта се подновява сама, докато стигнат целта.
Движението е в две фази: първо всички напускат системите си, чак тогава всички пристигат. Ето защо, например, два вражески флота, които си разменят системите, не се срещат: докато единият напуска, другият още не е пристигнал. Битките са после, само там, където след движението има и двата флота в една система.
Програмата движи три флота (X, Y и Z) по верига от системи 0, 1, 2, 3 (заповеди към 3, 0 и 1) за два хода и печата къде са и кои се срещат. После прави същото грешно: мести флот по флот и проверява среща веднага след всяко преместване.
static int stepToward(int pos, int dest) { if (pos < dest) return pos + 1; if (pos > dest) return pos - 1; return pos;}static void report(int turn, String[] names, int[] pos) { String line = "Ход " + turn + ": "; for (int i = 0; i < pos.length; i++) line += (i > 0 ? ", " : "") + names[i] + " в " + pos[i]; System.out.println(line); for (int s = 0; s < 4; s++) { String here = ""; int count = 0; for (int i = 0; i < pos.length; i++) { if (pos[i] == s) here += (count++ > 0 ? ", " : "") + names[i]; } if (count > 1) System.out.println("среща в " + s + ": " + here); }}String[] names = {"X", "Y", "Z"};int[] dest = {3, 0, 1};int[] pos = {0, 1, 2};for (int turn = 1; turn <= 2; turn++) { int[] next = new int[pos.length]; for (int i = 0; i < pos.length; i++) next[i] = stepToward(pos[i], dest[i]); pos = next; report(turn, names, pos);}int[] live = {0, 1, 2};for (int i = 0; i < live.length; i++) { live[i] = stepToward(live[i], dest[i]); for (int j = 0; j < live.length; j++) { if (j != i && live[j] == live[i]) System.out.println("Един по един: " + 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 е „във въздуха“: корабите вече са напуснали флота си, но още не са пристигнали:
static void resolveMovement(World world) { List<Move> moving = new ArrayList<>(); for (Empire empire : world.empires) { if (!empire.alive) continue; for (Order order : empire.orders) { Fleet fleet = fleetAt(world, empire.id, order.from); if (fleet == null) continue; int[] ships = {0, 0, 0, 0}; for (int 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.add(new Move(empire.id, order.to, order.dest, ships)); } empire.orders = new ArrayList<>(); } for (Move move : moving) { addShips(world, move.owner, move.to, move.ships); // дългият път продължава сам: нова поръчка от мястото, където стигнаха if (move.to != move.dest) { world.empires.get(move.owner).orders.add(new Order(move.to, nextStep(world.systems, move.to, move.dest), move.dest, move.ships.clone())); } } removeEmptyFleets(world);}Идеята
Симулация на битка
Битката е чиста функция, затова не е нужно да я гледаш, за да я разбереш: можеш да я пускаш върху много флотове и да четеш таблица. Така се настройва балансът на играта. Програмата сравнява еднакви флоти от по 6 кораба на няколко двойки роли:
int[][] pairs = {{0, 1}, {1, 2}, {0, 2}, {0, 3}};for (int[] pair : pairs) { int[] a = new int[KINDS]; int[] b = new int[KINDS]; a[pair[0]] = 6; b[pair[1]] = 6; String head = countsText(a) + " срещу " + countsText(b) + ": "; runBattle(a, b, "a", "b", new ArrayList<>()); if (total(a) > 0 && total(b) == 0) System.out.println(head + "печели " + LETTERS.charAt(pair[0]) + ", остават " + countsText(a)); else if (total(b) > 0 && total(a) == 0) System.out.println(head + "печели " + LETTERS.charAt(pair[1]) + ", остават " + countsText(b)); else System.out.println(head + "без победител");}6I срещу 6B: печели I, остават 3I6B срещу 6F: печели B, остават 4B6I срещу 6F: печели F, остават 6F6I срещу 6D: печели D, остават 6DТриъгълникът се вижда в числата. Шест прехващача побеждават шест бомбардировача и губят три. Шест бомбардировача побеждават шест фрегати и губят два. Шест фрегати унищожават шест прехващача без нито една загуба: прехващачите падат още в първия рунд. Дредноутът (90 живот, атака 18) е друг случай: шест дредноута унищожават шест прехващача, също без загуба, защото всеки от тях издържа много повече (в играта той иска технология и повече ресурси). „Кой е по-силен“ няма един отговор: зависи от ролята на врага. Затова и силата на флота (сборът от силите на ролите) е само груба оценка.
Същото се прави в играта с цели игри: --simulate=200 пуска двеста игри без екран и печата кой печели. Това е темата на последния урок. Битките в тях са тази функция, пускана хиляди пъти.
Пробвай
Предвиди изхода
Три бомбардировача срещу три прехващача. Кой печели и колко рунда трае битката? Не пускай програмата: пресметни рундовете на ръка с таблиците от урока (живот 10 и атака 5 за прехващача, 16 и 7 за бомбардировача) и не забравяй раните.
int[] a = {0, 3, 0, 0, 0};int[] b = {3, 0, 0, 0, 0};List<String> log = new ArrayList<>();runBattle(a, b, "Земна империя", "Хегемония Орион", log);for (String line : log) System.out.println(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. Така не му трябват копията за залповете. Двубой от два прехващача срещу два бомбардировача печата:
int[] a = {2, 0, 0, 0, 0};int[] b = {0, 2, 0, 0, 0};List<String> log = new ArrayList<>();runBattle(a, b, "a", "b", log);for (String line : log) System.out.println(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: общото за всички · Списъци
- Задачи по темата: Epic Cosmos: империята