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() ;
}
}