checkpoint

This commit is contained in:
2022-06-12 12:37:32 -05:00
parent 2dcc6ca417
commit 4ca0b63564
43 changed files with 563 additions and 2128 deletions

View File

@@ -1,5 +1,4 @@
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
@@ -7,10 +6,8 @@ using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Controllers
@@ -169,25 +166,48 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
return NotFound();
}
var playerPerspective = WhichPerspective.Spectator;
if (session.Player1.Id == user.Id)
{
playerPerspective = WhichPerspective.Player1;
}
else if (session.Player2?.Id == user.Id)
{
playerPerspective = WhichPerspective.Player2;
}
var playerPerspective = session.Player2Name == user.Id
? WhichPlayer.Player2
: WhichPlayer.Player1;
communicationManager.SubscribeToGame(session, user!.Id);
var response = new GetSessionResponse()
var response = new Session
{
Game = new Models.SessionMetadata(session).ToServiceModel(),
BoardState = session.Shogi.ToServiceModel(),
MoveHistory = session.Shogi.MoveHistory.Select(_ => _.ToServiceModel()).ToList(),
PlayerPerspective = playerPerspective
BoardState = new BoardState
{
Board = null,
Player1Hand = session.Player1Hand.Select(MapPiece).ToList(),
Player2Hand = session.Player2Hand.Select(MapPiece).ToList(),
PlayerInCheck = session.InCheck.HasValue ? Map(session.InCheck.Value) : null
},
GameName = session.Name,
Player1 = session.Player1Name,
Player2 = session.Player2Name
};
return new JsonResult(response);
return this.Ok(response);
static WhichPlayer Map(Shogi.Domain.WhichPlayer whichPlayer)
{
return whichPlayer == Shogi.Domain.WhichPlayer.Player1
? WhichPlayer.Player1
: WhichPlayer.Player2;
}
static Piece MapPiece(Shogi.Domain.Pieces.Piece piece)
{
var owner = Map(piece.Owner);
var whichPiece = piece.WhichPiece switch
{
Shogi.Domain.WhichPiece.King => WhichPiece.King,
Shogi.Domain.WhichPiece.GoldGeneral => WhichPiece.GoldGeneral,
Shogi.Domain.WhichPiece.SilverGeneral => WhichPiece.SilverGeneral,
Shogi.Domain.WhichPiece.Bishop => WhichPiece.Bishop,
Shogi.Domain.WhichPiece.Rook => WhichPiece.Rook,
Shogi.Domain.WhichPiece.Knight => WhichPiece.Knight,
Shogi.Domain.WhichPiece.Lance => WhichPiece.Lance,
Shogi.Domain.WhichPiece.Pawn => WhichPiece.Pawn,
_ => throw new ArgumentException($"Unknown value for {nameof(WhichPiece)}")
};
return new Piece { IsPromoted = piece.IsPromoted, Owner = owner, WhichPiece = whichPiece };
}
}
[HttpGet]
@@ -207,8 +227,8 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
return new GetSessionsResponse
{
PlayerHasJoinedSessions = new Collection<Game>(sessionsJoinedByUser),
AllOtherSessions = new Collection<Game>(sessionsNotJoinedByUser)
PlayerHasJoinedSessions = new Collection<Session>(sessionsJoinedByUser),
AllOtherSessions = new Collection<Session>(sessionsNotJoinedByUser)
};
}

View File

@@ -1,6 +1,5 @@
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Managers;
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Api;
using Microsoft.AspNetCore.Authentication;
@@ -56,7 +55,7 @@ namespace Gameboard.ShogiUI.Sockets.Controllers
var userId = User?.UserId();
if (!string.IsNullOrEmpty(userId))
{
connectionManager.UnsubscribeFromBroadcastAndGames(userId);
connectionManager.Unsubscribe(userId);
}
await signoutTask;

View File

@@ -1,63 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.Text;
using System.Text.RegularExpressions;
namespace Gameboard.ShogiUI.Sockets.Extensions
{
public static class ModelExtensions
{
public static string GetShortName(this Models.Piece self)
{
var name = self.WhichPiece switch
{
WhichPiece.King => " K ",
WhichPiece.GoldGeneral => " 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 == WhichPerspective.Player2)
name = Regex.Replace(name, @"([^\s]+)\s", "$1.");
return name;
}
public static string PrintStateAsAscii(this Models.Shogi self)
{
var builder = new StringBuilder();
builder.Append(" Player 2(.)");
builder.AppendLine();
for (var y = 8; y >= 0; 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");
return builder.ToString();
}
}
}

View File

@@ -21,9 +21,12 @@
<ItemGroup>
<ProjectReference Include="..\Gameboard.ShogiUI.Sockets.ServiceModels\Gameboard.ShogiUI.Sockets.ServiceModels.csproj" />
<ProjectReference Include="..\PathFinding\PathFinding.csproj" />
<ProjectReference Include="..\Shogi.Domain\Shogi.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\Utility\" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,4 @@
using Gameboard.ShogiUI.Sockets.Extensions;
using Gameboard.ShogiUI.Sockets.Models;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -14,12 +13,8 @@ namespace Gameboard.ShogiUI.Sockets.Managers
public interface ISocketConnectionManager
{
Task BroadcastToAll(IResponse response);
//Task BroadcastToGame(string gameName, IResponse response);
//Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2);
void SubscribeToGame(Session session, string playerName);
void SubscribeToBroadcast(WebSocket socket, string playerName);
void UnsubscribeFromBroadcastAndGames(string playerName);
void UnsubscribeFromGame(string gameName, string playerName);
void Subscribe(WebSocket socket, string playerName);
void Unsubscribe(string playerName);
Task BroadcastToPlayers(IResponse response, params string?[] playerNames);
}
@@ -31,59 +26,23 @@ namespace Gameboard.ShogiUI.Sockets.Managers
/// <summary>Dictionary key is player name.</summary>
private readonly ConcurrentDictionary<string, WebSocket> connections;
/// <summary>Dictionary key is game name.</summary>
private readonly ConcurrentDictionary<string, Session> sessions;
private readonly ILogger<SocketConnectionManager> logger;
public SocketConnectionManager(ILogger<SocketConnectionManager> logger)
{
this.logger = logger;
connections = new ConcurrentDictionary<string, WebSocket>();
sessions = new ConcurrentDictionary<string, Session>();
}
public void SubscribeToBroadcast(WebSocket socket, string playerName)
public void Subscribe(WebSocket socket, string playerName)
{
connections.TryRemove(playerName, out var _);
connections.TryAdd(playerName, socket);
}
public void UnsubscribeFromBroadcastAndGames(string playerName)
public void Unsubscribe(string playerName)
{
connections.TryRemove(playerName, out _);
foreach (var kvp in sessions)
{
var sessionName = kvp.Key;
UnsubscribeFromGame(sessionName, playerName);
}
}
/// <summary>
/// Unsubscribes the player from their current game, then subscribes to the new game.
/// </summary>
public void SubscribeToGame(Session session, string playerName)
{
// Unsubscribe from any other games
foreach (var kvp in sessions)
{
var gameNameKey = kvp.Key;
UnsubscribeFromGame(gameNameKey, playerName);
}
// Subscribe
if (connections.TryGetValue(playerName, out var socket))
{
var s = sessions.GetOrAdd(session.Name, session);
s.Subscriptions.TryAdd(playerName, socket);
}
}
public void UnsubscribeFromGame(string gameName, string playerName)
{
if (sessions.TryGetValue(gameName, out var s))
{
s.Subscriptions.TryRemove(playerName, out _);
if (s.Subscriptions.IsEmpty) sessions.TryRemove(gameName, out _);
}
}
public async Task BroadcastToPlayers(IResponse response, params string?[] playerNames)
@@ -113,15 +72,15 @@ namespace Gameboard.ShogiUI.Sockets.Managers
tasks.Add(socket.SendTextAsync(message));
}
catch (WebSocketException webSocketException)
catch (WebSocketException)
{
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
UnsubscribeFromBroadcastAndGames(kvp.Key);
Unsubscribe(kvp.Key);
}
catch (Exception exception)
catch
{
logger.LogInformation("Tried sending a message to socket connection for user [{user}], but found the connection has closed.", kvp.Key);
UnsubscribeFromBroadcastAndGames(kvp.Key);
Unsubscribe(kvp.Key);
}
}
try
@@ -129,34 +88,11 @@ namespace Gameboard.ShogiUI.Sockets.Managers
var task = Task.WhenAll(tasks);
return task;
}
catch (Exception e)
catch
{
Console.WriteLine("Yo");
}
return Task.FromResult(0);
}
//public Task BroadcastToGame(string gameName, IResponse forPlayer1, IResponse forPlayer2)
//{
// if (sessions.TryGetValue(gameName, out var session))
// {
// var serialized1 = JsonConvert.SerializeObject(forPlayer1);
// var serialized2 = JsonConvert.SerializeObject(forPlayer2);
// return Task.WhenAll(
// session.SendToPlayer1(serialized1),
// session.SendToPlayer2(serialized2));
// }
// return Task.CompletedTask;
//}
//public Task BroadcastToGame(string gameName, IResponse messageForAllPlayers)
//{
// if (sessions.TryGetValue(gameName, out var session))
// {
// var serialized = JsonConvert.SerializeObject(messageForAllPlayers);
// return session.Broadcast(serialized);
// }
// return Task.CompletedTask;
//}
}
}

