TicTacToe

Aus HRW FabLab MediaWiki
Wechseln zu: Navigation, Suche
Plakat der Präsentation am 06.04.2021

Entwickler

Tim Büttel, Jonas Peltzer, Andre Simon und Lars Tolksdorf


Das Projekt "TicTacToe" stellt eine per Hardware realisierte Variante des Spiels Tic-Tac-Toe (auch drei gewinnt oder Dodelschach genannt) dar. Es wurde realisiert, um es auch während der Corona-Pandemie zwei Menschen zu ermöglichen, im Wettbewerb gegeneinander anzutreten. Dazu erhält jeder Spieler ein Spielbrett mit einer LED-Matrix als Anzeige- sowie ein Bedienfeld. Über die Tasten des Bedienfeldes können die Spieler ihr Symbol auf das gewählte Feld setzen. Anschließend wird dieses Feld bei beiden Spielern durch eine LED farbig als belegt gekennzeichnet. Der MQTT-Server übernimmt dabei die Vermittlung zwischen den Spielbrettern und synchronisiert die Spielzüge.


Spielverlauf

Die Spieler wählen abwechselnd auf einem quadratischen Spielfeld (3x3 Felder) ein bisher unbelegtes Feld aus. Ziel ist es als erster Spieler drei seiner Farben in horizontaler, vertikaler oder diagonaler Reihe zu haben, um so das Spiel zu gewinnen.

Komponenten

Anzahl Komponente Preis
2 ESP32 X*
2 LED-Matrix je 9,99 Euro
2 Remote-Controller X*
2 LED X*
2 Widerstände X*
2 IR-Empfänger X*
2 Buzzer X*
20 Kabel X*
1 Filament (Rolle) 8,95 Euro

X* = Abgedeckt durch die zur Verfügung gestellte Hardware/Baukästen

Hardware

TicTacToe-Spiel

LED

Es wurde eine grüne LED mit einem 330 Ohm Vorwiderstand verbaut. Diese wird genutzt, um die erfolgreich hergestellte Verbindung zum eigen WLAN und zum MQQT-Server zu bestätigen.

Buzzer

Um den Spieler auch ein akustisches Feedback geben zu können wurde ein Piezospeaker angeschlossen. Dieser wird mit 5 Volt angesteuert und kann Warntöne und Melodien erzeugen. So wird beim Sieg der imperiale Marsch gespielt.

Infrarot Remote / Receiver

Die Spielereingaben werden über eine Infarot-Fernbedienung getätigt, die vom einem im Gehäuse eingelassenen Empfänger gelesen und an den ESP32-Controller weitergeleitet werden. Die Fernbediung kann auch entnommen werden um aus der Ferne das Spiel bedienen zu können.

LED-Matrix

Das Spielfeld wird über eine 8x8 große LED-Matrix realisiert. Das Feld wird durch Gittermuster in einzelne Felder aufgeteilt. Jedes der neun Felder ist 2x2 LEDs Groß und wird je nach Spielereingabe rot oder grün beleuchtet. Der Große Vorteil dieser Matrix ist, dass alle 64 LEDs über drei Leitungen angesteuert werden können. Normale LEDs hätten mehr Pins benötigt als der ESP32 bereitgestellt hätte.

ESP32

Als "Gehirn" des TicTacToe wird ein ESP32-Controller genutzt. Seine 5V-Pins reichen aus, um die Sensoren und Aktoren mit Strom zu versorgen und er liefert genug digitale Pins zur Verarbeitung der Signale.

Gehäuse

Das Gehäuse unseres TicTacToe-Spiels wurde in Catia als 3D-Modell erstellt. In diesem Prozess wurde darauf geachtet, dass sich die LED-Matrix , der Infrarotsensor, die Status-LED und der ESP32 gut in die Oberseite des Gehäuse einpassen. Die Aussparung für den ESP32 wurde so gestaltet, dass der Reset Knopf auch im geschlossenen Zustand noch erreichbar ist. Für die Fernbedienung wurde eine Aussparung eingebracht die eine leichte Entnahme und so eine Bedienung aus der Ferne ermöglicht. In der Gehäuseunterseite wurden Luftlöcher zur besseren Kühlung eingeplant. Das Gehäuse wurde anschließend in PLA auf einem Prusa Mini 3D Drucker gedruckt.

