Design eines rundenbasierten Spiels, in dem Aktionen Nebenwirkungen haben

19

Ich schreibe eine Computerversion des Spiels Dominion . Es ist ein rundenbasiertes Kartenspiel, bei dem Aktionskarten, Schatzkarten und Siegpunktkarten im persönlichen Deck eines Spielers gesammelt werden. Ich habe die Klassenstruktur ziemlich gut entwickelt und beginne, die Spiellogik zu entwerfen. Ich verwende Python und kann später eine einfache Benutzeroberfläche mit Pygame hinzufügen.

Die Spielreihenfolge der Spieler wird von einem sehr einfachen Automaten gesteuert. Wendet sich im Uhrzeigersinn und ein Spieler kann das Spiel nicht beenden, bevor es vorbei ist. Das Spiel einer einzelnen Runde ist auch eine Zustandsmaschine; Im Allgemeinen durchlaufen die Spieler eine "Aktionsphase", eine "Kaufphase" und eine "Aufräumphase" (in dieser Reihenfolge). Basierend auf der Antwort auf die Frage Wie implementiert man eine rundenbasierte Spiel-Engine? Die Zustandsmaschine ist eine Standardtechnik für diese Situation.

Mein Problem ist, dass sie während der Aktionsphase einer Spielerin eine Aktionskarte verwenden kann, die Nebenwirkungen auf sich selbst oder auf einen oder mehrere der anderen Spieler hat. Beispielsweise kann ein Spieler mit einer Aktionskarte unmittelbar nach Abschluss des aktuellen Zuges einen zweiten Zug ausführen. Eine andere Aktionskarte bewirkt, dass alle anderen Spieler zwei Karten aus ihren Händen ablegen. Eine weitere Aktionskarte bewirkt in der aktuellen Runde nichts, erlaubt es einem Spieler jedoch, in ihrer nächsten Runde zusätzliche Karten zu ziehen. Um die Dinge noch komplizierter zu machen, gibt es häufig neue Erweiterungen des Spiels, die neue Karten hinzufügen. Es scheint mir, dass eine harte Codierung der Ergebnisse jeder Aktionskarte in die Zustandsmaschine des Spiels sowohl hässlich als auch nicht anpassbar wäre. Die Antwort auf die rundenbasierte Strategie-Schleife geht nicht auf eine Detailebene ein, die Entwürfe zur Lösung dieses Problems anspricht.

Welche Art von Programmiermodell sollte ich verwenden, um die Tatsache zu berücksichtigen, dass das allgemeine Muster für das Abwechseln durch Aktionen geändert werden kann, die innerhalb der Abwechslung stattfinden? Sollte das Spielobjekt die Auswirkungen jeder Aktionskarte verfolgen? Oder, wenn die Karten ihre eigenen Effekte implementieren sollen (z. B. durch Implementieren einer Schnittstelle), welches Setup ist erforderlich, um ihnen genügend Leistung zu geben? Ich habe mir ein paar Lösungen für dieses Problem ausgedacht, aber ich frage mich, ob es einen Standardweg gibt, um es zu lösen. Insbesondere würde ich gerne wissen, welches Objekt / welche Klasse / was auch immer dafür verantwortlich ist, die Aktionen zu verfolgen, die jeder Spieler als Folge einer gespielten Aktionskarte ausführen muss, und auch, wie sich dies auf vorübergehende Änderungen in der normalen Reihenfolge von auswirkt die Turn-State-Maschine.

Apis Utilis
quelle
2
Hallo Apis Utilis, und willkommen auf der GDSE. Ihre Frage ist gut geschrieben und es ist großartig, dass Sie auf die zugehörigen Fragen verwiesen haben. Ihre Frage deckt jedoch viele verschiedene Probleme ab, und um sie vollständig abzudecken, müsste eine Frage wahrscheinlich riesig sein. Möglicherweise erhalten Sie immer noch eine gute Antwort, aber Sie selbst und die Site werden davon profitieren, wenn Sie Ihr Problem noch weiter lösen. Vielleicht mit einem einfacheren Spiel beginnen und auf Dominion aufbauen?
michael.bartnett
1
Ich fange damit an, jeder Karte ein Skript zu geben, das den Spielstatus ändert, und wenn nichts Unheimliches passiert, greife auf die Standard-Spielregeln zurück ...
Jari Komppa

Antworten:

11

Ich stimme Jari Komppa zu, dass das Definieren von Karteneffekten mit einer leistungsstarken Skriptsprache der richtige Weg ist. Ich bin jedoch der Meinung, dass der Schlüssel zu maximaler Flexibilität die Skript-fähige Ereignisbehandlung ist.

