Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # 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)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement