Spring naar hoofdtekst

Tectonic/Suguru puzzels in PHP

Geplaatst op door .
Laatste aanpassing op .

Inleiding

Puzzelen kan bij iemands leven horen. Met een pen en tijdschrift, TV-gids of puzzelboekje op schoot vliegt de tijd voorbij. Maar niet alleen als puzzel-consument is het goed toeven met breinbrekers op papier; als software-ontwikkelaar gaan ook de hersens aan de slag: hoe zou ik dit proces kunnen automatiseren? Hoe giet ik deze werkwijze in code?

Als eerste waagde ik me aan een Sudoku-oplosser, maar deze heeft het nooit veel verder geschopt dan een kanttekening in mijn lokale Git-historie. Daarna volgde begin dit jaar een Binaire oplosser met bijbehorend artikel in dit weblog. Op zoek naar afwisseling vonden mijn vrouw en ik een boekje van Denksport genaamd Tectonic: een soort Sudoku, maar dan anders. De oplosser die ik in dit artikel beschrijf is online beschikbaar.

Spelregels

Een Tectonic-puzzel, ook wel Suguru genoemd, bestaat uit een rechthoekig vlak met cellen. Deze zijn gegroepeerd in puzzelstukken van 1 tot maximaal 5 cellen. In elk puzzelstuk mag een cijfer slechts één keer voorkomen. Dezelfde cijfers mogen elkaar niet raken; ook niet diagonaal.

Opgave

Op papier is een Tectonic opgebouwd uit een legpuzzel van cellen met een dunne rand, die elk deel uitmaken van een (eventueel onregelmatig) puzzelstuk met een dikke rand. Hierin zijn dan één of meer cellen vooringevuld. De eerste uitdaging was een formaat te bedenken waarin ik deze informatie kon opslaan en verwerken.

Met het oog op de toekomst en eventuele andere applicaties die met dezelfde data aan de slag zouden kunnen, viel mijn keuze al snel op XML. Met een bijbehorend XSD-schema kon ik vooruit vastleggen hoe de structuur moest zijn en er in mijn code op vertrouwen dat nooit van deze opbouw zou worden afgeweken.

<?xml version="1.0" encoding="UTF-8"?>
<Puzzles xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:noNamespaceSchemaLocation="TectonicPuzzles.xsd">

  <Puzzle height="5" width="3">
    <Cell PieceNumber="1" Value="" />
    <Cell PieceNumber="1" Value="" />
    <Cell PieceNumber="2" Value="" />

    <Cell PieceNumber="3" Value="" />
    <Cell PieceNumber="1" Value="3" />
    <Cell PieceNumber="2" Value="" />

    <Cell PieceNumber="3" Value="" />
    <Cell PieceNumber="1" Value="" />
    <Cell PieceNumber="1" Value="2" />

    <Cell PieceNumber="3" Value="" />
    <Cell PieceNumber="3" Value="" />
    <Cell PieceNumber="4" Value="" />

    <Cell PieceNumber="5" Value="" />
    <Cell PieceNumber="4" Value="" />
    <Cell PieceNumber="4" Value="1" />
  </Puzzle>

</Puzzles>

Het bijpassende schema ziet er relatief eenvoudig uit:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="Puzzles" type="PuzzlesType" />

  <xs:complexType name="PuzzlesType">
    <xs:sequence minOccurs="1" maxOccurs="1">
      <xs:element name="Puzzle" type="PuzzleType" />
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="PuzzleType">
    <xs:sequence minOccurs="1" maxOccurs="unbounded">
      <xs:element name="Cell" type="CellType" />
    </xs:sequence>

    <xs:attribute name="width" type="xs:nonNegativeInteger" use="required" />
    <xs:attribute name="height" type="xs:nonNegativeInteger" use="required" />
  </xs:complexType>

  <xs:complexType name="CellType">
    <xs:attribute name="PieceNumber" type="xs:nonNegativeInteger" />
    <xs:attribute name="Value" type="EmptyValue" />
  </xs:complexType>

  <xs:simpleType name="EmptyValue">
    <xs:restriction base="xs:string">
      <xs:pattern value="[1-5]?" />
    </xs:restriction>
  </xs:simpleType>

</xs:schema>

Opbouw

