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

Съперниците

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

Проблем

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

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

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

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

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

Идеята

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

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

@dataclass(frozen=True)
class Option:
kind: str
target: str
utility: int
class Planet:
def __init__(self, name, rich, mines, yard):
self.name = name
self.rich = rich
self.mines = mines
self.lab = False
self.yard = yard
class Empire:
def __init__(self):
self.metal = 100
self.power = 10
self.turn = 5
self.owned = []
self.free = []
MAX_MINE = 3
LAB_COST = 40
SHIP_COST = 8
SHIP_POWER = 5
def new_empire():
empire = Empire()
empire.owned.append(Planet("Kora I", 4, 0, True))
empire.owned.append(Planet("Kora II", 3, 1, False))
empire.free.append(Planet("Vesta I", 3, 0, False))
empire.free.append(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.
def options(empire, p):
# Всяко действие, което империята може да плати, получава оценка.
result = []
labs = 0
for planet in empire.owned:
if planet.lab:
labs += 1
for planet in empire.owned:
if planet.mines < MAX_MINE and empire.metal >= mine_cost(planet):
result.append(Option("mine", planet.name, p.build * planet.rich // (4 * (planet.mines + 1))))
if not planet.lab and empire.metal >= LAB_COST:
result.append(Option("lab", planet.name, p.research * 6 // 5 - 40 * labs))
cost = colony_cost(empire)
if empire.metal >= cost:
for planet in empire.free:
value = planet.rich + 6
result.append(Option("colony", planet.name, p.expand * value * 50 // (cost * 6)))
want = wanted_power(empire, p)
for planet in empire.owned:
if planet.yard and empire.power < want and ship_count(empire) > 0:
urge = min(100, (want - empire.power) // 2)
result.append(Option("ships", planet.name, 60 + p.attack // 2 + urge))
break
return result

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

Option е @dataclass(frozen=True): три полета и нищо повече, а Planet и Empire са обикновени класове. Целочисленото деление е // и отрязва остатъка: всички числа са положителни, затова няма изненади с отрицателните.
По-точно, за любопитните

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

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

Идеята

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

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

def best(candidates, floor):
# Печели строго по-голямата оценка, затова при равни е първото.
pick = None
best_u = floor
for option in candidates:
if option.utility > best_u:
pick = option
best_u = option.utility
return pick
def label(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

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

for p in PERSONALITIES:
options_list = options(new_empire(), p)
print(p.name)
for option in options_list:
print(" " + pad(label(option), 21) + pad_left(str(option.utility), 4))
pick = best(options_list, 0)
print(" → " + label(pick) + " (" + str(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, оценена от воина, строителя и учения. Редовете са действията в реда на списъка, лентите са оценките, а оранжевата черта е „най-доброто досега“: тя се мести само при строго по-голямо число. В четвъртия етап строителят вече е платил за колонията и оценява наново.

def options(empire, p):
# Всяко действие, което империята може да плати, получава оценка.
result = []
labs = 0
for planet in empire.owned:
if planet.lab:
labs += 1
for planet in empire.owned:
if planet.mines < MAX_MINE and empire.metal >= mine_cost(planet):
result.append(Option("mine", planet.name, p.build * planet.rich // (4 * (planet.mines + 1))))
if not planet.lab and empire.metal >= LAB_COST:
result.append(Option("lab", planet.name, p.research * 6 // 5 - 40 * labs))
cost = colony_cost(empire)
if empire.metal >= cost:
for planet in empire.free:
value = planet.rich + 6
result.append(Option("colony", planet.name, p.expand * value * 50 // (cost * 6)))
want = wanted_power(empire, p)
for planet in empire.owned:
if planet.yard and empire.power < want and ship_count(empire) > 0:
urge = min(100, (want - empire.power) // 2)
result.append(Option("ships", planet.name, 60 + p.attack // 2 + urge))
break
return result
def best(candidates, floor):
# Печели строго по-голямата оценка, затова при равни е първото.
pick = None
best_u = floor
for option in candidates:
if option.utility > best_u:
pick = option
best_u = 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 пъти на ход (в канона е същото) или докато няма какво да се прави.

def find(planets, name):
for planet in planets:
if planet.name == name:
return planet
return None
def apply(empire, option):
if option.kind == "mine":
planet = find(empire.owned, option.target)
empire.metal -= mine_cost(planet)
planet.mines += 1
elif option.kind == "lab":
empire.metal -= LAB_COST
find(empire.owned, option.target).lab = True
elif option.kind == "colony":
planet = find(empire.free, option.target)
empire.metal -= colony_cost(empire)
empire.free.remove(planet)
empire.owned.append(planet)
else:
count = ship_count(empire)
empire.metal -= count * SHIP_COST
empire.power += count * SHIP_POWER
def end_turn(empire):
income = 4
for planet in empire.owned:
income += planet.rich * (2 + planet.mines) // 2
empire.metal += income
empire.turn += 1
def play_turn(empire, p, log):
# Плащане, нова оценка, и така докато има какво да се прави (най-много 6 пъти).
for step in range(1, 7):
option = best(options(empire, p), 0)
if option is None:
if log:
print(" няма какво повече: метал " + str(empire.metal))
return
before = empire.metal
apply(empire, option)
if log:
print(" " + str(step) + ". " + label(option) + " (" + str(option.utility) + "): метал " + str(before) + " → " + str(empire.metal))
def summarize(p):
empire = new_empire()
for _ in range(6):
play_turn(empire, p, False)
end_turn(empire)
mines = 0
labs = 0
for planet in empire.owned:
mines += planet.mines
if planet.lab:
labs += 1
print(p.name + ": планети " + str(len(empire.owned)) + ", мини " + str(mines) + ", лаб. " + str(labs)
+ ", сила " + str(empire.power) + ", метал " + str(empire.metal))

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

builder = PERSONALITIES[1]
empire = new_empire()
print(builder.name + ", ход " + str(empire.turn) + ":")
play_turn(empire, builder, True)
print()
print("След 6 хода:")
for 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, изобщо не се разглежда.

FLEET_POWER = 40
STAY = 20
SPOTS = [
Spot("Brisa", 1, 12, 0, 0),
Spot("Altan", 5, 0, 12, 10),
Spot("Cygra", 3, 0, 18, 60),
]
def target_options(power, p):
result = []
for spot in SPOTS:
steps = spot.distance + 1
if spot.free_value > 0 and spot.enemy_power == 0 and spot.distance <= 5:
result.append(Option("settle", spot.name, p.expand * spot.free_value // (6 * steps)))
ratio = power * 100 * 100 // (spot.enemy_power * 100 + 100)
if spot.enemy_value > 0 and ratio >= 130 and spot.distance <= 6:
sure = min(ratio, 300)
result.append(Option("attack", spot.name, p.attack * spot.enemy_value * sure // (4 * steps * 150)))
return result

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

for p in PERSONALITIES:
options_list = target_options(FLEET_POWER, p)
print(p.name)
for option in options_list:
print(" " + pad(label(option), 18) + pad_left(str(option.utility), 4))
pick = best(options_list, STAY)
print(" → " + ("стои" if pick is None else label(pick) + " (" + str(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(оценка, действие) вместо списък:

# Едно действие в икономиката: най-доброто по полезност, или None.
def ai_best_action(world, empire):
p = empire.personality
best = None
best_u = 0
def consider(u, action):
nonlocal best, best_u
if u > best_u:
best_u = u
best = action
mine = owned_planets(world, empire.id)
labs = 0
yards = 0
for planet in mine:
labs += planet.lab
yards += planet.yard
mods = tech_mods(empire)
for planet in mine:
if planet.mines < MAX_MINE and can_pay(empire, build_cost('mine', planet)):
consider((p.build * rich_sum(planet)) // (4 * (planet.mines + 1)), SimpleNamespace(kind='mine', planet_id=planet.id))
if planet.lab == 0 and can_pay(empire, build_cost('lab', planet)):
consider((p.research * 6) // 5 - 40 * labs, SimpleNamespace(kind='lab', planet_id=planet.id))
if planet.yard == 0 and can_pay(empire, build_cost('yard', planet)):
consider(250 if yards == 0 else (60 if len(mine) >= 5 and yards < 2 else 0), SimpleNamespace(kind='yard', planet_id=planet.id))
if mods['fort'] > 0 and planet.fort < MAX_FORT and can_pay(empire, build_cost('fort', planet)):
threat = enemy_power_at(world, empire, planet.system_id)
for nxt in world.systems[planet.system_id].links:
threat += enemy_power_at(world, empire, nxt)
if threat > 0:
consider(40 + min_of(160, threat) + (30 if planet.id == empire.capital else 0), SimpleNamespace(kind='fort', planet_id=planet.id))
cost = colony_cost(world, empire)
if can_pay(empire, cost):
for planet in world.planets:
if planet.owner >= 0 or not has_presence(world, empire.id, planet.system_id) or hostile_in(world, empire.id, planet.system_id):
continue
consider((p.expand * (rich_sum(planet) + 6) * 50) // (cost[0] * 6), SimpleNamespace(kind='col', planet_id=planet.id))
# Кораби: докато силата е под желаната за този ход.
want = ((20 + 3 * world.turn) * (140 + p.attack)) // 240
for other in world.empires:
if other.alive and other.id != empire.id and empire.war[other.id] == 1:
want = (want * 3) // 2
have = empire_power(world, empire.id) + queued_power(world, empire.id)
if have < want:
for planet in mine:
if planet.yard == 0:
continue
kind = counter_kind(world, empire)
if ship_allowed(empire, 3) and can_pay(empire, SHIPS[3].cost) and have >= 30:
kind = 3
count = 4
while count > 0 and not can_pay(empire, scale_cost(SHIPS[kind].cost, count)):
count -= 1
if count > 0:
consider(60 + p.attack // 2 + min_of(100, (want - have) // 2), SimpleNamespace(kind='ship', planet_id=planet.id, ship_kind=kind, count=count))
break
return best

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

def ai_economy(world, empire):
for step in range(6):
action = ai_best_action(world, empire)
if action is None:
return
planet = world.planets[action.planet_id]
error = None
if action.kind == 'col':
error = colonize(world, empire, planet)
elif action.kind == 'ship':
error = queue_ships(world, empire, planet, action.ship_kind, action.count)
else:
error = build_structure(world, empire, action.kind, planet)
if error is not None:
return
def ai_play(world, empire):
# Изкуственият интелект планира наново всеки ход.
empire.orders = []
ai_research(world, empire)
ai_economy(world, empire)
ai_fleets(world, empire)

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

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

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

def ai_fleets(world, empire):
p = empire.personality
snapshot = []
for fleet in world.fleets:
if fleet.owner == empire.id:
snapshot.append(fleet)
for fleet in snapshot:
busy = ordered_ships(empire, fleet.system_id)
free = [0, 0, 0, 0]
for k in range(SHIP_KIND_COUNT):
free[k] = fleet.ships[k] - busy[k]
my_power = counts_power(free)
if my_power == 0:
continue
here = fleet.system_id
dist = bfs(world.systems, here).dist
# Задържане: ако сме у дома под заплаха или обсаждаме враг.
best_u = 20
target = -1
split = False
siege = False
for system in world.systems:
foe = enemy_power_at(world, empire, system.id)
free_value = 0
enemy_value = 0
own_here = False
for pid in system.planets:
planet = world.planets[pid]
if planet.owner < 0:
free_value += rich_sum(planet) + 6
elif planet.owner == empire.id:
own_here = True
elif empire.war[planet.owner] == 1:
enemy_value += rich_sum(planet) + 10
d = dist[system.id]
if d < 0:
continue
ratio = percent(my_power * 100, foe * 100 + 100)
if system.id == here:
if enemy_value > 0 and ratio >= 80:
best_u = 250
siege = True
elif own_here and foe > 0 and ratio >= 60:
best_u = 200
siege = True
continue
if free_value > 0 and foe == 0 and d <= 5:
u = (p.expand * free_value) // (6 * (d + 1))
if u > best_u:
best_u = u
target = system.id
split = total_of(free) >= 6
siege = False
if enemy_value > 0 and ratio >= 130 and d <= 6:
u = (p.attack * enemy_value * min_of(ratio, 300)) // (4 * (d + 1) * 150)
if u > best_u:
best_u = u
target = system.id
split = False
siege = False
if own_here and foe > 0 and d <= 4 and ratio >= 60:
u = 150 - 10 * d
if u > best_u:
best_u = u
target = system.id
split = False
siege = False
if target < 0 or siege:
continue
step = next_step(world.systems, here, target)
if step < 0:
continue
order_move(world, empire, here, target, half_of(free) if split else None)

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

Пробвай

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

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

options_list = [
Option("mine", "Kora I", 90),
Option("lab", "Kora I", 132),
Option("lab", "Kora II", 132),
Option("colony", "Vesta I", 100),
]
pick = best(options_list, 0)
print(label(pick) + " (" + str(pick.utility) + ")")
Отговор

Пробвай

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

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

def best(candidates, floor):
    # Печели строго по-голямата оценка, затова при равни е първото.
    pick = None
    best_u = 
    for option in candidates:
        if option.utility > best_u:
            pick = option
            best_u = option.utility
    return pick

Пробвай

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

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

for p in PERSONALITIES:
options_list = options(new_empire(), p)
print(p.name)
for option in options_list:
print(" " + pad(label(option), 21) + pad_left(str(option.utility), 4))
pick = best(options_list, 0)
print(" → " + label(pick) + " (" + str(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 за достъпно обучение по програмиране ·