andere opgaven: acht koninginnen | boter, kaas en eieren | logiquiz | sudoku | laatste wint

Logiquiz puzzel oplossen

Hoe een logiquiz puzzel oplossen, of hoe zelf een logiquiz ontwerpen ? Hier wordt uitgelegd hoe je dat met behulp van de computer kan doen.

Inhoud:
de puzzel
benadering
bouwstenen
aanwijzingen
tips
programma afloop
voorbeeld
zelf logiquiz ontwerpen
de code
 
contact

De puzzel:

Een logiquiz puzzel (ook wel logikwis of logigram genoemd, logic grid puzzle) is een puzzel waarbij je met behulp van aanwijzingen moet achterhalen hoe een gebeurtenis (wie-wat-waar-wanneer-hoeveel-...) heeft plaatsgevonden. Voor een voorbeeld: zie hier.
Als deze puzzel met de hand wordt opgelost, worden de aanwijzingen gebruikt om (on)mogelijke eigenschappen te matchen, net zolang tot er één combinatie overblijft. De moeilijkheidsgraad wordt o.a. bepaald door de afmetingen van het grid, en de soort aanwijzingen (bevestigingen, ontkenningen, relatieve (on)waarheden).

Benadering:

Een andere aanpak om zo'n puzzel op te lossen, is: verifieer alle mogelijke oplossingen die niet voldoen aan de aanwijzingen, dan blijft er (als het goed is) één oplossing over: dè oplossing.
 
Om alle mogelijke oplossingen te vinden, is het begrip permutatie interessant, dat geeft aan hoeveel mogelijke combinaties er bestaan.
Bijvoorbeeld: op hoeveel verschillende manieren zijn drie gekleurde ballen (rood, groen, blauw) te verdelen over drie posities ?
dat zijn 6 verschillende manieren:

(hoeveel permutaties mogelijk zijn, is te berekenen met het begrip faculteit. Op een standaard rekenmachine is dit te vinden onder de '!'-knop: bv faculteit 3 = 3! = 3 x 2 x 1 = 6  )
om het voorbeeld van de gekleurde ballen automatisch te kunnen verwerken wordt hier een getalnotatie gebruikt:

 
Bovenstaande geldt voor één eigenschap ('kleur'), dit kan worden uitgebreid met een extra eigenschap (bijvoorbeeld 'vorm'):

het aantal combinaties is nu 3! x 3! = 36 (kartesiaans produkt)
 

Bouwstenen

In dit script wordt gesproken over actoren, eigenschappen en waarden. In de opgave zijn deze te vinden als in dit voorbeeld:

 
De grotere logiquiz puzzels hebben 5 'actoren' met ieder 3 'eigenschappen', dat betekent (5!)3 = 1,728,000 mogelijke combinaties (!).
 
Om de aanwijzingen van de puzzel te kunnen verwerken door een computer, zijn deze twee functies nodig:
• wat is de waarde (van een eigenschap) van een actor (zie blauwe pijl);
• wat is de actor, die bij een waarde (van een eigenschap) hoort (zie groene pijl in de figuur);
met deze twee bouwstenen zijn alle puzzels op te lossen (!)
 
Bijvoorbeeld, om een actor te vergelijken op basis van zijn eigenschappen (zie rode pijl)
 

Aanwijzingen

Voor het vastleggen van de aanwijzingen van de puzzel, kan gebruik worden gemaakt van deze formuleringen:

beschrijvingformulering
Biologie is het favoriete vak van Ariearie == biologie
Jacobus behaalde de hoogst mogelijke scorecijfer.jacobus == 10
voor Engels werd geen zeven behaaldcijfer.engels != 7
Wilhelmina volgt haar favoriete vak bij mw. deGrootwilhelmina == degroot
dhr. Visser geeft Engels
(geeft dezelfde uitkomst)
visser == engels
visser == vak.engels
Wilhelmina behaalde een betere score dan Dirkcijfer.wilhemina > cijfer.dirk
Met geringe kennis van python operatoren kan vrijwel iedere aanwijzing aangepakt worden:
het vak Frans wordt gegeven door mw. deGroot, en Economie door dhr. Jansen, of omgekeerdfrans == degroot and economie == jansen or frans == jansen and economie == degroot
het cijfer dat dhr. Mulder gaf aan zijn leerling, kwam overeen met het aantal letters waaruit zijn vak bestaatlen(vak.mulder) == cijfer.mulder
voor economie en engels werden de minste cijfers gescoordcijfer.economie in [5, 7] and cijfer.engels in [5, 7]
 
