import csv

# Οι μεταβλητές row_count & column_count είναι global variables
# (εδώ γίνεται αρχικοποίηση για λόγους δοκιμών και ανάπτυξης του κώδικα.)
# Καθορίζουν τις διαστάσεις του ταμπλώ (στην άσκηση είναι ίσες εξ ορισμού)
#row_count = 8  # row_count    σειρές -> index : 0 έως row_count-1
#column_count = 8  # column_count στήλες -> index : 0 έως column_count-1

# Λίστα για κατακόρυφη αρίθμηση των γραμμών - έως 10
GRAMMES = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']

# Λίστα με τα σύμβολα των τοποθετήσεων των πιονιών των δύο παικτών
Players = [[1, 'X'], [2, 'O']]

# Συντελεστής βαρύτητας για τα επιπλέον πιόνια (δηλ. πλέον των 4)
# που υπάρχουν στη νικηφόρα θέση που προσδιορίστηκε
weight_of_extra_points = 1

# Αντιστοίχιση σήμανσης / ονομασίας για όλες τις κατευθύνσεις του ταμπλώ
# όπου θα αναζητηθεί μια νικηφόρα κατάσταση / θέση
directions = {'H': 'οριζόντια', 'V': 'κατακόρυφη',
              'P': 'διαγώνιος με θετική κλίση', 'N': 'διαγώνιος με αρνητική κλίση'}

# Συνάρτηση για την αρχική δημιουργία του ταμπλώ του παιχνιδιού
# συναρτήσει του αριθμού στηλών (και γραμμών) που δίνονται στην είσοδο.
# Επιστρέφει το αρχικοποιημένο ταμπλώ με κενούς χαρακτήρες παντού


def construct_tableux():
    # κατασκευή του ταμπλώ με χρήση εμφωλιασμένων λιστών : γραμμές - στήλες
    # αρχικοποίηση  με κενούς χαρατκτήρες με χρήση list comprehension
    # ειδική χρήση του συμβόλου _ για μη χρησιμοποιούμενη μεταβλητή
    tableux = [[' ' for _ in range(column_count)] for _ in range(row_count)]
    return tableux


def printing_tableux(tableux):
 # Αρχικά εκτυπώνουμε την επικεφαλίδα με την αρίθμηση των στηλών
    # αλλαγή από τα ίδια κενά αλλά με ""
    print('     ', end="")
    for stili in range(column_count):
        print(f" {stili+1}    ", end="")
    print(' ')                              # αλλαγή από ""
# Αρχικά εκτυπώνουμε μια διακεκομένη γραμμή κατάλληλου μήκους
    print(f"{'-' *(column_count+1)*6}")
# Στη συνέχεια εκτυπώνουμε ανά γραμμή:
# Πρώτα το γράμμα της αλφαριθμητικής αρίθμησής της κάθε γραμμής &
# κατόπιν το περιεχόμενο των αντίστοιχων θέσεων του ταμπλώ.
    for grammi in range(row_count):
        print(f"{GRAMMES[grammi]}  |", end="")
        for stili in range(column_count):
            print(f"  {tableux[grammi][stili]}  |", end="")
        print("")
# Τελειώνουμε πάλι εκτυπώνοντας μια άλλη διακεκομένη γραμμή
    print(f"{'-' *(column_count+1)*6 }\n")


# Συνάρτηση τοποθέτησης του πιονιού με το κατάλληλο σύμβολο
# (αναλόγως του ποιός παίκτης παίζει στη δεδομένη στιγμή)
# στο προκαθορισμένο σημείο του ταμπλώ του παιχνιδιού.
# Δίνονται οι αριθμοί γραμμής & στήλης του σημείου
# το σύμβολο του τρέχοντος παίκτη και το ταμπλώ.
def set_pioni(row, column, pioni, tableux):
    tableux[row-1][column-1] = pioni


# Συνάρτηση για έλεγχο πλήρους στήλης εισόδου πιονιού :
# Επιστρέφει True εάν η στήλη εισόδου είναι πλήρης
# Δίνεται ο αριθμός της στήλης προτίμησης και το ταμπλώ
def stili_full(column, tableux):
    return tableux[0][column-1] != ' '

# Συνάρτηση για τερματισμό του παιχνιδιού, εάν δέν υπάρχει
# κανένα διαθέσιμο (δηλ. κενό) κενό στο ταμπλώ
# Επιστρέφει True εάν όλες οι στήλες είναι πλήρεις

def stiles_full(tableux):
    plirotita = [False for _ in range(column_count)]
    for stili in range(column_count):
        if tableux[0][stili] != ' ' :
            plirotita[stili] = True
    return all(plirotita)

# Συνάρτηση για εντοπισμό της γραμμής στην οποία θα πρέπει να
# τοποθετηθεί το πιόνι (για τη δεδομένη στήλη εισόδου)
# Καλείται μόνο εφόσον υπάρχει όντως διαθέσιμος κενός χώρος
# Ο έλεγχος πραγματοποιείται με σημείο εκκίνησης τη κάτω - κάτω
# γραμμή και κινούμενοι προς τα επάνω μέχρι το σημείο εισόδου.
# Επιστρέφει τον αριθμό της γραμμής για τη τοποθέτηση του πιονιού.
# Δίνεται ο αριθμός της στήλης προτίμησης και το ταμπλώ

def find_free_row(column, tableux):
    for row in range(row_count-1, -1, -1):
        if tableux[row][column-1] == ' ':
            return row + 1


# Συνάρτηση εξακρίβωσης της εγκυρότητας των συντεταγμένων
# ενός υποτιθέμενου σημείου του ταμπλώ (κατά τη διαδικασία
# διερεύνησης της ύπαρξης νικηφόρας θέσης κάποιου παίκτη:
# Εάν δηλαδή η θέση του υποτιθέμενου σημείου βρίσκεται
# όντως εντός των προσδιορισμένων ορίων του ταμπλώ.
#                   Προσοχή :
# Δίνονται οι δείκτες (και όχι οι αριθμοί) γραμμής & στήλης
# Επιστρέφει True/False γιά έγκυρη/μή έγκυρη θέση αντίστοιχα
def entos_orion(row, column):
    return row >= 0 and row <= row_count-1 and column >= 0 and column <= column_count-1


# Συνάρτηση εξακρίβωσης ύπαρξης νικηφόρας θέσης στο ταμπλώ
# Πραγματοποιείται διερεύνηση ύπαρξης νικηφόρων θέσεων σε όλες τις πιθανές
# διευθύνσεις (δεν σταματά στη πρώτη διαπίστωση νικηφόρας θέσης.
# Το ταμπλώ # ελέγχεται με την ακόλουθη σειρά :
# (1) Οριζοντίως - (2) Κατακορύφως -
# (3) Διαγωνίως με θετική κλίση & (4) Διαγωνίως με αρνητική κλίση
# Υπολογίζεται το σκορ που αποδίδει η κάθε μία νικηφόρα θέση στο
# παίκτη και επιλέγεται αυτομάτως αυτή με το μεγαλύτερο σκορ.
# Δίνεται το ταμπλώ και οι πληροφορίες της τελευταίας κίνησης

def victory_position(tableux, last_move):

    last_row = last_move[0] - 1
    last_column = last_move[1] - 1
    symbol = last_move[2]
    player = last_move[3]

# Σ' όλες τις επόμενες υλοποιήσεις διερεύνησης για εξακρίβωση
# μιας νικηφόρας θέσης ακολουθείται η εξής προσέγγιση:

# Υπολογίζουμε τον αριθμό των υφιστάμενων γραμμών πριν (αριστερά) και
# μετά (δεξιά) από τη γραμμή που βρίσκεται αυτή τη στιγμή το πιόνι καθώς
# επίσης και τον αριθμό των υφιστάμενων στηλών πριν (επάνω) και
# μετά (κάτω) από τη στήλη που βρίσκεται τώρα το πιόνι.

# Με τη λογική αυτή έχουμε τη δυνατότητα επιλογής της αποφυγής ελέγχου
# για δείκτες υποτιθέμενου πιονιού εντός ορίων του ταμπλώ κατά τη σάρωση
# με κέρδος την αποφυγή επιβράδυνσης της εκτέλεσης του προγράμματος, καθώς
# αποφεύγουμε μιας τέτοιας μορφής έλεγχο εκεί που δεν πράγματι απαιτείται.

# Ελέγχουμε για συνολικό αριθμό μεγαλύτερο ή ίσο του 4 συνεχόμενων συμβόλων
# σε όλες τις πιθανές κατευθύνσεις ανεξαρτήτως του προσδιορισμού νικηφόρας θέσης
# σε κάποια συγκεκριμένη διεύθυνση, εφόσον θεωρητικώς υφίσταται και η περίπτωση
# να υφίστανται ταυτόχρονα περισσότερες της μιας νικηφόρες θέσειςσ στο ταμπλώ.

# Λίστα αποθήκευσης (σε επιμέρους αντίστοιχες εμφωλιασμένες υπολίστες)
# (1) της σήμανασης των κατευθύνσεων που διερευνήθηκαν για πιθανή ύπαρξη νικηφόρας θέσης
# (2) ο αριθμός των διαδοχικών πιονιών που εντοπίστηκαν σε κάθε διεύθυνση (default is 1)
# (3) ο αριθμός πόντων που προσφέρει η κάθε μία τις τρέχουσες κατευθύνσεις (default is 0)
# (4) μια μεταβλητή boolean που καθορίζει εάν η θέση είναι νικηφόρα ή όχι.

    boundaries = []
    victory_count = 0
# (1) Έλεγχος νικηφόρας θέσης στην οριζόντια  διεύθυνση
# -> Οριζόντια διεύθυνση (αριστερά - δεξιά)

# Αρχικοποίηση των πιονιών που θα μετρηθούν στην ίδια σειρά με το πιόνι αναφοράς.
    pieces_counted = 1
# Προσάρτηση της υπολίστας με τα στοιχεία της παρούσας διεύθυνσης
    boundaries.append(['H', pieces_counted, 0, False])

# Ακραίες προκαθορισμένες τιμές στηλών, εφόσον η λογική της
# σάρωσης προ-υπολογισμένου μήκους διαδρομών που ακολουθούμε,
# μας επιτρέπει να μη καταφύγουμε στη χρήση της συνάρτησης
# που διερευνά την εγκυρότητα των συντεταγμένων ενός
# υποτιθέμενου κενού - θέσης του ταμπλώ του παιχνιδιού.

    leftmost = -1
    rightmost = column_count

# (Ι) for loop σάρωσης από τη στήλη που προηγείται αυτής του πιονιού
# της τελευταίας κίνησης, μέχρι και την 1η στήλη :
# Κίνηση δηλαδή από τη στήλη του πιονιού με φορά προς τα αριστερά,
# δηλ. προς μειούμενους δείκτες στήλης πάντα επί της δεδομένης
# σειράς του πιονιού της τελευταίας κίνησης ( = πιόνι αναφοράς).

    for column in range(last_column-1, -1, -1):
        if tableux[last_row][column] == symbol:
            pieces_counted += 1
        else:
            leftmost = column
            break

# (ΙΙ) for loop σάρωσης από τη στήλη που έπεται αυτής του πιονιού
# της τελευταίας κίνησης, μέχρι και την τελευταία στήλη :
# Κίνηση δηλαδή από τη στήλη του πιονιού με φορά προς τα δεξιά,
# δηλ. προς αυξανόμενους δείκτες στήλης πάντα επί της δεδομένης
# σειράς του πιονιού της τελευταίας κίνησης ( = πιόνι αναφοράς).

    for column in range(last_column+1, column_count):
        if tableux[last_row][column] == symbol:
            pieces_counted += 1
        else:
            rightmost = column
            break

    if pieces_counted >= 4:
        # Εκτυπώνεται η αναγγελία νικηφόρας κατάστασης στην συγκεκριμένη διεύθυνση
        # και ενημερώνονται τα στοιχεία του αριθμού των νικηφόρων πιονιών
        # και μετατρέπεται η τιμη της boolean μεταβλητής σε True.
        boundaries[0][1] = pieces_counted
        boundaries[0][2] = 1 + (pieces_counted - 4) * weight_of_extra_points
        boundaries[0][3] = True
        print(
            f"Νίκη του παίκτη {player} στην {directions[boundaries[0][0]]} διεύθυνση που δίνει {boundaries[0][2]} βαθμούς")
        victory_count += 1

# (2) Έλεγχος νικηφόρας θέσης στη  κατακόρυφη διεύθυνση
#  -> Κατακόρυφη διεύθυνση (επάνω - κάτω)

# Αρχικοποίηση των πιονιών που θα μετρηθούν στην ίδια σειρά με το πιόνι αναφοράς.
    pieces_counted = 1
# Προσάρτηση της υπολίστας με τα στοιχεία της παρούσας διεύθυνσης
    boundaries.append(['V', pieces_counted, 0, False])

# Ακραίες προκαθορισμένες τιμές γραμμών
    topmost = -1
    bottommost = row_count

# (Ι) for loop σάρωσης από τη γραμμή που προηγείται της γραμμής
# του πιονιού αναφοράς, μέχρι και την 1η γραμμή :
# Κίνηση δηλαδή από τη γραμμή του πιονιού με φορά προς τα επάνω,
# δηλ. προς μειούμενους δείκτες γραμμής πάντα επί της δεδομένης
# στήλης του πιονιού αναφοράς.

    for row in range(last_row - 1, -1, -1):
        if tableux[row][last_column] == symbol:
            pieces_counted += 1
        else:
            topmost = row
            break

# (ΙΙ) for loop σάρωσης από τη γραμμή που έπεται της γραμμής
# του πιονιού αναφοράς, μέχρι και τη τελευταία γραμμή :
# Κίνηση δηλαδή από τη γραμμή του πιονιού με φορά προς τα κάτω,
# δηλ. προς αυξανόμενους δείκτες γραμμής πάντα επί της δεδομένης
# στήλης του πιονιού αναφοράς.

    for row in range(last_row + 1, row_count):
        if tableux[row][last_column] == symbol:
            pieces_counted += 1
        else:
            bottommost = row
            break

    if pieces_counted >= 4:
        # Εκτυπώνεται η αναγγελία νικηφόρας κατάστασης στην συγκεκριμένη διεύθυνση
        # και ενημερώνονται τα στοιχεία του αριθμού των νικηφόρων πιονιών
        # και μετατρέπεται η τιμη της boolean μεταβλητής σε True.
        boundaries[1][1] = pieces_counted
        boundaries[1][2] = 1 + (pieces_counted - 4) * weight_of_extra_points
        boundaries[1][3] = True
        print(
            f"Νίκη του παίκτη {player} στην {directions[boundaries[1][0]]} διεύθυνση που δίνει {boundaries[1][2]} βαθμούς")
        victory_count += 1

# (3) Έλεγχος νικηφόρας θέσης στη διαγώνια διεύθυνση με θετική κλίση
# -> Θετική κλίση (διεύθυνση επάνω δεξιά <---> κάτω αριστερά)

# Αρχικοποίηση των πιονιών που θα μετρηθούν στη διαγώνιο
# θετικής κλίσης στην οποία βρίσκεται το πιόνι αναφοράς.
    pieces_counted = 1
# Προσάρτηση της υπολίστας με τα στοιχεία της παρούσας διεύθυνσης
    boundaries.append(['P', pieces_counted, 0, False])

 # Διαγώνια σάρωση με φορά προς τα επάνω και δεξιά του ταμπλώ:
 #  diag_pos_up   = [-1,  1]

# (Ι) for loop σάρωσης επί της κάποιας διαγωνίου η υποδιαγωνίου
# με θετική κλίση που βρίσκεται το πιόνι αναφοράς όπου η σάρωση
# υλοποιείται με φορά : προς τα επάνω και δεξιά του ταμπλώ :

# Σάρωση από τη γραμμή που προηγείται της γραμμής του πιονιού αναφοράς,
# μέχρι και (θεωρητικά : εφόσον βρισκόμαστε εντός των ορίων του ταμπλώ)
# την 1η γραμμή (δηλαδή προς μειούμενο δείκτη γραμμής)
#                   & ταυτόχρονα
# Σάρωση από τη στήλη που έπεται της στήλης του πιονιού αναφοράς,
# μέχρι και (θεωρητικά : εφόσον βρισκόμαστε εντός των ορίων του ταμπλώ)
# την τελευταία στήλη (δηλαδή προς αυξανόμενο δείκτη στήλης)

# 0 is our current [starting] position scan 1 to column_count -1 (max case)

    for cell in range(column_count):
        row = last_row + (-1) * (cell+1)
        column = last_column + (+1) * (cell+1)
        if entos_orion(row, column) and tableux[row][column] == symbol:
            pieces_counted += 1
        else:
            uppermost_row_P = row
            uppermost_column_P = column
            break

# Διαγώνια σάρωση με φορά προς τα κάτω και αριστερά του ταμπλώ:
#   diag_pos_down = [ 1, -1]

# (ΙΙ) for loop σάρωσης επί της κάποιας διαγωνίου η υποδιαγωνίου
# με θετική κλίση που βρίσκεται το πιόνι αναφοράς όπου η σάρωση
# υλοποιείται με φορά : προς τα κάτω και αριστερά του ταμπλώ :

# Σάρωση από τη γραμμή που έπεται της γραμμής του πιονιού αναφοράς,
# μέχρι και (θεωρητικά : εφόσον βρισκόμαστε εντός των ορίων του ταμπλώ)
# την τελευταία γραμμή (δηλαδή προς αυξανόμενο δείκτη γραμμής)
#                   & ταυτόχρονα
# Σάρωση από τη στήλη που προηγείται της στήλης του πιονιού αναφοράς,
# μέχρι και (θεωρητικά : εφόσον βρισκόμαστε εντός των ορίων του ταμπλώ)
# την 1η στήλη (δηλαδή προς μειούμενο δείκτη στήλης)

    for cell in range(column_count):
        row = last_row + (+1) * (cell + 1)
        column = last_column + (-1) * (cell + 1)
        if entos_orion(row, column) and tableux[row][column] == symbol:
            pieces_counted += 1
        else:
            lowermost_row_P = row
            lowermost_column_P = column
            break

    if pieces_counted >= 4:
        # Εκτυπώνεται η αναγγελία νικηφόρας κατάστασης στην συγκεκριμένη διεύθυνση
        # και ενημερώνονται τα στοιχεία του αριθμού των νικηφόρων πιονιών
        # και μετατρέπεται η τιμη της boolean μεταβλητής σε True.
        boundaries[2][1] = pieces_counted
        boundaries[2][2] = 1 + (pieces_counted - 4) * weight_of_extra_points
        boundaries[2][3] = True
        print(
            f"Νίκη του παίκτη {player} στην {directions[boundaries[2][0]]} διεύθυνση που δίνει {boundaries[2][2]} βαθμούς")
        victory_count += 1

# (4) Έλεγχος νικηφόρας θέσης στη διαγώνια διεύθυνση με αρνητική κλίση
# -> Αρνητική κλίση (επάνω αριστερά <---> κάτω δεξιά)

# Αρχικοποίηση των πιονιών που θα μετρηθούν στη διαγώνιο
# αρνητικής κλίσης που βρίσκεται το πιόνι αναφοράς.
    pieces_counted = 1
# Προσάρτηση της υπολίστας με τα στοιχεία της παρούσας διεύθυνσης
    boundaries.append(['N', pieces_counted, 0, False])

# Διαγώνια σάρωση με φορά προς τα επάνω και αριστερά του ταμπλώ:
 #  diag_neg_up   = [-1,  -1]

# (Ι) for loop σάρωσης επί της κάποιας διαγωνίου η υποδιαγωνίου
# με αρνητική κλίση που βρίσκεται το πιόνι αναφοράς όπου η σάρωση
# υλοποιείται με φορά : προς τα επάνω και αριστερά του ταμπλώ :

# Σάρωση από τη γραμμή που προηγείται της γραμμής του πιονιού αναφοράς,
# μέχρι και (θεωρητικά : εφόσον βρισκόμαστε εντός των ορίων του ταμπλώ)
# την 1η γραμμή (δηλαδή προς μειούμενο δείκτη γραμμής)
#                   & ταυτόχρονα
# Σάρωση από τη στήλη που προηγείται της στήλης του πιονιού αναφοράς,
# μέχρι και (θεωρητικά : εφόσον βρισκόμαστε εντός των ορίων του ταμπλώ)
# την 1η στήλη (δηλαδή προς μειούμενο δείκτη στήλης)

    for cell in range(column_count):
        row = last_row + (-1) * (cell+1)
        column = last_column + (-1) * (cell+1)
        if entos_orion(row, column) and tableux[row][column] == symbol:
            pieces_counted += 1
        else:
            uppermost_row_N = row
            uppermost_column_N = column
            break

# Διαγώνια σάρωση με φορά προς τα κάτω και δεξιά του ταμπλώ:
# diag_neg_down = [ +1,  +1]

# (ΙΙ) for loop σάρωσης επί της κάποιας διαγωνίου η υποδιαγωνίου
# με αρνητική κλίση που βρίσκεται το πιόνι αναφοράς όπου η σάρωση
# υλοποιείται με φορά : προς τα κάτω και δεξιά του ταμπλώ :

# Σάρωση από τη γραμμή που έπεται της γραμμής του πιονιού αναφοράς,
# μέχρι και (θεωρητικά : εφόσον βρισκόμαστε εντός των ορίων του ταμπλώ)
# την τελευταία γραμμή (δηλαδή προς αυξανόμενο δείκτη γραμμής)
#                   & ταυτόχρονα
# Σάρωση από τη στήλη που προηγείται της στήλης του πιονιού αναφοράς,
# μέχρι και (θεωρητικά : εφόσον βρισκόμαστε εντός των ορίων του ταμπλώ)
# την τελευταία στήλη (δηλαδή προς αυξανόμενο δείκτη στήλης)

    for cell in range(column_count):
        row = last_row + (+1) * (cell+1)
        column = last_column + (+1) * (cell+1)
        if entos_orion(row, column) and tableux[row][column] == symbol:
            pieces_counted += 1
        else:
            lowermost_row_N = row
            lowermost_column_N = column
            break

    if pieces_counted >= 4:
        # Εκτυπώνεται η αναγγελία νικηφόρας κατάστασης στην συγκεκριμένη διεύθυνση
        # και ενημερώνονται τα στοιχεία του αριθμού των νικηφόρων πιονιών
        # και μετατρέπεται η τιμη της boolean μεταβλητής σε True.
        boundaries[3][1] = pieces_counted
        boundaries[3][2] = 1 + (pieces_counted - 4) * weight_of_extra_points
        boundaries[3][3] = True
        print(
            f"Νίκη του παίκτη {player} στην {directions[boundaries[3][0]]} διεύθυνση που δίνει {boundaries[3][2]} βαθμούς")
        victory_count += 1

# -------------------------------------------------------------------------------------

    # Διερεύνηση των αποτελεσμάτων για ύπαρξη νικηφόρας θέσης σε μία οποιαδήποτε διεύθυνση
    victory = False
    if victory_count > 1:

        # Καθορισμός της τελικής νικηφόρας διεύθυνσης (εάν τυγχάνει να είναι περισσότερες
        # της μιας), καθώς και των αντίστοιχωνν βαθμών που προσφέρει στο παίκτη.

        direction = [' ', -1]
        for slopes in range(len(boundaries)):
            if boundaries[slopes][2] > direction[1]:
                direction[0] = boundaries[slopes][0]
                direction[1] = boundaries[slopes][2]
        victory = True
        print(
            f"Επιλέχθηκε η {directions[direction[0]]} διεύθυνση που δίνει {direction[1]} βαθμούς")

    elif victory_count == 1:
        direction = [' ', False]
        for slopes in range(len(boundaries)):
            if boundaries[slopes][3]:
                direction[0] = boundaries[slopes][0]
                direction[1] = boundaries[slopes][2]
        victory = True

    if victory:
        slope = direction[0]
        points = direction[1]
    else:
        slope = ' '
        points = 0
# -------------->>>>

# Φάση αντικατάστασης όλων των συμβόλων παίκτη της νικηφόρας θέσης και
# (1) δημιουργία εναλλακτικής απεικόνισης με "*" στη θέση τους, στη συνέχεια
# (2) σβήσιμο τους, δηλαδή αντικατάσταση των "*" με ' ' στη θέση τους και τέλος
# (3) ολίσθηση (gravity pull) όλων των πιονιών που βρίσκονταν πάνω από αυτά.
#  Η ολίσθηση θα γίνεται για τη νικητήρια θέση που αντιπροσωπεύει το μεγαλύτερο σκορ.


# ----------------------> Οριζόντια διεύθυνση :

    if victory and slope == 'H':
        # Τα '*' θα μπουν στις στήλες : leftmost+1 έως και rightmost-1
        # για τη γραμμή του πιονιού αναφοράς : last_row
        # δηλ. από τη θέση μετά ακριβώς το αριστερό όριο και
        # μέχρι τη θέση ακριβώς πριν το δεξιό όριο

        for column in range(leftmost+1, rightmost):
            tableux[last_row][column] = '*'

        printing_tableux(tableux)
#
# Σχετικός κώδικας για "σβήσιμο" των νικητήριων πιονιών
# και ολίσθηση αυτών που βρίσκονται από πάνω τους.

# "Σβήσιμο" διαδοχικά των νικητήριων κελλιών (δηλ. των "*") και ...

        for column in range(leftmost+1, rightmost):
            tableux[last_row][column] = ' '

# ... διερεύνηση στη κατακόρυφη διεύθυνση με φορά προς τα επάνω και
# μέχρι και ακριβώς τη 1η γραμμή (δείκτης : 0) (οπότε και δεν
# απαιτείται η συνάρτηση ελέγχου της θέση εντός των ορίων)
# για την υλοποίηση της ολίσθησης των πιονιών που τυγχάνει να
# βρίσκονται υπεράνω του τρέχοντος κελλιού με την εξής λογική:
# Ελέγχεται διαδοχικά το αμέσως υπεράνω κελλί και εάν τυγχάνει να μην
# είναι κενό, τότε το σύμβολο που περιέχει αντιγράφεται στο τρέχον
# κελλί (χωρίς να "σβηστεί" το σύμβολο στην αρχική του θέση).
# Εάν τύχει το κελλί που ελέγχεται να είναι κενό, τότε η
# διαδικασία τερματίζεται, αλλά αμέσως πριν διαγράφεται επίσης
# το σύμβολο του τρέχοντος κελιού (που είχε ήδη μόλις αντιγραφεί).
#
            for row_up in range(last_row-1, -1, -1):
                if tableux[row_up][column] != ' ':
                    tableux[row_up + 1][column] = tableux[row_up][column]
                else:
                    tableux[row_up + 1][column] = ' '
                    break

        printing_tableux(tableux)

# ----------------------> Κατακόρυφη διεύθυνση

    if victory and slope == 'V':
        # Τα '*' θα μπουν στις στήλες : topmost+1 έως και bottommost-1
        # για τη στήλη του πιονιού αναφοράς : last_column
        # δηλ. από τη θέση μετά ακριβώς το επάνω όριο και
        # μέχρι τη θέση ακριβώς πριν το κάτω όριο

        for row in range(topmost+1, bottommost):
            tableux[row][last_column] = '*'

        printing_tableux(tableux)

# Σχετικός κώδικας για "σβήσιμο" των νικητήριων πιονιών

# Ο αριθμός των κελλιών με '*' που "σβήστηκαν" δηλ. το συνολικό πλήθος των πιονιών
# της νικητήριας ομάδας είναι :->  stride__length = (bottommost - topmost) - 1

        for row in range(topmost+1, bottommost):
            tableux[row][last_column] = ' '

# Δεν είναι δυνατόν να υπήρχαν πιόνια στα κελλιά που βρίσκονταν
# υπεράνω της νικητήριας κατακλορυφης ομάδας πιονιών.
# Συνεπώς η έννοια της"ολίσθησησ" δεν υφίσταται στη περίπτωση αυτή.

        """        
        # Θεωρητική ολίσθηση μόνο στη δεδομένη στήλη κατά το μήκος της νικητήριας ομάδας
        # (εάν θα ήταν δυνατόν να υπήρχαν πιόνια στα κελλιά άνωθεν αυτής της στήλης)
        # Εκκίνηση από το οριακό κελλί ακριβώς "επάνω" από την ομάδα των πιονιών

            for row_up in range (topmost, -1, -1) : # (topmost element included in this case)
                if tableux[row_up][column] != ' ' # or entos_orion(row_up-1, column) :
                    tableux[row_up + stride__length][last_column] = tableux[row_up][last_column]
                else:
                    break
        """
        printing_tableux(tableux)

