Irgendwelche Muster zum Modellieren von Brettspielen? [geschlossen]


93

Zum Spaß versuche ich, eines der Lieblingsbrettspiele meines Sohnes als Software zu schreiben. Irgendwann erwarte ich, eine WPF-Benutzeroberfläche darüber zu erstellen, aber im Moment baue ich die Maschine, die die Spiele und ihre Regeln modelliert.

Dabei sehe ich immer wieder Probleme, von denen ich denke, dass sie vielen Brettspielen gemeinsam sind, und vielleicht haben andere sie bereits besser gelöst als ich.

(Beachten Sie, dass KI für das Spielen des Spiels und Muster für hohe Leistung für mich nicht interessant sind.)

Bisher sind meine Muster:

  • Verschiedene unveränderliche Typen, die Entitäten in der Spielbox darstellen, z. B. Würfel, Steine, Karten, ein Brett, Felder auf dem Brett, Geld usw.

  • Ein Objekt für jeden Spieler, das die Ressourcen des Spielers (z. B. Geld, Punktzahl), seinen Namen usw. enthält.

  • Ein Objekt, das den Status des Spiels darstellt: die Spieler, wer an der Reihe ist, das Layout der Teile auf dem Brett usw.

  • Eine Zustandsmaschine, die die Abbiegefolge verwaltet. Zum Beispiel haben viele Spiele ein kleines Vorspiel, in dem jeder Spieler würfelt, um zu sehen, wer zuerst geht. Das ist der Startzustand. Wenn ein Spieler an der Reihe ist, würfelt er zuerst, bewegt sich dann, muss an Ort und Stelle tanzen, dann raten andere Spieler, um welche Hühnerrasse es sich handelt, und erhalten dann Punkte.

Gibt es einen Stand der Technik, den ich nutzen kann?

EDIT: Eine Sache, die ich kürzlich erkannt habe, ist, dass der Spielstatus in zwei Kategorien unterteilt werden kann:

  • Spielartefaktstatus . "Ich habe 10 Dollar" oder "Meine linke Hand ist blau".

  • Status der Spielsequenz . "Ich habe zweimal Doppel gewürfelt; der nächste bringt mich ins Gefängnis". Eine Zustandsmaschine kann hier sinnvoll sein.

EDIT: Was ich hier wirklich suche, ist der beste Weg, um rundenbasierte Multiplayer-Spiele wie Chess oder Scrabble oder Monopoly zu implementieren. Ich bin mir sicher, dass ich ein solches Spiel erstellen könnte, indem ich es von Anfang bis Ende durcharbeite, aber wie bei anderen Designmustern gibt es wahrscheinlich einige Möglichkeiten, die Dinge viel reibungsloser zu gestalten, die ohne sorgfältiges Studium nicht offensichtlich sind. Darauf hoffe ich.


3
Sie bauen eine Art Hokey Pokey, Monopoly, Charades Mashup?
Anthony Mastrean

Sie benötigen eine Zustandsmaschine für jede Regel, die auf dem Zustand beruht (äh ...), wie die Drei-Doppel-Regel für Monopoly. Ich würde eine ausführlichere Antwort posten, aber ich habe keine Erfahrung damit. Ich könnte es jedoch pontifizieren.
MSN

Antworten:


115

Es scheint, dass dies ein 2 Monate alter Thread ist, den ich gerade bemerkt habe, aber was solls. Ich habe zuvor das Gameplay-Framework für ein kommerzielles, vernetztes Brettspiel entworfen und entwickelt. Wir hatten eine sehr angenehme Erfahrung damit zu arbeiten.

Ihr Spiel kann sich wahrscheinlich in einer (nahezu) unendlichen Anzahl von Zuständen befinden, aufgrund der Permutationen von Dingen wie wie viel Geld Spieler A hat, wie viel Geld Spieler B hat und so weiter. Daher bin ich mir ziemlich sicher, dass Sie es wollen sich von Staatsmaschinen fernhalten.

Die Idee hinter unserem Framework war es, den Spielstatus als Struktur mit allen Datenfeldern darzustellen, die zusammen den vollständigen Spielstatus liefern (dh wenn Sie das Spiel auf der Festplatte speichern möchten, schreiben Sie diese Struktur aus).

Wir haben das Befehlsmuster verwendet , um alle gültigen Spielaktionen darzustellen, die ein Spieler ausführen kann. Hier wäre eine Beispielaktion:

