Conception d'un jeu au tour par tour où les actions ont des effets secondaires

19

J'écris une version informatique du jeu Dominion . Il s'agit d'un jeu de cartes au tour par tour où les cartes d'action, les cartes au trésor et les cartes de points de victoire sont accumulées dans le jeu personnel d'un joueur. J'ai la structure de classe assez bien développée et je commence à concevoir la logique du jeu. J'utilise python, et je peux ajouter une interface graphique simple avec pygame plus tard.

La séquence de tours des joueurs est régie par une machine d'état très simple. Les tours passent dans le sens des aiguilles d'une montre et un joueur ne peut pas quitter le jeu avant la fin. Le jeu d'un seul tour est également une machine d'état; en général, les joueurs passent par une "phase d'action", une "phase d'achat" et une "phase de nettoyage" (dans cet ordre). Basé sur la réponse à la question Comment implémenter le moteur de jeu au tour par tour? , la machine d'état est une technique standard pour cette situation.

Mon problème est que pendant la phase d'action d'un joueur, elle peut utiliser une carte d'action qui a des effets secondaires, soit sur elle-même, soit sur un ou plusieurs des autres joueurs. Par exemple, une carte action permet à un joueur de prendre un deuxième tour immédiatement après la fin du tour en cours. Une autre carte d'action oblige tous les autres joueurs à se défausser de deux cartes de leurs mains. Une autre carte action ne fait rien pour le tour en cours, mais permet à un joueur de piocher des cartes supplémentaires lors de son prochain tour. Pour rendre les choses encore plus compliquées, il y a souvent de nouvelles extensions dans le jeu qui ajoutent de nouvelles cartes. Il me semble que coder en dur les résultats de chaque carte d'action dans la machine d'état du jeu serait à la fois laid et inadaptable. La réponse à la boucle de stratégie au tour par tour n'entre pas dans un niveau de détail qui traite des conceptions pour résoudre ce problème.

Quel type de modèle de programmation dois-je utiliser pour englober le fait que le schéma général de la rotation peut être modifié par des actions qui ont lieu pendant la rotation? L'objet de jeu doit-il garder une trace des effets de chaque carte action? Ou, si les cartes doivent implémenter leurs propres effets (par exemple en implémentant une interface), quelle configuration est requise pour leur donner suffisamment de puissance? J'ai pensé à quelques solutions à ce problème, mais je me demande s'il existe un moyen standard de le résoudre. Plus précisément, je voudrais savoir quel objet / classe / quoi que ce soit qui est responsable du suivi des actions que chaque joueur doit faire à la suite de la lecture d'une carte d'action, et aussi comment cela se rapporte aux changements temporaires dans la séquence normale de la machine d'état de virage.

Apis Utilis
la source
2
Bonjour Apis Utilis et bienvenue à GDSE. Votre question est bien écrite et c'est formidable que vous ayez référencé les questions connexes. Cependant, votre question couvre de nombreux problèmes différents, et pour la couvrir entièrement, une question devrait probablement être énorme. Vous pouvez toujours obtenir une bonne réponse, mais vous-même et le site en bénéficierez si vous décomposez un peu plus votre problème. Peut-être commencer par construire un jeu plus simple et passer à Dominion?
michael.bartnett
1
Je commencerais par donner à chaque carte un script qui modifie l'état du jeu, et si rien de bizarre ne se passe, retomber sur les règles de tour par défaut ...
Jari Komppa

Réponses:

11

Je suis d'accord avec Jari Komppa que la définition d'effets de carte avec un langage de script puissant est la voie à suivre. Mais je crois que la clé d'une flexibilité maximale est la gestion des événements par script.

