Skip to content
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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added identifier.sqlite
Empty file.
55 changes: 55 additions & 0 deletions src/Abstractions/BaseTypes.cs
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;
Expand Down Expand Up @@ -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; }
Copy link
Owner Author

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.


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"};
Copy link
Owner Author

Choose a reason for hiding this comment

The 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";
Copy link
Owner Author

Choose a reason for hiding this comment

The 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);
}
}
}
122 changes: 122 additions & 0 deletions src/Abstractions/Games/Dice.cs
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"},
};
}

}
2 changes: 1 addition & 1 deletion src/Host/HostSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Copy link
Owner Author

Choose a reason for hiding this comment

The 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

Copy link
Owner Author

Choose a reason for hiding this comment

The 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
Expand Down
2 changes: 2 additions & 0 deletions src/Host/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public void ConfigureServices(IServiceCollection services)
HostSettings = tmpServices.GetRequiredService<HostSettings>();

// DbContext & related services

// HostSettings.UseSqlite = true;
Copy link
Owner Author

Choose a reason for hiding this comment

The 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 => {
Expand Down
1 change: 1 addition & 0 deletions src/SharedServices/ClientServicesModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public override void Use()
{
// Game engines
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IGameEngine, GomokuEngine>());
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IGameEngine, DiceEngine>());
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI this changed in the latest game verison.

Copy link
Owner Author

Choose a reason for hiding this comment

The 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));

Expand Down
96 changes: 96 additions & 0 deletions src/UI/Dice/DicePlay.razor
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);
}

}
4 changes: 4 additions & 0 deletions src/UI/Dice/DiceRules.razor
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>
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll write a bit more details here :)