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

Съперниците

Накратко: Съперникът оценява с число всяко действие, което може да плати, взима най-голямото, плаща и оценява наново. Характерът на империята са теглата в същите формули.

Проблем

Съперник, който не мисли

В Epic Cosmos играеш срещу три империи. Докато ти пишеш команди, всяка от тях решава какво да прави на този ход: да постави мина, да изгради лаборатория, да заселва нова планета, да поръча кораби, да прати флота. Как се прави такъв съперник?

Първият опит е сценарий: „ход 1 мина, ход 2 мина, ход 3 колония…“. Играчът го разбира за три партии и после го бие всеки път. Вторият опит е веригата „ако … иначе“: ако има пари за колония, колонизирай; иначе ако е в опасност, строй кораби; иначе… Всяко ново действие иска нов ред и нови изключения към другите, а съперникът пак е предвидим и прави глупости, защото никой не е предвидил точно това положение.

Има и по-хубав начин. Дай на всяко възможно действие число: колко е добро за тази империя сега. Направи действието с най-голямото число. „Колония или кораби?“ е сравнение на две числа, а не правило за тази двойка. Нов вид действие е нов ред в списъка с оценки, без да се пипа останалото.

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

Идеята

Числото за всяко действие

Ето положението, с което работи целият урок. Ход 5, една империя с две планети (Кора I със завод за кораби и Кора II с една мина), още две свободни планети в същата система (Веста I и II), 100 метал и сила на флота 10:

record Option(string Kind, string Target, int Utility);
class Planet
{
public readonly string Name;
public readonly int Rich;
public int Mines;
public bool Lab;
public readonly bool Yard;
public Planet(string name, int rich, int mines, bool yard)
{
Name = name;
Rich = rich;
Mines = mines;
Yard = yard;
}
}
class Empire
{
public int Metal = 100;
public int Power = 10;
public int Turn = 5;
public readonly List<Planet> Owned = new();
public readonly List<Planet> Free = new();
}
const int MAX_MINE = 3;
const int LAB_COST = 40;
const int SHIP_COST = 8;
const int SHIP_POWER = 5;
static Empire NewEmpire()
{
var empire = new Empire();
empire.Owned.Add(new Planet("Kora I", 4, 0, true));
empire.Owned.Add(new Planet("Kora II", 3, 1, false));
empire.Free.Add(new Planet("Vesta I", 3, 0, false));
empire.Free.Add(new Planet("Vesta II", 1, 0, false));
return empire;
}

Какво може да направи тази империя? Да построи мина на планета (струва 15 × (мини + 1)), да построи лаборатория (40), да заселва колония на свободна планета (с всяка планета струва повече: сега 52) или да поръча кораби. Всяко от тях получава оценка, а оценките са формули. Първо двете постройки:

  • мина = строеж × богатство / (4 × (мини + 1)): богатата планета е по-ценна, а всяка нова мина на същата планета е по-малко ценна (делителят расте);
  • лаборатория = наука × 6 / 5 − 40 × лаборатории: втората лаборатория е с 40 по-малко ценна от първата.

После колониите и корабите:

  • колония = разширяване × (богатство + 6) × 50 / (цена × 6): свободната богата планета е примамлива, а скъпата колония е по-малко;
  • кораби = 60 + нападение / 2 + (желана сила − сила) / 2 (последният член е най-много 100), но само докато силата е под желаната. Желаната сила расте с хода: (20 + 3 × ход) × (140 + нападение) / 240.
static List<Option> Options(Empire empire, Personality p)
{
// Всяко действие, което империята може да плати, получава оценка.
var list = new List<Option>();
int labs = 0;
foreach (Planet planet in empire.Owned)
{
if (planet.Lab) labs++;
}
foreach (Planet planet in empire.Owned)
{
if (planet.Mines < MAX_MINE && empire.Metal >= MineCost(planet))
{
list.Add(new Option("mine", planet.Name, p.Build * planet.Rich / (4 * (planet.Mines + 1))));
}
if (!planet.Lab && empire.Metal >= LAB_COST)
{
list.Add(new Option("lab", planet.Name, p.Research * 6 / 5 - 40 * labs));
}
}
int cost = ColonyCost(empire);
if (empire.Metal >= cost)
{
foreach (Planet planet in empire.Free)
{
int value = planet.Rich + 6;
list.Add(new Option("colony", planet.Name, p.Expand * value * 50 / (cost * 6)));
}
}
int want = WantedPower(empire, p);
foreach (Planet planet in empire.Owned)
{
if (planet.Yard && empire.Power < want && ShipCount(empire) > 0)
{
int urge = Math.Min(100, (want - empire.Power) / 2);
list.Add(new Option("ships", planet.Name, 60 + p.Attack / 2 + urge));
break;
}
}
return list;
}

