Aller au contenu principal

TP2 (Suite de Super Cartes Infinies)

Le projet​

Ajouter des fonctionnalités au jeu de cartes développé lors du Sprint 1.

info

Vous devez choisir une des tâches suivantes qui sera évaluée de façon individuelle. Vous devez écrire vous-même le code, mais vous pouvez collaborer avec vos collègues.

attention

Si vous avez choisi la tâche MVC pour le TP1, vous ne pouvez pas choisir la tâche MVC pour le TP2.

Les tâches individuelles​

  • [Obligatoire] Simulation
  • [Obligatoire] MVC - Pouvoirs et configurations de simulation
  • [Obligatoire] Logique de jeu
  • Rareté et achat de paquets de cartes

Contraintes​

  • Le travail doit être effectué en équipes de 3 ou 4.
  • Vous devez utiliser Git/GitHub.
  • Vous devez utiliser DevOps pour la gestion des tâches ET utiliser les sprints.

Les étapes et évaluations​

Il y a 2 évaluations, mais c'est votre responsabilité de terminer votre tâches individuelle au moins une semaine avant la remise d'équipe pour donner du temps à votre équipe pour terminer la partie en équipe:

  • Premier livrable d'équipe (Première évaluation)
  • Fonctionnalités individuelles
  • Merge et dernière fonctionnalité (Deuxième évaluation)

