Het spelletje "∞ lus" wordt gespeeld met $$m \times n$$ tegels, gerangschikt in een $$m \times n$$ rooster met $$m$$ rijen en $$n$$ kolommen. Er zijn in totaal 16 verschillende tegelconfiguraties:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Elke tegel kan aansluitingen hebben naar boven, naar rechts, naar beneden, en naar links. De tegelconfiguraties worden dus gevormd door alle mogelijke combinaties van aansluitingen in deze vier richtingen, inclusief de tegel zonder aansluitingen.

Bij aanvang van het spel wordt het rooster gevuld met een selectie van tegels. De speler kiest vervolgens een tegel, en draait die 90° in wijzerzin of tegenwijzerzin. Als we bijvoorbeeld tegel in wijzerin draaien dan bekomen we tegel , tegel wordt , maar tegel blijft . De speler kan deze procedure eindeloos blijven herhalen.

Het doel van het spel is om gesloten circuits te vormen: elke aansluiting moet connecteren op een aansluiting van de naburige tegel langs dezelfde zijde. Als een tegel bijvoorbeeld een aansluiting naar rechts heeft, dan moet de tegel rechts daarvan in het rooster een aansluiting naar links hebben. Bij wijze van illustratie toont onderstaand videofragment een speler die puzzels uit het spelletje "∞ lus" probeert op te lossen.

Opgave

Een eenvoudige manier om tegels voor te stellen — zonder gebruik te moeten maken van Unicode-karakters — is door gebruik te maken van een bitmasktechniek. Hierbij wordt elke tegel voorgesteld als een natuurlijk getal uit het interval $$[0, 15]$$ door gebruik te maken van de volgende code:

Grafisch ziet dat er als volgt uit:

tegel

Op die manier kan de tegel voorgesteld worden als het getal $$1 + 4 + 8 = 13$$. Als we de binaire voorstelling van het getal bekijken, dan zien we dat

Zo komt 13 overeen met het binaire getal 1101, waaruit we direct kunnen afleiden dat de tegel aansluitingen heeft naar alle richtingen behalve naar rechts.

Tegel

Definieer een klasse Tegel waarmee tegels uit het spelletje "∞ lus" kunnen voorgesteld worden. Bij het aanmaken van een nieuwe tegel (Tegel) zijn er drie mogelijkheden:

Als er een argument wordt doorgegeven dat niet correspondeert met één van de eerste twee mogelijkheden, dan moet een AssertionError opgeworpen worden met de boodschap ongeldige tegel.

Als er een tegel $$t$$ (Tegel) wordt doorgegeven aan de ingebouwde functie int, dan moet die het natuurlijk getal (int) uit het interval $$[0, 15]$$ teruggeven dat correspondeert met tegel $$t$$.

Als er een tegel $$t$$ (Tegel) wordt doorgegeven aan de ingebouwde functie str, dan moet die het Unicode-karakter (str) teruggeven dat correspondeert met tegel $$t$$.

Als er een tegel $$t$$ (Tegel) wordt doorgegeven aan de ingebouwde functie repr, dan moet die een string (str) teruggeven die leest als een Python-expressie waarmee een nieuwe tegel (Tegel) kan aangemaakt worden met dezelfde aansluitingen als tegel $$t$$. In die expressie moet er altijd een Unicode-karakter als argument doorgegeven worden.

Een tegel $$t$$ (Tegel) moet minstens de volgende getters (of eigenschappen) hebben:

Daarnaast moet je op een tegel $$t$$ (Tegel) een methode draai kunnen aanroepen, waaraan een Booleaanse waarde (bool) kan doorgegeven worden (standaardwaarde: True). Als de waarde True wordt doorgegeven, dan moet tegel $$t$$ 90° in wijzerzin gedraaid worden. Anders moet tegel $$t$$ 90° in tegenwijzerzin gedraaid worden. De methode moet een verwijzing naar tegel $$t$$ teruggeven.

Zorg ervoor dat de volgende bewerkingen het corresponderende resultaat opleveren voor tegels $$t$$, $$t_1$$ en $$t_2$$ (Tegel) en natuurlijk getal $$s$$ (int):

bewerking resultaat
$$t_1$$ == $$t_2$$ een Booleaanse waarde (bool) die aangeeft of tegels $$t_1$$ en $$t_2$$ dezelfde aansluitingen hebben
$$t_1$$ != $$t_2$$ een Booleaanse waarde (bool) die aangeeft of tegels $$t_1$$ en $$t_2$$ verschillende aansluitingen hebben
$$t$$ >> $$s$$ een nieuwe tegel (Tegel) die je zou bekomen door tegel $$t$$ $$s$$ keer 90° in wijzerzin te draaien
$$t$$ << $$s$$ een nieuwe tegel (Tegel) die je zou bekomen door tegel $$t$$ $$s$$ keer 90° in tegenwijzerzin te draaien
$$t_1$$ & $$t_2$$ een nieuwe tegel (Tegel) die aansluitingen heeft waar zowel tegel $$t_1$$ als $$t_2$$ aansluitingen hebben
$$t_1$$ | $$t_2$$ een nieuwe tegel (Tegel) die aansluitingen heeft waar tegel $$t_1$$ of $$t_2$$ (of beide) aansluitingen hebben

