Oplossing en python code bij SOS-Spel: analyse

Het antwoord op de laatste vraag:

Net als bij $N = 16$ verliest de beginner.

Dan de code van Pi en Phi (of misschien toch van Tom en Iris?): 

(Je kunt de code ook rechstreeks downloaden als .py bestand)

"""Python programma bij "Een wiskundige doet de afwas 14 -- Het SOS spel: analyse"
    Pythagoras, 63ste jaargang, nummer 2, november 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 typing import Callable


@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`.
    In geval van winst 'W' of remise '=', geef ook een zet die dat bereikt.
    Verlies wordt aangegeven met 'X'.
    De aanname is dat beide spelers optimaal spelen.
    Dit is het eerste programma van Pi (21 regels code + 11 commentaar/leeg).
    """
    if "SOS" in bord:
        return ('X', '')
    elif '-' not in bord:
        return ('=', '')

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

    # als het bord een palindroom is dan hoeven we maar de helft te onderzoeken
    for i, hokje in enumerate(bord[:(len(bord) + 1) // 2] if bord == bord[::-1] else bord):
        if hokje == '-':
            for letter in "OS":
                mutable_bord[i] = letter
                uitslag, zet = analyseer_1(''.join(mutable_bord))
                mutable_bord[i] = '-'
                if uitslag == 'X':  # 2e speler verliest, hoe dan ook; 1e speler kan dus winnen
                    return 'W', f"{letter} {i + 1}"
                elif uitslag == '=':  # 2e speler kan alleen remise houden; 1e speler kan dat dus ook
                    if beste_uitslag == 'X':
                        beste_uitslag, beste_zet = '=', f"{letter} {i + 1}"
                # else: uitslag == 'W', 2e speler kan winnen; 1e speler vermijdt dit liefst

    return beste_uitslag, beste_zet


DIEPTE: int = 0  # tot welke diepte het verloop getoond wordt in analyseer_2


@lru_cache(maxsize=None)
def analyseer_2(bord, diepte=0):
    # type: (str, int) -> tuple[str, str]
    """Tweede programma, na advies van Phi.
       Kan ook het verloop laten zien.
    """
    indent = diepte * '  '
    if diepte < DIEPTE:
        print(f"{indent}analyseer({bord!r})")
    if "SOS" in bord:
        return ('X', '')
    elif '-' not in bord:
        return ('=', '')
    # optimalisaties van Phi (zijn niet nodig, maar maken het wel veel sneller)
    elif "SO-" in bord:
        return ('W', 'S bij SO-')
    elif "S-S" in bord:
        return ('W', 'O bij S-S')
    elif "-OS" in bord:
        return ('W', 'S bij -OS')
    elif "S--S" in bord:
        if bord.count('-') % 2 == 0:  # je moet een keer als 1e tussen S--S zetten
            return ('X', '')
        else:  # je kunt nog ergens buiten S--S zetten
            return ('W', 'buiten S--S')

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

    for i, hokje in enumerate(bord[:(len(bord) + 1) // 2] if bord == bord[::-1] else bord):
        if hokje == '-':
            for letter in "OS":
                if diepte < DIEPTE:
                    print(f"{indent}zet {letter} {i + 1}")
                mutable_bord[i] = letter
                uitslag, zet = analyseer_2(''.join(mutable_bord), diepte + 1)
                if diepte < DIEPTE:
                    print(f"{indent}  -> {uitslag} met zet {zet}")
                mutable_bord[i] = '-'
                if uitslag == 'X':  # 2e speler verliest, hoe dan ook; 1e speler kan dus winnen
                    return 'W', f"{letter} {i + 1}"
                elif uitslag == '=':  # 2e speler kan alleen remise houden; 1e speler kan dat dus ook
                    if beste_uitslag == 'X':
                        beste_uitslag, beste_zet = '=', f"{letter} {i + 1}"

    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 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 * '-'), 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)


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

    mutable_bord = n * ['-']
    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} (letter, index)? ")
            if len(zet) < 2:
                print(f"Zet {zet!r} bevat niet genoeg informatie")
                continue
            letter = zet[0].upper()
            if letter not in "OS":
                print(f"Letter {letter!r} is geen O of S (kleine letter mag ook)")
                continue
            try:
                i = int(zet[1:])
            except ValueError:
                print(f"Index {zet[1:]!r} is geen getal")
                continue
            if not (1 <= i <= len(mutable_bord)):
                print(f"Index {i} ligt niet tussen 1 en {len(mutable_bord)}")
                continue
            if mutable_bord[i - 1] != '-':
                print(f"Hokje {i} bevat al een {mutable_bord[i - 1]}")
                continue
            break

        mutable_bord[i - 1] = letter
        bord = ''.join(mutable_bord)
        print(f"{zet_nr:6}: {zet:3} | analyseer({bord!r}) -> {analyseer(bord)}")

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


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

    # 1
    # maak_tabel(16, analyseer_1)  # kost ca. 5 minuten (op mijn laptop)

    # 2
    maak_tabel(16, analyseer_2)  # kost ca. 1 minuut (op mijn laptop)

    # 3
    # DIEPTE = 2
    # print(analyseer_2(7 * '-'))

    # 4
    # DIEPTE = 0
    # interactief(7, analyseer_2)