Advertisement
Guest User

Untitled

a guest
Apr 27th, 2017
77
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.96 KB | None | 0 0
  1. # coding: utf-8
  2. import argparse
  3. import csv
  4. import re
  5. import socket
  6.  
  7.  
  8. # Expression régulière pour valider des coordonnées valides
  9. COORDS_REGEX = re.compile(r'[A-J]([1-9]|10)')
  10. # Liste des colonnes autorisées
  11. COORDS_COLUMNS = 'ABCDEFGHIJ'
  12.  
  13.  
  14. class Ship:
  15. """
  16. Classe représentant un navire en jeu
  17. """
  18. # Les différents types de navires possibles
  19. # code : label, size, count
  20. TYPES = {
  21. 'P': ("Porte-avion", 5, 1),
  22. 'C': ("Croiseur", 4, 2),
  23. 'D': ("Destroyeur", 3, 3),
  24. 'S': ("Sous-marin", 2, 4),
  25. }
  26.  
  27. def __init__(self, type, orientation, position):
  28. """
  29. Nouveau navire
  30. :param type: Type (P, C, D, S)
  31. :param orientation: Orientation (H(orizontal), V(ertical))
  32. :param position: Position de départ (coordonnées)
  33. """
  34. # Récupère les informations techniques du type de navire
  35. self.label, size, nombre = self.TYPES[type]
  36. # Orientation du navire
  37. horizontal, vertical = orientation == 'H', orientation == 'V'
  38.  
  39. # Couverture en cases des différentes parties du navire
  40. self.parts = {}
  41. # Convertion de la colonne en index chiffré et récupération du numéro de ligne
  42. col, row = COORDS_COLUMNS.index(position[0]), int(position[1:])
  43. for chunk in range(size):
  44. # Pour chaque fragment, on enregistre sa place sur la grille
  45. coords = COORDS_COLUMNS[col] + str(row)
  46. assert re.match(COORDS_REGEX, coords), "Le navire dépasse de l'aire de jeu."
  47. self.parts[coords] = False # Fragment touché ou non (à faux par défaut)
  48. # Incrémente la ligne ou de la colonne selon l'orientation
  49. # On se sert ici du booléen comme un multiplicateur qui vaut soit 0 soit 1
  50. col, row = col + 1 * vertical, row + 1 * horizontal
  51.  
  52. @property
  53. def cells(self):
  54. # Une propriété pour récupérer les différentes cases couvertes par ce navire
  55. return set(self.parts.keys())
  56.  
  57. @property
  58. def sunk(self):
  59. # Une propriété permettant de savoir si le navire a coulé ou non
  60. # On parcourt tous les fragments du navire pour le déterminer
  61. return all(value for value in self.parts.values())
  62.  
  63. @classmethod
  64. def parse(cls, file):
  65. """
  66. Méthode permettant de générer des navires à partir d'un fichier CSV
  67. Le contenu du fichier doit être de la forme suivante :
  68. Type;Orientation;Position (ex: P;H;A1)
  69. :param file: Chemin vers le fichier CSV
  70. :return: Liste de navires
  71. """
  72. ships = []
  73. cells = set()
  74. # Ouverture du fichier en texte
  75. with open(file, 'r') as f:
  76. # Interprétation du fichier comme un CSV séparé par des ";"
  77. for data in csv.reader(f, delimiter=';'):
  78. # Construction d'un navire à partir des données du CSV
  79. # qui sont dans le même ordre que les arguments du constructeur
  80. ship = cls(*data)
  81. # Vérification qu'un navire n'occupe pas la place d'un autre
  82. assert not cells or not (cells & ship.cells), "Un navire occupe le même espace qu'un autre."
  83. # Ajout du nouveau navire à la liste
  84. ships.append(ship)
  85. return ships
  86.  
  87. def __str__(self):
  88. return self.label
  89.  
  90.  
  91. class Game:
  92. """
  93. Gestionnaire de jeu (serveur ou client)
  94. """
  95. # Liste des différents états retournés par les clients/serveurs
  96. STATES = {
  97. 'T': "Touché !",
  98. 'C': "Coulé !",
  99. 'R': "Raté...",
  100. 'G': "Gagné ! :)",
  101. }
  102.  
  103. def __init__(self, file, host='localhost', port=12345, server=False):
  104. """
  105. Constructeur d'une partie
  106. :param file: Chemin vers le fichier CSV du positionnement des navires (voir Ship.parse)
  107. :param host: IP d'écoute du serveur ou de connexion du client
  108. :param port: Port d'écoute du serveur ou de connexion du client
  109. :param server: Exécuté en tant que serveur ou client ?
  110. """
  111. # Liste des navires du joueur à partir du fichier CSV
  112. self.ships = Ship.parse(file)
  113. # Historiques des coups du joueur et de son adversaire
  114. self.my_hits, self.your_hits = {}, {}
  115.  
  116. self.server = server
  117. self.address = (host, port)
  118. # Création du socket commun (client/serveur)
  119. self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  120. self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  121.  
  122. if server:
  123. # En tant que serveur, on attend qu'un seul client se connecte à nous
  124. print(f"Le serveur écoute sur {self.address}")
  125. self.socket.bind(self.address)
  126. self.socket.listen()
  127. # On substitue le socket serveur (désormais inutile) par le socket client
  128. self.socket, client_address = self.socket.accept()
  129. print(f"Nouvelle connexion de {client_address}")
  130. else:
  131. # En tant que client, on se connecte juste au serveur
  132. self.socket.connect(self.address)
  133. print(f"Connecté à {self.address}")
  134.  
  135. # Boucle de jeu principale
  136. self.game_over = False
  137. if not self.server:
  138. # Le client porte le premier coup
  139. self.do_hit()
  140. # Tant que la partie n'est pas terminée
  141. while not self.game_over:
  142. self.get_hit() # Attendre et gérer le coup de l'adversaire
  143. if self.game_over:
  144. break
  145. self.do_hit() # Porter un coup à l'adversaire
  146. self.socket.close()
  147.  
  148. @property
  149. def has_ships(self):
  150. # Permet de savoir si le joueur possède encore au moins un navire
  151. return not all(ship.sunk for ship in self.ships)
  152.  
  153. def get_ship(self, coords):
  154. """
  155. Permet de retrouver un navire à partir de coordonnées
  156. :param coords: Coordonnées
  157. :return: Navire ou rien si non trouvé
  158. """
  159. for ship in self.ships:
  160. if coords in ship.parts:
  161. return ship
  162. return None # Inutile, mais "explicit is better than implicit"
  163.  
  164. def send(self, data):
  165. """
  166. Fonction utilitaire pour envoyer une chaîne via socket
  167. """
  168. data = data.upper().encode()
  169. return self.socket.sendall(data)
  170.  
  171. def receive(self):
  172. """
  173. Fonction utilitaire pour recevoir une chaîne via socket
  174. """
  175. data = self.socket.recv(100)
  176. return data.decode().upper()
  177.  
  178. def check_coords(self, coords):
  179. """
  180. Vérifie qu'une coordonnées est valide et n'a pas déjà été utilisée
  181. """
  182. return re.match(COORDS_REGEX, coords or '') and coords not in self.my_hits
  183.  
  184. def do_hit(self):
  185. """
  186. Demande au joueur une coordonnée à attaquer chez l'adversaire
  187. """
  188. coords, state = None, None
  189. # On boucle tant l'on touche ou coule un navire
  190. while state in ['T', 'C'] or not state:
  191. while not self.check_coords(coords):
  192. self.print() # Affichage des grilles
  193. coords = input("Coordonnées : ")
  194. self.send(coords) # Envoie les coordonnées de l'attaque à l'adversaire
  195. state = self.receive() # Attend la réponse de l'adversaire
  196. self.my_hits[coords] = state # Garde une trace dans l'historique
  197. print(self.STATES.get(state)) # Affiche le résultat de l'attaque
  198. if state == 'G': # En cas de victoire, c'est terminé
  199. self.game_over = True
  200. break
  201. coords = None
  202.  
  203. def get_hit(self):
  204. """
  205. Attend de recevoir une attaque de la part de l'adversaire
  206. """
  207. state = None
  208. # On boucle tant que l'adversaire touche ou coule nos navires
  209. while state in ['T', 'C'] or not state:
  210. state = 'R' # Par défaut, le statut est "raté"
  211. defeat = True
  212. coords = self.receive() # On reçoit les coordonnées de l'attaque
  213. # On parcourt tous les navires du joueur
  214. for ship in self.ships:
  215. # Si le navire est touché
  216. if coords in ship.parts:
  217. ship.parts[coords] = True # On détruit le fragment ciblé
  218. if ship.sunk: # Dans le cas où le navire est coulé
  219. state = 'C'
  220. print(f"{ship} coulé en {coords} !")
  221. else: # ... et dans le cas où il est juste touché
  222. state = 'T'
  223. print(f"{ship} touché en {coords} !")
  224. # On regarde pour chaque navire s'il a été coulé pour déclarer la défaite
  225. defeat &= ship.sunk
  226. # On garde l'historique des attaques de l'adversaire
  227. self.your_hits[coords] = state
  228. # Le retour change en cas de défaite
  229. state = 'G' if defeat else state
  230. if defeat:
  231. self.game_over = True
  232. print(f"Perdu ! :(")
  233. # Envoie le retour à l'adversaire
  234. self.send(state)
  235.  
  236. def print(self):
  237. """
  238. Fonction bordélique pour afficher deux belles grilles :
  239. - La première contient les tentatives du joueur sur l'adversaire
  240. - La deuxième présente ses propres navires ainsi que les tentatives de l'adversaire
  241. :return: Rien
  242. """
  243.  
  244. # Petite fonction interne pour les états de chaque case de la première grille
  245. def get_mines(row, col):
  246. coords = col + str(row)
  247. return {'T': 'X', 'C': 'X', 'R': 'O'}.get(self.my_hits.get(coords), ' ')
  248.  
  249. # Petite fonction interne pour les états de chaque case de la seconde grille avec les navires
  250. def get_yours(row, col):
  251. coords = col + str(row)
  252. cell = {'T': 'X', 'C': 'X', 'R': 'O'}.get(self.your_hits.get(coords))
  253. if cell:
  254. return cell
  255. return '#' if self.get_ship(coords) else ' '
  256.  
  257. # C'est juste de l'affichage un minimum beau
  258. header = [c.center(3) for c in COORDS_COLUMNS]
  259. headers = [' ' * 4] + header + [' ' * 5] + header
  260. print(*headers)
  261. for row in range(1, 11):
  262. print(' ', ('+---' * 10) + '+', ' ', ('+---' * 10) + '+')
  263. line = [str(row).center(3), '| ' + ' | '.join([get_mines(row, col) for col in COORDS_COLUMNS]) + ' |',
  264. str(row).center(3), '| ' + ' | '.join([get_yours(row, col) for col in COORDS_COLUMNS]) + ' |']
  265. print(*line)
  266. print(' ', ('+---' * 10) + '+', ' ', ('+---' * 10) + '+')
  267.  
  268.  
  269. # Gestion des arguments d'appel au script
  270. parser = argparse.ArgumentParser()
  271. parser.add_argument('file', type=str)
  272. parser.add_argument('--host', type=str, default='localhost', dest='host')
  273. parser.add_argument('--port', type=int, default=12345, dest='port')
  274. parser.add_argument('--server', action='store_true', default=False, dest='server')
  275. args = parser.parse_args()
  276. if args:
  277. # Démarrage d'une session de jeu
  278. Game(args.file, args.host, args.port, args.server)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement