diff --git a/Morris/AI/NegamaxAI.cs b/Morris/AI/NegamaxAI.cs
new file mode 100644
index 0000000..ce7c385
--- /dev/null
+++ b/Morris/AI/NegamaxAI.cs
@@ -0,0 +1,86 @@
+/*
+ * NegamaxAI.cs
+ * Copyright (c) 2016 Markus Himmel
+ * This file is distributed under the terms of the MIT license
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Morris.AI
+{
+ ///
+ /// Eine einfache Version des Negamax-Algorithmus mit einer primitiven Heuristik
+ ///
+ [SelectorName("Einfacher Negamax")]
+ class NegamaxAI : IMoveProvider
+ {
+ // Alle gültigen Züge, die basierend auf move einen Spielstein entfernen
+ private IEnumerable validRemoves(GameMove move, IReadOnlyGameState state)
+ {
+ bool allInMill = Enumerable.Range(0, GameState.FIELD_SIZE)
+ .Where(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent()))
+ .All(point => GameState.Mills.Any(mill => mill.Contains(point) && mill.All(mp => state.Board[mp].IsOccupiedBy(state.NextToMove.Opponent()))));
+
+ Func filter;
+ if (allInMill)
+ filter = _ => true;
+ else
+ // Wenn es Steine gibt, die in keiner Mühle sind, müssen wir einen solchen Stein entfernen
+ filter = point => GameState.Mills.All(mill => !mill.Contains(point) || mill.Any(mp => !state.Board[mp].IsOccupiedBy(state.NextToMove.Opponent())));
+
+ return Enumerable.Range(0, GameState.FIELD_SIZE)
+ .Where(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent()) && filter(point))
+ .Select(move.WithRemove);
+ }
+
+ // Alle gültigen Züge
+ private IEnumerable allMoves(IReadOnlyGameState state)
+ {
+ return state.BasicMoves()
+ .SelectMany(move => state.IsValidMove(move) == MoveValidity.ClosesMill ? validRemoves(move, state) : new[] { move });
+ }
+
+ // Primitive Negamax-Implementation nach https://en.wikipedia.org/wiki/Negamax
+ private Tuple negamax(GameState state, int depth, int color)
+ {
+ if (state.Result != GameResult.Running)
+ {
+ switch (state.Result)
+ {
+ case GameResult.WhiteVictory:
+ return Tuple.Create(10 * color, (GameMove)null);
+
+ case GameResult.BlackVictory:
+ return Tuple.Create(-10 * color, (GameMove)null);
+
+ case GameResult.Draw:
+ return Tuple.Create(0, (GameMove)null);
+ }
+ }
+ // Die Heuristik für den Base Case ist auch hier wieder sehr primitiv und lautet: Differenz in der Zahl der Spielsteine
+ if (depth == 0)
+ return Tuple.Create((state.GetCurrentStones(Player.White) - state.GetCurrentStones(Player.Black)) * color, (GameMove)null);
+
+ // Ab hier ist alles Standard, siehe Wikipedia
+ int bestValue;
+ GameMove goodMove = allMoves(state).AllMaxBy(next =>
+ {
+ // Was-wäre-wenn Analyse findet anhand von Arbeitskopien des Zustands statt
+ var newState = new GameState(state);
+ if (newState.TryApplyMove(next) != MoveResult.OK)
+ return int.MinValue;
+
+ return -negamax(newState, depth - 1, -color).Item1;
+ }, out bestValue).ToList().ChooseRandom();
+
+ return Tuple.Create(bestValue, goodMove);
+ }
+
+ public GameMove GetNextMove(IReadOnlyGameState state)
+ {
+ return negamax(new GameState(state), 4, state.NextToMove == Player.White ? 1 : -1).Item2;
+ }
+ }
+}
diff --git a/Morris/RandomBot.cs b/Morris/AI/RandomBot.cs
similarity index 85%
rename from Morris/RandomBot.cs
rename to Morris/AI/RandomBot.cs
index 70ee7dc..562aeb0 100644
--- a/Morris/RandomBot.cs
+++ b/Morris/AI/RandomBot.cs
@@ -12,6 +12,7 @@ namespace Morris
///
/// Ein extrem einfacher KI-Spieler, der einen zufälligen gültigen Spielzug auswählt
///
+ [SelectorName("Zufalls-KI")]
internal class RandomBot : IMoveProvider
{
// Anhand dieser Klasse können wir sehen, wie einfach es ist, einen Computerspieler zu implementieren.
@@ -27,6 +28,8 @@ namespace Morris
GameMove chosen = state.BasicMoves().ToList().ChooseRandom();
// Wenn wir einen Stein entfernen dürfen, wählen wir einen zufälligen Punkt aus, auf dem sich ein gegnerischer Stein befindet
+ // Anmerkung: Hier kann ein ungültiger Zug bestimmt werden, weil man z.T. nicht alle gegnerischen Steine nehmen darf, aber
+ // dann wird GetNextMove einfach noch einmal aufgerufen.
if (state.IsValidMove(chosen) == MoveValidity.ClosesMill)
return chosen.WithRemove(Enumerable
.Range(0, GameState.FIELD_SIZE)
diff --git a/Morris/AI/StupidAI.cs b/Morris/AI/StupidAI.cs
new file mode 100644
index 0000000..a80e430
--- /dev/null
+++ b/Morris/AI/StupidAI.cs
@@ -0,0 +1,68 @@
+/*
+ * StupidAI.cs
+ * Copyright (c) 2016 Markus Himmel
+ * This file is distributed under the terms of the MIT license
+ */
+
+using System;
+using System.Linq;
+
+namespace Morris
+{
+ ///
+ /// Eine sehr primitive KI, die lediglich die direkten nächsten Züge nach simplen Kriterien untersucht
+ ///
+ [SelectorName("Dumme KI")]
+ internal class StupidAI : IMoveProvider
+ {
+ private int scoreNonRemoving(GameMove move, IReadOnlyGameState state)
+ {
+ // Diese Veriablen enthalten genau das, was ihre Namen suggerieren
+ bool closesMill = GameState.Mills.Any(mill => mill.All(point => (!move.From.HasValue || point != move.From.Value) && state.Board[point].IsOccupiedBy(state.NextToMove) || point == move.To));
+ bool preventsMill = GameState.Mills.Any(mill => mill.All(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent()) || point == move.To));
+ bool opensMill = move.From.HasValue && GameState.Mills.Any(mill => mill.Contains(move.From.Value) && mill.All(point => state.Board[point].IsOccupiedBy(state.NextToMove)));
+
+ // Als "Tiebraker" dient das Kriterium, wie viele andere eigene Spielsteine sich in den potenziellen Mühlen des Zielfeldes befinden.
+ // Dieses Kriterium ist extrem schwach, weil es sehr leicht in lokalen Maxima stecken bleibt
+ // In anderen Worten ist es sehr schwierig für diese KI, nach der Setzphase noch neue Mühlen zu bilden
+ int inRange = GameState.Mills.Sum(mill => mill.Contains(move.To) ? mill.Count(point => state.Board[point].IsOccupiedBy(state.NextToMove)) : 0);
+
+ return (closesMill ? 10 : 0) + (preventsMill ? 12 : 0) + (opensMill ? 9 : 0) + inRange;
+ }
+
+ private int scoreRemove(int remove, IReadOnlyGameState state)
+ {
+ return GameState.Mills.Sum(mill => mill.Contains(remove) ? mill.Count(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent())) : 0);
+ }
+
+ public GameMove GetNextMove(IReadOnlyGameState state)
+ {
+ int ignored;
+ // Simples Prinzip: Alle Züge werden nach den Bepunktungsfunktionen bewertet, von den besten Zügen wird ein zufälliger Zug ausgewählt
+ GameMove move = state.BasicMoves().AllMaxBy(x => scoreNonRemoving(x, state), out ignored).ToList().ChooseRandom();
+
+ if (state.IsValidMove(move) == MoveValidity.ClosesMill)
+ {
+ bool allInMill = Enumerable.Range(0, GameState.FIELD_SIZE)
+ .Where(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent()))
+ .All(point => GameState.Mills.Any(mill => mill.Contains(point) && mill.All(mp => state.Board[mp].IsOccupiedBy(state.NextToMove.Opponent()))));
+
+ Func filter;
+ if (allInMill)
+ filter = _ => true;
+ else
+ // Wenn es Steine gibt, die in keiner Mühle sind, müssen wir einen solchen Stein entfernen
+ filter = point => GameState.Mills.All(mill => !mill.Contains(point) || mill.Any(mp => !state.Board[mp].IsOccupiedBy(state.NextToMove.Opponent())));
+
+ return move.WithRemove(
+ Enumerable.Range(0, GameState.FIELD_SIZE)
+ .Where(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent()) && filter(point))
+ .AllMaxBy(x => scoreRemove(x, state), out ignored)
+ .ToList().ChooseRandom()
+ );
+ }
+
+ return move;
+ }
+ }
+}
diff --git a/Morris/Controller.xaml b/Morris/Control/Controller.xaml
similarity index 88%
rename from Morris/Controller.xaml
rename to Morris/Control/Controller.xaml
index a66cc14..257624c 100644
--- a/Morris/Controller.xaml
+++ b/Morris/Control/Controller.xaml
@@ -6,8 +6,8 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Morris"
mc:Ignorable="d"
- Title="Morris" Height="332.409" Width="285.955" Closed="Window_Closed">
-
+ Title="Morris" Height="340.409" Width="454.955" Closed="Window_Closed">
+
@@ -18,6 +18,7 @@
+
diff --git a/Morris/Controller.xaml.cs b/Morris/Control/Controller.xaml.cs
similarity index 95%
rename from Morris/Controller.xaml.cs
rename to Morris/Control/Controller.xaml.cs
index cfefef7..a7e0757 100644
--- a/Morris/Controller.xaml.cs
+++ b/Morris/Control/Controller.xaml.cs
@@ -10,7 +10,6 @@ using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Reflection;
-using System.Threading;
using System.Collections.ObjectModel;
using Microsoft.Win32;
@@ -31,9 +30,8 @@ namespace Morris
displayBox.ItemsSource = displays;
}
- // Das aktuelle Spiel und der Thread, auf dem es läuft
+ // Das aktuelle Spiel
private Game theGame;
- private Thread gameThread;
// Die Objekte, die die ComboBoxen und die ListBox nehmen und wieder zurückgeben
private ObservableCollection players = new ObservableCollection();
@@ -138,8 +136,8 @@ namespace Morris
private void newGame_Click(object sender, RoutedEventArgs e)
{
// Altes Spiel terminieren
- if (gameThread != null)
- gameThread.Abort();
+ if (theGame != null)
+ theGame.Stop();
var white = getFromBox(whiteBox);
var black = getFromBox(blackBox);
@@ -148,15 +146,14 @@ namespace Morris
return;
theGame = new Game(white, black, (int)delay.Value);
+ moveBox.ItemsSource = theGame.Moves;
foreach (SelectorType type in displayBox.SelectedItems)
{
tryAddDisplay(type);
}
- gameThread = new Thread(() => theGame.Run());
- gameThread.Start();
-
+ theGame.Start();
}
private void white_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -217,5 +214,10 @@ namespace Morris
if (theGame != null)
theGame.Delay = (int)e.NewValue;
}
+
+ private void moveBox_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ theGame.RewindTo(moveBox.SelectedItem as GameMove);
+ }
}
}
diff --git a/Morris/Control/Program.cs b/Morris/Control/Program.cs
new file mode 100644
index 0000000..6849619
--- /dev/null
+++ b/Morris/Control/Program.cs
@@ -0,0 +1,47 @@
+/*
+ * Program.cs
+ * Copyright (c) 2016 Markus Himmel
+ * This file is distributed under the terms of the MIT license
+ */
+
+using System;
+using System.Windows;
+
+namespace Morris
+{
+ internal class Program
+ {
+ [STAThread]
+ static void Main(string[] args)
+ {
+ // Die Verwendung der Controller-Klasse und der anderen Klassen
+ // im Ordner Control ist für die Kernlogik des Spiels vollkommen
+ // irrelevant. Stattdessen könnten hier auch ein paar fest ein-
+ // programmierte Befehle zum Erstellen das Game-Objekts oder ein
+ // Command Line Interface oder oder oder stehen, je nachdem, wie
+ // die Logik eingesetzt wird. Die Architektur der Logik lässt es zu,
+ // die Logik (und auch die KIs) in allen möglichen Formen der
+ // Benutzerinteraktion wiederzuverwenden, sei es eine WPF-Applikation,
+ // eine Kommandozeilenanwendung, die auch auf macOS und Linux läuft,
+ // eine Xamarin Mobile App, die auf iOS und Android zuhause ist,
+ // eine ASP.NET-Webapplikation, die das Spiel im Browser spielbar
+ // macht, eine App auf der Universal Windows Platform, sodass das
+ // Spiel auf Windows Phone und Xbox One läuft, etc. etc. etc.
+ // Und das ist nur die "Spitze des Eisbergs", denn es können auch
+ // KIs und Displays eingebunden werden, die in C++/CLI verfasst sind,
+ // sprich Qt, OpenGL, etc.
+ // Allerdings sind das lediglich Möglichkeiten, die die Architektur
+ // der Software zulässt, tatsächlich vorhanden sind eine WPF-GUI
+ // zur Auswahl von KI, Display und zur Kontrolle des Spiel sowie
+ // GUIs für WPF und die Konsole sowie eine handvoll KIs, die alle
+ // recht mäßig spielen, und eine Brücke zur Mühleplattform Malom,
+ // durch die zwei weitere KIs verfügbar werden: Eine perfekte KI,
+ // die auf der vollständigen Lösung von Mühle beruht und stets
+ // das spieltheoretisch beste aus einer Spielsituation macht, die
+ // allerdings auch eine Datenbank benötigt, die groß und recht
+ // aufwändig zu berechnen ist, und eine heuristische KI, die auf
+ // Alpha-Beta-Pruning beruht.
+ new Application().Run(new Controller());
+ }
+ }
+}
diff --git a/Morris/SelectorNameAttribute.cs b/Morris/Control/SelectorNameAttribute.cs
similarity index 93%
rename from Morris/SelectorNameAttribute.cs
rename to Morris/Control/SelectorNameAttribute.cs
index e24bbd8..ae87384 100644
--- a/Morris/SelectorNameAttribute.cs
+++ b/Morris/Control/SelectorNameAttribute.cs
@@ -1,6 +1,6 @@
/*
* SelectorNameAttribute.cs
- * Copyright (c) 2016 Makrus Himmel
+ * Copyright (c) 2016 Markus Himmel
* This file is distributed un der the terms of the MIT license
*/
diff --git a/Morris/SelectorType.cs b/Morris/Control/SelectorType.cs
similarity index 100%
rename from Morris/SelectorType.cs
rename to Morris/Control/SelectorType.cs
diff --git a/Morris/SingleInstanceAttribute.cs b/Morris/Control/SingleInstanceAttribute.cs
similarity index 100%
rename from Morris/SingleInstanceAttribute.cs
rename to Morris/Control/SingleInstanceAttribute.cs
diff --git a/Morris/Core/Game.cs b/Morris/Core/Game.cs
new file mode 100644
index 0000000..1fcb3d8
--- /dev/null
+++ b/Morris/Core/Game.cs
@@ -0,0 +1,225 @@
+/*
+ * Game.cs
+ * Copyright (c) 2016 Markus Himmel
+ * This file is distributed under the terms of the MIT license
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Threading;
+using System.Windows.Data;
+
+namespace Morris
+{
+ ///
+ /// Repräsentiert ein einzelnes Mühlespiel
+ ///
+ internal class Game
+ {
+ // Alle Anzeigen
+ private List observers = new List();
+
+ // Der Spielzustand
+ private GameState state;
+
+ // Die Spieler/KIs
+ private Dictionary providers;
+
+ // Alle bisherigen Züge
+ private ObservableCollection moves = new ObservableCollection();
+ // Alle bisherigen Züge (Lesezugriff)
+ private ReadOnlyObservableCollection movesReadOnly;
+
+ // Der Thread, auf dem die Spiellogik und die Provider laufen
+ private Thread gameThread;
+
+ public ReadOnlyObservableCollection Moves
+ {
+ get
+ {
+ return movesReadOnly;
+ }
+ }
+
+ public IMoveProvider White
+ {
+ get
+ {
+ return providers[Player.White];
+ }
+ set
+ {
+ providers[Player.White] = value;
+ if (state.NextToMove == Player.White)
+ KickOver();
+ }
+ }
+
+ public IMoveProvider Black
+ {
+ get
+ {
+ return providers[Player.Black];
+ }
+ set
+ {
+ providers[Player.Black] = value;
+ if (state.NextToMove == Player.Black)
+ KickOver();
+ }
+ }
+
+ public int Delay
+ {
+ get;
+ set;
+ }
+
+ private static object movesLock = new object();
+
+ public Game(IMoveProvider white, IMoveProvider black, int delay)
+ {
+ state = new GameState();
+
+ // moves bzw. movesReadOnly ist eine ObservableCollection. Das bedeutet, dass jemand,
+ // der die Referenz zu diesem Objekt hat, sich notifizieren lassen kann, wenn sich diese
+ // geändert hat. Das ist sehr praktisch, da wir die Referenz zu movesReadOnly an die UI
+ // geben können. Dann setzen wir einfach die ItemsSource der ListBox für die Züge, und es
+ // werden automatisch immer die richtigen Züge angezeigt. Aufwand: ca. 2 Zeilen Code.
+ // Die kryptische Zeile mit EnableCollectionSynchronization ermöglicht, dass die UI
+ // auf die Änderung an movesReadOnly auch dann reagieren kann, wenn diese Änderung nicht
+ // durch den UI-Thread ausgelöst wird.
+ movesReadOnly = new ReadOnlyObservableCollection(moves);
+ BindingOperations.EnableCollectionSynchronization(movesReadOnly, movesLock);
+
+ providers = new Dictionary()
+ {
+ [Player.White] = white,
+ [Player.Black] = black
+ };
+ Delay = delay;
+ }
+
+ // Startet den Spielthread
+ public void Start()
+ {
+ if (gameThread != null)
+ return;
+
+ gameThread = new Thread(doRun);
+ gameThread.Start();
+ }
+
+ // "Have you tried turning it off and on again?"
+ // Tatsächlich ist diese Methode da, damit, falls sich der Spielthread gerade
+ // im Code eines Spielers befindet, während dieser geändert wird, die aktuelle
+ // Anfrage abgebrochen und eine neue Anfrage beim neuen Spieler gestartet wird.
+ private void KickOver()
+ {
+ Stop();
+ Start();
+ }
+
+ // Stoppt den Spielthread
+ public void Stop()
+ {
+ if (gameThread == null)
+ return;
+
+ gameThread.Abort();
+ gameThread = null;
+ }
+
+ ///
+ /// Spielt eine gesamte Runde Mühle
+ ///
+ /// Das Spielergebnis
+ private void doRun()
+ {
+ notifyOberservers();
+ MoveResult res;
+
+ // Äußere Schleife läuft einmal pro tatsächlichem Zug
+ while (state.Result == GameResult.Running)
+ {
+ // Innere Schleife läuft einmal pro Zugversuch
+ GameMove lastMove;
+ do
+ {
+ res = state.TryApplyMove(lastMove = providers[state.NextToMove].GetNextMove(state));
+ } while (res == MoveResult.InvalidMove);
+
+ notifyOberservers();
+ moves.Add(lastMove);
+
+ Thread.Sleep(Delay);
+ }
+ }
+
+ ///
+ /// Registriert einen für kommende Spielereignisse
+ ///
+ /// Das zu notifizierende Objekt
+ public void AddObserver(IGameStateObserver observer)
+ {
+ observers.Add(observer);
+ observer.Notify(state);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// Setzt das Spiel zurück auf den Zustand direkt nach move.
+ ///
+ public void RewindTo(GameMove to)
+ {
+ if (!moves.Contains(to))
+ throw new ArgumentException("Kann nur auf einen Zug zurückspulen, der Teil des Spiels ist.");
+
+ Stop();
+
+ // Entgegenden dem Namen der Methode spulen wir nicht zurück,
+ // sondern simulieren den Anfang des Spiels erneut
+ GameState newState = new GameState();
+ int numReplayed = 0;
+ foreach (var move in moves)
+ {
+ if (newState.TryApplyMove(move) != MoveResult.OK)
+ throw new InvalidOperationException("Vorheriger Zug konnte nicht nachvollzogen werden");
+
+ numReplayed++;
+
+ if (move == to)
+ break;
+ }
+ state = newState;
+
+ // Alle Züge, die jetzt nicht mehr existieren, werden gelöscht.
+ // Rückwärts, um Aufrücken zu verhindern. O(n^2) -> O(n). Nicht dass die Verbesserung messbar wäre.
+ int oldCount = moves.Count;
+ for (int i = oldCount - 1; i >= numReplayed; i--)
+ moves.RemoveAt(i);
+
+ Start();
+ }
+
+ // 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/Core/GameMove.cs
similarity index 98%
rename from Morris/GameMove.cs
rename to Morris/Core/GameMove.cs
index e7f291f..1dfb435 100644
--- a/Morris/GameMove.cs
+++ b/Morris/Core/GameMove.cs
@@ -35,7 +35,7 @@ namespace Morris
public override string ToString()
{
- return $"{(!From.HasValue ? string.Empty : CoordinateTranslator.HumanReadableFromID(From.Value) + "-")}{CoordinateTranslator.HumanReadableFromID(To)}{(Remove.HasValue ? "," + CoordinateTranslator.HumanReadableFromID(Remove.Value) : string.Empty)}";
+ return $"{(!From.HasValue ? string.Empty : CoordinateTranslator.HumanReadableFromID(From.Value) + "-")}{CoordinateTranslator.HumanReadableFromID(To)}{(Remove.HasValue ? "," + CoordinateTranslator.HumanReadableFromID(Remove.Value) : string.Empty)}".ToUpper();
}
///
diff --git a/Morris/GameResult.cs b/Morris/Core/GameResult.cs
similarity index 100%
rename from Morris/GameResult.cs
rename to Morris/Core/GameResult.cs
diff --git a/Morris/GameState.cs b/Morris/Core/GameState.cs
similarity index 87%
rename from Morris/GameState.cs
rename to Morris/Core/GameState.cs
index f39043a..bf1b6ef 100644
--- a/Morris/GameState.cs
+++ b/Morris/Core/GameState.cs
@@ -21,6 +21,7 @@ namespace Morris
public Occupation[] Board { get; private set; }
public Player NextToMove { get; private set; }
public GameResult Result { get; private set; }
+ public int MovesSinceLastStoneCountChange { get; private set; } // Tut mir leid.. mir ist kein besserer Name einfallen
private List history = new List();
@@ -31,6 +32,7 @@ namespace Morris
public const int FIELD_SIZE = 24;
public const int STONES_MAX = 9;
public const int FLYING_MAX = 3;
+ public const int UNCHANGED_MOVES_MAX = 50;
// Jeder Eintrag repräsentiert eine mögliche Mühle
public static readonly ReadOnlyCollection> Mills = Array.AsReadOnly(new[]
@@ -73,15 +75,6 @@ namespace Morris
}
}
- ///
- /// Gibt alle Felder zurück, die mit einem Feld verbunden sind
- ///
- /// Das zu untersuchende Feld
- public static IEnumerable GetConnected(int ID)
- {
- return Enumerable.Range(0, FIELD_SIZE).Where(id => connections[ID, id]);
- }
-
public GameState()
{
// Leeres Feld
@@ -106,6 +99,29 @@ namespace Morris
[Player.Black] = 0,
[Player.White] = 0
};
+
+ MovesSinceLastStoneCountChange = 0;
+ }
+
+ public GameState(IReadOnlyGameState other)
+ {
+ Board = other.Board.ToArray();
+ NextToMove = other.NextToMove;
+ Result = other.Result;
+ MovesSinceLastStoneCountChange = other.MovesSinceLastStoneCountChange;
+ Player[] players = new[] { Player.Black, Player.White };
+ playerPhase = players.ToDictionary(p => p, p => other.GetPhase(p));
+ stonesPlaced = players.ToDictionary(p => p, p => other.GetStonesPlaced(p));
+ currentStones = players.ToDictionary(p => p, p => other.GetCurrentStones(p));
+ history = other.History.Select(elem => elem.ToArray()).ToList();
+ }
+
+ public IEnumerable> History
+ {
+ get
+ {
+ return history.Select(elem => Array.AsReadOnly(elem));
+ }
}
ReadOnlyCollection IReadOnlyGameState.Board
@@ -116,6 +132,15 @@ namespace Morris
}
}
+ ///
+ /// Gibt alle Felder zurück, die mit einem Feld verbunden sind
+ ///
+ /// Das zu untersuchende Feld
+ public static IEnumerable GetConnected(int ID)
+ {
+ return Enumerable.Range(0, FIELD_SIZE).Where(id => connections[ID, id]);
+ }
+
// 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
@@ -237,7 +262,7 @@ namespace Morris
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
+ if (!Board[move.From.Value].IsOccupiedBy(NextToMove))
return MoveValidity.Invalid; // Kein Stein zum Bewegen
if (playerPhase[NextToMove] == Phase.Moving && !connections[move.From.Value, move.To])
@@ -251,7 +276,7 @@ namespace Morris
mill.Contains(move.To) && // den neu gesetzten Stein enthält und
mill.All(point => // bei der alle Punkte
(!move.From.HasValue || point != move.From) && // nicht der Ursprungspunkt der aktuellen Steinbewegung sind und
- (int)Board[point] == (int)NextToMove || point == move.To)); // entweder schon vom Spieler bestzt sind oder Ziel der aktuellen Steinbewegung sind.
+ Board[point].IsOccupiedBy(NextToMove) || point == move.To)); // entweder schon vom Spieler bestzt sind oder Ziel der aktuellen Steinbewegung sind.
// 4.: Verifikation des Mühlenparameters
if (millClosed)
@@ -262,7 +287,7 @@ namespace Morris
if (move.Remove < 0 || move.Remove >= FIELD_SIZE)
return MoveValidity.Invalid; // OOB
- if ((int)Board[move.Remove.Value] != (int)NextToMove.Opponent())
+ if (!Board[move.Remove.Value].IsOccupiedBy(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
@@ -271,10 +296,10 @@ namespace Morris
// "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())));
+ .Where(point => Board[point].IsOccupiedBy(NextToMove.Opponent()))
+ .All(point => Mills.Any(mill => mill.Contains(point) && mill.All(mp => Board[mp].IsOccupiedBy(NextToMove.Opponent()))));
- if (!allInMill && Mills.Any(mill => mill.Contains(move.Remove.Value) && mill.All(point => (int)Board[point] == (int)NextToMove.Opponent())))
+ if (!allInMill && Mills.Any(mill => mill.Contains(move.Remove.Value) && mill.All(point => Board[point].IsOccupiedBy(NextToMove.Opponent()))))
return MoveValidity.Invalid; // Versuch, einen Stein aus einer Mühle zu entfernen, obwohl Steine frei sind
}
else if (move.Remove.HasValue)
@@ -326,11 +351,15 @@ namespace Morris
playerPhase[NextToMove.Opponent()] = Phase.Flying;
}
+ // Zu lange keine
+ MovesSinceLastStoneCountChange = move.From.HasValue && !move.Remove.HasValue ? MovesSinceLastStoneCountChange + 1 : 0;
+ if (MovesSinceLastStoneCountChange == UNCHANGED_MOVES_MAX)
+ Result = GameResult.Draw;
+
// Wiederholte Stellung
if (!playerPhase.Values.All(phase => phase == Phase.Placing) && history.Any(pastBoard => Board.SequenceEqual(pastBoard)))
Result = GameResult.Draw;
-
// Gegner hat nur noch zwei Steine
if (playerPhase[NextToMove.Opponent()] != Phase.Placing && currentStones[NextToMove.Opponent()] == 2)
Result = (GameResult)NextToMove;
diff --git a/Morris/IGameStateObserver.cs b/Morris/Core/IGameStateObserver.cs
similarity index 100%
rename from Morris/IGameStateObserver.cs
rename to Morris/Core/IGameStateObserver.cs
diff --git a/Morris/IMoveProvider.cs b/Morris/Core/IMoveProvider.cs
similarity index 100%
rename from Morris/IMoveProvider.cs
rename to Morris/Core/IMoveProvider.cs
diff --git a/Morris/IReadOnlyGameState.cs b/Morris/Core/IReadOnlyGameState.cs
similarity index 89%
rename from Morris/IReadOnlyGameState.cs
rename to Morris/Core/IReadOnlyGameState.cs
index 76cea76..3ca1c57 100644
--- a/Morris/IReadOnlyGameState.cs
+++ b/Morris/Core/IReadOnlyGameState.cs
@@ -33,6 +33,15 @@ namespace Morris
///
GameResult Result { get; }
+ ///
+ /// Gibt an, wie viele Züge vergangen sind, seit sich die Zahl der Steine auf dem Spielfeld das letzte Mal verändert hat
+ ///
+ int MovesSinceLastStoneCountChange { get; }
+
+ ///
+ /// Alle Vergangenen Zustände des Spielfelds
+ ///
+ IEnumerable> History { get; }
// Methoden, die Auskunft über die Spielsituation geben
// (siehe hierzu auch den Kommentar in GameState.cs)
diff --git a/Morris/MoveResult.cs b/Morris/Core/MoveResult.cs
similarity index 100%
rename from Morris/MoveResult.cs
rename to Morris/Core/MoveResult.cs
diff --git a/Morris/MoveValidity.cs b/Morris/Core/MoveValidity.cs
similarity index 100%
rename from Morris/MoveValidity.cs
rename to Morris/Core/MoveValidity.cs
diff --git a/Morris/Occupation.cs b/Morris/Core/Occupation.cs
similarity index 100%
rename from Morris/Occupation.cs
rename to Morris/Core/Occupation.cs
diff --git a/Morris/Phase.cs b/Morris/Core/Phase.cs
similarity index 100%
rename from Morris/Phase.cs
rename to Morris/Core/Phase.cs
diff --git a/Morris/Player.cs b/Morris/Core/Player.cs
similarity index 100%
rename from Morris/Player.cs
rename to Morris/Core/Player.cs
diff --git a/Morris/ExtensionMethods.cs b/Morris/ExtensionMethods.cs
deleted file mode 100644
index 7bda728..0000000
--- a/Morris/ExtensionMethods.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Morris
-{
- public 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;
- }
-
- private static Random rng = new Random();
-
- ///
- /// Gibt ein zufälliges Element der IList zurück
- ///
- public static T ChooseRandom(this IList it)
- {
- return it[rng.Next(it.Count)];
- }
- }
-}
diff --git a/Morris/Game.cs b/Morris/Game.cs
deleted file mode 100644
index 69d136d..0000000
--- a/Morris/Game.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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
- ///
- internal class Game
- {
- private List observers = new List();
- private GameState state;
- private Dictionary providers;
-
- public IMoveProvider White
- {
- get
- {
- return providers[Player.White];
- }
- set
- {
- providers[Player.White] = value;
- }
- }
-
- public IMoveProvider Black
- {
- get
- {
- return providers[Player.Black];
- }
- set
- {
- providers[Player.Black] = value;
- }
- }
-
- public int Delay
- {
- get;
- set;
- }
-
- public Game(IMoveProvider white, IMoveProvider black, int delay)
- {
- state = new GameState();
- providers = new Dictionary()
- {
- [Player.White] = white,
- [Player.Black] = black
- };
- Delay = delay;
- }
-
- ///
- /// Spielt eine gesamte Runde Mühle
- ///
- /// Das Spielergebnis
- public GameResult Run()
- {
- 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(Delay);
- } 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);
- observer.Notify(state);
- }
-
- ///
- /// 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/Morris.csproj b/Morris/Morris.csproj
index 309f856..fdb71c6 100644
--- a/Morris/Morris.csproj
+++ b/Morris/Morris.csproj
@@ -48,43 +48,45 @@
-
-
+
+
+ Controller.xaml
-
-
-
-
-
-
+
+
+
+
+
+ GameWindow.xaml
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
+ DesignerMSBuild:Compile
-
+ DesignerMSBuild:Compile
diff --git a/Morris/Program.cs b/Morris/Program.cs
deleted file mode 100644
index e4df052..0000000
--- a/Morris/Program.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Windows;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Morris
-{
- internal class Program
- {
- [STAThread]
- static void Main(string[] args)
- {
- //var a = new ConsoleInteraction();
- //var b = new RandomBot();
- //var w = new GameWindow();
- //var g = new Game(a, b);
- //g.AddObserver(a);
- //g.AddObserver(w);
- //Task.Run(() => g.Run(0));
- //new Application().Run(w);
- new Application().Run(new Controller());
- }
- }
-}
diff --git a/Morris/ConsoleInteraction.cs b/Morris/UI/ConsoleInteraction.cs
similarity index 100%
rename from Morris/ConsoleInteraction.cs
rename to Morris/UI/ConsoleInteraction.cs
diff --git a/Morris/GameWindow.xaml b/Morris/UI/GameWindow.xaml
similarity index 100%
rename from Morris/GameWindow.xaml
rename to Morris/UI/GameWindow.xaml
diff --git a/Morris/GameWindow.xaml.cs b/Morris/UI/GameWindow.xaml.cs
similarity index 100%
rename from Morris/GameWindow.xaml.cs
rename to Morris/UI/GameWindow.xaml.cs
diff --git a/Morris/CoordinateTranslator.cs b/Morris/Util/CoordinateTranslator.cs
similarity index 100%
rename from Morris/CoordinateTranslator.cs
rename to Morris/Util/CoordinateTranslator.cs
diff --git a/Morris/Util/ExtensionMethods.cs b/Morris/Util/ExtensionMethods.cs
new file mode 100644
index 0000000..49fc5e8
--- /dev/null
+++ b/Morris/Util/ExtensionMethods.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Morris
+{
+ public 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;
+ }
+
+ ///
+ /// Gibt an, ob ein Feld von einem bestimmten Spieler besetzt ist
+ ///
+ public static bool IsOccupiedBy(this Occupation o, Player p)
+ {
+ return o == (Occupation)p;
+ }
+
+ private static Random rng = new Random();
+
+ ///
+ /// Gibt ein zufälliges Element der IList zurück
+ ///
+ public static T ChooseRandom(this IList it)
+ {
+ if (it == null)
+ throw new ArgumentNullException(nameof(it));
+
+ return it[rng.Next(it.Count)];
+ }
+
+ ///
+ /// Gibt alle Element in input zurück, für die der Wert, der selector zurückgibt, laut comparer maximal ist
+ ///
+ public static IEnumerable AllMaxBy(this IEnumerable input, Func selector, out TCompare finalMax, IComparer comparer = null)
+ {
+ if (input == null)
+ throw new ArgumentNullException(nameof(input));
+
+ if (selector == null)
+ throw new ArgumentNullException(nameof(input));
+
+ comparer = comparer ?? Comparer.Default;
+
+ List collector = new List(); // Enthält alle Elemente, die den höchsten gesehenen Wert von selector(element) aufweisen
+ bool hasMax = false; // Ob wir bereits überhaupt ein Element gesehen haben und daher den Wert von max verwenden können
+ TCompare max = default(TCompare); // Der höchste gesehene Wert von selector(element)
+
+ foreach (T element in input)
+ {
+ TCompare current = selector(element);
+ int comparisonResult = comparer.Compare(current, max);
+ if (!hasMax || comparisonResult > 0)
+ {
+ // Es gibt einen neuen maximalen Wert von selector(element)
+ hasMax = true;
+ max = current;
+ collector.Clear();
+ collector.Add(element);
+ }
+ else if (comparisonResult == 0)
+ collector.Add(element);
+ }
+ finalMax = max;
+ return collector;
+ }
+ }
+}