Code smells

This commit is contained in:
2021-03-04 20:35:23 -06:00
parent e64f75e3cc
commit 7ed771d467
31 changed files with 310 additions and 339 deletions

View File

@@ -12,7 +12,7 @@ namespace Benchmarking
{ {
private readonly Move[] moves; private readonly Move[] moves;
private readonly Vector2[] directions; private readonly Vector2[] directions;
private readonly Consumer consumer = new Consumer(); private readonly Consumer consumer = new();
public Benchmarks() public Benchmarks()
{ {

View File

@@ -1,18 +0,0 @@
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState
{
public static class Direction
{
public static readonly Vector2 Up = new Vector2(0, 1);
public static readonly Vector2 Down = new Vector2(0, -1);
public static readonly Vector2 Left = new Vector2(-1, 0);
public static readonly Vector2 Right = new Vector2(1, 0);
public static readonly Vector2 UpLeft = new Vector2(-1, 1);
public static readonly Vector2 UpRight = new Vector2(1, 1);
public static readonly Vector2 DownLeft = new Vector2(-1, -1);
public static readonly Vector2 DownRight = new Vector2(1, -1);
public static readonly Vector2 KnightLeft = new Vector2(-1, 2);
public static readonly Vector2 KnightRight = new Vector2(1, 2);
}
}

View File

@@ -1,19 +0,0 @@
using System;
namespace Gameboard.ShogiUI.BoardState
{
public static class Extensions
{
public static void ForEachNotNull(this Piece[,] array, Action<Piece, int, int> action)
{
for (var x = 0; x < array.GetLength(0); x++)
for (var y = 0; y < array.GetLength(1); y++)
{
var piece = array[x, y];
if (piece != null)
{
action(piece, x, y);
}
}
}
}
}

View File

@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,34 +1,32 @@
using PathFinding; using PathFinding;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState.Pieces namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
public class Bishop : Piece public class Bishop : Piece
{ {
private static readonly List<Path> MoveSet = new List<Path>(4) private static readonly List<PathFinding.Move> Moves = new(4)
{ {
new Path(Direction.UpLeft, Distance.MultiStep), new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
new Path(Direction.UpRight, Distance.MultiStep), new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
new Path(Direction.DownLeft, Distance.MultiStep), new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
new Path(Direction.DownRight, Distance.MultiStep) new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
}; };
private static readonly List<Path> PromotedMoveSet = new List<Path>(8) private static readonly List<PathFinding.Move> PromotedMoves = new(8)
{ {
new Path(Direction.Up), new PathFinding.Move(Direction.Up),
new Path(Direction.Left), new PathFinding.Move(Direction.Left),
new Path(Direction.Right), new PathFinding.Move(Direction.Right),
new Path(Direction.Down), new PathFinding.Move(Direction.Down),
new Path(Direction.UpLeft, Distance.MultiStep), new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
new Path(Direction.UpRight, Distance.MultiStep), new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
new Path(Direction.DownLeft, Distance.MultiStep), new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
new Path(Direction.DownRight, Distance.MultiStep) new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
}; };
public Bishop(WhichPlayer owner) : base(WhichPiece.Bishop, owner) public Bishop(WhichPlayer owner) : base(WhichPiece.Bishop, owner)
{ {
// TODO: If this strat works out, we can do away with the Direction class entirely. moveSet = new MoveSet(this, Moves);
PromotedMoveSet.AddRange(MoveSet); promotedMoveSet = new MoveSet(this, PromotedMoves);
} }
public override Piece DeepClone() public override Piece DeepClone()
@@ -37,11 +35,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
if (IsPromoted) clone.Promote(); if (IsPromoted) clone.Promote();
return clone; return clone;
} }
public override ICollection<Path> GetPaths()
{
var moveSet = IsPromoted ? PromotedMoveSet : MoveSet;
return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList();
}
} }
} }

View File

@@ -1,23 +1,23 @@
using PathFinding; using PathFinding;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState.Pieces namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
public class GoldenGeneral : Piece public class GoldenGeneral : Piece
{ {
public static readonly List<Path> MoveSet = new List<Path>(6) public static readonly List<PathFinding.Move> Moves = new(6)
{ {
new Path(Direction.Up), new PathFinding.Move(Direction.Up),
new Path(Direction.UpLeft), new PathFinding.Move(Direction.UpLeft),
new Path(Direction.UpRight), new PathFinding.Move(Direction.UpRight),
new Path(Direction.Left), new PathFinding.Move(Direction.Left),
new Path(Direction.Right), new PathFinding.Move(Direction.Right),
new Path(Direction.Down) new PathFinding.Move(Direction.Down)
}; };
public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldenGeneral, owner) public GoldenGeneral(WhichPlayer owner) : base(WhichPiece.GoldenGeneral, owner)
{ {
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, Moves);
} }
public override Piece DeepClone() public override Piece DeepClone()
@@ -26,9 +26,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
if (IsPromoted) clone.Promote(); if (IsPromoted) clone.Promote();
return clone; return clone;
} }
public override ICollection<Path> GetPaths() => Owner == WhichPlayer.Player1
? MoveSet
: MoveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList();
} }
} }

