Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Praxisbeispiel: Tic Tac Toe Vibe Coding

Heinrich-Heine-Universität Düsseldorf

Hier ist eine Abfolge von iterativen Vibe Coding Experimenten, also jeweils 1-2 Prompts von einer Version zur nächsten. Etwa bei Version 6 ist eine Komplexitätsschwelle erreicht, bei der unbedarftes Vibe Coding nicht mehr mit billigen Modellen gut gelingt (mit teureren oder stärkerer Vorstrukturierung schon).

Der volle Verlauf ist hier zu finden — Freigabe-Links dieser Art überleben erfahrungsgemäß keine Jahre, deshalb ist der Ausgangs-Prompt zusätzlich in generated-transcript.md archiviert.

Übungsaufgaben dazu:

  • Wählen Sie eine Variante aus, z.B. die letzte (gen7) und versuchen Sie (mit KI-Hilfe) einen Prompt zu rekonstruieren, der ungefähr diesen Code (ähnliche Struktur, gleiche Semantik) generiert ohne dass man mehrfach prompten muss (“one-shot”).

  • Denken Sie sich eine weitere Tic Tac Toe Variante aus und lassen sie diese hinzu vibe coden. Wie prüfen Sie, ob es (gut) geklappt hat?

  • Reproduzieren Sie gen7 mit einem Terminal-Agenten statt mit einem Chat-Fenster (siehe Agentic Coding). Der Ausgangs-Prompt steht in generated-transcript.md. Halten Sie dabei drei Zahlen fest: verbrauchte Zeit, verbrauchte Token (bzw. Kosten, falls Ihr Werkzeug das anzeigt) und Anzahl der Prompts, bis das Ergebnis lief. Vergleichen Sie mit den sieben Iterationen aus diesem Kapitel — und beurteilen Sie nicht nur die Zahlen, sondern auch, ob der entstandene Code lesbarer ist. Ein Agent, der pytest selbst aufrufen kann, hat einen Vorteil, den ein Chat-Fenster nicht hat.

generated-0.py

generated-0.py
from functools import cache

# Permutationen der D8-Gruppe (Identität, 3x Rotation, 4x Spiegelung)
P = [[0,1,2,3,4,5,6,7,8], [6,3,0,7,4,1,8,5,2], [8,7,6,5,4,3,2,1,0], [2,5,8,1,4,7,0,3,6],
     [2,1,0,5,4,3,8,7,6], [6,7,8,3,4,5,0,1,2], [0,3,6,1,4,7,2,5,8], [8,5,2,7,4,1,6,3,0]]
W = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

def norm(b): 
    # Lexikographisches Maximum liefert bis auf Symmetrie eindeutige Normalform (X > O > .)
    return max("".join(b[i] for i in p) for p in P)

def win(b, p): 
    return any(all(b[i] == p for i in w) for w in W)

@cache
def mm(b, p):
    if win(b, 'X'): return 1
    if win(b, 'O'): return -1
    if '.' not in b: return 0
    q = 'O' if p == 'X' else 'X'
    res = [mm(norm(b[:i]+p+b[i+1:]), q) for i in range(9) if b[i] == '.']
    return max(res) if p == 'X' else min(res)

def print_board(b):
    print(f"\n {b[0]} | {b[1]} | {b[2]} \n---+---+---\n {b[3]} | {b[4]} | {b[5]} \n---+---+---\n {b[6]} | {b[7]} | {b[8]} \n")

b = "........."
while '.' in b and not win(b, 'X') and not win(b, 'O'):
    b = norm(b)
    print_board(b)
    
    if b.count('.') % 2 != 0: # X ist am Zug (Mensch)
        while True:
            try:
                m = int(input("Zug X (0-8): "))
                if 0 <= m <= 8 and b[m] == '.':
                    b = b[:m] + 'X' + b[m+1:]
                    break
            except ValueError: pass
    else: # O ist am Zug (Computer)
        _, m = min((mm(norm(b[:i]+'O'+b[i+1:]), 'X'), i) for i in range(9) if b[i] == '.')
        b = b[:m] + 'O' + b[m+1:]

b = norm(b)
print("Endstand:")
print_board(b)
print("Gewinner:", 'X' if win(b, 'X') else 'O' if win(b, 'O') else 'Unentschieden')
!printf "0\n1\n2\n2\n1\n0\n3\n" | uv run generated-0.py

 . | . | . 
---+---+---
 . | . | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | . | . 
---+---+---
 . | . | . 
---+---+---
 . | . | . 


 X | . | . 
---+---+---
 . | O | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | X | . 
---+---+---
 . | O | . 
---+---+---
 . | . | . 


 X | X | O 
---+---+---
 . | O | . 
---+---+---
 . | . | . 

Zug X (0-8): Zug X (0-8): Zug X (0-8): Zug X (0-8): Zug X (0-8): 
 X | X | O 
---+---+---
 X | O | . 
---+---+---
 . | . | . 

Endstand:

 X | X | O 
---+---+---
 X | O | . 
---+---+---
 O | . | . 

Gewinner: O

generated-1.py

generated-1.py
import deal
from hypothesis import given, strategies as st
from functools import cache

# --- Kernlogik ---
P = [[0,1,2,3,4,5,6,7,8], [6,3,0,7,4,1,8,5,2], [8,7,6,5,4,3,2,1,0], [2,5,8,1,4,7,0,3,6],
     [2,1,0,5,4,3,8,7,6], [6,7,8,3,4,5,0,1,2], [0,3,6,1,4,7,2,5,8], [8,5,2,7,4,1,6,3,0]]
W = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

def norm(b: str) -> str: return max("".join(b[i] for i in p) for p in P)
def win(b: str, p: str) -> bool: return any(all(b[i] == p for i in w) for w in W)

@cache
def mm(b: str, p: str) -> int:
    if win(b, 'X'): return 1
    if win(b, 'O'): return -1
    if '.' not in b: return 0
    q = 'O' if p == 'X' else 'X'
    res = [mm(norm(apply_move(b, p, i)), q) for i in range(9) if b[i] == '.']
    return max(res) if p == 'X' else min(res)

# --- Verträge ---
# --- Verträge ---
@deal.pre(lambda b, p, m: len(b) == 9)
@deal.pre(lambda b, p, m: p in ('X', 'O'))
@deal.pre(lambda b, p, m: 0 <= m <= 8)
@deal.pre(lambda b, p, m: b[m] == '.')
@deal.post(lambda res: len(res) == 9)
def apply_move(b: str, p: str, m: int) -> str:
    return b[:m] + p + b[m+1:]

@deal.pre(lambda b: len(b) == 9)
@deal.pre(lambda b: '.' in b and not win(b, 'X') and not win(b, 'O'))
@deal.post(lambda res: 0 <= res <= 8)
def best_ai_move(b: str) -> int:
    return min((mm(norm(apply_move(b, 'O', i)), 'X'), i) for i in range(9) if b[i] == '.')[1]

# --- Tests ---
valid_boards = st.text(alphabet='XO.', min_size=9, max_size=9).filter(
    lambda b: '.' in b and not win(b, 'X') and not win(b, 'O') and abs(b.count('X') - b.count('O')) <= 1)

@given(valid_boards, st.integers(0, 8))
def test_move_validity(b: str, m: int):
    if b[m] == '.': assert apply_move(b, 'X', m).count('.') == b.count('.') - 1

@given(valid_boards)
def test_ai_optimality(b: str):
    m = best_ai_move(b)
    v_opt = mm(norm(apply_move(b, 'O', m)), 'X')
    assert all(v_opt <= mm(norm(apply_move(b, 'O', i)), 'X') for i in range(9) if b[i] == '.')

# --- Spielschleife ---
if __name__ == '__main__':
    def print_board(b): print(f"\n {b[0]} | {b[1]} | {b[2]} \n---+---+---\n {b[3]} | {b[4]} | {b[5]} \n---+---+---\n {b[6]} | {b[7]} | {b[8]} \n")
    
    b = "........."
    while '.' in b and not win(b, 'X') and not win(b, 'O'):
        b = norm(b)
        print_board(b)
        
        if b.count('.') % 2 != 0:
            try:
                m = int(input("Zug X (0-8): "))
                if 0 <= m <= 8 and b[m] == '.':
                    b = apply_move(b, 'X', m)
            except ValueError: pass
        else:
            b = apply_move(b, 'O', best_ai_move(b))

    b = norm(b)
    print("Endstand:")
    print_board(b)
    print("Gewinner:", 'X' if win(b, 'X') else 'O' if win(b, 'O') else 'Unentschieden')
!uv run crosshair check generated-1.py
!uv run pytest generated-1.py
!printf "0\n1\n2\n2\n1\n0\n3\n" | uv run generated-1.py
============================= test session starts ==============================
platform linux -- Python 3.13.2, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/user/sciebo/hhu/eipy-26/eipy-skript
configfile: pyproject.toml
plugins: cov-5.0.0, hypothesis-6.152.9, jaxtyping-0.3.11, gremlins-1.8.1, anyio-4.13.0, typeguard-4.5.2, Faker-40.23.0, hydra-core-1.3.2, logfire-4.37.0
collected 2 items                                                              

generated-1.py ..                                                        [100%]

============================== 2 passed in 0.50s ===============================

 . | . | . 
---+---+---
 . | . | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | . | . 
---+---+---
 . | . | . 
---+---+---
 . | . | . 


 X | . | . 
---+---+---
 . | O | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | X | . 
---+---+---
 . | O | . 
---+---+---
 . | . | . 


 X | X | O 
---+---+---
 . | O | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | X | O 
---+---+---
 . | O | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | X | O 
---+---+---
 . | O | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | X | O 
---+---+---
 . | O | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | X | O 
---+---+---
 . | O | . 
---+---+---
 . | . | . 

Zug X (0-8): 
 X | X | O 
---+---+---
 X | O | . 
---+---+---
 . | . | . 

Endstand:

 X | X | O 
---+---+---
 X | O | . 
---+---+---
 O | . | . 

Gewinner: O

generated-2.py

generated-2.py
import sys, deal
from hypothesis import given, strategies as st
from functools import cache

# --- Kernlogik & Verträge ---
P = [[0,1,2,3,4,5,6,7,8], [6,3,0,7,4,1,8,5,2], [8,7,6,5,4,3,2,1,0], [2,5,8,1,4,7,0,3,6],
     [2,1,0,5,4,3,8,7,6], [6,7,8,3,4,5,0,1,2], [0,3,6,1,4,7,2,5,8], [8,5,2,7,4,1,6,3,0]]
W = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

def norm(b: str) -> str: return max("".join(b[i] for i in p) for p in P)
def win(b: str, p: str) -> bool: return any(all(b[i] == p for i in w) for w in W)

@deal.pre(lambda b, p, m: len(b) == 9 and p in ('X', 'O') and 0 <= m <= 8 and b[m] == '.')
@deal.post(lambda res: len(res) == 9)
def apply_move(b: str, p: str, m: int) -> str: return b[:m] + p + b[m+1:]

