Estou escrevendo uma versão em computador do jogo Dominion . É um jogo de cartas baseado em turnos, onde cartas de ação, cartas de tesouro e cartas de pontos de vitória são acumuladas no baralho pessoal de um jogador. Eu tenho a estrutura de classes muito bem desenvolvida e estou começando a projetar a lógica do jogo. Estou usando python, e posso adicionar uma GUI simples com pygame mais tarde.
A sequência de turnos dos jogadores é governada por uma máquina de estado muito simples. Os turnos passam no sentido horário e um jogador não pode sair do jogo antes que ele termine. A jogada de um único turno também é uma máquina de estado; em geral, os jogadores passam por uma "fase de ação", uma "fase de compra" e uma "fase de limpeza" (nessa ordem). Com base na resposta à pergunta Como implementar o mecanismo de jogo baseado em turnos? , a máquina de estado é uma técnica padrão para essa situação.
Meu problema é que, durante a fase de ação de um jogador, ele pode usar uma carta de ação que tenha efeitos colaterais, seja em si mesma ou em um ou mais dos outros jogadores. Por exemplo, uma carta de ação permite que um jogador faça um segundo turno imediatamente após a conclusão do turno atual. Outra carta de ação faz com que todos os outros jogadores descartem duas cartas de suas mãos. Ainda outra carta de ação não faz nada no turno atual, mas permite que um jogador compre cartas extras no próximo turno. Para tornar as coisas ainda mais complicadas, frequentemente existem novas expansões no jogo que adicionam novas cartas. Parece-me que codificar os resultados de todas as cartas de ação na máquina de estado do jogo seria feio e não adaptável. A resposta ao loop estratégico baseado em turnos não entra em um nível de detalhe que aborda os projetos para resolver esse problema.
Que tipo de modelo de programação devo usar para abranger o fato de que o padrão geral de revezamento pode ser modificado por ações que ocorrem dentro do turno? O objeto do jogo deve acompanhar os efeitos de cada carta de ação? Ou, se os cartões implementarem seus próprios efeitos (por exemplo, implementando uma interface), que configuração é necessária para fornecer energia suficiente? Eu pensei em algumas soluções para esse problema, mas estou me perguntando se existe uma maneira padrão de resolvê-lo. Especificamente, eu gostaria de saber qual objeto / classe / o que é responsável por acompanhar as ações que todo jogador deve executar como consequência de uma carta de ação ser jogada, e também como isso se relaciona a mudanças temporárias na sequência normal de a máquina de estado de virada.
fonte
Respostas:
Concordo com Jari Komppa que definir efeitos de cartão com uma linguagem de script poderosa é o caminho a percorrer. Mas acredito que a chave para a máxima flexibilidade é a manipulação de eventos com script.
Para permitir que os cartões interajam com eventos posteriores do jogo, você pode adicionar uma API de script para adicionar "ganchos de script" a determinados eventos, como o início e o final das fases do jogo ou determinadas ações que os jogadores podem executar. Isso significa que o script que é executado quando uma carta é jogada é capaz de registrar uma função que é chamada na próxima vez que uma fase específica for atingida. O número de funções que podem ser registradas para cada evento deve ser ilimitado. Quando houver mais de um, eles serão chamados em sua ordem de registro (a menos que haja uma regra básica do jogo que diga algo diferente).
Deveria ser possível registrar esses ganchos para todos os jogadores ou apenas para determinados jogadores. Eu também sugeriria adicionar a possibilidade de os ganchos decidirem por si mesmos se devem continuar sendo chamados ou não. Nestes exemplos, o valor de retorno da função hook (true ou false) é usado para expressar isso.
Seu cartão de volta dupla faria algo assim:
(Eu não tenho idéia se Dominion tem algo como uma "fase de limpeza" - neste exemplo, é a última fase hipotética da vez dos jogadores)
Uma carta que permita a cada jogador comprar uma carta adicional no início de sua fase de compra ficaria assim:
Uma carta que faz com que o jogador alvo perca um ponto de vida sempre que jogar uma carta seria assim:
Você não terá que codificar algumas ações do jogo, como comprar cartas ou perder pontos de vida, porque sua definição completa - o que exatamente significa "comprar uma carta" - faz parte da mecânica principal do jogo. Por exemplo, eu conheço alguns TCGs em que quando você precisa comprar uma carta por qualquer motivo e seu baralho estiver vazio, você perde o jogo. Essa regra não é impressa em todos os cartões, o que faz você comprar cartões, porque está no livro de regras. Portanto, você também não deve procurar essa condição perdida no script de todos os cartões. Verificar coisas assim deve fazer parte do código embutido
drawCard()
função (que, a propósito, também seria um bom candidato para um evento que pode ser conectado).A propósito: É improvável que você consiga planejar com antecedência todas as futuras edições obscuras de mecânicos que possam surgir ; portanto, não importa o que faça, você ainda precisará adicionar novas funcionalidades para futuras edições de vez em quando (neste caso, um minijogo de confete).
fonte
Eu dei esse problema - mecanismo de jogo de cartas computadorizado flexível - alguns pensaram há algum tempo.
Primeiro, um jogo de cartas complexo como Chez Geek ou Fluxx (e, acredito, Dominion) exigiria que os cartões fossem escrituráveis. Basicamente, cada cartão viria com seu próprio monte de scripts que podem mudar o estado do jogo de várias maneiras. Isso permitiria que o sistema fosse preparado para o futuro, pois os scripts podem fazer coisas que você não consegue pensar agora, mas podem vir em uma expansão futura.
Segundo, a rígida "curva" pode estar causando problemas.
Você precisa de algum tipo de "turn stack" que contenha "turnos especiais", como "descartar 2 cartas". Quando a pilha está vazia, a volta normal padrão continua.
No Fluxx, é perfeitamente possível que um turno seja algo como:
..e assim por diante. Portanto, projetar uma estrutura de curvas que possa lidar com os abusos acima pode ser bastante complicado. Acrescente a isso os inúmeros jogos com cartões "sempre" (como em "chez geek"), onde os cartões "sempre" podem atrapalhar o fluxo normal, por exemplo, cancelando o que foi jogado pela última vez.
Então, basicamente, eu começaria projetando uma estrutura de turnos muito flexível, projetando-a para que ela possa ser descrita como um script (já que cada jogo precisaria de seu próprio "script mestre" para lidar com a estrutura básica do jogo). Então, qualquer cartão deve ser programável; a maioria das cartas provavelmente não faz nada de estranho, mas outras fazem. As cartas também podem ter vários atributos - se elas podem ser mantidas em mãos, jogadas "sempre", se podem ser armazenadas como ativos (como 'keepers' do fluxx, ou várias coisas no 'chez geek' como comida) ...
Na verdade, nunca comecei a implementar nada disso; portanto, na prática, você pode encontrar muitos outros desafios. A maneira mais fácil de começar seria começar com o que você sabe sobre o sistema que deseja implementar e implementá-los de maneira programável, definindo o mínimo possível de pedras. Portanto, quando uma expansão ocorrer, você não precisará revisar o sistema básico - muito. =)
fonte
Hearthstone parece fazer coisas relacionáveis e, honestamente, acho que a melhor maneira de obter flexibilidade é através de um mecanismo ECS com um design orientado a dados. Tentou criar um clone de Hearthstone e, de outra forma, é impossível. Todos os casos extremos. Se você está enfrentando muitos desses casos estranhos, provavelmente essa é a melhor maneira de fazer isso. Sou bastante tendencioso, apesar da experiência recente de experimentar essa técnica.
Edit: ECS pode até não ser necessário, dependendo do tipo de flexibilidade e otimização que você deseja. É apenas uma maneira de conseguir isso. DOD eu pensei erroneamente como programação procedural, embora eles se relacionem muito. O que eu quero dizer é. Que você deve considerar acabar com o OOP total ou principalmente pelo menos e, em vez disso, concentrar sua atenção nos dados e em como eles são organizados. Evite herança e métodos. Em vez disso, concentre-se em funções públicas (sistemas) para manipular os dados do seu cartão. Cada ação não é uma coisa ou lógica modelada de qualquer tipo, mas sim dados brutos. Onde seus sistemas o usam para executar a lógica. A caixa de chave inteira ou o uso de um número inteiro para acessar uma matriz de ponteiros de função ajudam a descobrir com eficiência a lógica desejada dos dados de entrada.
As regras básicas a seguir são: você deve evitar vincular a lógica diretamente aos dados, evitar que os dados dependam um do outro o máximo possível (exceções podem ser aplicadas) e que quando você deseja uma lógica flexível que pareça inacessível ... Considere convertê-lo em dados.
Há benefícios a serem feitos ao fazer isso. Cada carta pode ter um valor de enumeração ou sequência (s) para representar sua (s) ação (ões). Esse estagiário permite criar cartões por meio de arquivos de texto ou json e permitir que o programa os importe automaticamente. Se você faz das ações dos jogadores uma lista de dados, isso oferece ainda mais flexibilidade, especialmente se um cartão depende da lógica do passado, como o Hearthstone, ou se você deseja salvar o jogo ou a repetição de um jogo a qualquer momento. Existe potencial para criar IA mais facilmente. Especialmente ao usar um "sistema utilitário" em vez de uma "árvore de comportamento". A rede também se torna mais fácil porque, em vez de precisar descobrir como transferir objetos inteiros, possivelmente polimórficos, através da conexão e como a serialização seria configurada após o fato, o fato de você já ter seus objetos de jogo não passa de dados simples, o que acaba sendo muito fácil de se movimentar. E por último, mas definitivamente não menos importante, isso permite que você otimize mais facilmente porque, em vez de perder tempo se preocupando com o código, é capaz de organizar melhor seus dados, para que o processador tenha mais facilidade em lidar com eles. O Python pode ter problemas aqui, mas consulte "linha de cache" e como isso se relaciona com o desenvolvedor do jogo. Talvez não seja importante para a criação de protótipos, mas, no futuro, será muito útil.
Alguns links úteis.
Nota: O ECS permite adicionar / remover dinamicamente variáveis (chamadas componentes) em tempo de execução. Um exemplo de programa c de como o ECS "pode" parecer (há várias maneiras de fazê-lo).
Cria um monte de entidades com dados de textura e posição e, no final, destrói uma entidade que possui um componente de textura que está no terceiro índice da matriz de componentes de textura. Parece peculiar, mas é uma maneira de fazer as coisas. Aqui está um exemplo de como você renderizaria tudo o que possui um componente de textura.
fonte