Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34de3c1abc | ||
|
|
311d62a719 | ||
|
|
4217bf3703 | ||
|
|
fd68ada82e | ||
|
|
7c77cd0e52 | ||
|
|
93bdb3518a | ||
|
|
69e1e8450d | ||
|
|
09b408a75b | ||
|
|
e74de40d2e | ||
|
|
03dd308449 | ||
|
|
54e4f25be7 | ||
|
|
c2622132bc | ||
|
|
9bd7eb283e | ||
|
|
9b86bfafe0 | ||
|
|
95b4ae63ea | ||
|
|
edab13f8e4 | ||
|
|
ea78302041 | ||
|
|
3da59f9e93 | ||
|
|
4bc69d1989 | ||
|
|
c98a78d642 | ||
|
|
b6990a30b7 | ||
|
|
86888b21d2 | ||
|
|
140fbbfa76 | ||
|
|
a176e29388 | ||
|
|
b425c4d5d6 | ||
|
|
3b91055666 | ||
|
|
a3b384b221 | ||
|
|
eb3d15527a | ||
|
|
636f133288 |
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(dotnet test:*)",
|
||||||
|
"Bash(dotnet build)",
|
||||||
|
"Bash(dir:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, main ]
|
||||||
|
push:
|
||||||
|
branches: [ master, main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
playwright-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 8.0.x
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build solution
|
||||||
|
run: dotnet build --no-restore
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: |
|
||||||
|
cd JoshHeaps.Net.UiTests
|
||||||
|
dotnet build
|
||||||
|
# Install Chromium browser
|
||||||
|
pwsh bin/Debug/net8.0/playwright.ps1 install chromium
|
||||||
|
# Install system dependencies manually for Ubuntu 24.04 compatibility
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libasound2t64 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libdrm2 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxrandr2 \
|
||||||
|
libgbm1 \
|
||||||
|
libxss1 \
|
||||||
|
libasound2-data \
|
||||||
|
xvfb
|
||||||
|
|
||||||
|
- name: Start server in background
|
||||||
|
run: |
|
||||||
|
cd JoshHeaps.Net
|
||||||
|
dotnet run --urls "https://localhost:7118" &
|
||||||
|
echo $! > server.pid
|
||||||
|
# Wait for server to start - try health endpoint first, then chess page
|
||||||
|
echo "Waiting for server to start..."
|
||||||
|
timeout 60 bash -c 'until curl -k -s https://localhost:7118/health >/dev/null 2>&1 || curl -k -s https://localhost:7118/chess >/dev/null 2>&1; do echo "Waiting..."; sleep 2; done'
|
||||||
|
echo "Server appears to be running"
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: |
|
||||||
|
cd JoshHeaps.Net.UiTests
|
||||||
|
xvfb-run -a dotnet test --logger "trx;LogFileName=test-results.trx" --logger "console;verbosity=detailed"
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
GITHUB_ACTIONS: true
|
||||||
|
DISPLAY: :99
|
||||||
|
|
||||||
|
- name: Stop server
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ -f JoshHeaps.Net/server.pid ]; then
|
||||||
|
kill $(cat JoshHeaps.Net/server.pid) || true
|
||||||
|
fi
|
||||||
|
pkill -f "dotnet run" || true
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-test-results
|
||||||
|
path: |
|
||||||
|
JoshHeaps.Net.UiTests/TestResults/
|
||||||
|
JoshHeaps.Net.UiTests/test-results/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Upload Playwright videos
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-videos
|
||||||
|
path: JoshHeaps.Net.UiTests/test-results/videos/
|
||||||
|
retention-days: 7
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using Microsoft.Playwright;
|
||||||
|
using Microsoft.Playwright.NUnit;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.UiTests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class ApiTests : PageTest
|
||||||
|
{
|
||||||
|
private IAPIRequestContext? _apiContext;
|
||||||
|
private TestConfiguration Config => TestConfiguration.Instance;
|
||||||
|
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public async Task OneTimeSetUp()
|
||||||
|
{
|
||||||
|
if (!Config.Playwright.Headless)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("HEADED", "1");
|
||||||
|
}
|
||||||
|
if (Config.Playwright.SlowMotion > 0)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("PWSLOWMO", Config.Playwright.SlowMotion.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public async Task Setup()
|
||||||
|
{
|
||||||
|
_apiContext = await Playwright.APIRequest.NewContextAsync(new()
|
||||||
|
{
|
||||||
|
BaseURL = Config.Test.BaseUrl,
|
||||||
|
IgnoreHTTPSErrors = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public async Task TearDown()
|
||||||
|
{
|
||||||
|
if (_apiContext != null)
|
||||||
|
{
|
||||||
|
await _apiContext.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task JoinGame_Returns_Valid_Response()
|
||||||
|
{
|
||||||
|
var response = await _apiContext!.GetAsync("/api/chess/JoinGame");
|
||||||
|
|
||||||
|
Assert.That(response.Status, Is.EqualTo(200));
|
||||||
|
|
||||||
|
var jsonResponse = await response.JsonAsync();
|
||||||
|
var gameData = JsonSerializer.Deserialize<JsonElement>(jsonResponse.ToString()!);
|
||||||
|
|
||||||
|
Assert.That(gameData.TryGetProperty("id", out _), Is.True);
|
||||||
|
Assert.That(gameData.TryGetProperty("isWhite", out _), Is.True);
|
||||||
|
Assert.That(gameData.TryGetProperty("gameId", out _), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task CreateCPUGame_Returns_Valid_Response()
|
||||||
|
{
|
||||||
|
var response = await _apiContext!.GetAsync("/api/chess/new/5");
|
||||||
|
|
||||||
|
Assert.That(response.Status, Is.EqualTo(200));
|
||||||
|
|
||||||
|
var jsonResponse = await response.JsonAsync();
|
||||||
|
var gameData = JsonSerializer.Deserialize<JsonElement>(jsonResponse.ToString()!);
|
||||||
|
|
||||||
|
Assert.That(gameData.TryGetProperty("id", out _), Is.True);
|
||||||
|
Assert.That(gameData.TryGetProperty("isWhite", out _), Is.True);
|
||||||
|
Assert.That(gameData.TryGetProperty("gameId", out _), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetGameState_Returns_Valid_Game_Data()
|
||||||
|
{
|
||||||
|
// First create a game
|
||||||
|
var createResponse = await _apiContext!.GetAsync("/api/chess/JoinGame");
|
||||||
|
var createData = JsonSerializer.Deserialize<JsonElement>((await createResponse.JsonAsync()).ToString()!);
|
||||||
|
var gameId = createData.GetProperty("gameId").GetString();
|
||||||
|
|
||||||
|
// Then get the game state
|
||||||
|
var response = await _apiContext.GetAsync($"/api/chess/{gameId}");
|
||||||
|
|
||||||
|
Assert.That(response.Status, Is.EqualTo(200));
|
||||||
|
|
||||||
|
var jsonResponse = await response.JsonAsync();
|
||||||
|
var gameState = JsonSerializer.Deserialize<JsonElement>(jsonResponse.ToString()!);
|
||||||
|
|
||||||
|
Assert.That(gameState.TryGetProperty("gameId", out _), Is.True);
|
||||||
|
Assert.That(gameState.TryGetProperty("currentPlayer", out _), Is.True);
|
||||||
|
Assert.That(gameState.TryGetProperty("pieces", out var pieces), Is.True);
|
||||||
|
|
||||||
|
// Should have 32 pieces initially
|
||||||
|
Assert.That(pieces.GetArrayLength(), Is.EqualTo(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task GetLegalMoves_Returns_Valid_Moves()
|
||||||
|
{
|
||||||
|
// First create a game
|
||||||
|
var createResponse = await _apiContext!.GetAsync("/api/chess/JoinGame");
|
||||||
|
var createData = JsonSerializer.Deserialize<JsonElement>((await createResponse.JsonAsync()).ToString()!);
|
||||||
|
var gameId = createData.GetProperty("gameId").GetString();
|
||||||
|
|
||||||
|
// Get game state to find a piece
|
||||||
|
var gameStateResponse = await _apiContext.GetAsync($"/api/chess/{gameId}");
|
||||||
|
var gameState = JsonSerializer.Deserialize<JsonElement>((await gameStateResponse.JsonAsync()).ToString()!);
|
||||||
|
var pieces = gameState.GetProperty("pieces");
|
||||||
|
var firstPiece = pieces[0];
|
||||||
|
var pieceId = firstPiece.GetProperty("id").GetString();
|
||||||
|
|
||||||
|
// Get legal moves for the piece
|
||||||
|
var response = await _apiContext.GetAsync($"/api/chess/{gameId}/legalMoves/{pieceId}");
|
||||||
|
|
||||||
|
Assert.That(response.Status, Is.EqualTo(200));
|
||||||
|
|
||||||
|
var moves = await response.JsonAsync();
|
||||||
|
Assert.That(moves, Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Invalid_GameId_Returns_NotFound()
|
||||||
|
{
|
||||||
|
var invalidGameId = Guid.NewGuid().ToString();
|
||||||
|
var response = await _apiContext!.GetAsync($"/api/chess/{invalidGameId}");
|
||||||
|
|
||||||
|
Assert.That(response.Status, Is.EqualTo(404));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using Microsoft.Playwright;
|
||||||
|
using Microsoft.Playwright.NUnit;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.UiTests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class ChessGameTests : PageTest
|
||||||
|
{
|
||||||
|
private TestConfiguration Config => TestConfiguration.Instance;
|
||||||
|
|
||||||
|
public override BrowserNewContextOptions ContextOptions()
|
||||||
|
{
|
||||||
|
return Config.GetBrowserContextOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public async Task OneTimeSetUp()
|
||||||
|
{
|
||||||
|
// Set Playwright browser options for headless control
|
||||||
|
if (!Config.Playwright.Headless)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("HEADED", "1");
|
||||||
|
}
|
||||||
|
if (Config.Playwright.SlowMotion > 0)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("PWSLOWMO", Config.Playwright.SlowMotion.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public async Task Setup()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||||
|
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task WaitDefault()
|
||||||
|
{
|
||||||
|
await Page.WaitForTimeoutAsync(Config.Test.WaitTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Chess_Page_Loads_Successfully()
|
||||||
|
{
|
||||||
|
await Expect(Page.Locator("h1")).ToContainTextAsync("Chess");
|
||||||
|
await Expect(Page.Locator("#startGameBtn")).ToBeVisibleAsync();
|
||||||
|
await Expect(Page.Locator("#startCPUGame")).ToBeVisibleAsync();
|
||||||
|
await Expect(Page.Locator("#chessBoard")).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Chess_Board_Has_64_Squares()
|
||||||
|
{
|
||||||
|
var squares = Page.Locator(".chessSquare");
|
||||||
|
await Expect(squares).ToHaveCountAsync(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Start_New_Game_Button_Works()
|
||||||
|
{
|
||||||
|
await Page.ClickAsync("#startGameBtn");
|
||||||
|
|
||||||
|
// Wait for game to start (pieces should appear)
|
||||||
|
await Page.Locator(".chessPiece").First.WaitForAsync();
|
||||||
|
|
||||||
|
// Check that pieces have been rendered
|
||||||
|
var pieces = Page.Locator(".chessPiece");
|
||||||
|
await Expect(pieces).ToHaveCountAsync(32); // Standard chess has 32 pieces
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Start_CPU_Game_Shows_Difficulty_Modal()
|
||||||
|
{
|
||||||
|
await Page.ClickAsync("#startCPUGame");
|
||||||
|
|
||||||
|
// Should show difficulty modal
|
||||||
|
await Expect(Page.Locator("#difficultyModal")).ToBeVisibleAsync();
|
||||||
|
await Expect(Page.Locator("#difficultyModal p")).ToContainTextAsync("Set bot difficulty to:");
|
||||||
|
|
||||||
|
// Should have difficulty buttons 1-20
|
||||||
|
var difficultyButtons = Page.Locator("#difficultyButtonContainer button");
|
||||||
|
await Expect(difficultyButtons).ToHaveCountAsync(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task CPU_Game_Starts_After_Selecting_Difficulty()
|
||||||
|
{
|
||||||
|
await Page.ClickAsync("#startCPUGame");
|
||||||
|
|
||||||
|
// Select difficulty 5
|
||||||
|
await Page.ClickAsync("#difficultyButtonContainer button:nth-child(5)");
|
||||||
|
|
||||||
|
// Modal should close
|
||||||
|
await Expect(Page.Locator("#difficultyModal")).ToBeHiddenAsync();
|
||||||
|
|
||||||
|
// Wait for game to start
|
||||||
|
await Page.Locator(".chessPiece").First.WaitForAsync();
|
||||||
|
|
||||||
|
// Check that pieces have been rendered
|
||||||
|
var pieces = Page.Locator(".chessPiece");
|
||||||
|
await Expect(pieces).ToHaveCountAsync(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Pieces_WhenClicked_HighlightLegalMoves()
|
||||||
|
{
|
||||||
|
// Start a new game first
|
||||||
|
await Page.ClickAsync("#startGameBtn");
|
||||||
|
|
||||||
|
// Wait for pieces to be rendered
|
||||||
|
await Page.Locator(".chessPiece").First.WaitForAsync();
|
||||||
|
|
||||||
|
// Find any white piece and click it
|
||||||
|
var pawn = Page.Locator("#chessBoard div:has(img[src*='WhitePawn'])").First;
|
||||||
|
await pawn.ClickAsync();
|
||||||
|
|
||||||
|
var legalMoves = Page.Locator(".legal");
|
||||||
|
await Expect(legalMoves).ToHaveCountAsync(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
using Microsoft.Playwright;
|
||||||
|
using Microsoft.Playwright.NUnit;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.UiTests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class GameplayTests : PageTest
|
||||||
|
{
|
||||||
|
private TestConfiguration Config => TestConfiguration.Instance;
|
||||||
|
|
||||||
|
public override BrowserNewContextOptions ContextOptions()
|
||||||
|
{
|
||||||
|
return Config.GetBrowserContextOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public async Task OneTimeSetUp()
|
||||||
|
{
|
||||||
|
if (!Config.Playwright.Headless)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("HEADED", "1");
|
||||||
|
}
|
||||||
|
if (Config.Playwright.SlowMotion > 0)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("PWSLOWMO", Config.Playwright.SlowMotion.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public async Task Setup()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||||
|
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Scholars_Mate_Results_In_Checkmate()
|
||||||
|
{
|
||||||
|
// Start a multiplayer game
|
||||||
|
await Page.ClickAsync("#startGameBtn");
|
||||||
|
await Page.WaitForSelectorAsync(".chessPiece");
|
||||||
|
|
||||||
|
if (await Page.Locator("#square-55:has(img[src='/images/Chess Images/WhitePawn.svg'])").CountAsync() == 0)
|
||||||
|
{
|
||||||
|
await Page.ClickAsync("#startGameBtn");
|
||||||
|
await Page.WaitForSelectorAsync(".chessPiece");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a second browser context for the second player
|
||||||
|
await using var browser = await Playwright.Chromium.LaunchAsync();
|
||||||
|
await using var context2 = await browser.NewContextAsync(ContextOptions());
|
||||||
|
var page2 = await context2.NewPageAsync();
|
||||||
|
await page2.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||||
|
await page2.ClickAsync("#startGameBtn");
|
||||||
|
await page2.WaitForSelectorAsync(".chessPiece");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scholar's Mate sequence:
|
||||||
|
* 1. e4 e5
|
||||||
|
* 2. Bc4 Nc6
|
||||||
|
* 3. Qh5 Nf6??
|
||||||
|
* 4. Qxf7# (checkmate)
|
||||||
|
*
|
||||||
|
* In source square to destination square, accounting for black ui swapping:
|
||||||
|
* 1. 52-36 51-35
|
||||||
|
* 2. 61-34 58-37
|
||||||
|
* 3. 59-45 62-45
|
||||||
|
* 4. 45-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Move 1: White plays e4 (pawn e2-e4)
|
||||||
|
await MakeMove(Page, 52, 36); // e2 to e4
|
||||||
|
|
||||||
|
// Move 1: Black plays e5 (pawn e7-e5)
|
||||||
|
await MakeMove(page2, 51, 35); // e7 to e5
|
||||||
|
|
||||||
|
// Move 2: White plays Bc4 (bishop f1-c4)
|
||||||
|
await MakeMove(Page, 61, 34); // f1 to c4
|
||||||
|
|
||||||
|
// Move 2: Black plays Nc6 (knight b8-c6)
|
||||||
|
await MakeMove(page2, 58, 37); // b8 to c6
|
||||||
|
|
||||||
|
// Move 3: White plays Qh5 (queen d1-h5)
|
||||||
|
await MakeMove(Page, 59, 45); // d1 to h5
|
||||||
|
|
||||||
|
// Move 3: Black plays Nf6 (knight g8-f6) - the blunder
|
||||||
|
await MakeMove(page2, 62, 45); // g8 to f6
|
||||||
|
|
||||||
|
string? alertMessage = null;
|
||||||
|
|
||||||
|
Page.Dialog += async (_, dialog) =>
|
||||||
|
{
|
||||||
|
alertMessage = dialog.Message;
|
||||||
|
await dialog.AcceptAsync();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move 4: White plays Qxf7# (queen h5-f7, checkmate)
|
||||||
|
await MakeMove(Page, 45, 13); // h5 to f7
|
||||||
|
|
||||||
|
// The test passes if we can execute all moves without errors
|
||||||
|
// Specific checkmate verification depends on how your UI handles game end
|
||||||
|
Assert.That(alertMessage, Is.Not.Null);
|
||||||
|
Assert.That(alertMessage, Contains.Substring("Checkmate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Pawn_Promotion_Shows_Modal()
|
||||||
|
{
|
||||||
|
// Start a multiplayer game
|
||||||
|
await Page.ClickAsync("#startGameBtn");
|
||||||
|
await Page.WaitForSelectorAsync(".chessPiece");
|
||||||
|
|
||||||
|
if (await Page.Locator("#square-55:has(img[src='/images/Chess Images/WhitePawn.svg'])").CountAsync() == 0)
|
||||||
|
{
|
||||||
|
await Page.ClickAsync("#startGameBtn");
|
||||||
|
await Page.WaitForSelectorAsync(".chessPiece");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a second browser context for the second player (black)
|
||||||
|
await using var browser = await Playwright.Chromium.LaunchAsync();
|
||||||
|
await using var context2 = await browser.NewContextAsync(ContextOptions());
|
||||||
|
var page2 = await context2.NewPageAsync();
|
||||||
|
await page2.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||||
|
await page2.ClickAsync("#startGameBtn");
|
||||||
|
await page2.WaitForSelectorAsync(".chessPiece");
|
||||||
|
|
||||||
|
// Exact sequence: 1. h4 g5 2. hxg5 h6 3. gxh6 a6 4. h7 a5 5. hxg8=Q
|
||||||
|
// in square numbers: 55-39 49-33 39-30 48-40 30-23 55-47 23-15 47-39 15-6
|
||||||
|
|
||||||
|
// Move 1: White h4 (h2-h4, square 55 to 39)
|
||||||
|
await MakeMove(Page, 55, 39); // h2 to h4
|
||||||
|
|
||||||
|
// Move 1: Black g5 (g7-g5, square 14 to 30)
|
||||||
|
await MakeMove(page2, 49, 33); // g7 to g5
|
||||||
|
|
||||||
|
// Move 2: White hxg5 (h4 captures g5, square 39 to 30)
|
||||||
|
await MakeMove(Page, 39, 30); // h4 captures g5
|
||||||
|
|
||||||
|
// Move 2: Black h6 (h7-h6, square 15 to 23)
|
||||||
|
await MakeMove(page2, 48, 40); // h7 to h6
|
||||||
|
|
||||||
|
// Move 3: White gxh6 (g5 captures h6, square 30 to 23)
|
||||||
|
await MakeMove(Page, 30, 23); // g5 captures h6
|
||||||
|
|
||||||
|
// Move 3: Black a6 (a7-a6, square 8 to 16)
|
||||||
|
await MakeMove(page2, 55, 47); // a7 to a6
|
||||||
|
|
||||||
|
// Move 4: White h7 (h6-h7, square 23 to 15)
|
||||||
|
await MakeMove(Page, 23, 15); // h6 to h7
|
||||||
|
|
||||||
|
// Move 4: Black a5 (a6-a5, square 16 to 24)
|
||||||
|
await MakeMove(page2, 47, 39); // a6 to a5
|
||||||
|
|
||||||
|
// Move 5: White hxg8=Q (h7 captures g8 and promotes, square 15 to 6)
|
||||||
|
await MakeMove(Page, 15, 6); // h7 captures g8 (promotion)
|
||||||
|
|
||||||
|
// Check if promotion modal appears
|
||||||
|
var promotionModal = Page.Locator("#promotionModal");
|
||||||
|
var queenOption = Page.Locator("button:has(img[alt='Queen'])");
|
||||||
|
var promotedQueenSquare = Page.Locator("#square-6:has(img[src='/images/Chess Images/WhiteQueen.svg'])");
|
||||||
|
var isModalVisible = await promotionModal.IsVisibleAsync();
|
||||||
|
|
||||||
|
await Expect(promotionModal).ToBeVisibleAsync();
|
||||||
|
await Expect(queenOption).ToBeVisibleAsync();
|
||||||
|
|
||||||
|
await queenOption.ClickAsync();
|
||||||
|
|
||||||
|
await promotionModal.WaitForAsync(new() { State = WaitForSelectorState.Hidden });
|
||||||
|
await Expect(promotedQueenSquare).ToBeVisibleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MakeMove(IPage page, int fromSquare, int toSquare)
|
||||||
|
{
|
||||||
|
// Click on the source square/piece
|
||||||
|
var fromSquareElement = page.Locator($"#square-{fromSquare}");
|
||||||
|
var piece = fromSquareElement.Locator(".chessPiece");
|
||||||
|
|
||||||
|
if (await piece.CountAsync() > 0)
|
||||||
|
{
|
||||||
|
await piece.ClickAsync();
|
||||||
|
await page.WaitForTimeoutAsync(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on the destination square
|
||||||
|
var toSquareElement = page.Locator($"#square-{toSquare}");
|
||||||
|
await toSquareElement.ClickAsync();
|
||||||
|
await page.WaitForTimeoutAsync(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageReference Include="Microsoft.Playwright.NUnit" Version="1.27.1" />
|
||||||
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.9.0" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.Playwright.NUnit" />
|
||||||
|
<Using Include="NUnit.Framework" />
|
||||||
|
<Using Include="System.Text.RegularExpressions" />
|
||||||
|
<Using Include="System.Threading.Tasks" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="appsettings.Integration.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="appsettings.CI.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.Playwright;
|
||||||
|
using Microsoft.Playwright.NUnit;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.UiTests;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class MultiplayerTests : PageTest
|
||||||
|
{
|
||||||
|
private TestConfiguration Config => TestConfiguration.Instance;
|
||||||
|
|
||||||
|
public override BrowserNewContextOptions ContextOptions()
|
||||||
|
{
|
||||||
|
return Config.GetBrowserContextOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public async Task OneTimeSetUp()
|
||||||
|
{
|
||||||
|
if (!Config.Playwright.Headless)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("HEADED", "1");
|
||||||
|
}
|
||||||
|
if (Config.Playwright.SlowMotion > 0)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("PWSLOWMO", Config.Playwright.SlowMotion.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public async Task Setup()
|
||||||
|
{
|
||||||
|
await Page.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||||
|
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Two_Players_Can_Join_Same_Game()
|
||||||
|
{
|
||||||
|
// This test requires running two browser contexts
|
||||||
|
await using var browser = await Playwright.Chromium.LaunchAsync();
|
||||||
|
await using var context1 = await browser.NewContextAsync(ContextOptions());
|
||||||
|
await using var context2 = await browser.NewContextAsync(ContextOptions());
|
||||||
|
|
||||||
|
var page1 = await context1.NewPageAsync();
|
||||||
|
var page2 = await context2.NewPageAsync();
|
||||||
|
|
||||||
|
await page1.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||||
|
await page2.GotoAsync($"{Config.Test.BaseUrl}/chess");
|
||||||
|
|
||||||
|
// Player 1 starts a game
|
||||||
|
await page1.ClickAsync("#startGameBtn");
|
||||||
|
await page1.Locator(".chessPiece").First.WaitForAsync();
|
||||||
|
|
||||||
|
// Player 2 joins the same game
|
||||||
|
await page2.ClickAsync("#startGameBtn");
|
||||||
|
await page2.Locator(".chessPiece").First.WaitForAsync();
|
||||||
|
|
||||||
|
// Both players should see pieces
|
||||||
|
var pieces1 = page1.Locator(".chessPiece");
|
||||||
|
var pieces2 = page2.Locator(".chessPiece");
|
||||||
|
|
||||||
|
await Expect(pieces1).ToHaveCountAsync(32);
|
||||||
|
await Expect(pieces2).ToHaveCountAsync(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task SignalR_Connection_Established()
|
||||||
|
{
|
||||||
|
// Start a game to trigger SignalR connection
|
||||||
|
await Page.ClickAsync("#startGameBtn");
|
||||||
|
await Page.Locator(".chessPiece").First.WaitForAsync();
|
||||||
|
|
||||||
|
// Check browser console for SignalR connection message
|
||||||
|
var consoleLogs = new List<string>();
|
||||||
|
Page.Console += (_, e) => consoleLogs.Add(e.Text);
|
||||||
|
|
||||||
|
await Page.ReloadAsync();
|
||||||
|
await Page.WaitForTimeoutAsync(1000);
|
||||||
|
|
||||||
|
// Should see SignalR connected message in console
|
||||||
|
Assert.That(consoleLogs, Does.Contain("✅ SignalR connected").Or.Contain("SignalR connected"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# JoshHeaps.Net UI Tests
|
||||||
|
|
||||||
|
This project contains end-to-end tests for the JoshHeaps.Net website using Playwright.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Install Playwright browsers:
|
||||||
|
```bash
|
||||||
|
# From the test project directory
|
||||||
|
dotnet build
|
||||||
|
# Then install browsers (requires PowerShell or appropriate command for your OS)
|
||||||
|
bin/Debug/net8.0/playwright.cmd install # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make sure the main application is running on `https://localhost:7065`
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
dotnet test
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
dotnet test --filter "FullyQualifiedName~ChessGameTests"
|
||||||
|
|
||||||
|
# Run with verbose output
|
||||||
|
dotnet test --logger "console;verbosity=detailed"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
### ChessGameTests
|
||||||
|
- Basic chess page functionality
|
||||||
|
- Game start/stop operations
|
||||||
|
- Piece interaction and move validation
|
||||||
|
- UI element verification
|
||||||
|
|
||||||
|
### MultiplayerTests
|
||||||
|
- Two-player game scenarios
|
||||||
|
- SignalR connection testing
|
||||||
|
- Real-time move synchronization
|
||||||
|
|
||||||
|
### ApiTests
|
||||||
|
- Chess API endpoint testing
|
||||||
|
- Game creation and state management
|
||||||
|
- Move validation at API level
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The tests are configured to:
|
||||||
|
- Use headless browsers by default
|
||||||
|
- Take screenshots on failure
|
||||||
|
- Record videos on failure
|
||||||
|
- Automatically start the web server if needed (port 7065)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Tests expect the main application to be available at `https://localhost:7065`
|
||||||
|
- Some tests require browsers to be installed via Playwright CLI
|
||||||
|
- Browser installations may require PowerShell on Windows systems
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Playwright;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.UiTests;
|
||||||
|
|
||||||
|
public class TestConfiguration
|
||||||
|
{
|
||||||
|
private static TestConfiguration? _instance;
|
||||||
|
private static readonly object _lock = new object();
|
||||||
|
|
||||||
|
public PlaywrightSettings Playwright { get; }
|
||||||
|
public TestSettings Test { get; }
|
||||||
|
|
||||||
|
private TestConfiguration()
|
||||||
|
{
|
||||||
|
var isCI = Environment.GetEnvironmentVariable("CI") == "true" ||
|
||||||
|
Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true";
|
||||||
|
|
||||||
|
var configFile = isCI ? "appsettings.CI.json" : "appsettings.Integration.json";
|
||||||
|
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile(configFile, optional: true, reloadOnChange: false)
|
||||||
|
.AddJsonFile("appsettings.Integration.json", optional: true, reloadOnChange: false) // fallback
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Playwright = configuration.GetSection("PlaywrightSettings").Get<PlaywrightSettings>() ?? new PlaywrightSettings();
|
||||||
|
Test = configuration.GetSection("TestSettings").Get<TestSettings>() ?? new TestSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TestConfiguration Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
{
|
||||||
|
_instance = new TestConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BrowserNewContextOptions GetBrowserContextOptions()
|
||||||
|
{
|
||||||
|
return new BrowserNewContextOptions
|
||||||
|
{
|
||||||
|
ViewportSize = new ViewportSize
|
||||||
|
{
|
||||||
|
Width = Playwright.Viewport.Width,
|
||||||
|
Height = Playwright.Viewport.Height
|
||||||
|
},
|
||||||
|
RecordVideoDir = Playwright.EnableVideoRecording ? Path.Combine(Directory.GetCurrentDirectory(), "test-results", "videos") : null,
|
||||||
|
IgnoreHTTPSErrors = Playwright.IgnoreHTTPSErrors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public BrowserTypeLaunchOptions GetLaunchOptions()
|
||||||
|
{
|
||||||
|
return new BrowserTypeLaunchOptions
|
||||||
|
{
|
||||||
|
Headless = Playwright.Headless,
|
||||||
|
SlowMo = Playwright.SlowMotion,
|
||||||
|
Timeout = Playwright.BrowserTimeout
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlaywrightSettings
|
||||||
|
{
|
||||||
|
public bool Headless { get; set; } = true;
|
||||||
|
public float SlowMotion { get; set; } = 0;
|
||||||
|
public ViewportSettings Viewport { get; set; } = new();
|
||||||
|
public float BrowserTimeout { get; set; } = 30000;
|
||||||
|
public float ActionTimeout { get; set; } = 10000;
|
||||||
|
public bool EnableVideoRecording { get; set; } = false;
|
||||||
|
public bool IgnoreHTTPSErrors { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ViewportSettings
|
||||||
|
{
|
||||||
|
public int Width { get; set; } = 1280;
|
||||||
|
public int Height { get; set; } = 720;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestSettings
|
||||||
|
{
|
||||||
|
public string BaseUrl { get; set; } = "https://localhost:7118";
|
||||||
|
public int WaitTimeout { get; set; } = 3000;
|
||||||
|
public bool EnableScreenshots { get; set; } = true;
|
||||||
|
public bool EnableVideoRecording { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"PlaywrightSettings": {
|
||||||
|
"Headless": true,
|
||||||
|
"SlowMotion": 0,
|
||||||
|
"Viewport": {
|
||||||
|
"Width": 1280,
|
||||||
|
"Height": 720
|
||||||
|
},
|
||||||
|
"BrowserTimeout": 60000,
|
||||||
|
"ActionTimeout": 30000,
|
||||||
|
"EnableVideoRecording": true,
|
||||||
|
"IgnoreHTTPSErrors": true
|
||||||
|
},
|
||||||
|
"TestSettings": {
|
||||||
|
"BaseUrl": "https://localhost:7118",
|
||||||
|
"WaitTimeout": 5000,
|
||||||
|
"EnableScreenshots": true,
|
||||||
|
"EnableVideoRecording": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"PlaywrightSettings": {
|
||||||
|
"Headless": false,
|
||||||
|
"SlowMotion": 500,
|
||||||
|
"Viewport": {
|
||||||
|
"Width": 1280,
|
||||||
|
"Height": 720
|
||||||
|
},
|
||||||
|
"BrowserTimeout": 30000,
|
||||||
|
"ActionTimeout": 10000,
|
||||||
|
"IgnoreHTTPSErrors": true
|
||||||
|
},
|
||||||
|
"TestSettings": {
|
||||||
|
"BaseUrl": "https://localhost:7118",
|
||||||
|
"WaitTimeout": 3000,
|
||||||
|
"EnableScreenshots": true,
|
||||||
|
"EnableVideoRecording": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"use": {
|
||||||
|
"headless": true,
|
||||||
|
"viewport": { "width": 1280, "height": 720 },
|
||||||
|
"ignoreHTTPSErrors": true,
|
||||||
|
"video": "retain-on-failure",
|
||||||
|
"screenshot": "only-on-failure"
|
||||||
|
},
|
||||||
|
"projects": [
|
||||||
|
{
|
||||||
|
"name": "chromium",
|
||||||
|
"use": { "browserName": "chromium" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "firefox",
|
||||||
|
"use": { "browserName": "firefox" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"webServer": {
|
||||||
|
"command": "dotnet run --project ../JoshHeaps.Net",
|
||||||
|
"port": 7065,
|
||||||
|
"reuseExistingServer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-5
@@ -5,7 +5,7 @@ VisualStudioVersion = 17.9.34728.123
|
|||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net", "JoshHeaps.Net\JoshHeaps.Net.csproj", "{9F0182CC-470F-4D1A-99F5-348D7921751E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net", "JoshHeaps.Net\JoshHeaps.Net.csproj", "{9F0182CC-470F-4D1A-99F5-348D7921751E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{82206AD3-19DF-4DDA-9647-02B691B78CE8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JoshHeaps.Net.UiTests", "JoshHeaps.Net.UiTests\JoshHeaps.Net.UiTests.csproj", "{360264F4-8292-4EB3-B67D-98376C13438B}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -17,10 +17,10 @@ Global
|
|||||||
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9F0182CC-470F-4D1A-99F5-348D7921751E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{360264F4-8292-4EB3-B67D-98376C13438B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{360264F4-8292-4EB3-B67D-98376C13438B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{360264F4-8292-4EB3-B67D-98376C13438B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{82206AD3-19DF-4DDA-9647-02B691B78CE8}.Release|Any CPU.Build.0 = Release|Any CPU
|
{360264F4-8292-4EB3-B67D-98376C13438B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -9,14 +9,21 @@ namespace JoshHeaps.Net.Controllers;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class ChessController(IChessService chessService, IHubContext<ChessHub> chessHub, IBackgroundTaskQueue queue) : ControllerBase
|
public class ChessController(
|
||||||
|
IChessService chessService,
|
||||||
|
IHubContext<ChessHub> chessHub,
|
||||||
|
IBackgroundTaskQueue queue) : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Store of ongoing games.
|
/// Store of ongoing games.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly ConcurrentDictionary<Guid, GameState> _games = [];
|
private static readonly ConcurrentDictionary<Guid, GameState> _games = [];
|
||||||
|
private static readonly ConcurrentDictionary<Guid, Task> _gameRemovalTasks = [];
|
||||||
|
private static readonly ConcurrentDictionary<Guid, CancellationTokenSource> _gameRemovalCancellationTokens = [];
|
||||||
|
|
||||||
private static ConcurrentDictionary<Guid, Task> _gameRemovalTasks = [];
|
private static readonly TimeSpan _computerGameTimeout = TimeSpan.FromHours(1);
|
||||||
|
private static readonly TimeSpan _multiplayerGameTimeout = TimeSpan.FromDays(1);
|
||||||
|
private static readonly TimeSpan _gameCleanupTimeout = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new chess game and store it in-memory.
|
/// Create a new chess game and store it in-memory.
|
||||||
@@ -54,7 +61,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1));
|
ScheduleRemoveGame(gameState.GameId, _computerGameTimeout);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
@@ -96,7 +103,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
|
|||||||
isWhite = false;
|
isWhite = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1));
|
ScheduleRemoveGame(gameState.GameId, _multiplayerGameTimeout);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
@@ -158,7 +165,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
|
|||||||
var expectedPlayerId = isWhiteMove ? gameState.WhitePlayerId : gameState.BlackPlayerId;
|
var expectedPlayerId = isWhiteMove ? gameState.WhitePlayerId : gameState.BlackPlayerId;
|
||||||
|
|
||||||
if (moveDto.PlayerId != expectedPlayerId)
|
if (moveDto.PlayerId != expectedPlayerId)
|
||||||
return Forbid("You are not the current player.");
|
return StatusCode(403, "You are not the current player.");
|
||||||
|
|
||||||
// Make sure player owns the piece
|
// Make sure player owns the piece
|
||||||
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId);
|
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId);
|
||||||
@@ -167,7 +174,7 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
|
|||||||
return NotFound("Chess piece Id does not exist");
|
return NotFound("Chess piece Id does not exist");
|
||||||
|
|
||||||
if ((isWhiteMove && piece.Color != PieceColor.White) || (!isWhiteMove && piece?.Color != PieceColor.Black))
|
if ((isWhiteMove && piece.Color != PieceColor.White) || (!isWhiteMove && piece?.Color != PieceColor.Black))
|
||||||
return Forbid("You cannot move this piece.");
|
return StatusCode(403, "You cannot move this piece.");
|
||||||
|
|
||||||
var result = chessService.MakeMove(gameState, moveDto);
|
var result = chessService.MakeMove(gameState, moveDto);
|
||||||
|
|
||||||
@@ -175,18 +182,11 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
|
|||||||
return BadRequest(result);
|
return BadRequest(result);
|
||||||
|
|
||||||
if (result.IsCheckmate || result.IsStalemate)
|
if (result.IsCheckmate || result.IsStalemate)
|
||||||
{
|
ScheduleRemoveGame(gameState.GameId, _gameCleanupTimeout);
|
||||||
// queue game removal
|
else if (gameState.IsVsComputer)
|
||||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromMinutes(1));
|
ScheduleRemoveGame(gameState.GameId, _computerGameTimeout);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
ScheduleRemoveGame(gameState.GameId, _multiplayerGameTimeout);
|
||||||
// increase timeout if play continues.
|
|
||||||
if (gameState.IsVsComputer)
|
|
||||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1));
|
|
||||||
else
|
|
||||||
ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameState.IsVsComputer && gameState.Computer is not null)
|
if (gameState.IsVsComputer && gameState.Computer is not null)
|
||||||
queue.Queue(() => gameState.Computer.MakeMove(gameState, chessHub, chessService));
|
queue.Queue(() => gameState.Computer.MakeMove(gameState, chessHub, chessService));
|
||||||
@@ -229,31 +229,36 @@ public class ChessController(IChessService chessService, IHubContext<ChessHub> c
|
|||||||
|
|
||||||
private static void ScheduleRemoveGame(Guid id, TimeSpan delay)
|
private static void ScheduleRemoveGame(Guid id, TimeSpan delay)
|
||||||
{
|
{
|
||||||
if (_gameRemovalTasks.ContainsKey(id))
|
if (_gameRemovalCancellationTokens.TryRemove(id, out var oldCts))
|
||||||
{
|
{
|
||||||
|
oldCts.Cancel();
|
||||||
|
oldCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
_gameRemovalCancellationTokens[id] = cts;
|
||||||
|
|
||||||
_gameRemovalTasks[id] = Task.Run(async () =>
|
_gameRemovalTasks[id] = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await Task.Delay(delay);
|
try
|
||||||
|
|
||||||
if (_games[id].Computer is not null)
|
|
||||||
await _games[id].Computer!.DisposeAsync();
|
|
||||||
|
|
||||||
_games.Remove(id, out _);
|
|
||||||
_gameRemovalTasks.Remove(id, out _);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameRemovalTasks.TryAdd(id, Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
await Task.Delay(delay);
|
await Task.Delay(delay, cts.Token);
|
||||||
|
|
||||||
if (_games[id].Computer is not null)
|
if (_games.TryGetValue(id, out var game) && game.Computer is not null)
|
||||||
await _games[id].Computer!.DisposeAsync();
|
await game.Computer.DisposeAsync();
|
||||||
|
|
||||||
_games.Remove(id, out _);
|
_games.Remove(id, out _);
|
||||||
_gameRemovalTasks.Remove(id, out _);
|
}
|
||||||
}));
|
catch (OperationCanceledException) { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_gameRemovalCancellationTokens.TryGetValue(id, out var currentCts) && currentCts == cts)
|
||||||
|
{
|
||||||
|
_gameRemovalCancellationTokens.TryRemove(id, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
cts.Dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UserSecretsId>53ed685c-bdff-4306-8cc2-9fbe55c85713</UserSecretsId>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -59,7 +59,14 @@
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="~/js/signalr/signalr.min.js"></script>
|
<script src="~/js/signalr/signalr.min.js"></script>
|
||||||
<script src="~/js/ChessScripts/chessLogic.js"></script>
|
<script src="~/js/ChessScripts/GameState.js"></script>
|
||||||
|
<script src="~/js/ChessScripts/ChessUtils.js"></script>
|
||||||
|
<script src="~/js/ChessScripts/ChessAPI.js"></script>
|
||||||
|
<script src="~/js/ChessScripts/ChessSignalR.js"></script>
|
||||||
|
<script src="~/js/ChessScripts/ChessInteractions.js"></script>
|
||||||
|
<script src="~/js/ChessScripts/ChessBoard.js"></script>
|
||||||
|
<script src="~/js/ChessScripts/ChessModals.js"></script>
|
||||||
|
<script src="~/js/ChessScripts/chessMain.js"></script>
|
||||||
}
|
}
|
||||||
|
|
||||||
@section Styles {
|
@section Styles {
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
<div id="ProjectsBox">
|
<div id="ProjectsBox">
|
||||||
<div id="ChessProject" class="projectContainer">
|
<div id="ChessProject" class="projectContainer">
|
||||||
<div class="displayBox diagonal-section-left">
|
<div class="displayBox diagonal-section-left">
|
||||||
<a target="_blank" href="https://github.com/JoshHeaps/OnlineChess">
|
<a target="_blank" href="https://github.com/JoshHeaps/JoshHeaps.Net/blob/master/JoshHeaps.Net/Pages/Chess.cshtml">
|
||||||
<img id="ChessImage" class="projectImage projectImageLeft" src="/images/Chess.jpg" title="Chessboard"/>
|
<img id="ChessImage" class="projectImage projectImageLeft" src="/images/Chess.jpg" title="Chessboard"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="ChessText" class="projectTextRight">
|
<div id="ChessText" class="projectTextRight">
|
||||||
<h2 id="ChessTitle" class="projectHeaderRight projectHeader">Chess</h2>
|
<h2 id="ChessTitle" class="projectHeaderRight projectHeader">Chess</h2>
|
||||||
<p id="ChessDescription" class="projectDescriptionRight projectDescription">
|
<p id="ChessDescription" class="projectDescriptionRight projectDescription">
|
||||||
As someone who loves chess, I wanted to create a chess game that I could play with my friends and family. This project is a work in progress, as currently it can only be played locally. It was built in C# on .NET 8.0, using winforms, because I like a challenge. I plan to add online multiplayer functionality in the future.
|
As someone who loves chess, I wanted to create a chess game that I could play with my friends and family. It was made using .NET razor pages, a .NET web api backend, and a good amount of javascript on the frontend. Feel free to check it out in the demos below :)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,9 +49,15 @@
|
|||||||
<button id="ChessDemo" class="demoButton" onclick="window.location.href='/chess'">
|
<button id="ChessDemo" class="demoButton" onclick="window.location.href='/chess'">
|
||||||
Play Chess
|
Play Chess
|
||||||
</button>
|
</button>
|
||||||
<button id="ChessDemo" class="demoButton" onclick="window.location.href='/particles'">
|
<button id="ParticleDemo" class="demoButton" onclick="window.location.href='/particles'">
|
||||||
Particle Simulator
|
Particle Simulator
|
||||||
</button>
|
</button>
|
||||||
|
<button id="MemoryLane" class="demoButton" onclick="window.location.href='/memorylane'">
|
||||||
|
Memory Lane
|
||||||
|
</button>
|
||||||
|
<button id="CloadStorageDemo" class="demoButton" onclick="window.location.href='https://media.joshheaps.net'">
|
||||||
|
Cloud Image Storage
|
||||||
|
</button>
|
||||||
<button id="MoreFiller" class="demoButton">
|
<button id="MoreFiller" class="demoButton">
|
||||||
More coming soon...
|
More coming soon...
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
@page
|
||||||
|
@model JoshHeaps.Net.Pages.MemoryLaneModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Memory Lane";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="memory-lane-container">
|
||||||
|
<h1 class="memory-lane-header">Memory Lane</h1>
|
||||||
|
<p class="memory-lane-subtitle">Special moments and celebrations from our life together</p>
|
||||||
|
|
||||||
|
<div class="events-grid">
|
||||||
|
<a href="/memoryLane/fifthTempleAnniversary" class="event-card">
|
||||||
|
<div class="event-icon">💝</div>
|
||||||
|
<div class="event-title">Our 5th Sealing Anniversary</div>
|
||||||
|
<div class="event-description">
|
||||||
|
Celebrating the fifth year of our eternal journey together
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="event-card disabled">
|
||||||
|
<div class="event-icon">🎂</div>
|
||||||
|
<div class="event-title">Birthdays & Births</div>
|
||||||
|
<div class="event-description">
|
||||||
|
Welcoming new life and celebrating another year
|
||||||
|
</div>
|
||||||
|
<div class="coming-soon">Coming soon...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="event-card disabled">
|
||||||
|
<div class="event-icon">🌟</div>
|
||||||
|
<div class="event-title">Milestones</div>
|
||||||
|
<div class="event-description">
|
||||||
|
Big achievements and unforgettable moments
|
||||||
|
</div>
|
||||||
|
<div class="coming-soon">Coming soon...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="event-card disabled">
|
||||||
|
<div class="event-icon">✨</div>
|
||||||
|
<div class="event-title">More Memories</div>
|
||||||
|
<div class="event-description">
|
||||||
|
Even more special occasions and celebrations
|
||||||
|
</div>
|
||||||
|
<div class="coming-soon">Coming soon...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="home-button">← Back to Home</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<link href="/css/memoryLane/page.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.Pages
|
||||||
|
{
|
||||||
|
public class MemoryLaneModel() : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
@page
|
||||||
|
@model JoshHeaps.Net.Pages.MemoryLane.FifthTempleAnniversary
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Happy 5th Anniversary, Morgan!";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="anniversary-container">
|
||||||
|
<h1 class="anniversary-header">Happy 5th Anniversary, Morgan!</h1>
|
||||||
|
|
||||||
|
<div id="WelcomeMessage"></div>
|
||||||
|
|
||||||
|
<div class="hearts">💖 💕 💗</div>
|
||||||
|
|
||||||
|
<div class="photo-gallery">
|
||||||
|
<div class="photo-frame">
|
||||||
|
<div class="photo-wrapper">
|
||||||
|
<img src="/images/Wife Images/45053.jpeg" alt="Morgan with our beautiful baby">
|
||||||
|
</div>
|
||||||
|
<div class="photo-caption">The most amazing mother to our precious child</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="photo-frame">
|
||||||
|
<div class="photo-wrapper">
|
||||||
|
<img src="/images/Wife Images/45054.jpeg" alt="Josh and Morgan together">
|
||||||
|
</div>
|
||||||
|
<div class="photo-caption">My best friend and eternal companion</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="love-message">
|
||||||
|
<h2>Five Years of Forever</h2>
|
||||||
|
<p>
|
||||||
|
Five years ago today, on October 2nd, 2020, I knelt across the altar from you in the temple
|
||||||
|
and we were sealed for time and all eternity. It was the best decision I ever made.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Morgan, you are everything I could have ever hoped for and so much more. You're not just
|
||||||
|
my wife—you're my best friend, my partner in every adventure, and the love of my life.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You make me laugh every single day. Your kindness touches everyone around you. Your
|
||||||
|
strength amazes me. Your beauty, inside and out, takes my breath away. Watching you
|
||||||
|
become a mother has shown me depths of love and dedication I never knew existed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Thank you for five incredible years of love, laughter, growth, and joy. Thank you for
|
||||||
|
choosing me, for believing in me, and for building this beautiful life with me.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Here's to five years down and an eternity to go. I love you more today than yesterday,
|
||||||
|
and I'll love you even more tomorrow.
|
||||||
|
</p>
|
||||||
|
<p style="font-size: 1.5rem; margin-top: 2rem; color: #764ba2;">
|
||||||
|
<strong>I love you, Morgan. Forever and always. 💖</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hearts">💝 💞 💓</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<link href="/css/anniversary/page.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script src="/js/anniversary/page.js"></script>
|
||||||
|
<script src="/js/utils.js"></script>
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace JoshHeaps.Net.Pages.MemoryLane
|
||||||
|
{
|
||||||
|
public class FifthTempleAnniversary() : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,31 +8,23 @@ public class AutoIpUpdateService(
|
|||||||
ILogger<AutoIpUpdateService> log)
|
ILogger<AutoIpUpdateService> log)
|
||||||
: BackgroundService
|
: BackgroundService
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5);
|
|
||||||
private static readonly HttpClient httpClient = new();
|
|
||||||
public static bool IsEnabled { get; private set; } = false;
|
public static bool IsEnabled { get; private set; } = false;
|
||||||
|
|
||||||
|
private static readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1);
|
||||||
|
private static readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stop)
|
protected override async Task ExecuteAsync(CancellationToken stop)
|
||||||
{
|
{
|
||||||
IsEnabled = true;
|
IsEnabled = true;
|
||||||
var timer = new PeriodicTimer(CheckInterval);
|
var timer = new PeriodicTimer(_checkInterval);
|
||||||
AAAARecord dnsRecord = await GetDnsRecordAsync();
|
|
||||||
string lastKnownIp = dnsRecord.Content;
|
|
||||||
|
|
||||||
while (await timer.WaitForNextTickAsync(stop))
|
while (await timer.WaitForNextTickAsync(stop))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string currentIp = await GetPublicIpAsync() ?? "";
|
await UpdateIpAddressIfChanged();
|
||||||
|
|
||||||
if (lastKnownIp != currentIp)
|
|
||||||
{
|
|
||||||
await UpdateDnsIpAsync(config, dnsRecord, currentIp);
|
|
||||||
|
|
||||||
lastKnownIp = currentIp;
|
|
||||||
}
|
}
|
||||||
}
|
catch (OperationCanceledException) { break; }
|
||||||
catch (OperationCanceledException) { /* shutting down */ }
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log.LogError(ex, "Error while attempting ip update");
|
log.LogError(ex, "Error while attempting ip update");
|
||||||
@@ -40,11 +32,33 @@ public class AutoIpUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpdateIpAddressIfChanged()
|
||||||
|
{
|
||||||
|
var dnsRecords = await GetDnsRecordAsync();
|
||||||
|
string dnsRecordIp = dnsRecords[0].Content;
|
||||||
|
|
||||||
|
if (!dnsRecords.Records.All(x => x.Content == dnsRecords[0].Content))
|
||||||
|
dnsRecordIp = string.Empty;
|
||||||
|
|
||||||
|
string publicIp = await GetPublicIpAsync() ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(publicIp) || dnsRecordIp == publicIp)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var dnsRecord in dnsRecords.Records)
|
||||||
|
{
|
||||||
|
if (dnsRecord.Content == publicIp)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await UpdateDnsIpAsync(config, dnsRecord, publicIp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<string> GetPublicIpAsync()
|
private static async Task<string> GetPublicIpAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await httpClient.GetStringAsync(@"https://api.ipify.org/");
|
return await _httpClient.GetStringAsync(@"https://api.ipify.org/");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -55,7 +69,7 @@ public class AutoIpUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<AAAARecord> GetDnsRecordAsync()
|
private async Task<RecordList> GetDnsRecordAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -64,8 +78,8 @@ public class AutoIpUpdateService(
|
|||||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||||
var result = await cfClient.GetAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records");
|
var result = await cfClient.GetAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records");
|
||||||
Console.WriteLine(await result.Content.ReadAsStringAsync());
|
Console.WriteLine(await result.Content.ReadAsStringAsync());
|
||||||
var records = System.Text.Json.JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
|
var records = JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
|
||||||
return records!.Result[0];
|
return records!;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -76,22 +90,23 @@ public class AutoIpUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task UpdateDnsIpAsync(IConfiguration config, AAAARecord record, string ip)
|
private static async Task UpdateDnsIpAsync(IConfiguration config, DnsRecord record, string ip)
|
||||||
{
|
{
|
||||||
HttpClient cfClient = new();
|
HttpClient cfClient = new();
|
||||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
|
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
|
||||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||||
object content = new
|
|
||||||
|
object updateRequestBody = new
|
||||||
{
|
{
|
||||||
comment = "Update as needed",
|
name = record.Name,
|
||||||
|
ttl = record.Ttl,
|
||||||
|
type = record.Type,
|
||||||
|
comment = record.Comment,
|
||||||
content = ip,
|
content = ip,
|
||||||
name = "@",
|
proxied = record.Proxied,
|
||||||
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);
|
var result = await cfClient.PatchAsJsonAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records/{record.Id}", updateRequestBody);
|
||||||
|
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -102,11 +117,23 @@ public class AutoIpUpdateService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record AAAARecord(
|
record DnsRecord(
|
||||||
|
[property: JsonPropertyName("name")] string Name,
|
||||||
|
[property: JsonPropertyName("ttl")] int Ttl,
|
||||||
|
[property: JsonPropertyName("type")] string Type,
|
||||||
[property: JsonPropertyName("comment")] string Comment,
|
[property: JsonPropertyName("comment")] string Comment,
|
||||||
[property: JsonPropertyName("content")] string Content,
|
[property: JsonPropertyName("content")] string Content,
|
||||||
[property: JsonPropertyName("name")] string Name,
|
[property: JsonPropertyName("proxied")] bool Proxied,
|
||||||
[property: JsonPropertyName("id")] string Id);
|
[property: JsonPropertyName("id")] string Id);
|
||||||
|
|
||||||
record RecordList([property: JsonPropertyName("result")] List<AAAARecord> Result);
|
record RecordList([property: JsonPropertyName("result")] List<DnsRecord> Records)
|
||||||
|
{
|
||||||
|
public DnsRecord this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Records[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ public class ChessService : IChessService
|
|||||||
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == pieceId);
|
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == pieceId);
|
||||||
|
|
||||||
if (piece == null) return [];
|
if (piece == null) return [];
|
||||||
if (piece.Color != gameState.CurrentPlayer) return [];
|
|
||||||
|
|
||||||
var candidateMoves = GenerateCandidateMoves(gameState, piece);
|
var candidateMoves = GenerateCandidateMoves(gameState, piece);
|
||||||
var legalMoves = new List<Position>();
|
var legalMoves = new List<Position>();
|
||||||
@@ -529,8 +528,15 @@ public class ChessService : IChessService
|
|||||||
var ep = gs.EnPassantTarget.Value;
|
var ep = gs.EnPassantTarget.Value;
|
||||||
|
|
||||||
if (ep.Row == forward1 && Math.Abs(ep.Col - startCol) == 1)
|
if (ep.Row == forward1 && Math.Abs(ep.Col - startCol) == 1)
|
||||||
|
{
|
||||||
|
// Verify there's an enemy pawn to capture
|
||||||
|
int enemyPawnRow = piece.Color == PieceColor.White ? ep.Row + 1 : ep.Row - 1;
|
||||||
|
var enemyPawn = gs.Board[enemyPawnRow, ep.Col];
|
||||||
|
|
||||||
|
if (enemyPawn != null && enemyPawn.Type == PieceType.Pawn && enemyPawn.Color != piece.Color)
|
||||||
moves.Add(ep);
|
moves.Add(ep);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return moves;
|
return moves;
|
||||||
}
|
}
|
||||||
@@ -542,11 +548,7 @@ public class ChessService : IChessService
|
|||||||
=> GenerateSlidingMoves(gs, piece, [(1, 1), (1, -1), (-1, 1), (-1, -1)]);
|
=> GenerateSlidingMoves(gs, piece, [(1, 1), (1, -1), (-1, 1), (-1, -1)]);
|
||||||
|
|
||||||
private static List<Position> GenerateQueenMoves(GameState gs, ChessPiece piece)
|
private static List<Position> GenerateQueenMoves(GameState gs, ChessPiece piece)
|
||||||
=> GenerateSlidingMoves(gs, piece,
|
=> [..GenerateRookMoves(gs, piece), ..GenerateBishopMoves(gs, piece)];
|
||||||
[
|
|
||||||
(1, 0), (-1, 0), (0, 1), (0, -1),
|
|
||||||
(1, 1), (1, -1), (-1, 1), (-1, -1)
|
|
||||||
]);
|
|
||||||
|
|
||||||
private static List<Position> GenerateSlidingMoves(GameState gs, ChessPiece piece, (int dr, int dc)[] directions)
|
private static List<Position> GenerateSlidingMoves(GameState gs, ChessPiece piece, (int dr, int dc)[] directions)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-header {
|
||||||
|
color: white;
|
||||||
|
font-size: 3.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||||
|
animation: fadeInDown 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#WelcomeMessage {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2rem 2.5rem;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
max-width: 900px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||||
|
animation: fadeIn 2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hearts {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
animation: heartbeat 1.5s ease-in-out infinite;
|
||||||
|
width: fit-content;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-gallery {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 1200px;
|
||||||
|
padding: 0 1rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-frame {
|
||||||
|
background: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 15px 40px rgba(0,0,0,0.3);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
animation: fadeInUp 1.5s ease-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-frame:hover {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
box-shadow: 0 20px 50px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-frame img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-frame:nth-child(2) img {
|
||||||
|
object-position: center 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-caption {
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.love-message {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 2.5rem 3rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 1000px;
|
||||||
|
color: #444;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||||
|
animation: fadeIn 2.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.love-message h2 {
|
||||||
|
color: #764ba2;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.love-message p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.9;
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.love-message p:last-of-type {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes heartbeat {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.photo-gallery {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper {
|
||||||
|
height: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.anniversary-container {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-header {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#WelcomeMessage {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.love-message {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.love-message h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.love-message p {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hearts {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
#chessBoard {
|
#boardContainer {
|
||||||
|
position: relative;
|
||||||
|
width: fit-content;
|
||||||
margin: 2vw auto;
|
margin: 2vw auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chessBoard {
|
||||||
width: 60vw;
|
width: 60vw;
|
||||||
height: 60vw;
|
height: 60vw;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -159,3 +164,29 @@
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
pointer-events: none; /* ensures img doesn't steal the click */
|
pointer-events: none; /* ensures img doesn't steal the click */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chessSquare .coordinate-label {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 1.2vw;
|
||||||
|
font-weight: bold;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chessSquare.light .coordinate-label {
|
||||||
|
color: #656770;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chessSquare.dark .coordinate-label {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chessSquare .row-label {
|
||||||
|
top: 2px;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chessSquare .col-label {
|
||||||
|
bottom: 2px;
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
body {
|
||||||
|
background: #1a1a1a;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-lane-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-lane-header {
|
||||||
|
color: #f5f5f5;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-lane-subtitle {
|
||||||
|
color: #a0a0a0;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 3rem auto;
|
||||||
|
max-width: 900px;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card:hover {
|
||||||
|
background: #2f2f2f;
|
||||||
|
border-color: #4a4a4a;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card.disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card.disabled:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #f5f5f5;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-button {
|
||||||
|
margin-top: 4rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: transparent;
|
||||||
|
color: #f5f5f5;
|
||||||
|
border: 1px solid #3a3a3a;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-button:hover {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.memory-lane-header {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-lane-subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,113 @@
|
|||||||
|
const ChessAPI = {
|
||||||
|
async joinGame() {
|
||||||
|
const response = await fetch('/api/chess/JoinGame');
|
||||||
|
return await response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async createCPUGame(difficulty) {
|
||||||
|
const response = await fetch(`/api/chess/new/${difficulty}`);
|
||||||
|
return await response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async getGameState(gameId) {
|
||||||
|
const response = await fetch(`/api/chess/${gameId}`);
|
||||||
|
return await response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async getLegalMoves(pieceId) {
|
||||||
|
const response = await fetch(`/api/chess/${GameState.currentGameId}/legalMoves/${pieceId}`);
|
||||||
|
if (!response.ok) throw new Error("API failed");
|
||||||
|
return await response.json();
|
||||||
|
},
|
||||||
|
|
||||||
|
async makeMove(moveDto) {
|
||||||
|
const response = await fetch("/api/chess/move", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(moveDto)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let message = await response.text();
|
||||||
|
throw new Error(message || "Invalid move or not your turn.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message || "Invalid move or not your turn.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleMove(targetRow, targetCol) {
|
||||||
|
if (!GameState.selectedPiece) return;
|
||||||
|
|
||||||
|
const isPawn = GameState.selectedPiece.type === 0;
|
||||||
|
const reachedEnd = (Number(GameState.selectedPiece.color) === 0 && Number(targetRow) === 0) ||
|
||||||
|
(Number(GameState.selectedPiece.color) === 1 && Number(targetRow) === 0);
|
||||||
|
|
||||||
|
let choice = null;
|
||||||
|
|
||||||
|
if (isPawn && reachedEnd) {
|
||||||
|
choice = await ChessModals.promptPromotion();
|
||||||
|
if (!choice) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[targetRow, targetCol] = ChessUtils.flipCoordinates(targetRow, targetCol);
|
||||||
|
|
||||||
|
const moveDto = {
|
||||||
|
GameId: GameState.currentGameId,
|
||||||
|
PlayerId: GameState.currentPlayerId,
|
||||||
|
PieceId: GameState.selectedPiece.id,
|
||||||
|
SourceRow: GameState.selectedPiece.row,
|
||||||
|
SourceCol: GameState.selectedPiece.col,
|
||||||
|
TargetRow: targetRow,
|
||||||
|
TargetCol: targetCol,
|
||||||
|
PromotionChoice: choice
|
||||||
|
};
|
||||||
|
|
||||||
|
GameState.setPreviousMove([GameState.selectedPiece.row, GameState.selectedPiece.col], [targetRow, targetCol]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const moveResult = await this.makeMove(moveDto);
|
||||||
|
|
||||||
|
const updatedGame = await this.getGameState(GameState.currentGameId);
|
||||||
|
ChessBoard.renderPieces(updatedGame.pieces);
|
||||||
|
|
||||||
|
GameState.clearSelection();
|
||||||
|
ChessInteractions.clearHighlights();
|
||||||
|
|
||||||
|
await ChessSignalR.notifyMoveMade(moveDto, moveResult);
|
||||||
|
this.alertGameStatusChange(moveResult);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
alert("❌ " + error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
alertGameStatusChange(moveResult) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
let gameOver = false;
|
||||||
|
|
||||||
|
if (moveResult.isCheck) {
|
||||||
|
console.log("🛑 Check!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moveResult.isCheckmate) {
|
||||||
|
alert("♟️ Checkmate!");
|
||||||
|
gameOver = true;
|
||||||
|
} else if (moveResult.isStalemate) {
|
||||||
|
alert("🤝 Stalemate!");
|
||||||
|
gameOver = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameOver) {
|
||||||
|
await ChessSignalR.leaveGame();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("ChessAPI.js loaded");
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
const ChessBoard = {
|
||||||
|
renderPieces(pieces) {
|
||||||
|
this.clearAllSquares();
|
||||||
|
this.renderCoordinateLabels();
|
||||||
|
this.placePieces(pieces);
|
||||||
|
this.setupSquareEventHandlers();
|
||||||
|
this.highlightPreviousMove();
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAllSquares() {
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const square = document.getElementById(`square-${i}`);
|
||||||
|
square.innerHTML = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
placePieces(pieces) {
|
||||||
|
pieces.forEach(piece => {
|
||||||
|
const [r, c] = ChessUtils.flipCoordinates(piece.row, piece.col);
|
||||||
|
const index = r * 8 + c;
|
||||||
|
const square = document.getElementById(`square-${index}`);
|
||||||
|
if (!square) return;
|
||||||
|
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = ChessUtils.getPieceImageUrl(piece);
|
||||||
|
img.alt = piece.type;
|
||||||
|
img.classList.add("chessPiece");
|
||||||
|
|
||||||
|
this.setupPieceDragEvents(img, piece);
|
||||||
|
this.setupPieceClickEvent(img, piece);
|
||||||
|
|
||||||
|
square.appendChild(img);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupPieceDragEvents(img, piece) {
|
||||||
|
img.draggable = true;
|
||||||
|
img.ondragstart = async (e) => {
|
||||||
|
GameState.setSelectedPiece(piece);
|
||||||
|
try {
|
||||||
|
const moves = await ChessAPI.getLegalMoves(piece.id);
|
||||||
|
GameState.setLegalMoves(moves);
|
||||||
|
ChessInteractions.highlightSelected(piece.row, piece.col);
|
||||||
|
ChessInteractions.highlightLegalMoves(moves);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.dataTransfer.setData("text/plain", JSON.stringify({
|
||||||
|
srcRow: piece.Row,
|
||||||
|
srcCol: piece.Col
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
img.ondragend = () => ChessInteractions.clearHighlights();
|
||||||
|
},
|
||||||
|
|
||||||
|
setupPieceClickEvent(img, piece) {
|
||||||
|
img.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
ChessInteractions.handlePieceClick(piece);
|
||||||
|
console.log("Clicked on:", piece);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
setupSquareEventHandlers() {
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const square = document.getElementById(`square-${i}`);
|
||||||
|
square.classList.remove("previous-start", "previous-end");
|
||||||
|
|
||||||
|
if (!square.classList.contains("legal")) {
|
||||||
|
square.onclick = () => ChessInteractions.clearHighlights();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightPreviousMove() {
|
||||||
|
if (GameState.previousMoveStart && GameState.previousMoveEnd) {
|
||||||
|
const [startRow, startCol] = ChessUtils.flipCoordinates(...GameState.previousMoveStart);
|
||||||
|
const [endRow, endCol] = ChessUtils.flipCoordinates(...GameState.previousMoveEnd);
|
||||||
|
|
||||||
|
const startIndex = startRow * 8 + startCol;
|
||||||
|
const endIndex = endRow * 8 + endCol;
|
||||||
|
|
||||||
|
document.getElementById(`square-${startIndex}`)?.classList.add("previous-start");
|
||||||
|
document.getElementById(`square-${endIndex}`)?.classList.add("previous-end");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupDragAndDrop() {
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const square = document.getElementById(`square-${i}`);
|
||||||
|
|
||||||
|
square.ondragover = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
square.ondrop = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!GameState.selectedPiece) return;
|
||||||
|
|
||||||
|
const index = parseInt(square.id.split('-')[1], 10);
|
||||||
|
const targetRow = Math.floor(index / 8);
|
||||||
|
const targetCol = index % 8;
|
||||||
|
|
||||||
|
ChessAPI.handleMove(targetRow, targetCol);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCoordinateLabels() {
|
||||||
|
const files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
|
||||||
|
const ranks = ['8', '7', '6', '5', '4', '3', '2', '1'];
|
||||||
|
|
||||||
|
// If player is black, reverse the coordinates
|
||||||
|
if (!GameState.currentPlayerIsWhite) {
|
||||||
|
files.reverse();
|
||||||
|
ranks.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const square = document.getElementById(`square-${i}`);
|
||||||
|
const row = Math.floor(i / 8);
|
||||||
|
const col = i % 8;
|
||||||
|
|
||||||
|
// Add rank label (1-8) on the leftmost column
|
||||||
|
if (col === 0) {
|
||||||
|
const rankLabel = document.createElement('span');
|
||||||
|
rankLabel.className = 'coordinate-label row-label';
|
||||||
|
rankLabel.textContent = ranks[row];
|
||||||
|
square.appendChild(rankLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file label (a-h) on the bottom row
|
||||||
|
if (row === 7) {
|
||||||
|
const fileLabel = document.createElement('span');
|
||||||
|
fileLabel.className = 'coordinate-label col-label';
|
||||||
|
fileLabel.textContent = files[col];
|
||||||
|
square.appendChild(fileLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("ChessBoard.js loaded");
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
const ChessInteractions = {
|
||||||
|
async handlePieceClick(piece) {
|
||||||
|
this.clearHighlights();
|
||||||
|
GameState.setSelectedPiece(piece);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const moves = await ChessAPI.getLegalMoves(piece.id);
|
||||||
|
GameState.setLegalMoves(moves);
|
||||||
|
this.highlightSelected(piece.row, piece.col);
|
||||||
|
this.highlightLegalMoves(moves);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in handlePieceClick:", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightSelected(row, col) {
|
||||||
|
const [r, c] = ChessUtils.flipCoordinates(row, col);
|
||||||
|
const index = r * 8 + c;
|
||||||
|
document.getElementById(`square-${index}`).classList.add("selected");
|
||||||
|
},
|
||||||
|
|
||||||
|
highlightLegalMoves(moves) {
|
||||||
|
moves.forEach(move => {
|
||||||
|
const [r, c] = ChessUtils.flipCoordinates(move.row, move.col);
|
||||||
|
const index = r * 8 + c;
|
||||||
|
const square = document.getElementById(`square-${index}`);
|
||||||
|
square.classList.add("legal");
|
||||||
|
|
||||||
|
const img = square.querySelector("img");
|
||||||
|
if (img) img.onclick = null;
|
||||||
|
|
||||||
|
let overlay = document.createElement("div");
|
||||||
|
overlay.className = "legalOverlay";
|
||||||
|
square.appendChild(overlay);
|
||||||
|
|
||||||
|
square.onclick = () => ChessAPI.handleMove(r, c);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
clearHighlights() {
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const square = document.getElementById(`square-${i}`);
|
||||||
|
|
||||||
|
const overlay = square.querySelector(".legalOverlay");
|
||||||
|
if (overlay) square.removeChild(overlay);
|
||||||
|
|
||||||
|
if (square.classList.contains("legal")) {
|
||||||
|
square.classList.remove("legal");
|
||||||
|
square.onclick = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
square.classList.remove("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState.clearSelection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("ChessInteractions.js loaded");
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const ChessModals = {
|
||||||
|
promptPromotion() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let color = 1;
|
||||||
|
if (GameState.currentPlayerIsWhite)
|
||||||
|
color = 0;
|
||||||
|
|
||||||
|
ChessUtils.updatePromotionModalImages(color);
|
||||||
|
|
||||||
|
document.getElementById("promotionModal").style.display = "block";
|
||||||
|
window.selectPromotion = (piece) => {
|
||||||
|
document.getElementById("promotionModal").style.display = "none";
|
||||||
|
resolve(piece);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
promptDifficulty() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
document.getElementById("difficultyModal").style.display = "block";
|
||||||
|
window.selectDifficulty = (difficulty) => {
|
||||||
|
document.getElementById("difficultyModal").style.display = "none";
|
||||||
|
resolve(difficulty);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("ChessModals.js loaded");
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
const ChessSignalR = {
|
||||||
|
connection: null,
|
||||||
|
|
||||||
|
async setupConnection() {
|
||||||
|
this.connection = new signalR.HubConnectionBuilder()
|
||||||
|
.withUrl("/chessHub")
|
||||||
|
.configureLogging(signalR.LogLevel.Information)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
this.connection.onclose(err => {
|
||||||
|
console.error("❌ SignalR connection closed:", err?.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.connection.on("ReceiveMoveUpdate", async (gameId, moveDto, moveResultDto) => {
|
||||||
|
await this.handleMoveUpdate(gameId, moveDto, moveResultDto);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.connection.start();
|
||||||
|
console.log("✅ SignalR connected");
|
||||||
|
await this.connection.invoke("JoinWebsocketGroup", GameState.currentGameId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ SignalR failed to start or join:", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleMoveUpdate(gameId, moveDto, moveResultDto) {
|
||||||
|
if (gameId !== GameState.currentGameId) return;
|
||||||
|
|
||||||
|
const gameState = await ChessAPI.getGameState(gameId);
|
||||||
|
GameState.setPreviousMove([moveDto.sourceRow, moveDto.sourceCol], [moveDto.targetRow, moveDto.targetCol]);
|
||||||
|
ChessBoard.renderPieces(gameState.pieces);
|
||||||
|
|
||||||
|
ChessAPI.alertGameStatusChange(moveResultDto);
|
||||||
|
},
|
||||||
|
|
||||||
|
async notifyMoveMade(moveDto, moveResult) {
|
||||||
|
if (this.connection) {
|
||||||
|
await this.connection.invoke("MoveMade", GameState.currentGameId, moveDto, moveResult);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async leaveGame() {
|
||||||
|
if (this.connection) {
|
||||||
|
await this.connection.invoke("LeaveWebsocketGroup", GameState.currentGameId);
|
||||||
|
await this.connection.stop();
|
||||||
|
this.connection = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async stopConnection() {
|
||||||
|
if (this.connection) {
|
||||||
|
await this.connection.stop();
|
||||||
|
this.connection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("ChessSignalR.js loaded");
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const ChessUtils = {
|
||||||
|
flipCoordinates(row, col) {
|
||||||
|
if (!GameState.currentPlayerIsWhite) {
|
||||||
|
return [7 - row, 7 - col];
|
||||||
|
}
|
||||||
|
return [row, col];
|
||||||
|
},
|
||||||
|
|
||||||
|
getCookie(name) {
|
||||||
|
const value = document.cookie.split('; ')
|
||||||
|
.find(row => row.startsWith(name + '='));
|
||||||
|
return value ? value.split('=')[1] : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
setCookie(name, value, maxAge = 86400) {
|
||||||
|
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPieceImageUrl(piece) {
|
||||||
|
const basePath = "/images/Chess Images/";
|
||||||
|
const color = piece.color === 0 ? "White" : "Black";
|
||||||
|
const typeMap = {
|
||||||
|
0: "Pawn",
|
||||||
|
1: "Rook",
|
||||||
|
2: "Knight",
|
||||||
|
3: "Bishop",
|
||||||
|
4: "Queen",
|
||||||
|
5: "King"
|
||||||
|
};
|
||||||
|
|
||||||
|
return basePath + color + typeMap[piece.type] + ".svg";
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePromotionModalImages(color) {
|
||||||
|
const pieceNames = ["Queen", "Rook", "Bishop", "Knight"];
|
||||||
|
const buttons = document.querySelectorAll("#promotionModal button img");
|
||||||
|
|
||||||
|
buttons.forEach((img, index) => {
|
||||||
|
img.src = `/images/Chess Images/${color === 0 ? "White" : "Black"}${pieceNames[index]}.svg`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("ChessUtils.js loaded");
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
const GameState = {
|
||||||
|
currentGameId: null,
|
||||||
|
currentPlayerId: null,
|
||||||
|
currentPlayerIsWhite: null,
|
||||||
|
selectedPiece: null,
|
||||||
|
legalMoves: [],
|
||||||
|
previousMoveStart: null,
|
||||||
|
previousMoveEnd: null,
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.currentGameId = null;
|
||||||
|
this.currentPlayerId = null;
|
||||||
|
this.currentPlayerIsWhite = null;
|
||||||
|
this.selectedPiece = null;
|
||||||
|
this.legalMoves = [];
|
||||||
|
this.previousMoveStart = null;
|
||||||
|
this.previousMoveEnd = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
setGameInfo(gameId, playerId, isWhite) {
|
||||||
|
this.currentGameId = gameId;
|
||||||
|
this.currentPlayerId = playerId;
|
||||||
|
this.currentPlayerIsWhite = isWhite;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelectedPiece(piece) {
|
||||||
|
this.selectedPiece = piece;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSelection() {
|
||||||
|
this.selectedPiece = null;
|
||||||
|
this.legalMoves = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
setLegalMoves(moves) {
|
||||||
|
this.legalMoves = moves;
|
||||||
|
},
|
||||||
|
|
||||||
|
setPreviousMove(startPos, endPos) {
|
||||||
|
this.previousMoveStart = startPos;
|
||||||
|
this.previousMoveEnd = endPos;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearPreviousMove() {
|
||||||
|
this.previousMoveStart = null;
|
||||||
|
this.previousMoveEnd = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("GameState.js loaded");
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
async function startNewGame() {
|
||||||
|
await ChessSignalR.stopConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gameData = await ChessAPI.joinGame();
|
||||||
|
|
||||||
|
GameState.setGameInfo(gameData.gameId, gameData.id, gameData.isWhite);
|
||||||
|
GameState.clearPreviousMove();
|
||||||
|
|
||||||
|
ChessUtils.setCookie('chessGameId', gameData.gameId);
|
||||||
|
ChessUtils.setCookie('chessPlayerId', gameData.id);
|
||||||
|
ChessUtils.setCookie('chessPlayerIsWhite', gameData.isWhite);
|
||||||
|
|
||||||
|
console.log("🆕 Game started:", gameData.gameId);
|
||||||
|
|
||||||
|
await ChessSignalR.setupConnection();
|
||||||
|
|
||||||
|
const gameState = await ChessAPI.getGameState(gameData.gameId);
|
||||||
|
ChessBoard.renderPieces(gameState.pieces);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start new game:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startCPUGame() {
|
||||||
|
await ChessSignalR.stopConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const difficulty = await ChessModals.promptDifficulty();
|
||||||
|
const gameData = await ChessAPI.createCPUGame(difficulty);
|
||||||
|
|
||||||
|
GameState.setGameInfo(gameData.gameId, gameData.id, gameData.isWhite);
|
||||||
|
GameState.clearPreviousMove();
|
||||||
|
|
||||||
|
ChessUtils.setCookie('chessGameId', gameData.gameId);
|
||||||
|
ChessUtils.setCookie('chessPlayerId', gameData.id);
|
||||||
|
ChessUtils.setCookie('chessPlayerIsWhite', gameData.isWhite);
|
||||||
|
|
||||||
|
console.log("🆕 CPU Game started:", gameData.gameId);
|
||||||
|
|
||||||
|
await ChessSignalR.setupConnection();
|
||||||
|
|
||||||
|
const gameState = await ChessAPI.getGameState(gameData.gameId);
|
||||||
|
ChessBoard.renderPieces(gameState.pieces);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to start CPU game:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeSavedGame() {
|
||||||
|
const savedGameId = ChessUtils.getCookie("chessGameId");
|
||||||
|
const savedPlayerId = ChessUtils.getCookie("chessPlayerId");
|
||||||
|
const savedPlayerIsWhite = ChessUtils.getCookie("chessPlayerIsWhite");
|
||||||
|
|
||||||
|
if (savedGameId && savedPlayerId) {
|
||||||
|
try {
|
||||||
|
const gameState = await ChessAPI.getGameState(savedGameId);
|
||||||
|
|
||||||
|
console.log("🧠 Rejoining saved game...");
|
||||||
|
GameState.setGameInfo(savedGameId, savedPlayerId, savedPlayerIsWhite === "true");
|
||||||
|
|
||||||
|
await ChessSignalR.setupConnection();
|
||||||
|
ChessBoard.renderPieces(gameState.pieces);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Saved game not found or expired.", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.startNewGame = startNewGame;
|
||||||
|
window.startCPUGame = startCPUGame;
|
||||||
|
|
||||||
|
window.addEventListener('load', resumeSavedGame);
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
ChessBoard.setupDragAndDrop();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("chessMain.js loaded");
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
window.onload = function () {
|
||||||
|
const element = document.getElementById("WelcomeMessage");
|
||||||
|
element.style.display = "none"; // Trigger reflow
|
||||||
|
element.offsetHeight; // Force reflow
|
||||||
|
element.style.display = ""; // Restore the original display
|
||||||
|
typeText("#WelcomeMessage", "Today, October 2nd, marks the 5th anniversary of the day I was sealed for time and all eternity to my wonderful wife. She's the kindest, sweetest, funniest, most beautiful woman I've ever met. I love you Morgan 💖");
|
||||||
|
}
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
element.style.display = "none"; // Trigger reflow
|
element.style.display = "none"; // Trigger reflow
|
||||||
element.offsetHeight; // Force reflow
|
element.offsetHeight; // Force reflow
|
||||||
element.style.display = ""; // Restore the original display
|
element.style.display = ""; // Restore the original display
|
||||||
typeText("#WelcomeMessage", "Hey! You found me! Since I have your attention, why don't I tell you about myself?");
|
typeText("#WelcomeMessage", "Hey, you found me...!?``````````````````````No that's stupid...```````````````````Just...```````uhhhh...````````Hello! My name is Josh. I'm a software engineer, and I try to have a lot of fun with what I do. While I have your attention, why don't I tell you more about myself?");
|
||||||
}
|
}
|
||||||
@@ -12,23 +12,27 @@
|
|||||||
let intervalId;
|
let intervalId;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const punctuation = ['.', '!', '?'];
|
const punctuation = ['.', '!', '?'];
|
||||||
|
const punctuationDict = {
|
||||||
|
'.': 150,
|
||||||
|
'!': 200,
|
||||||
|
'?': 200,
|
||||||
|
',': 100,
|
||||||
|
'`': 25,
|
||||||
|
};
|
||||||
|
|
||||||
let currentText = "";
|
let currentText = "";
|
||||||
|
|
||||||
function changeIntervalTime(element, text) {
|
function changeIntervalTime(element, text) {
|
||||||
if (punctuation.includes(text.charAt(index))) {
|
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
simulateTyping(element, text, 200);
|
simulateTyping(element, text, punctuationDict[text.charAt(index)] ?? 50);
|
||||||
}
|
|
||||||
else {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
simulateTyping(element, text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function simulateTyping(element, text, speed = 50) {
|
function simulateTyping(element, text, speed = 50) {
|
||||||
intervalId = setInterval(() => {
|
intervalId = setInterval(() => {
|
||||||
if (index < text.length) {
|
if (index < text.length) {
|
||||||
|
if (text.charAt(index) === '`')
|
||||||
|
currentText = currentText.slice(0, -1);
|
||||||
|
else
|
||||||
currentText += text.charAt(index);
|
currentText += text.charAt(index);
|
||||||
changeIntervalTime(element, text);
|
changeIntervalTime(element, text);
|
||||||
index++;
|
index++;
|
||||||
|
|||||||
Reference in New Issue
Block a user