Afin de permettre aux cartes d'interagir avec des événements de jeu ultérieurs, vous pouvez ajouter une API de script pour ajouter des "crochets de script" à certains événements, comme les débuts et les fins des phases de jeu, ou certaines actions que les joueurs peuvent effectuer. Cela signifie que le script qui est exécuté lorsqu'une carte est jouée est capable d'enregistrer une fonction qui est appelée la prochaine fois qu'une phase spécifique est atteinte. Le nombre de fonctions pouvant être enregistrées pour chaque événement doit être illimité. Lorsqu'il y en a plus d'un, ils sont ensuite appelés dans leur ordre d'inscription (à moins bien sûr qu'il y ait une règle de jeu de base qui dit quelque chose de différent).

Il devrait être possible d'enregistrer ces crochets pour tous les joueurs ou pour certains joueurs uniquement. Je suggérerais également d'ajouter la possibilité pour les hameçons de décider eux-mêmes s'ils devraient continuer à être appelés ou non. Dans ces exemples, la valeur de retour de la fonction hook (true ou false) est utilisée pour exprimer cela.

Votre carte à double tour ferait alors quelque chose comme ceci:

add_event_hook('cleanup_phase_end', current_player, function {
     setNextPlayer(current_player); // make the player take another turn
     return false; // unregister this hook afterwards
});

(Je n'ai aucune idée si Dominion a même quelque chose comme une "phase de nettoyage" - dans cet exemple, c'est la dernière phase hypothétique du tour des joueurs)

Une carte qui permet à chaque joueur de piocher une carte supplémentaire au début de sa phase de pioche ressemblerait à ceci:

add_event_hook('draw_phase_begin', NULL, function {
    drawCard(current_player); // draw a card
    return true; // keep doing this until the hook is removed explicitely
});

Une carte qui fait perdre au joueur ciblé un point de vie à chaque fois qu'il joue une carte ressemblerait à ceci:

add_event_hook('play_card', target_player, function {
    changeHitPoints(target_player, -1); // remove a hit point
    return true; 
});

Vous ne pourrez pas contourner le codage en dur de certaines actions de jeu, comme dessiner des cartes ou perdre des points de vie, car leur définition complète - ce que signifie exactement "dessiner une carte" - fait partie des mécanismes de base du jeu. Par exemple, je connais certains TCG où lorsque vous devez piocher une carte pour une raison quelconque et que votre deck est vide, vous perdez la partie. Cette règle n'est pas imprimée sur chaque carte, ce qui vous fait piocher des cartes, car elle est dans le livre de règles. Donc, vous ne devriez pas non plus avoir à vérifier cette condition de perte dans le script de chaque carte. La vérification de telles choses devrait faire partie de la drawCard()fonction codée en dur (qui, soit dit en passant, serait également un bon candidat pour un événement raccordable).

Soit dit en passant: il est peu probable que vous puissiez planifier à l'avance pour chaque mécanisme obscur que les futures éditions pourraient proposer , donc quoi que vous fassiez, vous devrez toujours ajouter de nouvelles fonctionnalités pour les futures éditions de temps en temps (dans ce cas, un mini-jeu de lancer de confettis).

Philipp
la source
1
Sensationnel. Ce truc de confettis du chaos.
Jari Komppa
Excellente réponse, @Philipp, et cela s'occupe de beaucoup de choses faites dans Dominion. Cependant, certaines actions doivent se produire immédiatement lorsqu'une carte est jouée, c'est-à-dire qu'une carte est jouée qui oblige un autre joueur à retourner la carte du dessus de sa bibliothèque et permettant au joueur actuel de dire "Gardez-la" ou "Défaussez-la". Souhaitez-vous écrire des hooks d'événement pour prendre en charge de telles actions immédiates, ou auriez-vous besoin de trouver des méthodes supplémentaires de scripting des cartes?
fnord
2
Lorsque quelque chose doit se produire immédiatement, le script doit appeler directement les fonctions appropriées et ne pas enregistrer une fonction de raccordement.
Philipp
@JariKomppa: L'ensemble Unglued était délibérément absurde et plein de cartes folles qui n'avaient aucun sens. Mon préféré était une carte qui faisait que tout le monde prenait un point de dégâts quand ils disaient un mot particulier. J'ai choisi «le».
Jack Aidley
9