(zie dit tekst bestand voor de uitwerking van enkele puzzels, en voor meer ideeen om formuleringen samen te stellen)

Tips:

• bij het opstellen van de formuleringen, let goed op wat er links en rechts van het '==' teken komt te staan. Wat vergelijk je: een eigenschap, waarde of actor ? En index of feitelijke waarden ?
• '==' betekent 'gelijk aan'; '!=' betekent 'ongelijk aan'
• dit script kan zowel overweg met absolute (on)waarheden ('de driehoek is rood', 'Jan is niet ziek', 'de fietser ging naar school'), als ook met relatieve waarheden ('Karel is langer dan Marie', 'een appel is 10 cent duurder dan een ei')
• dit script kan overweg met getallen. Echter alleen met gehele getallen, en niet met fracties. Dit kan altijd creatief worden opgelost, door in zo'n geval de getallen te vermenigvuldigen met de gemeenschappelijke deler, of komma's te verplaatsen.
• dit script kan niet overweg met actoren die een getal zijn. Zo'n actor kan gerust geruild worden met een eigenschap (dat maakt voor de uitkomst niet uit)
• gebruik geen spaties in eigenschappen of waarden: vervang spaties door underscores, of verzin een andere naam zonder spaties
• streef ernaar alleen kleine letters te gebruiken, dat voorkomt verwarring en problemen
• geef de oplossing in het puzzel.txt bestand (zie andere voorbeelden aldaar), het script zal dan helpen zoeken naar tegenstrijdigheden.

Programma afloop:

1. het bestand met de voorbeeld opgaven wordt ingelezen, en de gebruiker wordt gevraagd een keuze te maken uit de voorbeelden
 
2. de actoren, eigenschappen en waarden worden uit hetzefde bestand gehaald
 
3. een nieuwe instantie van de Logigram-klasse ( Logigram() ) wordt aangemaakt, waarbij de actoren, eigenschappen en waarden worden meegegeven.
De starttijd wordt onthouden.
 
4. de aanwijzingen worden toegevoegd aan de puzzel ( add_clue() )
 
5. een voor een worden de aanwijzingen losgelaten op de oplossingen ( los_op() )
 
6. de resultaten worden samengevat ( __str__() )
 
7. bij minder dan 10 oplossingen, worden deze getoond ( tabelleer_oplossing() )
 
8. bij het vervallen van de instantie wordt berekend hoe lang eea heeft geduurd. ( __del__() )
 
 

Voorbeeld

Onderstaande puzzel is afkomstig van de website: logiquiz.nl
 
Opgave Gastsprekers © logiquiz.nl
 
Groep 8 ontvangt dit schooljaar enkele gastsprekers.

 aanwijzinglogica
1.in december komt iemand, die geen tycho heet, over vuurwerk vertellen.
In mei spreekt een 45-jarige over vrijheid.
vuurwerk == december
tycho != vuurwerk
vrijheid == mei
leeftijd.mei == 45
2.Jeanet vertelt over nepwapensjeanet == nepwapens
3.Degene die in februari over alcohol spreekt is 2 jaar jonger dan Peteralcohol == februari
leeftijd.peter - leeftijd.alcohol == 2
4.Tycho is 42 jaar oud. Cor is ouder dan Ninaleeftijd.tycho == 42
leeftijd.cor > leeftijd.nina
5.Wie in juni spreekt, heeft het niet over EHBOehbo != juni
 
Deze opgave is terug te vinden in het 'puzzel.txt' bestand (link).

Zelf een logiquiz ontwerpen

Met dit programma is het ook eenvoudig om zelf een logiquiz te ontwerpen:
 
1. bedenk een onderwerp met bijbehorende actoren, eigenschappen, en waarden.
 
2. zet deze op papier (of Excel) tegen elkaar uit, en kies één enkele oplossing.
 
