diff --git a/Morris/ExtensionMethods.cs b/Morris/ExtensionMethods.cs new file mode 100644 index 0000000..999376a --- /dev/null +++ b/Morris/ExtensionMethods.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Morris +{ + internal static class ExtensionMethods + { + public static Player Opponent(this Player p) + { + return ~p; + } + } +} diff --git a/Morris/GameState.cs b/Morris/GameState.cs index ece2046..c1f001a 100644 --- a/Morris/GameState.cs +++ b/Morris/GameState.cs @@ -20,16 +20,176 @@ namespace Morris public Occupation[] Board { get; private set; } public Player NextToMove { get; private set; } public GameResult Result { get; private set; } + public bool IsGameRunning { get; private set; } + + private Dictionary playerPhase; + + private const int FIELD_SIZE = 24; + + static GameState() + { + + connections = new bool[FIELD_SIZE, FIELD_SIZE]; + foreach (int[] mill in mills) + { + for (int i = 0; i < mill.Length - 1; i++) + { + connections[mill[i], mill[i + 1]] = true; + connections[mill[i + 1], mill[i]] = true; + } + } + } public GameState() { // Leeres Feld - Board = Enumerable.Repeat(Occupation.Free, 24).ToArray(); + Board = Enumerable.Repeat(Occupation.Free, FIELD_SIZE).ToArray(); NextToMove = Player.White; Result = GameResult.Running; + playerPhase = new Dictionary() + { + [Player.Black] = Phase.Placing, + [Player.White] = Phase.Placing + }; } - public bool IsValidMove() + // Jeder Eintrag repräsentiert eine mögliche Mühle + private static readonly int[][] mills = new[] + { + // Horizontal + new[] { 0, 1, 2 }, + new[] { 3, 4, 5 }, + new[] { 6, 7, 8 }, + new[] { 9, 10, 11 }, + new[] { 12, 13, 14 }, + new[] { 15, 16, 17 }, + new[] { 18, 19, 20 }, + new[] { 21, 22, 23 }, + + //Vertikal + new[] { 0, 9, 21 }, + new[] { 3, 10, 18 }, + new[] { 6, 11, 15 }, + new[] { 1, 4, 7 }, + new[] { 16, 19, 22 }, + new[] { 8, 12, 17 }, + new[] { 5, 12, 20 }, + new[] { 2, 14, 23 } + }; + + // Gibt an, ob zwei Felder verbunden sind. + // Wird aus den Daten in mills im statischen Konstruktor generiert + private static bool[,] connections; + + /// + /// Bestimmt, ob ein Zug in der aktuellen Spielsituation gültig ist + /// + /// Der Zug, der überprüft werden soll + /// + /// , wenn der Zug gültig ist. + /// , wenn der Zug gültig ist, aber eine Mühle schließt, und kein zu entfernender Stein angegeben wurde. + /// , wenn der Zug gültig ist, aber ein zu entfernender Stein angegeben wurde, obwohl der Zug keine Mühle schließt. + /// , wenn der Zug ungültig ist. + /// + public MoveValidity IsValidMove(GameMove move) + { + // Die Verifikation wurde aus Gründen der Lesbarkeit nicht in eine gigantische + // bedingte Anweisung gepackt, auch wenn dies kompakter und eventuell marginal + // schneller wäre + + // 1.: Ziel verifizieren + if (move.To < 0 || move.To >= FIELD_SIZE) + return MoveValidity.Invalid; // OOB + + if (Board[move.To] != Occupation.Free) + return MoveValidity.Invalid; // Zielplatz belegt + + // 2.: "Steinherkunft" verifizieren + if (move.From.HasValue) // Bewegung + { + if (playerPhase[NextToMove] == Phase.Placing) + return MoveValidity.Invalid; // Darf noch keinen Stein bewegen + + if (move.From < 0 || move.From >= FIELD_SIZE) + return MoveValidity.Invalid; // OOB + + if ((int)Board[move.From.Value] != (int)NextToMove) // In der Enum-Definition von Occupation gleichgesetzt + return MoveValidity.Invalid; // Kein Stein zum Bewegen + + if (playerPhase[NextToMove] == Phase.Moving && !connections[move.From.Value, move.To]) + return MoveValidity.Invalid; // Darf noch nicht springen + } + else if (playerPhase[NextToMove] != Phase.Placing) + return MoveValidity.Invalid; // Darf keinen Stein mehr platzieren + + // 3.: Wurde eine Mühle geschlossen? + bool millClosed = mills.Any(mill => mill.All(point => (int)Board[point] == (int)NextToMove || point == move.To)); + + // 4.: Verifikation des Mühlenparameters + if (millClosed) + { + if (!move.Remove.HasValue) + return MoveValidity.ClosesMill; // Kein Parameter gegeben + + if (move.Remove < 0 || move.Remove >= FIELD_SIZE) + return MoveValidity.Invalid; // OOB + + if ((int)Board[move.Remove.Value] != (int)NextToMove.Opponent()) + return MoveValidity.Invalid; // Auf dem Feld liegt kein gegnerischer Stein + + // Es darf kein Stein aus einer geschlossenen Mühle entnommen werden, falls es Steine gibt, die in keiner + // Mühle sind. + // Die LINQ-Abfrage drückt folgendes aus: + // "Für alle gegnerischen Steine gilt, dass eine Mühle existiert, die diesen Stein enthält und von der alle + // Felder durch gegnerische Steine besetzt sind (die Mühle also geschlossen ist)" + bool allInMill = Enumerable.Range(0, FIELD_SIZE) + .Where(point => (int)Board[point] != (int)NextToMove.Opponent()) + .All(point => mills.Any(mill => mill.Contains(point) && mill.All(mp => (int)Board[point] == (int)NextToMove.Opponent()))); + + if (!allInMill && mills.Any(mill => mill.Contains(move.Remove.Value) && mill.All(point => (int)Board[point] == (int)NextToMove.Opponent()))) + return MoveValidity.Invalid; // Versuch, einen Stein aus einer Mühle zu entfernen, obwohl Steine frei sind + } + else if (move.Remove.HasValue) + return MoveValidity.DoesNotCloseMill; // Unnötiger Parameter gegeben + + return MoveValidity.Valid; + } + + /// + /// Versucht, einen Zug auszuführen + /// + /// Der auszuführende Spielzug + /// + /// , wenn das Spiel nicht läuft. + /// , wenn der Zug ungültig ist. kann helfen, zu bestimmen, warum der Zug nicht gültig ist. + /// , wenn der Zug erfolgreicht ausgeführt wurde. + /// + public MoveResult TryApplyMove(GameMove move) + { + if (!IsGameRunning) + return MoveResult.GameNotRunning; + + if (IsValidMove(move) != MoveValidity.Valid) + return MoveResult.InvalidMove; + + // Weiteres Error Checking ist nicht notwendig, da dieses in IsValidMove vorgenommen wurde + + // ggf. wegbewegter Stein + if (move.From.HasValue) + Board[move.From.Value] = Occupation.Free; + + // Hinbewegter Stein + Board[move.To] = (Occupation)NextToMove; + + // ggf. entfernter Stein + if (move.Remove.HasValue) + Board[move.Remove.Value] = Occupation.Free; + + // Gegner ist jetzt dran + NextToMove = NextToMove.Opponent(); + + return MoveResult.OK; + } } diff --git a/Morris/Morris.csproj b/Morris/Morris.csproj index 20bded4..4a38c72 100644 --- a/Morris/Morris.csproj +++ b/Morris/Morris.csproj @@ -43,6 +43,7 @@ + @@ -50,6 +51,7 @@ + diff --git a/Morris/MoveValidity.cs b/Morris/MoveValidity.cs new file mode 100644 index 0000000..a26d5ac --- /dev/null +++ b/Morris/MoveValidity.cs @@ -0,0 +1,19 @@ +/* + * MoveValidity.cs + * Copyright (c) 2016 Markus Himmel + * This file is distributed under the terms of the MIT license + */ + +namespace Morris +{ + /// + /// Gibt an, ob ein Spielzug gültig ist + /// + public enum MoveValidity + { + Valid, + Invalid, + ClosesMill, + DoesNotCloseMill + } +} diff --git a/Morris/Occupation.cs b/Morris/Occupation.cs index 554451e..0ada8a3 100644 --- a/Morris/Occupation.cs +++ b/Morris/Occupation.cs @@ -12,7 +12,10 @@ namespace Morris public enum Occupation { Free, - White, - Black + // Die folgende Gleichsetzung spart einige Verzweigungen, + // indem ein Wert von Occupation direkt mit einem Wert + // von Player verglichen werden kann + White = Player.White, + Black = Player.Black } } \ No newline at end of file diff --git a/Morris/Player.cs b/Morris/Player.cs index c7647f5..20d3e88 100644 --- a/Morris/Player.cs +++ b/Morris/Player.cs @@ -9,9 +9,9 @@ namespace Morris /// /// Repräsentiert einen Spieler /// - public enum Player + public enum Player : byte { - White, - Black + White = 170, // (10101010) base 2 + Black = 85 // (01010101) base 2 } }