Het Puzzle-object heeft een verzameling PuzzleCell-objecten ($puzzle->_cells). De index van zo'n cel wordt van linksboven naar rechts, dan naar beneden bepaald. Elke cel is met referenties gekoppeld aan één PuzzlePiece-object ($cell->piece). Ook heeft elke cel maximaal acht referenties naar de naburige cellen ($cell->neighbourCells).

Aanpak

Net zoals op papier, baseert mijn aanpak op het achtereenvolgens wegstrepen van mogelijkheden in een cel; totdat er nog maar één mogelijkheid overblijft. Dus begint elke cel in eerste instantie met net zoveel mogelijkheden als er cellen zijn in dat betreffende puzzelstuk. Vooringevulde cellen worden daarbij overgeslagen.

foreach ($this->_cells as $ix => $c) {
    if (strlen($c->value) == 1) {
        continue;
    }
    $c->value = implode('', range(1, count($c->piece->cells)));
}

Daarna worden de naburige cellen van elke cel verzameld.

Code in/uitklappen
private function _getRowIndex(int $cellIndex) : int
{
    if (array_key_exists($cellIndex, $this->_cells)) {
        return intdiv($cellIndex, $this->_width);
    }
    return -1;
}

/**
 * Hook up all cell's neighbours as an array of referenced cells
 * @return void
 */
private function _hookupNeighbours() : void
{
    foreach ($this->_cells as $ix => $c) {

        // Loop through all cells to find neighbour cells
        $rowIndex = $this->_getRowIndex($ix);

        // top left
        $neighbourIx = $ix - 1 - $this->_width;
        $neighbourRowIndex = $this->_getRowIndex($neighbourIx);
        if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex - 1) {
            $c->neighbourCells[0] = $this->_cells[$neighbourIx];
        }

        // top
        $neighbourIx = $ix - $this->_width;
        $neighbourRowIndex = $this->_getRowIndex($neighbourIx);
        if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex - 1) {
            $c->neighbourCells[1] = $this->_cells[$neighbourIx];
        }

        // top right
        $neighbourIx = $ix + 1 - $this->_width;
        $neighbourRowIndex = $this->_getRowIndex($neighbourIx);
        if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex - 1) {
            $c->neighbourCells[2] = $this->_cells[$neighbourIx];
        }

        // right
        $neighbourIx = $ix + 1;
        $neighbourRowIndex = $this->_getRowIndex($neighbourIx);
        if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex) {
            $c->neighbourCells[5] = $this->_cells[$neighbourIx];
        }

        // bottom right
        $neighbourIx = $ix + 1 + $this->_width;
        $neighbourRowIndex = $this->_getRowIndex($neighbourIx);
        if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex + 1) {
            $c->neighbourCells[8] = $this->_cells[$neighbourIx];
        }

        // bottom
        $neighbourIx = $ix + $this->_width;
        $neighbourRowIndex = $this->_getRowIndex($neighbourIx);
        if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex + 1) {
            $c->neighbourCells[7] = $this->_cells[$neighbourIx];
        }

        // bottom left
        $neighbourIx = $ix - 1 + $this->_width;
        $neighbourRowIndex = $this->_getRowIndex($neighbourIx);
        if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex + 1) {
            $c->neighbourCells[6] = $this->_cells[$neighbourIx];
        }

        // left
        $neighbourIx = $ix - 1;
        $neighbourRowIndex = $this->_getRowIndex($neighbourIx);
        if ($neighbourRowIndex != -1 && $neighbourRowIndex == $rowIndex) {
            $c->neighbourCells[3] = $this->_cells[$neighbourIx];
        }
    }
}

Oplossen: stap voor stap

De kern van de oplosser is de methode solve(). Met een do-while-loop wordt net zolang geprobeerd mogelijkheden weg te strepen, totdat de status van de cellen niet meer verandert (strategieën 1 t/m 8). Dan wordt bij één van de cellen, met slechts twee mogelijkheden, geprobeerd of de eerste optie tot een geldige oplossing leidt (strategie 9). Zo nee, wordt de tweede optie ingevuld.

Code in/uitklappen
/**
 * Tries to solve the puzzle
 * @param int $depth the current recursion depth
 * @return bool TRUE on success, FALSE on failure
 */