View File

@@ -1,95 +0,0 @@
using PathFinding;
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.Models
{
public static class MoveSets
{
public static readonly List<PathFinding.Move> King = new(8)
{
new PathFinding.Move(Direction.Up),
new PathFinding.Move(Direction.Left),
new PathFinding.Move(Direction.Right),
new PathFinding.Move(Direction.Down),
new PathFinding.Move(Direction.UpLeft),
new PathFinding.Move(Direction.UpRight),
new PathFinding.Move(Direction.DownLeft),
new PathFinding.Move(Direction.DownRight)
};
public static readonly List<PathFinding.Move> Bishop = new(4)
{
new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
};
public static readonly List<PathFinding.Move> PromotedBishop = new(8)
{
new PathFinding.Move(Direction.Up),
new PathFinding.Move(Direction.Left),
new PathFinding.Move(Direction.Right),
new PathFinding.Move(Direction.Down),
new PathFinding.Move(Direction.UpLeft, Distance.MultiStep),
new PathFinding.Move(Direction.UpRight, Distance.MultiStep),
new PathFinding.Move(Direction.DownLeft, Distance.MultiStep),
new PathFinding.Move(Direction.DownRight, Distance.MultiStep)
};
public static readonly List<PathFinding.Move> GoldGeneral = new(6)
{
new PathFinding.Move(Direction.Up),
new PathFinding.Move(Direction.UpLeft),
new PathFinding.Move(Direction.UpRight),
new PathFinding.Move(Direction.Left),
new PathFinding.Move(Direction.Right),
new PathFinding.Move(Direction.Down)
};
public static readonly List<PathFinding.Move> Knight = new(2)
{
new PathFinding.Move(Direction.KnightLeft),
new PathFinding.Move(Direction.KnightRight)
};
public static readonly List<PathFinding.Move> Lance = new(1)
{
new PathFinding.Move(Direction.Up, Distance.MultiStep),
};
public static readonly List<PathFinding.Move> Pawn = new(1)
{
new PathFinding.Move(Direction.Up)
};
public static readonly List<PathFinding.Move> Rook = new(4)
{
new PathFinding.Move(Direction.Up, Distance.MultiStep),
new PathFinding.Move(Direction.Left, Distance.MultiStep),
new PathFinding.Move(Direction.Right, Distance.MultiStep),
new PathFinding.Move(Direction.Down, Distance.MultiStep)
};
public static readonly List<PathFinding.Move> PromotedRook = new(8)
{
new PathFinding.Move(Direction.Up, Distance.MultiStep),
new PathFinding.Move(Direction.Left, Distance.MultiStep),
new PathFinding.Move(Direction.Right, Distance.MultiStep),
new PathFinding.Move(Direction.Down, Distance.MultiStep),
new PathFinding.Move(Direction.UpLeft),
new PathFinding.Move(Direction.UpRight),
new PathFinding.Move(Direction.DownLeft),
new PathFinding.Move(Direction.DownRight)
};
public static readonly List<PathFinding.Move> SilverGeneral = new(4)
{
new PathFinding.Move(Direction.Up),
new PathFinding.Move(Direction.UpLeft),
new PathFinding.Move(Direction.UpRight),
new PathFinding.Move(Direction.DownLeft),
new PathFinding.Move(Direction.DownRight)
};
}
}

View File

@@ -1,70 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using PathFinding;
using System.Diagnostics;
namespace Gameboard.ShogiUI.Sockets.Models
{
[DebuggerDisplay("{WhichPiece} {Owner}")]
public class Piece : IPlanarElement
{
public WhichPiece WhichPiece { get; }
public WhichPerspective Owner { get; private set; }
public bool IsPromoted { get; private set; }
public bool IsUpsideDown => Owner == WhichPerspective.Player2;
public Piece(WhichPiece piece, WhichPerspective owner, bool isPromoted = false)
{
WhichPiece = piece;
Owner = owner;
IsPromoted = isPromoted;
}
public Piece(Piece piece) : this(piece.WhichPiece, piece.Owner, piece.IsPromoted)
{
}
public bool CanPromote => !IsPromoted
&& WhichPiece != WhichPiece.King
&& WhichPiece != WhichPiece.GoldGeneral;
public void ToggleOwnership()
{
Owner = Owner == WhichPerspective.Player1
? WhichPerspective.Player2
: WhichPerspective.Player1;
}
public void Promote() => IsPromoted = CanPromote;
public void Demote() => IsPromoted = false;
public void Capture()
{
ToggleOwnership();
Demote();
}
// TODO: There is no reason to make "new" MoveSets every time this property is accessed.
public MoveSet MoveSet => WhichPiece switch
{
WhichPiece.King => new MoveSet(this, MoveSets.King),
WhichPiece.GoldGeneral => new MoveSet(this, MoveSets.GoldGeneral),
WhichPiece.SilverGeneral => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.SilverGeneral),
WhichPiece.Bishop => new MoveSet(this, IsPromoted ? MoveSets.PromotedBishop : MoveSets.Bishop),
WhichPiece.Rook => new MoveSet(this, IsPromoted ? MoveSets.PromotedRook : MoveSets.Rook),
WhichPiece.Knight => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Knight),
WhichPiece.Lance => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Lance),
WhichPiece.Pawn => new MoveSet(this, IsPromoted ? MoveSets.GoldGeneral : MoveSets.Pawn),
_ => throw new System.NotImplementedException()
};
public ServiceModels.Types.Piece ToServiceModel()
{
return new ServiceModels.Types.Piece
{
IsPromoted = IsPromoted,
Owner = Owner,
WhichPiece = WhichPiece
};
}
}
}