View File

@@ -1,25 +1,25 @@
using PathFinding; using PathFinding;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState.Pieces namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
public class King : Piece public class King : Piece
{ {
private static readonly List<Path> MoveSet = new List<Path>(8) private static readonly List<PathFinding.Move> Moves = new(8)
{ {
new Path(Direction.Up), new PathFinding.Move(Direction.Up),
new Path(Direction.Left), new PathFinding.Move(Direction.Left),
new Path(Direction.Right), new PathFinding.Move(Direction.Right),
new Path(Direction.Down), new PathFinding.Move(Direction.Down),
new Path(Direction.UpLeft), new PathFinding.Move(Direction.UpLeft),
new Path(Direction.UpRight), new PathFinding.Move(Direction.UpRight),
new Path(Direction.DownLeft), new PathFinding.Move(Direction.DownLeft),
new Path(Direction.DownRight) new PathFinding.Move(Direction.DownRight)
}; };
public King(WhichPlayer owner) : base(WhichPiece.King, owner) public King(WhichPlayer owner) : base(WhichPiece.King, owner)
{ {
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, Moves);
} }
public override Piece DeepClone() public override Piece DeepClone()
@@ -28,7 +28,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
if (IsPromoted) clone.Promote(); if (IsPromoted) clone.Promote();
return clone; return clone;
} }
// The move set for a King is the same regardless of orientation.
public override ICollection<Path> GetPaths() => MoveSet;
} }
} }

View File

@@ -7,14 +7,16 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
public class Knight : Piece public class Knight : Piece
{ {
private static readonly List<Path> MoveSet = new List<Path>(2) private static readonly List<PathFinding.Move> Moves = new(2)
{ {
new Path(Direction.KnightLeft), new PathFinding.Move(Direction.KnightLeft),
new Path(Direction.KnightRight) new PathFinding.Move(Direction.KnightRight)
}; };
public Knight(WhichPlayer owner) : base(WhichPiece.Knight, owner) public Knight(WhichPlayer owner) : base(WhichPiece.Knight, owner)
{ {
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves);
} }
public override Piece DeepClone() public override Piece DeepClone()
@@ -23,11 +25,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
if (IsPromoted) clone.Promote(); if (IsPromoted) clone.Promote();
return clone; return clone;
} }
public override ICollection<Path> GetPaths()
{
var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet;
return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList();
}
} }
} }

View File

@@ -1,19 +1,19 @@
using PathFinding; using PathFinding;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState.Pieces namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
public class Lance : Piece public class Lance : Piece
{ {
private static readonly List<Path> MoveSet = new List<Path>(1) private static readonly List<PathFinding.Move> Moves = new(1)
{ {
new Path(Direction.Up, Distance.MultiStep), new PathFinding.Move(Direction.Up, Distance.MultiStep),
}; };
public Lance(WhichPlayer owner) : base(WhichPiece.Lance, owner) public Lance(WhichPlayer owner) : base(WhichPiece.Lance, owner)
{ {
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves);
} }
public override Piece DeepClone() public override Piece DeepClone()
@@ -22,11 +22,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
if (IsPromoted) clone.Promote(); if (IsPromoted) clone.Promote();
return clone; return clone;
} }
public override ICollection<Path> GetPaths()
{
var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet;
return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList();
}
} }
} }

View File

@@ -1,19 +1,19 @@
using PathFinding; using PathFinding;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState.Pieces namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
public class Pawn : Piece public class Pawn : Piece
{ {
private static readonly List<Path> MoveSet = new List<Path>(1) private static readonly List<PathFinding.Move> Moves = new(1)
{ {
new Path(Direction.Up) new PathFinding.Move(Direction.Up)
}; };
public Pawn(WhichPlayer owner) : base(WhichPiece.Pawn, owner) public Pawn(WhichPlayer owner) : base(WhichPiece.Pawn, owner)
{ {
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves);
} }
public override Piece DeepClone() public override Piece DeepClone()
@@ -22,11 +22,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
if (IsPromoted) clone.Promote(); if (IsPromoted) clone.Promote();
return clone; return clone;
} }
public override ICollection<Path> GetPaths()
{
var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet;
return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList();
}
} }
} }

View File

