Ходът на галактиката
Накратко: Ходът на галактиката е редица от фази в твърд ред; флотите се движат едновременно (първо тръгват всички, после пристигат), събитията се теглят по тегла, мирът и войната имат правило за най-малко MIN_PEACE хода, а новините и озлобването слушат шината от събития, без публикуващият да знае за тях.
Проблем
Всички играят наведнъж, а редът решава
В стратегията по ходове играчът пише end (или wait 3 за три хода подред) и цялата галактика се мърда: съперниците решават какво да правят, флотите тръгват, стават битки, планетите произвеждат, понякога дойдат пирати. Това, което за играча е едно натискане на клавиш, в кода е ход: поредица от фази в твърд ред.
Редът не е украса. Вземи най-простия пример: три флота във верига, A→B, B→C, C→D, и всичките 3 кораба са в A. Ако обработваш поръчките една след друга и местиш корабите в същия масив, корабите стигат от A до D за един ход: първата поръчка ги слага в B, втората ги вижда там и ги праща в C, третата в D. Играч, който види това, разбира веднага, че флотите са телепорти.
Този урок строи хода на малка галактика от три империи (A, B, C) и четири системи по права линия, със същите четири идеи като в канона:
- фазите на хода и едновременното движение;
- случайните събития: колко често идват и кои по-често;
- мирът и войната: правило, което не позволява да се обявява и сключва мир всеки ход;
- шината от събития: как новините, озлобването и сигналите научават какво се е случило, без никой да вика никого.
В края има цял ход от фази, който печата какво става, и сцена, която го показва.
Идеята
Фази и едновременно движение
Ходът е функция, която вика фази една след друга. В канона те са: дипломация на изкуствения интелект, неговите поръчки, движение на всички флотове, битки, завладяване, производство и наука, събитие, край на хода. В нашата малка галактика са пет: дипломация, движение, битки, производство, събитие.
Движението се разбива на две: първо всички флоти напускат системите си, чак тогава пристигат. Ето двата начина върху веригата от по-горе. Поръчката е [от, към, кораби]; корабите са 3 в системата 0:
// [от, към, кораби]int[][] orders = [[0, 1, 3], [1, 2, 3], [2, 3, 3]];
int[] MoveOneByOne(int[] start, int[][] orders){ var ships = (int[])start.Clone(); foreach (int[] order in orders) { int take = Math.Min(order[2], ships[order[0]]); ships[order[0]] -= take; ships[order[1]] += take; } return ships;}
int[] MoveTogether(int[] start, int[][] orders){ var ships = (int[])start.Clone(); var takes = new int[orders.Length]; // Първо всички флоти напускат системите си... for (int i = 0; i < orders.Length; i++) { takes[i] = Math.Min(orders[i][2], ships[orders[i][0]]); ships[orders[i][0]] -= takes[i]; } // ...и чак тогава пристигат. for (int i = 0; i < orders.Length; i++) ships[orders[i][1]] += takes[i]; return ships;}int[] start = [3, 0, 0, 0];Console.WriteLine("последователно: " + Show(MoveOneByOne(start, orders)));Console.WriteLine("едновременно: " + Show(MoveTogether(start, orders)));последователно: 0 0 0 3едновременно: 0 3 0 0Последователният вариант е тихо грешен: програмата не се чупи, флотите просто стават бързи. Едновременният взима за всяка поръчка min(кораби от поръчката, кораби в системата в началото) и ги маха (напускане), а после ги слага (пристигане). Втората поръчка (B→C) вижда в B нула кораби, защото първите още не са пристигнали, и не мести нищо. Затова веригата е 0 3 0 0: корабите са стигнали само до B.
(int[])start.Clone() прави копие: без него функцията би променила масива на викащия.По-точно, за любопитните
Същото правило важи за всичко, което се случва „наведнъж“: две империи, които си разменят системи, два флота, които се разминават. Ако движиш ги един по един, вторият вижда света, който първият вече е променил, и резултатът зависи от реда на играчите. Правилото на двете стъпки (вземи всичко, после върни всичко) е същото като при размяна на две променливи с помощна: нужна е памет за „старото“.
В канона движението е още по-близо до нашето, но с една добавка: дългият път продължава сам. Поръчката помни крайната цел (dest), а след пристигане се записва нова поръчка за следващата стъпка. Затова флот, пратен през пет системи, върви пет хода, по една система на ход.
Идеята
Случайни събития по тегла
На всеки ход с шанс 40% се случва едно събитие на случайна империя. Кое точно? Ако всички видове имат равен шанс, пиратите ще идват на първия ход, когато империята няма и един кораб. Затова всеки вид има тегло (колко често е) и най-ранен ход (от кога е позволен). Ето таблицата на канона и избора:
// id, тегло, най-ранен ходEventDef[] eventDefs =[ new EventDef("meteor", 20, 1), new EventDef("vein", 15, 1), new EventDef("pirates", 20, 6), new EventDef("breakthrough", 15, 1), new EventDef("unrest", 10, 5), new EventDef("boom", 15, 1), new EventDef("flare", 10, 1), new EventDef("derelict", 8, 3)];
int PickWeighted(Rng rng, int[] weights){ int total = 0; foreach (int weight in weights) total += weight; int roll = rng.NextInt(total); for (int i = 0; i < weights.Length; i++) { if (roll < weights[i]) return i; roll -= weights[i]; } return weights.Length - 1;}
Roll? RollEvent(Rng rng, int turn){ if (!rng.Chance(EventChance)) return null; int empire = rng.NextInt(names.Length); var weights = new int[eventDefs.Length]; for (int i = 0; i < weights.Length; i++) { weights[i] = turn >= eventDefs[i].MinTurn ? eventDefs[i].Weight : 0; } return new Roll(PickWeighted(rng, weights), empire);}Изборът по тегла е „рулетка“: хвърля се число от 0 до сбора на теглата и се върви по видовете, като се изважда теглото на всеки, докато числото стане по-малко от текущото тегло. Вид, който още не е позволен, получава тегло 0 и никога не излиза, а сборът намалява, така че другите стават по-вероятни. Програмата пуска дванайсет хода и после още хиляда, за да се види колко пъти излиза всеки вид:
var rng = new Rng(7);for (int turn = 1; turn <= 12; turn++){ Roll? roll = RollEvent(rng, turn); if (roll != null) Console.WriteLine("ход " + turn + ": " + eventDefs[roll.Kind].Id + " → " + names[roll.Empire]);}
var counts = new int[eventDefs.Length];for (int i = 0; i < 1000; i++){ Roll? roll = RollEvent(rng, 99); if (roll != null) counts[roll.Kind]++;}Console.WriteLine("Събития за 1000 хода:");for (int i = 0; i < counts.Length; i++){ Console.WriteLine(eventDefs[i].Id + " " + eventDefs[i].Weight + ": " + counts[i]);}ход 1: boom → Съюз Лираход 2: breakthrough → Академия Вегаход 3: vein → Академия Вегаход 4: boom → Академия Вегаход 8: pirates → Съюз Лираход 9: breakthrough → Земна империяСъбития за 1000 хода:meteor 20: 70vein 15: 53pirates 20: 78breakthrough 15: 58unrest 10: 30boom 15: 50flare 10: 28derelict 8: 28Пиратите (най-ранен ход 6) се виждат чак на ход 8. В хилядата хода най-често излизат пиратите (78) и метеоритите (70) с тегло 20, най-рядко изригването (28) и изоставените кораби (28) с тегла 10 и 8. Бройките не са равни на теглата, защото е случайно, но редът им е същият. Шансът за събитие е 40% (EVENT_CHANCE), а генераторът е със семе: един и същ вход дава един и същи изход на четирите езика, затова записът на играта може да се повтори.
По-точно, за любопитните
В канона всяко събитие прави нещо различно: метеоритите отнемат метал, жилата вдига богатството на една планета, пиратите се бият с флота на империята (и се печелят или губят ресурси), пробивът в науката дава точки, размириците свалят ниво на мина, бумът дава газ и кристал, изригването спира лабораториите за един ход, изоставените кораби дават две фрегати. Ето всичките (регион events от канона). Обърни внимание на края на rollEvent: резултатът не се пише на екрана, а се публикува. Какво значи това, е в картата за шината.
// Случайните събития: на всеки ход с 40% шанс се случва едно на случайна империя. Видът е по// тегла (само от най-ранния му ход нататък). Всичко се публикува в шината; новините, статистиката// и озлобването са абонати, а събитията не знаят за тях.static partial class Cosmos{ public const int EVENT_CHANCE = 40;
public static Planet? PickEmpirePlanet(World world, Empire empire) { List<Planet> list = OwnedPlanets(world, empire.Id); if (list.Count == 0) return null; return list[world.Rng.NextInt(list.Count)]; }
public static int[] PirateFleet(World world) { int strength = world.Turn / 6; return new int[] { 2 + strength, strength / 2, strength / 3, 0, 0 }; }
public static List<(string Name, object Value)>? ApplyEvent(World world, string kind, Empire empire) { Random rng = world.Rng; List<(string Name, object Value)> vars = new List<(string Name, object Value)> { ("empire", EmpireName(empire)) }; if (kind == "meteor") { Planet? planet = PickEmpirePlanet(world, empire); if (planet == null) return null; int loss = MinOf(empire.Res[0], 8 + rng.NextInt(8)); empire.Res[0] -= loss; vars.Add(("planet", PlanetName(world, planet))); vars.Add(("n", loss)); } else if (kind == "vein") { Planet? planet = PickEmpirePlanet(world, empire); if (planet == null) return null; int r = rng.NextInt(RES_COUNT); int tries = 0; while (planet.Rich[r] >= 5 && tries < RES_COUNT) { r = (r + 1) % RES_COUNT; tries++; } if (planet.Rich[r] >= 5) return null; planet.Rich[r]++; vars.Add(("planet", PlanetName(world, planet))); vars.Add(("res", T("res." + r))); } else if (kind == "pirates") { Planet? planet = PickEmpirePlanet(world, empire); if (planet == null) return null; int systemId = planet.SystemId; int[] mine = EmptyCounts(); Fleet? fleet = FleetAt(world, empire.Id, systemId); for (int k = 0; k < SHIP_KIND_COUNT; k++) mine[k] = fleet == null ? 0 : fleet.Ships[k]; mine[FORT_KIND] = FortsIn(world, empire.Id, systemId); BattleResult result = RunBattle(SideStatsOf(empire), mine, SideStatsOf(null), PirateFleet(world)); for (int k = 0; k < SHIP_KIND_COUNT; k++) { if (fleet != null) fleet.Ships[k] = result.A[k]; } string systemName = world.Systems[systemId].Name; vars.Add(("system", systemName)); vars.Add(("fleet", CountsText(PirateFleet(world)))); if (result.Winner == "a") { empire.Res[0] += 20; empire.Res[1] += 10; vars.Add(("outcome", T("event.pirates.win"))); } else { int m = empire.Res[0] / 5; int g = empire.Res[1] / 5; empire.Res[0] -= m; empire.Res[1] -= g; vars.Add(("outcome", T("event.pirates.lose", ("m", m), ("g", g)))); } if (!world.Quiet) world.LastBattle = BattleLines(systemName, EmpireName(empire), T("pirates"), result); world.Stats.Battles++; world.Stats.Lost += TotalOf(mine) - TotalOf(result.A) + TotalOf(PirateFleet(world)) - TotalOf(result.B); } else if (kind == "breakthrough") { int n = 6 + rng.NextInt(7); empire.Progress += n; vars.Add(("n", n)); } else if (kind == "unrest") { Planet? planet = PickEmpirePlanet(world, empire); if (planet == null || planet.Mines == 0) return null; planet.Mines--; vars.Add(("planet", PlanetName(world, planet))); } else if (kind == "boom") { empire.Res[1] += 20; empire.Res[2] += 10; } else if (kind == "flare") { empire.Flare = 1; } else if (kind == "derelict") { Planet? planet = PickEmpirePlanet(world, empire); if (planet == null) return null; AddShips(world, empire.Id, planet.SystemId, new int[] { 0, 0, 2, 0 }); vars.Add(("system", world.Systems[planet.SystemId].Name)); } return vars; }
public static void RollEvent(World world) { Random rng = world.Rng; if (!rng.Chance(EVENT_CHANCE)) return; List<Empire> living = new List<Empire>(); foreach (Empire e in world.Empires) { if (e.Alive) living.Add(e); } if (living.Count == 0) return; Empire empire = living[rng.NextInt(living.Count)]; List<EventDef> allowed = new List<EventDef>(); List<int> weights = new List<int>(); foreach (EventDef eventDef in EVENT_DEFS) { if (world.Turn >= eventDef.MinTurn) { allowed.Add(eventDef); weights.Add(eventDef.Weight); } } EventDef def = allowed[PickWeighted(rng, weights.ToArray())]; List<(string Name, object Value)>? vars = ApplyEvent(world, def.Id, empire); if (vars == null) return; world.Stats.Events++; world.Bus.Publish(new GameEvent { Type = "event", Kind = def.Id, Empire = empire.Id, Vars = vars }); }
// Абонатите: новините за играча, озлобването след битки и завладяване. public static void AddNews(World world, string text) { world.News.Add(world.Turn + ": " + text); while (world.News.Count > 60) world.News.RemoveAt(0); world.TurnLines.Add(text); }
public static void InstallSubscribers(World world, bool quiet) { EventBus bus = world.Bus; Func<int, int, bool> involvesHuman = (a, b) => world.Empires[a].Human || world.Empires[b].Human; bus.Subscribe("battle", e => { if (e.LostA > 0) world.Empires[e.B].Grudge[e.A]++; if (e.LostB > 0) world.Empires[e.A].Grudge[e.B]++; }); bus.Subscribe("conquest", e => { if (e.From >= 0) world.Empires[e.From].Grudge[e.Empire] += 3; }); // Сигнал към играча: нещо се случи, при което „wait“ спира. bus.Subscribe("battle", e => { if (involvesHuman(e.A, e.B)) world.Alert++; }); bus.Subscribe("conquest", e => { if (world.Empires[e.Empire].Human || (e.From >= 0 && world.Empires[e.From].Human)) world.Alert++; }); bus.Subscribe("war", e => { if (involvesHuman(e.A, e.B)) world.Alert++; }); bus.Subscribe("siege", e => { if (world.Empires[e.From].Human) world.Alert++; }); bus.Subscribe("offer", e => { if (world.Empires[e.To].Human) world.Alert++; }); if (quiet) return; bus.Subscribe("battle", e => { if (involvesHuman(e.A, e.B)) { foreach (string line in e.Lines) AddNews(world, line); } else { AddNews(world, T("news.battle", ("system", world.Systems[e.SystemId].Name), ("a", EmpireName(world.Empires[e.A])), ("b", EmpireName(world.Empires[e.B])), ("la", e.LostA), ("lb", e.LostB))); } }); bus.Subscribe("colony", e => { AddNews(world, T("news.colony", ("empire", EmpireName(world.Empires[e.Empire])), ("planet", PlanetName(world, world.Planets[e.PlanetId])))); }); bus.Subscribe("tech", e => { if (world.Empires[e.Empire].Human) AddNews(world, T("news.tech", ("tech", Nm(TECHS.Get(e.TechId))))); }); bus.Subscribe("war", e => { AddNews(world, T("news.war", ("a", EmpireName(world.Empires[e.A])), ("b", EmpireName(world.Empires[e.B])))); }); bus.Subscribe("peace", e => { AddNews(world, T("news.peace", ("a", EmpireName(world.Empires[e.A])), ("b", EmpireName(world.Empires[e.B])))); }); bus.Subscribe("offer", e => { AddNews(world, T("news.offer", ("empire", EmpireName(world.Empires[e.From])), ("letter", world.Empires[e.From].Def.Letter))); }); bus.Subscribe("siege", e => { AddNews(world, T("news.siege", ("empire", EmpireName(world.Empires[e.Empire])), ("planet", PlanetName(world, world.Planets[e.PlanetId])))); }); bus.Subscribe("conquest", e => { AddNews(world, T("news.conquest", ("empire", EmpireName(world.Empires[e.Empire])), ("planet", PlanetName(world, world.Planets[e.PlanetId])))); }); bus.Subscribe("eliminated", e => { AddNews(world, T("news.eliminated", ("empire", EmpireName(world.Empires[e.Empire])))); }); bus.Subscribe("event", e => { AddNews(world, T("event." + e.Kind, e.Vars.ToArray())); }); }}Идеята
Мир и война: правило срещу бъркотията
Дипломацията в играта е два обекта: двойка империи е във война или в мир (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{ public int Turn = 1; public int[,] Ships = new int[3, 4]; public int[,] War = new int[3, 3]; public int[,] Since = new int[3, 3]; public int[,] Grudge = new int[3, 3];}
bool AtWar(World world, int a, int b){ return world.War[a, b] == 1;}
int Power(World world, int empire){ int total = 0; for (int s = 0; s < 4; s++) total += world.Ships[empire, s]; return total;}
void SetStance(World world, int a, int b, int war){ world.War[a, b] = war; world.War[b, a] = war; world.Since[a, b] = world.Turn; world.Since[b, a] = world.Turn;}
string? DeclareWar(World world, int a, int b){ if (AtWar(world, a, b)) return "alreadyWar"; // Мирът важи най-малко MIN_PEACE хода. if (world.Turn - world.Since[a, b] < MinPeace && world.Turn > MinPeace) return "tooSoon"; SetStance(world, a, b, 1); return null;}
void MakePeace(World world, int a, int b){ SetStance(world, a, b, 0); world.Grudge[a, b] = 0; world.Grudge[b, a] = 0;}
int PeaceWish(World world, int me, int other){ // Колко по-слаба е империята от другата. int ratio = Math.Min(150, Power(world, me) * 100 / (Power(world, other) + 1)); int otherWars = 0; for (int x = 0; x < 3; x++) { if (x != me && x != other && AtWar(world, me, x)) otherWars++; } return Math.Max(0, peace[me] + (150 - ratio) / 2 + otherWars * 15 - world.Grudge[me, other] * 3);}
string? ProposePeace(World world, int a, int b){ if (!AtWar(world, a, b)) return "noWar"; if (world.Turn - world.Since[a, b] < 2) return "tooSoon"; // От 90 нагоре империята приема мир. if (PeaceWish(world, b, a) >= 90) { MakePeace(world, a, b); return null; } // Отказът озлобява: следващият мир е по-труден. world.Grudge[b, a]++; return "refused";}void Step(World world, int turn, string kind, int a, int b){ world.Turn = turn; int wish = kind == "peace" ? PeaceWish(world, b, a) : 0; string? error = kind == "war" ? DeclareWar(world, a, b) : ProposePeace(world, a, b); string shown = kind == "peace" && (error == null || error == "refused") ? " (желание " + wish + ")" : ""; Console.WriteLine("ход " + turn + ": " + kind + " " + names[a] + " → " + names[b] + ": " + (error ?? "ok") + shown);}
var world = new 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).
bool AtWar(World world, int a, int b)
{
return world.War[a, b] == 1;
}
int Power(World world, int empire)
{
int total = 0;
for (int s = 0; s < 4; s++) total += world.Ships[empire, s];
return total;
}
void SetStance(World world, int a, int b, int war)
{
world.War[a, b] = war;
world.War[b, a] = war;
world.Since[a, b] = world.Turn;
world.Since[b, a] = world.Turn;
}
string? DeclareWar(World world, int a, int b)
{
if (AtWar(world, a, b)) return "alreadyWar";
// Мирът важи най-малко MIN_PEACE хода.
if (world.Turn - world.Since[a, b] < MinPeace && world.Turn > MinPeace) return "tooSoon";
SetStance(world, a, b, 1);
return null;
}
void MakePeace(World world, int a, int b)
{
SetStance(world, a, b, 0);
world.Grudge[a, b] = 0;
world.Grudge[b, a] = 0;
}
int PeaceWish(World world, int me, int other)
{
// Колко по-слаба е империята от другата.
int ratio = Math.Min(150, Power(world, me) * 100 / (Power(world, other) + 1));
int otherWars = 0;
for (int x = 0; x < 3; x++)
{
if (x != me && x != other && AtWar(world, me, x)) otherWars++;
}
return Math.Max(0, peace[me] + (150 - ratio) / 2 + otherWars * 15 - world.Grudge[me, other] * 3);
}
string? ProposePeace(World world, int a, int b)
{
if (!AtWar(world, a, b)) return "noWar";
if (world.Turn - world.Since[a, b] < 2) return "tooSoon";
// От 90 нагоре империята приема мир.
if (PeaceWish(world, b, a) 90)
{
MakePeace(world, a, b);
return null;
}
// Отказът озлобява: следващият мир е по-труден.
world.Grudge[b, a]++;
return "refused";
}Прагът е „от 90 нагоре“, т.е. 90 е достатъчно: >=. С > желание точно 90 би било отказ, а правилото в играта казва обратното.
По-точно, за любопитните
Резултатът от функциите е низ или нищо: null, ако промяната е станала, и ключ на грешка (alreadyWar, tooSoon, refused, noWar), ако не. Грешките са ключове, а не готови изречения, за да ги превежда екранът (играта е на четири езика). Така същата функция служи и на играча (команда peace B), и на изкуствения интелект.
Канонът има и предложения към играча: изкуственият интелект не може да сключи мир с човека сам, затова записва предложение (offers), което важи три хода, и публикува събитие „offer“. Ето целия регион diplomacy:
// Дипломацията е мир или война между две империи. Мирът важи най-малко MIN_PEACE хода преди// нова война. Изкуственият интелект приема мир по мерило: колко е по-слаб, какъв е характерът// му и колко е озлобен. Предложение към играча се записва и важи три хода.static partial class Cosmos{ public static bool AtWar(World world, int a, int b) { return world.Empires[a].War[b] == 1; }
public static void SetStance(World world, int a, int b, int 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; }
public static string? DeclareWar(World world, int a, int b) { if (a == b) return "dip.self"; if (!world.Empires[b].Alive) return "dip.dead"; if (AtWar(world, a, b)) return "dip.alreadyWar"; if (world.Turn - world.Empires[a].Since[b] < MIN_PEACE && world.Turn > MIN_PEACE) return "dip.tooSoon"; SetStance(world, a, b, 1); world.Bus.Publish(new GameEvent { Type = "war", A = a, B = b }); return null; }
public static void MakePeace(World world, int a, int b) { SetStance(world, a, b, 0); world.Empires[a].Grudge[b] = 0; world.Empires[b].Grudge[a] = 0; world.Bus.Publish(new GameEvent { Type = "peace", A = a, B = b }); }
// Най-малкото разстояние в пътища между системи с планети на двете империи. public static int EmpireDistance(World world, int a, int b) { int best = 99; foreach (Planet pa in world.Planets) { if (pa.Owner != a) continue; int[] dist = Bfs(world.Systems, pa.SystemId).Dist; foreach (Planet pb in world.Planets) { if (pb.Owner == b && dist[pb.SystemId] >= 0 && dist[pb.SystemId] < best) best = dist[pb.SystemId]; } } return best; }
public static int PowerRatio(World world, int a, int b, int top) { return Clamp(Percent((long)EmpirePower(world, a) * 100, (long)EmpirePower(world, b) * 100 + 100), 0, top); }
// Колко иска империята мир с другата (0 и нагоре; от 90 нагоре приема). public static int PeaceWish(World world, int me, int other) { Empire e = world.Empires[me]; int weaker = 150 - PowerRatio(world, me, other, 150); int otherWars = 0; foreach (Empire x in world.Empires) { if (x.Alive && x.Id != me && x.Id != other && e.War[x.Id] == 1) otherWars++; } int wish = e.Personality.Peace + weaker / 2 + otherWars * 15 - e.Grudge[other] * 3; return MaxOf(0, wish); }
// Колко иска империята война с другата. public static int WarWish(World world, int me, int other) { Empire e = world.Empires[me]; int ratio = PowerRatio(world, me, other, 300); int wish = e.Personality.Attack / 2; if (ratio > 100) wish += (ratio - 100) / 4; else wish -= (100 - ratio) / 2; if (EmpireDistance(world, me, other) <= 3) wish += 25; foreach (Empire x in world.Empires) { if (x.Alive && x.Id != me && e.War[x.Id] == 1) wish -= 30; } return wish; }
public static bool HasOffer(World world, int from, int to) { foreach (Offer offer in world.Offers) { if (offer.From == from && offer.To == to && world.Turn - offer.Turn <= 3) return true; } return false; }
// Предложение за мир от a към b. Връща ключ на грешка или null (мирът е сключен). public static string? ProposePeace(World world, int a, int b) { if (a == b) return "dip.self"; if (!world.Empires[b].Alive) return "dip.dead"; if (!AtWar(world, a, b)) return "dip.noWar"; if (world.Turn - world.Empires[a].Since[b] < 2) return "dip.tooSoon"; if (world.Empires[b].Human) { if (HasOffer(world, b, a)) { MakePeace(world, a, b); return null; } if (!HasOffer(world, a, b)) world.Offers.Add(new Offer { From = a, To = b, Turn = world.Turn }); return "dip.sent"; } if (HasOffer(world, b, a) || PeaceWish(world, b, a) >= 90) { MakePeace(world, a, b); return null; } world.Empires[b].Grudge[a]++; return "dip.refused"; }
// Дипломацията на изкуствения интелект в края на хода: мир, когато е по-слаб, война, когато е по-силен. public static void AiDiplomacy(World world, Empire empire) { foreach (Empire other in world.Empires) { if (!other.Alive || other.Id == empire.Id) continue; if (AtWar(world, empire.Id, other.Id)) { if (world.Turn - empire.Since[other.Id] >= 3 && PeaceWish(world, empire.Id, other.Id) >= 90) { if (other.Human) { if (!HasOffer(world, empire.Id, other.Id)) { world.Offers.Add(new Offer { From = empire.Id, To = other.Id, Turn = world.Turn }); world.Bus.Publish(new GameEvent { Type = "offer", From = empire.Id, To = other.Id }); } } else { ProposePeace(world, empire.Id, other.Id); } } } else if (world.Turn - empire.Since[other.Id] >= MIN_PEACE + 2 && world.Turn > MIN_PEACE + 2) { if (WarWish(world, empire.Id, other.Id) >= 95 && world.Rng.Chance(40)) DeclareWar(world, empire.Id, other.Id); } } }}Идеята
Шина от събития: който публикува, не знае кой слуша
Когато се обяви война, трябва да стане много: да се напише новина, да порасне озлобването на двете империи, да светне сигнал на играча, да се преброи в статистиката. Най-простото е declareWar да вика всичко това. Тогава declareWar знае за новините, за озлобването, за сигнала и за статистиката, а всяка нова възможност е още един ред в нея и още една причина да се счупи. Шината обръща посоката: declareWar казва „обявена е война между A и B“ и свършва. Който се интересува, се е абонирал за „war“.
record Event(string Type, int A, int B, int Place, int N){ public Event(string type, int a, int b) : this(type, a, b, 0, 0) { }}
class EventBus{ public Dictionary<string, List<Action<Event>>> Handlers = new Dictionary<string, List<Action<Event>>>();
public void Subscribe(string type, Action<Event> handler) { if (!Handlers.ContainsKey(type)) Handlers[type] = new List<Action<Event>>(); Handlers[type].Add(handler); }
public void Publish(Event gameEvent) { if (!Handlers.ContainsKey(gameEvent.Type)) return; foreach (Action<Event> handler in new List<Action<Event>>(Handlers[gameEvent.Type])) handler(gameEvent); }}subscribe записва обработчик за вид събитие, publish вика всички обработчици на този вид. Вид без абонати не е грешка: събитието отива в празното. Ето публикуването в двете места, където става (обява на война и сключване на мир). Единственото ново е редът с publish:
string? DeclareWar(World world, int a, int b){ if (AtWar(world, a, b)) return "alreadyWar"; // Мирът важи най-малко MIN_PEACE хода. if (world.Turn - world.Since[a, b] < MinPeace && world.Turn > MinPeace) return "tooSoon"; SetStance(world, a, b, 1); world.Bus.Publish(new Event("war", a, b)); return null;}
void MakePeace(World world, int a, int b){ SetStance(world, a, b, 0); world.Grudge[a, b] = 0; world.Grudge[b, a] = 0; world.Bus.Publish(new Event("peace", a, b));}А ето програмата. Новините слушат всички видове, озлоблението само битките, а report публикува и неуспешните опити (refused, tooSoon) като събития:
var world = new World();world.Ships[1, 1] = 1;world.Grudge[1, 0] = 1;// Новините слушат всички видове събития, озлобването само битките.foreach (string type in newsTypes){ world.Bus.Subscribe(type, e => Console.WriteLine(NewsText(e)));}world.Bus.Subscribe("battle", e =>{ world.Grudge[e.A, e.B]++; world.Grudge[e.B, e.A]++;});
world.Turn = 1;Report(world, DeclareWar(world, 1, 0), 1, 0);world.Bus.Publish(new Event("battle", 1, 0, 1, 2));world.Turn = 3;Report(world, ProposePeace(world, 0, 1), 0, 1);Report(world, DeclareWar(world, 1, 0), 1, 0);Console.WriteLine("озлобление " + names[0] + " → " + names[1] + ": " + world.Grudge[0, 1]);Console.WriteLine("озлобление " + names[1] + " → " + names[0] + ": " + world.Grudge[1, 0]);Хегемония Орион обявява война на Земна империя.Битка при Vesta: Хегемония Орион и Земна империя губят кораби (по 2).Хегемония Орион отказва мир с Земна империя.Хегемония Орион вече е във война с Земна империя.озлобление Земна империя → Хегемония Орион: 1озлобление Хегемония Орион → Земна империя: 3Опитът на ход 1 е успешен, битката е публикувана на ръка, отказът на ход 3 е събитие (refused), а повторната война е alreadyWar. Озлоблението на A към B е 1 (от битката), а на B към A е 3: едно старо, едно от битката и едно от отказа (отказът вдига озлоблението на този, който отказва, към предложилия).
Action<Event> е делегат с един параметър: обработчикът е ламбда, e => ....По-точно, за любопитните
Без шина всяко ново нещо, което трябва да научи за война, иска промяна в declareWar. С шината нов абонат е само нов ред при създаването на света, а функцията за война не се пипа. Това е шаблонът наблюдател (observer) от модула за шаблони за дизайн.
Малка, но важна подробност: publish върти по копие на списъка с абонати. Обработчик може да се абонира за друго събитие, докато го викат; ако списъкът се променя по време на обхождането, абонатите се прескачат или се повтарят. Копието прави правилото ясно: новият абонат чува от следващото публикуване.
Редът на абонатите е редът на subscribe. Тук няма значение (новините и озлоблението не си пречат), но ако два абоната зависят един от друг, редът ще решава изхода, и това е причина да ги държиш независими.
Идеята
Целият ход
Сега всичко заедно. Светът е World: ход, фаза, генератор, шина, флотите на трите империи (по системи), метал и таблиците за война (war), от кой ход (since) и озлобление (grudge):
class World{ public int Turn = 1; public string Phase = ""; public Rng Rng; public EventBus Bus = new EventBus(); public int[,] Ships = new int[,] { { 3, 0, 0, 0 }, { 0, 0, 0, 4 }, { 0, 2, 0, 0 } }; public int[] Metal = [50, 50, 50]; public int[,] War = new int[3, 3]; public int[,] Since = new int[3, 3]; public int[,] Grudge = new int[3, 3];
public World(uint seed) { Rng = new Rng(seed); }}Поръчките и дипломацията на хода са сценарий (в играта ги решава изкуственият интелект или играчът): поръчката е [ход, империя, от, към, кораби], а дипломацията [ход, вид, от, към].
// [ход, империя, от, към, кораби]int[][] orders = [[1, 1, 3, 2, 4], [1, 0, 0, 1, 3], [2, 1, 2, 1, 4]];// [ход, вид (0 война, 1 мир), от, към]int[][] diplo = [[1, 0, 1, 0], [3, 0, 2, 1], [3, 1, 0, 1], [4, 1, 0, 1], [5, 0, 1, 0]];Фазите са функции. Дипломацията изпълнява сценария за хода и публикува грешките. Движението е точно двете стъпки от първата карта. Битките обхождат системите: където две империи във война имат кораби, всяка губи толкова, колкото е по-малката флота, и се публикува „battle“ (озлоблението се абонира за него):
void ScriptedDiplomacy(World world){ world.Phase = "дипломация"; foreach (int[] step in diplo) { if (step[0] != world.Turn) continue; string? error = step[1] == 0 ? DeclareWar(world, step[2], step[3]) : ProposePeace(world, step[2], step[3]); Report(world, error, step[2], step[3]); }}void ResolveMovement(World world){ world.Phase = "движение"; var moving = new List<Move>(); // Първо всички флоти напускат системите си... foreach (int[] order in orders) { if (order[0] != world.Turn) continue; int take = Math.Min(order[4], world.Ships[order[1], order[2]]); world.Ships[order[1], order[2]] -= take; if (take > 0) moving.Add(new Move(order[1], order[2], order[3], take)); } // ...и чак тогава пристигат. foreach (Move move in moving) { world.Ships[move.Empire, move.To] += move.N; world.Bus.Publish(new Event("move", move.Empire, move.From, move.To, move.N)); }}void ResolveBattles(World world){ world.Phase = "битки"; for (int system = 0; system < 4; system++) { for (int a = 0; a < 3; a++) { for (int b = a + 1; b < 3; b++) { if (!AtWar(world, a, b) || world.Ships[a, system] == 0 || world.Ships[b, system] == 0) continue; int lost = Math.Min(world.Ships[a, system], world.Ships[b, system]); world.Ships[a, system] -= lost; world.Ships[b, system] -= lost; world.Bus.Publish(new Event("battle", a, b, system, lost)); } } }}
void Produce(World world){ world.Phase = "производство"; for (int empire = 0; empire < 3; empire++) world.Metal[empire] += 10;}След тях е събитието: теглене по теглата от втората карта, с тези ефекти: метеор отнема метал, бум дава метал, изоставените кораби дават два кораба в столицата. Накрая runTurn ги вика в ред:
void RollEvent(World world){ world.Phase = "събитие"; if (!world.Rng.Chance(EventChance)) return; int empire = world.Rng.NextInt(3); var weights = new int[eventDefs.Length]; for (int i = 0; i < weights.Length; i++) { weights[i] = world.Turn >= eventDefs[i].MinTurn ? eventDefs[i].Weight : 0; } string kind = eventDefs[PickWeighted(world.Rng, weights)].Id; if (kind == "meteor") { int loss = Math.Min(world.Metal[empire], 8 + world.Rng.NextInt(8)); world.Metal[empire] -= loss; world.Bus.Publish(new Event("meteor", empire, 0, 0, loss)); } else if (kind == "boom") { world.Metal[empire] += 20; world.Bus.Publish(new Event("boom", empire, 0, 0, 20)); } else { world.Ships[empire, home[empire]] += 2; world.Bus.Publish(new Event("derelict", empire, 0, home[empire], 2)); }}void RunTurn(World world){ // Фазите вървят в този ред и редът е важен. ScriptedDiplomacy(world); ResolveMovement(world); ResolveBattles(world); Produce(world); RollEvent(world); world.Turn++;}Програмата абонира новините (те слагат пред всеки ред фазата, в която е станало събитието) и озлоблението, пуска пет хода и печата резултата. Редовете са точно новините на играта:
var world = new World(7);world.Bus.Subscribe("battle", e =>{ world.Grudge[e.A, e.B]++; world.Grudge[e.B, e.A]++;});foreach (string type in newsTypes){ world.Bus.Subscribe(type, e => Console.WriteLine(" [" + world.Phase + "] " + NewsText(e)));}
for (int i = 0; i < 5; i++){ Console.WriteLine(Fill("-- ход {0} --", world.Turn)); RunTurn(world);}Console.WriteLine("-- край --");for (int empire = 0; empire < 3; empire++){ Console.WriteLine(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: нова война е „прекалено скоро“, а Орион намира два изоставени кораба в столицата си.
Виж го
Един ход на галактиката
Ето същото по ходове. Горе светва фазата, в която е станало нещо. По средата са системите и флотите на трите империи: при движението всички флотове излитат наведнъж и чак тогава кацат. Под тях се вижда дали всяка двойка е във война, а най-долу шината: публикуваното събитие светва абоната „новини“, а ако е битка, и „озлобление“.
void RunTurn(World world){ // Фазите вървят в този ред и редът е важен. ScriptedDiplomacy(world); ResolveMovement(world); ResolveBattles(world); Produce(world); RollEvent(world); world.Turn++;}
class EventBus{ public Dictionary<string, List<Action<Event>>> Handlers = new Dictionary<string, List<Action<Event>>>();
public void Subscribe(string type, Action<Event> handler) { if (!Handlers.ContainsKey(type)) Handlers[type] = new List<Action<Event>>(); Handlers[type].Add(handler); }
public void Publish(Event gameEvent) { if (!Handlers.ContainsKey(gameEvent.Type)) return; foreach (Action<Event> handler in new List<Action<Event>>(Handlers[gameEvent.Type])) handler(gameEvent); }}Стъпка по стъпка, с думи
- A = Земна империя, B = Хегемония Орион, C = Съюз Лира. Четири системи по права линия; ходът минава през фазите отгоре надолу.
- ход 1 · дипломация: Хегемония Орион обявява война на Земна империя.
- ход 1 · движение: Хегемония Орион праща флот от Brisa към Altan (кораби: 4). Земна империя праща флот от Kora към Vesta (кораби: 3).
- ход 1 · събитие: Метеоритен дъжд: Съюз Лира губи 14 метал.
- ход 2 · движение: Хегемония Орион праща флот от Altan към Vesta (кораби: 4).
- ход 2 · битки: Битка при Vesta: Земна империя и Хегемония Орион губят кораби (по 3).
- ход 3 · дипломация: Съюз Лира обявява война на Хегемония Орион. Хегемония Орион отказва мир с Земна империя.
- ход 3 · битки: Битка при Vesta: Хегемония Орион и Съюз Лира губят кораби (по 1).
- ход 4 · дипломация: Мир между Земна империя и Хегемония Орион.
- ход 4 · събитие: Метеоритен дъжд: Съюз Лира губи 8 метал.
- ход 5 · дипломация: Хегемония Орион още не може да смени отношенията си с Земна империя: прекалено скоро.
- ход 5 · събитие: Хегемония Орион намира изоставени кораби при Brisa (кораби: 2).
- След 5 хода: Земна империя: кораби 0, метал 100 · Хегемония Орион: кораби 2, метал 100 · Съюз Лира: кораби 1, метал 78
Кодът до сцената е runTurn и EventBus от програмата. Светят редовете, които работят в момента.
Идеята
В играта
Това, което построихме, е същото като в канона, само по-малко. Ето истинския ход (регион turn). Над runTurn са движението, което прави същите две стъпки (напускане, пристигане), битките и завладяването на планети; runTurn вика фазите в твърд ред, а в края на хода озлоблението остарява (на всеки пети ход намалява с 1) и се изхвърлят остарелите предложения за мир.
// Един ход на галактиката има фази: дипломация на изкуствения интелект, поръчките му, движение на// всички флотове наведнъж, битки, завладяване, производство и наука, събитие, край на хода.// Движението е едновременно: първо всички кораби напускат системите си, чак тогава пристигат.class Move{ public int Owner; public int To; public int Dest; public int[] Ships = new int[4];}
static partial class Cosmos{ public static void ResolveMovement(World world) { List<Move> moving = new List<Move>(); foreach (Empire empire in world.Empires) { if (!empire.Alive) continue; foreach (Order order in empire.Orders) { Fleet? fleet = FleetAt(world, empire.Id, order.From); if (fleet == null) continue; int[] ships = { 0, 0, 0, 0 }; for (int k = 0; k < SHIP_KIND_COUNT; k++) { ships[k] = MinOf(order.Ships[k], fleet.Ships[k]); fleet.Ships[k] -= ships[k]; } if (TotalOf(ships) > 0) moving.Add(new Move { Owner = empire.Id, To = order.To, Dest = order.Dest, Ships = ships }); } empire.Orders = new List<Order>(); } foreach (Move move in moving) { AddShips(world, move.Owner, move.To, move.Ships); // дългият път продължава сам: нова поръчка от мястото, където стигнаха if (move.To != move.Dest) { world.Empires[move.Owner].Orders.Add(new Order { From = move.To, To = NextStep(world.Systems, move.To, move.Dest), Dest = move.Dest, Ships = (int[])move.Ships.Clone() }); } } RemoveEmptyFleets(world); }
public static bool HasUnits(World world, int owner, int systemId) { Fleet? fleet = FleetAt(world, owner, systemId); return (fleet != null && TotalOf(fleet.Ships) > 0) || FortsNow(world, owner, systemId) > 0; }
public static void ResolveBattles(World world) { foreach (StarSystem system in world.Systems) { for (int a = 0; a < world.Empires.Count; a++) { for (int b = a + 1; b < world.Empires.Count; b++) { if (!world.Empires[a].Alive || !world.Empires[b].Alive || !AtWar(world, a, b)) continue; if (HasUnits(world, a, system.Id) && HasUnits(world, b, system.Id)) FightAt(world, system.Id, a, b); } } } RemoveEmptyFleets(world); }
// Планета пада, когато враг е във война с владетеля ѝ, има кораби в системата, а владетелят няма // нито кораби, нито оцелели крепости там, и така SIEGE_TURNS хода подред: първите ходове са обсада, // последният е превземане. Защитникът има време да доведе флот. Постройките се разрушават. public static void ResolveConquest(World world) { foreach (StarSystem system in world.Systems) { foreach (int pid in system.Planets) { Planet planet = world.Planets[pid]; if (planet.Owner < 0) continue; int owner = planet.Owner; int attacker = -1; if (!HasUnits(world, owner, system.Id)) { foreach (Empire e in world.Empires) { if (!e.Alive || e.Id == owner || !AtWar(world, e.Id, owner)) continue; Fleet? fleet = FleetAt(world, e.Id, system.Id); if (fleet != null && TotalOf(fleet.Ships) > 0) { attacker = e.Id; break; } } } if (attacker < 0) { planet.Siege = 0; continue; } planet.Siege++; if (planet.Siege < SIEGE_TURNS) { world.Bus.Publish(new GameEvent { Type = "siege", Empire = attacker, From = owner, PlanetId = planet.Id }); continue; } planet.Siege = 0; planet.Owner = attacker; planet.Mines = MaxOf(0, planet.Mines - 1); planet.Lab = 0; planet.Yard = 0; planet.Fort = 0; world.Bus.Publish(new GameEvent { Type = "conquest", Empire = attacker, From = owner, PlanetId = planet.Id }); } } foreach (Empire empire in world.Empires) { if (!empire.Alive || OwnedPlanets(world, empire.Id).Count > 0) continue; empire.Alive = false; List<Fleet> kept = new List<Fleet>(); foreach (Fleet fleet in world.Fleets) { if (fleet.Owner != empire.Id) kept.Add(fleet); } world.Fleets = kept; empire.Orders = new List<Order>(); world.Bus.Publish(new GameEvent { Type = "eliminated", Empire = empire.Id }); } }
public static void RunTurn(World world) { world.TurnLines = new List<string>(); world.FortLeft = new Dictionary<int, int>(); foreach (Empire empire in world.Empires) { if (empire.Alive && !empire.Human) AiDiplomacy(world, empire); } foreach (Empire empire in world.Empires) { if (empire.Alive && !empire.Human) AiPlay(world, empire); } ResolveMovement(world); ResolveBattles(world); ResolveConquest(world); foreach (Empire empire in world.Empires) { if (!empire.Alive) continue; Produce(world, empire); CompleteResearch(world, empire); } FinishQueue(world); RollEvent(world); CheckVictory(world); world.Turn++; if (world.Turn % 5 == 0) { foreach (Empire empire in world.Empires) { for (int i = 0; i < empire.Grudge.Length; i++) empire.Grudge[i] = MaxOf(0, empire.Grudge[i] - 1); } } List<Offer> fresh = new List<Offer>(); foreach (Offer offer in world.Offers) { if (world.Turn - offer.Turn <= 3) fresh.Add(offer); } world.Offers = fresh; }}А ето шината, която слушат новините, озлоблението и сигналът към играча. Събитията са един клас с повече полета (в JavaScript — обект), а шината е същата:
// Шината от събития: който публикува, не знае кой слуша. Новините, злопаметността на империите и// статистиката се абонират сами, а битката само казва „флот се би при тази система“.class GameEvent{ public string Type = ""; public int SystemId; public int A; public int B; public int LostA; public int LostB; public string Winner = ""; public List<string> Lines = new List<string>(); public int Empire; public int From; public int To; public int PlanetId; public string TechId = ""; public string Kind = ""; public List<(string Name, object Value)> Vars = new List<(string Name, object Value)>();}
class EventBus{ public Dictionary<string, List<Action<GameEvent>>> Handlers = new Dictionary<string, List<Action<GameEvent>>>();
public void Subscribe(string type, Action<GameEvent> handler) { if (!Handlers.ContainsKey(type)) Handlers[type] = new List<Action<GameEvent>>(); Handlers[type].Add(handler); }
public void Publish(GameEvent gameEvent) { if (!Handlers.ContainsKey(gameEvent.Type)) return; foreach (Action<GameEvent> handler in new List<Action<GameEvent>>(Handlers[gameEvent.Type])) handler(gameEvent); }}Разликите от нашата програма са много, и затова са важни:
- Няма сценарий. Поръчките и дипломацията идват от
aiPlayиaiDiplomacy(урокът за съперниците) и от командите на играча. - Галактиката е граф (урокът за генерирането ѝ), а не права линия. Поръчката помни крайната цел и продължава сама.
- Битката (
fightAt, урокът за флотите) е с роли на корабите и по шест рунда, а не „всеки губи по по-малкото“. Има и крепости, и завладяване на планета (две обсади). - Желанието за мир ползва силата на империята (не броя кораби), характера ѝ и нейното желание за война (
warWish). Предложение към играча се записва за три хода. - Производството тук е плоски 10 метал на империя; в канона зависи от планетите, мините и технологиите.
- Събитията са осем вида, с по-различен ефект (пиратите се бият със същия модел на битка).
- Шината носи и други видове: завладяване, обсада, откриване на технология, колония, унищожена империя. Новините и сигналът към играча ги слушат, а
waitспира, щом сигналът порасне.
Пробвай
Предвиди изхода
Два флота: първата поръчка праща 3-те кораба от системата 0 към 1, втората — 2 кораба от системата 1 към 2. Поръчките се обработват едновременно. Какво печата програмата?
int[] MoveTogether(int[] start, int[][] orders){ var ships = (int[])start.Clone(); var takes = new int[orders.Length]; // Първо всички флоти напускат системите си... for (int i = 0; i < orders.Length; i++) { takes[i] = Math.Min(orders[i][2], ships[orders[i][0]]); ships[orders[i][0]] -= takes[i]; } // ...и чак тогава пристигат. for (int i = 0; i < orders.Length; i++) ships[orders[i][1]] += takes[i]; return ships;}
string Show(int[] ships){ return string.Join(" ", ships);}
int[][] orders = [[0, 1, 3], [1, 2, 2]];Console.WriteLine(Show(MoveTogether([3, 0, 0, 0], orders)));Първо всички тръгват: първата поръчка взима 3 кораба от системата 0, а втората вижда в системата 1 нула кораби (първите още не са пристигнали) и взима 0. После пристигат: 3 кораба в системата 1, 0 в системата 2. Втората възможност е резултатът при поръчки една след друга: 2 от пристигналите кораби тръгват пак същия ход; третата е началното състояние, без движение.
Пробвай
Намери грешката
Колега „оправи“ едновременното движение, но при него корабите пак минават по няколко системи на ход:
int[] start = [3, 0, 0, 0];Console.WriteLine("едновременно: " + Show(MoveTogether(start, orders)));едновременно: 0 0 0 3Резултатът е като при последователното движение. Кой ред праща корабите на местоназначението твърде рано?
Избери реда с грешката.
Корабите пристигат (+= в местоназначението) още в цикъла за напускане. Когато дойде втората поръчка, корабите на първата вече са там и тя ги взима. Правилно: в първия цикъл само се вземат (-=) и се помнят в takes; пристигането е втори цикъл, след като всички са тръгнали.
По-точно
По-точно: ред, справедливост и случайност
Редът на играчите. При едновременно движение редът на поръчките не променя резултата, защото първо всички вземат от началото на хода. При битките обаче има ред: система по система, двойка по двойка (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, списъка с видовете за новините? А шината?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Списъци · Dictionary и HashSet · Ламбди и делегати · record, switch и шаблони
- Задачи по темата: Epic Cosmos: империята