Zum Inhalt
wannacode

Die Rivalen

Kurz gesagt: Ein Rivale gibt jeder Aktion, die er bezahlen kann, eine Zahl, nimmt die höchste, bezahlt und bewertet neu. Der Charakter des Reichs sind die Gewichte in denselben Formeln.

Problem

Ein Rivale, der nicht denkt

In Epic Cosmos spielst du gegen drei Reiche. Während du Befehle tippst, entscheidet jedes von ihnen, was es in dieser Runde tut: eine Mine setzen, ein Labor bauen, einen neuen Planeten besiedeln, Schiffe bestellen, eine Flotte schicken. Wie baut man so einen Rivalen?

Der erste Versuch ist ein Drehbuch: „Runde 1 Mine, Runde 2 Mine, Runde 3 Kolonie …“. Der Spieler durchschaut es in drei Partien und schlägt es danach jedes Mal. Der zweite Versuch ist die Kette „wenn … sonst“: Wenn Geld für eine Kolonie da ist, kolonisiere; sonst, wenn Gefahr droht, baue Schiffe; sonst … Jede neue Aktion braucht eine neue Zeile und neue Ausnahmen zu den anderen, und der Rivale ist trotzdem berechenbar und tut dumme Dinge, weil niemand genau diese Lage vorhergesehen hat.

Es gibt einen besseren Weg. Gib jeder möglichen Aktion eine Zahl: wie gut sie für dieses Reich jetzt ist. Tu die Aktion mit der höchsten Zahl. „Kolonie oder Schiffe?“ wird zum Vergleich zweier Zahlen, nicht zu einer Regel für dieses Paar. Eine neue Art von Aktion ist eine neue Zeile in der Liste der Bewertungen, und sonst wird nichts angefasst.

Und noch etwas macht das Spiel interessant: der Charakter. Der Kriegsherr, der Baumeister und der Gelehrte sind nicht drei Programme, sondern derselbe Code mit verschiedenen Gewichten in den Formeln.

Die Idee

Eine Zahl für jede Aktion

Das ist die Lage, mit der die ganze Lektion arbeitet. Runde 5, ein Reich mit zwei Planeten (Kora I mit einer Werft und Kora II mit einer Mine), zwei weitere freie Planeten im selben System (Vesta I und II), 100 Metall und eine Flottenmacht von 10:

record Option(string Kind, string Target, int Utility);
class Planet
{
public readonly string Name;
public readonly int Rich;
public int Mines;
public bool Lab;
public readonly bool Yard;
public Planet(string name, int rich, int mines, bool yard)
{
Name = name;
Rich = rich;
Mines = mines;
Yard = yard;
}
}
class Empire
{
public int Metal = 100;
public int Power = 10;
public int Turn = 5;
public readonly List<Planet> Owned = new();
public readonly List<Planet> Free = new();
}
const int MAX_MINE = 3;
const int LAB_COST = 40;
const int SHIP_COST = 8;
const int SHIP_POWER = 5;
static Empire NewEmpire()
{
var empire = new Empire();
empire.Owned.Add(new Planet("Kora I", 4, 0, true));
empire.Owned.Add(new Planet("Kora II", 3, 1, false));
empire.Free.Add(new Planet("Vesta I", 3, 0, false));
empire.Free.Add(new Planet("Vesta II", 1, 0, false));
return empire;
}

Was kann dieses Reich tun? Eine Mine auf einem Planeten bauen (sie kostet 15 × (Minen + 1)), ein Labor bauen (40), eine Kolonie auf einem freien Planeten gründen (sie kostet mit jedem Planeten mehr: jetzt 52) oder Schiffe bestellen. Jedes davon bekommt eine Bewertung, und die Bewertungen sind Formeln. Zuerst die beiden Gebäude:

  • Mine = Bau × Reichtum / (4 × (Minen + 1)): Ein reicher Planet ist mehr wert, und jede neue Mine auf demselben Planeten ist weniger wert (der Teiler wächst);
  • Labor = Forschung × 6 / 5 − 40 × Labore: Das zweite Labor ist um 40 weniger wert als das erste.

Dann die Kolonien und die Schiffe:

  • Kolonie = Ausbau × (Reichtum + 6) × 50 / (Kosten × 6): Ein freier reicher Planet lockt, und eine teure Kolonie weniger;
  • Schiffe = 60 + Angriff / 2 + (gewünschte Macht − Macht) / 2 (der letzte Term ist höchstens 100), aber nur, solange die Macht unter der gewünschten liegt. Die gewünschte Macht wächst mit der Runde: (20 + 3 × Runde) × (140 + Angriff) / 240.
static List<Option> Options(Empire empire, Personality p)
{
// Jede Aktion, die das Reich bezahlen kann, bekommt eine Bewertung.
var list = new List<Option>();
int labs = 0;
foreach (Planet planet in empire.Owned)
{
if (planet.Lab) labs++;
}
foreach (Planet planet in empire.Owned)
{
if (planet.Mines < MAX_MINE && empire.Metal >= MineCost(planet))
{
list.Add(new Option("mine", planet.Name, p.Build * planet.Rich / (4 * (planet.Mines + 1))));
}
if (!planet.Lab && empire.Metal >= LAB_COST)
{
list.Add(new Option("lab", planet.Name, p.Research * 6 / 5 - 40 * labs));
}
}
int cost = ColonyCost(empire);
if (empire.Metal >= cost)
{
foreach (Planet planet in empire.Free)
{
int value = planet.Rich + 6;
list.Add(new Option("colony", planet.Name, p.Expand * value * 50 / (cost * 6)));
}
}
int want = WantedPower(empire, p);
foreach (Planet planet in empire.Owned)
{
if (planet.Yard && empire.Power < want && ShipCount(empire) > 0)
{
int urge = Math.Min(100, (want - empire.Power) / 2);
list.Add(new Option("ships", planet.Name, 60 + p.Attack / 2 + urge));
break;
}
}
return list;
}

Das ist die ganze Funktion. Sie wählt nicht: Sie sammelt nur alles, was das Reich bezahlen kann, mit den Bewertungen in einer Liste. Die Bedingung „kann bezahlen“ steht vor der Bewertung: Eine Aktion, die man nicht bezahlen kann, kommt nicht in die Liste.

Option ist ein Record: drei Felder und nichts weiter, während Planet und Empire Klassen mit veränderlichen Feldern sind. Die Ganzzahldivision schneidet den Rest ab, und genau das wollen wir hier: Alle Zahlen sind positiv.
Genauer, für Neugierige

Die Formeln sind abgestimmt, nicht hergeleitet: Die Koeffizienten 4, 50, 6 und 60 sind so gewählt, dass die Bewertungen von Mine, Kolonie und Schiffen vergleichbar sind (Hunderter) und nicht eine tausendmal schwerer wiegt als die andere. Deshalb lässt die letzte Lektion Hunderte KI-gegen-KI-Spiele laufen und misst, ob ein Charakter aus dem Gleichgewicht ist.

Im Spiel gibt es vier Ressourcen (Metall, Gas, Kristall, Uran), und der Reichtum eines Planeten besteht aus vier Zahlen. Hier gibt es Metall und einen Reichtum, damit man jede Zahl sieht.

Die Idee

Die höchste Zahl gewinnt: streng

Ist die Liste fertig, ist die Wahl eine einfache Schleife. Eine streng höhere Bewertung ersetzt die Wahl. Bei gleichen höchsten Bewertungen gewinnt also die, die zuerst angesehen wurde. Der Startwert ist die Schwelle: Eine Bewertung, die sie nicht übertrifft, lohnt sich nicht. Für die Wirtschaft ist sie 0 (eine Aktion mit der Bewertung 0 ist „sinnlos“), für die Flotte wird sie 20 sein (bleiben ist besser).

static Option? Best(List<Option> list, int floor)
{
// Nur eine streng höhere Bewertung gewinnt, bei Gleichstand bleibt die erste.
Option? pick = null;
int bestU = floor;
foreach (Option option in list)
{
if (option.Utility > bestU)
{
pick = option;
bestU = option.Utility;
}
}
return pick;
}
static string Label(Option option)
{
if (option.Kind == "mine") return "Mine " + option.Target;
if (option.Kind == "lab") return "Labor " + option.Target;
if (option.Kind == "colony") return "Kolonie " + option.Target;
return "Schiffe " + option.Target;
}

Die Lage von oben, für die drei Charaktere durchgespielt (jeder bekommt ein neues Reich mit denselben Planeten und demselben Geld). Für jeden stehen die Bewertungen in der Reihenfolge der Liste, am Ende die Wahl:

foreach (Personality p in personalities)
{
List<Option> list = Options(NewEmpire(), p);
Console.WriteLine(p.Name);
foreach (Option option in list)
{
Console.WriteLine(" " + Pad(Label(option), 21) + PadLeft(option.Utility.ToString(), 4));
}
Option? pick = Best(list, 0);
Console.WriteLine(" → " + Label(pick!) + " (" + pick!.Utility + ")");
}
Ausgabe
Kriegsherr
Mine Kora I 90
Labor Kora I 72
Mine Kora II 33
Labor Kora II 72
Kolonie Vesta I 115
Kolonie Vesta II 89
Schiffe Kora I 162
→ Schiffe Kora I (162)
Baumeister
Mine Kora I 140
Labor Kora I 96
Mine Kora II 52
Labor Kora II 96
Kolonie Vesta I 230
Kolonie Vesta II 179
Schiffe Kora I 93
→ Kolonie Vesta I (230)
Gelehrter
Mine Kora I 100
Labor Kora I 132
Mine Kora II 37
Labor Kora II 132
Kolonie Vesta I 100
Kolonie Vesta II 78
Schiffe Kora I 99
→ Labor Kora I (132)

Eine Lage, drei verschiedene Wahlen. Der Kriegsherr wählt die Schiffe (162): Seine gewünschte Macht ist 45, und die Flotte hat nur 10. Der Baumeister wählt die Kolonie auf Vesta I (230), weil sein Ausbau 160 wiegt. Der Gelehrte wählt das Labor auf Kora I (132): Das Labor auf Kora II hat dieselbe Bewertung und bleibt zweiter.

Die Idee

Ein Charakter ist eine Tabelle von Gewichten

Was unterscheidet die drei Reiche im Code? Nur vier Zahlen. Hier sind sie aus dem Kanon des Spiels, PERSONALITIES_DATA:

Charakter Ausbau Bau Angriff Forschung Frieden
Kriegsherr 80 90 170 60 40
Baumeister 160 140 50 80 120
Gelehrter 70 100 60 110 110

Die Spalte „Frieden“ gehört zur Diplomatie (Lektion 5). Die anderen vier werden in die Bewertungen multipliziert: Ausbau in die Kolonien, Bau in die Minen, Angriff in die Schiffe und in den Angriff der Flotte, Forschung in die Labore. Der Kriegsherr ist nicht darauf programmiert anzugreifen: Sein Angriff wiegt einfach 170, beim Baumeister 50. Ein neuer Charakter ist eine neue Zeile in der Tabelle, ohne neuen Code.