class RollDice : public Action
{
  public:
  RollDice(int player);

  virtual void Apply(GameState& gameState) const; // Apply the action to the gamestate, modifying the gamestate
  virtual bool IsLegal(const GameState& gameState) const; // Returns true if this is a legal action
};

Um zu entscheiden, ob ein Zug gültig ist, können Sie diese Aktion erstellen und dann die IsLegal-Funktion aufrufen, die den aktuellen Spielstatus übergibt. Wenn es gültig ist und der Spieler die Aktion bestätigt, können Sie die Apply-Funktion aufrufen, um den Spielstatus tatsächlich zu ändern. Indem Sie sicherstellen, dass Ihr Gameplay-Code den Spielstatus nur durch Erstellen und Übermitteln legaler Aktionen ändern kann (mit anderen Worten, die Action :: Apply-Methodenfamilie ist das einzige, was den Spielstatus direkt ändert), stellen Sie sicher, dass Ihr Spiel Zustand wird niemals ungültig sein. Mithilfe des Befehlsmusters können Sie außerdem die gewünschten Züge Ihres Spielers serialisieren und über ein Netzwerk senden, um sie in den Spielzuständen anderer Spieler auszuführen.

Es gab ein Problem mit diesem System, das sich als ziemlich elegante Lösung herausstellte. Manchmal haben Aktionen zwei oder mehr Phasen. Zum Beispiel kann der Spieler auf einem Grundstück in Monopoly landen und muss nun eine neue Entscheidung treffen. Wie ist der Spielstatus zwischen dem Würfeln des Spielers und der Entscheidung, eine Immobilie zu kaufen oder nicht? Wir haben Situationen wie diese bewältigt, indem wir ein "Action Context" -Mitglied unseres Spielstatus vorgestellt haben. Der Aktionskontext ist normalerweise null, was darauf hinweist, dass sich das Spiel derzeit in keinem speziellen Zustand befindet. Wenn der Spieler würfelt und die Würfelwurfaktion auf den Spielstatus angewendet wird, erkennt er, dass der Spieler auf einem nicht im Besitz befindlichen Grundstück gelandet ist, und kann eine neue "PlayerDecideToPurchaseProperty" erstellen. Aktionskontext, der den Index des Spielers enthält, von dem wir auf eine Entscheidung warten. Bis die RollDice-Aktion abgeschlossen ist, zeigt unser Spielstatus an, dass er derzeit darauf wartet, dass der angegebene Spieler entscheidet, ob er eine Immobilie kauft oder nicht. Es ist jetzt für alle IsLegal-Methoden aller Aktionen einfach, false zurückzugeben, mit Ausnahme der Aktionen "BuyProperty" und "PassPropertyPurchaseOpportunity", die nur zulässig sind, wenn der Spielstatus den Aktionskontext "PlayerDecideToPurchaseProperty" enthält.

Durch die Verwendung von Aktionskontexten gibt es nie einen einzigen Punkt in der Lebensdauer des Brettspiels, an dem die Struktur des Spielstatus nicht genau das darstellt, was zu diesem Zeitpunkt im Spiel passiert. Dies ist eine sehr wünschenswerte Eigenschaft Ihres Brettspielsystems. Es wird für Sie viel einfacher sein, Code zu schreiben, wenn Sie alles finden, was Sie jemals über das Geschehen im Spiel wissen möchten, indem Sie nur eine Struktur untersuchen.

Darüber hinaus erstreckt es sich sehr gut auf Netzwerkumgebungen, in denen Clients ihre Aktionen über ein Netzwerk an einen Host-Computer senden können, der die Aktion auf den "offiziellen" Spielstatus des Hosts anwenden und diese Aktion dann an alle anderen Clients zurücksenden kann Lassen Sie sie es auf ihre replizierten Spielzustände anwenden.

Ich hoffe das war prägnant und hilfreich.


4
Ich denke nicht, dass es prägnant ist, aber es ist hilfreich! Upvoted.
Jay Bazuzi

Ich bin froh, dass es hilfreich war ... Welche Teile waren nicht prägnant? Ich würde gerne eine Klarstellung bearbeiten.
Andrew Top

Ich baue gerade ein rundenbasiertes Spiel und dieser Beitrag war wirklich hilfreich!
Kiv