Schaltplan

Die folgende Grafik gibt einen Überblick über die Verkabelung der Komponenten:

Software

Programmcode

Im setup() werden alle Teile Konfiguriert, das Spielfeld wird initialisiert und die Verbindung zum MQTT-Server wird hergestellt:

void setup()
{
 //Config LED
 pinMode(LED,OUTPUT);
 digitalWrite(LED, LOW);
 Serial.begin(115200);
 //Start connecting to WiFi
 WiFi.begin(ssid, password);
 while (WiFi.status() != WL_CONNECTED) {
   delay(500);
   Serial.println("Connecting to WiFi..");
 }
 Serial.println("Connected to the WiFi network");
 //Connected to Wifi
 //Connecting to MQTT
 client.setServer(mqttServer, mqttPort);
 client.setCallback(callback);
 while (!client.connected()) {
   Serial.println("Connecting to MQTT...");
   if (client.connect("ESP32ClientPlayerEins", mqttUser, mqttPassword )) {
     Serial.println("connected");
     digitalWrite(LED, HIGH);
   } else {
     Serial.print("failed with state ");
     Serial.print(client.state());
     delay(2000);
     digitalWrite(LED, LOW);
   }
 }
 //LED panel setup
 tft.init();
 tft.setRotation(1);
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
 clock_prescale_set(clock_div_1);
#endif
 pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
 ledcSetup(0, 1E5, 12);
 ledcAttachPin(36, 0);
 Serial.begin(115200);                
 ledcSetup(channel, freq, resolution);
 ledcAttachPin(12, channel);
 irrecv.enableIRIn();                
 tft.fillScreen(TFT_BLACK);
 tft.setCursor(0, 0, 2);
 //Subscribing to MQTT
 client.subscribe("ES/WS20/gruppe3/#");
 //Default Values for board at start
 boardJS["A"] = 0;
 boardJS["B"] = 0;
 boardJS["C"] = 0;
 boardJS["D"] = 0;
 boardJS["E"] = 0;
 boardJS["F"] = 0;
 boardJS["G"] = 0;
 boardJS["H"] = 0;
 boardJS["I"] = 0;
 boardJS["P"] = 1;
}

Zu Beginn des Spiels wird angezeigt welcher Spieler an der Reihe ist:

 if (lastInputValue == 69) {
   ledcWriteTone(channel, 0);
   tft.fillScreen(TFT_BLACK);
   tft.setCursor(0, 0, 2);
   tft.setTextColor(TFT_BLUE);  tft.setTextSize(3);
   switch(player){
     case 1:
       tft.println("You are");
       tft.println("First");
     break;
     case 2:
       tft.println("You are");
       tft.println("Second");
     break;
     default:
       tft.println("ERROR!");
     break;
   }
   ledcWriteTone(channel, 0);
 } else {

Wenn das Spiel schon am laufen ist wird hier ebenfalls angezeigt ob der Spieler am Zug ist, oder nicht:

   if (player == activePlayer) {
     ledcWriteTone(channel, 0);
     ledcWriteTone(channel, 900);
     delay(500);
     ledcWriteTone(channel, 0);
     tft.fillScreen(TFT_BLACK);
     tft.setCursor(0, 0, 2);
     tft.setTextColor(TFT_GREEN);  tft.setTextSize(3);
     tft.println("Its Your");
     tft.println("Turn");
     ledcWriteTone(channel, 0);
   }
   if (player != activePlayer) {
     ledcWriteTone(channel, 0);
     tft.fillScreen(TFT_BLACK);
     tft.setCursor(0, 0, 2);
     tft.setTextColor(TFT_RED);  tft.setTextSize(3);
     tft.println("Pls Wait for");
     tft.println("your");
     tft.println("Opponent");
     ledcWriteTone(channel, 0);
   }
 }

