Many changes

This commit is contained in:
Markus Himmel
2016-08-31 00:56:00 +02:00
parent ec1f8f803e
commit 54c849eeb4
30 changed files with 604 additions and 228 deletions

86
Morris/AI/NegamaxAI.cs Normal file
View File

@@ -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
{
/// <summary>
/// Eine einfache Version des Negamax-Algorithmus mit einer primitiven Heuristik
/// </summary>
[SelectorName("Einfacher Negamax")]
class NegamaxAI : IMoveProvider
{
// Alle gültigen Züge, die basierend auf move einen Spielstein entfernen
private IEnumerable<GameMove> 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<int, bool> 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<GameMove> 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<int, GameMove> 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;
}
}
}

View File

@@ -12,6 +12,7 @@ namespace Morris
/// <summary>
/// Ein extrem einfacher KI-Spieler, der einen zufälligen gültigen Spielzug auswählt
/// </summary>
[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)

68
Morris/AI/StupidAI.cs Normal file
View File

@@ -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
{
/// <summary>
/// Eine sehr primitive KI, die lediglich die direkten nächsten Züge nach simplen Kriterien untersucht
/// </summary>
[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<int, bool> 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;
}
}
}

View File

@@ -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">
<Grid>
Title="Morris" Height="340.409" Width="454.955" Closed="Window_Closed">
<Grid Margin="0,0,-8,-7">
<ComboBox x:Name="whiteBox" HorizontalAlignment="Left" Margin="54,35,0,0" VerticalAlignment="Top" Width="194" SelectionChanged="white_SelectionChanged"/>
<ComboBox x:Name="blackBox" HorizontalAlignment="Left" Margin="71,62,0,0" VerticalAlignment="Top" Width="177" SelectionChanged="black_SelectionChanged"/>
<Label Content="Weiß:" HorizontalAlignment="Left" Margin="10,35,0,0" VerticalAlignment="Top"/>
@@ -18,6 +18,7 @@
<Button x:Name="loadAssembly" Content="Assembly laden..." HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="109" Click="loadAssembly_Click"/>
<Slider x:Name="delay" HorizontalAlignment="Left" Margin="10,254,0,0" VerticalAlignment="Top" Width="238" Maximum="2000" SmallChange="1" TickFrequency="100" TickPlacement="BottomRight" ValueChanged="delay_ValueChanged"/>
<Label x:Name="label1" Content="Verzögerung:" HorizontalAlignment="Left" Margin="10,228,0,0" VerticalAlignment="Top"/>
<ListBox x:Name="moveBox" HorizontalAlignment="Left" Height="268" Margin="253,10,0,0" VerticalAlignment="Top" Width="170" MouseDoubleClick="moveBox_MouseDoubleClick"/>
</Grid>
</Window>

View File

@@ -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<SelectorType> players = new ObservableCollection<SelectorType>();
@@ -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);
}
}
}

47
Morris/Control/Program.cs Normal file
View File

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

View File

@@ -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
*/

225
Morris/Core/Game.cs Normal file
View File

