Съперниците
Накратко: Съперникът оценява с число всяко действие, което може да плати, взима най-голямото, плаща и оценява наново. Характерът на империята са теглата в същите формули.
Проблем
Съперник, който не мисли
В 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 = 3LAB_COST = 40SHIP_COST = 8SHIP_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 20C Съюз Лира Строител 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Стъпка по стъпка, с думи
- Ход 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 пъти на ход (в канона е същото) или докато няма какво да се прави.
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 = 40STAY = 20SPOTS = [ 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) + ")")Лабораторията на Кора I е първата с оценка 132. Лабораторията на Кора II има същата оценка, но не е строго по-голяма и не сменя избора. Колонията (100) е по-малка. Вторият отговор би излязъл при >=: тогава всеки равен сменя избора и печели последният.
Пробвай
Попълни празнината
Началната стойност на най-доброто е параметърът на функцията: за икономиката е 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Започва се от прага: floor. Така същата функция върши и за икономиката (0), и за флота (20), а оценка, която не го надхвърля, не печели нищо: резултатът е null. Ако се започне от 0 за всичко, флотът би тръгвал за нападение, чиято оценка е 5, вместо да стои.
Пробвай
Намери грешката
Колега смята колониите без скоби и сега всяка империя, дори воинът, само колонизира. Ето оценките на строителя:
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. Кой ред смята грешно?
Избери реда с грешката.
В … * 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)? Как се променя изборът на воина и строителя?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Изключения · Хвърляне и свои изключения · Списъци: append, remove и sort · dataclass, match и шаблони
- Задачи по темата: Epic Cosmos: империята