import random as rd

THEMES = {"0": {"name": "Default", 0: "", 2: "2", 4: "4", 8: "8", 16: "16", 32:"32", 64: "64", 128: "128", 256: "256", 512: "512", 1024: "1024", 2048: "2048",4096: "4096", 8192: "8192"}, "1": {"name": "Chemistry", 0: "", 2: "H", 4: "He", 8:"Li", 16: "Be", 32: "B", 64: "C", 128: "N", 256: "O", 512: "F", 1024: "Ne", 2048:"Na", 4096: "Mg", 8192: "Al"}, "2": {"name": "Alphabet", 0: "", 2: "A", 4: "B", 8:"C", 16: "D", 32: "E", 64: "F", 128: "G", 256: "H", 512: "I", 1024: "J", 2048: "K",4096: "L", 8192: "M"}}

#FONCTIONNALITE 1

def create_grid(n):
    game_grid = []
    for i in range(n):
        #crée le bon nombre de ligne
        game_grid.append([0]*n) #crée le bon nombre de colonnes
    return (game_grid)


def get_value_new_tile():
    #choisi la valeur de la nouvelle case avec la probabilté indiquée dans l'énoncé
    return rd.choice([2,2,2,2,2,2,2,2,2,4])

def get_all_tiles(grid):
    tiles = []
    for line in grid: #parcours toutes les lignes
        for tile in line: #parcours toutes les colonnes
            if tile == ' ': tiles.append(0) #traite le cas particulier de la double notation 0 ou " " pour les emplacments vides
            else: tiles.append(tile)
    return tiles


def get_empty_tiles_positions(grid):
    empty_tiles = []
    nb_ligne = len(grid)
    nb_colonne = len(grid[0])
    #on suppose que la grille a au moins une ligne
    #on suppose que toutes les lignes ont le même nombre de colonnes

    for i in range(nb_ligne):
        for j in range(nb_colonne):

            if grid[i][j] == ' ' or grid[i][j] == 0:
            #traite le cas particulier de la double notation 0 ou " " pour les emplacments vides
                empty_tiles.append((i,j))
    return empty_tiles


def get_new_position(grid):
    #choisi une postion au hasard parmis les position libre
    empty_tiles = get_empty_tiles_positions(grid)
    return (rd.choice(empty_tiles))

def grid_get_value(grid,x,y):
    if grid[x][y] == ' ':
        #traite le cas particulier de la double notation 0 ou " "
        return 0
    return grid[x][y]

def grid_add_new_tile(grid):
    x,y = get_new_position(grid)
    grid[x][y] = get_value_new_tile()
    #attention, la fonction modifie la liste passée en argument !
    return grid


def init_game(n = 4):

    grid = create_grid(n)
    for new_tile in range(2):
        grid = grid_add_new_tile(grid)
    return grid


#FONCTIONNALITE 2

def grid_to_string_with_size_and_theme(grid, theme,n):
    longest_str = long_value_with_theme(grid,theme)
    affichage = ""
    for ligne in range(n):

        #création de la délimation du dessus
        affichage += (" " + "="*(longest_str+2))*n + "\n"
        #calcul du nombre nécéssaire d'espaces

        for colonne in range(n):
            affichage += "| {}".format(theme[grid[ligne][colonne]]) + " "*(1 + longest_str - len(theme[grid[ligne][colonne]]))
        affichage += "|\n" #fini la ligne

    #création de la dernière délimation du bas
    affichage += (" " + "="*(longest_str+2))*n
    return affichage

def long_value_with_theme(grid,theme):
    return max(len(theme[v]) for v in get_all_tiles(grid))
    
# FONCTIONNALITE 4

def del_zeros(row):
    #fonction permettant de supprimer les zeros d'une ligne, et donne la taille de la liste initiale qui sont inutiles lors du processus de fusion des cases
    n = len(row)
    new_row = []
    for i in range(n):
        if row[i] != 0:
            new_row.append(row[i])
    return new_row, n

def move_left_bis(row,i=0):
    #fonction aide pour la fonction recursive finale
    n = len(row)
    if i >= n-1:
        return row
    if row[i] == row[i+1]:
        row[i] = row[i]*2
        for j in range(i+1,n-1):
            row[j] = row[j+1]
        row[n-1] = 0
    return move_left_bis(row,i+1)

def move_row_left(row):
    row,n = del_zeros(row)
    new_row = move_left_bis(row)
    return new_row + [0]*(n - len(new_row))

def move_row_right(row):
    #bouger à droite revient à bouger à gauche la liste lue dans le sens inverse
    return move_row_left(row[::-1])[::-1]

def move_grid(grid,d):
    taille=len(grid)

    #cas droite et gauche plus simple car utilisation directe des fonctions codées ci dessus
    if d=="left":
        new_grid=[]
        for ligne in range (taille):
            row=grid[ligne]
            new_grid.append(move_row_left(row))
    elif d=="right":
        new_grid=[]
        for ligne in range (taille):
            row=grid[ligne]
            new_grid.append(move_row_right(row))

    #si on doit bouger en haut ou en bas celà revient à bouger la transposer à gauche ou à droite
    else:
        grid_t=transpose_grid(grid) #on s'interresse donc à la transposée
        new_grid_t=[]
        if d=="up":
            for ligne in range (taille):
                row=grid_t[ligne]
                new_grid_t.append(move_row_left(row))
        else : #on ne peut bouger que en haut,bas,droite,gauche, pas besoin de elif donc par deduction
            for ligne in range (taille):
                row=grid_t[ligne]
                new_grid_t.append(move_row_right(row))
        #on obtient la transposée de la grille finale
        new_grid=transpose_grid(new_grid_t)  #permet de retoruver la grille "dans le bon sens"
    return(new_grid)

def transpose_grid (liste):
    #fonction permettant de transposer une liste de liste que l'on identifie à sa matrice
    #cette fonction ne marche que sur des matrice carrée
   n=len(liste)
   transpo_liste=[]
   for colonne in range (n) :
       ligne_transpo=[] #création des lignes de la transposée
       for ligne in range (n) :
          ligne_transpo.append(liste[ligne][colonne])
       transpo_liste.append(ligne_transpo)
    #renvoie une liste
   return(transpo_liste)


# FONCTIONNALITE 5

def is_grid_full(grid):
    return get_empty_tiles_positions(grid) == []

def move_possible(grid):
    #renvoie une liste de 4 booléens indiquant si le déplacement à gauche,droite,haut,bas est possible.
    possibles = []
    for d in ["left","right","up","down"]:
        possibles.append(move_grid(grid,d) != grid) #si le deplacement n'est pas possible, la grille reste inchangée apres avoir effectué ce déplacement
    return possibles

def is_game_over(grid):
    #on a perdu si la grille est pleine et que aucun mouvement n'est possible
    return is_grid_full(grid) and True not in move_possible(grid)

def get_grid_tile_max(grid):
    return max(get_all_tiles(grid))

def did_you_win (grid):
    return (get_grid_tile_max(grid)>=2048)


#Fonctinnalité 6

def list_move_possible (grid):
    #renvoie la liste des deplacements possibles
    list_bool=move_possible(grid)
    list_final=[]
    if list_bool[0]:
        list_final.append("left")
    if list_bool[1]:
        list_final.append("right")
    if list_bool[2]:
        list_final.append("up")
    if list_bool[3]:
        list_final.append("down")
    return(list_final)


def random_play():
    #on suppose que le jeu par defaut comprend une grille de 4x4

    #phase d'initialisation du jeu
    grid_game=init_game(4)
    print(grid_to_string_with_size_and_theme(grid_game,THEMES["0"],4))

    #phase de jeu
    while not is_game_over(grid_game):

        #pour chaque tour
        d=rd.choice(list_move_possible(grid_game)) #on choisi un mouvement
        grid_game=move_grid(grid_game,d) #on le fait
        grid_game=grid_add_new_tile(grid_game) #une tuile se crée
        print(grid_to_string_with_size_and_theme(grid_game,THEMES["0"],4)) #on affuche le résulat
        print("\n***********************************\n") #permet aux différentes grilles d'être plus lisible

    #vérification si le jeu est gagnant ou perdant
    if did_you_win(grid_game):
        print("CONGRATS ! YOU WON !!!")
    else :
        print ("YOU FAIL, TRY AGAIN")
    return()