Extra

Deze pagina bevat uitvoerbare code.

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.