Цялата функция е това. Тя не избира: само събира в списък всичко, което империята може да плати, с оценките му. Условието „може да плати“ е пред оценката: действие, което не се плаща, не влиза в списъка.

Option е запис (record): три полета и нищо повече, а Planet и Empire са класове с изменяеми полета. Целочисленото деление отрязва остатъка, което тук е точно каквото искаме: всички числа са положителни.
По-точно, за любопитните

Формулите са нагласени, не изведени: коефициентите 4, 50, 6 и 60 са избрани така, че оценките на мина, колония и кораби да са сравними (стотици), а не да тежи едното хиляди пъти повече. Затова последният урок пуска стотици игри на изкуствен интелект срещу изкуствен интелект и мери дали някой характер не е нагласен.

В играта ресурсите са четири (метал, газ, кристал, уран), а богатството на планетата е четири числа. Тук има метал и едно богатство, за да се вижда всяко число.

Идеята

Най-голямото число печели: строго

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

static Option? Best(List<Option> list, int floor)
{
// Печели строго по-голямата оценка, затова при равни е първото.
Option? pick = null;
int bestU = floor;
foreach (Option option in list)
{
if (option.Utility > bestU)
{
pick = option;
bestU = option.Utility;
}
}
return pick;
}
static string Label(Option option)
{
if (option.Kind == "mine") return "мина " + option.Target;
if (option.Kind == "lab") return "лаборатория " + option.Target;
if (option.Kind == "colony") return "колония " + option.Target;
return "кораби " + option.Target;
}

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

foreach (Personality p in personalities)
{
List<Option> list = Options(NewEmpire(), p);
Console.WriteLine(p.Name);
foreach (Option option in list)
{
Console.WriteLine(" " + Pad(Label(option), 21) + PadLeft(option.Utility.ToString(), 4));
}
Option? pick = Best(list, 0);
Console.WriteLine(" → " + Label(pick!) + " (" + pick!.Utility + ")");
}
Изход
Воин
мина Kora I 90
лаборатория Kora I 72
мина Kora II 33
лаборатория Kora II 72
колония Vesta I 115
колония Vesta II 89
кораби Kora I 162
→ кораби Kora I (162)
Строител
мина Kora I 140
лаборатория Kora I 96
мина Kora II 52
лаборатория Kora II 96
колония Vesta I 230
колония Vesta II 179
кораби Kora I 93
→ колония Vesta I (230)
Учен
мина Kora I 100
лаборатория Kora I 132
мина Kora II 37
лаборатория Kora II 132
колония Vesta I 100
колония Vesta II 78
кораби Kora I 99
→ лаборатория Kora I (132)

Едно и също положение, три различни избора. Воинът избира корабите (162): желаната му сила е 45, а флотата е само 10. Строителят избира колонията на Веста I (230), защото разширяването му тежи 160. Ученият избира лабораторията на Кора I (132): лабораторията на Кора II има същата оценка и остава втора.

Идеята

Характерът е таблица с тегла

Какво се различава между трите империи в кода? Само четири числа. Ето ги от канона на играта, PERSONALITIES_DATA:

Характер разширяване строеж нападение наука мир
Воин 80 90 170 60 40
Строител 160 140 50 80 120
Учен 70 100 60 110 110

Колоната „мир“ е за дипломацията (урок 5). Останалите четири се умножават в оценките: разширяване в колониите, строеж в мините, нападение в корабите и в нападението на флота, наука в лабораториите. Воинът не е програмиран да напада: нападението му просто тежи 170, а при строителя 50. Нов характер е нов ред в таблицата, без нов код.

В играта съперниците се виждат в командата emp (характерът е втората колона):

Империя Характер Пл Сила Тех Точки Към теб
A Земна империя Строител 1 109 0 20
C Съюз Лира Строител 1 109 0 20 мир
D Академия Вега Учен 1 109 0 20 мир
B Хегемония Орион Воин 1 109 0 20 мир

Виж го

Оценката в действие