Im Spiel sieht man die Rivalen mit dem Befehl emp (der Charakter steht in der zweiten Spalte):

Reich Charakter Pl Macht TecPunkte Zu dir
A Terranisches Reich Baumeister 1 109 0 20
C Lyra-Union Baumeister 1 109 0 20 Frieden
D Vega-Akademie Gelehrter 1 109 0 20 Frieden
B Orion-Hegemonie Kriegsherr 1 109 0 20 Frieden

Sieh es dir an

Die Bewertung in Aktion

Die Szene geht durch vier Etappen. Die ersten drei sind dasselbe Reich in Runde 5, bewertet vom Kriegsherrn, vom Baumeister und vom Gelehrten. Die Zeilen sind die Aktionen in der Reihenfolge der Liste, die Balken sind die Bewertungen, und die orange Linie ist „bisher am besten“: Sie wandert nur bei einer streng höheren Zahl. In der vierten Etappe hat der Baumeister die Kolonie schon bezahlt und bewertet neu.

static List<Option> Options(Empire empire, Personality p)
{
// Jede Aktion, die das Reich bezahlen kann, bekommt eine Bewertung.
var list = new List<Option>();
int labs = 0;
foreach (Planet planet in empire.Owned)
{
if (planet.Lab) labs++;
}
foreach (Planet planet in empire.Owned)
{
if (planet.Mines < MAX_MINE && empire.Metal >= MineCost(planet))
{
list.Add(new Option("mine", planet.Name, p.Build * planet.Rich / (4 * (planet.Mines + 1))));
}
if (!planet.Lab && empire.Metal >= LAB_COST)
{
list.Add(new Option("lab", planet.Name, p.Research * 6 / 5 - 40 * labs));
}
}
int cost = ColonyCost(empire);
if (empire.Metal >= cost)
{
foreach (Planet planet in empire.Free)
{
int value = planet.Rich + 6;
list.Add(new Option("colony", planet.Name, p.Expand * value * 50 / (cost * 6)));
}
}
int want = WantedPower(empire, p);
foreach (Planet planet in empire.Owned)
{
if (planet.Yard && empire.Power < want && ShipCount(empire) > 0)
{
int urge = Math.Min(100, (want - empire.Power) / 2);
list.Add(new Option("ships", planet.Name, 60 + p.Attack / 2 + urge));
break;
}
}
return list;
}
static Option? Best(List<Option> list, int floor)
{
// Nur eine streng höhere Bewertung gewinnt, bei Gleichstand bleibt die erste.
Option? pick = null;
int bestU = floor;
foreach (Option option in list)
{
if (option.Utility > bestU)
{
pick = option;
bestU = option.Utility;
}
}
return pick;
}
Schritt für Schritt, in Worten
  1. Runde 5: 100 Metall, Macht 10. Das Reich bewertet jede Aktion, die es bezahlen kann. Zuerst kommt der Kriegsherr mit den Gewichten 80/90/170/60: Bau 90, Angriff 170.
  2. Kriegsherr: Eine Mine auf Kora I = Bau × Reichtum / (4 × (Minen + 1)) = 90; auf Kora II 33, weil dort schon eine Mine steht. Ein Labor ist Forschung × 6 / 5 = 72, auf beiden Planeten gleich.
  3. Eine Kolonie auf Vesta I = Ausbau × (Reichtum + 6) × 50 / (Kosten × 6) = 115 bei Kosten 52. Schiffe: Die gewünschte Macht ist 45, vorhanden sind 10, also 60 + Angriff / 2 + (45 − 10) / 2 = 162.
  4. Die höchste Bewertung ist 162: Der Kriegsherr baut Schiffe. Die Linie „bisher am besten“ ist dreimal gewandert (Mine 90, Kolonie 115, Schiffe 162), jedes Mal bei einer streng höheren Bewertung.
  5. Dieselbe Welt, andere Gewichte. Der Baumeister (160/140/50/80) schätzt das Bauen: Mine auf Kora I 140, Labor 96. Die Planeten, das Geld und die Formeln sind dieselben.
  6. Die Kolonien bekommen 230 und 179: Der Ausbau wiegt 160. Schiffe nur 93, weil die gewünschte Macht des Baumeisters 27 ist (sein Angriff ist 50).
  7. Der Baumeister wählt die Kolonie auf Vesta I (230). Der Kriegsherr hat mit genau denselben Zahlen Schiffe gewählt.
  8. Der Gelehrte (70/100/60/110) schätzt die Forschung: Ein Labor bekommt 132 auf beiden Planeten, eine Mine auf Kora I 100.
  9. Die Kolonien bekommen 100 und 78, die Schiffe 99: Die gewünschte Macht ist 29.
  10. Zwei Labore mit derselben Bewertung 132. Das zweite ist nicht streng höher, also bleibt die Linie beim ersten: Das Labor auf Kora I gewinnt.
  11. Der Baumeister hat die Kolonie bezahlt: 48 Metall bleiben. Die nächste Kolonie kostet jetzt 64 (jeder Planet macht sie teurer), ist nicht zu bezahlen und bekommt keine Bewertung: Ihre Zeile ist leer. Alles wird neu bewertet.
  12. Die Mine auf Kora I ist wieder 140, Schiffe 93. Jetzt reichen 48 Metall für Mine, Labor oder Schiffe, aber nicht für eine Kolonie.
  13. Der zweite Schritt derselben Runde: Der Baumeister baut eine Mine auf Kora I (140). Der Kreislauf ist: alles bewerten, das Beste tun, bezahlen und neu bewerten.

