Disclamer
Wir werden dieses Projekt bei der Abschlusspräsentation vorstellen, dort bekommt man dann auch einen besseren Eindruck vom Spiel.
Vorbereitung
Bei mir lief die Einrichtung der ESPs, anders als bei manchen anderen Kommilitonen, sehr flüssig ab. Über die Tutorials, auf die in der Laboranleitung verwiesen wurde, konnte ich alles schnell zum Laufen bringen.
Einzige Hürde war die Wahl des richtigen USB-Kabels, damit auch Daten übertragen werden können. Ich habe noch ein 2m langes USB-Kabel, von dem ich weiß, dass es dafür geeignet ist. Der Upload ist aber regelmäßig abgebrochen. Nachdem ich die Uploadrate reduziert habe (bei dem langen Kabel kam es Wohl zu Datenverlust) lief der Upload problemlos. Den ESP 8266 hab ich mit dem Beispielskript Blink und Scan WiFi getestet. Da der ESP 32 keine interne Led besitzt, habe ich hier nur den Sketch Scan WiFi getestet.
Die Einrichtung von nkrog lief ebenso problemlos ab. Die vom ESP gehostete Website (wie im Tutorial beschrieben) konnte ich sowohl über die lokale IP, als auch über die nkrog Website aufrufen.
Die Vorbereitung ist also abgeschlossen.
Multiplayer Game
Bei diesem Lab sollte in einem Team ein kleines Multiplayer-Spiel entwickelt werden. Benni, Simon und ich sind uns schnell einig gewesen, dass der Controller auf jeden Fall genutzt werden sollte. Nach ein wenig Überlegung hatten wir die Idee, mit der LED-Matrix ein 3-Personen 4-Gewinnt-Spiel zu entwickeln.
Leider ist in dem Set kein Treiberboard für die LED Matrix dabeigewesen. Die Matrix selbst zu verkabeln macht wenig Spaß und erfordert viele freie Pins. Der ESP32, für den wir uns zunächst entschieden hatten, ermöglicht den betrieb der LED-Matrix also nicht.
Als Alternative haben wir uns überlegt, den kleinen Bildschirm am ESP32 für eine Spielfeldanzeige zu nutzen. Jedoch ist der Bildschirm sehr klein und nur schwarz-weiß. Bei 3 Personen wäre eine farbliche Anzeige sehr sinnvoll.
Deshalb nutzen wir nun eine Website zur Darstellung des Spielfeldes. Ein Server, der den Spielstand und die Logik verwaltet, soll lokal bei einem Spieler auf dem PC gehostet werden. Der Server wird über ngrok veröffentlicht.
Im Team haben wir nun die Aufgaben verteilt:
- Simon kümmert sich um das Front End
- Benni kümmert sich um das Back End
- Ich kümmere mich um den ESP, der den Joystick auslesen soll und die Daten an den Server verschickt sowie um eine Python-Klasse die erkennt, ob jemand gewonnen hat.
Auslesen vom Joystick-Daten mit dem ESP32
Zunächst wollten wir den ESP32 nutzen, da Benni und Simon beide Probleme mit dem ESP8266 hatten. Bei diesem ist jedoch der einzig freie Analog-Pin bei WLAN-Nutzung nicht verfügbar. Da wir aber sowohl die Controller-Daten auslesen als auch die Daten an den Server schicken müssen, haben wir uns doch für den ESP8266 entschieden.
Auslesen vom Joystick-Daten mit dem ESP8266
Wir nutzen nur eine Achse des Controllers um die Spalte, in der der 4-Gewinnt-Stein eingeworfen werden soll, gewählt werden kann. Der Controller ist letztlich ein einfacher Spannungsteiler, da in Ruheposition 2,5V und in den ausgeschlagenen Positionen 0V bzw. 5V abfallen. Falls der Controller einen Ausschlag erkennt, wird eine Zahl erhöht bzw. verringert. Diese Zahl stellt die gewählte Spalte dar. Drückt der Spieler nun auf den Controllerknopf, wird die Zahl an den Server übermittelt.
Der Code ist in folgende Funktionen aufgeteilt:
setup
: Im Setup verbindet sich der ESP mit dem Router und ruft die Funktionupdate_website
auf. Durch diesen erstmaligen Aufruf wird dem Server mitgeteilt, dass der Spieler am Spiel teilnehmen will.loop
: Im Loop werden die Funktionenread_values
undcheck_thresholds
nacheinander aufgerufen sowie für 20ms gewartet. Hierdurch wird der Button entprellt.read_values
: Die Spannung der Controller-Position und die des Buttons werden gemessen.check_thresholds
: Die eben aufgenommenen Messwerte werden verarbeitet. Hierzu sind Schwellwerte definiert. Übersteigt ein Messwert einen Schwellwert wird, eine Aktion ausgelöst. Diese Aktionen sind in den Funktionengo_left
,go_right
undbutton_action
definiert.go_left
: Die Spaltennummer wird um 1 vermindert.go_right
: Die Spaltennummer wird um 1 erhöht.button_action
: Die Funktionupdate_wabsite
wird aufgerufen.update_website
: Eine Verbindung zum Server wird aufgebaut. Die Spaltennummer sowie die Information, ob es sich um den initialen Aufruf handelt werden dem Server als JSON übermittelt. Anschließend wird der Response-Code und die Response-Message in den seriellen Monitor gedruckt.print_position
: Die Position wird in den seriellen Monitor gedruckt. Diese Funktion diente vor allem zum Debuggen
Der Code auf dem ESP sieht wie folgt aus:
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#define joyPin A0
#define butPin 0
int joy = 0;
bool but = false;
bool moved = false;
bool pressed = false;
int selection = 0;
const int selection_len = 10;
const char* ssid = "SSID";
const char* password = "PASSWORD";
const char* serverName = "SERVER";
WiFiServer server(80);
String header;
void setup() {
Serial.begin(115200);
Serial.println("");
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
// Print local IP address and start web server
Serial.println("");
Serial.println("WiFi connected.");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
Serial.println("Connecting to host: ");
update_website(1);
Serial.println("");
}
void loop() {
read_values();
check_thresholds();
delay(20);
}
void read_values() {
joy = analogRead(joyPin);
but = !digitalRead(butPin);
}
void check_thresholds() {
int centered = 535;
int bound = 100;
int upper = centered + bound;
int lower = centered - bound;
if (joy > upper && moved == false) {
moved = true;
go_left();
}
if (joy < lower && moved == false) {
moved = true;
go_right();
}
if (joy < upper && joy > lower) {
moved = false;
}
if (but == true && pressed == false) {
pressed = true;
button_action();
}
if (but == false && pressed == true){
pressed = false;
}
}
void go_left() {
if (selection > 0) {
selection = selection - 1;
}
update_website(0);
print_position();
}
void go_right() {
if (selection < selection_len - 1) {
selection = selection + 1;
}
update_website(0);
print_position();
}
void button_action() {
update_website(0);
Serial.println("BUTTON");
}
void update_website(int lobby) {
if(WiFi.status()== WL_CONNECTED){
HTTPClient http;
http.begin(serverName);
http.addHeader("Content-Type", "application/json");
int httpResponseCode = http.POST("{\"position\":\"" + String(selection) + "\",\"button\":\"" + String(but) + "\",\"lobby\":\"" + lobby + "\"}");
Serial.print("HTTP Response code: ");
Serial.print(httpResponseCode);
Serial.print(" | ");
if (httpResponseCode < 500){
String payload = http.getString();
if (payload.length() > 0){
Serial.print("Payload: ");
Serial.print(payload);
}
}
Serial.println("");
http.end();
}
}
void print_position() {
for (int i=0; i < selection_len; i++) {
if (selection == i) {
Serial.print(1);
}
else{
Serial.print(0);
}
}
Serial.println("");
}
Python-Klasse zur Erkennung ob jemand gewonnen hat
Damit die Arbeitsteilung fair blieb war es außerdem meine Aufgabe, eine Python-Klasse zur Sieg-Erkennung zu schrieben. Das Spielfeld wird als Matrix interpretiert. Die Zahlen stehen für den zugehörigen Spieler. Hier sieht man ein vereinfachtes Beispiel:
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 1 | 0 |
3 | 2 | 1 | 3 | 0 |
2 | 1 | 2 | 3 | 0 |
1 | 2 | 1 | 3 | 0 |
Der Spieler 1 hat auf der Diagonalen vier Steine und hat damit gewonnen. Zur leichteren Erkennung wird pro Spieler eine Matrix erstellt und normiert:
Spieler 1:
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 1 | 0 |
0 | 0 | 1 | 0 | 0 |
0 | 1 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 0 |
Spieler 2:
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 0 |
0 | 1 | 0 | 0 | 0 |
Spieler 3:
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 1 | 0 |
0 | 0 | 0 | 1 | 0 |
0 | 0 | 0 | 1 | 0 |
Nun können in jeder Spielermatrix Sieges-Muster gesucht werden. Ein paar verschiedene Muster sind hier dargestellt:
1 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
1 | 1 | 1 | 1 | 0 |
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 0 | 0 |
0 | 0 | 1 | 0 | 0 |
0 | 0 | 0 | 1 | 0 |
0 | 0 | 0 | 0 | 0 |
Bei einem 10 x 10 Spielfeld gibt es mehrere Hundert Muster. Wenn Eine Spielermatrix mit einem Muster übereinstimmt hat dieser gewonnen. Das Python-Skript sieht wie folgt aus:
import numpy as np
class Victory:
def __init__(self, player_num, field_size):
self.field_size = field_size
self.player_num = player_num
m_hori = self._make_m_hori()
m_vert = self._make_m_vert()
m_left = self._make_m_left()
m_right = self._make_m_reight()
self.patterns = m_hori + m_vert + m_left + m_right
def _make_m_hori(self):
v = np.array([1,1,1,1])
ms = []
for i in range(self.field_size[0]):
for k in range(self.field_size[1]-3):
m = self._get_empty_m()
m[i,k:k+4] = v
ms.append(m)
return ms
def _make_m_vert(self):
v = np.array([1,1,1,1]).reshape(1,-1)
ms = []
for i in range(self.field_size[0]-3):
for k in range(self.field_size[1]):
m = self._get_empty_m()
m[i:i+4,k] = v
ms.append(m)
return ms
def _make_m_left(self):
v = np.eye(4)
ms = []
for i in range(self.field_size[0]-3):
for k in range(self.field_size[1]-3):
m = self._get_empty_m()
m[i:i+4,k:k+4] = v
ms.append(m)
return ms
def _make_m_reight(self):
v = np.fliplr(np.eye(4))
ms = []
for i in range(self.field_size[0]-3):
for k in range(self.field_size[1]-3):
m = self._get_empty_m()
m[i:i+4,k:k+4] = v
ms.append(m)
return ms
def _get_empty_m(self):
field = [[0 for _ in range(self.field_size[0])] for _ in range(self.field_size[1])]
return np.array(field)
def _split(self, field):
ms = []
for i in range(self.player_num):
controll_m = (i+1) * np.ones(self.field_size)
m = (controll_m == field).astype(int)
ms.append(m)
return ms
def check(self, field):
field = np.array(field).reshape(self.field_size)
players_ms = self._split(field)
player_num = 0
for players_m in players_ms:
player_num = player_num + 1
for pattern in self.patterns:
m = (players_m==1)&(players_m == pattern)
if np.count_nonzero(m) == 4:
return player_num
return 0
if __name__ == "__main__":
# Beispiel Spielfeld
fieldsize = (6,6)
fd = [[0 for _ in range(fieldsize[0])] for _ in range(fieldsize[1])]
fd[1][1:5] = [1,2,3,4]
fd[2][1:5] = [1,0,3,4]
fd[3][1:5] = [1,2,1,4]
fd[4][1:5] = [1,2,3,1]
# Flatten
ff = []
for i in fd:
for k in i:
ff.append(k)
# Victory API
player_num = 3
vic = Victory(player_num, fieldsize)
zahl = vic.check(ff) # 0 == Keiner hat gewonnen, 1/2/3 Spieler hat gewonnen
print(zahl)