Читать книгу Dojos für Entwickler - Stefan Lieser - Страница 7

Vier Steine finden

Оглавление

Nun sind mit dem zweidimensionalen Array und der Methode LegeSteinInSpalte() bereits zwei Teilprobleme des Spielzustands gelöst: Im zweidimensionalen Array ist der Zustand des Spielbretts hinterlegt, und die Methode LegeSteinlnSpalteO realisiert die Platzierungslogik. Das dritte Problem ist die Erkennung von Vierergruppen, also eines Gewinners.

Vier zusammenhängende Steine können beim Vier-gewinnt-Spiel in vier Varianten auftreten: horizontal, vertikal, diagonal nach oben, diagonal nach unten.

Diese vier Varianten gilt es zu implementieren. Dabei ist wichtig zu beachten, dass die vier Steine unmittelbar zusammen liegen müssen, es darf sich also kein gegnerischer Stein dazwischen befinden.

Ich habe zuerst versucht, diese Vierergruppenerkennung direkt auf dem zweidimensionalen Array zu lösen. Dabei habe ich festgestellt, dass das Problem in zwei Teilprobleme zerlegt werden kann:

 Ermitteln der Indizes benachbarter Felder.

 Prüfung, ob vier benachbarte Felder mit Steinen gleicher Farbe besetzt sind.

Für das Ermitteln der Indizes habe ich daher jeweils eigene Klassen implementiert, welche die Logik der benachbarten Indizes enthalten. Eine solche Vierergruppe wird mit einem Startindex instanziert und liefert dann die Indizes der vier benachbarten Felder. Diese Vierergruppen werden anschließend verwendet, um im Spielfeld zu ermitteln, ob die betreffenden Felder alle Steine derselben Farbe enthalten. Die betreffenden Klassen heißen HorizontalerVierer, VertikalerVierer, DiagonalHochVierer und DiagonalRunterVierer. Listing 1 zeigt exemplarisch die Klasse HorizontalerVierer.

Listing 1: Vierergruppe ermitteln.

internal struct HorizontalerVierer : IVierer

{

private readonly int x;

private readonly int y;

public HorizontalerVierer(int x, int y) {

this.x = x;

this.y = y;

}

public Koordinate Eins {

get { return new Koordinate(x, y); }

}

public Koordinate Zwei {

get { return new Koordinate(x + 1, y); }

}

public Koordinate Drei {

get { return new Koordinate(x + 2, y); }

}

public Koordinate Vier {

get { return new Koordinate(x + 3, y); }

}

public override string ToString() {

return string.Format("Horizontal X: {0},

Y: {1}", x, y);

}

}

Zunächst fällt auf, dass die Klasse internal ist. Sie wird im Rahmen der Spiellogik nur intern benötigt, daher soll sie nicht außerhalb der Komponente sichtbar sein. Damit Unit-Tests für die Klasse möglich sind, habe ich auf der Assembly das Attribut InternalsVisibleTo gesetzt. Dadurch kann die Assembly, welche die Tests enthält, auf die internen Details zugreifen.

Aufgabe der Klasse HorizontalerVierer ist es, vier Koordinaten zu horizontal nebeneinander liegenden Spielfeldern zu liefern. Dies erfolgt in den Properties Eins, Zwei, Drei und Vier. Dort werden jeweils die Indizes ermittelt.

Das Ermitteln eines Gewinners geschieht anschließend in einem Flow aus zwei Schritten. Im ersten Schritt wird aus einem Spielfeld die Liste der möglichen Vierergruppen bestimmt. Im zweiten Schritt wird aus dem Spielfeld und den möglichen Vierergruppen ermittelt, ob eine der Vierergruppen Steine derselben Farbe enthält.

Die beiden Schritte des Flows sind als Extension Methods realisiert. Dadurch sind sie leicht isoliert zu testen. Anschließend können sie hintereinander ausgeführt, also als Flow zusammengeschaltet werden:

var gewinnerVierer = spielfeld

.AlleVierer()

.SelbeFarbe(spielfeld);

Der Flow wird an zwei Stellen verwendet: zum einen beim Ermitteln des Gewinners, zum anderen, um zu bestimmen, welche Steine zum Sieg geführt haben. Da die Methode AlleVierer() ein IEnumerable liefert und SelbeFarbe() dies als ersten Parameter erwartet, können die beiden Extension Methods hintereinander geschrieben werden. Da das Spielfeld in beiden Methoden benötigt wird, verfügt SelbeFarbe() über zwei Parameter.

Das Ermitteln von vier jeweils nebeneinander liegenden Feldern übernimmt die Methode AlleVierer(). Ein kurzer Ausschnitt zeigt die Arbeitsweise:

internal static IEnumerable<IVierer>

AlleVierer(this int[,] feld) {

for (var x = 0; x <=

feld.GetLength(0) - 4; x++) {

for (var y = 0; y <

feld.GetLength(1); y++) {

yield return new

HorizontalerVierer(x, y);

}

}

// Ebenso für Vertikal und Diagonal

}

Auch diese Methode ist internal, da sie außerhalb der Komponente nicht benötigt wird. In zwei geschachtelten Schleifen werden die Anfangsindizes von horizontalen Vierergruppen ermittelt. Für jeden Anfangsindex wird mit yield return eine Instanz eines HorizontalerVierers geliefert. Dieser übernimmt das Ermitteln der drei anderen Indizes.

Eine Alternative zur gezeigten Methode wäre, die möglichen Vierer als Konstanten zu hinterlegen. Es würde dann die Berechnung in AlleVierer() entfallen, ferner die Klassen HorizontalerVierer et cetera. Ob die Felder einer Vierergruppe alle mit Steinen der gleichen Farbe besetzt sind, analysiert die Methode SelbeFarbe(). Durch die Verwendung der Klassen HorizontalerVierer et cetera ist dies einfach: Jeder Vierer liefert seine vier Koordinaten. Damit muss nur noch im Spielfeld nachgesehen werden, ob sich an allen vier Koordinaten Steine gleicher Farbe befinden, siehe Listing 2.

Listing 2: Prüfen auf gleiche Farben.

internal static IEnumerable<IVierer> SelbeFarbe(this IEnumerable<IVierer> vierer,

int[,] feld) {

foreach (var vier in vierer) {

if ((feld[vier.Eins.X, vier.Eins.Y] != 0) &&

(feld[vier.Eins.X, vier.Eins.Y] == feld[vier.Zwei.X, vier.Zwei.Y]) &&

(feld[vier.Eins.X, vier.Eins.Y] == feld[vier.Drei.X, vier.Drei.Y]) &&

(feld[vier.Eins.X, vier.Eins.Y] == feld[vier.Vier.X, vier.Vier.Y])) {

yield return vier; } } }

Am Ende müssen die einzelnen Funktionseinheiten nur noch gemeinsam verwendet werden. Die dafür verantwortliche Klasse heißt VierGewinntSpiel. Sie ist public und repräsentiert nach außen die Komponente. Die Klasse ist für die Spielregeln zuständig. Da das abwechselnde Ziehen so einfach ist, habe ich mich entschlossen, diese Logik nicht auszulagern. In der Methode LegeSteinInSpalte(int spalte) wird der Zustand des Spiels aktualisiert. Dies geht ansatzweise wie folgt:

if (Zustand == Zustaende.RotIstAmZug) {

spielbrett.SpieleStein(Spieler.Rot,

spalte);

Zustand = Zustaende.GelbIstAmZug;

}

Es wird also ein entsprechender Spielstein

gelegt und anschließend ermittelt,

wer am Zug ist. Etwas später folgt dann die

Auswertung eines möglichen Gewinners:

if (spielbrett.Gewinner == Spieler.Rot){

Zustand = Zustaende.RotHatGewonnen;

}

Die Ermittlung eines Gewinners erfolgt also im Spielbrett, während hier nur der Zustand des Spiels verwaltet wird.

Fazit: Die richtigen Vorüberlegungen sind der Schlüssel zu einer erfolgreichen Implementierung. [ml]

[1] Stefan Lieser, Wer übt, gewinnt, dotnetpro 3/2010, Seite 118 f., www.dotnetpro.de/A1003dojo

Dojos für Entwickler

Подняться наверх