Schiffe versenken gegen einen einfachen Gegner
Schiffe versenken gegen einen verbesserten KI-Gegner
Wir werden Windows-Programme mit C# in Visual Studio 2022
erstellen.
Wir schauen uns nun an, wie man mit einer Windows Forms App
beginnt.
Hoffentlich funktioniert das bei Ihnen genau so. Wenn ja, ist der praktische Einstieg in die Programmierung mit WindowsForms (.NET) und C# gelungen.
Nun gehen wir ein beliebtes Spiel an: Tic-Tac-Toe. Dies
ist eine interessante Einsteigerübung, da hier ein Spielfluss programmiert
werden muss. Zunächst erstellen wir das Projekt im MS Visual Studio 2022:
Fügen Sie auf der Form (Form1.cs) folgende Steuerelemente hinzu (rechts die Toolbox öffnen):
Anordnung:
btn1
,
btn2
,
btn3
,
..., btn9
.
Das Feld Text machen Sie leer.
lblStatus
heißen. Das Feld Text machen Sie leer.
btnRestart
heißen. Im Feld Text sollte "Restart" stehen.Da muss nun noch einiges in der Form- und Ablauflogik geändert werden:
Gehen Sie nach Form1.cs und
wählen Sie in der Ansicht "Code". So sollte das aussehen:
using
System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using
System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp_TicTacToe
{
public partial class Form1 :
Form
{
// Variablen für das Spiel
private string currentPlayer = "X";
private int moveCount = 0;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
lblStatus.Text = "Spieler X ist an der Reihe";
}
private
void btn_Click(object sender, EventArgs e)
{
Button btn =
(Button)sender;
if (btn.Text == "")
{
btn.Text = currentPlayer;
moveCount++;
if (CheckForWinner())
{
lblStatus.Text = $"Spieler {currentPlayer} gewinnt!";
DisableButtons();
}
else if
(moveCount == 9)
{
lblStatus.Text = "Unentschieden!";
}
else
{
currentPlayer =
(currentPlayer == "X") ? "O" : "X";
lblStatus.Text = $"Spieler {currentPlayer} ist an der Reihe";
}
// Sicherstellen, dass der
Text auf dem Restart-Button korrekt bleibt
btnRestart.Text = "Restart";
btnRestart.Invalidate();
// Erzwinge das Neuzeichnen des Buttons
btnRestart.Update();
}
}
private bool CheckForWinner()
{
bool winner = false;
// Horizontale
Überprüfung
if ((btn1.Text == currentPlayer && btn2.Text ==
currentPlayer && btn3.Text == currentPlayer) ||
(btn4.Text ==
currentPlayer && btn5.Text == currentPlayer && btn6.Text == currentPlayer) ||
(btn7.Text == currentPlayer && btn8.Text == currentPlayer && btn9.Text ==
currentPlayer))
{
winner = true;
}
// Vertikale Überprüfung
else
if ((btn1.Text == currentPlayer && btn4.Text == currentPlayer && btn7.Text ==
currentPlayer) ||
(btn2.Text == currentPlayer && btn5.Text ==
currentPlayer && btn8.Text == currentPlayer) ||
(btn3.Text ==
currentPlayer && btn6.Text == currentPlayer && btn9.Text == currentPlayer))
{
winner = true;
}
// Diagonale Überprüfung
else if ((btn1.Text == currentPlayer
&& btn5.Text == currentPlayer && btn9.Text == currentPlayer) ||
(btn3.Text == currentPlayer && btn5.Text == currentPlayer && btn7.Text ==
currentPlayer))
{
winner = true;
}
return winner;
}
private void
DisableButtons()
{
foreach (Control c in Controls)
{
Button btn = c as Button;
if (btn
!= null && btn != btnRestart) // Doppelte
Klicks
auf einen Button vermeiden und Restart-Button nicht deaktivieren
{
btn.Enabled = false;
}
}
}
private void btnRestart_Click(object sender, EventArgs e)
{
currentPlayer = "X";
moveCount = 0;
lblStatus.Text =
"Spieler X ist an der Reihe";
foreach (Control c in Controls)
{
Button btn = c as Button;
if (btn
!= null)
{
btn.Text =
"";
btn.Enabled = true;
}
}
}
}
}
Die Fontgröße der neun Buttons (3 x 3) habe ich in der
Designer-Ansicht bei Eigenschaften
(Rechtsklick) auf 24 geändert.
Beachten Sie: Man kann alle neun Buttons auf
einmal(!) auswählen und dies erledigen.
Das Label hat Fontgröße 10. Der
Restart-Button wird ebenfalls auf 10 eingestellt.
Anstelle der
Designer-Ansicht kann man das auch direkt im Code erledigen, aber im Designer
geht es sicherer.
In der wichtigen Funktion
InitializeComponent() müssen wir noch einiges manuell hinzufügen.
Am
besten geht das mit "Definition einsehen" (Rechtsklick, Kontextmenü):
private void InitializeComponent()
{
this.btn1 =
new System.Windows.Forms.Button();
this.btn2 = new
System.Windows.Forms.Button();
this.btn3 = new System.Windows.Forms.Button();
this.btn4 = new System.Windows.Forms.Button();
this.btn5 = new
System.Windows.Forms.Button();
this.btn6 = new System.Windows.Forms.Button();
this.btn7 = new System.Windows.Forms.Button();
this.btn8 = new
System.Windows.Forms.Button();
this.btn9 = new System.Windows.Forms.Button();
this.lblStatus = new System.Windows.Forms.Label();
this.btnRestart = new
System.Windows.Forms.Button();
this.SuspendLayout();
//
// btn1
//
this.btn1.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn1.Location = new System.Drawing.Point(34, 32);
this.btn1.Name = "btn1";
this.btn1.Size = new System.Drawing.Size(93, 86);
this.btn1.TabIndex = 0;
this.btn1.UseVisualStyleBackColor = true;
this.btn1.Click += new
System.EventHandler(this.btn_Click);
//
// btn2
//
this.btn2.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn2.Location = new System.Drawing.Point(133, 32);
this.btn2.Name = "btn2";
this.btn2.Size = new System.Drawing.Size(93, 86);
this.btn2.TabIndex = 1;
this.btn2.UseVisualStyleBackColor = true;
this.btn2.Click += new
System.EventHandler(this.btn_Click);
//
// btn3
//
this.btn3.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn3.Location = new System.Drawing.Point(232, 32);
this.btn3.Name = "btn3";
this.btn3.Size = new System.Drawing.Size(93, 86);
this.btn3.TabIndex = 2;
this.btn3.UseVisualStyleBackColor = true;
this.btn3.Click += new
System.EventHandler(this.btn_Click);
//
// btn4
//
this.btn4.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn4.Location = new System.Drawing.Point(34, 124);
this.btn4.Name = "btn4";
this.btn4.Size = new System.Drawing.Size(93, 86);
this.btn4.TabIndex = 3;
this.btn4.UseVisualStyleBackColor = true;
this.btn4.Click += new
System.EventHandler(this.btn_Click);
//
// btn5
//
this.btn5.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn5.Location = new System.Drawing.Point(133, 124);
this.btn5.Name = "btn5";
this.btn5.Size = new System.Drawing.Size(93, 86);
this.btn5.TabIndex = 4;
this.btn5.UseVisualStyleBackColor = true;
this.btn5.Click += new
System.EventHandler(this.btn_Click);
//
// btn6
//
this.btn6.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn6.Location = new System.Drawing.Point(232, 124);
this.btn6.Name = "btn6";
this.btn6.Size = new System.Drawing.Size(93, 86);
this.btn6.TabIndex = 5;
this.btn6.UseVisualStyleBackColor = true;
this.btn6.Click += new
System.EventHandler(this.btn_Click);
//
// btn7
//
this.btn7.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn7.Location = new System.Drawing.Point(34, 216);
this.btn7.Name = "btn7";
this.btn7.Size = new System.Drawing.Size(93, 86);
this.btn7.TabIndex = 6;
this.btn7.UseVisualStyleBackColor = true;
this.btn7.Click += new
System.EventHandler(this.btn_Click);
//
// btn8
//
this.btn8.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn8.Location = new System.Drawing.Point(133, 216);
this.btn8.Name = "btn8";
this.btn8.Size = new System.Drawing.Size(93, 86);
this.btn8.TabIndex = 7;
this.btn8.UseVisualStyleBackColor = true;
this.btn8.Click += new
System.EventHandler(this.btn_Click);
//
// btn9
//
this.btn9.Font = new
System.Drawing.Font("Microsoft Sans Serif", 24F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btn9.Location = new System.Drawing.Point(232, 216);
this.btn9.Name = "btn9";
this.btn9.Size = new System.Drawing.Size(93, 86);
this.btn9.TabIndex = 8;
this.btn9.UseVisualStyleBackColor = true;
this.btn9.Click += new
System.EventHandler(this.btn_Click);
//
// lblStatus
//
this.lblStatus.AutoSize = true;
this.lblStatus.Font = new System.Drawing.Font("Microsoft Sans Serif", 10.2F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.lblStatus.Location = new System.Drawing.Point(31, 331);
this.lblStatus.Name = "lblStatus";
this.lblStatus.Size = new
System.Drawing.Size(0, 20);
this.lblStatus.TabIndex = 9;
//
// btnRestart
//
this.btnRestart.Font = new
System.Drawing.Font("Microsoft Sans Serif", 10F,
System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point,
((byte)(0)));
this.btnRestart.ForeColor = System.Drawing.Color.Black;
this.btnRestart.Location = new System.Drawing.Point(34, 378);
this.btnRestart.Name = "btnRestart";
this.btnRestart.Size = new
System.Drawing.Size(137, 44);
this.btnRestart.TabIndex = 10;
this.btnRestart.Text = "Restart";
this.btnRestart.UseVisualStyleBackColor =
true;
this.btnRestart.Click += new
System.EventHandler(this.btnRestart_Click);
//
// Form1
//
this.AutoScaleDimensions = new
System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode =
System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new
System.Drawing.Size(359, 450);
this.Controls.Add(this.btnRestart);
this.Controls.Add(this.lblStatus);
this.Controls.Add(this.btn9);
this.Controls.Add(this.btn8);
this.Controls.Add(this.btn7);
this.Controls.Add(this.btn6);
this.Controls.Add(this.btn5);
this.Controls.Add(this.btn4);
this.Controls.Add(this.btn3);
this.Controls.Add(this.btn2);
this.Controls.Add(this.btn1);
this.Name =
"Form1";
this.Text = "Tic-Tac-Toe";
this.Load += new System.EventHandler(this.Form1_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
Nun können wir das Programm erstellen.
So sollte
das Programm aussehen, während Sie ihre X und O eingeben:
Wenn das Spiel nach neun abwechselnden Eingaben zu
Ende ist, wechselt die Ansicht.
Wichtig ist, dass der Button "Restart" nicht
ausgegraut/deaktiviert wird:
Nach dem Klick auf Restart, geht es nun so weiter:
Sobald das erste X eingegeben wird, erscheint der
Text für Restart erneut.
Man kann mit obiger Logik somit auch mitten im Ablauf
neu starten.
Ich hoffe es klappt bei Ihnen soweit.
Hier noch einmal eine Zusammenfassung mit Ausblick:
Projektziel:
Erstellung eines einfachen Tic-Tac-Toe-Spiels, bei dem
zwei Spieler abwechselnd X und O auf einem 3x3-Raster setzen.
Das Spiel
erkennt einen Gewinner oder ein Unentschieden und bietet eine Möglichkeit zum
Neustart.
Spielzustandsverwaltung: Das Spiel
verfolgt den aktuellen Spieler (X oder O) und die Anzahl der Züge, die
gemacht wurden.
Diese Informationen werden verwendet, um den Spielstatus
zu aktualisieren und zu bestimmen, wann ein Spiel endet.
Interaktion und Ereignisbehandlung:
Gewinnererkennung:
Unentschieden-Erkennung:
Dynamische Statusanzeige:
Das
Label wird dynamisch aktualisiert, um dem Benutzer den aktuellen Status des
Spiels anzuzeigen, wie z. B. welcher Spieler an der Reihe ist, wer gewonnen
hat oder ob das Spiel unentschieden ist.
Interaktive Spielfeld-Buttons: Die Buttons reagieren auf Benutzereingaben, indem sie das entsprechende Symbol (X oder O) anzeigen und sich nach einem Spielzug deaktivieren, um weitere Eingaben zu verhindern.
Neustartfunktion: Nach Abschluss
eines Spiels ermöglicht der Neustart-Button dem Benutzer, das Spiel schnell
und einfach zurückzusetzen, ohne die Anwendung neu starten zu müssen.
Verbesserte Grafiken: Anstelle von Standard-Buttons und Text könnte das Spiel mit benutzerdefinierten Grafiken für X und O sowie einem ansprechenderen UI-Design erweitert werden.
Multiplayer-Option: Implementierung eines Netzwerk- oder lokalen Mehrspielermodus, bei dem zwei Spieler auf getrennten Geräten spielen können.
KI-Gegner: Hinzufügen einer einfachen KI, gegen die ein Spieler antreten kann.
Soundeffekte und Animationen: Das Hinzufügen von Soundeffekten und Animationen für Spielzüge, Siege und das Zurücksetzen des Spiels könnte das Spielerlebnis weiter verbessern.
Das nächste Windows-Programm wird etwas anspruchsvoller.
Wir erstellen eine Basis-Version des Klassikers "Schiffe versenken". Zunächst
die Designer-Ansicht unseres Spiels:
Wir haben 100 Buttons im Spieler-Feld und 100 Buttons im Gegner-Feld. Jeweils 100 Buttons werden einem "Panel" (siehe Toolbox) zugeordnet. Ansonsten gibt es ein Label für Textausgaben und einen Start-Button.
Als Namen wählen wir:
btnPlayer00, btnPlayer01, ... btnPlayer99
btnEnemy00,
btnEnemy01, ... btnEnemy99
panelPlayerGrid
panelEnemyGrid
labelStatus
btnStart
Wir fügen eine Klasse hinzu. Das geht durch Rechtsklick
auf unser Projekt, Hinzufügen, Klasse. Wir erzeugen auf diese Weise die Klasse
"Ship" im neuen Modul "Ship.cs":
using
System.Collections.Generic;
using System.Windows.Forms;
namespace
WindowsFormsApp_BattleShip
{
public class Ship
{
public string Name { get; set; }
public int Size { get;
set; }
public List<Button> Position { get; set; } = new
List<Button>();
public bool IsVertical { get; set; } =
true; // Standardausrichtung
public bool IsSunk { get; set; } = false; // Gesunken?
public Ship(string name, int size)
{
Name = name;
Size = size;
}
}
}
In Form1.cs findet sich folgender Code, der den
Ablauf steuert:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace WindowsFormsApp_BattleShip
{
public partial class Form1 :
Form
{
private Button[,] playerButtons = new Button[10, 10];
private
Button[,] enemyButtons = new Button[10, 10];
public Form1()
{
InitializeComponent();
this.KeyPreview = true; //
Ermöglicht es dem Formular, KeyDown-Ereignisse zu erfassen
this.KeyDown += new KeyEventHandler(Form1_KeyDown);
InitializeGame();
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if
(e.KeyCode == Keys.Space && currentShip != null)
{
// Ändere die Ausrichtung des aktuellen Schiffs
currentShip.IsVertical = !currentShip.IsVertical;
// Zeige die aktuelle Ausrichtung an
lblStatus.Text = $"Platzieren Sie das {currentShip.Name}
({(currentShip.IsVertical ? "Vertikal" : "Horizontal")})";
}
}
private List<Ship> playerShips = new List<Ship>
{
new Ship("Submarine",
1),
new Ship("Destroyer", 2),
new Ship("Cruiser", 3),
new
Ship("Battleship", 4),
new Ship("Carrier", 5)
};
private List<Ship>
enemyShips = new List<Ship>
{
new Ship("Submarine", 1),
new
Ship("Destroyer", 2),
new Ship("Cruiser", 3),
new Ship("Battleship", 4),
new Ship("Carrier", 5)
};
private Ship currentShip = null;
// Das Schiff, das gerade platziert wird
private int shipIndex = 0; // Index des aktuellen
Schiffs in der Liste
private void InitializeGame()
{
// Spielerbuttons initialisieren
for (int i
= 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
string buttonName
= "btnPlayer" + i.ToString("D1") + j.ToString("D1");
playerButtons[i, j] =
this.Controls.Find(buttonName, true).FirstOrDefault() as Button;
playerButtons[i, j].Click += PlayerButton_Click;
}
}
// KI-Buttons initialisieren
for (int i = 0;
i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
string buttonName =
"btnEnemy" + i.ToString("D1") + j.ToString("D1");
enemyButtons[i, j] =
this.Controls.Find(buttonName, true).FirstOrDefault() as Button;
enemyButtons[i, j].Click += EnemyButton_Click;
}
}
// Statusanzeige initialisieren
lblStatus.Text = "Platzieren Sie Ihr Submarine!";
}
private void
PlayerButton_Click(object sender, EventArgs e)
{
Button clickedButton =
sender as Button;
// Logik zur Platzierung von
Schiffen
if (currentShip == null && shipIndex < playerShips.Count)
{
currentShip = playerShips[shipIndex];
}
if (currentShip != null)
{
if (CanPlaceShip(clickedButton))
{
PlaceShip(clickedButton);
shipIndex++;
if (shipIndex < playerShips.Count)
{
currentShip =
playerShips[shipIndex];
lblStatus.Text = $"Platzieren Sie das
{currentShip.Name}";
}
else
{
currentShip = null;
lblStatus.Text
= "Alle Schiffe platziert. Starten Sie das Spiel!";
}
}
else
{
MessageBox.Show("Ungültige Platzierung. Das Schiff überschneidet sich oder ist
außerhalb des Spielfelds.");
}
}
}
private bool
CanPlaceShip(Button startButton)
{
// Finde die
Position des Buttons im Spielfeld
int row = -1, col = -1;
// Durchlaufe das Spielfeld, um die Position des
Buttons zu finden
for (int i = 0; i < 10; i++)
{
for (int j = 0;
j < 10; j++)
{
if (playerButtons[i, j] == startButton)
{
row = i;
col = j;
break;
}
}
if (row != -1) break;
}
// Wenn der Button nicht gefunden wurde, gib false
zurück
if (row == -1 || col == -1) return false;
// Überprüfe die Platzierung basierend auf der
Ausrichtung (vertikal/horizontal)
if (currentShip.IsVertical)
{
// Überprüfe, ob das Schiff innerhalb des Spielfelds
platziert werden kann
if (row + currentShip.Size > 10) return false;
// Überprüfe, ob sich das Schiff mit einem anderen
überschneidet
for (int i = 0; i < currentShip.Size; i++)
{
if
(playerButtons[row + i, col].BackColor == Color.LightGray)
// Bereits belegt
{
return false;
}
}
}
else
{
// Überprüfe, ob das Schiff
innerhalb des Spielfelds platziert werden kann
if (col +
currentShip.Size > 10) return false;
//
Überprüfe, ob sich das Schiff mit einem anderen überschneidet
for (int
i = 0; i < currentShip.Size; i++)
{
if (playerButtons[row, col +
i].BackColor == Color.LightGray) // Bereits belegt
{
return false;
}
}
}
return true;
}
private void
PlaceShip(Button startButton)
{
int row = -1, col = -1;
for (int i = 0;
i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
if (playerButtons[i,
j] == startButton)
{
row = i;
col = j;
break;
}
}
if (row
!= -1) break;
}
if (row == -1 || col == -1) return;
if
(currentShip.IsVertical)
{
for (int i = 0; i < currentShip.Size; i++)
{
playerButtons[row + i, col].BackColor = Color.LightGray;
playerButtons[row +
i, col].Tag = "Ship";
playerButtons[row + i, col].Text =
currentShip.Name[0].ToString(); // Zeige den
Anfangsbuchstaben des Schiffs
currentShip.Position.Add(playerButtons[row + i, col]);
}
}
else
{
for (int i = 0; i < currentShip.Size; i++)
{
playerButtons[row, col +
i].BackColor = Color.LightGray;
playerButtons[row, col + i].Tag = "Ship";
playerButtons[row, col + i].Text = currentShip.Name[0].ToString();
// Zeige den Anfangsbuchstaben des Schiffs
currentShip.Position.Add(playerButtons[row, col + i]);
}
}
MessageBox.Show($"{currentShip.Name} platziert!", "Schiff platziert",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void
KIMove()
{
Random rand = new Random();
bool shotFired = false;
while (!shotFired)
{
int row = rand.Next(0, 10);
int col = rand.Next(0,
10);
Button targetButton = playerButtons[row, col];
// Überprüfen, ob der Button bereits angeklickt wurde
if (targetButton.BackColor == Color.LightSkyBlue || targetButton.BackColor ==
Color.Orange)
{
continue; // Wähle einen neuen
Schuss, wenn das Feld schon beschossen wurde
}
// Überprüfen, ob der Schuss ein Schiff getroffen hat
if (targetButton.Tag != null && targetButton.Tag.ToString() == "Ship")
{
targetButton.BackColor = Color.LightSkyBlue; //
Hellblau zeigt einen Treffer an
CheckIfShipSunk(playerShips,
targetButton);
}
else
{
targetButton.BackColor = Color.Orange;
// Schuss ins Wasser
}
shotFired =
true; // Schuss wurde abgegeben
}
// Nachdem die KI ihren Zug gemacht hat, ist wieder
der Spieler dran
lblStatus.Text = "Spieler ist am Zug";
}
private void EnemyButton_Click(object sender, EventArgs e)
{
Button
clickedButton = sender as Button;
if (clickedButton.BackColor ==
Color.LightSkyBlue || clickedButton.BackColor == Color.Orange)
{
MessageBox.Show("Sie haben hier bereits geschossen.", "Achtung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
lblStatus.Text = "Spieler ist am Zug";
if (clickedButton.Tag != null &&
clickedButton.Tag.ToString() == "Ship")
{
clickedButton.BackColor =
Color.LightSkyBlue;
CheckIfShipSunk(enemyShips, clickedButton);
}
else
{
clickedButton.BackColor = Color.Orange;
}
lblStatus.Text = "KI
ist am Zug";
// Timer erstellen
Timer
timer = new Timer();
timer.Interval = 500; // 500
Millisekunden Pause
timer.Tick += (s, args) =>
{
timer.Stop();
KIMove();
lblStatus.Text = "Spieler ist am Zug";
};
timer.Start();
}
private void CheckIfShipSunk(List<Ship> ships, Button clickedButton)
{
foreach (var ship in ships)
{
if (ship.Position.Contains(clickedButton))
{
// Überprüfen, ob alle Teile des Schiffes
getroffen wurden
bool isSunk = ship.Position.All(btn => btn.BackColor
== Color.LightSkyBlue);
if (isSunk)
{
ship.IsSunk = true;
// Bestimmen, ob es sich um ein eigenes oder ein
feindliches Schiff handelt
string shipOwner = (ships == playerShips) ?
"Dein eigenes Schiff" : "Ein feindliches Schiff";
// Zeige eine Benachrichtigung an, dass das Schiff
versenkt wurde
MessageBox.Show($"{shipOwner}, das {ship.Name}, wurde
versenkt!", "Schiff versenkt", MessageBoxButtons.OK,
MessageBoxIcon.Information);
// Wenn es sich um
ein feindliches Schiff handelt, den Anfangsbuchstaben des Schiffes auf allen
betroffenen Buttons anzeigen
if (ships == enemyShips)
{
foreach
(var btn in ship.Position)
{
btn.Text = ship.Name[0].ToString();
// Anfangsbuchstabe des Schiffes anzeigen
}
}
// Überprüfe, ob das Spiel zu Ende ist
CheckGameEnd(ships);
}
break;
}
}
}
private void
CheckGameEnd(List<Ship> ships)
{
if (ships.All(ship => ship.IsSunk))
{
if (ships == playerShips)
{
MessageBox.Show("Alle Ihre Schiffe wurden
versenkt. Sie haben verloren!", "Spiel beendet", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
else
{
MessageBox.Show("Alle
feindlichen Schiffe wurden versenkt. Sie haben gewonnen!", "Spiel beendet",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
// Das Spiel zurücksetzen oder beenden
ResetGame();
}
}
private void ResetGame()
{
// Logik zum Zurücksetzen des Spiels
Application.Restart(); // Einfaches Beispiel: Spiel
neu starten
}
private void btnStart_Click(object sender,
EventArgs e)
{
// Logik zum Start des Spiels
PlaceEnemyShips();
lblStatus.Text = "Spieler ist am Zug!";
}
private void PlaceEnemyShips()
{
Random rand = new Random();
foreach (var ship in enemyShips)
{
bool placed = false;
while
(!placed)
{
// Wähle eine zufällige
Startposition
int row = rand.Next(0, 10);
int col = rand.Next(0,
10);
// Wähle zufällig eine Ausrichtung
ship.IsVertical = rand.Next(0, 2) == 0;
//
Überprüfe, ob das Schiff an dieser Position platziert werden kann
if
(CanPlaceEnemyShip(row, col, ship))
{
PlaceEnemyShip(row, col, ship);
placed = true;
}
}
}
MessageBox.Show("KI-Schiffe wurden
erfolgreich platziert.", "KI-Platzierung", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
private bool CanPlaceEnemyShip(int row,
int col, Ship ship)
{
if (ship.IsVertical)
{
// Überprüfe, ob das Schiff innerhalb des Spielfelds
platziert werden kann
if (row + ship.Size > 10) return false;
// Überprüfe, ob sich das Schiff mit einem anderen
überschneidet
for (int i = 0; i < ship.Size; i++)
{
if
(enemyButtons[row + i, col].Tag != null && enemyButtons[row + i,
col].Tag.ToString() == "Ship")
{
return false;
// Das Feld ist bereits von einem anderen Schiff belegt
}
}
}
else
{
// Überprüfe, ob das Schiff innerhalb des
Spielfelds platziert werden kann
if (col + ship.Size > 10) return
false;
// Überprüfe, ob sich das Schiff mit
einem anderen überschneidet
for (int i = 0; i < ship.Size; i++)
{
if (enemyButtons[row, col + i].Tag != null && enemyButtons[row, col +
i].Tag.ToString() == "Ship")
{
return false; //
Das Feld ist bereits von einem anderen Schiff belegt
}
}
}
return true;
}
private void PlaceEnemyShip(int row, int col, Ship
ship)
{
if (ship.IsVertical)
{
for (int i = 0; i < ship.Size; i++)
{
enemyButtons[row + i, col].BackColor = Color.White;
// Markiere die Buttons zur Kontrolle (Debug)
enemyButtons[row + i, col].Tag = "Ship"; // Markiere
das Feld als Teil eines Schiffs
ship.Position.Add(enemyButtons[row +
i, col]);
}
}
else
{
for (int i = 0; i < ship.Size; i++)
{
enemyButtons[row, col + i].BackColor = Color.White; //
Markiere die Buttons zur Kontrolle (Debug)
enemyButtons[row, col +
i].Tag = "Ship"; // Markiere das Feld als Teil eines
Schiffs
ship.Position.Add(enemyButtons[row, col + i]);
}
}
}
}
}
Hier folgt die Beschreibung für den obigen Code. Das Spiel "Schiffe versenken" in C# mit Windows Forms ermöglicht es zwei Parteien (Spieler und KI), Schiffe auf einem Raster zu platzieren und abwechselnd auf die gegnerischen Schiffe zu schießen, bis alle Schiffe einer Partei versenkt sind.
Ship
-KlasseDie Ship
-Klasse
repräsentiert ein Schiff im Spiel. Jedes Schiff hat folgende Eigenschaften:
Name
:
Der Name des Schiffes (z.B. "Battleship").Size
:
Die Größe des Schiffes, d.h., wie viele Felder es auf dem Spielfeld belegt.Position
:
Eine Liste von Button
-Objekten,
die die Felder repräsentieren, auf denen das Schiff platziert ist.IsVertical
:
Ein boolescher Wert, der angibt, ob das Schiff vertikal (true) oder
horizontal (false) ausgerichtet ist.IsSunk
:
Ein boolescher Wert, der angibt, ob das Schiff vollständig versenkt wurde.Die Ship
-Klasse
hat einen Konstruktor, der den Namen und die Größe des Schiffes initialisiert.
Form1
-KlasseDie Form1
-Klasse
ist die Hauptbenutzeroberfläche des Spiels und enthält die gesamte Logik zur
Steuerung des Spiels. Diese Klasse enthält mehrere wichtige Bestandteile:
playerButtons
und enemyButtons
):
2D-Arrays von Buttons, die die Spielfelder des Spielers und der KI
darstellen.playerShips
und enemyShips
):
Listen von Ship
-Objekten,
die die Schiffe des Spielers und der KI enthalten.currentShip
:
Das Schiff, das gerade vom Spieler platziert wird.shipIndex
:
Ein Index, der den Fortschritt beim Platzieren der Schiffe verfolgt.InitializeGame
)Die Methode
InitializeGame
initialisiert die
Spielfelder und verknüpft die Buttons mit den entsprechenden Ereignis-Handlern:
playerButtons
):
Jeder Button im Spielerfeld wird mit einem
Click
-Eventhandler
(PlayerButton_Click
)
verknüpft, um die Schiffsplatzierung zu steuern.enemyButtons
):
Jeder Button im KI-Feld wird ebenfalls mit einem
Click
-Eventhandler
(EnemyButton_Click
)
verknüpft, um die Schüsse des Spielers auf die KI-Schiffe zu steuern.PlayerButton_Click
,
PlaceShip
,
CanPlaceShip
)Der Spieler platziert seine Schiffe durch Klicken
auf die Buttons im eigenen Spielfeld. Die Methode
PlayerButton_Click
steuert diesen Prozess:
CanPlaceShip
):
Vor der Platzierung eines Schiffes wird überprüft, ob das Schiff in das
Spielfeld passt und sich nicht mit einem anderen Schiff überschneidet.PlaceShip
):
Wenn die Platzierung gültig ist, wird das Schiff auf dem Spielfeld
platziert, die entsprechenden Buttons werden markiert, und der nächste
Schiffsplatzierungszyklus beginnt.EnemyButton_Click
,
KIMove
)Nachdem alle Schiffe platziert wurden, beginnt das eigentliche Spiel:
EnemyButton_Click
):
Der Spieler schießt auf die KI, indem er auf ein Feld im gegnerischen
Spielfeld klickt. Trifft der Schuss ein Schiff, wird das Feld hellblau
eingefärbt; ein Fehlschuss wird orange markiert.KIMove
):
Nach jedem Spielerschuss schießt die KI zufällig auf das Spielerfeld. Die KI
vermeidet Felder, die bereits beschossen wurden.CheckIfShipSunk
)Nach jedem Treffer wird überprüft, ob das Schiff vollständig versenkt ist:
CheckIfShipSunk
:
Diese Methode überprüft, ob alle Felder des getroffenen Schiffes getroffen
wurden. Wenn ja, wird eine Meldung angezeigt, ob ein eigenes oder ein
gegnerisches Schiff versenkt wurde.CheckGameEnd
)Nach jedem Schuss wird überprüft, ob alle Schiffe einer Partei versenkt wurden:
CheckGameEnd
:
Diese Methode überprüft, ob alle Schiffe des Spielers oder der KI versenkt
sind. Wenn ja, wird das Spiel beendet und eine entsprechende Nachricht
angezeigt.ResetGame
)Nach dem Ende des Spiels kann das Spiel durch einen Neustart der Anwendung zurückgesetzt werden.
Dieses Beispiel zeigt, wie Schiffe auf einem Raster
platziert werden, wie die Schusslogik implementiert wird und wie das Spiel seine
Zustände verwaltet.
Dies kann als Grundlage für Erweiterungen und
Verbesserungen dienen, z.B. eine raffiniertere KI oder erweiterte
Spielregeln.
Hier wird der Ablauf des Spiels gezeigt:
battleship.mp4
Wir verwenden genau den gleichen Aufbau mit zwei 10 x 10
Feldern, eines für den Spieler (Player) und eines für die KI (enemy). Zusätzlich
haben wir ein Label für Textausgaben und einen Startbutton, der die KI-Schiffe
platziert und das eigentliche Spiel eröffnet. Daher kann ich mich hier darauf
beschränken den Code darzustellen und zu erklären.
Zunächst die Klasse "Ship", die sich in Ship.cs befindet:
Dieser Codeabschnitt importiert die erforderlichen Namespaces
System.Collections.Generic
und System.Drawing
, die Klassen
und Datenstrukturen wie List
und Point
bereitstellen.
Die Ship
-Klasse enthält die wesentlichen Eigenschaften und Methoden
für ein Schiff im Spiel "Schiffe versenken" (Battleship).
Eigenschaften:
Positions
: Eine Liste von Point
-Objekten,
die die Positionen des Schiffs auf dem Spielfeld darstellen.Hits
: Eine Liste von Point
-Objekten,
die die getroffenen Positionen des Schiffs speichert.Name
: Der Name oder Typ des Schiffs (z. B.
"Battleship", "Cruiser").Size
: Die Größe des Schiffs, basierend auf
der Anzahl der Felder, die es auf dem Spielfeld belegt.IsVertical
: Eine boolesche Eigenschaft,
die angibt, ob das Schiff vertikal oder horizontal ausgerichtet ist.Der erste Konstruktor initialisiert ein neues Ship
-Objekt
durch eine Liste von Point
-Positionen und einen Namen. Die Anzahl
der Positionen bestimmt die Größe des Schiffs. Standardmäßig wird das Schiff als
vertikal ausgerichtet festgelegt.
Der zweite Konstruktor initialisiert ein neues Ship
-Objekt
durch Angabe des Namens und der Größe (Anzahl der Felder, die das Schiff
belegt). Die Positionen werden zunächst leer gelassen und später festgelegt.
Auch hier wird die Ausrichtung standardmäßig auf vertikal gesetzt.
Die Contains-
Methode überprüft, ob ein
bestimmter Punkt (Point
) in den Positionen des Schiffs enthalten
ist. Sie gibt true
zurück, wenn der Punkt Teil des Schiffs ist,
andernfalls false
.
Die RegisterHit-
Methode erfasst Treffer auf
das Schiff. Wenn der angegebene Punkt Teil des Schiffs ist und noch nicht zuvor
getroffen wurde, wird er der Hits
-Liste hinzugefügt.
Die IsSunk-
Methode prüft, ob das Schiff
vollständig versenkt wurde. Ein Schiff gilt als versenkt, wenn die Anzahl der
Treffer (in der Hits
-Liste) gleich der Anzahl der Positionen ist,
die das Schiff belegt.
using System.Collections.Generic;
using
System.Drawing;
namespace WindowsFormsApp_BattleShip
{
public class
Ship
{
public List<Point> Positions { get; private set; }
public
List<Point> Hits { get; private set; }
public string Name { get; private set;
} // Optionaler Name oder Typ des Schiffs
public int Size { get; private set; } // Größe des
Schiffs (Anzahl der Felder)
public bool IsVertical { get; set; }
// Ausrichtung des Schiffs
public
Ship(List<Point> positions, string name)
{
Positions = positions;
Hits = new List<Point>();
Name = name;
Size =
positions.Count; // Die Größe wird anhand der Anzahl
der Positionen bestimmt
IsVertical = true;
// Standardmäßig vertikal
}
public
Ship(string name, int size)
{
Positions = new List<Point>();
Hits = new List<Point>();
Name = name;
Size = size;
IsVertical = true; // Standardmäßig vertikal
}
public bool Contains(Point point)
{
return
Positions.Contains(point);
}
public void RegisterHit(Point point)
{
if (Contains(point) && !Hits.Contains(point))
{
Hits.Add(point); // Treffer erfassen
}
}
public bool IsSunk()
{
return Hits.Count ==
Positions.Count; // Alle Felder sind getroffen
}
}
}
Der gesamte Ablauf wird via Form1.cs gesteuert. Hier ist
der Code, der bereits einen recht starken KI-Gegner und Debugging für die
weitere Entwicklung umfasst:
Dieser Abschnitt importiert die erforderlichen Namespaces, die verschiedene Klassen und Methoden bereitstellen:
System
:
Grundlegende Funktionen und Datentypen.System.Collections.Generic
:
Generische Sammlungen wie List
.System.Drawing
:
Klassen für die Handhabung von Grafiken, Farben und Positionen (Point
).System.Linq
:
Erweiterungsmethoden für die Arbeit mit Sammlungen und LINQ-Abfragen.System.Windows.Forms
:
Klassen für die Erstellung von Windows Forms-Anwendungen.Die Klasse
Form1
wird als
partial class
deklariert, da sie in mehreren Dateien aufgeteilt sein könnte. Diese Klasse
repräsentiert das Hauptformular der Anwendung.
rand
: Ein Random
-Objekt zur
Generierung von Zufallszahlen, das beispielsweise für die Platzierung von
KI-Schiffen verwendet wird.playerButtons
und enemyButtons
:
Zwei 2D-Arrays von Button
, die die Spielfelder für den Spieler und
die KI darstellen. Jedes Button
-Element repräsentiert ein Feld auf
dem 10x10-Schlachtfeld.playerShips
und enemyShips
: Zwei
Listen von Ship
-Objekten, die die Schiffe des Spielers und der KI
enthalten.currentShip
: Ein Verweis auf das derzeit zu
platzierende Schiff.shipIndex
: Ein Index, der das aktuelle Schiff
in der Liste der Schiffe verfolgt, das platziert wird.gameStarted
: Ein boolesches Flag, das angibt,
ob das Spiel gestartet wurde.playerCanShoot
und KIcanShoot
:
Diese Flags steuern, ob der Spieler oder die KI schießen kann. KIcanShoot
sorgt dafür, dass die KI nur einmal pro Runde schießt.lastHit
und lastHitOld
: Diese
Point?
-Variablen speichern die Position des letzten Treffers und
des vorherigen Treffers, um die Schussstrategie der KI zu verbessern.currentDirection
: Ein Direction?
-Typ,
der die Richtung speichert, in die die KI weiterschießen soll, nachdem sie einen
Treffer erzielt hat.checkerboardTargets
und clusterTargets
:
Listen von Point
, die Zielkoordinaten für die KI enthalten.
checkerboardTargets
wird für ein Schachbrettmuster verwendet, um
strategisch Schüsse zu verteilen, während clusterTargets
eng
beieinanderliegende Ziele enthält, wenn ein Schiff getroffen wurde.Der Konstruktor der Form1
-Klasse wird aufgerufen, wenn das
"Formular" erstellt wird:
InitializeComponent()
: Eine Methode, die
die Komponenten des Formulars initialisiert. Sie wird automatisch vom
Designer generiert.KeyPreview
: Diese Eigenschaft wird auf
true
gesetzt, damit das Formular Tastatureingaben verarbeitet,
bevor sie an die Steuerelemente weitergeleitet werden.KeyDown
: Ein Ereignis-Handler, der
aufgerufen wird, wenn eine Taste gedrückt wird. Die Methode
Form1_KeyDown
wird diesem Ereignis zugewiesen.InitializeGame()
: Diese Methode
initialisiert das Spiel, indem sie das Schlachtfeld und andere
Spielfunktionen einrichtet.
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace WindowsFormsApp_BattleShip
{
public partial class Form1 :
Form
{
private readonly Random rand = new Random();
private
Button[,] playerButtons = new Button[10, 10];
private Button[,] enemyButtons
= new Button[10, 10];
private List<Ship> playerShips = new List<Ship>();
private List<Ship> enemyShips = new List<Ship>();
private Ship currentShip =
null; // Das Schiff, das gerade platziert wird
private int shipIndex = 0; // Index des aktuellen
Schiffs in der Liste
private bool gameStarted = false;
private
bool playerCanShoot = true;
private bool KIcanShoot = false;
// Kontrollvariable, um sicherzustellen, dass die KI
nur einmal schießt
private Point? lastHit = null;
// Position des letzten Treffers
private Point? lastHitOld = null; // Variable, um den
vorherigen Treffer zu speichern
private Direction? currentDirection =
null; // Richtung, in die weiter geschossen wird
private readonly List<Point> checkerboardTargets = new List<Point>();
private
List<Point> clusterTargets = new List<Point>();
private int playerShots =
0;
private int kiShots = 0;
private enum Direction { Up, Down, Left,
Right }
public Form1()
{
InitializeComponent();
this.KeyPreview
= true; // Ermöglicht es dem Formular,
KeyDown-Ereignisse zu erfassen
this.KeyDown += new
KeyEventHandler(Form1_KeyDown);
InitializeGame();
// Initialisiere das Spiel hier
}
Die Methode
Form1_KeyDown behandelt das
KeyDown
-Ereignis
des Formulars. Es wird aufgerufen, wenn eine Taste gedrückt wird, während das
Formular den Fokus hat.
Die Methode überprüft konkret, ob die Leertaste (Space
)
gedrückt wurde, und bei Auswahl eines Schiffs (currentShip
)
wechselt sie beim Platzieren (linke Maustaste gedrückt halten) die Ausrichtung
des Schiffs zwischen vertikal und horizontal.
if (e.KeyCode == Keys.Space &&
currentShip != null)
:
currentShip.IsVertical =
!currentShip.IsVertical;
:
currentShip
).
IsVertical
ist true
),
wird es horizontal ausgerichtet (IsVertical
wird false
)
und umgekehrt.lblStatus.Text = $"Platzieren Sie
das {currentShip.Name} ({(currentShip.IsVertical ? "Vertikal" :
"Horizontal")})";
:
lblStatus
),
um die neue Ausrichtung des Schiffs anzuzeigen.
private void
Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space
&& currentShip != null)
{
// Ändere die
Ausrichtung des aktuellen Schiffs:
// Für horizontale Ausrichtung die Maus
auf dem ersten Feld gedrückt halten und gleichzeitig die Space-Taste nur einmal
kurz betätigen
currentShip.IsVertical = !currentShip.IsVertical;
// Zeige die aktuelle Ausrichtung an
lblStatus.Text = $"Platzieren Sie das {currentShip.Name}
({(currentShip.IsVertical ? "Vertikal" : "Horizontal")})";
}
}
Die Methode
InitializeGame()
wird verwendet, um das
Spiel zu initialisieren und die anfänglichen Einstellungen vorzunehmen, bevor
das Spiel beginnt.
Spielzustand zurücksetzen:
gameStarted
wird auf false
gesetzt. Checkerboard-Strategie festlegen:
InitializeCheckerboardTargets()
implementiert.Erstellung der Schiffsliste:
shipNames
),
die die verschiedenen Schiffsarten im Spiel repräsentieren (z.B.
Submarine
,
Destroyer
).playerShips
)
als auch die der KI (enemyShips
)
werden auf Basis dieser Namen initialisiert. Die Schiffe werden jedoch
zunächst ohne Positionen erstellt.Initialisierung der Spielfelder (Buttons):
playerButtons
)
wird durch eine Schleife erstellt und findet ihren Platz auf dem
Spielfeld.
PlayerButton_Click
registriert, um Klicks des Spielers zu verarbeiten.enemyButtons
)
erstellt und mit dem Ereignis
EnemyButton_Click
verknüpft. Statusanzeige initialisieren:
lblStatus
)
aktualisiert, um den Spieler darüber zu informieren, dass er das erste
Schiff (das Submarine
)
platzieren soll.
private void InitializeGame()
{
gameStarted = false;
// Checkerboard Strategie festlegen
InitializeCheckerboardTargets();
// Zentrale
Liste der Schiffsnamen
var shipNames = new List<string> { "Submarine",
"Destroyer", "Cruiser", "Battleship", "Carrier" };
// Initialisierung der playerShips-Liste
playerShips = shipNames.Select(name => new Ship([], name)).ToList();
// Initialisierung der enemyShips-Liste
enemyShips = shipNames.Select(name => new Ship([], name)).ToList();
// Spielerbuttons initialisieren
for (int i
= 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
string buttonName = "btnPlayer" + i.ToString("D1") + j.ToString("D1");
playerButtons[i, j] = this.Controls.Find(buttonName, true).FirstOrDefault() as
Button;
playerButtons[i, j].Click += PlayerButton_Click;
}
}
// KI-Buttons initialisieren
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
string buttonName = "btnEnemy" + i.ToString("D1") +
j.ToString("D1");
enemyButtons[i, j] =
this.Controls.Find(buttonName, true).FirstOrDefault() as Button;
enemyButtons[i, j].Click += EnemyButton_Click;
}
}
// Statusanzeige initialisieren
lblStatus.Text = "Platzieren Sie Ihr Submarine!"; //
Dies fordert den Spieler zum Platzieren des ersten Schiffs auf
}
Die Methode
PlayerButton_Click()
wird aufgerufen,
wenn der Spieler auf einen Button auf seinem Spielfeld klickt.
Sie verwaltet
die Logik sowohl für die Platzierungsphase der Schiffe als auch für die
Schussphase des Spiels.
Sie sorgt dafür, dass die Spieler- und KI-Züge
korrekt abwechseln und das Spiel ordnungsgemäß abläuft.
Überprüfung der Platzierungsphase:
gameStarted
== false
), befindet sich das
Spiel in der Phase, in der der Spieler seine Schiffe platziert.currentShip
)
und ob es korrekt platziert werden kann. Wenn das Schiff korrekt
platziert wird, wird es auf dem Spielfeld verankert, und der Spieler
kann mit dem nächsten Schiff fortfahren.Schussphase:
gameStarted
== true
), d.h. die KI-Schiffe
wurden erstellt, befindet sich das Spiel in der Schussphase.playerCanShoot ==
true
), um sicherzustellen, dass
die Züge der KI und des Spielers korrekt abwechseln.
LightSkyBlue
für Treffer oder Orange
für Fehlschuss), erhält er eine Warnung.LightSkyBlue
),
und es wird überprüft, ob das getroffene Schiff versenkt wurde. Wenn
es kein Treffer ist, wird das Feld orange gefärbt (Orange
).playerCanShoot
= false
), und die KI wird
durch einen kurzen Timer aktiviert, um den Spielfluss realistischer
zu gestalten.
private void PlayerButton_Click(object sender, EventArgs e)
{
Button
clickedButton = sender as Button;
// Wenn das
Spiel noch nicht gestartet ist (Start-Button), befinden wir uns in der
Platzierungsphase
if (!gameStarted)
{
if (currentShip ==
null && shipIndex < playerShips.Count)
{
currentShip = playerShips[shipIndex];
}
if (currentShip
!= null)
{
if (CanPlaceShip(clickedButton))
{
PlaceShip(clickedButton);
shipIndex++;
if (shipIndex <
playerShips.Count)
{
currentShip = playerShips[shipIndex];
lblStatus.Text = $"Platzieren Sie das {currentShip.Name}";
}
else
{
currentShip = null;
lblStatus.Text
= "Alle Schiffe platziert. Starten Sie das Spiel!";
}
}
else
{
MessageBox.Show("Ungültige Platzierung. Das Schiff überschneidet sich oder ist
außerhalb des Spielfelds.");
}
}
return; // Ende der Platzierungsphase
}
// Wenn das Spiel gestartet ist, befinden wir uns
in der Schussphase
//
if (!playerCanShoot)
// Wichtig, damit der Spieler nicht zweimal schießt
{
MessageBox.Show("Bitte warten Sie, bis die KI ihren Zug beendet
hat!", "Warnung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// Spieler schießt
if
(clickedButton.BackColor == Color.LightSkyBlue || clickedButton.BackColor ==
Color.Orange)
{
MessageBox.Show("Sie haben hier bereits
geschossen.", "Achtung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (clickedButton.Tag != null &&
clickedButton.Tag.ToString() == "Ship")
{
clickedButton.BackColor =
Color.LightSkyBlue; // Treffer
CheckIfShipSunk(enemyShips, clickedButton);
}
else
{
clickedButton.BackColor = Color.Orange; // Fehlschuss
}
playerShots++;
Console.WriteLine($"Spieler hat
{playerShots} mal geschossen.");
// Der Spieler
hat geschossen, jetzt ist die KI dran
playerCanShoot = false;
lblStatus.Text = "KI ist am Zug";
// Kurze
Pause vor dem Zug der KI
Timer timer = new Timer();
timer.Interval
= 500; // 500 ms Pause
timer.Tick += (s,
args) =>
{
timer.Stop();
timer.Dispose();
// Timer entsorgen, wenn er nicht mehr benötigt wird
KIMove();
};
timer.Start();
}
Die Methode CanPlaceShip
sorgt dafür, dass
ein Schiff nur dann auf das Spielfeld gesetzt wird, wenn es innerhalb der
Spielfeldgrenzen liegt und keine Kollisionen mit bereits platzierten Schiffen
auftreten.
Diese Validierung verhindert fehlerhafte Platzierungen und stellt
sicher, dass das Spielfeld korrekt genutzt wird.
Bestimmung der Startposition:
startButton
)
im 10x10 Spielfeld (playerButtons
)
zu ermitteln.
i
(Reihe) und j
(Spalte) bestimmt.row
und col
mit den entsprechenden Koordinaten des Buttons belegt.Abbruch bei ungültiger Position:
row
und col
nicht gesetzt werden können (d.h., der Button wurde nicht gefunden),
gibt die Methode false
zurück. Dies bedeutet, dass das Schiff nicht platziert werden kann.Bestimmung der Schiffgröße:
shipSize
)
wird anhand seines Namens festgelegt. Je nach Schiffstyp (Submarine
,
Destroyer
,
etc.) wird eine entsprechende Anzahl an Feldern (shipSize
)
zugewiesen.Überprüfung der Platzierung:
currentShip.IsVertical
),
wird überprüft, ob es nach unten hin über das Spielfeld hinausragt.
Dazu wird row + shipSize > 10
geprüft.playerButtons[row
+ i, col].Tag == null
).
Falls eines dieser Felder bereits belegt ist, gibt die Methode
false
zurück.col +
shipSize > 10
).Rückgabe des Ergebnisses:
true
zurück, was bedeutet, dass das Schiff an der gewünschten Position
platziert werden kann.
false
zurück, was bedeutet, dass die Platzierung nicht möglich ist.
private bool CanPlaceShip(Button
startButton)
{
int row = -1, col = -1;
//
Finde die Position des Buttons im Spielfeld
for (int i = 0; i < 10;
i++)
{
for (int j = 0; j < 10; j++)
{
if (playerButtons[i, j] == startButton)
{
row = i;
col = j;
break;
}
}
if (row != -1) break;
}
if (row == -1 || col == -1) return false;
int shipSize =
currentShip.Name switch
{
"Submarine" => 1,
"Destroyer" => 2,
"Cruiser" => 3,
"Battleship" => 4,
"Carrier" => 5,
_ => 1
};
// Überprüfe, ob das Schiff innerhalb des Spielfelds
bleibt
if (currentShip.IsVertical)
{
if (row + shipSize >
10) return false; // Überprüfe, ob das Schiff unten
aus dem Feld ragt
for (int i = 0; i < shipSize; i++)
{
if (playerButtons[row + i, col].Tag != null) return
false; // Überprüfe, ob das Feld belegt ist
}
}
else
{
if (col + shipSize > 10) return false;
// Überprüfe, ob das Schiff rechts aus dem Feld ragt
for (int i = 0; i < shipSize; i++)
{
if
(playerButtons[row, col + i].Tag != null) return false;
// Überprüfe, ob das Feld belegt ist
}
}
return true;
}
Die Methode PlaceShip
sorgt dafür, dass das
aktuelle Schiff korrekt auf dem Spielfeld platziert wird, indem es die
relevanten Felder einfärbt und als belegt markiert.
Gleichzeitig werden die
genauen Positionen des Schiffs gespeichert, um spätere Aktionen wie
Treffererkennung zu ermöglichen.
Bestimmung der Startposition:
startButton
)
im 10x10 Spielfeld (playerButtons
)
zu ermitteln. Dies geschieht durch Iteration über die Reihen (i
)
und Spalten (j
)
des Spielfelds.row
(Reihe) und col
(Spalte) mit den entsprechenden Werten belegt. Diese Position stellt den
Startpunkt für die Platzierung des Schiffs dar.Abbruch bei ungültiger Position:
row
und col
auf ihrem Initialwert von -1
.
In diesem Fall beendet die Methode die Ausführung ohne weitere Aktionen (return
).Bestimmung der Schiffgröße:
shipSize
)
wird anhand des Namens des Schiffs bestimmt. Dies erfolgt durch einen
switch
-Ausdruck,
der für jedes Schiff (z.B. Submarine
,
Destroyer
,
etc.) eine entsprechende Größe in Feldern zuweist.Platzierung des Schiffs:
currentShip.IsVertical
),
wird das Schiff von der Startposition aus nach unten auf dem Spielfeld
platziert.
LightGray
gesetzt, um die Platzierung zu visualisieren.Tag
-Eigenschaft
des Buttons auf "Ship"
als belegt markiert.Positions
-Liste
des Schiffs hinzugefügt, um die gesamte Position des Schiffs zu
speichern.Sicherheitsüberprüfungen:
row
+ i < 10
bzw.
col + i < 10
),
um Indexfehler zu vermeiden.Debugging und Bestätigung:
private void PlaceShip(Button
startButton)
{
int row = -1, col = -1;
for (int i = 0; i
< 10; i++)
{
for (int j = 0; j < 10; j++)
{
if (playerButtons[i, j] == startButton)
{
row = i;
col = j;
break;
}
}
if (row != -1) break;
}
if (row == -1 || col == -1) return;
int shipSize =
currentShip.Name switch
{
"Submarine" => 1,
"Destroyer" => 2,
"Cruiser" => 3,
"Battleship" => 4,
"Carrier" => 5,
_ => 1
};
if
(currentShip.IsVertical)
{
for (int i = 0; i <
shipSize; i++)
{
if (row
+ i < 10) // Zusätzliche Sicherheitsprüfung, um
sicherzustellen, dass der Index innerhalb der Grenzen liegt
{
playerButtons[row + i,
col].BackColor = Color.LightGray;
playerButtons[row + i, col].Tag = "Ship";
playerButtons[row + i, col].Text = currentShip.Name[0].ToString();
currentShip.Positions.Add(new Point(row + i, col));
}
}
}
else
{
for (int i = 0; i < shipSize; i++)
{
if (col + i < 10) // Zusätzliche Sicherheitsprüfung,
um sicherzustellen, dass der Index innerhalb der Grenzen liegt
{
playerButtons[row, col +
i].BackColor = Color.LightGray;
playerButtons[row, col + i].Tag = "Ship";
playerButtons[row, col + i].Text = currentShip.Name[0].ToString();
currentShip.Positions.Add(new Point(row, col + i));
}
}
}
Console.WriteLine($"{currentShip.Name} platziert! Positionen: {string.Join(", ",
currentShip.Positions)}");
}
Die Methode InitializeCheckerboardTargets()
erstellt eine Liste von Feldern, die die KI in einem Schachbrettmuster angreifen
soll. Damit werden alle Schiffe außer dem Submarine gefunden.
Die Methode
dient als Start-Strategie bis zum ersten Schiffstreffer.
Um die Schüsse der KI unvorhersehbarer zu gestalten, wählt die Methode zufällig eines von vier möglichen Schachbrettmustern:
Musterwahl:
Checkerboard-Logik:
Zielsetzung:
checkerboardTargets
-Liste
hinzugefügt. Diese Liste steuert später, in welcher Reihenfolge die KI
Felder angreift, um effizient nach Schiffen zu suchen.
private void
InitializeCheckerboardTargets() // Suchstrategie
{
checkerboardTargets.Clear();
int patternType =
rand.Next(0, 4); // Zufällige Wahl zwischen den vier
Mustern
int startOffset = rand.Next(0, 2);
// Zufällige Wahl zwischen 0 oder 1 als Startpunkt
switch (patternType)
{
case 0:
// Von oben nach unten, Start: 0,0 oder 0,1
for (int i = 0; i < 10; i++)
{
for (int j = 0; j
< 10; j++)
{
if ((i + j
+ startOffset) % 2 == 0)
{
checkerboardTargets.Add(new Point(i, j));
}
}
}
break;
case 1:
// Von unten nach oben, Start: 0,0 oder 0,1
for (int i = 9; i
>= 0; i--)
{
for (int j = 0; j < 10; j++)
{
if ((i + j + startOffset) % 2 == 0)
{
checkerboardTargets.Add(new
Point(i, j));
}
}
}
break;
case 2: // Von links
nach rechts, Start: 0,0 oder 1,0
for (int j = 0; j < 10; j++)
{
for (int i = 0; i < 10; i++)
{
if ((i + j + startOffset) % 2 == 0)
{
checkerboardTargets.Add(new Point(i, j));
}
}
}
break;
case 3:
// Von rechts nach links, Start: 0,0 oder 1,0
for (int j = 9; j
>= 0; j--)
{
for (int i = 0; i < 10; i++)
{
if ((i + j + startOffset) % 2 == 0)
{
checkerboardTargets.Add(new
Point(i, j));
}
}
}
break;
}
}
Die Methode
AddClusterTargets
spielt eine
entscheidende Rolle in der Cluster-Such-Strategie der KI.
Sie wird
aufgerufen, sobald die KI einen Treffer auf ein Schiff erzielt hat, um mögliche
weitere Trefferzonen (Cluster) um den getroffenen Punkt herum zu identifizieren
und in einer Liste zu speichern.
Diese Punkte werden dann für zukünftige
Angriffe priorisiert.
Es ist eine wichtige Methode, die die Effizienz der
KI-Angriffe deutlich erhöht, indem sie intelligent Cluster-Ziele rund um
Trefferpunkte erstellt und unnötige Ziele entfernt, sobald die Richtung eines
Schiffs identifiziert wurde.
Dies macht die KI präziser und schwieriger zu
besiegen.
Debug-Ausgaben: Zu Beginn der
Methode werden der aktuelle Trefferpunkt (hitPoint
)
und der vorherige Trefferpunkt (lastHitOld
)
ausgegeben. Dies hilft, zwischen einem ersten Treffer und einem Folgetreffer
auf dasselbe Schiff zu unterscheiden.
Bestimmung angrenzender Punkte:
Die Methode ruft GetAdjacentPoints
auf, um alle benachbarten Felder des aktuellen Treffers zu ermitteln. Diese
Felder werden als potenzielle Ziele für weitere Treffer betrachtet.
clusterTargets
-Liste
enthalten ist. Falls diese Bedingungen erfüllt sind, wird der Punkt zur
Liste der Cluster-Ziele hinzugefügt und visuell durch eine hellrosa Farbe
hervorgehoben.Unterscheidung zwischen Erst- und Folgetreffer: Falls dies der erste Treffer auf das Schiff ist oder derselbe Punkt erneut getroffen wurde, wird die Methode an dieser Stelle beendet. Es wird nur der Treffer gespeichert, ohne die Liste der Cluster-Ziele zu verändern.
Bestimmung der Ausrichtung des Schiffs:
Wenn jedoch bereits ein vorheriger Treffer (lastHitOld
)
vorliegt, bestimmt die Methode, ob das Schiff horizontal oder vertikal
verläuft, indem sie die Positionen von
lastHitOld
und hitPoint
vergleicht.
Entfernen von Punkten außerhalb der
Schiffsrichtung: Basierend auf der ermittelten Ausrichtung
(horizontal oder vertikal) entfernt die Methode alle Punkte aus der
clusterTargets
-Liste,
die nicht in der Richtung des Schiffs liegen. Diese Punkte werden auch
visuell zurückgesetzt, um den Spieler nicht zu verwirren.
Rücksetzen der Farben: Nach dem Entfernen der Punkte aus der Liste werden diese Punkte wieder auf ihre ursprüngliche Farbe (z. B. weiß oder hellgrau) gesetzt.
lastHitOld
nicht direkt. Diese Variable wird in der Methode
FireAtPlayerPosition
aktualisiert, um sicherzustellen, dass immer der vorherige Treffer korrekt
gespeichert wird, bevor lastHit
auf den aktuellen Treffer gesetzt wird.
private void AddClusterTargets(Point
hitPoint) // Wichtige Cluster-Such-Strategie
{
// Debug-Ausgabe von hitPoint und
lastHitOld (wichtig für Unterscheidung: Erst- oder Folgetreffer am Schiff)
Console.WriteLine($"AddClusterTargets aufgerufen mit hitPoint: ({hitPoint.X},
{hitPoint.Y})");
if (lastHitOld.HasValue)
{
Console.WriteLine($"lastHitOld ist gesetzt auf: ({lastHitOld.Value.X},
{lastHitOld.Value.Y})");
}
else
{
Console.WriteLine("lastHitOld ist noch nicht gesetzt.");
}
// Bestimme die aktuellen Cluster-Ziele
basierend auf angrenzenden Punkten
List<Point> adjacentPoints =
GetAdjacentPoints(hitPoint).ToList();
foreach (var point in
adjacentPoints)
{
if (IsValidPoint(point) &&
!HasAlreadyBeenShot(point) && !clusterTargets.Contains(point))
{
clusterTargets.Add(point);
playerButtons[point.X, point.Y].BackColor = Color.LightPink;
Console.WriteLine($"Hinzugefügt zu clusterTargets: ({point.X}, {point.Y})");
}
}
// Falls dies der erste
Treffer auf dieses Schiff ist, tun wir nichts weiter. Wir brauchen einen zweiten
Treffer zur Bestimmung der Lage
if (!lastHitOld.HasValue ||
lastHitOld.Value == hitPoint)
{
Console.WriteLine($"Erster Treffer oder Treffer auf denselben Punkt. lastHitOld
gesetzt auf: ({lastHit.Value.X}, {lastHit.Value.Y})");
return; // Beende die Methode hier, da es der erste
Treffer ist
}
// Wenn lastHitOld schon gesetzt ist, dann haben wir
den zweiten Treffer
bool isHorizontal =
lastHitOld.Value.X == hitPoint.X; // X ist die Reihe
von oben ab 0 gezählt
bool isVertical = lastHitOld.Value.Y ==
hitPoint.Y; // Y ist die Spalte von links ab 0 gezählt
List<Point> removedPoints = new List<Point>();
// Entferne die Zielpunkte, die nicht in der richtigen
Richtung des Schiffes liegen
if
(isHorizontal)
{
removedPoints =
clusterTargets.Where(p => p.X != hitPoint.X).ToList();
clusterTargets.RemoveAll(p => p.X != hitPoint.X);
Console.WriteLine($"Entfernt von clusterTargets (Schiff liegt horizontal):
{string.Join(", ", removedPoints.Select(p => $"({p.X}, {p.Y})"))}");
}
else if (isVertical)
{
removedPoints =
clusterTargets.Where(p => p.Y != hitPoint.Y).ToList();
clusterTargets.RemoveAll(p => p.Y != hitPoint.Y);
Console.WriteLine($"Entfernt von clusterTargets (Schiff liegt vertikal):
{string.Join(", ", removedPoints.Select(p => $"({p.X}, {p.Y})"))}");
}
// Setze die Farben der entfernten Punkte
korrekt zurück
foreach (var point in removedPoints)
{
ResetClusterTargetColor(point);
}
// lastHitOld wird in FireAtPlayerPosition
aktualisiert, nicht hier.
}
Die Methode
DebugClusterTargets
dient der Ausgabe
der aktuellen Cluster-Ziele zu Debugging-Zwecken.
Sie gibt alle Punkte in
der Liste clusterTargets
in der Konsole aus, um den aktuellen Stand der von der KI anvisierten Felder zu
überprüfen.
Dies hilft, die Funktionsweise der Cluster-Such-Strategie
während der Entwicklung und Fehlersuche besser zu verstehen.
Durch die Ausgabe jedes Cluster-Ziels in der Form
(X, Y)
kann man nachvollziehen, welche Felder die KI als potenzielle Trefferzonen
ausgewählt hat.
private void
DebugClusterTargets()
{
Console.WriteLine("Aktuelle
clusterTargets:");
foreach (var point in clusterTargets)
{
Console.WriteLine($"Cluster-Ziel: ({point.X}, {point.Y})");
}
}
Die Methode
KIMove
ist der zentrale
Steuerungsmechanismus für die verschiedenen Strategien der KI in diesem
Battleship-Spiel.
Sie bestimmt, welche Strategie die KI in ihrem aktuellen
Zug anwenden soll, basierend auf der aktuellen Spielsituation und den
verfügbaren Informationen.
Hier folgt eine detaillierte Erklärung der
einzelnen Schritte und Entscheidungen:
KI-Zug beginnen:
KIcanShoot = true;
wird gesetzt, um sicherzustellen, dass die KI in diesem Zug schießen
kann.Überprüfung der verbleibenden großen Schiffe:
Bestimmung der Strategie:
clusterTargets
)
gibt, wird diese Strategie bevorzugt, um die Wahrscheinlichkeit zu
erhöhen, weitere Teile eines bereits getroffenen Schiffs zu treffen.lastHit
)
bekannt ist, versucht die KI, in derselben Richtung weiterzuschießen, um
das Schiff zu versenken.Auswahl und Ausführung der Strategie:
DetermineDirection
,
FireRandomShot
,
FireClusterShot
oder FireCheckerboardShot
aufgerufen. Diese Methoden enthalten die spezifische Logik, wie die KI
ihre Schüsse abgibt.KI-Zug beenden:
playerCanShoot = true;
gesetzt, um dem Spieler das Schießen zu ermöglichen.KIcanShoot = false;
signalisiert, dass die KI ihren Zug beendet hat, und der Status wird
entsprechend aktualisiert (lblStatus.Text
= "Spieler ist am Zug";
).Durch die zentrale Steuerung in
KIMove
kann
die KI je nach Spielsituation flexibel reagieren und zwischen systematischen und
zufälligen Schussstrategien wechseln, um den Spieler herauszufordern.
private void KIMove() // Zentrale Steuerung der
KI-Strategien
{
KIcanShoot = true;
// Setze KIcanShoot, um den Zug zu starten
// Überprüfen, wie viele große Schiffe
außer dem U-Boot noch übrig sind
int remainingLargeShips =
playerShips.Count(ship => !ship.IsSunk() && ship.Name != "Submarine");
// Bestimme die aktuelle Strategie basierend auf der
Spielsituation
string strategy;
// Wenn es Cluster-Ziele gibt, bevorzuge die
Cluster-Strategie
if (clusterTargets.Count
> 0)
{
strategy = "cluster";
}
else if (lastHit.HasValue)
{
strategy =
"continueDirection";
}
else if (remainingLargeShips == 0)
{
strategy = "random"; // Nur zufällige Schüsse, wenn nur
noch das U-Boot übrig ist
}
else if
(checkerboardTargets.Count > 0)
{
strategy =
"checkerboard";
}
else
{
strategy = "random"; // Zufällige Schüsse, wenn alle anderen Optionen erschöpft
sind
}
// Switch-case
basierend auf der gewählten Strategie
switch (strategy)
{
case "continueDirection":
Console.WriteLine(">>>>>>>>>>>>> KI STRATEGY: continueDirection");
DetermineDirection(lastHit.Value.X, lastHit.Value.Y);
break;
case "random":
Console.WriteLine(">>>>>>>>>>>>> KI STRATEGY: random");
FireRandomShot();
break;
case "cluster":
Console.WriteLine(">>>>>>>>>>>>> KI STRATEGY: cluster");
FireClusterShot();
break;
case "checkerboard":
Console.WriteLine(">>>>>>>>>>>>> KI STRATEGY: checkerboard");
FireCheckerboardShot();
break;
}
// Nach einem Schuss der KI, setze
playerCanShoot auf true, um den Spieler wieder dran zu lassen
playerCanShoot = true;
KIcanShoot = false;
// KI beendet ihren Zug, bis der Spieler wieder an der Reihe war
lblStatus.Text = "Spieler ist am Zug";
}
Die Methode
ResetClusterTargetColor
hat eine klare
Aufgabe: Sie setzt die Farbe eines Buttons im Spielfeld zurück, der als
potenzielles Ziel für die Cluster-Strategie markiert wurde. Dabei achtet sie
darauf, dass nur die Felder zurückgesetzt werden, die tatsächlich rosa
(LightPink) gefärbt sind, um versehentliches Zurücksetzen anderer Farben zu
vermeiden.
Button-Erkennung:
playerButtons
-Array
anhand der übergebenen Point
-Koordinaten
(target.X
,
target.Y
)
identifiziert.Prüfung der Farbe:
LightPink
hat. Diese Farbe signalisiert, dass der Punkt als potenzielles Ziel für
die Cluster-Strategie markiert wurde.Rücksetzen der Farbe:
button.Tag
!= null && button.Tag.ToString() == "Ship"
),
wird er auf LightGray
gesetzt. Diese Farbe repräsentiert ein unbeschädigtes Schiff.
White
gesetzt, um seine
Neutralität zu signalisieren.Vermeidung ungewollter Änderungen:
Orange
),
nicht fälschlicherweise zurückgesetzt werden. Diese Methode ist essenziell, um sicherzustellen, dass die Cluster-Strategie visuell korrekt umgesetzt wird, ohne die bestehende Spielinformation auf dem Spielfeld zu verfälschen.
private void
ResetClusterTargetColor(Point target)
{
Button button =
playerButtons[target.X, target.Y];
//
Stelle sicher, dass nur die Punkte zurückgesetzt werden, die rosa sind
if (button.BackColor == Color.LightPink)
{
if
(button.Tag != null && button.Tag.ToString() == "Ship")
{
button.BackColor = Color.LightGray; // Setze auf
hellgrau zurück, wenn es ein Schiff ist
}
else
{
button.BackColor
= Color.White; // Setze auf weiß zurück, wenn es kein
Schiff ist
}
}
// Ansonsten nichts tun, um bereits beschossene Felder
(orange) nicht zu verändern
}
FireClusterShot
Methode:Diese Methode steuert das Schießen der KI basierend auf der Cluster-Strategie:
Zielauswahl:
clusterTargets.Count
> 0
).clusterTargets[0]
)
ausgewählt und sofort aus der Liste entfernt (clusterTargets.RemoveAt(0)
).Schussvorbereitung:
ResetClusterTargetColor(target)
).Schussausführung:
FireAtPlayerPosition(target.X,
target.Y)
).Spielerzug-Freigabe:
playerCanShoot = true
)
und der Status entsprechend aktualisiert.FireCheckerboardShot
Methode:Diese Methode steuert das Schießen der KI basierend auf der Checkerboard-Strategie:
Zielauswahl:
checkerboardTargets.Count
> 0
), versucht die KI, ein
unbeschossenes Ziel zu finden.Schussvalidierung:
HasAlreadyBeenShot(target)
).FireAtPlayerPosition(target.X,
target.Y)
), und die Methode
endet sofort (return
).Weiteres Vorgehen bei beschossenem Ziel:
Strategiewechsel:
FireRandomShot()
).Beide Methoden sind entscheidend für die taktische Vielfalt der KI, indem sie unterschiedliche Schussstrategien umsetzen, abhängig von der aktuellen Spielsituation.
private void
FireClusterShot()
{
if (clusterTargets.Count > 0)
{
Point target = clusterTargets[0]; // Nimm das erste
Ziel aus der Cluster-Liste
clusterTargets.RemoveAt(0); // Entferne das Ziel aus
der Liste
Console.WriteLine($"Schieße auf
Cluster-Ziel: ({target.X}, {target.Y})");
// Setze die Farbe des Buttons zurück, da das Ziel entfernt wurde
ResetClusterTargetColor(target); // Setze die Farbe
korrekt zurück
//
Führe den Schuss aus und überprüfe, ob es ein Treffer war
FireAtPlayerPosition(target.X, target.Y);
// Wenn es keine Cluster-Ziele mehr gibt, gib den Zug
an den Spieler zurück
if
(clusterTargets.Count == 0)
{
playerCanShoot = true;
lblStatus.Text =
"Spieler ist am Zug";
}
}
}
private
void FireCheckerboardShot()
{
while (checkerboardTargets.Count > 0)
{
Point target = checkerboardTargets[0];
checkerboardTargets.RemoveAt(0);
if
(!HasAlreadyBeenShot(target))
{
Console.WriteLine($"Schieße auf Checkerboard-Ziel: ({target.X}, {target.Y})");
FireAtPlayerPosition(target.X, target.Y);
return; // Verlasse die Methode, nachdem ein gültiger
Schuss abgegeben wurde
}
else
{
Console.WriteLine($"Checkerboard-Ziel bereits beschossen: ({target.X},
{target.Y}), überspringe...");
}
}
// Falls alle Checkerboard-Ziele bereits beschossen
wurden, wechsle zu einer anderen Strategie
if (clusterTargets.Count > 0)
{
FireClusterShot();
}
else
{
FireRandomShot();
}
}
Die Methode FireAtPlayerPosition führt den Schuss der KI auf eine bestimmte Position auf dem Spielfeld des Spielers aus:
Zielbestimmung:
targetButton
)
anhand der angegebenen Zeile (row
)
und Spalte (col
)
zu identifizieren.
Point
-Objekt
(shotPoint
)
wird erstellt, um die Position des Schusses darzustellen.Prüfung auf bereits beschossene Felder:
HasAlreadyBeenShot(shotPoint)
).Schusszählung und Konsistenzprüfung:
kiShots
)
wird um 1 erhöht und mit der Anzahl der Schüsse des Spielers verglichen
(playerShots
).
Treffer- oder Fehlschussprüfung:
targetButton.Tag
== "Ship"
):Color.LightSkyBlue
),
um den Treffer anzuzeigen.lastHitOld
wird auf den vorherigen Treffer gesetzt, bevor
lastHit
auf die aktuelle Schussposition aktualisiert wird.
clusterTargets
entfernt.Color.Orange
).
Dies zeigt einen Fehlschuss an.Zugübergabe:
playerCanShoot
auf
true
gesetzt wird und KIcanShoot
auf false
.
private void FireAtPlayerPosition(int
row, int col)
{
Button targetButton = playerButtons[row, col];
Point shotPoint = new Point(row, col);
if
(HasAlreadyBeenShot(shotPoint))
{
Console.WriteLine($"KRITISCH >>>>>>>>>>>>>>>>>>>>>>>>> Versuchter Schuss auf
bereits beschossenes Feld: ({row}, {col})");
return;
}
kiShots++;
Console.WriteLine($"KI hat {kiShots} mal
geschossen.");
if (kiShots != playerShots)
{
Console.WriteLine("KRITISCH >>>>>>>>>>>>>>>>>>>>>>>>> kiShots != playerShots");
}
if (targetButton.Tag != null && targetButton.Tag.ToString() ==
"Ship")
{
targetButton.BackColor =
Color.LightSkyBlue;
//
Speichere den vorherigen lastHit für AddClusterTargets(...)
lastHitOld = lastHit;
lastHit = shotPoint;
Console.WriteLine($"KI Treffer auf: ({row}, {col})");
// Entferne den getroffenen Punkt aus clusterTargets
clusterTargets.Remove(shotPoint);
Ship hitShip =
playerShips.FirstOrDefault(ship => ship.Contains(shotPoint));
hitShip?.RegisterHit(shotPoint);
if (hitShip != null
&& hitShip.IsSunk())
{
Console.WriteLine($"KI hat ein Schiff versenkt: {hitShip.Name}");
MessageBox.Show($"Dein {hitShip.Name} wurde versenkt!", "Schiff versenkt",
MessageBoxButtons.OK, MessageBoxIcon.Information);
if (playerShips.All(ship => ship.IsSunk()))
{
MessageBox.Show("Alle deine Schiffe wurden versenkt. Die KI hat gewonnen!",
"Spiel beendet", MessageBoxButtons.OK, MessageBoxIcon.Information);
ResetGame();
return;
}
ResetAfterSinking();
}
else
{
// Füge Cluster-Ziele hinzu, wenn das Schiff noch
nicht versenkt wurde
AddClusterTargets(shotPoint);
}
}
else
{
targetButton.BackColor = Color.Orange;
Console.WriteLine($"KI Fehlschuss auf: ({row}, {col})");
// Cluster-Strategie fortzusetzen
}
// Gebe den Zug an den Spieler zurück
playerCanShoot = true;
KIcanShoot = false;
lblStatus.Text =
"Spieler ist am Zug";
}
Die Methode ResetAfterSinking wird aufgerufen, nachdem die KI ein Schiff des Spielers versenkt hat:
Zurücksetzen der Schussinformationen:
lastHit
und currentDirection
werden auf null
gesetzt, da das Schiff versenkt wurde und die KI sich neu orientieren
soll.Cluster-Ziele bereinigen:
Aktualisierung und Debugging:
private void ResetAfterSinking()
{
lastHit = null;
currentDirection = null;
// Behalte Cluster-Ziele bei, die noch relevant sein
könnten
List<Point> remainingTargets = new
List<Point>();
foreach (var point in clusterTargets)
{
if (IsValidPoint(point) && !HasAlreadyBeenShot(point))
{
remainingTargets.Add(point);
}
else
{
ResetClusterTargetColor(point);
// Setze die Farbe zurück, falls es kein Ziel mehr ist
}
}
clusterTargets = remainingTargets;
Console.WriteLine("Cluster-Ziele nach Reset aktualisiert.");
DebugClusterTargets();
}
Die Methode
HasAlreadyBeenShot
prüft, ob ein bestimmtes
Spielfeld bereits beschossen wurde:
Grenzprüfung:
point.X
,
point.Y
) innerhalb der
gültigen Spielfeldgrenzen liegen.
ArgumentOutOfRangeException
geworfen, um auf einen ungültigen
Punkt hinzuweisen.Zustand des Spielfeldes:
Button
-Objekt (targetButton
),
das dem angegebenen Punkt entspricht, abgerufen.
true
zurück, wenn das Spielfeld
bereits beschossen wurde, d.h. wenn seine Hintergrundfarbe entweder
LightSkyBlue
(Treffer) oder
Orange
(Fehlschuss) ist.
false
zurückgegeben.
private bool HasAlreadyBeenShot(Point point)
{
// Überprüfen, ob die Koordinaten
innerhalb der gültigen Grenzen liegen
if
(point.X < 0 || point.X >= playerButtons.GetLength(0) || point.Y < 0 || point.Y
>= playerButtons.GetLength(1))
{
// Wenn die Koordinaten außerhalb der Grenzen liegen,
wird eine Exception geworfen
throw new ArgumentOutOfRangeException($"Ungültiger Punkt: ({point.X},
{point.Y})");
}
Button targetButton =
playerButtons[point.X, point.Y];
return targetButton.BackColor ==
Color.LightSkyBlue || targetButton.BackColor == Color.Orange;
}
Die Methode
GetNextValidPointInDirection
bestimmt
den nächsten möglichen Punkt in einer bestimmten Richtung, um zu überprüfen, ob
dieser Punkt für einen Schuss gültig ist:
Berechnung des nächsten Punktes:
nextPoint
)
basierend auf der aktuellen Position (row
,
col
)
und der angegebenen Schussrichtung (direction
).
switch
-Ausdrucks
ausgewertet.Gültigkeitsprüfung:
nextPoint
)
innerhalb der Spielfeldgrenzen liegt und ob das Feld bereits beschossen
wurde.
null
zurückgegeben.nextPoint
zurückgegeben.
private Point? GetNextValidPointInDirection(int row, int col, Direction
direction)
{
Point nextPoint = direction switch
{
Direction.Up => new Point(row - 1, col),
Direction.Down =>
new Point(row + 1, col),
Direction.Left => new Point(row,
col - 1),
Direction.Right => new Point(row, col + 1),
_ => throw new InvalidOperationException("Ungültige Richtung")
};
// Überprüfe, ob der Punkt innerhalb des
gültigen Bereichs liegt
if
(!IsValidPoint(nextPoint) || HasAlreadyBeenShot(nextPoint))
{
return null;
}
return nextPoint;
}
Die Methode IsValidPoint
prüft,
ob ein gegebener Punkt (Point
)
innerhalb der gültigen Spielfeldgrenzen liegt.
Grenzprüfung:
isValid
,
indem sie überprüft, ob die X- und Y-Koordinaten des Punktes innerhalb
der Grenzen des Spielfelds liegen.
point.X
und point.Y
jeweils größer oder gleich 0 und kleiner als 10 sind. Diese Grenzwerte
entsprechen den Dimensionen unseres 10x10-Battleship-Spielfelds.Rückgabewert:
isValid
zurück, der angibt, ob der Punkt innerhalb der gültigen Spielfeldgrenzen
liegt.
private
bool IsValidPoint(Point point)
{
bool isValid = point.X >= 0 &&
point.X < 10 && point.Y >= 0 && point.Y < 10;
//Console.WriteLine($"Punkt ({point.X}, {point.Y}) ist
gültig: {isValid}");
return isValid;
}
Die Methode
DetermineDirection
wird verwendet, um
die Richtung für den nächsten Schuss der KI zu bestimmen, wenn bereits ein
Treffer gelandet wurde.
Sie ist flexibel genug, um je nach Situation
verschiedene Strategien auszuwählen, und stellt sicher, dass die KI immer einen
sinnvollen Schuss abgibt, selbst wenn die Verfolgung eines vorherigen Treffers
fehlschlägt.
Überprüfung auf letzten Treffer:
lastHit
gesetzt ist. Dies zeigt an, dass es einen vorherigen Treffer gab, den
die KI jetzt weiterverfolgen will, um das Schiff des Spielers zu
versenken.Richtungsversuche:
Up
,
Down
,
Left
,
Right
)
mithilfe einer foreach
-Schleife.
GetNextValidPointInDirection
aufgerufen, um den nächsten potenziellen Schusspunkt basierend auf der
letzten Trefferposition (lastHit
)
und der aktuellen Richtung zu berechnen.
FireAtPlayerPosition
aufgerufen
wird, und die Schleife wird abgebrochen (break
).Wenn kein Schuss abgegeben wurde:
lastHit
und currentDirection
auf null
zurück. Das bedeutet, dass die KI keine direkte Fortsetzung eines
vorherigen Treffers mehr verfolgt.
FireClusterShot
aufgerufen.
FireCheckerboardShot
aufgerufen.
FireRandomShot
aufgerufen,
wenn keine der anderen Strategien verfügbar ist.Status-Update:
KIcanShoot
auf false
gesetzt und playerCanShoot
auf true
.
Damit wird sichergestellt, dass der Spieler nun am Zug ist.
private void DetermineDirection(int row, int col)
{
if
(lastHit.HasValue)
{
Point last = lastHit.Value;
bool shotFired = false;
//
Versuche in den vier Hauptrichtungen (oben, unten, links, rechts)
weiterzuschießen
foreach
(Direction dir in Enum.GetValues(typeof(Direction)))
{
Point? nextPoint = GetNextValidPointInDirection(last.X, last.Y, dir);
if (nextPoint.HasValue && !HasAlreadyBeenShot(nextPoint.Value))
{
Console.WriteLine($"KI versucht
zu schießen in Richtung: {dir} auf ({nextPoint.Value.X}, {nextPoint.Value.Y})");
FireAtPlayerPosition(nextPoint.Value.X, nextPoint.Value.Y);
shotFired = true;
break;
// Beende die Schleife nach einem erfolgreichen Schuss
}
else
{
Console.WriteLine($"Richtung {dir} ist ungültig oder bereits beschossen
({nextPoint?.X}, {nextPoint?.Y})");
}
}
// Wenn kein Schuss abgegeben
wurde, setze lastHit und currentDirection zurück
if (!shotFired)
{
Console.WriteLine("Kein Treffer möglich, setze lastHit und currentDirection
zurück.");
lastHit = null;
currentDirection = null;
// Wechsel zu einer anderen Strategie (Cluster oder Checkerboard)
if (clusterTargets.Count > 0)
{
FireClusterShot();
}
else if (checkerboardTargets.Count > 0)
{
FireCheckerboardShot();
}
else
{
FireRandomShot(); // Fallback-Strategie, falls nichts
anderes möglich ist
}
}
else
{
// Gib den Zug an den Spieler zurück
KIcanShoot = false;
playerCanShoot = true;
lblStatus.Text = "Spieler ist am Zug";
}
}
}
Die FireRandomShot
-Methode ist eine
Fallback-Strategie der KI, die in Situationen eingesetzt wird, in denen keine
gezielten Angriffe möglich sind.
Zufällige Zielauswahl:
row
und
col
,
werden generiert, die als Koordinaten für den nächsten Schuss der KI dienen.
Point
-Objekt
gespeichert, das die Zielposition repräsentiert.Überprüfung auf bereits beschossenes Feld:
do-while
-Loop stellt sicher, dass
die KI nicht auf ein neues noch nicht beschossenes Feld schießt.
HasAlreadyBeenShot
prüft, ob das zufällig ausgewählte Feld bereits getroffen wurde.Schuss abgeben:
FireAtPlayerPosition
auf, um den
Schuss auszuführen.Zugwechsel:
playerCanShoot
wird auf true
gesetzt und KIcanShoot
auf false
,
um anzuzeigen, dass der Spieler nun wieder am Zug ist.
private void FireRandomShot()
{
Point target;
do
{
int row = rand.Next(0, 10);
int
col = rand.Next(0, 10);
target = new Point(row, col);
} while (HasAlreadyBeenShot(target));
FireAtPlayerPosition(target.X, target.Y);
// Nach dem Zufallsschuss ist der Spieler wieder an
der Reihe
playerCanShoot = true;
KIcanShoot = false;
lblStatus.Text = "Spieler ist am Zug";
}
Die EnemyButton_Click
Methode stellt
sicher, dass das Spiel in geordneten Runden abläuft, indem sie die Reihenfolge
der Züge zwischen Spieler und KI regelt.
Die Methode ist entscheidend für die Interaktivität des
Spiels, da sie die Eingaben des Spielers verarbeitet und auf Grundlage dieser
Eingaben den weiteren Verlauf des Spiels steuert.
Überprüfung, ob das Spiel gestartet ist:
Überprüfung, ob der Spieler schießen darf:
Identifizierung des angeklickten Buttons:
Überprüfung, ob das Feld bereits beschossen wurde:
Schuss auf feindliches Schiff:
Tag
-Attribut),
wird der Button auf die Farbe LightSkyBlue gesetzt, um einen Treffer
anzuzeigen.
CheckIfShipSunk
wird aufgerufen, um zu überprüfen, ob das getroffene Schiff vollständig
versenkt wurde.Fehlschuss:
Zugwechsel:
playerShots
wird um 1 erhöht, um die Anzahl der abgegebenen Schüsse des Spielers zu
zählen.
playerCanShoot
auf
false
gesetzt und der Status aktualisiert wird.Verzögerung vor dem KI-Zug:
KIMove
aufgerufen, um den Zug der KI durchzuführen.
private void EnemyButton_Click(object sender, EventArgs e)
{
if
(!gameStarted) // Verhindert das Klicken, bevor das
Spiel startet
{
MessageBox.Show("Bitte starten Sie das Spiel (feindliche
Schiffe werden unsichtbar platziert), bevor Sie angreifen!", "Warnung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (!playerCanShoot)
// Verhindert doppeltes Klicken des Spielers
{
MessageBox.Show("Bitte warten Sie, bis die KI ihren Zug beendet hat!",
"Warnung", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
Button clickedButton = sender as Button;
if (clickedButton.BackColor
== Color.LightSkyBlue || clickedButton.BackColor == Color.Orange)
{
MessageBox.Show("Sie haben hier bereits geschossen.", "Achtung",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
lblStatus.Text = "Spieler ist am Zug";
if (clickedButton.Tag != null &&
clickedButton.Tag.ToString() == "Ship")
{
clickedButton.BackColor =
Color.LightSkyBlue;
CheckIfShipSunk(enemyShips, clickedButton);
}
else
{
clickedButton.BackColor = Color.Orange;
}
playerShots++;
Console.WriteLine($"Spieler hat {playerShots} mal geschossen.");
//
Spieler kann nun nicht mehr schießen, bis die KI ihren Zug beendet hat
playerCanShoot = false;
lblStatus.Text = "KI ist am Zug";
Timer timer
= new Timer();
timer.Interval = 500; // 500 ms Pause
timer.Tick += (s,
args) =>
{
timer.Stop();
timer.Dispose(); // Timer
entsorgen, wenn er nicht mehr benötigt wird
KIMove();
};
timer.Start();
}
Die
CheckIfShipSunk
Methode stellt sicher,
dass das Spiel den Status jedes Schiffs korrekt überwacht und entsprechend
reagiert, wenn ein Schiff vollständig zerstört wurde.
Sie sorgt für eine flüssige Spielerfahrung, indem sie den Spieler über wichtige Ereignisse wie die Versenkung eines Schiffs informiert.
Durch die Integration der
CheckGameEnd
Methode wird das Spielende überwacht und der Spielfluss bleibt konsistent.
Die Methode trägt wesentlich zur strategischen Tiefe des Spiels bei, indem sie sicherstellt, dass das Spiel korrekt auf die Aktionen des Spielers und der KI reagiert.
Ermittlung der Position des angeklickten Buttons:
Point
)
des angeklickten Buttons auf dem Spielfeld zu ermitteln. Dies erfolgt über
die Methode GetButtonPosition
.Durchlaufen der Liste der Schiffe:
ships
),
um herauszufinden, ob das getroffene Feld zu einem der Schiffe gehört.
Contains
-Methode
des Ship
-Objekts,
die überprüft, ob der clickedPoint
zu den Positionen des jeweiligen Schiffs gehört.Registrierung des Treffers:
RegisterHit
beim betroffenen Schiff registriert.Überprüfung, ob das Schiff versenkt wurde:
IsSunk
,
ob alle Positionen des Schiffs getroffen wurden, was bedeutet, dass das
Schiff vollständig versenkt wurde.Benachrichtigung bei Versenkung:
enemyShips
),
so werden alle Positionen dieses Schiffs auf dem Spielfeld mit dem
entsprechenden Schiffsnamen markiert (z. B. mit einem Buchstaben wie "C" für
"Cruiser").Überprüfung des Spielendes:
CheckGameEnd
aufgerufen, um zu prüfen, ob alle Schiffe einer Seite versenkt wurden. Wenn
ja, wird das Spiel beendet.currentDirection
wird auf null
gesetzt, um die aktuelle Schussrichtung der KI zurückzusetzen, falls sie
während des Spiels einen spezifischen Kurs verfolgte.Abbruch der Schleife:
break
beendet. Dies verhindert unnötige weitere Durchläufe, sobald das betroffene
Schiff gefunden wurde.
private void CheckIfShipSunk(List<Ship> ships, Button clickedButton)
{
Point clickedPoint = GetButtonPosition(clickedButton, enemyButtons);
foreach (var ship in ships)
{
if (ship.Contains(clickedPoint))
{
ship.RegisterHit(clickedPoint);
if (ship.IsSunk())
{
string
shipOwner = (ships == playerShips) ? "Dein eigenes Schiff" : "Ein feindliches
Schiff";
MessageBox.Show($"{shipOwner}, das {ship.Name}, wurde versenkt!",
"Schiff versenkt", MessageBoxButtons.OK, MessageBoxIcon.Information);
if
(ships == enemyShips)
{
foreach (var pos in ship.Positions)
{
Button
btn = enemyButtons[pos.X, pos.Y];
btn.Text = ship.Name[0].ToString();
}
}
CheckGameEnd(ships);
currentDirection = null;
}
break;
}
}
}
Die Methode GetAdjacentPoints
ermittelt
alle benachbarten Punkte zu einem gegebenen Punkt auf dem Spielfeld. Sie
generiert Nachbarpunkte (oben, unten, links, rechts) und filtert dabei
diejenigen heraus, die innerhalb der gültigen Spielfeldgrenzen liegen, indem die
IsValidPoint
Methode aufgerufen wird. Dies ist nützlich für die
Cluster-Strategie, um Treffer in der Nähe zu identifizieren und daraufhin
weitere Schüsse zu platzieren.
// Hilfsmethode zur Bestimmung angrenzender Punkte
innerhalb des Spielfelds
private IEnumerable<Point>
GetAdjacentPoints(Point point)
{
return new List<Point>
{
new
Point(point.X - 1, point.Y),
new Point(point.X + 1, point.Y),
new
Point(point.X, point.Y - 1),
new Point(point.X, point.Y + 1)
}.Where(p =>
IsValidPoint(p));
}
Die Methode CheckAndCountButtonColor
überprüft die Hintergrundfarbe eines Buttons und zählt entsprechend die Anzahl
der Felder, die entweder weiß (unbeschossen), hellgrau (Schiff, aber
unbeschossen), oder hellrosa (potenzielles Ziel in der Cluster-Strategie) sind.
Die Zähler werden durch Referenzparameter (ref
) aktualisiert. Dies
ist als Debugging Tool nützlich, um den Zustand des Spielfelds zu analysieren,
insbesondere nach dem Spielende oder zur laufenden Spielanalyse.
// Hilfsmethode zur Bestimmung nicht beschossener Punkte
private void CheckAndCountButtonColor(Button
button, ref int whiteCount, ref int lightGrayCount, ref int lightPinkCount)
{
if (button.BackColor == Color.White)
{
whiteCount++;
}
else if
(button.BackColor == Color.LightGray)
{
lightGrayCount++;
}
else if
(button.BackColor == Color.LightPink)
{
lightPinkCount++;
}
else
{
//Console.WriteLine($"Unbekannte Farbe auf Feld ({GetButtonPosition(button,
playerButtons)}): {button.BackColor}");
}
}
Die Methode
CheckGameEnd
überprüft, ob alle Schiffe
einer Liste (ships
)
versenkt sind, was das Ende des Spiels signalisiert.
Wenn dies der Fall ist,
werden zwei Zählvariablen für den Spieler und die KI initialisiert, um die
Anzahl der weißen, hellgrauen und hellrosa Felder auf dem Spielfeld zu zählen.
In zwei geschachtelten Schleifen werden alle
Buttons des Spieler- und KI-Spielfelds durchlaufen und die Methode
CheckAndCountButtonColor
aufgerufen, um die Farben der Buttons zu überprüfen und die Zähler entsprechend
zu aktualisieren. Anschließend werden die Ergebnisse als Debug-Informationen
ausgegeben.
Nach der Zählung und Ausgabe der Ergebnisse wird
das Spiel durch Aufruf der Methode
ResetGame
zurückgesetzt.
Diese
beendet das laufende Spiel, ermöglicht es, den Start-Button erneut zu
aktivieren, und startet die Anwendung neu, um ein neues Spiel zu beginnen.
private void
CheckGameEnd(List<Ship> ships)
{
if (ships.All(ship => ship.IsSunk()))
{
int playerWhiteCount = 0, playerLightGrayCount = 0, playerLightPinkCount =
0;
int kiWhiteCount = 0, kiLightGrayCount = 0, kiLightPinkCount = 0;
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
CheckAndCountButtonColor(playerButtons[i, j], ref playerWhiteCount, ref
playerLightGrayCount, ref playerLightPinkCount);
CheckAndCountButtonColor(enemyButtons[i, j], ref kiWhiteCount, ref
kiLightGrayCount, ref kiLightPinkCount);
}
}
// Ausgabe der
Ergebnisse als Debug-Information
Console.WriteLine("Spieler Felder:");
Console.WriteLine($"Weiße Felder: {playerWhiteCount}, Hellgraue Felder:
{playerLightGrayCount}, Hellrosa Felder: {playerLightPinkCount}, Summe:
{playerWhiteCount + playerLightGrayCount + playerLightPinkCount}");
Console.WriteLine("KI Felder:");
Console.WriteLine($"Weiße Felder:
{kiWhiteCount}, Hellgraue Felder: {kiLightGrayCount}, Hellrosa Felder:
{kiLightPinkCount}, Summe: {kiWhiteCount + kiLightGrayCount +
kiLightPinkCount}");
ResetGame();
}
}
private
void ResetGame()
{
// Beende das Spiel und setze es zurück
gameStarted
= false;
btnStart.Enabled = true;
MessageBox.Show("Das Spiel wird jetzt
neu gestartet.", "Spiel Reset", MessageBoxButtons.OK,
MessageBoxIcon.Information);
Application.Restart();
// Einfaches Beispiel:
Spiel neu starten
}
private void btnStart_Click(object sender,
EventArgs e)
{
if (gameStarted)
{
MessageBox.Show("Das Spiel läuft
bereits!");
return;
}
// Prüfen, ob alle Schiffe des Spielers
platziert wurden
if (playerShips.Any(ship => ship.Positions.Count == 0))
{
MessageBox.Show("Bitte platzieren Sie alle Ihre Schiffe, bevor Sie das Spiel
starten!");
return;
}
// KI-Schiffe platzieren
PlaceEnemyShips();
// Spiel als gestartet markieren
gameStarted =
true;
btnStart.Enabled = false; // Deaktiviert den Start-Button
lblStatus.Text = "Das Spiel hat begonnen! Ihr Zug.";
}
Die Methode
PlaceEnemyShips
ist verantwortlich für
die Platzierung der Schiffe der KI auf dem Spielfeld. Zunächst wird eine Liste
mit den verschiedenen Schiffstypen der KI erstellt.
Für jedes dieser Schiffe
wird eine Position und eine zufällige Ausrichtung (vertikal oder horizontal)
gewählt.
Die Methode überprüft dann, ob das Schiff an der
gewählten Position platziert werden kann, ohne das Spielfeld zu verlassen oder
mit einem bereits platzierten Schiff zu kollidieren.
Diese Überprüfung
erfolgt durch die Methode
CanPlaceEnemyShip
.
Wenn die Position gültig ist, wird das Schiff mit
der Methode PlaceEnemyShip
an der gewählten Position platziert.
Dabei werden die entsprechenden Buttons
im enemyButtons
-Array
markiert, um anzuzeigen, dass sich dort ein Schiff befindet.
Die Positionen
des Schiffs werden in der Positions
-Liste
des Schiffs gespeichert.
Nach der erfolgreichen Platzierung aller Schiffe wird eine Erfolgsmeldung ausgegeben.
Die Methode
CanPlaceEnemyShip
überprüft, ob ein
Schiff in der gewählten Ausrichtung und Position auf dem Spielfeld platziert
werden kann, ohne das Spielfeld zu verlassen oder mit einem anderen Schiff zu
kollidieren.
PlaceEnemyShip
platziert schließlich das Schiff auf dem Spielfeld und markiert die
entsprechenden Positionen als belegt.
private
void PlaceEnemyShips()
{
Random rand = new Random();
// Erstellen
der Schiffe für die KI
enemyShips = new List<Ship>
{
new Ship(new
List<Point>(), "Submarine"),
new Ship(new List<Point>(), "Destroyer"),
new
Ship(new List<Point>(), "Cruiser"),
new Ship(new List<Point>(),
"Battleship"),
new Ship(new List<Point>(), "Carrier")
};
foreach
(var ship in enemyShips)
{
bool placed = false;
while (!placed)
{
int row = rand.Next(0, 10);
int col = rand.Next(0, 10);
ship.IsVertical = rand.Next(0, 2) == 0; // Zufällige Ausrichtung
//
Bestimmen der Schiffslänge basierend auf dem Schiffstyp
int shipSize =
ship.Name switch
{
"Submarine" => 1,
"Destroyer" => 2,
"Cruiser" =>
3,
"Battleship" => 4,
"Carrier" => 5,
_ => 1
};
ship.Positions.Clear(); // Sicherstellen, dass die Positionen vor der
Platzierung leer sind
if (CanPlaceEnemyShip(row, col, shipSize,
ship.IsVertical))
{
PlaceEnemyShip(row, col, ship, shipSize);
placed =
true;
}
}
}
MessageBox.Show("KI-Schiffe wurden erfolgreich
platziert.", "Info", MessageBoxButtons.OK, MessageBoxIcon.Information);
Console.WriteLine("KI-Schiffe wurden erfolgreich platziert.");
}
private bool CanPlaceEnemyShip(int row, int col, int shipSize, bool isVertical)
{
if (isVertical)
{
if (row + shipSize > 10) return false;
// Prüfen,
ob das Schiff vertikal aus dem Spielfeld ragt
for (int i = 0; i <
shipSize; i++)
{
if (enemyButtons[row + i, col].Tag != null &&
enemyButtons[row + i, col].Tag.ToString() == "Ship")
{
return false;
//
Stelle sicher, dass die Felder frei sind
}
}
}
else
{
if (col
+ shipSize > 10) return false; // Prüfen, ob das Schiff horizontal aus dem
Spielfeld ragt
for (int i = 0; i < shipSize; i++)
{
if
(enemyButtons[row, col + i].Tag != null && enemyButtons[row, col +
i].Tag.ToString() == "Ship")
{
return false;
// Stelle sicher, dass die
Felder frei sind
}
}
}
return true;
}
private void
PlaceEnemyShip(int row, int col, Ship ship, int shipSize)
{
if
(ship.IsVertical)
{
for (int i = 0; i < shipSize; i++)
{
enemyButtons[row + i, col].Tag = "Ship";
ship.Positions.Add(new Point(row +
i, col));
}
}
else
{
for (int i = 0; i < shipSize; i++)
{
enemyButtons[row, col + i].Tag = "Ship";
ship.Positions.Add(new Point(row,
col + i));
}
}
Console.WriteLine($"{ship.Name} platziert!
Positionen: {string.Join(", ", ship.Positions)}");
}
Die Methode
GetButtonPosition
dient dazu, die
Position eines bestimmten Buttons in einem zweidimensionalen Array von Buttons (buttonsArray
)
zu ermitteln.
buttonsArray
.button
)
übereinstimmt.
Point
zurückgegeben. Hierbei steht i
für die Zeile (X-Koordinate) und j
für die Spalte (Y-Koordinate).Point.Empty
zurück. Diese Methode ist besonders nützlich, um herauszufinden, wo sich ein bestimmter Button im Spielfeld-Array befindet, etwa um gezielte Aktionen (wie Treffer oder Fehlschüsse) auf Basis seiner Position durchzuführen.
private
Point GetButtonPosition(Button button, Button[,] buttonsArray)
{
for (int
i = 0; i < buttonsArray.GetLength(0); i++)
{
for (int j = 0; j <
buttonsArray.GetLength(1); j++)
{
if (buttonsArray[i, j] == button)
{
return new Point(i, j);
}
}
}
return Point.Empty;
// Sollte nicht
passieren, wenn der Button tatsächlich im Array ist
}
} //
class Form1
} //
namespace ...
Da es sich hier bereits um einen umfangreicheren Code handelt, findet man hier das Projekt zum Download (incl. exe-Dateien zum sofortigen Testen).
Viel Spaß beim Testen und bei der Weiterentwicklung!
Wer das Spiel in Aktion
sehen möchte,
hier gibt es
ein kurzes Video.
Nun stellt sich die Frage, wie man ausgehend von diesem
Code die KI-Strategien selbst weiter verbessern könnte. Es fällt z.B. auf, dass
die KI bei vier Möglichkeiten um einen Treffer immer in der festgelegten Serie
oben, unten, links, rechts vorgeht. Der Spieler könnte nun all seine Schiffe
horizontal platzieren, damit es drei oder vier Züge dauert, bis der zweite
Treffer auf das Schiff gelingt, der die Orientierung preisgibt.
Hier
schlage ich folgende Ideen vor: Wir verwenden in zwei Methoden den Zufall, um
die Regelmäßigkeit zu umgehen.
private void
FireClusterShot()
{
if (clusterTargets.Count > 0)
{
// Wähle
zufällig ein Ziel aus
der Cluster-Liste
int randomIndex = rand.Next(0,
clusterTargets.Count);
//
rand.Next(int minValue, int maxValue)
: Diese Methode erzeugt
eine zufällige Ganzzahl, die einschließlich
minValue
und ausschließlich
maxValue
ist.
Point target = clusterTargets[randomIndex];
clusterTargets.RemoveAt(randomIndex); // Entferne das
Ziel aus der Liste
Console.WriteLine($"Schieße auf
Cluster-Ziel: ({target.X}, {target.Y})");
// Setze die Farbe des Buttons zurück, da das Ziel
entfernt wurde
ResetClusterTargetColor(target);
// Führe den Schuss aus und
überprüfe, ob es ein Treffer war
FireAtPlayerPosition(target.X, target.Y);
// Wenn es keine Cluster-Ziele mehr gibt, gib den Zug
an den Spieler zurück
if (clusterTargets.Count ==
0)
{
playerCanShoot =
true;
lblStatus.Text = "Spieler ist am Zug";
}
}
}
private void DetermineDirection(int row, int col)
{
if (lastHit.HasValue)
{
Point last =
lastHit.Value;
bool shotFired = false;
// Liste der Richtungen
List<Direction> directions = new List<Direction> { Direction.Up, Direction.Down,
Direction.Left, Direction.Right };
// Mischen
der Richtungen
directions = directions.OrderBy(x =>
rand.Next()).ToList();
//
Debug-Ausgabe der zufälligen Reihenfolge
Console.WriteLine("Zufällige Reihenfolge der Richtungen:");
foreach (var dir in directions)
{
Console.WriteLine(dir);
}
// Versuche in den vier (zufällig sortierten)
Richtungen weiterzuschießen
foreach (Direction dir
in directions)
{
Point?
nextPoint = GetNextValidPointInDirection(last.X, last.Y, dir);
if (nextPoint.HasValue && !HasAlreadyBeenShot(nextPoint.Value))
{
Console.WriteLine($"KI versucht
zu schießen in Richtung: {dir} auf ({nextPoint.Value.X}, {nextPoint.Value.Y})");
FireAtPlayerPosition(nextPoint.Value.X, nextPoint.Value.Y);
shotFired = true;
break;
// Beende die Schleife nach einem erfolgreichen Schuss
}
else
{
Console.WriteLine($"Richtung {dir} ist ungültig oder bereits beschossen
({nextPoint?.X}, {nextPoint?.Y})");
}
}
// Wenn kein Schuss
abgegeben wurde, setze lastHit und currentDirection zurück
if (!shotFired)
{
Console.WriteLine("Kein Treffer möglich, setze lastHit und currentDirection
zurück.");
lastHit = null;
currentDirection = null;
// Wechsel zu einer anderen Strategie (Cluster oder
Checkerboard)
if (clusterTargets.Count
> 0)
{
FireClusterShot();
}
else if (checkerboardTargets.Count > 0)
{
FireCheckerboardShot();
}
else
{
FireRandomShot(); // Fallback-Strategie, falls nichts
anderes möglich ist
}
}
else
{
// Gib den Zug an den Spieler zurück
KIcanShoot = false;
playerCanShoot = true;
lblStatus.Text = "Spieler ist am Zug";
}
}
}
Solche kleinen Veränderungen erbringen eine große
Wirkung, denn jede erkannte Regelmäßigkeit kann durch den Spieler ausgenutzt
werden.
Vielleicht haben Sie viel bessere Ideen. Nur her damit! Meine Email
finden Sie im Impressum.
Mastermind ist ein interessantes Spiel, bei dem es gilt einen Code zu knacken. Vier Farben müssen in der richtigen Reihenfolge ausgewählt werden. Als Feedback gibt es schwarze (oder rote) Pins für eine korrekte Farbe auf der richtigen Position. Weiße Pins zeigen an, ob beim Rest richtige Farben - allerdings auf falscher Position - dabei sind.
Das Design ist einfach:
Oben haben wir ein label1 mit "Wähle deine Farben".
Darunter kommen vier ComboBoxes mit den sechs Farben Red, Blue, Green, Yellow,
White, Black.
Zur Steuerung haben wir den Button btnCheckAttempt mit dem
Text "Überprüfen" und den Button btnNewGame mit dem Text "Neues Spiel".
Zur Ausgabe unserer Versuche verwenden wir eine ListBox listBoxAttempts. Diese wird zum Nachverfolgen die gewählten Farben und schwarze und weiße Pins ausgeben.
Ganz unten finden wir zur Unterstützung des Spielers das
Label labelCorrectPositions, das die korrekten Positionen, die wir finden,
ausgibt.
Lässt man diese Ausgabe weg, ist das Spiel deutlich schwieriger.
Bisher haben wir User Interface (UI) und Spiellogik
in einem Modul belassen. In diesem Projekt trennen wir UI, Spiel und Künstliche
Intelligenz.
Das Modul Program.cs sieht wie folgt aus:
Die Program
-Klasse ist als static
deklariert, da sie keine Instanz benötigt, um Methoden aufzurufen.
Die
Klasse ist internal
, was bedeutet, dass sie nur innerhalb desselben
Projekts zugänglich ist.
Die Main
-Methode ist der
Haupteinstiegspunkt für die Anwendung. Sie wird als erstes aufgerufen, wenn das
Programm startet.
Main
-Methode, dass die
Anwendung einen Single-Threaded Apartment (STA)-Modus für den COM-Zugriff
verwendet. Clipboard
und
Drag and Drop
korrekt zu verwenden.Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
false
wird die neue GDI+-basierte Textwiedergabe verwendet.Application.Run(new MainForm());
MainForm
).
Die Methode Run
hält die Anwendung in einer Schleife, bis das
Hauptfenster geschlossen wird.
using System;
using
System.Windows.Forms;
namespace Mastermind.UI
{
internal
static class Program
{
/// <summary>
/// Der Haupteinstiegspunkt für die Anwendung.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
Das Modul für die künstliche Intelligenz (wenig
gefordert) ist recht einfach:
Die AI
-Klasse repräsentiert
eine einfache künstliche Intelligenz für das Mastermind-Spiel. Sie kann
zufällige Farbcodes generieren und zufällige Farbversuche machen.
Die
Methoden GenerateCode
und MakeGuess
nutzen eine Liste
verfügbarer Farben und eine Länge, um Zufallslisten von Farbnamen zu erstellen.
Dies ist eine grundlegende Implementierung, die als Ausgangspunkt für
komplexere KI-Algorithmen verwendet werden kann.
private static readonly Random random = new Random();
Random
-Objekt, das für die
Generierung zufälliger Zahlen verwendet wird.static readonly
bedeutet, dass das
Random
-Objekt
für alle Instanzen der AI
-Klasse gleich ist und nicht geändert
werden kann, nachdem es initialisiert wurde.
private List<string> availableColors;
public AI(List<string> availableColors)
AI
-Klasse.
availableColors
ist eine Liste
von Strings, die die möglichen Farben enthält.
availableColors
null
ist oder leer ist, wird eine
ArgumentException
geworfen. Dies stellt sicher, dass die KI eine gültige Liste von Farben zum
Arbeiten hat.
public List<string> GenerateCode(int codeLength)
codeLength
ist die gewünschte
Länge des zu generierenden Codes.
codeLength
kleiner oder
gleich null ist, wird eine ArgumentException
geworfen.
code
.
codeLength
-mal.
availableColors
zur
code
-Liste hinzugefügt.code
-Liste
zurück.
public List<string> MakeGuess(int codeLength)
codeLength
ist die gewünschte
Länge des zu erstellenden Versuchs.
codeLength
kleiner oder
gleich null ist, wird eine ArgumentException
geworfen.
guess
.
codeLength
-mal.
availableColors
zur
guess
-Liste hinzugefügt.guess
-Liste
zurück.
using System;
using
System.Collections.Generic;
namespace Mastermind.core
{
public class AI
{
private static readonly Random random = new Random();
private List<string> availableColors;
public
AI(List<string> availableColors)
{
if (availableColors == null || availableColors.Count == 0)
{
throw new ArgumentException("Die
Liste der verfügbaren Farben darf nicht leer sein.", nameof(availableColors));
}
this.availableColors = availableColors;
}
public List<string> GenerateCode(int codeLength)
{
if (codeLength <= 0)
{
throw new ArgumentException("Die
Länge des Codes muss größer als 0 sein.", nameof(codeLength));
}
var code
= new List<string>();
for (int i = 0; i <
codeLength; i++)
{
code.Add(availableColors[random.Next(availableColors.Count)]);
}
return code;
}
public List<string> MakeGuess(int codeLength)
{
if (codeLength <= 0)
{
throw new ArgumentException("Die
Länge des Codes muss größer als 0 sein.", nameof(codeLength));
}
var guess = new List<string>();
for (int i = 0; i < codeLength; i++)
{
guess.Add(availableColors[random.Next(availableColors.Count)]);
}
return guess;
}
}
}
Nun kommt das Modul MastermindGame.cs, das die Spiellogik
steuert:
Die MastermindGame
-Klasse bildet die Kernlogik des
Mastermind-Spiels ab und verwaltet den Spielablauf.
Sie ist dafür
verantwortlich, einen geheimen Code zu generieren, Versuche zu überprüfen und
den Spielstatus zu aktualisieren.
Diese Klasse ermöglicht es, das Spiel zu
spielen und die Spielregeln zu implementieren, wie sie im traditionellen
Mastermind-Spiel gelten.
Felder und Eigenschaften
codeLength
: Die Länge des zu generierenden
Codes (Standard: 4).availableColors
: Die Liste der möglichen
Farben, die im Spiel verwendet werden können.Code
: Der generierte geheime Code, den der
Spieler erraten muss.Attempts
: Die Anzahl der Versuche, die der
Spieler bereits unternommen hat.IsGameOver
: Ein boolescher Wert, der
angibt, ob das Spiel beendet ist.
Konstruktor
MastermindGame
Methode
GenerateCode
codeLength
und den availableColors
.
Random
-Generator, um zufällig Farben aus
der verfügbaren Liste auszuwählen.
Methode
CheckAttempt
attempt
), indem sie ihn mit dem
geheimen Code vergleicht.blackPins
): Zeigen an, wie
viele Farben an der richtigen Position sind.whitePins
): Zeigen an, wie
viele Farben zwar korrekt sind, aber an der falschen Position.IsGameOver
auf
true
, wenn der Spieler
den Code korrekt erraten hat.
using System;
using
System.Collections.Generic;
namespace Mastermind.Core
{
public class MastermindGame
{
private readonly
int codeLength;
private readonly List<string>
availableColors;
public List<string> Code { get; private
set; }
public int Attempts { get; private set; }
public bool IsGameOver { get; private set; }
public
MastermindGame(int codeLength = 4, List<string> availableColors = null)
{
this.codeLength = codeLength;
this.availableColors = availableColors ?? new List<string> { "Red", "Blue",
"Green", "Yellow", "White", "Black" };
Code =
GenerateCode();
Attempts = 0;
IsGameOver = false;
}
private
List<string> GenerateCode()
{
var random = new Random();
var code = new
List<string>();
for (int i = 0; i <
codeLength; i++)
{
code.Add(availableColors[random.Next(availableColors.Count)]);
}
return code;
}
public (int blackPins, int whitePins, List<int>
correctPositions) CheckAttempt(List<string> attempt)
{
Attempts++;
var blackPins = 0;
var whitePins = 0;
var correctPositions = new
List<int>();
var codeCopy = new
List<string>(Code);
var attemptCopy = new
List<string>(attempt);
// Check for black pins (correct color and position)
for (int i = 0; i < codeCopy.Count; i++)
{
if (attemptCopy[i] == codeCopy[i])
{
blackPins++;
correctPositions.Add(i); // Speichere die korrekte
Position
codeCopy[i] = null; // Mark as matched
attemptCopy[i] = null; // Mark as matched
}
}
// Check for white pins (correct color, wrong
position)
for (int i = 0; i <
attemptCopy.Count; i++)
{
if (attemptCopy[i] != null && codeCopy.Contains(attemptCopy[i]))
{
whitePins++;
codeCopy[codeCopy.IndexOf(attemptCopy[i])] = null; //
Mark as matched
}
}
if (blackPins == Code.Count)
// Code.Count sollte 4 sein, da es 4 Positionen gibt
{
IsGameOver = true;
}
return (blackPins, whitePins,
correctPositions);
}
}
}
Zu guter letzt folgt das User Interface Form1.cs:
Die MainForm
-Klasse ist das Herzstück der Benutzeroberfläche für
das Mastermind-Spiel. Hier werden die verschiedenen UI-Komponenten verwaltet und
die Logik des Spiels mit der Benutzeroberfläche verknüpft.
Sie ermöglicht es
dem Spieler, Farben auszuwählen, Versuche zu machen, und zeigt die Ergebnisse
dieser Versuche in einer klaren und benutzerfreundlichen Weise an. Die Klasse
sorgt auch dafür, dass das Spiel korrekt zurückgesetzt wird, wenn der Spieler
ein neues Spiel starten möchte.
MainForm
-KlasseFelder und Konstruktor
game
: Dieses Feld speichert eine
Instanz des MastermindGame
, die die eigentliche
Spielmechanik enthält.MainForm()
: Der Konstruktor der
MainForm
initialisiert die Benutzeroberfläche, konfiguriert die
ComboBox-Elemente und startet ein neues Spiel.InitializeComboBoxes
-Methode
colorOptions
: Ein Dictionary, das
Farbnamen (string
) den tatsächlichen
Color
-Objekten
zuordnet.InitializeComboBox
: Eine Hilfsmethode,
die jede ComboBox so einrichtet, dass sie benutzerdefinierte Zeichnungen
unterstützt und die Farbnamen als Auswahloptionen hinzufügt.InitializeComboBox
-Methode
DrawMode.OwnerDrawFixed
: Setzt die
ComboBox so, dass sie benutzerdefinierte Zeichnungen für die Elemente
unterstützt.
DrawItem
-Methode wird verwendet, um die Farbbalken
und Farbnamen in der ComboBox anzuzeigen.StartNewGame
-Methode
MastermindGame
erstellt und die Benutzeroberfläche
zurücksetzt.Event-Handler für
btnNewGame
und
btnCheckAttempt
btnNewGame_Click
: Startet ein neues
Spiel, wenn der "Neues Spiel"-Button geklickt wird.btnCheckAttempt_Click
: Überprüft den
aktuellen Versuch des Spielers und zeigt das Ergebnis an. Wenn das Spiel
vorbei ist, wird eine Nachricht angezeigt.UpdateCorrectPositionsLabel
-Methode
GetAttemptFromUI
-Methode
DisplayResult
-Methode
ResetUI
-Methode
using System;
using
System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using Mastermind.Core;
namespace
Mastermind.UI
{
public partial class MainForm : Form
{
private MastermindGame game;
public MainForm()
{
InitializeComponent();
InitializeComboBoxes();
StartNewGame();
}
private void
InitializeComboBoxes()
{
// Farbnamen und deren Zuordnung zu tatsächlichen
Farben
var colorOptions = new
Dictionary<string, Color>
{
{ "Red", Color.Red },
{ "Green", Color.Green
},
{ "Blue", Color.Blue },
{ "Yellow", Color.Yellow },
{ "White",
Color.White },
{ "Black", Color.Black }
};
// Jede ComboBox
initialisieren
InitializeComboBox(comboBox1,
colorOptions);
InitializeComboBox(comboBox2,
colorOptions);
InitializeComboBox(comboBox3,
colorOptions);
InitializeComboBox(comboBox4,
colorOptions);
}
private void
InitializeComboBox(System.Windows.Forms.ComboBox comboBox, Dictionary<string,
Color> colorOptions)
{
comboBox.Items.Clear();
// Löscht alle vorhandenen Items in der ComboBox
comboBox.DrawMode = DrawMode.OwnerDrawFixed;
comboBox.DropDownStyle = ComboBoxStyle.DropDownList;
// Füge die Farbnamen als Items hinzu
foreach (var colorOption in colorOptions)
{
comboBox.Items.Add(colorOption.Key);
}
comboBox.DrawItem += (sender, e) =>
{
e.DrawBackground();
if (e.Index >= 0)
{
var selectedColorName =
comboBox.Items[e.Index].ToString();
var selectedColor = colorOptions[selectedColorName];
// Rechteck für den Farbbalken
Rectangle rectangle = new Rectangle(2, e.Bounds.Top + 2, e.Bounds.Height - 4,
e.Bounds.Height - 4);
e.Graphics.FillRectangle(new SolidBrush(selectedColor), rectangle);
// Farbnamen daneben zeichnen
e.Graphics.DrawString(selectedColorName, e.Font, Brushes.Black, e.Bounds.Height,
e.Bounds.Top + 2);
}
e.DrawFocusRectangle();
};
}
private void ComboBox_DrawItem(object sender, DrawItemEventArgs e)
{
var comboBox = sender as ComboBox;
if (comboBox == null ||
e.Index < 0) return;
e.DrawBackground();
// Holen Sie sich die Farbauswahl und den zugehörigen
Namen
var selectedColorName =
comboBox.Items[e.Index].ToString();
var selectedColor =
Color.FromName(selectedColorName); // Konvertiere den
Farbnamen in eine tatsächliche Farbe
// Rechteck für den Farbbalken
Rectangle rectangle = new Rectangle(2, e.Bounds.Top + 2, e.Bounds.Height - 4,
e.Bounds.Height - 4);
e.Graphics.FillRectangle(new
SolidBrush(selectedColor), rectangle);
// Farbnamen daneben zeichnen
e.Graphics.DrawString(selectedColorName, e.Font, Brushes.Black, e.Bounds.Height,
e.Bounds.Top + 2);
e.DrawFocusRectangle();
}
private void StartNewGame()
{
game =
new MastermindGame();
ResetUI();
}
private void btnNewGame_Click(object sender, EventArgs e)
{
StartNewGame();
}
private void
btnCheckAttempt_Click(object sender, EventArgs e)
{
var attempt = GetAttemptFromUI();
if (attempt == null)
return; // Abbrechen, wenn ein Fehler aufgetreten ist
var (correctPositionsCount, correctColors,
correctPositions) = game.CheckAttempt(attempt);
DisplayResult(correctPositionsCount, correctColors, attempt);
UpdateCorrectPositionsLabel(correctPositions);
if
(game.IsGameOver)
{
MessageBox.Show("Game Over");
}
}
private void UpdateCorrectPositionsLabel(List<int> correctPositions)
{
if (correctPositions.Count > 0)
{
// Erhöhe jede Position um 1
var adjustedPositions = correctPositions.Select(pos => pos + 1).ToList();
labelCorrectPositions.Text = $"Korrekte Positionen: {string.Join(", ",
adjustedPositions)}";
}
else
{
labelCorrectPositions.Text = "Korrekte
Positionen: Keine";
}
}
private
List<string> GetAttemptFromUI()
{
List<string>
attempt = new List<string>
{
comboBox1.SelectedItem?.ToString(),
comboBox2.SelectedItem?.ToString(),
comboBox3.SelectedItem?.ToString(),
comboBox4.SelectedItem?.ToString()
};
// Überprüfen, ob alle ComboBoxes eine Auswahl haben
if (attempt.Any(item => string.IsNullOrEmpty(item)))
{
MessageBox.Show("Bitte wähle eine Farbe in jeder ComboBox aus.", "Fehler",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return null; // Abbrechen, wenn eine ComboBox leer ist
}
return attempt;
}
private
void DisplayResult(int correctPositions, int correctColors, List<string>
attempt)
{
// Versuch
als Text formatieren
string
attemptText = string.Join(", ", attempt);
// Formatieren der Ausgabe wie im echten Spiel: Schwarze und weiße Pins
string resultText = $"{attemptText} - Schwarze Pins: {correctPositions}, Weiße
Pins: {correctColors}";
//
Füge den Text in die ListBox ein
listBoxAttempts.Items.Add(resultText);
if
(correctPositions == 4)
{
MessageBox.Show("Herzlichen Glückwunsch! Du hast den Code geknackt!",
"Gewonnen", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private void ResetUI()
{
// Setze die Labels für korrekte Positionen und Farben
auf 0 zurück
labelCorrectPositions.Text = "Richtige Positionen: Keine";
// Wenn Komboboxen verwendet werden:
comboBox1.SelectedIndex = -1;
comboBox2.SelectedIndex =
-1;
comboBox3.SelectedIndex = -1;
comboBox4.SelectedIndex = -1;
// Liste der Versuche
leeren
listBoxAttempts.Items.Clear();
}
}
}
Zur Kontrolle die implizit erzeugte Funktion
InitializeComponent():
private void
InitializeComponent()
{
this.label1 = new System.Windows.Forms.Label();
this.comboBox1 = new System.Windows.Forms.ComboBox();
this.comboBox2 = new
System.Windows.Forms.ComboBox();
this.comboBox3 = new
System.Windows.Forms.ComboBox();
this.comboBox4 = new
System.Windows.Forms.ComboBox();
this.btnCheckAttempt = new
System.Windows.Forms.Button();
this.listBoxAttempts = new
System.Windows.Forms.ListBox();
this.lblStatus = new
System.Windows.Forms.Label();
this.btnNewGame = new
System.Windows.Forms.Button();
this.labelCorrectPositions = new
System.Windows.Forms.Label();
this.SuspendLayout();
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 20);
this.label1.Name =
"label1";
this.label1.Size = new System.Drawing.Size(129, 16);
this.label1.TabIndex = 0;
this.label1.Text = "Wähle deine Farben";
//
// comboBox1
//
this.comboBox1.DrawMode =
System.Windows.Forms.DrawMode.OwnerDrawFixed;
this.comboBox1.FormattingEnabled = true;
this.comboBox1.Items.AddRange(new
object[] {
"Red",
"Blue",
"Green",
"Yellow",
"White",
"Black"});
this.comboBox1.Location = new System.Drawing.Point(20, 76);
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new
System.Drawing.Size(121, 23);
this.comboBox1.TabIndex = 1;
this.comboBox1.DrawItem += new
System.Windows.Forms.DrawItemEventHandler(this.ComboBox_DrawItem);
//
// comboBox2
//
this.comboBox2.DrawMode =
System.Windows.Forms.DrawMode.OwnerDrawFixed;
this.comboBox2.FormattingEnabled = true;
this.comboBox2.Items.AddRange(new
object[] {
"Red",
"Blue",
"Green",
"Yellow",
"White",
"Black"});
this.comboBox2.Location = new System.Drawing.Point(170, 76);
this.comboBox2.Name = "comboBox2";
this.comboBox2.Size = new
System.Drawing.Size(121, 23);
this.comboBox2.TabIndex = 2;
this.comboBox2.DrawItem += new
System.Windows.Forms.DrawItemEventHandler(this.ComboBox_DrawItem);
//
// comboBox3
//
this.comboBox3.DrawMode =
System.Windows.Forms.DrawMode.OwnerDrawFixed;
this.comboBox3.FormattingEnabled = true;
this.comboBox3.Items.AddRange(new
object[] {
"Red",
"Blue",
"Green",
"Yellow",
"White",
"Black"});
this.comboBox3.Location = new System.Drawing.Point(317, 76);
this.comboBox3.Name = "comboBox3";
this.comboBox3.Size = new
System.Drawing.Size(121, 23);
this.comboBox3.TabIndex = 3;
this.comboBox3.DrawItem += new
System.Windows.Forms.DrawItemEventHandler(this.ComboBox_DrawItem);
//
// comboBox4
//
this.comboBox4.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawFixed;
this.comboBox4.FormattingEnabled = true;
this.comboBox4.Items.AddRange(new
object[] {
"Red",
"Blue",
"Green",
"Yellow",
"White",
"Black"});
this.comboBox4.Location = new System.Drawing.Point(466, 76);
this.comboBox4.Name = "comboBox4";
this.comboBox4.Size = new
System.Drawing.Size(121, 23);
this.comboBox4.TabIndex = 4;
this.comboBox4.DrawItem += new
System.Windows.Forms.DrawItemEventHandler(this.ComboBox_DrawItem);
//
// btnCheckAttempt
//
this.btnCheckAttempt.Location = new
System.Drawing.Point(637, 67);
this.btnCheckAttempt.Name = "btnCheckAttempt";
this.btnCheckAttempt.Size = new System.Drawing.Size(139, 33);
this.btnCheckAttempt.TabIndex = 5;
this.btnCheckAttempt.Text = "Überprüfen";
this.btnCheckAttempt.UseVisualStyleBackColor = true;
this.btnCheckAttempt.Click += new
System.EventHandler(this.btnCheckAttempt_Click);
//
// listBoxAttempts
//
this.listBoxAttempts.FormattingEnabled = true;
this.listBoxAttempts.ItemHeight = 16;
this.listBoxAttempts.Location = new
System.Drawing.Point(15, 169);
this.listBoxAttempts.Name = "listBoxAttempts";
this.listBoxAttempts.Size = new System.Drawing.Size(418, 404);
this.listBoxAttempts.TabIndex = 6;
//
// lblStatus
//
this.lblStatus.AutoSize = true;
this.lblStatus.Location = new System.Drawing.Point(15, 815);
this.lblStatus.Name = "lblStatus";
this.lblStatus.Size = new
System.Drawing.Size(0, 16);
this.lblStatus.TabIndex = 7;
//
// btnNewGame
//
this.btnNewGame.Location = new
System.Drawing.Point(637, 107);
this.btnNewGame.Name = "btnNewGame";
this.btnNewGame.Size = new System.Drawing.Size(139, 35);
this.btnNewGame.TabIndex = 8;
this.btnNewGame.Text = "Neues Spiel";
this.btnNewGame.UseVisualStyleBackColor = true;
this.btnNewGame.Click += new
System.EventHandler(this.btnNewGame_Click);
//
//
labelCorrectPositions
//
this.labelCorrectPositions.AutoSize = true;
this.labelCorrectPositions.Location = new System.Drawing.Point(18, 616);
this.labelCorrectPositions.Name = "labelCorrectPositions";
this.labelCorrectPositions.Size = new System.Drawing.Size(0, 16);
this.labelCorrectPositions.TabIndex = 9;
//
// MainForm
//
this.AutoScaleDimensions = new
System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode =
System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new
System.Drawing.Size(800, 726);
this.Controls.Add(this.labelCorrectPositions);
this.Controls.Add(this.btnNewGame);
this.Controls.Add(this.lblStatus);
this.Controls.Add(this.listBoxAttempts);
this.Controls.Add(this.btnCheckAttempt);
this.Controls.Add(this.comboBox4);
this.Controls.Add(this.comboBox3);
this.Controls.Add(this.comboBox2);
this.Controls.Add(this.comboBox1);
this.Controls.Add(this.label1);
this.Name = "MainForm";
this.Text = "Mastermind";
this.ResumeLayout(false);
this.PerformLayout();
}
Den Code, die Projektdateien und die exe-Datei findet man hier zum Download.
Nun basteln wir ein eigenes Spiel. Wir nennen es Pixel Maze Challenge. Eine Spielfigur (rotes Quadrat) bewegt sich in einem Feld und soll alle Schätze (gelbe Quadrate) in vorgegebener Zeit einsammeln. Erschwert wird die Aktion durch bewegliche Hindernisse (schwarze Quadrate), also ein Action Game auf Zeit.
Da es sich in dieser Form um ein recht einfaches Spiel handelt, belassen wir den Code in einem Modul.
Dieser Code erstellt ein einfaches Spiel, in dem der Spieler eine Spielfigur durch ein Raster bewegen muss, um Punkte zu sammeln und Hindernissen auszuweichen. Das Spiel verwendet Windows Forms als Benutzeroberfläche und ist in C# geschrieben.
Deklaration von Variablen und Initialisierung:
int
,
der den aktuellen Punktestand des Spielers speichert.int
,
der die verbleibende Spielzeit in Sekunden speichert.int
,
der die Bewegungsgeschwindigkeit der Spielfigur angibt.Timer
-Objekt,
das die Bewegung der Hindernisse steuert.
Random
-Objekt,
das für die zufällige Platzierung von Punkten und Bewegung von
Hindernissen verwendet wird.Formular-Konstruktor
Form1()
:
KeyPreview
:
Diese Eigenschaft wird auf true
gesetzt, damit die Form Tastatureingaben empfangen kann, selbst wenn ein
anderes Steuerelement den Fokus hat.obstacleTimer
:
Wird mit einem Intervall von 500 ms initialisiert und startet die
Bewegung der Hindernisse alle 500 Millisekunden.Formular-Ladeereignis
Form1_Load
:
StartGame()
-Methode
aufgerufen wird.StartGame
Methode:
score
)
wird auf 0 und die verbleibende Zeit (timeLeft
)
auf 45 Sekunden gesetzt.InitializeObstacles
Methode:
InitializePoints
Methode:
gameTimer_Tick
Ereignishandler:
ObstacleTimer_Tick
Ereignishandler:
MoveObstacles
Methode:
CheckCollision
Methode:
restartButton_Click
Ereignishandler:
gamePanel_PreviewKeyDown
Ereignishandler:
gamePanel_PreviewKeyDown_1
Ereignishandler:
Hier ist der gesamte Code:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace
WindowsFormsApp_PixelMazeChallenge
{
public partial class Form1 :
Form
{
private int score;
private int timeLeft;
private int playerSpeed = 10;
//
Geschwindigkeit der Spielfigur
private Timer
obstacleTimer; // Timer für Hindernisbewegung
private
readonly Random random = new Random(); // Zufallszahlengenerator für
Hindernisbewegung
public Form1()
{
InitializeComponent();
this.KeyPreview = true;
//
Aktiviert das KeyDown-Ereignis für das Formular
this.Focus();
gamePanel.PreviewKeyDown += new
PreviewKeyDownEventHandler(this.gamePanel_PreviewKeyDown);
gamePanel.TabStop = true; // Stellt sicher, dass das Panel den Fokus bekommen
kann
// Timer für Hindernisbewegung initialisieren
obstacleTimer = new Timer();
obstacleTimer.Interval = 500;
// Hindernisse alle 500ms bewegen
obstacleTimer.Tick +=
ObstacleTimer_Tick;
}
private void Form1_Load(object
sender, EventArgs e)
{
this.Focus();
StartGame();
}
private void StartGame()
{
score = 0;
timeLeft = 45; // Spielzeit in Sekunden
scoreLabel.Text = "Score: " + score;
timeLabel.Text =
"Time: " + timeLeft;
// Setzt die Spielfigur auf die
Startposition
playerCharacter.Location = new
System.Drawing.Point(0, 0);
gameTimer.Interval = 1000;
// Beispiel: Jede Sekunde
gameTimer.Start();
obstacleTimer.Start(); // Hindernis-Timer starten
//
Fokus auf das gamePanel setzen
gamePanel.Focus();
gamePanel.Select();
InitializeObstacles();
InitializePoints();
}
private void InitializeObstacles()
{
// Hier Hindernisse auf dem Spielfeld positionieren
obstacle1.Location = new Point(100, 100);
obstacle2.Location = new Point(200, 160);
obstacle3.Location = new Point(160, 100);
// Weitere
Hindernisse hinzufügen oder bewegen
}
private void
InitializePoints()
{
// Größe des Rasters (z.B.
20 Pixel)
int gridSize = 20;
//
Eine Liste zur Überprüfung der bereits belegten Positionen
List<Point> occupiedPositions = new List<Point>();
//
Eine Methode, um eine zufällige Position zu generieren, die nicht belegt ist
Point GenerateUniquePosition()
{
Point newPoint;
do
{
newPoint = new Point(
random.Next(0, gamePanel.Width / gridSize) * gridSize,
random.Next(0, gamePanel.Height / gridSize) * gridSize);
} while (occupiedPositions.Contains(newPoint));
occupiedPositions.Add(newPoint);
return
newPoint;
}
// Punkt 1 sichtbar
machen und auf einem zufälligen, einzigartigen Rasterplatz platzieren
point1.Visible = true;
point1.Location =
GenerateUniquePosition();
// Punkt 2 sichtbar machen
und auf einem anderen zufälligen, einzigartigen Rasterplatz platzieren
point2.Visible = true;
point2.Location =
GenerateUniquePosition();
// Punkt 3 sichtbar machen
und auf einem anderen zufälligen, einzigartigen Rasterplatz platzieren
point3.Visible = true;
point3.Location =
GenerateUniquePosition();
// Punkt 4 sichtbar machen
und auf einem anderen zufälligen, einzigartigen Rasterplatz platzieren
point4.Visible = true;
point4.Location =
GenerateUniquePosition();
// Punkt 5 sichtbar machen
und auf einem anderen zufälligen, einzigartigen Rasterplatz platzieren
point5.Visible = true;
point5.Location =
GenerateUniquePosition();
// Punkt 6 sichtbar machen
und auf einem anderen zufälligen, einzigartigen Rasterplatz platzieren
point6.Visible = true;
point6.Location =
GenerateUniquePosition();
// Punkt 7 sichtbar machen
und auf einem anderen zufälligen, einzigartigen Rasterplatz platzieren
point7.Visible = true;
point7.Location =
GenerateUniquePosition();
// Punkt 8 sichtbar machen
und auf einem anderen zufälligen, einzigartigen Rasterplatz platzieren
point8.Visible = true;
point8.Location =
GenerateUniquePosition();
}
private void
gameTimer_Tick(object sender, EventArgs e)
{
timeLeft--;
timeLabel.Text = "Time: " + timeLeft;
if (timeLeft <= 0)
{
gameTimer.Stop();
obstacleTimer.Stop();
MessageBox.Show("Game Over! Your score is: " + score);
}
}
private void ObstacleTimer_Tick(object sender, EventArgs e)
{
MoveObstacles();
}
private void
MoveObstacles()
{
// Beispiel für zufällige
Bewegung von Hindernis 1
int moveX1 = random.Next(-10,
11); // Zufällige Bewegung zwischen -10 und 10 für Hindernis 1
int moveY1 = random.Next(-10, 11);
// Hindernis 1
bewegen
obstacle1.Left += moveX1;
obstacle1.Top += moveY1;
// Überprüfen, ob Hindernis 1
innerhalb der Spielfeldgrenzen bleibt
if (obstacle1.Left <
0 || obstacle1.Right > gamePanel.Width)
{
obstacle1.Left -= moveX1; // Bewegung rückgängig machen
}
if (obstacle1.Top < 0 || obstacle1.Bottom > gamePanel.Height)
{
obstacle1.Top -= moveY1;
// Bewegung
rückgängig machen
}
// Beispiel
für zufällige Bewegung von Hindernis 2
int moveX2 =
random.Next(-10, 11); // Zufällige Bewegung zwischen -10 und 10 für Hindernis 2
int moveY2 = random.Next(-10, 11);
// Hindernis 2
bewegen
obstacle2.Left += moveX2;
obstacle2.Top += moveY2;
// Überprüfen, ob Hindernis 2
innerhalb der Spielfeldgrenzen bleibt
if (obstacle2.Left <
0 || obstacle2.Right > gamePanel.Width)
{
obstacle2.Left -= moveX2; // Bewegung rückgängig machen
}
if (obstacle2.Top < 0 || obstacle2.Bottom > gamePanel.Height)
{
obstacle2.Top -= moveY2;
// Bewegung
rückgängig machen
}
// Beispiel
für zufällige Bewegung von Hindernis 3
int moveX3 =
random.Next(-10, 11); // Zufällige Bewegung zwischen -10 und 10 für Hindernis 3
int moveY3 = random.Next(-10, 11);
// Hindernis 3
bewegen
obstacle3.Left += moveX3;
obstacle3.Top += moveY3;
// Überprüfen, ob Hindernis 3
innerhalb der Spielfeldgrenzen bleibt
if (obstacle3.Left <
0 || obstacle2.Right > gamePanel.Width)
{
obstacle3.Left -= moveX3; // Bewegung rückgängig machen
}
if (obstacle3.Top < 0 || obstacle3.Bottom > gamePanel.Height)
{
obstacle3.Top -= moveY3;
// Bewegung
rückgängig machen
}
//
Kollisionserkennung zwischen Spieler und Hindernissen
CheckCollision();
}
private void CheckCollision()
{
// Kollision mit Hindernissen
if
(playerCharacter.Bounds.IntersectsWith(obstacle1.Bounds) ||
playerCharacter.Bounds.IntersectsWith(obstacle2.Bounds) ||
playerCharacter.Bounds.IntersectsWith(obstacle3.Bounds))
{
gameTimer.Stop();
obstacleTimer.Stop();
MessageBox.Show("Game Over! You hit an obstacle.");
}
// Überprüfen, ob der Spieler Punkt 1 gesammelt hat
if (point1.Visible && playerCharacter.Bounds.IntersectsWith(point1.Bounds))
{
score++;
scoreLabel.Text = "Score: " + score;
point1.Visible = false; // Punkt entfernen
}
// Überprüfen, ob der Spieler Punkt 2 gesammelt hat
if
(point2.Visible && playerCharacter.Bounds.IntersectsWith(point2.Bounds))
{
score++;
scoreLabel.Text = "Score: " + score;
point2.Visible = false; // Punkt entfernen
}
// Überprüfen, ob der Spieler Punkt 3 gesammelt hat
if
(point3.Visible && playerCharacter.Bounds.IntersectsWith(point3.Bounds))
{
score++;
scoreLabel.Text = "Score: " + score;
point3.Visible = false; // Punkt entfernen
}
// Überprüfen, ob der Spieler Punkt 4 gesammelt hat
if
(point4.Visible && playerCharacter.Bounds.IntersectsWith(point4.Bounds))
{
score++;
scoreLabel.Text = "Score: " + score;
point4.Visible = false; // Punkt entfernen
}
// Überprüfen, ob der Spieler Punkt 5 gesammelt hat
if
(point5.Visible && playerCharacter.Bounds.IntersectsWith(point5.Bounds))
{
score++;
scoreLabel.Text = "Score: " + score;
point5.Visible = false; // Punkt entfernen
}
// Überprüfen, ob der Spieler Punkt 6 gesammelt hat
if
(point6.Visible && playerCharacter.Bounds.IntersectsWith(point6.Bounds))
{
score++;
scoreLabel.Text = "Score: " + score;
point6.Visible = false; // Punkt entfernen
}
// Überprüfen, ob der Spieler Punkt 7 gesammelt hat
if
(point7.Visible && playerCharacter.Bounds.IntersectsWith(point7.Bounds))
{
score++;
scoreLabel.Text = "Score: " + score;
point7.Visible = false; // Punkt entfernen
}
// Überprüfen, ob der Spieler Punkt 8 gesammelt hat
if
(point8.Visible && playerCharacter.Bounds.IntersectsWith(point8.Bounds))
{
score++;
scoreLabel.Text = "Score: " + score;
point8.Visible = false; // Punkt entfernen
}
// Überprüfen, ob alle Punkte gesammelt wurden
if (score
== 8)
{
gameTimer.Stop();
obstacleTimer.Stop();
MessageBox.Show("Congratulations! You've collected all the points!");
}
}
private void restartButton_Click(object sender,
EventArgs e)
{
StartGame();
this.Focus(); // Fokus zurück auf das Formular setzen
}
private void gamePanel_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Up:
if
(playerCharacter.Top > 0)
playerCharacter.Top -= playerSpeed;
break;
case Keys.Down:
if
(playerCharacter.Bottom + playerSpeed < gamePanel.Height)
playerCharacter.Top += playerSpeed;
break;
case Keys.Left:
if
(playerCharacter.Left > 0)
playerCharacter.Left -= playerSpeed;
break;
case Keys.Right:
if
(playerCharacter.Right + playerSpeed < gamePanel.Width)
playerCharacter.Left += playerSpeed;
break;
}
}
private void gamePanel_PreviewKeyDown_1(object
sender, PreviewKeyDownEventArgs e)
{
// Markiert
bestimmte Schlüssel als Eingabeaufforderung für das Panel
if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down || e.KeyCode == Keys.Left ||
e.KeyCode == Keys.Right)
{
e.IsInputKey = true;
}
}
}
}
Der Code für "Pixel Maze Challenge" bietet bereits in dieser Basisversion ein einfaches und gleichzeitig spannendes Spiel, das den Spieler herausfordert, Punkte zu sammeln und Hindernissen auszuweichen. Es verwendet grundlegende Programmierkonzepte wie Konditionen, Schleifen, Zufallszahlengenerierung, Timer und Ereignishandler, um das Gameplay zu steuern und sicherzustellen, dass das Spiel dynamisch und interaktiv ist. Das Verständnis dieses Codes kann als Grundlage für das Erstellen eigener Spiele und interaktiver Anwendungen dienen.
Hier ist der Code zum Download.
Das Spiel Snake ist ein Klassiker. Man kann dabie eine
Menge lernen und experimentieren.
Drei Punkte machen wir dieses Mal anders:
Wir verwenden nicht den Designer, sondern erstellen alles im Code.
Wir
verwenden Double Buffering zum Zeichnen, um die Anzeige ruhiger zu gestalten.
Wir legen eine Pause-Taste auf 'P', da das Spiel actiongeladen ist.
Zunächst ein Bild:
Ich habe einen dunkelgrauen Hintergrund gewählt, da der
Kontrast bei Schwarz ziemlich stark ist.
Sie kennen es: Die Schlange
wird mit jeder Futteraufnahme länger, und sie bewegt sich schneller.
Los
geht's. Wir beginnen mit der Klasse Game, die wir als Modul Game.cs hinzufügen.
Dieser Code implementiert die Kernlogik eines einfachen
Snake-Spiels. Er verfolgt die Position der Schlange und des Futters, handhabt
die Bewegung und überprüft Kollisionen.
Bei jeder Bewegung der Schlange wird
überprüft, ob sie das Spielfeld verlässt, sich selbst trifft oder das Futter
erreicht, um das Spiel entsprechend zu aktualisieren.
Direction
:
Ein enum
,
das die möglichen Bewegungsrichtungen der Schlange definiert:
Up
,
Down
,
Left
,
Right
.
SnakePart
:
Eine Klasse, die die Position eines Teils der Schlange auf dem Spielfeld
speichert. Sie hat zwei Eigenschaften,
X
und
Y
,
die die Position in einem 2D-Raster darstellen.
Game
-KlasseDie Game
-Klasse
enthält die Hauptlogik für das Snake-Spiel:
Eigenschaften:
Snake
:
Eine Liste von SnakePart
-Objekten,
die die Schlange repräsentieren.Food
:
Ein SnakePart
-Objekt,
das die Position des Futters auf dem Spielfeld darstellt.Score
:
Ein int
,
der die aktuelle Punktzahl des Spielers speichert.SnakeDirection
:
Eine Direction
,
die die aktuelle Bewegungsrichtung der Schlange angibt.GameOver
:
Ein bool
,
der angibt, ob das Spiel vorbei ist.Konstruktor:
Methoden:
InitializeGame
:
Setzt das Spiel zurück, indem es die Schlange in ihre Ausgangsposition
bringt, die Richtung festlegt, die Punktzahl auf null setzt und das
erste Futter generiert.
Move
:
Bewegt die Schlange in die aktuelle Richtung. Prüft, ob die Schlange das
Futter erreicht oder das Spielfeld verlassen hat oder sich selbst
getroffen hat.
Falls ja, setzt sie
GameOver
auf true
.
Wenn die Schlange das Futter erreicht, erhöht sie die Punktzahl und
generiert neues Futter.
Andernfalls bewegt sich die Schlange weiter
und das letzte Segment wird entfernt.
GenerateFood
:
Generiert zufällig eine neue Position für das Futter, die nicht mit der
Position der Schlange kollidiert.
using System;
using
System.Collections.Generic;
using System.Linq;
namespace
WindowsFormsApp_Snake
{
public enum Direction { Up, Down, Left,
Right }
public class SnakePart
{
public int X { get; set; }
public int Y { get; set; }
}
public class Game
{
public
List<SnakePart> Snake { get; private set; }
public
SnakePart Food { get; private set; }
public int Score {
get; private set; }
public Direction SnakeDirection { get;
set; }
public bool GameOver { get; private set; }
private int _width;
private int _height;
private Random _random;
public Game(int width, int
height)
{
_width =
width;
_height = height;
_random = new Random();
Snake = new
List<SnakePart>();
InitializeGame();
}
private void InitializeGame()
{
Snake.Clear();
Snake.Add(new SnakePart() { X = 10, Y = 10 });
SnakeDirection = Direction.Right;
Score = 0;
GameOver = false;
GenerateFood();
}
public void Move()
{
if (GameOver)
return;
SnakePart head = new SnakePart()
{
X = Snake[0].X,
Y = Snake[0].Y
};
switch (SnakeDirection)
{
case Direction.Up: head.Y--;
break;
case Direction.Down:
head.Y++; break;
case
Direction.Left: head.X--; break;
case Direction.Right: head.X++; break;
}
if (head.X < 0 || head.X >= _width || head.Y
< 0 || head.Y >= _height || Snake.Any(part => part.X == head.X && part.Y ==
head.Y))
{
GameOver = true;
return;
}
if (head.X == Food.X && head.Y ==
Food.Y)
{
Score++;
GenerateFood();
}
else
{
Snake.RemoveAt(Snake.Count - 1);
}
Snake.Insert(0, head);
}
private
void GenerateFood()
{
int x, y;
do
{
x = _random.Next(0, _width);
y = _random.Next(0, _height);
} while
(Snake.Any(part => part.X == x && part.Y == y));
Food = new SnakePart() { X = x, Y = y };
}
}
}
MainForm
-KlasseDiese
MainForm
-Klasse verwaltet die Anzeige
des Spiels und die Benutzerinteraktionen.
Sie nutzt Double Buffering, um das
Flackern zu reduzieren, und bietet Funktionen zum Pausieren und Anpassen der
Spielgeschwindigkeit.
Die Schlange und das Futter werden als abgerundete
Quadrate gezeichnet, und das Spiel wird über die Tastatur (Pfeiltasten)
gesteuert.
Sie findet sich im Modul Form1.cs.
Felder:
lastScoreForSpeedIncrease
:
Ein int
,
der den Punktestand speichert, bei dem zuletzt die Geschwindigkeit
erhöht wurde.gameTimer
:
Ein Timer
,
der das Spiel in festen Intervallen aktualisiert.game
:
Eine Instanz der Game
-Klasse,
die die Logik des Spiels enthält.isPaused
:
Ein bool
,
der verfolgt, ob das Spiel pausiert ist.Konstruktor:
game
)
und startet den gameTimer
.UpdateScreen
:
Invalidate()
,
um die Form neu zu zeichnen.OnPaint
:
CreateRoundedRectangle
,
um die Schlange und das Futter als abgerundete Rechtecke zu zeichnen.OnKeyDown
:
gameTimer_Tick
:
UpdateScreen
bei jedem Timer-Tick auf.CreateRoundedRectangle
:
GraphicsPath
für ein abgerundetes Rechteck, das für das Zeichnen der Schlange und des
Futters verwendet wird.
using System;
using System.Drawing;
using
System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace
WindowsFormsApp_Snake
{
public partial class MainForm : Form
{
private int lastScoreForSpeedIncrease = 0;
private Timer gameTimer;
private Game game;
private bool isPaused = false; // Variable zum Verfolgen des Pausenzustands
// Im Konstruktor der MainForm
public MainForm()
{
InitializeComponent();
this.Text = "Snake
Game - Score: 0";
this.ClientSize = new
Size(800, 800);
this.BackColor =
Color.DarkGray;
this.KeyPreview = true;
// Aktivieren von
Double Buffering
this.DoubleBuffered = true;
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.UserPaint, true);
// Spiel initialisieren
game = new Game(this.ClientSize.Width / 20, this.ClientSize.Height / 20);
gameTimer = new Timer()
{
Interval = 250
// Spielgeschwindigkeit (Millisekunden)
};
gameTimer.Tick += UpdateScreen;
gameTimer.Start();
}
private
void MainForm_Load(object sender, EventArgs e)
{
game = new Game(this.ClientSize.Width / 20, this.ClientSize.Height / 20);
}
private void UpdateScreen(object sender, EventArgs
e)
{
if (game.GameOver)
{
gameTimer.Stop();
MessageBox.Show("Game Over! Your score: " + game.Score);
return;
}
game.Move();
//
Aktualisieren des Fenstertitels mit dem aktuellen Punktestand und
Timer-Intervall
this.Text = $"Snake Game - Score: {game.Score} - Intervall: {gameTimer.Interval}
ms";
//
Dynamische Anpassung der Geschwindigkeit
if (game.Score > 0 && game.Score % 3 == 0 && game.Score !=
lastScoreForSpeedIncrease)
{
gameTimer.Interval = Math.Max(50, gameTimer.Interval - 10);
// Erhöht die Geschwindigkeit
//
Debug-Ausgabe von Score und aktuellem Intervall
System.Diagnostics.Debug.WriteLine("Score: " + game.Score + ", Aktuelles
Timer-Intervall: " + gameTimer.Interval);
// Aktualisieren des letzten Punktestands, bei dem die
Geschwindigkeit erhöht wurde
lastScoreForSpeedIncrease = game.Score;
}
Invalidate(); //
Erzwingt das Neuzeichnen der Form
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics canvas = e.Graphics;
int cornerRadius = 10; // Radius der Ecken für
abgerundete Quadrate
if (game !=
null)
{
Brush snakeColor = Brushes.Green;
Brush foodColor = Brushes.Red;
// Zeichnen der Schlange
foreach (var part in game.Snake)
{
Rectangle snakeRect = new Rectangle(part.X * 20, part.Y * 20, 20, 20);
using (GraphicsPath path = CreateRoundedRectangle(snakeRect, cornerRadius))
{
canvas.FillPath(snakeColor, path);
}
}
// Zeichnen des Futters nur, wenn es initialisiert
wird oder sich bewegt
if (game.Food != null)
{
Rectangle foodRect = new Rectangle(game.Food.X * 20, game.Food.Y * 20, 20, 20);
using (GraphicsPath path = CreateRoundedRectangle(foodRect, cornerRadius))
{
canvas.FillPath(foodColor, path);
}
}
}
}
protected override void
OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.KeyCode ==
Keys.P)
{
// Pausetaste "P" wurde gedrückt
if (isPaused)
{
// Spiel fortsetzen
gameTimer.Start();
isPaused = false;
}
else
{
// Spiel pausieren
gameTimer.Stop();
isPaused = true;
}
}
else if (!isPaused)
{
//
Bewegung der Schlange nur verarbeiten, wenn das Spiel nicht pausiert ist
switch (e.KeyCode)
{
case Keys.Up:
if (game.SnakeDirection != Direction.Down)
game.SnakeDirection = Direction.Up;
break;
case Keys.Down:
if (game.SnakeDirection != Direction.Up)
game.SnakeDirection = Direction.Down;
break;
case Keys.Left:
if (game.SnakeDirection != Direction.Right)
game.SnakeDirection = Direction.Left;
break;
case
Keys.Right:
if (game.SnakeDirection != Direction.Left)
game.SnakeDirection = Direction.Right;
break;
}
}
}
private void
gameTimer_Tick(object sender, EventArgs e)
{
UpdateScreen(sender, e);
}
private GraphicsPath CreateRoundedRectangle(Rectangle rect, int cornerRadius)
{
GraphicsPath path = new GraphicsPath();
path.StartFigure();
path.AddArc(rect.X, rect.Y, cornerRadius, cornerRadius, 180, 90);
path.AddArc(rect.Right - cornerRadius, rect.Y, cornerRadius, cornerRadius, 270,
90);
path.AddArc(rect.Right - cornerRadius,
rect.Bottom - cornerRadius, cornerRadius, cornerRadius, 0, 90);
path.AddArc(rect.X, rect.Bottom - cornerRadius, cornerRadius, cornerRadius, 90,
90);
path.CloseFigure();
return path;
}
}
}
Den Code für diese Basisversion findet man hier. Viel Spaß mit diesem immer hektischer werden Spiel.
Pong ist eines der ersten und bekanntesten Videospiele der Geschichte. 1972 von Atari entwickelt ist es ein einfaches Zwei-Spieler-Tischtennisspiel, das in einer minimalistischen, pixelbasierten Grafik umgesetzt wurde. Trotz seiner Einfachheit hat Pong die Grundlage für viele spätere Videospiele gelegt und bleibt bis heute ein beliebter Klassiker.
Das Spiel simuliert eine Tischtennispartie, bei der zwei Spieler versuchen, einen Ball mit Hilfe eines Schlägers (Paddle) hin und her zu schlagen. Jeder Spieler steuert ein Paddle, das sich vertikal auf dem Bildschirm bewegt. Das Ziel des Spiels ist es, den Ball so zu schlagen, dass der Gegner ihn nicht zurückspielen kann.
Spielregeln:
Das Programmieren von Pong ist ein Muss. Es hilft,
grundlegende Konzepte wie Kollisionsabfrage, Bewegung und einfache Spielphysik
zu verstehen. Aufgrund seiner einfachen Mechanik, seines minimalistischen
Designs und seines überschaubaren Codes eignet sich Pong gut für Einsteiger und
bietet dennoch Raum für Erweiterungen und Kreativität.
Beginnen wir mit dem Modul Form1.cs:
Der Code definiert das Verhalten und die
Benutzeroberfläche eines Pong-Spiels in einer Windows Forms-Anwendung. Es
initialisiert das Spiel, steuert die Eingaben des Benutzers und verwaltet das
Rendering des Spiels und des Fenstertitels. Die Verwendung eines Timers
ermöglicht ein reibungsloses Spiel, während die Verwendung von Ereignishandlern
eine reaktive Steuerung und Benutzeroberfläche bietet.
Felder und Variablen:
gameEngine
:
Eine Instanz der GameEngine
,
die die Logik und den Zustand des Spiels verwaltet.gameTimer
:
Ein Timer-Objekt, das verwendet wird, um das Spiel in regelmäßigen
Abständen zu aktualisieren (ca. 60 Frames pro Sekunde).isPaused
:
Ein boolescher Wert, der speichert, ob das Spiel pausiert ist oder
nicht.Konstruktor (GameForm()
):
DoubleBuffered
,
FormBorderStyle
,
MaximizeBox
,
StartPosition
und BackColor
,
um das Aussehen und Verhalten der Form zu steuern.
GameEngine
basierend auf der aktuellen Größe der Form.Paint
),
das Drücken einer Taste (KeyDown
),
und das Loslassen einer Taste (KeyUp
)
zu.
InitializeGame()
auf, um den Spiel-Timer zu starten.InitializeGame()
:
gameTimer
,
um das Spiel regelmäßig zu aktualisieren. Der Timer wird auf einen
Intervall von 16 Millisekunden eingestellt, was etwa 60 FPS entspricht.
isPaused
auf false
,
um sicherzustellen, dass das Spiel nicht im Pausenmodus beginnt.GameLoop()
:
gameTimer
ein "Tick"-Ereignis auslöst.isPaused
ist false
),
aktualisiert es die gameEngine
,
zeichnet das Spiel neu und aktualisiert den Fenstertitel.UpdateTitle()
:
OnPaint()
:
DrawTitle()
-Methode
auf, um den Titel anzuzeigen, und die
gameEngine.Draw()
-Methode,
um das Spiel zu zeichnen.DrawText()
:
OnKeyDown()
und OnKeyUp()
:
OnKeyDown()
:
Reagiert auf das Drücken von Tasten. Wenn die 'P'-Taste gedrückt wird,
wechselt das Spiel in den Pausenmodus. gameEngine
weitergeleitet, um das Spiel zu steuern.OnKeyUp()
:
Beendet die Aktion, wenn eine Taste losgelassen wird, und benachrichtigt
die gameEngine
.TogglePause()
:
UpdateTitle()
auf, um den Titel zu aktualisieren.
using System;
using System.Drawing;
using
System.Windows.Forms;
namespace WindowsFormsApp_Pong
{
public partial class GameForm : Form
{
private
GameEngine gameEngine;
private Timer gameTimer;
private bool isPaused;
public GameForm()
{
InitializeComponent();
this.DoubleBuffered = true; // Aktiviert Double
Buffering
this.FormBorderStyle =
FormBorderStyle.FixedSingle; // Begrenzung der
Fenstergröße
this.MaximizeBox = false;
// Deaktiviert Maximieren
this.StartPosition = FormStartPosition.CenterScreen;
this.BackColor = Color.Black;
// Initialisierung der Spiel-Engine
gameEngine = new GameEngine(this.ClientSize);
// Ereignis-Handler zuweisen
this.Paint += new PaintEventHandler(OnPaint);
this.KeyDown += new KeyEventHandler(OnKeyDown);
this.KeyUp += new KeyEventHandler(OnKeyUp);
InitializeGame();
}
private
void InitializeGame()
{
gameTimer = new Timer();
gameTimer.Interval =
16; // ca. 60 FPS (1000 / 60 = 16,67)
gameTimer.Tick += new EventHandler(GameLoop);
gameTimer.Start();
isPaused = false;
// Spiel startet nicht im Pausenmodus
}
private void GameLoop(object sender, EventArgs e)
{
if (!isPaused)
{
gameEngine.Update();
this.Invalidate(); // Erzwingt ein Neuzeichnen des
Formulars
UpdateTitle(); // Aktualisiert den Titel
}
}
private void UpdateTitle()
{
// Setzt den Titel
der Form. Man kann hier auch weitere Informationen ausgeben.
this.Text = "Pong";
}
private
void OnPaint(object sender, PaintEventArgs e)
{
// Zeichnen Sie eine Ausgabe (Punktestand) in
einer benutzerdefinierten Schriftart
DrawText(e.Graphics);
gameEngine.Draw(e.Graphics);
}
private void DrawText(Graphics g)
{
string text ="";
if
(gameEngine.GetPlayerScore() < 10)
text =
$"Player: {gameEngine.GetPlayerScore()}
AI: {gameEngine.GetAIScore()}";
if
(gameEngine.GetPlayerScore() >= 10)
text =
$"Player: {gameEngine.GetPlayerScore()}
AI: {gameEngine.GetAIScore()}";
if
(gameEngine.GetPlayerScore() >= 100)
text =
$"Player: {gameEngine.GetPlayerScore()}
AI: {gameEngine.GetAIScore()}";
Font font =
new Font("Consolas", 20, FontStyle.Bold); //
Benutzerdefinierte Schriftart
Brush
brush = Brushes.White;
PointF point = new
PointF(10, 10); // Startposition für den Text
g.DrawString(title, font, brush, point);
}
private void OnKeyDown(object sender, KeyEventArgs
e)
{
if (e.KeyCode ==
Keys.P)
{
TogglePause(); // Pausenzustand umschalten, wenn "P"
gedrückt wird
}
else
{
gameEngine.HandleKeyDown(e.KeyCode);
}
}
private void OnKeyUp(object sender, KeyEventArgs e)
{
gameEngine.HandleKeyUp(e.KeyCode);
}
private void TogglePause()
{
isPaused = !isPaused; // Pausenzustand umschalten
if (isPaused)
{
this.Text = "Pong - Spiel pausiert"; // Titel
aktualisieren, wenn pausiert
}
else
{
UpdateTitle(); // Titel aktualisieren, wenn
fortgesetzt
}
}
}
}
Weiter geht es mit der AI im Modul AI.cs.
Diese Klasse namens
AI
, die die
künstliche Intelligenz (KI) für das Paddle des Gegners in einem Pong-Spiel
implementiert, steuert die Bewegung des gegnerischen Paddles basierend auf der
Position des Balls, um ihn zurückzuspielen. Durch das ständige Aktualisieren der
Position des Paddles in der Update()
-Methode
basierend auf der Position des Balls versucht die KI, das Paddle so zu
positionieren, dass es den Ball zurückspielen kann. Die aktuelle Implementierung
ist sehr einfach und reagiert direkt auf die Position des Balls, kann aber
erweitert werden, um komplexere und realistischere Verhaltensweisen zu
simulieren. In der aktuellen Form kann der menschliche Spieler leicht gewinnen.
Felder der Klasse
AI
:
Paddle aiPaddle
:
Eine Instanz der Paddle
-Klasse,
die das gegnerische Paddle repräsentiert.Ball ball
:
Eine Instanz der Ball
-Klasse,
die den Ball im Spiel repräsentiert.int aiSpeed
:
Eine Ganzzahl, die die Geschwindigkeit des KI-Paddles festlegt. Konstruktor (AI(Paddle
aiPaddle, Ball ball)
):
AI
-Klasse,
indem die Referenzen auf das KI-Paddle und den Ball übergeben und
zugewiesen werden.Update()
-Methode:
ball.Position.Y <
aiPaddle.Position.Y
),
bewegt die KI das Paddle nach oben, indem sie
aiPaddle.MoveUp()
aufruft.ball.Position.Y >
aiPaddle.Position.Y + aiPaddle.Bounds.Height
),
bewegt die KI das Paddle nach unten, indem sie
aiPaddle.MoveDown()
aufruft.aiPaddle.Stop()
aufgerufen wird.namespace WindowsFormsApp_Pong
{
public
class AI
{
private Paddle aiPaddle;
private Ball ball;
private int aiSpeed = 3;
public AI(Paddle aiPaddle, Ball ball)
{
this.aiPaddle = aiPaddle;
this.ball = ball;
}
public void Update()
{
// AI folgt dem Ball
if (ball.Position.Y < aiPaddle.Position.Y)
{
aiPaddle.MoveUp();
}
else if (ball.Position.Y > aiPaddle.Position.Y + aiPaddle.Bounds.Height)
{
aiPaddle.MoveDown();
}
else
{
aiPaddle.Stop();
}
}
}
}
Nun zum Modul Ball.cs:
Die Ball
-Klasse
verwaltet die Bewegung und Darstellung des Balls im Pong-Spiel. Sie enthält die
Logik, um Kollisionen mit den Spielfeldrändern zu erkennen und entsprechend zu
reagieren. Durch die Methode Reset()
kann der Ball nach einem Punkt oder beim Start des Spiels wieder in die Mitte
des Spielfelds gesetzt werden. Die Verwendung einer statischen Instanz von
Random
sorgt dafür, dass die Richtung des Balls bei jedem Reset zufällig bestimmt wird.
Felder und Eigenschaften:
Position
:
Ein Point
-Objekt,
das die aktuelle Position des Balls im Spielfeld definiert.clientSize
:
Ein Size
-Objekt,
das die Größe des Spielfelds speichert und verwendet wird, um
Kollisionen mit den Spielfeldrändern zu erkennen.speedX
und speedY
:
Ganzzahlen, die die Geschwindigkeit des Balls in der horizontalen und
vertikalen Richtung bestimmen.Diameter
:
Der Durchmesser des Balls, standardmäßig auf 10 Pixel gesetzt.Bounds
:
Eine Eigenschaft, die ein
Rectangle
zurückgibt und den
Bereich des Balls im Spielfeld beschreibt, basierend auf seiner Position
und seinem Durchmesser.rand
:
Eine statische Instanz der Klasse
Random
,
die verwendet wird, um die Richtung des Balls zufällig zu
initialisieren.Konstruktor:
Ball(Size clientSize)
:
Initialisiert eine neue Instanz der
Ball
-Klasse.
Reset()
-Methode
in die Mitte des Spielfelds gesetzt und seine Geschwindigkeit zufällig
initialisiert.Methoden:
Update()
:
Aktualisiert die Position des Balls basierend auf seiner
Geschwindigkeit. BounceY()
),
sodass der Ball zurückprallt.Draw(Graphics g)
:
Zeichnet den Ball auf dem Bildschirm als gelben (man muss auch mal was
ändern!) Kreis basierend auf seiner Position und Größe.BounceX()
:
Kehrt die horizontale Geschwindigkeit des Balls um, wodurch er in die
entgegengesetzte Richtung prallt. Diese Methode wird aufgerufen, wenn
der Ball auf ein Paddle trifft.BounceY()
:
Kehrt die vertikale Geschwindigkeit des Balls um, was passiert, wenn der
Ball auf die obere oder untere Wand prallt.Reset()
:
Setzt die Position des Balls auf die Mitte des Spielfelds zurück und
initialisiert seine Geschwindigkeit zufällig nach links oder rechts und
nach oben oder unten.
using System;
using System.Drawing;
namespace WindowsFormsApp_Pong
{
public class Ball
{
public Point Position { get;
private set; }
private Size clientSize;
private int speedX = 4;
private int speedY = 4;
public int Diameter { get; private
set; } = 10;
public Rectangle Bounds => new Rectangle(Position.X, Position.Y, Diameter,
Diameter);
// Statische Instanz von Random, um
konsistente Zufallszahlen zu erhalten
private static Random rand = new
Random();
public Ball(Size clientSize)
{
this.clientSize =
clientSize;
Reset();
}
public void Update()
{
Position = new
Point(Position.X + speedX, Position.Y + speedY);
if (Position.Y + Diameter >= clientSize.Height)
{
Position =
new Point(Position.X, clientSize.Height - Diameter);
// Setze den Ball direkt an die untere Wand
if (speedY
> 0) // Nur wenn der Ball nach unten geht
{
BounceY(); // Umkehren der vertikalen Geschwindigkeit
}
}
}
public void
Draw(Graphics g)
{
g.FillEllipse(Brushes.Yellow, Bounds);
}
public void
BounceX()
{
speedX = -speedX;
}
public void BounceY()
{
speedY = -speedY;
}
public void
Reset()
{
Position = new Point(clientSize.Width / 2, clientSize.Height / 2);
// Verwende die statische Instanz von Random für die
Geschwindigkeiten
speedX = rand.Next(0,
2) == 0 ? 4 : -4; // Zufällige horizontale Richtung
speedY = rand.Next(0,
2) == 0 ? 4 : -4; // Zufällige vertikale Richtung
}
}
}
Um den Spielfluss kümmert sich GameEngine.cs:
Die
GameEngine
-Klasse ist zentral für das
Pong-Spiel und steuert die gesamte Spielmechanik, einschließlich der Bewegung
der Spielobjekte, der Kollisionserkennung, der Punktvergabe und der
Benutzerinteraktion. Sie verwaltet die Aktualisierung und das Zeichnen des
Spiels, reagiert auf Benutzereingaben und stellt sicher, dass das Spiel korrekt
funktioniert.
Felder und Eigenschaften:
playerPaddle
:
Das Paddle, das vom Spieler gesteuert wird.aiPaddle
:
Das Paddle, das von der KI gesteuert wird.ball
:
Das Ball-Objekt, das im Spiel verwendet wird.ai
:
Ein AI-Objekt, das die Logik für das gegnerische Paddle enthält.clientSize
:
Die Größe des Spielfelds, die verwendet wird, um Kollisionen und
Grenzen zu berechnen.playerScore
:
Die Punktzahl des Spielers.aiScore
:
Die Punktzahl der KI.Konstruktor (GameEngine(Size
clientSize)
):
playerPaddle
,
aiPaddle
,
ball
,
ai
)
mit den entsprechenden Startpositionen und der Größe des Spielfelds.Draw(Graphics g)
:
Draw
-Methoden
der einzelnen Spielobjekte (playerPaddle
,
aiPaddle
,
ball
)
auf, um sie auf dem Bildschirm anzuzeigen.Update()
:
CheckCollisions()
und CheckScore()
auf, um Kollisionen zu erkennen und die Punktzahl zu überprüfen.CheckCollisions()
:
BounceX()
).BounceY()
).CheckScore()
:
ResetGame()
).ResetGame()
:
GetPlayerScore()
und GetAIScore()
:
HandleKeyDown(Keys key)
und HandleKeyUp(Keys key)
:
HandleKeyDown(Keys key)
:
Verarbeitet die Tasteneingaben des Spielers und bewegt das
Spieler-Paddle nach oben oder unten, wenn die entsprechenden Tasten
gedrückt werden.HandleKeyUp(Keys key)
:
Stoppt die Bewegung des Spieler-Paddles, wenn die Tasten losgelassen
werden.using System.Drawing;
using System.Windows.Forms;
namespace WindowsFormsApp_Pong
{
public class GameEngine
{
private Paddle playerPaddle;
private Paddle aiPaddle;
private Ball ball;
private Size clientSize;
private AI ai;
// Punkte der Spieler
private int playerScore;
private int aiScore;
public
GameEngine(Size clientSize)
{
int fieldHeight =
clientSize.Height; // Erhalten Sie die Spielfeldhöhe
this.clientSize =
clientSize;
playerPaddle = new Paddle(new Point(30, fieldHeight / 2), true, fieldHeight);
aiPaddle = new
Paddle(new Point(clientSize.Width - 40, fieldHeight / 2), false, fieldHeight);
ball = new
Ball(clientSize);
ai = new AI(aiPaddle, ball);
// Initialisieren der Punkte
playerScore = 0;
aiScore = 0;
}
public void
Draw(Graphics g)
{
// Zeichne die gestrichelte Linie in der Mitte
using (Pen dashedPen =
new Pen(Color.White, 5)) // Breitere Linie und
Striche, 5 Pixel breit
{
dashedPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Custom;
// Verwenden eines benutzerdefinierten DashStyle
dashedPen.DashPattern = new float[] { 11, 5 }; //
Breitere Striche (11 Pixel lang) und ausreichen Abstand (5 Pixel)
float
centerX = clientSize.Width / 2;
g.DrawLine(dashedPen, centerX, 0, centerX, clientSize.Height);
}
playerPaddle.Draw(g);
aiPaddle.Draw(g);
ball.Draw(g);
}
public void Update()
{
playerPaddle.Update();
aiPaddle.Update();
ball.Update();
ai.Update();
CheckCollisions();
CheckScore();
}
private void CheckCollisions()
{
// Kollisionen zwischen Ball und Paddle erkennen
if
(ball.Bounds.IntersectsWith(playerPaddle.Bounds) ||
ball.Bounds.IntersectsWith(aiPaddle.Bounds))
{
ball.BounceX();
}
// Kollisionen mit der oberen und unteren Wand
if (ball.Position.Y <=
0 || ball.Position.Y >= clientSize.Height)
{
ball.BounceY();
}
}
private
void CheckScore()
{
// Wenn der Ball ins linke oder rechte Aus geht
if (ball.Position.X <=
0)
{
aiScore++;
ResetGame();
}
else if (ball.Position.X >= clientSize.Width)
{
playerScore++;
ResetGame();
}
}
private
void ResetGame()
{
ball.Reset();
playerPaddle.Reset(new Point(30, clientSize.Height / 2));
aiPaddle.Reset(new
Point(clientSize.Width - 50, clientSize.Height / 2));
}
public int
GetPlayerScore()
{
return playerScore;
}
public int GetAIScore()
{
return aiScore;
}
public void
HandleKeyDown(Keys key)
{
if (key == Keys.Up) playerPaddle.MoveUp();
if (key == Keys.Down)
playerPaddle.MoveDown();
}
public void
HandleKeyUp(Keys key)
{
if (key == Keys.Up || key == Keys.Down) playerPaddle.Stop();
}
}
}
Nun kommen wir zum wichtigen "Paddle" im Modul Paddle.cs:
Die Paddle
-Klasse
verwaltet die Position und Bewegung eines Paddles im Pong-Spiel. Sie stellt
sicher, dass das Paddle innerhalb der Spielfeldgrenzen bleibt und ermöglicht es
dem Spieler oder der KI, das Paddle nach oben oder unten zu bewegen oder es
anzuhalten. Die Update()
-Methode
wird verwendet, um die Position des Paddles basierend auf seiner Geschwindigkeit
zu aktualisieren und gleichzeitig sicherzustellen, dass es die oberen und
unteren Spielfeldgrenzen nicht überschreitet.
Felder und Eigenschaften:
Position
:
Ein Point
-Objekt,
das die aktuelle Position des Paddles im Spielfeld speichert.isPlayer
:
Ein boolescher Wert, der angibt, ob das Paddle vom Spieler (true
)
oder von der KI (false
)
gesteuert wird.speed
:
Eine Ganzzahl, die die Bewegungsgeschwindigkeit des Paddles bestimmt.
Der Wert gibt an, wie viele Pixel das Paddle pro Frame bewegt wird.Bounds
:
Eine Eigenschaft, die ein
Rectangle
zurückgibt, das den
Bereich des Paddles basierend auf seiner Position und seinen Abmessungen
(Breite von 10 und Höhe von 60 Pixel) beschreibt.velocity
:
Eine Ganzzahl, die die aktuelle vertikale Geschwindigkeit des Paddles
speichert. fieldHeight
:
Eine Ganzzahl, die die Höhe des Spielfelds speichert und verwendet wird,
um sicherzustellen, dass das Paddle nicht über die Spielfeldgrenzen
hinausgeht.Konstruktor (Paddle(Point
startPosition, bool isPlayer, int fieldHeight)
):
Paddle
-Klasse
und setzt die Startposition des Paddles, ob es vom Spieler oder der KI
gesteuert wird und die Höhe des Spielfelds.
fieldHeight
wird gespeichert, um die Bewegungen des Paddles auf die Höhe des
Spielfelds zu beschränken.Methoden:
Draw(Graphics g)
:
Zeichnet das Paddle als weißes Rechteck an seiner aktuellen Position auf
dem Bildschirm.Update()
:
Aktualisiert die Position des Paddles basierend auf seiner aktuellen
Geschwindigkeit (velocity
).MoveUp()
:
Setzt die vertikale Geschwindigkeit (velocity
)
des Paddles auf negativ, sodass es sich nach oben bewegt.MoveDown()
:
Setzt die vertikale Geschwindigkeit (velocity
)
des Paddles auf positiv, sodass es sich nach unten bewegt.Stop()
:
Setzt die Geschwindigkeit (velocity
)
des Paddles auf 0, wodurch es angehalten wird.Reset(Point startPosition)
:
Setzt das Paddle auf eine neue Startposition zurück und stoppt jede
Bewegung.
using System.Drawing;
namespace
WindowsFormsApp_Pong
{
public class Paddle
{
public Point Position { get; private set; }
private bool
isPlayer;
private int speed = 4;
public Rectangle Bounds => new Rectangle(Position.X, Position.Y, 10, 60);
private int velocity;
private int fieldHeight;
// Speichert die Höhe des Spielfelds
public Paddle(Point startPosition, bool isPlayer, int fieldHeight)
{
Position = startPosition;
this.isPlayer = isPlayer;
this.fieldHeight =
fieldHeight; // Initialisierung der Spielfeldhöhe
}
public void Draw(Graphics g)
{
g.FillRectangle(Brushes.White, Bounds);
}
public void Update()
{
Position = new Point(Position.X, Position.Y + velocity);
// Begrenzung innerhalb des Spielfeldes
if (Position.Y < 0)
Position = new
Point(Position.X, 0);
if (Position.Y +
Bounds.Height > fieldHeight) // Anpassung der unteren
Begrenzung
Position = new
Point(Position.X, fieldHeight - Bounds.Height);
}
public void MoveUp()
{
velocity = -speed;
}
public void
MoveDown()
{
velocity =
speed;
}
public void Stop()
{
velocity = 0;
}
public void Reset(Point startPosition)
{
Position = startPosition;
Stop();
}
}
}
Im Zeitalter des Internets ist es interessant, mit einem
Programm gezielt Daten abzurufen und zu bearbeiten, die für uns nützlich sein
können.
Als Beispiel nehmen wir den Anbieter
https://openweathermap.org/ , der uns
kostenlos das Wetter der nächsten fünf Tage an einem bestimmten Ort zur
Verfügung stellt.
Hierfür benötigt man einen kostenlosen API Key, den man
auf der Website anfordern kann.
Der Ort wird durch geographische Länge und
Breite bestimmt.
Diesen Key hinterlegen wir in unseren Umgebungsvariablen
im Betriebssystem:
Windows-Taste betätigen und "Umgebungsvariablen" in
die Suchleiste eingeben.
Bei Benutzervariablen für NN bei Variable
OPEN_WEATHER_KEY und bei Wert den erhaltenen API Key einfügen.
Mit OK
bestätigen und MS Visual Studio neu starten.
Damit können wir den
Begriff OPEN_WEATHER_KEY in unserem C# Programm verwenden.
Wir bauen das
Programm so auf, dass die UI Elemente vom Programm komplett im Code erstellt
werden.
using System;
// Grundlegende Funktionen
using
System.Collections.Generic; // Für generische Listen
using System.Net.Http; // Für HTTP-Anfragen
using System.Threading.Tasks; // Für asynchrone
Programmierung
using System.Windows.Forms;
// Für die Windows Forms Elemente
using Newtonsoft.Json;
// Newtonsoft.Json NuGet-Paket installieren zum
Verarbeiten von JSON-Daten
using System.Drawing; //
Für grafische Elemente wie Farben und Schriftarten
using System.Globalization; // Für kulturelle
Informationen, z.B. Datumsformate
using System.Linq;
// Für LINQ-Abfragen (Language
Integrated Query): https://de.wikipedia.org/wiki/LINQ
namespace
WindowsFormsApp_GetWeather
{
public class Form1 : Form
{
// Durch die Verwendung einer Umgebungsvariable wird
verhindert, dass der API-Schlüssel im Quellcode sichtbar ist.
private string apiKey = Environment.GetEnvironmentVariable("OPEN_WEATHER_KEY");
// UI-Komponenten
private
ComboBox cmbLocations; // Dropdown-Liste für Orte
private Button btnGetWeather; // Abrufen
private DataGridView dataGridViewWeather; // Anzeige
public Form1()
{
InitializeComponent();
InitializeLocations();
}
// Initialisiert die UI-Komponenten
private void InitializeComponent()
{
this.cmbLocations = new System.Windows.Forms.ComboBox();
this.btnGetWeather = new System.Windows.Forms.Button();
this.dataGridViewWeather = new System.Windows.Forms.DataGridView();
((System.ComponentModel.ISupportInitialize)(this.dataGridViewWeather)).BeginInit();
this.SuspendLayout();
//
//
cmbLocations
//
this.cmbLocations.DropDownStyle =
System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cmbLocations.Location = new System.Drawing.Point(12, 12);
this.cmbLocations.Name = "cmbLocations";
this.cmbLocations.Size = new System.Drawing.Size(200, 24);
this.cmbLocations.TabIndex = 0;
//
// btnGetWeather
//
this.btnGetWeather.Location = new
System.Drawing.Point(220, 10);
this.btnGetWeather.Name =
"btnGetWeather";
this.btnGetWeather.Size = new
System.Drawing.Size(100, 23);
this.btnGetWeather.TabIndex
= 1;
this.btnGetWeather.Text = "Wetter abrufen";
this.btnGetWeather.UseVisualStyleBackColor = true;
this.btnGetWeather.Click += new System.EventHandler(this.btnGetWeather_Click);
//
// dataGridViewWeather
//
this.dataGridViewWeather.AutoSizeColumnsMode =
System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
this.dataGridViewWeather.ColumnHeadersHeightSizeMode =
System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dataGridViewWeather.Location = new System.Drawing.Point(12, 50);
this.dataGridViewWeather.Name = "dataGridViewWeather";
this.dataGridViewWeather.RowHeadersWidth = 51;
this.dataGridViewWeather.Size = new System.Drawing.Size(760, 944);
this.dataGridViewWeather.TabIndex = 2;
// Setzen der Schriftart für Zellen
dataGridViewWeather.DefaultCellStyle.Font = GetFont("Lucida Console", 9,
FontStyle.Regular);
//
Setzen der Schriftart für Spaltenüberschriften
dataGridViewWeather.ColumnHeadersDefaultCellStyle.Font = GetFont("Lucida
Console", 9, FontStyle.Bold);
//
// Form1
//
this.ClientSize = new System.Drawing.Size(784,
1006);
this.Controls.Add(this.cmbLocations);
this.Controls.Add(this.btnGetWeather);
this.Controls.Add(this.dataGridViewWeather);
this.Name =
"Form1";
this.Text = "Wettervorhersage";
((System.ComponentModel.ISupportInitialize)(this.dataGridViewWeather)).EndInit();
this.ResumeLayout(false);
}
//
Überprüfen, ob die Schriftart verfügbar ist
private Font
GetFont(string fontName, float size, FontStyle style)
{
if (FontFamily.Families.Any(f => f.Name == fontName))
{
return new Font(fontName, size, style);
}
else
{
// Fallback auf eine Standard-Schriftart
return new Font("Consolas", size, style);
}
}
// Initialisiert die Ortsliste
private void InitializeLocations()
{
// Erstellen einer Liste von Orten mit ihren Koordinaten
var locations = new List<Location>
{
new Location
{
Name = "Einhausen",
Latitude =
49.672222,
Longitude = 8.545139
},
new Location
{
Name = "Maspalomas Gran
Canaria",
Latitude = 27.736944,
Longitude = -15.599444
},
// Weitere Orte können
hier hinzugefügt werden
};
// Binden der Ortsliste an die
ComboBox
cmbLocations.DataSource = locations;
cmbLocations.DisplayMember = "Name";
}
// Klick-Ereignishandler für den Button
private async void btnGetWeather_Click(object sender, EventArgs e)
{
var selectedLocation = cmbLocations.SelectedItem as Location;
if (selectedLocation != null)
{
await GetWeatherDataAsync(selectedLocation);
}
else
{
MessageBox.Show("Bitte wählen Sie einen Ort aus.");
}
}
// Abrufen der Wetterdaten mittels API
// URL-Zusammenstellung: Baut die API-Anfrage-URL mit den Koordinaten des ausgewählten Ortes.
// units=metric
:
Stellt sicher, dass die Temperatur in Celsius zurückgegeben wird.
// lang=de
:
Setzt die Sprache auf Deutsch.
// JSON-Antwort:
Verwendet GetJsonAsync
,
um die JSON-Daten abzurufen.
//
Deserialisierung: Verwendet
JsonConvert.DeserializeObject
, um die
JSON-Daten in C#-Objekte umzuwandeln.
// Anzeige: Ruft
DisplayWeatherData
auf, um die Daten anzuzeigen.
private async Task
GetWeatherDataAsync(Location location)
{
string
url = $"https://api.openweathermap.org/data/2.5/forecast?lat={location.Latitude}&lon={location.Longitude}&appid={apiKey}&units=metric&lang=de";
string json = await GetJsonAsync(url);
if (json != null)
{
var
weatherResponse = JsonConvert.DeserializeObject<WeatherResponse>(json);
DisplayWeatherData(weatherResponse);
}
else
{
MessageBox.Show("Fehler beim Abrufen der Wetterdaten.");
}
}
// Sendet eine asynchrone HTTP-Anfrage
und erhält die JSON-Antwort
private async Task<string>
GetJsonAsync(string url)
{
try
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response =
await client.GetAsync(url);
if
(response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync();
}
else
{
MessageBox.Show("Fehler: " + response.ReasonPhrase);
}
}
}
catch (Exception ex)
{
MessageBox.Show($"Fehler: {ex.Message}");
}
return null;
}
// Methode zum
Runden auf die nächste halbe Zahl
private double
RoundToNearestHalf(double value)
{
return
Math.Round(value * 2, MidpointRounding.AwayFromZero) / 2;
}
// Zeigt die Wetterdaten im DataGridView an
private void DisplayWeatherData(WeatherResponse weatherResponse)
{
// Löschen der vorherigen Daten
dataGridViewWeather.Rows.Clear();
dataGridViewWeather.Columns.Clear();
// Spalten hinzufügen
dataGridViewWeather.Columns.Add("DatumZeit", "Datum/Zeit");
dataGridViewWeather.Columns.Add("Temperatur", "Temperatur (°C)");
dataGridViewWeather.Columns.Add("Beschreibung", "Beschreibung");
dataGridViewWeather.Columns.Add("Luftfeuchtigkeit", "Luftfeuchtigkeit (%)");
// Datenzeilen hinzufügen
foreach (var item in weatherResponse.list)
{
// Runden der
Temperatur
double roundedTemp =
RoundToNearestHalf(item.main.temp);
// Datum und Uhrzeit parsen
DateTime
dateTime = DateTime.Parse(item.dt_txt);
// Datum und Uhrzeit formatieren
string
formattedDateTime = dateTime.ToString("ddd, dd.MM.yyyy HH:mm", new
CultureInfo("de-DE"));
// Luftfeuchtigkeit abrufen und auf ganze Zahl abrunden
int humidityValue = (int)Math.Floor((double)item.main.humidity);
// Zeile hinzufügen
int rowIndex = dataGridViewWeather.Rows.Add
(
formattedDateTime,
roundedTemp.ToString("F1"),
item.weather[0].description,
humidityValue.ToString()
);
// Prüfen, ob die Zeit
"12:00:00" ist
if (dateTime.Hour == 12
&& dateTime.Minute == 0)
{
// Setzen
der Hintergrundfarbe auf ein leichtes Grün
dataGridViewWeather.Rows[rowIndex].DefaultCellStyle.BackColor =
Color.LightGreen;
}
}
}
}
// Klasse für den Ort
public class Location
{
public
string Name { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
}
//
Datenmodelle für die JSON-Deserialisierung
// Liste von Wettervorhersageeinträgen
public class WeatherResponse
{
public List<WeatherItem> list { get;
set; }
}
// Enthält die
Hauptwetterdaten, eine Liste von Wetterbeschreibungen und den Zeitstempel
public class WeatherItem
{
public Main main { get; set; }
public List<WeatherDescription> weather { get; set; }
public string
dt_txt { get; set; }
}
// Enthält die
Beschreibung des Wetters, z.B. "klarer Himmel"
public class
WeatherDescription
{
public string description { get; set; }
}
public class Main
{
public double temp { get; set; }
public int humidity { get; set; }
}
}
Als Ergebnis
erhalten wir: