Cartouche

Champ Valeur
Auteur·e Élise
Édition 2024-03-04
Durée 2 séances
Taille des équipes 1~2 personnes
login des auteur·rice·s dans author.txt
Rendu via git, dépôt 2023_morpion, branch TD 2, droits en lecture à delivery_collector

Préambule

Notation

5 points par étape

Un rendu sale et du code qui ne respecte pas la norme peut vous faire perdre une partie ou la totalité des points acquis.

Dépôt de référence

Le code source sur lequel vous allez intervenir se trouve ici : https://git.ecole-89.com/eriizu/2023_morpion.

Introduction

Le dépôt qui vous est fournit contient un jeu du morpion fonctionnel. Le jeu peut s’afficher dans une ou plusieurs fenêtres SFML, ainsi que dans le terminal, à condition de changer quelques lignes dans le main.cpp.

Ce code source à plusieurs problèmes. En l’état si vous fermez une des fenêtres graphiques, le jeu ne peut pas s’arrêter et l’autre fenêtre est bloquée. C’est pas super comme comportement.

De la même façon : si on essaie de fermer une fenêtre dont ça n’est pas le tour de jouer : ça ne marche pas tant que l’autres personne n’a pas jouer (la faisant jouer inutilement.)

Ces problèmes sont dû au fait que chaque fenêtre bloque tant que l’utilisateur n’a pas joué.

Un problème dans la même lignée : quand une fenêtre attends que son joueur joue : elle prend 100% du cœur de CPU sur lequel elle s’exécute. Cela est dû au fait que tant qu’aucun coup n’a été fait on demande en boucle à la fenêtre si elle a des événements. Une solution classique lorsqu’on a un programme qui n’a rien a faire en l’attente de quelque chose : c’est de temporiser. Lui apprendre la patience. Lui apprendre à supporter l’ennui. Pour ce faire on peut utiliser :

#include <chrono>
#include <thread>

// [...]
std::this_thread::sleep_for(std::chrono::miliseconds(100));

Rien que d’attendre 100 ms, ça permet à notre processeur de respirer et à notre programme de partager un peu les resources.

Classes

Le dépôt contient quelques classes que vous allez devoir étudier, chacune avec un rôle bien précis. Pour comprendre leurs interactions, il vous suffit d’étudier le fichier main.cpp.

MorpionGame

Cette classe se concentre exclusivement sur la logique de jeu. Elle utilise deux énumérations :

  • MorpionGame::StartWith pour choisir qui commence lorsque vous construisez une partie :
    • valeur possibles : PX, PO ;
  • MorpionGame::Status qui permet de voir dans quel état est la partie :
    • PXTurn, POTurn si c’est le tour de quelqu’un,
    • PXWin, POWin si quelqu’un a gagné,
    • Draw en cas d’égalité.

L’état de la partie est accessible avec .status().

On peut faire jouer quelqu’un en donnant son symbole ‘x’ ou ‘o’ et l’index sur le plateau de jeu où placer le pion à la fonction .play(char, unsigned int). La case en haut à gauche est la case 0 et en bas à droit la case 8.

On peut récupérer la board au format d’un std::array<char, 9> grâce à la fonction .array().

Enfin, la fonction .done() vous permet de vérifier si la partie est terminée.

IPlayer

L’interface IPlayer prévoit les fonctions membres :

  • set_board_state pour envoyer le plateau de jeu à un joueur ;
  • set_player_symbol pour dire à un joueur s’il est x ou o ;
  • get_move pour demander au joueur une action à faire ;
  • set_draw pour indiquer au joueur une égalité ;
  • set_win pour indiquer au joueur qui a gagné.

GfxPlayer et TermPlayer implémentent IPlayer.

Consignes

Étape 1 : fermeture d’une fenêtre

Faites en sorte qu’il soit possible d’interrompre une partie : si le programme remarque la fermeture d’une fenêtre ou la eof sur un flux (pour TermReader), faite s’arrêter la partie.

Une piste pour y arriver serait d’utiliser un mécanisme similaire au .done de la classe MorpionGame. Vous êtes libres de faire les choses comme vous le souhaitez.

Vous avez le droit de modifier :

  • main.cpp ;
  • IPlayer.hpp ;
  • GfxPlayer.{cpp,hpp} ;
  • TermPlayer.{cpp,hpp}.

Étape 2 : fermeture à tout moment

Pour cette étape vous devez faire en sorte que si on ferme la fenêtre d’une personne dont ça n’est pas le tour, la partie s’arrête quand même tout de suite et proprement.

La solution se trouve dans la capacité à demander à un IPLayer de jouer sans bloquer lors de l’attente. La boucle principale peut à chaque tour de cycle consulter chaque IPlayer pour voir s’il est encore actif avec sa méthode .done().

Dans le IPlayer actuellement on a la fonction get_move qui demande à jouer et qui bloque tant qu’il n’y a pas de coup qui a été fait.

Dans l’idéal on a besoin de quelque chose comme :

  • ask_for_move qui indique au joueur qu’il doit jouer ;
  • get_move qui vérifie sans bloquer si le joueur à joué ;
  • done qui permet de s’avoir si le joueur est toujours là ou si sa fenêtre à quitté.

Vous avez le droit de modifier :

  • main.cpp ;
  • IPlayer.hpp ;
  • GfxPlayer.{cpp,hpp} ;
  • votre propre classe d’exécution d’une partie si vous en avez une.

Étape 3 : inspirez ~ expirez

Si vous ne l’avez pas encore fait : assurez vous que votre programme, dans l’attente d’un évènement, ne se mette pas à utiliser 100% d’un coeur de CPU.

Faites patienter votre programme entre deux tours de boucles avec std::this_thread::sleep_for.

Étape 4 : TermPlayer asynchrone

En utilisant ce qu’on a vu lors du TD 1 • Lecture asynchrone rendez le TermPlayer asynchrone pour que lui non plus ne bloque pas lorsqu’on attend que son joueur choisisse quel coup faire.