mirror of
https://github.com/TwoFX/Morris.git
synced 2025-12-13 08:22:51 +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>
|
||||
/// 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
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: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>
|
||||
@@ -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
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
|
||||
* 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
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()
|
||||
{
|
||||
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>
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
@@ -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" />
|
||||
</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>
|
||||
|
||||
@@ -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