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:
@dataclass(frozen=True)class Option: kind: str target: str utility: int
class Planet: def __init__(self, name, rich, mines, yard): self.name = name self.rich = rich self.mines = mines self.lab = False self.yard = yard
class Empire: def __init__(self): self.metal = 100 self.power = 10 self.turn = 5 self.owned = [] self.free = []MAX_MINE = 3LAB_COST = 40SHIP_COST = 8SHIP_POWER = 5
def new_empire(): empire = Empire() empire.owned.append(Planet("Kora I", 4, 0, True)) empire.owned.append(Planet("Kora II", 3, 1, False)) empire.free.append(Planet("Vesta I", 3, 0, False)) empire.free.append(Planet("Vesta II", 1, 0, False)) return empireWas 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.
def options(empire, p): # Jede Aktion, die das Reich bezahlen kann, bekommt eine Bewertung. result = [] labs = 0 for planet in empire.owned: if planet.lab: labs += 1 for planet in empire.owned: if planet.mines < MAX_MINE and empire.metal >= mine_cost(planet): result.append(Option("mine", planet.name, p.build * planet.rich // (4 * (planet.mines + 1)))) if not planet.lab and empire.metal >= LAB_COST: result.append(Option("lab", planet.name, p.research * 6 // 5 - 40 * labs)) cost = colony_cost(empire) if empire.metal >= cost: for planet in empire.free: value = planet.rich + 6 result.append(Option("colony", planet.name, p.expand * value * 50 // (cost * 6))) want = wanted_power(empire, p) for planet in empire.owned: if planet.yard and empire.power < want and ship_count(empire) > 0: urge = min(100, (want - empire.power) // 2) result.append(Option("ships", planet.name, 60 + p.attack // 2 + urge)) break return resultDas 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 eine @dataclass(frozen=True): drei Felder und nichts weiter, während Planet und Empire gewöhnliche Klassen sind. Die Ganzzahldivision ist // und schneidet den Rest ab: Alle Zahlen sind positiv, also gibt es keine Überraschungen mit negativen Zahlen.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).
def best(candidates, floor): # Nur eine streng höhere Bewertung gewinnt, bei Gleichstand bleibt die erste. pick = None best_u = floor for option in candidates: if option.utility > best_u: pick = option best_u = option.utility return pick
def label(option): if option.kind == "mine": return "Mine " + option.target if option.kind == "lab": return "Labor " + option.target if option.kind == "colony": return "Kolonie " + option.target return "Schiffe " + option.targetDie 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:
for p in PERSONALITIES: options_list = options(new_empire(), p) print(p.name) for option in options_list: print(" " + pad(label(option), 21) + pad_left(str(option.utility), 4)) pick = best(options_list, 0) print(" → " + label(pick) + " (" + str(pick.utility) + ")")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 dirA Terranisches Reich Baumeister 1 109 0 20C Lyra-Union Baumeister 1 109 0 20 FriedenD Vega-Akademie Gelehrter 1 109 0 20 FriedenB Orion-Hegemonie Kriegsherr 1 109 0 20 FriedenSieh 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.
def options(empire, p): # Jede Aktion, die das Reich bezahlen kann, bekommt eine Bewertung. result = [] labs = 0 for planet in empire.owned: if planet.lab: labs += 1 for planet in empire.owned: if planet.mines < MAX_MINE and empire.metal >= mine_cost(planet): result.append(Option("mine", planet.name, p.build * planet.rich // (4 * (planet.mines + 1)))) if not planet.lab and empire.metal >= LAB_COST: result.append(Option("lab", planet.name, p.research * 6 // 5 - 40 * labs)) cost = colony_cost(empire) if empire.metal >= cost: for planet in empire.free: value = planet.rich + 6 result.append(Option("colony", planet.name, p.expand * value * 50 // (cost * 6))) want = wanted_power(empire, p) for planet in empire.owned: if planet.yard and empire.power < want and ship_count(empire) > 0: urge = min(100, (want - empire.power) // 2) result.append(Option("ships", planet.name, 60 + p.attack // 2 + urge)) break return result
def best(candidates, floor): # Nur eine streng höhere Bewertung gewinnt, bei Gleichstand bleibt die erste. pick = None best_u = floor for option in candidates: if option.utility > best_u: pick = option best_u = option.utility return pickSchritt für Schritt, in Worten
- 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.
- 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.
- 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.
- 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.
- 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.
- 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).
- Der Baumeister wählt die Kolonie auf Vesta I (230). Der Kriegsherr hat mit genau denselben Zahlen Schiffe gewählt.
- Der Gelehrte (70/100/60/110) schätzt die Forschung: Ein Labor bekommt 132 auf beiden Planeten, eine Mine auf Kora I 100.
- Die Kolonien bekommen 100 und 78, die Schiffe 99: Die gewünschte Macht ist 29.
- 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.
- 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.
- 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.
- 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.
def find(planets, name): for planet in planets: if planet.name == name: return planet return None
def apply(empire, option): if option.kind == "mine": planet = find(empire.owned, option.target) empire.metal -= mine_cost(planet) planet.mines += 1 elif option.kind == "lab": empire.metal -= LAB_COST find(empire.owned, option.target).lab = True elif option.kind == "colony": planet = find(empire.free, option.target) empire.metal -= colony_cost(empire) empire.free.remove(planet) empire.owned.append(planet) else: count = ship_count(empire) empire.metal -= count * SHIP_COST empire.power += count * SHIP_POWER
def end_turn(empire): income = 4 for planet in empire.owned: income += planet.rich * (2 + planet.mines) // 2 empire.metal += income empire.turn += 1
def play_turn(empire, p, log): # Bezahlen, neu bewerten, und das wiederholen, solange es etwas zu tun gibt (höchstens 6-mal). for step in range(1, 7): option = best(options(empire, p), 0) if option is None: if log: print(" nichts mehr zu tun: Metall " + str(empire.metal)) return before = empire.metal apply(empire, option) if log: print(" " + str(step) + ". " + label(option) + " (" + str(option.utility) + "): Metall " + str(before) + " → " + str(empire.metal))
def summarize(p): empire = new_empire() for _ in range(6): play_turn(empire, p, False) end_turn(empire) mines = 0 labs = 0 for planet in empire.owned: mines += planet.mines if planet.lab: labs += 1 print(p.name + ": Planeten " + str(len(empire.owned)) + ", Minen " + str(mines) + ", Labore " + str(labs) + ", Macht " + str(empire.power) + ", Metall " + str(empire.metal))Hier ist eine Runde des Baumeisters, Aktion für Aktion, und dann, was die drei Charaktere in sechs Runden erreicht haben:
builder = PERSONALITIES[1]empire = new_empire()print(builder.name + ", Runde " + str(empire.turn) + ":")play_turn(empire, builder, True)print()print("Nach 6 Runden:")for p in PERSONALITIES: summarize(p)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 19Baumeister: Planeten 3, Minen 3, Labore 1, Macht 40, Metall 38Gelehrter: Planeten 2, Minen 4, Labore 1, Macht 45, Metall 21Die 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.
FLEET_POWER = 40STAY = 20SPOTS = [ Spot("Brisa", 1, 12, 0, 0), Spot("Altan", 5, 0, 12, 10), Spot("Cygra", 3, 0, 18, 60),]
def target_options(power, p): result = [] for spot in SPOTS: steps = spot.distance + 1 if spot.free_value > 0 and spot.enemy_power == 0 and spot.distance <= 5: result.append(Option("settle", spot.name, p.expand * spot.free_value // (6 * steps))) ratio = power * 100 * 100 // (spot.enemy_power * 100 + 100) if spot.enemy_value > 0 and ratio >= 130 and spot.distance <= 6: sure = min(ratio, 300) result.append(Option("attack", spot.name, p.attack * spot.enemy_value * sure // (4 * steps * 150))) return resultDie 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.
for p in PERSONALITIES: options_list = target_options(FLEET_POWER, p) print(p.name) for option in options_list: print(" " + pad(label(option), 18) + pad_left(str(option.utility), 4)) pick = best(options_list, STAY) print(" → " + ("bleibt" if pick is None else label(pick) + " (" + str(pick.utility) + ")"))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 None.def ai_best_action(world, empire): p = empire.personality best = None best_u = 0
def consider(u, action): nonlocal best, best_u if u > best_u: best_u = u best = action
mine = owned_planets(world, empire.id) labs = 0 yards = 0 for planet in mine: labs += planet.lab yards += planet.yard mods = tech_mods(empire) for planet in mine: if planet.mines < MAX_MINE and can_pay(empire, build_cost('mine', planet)): consider((p.build * rich_sum(planet)) // (4 * (planet.mines + 1)), SimpleNamespace(kind='mine', planet_id=planet.id)) if planet.lab == 0 and can_pay(empire, build_cost('lab', planet)): consider((p.research * 6) // 5 - 40 * labs, SimpleNamespace(kind='lab', planet_id=planet.id)) if planet.yard == 0 and can_pay(empire, build_cost('yard', planet)): consider(250 if yards == 0 else (60 if len(mine) >= 5 and yards < 2 else 0), SimpleNamespace(kind='yard', planet_id=planet.id)) if mods['fort'] > 0 and planet.fort < MAX_FORT and can_pay(empire, build_cost('fort', planet)): threat = enemy_power_at(world, empire, planet.system_id) for nxt in world.systems[planet.system_id].links: threat += enemy_power_at(world, empire, nxt) if threat > 0: consider(40 + min_of(160, threat) + (30 if planet.id == empire.capital else 0), SimpleNamespace(kind='fort', planet_id=planet.id)) cost = colony_cost(world, empire) if can_pay(empire, cost): for planet in world.planets: if planet.owner >= 0 or not has_presence(world, empire.id, planet.system_id) or hostile_in(world, empire.id, planet.system_id): continue consider((p.expand * (rich_sum(planet) + 6) * 50) // (cost[0] * 6), SimpleNamespace(kind='col', planet_id=planet.id)) # Schiffe: solange die Stärke unter der für diese Runde gewünschten liegt. want = ((20 + 3 * world.turn) * (140 + p.attack)) // 240 for other in world.empires: if other.alive and other.id != empire.id and empire.war[other.id] == 1: want = (want * 3) // 2 have = empire_power(world, empire.id) + queued_power(world, empire.id) if have < want: for planet in mine: if planet.yard == 0: continue kind = counter_kind(world, empire) if ship_allowed(empire, 3) and can_pay(empire, SHIPS[3].cost) and have >= 30: kind = 3 count = 4 while count > 0 and not can_pay(empire, scale_cost(SHIPS[kind].cost, count)): count -= 1 if count > 0: consider(60 + p.attack // 2 + min_of(100, (want - have) // 2), SimpleNamespace(kind='ship', planet_id=planet.id, ship_kind=kind, count=count)) break return bestUnd 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).
def ai_economy(world, empire): for step in range(6): action = ai_best_action(world, empire) if action is None: return planet = world.planets[action.planet_id] error = None if action.kind == 'col': error = colonize(world, empire, planet) elif action.kind == 'ship': error = queue_ships(world, empire, planet, action.ship_kind, action.count) else: error = build_structure(world, empire, action.kind, planet) if error is not None: return
def ai_play(world, empire): # Die künstliche Intelligenz plant jede Runde neu. empire.orders = [] ai_research(world, empire) ai_economy(world, empire) ai_fleets(world, empire)Die Unterschiede zu unserem Programm sind gering:
- Vier Ressourcen statt einer.
canPayvergleicht vier Zahlen, und der Reichtum eines Planeten besteht aus vier Zahlen (richSumaddiert sie und gewichtet Kristall und Uran doppelt). - Dieselben Funktionen wie der Spieler. Der Rivale ruft
colonize,queueShipsundbuildStructureauf, die auch der Spieler mit den Befehlencol,shipundbaufruft. 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:
def ai_fleets(world, empire): p = empire.personality snapshot = [] for fleet in world.fleets: if fleet.owner == empire.id: snapshot.append(fleet) for fleet in snapshot: busy = ordered_ships(empire, fleet.system_id) free = [0, 0, 0, 0] for k in range(SHIP_KIND_COUNT): free[k] = fleet.ships[k] - busy[k] my_power = counts_power(free) if my_power == 0: continue here = fleet.system_id dist = bfs(world.systems, here).dist # Halten: wenn wir zu Hause unter Bedrohung sind oder einen Feind belagern. best_u = 20 target = -1 split = False siege = False for system in world.systems: foe = enemy_power_at(world, empire, system.id) free_value = 0 enemy_value = 0 own_here = False for pid in system.planets: planet = world.planets[pid] if planet.owner < 0: free_value += rich_sum(planet) + 6 elif planet.owner == empire.id: own_here = True elif empire.war[planet.owner] == 1: enemy_value += rich_sum(planet) + 10 d = dist[system.id] if d < 0: continue ratio = percent(my_power * 100, foe * 100 + 100) if system.id == here: if enemy_value > 0 and ratio >= 80: best_u = 250 siege = True elif own_here and foe > 0 and ratio >= 60: best_u = 200 siege = True continue if free_value > 0 and foe == 0 and d <= 5: u = (p.expand * free_value) // (6 * (d + 1)) if u > best_u: best_u = u target = system.id split = total_of(free) >= 6 siege = False if enemy_value > 0 and ratio >= 130 and d <= 6: u = (p.attack * enemy_value * min_of(ratio, 300)) // (4 * (d + 1) * 150) if u > best_u: best_u = u target = system.id split = False siege = False if own_here and foe > 0 and d <= 4 and ratio >= 60: u = 150 - 10 * d if u > best_u: best_u = u target = system.id split = False siege = False if target < 0 or siege: continue step = next_step(world.systems, here, target) if step < 0: continue order_move(world, empire, here, target, half_of(free) if split else None)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.)
options_list = [ Option("mine", "Kora I", 90), Option("lab", "Kora I", 132), Option("lab", "Kora II", 132), Option("colony", "Vesta I", 100),]pick = best(options_list, 0)print(label(pick) + " (" + str(pick.utility) + ")")Das Labor auf Kora I ist das erste mit der Bewertung 132. Das Labor auf Kora II hat dieselbe Bewertung, ist aber nicht streng höher und ändert die Wahl nicht. Die Kolonie (100) ist niedriger. Die zweite Antwort käme bei >= heraus: Dann ändert jede gleiche Bewertung die Wahl, und die letzte gewinnt.
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?
def best(candidates, floor):
# Nur eine streng höhere Bewertung gewinnt, bei Gleichstand bleibt die erste.
pick = None
best_u =
for option in candidates:
if option.utility > best_u:
pick = option
best_u = option.utility
return pickEs beginnt bei der Schwelle: floor. So taugt dieselbe Funktion für die Wirtschaft (0) und für die Flotte (20), und eine Bewertung, die sie nicht übertrifft, gewinnt nichts: Das Ergebnis ist null. Würde alles bei 0 beginnen, zöge die Flotte zu einem Angriff mit der Bewertung 5 los, statt zu bleiben.
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:
for p in PERSONALITIES: options_list = options(new_empire(), p) print(p.name) for option in options_list: print(" " + pad(label(option), 21) + pad_left(str(option.utility), 4)) pick = best(options_list, 0) print(" → " + label(pick) + " (" + str(pick.utility) + ")")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.
In … * 50 / cost * 6 wird zuerst durch die Kosten geteilt (ganzzahlig, der Rest abgeschnitten) und dann mit 6 multipliziert: 72000 / 52 = 1384, 1384 × 6 = 8304. Die Formel ist … × 50 / (Kosten × 6): Der Teiler ist das ganze Produkt. Ohne die Klammern ist die Kolonie etwa 36-mal größer als nötig und schlägt alles. Lehre: Die Ganzzahldivision ist nicht assoziativ, und eine Formelbewertung braucht ihre Klammern.
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):
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
applyden 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
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Exceptions · Werfen und eigene Exceptions · Listen: append, remove und sort · dataclass, match und Muster
- Aufgaben zum Thema: Epic Cosmos: das Imperium