Ich habe gelesen, dass Memento das Muster ist, das zum Rückgängigmachen verwendet werden soll ... Memento vs Command Pattern zum Rückgängigmachen, Ihre Gedanken plz ..
zotherstupidguy

Dies ist die beste Antwort, die ich bisher in Stackoverflow gelesen habe. VIELEN DANK!
Papipo

18

Die Grundstruktur Ihrer Spiel-Engine verwendet das Statusmuster . Die Gegenstände Ihrer Spielbox sind Singletons verschiedener Klassen. Die Struktur jedes Staates kann ein Strategiemuster oder die Vorlagenmethode verwenden .

Eine Factory wird verwendet, um die Spieler zu erstellen, die in eine Liste von Spielern eingefügt werden, ein weiterer Singleton. Die GUI überwacht die Game Engine mithilfe des Observer-Musters und interagiert mit diesem mithilfe eines von mehreren Befehlsobjekten, die mithilfe des Befehlsmusters erstellt wurden . Die Verwendung von Observer und Command kann im Kontext einer passiven Ansicht verwendet werden. Abhängig von Ihren Einstellungen kann jedoch nahezu jedes MVP / MVC-Muster verwendet werden. Wenn Sie das Spiel speichern, müssen Sie ein Andenken an den aktuellen Status abrufen

Ich empfehle, einige der Muster auf dieser Website zu überprüfen und zu prüfen, ob Sie von diesen als Ausgangspunkt herangezogen werden. Wieder wird das Herz Ihres Spielbretts eine Zustandsmaschine sein. Die meisten Spiele werden durch zwei Zustände vor dem Spiel / Setup und das eigentliche Spiel dargestellt. Sie können jedoch mehr Status haben, wenn das Spiel, das Sie modellieren, mehrere unterschiedliche Spielmodi hat. Staaten müssen nicht sequentiell sein, zum Beispiel hat das Wargame Axis & Battles ein Battle Board, mit dem die Spieler Schlachten lösen können. Es gibt also drei Zustände vor dem Spiel, Hauptbrett, Schlachtbrett, wobei das Spiel ständig zwischen Hauptbrett und Schlachtbrett wechselt. Natürlich kann die Drehfolge auch durch eine Zustandsmaschine dargestellt werden.


15

Ich habe gerade ein zustandsbasiertes Spiel mit Polymorphismus entworfen und implementiert.

Die Verwendung einer abstrakten Basisklasse mit dem Namen GamePhasehat eine wichtige Methode

abstract public GamePhase turn();

Dies bedeutet, dass jedes GamePhaseObjekt den aktuellen Status des Spiels enthält und ein Aufruf, turn()den aktuellen Status zu überprüfen und den nächsten zurückzugeben GamePhase.

Jeder Beton GamePhasehat Konstruktoren, die den gesamten Spielzustand halten . In jeder turn()Methode sind einige Spielregeln enthalten. Während dies die Regeln verteilt, werden verwandte Regeln eng zusammengehalten. Das Endergebnis von jedem turn()ist nur das Erstellen des nächsten GamePhaseund das Übergehen im vollen Zustand in die nächste Phase.

Dies ermöglicht turn()es, sehr flexibel zu sein. Abhängig von Ihrem Spiel kann ein bestimmter Status in viele verschiedene Arten von Phasen verzweigen. Dies bildet eine grafische Darstellung aller Spielphasen.

Auf der höchsten Ebene ist der Code zum Fahren sehr einfach:

GamePhase state = ...initial phase
while(true) {
    // read the state, do some ui work
    state = state.turn();
}

Dies ist äußerst nützlich, da ich jetzt problemlos jeden Status / jede Phase des Spiels zum Testen erstellen kann

Um nun den zweiten Teil Ihrer Frage zu beantworten: Wie funktioniert das im Mehrspielermodus? Innerhalb bestimmt GamePhases , die eine Benutzereingabe erfordern, von einem Anruf turn()fragen würde die aktuelle Playersie Strategyangesichts der aktuelle Zustand / Phase. Strategyist nur eine Schnittstelle aller möglichen Entscheidungen, die Playerman treffen kann. Dieses Setup ermöglicht auch die StrategyImplementierung mit AI!

Auch Andrew Top sagte:

Ihr Spiel kann sich wahrscheinlich in einer (nahezu) unendlichen Anzahl von Zuständen befinden, aufgrund der Permutationen von Dingen wie wie viel Geld Spieler A hat, wie viel Geld Spieler B hat und so weiter. Daher bin ich mir ziemlich sicher, dass Sie es wollen sich von Staatsmaschinen fernhalten.