J'ai donné ce problème - moteur de jeu de cartes informatisé flexible - certains ont pensé il y a quelque temps.

Tout d'abord, un jeu de cartes complexe comme Chez Geek ou Fluxx (et, je crois, Dominion) exigerait que les cartes soient scriptables. Fondamentalement, chaque carte est livrée avec son propre groupe de scripts qui peuvent changer l'état du jeu de diverses manières. Cela vous permettrait de donner au système une certaine pérennité, car les scripts pourraient peut-être faire des choses auxquelles vous ne pouvez pas penser en ce moment, mais pourraient venir dans une future extension.

Deuxièmement, le «virage» rigide peut causer des problèmes.

Vous avez besoin d'une sorte de "pile de tours" qui contient les "tours spéciaux", comme "défaussez 2 cartes". Lorsque la pile est vide, le tour normal par défaut continue.

Dans Fluxx, il est tout à fait possible qu'un tour se passe comme:

  • Choisissez N cartes (comme indiqué par les règles actuelles, modifiables via les cartes)
  • Jouez N cartes (comme indiqué par les règles actuelles, modifiables via les cartes)
    • L'une des cartes peut être "en prendre 3, en jouer 2"
      • Une de ces cartes pourrait bien être "prendre un autre tour"
    • L'une des cartes peut être «défausse et pioche»
  • Si vous changez les règles pour choisir plus de cartes que vous n'en aviez lors de votre tour, choisissez plus de cartes
  • Si vous modifiez les règles pour moins de cartes en main, tout le monde doit immédiatement défausser les cartes
  • Lorsque votre tour se termine, défaussez-vous des cartes jusqu'à ce que vous ayez N cartes (modifiables via les cartes, encore), puis prenez un autre tour (si vous avez joué la carte "prenez un autre tour" dans le désordre ci-dessus).

..et ainsi de suite. La conception d'une structure de virage capable de gérer les abus ci-dessus peut donc être assez délicate. Ajoutez à cela les nombreux jeux avec des cartes "à chaque fois" (comme dans "chez geek") où les cartes "à chaque fois" peuvent perturber le flux normal en annulant, par exemple, la dernière carte jouée.

Donc, fondamentalement, je commencerais par concevoir une structure de tour très flexible, la concevoir de manière à ce qu'elle puisse être décrite comme un script (car chaque jeu aurait besoin de son propre "script maître" gérant la structure de base du jeu). Ensuite, toute carte doit être scriptable; la plupart des cartes ne font probablement rien d'étrange, mais d'autres le font. Les cartes peuvent également avoir différents attributs - si elles peuvent être gardées en main, jouées "à chaque fois", si elles peuvent être stockées comme des actifs (comme les "gardiens" de Fluxx, ou diverses choses dans "chez geek" comme la nourriture) ...

Je n'ai jamais réellement commencé à implémenter tout cela, donc dans la pratique, vous pouvez trouver beaucoup d'autres défis. La façon la plus simple de commencer serait de commencer par tout ce que vous savez du système que vous souhaitez implémenter, et de les implémenter de manière scriptable, en définissant le moins de pierre possible, donc quand une extension arrive, vous n'aurez pas besoin de réviser le système de base - beaucoup. =)

Jari Komppa
la source
C'est une excellente réponse, et j'aurais accepté les deux si j'avais pu. J'ai rompu le lien en acceptant la réponse de la personne de moindre réputation :)
Apis Utilis
Pas de problème, j'y suis déjà habitué .. =)
Jari Komppa
0

Hearthstone semble faire les choses en toute honnêteté et honnêtement, je pense que la meilleure façon d'atteindre la flexibilité est d'utiliser un moteur ECS avec une conception orientée données. J'ai essayé de faire un clone de foyer et c'est impossible autrement. Tous les cas de bord. Si vous faites face à beaucoup de ces cas bizarres, c'est probablement la meilleure façon de procéder. Je suis assez partisan cependant de l'expérience récente d'essayer cette technique.

