a lot of stuff

This commit is contained in:
2023-06-06 03:25:39 -05:00
parent 0272be35ad
commit 17033ac81f
13 changed files with 516 additions and 3 deletions

4
.editorconfig Normal file
View File

@@ -0,0 +1,4 @@
[*.cs]
# IDE1006: Naming Styles
dotnet_diagnostic.IDE1006.severity = none

View File

@@ -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

View File

@@ -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<IResult> 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<IResult> 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<IResult> GetTestCard(string text, int value)
=> TypedResults.Ok(new Card(text, value));
internal static async Task<IResult> 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<IResult> 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";
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<GameCode>(() => 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<Card>(200, "application/json")
.WithName("Test_GetCard")
.WithOpenApi();
apiPrefix.MapPost("/create_game", Api.CreateGame)
.Produces<GameCode>(200, "text/plain")
.Produces<string>(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<Card>(200, "application/json")
.Produces<string>(400, "text/plain")
.WithName("DrawCard")
.WithOpenApi();
playPrefix.MapPut("/join_game", (Delegate)Api.JoinGame)
.Produces(200)
.Produces<string>(400, "text/plain")
.Produces<string>(409, "text/plain")
.WithName("JoinGame")
.WithOpenApi();
playPrefix.MapPut("/play_cards", (Delegate)Api.PlayCards)
.Produces(200)
.Produces<string>(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);
}
*/

View File

@@ -7,6 +7,10 @@
<RootNamespace>Squiddler_Server_Lite</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />

View File

@@ -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<UserState> users { get; set; }
public int activePlayer { get; set; }
public List<Card> 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<GameState> 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,
}
}
}

View File

@@ -0,0 +1,17 @@
using Squiddler_Server_Lite.TransferObjects;
namespace Squiddler_Server_Lite.StateObjects
{
public class PlayAreaState
{
public readonly List<Card>[] 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));
}
}
}

View File

@@ -0,0 +1,19 @@
using Squiddler_Server_Lite.TransferObjects;
namespace Squiddler_Server_Lite.StateObjects
{
public class UserState
{
public string? userName { get; set; }
public List<Card> hand { get; } = new();
public bool TryJoin(string userName)
{
if (this.userName is null)
{
this.userName = userName;
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,38 @@
namespace Squiddler_Server_Lite.TransferObjects
{
public class Card : IComparable<Card>
{
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;
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,32 @@
namespace Squiddler_Server_Lite.TransferObjects
{
public class PlayedCards
{
public List<Card> word1 { get; set; }
public List<Card> word2 { get; set; }
public List<Card> word3 { get; set; }
public List<Card> word4 { get; set; }
public List<Card> word5 { get; set; }
public List<Card> unplayed { get; set; }
public Card discarded { get; set; }
public IEnumerable<Card> AllCards()
=> word1.Concat(word2).Concat(word3)
.Concat(word4).Concat(word5).Append(discarded);
public PlayedCards(IEnumerable<Card> word1,
IEnumerable<Card> word2,
IEnumerable<Card> word3,
IEnumerable<Card> word4,
IEnumerable<Card> word5,
IEnumerable<Card> 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;
}
}
}