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

Ходът на галактиката

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

Проблем

Всички играят наведнъж, а редът решава

В стратегията по ходове играчът пише end (или wait 3 за три хода подред) и цялата галактика се мърда: съперниците решават какво да правят, флотите тръгват, стават битки, планетите произвеждат, понякога дойдат пирати. Това, което за играча е едно натискане на клавиш, в кода е ход: поредица от фази в твърд ред.

Редът не е украса. Вземи най-простия пример: три флота във верига, A→B, B→C, C→D, и всичките 3 кораба са в A. Ако обработваш поръчките една след друга и местиш корабите в същия масив, корабите стигат от A до D за един ход: първата поръчка ги слага в B, втората ги вижда там и ги праща в C, третата в D. Играч, който види това, разбира веднага, че флотите са телепорти.

Този урок строи хода на малка галактика от три империи (A, B, C) и четири системи по права линия, със същите четири идеи като в канона:

  • фазите на хода и едновременното движение;
  • случайните събития: колко често идват и кои по-често;
  • мирът и войната: правило, което не позволява да се обявява и сключва мир всеки ход;
  • шината от събития: как новините, озлобването и сигналите научават какво се е случило, без никой да вика никого.

В края има цял ход от фази, който печата какво става, и сцена, която го показва.

Идеята

Фази и едновременно движение

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

Движението се разбива на две: първо всички флоти напускат системите си, чак тогава пристигат. Ето двата начина върху веригата от по-горе. Поръчката е [от, към, кораби]; корабите са 3 в системата 0:

# [от, към, кораби]
ORDERS = [(0, 1, 3), (1, 2, 3), (2, 3, 3)]
def move_one_by_one(start, orders):
ships = start[:]
for source, target, count in orders:
take = min(count, ships[source])
ships[source] -= take
ships[target] += take
return ships
def move_together(start, orders):
ships = start[:]
takes = []
# Първо всички флоти напускат системите си...
for source, target, count in orders:
take = min(count, ships[source])
ships[source] -= take
takes.append(take)
# ...и чак тогава пристигат.
for i, order in enumerate(orders):
ships[order[1]] += takes[i]
return ships
start = [3, 0, 0, 0]
print("последователно: " + show(move_one_by_one(start, ORDERS)))
print("едновременно: " + show(move_together(start, ORDERS)))
Изход
последователно: 0 0 0 3
едновременно: 0 3 0 0

Последователният вариант е тихо грешен: програмата не се чупи, флотите просто стават бързи. Едновременният взима за всяка поръчка min(кораби от поръчката, кораби в системата в началото) и ги маха (напускане), а после ги слага (пристигане). Втората поръчка (B→C) вижда в B нула кораби, защото първите още не са пристигнали, и не мести нищо. Затова веригата е 0 3 0 0: корабите са стигнали само до B.

start[:] е копие на списъка: без него функцията би променила списъка на викащия.
По-точно, за любопитните

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

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

Идеята

Случайни събития по тегла

На всеки ход с шанс 40% се случва едно събитие на случайна империя. Кое точно? Ако всички видове имат равен шанс, пиратите ще идват на първия ход, когато империята няма и един кораб. Затова всеки вид има тегло (колко често е) и най-ранен ход (от кога е позволен). Ето таблицата на канона и избора:

# id, тегло, най-ранен ход
EVENT_DEFS = [
("meteor", 20, 1),
("vein", 15, 1),
("pirates", 20, 6),
("breakthrough", 15, 1),
("unrest", 10, 5),
("boom", 15, 1),
("flare", 10, 1),
("derelict", 8, 3)
]
def pick_weighted(rng, weights):
total = sum(weights)
roll = rng.next_int(total)
for i, weight in enumerate(weights):
if roll < weight:
return i
roll -= weight
return len(weights) - 1
def roll_event(rng, turn):
if not rng.chance(EVENT_CHANCE):
return None
empire = rng.next_int(len(NAMES))
weights = [weight if turn >= min_turn else 0 for _, weight, min_turn in EVENT_DEFS]
return (pick_weighted(rng, weights), empire)

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

rng = Rng(7)
for turn in range(1, 13):
roll = roll_event(rng, turn)
if roll is not None:
print("ход " + str(turn) + ": " + EVENT_DEFS[roll[0]][0] + " → " + NAMES[roll[1]])
counts = [0] * len(EVENT_DEFS)
for _ in range(1000):
roll = roll_event(rng, 99)
if roll is not None:
counts[roll[0]] += 1
print("Събития за 1000 хода:")
for i, (event_id, weight, _) in enumerate(EVENT_DEFS):
print(event_id + " " + str(weight) + ": " + str(counts[i]))
Изход
ход 1: boom → Съюз Лира
ход 2: breakthrough → Академия Вега
ход 3: vein → Академия Вега
ход 4: boom → Академия Вега
ход 8: pirates → Съюз Лира
ход 9: breakthrough → Земна империя
Събития за 1000 хода:
meteor 20: 70
vein 15: 53
pirates 20: 78
breakthrough 15: 58
unrest 10: 30
boom 15: 50
flare 10: 28
derelict 8: 28

Пиратите (най-ранен ход 6) се виждат чак на ход 8. В хилядата хода най-често излизат пиратите (78) и метеоритите (70) с тегло 20, най-рядко изригването (28) и изоставените кораби (28) с тегла 10 и 8. Бройките не са равни на теглата, защото е случайно, но редът им е същият. Шансът за събитие е 40% (EVENT_CHANCE), а генераторът е със семе: един и същ вход дава един и същи изход на четирите езика, затова записът на играта може да се повтори.

По-точно, за любопитните

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

