Checking in before major rewrite

This commit is contained in:
2023-06-11 01:01:12 -05:00
parent 17033ac81f
commit 0e904384fb
10 changed files with 129407 additions and 44 deletions

View File

@@ -23,8 +23,11 @@ namespace Squiddler_Server_Lite
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 card = gameState.PlayerDraw();
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));
@@ -39,20 +42,83 @@ namespace Squiddler_Server_Lite
var gameState = GameState.FindGame(gameCode);
if (gameState is null)
return TypedResults.BadRequest("Game code not found");
switch (gameState.JoinGame(playerName))
return gameState.JoinGame(playerName) switch
{
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();
}
GameState.JoinGameResult.Success => TypedResults.Ok(),
GameState.JoinGameResult.AlreadyJoined => TypedResults.Ok(),
GameState.JoinGameResult.Full => TypedResults.Conflict("Game is full"),
_ => throw new InvalidOperationException(),
};
}
internal static async Task<IResult> StartGame(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");
if (gameState.players.Any(p => p.userName is null))
return TypedResults.Conflict("Not all players have joined yet");
if (!gameState.Start())
return TypedResults.Conflict("Game already started");
return TypedResults.Ok();
}
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");
Card? mustDiscardOnFail = null;
if (!gameState.playerDrew)
mustDiscardOnFail = gameState.PlayerTakesFromDiscard();
if (!gameState.ValidatePlayedCards(played))
{
gameState.PlayerDiscard(mustDiscardOnFail);
return TypedResults.BadRequest("Cards don't match player hand");
}
//player not required to use all cards
if (gameState.playerWentOut >= 0)
{
gameState.ActivePlayer().roundScores.Add(played.BaseScore());
bool discardSuccess = gameState.PlayerDiscard(played.discarded);
Debug.Assert(discardSuccess);
gameState.ActivePlayer().playArea = new(played);
gameState.NextTurn();
return TypedResults.Ok();
}
else
{
if (played.unplayed.Any())
{
gameState.PlayerDiscard(mustDiscardOnFail);
return TypedResults.BadRequest("Cannot play cards without going out");
}
gameState.ActivePlayer().roundScores.Add(played.BaseScore());
gameState.playerWentOut = gameState.activePlayer;
bool discardSuccess = gameState.PlayerDiscard(played.discarded);
Debug.Assert(discardSuccess);
gameState.ActivePlayer().playArea = new(played);
gameState.NextTurn();
return TypedResults.Ok();
}
}
internal static async Task<IResult> DiscardEndTurn(HttpContext context,
[FromBody] [Required] Card discard)
{
var gameCode = GetGameCode(context.Request);
var playerName = GetPlayerName(context.Request);
@@ -68,15 +134,54 @@ namespace Squiddler_Server_Lite
return TypedResults.BadRequest("Not the active player");
if (!gameState.playerDrew)
{
var drawnCard = gameState.PlayerTakesFromDiscard();
if (!gameState.ValidatePlayedCards(played))
if (discard == gameState.discard.Last())
{
gameState.PlayerDiscard(drawnCard);
return TypedResults.BadRequest("Cards don't match player hand");
gameState.NextTurn();
return TypedResults.Ok();
}
throw new NotImplementedException();
var card = gameState.PlayerTakesFromDiscard();
if (!gameState.PlayerDiscard(discard))
{
gameState.PlayerDiscard(card);
return TypedResults.BadRequest("Card not in hand or top of discard");
}
gameState.NextTurn();
return TypedResults.Ok();
}
throw new NotImplementedException();
else
{
if (!gameState.PlayerDiscard(discard))
return TypedResults.BadRequest("Card not in hand");
gameState.NextTurn();
return TypedResults.Ok();
}
}
internal static async Task<IResult> GetState(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");
var player = gameState.GetPlayerFromName(playerName);
if (player is int playerIndex)
return TypedResults.Ok(new VisibleState()
{
gameName = gameState.gameName,
playerName = gameState.players[playerIndex].userName,
players = gameState.players.Select(p => p.userName).ToList(),
roundNumber = gameState.round,
activePlayer = gameState.activePlayer,
hand = gameState.players[playerIndex].hand.ToList(),
discard = gameState.discard.ToList(),
otherPlayersCards = gameState.players.Select(p => p.playArea).ToList(),
});
else
return TypedResults.BadRequest("Player not found");
}
private static GameCode? GetGameCode(HttpRequest request)
{

View File

@@ -0,0 +1,31 @@
A 2 10
B 8 2
C 8 2
D 5 4
E 2 12
F 6 2
G 6 4
H 7 2
I 2 8
J 13 2
K 8 2
L 3 4
M 5 2
N 5 6
O 2 8
P 6 2
Q 15 2
R 5 6
S 3 4
T 3 6
U 4 6
V 11 2
W 10 2
X 12 2
Y 4 4
Z 14 2
Qu 9 2
In 7 2
Er 7 2
Cl 10 2
Th 9 2

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@ var playPrefix = apiPrefix.MapGroup("/play")
{
Type = "string",
MaxLength = 4,
MinLength = 4
MinLength = 4,
},
});
newOp.Parameters.Add(new OpenApiParameter
@@ -88,12 +88,30 @@ playPrefix.MapPut("/join_game", (Delegate)Api.JoinGame)
.WithName("JoinGame")
.WithOpenApi();
playPrefix.MapPut("/start_game", (Delegate)Api.StartGame)
.Produces(200)
.Produces<string>(400, "text/plain")
.Produces<string>(409, "text/plain")
.WithName("StartGame")
.WithOpenApi();
playPrefix.MapPut("/play_cards", (Delegate)Api.PlayCards)
.Produces(200)
.Produces<string>(400, "text/plain")
.WithName("PlayCards")
.WithOpenApi();
playPrefix.MapPost("/discard_end_turn", (Delegate)Api.DiscardEndTurn)
.Produces(200)
.Produces<string>(400, "text/plain")
.WithName("DiscardEndTurn")
.WithOpenApi();
playPrefix.MapGet("/get_state", (Delegate)Api.GetState)
.Produces<VisibleState>(200)
.Produces<string>(400, "text/plain")
.WithName("GetState")
.WithOpenApi();
app.Run();

View File

@@ -1,4 +1,6 @@
using Squiddler_Server_Lite.TransferObjects;
using System.Diagnostics;
using System.Linq;
namespace Squiddler_Server_Lite.StateObjects
{
@@ -6,12 +8,15 @@ namespace Squiddler_Server_Lite.StateObjects
{
public GameCode gameCode { get; set; }
public string gameName { get; set; }
public List<Card> deck { get; set; }
public List<Card> resetDeck { get; set; }
public readonly int FIRST_ROUND;
public readonly int LAST_ROUND;
public int round { get; set; }
public List<UserState> users { get; set; }
public List<UserState> players { get; set; }
public int activePlayer { get; set; }
public List<Card> discard { get; set; } = new();
protected object Lock { get; } = new();
//index of player who first went out, otherwise -1
public int playerWentOut { get; set; } = -1;
#region TURNSTATE
@@ -19,35 +24,101 @@ namespace Squiddler_Server_Lite.StateObjects
#endregion
public void AddToDiscard(Card card)
{
discard.Add(new Card(card));
lock (Lock)
discard.Add(new Card(card));
}
public void DeckToDiscard()
{
lock(Lock)
{
var index = Random.Shared.Next(deck.Count);
var card = deck[index];
deck.RemoveAt(index);
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);
lock (Lock)
{
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());
{
lock (Lock)
return playedCards.AllCards().Order().SequenceEqual(ActivePlayer().hand.Order());
}
public bool PlayerDiscard(Card? card)
{
if (card is null)
return true;
lock (Lock)
{
if (!ActivePlayer().hand.Remove(card))
return false;
discard.Add(new Card(card));
return true;
}
}
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;
lock (Lock)
{
if (players.Any(u => CompareNames(u.userName, playerName)))
return JoinGameResult.AlreadyJoined;
//kind of an abuse of Any() to have side effects
return players.Any(u => u.TryJoin(playerName))
? JoinGameResult.Success : JoinGameResult.Full;
}
}
public Card PlayerDraw(int player)
{
lock (Lock)
{
int index = Random.Shared.Next(deck.Count);
var card = deck[index];
deck.RemoveAt(index);
players[player].hand.Add(card);
return card;
}
}
//the active player draws a card
public Card PlayerDraw()
{
lock (Lock)
return PlayerDraw(activePlayer);
}
public UserState ActivePlayer()
{
return users[activePlayer];
lock (Lock)
return players[activePlayer];
}
public bool Start()
{
lock (Lock)
{
if (Started())
return false;
round = FIRST_ROUND;
DrawHands();
return true;
}
}
public bool Started()
{
return round > 0;
}
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
//the Create method guarantees this value will be written to
private GameState(int firstRound, int lastRound)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
FIRST_ROUND = firstRound;
LAST_ROUND = lastRound;
@@ -58,8 +129,9 @@ namespace Squiddler_Server_Lite.StateObjects
{
gameCode = GameCode.CreateRandom(),
gameName = config.gameName,
users = Enumerable.Range(0, config.playerCount)
players = Enumerable.Range(0, config.playerCount)
.Select(i => new UserState()).ToList(),
resetDeck = LoadDeck(Path.Join("Assets", "Decks", "DefaultDeck.txt")),
};
ActiveStates.Add(gameState);
return gameState;
@@ -67,18 +139,116 @@ namespace Squiddler_Server_Lite.StateObjects
private static readonly List<GameState> ActiveStates = new();
public static GameState? FindGame(GameCode gameCode)
{
return ActiveStates.FirstOrDefault(g => g.gameCode == gameCode, null);
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)
internal void NextTurn()
{
throw new NotImplementedException();
lock (Lock)
{
activePlayer = (activePlayer + 1) % players.Count;
playerDrew = false;
if (activePlayer == playerWentOut)
NextRound();
else
return;
throw new NotImplementedException();
}
}
private void ResetCards()
{
deck = resetDeck.ToList();
foreach (var player in players)
player.hand.Clear();
discard.Clear();
DeckToDiscard();
}
private void DrawHands()
{
ResetCards();
for (int player = 0; player < players.Count; player++)
for (int i = 0; i < round; i++)
PlayerDraw(player);
}
private void NextRound()
{
int PlayerWordCount(PlayedCards played)
=> played.Words().Sum(w => w.Any() ? 1 : 0);
int PlayerLongestWord(PlayedCards played)
{
return played.Words().Max(w => w.Select(w => w.text).Aggregate((w1, w2) => w1 + w1).Length);
}
int GreatestWithoutTie(IList<int> values)
{
int max = values.Max();
if (values.Where(v => v == max).Count() == 1)
return values.IndexOf(max);
else
return -1;
}
lock (Lock)
{
foreach (var player in players)
Debug.Assert(player.playArea is not null);
var wordCounts = players.Select(u => PlayerWordCount(u.playArea!)).ToList();
var mostWordsPlayer = GreatestWithoutTie(wordCounts);
var wordLengths = players.Select(u => PlayerLongestWord(u.playArea!)).ToList();
var longestWordPlayer = GreatestWithoutTie(wordLengths);
if (mostWordsPlayer >= 0)
players[mostWordsPlayer].roundScores[players[mostWordsPlayer].roundScores.Count] += 10;
if (longestWordPlayer >= 0)
players[longestWordPlayer].roundScores[players[longestWordPlayer].roundScores.Count] += 10;
playerWentOut = -1;
round++;
if (round > LAST_ROUND)
EndGame();
else
{
DrawHands();
}
}
}
public int? GetPlayerFromName(string name)
{
for (int i = 0; i < players.Count; i++)
if (CompareNames(name, players[i].userName))
return i;
return null;
}
private static List<Card> LoadDeck(string filepath)
{
List<Card> deck = new();
foreach (var line in File.ReadAllLines(filepath))
{
if (string.IsNullOrWhiteSpace(line))
continue;
var spl = line.Split(' ');
if (spl.Length != 3)
throw new FormatException();
var text = spl[0];
var value = int.Parse(spl[1]);
var count = int.Parse(spl[2]);
for (int i = 0; i < count; i++)
deck.Add(new Card(text, value));
}
return deck;
}
private static bool CompareNames(string name1, string name2)
{
if (name1 is null || name2 is null)
return false;
return name1.ToUpper() == name2.ToUpper();
}
private void EndGame()
{
lock (Lock)
{
throw new NotImplementedException();
}
}
public enum JoinGameResult
{
Success,

View File

@@ -6,6 +6,10 @@ namespace Squiddler_Server_Lite.StateObjects
{
public string? userName { get; set; }
public List<Card> hand { get; } = new();
public List<int> roundScores { get; } = new();
public PlayedCards? playArea { get; set; }
public int TotalScore()
=> roundScores.Sum();
public bool TryJoin(string userName)
{
if (this.userName is null)

View File

@@ -0,0 +1,29 @@
//using System.Path
namespace Squiddler_Server_Lite.StateObjects
{
public class WordList
{
public HashSet<string> words { get; } = new();
public void AddWord(string word)
{
words.Add(word.Trim().ToUpper());
}
public void AddRange(IEnumerable<string> newWords)
{
foreach (var w in newWords)
AddWord(w);
}
public void LoadFile(string filepath)
{
AddRange(File.ReadLines(filepath));
}
//temporary, this should be replaced with something more flexible later
public void LoadEOWL()
=> LoadFile(Path.Join("Assets", "WordLists", "EOWL.txt"));
public WordList()
{
}
}
}

View File

@@ -1,11 +1,14 @@
namespace Squiddler_Server_Lite.TransferObjects
using System.Text.Json.Serialization;
namespace Squiddler_Server_Lite.TransferObjects
{
public class Card : IComparable<Card>
public class Card : IComparable<Card>, IEquatable<Card>
{
public string text { get; set; }
public int value { get; set; }
public override string ToString()
=> $"[\"{text}\": {value}";
[JsonConstructor]
public Card(string text, int value)
{
this.text = FixCaps(text) ?? throw new ArgumentNullException(nameof(text));
@@ -24,7 +27,6 @@
return text;
return text[..1].ToUpper() + text[1..].ToLower();
}
public int CompareTo(Card? other)
{
if (other is null)
@@ -34,5 +36,7 @@
return value.CompareTo(other.value);
return sCmp;
}
public bool Equals(Card? other)
=> CompareTo(other) == 0;
}
}

View File

@@ -1,4 +1,6 @@
namespace Squiddler_Server_Lite.TransferObjects
using System.Text.Json.Serialization;
namespace Squiddler_Server_Lite.TransferObjects
{
public class PlayedCards
{
@@ -10,8 +12,27 @@
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);
=> Words().Aggregate<IEnumerable<Card>>(Enumerable.Concat)
.Concat(unplayed).Append(discarded);
public IEnumerable<List<Card>> Words()
{
yield return word1;
yield return word2;
yield return word3;
yield return word4;
yield return word5;
}
public int BaseScore()
=> word1.Sum(c => c.value)
+ word2.Sum(c => c.value)
+ word3.Sum(c => c.value)
+ word4.Sum(c => c.value)
+ word5.Sum(c => c.value)
- unplayed.Sum(c => c.value);
public PlayedCards()
{
}
public PlayedCards(IEnumerable<Card> word1,
IEnumerable<Card> word2,
IEnumerable<Card> word3,
@@ -26,7 +47,14 @@
this.word4 = word4.ToList();
this.word5 = word5.ToList();
this.unplayed = unplayed.ToList();
this.discarded = discarded;
this.discarded = new(discarded);
}
public PlayedCards(PlayedCards playedCards)
: this(playedCards.word1, playedCards.word2, playedCards.word3,
playedCards.word4, playedCards.word5, playedCards.unplayed,
playedCards.discarded)
{
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Squiddler_Server_Lite.TransferObjects
{
public class VisibleState
{
public string gameName { get; set; }
public string playerName { get; set; }
public int roundNumber { get; set; }
public List<string> players { get; set; }
public int activePlayer { get; set; }
public List<Card> hand { get; set; }
public List<Card> discard { get; set; }
public List<PlayedCards?> otherPlayersCards { get; set; }
}
}