Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Codel-Cloud-Native.ApiService/DTOs/GameDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public record GameSessionDto(
int MaxAttempts,
bool IsComplete,
bool IsWin,
IReadOnlyList<GuessResultDto> Guesses
IReadOnlyList<GuessResultDto> Guesses,
IReadOnlyDictionary<char, string> GuessedLetters // Key: letter, Value: status string
);

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion Codel-Cloud-Native.ApiService/Mapping/DtoMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public static GameSessionDto ToDto(this GameSession gameSession)
gameSession.MaxAttempts,
gameSession.IsComplete,
gameSession.IsWin,
gameSession.Attempts.Select(a => a.ToDto()).ToList().AsReadOnly()
gameSession.Attempts.Select(a => a.ToDto()).ToList().AsReadOnly(),
gameSession.GuessedLetters.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString()).AsReadOnly()
);
}

Expand Down
68 changes: 68 additions & 0 deletions Codel-Cloud-Native.Tests/UnitTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Net;
using System;
using CodeleLogic;
using CodeleLogic.Models;
using CodeleLogic.Services;

namespace Codel_Cloud_Native.Tests;

Expand Down Expand Up @@ -163,4 +165,70 @@ public void TestIsGuessWrongLength()
// Assert
Assert.False(guess.IsWinningGuess(answer));
}

[Fact]
public void TestGameSession_TrackGuessedLetters_SingleGuess()
{
// Arrange
var gameSession = new GameSession(Guid.NewGuid(), "APPLE", 5);
var guessEvaluator = new DefaultGuessEvaluator();

// Act
var guessResult = guessEvaluator.EvaluateGuess("HELLO", "APPLE");
gameSession.AddGuess(guessResult);

// Assert - HELLO vs APPLE:
// H=Incorrect, E=IncorrectPosition (E is in position 4 in APPLE), L=IncorrectPosition (L is in position 2), L=Incorrect, O=Incorrect
// Since we track the best status per letter, L should be IncorrectPosition (not Incorrect)
Assert.Equal(4, gameSession.GuessedLetters.Count); // H, E, L, O (duplicates consolidated)
Assert.Equal(LetterStatus.Incorrect, gameSession.GuessedLetters['H']);
Assert.Equal(LetterStatus.IncorrectPosition, gameSession.GuessedLetters['E']); // E is in APPLE but wrong position
Assert.Equal(LetterStatus.IncorrectPosition, gameSession.GuessedLetters['L']); // L is in APPLE but wrong position (best status wins)
Assert.Equal(LetterStatus.Incorrect, gameSession.GuessedLetters['O']);
}

[Fact]
public void TestGameSession_TrackGuessedLetters_LetterStatusUpgrade()
{
// Arrange
var gameSession = new GameSession(Guid.NewGuid(), "APPLE", 5);
var guessEvaluator = new DefaultGuessEvaluator();

// Act - first guess: HELLO vs APPLE (L will be IncorrectPosition)
var guessResult1 = guessEvaluator.EvaluateGuess("HELLO", "APPLE");
gameSession.AddGuess(guessResult1);

// Act - second guess: APPLE vs APPLE (all letters correct, including L upgraded to Correct)
var guessResult2 = guessEvaluator.EvaluateGuess("APPLE", "APPLE");
gameSession.AddGuess(guessResult2);

// Assert - L should be upgraded from IncorrectPosition to Correct
Assert.Equal(LetterStatus.Correct, gameSession.GuessedLetters['A']);
Assert.Equal(LetterStatus.Correct, gameSession.GuessedLetters['P']);
Assert.Equal(LetterStatus.Correct, gameSession.GuessedLetters['L']); // Upgraded from IncorrectPosition to Correct
Assert.Equal(LetterStatus.Correct, gameSession.GuessedLetters['E']); // Upgraded from IncorrectPosition to Correct
}

