Playing Checkers
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
6
down vote
favorite
Continuing with my web-based checkers game, this question is about the actual playing system. I'll ask for a review on my UI system in the next post.
First, my BoardController.cs. Note that I have references to a ComponentGenerator
class here and in other files. I consider this part of the UI, and will post it later. In fact, that is part of the reason I want a dedicated UI post--so it is up front and center.
public class BoardController : Controller
private readonly IMediator _mediator;
private readonly Database.Context _context;
private readonly IHubContext<GameHub> _signalRHub;
private readonly ComputerPlayer _computerPlayer;
public BoardController(Database.Context context,
IHubContext<GameHub> signalRHub,
ComputerPlayer computerPlayer,
IMediator mediator)
_context = context;
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
_mediator = mediator;
private Theme GetThemeOrDefault()
if (Request.Cookies.Keys.All(a => a != "theme"))
return Theme.Steel;
return Enum.Parse(typeof(Theme), Request.Cookies["theme"]) as Theme? ?? Theme.Steel;
private Guid? GetPlayerID()
if (Request.Cookies.TryGetValue("playerID", out var id))
return Guid.Parse(id);
return null;
private string GetClientConnection(Guid id)
return _context.Players.Find(id).ConnectionID;
public ActionResult MovePiece(Guid id, Coord start, Coord end)
(game.WhitePlayerID != playerID && game.CurrentPlayer == (int)Player.White))
Response.StatusCode = 403;
return Content("");
var controller = game.ToGameController();
if (!controller.IsValidMove(start, end))
Response.StatusCode = 403;
return Content("");
var move = controller.Move(start, end);
move.ID = game.ID;
var turn = move.MoveHistory.Last().ToPdnTurn();
if (game.Turns.Any(t => t.MoveNumber == turn.MoveNumber))
var recordedTurn = game.Turns.Single(s => s.MoveNumber == turn.MoveNumber);
Database.PdnMove newMove;
switch (controller.CurrentPlayer)
case Player.White:
newMove = move.MoveHistory.Last().WhiteMove.ToPdnMove();
break;
case Player.Black:
newMove = move.MoveHistory.Last().BlackMove.ToPdnMove();
break;
default:
throw new ArgumentException();
var existingMove = recordedTurn.Moves.FirstOrDefault(a => a.Player == (int)controller.CurrentPlayer);
if (existingMove != null)
recordedTurn.Moves.Remove(existingMove);
recordedTurn.Moves.Add(newMove);
game.Fen = newMove.ResultingFen;
else
game.Turns.Add(move.MoveHistory.Last().ToPdnTurn());
game.Fen = turn.Moves.Single().ResultingFen;
game.CurrentPosition = move.GetCurrentPosition();
game.CurrentPlayer = (int)move.CurrentPlayer;
game.GameStatus = (int)move.GetGameStatus();
game.RowVersion = DateTime.Now;
_context.SaveChanges();
var viewModel = game.ToGameViewModel();
_mediator.Publish(new OnMoveNotification(viewModel)).Wait();
return Content("");
public ActionResult Undo(Guid id)
game.BlackPlayerID == ComputerPlayer.ComputerPlayerID
public ActionResult Resign(Guid id)
game.GameStatus != (int) Status.InProgress
public ActionResult DisplayGame(Guid moveID, Player orientation)
var game = _context.Games
.Include("Turns")
.Include("Turns.Moves")
.FirstOrDefault(f => f.Turns.Any(a => a.Moves.Any(m => m.ID == moveID)));
if (game == null)
Response.StatusCode = 403;
return Content("");
var move = game.Turns.SelectMany(t => t.Moves).First(f => f.ID == moveID);
var viewData = new Dictionary<string, object>
["playerID"] = GetPlayerID(),
["orientation"] = orientation
;
var controller = GameController.FromPosition(Variant.AmericanCheckers, move.ResultingFen);
var viewModel = game.ToGameViewModel();
viewModel.Board.GameBoard = controller.Board.GameBoard;
viewModel.DisplayingLastMove = false;
var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
return Content(board);
public ActionResult Join(Guid id)
public ActionResult Orientate(Guid id, Guid? moveID, Player orientation)
var game = _context.Games
.Include("Turns")
.Include("Turns.Moves")
.FirstOrDefault(f => f.ID == id);
if (game == null)
Response.StatusCode = 403;
return Content("");
var move = game.Turns.SelectMany(t => t.Moves).FirstOrDefault(f => f.ID == moveID) ??
game.Turns.OrderBy(o => o.MoveNumber).LastOrDefault()?.Moves.OrderBy(a => a.CreatedOn).LastOrDefault();
Dictionary<string, object>
viewData = new Dictionary<string, object>
["playerID"] = GetPlayerID(),
["orientation"] = orientation
;
var viewModel = game.ToGameViewModel();
if (moveID != null && moveID.Value != game.Turns.Last().Moves.OrderBy(o => o.CreatedOn).Last().ID)
var fen = move.ResultingFen;
var controller = GameController.FromPosition((Variant)game.Variant, fen);
viewModel.Board.GameBoard = controller.Board.GameBoard;
viewModel.DisplayingLastMove = false;
var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
return Content(board);
I set up the concept of an Action
to make it easy to perform multiple discrete responses when something happens. Here they are, in sequence of being performed:
The GameCreated actions (actually used in the HomeController
, but since I'm getting the rest of them reviewed anyway...):
public class OnGameCreatedNotification : INotification
public GameViewModel ViewModel get;
public OnGameCreatedNotification(GameViewModel viewModel, Guid currentPlayerID)
ViewModel = viewModel;
public class DoComputerMoveAction : INotificationHandler<OnGameCreatedNotification>
private readonly ComputerPlayer _computerPlayer;
private readonly IHubContext<GameHub> _signalRHub;
public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
public async Task Handle(OnGameCreatedNotification request, CancellationToken cancellationToken)
await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
public class AddGameToLobbyAction : INotificationHandler<OnGameCreatedNotification>
private readonly IHubContext<GameHub> _signalRHub;
public AddGameToLobbyAction(IHubContext<GameHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnGameCreatedNotification notification, CancellationToken cancellationToken)
var lobbyEntry =
$@"<tr>
<td><a href=""/Home/Game/notification.ViewModel.ID"">Resources.Resources.ResourceManager.GetString(notification.ViewModel.Variant.ToString())</a></td>
<td>Resources.Resources.ResourceManager.GetString(notification.ViewModel.GameStatus.ToString())</td>
</tr>";
_signalRHub.Clients.Group("home").InvokeAsync("GameCreated", lobbyEntry);
return Task.CompletedTask;
The GameJoined actions:
public class OnGameJoinedNotification : INotification
public GameViewModel ViewModel get;
public Guid CurrentPlayerID get;
public OnGameJoinedNotification(GameViewModel viewModel, Guid currentPlayerID)
ViewModel = viewModel;
CurrentPlayerID = currentPlayerID;
public class RemoveGameFromLobbyAction : INotificationHandler<OnGameJoinedNotification>
private readonly IHubContext<GameHub> _signalRHub;
public RemoveGameFromLobbyAction(IHubContext<GameHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
_signalRHub.Clients.Group("home").InvokeAsync("GameJoined", notification.ViewModel.ID);
return Task.CompletedTask;
public class UpdateControlsAction : INotificationHandler<OnGameJoinedNotification>
private readonly IMediator _mediator;
private readonly IHubContext<GameHub> _signalRHub;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
_signalRHub.Clients.All.InvokeAsync("AddClass", "join", "hide");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", notification.ViewModel.BlackPlayerID == notification.CurrentPlayerID ? "black-player-text" : "white-player-text", "bold");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", "new-game", "hide");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("RemoveClass", "resign", "hide");
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
client.InvokeAsync("SetAttribute", "resign", "title", "Resign");
client.InvokeAsync("SetHtml", "#resign .sr-only", "Resign");
return Task.CompletedTask;
The Move actions:
public class OnMoveNotification : INotification
public GameViewModel ViewModel get;
public OnMoveNotification(GameViewModel viewModel)
ViewModel = viewModel;
public class DoComputerMoveAction : INotificationHandler<OnMoveNotification>
private readonly ComputerPlayer _computerPlayer;
private readonly IHubContext<GameHub> _signalRHub;
public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
public async Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
public class UpdateMoveHistoryAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateMoveHistoryAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateMoves", request.ViewModel.ID, lastMoveDate, ComponentGenerator.GetMoveControl(request.ViewModel.Turns));
return Task.CompletedTask;
public class UpdateOpponentStateAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateOpponentStateAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateOpponentState", request.ViewModel.ID, lastMoveDate, request.ViewModel.CurrentPlayer.ToString(), request.ViewModel.GameStatus.ToString());
return Task.CompletedTask;
public class UpdateControlsAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
request.ViewModel.GameStatus != Status.InProgress)
client.InvokeAsync("SetAttribute", "undo", "disabled", "");
else
client.InvokeAsync("RemoveAttribute", "undo", "disabled");
client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "RemoveClass" : "AddClass", "new-game", "hide");
client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "AddClass" : "RemoveClass", "resign", "hide");
return Task.CompletedTask;
This next action isn't used by the game, but rather publishes the data so another system can hook into mine and receive updates about games.
public class SignalGameStateAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<APIHub> _signalRHub;
public SignalGameStateAction(IHubContext<APIHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var data = JsonConvert.SerializeObject(request.ViewModel);
_signalRHub.Clients.All.InvokeAsync("GameChanged", data);
return Task.CompletedTask;
And finally, I really hate this last one, but I don't see any other way in SignalR Core to publish a message to a group with exceptions by connection ID. They have this functionality in SignalR non-Core, so hopefully soon...
public class UpdateBoardAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly Database.Context _context;
private readonly IMediator _mediator;
public UpdateBoardAction(IHubContext<GameHub> signalRHub, Database.Context context, IMediator mediator)
_signalRHub = signalRHub;
_context = context;
_mediator = mediator;
private string GetClientConnection(Guid id)
return _context.Players.Find(id).ConnectionID;
Dictionary<string, object> GetViewData(Guid localPlayerID, Player orientation)
return new Dictionary<string, object>
["playerID"] = localPlayerID,
["orientation"] = orientation
;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
var blackConnection = GetClientConnection(request.ViewModel.BlackPlayerID);
var whiteConnection = GetClientConnection(request.ViewModel.WhitePlayerID);
if (request.ViewModel.BlackPlayerID != ComputerPlayer.ComputerPlayerID)
_signalRHub.Clients.Client(blackConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.White)));
if (request.ViewModel.WhitePlayerID != ComputerPlayer.ComputerPlayerID)
_signalRHub.Clients.Client(whiteConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.White)));
_signalRHub.Groups.RemoveAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Groups.RemoveAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.White)));
_signalRHub.Groups.AddAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Groups.AddAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();
return Task.CompletedTask;
And the GameCompleted actions:
public class OnGameCompletedNotification : INotification
public GameViewModel ViewModel get;
public OnGameCompletedNotification(GameViewModel viewModel)
ViewModel = viewModel;
public class UpdateControlsAction : INotificationHandler<OnGameCompletedNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnGameCompletedNotification request, CancellationToken cancellationToken)
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
client.InvokeAsync("SetAttribute", "undo", "disabled", "");
client.InvokeAsync("RemoveClass", "new-game", "hide");
client.InvokeAsync("AddClass", "resign", "hide");
return Task.CompletedTask;
My main concerns here are:
A) Am I following best web practices, including my MVC structure?
B) Is my choice of MediatR for the action system good?
C) See anything else I could change for the better?
c# game asp.net-core checkers-draughts signalr
add a comment |Â
up vote
6
down vote
favorite
Continuing with my web-based checkers game, this question is about the actual playing system. I'll ask for a review on my UI system in the next post.
First, my BoardController.cs. Note that I have references to a ComponentGenerator
class here and in other files. I consider this part of the UI, and will post it later. In fact, that is part of the reason I want a dedicated UI post--so it is up front and center.
public class BoardController : Controller
private readonly IMediator _mediator;
private readonly Database.Context _context;
private readonly IHubContext<GameHub> _signalRHub;
private readonly ComputerPlayer _computerPlayer;
public BoardController(Database.Context context,
IHubContext<GameHub> signalRHub,
ComputerPlayer computerPlayer,
IMediator mediator)
_context = context;
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
_mediator = mediator;
private Theme GetThemeOrDefault()
if (Request.Cookies.Keys.All(a => a != "theme"))
return Theme.Steel;
return Enum.Parse(typeof(Theme), Request.Cookies["theme"]) as Theme? ?? Theme.Steel;
private Guid? GetPlayerID()
if (Request.Cookies.TryGetValue("playerID", out var id))
return Guid.Parse(id);
return null;
private string GetClientConnection(Guid id)
return _context.Players.Find(id).ConnectionID;
public ActionResult MovePiece(Guid id, Coord start, Coord end)
(game.WhitePlayerID != playerID && game.CurrentPlayer == (int)Player.White))
Response.StatusCode = 403;
return Content("");
var controller = game.ToGameController();
if (!controller.IsValidMove(start, end))
Response.StatusCode = 403;
return Content("");
var move = controller.Move(start, end);
move.ID = game.ID;
var turn = move.MoveHistory.Last().ToPdnTurn();
if (game.Turns.Any(t => t.MoveNumber == turn.MoveNumber))
var recordedTurn = game.Turns.Single(s => s.MoveNumber == turn.MoveNumber);
Database.PdnMove newMove;
switch (controller.CurrentPlayer)
case Player.White:
newMove = move.MoveHistory.Last().WhiteMove.ToPdnMove();
break;
case Player.Black:
newMove = move.MoveHistory.Last().BlackMove.ToPdnMove();
break;
default:
throw new ArgumentException();
var existingMove = recordedTurn.Moves.FirstOrDefault(a => a.Player == (int)controller.CurrentPlayer);
if (existingMove != null)
recordedTurn.Moves.Remove(existingMove);
recordedTurn.Moves.Add(newMove);
game.Fen = newMove.ResultingFen;
else
game.Turns.Add(move.MoveHistory.Last().ToPdnTurn());
game.Fen = turn.Moves.Single().ResultingFen;
game.CurrentPosition = move.GetCurrentPosition();
game.CurrentPlayer = (int)move.CurrentPlayer;
game.GameStatus = (int)move.GetGameStatus();
game.RowVersion = DateTime.Now;
_context.SaveChanges();
var viewModel = game.ToGameViewModel();
_mediator.Publish(new OnMoveNotification(viewModel)).Wait();
return Content("");
public ActionResult Undo(Guid id)
game.BlackPlayerID == ComputerPlayer.ComputerPlayerID
public ActionResult Resign(Guid id)
game.GameStatus != (int) Status.InProgress
public ActionResult DisplayGame(Guid moveID, Player orientation)
var game = _context.Games
.Include("Turns")
.Include("Turns.Moves")
.FirstOrDefault(f => f.Turns.Any(a => a.Moves.Any(m => m.ID == moveID)));
if (game == null)
Response.StatusCode = 403;
return Content("");
var move = game.Turns.SelectMany(t => t.Moves).First(f => f.ID == moveID);
var viewData = new Dictionary<string, object>
["playerID"] = GetPlayerID(),
["orientation"] = orientation
;
var controller = GameController.FromPosition(Variant.AmericanCheckers, move.ResultingFen);
var viewModel = game.ToGameViewModel();
viewModel.Board.GameBoard = controller.Board.GameBoard;
viewModel.DisplayingLastMove = false;
var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
return Content(board);
public ActionResult Join(Guid id)
public ActionResult Orientate(Guid id, Guid? moveID, Player orientation)
var game = _context.Games
.Include("Turns")
.Include("Turns.Moves")
.FirstOrDefault(f => f.ID == id);
if (game == null)
Response.StatusCode = 403;
return Content("");
var move = game.Turns.SelectMany(t => t.Moves).FirstOrDefault(f => f.ID == moveID) ??
game.Turns.OrderBy(o => o.MoveNumber).LastOrDefault()?.Moves.OrderBy(a => a.CreatedOn).LastOrDefault();
Dictionary<string, object>
viewData = new Dictionary<string, object>
["playerID"] = GetPlayerID(),
["orientation"] = orientation
;
var viewModel = game.ToGameViewModel();
if (moveID != null && moveID.Value != game.Turns.Last().Moves.OrderBy(o => o.CreatedOn).Last().ID)
var fen = move.ResultingFen;
var controller = GameController.FromPosition((Variant)game.Variant, fen);
viewModel.Board.GameBoard = controller.Board.GameBoard;
viewModel.DisplayingLastMove = false;
var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
return Content(board);
I set up the concept of an Action
to make it easy to perform multiple discrete responses when something happens. Here they are, in sequence of being performed:
The GameCreated actions (actually used in the HomeController
, but since I'm getting the rest of them reviewed anyway...):
public class OnGameCreatedNotification : INotification
public GameViewModel ViewModel get;
public OnGameCreatedNotification(GameViewModel viewModel, Guid currentPlayerID)
ViewModel = viewModel;
public class DoComputerMoveAction : INotificationHandler<OnGameCreatedNotification>
private readonly ComputerPlayer _computerPlayer;
private readonly IHubContext<GameHub> _signalRHub;
public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
public async Task Handle(OnGameCreatedNotification request, CancellationToken cancellationToken)
await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
public class AddGameToLobbyAction : INotificationHandler<OnGameCreatedNotification>
private readonly IHubContext<GameHub> _signalRHub;
public AddGameToLobbyAction(IHubContext<GameHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnGameCreatedNotification notification, CancellationToken cancellationToken)
var lobbyEntry =
$@"<tr>
<td><a href=""/Home/Game/notification.ViewModel.ID"">Resources.Resources.ResourceManager.GetString(notification.ViewModel.Variant.ToString())</a></td>
<td>Resources.Resources.ResourceManager.GetString(notification.ViewModel.GameStatus.ToString())</td>
</tr>";
_signalRHub.Clients.Group("home").InvokeAsync("GameCreated", lobbyEntry);
return Task.CompletedTask;
The GameJoined actions:
public class OnGameJoinedNotification : INotification
public GameViewModel ViewModel get;
public Guid CurrentPlayerID get;
public OnGameJoinedNotification(GameViewModel viewModel, Guid currentPlayerID)
ViewModel = viewModel;
CurrentPlayerID = currentPlayerID;
public class RemoveGameFromLobbyAction : INotificationHandler<OnGameJoinedNotification>
private readonly IHubContext<GameHub> _signalRHub;
public RemoveGameFromLobbyAction(IHubContext<GameHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
_signalRHub.Clients.Group("home").InvokeAsync("GameJoined", notification.ViewModel.ID);
return Task.CompletedTask;
public class UpdateControlsAction : INotificationHandler<OnGameJoinedNotification>
private readonly IMediator _mediator;
private readonly IHubContext<GameHub> _signalRHub;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
_signalRHub.Clients.All.InvokeAsync("AddClass", "join", "hide");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", notification.ViewModel.BlackPlayerID == notification.CurrentPlayerID ? "black-player-text" : "white-player-text", "bold");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", "new-game", "hide");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("RemoveClass", "resign", "hide");
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
client.InvokeAsync("SetAttribute", "resign", "title", "Resign");
client.InvokeAsync("SetHtml", "#resign .sr-only", "Resign");
return Task.CompletedTask;
The Move actions:
public class OnMoveNotification : INotification
public GameViewModel ViewModel get;
public OnMoveNotification(GameViewModel viewModel)
ViewModel = viewModel;
public class DoComputerMoveAction : INotificationHandler<OnMoveNotification>
private readonly ComputerPlayer _computerPlayer;
private readonly IHubContext<GameHub> _signalRHub;
public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
public async Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
public class UpdateMoveHistoryAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateMoveHistoryAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateMoves", request.ViewModel.ID, lastMoveDate, ComponentGenerator.GetMoveControl(request.ViewModel.Turns));
return Task.CompletedTask;
public class UpdateOpponentStateAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateOpponentStateAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateOpponentState", request.ViewModel.ID, lastMoveDate, request.ViewModel.CurrentPlayer.ToString(), request.ViewModel.GameStatus.ToString());
return Task.CompletedTask;
public class UpdateControlsAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
request.ViewModel.GameStatus != Status.InProgress)
client.InvokeAsync("SetAttribute", "undo", "disabled", "");
else
client.InvokeAsync("RemoveAttribute", "undo", "disabled");
client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "RemoveClass" : "AddClass", "new-game", "hide");
client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "AddClass" : "RemoveClass", "resign", "hide");
return Task.CompletedTask;
This next action isn't used by the game, but rather publishes the data so another system can hook into mine and receive updates about games.
public class SignalGameStateAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<APIHub> _signalRHub;
public SignalGameStateAction(IHubContext<APIHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var data = JsonConvert.SerializeObject(request.ViewModel);
_signalRHub.Clients.All.InvokeAsync("GameChanged", data);
return Task.CompletedTask;
And finally, I really hate this last one, but I don't see any other way in SignalR Core to publish a message to a group with exceptions by connection ID. They have this functionality in SignalR non-Core, so hopefully soon...
public class UpdateBoardAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly Database.Context _context;
private readonly IMediator _mediator;
public UpdateBoardAction(IHubContext<GameHub> signalRHub, Database.Context context, IMediator mediator)
_signalRHub = signalRHub;
_context = context;
_mediator = mediator;
private string GetClientConnection(Guid id)
return _context.Players.Find(id).ConnectionID;
Dictionary<string, object> GetViewData(Guid localPlayerID, Player orientation)
return new Dictionary<string, object>
["playerID"] = localPlayerID,
["orientation"] = orientation
;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
var blackConnection = GetClientConnection(request.ViewModel.BlackPlayerID);
var whiteConnection = GetClientConnection(request.ViewModel.WhitePlayerID);
if (request.ViewModel.BlackPlayerID != ComputerPlayer.ComputerPlayerID)
_signalRHub.Clients.Client(blackConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.White)));
if (request.ViewModel.WhitePlayerID != ComputerPlayer.ComputerPlayerID)
_signalRHub.Clients.Client(whiteConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.White)));
_signalRHub.Groups.RemoveAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Groups.RemoveAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.White)));
_signalRHub.Groups.AddAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Groups.AddAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();
return Task.CompletedTask;
And the GameCompleted actions:
public class OnGameCompletedNotification : INotification
public GameViewModel ViewModel get;
public OnGameCompletedNotification(GameViewModel viewModel)
ViewModel = viewModel;
public class UpdateControlsAction : INotificationHandler<OnGameCompletedNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnGameCompletedNotification request, CancellationToken cancellationToken)
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
client.InvokeAsync("SetAttribute", "undo", "disabled", "");
client.InvokeAsync("RemoveClass", "new-game", "hide");
client.InvokeAsync("AddClass", "resign", "hide");
return Task.CompletedTask;
My main concerns here are:
A) Am I following best web practices, including my MVC structure?
B) Is my choice of MediatR for the action system good?
C) See anything else I could change for the better?
c# game asp.net-core checkers-draughts signalr
add a comment |Â
up vote
6
down vote
favorite
up vote
6
down vote
favorite
Continuing with my web-based checkers game, this question is about the actual playing system. I'll ask for a review on my UI system in the next post.
First, my BoardController.cs. Note that I have references to a ComponentGenerator
class here and in other files. I consider this part of the UI, and will post it later. In fact, that is part of the reason I want a dedicated UI post--so it is up front and center.
public class BoardController : Controller
private readonly IMediator _mediator;
private readonly Database.Context _context;
private readonly IHubContext<GameHub> _signalRHub;
private readonly ComputerPlayer _computerPlayer;
public BoardController(Database.Context context,
IHubContext<GameHub> signalRHub,
ComputerPlayer computerPlayer,
IMediator mediator)
_context = context;
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
_mediator = mediator;
private Theme GetThemeOrDefault()
if (Request.Cookies.Keys.All(a => a != "theme"))
return Theme.Steel;
return Enum.Parse(typeof(Theme), Request.Cookies["theme"]) as Theme? ?? Theme.Steel;
private Guid? GetPlayerID()
if (Request.Cookies.TryGetValue("playerID", out var id))
return Guid.Parse(id);
return null;
private string GetClientConnection(Guid id)
return _context.Players.Find(id).ConnectionID;
public ActionResult MovePiece(Guid id, Coord start, Coord end)
(game.WhitePlayerID != playerID && game.CurrentPlayer == (int)Player.White))
Response.StatusCode = 403;
return Content("");
var controller = game.ToGameController();
if (!controller.IsValidMove(start, end))
Response.StatusCode = 403;
return Content("");
var move = controller.Move(start, end);
move.ID = game.ID;
var turn = move.MoveHistory.Last().ToPdnTurn();
if (game.Turns.Any(t => t.MoveNumber == turn.MoveNumber))
var recordedTurn = game.Turns.Single(s => s.MoveNumber == turn.MoveNumber);
Database.PdnMove newMove;
switch (controller.CurrentPlayer)
case Player.White:
newMove = move.MoveHistory.Last().WhiteMove.ToPdnMove();
break;
case Player.Black:
newMove = move.MoveHistory.Last().BlackMove.ToPdnMove();
break;
default:
throw new ArgumentException();
var existingMove = recordedTurn.Moves.FirstOrDefault(a => a.Player == (int)controller.CurrentPlayer);
if (existingMove != null)
recordedTurn.Moves.Remove(existingMove);
recordedTurn.Moves.Add(newMove);
game.Fen = newMove.ResultingFen;
else
game.Turns.Add(move.MoveHistory.Last().ToPdnTurn());
game.Fen = turn.Moves.Single().ResultingFen;
game.CurrentPosition = move.GetCurrentPosition();
game.CurrentPlayer = (int)move.CurrentPlayer;
game.GameStatus = (int)move.GetGameStatus();
game.RowVersion = DateTime.Now;
_context.SaveChanges();
var viewModel = game.ToGameViewModel();
_mediator.Publish(new OnMoveNotification(viewModel)).Wait();
return Content("");
public ActionResult Undo(Guid id)
game.BlackPlayerID == ComputerPlayer.ComputerPlayerID
public ActionResult Resign(Guid id)
game.GameStatus != (int) Status.InProgress
public ActionResult DisplayGame(Guid moveID, Player orientation)
var game = _context.Games
.Include("Turns")
.Include("Turns.Moves")
.FirstOrDefault(f => f.Turns.Any(a => a.Moves.Any(m => m.ID == moveID)));
if (game == null)
Response.StatusCode = 403;
return Content("");
var move = game.Turns.SelectMany(t => t.Moves).First(f => f.ID == moveID);
var viewData = new Dictionary<string, object>
["playerID"] = GetPlayerID(),
["orientation"] = orientation
;
var controller = GameController.FromPosition(Variant.AmericanCheckers, move.ResultingFen);
var viewModel = game.ToGameViewModel();
viewModel.Board.GameBoard = controller.Board.GameBoard;
viewModel.DisplayingLastMove = false;
var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
return Content(board);
public ActionResult Join(Guid id)
public ActionResult Orientate(Guid id, Guid? moveID, Player orientation)
var game = _context.Games
.Include("Turns")
.Include("Turns.Moves")
.FirstOrDefault(f => f.ID == id);
if (game == null)
Response.StatusCode = 403;
return Content("");
var move = game.Turns.SelectMany(t => t.Moves).FirstOrDefault(f => f.ID == moveID) ??
game.Turns.OrderBy(o => o.MoveNumber).LastOrDefault()?.Moves.OrderBy(a => a.CreatedOn).LastOrDefault();
Dictionary<string, object>
viewData = new Dictionary<string, object>
["playerID"] = GetPlayerID(),
["orientation"] = orientation
;
var viewModel = game.ToGameViewModel();
if (moveID != null && moveID.Value != game.Turns.Last().Moves.OrderBy(o => o.CreatedOn).Last().ID)
var fen = move.ResultingFen;
var controller = GameController.FromPosition((Variant)game.Variant, fen);
viewModel.Board.GameBoard = controller.Board.GameBoard;
viewModel.DisplayingLastMove = false;
var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
return Content(board);
I set up the concept of an Action
to make it easy to perform multiple discrete responses when something happens. Here they are, in sequence of being performed:
The GameCreated actions (actually used in the HomeController
, but since I'm getting the rest of them reviewed anyway...):
public class OnGameCreatedNotification : INotification
public GameViewModel ViewModel get;
public OnGameCreatedNotification(GameViewModel viewModel, Guid currentPlayerID)
ViewModel = viewModel;
public class DoComputerMoveAction : INotificationHandler<OnGameCreatedNotification>
private readonly ComputerPlayer _computerPlayer;
private readonly IHubContext<GameHub> _signalRHub;
public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
public async Task Handle(OnGameCreatedNotification request, CancellationToken cancellationToken)
await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
public class AddGameToLobbyAction : INotificationHandler<OnGameCreatedNotification>
private readonly IHubContext<GameHub> _signalRHub;
public AddGameToLobbyAction(IHubContext<GameHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnGameCreatedNotification notification, CancellationToken cancellationToken)
var lobbyEntry =
$@"<tr>
<td><a href=""/Home/Game/notification.ViewModel.ID"">Resources.Resources.ResourceManager.GetString(notification.ViewModel.Variant.ToString())</a></td>
<td>Resources.Resources.ResourceManager.GetString(notification.ViewModel.GameStatus.ToString())</td>
</tr>";
_signalRHub.Clients.Group("home").InvokeAsync("GameCreated", lobbyEntry);
return Task.CompletedTask;
The GameJoined actions:
public class OnGameJoinedNotification : INotification
public GameViewModel ViewModel get;
public Guid CurrentPlayerID get;
public OnGameJoinedNotification(GameViewModel viewModel, Guid currentPlayerID)
ViewModel = viewModel;
CurrentPlayerID = currentPlayerID;
public class RemoveGameFromLobbyAction : INotificationHandler<OnGameJoinedNotification>
private readonly IHubContext<GameHub> _signalRHub;
public RemoveGameFromLobbyAction(IHubContext<GameHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
_signalRHub.Clients.Group("home").InvokeAsync("GameJoined", notification.ViewModel.ID);
return Task.CompletedTask;
public class UpdateControlsAction : INotificationHandler<OnGameJoinedNotification>
private readonly IMediator _mediator;
private readonly IHubContext<GameHub> _signalRHub;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
_signalRHub.Clients.All.InvokeAsync("AddClass", "join", "hide");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", notification.ViewModel.BlackPlayerID == notification.CurrentPlayerID ? "black-player-text" : "white-player-text", "bold");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", "new-game", "hide");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("RemoveClass", "resign", "hide");
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
client.InvokeAsync("SetAttribute", "resign", "title", "Resign");
client.InvokeAsync("SetHtml", "#resign .sr-only", "Resign");
return Task.CompletedTask;
The Move actions:
public class OnMoveNotification : INotification
public GameViewModel ViewModel get;
public OnMoveNotification(GameViewModel viewModel)
ViewModel = viewModel;
public class DoComputerMoveAction : INotificationHandler<OnMoveNotification>
private readonly ComputerPlayer _computerPlayer;
private readonly IHubContext<GameHub> _signalRHub;
public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
public async Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
public class UpdateMoveHistoryAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateMoveHistoryAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateMoves", request.ViewModel.ID, lastMoveDate, ComponentGenerator.GetMoveControl(request.ViewModel.Turns));
return Task.CompletedTask;
public class UpdateOpponentStateAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateOpponentStateAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateOpponentState", request.ViewModel.ID, lastMoveDate, request.ViewModel.CurrentPlayer.ToString(), request.ViewModel.GameStatus.ToString());
return Task.CompletedTask;
public class UpdateControlsAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
request.ViewModel.GameStatus != Status.InProgress)
client.InvokeAsync("SetAttribute", "undo", "disabled", "");
else
client.InvokeAsync("RemoveAttribute", "undo", "disabled");
client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "RemoveClass" : "AddClass", "new-game", "hide");
client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "AddClass" : "RemoveClass", "resign", "hide");
return Task.CompletedTask;
This next action isn't used by the game, but rather publishes the data so another system can hook into mine and receive updates about games.
public class SignalGameStateAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<APIHub> _signalRHub;
public SignalGameStateAction(IHubContext<APIHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var data = JsonConvert.SerializeObject(request.ViewModel);
_signalRHub.Clients.All.InvokeAsync("GameChanged", data);
return Task.CompletedTask;
And finally, I really hate this last one, but I don't see any other way in SignalR Core to publish a message to a group with exceptions by connection ID. They have this functionality in SignalR non-Core, so hopefully soon...
public class UpdateBoardAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly Database.Context _context;
private readonly IMediator _mediator;
public UpdateBoardAction(IHubContext<GameHub> signalRHub, Database.Context context, IMediator mediator)
_signalRHub = signalRHub;
_context = context;
_mediator = mediator;
private string GetClientConnection(Guid id)
return _context.Players.Find(id).ConnectionID;
Dictionary<string, object> GetViewData(Guid localPlayerID, Player orientation)
return new Dictionary<string, object>
["playerID"] = localPlayerID,
["orientation"] = orientation
;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
var blackConnection = GetClientConnection(request.ViewModel.BlackPlayerID);
var whiteConnection = GetClientConnection(request.ViewModel.WhitePlayerID);
if (request.ViewModel.BlackPlayerID != ComputerPlayer.ComputerPlayerID)
_signalRHub.Clients.Client(blackConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.White)));
if (request.ViewModel.WhitePlayerID != ComputerPlayer.ComputerPlayerID)
_signalRHub.Clients.Client(whiteConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.White)));
_signalRHub.Groups.RemoveAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Groups.RemoveAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.White)));
_signalRHub.Groups.AddAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Groups.AddAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();
return Task.CompletedTask;
And the GameCompleted actions:
public class OnGameCompletedNotification : INotification
public GameViewModel ViewModel get;
public OnGameCompletedNotification(GameViewModel viewModel)
ViewModel = viewModel;
public class UpdateControlsAction : INotificationHandler<OnGameCompletedNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnGameCompletedNotification request, CancellationToken cancellationToken)
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
client.InvokeAsync("SetAttribute", "undo", "disabled", "");
client.InvokeAsync("RemoveClass", "new-game", "hide");
client.InvokeAsync("AddClass", "resign", "hide");
return Task.CompletedTask;
My main concerns here are:
A) Am I following best web practices, including my MVC structure?
B) Is my choice of MediatR for the action system good?
C) See anything else I could change for the better?
c# game asp.net-core checkers-draughts signalr
Continuing with my web-based checkers game, this question is about the actual playing system. I'll ask for a review on my UI system in the next post.
First, my BoardController.cs. Note that I have references to a ComponentGenerator
class here and in other files. I consider this part of the UI, and will post it later. In fact, that is part of the reason I want a dedicated UI post--so it is up front and center.
public class BoardController : Controller
private readonly IMediator _mediator;
private readonly Database.Context _context;
private readonly IHubContext<GameHub> _signalRHub;
private readonly ComputerPlayer _computerPlayer;
public BoardController(Database.Context context,
IHubContext<GameHub> signalRHub,
ComputerPlayer computerPlayer,
IMediator mediator)
_context = context;
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
_mediator = mediator;
private Theme GetThemeOrDefault()
if (Request.Cookies.Keys.All(a => a != "theme"))
return Theme.Steel;
return Enum.Parse(typeof(Theme), Request.Cookies["theme"]) as Theme? ?? Theme.Steel;
private Guid? GetPlayerID()
if (Request.Cookies.TryGetValue("playerID", out var id))
return Guid.Parse(id);
return null;
private string GetClientConnection(Guid id)
return _context.Players.Find(id).ConnectionID;
public ActionResult MovePiece(Guid id, Coord start, Coord end)
(game.WhitePlayerID != playerID && game.CurrentPlayer == (int)Player.White))
Response.StatusCode = 403;
return Content("");
var controller = game.ToGameController();
if (!controller.IsValidMove(start, end))
Response.StatusCode = 403;
return Content("");
var move = controller.Move(start, end);
move.ID = game.ID;
var turn = move.MoveHistory.Last().ToPdnTurn();
if (game.Turns.Any(t => t.MoveNumber == turn.MoveNumber))
var recordedTurn = game.Turns.Single(s => s.MoveNumber == turn.MoveNumber);
Database.PdnMove newMove;
switch (controller.CurrentPlayer)
case Player.White:
newMove = move.MoveHistory.Last().WhiteMove.ToPdnMove();
break;
case Player.Black:
newMove = move.MoveHistory.Last().BlackMove.ToPdnMove();
break;
default:
throw new ArgumentException();
var existingMove = recordedTurn.Moves.FirstOrDefault(a => a.Player == (int)controller.CurrentPlayer);
if (existingMove != null)
recordedTurn.Moves.Remove(existingMove);
recordedTurn.Moves.Add(newMove);
game.Fen = newMove.ResultingFen;
else
game.Turns.Add(move.MoveHistory.Last().ToPdnTurn());
game.Fen = turn.Moves.Single().ResultingFen;
game.CurrentPosition = move.GetCurrentPosition();
game.CurrentPlayer = (int)move.CurrentPlayer;
game.GameStatus = (int)move.GetGameStatus();
game.RowVersion = DateTime.Now;
_context.SaveChanges();
var viewModel = game.ToGameViewModel();
_mediator.Publish(new OnMoveNotification(viewModel)).Wait();
return Content("");
public ActionResult Undo(Guid id)
game.BlackPlayerID == ComputerPlayer.ComputerPlayerID
public ActionResult Resign(Guid id)
game.GameStatus != (int) Status.InProgress
public ActionResult DisplayGame(Guid moveID, Player orientation)
var game = _context.Games
.Include("Turns")
.Include("Turns.Moves")
.FirstOrDefault(f => f.Turns.Any(a => a.Moves.Any(m => m.ID == moveID)));
if (game == null)
Response.StatusCode = 403;
return Content("");
var move = game.Turns.SelectMany(t => t.Moves).First(f => f.ID == moveID);
var viewData = new Dictionary<string, object>
["playerID"] = GetPlayerID(),
["orientation"] = orientation
;
var controller = GameController.FromPosition(Variant.AmericanCheckers, move.ResultingFen);
var viewModel = game.ToGameViewModel();
viewModel.Board.GameBoard = controller.Board.GameBoard;
viewModel.DisplayingLastMove = false;
var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
return Content(board);
public ActionResult Join(Guid id)
public ActionResult Orientate(Guid id, Guid? moveID, Player orientation)
var game = _context.Games
.Include("Turns")
.Include("Turns.Moves")
.FirstOrDefault(f => f.ID == id);
if (game == null)
Response.StatusCode = 403;
return Content("");
var move = game.Turns.SelectMany(t => t.Moves).FirstOrDefault(f => f.ID == moveID) ??
game.Turns.OrderBy(o => o.MoveNumber).LastOrDefault()?.Moves.OrderBy(a => a.CreatedOn).LastOrDefault();
Dictionary<string, object>
viewData = new Dictionary<string, object>
["playerID"] = GetPlayerID(),
["orientation"] = orientation
;
var viewModel = game.ToGameViewModel();
if (moveID != null && moveID.Value != game.Turns.Last().Moves.OrderBy(o => o.CreatedOn).Last().ID)
var fen = move.ResultingFen;
var controller = GameController.FromPosition((Variant)game.Variant, fen);
viewModel.Board.GameBoard = controller.Board.GameBoard;
viewModel.DisplayingLastMove = false;
var board = ComponentGenerator.GetBoard(viewModel, viewData).Replace("[theme]", GetThemeOrDefault().ToString());
return Content(board);
I set up the concept of an Action
to make it easy to perform multiple discrete responses when something happens. Here they are, in sequence of being performed:
The GameCreated actions (actually used in the HomeController
, but since I'm getting the rest of them reviewed anyway...):
public class OnGameCreatedNotification : INotification
public GameViewModel ViewModel get;
public OnGameCreatedNotification(GameViewModel viewModel, Guid currentPlayerID)
ViewModel = viewModel;
public class DoComputerMoveAction : INotificationHandler<OnGameCreatedNotification>
private readonly ComputerPlayer _computerPlayer;
private readonly IHubContext<GameHub> _signalRHub;
public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
public async Task Handle(OnGameCreatedNotification request, CancellationToken cancellationToken)
await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
public class AddGameToLobbyAction : INotificationHandler<OnGameCreatedNotification>
private readonly IHubContext<GameHub> _signalRHub;
public AddGameToLobbyAction(IHubContext<GameHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnGameCreatedNotification notification, CancellationToken cancellationToken)
var lobbyEntry =
$@"<tr>
<td><a href=""/Home/Game/notification.ViewModel.ID"">Resources.Resources.ResourceManager.GetString(notification.ViewModel.Variant.ToString())</a></td>
<td>Resources.Resources.ResourceManager.GetString(notification.ViewModel.GameStatus.ToString())</td>
</tr>";
_signalRHub.Clients.Group("home").InvokeAsync("GameCreated", lobbyEntry);
return Task.CompletedTask;
The GameJoined actions:
public class OnGameJoinedNotification : INotification
public GameViewModel ViewModel get;
public Guid CurrentPlayerID get;
public OnGameJoinedNotification(GameViewModel viewModel, Guid currentPlayerID)
ViewModel = viewModel;
CurrentPlayerID = currentPlayerID;
public class RemoveGameFromLobbyAction : INotificationHandler<OnGameJoinedNotification>
private readonly IHubContext<GameHub> _signalRHub;
public RemoveGameFromLobbyAction(IHubContext<GameHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
_signalRHub.Clients.Group("home").InvokeAsync("GameJoined", notification.ViewModel.ID);
return Task.CompletedTask;
public class UpdateControlsAction : INotificationHandler<OnGameJoinedNotification>
private readonly IMediator _mediator;
private readonly IHubContext<GameHub> _signalRHub;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnGameJoinedNotification notification, CancellationToken cancellationToken)
_signalRHub.Clients.All.InvokeAsync("AddClass", "join", "hide");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", notification.ViewModel.BlackPlayerID == notification.CurrentPlayerID ? "black-player-text" : "white-player-text", "bold");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("AddClass", "new-game", "hide");
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.CurrentPlayerID)).Result).InvokeAsync("RemoveClass", "resign", "hide");
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(notification.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
client.InvokeAsync("SetAttribute", "resign", "title", "Resign");
client.InvokeAsync("SetHtml", "#resign .sr-only", "Resign");
return Task.CompletedTask;
The Move actions:
public class OnMoveNotification : INotification
public GameViewModel ViewModel get;
public OnMoveNotification(GameViewModel viewModel)
ViewModel = viewModel;
public class DoComputerMoveAction : INotificationHandler<OnMoveNotification>
private readonly ComputerPlayer _computerPlayer;
private readonly IHubContext<GameHub> _signalRHub;
public DoComputerMoveAction(IHubContext<GameHub> signalRHub, ComputerPlayer computerPlayer)
_signalRHub = signalRHub;
_computerPlayer = computerPlayer;
public async Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
await _computerPlayer.DoComputerMove(request.ViewModel.ID).ConfigureAwait(false);
public class UpdateMoveHistoryAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateMoveHistoryAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateMoves", request.ViewModel.ID, lastMoveDate, ComponentGenerator.GetMoveControl(request.ViewModel.Turns));
return Task.CompletedTask;
public class UpdateOpponentStateAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateOpponentStateAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateOpponentState", request.ViewModel.ID, lastMoveDate, request.ViewModel.CurrentPlayer.ToString(), request.ViewModel.GameStatus.ToString());
return Task.CompletedTask;
public class UpdateControlsAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
request.ViewModel.GameStatus != Status.InProgress)
client.InvokeAsync("SetAttribute", "undo", "disabled", "");
else
client.InvokeAsync("RemoveAttribute", "undo", "disabled");
client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "RemoveClass" : "AddClass", "new-game", "hide");
client.InvokeAsync(request.ViewModel.GameStatus != Status.InProgress ? "AddClass" : "RemoveClass", "resign", "hide");
return Task.CompletedTask;
This next action isn't used by the game, but rather publishes the data so another system can hook into mine and receive updates about games.
public class SignalGameStateAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<APIHub> _signalRHub;
public SignalGameStateAction(IHubContext<APIHub> signalRHub)
_signalRHub = signalRHub;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var data = JsonConvert.SerializeObject(request.ViewModel);
_signalRHub.Clients.All.InvokeAsync("GameChanged", data);
return Task.CompletedTask;
And finally, I really hate this last one, but I don't see any other way in SignalR Core to publish a message to a group with exceptions by connection ID. They have this functionality in SignalR non-Core, so hopefully soon...
public class UpdateBoardAction : INotificationHandler<OnMoveNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly Database.Context _context;
private readonly IMediator _mediator;
public UpdateBoardAction(IHubContext<GameHub> signalRHub, Database.Context context, IMediator mediator)
_signalRHub = signalRHub;
_context = context;
_mediator = mediator;
private string GetClientConnection(Guid id)
return _context.Players.Find(id).ConnectionID;
Dictionary<string, object> GetViewData(Guid localPlayerID, Player orientation)
return new Dictionary<string, object>
["playerID"] = localPlayerID,
["orientation"] = orientation
;
public Task Handle(OnMoveNotification request, CancellationToken cancellationToken)
var lastMoveDate = _mediator.Send(new GetLastMoveDateMessage(request.ViewModel)).Result;
var blackConnection = GetClientConnection(request.ViewModel.BlackPlayerID);
var whiteConnection = GetClientConnection(request.ViewModel.WhitePlayerID);
if (request.ViewModel.BlackPlayerID != ComputerPlayer.ComputerPlayerID)
_signalRHub.Clients.Client(blackConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.BlackPlayerID, Player.White)));
if (request.ViewModel.WhitePlayerID != ComputerPlayer.ComputerPlayerID)
_signalRHub.Clients.Client(whiteConnection).InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(request.ViewModel.WhitePlayerID, Player.White)));
_signalRHub.Groups.RemoveAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Groups.RemoveAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Clients
.Group(request.ViewModel.ID.ToString())
.InvokeAsync("UpdateBoard", request.ViewModel.ID, lastMoveDate,
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.Black)),
ComponentGenerator.GetBoard(request.ViewModel, GetViewData(Guid.Empty, Player.White)));
_signalRHub.Groups.AddAsync(blackConnection, request.ViewModel.ID.ToString()).Wait();
_signalRHub.Groups.AddAsync(whiteConnection, request.ViewModel.ID.ToString()).Wait();
return Task.CompletedTask;
And the GameCompleted actions:
public class OnGameCompletedNotification : INotification
public GameViewModel ViewModel get;
public OnGameCompletedNotification(GameViewModel viewModel)
ViewModel = viewModel;
public class UpdateControlsAction : INotificationHandler<OnGameCompletedNotification>
private readonly IHubContext<GameHub> _signalRHub;
private readonly IMediator _mediator;
public UpdateControlsAction(IHubContext<GameHub> signalRHub, IMediator mediator)
_signalRHub = signalRHub;
_mediator = mediator;
public Task Handle(OnGameCompletedNotification request, CancellationToken cancellationToken)
var clients = new List<IClientProxy>
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.BlackPlayerID)).Result),
_signalRHub.Clients.Client(_mediator.Send(new GetClientConnectionMessage(request.ViewModel.WhitePlayerID)).Result)
;
foreach (var client in clients)
client.InvokeAsync("SetAttribute", "undo", "disabled", "");
client.InvokeAsync("RemoveClass", "new-game", "hide");
client.InvokeAsync("AddClass", "resign", "hide");
return Task.CompletedTask;
My main concerns here are:
A) Am I following best web practices, including my MVC structure?
B) Is my choice of MediatR for the action system good?
C) See anything else I could change for the better?
c# game asp.net-core checkers-draughts signalr
asked May 14 at 1:28
Hosch250
16.9k561153
16.9k561153
add a comment |Â
add a comment |Â
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f194333%2fplaying-checkers%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password