Schiffe versenken

Aus HRW FabLab MediaWiki
Wechseln zu: Navigation, Suche

Das Spiel Schiffe versenken soll es ermöglichen einen gemeinsamen Spieleabend mit Freunden und Familie zu machen, da dies aufgrund der aktuellen Pandemiesituation nur eingeschränkt möglich ist.

Die Entwickler des Spiels sind Murat Turcan, Tayyib Iric, Julien Schminke und Lisa-Marie Emidio Raimundo.

Schiffe versenken

Schiffe versenken.png

Entwickler

Murat Turcan, Tayyib Iric, Julien Schminke, Lisa-Marie Emidio Raimundo

Verwendete Programmiersprache

Arduino, C++

Projekt[Bearbeiten]

Ziel[Bearbeiten]

„Entwicklung von verteilten Eingebetteten Systemen zur (sozialen) Interaktion während der Corona-Zeit“.

Projektidee[Bearbeiten]

In Zeiten des Coronavirus sind die Kontaktbeschränkungen ein großes Problem. Da die Menschen sich untereinander nur eingeschränkt sehen können, fallen natürlich auch die gemeinsamen Spieleabende weg. Damit soll mit diesem Projekt Schluss sein. Das Konzept besteht daraus, dass nur eine Hardware gekauft werden muss, der Rest der Gruppe kann per Internet beitreten.
Da Schiffe versenken ein älteres Spiel ist, ist es mit Sicherheit auch den Großeltern und Eltern bekannt.

Umsetzung[Bearbeiten]

Übersicht der verwendeten Hardware[Bearbeiten]

Aufbau Hardware
Übersicht
Breadboard 1x
Taster 4x
200 Ohm-Widerstände 4x
WS2812B LED Streifen 1x
Male-Male Kabel 9x
Male-Female Kabel 6x
ESP32 TTGO 1x
5V Stecker 1x
Lötzinn -
Aufbau Knöpfe

Erklärung der Hardware[Bearbeiten]

Der LED Streifen ist zu einer 7x7 Matrix verlötet. Der Streifen hat 5V+, Digital(-write) und Ground. Der LED Streifen wurde dazu, nach jeder 7. LED getrennt und verlötet, wodurch die 7x7 Matrix entsteht.
Das ganze ist auf einer Platte befestigt, worunter das Breadboard liegt, auf denen sich die Knöpfe waagerecht, senkrecht, bestätigen und rotieren befinden. Die Knöpfe haben jeweils 200 Ohm Widerstände, das an Minus angeschlossen ist und jeweils eine Kabelverbindung zu Plus und zum ESP32.
Das Breadboard wird mit 5 Volt betrieben. Auf dem Bild erkennbar ist, dass das braune Kabel an Plus und das schwarze Kabel an Minus angeschlossen ist. Das Kabel für den senkrecht Knopf ist am ESP32 an Data 2 angeschlossen, gekennzeichnet durch das orangene Kabel. An 7 ist das grüne Kabel angeschlossen welches den Knopf für waagerecht verbindet. An 22 ist der Knopf für rotieren angeschlossen, hier sichtbar durch das braune Kabel und an 21 ist das weiße Kabel, welches den Knopf zum bestätigen verbindet. Ground auf Ground ist ebenfalls durch ein weißes Kabel gekennzeichnet. Die Matrix ist ebenfalls am Breadboard angeschlossen durch 5 Volt, Data und Ground.
Die eigenen Schiffe, der Angriff, sowie der Fehlschuss werden mit entsprechenden Farben gekennzeichnet.

Schaltplan

Erklärung der Software[Bearbeiten]

Beispiel GUI Spielverlauf. Für die Animation bitte auf das Bild klicken.

Es wird zwischen der Software der Spielregeln und der Software der GUI unterschieden. Zum Aufbau der GUI: Die GUI ist wie auch die Hardware Matrix 7x7 Felder groß. Auf der linken Seite der GUI befindet sich die Matrix und auf der rechten Seite die Knöpfe. Diese Knöpfe haben dieselbe Funktion wie bei der Hardware, also Vertikal oder Horizontal verschieben, Rotieren und Bestätigen/Angriff. Entsprechende Handlungen werden ebenfalls in bestimmten Farben ausgeführt.