Zurücksetzen des Feldes um neues Spiel zu starten:

if (lastInputValue == 10) //C
 {
   Serial.println(boardJS.size());
   Serial.println("Alles löschen");
   ledcWriteTone(channel, 300);
   delay(650);
   ledcWriteTone(channel, 0);
   ledcWriteTone(channel, 600);
   delay(650);
   ledcWriteTone(channel, 0);
   ledcWriteTone(channel, 900);
   delay(650);
   ledcWriteTone(channel, 0);
   for (int i = 0; i < 9; i++) {  
     Serial.println(board[i]);
     boardJS[board[i]] = 0;
  }  
   gameOverSound = 0;
   stopSound=0;
   results.value = 99;
   activePlayer = 1;
   lastInputValue = 69;
   for (int i = 0; i < 4; i++) {
     x[i] = 99;
   }
   pixels.clear(); // Set all pixel colors to 'off'
   for (int i = 0; i < 28; i++) {
     pixels.setPixelColor(frame[i], pixels.Color( 0, 0, 255));
     pixels.show();
   }
   Serial.println(boardJS.size());
   boardJS["P"] = 1;
   char buffer[256];
   size_t n = serializeJson(boardJS, buffer);
   client.publish("ES/WS20/gruppe3/Board", buffer, n);
 }

Bei Änderungen an der JSON Datei auf dem MQTT Server wird die callback funktion aufgerufen um diese Änderungen auf das Spielbrett des Spielers zu übertragen:

void callback(char* topic, byte* payload, unsigned int length) {
 int color[3];
 Serial.println("CALLBACK");
 Serial.println(topic);
 Serial.println();
 Serial.println("-----------------------");
 if (strcmp(topic, "ES/WS20/gruppe3/Board") == 0) {
   deserializeJson(boardJS, payload, length);
   activePlayer = boardJS["P"];
   for (int i = 0; i < 9; i++) {
     if (boardJS[board[i]] == 1) {
       setBoardLED(LEDArray[i], green);
     }
     if (boardJS[board[i]] == 2) {
       setBoardLED(LEDArray[i], red);
     }
   }
 }
}

Die Funktion setBoardLED() wird aufgerufen um die gemachten Züge auf der LED-Matrix anzeigen zu lassen:

void setBoardLED(int LED[4], int color[3]) {
 pixels.setPixelColor(LED[0], pixels.Color(color[0], color[1], color[2]));
 pixels.setPixelColor(LED[1], pixels.Color(color[0], color[1], color[2]));
 pixels.setPixelColor(LED[2], pixels.Color(color[0], color[1], color[2]));
 pixels.setPixelColor(LED[3], pixels.Color(color[0], color[1], color[2]));
}

Der Zug des Spielers wird ausgeführt und auf den MQTT-Server hochgeladen:

  //Next move is executed and uploaded to MQTT
   for (int i = 0; i < 4; i++) {
     x[i] = LEDArray[lastInputValue][i];
   }
   
   if (lastInputValue >= 0 && lastInputValue < 9) {
     ledcWriteTone(channel, 500);
     delay(650);
     ledcWriteTone(channel, 0);
     Serial.println(board[lastInputValue]);
     Serial.println(boardJS[board[lastInputValue]].as<const char*>());
     //if move legal
     if (boardJS[board[lastInputValue]] == 0) {
       boardJS[board[lastInputValue]] = player;
       activePlayer = activePlayer + playerSwitch;
       boardJS["P"] = activePlayer;
       char buffer[256];
       size_t n = serializeJson(boardJS, buffer);
       client.publish("ES/WS20/gruppe3/Board", buffer, n);
       lastInputValue = 99;
     }
   }