public function solve(int $depth = 0) : bool
{
    if ($depth > 9) {
        // Emergency break for stopping recursion
        return false;
    }
    do {
        $previousHTML = $this->toHTML();

        $this->_solveStrategy1();
        $this->_solveStrategy2();
        $this->_solveStrategy3();
        $this->_solveStrategy4();
        $this->_solveStrategy5();
        $this->_solveStrategy6();
        $this->_solveStrategy7();
        $this->_solveStrategy8();

    } while ($this->_isValid() && $this->toHTML() != $previousHTML);

    if ($this->_isComplete() && $this->_isValid()) {
        return true;
    }
    return $this->_solveStrategy9($depth);
}
/**
 * Whether the puzzle contains no unsure values
 * @return bool
 */
private function _isComplete() : bool
{
    $cellsWithSingleValue = array_filter(
        $this->_cells,
        function ($c) {
            return strlen($c->value) == 1;
        }
    );
    return count($cellsWithSingleValue) == count($this->_cells);
}
/**
 * Whether this puzzle's solution is valid
 * @return bool
 */
private function _isValid() : bool
{
    foreach ($this->_cells as $c) {
        if (empty($c->value)) {
            return false;
        }
        if (strlen($c->value) > 1) {
            continue;
        }
        $neighboursWithSameValue = array_filter(
            $c->neighbourCells,
            function ($c2) use ($c) {
                return $c2->value == $c->value;
            }
        );
        if ($neighboursWithSameValue) {
            return false;
        }
    }
    foreach ($this->_pieces as $p) {
        for ($i = 1; $i <= count($p->cells); $i++) {
            $cellsWithThisOption = array_filter(
                $p->cells,
                function ($c) use ($i) {
                    return $c->value == (string)$i;
                }
            );
            if (count($cellsWithThisOption) > 1) {
                return false;
            }
        }    
    }
    return true;
}

Wegstrepen

/**
 * Removes a specific option from the cell's value
 * @param int|string $v the single value to remove
 * @return bool TRUE when the value was present before being removed
 */
public function removeValue($v) : bool
{
    $v = (string)$v;
    if ($vIsPresent = (strpos($this->value, $v) !== false)) {
        $this->value = str_replace($v, '', $this->value);
    }
    return $vIsPresent;
}

Strategie 1

/**
 * Remove solved cells' values from current cells' options
 * @return void
 */
private function _solveStrategy1() : void
{
    foreach ($this->_pieces as $p) {
        foreach ($p->cells as $c) {
            if (strlen($c->value) > 1) {
                continue;
            }
            foreach ($p->cells as $c2) {
                if ($c2 === $c) {
                    continue;
                }
                if (strlen($c2->value) == 1) {
                    continue;
                }
                $c2->removeValue($c->value);
            }
        }
    }
    return;
}

Strategie 2

/**
 * Remove solved cells' values from neighbour cells
 * @return void
 */
private function _solveStrategy2() : void
{
    foreach ($this->_cells as $ix => $c) {
        if (strlen($c->value) == 1) {
            foreach ($c->neighbourCells as $c2) {
                $c2->removeValue($c->value);
            }
        }
    }
    return;
}

Strategie 3

/**
 * Set cell's value, if no other cell in that piece has that value
 * @return void
 */
private function _solveStrategy3() : void
{
    foreach ($this->_pieces as $p) {
        $cellCountThisPiece = count($p->cells);
        for ($i = 1; $i <= $cellCountThisPiece; $i++) {
            $cellsWithThisOption = array_filter(
                $p->cells,
                function ($c) use ($i) {
                    return strpos($c->value, (string)$i) !== false;
                }
            );
            if (count($cellsWithThisOption) == 1) {
                $c = array_shift($cellsWithThisOption);

                if (strlen($c->value) > 1) {
                    $c->setValue($i);
                }
            }
        }
    }
    return;
}

Buren

Elke cel heeft drie tot maximaal acht naburige cellen. In een aantal strategiën worden deze buren gebruikt om mogelijkheden weg te strepen. Zie de methode _hookupNeighbours() en getNeighbour():

/**
 * Gets the cell's neighbour at the specified index
 * Neighbours are indexed in the following order:
 *  0 1 2
 *  3 X 5
 *  6 7 8
 * where X is the current cell
 * @param int $ix the neighbour's 0-based index
 * @return PuzzleCell|NULL
 */