Ich denke, diese Aussage ist sehr irreführend, obwohl es viele verschiedene Spielzustände gibt, gibt es nur wenige Spielphasen. Um sein Beispiel zu behandeln, wäre alles, was es ist, ein ganzzahliger Parameter für die Konstruktoren meiner konkreten GamePhases.

Monopol

Ein Beispiel für einige GamePhases wäre:

  • GameStarts
  • PlayerRolls
  • PlayerLandsOnProperty (FreeParking, GoToJail, Go usw.)
  • PlayerTrades
  • PlayerPurchasesProperty
  • PlayerPurchasesHouses
  • PlayerPurchasesHotels
  • PlayerPaysRent
  • PlayerBankrupts
  • (Alle Zufalls- und Community-Truhenkarten)

Und einige Staaten in der Basis GamePhasesind:

  • Liste der Spieler
  • Aktueller Spieler (wer ist an der Reihe)
  • Geld / Eigentum des Spielers
  • Häuser / Hotels auf Immobilien
  • Spielerposition

Und dann würden einige Phasen nach Bedarf ihren eigenen Status aufzeichnen, zum Beispiel würde PlayerRolls aufzeichnen, wie oft ein Spieler aufeinanderfolgende Doppel gewürfelt hat. Sobald wir die PlayerRolls-Phase verlassen haben, kümmern wir uns nicht mehr um aufeinanderfolgende Würfe.

Viele Phasen können wiederverwendet und miteinander verknüpft werden. Zum Beispiel GamePhase CommunityChestAdvanceToGowürde die nächste Phase PlayerLandsOnGomit dem aktuellen Status erstellt und zurückgegeben. Im Konstruktor des PlayerLandsOnGoaktuellen Spielers würde nach Go verschoben und sein Geld würde um 200 $ erhöht.


8

Natürlich gibt es viele, viele, viele, viele, viele, viele, viele Ressourcen zu diesem Thema. Aber ich denke, Sie sind auf dem richtigen Weg, die Objekte aufzuteilen und sie ihre eigenen Ereignisse / Daten und so weiter verarbeiten zu lassen.

Wenn Sie Brettspiele auf Kachelbasis ausführen, ist es hilfreich, Routinen zu haben, die neben anderen Funktionen zwischen dem Brettarray und Zeile / Spalte und zurück abgebildet werden können. Ich erinnere mich an mein erstes Brettspiel (vor langer, langer Zeit), als ich Schwierigkeiten hatte, Row / Col aus Boardarray 5 zu bekommen.

1  2  3  
4 (5) 6  BoardArray 5 = row 2, col 2
7  8  9  

Nostalgie. ;)

Auf jeden Fall ist http://www.gamedev.net/ ein guter Ort für Informationen. http://www.gamedev.net/reference/


Warum verwenden Sie nicht einfach ein zweidimensionales Array? Dann kann der Compiler dies für Sie erledigen.
Jay Bazuzi

Meine Entschuldigung ist, dass dies vor langer, langer Zeit war. ;)
Stefan

1
Gamedev hat eine Menge Zeug, aber ich habe nicht genau gesehen, wonach ich gesucht habe.
Jay Bazuzi

Welche Sprache hast du benutzt?
zotherstupidguy

Basic, Basica, QB, QuickBasic und so weiter. ;)
Stefan


3

Three Rings bietet LGPL-Java-Bibliotheken. Nenya und Vilya sind die Bibliotheken für spielbezogene Dinge.

Natürlich wäre es hilfreich, wenn in Ihrer Frage Plattform- und / oder Sprachbeschränkungen erwähnt würden.


"Irgendwann erwarte ich, eine WPF-Benutzeroberfläche zu erstellen" - das bedeutet .NET. Zumindest soweit ich das beurteilen kann.
Mark Allen

Alphabetsuppe ist mir nicht bekannt.
jmucchiello

Ja, ich mache .NET, aber meine Frage ist nicht sprach- oder plattformspezifisch.
Jay Bazuzi

2

Ich stimme der Antwort von Pyrolistical zu und ich bevorzuge seine Art, Dinge zu tun (ich habe jedoch nur die anderen Antworten überflogen).

