Vier Gewinnt

1. Aufgabe

Wer kennt es nicht, das gute alte Strategiespiel “Vier Gewinnt”? In dieser Aufgabe entwickeln wir eine einfache Version dieses Spiels, die zwei menschliche Wesen gegeneinander antreten lässt. Eine anspruchsvollere Version könnte den Computer als Spielpartner mit einbeziehen.

1.1 Spielregeln

Das Spiel beginnt mit einem leeren Spielfeld, welches 7 Felder breit und 6 Felder hoch ist. Abwechselnd wird jeweils von einem Spieler eine Kugel seiner Farbe gesetzt. Dabei wird die Kugel oben am Feld angesetzt und fällt dann unter Berücksichtigung der Schwerkraft nach unten, bis sie entweder ganz unten angekommen ist oder auf eine bereits liegende Kugel trifft. Der Spieler, dem es als erstes gelingt, eine gerade Reihe von 4 eigenen Kugeln zu bilden, hat gewonnen. Eine gültige 4er-Reihe ist horizontal, vertikal und auch diagonal möglich, siehe zum Beispiel die drei 4er-Reihen in Abbildung 1. Reihen, welche links oder rechts (sowie oben oder unten) an den Spielfeldrand stoßen, enden dort. Das heißt, es sind keine 4er Reihen erlaubt, die auf der entgegengesetzten Seite des Spielfeldes fortgesetzt werden. Wenn alle Felder belegt sind, ohne dass eine 4er Reihe durch einen Spieler gebildet werden konnte, endet das Spiel unentschieden.

Drei mögliche Spielsituationen, in denen der Spieler mit den schwarzen Kugeln gewonnen hat.

Abbildung 1. Drei mögliche Spielsituationen, in denen der Spieler mit den schwarzen Kugeln gewonnen hat.


Auf Grund der Komplexität der Aufgabenstellung teilen wir die Realisierung in mehrere Teilschritte wie folgt auf:

1.2 Implementierung der Oberfläche

Erstellen Sie eine .NET WPF Applikation mit einer Oberfläche, wie sie in Abbildung 2 vorgegeben ist.

Oberfläche der “Vier Gewinnt” Applikation nach dem Start.

Abbildung 2. Oberfläche der “Vier Gewinnt” Applikation nach dem Start.


Der Teilbereich des Spielfelds, der ausschließlich die Kugeln darstellt, ist in einem WPF-Steuerelement des Typs UniformGrid zu realisieren. Die Kugeln des Spiels sind Objekte des Typs Ellipse, die bei gleicher Breite und Höhe einen Kreis visualisieren.

1.3 Interaktion mit dem Benutzer

Das Betätigen der Maus über einer Spalte führt dazu, dass in der entsprechenden Spalte ein Zug (sofern zulässig) durchgeführt wird. Ist kein weiterer Zug möglich, erfolgt keine Reaktion. Ergänzen Sie die Ellipse-Objekte um eine entsprechende Verarbeitung von Mausereignissen. Die Kugeln des Spielfelds sind in Abhängigkeit vom Spieler in den Farben rot bzw. grün darzustellen.

Implementieren Sie die Clear-Schaltfläche in diesem Teilschritt. Nach dem Drücken der Schaltfläche sind alle gespielten Kugeln zu löschen, die Statusanzeige geht in den Grundzustand über.

1.4 Logik des Spiels

In diesem Teilschritt ist die Logik des Spiels zu implementieren. Stellen Sie das Ende des Spiels in einer Textkomponente (TextBlock-Objekt) mit einer entsprechenden Meldung dar (Game over, siehe Abbildung 3):

Oberfläche der “Vier Gewinnt” Applikation am Ende des Spiels.

Abbildung 3. Oberfläche der “Vier Gewinnt” Applikation am Ende des Spiels.

2. Lösung

Es genügt bei diesem einfachen Beispiel, die beiden Teile der Hauptklasse MainWindow zu betrachten: Ihre Definition und Realisierung ist auf die zwei Teile XAML und Code-Behind aufgeteilt, siehe Listing 1 und Listing 2:

001: <Window x:Class="WpfFourWins.MainWindow"
002:         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
003:         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
004:         Title="Four Wins" Height="500" Width="530">
005:     <DockPanel LastChildFill="True">
006: 
007:         <DockPanel DockPanel.Dock="tOP" LastChildFill="True">
008:             <Button
009:                 DockPanel.Dock="Left" FontSize="14" Margin="3" Width="100"
010:                 Click="Button_Click">Clear</Button>
011:             <TextBlock
012:                 FontSize="14" Margin="3" VerticalAlignment="Center"
013:                 Name="TextBlock_Status"></TextBlock>
014:         </DockPanel>
015: 
016:         <UniformGrid Rows="6"  Columns="7" Name="FourWinsBoard" Background="LightGray">
017:             <Ellipse Name="R5C0" Stroke="Black" StrokeThickness="5" Margin="2"
018:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
019:             <Ellipse Name="R5C1" Stroke="Black" StrokeThickness="5" Margin="2"
020:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
021:             <Ellipse Name="R5C2" Stroke="Black" StrokeThickness="5" Margin="2"
022:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
023:             <Ellipse Name="R5C3" Stroke="Black" StrokeThickness="5" Margin="2"
024:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
025:             <Ellipse Name="R5C4" Stroke="Black" StrokeThickness="5" Margin="2"
026:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
027:             <Ellipse Name="R5C5" Stroke="Black" StrokeThickness="5" Margin="2"
028:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
029:             <Ellipse Name="R5C6" Stroke="Black" StrokeThickness="5" Margin="2"
030:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
031: 
032:             <Ellipse Name="R4C0" Stroke="Black" StrokeThickness="5" Margin="2"
033:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
034:             <Ellipse Name="R4C1" Stroke="Black" StrokeThickness="5" Margin="2"
035:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
036:             <Ellipse Name="R4C2" Stroke="Black" StrokeThickness="5" Margin="2"
037:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
038:             <Ellipse Name="R4C3" Stroke="Black" StrokeThickness="5" Margin="2"
039:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
040:             <Ellipse Name="R4C4" Stroke="Black" StrokeThickness="5" Margin="2"
041:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
042:             <Ellipse Name="R4C5" Stroke="Black" StrokeThickness="5" Margin="2"
043:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
044:             <Ellipse Name="R4C6" Stroke="Black" StrokeThickness="5" Margin="2"
045:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
046: 
047:             <Ellipse Name="R3C0" Stroke="Black" StrokeThickness="5" Margin="2"
048:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
049:             <Ellipse Name="R3C1" Stroke="Black" StrokeThickness="5" Margin="2"
050:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
051:             <Ellipse Name="R3C2" Stroke="Black" StrokeThickness="5" Margin="2"
052:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
053:             <Ellipse Name="R3C3" Stroke="Black" StrokeThickness="5" Margin="2"
054:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
055:             <Ellipse Name="R3C4" Stroke="Black" StrokeThickness="5" Margin="2"
056:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
057:             <Ellipse Name="R3C5" Stroke="Black" StrokeThickness="5" Margin="2"
058:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
059:             <Ellipse Name="R3C6" Stroke="Black" StrokeThickness="5" Margin="2"
060:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
061: 
062:             <Ellipse Name="R2C0" Stroke="Black" StrokeThickness="5" Margin="2"
063:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
064:             <Ellipse Name="R2C1" Stroke="Black" StrokeThickness="5" Margin="2"
065:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
066:             <Ellipse Name="R2C2" Stroke="Black" StrokeThickness="5" Margin="2"
067:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
068:             <Ellipse Name="R2C3" Stroke="Black" StrokeThickness="5" Margin="2"
069:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
070:             <Ellipse Name="R2C4" Stroke="Black" StrokeThickness="5" Margin="2"
071:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
072:             <Ellipse Name="R2C5" Stroke="Black" StrokeThickness="5" Margin="2"
073:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
074:             <Ellipse Name="R2C6" Stroke="Black" StrokeThickness="5" Margin="2"
075:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
076: 
077:             <Ellipse Name="R1C0" Stroke="Black" StrokeThickness="5" Margin="2"
078:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
079:             <Ellipse Name="R1C1" Stroke="Black" StrokeThickness="5" Margin="2"
080:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
081:             <Ellipse Name="R1C2" Stroke="Black" StrokeThickness="5" Margin="2"
082:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
083:             <Ellipse Name="R1C3" Stroke="Black" StrokeThickness="5" Margin="2"
084:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
085:             <Ellipse Name="R1C4" Stroke="Black" StrokeThickness="5" Margin="2"
086:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
087:             <Ellipse Name="R1C5" Stroke="Black" StrokeThickness="5" Margin="2"
088:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
089:             <Ellipse Name="R1C6" Stroke="Black" StrokeThickness="5" Margin="2"
090:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
091: 
092:             <Ellipse Name="R0C0" Stroke="Black" StrokeThickness="5" Margin="2"
093:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
094:             <Ellipse Name="R0C1" Stroke="Black" StrokeThickness="5" Margin="2"
095:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
096:             <Ellipse Name="R0C2" Stroke="Black" StrokeThickness="5" Margin="2"
097:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
098:             <Ellipse Name="R0C3" Stroke="Black" StrokeThickness="5" Margin="2"
099:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
100:             <Ellipse Name="R0C4" Stroke="Black" StrokeThickness="5" Margin="2"
101:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
102:             <Ellipse Name="R0C5" Stroke="Black" StrokeThickness="5" Margin="2"
103:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
104:             <Ellipse Name="R0C6" Stroke="Black" StrokeThickness="5" Margin="2"
105:                      Fill="White" MouseDown="Ellipse_MouseDown"></Ellipse>
106:         </UniformGrid>
107:     </DockPanel>
108: </Window>