Nach jedem Zug wird geprüft ob der Spieler gewonnen oder verloren hat:

 //winning condition
 if ((boardJS[board[2]] == player && boardJS[board[5]] == player && boardJS[board[8]] == player) || (boardJS[board[1]] == player && boardJS[board[4]] == player && boardJS[board[7]] == player) || 
  (boardJS[board[0]] == player && boardJS[board[3]] == player && boardJS[board[6]] == player) || (boardJS[board[0]] == player && boardJS[board[1]] == player && boardJS[board[2]] == player) || 
  (boardJS[board[3]] == player && boardJS[board[4]] == player && boardJS[board[5]] == player) || (boardJS[board[6]] == player && boardJS[board[7]] == player && boardJS[board[8]] == player) || 
  (boardJS[board[0]] == player && boardJS[board[4]] == player && boardJS[board[8]] == player) || (boardJS[board[6]] == player && boardJS[board[4]] == player && boardJS[board[2]] == player)) {
   for  (int x = 0; x < 11; x++) {
     for  (int i = 0; i < 64; i++) {
       if (player == 1) {
         pixels.setPixelColor(i, pixels.Color(green[0], green[1], green[2]));
       }
       if (player == 2) {
         pixels.setPixelColor(i, pixels.Color(red[0], red[1], red[2]));
       }
     }
   }
   tft.fillScreen(TFT_BLACK);
   tft.setCursor(0, 0, 2);
   tft.setTextColor(TFT_RED);  tft.setTextSize(3);
   tft.println("Congrats");
   tft.println("You have ");
   tft.println("won");
   gameOverSound = 1;
   activePlayer = 3;
 }

Bedingung für verloren:

 //loosing condition
 if ((boardJS[board[2]] == otherPlayer && boardJS[board[5]] == otherPlayer && boardJS[board[8]] == otherPlayer) || (boardJS[board[1]] == otherPlayer && boardJS[board[4]] == otherPlayer && 
  boardJS[board[7]] == otherPlayer) || (boardJS[board[0]] == otherPlayer && boardJS[board[3]] == otherPlayer && boardJS[board[6]] == otherPlayer) || (boardJS[board[0]] == otherPlayer && 
  boardJS[board[1]] == otherPlayer && boardJS[board[2]] == otherPlayer) || (boardJS[board[3]] == otherPlayer && boardJS[board[4]] == otherPlayer && boardJS[board[5]] == otherPlayer) || 
  (boardJS[board[6]] == otherPlayer && boardJS[board[7]] == otherPlayer && boardJS[board[8]] == otherPlayer) || (boardJS[board[0]] == otherPlayer && boardJS[board[4]] == otherPlayer && 
  boardJS[board[8]] == otherPlayer) || (boardJS[board[6]] == otherPlayer && boardJS[board[4]] == otherPlayer && boardJS[board[2]] == otherPlayer)) {
   for  (int x = 0; x < 11; x++) {
     for  (int i = 0; i < 64; i++) {
       if (player == 2) {
         pixels.setPixelColor(i, pixels.Color(green[0], green[1], green[2]));
       }
       if (player == 1) {
         pixels.setPixelColor(i, pixels.Color(red[0], red[1], red[2]));
       }
     }
   }
   tft.fillScreen(TFT_BLACK);
   tft.setCursor(0, 0, 2);
   tft.setTextColor(TFT_RED);  tft.setTextSize(3);
   tft.println("You have ");
   tft.println("lost! More ");
   tft.println("luck next try");
   gameOverSound = 1;
   activePlayer = 3;
 }

Sonstige

CATIA V5

CATIA ist ein CAD-System der französischen Firma Dassault Systèmes, das ursprünglich für den Flugzeugbau entwickelt wurde und sich heute in verschiedenen Branchen etabliert hat. Wir haben damit unser Gehäuse Modell erstellt.

Prusa Slicer

Mit dem Prusa Slicer wurde das in Catia erstelle STL-File in für den 3D-Drucker kompatiblen G-Code umgewandelt.

Das Gehäuse wurde auf einem Prusa Mini 3D Drucker gedruckt. Die Druckzeit belief sich auf circa 10 Stunden.