Posted on Leave a comment

Tic Tac Toe in C# 2

Tic Tac Toe in C# (Teil 2 von 3)

Schritt 2: Die Spiellogik

Im ersten Teil des TicTacToe-Tutorials haben wir eine komplette Oberfläche, bestehend aus einem 3×3 Spielfeld, einem Label zur Anzeige der aktuellen Spielinformation und einem Button zum Neustart erstellt. In diesem Teil des Tutorials soll es nun darum gehen, die eigentliche Spiellogik zu implementieren. Dazu sollte man sich wieder zuerst überlegen, aus welchen Teilen sich die Logik eigentlich zusammensetzt, bevor man mit dem Programmieren beginnt:Wenn auf ein Feld geklickt wird, soll entweder ein O oder ein X in das Feld eingetragen werden. Diese Markierung darf später nicht geändert werden.

  • Wenn auf ein Feld geklickt wird, soll entweder ein O oder ein X in das Feld eingetragen werden. Diese Markierung darf später nicht geändert werden.
  • Dementsprechend muss es immer einen aktiven Spieler geben, der ein Feld wählen darf. Der aktive Spieler wechselt jedes Mal, nachdem ein Feld markiert wurde
  • Wenn ein Spieler drei Felder in einer Reihe, Spalte oder Diagonale für sich markiert hat, hat er das Spiel gewonnen. Danach dürfen keine neuen Felder mehr markiert werden, bis ein neues Spiel gestartet wird

Beginnen wir nun also damit, dieses Verhalten zu implementieren. Die meisten Änderungen werden wir dazu in der MainWindow.xaml.cs – sozusagen dem Code Behind der Oberfläche – vornehmen.

Dort gibt es bereits eine Funktion:

Diese Funktion ist der Konstruktor des Fensters, d.h. er wird aufgerufen, wenn das Fenster erzeugt wird und initialisiert die Komponente. Alles, was bereits beim Erzeugen das Fensters geschehen muss, muss im Konstruktor implementiert werden.

Schritt 2.1: Spieler und Variablen definieren

Bevor mit dem Coden in der MainWindow.xaml.cs beginnen, brauchen wir noch eine weitere Datei. Wir wissen, dass es zwei Spieler (Player1 und Player2) gibt. Einer der beiden Spieler ist dabei immer gerade am Zug. Jede Zelle kann außerdem einem dieser beiden Spieler oder keinem Spieler zugeordnet sein. Wir definieren daher zuerst ein Enum Player mit den Werten None, Player1 und Player2. Ein Enum ist eine Aufzählung (Enumeration) von Werten. Jeder Wert entspricht einfach einer Zahl. Enums werden zum einen verwendet, um die Lesbarkeit des Codes zu erhöhen, sie geben aber auch eine genaue Aussage, welche Werte für einen bestimmten Sachverhalt erlaubt sind.

Klassen, Enums oder Interfaces werden in C# jeweils in eigenen Dateien definiert. Dadurch kann man sich im Projekt später besser zurechtfinden. Um eine neue Datei hinzuzufügen, klicken Sie mit einem Rechtslick auf das Projekt und wählen „Hinzufügen“. Anschließend wählen Sie „Neues Element“. Im nächsten Fenster können Sie Elemente von verschiedenen Typen auswählen, die dem Projekt hinzugefügt werden. Wählen Sie den Typ „Klasse“, denn Enums stehen in der Standardauswahl nicht zur Verfügung.

Nachdem Sie der Klasse einen Namen gegeben und bestätigt haben, erhalten Sie eine Datei, die nur den Inhalt

hat. Ein Namespace oder Namensraum ist ein Bereich, in dem jedes Objekt einen eindeutigen Namen hat – das heißt, in einem Namespace darf es nicht zwei Klassen, Interfaces oder Enums mit demselben Namen geben. Namespace können genutzt werden, um größere Projekte zu strukturieren und Objekte mit gleichen Bezeichnern voneinander zu unterscheiden. Für unser Projekt bewegen wir uns lediglich in einem Namespace.

Wir wollen an dieser Stelle außerdem keine Klasse, sondern lediglich ein Enum. Dieses kann öffentlich verfügbar sein. Daher passen wir den Code an:

Nachdem wir das Enum angelegt haben, wechseln wir wieder zu MainWindow.xaml.cs. Hier brauchen wir eine Variable, die den aktuellen Spieler speichert und zum anderen eine zweidimensionale Liste für die Felder. Neben diesen Feldern brauchen wir noch einen Boolean, der angibt, ob gerade ein Spiel läuft.

Die Variablen können private sein, denn nur dieses Fenster muss sie kennen.

Im Konstruktor setzen wir außerdem die Zeile:

Hinweise zur Variablenbenennung:

In C# gibt es diverse Konventionen, wie Code formatiert wird und wie Variablen oder Methoden benannt werden. Eine Konvention ist, dass öffentliche Felder in PascalCase geschrieben werden (also beispielsweise CurrentPlayer), während private Felder in camelCase, also mit einem kleinen Anfangsbuchstaben geschrieben werden (beispielsweise currentPlayer). Es finden sich auch diverse Codebeispiele, bei denen private Variablen einen zusätzlichen _ vor der Bezeichnung haben. Wichtig ist vor allem, sich innerhalb eines Projektes an eine Konvention zu halten.

Schritt 2.2.: Klick-Funktion hinzufügen

Wenn ein Spieler auf ein Feld klickt, soll eine bestimmte Aktion passieren. Um diese zu implementieren, definieren wir zuerst eine allgemeine Funktion Button_Click.

Diese Funktion erhält als Parameter ein Objekt sender und ein Objekt vom Typ EventArgs. Diese Parameter sind für Events, die durch die Oberfläche ausgelöst werden, relevant, denn sie stellen Informationen über die sendende Komponente und das Event selbst bereit. Diese Methodensignatur ist daher notwendig, damit die Funktion später in der Oberfläche mit einem Oberflächenelement verknüpft werden kann.

Im ersten Schritt der Funktion holen wir uns nun Informationen über den Sender:

Dazu casten wir das Objekt zu einem Button – denn wir werden später sicherstellen, dass die Funktion nur durch Buttons aufgerufen werden kann. Wäre der Sender kein Button, sondern beispielsweise ein Label, würde diese Zeile zu einer Exception führen. Anschließend holen wir uns die Zeile und Spalte, die der Button im überliegenden Grid hat, damit wir ihn auf dem Spielfeld zuordnen können.

Nun soll folgendes passieren: Wenn gerade ein Spiel läuft und das Feld noch nicht durch einen Spieler markiert wurde, dann wird das Feld markiert und der Spieler gewechselt.

In der If-Anweisung werden zuerst die eben genannte Bedingungen überprüft. In den nächsten Zeilen wird das aktuelle Symbol gesetzt, da dieser Wert an einigen Stellen verwendet wird. Mit dem aktuellen Wert können wir den Content des Buttons setzen, sodass er die Markierung des aktiven Spielers anzeigt. Danach merken wir uns in unserer Liste, welcher Spieler das Feld markiert hat – damit können wir später programmatisch einfacher bestimmen, ob ein Spieler gewonnen hat. Abschließend wird der Spieler gewechselt. Welcher Spieler am Zug ist, wird im Informationslabel angezeigt.

In der XAML weisen wir nun jedem Spielfeld-Button diese Funktion hinzu:

Wird einer dieser Buttons geklickt, ruft er nun die oben beschrieben Funktion auf. Dadurch können Spieler nun Felder innerhalb des Rasters markieren.

Schritt 2.3: Gewinner ermitteln

Jetzt können beide Spieler schon gegeneinander spielen – es gibt allerdings noch keine Abfrage, ob ein Spieler gewonnen hat. Wir brauchen also zum einen eine Funktion, die den Gewinner ermittelt und zum anderen müssen wir den Zeitpunkt bestimmen, an dem diese Prüfung stattfindet.

Beginnen wir mit der Funktion. Es gibt drei Fälle, in denen ein Spieler gewinnen kann: Entweder er hat eine Reihe, eine Spalte oder eine Diagonale für sich markiert. Es gibt diverse Möglichkeiten, wie genau das auf Basis unserer Felderliste geprüft werden kann. Bevor Sie sich den Code dazu ansehen, können Sie selbst darüber nachdenken, wie Sie an das Problem herangehen würden. Insbesondere eine dynamische Funktion zu schreiben, die das Problem auch für ein Raster mit mehr als 3 Feldern pro Reihe und Spalte löst, kann anspruchsvoll sein.