3. bedenk nu enkele aanwijzingen die overeenkomen met de oplossing, en vertaal die in formuleringen.
 
4. run het script, en bekijk hoeveel oplossingen dit oplevert:
   → als er geen (0) oplossingen zijn dan is een aanwijzing strijdig met de oplossing
   → als er nog ~10 (of meer) oplossingen zijn dan zullen nog meer aanwijzingen moeten worden toegevoegd
   → als er slechts enkele oplossingen overblijven, vergelijk dan die oplossingen onderling om specifieke aanwijzingen te bedenken.
 
5. totdat er één oplossing overblijft.
 
tip: geef de oplossing in het puzzel.txt bestand (zie andere voorbeelden aldaar), het script zal dan helpen zoeken naar tegenstrijdigheden.
 
 
Veel plezier en succes !!
 

De code



#!/usr/bin/env python

"""zoekt naar de oplossing van een Logiquiz puzzel"""
__author__ = "Mark Nauta"
__copyright__ = "Copyright 2021"
__credits__ = "Mark Nauta"
__license__ = "GPL"
__version__ = "0.4.0"
__maintainer__ = "Mark Nauta"
__email__ = "markatnadropuntnl"
__status__ = "Production"

# lees het volledige verhaal: https://nadro.nl/mark/puzzels/logiquiz.html  

import ast
from operator import lt, le, gt, ge, sub, add
import os
import re
import sys
from datetime import datetime
from itertools import permutations, product, chain
from math import factorial