@@ -1,15 +1,20 @@
using PathFinding; using PathFinding;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
namespace Gameboard.ShogiUI.BoardState namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
[DebuggerDisplay("{WhichPiece} {Owner}")] [DebuggerDisplay("{WhichPiece} {Owner}")]
public abstract class Piece : IPlanarElement public abstract class Piece : IPlanarElement
{ {
protected MoveSet promotedMoveSet;
protected MoveSet moveSet;
public MoveSet MoveSet => IsPromoted ? promotedMoveSet : moveSet;
public abstract Piece DeepClone();
public WhichPiece WhichPiece { get; } public WhichPiece WhichPiece { get; }
public WhichPlayer Owner { get; private set; } public WhichPlayer Owner { get; private set; }
public bool IsPromoted { get; private set; } public bool IsPromoted { get; private set; }
public bool IsUpsideDown => Owner == WhichPlayer.Player2;
public Piece(WhichPiece piece, WhichPlayer owner) public Piece(WhichPiece piece, WhichPlayer owner)
{ {
@@ -22,27 +27,6 @@ namespace Gameboard.ShogiUI.BoardState
&& WhichPiece != WhichPiece.King && WhichPiece != WhichPiece.King
&& WhichPiece != WhichPiece.GoldenGeneral; && WhichPiece != WhichPiece.GoldenGeneral;
public string ShortName => WhichPiece switch
{
WhichPiece.King => " K ",
WhichPiece.GoldenGeneral => " G ",
WhichPiece.SilverGeneral => IsPromoted ? "^S^" : " S ",
WhichPiece.Bishop => IsPromoted ? "^B^" : " B ",
WhichPiece.Rook => IsPromoted ? "^R^" : " R ",
WhichPiece.Knight => IsPromoted ? "^k^" : " k ",
WhichPiece.Lance => IsPromoted ? "^L^" : " L ",
WhichPiece.Pawn => IsPromoted ? "^P^" : " P ",
_ => " ? ",
};
public bool IsRanged => WhichPiece switch
{
WhichPiece.Bishop => true,
WhichPiece.Rook => true,
WhichPiece.Lance => !IsPromoted,
_ => false,
};
public void ToggleOwnership() public void ToggleOwnership()
{ {
Owner = Owner == WhichPlayer.Player1 Owner = Owner == WhichPlayer.Player1
@@ -59,11 +43,5 @@ namespace Gameboard.ShogiUI.BoardState
ToggleOwnership(); ToggleOwnership();
Demote(); Demote();
} }
public abstract ICollection<Path> GetPaths();
public abstract Piece DeepClone();
public bool IsUpsideDown => Owner == WhichPlayer.Player2;
} }
} }

View File

@@ -1,32 +1,32 @@
using PathFinding; using PathFinding;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState.Pieces namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
public class Rook : Piece public class Rook : Piece
{ {
private static readonly List<Path> MoveSet = new List<Path>(4) private static readonly List<PathFinding.Move> Moves = new(4)
{ {
new Path(Direction.Up, Distance.MultiStep), new PathFinding.Move(Direction.Up, Distance.MultiStep),
new Path(Direction.Left, Distance.MultiStep), new PathFinding.Move(Direction.Left, Distance.MultiStep),
new Path(Direction.Right, Distance.MultiStep), new PathFinding.Move(Direction.Right, Distance.MultiStep),
new Path(Direction.Down, Distance.MultiStep) new PathFinding.Move(Direction.Down, Distance.MultiStep)
}; };
private static readonly List<Path> PromotedMoveSet = new List<Path>(8) private static readonly List<PathFinding.Move> PromotedMoves = new(8)
{ {
new Path(Direction.Up, Distance.MultiStep), new PathFinding.Move(Direction.Up, Distance.MultiStep),
new Path(Direction.Left, Distance.MultiStep), new PathFinding.Move(Direction.Left, Distance.MultiStep),
new Path(Direction.Right, Distance.MultiStep), new PathFinding.Move(Direction.Right, Distance.MultiStep),
new Path(Direction.Down, Distance.MultiStep), new PathFinding.Move(Direction.Down, Distance.MultiStep),
new Path(Direction.UpLeft), new PathFinding.Move(Direction.UpLeft),
new Path(Direction.UpRight), new PathFinding.Move(Direction.UpRight),
new Path(Direction.DownLeft), new PathFinding.Move(Direction.DownLeft),
new Path(Direction.DownRight) new PathFinding.Move(Direction.DownRight)
}; };
public Rook(WhichPlayer owner) : base(WhichPiece.Rook, owner) public Rook(WhichPlayer owner) : base(WhichPiece.Rook, owner)
{ {
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, PromotedMoves);
} }
public override Piece DeepClone() public override Piece DeepClone()
@@ -35,10 +35,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
if (IsPromoted) clone.Promote(); if (IsPromoted) clone.Promote();
return clone; return clone;
} }
public override ICollection<Path> GetPaths()
{
var moveSet = IsPromoted ? PromotedMoveSet : MoveSet;
return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList();
}
} }
} }