Сцената минава през четири етапа. Първите три са една и съща империя в ход 5, оценена от воина, строителя и учения. Редовете са действията в реда на списъка, лентите са оценките, а оранжевата черта е „най-доброто досега“: тя се мести само при строго по-голямо число. В четвъртия етап строителят вече е платил за колонията и оценява наново.

static List<Option> Options(Empire empire, Personality p)
{
// Всяко действие, което империята може да плати, получава оценка.
var list = new List<Option>();
int labs = 0;
foreach (Planet planet in empire.Owned)
{
if (planet.Lab) labs++;
}
foreach (Planet planet in empire.Owned)
{
if (planet.Mines < MAX_MINE && empire.Metal >= MineCost(planet))
{
list.Add(new Option("mine", planet.Name, p.Build * planet.Rich / (4 * (planet.Mines + 1))));
}
if (!planet.Lab && empire.Metal >= LAB_COST)
{
list.Add(new Option("lab", planet.Name, p.Research * 6 / 5 - 40 * labs));
}
}
int cost = ColonyCost(empire);
if (empire.Metal >= cost)
{
foreach (Planet planet in empire.Free)
{
int value = planet.Rich + 6;
list.Add(new Option("colony", planet.Name, p.Expand * value * 50 / (cost * 6)));
}
}
int want = WantedPower(empire, p);
foreach (Planet planet in empire.Owned)
{
if (planet.Yard && empire.Power < want && ShipCount(empire) > 0)
{
int urge = Math.Min(100, (want - empire.Power) / 2);
list.Add(new Option("ships", planet.Name, 60 + p.Attack / 2 + urge));
break;
}
}
return list;
}
static Option? Best(List<Option> list, int floor)
{
// Печели строго по-голямата оценка, затова при равни е първото.
Option? pick = null;
int bestU = floor;
foreach (Option option in list)
{
if (option.Utility > bestU)
{
pick = option;
bestU = option.Utility;
}
}
return pick;
}
Стъпка по стъпка, с думи
  1. Ход 5: метал 100, сила 10. Империята оценява всяко действие, което може да плати. Първият е воинът с тегла 80/90/170/60: строеж 90, нападение 170.
  2. Воинът: мина на Кора I = строеж × богатство / (4 × (мини + 1)) = 90; на Кора II 33, защото там вече има една мина. Лабораторията е наука × 6 / 5 = 72, еднаква на двете планети.
  3. Колония на Веста I = разширяване × (богатство + 6) × 50 / (цена × 6) = 115 при цена 52. Корабите: желаната сила е 45, а има 10, затова 60 + нападение / 2 + (45 − 10) / 2 = 162.
  4. Най-голямата оценка е 162: воинът строи кораби. Чертата „най-доброто досега“ се премести три пъти (мината 90, колонията 115, корабите 162), всеки път при строго по-голяма оценка.
  5. Същият свят, други тегла. Строителят (160/140/50/80) цени строежа: мина на Кора I 140, лаборатория 96. Планетите, парите и формулите са същите.
  6. Колониите са 230 и 179: разширяването тежи 160. Корабите са само 93, защото желаната сила на строителя е 27 (нападението му е 50).
  7. Строителят избира колонията на Веста I (230). Воинът с точно същите числа избра кораби.
  8. Ученият (70/100/60/110) цени науката: лабораторията е 132 на двете планети, мината на Кора I 100.
  9. Колониите са 100 и 78, корабите 99: желаната сила е 29.
  10. Две лаборатории с една и съща оценка 132. Втората не е строго по-голяма, затова чертата остава на първата: печели лабораторията на Кора I.
  11. Строителят плати колонията: остават 48 метал. Следващата колония струва вече 64 (всяка планета я прави по-скъпа), не може да се плати и няма оценка: редът ѝ е празен. Оценява се наново.
  12. Мината на Кора I пак е 140, корабите 93. Сега 48 метал стигат за мина, лаборатория или кораби, но не за колония.
  13. Втора стъпка от същия ход: строителят строи мина на Кора I (140). Цикълът е: оцени всичко, направи най-доброто, плати и оцени наново.

Кодът до сцената е options и best от програмата. На всяка стъпка светят редовете, които работят в този момент.

Идеята

Плащаш и пак смяташ

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