View File

@@ -1,38 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Newtonsoft.Json;
using System.Collections.Concurrent;
using System.Net.WebSockets;
namespace Gameboard.ShogiUI.Sockets.Models
{
public class Session
{
// TODO: Separate subscriptions to the Session from the Session.
[JsonIgnore] public ConcurrentDictionary<string, WebSocket> Subscriptions { get; }
public string Name { get; }
public User Player1 { get; }
public User? Player2 { get; private set; }
public bool IsPrivate { get; }
// TODO: Don't retain the entire rules system within the Session model. It just needs the board state after rules are applied.
public Shogi Shogi { get; }
public Session(string name, bool isPrivate, Shogi shogi, User player1, User? player2 = null)
{
Subscriptions = new ConcurrentDictionary<string, WebSocket>();
Name = name;
Player1 = player1;
Player2 = player2;
IsPrivate = isPrivate;
Shogi = shogi;
}
public void SetPlayer2(User user)
{
Player2 = user;
}
public Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
}
}

View File

@@ -32,6 +32,6 @@
public bool IsSeated(User user) => user.Id == Player1.Id || user.Id == Player2?.Id;
public ServiceModels.Types.Game ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
public ServiceModels.Types.Session ToServiceModel() => new(Name, Player1.DisplayName, Player2?.DisplayName);
}
}

View File

@@ -1,463 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Gameboard.ShogiUI.Sockets.Utilities;
using PathFinding;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.Sockets.Models
{
/// <summary>
/// Facilitates Shogi board state transitions, cognisant of Shogi rules.
/// The board is always from Player1's perspective.
/// [0,0] is the lower-left position, [8,8] is the higher-right position
/// </summary>
public class Shogi
{
private delegate void MoveSetCallback(Piece piece, Vector2 position);
private readonly PathFinder2D<Piece> pathFinder;
private Shogi? validationBoard;
private Vector2 player1King;
private Vector2 player2King;
private List<Piece> Hand => WhoseTurn == WhichPerspective.Player1 ? Player1Hand : Player2Hand;
public List<Piece> Player1Hand { get; }
public List<Piece> Player2Hand { get; }
public CoordsToNotationCollection Board { get; } //TODO: Hide this being a getter method
public List<Move> MoveHistory { get; }
public WhichPerspective WhoseTurn => MoveHistory.Count % 2 == 0 ? WhichPerspective.Player1 : WhichPerspective.Player2;
public WhichPerspective? InCheck { get; private set; }
public bool IsCheckmate { get; private set; }
public string Error { get; private set; }
public Shogi()
{
Board = new CoordsToNotationCollection();
MoveHistory = new List<Move>(20);
Player1Hand = new List<Piece>();
Player2Hand = new List<Piece>();
pathFinder = new PathFinder2D<Piece>(Board, 9, 9);
player1King = new Vector2(4, 0);
player2King = new Vector2(4, 8);
Error = string.Empty;
InitializeBoardState();
}
public Shogi(IList<Move> moves) : this()
{
for (var i = 0; i < moves.Count; i++)
{
if (!Move(moves[i]))
{
// Todo: Add some smarts to know why a move was invalid. In check? Piece not found? etc.
throw new InvalidOperationException($"Unable to construct ShogiBoard with the given move at index {i}. {Error}");
}
}
}
private Shogi(Shogi toCopy)
{
Board = new CoordsToNotationCollection();
foreach (var kvp in toCopy.Board)
{
Board[kvp.Key] = kvp.Value == null ? null : new Piece(kvp.Value);
}
pathFinder = new PathFinder2D<Piece>(Board, 9, 9);
MoveHistory = new List<Move>(toCopy.MoveHistory);
Player1Hand = new List<Piece>(toCopy.Player1Hand);
Player2Hand = new List<Piece>(toCopy.Player2Hand);
player1King = toCopy.player1King;
player2King = toCopy.player2King;
Error = toCopy.Error;
}
public bool Move(Move move)
{
var otherPlayer = WhoseTurn == WhichPerspective.Player1 ? WhichPerspective.Player2 : WhichPerspective.Player1;
var moveSuccess = TryMove(move);
if (!moveSuccess)
{
return false;
}
// Evaluate check
if (EvaluateCheckAfterMove(move, otherPlayer))
{
InCheck = otherPlayer;
IsCheckmate = EvaluateCheckmate();
}
else
{
InCheck = null;
}
return true;
}
/// <summary>
/// Attempts a given move. Returns false if the move is illegal.
/// </summary>
private bool TryMove(Move move)
{
// Try making the move in a "throw away" board.
if (validationBoard == null)
{
validationBoard = new Shogi(this);
}
var isValid = move.PieceFromHand.HasValue
? validationBoard.PlaceFromHand(move)
: validationBoard.PlaceFromBoard(move);
if (!isValid)
{
// Surface the error description.
Error = validationBoard.Error;
// Invalidate the "throw away" board.
validationBoard = null;
return false;
}
// If already in check, assert the move that resulted in check no longer results in check.
if (InCheck == WhoseTurn)
{
if (validationBoard.EvaluateCheckAfterMove(MoveHistory[^1], WhoseTurn))
{
// Sneakily using this.WhoseTurn instead of validationBoard.WhoseTurn;
return false;
}
}
// The move is valid and legal; update board state.
if (move.PieceFromHand.HasValue) PlaceFromHand(move);
else PlaceFromBoard(move);
return true;
}
/// <returns>True if the move was successful.</returns>
private bool PlaceFromHand(Move move)
{
var index = Hand.FindIndex(p => p.WhichPiece == move.PieceFromHand);
if (index < 0)
{
Error = $"{move.PieceFromHand} does not exist in the hand.";
return false;
}
if (Board[move.To] != null)
{
Error = $"Illegal move - attempting to capture while playing a piece from the hand.";
return false;
}
switch (move.PieceFromHand!.Value)
{
case WhichPiece.Knight:
{
// Knight cannot be placed onto the farthest two ranks from the hand.
if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y > 6)
|| (WhoseTurn == WhichPerspective.Player2 && move.To.Y < 2))
{
Error = $"Knight has no valid moves after placed.";
return false;
}
break;
}
case WhichPiece.Lance:
case WhichPiece.Pawn:
{
// Lance and Pawn cannot be placed onto the farthest rank from the hand.
if ((WhoseTurn == WhichPerspective.Player1 && move.To.Y == 8)
|| (WhoseTurn == WhichPerspective.Player2 && move.To.Y == 0))
{
Error = $"{move.PieceFromHand} has no valid moves after placed.";
return false;
}
break;
}
}
// Mutate the board.
Board[move.To] = Hand[index];
Hand.RemoveAt(index);
MoveHistory.Add(move);
return true;
}
/// <returns>True if the move was successful.</returns>
private bool PlaceFromBoard(Move move)
{
var fromPiece = Board[move.From!.Value];
if (fromPiece == null)
{
Error = $"No piece exists at {nameof(move)}.{nameof(move.From)}.";
return false; // Invalid move
}
if (fromPiece.Owner != WhoseTurn)
{
Error = "Not allowed to move the opponents piece";
return false; // Invalid move; cannot move other players pieces.
}
if (IsPathable(move.From.Value, move.To) == false)
{
Error = $"Illegal move for {fromPiece.WhichPiece}. {nameof(move)}.{nameof(move.To)} is not part of the move-set.";
return false; // Invalid move; move not part of move-set.
}
var captured = Board[move.To];
if (captured != null)
{
if (captured.Owner == WhoseTurn) return false; // Invalid move; cannot capture your own piece.
captured.Capture();
Hand.Add(captured);
}
//Mutate the board.
if (move.IsPromotion)
{
if (WhoseTurn == WhichPerspective.Player1 && (move.To.Y > 5 || move.From.Value.Y > 5))
{
fromPiece.Promote();
}
else if (WhoseTurn == WhichPerspective.Player2 && (move.To.Y < 3 || move.From.Value.Y < 3))
{
fromPiece.Promote();
}
}
Board[move.To] = fromPiece;
Board[move.From!.Value] = null;
if (fromPiece.WhichPiece == WhichPiece.King)
{
if (fromPiece.Owner == WhichPerspective.Player1)
{
player1King.X = move.To.X;
player1King.Y = move.To.Y;
}
else if (fromPiece.Owner == WhichPerspective.Player2)
{
player2King.X = move.To.X;
player2King.Y = move.To.Y;
}
}
MoveHistory.Add(move);
return true;
}
private bool IsPathable(Vector2 from, Vector2 to)
{
var piece = Board[from];
if (piece == null) return false;
var isObstructed = false;
var isPathable = pathFinder.PathTo(from, to, (other, position) =>
{
if (other.Owner == piece.Owner) isObstructed = true;
});
return !isObstructed && isPathable;
}
#region Rules Validation
private bool EvaluateCheckAfterMove(Move move, WhichPerspective WhichPerspective)
{
if (WhichPerspective == InCheck) return true; // If we already know the player is in check, don't bother.
var isCheck = false;
var kingPosition = WhichPerspective == WhichPerspective.Player1 ? player1King : player2King;
// Check if the move put the king in check.
if (pathFinder.PathTo(move.To, kingPosition)) return true;
if (move.From.HasValue)
{
// Get line equation from king through the now-unoccupied location.
var direction = Vector2.Subtract(kingPosition, move.From!.Value);
var slope = Math.Abs(direction.Y / direction.X);
// If absolute slope is 45°, look for a bishop along the line.
// If absolute slope is 0° or 90°, look for a rook along the line.
// if absolute slope is 0°, look for lance along the line.
if (float.IsInfinity(slope))
{
// if slope of the move is also infinity...can skip this?
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
{
if (piece.Owner != WhichPerspective)
{
switch (piece.WhichPiece)
{
case WhichPiece.Rook:
isCheck = true;
break;
case WhichPiece.Lance:
if (!piece.IsPromoted) isCheck = true;
break;
}
}
});
}
else if (slope == 1)
{
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
{
if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Bishop)
{
isCheck = true;
}
});
}
else if (slope == 0)
{
pathFinder.LinePathTo(kingPosition, direction, (piece, position) =>
{
if (piece.Owner != WhichPerspective && piece.WhichPiece == WhichPiece.Rook)
{
isCheck = true;
}
});
}
}
else
{
// TODO: Check for illegal move from hand. It is illegal to place from the hand such that you check-mate your opponent.
// Go read the shogi rules to be sure this is true.
}
return isCheck;
}
private bool EvaluateCheckmate()
{
if (!InCheck.HasValue) return false;
// Assume true and try to disprove.
var isCheckmate = true;
Board.ForEachNotNull((piece, from) => // For each piece...
{
// Short circuit
if (!isCheckmate) return;
if (piece.Owner == InCheck) // ...owned by the player in check...
{
// ...evaluate if any move gets the player out of check.
pathFinder.PathEvery(from, (other, position) =>
{
if (validationBoard == null) validationBoard = new Shogi(this);
var moveToTry = new Move(from, position);
var moveSuccess = validationBoard.TryMove(moveToTry);
if (moveSuccess)
{
validationBoard = null;
if (!EvaluateCheckAfterMove(moveToTry, InCheck.Value))
{
isCheckmate = false;
}
}
});
}
});
return isCheckmate;
}
#endregion
private void InitializeBoardState()
{
Board["A1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1);
Board["B1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1);
Board["C1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1);
Board["D1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1);
Board["E1"] = new Piece(WhichPiece.King, WhichPerspective.Player1);
Board["F1"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player1);
Board["G1"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player1);
Board["H1"] = new Piece(WhichPiece.Knight, WhichPerspective.Player1);
Board["I1"] = new Piece(WhichPiece.Lance, WhichPerspective.Player1);
Board["A2"] = null;
Board["B2"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player1);
Board["C2"] = null;
Board["D2"] = null;
Board["E2"] = null;
Board["F2"] = null;
Board["G2"] = null;
Board["H2"] = new Piece(WhichPiece.Rook, WhichPerspective.Player1);
Board["I2"] = null;
Board["A3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["B3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["C3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["D3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["E3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["F3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["G3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["H3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["I3"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player1);
Board["A4"] = null;
Board["B4"] = null;
Board["C4"] = null;
Board["D4"] = null;
Board["E4"] = null;
Board["F4"] = null;
Board["G4"] = null;
Board["H4"] = null;
Board["I4"] = null;
Board["A5"] = null;
Board["B5"] = null;
Board["C5"] = null;
Board["D5"] = null;
Board["E5"] = null;
Board["F5"] = null;
Board["G5"] = null;
Board["H5"] = null;
Board["I5"] = null;
Board["A6"] = null;
Board["B6"] = null;
Board["C6"] = null;
Board["D6"] = null;
Board["E6"] = null;
Board["F6"] = null;
Board["G6"] = null;
Board["H6"] = null;
Board["I6"] = null;
Board["A7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["B7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["C7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["D7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["E7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["F7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["G7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["H7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["I7"] = new Piece(WhichPiece.Pawn, WhichPerspective.Player2);
Board["A8"] = null;
Board["B8"] = new Piece(WhichPiece.Rook, WhichPerspective.Player2);
Board["C8"] = null;
Board["D8"] = null;
Board["E8"] = null;
Board["F8"] = null;
Board["G8"] = null;
Board["H8"] = new Piece(WhichPiece.Bishop, WhichPerspective.Player2);
Board["I8"] = null;
Board["A9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2);
Board["B9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2);
Board["C9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2);
Board["D9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2);
Board["E9"] = new Piece(WhichPiece.King, WhichPerspective.Player2);
Board["F9"] = new Piece(WhichPiece.GoldGeneral, WhichPerspective.Player2);
Board["G9"] = new Piece(WhichPiece.SilverGeneral, WhichPerspective.Player2);
Board["H9"] = new Piece(WhichPiece.Knight, WhichPerspective.Player2);
Board["I9"] = new Piece(WhichPiece.Lance, WhichPerspective.Player2);
}
public BoardState ToServiceModel()
{
return new BoardState
{
Board = Board.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToServiceModel()),
PlayerInCheck = InCheck,
WhoseTurn = WhoseTurn,
Player1Hand = Player1Hand.Select(_ => _.ToServiceModel()).ToList(),
Player2Hand = Player2Hand.Select(_ => _.ToServiceModel()).ToList()
};
}
}
}

View File

@@ -1,65 +1,52 @@
using Gameboard.ShogiUI.Sockets.Utilities;
using Shogi.Domain;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class BoardStateDocument : CouchDocument
{
public string Name { get; set; }
public class BoardStateDocument : CouchDocument
{
public string Name { get; set; }
/// <summary>
/// A dictionary where the key is a board-notation position, like D3.
/// </summary>
public Dictionary<string, Piece?> Board { get; set; }
/// <summary>
/// A dictionary where the key is a board-notation position, like D3.
/// </summary>
public Dictionary<string, Piece?> Board { get; set; }
public Piece[] Player1Hand { get; set; }
public Piece[] Player1Hand { get; set; }
public Piece[] Player2Hand { get; set; }
public Piece[] Player2Hand { get; set; }
/// <summary>
/// Move is null for first BoardState of a session - before anybody has made moves.
/// </summary>
public Move? Move { get; set; }
/// <summary>
/// Move is null for first BoardState of a session - before anybody has made moves.
/// </summary>
public Move? Move { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public BoardStateDocument() : base(WhichDocumentType.BoardState)
{
Name = string.Empty;
Board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
Player1Hand = Array.Empty<Piece>();
Player2Hand = Array.Empty<Piece>();
}
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public BoardStateDocument() : base(WhichDocumentType.BoardState)
{
Name = string.Empty;
Board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
Player1Hand = Array.Empty<Piece>();
Player2Hand = Array.Empty<Piece>();
}
public BoardStateDocument(string sessionName, Models.Shogi shogi)
: base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState)
{
Name = sessionName;
Board = new Dictionary<string, Piece?>(81, StringComparer.OrdinalIgnoreCase);
public BoardStateDocument(string sessionName, Session shogi)
: base($"{sessionName}-{DateTime.Now:O}", WhichDocumentType.BoardState)
{
static Piece MapPiece(Shogi.Domain.Pieces.Piece piece)
{
return new Piece { IsPromoted = piece.IsPromoted, Owner = piece.Owner, WhichPiece = piece.WhichPiece };
}
for (var x = 0; x < 9; x++)
for (var y = 0; y < 9; y++)
{
var position = new Vector2(x, y);
var piece = shogi.Board[y, x];
Name = sessionName;
Board = shogi.BoardState.ToDictionary(kvp => kvp.Key, kvp => kvp.Value == null ? null : MapPiece(kvp.Value));
if (piece != null)
{
var positionNotation = NotationHelper.ToBoardNotation(position);
Board[positionNotation] = new Piece(piece);
}
}
Player1Hand = shogi.Player1Hand.Select(model => new Piece(model)).ToArray();
Player2Hand = shogi.Player2Hand.Select(model => new Piece(model)).ToArray();
if (shogi.MoveHistory.Count > 0)
{
Move = new Move(shogi.MoveHistory[^1]);
}
}
}
Player1Hand = shogi.Player1Hand.Select(piece => MapPiece(piece)).ToArray();
Player2Hand = shogi.Player2Hand.Select(piece => MapPiece(piece)).ToArray();
}
}
}

View File

@@ -1,5 +1,4 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using System.Numerics;
using Shogi.Domain;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
@@ -29,28 +28,5 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
To = string.Empty;
}
public Move(Models.Move move)
{
if (move.From.HasValue)
{
From = ToBoardNotation(move.From.Value);
}
IsPromotion = move.IsPromotion;
To = ToBoardNotation(move.To);
PieceFromHand = move.PieceFromHand;
}
private static readonly char A = 'A';
private static string ToBoardNotation(Vector2 vector)
{
var file = (char)(vector.X + A);
var rank = vector.Y + 1;
return $"{file}{rank}";
}
public Models.Move ToDomainModel() => PieceFromHand.HasValue
? new(PieceFromHand.Value, To)
: new(From!, To, IsPromotion);
}
}

View File

@@ -1,27 +1,11 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Shogi.Domain;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class Piece
{
public bool IsPromoted { get; set; }
public WhichPerspective Owner { get; set; }
public WhichPiece WhichPiece { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
/// </summary>
public Piece()
{
}
public Piece(Models.Piece piece)
{
IsPromoted = piece.IsPromoted;
Owner = piece.Owner;
WhichPiece = piece.WhichPiece;
}
public Models.Piece ToDomainModel() => new(WhichPiece, Owner, IsPromoted);
}
public class Piece
{
public bool IsPromoted { get; set; }
public WhichPlayer Owner { get; set; }
public WhichPiece WhichPiece { get; set; }
}
}

View File

@@ -1,14 +1,11 @@
using System.Collections.Generic;
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
{
public class SessionDocument : CouchDocument
public class SessionDocument : CouchDocument
{
public string Name { get; set; }
public string Player1Id { get; set; }
public string? Player2Id { get; set; }
public bool IsPrivate { get; set; }
public IList<BoardStateDocument> History { get; set; }
/// <summary>
/// Default constructor and setters are for deserialization.
@@ -18,17 +15,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
Name = string.Empty;
Player1Id = string.Empty;
Player2Id = string.Empty;
History = new List<BoardStateDocument>(0);
}
public SessionDocument(Models.Session session)
: base(session.Name, WhichDocumentType.Session)
{
Name = session.Name;
Player1Id = session.Player1.Id;
Player2Id = session.Player2?.Id;
IsPrivate = session.IsPrivate;
History = new List<BoardStateDocument>(0);
}
public SessionDocument(Models.SessionMetadata sessionMetaData)
@@ -38,7 +24,6 @@ namespace Gameboard.ShogiUI.Sockets.Repositories.CouchModels
Player1Id = sessionMetaData.Player1.Id;
Player2Id = sessionMetaData.Player2?.Id;
IsPrivate = sessionMetaData.IsPrivate;
History = new List<BoardStateDocument>(0);
}
}
}

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Shogi.Domain;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -11,282 +12,302 @@ using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace Gameboard.ShogiUI.Sockets.Repositories
{
public interface IGameboardRepository
{
Task<bool> CreateBoardState(Models.Session session);
Task<bool> CreateSession(Models.SessionMetadata session);
Task<bool> CreateUser(Models.User user);
Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas();
Task<Models.Session?> ReadSession(string name);
Task<bool> UpdateSession(Models.SessionMetadata session);
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
Task<Models.User?> ReadUser(string userName);
}
public interface IGameboardRepository
{
Task<bool> CreateBoardState(Session session);
Task<bool> CreateSession(Models.SessionMetadata session);
Task<bool> CreateUser(Models.User user);
Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas();
Task<Session?> ReadSession(string name);
Task<bool> UpdateSession(Models.SessionMetadata session);
Task<Models.SessionMetadata?> ReadSessionMetaData(string name);
Task<Models.User?> ReadUser(string userName);
}
public class GameboardRepository : IGameboardRepository
{
/// <summary>
/// Returns session, board state, and user documents, grouped by session.
/// </summary>
private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate";
/// <summary>
/// Returns session and user documents, grouped by session.
/// </summary>
private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata";
private static readonly string View_User = "_design/user/_view/user";
private const string ApplicationJson = "application/json";
private readonly HttpClient client;
private readonly ILogger<GameboardRepository> logger;
public class GameboardRepository : IGameboardRepository
{
/// <summary>
/// Returns session, board state, and user documents, grouped by session.
/// </summary>
private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate";
/// <summary>
/// Returns session and user documents, grouped by session.
/// </summary>
private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata";
private static readonly string View_User = "_design/user/_view/user";
private const string ApplicationJson = "application/json";
private readonly HttpClient client;
private readonly ILogger<GameboardRepository> logger;
public GameboardRepository(IHttpClientFactory clientFactory, ILogger<GameboardRepository> logger)
{
client = clientFactory.CreateClient("couchdb");
this.logger = logger;
}
public GameboardRepository(IHttpClientFactory clientFactory, ILogger<GameboardRepository> logger)
{
client = clientFactory.CreateClient("couchdb");
this.logger = logger;
}
public async Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas()
{
var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null)
{
var groupedBySession = result.rows.GroupBy(row => row.id);
var sessions = new List<Models.SessionMetadata>(result.total_rows / 3);
foreach (var group in groupedBySession)
{
/**
public async Task<Collection<Models.SessionMetadata>> ReadSessionMetadatas()
{
var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString();
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null)
{
var groupedBySession = result.rows.GroupBy(row => row.id);
var sessions = new List<Models.SessionMetadata>(result.total_rows / 3);
foreach (var group in groupedBySession)
{
/**
* A group contains 3 elements.
* 1) The session metadata.
* 2) User document of Player1.
* 3) User document of Player2.
*/
var session = group.FirstOrDefault()?.doc.ToObject<SessionDocument>();
var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject<UserDocument>();
var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject<UserDocument>();
if (session != null && player1Doc != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2));
}
}
return new Collection<Models.SessionMetadata>(sessions);
}
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>());
}
var session = group.FirstOrDefault()?.doc.ToObject<SessionDocument>();
var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject<UserDocument>();
var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject<UserDocument>();
if (session != null && player1Doc != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
sessions.Add(new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2));
}
}
return new Collection<Models.SessionMetadata>(sessions);
}
return new Collection<Models.SessionMetadata>(Array.Empty<Models.SessionMetadata>());
}
public async Task<Models.Session?> ReadSession(string name)
{
var queryParams = new QueryBuilder
{
{ "include_docs", "true" },
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
}.ToQueryString();
var query = $"{View_SessionWithBoardState}{queryParams}";
logger.LogInformation("ReadSession() query: {query}", query);
var response = await client.GetAsync(query);
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null && result.rows.Length > 2)
{
var group = result.rows;
/**
* A group contains 3 type of elements.
* 1) The session metadata.
* 2) User documents of Player1 and Player2.
public async Task<Session?> ReadSession(string name)
{
static Shogi.Domain.Pieces.Piece? MapPiece(Piece? piece)
{
return piece == null
? null
: Shogi.Domain.Pieces.Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted);
}
var queryParams = new QueryBuilder
{
{ "include_docs", "true" },
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
}.ToQueryString();
var query = $"{View_SessionWithBoardState}{queryParams}";
logger.LogInformation("ReadSession() query: {query}", query);
var response = await client.GetAsync(query);
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null && result.rows.Length > 2)
{
var group = result.rows;
/**
* A group contains multiple elements.
* 0) The session metadata.
* 1) User documents of Player1.
* 2) User documents of Player1.
* 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :(
* 3) BoardState
* Everything Else) Snapshots of the boardstate after every player move.
*/
var session = group[0].doc.ToObject<SessionDocument>();
var player1Doc = group[1].doc.ToObject<UserDocument>();
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
? group[2].doc.ToObject<UserDocument>()
: null;
var moves = group
.Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session.
// TODO: Deserialize just the Move property.
.Select(row => row.doc.ToObject<BoardStateDocument>())
.Select(boardState =>
{
var move = boardState!.Move!;
return move.PieceFromHand.HasValue
? new Models.Move(move.PieceFromHand.Value, move.To)
: new Models.Move(move.From!, move.To, move.IsPromotion);
})
.ToList();
var session = group[0].doc.ToObject<SessionDocument>();
var player1Doc = group[1].doc.ToObject<UserDocument>();
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
? group[2].doc.ToObject<UserDocument>()
: null;
var boardState = group.Last().doc.ToObject<BoardStateDocument>();
var shogi = new Models.Shogi(moves);
if (session != null && player1Doc != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
return new Models.Session(session.Name, session.IsPrivate, shogi, new(player1Doc), player2);
}
}
return null;
}
if (session != null && player1Doc != null && boardState != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1Doc.DisplayName, player2Doc?.DisplayName);
var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value)));
return new Session(shogiBoardState, metaData);
}
}
return null;
}
public async Task<Models.SessionMetadata?> ReadSessionMetaData(string name)
{
var queryParams = new QueryBuilder
{
{ "include_docs", "true" },
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
}.ToQueryString();
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null && result.rows.Length > 2)
{
var group = result.rows;
/**
public async Task<Models.SessionMetadata?> ReadSessionMetaData(string name)
{
var queryParams = new QueryBuilder
{
{ "include_docs", "true" },
{ "startkey", JsonConvert.SerializeObject(new [] {name}) },
{ "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) }
}.ToQueryString();
var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<JObject>>(responseContent);
if (result != null && result.rows.Length > 2)
{
var group = result.rows;
/**
* A group contains 3 elements.
* 1) The session metadata.
* 2) User document of Player1.
* 3) User document of Player2.
*/
var session = group[0].doc.ToObject<SessionDocument>();
var player1Doc = group[1].doc.ToObject<UserDocument>();
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
? group[2].doc.ToObject<UserDocument>()
: null;
if (session != null && player1Doc != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2);
}
}
return null;
}
var session = group[0].doc.ToObject<SessionDocument>();
var player1Doc = group[1].doc.ToObject<UserDocument>();
var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value<string>();
var player2Doc = group2DocumentType == WhichDocumentType.User.ToString()
? group[2].doc.ToObject<UserDocument>()
: null;
if (session != null && player1Doc != null)
{
var player2 = player2Doc == null ? null : new Models.User(player2Doc);
return new Models.SessionMetadata(session.Name, session.IsPrivate, new(player1Doc), player2);
}
}
return null;
}
/// <summary>
/// Saves a snapshot of board state and the most recent move.
/// </summary>
public async Task<bool> CreateBoardState(Models.Session session)
{
var boardStateDocument = new BoardStateDocument(session.Name, session.Shogi);
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
}
/// <summary>
/// Saves a snapshot of board state and the most recent move.
/// </summary>
public async Task<bool> CreateBoardState(Session session)
{
Piece? MapPiece(Shogi.Domain.Pieces.Piece? piece)
{
return piece == null
? null
: new Piece { IsPromoted = piece.IsPromoted, Owner = piece.Owner, WhichPiece = piece.WhichPiece };
}
public async Task<bool> CreateSession(Models.SessionMetadata session)
{
var sessionDocument = new SessionDocument(session);
var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent);
var boardStateDocument = new BoardStateDocument(session.Name, session);
var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
}
var boardStateDocument = new BoardStateDocument(session.Name, new Models.Shogi());
var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
public async Task<bool> CreateSession(Models.SessionMetadata session)
{
var sessionDocument = new SessionDocument(session);
var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson);
var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent);
if ((await postSessionDocumentTask).IsSuccessStatusCode)
{
var response = await client.PostAsync(string.Empty, boardStateContent);
return response.IsSuccessStatusCode;
}
return false;
}
var boardStateDocument = new BoardStateDocument(session.Name, new Session());
var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson);
public async Task<bool> UpdateSession(Models.SessionMetadata session)
{
// GET existing session to get revisionId.
var readResponse = await client.GetAsync(session.Name);
if (!readResponse.IsSuccessStatusCode) return false;
var sessionDocument = JsonConvert.DeserializeObject<SessionDocument>(await readResponse.Content.ReadAsStringAsync());
if ((await postSessionDocumentTask).IsSuccessStatusCode)
{
var response = await client.PostAsync(string.Empty, boardStateContent);
return response.IsSuccessStatusCode;
}
return false;
}
// PUT the document with the revisionId.
var couchModel = new SessionDocument(session)
{
RevisionId = sessionDocument?.RevisionId
};
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PutAsync(couchModel.Id, content);
return response.IsSuccessStatusCode;
}
//public async Task<bool> PutJoinPublicSession(PutJoinPublicSession request)
//{
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
// var response = await client.PutAsync(JoinSessionRoute, content);
// var json = await response.Content.ReadAsStringAsync();
// return JsonConvert.DeserializeObject<PutJoinPublicSessionResponse>(json).JoinSucceeded;
//}
public async Task<bool> UpdateSession(Models.SessionMetadata session)
{
// GET existing session to get revisionId.
var readResponse = await client.GetAsync(session.Name);
if (!readResponse.IsSuccessStatusCode) return false;
var sessionDocument = JsonConvert.DeserializeObject<SessionDocument>(await readResponse.Content.ReadAsStringAsync());
//public async Task<string> PostJoinPrivateSession(PostJoinPrivateSession request)
//{
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
// var response = await client.PostAsync(JoinSessionRoute, content);
// var json = await response.Content.ReadAsStringAsync();
// var deserialized = JsonConvert.DeserializeObject<PostJoinPrivateSessionResponse>(json);
// if (deserialized.JoinSucceeded)
// {
// return deserialized.SessionName;
// }
// return null;
//}
// PUT the document with the revisionId.
var couchModel = new SessionDocument(session)
{
RevisionId = sessionDocument?.RevisionId
};
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PutAsync(couchModel.Id, content);
return response.IsSuccessStatusCode;
}
//public async Task<bool> PutJoinPublicSession(PutJoinPublicSession request)
//{
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
// var response = await client.PutAsync(JoinSessionRoute, content);
// var json = await response.Content.ReadAsStringAsync();
// return JsonConvert.DeserializeObject<PutJoinPublicSessionResponse>(json).JoinSucceeded;
//}
//public async Task<List<Move>> GetMoves(string gameName)
//{
// var uri = $"Session/{gameName}/Moves";
// var get = await client.GetAsync(Uri.EscapeUriString(uri));
// var json = await get.Content.ReadAsStringAsync();
// if (string.IsNullOrWhiteSpace(json))
// {
// return new List<Move>();
// }
// var response = JsonConvert.DeserializeObject<GetMovesResponse>(json);
// return response.Moves.Select(m => new Move(m)).ToList();
//}
//public async Task<string> PostJoinPrivateSession(PostJoinPrivateSession request)
//{
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
// var response = await client.PostAsync(JoinSessionRoute, content);
// var json = await response.Content.ReadAsStringAsync();
// var deserialized = JsonConvert.DeserializeObject<PostJoinPrivateSessionResponse>(json);
// if (deserialized.JoinSucceeded)
// {
// return deserialized.SessionName;
// }
// return null;
//}
//public async Task PostMove(string gameName, PostMove request)
//{
// var uri = $"Session/{gameName}/Move";
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
// await client.PostAsync(Uri.EscapeUriString(uri), content);
//}
//public async Task<List<Move>> GetMoves(string gameName)
//{
// var uri = $"Session/{gameName}/Moves";
// var get = await client.GetAsync(Uri.EscapeUriString(uri));
// var json = await get.Content.ReadAsStringAsync();
// if (string.IsNullOrWhiteSpace(json))
// {
// return new List<Move>();
// }
// var response = JsonConvert.DeserializeObject<GetMovesResponse>(json);
// return response.Moves.Select(m => new Move(m)).ToList();
//}
public async Task<string> PostJoinCode(string gameName, string userName)
{
// var uri = $"JoinCode/{gameName}";
// var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName });
// var content = new StringContent(serialized, Encoding.UTF8, MediaType);
// var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync();
// return JsonConvert.DeserializeObject<PostJoinCodeResponse>(json).JoinCode;
return string.Empty;
}
//public async Task PostMove(string gameName, PostMove request)
//{
// var uri = $"Session/{gameName}/Move";
// var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType);
// await client.PostAsync(Uri.EscapeUriString(uri), content);
//}
public async Task<Models.User?> ReadUser(string id)
{
var queryParams = new QueryBuilder
{
{ "include_docs", "true" },
{ "key", JsonConvert.SerializeObject(id) },
}.ToQueryString();
var response = await client.GetAsync($"{View_User}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<UserDocument>>(responseContent);
if (result != null && result.rows.Length > 0)
{
return new Models.User(result.rows[0].doc);
}
public async Task<string> PostJoinCode(string gameName, string userName)
{
// var uri = $"JoinCode/{gameName}";
// var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName });
// var content = new StringContent(serialized, Encoding.UTF8, MediaType);
// var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync();
// return JsonConvert.DeserializeObject<PostJoinCodeResponse>(json).JoinCode;
return string.Empty;
}
return null;
}
public async Task<Models.User?> ReadUser(string id)
{
var queryParams = new QueryBuilder
{
{ "include_docs", "true" },
{ "key", JsonConvert.SerializeObject(id) },
}.ToQueryString();
var response = await client.GetAsync($"{View_User}{queryParams}");
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<CouchViewResult<UserDocument>>(responseContent);
if (result != null && result.rows.Length > 0)
{
return new Models.User(result.rows[0].doc);
}
public async Task<bool> CreateUser(Models.User user)
{
var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
}
return null;
}
}
public async Task<bool> CreateUser(Models.User user)
{
var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform);
var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson);
var response = await client.PostAsync(string.Empty, content);
return response.IsSuccessStatusCode;
}
public void ReadMoveHistory()
{
//TODO: Separate move history into a separate request.
//var moves = group
// .Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session.
// // TODO: Deserialize just the Move property.
// .Select(row => row.doc.ToObject<BoardStateDocument>())
// .Select(boardState =>
// {
// var move = boardState!.Move!;
// return move.PieceFromHand.HasValue
// ? new Models.Move(move.PieceFromHand.Value, move.To)
// : new Models.Move(move.From!, move.To, move.IsPromotion);
// })
// .ToList();
}
}
}