@@ -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
{
/// <summary>
/// Repräsentiert ein einzelnes Mühlespiel
/// </summary>
internal class Game
{
// Alle Anzeigen
private List<IGameStateObserver> observers = new List<IGameStateObserver>();
// Der Spielzustand
private GameState state;
// Die Spieler/KIs
private Dictionary<Player, IMoveProvider> providers;
// Alle bisherigen Züge
private ObservableCollection<GameMove> moves = new ObservableCollection<GameMove>();
// Alle bisherigen Züge (Lesezugriff)
private ReadOnlyObservableCollection<GameMove> movesReadOnly;
// Der Thread, auf dem die Spiellogik und die Provider laufen
private Thread gameThread;
public ReadOnlyObservableCollection<GameMove> 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<GameMove>(moves);
BindingOperations.EnableCollectionSynchronization(movesReadOnly, movesLock);
providers = new Dictionary<Player, IMoveProvider>()
{
[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;
}
/// <summary>
/// Spielt eine gesamte Runde Mühle
/// </summary>
/// <returns>Das Spielergebnis</returns>
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);
}
}
/// <summary>
/// Registriert einen <see cref="IGameStateObserver"/> für kommende Spielereignisse
/// </summary>
/// <param name="observer">Das zu notifizierende Objekt</param>
public void AddObserver(IGameStateObserver observer)
{
observers.Add(observer);
observer.Notify(state);
}
/// <summary>
/// Meldet einen <see cref="IGameStateObserver"/> von kommenden Spielereignissen ab
/// </summary>
/// <param name="observer">Das abzumeldende Objekt</param>
/// <returns>Wahr, wenn das Objekt tatsächlich entfernt wurde.
/// Falsch, wenn es nicht gefunden wurde.</returns>
public bool RemoveObserver(IGameStateObserver observer)
{
return observers.Remove(observer);
}
/// <summary>
/// Setzt das Spiel zurück auf den Zustand direkt nach move.
/// </summary>
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);
}
}
}
}

View File

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

View File

@@ -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<Occupation[]> history = new List<Occupation[]>();
@@ -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<ReadOnlyCollection<int>> Mills = Array.AsReadOnly(new[]
@@ -73,15 +75,6 @@ namespace Morris
}
}
/// <summary>
/// Gibt alle Felder zurück, die mit einem Feld verbunden sind
/// </summary>
/// <param name="ID">Das zu untersuchende Feld</param>
public static IEnumerable<int> 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<ReadOnlyCollection<Occupation>> History
{
get
{
return history.Select(elem => Array.AsReadOnly(elem));
}
}
ReadOnlyCollection<Occupation> IReadOnlyGameState.Board
@@ -116,6 +132,15 @@ namespace Morris
}
}
/// <summary>
/// Gibt alle Felder zurück, die mit einem Feld verbunden sind
/// </summary>
/// <param name="ID">Das zu untersuchende Feld</param>
public static IEnumerable<int> 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;

View File

@@ -33,6 +33,15 @@ namespace Morris
/// </summary>
GameResult Result { get; }
/// <summary>
/// Gibt an, wie viele Züge vergangen sind, seit sich die Zahl der Steine auf dem Spielfeld das letzte Mal verändert hat
/// </summary>
int MovesSinceLastStoneCountChange { get; }
/// <summary>
/// Alle Vergangenen Zustände des Spielfelds
/// </summary>
IEnumerable<ReadOnlyCollection<Occupation>> History { get; }
// Methoden, die Auskunft über die Spielsituation geben
// (siehe hierzu auch den Kommentar in GameState.cs)

View File

@@ -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
{
/// <summary>
/// Gibt den Gegner des Spielers zurück
/// </summary>
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();
/// <summary>
/// Gibt ein zufälliges Element der IList zurück
/// </summary>
public static T ChooseRandom<T>(this IList<T> it)
{
return it[rng.Next(it.Count)];
}
}
}

View File

@@ -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
{
/// <summary>
/// Repräsentiert ein einzelnes Mühlespiel
/// </summary>
internal class Game
{
private List<IGameStateObserver> observers = new List<IGameStateObserver>();
private GameState state;
private Dictionary<Player, IMoveProvider> 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, IMoveProvider>()
{
[Player.White] = white,
[Player.Black] = black
};
Delay = delay;
}
/// <summary>
/// Spielt eine gesamte Runde Mühle
/// </summary>
/// <returns>Das Spielergebnis</returns>
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;
}
/// <summary>
/// Registriert einen <see cref="IGameStateObserver"/> für kommende Spielereignisse
/// </summary>
/// <param name="observer">Das zu notifizierende Objekt</param>
public void AddObserver(IGameStateObserver observer)
{
observers.Add(observer);
observer.Notify(state);
}
/// <summary>
/// Meldet einen <see cref="IGameStateObserver"/> von kommenden Spielereignissen ab
/// </summary>
/// <param name="observer">Das abzumeldende Objekt</param>
/// <returns>Wahr, wenn das Objekt tatsächlich entfernt wurde.
/// Falsch, wenn es nicht gefunden wurde.</returns>
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);
}
}
}
}

