-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Dice game #2
base: main
Are you sure you want to change the base?
Changes from 8 commits
d00f575
2331f5d
603f7eb
187f9ed
a06af13
b44cd41
a4bc099
3ba171d
0bf13c5
d98c600
ed4ebc3
2adf16b
b2979fa
ba8bdcd
4119a59
20f0f01
67da876
6e8a5a1
d84f276
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Text; | ||
|
@@ -131,4 +132,58 @@ public CharBoard Set(int r, int c, char value) | |
return new CharBoard(Size, newCells); | ||
} | ||
} | ||
|
||
public record DiceBoard | ||
{ | ||
private static readonly ConcurrentDictionary<int, DiceBoard> EmptyCache = new(); | ||
public static DiceBoard Empty(int size) => EmptyCache.GetOrAdd(size, size1 => new DiceBoard(size1)); | ||
|
||
public int Size { get; } | ||
public Dictionary<int, string[]> Cells { get; } | ||
|
||
public string[] this[int r, int c] { | ||
get { | ||
var cellIndex = GetCellIndex(r, c); | ||
if (cellIndex < 0 || cellIndex >= Cells.Count) | ||
return new string[] {"lightblue", "lightblue", "lightblue", "lightblue"}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO it's a bad idea to use string as underlying player color or something like this. Maybe an enum? |
||
return Cells[cellIndex]; | ||
} | ||
} | ||
|
||
public DiceBoard(int size) | ||
{ | ||
var defaultValue = "lightblue"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what "lightblue" means :) Color? Empty cell? |
||
if (size < 1) | ||
throw new ArgumentOutOfRangeException(nameof(size)); | ||
Size = size; | ||
Cells = new Dictionary<int, string[]>(); | ||
for (int i = 0; i < size * size; i++) { | ||
Cells[i] = new string[] {defaultValue, defaultValue, defaultValue, defaultValue}; | ||
} | ||
} | ||
|
||
[JsonConstructor] | ||
public DiceBoard(int size, Dictionary<int, string[]> cells) | ||
{ | ||
if (size < 1) | ||
throw new ArgumentOutOfRangeException(nameof(size)); | ||
if (size * size != cells.Count) | ||
throw new ArgumentOutOfRangeException(nameof(size)); | ||
Size = size; | ||
Cells = cells; | ||
} | ||
|
||
public int GetCellIndex(int r, int c) => r * Size + c; | ||
|
||
public DiceBoard Set(int r, int c, int playerIndex, string value) | ||
{ | ||
if (r < 0 || r >= Size) | ||
throw new ArgumentOutOfRangeException(nameof(r)); | ||
if (c < 0 || c >= Size) | ||
throw new ArgumentOutOfRangeException(nameof(c)); | ||
var cellIndex = GetCellIndex(r, c); | ||
Cells[cellIndex][playerIndex] = value; | ||
return new DiceBoard(Size, Cells); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Drawing; | ||
using System.Linq; | ||
using Stl.Fusion; | ||
using Stl.Time; | ||
using Stl.Time.Internal; | ||
|
||
namespace BoardGames.Abstractions.Games | ||
{ | ||
public record DiceState(DiceBoard Board, Dictionary<int, int> Scores, int MoveIndex = 0, int FirstPlayerIndex = 0) | ||
|
||
{ | ||
public int PlayerIndex => (MoveIndex + FirstPlayerIndex) % 2; | ||
public int NextPlayerIndex => (PlayerIndex + 1) % 2; | ||
public DiceState() : this((DiceBoard) null!, (Dictionary<int, int>) null!) { } | ||
} | ||
|
||
public record DiceMove(int PlayerIndex, int Value, Moment Time = default) : GameMove(Time) | ||
{ | ||
public DiceMove() : this(0, 0) {} | ||
} | ||
|
||
public class DiceEngine : GameEngine<DiceState, DiceMove> | ||
{ | ||
public static int BoardSize { get; } = 8; | ||
public override string Id => "dice"; | ||
public override string Title => "Dice"; | ||
public override string Icon => "fa-dice-five"; | ||
public override int MinPlayerCount => 2; | ||
public override int MaxPlayerCount => 4; | ||
public override bool AutoStart => true; | ||
|
||
public override Game Start(Game game) | ||
{ | ||
var scores = new Dictionary<int, int>() { | ||
{0, -1}, | ||
{1, -1}, | ||
{2, -1}, | ||
{3, -1}, | ||
}; | ||
var firstPlayerIndex = CoarseStopwatch.RandomInt32 & 1; | ||
var state = new DiceState(DiceBoard.Empty(BoardSize), scores,0, firstPlayerIndex); | ||
var player = game.Players[state.PlayerIndex]; | ||
return game with { | ||
StateJson = SerializeState(state), | ||
StateMessage = StandardMessages.MoveTurn(new GameUser(player.UserId)), | ||
}; | ||
} | ||
|
||
public override Game Move(Game game, DiceMove move) | ||
{ | ||
if (game.Stage == GameStage.Ended) | ||
throw new ApplicationException("Game is ended."); | ||
var state = DeserializeState(game.StateJson); | ||
if (move.PlayerIndex != state.PlayerIndex) | ||
throw new ApplicationException("It's another player's turn."); | ||
var board = state.Board; | ||
var player = game.Players[state.PlayerIndex]; | ||
state.Scores[state.PlayerIndex] += move.Value; | ||
var playerScore = state.Scores[state.PlayerIndex]; | ||
var scores = state.Scores; | ||
board = RemovePreviousMoves(board, move.PlayerIndex); | ||
if (playerScore >= (BoardSize * BoardSize) - 1) { | ||
var newState = state with {Board = board, MoveIndex = state.MoveIndex + 1, Scores = scores}; | ||
var newGame = game with {StateJson = SerializeState(newState)}; | ||
newGame = IncrementPlayerScore(newGame, move.PlayerIndex, 1) with { | ||
StateMessage = StandardMessages.Win(new GameUser(player.UserId)), | ||
Stage = GameStage.Ended, | ||
}; | ||
return newGame; | ||
} | ||
|
||
var rowAndCol = GetRowAndColValues(playerScore); | ||
|
||
var nextBoard = board.Set(rowAndCol.Item1, rowAndCol.Item2, state.PlayerIndex, DicePiece[move.PlayerIndex]); | ||
var nextState = state with { | ||
Board = nextBoard, | ||
MoveIndex = state.MoveIndex + 1, | ||
Scores = scores, | ||
}; | ||
var nextPlayer = game.Players[nextState.PlayerIndex]; | ||
var nextGame = game with {StateJson = SerializeState(nextState)}; | ||
nextGame = nextGame with { | ||
StateMessage = StandardMessages.MoveTurn(new GameUser(nextPlayer.UserId)), | ||
}; | ||
return nextGame; | ||
} | ||
|
||
private DiceBoard RemovePreviousMoves(DiceBoard board, int playerIndex) | ||
{ | ||
var playerColor = DicePiece[playerIndex]; | ||
var cells = board.Cells; | ||
cells = cells.ToDictionary(kv => kv.Key, | ||
kv => kv.Value[playerIndex] != playerColor ? kv.Value : UpdatePlayerCellToDefault(kv.Value, playerIndex)); | ||
return new DiceBoard(BoardSize, cells); | ||
} | ||
|
||
private string[] UpdatePlayerCellToDefault(string[] colors, int playerIndex) | ||
{ | ||
colors[playerIndex] = DefaultCell; | ||
return colors; | ||
} | ||
|
||
private (int, int) GetRowAndColValues(long value) | ||
{ | ||
var row = value / BoardSize; | ||
var col = value % BoardSize; | ||
return ((int)row, (int)col); | ||
} | ||
|
||
readonly string DefaultCell = "lightblue"; | ||
|
||
readonly Dictionary<int, string> DicePiece = new Dictionary<int, string>() { | ||
{0, "blue"}, | ||
{1, "green"}, | ||
{2, "red"}, | ||
{3, "yellow"}, | ||
}; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,7 @@ public class HostSettings | |
|
||
// DBs | ||
public string UsePostgreSql { get; set; } = | ||
"Server=localhost;Database=board_games_dev;Port=5432;User Id=postgres;Password=Fusion.0.to.1"; | ||
"Server=localhost;Database=board_games_dev;Port=5432;User Id=postgres;Password=pg051825"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO you shouldn't change the default password that matches the password from docker-compose.yml There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just use the original docker-compose.yml? |
||
public bool UseSqlite { get; set; } = false; | ||
|
||
// Sign-in | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,6 +75,8 @@ public void ConfigureServices(IServiceCollection services) | |
HostSettings = tmpServices.GetRequiredService<HostSettings>(); | ||
|
||
// DbContext & related services | ||
|
||
// HostSettings.UseSqlite = true; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This commented out piece should be removed. |
||
var appTempDir = PathEx.GetApplicationTempDirectory("", true); | ||
var sqliteDbPath = appTempDir & "App_v0_1.db"; | ||
services.AddDbContextFactory<AppDbContext>(builder => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ public override void Use() | |
{ | ||
// Game engines | ||
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IGameEngine, GomokuEngine>()); | ||
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IGameEngine, DiceEngine>()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI this changed in the latest game verison. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check out the attributes on GomokuEngine & copy them to get this working. |
||
Services.AddSingleton(c => | ||
c.GetRequiredService<IEnumerable<IGameEngine>>().ToImmutableDictionary(e => e.Id)); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
@attribute [MatchFor(typeof(DiceEngine), ComponentScopes.GamePlay)] | ||
@using Stl.Time | ||
@inherits GamePlayBase | ||
|
||
@{ | ||
var gameEngine = (DiceEngine) GameEngine; | ||
var gameState = gameEngine.DeserializeState(Game.StateJson); | ||
var board = gameState.Board; | ||
} | ||
|
||
<WhenCommandError Exception="CommandRunner.Error"/> | ||
|
||
@if (Game.Stage != GameStage.Ended) { | ||
<Paragraph> | ||
<GameMessage Message="@Game.StateMessage" Players="Game.Players" Users="Users" /> | ||
<hr> | ||
|
||
<Button Color="@GetDiceColor(gameState.PlayerIndex)" | ||
Style="align-content: center; border-radius: 3px;" | ||
disabled="@(gameState.PlayerIndex != MyPlayerIndex)" | ||
@onclick="_ => MoveAsync()"> | ||
Drop Dice | ||
</Button> | ||
<Button Color="Color.Primary" Style="align-content: center; border: 1px solid darkblue;" disabled>@diceValue</Button> | ||
|
||
</Paragraph> | ||
} | ||
|
||
<Row><Column ColumnSize="ColumnSize.Is6"> | ||
<table><tbody> | ||
@for (var r = 0; r < DiceEngine.BoardSize; r++) { | ||
var row = r; | ||
<tr @key=@row> | ||
@for (var c = 0; c < DiceEngine.BoardSize; c++) { | ||
var col = c; | ||
var cell = board[row, col]; | ||
<td @key=@((row, col))> | ||
|
||
<table style="margin: 1px; min-width: 50px; min-height: 50px; background: lightblue;"> | ||
<tbody> | ||
<tr> | ||
<td align="center"> | ||
<div align="center"> | ||
<i class="fas fa-circle" style="color: @cell[0];"></i> | ||
</div> | ||
</td> | ||
<td align="center"> | ||
<div align="center"> | ||
<i class="fas fa-circle" style="color: @cell[1];"></i> | ||
</div> | ||
</td> | ||
</tr> | ||
<tr> | ||
<td align="center"> | ||
<div align="center"> | ||
<i class="fas fa-circle" style="color: @cell[2];"></i> | ||
</div> | ||
</td> | ||
<td align="center"> | ||
<div align="center"> | ||
<i class="fas fa-circle" style="color: @cell[3];"></i> | ||
</div> | ||
</td> | ||
</tr> | ||
|
||
</tbody> | ||
</table> | ||
</td> | ||
} | ||
</tr> | ||
} | ||
</tbody></table> | ||
</Column> | ||
</Row> | ||
|
||
@code { | ||
private int diceValue; | ||
|
||
private Task MoveAsync() | ||
{ | ||
diceValue = GetRandomDigit(); | ||
var move = new DiceMove(MyPlayerIndex, diceValue); | ||
var command = new Game.MoveCommand(Session, Game.Id, move); | ||
return CommandRunner.CallAsync(command); | ||
} | ||
|
||
private Color GetDiceColor(int playerIndex) | ||
=> playerIndex == MyPlayerIndex ? Color.Success : Color.Danger; | ||
|
||
private int GetRandomDigit() | ||
{ | ||
var rnd = new Random(); | ||
return rnd.Next(1, 7); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
@attribute [MatchFor(typeof(DiceEngine), ComponentScopes.GameRules)] | ||
@inherits GameRulesBase | ||
|
||
<p><b>Rules</b>: Get to the finish line first.</p> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'll write a bit more details here :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most of Game* types are immutable, this dictionary is mutable.