[Fact]
public void TestGameSession_TrackGuessedLetters_NoDowngrade()
{
// Arrange
var gameSession = new GameSession(Guid.NewGuid(), "APPLE", 5);
var guessEvaluator = new DefaultGuessEvaluator();

// Act - first guess: A is correct in APPLE
var guessResult1 = guessEvaluator.EvaluateGuess("ALOFT", "APPLE");
gameSession.AddGuess(guessResult1);

// Act - second guess: A would be incorrect position if it appeared elsewhere, but shouldn't downgrade
var guessResult2 = guessEvaluator.EvaluateGuess("BEAUT", "APPLE"); // A is not in APPLE at position 2
gameSession.AddGuess(guessResult2);

// Assert - A should remain Correct from first guess
Assert.Equal(LetterStatus.Correct, gameSession.GuessedLetters['A']);
Assert.Contains('B', gameSession.GuessedLetters.Keys);
Assert.Contains('E', gameSession.GuessedLetters.Keys);
Assert.Contains('U', gameSession.GuessedLetters.Keys);
Assert.Contains('T', gameSession.GuessedLetters.Keys);
}
}
29 changes: 29 additions & 0 deletions Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,35 @@

<br>

<!-- Letter Tracker -->
@if (currentGame?.GuessedLetters != null && currentGame.GuessedLetters.Count > 0)
{
<div class="letter-tracker mb-3">
<h5>Letters Guessed:</h5>
<div class="btn-group" role="group" aria-label="Letter tracker">
@foreach (char letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
{
@if (currentGame.GuessedLetters.ContainsKey(letter))
{
var status = currentGame.GuessedLetters[letter];
var cssClass = status switch
{
"Correct" => "btn btn-success",
"IncorrectPosition" => "btn btn-warning",
"Incorrect" => "btn btn-secondary",
_ => "btn btn-outline-dark"
};
<button type="button" class="@cssClass" disabled="true">@letter</button>
}
else
{
<button type="button" class="btn btn-outline-dark" disabled="true">@letter</button>
}
}
</div>
</div>
}

<p><strong>Attempt #: @attempts</strong></p>

<!-- display the player's guesses -->
Expand Down
3 changes: 2 additions & 1 deletion Codel-Cloud-Native.Web/DTOs/GameDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public record GameSessionDto(
int MaxAttempts,
bool IsComplete,
bool IsWin,
IReadOnlyList<GuessResultDto> Guesses
IReadOnlyList<GuessResultDto> Guesses,
IReadOnlyDictionary<char, string> GuessedLetters // Key: letter, Value: status string
);

/// <summary>
Expand Down
35 changes: 35 additions & 0 deletions CodeleLogic/Models/GameSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@ public class GameSession
public bool IsComplete => IsWin || Attempts.Count >= MaxAttempts;
public bool IsWin => Attempts.Any(a => a.IsWin);

/// <summary>
/// Dictionary tracking the status of letters that have been guessed
/// Key: letter character (uppercase), Value: best status achieved for that letter
/// </summary>
public Dictionary<char, LetterStatus> GuessedLetters { get; }

public GameSession(Guid gameId, string targetWord, int maxAttempts = 5)
{
GameId = gameId;
TargetWord = targetWord ?? throw new ArgumentNullException(nameof(targetWord));
MaxAttempts = maxAttempts;
Attempts = new List<GuessResult>();
GuessedLetters = new Dictionary<char, LetterStatus>();
}

/// <summary>
Expand All @@ -33,5 +40,33 @@ public void AddGuess(GuessResult guessResult)
throw new InvalidOperationException("Cannot add guess to completed game");

Attempts.Add(guessResult);

// Update guessed letters tracking
foreach (var letterResult in guessResult.Letters)
{
char upperLetter = char.ToUpper(letterResult.Letter);

// Only update if we don't have this letter or if the new status is better
if (!GuessedLetters.ContainsKey(upperLetter) ||
GetStatusPriority(letterResult.Status) > GetStatusPriority(GuessedLetters[upperLetter]))
{
GuessedLetters[upperLetter] = letterResult.Status;
}
}
}

/// <summary>
/// Gets the priority of a letter status for tracking purposes
/// Higher priority statuses take precedence over lower ones
/// </summary>
private static int GetStatusPriority(LetterStatus status)
{
return status switch
{
LetterStatus.Correct => 3,
LetterStatus.IncorrectPosition => 2,
LetterStatus.Incorrect => 1,
_ => 0
};
}
}