@cache
def mm(b: str, p: str) -> int:
    if win(b, 'X'): return 1
    if win(b, 'O'): return -1
    if '.' not in b: return 0
    q = 'O' if p == 'X' else 'X'
    res = [mm(norm(apply_move(b, p, i)), q) for i in range(9) if b[i] == '.']
    return max(res) if p == 'X' else min(res)

@deal.pre(lambda b: len(b) == 9 and '.' in b and not win(b, 'X') and not win(b, 'O'))
@deal.post(lambda res: 0 <= res <= 8)
def best_ai_move(b: str) -> int:
    return min((mm(norm(apply_move(b, 'O', i)), 'X'), i) for i in range(9) if b[i] == '.')[1]

# --- Tests (Hypothesis) ---
valid_boards = st.text(alphabet='XO.', min_size=9, max_size=9).filter(
    lambda b: '.' in b and not win(b, 'X') and not win(b, 'O') and abs(b.count('X') - b.count('O')) <= 1)

@given(valid_boards, st.integers(0, 8))
def test_move_validity(b: str, m: int):
    if b[m] == '.': assert apply_move(b, 'X', m).count('.') == b.count('.') - 1

@given(valid_boards)
def test_ai_optimality(b: str):
    v_opt = mm(norm(apply_move(b, 'O', best_ai_move(b))), 'X')
    assert all(v_opt <= mm(norm(apply_move(b, 'O', i)), 'X') for i in range(9) if b[i] == '.')

# --- UI & Spielschleife ---
if __name__ == '__main__':
    auto_mode = '--auto' in sys.argv

    def print_board(b, u, f):
        d = [f"\033[91m{i}\033[0m" if i in f else f"\033[92m{i}\033[0m" if i in u else '.' if b[i]=='.' else b[i] for i in range(9)]
        print(f"\n {d[0]} | {d[1]} | {d[2]} \n---+---+---\n {d[3]} | {d[4]} | {d[5]} \n---+---+---\n {d[6]} | {d[7]} | {d[8]} \n")

    b = "........."
    while '.' in b and not win(b, 'X') and not win(b, 'O'):
        b = norm(b)
        
        if b.count('.') % 2 != 0:
            sym_moves = {}
            for i in range(9):
                if b[i] == '.':
                    nb = norm(apply_move(b, 'X', i))
                    if nb not in sym_moves: sym_moves[nb] = i
            uniq = list(sym_moves.values())
            
            forced = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'O', i), 'O')]
            print_board(b, uniq, forced)

            if auto_mode and (forced or len(uniq) == 1):
                m = forced[0] if forced else uniq[0]
                print(f"[\033[93mAUTO\033[0m] Führe determinierten Zug aus: {m}")
                b = apply_move(b, 'X', m)
                continue

            while True:
                try:
                    m = int(input("Zug X (Grün=Optionen, Rot=Zwang): "))
                    if m in uniq or m in forced:  # Beschränkt Eingabe auf kanonische/zwingende Züge
                        b = apply_move(b, 'X', m)
                        break
                    elif 0 <= m <= 8 and b[m] == '.':
                        print("Bitte wähle einen der farblich markierten Repräsentanten aus.")
                except ValueError: pass
        else:
            b = apply_move(b, 'O', best_ai_move(b))

    b = norm(b)
    print("Endstand:")
    print_board(b, [], [])
    print("Gewinner:", 'X' if win(b, 'X') else 'O' if win(b, 'O') else 'Unentschieden')
!uv run crosshair check generated-2.py
!uv run pytest generated-2.py
!printf "0\n1\n2\n2\n1\n0\n3\n" | uv run generated-2.py
============================= test session starts ==============================
platform linux -- Python 3.13.2, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/user/sciebo/hhu/eipy-26/eipy-skript
configfile: pyproject.toml
plugins: cov-5.0.0, hypothesis-6.152.9, jaxtyping-0.3.11, gremlins-1.8.1, anyio-4.13.0, typeguard-4.5.2, Faker-40.23.0, hydra-core-1.3.2, logfire-4.37.0
collected 2 items                                                              

generated-2.py ..                                                        [100%]

============================== 2 passed in 0.51s ===============================

 0 | 1 | . 
---+---+---
 . | 4 | . 
---+---+---
 . | . | . 

Zug X (Grün=Optionen, Rot=Zwang): 
 X | 1 | 2 
---+---+---
 . | O | 5 
---+---+---
 . | . | 8 

Zug X (Grün=Optionen, Rot=Zwang): 
 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | 7 | 8 

Zug X (Grün=Optionen, Rot=Zwang): Zug X (Grün=Optionen, Rot=Zwang): Zug X (Grün=Optionen, Rot=Zwang): Zug X (Grün=Optionen, Rot=Zwang): Zug X (Grün=Optionen, Rot=Zwang): Endstand:

 X | X | O 
---+---+---
 X | O | . 
---+---+---
 O | . | . 

Gewinner: O

generated-3.py

generated-3.py
import sys, deal
from hypothesis import given, strategies as st
from functools import cache

# --- Kernlogik & Verträge ---
P = [[0,1,2,3,4,5,6,7,8], [6,3,0,7,4,1,8,5,2], [8,7,6,5,4,3,2,1,0], [2,5,8,1,4,7,0,3,6],
     [2,1,0,5,4,3,8,7,6], [6,7,8,3,4,5,0,1,2], [0,3,6,1,4,7,2,5,8], [8,5,2,7,4,1,6,3,0]]
W = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

def norm(b: str) -> str: return max("".join(b[i] for i in p) for p in P)
def win(b: str, p: str) -> bool: return any(all(b[i] == p for i in w) for w in W)

@deal.pre(lambda b, p, m: len(b) == 9 and p in ('X', 'O') and 0 <= m <= 8 and b[m] == '.')
@deal.post(lambda res: len(res) == 9)
def apply_move(b: str, p: str, m: int) -> str: return b[:m] + p + b[m+1:]

@cache
def mm(b: str, p: str) -> int:
    if win(b, 'X'): return 1
    if win(b, 'O'): return -1
    if '.' not in b: return 0
    q = 'O' if p == 'X' else 'X'
    res = [mm(norm(apply_move(b, p, i)), q) for i in range(9) if b[i] == '.']
    return max(res) if p == 'X' else min(res)

@deal.pre(lambda b: len(b) == 9 and '.' in b and not win(b, 'X') and not win(b, 'O'))
@deal.post(lambda res: 0 <= res <= 8)
def best_ai_move(b: str) -> int:
    return min((mm(norm(apply_move(b, 'O', i)), 'X'), i) for i in range(9) if b[i] == '.')[1]

# --- Zustandsraum-Analyse ---
@cache
def count_paths(b: str) -> int:
    if win(b, 'X') or win(b, 'O') or '.' not in b: return 1
    
    if b.count('.') % 2 != 0: 
        win_x = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'X', i), 'X')]
        win_o = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'O', i), 'O')]
        forced = win_x or win_o
        
        if forced: return count_paths(norm(apply_move(b, 'X', forced[0])))
        
        sym_moves = {norm(apply_move(b, 'X', i)): i for i in range(9) if b[i] == '.'}
        uniq = list(sym_moves.values())
        
        if len(uniq) == 1: return count_paths(norm(apply_move(b, 'X', uniq[0])))
        return sum(count_paths(norm(apply_move(b, 'X', m))) for m in uniq)
    else: 
        # KI verzweigt über alle Züge, die das Minimax-Optimum (bis auf Symmetrie) realisieren
        v_opt = min(mm(norm(apply_move(b, 'O', i)), 'X') for i in range(9) if b[i] == '.')
        opt_states = {norm(apply_move(b, 'O', i)) for i in range(9) if b[i] == '.' and mm(norm(apply_move(b, 'O', i)), 'X') == v_opt}
        return sum(count_paths(nb) for nb in opt_states)

# --- Tests (Hypothesis) ---
valid_boards = st.text(alphabet='XO.', min_size=9, max_size=9).filter(
    lambda b: '.' in b and not win(b, 'X') and not win(b, 'O') and abs(b.count('X') - b.count('O')) <= 1)

@given(valid_boards, st.integers(0, 8))
def test_move_validity(b: str, m: int):
    if b[m] == '.': assert apply_move(b, 'X', m).count('.') == b.count('.') - 1

@given(valid_boards)
def test_ai_optimality(b: str):
    v_opt = mm(norm(apply_move(b, 'O', best_ai_move(b))), 'X')
    assert all(v_opt <= mm(norm(apply_move(b, 'O', i)), 'X') for i in range(9) if b[i] == '.')

# --- UI & Spielschleife ---
if __name__ == '__main__':
    auto_mode = '--auto' in sys.argv

    def print_board(b, u, f):
        d = [f"\033[91m{i}\033[0m" if i in f else f"\033[92m{i}\033[0m" if i in u else '.' if b[i]=='.' else b[i] for i in range(9)]
        print(f"\n {d[0]} | {d[1]} | {d[2]} \n---+---+---\n {d[3]} | {d[4]} | {d[5]} \n---+---+---\n {d[6]} | {d[7]} | {d[8]} \n")

    b = norm(".........")
    while '.' in b and not win(b, 'X') and not win(b, 'O'):
        print(f"\n========================================\nVerbleibende distinkte Spielverläufe ab hier: {count_paths(b)}")
        
        if b.count('.') % 2 != 0:
            win_x = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'X', i), 'X')]
            win_o = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'O', i), 'O')]
            forced = win_x or win_o
            
            sym_moves = {}
            for i in range(9):
                if b[i] == '.':
                    nb = norm(apply_move(b, 'X', i))
                    if nb not in sym_moves: sym_moves[nb] = i
            uniq = list(sym_moves.values())
            
            print_board(b, uniq, forced)

            if auto_mode and (forced or len(uniq) == 1):
                m = forced[0] if forced else uniq[0]
                print(f"[\033[93mAUTO\033[0m] Führe determinierten Zug aus: {m}")
                b = norm(apply_move(b, 'X', m))
                continue

            while True:
                try:
                    m = int(input("Zug X (Grün=Optionen, Rot=Zwang): "))
                    if m in uniq or m in forced: 
                        b = norm(apply_move(b, 'X', m))
                        break
                    elif 0 <= m <= 8 and b[m] == '.':
                        print("Bitte wähle einen der farblich markierten Repräsentanten aus.")
                except ValueError: pass
        else:
            print("KI zieht...")
            # KI zieht im tatsächlichen Spiel weiterhin den Repräsentanten mit kleinstem Index
            b = norm(apply_move(b, 'O', best_ai_move(b)))

    print("\n========================================")
    print("Endstand:")
    print_board(b, [], [])
    print("Gewinner:", 'X' if win(b, 'X') else 'O' if win(b, 'O') else 'Unentschieden')
!uv run crosshair check generated-3.py
!uv run pytest generated-3.py
!printf "0\n1\n2\n2\n1\n0\n3\n" | uv run generated-3.py
============================= test session starts ==============================
platform linux -- Python 3.13.2, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/user/sciebo/hhu/eipy-26/eipy-skript
configfile: pyproject.toml
plugins: cov-5.0.0, hypothesis-6.152.9, jaxtyping-0.3.11, gremlins-1.8.1, anyio-4.13.0, typeguard-4.5.2, Faker-40.23.0, hydra-core-1.3.2, logfire-4.37.0
collected 2 items                                                              

generated-3.py ..                                                        [100%]

============================== 2 passed in 0.48s ===============================

========================================
Verbleibende distinkte Spielverläufe ab hier: 354

 0 | 1 | . 
---+---+---
 . | 4 | . 
---+---+---
 . | . | . 

Zug X (Grün=Optionen, Rot=Zwang): 
========================================
Verbleibende distinkte Spielverläufe ab hier: 31
KI zieht...

========================================
Verbleibende distinkte Spielverläufe ab hier: 31

 X | 1 | 2 
---+---+---
 . | O | 5 
---+---+---
 . | . | 8 

Zug X (Grün=Optionen, Rot=Zwang): 
========================================
Verbleibende distinkte Spielverläufe ab hier: 2
KI zieht...

========================================
Verbleibende distinkte Spielverläufe ab hier: 2

 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | 7 | 8 

Zug X (Grün=Optionen, Rot=Zwang): Zug X (Grün=Optionen, Rot=Zwang): Zug X (Grün=Optionen, Rot=Zwang): Zug X (Grün=Optionen, Rot=Zwang): Zug X (Grün=Optionen, Rot=Zwang): 
========================================
Verbleibende distinkte Spielverläufe ab hier: 1
KI zieht...

========================================
Endstand:

 X | X | O 
---+---+---
 X | O | . 
---+---+---
 O | . | . 

Gewinner: O

generated-4.py

generated-4.py
import sys, deal
from hypothesis import given, strategies as st
from functools import cache

# --- Kernlogik & Verträge ---
P = [[0,1,2,3,4,5,6,7,8], [6,3,0,7,4,1,8,5,2], [8,7,6,5,4,3,2,1,0], [2,5,8,1,4,7,0,3,6],
     [2,1,0,5,4,3,8,7,6], [6,7,8,3,4,5,0,1,2], [0,3,6,1,4,7,2,5,8], [8,5,2,7,4,1,6,3,0]]
W = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

def norm(b: str) -> str: return max("".join(b[i] for i in p) for p in P)
def win(b: str, p: str) -> bool: return any(all(b[i] == p for i in w) for w in W)

@deal.pre(lambda b, p, m: len(b) == 9 and p in ('X', 'O') and 0 <= m <= 8 and b[m] == '.')
@deal.post(lambda res: len(res) == 9)
def apply_move(b: str, p: str, m: int) -> str: return b[:m] + p + b[m+1:]

@cache
def mm(b: str, p: str) -> int:
    if win(b, 'X'): return 1
    if win(b, 'O'): return -1
    if '.' not in b: return 0
    q = 'O' if p == 'X' else 'X'
    res = [mm(norm(apply_move(b, p, i)), q) for i in range(9) if b[i] == '.']
    return max(res) if p == 'X' else min(res)

@deal.pre(lambda b: len(b) == 9 and '.' in b and not win(b, 'X') and not win(b, 'O'))
@deal.post(lambda res: 0 <= res <= 8)
def best_ai_move(b: str) -> int:
    return min((mm(norm(apply_move(b, 'O', i)), 'X'), i) for i in range(9) if b[i] == '.')[1]

# --- Zustandsraum-Analyse ---
@cache
def count_paths(b: str) -> int:
    if win(b, 'X') or win(b, 'O') or '.' not in b: return 1
    
    if b.count('.') % 2 != 0: 
        win_x = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'X', i), 'X')]
        win_o = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'O', i), 'O')]
        forced = win_x or win_o
        if forced: return count_paths(norm(apply_move(b, 'X', forced[0])))
        
        sym_moves = {norm(apply_move(b, 'X', i)): i for i in range(9) if b[i] == '.'}
        uniq = list(sym_moves.values())
        if len(uniq) == 1: return count_paths(norm(apply_move(b, 'X', uniq[0])))
        return sum(count_paths(norm(apply_move(b, 'X', m))) for m in uniq)
    else: 
        v_opt = min(mm(norm(apply_move(b, 'O', i)), 'X') for i in range(9) if b[i] == '.')
        opt_states = {norm(apply_move(b, 'O', i)) for i in range(9) if b[i] == '.' and mm(norm(apply_move(b, 'O', i)), 'X') == v_opt}
        return sum(count_paths(nb) for nb in opt_states)

# --- Tests (Hypothesis) ---
valid_boards = st.text(alphabet='XO.', min_size=9, max_size=9).filter(
    lambda b: '.' in b and not win(b, 'X') and not win(b, 'O') and abs(b.count('X') - b.count('O')) <= 1)

@given(valid_boards, st.integers(0, 8))
def test_move_validity(b: str, m: int):
    if b[m] == '.': assert apply_move(b, 'X', m).count('.') == b.count('.') - 1

@given(valid_boards)
def test_ai_optimality(b: str):
    v_opt = mm(norm(apply_move(b, 'O', best_ai_move(b))), 'X')
    assert all(v_opt <= mm(norm(apply_move(b, 'O', i)), 'X') for i in range(9) if b[i] == '.')

# --- UI & Spielschleife (Queue-basiert) ---
if __name__ == '__main__':
    auto_mode = '--auto' in sys.argv

    def print_board(b, u, f):
        d = [f"\033[91m{i}\033[0m" if i in f else f"\033[92m{i}\033[0m" if i in u else '.' if b[i]=='.' else b[i] for i in range(9)]
        print(f"\n {d[0]} | {d[1]} | {d[2]} \n---+---+---\n {d[3]} | {d[4]} | {d[5]} \n---+---+---\n {d[6]} | {d[7]} | {d[8]} \n")

    # Queue-Tupel: (Zustand, Historie, is_full_flag)
    queue = [(norm("........."), [], False)]
    stats = {'X': 0, 'O': 0, 'Draw': 0}
    completed = 0

    try:
        while queue:
            b, path, is_full = queue.pop(0)
            
            if win(b, 'X') or win(b, 'O') or '.' not in b:
                res = 'X' if win(b, 'X') else 'O' if win(b, 'O') else 'Draw'
                stats[res] += 1
                completed += 1
                print(f"[{completed}] Ast beendet ({'Sieg ' + res if res != 'Draw' else 'Unentschieden'}): {' -> '.join(path)}")
                continue

            print(f"\n========================================")
            print(f"Aktueller Pfad: {' -> '.join(path) if path else 'Start'}")
            print(f"Pendente Zweige: {len(queue)} | Abgeschlossen: {completed}")
            print(f"Verbleibende distinkte Spielverläufe ab hier: {count_paths(b)}")
            
            if b.count('.') % 2 != 0:
                win_x = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'X', i), 'X')]
                win_o = [i for i in range(9) if b[i] == '.' and win(apply_move(b, 'O', i), 'O')]
                forced = win_x or win_o
                
                sym_moves = {}
                for i in range(9):
                    if b[i] == '.':
                        nb = norm(apply_move(b, 'X', i))
                        if nb not in sym_moves: sym_moves[nb] = i
                uniq = list(sym_moves.values())
                
                print_board(b, uniq, forced)

                if auto_mode and (forced or len(uniq) == 1):
                    m = forced[0] if forced else uniq[0]
                    print(f"[\033[93mAUTO\033[0m] Deterministischer Zug: {m}")
                    queue.insert(0, (norm(apply_move(b, 'X', m)), path + [f"X:{m}"], is_full))
                    continue

                if is_full:
                    print("[\033[95mFULL\033[0m] Auto-expandiere alle Optionen...")
                    moves = forced if forced else uniq
                    for m in reversed(moves):
                        queue.insert(0, (norm(apply_move(b, 'X', m)), path + [f"X:{m}"], True))
                    continue

                while True:
                    try:
                        inp = input("Zug X (z.B. '1', '1,4', 'all', 'full'): ").strip().lower()
                        
                        if inp == 'full':
                            moves = forced if forced else uniq
                            for m in reversed(moves):
                                queue.insert(0, (norm(apply_move(b, 'X', m)), path + [f"X:{m}"], True))
                            break
                        elif inp == 'all':
                            moves = forced if forced else uniq
                            for m in reversed(moves):
                                queue.insert(0, (norm(apply_move(b, 'X', m)), path + [f"X:{m}"], False))
                            break
                        else:
                            moves = [int(x.strip()) for x in inp.split(',')]
                            if all(m in uniq or m in forced for m in moves) and moves:
                                for m in reversed(moves): 
                                    queue.insert(0, (norm(apply_move(b, 'X', m)), path + [f"X:{m}"], False))
                                break
                            else:
                                print("Ungültig. Wähle aus den farblich markierten Repräsentanten.")
                    except ValueError: pass
            else:
                if not is_full: print("KI verarbeitet optimale Züge...")
                v_opt = min(mm(norm(apply_move(b, 'O', i)), 'X') for i in range(9) if b[i] == '.')
                opt_states = {norm(apply_move(b, 'O', i)): i for i in range(9) if b[i] == '.' and mm(norm(apply_move(b, 'O', i)), 'X') == v_opt}
                
                for nb, m in opt_states.items():
                    queue.insert(0, (nb, path + [f"O:{m}"], is_full))

    except KeyboardInterrupt:
        print("\n\nExploration durch Nutzer abgebrochen.")

    # --- Finale Summary ---
    print("\n" + "#"*50)
    print("ZUSAMMENFASSUNG DER EXPLORATION")
    print("#"*50)
    print(f"Komplett explorierte Endstände : {completed}")
    print(f"Davon Sieg X                   : {stats['X']}")
    print(f"Davon Sieg KI (O)              : {stats['O']}")
    print(f"Davon Unentschieden            : {stats['Draw']}")
    print(f"Noch in Queue verblieben       : {len(queue)}")
    print("#"*50 + "\n")
!uv run crosshair check generated-4.py
!uv run pytest generated-4.py
!printf "0\n1\n2\n2\n1\n0\n3\n" | uv run generated-4.py
============================= test session starts ==============================
platform linux -- Python 3.13.2, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/user/sciebo/hhu/eipy-26/eipy-skript
configfile: pyproject.toml
plugins: cov-5.0.0, hypothesis-6.152.9, jaxtyping-0.3.11, gremlins-1.8.1, anyio-4.13.0, typeguard-4.5.2, Faker-40.23.0, hydra-core-1.3.2, logfire-4.37.0
collected 2 items                                                              

