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

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

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

Проблем

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

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

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

Ход 28/150 Земна империя (A) точки 147
M3+27 G203+36 C11+24 U52+5 наука +16
Проучава: Торпеда 28/31
Битка при Ismar: Земна империя срещу Хегемония Орион
Рунд 1: 4I | 2B 2F загуби 2 / 2
Рунд 2: 2I | 2F загуби 2 / 0
Победа за Хегемония Орион. Остават: 2F.
Enter или map: към картата help: команди

Играта сгъва интервалите на редовете (battle ги пренарежда по ширината на екрана), затова „Рунд 1: 4I | 2B 2F“ няма колони. Програмите в урока печатат същите думи.

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

Идеята

Флотът е броят

Флотът не е списък от кораби, а пет числа: колко прехващача (I), бомбардировача (B), фрегати (F), дредноута (D) и крепости (#) има една империя в една система. Всички кораби от една роля са еднакви, затова обект за всеки кораб не трябва: битка с 500 кораба е същите пет числа. Крепостите на планетите там се бият като пета роля.

Показателите на ролите са две таблици с индекс по роля: живот и атака. Третата таблица е множителят, за който е следващата стъпка:

KINDS = 5
ROUNDS = 6
LETTERS = "IBFD#"
HP = [10, 16, 30, 90, 25]
ATK = [5, 7, 6, 18, 5]
MULT = [
[100, 175, 50, 50, 60],
[50, 100, 175, 80, 150],
[175, 50, 100, 80, 100],
[110, 110, 110, 100, 110],
[100, 100, 100, 100, 100],
]

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

def total(counts):
result = 0
for n in counts:
result += n
return result
def counts_text(counts):
parts = []
for k in range(KINDS):
if counts[k] > 0:
parts.append(str(counts[k]) + LETTERS[k])
return " ".join(parts) if parts else "-"
def unit_power(kind):
return HP[kind] * ATK[kind] // 10
def power(counts):
result = 0
for k in range(KINDS):
result += counts[k] * unit_power(k)
return result

Програмата показва флота 3I 2B 2F: текст, брой, сила и празен флот.

fleet = [3, 2, 2, 0, 0]
print("Флот: " + counts_text(fleet))
print("Кораби: " + str(total(fleet)))
print("Сила: " + str(power(fleet)))
print("Празен флот: " + counts_text([0] * KINDS))
Изход
Флот: 3I 2B 2F
Кораби: 7
Сила: 73
Празен флот: -

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

// е целочислено деление. Числата не са отрицателни, затова // и / на другите езици дават едно и също.

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

def fleet_at(world, owner, system_id):
for fleet in world.fleets:
if fleet.owner == owner and fleet.system_id == system_id:
return fleet
return None
# Добавя кораби към флота на империята в системата; нов флот влиза на мястото си в реда.
def add_ships(world, owner, system_id, counts):
fleet = fleet_at(world, owner, system_id)
if fleet is None:
fleet = SimpleNamespace(owner=owner, system_id=system_id, ships=[0, 0, 0, 0])
at = len(world.fleets)
while at > 0:
before = world.fleets[at - 1]
if before.owner < owner or (before.owner == owner and before.system_id < system_id):
break
at -= 1
world.fleets.insert(at, fleet)
for k in range(SHIP_KIND_COUNT):
fleet.ships[k] += counts[k]

Идеята

Камък, ножица, хартия

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

# Множителят на щетата в проценти: ред е нападателят, колона е целта (прехващач, бомбардировач,
# фрегата, дредноут, крепост).
MULT = [
[100, 175, 50, 50, 60],
[50, 100, 175, 80, 150],
[175, 50, 100, 80, 100],
[110, 110, 110, 100, 110],
[100, 100, 100, 100, 100],
]

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

def choose_target(kind, enemy):
best = -1
for k in range(KINDS):
if enemy[k] > 0 and (best < 0 or MULT[kind][k] > MULT[kind][best]):
best = k
return best
def choose_target(kind, enemy):
    best = -1
    for k in range(KINDS):
        if enemy[k] > 0 and (best < 0 or MULT[kind][k]  MULT[kind][best]):
            best = k
    return best

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

all_kinds = [1, 1, 1, 1, 1]
for kind in range(KINDS):
target = choose_target(kind, all_kinds)
print(NAMES[kind] + " -> " + NAMES[target] + " " + str(MULT[kind][target]) + "%")
no_frigates = [1, 1, 0, 1, 1]
alt = choose_target(1, no_frigates)
print("Без фрегати: " + NAMES[1] + " -> " + NAMES[alt] + " " + str(MULT[1][alt]) + "%")
Изход
прехващач -> бомбардировач 175%
бомбардировач -> фрегата 175%
фрегата -> прехващач 175%
дредноут -> прехващач 110%
крепост -> прехващач 100%
Без фрегати: бомбардировач -> крепост 150%

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

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

def choose_target(kind, enemy):
best = -1
for k in range(KIND_COUNT):
if enemy[k] > 0 and (best < 0 or MULT[kind][k] > MULT[kind][best]):
best = k
return best

Идеята

Залп и рана

Групата, която е избрала цел, нанася щета = броят × атака × множител / 100. Деленето е целочислено и се отрязва: 6 бомбардировача по 7 срещу фрегати са 6 × 7 × 175 / 100 = 73 (не 73,5). Щетата на всички групи на една страна се събира по роля на целта.

def volley(mine, enemy):
damage = [0] * KINDS
for k in range(KINDS):
if mine[k] == 0:
continue
target = choose_target(k, enemy)
if target < 0:
continue
damage[target] += mine[k] * ATK[k] * MULT[k][target] // 100
return damage

После щетата се прилага. Щетата по роля се дели на живота на кораба: колкото цели кораба се побират, толкова падат. Ако падат всички, ролята изчезва. Иначе остатъкът (щетата, която не стига за още един кораб) се записва като рана и се добавя към следващия залп. Без раните бавният огън никога нямаше да убива кораб: 9 щета по кораб с живот 10 са нищо, а втори път са 18 и един кораб пада.

def apply_damage(counts, wound, damage):
for k in range(KINDS):
if damage[k] == 0 or counts[k] == 0:
continue
total_damage = wound[k] + damage[k]
killed = total_damage // HP[k]
if killed >= counts[k]:
counts[k] = 0
wound[k] = 0
else:
counts[k] -= killed
wound[k] = total_damage - killed * HP[k]

Програмата прави първия рунд на битката при Измар: двата залпа (колоните са I, B, F, D, #), после прилагането. Малката функция numbers печата масив с интервали:

def numbers(values):
return " ".join(str(v) for v in values)

Програмата:

a = [4, 0, 0, 0, 0]
b = [0, 2, 2, 0, 0]
hit_b = volley(a, b)
hit_a = volley(b, a)
print("a -> b: " + numbers(hit_b))
print("b -> a: " + numbers(hit_a))
wound_a = [0] * KINDS
wound_b = [0] * KINDS
apply_damage(b, wound_b, hit_b)
apply_damage(a, wound_a, hit_a)
print("a: " + counts_text(a) + ", рани " + numbers(wound_a))
print("b: " + counts_text(b) + ", рани " + numbers(wound_b))
Изход
a -> b: 0 35 0 0 0
b -> a: 28 0 0 0 0
a: 2I, рани 8 0 0 0 0
b: 2F, рани 0 0 0 0 0

Страната a удря само бомбардировачите: 4 × 5 × 175 / 100 = 35, а два бомбардировача са 32 живот, затова и двата падат (35 / 16 = 2). Страната b удря само прехващачите: бомбардировачите с 50% (2 × 7 × 50 / 100 = 7), фрегатите със 175% (2 × 6 × 175 / 100 = 21), общо 28. 28 / 10 = 2 прехващача падат, а 8 остават като рана на третия. Фрегатите в първия рунд не са ударени: прехващачите стрелят само по бомбардировачите.

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

# Щетата, която една страна нанася на другата, по роля на целта.
def volley(stats, mine, enemy):
damage = empty_counts()
for k in range(KIND_COUNT):
if mine[k] == 0:
continue
target = choose_target(k, enemy)
if target < 0:
continue
damage[target] += (mine[k] * stats.atk[k] * MULT[k][target]) // 100
return damage
# Щетата се трупа: единиците губят живот, но първият оцелял пази раната си за следващия рунд.
def apply_damage(stats, counts, wound, damage):
for k in range(KIND_COUNT):
if damage[k] == 0 or counts[k] == 0:
continue
total = wound[k] + damage[k]
killed = total // stats.hp[k]
if killed >= counts[k]:
counts[k] = 0
wound[k] = 0
else:
counts[k] -= killed
wound[k] = total - killed * stats.hp[k]

Виж го

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

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

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

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

def run_battle(a, b, name_a, name_b, log):
wound_a = [0] * KINDS
wound_b = [0] * KINDS
for round_no in range(1, ROUNDS + 1):
if total(a) == 0 or total(b) == 0:
break
start_a = list(a)
start_b = list(b)
hit_b = volley(start_a, start_b)
hit_a = volley(start_b, start_a)
apply_damage(b, wound_b, hit_b)
apply_damage(a, wound_a, hit_a)
log.append(f"Рунд {round_no}: {counts_text(start_a)} | {counts_text(start_b)} загуби {total(start_a) - total(a)} / {total(start_b) - total(b)}")
if total(a) > 0 and total(b) == 0:
log.append(f"Победа за {name_a}. Остават: {counts_text(a)}.")
elif total(b) > 0 and total(a) == 0:
log.append(f"Победа за {name_b}. Остават: {counts_text(b)}.")
elif total(a) == 0 and total(b) == 0:
log.append("Двата флота са унищожени.")
else:
log.append(f"Няма победител след шест рунда: {counts_text(a)} | {counts_text(b)}.")
a = [4, 0, 0, 0, 0]
b = [0, 2, 2, 0, 0]
where = "Ismar"
name_a = "Земна империя"
name_b = "Хегемония Орион"
log = []
run_battle(a, b, name_a, name_b, log)
print(f"Битка при {where}: {name_a} срещу {name_b}")
for line in log:
print(line)
Изход
Битка при Ismar: Земна империя срещу Хегемония Орион
Рунд 1: 4I | 2B 2F загуби 2 / 2
Рунд 2: 2I | 2F загуби 2 / 0
Победа за Хегемония Орион. Остават: 2F.

Това са точно редовете на играта. Вторият рунд е кратък: от четирите прехващача са останали два (с рана 8), а от бомбардировачите няма следа. Двата прехващача бият фрегатите само с 50% (2 × 5 × 50 / 100 = 5 щета, по-малко от живота на един кораб), а фрегатите бият прехващачите със 175%: 2 × 6 × 175 / 100 = 21, плюс раната 8 дава 29. Двата прехващача падат. Фрегатите печелят, без да загубят кораб. Следи рундовете в сцената: стрелките и числата са едновременни.

def run_battle(a, b, name_a, name_b, log):
wound_a = [0] * KINDS
wound_b = [0] * KINDS
for round_no in range(1, ROUNDS + 1):
if total(a) == 0 or total(b) == 0:
break
start_a = list(a)
start_b = list(b)
hit_b = volley(start_a, start_b)
hit_a = volley(start_b, start_a)
apply_damage(b, wound_b, hit_b)
apply_damage(a, wound_a, hit_a)
log.append(f"Рунд {round_no}: {counts_text(start_a)} | {counts_text(start_b)} загуби {total(start_a) - total(a)} / {total(start_b) - total(b)}")
if total(a) > 0 and total(b) == 0:
log.append(f"Победа за {name_a}. Остават: {counts_text(a)}.")
elif total(b) > 0 and total(a) == 0:
log.append(f"Победа за {name_b}. Остават: {counts_text(b)}.")
elif total(a) == 0 and total(b) == 0:
log.append("Двата флота са унищожени.")
else:
log.append(f"Няма победител след шест рунда: {counts_text(a)} | {counts_text(b)}.")
Стъпка по стъпка, с думи
  1. Два флота при Измар: a има 4I, b има 2B 2F. Флотът е само броят кораби по роли, а битката е функция на два такива масива.
  2. Рунд 1. Всяка страна копира броя си в началото на рунда. И двата залпа ще се смятат от тези копия.
  3. Всяка роля избира цел с най-голям множител. a: 4I → B (175%). b: 2B → I (50%), 2F → I (175%). Камък-ножица-хартия.
  4. Залповете: a: 4 × 5 × 175 / 100 = 35 по бомбардировачите. b: 2 × 7 × 50 / 100 = 7; 2 × 6 × 175 / 100 = 21 по прехващачите, общо 28. Удрят едновременно.
  5. Щетата се прилага: 35 / 16 = 2 бомбардировача падат. 28 / 10 = 2 прехващача падат, остатъкът 8 е рана. Дневникът: загуби 2 / 2.
  6. Рунд 2: 2I срещу 2F. Бомбардировачите ги няма, затова целите се избират наново.
  7. Прехващачите бият фрегатите само с 50%: 2 × 5 × 50 / 100 = 5. Фрегатите бият прехващачите със 175%: 2 × 6 × 175 / 100 = 21.
  8. Щетата по прехващачите е 21 плюс раната 8 = 29: падат и двата. Фрегатите получават 5, по-малко от живота 30: рана 5, никоя не пада.
  9. a няма кораби, b има 2F: Победа за Хегемония Орион. Остават: 2F. Така е и в играта. Битката е чиста функция.

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

def run_battle(stats_a, counts_a, stats_b, counts_b):
a = list(counts_a)
b = list(counts_b)
wound_a = empty_counts()
wound_b = empty_counts()
rounds = []
for round_no in range(1, BATTLE_ROUNDS + 1):
if total_of(a) == 0 or total_of(b) == 0:
break
start_a = list(a)
start_b = list(b)
hit_b = volley(stats_a, start_a, start_b)
hit_a = volley(stats_b, start_b, start_a)
apply_damage(stats_b, b, wound_b, hit_b)
apply_damage(stats_a, a, wound_a, hit_a)
lost_a = empty_counts()
lost_b = empty_counts()
for k in range(KIND_COUNT):
lost_a[k] = start_a[k] - a[k]
lost_b[k] = start_b[k] - b[k]
rounds.append(SimpleNamespace(a=start_a, b=start_b, lost_a=lost_a, lost_b=lost_b))
winner = 'draw'
if total_of(a) > 0 and total_of(b) == 0:
winner = 'a'
elif total_of(b) > 0 and total_of(a) == 0:
winner = 'b'
elif total_of(a) == 0 and total_of(b) == 0:
winner = 'none'
return SimpleNamespace(rounds=rounds, a=a, b=b, winner=winner)
По-точно, за любопитните

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

Рундовете са най-много шест (BATTLE_ROUNDS): ако двата флота не могат да се унищожат, например малък флот срещу голяма крепост, боят свършва без победител и играта пише „няма победител след шест рунда“. Така битка не виси вечно.

Идеята

Едновременно и при ходовете

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

Движението е в две фази: първо всички напускат системите си, чак тогава всички пристигат. Ето защо, например, два вражески флота, които си разменят системите, не се срещат: докато единият напуска, другият още не е пристигнал. Битките са после, само там, където след движението има и двата флота в една система.

Програмата движи три флота (X, Y и Z) по верига от системи 0, 1, 2, 3 (заповеди към 3, 0 и 1) за два хода и печата къде са и кои се срещат. После прави същото грешно: мести флот по флот и проверява среща веднага след всяко преместване.

def step_toward(pos, dest):
if pos < dest:
return pos + 1
if pos > dest:
return pos - 1
return pos
def report(turn, names, pos):
parts = [names[i] + " в " + str(pos[i]) for i in range(len(pos))]
print("Ход " + str(turn) + ": " + ", ".join(parts))
for s in range(4):
here = [names[i] for i in range(len(pos)) if pos[i] == s]
if len(here) > 1:
print("среща в " + str(s) + ": " + ", ".join(here))
names = ["X", "Y", "Z"]
dest = [3, 0, 1]
pos = [0, 1, 2]
for turn in range(1, 3):
nxt = [step_toward(pos[i], dest[i]) for i in range(len(pos))]
pos = nxt
report(turn, names, pos)
live = [0, 1, 2]
for i in range(len(live)):
live[i] = step_toward(live[i], dest[i])
for j in range(len(live)):
if j != i and live[j] == live[i]:
print("Един по един: " + names[i] + " среща " + names[j])
Изход
Ход 1: X в 1, Y в 0, Z в 1
среща в 1: X, Z
Ход 2: X в 2, Y в 0, Z в 1
Един по един: X среща Y
Един по един: Z среща X

В първия ход X (от 0 към 3) и Y (от 1 към 0) си разменят системите и не се срещат, но X и Z (от 2 към 1) са едновременно в система 1: среща. Грешният начин вижда „среща“ и между X и Y: X пристига в 1, докато Y още е там и тъкмо тръгва. Проверката вътре в цикъла вижда половин ход. Правилният начин първо смята къде ще бъде всеки (next) от стария ход и чак тогава ги слага (pos = next).

Ето движението в играта. Списъкът moving е „във въздуха“: корабите вече са напуснали флота си, но още не са пристигнали:

# Един ход на галактиката има фази: дипломация на изкуствения интелект, поръчките му, движение на
# всички флотове наведнъж, битки, завладяване, производство и наука, събитие, край на хода.
# Движението е едновременно: първо всички кораби напускат системите си, чак тогава пристигат.
def resolve_movement(world):
moving = []
for empire in world.empires:
if not empire.alive:
continue
for order in empire.orders:
fleet = fleet_at(world, empire.id, order.from_)
if fleet is None:
continue
ships = [0, 0, 0, 0]
for k in range(SHIP_KIND_COUNT):
ships[k] = min_of(order.ships[k], fleet.ships[k])
fleet.ships[k] -= ships[k]
if total_of(ships) > 0:
moving.append(SimpleNamespace(owner=empire.id, to=order.to, dest=order.dest, ships=ships))
empire.orders = []
for move in moving:
add_ships(world, move.owner, move.to, move.ships)
# дългият път продължава сам: нова поръчка от мястото, където стигнаха
if move.to != move.dest:
world.empires[move.owner].orders.append(SimpleNamespace(from_=move.to, to=next_step(world.systems, move.to, move.dest), dest=move.dest, ships=list(move.ships)))
remove_empty_fleets(world)

Идеята

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

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

pairs = [(0, 1), (1, 2), (0, 2), (0, 3)]
for first, second in pairs:
a = [0] * KINDS
b = [0] * KINDS
a[first] = 6
b[second] = 6
head = counts_text(a) + " срещу " + counts_text(b) + ": "
run_battle(a, b, "a", "b", [])
if total(a) > 0 and total(b) == 0:
print(head + "печели " + LETTERS[first] + ", остават " + counts_text(a))
elif total(b) > 0 and total(a) == 0:
print(head + "печели " + LETTERS[second] + ", остават " + counts_text(b))
else:
print(head + "без победител")
Изход
6I срещу 6B: печели I, остават 3I
6B срещу 6F: печели B, остават 4B
6I срещу 6F: печели F, остават 6F
6I срещу 6D: печели D, остават 6D

Триъгълникът се вижда в числата. Шест прехващача побеждават шест бомбардировача и губят три. Шест бомбардировача побеждават шест фрегати и губят два. Шест фрегати унищожават шест прехващача без нито една загуба: прехващачите падат още в първия рунд. Дредноутът (90 живот, атака 18) е друг случай: шест дредноута унищожават шест прехващача, също без загуба, защото всеки от тях издържа много повече (в играта той иска технология и повече ресурси). „Кой е по-силен“ няма един отговор: зависи от ролята на врага. Затова и силата на флота (сборът от силите на ролите) е само груба оценка.

Същото се прави в играта с цели игри: --simulate=200 пуска двеста игри без екран и печата кой печели. Това е темата на последния урок. Битките в тях са тази функция, пускана хиляди пъти.

Пробвай

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

Три бомбардировача срещу три прехващача. Кой печели и колко рунда трае битката? Не пускай програмата: пресметни рундовете на ръка с таблиците от урока (живот 10 и атака 5 за прехващача, 16 и 7 за бомбардировача) и не забравяй раните.

a = [0, 3, 0, 0, 0]
b = [3, 0, 0, 0, 0]
log = []
run_battle(a, b, "Земна империя", "Хегемония Орион", log)
for line in log:
print(line)
Отговор

Пробвай

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

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

a = [2, 0, 0, 0, 0]
b = [0, 2, 0, 0, 0]
log = []
run_battle(a, b, "a", "b", log)
for line in log:
print(line)
Изход
Рунд 1: 2I | 2B загуби 0 / 1
Рунд 2: 2I | 1B загуби 0 / 1
Победа за a. Остават: 2I.

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

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

По-точно

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

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

Технологиите дават бонуси на атаката и на живота (и допълнителни за бомбардировачите и за фрегатите). Те влизат през stats на volley и applyDamage: един и същ масив с брой кораби се бие различно за две империи. Затова параметърът го има в канона, а тук не.

Крепостите са пета роля (FORT_KIND): планетите на една империя в системата им дават станции, които стрелят и падат като кораби. След битката оцелелите станции се пазят до края на хода, а в началото на следващия са пълни.

Повече от две империи в една система се бият по двойки: за всяка двойка във война и с кораби или станции там се вика една битка. Редът на двойките е фиксиран, затова изходът е определен.

Няма отстъпление по средата на битката и няма избор на цел. Отстъплението е заповед за движение, която се изпълнява преди битката. Групата винаги стреля по най-изгодната роля; по-богата версия би дала на играча команда „фокус върху една роля“, но тогава битката вече не би била функция само на двата флота.

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

Упражнение

Битка по входа

Реши задачата в редактора си.

Битка по входа. Програмата чете от входа два реда по пет числа: броя на I, B, F, D и # за флота a, после същото за b. Печата редовете на битката като в урока: един ред на рунд и последния ред с победата (с имена „a“ и „b“; няма заглавие).

С вход 5 0 4 0 0 | 0 6 2 0 0 (по един флот на ред) изходът е:

Изход
Рунд 1: 5I 4F | 6B 2F загуби 4 / 2
Рунд 2: 3I 2F | 4B 2F загуби 4 / 3
Рунд 3: 1I | 2B 1F загуби 1 / 0
Победа за b. Остават: 2B 1F.

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

Още задачи

  • Колко прехващача трябват? За колко прехващача (от 1 до 30) побеждават 4 фрегати? Печатай първото число, при което a печели. Пробвай и с бомбардировачи срещу 4 фрегати.
  • Размяна на реда. Пусни една и съща битка като (a, b) и като (b, a). Едно и също ли е? Защо?
  • Дредноут. Добави на a дредноут. Колко прехващача на b трябват, за да го унищожат?
  • Празен флот. Какво прави програмата, ако едната страна няма кораби? Не трябва да се чупи.

Тест

Провери се

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

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