Das ChessDestroyer-Projekt ist eine Webanwendung, die es einem Nutzer ermöglicht gegen eine Stockfish Chess Engine mit verschiedenen Schwierigkeitsstufen zu spielen. Gewinnt ein Spieler in einer Runde, wird er zudem noch mit einem Score bewertet und in ein persistentes Scoreboard eingetragen.
Die Aufgaben innerhalb der Webanwendung werden strikt zwischen dem Backend und dem Frontend aufgeteilt und wobei das Backend alle Angaben des Frontends nach dem Zero-Trust Prinzip validiert.
Das Frontend bassiert auf zwei Javascript frameworks:
- Chessboard.js v2
- Ist für das bereitstellen des Schachbrettes verantwortlich und generiert sowohl das Brett an sich als auch die Figuren.
- Chessboard.js stellt schnittstellen bereit, um eine Aufstellung aus einem String zu laden und die einzelnen Figuren zu bewegen.
- Auch Callbacks für Nutzereingaben werden Bereitgestellt, so dass das Auslesen von angeklickten Feldern einfacher wird.
- Chessboard.js enthält keinerlei Spiellogik, auf dem Brett könnte im grunde alles angezeigt werden.
- Chess.js
- Benutzen wir um im Frontend bereits die Schachlogik bereitstellen zu können.
- Wir benutzen Chess.js hauptsächlich um Züge zu validieren oder um zu erkennen, ob und wie ein Spiel beendet wurde (Schachmatt oder Unentschieden).
Das komplette Projekt besteht lediglich aus 4 Unterseiten (3 Seiten + Impressum). Da die verknüpfungen der Seiten schwierig textuell zu beschreiben sind, haben wir dieses Diagramm erstellt.
flowchart LR
A("welcome_page.html")
B("settings.html")
C("imprint.html")
D("game.html")
A -- "Play Now!" --> B
A -- "Imprint" --> C
B -- "Play!" --> D
D -- "Play again" ----> B
Der Nutzer Startet auf der welcome_page.html. Hier befinden sich allgemeine Informationen, was den Nutzer auf den Nächsten Seiten erwartet und worauf er genau geklickt hat. Von welcome_page.html aus kann der Nutzer über das Menü und den Button "Imprint" zu imprint.html gelangen. Alternativ kann er ausgehend von der welcome_page.html mit dem Button "Play Now!" auch auf die settings.html gelangen. Die settings.html Unterseite gibt dem Nutzer die Möglichkeit, seinen Nutzernamen (Der später auch auf dem Scoreboard landet), die Schwierigkeit des Computergegners und die Figurenfarbe (Schwarz / Weis / Zufällig) anzupassen. Über den Button "Play!" gelangt der Nutzer schlussendlich auf die game.html Seite. Hier findet das eigentliche Spiel statt. Nach einem beendeten Spiel kann mit dem Button "Play again" zurück zur settings.html navigiert werden, um ein neues Spiel zu starten.
Die Landing Page für die Webanwendung "Chessdestroyer 4000" bietet eine kurze Einführung in das Produkt, hebt seine Hauptmerkmale hervor und bietet einfache Navigationselemente sowie eine Schaltfläche zum Starten des Spiels.\
- Durch das Meta-Viewport-Tag und die externe CSS-Datei ist die Seite auf verschiedene Bildschirmgrößen angepasst.
- Ein Formular mit einer Schaltfläche zum Starten des Spiels, das den Benutzer zur Seite "settings.html" führt.
- Multimedia: Ein Hintergrundvideo, das der Seite ein dynamisches und ansprechendes Aussehen verleiht.
Die Einstellungsseite ermöglicht es Benutzern, ihre Spielerinformationen und -präferenzen festzulegen, bevor sie ein neues Spiel starten.
- Verstecktes Feld: Enthält ein verstecktes Eingabefeld (hidden), um eine neue Sitzung zu kennzeichnen.
- Benutzernamen-Feld: Ein Eingabefeld für den Benutzernamen (username), das erforderlich ist und auf 20 Zeichen begrenzt ist.
- Schwierigkeitsgrad: Ein Schieberegler (range) für die Auswahl des Schwierigkeitsgrads mit einem Bereich von 1 bis 3, wobei der aktuelle Wert angezeigt wird.
- Farbauswahl: Radiobuttons für die Auswahl der Spielerfarbe (Weiß, Zufällig, Schwarz).
- Absenden: Eine Schaltfläche zum Absenden des Formulars und Starten des Spiels.
Die Seite stellt eine rechtskonforme Impressumsseite für die Chessdestroyer 4000 Webanwendung bereit, die den Nutzern alle erforderlichen rechtlichen Informationen und Kontaktmöglichkeiten bietet.
Um das Spiel Layout einfacher auf mobilgeräte anpassen zu können, wird hier ein Gridlayout mit einem 2X3 Grid verwendet. Das Spielbrett nimmt ein 2X2 Felder ein, die Beschreibung von Spieler und Gegner jeweils 1 Feld. Um Das Spiel effektiv Centern zu können, besitzt das Grid zwei Parent divs.
Neben dem Spielfeld besteht die Website noch aus dem game-over-container. Dieser ist beim laden der Seite verborgen und wird erst später durch javascript sichtbar gesetzt. Auch im game-over-container befindet sich die gleiche struktur um den inhalt zu Centern. Des weiteren enthält der game-over-container noch den "Game Over" Schriftzug, einen link Button um zurück zur settings.html seite gelangen zu können und zwei Platzhalter, einen für die over-description und einen für das Scoreboard, welches ebenfalls durch Javascript eingefügt wird.
Die gesamte Logik des Frontends spielt sich in der game.html.hbs datei ab, hier ist das eigentliche Schach Spiel implementiert. Somit ist der Nutzer die meiste Zeit auf der game.html.hbs seite.
Beim Rendern der Handlebars setzt da Backend bereits drei Variablen in das Frontend ein:
| Name | Nutzen |
|---|---|
| username | Durch das einfügen des Nutzernamens ist sichergestellt, dass das Frontend und Backend den gleichen Nutzernamen führen. Dies ist für die Korrektheit des Scoreboard wichtig. |
| difficulty | Die Schwierigkeit wird im Backend rein durch Zahlen abgebildet. Dass der Nutzer trotzdem auch während des Spiels die Möglichkeit hat, die Schwierigkeit einzustufen, wird ihm im Frontend eine Lesbare Schwierigkeit angezeigt. |
| color | Da wir nach dem Zero-Trust Prinzip vorgehen, wird bei auswahl einer Zufälligen Farbe, eben diese Farbe im Backend generiert. Da die spiel logik des Frontends allerdings auch eine Spieler farbe benötigt, wird diese per handelbar in ein hidden input Feld gerendert, wo sie per javascript später ausgelesen werden kann. |
Im Wesentlichen besteht die Logik des Frontend aus den folgenden Funktionen:
| Fuktionsname | Beschreibung |
|---|---|
| isPiecePlayerColor (piece) | Überprüft mittels REGEX ob die Farbe der Figur (pice) der Farbe des spielers entspricht |
| onDragStart (dragStartEvt) | Wird von Chessboard.js aufgerufen, sobald ein Spieler versucht eine Figur zu bewegen. 1. Zuerst wird die Funktion isPiecePlayerColor() aufgerufen, danach Chess.js konsultiert, ob der Spieler am Zug ist.2. Nur wenn beide Abfragen erfolgreich sind, darf der Spieler die Figur bewegen. 3. Außerdem werden mithilfe von Chess.js alle legalen züge der Figur generiert.4. Über die Erlaubten züge wird iteriert und auf jedes Zielfeld eines Zuges ein Kreis gesetzt. |
| onDrop (dropEvt) | Wird von Chessboard.js aufgerufen, sobald ein Spieler versucht eine Figur auf ein neues Feld gezogen und losgelassen hat. 1. Der Zug wird an Chess.js weitergereicht, das Ergebnis des Zuges wird für später gespeichert. 2. Alle Punkte (welche zuvor durch onDragStart gesetzt wurden, werden gelöscht.3. Nun wird das zuvor gespeicherte Ergebnis von Chess.js überprüft.4a. Falls der Zug nicht erlaubt war, wird die Figur auf das Ursprüngliche Feld zurückgesetzt. 4b. Falls der Zug erlaubt war, wird das Schachbrett auf die Aktuelle Position gesetzt. 5b. Die Funktion opponentMove(dropEvt.source, dropEvt.target); wird aufgerufen. 6b. Die Funktion checkGameOver(); |
| fetchBoard(src, dest) | Schickt die src und dest Felder and das Backend. |
| checkGameOver() | Ist Relativ selbsterklärend, Chess.jswird Konsultiert, ob das Spiel beendet wurde.Wenn laut Chess.js das Spiel Beendet wurde, wird das Backend konsultiert, ob das Spiel auch im backend beendet ist. Dieses Verfahren ist teil unseres Zero-Trust Prinzips Falls das Spiel im Backend nicht beendet ist, wird der aktuelle Spielstand mit dem Stand aus dem Backend überschrieben. Wenn sich das Backend und Chess.js einig sind, wird der "game-over-container" sichtbar gesetzt und sperrt somit das Spielfeld. Außerdem wird der Gewinner in die Beschreibung des Game Over Popup eingefügt.Bei Jedem Aufruf der checkGameOver() Funktion wird auch die highlightTurn() Funktion aufgerufen. |
| opponentMove(src, dest) | Stellt zuerst sicher, dass wirklich der Gegner am Zug ist. Dann wird: 1. Die Funktion fetchBoard wird aufgerufen, der Zug des Backends mit Chess.js synchronisiert und das Board auf die aktuelle Position gesetzt.2. Die Funktion checkGameOver() wird ausgeführt. |
| highlightTurn() | Setzt die Deckkraft des Spieler namens, welcher aktuell am Zug ist auf 100% während die Deckkraft des nicht aktiven Spieler namens auf 50% Reduziert wird. Außerdem wird von Chess.jsberechnet, ob aktuell ein Spieler im Schach steht. Sollte dies der Fall sein, wird das Feld auf dem der Betroffene König steht Rot eingefärbt. |
| firstMove() | Zuerst wird die Funktion highlightTurn() aufgerufen. Falls die Farbe des Menschlichen Spielers Schwarz ist, wird dann ein erste Zug aus dem Backend angefordert mit dem Aufruf `opponentMove(0, 0). |
Ablauf der Frontend Logik, nachdem die Website fertig geladen ist (Dies passiert ohne Interaktion):
- Einlesen der color, welche zuvor mit Handlebars gesetzt wurde.
- Erstellen einer Instanz von
Chess.jsin Startaufstellung. - Erstellen des Schachboards mit
Chessboard.js, die Ausrichtung wird von der zuvor eingelesenen color angegeben und die Aufstellung aus derChess.jsInstanz übernommen. - Die Funktion
firstMove()wird ausgeführt:- Die Funktion
highlightTurn()wird ausgeführt und highlightet im UI den Spieler der aktuell am Zug ist. - Falls der Spieler die Figurenfarbe Schwarz hat, wird die Funktion
opponentMove("0", "0")aufgerufen.
- Die Funktion
Der Quellcode des Backends befindet sich im src Verzeichnis des Projekts. Der Einstiegspunkt ist hierbei die main.rs mit der Funktion rocket.
Die Cargo.toml im Root-Verzeichnis gibt die Abhängigkeiten des Projektes an und wird von cargo (dem Package-Manager von Rust) benötigt.
Für das Backend der Webanwendung wurde Rust in Kombination mit dem Rocket Framework ausgewählt. Diese Entscheidung resultierte aus den spezifischen Anforderungen des Projekts, in dem die Anwendung äußerst robust sein und eine ausgezeichnete Fehlerbehandlung bieten sollte. Zudem war es wichtig, zu verhindern, dass bei fehlerhaften Serveranfragen Laufzeitfehler entstehen.
Rust, als typensichere Programmiersprache mit hervorragender Fehlerbehandlung, wurde als ideal geeignet betrachtet. Das Rocket Framework überzeugte durch seine einfache Syntax, die eine schnelle Implementierung eines Webservers ermöglicht. Darüber hinaus unterstützt es nativ die asynchrone Anfragenverarbeitung und bietet eine sehr gute Laufzeitperformance. Das Konzept der mountable applications erleichtert die Unterteilung der Anwendung in Komponenten, was die Integration neuer Funktionen, wie zum Beispiel des dynamisches HTML-Rendering, erheblich vereinfacht und entsprechend effizient umsetzt.
Für die Verwaltung und Durchführung des Schachspiels wurde die Shakmaty Bibliothek eingesetzt. Diese Bibliothek übernimmt die gesamte Spielverwaltung und sorgt für eine reibungslose Handhabung. Ein wichtiges Merkmal dieser Bibliothek ist die Kompatibilität mit dem UCI (Universal Chess Interface) und der FEN (Forsyth-Edwards Notation), was eine einfache Kommunikation mit der Stockfish-Instanz und eine minimale und zugleich genormte Darstellung des Schachbrettzustandes ermöglicht.
Für die persistente Speicherung des Scoreboards und das gleichzeitige Durchführen von Änderungen aus einem asynchronen Kontext ohne das Auftreten von race conditions wurde die Rusqlite Bibliothek verwendet. Diese basiert auf SQLite und ermöglicht eine effiziente Fehlerbehandlung, wobei die Vorteile einer SQLite-basierten Datenbank voll zum Tragen kommen.
Im Rahmen der Instanziierung des Webservers mit der Funktion rocket, werden zunächst mehrere Objekte generiert, die als Zustände in das Rocket Framework übergeben werden. Dabei wird ein SessionHandler erstellt, welcher Game Instanzen speichert und diese einem Client durch später platzierte SessionId-Cookies zuordnet. Zudem wird ein DB Objekt, als Verbindung zur persistenten rusqlite Datenbank, generiert und ebenfalls an das Rocket Framework übergeben.
Der Webserver unterstützt fünf explizit definierte Routen sowie eine Route für die statische Dateibereitstellung. Die Funktionen der einzelnen Routen werden im Folgenden genauer erläutert:
-
GET /
Diese Route wird normalerweise als erste aufgerufen und leitet die Anfrage des Clients permanent an diewelcome_page.htmlweiter. -
POST /game
Diese Route wird üblicherweise über das Formular dersettings.htmlaufgerufen und dient der Erstellung eines Spieles.
Sie muss folgende Parameter beinhalten:-
new_session: Option
Ein optionaler Parameter, der angibt, ob eine möglicherweise laufende Sitzung überschrieben werden soll. -
username: String
Der Nutzername des Spielers, der unter anderem für den Scoreboard-Eintrag genutzt wird. -
difficulty: Integer$\in{1,2,3}$
Eine Zahl die die vom Spieler gewählte Schwierigkeitsstufe angibt. -
color: Character$\in {w,b,r}$
Ein Textzeichen, welches die vom Spieler gewählte Farbe oder$r$ für eine zufällige Farbe angibt.
Anfangs überprüft die Route mit
find_session, ob der Client bereits eine Sitzung hat. Existiert diese und ist dernew_sessionParameter gesetzt, wird diese gelöscht.
Dann wird ein neues Spiel mit den vorgegebenen Parametern generiert. Dabei wird über dieGame-Instanz unter anderem ein Unterprozess der Stockfish-Instanz erstellt.
Ist das Spiel erfolgreich erstellt worden, wird anschließend eine neue Sitzung, die mit derGame-Instanz verknüpft ist, erstellt und imSessionHandlergespeichert.
Schließlich wird diegame.htmlüber Handlebars generiert und als Antwort zurückgegeben, wobei wichtige Informationen, wie die Spielerfarbe und der Nutzername, in das HTML eingefügt werden. -
-
POST /move
Diese Route wird während eines Spiels wiederholt von dergame.htmlaufgerufen, um einen vom Spieler ausgeführten Zug zu validieren und den entsprechenden Gegenzug der Stockfish-Engine zu ermitteln.
Die Anfrage muss folgenden Body haben:-
move: String Ein vierstelliger String, der den Zug beschreibt. Die ersten zwei Zeichen bezeichnen das Quellfeld des Zuges, die letzten beiden das Zielfeld
Zunächst wird überprüft, ob der anfordernde Client überhaupt eine Session besitzt. Ist dies der Fall, wird die zugehörige Spielsitzung geladen und der Zug des Spielers validiert und ausgeführt.
Sollte der Zug ungültig sein, wird der letzte Schachbrettzustand als FEN mit einem 406-Statuscode zurück an den Client gesendet, um eine Fortsetzung des Spiels mit einem validierten Schachbrett zu ermöglichen.
Im weiteren Verlauf berechnet und vollzieht die Stockfish-Engine, die derGame-Instanz zugeordnet ist, den Gegenzug. Der neue Schachbrettzustand wird dann als FEN mit einem positiven Statuscode zurückgegeben. -
-
GET /game_end
Der Aufruf dieser Route erfolgt, wenn der Client auf dergame.htmlvermutet, dass das Spiel beendet ist.
Die Route überprüft, ob das Spiel tatsächlich beendet ist. Im Fall, dass das Spiel beendet ist, wird ein 200er-Statuscode zurückgegeben. Andernfalls wird mit dem Statuscode 406 der letzte Spielzustand als FEN zurückgesendet. -
GET /scoreboard
Der Aufruf dieser Route erfolgt, sobald ein Spiel beendet ist und das Scoreboard geladen werden soll.
Sie muss folgenden Parameter beinhalten:-
count: int
Ein optionaler Parameter, der die Anzahl der Scoreboard-Einträge, die geladen werden sollen, angibt.
Diese Route öffnet über den
DBZustand eine Verbindung zur Datenbank und gibt die entsprechenden Einträge, sofern vorhanden, aus. Sie werden als Liste im JSON-Format an den Client übermittelt. -
-
GET /<document_name>
Der Aufruf dieser Route dient dazu, alle statischen Ressourcen bereitzustellen, die im Ordnerstaticvorliegen. Sie wird über denFileServerinstanziiert.
Über diese Route werden insbesondere diewelcome_page.html, diesettings.html, dieimpressum.htmlsowie die zugehörigen CSS- und JS-Dateien bereitgestellt.