View File

@@ -1,22 +1,22 @@
using PathFinding; using PathFinding;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState.Pieces namespace Gameboard.ShogiUI.BoardState.Pieces
{ {
public class SilverGeneral : Piece public class SilverGeneral : Piece
{ {
private static readonly List<Path> MoveSet = new List<Path>(4) private static readonly List<PathFinding.Move> Moves = new(4)
{ {
new Path(Direction.Up), new PathFinding.Move(Direction.Up),
new Path(Direction.UpLeft), new PathFinding.Move(Direction.UpLeft),
new Path(Direction.UpRight), new PathFinding.Move(Direction.UpRight),
new Path(Direction.DownLeft), new PathFinding.Move(Direction.DownLeft),
new Path(Direction.DownRight) new PathFinding.Move(Direction.DownRight)
}; };
public SilverGeneral(WhichPlayer owner) : base(WhichPiece.SilverGeneral, owner) public SilverGeneral(WhichPlayer owner) : base(WhichPiece.SilverGeneral, owner)
{ {
moveSet = new MoveSet(this, Moves);
promotedMoveSet = new MoveSet(this, GoldenGeneral.Moves);
} }
public override Piece DeepClone() public override Piece DeepClone()
@@ -25,11 +25,5 @@ namespace Gameboard.ShogiUI.BoardState.Pieces
if (IsPromoted) clone.Promote(); if (IsPromoted) clone.Promote();
return clone; return clone;
} }
public override ICollection<Path> GetPaths()
{
var moveSet = IsPromoted ? GoldenGeneral.MoveSet : MoveSet;
return Owner == WhichPlayer.Player1 ? moveSet : moveSet.Select(_ => new Path(Vector2.Negate(_.Direction), _.Distance)).ToList();
}
} }
} }

View File

@@ -2,19 +2,17 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics;
namespace Gameboard.ShogiUI.BoardState namespace Gameboard.ShogiUI.BoardState
{ {
public class Array2D<T> : IPlanarCollection<T>, IEnumerable<T> public class PlanarCollection<T> : IPlanarCollection<T>, IEnumerable<T> where T : IPlanarElement
{ {
/// <returns>False to stop iterating.</returns>
public delegate void ForEachDelegate(T element, int x, int y); public delegate void ForEachDelegate(T element, int x, int y);
private readonly T[] array; private readonly T[] array;
private readonly int width; private readonly int width;
private readonly int height; private readonly int height;
public Array2D(int width, int height) public PlanarCollection(int width, int height)
{ {
this.width = width; this.width = width;
this.height = height; this.height = height;
@@ -39,17 +37,6 @@ namespace Gameboard.ShogiUI.BoardState
_ => throw new IndexOutOfRangeException() _ => throw new IndexOutOfRangeException()
}; };
public void ForEach(ForEachDelegate callback)
{
for (var x = 0; x < width; x++)
{
for (var y = 0; y < height; y++)
{
callback(this[x, y], x, y);
}
}
}
public void ForEachNotNull(ForEachDelegate callback) public void ForEachNotNull(ForEachDelegate callback)
{ {
for (var x = 0; x < width; x++) for (var x = 0; x < width; x++)
@@ -62,19 +49,6 @@ namespace Gameboard.ShogiUI.BoardState
} }
} }
public Vector2? IndexOf(Predicate<T> predicate)
{
for (var x = 0; x < width; x++)
for (var y = 0; y < height; y++)
{
if (this[x, y] != null && predicate(this[x, y]))
{
return new Vector2(x, y);
}
}
return null;
}
public IEnumerator<T> GetEnumerator() public IEnumerator<T> GetEnumerator()
{ {
foreach (var item in array) yield return item; foreach (var item in array) yield return item;

View File

@@ -1,35 +0,0 @@
using System;
namespace Gameboard.ShogiUI.BoardState
{
public class Position
{
private int x;
private int y;
public int X
{
get => x;
set {
if (value > 8 || value < 0) throw new ArgumentOutOfRangeException();
x = value;
}
}
public int Y
{
get => y;
set
{
if (value > 8 || value < 0) throw new ArgumentOutOfRangeException();
y = value;
}
}
public Position(int x, int y)
{
X = x;
Y = y;
}
}
}

View File

@@ -20,7 +20,7 @@ namespace Gameboard.ShogiUI.BoardState
private Vector2 player1King; private Vector2 player1King;
private Vector2 player2King; private Vector2 player2King;
public IReadOnlyDictionary<WhichPlayer, List<Piece>> Hands { get; } public IReadOnlyDictionary<WhichPlayer, List<Piece>> Hands { get; }
public Array2D<Piece> Board { get; } public PlanarCollection<Piece> Board { get; }
public List<Move> MoveHistory { get; } public List<Move> MoveHistory { get; }
public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2; public WhichPlayer WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPlayer.Player1 : WhichPlayer.Player2;
public WhichPlayer? InCheck { get; private set; } public WhichPlayer? InCheck { get; private set; }
@@ -28,7 +28,7 @@ namespace Gameboard.ShogiUI.BoardState
public ShogiBoard() public ShogiBoard()
{ {
Board = new Array2D<Piece>(9, 9); Board = new PlanarCollection<Piece>(9, 9);
MoveHistory = new List<Move>(20); MoveHistory = new List<Move>(20);
Hands = new Dictionary<WhichPlayer, List<Piece>> { Hands = new Dictionary<WhichPlayer, List<Piece>> {
{ WhichPlayer.Player1, new List<Piece>()}, { WhichPlayer.Player1, new List<Piece>()},
@@ -54,7 +54,7 @@ namespace Gameboard.ShogiUI.BoardState
private ShogiBoard(ShogiBoard toCopy) private ShogiBoard(ShogiBoard toCopy)
{ {
Board = new Array2D<Piece>(9, 9); Board = new PlanarCollection<Piece>(9, 9);
for (var x = 0; x < 9; x++) for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++) for (var y = 0; y < 9; y++)
Board[x, y] = toCopy.Board[x, y]?.DeepClone(); Board[x, y] = toCopy.Board[x, y]?.DeepClone();
@@ -213,40 +213,6 @@ namespace Gameboard.ShogiUI.BoardState
return !isObstructed && isPathable; return !isObstructed && isPathable;
} }
public void PrintStateAsAscii()
{
var builder = new StringBuilder();
builder.Append(" Player 2");
builder.AppendLine();
for (var y = 8; y > -1; y--)
{
builder.Append("- ");
for (var x = 0; x < 8; x++) builder.Append("- - ");
builder.Append("- -");
builder.AppendLine();
builder.Append('|');
for (var x = 0; x < 9; x++)
{
var piece = Board[x, y];
if (piece == null)
{
builder.Append(" ");
}
else
{
builder.AppendFormat("{0}", piece.ShortName);
}
builder.Append('|');
}
builder.AppendLine();
}
builder.Append("- ");
for (var x = 0; x < 8; x++) builder.Append("- - ");
builder.Append("- -");
builder.AppendLine();
builder.Append(" Player 1");
Console.WriteLine(builder.ToString());
}
#region Rules Validation #region Rules Validation
private bool EvaluateCheckAfterMove(Move move, WhichPlayer whichPlayer) private bool EvaluateCheckAfterMove(Move move, WhichPlayer whichPlayer)
{ {

View File

@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -44,7 +44,7 @@ namespace Gameboard.ShogiUI.Sockets.Models
IsPromotion = move.IsPromotion; IsPromotion = move.IsPromotion;
PieceFromCaptured = pieceFromCaptured; PieceFromCaptured = pieceFromCaptured;
} }
public ServiceModels.Socket.Types.Move ToServiceModel() => new ServiceModels.Socket.Types.Move public ServiceModels.Socket.Types.Move ToServiceModel() => new()
{ {
From = From.ToBoardNotation(), From = From.ToBoardNotation(),
IsPromotion = IsPromotion, IsPromotion = IsPromotion,

View File

@@ -0,0 +1,65 @@
using Gameboard.ShogiUI.BoardState;
using Gameboard.ShogiUI.BoardState.Pieces;
using System;
using System.Text;
using System.Text.RegularExpressions;
namespace Gameboard.ShogiUI.UnitTests.BoardState
{
public static class BoardStateExtensions
{
public static string GetShortName(this Piece self)
{
var name = self.WhichPiece switch
{
WhichPiece.King => " K ",
WhichPiece.GoldenGeneral => " G ",
WhichPiece.SilverGeneral => self.IsPromoted ? "^S " : " S ",
WhichPiece.Bishop => self.IsPromoted ? "^B " : " B ",
WhichPiece.Rook => self.IsPromoted ? "^R " : " R ",
WhichPiece.Knight => self.IsPromoted ? "^k " : " k ",
WhichPiece.Lance => self.IsPromoted ? "^L " : " L ",
WhichPiece.Pawn => self.IsPromoted ? "^P " : " P ",
_ => " ? ",
};
if (self.Owner == WhichPlayer.Player2)
name = Regex.Replace(name, @"([^\s]+)\s", "$1.");
return name;
}
public static void PrintStateAsAscii(this ShogiBoard self)
{
var builder = new StringBuilder();
builder.Append(" Player 2(.)");
builder.AppendLine();
for (var y = 8; y > -1; y--)
{
builder.Append("- ");
for (var x = 0; x < 8; x++) builder.Append("- - ");
builder.Append("- -");
builder.AppendLine();
builder.Append('|');
for (var x = 0; x < 9; x++)
{
var piece = self.Board[x, y];
if (piece == null)
{
builder.Append(" ");
}
else
{
builder.AppendFormat("{0}", piece.GetShortName());
}
builder.Append('|');
}
builder.AppendLine();
}
builder.Append("- ");
for (var x = 0; x < 8; x++) builder.Append("- - ");
builder.Append("- -");
builder.AppendLine();
builder.Append(" Player 1");
Console.WriteLine(builder.ToString());
}
}
}

View File

@@ -1,5 +1,6 @@
using FluentAssertions; using FluentAssertions;
using Gameboard.ShogiUI.BoardState; using Gameboard.ShogiUI.BoardState;
using Gameboard.ShogiUI.BoardState.Pieces;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System; using System;
using System.Linq; using System.Linq;
@@ -81,7 +82,6 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState
shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn); shogi.Board[0, 3].WhichPiece.Should().Be(WhichPiece.Pawn);
} }
[TestMethod] [TestMethod]
public void PreventInvalidMoves_MoveFromEmptyPosition() public void PreventInvalidMoves_MoveFromEmptyPosition()
{ {
@@ -245,30 +245,110 @@ namespace Gameboard.ShogiUI.UnitTests.BoardState
var shogi = new ShogiBoard(moves); var shogi = new ShogiBoard(moves);
shogi.PrintStateAsAscii(); shogi.PrintStateAsAscii();
// Prerequisite // Prerequisites
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Knight);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
// Act | Assert - It is P1 turn // Act | Assert - It is P1 turn
// try illegally placing Knight from the hand. /// try illegally placing Knight from the hand.
shogi.Board[7, 8].Should().BeNull(); shogi.Board[7, 8].Should().BeNull();
var moveSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 8) }); var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 8) });
shogi.PrintStateAsAscii(); dropSuccess.Should().BeFalse();
moveSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance); shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[7, 8].Should().BeNull(); shogi.Board[7, 8].Should().BeNull();
dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Knight, To = new Vector2(7, 7) });
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[7, 7].Should().BeNull();
// Assert /// try illegally placing Pawn from the hand
//var pawnDropSuccess = shogi.Move(new Move) dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Pawn, To = new Vector2(7, 8) });
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Pawn);
shogi.Board[7, 8].Should().BeNull();
// Assert /// try illegally placing Lance from the hand
dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(7, 8) });
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
shogi.Board[7, 8].Should().BeNull();
} }
[TestMethod] [TestMethod]
public void PreventInvalidDrop_Check() public void PreventInvalidDrop_Check()
{ {
// Arrange
var moves = new[]
{
// P1 Pawn
new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) },
// P2 Pawn
new Move { From = new Vector2(8, 6), To = new Vector2(8, 5) },
// P1 Bishop, check
new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) },
// P2 Gold, block check
new Move { From = new Vector2(5, 8), To = new Vector2(5, 7) },
// P1 arbitrary move
new Move { From = new Vector2(0, 2), To = new Vector2(0, 3) },
// P2 Bishop
new Move { From = new Vector2(7, 7), To = new Vector2(8, 6) },
// P1 Bishop takes P2 Lance
new Move { From = new Vector2(6, 6), To = new Vector2(8, 8) },
// P2 Bishop
new Move { From = new Vector2(8, 6), To = new Vector2(7, 7) },
// P1 arbitrary move
new Move { From = new Vector2(0, 3), To = new Vector2(0, 4) },
// P2 Bishop, check
new Move { From = new Vector2(7, 7), To = new Vector2(2, 2) },
};
var shogi = new ShogiBoard(moves);
// Prerequisites
shogi.InCheck.Should().Be(WhichPlayer.Player1);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
// Act - P1 tries to place a Lance while in check.
var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Lance, To = new Vector2(4, 4) });
// Assert
dropSuccess.Should().BeFalse();
shogi.Board[4, 4].Should().BeNull();
shogi.InCheck.Should().Be(WhichPlayer.Player1);
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Lance);
}
[TestMethod]
public void PreventInvalidDrop_Capture()
{
// Arrange
var moves = new[]
{
// P1 Pawn
new Move { From = new Vector2(2, 2), To = new Vector2(2, 3) },
// P2 Pawn
new Move { From = new Vector2(6, 6), To = new Vector2(6, 5) },
// P1 Bishop, capture P2 Pawn, check
new Move { From = new Vector2(1, 1), To = new Vector2(6, 6) },
// P2 Gold, block check
new Move { From = new Vector2(5, 8), To = new Vector2(5, 7) },
// P1 Bishop capture P2 Bishop
new Move { From = new Vector2(6, 6), To = new Vector2(7, 7) },
// P2 arbitrary move
new Move { From = new Vector2(0, 8), To = new Vector2(0, 7) },
};
var shogi = new ShogiBoard(moves);
// Prerequisites
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
// Act - P1 tries to place Bishop from hand to an already-occupied position
var dropSuccess = shogi.Move(new Move { PieceFromCaptured = WhichPiece.Bishop, To = new Vector2(4, 0) });
// Assert
dropSuccess.Should().BeFalse();
shogi.Hands[WhichPlayer.Player1].Should().ContainSingle(_ => _.WhichPiece == WhichPiece.Bishop);
shogi.Board[4, 0].WhichPiece.Should().Be(WhichPiece.King);
} }
[TestMethod] [TestMethod]

