¿Algún patrón para modelar juegos de mesa? [cerrado]

93

Para divertirme, estoy tratando de escribir uno de los juegos de mesa favoritos de mi hijo como una pieza de software. Eventualmente espero construir una interfaz de usuario de WPF sobre ella, pero ahora mismo estoy construyendo la máquina que modela los juegos y sus reglas.

Mientras hago esto, sigo viendo problemas que creo que son comunes a muchos juegos de mesa, y quizás otros ya los hayan resuelto mejor que yo.

(Tenga en cuenta que la inteligencia artificial para jugar el juego y los patrones de alto rendimiento no me interesan).

Hasta ahora mis patrones son:

  • Varios tipos inmutables que representan entidades en la caja del juego, por ejemplo, dados, damas, cartas, un tablero, espacios en el tablero, dinero, etc.

  • Un objeto para cada jugador, que contiene los recursos de los jugadores (por ejemplo, dinero, puntuación), su nombre, etc.

  • Un objeto que representa el estado del juego: los jugadores, quién es el turno, la disposición de las piezas en el tablero, etc.

  • Una máquina de estado que gestiona la secuencia de turnos. Por ejemplo, muchos juegos tienen un pequeño pre-juego donde cada jugador tira para ver quién va primero; ese es el estado de inicio. Cuando comienza el turno de un jugador, primero rueda, luego se mueve, luego tiene que bailar en su lugar, luego otros jugadores adivinan qué raza de pollo son, luego reciben puntos.

¿Existe algún estado de la técnica que pueda aprovechar?

EDITAR: Una cosa de la que me di cuenta recientemente es que el estado del juego se puede dividir en dos categorías:

  • Estado del artefacto del juego . "Tengo $ 10" o "mi mano izquierda está en azul".

  • Estado de la secuencia del juego . "He tirado dobles dos veces; la siguiente me mete en la cárcel". Una máquina de estados puede tener sentido aquí.

EDITAR: Lo que realmente estoy buscando aquí es la mejor manera de implementar juegos multijugador basados ​​en turnos como Ajedrez, Scrabble o Monopolio. Estoy seguro de que podría crear un juego de este tipo simplemente trabajando en él de principio a fin, pero, al igual que otros patrones de diseño, es probable que haya algunas formas de hacer que las cosas funcionen mucho mejor que no son obvias sin un estudio cuidadoso. Eso es lo que espero.

Jay Bazuzi
fuente
3
¿Estás construyendo una especie de Hokey Pokey, Monopoly, charadas mashup?
Anthony Mastrean
Querrá una máquina de estado para cualquier regla que se base en el estado (err ...) como la regla de los tres dobles para Monopoly. Publicaría una respuesta más completa, pero no tengo experiencia en hacer esto. Aunque podría pontificar al respecto.
MSN

Respuestas:

115

Parece que este es un hilo de 2 meses que acabo de notar ahora, pero qué diablos. Ya diseñé y desarrollé el marco de juego para un juego de mesa comercial en red. Tuvimos una experiencia muy agradable trabajando con él.

Tu juego probablemente puede estar en una cantidad (cercana a) infinita de estados debido a las permutaciones de cosas como cuánto dinero tiene el jugador A, cuánto dinero tiene el jugador B, etc. Por lo tanto, estoy bastante seguro de que quieres mantenerse alejado de las máquinas de estado.

La idea detrás de nuestro marco era representar el estado del juego como una estructura con todos los campos de datos que, juntos, proporcionan el estado completo del juego (es decir, si quieres guardar el juego en el disco, escribes esa estructura).

Usamos el patrón de comando para representar todas las acciones de juego válidas que un jugador podía realizar. A continuación se muestra una acción de ejemplo:

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

Entonces ves que para decidir si un movimiento es válido, puedes construir esa acción y luego llamar a su función IsLegal, pasando al estado actual del juego. Si es válido y el jugador confirma la acción, puede llamar a la función Aplicar para modificar el estado del juego. Asegurándose de que su código de juego solo pueda modificar el estado del juego creando y enviando Acciones legales (en otras palabras, la familia de métodos Action :: Apply es lo único que modifica directamente el estado del juego), entonces se asegura de que su juego El estado nunca será inválido. Además, al usar el patrón de comando, hace posible serializar los movimientos deseados de su jugador y enviarlos a través de una red para que se ejecuten en los estados de juego de otros jugadores.

Terminó siendo un problema con este sistema que resultó tener una solución bastante elegante. A veces, las acciones tendrían dos o más fases. Por ejemplo, el jugador puede aterrizar en una propiedad en Monopoly y ahora debe tomar una nueva decisión. ¿Cuál es el estado del juego entre el momento en que el jugador lanzó los dados y antes de que decida comprar una propiedad o no? Manejamos situaciones como esta presentando un miembro de "Contexto de acción" de nuestro estado de juego. El contexto de la acción normalmente sería nulo, lo que indica que el juego no se encuentra actualmente en ningún estado especial. Cuando el jugador lanza los dados y la acción de tirar los dados se aplica al estado del juego, se dará cuenta de que el jugador ha aterrizado en una propiedad que no tiene y puede crear una nueva "PlayerDecideToPurchaseProperty" contexto de acción que contiene el índice del jugador del que estamos esperando una decisión. Para cuando la acción RollDice se haya completado, nuestro estado de juego representa que actualmente está esperando que el jugador especificado decida si comprar una propiedad o no. Ahora es fácil que el método IsLegal de todas las demás acciones devuelva falso, excepto las acciones "BuyProperty" y "PassPropertyPurchaseOpportunity", que solo son legales cuando el estado del juego tiene el contexto de acción "PlayerDecideToPurchaseProperty".

Mediante el uso de contextos de acción, nunca hay un solo punto en la vida del juego de mesa en el que la estructura del estado del juego no represente EXACTAMENTE lo que está sucediendo en el juego en ese momento. Esta es una propiedad muy deseable de su sistema de juego de mesa. Te resultará mucho más fácil escribir código cuando puedas encontrar todo lo que quieras saber sobre lo que está sucediendo en el juego examinando solo una estructura.

Además, se extiende muy bien a entornos en red, donde los clientes pueden enviar sus acciones a través de una red a una máquina host, que puede aplicar la acción al estado de juego "oficial" del host, y luego repetir esa acción a todos los demás clientes para pídales que lo apliquen a sus estados de juego replicados.

Espero que esto haya sido conciso y útil.

Andrew Top
fuente
4
No creo que sea conciso, ¡pero es útil! Voto a favor.
Jay Bazuzi
Me alegro de que haya sido útil ... ¿Qué partes no fueron concisas? Estaría feliz de hacer una edición aclaratoria.
Andrew Top
¡Estoy construyendo un juego por turnos en este momento y esta publicación ha sido realmente útil!
Kiv
Leí que Memento es el patrón que se usa para deshacer ... Memento vs Patrón de comando para deshacer, sus pensamientos por
favor
Esta es la mejor respuesta que he leído en Stackoverflow hasta ahora. ¡GRACIAS!
Papipo
18

La estructura básica de su motor de juego utiliza el patrón de estado . Los elementos de tu caja de juego son singletons de varias clases. La estructura de cada estado puede utilizar el Patrón de estrategia o el Método de plantilla .

Se utiliza una fábrica para crear los jugadores que se insertan en una lista de jugadores, otro singleton. La GUI vigilará el motor de juego mediante el uso del patrón Observer e interactuará con este mediante el uso de uno de los varios objetos de comando creados con el patrón de comando . El uso de Observer y Command se puede usar en el contexto de una vista pasiva, pero se puede usar casi cualquier patrón MVP / MVC según sus preferencias. Cuando guarde el juego, debe tomar un recuerdo de su estado actual

Recomiendo mirar algunos de los patrones en este sitio y ver si alguno de ellos lo toma como punto de partida. Una vez más, el corazón de su tablero de juego será una máquina de estados. La mayoría de los juegos estarán representados por dos estados antes del juego / configuración y el juego real. Pero puedes tener más estados si el juego que estás modelando tiene varios modos de juego distintos. Los estados no tienen que ser secuenciales, por ejemplo, el juego de guerra Axis & Battles tiene un tablero de batalla que los jugadores pueden usar para resolver batallas. Así que hay tres estados antes del juego, tablero principal, tablero de batalla con el juego cambiando continuamente entre el tablero principal y el tablero de batalla. Por supuesto, la secuencia de turnos también se puede representar mediante una máquina de estado.

