Jakieś wzorce do modelowania gier planszowych? [Zamknięte]

94

Dla zabawy próbuję napisać jedną z ulubionych gier planszowych mojego syna jako oprogramowanie. W końcu spodziewam się zbudowania na nim interfejsu użytkownika WPF, ale teraz buduję maszynę, która modeluje gry i jej zasady.

Robiąc to, ciągle widzę problemy, które moim zdaniem są wspólne dla wielu gier planszowych, a być może inni rozwiązali je już lepiej niż ja.

(Zauważ, że sztuczna inteligencja do grania w grę i wzorce wokół wysokiej wydajności nie są dla mnie interesujące).

Jak dotąd moje wzorce to:

  • Kilka niezmiennych typów reprezentujących elementy w pudełku gry, np. Kości, warcaby, karty, plansza, pola na planszy, pieniądze itp.

  • Obiekt dla każdego gracza, który zawiera zasoby gracza (np. Pieniądze, punkty), jego nazwę itp.

  • Obiekt, który reprezentuje stan gry: gracze, kto jest w kolejce, układ elementów na planszy itp.

  • Maszyna stanowa zarządzająca sekwencją tur. Na przykład wiele gier ma małą przedmeczę, w której każdy gracz rzuca, aby zobaczyć, kto pierwszy; to jest stan początkowy. Kiedy zaczyna się tura gracza, najpierw rzuca, potem się porusza, potem musi tańczyć w miejscu, potem inni gracze odgadują, jakiego rodzaju kurczaków jest, a potem otrzymują punkty.

Czy jest jakiś stan wiedzy, z którego mogę skorzystać?

EDYCJA: Ostatnio zdałem sobie sprawę, że stan gry można podzielić na dwie kategorie:

  • Stan artefaktów w grze . „Mam 10 dolarów” lub „Moja lewa ręka jest na niebiesko”.

  • Stan sekwencji gry . „Dwa razy wyrzuciłem dublety; za następnym trafiam do więzienia”. Maszyna stanów może mieć tutaj sens.

EDYCJA: To, czego naprawdę szukam, to najlepszy sposób na zaimplementowanie gier turowych dla wielu graczy, takich jak Chess, Scrabble lub Monopoly. Jestem pewien, że mógłbym stworzyć taką grę, po prostu pracując nad nią od początku do końca, ale, podobnie jak inne wzorce projektowe, prawdopodobnie istnieją sposoby, aby wszystko przebiegało znacznie płynniej, które nie są oczywiste bez dokładnego zbadania. Na to mam nadzieję.

Jay Bazuzi
źródło
3
Tworzysz coś w rodzaju Hokey Pokey, Monopoly, mashup szarady?
Anthony Mastrean
Będziesz potrzebował maszyny stanowej dla każdej reguły, która opiera się na stanie (błąd ...), jak reguła trzech podwójnych reguł w Monopoly. Opublikowałbym pełniejszą odpowiedź, ale nie mam w tym doświadczenia. Mógłbym jednak o tym pontyfikować.
MSN

Odpowiedzi:

116

wygląda na to, że to wątek sprzed 2 miesięcy, który właśnie zauważyłem, ale co do cholery. Wcześniej zaprojektowałem i opracowałem ramy rozgrywki dla komercyjnej, sieciowej gry planszowej. Praca z nim była bardzo przyjemna.

Twoja gra może prawdopodobnie znajdować się w (prawie) nieskończonej liczbie stanów z powodu permutacji takich rzeczy, jak ilość pieniędzy gracz A, ile ma gracz B itd. Dlatego jestem prawie pewien, że chcesz trzymać się z dala od maszyn państwowych.

Ideą naszego frameworka było przedstawienie stanu gry jako struktury ze wszystkimi polami danych, które razem zapewniają kompletny stan gry (tj .: jeśli chcesz zapisać grę na dysku, wypisz tę strukturę).

Użyliśmy wzorca poleceń, aby przedstawić wszystkie prawidłowe działania w grze, które gracz może wykonać. Oto przykładowa akcja:

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
};

Widzisz więc, że aby zdecydować, czy ruch jest prawidłowy, możesz skonstruować tę akcję, a następnie wywołać jej funkcję IsLegal, przekazując bieżący stan gry. Jeśli jest prawidłowy, a gracz potwierdzi działanie, możesz wywołać funkcję Zastosuj, aby faktycznie zmodyfikować stan gry. Upewniając się, że kod rozgrywki może modyfikować stan gry jedynie poprzez tworzenie i przesyłanie legalnych działań (czyli innymi słowy, rodzina metod Action :: Apply jest jedyną rzeczą, która bezpośrednio modyfikuje stan gry), wówczas zapewniasz, że Twoja gra stan nigdy nie będzie nieważny. Ponadto, używając wzorca poleceń, umożliwiasz serializację żądanych ruchów gracza i wysyłanie ich przez sieć w celu wykonania w stanach gry innego gracza.

Skończyło się na tym, że z tym systemem był jeden problem, który okazał się mieć dość eleganckie rozwiązanie. Czasami akcje miały dwie lub więcej faz. Na przykład gracz może wylądować na nieruchomości w Monopoly i musi teraz podjąć nową decyzję. Jaki jest stan gry między momentem rzutu kostką przez gracza, a podjęciem decyzji o zakupie nieruchomości? Poradziliśmy sobie z takimi sytuacjami, udostępniając członka „kontekstu akcji” naszego stanu gry. Kontekst akcji byłby normalnie pusty, co oznacza, że ​​gra nie jest obecnie w żadnym specjalnym stanie. Kiedy gracz rzuca kośćmi, a akcja rzucania kośćmi zostaje zastosowana do stanu gry, zda sobie sprawę, że gracz wylądował na nienależącej do niego nieruchomości i może utworzyć nową właściwość „PlayerDecideToPurchaseProperty” kontekst akcji zawierający indeks gracza, na którego decyzję czekamy. Do czasu zakończenia akcji RollDice stan naszej gry oznacza, że ​​aktualnie czeka, aż określony gracz zdecyduje, czy kupić nieruchomość, nie. Teraz metoda IsLegal wszystkich innych akcji może zwracać fałsz, z wyjątkiem działań „BuyProperty” i „PassPropertyPurchaseOpportunity”, które są legalne tylko wtedy, gdy stan gry ma kontekst akcji „PlayerDecideToPurchaseProperty”.

Dzięki wykorzystaniu kontekstów akcji nie ma ani jednego punktu w czasie życia gry planszowej, w którym struktura stanu gry nie odzwierciedla DOKŁADNIE tego, co dzieje się w grze w danym momencie. Jest to bardzo pożądana właściwość Twojego systemu gier planszowych. Znacznie łatwiej będzie ci pisać kod, jeśli możesz znaleźć wszystko, co chcesz wiedzieć o tym, co dzieje się w grze, badając tylko jedną strukturę.

Co więcej, bardzo ładnie rozciąga się na środowiska sieciowe, w których klienci mogą przesyłać swoje działania przez sieć do komputera głównego, który może zastosować akcję do „oficjalnego” stanu gry hosta, a następnie powtórzyć tę akcję z powrotem do wszystkich innych klientów, aby niech zastosują ją do swoich replikowanych stanów gry.

Mam nadzieję, że to było zwięzłe i pomocne.

Andrew Top
źródło
4
Nie sądzę, żeby to było zwięzłe, ale jest pomocne! Głosowano za.
Jay Bazuzi
Cieszę się, że było to pomocne ... Które części nie były zwięzłe? Z przyjemnością dokonam edycji wyjaśniającej.
Andrew Top
Buduję teraz grę turową i ten post był naprawdę pomocny!
Kiv
Czytałem, że Memento jest wzorem do cofnięcia ... Memento vs Command Pattern do cofnięcia, twoje myśli plz ..
zotherstupidguy
To najlepsza odpowiedź, jaką do tej pory przeczytałem w Stackoverflow. DZIĘKI!
Papipo
19

Podstawowa struktura twojego silnika gry korzysta z wzorca stanu . Elementy w Twoim pudełku z grą to pojedyncze elementy różnych klas. Struktura każdego stanu może wykorzystywać wzorzec strategii lub metodę szablonową .

Fabryczne służy do tworzenia graczy, które są wkładane do listy graczy, innym Singleton. GUI będzie obserwować silnik gry, używając wzorca Observer i wchodzić z nim w interakcję, używając jednego z kilku obiektów polecenia utworzonych za pomocą wzorca poleceń . Korzystanie z Observer i Command może być używane w kontekście widoku pasywnego, ale w zależności od twoich preferencji można użyć prawie każdego wzorca MVP / MVC. Podczas zapisywania gry musisz złapać pamiątkę z jej aktualnego stanu

Zalecam przejrzenie niektórych wzorców na tej stronie i sprawdzenie, czy któryś z nich jest punktem wyjścia. Znowu sercem twojej planszy będzie automat stanowy. Większość gier będzie reprezentowana przez dwa stany przed grą / konfiguracją i samą grę. Ale możesz mieć więcej stanów, jeśli modelowana gra ma kilka różnych trybów gry. Stany nie muszą być sekwencyjne, na przykład gra wojenna Axis & Battles ma planszę bitewną, której gracze mogą używać do rozstrzygania bitew. Są więc trzy stany przed grą, plansza główna, plansza bitewna, przy czym gra nieustannie przełącza się między planszą główną i planszą bitwy. Oczywiście sekwencja tur może być również reprezentowana przez maszynę stanów.

RS Conley
źródło
17

Właśnie skończyłem projektowanie i wdrażanie gry opartej na stanach z wykorzystaniem polimorfizmu.

Korzystanie z podstawowej klasy abstrakcyjnej o nazwie, GamePhasektóra ma jedną ważną metodę

abstract public GamePhase turn();

Oznacza to, że każdy GamePhaseobiekt przechowuje aktualny stan gry, a wywołanie turn()patrzy na jego aktualny stan i zwraca następny GamePhase.

Każdy beton GamePhasema konstruktorów, które przechowują cały stan gry. Każda turn()metoda zawiera trochę reguł gry. Chociaż rozpowszechnia to reguły, utrzymuje powiązane zasady blisko siebie. Końcowym rezultatem każdego z nich turn()jest po prostu utworzenie następnej GamePhasei przejście w pełnym stanie do następnej fazy.

Pozwala turn()to być bardzo elastycznym. W zależności od twojej gry, dany stan może rozgałęziać się do wielu różnych typów faz. Tworzy to wykres wszystkich faz gry.

Na najwyższym poziomie kod do kierowania jest bardzo prosty:

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

Jest to niezwykle przydatne, ponieważ mogę teraz łatwo stworzyć dowolny stan / fazę gry do testów

A teraz odpowiedz na drugą część twojego pytania, jak to działa w trybie dla wielu graczy? W niektórych przypadkach, GamePhasektóre wymagają interwencji użytkownika, wywołanie z turn()zapytałoby o bieżący Playerich Strategystan / fazę. Strategyto tylko interfejs wszystkich możliwych decyzji, jakie Playermożna podjąć. Ta konfiguracja umożliwia równieżStrategy na implementację z AI!

Andrew Top powiedział również:

Twoja gra może prawdopodobnie znajdować się w (prawie) nieskończonej liczbie stanów z powodu permutacji takich rzeczy, jak ilość pieniędzy gracz A, ile ma gracz B itd. Dlatego jestem pewien, że chcesz trzymać się z dala od maszyn państwowych.

Myślę, że to stwierdzenie jest bardzo mylące, podczas gdy prawdą jest, że istnieje wiele różnych stanów gry, jest tylko kilka faz. Aby poradzić sobie z jego przykładem, wystarczyłoby to jako parametr całkowity dla konstruktorów mojego betonuGamePhase s.

Monopol

Przykładem GamePhasemoże być:

  • GameStarts
  • PlayerRolls
  • PlayerLandsOnProperty (FreeParking, GoToJail, Go itp.)
  • PlayerTrades
  • PlayerPurchasesProperty
  • GraczZakupyDomy
  • GraczZakupyHotele
  • PlayerPaysRent
  • PlayerBankrupts
  • (Wszystkie karty szans i skrzyń społeczności)

Niektóre stany w bazie GamePhaseto:

  • Lista graczy
  • Bieżący gracz (kto ma kolej)
  • Pieniądze / własność gracza
  • Domy / Hotele na Nieruchomości
  • Pozycja gracza

A potem niektóre fazy zapisywałyby swój własny stan w razie potrzeby, na przykład PlayerRolls zapisywałoby, ile razy gracz wykonał kolejne dublety. Gdy opuścimy fazę PlayerRolls, nie przejmujemy się już kolejnymi rzutami.

Wiele faz można ponownie wykorzystać i połączyć. Na przykład GamePhase CommunityChestAdvanceToGoutworzy następną fazę PlayerLandsOnGoz obecnym stanem i zwróci ją. W konstruktorze PlayerLandsOnGoobecnego gracza zostanie przeniesiony do Go, a jego pieniądze zostaną zwiększone o 200 $.

Pirolistyczne
źródło
9

Oczywiście istnieje wiele, wiele, wiele, wiele, wiele, wiele, wiele zasobów dotyczących tego tematu. Ale myślę, że jesteś na dobrej drodze, dzieląc obiekty i pozwalając im obsługiwać własne zdarzenia / dane i tak dalej.

Podczas tworzenia gier planszowych opartych na kafelkach dobrze jest mieć procedury mapowania między tablicą planszową a wierszem / kolumną iz powrotem, wraz z innymi funkcjami. Pamiętam moją pierwszą grę planszową (dawno temu), kiedy zmagałem się z tym, jak uzyskać wiersz / kolumnę z boardarray 5.

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

Nostalgia. ;)

