Към съдържанието
wannacode

Флотите и космическата битка

Накратко: Флотът е масив с брой кораби по роли. Ролите са камък-ножица-хартия (множител в проценти), всяка група стреля по най-изгодната цел, щетата, която не стига за цял кораб, остава като рана, а двата залпа на рунда се смятат от броя в началото му и удрят едновременно. Същото е и при ходовете: първо всички тръгват, после всички пристигат.

Проблем

Кой стреля пръв?

Два флота се срещат в една система. Първият отговор е на ход: стреля първият, после вторият. Но така първата страна печели само заради реда в кода: корабите, които тя унищожи, вече не отвръщат. Играта иска друго: в един рунд и двете страни стрелят едновременно, а флотовете се местят между системите също наведнъж.

Ето кадър от играта (командата battle, от минаването, с което се проверява играта): при Измар четири прехващача срещу два бомбардировача и две фрегати. Празните редове на екрана са съкратени.

Ход 28/150 Земна империя (A) точки 147
M3+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 int Kinds = 5;
const int Rounds = 6;
const string Letters = "IBFD#";
static readonly int[] Hp = { 10, 16, 30, 90, 25 };
static readonly int[] Atk = { 5, 7, 6, 18, 5 };
static readonly int[][] Mult =
{
new[] { 100, 175, 50, 50, 60 },
new[] { 50, 100, 175, 80, 150 },
new[] { 175, 50, 100, 80, 100 },
new[] { 110, 110, 110, 100, 110 },
new[] { 100, 100, 100, 100, 100 },
};

Две малки функции са навсякъде: сборът на корабите и текстът „3I 2B 2F“, който играта пише за един флот (празният е „-“). Силата на роля е живот × атака / 10; на флота е сборът по роли. Играта я ползва, за да сравнява флотове (изкуственият интелект, следващите уроци).

static int Total(int[] counts)
{
int sum = 0;
foreach (int n in 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.Length == 0 ? "" : " ") + counts[k] + Letters[k];
}
return text.Length == 0 ? "-" : 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 };
Console.WriteLine("Флот: " + CountsText(fleet));
Console.WriteLine("Кораби: " + Total(fleet));
Console.WriteLine("Сила: " + Power(fleet));
Console.WriteLine("Празен флот: " + CountsText(new int[Kinds]));
Изход
Флот: 3I 2B 2F
Кораби: 7
Сила: 73
Празен флот: -

Силата е 3 × 5 + 2 × 11 + 2 × 18 = 73. Прехващачът е 5, бомбардировачът 11, фрегатата 18, а дредноутът 162.

В C# Hp[kind] * Atk[kind] / 10 е целочислено, защото и трите са int. Таблицата на множителите е масив от масиви (int[][]).

В играта флотовете са в списък world.fleets, подреден по империя и после по система. fleetAt търси флота на империята в системата, а addShips го създава (на мястото му в реда) или добавя кораби към съществуващия:

public static Fleet? FleetAt(World world, int owner, int systemId)
{
foreach (Fleet fleet in world.Fleets)
{
if (fleet.Owner == owner && fleet.SystemId == systemId) return fleet;
}
return null;
}
// Добавя кораби към флота на империята в системата; нов флот влиза на мястото си в реда.
public static void AddShips(World world, int owner, int systemId, int[] counts)
{
Fleet? fleet = FleetAt(world, owner, systemId);
if (fleet == null)
{
fleet = new Fleet { Owner = owner, SystemId = systemId, Ships = new int[] { 0, 0, 0, 0 } };
int at = world.Fleets.Count;
while (at > 0)
{
Fleet before = world.Fleets[at - 1];
if (before.Owner < owner || (before.Owner == owner && before.SystemId < systemId)) break;
at--;
}
world.Fleets.Insert(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%): той е силен сам по себе си, не по роля.

// Множителят на щетата в проценти: ред е нападателят, колона е целта (прехващач, бомбардировач,
// фрегата, дредноут, крепост).
public static readonly int[][] MULT =
{
new int[] { 100, 175, 50, 50, 60 },
new int[] { 50, 100, 175, 80, 150 },
new int[] { 175, 50, 100, 80, 100 },
new int[] { 110, 110, 110, 100, 110 },
new int[] { 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);
Console.WriteLine(Names[kind] + " -> " + Names[target] + " " + Mult[kind][target] + "%");
}
int[] noFrigates = { 1, 1, 0, 1, 1 };
int alt = ChooseTarget(1, noFrigates);
Console.WriteLine("Без фрегати: " + Names[1] + " -> " + Names[alt] + " " + Mult[1][alt] + "%");
Изход
прехващач -> бомбардировач 175%
бомбардировач -> фрегата 175%
фрегата -> прехващач 175%
дредноут -> прехващач 110%
крепост -> прехващач 100%
Без фрегати: бомбардировач -> крепост 150%

Първите три реда са триъгълникът. Дредноутът и крепостта нямат роля, срещу която да са по-силни: най-големият им множител (110% и 100%) е на първото място в реда, прехващача. Последният ред е най-интересен: бомбардировачът иска фрегати, а ги няма, затова избира следващия по изгода, крепостта (150%), не прехващача (50%) и не дредноута (80%).

Ето функцията в играта:

public 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)
{
return string.Join(" ", values);
}

Програмата:

int[] a = { 4, 0, 0, 0, 0 };
int[] b = { 0, 2, 2, 0, 0 };
int[] hitB = Volley(a, b);
int[] hitA = Volley(b, a);
Console.WriteLine("a -> b: " + Numbers(hitB));
Console.WriteLine("b -> a: " + Numbers(hitA));
int[] woundA = new int[Kinds];
int[] woundB = new int[Kinds];
ApplyDamage(b, woundB, hitB);
ApplyDamage(a, woundA, hitA);
Console.WriteLine("a: " + CountsText(a) + ", рани " + Numbers(woundA));
Console.WriteLine("b: " + CountsText(b) + ", рани " + Numbers(woundB));
Изход
a -> b: 0 35 0 0 0
b -> a: 28 0 0 0 0
a: 2I, рани 8 0 0 0 0
b: 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 би препълнил:

// Щетата, която една страна нанася на другата, по роля на целта.
public static int[] Volley(SideStats stats, int[] mine, int[] enemy)
{
int[] damage = EmptyCounts();
for (int k = 0; k < KIND_COUNT; k++)
{
if (mine[k] == 0) continue;
int target = ChooseTarget(k, enemy);
if (target < 0) continue;
damage[target] += (int)(((long)mine[k] * stats.Atk[k] * MULT[k][target]) / 100);
}
return damage;
}
// Щетата се трупа: единиците губят живот, но първият оцелял пази раната си за следващия рунд.
public static void ApplyDamage(SideStats stats, int[] counts, int[] wound, int[] damage)
{
for (int k = 0; k < KIND_COUNT; k++)
{
if (damage[k] == 0 || counts[k] == 0) continue;
int total = wound[k] + damage[k];
int killed = 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];
}
}
}