static Planet Find(List<Planet> planets, string name)
{
foreach (Planet planet in planets)
{
if (planet.Name == name) return planet;
}
return null!;
}
static void Apply(Empire empire, Option option)
{
if (option.Kind == "mine")
{
Planet planet = Find(empire.Owned, option.Target);
empire.Metal -= MineCost(planet);
planet.Mines++;
}
else if (option.Kind == "lab")
{
empire.Metal -= LAB_COST;
Find(empire.Owned, option.Target).Lab = true;
}
else if (option.Kind == "colony")
{
Planet planet = Find(empire.Free, option.Target);
empire.Metal -= ColonyCost(empire);
empire.Free.Remove(planet);
empire.Owned.Add(planet);
}
else
{
int count = ShipCount(empire);
empire.Metal -= count * SHIP_COST;
empire.Power += count * SHIP_POWER;
}
}
static void EndTurn(Empire empire)
{
int income = 4;
foreach (Planet planet in empire.Owned)
{
income += planet.Rich * (2 + planet.Mines) / 2;
}
empire.Metal += income;
empire.Turn++;
}
static void PlayTurn(Empire empire, Personality p, bool log)
{
// Плащане, нова оценка, и така докато има какво да се прави (най-много 6 пъти).
for (int step = 1; step <= 6; step++)
{
Option? option = Best(Options(empire, p), 0);
if (option == null)
{
if (log) Console.WriteLine(" няма какво повече: метал " + empire.Metal);
return;
}
int before = empire.Metal;
Apply(empire, option);
if (log)
{
Console.WriteLine(" " + step + ". " + Label(option) + " (" + option.Utility + "): метал " + before + " → " + empire.Metal);
}
}
}
static void Summarize(Personality p)
{
var empire = NewEmpire();
for (int turn = 0; turn < 6; turn++)
{
PlayTurn(empire, p, false);
EndTurn(empire);
}
int mines = 0;
int labs = 0;
foreach (Planet planet in empire.Owned)
{
mines += planet.Mines;
if (planet.Lab) labs++;
}
Console.WriteLine(p.Name + ": планети " + empire.Owned.Count + ", мини " + mines + ", лаб. " + labs
+ ", сила " + empire.Power + ", метал " + empire.Metal);
}

Ето един ход на строителя, действие по действие, и след това какво са постигнали трите характера за шест хода:

Personality builder = personalities[1];
var home = NewEmpire();
Console.WriteLine(builder.Name + ", ход " + home.Turn + ":");
PlayTurn(home, builder, true);
Console.WriteLine();
Console.WriteLine("След 6 хода:");
foreach (Personality p in personalities) Summarize(p);
Изход
Строител, ход 5:
1. колония Vesta I (230): метал 100 → 48
2. мина Kora I (140): метал 48 → 33
3. мина Vesta I (105): метал 33 → 18
4. кораби Kora I (93): метал 18 → 2
няма какво повече: метал 2
След 6 хода:
Воин: планети 2, мини 4, лаб. 0, сила 75, метал 19
Строител: планети 3, мини 3, лаб. 1, сила 40, метал 38
Учен: планети 2, мини 4, лаб. 1, сила 45, метал 21

Първото действие е колонията (230), която взима 52 метал. После оценките са други: следващата колония вече не се плаща, а най-доброто е мината на Кора I (140). След 4 действия парите не стигат за нищо и ходът свършва. След шест хода разликите са реални, но не драматични: воинът има най-голяма сила (75) и няма лаборатория, строителят има най-много планети (3), ученият е с лаборатория и 4 мини. Теглата не изключват нищо: и строителят построява лаборатория, когато му е останало най-доброто.

Има и граница, която трябва да знаеш: изборът е алчен. Съперникът не мисли „ще спестя за колонията“, а взима най-доброто сега: строителят купува два малки кораба (оценка 93), вместо да чака.

Идеята

Флотата: заселвай или напади

Строежът е половината. Другата са флотите: накъде да отиде флотът? Същият похват, друг списък. Кандидатите са системите наоколо, а действията са два вида: заселване (в системата има свободни планети и няма враг) и нападение (има вражески планети и сме достатъчно силни). Преди оценката има филтри: нападение, при което съотношението на силите е под 130% или целта е по-далече от 6, изобщо не се разглежда.

