Python code bij Schildpadden redden

Je kunt de code ook als Jupyter notebook downloaden

 

"""Python programma bij "Een wiskundige doet de afwas 16 -- Schildpadden redden: analyse"
    Pythagoras, 63ste jaargang, nummer 4, februari 2023

    Auteur: Tom Verhoeff, TU Eindhoven

    In Python >= 3.9, `@lru_cache(maxsize=None)` kan vervangen worden door `@cache`.

    De type hints kunnen in Python >= 3.9 in de functie aanhef opgenomen worden.
"""

from functools import lru_cache  # vanaf Python 3.2
# from functools import cache  # vereist Python 3.9
import datetime as dt
from collections import Counter

@lru_cache(maxsize=None)  # onthoud vorige resultaten; gaat sneller maar kost meer geheugen
def analyseer_1(bord):
    # type: (str) -> tuple[str, str]
    """Bepaal uitslag voor eerste speler wanneer die begint met `bord`,
    door alle mogelijkheden na te gaan.  Dit is niet efficiënt
    In geval van winst 'W', geef ook een zet die dat bereikt.
    Verlies wordt aangegeven met 'X'.
    De aanname is dat beide spelers optimaal spelen.
    """
    if 'X' not in bord:
        return 'X', ''

    beste_uitslag, beste_zet = 'X', ''  # voor als we niets beters vinden
    mutable_bord = list(bord)

    for i, schildpad in enumerate(bord):
        if schildpad == 'X':
            mutable_bord[i] = 'O'
            for j in range(-1, i):
                if j >= 0:
                    mutable_bord[j] = 'O' if bord[j] == 'X' else 'X'
                uitslag, zet = analyseer_1(''.join(mutable_bord))
                if j >= 0:
                    mutable_bord[j] = bord[j]
                if uitslag == 'X':  # 2e speler verliest, hoe dan ook; 1e speler kan dus winnen
                    return 'W', f"{i + 1} {j + 1 if j >= 0 else ''}"

            mutable_bord[i] = 'X'

    return beste_uitslag, beste_zet


def maak_tabel(n_max, analyseer):
    # type: (int, Callable[[str], tuple[str, str]]) -> None
    """"Druk tabel af met uitslagen voor borden met alleen X t/m n_max.
    """
    start_alle = dt.datetime.now()

    for n in range(n_max+1):
        print(f"{n:2}", end=': ')
        if hasattr(analyseer, 'cache_clear'):
            analyseer.cache_clear()  # type: ignore
        start = dt.datetime.now()
        print(analyseer(n * 'X'), dt.datetime.now() - start, end=' ')
        if hasattr(analyseer, 'cache_info'):
            print(analyseer.cache_info())  # type: ignore
        else:
            print()

    print("Totale looptijd:", dt.datetime.now() - start_alle)


HARD_OP = True  # Laat wel/niet  extra informatie afdrukken


def analyseer_2(bord):
    # type: (list[int]) -> tuple[str, str]
    """Bepaal uitslag voor eerste speler wanneer die begint met `bord`,
    via het algoritme van Pi, door Schildpadden redden te zien als Nim spel.
    In geval van winst 'W', geef ook een zet die dat bereikt.
    Verlies wordt aangegeven met 'X'.
    De aanname is dat beide spelers optimaal spelen.
    """
    def splits(n):
        # type: (int) -> list[int]
        """Splits getal in verschillende groepjes via herhaald samenvoegen van gelijke.
        """
        g = 1  # grootte van de n nog losse groepjes
        groepjes = []  # bekende groepjes die uniek zijn
        # invariant: oorspronkelijke n == sum(groepjes) + n * g

        while n > 0:
            if n % 2 == 1:  # dan is er een groepje dat niet meer samengevoegd kan worden
                groepjes.append(g)
                n = n - 1
            g, n = 2 * g, n // 2

        return groepjes

    def splits_alle(bord):
        # type: (list[int]) -> list[list[int]]
        """Splits alle posities van schildpadden die op hun rug liggen.
        """
        result = []  # type: list[list[int]]

        for i, schildpad in enumerate(bord, 1):
            if schildpad == 'X':
                result.append(splits(i))

        return result

    def nim_som(alle_groepjes):
        # type: (list[list[int]]) -> list[int]
        """Bepaal alle groepjes die een oneven aantal keer voorkomen.
        """
        counts = Counter(groepje for groepjes in alle_groepjes for groepje in groepjes)
        return [groepje for groepje, count in counts.items() if count % 2 == 1]

    alle_groepjes = splits_alle(bord)
    ns = nim_som(alle_groepjes)
    if HARD_OP:
        print("bord:", bord)
        print("gesplitst:", alle_groepjes)
        print("nim som (groepjes die oneven aantal keer voorkomen):", ns)
    if not ns:
        if HARD_OP:
            print("elk groepje komt even aantal keer voor")
        return 'X', ''
    m = max(ns)
    # vind schildpadden waar m als groepje in voorkomt
    grootste = [sum(groepjes) for groepjes in alle_groepjes if m in groepjes]
    if HARD_OP:
        print("grootste groepje dat oneven aantal keer voorkomt:", m)
        print("posities van schildpadden met grootste oneven groepje:", grootste)
    eerste = grootste[0]
    tweede = sum(nim_som([ns, splits(eerste)]))
    return 'W', f"{grootste[0]} {tweede if tweede > 0 else ''}"


