Zwei-Personen-Spiele und automatisches Spiel

Im folgenden betrachten wir Spiele, in denen zwei Spieler abwechselnd ziehen, (anders als bei vielen Kartenspielen) keine Information geheim bleibt und (anders als bei Würfelspielen) der Zufall keine Rolle spielt. Beispiele für solche Spiele sind Schach, Dame, Reversi, Tic Tac Toe oder Vier Gewinnt.

Wir werden Klassen definieren, die es erlauben Zwei-Personen-Spiele darzustellen und Klassen, die es erlauben, automatische Spieler für solche Spiele zu definieren. Da die Algorithmen unabhängig von den konkreten Spielen definiert werden können, trennen wir die Definition von Spielern von der Definition von Spielen.

Spieler für Zwei-Personen-Spiele sind Objekte der Klasse Player.

class Player:
    def __init__(self, name):
        self.name = name

    def set_game(self, game):
        self.game = game

    def __str__(self):
        return self.name

Zur Darstellung auf dem Bildschirm geben wir Spielern einen Namen, der im Konstruktor übergeben wird. Unterklassen der Klasse Player sollen eine Methode select_move implementieren, die einen ausgewählten Zug im Spiel zurückliefert, dass im Attribut game gespeichert ist. Unterklassen der Klasse Game sollen dazu eine Methode valid_moves definieren, die ein Array gültiger Züge liefert, aus dem Spieler wählen können.

Zwei-Personen-Spiele sind Objekte der Klasse Game:

class Game:
    def __init__(self, player1, player2):
        player1.set_game(self)
        player2.set_game(self)
        self.current_player = player1
        self.waiting_player = player2

    # weitere Definitionen folgen

Objekten der Klasse Game werden im Konstruktor zwei Spieler übergeben. Der zuerst übergebene Spieler beginnt das Spiel, und nach seinem Zug wechselt das Zugrecht. Dazu vertauscht die Methode next_turn die Rollen beider Spieler.

    def next_turn(self):
        player = self.current_player
        self.current_player = self.waiting_player
        self.waiting_player = player

Die Methode play wird aufgerufen um ein Spiel zu starten und seine Durchführung auf dem Bildschirm auszugeben.

    def play(self):
        while not self.has_ended():
            print(self)
            move = self.current_player.select_move()
            self.make_move(move)
            self.next_turn()
        print(self)

In jedem Schritt wird ein von dem Spieler, der an der Reihe ist, ausgewählter Zug ausgeführt, bis das Spiel beendet ist. Dazu müssen die Methoden has_ended und make_move von Unterklassen der Klasse Game implementiert werden.

Vor und nach jedem Zug wird das Spiel auf dem Bildschirm ausgegeben. Die folgende Methode gibt eine Zeichenkette zurück, die den Zustand des Spiels beschreibt.

    def __str__(self):
        if self.has_ended():
            w = self.winner()
            if w == None:
                return "draw"
            return str(w) + " won"
        return str(self.current_player) + "'s turn"

Ist das Spiel beendet, so wird mit Hilfe der Methode winner ermittelt, wer gewonnen hat, und das Ergebnis zurück geliefert. Auch diese Methode muss also in Unterklassen implementiert werden. Läuft das Spiel noch, liefert __str__ zurück, wer an der Reihe ist.

Die gezeigten Definitionen der Klassen Player und Game speichern wir in two_player_games.py, um sie zur Definition konkreter Spiele und Spieler in anderen Dateien verwenden zu können.

Als Beispiel für ein einfaches Zwei-Personen-Spiel implementieren eine einfache Version des Nim-Spiel’s als Unterklasse von Game.

class SimpleNim(Game):
    def __init__(self, player1, player2, count):
        super().__init__(player1, player2)
        self.count = count

    # weitere Definitionen folgen

Das Nim-Spiel wird mit einem Haufen Streichhölzern gespielt, dessen Größe im Konstruktor übergeben wird. Die Methode __str__ gibt die Anzahl der Streichhölzer neben dem Spielzustand zurück, den die überschriebene Methode der Oberklasse liefert.

    def __str__(self):
        return str(self.count) + "\tmatches, " + super().__str__()

Die Spieler nehmen abwechselnd Streichhölzer vom Haufen, bis keine mehr da sind.

    def has_ended(self):
        return self.count == 0

Die Methode make_move entfernt so viele Streichhölzer vom Haufen, wie im übergebenen Zug angegeben sind.

    def make_move(self, number):
        self.count = self.count - number

Wer das letzte Streichholz nimmt, verliert das Spiel. Es gewinnt also der Spieler, der bei Spielende an der Reihe ist.

    def winner(self):
        return self.current_player

Ein gültiger Zug entfernt ein bis drei Streichhölzer vom Haufen, sofern noch so viele dort liegen. Die Höchstzahl zu entfernender Streichhölzer wird mit der Methode min der Klasse Array berechnet.

    def valid_moves(self):
        moves = []
        for number in range(1, 1 + min(3, self.count)):
            moves.append(number)
        return moves

Um unsere Implementierung zu testen, definieren wir noch eine Klasse für Spieler, die in jedem Zug einen zufälligen der gültigen Züge auswählen.

from random import shuffle

class RandomPlayer(Player):
    def select_move(self):
        moves = self.game.valid_moves()
        shuffle(moves)
        return moves[0]

Wir können nun ein Spiel zwischen Zufallsspielern starten und den Verlauf beobachten.

>>> alice = RandomPlayer("Alice")
>>> bob = RandomPlayer("Bob")
>>> SimpleNim(alice,bob,21).play()
21      matches, Alice's turn
19      matches, Bob's turn
18      matches, Alice's turn
16      matches, Bob's turn
15      matches, Alice's turn
12      matches, Bob's turn
10      matches, Alice's turn
9       matches, Bob's turn
8       matches, Alice's turn
6       matches, Bob's turn
5       matches, Alice's turn
3       matches, Bob's turn
2       matches, Alice's turn
1       matches, Bob's turn
0       matches, Alice won