# --------------------> Διαγώνια διεύθυνση με θετική κλίση

# Υλοποίηση με επιλογή διεύθυνσης από : "επάνω δεξιά" προς "κάτω αριστερά"
# Τα '*' θα μπουν στα κελιά:
# από συντεταγμένες γραμμής : uppermost_row +1 & στήλης : uppermost_column -1

# (συντεταγμένες "επάνω δεξιά" συνοριακής θέσης - κελλιού
#              => uppermost_row  &  στήλης : uppermost_column)

# κινούμενοι σταθερά (παραμένοντας) πάνω στη διαγώνιο προς τα κάτω :
# αυξάνοντας κατά 1 το δείκτη της γραμμής και ταυτοχρόνως
# μειώνοντας κατά 1 το δείκτη της στήλης

# έως συντεταγμένες γραμμής lowermost_row -1 και στήλης lowermost_column +1

# { Με χρήση του αλγορίθμου row -1, column +1
#   δηλαδή με εκείνον που οδηγεί σε διαγώνια σάρωση με φορά
#   προς τα κάτω και αριστερά του ταμπλώ: diag_pos_down = [ 1, -1] }

    if victory and slope == 'P':

        # Υπολογισμός του αριθμού των κελλιών μεταξύ των "επάνω"
        # και "κάτω" συνοριακών κελλιών επί της διαγωνίου
        # (ισοδυναμεί με υπολογισμό των αντίστοιχων γραμμών του ταμπλώ)

        rows_in_between = (lowermost_row_P - uppermost_row_P) - 1

        for cell in range(rows_in_between):
            row = uppermost_row_P + (+1) * (cell + 1)
            column = uppermost_column_P + (-1) * (cell + 1)
            tableux[row][column] = '*'

        printing_tableux(tableux)

# Σχετικός κώδικας για "σβήσιμο" των νικητήριων πιονιών
# και ολίσθηση αυτών που βρίσκονται από πάνω τους.

# Διαδοχικό σβήσιμο κελιού με "*" και "ολίσθηση" όλων των
# υπεράνω αυτού κελλιών - (η αναλυτική διαδικασία περιγράφεται
# διεξοδικά στο αντίστοιχο τμήμα της οριζόντιας διεύθυνσης)

        for cell in range(rows_in_between):
            row = uppermost_row_P + (+1) * (cell + 1)
            column = uppermost_column_P + (-1) * (cell + 1)
            tableux[row][column] = ' '

        # Ολίσθηση στη λογική της "οριζόντιας διεύθυνσης"
            for row_up in range(row-1, -1, -1):
                if tableux[row_up][column] != ' ':
                    tableux[row_up+1][column] = tableux[row_up][column]
                else:
                    tableux[row_up+1][column] = ' '
                    break

        printing_tableux(tableux)

#------------------#------------------#------------------#------------------#
#                           ΣΗΜΕΙΩΣΗ :
#  Εναλλακτική υλοποίηση βάσει των στηλών αντί των γραμμών:
#  Υπολογισμός του αριθμού των κελλιών μεταξύ των "αριστερά"
#  και "δεξιά" συνοριακών κελλιών επί της διαγωνίου
#  (ισοδυναμεί με υπολογισμό των αντίστοιχων στηλών του ταμπλώ)

#   columns_in_between = (uppermost_column_P - lowermost_column_P) -1

#   for cell in range(columns_in_between):
#        column = uppermost_column_P + (-1) * (cell + 1)
#        row = uppermost_row_P + (+1) * (cell + 1)
#        tableux[row][column] = '*'

# ... και βέβαια αυτή η εναλλακτική προσέγγιση θα μπορούσε να χρησιμοποιηθεί
# και για την επόμενη περίπτωση της διαγώνιας αρνητικής κλίσης με

# columns_in_between = (uppermost_column_N - lowermost_column_N) -1  κ.λ.π.
#------------------#------------------#------------------#------------------#

# ------------------> Διαγώνια διεύθυνση με αρνητική κλίση

# Υλοποίηση με επιλογή διεύθυνσης από : "επάνω αριστερά" προς "κάτω δεξιά"
#
# Τα '*' θα μπουν στα κελιά:
# από συντεταγμένες γραμμής : uppermost_row +1 & στήλης : uppermost_column -1

# (συντεταγμένες "επάνω αριστερά" συνοριακής θέσης - κελλιού
#              => uppermost_row  &  στήλης : uppermost_column)

# κινούμενοι σταθερά (παραμένοντας) πάνω στη διαγώνιο προς τα κάτω :
# αυξάνοντας κατά 1 το δείκτη της γραμμής και ταυτοχρόνως
# αυξάνοντας κατά 1 το δείκτη της στήλης

# έως συντεταγμένες γραμμής lowermost_row -1 και στήλης lowermost_column +1

# { Με χρήση του αλγορίθμου row +1, column +1
#   δηλαδή με εκείνον που οδηγεί σε διαγώνια σάρωση με φορά
#   προς τα κάτω και δεξιά του ταμπλώ: diag_pos_down = [ +1, +1] }

# Υπολογισμός του αριθμού των κελλιών μεταξύ των "επάνω"
# και "κάτω" συνοριακών κελλιών επί της διαγωνίου

    if victory and slope == 'N':

        rows_in_between = (lowermost_row_N - uppermost_row_N) - 1

        for cell in range(rows_in_between):
            row = uppermost_row_N + (+1) * (cell + 1)
            column = uppermost_column_N + (+1) * (cell + 1)
            tableux[row][column] = '*'

        printing_tableux(tableux)

# Διαδοχικό σβήσιμο κελιού με "*" και "ολίσθηση" όλων των
# υπεράνω αυτού κελλιών - (η αναλυτική διαδικασία περιγράφεται
# διεξοδικά στο αντίστοιχο τμήμα της οριζόντιας διεύθυνσης)

        for cell in range(rows_in_between):
            row = uppermost_row_N + (+1) * (cell + 1)
            column = uppermost_column_N + (+1) * (cell + 1)
            tableux[row][column] = ' '

            # Ολίσθηση - ίδιος κώδικας με τη θετική κλίση, δηλαδή :
            # Ολίσθηση στη λογική της "οριζόντιας διεύθυνσης"
            for row_up in range(row-1, -1, -1):
                if tableux[row_up][column] != ' ':
                    tableux[row_up+1][column] = tableux[row_up][column]
                else:
                    tableux[row_up+1][column] = ' '
                    break

        printing_tableux(tableux)

# Υποτίθεται πως ο συντελεστής βαρύτητας των επιπλέον νικητήριων πιονιών είναι ακέραιος.

    return victory, int(points)
#--------------------------------------------------------------------#

#                    ----> ΕΚΚΙΝΗΣΗ ΠΑΙΧΝΙΔΙΟΥ <----

# Αρχικό καλωσόρισμα των παικτών στο παιχνίδι


print("Καλως ήρθατε στο παιχνίδι: Connect 4.\n Σκοπός του παιχνιδιού είναι να κάνετε πριν από τον αντίπαλό σας\n μία σειρά απο τουλάχιστον 4 διαδοχικά πιόνια σε οποιαδήποτε διεύθυνση.")
print(f"Μία νίκη με τέσσερα πιόνια δίνει στον αντίστοιχο παίχτη 1 πόντο, ενώ \n για κάθε επιπλέον πιόνι που ανήκει στην νικητήρια σειρά αυξάνει το σκορ του παίχτη κατά {weight_of_extra_points} πόντο.")
print("Ο Παίχτης 1 έχει το σύμβολο'Χ', ενώ ο Παίχτης 2 το σύμβολο 'Ο'")
print("Καλή απόλαυση !")
print()

game_choice = False
while not game_choice:
    choice_of_game = input(f"Επιθυμείτε νέο παιχνίδι (N) ή φόρτωση παιχνιδιού από αρχείο (S); ")
    choice_of_game = choice_of_game.title()


# Νέο παιχνίδι : Ερώτηση προτίμησης αριθμού στηλών για το μέγεθος του ταμπλώ (5-10)
# while loop (game_specs) : έλεγχος εισόδου στηλών, ελέγχου ορθότητας εισόδου, αρχικοποίησης μεταβλητών μεγέθους.
    if choice_of_game == 'N':
        new_game = True
        game_specs = False

        while not game_specs:
            try:
                choice_of_columns = int(input(f"Δώστε τον αριθμό των στηλών που επιθυμείτε (5 έως 10): "))
            except ValueError:
                print("Δεν δόθηκε αριθμός. Ξαναπροσπαθείστε")
            else:
                if choice_of_columns < 5 or choice_of_columns > 10:
                    print('Επιλογή εκτός ορίων: Ξαναπροσπαθείστε')
                else:

                    # Εντάξει με τον προσδιορισμό του μεγέθους του ταμπλώ.
                    # Αρχικοποίηση των αντίστοιχων μεταβλητών διαστάσεων του ταμπλώ.
                    column_count = choice_of_columns
                    row_count = column_count

    # (η game_specs  τίθεται ως True.)->  το εσωτερικό while loop δεν θα επαναληφθεί
                    game_specs = True

# (η game_choice  τίθεται ως True.)->  το εξωτερικό while loop δεν θα επαναληφθεί
# Η επιλογή νέου παιχνιδιού ελέγχθηκε πλήρως. Όλα εντάξει. Προχωρούμε στην επόμενη φάση.
        game_choice = True

# (η game_choice  τίθεται ως True.)->  το εξωτερικό while loop δεν θα επαναληφθεί
# Δόθηκε επιλογή αποθηκευμένου παιχνιδιού με τη  Προχωρούμε στην επόμενη φάση.
    elif choice_of_game == 'S':
        new_game = False
        game_choice = True

    # Δεν προσδιορίστηκε ορθά Y/N - Επιστροφή για σωστή επανασεισαγωγή επιλογής
    # Το εσωτερικό while loop θα επαναληφθεί (η game_specs δεν τίθεται ως True.)
    else:
        print('Η επιλογή σας δεν ήταν σωστή: Ξαναπροσπαθείστε (προσοχή:με αγγλικούς χαρακτήρες)')

# Αναλόγως της επιλογής των παικτών αναφορικά με νέο ή αποθηκευμένο παιχνίδι
# αρχικοποιούμε το ταμπλώ είτε με κενούς χαρακτήρες, είτε με τη κατανομή
# των πιονιών όπως αυτή ήταν κατά τη διακοπή του παιχνιδιού.
if new_game:
    board = construct_tableux()
    score = [0, 0]

else:
    # Προσδιορισμός του path του καταλόγου αποθήκευσης σε συγκεκριμένο δίσκο

    directory_l = "C:\\"

    while True:

        # Ζητείται από το παίκτη να ορίσει το όνομα του αρχείου αποθήκευσης
        try:
            print(f"Το αρχείο το οποίο επιθυμείτε να φορτώσετε πρέπει να βρίσκεται στον υπολογιστή σας στη διεύθυνση: {directory_l}")
            filename_l = input(f"Εισάγετε το όνομα του αρχείου (χωρίς κατάληξη): ")
            filepath_l = directory_l + filename_l + ".csv"

        # Μετά ελέγχεται η εγγυρότητα του ονόματος που δόθηκε, δηλαδή
        # εάν το συγκεκριμένο αρχείο όντως υπάρχει στο κατάλογο αποθήκευσης
            saved_file = open(filepath_l, "r", newline="", encoding="utf-8")

        except FileNotFoundError:
            # σε περίπτωση λανθασμένης καταχώρησης ονόματος, λαμβάνουμε το αντίστοιχο
            # exception -> FileNotFoundError και εκτυπώνουμε ένα σχετικό ενημερωτικό
            # μήνυμα, και διατηρούμε το while loop ενεργό, ώστε να ζητήσουμε από το
            # χρήστη να επαναεισαγάγει ένα (σωστό) όνομα αρχείου.
            print("Δεν υπάρχει αρχείο με το όνομα που δώσατε. Παρακαλώ ξαναπροσπαθείστε: ")

        else:
            # ο έλεγχος της εγκυρότητας του ονόματος του αρχείου που δόθηκε
            # από το χρήστη ολοκληρώθηκε χωρίς να υπάρξει το σχετικό exception
            # Συνεπώς διαφεύγουμε από τον παρόντα while (always True) loop,
            # εφόσον πλέον εξέλειπε ο λόγος (δηλ. ο έλεγχος) της παρουσίας του.
            break

# Δημιουργία μεταβλητής (για λόγους ευκρίνειας κώδικα) για το csv.reader
    dedomena = csv.reader(saved_file)

# Αντιγραφή των αποθηκευμένων δεδομένων σαν υπολίστες στην λίστα board
# Θα μπορούσε να υλοποιηθεί και απευθείας χωρίς το ενδιάμεσο dedomena.
    board = [row for row in dedomena]

#  Κλείνουμε το αρχείο που είχαμε ανοίξει εντός του try/except/else τμήματος του
#  κώδικα, εφόσον έχει ολοκληρωθεί πλήρως η διαδικασία ανάγνωσης των δεδομένων
#  και η αντιγραφή τους στη κατάλληλη λίστα board που περιέχει τις
#  εμφωλιασμένες λίστες που αναφέρονται στις γραμμές του ταμπλώ.
    saved_file.close()

# Παρατήρηση : Το κλείσιμο δεν είναι δυνατόν να πραγματοποιηθεί
# πριν την ολοκλήρωση της αντιγραφής στη λίστα, διότι τότε δεν είναι
# εφικτή η αντιγραφή, ακριβώς επειδή το file θα είναι κλειστό.
# Αυτός είναι και ο λόγος που σε αντίθεση με άλλους ελέγχους εγκυρότητας
# του string εισόδου δεν χρησιμοποιήσαμε εδώ -> context manager με το with

# Εξακρίβωση του αριθμού των στηλών στα αποθηκευμένα δεδομένα στο αρχείο
    column_count = len(board[0])
    row_count = column_count

# Διαγραφή της τελευταίας σειράς & μεταφορά των δεδομένων στη λίστα score
    score = board.pop(-1)
    score[0] = int(score[0])
    score[1] = int(score[1])

# Επαναφορά της λίστας των "κωδικοποιημένων" με 0/1/2 δεδομένων του αρχείου
# στην αναπαράσταση με σύμβολα " "/"Χ"/"Ο" για τη συνέχεια του παιχνιδιού
    for list in range(len(board)):
        for symbol in range(len(board[list])):

            if board[list][symbol] == '1':
                board[list][symbol] = 'X'
            elif board[list][symbol] == '2':
                board[list][symbol] = 'O'
            else:
                board[list][symbol] = ' '


