Created
April 27, 2017 20:50
-
-
Save debnet/9a3cd5689526848fecad11c9187fca23 to your computer and use it in GitHub Desktop.
Bataille Navale
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
import argparse | |
import csv | |
import re | |
import socket | |
# Expression régulière pour valider des coordonnées valides | |
COORDS_REGEX = re.compile(r'[A-J]([1-9]|10)') | |
# Liste des colonnes autorisées | |
COORDS_COLUMNS = 'ABCDEFGHIJ' | |
class Ship: | |
""" | |
Classe représentant un navire en jeu | |
""" | |
# Les différents types de navires possibles | |
# code : label, size, count | |
TYPES = { | |
'P': ("Porte-avion", 5, 1), | |
'C': ("Croiseur", 4, 2), | |
'D': ("Destroyeur", 3, 3), | |
'S': ("Sous-marin", 2, 4), | |
} | |
def __init__(self, type, orientation, position): | |
""" | |
Nouveau navire | |
:param type: Type (P, C, D, S) | |
:param orientation: Orientation (H(orizontal), V(ertical)) | |
:param position: Position de départ (coordonnées) | |
""" | |
# Récupère les informations techniques du type de navire | |
self.label, size, nombre = self.TYPES[type] | |
# Orientation du navire | |
horizontal, vertical = orientation == 'H', orientation == 'V' | |
# Couverture en cases des différentes parties du navire | |
self.parts = {} | |
# Convertion de la colonne en index chiffré et récupération du numéro de ligne | |
col, row = COORDS_COLUMNS.index(position[0]), int(position[1:]) | |
for chunk in range(size): | |
# Pour chaque fragment, on enregistre sa place sur la grille | |
coords = COORDS_COLUMNS[col] + str(row) | |
assert re.match(COORDS_REGEX, coords), "Le navire dépasse de l'aire de jeu." | |
self.parts[coords] = False # Fragment touché ou non (à faux par défaut) | |
# Incrémente la ligne ou de la colonne selon l'orientation | |
# On se sert ici du booléen comme un multiplicateur qui vaut soit 0 soit 1 | |
col, row = col + 1 * vertical, row + 1 * horizontal | |
@property | |
def cells(self): | |
# Une propriété pour récupérer les différentes cases couvertes par ce navire | |
return set(self.parts.keys()) | |
@property | |
def sunk(self): | |
# Une propriété permettant de savoir si le navire a coulé ou non | |
# On parcourt tous les fragments du navire pour le déterminer | |
return all(value for value in self.parts.values()) | |
@classmethod | |
def parse(cls, file): | |
""" | |
Méthode permettant de générer des navires à partir d'un fichier CSV | |
Le contenu du fichier doit être de la forme suivante : | |
Type;Orientation;Position (ex: P;H;A1) | |
:param file: Chemin vers le fichier CSV | |
:return: Liste de navires | |
""" | |
ships = [] | |
cells = set() | |
# Ouverture du fichier en texte | |
with open(file, 'r') as f: | |
# Interprétation du fichier comme un CSV séparé par des ";" | |
for data in csv.reader(f, delimiter=';'): | |
# Construction d'un navire à partir des données du CSV | |
# qui sont dans le même ordre que les arguments du constructeur | |
ship = cls(*data) | |
# Vérification qu'un navire n'occupe pas la place d'un autre | |
assert not cells or not (cells & ship.cells), "Un navire occupe le même espace qu'un autre." | |
# Ajout du nouveau navire à la liste | |
ships.append(ship) | |
return ships | |
def __str__(self): | |
return self.label | |
class Game: | |
""" | |
Gestionnaire de jeu (serveur ou client) | |
""" | |
# Liste des différents états retournés par les clients/serveurs | |
STATES = { | |
'T': "Touché !", | |
'C': "Coulé !", | |
'R': "Raté...", | |
'G': "Gagné ! :)", | |
} | |
def __init__(self, file, host='localhost', port=12345, server=False): | |
""" | |
Constructeur d'une partie | |
:param file: Chemin vers le fichier CSV du positionnement des navires (voir Ship.parse) | |
:param host: IP d'écoute du serveur ou de connexion du client | |
:param port: Port d'écoute du serveur ou de connexion du client | |
:param server: Exécuté en tant que serveur ou client ? | |
""" | |
# Liste des navires du joueur à partir du fichier CSV | |
self.ships = Ship.parse(file) | |
# Historiques des coups du joueur et de son adversaire | |
self.my_hits, self.your_hits = {}, {} | |
self.server = server | |
self.address = (host, port) | |
# Création du socket commun (client/serveur) | |
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
if server: | |
# En tant que serveur, on attend qu'un seul client se connecte à nous | |
print(f"Le serveur écoute sur {self.address}") | |
self.socket.bind(self.address) | |
self.socket.listen() | |
# On substitue le socket serveur (désormais inutile) par le socket client | |
self.socket, client_address = self.socket.accept() | |
print(f"Nouvelle connexion de {client_address}") | |
else: | |
# En tant que client, on se connecte juste au serveur | |
self.socket.connect(self.address) | |
print(f"Connecté à {self.address}") | |
# Boucle de jeu principale | |
self.game_over = False | |
if not self.server: | |
# Le client porte le premier coup | |
self.do_hit() | |
# Tant que la partie n'est pas terminée | |
while not self.game_over: | |
self.get_hit() # Attendre et gérer le coup de l'adversaire | |
if self.game_over: | |
break | |
self.do_hit() # Porter un coup à l'adversaire | |
self.socket.close() | |
@property | |
def has_ships(self): | |
# Permet de savoir si le joueur possède encore au moins un navire | |
return not all(ship.sunk for ship in self.ships) | |
def get_ship(self, coords): | |
""" | |
Permet de retrouver un navire à partir de coordonnées | |
:param coords: Coordonnées | |
:return: Navire ou rien si non trouvé | |
""" | |
for ship in self.ships: | |
if coords in ship.parts: | |
return ship | |
return None # Inutile, mais "explicit is better than implicit" | |
def send(self, data): | |
""" | |
Fonction utilitaire pour envoyer une chaîne via socket | |
""" | |
data = data.upper().encode() | |
return self.socket.sendall(data) | |
def receive(self): | |
""" | |
Fonction utilitaire pour recevoir une chaîne via socket | |
""" | |
data = self.socket.recv(100) | |
return data.decode().upper() | |
def check_coords(self, coords): | |
""" | |
Vérifie qu'une coordonnées est valide et n'a pas déjà été utilisée | |
""" | |
return re.match(COORDS_REGEX, coords or '') and coords not in self.my_hits | |
def do_hit(self): | |
""" | |
Demande au joueur une coordonnée à attaquer chez l'adversaire | |
""" | |
coords, state = None, None | |
# On boucle tant l'on touche ou coule un navire | |
while state in ['T', 'C'] or not state: | |
while not self.check_coords(coords): | |
self.print() # Affichage des grilles | |
coords = input("Coordonnées : ") | |
self.send(coords) # Envoie les coordonnées de l'attaque à l'adversaire | |
state = self.receive() # Attend la réponse de l'adversaire | |
self.my_hits[coords] = state # Garde une trace dans l'historique | |
print(self.STATES.get(state)) # Affiche le résultat de l'attaque | |
if state == 'G': # En cas de victoire, c'est terminé | |
self.game_over = True | |
break | |
coords = None | |
def get_hit(self): | |
""" | |
Attend de recevoir une attaque de la part de l'adversaire | |
""" | |
state = None | |
# On boucle tant que l'adversaire touche ou coule nos navires | |
while state in ['T', 'C'] or not state: | |
state = 'R' # Par défaut, le statut est "raté" | |
defeat = True | |
coords = self.receive() # On reçoit les coordonnées de l'attaque | |
# On parcourt tous les navires du joueur | |
for ship in self.ships: | |
# Si le navire est touché | |
if coords in ship.parts: | |
ship.parts[coords] = True # On détruit le fragment ciblé | |
if ship.sunk: # Dans le cas où le navire est coulé | |
state = 'C' | |
print(f"{ship} coulé en {coords} !") | |
else: # ... et dans le cas où il est juste touché | |
state = 'T' | |
print(f"{ship} touché en {coords} !") | |
# On regarde pour chaque navire s'il a été coulé pour déclarer la défaite | |
defeat &= ship.sunk | |
# On garde l'historique des attaques de l'adversaire | |
self.your_hits[coords] = state | |
# Le retour change en cas de défaite | |
state = 'G' if defeat else state | |
if defeat: | |
self.game_over = True | |
print(f"Perdu ! :(") | |
# Envoie le retour à l'adversaire | |
self.send(state) | |
def print(self): | |
""" | |
Fonction bordélique pour afficher deux belles grilles : | |
- La première contient les tentatives du joueur sur l'adversaire | |
- La deuxième présente ses propres navires ainsi que les tentatives de l'adversaire | |
:return: Rien | |
""" | |
# Petite fonction interne pour les états de chaque case de la première grille | |
def get_mines(row, col): | |
coords = col + str(row) | |
return {'T': 'X', 'C': 'X', 'R': 'O'}.get(self.my_hits.get(coords), ' ') | |
# Petite fonction interne pour les états de chaque case de la seconde grille avec les navires | |
def get_yours(row, col): | |
coords = col + str(row) | |
cell = {'T': 'X', 'C': 'X', 'R': 'O'}.get(self.your_hits.get(coords)) | |
if cell: | |
return cell | |
return '#' if self.get_ship(coords) else ' ' | |
# C'est juste de l'affichage un minimum beau | |
header = [c.center(3) for c in COORDS_COLUMNS] | |
headers = [' ' * 4] + header + [' ' * 5] + header | |
print(*headers) | |
for row in range(1, 11): | |
print(' ', ('+---' * 10) + '+', ' ', ('+---' * 10) + '+') | |
line = [str(row).center(3), '| ' + ' | '.join([get_mines(row, col) for col in COORDS_COLUMNS]) + ' |', | |
str(row).center(3), '| ' + ' | '.join([get_yours(row, col) for col in COORDS_COLUMNS]) + ' |'] | |
print(*line) | |
print(' ', ('+---' * 10) + '+', ' ', ('+---' * 10) + '+') | |
# Gestion des arguments d'appel au script | |
parser = argparse.ArgumentParser() | |
parser.add_argument('file', type=str) | |
parser.add_argument('--host', type=str, default='localhost', dest='host') | |
parser.add_argument('--port', type=int, default=12345, dest='port') | |
parser.add_argument('--server', action='store_true', default=False, dest='server') | |
args = parser.parse_args() | |
if args: | |
# Démarrage d'une session de jeu | |
Game(args.file, args.host, args.port, args.server) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment