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

# logiciel pour Python3

# (c) 2014 Raphaël SEBAN (tarball69)
# publié sous licence GNU General Public License GPLv3
# cf http://www.gnu.org/licenses/gpl.html

# on demande en premier lieu
# la librairie graphique Tkinter
from tkinter import *

# on veut aussi utiliser
# les nombres aléatoires
import random

# ------------------- DEBUT DU PROGRAMME -------------------------------

def debut_jeu ():
    "le programme commence ici"
    # init variables globales
    global fenetre, canvas, bouton_jouer
    global c_width, c_height, center_x, center_y
    # on crée la fenêtre principale
    fenetre = Tk()
    fenetre.title("Mon super jeu !")
    fenetre.resizable(width=False, height=False)
    # on ajoute des composants graphiques
    canvas = Canvas(fenetre, bg="black", width=400, height=300)
    canvas.pack(padx=5, pady=5)
    # on met des données courantes en globales
    c_width = canvas.winfo_reqwidth()
    c_height = canvas.winfo_reqheight()
    center_x = c_width // 2
    center_y = c_height // 2
    # bouton 'jouer'
    bouton_jouer = Button(fenetre)
    bouton_jouer.pack(side=LEFT, padx=50, pady=5)
    # bouton 'quitter'
    Button(
        fenetre, text="Quitter", command=fenetre.destroy
    ).pack(side=RIGHT, padx=5, pady=5)
    # on affiche une présentation du jeu
    presenter_jeu()
    # et on lance la
    # boucle événementielle principale
    fenetre.mainloop()
# end def

def presenter_jeu ():
    "on affiche ici une présentation du jeu"
    # on commence toujours par effacer le canevas graphique
    # qui nous fait office d'écran de jeu
    canvas.delete(ALL)
    # on affiche un titre
    # par rapport au centre de l'écran de jeu
    canvas.create_text(
        center_x, center_y - 100,
        text="Terrain miné",
        font="sans 16 bold",
        fill="navajowhite2",        # couleur du texte
    )
    # on affiche des règles du jeu succinctes
    canvas.create_text(
        center_x, center_y + 30,
        text="""\
Règles du jeu:

Vous devez déplacer une grosse boule vers un carré placé en bas de l'écran.
Malheureusement pour vous, le terrain est miné !
Saurez-vous atteindre votre but sans vous faire exploser ?
Utilisez les flèches du clavier pour vous déplacer.
        """,
        justify=LEFT,
        font="sans 10 bold italic",
        fill="Pale Goldenrod",
        width=0.8 * c_width,        # 80% de la largeur totale
    )
    # on s'assure que le joueur pourra (re)jouer
    bouton_jouer.configure(
        text="Jouer !", command=nouvelle_partie, state=NORMAL
    )
# end def

def nouvelle_partie ():
    "on réinitialise une nouvelle partie ici"
    # init variables globales
    global tid_joueur, tid_mines
    # on autorise le joueur à arrêter la partie quand il le souhaite
    bouton_jouer.configure(
        text="Stop !", command=arreter_partie, state=NORMAL
    )
    # on efface le canevas graphique
    canvas.delete(ALL)
    # on crée l'avatar du joueur (boule)
    creer_joueur()
    # on crée le carré d'arrivée
    creer_carre_final()
    # on mine le terrain
    miner_terrain()
    # on associe les touches fléchées du clavier
    # à nos propres fonctions de gestion du mouvement
    fenetre.bind_all("<Up>", deplacer_haut)
    fenetre.bind_all("<Down>", deplacer_bas)
    fenetre.bind_all("<Left>", deplacer_gauche)
    fenetre.bind_all("<Right>", deplacer_droite)
    # c'est ici qu'on lance les boucles inscrites dans le temps
    tid_joueur = fenetre.after(100, boucle_joueur)
    # les mines aussi vont avoir leur propre cycle de vie
    tid_mines = fenetre.after(1000, boucle_mines)
# end def

def creer_joueur ():
    """
        on crée l'avatar du joueur;
        ici le joueur est représenté par une grosse boule;
        on conserve l'identifiant de l'objet sur le canevas;
        ça nous servira plus tard;
        on prend des coordonnées au hasard;
    """
    # init variables globales
    global diametre_boule, rayon
    global boule_x, boule_y
    global sens_x, sens_y, pas
    global boule_joueur
    # inits
    diametre_boule = 20         # diamètre de la boule
    rayon = diametre_boule // 2
    boule_x = diametre_boule + random.randint(0, c_width - diametre_boule)
    boule_y = diametre_boule + random.randint(0, 10)
    sens_x, sens_y = (0, 0)     # sens de déplacement initial
    pas = random.randint(2, 5)  # quantité de déplacement (pixels)
    # on dessine l'avatar joueur (boule)
    boule_joueur = canvas.create_oval(
        boule_x - rayon, boule_y - rayon,
        boule_x + rayon, boule_y + rayon,
        outline="ivory2",       # couleur du contour
        width=1,                # épaisseur du trait de contour (pixels)
        fill="light sky blue",  # couleur du remplissage
    )
