-2

I want to generate several 3x3 puzzles (https://datawookie.netlify.app/blog/2019/04/sliding-puzzle-solvable/) with the same difficulty where difficulty is defined as the minimum necessary moves to reach the solution. For example, in a puzzle [1,2,3,4,5,6,7,0,8], the minimum necessary move is 1 because we can reach the solution by moving 8 up.

The above site has a python code to determine solvability, and I modified it a little bit so that it gives me the number of inversions:

def solvable(tiles):
    count = 0
    for i in range(8):
        for j in range(i+1, 9):
            if tiles[j] and tiles[i] and tiles[i] > tiles[j]:
                count += 1
    return [count, count % 2 == 0]

But the number of inversions is not the minimum necessary moves. How could I modify the code so that it also returns the minimum necessary moves? And, is there any way to automatically generate puzzles with the same minimum necessary moves?

  • 1
    So what _is_ difficulty and why is it not the same as the number of inversions? This is not a programming question, it’s about creating the specification. – Melebius Jun 02 '20 at 10:22
  • I’m voting to close this question because it’s about creating the specification (which is not a programming question), not about solving it. – Melebius Jun 02 '20 at 10:23
  • 3
    There are only 9!/2 = 181440 possible 3x3 puzzles. Enumerating and labelling them all with their minimum solution should not take long. – Paul Hankin Jun 02 '20 at 10:34
  • Sorry that my question was unclear. I've just edited my question. – Yuki Takahashi Jun 02 '20 at 11:12

3 Answers3

1

The "difficulty" of a puzzle can be estimated by different metrics (e.g. number of inversions, initial configuration, size, etc.). Some are meaningful, some are not. That's up to you to try different ones and decide whether they are good "difficulty" estimators. But keep in mind that sometimes, what you call "difficulty" is subjective.

Find those metrics and try to evaluate your puzzles with them.

Adam Boinet
  • 521
  • 8
  • 22
  • Yes, sorry that my question was unclear. The difficulty is the minimum necessary moves to reach the solution. I edited the question so that it's now clear (I hope). – Yuki Takahashi Jun 02 '20 at 11:13
1

Introducing a difficulties dictionary, as well as an is_solvable boolean in solvable(), and defining generate_tiles() to produce solvable game configurations using itertools.permutations(), as well as choose_difficulty() with default level set to easy:

from itertools import permutations
from pprint import pprint as pp


def solvable(tiles):
    count = 0
    for i in range(8):
        for j in range(i+1, 9):
            if tiles[j] and tiles[i] and tiles[i] > tiles[j]:
                count += 1

    is_solvable = count % 2 == 0

    if is_solvable:
        difficulties = {'0': 'trivial',
                        '2': 'easy',
                        '4': 'medium',
                        '6': 'hard'
                        }
        difficulty = difficulties.get(str(count), 'very hard')
        return [difficulty, count, is_solvable]

    return [count, is_solvable]


def generate_tiles(count=2):
    """Generate solvable tiles for the 3x3 puzzle."""
    tile_candidates = list(permutations(list(range(9))))
    good_tiles = []
    for tile_candidate in tile_candidates:
        if solvable(tile_candidate)[-1]:
            good_tiles.append(tile_candidate)
    return good_tiles


def choose_difficulty(tiles, level=2):
    """Choose difficulty for the 3x3 puzzle, default level is easy (2)."""
    labelled_tiles = []
    for tile in tiles:
        labelled_tiles.append({"tile": tile,
                               "label": solvable(tile)
                               })
    level_tiles = []
    for tile_dict in labelled_tiles:
        if tile_dict['label'][1] == level:
            level_tiles.append(tile_dict)
    return level_tiles


if __name__ == '__main__':
    # Generate all solvable and easy tiles
    tiles = generate_tiles()
    pp(choose_difficulty(tiles))

Returns all the easy tiles:

...
 {'label': ['easy', 2, True], 'tile': (2, 3, 1, 4, 5, 6, 7, 8, 0)},
 {'label': ['easy', 2, True], 'tile': (3, 0, 1, 2, 4, 5, 6, 7, 8)},
 {'label': ['easy', 2, True], 'tile': (3, 1, 0, 2, 4, 5, 6, 7, 8)},
 {'label': ['easy', 2, True], 'tile': (3, 1, 2, 0, 4, 5, 6, 7, 8)},
 {'label': ['easy', 2, True], 'tile': (3, 1, 2, 4, 0, 5, 6, 7, 8)},
 {'label': ['easy', 2, True], 'tile': (3, 1, 2, 4, 5, 0, 6, 7, 8)},
 {'label': ['easy', 2, True], 'tile': (3, 1, 2, 4, 5, 6, 0, 7, 8)},
 {'label': ['easy', 2, True], 'tile': (3, 1, 2, 4, 5, 6, 7, 0, 8)},
 {'label': ['easy', 2, True], 'tile': (3, 1, 2, 4, 5, 6, 7, 8, 0)}]
Gustav Rasmussen
  • 3,720
  • 4
  • 23
  • 53
  • 1
    Sorry if my unclear question was unclear: the difficulty is the minimum necessary moves to reach the solution. For example, in a puzzle [1, 2, 3, 4, 5, 6, 7, 0, 8], the minimum necessary move is 1 because we only have to move 8 up to solve the puzzle. How could I modify the code so that it returns the minimum necessary moves to each the solution? And, is there any way to generate puzzles with the same minimum necessary moves? Ps. I'll edit my question to make the difficulty clear. – Yuki Takahashi Jun 02 '20 at 11:09
  • 1
    Hi @Yuki, please run the provided choose_difficulty() function, and set the desired difficulty level in accordance with the "difficulties" dictionary specified in the solvable() function (default difficulty is "easy") – Gustav Rasmussen Jun 02 '20 at 11:24
  • 1
    Thank you Gustav! But I think the difficulty is still defined as the number of inversions. The puzzle [1,2,3,4,5,6,7,0,8] has inversion 0, but the minimum necessary moves is 1. Could we redefine difficulty (so if we set the difficulty = 1, then it returns all the puzzles with the minimum necessary moves equals 1)? – Yuki Takahashi Jun 02 '20 at 11:40
  • 1
    Ok, I will try to write a solve() function, and use it to derive a new difficulty metric and get back to you again as soon as possible. – Gustav Rasmussen Jun 02 '20 at 12:11
  • 1
    Ok, after researching this topic abit, I see that there are numerous general (NxN) Python solutions for the sliding puzzle game, e.g. http://www.openbookproject.net/py4fun/tiles/tiles.html , I propose implementing one of these, and use it to generate a difficulty_rating function to replace the difficulties-dictionary I provide in my answer above. – Gustav Rasmussen Jun 02 '20 at 13:12
1

You have 3 general approaches:

1 - If the number of puzzles to create is limited, you can generate a puzzle, and solve it to obtain the exact minimum number of moves - you then use that to classify the puzzles per level of difficulty.

2- From a solved position, you can scramble a puzzle by randomly sliding tiles - this will give you an estimate of the difficulty; some moves may cancel previous ones, so the number of moves will be capping.

2-bis) A more sophisticated scrambler will prevent repeated states, and give you a more exact path length as in (1) - You will still have some puzzle classified as hard (long path) when they are in fact easy, when there exist a more efficient shortcut in the random path.

3 - as was mentioned in other answers, you can find metrics that estimate the number of moves required, but this may not be easy to get a good estimate.

Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80