Le but du projet est de réaliser un jeu de Bataille Navale en ligne de commande (CLI). Le programme devra répondre aux spécifications suivantes :
- Dessiner une grille à taille variable pour les frappes.
- Dessiner une grille de même taille pour les navires.
- Placer des navires sur la grille.
Le jeu se déroulera en 2 phases :
- La phase de placement. L'utilisateur doit entrer les positions de ses 5 navires.
- La phase de jeu. L'utilisateur entre les positions de ses frappes.
Le jeu se finit lorsqu'un des joueur n'a plus de navire.
On compte les types de navires suivants :
- 1x Destroyer (D) - taille 2
- 2x Sub-marine (S) - taille 3
- 1x BattleShip (B) - taille 4
- 1x Aircraft-cairrier ( C) - taille 5
- Conditions, manipulation console, exceptions...
Récupérez les fichiers InputHelper, Main et ColorUtil.
Nous allons créer la classe Board, qui représente les 2 grilles sur lesquelles seront placés les navires et les frappes.
- Créer la classe
Board, composée d'un nom, d'un tableau 2D deCharacterpour les navires, et d'un tableau 2D deBooleanpour les frappes. - Créer un constructeur avec arguments, qui prend en entrée un nom, et la taille de la grille.
- Créer un constructeur avec argument, qui prend en entrée un nom seulement et initialise la grille avec une taille de 10.
- Créer une méthode print dans Board, qui efface la console et dessine les 2 grilles côte à côte.
- Créer une classe
Mainet sa méthodemain(), afin de tester l'affichage des grilles.
NB : il est possible de vider la console avec la commande suivante:
System.out.flush();Dans un premier temps, nous ne nous occupons pas des navires. Le Board reste donc vide.
Visuellement, le board affiché peut ressembler à ceci :
Navires : Frappes :
A B C D E F G H I J A B C D E F G H I J
1 . . . . . . . . . . 1 . . . . . . . . . .
2 . . . . . . . . C . 2 . . . . . . . . . .
3 . . S . . . . . C . 3 . . . . . . . . . .
4 . . S . . . . . C . 4 . . . . . . . . . .
5 . . S . D D . . C . 5 . . . . . . . . . .
6 . . . . . . . . C . 6 . . . . . . . . . .
7 . . . . . . . . . . 7 . . . . . . . . . .
8 . . S S S . . . . . 8 . . . . . . . . . .
9 . . . . . . . . . . 9 . . . . . . . . . .
10. . . . . B B B B . 10. . . . . . . . . .Question :
- Comment limiter la taille du tableau 2D ? Et comment gérer le cas d'erreur ?
- Classe, héritage, surcharge
Aucun fichier supplémentaire.
Nous allons nous attaquer à la création des navires. Par soucis de pédagogie, nous choisirons de créer une classe par type de navire, chacune d'elles étant une classe fille de AbstractShip.
La class AbstractShip possède les éléments suivants :
- Un constructeur avec arguments, qui prend en entrée un nom, un label (le
Characterqui le représente), une taille et une orientation. - les méthodes
getLabel()etgetLength(),getOrientation()etsetOrientation(/*...*/).
Vous devez :
- Créer la classe
AbstractShip - Créer les classes
Destroyer,Submarine,Battleship,Carrier, qui héritent deAbstractShip. - Les classes filles devront posséder un constructeur par défaut (qui placent l'orientation à
null), et un constructeur avec argument qui prends en paramètre l'orientation).
Questions :
- En quoi l'héritage est-il utile dans notre cas ?
- Il commence à y avoir beaucoup de fichiers source pêle-mêle dans notre projet. Comment remédier à cela?
- L'orientation est un ensemble fini de 4 valeurs, pouvant être
NORTH,SOUTH,EAST,WEST. Quelle est la meilleure manière de représenter cette information ? - Nous allons avoir besoin de placer un navire sur le board. Est-il judicieux de créer une méthode
ship.setPosition(int x, int y)?
- interfaces, exceptions
Récupérez le fichier IBoard.
Il est maintenant temps de placer les navires sur la grille. Il semble nécessaire que la classe Board soit dotée de méthodes pour placer les navires et les frappes.
Nous allons modifier la classe Board pour lui faire implémenter l'interface IBoard.
Rappel: Les interfaces sont des classes 100% abstraites. Elles n'ont que la définition des méthodes, sans leurs implémentations. Elles sont utilisées pour forcer les classes filles à implémenter un comportement générique. Il existe deux conventions de nommage pour les interfaces : Xxx-able (Drawable, Clickable, Obserable, ...) et I-xxxx (IEngine, IListener, ...). On s'efforce en général d'utiliser l'une des deux conventions.
- modifier
Boardpour lui faire implémenter l'interfaceIBoard. - écrire les méthodes
putShip,hasShip,setHitetgetHitqui placent les navires et les frappes dans leurs tableaux respectifs. - modifier la méthode
main()deMain, afin de placer quelques navires. - modifier la méthode
print()deBoard, afin d'afficher le label des navires à leurs emplacements respectifs.
Questions :
- Les indices x et y commencent-ils à 1 ou 0 ?
- Que se passe-t-il si la valeur "position + longueur" d'un navire mène hors de la grille ? Comment gérer ce cas ?
- Que se passe-t-il si deux navires se chevauchent ? Comment gérer ce cas ?
- Scanners, gestion d'exceptions.
Aucun fichier supplémentaire.
Nous souhaitons que notre application propose au joueur de placer chacun des 5 navires, par ordre croissant de longueur. Pour cela, l'utilisateur devra entrer à la suite les positions des navires, au format "A1 n", "B4 s".
A chaque fois que l'on récupère des données de l'utilisateur (saisie formulaire, etc), il est TRES IMPORTANT de vérifier la cohérence et l'exactitude de ces données. Il faudra donc utiliser un bloc
try... catch()pour s'assurer que les valeurs entrées sont correctes.
Afin de gagner du temps, vous pouvez utiliser la classe InputHelper fournie, dont la méthode readShipInput récupère les entrées de l'utilisateur et les converti en données exploitables. Notez bien la présence du bloc try... catch() pour sécuriser les entrées utilisateur.
Exemple d'utilisation de InputHelper :
do {
AbstractShip s = ships[i];
ShipInput res = InputHelper.readShipInput();
// TODO Convert orientation
// orientation = res.orientation;
s.setOrientation(orientation);
try {
board.putShip(s, res.x, res.y);
++i;
done = i == 5;
} catch(BoardException e) {
System.err.println("Impossible de placer le navire a cette position");
}
board.print();
} while (!done);Il ne reste plus qu'à modifier notre classe Main:
- Créer un tableau des 5 navires :
AbstractShip[] ships = { new Destroyer(), new Submarine(), new Submarine(), new BattleShip(), new Carrier() };
- Appeler la méthode
readShipInputtant que tous les navires ne sont pas correctement placés - Paramétrer le
Boardavec les valeurs retournées - Afficher l'état actuel du
Boardentre chaque saisies
Question :
- Quelle classe Java permet de lire les entrées clavier ?
- "refactors", exceptions
Aucun fichier supplémentaire
La conception actuelle du jeu nous pose maintenant 2 problèmes :
- Les
Hitspeuvent en réalité avoir 3 états : inconnu, touché, et manqué. Un boolean ne suffis plus. On peut en revanche se servir d'un Boolean qui peut être soit vrai, faux ou null. (Un enum serait aussi un choix judicieux) - Les navires sont placés avec un tableau de Character. Comment savoir où le navire commence et se termine, et donc s'il est totalement détruit ?
Il est courant de devoir modifier la conception d'un logiciel en cours de développement, et les raisons peuvent être multiples : Changement soudain du cahier des charges, erreur de conception, difficulté technique imprévue... Heureusement, ceci n'est pas toujours dramatique et il est parfois suffisant de quelques petites modifications pour remettre les choses dans l'ordre. C'est ce qu'on appelle dans le jargon un refacto. Nos IDE sont généralement capables de nous assister dans cette tâche.
Nous aurons besoin d'une classe ShipState intermédiaire entre le navire et la grille, capable de mémoriser l'état du navire en un point précis. La classe ShipState possède :
- un attribut de type
AbstractShip, référence vers le navire concerné par cet état - un attribut boolean
struck, qui vaut vrai si le navire est touché en cet endroit - une méthode
void addStrike(), pour marquer le navire comme touché - une méthode
boolean isStruck(), qui retourne la valeur de l'attribut struck - une méthode
String toString(), qui retourne le label du navire associé (en rouge si le navire est touché en cet endroit) - un méthode
boolean isSunk(), qui retourne vrai si le navire est totalement détruit - une méthode
AbstratShip getShip()qui retourne ne navire concerné par cet état
Travail demandé :
- Dans
AbstractShip, créer un attribut entierstrikeCountainsi qu'une méthodeaddStrike(), permettant de manipuler le nombre de frappes que le navire à reçu au total. Créer la méthodeisSunk(); - Créer la classe
ShipState. - Dans
Board, changer le tableau debooleanhits en un tableau deBoolean - Dans
Board, changer le tableau deCharacterships en un tableau deShipState - Dans
Board, changer la méthodeprint()pour afficher '.' si unHitest null, 'X' en blanc si unHitest faux, et 'X' en rouge si unHitest vrai. - Modifier la classe
Mainpour instancier un board, placer plusieurs navires, et appelersendHit()sur ce board. Vérifier que les navires et les hits changent de couleurs en cas de touche.
NB : Vous pouvez utiliser la classe
ColorUtilqui vous est fournie
System.out.print(ColorUtil.colorize("Hello World with COLOR!!!", ColorUtil.Color.RED));NB : Pour les utilisateurs d'Eclipse, vous pouvez télécharger ce plugin pour que la console Eclipse supporte les couleurs : https://marketplace.eclipse.org/content/ansi-escape-console
Question :
- Si on appelle
addStrike()plus d'une fois parShipState, le navire pourra donc être touché plus que le permet sa longueur. Comment gérer cet état illégal ?
- enums, interfaces
Récupérez le fichier Hit.
Avant d'aller plus loin, il va falloir doter notre Board d'une méthode lui permettant de recevoir les frappes de l'adversaire, et qui permettra à l'adversaire de recevoir les nôtres :
L'adversaire appelera donc la méthode sendHit() sur notre Board, tandis que nous appelerons sendHit() sur le sien.
L'enum Hit permet de renvoyer le status d'une frappe lancée. Les valeurs peuvent être MISS ou STRUCK, pour une frappe manquée ou réussie, ou bien le nom d'un des 4 navires lorsqu'un navire viens d'être coulé totalement.
NB : L'enum
Hitest particulier : Il possède un constructeur, ce qui nous permet de lui faire porter des valeur. Cela sera pratique lorsque nous voudrons créer l'enum directement à partir de la longueur du navire, grace à la méthodefromInt(), ou lorsque nous voudrons avoir le nom ("label") du navire détruit)
Travail demandé :
- Modifiez la méthode
sendHit()en prenant soin de retourner la bonne valeur si un navire est détruit. - Modifier la classe
Mainpour envoyer des frappes sur l'unique destroyer de votre board. Vérifier que le destroyer s'affiche en rouge. - Vérifier que
destroyer.isSunk()retourne vrai, et que le dernier appel àsendHit()retourneHit.DESTROYER. Afficher coulé le cas échéant.
Question :
- Que se passe t-il lorsque l'on appelle
sendHit()deux fois sur la même position d'un navire ? - que renvoit la méthode
hasShip(x, y)lorsque le navire en (x, y) a été touché?
- Random, Listes, retour par référence
Récupérez le fichier BattleShipsAI.
A ce point, nous commes capable de :
- saisir des coordonnées depuis le clavier
- placer les navires sans erreurs
- envoyer des frappes sur une grille
- détécter si un navire est touché ou coulé suite à une frappe.
Nous avons tous les éléments nécéssaires à la logique de notre jeu, mais il nous manque une chose essentielle : un adversaire.
La classe BattleShipsAI vous est fournie. Elle propose une Intelligence Artificielle rudimentaire. Il vous reste à compléter la méthode void putShips(AbstractShip ships[]), qui permet de placer les navires sur this.board de manière aléatoire.
NB : Notez que BattleShipsAI a besoin de deux objets
Board(un par joueur) pour fonctionner. VotreBoardimplémente l'interfaceIBoard, ce qui permet àBattleShipsAIde savoir comment interagir avec votre Board, sans en connaitre les détails d'implémentation.IBoardest en quelque sorte le "Manuel d'utilisation" d'un objetBoard(On parle de contrat).
Pour tester le placement des navires, nous allons faire jouer l'IA contre elle même sur un seul Board. Modifiez la classe Main. Elle devra :
- initialiser un objet
Boardet l'afficher - initialiser une liste de navires. Notez qu'un simple tableau pourrait suffire, mais les listes s'avèrerons utiles plus tard dans la partie Bonus.
- initialiser un objet ai de type
BattleShipsAI, qui utilise le mêmeBoardpour la grille amie et adverse. - créer un compteur qui compte le nombres de navires détruits.
- tant qu'il reste des navires,
- appeler la méthode
ai.sendHit() - afficher les coordonnées du hit et sont résultat (touché ou manqué, ou XXX coulé). La méthode
Hit.getLabel()vous sera utile. - afficher le nouvel état du board
- appeler la méthode
Vous pouvez utiliser la méthode sleep(int ms) suivante pour temporiser la boucle de jeu :
private static void sleep(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
}Travail à faire :
- Compléter
putShips()dansBattleShipsAI(utiliser unjava.util.Random) - Modifier la classe
Main
NB : Vous remarquerez que
Listutilise une syntaxe avec des chevrons (<>). C'est ce qu'on appelles unGeneric(équivallent des templates en c++). C'est un paramètre qui indique que les éléments contenus dans la liste serons du type passé entre chevrons.
Questions :
- La méthode
sendHit(int[] coords)est sensée choisir une frappe à des coordonnées aléatoires. Pourquoi lui passe t-on des coordonnées en paramètres ? - Quelles différences voyez vous entre une List et un simple tableau?
- Quelle type de liste avez vous utilisé ? Commentez.
- Scanners, héritage
Récupérez les fichiers Player et AIPlayer.
Notre jeu est presque prêt. Pour l'instant, la boucle de jeu consiste en une IA qui joue toute seule. Il est temps de lui rajouter un vrai adversaire. La classe Player représente un joueur. Elle possède entre autre deux Board (le siens, et celui de son adversaire), et une liste de navires. Ses méthodes putShip et sendHit (à compléter) doivent lire l'entrée clavier pour respectivement placer les navires et les frappes sur le Board ennemi.
La classe AIPlayer étends de Player. Elle doit redéfinir les méthodes putShip et sendHit pour utiliser son IA plutôt que le clavier.
Travail à faire :
- Compléter la classe
Player - Compléter la classe
AIPlayer: redéfinir les méthodesputShipetsendHit - permettre la saisie du nom du joueur 1 au clavier. (utiliser un scanner)
- modifier la boucle principale : Tant que aucun joueurs n'est hors jeu :
- afficher le nom du joueur 1 ainsi que son Board
- saisir les coordonnés de la frappe (
player.sendHit()) - réafficher le board
- afficher les coordonnées du hit et sont résultat
- recevoir la frappe de l'adversaire et réafficher le board
- afficher les coordonnées du hit et sont résultat
Question :
- En quoi l'héritage est t'il utile dans notre cas ?
- Comment pourrait-on modifier la classe "Game" pour proposer un mode 2 joueurs (sans IA) ?
Rendre le tirage des navires aléatoire (nombre et types de navires différents pour chaque joueurs)
- Serializable, File, InputStream On souhaite sauvegarder la partie avant de quitter l'application, de telle sorte à ce que la dernière partie jouée soit automatiquement restaurée au relancement du jeu.
L'idée est de, à chaque tour du jeu, écrire sur le disque dur un fichier qui mémorise l'état actuel du jeu. Au lancement du jeu, on vérifie la présence de ce fichier et on le charge le cas échéant.
"Oula, c'est bien trop compliqué à mettre en oeuvre..."
A vrai dire, cette fonctionnalité ne devrait prendre que quelques minutes à implémenter! Tout objet Java peut être sauveagardé dans un fichier, sous peu qu'il soit Serializable. Un objet dit serializable est un objet qui implémente l'interface Serializable, et dont tous les attributs implémentent aussi cette interface.
Exemple de serialization d'un objet :
public class TestSerialize {
static class Computer implements Serializable {
private List<Device> devices = new LinkedList<>();
public Computer(Device... device) {
this.devices.addAll(Arrays.asList(device));
}
}
static abstract class Device implements Serializable {
private String name;
public Device(String name) {
this.name = name;
}
}
static class Screen extends Device implements Serializable {
public Screen() {
super("screen");
}
}
static class Keyboard extends Device implements Serializable {
public Keyboard() {
super("keyboard");
}
}
public static void main(String[] args) {
Computer c = new Computer(new Keyboard(), new Screen());
File saveFile = new File("saveFile");
saveComputer(c, saveFile);
c = loadComputer(saveFile);
}
private static Computer loadComputer(File saveFile) {
Computer computer = null;
if (saveFile.exists()) {
try {
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream(saveFile));
computer = (Computer) ois.readObject();
ois.close(); // NEVER FORGET TO CLOSE STREAMS, OR IT WILL CAUSE MEMORY LEAKS !
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return computer;
}
public static void saveComputer(Computer c, File saveFile) {
try {
if (!saveFile.exists()) {
saveFile.getAbsoluteFile().getParentFile().mkdirs();
}
FileOutputStream out = new FileOutputStream(saveFile);
ObjectOutputStream oout = new ObjectOutputStream(out);
oout.writeObject(c);
oout.flush();
oout.close(); // NEVER FORGET TO CLOSE STREAMS, OR IT WILL CAUSE MEMORY LEAKS !
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}