# end def

def creer_carre_final ():
    """
        on crée le carré final (carré d'arrivée);
        on conserve l'identifiant de l'objet sur le canevas;
        ça nous servira plus tard;
        on prend des coordonnées au hasard;
    """
    # init variables globales
    global taille_carre, demi_carre
    global carre_x, carre_y
    global carre_final
    # on crée le carré d'arrivée
    taille_carre = diametre_boule + 6
    demi_carre = taille_carre // 2
    carre_x = 10 + random.randint(0, c_width - taille_carre - 10)
    carre_y = c_height - taille_carre - random.randint(5, 20)
    # on dessine le carré final (arrivée)
    carre_final = canvas.create_rectangle(
        carre_x - demi_carre, carre_y - demi_carre,
        carre_x + demi_carre, carre_y + demi_carre,
        outline="ivory",        # couleur du contour
        width=1,                # épaisseur du trait de contour (pixels)
        fill="palegreen1",      # couleur du remplissage
    )
# end def

def miner_terrain ():
    "on mine le terrain au début d'une partie"
    # on choisit un nombre au hasard
    nombre_mines = random.randint(10, 20)
    # on boucle pour poser autant de mines
    for i in range(nombre_mines):
        # on pose une mine au hasard
        poser_mine()
    # end for
# end def