Damit Karten mit späteren Spielereignissen interagieren können, können Sie eine Skript-API hinzufügen, um bestimmten Ereignissen, z. B. dem Beginn und dem Ende von Spielphasen oder bestimmten Aktionen, die die Spieler ausführen können, "Skript-Hooks" hinzuzufügen. Das heißt, das Skript, das beim Ausspielen einer Karte ausgeführt wird, kann eine Funktion registrieren, die beim nächsten Erreichen einer bestimmten Phase aufgerufen wird. Die Anzahl der Funktionen, die für jede Veranstaltung registriert werden können, sollte unbegrenzt sein. Wenn es mehr als eine gibt, werden sie in der Reihenfolge ihrer Registrierung aufgerufen (es sei denn, es gibt natürlich eine Kernspielregel, die etwas anderes besagt).

Es sollte möglich sein, diese Haken für alle Spieler oder nur für bestimmte Spieler zu registrieren. Ich würde auch vorschlagen, die Möglichkeit für Hooks hinzuzufügen, um selbst zu entscheiden, ob sie weiter angerufen werden sollen oder nicht. In diesen Beispielen wird der Rückgabewert der Hook-Funktion (true oder false) verwendet, um dies auszudrücken.

Ihre Double-Turn-Karte würde dann ungefähr so ​​aussehen:

add_event_hook('cleanup_phase_end', current_player, function {
     setNextPlayer(current_player); // make the player take another turn
     return false; // unregister this hook afterwards
});

(Ich habe keine Ahnung, ob Dominion überhaupt so etwas wie eine "Aufräumphase" hat - in diesem Beispiel ist es die hypothetische letzte Phase, in der die Spieler an der Reihe sind.)

Eine Karte, mit der jeder Spieler zu Beginn seiner Ziehphase eine weitere Karte ziehen kann, sieht folgendermaßen aus:

add_event_hook('draw_phase_begin', NULL, function {
    drawCard(current_player); // draw a card
    return true; // keep doing this until the hook is removed explicitely
});

Eine Karte, die den Zielspieler dazu bringt, einen Trefferpunkt zu verlieren, wenn er eine Karte ausspielt, sieht folgendermaßen aus:

add_event_hook('play_card', target_player, function {
    changeHitPoints(target_player, -1); // remove a hit point
    return true; 
});

Sie werden nicht darum herumkommen, einige Spielaktionen wie das Ziehen von Karten oder das Verlieren von Trefferpunkten hart zu codieren, da ihre vollständige Definition - was genau es bedeutet, eine Karte zu ziehen - Teil der Kernmechanik des Spiels ist. Ich kenne zum Beispiel einige TCGs, bei denen du das Spiel verlierst, wenn du aus irgendeinem Grund eine Karte ziehen musst und dein Deck leer ist. Diese Regel ist nicht auf jeder Karte aufgedruckt, mit der Sie Karten ziehen können, da sie im Regelbuch enthalten ist. Sie sollten also auch nicht bei jedem Kartenskript nach diesem Verlustzustand suchen müssen. Solche Überprüfungen sollten Teil der fest programmierten drawCard()Funktion sein (was übrigens auch ein guter Kandidat für ein Hook-fähiges Ereignis wäre).

Übrigens: Es ist unwahrscheinlich, dass Sie in der Lage sein werden, vorausschauend zu planen , was in zukünftigen Ausgaben im Dunkeln liegt. Unabhängig davon, was Sie tun, müssen Sie von Zeit zu Zeit neue Funktionen für zukünftige Ausgaben hinzufügen (in diesem Fall) Fall ein Konfetti werfendes Minispiel).

Philipp
quelle
1
Wow. Das Chaos-Konfetti-Ding.
Jari Komppa
Ausgezeichnete Antwort, @Philipp, und dies erledigt eine Menge Dinge, die in Dominion erledigt werden. Es gibt jedoch Aktionen, die sofort beim Ausspielen einer Karte ausgeführt werden müssen, dh, eine Karte zwingt einen anderen Spieler, die oberste Karte seiner Bibliothek umzudrehen und dem aktuellen Spieler zu erlauben, "Keep it" oder "Discard it" zu sagen. Würden Sie Ereignis-Hooks schreiben, um sich um solche Sofortaktionen zu kümmern, oder müssten Sie sich zusätzliche Methoden für die Skripterstellung der Karten ausdenken?
10.
2
Wenn etwas sofort geschehen muss, sollte das Skript die entsprechenden Funktionen direkt aufrufen und keine Hook-Funktion registrieren.
Philipp
@JariKomppa: Das ungeklebte Set war absichtlich unsinnig und voller verrückter Karten, die keinen Sinn machten. Mein Favorit war eine Karte, mit der jeder einen Punkt Schaden nimmt, wenn er ein bestimmtes Wort sagt. Ich habe 'the' gewählt.
Jack Aidley
9

