mirror of
https://github.com/TwoFX/Morris.git
synced 2026-02-04 13:02:52 +00:00
Many changes
This commit is contained in:
86
Morris/AI/NegamaxAI.cs
Normal file
86
Morris/AI/NegamaxAI.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ namespace Morris
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ein extrem einfacher KI-Spieler, der einen zufälligen gültigen Spielzug auswählt
|
/// Ein extrem einfacher KI-Spieler, der einen zufälligen gültigen Spielzug auswählt
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[SelectorName("Zufalls-KI")]
|
||||||
internal class RandomBot : IMoveProvider
|
internal class RandomBot : IMoveProvider
|
||||||
{
|
{
|
||||||
// Anhand dieser Klasse können wir sehen, wie einfach es ist, einen Computerspieler zu implementieren.
|
// 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();
|
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
|
// 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)
|
if (state.IsValidMove(chosen) == MoveValidity.ClosesMill)
|
||||||
return chosen.WithRemove(Enumerable
|
return chosen.WithRemove(Enumerable
|
||||||
.Range(0, GameState.FIELD_SIZE)
|
.Range(0, GameState.FIELD_SIZE)
|
||||||
68
Morris/AI/StupidAI.cs
Normal file
68
Morris/AI/StupidAI.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:Morris"
|
xmlns:local="clr-namespace:Morris"
|
||||||
mc:Ignorable="d"
|
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">
|
||||||
<Grid>
|
<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="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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
@@ -10,7 +10,6 @@ using System.Linq;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
|
|
||||||
@@ -31,9 +30,8 @@ namespace Morris
|
|||||||
displayBox.ItemsSource = displays;
|
displayBox.ItemsSource = displays;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Das aktuelle Spiel und der Thread, auf dem es läuft
|
// Das aktuelle Spiel
|
||||||
private Game theGame;
|
private Game theGame;
|
||||||
private Thread gameThread;
|
|
||||||
|
|
||||||
// Die Objekte, die die ComboBoxen und die ListBox nehmen und wieder zurückgeben
|
// Die Objekte, die die ComboBoxen und die ListBox nehmen und wieder zurückgeben
|
||||||
private ObservableCollection<SelectorType> players = new ObservableCollection<SelectorType>();
|
private ObservableCollection<SelectorType> players = new ObservableCollection<SelectorType>();
|
||||||
@@ -138,8 +136,8 @@ namespace Morris
|
|||||||
private void newGame_Click(object sender, RoutedEventArgs e)
|
private void newGame_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
// Altes Spiel terminieren
|
// Altes Spiel terminieren
|
||||||
if (gameThread != null)
|
if (theGame != null)
|
||||||
gameThread.Abort();
|
theGame.Stop();
|
||||||
|
|
||||||
var white = getFromBox(whiteBox);
|
var white = getFromBox(whiteBox);
|
||||||
var black = getFromBox(blackBox);
|
var black = getFromBox(blackBox);
|
||||||
@@ -148,15 +146,14 @@ namespace Morris
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
theGame = new Game(white, black, (int)delay.Value);
|
theGame = new Game(white, black, (int)delay.Value);
|
||||||
|
moveBox.ItemsSource = theGame.Moves;
|
||||||
|
|
||||||
foreach (SelectorType type in displayBox.SelectedItems)
|
foreach (SelectorType type in displayBox.SelectedItems)
|
||||||
{
|
{
|
||||||
tryAddDisplay(type);
|
tryAddDisplay(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
gameThread = new Thread(() => theGame.Run());
|
theGame.Start();
|
||||||
gameThread.Start();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void white_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void white_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
@@ -217,5 +214,10 @@ namespace Morris
|
|||||||
if (theGame != null)
|
if (theGame != null)
|
||||||
theGame.Delay = (int)e.NewValue;
|
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
47
Morris/Control/Program.cs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SelectorNameAttribute.cs
|
* SelectorNameAttribute.cs
|
||||||
* Copyright (c) 2016 Makrus Himmel
|
* Copyright (c) 2016 Markus Himmel
|
||||||
* This file is distributed un der the terms of the MIT license
|
* This file is distributed un der the terms of the MIT license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
225
Morris/Core/Game.cs
Normal file
225
Morris/Core/Game.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ namespace Morris
|
|||||||
|
|
||||||
public override string ToString()
|
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>
|
/// <summary>
|
||||||
@@ -21,6 +21,7 @@ namespace Morris
|
|||||||
public Occupation[] Board { get; private set; }
|
public Occupation[] Board { get; private set; }
|
||||||
public Player NextToMove { get; private set; }
|
public Player NextToMove { get; private set; }
|
||||||
public GameResult Result { 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[]>();
|
private List<Occupation[]> history = new List<Occupation[]>();
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ namespace Morris
|
|||||||
public const int FIELD_SIZE = 24;
|
public const int FIELD_SIZE = 24;
|
||||||
public const int STONES_MAX = 9;
|
public const int STONES_MAX = 9;
|
||||||
public const int FLYING_MAX = 3;
|
public const int FLYING_MAX = 3;
|
||||||
|
public const int UNCHANGED_MOVES_MAX = 50;
|
||||||
|
|
||||||
// Jeder Eintrag repräsentiert eine mögliche Mühle
|
// Jeder Eintrag repräsentiert eine mögliche Mühle
|
||||||
public static readonly ReadOnlyCollection<ReadOnlyCollection<int>> Mills = Array.AsReadOnly(new[]
|
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()
|
public GameState()
|
||||||
{
|
{
|
||||||
// Leeres Feld
|
// Leeres Feld
|
||||||
@@ -106,6 +99,29 @@ namespace Morris
|
|||||||
[Player.Black] = 0,
|
[Player.Black] = 0,
|
||||||
[Player.White] = 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
|
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 folgenden drei Methoden existieren, weil selbst eine Setter-Only Property,
|
||||||
// die die zugrundeliegenden Dictionaries zurückgibt, modifizierbar wäre.
|
// die die zugrundeliegenden Dictionaries zurückgibt, modifizierbar wäre.
|
||||||
// IReadOnlyDictionary ist keine Lösung, weil es Nutzer dieser Klasse eigentlich
|
// 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)
|
if (move.From < 0 || move.From >= FIELD_SIZE)
|
||||||
return MoveValidity.Invalid; // OOB
|
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
|
return MoveValidity.Invalid; // Kein Stein zum Bewegen
|
||||||
|
|
||||||
if (playerPhase[NextToMove] == Phase.Moving && !connections[move.From.Value, move.To])
|
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.Contains(move.To) && // den neu gesetzten Stein enthält und
|
||||||
mill.All(point => // bei der alle Punkte
|
mill.All(point => // bei der alle Punkte
|
||||||
(!move.From.HasValue || point != move.From) && // nicht der Ursprungspunkt der aktuellen Steinbewegung sind und
|
(!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
|
// 4.: Verifikation des Mühlenparameters
|
||||||
if (millClosed)
|
if (millClosed)
|
||||||
@@ -262,7 +287,7 @@ namespace Morris
|
|||||||
if (move.Remove < 0 || move.Remove >= FIELD_SIZE)
|
if (move.Remove < 0 || move.Remove >= FIELD_SIZE)
|
||||||
return MoveValidity.Invalid; // OOB
|
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
|
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
|
// 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
|
// "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)"
|
// Felder durch gegnerische Steine besetzt sind (die Mühle also geschlossen ist)"
|
||||||
bool allInMill = Enumerable.Range(0, FIELD_SIZE)
|
bool allInMill = Enumerable.Range(0, FIELD_SIZE)
|
||||||
.Where(point => (int)Board[point] == (int)NextToMove.Opponent())
|
.Where(point => Board[point].IsOccupiedBy(NextToMove.Opponent()))
|
||||||
.All(point => Mills.Any(mill => mill.Contains(point) && mill.All(mp => (int)Board[point] == (int)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
|
return MoveValidity.Invalid; // Versuch, einen Stein aus einer Mühle zu entfernen, obwohl Steine frei sind
|
||||||
}
|
}
|
||||||
else if (move.Remove.HasValue)
|
else if (move.Remove.HasValue)
|
||||||
@@ -326,11 +351,15 @@ namespace Morris
|
|||||||
playerPhase[NextToMove.Opponent()] = Phase.Flying;
|
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
|
// Wiederholte Stellung
|
||||||
if (!playerPhase.Values.All(phase => phase == Phase.Placing) && history.Any(pastBoard => Board.SequenceEqual(pastBoard)))
|
if (!playerPhase.Values.All(phase => phase == Phase.Placing) && history.Any(pastBoard => Board.SequenceEqual(pastBoard)))
|
||||||
Result = GameResult.Draw;
|
Result = GameResult.Draw;
|
||||||
|
|
||||||
|
|
||||||
// Gegner hat nur noch zwei Steine
|
// Gegner hat nur noch zwei Steine
|
||||||
if (playerPhase[NextToMove.Opponent()] != Phase.Placing && currentStones[NextToMove.Opponent()] == 2)
|
if (playerPhase[NextToMove.Opponent()] != Phase.Placing && currentStones[NextToMove.Opponent()] == 2)
|
||||||
Result = (GameResult)NextToMove;
|
Result = (GameResult)NextToMove;
|
||||||
@@ -33,6 +33,15 @@ namespace Morris
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
GameResult Result { get; }
|
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
|
// Methoden, die Auskunft über die Spielsituation geben
|
||||||
// (siehe hierzu auch den Kommentar in GameState.cs)
|
// (siehe hierzu auch den Kommentar in GameState.cs)
|
||||||
@@ -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)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
117
Morris/Game.cs
117
Morris/Game.cs
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -48,43 +48,45 @@
|
|||||||
<Reference Include="WindowsBase" />
|
<Reference Include="WindowsBase" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="ConsoleInteraction.cs" />
|
<Compile Include="AI\NegamaxAI.cs" />
|
||||||
<Compile Include="Controller.xaml.cs">
|
<Compile Include="UI\ConsoleInteraction.cs" />
|
||||||
|
<Compile Include="Control\Controller.xaml.cs">
|
||||||
<DependentUpon>Controller.xaml</DependentUpon>
|
<DependentUpon>Controller.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="CoordinateTranslator.cs" />
|
<Compile Include="Util\CoordinateTranslator.cs" />
|
||||||
<Compile Include="ExtensionMethods.cs" />
|
<Compile Include="Util\ExtensionMethods.cs" />
|
||||||
<Compile Include="Game.cs" />
|
<Compile Include="Core\Game.cs" />
|
||||||
<Compile Include="GameResult.cs" />
|
<Compile Include="Core\GameResult.cs" />
|
||||||
<Compile Include="GameState.cs" />
|
<Compile Include="Core\GameState.cs" />
|
||||||
<Compile Include="GameWindow.xaml.cs">
|
<Compile Include="UI\GameWindow.xaml.cs">
|
||||||
<DependentUpon>GameWindow.xaml</DependentUpon>
|
<DependentUpon>GameWindow.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="RandomBot.cs" />
|
<Compile Include="AI\RandomBot.cs" />
|
||||||
<Compile Include="IGameStateObserver.cs" />
|
<Compile Include="Core\IGameStateObserver.cs" />
|
||||||
<Compile Include="IMoveProvider.cs" />
|
<Compile Include="Core\IMoveProvider.cs" />
|
||||||
<Compile Include="GameMove.cs" />
|
<Compile Include="Core\GameMove.cs" />
|
||||||
<Compile Include="IReadOnlyGameState.cs" />
|
<Compile Include="Core\IReadOnlyGameState.cs" />
|
||||||
<Compile Include="MoveResult.cs" />
|
<Compile Include="Core\MoveResult.cs" />
|
||||||
<Compile Include="MoveValidity.cs" />
|
<Compile Include="Core\MoveValidity.cs" />
|
||||||
<Compile Include="Occupation.cs" />
|
<Compile Include="Core\Occupation.cs" />
|
||||||
<Compile Include="Phase.cs" />
|
<Compile Include="Core\Phase.cs" />
|
||||||
<Compile Include="Player.cs" />
|
<Compile Include="Core\Player.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Control\Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="SelectorType.cs" />
|
<Compile Include="Control\SelectorType.cs" />
|
||||||
<Compile Include="SelectorNameAttribute.cs" />
|
<Compile Include="Control\SelectorNameAttribute.cs" />
|
||||||
<Compile Include="SingleInstanceAttribute.cs" />
|
<Compile Include="Control\SingleInstanceAttribute.cs" />
|
||||||
|
<Compile Include="AI\StupidAI.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="App.config" />
|
<None Include="App.config" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Page Include="Controller.xaml">
|
<Page Include="Control\Controller.xaml">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</Page>
|
</Page>
|
||||||
<Page Include="GameWindow.xaml">
|
<Page Include="UI\GameWindow.xaml">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80
Morris/Util/ExtensionMethods.cs
Normal file
80
Morris/Util/ExtensionMethods.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user