def poser_mine ():
    """
        on pose une mine au hasard, mais jamais dans le voisinage
        immédiat du joueur;
    """
    # init variables locales
    tailles = [8, 10, 12]
    couleurs = ["sandy brown", "peru", "sienna1", "coral1"]
    couleur_contour = "burlywood1"
    # on boucle tant que ce n'est pas bon
    # ici, on bouclera cent fois (par précaution)
    # et on abandonnera si on n'y arrive pas
    for i in range(100):
        # on choisit de nouvelles coordonnées au hasard
        mine_x = random.randint(0, c_width)
        mine_y = random.randint(0, c_height)
        # les mines ont une taille variable;
        taille_mine = random.choice(tailles)
        # on recherche les collisions avec d'autres objets
        collisions = canvas.find_overlapping(
            *b_rect(mine_x, mine_y, taille_mine)
        )
        # aucun objet dans le voisinage ?
        if not collisions:
            # on choisit une couleur parmi une palette
            color = random.choice(couleurs)
            # on peut poser notre mine !
            canvas.create_oval(
                b_rect(mine_x, mine_y, taille_mine//2),
                outline=couleur_contour,
                width=1,        # épaisseur de trait de contour (pixels)
                fill=color,     # couleur du remplissage
            )
            # on remonte la boule joueur
            # au premier plan d'affichage
            canvas.tag_raise(boule_joueur, ALL)
            # c'est bon, on a posé notre mine
            # on peut quitter la boucle
            break
        # end if
    # end for
# end def

def b_rect (x, y, size):
    """
        retourne les coordonnées (top, left, bottom, right) d'un
        rectangle de délimitation autour d'un point P(x, y);
    """
    # rectangle d'influence (bounding rectangle)
    return (x - size, y - size, x + size, y + size)
# end def

def boucle_joueur ():
    "boucle temporelle qui gère les mouvements du joueur"
    # init variables globales
    global boule_x, boule_y
    global tid_joueur
    # le joueur a atteint le carré final ?
    if carre_x - demi_carre < boule_x < carre_x + demi_carre and \
            carre_y - demi_carre < boule_y < carre_y + demi_carre:
        # bravo ! c'est gagné !
        return bravo()
    # end if
    # on recherche des collisions
    collisions = canvas.find_overlapping(*canvas.bbox(boule_joueur))
    # le joueur a heurté autre chose que le carré final ?
    if len(collisions) > 1 and carre_final not in collisions:
        # aïe ! dommage !
        return game_over()
    # end if
    # tout est OK on déplace le joueur
    boule_x = (boule_x + sens_x * pas) % c_width
    boule_y = (boule_y + sens_y * pas) % c_height
    canvas.coords(
        boule_joueur,
        boule_x - rayon, boule_y - rayon,
        boule_x + rayon, boule_y + rayon,
    )
    # on boucle à nouveau dans le temps
    tid_joueur = fenetre.after(50, boucle_joueur)
# end def

def boucle_mines ():
    "boucle temporelle qui gère la vie des mines"
    # init variables globales
    global tid_mines
    # on pose une mine de temps en temps
    poser_mine()
    # prochaine mine dans... (millisecondes)
    delai = random.randint(1000, 2000)
    # on relance la boucle après ce délai
    tid_mines = fenetre.after(delai, boucle_mines)
# end def

def bravo ():
    "super ! le joueur a réussi !"
    # on affiche l'écran final
    ecran_final(
        titre="BRAVO",
        sous_titre="C'est gagné !",
        couleur_titre="lemon chiffon",
        couleur_sous_titre="pale green",
    )
# end def

def game_over ():
    "c'est ballot ! le joueur a perdu !"
    # on affiche l'écran final
    ecran_final(
        titre="GAME OVER",
        sous_titre="C'est perdu !",
        couleur_titre="firebrick1",
        couleur_sous_titre="floral white",
    )
# end def

def arreter_partie ():
    "le joueur souhaite arrêter la partie"
    # on affiche l'écran final
    ecran_final(
        titre="ABANDON",
        sous_titre="Partie annulée !",
        couleur_titre="gold",
        couleur_sous_titre="light grey",
    )
# end def

def ecran_final (**kw):
    "on effectue ici des opérations communes à tous les écrans finaux"
    # on bloque les clics intempestifs sur le bouton 'jouer'
    bouton_jouer.configure(state=DISABLED)
    # on stoppe les boucles temporelles qui pourraient
    # éventuellement être encore en cours d'exécution
    arreter_boucles()
    # on efface le canevas graphique
    canvas.delete(ALL)
    # on récupère le titre et sa couleur
    titre = kw.get("titre") or ""
    couleur = kw.get("couleur_titre") or "white"
    # on affiche le titre
    canvas.create_text(
        center_x, center_y - 40,
        text=titre,
        font="sans 36 bold",
        fill=couleur,
    )
    # on récupère le sous-titre et sa couleur
    sous_titre = kw.get("sous_titre") or ""
    couleur = kw.get("couleur_sous_titre") or "white"
    # on affiche le sous-titre
    canvas.create_text(
        center_x, center_y,
        text=sous_titre,
        font="sans 16 bold italic",
        fill=couleur,
    )
    # on relance presenter_jeu() après @delai
    # avec toutefois un minimum de 1 seconde
    delai = kw.get("delai") or 2000     # valeur par défaut
    fenetre.after(max(1000, delai), presenter_jeu)
# end def

def arreter_boucles ():
    "on arrête les boucles temporelles susceptibles de tourner encore"
    # init variables globales
    global tid_joueur, tid_mines
    # stooop !
    fenetre.after_cancel(tid_joueur)
    fenetre.after_cancel(tid_mines)
    # RAZ thread ids
    tid_joueur = tid_mines = 0
# end def

def deplacer_haut (event=None):
    "on déplace l'avatar du joueur vers le haut"
    # init variables globales
    global sens_x, sens_y
    # grâce à la boucle du jeu,
    # nous n'avons pas besoin de plus que ça
    sens_x, sens_y = (0, -1)
# end def

def deplacer_bas (event=None):
    "on déplace l'avatar du joueur vers le bas"
    # init variables globales
    global sens_x, sens_y
    # grâce à la boucle du jeu,
    # nous n'avons pas besoin de plus que ça
    sens_x, sens_y = (0, 1)
# end def

def deplacer_gauche (event=None):
    "on déplace l'avatar du joueur vers la gauche"
    # init variables globales
    global sens_x, sens_y
    # grâce à la boucle du jeu,
    # nous n'avons pas besoin de plus que ça
    sens_x, sens_y = (-1, 0)
# end def

def deplacer_droite (event=None):
    "on déplace l'avatar du joueur vers la droite"
    # init variables globales
    global sens_x, sens_y
    # grâce à la boucle du jeu,
    # nous n'avons pas besoin de plus que ça
    sens_x, sens_y = (1, 0)
# end def

# ------------------- FIN DU PROGRAMME ---------------------------------

# ce script est automatiquement lancé grâce à ceci :

# /!\ à connaître par coeur /!\
if __name__ == "__main__":
    # lancer le programme
    debut_jeu()
# end if