Der Code neben der Szene ist options und best aus dem Programm. In jedem Schritt leuchten die Zeilen, die gerade arbeiten.

Die Idee

Bezahlen und neu rechnen

Eine Bewertung gilt für die Lage jetzt. Sobald das Reich die beste Aktion tut, ändert sich die Lage: Das Geld wird weniger, der Preis der nächsten Kolonie steigt, eine neue Mine macht die nächste weniger wertvoll, eine neue Kolonie öffnet neue Aktionen. Deshalb nimmt der Rivale die Liste nicht einmal, sondern wiederholt: alles bewerten, das Beste tun, bezahlen, neu bewerten. Höchstens 6-mal pro Runde (der Kanon macht dasselbe) oder bis es nichts mehr zu tun gibt.

static Planet Find(List<Planet> planets, string name)
{
foreach (Planet planet in planets)
{
if (planet.Name == name) return planet;
}
return null!;
}
static void Apply(Empire empire, Option option)
{
if (option.Kind == "mine")
{
Planet planet = Find(empire.Owned, option.Target);
empire.Metal -= MineCost(planet);
planet.Mines++;
}
else if (option.Kind == "lab")
{
empire.Metal -= LAB_COST;
Find(empire.Owned, option.Target).Lab = true;
}
else if (option.Kind == "colony")
{
Planet planet = Find(empire.Free, option.Target);
empire.Metal -= ColonyCost(empire);
empire.Free.Remove(planet);
empire.Owned.Add(planet);
}
else
{
int count = ShipCount(empire);
empire.Metal -= count * SHIP_COST;
empire.Power += count * SHIP_POWER;
}
}
static void EndTurn(Empire empire)
{
int income = 4;
foreach (Planet planet in empire.Owned)
{
income += planet.Rich * (2 + planet.Mines) / 2;
}
empire.Metal += income;
empire.Turn++;
}
static void PlayTurn(Empire empire, Personality p, bool log)
{
// Bezahlen, neu bewerten, und das wiederholen, solange es etwas zu tun gibt (höchstens 6-mal).
for (int step = 1; step <= 6; step++)
{
Option? option = Best(Options(empire, p), 0);
if (option == null)
{
if (log) Console.WriteLine(" nichts mehr zu tun: Metall " + empire.Metal);
return;
}
int before = empire.Metal;
Apply(empire, option);
if (log)
{
Console.WriteLine(" " + step + ". " + Label(option) + " (" + option.Utility + "): Metall " + before + " → " + empire.Metal);
}
}
}
static void Summarize(Personality p)
{
var empire = NewEmpire();
for (int turn = 0; turn < 6; turn++)
{
PlayTurn(empire, p, false);
EndTurn(empire);
}
int mines = 0;
int labs = 0;
foreach (Planet planet in empire.Owned)
{
mines += planet.Mines;
if (planet.Lab) labs++;
}
Console.WriteLine(p.Name + ": Planeten " + empire.Owned.Count + ", Minen " + mines + ", Labore " + labs
+ ", Macht " + empire.Power + ", Metall " + empire.Metal);
}

Hier ist eine Runde des Baumeisters, Aktion für Aktion, und dann, was die drei Charaktere in sechs Runden erreicht haben:

Personality builder = personalities[1];
var home = NewEmpire();
Console.WriteLine(builder.Name + ", Runde " + home.Turn + ":");
PlayTurn(home, builder, true);
Console.WriteLine();
Console.WriteLine("Nach 6 Runden:");
foreach (Personality p in personalities) Summarize(p);
Ausgabe
Baumeister, Runde 5:
1. Kolonie Vesta I (230): Metall 100 → 48
2. Mine Kora I (140): Metall 48 → 33
3. Mine Vesta I (105): Metall 33 → 18
4. Schiffe Kora I (93): Metall 18 → 2
nichts mehr zu tun: Metall 2
Nach 6 Runden:
Kriegsherr: Planeten 2, Minen 4, Labore 0, Macht 75, Metall 19
Baumeister: Planeten 3, Minen 3, Labore 1, Macht 40, Metall 38
Gelehrter: Planeten 2, Minen 4, Labore 1, Macht 45, Metall 21

Die erste Aktion ist die Kolonie (230), die 52 Metall nimmt. Danach sind die Bewertungen andere: Die nächste Kolonie ist nicht mehr zu bezahlen, und das Beste ist die Mine auf Kora I (140). Nach 4 Aktionen reicht das Geld für nichts mehr, und die Runde endet. Nach sechs Runden sind die Unterschiede echt, aber nicht dramatisch: Der Kriegsherr hat die größte Macht (75) und kein Labor, der Baumeister hat die meisten Planeten (3), der Gelehrte hat ein Labor und 4 Minen. Die Gewichte schließen nichts aus: Auch der Baumeister baut ein Labor, wenn das das Beste ist, was ihm bleibt.

Es gibt auch eine Grenze, die du kennen solltest: Die Wahl ist gierig. Der Rivale denkt nicht „ich spare für die Kolonie“, sondern nimmt das Beste jetzt: Der Baumeister kauft zwei kleine Schiffe (Bewertung 93), statt zu warten.

Die Idee

Die Flotte: besiedeln oder angreifen

