KOBALTMEERCODE. FLOTTE. WETTKAMPF.
Anleitungen & Inhalt

ANLEITUNGEN

AUF DIESER SEITE

Bot-Anleitung (Python)

Ein Bot ist eine Python-Datei mit einer Klasse Bot. In jeder Runde bekommt er eine Beobachtung des Spielzustands und gibt Befehle für seine Schiffe zurück.

Der kleinste Bot

from kobaltmeer.sdk import BaseBot, Observation, Orders

class Bot(BaseBot):
    name = "Mein Bot"

    def decide(self, observation: Observation, player: int) -> Orders:
        orders = Orders()
        for ship in observation.fleet(player):
            orders.ships[ship.id] = "O"  # stehen bleiben und abbauen
        return orders

Dieser Bot baut nur ab und liefert nie etwas ab. Ein vollständiges Beispiel mit Bewegung, Heimfahrt und Schiffbau ist mein_bot.py. Kopiere es als Ausgangspunkt:

cp bots/mein_bot.py bots/test_bot.py
./start.sh gui --bots bots/test_bot.py collector --seed 42 --turns 240

Tipp: Ändere zunächst nur eine Entscheidung, etwa die Frachtgrenze für die Heimfahrt. Teste auf derselben Karte, schau dir das Replay an und vergleiche erst dann über mehrere Seeds und Startplätze. Im Fenster startet R dieselbe Karte mit frisch geladenem Code.

Lebenszyklus

Der Konstruktor muss das Schlüsselwort seed annehmen. Wer von BaseBot erbt, bekommt self.rng, einen NumPy-Zufallsgenerator aus diesem Seed. Nutze ihn für jeden Zufall, dann bleiben deine Partien reproduzierbar.

class Bot(BaseBot):
    name = "Mein Strategieexperiment"

    def __init__(self, seed=0):
        super().__init__(seed)
        self.heimfahrt = set()

Pro Partie und Startplatz entsteht eine neue Instanz. Attribute bleiben zwischen den Runden erhalten, zwischen Partien nicht. Belegt derselbe Bot mehrere Startplätze, laufen unabhängige Instanzen in eigenen Prozessen.

Die Beobachtung

Alle Flotten entscheiden auf demselben Zustand vor dem Zug. Kein Bot sieht die Befehle eines anderen.

Zugriff Bedeutung
observation.rules Regeln, z. B. size, turns, capacity, ship_cost, relay_cost
observation.turn Bisher gespielte Runden, beginnend bei 0
observation.resource[y, x] Lumen auf einem Feld – zuerst y, dann x
observation.ships Alle Schiffe aller Flotten
observation.players Alle Flotten in der Reihenfolge ihrer IDs
observation.sites Alle Werften und Relais
observation.fleet(player) Deine Schiffe
observation.harbors(player) Deine Ablieferstellen als (x, y)
observation.distance(a, b) Entfernung über den umlaufenden Kartenrand
observation.move_cost(ship) Bewegungskosten mit der Rundung des aktuellen Profils
observation.inspired(ship) Ob ein Schiff an seiner Position inspiriert ist
observation.destination(pos, action) Zielfeld einer Aktion
observation.toward(pos, target) Eine Richtung auf kürzestem Weg; O, wenn schon dort

Das Raster ist schreibgeschützt. Änderungen an den Daten in deinem Bot ändern das Spiel nicht.

Schiff (ShipInfo): id, owner, x, y, cargo, inspired und pos. Eine Schiff-ID bleibt das ganze Schiffsleben gleich und wird nie wiederverwendet.

Flotte (FleetInfo): id, name, bank (Lager), yard (Werft), deposited, mined, lost, built, spent, bonus, alive, last_turn_alive und rank.

  • mined ist die vom Raster entnommene Menge, einschließlich des Feldvorkommens bei einem Relaisbau.
  • bonus ist die zusätzlich durch Inspiration entstandene Fracht.
  • deposited zählt alle Ablieferungen, auch die Gutschrift beim Relaisbau.
  • spent zählt alle Ausgaben, einschließlich Treibstoff (der aus der Fracht bezahlt wird).
  • alive und rank zeigen Ausscheiden und aktuelle Rangfolge.

Hafen (Site): owner, x, y, kind (yard oder relay) und pos. Neue Schiffe entstehen nur an der Werft (yard).

Befehle

orders = Orders()
orders.ships[ship.id] = "E"
orders.spawn = True
return orders
Aktion Bedeutung
O Stehen bleiben und abbauen
N Nach Norden, y − 1
E Nach Osten, x + 1
S Nach Süden, y + 1
W Nach Westen, x − 1
B Schiff in ein eigenes Relais umwandeln

spawn = True beantragt ein neues Schiff. Fehlende Schiffsbefehle werden zu O. Befehle für fremde oder nicht vorhandene Schiffe werden ignoriert. Ein unbekannter Aktionsname gilt dagegen als Fehler und schaltet den Bot für den Rest der Partie ab.

Statt Orders darfst du auch ein Dictionary liefern:

return {"ships": {str(ship.id): "N"}, "spawn": False}

Verwende dabei normale Python-bool-Werte, keine NumPy-Werte.