def winnende_zet(stapels):
    # type: (list[int]) -> tuple[int, int]
    """Bepaal zet om te winnen bij Nim spel, indien mogelijk, en anders None,
    via het algoritme van Phi.
    Een zet is een tupel van een stapelnummer en aantal achtergelaten
    """
    s = sum(stapels)
    if s == 0:
        return None
    else:
        halve_stapels = [stapel // 2 for stapel in stapels]
        halve_zet = winnende_zet(halve_stapels)
        if halve_zet:  # niet None
            i, a = halve_zet
            # verdubbel deze zet en maak som even
            return i, 2 * a + (s - stapels[i - 1]) % 2
        else:
            if s % 2:  # oneven som: kies oneven stapel
                for i, stapel in enumerate(stapels, 1):
                    if stapel % 2:
                        # neem 1; dat maakt de som even en laat halve_stapels invariant
                        return i, stapels[i - 1] - 1
            else:
                return None  # label D


def analyseer_3(bord):
    # type: (str) -> typle[str, str]
    """Bepaal uitslag voor eerste speler wanneer die begint met `bord`,
    via het recursieve algoritme van Phi (zie functie `winnende_zet`).
    In geval van winst 'W', geef ook een zet die dat bereikt.
    Verlies wordt aangegeven met 'X'.
    De aanname is dat beide spelers optimaal spelen.
    """
    # Bepaal Nim spel bij het bord voor Schildpadden redden
    stapels = [positie
               for positie, schildpad in enumerate(bord, 1)
               if schildpad == 'X'
              ]
    zet = winnende_zet(stapels)
    if zet:  # niet None
        i, a = zet
        return 'W', f"{stapels[i - 1]} {a if a > 0 else ''}"
    else:
        return 'X'


def interactief(n, analyseer):
    # type: (int, Callable[[str], tuple[str, str]]) -> None
    """Laat interactief een spel spelen via een opgegeven analyse functie.
    Toon ook analyse resultaat (best haalbare uitslag).
    """
    assert 1 <= n <= 16, f"Aantal {n} ligt niet tussen 1 en 16"

    mutable_bord = n * ['X']
    zet_nr = 0
    bord = ''.join(mutable_bord)
    print(f"zet_nr: zet | analyseer({bord!r}) -> {analyseer(bord)}")

    while True:
        zet_nr += 1

        while True:
            zet = input(f"Zet {zet_nr} (index [index])? ")
            posities = zet.split()
            if not (1 <= len(posities) <= 2):
                print(f"Een of twee posities nodig, gescheiden door spatie")
                continue
            try:
                posities = [int(positie) for positie in posities]
            except ValueError:
                print(f"Posities moeten getallen zijn")
                continue
            i = posities[0] - 1
            if len(posities) == 2:
                j = posities[1] - 1
            if not (0 <= i < len(bord)):
                print(f"Eerste positie {posities[0]} valt buiten het bord")
                continue
            if bord[i] != 'X':
                print(f"Schildpad op eerste positie ligt niet op zijn rug")
                continue
            if len(posities) == 2 and not (0 <= j < i):
                print(f"Tweede positie ligt niet op het bord links van eerste positie")
                continue
            break

        mutable_bord[i] = 'O'
        if len(posities) == 2:
            mutable_bord[j] = 'O' if mutable_bord[j] == 'X' else 'X'
        bord = ''.join(mutable_bord)
        print(f"{zet_nr:6}: {zet:3} | analyseer({bord!r}) -> {analyseer(bord)}")

        if "X" not in bord:
            print(f"{2 - zet_nr % 2}e speler wint")
            break


if __name__ == '__main__':
    # Kies via "uitcommentariëren" welke functie je wilt aanroepen

    # 1
    # maak_tabel(16, analyseer_1)  # kost ca. 2 seconde (op mijn laptop)

    # 2
#     print(analyseer_2("XXXXXX"))

    # 3
    # HARD_OP = False
    maak_tabel(16, analyseer_2)  # kost fractie van seconde (op mijn laptop)
    maak_tabel(16, analyseer_3)  # kost fractie van seconde (op mijn laptop)

    # 4
    HARD_OP = False
    interactief(8, analyseer_3)