Виж го

Целият рунд, едновременно

Сега всичко се събира. Цикълът на битката прави за всеки рунд следното:

  1. Ако някоя страна няма кораби, край.
  2. Копира броя на двете страни: това е „снимката“ на началото на рунда.
  3. Смята и двата залпа от снимките: залпа на a по b и залпа на b по a.
  4. Прилага щетата по b, после по a. Редът няма значение, защото залповете вече са изчислени.
  5. Записва реда на рунда: колко кораба са имали двете страни в началото и колко са загубили.

След шест рунда, ако още има кораби и от двете страни, няма победител. Ето цялата функция и програмата, която прави битката при Измар:

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 = (int[])a.Clone();
int[] startB = (int[])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 = "Хегемония Орион";
var log = new List<string>();
RunBattle(a, b, nameA, nameB, log);
Console.WriteLine("Битка при " + where + ": " + nameA + " срещу " + nameB);
foreach (string line in log) Console.WriteLine(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 = (int[])a.Clone();
int[] startB = (int[])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) + ".");
}
Стъпка по стъпка, с думи
  1. Два флота при Измар: a има 4I, b има 2B 2F. Флотът е само броят кораби по роли, а битката е функция на два такива масива.
  2. Рунд 1. Всяка страна копира броя си в началото на рунда. И двата залпа ще се смятат от тези копия.
  3. Всяка роля избира цел с най-голям множител. a: 4I → B (175%). b: 2B → I (50%), 2F → I (175%). Камък-ножица-хартия.
  4. Залповете: a: 4 × 5 × 175 / 100 = 35 по бомбардировачите. b: 2 × 7 × 50 / 100 = 7; 2 × 6 × 175 / 100 = 21 по прехващачите, общо 28. Удрят едновременно.
  5. Щетата се прилага: 35 / 16 = 2 бомбардировача падат. 28 / 10 = 2 прехващача падат, остатъкът 8 е рана. Дневникът: загуби 2 / 2.
  6. Рунд 2: 2I срещу 2F. Бомбардировачите ги няма, затова целите се избират наново.
  7. Прехващачите бият фрегатите само с 50%: 2 × 5 × 50 / 100 = 5. Фрегатите бият прехващачите със 175%: 2 × 6 × 175 / 100 = 21.
  8. Щетата по прехващачите е 21 плюс раната 8 = 29: падат и двата. Фрегатите получават 5, по-малко от живота 30: рана 5, никоя не пада.
  9. a няма кораби, b има 2F: Победа за Хегемония Орион. Остават: 2F. Така е и в играта. Битката е чиста функция.

В играта битката е същата функция, само с повече параметри (stats за технологиите), и връща списък от рундове, а текстът се пише от друга функция. Ето я:

