Skip to main content

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) const zum Vergleichen zweier Farbreihen.
  • int Row::countColorMatches(const Row &other) const zum Zählen der Pins, die der Farbe nach (aber nicht der Position nach) stimmen.
  • int Row::countExactMatches(const Row &other) const zum Zählen der Pins, die richtig platziert sind.
  • void check(const Row &solution) zum Prüfen der richtigen Pins. Diese Funktion soll die Ergebnisse der count*-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 Operator std::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 Funktion Row.check auf, um festzustellen, wie viele Pins vollständig richtig sind und wie viele Pins von einer vorkommenden Farbe sind.
  • bool Board::isSolved() cost gibt zurück, ob der letzte Lösungsversuch die gesuchte Lösung enthalten hat.
  • bool Board::noMoreMoves() const gibt zurück, ob die maximale Anzahl an Versuchen gemacht wurde.
  • void Board::print() const gibt das gesamte Spielbrett auf der Konsole aus. Sie können auch den Operator std::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 Parameter isHit (der Schuss ist ein Treffer) und isDestroyed (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() const gibt 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) const prüft, ob das übergebene Schiff mit einem der bestehenden Schiffe kollidiert. Sollte dies der Fall sein, gibt die Funktion truezurück.
  • virtual void Player::placeShip(const int length) = 0 ist 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) ruft Ship::calculateHit für jedes Schiff des Spielers auf.
  • bool Player::hasLost() const gibt true zurück, wenn alle Schiffe versenkt wurden.
  • virtual void Player::makeGuess() = 0 rät die Position eines Schiffs.
  • void Player::placeShips() platziert alle Schiffe, indem es für jedes Schiff (mit der entsprechenden Länge) die Funktion Player::placeShip aufruft.

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.