Zufälligerweise habe ich auch seine "GamePhase" -Namen verwendet. Grundsätzlich würde ich im Fall eines rundenbasierten Brettspiels tun, dass Ihre GameState-Klasse ein Objekt der abstrakten GamePhase enthält, wie von Pyrolistical erwähnt.

Nehmen wir an, die Spielzustände sind:

  1. Rollen
  2. Bewegung
  3. Kaufen / nicht kaufen
  4. Gefängnis

Sie könnten konkrete abgeleitete Klassen für jeden Zustand haben. Haben Sie virtuelle Funktionen mindestens für:

StartPhase();
EndPhase();
Action();

In der Funktion StartPhase () können Sie alle Anfangswerte für einen Status festlegen, z. B. die Eingabe des anderen Players deaktivieren und so weiter.

Wenn roll.EndPhase () aufgerufen wird, stellen Sie sicher, dass der GamePhase-Zeiger auf den nächsten Status gesetzt wird.

phase = new MovePhase();
phase.StartPhase();

In dieser MovePhase :: StartPhase () würden Sie beispielsweise die verbleibenden Züge des aktiven Spielers auf den Betrag setzen, der in der vorherigen Phase gewürfelt wurde.

Mit diesem Design können Sie nun Ihr Problem "3 x double = jail" innerhalb der Roll-Phase lösen. Die RollPhase-Klasse kann ihren eigenen Status verarbeiten. Beispielsweise

GameState state; //Set in constructor.
Die die;         // Only relevant to the roll phase.
int doublesRemainingBeforeJail;
StartPhase()
{
    die = new Die();
    doublesRemainingBeforeJail = 3;
}

Action()
{
    if(doublesRemainingBeforeJail<=0)
    {
       state.phase = new JailPhase(); // JailPhase::StartPhase(){set moves to 0};            
       state.phase.StartPhase();
       return;
    }

    int die1 = die.Roll();
    int die2 = die.Roll();

    if(die1 == die2)
    {
       --doublesRemainingBeforeJail;
       state.activePlayer.AddMovesRemaining(die1 + die2);
       Action(); //Roll again.
    }

    state.activePlayer.AddMovesRemaining(die1 + die2);
    this.EndPhase(); // Continue to moving phase. Player has X moves remaining.
}

Ich unterscheide mich von Pyrolistical darin, dass es für alles eine Phase geben sollte, auch wenn der Spieler auf der Community-Truhe landet oder so. Ich würde das alles in der MovePhase erledigen. Dies liegt daran, dass sich der Spieler sehr wahrscheinlich zu "geführt" fühlt, wenn Sie zu viele aufeinanderfolgende Phasen haben. Wenn es zum Beispiel eine Phase gibt, in der der Spieler NUR Immobilien kaufen und dann NUR Hotels kaufen und dann NUR Häuser kaufen kann, gibt es keine Freiheit. Schlagen Sie einfach alle diese Teile in eine BuyPhase und geben Sie dem Spieler die Freiheit, alles zu kaufen, was er will. Die BuyPhase-Klasse kann leicht genug damit umgehen, welche Käufe legal sind.

Lassen Sie uns zum Schluss das Spielbrett ansprechen. Obwohl ein 2D-Array in Ordnung ist, würde ich empfehlen, ein Kacheldiagramm zu haben (wobei eine Kachel eine Position auf dem Brett ist). Im Falle eines Monopols wäre es eher eine doppelt verknüpfte Liste. Dann hätte jede Fliese ein:

  1. previousTile
  2. nextTile

Es wäre also viel einfacher, etwas zu tun wie:

While(movesRemaining>0)
  AdvanceTo(currentTile.nextTile);

Die AdvanceTo-Funktion kann Ihre schrittweisen Animationen oder was auch immer Sie möchten verarbeiten. Und natürlich auch die verbleibenden Züge verringern.

Der Rat von RS Conley zum Beobachtermuster für die GUI ist gut.

Ich habe noch nicht viel gepostet. Hoffe das hilft jemandem.


1

Gibt es einen Stand der Technik, den ich nutzen kann?

Wenn Ihre Frage nicht sprach- oder plattformspezifisch ist. dann würde ich empfehlen, dass Sie AOP-Muster für State, Memento, Command usw. berücksichtigen.

Was ist die .NET Antwort auf AOP ???

Versuchen Sie auch, einige coole Websites wie http://www.chessbin.com zu finden

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.