Beispiel 1. Klasse MainWindow der Vier Gewinnt-Anwendung: XAML.


Der XAML-Code in Listing 1 ist sehr länglich geraten, um nicht zu sagen, zu länglich. Trotzdem habe ich ihn so belassen, da ich im Code-Behind-Anteil bei dieser Übung nur die Logik ansiedeln wollte. Es wäre natürlich ein Einfaches gewesen, die Gestaltung des Spielbretts mit zwei for-Wiederholungsschleifen im Code-Behind-Anteil (Listing 2) erheblich kompakter zu programmieren:

001: namespace WpfFourWins
002: {
003:     public enum GameStatus
004:     {
005:         WhiteIsNext,
006:         BlackIsNext,
007:         WhiteHasWon,
008:         BlackHasWon
009:     }
010: 
011:     public partial class MainWindow : Window
012:     {
013:         private const int MaxRow = 6;
014:         private const int MaxCol = 7;
015: 
016:         // game status
017:         private GameStatus status;
018:         
019:         // game board
020:         private enum Stone { Empty, Black, White };
021:         private Stone[,] board;
022: 
023:         public MainWindow()
024:         {
025:             this.InitializeComponent();
026:             this.InitBoard();
027:             this.InitGame();
028:         }
029: 
030:         private void InitBoard()
031:         {
032:             if (this.board == null)
033:                 this.board = new Stone[MaxRow, MaxCol];
034: 
035:             // reset board
036:             for (int i = 0; i < MaxRow; i++)
037:                 for (int j = 0; j < MaxCol; j++)
038:                     this.board[i, j] = Stone.Empty;
039:         }
040: 
041:         private void InitGame()
042:         {
043:             this.status = GameStatus.WhiteIsNext; 
044:             this.InitBoard();
045:             this.DrawBoard();
046:             this.TextBlock_Status.Text = "";
047:             this.TextBlock_Status.Background = Brushes.White;
048:         }
049: 
050:         private void Button_Click(Object sender, RoutedEventArgs e)
051:         {
052:             this.InitGame();            
053:         }
054: 
055:         private void Ellipse_MouseDown(Object sender, MouseButtonEventArgs e)
056:         {
057:             // calculate cell
058:             String name = ((Ellipse) sender).Name;  // Format: "RxCy"
059:             int row = name[1] - '0';
060:             int col = name[3] - '0';
061: 
062:             if (!this.IsValidClick(row, col))
063:                 return;
064: 
065:             // update board
066:             this.board[row, col] =
067:                 (this.status == GameStatus.WhiteIsNext) ?
068:                     Stone.White : Stone.Black;
069: 
070:             // evaluate last stone
071:             if (this.IsGameOver(row, col, this.board[row, col]))
072:             {
073:                 this.status = (this.status == GameStatus.WhiteIsNext) ?
074:                     GameStatus.WhiteHasWon : GameStatus.BlackHasWon;
075:             }
076:             else
077:             {
078:                 this.status = (this.status == GameStatus.WhiteIsNext) ?
079:                     GameStatus.BlackIsNext : GameStatus.WhiteIsNext;
080:             }
081: 
082:             this.GameStatusChanged();
083:             this.DrawBoard();
084:         }
085: 
086:         private bool IsValidClick(int row, int col)
087:         {
088:             // check values
089:             if (row < 0 || row >= MaxRow)
090:                 return false;
091:             if (col < 0 || col >= MaxCol)
092:                 return false;
093: 
094:             // is position occupied
095:             if (this.board[row, col] != Stone.Empty)
096:                 return false;
097: 
098:             // is position valid
099:             for (int i = 0; i < row; i++)
100:                 if (this.board[i, col] == Stone.Empty)
101:                     return false;
102: 
103:             // game is already over
104:             if (this.status == GameStatus.WhiteHasWon ||
105:                 this.status == GameStatus.BlackHasWon)
106:                 return false;
107: 
108:             // position is okay
109:             return true;
110:         }
111: 
112:         private bool IsGameOver(int row, int col, Stone lastStone)
113:         {
114:             // handling vertical diagonal
115:             int count = 0;
116:             for (int i = 0; i < MaxRow; i++)
117:             {
118:                 if (this.board[i, col] == lastStone)
119:                 {
120:                     count++;
121:                     if (count == 4)
122:                         return true;
123:                 }
124:                 else
125:                     count = 0;
126:             }
127: 
128:             // handling horizontal diagonal
129:             count = 0;
130:             for (int j = 0; j < MaxCol; j++)
131:             {
132:                 if (this.board[row, j] == lastStone)
133:                 {
134:                     count++;
135:                     if (count == 4)
136:                         return true;
137:                 }
138:                 else
139:                     count = 0;
140:             }
141: 
142:             // handling first diagonal ([left,bottom] -> [right,top])
143:             int delta1 = Math.Min(row, col);
144:             int delta2 = Math.Min(MaxRow - row, MaxCol - col);
145: 
146:             int row1 = row - delta1;
147:             int col1 = col - delta1;
148:             int row2 = row + delta2;
149:             int col2 = col + delta2;
150: 
151:             count = 0;
152:             for (int i = row1, j = col1; i < row2 && j < col2; i++, j++)
153:             {
154:                 if (this.board[i, j] == lastStone)
155:                 {
156:                     count++;
157:                     if (count == 4)
158:                         return true;
159:                 }
160:                 else
161:                     count = 0;
162:             }
163: 
164:             // handling second diagonal ([left,top] -> [right,bottom])
165:             delta1 = Math.Min(MaxRow - row - 1, col);
166:             delta2 = Math.Min(row, MaxCol - col - 1);
167: 
168:             row1 = row + delta1;
169:             col1 = col - delta1;
170:             row2 = row - delta2;
171:             col2 = col + delta2;
172: 
173:             count = 0;
174:             for (int i = row1, j = col1; i >= row2 && j <= col2; i--, j++)
175:             {
176:                 if (this.board[i, j] == lastStone)
177:                 {
178:                     count++;
179:                     if (count == 4)
180:                         return true;
181:                 }
182:                 else
183:                     count = 0;
184:             }
185: 
186:             return false;
187:         }
188: 
189:         // private helper methods
190:         private void DrawBoard()
191:         {
192:             for (int i = 0; i < MaxRow; i++)
193:             {
194:                 for (int j = 0; j < MaxCol; j++)
195:                 {
196:                     Stone s = this.board[MaxRow - i - 1, j];
197:                     Brush b =
198:                         (s == Stone.White) ? Brushes.Green :
199:                         (s == Stone.Black) ? Brushes.Red : Brushes.LightBlue;
200: 
201:                     int index = i * MaxCol + j;
202:                     Ellipse ell = (Ellipse) this.FourWinsBoard.Children[index];
203:                     ell.Fill = b;
204:                 }
205:             }
206:         }
207: 
208:         private void GameStatusChanged()
209:         {
210:             switch (this.status)
211:             {
212:                 case GameStatus.WhiteIsNext:
213:                     this.TextBlock_Status.Text = "1. Player is next ...";
214:                     break;
215: 
216:                 case GameStatus.BlackIsNext:
217:                     this.TextBlock_Status.Text = "2. Player is next ...";
218:                     break;
219: 
220:                 case GameStatus.WhiteHasWon:
221:                     this.TextBlock_Status.Background = Brushes.Yellow;
222:                     this.TextBlock_Status.Text = "Game over: 1. Player has won !!!";
223:                     break;
224: 
225:                 case GameStatus.BlackHasWon:
226:                     this.TextBlock_Status.Background = Brushes.Yellow;
227:                     this.TextBlock_Status.Text = "Game over: 2. Player has won !!!";
228:                     break;
229:             }
230:         }
231:     }
232: }

Beispiel 2. Klasse MainWindow der Vier Gewinnt-Anwendung: Code-Behind.