Algum padrão para modelar jogos de tabuleiro? [fechadas]


93

Para me divertir, estou tentando escrever um dos jogos de tabuleiro favoritos do meu filho como um software. Eventualmente, espero construir uma IU do WPF sobre ele, mas agora estou construindo a máquina que modela os jogos e suas regras.

Enquanto faço isso, continuo vendo problemas que considero comuns a muitos jogos de tabuleiro e talvez outros já os tenham resolvido melhor do que eu.

(Observe que a IA para jogar o jogo e os padrões de alto desempenho não são interessantes para mim.)

Até agora, meus padrões são:

  • Vários tipos imutáveis ​​que representam entidades na caixa do jogo, por exemplo, dados, damas, cartas, um tabuleiro, espaços no tabuleiro, dinheiro, etc.

  • Um objeto para cada jogador, que contém os recursos dos jogadores (por exemplo, dinheiro, pontuação), seu nome, etc.

  • Um objeto que representa o estado do jogo: os jogadores, quem é a vez, o layout das peças no tabuleiro, etc.

  • Uma máquina de estado que gerencia a sequência de curvas. Por exemplo, muitos jogos têm um pequeno pré-jogo onde cada jogador rola para ver quem vai primeiro; esse é o estado inicial. Quando o turno de um jogador começa, primeiro eles rolam, depois se movem, então eles têm que dançar no lugar, então os outros jogadores adivinham que raça de frango eles são e então recebem pontos.

Existe alguma arte anterior da qual eu possa aproveitar?

EDIT: Uma coisa que percebi recentemente é que o estado do jogo pode ser dividido em duas categorias:

  • Estado do artefato do jogo . "Eu tenho $ 10" ou "minha mão esquerda está azul".

  • Estado da sequência do jogo . "Já rolei duplas duas vezes; a próxima me põe na prisão". Uma máquina de estado pode fazer sentido aqui.

EDIT: O que estou realmente procurando aqui é a melhor maneira de implementar jogos multiplayer baseados em turnos, como xadrez, Scrabble ou Banco Imobiliário. Tenho certeza de que poderia criar esse jogo apenas trabalhando nele do início ao fim, mas, como outros Design Patterns, provavelmente há algumas maneiras de fazer as coisas correrem muito mais suavemente que não são óbvias sem um estudo cuidadoso. É isso que estou esperando.


3
Você está construindo algum tipo de mashup Hokey Pokey, Banco Imobiliário e charadas?
Anthony Mastrean

Você vai querer uma máquina de estado para qualquer regra que dependa do estado (err ...) como a regra das três duplas para o Monopólio. Eu postaria uma resposta mais completa, mas não tenho experiência em fazer isso. Eu poderia pontificar sobre isso, no entanto.
MSN

Respostas:


115

parece que este é um tópico de 2 meses que acabei de notar agora, mas que diabos. Eu projetei e desenvolvi a estrutura de jogo para um jogo de tabuleiro comercial em rede antes. Nós tivemos uma experiência muito agradável trabalhando com ele.

Seu jogo pode provavelmente estar em uma quantidade (perto de) infinita de estados por causa das permutações de coisas como quanto dinheiro o jogador A tem, quanto dinheiro o jogador B tem, e etc ... Portanto, tenho certeza que você deseja para ficar longe das máquinas de estado.

A ideia por trás do nosso framework era representar o estado do jogo como uma estrutura com todos os campos de dados que, juntos, fornecem o estado completo do jogo (ou seja: se você quiser salvar o jogo no disco, você escreve essa estrutura).

Usamos o Padrão de Comando para representar todas as ações válidas do jogo que um jogador poderia realizar. Aqui estaria um exemplo de ação:

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

Então você vê que para decidir se um movimento é válido, você pode construir essa ação e então chamar sua função IsLegal, passando no estado de jogo atual. Se for válido e o jogador confirmar a ação, você pode chamar a função Aplicar para modificar o estado do jogo. Ao garantir que seu código de jogo só pode modificar o estado do jogo criando e enviando ações legais (em outras palavras, a família de métodos Action :: Apply é a única coisa que modifica diretamente o estado do jogo), então você garante que seu jogo estado nunca será inválido. Além disso, usando o padrão de comando, você torna possível serializar os movimentos desejados de seu jogador e enviá-los por uma rede para serem executados nos estados de jogo de outro jogador.