# Случайните събития: на всеки ход с 40% шанс се случва едно на случайна империя. Видът е по
# тегла (само от най-ранния му ход нататък). Всичко се публикува в шината; новините, статистиката
# и озлоблението са абонати, а събитията не знаят за тях.
EVENT_CHANCE = 40
def pick_empire_planet(world, empire):
items = owned_planets(world, empire.id)
if len(items) == 0:
return None
return items[world.rng.next_int(len(items))]
def pirate_fleet(world):
strength = world.turn // 6
return [2 + strength, strength // 2, strength // 3, 0, 0]
def apply_event(world, kind, empire):
rng = world.rng
values = {'empire': empire_name(empire)}
if kind == 'meteor':
planet = pick_empire_planet(world, empire)
if planet is None:
return None
loss = min_of(empire.res[0], 8 + rng.next_int(8))
empire.res[0] -= loss
values['planet'] = planet_name(world, planet)
values['n'] = loss
elif kind == 'vein':
planet = pick_empire_planet(world, empire)
if planet is None:
return None
r = rng.next_int(RES_COUNT)
tries = 0
while planet.rich[r] >= 5 and tries < RES_COUNT:
r = (r + 1) % RES_COUNT
tries += 1
if planet.rich[r] >= 5:
return None
planet.rich[r] += 1
values['planet'] = planet_name(world, planet)
values['res'] = t('res.' + str(r))
elif kind == 'pirates':
planet = pick_empire_planet(world, empire)
if planet is None:
return None
system_id = planet.system_id
mine = empty_counts()
fleet = fleet_at(world, empire.id, system_id)
for k in range(SHIP_KIND_COUNT):
mine[k] = 0 if fleet is None else fleet.ships[k]
mine[FORT_KIND] = forts_in(world, empire.id, system_id)
result = run_battle(side_stats(empire), mine, side_stats(None), pirate_fleet(world))
for k in range(SHIP_KIND_COUNT):
if fleet is not None:
fleet.ships[k] = result.a[k]
values['system'] = world.systems[system_id].name
values['fleet'] = counts_text(pirate_fleet(world))
if result.winner == 'a':
empire.res[0] += 20
empire.res[1] += 10
values['outcome'] = t('event.pirates.win')
else:
m = empire.res[0] // 5
g = empire.res[1] // 5
empire.res[0] -= m
empire.res[1] -= g
values['outcome'] = t('event.pirates.lose', {'m': m, 'g': g})
if not world.quiet:
world.last.battle = battle_lines(values['system'], empire_name(empire), t('pirates'), result)
world.stats.battles += 1
world.stats.lost += total_of(mine) - total_of(result.a) + total_of(pirate_fleet(world)) - total_of(result.b)
elif kind == 'breakthrough':
n = 6 + rng.next_int(7)
empire.progress += n
values['n'] = n
elif kind == 'unrest':
planet = pick_empire_planet(world, empire)
if planet is None or planet.mines == 0:
return None
planet.mines -= 1
values['planet'] = planet_name(world, planet)
elif kind == 'boom':
empire.res[1] += 20
empire.res[2] += 10
elif kind == 'flare':
empire.flare = 1
elif kind == 'derelict':
planet = pick_empire_planet(world, empire)
if planet is None:
return None
add_ships(world, empire.id, planet.system_id, [0, 0, 2, 0])
values['system'] = world.systems[planet.system_id].name
return values
def roll_event(world):
rng = world.rng
if not rng.chance(EVENT_CHANCE):
return
living = []
for e in world.empires:
if e.alive:
living.append(e)
if len(living) == 0:
return
empire = living[rng.next_int(len(living))]
allowed = []
weights = []
for definition in EVENT_DEFS:
if world.turn >= definition.min_turn:
allowed.append(definition)
weights.append(definition.weight)
definition = allowed[pick_weighted(rng, weights)]
values = apply_event(world, definition.id, empire)
if values is None:
return
world.stats.events += 1
world.bus.publish(SimpleNamespace(type='event', kind=definition.id, empire=empire.id, vars=values))
# Абонатите: новините за играча, озлобването след битки и завладяване.
def add_news(world, text):
world.news.append(str(world.turn) + ': ' + text)
while len(world.news) > 60:
world.news.pop(0)
world.turn_lines.append(text)
def install_subscribers(world, quiet):
bus = world.bus
def involves_human(a, b):
return world.empires[a].human or world.empires[b].human
def grudge_after_battle(e):
if e.lost_a > 0:
world.empires[e.b].grudge[e.a] += 1
if e.lost_b > 0:
world.empires[e.a].grudge[e.b] += 1
def grudge_after_conquest(e):
if e.from_ >= 0:
world.empires[e.from_].grudge[e.empire] += 3
# Сигнал към играча: нещо се случи, при което „wait“ спира.
def alert_battle(e):
if involves_human(e.a, e.b):
world.alert += 1
def alert_conquest(e):
if world.empires[e.empire].human or (e.from_ >= 0 and world.empires[e.from_].human):
world.alert += 1
def alert_war(e):
if involves_human(e.a, e.b):
world.alert += 1
def alert_siege(e):
if world.empires[e.from_].human:
world.alert += 1
def alert_offer(e):
if world.empires[e.to].human:
world.alert += 1
bus.subscribe('battle', grudge_after_battle)
bus.subscribe('conquest', grudge_after_conquest)
bus.subscribe('battle', alert_battle)
bus.subscribe('conquest', alert_conquest)
bus.subscribe('war', alert_war)
bus.subscribe('siege', alert_siege)
bus.subscribe('offer', alert_offer)
if quiet:
return
def news_battle(e):
if involves_human(e.a, e.b):
for line in e.lines:
add_news(world, line)
else:
add_news(world, t('news.battle', {
'system': world.systems[e.system_id].name, 'a': empire_name(world.empires[e.a]), 'b': empire_name(world.empires[e.b]),
'la': e.lost_a, 'lb': e.lost_b,
}))
def news_colony(e):
add_news(world, t('news.colony', {'empire': empire_name(world.empires[e.empire]), 'planet': planet_name(world, world.planets[e.planet_id])}))
def news_tech(e):
if world.empires[e.empire].human:
add_news(world, t('news.tech', {'tech': nm(TECHS[e.tech_id])}))
def news_war(e):
add_news(world, t('news.war', {'a': empire_name(world.empires[e.a]), 'b': empire_name(world.empires[e.b])}))
def news_peace(e):
add_news(world, t('news.peace', {'a': empire_name(world.empires[e.a]), 'b': empire_name(world.empires[e.b])}))
def news_offer(e):
add_news(world, t('news.offer', {'empire': empire_name(world.empires[e.from_]), 'letter': world.empires[e.from_].def_.letter}))
def news_siege(e):
add_news(world, t('news.siege', {'empire': empire_name(world.empires[e.empire]), 'planet': planet_name(world, world.planets[e.planet_id])}))
def news_conquest(e):
add_news(world, t('news.conquest', {'empire': empire_name(world.empires[e.empire]), 'planet': planet_name(world, world.planets[e.planet_id])}))
def news_eliminated(e):
add_news(world, t('news.eliminated', {'empire': empire_name(world.empires[e.empire])}))
def news_event(e):
add_news(world, t('event.' + e.kind, e.vars))
bus.subscribe('battle', news_battle)
bus.subscribe('colony', news_colony)
bus.subscribe('tech', news_tech)
bus.subscribe('war', news_war)
bus.subscribe('peace', news_peace)
bus.subscribe('offer', news_offer)
bus.subscribe('siege', news_siege)
bus.subscribe('conquest', news_conquest)
bus.subscribe('eliminated', news_eliminated)
bus.subscribe('event', news_event)

Идеята

Мир и война: правило срещу бъркотията

Дипломацията в играта е два обекта: двойка империи е във война или в мир (war[a][b]). Кое е трудното? Без правило империя би обявявала война на един ход, сключвала мир на следващия и пак война след него. Затова:

  • мирът важи най-малко MIN_PEACE = 4 хода преди нова война (turn − since < MIN_PEACE; в началото на играта правилото не важи, за да може някой да започне);
  • предложение за мир е позволено най-рано два хода след промяната;
  • изкуственият интелект приема мир по желание: колко е по-слаб от другия, какъв е характерът му, колко други войни води и колко е озлобен. От 90 нагоре приема.

В програмата желанието е опростено: характер + (150 − съотношение на корабите) / 2 + други войни × 15 − озлобление × 3, където характерът е 40 за Орион (воин) и 120 за двете други (строители). Отказът увеличава озлоблението, затова следващият опит е по-труден. Сценарият е от историята на сцената: Орион (B) обявява война на Земната империя (A) на ход 1, Лира (C) на Орион на ход 3, а A предлага мир:

class World:
def __init__(self):
self.turn = 1
self.ships = [[0] * 4 for _ in range(3)]
self.war = [[0] * 3 for _ in range(3)]
self.since = [[0] * 3 for _ in range(3)]
self.grudge = [[0] * 3 for _ in range(3)]
def at_war(world, a, b):
return world.war[a][b] == 1
def power(world, empire):
return sum(world.ships[empire])
def set_stance(world, a, b, war):
world.war[a][b] = war
world.war[b][a] = war
world.since[a][b] = world.turn
world.since[b][a] = world.turn
def declare_war(world, a, b):
if at_war(world, a, b):
return "alreadyWar"
# Мирът важи най-малко MIN_PEACE хода.
if world.turn - world.since[a][b] < MIN_PEACE and world.turn > MIN_PEACE:
return "tooSoon"
set_stance(world, a, b, 1)
return None
def make_peace(world, a, b):
set_stance(world, a, b, 0)
world.grudge[a][b] = 0
world.grudge[b][a] = 0
def peace_wish(world, me, other):
# Колко по-слаба е империята от другата.
ratio = min(150, power(world, me) * 100 // (power(world, other) + 1))
other_wars = 0
for x in range(3):
if x != me and x != other and at_war(world, me, x):
other_wars += 1
return max(0, PEACE[me] + (150 - ratio) // 2 + other_wars * 15 - world.grudge[me][other] * 3)
def propose_peace(world, a, b):
if not at_war(world, a, b):
return "noWar"
if world.turn - world.since[a][b] < 2:
return "tooSoon"
# От 90 нагоре империята приема мир.
if peace_wish(world, b, a) >= 90:
make_peace(world, a, b)
return None
# Отказът озлобява: следващият мир е по-труден.
world.grudge[b][a] += 1
return "refused"
def step(world, turn, kind, a, b):
world.turn = turn
wish = peace_wish(world, b, a) if kind == "peace" else 0
error = declare_war(world, a, b) if kind == "war" else propose_peace(world, a, b)
shown = " (желание " + str(wish) + ")" if kind == "peace" and error in (None, "refused") else ""
print("ход " + str(turn) + ": " + kind + " " + NAMES[a] + " → " + NAMES[b] + ": " + (error or "ok") + shown)
world = World()
world.ships[1][1] = 1
world.grudge[1][0] = 1
step(world, 1, "war", 1, 0)
step(world, 2, "war", 1, 0)
step(world, 2, "peace", 0, 1)
step(world, 3, "war", 2, 1)
step(world, 3, "peace", 0, 1)
world.ships[1][1] = 0
step(world, 4, "peace", 0, 1)
step(world, 5, "war", 1, 0)
step(world, 9, "war", 1, 0)
Изход
ход 1: war Хегемония Орион → Земна империя: ok
ход 2: war Хегемония Орион → Земна империя: alreadyWar
ход 2: peace Земна империя → Хегемония Орион: tooSoon
ход 3: war Съюз Лира → Хегемония Орион: ok
ход 3: peace Земна империя → Хегемония Орион: refused (желание 77)
ход 4: peace Земна империя → Хегемония Орион: ok (желание 124)
ход 5: war Хегемония Орион → Земна империя: tooSoon
ход 9: war Хегемония Орион → Земна империя: ok

Ход 2: Орион пак обявява война на същия противник (alreadyWar), а A предлага мир едва един ход след войната (tooSoon). На ход 3 A предлага мир, но желанието на Орион е 40 + 25 + 15 − 3 = 77 (характер, един кораб срещу нула, още една война с Лира, озлобление 1) и е под 90: отказ, а озлоблението става 2. На ход 4 Орион няма кораби и желанието му е 40 + 75 + 15 − 6 = 124: приема. Ход 5: Орион иска нова война, но мирът е от ход 4 и 5 − 4 = 1 е по-малко от 4: tooSoon. На ход 9 вече е позволено (9 − 4 = 5).

def at_war(world, a, b):
    return world.war[a][b] == 1


def power(world, empire):
    return sum(world.ships[empire])


def set_stance(world, a, b, war):
    world.war[a][b] = war
    world.war[b][a] = war
    world.since[a][b] = world.turn
    world.since[b][a] = world.turn


def declare_war(world, a, b):
    if at_war(world, a, b):
        return "alreadyWar"
    # Мирът важи най-малко MIN_PEACE хода.
    if world.turn - world.since[a][b] < MIN_PEACE and world.turn > MIN_PEACE:
        return "tooSoon"
    set_stance(world, a, b, 1)
    return None


def make_peace(world, a, b):
    set_stance(world, a, b, 0)
    world.grudge[a][b] = 0
    world.grudge[b][a] = 0


def peace_wish(world, me, other):
    # Колко по-слаба е империята от другата.
    ratio = min(150, power(world, me) * 100 // (power(world, other) + 1))
    other_wars = 0
    for x in range(3):
        if x != me and x != other and at_war(world, me, x):
            other_wars += 1
    return max(0, PEACE[me] + (150 - ratio) // 2 + other_wars * 15 - world.grudge[me][other] * 3)


def propose_peace(world, a, b):
    if not at_war(world, a, b):
        return "noWar"
    if world.turn - world.since[a][b] < 2:
        return "tooSoon"
    # От 90 нагоре империята приема мир.
    if peace_wish(world, b, a)  90:
        make_peace(world, a, b)
        return None
    # Отказът озлобява: следващият мир е по-труден.
    world.grudge[b][a] += 1
    return "refused"
По-точно, за любопитните

Резултатът от функциите е низ или нищо: null, ако промяната е станала, и ключ на грешка (alreadyWar, tooSoon, refused, noWar), ако не. Грешките са ключове, а не готови изречения, за да ги превежда екранът (играта е на четири езика). Така същата функция служи и на играча (команда peace B), и на изкуствения интелект.

Канонът има и предложения към играча: изкуственият интелект не може да сключи мир с човека сам, затова записва предложение (offers), което важи три хода, и публикува събитие „offer“. Ето целия регион diplomacy:

# Дипломацията е мир или война между две империи. Мирът важи най-малко MIN_PEACE хода преди
# нова война. Изкуственият интелект приема мир по мерило: колко е по-слаб, какъв е характерът
# му и колко е озлобен. Предложение към играча се записва и важи три хода.
def at_war(world, a, b):
return world.empires[a].war[b] == 1
def set_stance(world, a, b, war):
world.empires[a].war[b] = war
world.empires[b].war[a] = war
world.empires[a].since[b] = world.turn
world.empires[b].since[a] = world.turn
def declare_war(world, a, b):
if a == b:
return 'dip.self'
if not world.empires[b].alive:
return 'dip.dead'
if at_war(world, a, b):
return 'dip.alreadyWar'
if world.turn - world.empires[a].since[b] < MIN_PEACE and world.turn > MIN_PEACE:
return 'dip.tooSoon'
set_stance(world, a, b, 1)
world.bus.publish(SimpleNamespace(type='war', a=a, b=b))
return None
def make_peace(world, a, b):
set_stance(world, a, b, 0)
world.empires[a].grudge[b] = 0
world.empires[b].grudge[a] = 0
world.bus.publish(SimpleNamespace(type='peace', a=a, b=b))
# Най-малкото разстояние в пътища между системи с планети на двете империи.
def empire_distance(world, a, b):
best = 99
for pa in world.planets:
if pa.owner != a:
continue
dist = bfs(world.systems, pa.system_id).dist
for pb in world.planets:
if pb.owner == b and dist[pb.system_id] >= 0 and dist[pb.system_id] < best:
best = dist[pb.system_id]
return best
def power_ratio(world, a, b, top):
return clamp(percent(empire_power(world, a) * 100, empire_power(world, b) * 100 + 100), 0, top)
# Колко иска империята мир с другата (0 и нагоре; от 90 нагоре приема).
def peace_wish(world, me, other):
e = world.empires[me]
weaker = 150 - power_ratio(world, me, other, 150)
other_wars = 0
for x in world.empires:
if x.alive and x.id != me and x.id != other and e.war[x.id] == 1:
other_wars += 1
wish = e.personality.peace + weaker // 2 + other_wars * 15 - e.grudge[other] * 3
return max_of(0, wish)
# Колко иска империята война с другата.
def war_wish(world, me, other):
e = world.empires[me]
ratio = power_ratio(world, me, other, 300)
wish = e.personality.attack // 2
if ratio > 100:
wish += (ratio - 100) // 4
else:
wish -= (100 - ratio) // 2
if empire_distance(world, me, other) <= 3:
wish += 25
for x in world.empires:
if x.alive and x.id != me and e.war[x.id] == 1:
wish -= 30
return wish
def has_offer(world, src, to):
for offer in world.offers:
if offer.from_ == src and offer.to == to and world.turn - offer.turn <= 3:
return True
return False
# Предложение за мир от a към b. Връща ключ на грешка или None (мирът е сключен).
def propose_peace(world, a, b):
if a == b:
return 'dip.self'
if not world.empires[b].alive:
return 'dip.dead'
if not at_war(world, a, b):
return 'dip.noWar'
if world.turn - world.empires[a].since[b] < 2:
return 'dip.tooSoon'
if world.empires[b].human:
if has_offer(world, b, a):
make_peace(world, a, b)
return None
if not has_offer(world, a, b):
world.offers.append(SimpleNamespace(from_=a, to=b, turn=world.turn))
return 'dip.sent'
if has_offer(world, b, a) or peace_wish(world, b, a) >= 90:
make_peace(world, a, b)
return None
world.empires[b].grudge[a] += 1
return 'dip.refused'
# Дипломацията на изкуствения интелект в края на хода: мир, когато е по-слаб, война, когато е по-силен.
def ai_diplomacy(world, empire):
for other in world.empires:
if not other.alive or other.id == empire.id:
continue
if at_war(world, empire.id, other.id):
if world.turn - empire.since[other.id] >= 3 and peace_wish(world, empire.id, other.id) >= 90:
if other.human:
if not has_offer(world, empire.id, other.id):
world.offers.append(SimpleNamespace(from_=empire.id, to=other.id, turn=world.turn))
world.bus.publish(SimpleNamespace(type='offer', from_=empire.id, to=other.id))
else:
propose_peace(world, empire.id, other.id)
elif world.turn - empire.since[other.id] >= MIN_PEACE + 2 and world.turn > MIN_PEACE + 2:
if war_wish(world, empire.id, other.id) >= 95 and world.rng.chance(40):
declare_war(world, empire.id, other.id)

Идеята

Шина от събития: който публикува, не знае кой слуша

Когато се обяви война, трябва да стане много: да се напише новина, да порасне озлобването на двете империи, да светне сигнал на играча, да се преброи в статистиката. Най-простото е declareWar да вика всичко това. Тогава declareWar знае за новините, за озлобването, за сигнала и за статистиката, а всяка нова възможност е още един ред в нея и още една причина да се счупи. Шината обръща посоката: declareWar казва „обявена е война между A и B“ и свършва. Който се интересува, се е абонирал за „war“.

@dataclass(frozen=True)
class Event:
type: str
a: int
b: int
place: int = 0
n: int = 0
class EventBus:
def __init__(self):
self.handlers = {}
def subscribe(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def publish(self, event):
if event.type not in self.handlers:
return
for handler in list(self.handlers[event.type]):
handler(event)

subscribe записва обработчик за вид събитие, publish вика всички обработчици на този вид. Вид без абонати не е грешка: събитието отива в празното. Ето публикуването в двете места, където става (обява на война и сключване на мир). Единственото ново е редът с publish:

def declare_war(world, a, b):
if at_war(world, a, b):
return "alreadyWar"
# Мирът важи най-малко MIN_PEACE хода.
if world.turn - world.since[a][b] < MIN_PEACE and world.turn > MIN_PEACE:
return "tooSoon"
set_stance(world, a, b, 1)
world.bus.publish(Event("war", a, b))
return None
def make_peace(world, a, b):
set_stance(world, a, b, 0)
world.grudge[a][b] = 0
world.grudge[b][a] = 0
world.bus.publish(Event("peace", a, b))

А ето програмата. Новините слушат всички видове, озлоблението само битките, а report публикува и неуспешните опити (refused, tooSoon) като събития:

world = World()
world.ships[1][1] = 1
world.grudge[1][0] = 1
def grudge(event):
world.grudge[event.a][event.b] += 1
world.grudge[event.b][event.a] += 1
# Новините слушат всички видове събития, озлобването само битките.
for event_type in NEWS_TYPES:
world.bus.subscribe(event_type, lambda event: print(news_text(event)))
world.bus.subscribe("battle", grudge)
world.turn = 1
report(world, declare_war(world, 1, 0), 1, 0)
world.bus.publish(Event("battle", 1, 0, 1, 2))
world.turn = 3
report(world, propose_peace(world, 0, 1), 0, 1)
report(world, declare_war(world, 1, 0), 1, 0)
print("озлобление " + NAMES[0] + " → " + NAMES[1] + ": " + str(world.grudge[0][1]))
print("озлобление " + NAMES[1] + " → " + NAMES[0] + ": " + str(world.grudge[1][0]))
Изход
Хегемония Орион обявява война на Земна империя.
Битка при Vesta: Хегемония Орион и Земна империя губят кораби (по 2).
Хегемония Орион отказва мир с Земна империя.
Хегемония Орион вече е във война с Земна империя.
озлобление Земна империя → Хегемония Орион: 1
озлобление Хегемония Орион → Земна империя: 3

Опитът на ход 1 е успешен, битката е публикувана на ръка, отказът на ход 3 е събитие (refused), а повторната война е alreadyWar. Озлоблението на A към B е 1 (от битката), а на B към A е 3: едно старо, едно от битката и едно от отказа (отказът вдига озлоблението на този, който отказва, към предложилия).

Обработчикът е обикновена функция (или lambda); класът EventBus не знае нищо за нея освен че може да я вика.
По-точно, за любопитните

Без шина всяко ново нещо, което трябва да научи за война, иска промяна в declareWar. С шината нов абонат е само нов ред при създаването на света, а функцията за война не се пипа. Това е шаблонът наблюдател (observer) от модула за шаблони за дизайн.

Малка, но важна подробност: publish върти по копие на списъка с абонати. Обработчик може да се абонира за друго събитие, докато го викат; ако списъкът се променя по време на обхождането, абонатите се прескачат или се повтарят. Копието прави правилото ясно: новият абонат чува от следващото публикуване.

Редът на абонатите е редът на subscribe. Тук няма значение (новините и озлоблението не си пречат), но ако два абоната зависят един от друг, редът ще решава изхода, и това е причина да ги държиш независими.

Идеята

Целият ход

Сега всичко заедно. Светът е World: ход, фаза, генератор, шина, флотите на трите империи (по системи), метал и таблиците за война (war), от кой ход (since) и озлобление (grudge):

class World:
def __init__(self, seed):
self.turn = 1
self.phase = ""
self.rng = Rng(seed)
self.bus = EventBus()
self.ships = [[3, 0, 0, 0], [0, 0, 0, 4], [0, 2, 0, 0]]
self.metal = [50, 50, 50]
self.war = [[0] * 3 for _ in range(3)]
self.since = [[0] * 3 for _ in range(3)]
self.grudge = [[0] * 3 for _ in range(3)]

Поръчките и дипломацията на хода са сценарий (в играта ги решава изкуственият интелект или играчът): поръчката е [ход, империя, от, към, кораби], а дипломацията [ход, вид, от, към].

# [ход, империя, от, към, кораби]
ORDERS = [(1, 1, 3, 2, 4), (1, 0, 0, 1, 3), (2, 1, 2, 1, 4)]
# [ход, вид (0 война, 1 мир), от, към]
DIPLO = [(1, 0, 1, 0), (3, 0, 2, 1), (3, 1, 0, 1), (4, 1, 0, 1), (5, 0, 1, 0)]

Фазите са функции. Дипломацията изпълнява сценария за хода и публикува грешките. Движението е точно двете стъпки от първата карта. Битките обхождат системите: където две империи във война имат кораби, всяка губи толкова, колкото е по-малката флота, и се публикува „battle“ (озлоблението се абонира за него):

def scripted_diplomacy(world):
world.phase = "дипломация"
for turn, kind, a, b in DIPLO:
if turn != world.turn:
continue
error = declare_war(world, a, b) if kind == 0 else propose_peace(world, a, b)
report(world, error, a, b)
def resolve_movement(world):
world.phase = "движение"
moving = []
# Първо всички флоти напускат системите си...
for turn, empire, source, target, count in ORDERS:
if turn != world.turn:
continue
take = min(count, world.ships[empire][source])
world.ships[empire][source] -= take
if take > 0:
moving.append((empire, source, target, take))
# ...и чак тогава пристигат.
for empire, source, target, take in moving:
world.ships[empire][target] += take
world.bus.publish(Event("move", empire, source, target, take))
def resolve_battles(world):
world.phase = "битки"
for system in range(4):
for a in range(3):
for b in range(a + 1, 3):
if not at_war(world, a, b) or world.ships[a][system] == 0 or world.ships[b][system] == 0:
continue
lost = min(world.ships[a][system], world.ships[b][system])
world.ships[a][system] -= lost
world.ships[b][system] -= lost
world.bus.publish(Event("battle", a, b, system, lost))
def produce(world):
world.phase = "производство"
for empire in range(3):
world.metal[empire] += 10

След тях е събитието: теглене по теглата от втората карта, с тези ефекти: метеор отнема метал, бум дава метал, изоставените кораби дават два кораба в столицата. Накрая runTurn ги вика в ред:

def roll_event(world):
world.phase = "събитие"
if not world.rng.chance(EVENT_CHANCE):
return
empire = world.rng.next_int(3)
weights = [weight if world.turn >= min_turn else 0 for _, weight, min_turn in EVENT_DEFS]
kind = EVENT_DEFS[pick_weighted(world.rng, weights)][0]
if kind == "meteor":
loss = min(world.metal[empire], 8 + world.rng.next_int(8))
world.metal[empire] -= loss
world.bus.publish(Event("meteor", empire, 0, 0, loss))
elif kind == "boom":
world.metal[empire] += 20
world.bus.publish(Event("boom", empire, 0, 0, 20))
else:
world.ships[empire][HOME[empire]] += 2
world.bus.publish(Event("derelict", empire, 0, HOME[empire], 2))
def run_turn(world):
# Фазите вървят в този ред и редът е важен.
scripted_diplomacy(world)
resolve_movement(world)
resolve_battles(world)
produce(world)
roll_event(world)
world.turn += 1

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

world = World(7)
def grudge(event):
world.grudge[event.a][event.b] += 1
world.grudge[event.b][event.a] += 1
def news(event):
print(" [" + world.phase + "] " + news_text(event))
world.bus.subscribe("battle", grudge)
for event_type in NEWS_TYPES:
world.bus.subscribe(event_type, news)
for _ in range(5):
print(fill("-- ход {0} --", world.turn))
run_turn(world)
print("-- край --")
for empire in range(3):
print(fill("{0}: кораби {1}, метал {2}", NAMES[empire], power(world, empire), world.metal[empire]))
Изход
-- ход 1 --
[дипломация] Хегемония Орион обявява война на Земна империя.
[движение] Хегемония Орион праща флот от Brisa към Altan (кораби: 4).
[движение] Земна империя праща флот от Kora към Vesta (кораби: 3).
[събитие] Метеоритен дъжд: Съюз Лира губи 14 метал.
-- ход 2 --
[движение] Хегемония Орион праща флот от Altan към Vesta (кораби: 4).
[битки] Битка при Vesta: Земна империя и Хегемония Орион губят кораби (по 3).
-- ход 3 --
[дипломация] Съюз Лира обявява война на Хегемония Орион.
[дипломация] Хегемония Орион отказва мир с Земна империя.
[битки] Битка при Vesta: Хегемония Орион и Съюз Лира губят кораби (по 1).
-- ход 4 --
[дипломация] Мир между Земна империя и Хегемония Орион.
[събитие] Метеоритен дъжд: Съюз Лира губи 8 метал.
-- ход 5 --
[дипломация] Хегемония Орион още не може да смени отношенията си с Земна империя: прекалено скоро.
[събитие] Хегемония Орион намира изоставени кораби при Brisa (кораби: 2).
-- край --
Земна империя: кораби 0, метал 100
Хегемония Орион: кораби 2, метал 100
Съюз Лира: кораби 1, метал 78

Прочети изхода като история. Ход 1: Орион обявява война, двата флота (на Орион и на Земната империя) тръгват едновременно, а Лира губи метал от метеор. Ход 2: флотът на Орион пристига във Vesta, където вече е Земната империя, и двамата губят по 3 кораба. Ход 3: Лира се включва във войната и в боя във Vesta губят по 1; преди това A предлага мир и получава отказ. Ход 4: Орион няма кораби, желанието му е 124 и мирът е сключен. Ход 5: нова война е „прекалено скоро“, а Орион намира два изоставени кораба в столицата си.

Виж го

Един ход на галактиката

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

def run_turn(world):
# Фазите вървят в този ред и редът е важен.
scripted_diplomacy(world)
resolve_movement(world)
resolve_battles(world)
produce(world)
roll_event(world)
world.turn += 1
class EventBus:
def __init__(self):
self.handlers = {}
def subscribe(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def publish(self, event):
if event.type not in self.handlers:
return
for handler in list(self.handlers[event.type]):
handler(event)
Стъпка по стъпка, с думи
  1. A = Земна империя, B = Хегемония Орион, C = Съюз Лира. Четири системи по права линия; ходът минава през фазите отгоре надолу.
  2. ход 1 · дипломация: Хегемония Орион обявява война на Земна империя.
  3. ход 1 · движение: Хегемония Орион праща флот от Brisa към Altan (кораби: 4). Земна империя праща флот от Kora към Vesta (кораби: 3).
  4. ход 1 · събитие: Метеоритен дъжд: Съюз Лира губи 14 метал.
  5. ход 2 · движение: Хегемония Орион праща флот от Altan към Vesta (кораби: 4).
  6. ход 2 · битки: Битка при Vesta: Земна империя и Хегемония Орион губят кораби (по 3).
  7. ход 3 · дипломация: Съюз Лира обявява война на Хегемония Орион. Хегемония Орион отказва мир с Земна империя.
  8. ход 3 · битки: Битка при Vesta: Хегемония Орион и Съюз Лира губят кораби (по 1).
  9. ход 4 · дипломация: Мир между Земна империя и Хегемония Орион.
  10. ход 4 · събитие: Метеоритен дъжд: Съюз Лира губи 8 метал.
  11. ход 5 · дипломация: Хегемония Орион още не може да смени отношенията си с Земна империя: прекалено скоро.
  12. ход 5 · събитие: Хегемония Орион намира изоставени кораби при Brisa (кораби: 2).
  13. След 5 хода: Земна империя: кораби 0, метал 100 · Хегемония Орион: кораби 2, метал 100 · Съюз Лира: кораби 1, метал 78

Кодът до сцената е runTurn и EventBus от програмата. Светят редовете, които работят в момента.

Идеята

В играта

Това, което построихме, е същото като в канона, само по-малко. Ето истинския ход (регион turn). Над runTurn са движението, което прави същите две стъпки (напускане, пристигане), битките и завладяването на планети; runTurn вика фазите в твърд ред, а в края на хода озлоблението остарява (на всеки пети ход намалява с 1) и се изхвърлят остарелите предложения за мир.

# Един ход на галактиката има фази: дипломация на изкуствения интелект, поръчките му, движение на
# всички флотове наведнъж, битки, завладяване, производство и наука, събитие, край на хода.
# Движението е едновременно: първо всички кораби напускат системите си, чак тогава пристигат.
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)
def has_units(world, owner, system_id):
fleet = fleet_at(world, owner, system_id)
return (fleet is not None and total_of(fleet.ships) > 0) or forts_now(world, owner, system_id) > 0
def resolve_battles(world):
for system in world.systems:
for a in range(len(world.empires)):
for b in range(a + 1, len(world.empires)):
if not world.empires[a].alive or not world.empires[b].alive or not at_war(world, a, b):
continue
if has_units(world, a, system.id) and has_units(world, b, system.id):
fight_at(world, system.id, a, b)
remove_empty_fleets(world)
# Планета пада, когато враг е във война с владетеля ѝ, има кораби в системата, а владетелят няма
# нито кораби, нито оцелели крепости там, и така SIEGE_TURNS хода подред: първите ходове са обсада,
# последният е превземане. Защитникът има време да доведе флот. Постройките се разрушават.
def resolve_conquest(world):
for system in world.systems:
for pid in system.planets:
planet = world.planets[pid]
if planet.owner < 0:
continue
owner = planet.owner
attacker = -1
if not has_units(world, owner, system.id):
for e in world.empires:
if not e.alive or e.id == owner or not at_war(world, e.id, owner):
continue
fleet = fleet_at(world, e.id, system.id)
if fleet is not None and total_of(fleet.ships) > 0:
attacker = e.id
break
if attacker < 0:
planet.siege = 0
continue
planet.siege += 1
if planet.siege < SIEGE_TURNS:
world.bus.publish(SimpleNamespace(type='siege', empire=attacker, from_=owner, planet_id=planet.id))
continue
planet.siege = 0
planet.owner = attacker
planet.mines = max_of(0, planet.mines - 1)
planet.lab = 0
planet.yard = 0
planet.fort = 0
world.bus.publish(SimpleNamespace(type='conquest', empire=attacker, from_=owner, planet_id=planet.id))
for empire in world.empires:
if not empire.alive or len(owned_planets(world, empire.id)) > 0:
continue
empire.alive = False
kept = []
for fleet in world.fleets:
if fleet.owner != empire.id:
kept.append(fleet)
world.fleets = kept
empire.orders = []
world.bus.publish(SimpleNamespace(type='eliminated', empire=empire.id))
def run_turn(world):
world.turn_lines = []
world.fort_left = {}
for empire in world.empires:
if empire.alive and not empire.human:
ai_diplomacy(world, empire)
for empire in world.empires:
if empire.alive and not empire.human:
ai_play(world, empire)
resolve_movement(world)
resolve_battles(world)
resolve_conquest(world)
for empire in world.empires:
if not empire.alive:
continue
produce(world, empire)
complete_research(world, empire)
finish_queue(world)
roll_event(world)
check_victory(world)
world.turn += 1
if world.turn % 5 == 0:
for empire in world.empires:
for i in range(len(empire.grudge)):
empire.grudge[i] = max_of(0, empire.grudge[i] - 1)
fresh = []
for offer in world.offers:
if world.turn - offer.turn <= 3:
fresh.append(offer)
world.offers = fresh

А ето шината, която слушат новините, озлоблението и сигналът към играча. Събитията са един клас с повече полета (в JavaScript — обект), а шината е същата:

# Шината от събития: който публикува, не знае кой слуша. Новините, злопаметността на империите и
# статистиката се абонират сами, а битката само казва „флот се би при тази система“.
class EventBus:
def __init__(self):
self.handlers = {}
def subscribe(self, event_type, handler):
if event_type not in self.handlers:
self.handlers[event_type] = []
self.handlers[event_type].append(handler)
def publish(self, event):
if event.type not in self.handlers:
return
for handler in list(self.handlers[event.type]):
handler(event)

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

  • Няма сценарий. Поръчките и дипломацията идват от aiPlay и aiDiplomacy (урокът за съперниците) и от командите на играча.
  • Галактиката е граф (урокът за генерирането ѝ), а не права линия. Поръчката помни крайната цел и продължава сама.
  • Битката (fightAt, урокът за флотите) е с роли на корабите и по шест рунда, а не „всеки губи по по-малкото“. Има и крепости, и завладяване на планета (две обсади).
  • Желанието за мир ползва силата на империята (не броя кораби), характера ѝ и нейното желание за война (warWish). Предложение към играча се записва за три хода.
  • Производството тук е плоски 10 метал на империя; в канона зависи от планетите, мините и технологиите.
  • Събитията са осем вида, с по-различен ефект (пиратите се бият със същия модел на битка).
  • Шината носи и други видове: завладяване, обсада, откриване на технология, колония, унищожена империя. Новините и сигналът към играча ги слушат, а wait спира, щом сигналът порасне.

Пробвай

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

Два флота: първата поръчка праща 3-те кораба от системата 0 към 1, втората — 2 кораба от системата 1 към 2. Поръчките се обработват едновременно. Какво печата програмата?

def move_together(start, orders):
ships = start[:]
takes = []
# Първо всички флоти напускат системите си...
for source, target, count in orders:
take = min(count, ships[source])
ships[source] -= take
takes.append(take)
# ...и чак тогава пристигат.
for i, order in enumerate(orders):
ships[order[1]] += takes[i]
return ships
def show(ships):
return " ".join(str(n) for n in ships)
orders = [(0, 1, 3), (1, 2, 2)]
print(show(move_together([3, 0, 0, 0], orders)))
Отговор

Пробвай

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

Колега „оправи“ едновременното движение, но при него корабите пак минават по няколко системи на ход:

start = [3, 0, 0, 0]
print("едновременно: " + show(move_together(start, ORDERS)))
Изход
едновременно: 0 0 0 3

Резултатът е като при последователното движение. Кой ред праща корабите на местоназначението твърде рано?

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

По-точно

По-точно: ред, справедливост и случайност

Редът на играчите. При едновременно движение редът на поръчките не променя резултата, защото първо всички вземат от началото на хода. При битките обаче има ред: система по система, двойка по двойка (a < b). Ако на една система са три империи във война, първата двойка в реда се бие първа и оцелелите стигат до следващата. Редът е твърд, затова играта остава повторяема.

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

Случайност и повторяемост. Всичко случайно (шанс за събитие, коя империя, кой вид, колко метал) минава през един генератор със семе. Ако две фази си разменят местата и двете теглят числа, поредицата е друга. В нашата програма само rollEvent ползва генератора, затова изходът е стабилен; в канона дипломацията и изкуственият интелект също теглят числа, и редът на фазите решава кой взема кое число.

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

Упражнение

Сигнал за играча

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

Сигнал. В играта wait спира, щом нещо се случи на играча. Добави абонат „сигнал“, който брои събитията war, peace и battle, в които участва империя 0 (играчът): като a или като b. След обобщението програмата печата още един ред със сигналите (сигнали: …). Не пипай declareWar, makePeace и resolveBattles: абонатът е само нов ред при създаването на света и една функция.

Изходът е (последният ред е новият):

Изход
-- ход 1 --
[дипломация] Хегемония Орион обявява война на Земна империя.
[движение] Хегемония Орион праща флот от Brisa към Altan (кораби: 4).
[движение] Земна империя праща флот от Kora към Vesta (кораби: 3).
[събитие] Метеоритен дъжд: Съюз Лира губи 14 метал.
-- ход 2 --
[движение] Хегемония Орион праща флот от Altan към Vesta (кораби: 4).
[битки] Битка при Vesta: Земна империя и Хегемония Орион губят кораби (по 3).
-- ход 3 --
[дипломация] Съюз Лира обявява война на Хегемония Орион.
[дипломация] Хегемония Орион отказва мир с Земна империя.
[битки] Битка при Vesta: Хегемония Орион и Съюз Лира губят кораби (по 1).
-- ход 4 --
[дипломация] Мир между Земна империя и Хегемония Орион.
[събитие] Метеоритен дъжд: Съюз Лира губи 8 метал.
-- ход 5 --
[дипломация] Хегемония Орион още не може да смени отношенията си с Земна империя: прекалено скоро.
[събитие] Хегемония Орион намира изоставени кораби при Brisa (кораби: 2).
-- край --
Земна империя: кораби 0, метал 100
Хегемония Орион: кораби 2, метал 100
Съюз Лира: кораби 1, метал 78
сигнали: 3

Още задачи

  • Статистика. Добави още един абонат, който брои битките и сбора на загубените кораби (всяка битка губи n на всяка от двете страни). Печата ги след сигналите.
  • Примирие. Намали MIN_PEACE на 2 и пусни програмата. Кои редове от изхода се менят и защо ход 5 вече не е „прекалено скоро“?
  • Нов вид събитие. Добави plague с най-ранен ход 4 и тегло 10: империята губи 5 метал. Кои места променяш: таблицата, rollEvent, newsText, списъка с видовете за новините? А шината?

Тест

Провери се

1. Три поръчки са наредени във верига: A→B, B→C, C→D, по 3 кораба във всяка. В началото 3-те кораба са в A. Последователно (поръчка след поръчка) корабите по системи A, B, C, D стават 0 0 0 3 (всички в D), а едновременно 0 3 0 0 (всички в B). Защо?
2. В таблицата на канона 8 вида събитие имат тегла със сбор 113. На ход 3 пиратите (най-ранен ход 6) и размириците (5) още не са позволени. Каква е вероятността метеоритен дъжд (тегло 20) да е събитието, ако на хода се случва събитие?
3. Орион и Земната империя сключват мир на ход 4. На ход 5 Орион пак иска война. Защо му казват „прекалено скоро“?
4. Защо publish върти по копие на списъка с абонати, а не по самия списък?
5. Искаш да броиш колко пъти е обявена война. Какво променяш в declareWar?

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