mirror of
https://github.com/TwoFX/Morris.git
synced 2025-12-13 08:22:51 +00:00
Volle Logik bisa uf Unentschieden, Spielschleife
This commit is contained in:
@@ -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
88
Morris/Game.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace Morris
|
||||
public enum GameResult
|
||||
{
|
||||
Running,
|
||||
WhiteVictory,
|
||||
BlackVictory,
|
||||
Draw
|
||||
Draw,
|
||||
WhiteVictory = Player.White,
|
||||
BlackVictory = Player.Black
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user