const int FLEET_POWER = 40;
const int STAY = 20;
var spots = new List<Spot>
{
new("Brisa", 1, 12, 0, 0),
new("Altan", 5, 0, 12, 10),
new("Cygra", 3, 0, 18, 60),
};
static List<Option> TargetOptions(int power, Personality p, List<Spot> spots)
{
var list = new List<Option>();
foreach (Spot spot in spots)
{
int steps = spot.Distance + 1;
if (spot.FreeValue > 0 && spot.EnemyPower == 0 && spot.Distance <= 5)
{
list.Add(new Option("settle", spot.Name, p.Expand * spot.FreeValue / (6 * steps)));
}
int ratio = power * 100 * 100 / (spot.EnemyPower * 100 + 100);
if (spot.EnemyValue > 0 && ratio >= 130 && spot.Distance <= 6)
{
int sure = Math.Min(ratio, 300);
list.Add(new Option("attack", spot.Name, p.Attack * spot.EnemyValue * sure / (4 * steps * 150)));
}
}
return list;
}

Флотата има сила 40. Системата Brisa е свободна и близо (разстояние 1), Altan е вражеска и далече (5, но врагът е слаб), а Cygra е вражеска със сила 60. Съотношението на силите срещу Altan е 363%, но в оценката се вдига най-много до 300%. Срещу Cygra е 65%, под 130%: нападение там не се оценява. Този път прагът е STAY, 20: ако нищо не го надхвърли, флотът стои.

foreach (Personality p in personalities)
{
List<Option> list = TargetOptions(FLEET_POWER, p, spots);
Console.WriteLine(p.Name);
foreach (Option option in list)
{
Console.WriteLine(" " + Pad(Label(option), 18) + PadLeft(option.Utility.ToString(), 4));
}
Option? pick = Best(list, STAY);
Console.WriteLine(" → " + (pick == null ? "стои" : Label(pick) + " (" + pick.Utility + ")"));
}
Изход
Воин
заселва Brisa 80
напада Altan 170
→ напада Altan (170)
Строител
заселва Brisa 160
напада Altan 50
→ заселва Brisa (160)
Учен
заселва Brisa 70
напада Altan 60
→ заселва Brisa (70)

Воинът напада Altan (170), въпреки разстоянието. Строителят заселва Brisa (160). Ученият също заселва Brisa, и то на косъм: 70 срещу 60. Формулите са същите, различни са само теглата.

Идеята

В играта

Това, което построихме, е същото като в канона на Epic Cosmos, само по-малко. Ето истинската оценка на икономиката. В нея се познават мината, лабораторията, колонията и корабите от нашата програма. Освен тях има още две действия: завод за кораби (yard, оценка 250 за първия, защото без него няма флот) и крепост (fort, само когато наблизо има вражеска сила). Изборът е същата идея, но с consider(оценка, действие) вместо списък:

// Едно действие в икономиката: най-доброто по полезност, или null.
public static AiAction? AiBestAction(World world, Empire empire)
{
Personality p = empire.Personality;
AiAction? best = null;
int bestU = 0;
void Consider(int u, AiAction action)
{
if (u > bestU)
{
bestU = u;
best = action;
}
}
List<Planet> mine = OwnedPlanets(world, empire.Id);
int labs = 0;
int yards = 0;
foreach (Planet planet in mine)
{
labs += planet.Lab;
yards += planet.Yard;
}
Dictionary<string, int> mods = TechMods(empire);
foreach (Planet planet in mine)
{
if (planet.Mines < MAX_MINE && CanPay(empire, BuildCost("mine", planet)))
{
Consider((p.Build * RichSum(planet)) / (4 * (planet.Mines + 1)), new AiAction { Kind = "mine", PlanetId = planet.Id });
}
if (planet.Lab == 0 && CanPay(empire, BuildCost("lab", planet)))
{
Consider((p.Research * 6) / 5 - 40 * labs, new AiAction { Kind = "lab", PlanetId = planet.Id });
}
if (planet.Yard == 0 && CanPay(empire, BuildCost("yard", planet)))
{
Consider(yards == 0 ? 250 : mine.Count >= 5 && yards < 2 ? 60 : 0, new AiAction { Kind = "yard", PlanetId = planet.Id });
}
if (mods["fort"] > 0 && planet.Fort < MAX_FORT && CanPay(empire, BuildCost("fort", planet)))
{
int threat = EnemyPowerAt(world, empire, planet.SystemId);
foreach (int next in world.Systems[planet.SystemId].Links) threat += EnemyPowerAt(world, empire, next);
if (threat > 0) Consider(40 + MinOf(160, threat) + (planet.Id == empire.Capital ? 30 : 0), new AiAction { Kind = "fort", PlanetId = planet.Id });
}
}
int[] cost = ColonyCost(world, empire);
if (CanPay(empire, cost))
{
foreach (Planet planet in world.Planets)
{
if (planet.Owner >= 0 || !HasPresence(world, empire.Id, planet.SystemId) || HostileIn(world, empire.Id, planet.SystemId)) continue;
Consider((p.Expand * (RichSum(planet) + 6) * 50) / (cost[0] * 6), new AiAction { Kind = "col", PlanetId = planet.Id });
}
}
// Кораби: докато силата е под желаната за този ход.
int want = ((20 + 3 * world.Turn) * (140 + p.Attack)) / 240;
foreach (Empire other in world.Empires)
{
if (other.Alive && other.Id != empire.Id && empire.War[other.Id] == 1) want = (want * 3) / 2;
}
int have = EmpirePower(world, empire.Id) + QueuedPower(world, empire.Id);
if (have < want)
{
foreach (Planet planet in mine)
{
if (planet.Yard == 0) continue;
int kind = CounterKind(world, empire);
if (ShipAllowed(empire, 3) && CanPay(empire, SHIPS[3].Cost) && have >= 30) kind = 3;
int count = 4;
while (count > 0 && !CanPay(empire, ScaleCost(SHIPS[kind].Cost, count))) count--;
if (count > 0) Consider(60 + p.Attack / 2 + MinOf(100, (want - have) / 2), new AiAction { Kind = "ship", PlanetId = planet.Id, ShipKind = kind, Count = count });
break;
}
}
return best;
}

