Skip to main content

A collection of tools to aid in solving logic grid puzzles

Project description

logicpuzzletools Package

last change 20-Mar-2022

An collection of tools to aid in solving logic puzzle problems with Python 3.x .

This does not use sophisticated Artificial Intelligence methods. Instead it helps to generate permutations and tools to filter these permuations. Finally, a ready made way of outputing those solution(s).

Author: Daniel L. Bates (dan.bates)

The pypi module logicpuzzletools

What kind of logic puzzle problems?

The kind of logic puzzle problem the tools help solve are also called logic grid puzzles. The logic grid puzzle has two or more categories. Each category is a set of something, Often one category is people with a set of names, although this is not always true. Each set has the same cardinality.

The elements are sometimes the same as the formulation. In this set of tools they must be unique. If the problem has 5 girls who are friends each naming there pet after one of there 4 friends you have to go through the gymnastics of making them different. For instance 'diane' and 'diane_pet'.

You can always find one category not to permute. This is our fixed category. It is good to pick some category which has some order to it, like house numbers or floor numbers in a hotel (yes you can leave out floor 13).

Categories are global or nonlocal such that all the solution code has access to them.

There are also a set of rules describing relationships between elements of disparate (or the same) category. The are commonly solved with the upper left of a matrix which includes the lower left to upper right diagonal. In the columns typically one category is left out. In the rows typically a different category is left out. The set element names label the rows and columns.

!(seeA Simple example from the Brainzilla website.)

The Einstein puzzle.

On December 17, 1962 Life International published a logic grid puzzle called "Who owns the zebra". This has been called the Zebra puzzle. The problem was allegedly invented by Albert Einstein, so it is also called the Einstein puzzle. A more recent incarnation of the problem, without the Zebra is stated here:

Categories House #s colors nationalites drinks Pets Smokes
indexes 1 yellow swede beer fish bluemasters
2 red dane coffee birds dunhills
3 green german milk horses princes
4 blue brit tea cats blends
5 white norwegian water dogs pallmalls

Rules:

    There are five houses in a row in different colors.
    In each house lives a person with a different nationality.
    The five owners drink a different drink,
    smoke a different brand of cigar
    and keep a different pet, one of which is a Walleye Pike.
    
     1. The Brit lives in the red house.
     2. The Swede keeps dogs as pets.
     3. The Dane drinks tea.
     4. The green house is on the left of the white house.
              4. has to be interpreted as immediatly to the left of
              or there is more than one solution
     5. The green house owner drinks coffee.
     6. The person who smokes Pall Malls keeps birds.
     7. The owner of the yellow house smokes Dunhills.
     8. The man living in the house right in the center drinks milk.
     9. The man who smokes Blends lives next to the one who keeps cats.
    10. The Norwegian lives in the first house.
    11. The man who keeps horses lives next to the one who smokes Dunhills.
    12. The owner who smokes Bluemasters drinks beer.
    13. The German smokes Princes.
    14. The Norwegian lives next to the blue house.
    15. The man who smokes Blends has a neighbor who drinks water.
              15. has to be interpreted as lives adjacent to.  or there
              is more than one solution.
    The question is-- who owns the fish?

The code I give you to solve this does not use a grid. It is instead a constraint satisfaction method. This generates permutations, Not all permutatations are completly examined as some are pruned. There are permutations for each category. Elements are related to elements of different categories by there index in their own category.

These permutations are taken through filters which are the rules.

Because position is important for each category we use lists instead of sets to represent them.

Here is the code using logic_puzzle_tools for the Einstein problem:

from logic_puzzle_tools import *
...
def einstein ():
    '''
    Einstein puzzle

    There are five houses in a row in different colors.
    In each house lives a person with a different nationality.
    The five owners drink a different drink,
    smoke a different brand of cigar
    and keep a different pet, one of which is a Walleye Pike.
    
     1. The Brit lives in the red house.
     2. The Swede keeps dogs as pets.
     3. The Dane drinks tea.
     4. The green house is on the left of the white house.
              4. has to be interpreted as immediatly to the left of
              or there is more than one solution
     5. The green house owner drinks coffee.
     6. The person who smokes Pall Malls keeps birds.
     7. The owner of the yellow house smokes Dunhills.
     8. The man living in the house right in the center drinks milk.
     9. The man who smokes Blends lives next to the one who keeps cats.
    10. The Norwegian lives in the first house.
    11. The man who keeps horses lives next to the one who smokes Dunhills.
    12. The owner who smokes Bluemasters drinks beer.
    13. The German smokes Princes.
    14. The Norwegian lives next to the blue house.
    15. The man who smokes Blends has a neighbor who drinks water.
              15. has to be interpreted as lives adjacent to.  or there
              is more than one solution.
    The question is-- who owns the fish?
    '''
    logic_puzzle_setup()
    houseposition = [1, 2, 3, 4, 5] # This is fixed Left:1 2 3 4 5:Right
    # these belong to scope of einstein
    nationalities = [None] * 5
    drinks        = [None] * 5
    housecolor    = [None] * 5
    pets          = [None] * 5
    smokes        = [None] * 5

    def choose_nationalities():
        nonlocal nationalities
        permute_nationalities = permute('norwegian, brit, swede, dane, german', nationalities)
        for p in permute_nationalities:
            category(nationalities, p)
            if fails(is_associated('norwegian', 1)):                    #R10
                continue
            choose_drinks()

    def choose_drinks ():
        nonlocal drinks
        permute_drinks = permute('coffee, milk, beer, tea, water', drinks)
        for p in permute_drinks:
            category(drinks, p)
            if fails(is_associated('dane', 'tea')):                     # R3
                continue
            if fails(is_associated('milk', 3)):                         # R8
                continue
            choose_housecolor()

    def choose_housecolor ():
        nonlocal housecolor
        permute_housecolor = permute('blue, green, white, red, yellow', housecolor)
        for p in permute_housecolor:
            category(housecolor, p)
            if fails(is_associated('brit', 'red')):                     # R1
                continue
            if fails(is_just_left_of('green', 'white')):                # R4
                continue
            if fails(is_associated('green', 'coffee')):                 # R5
                continue
            if fails(is_adjacent_to('norwegian', 'blue')):              # R14
                continue
            choose_pets()
            
    def choose_pets ():
        nonlocal pets
        permute_pets = permute('fish, birds, dogs, horses, cats', pets)
        for p in permute_pets:
            category(pets, p)
            if fails(is_associated('swede', 'dogs')):                   # R2
                continue
            choose_smokes()

    def choose_smokes ():
        nonlocal smokes
        permute_smokes = permute('blends, pallmalls, bluemasters, dunhills, princes', smokes)
        for p in permute_smokes:
            category(smokes, p)
            if fails(is_associated('pallmalls', 'birds')):              # R6
                continue
            if fails(is_associated('yellow', 'dunhills')):              # R7
                continue
            if fails(is_adjacent_to('blends', 'cats')):                 # R9
                continue
            if fails(is_adjacent_to('horses', 'dunhills')):             # R11
                continue
            if fails(is_associated('bluemasters', 'beer')):             # R12
                continue
            if fails(is_associated('german', 'princes')):               # R13
                continue
            if fails(is_adjacent_to('blends', 'water')):                # R15
                continue
            a_solution()

    def a_solution ():
        # a_solution. If not enough constraints, there will be more
        # than one. If too many constraint, there will be none.
        fmt1 = '%7s %11s %8s %11s %8s %12s'
        fmt2 = '%7d %11s %8s %11s %8s %12s'
        print(fmt1%('house #','nationality','drinks','housecolor','pet','smokes'))
        print('=' * 70)
        PrintSolution(fmt2, houseposition, nationalities, drinks, housecolor, pets, smokes)
        # who owns the fish
        print('the', nationalities[idx('fish')],'owns the fish, (the Walleye Pike)')

    fixed_cat(houseposition)
    choose_nationalities()  # einstein() back to the first function at the top

The soloution produced is:

house # nationality   drinks  housecolor      pet       smokes
======================================================================
      1   norwegian    water      yellow     cats     dunhills
      2        dane      tea        blue   horses       blends
      3        brit     milk         red    birds    pallmalls
      4      german   coffee       green     fish      princes
      5       swede     beer       white     dogs  bluemasters
the german owns the fish, (the Walleye Pike)

First step if you are solving more than one puzzle.

If you are going to solve more than one puzzle, you must call logic_puzzle_setup() for all but the first one. It will do no harm to call this for the first puzzle as well.

The form this takes on is to loop over permutations making each permution the category. The body of the loop tests such criteria as can be determined by the categories that have been defined. When a rule has been broken we continue with the next permutation. At the very end of the loop we bring another category into play (most easily by calling another function). Rather than actually negate each rule there is a function fails(rule) which make the code read better but does very little. The fails(rule) is equivalent to not (rule). This lets the code match the verbage of the rule.

The means of making a permutation is by calling the function category(cat : list, permutation : list). This function keeps all the references to that particular category valid. This does an element by element copy all the elements of permutation to the elements of category.

There is always one category you do not permute. To set the fixed category you call the following:

fixed_cat (cat: list) -> None

