# ==============================================================================
"""SLIDE : a color-based slide puzzle game"""
# ==============================================================================
__author__  = "Christophe Schlick modified by Philippe Blasi"
__version__ = "2.0" # play game with keyboard
__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, key=on_key) # create main window
  colors = ('#FFF','#F00','#0F0','#00F','#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 = [[0 for col in range(5)] for row in range(5)]
  for i in range(25):
    row,col = divmod(i,5) # get cell coords by Euclidian division
    win.grid[row][col] = win.cells[i]

    # position for the white square in win.col_white and win.row_white
    if win.grid[row][col] == 0:
      win.row_white = row
      win.col_white = col

  show()
# ------------------------------------------------------------------------------
def on_key(widget:object, code:str, mods:str) -> None:
  """callback function for all keyboard events"""
  if code not in ('Right','Left','Up','Down'): return # wrong key
  move(code) # find position of empty cell and move its neighboring cell
# ------------------------------------------------------------------------------
def move(direction:str) -> None:
  """find empty cell and move its neighboring cell in provided direction"""
  row, col = win.row_white, win.col_white
  if   direction == 'Right' and col > 0: r, c = row, col-1 # get left-side cell
  elif direction == 'Left' and col < 4: r, c = row, col+1 # get right-side cell
  elif direction == 'Down' and row > 0: r, c = row-1, col # get upper-side cell
  elif direction == 'Up' and row < 4: r, c = row+1, col # get lower-side cell
  else: return # move is not valid
  win.grid[row][col], win.grid[r][c] = win.grid[r][c], 0 # apply cell move
  win.row_white, win.col_white = r, c
  if check(): win.grid = []; victory() # start animation if victory
  else: show() # show new board configuration after cell displacement
# ------------------------------------------------------------------------------
def show():
  """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()
# ==============================================================================