Heimfahrt merken

Fährt ein Schiff ab 560 Fracht heim, kostet jeder Schritt Treibstoff. Prüfst du danach erneut nur cargo >= 560, bricht es die Heimfahrt ab. Merke dir deshalb, wer gerade heimfährt:

if ship.cargo >= 560:
    self.heimfahrt.add(ship.id)
if ship.pos in observation.harbors(player):
    self.heimfahrt.discard(ship.id)
if ship.id in self.heimfahrt:
    ...  # weiter zum Hafen

Denk auch an das Spielende: Volle Fracht weit weg vom Hafen zählt nicht. Ein neues Schiff lohnt sich nur, wenn es seine Kosten bis zum Ende wieder einsammeln kann.

Eigene Kollisionen vermeiden

observation.toward() kennt nur Entfernungen, keine anderen Schiffe. Der SDK-Koordinator plant deine ganze Flotte gemeinsam so, dass keine zwei eigenen Schiffe auf demselben Feld landen:

from kobaltmeer.sdk import coordinate

preferences = {}
for ship in observation.fleet(player):
    preferred = observation.toward(ship.pos, observation.players[player].yard)
    preferences[ship.id] = [preferred, "O", "N", "E", "S", "W"]

orders.ships = coordinate(observation, player, preferences)
  • Jedes Schiff bekommt eine Liste bevorzugter Aktionen; O ist immer der Notfall.
  • Platztausch und größere Kreisbewegungen sind erlaubt.
  • Schiffe ohne genug Treibstoff bleiben automatisch stehen.
  • Später eingetragene Schiffe haben Vorrang. Trage dringende Heimfahrer deshalb zuletzt ein.

Gegnerische Kollisionen kann der Koordinator nicht verhindern. Schiffe, die du mit B in ein Relais umwandelst, plane mit ["O"] ein und setze danach wieder B. So bleibt ihr Feld reserviert, falls der Bau nicht bezahlt werden kann.

Diagnose

  • print() landet im Debugprotokoll (stderr_tail in Berichten und Replays).
  • Ausnahmen erscheinen unter faults und als bot_error im Replay.
  • Ein hängender Bot wird nach dem Zeitlimit beendet; seine Flotte wartet danach.
  • timings_ms enthält die Summe deiner Entscheidungszeiten.
./start.sh play --bots bots/test_bot.py collector --seed 42 \
  --replay replays/fehleranalyse.json.gz --output runs/fehleranalyse.json
./start.sh gui --replay replays/fehleranalyse.json.gz

Ziele und Gründe aufzeichnen

Mit explain() hängst du an einen Befehl an, was ein Schiff vorhat und warum. Der Replay-Viewer zeigt das mit „Ziele & Gründe“ an. Das ist sehr hilfreich, um eigene Fehlentscheidungen zu finden.

orders.ships[ship.id] = "E"
orders.explain(ship.id, target=(12, 8), reason="Volle Fracht zum Hafen bringen",
               path=[ship.pos, observation.destination(ship.pos, "E")])

explain() erteilt keinen Befehl und ändert nichts am Spiel. Erlaubt sind nur eigene Schiffe; pro Runde werden höchstens 128 Einträge mit je 64 Pfadpunkten übernommen, Gründe werden auf 240 Zeichen gekürzt. Fehlerhafte Einträge werden verworfen, ohne den Bot abzuschalten.

Regelprofile berücksichtigen

Lies Kosten und Grenzen immer aus observation.rules und nutze observation.move_cost(ship) statt einer festen Rundung. Dann funktioniert dein Bot in beiden Profilen. Im Profil Classic gibt es kein kostenloses Startschiff: Dein Bot muss zuerst spawn = True beantragen.

Die Engine direkt verwenden

Die Engine braucht weder Fenster noch Botprozesse. Für eigene Experimente:

from kobaltmeer.engine import Game, Rules, Orders
from kobaltmeer.sdk import Observation

game = Game(Rules.for_profile("classic", players=1, seed=42, turns=240))
observation = Observation.from_snapshot(game.rules, game.snapshot())
game.step({0: Orders(spawn=True)})

Rules.for_profile("kobaltmeer" | "classic", **werte) lädt ein Profil und überschreibt einzelne Felder. Eine Lernumgebung mit reset() und step() beschreibt die Seite Training.

Replays selbst auswerten

Replays sind gzip-komprimiertes JSON. Lies sie mit load_replay(), das liefert für jede Runde den vollständigen Zustand:

from kobaltmeer.replay import load_replay

replay = load_replay("replays/erste-partie.json.gz")
for frame in replay["frames"]:
    print(frame["turn"], [p["bank"] for p in frame["players"]])

Jeder Frame enthält turn, resource, ships, players, sites, events sowie die angeforderten orders und aufgezeichneten debug-Einträge des Übergangs in diese Runde. In der Datei selbst speichert das Format kobaltmeer-replay-v2 die Ressourcen ab Runde 1 nur als Änderungen; load_replay() setzt sie wieder zusammen.

Andere Sprachen

Bots in C++, C#, Go, Rust oder JavaScript sprechen dasselbe Spiel über ein einfaches JSON-Protokoll. Siehe Andere Sprachen.