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