Edit: ECS pourrait même ne pas être nécessaire selon le type de flexibilité et d'optimisation que vous souhaitez. Ce n'est qu'une façon d'y parvenir. DOD J'ai pensé à tort comme une programmation procédurale, bien qu'ils se rapportent beaucoup. Ce que je veux dire est. Que vous devriez envisager de supprimer totalement ou principalement la POO et de concentrer plutôt votre attention sur les données et la façon dont elles sont organisées. Évitez l'héritage et les méthodes. Concentrez-vous plutôt sur les fonctions publiques (systèmes) pour manipuler les données de votre carte. Chaque action n'est pas un modèle ou une logique quelconque, mais plutôt des données brutes. Où vos systèmes l'utilisent ensuite pour exécuter la logique. Le boîtier de commutation entier ou l'utilisation d'un entier pour accéder à un tableau de pointeurs de fonction aide à déterminer efficacement la logique souhaitée à partir des données d'entrée.

Les règles de base à suivre sont que vous devez éviter de lier la logique directement avec les données, vous devez éviter de faire en sorte que les données dépendent les unes des autres autant que possible (des exceptions peuvent s'appliquer), et cela lorsque vous voulez une logique flexible qui semble hors de portée ... Pensez à le convertir en données.

Il y a des avantages à le faire. Chaque carte peut avoir une valeur ou une chaîne d'énumération (s) pour représenter ses actions. Ce stagiaire vous permet de concevoir des cartes via des fichiers texte ou json et permet au programme de les importer automatiquement. Si vous faites des actions des joueurs une liste de données, cela donne encore plus de flexibilité, surtout si une carte dépend de la logique passée comme le fait Hearthstone, ou si vous souhaitez enregistrer le jeu ou une relecture d'un jeu à tout moment. Il est possible de créer plus facilement l'IA. Surtout lors de l'utilisation d'un "système utilitaire" au lieu d'un "arbre de comportement". La mise en réseau devient également plus facile, car au lieu d'avoir à trouver comment transférer des objets entiers éventuellement polymorphes sur le câble et comment la sérialisation serait configurée après coup, vous avez déjà vos objets de jeu ne sont rien de plus que de simples données qui finissent par être très faciles à déplacer. Et enfin et surtout, cela vous permet d'optimiser plus facilement, car au lieu de perdre du temps à vous soucier du code, vous pouvez mieux organiser vos données afin que le processeur ait plus de facilité à les traiter. Python peut avoir des problèmes ici, mais recherchez la "ligne de cache" et sa relation avec le développement du jeu. Ce n'est peut-être pas important pour le prototypage, mais en fin de compte, cela vous sera très utile.

Quelques liens utiles.

Remarque: ECS permet d'ajouter / supprimer dynamiquement des variables (appelées composants) au moment de l'exécution. Un exemple de programme c sur la façon dont ECS "pourrait" ressembler (il y a une tonne de façons de le faire).

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

Crée un tas d'entités avec des données de texture et de position et à la fin détruit une entité qui a un composant de texture qui se trouve être au troisième index du tableau de composants de texture. Semble excentrique, mais c'est une façon de faire les choses. Voici un exemple de la façon dont vous rendriez tout ce qui a un composant de texture.

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}
Blue_Pyro
la source
1
Cette réponse serait meilleure si elle expliquait plus en détail comment vous recommanderiez de configurer votre ECS orienté données et de l'appliquer pour résoudre ce problème spécifique.
DMGregory
Merci de l'avoir signalé.
Blue_Pyro
En général, je pense qu'il est mauvais de dire à quelqu'un "comment" configurer ce type d'approche, mais plutôt de le laisser concevoir sa propre solution. Il s'avère être à la fois une bonne façon de pratiquer et permet une solution potentiellement meilleure au problème. En pensant aux données plus qu'à la logique de cette manière, cela finit par être qu'il y a beaucoup de façons d'accomplir la même chose et tout dépend des besoins de l'application. Ainsi que le temps / connaissances du programmeur.
Blue_Pyro