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 @@
+