# ==============================================================================
"""SLIDE : a color-based slide puzzle game"""
# ==============================================================================
__author__  = "Christophe Schlick modified by Philippe Blasi"
__version__ = "1.0" # play game with mouse
__date__    = "2022-11-12"
# ==============================================================================
from ezTK import *
from random import shuffle, randrange as rr
# ------------------------------------------------------------------------------
def main():
  """create the main window and pack the widgets"""
  global win
  win = Win(title='SLIDE', op=5, click=on_click) # create main window
  colors = ('#FFF','#F00','#0F0','#07F','#FF0') # colors for board cells
  # ----------------------------------------------------------------------------
  frame = Frame(win, grow=False)
  Button(frame, text='SHUFFLE', width=9, command=on_shuffle)
  Button(frame, text='RESET', width=9, command=on_reset)
  win.board = Frame(win, fold=5, op=0, border=1)
  for n in range(25): # create all board cells as multi-state Brick widgets
    Brick(win.board, bg=colors, width=64, height=64, border=1)
  # ----------------------------------------------------------------------------
  on_shuffle(); win.loop() # shuffle board and start game
# ------------------------------------------------------------------------------
def on_shuffle() -> None:
  """callback function for the 'SHUFFLE' button"""
  # create list of 25 cells, shuffle it and call 'on_reset' to show new grid
  win.cells = 6*[1,2,3,4] + [0]; shuffle(win.cells); on_reset()
# ------------------------------------------------------------------------------
def on_reset() -> None:
  """callback function for the 'RESET' button"""
  # convert list of 25 cells into a 5x5 grid and show it on game board
  win.grid = [win.cells[n:n+5] for n in range(0,25,5)]; show()
# ------------------------------------------------------------------------------
def on_click(widget:object, code:str, mods:str) -> None:
  """callback function for all mouse click events"""
  if widget.master != win.board: return # wrong click (= not on a board cell)
  row, col = widget.index; move(row, col) # apply move on selected cell
# ------------------------------------------------------------------------------
def move(row:int, col:int) -> None:
  """move cell located at (row,col) to the position of the empty cell"""
  if   col > 0 and win.grid[row][col-1] == 0: r,c = row,col-1 # left-side cell
  elif col < 4 and win.grid[row][col+1] == 0: r,c = row,col+1 # right-side cell
  elif row > 0 and win.grid[row-1][col] == 0: r,c = row-1,col # upper-side cell
  elif row < 4 and win.grid[row+1][col] == 0: r,c = row+1,col # lower-side cell
  else: return # move is not valid (no empty cell in neighboring cells)
  win.grid[row][col], win.grid[r][c] = 0, win.grid[row][col] # apply cell move
  if check(): win.grid = []; victory() # start animation if victory
  else: show() # show new board configuration after cell displacement
# ------------------------------------------------------------------------------
def show() -> None:
  """show current game board by setting state defined for each grid cell"""
  for n in range(25): # loop over grid cells and change state of board cells
    row, col = n//5, n%5; win.board[row][col].state = win.grid[row][col]
# ------------------------------------------------------------------------------
def check() -> bool:
  """check victory by counting number of peer cells with same state"""
  for loop in range(25): # loop over all board cells
    if loop == 12: continue # skip central cell (should be empty, anyway)
    row, col = loop//5, loop%5 # get cell coords by Euclidian division
    peers = [win.grid[row+r][col+c] for r,c in ((1,0),(-1,0),(0,1),(0,-1))
      if 0 <= row+r <= 4 and 0 <= col+c <= 4] # get states for all peer cells
    if peers.count(win.grid[row][col]) not in (2,3): return False # no victory
  return True # all cells have 2 or 3 peers with same state ==> victory
# ------------------------------------------------------------------------------
def victory() -> None:
  """play victory animation"""
  win.board[rr(5)][rr(5)].state = rr(1,5) # change color of random grid cell
  if not win.grid: win.after(10, victory) # launch 'victory' every 10 ms
# ==============================================================================
if __name__ == '__main__':
  main()
# ==============================================================================