View File

@@ -48,43 +48,45 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="ConsoleInteraction.cs" />
<Compile Include="Controller.xaml.cs">
<Compile Include="AI\NegamaxAI.cs" />
<Compile Include="UI\ConsoleInteraction.cs" />
<Compile Include="Control\Controller.xaml.cs">
<DependentUpon>Controller.xaml</DependentUpon>
</Compile>
<Compile Include="CoordinateTranslator.cs" />
<Compile Include="ExtensionMethods.cs" />
<Compile Include="Game.cs" />
<Compile Include="GameResult.cs" />
<Compile Include="GameState.cs" />
<Compile Include="GameWindow.xaml.cs">
<Compile Include="Util\CoordinateTranslator.cs" />
<Compile Include="Util\ExtensionMethods.cs" />
<Compile Include="Core\Game.cs" />
<Compile Include="Core\GameResult.cs" />
<Compile Include="Core\GameState.cs" />
<Compile Include="UI\GameWindow.xaml.cs">
<DependentUpon>GameWindow.xaml</DependentUpon>
</Compile>
<Compile Include="RandomBot.cs" />
<Compile Include="IGameStateObserver.cs" />
<Compile Include="IMoveProvider.cs" />
<Compile Include="GameMove.cs" />
<Compile Include="IReadOnlyGameState.cs" />
<Compile Include="MoveResult.cs" />
<Compile Include="MoveValidity.cs" />
<Compile Include="Occupation.cs" />
<Compile Include="Phase.cs" />
<Compile Include="Player.cs" />
<Compile Include="Program.cs" />
<Compile Include="AI\RandomBot.cs" />
<Compile Include="Core\IGameStateObserver.cs" />
<Compile Include="Core\IMoveProvider.cs" />
<Compile Include="Core\GameMove.cs" />
<Compile Include="Core\IReadOnlyGameState.cs" />
<Compile Include="Core\MoveResult.cs" />
<Compile Include="Core\MoveValidity.cs" />
<Compile Include="Core\Occupation.cs" />
<Compile Include="Core\Phase.cs" />
<Compile Include="Core\Player.cs" />
<Compile Include="Control\Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SelectorType.cs" />
<Compile Include="SelectorNameAttribute.cs" />
<Compile Include="SingleInstanceAttribute.cs" />
<Compile Include="Control\SelectorType.cs" />
<Compile Include="Control\SelectorNameAttribute.cs" />
<Compile Include="Control\SingleInstanceAttribute.cs" />
<Compile Include="AI\StupidAI.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<Page Include="Controller.xaml">
<Page Include="Control\Controller.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="GameWindow.xaml">
<Page Include="UI\GameWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>

View File

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

View File

@@ -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
{
/// <summary>
/// Gibt den Gegner des Spielers zurück
/// </summary>
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;
}
/// <summary>
/// Gibt an, ob ein Feld von einem bestimmten Spieler besetzt ist
/// </summary>
public static bool IsOccupiedBy(this Occupation o, Player p)
{
return o == (Occupation)p;
}
private static Random rng = new Random();
/// <summary>
/// Gibt ein zufälliges Element der IList zurück
/// </summary>
public static T ChooseRandom<T>(this IList<T> it)
{
if (it == null)
throw new ArgumentNullException(nameof(it));
return it[rng.Next(it.Count)];
}
/// <summary>
/// Gibt alle Element in input zurück, für die der Wert, der selector zurückgibt, laut comparer maximal ist
/// </summary>
public static IEnumerable<T> AllMaxBy<T, TCompare>(this IEnumerable<T> input, Func<T, TCompare> selector, out TCompare finalMax, IComparer<TCompare> comparer = null)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
if (selector == null)
throw new ArgumentNullException(nameof(input));
comparer = comparer ?? Comparer<TCompare>.Default;
List<T> collector = new List<T>(); // 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;
}
}
}