# Initialization of control variables : (1) game_finished & (2) turn (3) score

# Εδώ είτε σε νέο παιχνίδι - είτε σε έναρξη από σωσμένο
# (όχι σε νέο γύρο κατά τη διάρκεια ενός παιχνιδιού)

# εκτύπωση της τρέχουσας κατάστασης (άδειο ή αποθηκευμένο)
printing_tableux(board)
print(f"Το σκορ είναι {score[0]} - {score[1]}")

game_finished = False
current_round_finished = False
round = 1
turn = 0
moves_played = 0

# Φάση έναρξης νέου γύρου [εκτός του 1ου] (δηλ. είναι η σειρά του παίκτη 1)
while not game_finished:

    # Ορίζουμε μια μεταβλητή ελέγχου της επιλογής αποθήκευσης του παιχνιδιού
    stop_choice = False

# while loop για τη διαχείριση της διαδικασίας αποθήκευσης ή συνέχισης του παιχνιδιού
# Επιλογή με έξοδο από το loop μέσω break στα κατάλληλα σημεία - καταστάσεις.
    if turn == 0 and moves_played > 0:

        while True:
            # Θα δίνεται η επιλογή αποθήκευσης του τρέχοντος παιχνιδιού μόνο εφόσον :
            # (1) έχει συμπληρωθεί τουλάχιστον η 1η σειρά κινήσεων ενός νέου παιχνιδιού ή
            # (2) έχει συμπληρωθεί τουλάχιστον η 1η σειρά κινήσεων ενός αποθηκευμένου παιχνιδιού ή
            # (3) κατά την έναρξη κάθε σειράς κινήσεων (δηλ. ακόμη και της 1ης σειράς)
            #     κάθε γύρου ενός παιχνιδιού που συνεχίζεται μετά από νίκη κάποιου παίκτη

            stop = input(f"Επιθυμείτε να αποθηκεύσετε την τρέχουσα κατάσταση του παιχνιδιού ; (Απαντήστε με Y ή Ν): ")
            stop = stop.title()

            # Θα γίνει τελικά αποθήκευση του παιχνιδιού
            if stop == "Y":

                # Μετασχηματισμός της λίστας των δεδομένων του ταμπλώ
                # στη κατάλληλη "κωδικοποιημένη" μορφή προς αποθήκευση στο αρχείο

                for list in range(len(board)):
                    for symbol in range(len(board[list])):

                        if board[list][symbol] == 'X':
                            board[list][symbol] = '1'
                        elif board[list][symbol] == 'O':
                            board[list][symbol] = '2'
                        else:
                            board[list][symbol] = '0'   

                print('Θα γίνει αποθήκευση της τρέχουσας κατάστασης του παιχνιδιού.')

                # Καθορισμός του καταλόγου αποθήκευσης του αρχείου
               # directory_s = "H:\Python Projects\\"

                #directory_s = "C:\OPA\\"
                directory_s = "C:\\" # needs to be run as administrator for writing onto disk c: (root)
                # Εισαγωγή του ονόματος του αρχείου του αποθηκευμένου παιχνιδιού

                filename_s = input(f"Εισάγετε το όνομα του αρχείου (χωρίς κατάληξη):")
                filepath_s = directory_s + filename_s + ".csv"

                # Δημιουργία του αρχείου & άνοιγμα για εγγραφή σ' αυτό διαμέσου του csv.writer()
                with open(filepath_s, "w", newline="", encoding="utf-8") as save_file:
                    plirofories_tableux = csv.writer(save_file, delimiter=",")

                # Εγγραφή των κωδικοποιημένων δεδομένων των εμφωλιασμένων λιστών της λίστας board
                    # Αντί για for row in board: plirofories_tableux.writerow(row)
                    plirofories_tableux.writerows(board)

                # Εγγραφή του σκόρ του παιχνιδιού σε επόμενη γραμμή μετά τα δεδομένα
                    plirofories_tableux.writerow(score)

                print("Η αποθήκευση του παιχνιδιού σας ολοκληρώθηκε επιτυχώς.")
                print(f"Η αποθήκευση του αρχείου σας έγινε στην διεύθυνση: {filepath_s}")

                # Η εντολή για αποθήκευση του παιχνιδιού έχει δοθεί και ελεγχθεί πλήρως
                stop_choice = True
                # Μετά την αποθήκευση θα τερματιστεί επίσης και το τρέχον παιχνίδι.
                game_finished = True
                # Διαφυγή από το while loop (True) ελέγχου εγκυρότητας επιλογής του χρήστη
                break  # or quit or sys.exit

            # Δεν ζητήθηκε αποθήκευση του παιχνιδιού
            elif stop == "N":

                # Τελικά το τρέχον παιχνίδι θα συνεχιστεί.
                stop_choice = False
                # Διαφυγή από το while loop (True) ελέγχου εγκυρότητας επιλογής του χρήστη
                break

            # Λανθασμένη απάντηση χρήστη : Ούτε ναι , ούτε όχι -> Ο παίκτης πρέπει να ξαναδηλώσει τι θέλει
            # Παραμονή εντός του  while loop (True) έως ότου δοθεί αποδεκτή απόκριση από το χρήστη
            else:
                print("Δεν επιλέξατε συνέχιση ή αποθήκευση του παιχνιδιού. Παρακαλώ ξαναπροσπαθείστε: ")

# Εφόσον έγινε η αποθήκευση το παιχνίδι θα τερματιστεί, χρησιμοποιώντας
# την εντολή break για διαφυγή απο το while loop (not_game_finished)
    if stop_choice:
        print("Ευχαριστούμε που παίξατε το παιχνίδι.")
        print(f"Σας υπενθυμίζουμε ότι το σκορ είναι {score[0]}-{score[1]}. Γειά σας.")
        break
    
# Ελέγχουμε εάν όλες οι στήλες είναι ήδη πλήρεις. Δεν μπορεί συνεπώς να παιχθεί περαιτέρω "κίνηση".
# Το παιχνίδι τερματίζεται, γίνεται η σχετική ενημέρωση και το σκόρ παραμένει ως είχε τη στιγμή αυτή.
# Παρατήρηση : Θα μπορούσε ιδανικά να τοποθετηθεί ό έλεγχος μετά την ολοκλήρωση της όλης διαδικασίας
# που ακολουθεί τη κάθε κίνηση, για να προσδιοριστεί η πληρότητα του ταμπλώ γρηγορότερα, δηλ. 
# μετά τη κίνηση ακριβώς εκείνη όπου το πιόνι του τρέχοντος παίκτη θα καταλάμβανε το τελευταίο
# διαθέσιμο κενό και με τη προυπόθεση φυσικά πως η κίνηση αυτή δεν θα οδηγούσε στη συνέχεια στη
# δημιουργία κάποιας νικηφόρας θέσης, έτσι ώστε να υπάρχει θεωρητικώς η δυνατότητα 
# μέσω της διαδικασίας της ολίσθησης που θα ακολουθούσε να ξαναδημιουργηθούν νέα κενά.

    if stiles_full(board):
        print("Δεν υπάρχει πλέον κανένα ελεύθερο κενό. Το παιχνίδι θα τερματιστεί άμεσα.")
        print(f"Το τελικό σκορ είναι {score[0]}-{score[1]}. Γειά σας.")
        break