Farben und Bedeutung
Platzierte Schiffe grün
Fehlschuss rot
getroffene Gegner gelb
eigene getroffene Schiffe orange


Nun zu den Spielregeln: Die Spielregeln sind der Kopf bei diesem Spiel. Sie kontrollieren und überprüfen erhaltene Befehle und Zustände. Zum Beispiel wird bei weniger als 2 Spielern eine Fehlermeldung ausgegeben.
Zuerst beginnt jedoch eine Vorbereitung. Diese Vorbereitung ist notwendig, um das Spiel überhaupt spielen zu können. Folgendes gilt:

  • Die Schiffe dürfen nicht aneinanderstoßen.
  • Die Schiffe dürfen nicht über Eck gebaut sein oder Ausbuchtungen besitzen.
  • Die Schiffe dürfen auch am Rand liegen.
  • Die Schiffe dürfen nicht diagonal aufgestellt werden.

Jeder verfügt über insgesamt 4 Schiffe (in Klammern die Größe):

  • ein Schlachtschiff (5 Kästchen)
  • ein U-Boot (je 4 Kästchen)
  • zwei Matrosenschiffe (je 3 Kästchen)

Name und Größe der Schiffe können dabei unterschiedlich sein – man sollte sich entsprechend mit dem Mitspieler einig werden. Der Host beginnt immer. Der Schießende gibt eine Koordinate an, auf die er feuert, zum Beispiel D3. Der Beschossene sieht auf seiner GUI und antwortet mit Fehlschuss (rot), Treffer (gelb) oder versenkt. Ein Schiff gilt als versenkt, wenn alle Felder des Schiffes getroffen wurden. Wenn das eigene Schiff getroffen worden ist, wird orange angezeigt. Wenn alle Schiffe versenkt worden sind, gibt die Software das Kommando für eine Niederlage.

Zusammenspiel[Bearbeiten]

Sobald der Host (Hardware) eingeschaltet ist, wird eine Verbindungsanfrage per MQTT (der reconnect code) dauerhaft geschickt. Sobald die GUI reagiert, beginnt schon das Spiel. Leider hat die Hardware nicht genau die Übersicht, wer wann dran ist, aber die Software schon. Eine Reihenfolge zwischen den Spielern bei mehr als 2 wird eingehalten. Danach kann man dank der Knöpfe die Position auswählen, die man beim Gegenüber angreifen möchte. Sobald man auf „bestätigen“ drückt, schaut man bei den Spielregeln, ob alles noch funktioniert, und dann wird der Befehl ausgeführt und geschaut, was sich dort befindet. Dann wird die entsprechende Farbe bei einem selbst angezeigt. Das Spiel geht so lange, bis es keine Schiffe gibt, also nach 15 erfolgreichen Treffern. Danach wird bei mehr als 2 Spielern eine fortgeführt und eine Platzierung angegeben, ansonsten bei 2 Spielern wird das Spiel beendet und haben somit ein Gewinner.

Code[Bearbeiten]

Um eine Runde zu starten wird der Konstruktor der Klasse Game aufgerufen. Dieser überprüft zunächst, ob mindestens zwei Spieler an der Partie teilnehmen. Wenn nicht, wird eine Fehlermeldung ausgegeben und abgebrochen.
Die Spieler werden daraufhin zwischengespeichert. Außerdem wird ihr Zähler für kaputte Schiffsteile (m_broken_ships_of_player[index des Spielers]) auf 0 gesetzt.
Als nächsten wird die Methode void start_placing() ausgeführt. Diese Methode ist für die Runden zuständig, in denen alle Spieler ihre Schiffe setzen können. Man beginnt mit dem größten Schiff (5er).
Sobald alle Schiffe platziert wurden, führt der Konstuktor, die Methode start_game() aus. In dieser Methode speichert die Variable attack_pos die Position des, für den Angriff, ausgewählten Feldes, beginnend mit dem Wert 0. In einer Endlosschleife, wird zunächst auf ein Kommando gewartet. Die Kommandos GoRight und GoDown tun dassselbe wie zuvor in start_placing(). Die Formeln sind allerdings kürzer, da sie nur eine Position bewegen müssen und nicht mehrere, wie es zuvor der Fall ist.

