#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
TP AP1
Licence SESI 1ère année
Univ. Lille 1

bataille_navale.py

Module pour le jeu de bataille navale.

Pour une description générale du jeu voir
http://fr.wikipedia.org/wiki/Bataille_navale_%28jeu%29

"""

__author__ = 'BEAUSSART Jean-loup & PREUD\'HOMME Geoffrey'
__date_creation__ = 'Mon, 16 Feb 2015 19:30:54 +0100'


###############################################
# Modules utilisés
###############################################

# Pour la disposition aléatoire des navires
from random import randint

# Pour le fichier des scores
from datetime import datetime

###############################################
# Constantes nommées utilisées
###############################################

# les deux dispositions possibles pour un navire
#  sur le plateau :
# - H : horizontale
# - V : verticale
DISPOSITIONS = "HV"


# codes réponses d'analyse de tirs
RATE = 0
TOUCHE = 1
COULE = 2

# nom du fichier contenant les scores réalisés
FICHIER_RESULT = 'bataille_navale_scores.txt'

###############################################
# Procédure principale du jeu
###############################################


def jouer(nom, descr):
    """
    str, str -> ()
    procédure de jeu complet de bataille navale,
    le nom du joueur est donné par le paramètre nom,
    et le jeu est décrit dans le fichier descr.

    CU : le fichier jeu doit exister et être conforme
    à un fichier de description.
    """
    jeu = cree_jeu(descr)
    decrire_le_jeu(jeu)
    nbre_tirs = 0
    while not tous_coules(jeu):
        # afficher_jeu(jeu) # Décommenter pour une nouvelle fonctionalité
        tir = lire_un_tir(nom)
        nbre_tirs += 1
        nav, res = analyse_un_tir(jeu, tir)
        if res == RATE:
            print("raté.")
        elif res == TOUCHE:
            print(nav + " touché.")
        else:
            print(nav + " coulé.")
    sauver_result(nom, descr, nbre_tirs)
    print("Terminé en {0} tirs".format(nbre_tirs))

###############################################
# Opérations sur les fichiers
###############################################


def lire_donnees(num_descr):
    """
    str -> tuple
    renvoie un triplet dont les deux premières composantes sont
    et la troisième une liste de couples (nature, taille) où
    nature est une chaîne de caractères décrivant la nature du navire
    et taille un entier désignant le nombre de cases occupées par ce navire.
    Toutes ces données sont lues dans un fichier nommé 'jeu'+num_descr+'.txt'.

    CU : le fichier 'jeu'+num_descr+'.txt' doit exister et être au bon format,
    ie un  fichier texte contenant :
    larg : haut
    nature1 : taille1
    nature2 : taille2
    ...
    """
   # with s'occupe de fermer automatiquement le fichier ouvert, c'est un gestionnaire de contexte.
   # Il ferme le fichier à la fin du bloc, si une exception est levée ou si on rencontre un
   # break/return, autrement dit, quoi qu'il arrive
    with open('jeu%s.txt' % num_descr, 'r') as fichier:
        dimensions = fichier.readline().split(':')
        largeur, hauteur = int(dimensions[0].strip()), int(dimensions[1].strip())

        navires = list()

        for ligne in fichier:
            couple = ligne.split(':')
            navires.append((couple[0].strip(), int(couple[1].strip())))

        return (largeur, hauteur, navires)

# Tests
# print(lire_donnees('1'))
# print(lire_donnees('2'))
# print(lire_donnees('3')) # Fichier personnalisé


# Sauvegarde du bilan

# >>> datetime.today()
# datetime.datetime(2015, 2, 16, 19, 6, 17, 207811)
# On obtient la date et l'heure sous forme d'objet datetime

# >>> str(datetime.today())
# '2015-02-16 19:07:31.624846'
# On obtient la date et l'heure au format AAAA-MM-JJ HH:MM:SS.UUUUUU


def sauver_result(nom, jeu, nbre):
    """
    str, str, int -> NoneType
    ajoute une ligne dans le fichier FICHIER_RESULT
    contenant le nom, le numéro du jeu joué et le nombre de tirs effectués
    dans la partie.

    CU : aucune
    """

    with open(FICHIER_RESULT, 'a') as fichierScores:
        fichierScores.write(':'.join([nom, str(jeu), str(nbre), str(datetime.today())]) + '\n')

# sauver_result('Pisitrate', 2, 125)
# sauver_result('Aristide', 52, 12)
# sauver_result('Caligula', 1112, 12356)
# sauver_result('Périclès', 12, 1458)

###############################################
# Procédures de construction du jeu
###############################################<


def cree_jeu(descr):
    """
    str -> dict
    renvoie un nouveau jeu de bataille navale construit à partir des données
    lues dans le fichier descr.


    Le jeu est représenté par un dictionnaire à quatre clés :
    - 'plateau' pour représenter le plateau de jeu (l'espace maritime) avec ses navires
    - 'nb_cases_occupees' dont la valeur associée est le nombre de cases du plateau
                          occupées par les navires
    - 'touches' dictionnaire contenant deux champs :
                * l'un nomme 'nb_touches' contenant un entier
                * l'autre nomme 'etats_navires' qui contient un dictionnaire
                  donnant pour chaque navire le nombre de tirs
                  qu'ils peuvent encore recevoir avant d'être coulés
    - 'coups_joues' ensemble des coups joués depuis le début de la partie.

    CU : le fichier doit contenir une description correcte du jeu (cf lire_donnees)
    """

    donnees = lire_donnees(descr)
    nb_cases_occupees = 0
    etats_navires = dict()
    for n in donnees[2]:
        etats_navires[n[0]] = n[1]
        nb_cases_occupees += n[1]
    return {'plateau': cree_plateau(*donnees), 'nb_cases_occupees' : nb_cases_occupees, \
        'touches': {'nb_touches': 0, 'etats_navires': etats_navires}, 'coups_joues': set()}

# Tests
# print(cree_jeu('1'))
# print(cree_jeu('2'))
# print(cree_jeu('3'))

def cree_plateau(l, h, l_nav):
    """
    int, int, list -> dict
    renvoie un plateau de largeur l et de hauteur h occupé par les navires
    de l_nav.
    La disposition est aléatoire.

    CU : les dimensions doivent permettre le placement de tous les navires
    """
    esp = {'larg': l, 'haut': h}
    for nav in l_nav:
        placer(esp, nav)
    return esp

def cases_du_navire(nav, pos, disp):
    """
    tuple, tuple, str -> list
    fournit une liste des cases occupées par un navire nav
    aux coordonnées pos dans la disposition disp
    CU : disp = 'H' ou 'V'
    """
    assert(disp in DISPOSITIONS), 'disp = \'H\' ou \'V\''

    cases = list()
    if disp == 'H':
        for i in range(pos[0], pos[0]+nav[1]):
            cases.append((i, pos[1]))
    elif disp == 'V':
        for i in range(pos[1], pos[1]+nav[1]):
            cases.append((pos[0], i))
    return cases

def est_placable(esp, nav, pos, disp):
    """
    dict, tuple, tuple, str -> bool
    indique si le navire nav aux coordonnées pos peut être posé dans la
    disposition disp dans l'espace maritime esp
    CU : disp = 'H' ou 'V'
    """

    assert(disp in DISPOSITIONS), 'disp = \'H\' ou \'V\''

    for c in cases_du_navire(nav, pos, disp):
        if c[0] > esp['larg'] or c[0] <= 0 or c[1] <= 0 or c[1] > esp['haut'] or c in esp:
            return False
    return True

# Tests
# espTest = {'larg': 4, 'haut': 3, (1, 2): 'grand', (2, 2): 'grand', (4, 1): 'petit'}
# print(est_placable(espTest, ('Poutre', 3), (1, 1), 'H')) # True
# print(est_placable(espTest, ('Buche', 3), (1, 1), 'V')) # False
# print(est_placable(espTest, ('Règle', 3), (3, 1), 'V')) # True
# print(est_placable(espTest, ('Antenne', 3), (3, 2), 'V')) # False

def placer(esp, nav):
    """
    dict, tuple -> NoneType
    place le navire nav dans l'espace maritime esp.
    Choix de l'emplacement aléatoire.

    CU : il doit rester de la place
    """

    while True:
        pos = (randint(0, esp['larg']-1), randint(0, esp['haut']-1))
        disp = DISPOSITIONS[randint(0, 1)]
        if est_placable(esp, nav, pos, disp):
            for c in cases_du_navire(nav, pos, disp):
                esp[c] = nav[0]
            return

# Tests
# espTest2 = {'larg': 4, 'haut': 3}
# placer(espTest2, ('grand', 2))
# placer(espTest2, ('petit', 1))
# print(espTest2) # Aléatoire


###############################################
# Procédures de déroulement du jeu
###############################################

def decrire_le_jeu(jeu):
    """
    dict -> ()
    imprime une description du jeu.

    CU : aucune
    """
    print('Dimensions du plateau de jeu :')
    print('- largeur : %d' % jeu['plateau']['larg'])
    print('- hauteur : %d' % jeu['plateau']['haut'])

    print('Navires :')
    etats = jeu['touches']['etats_navires']
    for navire in etats:
        print('- %s : %d case%s' %
              (navire, etats[navire], 's' * (etats[navire] >= 2)))
    print(
        'À vous de jouer en répondant à l\'invite ?- par deux nombres séparés par une virgule.')

def afficher_jeu(jeu):
    """
    dict -> ()
    imprime une description visuelle du jeu

    CU : aucune
    """
    if jeu['plateau']['larg'] > 30:
        return
    s = ''
    for x in range(1, jeu['plateau']['larg']+1):
        for y in range(1, jeu['plateau']['haut']+1):
            if (x, y) in jeu['coups_joues']:
                if (x, y) in jeu['plateau']:
                    s += 'o '
                else:
                    s += 'x '
            else:
                s += '. '
        s += '\n'
    print(s)

def lire_un_tir(nom):
    """
    str -> tuple
    renvoie un couple d'entiers lus sur l'entrée standard

    CU : l'entrée doit être de la forme xxx,yyy avec xxx et yyy
         une représentation décimale de deux nombres entiers
    """
    while True:
        try:
            entree = input('%s - ' % nom)
            dec = entree.split(',')
            x, y = int(dec[0]), int(dec[1])
            assert x > 0 and y > 0
            return (x, y)
        except KeyboardInterrupt:
            sys.exit(0)
        except IndexError:
            print('Merci de saisir deux composantes séparées par une virgule')
        except ValueError:
            print('Merci de saisir des composantes entières')
        except AssertionError:
            print('Merci de saisir des composantes strictement positives')


def analyse_un_tir(jeu, tir):
    """
    dict, tuple -> str,int
    renvoie
      - ("",RATE) si tir raté
      - (nav,TOUCHE) si nav touché
      - (nav,COULE) si nav coulé
    et modifie l'état du jeu en conséquence.

    CU : aucune
    """
    if tir in jeu['coups_joues']:
        return ('', RATE)
    elif tir in jeu['plateau']:
        jeu['coups_joues'].add(tir)
        nav = jeu['plateau'][tir]
        jeu['touches']['nb_touches'] += 1
        jeu['touches']['etats_navires'][nav] += -1
        if jeu['touches']['etats_navires'][nav] > 0:
            return (nav, TOUCHE)
        else:
            return (nav, COULE)
    else:
        jeu["coups_joues"].add(tir)
        return ('', RATE)

# Tests
# test = {'nb_cases_occupees': 3, 'plateau': {'haut': 3, (2, 3): 'grand', 'larg': 4, (2, 1): \
# 'petit', (2, 2): 'grand'}, 'coups_joues': set((1, 2)), 'touches': {'etats_navires': {'grand': 2, \
# 'petit': 1}, 'nb_touches': 0}}
# print(analyse_un_tir(test, (1, 1)) == ('', RATE))
# print(analyse_un_tir(test, (1, 2)) == ('', RATE))
# print(analyse_un_tir(test, (2, 2)) == ('grand', TOUCHE))
# print(analyse_un_tir(test, (2, 1)) == ('petit', COULE))

def tous_coules(jeu):
    """
    dict -> bool
    renvoie True si tous les navires du plateau de jeu ont été coulés
            et False dans le cas contraire.

    CU : aucune
    """
    return jeu['touches']['nb_touches'] >= jeu['nb_cases_occupees']


###############################################
# Pour une utilisation du module depuis un terminal
###############################################

if __name__ == '__main__':
    import sys
    if len(sys.argv) != 3:
        jouer('Jean Bart', '1')
    else:
        jouer(sys.argv[1], sys.argv[2])