RS Conley
fuente
15

Acabo de terminar de diseñar e implementar un juego basado en estados usando polimorfismo.

Usando una clase abstracta base llamada GamePhaseque tiene un método importante

abstract public GamePhase turn();

Lo que esto significa es que cada GamePhaseobjeto contiene el estado actual del juego, y una llamada a turn()mira su estado actual y devuelve el siguiente GamePhase.

Cada hormigón GamePhasetiene constructores que mantienen todo el estado del juego. Cada turn()método tiene un poco de las reglas del juego dentro de ellos. Si bien esto difunde las reglas, mantiene las reglas relacionadas juntas. El resultado final de cada uno turn()es simplemente crear el siguiente GamePhasey pasar en el estado completo a la siguiente fase.

Esto permite turn()ser muy flexible. Dependiendo de su juego, un estado dado puede ramificarse en muchos tipos diferentes de fases. Esto forma un gráfico de todas las fases del juego.

En el nivel más alto, el código para manejarlo es muy simple:

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

Esto es extremadamente útil ya que ahora puedo crear fácilmente cualquier estado / fase del juego para probar

Ahora, para responder a la segunda parte de tu pregunta, ¿cómo funciona esto en el modo multijugador? Dentro de ciertos mensajes de correo GamePhaseelectrónico que requieren la entrada del usuario, una llamada de turn()preguntará a la corriente Playersu Strategyestado / fase actual. Strategyes solo una interfaz de todas las decisiones posibles que Playerpuede tomar. ¡Esta configuración también permite Strategyser implementada con IA!

También Andrew Top dijo:

Tu juego probablemente puede estar en una cantidad (cercana a) infinita de estados debido a las permutaciones de cosas como cuánto dinero tiene el jugador A, cuánto dinero tiene el jugador B, etc. Por lo tanto, estoy bastante seguro de que quieres mantenerse alejado de las máquinas de estado.

Creo que esa afirmación es muy engañosa, si bien es cierto que hay muchos estados de juego diferentes, solo hay unas pocas fases de juego. Para manejar su ejemplo, todo lo que sería un parámetro entero para los constructores de mi GamePhases concreto .

Monopolio

Ejemplo de algunos GamePhases sería:

  • GameStarts
  • PlayerRolls
  • PlayerLandsOnProperty (FreeParking, GoToJail, Go, etc.)
  • PlayerTrades
  • JugadorComprasPropiedad
  • JugadorComprasCasas
  • JugadorComprasHoteles
  • PlayerPaysRent
  • PlayerBankrupts
  • (Cartas de todos los cofres de oportunidad y comunidad)

Y algunos estados en la base GamePhaseson:

  • Lista de jugadores
  • Jugador actual (quién es el turno)
  • Dinero / Propiedad del jugador
  • Casas / Hoteles en Propiedades
  • Posición de jugador

Y luego, algunas fases registrarían su propio estado según sea necesario, por ejemplo, PlayerRolls registraría la cantidad de veces que un jugador ha lanzado dobles consecutivos. Una vez que dejamos la fase PlayerRolls, ya no nos importan las tiradas consecutivas.

Se pueden reutilizar y vincular muchas fases. Por ejemplo GamePhase CommunityChestAdvanceToGo, crearía la siguiente fase PlayerLandsOnGocon el estado actual y la devolvería. En el constructor del PlayerLandsOnGojugador actual se movería a Go y su dinero se incrementaría en $ 200.

Pirolístico
fuente
8

Por supuesto que hay muchos, muchos, muchos, muchos, muchos, muchos, muchos recursos sobre este tema. Pero creo que está en el camino correcto al dividir los objetos y dejar que manejen sus propios eventos / datos, etc.

Al hacer juegos de mesa basados ​​en mosaicos, le resultará agradable tener rutinas para mapear entre la matriz del tablero y la fila / columna y viceversa, junto con otras características. Recuerdo mi primer juego de mesa (hace mucho tiempo) cuando luché con cómo obtener row / col de boardarray 5.

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

Nostalgia. ;)

De todos modos, http://www.gamedev.net/ es un buen lugar para obtener información. http://www.gamedev.net/reference/