Ich habe dieses Problem - die flexible computergesteuerte Kartenspiel-Engine - vor einiger Zeit überlegt.

Zunächst einmal würde ein komplexes Kartenspiel wie Chez Geek oder Fluxx (und, glaube ich, Dominion) erfordern, dass Karten skriptfähig sind. Grundsätzlich wird jede Karte mit einer Reihe von Skripten geliefert, die den Status des Spiels auf verschiedene Weise ändern können. Auf diese Weise können Sie das System zukunftssicher machen, da die Skripte möglicherweise Dinge ausführen können, an die Sie derzeit nicht denken können, die aber in Zukunft möglicherweise erweitert werden.

Zweitens kann die starre "Drehung" Probleme verursachen.

Sie benötigen eine Art "Rundenstapel", der die "Spezialrunden" enthält, z. B. "2 Karten abwerfen". Wenn der Stapel leer ist, wird der normale Standardzug fortgesetzt.

In Fluxx ist es durchaus möglich, dass eine Runde ungefähr so ​​abläuft:

  • Wähle N Karten (wie in den aktuellen Regeln festgelegt, über Karten veränderbar)
  • N Karten spielen (wie in den aktuellen Regeln festgelegt, über Karten änderbar)
    • Eine der Karten kann "3 nehmen, 2 davon spielen" sein.
      • Eine dieser Karten könnte durchaus "eine weitere Runde machen" sein.
    • Eine der Karten kann "Ablegen und Ziehen" sein
  • Wenn Sie die Regeln ändern, um mehr Karten als zu Beginn Ihres Zuges zu wählen, wählen Sie mehr Karten
  • Wenn Sie die Regeln für weniger Karten auf der Hand ändern, müssen alle anderen Karten sofort abwerfen
  • Wenn dein Spielzug endet, wirf Karten ab, bis du N Karten hast (wieder durch Karten änderbar), und mache dann einen weiteren Spielzug (falls du irgendwann im oben genannten Durcheinander "mache einen weiteren Spielzug" gespielt hast).

..und so weiter und so fort. Das Entwerfen einer Turn-Struktur, die den oben genannten Missbrauch bewältigen kann, kann daher recht schwierig sein. Hinzu kommen die zahlreichen Spiele mit "wann" -Karten (wie in "chez geek"), bei denen die "wann" -Karten den normalen Kartenfluss stören können, indem zum Beispiel die zuletzt gespielte Karte storniert wird.

Im Grunde genommen würde ich damit beginnen, eine sehr flexible Rundenstruktur zu entwerfen, die so gestaltet ist, dass sie als Skript beschrieben werden kann (da jedes Spiel ein eigenes "Masterskript" benötigt, das die grundlegende Spielstruktur handhabt). Dann sollte jede Karte skriptfähig sein. Die meisten Karten machen wahrscheinlich nichts Seltsames, andere dagegen schon. Karten können auch verschiedene Attribute haben - ob sie in der Hand gehalten, "wann immer" gespielt werden können, ob sie als Gut aufbewahrt werden können (wie Fluxx "Keeper" oder verschiedene Dinge in "Chez Geek" wie Essen) ...

Ich habe nie damit begonnen, etwas davon zu implementieren. In der Praxis gibt es also möglicherweise viele andere Herausforderungen. Der einfachste Weg wäre, mit dem System zu beginnen, das Sie implementieren möchten, und es auf skriptfähige Weise zu implementieren, wobei so wenig wie möglich in Stein gemeißelt wird. Wenn also eine Erweiterung folgt, müssen Sie keine Änderungen vornehmen das Basissystem - viel. =)

Jari Komppa
quelle
Dies ist eine großartige Antwort, und ich hätte beides akzeptiert, wenn ich hätte. Ich habe die Krawatte abgebrochen, indem ich die Antwort der Person mit dem niedrigeren Ruf akzeptiert habe :)
Apis Utilis
Kein Problem, ich bin mittlerweile daran gewöhnt. =)
Jari Komppa
0

Hearthstone scheint Dinge zu tun, die in Beziehung stehen, und ich denke, der beste Weg, um Flexibilität zu erreichen, ist eine ECS-Engine mit datenorientiertem Design. Ich habe versucht, einen Hearthstone-Klon zu erstellen, und es hat sich als unmöglich erwiesen. Alle Randfälle. Wenn Sie mit vielen dieser seltsamen Fälle konfrontiert sind, ist dies wahrscheinlich der beste Weg, dies zu tun. Ich bin allerdings ziemlich voreingenommen, weil ich diese Technik ausprobiert habe.

