In diesem Aufgabenblock werden wir mit Objektorientierung arbeiten. Hierfür werden wir einfache Spiele implementieren. Beide Aufgaben können natürlich um eine Ausgabe des Spielfelds als Grafik erweitert werden, um sich den aktuellen Spielstand besser anschauen zu können.
4.1 Mastermind
Ziel des Spiels Mastermind ist es, eine zufällig gewählte Reihenfolge von farbigen Spielsteinen (Pins) in möglichst wenigen Zügen zu erraten. Der erste Spieler wählt vier farbige Spielsteine aus. Insgesamt gibt es sechs verschiedene Farben. Der zweite Spieler hat nun die Aufgabe, die Farbreihenfolge richtig zu raten. Wenn der zweite Spieler eine Reihenfolge eingegeben hat, dann prüft der erste Spieler, wie viele Steine mit der richtigen Farbe an der richtigen Position sind und wie viele Steine die richtige Farbe haben, aber an der falschen Position sind. Mit diesem Wissen kann der zweite Spieler seinen nächsten Zug planen. Insgesamt hat der zweite Spieler zwölf Züge, um die Farbreihenfolge zu erraten.
In unserer Implementierung soll der Computer die Aufgabe übernehmen, zufällig eine Farbreihenfolge zu wählen, und der menschliche Spieler soll diese Reihenfolge erraten.
Vereinfachung: Damit wir nicht unterschiedliche Farben in unserem Spiel codieren müssen, werden wir anstelle von unterschiedlichen Farben einfach unterschiedliche Zahlen nehmen. Trotz der Vereinfachung werden wir im Folgenden von Farben sprechen. Ein beispielhafter Programmablauf sieht wie folgt aus:
> mastermind.exe
Der aktuelle Spielstand:
(leer)
Bitte gib einen Versuch ein: 1 3 2 5
Der aktuelle Spielstand:
1 3 2 5 (0/3)
Bitte gib einen Versuch ein: 4 3 2 5
Der aktuelle Spielstand:
1 3 2 5 (0/3)
4 3 2 5 (0/4)
Lösungshinweise: Implementieren Sie das Programm Klasse für Klasse und testen Sie
Ihre bisherige Implementierung immer aus der main-Funktion.
Klasse Farbreihe (class Row): Die erste Klasse bildet eine Farbreihe ab. Diese soll den Farbcode von vier Pins und die Anzahl der richtig platzierten Pins und die Anzahl der Pins mit richtiger Farbe in Klassen-Variablen speichern können. Abhängig von Ihrer Implementierung werden Sie Konstruktoren brauchen, die eine Farbreihe mit übergebenen Werten initialisert. Zudem braucht Ihre Klasse die folgenden Funktionen:
bool Row::operator==(const Row &other) constzum Vergleichen zweier Farbreihen.int Row::countColorMatches(const Row &other) constzum Zählen der Pins, die der Farbe nach (aber nicht der Position nach) stimmen.int Row::countExactMatches(const Row &other) constzum Zählen der Pins, die richtig platziert sind.void check(const Row &solution)zum Prüfen der richtigen Pins. Diese Funktion soll die Ergebnisse dercount*-Funktionen in den entsprechenden Member-Variablen speichern.void Row::print() constzum Ausgeben einer Farbreihe inkl. der Anzahl der richtigen Pins und Farben. Sie können alternativ auch den Operatorstd::ostream &operator<<(std::ostream &stream, const Row &row)implementieren.
Klasse Spielfeld (class Board): Die zweite Klasse bildet das Spielfeld ab und speichert die gesuchte Lösung und alle durchgeführten Spielzüge ab. Sie enthält somit die gesamte Historie der Züge. Neben Funktionen zum Setzen und Auslesen der gesuchten Lösung benötigt diese Klasse noch folgende Funktionen:
void Board::addGuess(const Row &guess)zum Hinzufügen eines Lösungsversuchs zum Spielbrett. Diese Funktion ruft beim Hinzufügen die FunktionRow.checkauf, um festzustellen, wie viele Pins vollständig richtig sind und wie viele Pins von einer vorkommenden Farbe sind.bool Board::isSolved() costgibt zurück, ob der letzte Lösungsversuch die gesuchte Lösung enthalten hat.bool Board::noMoreMoves() constgibt zurück, ob die maximale Anzahl an Versuchen gemacht wurde.void Board::print() constgibt das gesamte Spielbrett auf der Konsole aus. Sie können auch den Operatorstd::ostream &operator<<(std::ostream &stream, const Board &board)implementieren.
Klasse Spieler (class Player): Die letzte Klasse bildet einen Spieler ab und erhält in ihrem Konstruktor einen Zeiger auf das Spielfeld. Diese Klasse braucht zwei Funktionen:
void Player::chooseSolution()wählt zufällig vier Farben aus, erzeugt eine Farbreihe und setzt diese als gewünschte Lösung im Spielfeld.void Player::makeGuess()liest vom Terminal eine mögliche Lösung ein und validiert die Eingabe. Anschließend erzeugt sie eine Farbreihe und fügt die geratene Reihenfolge an das Spielfeld an.
Die int main()-Funktion initialisiert den Zufallszahlengenerator und legt ein
Spielbrett und zwei Spieler auf dem Stack an. Der Computer-Spieler erzeugt zunächst
eine Lösung und anschließend darf der menschliche Spieler mögliche Farbkombinationen
raten, bis das Spiel gelöst wurde oder alle zwölf Züge aufgebraucht sind. In jeder Runde
soll das Spielfeld ausgegeben werden, damit der Spieler seine bisherigen Eingaben sehen
kann. Nachdem eines der beiden Abbruchkriterien zutrifft, wird ausgegeben, ob das Spiel
gewonnen oder verloren wurde.
4.2 Schiffe versenken
Ein weiterer Spielklassiker, den Sie programmieren sollen, ist das Spiel Schiffe versenken. Bei diesem Spiel gibt es zwei Spieler, die zu Beginn Schiffe unterschiedlicher Länge (zum Beispiel ein Schiff der Länge fünf, drei Schiffe der Länge drei und fünf Schiffe der Länge eins) auf dem eigenen Spielfeld platzieren. Das Spielfeld ist quadratisch und besteht aus zehn mal zehn Feldern. Die platzierten Schiffe dürfen sich auf dem Spielfeld nicht berühren. Nachdem die Schiffe platziert wurden, feuern die Spieler abwechselnd auf das gegnerische Spielfeld und bekommen vom Gegner die Information, ob sie ein Schiff getroffen oder versenkt haben. Sollte ein Spieler ein gegnerisches Schiff getroffen haben, darf er noch einen weiteren Schuss versuchen. Ziel des Spieles ist, dass man alle gegnerischen Schiffe versenkt. Für die Implementierung ist es wiederum sinnvoll, einen objektorientierten Entwurf zu verwenden und dabei auch Vererbung und virtuelle Funktionen zu verwenden.
Lösungshinweise: Implementieren Sie das Programm Klasse für Klasse und
testen Sie Ihre bisherige Lösung immer aus der main-Funktion.
Die Klasse Coordinate ist eine Koordinate auf dem Spielfeld und besteht
aus den Member-Variablen x und y und sinnvollen zusätzlichen
Funktionen und Operatoren.
Die Klasse Ship modelliert ein Schiff und speichert folgende Informationen
in Member-Variablen:
- Die Start-Koordinate des Schiffs
- Die Länge des Schiffs
- Die Ausrichtung des Schiffes (horizontal, vertikal)
- Eine Liste von Schiffsegmenten, die bereits getroffen wurden
Basierend auf diesen Information müssen folgende Funktionen implementiert werden:
void Ship::calculateHit(const Coordinate &coordinate, bool &isHit, bool &isDestroyed): Überprüft, ob ein Schuss an die übergebene Koordinate ein Segment des Schiffs trifft. Entsprechend der Berechnung werden die ParameterisHit(der Schuss ist ein Treffer) undisDestroyed(der Schuss hat das Schiff versenkt) gesetzt.bool Ship::collides(const Ship &other) constüberprüft, ob die beiden Schiffe kollidieren. Zwei Schiffe kollidieren, wenn sie nebeneinander auf dem Spielfeld liegen. Sie können dies prüfen, indem Sie für jedes Segment des ersten Schiffs die Differenz zu jeder Segment-Koordinate des zweiten Schiffs berechnen. Ist die Differenz entweder der X-Komponenten oder der Y-Komponente (nicht beider Komponenten gleichzeitig) kleiner oder gleich eins, so sind sich die Schiffe zu nah.bool Ship::isSunk() constgibt zurück, ob das Schiff gesunken (alle Segmente des Schiffs wurden getroffen) ist.
Die Klasse Board dient dazu, die durchgeführten Schüsse zu
protokollieren. Dazu hat die Klasse ein zwei-dimensionales
Feld in der Größe des Spielfelds. Zu Anfang des Spiels
wird in allen Einträgen eingetragen, dass dieses Feld noch
nicht gewählt wurde. Wurde ein Spielfeld ausgewählt, wird in
ihm eine 0 oder eine 1 gespeichert. Der Wert hängt davon ab,
ob dort ein Schiff getroffen wurde. Zudem ist es sinnvoll,
die Funktion std::ostream &operator(std::ostream &stream, const Board &board) zu implementieren, um das Spielfeld
auf der Konsole auszugeben.
Die Klasse Player ist die Basisklasse der Klassen HumanPlayer und ComputerPlayer. Ein Spieler,
ein Spielfeld, einen Zeiger auf den Gegner und eine Liste
der Schiffe, die ihm gehörten.
bool Player::checkPlacing(const Ship &ship) constprüft, ob das übergebene Schiff mit einem der bestehenden Schiffe kollidiert. Sollte dies der Fall sein, gibt die Funktiontruezurück.virtual void Player::placeShip(const int length) = 0ist eine virtuelle Funktion, die aufgerufen wird, um ein Schiff der übergebenen Länge auf dem Spielfeld zu platzieren.void Player::calculateHit(const Coordinate &coordiante, bool &hit, bool &destroyed)ruftShip::calculateHitfür jedes Schiff des Spielers auf.bool Player::hasLost() constgibttruezurück, wenn alle Schiffe versenkt wurden.virtual void Player::makeGuess() = 0rät die Position eines Schiffs.void Player::placeShips()platziert alle Schiffe, indem es für jedes Schiff (mit der entsprechenden Länge) die FunktionPlayer::placeShipaufruft.
Die Klasse HumanPlayer implementiert die Funktionen makeGuess und placeShip. Dazu fragt es die entsprechenden Informationen vom Nutzer des
Programms ab. Die Funktion HumanPlayer::placeShip verwendet die
Funktion Player::checkPlacing, um zu prüfen, ob die eingegebene Position
gültig ist. Sollte die Platzierung ungültig sein, so werden die Informationen
für das Schiff erneut abgefragt. Die Funktion HumanPlayer::makeGuess zeigt
dem Nutzer erst die bisher geratenen Felder an, um dann seine Auswahl zu treffen.
Die Klasse ComputerPlayer implementiert ebenfalls die Funktionen makeGuess und placeShip. In beiden Fällen wird aber der Zufall
verwendet, um die Positionen und die Ausrichtung zu wählen.
Die Funktion int main() erzeugt einen menschlichen Spieler und einen
Computerspieler. Anschließend werden beide Spiele aufgefordert, Ihre Schiffe zu
platzieren. Dann fordert die Funktion beide Spieler abwechselnd auf, ein
Feld zu raten (Player::makeGuess()). Sollte der gegnerische Spieler nach
dem Zug verloren haben (Player::hasLost()), wird das Spiel beendet.
Ergänzung: Sie können natürlich auch die Hauptfunktionalität noch in eine
eigene Klasse Battleship mit einer void Battleship::run() auslagern,
die von der main-Funktion verwendet wird.
Erweiterung: Der Algorithmus des Computerspielers wählt seine Schüsse nur zufällig aus. Sie können weitere Computerspieler implementieren, die unterschiedliche (bessere) Algorithmen implementieren. Sie können jeweils zwei Algorithmen gegeneinander antreten lassen und zählen, wie häufig die jeweiligen Algorithmen gewinnen. Wenn Sie das Spiel als Gruppe implementiert haben, dann kann jeder eigene Computerspieler implementieren und ein Gruppentunier veranstalten.