Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34de3c1abc | ||
|
|
311d62a719 | ||
|
|
4217bf3703 | ||
|
|
fd68ada82e | ||
|
|
7c77cd0e52 | ||
|
|
93bdb3518a | ||
|
|
69e1e8450d | ||
|
|
e74de40d2e | ||
|
|
9b86bfafe0 |
@@ -11,6 +11,19 @@ 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()
|
||||
{
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ChessController(
|
||||
private static readonly ConcurrentDictionary<Guid, CancellationTokenSource> _gameRemovalCancellationTokens = [];
|
||||
|
||||
private static readonly TimeSpan _computerGameTimeout = TimeSpan.FromHours(1);
|
||||
private static readonly TimeSpan _multiplayerGameTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan _multiplayerGameTimeout = TimeSpan.FromDays(1);
|
||||
private static readonly TimeSpan _gameCleanupTimeout = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
@@ -165,7 +165,7 @@ public class ChessController(
|
||||
var expectedPlayerId = isWhiteMove ? gameState.WhitePlayerId : gameState.BlackPlayerId;
|
||||
|
||||
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
|
||||
var piece = gameState.Pieces.FirstOrDefault(p => p.Id == moveDto.PieceId);
|
||||
@@ -174,7 +174,7 @@ public class ChessController(
|
||||
return NotFound("Chess piece Id does not exist");
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>53ed685c-bdff-4306-8cc2-9fbe55c85713</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
<div id="ProjectsBox">
|
||||
<div id="ChessProject" class="projectContainer">
|
||||
<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"/>
|
||||
</a>
|
||||
</div>
|
||||
<div id="ChessText" class="projectTextRight">
|
||||
<h2 id="ChessTitle" class="projectHeaderRight projectHeader">Chess</h2>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,31 +8,23 @@ public class AutoIpUpdateService(
|
||||
ILogger<AutoIpUpdateService> log)
|
||||
: BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(5);
|
||||
private static readonly HttpClient httpClient = new();
|
||||
public static bool IsEnabled { get; private set; } = false;
|
||||
|
||||
private static readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1);
|
||||
private static readonly HttpClient _httpClient = new();
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stop)
|
||||
{
|
||||
IsEnabled = true;
|
||||
var timer = new PeriodicTimer(CheckInterval);
|
||||
AAAARecord dnsRecord = await GetDnsRecordAsync();
|
||||
string lastKnownIp = dnsRecord.Content;
|
||||
var timer = new PeriodicTimer(_checkInterval);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stop))
|
||||
{
|
||||
try
|
||||
{
|
||||
string currentIp = await GetPublicIpAsync() ?? "";
|
||||
|
||||
if (lastKnownIp != currentIp)
|
||||
{
|
||||
await UpdateDnsIpAsync(config, dnsRecord, currentIp);
|
||||
|
||||
lastKnownIp = currentIp;
|
||||
await UpdateIpAddressIfChanged();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { /* shutting down */ }
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
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()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await httpClient.GetStringAsync(@"https://api.ipify.org/");
|
||||
return await _httpClient.GetStringAsync(@"https://api.ipify.org/");
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -55,7 +69,7 @@ public class AutoIpUpdateService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AAAARecord> GetDnsRecordAsync()
|
||||
private async Task<RecordList> GetDnsRecordAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -64,8 +78,8 @@ public class AutoIpUpdateService(
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Key", config["cfKey"]);
|
||||
var result = await cfClient.GetAsync(@$"https://api.cloudflare.com/client/v4/zones/{config["zoneId"]}/dns_records");
|
||||
Console.WriteLine(await result.Content.ReadAsStringAsync());
|
||||
var records = System.Text.Json.JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
|
||||
return records!.Result[0];
|
||||
var records = JsonSerializer.Deserialize<RecordList>(await result.Content.ReadAsStringAsync());
|
||||
return records!;
|
||||
}
|
||||
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();
|
||||
cfClient.DefaultRequestHeaders.Add("X-Auth-Email", config["cfEmail"]);
|
||||
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,
|
||||
name = "@",
|
||||
proxied = true,
|
||||
ttl = 3600,
|
||||
type = "AAAA"
|
||||
proxied = record.Proxied,
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -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("content")] string Content,
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("proxied")] bool Proxied,
|
||||
[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);
|
||||
|
||||
if (piece == null) return [];
|
||||
if (piece.Color != gameState.CurrentPlayer) return [];
|
||||
|
||||
var candidateMoves = GenerateCandidateMoves(gameState, piece);
|
||||
var legalMoves = new List<Position>();
|
||||
@@ -529,8 +528,15 @@ public class ChessService : IChessService
|
||||
var ep = gs.EnPassantTarget.Value;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return moves;
|
||||
}
|
||||
@@ -542,11 +548,7 @@ public class ChessService : IChessService
|
||||
=> GenerateSlidingMoves(gs, piece, [(1, 1), (1, -1), (-1, 1), (-1, -1)]);
|
||||
|
||||
private static List<Position> GenerateQueenMoves(GameState gs, ChessPiece piece)
|
||||
=> GenerateSlidingMoves(gs, piece,
|
||||
[
|
||||
(1, 0), (-1, 0), (0, 1), (0, -1),
|
||||
(1, 1), (1, -1), (-1, 1), (-1, -1)
|
||||
]);
|
||||
=> [..GenerateRookMoves(gs, piece), ..GenerateBishopMoves(gs, piece)];
|
||||
|
||||
private static List<Position> GenerateSlidingMoves(GameState gs, ChessPiece piece, (int dr, int dc)[] directions)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#chessBoard {
|
||||
#boardContainer {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
margin: 2vw auto;
|
||||
}
|
||||
|
||||
#chessBoard {
|
||||
width: 60vw;
|
||||
height: 60vw;
|
||||
display: grid;
|
||||
@@ -159,3 +164,29 @@
|
||||
height: 50px;
|
||||
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;
|
||||
}
|
||||
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 |
@@ -27,9 +27,14 @@ const ChessAPI = {
|
||||
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 (!response.ok || !result.success) {
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "Invalid move or not your turn.");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const ChessBoard = {
|
||||
renderPieces(pieces) {
|
||||
this.clearAllSquares();
|
||||
this.renderCoordinateLabels();
|
||||
this.placePieces(pieces);
|
||||
this.setupSquareEventHandlers();
|
||||
this.highlightPreviousMove();
|
||||
@@ -106,6 +107,39 @@ const ChessBoard = {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
element.style.display = "none"; // Trigger reflow
|
||||
element.offsetHeight; // Force reflow
|
||||
element.style.display = ""; // Restore the original display
|
||||
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?");
|
||||
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,18 +12,19 @@
|
||||
let intervalId;
|
||||
let index = 0;
|
||||
const punctuation = ['.', '!', '?'];
|
||||
const punctuationDict = {
|
||||
'.': 150,
|
||||
'!': 200,
|
||||
'?': 200,
|
||||
',': 100,
|
||||
'`': 25,
|
||||
};
|
||||
|
||||
let currentText = "";
|
||||
|
||||
function changeIntervalTime(element, text) {
|
||||
if (punctuation.includes(text.charAt(index))) {
|
||||
clearInterval(intervalId);
|
||||
simulateTyping(element, text, 200);
|
||||
}
|
||||
else {
|
||||
clearInterval(intervalId);
|
||||
simulateTyping(element, text);
|
||||
}
|
||||
simulateTyping(element, text, punctuationDict[text.charAt(index)] ?? 50);
|
||||
}
|
||||
|
||||
function simulateTyping(element, text, speed = 50) {
|
||||
|
||||
Reference in New Issue
Block a user