Each item in cat is linked back to category cat through a dictionary.

Making a generator for the permutations of each category.

The tool permute uses itertools.permute to create a generator for a category and to return it. All the categories except one need to be permuted. The generator also associates element name with the category which contains it.

def permute (things : str, cat : list) -> list:

Return a generator which gives new permutations of things.
And map each element to the category cat.

Example: permute_housecolor = permute('blue, green, white, red, yellow', housecolor)

Tools for filtering.

Unlike a much earlier version of this package, you do not ever need to specify a category.

In most of the following rule related helpers the string arg1 and arg2 are elements. They usually to different categorys. These are predicates which return True if the relationship is True and False otherwise. The relationships are for the indexes of the elements within there respective categories. All these should be thought of as the relationship by the associated fixed category. These are:

is_associated (arg1 : str, arg2 : str) -> bool:

Returns True when arg1 is the same as arg2 like 'green' and 'coffee'.

is_not_associated (arg1 : str, arg2 : str) -> bool:

Returns False when arg1 is not the same as arg2 like 'green' and 'coffee'.

For is_associated and is_not_associated to make sense, arg1 and arg2 must be in seperate categories. Ex: is_associated('green', 'green') will always be True and is_not_associated('green', 'green') will always be False.

is_just_left_of (arg1 : str, arg2 : str) -> bool:

is_just_below (arg1 : str, arg2 : str) -> bool:

These two are identical. Use the one which best matches the verbage of the rule. The truth of idx(arg2) - idx(arg1) == 1.

is_just_right_of (arg1 : str, arg2 : str) -> bool:

is_just_above (arg1 : str, arg2 : str) -> bool:

These are also identical. Pick what reads the best. The truth of idx(arg2) - idx(arg1) == -1.

is_below (arg1 : str, arg2 : str) -> bool:

The truth of idx(arg1) < idx(arg2).

is_above (arg1 : str, arg2 : str) -> bool:

The truth of idx(arg1) > idx(arg2)

is_adjacent_to (arg1 : str, arg2 : str) -> bool:

The truth of abs(idx(arg1) - idx(arg2)) == 1.

is_not_adjacent_to (arg1 : str, arg2 : str) -> bool:

The truth of abs(idx(arg1) - idx(arg2)) != 1.

after (arg1 : str, arg2 : str) -> bool:

The same as is_above.

before (arg1 : str, arg2 : str) -> bool:

The same as is_below. Use the most natural form.

just_after (arg1 : str, arg2 : str) -> bool:

The same as is_just_right_of.

just_before (arg1 : str, arg2 : str) -> bool:

The same as is_just_left_of.

idx (element : str) -> int:

No matter how many primitives are provided here, a problem will arise where they do not suffice. The function idx uses the category of element and returns the index of element within that category. With this you can build the rule you need.

Outputing a solution

To help output the solution a function PrintSolution (fmt_str, *ListSet) is provided. the parameter fmt_str is a C printf style format string for one row. ListSet are categories to be the columns. An example is the a_solution function in the einstein code.

Watch for:

This topic is discussed in more detail in a book I have written. I will update this with information when the book is published.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

logicpuzzletools-0.1.0.tar.gz (15.3 kB view details)

Uploaded Source

Built Distribution

logicpuzzletools-0.1.0-py3-none-any.whl (13.5 kB view details)

Uploaded Python 3

File details

Details for the file logicpuzzletools-0.1.0.tar.gz.

File metadata

  • Download URL: logicpuzzletools-0.1.0.tar.gz
  • Upload date:
  • Size: 15.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.7.1 importlib_metadata/4.10.0 pkginfo/1.8.2 requests/2.27.1 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.10.1

File hashes

Hashes for logicpuzzletools-0.1.0.tar.gz
Algorithm Hash digest
SHA256 9c38a88b361da55ba5bcf36bc937977663a2c82705954f1d5e5fe912daf4662b
MD5 74edefe09200490bca8712e815d9277b
BLAKE2b-256 948b04b2b84e0fccd07d5ce5c3d0b212d368b417e033762ab40ebb673076e339

See more details on using hashes here.

File details

Details for the file logicpuzzletools-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: logicpuzzletools-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.7.1 importlib_metadata/4.10.0 pkginfo/1.8.2 requests/2.27.1 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.10.1

File hashes

Hashes for logicpuzzletools-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5f331e638d0413484121850535516cc70efb8d1b2e44b6ff5f1fd840d798a018
MD5 5253bba408c1084108c74a5efde63c90
BLAKE2b-256 7b26ea789196c3543cd07747f1a62ee1d8458bc57f0011e17e81e42cd0f2a552

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page