From 2c3f1ce50283510b1440e60fe67c5d5da9846171 Mon Sep 17 00:00:00 2001 From: Markus Himmel Date: Fri, 26 Aug 2016 00:07:53 +0200 Subject: [PATCH] Volle Logik bisa uf Unentschieden, Spielschleife --- Morris/ExtensionMethods.cs | 7 ++ Morris/Game.cs | 88 ++++++++++++++++ Morris/GameMove.cs | 25 ++++- Morris/GameResult.cs | 6 +- Morris/GameState.cs | 196 +++++++++++++++++++++++++++-------- Morris/IGameStateObserver.cs | 19 +++- Morris/IReadOnlyGameState.cs | 22 ++-- Morris/Morris.csproj | 1 + 8 files changed, 308 insertions(+), 56 deletions(-) create mode 100644 Morris/Game.cs diff --git a/Morris/ExtensionMethods.cs b/Morris/ExtensionMethods.cs index 999376a..1d32fe4 100644 --- a/Morris/ExtensionMethods.cs +++ b/Morris/ExtensionMethods.cs @@ -8,8 +8,15 @@ namespace Morris { internal static class ExtensionMethods { + /// + /// Gibt den Gegner des Spielers zurück + /// public static Player Opponent(this Player p) { + // Es ist in der Regel vermutlich einfacher ~player anstatt player.Opponent() zu + // schreiben, Änderungen am Schema von Player sind so jedoch von dem Code, der + // Player verwendet, wegabstrahiert und die semantische Bedeutung von Code, + // der .Opponent verwendet, ist einfacher zu erkennen (Kapselung). return ~p; } } diff --git a/Morris/Game.cs b/Morris/Game.cs new file mode 100644 index 0000000..7836a51 --- /dev/null +++ b/Morris/Game.cs @@ -0,0 +1,88 @@ +/* + * Game.cs + * Copyright (c) 2016 Markus Himmel + * This file is distributed under the terms of the MIT license + */ + +using System.Collections.Generic; +using System.Threading; + +namespace Morris +{ + /// + /// Repräsentiert ein einzelnes Mühlespiel + /// + class Game + { + private List observers = new List(); + private GameState state; + private Dictionary providers; + + public Game(IMoveProvider white, IMoveProvider black) + { + state = new GameState(); + providers = new Dictionary() + { + [Player.White] = white, + [Player.Black] = black + }; + } + + /// + /// Spielt eine gesamte Runde Mühle + /// + /// Die Zeit, in Millisekunden, die gewartet wird, bevor nach einem + /// erfolgreichem Zug der nächste Zug angefordert wird (damit KI vs. KI-Spiele in einem + /// angemessenen Tempo angesehen werden können) + /// Das Spielergebnis + public GameResult Run(int moveDelay = 0) + { + notifyOberservers(); + MoveResult res; + + // Äußere Schleife läuft einmal pro tatsächlichem Zug + do + { + // Innere Schleife läuft einmal pro Zugversuch + do + { + res = state.TryApplyMove(providers[state.NextToMove].GetNextMove(state)); + } while (res == MoveResult.InvalidMove); + + notifyOberservers(); + Thread.Sleep(moveDelay); + } while (state.Result == GameResult.Running); + + return state.Result; + } + + /// + /// Registriert einen für kommende Spielereignisse + /// + /// Das zu notifizierende Objekt + public void AddObserver(IGameStateObserver observer) + { + observers.Add(observer); + } + + /// + /// Meldet einen von kommenden Spielereignissen ab + /// + /// Das abzumeldende Objekt + /// Wahr, wenn das Objekt tatsächlich entfernt wurde. + /// Falsch, wenn es nicht gefunden wurde. + public bool RemoveObserver(IGameStateObserver observer) + { + return observers.Remove(observer); + } + + // Meldet dem Spielzustand an alle Observer + private void notifyOberservers() + { + foreach (var observer in observers) + { + observer.Notify(state); + } + } + } +} diff --git a/Morris/GameMove.cs b/Morris/GameMove.cs index 0368418..6b2e3c5 100644 --- a/Morris/GameMove.cs +++ b/Morris/GameMove.cs @@ -70,11 +70,34 @@ namespace Morris /// /// Wo sich der Stein vor dem Zug befindet /// Wo sich der Stein nach dem Zug befindet - /// Welcher gegnerische Stein bewegt werden soll + /// Welcher gegnerische Stein entfernt werden soll /// Einen nicht zwangsläufig gültigen Spielzug public static GameMove MoveRemove(int from, int to, int remove) { return new GameMove(from, to, remove); } + + // Die nachfolgenden beiden Methoden existieren, weil GameMove immutable sein soll, damit es keine lustigen + // Aliasing-Bugs gibt, wenn MoveProviders komische Dinge mit den Zügen machen, die sie von GameState.BasicMoves + // zurückbekommen + + /// + /// Gibt eine Kopie des GameMove ohne Informationen zur Entfernung zurück + /// + /// Eine neuen Spielzug + public GameMove WithoutRemove() + { + return new GameMove(From, To, null); + } + + /// + /// Erstellt eine Kopie des GameMove mit Zusatzinformation zur Entfernung eines gegnerischen Steins zurück + /// + /// Welcher gegnerische Stein entfernt werden soll + /// Einen neuen Spielzug + public GameMove WithRemove(int remove) + { + return new GameMove(From, To, remove); + } } } diff --git a/Morris/GameResult.cs b/Morris/GameResult.cs index b95451b..6e8cd56 100644 --- a/Morris/GameResult.cs +++ b/Morris/GameResult.cs @@ -12,8 +12,8 @@ namespace Morris public enum GameResult { Running, - WhiteVictory, - BlackVictory, - Draw + Draw, + WhiteVictory = Player.White, + BlackVictory = Player.Black } } diff --git a/Morris/GameState.cs b/Morris/GameState.cs index f42b2fe..838ab84 100644 --- a/Morris/GameState.cs +++ b/Morris/GameState.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Collections.ObjectModel; using System.Text; using System.Threading.Tasks; @@ -15,52 +16,19 @@ namespace Morris /// /// Repräsentiert eine Mühle-Spielsituation /// - public class GameState + public class GameState : IReadOnlyGameState { public Occupation[] Board { get; private set; } public Player NextToMove { get; private set; } public GameResult Result { get; private set; } private Dictionary playerPhase; + private Dictionary stonesPlaced; + private Dictionary currentStones; - /// - /// Gibt die Phase, in der sich ein Spieler befindet, zurück - /// - /// Der Spieler, dessen Phase gesucht ist - /// Eine Phase - public Phase GetPhase(Player player) - { - return playerPhase[player]; - } - - 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, FIELD_SIZE).ToArray(); - NextToMove = Player.White; - Result = GameResult.Running; - playerPhase = new Dictionary() - { - [Player.Black] = Phase.Placing, - [Player.White] = Phase.Placing - }; - } + public const int FIELD_SIZE = 24; + public const int STONES_MAX = 9; + public const int FLYING_MAX = 3; // Jeder Eintrag repräsentiert eine mögliche Mühle private static readonly int[][] mills = new[] @@ -90,6 +58,139 @@ namespace Morris // Wird aus den Daten in mills im statischen Konstruktor generiert private static bool[,] connections; + 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, FIELD_SIZE).ToArray(); + NextToMove = Player.White; + Result = GameResult.Running; + + playerPhase = new Dictionary() + { + [Player.Black] = Phase.Placing, + [Player.White] = Phase.Placing + }; + + stonesPlaced = new Dictionary() + { + [Player.Black] = 0, + [Player.White] = 0 + }; + + currentStones = new Dictionary() + { + [Player.Black] = 0, + [Player.White] = 0 + }; + } + + ReadOnlyCollection IReadOnlyGameState.Board + { + get + { + return Array.AsReadOnly(Board); + } + } + + // Die folgenden drei Methoden existieren, weil selbst eine Setter-Only Property, + // die die zugrundeliegenden Dictionaries zurückgibt, modifizierbar wäre. + // IReadOnlyDictionary ist keine Lösung, weil es Nutzer dieser Klasse eigentlich + // nicht interessiert, wie diese Daten gespeichtert sind. In einer späteren Version + // könnte sich das auch ändern (weil Dictionaries ziemlich "overkill" zur Speicherung + // zweier Ganzzahlen sind). + + /// + /// Gibt die Phase, in der sich ein Spieler befindet, zurück + /// + /// Der Spieler, dessen Phase gesucht ist + /// Eine Phase + public Phase GetPhase(Player player) + { + return playerPhase[player]; + } + + /// + /// Gibt die von einem Spieler insgesamt gesetzten Steine zurück + /// + /// Der Spieler, dessen gesetzten Steine gesucht sind + /// Eine Zahl zwischen 0 und + public int GetStonesPlaced(Player player) + { + return stonesPlaced[player]; + } + + /// + /// Gibt die Zahl der Steine auf dem Spielfeld, die einem Spieler gehören, zurück + /// + /// Der Spieler, dessen aktuelle Steinzahl gesucht ist + /// Eine Zahl zwischen 0 und + public int GetCurrentStones(Player player) + { + return currentStones[player]; + } + + // Gibt alle Paare von Spielfeldpositionen (p, p2) zurück, sodass + // p vom aktuellen Spieler belegt ist und p2 frei ist und + // zusätzlich pred(p, p2) erfüllt ist. + // Hilfsmethode für BasicMoves; in C# 7 könnte man da eine coole + // lokale Funktion draus machen, das hier ist aber ein + // C# 6-Projekt... + private IEnumerable pairs(Func pred) + { + return Enumerable.Range(0, FIELD_SIZE) + .Where(p => (int)Board[p] == (int)NextToMove) + .SelectMany(p => Enumerable.Range(0, FIELD_SIZE) + .Where(p2 => Board[p2] == Occupation.Free && pred(p, p2)) + .Select(p2 => GameMove.Move(p, p2))); + } + + /// + /// Gibt alle möglichen Spielzüge für den Spieler, der aktuell am Zug ist, + /// ohne Informationen über zu entfernende gegnerische Steine zurück. + /// + /// Für von dieser Methode zurückgegebene Züge kann mithilfe von + /// bestimmt werden, ob ein Stein + /// entfernt werden darf. + /// + public IEnumerable BasicMoves() + { + switch (playerPhase[NextToMove]) + { + case Phase.Placing: + // Ein neuer Zug für alle freien Felder + return Enumerable.Range(0, FIELD_SIZE) + .Where(p => Board[p] == Occupation.Free) + .Select(p => GameMove.Place(p)); + + case Phase.Moving: + // Ein neuer Zug für jedes Paar von Positionen (p, p2), bei dem + // p vom aktuellen Spieler belegt ist und p2 frei und mit p + // verbunden ist + return pairs((p, p2) => connections[p, p2]); + + case Phase.Flying: + // Ein neuer Zug für jedes Paar von Positionen (p, p2), bei dem + // p vom aktuellen Spieler belegt ist und p2 frei ist + return pairs((p, p2) => true); + + default: + throw new InvalidOperationException("Sollte nie erreicht werden"); + } + } + /// /// Bestimmt, ob ein Zug in der aktuellen Spielsituation gültig ist /// @@ -186,20 +287,33 @@ namespace Morris // ggf. wegbewegter Stein if (move.From.HasValue) Board[move.From.Value] = Occupation.Free; + else if (++stonesPlaced[NextToMove] == STONES_MAX) + playerPhase[NextToMove] = Phase.Moving; // Hinbewegter Stein Board[move.To] = (Occupation)NextToMove; // ggf. entfernter Stein if (move.Remove.HasValue) + { Board[move.Remove.Value] = Occupation.Free; + if (--currentStones[NextToMove.Opponent()] == FLYING_MAX) + playerPhase[NextToMove.Opponent()] = Phase.Flying; + } + + // Gegner hat nur noch zwei Steine + if (currentStones[NextToMove.Opponent()] == 2) + Result = (GameResult)NextToMove; // Gegner ist jetzt dran NextToMove = NextToMove.Opponent(); + // Wenn der (jetzt) aktuelle Spieler keine gültigen Züge hat, + // hat er verloren + if (!BasicMoves().Any()) + Result = (GameResult)NextToMove.Opponent(); + return MoveResult.OK; } - - } } diff --git a/Morris/IGameStateObserver.cs b/Morris/IGameStateObserver.cs index e90a47b..802023d 100644 --- a/Morris/IGameStateObserver.cs +++ b/Morris/IGameStateObserver.cs @@ -1,12 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +/* + * IGameStateObserver.cs + * Copyright (c) 2016 Markus Himmel + * This file is distributed under the terms of the MIT license + */ namespace Morris { + /// + /// Eine Entität, die ein Spiel "abbonieren" kann und dann über Änderungen + /// des Spielzustands in Kenntnis gesetzt wird + /// interface IGameStateObserver { + /// + /// Wird aufgerufen, wenn sich der aktuelle Spielzustand geändert hat + /// + /// Lesesicht auf den aktuellen Spielzustand + void Notify(IReadOnlyGameState state); } } diff --git a/Morris/IReadOnlyGameState.cs b/Morris/IReadOnlyGameState.cs index c035047..e1dd49a 100644 --- a/Morris/IReadOnlyGameState.cs +++ b/Morris/IReadOnlyGameState.cs @@ -4,18 +4,14 @@ * This file is distributed under the terms of the MIT license */ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Collections.ObjectModel; namespace Morris { /// /// Eine schreibgeschützte Sicht auf ein Spielfeld, anhand der ein - /// einen Nachfolgezug bestimmen soll + /// einen Nachfolgezug bestimmen soll und ein die Spielsituation + /// komsumieren kann /// public interface IReadOnlyGameState { @@ -38,6 +34,7 @@ namespace Morris // Methoden, die Auskunft über die Spielsituation geben + // (siehe hierzu auch den Kommentar in GameState.cs) /// /// Gibt die Phase, in der sich ein Spieler befindet, zurück @@ -46,6 +43,19 @@ namespace Morris /// Eine Phase Phase GetPhase(Player player); + /// + /// Gibt die von einem Spieler insgesamt gesetzten Steine zurück + /// + /// Der Spieler, dessen gesetzten Steine gesucht sind + /// Eine Zahl zwischen 0 und + int GetStonesPlaced(Player player); + + /// + /// Gibt die Zahl der Steine auf dem Spielfeld, die einem Spieler gehören, zurück + /// + /// Der Spieler, dessen aktuelle Steinzahl gesucht ist + /// Eine Zahl zwischen 0 und + int GetCurrentStones(Player player); // Methoden zur Vereinfachung der Arbeit von IMoveProvider diff --git a/Morris/Morris.csproj b/Morris/Morris.csproj index b99193a..96f53c7 100644 --- a/Morris/Morris.csproj +++ b/Morris/Morris.csproj @@ -44,6 +44,7 @@ +