View File

@@ -5,7 +5,6 @@ using Gameboard.ShogiUI.Sockets.Managers.ClientActionHandlers;
using Gameboard.ShogiUI.Sockets.Repositories;
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
using Gameboard.ShogiUI.Sockets.Services.Utility;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
@@ -17,7 +16,7 @@ using System.Threading.Tasks;
namespace Gameboard.ShogiUI.Sockets.Services
{
public interface ISocketService
public interface ISocketService
{
Task HandleSocketRequest(HttpContext context);
}
@@ -74,7 +73,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
}
var socket = await context.WebSockets.AcceptWebSocketAsync();
communicationManager.SubscribeToBroadcast(socket, userName);
communicationManager.Subscribe(socket, userName);
while (socket.State == WebSocketState.Open)
{
try
@@ -82,7 +81,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
var message = await socket.ReceiveTextAsync();
if (string.IsNullOrWhiteSpace(message)) continue;
logger.LogInformation("Request \n{0}\n", message);
var request = JsonConvert.DeserializeObject<Request>(message);
var request = JsonConvert.DeserializeObject<IRequest>(message);
if (request == null || !Enum.IsDefined(typeof(ClientAction), request.Action))
{
await socket.SendTextAsync("Error: Action not recognized.");
@@ -114,7 +113,7 @@ namespace Gameboard.ShogiUI.Sockets.Services
logger.LogInformation("Probably tried writing to a closed socket.");
logger.LogError(ex.Message);
}
communicationManager.UnsubscribeFromBroadcastAndGames(userName);
communicationManager.Unsubscribe(userName);
}
}

