Skip to content

Instantly share code, notes, and snippets.

@debnet
Created April 27, 2017 20:50
Show Gist options
  • Save debnet/9a3cd5689526848fecad11c9187fca23 to your computer and use it in GitHub Desktop.
Save debnet/9a3cd5689526848fecad11c9187fca23 to your computer and use it in GitHub Desktop.
Bataille Navale
# 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