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
gen7mit einem Terminal-Agenten statt mit einem Chat-Fenster (siehe Agentic Coding). Der Ausgangs-Prompt steht ingenerated-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, derpytestselbst aufrufen kann, hat einen Vorteil, den ein Chat-Fenster nicht hat.
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
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
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
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
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
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.pygenerated-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
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
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
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