View File

@@ -1,15 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
namespace Gameboard.ShogiUI.Sockets.Services.Utility
{
public class JsonRequest
{
public IRequest Request { get; private set; }
public string Json { get; private set; }
public JsonRequest(IRequest request, string json)
{
Request = request;
Json = json;
}
}
}

View File

@@ -1,10 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
using Gameboard.ShogiUI.Sockets.ServiceModels.Types;
namespace Gameboard.ShogiUI.Sockets.Services.Utility
{
public class Request : IRequest
{
public ClientAction Action { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
using Gameboard.ShogiUI.Sockets.ServiceModels.Socket;
namespace Gameboard.ShogiUI.Sockets.Services.Utility
{
public class Response : IResponse
{
public string Action { get; set; }
}
}

View File

@@ -1,48 +0,0 @@
using Gameboard.ShogiUI.Sockets.Models;
using PathFinding;
using System;
using System.Collections.Generic;
using System.Numerics;
namespace Gameboard.ShogiUI.Sockets.Utilities
{
public class CoordsToNotationCollection : Dictionary<string, Piece?>, IPlanarCollection<Piece>
{
public delegate void ForEachDelegate(Piece element, Vector2 position);
public CoordsToNotationCollection() : base(81, StringComparer.OrdinalIgnoreCase)
{
}
public CoordsToNotationCollection(Dictionary<string, Piece?> board) : base(board, StringComparer.OrdinalIgnoreCase)
{
}
public Piece? this[Vector2 vector]
{
get => this[NotationHelper.ToBoardNotation(vector)];
set => this[NotationHelper.ToBoardNotation(vector)] = value;
}
public Piece? this[int x, int y]
{
get => this[NotationHelper.ToBoardNotation(x, y)];
set => this[NotationHelper.ToBoardNotation(x, y)] = value;
}
public void ForEachNotNull(ForEachDelegate callback)
{
for (var x = 0; x < 9; x++)
{
for (var y = 0; y < 9; y++)
{
var position = new Vector2(x, y);
var elem = this[position];
if (elem != null)
callback(elem, position);
}
}
}
}
}

View File

@@ -1,36 +0,0 @@
using System;
using System.Numerics;
using System.Text.RegularExpressions;
namespace Gameboard.ShogiUI.Sockets.Utilities
{
public static class NotationHelper
{
private static readonly string BoardNotationRegex = @"(?<file>[a-iA-I])(?<rank>[1-9])";
private static readonly char A = 'A';
public static string ToBoardNotation(Vector2 vector)
{
return ToBoardNotation((int)vector.X, (int)vector.Y);
}
public static string ToBoardNotation(int x, int y)
{
var file = (char)(x + A);
var rank = y + 1;
return $"{file}{rank}";
}
public static Vector2 FromBoardNotation(string notation)
{
notation = notation.ToUpper();
if (Regex.IsMatch(notation, BoardNotationRegex))
{
var match = Regex.Match(notation, BoardNotationRegex);
char file = match.Groups["file"].Value[0];
int rank = int.Parse(match.Groups["rank"].Value);
return new Vector2(file - A, rank - 1);
}
throw new ArgumentException($"Board notation not recognized. Notation given: {notation}");
}
}
}