public function getNeighbour(int $ix) : ?PuzzleCell
{
    if (array_key_exists($ix, $this->neighbourCells)) {
        return $this->neighbourCells[$ix];
    }
    return null;
}

Strategie 4

/**
 * Removes possibility from cell if certain neighbours have
 * two same possibilities
 * @return void
 */
private function _solveStrategy4() : void
{
    foreach ($this->_cells as $ix => $c) {
        if (strlen($c->value) != 2) {
            continue;
        }
        // ...  Current cell X is north-west partner
        // .XA  or south-east partner (being Y)
        // .BY
        $neighY = $c->getNeighbour(8);
        $neighA = $c->getNeighbour(5);
        $neighB = $c->getNeighbour(7);
        $this->_removeFromTwo($c, $neighY, $neighA, $neighB);

        // ...  Current cell X is north-east partner
        // AX.  or south-west partner (being Y)
        // YB.
        $neighY = $c->getNeighbour(6);
        $neighA = $c->getNeighbour(3);
        $neighB = $c->getNeighbour(7);
        $this->_removeFromTwo($c, $neighY, $neighA, $neighB);
    }
    return;
}

Strategie 5

/**
 * Removes possibility from cell if certain neighbours have
 * two same possibilities
 * @return void
 */
private function _solveStrategy5() : void
{
    foreach ($this->_cells as $ix => $c) {

        // .AB Current cell X is horizontal partner
        // .XY
        // .CD
        $cY = $c->getNeighbour(5);
        $cA = $c->getNeighbour(1);
        $cB = $c->getNeighbour(2);
        $cC = $c->getNeighbour(7);
        $cD = $c->getNeighbour(8);
        $this->_removeFromFour($c, $cY, $cA, $cB, $cC, $cD);

        // ... Current cell X is vertical partner
        // AXC
        // BYD
        $cY = $c->getNeighbour(7);
        $cA = $c->getNeighbour(3);
        $cB = $c->getNeighbour(6);
        $cC = $c->getNeighbour(5);
        $cD = $c->getNeighbour(8);
        $this->_removeFromFour($c, $cY, $cA, $cB, $cC, $cD);
    }
    return;
}

Strategie 6

/**
 * Removes paired possibilities from piece's cells if two other cells match
 * @return void
 */
private function _solveStrategy6() : void
{
    foreach ($this->_pieces as $nr => $p) {
        foreach ($p->cells as $c) {
            if (strlen($c->value) != 2) {
                continue;
            }
            $partner = array_filter(
                $p->cells,
                function ($c2) use ($c) {
                    return ($c2 !== $c && $c2->value === $c->value);
                }
            );
            if (!$partner) {
                continue;
            }
            $partner = array_shift($partner);
            foreach ($p->cells as $c2) {
                if ($c2 === $c) {
                    continue;
                }
                if ($c2 === $partner) {
                    continue;
                }
                foreach (str_split($c->value) as $v) {
                    $c2->removeValue($v);
                }
            }
        }
    }
    return;
}

Strategie 7

/**
 * Removes possibility from cell if certain neighbours have
 * three same possibilities 
 * @return void
 */
private function _solveStrategy7() : void
{
    foreach ($this->_cells as $ix => $c) {
        // ... Current cell is top left
        // .XA
        // .BC
        $neighA = $c->getNeighbour(5);
        $neighB = $c->getNeighbour(7);
        $neighC = $c->getNeighbour(8);
        $this->_removeFromThree($c, $neighA, $neighB, $neighC);

        // ... Current cell is top right
        // AX.
        // BC.
        $neighA = $c->getNeighbour(3);
        $neighB = $c->getNeighbour(6);
        $neighC = $c->getNeighbour(7);
        $this->_removeFromThree($c, $neighA, $neighB, $neighC);

        // .AB Current cell is bottom left
        // .XC
        // ...
        $neighA = $c->getNeighbour(1);
        $neighB = $c->getNeighbour(2);
        $neighC = $c->getNeighbour(5);
        $this->_removeFromThree($c, $neighA, $neighB, $neighC);

        // AB. Current cell is bottom right
        // CX.
        // ...
        $neighA = $c->getNeighbour(0);
        $neighB = $c->getNeighbour(1);
        $neighC = $c->getNeighbour(3);
        $this->_removeFromThree($c, $neighA, $neighB, $neighC);
    }
    return;
}

