Bataille navale POO
Nous allons retravailler la bataille navale développée de façon procédurale pour en faire un programme orienté objet.
Acquis attendus
Objectifs
Dans cette bataille navale, nous allons gérer des Navires de différentes tailles et positions dans une matrice symétrique de 0 par 9 de côté.
0 1 2 3 4 5 6 7 8 9
0 . . . . . . . . . .
1 . . . . . . . . . .
2 . . . . . . . . . .
3 . . . . . . . X . .
4 . . . . . . . . . .
5 . . . . . . . . . .
6 . . . . . . . . . .
7 . . . . . . . . . .
8 . . . . . . . . . .
9 . . . . . . . . . .
Dans notre programme, toutes les coordonnées sont gérés sous la forme d'un tuple au format (y,x) car pour une liste 2D, le premier indice correspond aux lignes, et le second indice aux colonnes.
Dans la matrice ci-dessus, le X se situe donc aux coordonées (3,7).
Fait particulier de notre programme, et contrairement à la version sans POO, nous n'allons pas utiliser de matrice pour gérer le jeu. Ce sont les Navires qui seront en charge de leur coordonnées initiale ainsi que designe (y,x) désignant leur structure.
Nous ferons usage d'une liste 2D seulement pour afficher les informations au joueur.
Pour cette exercice nous allons nous appuyer sur trois type de structures de données :
- des tuples pour les positions (ie:
(2,7)pour la ligne 2, la colonne 7) - des dictionnaires pour stocker les positions et l'état des positions correspondant à la structure (ie
{(2,7):True}) - les classes et les objets qui en découlent :
Ship,Player,Game
The Ship Class
C'est l'unité la plus fine de notre jeu, c'est pourquoi nous commençons par la décrire pour voir quels attributs et quelles capacités nous voulons lui donner.
Notre navire de classe Ship doit pouvoir rendre compte de sa santé précisément, et pour cela il y a plusieurs possibilités.
- La première c'est de répercuter la structure de chaque bateau dans une matrice. C'est plus ou moins ce que l'on a fait dans l'exercice précédent.
- La deuxième c'est de gérer les coordonnées associées à chaque case directement dans les Navires, c'est cette version que nous allons implémenté.
class Ship:
def __init__(self,name, longueur, direction, pos):
self.name = name
self.longueur = longueur
self.direction = direction
self.pos = pos
self.structure = self.buildStructure(pos, longueur, direction)
self.alive = True
- La taille en nombre de cases représente la vie de ce Navire, par exemple un Croiseur de 4 cases aura 4 vie avant d'être coulé. Les vies sont réparties de façon homogène sur chaque Navire, une case = une vie. Nous allons stocker ces valeurs au sein d'un attribut
structure - Les cases du Navire se répartissent sur la grille via un attribut
directionqui prennent soit la valeurhorizontaleouverticale - La tête du bateau designe la première case, celle-ci est stockée dans un tuple
(y,x). Cette information est nécessaire pour positionner le bateau. Avec les informations dedirectionet delongueuron est capable de reconstruire assez facilement la forme du bateau sur la grille.
Un tuple est une structure de donnée qui contient 2 valeurs. On accède à ces valeurs en utilisant la même notation que les listes. Par exemple pour l'attribut pos de notre classe Ship :
Nous allons utiliser un système simple basé sur un dictionnaire qui nous permettra de savoir rapidement si un bateau est endommagé ou pas.
Par exemple, un croiseur avec une longueur de 4, de position initiale (3,2), de direction verticale et en bon été sera défini par la structure suivante :
{(3,2):True, (4,2):True, (5,2):True, (6,2):True}
Cette structure de donnée clef:valeur nous permet d'associer deux informations ensemble en se basant sur une clef unique. Dans notre cas, nous allons associer chaque tuple coordonnée avec une valeur True or False indiquant si la structure du Navire est endommagé ou pas à cette position.
Un exemple simple, avec un Sous-Marin de 3 cases, vertical, démarrant à la position (2,4), l'appel à un dictionnaire se fait ainsi :
ssmarin={(2,4):True, (2,5):True, (2,6):True}
ssmarin[(2,4)] # => renvoie True
ssmarin[(2,7)] # => renvoie une erreur
Autre intérêt du dictionnaire c'est qu'il est facile en utilisant du mot clef in de savoir si un tuple position est contenue ou pas dans cette structure.
- Pour récupérer les clefs d'un dictionnaire :
ssmarin.keys() - Pour récupérer les valeurs d'un dictionnaire :
ssmarin.values() - Pour récupérer les clefs et les valeurs d'un dictionnaire :
ssmarin.items() - Pour parcourir un dictionnaire en ayant accès aux clef et aux valeurs :
Enfin, dernière astuce, il est possible d'utiliser la fonction sum() sur des structures de données contenant des Booléen : sum(ssmarin.values()) renvoie 3 car True est présent 3 fois.
Sachant tout cela on peut construire plusieurs fonctions dans Ship qui nous renseigne :
- sur l'état global de
Ship: combien de structure valide me reste-t-il ? - sur l'état détaillé du
Ship: quel partie est touché ? quel partie est ok ? - sur du calcul d'intersection avec une autre structure de navire, nécessaire pour éviter les collisions lors du placement.
La première getStructureLife() retourne le nombre de cases valides restantes :
La deuxième getStructureByType renvoie une liste contenant deux listes de positions : la première contient les tuples positions touchées, la deuxième contient les tuples positions encore valide.
def getStructureByType(self):
structWithDamage = []
structValid = []
for key, value in self.structure.items():
if value == False: structWithDamage.append(key)
else: structValid.append(key)
return [structWithDamage,structValid]
Pour gérer les collisions au niveau du placement des bateaux, et il y'en aura, on peut demander à une instance de Ship si une autre structure de bateau {(y,x), ...} passé en paramètre recoupe ou pas les coordonnées existantes du bateau.
Pour cela, on peux exploiter des fonctions bien pratique d'union, d'intersection applicable sur les dictionnaires.
Pour calculer l'intersection entre deux dictionnaires, on compare les clefs en utilisant la notation & .
Si l'intersection entre la structure de notre navire et celle passée en paramètre renvoie une valeur, c'est que cette position est partagée, et donc qu'elle n'est pas utilisable !
def posExistAt(self, structureToTest):
if structureToTest.keys() & self.structure.keys() :
return True
else:
return False
La fonction buildStructure est la plus complexe de notre classe Ship car c'est elle qui construit notre tableau de coordonnées propre à la structure. Le code n'est pourtant pas si différent de la version de la bataille navale sans POO, nous l'avons juste transformé pour un usage avec notre nouveau système de coordonées utilisant des tuples et des dictionnaires.
Chaque Ship se voit donc attribuer un dictionnaire contenant autant d'éléments que le paramètre longueur. Le départ de notre calcul est donné par le paramètre position, et enfin la direction nous dit si il s'agit d'incrémenter la structure en se déplacant sur l'axe x ou l'axe y.
def buildStructure(self, pos, longueur, direction):
structure = {}
if direction == "v":
xPos= pos[1]
toYPos=pos[0] + longueur
for y in range(pos[0], toYPos):
structure[(y,xPos)]=True
else:
yPos=pos[0]
toXPos=pos[1] + longueur
for x in range(pos[1], toXPos):
structure[(yPos,x)]=True
print(structure)
return structure
Enfin, il nous faut une fonction destroy qui prend une position en paramètre. En fonction du nombre de structure encore valide (getStructureLife()) sur ce bateau, elle détermine aussi si le bateau est fonctionnel, ou si c'est devenu une épave inactive (self.alive).
def destroy(self,pos):
self.structure[pos] = False
if self.getStructureLife() == 0:
self.alive = False
The Player class
class Player:
def __init__(self, name, typeOf):
self.name = name
self.typeOf = typeOf
self.missedCoordinates = []
self.ship = []
Attributs
La classe Player va porter la responsabilité de sa flotte de Navire qui sera stockée dans un attribut self.ship
Nous savons qu'il y aura un joueur ordinateur, et un joueur humain, nous en profitons pour stocker cette différence à l'initialisation dans l'attribut self.typeof
Chaque joueur maintient également les positions (y,x) qu'il a déjà joué dans une liste de position self.missedCoordinates
Fonctions
On imagine les signatures suivantes pour nos fonctions
def selectCoordinate(self):
pass
def selectDirection(self):
pass
def addAShip(self, shipObject):
pass
def isThereOverlapAt(self, navireToTest):
pass
def isThereHitAt(self, pos):
pass
def getLifeOfShips(self):
pass
def isThereShipAlive(self):
pass
Les deux premières fonctions sont assez explicites, elle renvoie des coordonnées et une direction en fonction du type de joueur :
- si self.typeOf == "human" alors ces coordonnées sont saisies par l'utilisateur
- si self.typeOf == "AI" alors ces coordonnées et la direction sont calculées de façon random.
Ces fonctions ne teste ni de la validité de la position ou des coordonnées saisies, c'est une tâche que l'on va pour le moment délèguer à notre future classe Game dans la fonction d'initialisation des joueurs et de leur flottes de Navire playerInit(self, name, typeOf). C'est un choix, mais il pourrait en être autrement.
La fonction addAShip ajoute un objet de classe Ship à sa liste existante.
La fonction isThereOverlapAt(self,navireToTest) détermine si oui ou non il y existe une intersection entre la structure d'un objet Ship passé en paramètre (navireToTest) et la structure d'un des bateaux dans notre flotte. Si c'est le cas alors la fonction renvoie True
La fonction isThereHitAt(self,pos) prend le paramètre pos qui représente une cible à toucher dans notre flotte. Si une structure est concernée par cette position, et qu'elle n'a pas déjà été touché, alors on applique la fonction de détruction (destroy(pos)) sur ce navire à la position donnée. On renvoie l'objet Ship concerné par la destruction.
La fonction getLifeOfShips(self) renvoie un dictionnaire qui contient pour chaque bateau la liste des position / structures encore valide.
La fonction isThereShipAlive(self) parcourt la flotte pour voir si il reste encore un bateau dont l'attribut alive est à True. Si ce n'est pas le cas alors elle renvoie False, sinon True
The Game class
La classe Game intègre la logique de jeu ou les matrice des joueurs, avec la logique associée.
class Game:
def __init__(self,size):
self.size = size
self.players = []
self.typeOfShip = {"Croiseur": 4, "Porte-Avion": 5, "Sous-Marin": 3, "Torpilleur": 1, "Contre-Torpilleur":3}
self.symbol = {"ship_sink":"x", "miss":"o", "other":"~" }
self.symbolOfShip = {"Croiseur": "C", "Porte-Avion": "P", "Sous-Marin": "U", "Torpilleur": "V", "Contre-Torpilleur":"T"}
Les signatures de fonctions
def playerInit(self, name, typeOf):
pass
def run(self):
pass
def buildBaseDisplay(self):
pass
def prettyDisplay(self, matrix):
pass
def displayMap(self, player, layersToHide):
pass
def isPosIntoWorld(self, pos, shipSize, direction):
pass
La fonction playerInit() met en place tout les éléments du jeu avant son lancement :
- création et stockage des joueurs
Player - création, placement (et vérification) des positions de bateaux
Shippar les joueurs
La fonction run() contient la boucle de jeu principale, on organise l'alternance des combats en s'appuyant sur un dictionnaire définis ainsi :
Les 3 fonctions suivantes (buildBaseDisplay, prettyDisplay, displayMap) sont utilisé pour calculer la matrice d'affichage qui sera affiché pour chaque joueur à chaque tour.
Enfin la fonction isPosIntoWorld teste si un bateau qui est dans une position initiale pos, de longueur shipSize et de direction direction dépasse ou pas de nombre univers de jeu. Si c'est le cas alors cette fonction renvoie faux.