А ето цикълът „оцени, направи, повтори“ (aiEconomy) и целият ход на един съперник (aiPlay): наука, икономика, флоти. Науката е същият похват в малък: aiResearch оценява всяка достъпна технология с наука × (100 + бонус на характера) × 10 / (цена + 5) и взима най-голямата (дървото на технологиите е в урок 2).

public static void AiEconomy(World world, Empire empire)
{
for (int step = 0; step < 6; step++)
{
AiAction? action = AiBestAction(world, empire);
if (action == null) return;
Planet planet = world.Planets[action.PlanetId];
string? error = null;
if (action.Kind == "col") error = Colonize(world, empire, planet);
else if (action.Kind == "ship") error = QueueShips(world, empire, planet, action.ShipKind, action.Count);
else error = BuildStructure(world, empire, action.Kind, planet);
if (error != null) return;
}
}
public static void AiPlay(World world, Empire empire)
{
// Изкуственият интелект планира наново всеки ход.
empire.Orders = new List<Order>();
AiResearch(world, empire);
AiEconomy(world, empire);
AiFleets(world, empire);
}

Разликите от нашата програма са малко:

  • Четири ресурса вместо един. canPay сравнява четири числа, а богатството на планетата е четири числа (richSum ги събира и тежи урана и кристала двойно).
  • Едни и същи функции с играча. Съперникът вика colonize, queueShips и buildStructure, които вика и играчът с командите col, ship и b. Плаща по същите цени и по същите правила.
  • Войната променя желаната сила. За всяка империя, с която сме във война, желаната сила се умножава по 3/2, а видът на корабите се избира по врага (counterKind: прехващач бие бомбардировач, бомбардировач бие фрегата, фрегата бие прехващач; ролите са от урок 3).
  • Защитата е в оценките. Крепост се строи, ако наблизо има враг, а флотът се връща да брани собствена система (ownHere).
  • Няма случайност. Нито едно зарче в тези решения: един и същ вход дава един и същ ход на Java, C#, Python и JavaScript.

А ето и избора на цел на флота:

public static void AiFleets(World world, Empire empire)
{
Personality p = empire.Personality;
List<Fleet> snapshot = new List<Fleet>();
foreach (Fleet fleet in world.Fleets)
{
if (fleet.Owner == empire.Id) snapshot.Add(fleet);
}
foreach (Fleet fleet in snapshot)
{
int[] busy = OrderedShips(empire, fleet.SystemId);
int[] free = { 0, 0, 0, 0 };
for (int k = 0; k < SHIP_KIND_COUNT; k++) free[k] = fleet.Ships[k] - busy[k];
int myPower = CountsPower(free);
if (myPower == 0) continue;
int here = fleet.SystemId;
int[] dist = Bfs(world.Systems, here).Dist;
// Задържане: ако сме у дома под заплаха или обсаждаме враг.
int bestU = 20;
int target = -1;
bool split = false;
bool siege = false;
foreach (StarSystem system in world.Systems)
{
int foe = EnemyPowerAt(world, empire, system.Id);
int freeValue = 0;
int enemyValue = 0;
bool ownHere = false;
foreach (int pid in system.Planets)
{
Planet planet = world.Planets[pid];
if (planet.Owner < 0) freeValue += RichSum(planet) + 6;
else if (planet.Owner == empire.Id) ownHere = true;
else if (empire.War[planet.Owner] == 1) enemyValue += RichSum(planet) + 10;
}
int d = dist[system.Id];
if (d < 0) continue;
int ratio = Percent((long)myPower * 100, (long)foe * 100 + 100);
if (system.Id == here)
{
if (enemyValue > 0 && ratio >= 80)
{
bestU = 250;
siege = true;
}
else if (ownHere && foe > 0 && ratio >= 60)
{
bestU = 200;
siege = true;
}
continue;
}
if (freeValue > 0 && foe == 0 && d <= 5)
{
int u = (p.Expand * freeValue) / (6 * (d + 1));
if (u > bestU)
{
bestU = u;
target = system.Id;
split = TotalOf(free) >= 6;
siege = false;
}
}
if (enemyValue > 0 && ratio >= 130 && d <= 6)
{
int u = (p.Attack * enemyValue * MinOf(ratio, 300)) / (4 * (d + 1) * 150);
if (u > bestU)
{
bestU = u;
target = system.Id;
split = false;
siege = false;
}
}
if (ownHere && foe > 0 && d <= 4 && ratio >= 60)
{
int u = 150 - 10 * d;
if (u > bestU)
{
bestU = u;
target = system.Id;
split = false;
siege = false;
}
}
}
if (target < 0 || siege) continue;
int step = NextStep(world.Systems, here, target);
if (step < 0) continue;
OrderMove(world, empire, here, target, split ? HalfOf(free) : null);
}
}

Прилича на нашата програма: същите две формули (заселване и нападение), филтърът 130%, прагът 20. Допълнително има „задържане“ (в система с врагове при добро съотношение флотът остава и обсажда: 250), защита на собствена система (150 − 10 × разстояние) и разделяне на флота на две, когато е голям.

Пробвай

Предвиди изхода

Четири действия, две от тях с най-високата оценка 132. Какво печата програмата? (Функциите best и label са от урока.)

var tie = new List<Option>
{
new("mine", "Kora I", 90),
new("lab", "Kora I", 132),
new("lab", "Kora II", 132),
new("colony", "Vesta I", 100),
};
Option? winner = Best(tie, 0);
Console.WriteLine(Label(winner!) + " (" + winner!.Utility + ")");
Отговор

Пробвай

Попълни празнината

Началната стойност на най-доброто е параметърът на функцията: за икономиката е 0, за флота е 20. С какво започва bestU?

static Option? Best(List<Option> list, int floor)
{
    // Печели строго по-голямата оценка, затова при равни е първото.
    Option? pick = null;
    int bestU = ;
    foreach (Option option in list)
    {
        if (option.Utility > bestU)
        {
            pick = option;
            bestU = option.Utility;
        }
    }
    return pick;
}

Пробвай

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

Колега смята колониите без скоби и сега всяка империя, дори воинът, само колонизира. Ето оценките на строителя:

foreach (Personality p in personalities)
{
List<Option> list = Options(NewEmpire(), p);
Console.WriteLine(p.Name);
foreach (Option option in list)
{
Console.WriteLine(" " + Pad(Label(option), 21) + PadLeft(option.Utility.ToString(), 4));
}
Option? pick = Best(list, 0);
Console.WriteLine(" → " + Label(pick!) + " (" + pick!.Utility + ")");
}
Изход
Воин
мина Kora I 90
лаборатория Kora I 72
мина Kora II 33
лаборатория Kora II 72
колония Vesta I 4152
колония Vesta II 3228
кораби Kora I 162
→ колония Vesta I (4152)
Строител
мина Kora I 140
лаборатория Kora I 96
мина Kora II 52
лаборатория Kora II 96
колония Vesta I 8304
колония Vesta II 6456
кораби Kora I 93
→ колония Vesta I (8304)
Учен
мина Kora I 100
лаборатория Kora I 132
мина Kora II 37
лаборатория Kora II 132
колония Vesta I 3630
колония Vesta II 2826
кораби Kora I 99
→ колония Vesta I (3630)

Колонията на Веста I е 8304 вместо 230. Кой ред смята грешно?

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

По-точно

По-точно: какво не прави оценката