Für unser Beispiel schreiben wir eine einfache Funktion, die nur mit einem 3×3-Feld umgehen können muss:

Es genügt immer zu prüfen, ob die Werte in den drei relevanten Feldern gleich sind und nicht Player.None entsprechen – also unmarkiert sind. Für die Reihen können wir dazu mit einer foreach-Schleife jede Liste an Feldern betrachten. Dadurch werden die Reihen 1 bis 3 geprüft. Für die Spalten müssen wir von 0 bis 2 zählen, denn so erhalten wir den Index der aktuellen Spalte, die wir prüfen wollen. Nun werden die Werte in den Reihen an diesem jeweiligen Index miteinander verglichen. Zuletzt werden dann die beiden Diagonalen geprüft.

Diese Prüfung soll jedes Mal stattfinden, nachdem ein Spieler ein Feld markiert hat – also in der Button_Click-Methode nachdem die Liste mit den Felddaten aktualisiert wurde. Aus diesem Grund ist der Spieler, der gewinnt, auch immer der aktive Spieler.

Beim Einfügen des Aufrufs in die Button_Click-Methode muss folgendes beachtet werden: Wenn ein Spieler gewonnen hat, soll kein Spielerwechsel stattfinden und der neue Spieler, der am Zug ist, im Label eingetragen werden. Stattdessen wollen wir das Spiel beenden und anzeigen, welcher Spieler der Gewinner ist.

Schritt 2.4: Unentschieden feststellen

Nun haben wir sichergestellt, dass wir korrekt feststellen, ob ein Spieler gewonnen hat. Wer TicTacToe allerdings schon einmal gespielt hat, der weiß, dass das Spiel auch oft unentschieden ausgehen kann. Um zu ermitteln, ob ein Spiel unentschieden ist, gibt es mehrere Wege – so könnte beispielsweise geprüft werden, ob alle Felder belegt sind. Ein einfacherer Weg, für den wir den Inhalt des Feldes nicht noch einmal betrachten müssen, ist zu zählen wie viele Felder markiert werden. Haben wir das 9te Feld markiert, ohne einen Gewinner zu ermitteln, muss das Spiel unentschieden ausgegangen sein.

Um das Verhalten zu implementieren, erstellen wir eine private Variable fieldsMarked.

Diese Zahl zählen wir in der Button_Click-Methode immer nachdem ein Feld markiert wurde nach oben. Nach der Prüfung, ob ein Spieler gewonnen hat, setzen wir das Label auf „Unentschieden“ und beenden das Spiel, insofern bereits neun Felder markiert wurden und es keinen Gewinner gibt.

Damit sieht die gesamte Button_Click-Methode nun so aus:

Schritt 2.5: Neues Spiel starten

Zuletzt müssen die Spieler nun natürlich noch dazu in der Lage sein, ein neues Spiel zu starten. Dazu gibt es eine Funktion StartButton_Click:

Diese setzt die Variable gameActive wieder auf true und leert alle Felder – sowohl die Speicherung der Markierung, als auch die Anzeige auf den Buttons. Zuletzt müssen wir natürlich auch den Zähler für die markierten Felder zurücksetzen. Auch diese Funktion muss nun in der XAML einem Button zugewiesen werden, nämlich dem Start-Button:

Nach diesen Schritten sollte das Programm insgesamt bereits laufen. Im Editor sieht die Oberfläche noch so aus, wie wir sie im ersten Teil verlassen haben:

Wird das Spiel gestartet, sehen wir, dass wir nach und nach die Felder markieren können und entsprechende Informationen über den aktuellen Spielzustand erhalten:

Allerdings gibt es noch einige Stellen im Code, die verbessert werden können. Zudem wird das Programm nur aus Visual Studio heraus gestartet. Im letzten Teil des Tutorials werden wir daher noch auf einige Clean Code-Praktiken eingehen, lernen, wo die ausführbare Datei für den Code liegt und letzte Verbesserung an der Oberfläche vornehmen.

Hier der abschließende Code der MainWindow.xaml.cs:

Und der angepasste Code der MainWindow.xaml:

Ähnliche Produkte

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.