generated-4.py ..                                                        [100%]

============================== 2 passed in 0.51s ===============================

========================================
Aktueller Pfad: Start
Pendente Zweige: 0 | Abgeschlossen: 0
Verbleibende distinkte Spielverläufe ab hier: 354

 0 | 1 | . 
---+---+---
 . | 4 | . 
---+---+---
 . | . | . 

Zug X (z.B. '1', '1,4', 'all', 'full'): 
========================================
Aktueller Pfad: X:0
Pendente Zweige: 0 | Abgeschlossen: 0
Verbleibende distinkte Spielverläufe ab hier: 31
KI verarbeitet optimale Züge...

========================================
Aktueller Pfad: X:0 -> O:4
Pendente Zweige: 0 | Abgeschlossen: 0
Verbleibende distinkte Spielverläufe ab hier: 31

 X | 1 | 2 
---+---+---
 . | O | 5 
---+---+---
 . | . | 8 

Zug X (z.B. '1', '1,4', 'all', 'full'): 
========================================
Aktueller Pfad: X:0 -> O:4 -> X:1
Pendente Zweige: 0 | Abgeschlossen: 0
Verbleibende distinkte Spielverläufe ab hier: 2
KI verarbeitet optimale Züge...

========================================
Aktueller Pfad: X:0 -> O:4 -> X:1 -> O:2
Pendente Zweige: 0 | Abgeschlossen: 0
Verbleibende distinkte Spielverläufe ab hier: 2

 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | 7 | 8 

Zug X (z.B. '1', '1,4', 'all', 'full'): Ungültig. Wähle aus den farblich markierten Repräsentanten.
Zug X (z.B. '1', '1,4', 'all', 'full'): Ungültig. Wähle aus den farblich markierten Repräsentanten.
Zug X (z.B. '1', '1,4', 'all', 'full'): Ungültig. Wähle aus den farblich markierten Repräsentanten.
Zug X (z.B. '1', '1,4', 'all', 'full'): Ungültig. Wähle aus den farblich markierten Repräsentanten.
Zug X (z.B. '1', '1,4', 'all', 'full'): 
========================================
Aktueller Pfad: X:0 -> O:4 -> X:1 -> O:2 -> X:3
Pendente Zweige: 0 | Abgeschlossen: 0
Verbleibende distinkte Spielverläufe ab hier: 1
KI verarbeitet optimale Züge...
[1] Ast beendet (Sieg O): X:0 -> O:4 -> X:1 -> O:2 -> X:3 -> O:6

##################################################
ZUSAMMENFASSUNG DER EXPLORATION
##################################################
Komplett explorierte Endstände : 1
Davon Sieg X                   : 0
Davon Sieg KI (O)              : 1
Davon Unentschieden            : 0
Noch in Queue verblieben       : 0
##################################################

generated-5.py

generated-5.py
import sys, deal
from hypothesis import given, strategies as st
from functools import cache

# --- Abstrakte Spiele-Engine ---
class Game:
    P = [[0,1,2,3,4,5,6,7,8], [6,3,0,7,4,1,8,5,2], [8,7,6,5,4,3,2,1,0], [2,5,8,1,4,7,0,3,6],
         [2,1,0,5,4,3,8,7,6], [6,7,8,3,4,5,0,1,2], [0,3,6,1,4,7,2,5,8], [8,5,2,7,4,1,6,3,0]]
    W = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]
    
    def initial(self) -> str: return "........."
    def is_turn_p1(self, b: str) -> bool: return b.count('.') % 2 != 0
    def norm(self, b: str) -> str: return max("".join(b[i] for i in p) for p in self.P)
    def valid_moves(self, b: str) -> list[int]: return [i for i in range(9) if b[i] == '.']
    
    # Zu überschreiben in Varianten:
    def apply_move(self, b: str, m: int) -> str: pass
    def is_terminal(self, b: str) -> tuple[bool, int]: pass
    def forced_moves(self, b: str) -> list[int]: pass

# --- Klassisches Tic-Tac-Toe ---
class TicTacToe(Game):
    name = "Tic-Tac-Toe"

    @deal.pre(lambda self, b, m: len(b) == 9)
    @deal.pre(lambda self, b, m: 0 <= m <= 8 and b[m] == '.')
    @deal.post(lambda res: len(res) == 9)
    def apply_move(self, b: str, m: int) -> str: 
        return b[:m] + ('X' if self.is_turn_p1(b) else 'O') + b[m+1:]
    
    def is_terminal(self, b: str) -> tuple[bool, int]:
        p = 'O' if self.is_turn_p1(b) else 'X' 
        # Wenn der vorherige Spieler eine Linie hat, hat der aktuelle Spieler verloren (-1)
        if any(all(b[i] == p for i in w) for w in self.W): return True, -1
        if '.' not in b: return True, 0
        return False, 0
        
    def forced_moves(self, b: str) -> list[int]:
        p = 'X' if self.is_turn_p1(b) else 'O'
        q = 'O' if p == 'X' else 'X'
        for i in self.valid_moves(b):
            if any(all((b[:i]+p+b[i+1:])[j] == p for j in w) for w in self.W): return [i] # Direkter Sieg
        for i in self.valid_moves(b):
            if any(all((b[:i]+q+b[i+1:])[j] == q for j in w) for w in self.W): return [i] # Block
        return []

# --- Notakto (Inverse Tic-Tac-Toe) ---
class Notakto(Game):
    name = "Notakto"

    @deal.pre(lambda self, b, m: len(b) == 9)
    @deal.pre(lambda self, b, m: 0 <= m <= 8 and b[m] == '.')
    @deal.post(lambda res: len(res) == 9)
    def apply_move(self, b: str, m: int) -> str: 
        return b[:m] + 'X' + b[m+1:] # Jeder spielt X
        
    def is_terminal(self, b: str) -> tuple[bool, int]:
        # Wenn eine Linie existiert, hat der vorherige Spieler sie gebaut (und somit verloren).
        # Der aktuelle Spieler gewinnt (+1).
        if any(all(b[i] == 'X' for i in w) for w in self.W): return True, 1 
        if '.' not in b: return True, 0
        return False, 0
        
    def forced_moves(self, b: str) -> list[int]:
        # Züge, die keine 3er-Reihe erzeugen, sind "sicher".
        safe = [i for i in self.valid_moves(b) if not any(all((b[:i]+'X'+b[i+1:])[j] == 'X' for j in w) for w in self.W)]
        if len(safe) == 1: return safe
        if not safe and self.valid_moves(b): return [self.valid_moves(b)[0]] # Alle verlieren, wähle einfach den ersten
        return []

# --- Negamax Engine ---
class Engine:
    def __init__(self, game: Game):
        self.g = game
        self.mm_cache = {}
        self.path_cache = {}

    def mm(self, b: str) -> int:
        if b in self.mm_cache: return self.mm_cache[b]
        term, val = self.g.is_terminal(b)
        if term: return val
        
        # Negamax: Maximiere den negierten Score des Folgezustands
        res = max(-self.mm(self.g.norm(self.g.apply_move(b, m))) for m in self.g.valid_moves(b))
        self.mm_cache[b] = res
        return res

    def count_paths(self, b: str) -> int:
        if b in self.path_cache: return self.path_cache[b]
        term, _ = self.g.is_terminal(b)
        if term: return 1
        
        if self.g.is_turn_p1(b):
            forced = self.g.forced_moves(b)
            if forced:
                res = self.count_paths(self.g.norm(self.g.apply_move(b, forced[0])))
            else:
                sym_moves = {self.g.norm(self.g.apply_move(b, m)): m for m in self.g.valid_moves(b)}
                uniq = list(sym_moves.values())
                if len(uniq) == 1:
                    res = self.count_paths(self.g.norm(self.g.apply_move(b, uniq[0])))
                else:
                    res = sum(self.count_paths(self.g.norm(self.g.apply_move(b, m))) for m in uniq)
        else:
            opt_val = max(-self.mm(self.g.norm(self.g.apply_move(b, m))) for m in self.g.valid_moves(b))
            opt_states = {self.g.norm(self.g.apply_move(b, m)) for m in self.g.valid_moves(b) 
                          if -self.mm(self.g.norm(self.g.apply_move(b, m))) == opt_val}
            res = sum(self.count_paths(nb) for nb in opt_states)
            
        self.path_cache[b] = res
        return res


from hypothesis import given, strategies as st

# --- Property-Based Testing (hypothesis & pytest) ---
valid_ttt = st.text(alphabet='XO.', min_size=9, max_size=9).filter(
    lambda b: '.' in b and not TicTacToe().is_terminal(b)[0] and abs(b.count('X') - b.count('O')) <= 1)

valid_notakto = st.text(alphabet='X.', min_size=9, max_size=9).filter(
    lambda b: '.' in b and not Notakto().is_terminal(b)[0])

@given(valid_ttt, st.integers(0, 8))
def test_ttt_move_validity(b: str, m: int):
    g = TicTacToe()
    if m in g.valid_moves(b): assert g.apply_move(b, m).count('.') == b.count('.') - 1

@given(valid_notakto, st.integers(0, 8))
def test_notakto_move_validity(b: str, m: int):
    g = Notakto()
    if m in g.valid_moves(b): assert g.apply_move(b, m).count('.') == b.count('.') - 1

@given(valid_ttt)
def test_ttt_ai_optimality(b: str):
    g, eng = TicTacToe(), Engine(TicTacToe())
    moves = g.valid_moves(b)
    if moves:
        v_opt = max(-eng.mm(g.norm(g.apply_move(b, m))) for m in moves)
        assert all(v_opt >= -eng.mm(g.norm(g.apply_move(b, m))) for m in moves)

@given(valid_notakto)
def test_notakto_ai_optimality(b: str):
    g, eng = Notakto(), Engine(Notakto())
    moves = g.valid_moves(b)
    if moves:
        v_opt = max(-eng.mm(g.norm(g.apply_move(b, m))) for m in moves)
        assert all(v_opt >= -eng.mm(g.norm(g.apply_move(b, m))) for m in moves)

    
