1 Le jeu « Puissance 4 » 2 Comprendre un programme
Transcription
1 Le jeu « Puissance 4 » 2 Comprendre un programme
Université de Nice-Sophia Antipolis Algorithmique & Programmation Deug MIAS-MI 1 2002–2003 TP N 3 Procédures et fonctions Buts : – Manipuler des objets complexes – Tableaux à deux dimensions – Usage des sentinelles – Utilisation des constantes – Apprendre à modifier un programme 1 Le jeu « Puissance 4 » Ce jeu se joue à deux joueurs sur une grille rectangulaire supposée verticale. Chacun à leur tour les joueurs posent un pion en haut d’une colonne non pleine : ce pion glisse vers le bas jusqu’à être arrêté par un autre pion ou par le bas de la grille. Le gagnant est le premier qui aligne 4 de ses pions, horizontalement, verticalement ou en diagonale. Voici un exemple de partie en cours : 1 . . . . . x 2 . . . . o x 3 . . . o x x 4 . . . x o o 5 . . . o x o 6 . . . . . . 7 . . . . . . C’est à X de jouer. Il peut gagner en jouant en colonne 5 mais, s’il ne le voit pas et commet l’erreur de jouer en colonne 2, c’est O qui pourra gagner en jouant lui aussi en colonne 2 ! Nous avons programmé pour vous un gestionnaire de partie pour ce jeu : un programme qui demande alternativement à deux joueurs humains 1 quel coup ils jouent, affiche l’état de la partie et détermine qui gagne. Ce programme, trop long et trop complexe pour que vous l’écriviez vousmême, vous est fourni dans les Documents de cours et il est reproduit à la fin de cette feuille de TP. Votre premier travail sera de le comprendre, le second sera d’y faire quelques modifications pour en améliorer la programmation ou la qualité. 2 Comprendre un programme 2.1 Objets complexes Les objets définis par la classe instanciable Power4Game sont, comme leur nom l’indique, des parties en cours de « Puissance 4 ». Le constructeur sera chargé de créer la partie dans son état initial (personne n’a encore joué) et la classe exportera une seule méthode d’instance, l’ordre de « jouer la partie » ! Regardez la classe Power4 pour voir comment la classe Power4Game est utilisée et vérifiez que toutes les autres méthodes de Power4Game sont déclarées private. 1. Programmer un jeu « humain contre ordinateur » n’est pas encore de votre niveau : attendez la fin du semestre ! Les attributs d’une partie en cours seront, très naturellement, l’état de la grille de jeu (les coups déjà joués), les noms des joueurs (pour l’affichage des dialogues), celui dont c’est le tour de jouer et le résultat provisoire (la partie est-elle finie? y a-t-il un gagnant?). 2.2 Codage des joueurs Comme beaucoup de langages, Java ne dispose pas de type énuméré et, pour traduire une déclaration comme : type joueur = (joueur1, joueur2, personne) il faudra donner explicitement un codage par l’ordinal de chacune des trois constantes (ici, 0, 1 et 2) et considérer ce type joueur comme un type entier. 2.3 Tableaux à deux dimensions La grille de jeu est une matrice rectangulaire qui contient des entiers (ces entiers codent l’état de chaque case). Représenter une telle matrice ne pose aucun problème, il suffit de la considérer ligne par ligne : chaque case étant de type int, chaque ligne sera représentée par un tableau de type int[] et, comme on peut définir des tableaux de n’importe quoi, on dira que la matrice est un tableau de lignes, donc de type int[][]. La ligne d’indice line sera donc grid[][line] et sa valeur dans la colonne d’indice column sera simplement grid[column][line]. Les informaticiens n’utilisent pas le terme « matrice » pour désigner une telle structure, ils parlent plutôt de tableaux à deux dimensions 2 . 2.4 Usage des sentinelles Pour savoir si le coup d’un joueur le fait gagner, il va être nécessaire de se déplacer dans la grille de jeu et de tester la valeur de chaque case. Ce problème s’apparente à celui de la recherche séquentielle (chercher une valeur dans une liste ou un tableau) et, comme lui, est compliqué par le fait qu’il ne faut pas dépasser les bornes. La façon la plus simple de s’en sortir est de placer des sentinelles, valeurs fictives, juste hors des bornes, pour lesquelles le résultat de la recherche est connu d’avance et qui permettront de s’arrêter sans avoir à tester qu’on reste dans les bornes. Ici, on va entourer la grille de jeu de deux lignes et de deux colonnes supplémentaires dans lesquelles, bien sûr, il sera toujours interdit de jouer ! Ainsi, quand on cherchera des alignements, la présence de ces cases vides arrêtera la recherche sans que d’autres tests soit nécessaires. Et, évidemment, on n’affichera pas ces cases fictives quand on affichera la grille de jeu. 2.5 Utilisation des constantes Java ne connaissant ni les types énumérés ni les intervalles, un très grand nombre des valeurs manipulées par un programme seront des entiers, soit comme valeur effective, soit comme codage, et il serait très facile de s’y perdre : Qu’est-ce que « 2 » dans un programme ? Le nombre entier par lequel on multiplie une valeur pour la doubler ? un indice dans un tableau ? la troisième constante d’un type énuméré ? Et supposons que ce soit le dernier cas et que, mettant le programme au point, on s’aperçoive qu’il faut modifier le type énuméré et que cette constante devienne la cinquième : comment savoir quels sont, dans le programme, les « 2 » qu’il faut remplacer par des « 4 »? 2. Beaucoup de langages de programmation (et notre langage algorithmique) permettent d’utiliser l’écriture équivalente grid[column,line] ; ce n’est pas le cas de Java. Toutes ces difficultés sont résolues par l’usage systématique de constantes nommées. Le programme donné ne lésine pas là-dessus et c’est ainsi qu’il faut procéder 3. Examinons en détail ces déclarations : – les lignes 7 à 9 codent le type énuméré « joueur » mais aussi les indices du tableau de la ligne 10 qui dit quel caractère doit être affiché selon l’état de la case ; – comme les lignes 12 et 13 déclarent une trop grande grille (à cause des sentinelles), les lignes 14 et 17 donnent des noms explicites aux indices où commence et finit le plateau de jeu réel ; – les lignes 19 et 20 précisent que, quand un tableau de deux éléments représente un vecteur, son premier indice correspond à l’horizontale, son second à la verticale (ces constantes seront utilisées entre les lignes 74 et 83) ; – les lignes 21 à 24 ne servent qu’à indiquer la signification de la ligne 25, les constantes qu’elles définissent ne seront plus jamais utilisées ensuite ! – enfin, cette ligne 25 précise les directions dans lesquelles on cherchera des alignements de quatre pions. Dans un premier temps, les meilleurs d’entre vous penseront sans doute que ce travail de déclaration est fastidieux, qu’ils ne sont pas des imbéciles et qu’ils n’ont pas de temps à perdre avec ça. Ce n’est pas grave, ils changeront d’avis quand ils s’apercevront qu’ils ne sont plus les meilleurs. 2.6 Variables d’instance La variable impactLine déclarée en ligne 34 joue un rôle un peu particulier : elle ne sert qu’à mémoriser la ligne d’arrivée d’un pion, déterminée par la procédure play, le temps qu’elle soit utilisée comme argument de isWinning en ligne 127. Une programmation peu soigneuse pourrait la mettre avec les attributs (elle se déclare exactement de la même façon) et, bien sûr, ça marcherait. Cependant, il est difficile de la considérer comme un attribut au sens « objet » du terme, elle n’est qu’un intermédiaire, non une caractéristique de la partie en cours et, d’ailleurs, on serait bien en peine de la faire initialiser par le constructeur ! Le mieux est de la considérer comme une sorte de variable locale, tout comme les indices de boucles ou celles qui servent à échanger deux valeurs. Si l’on tient à la terminologie objet, on pourra éventuellement parler de variable d’instance. 3 Modifier un programme À présent que vous avez à peu près compris comment marche ce programme, nous allons voir si vous êtes capables de lui faire subir quelques améliorations ou modifications mineures. Les exercices proposés sont tous indépendants et ils doivent être réalisés sans bouleverser le programme de façon importante. a. Modifiez le programme pour que, quand il affiche l’état de la partie, il dise aussi à qui c’est le tour de jouer. b. La convention qui a été choisie est de numéroter les lignes de bas en haut. Ce n’est pas la convention usuelle en informatique. Modifiez le programme pour qu’elles soient numérotées de haut en bas. c. Les lignes 98 et 99 sont un peu violentes car elles peuvent interrompre la partie pour une simple faute de frappe. Modifiez le programme pour que, si un joueur joue dans une colonne pleine, on lui signale son erreur et on lui demande un autre choix. d. La méthode playable est un peu lourde puisqu’elle réexamine toutes les têtes de colonne à chaque coup alors qu’elles ne changent pas souvent. Modifiez le programme pour que la fin de partie par impossibilité de jouer (partie nulle) soit décelée plus économiquement. e. (difficile) On change les règles en déclarant qu’un alignement de quatre pions est aussi gagnant s’il est réalisé selon la marche du cavalier aux échecs (c’est-à-dire selon une pente 2, –2, 1/2 ou –1/2). Modifiez le programme pour tenir compte de cette nouvelle règle. Faites-le de telle sorte qu’on puisse revenir très facilement aux anciennes règles. 3. Il aurait même été encore plus propre de les déclarer private. f. (difficile) Il n’est pas facile de repérer les alignements. Modifiez le programme de telle sorte que, quand un joueur gagne, l’alignement qu’il vient de compléter soit mis en majuscules. g. (programme d’imitation, facultatif) Le jeu japonais appelé gomoku se joue avec les règles de notre « morpion » (un joueur a les X, l’autre les O, ils jouent chacun leur tour en posant leur pion sur une intersection d’un quadrillage, le premier qui aligne cinq a gagné) à la seule différence que la zone de jeu est un carré . Dans la variante la plus jouée, appelée gomoku ninuki, une règle de prise est ajoutée : quand un joueur, au moment où il joue, prend deux pions adverses en tenaille avec un autre de ses pions, ils sont considérés comme prisonniers et retirés du jeu (par exemple, si X joue sur le point dans la position X O O . , la situation devient X . . X ; en revanche, O peut sans danger jouer sur le point dans une position X O . X ). La prise peut être multiple et, comme l’alignement, elle se fait dans n’importe quelle direction. Le gagnant est le premier qui a aligné cinq ou fait dix prisonniers. Programmez le jeu de base ou la variante. La classe instanciable Power4Game 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import unsa.Console ; public class Power4Game { // CONSTANTES // joueurs final static int PLAYER_1 = 0 ; final static int PLAYER_2 = 1 ; final static int EMPTY = 2 ; final static char[] SYMBOL = new char[] { ’ x ’ , ’ o ’ , ’ . ’ } ; // coordonnées final static int GRID_WIDTH = 9 ; // 7 + sentinelles final static int GRID_HEIGHT = 8 ; // 6 + sentinelles final static int FIRST_COL = 1 ; final static int LAST_COL = GRID_WIDTH - 2 ; final static int TOP_LINE = GRID_HEIGHT - 2 ; final static int BOTTOM_LINE = 1 ; // directions final static int H = 0 ; // horizontalement final static int V = 1 ; // verticalement final static int[] HOR = new int[] {1, 0} ; // horizontale final static int[] VER = new int[] {0, 1} ; // verticale final static int[] UP = new int[] {1, 1} ; // diagonale montante final static int[] DOWN = new int[] {1, -1} ; // diagonale descendante final static int[][] DIRECTION = new int[][] {HOR, VER, UP, DOWN} ; // Attributs private int[][] grid ; // grille de jeu private String[] name ; // noms des joueurs private int player ; // qui doit jouer private int result ; // vainqueur ou partie nulle (si EMPTY) // Variable d’instance private int impactLine ; // ligne atteinte par le dernier pion joué 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 public Power4Game (String[] name) { // noms des joueurs // tout mettre à vide, y compris les sentinelles ! grid = new int[GRID_WIDTH][GRID_HEIGHT] ; for (int i = 0 ; i < GRID_WIDTH ; i++) { for (int k = 0 ; k < GRID_HEIGHT ; k++) grid[i][k] = EMPTY ; } this.name = name ; player = PLAYER_1 ; result = EMPTY ; } // Fonctions private boolean isFree (int column) { return grid[column][TOP_LINE] == EMPTY ; } private boolean playable () { // cherche s’il existe une colonne libre int column = FIRST_COL ; while (column <= LAST_COL) { if (isFree(column)) return true ; column++ ; } return false ; } private boolean isWinning (int column, int line) { // cherche s’il existe une direction alignant 4 int i = 0 ; while (i < DIRECTION.length) { if (isWinning(column, line, DIRECTION[i])) return true ; i++ ; } return false ; } private boolean isWinning (int column, int line, int[] dir) { int nextColumn = column + dir[H] ; int nextLine = line + dir[V] ; int forward = 0 ; // nombre de cases du joueur vers l’avant while (grid[nextColumn][nextLine] == player) { nextColumn += dir[H] ; nextLine += dir[V] ; forward++ ; } // reculer nextColumn = column - dir[H] ; nextLine = line - dir[V] ; int backward = 0 ; // nombre de cases du joueur vers l’arrière while (grid[nextColumn][nextLine] == player) { nextColumn -= dir[H] ; nextLine -= dir[V] ; backward++ ; } return backward + 1 + forward >= 4 ; } // Procédures private void nextPlayer () { // préférer un test clair à une astuce fondée sur les valeurs // arbitraires des constantes comme ‘player = 1 - player‘ if (player == PLAYER_1) player = PLAYER_2 ; else player = PLAYER_1 ; } 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 private void play (int column) { if (!isFree(column)) throw new RuntimeException( " colonne p l e i n e " ) ; impactLine = TOP_LINE ; // on sait qu’elle est libre while (impactLine > BOTTOM_LINE && grid[column][impactLine - 1] == EMPTY) impactLine-- ; // on est en bas ou au-dessus d’un pion grid[column][impactLine] = player ; } // Affichage private void printBoard () { // numéros des colonnes for (int column = FIRST_COL ; column <= LAST_COL ; column++) System.out.print( " " + column + " ") ; System.out.println() ; // état de la partie for (int line = TOP_LINE ; line >= BOTTOM_LINE ; line--) { for (int column = FIRST_COL ; column <= LAST_COL ; column++) System.out.print( " " + SYMBOL[grid[column][line]] + " ") ; System.out.println() ; } } // Jeu d’une partie public void playGame () { while (result == EMPTY && playable()) { printBoard() ; int column = Console.readInt(name[player] + " j o u e en colonne ?") ; play(column) ; // la variable ‘impactLine‘ a reçu une valeur if (isWinning(column, impactLine)) result = player ; else nextPlayer() ; } printBoard() ; if (result == EMPTY) System.out.println( " p a r t i e n u l l e " ) ; else System.out.println(name[result] + " gagne " ) ; } } La classe exécutable Power4 1 2 3 4 5 6 7 8 public class Power4 { public static void main (String[ ] args) { // les noms des joueurs sont donnés en arguments d’exécution Power4Game game = new Power4Game(args) ; game.playGame() ; } }