Acabou sendo um problema com esse sistema que acabou sendo uma solução bastante elegante. Às vezes, as ações teriam duas ou mais fases. Por exemplo, o jogador pode pousar em uma propriedade no Monopólio e agora deve tomar uma nova decisão. Qual é o estado do jogo entre o momento em que o jogador lança os dados e o momento em que decide comprar ou não uma propriedade? Gerenciamos situações como essa apresentando um membro do "Contexto de Ação" do nosso estado de jogo. O contexto de ação normalmente seria nulo, indicando que o jogo não está em nenhum estado especial. Quando o jogador lança os dados e a ação de lançamento dos dados é aplicada ao estado do jogo, ele perceberá que o jogador pousou em uma propriedade não pertencente a ele e pode criar um novo "PlayerDecideToPurchaseProperty" contexto de ação que contém o índice do jogador do qual estamos aguardando uma decisão. No momento em que a ação RollDice é concluída, nosso estado de jogo representa que ele está aguardando que o jogador especificado decida se deseja comprar uma propriedade, não. Agora é fácil para o método IsLegal de todas as outras ações retornar falso, exceto para as ações "BuyProperty" e "PassPropertyPurchaseOpportunity", que só são legais quando o estado do jogo tem o contexto de ação "PlayerDecideToPurchaseProperty".

Por meio do uso de contextos de ação, nunca há um único ponto na vida do jogo de tabuleiro em que a estrutura do estado do jogo não represente EXATAMENTE o que está acontecendo no jogo naquele momento. Esta é uma propriedade muito desejável em seu sistema de jogo de tabuleiro. Será muito mais fácil para você escrever código quando puder encontrar tudo o que deseja saber sobre o que está acontecendo no jogo examinando apenas uma estrutura.

Além disso, ele se estende muito bem a ambientes de rede, onde os clientes podem enviar suas ações através de uma rede para uma máquina host, que pode aplicar a ação ao estado de jogo "oficial" do host e, em seguida, ecoar essa ação de volta para todos os outros clientes para peça-lhes que o apliquem aos seus estados de jogo replicados.

Espero que tenha sido conciso e útil.


4
Não acho que seja conciso, mas ajuda! Votado.
Jay Bazuzi

Ainda bem que foi útil ... Quais partes não foram concisas? Eu ficaria feliz em fazer uma edição de esclarecimento.
Andrew Top

Estou construindo um jogo baseado em turnos agora e este post tem sido muito útil!
Kiv

Eu li que Memento é o padrão a ser usado para desfazer ... Memento vs Padrão de Comando para desfazer, seus pensamentos plz ..
zotherstupidguy

Esta é a melhor resposta que li no Stackoverflow até agora. OBRIGADO!
Papipo

18

A estrutura básica do seu motor de jogo usa o padrão de estado . Os itens de sua caixa de jogo são singletons de várias classes. A estrutura de cada estado pode usar o Strategy Pattern ou o Template Method .

Uma fábrica é usada para criar os jogadores que são inseridos em uma lista de jogadores, outro singleton. A GUI manterá a vigilância no Game Engine usando o padrão Observer e interagirá com ele usando um dos vários objetos Command criados usando o Command Pattern . O uso de Observador e Comando pode ser usado no contexto de uma Visualização Passiva, mas praticamente qualquer padrão MVP / MVC pode ser usado, dependendo de suas preferências. Quando você salva o jogo você precisa pegar uma lembrança de seu estado atual

Eu recomendo examinar alguns dos padrões neste site e ver se algum deles te pega como ponto de partida. Mais uma vez, o coração do seu tabuleiro de jogo será uma máquina de estados. A maioria dos jogos será representada por dois estados pré-jogo / configuração e o jogo real. Mas você pode ter mais estados se o jogo que está modelando tiver vários modos distintos de jogo. Os estados não precisam ser sequenciais, por exemplo, o jogo de guerra Axis & Battles tem um tabuleiro de batalha que os jogadores podem usar para resolver as batalhas. Portanto, há três estados pré-jogo, tabuleiro principal e tabuleiro de batalha, com o jogo alternando continuamente entre o tabuleiro principal e o tabuleiro de batalha. É claro que a sequência de curvas também pode ser representada por uma máquina de estados.


15

Acabei de projetar e implementar um jogo baseado em estado usando polimorfismo.

Usando uma classe base abstrata chamada GamePhaseque tem um método importante

abstract public GamePhase turn();

Isso significa que cada GamePhaseobjeto mantém o estado atual do jogo, e uma chamada para verifica turn()seu estado atual e retorna o próximo GamePhase.

Cada concreto GamePhasepossui construtores que mantêm todo o estado do jogo. Cada turn()método contém um pouco das regras do jogo. Enquanto isso espalha as regras, mantém as regras relacionadas próximas. O resultado final de cada um turn()é apenas criar o próximo GamePhasee passar no estado completo para a próxima fase.

Isso permite turn()ser muito flexível. Dependendo do seu jogo, um determinado estado pode se ramificar para muitos tipos diferentes de fases. Isso forma um gráfico de todas as fases do jogo.

No nível mais alto, o código para conduzi-lo é muito simples:

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

Isso é extremamente útil, pois agora posso criar facilmente qualquer estado / fase do jogo para teste

Agora, para responder à segunda parte da sua pergunta, como isso funciona no modo multijogador? Dentro de certos GamePhases que requerem entrada do usuário, uma chamada de turn()perguntaria à corrente Playerseu Strategyestado / fase atual. Strategyé apenas uma interface de todas as decisões possíveis que você Playerpode tomar. Esta configuração também permite Strategyser implementada com AI!

Andrew Top também disse:

Seu jogo pode provavelmente estar em uma quantidade (perto de) infinita de estados por causa das permutações de coisas como quanto dinheiro o jogador A tem, quanto dinheiro o jogador B tem, e etc ... Portanto, tenho certeza que você deseja para ficar longe das máquinas de estado.

Acho que essa afirmação é muito enganosa, embora seja verdade que existem muitos estados de jogo diferentes, existem apenas algumas fases do jogo. Para lidar com seu exemplo, tudo que seria um parâmetro inteiro para os construtores de meus GamePhases concretos .

Monopólio

Exemplo de alguns GamePhases seria:

  • GameStarts
  • PlayerRolls
  • PlayerLandsOnProperty (FreeParking, GoToJail, Go, etc)
  • PlayerTrades
  • PlayerPurchasesProperty
  • PlayerPurchasesHouses
  • PlayerPurchasesHotels
  • PlayerPaysRent
  • PlayerBankrupts
  • (Todos os cartões de Chance e Community Chest)

E alguns estados da base GamePhasesão:

  • Lista de jogadores
  • Jogador atual (quem é a vez)
  • Dinheiro / propriedade do jogador
  • Casas / Hotéis em Propriedades
  • Posição do jogador

E então algumas fases registrariam seu próprio estado conforme necessário, por exemplo PlayerRolls registraria o número de vezes que um jogador jogou duplas consecutivas. Assim que deixarmos a fase PlayerRolls, não nos importamos mais com lançamentos consecutivos.

Muitas fases podem ser reutilizadas e vinculadas. Por exemplo, o GamePhase CommunityChestAdvanceToGocriaria a próxima fase PlayerLandsOnGocom o estado atual e o retornaria. No construtor do PlayerLandsOnGojogador atual seria movido para Go e seu dinheiro seria incrementado em $ 200.


8

Claro que existem muitos, muitos, muitos, muitos, muitos, muitos, muitos recursos sobre este tópico. Mas acho que você está no caminho certo dividindo os objetos e deixando que eles tratem de seus próprios eventos / dados e assim por diante.

Ao fazer jogos de tabuleiro baseados em blocos, você achará bom ter rotinas para mapear entre a matriz do tabuleiro e a linha / coluna e atrás, junto com outros recursos. Lembro-me do meu primeiro jogo de tabuleiro (muito tempo atrás) quando me esforcei para obter uma linha / cor do boardarray 5.

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

