.. _20212048classerst: =================== 2048 et les classes =================== .. only:: html **Links:** :download:`notebook <2021_2048_classe.ipynb>`, :downloadlink:`html <2021_2048_classe2html.html>`, :download:`python <2021_2048_classe.py>`, :downloadlink:`slides <2021_2048_classe.slides.html>`, :githublink:`GitHub|_doc/notebooks/td1a_home/2021_2048_classe.ipynb|*` Le jeu `2048 `__ est assez addictif mais peut-on imaginer une stratégie qui joue à notre place est maximise le gain… Le jeu se joue sur une matrice *4x4*. .. code:: ipython3 from jyquickhelper import add_notebook_menu add_notebook_menu() .. contents:: :local: Décomposition du problème ------------------------- 0. Création de la matrice de jeu 1. Ajout d’un nombre aléatoire dans ``{2,4}`` à une position aléatoire pourvu qu’elle soit libre 2. Détermination de toutes les cases libres 3. A-t-on perdu ? 4. Joue un coup sachant une direction donnée 5. Aggrège les nombres dans un tableau que ce soit une ligne ou une colonne 6. Score… 7. Choisit le coup suivant (un coup au hasard selon deux directions possibles) 8. Joue une partie en appelant toutes les fonctions précédentes. 0 - Création de la matrice de jeu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 import numpy def creer_jeu(dim): return numpy.zeros((dim, dim), dtype=int) jeu = creer_jeu(4) jeu .. parsed-literal:: array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]) 2 - Détermination de toutes les cases libres ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 def position_libre(jeu): pos = [] for i in range(jeu.shape[0]): for j in range(jeu.shape[1]): if jeu[i, j] == 0: pos.append((i, j)) return pos position_libre(jeu) .. parsed-literal:: [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3)] 1 - Ajout d’un nombre aléatoire dans {2,4} à une position aléatoire pourvu qu’elle soit libre ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 def nombre_aleatoire(jeu): pos = position_libre(jeu) nb = numpy.random.randint(0, 2) * 2 + 2 i = numpy.random.randint(0, len(pos)) p = pos[i] jeu[p] = nb nombre_aleatoire(jeu) jeu .. parsed-literal:: array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 4, 0, 0]]) 3 - A-t-on perdu ? ~~~~~~~~~~~~~~~~~~ .. code:: ipython3 def perdu(jeu): pos = position_libre(jeu) return len(pos) == 0 perdu(jeu) .. parsed-literal:: False 5 - Aggrège les nombres dans un tableau que ce soit une ligne ou une colonne ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 def joue_ligne_colonne(lc): # on enlève les 0 non_null = [a for a in lc if a != 0] # on additionne les nombres identiques consécutifs i = len(non_null) - 1 while i > 0: if non_null[i] != 0 and non_null[i] == non_null[i-1]: non_null[i-1] *= 2 non_null[i] = 0 i -= 2 else: i -= 1 # on enlève à nouveau les zéros non_null2 = [a for a in non_null if a != 0] final = numpy.zeros(len(lc), dtype=int) final[:len(non_null2)] = non_null2 return final joue_ligne_colonne(numpy.array([2, 4, 2, 2])) .. parsed-literal:: array([2, 4, 4, 0]) .. code:: ipython3 assert joue_ligne_colonne(numpy.array([0, 2, 0, 2])).tolist() == [4, 0, 0, 0] assert joue_ligne_colonne(numpy.array([2, 2, 2, 2])).tolist() == [4, 4, 0, 0] assert joue_ligne_colonne(numpy.array([2, 4, 2, 2])).tolist() == [2, 4, 4, 0] 4 - Joue un coup sachant une direction donnée ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 def joue_coup(jeu, direction): if direction == 0: # gauche for i in range(jeu.shape[0]): jeu[i, :] = joue_ligne_colonne(jeu[i, :]) elif direction == 1: # droite for i in range(jeu.shape[0]): jeu[i, ::-1] = joue_ligne_colonne(jeu[i, ::-1]) elif direction == 2: # haut for i in range(jeu.shape[0]): jeu[:, i] = joue_ligne_colonne(jeu[:, i]) elif direction == 3: # bas for i in range(jeu.shape[0]): jeu[::-1, i] = joue_ligne_colonne(jeu[::-1, i]) joue_coup(jeu, 0) jeu .. parsed-literal:: array([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [4, 0, 0, 0]]) 6 - score ~~~~~~~~~ .. code:: ipython3 def score(jeu): return jeu.max() # à ne pas confondre avec max(jeu) # max(jeu) appelle la fonction max de python (et non celle du numpy), # elle cherche le maximum sur toutes les lignes # et comparer deux lignes est ambigü, comparaison terme à terme ? ce n'est pas un ordre total score(jeu) .. parsed-literal:: 4 Voir `ordre total `__. 7 - coup suivant ~~~~~~~~~~~~~~~~ .. code:: ipython3 def coup_suivant(jeu): # une direction aléatoire parmi 0 ou 4 h = numpy.random.randint(0, 2) return h * 2 coup_suivant(jeu) .. parsed-literal:: 2 8 - la fonction faisant tout ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 def partie(dim): jeu = creer_jeu(dim) coup = 0 while not perdu(jeu): nombre_aleatoire(jeu) d = coup_suivant(jeu) joue_coup(jeu, d) coup += 1 return coup, jeu coup, jeu = partie(4) print("nombre de coups: %d, score=%d jeu:" % (coup, score(jeu))) print(jeu) .. parsed-literal:: nombre de coups: 68, score=64 jeu: [[64 32 16 4] [32 16 8 2] [ 2 8 4 2] [ 8 2 4 2]] Classes ------- .. code:: ipython3 class c2048: def __init__(self, dim=4): self.jeu = self.creer_jeu(dim) self.coup = 0 self.score = 0 def creer_jeu(self, dim): return creer_jeu(dim) def __repr__(self): return "coup=%d score=%d, jeu=\n%s" % (self.coup, self.score, self.jeu) J = c2048() J .. parsed-literal:: coup=0 score=0, jeu= [[0 0 0 0] [0 0 0 0] [0 0 0 0] [0 0 0 0]] .. code:: ipython3 print(J) # l'interpréteur python exécute implicitement : print(J.__repr__()) .. parsed-literal:: coup=0 score=0, jeu= [[0 0 0 0] [0 0 0 0] [0 0 0 0] [0 0 0 0]] Classe complète --------------- .. code:: ipython3 class c2048: def __init__(self, dim=4): self.jeu = self.creer_jeu(dim) self.coup = 0 self.score = 0 def creer_jeu(self, dim): return creer_jeu(dim) def __repr__(self): return "coup=%d score=%d, jeu=\n%s" % (self.coup, self.score, self.jeu) def position_libre(self): pos = [] for i in range(self.jeu.shape[0]): for j in range(self.jeu.shape[1]): if self.jeu[i, j] == 0: pos.append((i, j)) return pos def calcule_score(self): return self.jeu.max() def joue_ligne_colonne(self, lc): # on enlève les 0 non_null = [a for a in lc if a != 0] # on additionne les nombres identiques consécutifs i = len(non_null) - 1 while i > 0: if non_null[i] != 0 and non_null[i] == non_null[i-1]: non_null[i-1] *= 2 non_null[i] = 0 i -= 2 else: i -= 1 # on enlève à nouveau les zéros non_null2 = [a for a in non_null if a != 0] final = numpy.zeros(len(lc), dtype=int) final[:len(non_null2)] = non_null2 return final def joue_coup(self, direction): if direction == 0: # gauche for i in range(self.jeu.shape[0]): self.jeu[i, :] = joue_ligne_colonne(self.jeu[i, :]) elif direction == 1: # droite for i in range(self.jeu.shape[0]): self.jeu[i, ::-1] = joue_ligne_colonne(self.jeu[i, ::-1]) # identique à # self.jeu[i, :] = joue_ligne_colonne(self.jeu[i, ::-1])[::-1] elif direction == 2: # haut for i in range(self.jeu.shape[0]): self.jeu[:, i] = joue_ligne_colonne(self.jeu[:, i]) elif direction == 3: # bas for i in range(self.jeu.shape[0]): self.jeu[::-1, i] = joue_ligne_colonne(self.jeu[::-1, i]) def nombre_aleatoire(self): pos = self.position_libre() nb = numpy.random.randint(0, 2) * 2 + 2 i = numpy.random.randint(0, len(pos)) p = pos[i] self.jeu[p] = nb def perdu(self): pos = self.position_libre() return len(pos) == 0 def coup_suivant(self): # une direction aléatoire parmi 0 ou 4 h = numpy.random.randint(0, 2) return h * 2 def partie(self): self.coup = 0 while not self.perdu(): self.nombre_aleatoire() d = self.coup_suivant() self.joue_coup(d) self.coup += 1 self.score = self.calcule_score() J = c2048() J.partie() J .. parsed-literal:: coup=40 score=16, jeu= [[16 2 16 8] [ 2 16 4 2] [ 8 4 16 2] [ 4 2 8 4]] Un dernier graphe pour finir ---------------------------- On réalise plusieurs parties, on trace un graphe avec le nombre de coups en abscisse et le score en ordonnée. .. code:: ipython3 from tqdm import tqdm from pandas import DataFrame obs = [] for i in tqdm(range(500)): J = c2048() J.partie() obs.append(dict(score=J.score, coup=J.coup)) df = DataFrame(obs) df.head() .. parsed-literal:: 100%|██████████| 500/500 [00:01<00:00, 282.10it/s] .. raw:: html
score coup
0 64 67
1 128 96
2 128 84
3 16 29
4 16 33
.. code:: ipython3 df.plot(x='coup', y='score', kind='scatter', title="Score / Coup"); .. image:: 2021_2048_classe_30_0.png Une autre stratégie pour illustrer l’héritage --------------------------------------------- On veut essayer une autre stratégie et la comparer avec la précédente. Pour cela, on crée une seconde classe dans laquelle on remplace la méthode ``coup_suivant``. On pourrait tout copier coller mais c’est très souvent la plus mauvaise option. Et pour éviter cela, on crée une seconde classe qui `hérite `__ de la précédente, puis on remplace la méthode souhaitée. .. code:: ipython3 class c2048_4(c2048): def coup_suivant(self): # une direction aléatoire parmi 0 ou 4 h = numpy.random.randint(0, 3) return h obs = [] for i in tqdm(range(500)): J = c2048() J.partie() obs.append(dict(score=J.score, coup=J.coup)) df2 = DataFrame(obs) df2.head() .. parsed-literal:: 100%|██████████| 500/500 [00:01<00:00, 299.41it/s] .. raw:: html
score coup
0 64 55
1 64 65
2 32 52
3 32 51
4 64 67
.. code:: ipython3 import matplotlib.pyplot as plt fig, ax = plt.subplots(2, 2, figsize=(14, 4), sharey=True, sharex=True) df.plot(x='coup', y='score', kind='scatter', title="Score / Coup", label="Stratégie 1", ax=ax[0, 0]) df2.plot(x='coup', y='score', kind='scatter', title="Score / Coup", label="Stratégie 2", ax=ax[1, 0], color='red'); df2.plot(x='coup', y='score', kind='scatter', title="Score / Coup", label="Stratégie 2", ax=ax[0, 1], color='red'); .. image:: 2021_2048_classe_33_0.png La stratégie 2 paraît meilleure : jouer aléatoire dans toutes les directions est plus efficace que dans deux directions.