Dr. Erhard Henkes, Stand: 27.08.2024

Windows Forms mit C# (Teil 1)

 

Einstieg

Tic-Tac-Toe

Schiffe versenken gegen einen einfachen Gegner

Schiffe versenken gegen einen verbesserten KI-Gegner

Mastermind

Pixel Maze Challenge

Snake

Pong

 

 


Der Einstieg

Wir werden Windows-Programme mit C# in Visual Studio 2022 erstellen.
Wir schauen uns nun an, wie man mit einer Windows Forms App beginnt.

1. Neues Projekt erstellen

  1. Öffnen Sie Visual Studio 2022.
  2. Wählen Sie im Startbildschirm "Neues Projekt erstellen" aus.
  3. Suchen Sie nach "Windows Forms-App (.NET Framework)" und wählen Sie die Version mit C# es aus. Klicken Sie auf "Weiter".
  4. Geben Sie ihrem Projekt einen Namen, z.B. WindowsFormsApp1, und wählen Sie den Speicherort aus. Bei mir ist dies "F:\Projects".
  5. Wählen Sie das gewünschte .NET Framework aus. Bei mir ist dies Version 4.8.1. Klicken Sie auf "Erstellen".
  6. Klicken Sie rechts auf "Toolbox". Dort finden wir passende Steuerelemente für unser erstes Fenster.



2. Benutzeroberfläche gestalten

  1. Im Designer-Fenster gestalten Sie die Benutzeroberfläche des Programms visuell.
    Ziehen Sie Steuerelemente wie Buttons, Labels, TextBoxen usw. aus der Toolbox auf das Formular.
    Wir wählen einen Button und ein Label (aus der Toolbox).
  2. Passen Sie die Eigenschaften, z.B. Platzierung und Größe, der Steuerelemente im Eigenschaftenfenster oder mit der Maus an.

3. Code hinzufügen

  1. Doppelklicken Sie nun auf ein Steuerelement, z.B. den Button, um ein Ereignis zu erstellen und in den Code-Editor zu wechseln.
  2. Schreiben Sie den Code für das Klick-Ereignis: label1.Text = "Button wurde geklickt!";

 


4. Programm ausführen

  1. Stellen Sie um von Debug auf Release. Erstellen Sie das Programm mit F7 (Erstellen/Projektmappe erstellen).
  2. Suchen Sie zur Übung das Programm "F:\Projects\WindowsFormsApp1\bin\Release\WindowsFormsApp1.exe" und führen Sie es aus.
  3. Betätigen Sie den Button. Das programmierte Ereignis wird ausgeführt.

Hoffentlich funktioniert das bei Ihnen genau so. Wenn ja, ist der praktische Einstieg in die Programmierung mit WindowsForms (.NET) und C# gelungen.

 


Wir bauen ein Tic-Tac-Toe

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:

1. Neues Projekt erstellen

2. Benutzeroberfläche gestalten

Fügen Sie auf der Form (Form1.cs) folgende Steuerelemente hinzu (rechts die Toolbox öffnen):

3. Benennen Sie die Steuerelemente (verwechseln Sie nicht Name und Text)

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:
 

Tic-Tac-Toe:

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.

1. Benutzeroberfläche (UI = User Interface)

2. Funktionale Komponenten

3. Benutzererfahrung

4. Erweiterungsmöglichkeiten

 

 

Jetzt spielen wir "Schiffe versenken" - zunächst gegen einen einfachen künstlichen Gegner

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.

1. Die Ship-Klasse

Die Ship-Klasse repräsentiert ein Schiff im Spiel. Jedes Schiff hat folgende Eigenschaften:

Die Ship-Klasse hat einen Konstruktor, der den Namen und die Größe des Schiffes initialisiert.

2. Die Form1-Klasse

Die 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:

3. Initialisierung des Spiels (InitializeGame)

Die Methode InitializeGame initialisiert die Spielfelder und verknüpft die Buttons mit den entsprechenden Ereignis-Handlern:

4. Schiffsplatzierung (PlayerButton_Click, PlaceShip, CanPlaceShip)

Der Spieler platziert seine Schiffe durch Klicken auf die Buttons im eigenen Spielfeld. Die Methode PlayerButton_Click steuert diesen Prozess:

5. Schusslogik (EnemyButton_Click, KIMove)

Nachdem alle Schiffe platziert wurden, beginnt das eigentliche Spiel:

6. Überprüfung auf Versenkung (CheckIfShipSunk)

Nach jedem Treffer wird überprüft, ob das Schiff vollständig versenkt ist:

7. Spielende überprüfen (CheckGameEnd)

