Volle Logik bisa uf Unentschieden, Spielschleife

This commit is contained in:
Markus Himmel
2016-08-26 00:07:53 +02:00
parent 75e8dd0b28
commit 2c3f1ce502
8 changed files with 308 additions and 56 deletions

View File

@@ -8,8 +8,15 @@ namespace Morris
{
internal 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;
}
}

88
Morris/Game.cs Normal file
View File

@@ -0,0 +1,88 @@
/*
* 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>
class Game
{
private List<IGameStateObserver> observers = new List<IGameStateObserver>();
private GameState state;
private Dictionary<Player, IMoveProvider> providers;
public Game(IMoveProvider white, IMoveProvider black)
{
state = new GameState();
providers = new Dictionary<Player, IMoveProvider>()
{
[Player.White] = white,
[Player.Black] = black
};
}
/// <summary>
/// Spielt eine gesamte Runde Mühle
/// </summary>
/// <param name="moveDelay">Die Zeit, in Millisekunden, die gewartet wird, bevor nach einem
/// erfolgreichem Zug der nächste Zug angefordert wird (damit KI vs. KI-Spiele in einem
/// angemessenen Tempo angesehen werden können)</param>
/// <returns>Das Spielergebnis</returns>
public GameResult Run(int moveDelay = 0)
{
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(moveDelay);
} 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);
}
/// <summary>
/// Meldet einen <see cref="IGameStateObserver"/> von kommenden Spielereignissen ab
/// </summary>
/// <param name="observer">Das abzumeldende Objekt</param>
/// <returns>Wahr, wenn das Objekt tatsächlich entfernt wurde.
/// Falsch, wenn es nicht gefunden wurde.</returns>
public bool RemoveObserver(IGameStateObserver observer)
{
return observers.Remove(observer);
}
// Meldet dem Spielzustand an alle Observer
private void notifyOberservers()
{
foreach (var observer in observers)
{
observer.Notify(state);
}
}
}
}

View File

@@ -70,11 +70,34 @@ namespace Morris
/// </summary>
/// <param name="from">Wo sich der Stein vor dem Zug befindet</param>
/// <param name="to">Wo sich der Stein nach dem Zug befindet</param>
/// <param name="remove">Welcher gegnerische Stein bewegt werden soll</param>
/// <param name="remove">Welcher gegnerische Stein entfernt werden soll</param>
/// <returns>Einen nicht zwangsläufig gültigen Spielzug</returns>
public static GameMove MoveRemove(int from, int to, int remove)
{
return new GameMove(from, to, remove);
}
// Die nachfolgenden beiden Methoden existieren, weil GameMove immutable sein soll, damit es keine lustigen
// Aliasing-Bugs gibt, wenn MoveProviders komische Dinge mit den Zügen machen, die sie von GameState.BasicMoves
// zurückbekommen
/// <summary>
/// Gibt eine Kopie des GameMove ohne Informationen zur Entfernung zurück
/// </summary>
/// <returns>Eine neuen Spielzug</returns>
public GameMove WithoutRemove()
{
return new GameMove(From, To, null);
}
/// <summary>
/// Erstellt eine Kopie des GameMove mit Zusatzinformation zur Entfernung eines gegnerischen Steins zurück
/// </summary>
/// <param name="remove">Welcher gegnerische Stein entfernt werden soll</param>
/// <returns>Einen neuen Spielzug</returns>
public GameMove WithRemove(int remove)
{
return new GameMove(From, To, remove);
}
}
}

View File

@@ -12,8 +12,8 @@ namespace Morris
public enum GameResult
{
Running,
WhiteVictory,
BlackVictory,
Draw
Draw,
WhiteVictory = Player.White,
BlackVictory = Player.Black
}
}

View File

@@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.ObjectModel;
using System.Text;
using System.Threading.Tasks;
@@ -15,52 +16,19 @@ namespace Morris
/// <summary>
/// Repräsentiert eine Mühle-Spielsituation
/// </summary>
public class GameState
public class GameState : IReadOnlyGameState
{
public Occupation[] Board { get; private set; }
public Player NextToMove { get; private set; }
public GameResult Result { get; private set; }
private Dictionary<Player, Phase> playerPhase;
private Dictionary<Player, int> stonesPlaced;
private Dictionary<Player, int> currentStones;
/// <summary>
/// Gibt die Phase, in der sich ein Spieler befindet, zurück
/// </summary>
/// <param name="player">Der Spieler, dessen Phase gesucht ist</param>
/// <returns>Eine Phase</returns>
public Phase GetPhase(Player player)
{
return playerPhase[player];
}
private const int FIELD_SIZE = 24;
static GameState()
{
connections = new bool[FIELD_SIZE, FIELD_SIZE];
foreach (int[] mill in mills)
{
for (int i = 0; i < mill.Length - 1; i++)
{
connections[mill[i], mill[i + 1]] = true;
connections[mill[i + 1], mill[i]] = true;
}
}
}
public GameState()
{
// Leeres Feld
Board = Enumerable.Repeat(Occupation.Free, FIELD_SIZE).ToArray();
NextToMove = Player.White;
Result = GameResult.Running;
playerPhase = new Dictionary<Player, Phase>()
{
[Player.Black] = Phase.Placing,
[Player.White] = Phase.Placing
};
}
public const int FIELD_SIZE = 24;
public const int STONES_MAX = 9;
public const int FLYING_MAX = 3;
// Jeder Eintrag repräsentiert eine mögliche Mühle
private static readonly int[][] mills = new[]
@@ -90,6 +58,139 @@ namespace Morris
// Wird aus den Daten in mills im statischen Konstruktor generiert
private static bool[,] connections;
static GameState()
{
connections = new bool[FIELD_SIZE, FIELD_SIZE];
foreach (int[] mill in mills)
{
for (int i = 0; i < mill.Length - 1; i++)
{
connections[mill[i], mill[i + 1]] = true;
connections[mill[i + 1], mill[i]] = true;
}
}
}
public GameState()
{
// Leeres Feld
Board = Enumerable.Repeat(Occupation.Free, FIELD_SIZE).ToArray();
NextToMove = Player.White;
Result = GameResult.Running;
playerPhase = new Dictionary<Player, Phase>()
{
[Player.Black] = Phase.Placing,
[Player.White] = Phase.Placing
};
stonesPlaced = new Dictionary<Player, int>()
{
[Player.Black] = 0,
[Player.White] = 0
};
currentStones = new Dictionary<Player, int>()
{
[Player.Black] = 0,
[Player.White] = 0
};
}
ReadOnlyCollection<Occupation> IReadOnlyGameState.Board
{
get
{
return Array.AsReadOnly(Board);
}
}
// 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
// nicht interessiert, wie diese Daten gespeichtert sind. In einer späteren Version
// könnte sich das auch ändern (weil Dictionaries ziemlich "overkill" zur Speicherung
// zweier Ganzzahlen sind).
/// <summary>
/// Gibt die Phase, in der sich ein Spieler befindet, zurück
/// </summary>
/// <param name="player">Der Spieler, dessen Phase gesucht ist</param>
/// <returns>Eine Phase</returns>
public Phase GetPhase(Player player)
{
return playerPhase[player];
}
/// <summary>
/// Gibt die von einem Spieler insgesamt gesetzten Steine zurück
/// </summary>
/// <param name="player">Der Spieler, dessen gesetzten Steine gesucht sind</param>
/// <returns>Eine Zahl zwischen 0 und <see cref="STONES_MAX"/></returns>
public int GetStonesPlaced(Player player)
{
return stonesPlaced[player];
}
/// <summary>
/// Gibt die Zahl der Steine auf dem Spielfeld, die einem Spieler gehören, zurück
/// </summary>
/// <param name="player">Der Spieler, dessen aktuelle Steinzahl gesucht ist</param>
/// <returns>Eine Zahl zwischen 0 und <see cref="STONES_MAX"/></returns>
public int GetCurrentStones(Player player)
{
return currentStones[player];
}
// Gibt alle Paare von Spielfeldpositionen (p, p2) zurück, sodass
// p vom aktuellen Spieler belegt ist und p2 frei ist und
// zusätzlich pred(p, p2) erfüllt ist.
// Hilfsmethode für BasicMoves; in C# 7 könnte man da eine coole
// lokale Funktion draus machen, das hier ist aber ein
// C# 6-Projekt...
private IEnumerable<GameMove> pairs(Func<int, int, bool> pred)
{
return Enumerable.Range(0, FIELD_SIZE)
.Where(p => (int)Board[p] == (int)NextToMove)
.SelectMany(p => Enumerable.Range(0, FIELD_SIZE)
.Where(p2 => Board[p2] == Occupation.Free && pred(p, p2))
.Select(p2 => GameMove.Move(p, p2)));
}
/// <summary>
/// Gibt alle möglichen Spielzüge für den Spieler, der aktuell am Zug ist,
/// ohne Informationen über zu entfernende gegnerische Steine zurück.
///
/// Für von dieser Methode zurückgegebene Züge kann mithilfe von
/// <see cref="IsValidMove(GameMove)"/> bestimmt werden, ob ein Stein
/// entfernt werden darf.
/// </summary>
public IEnumerable<GameMove> BasicMoves()
{
switch (playerPhase[NextToMove])
{
case Phase.Placing:
// Ein neuer Zug für alle freien Felder
return Enumerable.Range(0, FIELD_SIZE)
.Where(p => Board[p] == Occupation.Free)
.Select(p => GameMove.Place(p));
case Phase.Moving:
// Ein neuer Zug für jedes Paar von Positionen (p, p2), bei dem
// p vom aktuellen Spieler belegt ist und p2 frei und mit p
// verbunden ist
return pairs((p, p2) => connections[p, p2]);
case Phase.Flying:
// Ein neuer Zug für jedes Paar von Positionen (p, p2), bei dem
// p vom aktuellen Spieler belegt ist und p2 frei ist
return pairs((p, p2) => true);
default:
throw new InvalidOperationException("Sollte nie erreicht werden");
}
}
/// <summary>
/// Bestimmt, ob ein Zug in der aktuellen Spielsituation gültig ist
/// </summary>
@@ -186,20 +287,33 @@ namespace Morris
// ggf. wegbewegter Stein
if (move.From.HasValue)
Board[move.From.Value] = Occupation.Free;
else if (++stonesPlaced[NextToMove] == STONES_MAX)
playerPhase[NextToMove] = Phase.Moving;
// Hinbewegter Stein
Board[move.To] = (Occupation)NextToMove;
// ggf. entfernter Stein
if (move.Remove.HasValue)
{
Board[move.Remove.Value] = Occupation.Free;
if (--currentStones[NextToMove.Opponent()] == FLYING_MAX)
playerPhase[NextToMove.Opponent()] = Phase.Flying;
}
// Gegner hat nur noch zwei Steine
if (currentStones[NextToMove.Opponent()] == 2)
Result = (GameResult)NextToMove;
// Gegner ist jetzt dran
NextToMove = NextToMove.Opponent();
// Wenn der (jetzt) aktuelle Spieler keine gültigen Züge hat,
// hat er verloren
if (!BasicMoves().Any())
Result = (GameResult)NextToMove.Opponent();
return MoveResult.OK;
}
}
}

View File

@@ -1,12 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
/*
* IGameStateObserver.cs
* Copyright (c) 2016 Markus Himmel
* This file is distributed under the terms of the MIT license
*/
namespace Morris
{
/// <summary>
/// Eine Entität, die ein Spiel "abbonieren" kann und dann über Änderungen
/// des Spielzustands in Kenntnis gesetzt wird
/// </summary>
interface IGameStateObserver
{
/// <summary>
/// Wird aufgerufen, wenn sich der aktuelle Spielzustand geändert hat
/// </summary>
/// <param name="state">Lesesicht auf den aktuellen Spielzustand</param>
void Notify(IReadOnlyGameState state);
}
}

