Skip to content

Commit

Permalink
Players & Turns (overengineering#15)
Browse files Browse the repository at this point in the history
* Adds Sharpasonne.Models.Tests to .vscode build tasks

* Replaces public Engine constructor with Factory that returns an optional type

* Adds Players class to encapsulate the concept of multiple players.

* Adds Players and CurrentPlayerTurn to Engine.

* Renames Players consts to use domain language and use C# stylistic convensions.

* Fixes typos in EngineTests and PlayersTests
  • Loading branch information
nadinengland authored and StefanoChiodino committed Mar 14, 2018
1 parent b8d89c0 commit adea469
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"tasks": [
{
"label": "Test",
"command": "dotnet test Sharpasonne.Tests",
"command": "dotnet test Sharpasonne.Tests && dotnet test Sharpasonne.Model.Tests",
"type": "shell",
"group": "test",
"presentation": {
Expand Down
92 changes: 51 additions & 41 deletions Sharpasonne.Tests/EngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,74 +12,80 @@ namespace Sharpasonne.Tests
{
public class EngineTests : UnitTest<PlaceTileGameAction>
{
class DummyRule : IRule<IGameAction>
{
public bool Verify<T1>(IEngine engine, T1 gameAction) where T1 : IGameAction
{
throw new NotImplementedException();
}
}

class DummyGameAction : IGameAction
{
public IEngine Perform(IEngine engine)
{
throw new NotImplementedException();
}
}

[Fact]
public void When_CreatingAnEngine_Then_BoardIsNotNull()
{
var engine = new Engine(
ImmutableQueue<IGameAction>.Empty,
ImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>>.Empty);
var engine = Engine
.Create(
ImmutableQueue<IGameAction>.Empty,
ImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>>.Empty,
Players.Create(2).ValueOrFailure())
.ValueOrFailure();

Assert.NotNull(engine.Board);
}

[Fact]
public void Given_ARuleSetWithANonGameActionKey_When_CreatingAnEngine_Then_Throw()
public void Given_ARuleSetWithANoneGameActionKey_When_CreatingAnEngine_Then_None()
{
var ruleSet = new Dictionary<Type, IImmutableList<IRule<IGameAction>>>
{
var ruleSet = new Dictionary<Type, IImmutableList<IRule<IGameAction>>> {
[typeof(string)] = ImmutableList.Create<IRule<IGameAction>>(new DummyRule())
}.ToImmutableDictionary();

};

var exception = Record.Exception(() => new Engine(
var option = Engine.Create(
ImmutableQueue<IGameAction>.Empty,
ruleSet));
ruleSet.ToImmutableDictionary(),
Players.Create(2).ValueOrFailure());

Assert.IsType<ArgumentOutOfRangeException>(exception);
Assert.False(option.HasValue);
option.MatchNone((exception) => Assert.IsType<ArgumentOutOfRangeException>(exception));
}

[Fact]
public void Given_ARuleSetWithAGameActionKey_When_CreatingAnEngine_Then_DontThrow()
public void Given_ARuleSetWithAGameActionKey_When_CreatingAnEngine_Then_Some()
{
var ruleSet = new Dictionary<Type, IImmutableList<IRule<IGameAction>>>
{
var ruleSet = new Dictionary<Type, IImmutableList<IRule<IGameAction>>> {
[typeof(DummyGameAction)] = ImmutableList.Create<IRule<IGameAction>>(new DummyRule())
}.ToImmutableDictionary();

};

var exception = Record.Exception(() => new Engine(
var option = Engine.Create(
ImmutableQueue<IGameAction>.Empty,
ruleSet));

Assert.Null(exception);
}

class DummyRule : IRule<IGameAction>
{
public bool Verify<T1>(IEngine engine, T1 gameAction) where T1 : IGameAction
{
throw new NotImplementedException();
}
}
ruleSet.ToImmutableDictionary(),
Players.Create(2).ValueOrFailure());

class DummyGameAction : IGameAction
{
public IEngine Perform(IEngine engine)
{
throw new NotImplementedException();
}
Assert.True(option.HasValue);
}

[Fact]
public void When_PlacingFirstTile_Then_ReturnsANewState()
{
// Arrange
var rules = new Dictionary<Type, IImmutableList<IRule<IGameAction>>>()
{
var rules = new Dictionary<Type, IImmutableList<IRule<IGameAction>>> {
[typeof(PlaceTileGameAction)] = ImmutableList<IRule<IGameAction>>.Empty
};

var engine = new Engine(rules.ToImmutableDictionary());
var engine = Engine
.Create(
ImmutableQueue<IGameAction>.Empty,
rules.ToImmutableDictionary(),
Players.Create(2).ValueOrFailure())
.ValueOrFailure();

// Act
var newState = engine.Perform(MakePlaceTile(0, 0));
Expand All @@ -92,12 +98,16 @@ public void When_PlacingFirstTile_Then_ReturnsANewState()
public void When_PlacingFirstTile_Then_ReturnsNewStateWithSinglePlacedTile()
{
// Arrange
var rules = new Dictionary<Type, IImmutableList<IRule<IGameAction>>>()
{
var rules = new Dictionary<Type, IImmutableList<IRule<IGameAction>>> {
[typeof(PlaceTileGameAction)] = ImmutableList<IRule<IGameAction>>.Empty
};

var engine = new Engine(rules.ToImmutableDictionary());
var engine = Engine
.Create(
ImmutableQueue<IGameAction>.Empty,
rules.ToImmutableDictionary(),
Players.Create(2).ValueOrFailure())
.ValueOrFailure();

// Act
var newState = engine.Perform(MakePlaceTile(0, 0));
Expand Down
56 changes: 56 additions & 0 deletions Sharpasonne.Tests/Engine_PlayerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Optional.Unsafe;
using Sharpasonne.GameActions;
using Sharpasonne.Models;
using Sharpasonne.Rules;
using Sharpasonne.Tests.Rules;
using Moq;
using Xunit;

namespace Sharpasonne.Tests
{
public class Engine_PlayerTests : UnitTest<PlaceTileGameAction>
{
protected Engine CreateEngine(int players)
{
var mockPlayers = new Mock<Players>();

mockPlayers.SetupGet(e => e.Count).Returns(players);

var option = Engine.Create(
ImmutableQueue<IGameAction>.Empty,
new Dictionary<Type, IImmutableList<IRule<IGameAction>>> {
[typeof(PlaceTileGameAction)] = ImmutableList<IRule<IGameAction>>.Empty
}.ToImmutableDictionary(),
mockPlayers.Object
);

return option.ValueOrFailure();
}

[Fact]
public void Given_2Players_When_FirstTurn_Then_NextPlayerIsFirstPlayer()
{
Assert.Equal(1, CreateEngine(players: 2).CurrentPlayerTurn);
}

[Fact]
public void Given_2Players_When_SecondTurn_Then_NextPlayerIsSecondPlayer()
{
var firstTurn = CreateEngine(2);
var secondTurn = firstTurn.Perform(MakePlaceTile(0, 0)).ValueOrFailure();
Assert.Equal(2, secondTurn.CurrentPlayerTurn);
}

[Fact]
public void Given_2Players_When_ThirdTurn_Then_NextPlayerIsFirstPlayerAgain()
{
var firstTurn = CreateEngine(2);
var secondTurn = firstTurn.Perform(MakePlaceTile(0, 0)).ValueOrFailure();
var thirdTurn = secondTurn.Perform(MakePlaceTile(0, 1)).ValueOrFailure();
Assert.Equal(1, thirdTurn.CurrentPlayerTurn);
}
}
}
59 changes: 59 additions & 0 deletions Sharpasonne.Tests/PlayersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Optional.Unsafe;
using Sharpasonne.GameActions;
using Sharpasonne.Models;
using Sharpasonne.Rules;
using Sharpasonne.Tests.Rules;
using Xunit;

namespace Sharpasonne.Tests
{
public class PlayersTests : UnitTest<PlaceTileGameAction>
{
[Fact]
public void Given_ZeroPlayers_When_CreatingPlayers_Then_NoneIsArgumentOutOfRangeException()
{
Players.Create(0).MatchNone(exception => Assert.IsType<ArgumentOutOfRangeException>(exception));
}

[Fact]
public void Given_1Player_When_CreatingPlayers_Then_NoneIsArgumentOutOfRangeException()
{
Players.Create(1).MatchNone(exception => Assert.IsType<ArgumentOutOfRangeException>(exception));
}

[Fact]
public void Given_2Players_When_CreatingPlayers_Then_SomeIsReturned()
{
Assert.NotNull(Players.Create(2).ValueOrFailure());
}

[Fact]
public void Given_6Players_When_CreatingPlayers_Then_NoneIsArgumentOutOfRangeException()
{
Players.Create(6).MatchNone(exception => Assert.IsType<ArgumentOutOfRangeException>(exception));
}

[Fact]
public void Given_NumberOutsideRange_When_FindingNextPlayer_Then_Is1()
{
Assert.Equal(1, Players.Create(2).ValueOrFailure().NextPlayer(0));
Assert.Equal(1, Players.Create(2).ValueOrFailure().NextPlayer(3));
}

[Fact]
public void Given_Player1_When_FindingNextPlayer_Then_Is2()
{
Assert.Equal(2, Players.Create(2).ValueOrFailure().NextPlayer(1));
}

[Fact]
public void Given_LastPlayerInBound_When_FindingNextPlayer_Then_Is1()
{
int count = 4;
Assert.Equal(1, Players.Create(count).ValueOrFailure().NextPlayer(count));
}
}
}
35 changes: 25 additions & 10 deletions Sharpasonne/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,28 @@ public class Engine : IEngine
{
public Board Board { get; } = new Board();

public IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> Rules { get; }
= ImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>>.Empty;
/// <summary>
/// Players collection for managing player stats and turns.
/// </summary>
public Players Players { get; }

/// <summary>
///
/// 1-index number of the player who's turn it is to play an action.
/// </summary>
public int CurrentPlayerTurn { get; } = 1;

public IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> Rules { get; }
= ImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>>.Empty;

/// <summary>Attempts to create a Game engine.</summary>
/// <param name="gameActions"></param>
/// <param name="rules">Must provide list for every action to be used by
/// Perform.</param>
public Engine(
[NotNull] IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> rules)
/// <param name="gameActions"></param>
public static Option<Engine, Exception> Create(
[NotNull] IImmutableQueue<IGameAction> gameActions,
[NotNull] IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> rules,
[NotNull] Players players)
{
var nonGameActions = rules.Keys
.Where(type => !typeof(IGameAction).IsAssignableFrom(type))
Expand All @@ -35,23 +46,27 @@ public Engine(
{
var typeNames = string.Join(", ", nonGameActions.Select(t => t.FullName));
var message = $"'{typeNames}' does not implement {nameof(IGameAction)}";
throw new ArgumentOutOfRangeException(message);
return Option.None<Engine, Exception>(new ArgumentOutOfRangeException(nameof(gameActions), message));
}

this.Rules = rules;
return Option.Some<Engine, Exception>(new Engine(gameActions, rules, players));
}

public Engine(
private Engine(
[NotNull] IImmutableQueue<IGameAction> gameActions,
[NotNull] IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> rules)
: this(rules)
[NotNull] IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> rules,
[NotNull] Players players)
{
this.Rules = rules;
this.Players = players;
}

private Engine(IEngine engine)
{
this.Board = engine.Board;
this.Rules = engine.Rules;
this.Players = engine.Players;
this.CurrentPlayerTurn = engine.Players.NextPlayer(engine.CurrentPlayerTurn);
}

public Optional.Option<Engine, IEnumerable<string>> Perform(
Expand Down
12 changes: 8 additions & 4 deletions Sharpasonne/GameActions/PlaceTileGameAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,23 @@ public PlaceTileGameAction(Point point, Placement placement)
public IEngine Perform(IEngine engine)
{
var board = engine.Board.Set(Placement.Tile, Point, Placement.Orientation);
return new EngineState(board, engine.Rules);
return new EngineState(board, engine);
}
}

public class EngineState : IEngine
{
public EngineState(Board board, IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> rules)
public EngineState(Board board, IEngine engine)
{
Board = board;
Rules = rules;
this.Board = board;
this.Rules = engine.Rules;
this.Players = engine.Players;
this.CurrentPlayerTurn = engine.CurrentPlayerTurn;
}

public Board Board { get; }
public Players Players { get; }
public int CurrentPlayerTurn { get; }
public IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> Rules { get; }
}
}
2 changes: 2 additions & 0 deletions Sharpasonne/IEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace Sharpasonne
public interface IEngine
{
Board Board { get; }
Players Players { get; }
int CurrentPlayerTurn { get; }
IImmutableDictionary<Type, IImmutableList<IRule<IGameAction>>> Rules { get; }
}
}
Loading

0 comments on commit adea469

Please sign in to comment.