Extra

Deze pagina bevat uitvoerbare code.

Vier op een rij: de klasse Board

Opgave: Vier op een rij: de klasse Board

De klasse hieronder bevat alle methoden van stap 1 tot en met 10. Voor wins_for gebruikt deze uitwerking geen losse in-een-rij-functies maar één hulpmethode, in_a_row, die vanaf een vakje vier stappen in een richting controleert. De richting is een verschil in rij en kolom: (0, 1) is naar rechts, (1, 0) naar beneden, en (1, 1) en (-1, 1) zijn de twee diagonalen. Zo zijn de vier richtingen één stuk code.

De opgave zegt dat je de in-een-rij-functies van Programmeren 1 buiten de klasse zet. Dat zijn gewone functies die een lijst van lijsten meekrijgen. in_a_row is anders gemaakt: ze leest self._data en de afmetingen van het bord zelf, en werkt dus alleen voor een Board. Daarom is ze hier een methode.

host_game vraagt om invoer met input en wordt hier daarom niet uitgevoerd.

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 host_game(self):
        """Laat twee mensen Vier op een rij spelen, om de beurt."""
        print("Welkom bij Vier op een rij!")
        print()
        print(self)
        print()
        ox = "X"
        while True:
            users_col = -1
            while not self.allows_move(users_col):
                users_col = int(input(f"Keuze van {ox}: "))
            self.add_move(users_col, ox)
            print()
            print(self)
            print()
            if self.wins_for(ox):
                print(f"{ox} wint -- Gefeliciteerd!")
                break
            if self.is_full():
                print("Gelijkspel!")
                break
            if ox == "X":
                ox = "O"
            else:
                ox = "X"

Stap 1: de constructor __init__(self, width, height)

Breedte en hoogte zijn van buiten te lezen, via de properties.

b = Board(7, 6)
assert b.width == 7
assert b.height == 6

Maar niet te veranderen:

b.width = 8
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 1
----> 1 b.width = 8

AttributeError: property 'width' of 'Board' object has no setter

Stap 2: __repr__(self)

print(Board(7, 6))
print(Board(15, 5))
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
---------------
 0 1 2 3 4 5 6
| | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | | |
-------------------------------
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4

Stap 3: add_move(self, col, ox)

De lus loopt van de onderste rij naar boven en zet de steen in het eerste lege vakje.

b = Board(7, 6)
b.add_move(0, "X")
b.add_move(0, "O")
b.add_move(0, "X")
b.add_move(3, "O")
b.add_move(4, "O")
b.add_move(5, "O")
b.add_move(6, "O")
print(b)
| | | | | | | |
| | | | | | | |
| | | | | | | |
|X| | | | | | |
|O| | | | | | |
|X| | |O|O|O|O|
---------------
 0 1 2 3 4 5 6

Stap 4: clear(self)

b.clear()
print(b)
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
---------------
 0 1 2 3 4 5 6

Stap 5: set_board(self, move_string)

De conditie is 0 <= col < self.width. Met <= zou een cijfer gelijk aan de breedte doorgelaten worden, en dan valt add_move buiten het bord.

b = Board(7, 6)
b.set_board("7")
print(b)
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
---------------
 0 1 2 3 4 5 6

Stap 6: allows_move(self, col)

Een kolom heeft nog ruimte zolang het bovenste vakje, in rij 0, leeg is.

b = Board(2, 2)
b.add_move(0, "X")
b.add_move(0, "O")
print(b)
assert not b.allows_move(-1)
assert not b.allows_move(0)
assert b.allows_move(1)
assert not b.allows_move(2)
|O| |
|X| |
-----
 0 1

Stap 7: is_full(self)

b = Board(2, 2)
assert not b.is_full()
b.set_board("0011")
print(b)
assert b.is_full()
|O|O|
|X|X|
-----
 0 1

Stap 8: del_move(self, col)

De lus loopt nu van boven naar beneden en maakt het eerste vakje met een steen leeg.

b = Board(2, 2)
b.set_board("0011")
b.del_move(1)
b.del_move(1)
b.del_move(1)
b.del_move(0)
print(b)
| | |
|X| |
-----
 0 1

Stap 9: wins_for(self, ox)

b = Board(7, 6)
b.set_board("00102030")
assert b.wins_for("X")
assert b.wins_for("O")

b = Board(7, 6)
b.set_board("23344545515")
print(b)
assert b.wins_for("X")
assert not b.wins_for("O")
| | | | | | | |
| | | | | | | |
| | | | | |X| |
| | | | |X|X| |
| | | |X|X|O| |
| |O|X|O|O|O| |
---------------
 0 1 2 3 4 5 6

Stap 10: host_game(self)

De methode staat in de klasse hierboven. Speel haar zelf, in je eigen bestand.