Не е вероятност и няма единица. 162 не значи „162 точки полза“. Числото има смисъл само за сравнение вътре в един списък на един съперник. Затова формулите са нагласени (4, 50, 6, 60), а не изведени. Теглата на характера са цели числа „в проценти“, както пише в таблицата на канона, за да няма дробни.

Алчен е. Съперникът гледа само следващото действие и не планира: не пести за колония и не знае, че след три хода ще е по-слаб. Това е цената на простотата. По-сериозните системи гледат напред: планиране на цели (GOAP), дърво на търсене или Monte Carlo (MCTS, както в Го). Но в стратегия по ходове алчният избор с добри формули обикновено е достатъчно умен, за да е забавен. В боя на Арканум е същият похват: Врагове, които мислят.

Оценките се настройват, не се пишат. Откъде са числата 80, 90, 170? Първо предположение, после баланс със симулация: игри на изкуствен интелект срещу изкуствен интелект, които броят кой характер колко печели. Този инструмент е в последния урок.

Съперникът не мами, напротив. Плаща по същите цени и вика същите функции като теб, но в играта с човек произвежда само 80% от нормалното (RIVAL_PACE, лесното ниво); в симулацията е 100%. Редът е в empireIncome и е казан на открито.

Равните оценки не се оставят на късмета. Строгото > и фиксираният ред на генериране правят избора еднакъв на четирите езика и при всяко повторение: мина, лаборатория, завод, крепост (за всяка планета), после колониите, накрая корабите. Затова редът е част от правилата, не подробност.

Упражнение

Крепостта

Реши задачата в редактора си. Тръгни от програмата за трите характера.

Крепост. Добави ново действие fort: орбитална крепост на планета. Тя струва 25 метал (FORT_COST) и се оценява според опасността: 40 + min(160, опасност) + 30, ако планетата е столицата (първата в списъка), иначе без +30. Опасността е нова стойност threat на империята и е 120 в тази задача. Действието е допустимо само ако опасността е положителна и има 25 метал. Редът в списъка е: за всяка планета мина, лаборатория, крепост.

Очакваният изход (добави и fort в label):

Изход
Воин
мина Kora I 90
лаборатория Kora I 72
крепост Kora I 190
мина Kora II 33
лаборатория Kora II 72
крепост Kora II 160
колония Vesta I 115
колония Vesta II 89
кораби Kora I 162
→ крепост Kora I (190)
Строител
мина Kora I 140
лаборатория Kora I 96
крепост Kora I 190
мина Kora II 52
лаборатория Kora II 96
крепост Kora II 160
колония Vesta I 230
колония Vesta II 179
кораби Kora I 93
→ колония Vesta I (230)
Учен
мина Kora I 100
лаборатория Kora I 132
крепост Kora I 190
мина Kora II 37
лаборатория Kora II 132
крепост Kora II 160
колония Vesta I 100
колония Vesta II 78
кораби Kora I 99
→ крепост Kora I (190)

Какво се променя в изборите? При опасност 120 крепостта на столицата е 190, и тя побеждава корабите на воина и лабораторията на учения. Строителят пак колонизира: 230 е повече от 190. Опасността не е характер: крепостта има еднаква оценка за всички, затова не е в таблицата на теглата.

Още задачи

  • Свободна работа за крепост. Добави в apply изграждането на крепост и пусни цял ход на воина с опасност 120: колко действия прави, преди да му свършат парите?
  • Нов характер. Добави четвърти ред (trader: разширяване 120, строеж 120, нападение 60, наука 70). Какво избира в началното положение? Промени ли се нещо в кода, освен таблицата?
  • Таван. Нападението във флота ползва min(съотношение, 300). Какво става без тавана, ако врагът е със сила 0 (съотношението е 4000)? Как се променя изборът на воина и строителя?

Тест

Провери се

1. Воинът и строителят оценяват един и същ свят: същите планети, същите пари, същите формули. Воинът избира кораби, строителят колония. Откъде е разликата?
2. Ученият има две лаборатории с оценка 132, и това е най-високата оценка. Коя ще бъде построена?
3. След колонията строителят има 48 метал и в списъка няма никаква колония, макар че Веста II още е свободна. Защо?
4. Колега пише „разширяване × стойност × 50 / цена × 6“ вместо „… / (цена × 6)“. Каква е оценката на колонията на Веста I за строителя?
5. Флотата има сила 40. В системата Cygra врагът е със сила 60 и никой характер не я напада, дори воинът. Защо?

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