Das Bauen ist die Hälfte. Die andere Hälfte sind die Flotten: Wohin soll die Flotte fliegen? Dieselbe Technik, eine andere Liste. Die Kandidaten sind die Systeme ringsum, und es gibt zwei Arten von Aktionen: besiedeln (das System hat freie Planeten und keinen Feind) und angreifen (es hat feindliche Planeten, und wir sind stark genug). Vor der Bewertung stehen Filter: Ein Angriff, bei dem das Kräfteverhältnis unter 130% liegt oder das Ziel weiter als 6 entfernt ist, wird gar nicht betrachtet.

const int FLEET_POWER = 40;
const int STAY = 20;
var spots = new List<Spot>
{
new("Brisa", 1, 12, 0, 0),
new("Altan", 5, 0, 12, 10),
new("Cygra", 3, 0, 18, 60),
};
static List<Option> TargetOptions(int power, Personality p, List<Spot> spots)
{
var list = new List<Option>();
foreach (Spot spot in spots)
{
int steps = spot.Distance + 1;
if (spot.FreeValue > 0 && spot.EnemyPower == 0 && spot.Distance <= 5)
{
list.Add(new Option("settle", spot.Name, p.Expand * spot.FreeValue / (6 * steps)));
}
int ratio = power * 100 * 100 / (spot.EnemyPower * 100 + 100);
if (spot.EnemyValue > 0 && ratio >= 130 && spot.Distance <= 6)
{
int sure = Math.Min(ratio, 300);
list.Add(new Option("attack", spot.Name, p.Attack * spot.EnemyValue * sure / (4 * steps * 150)));
}
}
return list;
}

Die Flotte hat Macht 40. Das System Brisa ist frei und nah (Entfernung 1), Altan ist feindlich und weit (5, aber der Feind ist schwach), und Cygra ist feindlich mit Macht 60. Das Kräfteverhältnis gegen Altan ist 363%, wird in der Bewertung aber auf höchstens 300% angehoben. Gegen Cygra ist es 65%, unter 130%: Ein Angriff dort wird nicht bewertet. Diesmal ist die Schwelle STAY, 20: Wenn nichts sie übertrifft, bleibt die Flotte.

foreach (Personality p in personalities)
{
List<Option> list = TargetOptions(FLEET_POWER, p, spots);
Console.WriteLine(p.Name);
foreach (Option option in list)
{
Console.WriteLine(" " + Pad(Label(option), 18) + PadLeft(option.Utility.ToString(), 4));
}
Option? pick = Best(list, STAY);
Console.WriteLine(" → " + (pick == null ? "bleibt" : Label(pick) + " (" + pick.Utility + ")"));
}
Ausgabe
Kriegsherr
besiedeln Brisa 80
angreifen Altan 170
→ angreifen Altan (170)
Baumeister
besiedeln Brisa 160
angreifen Altan 50
→ besiedeln Brisa (160)
Gelehrter
besiedeln Brisa 70
angreifen Altan 60
→ besiedeln Brisa (70)

Der Kriegsherr greift Altan an (170), trotz der Entfernung. Der Baumeister besiedelt Brisa (160). Der Gelehrte besiedelt ebenfalls Brisa, aber nur knapp: 70 gegen 60. Die Formeln sind dieselben, nur die Gewichte unterscheiden sich.

Die Idee

Im Spiel

Was wir gebaut haben, ist dasselbe wie im Kanon von Epic Cosmos, nur kleiner. Hier ist die echte Bewertung der Wirtschaft. Man erkennt die Mine, das Labor, die Kolonie und die Schiffe aus unserem Programm. Dazu kommen zwei weitere Aktionen: eine Werft (yard, Bewertung 250 für die erste, denn ohne sie gibt es keine Flotte) und eine Festung (fort, nur wenn feindliche Macht in der Nähe ist). Die Wahl ist dieselbe Idee, aber mit consider(Bewertung, Aktion) statt einer Liste:

// Eine Aktion in der Wirtschaft: die beste nach Nutzen, oder null.
public static AiAction? AiBestAction(World world, Empire empire)
{
Personality p = empire.Personality;
AiAction? best = null;
int bestU = 0;
void Consider(int u, AiAction action)
{
if (u > bestU)
{
bestU = u;
best = action;
}
}
List<Planet> mine = OwnedPlanets(world, empire.Id);
int labs = 0;
int yards = 0;
foreach (Planet planet in mine)
{
labs += planet.Lab;
yards += planet.Yard;
}
Dictionary<string, int> mods = TechMods(empire);
foreach (Planet planet in mine)
{
if (planet.Mines < MAX_MINE && CanPay(empire, BuildCost("mine", planet)))
{
Consider((p.Build * RichSum(planet)) / (4 * (planet.Mines + 1)), new AiAction { Kind = "mine", PlanetId = planet.Id });
}
if (planet.Lab == 0 && CanPay(empire, BuildCost("lab", planet)))
{
Consider((p.Research * 6) / 5 - 40 * labs, new AiAction { Kind = "lab", PlanetId = planet.Id });
}
if (planet.Yard == 0 && CanPay(empire, BuildCost("yard", planet)))
{
Consider(yards == 0 ? 250 : mine.Count >= 5 && yards < 2 ? 60 : 0, new AiAction { Kind = "yard", PlanetId = planet.Id });
}
if (mods["fort"] > 0 && planet.Fort < MAX_FORT && CanPay(empire, BuildCost("fort", planet)))
{
int threat = EnemyPowerAt(world, empire, planet.SystemId);
foreach (int next in world.Systems[planet.SystemId].Links) threat += EnemyPowerAt(world, empire, next);
if (threat > 0) Consider(40 + MinOf(160, threat) + (planet.Id == empire.Capital ? 30 : 0), new AiAction { Kind = "fort", PlanetId = planet.Id });
}
}
int[] cost = ColonyCost(world, empire);
if (CanPay(empire, cost))
{
foreach (Planet planet in world.Planets)
{
if (planet.Owner >= 0 || !HasPresence(world, empire.Id, planet.SystemId) || HostileIn(world, empire.Id, planet.SystemId)) continue;
Consider((p.Expand * (RichSum(planet) + 6) * 50) / (cost[0] * 6), new AiAction { Kind = "col", PlanetId = planet.Id });
}
}
// Schiffe: solange die Stärke unter der für diese Runde gewünschten liegt.
int want = ((20 + 3 * world.Turn) * (140 + p.Attack)) / 240;
foreach (Empire other in world.Empires)
{
if (other.Alive && other.Id != empire.Id && empire.War[other.Id] == 1) want = (want * 3) / 2;
}
int have = EmpirePower(world, empire.Id) + QueuedPower(world, empire.Id);
if (have < want)
{
foreach (Planet planet in mine)
{
if (planet.Yard == 0) continue;
int kind = CounterKind(world, empire);
if (ShipAllowed(empire, 3) && CanPay(empire, SHIPS[3].Cost) && have >= 30) kind = 3;
int count = 4;
while (count > 0 && !CanPay(empire, ScaleCost(SHIPS[kind].Cost, count))) count--;
if (count > 0) Consider(60 + p.Attack / 2 + MinOf(100, (want - have) / 2), new AiAction { Kind = "ship", PlanetId = planet.Id, ShipKind = kind, Count = count });
break;
}
}
return best;
}

Und hier die Schleife „bewerten, tun, wiederholen“ (aiEconomy) und die ganze Runde eines Rivalen (aiPlay): Forschung, Wirtschaft, Flotten. Die Forschung ist dieselbe Technik im Kleinen: aiResearch bewertet jede verfügbare Technologie mit Forschung × (100 + Bonus des Charakters) × 10 / (Kosten + 5) und nimmt die höchste (der Technologiebaum steht in Lektion 2).

public static void AiEconomy(World world, Empire empire)
{
for (int step = 0; step < 6; step++)
{
AiAction? action = AiBestAction(world, empire);
if (action == null) return;
Planet planet = world.Planets[action.PlanetId];
string? error = null;
if (action.Kind == "col") error = Colonize(world, empire, planet);
else if (action.Kind == "ship") error = QueueShips(world, empire, planet, action.ShipKind, action.Count);
else error = BuildStructure(world, empire, action.Kind, planet);
if (error != null) return;
}
}
public static void AiPlay(World world, Empire empire)
{
// Die künstliche Intelligenz plant jede Runde neu.
empire.Orders = new List<Order>();
AiResearch(world, empire);
AiEconomy(world, empire);
AiFleets(world, empire);
}

Die Unterschiede zu unserem Programm sind gering:

  • Vier Ressourcen statt einer. canPay vergleicht vier Zahlen, und der Reichtum eines Planeten besteht aus vier Zahlen (richSum addiert sie und gewichtet Kristall und Uran doppelt).
  • Dieselben Funktionen wie der Spieler. Der Rivale ruft colonize, queueShips und buildStructure auf, die auch der Spieler mit den Befehlen col, ship und b aufruft. Er zahlt dieselben Preise nach denselben Regeln.
  • Krieg ändert die gewünschte Macht. Für jedes Reich, mit dem wir im Krieg sind, wird die gewünschte Macht mit 3/2 multipliziert, und die Art der Schiffe wird nach dem Feind gewählt (counterKind: Abfangjäger schlägt Bomber, Bomber schlägt Fregatte, Fregatte schlägt Abfangjäger; die Rollen stammen aus Lektion 3).
  • Verteidigung steckt in den Bewertungen. Eine Festung wird gebaut, wenn ein Feind in der Nähe ist, und die Flotte kehrt zurück, um das eigene System zu verteidigen (ownHere).
  • Kein Zufall. Kein einziger Würfel in diesen Entscheidungen: Dieselbe Eingabe ergibt denselben Zug in Java, C#, Python und JavaScript.

Und hier die Wahl des Flottenziels:

public static void AiFleets(World world, Empire empire)
{
Personality p = empire.Personality;
List<Fleet> snapshot = new List<Fleet>();
foreach (Fleet fleet in world.Fleets)
{
if (fleet.Owner == empire.Id) snapshot.Add(fleet);
}
foreach (Fleet fleet in snapshot)
{
int[] busy = OrderedShips(empire, fleet.SystemId);
int[] free = { 0, 0, 0, 0 };
for (int k = 0; k < SHIP_KIND_COUNT; k++) free[k] = fleet.Ships[k] - busy[k];
int myPower = CountsPower(free);
if (myPower == 0) continue;
int here = fleet.SystemId;
int[] dist = Bfs(world.Systems, here).Dist;
// Halten: wenn wir zu Hause unter Bedrohung sind oder einen Feind belagern.
int bestU = 20;
int target = -1;
bool split = false;
bool siege = false;
foreach (StarSystem system in world.Systems)
{
int foe = EnemyPowerAt(world, empire, system.Id);
int freeValue = 0;
int enemyValue = 0;
bool ownHere = false;
foreach (int pid in system.Planets)
{
Planet planet = world.Planets[pid];
if (planet.Owner < 0) freeValue += RichSum(planet) + 6;
else if (planet.Owner == empire.Id) ownHere = true;
else if (empire.War[planet.Owner] == 1) enemyValue += RichSum(planet) + 10;
}
int d = dist[system.Id];
if (d < 0) continue;
int ratio = Percent((long)myPower * 100, (long)foe * 100 + 100);
if (system.Id == here)
{
if (enemyValue > 0 && ratio >= 80)
{
bestU = 250;
siege = true;
}
else if (ownHere && foe > 0 && ratio >= 60)
{
bestU = 200;
siege = true;
}
continue;
}
if (freeValue > 0 && foe == 0 && d <= 5)
{
int u = (p.Expand * freeValue) / (6 * (d + 1));
if (u > bestU)
{
bestU = u;
target = system.Id;
split = TotalOf(free) >= 6;
siege = false;
}
}
if (enemyValue > 0 && ratio >= 130 && d <= 6)
{
int u = (p.Attack * enemyValue * MinOf(ratio, 300)) / (4 * (d + 1) * 150);
if (u > bestU)
{
bestU = u;
target = system.Id;
split = false;
siege = false;
}
}
if (ownHere && foe > 0 && d <= 4 && ratio >= 60)
{
int u = 150 - 10 * d;
if (u > bestU)
{
bestU = u;
target = system.Id;
split = false;
siege = false;
}
}
}
if (target < 0 || siege) continue;
int step = NextStep(world.Systems, here, target);
if (step < 0) continue;
OrderMove(world, empire, here, target, split ? HalfOf(free) : null);
}
}

Es sieht aus wie unser Programm: dieselben zwei Formeln (besiedeln und angreifen), der Filter 130%, die Schwelle 20. Dazu kommen das „Halten“ (in einem System mit Feinden und gutem Verhältnis bleibt die Flotte und belagert: 250), die Verteidigung des eigenen Systems (150 − 10 × Entfernung) und das Teilen der Flotte in zwei Hälften, wenn sie groß ist.

Probier es aus

Sag die Ausgabe voraus

Vier Aktionen, zwei davon mit der höchsten Bewertung 132. Was gibt das Programm aus? (Die Funktionen best und label stammen aus der Lektion.)

var tie = new List<Option>
{
new("mine", "Kora I", 90),
new("lab", "Kora I", 132),
new("lab", "Kora II", 132),
new("colony", "Vesta I", 100),
};
Option? winner = Best(tie, 0);
Console.WriteLine(Label(winner!) + " (" + winner!.Utility + ")");
Antwort

Probier es aus

Fülle die Lücke

Der Startwert des Besten ist der Parameter der Funktion: 0 für die Wirtschaft, 20 für die Flotte. Womit beginnt bestU?

static Option? Best(List<Option> list, int floor)
{
    // Nur eine streng höhere Bewertung gewinnt, bei Gleichstand bleibt die erste.
    Option? pick = null;
    int bestU = ;
    foreach (Option option in list)
    {
        if (option.Utility > bestU)
        {
            pick = option;
            bestU = option.Utility;
        }
    }
    return pick;
}

Probier es aus

Finde den Fehler

Ein Kollege berechnet die Kolonien ohne Klammern, und jetzt kolonisiert jedes Reich, sogar der Kriegsherr, nur noch. Hier sind die Bewertungen des Baumeisters:

foreach (Personality p in personalities)
{
List<Option> list = Options(NewEmpire(), p);
Console.WriteLine(p.Name);
foreach (Option option in list)
{
Console.WriteLine(" " + Pad(Label(option), 21) + PadLeft(option.Utility.ToString(), 4));
}
Option? pick = Best(list, 0);
Console.WriteLine(" → " + Label(pick!) + " (" + pick!.Utility + ")");
}
Ausgabe
Kriegsherr
Mine Kora I 90
Labor Kora I 72
Mine Kora II 33
Labor Kora II 72
Kolonie Vesta I 4152
Kolonie Vesta II 3228
Schiffe Kora I 162
→ Kolonie Vesta I (4152)
Baumeister
Mine Kora I 140
Labor Kora I 96
Mine Kora II 52
Labor Kora II 96
Kolonie Vesta I 8304
Kolonie Vesta II 6456
Schiffe Kora I 93
→ Kolonie Vesta I (8304)
Gelehrter
Mine Kora I 100
Labor Kora I 132
Mine Kora II 37
Labor Kora II 132
Kolonie Vesta I 3630
Kolonie Vesta II 2826
Schiffe Kora I 99
→ Kolonie Vesta I (3630)

Die Kolonie auf Vesta I ist 8304 statt 230. Welche Zeile rechnet falsch?

Wähle die Zeile mit dem Fehler.

Genauer

Genauer: Was eine Bewertung nicht tut

Sie ist keine Wahrscheinlichkeit und hat keine Einheit. 162 heißt nicht „162 Punkte Nutzen“. Die Zahl hat nur zum Vergleich innerhalb einer Liste eines Rivalen einen Sinn. Deshalb sind die Formeln abgestimmt (4, 50, 6, 60), nicht hergeleitet. Die Gewichte des Charakters sind ganze Zahlen „in Prozent“, wie die Tabelle des Kanons sagt, damit es keine Brüche gibt.