# --- UI & Spielschleife ---
if __name__ == '__main__':
    auto_mode = '--auto' in sys.argv
    g = Notakto() if '--notakto' in sys.argv else TicTacToe()
    eng = Engine(g)

    def print_board(b, u, f):
        d = [f"\033[91m{i}\033[0m" if i in f else f"\033[92m{i}\033[0m" if i in u else '.' if b[i]=='.' else b[i] for i in range(9)]
        print(f"\n {d[0]} | {d[1]} | {d[2]} \n---+---+---\n {d[3]} | {d[4]} | {d[5]} \n---+---+---\n {d[6]} | {d[7]} | {d[8]} \n")

    print(f"Starte {g.name}...")
    queue = [(g.norm(g.initial()), [], False)]
    stats = {'Mensch (P1)': 0, 'KI (P2)': 0, 'Draw': 0}
    completed = 0

    try:
        while queue:
            b, path, is_full = queue.pop(0)
            term, val = g.is_terminal(b)
            
            if term:
                # Logik: Wer den terminalen Zustand erbt und val=1 sieht, hat gewonnen.
                # Ist val=-1, hat man verloren (also der vorherige Spieler hat gewonnen).
                res = 'Draw'
                if val != 0:
                    res = 'Mensch (P1)' if (val == 1 and g.is_turn_p1(b)) or (val == -1 and not g.is_turn_p1(b)) else 'KI (P2)'
                
                stats[res] += 1
                completed += 1
                print(f"[{completed}] Ast beendet ({'Sieg ' + res if res != 'Draw' else 'Unentschieden'}): {' -> '.join(path)}")
                continue

            print(f"\n========================================")
            print(f"Aktueller Pfad: {' -> '.join(path) if path else 'Start'}")
            print(f"Pendente Zweige: {len(queue)} | Abgeschlossen: {completed}")
            print(f"Verbleibende distinkte Spielverläufe ab hier: {eng.count_paths(b)}")
            
            if g.is_turn_p1(b):
                forced = g.forced_moves(b)
                sym_moves = {}
                for i in g.valid_moves(b):
                    nb = g.norm(g.apply_move(b, i))
                    if nb not in sym_moves: sym_moves[nb] = i
                uniq = list(sym_moves.values())
                
                print_board(b, uniq, forced)

                if auto_mode and (forced or len(uniq) == 1):
                    m = forced[0] if forced else uniq[0]
                    print(f"[\033[93mAUTO\033[0m] Deterministischer Zug: {m}")
                    queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], is_full))
                    continue

                if is_full:
                    print("[\033[95mFULL\033[0m] Auto-expandiere alle Optionen...")
                    moves = forced if forced else uniq
                    for m in reversed(moves):
                        queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], True))
                    continue

                while True:
                    try:
                        inp = input("Zug P1 (z.B. '1', '1,4', 'all', 'full'): ").strip().lower()
                        if inp == 'full':
                            for m in reversed(forced if forced else uniq):
                                queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], True))
                            break
                        elif inp == 'all':
                            for m in reversed(forced if forced else uniq):
                                queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], False))
                            break
                        else:
                            moves = [int(x.strip()) for x in inp.split(',')]
                            if all(m in uniq or m in forced for m in moves) and moves:
                                for m in reversed(moves): 
                                    queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], False))
                                break
                            else:
                                print("Ungültig. Wähle aus den farblich markierten Repräsentanten.")
                    except ValueError: pass
            else:
                if not is_full: print("KI verarbeitet optimale Züge...")
                opt_val = max(-eng.mm(g.norm(g.apply_move(b, m))) for m in g.valid_moves(b))
                opt_states = {g.norm(g.apply_move(b, m)): m for m in g.valid_moves(b) 
                              if -eng.mm(g.norm(g.apply_move(b, m))) == opt_val}
                
                for nb, m in opt_states.items():
                    queue.insert(0, (nb, path + [f"KI:{m}"], is_full))

    except KeyboardInterrupt:
        print("\n\nExploration durch Nutzer abgebrochen.")

    print("\n" + "#"*50 + f"\nZUSAMMENFASSUNG DER EXPLORATION ({g.name})\n" + "#"*50)
    print(f"Komplett explorierte Endstände : {completed}")
    print(f"Davon Sieg Mensch (P1)         : {stats['Mensch (P1)']}")
    print(f"Davon Sieg KI (P2)             : {stats['KI (P2)']}")
    print(f"Davon Unentschieden            : {stats['Draw']}")
    print(f"Noch in Queue verblieben       : {len(queue)}\n" + "#"*50)
!uv run crosshair check generated-5.py
!uv run pytest generated-5.py
!printf "0\nfull\n" | uv run generated-5.py
Fetching long content....

generated-6.py

generated-6.py
import sys, deal
from hypothesis import given, strategies as st
from functools import cache

# --- Abstrakte Spiele-Engine ---
class Game:
    P = [[0,1,2,3,4,5,6,7,8], [6,3,0,7,4,1,8,5,2], [8,7,6,5,4,3,2,1,0], [2,5,8,1,4,7,0,3,6],
         [2,1,0,5,4,3,8,7,6], [6,7,8,3,4,5,0,1,2], [0,3,6,1,4,7,2,5,8], [8,5,2,7,4,1,6,3,0]]
    W = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]
    
    def initial(self) -> str: return "........."
    def is_turn_p1(self, b: str) -> bool: return b.count('.') % 2 != 0
    def norm(self, b: str) -> str: return max("".join(b[i] for i in p) for p in self.P)
    def valid_moves(self, b: str) -> list[int]: return [i for i in range(9) if b[i] == '.']
    
    # Zu überschreiben in Varianten:
    def apply_move(self, b: str, m: int) -> str: pass
    def is_terminal(self, b: str) -> tuple[bool, int]: pass
    def forced_moves(self, b: str) -> list[int]: pass

# --- Klassisches Tic-Tac-Toe ---
class TicTacToe(Game):
    name = "Tic-Tac-Toe"

    @deal.pre(lambda self, b, m: len(b) == 9)
    @deal.pre(lambda self, b, m: 0 <= m <= 8 and b[m] == '.')
    @deal.post(lambda res: len(res) == 9)
    def apply_move(self, b: str, m: int) -> str: 
        return b[:m] + ('X' if self.is_turn_p1(b) else 'O') + b[m+1:]
    
    def is_terminal(self, b: str) -> tuple[bool, int]:
        p = 'O' if self.is_turn_p1(b) else 'X' 
        # Wenn der vorherige Spieler eine Linie hat, hat der aktuelle Spieler verloren (-1)
        if any(all(b[i] == p for i in w) for w in self.W): return True, -1
        if '.' not in b: return True, 0
        return False, 0
        
    def forced_moves(self, b: str) -> list[int]:
        p = 'X' if self.is_turn_p1(b) else 'O'
        q = 'O' if p == 'X' else 'X'
        for i in self.valid_moves(b):
            if any(all((b[:i]+p+b[i+1:])[j] == p for j in w) for w in self.W): return [i] # Direkter Sieg
        for i in self.valid_moves(b):
            if any(all((b[:i]+q+b[i+1:])[j] == q for j in w) for w in self.W): return [i] # Block
        return []

# --- Notakto (Inverse Tic-Tac-Toe) ---
class Notakto(Game):
    name = "Notakto"

    @deal.pre(lambda self, b, m: len(b) == 9)
    @deal.pre(lambda self, b, m: 0 <= m <= 8 and b[m] == '.')
    @deal.post(lambda res: len(res) == 9)
    def apply_move(self, b: str, m: int) -> str: 
        return b[:m] + 'X' + b[m+1:] # Jeder spielt X
        
    def is_terminal(self, b: str) -> tuple[bool, int]:
        # Wenn eine Linie existiert, hat der vorherige Spieler sie gebaut (und somit verloren).
        # Der aktuelle Spieler gewinnt (+1).
        if any(all(b[i] == 'X' for i in w) for w in self.W): return True, 1 
        if '.' not in b: return True, 0
        return False, 0
        
    def forced_moves(self, b: str) -> list[int]:
        # Züge, die keine 3er-Reihe erzeugen, sind "sicher".
        safe = [i for i in self.valid_moves(b) if not any(all((b[:i]+'X'+b[i+1:])[j] == 'X' for j in w) for w in self.W)]
        if len(safe) == 1: return safe
        if not safe and self.valid_moves(b): return [self.valid_moves(b)[0]] # Alle verlieren, wähle einfach den ersten
        return []

class WildTicTacToe(Game):
    name = "Wild Tic-Tac-Toe"
    
    def is_terminal(self, b: str) -> tuple[bool, int]:
        # Wer immer die 3er-Reihe vollendet, gewinnt (+1)
        if any(all(b[i] == 'X' for i in w) for w in self.W): return True, 1
        if any(all(b[i] == 'O' for i in w) for w in self.W): return True, 1
        if '.' not in b: return True, 0
        return False, 0
    
    def forced_moves(self, b: str) -> list[int]:
        # Siegzwang: Kann ich mit X oder O sofort gewinnen?
        for i in self.valid_moves(b):
            for sym in ['X', 'O']:
                nb = b[:i] + sym + b[i+1:]
                if any(all(nb[j] == sym for j in w) for w in self.W): return [i]
        return []

    def apply_move_custom(self, b: str, m: int, sym: str) -> str:
        return b[:m] + sym + b[m+1:]

    # Damit die Basis-Engine nicht crasht, falls sie apply_move aufruft:
    @deal.pre(lambda self, b, m: len(b) == 9)
    @deal.pre(lambda self, b, m: 0 <= m <= 8 and b[m] == '.')
    @deal.post(lambda res: len(res) == 9)
    def apply_move(self, b: str, m: int) -> str:
        return self.apply_move_custom(b, m, 'X')
        
# --- Negamax Engine ---
class Engine:
    def __init__(self, game: Game):
        self.g = game
        self.mm_cache = {}
        self.path_cache = {}

    def mm(self, b: str) -> int:
        if b in self.mm_cache: return self.mm_cache[b]
        term, val = self.g.is_terminal(b)
        if term: return val
        
        # Negamax: Maximiere den negierten Score des Folgezustands
        res = max(-self.mm(self.g.norm(self.g.apply_move(b, m))) for m in self.g.valid_moves(b))
        self.mm_cache[b] = res
        return res

    def count_paths(self, b: str) -> int:
        if b in self.path_cache: return self.path_cache[b]
        term, _ = self.g.is_terminal(b)
        if term: return 1
        
        if self.g.is_turn_p1(b):
            forced = self.g.forced_moves(b)
            if forced:
                res = self.count_paths(self.g.norm(self.g.apply_move(b, forced[0])))
            else:
                sym_moves = {self.g.norm(self.g.apply_move(b, m)): m for m in self.g.valid_moves(b)}
                uniq = list(sym_moves.values())
                if len(uniq) == 1:
                    res = self.count_paths(self.g.norm(self.g.apply_move(b, uniq[0])))
                else:
                    res = sum(self.count_paths(self.g.norm(self.g.apply_move(b, m))) for m in uniq)
        else:
            opt_val = max(-self.mm(self.g.norm(self.g.apply_move(b, m))) for m in self.g.valid_moves(b))
            opt_states = {self.g.norm(self.g.apply_move(b, m)) for m in self.g.valid_moves(b) 
                          if -self.mm(self.g.norm(self.g.apply_move(b, m))) == opt_val}
            res = sum(self.count_paths(nb) for nb in opt_states)
            
        self.path_cache[b] = res
        return res


from hypothesis import given, strategies as st

# --- Property-Based Testing (hypothesis & pytest) ---
valid_ttt = st.text(alphabet='XO.', min_size=9, max_size=9).filter(
    lambda b: '.' in b and not TicTacToe().is_terminal(b)[0] and abs(b.count('X') - b.count('O')) <= 1)