public static BattleResult RunBattle(SideStats statsA, int[] countsA, SideStats statsB, int[] countsB)
{
int[] a = (int[])countsA.Clone();
int[] b = (int[])countsB.Clone();
int[] woundA = EmptyCounts();
int[] woundB = EmptyCounts();
List<BattleRound> rounds = new List<BattleRound>();
for (int round = 1; round <= BATTLE_ROUNDS; round++)
{
if (TotalOf(a) == 0 || TotalOf(b) == 0) break;
int[] startA = (int[])a.Clone();
int[] startB = (int[])b.Clone();
int[] hitB = Volley(statsA, startA, startB);
int[] 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 BattleRound { A = startA, B = startB, LostA = lostA, LostB = 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 = 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) за два хода и печата къде са и кои се срещат. После прави същото грешно: мести флот по флот и проверява среща веднага след всяко преместване.

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];
Console.WriteLine(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) Console.WriteLine("среща в " + 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]) Console.WriteLine("Един по един: " + 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 е „във въздуха“: корабите вече са напуснали флота си, но още не са пристигнали:

public static void ResolveMovement(World world)
{
List<Move> moving = new List<Move>();
foreach (Empire empire in world.Empires)
{
if (!empire.Alive) continue;
foreach (Order order in 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 { Owner = empire.Id, To = order.To, Dest = order.Dest, Ships = ships });
}
empire.Orders = new List<Order>();
}
foreach (Move move in moving)
{
AddShips(world, move.Owner, move.To, move.Ships);
// дългият път продължава сам: нова поръчка от мястото, където стигнаха
if (move.To != move.Dest)
{
world.Empires[move.Owner].Orders.Add(new Order { From = move.To, To = NextStep(world.Systems, move.To, move.Dest), Dest = move.Dest, Ships = (int[])move.Ships.Clone() });
}
}
RemoveEmptyFleets(world);
}

Идеята

Симулация на битка

Битката е чиста функция, затова не е нужно да я гледаш, за да я разбереш: можеш да я пускаш върху много флотове и да четеш таблица. Така се настройва балансът на играта. Програмата сравнява еднакви флоти от по 6 кораба на няколко двойки роли:

int[][] pairs = { new[] { 0, 1 }, new[] { 1, 2 }, new[] { 0, 2 }, new[] { 0, 3 } };
foreach (int[] pair in 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 List<string>());
if (Total(a) > 0 && Total(b) == 0) Console.WriteLine(head + "печели " + Letters[pair[0]] + ", остават " + CountsText(a));
else if (Total(b) > 0 && Total(a) == 0) Console.WriteLine(head + "печели " + Letters[pair[1]] + ", остават " + CountsText(b));
else Console.WriteLine(head + "без победител");
}
Изход
6I срещу 6B: печели I, остават 3I
6B срещу 6F: печели B, остават 4B
6I срещу 6F: печели F, остават 6F
6I срещу 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 };
var log = new List<string>();
RunBattle(a, b, "Земна империя", "Хегемония Орион", log);
foreach (string line in log) Console.WriteLine(line);
Отговор

Пробвай

Намери грешката

Колега реши да „пооправи“ рунда: щом залпът на a е изчислен, веднага го прилага, и чак след това смята залпа на b. Така не му трябват копията за залповете. Двубой от два прехващача срещу два бомбардировача печата:

int[] a = { 2, 0, 0, 0, 0 };
int[] b = { 0, 2, 0, 0, 0 };
var log = new List<string>();
RunBattle(a, b, "a", "b", log);
foreach (string line in log) Console.WriteLine(line);
Изход
Рунд 1: 2I | 2B загуби 0 / 1
Рунд 2: 2I | 1B загуби 0 / 1
Победа за a. Остават: 2I.

Вторият рунд свършва с „загуби 0 / 1“, а победата оставя два прехващача. Преди промяната вторият рунд е „загуби 1 / 1“ и остава един прехващач (Рунд 2: 2I | 1B загуби 1 / 1). Кой ред се изпълнява твърде рано?

Избери реда с грешката.

По-точно

Какво не е в тази система

Битката в урока е ядрото; в играта има още няколко неща, които не променят принципа.

Технологиите дават бонуси на атаката и на живота (и допълнителни за бомбардировачите и за фрегатите). Те влизат през 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 трябват, за да го унищожат?
  • Празен флот. Какво прави програмата, ако едната страна няма кораби? Не трябва да се чупи.

Тест

Провери се

1. Защо и двата залпа на рунда се смятат от броя кораби в началото на рунда, а не единият след другия?
2. 6 бомбардировача (атака 7) стрелят по фрегати. Множителят на бомбардировача срещу фрегата е 175%. Колко щета нанасят?
3. Три прехващача (живот 10 всеки) получават общо 25 щета и нямат стара рана. Какво остава?
4. Прехващачите нямат пред себе си нито бомбардировачи, нито прехващачи, а само фрегати и дредноути. Множителите им са 50 срещу фрегата и 50 срещу дредноут. Кого избират?
5. Флот x е в система 1 и има заповед за система 2. Флот y на врага е в система 2 и има заповед за система 1. Двете страни са във война. Какво става в хода?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·