Premier livrable d'équipe​

  • Planification du travail dans Azure DevOps (Évalué individuellement)
  • Diagramme de classes
  • Changements au modèle de données (pour satisfaire à toutes les tâches individuelles de votre équipe)
  • Pouvoir jouer une carte (Les cartes jouées sont sur le BattleField, mais ne font rien pour l'instant)
  • Faire un seed avec les configurations de simulations de base et ajouter une méthode WebAPI qui retourne les configurations (voir plus bas)
  • Ajout des classes suivantes (entre autres) pour faire l'ajout de powers aux cartes et la logique de jeu

La classe Power

public class Power
{
public const int FIRST_STRIKE_ID = 1;
public const int THORNS_ID = 2;
public const int HEAL_ID = 3;

public int Id { get; set; }

// TODO: À compléter
}

La classe CardPower

public class CardPower
{
public int Id { get; set; }
public int CardId { get; set; }
public virtual Card Card { get; set; }
public int PowerId { get; set; }
public virtual Power Power { get; set; }
public int Value { get; set; }
}

Ajouter cette méthode à la classe Card

[ValidateNever]
public virtual List<CardPower> CardPowers { get; set; }

Ajouter les méthodes suivantes à la classe PlayableCard (Voir le code fournit dans la section "Logique de jeu")

public bool HasPower(int powerId)
{
// Retourne true si la carte possède ce pouvoir.
}
public int GetPowerValue(int powerId)
{
// Retourne les valeur du pouvoir pour cette carte.
// Simplement retourner 0 si la carte ne possède pas ce pouvoir.
}

Ajout du projet de tests unitaire TDD (Test Driven Development)​

  • Pour que les tests fournis compilent, il faut avoir déjà ajouté les classes nécessaires qui sont mentionnées plus haut.
  • Il faut extraire le contenu du fichier .zip dans votre solution et ensuite ajouter le projet à votre solution:
Détails

alt text

attention

Ces tests ne vont pas passer pour l'instant (C'est le travail d'un membre de votre équipe pour ce TP), mais ils devraient au moins compiler.

Ajouter vos propres tests​

  • Ajouter des tests unitaires pour vérifier que ces nouvelles méthodes fonctionnent correctement (Cette partie est un travail pour toute l'équipe!)
    • Un test qui vérifie que HasPower retourne false si un AUTRE power est présent sur la carte
    • Un test qui vérifie que GetPowerValue retourne 0 si un AUTRE power est présent sur la carte
    • Un test qui vérifie que HasPower retourne true si un power avec le même Id est présent sur la carte
    • Un test qui vérifie que GetPowerValue retourne la bonne valeur si un power avec le bon Id et une valeur est présent sur la carte
info

Pour l'évaluation, le projet de tests doit compiler et les tests que vous avez ajoutés pour les pouvoirs doivent bien vérifier le fonctionnement de ces méthodes et passer avec succès.

Gérer le PlayCardEvent

public class PlayCardEvent : MatchEvent
{
// TODO: Ajouter tout ce qui manque
public PlayCardEvent(MatchPlayerData currentPlayerData, int playableCardId)
{
}
}
  • Il faut ajouter une méthode PlayCard dans MatchService, qui va générer un PlayCardEvent.
  • Il faut ajouter une action PlayCard sur le Hub. Cette méthode va appeler PlayCard du MatchService et faire un ApplyEvents, comme les autres actions du Hub.

Configurations de simulation du seed​

Une fois que vous avez créer les classes nécessaires pour configurer un match de test. Regardez la section MVC pour voir à quoi le menu de configuration va ressembler.

Vous devez ajouter les 4 configurations de simulation de base suivante:

Victoire:

  • Le joueur B a seulement 1 de Health
  • Le joueur A a déjà une carte sur le BattleField
  • Le joueur A est le premier joueur
info

De cette façon, on gagne en cliquant simplement sur "Terminer"

Défaite:

  • Le joueur A a seulement 1 de Health
  • Le joueur B a déjà une carte sur le BattleField
  • Le joueur B est le premier joueur
info

De cette façon, on perd sans même avoir besoin de cliquer (c'est le tour du joueur B)

Partie Normale victoire A:

  • Les deux joueurs ont 20 de Health
  • Le joueur A a 20 Mana (Pour lui permettre de joueur de meilleurs cartes)
  • Chaque joueur a au moins une carte dans sa main et une dans son paquet (on veut voir que le bot peut jouer des cartes)

Partie Normale victoire B:

  • Les deux joueurs ont 20 de Health
  • Le joueur B a 20 Mana (Pour lui permettre de joueur de meilleurs cartes)
  • Chaque joueur a au moins une carte dans sa main et une dans son paquet (on veut voir que le bot peut jouer des cartes)

Tâches individuelles​

Simulation

Un des secrets pour qu'un projet ait du succès, c'est de développer les bons outils pour être capable de travailler et tester efficacement. On va donc mettre en place un système de test qui va nous permettre de tester facilement le jeu.

Sur le client on va pouvoir taper: "adminmode" sur la page d'accueil pour exécuter un méthode qui va ajouter une entrée dans le localStorage pour afficher les options de test.

alt text

Tester une partie:

  • On veut pouvoir voir la liste des tests sur le client et pouvoir les exécuter

alt text

  • Une fois que l'on clique sur un test, on va démarrer une partie et jouer normalement (voir les détails plus bas)
  • On va devoir gérer la partie pour les deux joueurs sur le même client:
    • La gestion du joueurA qui existe déjà et qui permet à un joueur de jouer les cartes cliqués et de terminer son tour ou abandonner
    • La gestion du joueurB qui va utiliser un mini bot qui va être exécuté par le client et qui va jouer chaque fois que c'est son tour (détails plus bas)

Logique du bot:

  • Le bot est très simple, lorsque c'est son tour il va vérifier s'il a assez de mana pour jouer la première carte dans ses mains
    • Si oui, il va la jouer et terminer son tour
    • Si non, il termine son tour

Comment faire pour que l'on puisse utiliser la fonctionnalité et créer des parties utilisables par un bot?

  • Ajouter une méthode CreateTestMatch au Hub et au MatchesServices qui va créer deux Players de test avec leur propre IdentyUser
    • Il faut utiliser l'information du TestMatch pour créer les players et modifier le match
  • Ajouter une méthode JoinTestMatch au Hub qui va prendre le matchId et UN playerId (elle va être appelé par le joueur et le bot)
  • CreateTestMatch retourne les playerIds et le matchId au client pour qu'ils puissent se rendre à la page de match avec cette information (le plus simple c'est de le mettre dans l'url pour passer l'information au match)
  • Sur la page de match on va maintenant avoir un comportement différent dans ce cas et appeler JoinTestMatch pour le joueur et pour le bot
  • Les méthodes Surrender, EndTurn et PlayCard (Du Hub et du service) vont maintenant devoir prendre un testPlayerId en paramètres car ce player n'est pas associé à la session!
attention

Ajoutez également un bool IsTest à la classe Match pour faire la différence entre les deux types de match. Lorsque l'on reçoit un appel avec un playerId qui n'est pas celui du User normale, on doit uniquement le permettre pour un match de test. Vous pouvez envoyer 0 comme testPlayerId pour les matches normaux pour simplifier la logique. (Les hubs ne supportent pas de ne pas passer un paramètre optionnel!)

  • Écrire au moins les deux tests unitaires suivants pour vérifier la méthode CreateTestMatch:
    • Créer un match avec un TestMatch qui fait gagner le joueur A et vérifier que le match se termine de la bonne façon
    • Même chose avec le test de défaite (Donc le joueur B gagne)
info

Vous devez faire un test d'intégration qui fait également un JoinTestMatch et un EndTurn pour vérifier que le match est bien terminé et que le bon joueur a gagné.

MVC - Ajout des pouvoirs et configuration des simulations

Partie .Net:

  • Les cartes peuvent avoir un certains nombres de pouvoirs.

  • Un pouvoir a un nom, une description et un icône.

  • Quand une carte possède un pouvoir, il peut y avoir une valeur entière en plus de la relation (Voir les pouvoirs dans les règles de jeu)

  • Utiliser les Ids de pouvoirs de la classe Power dans votre seed (Comme HEAL_ID)

  • Pouvoir associer les pouvoirs aux cartes dans le menu Edit (MVC)

  • Voir la liste des pouvoirs d'une carte et pouvoir les retirer un à un

  • Pouvoir ajouter un nouveau pouvoir avec une valeur entière

  • Afficher les pouvoirs (nom et valeur) des cartes dans l'index de cartes dans une nouvelle colonne

  • Ajouter les pouvoirs dans le seed. Chaque pouvoir doit être sur au moins une des cartes que le joueur possède par défaut.

  • Il faut ÉGALEMENT ajouter une configuration MVC avec un nouveau modèle de données qui permet de configurer une partie de test. Regardez avec vos collègues pour définir le modèle de données! Il n'est pas si simple car on peut également configurer des cartes pour être dans les mains, le battlefield et le paquet de cartes (pour être pigé).

  • Voici à quoi devra ressembler la configuration pour les parties Index:

alt text

Création et modification:

alt text

info

Dans cette exemple on peut voir que le test est utilse pour tester rapidement une défaite. Le joueur B commence et le joueurA (celui que l'on représente) a seulement 1 pv. En plus le joueur B a déjà une carte en jeu sur le BattleField. Le bot va donc nous attaquer et on va perdre. (Voir avec votre coéquipier pour bien comprendre le feature)

Partie React:

  • Voir les pouvoirs avec leur valeur sur les cartes sur le client, dans tous les endroits (Mes cartes, magasin, pendant une partie, etc)
  • Animer les pouvoirs en cliquant sur une carte dans Mes Cartes (Afficher l'icône du pouvoir sur la carte pendant 2 secondes)
  • Afficher les pouvoirs un après l'autre si il y en a plusieurs
  • Note: On fait ça pour tester les animations et être prêt à les afficher pendant un match
Logique de jeu
  • Faire passer les tests de TDD (Voir les règles de jeu)

  • IMPORTANT: Il faut ajouter un pouvoir de votre choix. Si vous n'êtes pas certain ou si vous manquez d'inspiration, demandez à votre enseignant!

  • Il faut écrire des tests pour le pouvoir au choix et ils doivent également passer avec succès

  • IMPORTANT: En écrivant la logique de jeu, il faut utiliser des MatchEvents. Ces MatchsEvents vont être utilisés pour rejouer les changements sur le client.

  • Il faut donc au moint un MatchEvent pour chaque pouvoir, mais également un lorsqu'une carte attaque, reçoit des dégâts ou meurt.

  • Conseil: Ajoutez également un CardActivationEvent, ce sera un bon endroit pour gérer le combat et vérifier les pouvoirs d'une carte et les déclencher

  • Conseil: Utilisez les méthodes HasPower et GetPowerValue que vous avez ajouté à PlayableCard pour écrire la logique de jeu.

  • Le diagramme suivant donne une façon de faire pour gérer les events lors d'un combat. Le CardActivationEvent est un bon event pour inclure la logique de certains pouvoirs, comme Heal.

  • En utilisant également un AttackEvent, on peut bien séparer les pouvoirs de la gestion des dégâts

Achat de paquets de cartes (Pour les équipes de 4)

Résumé: Les joueurs peuvent acheter des paquets de cartes (Pack) avec une monnaie virtuelle dans le jeu.

  • Ajouter une rareté aux cartes. Il doit y avoir 4 niveaux: (Utilisez un enum)
    • Commune (Gris)
    • Rare (Vert)
    • Épique (Mauve)
    • Légendaire (Orange)
  • Ajouter la possibilité de voir et changer la rareté d’une carte en MVC.
  • Modifier le seed des cartes avec au moins 2 cartes pour chaque rareté.
  • Sur le client, il faut afficher un code de couleur sur les cartes pour pouvoir voir leur rareté. Une option simple c’est de modifier la couleur de fond du titre.
  • Tâche MVC de configuration de la monnaie virtuelle:
    • Reçue à la création du compte
    • Reçue après une victoire
    • Reçue après une défaite (plus petit montant)
  • Sur le serveur
    • Modifier l'event EndMatchEvent pour qu'il modifie la quantité de monnaie des deux joueurs (le gagnant et le perdant)
    • EndMatchEvent devrait également avoir 2 nouvelles propriétés (MoneyReceivedByWinner et MoneyReceivedByLoser ) pour que le client sache combien d'arent son joueur a reçu
  • Sur le client
    • Afficher la monnaie virtuelle du joueur dans la barre de menu au haut de l'écran.
    • Modifier applyEvent dans MatchService pour traiter la nouvelle information à propos de l'argent reçu par EndMatchEvent. (N'oubliez pas que currentPlayerId vous permet de savoir si le joueur est le gagnant ou le perdant)
    • Afficher la monnaie gagné dans le menu de victoire/défaite.
    • Afficher les différents paquets que le joueur peut acheter.
    • Afficher l'ouverture de paquet sur le client avec un dialogue (qui affiche les cartes reçues)
  • Voir la section sur les paquets de cartes
  • Le code serveur doit être mis dans un service que vous devez créer.
  • Ajouter des tests unitaires pour le nouveau service. Avec au minimum les tests suivants:
    • Vérifier que lorsque j'achète un paquet, je reçois le bon nombre de cartes et dépense la bonne quantité d'argent
    • Vérifier que je ne peux pas acheter un paquet si je n'ai pas assez d'argent
    • Vérifier que les cartes obtenus par le deck le plus cher ne sont PAS communes et qu'au moins une carte est épique.
    attention

    C'est assez complexe de tester la rareté des cartes, car il y a un élément aléatoire. On va voir dans une séance future l'utilisation de Mocks pour ce genre de cas. Pour l'instant, ce n'est pas nécessaire de tester la rareté des cartes obtenus aléatoirement.

Merge et dernière étape​

Travail d'équipe
  • Compléter les tâches individuelles au moins une semaine avant la remise

  • Faire les merge vers votre branche commune

    attention

    N'oubliez pas de commencer par faire un merge de la branche commune vers votre branche individuelle pour régler les conflits!

  • Ajoutez une configuration de TestMatch pour chacun de vos pouvoirs pour vous permettre de les tester facilement

  • Ajoutez la gestion des évènements sur le client dans MatchService.

    • Il faut déplacer les cartes vers le BattleField lorsqu'elles sont jouées
    • Les déplacer vers le Graveyard lorsqu'elles meurent
    • Faire bouger une carte lorsqu'elle est activée (faire un mouvement qui montre qu'elle est activée)
    • Afficher l'icône des pouvoirs sur la carte lorsqu'ils sont activés (pour environ 1 seconde)
    • Mettre à jour le health des cartes lorsqu'il change
    • Mettre à jour le health des joueurs lorsqu'il change
  • Ajouter votre pouvoir au choix au seed et l'ajouter sur au moins une carte que le joueur possède par défaut

  • Vous allez probablement avoir un problème avec l'ordre de vos cartes sur le BattleField. Voici de l'aide pour vous aider à le comprendre et le régler: Ordre des éléments d'une liste avec EF

Détails​

Les règles de jeu​

Détails des règles de jeu
  • Mélanger les cartes dans le CardsPile avant de démarrer une partie pour ne pas toujours commencer avec les mêmes cartes.

  • On peut jouer plusieurs cartes par tour, selon leur coût en « Mana ».

    • Chaque carte à un coût en Mana différent.
    • Pendant son tour, un joueur peut jouer une carte si il a assez de mana. On réduit ensuite son mana du coût de la carte.
    • Lorsque le Mana n’est pas utilisé, il est conservé pour le tour d’après.
    • Lorsqu'un joueur clique sur terminer, les cartes qui sont déjà en jeu doivent être "activé" une après l'autre pour faire un combat.
  • Lorsqu'une carte est joué, elle se place à la droite des cartes déjà en jeu.

  • Chaque carte a donc son "index", celle qui est le plus à gauche à l'index 0, la 2e l'index 1, etc.

  • Lorsqu'une carte est activé et qu'il y a une carte devant elle (l'adversaire a une carte avec le même index), les deux cartes se donnent des dégâts.

    • Chaque carte perd du health égal à l'attaque de la carte en face d'elle
  • Lorsqu'une carte est activé et qu'il n'y a pas de carte devant elle, elle blesse l'adversaire. Le Player adverse perd du health de la valeur de l'attaque.

  • Les pouvoirs des cartes affectent les combats

  • Lorsque les cartes sont activées, il faut commencer par la dernière qui a été joué et terminer avec celle qui est en jeu depuis le plus longtemps. On active donc les cartes de droite à gauche. Ce qui veut également dire que l'on commence par la carte avec l'index le plus élevé et que l'on termine avec l'index 0.

    attention

    Pour l'activation des cartes, c'est plus facile d'utiliser une boucle for. Comme c'est possible qu'une carte meurt, la liste de cartes du BattleField risque d'être modifié et on ne peut pas modifier la liste pendant une itération avec un foreach.

Pouvoirs​

Les pouvoirs
  • Il y a 3 pouvoirs (Power) que vous devez ajouter et vous devez en ajouter 1 autre à votre choix.

    • First Strike permet à une carte d’attaquer en « premier » et de ne pas recevoir de dégât si elle tue la carte de l’adversaire. (Fonctionne uniquement à l’attaque, pas à la défense)
    • Thorns X lorsqu’une carte défend, elle inflige X de dégâts AVANT de recevoir des dégâts. Si l’attaquant est tué par ces dégâts, l’attaque s’arrête et le défenseur ne reçoit pas de dégâts.
    • Heal X soigne les cartes alliées de X incluant elle-même AVANT d’attaquer (mais les cartes ne peuvent pas avoir plus de health qu’au départ.)
    attention

    Pour l’instant, les pouvoirs proposés ne nécessitent pas de garder un « état » et c’est conseillé de choisir un pouvoir sans état pour le TP2. Un exemple de pouvoir avec état, c’est « Stun » qui ajoute l'état « Stunned » à une carte. Nous allons ajouter des pouvoir avec état au TP3.

    astuce

    FirstStrikeEvent peut très bien être un évènement qui sert uniquement à afficher sur le client que le pouvoir a eu un effet pendant l'exécution de la logique du combat. (Pour que le joueur puisse comprendre pourquoi l'attaquant n'a pas reçu de dégât). C'est tout à fait correct si il ne contient aucun sous évènement. - Thorns doit contenir un sous event CardDamageEvent - Heal doit contenir un sous event HealCardEvent

Détails sur les paquets de cartes​

  • Ajouter 3 paquets de cartes que les joueurs peuvent acheter. Les cartes sont obtenues au hasard, mais en suivant les règles suivantes.
TypeRareté par défautNb CartesRègles d’obtention des cartes (sinon c’est la rareté par défaut)
BasicCommune330% de rare
NormalCommune41 carte rare. Pour le reste : 30% rare, 10% épique, 2% légendaire
SuperRare51 carte épique. Pour le reste 25% épique, 10% légendaire. (Aucune commune)
  • Les paquets ont un nom, une url d’image et un prix.
  • La configuration des paquets et des probabilités est faite dans un seed, ce n’est pas nécessaire de pouvoir les modifier en MVC.
  • Voir le pseudo code suivant pour vous aider avec la logique d'obtention des cartes
Pseudo code pour la rareté des cartes d'un paquet

// Une Probability possède : une value décimale (entre 0 et 1), une "rarity" et un "baseQty"

// Faire une liste de rareté de carte à obtenir
List<Rarity> GenerateRarities(int nbCards, int defaultRarity, List<Probability> probabilities)
rarities = new List<Rarity>

// Ajouter la quantité de base pour chaque probability à la liste
foreach(probability of probabilities)
for probability.baseQty
add probability.rarity to rarities

// Continuer de remplir la liste jusqu'à atteindre la quantité voulue
while(rarities.Count < nbCards)
rarity = GetRandomRarity(probabilites)

if(rarity == null)
add defaultRarity to rarities
else
add rarity to rarities

return rarities

// Cette méthode permet d'obtenir une rareté au hasard
Rarity? GetRandomRarity(List<Probability> probabilities)
X = Random Number Between 0 and 1

for each rarity of probabilities:
if probability.value < X:
return probability.rarity
else:
X -= probability.value

return null

Une fois que l’on a une liste de rareté, on peut prendre une carte au hasard avec chacune des raretés pour faire notre paquet. Les doublons sont permis.

Grille de correction​

  • 12% de la note pour l’évaluation individuelle (voir le document sur la correction individuelle)
    • 2% pour l'utilisation d'Azure DevOps Boards
    • 10% pour le code et les fonctionnalités
  • 8% de la note pour l’évaluation de groupe
    • 4% pour la première remise
    • 4% pour la remise finale

Référence pour la remise finale en équipe​

Une référence pour voir un client et un serveur fonctionnels.

info

Username: admin@admin.com Le mot de passe: Passw0rd!