Game::Game(vector<Player*> player) {
        if(player.size() < 2) {
          cerr << "You need at least two Players!" << "endl;
          exit(1);
        }
        m_current_player = 0;
        m_players = player;
        for(unsigned long p = 0; p = 0; p < player.size(); p++) {
          m_broken_ships_of_players[p+1] = 0;
        }
        start_placing();
        start_game();
}

Als nächsten wird die Methode void start_placing() ausgeführt. Diese Methode ist für die Runden zuständig, in denen alle Spieler ihre Schiffe setzen können. Man beginnt mit dem größten Schiff (5er).
Zuerst wartet das Programm auf eine Eingabe. Diese erfolgt bei der GUI mit einem Klick auf einen Button sowie bei der Hardware mit einem Tastendruck. Sobald ein Kommando zurückkommt, wird überprüft, um welches Kommando es sich handelt.
Das erste Kommando ist GoRight. In diesem Falle wird das Schiff um eins nach rechts verschoben. Dafür geht es jedes Schiffsteil durch. Liegt es in der 6. Spalte - ship_position[i] % 7 == 6, also Position des Schiffsteils % Anzahl der Spalten == spaltenindex für die ganz rechte Spalte – dann wird das Schiffsteil zurück auf die ganz linke Spalte geschoben, indem von der aktuellen Position 7 (Anzahl der Spalten) abgezogen wird. Sollte sich das aktuelle Schiffsteil nicht in der letzten Spalte befinden, wird deren Position lediglich inkrementiert.
Das zweite Kommando ist GoDown. Auch hier werden alle Schiffsteile durchgegangen und es wird überprüft, ob sich das Schiffsteil in der untersten Reihe befindet (ship_position[i] >= 42, also Position des Schiffsteils >= Anzahl der Spalten * (Anzahl der Reihen-1)). Trifft dies zu, so wird das Schiffsteil in die erste Reihe verschoben. Deren Spalte bleibt unverändert. Bei allen anderen Reihen, würde die Position und 7 (Anzahl der Felder innerhalb einer Reihe) erhöht werden.
Der dritte Befehl lautet Rotate. Wechselt man von Horizontal auf Vertikal, so gilt für jedes Schiffsteil: Es wird zuerst überprüft, ob alle Schiffsteile sich untereinander oder nebeneinander befinden. Wenn diese Bedingung erfüllt ist, dann wird folgende Formel angewendet: Position des Schiffsteils = ((Position des Schiffsteils+Anzahl der Spalten/Reihen*aktueller index) % Anzahl aller Felder) – aktuellen Index). Ist die Bedingung nicht erfüllt, so wird zusätzlich zum Ergebnis dieser Formel nochmal die Anzahl der Spalten/Reihen hinzuaddiert.
Das letztmögliche Kommando lautet Place. Bei diesem Befehl wird das Schiff gesetzt, falls es möglich ist. Möglich ist dies nur, wenn das Schiff kein anderes Schiff, welches man bereits platziert hat, überschneidet und alle Schiffsteile nebeneinander bzw. Untereinander sich befindet. Dafür werden die Funktionen are_all_parts_together(ship_position, SHIP_PARTS[tmp ) und is_ship_placable_there(ship_position ) ausgeführt.

void Game::start_placing() {
        PlacingMode mode = Horizontal;
        int tmp = 0;
        vector<int> ship_position;
        for(int i = 0; i < SHIP_PARTS[tmp]; i++) {
          ship_position[i] = i;
        }
 
        while(true) {
          int command = m_players[m_current_player]->wait_for_command();
          
          if(command == GoRight) {
            for(int i = 0; i < SHIP_PARTS[tmp]; i++) {
              if(ship_position[i] % 7 == 6) {
                ship_position[i] -= 7;
                m_players[m_current_player]->show();
              } else {
                ship_position[i]++;
                m_players[m_current_player]->show();
              }
            }
          } else if(command == GoDown) {
            for(int i = 0; i < SHIP_PARTS[tmp]; i++) {
              if(ship_position[i] >= 42) {
                ship_position[i] -= 42;
                m_players[m_current_player]->show();
              } else {
                ship_position[i] += 7;
                m_players[m_current_player]->show();
              }
            }
          } else if(command == Rotate) {
            if(mode == Horizontal) {
              mode = Vertical;
              for(int i = 0; i < SHIP_PARTS[tmp]; i++) {
                if(are_all_parts_together(ship_position, i+1)) {
                  ship_position[i] = (ship_position[i] + 7 * i % 49) - i;
                  m_players[m_current_player]->show();
                } else {
                  ship_position[i] = (ship_position[i] + 7 * i % 49) - i + 7;
                  m_players[m_current_player]->show();
                }
              }
            } else {
              mode = Horizontal;
              for(int i = 0; i < SHIP_PARTS[tmp]; i++) {
                if(are_all_parts_together(ship_position, i+1)) {
                  ship_position[i] = ship_position[i] - 7 * i;
                  if(ship_position[i] >= 0) {
                    ship_position[i] += i;
                    m_players[m_current_player]->show();
                  } else {
                    ship_position[i] += 49 + i;
                    m_players[m_current_player]->show();
                  }
                } else {
                  ship_position[i] = ship_position[i] - 7 * i;
                  if(ship_position[i] >= 0) {
                    ship_position[i] += i;
                    m_players[m_current_player]->show();
                  } else {
                    ship_position[i] += 49 + i - 7;
                    m_players[m_current_player]->show();
                  }
                }
              }
            }
          } else if(command == Place) {
            if(are_all_parts_together(ship_position, SHIP_PARTS[tmp]) && m_players[m_current_player]->is_ship_placable_there(ship_position) {
              m_players[m_current_player]->set_ship(ship_position);
              if(next_player()) {
                if(++tmp == 4) {
                  return;
                }
              }
            }
          }
}

Sobald alle Schiffe platziert wurden, führt der Konstuktor, die Methode start_game() aus. In dieser Methode speichert die Variable attack_pos die Position des, für den Angriff, ausgewählten Feldes, beginnend mit dem Wert 0.
In einer Endlosschleife, wird zunächst auf ein Kommando gewartet.
Die Kommandos GoRight und GoDown tun dassselbe wie zuvor in start_placing(). Die Formeln sind allerdings kürzer, da sie nur eine Position bewegen müssen und nicht mehrere, wie es zuvor der Fall ist.
Das Kommando Place allerdings, platziert nun kein Schiff, sondern greift das entsprechende Feld an.
Dafür überprüft es zunächst, ob das Feld angreifbar ist. Angreifbar bedeutet, das das Feld noch nicht, angegriffen wurde.
Ist dies nicht der Fall, so wird dieses Kommando ignoriert und auf ein neues gewartet. Ansonsten wird das Feld angeschossen.

bool Game::start_game() {
    int attack_pos = 0;

    while (true) {
        int command = m_players[m_current_player]->wait_for_command();

        if (command == GoRight) {
            if (++attack_pos % 7 == 0) {
                attack_pos -= 7;
            }
        } else if (command == GoDown) {
            attack_pos += 7;
            if (attack_pos > 48) {
                attack_pos -= 49;
            }
        } else if (command == Place) {
            if (m_players[m_current_player]->is_field_shootable(attack_pos)) {
                m_players[m_current_player]->set_top_map(attack_pos, m_players[return_next_player_id()]->shoot(attack_pos));
                if (m_players[return_next_player_id()]->amount_of_broken_ship_pieces() == 15) {
                    m_players.erase(m_players.begin() + return_next_player_id());
                    if (m_players.size() == 1) {
                        return true;
                    }
                    next_player();
                }
            }
        }
    }
}