Nach jedem Schuss wird überprüft, ob alle Schiffe einer Partei versenkt wurden:

8. Reset des Spiels (ResetGame)

Nach dem Ende des Spiels kann das Spiel durch einen Neustart der Anwendung zurückgesetzt werden.

Zusammenfassung

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

 

 

 

Jetzt spielen wir "Schiffe versenken" - gegen einen verbesserten KI-Gegner

 

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:

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:

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.

    Die Variablen playerShots und kiShots zählen die Anzahl der Schüsse, die der Spieler und die KI abgegeben haben. Sie helfen dabei, den Fortschritt im Spiel zu verfolgen und können für Statistiken oder Debugging-Zwecke verwendet werden.

    Der Konstruktor der Form1-Klasse wird aufgerufen, wenn das "Formular" erstellt wird:

  •  


    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.



    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.

    1. Spielzustand zurücksetzen:

    2. Checkerboard-Strategie festlegen:

    3. Erstellung der Schiffsliste:

    4. Initialisierung der Spielfelder (Buttons):

    5. Statusanzeige initialisieren:

     



    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.

    1. Überprüfung der Platzierungsphase:

    2. Schussphase:

     



    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.

    1. Bestimmung der Startposition:

    2. Abbruch bei ungültiger Position:

    3. Bestimmung der Schiffgröße:

    4. Überprüfung der Platzierung:

    5. Rückgabe des Ergebnisses:

     



    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:

  • Abbruch bei ungültiger Position:

  • Bestimmung der Schiffgröße:

  • Platzierung des Schiffs:

  • Sicherheitsüberprüfungen:

  • 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:

    1. Musterwahl:

    2. Checkerboard-Logik:

    3. Zielsetzung:

     

     



    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.

    1. Debugging und Initialisierung

    2. Hinzufügen von Cluster-Zielen

    3. Bestimmung der Schiffsrichtung

    4. Entfernen unnötiger Cluster-Ziele

    5. Anmerkung

     

     



    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:

    1. KI-Zug beginnen:

    2. Überprüfung der verbleibenden großen Schiffe:

    3. Bestimmung der Strategie:

    4. Auswahl und Ausführung der Strategie:

    5. KI-Zug beenden:

    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.

    Erklärungen der einzelnen Schritte:

    1. Button-Erkennung:

    2. Prüfung der Farbe:

    3. Rücksetzen der Farbe:

    4. Vermeidung ungewollter Änderungen:

    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:

    1. Zielauswahl:

    2. Schussvorbereitung:

    3. Schussausführung:

    4. Spielerzug-Freigabe:

    FireCheckerboardShot Methode:

    Diese Methode steuert das Schießen der KI basierend auf der Checkerboard-Strategie:

    1. Zielauswahl:

    2. Schussvalidierung:

    3. Weiteres Vorgehen bei beschossenem Ziel:

    4. Strategiewechsel:

    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:

    1. Zielbestimmung:

    2. Prüfung auf bereits beschossene Felder:

    3. Schusszählung und Konsistenzprüfung:

    4. Treffer- oder Fehlschussprüfung:

    5. Zugübergabe:

     

     

     


    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:

    1. Zurücksetzen der Schussinformationen:

    2. Cluster-Ziele bereinigen:

    3. 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:

    1. Grenzprüfung:

    2. Zustand des Spielfeldes:

     

     

     

    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:

    1. Berechnung des nächsten Punktes:

    2. Gültigkeitsprüfung:

     

     

     

    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.

    1. Grenzprüfung:

    2. Rückgabewert:

     

     

     


    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.
     

    1. Überprüfung auf letzten Treffer:

    2. Richtungsversuche:

    3. Wenn kein Schuss abgegeben wurde:

    4. Status-Update:

     

     



    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:

  • Überprüfung auf bereits beschossenes Feld:

  • Schuss abgeben:

  • Zugwechsel:

  •  

     

     

     

    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:

  • Fehlschuss:

  • Zugwechsel:

  • Verzögerung vor dem KI-Zug:

  •  

     


    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:

  • Durchlaufen der Liste der Schiffe:

  • Registrierung des Treffers:

  • Überprüfung, ob das Schiff versenkt wurde:

  • Benachrichtigung bei Versenkung:

  • Überprüfung des Spielendes:

  • Abbruch der Schleife:



  •  

     

     

     

    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
    }

     

     

     

    Die Methode btnStart_Click wird ausgeführt, wenn der Start-Button geklickt wird. Zunächst prüft sie, ob das Spiel bereits gestartet wurde (Doppelklick vermeiden).
    Wenn das Spiel läuft, wird eine Nachricht angezeigt und die Methode beendet.

    Falls das Spiel noch nicht gestartet wurde, prüft die Methode, ob der Spieler alle Schiffe auf dem Spielfeld platziert hat.
    Wenn nicht, wird der Spieler aufgefordert, die Platzierung abzuschließen.

    Wenn alle Schiffe des Spielers platziert sind, werden auch die Schiffe der KI auf dem rechten Spielfeld (unsichtbar) positioniert.

    Danach wird das Spiel als gestartet markiert, der Start-Button deaktiviert (Doppelklick vermeiden).

    Eine Statusmeldung informiert den Spieler, dass das Spiel beginnt und er an der Reihe ist. Der Kampf kann beginnen.

     

     

     

     

    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.

    Funktionsweise:

    1. Iterieren über das Array: Die Methode durchläuft alle Zeilen und Spalten des Arrays buttonsArray.
    2. Vergleich: Für jeden Button im Array wird überprüft, ob er mit dem übergebenen Button (button) übereinstimmt.
    3. Rückgabe der Position: Wenn der Button gefunden wird, wird seine Position als Point zurückgegeben. Hierbei steht i für die Zeile (X-Koordinate) und j für die Spalte (Y-Koordinate).
    4. Fehlerbehandlung: Falls der Button aus irgendeinem Grund nicht im Array gefunden wird, gibt die Methode Point.Empty zurück.
      Dieser Rückgabewert signalisiert, dass die Suche erfolglos war, was in der Praxis jedoch nicht vorkommen sollte, wenn sichergestellt ist, dass der Button im Array vorhanden ist.

    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

    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.

    [STAThread]

    Application.EnableVisualStyles();

    Application.SetCompatibleTextRenderingDefault(false);

    Application.Run(new MainForm());

     


    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.

    1. private static readonly Random random = new Random();

    2. private List<string> availableColors;

    3. public AI(List<string> availableColors)

    4. public List<string> GenerateCode(int codeLength)

    5. public List<string> MakeGuess(int codeLength)

     

    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


  • Konstruktor MastermindGame


  • Methode GenerateCode


  • Methode CheckAttempt



  • 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.

    Hauptbestandteile der MainForm-Klasse

    1. Felder und Konstruktor

    2. InitializeComboBoxes-Methode

    3. InitializeComboBox-Methode

    4. StartNewGame-Methode

    5. Event-Handler für btnNewGame und btnCheckAttempt

    6. UpdateCorrectPositionsLabel-Methode

    7. GetAttemptFromUI-Methode

    8. DisplayResult-Methode

    9. 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.

     

     

     

    Pixel Maze Challenge

    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.

    Hauptbestandteile des Codes

    1. Deklaration von Variablen und Initialisierung:

    2. Formular-Konstruktor Form1():

    3. Formular-Ladeereignis Form1_Load:

    4. StartGame Methode:

    5. InitializeObstacles Methode:

    6. InitializePoints Methode:

    7. gameTimer_Tick Ereignishandler:

    8. ObstacleTimer_Tick Ereignishandler:

    9. MoveObstacles Methode:

    10. CheckCollision Methode:

    11. restartButton_Click Ereignishandler:

    12. gamePanel_PreviewKeyDown Ereignishandler:

    13. 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.

     

     

    Snake

    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.

    1. Enumerationen und Klassen

    2. Game-Klasse

    Die Game-Klasse enthält die Hauptlogik für das Snake-Spiel:

     


    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-Klasse

    Diese 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.

    1. Felder und Konstruktor

    2. Methoden

     

     

    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

    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.

    Grundprinzipien von Pong

    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. 

    1. Felder und Variablen:

    2. Konstruktor (GameForm()):

    3. InitializeGame():

    4. GameLoop():

    5. UpdateTitle():

    6. OnPaint():

    7. DrawText():

    8. OnKeyDown() und OnKeyUp():

    9. TogglePause():

     

     

    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. 

    1. Felder der Klasse AI:

    2. Konstruktor (AI(Paddle aiPaddle, Ball ball)):

    3. Update()-Methode:

    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. 

     

     

    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.

    1. Felder und Eigenschaften:

    2. Konstruktor (GameEngine(Size clientSize)):

    3. Draw(Graphics g):

    4. Update():

    5. CheckCollisions():

    6. CheckScore():

    7. ResetGame():

    8. GetPlayerScore() und GetAIScore():

    9. HandleKeyDown(Keys key) und HandleKeyUp(Keys key):

     

    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.

    1. Felder und Eigenschaften:

    2. Konstruktor (Paddle(Point startPosition, bool isPlayer, int fieldHeight)):

    3. Methoden:

     

     

    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();
        }
      }
    }