Ga naar hoofdinhoud

9.17 Er gaat iets mis — top-3 fouten

Leerdoel: je herkent de drie klassieke valkuilen bij het bouwen van een minimax-AI.

Bord muteren in result

Geen foutmelding — wél een onverklaarbaar verkeerd antwoord. Minimax "probeert" zetten en denkt dat het origineel niet meegaat — als dat wel zo is, raakt het hele algoritme van slag.

Oorzaak: zonder copy.deepcopy werkt Python met referenties. Een wijziging in nieuw_bord is ook zichtbaar in bord:

# FOUT
def result(bord, zet):
nieuw = bord # ← nieuw is GEEN kopie, het is hetzelfde object!
i, j = zet
nieuw[i][j] = player(bord)
return nieuw

Ook bord.copy() of list(bord) is niet genoeg — die maken alleen de buitenste lijst nieuw, de rijen daaronder blijven gedeeld.

Oplossing: gebruik copy.deepcopy(bord) voor een echte, diepe kopie:

# GOED
def result(bord, zet):
nieuw = copy.deepcopy(bord)
i, j = zet
nieuw[i][j] = player(bord)
return nieuw

Base case vergeten in max_value / min_value / minimax

Foutmelding: RecursionError: maximum recursion depth exceeded. Browser hangt soms even.

Oorzaak: een recursieve functie moet een stop-conditie hebben. Zonder roept hij zichzelf eeuwig aan op steeds nieuwere borden.

# FOUT — geen base case
def max_value(bord):
v = -math.inf
for zet in actions(bord):
v = max(v, min_value(result(bord, zet)))
return v

Op een terminaal bord doet actions(bord) niks (lege set) → de lus draait niet → v blijft -math.inf → return -math.inf. Dat is fout. En in de stap daarboven roept iets max_value aan op een bord dat geen recursie-einde heeft.

Oplossing: check eerst of het bord terminal is. Zo ja, return de utility (+1, -1, 0). Dat is de bodem van de recursie:

# GOED
def max_value(bord):
if terminal(bord):
return utility(bord)
v = -math.inf
for zet in actions(bord):
v = max(v, min_value(result(bord, zet)))
return v

Idem in min_value en in minimax zelf.

Min en max omdraaien per laag

Geen foutmelding — wél een AI die verliest of suboptimaal speelt.

Oorzaak: je hebt max_value per ongeluk max_value laten aanroepen (zichzelf), in plaats van afwisselend min_value. Of je verwart wie maximaliseert (X) en wie minimaliseert (O).

# FOUT — max_value roept max_value aan in plaats van min_value
def max_value(bord):
if terminal(bord):
return utility(bord)
v = -math.inf
for zet in actions(bord):
v = max(v, max_value(result(bord, zet))) # ← moet min_value zijn!
return v

Oplossing: onthoud het ritme — wisselbeurten. max_value zoekt naar X's beste keuze; de volgende beurt is O, dus min_value(result(...)). Andersom geldt hetzelfde.

# GOED
def max_value(bord):
if terminal(bord):
return utility(bord)
v = -math.inf
for zet in actions(bord):
v = max(v, min_value(result(bord, zet))) # ← afwisselend!
return v


def min_value(bord):
if terminal(bord):
return utility(bord)
v = math.inf
for zet in actions(bord):
v = min(v, max_value(result(bord, zet))) # ← afwisselend!
return v

Vuistregel: in max_value zie je het woord min voorkomen, en in min_value zie je het woord max. Zit dat omgekeerd? Dan klopt het niet.

Door naar stap 18: cheatsheet →.