class Logigram():
    def __init__(self, title, subtitles, actors, property_names, property_values, de_oplossing):
        self.title = title
        self.subtitles = subtitles
        self.actors = actors
        self.property_names = property_names
        self.property_values = property_values
        self.timerstart = datetime.now()
        self.timerend = None
        self.aantal_kandidaten = pow(factorial(len(actors)), len(property_names))
        self.de_oplossing = de_oplossing
        self.oplossingen = []
        self.clues = []

        print('puzzel:', self.title, end='\r')

    def add_clue(self, clue):
        self.clues.append( (clue[0], clue[1], clue[2]) )

    def los_op(self):
        # bepaal de volgorde van de clues om zo een sneller resultaat te behalen
        s = []
        for index in range(len(self.clues)):
            s.append(len(re.findall(r'\[', self.clues[index][2])) + len(re.findall(r'index', self.clues[index][2])) + len(re.findall(r'!=', self.clues[index][2])) + 2*len(re.findall(r'or', self.clues[index][2])))
        si = sorted(range(len(s)), key=lambda k: s[k])
    
        parse = ''
        parse +='from itertools import permutations, product\n'
        parse += '\n'
        for indx in range(len(self.clues)):
            clue = self.clues[indx]
            parse += '# {}\n# {}\n# {}\n'.format(clue[0], clue[1], clue[2])
            parse += 'def clue{}(opln):\n  for opl in opln:\n    if {}:\n      yield(opl)\n'.format(indx, clue[2])
            parse += '\n'
        parse += '\n'
        parse += 'def los_op():\n'
        parse += '  kandidaten = product(permutations(range({})), repeat={})\n'.format(len(self.actors), len(self.property_names))
        ss = 'kandidaten'
        for indx in range(len(self.clues)):
            ss = 'clue{}({})'.format(si[indx], ss)
        parse += '  return list({})\n'.format(ss)

        parse += '\n'
        parse += 'if __name__ == "__main__":\n'
        parse +=  '  print(los_op())\n'

        this_folder = os.path.dirname(__file__)
        fname = 'logicode.py'
        with open(os.path.join(this_folder, fname), 'w') as f:
            f.write(parse)
        import logicode
        self.oplossingen = logicode.los_op()
        self.timerend = datetime.now()

    def __del__(self):
        ''' 
        berekent hoe lang het heeft geduurd om de puzzel op te lossen
        '''
        delta = self.timerend - self.timerstart
        print('{} oplossing(en) over in {:0.3f} sec.'.format(len(self.oplossingen), delta.seconds + delta.microseconds/1000000))

    def __str__(self):
        '''
        de tekstuele representatie van een logigram
        = het logigram met alle resterende oplossingen
        '''
        uit = 'titel: {}\n'.format(self.title, )
        uit += 'opgave:\n' + self.subtitles + '\n'
        for indx in range(len(self.clues)):
            uit += '{}. {}\n'.format(indx, self.clues[indx][0], )
        uit += 'van {} mogelijke oplossingen '.format(f"{self.aantal_kandidaten:,}", )
        if (len(self.oplossingen) == 0):
            uit += 'is geen oplossing gevonden.\n'
        elif (len(self.oplossingen) == 1):
            uit += 'is 1 oplossing gevonden\n'
            uit += self.tabelleer_oplossing(self.oplossingen[0])
        else:
            uit += 'resteren {} oplossingen.\n'.format(len(self.oplossingen))
            if (len(self.oplossingen) < 10):
                for oplossing in self.oplossingen:
                    uit += self.tabelleer_oplossing(oplossing) + '\n'
            uit += 'deze puzzel kent {} resterende oplossingen\n'.format(len(self.oplossingen))
        for o in self.oplossingen[:10]: 
            uit += str(o) + '\n'
        return uit

    def tabelleer_oplossing(self, een_oplossing):
        '''
        maakt een leesbaar formaat van een oplossing in tabelvorm
        '''
        #een_oplossing = [ self.permutaties[indx] for indx in een_oplossing ]
        uit = ''
        act_width = max( [ len(actor) for actor in self.actors ] ) + 3
        prop_width = []
        for property_index in range(len(self.property_names)):
            property_width = len(self.property_names[property_index])
            max_prop_width = ( max( [ len(str(item)) for item in self.property_values[property_index] ] ) )
            prop_width.append(max(property_width, max_prop_width) + 3)
        uit += ' ' * act_width
        for property_index in range(len(self.property_names)):
            uit += self.property_names[property_index].ljust(prop_width[property_index])
        uit += '\n'

        transpose = [ [ een_oplossing[i][j] for i in range(len(self.property_names)) ] for j in range(len(self.actors)) ]
        for a_indx in range(len(self.actors)):
            uit += self.actors[a_indx].ljust(act_width)
            for e_indx in range(len(self.property_names)):
                uit += str(self.property_values[e_indx][transpose[a_indx][e_indx]]).ljust(prop_width[e_indx]) 
            uit += '\n'
        uit += '\n'

        return uit

    def test_clues(self, clue_index):
        ''' 
        gegeven van een correcte oplossing van de puzzel, bekijkt ieder van de clues of deze in strijd is met de oplossing
        handig bij debuggen van je puzzel
        '''
        if self.de_oplossing is None:
            print('bij de opgave is geen oplossing gegeven')
            return None

        opl = self.de_oplossing[:]
        clue = self.clues[clue_index]
        is_correct = eval(clue[2])
        if not is_correct:
            print('{}:\n  {}'.format('\ndeze aanwijzing klopt NIET', clue[1], ) )
            self.evaluate_statement(clue[2])
        return is_correct

    def evaluate_statement(self, parsed_line):
        if not self.de_oplossing: 
            return None

        print('uitwerken van de logika:')
        match =True
        while match:
            match = re.search(r'opl\[(\d+)\]\.index\((\d+)\)', parsed_line)
            if match:
                property_index = int(match.group(1))
                value_index = int(match.group(2))
                actor_index = self.de_oplossing[property_index].index(value_index)
                replacement = str(actor_index)
                parsed_line = parsed_line[:match.span()[0]] + replacement + parsed_line[match.span()[1]:]

        match =True
        while match:
            match = re.search(r'opl\[(\d+)\]\[(\d+)\]', parsed_line)
            if match: 
                property_index = int(match.group(1))
                actor_index = int(match.group(2))
                value_index = self.de_oplossing[property_index][actor_index]
                replacement = str(value_index)
                parsed_line = parsed_line[:match.span()[0]] + replacement + parsed_line[match.span()[1]:]

        match = True
        while match:
            match = re.search(r'\[(\'?\w+\'?\,?\s?)+\]\[(\d)\]', parsed_line)
            if match:
                which = int(match.group(2))
                m = re.search(r'(\w+?\,?\s?)+', match.group(0).replace('\'', ''))
                reeks = re.split(r',\s?', m.group(0))
                replacement = reeks[which]
                if not replacement.isnumeric():
                    replacement = '\'{}\''.format(replacement)
                parsed_line = parsed_line[:match.span()[0]] + replacement + parsed_line[match.span()[1]:]

        print('   {}'.format(parsed_line, ) , end='\n\n')