Geen van deze bewerkingen mag tegels $$t$$, $$t_1$$ of $$t_2$$ wijzigen.

OneindigeLus

Definieer ook nog een klasse OneindigeLus waarmee puzzels van het spelletje "∞ lus" kunnen voorgesteld worden. Bij het aanmaken van een nieuwe puzzel (OneindigeLus) moeten twee argumenten doorgegeven worden: i) een reeks (list of tuple) met natuurlijke getallen (int) uit het interval $$[0, 15]$$ die de tegels in het rooster voorstellen, uitgelezen in de leesrichting (van links naar rechts en van boven naar onder), en ii) een natuurlijk getal (int) dat aangeeft hoeveel kolommen het rooster telt. Als deze argumenten niet corresponderen met een geldig rooster van tegels, dan moet een AssertionError opgeworpen worden met de boodschap ongeldig rooster.

Als er een puzzel $$p$$ (OneindigeLus) wordt doorgegeven aan de ingebouwde functie str, dan moet die een stringvoorstelling (str) teruggeven van het rooster met tegels van puzzel $$p$$. Daarin vormt elke rij een afzonderlijke regel (opgelijst van boven naar onder), en worden de tegels op een rij voorgesteld door hun Unicode-karakter (opgelijst van links naar rechts).

Als er een puzzel $$p$$ (OneindigeLus) wordt doorgegeven aan de ingebouwde functie repr, dan moet die een string (str) teruggeven die leest als een Python-expressie waarmee een nieuwe puzzel (OneindigeLus) kan aangemaakt worden met dezelfde configuratie als puzzel $$p$$. In die expressie moet het eerste argument als een lijst (list) doorgegeven worden.

Op een puzzel $$p$$ (OneindigeLus) moet je minstens de volgende methoden kunnen aanroepen:

Voorbeeld

>>> tegel = Tegel(11)
>>> int(tegel)
11
>>> str(tegel)
'┻'
>>> tegel
Tegel('┻')
>>> tegel.boven
True
>>> tegel.rechts
True
>>> tegel.onder
False
>>> tegel.links
True
>>> tegel.draai()
Tegel('┣')
>>> tegel
Tegel('┣')
>>> tegel.draai().draai()
Tegel('┫')
>>> tegel.draai(False)
Tegel('┳')
>>> tegel.draai(False).draai(False)
Tegel('┻')
>>> tegel >> 3
Tegel('┫')
>>> tegel
Tegel('┻')
>>> tegel << 6
Tegel('┳')
>>> tegel
Tegel('┻')

>>> Tegel('┛') == Tegel('┓')
False
>>> Tegel('┛') != Tegel('┓')
True
>>> Tegel('┛') == Tegel('┓').draai()
True

>>> Tegel('┓') | Tegel('┗')
Tegel('╋')
>>> Tegel('┣') & Tegel('┻')
Tegel('┗')
>>> tegel | Tegel('┫')
Tegel('╋')
>>> tegel
Tegel('┻')
>>> Tegel('┃') & tegel
Tegel('╹')
>>> tegel
Tegel('┻')

>>> puzzel = OneindigeLus([9, 12, 12, 6, 10, 13, 13, 5, 3, 3, 9, 3], 4)
>>> puzzel
OneindigeLus([9, 12, 12, 6, 10, 13, 13, 5, 3, 3, 9, 3], 4)
>>> print(puzzel)
┛┓┓┏
━┫┫┃
┗┗┛┗
>>> puzzel.isopgelost()
False
>>> puzzel.draai(0, 0).draai(0, 0).draai(0, 2, False).draai(0, 3)
OneindigeLus([6, 12, 6, 12, 10, 13, 13, 5, 3, 3, 9, 3], 4)
>>> print(puzzel)
┏┓┏┓
━┫┫┃
┗┗┛┗
>>> puzzel.isopgelost()
False
>>> puzzel.draai(1, 0).draai(1, 1).draai(1, 1)
OneindigeLus([6, 12, 6, 12, 5, 7, 13, 5, 3, 3, 9, 3], 4)
>>> print(puzzel)
┏┓┏┓
┃┣┫┃
┗┗┛┗
>>> puzzel.isopgelost()
False
>>> puzzel.draai(2, 1, False).draai(2, 2).draai(2, 3, False)
OneindigeLus([6, 12, 6, 12, 5, 7, 13, 5, 3, 9, 3, 9], 4)
>>> print(puzzel)
┏┓┏┓
┃┣┫┃
┗┛┗┛
>>> puzzel.isopgelost()
True

Epiloog

In 2023 bouwde Jason Allemann deze oneindige dominolus in LEGO. Vierenzestig dominostenen vallen in een ononderbroken lus, met een robot die recht tegenover de cascade ronddraait om de stenen voortdurend terug rechtop te zetten.

Dit zijn de instructies om alles na te bouwen.