Othello in Java: Teil 1: Datenstrukturen
Du hast ein großes Problem. Jemand zwingt ermutigt dich, eine komplette Othello UI+KI in Java zu implementieren, aber du hast keine Ahnung, wie das gehen soll. Wenn du bereits weißt, wie man die Grundlagen implementiert und an fortgeschrittenen Strategiekonzepten interessiert bist, könnten die anderen Teile dieser Serie (noch in Arbeit) für dich interessant sein.
In dieser mehrteiligen Serie werde ich keine kompletten Lösungen für die Standard-Othello-Aufgaben bereitstellen. Stattdessen werde ich (hoffentlich) hilfreiche Tipps geben, wie man mit dem Programmieren anfangt, und erklären, wie der Code funktioniert.
Bearbeiten & Kompilieren
Wenn du ein Projekt wie Othello entwickeln möchtest (es ist nicht wirklich schwierig, aber auch nicht extrem einfach), solltest du bereits eine Möglichkeit gefunden haben, deinen Code zu bearbeiten und zu kompilieren. Wenn du deinen Code in einem normalen Texteditor bearbeitest und javac ausführst, um .class-Dateien zu erhalten, höre hier auf! Mach das nicht für Othello oder noch größere Projekte. Das Einzige, was es tun wird, ist gigantische Mengen deiner Zeit zu verschwenden. Glaub mir, du wirst die hier beschriebenen Techniken ohnehin für komplexere Projekte brauchen, also jetzt ist der Zeitpunkt, damit anzufangen.
Was du brauchst, ist etwas, das IDE (Integrated Development Environment) genannt wird – im Wesentlichen eine übersichtliche grafische Benutzeroberfläche mit einem intelligenten Editor, der deine Syntaxfehler in Echtzeit anzeigt, und einem „Run“-Button, der im Wesentlichen javac aufruft, wo nötig, und dann dein Programm startet. IDEs haben weitaus fortgeschrittenere Funktionen als diese, aber kümmere dich hier nicht darum.
Im Wesentlichen gibt es zwei große IDEs: NetBeans und Eclipse. Alles darüber hinaus ist entweder teuer (und nicht unbedingt besser) oder funktioniert nicht gut für diesen Zweck. Letztendlich ist es nicht wirklich wichtig, welche man wählt, aber ich empfehle dringend NetBeans wegen der deutlich besseren Integration mit fortgeschrittenen Werkzeugen wie einem GUI-Builder (ein Programm, in dem man die grafische Oberfläche zusammenklicken kann, anstatt den Code selbst zu schreiben) und anderen Gründen, die weit über den Rahmen dieses Artikels hinausgehen und möglicherweise in zukünftigen Beiträgen beschrieben werden.
Auf der NetBeans-Downloadseite musst du die Version „Java SE“ herunterladen. Die zusätzlichen Funktionen der anderen Versionen sind für Othello nicht relevant.
Das Othello-Spielbrett
Werfen wir einen Blick auf das grundlegende Spielbrett, das wir in eine Java-Klasse fitten müssen. Es ist ein einfaches 8x8-Brett – jede Zelle auf dem Brett kann jederzeit in drei verschiedenen Zuständen sein:
- Leer (normalerweise grün dargestellt, da dies die am häufigsten verwendete Farbe für die zugrunde liegende Leinwand ist)
- Vom weißen Spieler besetzt
- Vom schwarzen Spieler besetzt
Es gibt zahlreiche verschiedene Möglichkeiten, diese drei Zustände in Java auszudrücken. Aus Gründen der Übersichtlichkeit werde ich in diesem Artikel nur eine beschreiben: Enums. Wenn du noch nie davon gehört hast, schau dir das Java-Tutorial zu Enums an. Enums sind eine Möglichkeit, das, was man modellieren möchte, klarer auszudrücken als Alternativen wie Integer (z.B. könnte man sagen, dass 0 gleich leer, 1 gleich weiß und 2 gleich schwarz ist).
Zell-Datenstrukturen
Ausgehend nur von den obigen Informationen über das Brett können wir leicht ein Enum für den Zustand einer Zelle definieren:
public enum CellState {
EMPTY,
WHITE,
BLACK
}Brett-Datenstrukturen
Nun haben wir einen einfachen Objekttyp, der eine einzelne Zelle repräsentiert – aber wie können wir das 8x8-Brett aus Zellen implementieren? Auch hier gibt es zahlreiche verschiedene Möglichkeiten. Ich habe die offensichtlichste gewählt: Mehrdimensionale Arrays. Wenn du nicht mit ihnen vertraut bist, lies das Java-Tutorial.
In einer neuen Klasse, die das Brett repräsentiert, können wir nun ein zweidimensionales Array des zuvor definierten CellState-Enum-Typs deklarieren. Am besten deklariert man das Array als Instanz-Variable der gerade erstellten Klasse.
private CellState[][] board = new CellState[8][8];Wenn du das getan hast, hast du eine Datenstruktur, die jedes mögliche Othello-Brett repräsentieren kann.
Wenn wir ein neues Spiel starten möchten, müssen wir eine Instanz dieser Datenstruktur ordnungsgemäß initialisieren. Die einfachste Möglichkeit dazu ist der Konstruktor. Der Code im Konstruktor sollte etwa so aussehen:
//Alle Zellen auf leer setzen
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 8; y++) {
board[x][y] = OthelloCellState.EMPTY;
}
}
//Die 4 Startsteine setzen
board[4][3] = OthelloCellState.BLACK;
board[3][4] = OthelloCellState.BLACK;
board[3][3] = OthelloCellState.WHITE;
board[4][4] = OthelloCellState.WHITE;Wenn man ein mit diesem Code initialisiertes Brett mit einer geeigneten GUI anzeigt, sollte es ähnlich wie das Bild oben aussehen.
Ein wichtiger Aspekt, den man sich im obigen Code merken sollte, ist die verschachtelte for-Schleife, die über die x- und y-Koordinaten des Bretts iteriert, um alle Zellen auf leer zu setzen. Da Array-Indizes bei 0 beginnen (und daher der letzte gültige Index eines Arrays der Größe 8 bei 7 liegt), hat die am weitesten links oben liegende Zelle die Koordinaten 0,0, während die am weitesten rechts unten liegende Zelle die Koordinaten 7 hat. Beide Schleifen (für x- und y-Koordinaten) müssen daher bei 0 beginnen und bei 7 enden. Man wird diesen Code definitiv in anderen Teilen der Othello-Implementierung benötigen.
Um den Code übersichtlich zu halten (d.h. sich nicht selbst zu verwirren), sollte man sich entweder an Indizes, die bei 0 beginnen, ODER an Indizes, die bei 1 beginnen, halten. Da es in Java keinen einfachen Ansatz gibt, Arrays mit bei 1 beginnenden Indizes zu verwenden, bleibe ich bei sogenannten 0-basierten Indizes.
Nun, da wir ein ordnungsgemäß initialisiertes Brett haben, können wir Instanzmethoden der Brett-Klasse definieren, um leichter darauf zuzugreifen, zum Beispiel:
/**
* @param x Die x-Koordinate (0-basiert) der Zelle
* @param y Die y-Koordinate (0-basiert) der Zelle
* @return True, wenn die durch die Koordinaten beschriebene Zelle leer ist, sonst false
*/
public boolean isEmpty(int x, int y) {
return board[x][y] == OthelloCellState.EMPTY;
}Als Übung empfehle ich dir dringend, die folgenden Instanzmethoden selbst zu implementieren (die Methode soll genau das tun, was in dem Kommentar direkt darüber beschrieben ist. Das Format des Kommentars ist standardisiertes Javadoc). Glaub mir, es ist extrem einfach, gegeben das obige Beispiel:
/**
* @param x Die x-Koordinate (0-basiert) der Zelle
* @param y Die y-Koordinate (0-basiert) der Zelle
* @return True, wenn die durch die Koordinaten beschriebene Zelle einen weißen
* Stein enthält, sonst false
*/
public boolean isWhite(int x, int y) {
//Dein Code kommt hier hin
}
/**
* @param x Die x-Koordinate (0-basiert) der Zelle
* @param y Die y-Koordinate (0-basiert) der Zelle
* @return True, wenn die durch die Koordinaten beschriebene Zelle einen schwarzen
* Stein enthält, sonst false
*/
public boolean isBlack(int x, int y) {
//Dein Code kommt hier hin
}Nach der Implementierung dieser Funktionen solltest du eine funktionierende Datenstruktur für Othello haben. Wahrscheinlich wirst du weitere Methoden für deine Spieler-Implementierung benötigen, aber du kannst sie später bei Bedarf hinzufügen.
Hinweis: Ich habe dies ursprünglich als mehrteiliges Tutorial geplant, aber nie zu Teil 2 gekommen (zumindest noch nicht). Einer der Gründe dafür ist, dass das Schreiben von Othello-Implementierungen mittlerweile ein wirklich unwichtiger Teil meines Lebens ist.