Bearbeiten: ECS wird je nach gewünschter Flexibilität und Optimierung möglicherweise gar nicht benötigt. Es ist nur eine Möglichkeit, dies zu erreichen. DOD habe ich fälschlicherweise als prozedurale Programmierung angesehen, obwohl sie viel miteinander zu tun haben. Was ich meine ist. Das sollten Sie in Betracht ziehen, zumindest OOP ganz oder größtenteils abzuschaffen und stattdessen Ihre Aufmerksamkeit auf die Daten und deren Organisation zu lenken. Vermeiden Sie Vererbung und Methoden. Konzentrieren Sie sich stattdessen auf öffentliche Funktionen (Systeme), um Ihre Kartendaten zu manipulieren. Jede Aktion ist keine Vorlage oder Logik, sondern nur Rohdaten. Wo Ihre Systeme es dann verwenden, um Logik auszuführen. Ganzzahlschalter oder die Verwendung einer Ganzzahl für den Zugriff auf ein Array von Funktionszeigern helfen dabei, die gewünschte Logik effizient aus den Eingabedaten zu ermitteln.

Grundregeln, die zu befolgen sind, sind, dass Sie vermeiden sollten, Logik direkt mit Daten zu verknüpfen, dass Sie vermeiden sollten, Daten so weit wie möglich voneinander abhängig zu machen (möglicherweise gelten Ausnahmen), und dass, wenn Sie flexible Logik wünschen, die sich nicht erreichbar anfühlt ... Überlegen Sie, ob Sie es in Daten konvertieren möchten.

Das hat Vorteile. Jede Karte kann eine (n) Aufzählung (en) oder eine (n) Zeichenfolge (n) haben, um ihre (n) Aktion (en) darzustellen. Mit diesem Praktikanten können Sie Karten anhand von Text- oder JSON-Dateien entwerfen und vom Programm automatisch importieren lassen. Wenn Sie Spieleraktionen zu einer Datenliste machen, ist dies noch flexibler, insbesondere, wenn eine Karte wie Hearthstone von früherer Logik abhängt oder wenn Sie das Spiel oder eine Wiederholung eines Spiels zu einem beliebigen Zeitpunkt speichern möchten. Es gibt das Potenzial, KI einfacher zu erstellen. Vor allem bei der Verwendung eines "Utility Systems" anstelle eines "Behaviour Tree". Die Vernetzung wird auch einfacher, da nicht mehr herausgefunden werden muss, wie ganze möglicherweise polymorphe Objekte über die Leitung übertragen werden und wie die Serialisierung nachträglich eingerichtet wird. Ihre bereits vorhandenen Spielobjekte sind nichts anderes als einfache Daten, die sich wirklich leicht bewegen lassen. Und nicht zuletzt können Sie auf diese Weise einfacher optimieren, denn anstatt Zeit mit Code zu verschwenden, können Sie Ihre Daten besser organisieren, damit der Prozessor leichter damit umgehen kann. Python kann hier Probleme haben, aber schauen Sie nach "Cache-Zeile" und wie es sich auf Spielentwickler bezieht. Vielleicht nicht wichtig für das Prototyping von Sachen, aber später wird es sich als nützlich erweisen.

Einige nützliche Links.

Hinweis: Mit ECS können Variablen (so genannte Komponenten) zur Laufzeit dynamisch hinzugefügt / entfernt werden. Ein Beispiel c Programm, wie ECS "aussehen" könnte (es gibt eine Menge Möglichkeiten, dies zu tun).

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

Erstellt eine Reihe von Objekten mit Textur- und Positionsdaten und zerstört am Ende ein Objekt mit einer Texturkomponente, die sich zufällig am dritten Index des Texturkomponenten-Arrays befindet. Sieht schrullig aus, ist aber eine Möglichkeit, Dinge zu tun. Hier ist ein Beispiel, wie Sie alles rendern würden, was eine Texturkomponente enthält.

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}
Blue_Pyro
quelle
1
Diese Antwort wäre besser, wenn Sie näher erläutern würden, wie Sie die Einrichtung Ihres datenorientierten ECS empfehlen und es anwenden, um dieses spezielle Problem zu lösen.
DMGregory
Aktualisiert, danke, dass Sie darauf hingewiesen haben.
Blue_Pyro
Im Allgemeinen finde ich es schlecht, jemandem zu sagen, wie man einen solchen Ansatz einrichtet, sondern ihn seine eigene Lösung entwerfen zu lassen. Es erweist sich als eine gute Übungsmethode und ermöglicht eine potenziell bessere Lösung des Problems. Wenn man auf diese Weise mehr über Daten als über Logik nachdenkt, gibt es viele Möglichkeiten, dasselbe zu erreichen, und alles hängt von den Anforderungen der Anwendung ab. Sowie Programmiererzeit / Kenntnisse.
Blue_Pyro