Sie ist gierig. Der Rivale sieht nur auf die nächste Aktion und plant nicht: Er spart nicht für eine Kolonie und weiß nicht, dass er in drei Runden schwächer sein wird. Das ist der Preis der Einfachheit. Ernsthaftere Systeme schauen voraus: Zielplanung (GOAP), Suchbäume oder Monte Carlo (MCTS, wie im Go). Aber in einer rundenbasierten Strategie ist eine gierige Wahl mit guten Formeln meist klug genug, um Spaß zu machen. In den Kämpfen von Arkanum ist es dieselbe Technik: Gegner, die denken.

Bewertungen werden abgestimmt, nicht geschrieben. Woher kommen die Zahlen 80, 90, 170? Erst eine Schätzung, dann Balance per Simulation: KI-gegen-KI-Spiele, die zählen, welcher Charakter wie oft gewinnt. Dieses Werkzeug steht in der letzten Lektion.

Der Rivale betrügt nicht, im Gegenteil. Er zahlt dieselben Preise und ruft dieselben Funktionen auf wie du, aber in einem Spiel mit einem Menschen erzeugt er nur 80% des Normalen (RIVAL_PACE, die leichte Stufe); in der Simulation sind es 100%. Die Zeile steht in empireIncome und liegt offen da.

Gleiche Bewertungen werden nicht dem Zufall überlassen. Das strenge > und die feste Reihenfolge der Erzeugung machen die Wahl in den vier Sprachen und bei jeder Wiederholung gleich: für jeden Planeten Mine, Labor, Werft, Festung, dann die Kolonien, zuletzt die Schiffe. Deshalb gehört die Reihenfolge zu den Regeln, sie ist kein Detail.

Übung

Die Festung

Löse die Aufgabe in deinem Editor. Beginne mit dem Programm für die drei Charaktere.

Festung. Füge eine neue Aktion fort hinzu: eine Orbitalfestung auf einem Planeten. Sie kostet 25 Metall (FORT_COST) und wird nach der Gefahr bewertet: 40 + min(160, Gefahr) + 30, wenn der Planet die Hauptstadt ist (der erste in der Liste), sonst ohne das +30. Die Gefahr ist ein neuer Wert threat des Reichs und ist in dieser Aufgabe 120. Die Aktion ist nur zulässig, wenn die Gefahr positiv ist und 25 Metall da sind. Die Reihenfolge in der Liste ist: für jeden Planeten Mine, Labor, Festung.

Die erwartete Ausgabe (füge fort auch in label ein):

Ausgabe
Kriegsherr
Mine Kora I 90
Labor Kora I 72
Festung Kora I 190
Mine Kora II 33
Labor Kora II 72
Festung Kora II 160
Kolonie Vesta I 115
Kolonie Vesta II 89
Schiffe Kora I 162
→ Festung Kora I (190)
Baumeister
Mine Kora I 140
Labor Kora I 96
Festung Kora I 190
Mine Kora II 52
Labor Kora II 96
Festung Kora II 160
Kolonie Vesta I 230
Kolonie Vesta II 179
Schiffe Kora I 93
→ Kolonie Vesta I (230)
Gelehrter
Mine Kora I 100
Labor Kora I 132
Festung Kora I 190
Mine Kora II 37
Labor Kora II 132
Festung Kora II 160
Kolonie Vesta I 100
Kolonie Vesta II 78
Schiffe Kora I 99
→ Festung Kora I (190)

Was ändert sich bei den Wahlen? Bei Gefahr 120 hat die Festung der Hauptstadt 190, und sie schlägt die Schiffe des Kriegsherrn (162) und das Labor des Gelehrten (132). Der Baumeister kolonisiert weiter: 230 ist mehr als 190. Gefahr ist kein Charakter: Die Festung hat für alle dieselbe Bewertung, deshalb steht sie nicht in der Tabelle der Gewichte.

Weitere Aufgaben

  • Die Festung bauen. Füge in apply den Bau einer Festung hinzu und spiele eine ganze Runde des Kriegsherrn mit Gefahr 120 durch: Wie viele Aktionen macht er, bis ihm das Geld ausgeht?
  • Ein neuer Charakter. Füge eine vierte Zeile hinzu (trader: Ausbau 120, Bau 120, Angriff 60, Forschung 70). Was wählt er in der Ausgangslage? Ändert sich im Code etwas außer der Tabelle?
  • Die Obergrenze. Der Angriff der Flotte benutzt min(Verhältnis, 300). Was passiert ohne die Obergrenze, wenn der Feind Macht 0 hat (das Verhältnis ist 4000)? Wie ändert sich die Wahl des Kriegsherrn und des Baumeisters?

Test

Prüfe dich

1. Der Kriegsherr und der Baumeister bewerten dieselbe Welt: dieselben Planeten, dasselbe Geld, dieselben Formeln. Der Kriegsherr wählt Schiffe, der Baumeister eine Kolonie. Woher kommt der Unterschied?
2. Der Gelehrte hat zwei Labore mit der Bewertung 132, und das ist die höchste Bewertung. Welches wird gebaut?
3. Nach der Kolonie hat der Baumeister 48 Metall, und in der Liste steht gar keine Kolonie, obwohl Vesta II noch frei ist. Warum?
4. Ein Kollege schreibt „Ausbau × Wert × 50 / Kosten × 6“ statt „… / (Kosten × 6)“. Wie hoch ist die Bewertung der Kolonie auf Vesta I für den Baumeister?
5. Die Flotte hat Macht 40. Im System Cygra hat der Feind Macht 60, und kein Charakter greift es an, nicht einmal der Kriegsherr. Warum?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·