Stefan
fuente
¿Por qué no usas una matriz bidimensional? Entonces el compilador puede manejar esto por usted.
Jay Bazuzi
Mi excusa es que esto fue hace mucho, mucho tiempo. ;)
Stefan
1
gamedev tiene un montón de cosas, pero no vi exactamente lo que estaba buscando.
Jay Bazuzi
que idioma estabas usando
zotherstupidguy
Basic, Basica, QB, QuickBasic y así sucesivamente. ;)
Stefan
5

Gran parte de los materiales que puedo encontrar en línea son listas de referencias publicadas. La sección de publicaciones de Game Design Patterns tiene enlaces a versiones en PDF de los artículos y tesis. Muchos de estos parecen artículos académicos como Design Patterns for Games . También hay al menos un libro disponible en Amazon, Patterns in Game Design .

Eric Weilnau
fuente
3

Three Rings ofrece bibliotecas de Java LGPL. Nenya y Vilya son las bibliotecas para cosas relacionadas con el juego.

Por supuesto, ayudaría si su pregunta mencionara las restricciones de plataforma y / o idioma que podría tener.

jmucchiello
fuente
"Eventualmente espero construir una IU de WPF", eso significa .NET. Al menos, por lo que yo sé.
Mark Allen
Sopa de letras que no conozco.
jmucchiello
Sí, estoy haciendo .NET, pero mi pregunta no es específica de un idioma o plataforma.
Jay Bazuzi
2

Estoy de acuerdo con la respuesta de Pyrolistical y prefiero su forma de hacer las cosas (aunque solo eché un vistazo a las otras respuestas).

Casualmente también usé su nombre "GamePhase". Básicamente, lo que haría en el caso de un juego de mesa por turnos es que tu clase GameState contenga un objeto del GamePhase abstracto como lo menciona Pyrolistical.

Digamos que los estados del juego son:

  1. Rodar
  2. Moverse
  3. Comprar / No comprar
  4. Cárcel

Podría tener clases derivadas concretas para cada estado. Tener funciones virtuales al menos para:

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

En la función StartPhase () puede establecer todos los valores iniciales para un estado, por ejemplo, deshabilitar la entrada del otro jugador y así sucesivamente.

Cuando se llama a roll.EndPhase (), asegúrese de que el puntero de GamePhase se establezca en el siguiente estado.

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

En este MovePhase :: StartPhase (), por ejemplo, establecerías los movimientos restantes del jugador activo a la cantidad obtenida en la fase anterior.

Ahora, con este diseño en su lugar, podría resolver su problema de "3 x doble = cárcel" dentro de la fase Roll. La clase RollPhase puede manejar su propio estado. Por ejemplo

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

Me diferencia de Pyrolistical en que debería haber una fase para todo, incluso cuando el jugador aterriza en el cofre de la comunidad o algo así. Manejaría todo esto en MovePhase. Esto se debe a que si tiene demasiadas fases secuenciales, es muy probable que el jugador se sienta demasiado "guiado". Por ejemplo, si hay una fase en la que el jugador SOLO puede comprar propiedades y luego SOLO comprar hoteles y luego SOLO comprar casas, es como si no hubiera libertad. Simplemente coloque todas esas partes en una BuyPhase y déle al jugador la libertad de comprar lo que quiera. La clase BuyPhase puede manejar fácilmente qué compras son legales.

Finalmente, abordemos el tablero de juego. Aunque una matriz 2D está bien, recomendaría tener un gráfico de mosaico (donde un mosaico es una posición en el tablero). En el caso del monopolio, sería preferible una lista doblemente enlazada. Entonces cada mosaico tendría un:

  1. anteriorTile
  2. nextTile

Entonces sería mucho más fácil hacer algo como:

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

La función AdvanceTo puede manejar sus animaciones paso a paso o lo que quiera. Y también disminuir los movimientos restantes, por supuesto.

El consejo de RS Conley sobre el patrón de observador para la GUI es bueno.

No he publicado mucho antes. Espero que esto ayude a alguien.

Reasurria
fuente
1

¿Existe algún estado de la técnica que pueda aprovechar?

Si su pregunta no es específica de un idioma o plataforma. entonces le recomendaría que considere los patrones AOP para estado, recuerdo, comando, etc.

¿Cuál es la respuesta de .NET a AOP?

También intente encontrar algunos sitios web interesantes como http://www.chessbin.com

zotherstupidguy
fuente