View File

@@ -4,18 +4,14 @@
* This file is distributed under the terms of the MIT license
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
namespace Morris
{
/// <summary>
/// Eine schreibgeschützte Sicht auf ein Spielfeld, anhand der ein <see cref="IMoveProvider"/>
/// einen Nachfolgezug bestimmen soll
/// einen Nachfolgezug bestimmen soll und ein <see cref="IGameStateObserver"/> die Spielsituation
/// komsumieren kann
/// </summary>
public interface IReadOnlyGameState
{
@@ -38,6 +34,7 @@ namespace Morris
// Methoden, die Auskunft über die Spielsituation geben
// (siehe hierzu auch den Kommentar in GameState.cs)
/// <summary>
/// Gibt die Phase, in der sich ein Spieler befindet, zurück
@@ -46,6 +43,19 @@ namespace Morris
/// <returns>Eine Phase</returns>
Phase GetPhase(Player player);
/// <summary>
/// Gibt die von einem Spieler insgesamt gesetzten Steine zurück
/// </summary>
/// <param name="player">Der Spieler, dessen gesetzten Steine gesucht sind</param>
/// <returns>Eine Zahl zwischen 0 und <see cref="STONES_MAX"/></returns>
int GetStonesPlaced(Player player);
/// <summary>
/// Gibt die Zahl der Steine auf dem Spielfeld, die einem Spieler gehören, zurück
/// </summary>
/// <param name="player">Der Spieler, dessen aktuelle Steinzahl gesucht ist</param>
/// <returns>Eine Zahl zwischen 0 und <see cref="STONES_MAX"/></returns>
int GetCurrentStones(Player player);
// Methoden zur Vereinfachung der Arbeit von IMoveProvider

View File

@@ -44,6 +44,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ExtensionMethods.cs" />
<Compile Include="Game.cs" />
<Compile Include="GameResult.cs" />
<Compile Include="GameState.cs" />
<None Include="LICENSE" />