Strategie 8

/**
 * Removes possibility from cell if certain neighbours have
 * three same possibilities
 * @return void
 */
private function _solveStrategy8() : void
{
    foreach ($this->_cells as $ix => $c) {
        // ..A Current cell is left of stack of three
        // .XB
        // ..C
        $neighA = $c->getNeighbour(2);
        $neighB = $c->getNeighbour(5);
        $neighC = $c->getNeighbour(8);
        $this->_removeFromThree($c, $neighA, $neighB, $neighC);

        // A.. Current cell is right of stack of three
        // BX.
        // C..
        $neighA = $c->getNeighbour(0);
        $neighB = $c->getNeighbour(3);
        $neighC = $c->getNeighbour(6);
        $this->_removeFromThree($c, $neighA, $neighB, $neighC);

        // ... Current cell is on top of row of three
        // .X.
        // ABC
        $neighA = $c->getNeighbour(6);
        $neighB = $c->getNeighbour(7);
        $neighC = $c->getNeighbour(8);
        $this->_removeFromThree($c, $neighA, $neighB, $neighC);

        // ABC Current cell is under row of three
        // .X.
        // ...
        $neighA = $c->getNeighbour(0);
        $neighB = $c->getNeighbour(1);
        $neighC = $c->getNeighbour(2);
        $this->_removeFromThree($c, $neighA, $neighB, $neighC);
    }
    return;
}

Hulpjes

Onderstaande functies worden vanuit stategiën 4, 5, 7 en 8 aangeroepen:

Code in/uitklappen
/**
 * Removes values from cell enclosed by or adjacent to two same-value cells
 * @param PuzzleCell $x current cell
 * @param PuzzleCell $y current cell's partner
 * @param PuzzleCell $a first cell to be stripped of current cell's values
 * @param PuzzleCell $b second cell to be stripped of current cell's values
 * @return void
 */
private function _removeFromTwo(
    ?PuzzleCell $x, ?PuzzleCell $y, ?PuzzleCell $a, ?PuzzleCell $b
) : void {
    if ($x && $y && $a && $b && $y->value == $x->value) {
        foreach (str_split($x->value) as $v) {
            $a->removeValue($v);
            $b->removeValue($v);
        }
    }
    return;
}
/**
 * Removes values from cell enclosed by a three-cell piece or adjacent to
 * three same-value cells
 * @param PuzzleCell $x the current cell to remove values from
 * @param PuzzleCell $a the first neighbouring cell
 * @param PuzzleCell $b the second neighbouring cell
 * @param PuzzleCell $c the third neighbouring cell
 * @return void
 */
private function _removeFromThree(
    PuzzleCell $x, ?PuzzleCell $a, ?PuzzleCell $b, ?PuzzleCell $c
) : void {

    if ($a && $b && $c
        && $a->piece === $b->piece
        && $b->piece === $c->piece
        && $x->piece !== $a->piece
    ) {
        for ($i = 1; $i <= self::MAXVALUE; $i++) {
            if (strpos($a->value, (string)$i) === false
                || strpos($b->value, (string)$i) === false
                || strpos($c->value, (string)$i) === false
                || strpos($x->value, (string)$i) === false
            ) {
                continue;
            }
            $otherCellsWithThisOption = array_filter(
                $a->piece->cells,
                function ($c2) use ($a, $b, $c, $i) {
                    return
                        $c2 !== $a
                        && $c2 !== $b
                        && $c2 !== $c
                        && strpos($c2->value, (string)$i) !== false;
                }
            );
            if (!$otherCellsWithThisOption) {
                $x->removeValue($i);
            }
        }
    }
    return;
}
/**
 * Removes values from cell adjacent to two same-value cells
 * @param PuzzleCell $x current cell
 * @param PuzzleCell $y current cell's partner
 * @param PuzzleCell $a first cell to be stripped of current cell's values
 * @param PuzzleCell $b second cell to be stripped of current cell's values
 * @param PuzzleCell $c third cell to be stripped of current cell's values
 * @param PuzzleCell $d fourth cell to be stripped of current cell's values
 * @return void
 */