View File

@@ -1,8 +1,7 @@
using FluentAssertions; using FluentAssertions;
using Gameboard.ShogiUI.BoardState; using Gameboard.ShogiUI.BoardState.Pieces;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using PathFinding; using PathFinding;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
namespace Gameboard.ShogiUI.UnitTests.PathFinding namespace Gameboard.ShogiUI.UnitTests.PathFinding
@@ -10,32 +9,25 @@ namespace Gameboard.ShogiUI.UnitTests.PathFinding
[TestClass] [TestClass]
public class PathFinder2DShould public class PathFinder2DShould
{ {
class TestElement : IPlanarElement
{
public ICollection<Path> GetPaths() => throw new System.NotImplementedException();
public bool IsUpsideDown => false;
}
[TestMethod] [TestMethod]
public void Maths() public void Maths()
{ {
var finder = new PathFinder2D<TestElement>(new Array2D<TestElement>(5, 5));
var result = finder.IsPathable( var result = PathFinder2D<Piece>.IsPathable(
new Vector2(2, 2), new Vector2(2, 2),
new Vector2(7, 7), new Vector2(7, 7),
new Vector2(1, 1) new Vector2(1, 1)
); );
result.Should().BeTrue(); result.Should().BeTrue();
result = finder.IsPathable( result = PathFinder2D<Piece>.IsPathable(
new Vector2(2, 2), new Vector2(2, 2),
new Vector2(7, 7), new Vector2(7, 7),
new Vector2(0, 0) new Vector2(0, 0)
); );
result.Should().BeFalse(); result.Should().BeFalse();
result = finder.IsPathable( result = PathFinder2D<Piece>.IsPathable(
new Vector2(2, 2), new Vector2(2, 2),
new Vector2(7, 7), new Vector2(7, 7),
new Vector2(-1, 1) new Vector2(-1, 1)

18
PathFinding/Direction.cs Normal file
View File

@@ -0,0 +1,18 @@
using System.Numerics;
namespace PathFinding
{
public static class Direction
{
public static readonly Vector2 Up = new(0, 1);
public static readonly Vector2 Down = new(0, -1);
public static readonly Vector2 Left = new(-1, 0);
public static readonly Vector2 Right = new(1, 0);
public static readonly Vector2 UpLeft = new(-1, 1);
public static readonly Vector2 UpRight = new(1, 1);
public static readonly Vector2 DownLeft = new(-1, -1);
public static readonly Vector2 DownRight = new(1, -1);
public static readonly Vector2 KnightLeft = new(-1, 2);
public static readonly Vector2 KnightRight = new(1, 2);
}
}

8
PathFinding/Distance.cs Normal file
View File

@@ -0,0 +1,8 @@
namespace PathFinding
{
public enum Distance
{
OneStep,
MultiStep
}
}

View File

@@ -1,19 +0,0 @@
namespace PathFinding
{
public enum HaltCondition
{
/// <summary>
/// Do not stop until you reach the collection boundary.
/// </summary>
None,
/// <summary>
/// Halt after encountering a non-null element.
/// </summary>
AfterCollide
}
}
public enum Distance
{
OneStep,
MultiStep
}

View File

@@ -2,7 +2,7 @@
namespace PathFinding namespace PathFinding
{ {
public interface IPlanarCollection<T> : IEnumerable<T> public interface IPlanarCollection<T> : IEnumerable<T> where T : IPlanarElement
{ {
T this[float x, float y] { get; set; } T this[float x, float y] { get; set; }
int GetLength(int dimension); int GetLength(int dimension);

View File

@@ -1,12 +1,9 @@
using System.Collections.Generic;
namespace PathFinding namespace PathFinding
{ {
public interface IPlanarElement public interface IPlanarElement
{ {
ICollection<Path> GetPaths(); MoveSet MoveSet { get; }
bool IsUpsideDown { get; } bool IsUpsideDown { get; }
} }
} }

View File

@@ -4,11 +4,11 @@ using System.Numerics;
namespace PathFinding namespace PathFinding
{ {
[DebuggerDisplay("{Direction} - {Distance}")] [DebuggerDisplay("{Direction} - {Distance}")]
public class Path public class Move
{ {
public Vector2 Direction { get; } public Vector2 Direction { get; }
public Distance Distance { get; } public Distance Distance { get; }
public Path(Vector2 direction, Distance distance = Distance.OneStep) public Move(Vector2 direction, Distance distance = Distance.OneStep)
{ {
Direction = direction; Direction = direction;
Distance = distance; Distance = distance;

23
PathFinding/MoveSet.cs Normal file
View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace PathFinding
{
public class MoveSet
{
private readonly IPlanarElement element;
private readonly ICollection<Move> moves;
private readonly ICollection<Move> upsidedownMoves;
public MoveSet(IPlanarElement element, ICollection<Move> moves)
{
this.element = element;
this.moves = moves;
upsidedownMoves = moves.Select(_ => new Move(Vector2.Negate(_.Direction), _.Distance)).ToList();
}
public ICollection<Move> GetMoves() => element.IsUpsideDown ? upsidedownMoves : moves;
}
}

View File

@@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@@ -13,7 +12,6 @@ namespace PathFinding
/// <param name="position"></param> /// <param name="position"></param>
public delegate void Callback(T collider, Vector2 position); public delegate void Callback(T collider, Vector2 position);
private readonly IPlanarCollection<T> collection; private readonly IPlanarCollection<T> collection;
private readonly int width; private readonly int width;
private readonly int height; private readonly int height;
@@ -39,8 +37,9 @@ namespace PathFinding
return false; return false;
} }
var element = collection[origin.X, origin.Y]; var element = collection[origin.X, origin.Y];
var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); if (element == null) return false;
var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination);
if (!IsPathable(origin, destination, path.Direction)) if (!IsPathable(origin, destination, path.Direction))
{ {
// Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen. // Assumption: if a single best-choice step towards the destination cannot happen, no pathing can happen.
@@ -69,7 +68,7 @@ namespace PathFinding
public void PathEvery(Vector2 from, Callback callback) public void PathEvery(Vector2 from, Callback callback)
{ {
var element = collection[from.X, from.Y]; var element = collection[from.X, from.Y];
foreach (var path in element.GetPaths()) foreach (var path in element.MoveSet.GetMoves())
{ {
var shouldPath = true; var shouldPath = true;
var next = Vector2.Add(from, path.Direction); ; var next = Vector2.Add(from, path.Direction); ;
@@ -106,16 +105,15 @@ namespace PathFinding
} }
} }
public Path FindDirectionTowardsDestination(ICollection<Path> paths, Vector2 origin, Vector2 destination) => public static Move FindDirectionTowardsDestination(ICollection<Move> paths, Vector2 origin, Vector2 destination) =>
paths.Aggregate((a, b) => Vector2.Distance(destination, Vector2.Add(origin, a.Direction)) < Vector2.Distance(destination, Vector2.Add(origin, b.Direction)) ? a : b); paths.Aggregate((a, b) => Vector2.Distance(destination, Vector2.Add(origin, a.Direction)) < Vector2.Distance(destination, Vector2.Add(origin, b.Direction)) ? a : b);
public static bool IsPathable(Vector2 origin, Vector2 destination, T element)
public bool IsPathable(Vector2 origin, Vector2 destination, T element)
{ {
var path = FindDirectionTowardsDestination(element.GetPaths(), origin, destination); var path = FindDirectionTowardsDestination(element.MoveSet.GetMoves(), origin, destination);
return IsPathable(origin, destination, path.Direction); return IsPathable(origin, destination, path.Direction);
} }
public bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction) public static bool IsPathable(Vector2 origin, Vector2 destination, Vector2 direction)
{ {
direction = Vector2.Normalize(direction); direction = Vector2.Normalize(direction);
var next = Vector2.Add(origin, direction); var next = Vector2.Add(origin, direction);

View File

@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<EnableNETAnalyzers>true</EnableNETAnalyzers> <EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>5</AnalysisLevel>
</PropertyGroup> </PropertyGroup>
</Project> </Project>