# Δηλώνουμε την εκκίνηση της διαδικασίας δήλωσης της στήλης προτίμησης για τον τρέχοντα χρήστη
    column_specified = False

# Προτροπή προς το τρέχοντα χρήστη να εισάγει τον αριθμό της στήλης (μεταξύ της 1ης και
# της τελευταίας) που επιθυμεί να τοποθετήσει το πιόνι του.
# Ο αριθμός της τελευταίας στήλης έχει προκαθοριστεί από τον αριθμό των στηλών που έχει
# δηλωθεί από τους χρήστες (είτε ενός νέου, ή ενός αποθηκευμένου παιχνιδιού αντίστοιχα).

    while not column_specified:
        try:
            Column = int(input(f"Παίκτης {Players[turn][0]}: Επιλέξτε στήλη μεταξύ 1 και {column_count}: "))
        except ValueError:
            print('Λανθασμένη επιλογή : Δεν πληκτρολογήσατε κάποιον ακέραιο αριθμό')
        else:
            if Column < 1 or Column > column_count:
                print('Επιλογή στήλης εκτός ορθών ορίων: Παρακαλώ ξαναπροσπαθείστε')
            else:
                # Εφόσον ο αριθμός της στήλης που δόθηκε από το χρήστη είναι εντός των αποδεκτών ορίων
                # προχωρούμε επιπλέον να ελέγξουμε μήπως η στήλη αυτή είναι ήδη πλήρης
                #
                if not stili_full(Column, board):

                    # Εάν η στήλη δεν είναι πλήρης, προσδιορίζουμε το σημείο (δηλ. τον αριθμό της
                    #  γραμμής του ταμπλώ) στο οποίο πρέπει να τοποθετηθεί το πιόνι και κατόπιν ...
                    Row = find_free_row(Column, board)

                    # τοποθετούμε εκεί το κατάλληλο σύμβολο αναλόγως του παίκτη που παίζει εκείνη τη στιγμή
                    set_pioni(Row, Column, Players[turn][1], board)

                    # Δηλώνουμε πως η διαδικασία τρέχουσας δήλωσης στήλης προτίμησης ολοκληρώθηκε
                    column_specified = True

                    # Εάν μόλις έπαιξε ο 1ος παίκτης ενημερώνουμε τον αριθμό των γύρων που έχουν ξεκινήσει

                    if Players[turn][0] == 1:
                        moves_played += 1

                    # Ενημερώνουμε τα χαρακτηριστικά της τελευταίας κίνησης που παίχθηκε :
                    # Συντεταγμένες κίνησης (σε επίπεδο δεικτών), συμβόλου και "ονόματος" χρήστη
                    kinisi = [Row, Column, Players[turn][1], Players[turn][0]]
                else:

                    # Εάν η στήλη είναι πλήρης, ενημερώνουμε σχετικά και ζητάμε από το χρήστη
                    # να εισάγει ένα νέο αριθμό στήλης
                    print('Η στήλη που επιλέξατε είναι ήδη πλήρης. Ξαναπροσπαθείστε:')

    # Εκτύπωση του ταμπλώ μετά τη κίνηση του τρέχοντος παίκτη
    printing_tableux(board)

    # Η ύπαρξη νικηφόρας θέσης ελέγχεται όταν :
    # (1) Σε ένα νέο παιχνίδι ο παίκτης Νο.1 έχει παίξει τουλάχιστον 4 κινήσεις ή όταν
    # (2) σ΄οποιαδήποτε κίνηση παίκτη ενός νέου γύρου του τρέχοντος παιχνιδιού ή
    # [Ελέχονται από την συνθήκη: moves_played >= 4]
    # (3) σ΄οποιαδήποτε κίνηση παίκτη ενός αποθηκευμένου παιχνιδιού
    # [Ελέχεται από την συνθήκη: not new_game and moves_played < 4]
    # (εφόσον η κατανομή των πιονιών στο ταμπλώ  εκκίνησης των περιπτώσεων 2 & 3
    # μπορεί να δημιουργήσει μια νικηφόρα κατάσταση (θέση) ακόμα και μια μόνο κίνηση)

    # ----------------------------------------------------------------------------------
    if moves_played >= 4 or (not new_game and moves_played < 4):
        win = victory_position(board, kinisi)
        current_round_finished = win[0]
        score[turn] += win[1]

    if not current_round_finished:
        # To increase the turn by 1 we will use turn += 1.
        # To make it switch between player 1 and 2 alternatively, we use turn = turn % 2.

        turn += 1                   # turn=0  -> turn =1            | turn =1 -> turn =2
        # turn =1 -> turn (1 mod 2) = 1 | turn =2 -> turn (2 mod 2) = 0
        turn = turn % 2
    else:
        # Τερματισμός τρέχοντος γύρου με τη τη νίκη κάποιου παίκτη.
        # Ερώτηση για συνέχιση του παιχνιδιού με την έναρξη ενός νέου γύρου

        print(f"Ο γύρος {round} έληξε.")
        print(f"Το σκορ είναι {score[0]}-{score[1]}")
        answer = input(f"Επιθυμείτε να παίξετε ακόμα ένα γύρο (Y/N); ")
        answer = answer.title()

        while True:

            if answer == 'N':
                # Τερματισμός του ποιχνιδιού μετά από τουλάχιστον ένα γύρο και προφανώς
                # τη νίκη κάποιου παίκτη.
                game_finished = True
                print(f"Το παιχνίδι έληξε με τελικό σκορ {score[0]}-{score[1]}.")
                print(f"Ευχαριστούμε που παίξατε το παιχνίδι. Γειά σας.")
    # Διαφυγή από το while loop (True) και οριστική λήξη του τρέχοντος παιχνιδιού.
                break

            elif answer == 'Y':
                # Ξεκινά νέος γύρος - μετά από μία νίκη κάποιου από τους παίκτες.
                # Οι μεταβλητές αρχικοποιούνται όπως αρμόζει σε ενα καινουργιο παιχνίδι
                # -> εκτός του moves_played, εφόσον ενώ με τη λογική του νέου παιχνιδιού
                # ο έλεγχος υπαρξης νικητήριας θέσης δεν έχει νόημα να πραγματοποιηθεί
                # πριν από 4 τουλάχιστον κινήσεις του 1ου παίκτη, αντιθέτως επειδή ένας
                # νέος γύρος ξεκινά από μια μη μηδενική βάση (δηλ. θέση) ο νικητής μπορεί
                # να προκύψει ακόμα από μόλις μια και μοναδική κίνηση κάποιου παίκτη.

                round += 1
                turn = 0
                current_round_finished = False
                print(f"Γίνεται έναρξη νέου γύρου.")
                print()
    # Διαφυγή από το while loop (True) και συνέχιση του τρέχοντος παιχνιδιού με νέο γύρο.
                break

    # Ο χρήστης δεν προσδιόρισε σωστά συνέχιση ή τερματισμό του παιχνιδιού
    # Συνεχίζουμε με το while loop (True) μέχρι να ανταποκριθεί ορθώς.
            else:
                print("Δεν επιλέξατε συνέχιση ή λήξη του παιχνιδιού. Παρακαλώ ξαναπροσπαθείστε: ")
