Съперниците
Накратко: Съперникът оценява с число всяко действие, което може да плати, взима най-голямото, плаща и оценява наново. Характерът на империята са теглата в същите формули.
Проблем
Съперник, който не мисли
В Epic Cosmos играеш срещу три империи. Докато ти пишеш команди, всяка от тях решава какво да прави на този ход: да постави мина, да изгради лаборатория, да заселва нова планета, да поръча кораби, да прати флота. Как се прави такъв съперник?
Първият опит е сценарий: „ход 1 мина, ход 2 мина, ход 3 колония…“. Играчът го разбира за три партии и после го бие всеки път. Вторият опит е веригата „ако … иначе“: ако има пари за колония, колонизирай; иначе ако е в опасност, строй кораби; иначе… Всяко ново действие иска нов ред и нови изключения към другите, а съперникът пак е предвидим и прави глупости, защото никой не е предвидил точно това положение.
Има и по-хубав начин. Дай на всяко възможно действие число: колко е добро за тази империя сега. Направи действието с най-голямото число. „Колония или кораби?“ е сравнение на две числа, а не правило за тази двойка. Нов вид действие е нов ред в списъка с оценки, без да се пипа останалото.
И още нещо, което прави играта интересна: характерът. Воинът, строителят и ученият не са три програми, а един и същ код с различни тегла във формулите.
Идеята
Числото за всяко действие
Ето положението, с което работи целият урок. Ход 5, една империя с две планети (Кора I със завод за кораби и Кора II с една мина), още две свободни планети в същата система (Веста I и II), 100 метал и сила на флота 10:
record Option(String kind, String target, int utility) {}
class Planet { final String name; final int rich; int mines; boolean lab; final boolean yard;
Planet(String name, int rich, int mines, boolean yard) { this.name = name; this.rich = rich; this.mines = mines; this.yard = yard; }}
class Empire { int metal = 100; int power = 10; int turn = 5; final List<Planet> owned = new ArrayList<>(); final List<Planet> free = new ArrayList<>();}static final int MAX_MINE = 3;static final int LAB_COST = 40;static final int SHIP_COST = 8;static final int SHIP_POWER = 5;
static Empire newEmpire() { Empire 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) { // Всяко действие, което империята може да плати, получава оценка. List<Option> list = new ArrayList<>(); int labs = 0; for (Planet planet : empire.owned) { if (planet.lab) labs++; } for (Planet planet : 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) { for (Planet planet : 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); for (Planet planet : 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; for (Option option : list) { if (option.utility() > bestU) { pick = option; bestU = option.utility(); } } return pick;}
static String label(Option option) { String kind = option.kind(); if (kind.equals("mine")) return "мина " + option.target(); if (kind.equals("lab")) return "лаборатория " + option.target(); if (kind.equals("colony")) return "колония " + option.target(); return "кораби " + option.target();}Положението от по-горе, пуснато за трите характера (всеки получава нова империя със същите планети и пари). За всеки са изброени оценките в реда на списъка и в края изборът:
for (Personality p : PERSONALITIES) { List<Option> list = options(newEmpire(), p); System.out.println(p.name()); for (Option option : list) { System.out.println(" " + pad(label(option), 21) + padLeft(String.valueOf(option.utility()), 4)); } Option pick = best(list, 0); System.out.println(" → " + 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 20C Съюз Лира Строител 1 109 0 20 мирD Академия Вега Учен 1 109 0 20 мирB Хегемония Орион Воин 1 109 0 20 мирВиж го
Оценката в действие
Сцената минава през четири етапа. Първите три са една и съща империя в ход 5, оценена от воина, строителя и учения. Редовете са действията в реда на списъка, лентите са оценките, а оранжевата черта е „най-доброто досега“: тя се мести само при строго по-голямо число. В четвъртия етап строителят вече е платил за колонията и оценява наново.
static List<Option> options(Empire empire, Personality p) { // Всяко действие, което империята може да плати, получава оценка. List<Option> list = new ArrayList<>(); int labs = 0; for (Planet planet : empire.owned) { if (planet.lab) labs++; } for (Planet planet : 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) { for (Planet planet : 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); for (Planet planet : 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; for (Option option : list) { if (option.utility() > bestU) { pick = option; bestU = option.utility(); } } return pick;}Стъпка по стъпка, с думи
- Ход 5: метал 100, сила 10. Империята оценява всяко действие, което може да плати. Първият е воинът с тегла 80/90/170/60: строеж 90, нападение 170.
- Воинът: мина на Кора I = строеж × богатство / (4 × (мини + 1)) = 90; на Кора II 33, защото там вече има една мина. Лабораторията е наука × 6 / 5 = 72, еднаква на двете планети.
- Колония на Веста I = разширяване × (богатство + 6) × 50 / (цена × 6) = 115 при цена 52. Корабите: желаната сила е 45, а има 10, затова 60 + нападение / 2 + (45 − 10) / 2 = 162.
- Най-голямата оценка е 162: воинът строи кораби. Чертата „най-доброто досега“ се премести три пъти (мината 90, колонията 115, корабите 162), всеки път при строго по-голяма оценка.
- Същият свят, други тегла. Строителят (160/140/50/80) цени строежа: мина на Кора I 140, лаборатория 96. Планетите, парите и формулите са същите.
- Колониите са 230 и 179: разширяването тежи 160. Корабите са само 93, защото желаната сила на строителя е 27 (нападението му е 50).
- Строителят избира колонията на Веста I (230). Воинът с точно същите числа избра кораби.
- Ученият (70/100/60/110) цени науката: лабораторията е 132 на двете планети, мината на Кора I 100.
- Колониите са 100 и 78, корабите 99: желаната сила е 29.
- Две лаборатории с една и съща оценка 132. Втората не е строго по-голяма, затова чертата остава на първата: печели лабораторията на Кора I.
- Строителят плати колонията: остават 48 метал. Следващата колония струва вече 64 (всяка планета я прави по-скъпа), не може да се плати и няма оценка: редът ѝ е празен. Оценява се наново.
- Мината на Кора I пак е 140, корабите 93. Сега 48 метал стигат за мина, лаборатория или кораби, но не за колония.
- Втора стъпка от същия ход: строителят строи мина на Кора I (140). Цикълът е: оцени всичко, направи най-доброто, плати и оцени наново.
Кодът до сцената е options и best от програмата. На всяка стъпка светят редовете, които работят в този момент.
Идеята
Плащаш и пак смяташ
Оценката важи за положението сега. Щом империята направи най-доброто, положението се променя: парите намаляват, цената на следващата колония расте, нова мина прави следващата по-малко ценна, нова колония отваря нови действия. Затова съперникът не взима списъка веднъж, а повтаря: оцени всичко, направи най-доброто, плати, оцени наново. Най-много 6 пъти на ход (в канона е същото) или докато няма какво да се прави.
static Planet find(List<Planet> planets, String name) { for (Planet planet : planets) { if (planet.name.equals(name)) return planet; } return null;}
static void apply(Empire empire, Option option) { String kind = option.kind(); if (kind.equals("mine")) { Planet planet = find(empire.owned, option.target()); empire.metal -= mineCost(planet); planet.mines++; } else if (kind.equals("lab")) { empire.metal -= LAB_COST; find(empire.owned, option.target()).lab = true; } else if (kind.equals("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; for (Planet planet : empire.owned) { income += planet.rich * (2 + planet.mines) / 2; } empire.metal += income; empire.turn++;}
static void playTurn(Empire empire, Personality p, boolean log) { // Плащане, нова оценка, и така докато има какво да се прави (най-много 6 пъти). for (int step = 1; step <= 6; step++) { Option option = best(options(empire, p), 0); if (option == null) { if (log) System.out.println(" няма какво повече: метал " + empire.metal); return; } int before = empire.metal; apply(empire, option); if (log) { System.out.println(" " + step + ". " + label(option) + " (" + option.utility() + "): метал " + before + " → " + empire.metal); } }}
static void summarize(Personality p) { Empire empire = newEmpire(); for (int turn = 0; turn < 6; turn++) { playTurn(empire, p, false); endTurn(empire); } int mines = 0; int labs = 0; for (Planet planet : empire.owned) { mines += planet.mines; if (planet.lab) labs++; } System.out.println(p.name() + ": планети " + empire.owned.size() + ", мини " + mines + ", лаб. " + labs + ", сила " + empire.power + ", метал " + empire.metal);}Ето един ход на строителя, действие по действие, и след това какво са постигнали трите характера за шест хода:
Personality builder = PERSONALITIES.get(1);Empire empire = newEmpire();System.out.println(builder.name() + ", ход " + empire.turn + ":");playTurn(empire, builder, true);System.out.println();System.out.println("След 6 хода:");for (Personality p : 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, изобщо не се разглежда.
static final int FLEET_POWER = 40;static final int STAY = 20;static final List<Spot> SPOTS = List.of( new Spot("Brisa", 1, 12, 0, 0), new Spot("Altan", 5, 0, 12, 10), new Spot("Cygra", 3, 0, 18, 60));
static List<Option> targetOptions(int power, Personality p) { List<Option> list = new ArrayList<>(); for (Spot spot : 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: ако нищо не го надхвърли, флотът стои.
for (Personality p : PERSONALITIES) { List<Option> list = targetOptions(FLEET_POWER, p); System.out.println(p.name()); for (Option option : list) { System.out.println(" " + pad(label(option), 18) + padLeft(String.valueOf(option.utility()), 4)); } Option pick = best(list, STAY); System.out.println(" → " + (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(оценка, действие) вместо списък:
// Едно действие в икономиката: строеж, колония или поръчка на кораби.static class Action { String kind; int planetId; int shipKind = 0; int count = 0;
Action(String kind, int planetId) { this.kind = kind; this.planetId = planetId; }}
// Най-доброто действие досега: печели строго по-голямата оценка, затова при равни е първото.static class Best { Action action = null; int u = 0;
void consider(int u, Action action) { if (u > this.u) { this.u = u; this.action = action; } }}
// Едно действие в икономиката: най-доброто по полезност, или null.static Action aiBestAction(World world, Empire empire) { Personality p = empire.personality; Best pick = new Best(); List<Planet> mine = ownedPlanets(world, empire.id); int labs = 0; int yards = 0; for (Planet planet : mine) { labs += planet.lab; yards += planet.yard; } TechMods mods = techMods(empire); for (Planet planet : mine) { if (planet.mines < MAX_MINE && canPay(empire, buildCost("mine", planet))) { pick.consider((p.build * richSum(planet)) / (4 * (planet.mines + 1)), new Action("mine", planet.id)); } if (planet.lab == 0 && canPay(empire, buildCost("lab", planet))) { pick.consider((p.research * 6) / 5 - 40 * labs, new Action("lab", planet.id)); } if (planet.yard == 0 && canPay(empire, buildCost("yard", planet))) { pick.consider(yards == 0 ? 250 : mine.size() >= 5 && yards < 2 ? 60 : 0, new Action("yard", planet.id)); } if (mods.fort > 0 && planet.fort < MAX_FORT && canPay(empire, buildCost("fort", planet))) { int threat = enemyPowerAt(world, empire, planet.systemId); for (int next : world.systems.get(planet.systemId).links) threat += enemyPowerAt(world, empire, next); if (threat > 0) pick.consider(40 + minOf(160, threat) + (planet.id == empire.capital ? 30 : 0), new Action("fort", planet.id)); } } int[] cost = colonyCost(world, empire); if (canPay(empire, cost)) { for (Planet planet : world.planets) { if (planet.owner >= 0 || !hasPresence(world, empire.id, planet.systemId) || hostileIn(world, empire.id, planet.systemId)) continue; pick.consider((p.expand * (richSum(planet) + 6) * 50) / (cost[0] * 6), new Action("col", planet.id)); } } // Кораби: докато силата е под желаната за този ход. int want = ((20 + 3 * world.turn) * (140 + p.attack)) / 240; for (Empire other : 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) { for (Planet planet : mine) { if (planet.yard == 0) continue; int kind = counterKind(world, empire); if (shipAllowed(empire, 3) && canPay(empire, SHIPS.get(3).cost) && have >= 30) kind = 3; int count = 4; while (count > 0 && !canPay(empire, scaleCost(SHIPS.get(kind).cost, count))) count--; if (count > 0) { Action action = new Action("ship", planet.id); action.shipKind = kind; action.count = count; pick.consider(60 + p.attack / 2 + minOf(100, (want - have) / 2), action); } break; } } return pick.action;}А ето цикълът „оцени, направи, повтори“ (aiEconomy) и целият ход на един съперник (aiPlay): наука, икономика, флоти. Науката е същият похват в малък: aiResearch оценява всяка достъпна технология с наука × (100 + бонус на характера) × 10 / (цена + 5) и взима най-голямата (дървото на технологиите е в урок 2).
static void aiEconomy(World world, Empire empire) { for (int step = 0; step < 6; step++) { Action action = aiBestAction(world, empire); if (action == null) return; Planet planet = world.planets.get(action.planetId); String error = null; if (action.kind.equals("col")) error = colonize(world, empire, planet); else if (action.kind.equals("ship")) error = queueShips(world, empire, planet, action.shipKind, action.count); else error = buildStructure(world, empire, action.kind, planet); if (error != null) return; }}
static void aiPlay(World world, Empire empire) { // Изкуственият интелект планира наново всеки ход. empire.orders = new ArrayList<>(); 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.
А ето и избора на цел на флота:
static void aiFleets(World world, Empire empire) { Personality p = empire.personality; List<Fleet> snapshot = new ArrayList<>(); for (Fleet fleet : world.fleets) { if (fleet.owner == empire.id) snapshot.add(fleet); } for (Fleet fleet : 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; boolean split = false; boolean siege = false; for (StarSystem system : world.systems) { int foe = enemyPowerAt(world, empire, system.id); int freeValue = 0; int enemyValue = 0; boolean ownHere = false; for (int pid : system.planets) { Planet planet = world.planets.get(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(100L * myPower, 100L * foe + 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 са от урока.)
List<Option> list = List.of( new Option("mine", "Kora I", 90), new Option("lab", "Kora I", 132), new Option("lab", "Kora II", 132), new Option("colony", "Vesta I", 100));Option pick = best(list, 0);System.out.println(label(pick) + " (" + pick.utility() + ")");Лабораторията на Кора I е първата с оценка 132. Лабораторията на Кора II има същата оценка, но не е строго по-голяма и не сменя избора. Колонията (100) е по-малка. Вторият отговор би излязъл при >=: тогава всеки равен сменя избора и печели последният.
Пробвай
Попълни празнината
Началната стойност на най-доброто е параметърът на функцията: за икономиката е 0, за флота е 20. С какво започва bestU?
static Option best(List<Option> list, int floor) {
// Печели строго по-голямата оценка, затова при равни е първото.
Option pick = null;
int bestU = ;
for (Option option : list) {
if (option.utility() > bestU) {
pick = option;
bestU = option.utility();
}
}
return pick;
}Започва се от прага: floor. Така същата функция върши и за икономиката (0), и за флота (20), а оценка, която не го надхвърля, не печели нищо: резултатът е null. Ако се започне от 0 за всичко, флотът би тръгвал за нападение, чиято оценка е 5, вместо да стои.
Пробвай
Намери грешката
Колега смята колониите без скоби и сега всяка империя, дори воинът, само колонизира. Ето оценките на строителя:
for (Personality p : PERSONALITIES) { List<Option> list = options(newEmpire(), p); System.out.println(p.name()); for (Option option : list) { System.out.println(" " + pad(label(option), 21) + padLeft(String.valueOf(option.utility()), 4)); } Option pick = best(list, 0); System.out.println(" → " + 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. Кой ред смята грешно?
Избери реда с грешката.
В … * 50 / cost * 6 първо се дели на цената (целочислено, с отрязан остатък), после се умножава по 6: 72000 / 52 = 1384, 1384 × 6 = 8304. Формулата е … × 50 / (цена × 6): делителят е цялото произведение. Без скобите колонията е около 36 пъти по-голяма от нужното и бие всичко. Урок: целочисленото деление не е асоциативно, и на оценки от формула му е нужна скоба.
По-точно
По-точно: какво не прави оценката
Не е вероятност и няма единица. 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)? Как се променя изборът на воина и строителя?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Изключения · Хвърляне и свои изключения · Списъци · record, switch и шаблони
- Задачи по темата: Epic Cosmos: империята