Refactor project structure and update documentation. Replace PixiJS with plain DOM for UI rendering, enhance README with game features, and revise protocol documentation for HTTP API. Remove unused files and streamline client code for better maintainability.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
using HSchool.Server.Game;
|
||||
using HSchool.Simulation;
|
||||
|
||||
namespace HSchool.Server.Api;
|
||||
|
||||
/// <summary>
|
||||
/// The main menu talks to these: list, create, delete. Everything that mutates state is handed to
|
||||
/// the loop thread as a command and awaited, so schools stay single-threaded.
|
||||
/// </summary>
|
||||
internal static class SchoolEndpoints
|
||||
{
|
||||
/// <summary>How long a request waits for the loop thread before giving up.</summary>
|
||||
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
public static void MapSchoolEndpoints(this IEndpointRouteBuilder builder)
|
||||
{
|
||||
var schools = builder.MapGroup("/api/schools");
|
||||
|
||||
schools.MapGet("/", (GameLoopService loop) =>
|
||||
{
|
||||
var state = loop.SchoolsState;
|
||||
var options = loop.Options;
|
||||
|
||||
return new SchoolsResponse(
|
||||
state.MaxSchools,
|
||||
options.DefaultStartDate,
|
||||
options.GameMinutesPerRealSecond,
|
||||
[.. state.Schools.Select(SchoolResponse.From)]);
|
||||
})
|
||||
.WithName("GetSchools");
|
||||
|
||||
schools.MapGet("/random-name", async (GameCommandQueue commands, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var command = new GameCommand.SuggestName(NewCompletion<string>());
|
||||
commands.Enqueue(command);
|
||||
|
||||
var name = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken);
|
||||
return new RandomNameResponse(name);
|
||||
})
|
||||
.WithName("GetRandomSchoolName");
|
||||
|
||||
schools.MapPost("/", async (
|
||||
CreateSchoolRequest request,
|
||||
GameCommandQueue commands,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var command = new GameCommand.CreateSchool(
|
||||
request.Name ?? string.Empty,
|
||||
DateTime.SpecifyKind(request.StartDate, DateTimeKind.Utc),
|
||||
NewCompletion<SchoolCreationOutcome>());
|
||||
commands.Enqueue(command);
|
||||
|
||||
var outcome = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken);
|
||||
|
||||
return outcome.Error switch
|
||||
{
|
||||
SchoolCreationError.None =>
|
||||
Results.Created($"/api/schools/{outcome.School!.Id}", SchoolResponse.From(outcome.School)),
|
||||
SchoolCreationError.LimitReached =>
|
||||
Problem(StatusCodes.Status409Conflict, "school-limit-reached", "The school limit is already reached."),
|
||||
SchoolCreationError.InvalidName =>
|
||||
Problem(StatusCodes.Status400BadRequest, "invalid-name", $"A name must be 1 to {School.MaxNameLength} characters."),
|
||||
SchoolCreationError.InvalidStartDate =>
|
||||
Problem(StatusCodes.Status400BadRequest, "invalid-start-date", "The start date is outside the supported range."),
|
||||
_ => Results.Problem("Unknown error."),
|
||||
};
|
||||
})
|
||||
.WithName("CreateSchool");
|
||||
|
||||
schools.MapDelete("/{id:int}", async (
|
||||
int id,
|
||||
GameCommandQueue commands,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var command = new GameCommand.DeleteSchool(id, NewCompletion<bool>());
|
||||
commands.Enqueue(command);
|
||||
|
||||
var deleted = await command.Result.Task.WaitAsync(CommandTimeout, cancellationToken);
|
||||
return deleted ? Results.NoContent() : Results.NotFound();
|
||||
})
|
||||
.WithName("DeleteSchool");
|
||||
}
|
||||
|
||||
/// <summary>The loop thread must never be blocked by a continuation of a waiting request.</summary>
|
||||
private static TaskCompletionSource<T> NewCompletion<T>() =>
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
private static IResult Problem(int statusCode, string code, string detail) =>
|
||||
Results.Problem(detail: detail, statusCode: statusCode, title: code, extensions: new Dictionary<string, object?>
|
||||
{
|
||||
["code"] = code,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Body of <c>POST /api/schools</c>. The start date is a game calendar date, not a real one.</summary>
|
||||
internal sealed record CreateSchoolRequest(string? Name, DateTime StartDate);
|
||||
|
||||
internal sealed record SchoolResponse(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex)
|
||||
{
|
||||
public static SchoolResponse From(SchoolState school) =>
|
||||
new(school.Id, school.Name, school.GameTime, school.Running, school.SpeedIndex);
|
||||
}
|
||||
|
||||
/// <summary>Everything the main menu needs in one request.</summary>
|
||||
internal sealed record SchoolsResponse(
|
||||
int MaxSchools,
|
||||
DateTime DefaultStartDate,
|
||||
double GameMinutesPerRealSecond,
|
||||
IReadOnlyList<SchoolResponse> Schools);
|
||||
|
||||
internal sealed record RandomNameResponse(string Name);
|
||||
Reference in New Issue
Block a user