Add db for backup and memory happiness
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.DAL;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Implementations;
|
||||
using JoshHeaps.Net.Utilities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace JoshHeaps.Net.Tests.JsonTests;
|
||||
@@ -11,7 +13,7 @@ internal class JsonConversionTests
|
||||
public void ConvertBoard_WhenGivenStartingBoard_CanConvertToAndFromJson()
|
||||
{
|
||||
// Arrange
|
||||
var gameState = new ChessService().CreateNewGame();
|
||||
var gameState = new ChessService(null).CreateNewGame();
|
||||
|
||||
// Act
|
||||
var serializedGameState = JsonSerializer.Serialize(gameState);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.DAL;
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Implementations;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using JoshHeaps.Net.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -12,7 +15,9 @@ namespace JoshHeaps.Net.Controllers;
|
||||
public class ChessController(
|
||||
IChessService chessService,
|
||||
IHubContext<ChessHub> chessHub,
|
||||
IBackgroundTaskQueue queue) : ControllerBase
|
||||
BackgroundTaskQueue queue,
|
||||
ChessDbAccess dbAccess,
|
||||
StockfishManager stockfishManager) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Store of ongoing games.
|
||||
@@ -21,13 +26,21 @@ public class ChessController(
|
||||
|
||||
private static ConcurrentDictionary<Guid, Task> _gameRemovalTasks = [];
|
||||
|
||||
private static ConcurrentDictionary<Guid, DateTimeOffset> _lastUpdated = [];
|
||||
|
||||
private static ConcurrentDictionary<Guid, GameState> _deliquents = [];
|
||||
|
||||
private static readonly Guid SystemId = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Create a new chess game and store it in-memory.
|
||||
/// </summary>
|
||||
[HttpGet("new")]
|
||||
[HttpGet("new/{difficulty}")]
|
||||
public ActionResult CreateGame(int difficulty = 20)
|
||||
public async Task<ActionResult> CreateGame(int difficulty = 20)
|
||||
{
|
||||
await CheckForGameState();
|
||||
|
||||
var gameState = chessService.CreateNewGame();
|
||||
_games[gameState.GameId] = gameState;
|
||||
|
||||
@@ -37,28 +50,29 @@ public class ChessController(
|
||||
gameState.ComputerDifficulty = difficulty;
|
||||
Guid playerId = Guid.NewGuid();
|
||||
Guid computerId = Guid.NewGuid();
|
||||
var isWhite = Random.Shared.Next(2) == 0;
|
||||
|
||||
gameState.Computer = new(difficulty);
|
||||
var isWhite = false;
|
||||
|
||||
if (isWhite)
|
||||
{
|
||||
gameState.WhitePlayerId = playerId;
|
||||
gameState.BlackPlayerId = computerId;
|
||||
gameState.ComputerColor = PieceColor.Black;
|
||||
}
|
||||
else
|
||||
{
|
||||
gameState.WhitePlayerId = computerId;
|
||||
gameState.BlackPlayerId = playerId;
|
||||
queue.Queue(async () =>
|
||||
gameState.ComputerColor = PieceColor.White;
|
||||
|
||||
queue.Queue(async() =>
|
||||
{
|
||||
// Give user's browser time to connect to signalR and such.
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
await gameState.Computer.MakeMove(gameState, chessHub, chessService);
|
||||
});
|
||||
if (!await stockfishManager.Run(gameState))
|
||||
_deliquents[gameState.GameId] = gameState;
|
||||
}, gameState.GameId);
|
||||
}
|
||||
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1));
|
||||
await dbAccess.SaveAsync(gameState);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
@@ -74,8 +88,10 @@ public class ChessController(
|
||||
/// and a bool indicating if they are White.
|
||||
/// </summary>
|
||||
[HttpGet("JoinGame")]
|
||||
public ActionResult JoinGame()
|
||||
public async Task<ActionResult> JoinGame()
|
||||
{
|
||||
await CheckForGameState();
|
||||
|
||||
Console.WriteLine("joining game");
|
||||
GameState? gameState = _games.Values.FirstOrDefault(g => g.IsOpen);
|
||||
|
||||
@@ -100,7 +116,8 @@ public class ChessController(
|
||||
isWhite = false;
|
||||
}
|
||||
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1));
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromMinutes(10));
|
||||
await dbAccess.SaveAsync(gameState);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
@@ -114,9 +131,11 @@ public class ChessController(
|
||||
/// Get the state of an existing game by ID.
|
||||
/// </summary>
|
||||
[HttpGet("{gameId}")]
|
||||
public ActionResult GetGameState(Guid gameId)
|
||||
public async Task<ActionResult> GetGameState(Guid gameId)
|
||||
{
|
||||
if (!_games.TryGetValue(gameId, out var gameState))
|
||||
var gameState = await CheckForGameState(gameId);
|
||||
|
||||
if (gameState is null)
|
||||
return NotFound("Game not found");
|
||||
|
||||
var response = new
|
||||
@@ -152,9 +171,11 @@ public class ChessController(
|
||||
/// The test passes a JSON body with a MoveDto.
|
||||
/// </summary>
|
||||
[HttpPost("move")]
|
||||
public ActionResult MakeMove([FromBody] MoveDto moveDto)
|
||||
public async Task<ActionResult> MakeMove([FromBody] MoveDto moveDto)
|
||||
{
|
||||
if (!_games.TryGetValue(moveDto.GameId, out var gameState))
|
||||
var gameState = await CheckForGameState(moveDto.GameId);
|
||||
|
||||
if (gameState is null)
|
||||
return NotFound("Game not found");
|
||||
|
||||
// Check if player is authorized to move
|
||||
@@ -173,28 +194,32 @@ public class ChessController(
|
||||
if ((isWhiteMove && piece.Color != PieceColor.White) || (!isWhiteMove && piece?.Color != PieceColor.Black))
|
||||
return Forbid("You cannot move this piece.");
|
||||
|
||||
var result = chessService.MakeMove(gameState, moveDto);
|
||||
var result = await chessService.MakeMove(gameState, moveDto);
|
||||
|
||||
if (!result.Success)
|
||||
return BadRequest(result);
|
||||
|
||||
queue.Queue(async () =>
|
||||
{
|
||||
if (!await stockfishManager.Run(gameState))
|
||||
_deliquents[gameState.GameId] = gameState;
|
||||
}, gameState.GameId);
|
||||
|
||||
|
||||
if (result.IsCheckmate || result.IsStalemate)
|
||||
{
|
||||
// queue game removal
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromMinutes(1));
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromMinutes(10));
|
||||
}
|
||||
else
|
||||
{
|
||||
// increase timeout if play continues.
|
||||
if (gameState.IsVsComputer)
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1));
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromMinutes(10));
|
||||
else
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1));
|
||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromMinutes(10));
|
||||
}
|
||||
|
||||
if (gameState.IsVsComputer && gameState.Computer is not null)
|
||||
queue.Queue(() => gameState.Computer.MakeMove(gameState, chessHub, chessService));
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
@@ -202,12 +227,15 @@ public class ChessController(
|
||||
/// Get the legal moves for a specific piece in a specific game.
|
||||
/// </summary>
|
||||
[HttpGet("{gameId}/legalMoves/{pieceId}")]
|
||||
public ActionResult GetLegalMoves(Guid gameId, string pieceId)
|
||||
public async Task<ActionResult> GetLegalMoves(Guid gameId, string pieceId)
|
||||
{
|
||||
if (!_games.TryGetValue(gameId, out var gameState))
|
||||
var gameState = await CheckForGameState(gameId);
|
||||
|
||||
if (gameState is null)
|
||||
return NotFound("Game not found");
|
||||
|
||||
var moves = chessService.GetLegalMovesForPiece(gameState, pieceId);
|
||||
|
||||
return Ok(moves);
|
||||
}
|
||||
|
||||
@@ -216,9 +244,11 @@ public class ChessController(
|
||||
/// This was in your snippet, so we'll keep it.
|
||||
/// </summary>
|
||||
[HttpGet("{gameId}/legalMoves")]
|
||||
public ActionResult GetAllLegalMoves(Guid gameId)
|
||||
public async Task<ActionResult> GetAllLegalMoves(Guid gameId)
|
||||
{
|
||||
if (!_games.TryGetValue(gameId, out var gameState))
|
||||
var gameState = await CheckForGameState(gameId);
|
||||
|
||||
if (gameState is null)
|
||||
return NotFound("Game not found");
|
||||
|
||||
var allMoves = chessService.GetAllLegalMoves(gameState)
|
||||
@@ -228,36 +258,53 @@ public class ChessController(
|
||||
Moves = x.moves
|
||||
});
|
||||
|
||||
await stockfishManager.Run(gameState);
|
||||
|
||||
return Ok(allMoves);
|
||||
}
|
||||
|
||||
private static void ScheduleRemoveGame(Guid id, TimeSpan delay)
|
||||
private void ScheduleRemoveGame(Guid id, TimeSpan delay)
|
||||
{
|
||||
if (_gameRemovalTasks.ContainsKey(id))
|
||||
{
|
||||
_gameRemovalTasks[id] = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(delay);
|
||||
|
||||
if (_games[id].Computer is not null)
|
||||
await _games[id].Computer!.DisposeAsync();
|
||||
|
||||
_games.Remove(id, out _);
|
||||
_gameRemovalTasks.Remove(id, out _);
|
||||
});
|
||||
_lastUpdated[id] = DateTimeOffset.UtcNow;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_gameRemovalTasks.TryAdd(id, Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(delay);
|
||||
_lastUpdated[id] = DateTimeOffset.UtcNow;
|
||||
|
||||
if (_games[id].Computer is not null)
|
||||
await _games[id].Computer!.DisposeAsync();
|
||||
while (_lastUpdated[id].Add(delay) >= DateTimeOffset.UtcNow)
|
||||
await Task.Delay(TimeSpan.FromMinutes(10));
|
||||
|
||||
await GuidStore.RemoveAsync(id);
|
||||
_games.Remove(id, out _);
|
||||
_gameRemovalTasks.Remove(id, out _);
|
||||
}));
|
||||
}
|
||||
|
||||
private async Task<GameState?> CheckForGameState(Guid gameId = default)
|
||||
{
|
||||
if (!_deliquents.IsEmpty)
|
||||
queue.Queue(() => stockfishManager.RunDeliquents(_deliquents), SystemId);
|
||||
|
||||
if (_games.TryGetValue(gameId, out var existingGame))
|
||||
return existingGame;
|
||||
|
||||
var gameState = await dbAccess.LoadAsync(gameId);
|
||||
|
||||
if (gameState is not null)
|
||||
{
|
||||
_games.TryAdd(gameState.GameId, gameState);
|
||||
|
||||
if (gameState.ComputerColor == gameState.CurrentPlayer)
|
||||
_deliquents.TryAdd(gameState.GameId, gameState);
|
||||
}
|
||||
|
||||
queue.Queue(() => stockfishManager.RunDeliquents(_deliquents), SystemId);
|
||||
|
||||
return gameState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using JoshHeaps.Net.Services.Implementations;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace JoshHeaps.Net.Controllers;
|
||||
|
||||
@@ -9,6 +10,9 @@ public class DebugController : ControllerBase
|
||||
[HttpGet("IpCheck")]
|
||||
public ActionResult<string> GetIpCheckingStatus()
|
||||
{
|
||||
return Ok(Program.CheckingForIpUpdates.ToString());
|
||||
if (AutoIpUpdateService.IsEnabled)
|
||||
return Ok("true");
|
||||
|
||||
return Ok("false");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace JoshHeaps.Net.DAL;
|
||||
|
||||
public class ChessDbAccess(ChessDbContext db)
|
||||
public class ChessDbAccess(IDbContextFactory<ChessDbContext> factory)
|
||||
{
|
||||
private static readonly JsonSerializerOptions opts =
|
||||
new(JsonSerializerDefaults.Web) { WriteIndented = false };
|
||||
|
||||
public async Task SaveAsync(GameState state, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync(ct);
|
||||
|
||||
var json = JsonSerializer.Serialize(state, opts);
|
||||
var entity = await db.Games.FindAsync([state.GameId], ct)
|
||||
?? new GameStateEntity { GameId = state.GameId };
|
||||
var entity = await db.Games.FindAsync([state.GameId], ct);
|
||||
|
||||
if (entity is null)
|
||||
{
|
||||
entity = new GameStateEntity { GameId = state.GameId };
|
||||
db.Games.Add(entity); // INSERT path
|
||||
}
|
||||
else
|
||||
{
|
||||
db.Games.Update(entity); // UPDATE path
|
||||
}
|
||||
|
||||
entity.SerializedState = json;
|
||||
entity.LastMoveUtc = DateTime.UtcNow;
|
||||
|
||||
db.Update(entity);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<GameState?> LoadAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync(ct);
|
||||
|
||||
var e = await db.Games.FindAsync([id], ct);
|
||||
return e is null
|
||||
? null
|
||||
@@ -31,10 +44,54 @@ public class ChessDbAccess(ChessDbContext db)
|
||||
|
||||
public async Task DeleteAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync(ct);
|
||||
|
||||
if (await db.Games.FindAsync([id], ct) is { } e)
|
||||
{
|
||||
db.Remove(e);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteAllAsync(Func<GameState, bool>? filter = null, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync(ct);
|
||||
filter ??= _ => true;
|
||||
|
||||
// 1. Pull every row (just once, not per request)
|
||||
var entities = await db.Games
|
||||
.AsNoTracking() // no change tracking needed
|
||||
.Select(g => g.SerializedState)
|
||||
.ToListAsync(ct);
|
||||
|
||||
List<GameState> allGames = entities.Select(json => JsonSerializer.Deserialize<GameState>(json, opts))
|
||||
.OfType<GameState>()
|
||||
.Where(filter)
|
||||
.ToList();
|
||||
|
||||
foreach (var game in allGames)
|
||||
{
|
||||
await DeleteAsync(game.GameId, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<GameState>> LoadAllAsync(Func<GameState, bool>? filter = null, CancellationToken ct = default)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync(ct);
|
||||
|
||||
filter ??= _ => true;
|
||||
|
||||
// 1. Pull every row (just once, not per request)
|
||||
var entities = await db.Games
|
||||
.AsNoTracking() // no change tracking needed
|
||||
.Select(g => g.SerializedState)
|
||||
.ToListAsync(ct);
|
||||
|
||||
List<GameState> allGames = entities.Select(json => JsonSerializer.Deserialize<GameState>(json, opts))
|
||||
.OfType<GameState>()
|
||||
.Where(filter)
|
||||
.ToList();
|
||||
|
||||
return allGames;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,15 @@ namespace JoshHeaps.Net.DAL;
|
||||
|
||||
public class ChessDbContext : DbContext
|
||||
{
|
||||
public ChessDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ChessDbContext>()
|
||||
.UseSqlite("Data Source=Data/chess.db") // same conn-string as runtime
|
||||
.Options;
|
||||
|
||||
return new ChessDbContext(options);
|
||||
}
|
||||
|
||||
public ChessDbContext(DbContextOptions<ChessDbContext> opts) : base(opts) { }
|
||||
|
||||
public DbSet<GameStateEntity> Games => Set<GameStateEntity>();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -30,7 +30,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using JoshHeaps.Net.DAL;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JoshHeaps.Net.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChessDbContext))]
|
||||
[Migration("20250705173523_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("JoshHeaps.Net.Models.GameStateEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("GameId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastMoveUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SerializedState")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("GameId");
|
||||
|
||||
b.ToTable("Games");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JoshHeaps.Net.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Games",
|
||||
columns: table => new
|
||||
{
|
||||
GameId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
SerializedState = table.Column<string>(type: "TEXT", nullable: false),
|
||||
LastMoveUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Games", x => x.GameId);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Games");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using JoshHeaps.Net.DAL;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace JoshHeaps.Net.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChessDbContext))]
|
||||
partial class ChessDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("JoshHeaps.Net.Models.GameStateEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("GameId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("LastMoveUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SerializedState")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("GameId");
|
||||
|
||||
b.ToTable("Games");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,12 +47,9 @@ public class GameState
|
||||
public Guid BlackPlayerId { get; set; }
|
||||
|
||||
public bool IsVsComputer { get; set; } = false;
|
||||
public PieceColor ComputerColor { get; set; } = PieceColor.Black; // default to black for computer
|
||||
public int ComputerDifficulty { get; set; } = 20;
|
||||
|
||||
[JsonIgnore] // <-- exclude from JSON
|
||||
[NotMapped] // <-- EF won't try to persist it either
|
||||
public Stockfish? Computer { get; set; }
|
||||
|
||||
// optional: convenience
|
||||
public bool IsOpen => !WhiteJoined || !BlackJoined;
|
||||
|
||||
|
||||
+43
-145
@@ -4,168 +4,66 @@ using JoshHeaps.Net.Services.Implementations;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JoshHeaps.Net;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// need to declare the class so I can add the public static bool
|
||||
public class Program
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorPages();
|
||||
var configuration = builder.Configuration;
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
builder.Services.AddSingleton<IChessService, ChessService>();
|
||||
builder.Services.AddSingleton<BackgroundTaskQueue>();
|
||||
builder.Services.AddSingleton<ChessDbAccess>();
|
||||
builder.Services.AddSingleton<StockfishManager>();
|
||||
|
||||
var cs = builder.Configuration.GetConnectionString("ChessDatabase");
|
||||
|
||||
builder.Services.AddPooledDbContextFactory<ChessDbContext>(o => o.UseSqlite(cs));
|
||||
builder.Services.AddHostedService<GameCleanupService>();
|
||||
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
builder.Services.AddHostedService<AutoIpUpdateService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
public static bool CheckingForIpUpdates { get; private set; } = false;
|
||||
var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<ChessDbContext>>();
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
db.Database.Migrate();
|
||||
}
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorPages();
|
||||
var configuration = builder.Configuration;
|
||||
Task updateIpTask;
|
||||
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
updateIpTask = Run(configuration);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
builder.Services.AddSingleton<IChessService, ChessService>();
|
||||
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
|
||||
var cs = builder.Configuration.GetConnectionString("ChessDatabase");
|
||||
builder.Services.AddDbContext<ChessDbContext>(o => o.UseSqlite(cs));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ChessDbContext>();
|
||||
dbContext.Database.Migrate();
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
ctx.Context.Response.Headers.Append("Pragma", "no-cache");
|
||||
ctx.Context.Response.Headers.Append("Expires", "0");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.UseRouting();
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthorization();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapRazorPages();
|
||||
app.MapRazorPages();
|
||||
|
||||
app.MapControllers();
|
||||
app.MapControllers();
|
||||
|
||||
app.MapHub<ChessHub>("/chessHub");
|
||||
app.MapHub<ChessHub>("/chessHub");
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
static async Task Run(IConfiguration config)
|
||||
{
|
||||
CheckingForIpUpdates = true;
|
||||
HttpClient httpClient = new();
|
||||
AAAARecord dnsRecord = await GetDnsRecordAsync(config);
|
||||
string lastKnownIp = dnsRecord.content;
|
||||
TimeSpan checkInterval = TimeSpan.FromMinutes(1);
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
string currentIp = await GetPublicIpAsync(httpClient) ?? "";
|
||||
|
||||
if (lastKnownIp != currentIp)
|
||||
{
|
||||
await UpdateDnsIpAsync(config, dnsRecord, currentIp);
|
||||
|
||||
lastKnownIp = currentIp;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
}
|
||||
|
||||
await Task.Delay(checkInterval);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<string> GetPublicIpAsync(HttpClient client)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await client.GetStringAsync(@"https://api.ipify.org/");
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
Console.WriteLine("Reattempting to grab public ip");
|
||||
|
||||
return await GetPublicIpAsync(client);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<AAAARecord> GetDnsRecordAsync(IConfiguration config)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpClient cfClient = new();
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||
var result = await cfClient.GetAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records");
|
||||
Console.WriteLine(await result.Content.ReadAsStringAsync());
|
||||
var records = System.Text.Json.JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
|
||||
return records!.result[0];
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
Console.WriteLine("Reattempting to grab current ip");
|
||||
|
||||
return await GetDnsRecordAsync(config);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task UpdateDnsIpAsync(IConfiguration config, AAAARecord record, string ip)
|
||||
{
|
||||
HttpClient cfClient = new();
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||
object content = new
|
||||
{
|
||||
comment = "Update as needed",
|
||||
content = ip,
|
||||
name = "@",
|
||||
proxied = true,
|
||||
ttl = 3600,
|
||||
type = "AAAA"
|
||||
};
|
||||
|
||||
var result = await cfClient.PutAsJsonAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records{record.id}", content);
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
Console.WriteLine("Reattempting to update ip");
|
||||
|
||||
await UpdateDnsIpAsync(config, record, ip);
|
||||
}
|
||||
}
|
||||
|
||||
record AAAARecord(string comment, string content, string name, string id);
|
||||
|
||||
record RecordList(List<AAAARecord> result);
|
||||
}
|
||||
app.Run();
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
public class AutoIpUpdateService(
|
||||
IConfiguration config,
|
||||
ILogger<AutoIpUpdateService> log)
|
||||
: BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5);
|
||||
private static readonly HttpClient httpClient = new();
|
||||
public static bool IsEnabled { get; private set; } = false;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stop)
|
||||
{
|
||||
IsEnabled = true;
|
||||
var timer = new PeriodicTimer(CheckInterval);
|
||||
AAAARecord dnsRecord = await GetDnsRecordAsync();
|
||||
string lastKnownIp = dnsRecord.Content;
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stop))
|
||||
{
|
||||
try
|
||||
{
|
||||
string currentIp = await GetPublicIpAsync() ?? "";
|
||||
|
||||
if (lastKnownIp != currentIp)
|
||||
{
|
||||
await UpdateDnsIpAsync(config, dnsRecord, currentIp);
|
||||
|
||||
lastKnownIp = currentIp;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { /* shutting down */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogError(ex, "Error while attempting ip update");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> GetPublicIpAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await httpClient.GetStringAsync(@"https://api.ipify.org/");
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
Console.WriteLine("Reattempting to grab public ip");
|
||||
|
||||
return await GetPublicIpAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AAAARecord> GetDnsRecordAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpClient cfClient = new();
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||
var result = await cfClient.GetAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records");
|
||||
Console.WriteLine(await result.Content.ReadAsStringAsync());
|
||||
var records = System.Text.Json.JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
|
||||
return records!.Result[0];
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
Console.WriteLine("Reattempting to grab current ip");
|
||||
|
||||
return await GetDnsRecordAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task UpdateDnsIpAsync(IConfiguration config, AAAARecord record, string ip)
|
||||
{
|
||||
HttpClient cfClient = new();
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||
object content = new
|
||||
{
|
||||
comment = "Update as needed",
|
||||
content = ip,
|
||||
name = "@",
|
||||
proxied = true,
|
||||
ttl = 3600,
|
||||
type = "AAAA"
|
||||
};
|
||||
|
||||
var result = await cfClient.PutAsJsonAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records{record.Id}", content);
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
Console.WriteLine("Reattempting to update ip");
|
||||
|
||||
await UpdateDnsIpAsync(config, record, ip);
|
||||
}
|
||||
}
|
||||
|
||||
record AAAARecord(string Comment, string Content, string Name, string Id);
|
||||
|
||||
record RecordList(List<AAAARecord> Result);
|
||||
}
|
||||
@@ -4,18 +4,21 @@ using System.Threading.Channels;
|
||||
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
public class BackgroundTaskQueue : IBackgroundTaskQueue
|
||||
public class BackgroundTaskQueue
|
||||
{
|
||||
private readonly ConcurrentDictionary<int, Task> _runningTasks = new();
|
||||
private readonly ConcurrentDictionary<Guid, Task> _runningTasks = new();
|
||||
|
||||
public void Queue(Func<Task> workItem)
|
||||
public void Queue(Func<Task> workItem, Guid workId)
|
||||
{
|
||||
var task = Task.Run(workItem);
|
||||
_runningTasks.TryAdd(task.Id, task);
|
||||
if (_runningTasks.ContainsKey(workId))
|
||||
return;
|
||||
|
||||
task.ContinueWith(t => _runningTasks.TryRemove(t.Id, out _), TaskScheduler.Default);
|
||||
var task = Task.Run(workItem);
|
||||
_runningTasks.TryAdd(workId, task);
|
||||
|
||||
task.ContinueWith(t => _runningTasks.TryRemove(workId, out _), TaskScheduler.Default);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<Task> Running => [.. _runningTasks.Values];
|
||||
public Task WhenAllDone() => Task.WhenAll(Running);
|
||||
public IReadOnlyDictionary<Guid, Task> Running => _runningTasks;
|
||||
public Task WhenAllDone() => Task.WhenAll(Running.Values);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.DAL;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
public class ChessService : IChessService
|
||||
public class ChessService(ChessDbAccess dbAccess) : IChessService
|
||||
{
|
||||
public GameState CreateNewGame()
|
||||
{
|
||||
@@ -122,7 +123,7 @@ public class ChessService : IChessService
|
||||
return legalMoves;
|
||||
}
|
||||
|
||||
public MoveResultDto MakeMove(GameState gameState, MoveDto moveDto)
|
||||
public async Task<MoveResultDto> MakeMove(GameState gameState, MoveDto moveDto)
|
||||
{
|
||||
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId);
|
||||
|
||||
@@ -145,6 +146,12 @@ public class ChessService : IChessService
|
||||
var notation = $"{piece.Id}:{piece.Position}->{targetPos}";
|
||||
gameState.MoveHistory.Add(notation);
|
||||
|
||||
gameState.CurrentPlayer = gameState.CurrentPlayer == PieceColor.White
|
||||
? PieceColor.Black
|
||||
: PieceColor.White;
|
||||
|
||||
await dbAccess.SaveAsync(gameState);
|
||||
|
||||
return new MoveResultDto
|
||||
{
|
||||
Success = true,
|
||||
@@ -166,8 +173,8 @@ public class ChessService : IChessService
|
||||
piece.Position = targetPos;
|
||||
gs.Board[targetPos.Row, targetPos.Col] = piece;
|
||||
|
||||
if (captured != null && captured != piece)
|
||||
captured.Position = new Position(-1, -1);
|
||||
if (captured is not null && captured != piece)
|
||||
captured.Position = new Position(-1, -1); // Remove captured piece from board
|
||||
|
||||
bool wasFirstMove = !piece.HasMoved;
|
||||
piece.HasMoved = true;
|
||||
@@ -179,10 +186,6 @@ public class ChessService : IChessService
|
||||
HandlePawnPromotionIfNeeded(piece, moveDto);
|
||||
|
||||
UpdateCastlingRights(gs, piece, oldPos);
|
||||
|
||||
gs.CurrentPlayer = gs.CurrentPlayer == PieceColor.White
|
||||
? PieceColor.Black
|
||||
: PieceColor.White;
|
||||
}
|
||||
|
||||
private static void HandleEnPassantIfNeeded(GameState gs, ChessPiece piece, Position targetPos, ref ChessPiece? captured)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using JoshHeaps.Net.DAL;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
public sealed class GameCleanupService(
|
||||
IDbContextFactory<ChessDbContext> dbFactory,
|
||||
ILogger<GameCleanupService> log)
|
||||
: BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5);
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stop)
|
||||
{
|
||||
var timer = new PeriodicTimer(CheckInterval);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stop))
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var db = await dbFactory.CreateDbContextAsync(stop);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var expired = await db.Games
|
||||
.Where(g => g.LastMoveUtc.AddDays(7) < DateTime.UtcNow)
|
||||
.ToListAsync(stop);
|
||||
|
||||
if (expired.Count == 0) continue;
|
||||
|
||||
db.Games.RemoveRange(expired);
|
||||
await db.SaveChangesAsync(stop);
|
||||
|
||||
log.LogInformation("🗑️ Removed {Count} expired games", expired.Count);
|
||||
}
|
||||
catch (OperationCanceledException) { /* shutting down */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogError(ex, "Error while purging old games");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.DAL;
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using JoshHeaps.Net.Utilities;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -16,6 +18,12 @@ public sealed class Stockfish : IAsyncDisposable
|
||||
private readonly Channel<string> _stdout = Channel.CreateUnbounded<string>();
|
||||
private readonly int _skill;
|
||||
|
||||
private static readonly SemaphoreSlim _mutex = new(1, 1);
|
||||
private static readonly List<Guid> _statesRunning = [];
|
||||
|
||||
public bool IsRunning => _p is not null && !_p.HasExited;
|
||||
public bool InUse { get; set; }
|
||||
|
||||
public Stockfish(int skill = 20, int hash = 256)
|
||||
{
|
||||
_skill = skill;
|
||||
@@ -44,7 +52,7 @@ public sealed class Stockfish : IAsyncDisposable
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
CreateNoWindow = true,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,7 +78,6 @@ public sealed class Stockfish : IAsyncDisposable
|
||||
Send("uci");
|
||||
WaitFor("uciok").GetAwaiter().GetResult();
|
||||
|
||||
Send($"setoption name Skill Level value {skill}");
|
||||
Send($"setoption name Hash value {hash}");
|
||||
Send("isready");
|
||||
WaitFor("readyok").GetAwaiter().GetResult();
|
||||
@@ -109,8 +116,15 @@ public sealed class Stockfish : IAsyncDisposable
|
||||
_p.Dispose();
|
||||
}
|
||||
|
||||
public async Task MakeMove(GameState state, IHubContext<ChessHub> chessHub, IChessService chessService)
|
||||
public async Task<bool> MakeMove(GameState state, IHubContext<ChessHub> chessHub, IChessService chessService, ChessDbAccess dbAccess)
|
||||
{
|
||||
if (state.ComputerColor != state.CurrentPlayer)
|
||||
return false;
|
||||
|
||||
Send($"setoption name Skill Level value {state.ComputerDifficulty}");
|
||||
Send("isready");
|
||||
await WaitFor("readyok");
|
||||
|
||||
var move = await GetBestMoveAsync(state.ToFen());
|
||||
|
||||
var moveDto = move.ToMoveDto(
|
||||
@@ -119,8 +133,10 @@ public sealed class Stockfish : IAsyncDisposable
|
||||
? state.WhitePlayerId
|
||||
: state.BlackPlayerId);
|
||||
|
||||
var result = chessService.MakeMove(state, moveDto);
|
||||
var result = await chessService.MakeMove(state, moveDto);
|
||||
|
||||
await chessHub.Clients.Group(state.GameId.ToString()).SendAsync("ReceiveMoveUpdate", state.GameId.ToString(), moveDto, result);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using JoshHeaps.Net.DAL;
|
||||
using JoshHeaps.Net.Hubs;
|
||||
using JoshHeaps.Net.Models;
|
||||
using JoshHeaps.Net.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace JoshHeaps.Net.Services.Implementations;
|
||||
|
||||
public class StockfishManager(IHubContext<ChessHub> chessHub, IChessService chessService, ChessDbAccess dbAccess)
|
||||
{
|
||||
private readonly List<Stockfish> _workers = [];
|
||||
|
||||
public async Task<bool> Run(GameState state)
|
||||
{
|
||||
if (!state.IsVsComputer)
|
||||
return true;
|
||||
|
||||
if (!_workers.Any(x => x.InUse) && _workers.Count < 5)
|
||||
_workers.Add(new Stockfish(state.ComputerDifficulty));
|
||||
|
||||
var worker = _workers.First(x => !x.InUse);
|
||||
|
||||
worker.InUse = true;
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(500));
|
||||
return await worker.MakeMove(state, chessHub, chessService, dbAccess);
|
||||
}
|
||||
finally
|
||||
{
|
||||
worker.InUse = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<KeyValuePair<Guid, GameState>>> RunDeliquents(ConcurrentDictionary<Guid, GameState> deliquents)
|
||||
{
|
||||
ConcurrentDictionary<Guid, GameState> continuedDeliquents = [];
|
||||
List<Task> tasks = [];
|
||||
|
||||
foreach (var deliquent in deliquents)
|
||||
{
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var state = deliquent.Value;
|
||||
|
||||
bool result = false;
|
||||
|
||||
if (state.IsVsComputer && state.ComputerColor == state.CurrentPlayer)
|
||||
result = !(await Run(state));
|
||||
|
||||
if (result)
|
||||
continuedDeliquents[deliquent.Key] = deliquent.Value;
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
return continuedDeliquents;
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
public interface IBackgroundTaskQueue
|
||||
{
|
||||
void Queue(Func<Task> workItem);
|
||||
void Queue(Func<Task> workItem, Guid workId);
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ public interface IChessService
|
||||
GameState CreateNewGame();
|
||||
List<(ChessPiece piece, List<Position> moves)> GetAllLegalMoves(GameState gameState);
|
||||
List<Position> GetLegalMovesForPiece(GameState gameState, string pieceId);
|
||||
MoveResultDto MakeMove(GameState gameState, MoveDto moveDto);
|
||||
Task<MoveResultDto> MakeMove(GameState gameState, MoveDto moveDto);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace JoshHeaps.Net.Utilities;
|
||||
|
||||
public static class GuidStore
|
||||
{
|
||||
private static readonly SemaphoreSlim _mutex = new(1, 1);
|
||||
private static readonly List<Guid> _guids = [];
|
||||
|
||||
public static async Task AddAsync(Guid id)
|
||||
{
|
||||
await _mutex.WaitAsync();
|
||||
try { _guids.Add(id); }
|
||||
finally { _mutex.Release(); }
|
||||
}
|
||||
|
||||
public static async Task<List<Guid>> TakeAllAsync()
|
||||
{
|
||||
await _mutex.WaitAsync();
|
||||
try
|
||||
{
|
||||
var copy = _guids.ToList();
|
||||
_guids.Clear();
|
||||
return copy;
|
||||
}
|
||||
finally { _mutex.Release(); }
|
||||
}
|
||||
|
||||
public static async Task<bool> ContainsAsync(Guid id)
|
||||
{
|
||||
await _mutex.WaitAsync();
|
||||
try { return _guids.Contains(id); }
|
||||
finally { _mutex.Release(); }
|
||||
}
|
||||
|
||||
public static async Task RemoveAsync(Guid id)
|
||||
{
|
||||
await _mutex.WaitAsync();
|
||||
try { _guids.Remove(id); }
|
||||
finally { _mutex.Release(); }
|
||||
}
|
||||
|
||||
public static async Task<bool> AddIfAvailable(Guid id)
|
||||
{
|
||||
await _mutex.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!_guids.Contains(id))
|
||||
{
|
||||
_guids.Add(id);
|
||||
|
||||
return true; // Successfully added
|
||||
}
|
||||
|
||||
return false; // Already exists
|
||||
}
|
||||
finally { _mutex.Release(); }
|
||||
}
|
||||
}
|
||||
Generated
+2006
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "^8.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.30.1"
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
.chessSquare {
|
||||
background-color: inherit;
|
||||
position: relative;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.chessSquare.light {
|
||||
|
||||
@@ -105,8 +105,8 @@ function renderPieces(pieces) {
|
||||
}
|
||||
|
||||
e.dataTransfer.setData("text/plain", JSON.stringify({
|
||||
srcRow: piece.Row,
|
||||
srcCol: piece.Col
|
||||
srcRow: piece.row,
|
||||
srcCol: piece.col
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ function getCookie(name) {
|
||||
async function setupSignalRConnection() {
|
||||
signalRConnection = new signalR.HubConnectionBuilder()
|
||||
.withUrl("/chessHub")
|
||||
.configureLogging(signalR.LogLevel.Information)
|
||||
.configureLogging(signalR.LogLevel.Trace)
|
||||
.build();
|
||||
|
||||
signalRConnection.onclose(err => {
|
||||
@@ -386,7 +386,7 @@ function updatePromotionModalImages(color) {
|
||||
console.log("chessLogic.js loaded");
|
||||
window.startNewGame = startNewGame;
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const savedGameId = getCookie("chessGameId");
|
||||
const savedPlayerId = getCookie("chessPlayerId");
|
||||
const savedPlayerIsWhite = getCookie("chessPlayerIsWhite");
|
||||
|
||||
Reference in New Issue
Block a user