Playing Checkers

The name of the pictureThe name of the pictureThe name of the pictureClash 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?







share|improve this question

























    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?







    share|improve this question





















      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?







      share|improve this question











      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?









      share|improve this question










      share|improve this question




      share|improve this question









      asked May 14 at 1:28









      Hosch250

      16.9k561153




      16.9k561153

























          active

          oldest

          votes











          Your Answer




          StackExchange.ifUsing("editor", function ()
          return StackExchange.using("mathjaxEditing", function ()
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          );
          );
          , "mathjax-editing");

          StackExchange.ifUsing("editor", function ()
          StackExchange.using("externalEditor", function ()
          StackExchange.using("snippets", function ()
          StackExchange.snippets.init();
          );
          );
          , "code-snippets");

          StackExchange.ready(function()
          var channelOptions =
          tags: "".split(" "),
          id: "196"
          ;
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function()
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled)
          StackExchange.using("snippets", function()
          createEditor();
          );

          else
          createEditor();

          );

          function createEditor()
          StackExchange.prepareEditor(
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: false,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          );



          );








           

          draft saved


          draft discarded


















          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



































          active

          oldest

          votes













          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes










           

          draft saved


          draft discarded


























           


          draft saved


          draft discarded














          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













































































          Popular posts from this blog

          Chat program with C++ and SFML

          Function to Return a JSON Like Objects Using VBA Collections and Arrays

          Will my employers contract hold up in court?