W każdym razie http://www.gamedev.net/ to dobre miejsce na informacje. http://www.gamedev.net/reference/

Stefan
źródło
Dlaczego nie użyjesz po prostu dwuwymiarowej tablicy? Wtedy kompilator zajmie się tym za Ciebie.
Jay Bazuzi,
Moją wymówką jest to, że to było dawno temu. ;)
Stefan
1
gamedev ma mnóstwo rzeczy, ale nie widziałem tego, czego szukałem.
Jay Bazuzi
jakiego języka używałeś?
zotherstupidguy
Basic, Basica, QB, QuickBasic i tak dalej. ;)
Stefan
4

Three Rings oferuje biblioteki Java na licencji LGPL. Nenya i Vilya to biblioteki związane z grami.

Oczywiście pomocne byłoby, gdyby Twoje pytanie dotyczyło platformy i / lub ograniczeń językowych, które możesz mieć.

jmucchiello
źródło
„Ostatecznie spodziewam się zbudowania UI WPF” - to znaczy .NET. Przynajmniej, o ile wiem.
Mark Allen,
Zupa alfabetyczna, której nie jestem świadomy.
jmucchiello,
Tak, robię .NET, ale moje pytanie nie jest związane z językiem ani platformą.
Jay Bazuzi,
3

Zgadzam się z odpowiedzią Pyrolisticsa i wolę jego sposób robienia rzeczy (chociaż przejrzałem tylko inne odpowiedzi).

Przypadkowo użyłem też jego nazewnictwa „GamePhase”. Zasadniczo to, co zrobiłbym w przypadku gry planszowej z turami, to aby twoja klasa GameState zawierała obiekt abstrakcyjnej GamePhase, o której wspomniał Pyrolistics.

Powiedzmy, że stany gry to:

  1. Rolka
  2. Ruszaj się
  3. Kup / Nie kupuj
  4. Więzienie

Możesz mieć konkretne klasy pochodne dla każdego stanu. Posiadaj funkcje wirtualne przynajmniej dla:

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

W funkcji StartPhase () można ustawić wszystkie początkowe wartości stanu, na przykład wyłączenie wejścia innego gracza i tak dalej.

Gdy wywoływana jest funkcja roll.EndPhase (), upewnij się, że wskaźnik GamePhase jest ustawiony na następny stan.

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

W tej MovePhase :: StartPhase () możesz na przykład ustawić pozostałe ruchy aktywnego gracza na liczbę wyrzuconą w poprzedniej fazie.

Dzięki temu projektowi możesz rozwiązać problem „3 x podwójne = więzienie” w fazie rzutu. Klasa RollPhase może obsługiwać własny stan. Na przykład

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.
}

Różnię się od pirolistyki tym, że powinna istnieć faza na wszystko, łącznie z sytuacją, gdy gracz wyląduje w skrzynce społeczności lub coś w tym rodzaju. Zajmę się tym wszystkim w MovePhase. Dzieje się tak, ponieważ jeśli masz zbyt wiele kolejnych faz, gracz prawdopodobnie będzie czuł się zbyt „prowadzony”. Na przykład, jeśli jest faza, w której gracz może TYLKO kupować nieruchomości, a następnie TYLKO hotele, a następnie TYLKO domy, to tak, jakby nie było wolności. Po prostu zatrzaśnij wszystkie te części w jedną Fazę Kupna i daj graczowi swobodę kupowania wszystkiego, czego chce. Klasa BuyPhase z łatwością radzi sobie z legalnymi zakupami.

Na koniec zajmijmy się planszą. Chociaż tablica 2D jest w porządku, polecam mieć wykres kafelkowy (gdzie kafelek jest pozycją na planszy). W przypadku monopolii byłaby to raczej lista podwójnie połączona. Wtedy każda płytka miałaby:

  1. previousTile
  2. nextTile

Byłoby więc znacznie łatwiej zrobić coś takiego:

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

Funkcja AdvanceTo może obsłużyć Twoje animacje krok po kroku lub cokolwiek chcesz. A także oczywiście zmniejszaj pozostałe ruchy.

Porady RS Conley dotyczące wzorca obserwatorów dla GUI są dobre.

Nie publikowałem dużo wcześniej. Mam nadzieję, że to komuś pomoże.

Reasurria
źródło
2

Czy jest jakiś stan wiedzy, z którego mogę skorzystać?

Jeśli Twoje pytanie nie dotyczy języka lub platformy. wtedy zalecałbym rozważenie AOP Patterns for State, Memento, Command itp.

Jaka jest odpowiedź .NET na AOP ???

Spróbuj też znaleźć fajne strony internetowe, takie jak http://www.chessbin.com

zotherstupidguy
źródło