From 17033ac81f51cf9de0d92492a6984e0a33126793 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 6 Jun 2023 03:25:39 -0500 Subject: [PATCH] a lot of stuff --- .editorconfig | 4 + Squiddler-Server-Lite.sln | 5 + Squiddler-Server-Lite/Api.cs | 101 ++++++++++++++++++ .../Middleware/RequireGameCode.cs | 16 +++ Squiddler-Server-Lite/Program.cs | 87 ++++++++++++++- .../Squiddler-Server-Lite.csproj | 4 + .../StateObjects/GameState.cs | 89 +++++++++++++++ .../StateObjects/PlayAreaState.cs | 17 +++ .../StateObjects/UserState.cs | 19 ++++ Squiddler-Server-Lite/TransferObjects/Card.cs | 38 +++++++ .../TransferObjects/GameCode.cs | 71 ++++++++++++ .../TransferObjects/NewGameConfiguration.cs | 36 +++++++ .../TransferObjects/PlayedCards.cs | 32 ++++++ 13 files changed, 516 insertions(+), 3 deletions(-) create mode 100644 .editorconfig create mode 100644 Squiddler-Server-Lite/Api.cs create mode 100644 Squiddler-Server-Lite/Middleware/RequireGameCode.cs create mode 100644 Squiddler-Server-Lite/StateObjects/GameState.cs create mode 100644 Squiddler-Server-Lite/StateObjects/PlayAreaState.cs create mode 100644 Squiddler-Server-Lite/StateObjects/UserState.cs create mode 100644 Squiddler-Server-Lite/TransferObjects/Card.cs create mode 100644 Squiddler-Server-Lite/TransferObjects/GameCode.cs create mode 100644 Squiddler-Server-Lite/TransferObjects/NewGameConfiguration.cs create mode 100644 Squiddler-Server-Lite/TransferObjects/PlayedCards.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..abb819f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# IDE1006: Naming Styles +dotnet_diagnostic.IDE1006.severity = none diff --git a/Squiddler-Server-Lite.sln b/Squiddler-Server-Lite.sln index 03cda07..c99ed47 100644 --- a/Squiddler-Server-Lite.sln +++ b/Squiddler-Server-Lite.sln @@ -5,6 +5,11 @@ VisualStudioVersion = 17.6.33723.286 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squiddler-Server-Lite", "Squiddler-Server-Lite\Squiddler-Server-Lite.csproj", "{89A88EA7-771B-4E74-B662-C13363C4C140}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B32566A0-4B05-4EB6-8BB8-B98E7DA76B25}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Squiddler-Server-Lite/Api.cs b/Squiddler-Server-Lite/Api.cs new file mode 100644 index 0000000..0141549 --- /dev/null +++ b/Squiddler-Server-Lite/Api.cs @@ -0,0 +1,101 @@ +using Microsoft.AspNetCore.Mvc; +using Squiddler_Server_Lite.StateObjects; +using Squiddler_Server_Lite.TransferObjects; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; + +namespace Squiddler_Server_Lite +{ + internal static class Api + { + internal static async Task CreateGame([FromBody] [Required] NewGameConfiguration config) + { + if (!config.Validate()) + return TypedResults.BadRequest("Invalid configuration"); + var gameState = GameState.Create(config); + return TypedResults.Ok(gameState.gameCode); + } + internal static async Task DrawCard(HttpContext context) + { + var gameCode = GetGameCode(context.Request); + var playerName = GetPlayerName(context.Request); + if (gameCode is null) + return TypedResults.BadRequest("No/Invalid game code"); + if (playerName is null) + return TypedResults.BadRequest("No/Invalid player name"); + return TypedResults.Ok(new Card("A", 2)); + throw new NotImplementedException(); + } + internal static async Task GetTestCard(string text, int value) + => TypedResults.Ok(new Card(text, value)); + internal static async Task JoinGame(HttpContext context) + { + var gameCode = GetGameCode(context.Request); + var playerName = GetPlayerName(context.Request); + if (gameCode is null) + return TypedResults.BadRequest("No/Invalid game code"); + if (playerName is null) + return TypedResults.BadRequest("No/Invalid player name"); + var gameState = GameState.FindGame(gameCode); + if (gameState is null) + return TypedResults.BadRequest("Game code not found"); + switch (gameState.JoinGame(playerName)) + { + case GameState.JoinGameResult.Success: + return TypedResults.Ok(); + case GameState.JoinGameResult.AlreadyJoined: + return TypedResults.Ok(); + case GameState.JoinGameResult.Full: + return TypedResults.Conflict("Game is full"); + default: + throw new InvalidOperationException(); + } + } + internal static async Task PlayCards(HttpContext context, + [FromBody] [Required] PlayedCards played) + { + var gameCode = GetGameCode(context.Request); + var playerName = GetPlayerName(context.Request); + if (gameCode is null) + return TypedResults.BadRequest("No/Invalid game code"); + if (playerName is null) + return TypedResults.BadRequest("No/Invalid player name"); + var gameState = GameState.FindGame(gameCode); + if (gameState is null) + return TypedResults.BadRequest("Game code not found"); + var player = gameState.ActivePlayer(); + if (player.userName != playerName) + return TypedResults.BadRequest("Not the active player"); + if (!gameState.playerDrew) + { + var drawnCard = gameState.PlayerTakesFromDiscard(); + if (!gameState.ValidatePlayedCards(played)) + { + gameState.PlayerDiscard(drawnCard); + return TypedResults.BadRequest("Cards don't match player hand"); + } + throw new NotImplementedException(); + } + throw new NotImplementedException(); + } + private static GameCode? GetGameCode(HttpRequest request) + { + if (request.Headers.ContainsKey(Headers.GAMECODE)) + if (GameCode.TryParse(request.Headers[Headers.GAMECODE].ToString(), + out GameCode gameCode)) + return gameCode; + return null; + } + private static string? GetPlayerName(HttpRequest request) + { + if (request.Headers.ContainsKey(Headers.PLAYERNAME)) + return request.Headers[Headers.PLAYERNAME].ToString(); + return null; + } + public static class Headers + { + public const string GAMECODE = "GAME-CODE"; + public const string PLAYERNAME = "PLAYER-NAME"; + } + } +} diff --git a/Squiddler-Server-Lite/Middleware/RequireGameCode.cs b/Squiddler-Server-Lite/Middleware/RequireGameCode.cs new file mode 100644 index 0000000..439514a --- /dev/null +++ b/Squiddler-Server-Lite/Middleware/RequireGameCode.cs @@ -0,0 +1,16 @@ +namespace Squiddler_Server_Lite.Middleware +{ + public class RequireGameCode + { + private readonly RequestDelegate _next; + public RequireGameCode(RequestDelegate next) + { + _next = next; + } + public async Task Invoke(HttpContext context) + { + if (context.Request.Headers.ContainsKey(Api.Headers.GAMECODE)) + await _next.Invoke(context); + } + } +} diff --git a/Squiddler-Server-Lite/Program.cs b/Squiddler-Server-Lite/Program.cs index bb04eb2..40c94bb 100644 --- a/Squiddler-Server-Lite/Program.cs +++ b/Squiddler-Server-Lite/Program.cs @@ -1,9 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.OpenApi.Models; +using Squiddler_Server_Lite; +using Squiddler_Server_Lite.Middleware; +using Squiddler_Server_Lite.TransferObjects; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + options.SupportNonNullableReferenceTypes(); + options.MapType(() => new OpenApiSchema + { + Type = "string", + MinLength = 4, + MaxLength = 4, + }); +}); var app = builder.Build(); @@ -15,7 +30,74 @@ if (app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); +app.UseWebSockets(); +var apiPrefix = app.MapGroup("/api"); + +apiPrefix.MapGet("/test/card/{text}/{value}", Api.GetTestCard) +.Produces(200, "application/json") +.WithName("Test_GetCard") +.WithOpenApi(); + +apiPrefix.MapPost("/create_game", Api.CreateGame) +.Produces(200, "text/plain") +.Produces(400, "text/plain") +.WithName("CreateNewGame") +.WithOpenApi(); + +var playPrefix = apiPrefix.MapGroup("/play") +.WithOpenApi(op => +{ + OpenApiOperation newOp = new(op); + newOp.Parameters.Add(new OpenApiParameter + { + Required = true, + In = ParameterLocation.Header, + Style = ParameterStyle.Simple, + AllowEmptyValue = false, + Name = Api.Headers.GAMECODE, + Schema = new OpenApiSchema + { + Type = "string", + MaxLength = 4, + MinLength = 4 + }, + }); + newOp.Parameters.Add(new OpenApiParameter + { + Required = true, + In = ParameterLocation.Header, + Style = ParameterStyle.Simple, + AllowEmptyValue = false, + Name = Api.Headers.PLAYERNAME, + Schema = new OpenApiSchema { Type = "string" }, + }); + return newOp; +}); + +playPrefix.MapPut("/draw_card", (Delegate)Api.DrawCard) +.Produces(200, "application/json") +.Produces(400, "text/plain") +.WithName("DrawCard") +.WithOpenApi(); + +playPrefix.MapPut("/join_game", (Delegate)Api.JoinGame) +.Produces(200) +.Produces(400, "text/plain") +.Produces(409, "text/plain") +.WithName("JoinGame") +.WithOpenApi(); + +playPrefix.MapPut("/play_cards", (Delegate)Api.PlayCards) +.Produces(200) +.Produces(400, "text/plain") +.WithName("PlayCards") +.WithOpenApi(); + + +app.Run(); + +/* var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" @@ -36,9 +118,8 @@ app.MapGet("/weatherforecast", () => .WithName("GetWeatherForecast") .WithOpenApi(); -app.Run(); - internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } +*/ diff --git a/Squiddler-Server-Lite/Squiddler-Server-Lite.csproj b/Squiddler-Server-Lite/Squiddler-Server-Lite.csproj index 699ab8d..832281e 100644 --- a/Squiddler-Server-Lite/Squiddler-Server-Lite.csproj +++ b/Squiddler-Server-Lite/Squiddler-Server-Lite.csproj @@ -7,6 +7,10 @@ Squiddler_Server_Lite + + + + diff --git a/Squiddler-Server-Lite/StateObjects/GameState.cs b/Squiddler-Server-Lite/StateObjects/GameState.cs new file mode 100644 index 0000000..8f968f2 --- /dev/null +++ b/Squiddler-Server-Lite/StateObjects/GameState.cs @@ -0,0 +1,89 @@ +using Squiddler_Server_Lite.TransferObjects; + +namespace Squiddler_Server_Lite.StateObjects +{ + public class GameState + { + public GameCode gameCode { get; set; } + public string gameName { get; set; } + public readonly int FIRST_ROUND; + public readonly int LAST_ROUND; + public int round { get; set; } + public List users { get; set; } + public int activePlayer { get; set; } + public List discard { get; set; } = new(); + //index of player who first went out, otherwise -1 + public int playerWentOut { get; set; } = -1; + #region TURNSTATE + public bool playerDrew { get; set; } + #endregion + public void AddToDiscard(Card card) + { + discard.Add(new Card(card)); + } + public Card PlayerTakesFromDiscard() + { + Card card = discard[discard.Count - 1]; + discard.RemoveAt(discard.Count - 1); + ActivePlayer().hand.Add(card); + return new Card(card); + } + //matches the PlayedCards against the active player's hand + public bool ValidatePlayedCards(PlayedCards playedCards) + => playedCards.AllCards().Order().SequenceEqual(ActivePlayer().hand.Order()); + public JoinGameResult JoinGame(string playerName) + { + if (users.Any(u => u.userName == playerName)) + return JoinGameResult.AlreadyJoined; + //kind of an abuse of Any() to have side effects + return users.Any(u => u.TryJoin(playerName)) + ? JoinGameResult.Success : JoinGameResult.Full; + } + public UserState ActivePlayer() + { + return users[activePlayer]; + } + public bool Started() + { + return round > 0; + } + private GameState(int firstRound, int lastRound) + { + FIRST_ROUND = firstRound; + LAST_ROUND = lastRound; + } + public static GameState Create(NewGameConfiguration config) + { + GameState gameState = new(config.firstRound, config.lastRound) + { + gameCode = GameCode.CreateRandom(), + gameName = config.gameName, + users = Enumerable.Range(0, config.playerCount) + .Select(i => new UserState()).ToList(), + }; + ActiveStates.Add(gameState); + return gameState; + } + private static readonly List ActiveStates = new(); + public static GameState? FindGame(GameCode gameCode) + { + return ActiveStates.FirstOrDefault(g => g.gameCode == gameCode, null); + } + public static bool DeleteGame(GameCode gameCode) + { + return ActiveStates.RemoveAll(g => g.gameCode == gameCode) > 0; + } + + internal void PlayerDiscard(Card drawnCard) + { + throw new NotImplementedException(); + } + + public enum JoinGameResult + { + Success, + Full, + AlreadyJoined, + } + } +} diff --git a/Squiddler-Server-Lite/StateObjects/PlayAreaState.cs b/Squiddler-Server-Lite/StateObjects/PlayAreaState.cs new file mode 100644 index 0000000..0420404 --- /dev/null +++ b/Squiddler-Server-Lite/StateObjects/PlayAreaState.cs @@ -0,0 +1,17 @@ +using Squiddler_Server_Lite.TransferObjects; + +namespace Squiddler_Server_Lite.StateObjects +{ + public class PlayAreaState + { + public readonly List[] rows = + { + new(), new(), new(), new(), new(), + }; + //calculates sum of values of played cards + public int baseScore() + { + return rows.Sum(r => r.Sum(c => c.value)); + } + } +} diff --git a/Squiddler-Server-Lite/StateObjects/UserState.cs b/Squiddler-Server-Lite/StateObjects/UserState.cs new file mode 100644 index 0000000..cf3cfb5 --- /dev/null +++ b/Squiddler-Server-Lite/StateObjects/UserState.cs @@ -0,0 +1,19 @@ +using Squiddler_Server_Lite.TransferObjects; + +namespace Squiddler_Server_Lite.StateObjects +{ + public class UserState + { + public string? userName { get; set; } + public List hand { get; } = new(); + public bool TryJoin(string userName) + { + if (this.userName is null) + { + this.userName = userName; + return true; + } + return false; + } + } +} diff --git a/Squiddler-Server-Lite/TransferObjects/Card.cs b/Squiddler-Server-Lite/TransferObjects/Card.cs new file mode 100644 index 0000000..c48f1af --- /dev/null +++ b/Squiddler-Server-Lite/TransferObjects/Card.cs @@ -0,0 +1,38 @@ +namespace Squiddler_Server_Lite.TransferObjects +{ + public class Card : IComparable + { + public string text { get; set; } + public int value { get; set; } + public override string ToString() + => $"[\"{text}\": {value}"; + public Card(string text, int value) + { + this.text = FixCaps(text) ?? throw new ArgumentNullException(nameof(text)); + this.value = value; + } + public Card(Card card) + { + text = card.text; + value = card.value; + } + //fixes text so first letter is capitalized and the rest are lower + //behavior undefined is text contains characters other than ASCII letters + public static string FixCaps(string text) + { + if (string.IsNullOrWhiteSpace(text)) + return text; + return text[..1].ToUpper() + text[1..].ToLower(); + } + + public int CompareTo(Card? other) + { + if (other is null) + return 1; + var sCmp = text.CompareTo(other.text); + if (sCmp == 0) + return value.CompareTo(other.value); + return sCmp; + } + } +} diff --git a/Squiddler-Server-Lite/TransferObjects/GameCode.cs b/Squiddler-Server-Lite/TransferObjects/GameCode.cs new file mode 100644 index 0000000..4298081 --- /dev/null +++ b/Squiddler-Server-Lite/TransferObjects/GameCode.cs @@ -0,0 +1,71 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Runtime.ExceptionServices; +using System.Runtime.Serialization; + +namespace Squiddler_Server_Lite.TransferObjects +{ + //[KnownType(typeof(GameCode))] + public class GameCode + { + const string ALLOWED_CHARS = "ABCDEFGHIJKLMNPQRSTVWXYZ"; + public const int LENGTH = 4; + [StringLength(LENGTH, MinimumLength = LENGTH)] + public string code { get; set; } + private GameCode(string code) + { + this.code = code ?? throw new ArgumentNullException(nameof(code)); + } + public static GameCode CreateRandom() + { + var stringChars = new char[LENGTH]; + for (int i = 0; i < stringChars.Length; i++) + stringChars[i] = ALLOWED_CHARS[Random.Shared.Next(ALLOWED_CHARS.Length)]; + return new GameCode(new string(stringChars)); + } + public static bool TryParse(string text, out GameCode gameCode) + { + gameCode = new GameCode(text); + return gameCode.Validate(); + } + public static implicit operator string(GameCode gameCode) + => gameCode.code; + public static bool operator ==(GameCode first, GameCode second) + => first.code == second.code; + public static bool operator !=(GameCode first, GameCode second) + => !(first == second); + public bool Validate() + { + return code?.All(ALLOWED_CHARS.Contains) ?? false; + } + public class GameCodeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + if (sourceType == typeof(string)) + return true; + return base.CanConvertFrom(context, sourceType); + } + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is string v) + { + var code = new GameCode(v); + if (code.Validate()) + return code; + } + return base.ConvertFrom(context, culture, value); + } + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (destinationType == typeof(string)) + if (value is GameCode gameCode) + return gameCode.code; + return base.ConvertTo(context, culture, value, destinationType); + } + } + } +} diff --git a/Squiddler-Server-Lite/TransferObjects/NewGameConfiguration.cs b/Squiddler-Server-Lite/TransferObjects/NewGameConfiguration.cs new file mode 100644 index 0000000..c835f74 --- /dev/null +++ b/Squiddler-Server-Lite/TransferObjects/NewGameConfiguration.cs @@ -0,0 +1,36 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Squiddler_Server_Lite.TransferObjects +{ + public class NewGameConfiguration + { + public const int MIN_PLAYERS = 2; + public const int MAX_PLAYERS = 6; + public string gameName { get; set; } + [Range(MIN_PLAYERS, MAX_PLAYERS)] + public int playerCount { get; set; } + [DefaultValue(3)] + public int firstRound { get; set; } + [DefaultValue(10)] + public int lastRound { get; set; } + public NewGameConfiguration(string gameName, int playerCount, + int firstRound = 3, int lastRound = 10) + { + this.gameName = gameName ?? throw new ArgumentNullException(nameof(gameName)); + this.playerCount = playerCount; + this.firstRound = firstRound; + this.lastRound = lastRound; + } + public bool Validate() + { + if (string.IsNullOrWhiteSpace(gameName)) + return false; + if (playerCount > MAX_PLAYERS || playerCount < MIN_PLAYERS) + return false; + if (lastRound < firstRound) + return false; + return true; + } + } +} diff --git a/Squiddler-Server-Lite/TransferObjects/PlayedCards.cs b/Squiddler-Server-Lite/TransferObjects/PlayedCards.cs new file mode 100644 index 0000000..a608c4b --- /dev/null +++ b/Squiddler-Server-Lite/TransferObjects/PlayedCards.cs @@ -0,0 +1,32 @@ +namespace Squiddler_Server_Lite.TransferObjects +{ + public class PlayedCards + { + public List word1 { get; set; } + public List word2 { get; set; } + public List word3 { get; set; } + public List word4 { get; set; } + public List word5 { get; set; } + public List unplayed { get; set; } + public Card discarded { get; set; } + public IEnumerable AllCards() + => word1.Concat(word2).Concat(word3) + .Concat(word4).Concat(word5).Append(discarded); + public PlayedCards(IEnumerable word1, + IEnumerable word2, + IEnumerable word3, + IEnumerable word4, + IEnumerable word5, + IEnumerable unplayed, + Card discarded) + { + this.word1 = word1.ToList(); + this.word2 = word2.ToList(); + this.word3 = word3.ToList(); + this.word4 = word4.ToList(); + this.word5 = word5.ToList(); + this.unplayed = unplayed.ToList(); + this.discarded = discarded; + } + } +}