From 54c849eeb4534b6d81bdf1371445809e41ab2f94 Mon Sep 17 00:00:00 2001 From: Markus Himmel Date: Wed, 31 Aug 2016 00:56:00 +0200 Subject: [PATCH] Many changes --- Morris/AI/NegamaxAI.cs | 86 +++++++ Morris/{ => AI}/RandomBot.cs | 3 + Morris/AI/StupidAI.cs | 68 ++++++ Morris/{ => Control}/Controller.xaml | 5 +- Morris/{ => Control}/Controller.xaml.cs | 18 +- Morris/Control/Program.cs | 47 ++++ Morris/{ => Control}/SelectorNameAttribute.cs | 2 +- Morris/{ => Control}/SelectorType.cs | 0 .../{ => Control}/SingleInstanceAttribute.cs | 0 Morris/Core/Game.cs | 225 ++++++++++++++++++ Morris/{ => Core}/GameMove.cs | 2 +- Morris/{ => Core}/GameResult.cs | 0 Morris/{ => Core}/GameState.cs | 61 +++-- Morris/{ => Core}/IGameStateObserver.cs | 0 Morris/{ => Core}/IMoveProvider.cs | 0 Morris/{ => Core}/IReadOnlyGameState.cs | 9 + Morris/{ => Core}/MoveResult.cs | 0 Morris/{ => Core}/MoveValidity.cs | 0 Morris/{ => Core}/Occupation.cs | 0 Morris/{ => Core}/Phase.cs | 0 Morris/{ => Core}/Player.cs | 0 Morris/ExtensionMethods.cs | 33 --- Morris/Game.cs | 117 --------- Morris/Morris.csproj | 50 ++-- Morris/Program.cs | 26 -- Morris/{ => UI}/ConsoleInteraction.cs | 0 Morris/{ => UI}/GameWindow.xaml | 0 Morris/{ => UI}/GameWindow.xaml.cs | 0 Morris/{ => Util}/CoordinateTranslator.cs | 0 Morris/Util/ExtensionMethods.cs | 80 +++++++ 30 files changed, 604 insertions(+), 228 deletions(-) create mode 100644 Morris/AI/NegamaxAI.cs rename Morris/{ => AI}/RandomBot.cs (85%) create mode 100644 Morris/AI/StupidAI.cs rename Morris/{ => Control}/Controller.xaml (88%) rename Morris/{ => Control}/Controller.xaml.cs (95%) create mode 100644 Morris/Control/Program.cs rename Morris/{ => Control}/SelectorNameAttribute.cs (93%) rename Morris/{ => Control}/SelectorType.cs (100%) rename Morris/{ => Control}/SingleInstanceAttribute.cs (100%) create mode 100644 Morris/Core/Game.cs rename Morris/{ => Core}/GameMove.cs (98%) rename Morris/{ => Core}/GameResult.cs (100%) rename Morris/{ => Core}/GameState.cs (87%) rename Morris/{ => Core}/IGameStateObserver.cs (100%) rename Morris/{ => Core}/IMoveProvider.cs (100%) rename Morris/{ => Core}/IReadOnlyGameState.cs (89%) rename Morris/{ => Core}/MoveResult.cs (100%) rename Morris/{ => Core}/MoveValidity.cs (100%) rename Morris/{ => Core}/Occupation.cs (100%) rename Morris/{ => Core}/Phase.cs (100%) rename Morris/{ => Core}/Player.cs (100%) delete mode 100644 Morris/ExtensionMethods.cs delete mode 100644 Morris/Game.cs delete mode 100644 Morris/Program.cs rename Morris/{ => UI}/ConsoleInteraction.cs (100%) rename Morris/{ => UI}/GameWindow.xaml (100%) rename Morris/{ => UI}/GameWindow.xaml.cs (100%) rename Morris/{ => Util}/CoordinateTranslator.cs (100%) create mode 100644 Morris/Util/ExtensionMethods.cs diff --git a/Morris/AI/NegamaxAI.cs b/Morris/AI/NegamaxAI.cs new file mode 100644 index 0000000..ce7c385 --- /dev/null +++ b/Morris/AI/NegamaxAI.cs @@ -0,0 +1,86 @@ +/* + * NegamaxAI.cs + * Copyright (c) 2016 Markus Himmel + * This file is distributed under the terms of the MIT license + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Morris.AI +{ + /// + /// Eine einfache Version des Negamax-Algorithmus mit einer primitiven Heuristik + /// + [SelectorName("Einfacher Negamax")] + class NegamaxAI : IMoveProvider + { + // Alle gültigen Züge, die basierend auf move einen Spielstein entfernen + private IEnumerable validRemoves(GameMove move, IReadOnlyGameState state) + { + bool allInMill = Enumerable.Range(0, GameState.FIELD_SIZE) + .Where(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent())) + .All(point => GameState.Mills.Any(mill => mill.Contains(point) && mill.All(mp => state.Board[mp].IsOccupiedBy(state.NextToMove.Opponent())))); + + Func filter; + if (allInMill) + filter = _ => true; + else + // Wenn es Steine gibt, die in keiner Mühle sind, müssen wir einen solchen Stein entfernen + filter = point => GameState.Mills.All(mill => !mill.Contains(point) || mill.Any(mp => !state.Board[mp].IsOccupiedBy(state.NextToMove.Opponent()))); + + return Enumerable.Range(0, GameState.FIELD_SIZE) + .Where(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent()) && filter(point)) + .Select(move.WithRemove); + } + + // Alle gültigen Züge + private IEnumerable allMoves(IReadOnlyGameState state) + { + return state.BasicMoves() + .SelectMany(move => state.IsValidMove(move) == MoveValidity.ClosesMill ? validRemoves(move, state) : new[] { move }); + } + + // Primitive Negamax-Implementation nach https://en.wikipedia.org/wiki/Negamax + private Tuple negamax(GameState state, int depth, int color) + { + if (state.Result != GameResult.Running) + { + switch (state.Result) + { + case GameResult.WhiteVictory: + return Tuple.Create(10 * color, (GameMove)null); + + case GameResult.BlackVictory: + return Tuple.Create(-10 * color, (GameMove)null); + + case GameResult.Draw: + return Tuple.Create(0, (GameMove)null); + } + } + // Die Heuristik für den Base Case ist auch hier wieder sehr primitiv und lautet: Differenz in der Zahl der Spielsteine + if (depth == 0) + return Tuple.Create((state.GetCurrentStones(Player.White) - state.GetCurrentStones(Player.Black)) * color, (GameMove)null); + + // Ab hier ist alles Standard, siehe Wikipedia + int bestValue; + GameMove goodMove = allMoves(state).AllMaxBy(next => + { + // Was-wäre-wenn Analyse findet anhand von Arbeitskopien des Zustands statt + var newState = new GameState(state); + if (newState.TryApplyMove(next) != MoveResult.OK) + return int.MinValue; + + return -negamax(newState, depth - 1, -color).Item1; + }, out bestValue).ToList().ChooseRandom(); + + return Tuple.Create(bestValue, goodMove); + } + + public GameMove GetNextMove(IReadOnlyGameState state) + { + return negamax(new GameState(state), 4, state.NextToMove == Player.White ? 1 : -1).Item2; + } + } +} diff --git a/Morris/RandomBot.cs b/Morris/AI/RandomBot.cs similarity index 85% rename from Morris/RandomBot.cs rename to Morris/AI/RandomBot.cs index 70ee7dc..562aeb0 100644 --- a/Morris/RandomBot.cs +++ b/Morris/AI/RandomBot.cs @@ -12,6 +12,7 @@ namespace Morris /// /// Ein extrem einfacher KI-Spieler, der einen zufälligen gültigen Spielzug auswählt /// + [SelectorName("Zufalls-KI")] internal class RandomBot : IMoveProvider { // Anhand dieser Klasse können wir sehen, wie einfach es ist, einen Computerspieler zu implementieren. @@ -27,6 +28,8 @@ namespace Morris GameMove chosen = state.BasicMoves().ToList().ChooseRandom(); // Wenn wir einen Stein entfernen dürfen, wählen wir einen zufälligen Punkt aus, auf dem sich ein gegnerischer Stein befindet + // Anmerkung: Hier kann ein ungültiger Zug bestimmt werden, weil man z.T. nicht alle gegnerischen Steine nehmen darf, aber + // dann wird GetNextMove einfach noch einmal aufgerufen. if (state.IsValidMove(chosen) == MoveValidity.ClosesMill) return chosen.WithRemove(Enumerable .Range(0, GameState.FIELD_SIZE) diff --git a/Morris/AI/StupidAI.cs b/Morris/AI/StupidAI.cs new file mode 100644 index 0000000..a80e430 --- /dev/null +++ b/Morris/AI/StupidAI.cs @@ -0,0 +1,68 @@ +/* + * StupidAI.cs + * Copyright (c) 2016 Markus Himmel + * This file is distributed under the terms of the MIT license + */ + +using System; +using System.Linq; + +namespace Morris +{ + /// + /// Eine sehr primitive KI, die lediglich die direkten nächsten Züge nach simplen Kriterien untersucht + /// + [SelectorName("Dumme KI")] + internal class StupidAI : IMoveProvider + { + private int scoreNonRemoving(GameMove move, IReadOnlyGameState state) + { + // Diese Veriablen enthalten genau das, was ihre Namen suggerieren + bool closesMill = GameState.Mills.Any(mill => mill.All(point => (!move.From.HasValue || point != move.From.Value) && state.Board[point].IsOccupiedBy(state.NextToMove) || point == move.To)); + bool preventsMill = GameState.Mills.Any(mill => mill.All(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent()) || point == move.To)); + bool opensMill = move.From.HasValue && GameState.Mills.Any(mill => mill.Contains(move.From.Value) && mill.All(point => state.Board[point].IsOccupiedBy(state.NextToMove))); + + // Als "Tiebraker" dient das Kriterium, wie viele andere eigene Spielsteine sich in den potenziellen Mühlen des Zielfeldes befinden. + // Dieses Kriterium ist extrem schwach, weil es sehr leicht in lokalen Maxima stecken bleibt + // In anderen Worten ist es sehr schwierig für diese KI, nach der Setzphase noch neue Mühlen zu bilden + int inRange = GameState.Mills.Sum(mill => mill.Contains(move.To) ? mill.Count(point => state.Board[point].IsOccupiedBy(state.NextToMove)) : 0); + + return (closesMill ? 10 : 0) + (preventsMill ? 12 : 0) + (opensMill ? 9 : 0) + inRange; + } + + private int scoreRemove(int remove, IReadOnlyGameState state) + { + return GameState.Mills.Sum(mill => mill.Contains(remove) ? mill.Count(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent())) : 0); + } + + public GameMove GetNextMove(IReadOnlyGameState state) + { + int ignored; + // Simples Prinzip: Alle Züge werden nach den Bepunktungsfunktionen bewertet, von den besten Zügen wird ein zufälliger Zug ausgewählt + GameMove move = state.BasicMoves().AllMaxBy(x => scoreNonRemoving(x, state), out ignored).ToList().ChooseRandom(); + + if (state.IsValidMove(move) == MoveValidity.ClosesMill) + { + bool allInMill = Enumerable.Range(0, GameState.FIELD_SIZE) + .Where(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent())) + .All(point => GameState.Mills.Any(mill => mill.Contains(point) && mill.All(mp => state.Board[mp].IsOccupiedBy(state.NextToMove.Opponent())))); + + Func filter; + if (allInMill) + filter = _ => true; + else + // Wenn es Steine gibt, die in keiner Mühle sind, müssen wir einen solchen Stein entfernen + filter = point => GameState.Mills.All(mill => !mill.Contains(point) || mill.Any(mp => !state.Board[mp].IsOccupiedBy(state.NextToMove.Opponent()))); + + return move.WithRemove( + Enumerable.Range(0, GameState.FIELD_SIZE) + .Where(point => state.Board[point].IsOccupiedBy(state.NextToMove.Opponent()) && filter(point)) + .AllMaxBy(x => scoreRemove(x, state), out ignored) + .ToList().ChooseRandom() + ); + } + + return move; + } + } +} diff --git a/Morris/Controller.xaml b/Morris/Control/Controller.xaml similarity index 88% rename from Morris/Controller.xaml rename to Morris/Control/Controller.xaml index a66cc14..257624c 100644 --- a/Morris/Controller.xaml +++ b/Morris/Control/Controller.xaml @@ -6,8 +6,8 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Morris" mc:Ignorable="d" - Title="Morris" Height="332.409" Width="285.955" Closed="Window_Closed"> - + Title="Morris" Height="340.409" Width="454.955" Closed="Window_Closed"> +