Extra¶
Vier op een rij: min-max¶
Opgave: Vier op een rij: min-max
De klasse Board hieronder is die van de uitwerking van
week 6, met één methode erbij: score (stap 1). De andere
methoden, ook host_game, zijn hetzelfde gebleven.
import random
class Board:
"""Een bord voor Vier op een rij, met een willekeurig aantal rijen en kolommen."""
def __init__(self, width, height):
"""Maak een leeg bord met de gegeven breedte en hoogte."""
self._width = width
self._height = height
self._data = [[" "] * width for row in range(height)]
@property
def width(self):
"""Het aantal kolommen, alleen om te lezen."""
return self._width
@property
def height(self):
"""Het aantal rijen, alleen om te lezen."""
return self._height
def __repr__(self):
"""Geeft het bord als string, met de kolomnummers eronder."""
s = ""
for row in range(self._height):
s += "|"
for col in range(self._width):
s += self._data[row][col] + "|"
s += "\n"
s += (2 * self._width + 1) * "-" + "\n"
for col in range(self._width):
s += " " + str(col % 10)
return s
def add_move(self, col, ox):
"""Laat een steen ox in kolom col vallen."""
for row in range(self._height - 1, -1, -1):
if self._data[row][col] == " ":
self._data[row][col] = ox
return
def clear(self):
"""Maakt het bord leeg."""
for row in range(self._height):
for col in range(self._width):
self._data[row][col] = " "
def set_board(self, move_string):
"""Speelt de kolommen in move_string, om en om X en O, te beginnen met X.
b.set_board("012345") zet X en O om en om op de onderste rij,
b.set_board("000000") zet ze om en om in de linkerkolom.
move_string bestaat uit cijfers van één teken.
"""
next_checker = "X"
for col_char in move_string:
col = int(col_char)
if 0 <= col < self._width:
self.add_move(col, next_checker)
if next_checker == "X":
next_checker = "O"
else:
next_checker = "X"
def allows_move(self, col):
"""Geeft True als er in kolom col nog een steen bij kan."""
return 0 <= col < self._width and self._data[0][col] == " "
def is_full(self):
"""Geeft True als er nergens meer een steen bij kan."""
for col in range(self._width):
if self.allows_move(col):
return False
return True
def del_move(self, col):
"""Haalt de bovenste steen uit kolom col; doet niets bij een lege kolom."""
for row in range(self._height):
if self._data[row][col] != " ":
self._data[row][col] = " "
return
def wins_for(self, ox):
"""Geeft True als ox vier stenen op een rij heeft, in welke richting ook."""
for row in range(self._height):
for col in range(self._width):
if self.in_a_row(ox, row, col, 0, 1):
return True
if self.in_a_row(ox, row, col, 1, 0):
return True
if self.in_a_row(ox, row, col, 1, 1):
return True
if self.in_a_row(ox, row, col, -1, 1):
return True
return False
def in_a_row(self, ox, row, col, d_row, d_col):
"""Geeft True als er vanaf (row, col) vier keer ox ligt in richting (d_row, d_col)."""
for i in range(4):
r = row + i * d_row
c = col + i * d_col
if not (0 <= r < self._height and 0 <= c < self._width):
return False
if self._data[r][c] != ox:
return False
return True
def cols_to_win(self, ox):
"""Geeft een oplopende lijst van de kolommen waarin ox met één zet wint."""
cols = []
for col in range(self._width):
if self.allows_move(col):
self.add_move(col, ox)
if self.wins_for(ox):
cols.append(col)
self.del_move(col)
return cols
def score(self):
"""Geeft -100 als X gewonnen heeft, 100 als O gewonnen heeft, en anders 0."""
if self.wins_for("X"):
return -100
if self.wins_for("O"):
return 100
return 0
def host_game(self, px, po):
"""Laat px (met X) en po (met O) Vier op een rij spelen; px begint."""
print("Welkom bij Vier op een rij!")
print()
print(self)
print()
player = px
while True:
col = player.next_move(self)
self.add_move(col, player.ox)
print()
print(self)
print()
if self.wins_for(player.ox):
print(f"{player.ox} wint -- Gefeliciteerd!")
break
if self.is_full():
print("Gelijkspel!")
break
if player is px:
player = po
else:
player = px
De spelers van week 6, ongewijzigd:
class Player:
"""Een speler van Vier op een rij, met een eenvoudige standaardzet."""
def __init__(self, ox):
"""Maak een speler met de steen ox: "X" of "O"."""
self.ox = ox
def opponent(self):
"""Geeft de steen van de tegenstander."""
if self.ox == "X":
return "O"
return "X"
def next_move(self, board):
"""Geeft de meest linkse kolom waarin een zet mag."""
for col in range(board.width):
if board.allows_move(col):
return col
class HumanPlayer(Player):
"""Een mens, die zijn zet intypt."""
def next_move(self, board):
"""Vraagt om een kolom, net zo lang tot de zet mag."""
col = -1
while not board.allows_move(col):
col = int(input(f"Keuze van {self.ox}: "))
return col
class SimpleAIPlayer(Player):
"""Een computerspeler die één zet vooruitkijkt."""
def next_move(self, board):
"""Wint als dat kan, blokkeert als dat moet, en kiest anders de standaardzet."""
wins = board.cols_to_win(self.ox)
if len(wins) > 0:
return wins[0]
blocks = board.cols_to_win(self.opponent())
if len(blocks) > 0:
return blocks[0]
return super().next_move(board)
class ScriptedPlayer:
"""Een speler die een vaste lijst zetten afwerkt; geen subklasse van Player."""
def __init__(self, ox, moves):
"""Maak een speler met de steen ox die de kolommen in moves speelt."""
self.ox = ox
self._moves = list(moves)
self._turn = 0
def next_move(self, board):
"""Geeft de volgende kolom uit de lijst."""
col = self._moves[self._turn]
self._turn += 1
return col
Stap 1: score(self)¶
score gebruikt wins_for voor beide stenen. Het bord hoeft daarvoor niets te
weten van een speler.
b = Board(7, 6)
b.set_board("01020305")
assert b.score() == -100
b = Board(7, 6)
b.set_board("01010161")
assert b.score() == 100
assert Board(7, 6).score() == 0
Stap 2: de klasse ScoredMove¶
__eq__ en __lt__ vergelijken alleen score. c > a en max werken zonder
methode voor >, doordat Python de vergelijking omdraait naar a < c.
class ScoredMove:
"""Een zet: een kolom, met de waarde van het bord na die zet."""
def __init__(self, col, score):
"""Maak een gescoorde zet in kolom col met waarde score."""
self.col = col
self.score = score
def __repr__(self):
"""Geeft de zet als string, zoals ScoredMove(3, 100)."""
return f"ScoredMove({self.col}, {self.score})"
def __eq__(self, other):
"""Geeft True als other een ScoredMove is met dezelfde waarde."""
if not isinstance(other, ScoredMove):
return False
return self.score == other.score
def __lt__(self, other):
"""Geeft True als deze zet een lagere waarde heeft dan other."""
if not isinstance(other, ScoredMove):
raise TypeError("een ScoredMove is alleen met een ScoredMove te vergelijken")
return self.score < other.score
a = ScoredMove(2, 0)
b = ScoredMove(4, 0)
c = ScoredMove(5, 100)
assert a.col == 2
assert a.score == 0
assert repr(c) == "ScoredMove(5, 100)"
assert a == b
assert a is not b
assert not a == c
assert not a == 0
assert a < c
assert not a < b
assert c > a
assert min([c, a, b]).col == 2
assert max([a, c, b]).col == 5
Stap 3 tot en met 7: de klasse MinimaxPlayer¶
De klasse in één keer. scored_moves is het cadeau uit stap 5. In scores_for
staan de gevallen in de volgorde van de opgave: een volle kolom, een bord waarop
al iemand gewonnen heeft of ply 0, en anders een zet met daarna een
tegenstander met één ply minder. best van die tegenstander kiest voor hem: het
minimum als hij X speelt, het maximum als hij O speelt.
Van Board gebruikt de speler alleen width, allows_move, add_move,
del_move, is_full en score.
class MinimaxPlayer(Player):
"""Een computerspeler die ply zetten vooruitkijkt, met min-max."""
def __init__(self, ox, tbt, ply):
"""Maak een speler met steen ox, keuzestrategie tbt en ply zetten vooruit."""
super().__init__(ox)
self.tbt = tbt
self.ply = ply
def best(self, moves):
"""Geeft de beste zet uit moves: voor X de laagste, voor O de hoogste."""
if self.ox == "X":
return min(moves)
return max(moves)
def scores_for(self, board):
"""Geeft per kolom de waarde van het bord na een zet daar, of None als het niet mag."""
scores = []
for col in range(board.width):
if not board.allows_move(col):
scores.append(None)
elif board.score() != 0 or self.ply == 0:
scores.append(board.score())
else:
board.add_move(col, self.ox)
if board.score() != 0 or board.is_full():
scores.append(board.score())
else:
opponent = MinimaxPlayer(self.opponent(), self.tbt, self.ply - 1)
scores.append(opponent.best(opponent.scored_moves(board)).score)
board.del_move(col)
return scores
def scored_moves(self, board):
"""Geeft een ScoredMove voor elke kolom waarin een zet mag."""
scores = self.scores_for(board)
moves = []
for col in range(board.width):
if scores[col] is not None:
moves.append(ScoredMove(col, scores[col]))
return moves
def tiebreak_move(self, moves):
"""Kiest uit moves, van links naar rechts, een kolom volgens self.tbt."""
if self.tbt == "LEFT":
return moves[0].col
if self.tbt == "RIGHT":
return moves[-1].col
return random.choice(moves).col
def next_move(self, board):
"""Geeft de kolom van de beste zet; bij gelijke waarden beslist tbt."""
moves = self.scored_moves(board)
best = self.best(moves)
equal = []
for move in moves:
if move == best:
equal.append(move)
return self.tiebreak_move(equal)
Stap 3: de constructor en best¶
p = MinimaxPlayer("X", "LEFT", 2)
assert p.ox == "X"
assert p.tbt == "LEFT"
assert p.ply == 2
assert p.opponent() == "O"
moves = [ScoredMove(0, 100), ScoredMove(1, -100), ScoredMove(2, 0)]
assert p.best(moves).col == 1
assert MinimaxPlayer("O", "LEFT", 2).best(moves).col == 0
Stap 4: tiebreak_move(self, moves)¶
"RANDOM" wordt hier alleen getoetst met één zet, want dan ligt de uitkomst
vast.
moves = [ScoredMove(2, 0), ScoredMove(4, 0), ScoredMove(5, 0)]
assert MinimaxPlayer("X", "LEFT", 1).tiebreak_move(moves) == 2
assert MinimaxPlayer("X", "RIGHT", 1).tiebreak_move(moves) == 5
assert MinimaxPlayer("O", "RANDOM", 1).tiebreak_move([ScoredMove(3, 0)]) == 3
Stap 6: scores_for(self, board)¶
Stap 5 is het cadeau scored_moves in de klasse hierboven. De tests van
scores_for staan in twee cellen, omdat ply 4 de meeste tijd kost.
b = Board(7, 6)
b.set_board("1211244445")
before = repr(b)
assert MinimaxPlayer("X", "LEFT", 0).scores_for(b) == [0, 0, 0, 0, 0, 0, 0]
assert MinimaxPlayer("O", "LEFT", 1).scores_for(b) == [0, 0, 0, 100, 0, 0, 0]
assert MinimaxPlayer("X", "LEFT", 2).scores_for(b) == [100, 100, 100, 0, 100, 100, 100]
assert MinimaxPlayer("X", "LEFT", 3).scores_for(b) == [
100,
100,
100,
-100,
100,
100,
100,
]
assert MinimaxPlayer("O", "LEFT", 3).scores_for(b) == [0, 0, 0, 100, 0, 0, 0]
assert repr(b) == before
b = Board(7, 6)
b.set_board("1211244445")
before = repr(b)
assert MinimaxPlayer("O", "LEFT", 4).scores_for(b) == [
-100,
-100,
-100,
100,
-100,
-100,
-100,
]
assert repr(b) == before
b = Board(7, 6)
b.set_board("000000")
assert MinimaxPlayer("X", "LEFT", 1).scores_for(b) == [None, 0, 0, 0, 0, 0, 0]
Stap 7: next_move(self, board)¶
move == best gebruikt __eq__ van ScoredMove, en vindt dus alle zetten met de
beste waarde. Met move is best zou alleen de zet overblijven die best
teruggaf, en had tbt niets meer te kiezen.
b = Board(7, 6)
b.set_board("1211244445")
assert MinimaxPlayer("X", "LEFT", 1).next_move(b) == 0
assert MinimaxPlayer("X", "RIGHT", 1).next_move(b) == 6
assert MinimaxPlayer("X", "LEFT", 2).next_move(b) == 3
assert MinimaxPlayer("X", "RIGHT", 2).next_move(b) == 3
assert MinimaxPlayer("X", "RANDOM", 2).next_move(b) == 3
Stap 8: spelen¶
Op het bord van de val kiest de eenvoudige speler kolom 0, net als min-max met
ply 3. Pas met ply 4 blokkeert O in kolom 1.
b = Board(7, 6)
b.set_board("203")
assert SimpleAIPlayer("O").next_move(b) == 0
assert MinimaxPlayer("O", "LEFT", 3).next_move(b) == 0
assert MinimaxPlayer("O", "LEFT", 4).next_move(b) == 1
b = Board(7, 6)
b.set_board("20314")
assert MinimaxPlayer("O", "LEFT", 4).next_move(b) == 5
b.add_move(5, "O")
assert b.cols_to_win("X") == []
Een heel spel. De eerste zetten van O zijn allemaal de meest linkse kolom: zolang
niemand binnen drie zetten kan winnen, is elke kolom 0 waard.
b = Board(7, 6)
b.host_game(SimpleAIPlayer("X"), MinimaxPlayer("O", "LEFT", 3))
assert b.wins_for("O")
Welkom bij Vier op een rij!
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
---------------
0 1 2 3 4 5 6
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
|X| | | | | | |
---------------
0 1 2 3 4 5 6
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
|O| | | | | | |
|X| | | | | | |
---------------
0 1 2 3 4 5 6
| | | | | | | |
| | | | | | | |
| | | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | | | | | |
---------------
0 1 2 3 4 5 6
| | | | | | | |
| | | | | | | |
|O| | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | | | | | |
---------------
0 1 2 3 4 5 6
| | | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | | | | | |
---------------
0 1 2 3 4 5 6
|O| | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | | | | | |
---------------
0 1 2 3 4 5 6
|O| | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | | | | | |
|O| | | | | | |
|X|X| | | | | |
---------------
0 1 2 3 4 5 6
|O| | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | | | | | |
|O|O| | | | | |
|X|X| | | | | |
---------------
0 1 2 3 4 5 6
|O| | | | | | |
|X| | | | | | |
|O| | | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
---------------
0 1 2 3 4 5 6
|O| | | | | | |
|X| | | | | | |
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
---------------
0 1 2 3 4 5 6
|O| | | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
---------------
0 1 2 3 4 5 6
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
---------------
0 1 2 3 4 5 6
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X|X| | | | |
---------------
0 1 2 3 4 5 6
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X|X|O| | | |
---------------
0 1 2 3 4 5 6
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
|O|O|X| | | | |
|X|X|X|O| | | |
---------------
0 1 2 3 4 5 6
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|X|O| | | |
---------------
0 1 2 3 4 5 6
|O|O| | | | | |
|X|X| | | | | |
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|X|O| | | |
---------------
0 1 2 3 4 5 6
|O|O| | | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|X|O| | | |
---------------
0 1 2 3 4 5 6
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|X|O| | | |
---------------
0 1 2 3 4 5 6
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X|O| | | |
|X|X|X|O| | | |
---------------
0 1 2 3 4 5 6
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X|O| | | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X| | | | |
|X|X|O|O| | | |
|O|O|X|O| | | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X| | | | |
|X|X|O| | | | |
|O|O|X|X| | | |
|X|X|O|O| | | |
|O|O|X|O| | | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X| | | | |
|X|X|O|O| | | |
|O|O|X|X| | | |
|X|X|O|O| | | |
|O|O|X|O| | | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X|X| | | |
|X|X|O|O| | | |
|O|O|X|X| | | |
|X|X|O|O| | | |
|O|O|X|O| | | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X|X| | | |
|X|X|O|O| | | |
|O|O|X|X| | | |
|X|X|O|O| | | |
|O|O|X|O|O| | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X|X| | | |
|X|X|O|O| | | |
|O|O|X|X| | | |
|X|X|O|O|X| | |
|O|O|X|O|O| | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X|X| | | |
|X|X|O|O| | | |
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|O|O| | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X|X| | | |
|X|X|O|O|X| | |
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|O|O| | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|O|O| | |
|X|X|X|O|X| | |
---------------
0 1 2 3 4 5 6
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|O|O| | |
|X|X|X|O|X|X| |
---------------
0 1 2 3 4 5 6
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|O|O|O| |
|X|X|X|O|X|X| |
---------------
0 1 2 3 4 5 6
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|X|O| | |
|X|X|O|O|X|X| |
|O|O|X|O|O|O| |
|X|X|X|O|X|X| |
---------------
0 1 2 3 4 5 6
|O|O|X|X|O| | |
|X|X|O|O|X| | |
|O|O|X|X|O|O| |
|X|X|O|O|X|X| |
|O|O|X|O|O|O| |
|X|X|X|O|X|X| |
---------------
0 1 2 3 4 5 6
|O|O|X|X|O| | |
|X|X|O|O|X|X| |
|O|O|X|X|O|O| |
|X|X|O|O|X|X| |
|O|O|X|O|O|O| |
|X|X|X|O|X|X| |
---------------
0 1 2 3 4 5 6
|O|O|X|X|O|O| |
|X|X|O|O|X|X| |
|O|O|X|X|O|O| |
|X|X|O|O|X|X| |
|O|O|X|O|O|O| |
|X|X|X|O|X|X| |
---------------
0 1 2 3 4 5 6
|O|O|X|X|O|O| |
|X|X|O|O|X|X| |
|O|O|X|X|O|O| |
|X|X|O|O|X|X| |
|O|O|X|O|O|O| |
|X|X|X|O|X|X|X|
---------------
0 1 2 3 4 5 6
|O|O|X|X|O|O| |
|X|X|O|O|X|X| |
|O|O|X|X|O|O| |
|X|X|O|O|X|X| |
|O|O|X|O|O|O|O|
|X|X|X|O|X|X|X|
---------------
0 1 2 3 4 5 6
O wint -- Gefeliciteerd!
Tegen de computer spelen met HumanPlayer vraagt invoer, en staat hier daarom
niet. Speel het zelf, in je eigen bestand.