private function _removeFromFour(
    ?PuzzleCell $x, ?PuzzleCell $y,
    ?PuzzleCell $a, ?PuzzleCell $b, ?PuzzleCell $c, ?PuzzleCell $d
) : void {

    if (!$x || !$y) {
        return;
    }
    $this->_removeFromFourTwo($x, $y, $a, $b);
    $this->_removeFromFourTwo($x, $y, $c, $d);
}
/**
 * Removes values from cell adjacent to two same-value cells
 * @param PuzzleCell $x current cell
 * @param PuzzleCell $y current cell's partner
 * @param PuzzleCell $m first cell to be stripped of current cell's values
 * @param PuzzleCell $n second cell to be stripped of current cell's values
 * @return void
 */
private function _removeFromFourTwo(
    PuzzleCell $x, PuzzleCell $y, ?PuzzleCell $m, ?PuzzleCell $n
) : void {
    if (!$m || !$n) {
        return;
    }

    if ($x->piece === $y->piece
        && ($m->piece !== $x->piece || $n->piece !== $x->piece)
    ) {
        for ($i = 1; $i <= self::MAXVALUE; $i++) {
            if (strpos($x->value, (string)$i) !== false
                && strpos($y->value, (string)$i) !== false
            ) {
                $otherCellsWithThisValue = array_filter(
                    $x->piece->cells,
                    function ($c) use ($x, $y, $i) {
                        return
                            $c !== $x
                            && $c !== $y
                            && strpos($c->value, (string)$i) !== false;
                    }
                );

                if (!$otherCellsWithThisValue) {
                    if ($m->piece !== $x->piece) {
                        $m->removeValue($i);
                    }
                    if ($n->piece !== $x->piece) {
                        $n->removeValue($i);
                    }
                }
            }
        }
    }

    if ($y->value == $x->value && strlen($x->value) == 2) {
        foreach (str_split($x->value) as $v) {
            $m->removeValue($v);
            $n->removeValue($v);
        }
    }
}

Recursief proberen

Als het wegstrepen met de acht voorgaande strategiën de oplossing niet dichterbij brengt, komt er met strategie 9 een stukje recursiviteit om de hoek kijken. Hierin roept een stuk code zichzelf tijdens het uitvoeren opnieuw aan.

Er wordt bij de eerste cel met slechts twee opties een tijdelijke kopie van de huidige puzzel gemaakt. Dan wordt de eerste optie gekozen. Kan deze puzzel zo volledig worden ingevuld en opgelost, was de keuze juist. Zo niet, wordt de tweede optie gekozen en daarmee verder gepuzzeld.

/**
 * On cells with two options, try each value in turn
 * @param int $depth the current recursion depth 
 * @return bool
 */
private function _solveStrategy9(int &$depth) : bool
{
    foreach ($this->_cells as $ix => $c) {

        if (strlen($c->value) != 2) {
            continue;
        }
        $valuesToTry = str_split($c->value);

        foreach ($valuesToTry as $v) {
            $thisXML = $this->toXML();
            $tryPuzzle = new Puzzle($thisXML, true);
            $tryPuzzle->_cells[$ix]->value = $v;

            if ($tryPuzzle->solve(++$depth)) {
                $this->_pieces = $tryPuzzle->_pieces;
                $this->_cells = $tryPuzzle->_cells;
                return true;
            }
        }
    }
    return false;
}

Nawoord

Vanwege de leesbaarheid heb ik een aantal details van de hier beschreven code gewijzigd ten opzichte van de originele bron. Ook heb ik de logica van de stappen die leiden naar de uiteindelijke oplossing weggelaten. Deze is niet noodzakelijk voor het begrip van de werking van de oplosser.

Op dit moment is het invoeren van nieuwe puzzels nog een behoorlijk intensief maar vooral visueel stuk handwerk. Voor de toekomst zou ik graag een mogelijkheid zien, om als gebruiker eigen puzzels te kunnen toevoegen / uploaden. Misschien geautomatiseerde beeldherkenning van een gescande puzzel? Of analyse van een screenshot of een live webcam… Ik sta open voor suggesties!

Terug naar boven

Inhoudsopgave

Delen

Met de deel-knop van uw browser, of met onderstaande koppelingen deelt u deze pagina via sociale media of e-mail.

Atom-feed van FWiePs weblog

Artikelen


Categorieën

Doorzoek de onderstaande categorieën om de lijst met artikelen te filteren.


Terug naar boven