valid_notakto = st.text(alphabet='X.', min_size=9, max_size=9).filter(
    lambda b: '.' in b and not Notakto().is_terminal(b)[0])

@given(valid_ttt, st.integers(0, 8))
def test_ttt_move_validity(b: str, m: int):
    g = TicTacToe()
    if m in g.valid_moves(b): assert g.apply_move(b, m).count('.') == b.count('.') - 1

@given(valid_notakto, st.integers(0, 8))
def test_notakto_move_validity(b: str, m: int):
    g = Notakto()
    if m in g.valid_moves(b): assert g.apply_move(b, m).count('.') == b.count('.') - 1

@given(valid_ttt)
def test_ttt_ai_optimality(b: str):
    g, eng = TicTacToe(), Engine(TicTacToe())
    moves = g.valid_moves(b)
    if moves:
        v_opt = max(-eng.mm(g.norm(g.apply_move(b, m))) for m in moves)
        assert all(v_opt >= -eng.mm(g.norm(g.apply_move(b, m))) for m in moves)

@given(valid_notakto)
def test_notakto_ai_optimality(b: str):
    g, eng = Notakto(), Engine(Notakto())
    moves = g.valid_moves(b)
    if moves:
        v_opt = max(-eng.mm(g.norm(g.apply_move(b, m))) for m in moves)
        assert all(v_opt >= -eng.mm(g.norm(g.apply_move(b, m))) for m in moves)


# --- UI & Spielschleife ---
if __name__ == '__main__':
    auto_mode = '--auto' in sys.argv
    if '--notakto' in sys.argv:
        g = Notakto()
    elif '--wild' in sys.argv:
        g = WildTicTacToe()
    else:
        g = TicTacToe()
    eng = Engine(g)

    def print_board(b, u, f):
        d = [f"\033[91m{i}\033[0m" if i in f else f"\033[92m{i}\033[0m" if i in u else '.' if b[i]=='.' else b[i] for i in range(9)]
        print(f"\n {d[0]} | {d[1]} | {d[2]} \n---+---+---\n {d[3]} | {d[4]} | {d[5]} \n---+---+---\n {d[6]} | {d[7]} | {d[8]} \n")

    print(f"Starte {g.name}...")
    queue = [(g.norm(g.initial()), [], False)]
    stats = {'Mensch (P1)': 0, 'KI (P2)': 0, 'Draw': 0}
    completed = 0

    try:
        while queue:
            b, path, is_full = queue.pop(0)
            term, val = g.is_terminal(b)
            
            if term:
                # Logik: Wer den terminalen Zustand erbt und val=1 sieht, hat gewonnen.
                # Ist val=-1, hat man verloren (also der vorherige Spieler hat gewonnen).
                res = 'Draw'
                if val != 0:
                    res = 'Mensch (P1)' if (val == 1 and g.is_turn_p1(b)) or (val == -1 and not g.is_turn_p1(b)) else 'KI (P2)'
                
                stats[res] += 1
                completed += 1
                print(f"[{completed}] Ast beendet ({'Sieg ' + res if res != 'Draw' else 'Unentschieden'}): {' -> '.join(path)}")
                continue

            print(f"\n========================================")
            print(f"Aktueller Pfad: {' -> '.join(path) if path else 'Start'}")
            print(f"Pendente Zweige: {len(queue)} | Abgeschlossen: {completed}")
            print(f"Verbleibende distinkte Spielverläufe ab hier: {eng.count_paths(b)}")
            
            if g.is_turn_p1(b):
                forced = g.forced_moves(b)
                sym_moves = {}
                for i in g.valid_moves(b):
                    nb = g.norm(g.apply_move(b, i))
                    if nb not in sym_moves: sym_moves[nb] = i
                uniq = list(sym_moves.values())
                
                print_board(b, uniq, forced)

                if auto_mode and (forced or len(uniq) == 1):
                    m = forced[0] if forced else uniq[0]
                    print(f"[\033[93mAUTO\033[0m] Deterministischer Zug: {m}")
                    queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], is_full))
                    continue

                if is_full:
                    print("[\033[95mFULL\033[0m] Auto-expandiere alle Optionen...")
                    moves = forced if forced else uniq
                    for m in reversed(moves):
                        queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], True))
                    continue

                while True:
                    try:
                        inp = input("Zug P1 (z.B. '1', '1,4', 'all', 'full'): ").strip().lower()
                        if inp == 'full':
                            for m in reversed(forced if forced else uniq):
                                queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], True))
                            break
                        elif inp == 'all':
                            for m in reversed(forced if forced else uniq):
                                queue.insert(0, (g.norm(g.apply_move(b, m)), path + [f"P1:{m}"], False))
                            break
                        else:
                            parts = inp.split(',')
                            m = int(parts[0].strip())
                            sym = parts[1].strip().upper() if len(parts) > 1 else 'X'
                            if m in uniq or m in forced:
                                # Hier wird nun das gewählte Symbol übergeben
                                queue.insert(0, (g.norm(g.apply_move_custom(b, m, sym)), path + [f"P1:{m}({sym})"], False))
                                break
                            else:
                                print("Ungültig.")
                    except ValueError: pass
            else:
                if not is_full: print("KI verarbeitet optimale Züge...")
                opt_val = max(-eng.mm(g.norm(g.apply_move(b, m))) for m in g.valid_moves(b))
                opt_states = {g.norm(g.apply_move(b, m)): m for m in g.valid_moves(b) 
                              if -eng.mm(g.norm(g.apply_move(b, m))) == opt_val}
                
                for nb, m in opt_states.items():
                    queue.insert(0, (nb, path + [f"KI:{m}"], is_full))

    except KeyboardInterrupt:
        print("\n\nExploration durch Nutzer abgebrochen.")

    print("\n" + "#"*50 + f"\nZUSAMMENFASSUNG DER EXPLORATION ({g.name})\n" + "#"*50)
    print(f"Komplett explorierte Endstände : {completed}")
    print(f"Davon Sieg Mensch (P1)         : {stats['Mensch (P1)']}")
    print(f"Davon Sieg KI (P2)             : {stats['KI (P2)']}")
    print(f"Davon Unentschieden            : {stats['Draw']}")
    print(f"Noch in Queue verblieben       : {len(queue)}\n" + "#"*50)
!uv run crosshair check generated-6.py
!uv run pytest generated-6.py
!printf "0\n4\n" | uv run generated-6.py
============================= test session starts ==============================
platform linux -- Python 3.13.2, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/user/sciebo/hhu/eipy-26/eipy-skript
configfile: pyproject.toml
plugins: cov-5.0.0, hypothesis-6.152.9, jaxtyping-0.3.11, gremlins-1.8.1, anyio-4.13.0, typeguard-4.5.2, Faker-40.23.0, hydra-core-1.3.2, logfire-4.37.0
collected 4 items                                                              

generated-6.py ....                                                      [100%]

============================== 4 passed in 0.87s ===============================
Starte Tic-Tac-Toe...

========================================
Aktueller Pfad: Start
Pendente Zweige: 0 | Abgeschlossen: 0
Verbleibende distinkte Spielverläufe ab hier: 354

 0 | 1 | . 
---+---+---
 . | 4 | . 
---+---+---
 . | . | . 

Zug P1 (z.B. '1', '1,4', 'all', 'full'): Traceback (most recent call last):
  File "/home/user/sciebo/hhu/eipy-26/eipy-skript/part-practical/generated-6.py", line 260, in <module>
    queue.insert(0, (g.norm(g.apply_move_custom(b, m, sym)), path + [f"P1:{m}({sym})"], False))
                            ^^^^^^^^^^^^^^^^^^^
AttributeError: 'TicTacToe' object has no attribute 'apply_move_custom'

gen7engine.py

gen7engine.py
import deal


class Game:
    P = [
        [0, 1, 2, 3, 4, 5, 6, 7, 8], [6, 3, 0, 7, 4, 1, 8, 5, 2],
        [8, 7, 6, 5, 4, 3, 2, 1, 0], [2, 5, 8, 1, 4, 7, 0, 3, 6],
        [2, 1, 0, 5, 4, 3, 8, 7, 6], [6, 7, 8, 3, 4, 5, 0, 1, 2],
        [0, 3, 6, 1, 4, 7, 2, 5, 8], [8, 5, 2, 7, 4, 1, 6, 3, 0],
    ]
    W = [(0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), (0, 4, 8), (2, 4, 6)]

    def initial(self) -> str:
        return "........."

    def is_turn_p1(self, b: str) -> bool:
        return b.count(".") % 2 != 0

    @deal.pre(lambda self, b: len(b) == 9)
    @deal.post(lambda res: len(res) == 9)
    @deal.ensure(lambda self, b, result: sorted(list(result)) == sorted(list(b)))
    def norm(self, b: str) -> str:
        return max("".join(b[i] for i in p) for p in self.P)

    @deal.pre(lambda self, b: len(b) == 9)
    @deal.post(lambda res: all(0 <= i <= 8 for i in res))
    @deal.post(lambda res: len(set(res)) == len(res))
    def valid_moves(self, b: str) -> list[int]:
        return [i for i in range(9) if b[i] == "."]

    @deal.pre(lambda self, b, m, s=None: len(b) == 9 and 0 <= m <= 8 and b[m] == "." and (s is None or len(s) == 1))
    @deal.post(lambda res: len(res) == 9)
    @deal.ensure(lambda self, b, m, s=None, *, result, **kwargs: result.count('.') == b.count('.') - 1)
    def apply_move(self, b: str, m: int, s: str | None = None) -> str:
        symbol = s if s is not None else self.get_symbol(b, m)
        return b[:m] + symbol + b[m + 1 :]

    def get_canonical_moves(self, b: str) -> list[int]:
        """Ermittelt die Menge der eindeutigen Züge für das UI."""
        seen = set()
        uniq = []
        for m in self.valid_moves(b):
            nb = self.norm(self.apply_move(b, m))
            if nb not in seen:
                seen.add(nb)
                uniq.append(m)
        return uniq

    @deal.pre(lambda self, b: len(b) == 9)
    @deal.post(lambda res: res[1] in [-1, 0, 1])
    def is_terminal(self, b: str) -> tuple[bool, int]:
        raise NotImplementedError

    def forced_moves(self, b: str) -> list[int]:
        raise NotImplementedError

    def get_symbol(self, b: str, m: int) -> str:
        raise NotImplementedError