def parse_content(fullpath):
    '''
    het puzzelbestand 'puzzel.txt' bevat een aantal (voorbeeld) opgaven
    de gebruiker wordt gevraagd welke op te lossen
    '''
    if not(os.path.exists(fullpath)):
        sys.exit('het bestand \'{}\' kon niet worden gevonden'.format(fullpath, ))
    content = open(fullpath).read()
    info = []
    for match in re.finditer(r'(?<!#){(.+)}', content):
        info.append( (match.group(1), match.span()) )
    cols = 3
    items = len(info)
    matrix = [ list(range(i, items, items//cols+1)) for i in range(items//cols+1) ] 
    for rij in matrix:
        for cel in rij:
            print(('[{}] {}'.format(cel, info[cel][0])).ljust(25), end=' ')
        print('')
    while True:
        respons = input('Kies puzzel: ')  
        if respons.isnumeric():
            if int(respons) in range(len(info)):
                break
    info.append( ('einde', (len(content), len(content)+10)) )
    title = info[int(respons)][0]
    this_content = content[info[int(respons)][1][0]:info[int(respons)+1][1][0]]
    lines = this_content.splitlines()

    # read the subtitle(s) (puzzle header)
    subtitles = []
    for indx in range(len(lines)):
        line = lines[indx]
        if line[:1] == '%':
            subtitles.append(line[:])
            lines[indx] = ''
    subtitles = '\n'.join(subtitles).replace('%', '')

    # find the actors            
    actor_found = False
    property_names, property_values = [], []
    for indx in range(len(lines)): 
        line = lines[indx]
        match = re.search(r'^([a-z_]+)\s=\s(\w+,[\w\s,]{2,})', line)

        if match:
            prop, values = match.group(1), match.group(2).replace(' ', '').split(',')
            if not actor_found:
                actors = values[:]
                actor_found = True
            else:
                property_names.append(prop)
                property_values.append(values)
            lines[indx] = ''

    # vind DE oplossing (indien gegeven)
    de_oplossing = None
    for indx in range(len(lines)): 
        line = lines[indx]
        opll = []
        match = re.search(r'^opl.*(\((\((\d,?\s?)+\),?\s?)+\))', line)
        if match:
            for m in re.finditer(r'(\((\d,?\s?)+\))', line):
                reeks = []
                perm = m.group(1)
                for ma in re.finditer(r'(\d+),?\s?', perm):
                    reeks.append(int(ma.group(1)))
                opll.append(tuple(reeks))
            de_oplossing = tuple(opll)
            lines[indx] = ''

    logic_info = [ line for line in lines if len(line) > 3 ]

    if any( [ len(property_values[index]) != len(actors) for index in range(len(property_names)) ] ):
        sys.exit('fout: controleer het aantal actoren vs. het aantal mogelijke waarden per eigenschap')

    return title, subtitles, actors, property_names, property_values, logic_info, de_oplossing

def parse_logic(actors, property_names, property_values, logic_info):

    is_numeriek = [ all([ value.isnumeric() for value in property_values[property_index] ]) for property_index in range(len(property_names)) ]
    non_num_values = [ property_values[pi][pvi] for pvi in range(len(actors)) for pi in range(len(property_names)) if not is_numeriek[pi] ]
    all_values = [ property_values[pi][pvi] for pi in range(len(property_names)) for pvi in range(len(actors)) ]
    prop_combis = [ '{}.{}'.format(property_names[property_index], value) for property_index in range(len(property_names)) for value in property_values[property_index] ]
    x_combis_1 = [ '{}.{}'.format(property_names[property_index], property_values[prop_index][value_index]) for property_index in range(len(property_names)) for prop_index in range(len(property_names)) for value_index in range(len(actors)) if property_index != prop_index ]
    x_combis_2 = [ '{}.{}.{}'.format(property_names[property_index], property_names[prop_index], property_values[prop_index][value_index]) for property_index in range(len(property_names)) for prop_index in range(len(property_names)) for value_index in range(len(actors)) if property_index != prop_index ] 
    cross_combis = x_combis_1 + x_combis_2

    def p_indices(match):
        if match in non_num_values:
            property_index, property_value_index = divmod(all_values.index(match), len(actors))
        elif match in prop_combis:
            args = match.split('.')
            property_index = property_names.index(args[0])
            property_value_index = property_values[property_index].index(args[1])
        else:
            property_index, property_value_index = None, None
        return property_index, property_value_index

    rules = []
    clue_description = ''
    for line in logic_info:
        change = False
        line = line.strip()
        if line[:1] == '<': # header
            continue
        if line[:1] == '#': # comment. thus igored.
            continue
        match = re.search(r'^clue:\s?(.+)', line)
        if match:
            clue_description = match.group(1)
            continue

        parsed_lines = line.split(' and ')
        join_lines = []
        debug_out = ''

        for parsed_line in parsed_lines:
            change = False
            hit = ''
            debug_out += '{}\n'.format(line, )

            match, args, pi, pvi, pil, pvil, pir, pvir, hit, replacement = True, None, None, None, None, None, None, None, None, None
            # vergelijk actors op basis van waarde-index
            # mark / knutselen / sport.tennis == paardrijden / hobby.punniken
            while match: 
                match = re.search(r'(?<!\.)({})\s?(!=|==)\s?({})'.format('|'.join(actors + non_num_values + prop_combis), '|'.join(non_num_values + prop_combis)), parsed_line)
                if match:
                    operator = match.group(2)
                    # linkerzijde van operator
                    if match.group(1) in actors: # mark
                        left = actors.index(match.group(1))
                        hit = 10
                    else:
                        left = 'opl[{}]@index({})'.format(*p_indices(match.group(1)))
                        hit = 20
                    # rechterzijde van operator
                    right = 'opl[{}]@index({})'.format(*p_indices(match.group(3)))
                    replacement = '{}{}{}'.format(left, operator, right)  
                    parsed_line = parsed_line[:match.span()[0]] + replacement + parsed_line[match.span()[1]:]
                    change=True
                    debug_out += '  {} (CP{})\n    {}\n'.format(match.group(0), hit, replacement, )

            match, args, pi, pvi, pil, pvil, pir, pvir, hit, replacement = True, None, None, None, None, None, None, None, None, None
            # de volgende type clues zijn tbv berekeningen
            # sport.mark / leeftijd.mark
            # resultaat impliceert ALTIJD: de eigenlijke waarden (string / int)
            while match: 
                match = re.search(r'({})\.({})'.format('|'.join(property_names), '|'.join(actors), ), parsed_line)
                if match:
                    pi = property_names.index(match.group(1))
                    ai = actors.index(match.group(2))
                    replacement = 'opl[{}][{}]'.format(pi, ai, )
                    if is_numeriek[pi]:
                        #replacement = 'int({}[{}])'.format(property_values[pi], replacement, )
                        replacement = '{}[{}]'.format( [int(_) for _ in property_values[pi] ], replacement, )
                        hit = 2
                    else: 
                        hit = 1
                    parsed_line = parsed_line[:match.span()[0]] + replacement + parsed_line[match.span()[1]:]
                    change = True
                    debug_out += '  {} (B{})\n    {}\n'.format(match.group(0), hit, replacement, )

            match, args, pi, pvi, pil, pvil, pir, pvir, hit, replacement = True, None, None, None, None, None, None, None, None, None
            # tennis.actor / leeftijd.30.actor
            while match: 
                match = re.search(r'(?<!\.)({})\.actor'.format('|'.join(non_num_values + prop_combis), ), parsed_line)
                if match:
                    replacement = '{}[opl[{}].index({})]'.format(actors, *p_indices(match.group(1)))
                    parsed_line = parsed_line[:match.span()[0]] + replacement + parsed_line[match.span()[1]:]
                    change = True
                    debug_out += '  {} (C)\n    {}\n'.format(match.group(0), replacement, )

            match, args, pi, pvi, pil, pvil, pir, pvir, hit, replacement = True, None, None, None, None, None, None, None, None, None
            while match: # cross_combis
                match = re.search(r'({})'.format('|'.join(cross_combis), ), parsed_line)
                if match: 
                    arguments = match.group(1).split('.')
                    pi = property_names.index(arguments[0])
                    if len(arguments) == 2: # sport.breien welke sport beoefend diegene die breien als hobby heeft ?
                        pi2, pvi2 = divmod(all_values.index(arguments[1]), len(actors))
                        hit = 1
                    else: 
                        pi2 = property_names.index(arguments[1])
                        pvi2 = property_values[pi2].index(arguments[2])
                        hit = 2
                    replacement = '{}[opl[{}][opl[{}]@index({})]]'.format(property_values[pi], pi, pi2, pvi2, )
                    if is_numeriek[pi]:
                        #replacement = 'int({})'.format(replacement, )
                        replacement = '{}[opl[{}][opl[{}]@index({})]]'.format( [ int(_) for _ in property_values[pi] ], pi, pi2, pvi2, )
                        hit += 10
                    parsed_line = parsed_line[:match.span()[0]] + replacement + parsed_line[match.span()[1]:]
                    change = True
                    debug_out += '  {} (E{})\n    {}\n'.format(match.group(0), hit, replacement, )

            match, args, pi, pvi, pil, pvil, pir, pvir, hit, replacement = True, None, None, None, None, None, None, None, None, None
            # leeftijd.tennis / hobby.leeftijd.30
            # wat is de leeftijd van de tennis(er) / wat is de hobbby van de 30-jarige ?
            while match: 
                match = re.search(r'({})\.({})'.format('|'.join(property_names), '|'.join(non_num_values + prop_combis), ), parsed_line)
                if match:
                    # left side of the (first) dot
                    pil = property_names.index(match.group(1))
                    # right side of the (first) dot
                    if match.group(2) in non_num_values: # .tennis
                        pir, pvir = divmod(all_values.index(match.group(2)), len(actors))
                        hit = 1
                    elif match.group(2) in prop_combis: # .leeftijd.30
                        args = match.group(2).split('.')
                        pir = property_names.index(args[0])
                        pvir = property_values[pir].index(args[1])
                        hit = 2
                    replacement = 'opl[{}][opl[{}].index({})]'.format(pil, pir, pvir, )
                    if is_numeriek[pil]:
                        replacement = 'int({}[{}])'.format(property_values[pil], replacement, )
                        hit += 10
                    parsed_line = parsed_line[:match.span()[0]] + replacement + parsed_line[match.span()[1]:]
                    change = True
                    debug_out += '  {} (D{})\n    {}\n'.format(match.group(0), hit, replacement, )

            if not change:
                debug_out += '  niet herkend als clue (!)\n'
            if False:
                print(debug_out)

            if change:
                join_lines.append(parsed_line)

        if len(join_lines) > 0:
            joined_line = ' and '.join(join_lines).replace('@', '.')
            rule = [ clue_description, line, joined_line ]
            rules.append(rule)

    return rules

if __name__ == "__main__":
    script_path = os.path.dirname(os.path.abspath(__file__))
    title, subtitles, actors, property_names, property_values, logic_info, de_oplossing = parse_content(os.path.join(script_path, 'puzzels.txt'))
    deze_puzzel = Logigram(title, subtitles, actors, property_names, property_values, de_oplossing) 
    rules = parse_logic(actors, property_names, property_values, logic_info)
    for rule in rules:
        deze_puzzel.add_clue(rule)
    if False:
        for indx in range(len(deze_puzzel.clues)):
            print(deze_puzzel.clues[indx][0], '\n', deze_puzzel.clues[indx][1], '\n', deze_puzzel.clues[indx][2], '\n')
    deze_puzzel.los_op()
    print(deze_puzzel)

    if len(deze_puzzel.oplossingen) != 1:
        if deze_puzzel.de_oplossing is not None:
            print('Analyse van de aanwijzingen voor de opgegeven oplossing')
            for clue_index in range(len(deze_puzzel.clues)):
                deze_puzzel.test_clues(clue_index)
            print('\nopgegeven oplossing:')
            print(deze_puzzel.tabelleer_oplossing(deze_puzzel.de_oplossing))