mirror of
https://github.com/TwoFX/Morris.git
synced 2025-12-13 08:22:51 +00:00
GUI is now fully functional. Very awesome
This commit is contained in:
@@ -55,9 +55,9 @@ namespace Morris
|
||||
return new[] { 6 - (human[1] - '1'), human[0] - 'a' };
|
||||
}
|
||||
|
||||
public static string HumanReadableFromCoordinates(Tuple<int, int> coord)
|
||||
public static string HumanReadableFromCoordinates(int[] coord)
|
||||
{
|
||||
string res = new string(new [] { 'a' + coord.Item1, '1' + (char)coord.Item2 }.Cast<char>().ToArray());
|
||||
string res = new string(new[] { 'a' + coord[1], '1' + coord[0] }.Select(x => (char)x).ToArray());
|
||||
|
||||
if (humans.Keys.Contains(res))
|
||||
return res;
|
||||
@@ -70,7 +70,7 @@ namespace Morris
|
||||
return CoordinatesFromHumanReadable(HumanReadableFromID(id));
|
||||
}
|
||||
|
||||
public static int IDFromCoordinates(Tuple<int, int> coord)
|
||||
public static int IDFromCoordinates(int[] coord)
|
||||
{
|
||||
return IDFromHumanReadable(HumanReadableFromCoordinates(coord));
|
||||
}
|
||||
|
||||
@@ -218,6 +218,9 @@ namespace Morris
|
||||
// bedingte Anweisung gepackt, auch wenn dies kompakter und eventuell marginal
|
||||
// schneller wäre
|
||||
|
||||
if (move == null)
|
||||
return MoveValidity.Invalid;
|
||||
|
||||
// 1.: Ziel verifizieren
|
||||
if (move.To < 0 || move.To >= FIELD_SIZE)
|
||||
return MoveValidity.Invalid; // OOB
|
||||
@@ -315,7 +318,7 @@ namespace Morris
|
||||
Board[move.To] = (Occupation)NextToMove;
|
||||
|
||||
// Wiederholte Stellung
|
||||
if (!playerPhase.Values.Contains(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;
|
||||
|
||||
// ggf. entfernter Stein
|
||||
|
||||
@@ -5,46 +5,43 @@
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Morris
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Eine WPF-gestütze Mühle-GUI
|
||||
/// </summary>
|
||||
public partial class GameWindow : Window, IGameStateObserver
|
||||
public partial class GameWindow : Window, IGameStateObserver, IMoveProvider
|
||||
{
|
||||
|
||||
// Diese konstanten Steuern das Aussehen des Spielfelds.
|
||||
private const int BLOCK_SIZE = 100; // Sollte durch 2 teilbar sein
|
||||
private const int OFFSET_LEFT = 50;
|
||||
private const int OFFSET_TOP = 70;
|
||||
private const int OFFSET_TOP = 50;
|
||||
private const int OFFSET_BOTTOM = 90;
|
||||
private const int OFFSET_RIGHT = 10;
|
||||
private const int LINE_THICKNESS = 6; // Sollte durch 2 teilbar sein
|
||||
private const int PIECE_RADIUS = 30;
|
||||
private const int LEGEND_OFFSET = 30;
|
||||
private const int LABEL_BUFFER_SIZE = 40;
|
||||
private const int LEGEND_SIZE = 20;
|
||||
private const int STATUS_SIZE = 20;
|
||||
private const int STATUS_OFFSET_TOP = 10;
|
||||
|
||||
private Ellipse[] pieces;
|
||||
|
||||
private SolidColorBrush primaryColor = new SolidColorBrush(Color.FromRgb(0, 0, 0));
|
||||
|
||||
private SolidColorBrush primaryColor = Brushes.Black;
|
||||
private Label status;
|
||||
|
||||
public GameWindow()
|
||||
{
|
||||
// Dieser Konstruktor ist nicht sonderlich spannend zu lesen, da er einfach einen Haufen WPF-Objekte initialisiert.
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// Spielfield zeichnen
|
||||
@@ -84,7 +81,7 @@ namespace Morris
|
||||
l.Height = LABEL_BUFFER_SIZE;
|
||||
|
||||
l.Margin = new Thickness(OFFSET_LEFT - LEGEND_OFFSET, OFFSET_TOP + i * BLOCK_SIZE + BLOCK_SIZE / 2 - LABEL_BUFFER_SIZE / 2, 0, 0);
|
||||
l.FontSize = 20;
|
||||
l.FontSize = LEGEND_SIZE;
|
||||
l.Foreground = primaryColor;
|
||||
grid.Children.Add(l);
|
||||
}
|
||||
@@ -99,7 +96,7 @@ namespace Morris
|
||||
l.Content = (char)('a' + i);
|
||||
l.Width = LABEL_BUFFER_SIZE;
|
||||
l.Margin = new Thickness(OFFSET_LEFT + i * BLOCK_SIZE + BLOCK_SIZE / 2 - LABEL_BUFFER_SIZE / 2, OFFSET_TOP + 7 * BLOCK_SIZE, 0, 0);
|
||||
l.FontSize = 20;
|
||||
l.FontSize = LEGEND_SIZE;
|
||||
l.Foreground = primaryColor;
|
||||
grid.Children.Add(l);
|
||||
}
|
||||
@@ -109,27 +106,23 @@ namespace Morris
|
||||
Width = OFFSET_LEFT + 7 * BLOCK_SIZE + OFFSET_RIGHT;
|
||||
|
||||
|
||||
|
||||
// Es gibt nicht für jeden tatsächlichen Spielstein eine Ellipse, die sich bewegt. Stattdessen gibt es eine
|
||||
// Ellipse auf jedem der 24 Spielfeldpunkte, die je nach Belegung Schwarz, weiß oder transparent ist
|
||||
pieces = new Ellipse[GameState.FIELD_SIZE];
|
||||
for (int i = 0; i < GameState.FIELD_SIZE; i++)
|
||||
{
|
||||
var point = CoordinateTranslator.CoordinatesFromID(i);
|
||||
var e = new Ellipse();
|
||||
e.Fill = null;
|
||||
// e ist hier lediglich dazu da, kürzer als pieces[i] zu sein.
|
||||
var e = pieces[i] = new Ellipse();
|
||||
e.Fill = Brushes.Transparent;
|
||||
e.Width = e.Height = 2 * PIECE_RADIUS;
|
||||
e.HorizontalAlignment = HorizontalAlignment.Left;
|
||||
e.VerticalAlignment = VerticalAlignment.Top;
|
||||
|
||||
e.Margin = new Thickness(
|
||||
OFFSET_LEFT + BLOCK_SIZE * point[1] + BLOCK_SIZE / 2 - PIECE_RADIUS,
|
||||
OFFSET_TOP + BLOCK_SIZE * point[0] + BLOCK_SIZE / 2 - PIECE_RADIUS,
|
||||
0, 0);
|
||||
|
||||
e.MouseDown += ellipseMouseDown;
|
||||
e.MouseMove += ellipseMouseMove;
|
||||
e.MouseUp += ellipseMouseUp;
|
||||
resetPositon(i);
|
||||
grid.Children.Add(e);
|
||||
|
||||
pieces[i] = e;
|
||||
}
|
||||
|
||||
// Statusanzeige
|
||||
@@ -138,8 +131,8 @@ namespace Morris
|
||||
status.HorizontalAlignment = HorizontalAlignment.Left;
|
||||
status.VerticalAlignment = VerticalAlignment.Top;
|
||||
status.Width = 7 * BLOCK_SIZE + LINE_THICKNESS;
|
||||
status.Margin = new Thickness(OFFSET_LEFT, 10, 0, 0);
|
||||
status.FontSize = 40;
|
||||
status.Margin = new Thickness(OFFSET_LEFT, STATUS_OFFSET_TOP, 0, 0);
|
||||
status.FontSize = STATUS_SIZE;
|
||||
grid.Children.Add(status);
|
||||
}
|
||||
|
||||
@@ -153,15 +146,15 @@ namespace Morris
|
||||
switch (state.Board[i])
|
||||
{
|
||||
case Occupation.Free:
|
||||
pieces[i].Fill = null;
|
||||
pieces[i].Fill = Brushes.Transparent;
|
||||
break;
|
||||
|
||||
case Occupation.Black:
|
||||
pieces[i].Fill = new SolidColorBrush(Colors.Black);
|
||||
pieces[i].Fill = Brushes.Black;
|
||||
break;
|
||||
|
||||
case Occupation.White:
|
||||
pieces[i].Fill = new SolidColorBrush(Colors.White);
|
||||
pieces[i].Fill = Brushes.White;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -183,5 +176,220 @@ namespace Morris
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void resetPositon(int index)
|
||||
{
|
||||
var point = CoordinateTranslator.CoordinatesFromID(index);
|
||||
pieces[index].Margin = new Thickness(
|
||||
OFFSET_LEFT + BLOCK_SIZE * point[1] + BLOCK_SIZE / 2 - PIECE_RADIUS,
|
||||
OFFSET_TOP + BLOCK_SIZE * point[0] + BLOCK_SIZE / 2 - PIECE_RADIUS,
|
||||
0, 0);
|
||||
}
|
||||
|
||||
|
||||
// Überblick über die Benutzerinteraktion in der GUI:
|
||||
|
||||
// Wenn ein Zug angefordert wird, wird die Methode GetNextMove vom Spielthread
|
||||
// aus aufgerufen. Diese bestimmt, ob je nach Spielzustand ein Klick oder ein
|
||||
// Drag/Drop erforderlich ist und setzt mode entsprechend. Außerdem wird, basierend
|
||||
// auf dem Spielfeld in validSources abgespeichert, welche Felder angeklickt bzw. gezogen
|
||||
// werden dürfen. Dieser Thread muss
|
||||
// nun blockieren, bis ein Zug durch den Benutzer asusgeführt wurde. Dazu wartet
|
||||
// er auf das AutoResetEvent sync.
|
||||
// Wenn ein erlaubtes Feld angeklickt wurde, wird die ellipseMouseDown-Methode aufgerufen
|
||||
// (Event Handler). Wenn lediglich ein Klick notwendig war, speichert die Methode die ID
|
||||
// des Felds in der Instanzvariable source und löst das AutoResetEvent aus.
|
||||
// Falls ein Drag/Drop stattfinden soll, wird die Modusvariable entsprechend modifiziert,
|
||||
// die Maus eingefangen und die relative Position des Mauszeigers zum Spielstein im Feld
|
||||
// Offset gespeichert. Immer wenn die Maus bewegt wird, wird der Spielstein nun so bewegt,
|
||||
// dass die relative Position zur Maus gleich bleibt. Wird die Maus losgelassen, wird der
|
||||
// Mauszeiger freigelassen und das Feld, auf dem der Spielstein fallengelassen wurde, berechnet.
|
||||
// Dieses wird dann in der Instanz gespeichert und sync ausgelöst.
|
||||
|
||||
// Aus den gewonnen Daten wird dann wieder im Spielthread ein GameMove gebaut und geprüft, ob
|
||||
// dieser einen gegnerischen Stein schlägt. Wenn ja, wird ein weiterer Klick eingeholt.
|
||||
// *Nachdem* dies stattgefunden hat wird dann gegebenenfalls ein bewegter Spielstein zurück
|
||||
// auf seine Ursprungsposition gebracht (wie bereits erwähnt werden die Ellipsen nicht wirklich
|
||||
// bewegt, sondern die unsichtbare Ellipse am Zielort wird beim nächsten Aufruf von Notify
|
||||
// entsprechend eingefärbt).
|
||||
|
||||
private enum Mode
|
||||
{
|
||||
Normal,
|
||||
ExpectingClick,
|
||||
ExpectingDrag,
|
||||
Dragging
|
||||
}
|
||||
|
||||
private Mode mode = Mode.Normal;
|
||||
private Point offset;// Relative Position des bewegten Spielsteins zum Mauszeiger
|
||||
private bool error = false; // Ob der Stein auf ein nonexistentes Feld gezogen wurde
|
||||
private bool[] validSources = new bool[GameState.FIELD_SIZE]; // Felder, die angeklickt/gezogen werden dürfen
|
||||
private int source = -1; // Welcher Stein angeklickt/gezogen wurde
|
||||
private int destination = -1; // Wo der Stein hingezogen wurde
|
||||
AutoResetEvent sync = new AutoResetEvent(false); // Damit der Spielthread weiß, dass auf dem UI-Thread ein Zug gemacht wurde
|
||||
|
||||
private void ellipseMouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
Ellipse ellipse = sender as Ellipse;
|
||||
int index = Array.IndexOf(pieces, ellipse);
|
||||
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case Mode.ExpectingClick:
|
||||
if (!validSources[index])
|
||||
break;
|
||||
|
||||
source = index;
|
||||
sync.Set(); // Zurück zum Spielthread
|
||||
break;
|
||||
|
||||
case Mode.ExpectingDrag:
|
||||
if (!validSources[index])
|
||||
break;
|
||||
|
||||
source = index;
|
||||
mode = Mode.Dragging;
|
||||
offset = e.GetPosition(ellipse);
|
||||
Mouse.Capture(ellipse);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ellipseMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
Ellipse ellipse = sender as Ellipse;
|
||||
|
||||
if (mode != Mode.Dragging || e.LeftButton != MouseButtonState.Pressed)
|
||||
return;
|
||||
|
||||
// Den Spielstein, der gerade gezogen wird, auf die richtige Position bewegen
|
||||
Point pos = e.GetPosition(grid);
|
||||
ellipse.Margin = new Thickness(pos.X - offset.X, pos.Y - offset.Y, 0, 0);
|
||||
}
|
||||
|
||||
private void ellipseMouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
Ellipse ellipse = sender as Ellipse;
|
||||
|
||||
if (mode != Mode.Dragging)
|
||||
return;
|
||||
|
||||
// Das Spielfeld wird hier imaginär in ein Raster der Größe BLOCK_SIZE * BLOCK_SIZE aufgeteilt.
|
||||
// Im Zentrum jedes Blocks liegt ein Feld
|
||||
int left = (int)Math.Floor((ellipse.Margin.Left + PIECE_RADIUS - OFFSET_LEFT) / BLOCK_SIZE);
|
||||
// 6 - x, weil WPF von oben, Mühle aber von unten zählt
|
||||
int top = 6 - (int)Math.Floor((ellipse.Margin.Top + PIECE_RADIUS - OFFSET_TOP) / BLOCK_SIZE);
|
||||
|
||||
try
|
||||
{
|
||||
destination = CoordinateTranslator.IDFromCoordinates(new[] { top, left });
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// IDFromCoordinates schmeißt hier, wenn der Spielstein auf ein Stelle im Spielfeld, die kein
|
||||
// Feld enthält oder aus dem Feld heraus gezogen wurde
|
||||
error = true;
|
||||
}
|
||||
|
||||
// Maus freigeben
|
||||
Mouse.Capture(null);
|
||||
sync.Set(); // Zurück zum Spielthread
|
||||
}
|
||||
|
||||
public GameMove GetNextMove(IReadOnlyGameState state)
|
||||
{
|
||||
// Status
|
||||
string phase;
|
||||
switch (state.GetPhase(state.NextToMove))
|
||||
{
|
||||
case Phase.Placing:
|
||||
phase = "platziert";
|
||||
break;
|
||||
case Phase.Moving:
|
||||
phase = "bewegt";
|
||||
break;
|
||||
default:
|
||||
phase = "fliegt";
|
||||
break;
|
||||
}
|
||||
Dispatcher.Invoke(() => status.Content = $"{(state.NextToMove == Player.Black ? "Schwarz" : "Weiß")} {phase}.");
|
||||
|
||||
GameMove move = null;
|
||||
|
||||
int oldsource = -1; // Enthält ggf. den Spielstein, der bewegt wurde
|
||||
|
||||
if (state.GetPhase(state.NextToMove) == Phase.Placing)
|
||||
{
|
||||
// Wir brauchen nur einen Klick in ein freies Feld
|
||||
for (int i = 0; i < GameState.FIELD_SIZE; i++)
|
||||
{
|
||||
validSources[i] = state.Board[i] == Occupation.Free;
|
||||
}
|
||||
mode = Mode.ExpectingClick;
|
||||
|
||||
sync.WaitOne(); // Warten...
|
||||
|
||||
// source enthält jetzt den angeklickten Punkt
|
||||
mode = Mode.Normal;
|
||||
move = GameMove.Place(source);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wir brauchen Drag&Drop von einem vom aktuellen Spieler besetzten Feld
|
||||
for (int i = 0; i < GameState.FIELD_SIZE; i++)
|
||||
{
|
||||
validSources[i] = state.Board[i] == (Occupation)state.NextToMove;
|
||||
}
|
||||
mode = Mode.ExpectingDrag;
|
||||
error = false;
|
||||
|
||||
sync.WaitOne(); // Warten
|
||||
|
||||
// Ungültiges Feld, einfach einen ungültigen Zug zurückgeben, damit GetNextMove
|
||||
// erneut aufgerufen wird
|
||||
if (error)
|
||||
return null;
|
||||
|
||||
// Source und Destination erhalten Anfang und Ende des Drag-Events
|
||||
mode = Mode.Normal;
|
||||
move = GameMove.Move(source, destination);
|
||||
oldsource = source;
|
||||
}
|
||||
|
||||
if (state.IsValidMove(move) == MoveValidity.ClosesMill)
|
||||
{
|
||||
// Selbes Spiel wie oben.
|
||||
Dispatcher.Invoke(() => status.Content = "Bitte gegnerischen Stein zum Entfernen wählen.");
|
||||
for (int i = 0; i < GameState.FIELD_SIZE; i++)
|
||||
{
|
||||
validSources[i] = state.Board[i] == (Occupation)state.NextToMove.Opponent();
|
||||
}
|
||||
mode = Mode.ExpectingClick;
|
||||
sync.WaitOne();
|
||||
mode = Mode.Normal;
|
||||
move = move.WithRemove(source);
|
||||
}
|
||||
|
||||
// Erst jetzt setzen wir den Stein zurück. Grund dafür ist, dass
|
||||
// so der Stein nicht hin- und herspringt. Wenn wir ihn direkt zurücksetzen würden,
|
||||
// kann es passieren, dass der Stein zurückgestzt wird und dann aber noch ein zu
|
||||
// entfernender Stein abgefragt wird. Erst nachdem dieser abgefragt wurde, wird dann
|
||||
// der Zielstein richtig eingefärbt. Wenn man aber den Stein erst ganz am Ende zurücksetzt,
|
||||
// gibt es keine derarten Sprünge.
|
||||
if (oldsource >= 0)
|
||||
Dispatcher.Invoke(() => {
|
||||
// Kleiner Hack, es sieht schöner aus, wenn der Stein schon hier ausgeblendet wird. Wir wissen, dass das der
|
||||
// Fall sein wird, solange der Zug gültig ist, deshalb prüfen wir das hier schonmal.
|
||||
if (state.IsValidMove(move) == MoveValidity.Valid) pieces[oldsource].Fill = Brushes.Transparent;
|
||||
resetPositon(oldsource);
|
||||
});
|
||||
|
||||
Dispatcher.Invoke(() => status.Content = null);
|
||||
return move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ namespace Morris
|
||||
var a = new ConsoleInteraction();
|
||||
var b = new RandomBot();
|
||||
var w = new GameWindow();
|
||||
var g = new Game(b, b);
|
||||
var g = new Game(w, w);
|
||||
g.AddObserver(a);
|
||||
g.AddObserver(w);
|
||||
Task.Run(() => g.Run());
|
||||
Task.Run(() => g.Run(0));
|
||||
new Application().Run(w);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user