#
##
## SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.5 (Thu Feb 8 19:11:13 CET 2024)
## pyFormex is a tool for generating, manipulating and transforming 3D
## geometrical models by sequences of mathematical operations.
## Home page: https://pyformex.org
## Project page: https://savannah.nongnu.org/projects/pyformex/
## Development: https://gitlab.com/bverheg/pyformex
## Distributed under the GNU General Public License version 3 or later.
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see http://www.gnu.org/licenses/.
##
"""SlidingPuzzle
A sliding puzzle game built with the pyFormex engine.
"""
# from PIL import Image
import numpy as np
import pyformex as pf
from pyformex.gui.widgets import _I
_name = pf.Path(__file__).stem
[docs]def create_grid(nx, ny, cs=64, bg='yellow', fg='black', pt=24, st='normal',
wt='normal', sw=1, gr='northwest', lw=3,
grid=True, numbers=False, ofs=1,
output=None):
"""Create an image of an (nx,ny) grid with numbered cells.
If successful, returns the image filename, else None.
"""
if output is None:
output = f"puzzle_{nx}x{ny}.png"
lw2 = lw // 2
c2 = int(cs/2 - pt/2)
w = nx * cs + lw
h = ny * cs + lw
args = ['convert',
'-size', f'{w}x{h}', f'xc:{bg}',
'-stroke', f'{fg}',
'-strokewidth', f"{lw}",
'-pointsize', f'{pt}',
'-style', f'{st}',
'-weight', f'{wt}',
'-gravity', f'{gr}',
'-fill', 'none',
'-draw', f'rectangle 1,1 {w-2},{h-2}',
]
if grid:
for i in range(1, ny):
y = i * cs + lw2
args.extend(['-draw', f'line 0,{y} {w},{y}'])
for i in range(1, nx):
x = i * cs + lw2
args.extend(['-draw', f'line {x},0 {x},{h}'])
if numbers:
for i in range(0, ny):
y = i * cs + c2
for j in range(0, nx):
x = j * cs + c2
k = i * nx + j + ofs
args.extend([
'-strokewidth', f"{sw}",
'-fill', f'{fg}',
'-draw', f"text {x},{y} '{k}'"])
args.append(output)
P = pf.command(args, verbose=False, check=False)
if P.returncode == 0:
return output
[docs]def sliding_puzzle(filename, nx, ny, shownumbers=False, # noqa: 901
level='Random', numcolor='black'):
"""Create a sliding puzzle of size (nx,ny) from the image filename"""
pf.resetAll()
pf.flatwire()
pf.clear()
pf.transparent()
ne = nx * ny
Base = pf.Formex('4:0123').replic2(nx, ny, 1., 1.)
tc = Base.coords[..., :2] * (1./nx, 1./ny)
tgt = np.arange(ne)
pos = np.arange(ne)
invis = ne-1
solvable = level == 'Moves'
nmoves = 0
def get_moves(tocell):
"""Set possible moves to cell"""
y, x = divmod(tocell, nx)
# print(f"{tocell=} -> {x=}, {y=}")
moves = {}
if x > 0:
moves[tocell-1] = 'r'
if x < nx-1:
moves[tocell+1] = 'l'
if y > 0:
moves[tocell-nx] = 'u'
if y < ny-1:
moves[tocell+nx] = 'd'
return moves
def adjacent(cell1, cell2):
"""Check if cells are adjacent"""
return cell1 in get_moves(cell2)
def move(cell1, cell2):
"""Interchange cell1 and cell2. Return the incorrect cells"""
nonlocal nmoves
pos[[cell1, cell2]] = pos[[cell2, cell1]]
tc[[cell1, cell2]] = tc[[cell2, cell1]]
ok = pos == tgt
todo = np.where(~ok)[0]
color[:, 3] = 0.5
color[ok, 3] = 1.0
nmoves += 1
return todo
def draw():
nonlocal A
"""Draw the new situation"""
A = pf.draw(Base, texture=filename, color=color, texmode=2, texcoords=tc,
undraw=A)
if shownumbers:
showNumbers()
def showNumbers():
nonlocal B
pf.undraw(B) # todo: add undraw to drawNumbers
B = pf.drawNumbers(Base.coords[:, 2], numbers=pos, gravity='sw',
color=numcolor, size=30)
def shuffle_by_moves(n=0):
"""Do n random moves without showing"""
nonlocal invis
while n > 0:
n -= 1
moves = get_moves(invis)
cell = np.random.choice(list(moves.keys()))
pos[[invis, cell]] = pos[[cell, invis]]
tc[[invis, cell]] = tc[[cell, invis]]
invis = cell
def taxicab_parity(invis, nx):
"""Compute taxicab parity of the invis location"""
y, x = divmod(invis, nx)
return (y + x) % 2
def get_invariant(pos, invis):
"""Compute and display parity"""
perm_parity = {-1: 1, 1: 0}[pf.at.permutation_parity(pos)]
tax_parity = taxicab_parity(invis, nx)
invariant = (perm_parity + tax_parity) % 2
# print(f"Invariant = {perm_parity}, {tax_parity}, {invariant}")
return invariant
def shuffle_random():
"""Shuffle cells and find a proper invisible cell"""
nonlocal invis
np.random.shuffle(pos)
tc[:] = tc[pos]
invis = np.where(pos == ne-1)[0][0]
# print(f"{pos=}, {invis=}")
invariant = get_invariant(pos, invis)
final_invariant = taxicab_parity(ne-1, nx)
# print(f"{final_invariant=}")
if invariant != final_invariant:
print("UNSOLVABLE: I will swap cells 0 and 1")
cell0 = np.where(pos == 0)[0][0]
cell1 = np.where(pos == 1)[0][0]
pos[cell0] = 1
pos[cell1] = 0
print(f"{pos=}, {invis=}")
invariant = get_invariant(pos, invis)
if invariant != final_invariant:
pf.warning("This puzzle will need a swap at the end")
if level == 'Moves':
shuffle_by_moves(10*ne)
else:
shuffle_random()
# print(f"STARTING {pos=}, {invis=}")
color = pf.colorArray([(1., 1., 1., 1.0)] * ne)
invisible = 0.0 # 0.2 to make slightly visible
color[invis, 3] = invisible
A = pf.draw(Base, texture=filename, color=color, texmode=2, texcoords=tc)
B = None
pf.lockView()
if shownumbers:
showNumbers()
print("Click on a cell to move it to the free space")
while True:
moves = get_moves(invis)
# print(f"{moves=}")
sel = pf.pick('element', prompt='', oneshot=True)
# print(sel)
if sel:
cell = sel[0][0]
# print(f"{cell=}")
if cell in moves:
todo = move(cell, invis)
invis = cell
# get_invariant(pos, invis)
if not solvable and len(todo) == 2 and (todo != invis).all():
print(f"Possible block {todo=} {invis=}")
print("Cells are adjacent:", adjacent(*todo))
if pf.ack(f"This puzzle is not solvable. Should I swap "
f"cells {todo[0], todo[1]}?"):
todo = move(*todo)
color[invis, 3] = 1.0 if len(todo) == 0 else invisible
draw()
if len(todo) == 0:
print(f"Congrats, you've solved the puzzle in "
f"{nmoves} moves = {nmoves/ne} moves per cell")
C = pf.drawText(
"CONGRATULATIONS!",
pos=(pf.canvas.width()/2, pf.canvas.height()/2),
size=100, color='red', gravity='c')
pf.sleep(3)
if shownumbers:
pf.undraw(B)
pf.undraw(C)
break
else:
print("Can not move this cell")
else:
if pf.ask("No cell picked: stop or continue?",
['Stop', 'Continue']) == 'Stop':
break
def run():
pf.resetAll()
store = _name+'_data'
if store in pf.PF:
filename = pf.PF[store]['filename']
else:
filename = pf.cfg['datadir'] / 'puzzle.png'
from pyformex.gui.widgets import ImageView
viewer = ImageView(filename) # image viewer widget
# TODO: Integrate this in a ImageLoadDialog? or in a LoadImageViewer?
def selectImage(field):
fn = pf.askImageFile(field.value())
if fn:
viewer.showImage(fn)
dialog = pf.currentDialog()
print(dialog)
dialog = field.dialog()
print(dialog)
if dialog:
dialog.updateData({'filename': fn})
return fn
res = pf.askItems(store=_name+'_data', modal=False, items=[
_I('filename', filename, text='Image file', itemtype='button',
func=selectImage),
viewer, # image previewing widget
_I('nx', 4, min=2, text='Number of cells in horizontal direction'),
_I('ny', 4, min=2, text='Number of cells in vertical direction'),
_I('shownumbers', True, text='Show cell numbers'),
_I('level', choices=['Random', 'Moves'],
text='Shuffling method'),
])
if res:
# print(res)
if pf.Path(res['filename']).name == 'puzzle.png':
filename = create_grid(res['nx'], res['ny'], ofs=1,
numbers=not res['shownumbers'])
if filename and pf.Path(filename).exists():
res['filename'] = filename
sliding_puzzle(**res)
if __name__ == '__draw__':
run()
# End