class TicTacToe(Game):
    name = "Tic-Tac-Toe"

    def get_symbol(self, b: str, m: int) -> str:
        return "X" if self.is_turn_p1(b) else "O"

    def is_terminal(self, b: str) -> tuple[bool, int]:
        p = "O" if self.is_turn_p1(b) else "X"
        if any(all(b[i] == p for i in w) for w in self.W):
            return True, -1
        return (True, 0) if "." not in b else (False, 0)

    def forced_moves(self, b: str) -> list[int]:
        p, q = ("X", "O") if self.is_turn_p1(b) else ("O", "X")
        for i in self.valid_moves(b):
            if any(all((b[:i] + p + b[i + 1 :])[j] == p for j in w) for w in self.W):
                return [i]
        for i in self.valid_moves(b):
            if any(all((b[:i] + q + b[i + 1 :])[j] == q for j in w) for w in self.W):
                return [i]
        return []


class Notakto(Game):
    name = "Notakto"

    def get_symbol(self, b: str, m: int) -> str:
        return "X"

    def is_terminal(self, b: str) -> tuple[bool, int]:
        if any(all(b[i] == "X" for i in w) for w in self.W):
            return True, 1
        return (True, 0) if "." not in b else (False, 0)

    def forced_moves(self, b: str) -> list[int]:
        safe = [
            i
            for i in self.valid_moves(b)
            if not any(all((b[:i] + "X" + b[i + 1 :])[j] == "X" for j in w) for w in self.W)
        ]
        return safe if len(safe) == 1 else ([self.valid_moves(b)[0]] if self.valid_moves(b) else [])


class WildTicTacToe(Game):
    name = "Wild Tic-Tac-Toe"

    def get_symbol(self, b: str, m: int) -> str:
        return "X"

    def is_terminal(self, b: str) -> tuple[bool, int]:
        if any(all(b[i] in ["X", "O"] and b[i] == b[w[0]] for i in w) for w in self.W):
            return True, 1
        return (True, 0) if "." not in b else (False, 0)

    def forced_moves(self, b: str) -> list[int]:
        for i in self.valid_moves(b):
            for s in ["X", "O"]:
                if any(all((b[:i] + s + b[i + 1 :])[j] == s for j in w) for w in self.W):
                    return [i]
        return []


class Engine:
    """Reine KI-Evaluierungslogik (Minimax)."""
    def __init__(self, game: Game):
        self.g: Game = game
        self.mm_cache: dict[str, int] = {}

    @deal.pre(lambda self, b: len(b) == 9)
    @deal.post(lambda res: res in [-1, 0, 1])
    def mm(self, b: str) -> int:
        if b in self.mm_cache:
            return self.mm_cache[b]
        term, val = self.g.is_terminal(b)
        if term:
            return val
        res = max(-self.mm(self.g.norm(self.g.apply_move(b, m))) for m in self.g.valid_moves(b))
        self.mm_cache[b] = res
        return res


class GameAnalyzer:
    """Erweiterte Analytik für den Spielbaum und UI-Metriken."""
    def __init__(self, game: Game, engine: Engine):
        self.g = game
        self.eng = engine
        self.path_cache: dict[str, int] = {}

    def count_paths(self, b: str) -> int:
        if b in self.path_cache:
            return self.path_cache[b]
        term, _ = self.g.is_terminal(b)
        if term:
            return 1
        
        if self.g.is_turn_p1(b):
            forced = self.g.forced_moves(b)
            if forced:
                res = self.count_paths(self.g.norm(self.g.apply_move(b, forced[0])))
            else:
                uniq = self.g.get_canonical_moves(b)
                res = sum(self.count_paths(self.g.norm(self.g.apply_move(b, m))) for m in uniq)
        else:
            moves = self.g.valid_moves(b)
            opt_val = max(-self.eng.mm(self.g.norm(self.g.apply_move(b, m))) for m in moves)
            opt_states = {
                self.g.norm(self.g.apply_move(b, m))
                for m in moves
                if -self.eng.mm(self.g.norm(self.g.apply_move(b, m))) == opt_val
            }
            res = sum(self.count_paths(nb) for nb in opt_states)
            
        self.path_cache[b] = res
        return res

gen7ui.py

gen7ui.py
import sys

import deal
from gen7engine import Engine, GameAnalyzer, Notakto, TicTacToe, WildTicTacToe


def print_board(b, u, f):
    d = [
        f"\033[91m{i}\033[0m" if i in f else f"\033[92m{i}\033[0m" if i in u else "." if b[i] == "." else b[i]
        for i in range(9)
    ]
    print(f"\n {d[0]} | {d[1]} | {d[2]} \n---+---+---\n {d[3]} | {d[4]} | {d[5]} \n---+---+---\n {d[6]} | {d[7]} | {d[8]} \n")

def run_ui():
    deal.activate()
    auto_mode = "--auto" in sys.argv
    if "--notakto" in sys.argv:
        g = Notakto()
    elif "--wild" in sys.argv:
        g = WildTicTacToe()
    else:
        g = TicTacToe()

    eng = Engine(g)
    analyzer = GameAnalyzer(g, eng)
    queue = [(g.norm(g.initial()), [], False)]

    try:
        while queue:
            norm_b, path, is_full = queue.pop(0)
            
            term, val = g.is_terminal(norm_b)
            if term:
                print(f"Ast beendet (Wert: {val}): {' -> '.join(path)}")
                continue

            print(f"\nDistinkte Spielverläufe ab hier: {analyzer.count_paths(norm_b)}")

            if g.is_turn_p1(norm_b):
                if path:
                    print(f"\n\033[94m[{len(path)}] Aktueller Ast:\033[0m {' -> '.join(path)}")
                else:
                    print("\n\033[94m[0] Wurzelknoten (Start)\033[0m")
                forced = g.forced_moves(norm_b)
                uniq = g.get_canonical_moves(norm_b)
                print_board(norm_b, uniq, forced)

                if auto_mode and (forced or len(uniq) == 1):
                    m = forced[0] if forced else uniq[0]
                    queue.insert(0, (g.norm(g.apply_move(norm_b, m)), path + [f"P1:{m}"], is_full))
                    continue

                inp = "full" if is_full else input("Zug (z.B. '1' oder '1,O'): ").lower()
                
                if inp in ["full", "all"]:
                    for m in (forced if forced else uniq):
                        queue.insert(0, (g.norm(g.apply_move(norm_b, m)), path + [f"P1:{m}"], (inp == "full")))
                else:
                    try:
                        p = inp.split(",")
                        m = int(p[0])
                        s = p[1].upper() if len(p) > 1 else None
                        if m in g.valid_moves(norm_b):
                            nb = g.apply_move(norm_b, m, s)
                            queue.insert(0, (g.norm(nb), path + [f"P1:{m}({s if s is not None else 'X'})"], False))
                        else:
                            print(f"Zug {m} ist auf diesem Brett nicht erlaubt!")
                            queue.insert(0, (norm_b, path, is_full))
                    except Exception as e:
                        print("Eingabefehler:", e)
                        queue.insert(0, (norm_b, path, is_full))
            else:
                moves = [(m, s) for m in g.valid_moves(norm_b) for s in (["X", "O"] if isinstance(g, WildTicTacToe) else ["None"])]
                
                def eval_move(m, s):
                    return -eng.mm(g.norm(g.apply_move(norm_b, m, s if s != "None" else None)))
                
                opt_val = max(eval_move(m, s) for m, s in moves)
                
                for m, s in moves:
                    if eval_move(m, s) == opt_val:
                        nb = g.norm(g.apply_move(norm_b, m, s if s != "None" else None))
                        queue.insert(0, (nb, path + [f"KI:{m}({s if s != 'None' else g.get_symbol(norm_b, m)})"], is_full))
                        
    except KeyboardInterrupt:
        print("\nAbbruch.")

if __name__ == "__main__":
    run_ui()

gen7tests.py

gen7tests.py
import deal
import pytest
from gen7engine import (
    Engine,
    Notakto,
    TicTacToe,
    WildTicTacToe,
)
from hypothesis import given
from hypothesis import strategies as st

# Aktiviert deal-Contracts für Laufzeitprüfungen in den Tests
deal.activate()

valid_ttt = st.text(alphabet="XO.", min_size=9, max_size=9).filter(
    lambda b: "." in b and not TicTacToe().is_terminal(b)[0] and abs(b.count("X") - b.count("O")) <= 1
)
valid_notakto = st.text(alphabet="X.", min_size=9, max_size=9).filter(
    lambda b: "." in b and not Notakto().is_terminal(b)[0]
)

def test_canonical_moves_initial_board():
    g = TicTacToe()
    b = g.initial()
    canonical = g.get_canonical_moves(b)
    assert canonical == [0, 1, 4], f"Erwartet [0, 1, 4], aber erhalten: {canonical}"

def test_engine_perfect_play_draw():
    """Nullsummen-Garantie: Perfektes Spiel in klassischem Tic-Tac-Toe führt zum Remis."""
    g = TicTacToe()
    eng = Engine(g)
    assert eng.mm(g.initial()) == 0

@given(valid_ttt)
def test_norm_idempotence_ttt(b: str):
    """Property-based Test: norm(norm(b)) == norm(b)."""
    g = TicTacToe()
    assert g.norm(g.norm(b)) == g.norm(b)

@given(valid_ttt)
def test_ttt_ai_optimality(b: str):
    g, eng = TicTacToe(), Engine(TicTacToe())
    moves = g.valid_moves(b)
    if moves:
        v_opt = max(-eng.mm(g.norm(g.apply_move(b, m))) for m in moves)
        assert all(v_opt >= -eng.mm(g.norm(g.apply_move(b, m))) for m in moves)

@given(valid_notakto)
def test_notakto_ai_optimality(b: str):
    g, eng = Notakto(), Engine(Notakto())
    moves = g.valid_moves(b)
    if moves:
        v_opt = max(-eng.mm(g.norm(g.apply_move(b, m))) for m in moves)
        assert all(v_opt >= -eng.mm(g.norm(g.apply_move(b, m))) for m in moves)

@pytest.mark.parametrize("game_class", [TicTacToe, Notakto])
def test_terminal_invariance(game_class):
    g = game_class()
    test_boards = ["XXXOO....", "XOXOXOXOX"]
    for b in test_boards:
        if b.count('X') + b.count('O') != 9 and not any(all(b[i] != '.' and b[i] == b[w[0]] for i in w) for w in g.W):
            continue # Skip invalid intermediate states for the specific game
        term_b, val_b = g.is_terminal(b)
        term_n, val_n = g.is_terminal(g.norm(b))
        assert term_b == term_n
        assert val_b == val_n

def test_ui_logic_consistency():
    g = WildTicTacToe()
    b = g.initial()
    m = 4
    nb = g.apply_move(b, m, "X")
    with pytest.raises(deal.PreContractError):
        g.apply_move(nb, m) # Zug auf bereits besetztes Feld
# --report_all zeigt auch die Verträge, zu denen CrossHair nichts sagen kann.
# sed kürzt die absoluten Pfade aus der Ausgabe.
!uv run crosshair check --report_all gen7engine.py 2>&1 | sed 's|.*/||'
gen7engine.py:19: info: Not confirmed.
gen7engine.py:25: info: Not confirmed.
gen7engine.py:31: info: Not confirmed.
gen7engine.py:49: info: Not confirmed.
gen7engine.py:129: info: Not confirmed.

Dieses Ergebnis ist lehrreicher als ein grüner Haken. Ohne --report_all hätte die Zelle gar nichts ausgegeben und mit Exit-Code 0 geendet — und genau das wird gern als “verifiziert” gelesen. Es heißt aber nur: kein Gegenbeispiel gefunden.

Mit --report_all sieht man, was wirklich passiert ist. Zu allen fünf mit deal versehenen Methoden — norm (Zeile 19), valid_moves (25), apply_move (31), is_terminal (49) und mm (129) — sagt CrossHair Not confirmed: Es hat innerhalb seines Zeitbudgets weder einen Verstoß konstruiert noch die Nachbedingung beweisen können. Der Zustandsraum eines Tic-Tac-Toe-Bretts als 9-Zeichen-String ist für symbolische Ausführung unangenehm groß, und mm ist rekursiv.

Damit ordnet sich das Werkzeug ein: CrossHair ist gut darin, Gegenbeispiele zu finden, und schlecht darin, ihre Abwesenheit zu beweisen. Ein Fund ist ein harter Befund; ein Nicht-Fund ist eine Zeitangabe. Wer stärkere Aussagen will, verkleinert die zu prüfende Funktion, bis der Zustandsraum handhabbar wird — siehe Design by Contract.

!uv run pytest gen7tests.py
============================= test session starts ==============================
platform linux -- Python 3.13.2, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/user/sciebo/hhu/eipy-26/eipy-skript
configfile: pyproject.toml
plugins: cov-5.0.0, hypothesis-6.152.9, jaxtyping-0.3.11, gremlins-1.8.1, anyio-4.13.0, typeguard-4.5.2, Faker-40.23.0, hydra-core-1.3.2, logfire-4.37.0
collected 8 items                                                              

gen7tests.py ........                                                    [100%]

============================== 8 passed in 0.94s ===============================
!printf "0\n4\nfull" | uv run gen7ui.py

Distinkte Spielverläufe ab hier: 354

[0] Wurzelknoten (Start)

 0 | 1 | . 
---+---+---
 . | 4 | . 
---+---+---
 . | . | . 

Zug (z.B. '1' oder '1,O'): 
Distinkte Spielverläufe ab hier: 31

Distinkte Spielverläufe ab hier: 31

[2] Aktueller Ast: P1:0(X) -> KI:4(O)

 X | 1 | 2 
---+---+---
 . | O | 5 
---+---+---
 . | . | 8 

Zug (z.B. '1' oder '1,O'): Zug 4 ist auf diesem Brett nicht erlaubt!

Distinkte Spielverläufe ab hier: 31

[2] Aktueller Ast: P1:0(X) -> KI:4(O)

 X | 1 | 2 
---+---+---
 . | O | 5 
---+---+---
 . | . | 8 

Zug (z.B. '1' oder '1,O'): 
Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:7(O)

 X | O | 2 
---+---+---
 3 | O | 5 
---+---+---
 6 | 7 | X 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:7(O) -> P1:7 -> KI:2(O)

 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | O | X 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:7(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | O | X 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:8 -> KI:7(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:5

Distinkte Spielverläufe ab hier: 1

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:5(O)

 X | O | 2 
---+---+---
 3 | O | 5 
---+---+---
 6 | 7 | X 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:5(O) -> P1:7 -> KI:2(O)

 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | O | X 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:5(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | O | X 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:8 -> KI:5(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:5

Distinkte Spielverläufe ab hier: 1

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:3(O)

 X | O | 2 
---+---+---
 3 | O | 5 
---+---+---
 6 | 7 | X 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:3(O) -> P1:7 -> KI:2(O)

 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | O | X 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:3(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | O | X 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:8 -> KI:3(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:5

Distinkte Spielverläufe ab hier: 1

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:1(O)

 X | O | 2 
---+---+---
 3 | O | 5 
---+---+---
 6 | 7 | X 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:1(O) -> P1:7 -> KI:2(O)

 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | O | X 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:8 -> KI:1(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | O | X 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:8 -> KI:1(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:5

Distinkte Spielverläufe ab hier: 23

Distinkte Spielverläufe ab hier: 13

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O)

 X | 1 | 2 
---+---+---
 3 | O | X 
---+---+---
 6 | 7 | O 


Distinkte Spielverläufe ab hier: 5

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:6(O)

 X | 1 | O 
---+---+---
 3 | O | X 
---+---+---
 6 | X | O 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:6(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:6(O) -> P1:6 -> KI:3(O) -> P1:7

Distinkte Spielverläufe ab hier: 4

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O)

 X | O | 2 
---+---+---
 3 | O | X 
---+---+---
 6 | X | O 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O) -> P1:6 -> KI:3(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O) -> P1:3 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 6 | X | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O) -> P1:3 -> KI:2(O) -> P1:6

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O) -> P1:2 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O) -> P1:2 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O) -> P1:2 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:3(O) -> P1:2 -> KI:7(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:2(O)

 X | 1 | O 
---+---+---
 3 | O | X 
---+---+---
 6 | X | O 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:7

Distinkte Spielverläufe ab hier: 4

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O)

 X | O | 2 
---+---+---
 3 | O | X 
---+---+---
 6 | X | O 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O) -> P1:6 -> KI:3(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O) -> P1:3 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 6 | X | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O) -> P1:3 -> KI:2(O) -> P1:6

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O) -> P1:2 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O) -> P1:2 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O) -> P1:2 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:7 -> KI:1(O) -> P1:2 -> KI:7(O) -> P1:8

Distinkte Spielverläufe ab hier: 4

Distinkte Spielverläufe ab hier: 4

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O)

 X | O | X 
---+---+---
 3 | O | 5 
---+---+---
 O | X | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O) -> P1:8 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | O | X 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O) -> P1:8 -> KI:3(O) -> P1:5

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O) -> P1:5 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O) -> P1:5 -> KI:2(O) -> P1:7

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O) -> P1:3 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O) -> P1:3 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O) -> P1:3 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:6 -> KI:1(O) -> P1:3 -> KI:7(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:3 -> KI:2(O)

 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | X | O 


Distinkte Spielverläufe ab hier: 1
Ast beendet (Wert: -1): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:3 -> KI:2(O) -> P1:5 -> KI:6(O)

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 2

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:2 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | 7 | 8 


Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:2 -> KI:3(O) -> P1:5 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:2 -> KI:3(O) -> P1:5 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:2 -> KI:3(O) -> P1:5 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:2 -> KI:3(O) -> P1:5 -> KI:7(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:1 -> KI:2(O)

 X | X | O 
---+---+---
 3 | O | X 
---+---+---
 6 | 7 | O 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:8(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:7(O)

 X | 1 | 2 
---+---+---
 3 | O | X 
---+---+---
 6 | O | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:7(O) -> P1:1 -> KI:2(O)

 X | X | O 
---+---+---
 3 | O | X 
---+---+---
 6 | O | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:7(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:7(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:8

Distinkte Spielverläufe ab hier: 4

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O)

 X | 1 | O 
---+---+---
 3 | O | X 
---+---+---
 6 | 7 | 8 


Distinkte Spielverläufe ab hier: 4

Distinkte Spielverläufe ab hier: 4

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O)

 X | O | X 
---+---+---
 3 | O | 5 
---+---+---
 O | X | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O) -> P1:8 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | O | X 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O) -> P1:8 -> KI:3(O) -> P1:5

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O) -> P1:5 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O) -> P1:5 -> KI:2(O) -> P1:7

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O) -> P1:3 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O) -> P1:3 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O) -> P1:3 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:2(O) -> P1:6 -> KI:1(O) -> P1:3 -> KI:7(O) -> P1:8

Distinkte Spielverläufe ab hier: 5

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O)

 X | O | 2 
---+---+---
 3 | O | X 
---+---+---
 6 | 7 | 8 


Distinkte Spielverläufe ab hier: 5

Distinkte Spielverläufe ab hier: 4

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O)

 X | O | 2 
---+---+---
 3 | O | X 
---+---+---
 6 | X | O 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:6 -> KI:3(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:3 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 6 | X | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:3 -> KI:2(O) -> P1:6

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:2 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:2 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:2 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:2 -> KI:7(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:6(O)

 X | O | 2 
---+---+---
 3 | O | X 
---+---+---
 O | X | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:2 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:5 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:2 -> KI:2(O) -> P1:7

Distinkte Spielverläufe ab hier: 5

Distinkte Spielverläufe ab hier: 5

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O)

 X | O | X 
---+---+---
 3 | O | . 
---+---+---
 6 | 7 | . 


Distinkte Spielverläufe ab hier: 5

Distinkte Spielverläufe ab hier: 4

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O)

 X | O | X 
---+---+---
 3 | O | 5 
---+---+---
 O | X | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:8 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | O | X 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:8 -> KI:3(O) -> P1:5

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:5 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:5 -> KI:2(O) -> P1:7

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:3 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:3 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:3 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:8(O) -> P1:3 -> KI:7(O) -> P1:8

Distinkte Spielverläufe ab hier: 4

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O)

 X | O | X 
---+---+---
 3 | O | 5 
---+---+---
 O | X | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:8 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | O | X 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:8 -> KI:3(O) -> P1:5

Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:5 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:5 -> KI:2(O) -> P1:7

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:3 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:3 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:3 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:6(O) -> P1:3 -> KI:7(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:5(O)

 X | O | X 
---+---+---
 O | O | 5 
---+---+---
 6 | X | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:5(O) -> P1:5 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:5(O) -> P1:5 -> KI:2(O) -> P1:8

Distinkte Spielverläufe ab hier: 1

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:3(O)

 X | O | X 
---+---+---
 O | O | 5 
---+---+---
 6 | X | 8 


Distinkte Spielverläufe ab hier: 1

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:3(O) -> P1:5 -> KI:2(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:2 -> KI:1(O) -> P1:7 -> KI:3(O) -> P1:5 -> KI:2(O) -> P1:8

Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 2

[4] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:1 -> KI:2(O)

 X | X | O 
---+---+---
 3 | O | 5 
---+---+---
 6 | 7 | 8 


Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 2

[6] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O)

 X | X | O 
---+---+---
 O | O | 5 
---+---+---
 X | 7 | 8 


Distinkte Spielverläufe ab hier: 2

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:5 -> KI:8(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | 7 | O 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:5 -> KI:8(O) -> P1:7

Distinkte Spielverläufe ab hier: 1

[8] Aktueller Ast: P1:0(X) -> KI:4(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:5 -> KI:7(O)

 X | X | O 
---+---+---
 O | O | X 
---+---+---
 X | O | 8 

Ast beendet (Wert: 0): P1:0(X) -> KI:4(O) -> P1:1 -> KI:2(O) -> P1:6 -> KI:3(O) -> P1:5 -> KI:7(O) -> P1:8