Nostalgia. ;)

De qualquer forma, http://www.gamedev.net/ é um bom lugar para informações. http://www.gamedev.net/reference/


Por que você simplesmente não usa uma matriz bidimensional? Então, o compilador pode cuidar disso para você.
Jay Bazuzi

Minha desculpa é que isso foi há muito tempo. ;)
Stefan

1
gamedev tem uma tonelada de coisas, mas eu não vi exatamente o que estava procurando.
Jay Bazuzi

que lingua voce estava usando?
zotherstupidguy

Basic, Basica, QB, QuickBasic e assim por diante. ;)
Stefan


3

Three Rings oferece bibliotecas Java LGPL. Nenya e Vilya são as bibliotecas para coisas relacionadas a jogos.

Claro, ajudaria se sua pergunta mencionasse restrições de plataforma e / ou idioma que você possa ter.


"Eventualmente, espero construir uma interface de usuário WPF" - isso significa .NET. Pelo menos, pelo que posso dizer.
Mark Allen

Sopa do alfabeto que não conheço.
jmucchiello

Sim, estou usando .NET, mas minha pergunta não é específica para linguagem ou plataforma.
Jay Bazuzi

2

Eu concordo com a resposta de Pyrolistic e prefiro sua maneira de fazer as coisas (no entanto, apenas dei uma olhada nas outras respostas).

Coincidentemente, também usei seu nome "GamePhase". Basicamente, o que eu faria no caso de um jogo de tabuleiro baseado em turnos é que sua classe GameState contivesse um objeto da GamePhase abstrata, conforme mencionado por Pyrolistics.

Digamos que os estados do jogo sejam:

  1. Lista
  2. Mover
  3. Compre / Não compre
  4. Cadeia

Você poderia ter classes derivadas concretas para cada estado. Tenha funções virtuais pelo menos para:

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

Na função StartPhase () você pode definir todos os valores iniciais para um estado, por exemplo, desabilitando a entrada do outro jogador e assim por diante.

Quando roll.EndPhase () é chamado, certifique-se de que o ponteiro GamePhase seja definido para o próximo estado.

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

Neste MovePhase :: StartPhase () você, por exemplo, definiria os movimentos restantes do jogador ativo para a quantidade rolada na fase anterior.

Agora, com esse design implementado, você pode resolver seu problema "3 x double = jail" dentro da fase Roll. A classe RollPhase pode controlar seu próprio estado. Por exemplo

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

Eu difiro do Pirolístico porque deve haver uma fase para tudo, incluindo quando o jogador cai no baú da Comunidade ou algo assim. Eu lidaria com tudo isso no MovePhase. Isso ocorre porque se você tiver muitas fases sequenciais, o jogador provavelmente se sentirá muito "guiado". Por exemplo, se houver uma fase em que o jogador pode SOMENTE comprar propriedades e depois SOMENTE comprar hotéis e SÓ comprar casas, é como se não houvesse liberdade. Basta colocar todas essas partes em uma fase de compra e dar ao jogador a liberdade de comprar o que quiser. A classe BuyPhase pode controlar facilmente quais compras são legais.

Finalmente, vamos abordar o tabuleiro do jogo. Embora um array 2D seja bom, eu recomendo ter um gráfico de ladrilhos (onde um ladrilho é uma posição no tabuleiro). No caso de monopólia, seria preferível uma lista duplamente ligada. Então, cada bloco teria um:

  1. anteriorTile
  2. nextTile

Portanto, seria muito mais fácil fazer algo como:

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

A função AdvanceTo pode controlar suas animações passo a passo ou o que você quiser. E também diminui os movimentos restantes, é claro.

O conselho de RS Conley sobre o padrão do observador para a GUI é bom.

Eu não postei muito antes. Espero que isso ajude alguém.


1

Existe alguma arte anterior da qual eu possa aproveitar?

Se sua pergunta não for específica ao idioma ou à plataforma. então eu recomendo que você considere os padrões AOP para estado, memento, comando, etc.

Qual é a resposta .NET para AOP ??

Tente também encontrar alguns sites interessantes, como http://www.chessbin.com

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.