{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# TP — Draft d'Arène : un algorithme glouton en action\n", "\n", "Dans le mode **Arène** d'un jeu de cartes, on ne joue pas avec sa collection : on **drafte** son deck.\n", "On t'ouvre un booster de 3 cartes, tu en gardes **une seule**, et on passe au booster suivant.\n", "\n", "**Une carte écartée est perdue pour toujours. On ne revient jamais en arrière.**\n", "\n", "Cette règle du jeu est *exactement* la définition d'un algorithme glouton :\n", "\n", "- on procède par étapes (un booster à la fois) ;\n", "- à chaque étape on fait le meilleur choix possible ;\n", "- on ne revient jamais sur un choix déjà fait.\n", "\n", "Ce TP a un seul but : voir de nos propres yeux **ce que le glouton fait bien, et ce qu'il rate**.\n", "\n", "---\n", "\n", "## Les données\n", "\n", "Une carte est un tuple `(nom, puissance, cout)` :\n", "\n", "- la **puissance** mesure la force de la carte (plus c'est haut, mieux c'est) ;\n", "- le **coût** est le nombre de manas nécessaires pour la poser sur le plateau.\n", "\n", "> **Règle de jouabilité.** Une carte est dite **chère** si elle coûte **5 manas ou plus**.\n", "> Un deck n'est **jouable** que s'il contient **au plus 3 cartes chères**.\n", "> Sinon, on n'a rien à jouer pendant les premiers tours : on perd la partie avant même\n", "> d'avoir pu poser ses grosses cartes.\n", "\n", "**Objectif : obtenir le deck jouable de puissance totale maximale.**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ---------------------------------------------------------------\n", "# Données du TP — ne pas modifier\n", "# ---------------------------------------------------------------\n", "\n", "# Une carte = (nom, puissance, cout)\n", "BOOSTERS = [\n", " [(\"Gnome bricoleur\", 3, 1), (\"Yéti des roches\", 5, 4), (\"Golem de siège\", 6, 5)],\n", " [(\"Écuyer fidèle\", 2, 1), (\"Loup argenté\", 4, 3), (\"Chevalier du néant\", 5, 5)],\n", " [(\"Apprenti mage\", 3, 2), (\"Sorcière du marais\", 4, 4), (\"Dragon de bronze\", 5, 5)],\n", " [(\"Recrue de la garde\", 2, 1), (\"Berserker orc\", 4, 3), (\"Titan de pierre\", 9, 7)],\n", " [(\"Novice de l'ombre\", 1, 1), (\"Archer elfe\", 3, 2), (\"Léviathan des abysses\", 10, 8)],\n", " [(\"Sanglier tenace\", 2, 2), (\"Garde royal\", 5, 4), (\"Ogre lourdaud\", 4, 6)],\n", " [(\"Voleur agile\", 3, 2), (\"Prêtresse de la lune\", 4, 3), (\"Colosse runique\", 7, 6)],\n", " [(\"Gobelin ferrailleur\", 2, 1), (\"Chaman du totem\", 4, 4), (\"Hydre des marais\", 6, 6)],\n", " [(\"Éclaireur nain\", 3, 2), (\"Paladin d'argent\", 5, 4), (\"Behemoth ancien\", 6, 7)],\n", " [(\"Rat des égouts\", 1, 1), (\"Duelliste vétéran\", 5, 3), (\"Ver des sables\", 6, 5)],\n", "]\n", "\n", "COUT_CHER = 5 # une carte est \"chère\" à partir de 5 manas\n", "MAX_CHERES = 3 # au plus 3 cartes chères dans un deck jouable\n", "\n", "\n", "def afficher_deck(deck):\n", " \"\"\"Affiche un deck sous forme de tableau.\"\"\"\n", " for nom, puissance, cout in deck:\n", " marque = \" (chère)\" if cout >= COUT_CHER else \"\"\n", " print(f\" {nom:<24} puissance {puissance:>2} coût {cout}{marque}\")\n", "\n", "\n", "print(f\"{len(BOOSTERS)} boosters de 3 cartes.\")\n", "print(\"Booster 1 :\")\n", "afficher_deck(BOOSTERS[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Partie 1 — Savoir juger un deck\n", "\n", "Avant de drafter, il faut savoir répondre à deux questions sur un deck :\n", "**combien vaut-il ?** et **est-il seulement jouable ?**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercice 1\n", "# Écrire une fonction qui renvoie la somme des puissances des cartes d'un deck.\n", "\n", "def puissance_totale(deck):\n", " \"\"\"Renvoie la somme des puissances des cartes du deck.\"\"\"\n", " # À COMPLÉTER\n", " ...\n", "\n", "\n", "# Vérification\n", "test = [(\"A\", 3, 1), (\"B\", 5, 6), (\"C\", 2, 2)]\n", "assert puissance_totale(test) == 10\n", "assert puissance_totale([]) == 0\n", "print(\"Exercice 1 : OK\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercice 2\n", "# a) Compter les cartes chères (coût >= COUT_CHER) d'un deck.\n", "# b) En déduire si le deck est jouable (au plus MAX_CHERES cartes chères).\n", "\n", "def nb_cartes_cheres(deck):\n", " \"\"\"Renvoie le nombre de cartes coûtant COUT_CHER manas ou plus.\"\"\"\n", " # À COMPLÉTER\n", " ...\n", "\n", "\n", "def deck_jouable(deck):\n", " \"\"\"Renvoie True si le deck respecte la règle de jouabilité.\"\"\"\n", " # À COMPLÉTER\n", " ...\n", "\n", "\n", "# Vérification\n", "test = [(\"A\", 3, 1), (\"B\", 5, 6), (\"C\", 2, 7), (\"D\", 4, 5), (\"E\", 1, 8)]\n", "assert nb_cartes_cheres(test) == 4\n", "assert deck_jouable(test) is False\n", "assert deck_jouable(test[:3]) is True\n", "print(\"Exercice 2 : OK\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Partie 2 — Le glouton naïf\n", "\n", "La stratégie qui vient spontanément à l'esprit :\n", "\n", "> **À chaque booster, je prends la carte la plus puissante.**\n", "\n", "C'est bien un algorithme glouton : un choix par étape, le meilleur sur le moment,\n", "jamais de retour en arrière. Codons-la." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercice 3\n", "# Renvoyer la carte de plus grande puissance d'un booster.\n", "# (Ne pas utiliser max() : on veut voir la boucle de recherche du maximum.)\n", "\n", "def carte_la_plus_puissante(booster):\n", " \"\"\"Renvoie la carte de plus grande puissance du booster.\"\"\"\n", " # À COMPLÉTER\n", " ...\n", "\n", "\n", "# Vérification\n", "assert carte_la_plus_puissante(BOOSTERS[0]) == (\"Golem de siège\", 6, 5)\n", "assert carte_la_plus_puissante(BOOSTERS[4]) == (\"Léviathan des abysses\", 10, 8)\n", "print(\"Exercice 3 : OK\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercice 4\n", "# Drafter un deck complet : pour chaque booster, prendre la carte la plus puissante.\n", "\n", "def draft_naif(boosters):\n", " \"\"\"Renvoie le deck obtenu en prenant à chaque fois la carte la plus puissante.\"\"\"\n", " # À COMPLÉTER\n", " ...\n", "\n", "\n", "deck_naif = draft_naif(BOOSTERS)\n", "afficher_deck(deck_naif)\n", "print()\n", "print(\"Puissance totale :\", puissance_totale(deck_naif))\n", "print(\"Cartes chères :\", nb_cartes_cheres(deck_naif), \"( maximum autorisé :\", MAX_CHERES, \")\")\n", "print(\"Deck jouable ? :\", deck_jouable(deck_naif))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Question 1\n", "\n", "Le deck obtenu a une puissance totale de **65**, la plus haute qu'on puisse imaginer...\n", "et il est **injouable** : 9 cartes chères pour 3 autorisées.\n", "\n", "Remarque bien ce qui vient de se passer : le glouton n'a pas donné un résultat\n", "« un peu moins bon que l'optimal ». Il a donné un résultat **inutilisable**.\n", "\n", "**À toi.** Explique en une phrase pourquoi cet algorithme produit un deck injouable.\n", "Qu'a-t-il regardé, et surtout qu'a-t-il *ignoré* ?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Partie 3 — Le glouton contraint\n", "\n", "Réparons l'algorithme sans changer sa nature. Il reste glouton — un choix par étape,\n", "pas de retour en arrière — mais il compte au fur et à mesure les cartes chères déjà prises :\n", "\n", "> **À chaque booster, je prends la carte la plus puissante _parmi celles que j'ai encore le droit de prendre_.**\n", "\n", "Tant que le quota de cartes chères n'est pas atteint, toutes les cartes sont autorisées.\n", "Une fois le quota plein, seules les cartes à moins de `COUT_CHER` manas le sont.\n", "\n", "*(Les boosters sont construits pour qu'il y ait toujours au moins une carte à moins de 5 manas :\n", "l'algorithme ne peut donc jamais se retrouver bloqué.)*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Exercice 5\n", "# Drafter en respectant le quota de cartes chères.\n", "#\n", "# Indication : garder un compteur du nombre de cartes chères déjà prises.\n", "# Pour chaque booster, construire la liste des cartes autorisées, puis prendre\n", "# la plus puissante de cette liste.\n", "\n", "def draft_contraint(boosters):\n", " \"\"\"Renvoie le deck obtenu par un glouton qui respecte le quota de cartes chères.\"\"\"\n", " # À COMPLÉTER\n", " ...\n", "\n", "\n", "deck_glouton = draft_contraint(BOOSTERS)\n", "afficher_deck(deck_glouton)\n", "print()\n", "print(\"Puissance totale :\", puissance_totale(deck_glouton))\n", "print(\"Deck jouable ? :\", deck_jouable(deck_glouton))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Question 2\n", "\n", "Cette fois le deck est jouable, pour une puissance de **46**.\n", "\n", "Notre algorithme fonctionne. Mais une question reste ouverte, et c'est *la* question\n", "du chapitre : **46, est-ce le maximum ?**\n", "\n", "Le glouton, lui, en est incapable de le dire — il n'a jamais comparé son deck à un autre." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Partie 4 — La force brute : quel était le vrai maximum ?\n", "\n", "Pour le savoir, il n'y a qu'une méthode certaine : **essayer tous les decks possibles**\n", "et garder le meilleur deck jouable.\n", "\n", "À chaque booster on a 3 choix, et il y a 10 boosters : cela fait\n", "\n", "$$3^{10} = 59\\,049 \\text{ decks possibles.}$$\n", "\n", "C'est beaucoup pour un humain, très peu pour une machine. Le code ci-dessous est **fourni** :\n", "`product(*boosters)` fabrique toutes les combinaisons possibles, une par une.\n", "\n", "Lis-le, exécute-le, mais ne cherche pas à le réécrire : ce n'est pas l'objet du TP." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ---------------------------------------------------------------\n", "# Force brute — code fourni\n", "# ---------------------------------------------------------------\n", "from itertools import product\n", "from time import perf_counter\n", "\n", "\n", "def meilleur_deck_force_brute(boosters):\n", " \"\"\"Essaie TOUS les decks possibles et renvoie le meilleur deck jouable.\"\"\"\n", " meilleur = None\n", " meilleure_puissance = -1\n", " for deck in product(*boosters):\n", " if deck_jouable(deck) and puissance_totale(deck) > meilleure_puissance:\n", " meilleur = list(deck)\n", " meilleure_puissance = puissance_totale(deck)\n", " return meilleur, meilleure_puissance\n", "\n", "\n", "debut = perf_counter()\n", "deck_optimal, puissance_optimale = meilleur_deck_force_brute(BOOSTERS)\n", "duree = perf_counter() - debut\n", "\n", "afficher_deck(deck_optimal)\n", "print()\n", "print(\"Puissance optimale :\", puissance_optimale)\n", "print(f\"Calculé en {duree:.2f} s après avoir testé {3 ** len(BOOSTERS)} decks.\")\n", "print()\n", "print(\"Glouton :\", puissance_totale(deck_glouton), \" | Optimal :\", puissance_optimale)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Question 3\n", "\n", "Le glouton obtient **46**, l'optimum est **58**. Il s'est fait piéger — reste à comprendre où.\n", "\n", "Regarde les 3 cartes chères que chacun a retenues :\n", "\n", "- **glouton** : Golem de siège, Chevalier du néant, Dragon de bronze (les boosters 1, 2 et 3) ;\n", "- **optimum** : Titan de pierre, Léviathan des abysses, Colosse runique (les boosters 4, 5 et 7).\n", "\n", "Le tableau ci-dessous calcule, pour chaque booster, ce que rapporte le fait d'y « dépenser »\n", "une place de carte chère plutôt que de prendre la meilleure carte bon marché.\n", "\n", "Exécute-le, puis explique en quelques lignes l'erreur du glouton." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Ce que rapporte réellement chaque carte chère, comparée à la meilleure carte pas chère du même booster\n", "print(f\"{'Booster':<9}{'meilleure pas chère':<27}{'meilleure chère':<29}{'gain'}\")\n", "for i, booster in enumerate(BOOSTERS):\n", " pas_cheres = [c for c in booster if c[2] < COUT_CHER]\n", " cheres = [c for c in booster if c[2] >= COUT_CHER]\n", " if cheres == []:\n", " continue\n", " meilleure_pas_chere = carte_la_plus_puissante(pas_cheres)\n", " meilleure_chere = carte_la_plus_puissante(cheres)\n", " gain = meilleure_chere[1] - meilleure_pas_chere[1]\n", " print(f\"{i + 1:<9}{meilleure_pas_chere[0] + f' ({meilleure_pas_chere[1]})':<27}\"\n", " f\"{meilleure_chere[0] + f' ({meilleure_chere[1]})':<29}{gain:+d}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Partie 5 — Alors pourquoi ne pas toujours faire de la force brute ?\n", "\n", "Puisque la force brute donne toujours le bon résultat, autant l'utiliser partout, non ?\n", "\n", "La cellule suivante mesure le temps de calcul de la force brute pour un nombre croissant\n", "de boosters. Observe la colonne « temps »." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(f\"{'boosters':<11}{'decks testés':<16}{'temps (s)'}\")\n", "for n in range(4, len(BOOSTERS) + 1):\n", " debut = perf_counter()\n", " meilleur_deck_force_brute(BOOSTERS[:n])\n", " duree_n = perf_counter() - debut\n", " print(f\"{n:<11}{3 ** n:<16}{duree_n:.3f}\")\n", "\n", "# Extrapolation à un vrai draft d'Arène (30 boosters), à partir du temps mesuré ci-dessus\n", "vitesse = 3 ** len(BOOSTERS) / duree_n # decks testés par seconde\n", "secondes = 3 ** 30 / vitesse\n", "print()\n", "print(f\"Vitesse mesurée : environ {vitesse:.0f} decks testés par seconde.\")\n", "print(f\"Pour 30 boosters : {3 ** 30} decks à tester,\")\n", "print(f\"soit environ {secondes / (365 * 24 * 3600):.0f} années de calcul.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Question 4\n", "\n", "1. Quand on ajoute **un seul** booster, par combien le nombre de decks à tester est-il multiplié ?\n", " Et le temps de calcul ?\n", "2. Un vrai draft d'Arène compte **30 boosters**. Combien de decks faudrait-il tester ?\n", " En te servant du temps mesuré pour 10 boosters, estime la durée du calcul.\n", "3. Conclusion : dans quelle situation l'algorithme glouton est-il **le bon choix**,\n", " même s'il ne garantit pas l'optimum ?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Ce qu'il faut retenir\n", "\n", "| | Glouton | Force brute |\n", "|---|---|---|\n", "| Résultat | 46 (pas l'optimum) | 58 (l'optimum) |\n", "| Nombre d'opérations | 10 (une par booster) | 59 049 |\n", "| Avec 30 boosters | 30 opérations | plusieurs années |\n", "| Garantie | aucune | optimum certain |\n", "\n", "Un **algorithme glouton** construit une solution par étapes, en faisant à chaque étape\n", "le meilleur choix selon un critère local, et **sans jamais revenir en arrière**.\n", "\n", "Il est rapide. Il n'est pas toujours optimal. Quand ce n'est pas le cas, on parle\n", "d'**heuristique**.\n", "\n", "---\n", "\n", "## Pour aller plus loin\n", "\n", "Reprends le tableau des gains de la partie 4. Un autre algorithme glouton est possible :\n", "calculer le gain de chaque booster, puis dépenser ses 3 places de cartes chères sur les\n", "**3 plus gros gains**.\n", "\n", "1. Vérifie sur les données du TP que cette stratégie donne bien 58, c'est-à-dire l'optimum.\n", "2. Pourtant, cette stratégie est **impossible à appliquer dans un vrai draft**. Pourquoi ?\n", "\n", "
\n", "Indice\n", "\n", "De quelle information a-t-on besoin pour calculer le tableau des gains ? Et à quel moment\n", "un joueur en Arène dispose-t-il de cette information ?\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "Auteur : Florian Mathieu\n", "\n", "Licence CC BY-SA\n", "\n", "\"Licence
Ce cours est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3" } }, "nbformat": 4, "nbformat_minor": 4 }