Skip to content
Merged
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
5 changes: 4 additions & 1 deletion Codel-Cloud-Native.ApiService/DTOs/DtoMappingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public static GameSessionDto ToDto(this GameSession gameSession)
IsWin = gameSession.IsWin,
CreatedAt = gameSession.CreatedAt,
RemainingAttempts = gameSession.RemainingAttempts,
GuessHistory = gameSession.Attempts.Select(a => a.ToDto()).ToList()
GuessHistory = gameSession.Attempts.Select(a => a.ToDto()).ToList(),
GuessedLetters = gameSession.GuessedLetters.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.ToString())
};
}

Expand Down
1 change: 1 addition & 0 deletions Codel-Cloud-Native.ApiService/DTOs/GameDTOs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public record GameSessionDto
public DateTime CreatedAt { get; init; }
public int RemainingAttempts { get; init; }
public List<GuessResultDto> GuessHistory { get; init; } = new();
public Dictionary<char, string> GuessedLetters { get; init; } = new();
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions Codel-Cloud-Native.ApiService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@

app.Run();

public partial class Program { }



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.0.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
Expand Down
20 changes: 20 additions & 0 deletions Codel-Cloud-Native.Tests/DomainServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ public void GuessEvaluator_AllIncorrect_ReturnsAllIncorrectStatuses()
Assert.All(resultList, r => Assert.Equal(LetterStatus.Incorrect, r.Status));
}

[Fact]
public void GuessEvaluator_DuplicateLetter_WithOneExactMatchAndOneExtraGuessLetter_ScoresCorrectly()
{
// Arrange
var evaluator = new GuessEvaluator();
var guess = "floor";
var target = "razor";

// Act
var result = evaluator.EvaluateGuess(guess, target).ToList();

// Assert
Assert.Equal(5, result.Count);
Assert.Equal(LetterStatus.Incorrect, result[0].Status); // f
Assert.Equal(LetterStatus.Incorrect, result[1].Status); // l
Assert.Equal(LetterStatus.Incorrect, result[2].Status); // o (extra)
Assert.Equal(LetterStatus.Correct, result[3].Status); // o (exact)
Assert.Equal(LetterStatus.Correct, result[4].Status); // r (exact)
}

[Fact]
public void GuessEvaluator_IsWinningGuess_CorrectAnswer_ReturnsTrue()
{
Expand Down
98 changes: 98 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 @@ -108,6 +110,26 @@ public void TestGetGuessNotDuplicateLetters()
Assert.Equal(LetterStatus.Correct, guess.GuessStatus[4].Item2);
}

[Fact]
public void TestGetGuessStatuses_DuplicateLetter_WithOneExactMatchAndOneExtraGuessLetter_ScoresCorrectly()
{
// Arrange
var guess = new Guess("floor");
string answer = "razor";

// Act
guess.GetGuessStatuses(answer);

// Assert
Assert.NotNull(guess.GuessStatus);
Assert.Equal(5, guess.GuessStatus.Count);
Assert.Equal(LetterStatus.Incorrect, guess.GuessStatus[0].Item2); // f
Assert.Equal(LetterStatus.Incorrect, guess.GuessStatus[1].Item2); // l
Assert.Equal(LetterStatus.Incorrect, guess.GuessStatus[2].Item2); // o (extra)
Assert.Equal(LetterStatus.Correct, guess.GuessStatus[3].Item2); // o (exact)
Assert.Equal(LetterStatus.Correct, guess.GuessStatus[4].Item2); // r (exact)
}

[Fact]
public void TestIsWinningGuess()
{
Expand Down Expand Up @@ -163,4 +185,80 @@ public void TestIsGuessWrongLength()
// Assert
Assert.False(guess.IsWinningGuess(answer));
}

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

// Act
var statuses = guessEvaluator.EvaluateGuess("HELLO", "APPLE");
var letters = statuses.Select((ls, index) => new LetterResult(ls.Letter, ls.Status, index)).ToList();
var guessResult = new GuessResult("HELLO", letters, guessEvaluator.IsWinningGuess("HELLO", "APPLE"));
gameSession.AddAttempt(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().ToString(), "APPLE", 5);
var guessEvaluator = new GuessEvaluator();

// Act - first guess: HELLO vs APPLE (L will be IncorrectPosition)
var statuses1 = guessEvaluator.EvaluateGuess("HELLO", "APPLE");
var letters1 = statuses1.Select((ls, index) => new LetterResult(ls.Letter, ls.Status, index)).ToList();
var guessResult1 = new GuessResult("HELLO", letters1, guessEvaluator.IsWinningGuess("HELLO", "APPLE"));
gameSession.AddAttempt(guessResult1);

// Act - second guess: APPLE vs APPLE (all letters correct, including L upgraded to Correct)
var statuses2 = guessEvaluator.EvaluateGuess("APPLE", "APPLE");
var letters2 = statuses2.Select((ls, index) => new LetterResult(ls.Letter, ls.Status, index)).ToList();
var guessResult2 = new GuessResult("APPLE", letters2, guessEvaluator.IsWinningGuess("APPLE", "APPLE"));
gameSession.AddAttempt(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().ToString(), "APPLE", 5);
var guessEvaluator = new GuessEvaluator();

// Act - first guess: A is correct in APPLE
var statuses1 = guessEvaluator.EvaluateGuess("ALOFT", "APPLE");
var letters1 = statuses1.Select((ls, index) => new LetterResult(ls.Letter, ls.Status, index)).ToList();
var guessResult1 = new GuessResult("ALOFT", letters1, guessEvaluator.IsWinningGuess("ALOFT", "APPLE"));
gameSession.AddAttempt(guessResult1);

// Act - second guess: A would be incorrect position if it appeared elsewhere, but shouldn't downgrade
var statuses2 = guessEvaluator.EvaluateGuess("BEAUT", "APPLE"); // A is not in APPLE at position 2
var letters2 = statuses2.Select((ls, index) => new LetterResult(ls.Letter, ls.Status, index)).ToList();
var guessResult2 = new GuessResult("BEAUT", letters2, guessEvaluator.IsWinningGuess("BEAUT", "APPLE"));
gameSession.AddAttempt(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);
}
}
1 change: 1 addition & 0 deletions Codel-Cloud-Native.Web/CodeleApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public record GameSessionDto
public DateTime CreatedAt { get; init; }
public int RemainingAttempts { get; init; }
public List<GuessResultDto> GuessHistory { get; init; } = new();
public Dictionary<char, string> GuessedLetters { get; init; } = new();
}

public record GuessResultDto
Expand Down
28 changes: 28 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,34 @@

@if (gameSession != null)
{
@if (gameSession.GuessedLetters != null && gameSession.GuessedLetters.Count > 0)
{
<div class="letter-tracker mb-3">
<h5>Letters Guessed:</h5>
<div class="d-flex flex-wrap gap-1" role="group" aria-label="Letter tracker">
@foreach (char letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
{
@if (gameSession.GuessedLetters.ContainsKey(letter))
{
var status = gameSession.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 #: @(gameSession.Attempts + 1)</strong></p>
}

Expand Down
2 changes: 1 addition & 1 deletion CodeleLogic/CodeleLogic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.3.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Dapper" Version="2.1.35" />
</ItemGroup>
</Project>
47 changes: 36 additions & 11 deletions CodeleLogic/Guess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,54 @@ public void GetGuessStatuses(string answer)
{
if (!string.IsNullOrEmpty(Word))
{

GuessStatus = new();

// iterate over the overlapping length of the guess and answer to avoid index exceptions
// Iterate over the overlapping length of the guess and answer to avoid index exceptions.
var length = Math.Min(Word.Length, answer.Length);
var statuses = new LetterStatus[length];
var isExactMatch = new bool[length];
var unmatchedAnswerLetterCounts = new Dictionary<char, int>();

// First pass: mark exact matches and count remaining answer letters.
for (int i = 0; i < length; i++)
{
if (Word[i] == answer[i])
{
isExactMatch[i] = true;
statuses[i] = LetterStatus.Correct;
}
else
{
char answerLetter = answer[i];
unmatchedAnswerLetterCounts[answerLetter] = unmatchedAnswerLetterCounts.GetValueOrDefault(answerLetter) + 1;
}
}
Comment thread
webreidi marked this conversation as resolved.

// Second pass: consume unmatched answer letters for incorrect-position matches.
for (int i = 0; i < length; i++)
{
char letter = Word[i];
bool isDuplicateInAnswer = answer.Count(x => x == letter) > 1;
if (isExactMatch[i])
{
continue;
}

// Check for duplicate letters
if ((GuessStatus.Contains((letter, LetterStatus.Correct)) || GuessStatus.Contains((letter, LetterStatus.IncorrectPosition))) && !isDuplicateInAnswer)
char guessLetter = Word[i];
if (unmatchedAnswerLetterCounts.TryGetValue(guessLetter, out var count) && count > 0)
{
GuessStatus.Add((letter, LetterStatus.Incorrect));
statuses[i] = LetterStatus.IncorrectPosition;
unmatchedAnswerLetterCounts[guessLetter] = count - 1;
}
else // regular Wordle logic
else
{
if (Word[i] == answer[i]) GuessStatus.Add((letter, LetterStatus.Correct));
else if (answer.Contains(letter)) GuessStatus.Add((letter, LetterStatus.IncorrectPosition));
else GuessStatus.Add((letter, LetterStatus.Incorrect));
statuses[i] = LetterStatus.Incorrect;
}
}

for (int i = 0; i < length; i++)
{
GuessStatus.Add((Word[i], statuses[i]));
}

// If guess is longer than answer, mark remaining letters as incorrect
if (Word.Length > answer.Length)
{
Expand Down
36 changes: 36 additions & 0 deletions CodeleLogic/Models/GameSession.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CodeleLogic;
using CodeleLogic.Services;

namespace CodeleLogic.Models;
Expand All @@ -14,13 +15,20 @@ public class GameSession
public bool IsComplete { get; private set; }
public bool IsWin { get; private set; }
public DateTime CreatedAt { get; private set; }

/// <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; private set; }

public GameSession(string gameId, string targetWord, int maxAttempts = 5)
{
GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
TargetWord = targetWord ?? throw new ArgumentNullException(nameof(targetWord));
MaxAttempts = maxAttempts;
Attempts = new List<GuessResult>();
GuessedLetters = new Dictionary<char, LetterStatus>();
IsComplete = false;
IsWin = false;
CreatedAt = DateTime.UtcNow;
Expand All @@ -41,6 +49,19 @@ public void AddAttempt(GuessResult guessResult)

Attempts.Add(guessResult);

// Update guessed letters tracking
foreach (var letterResult in guessResult.Letters)
{
char upperLetter = char.ToUpperInvariant(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;
}
}

// Check if this attempt wins the game
if (guessResult.IsWin)
{
Expand All @@ -54,6 +75,21 @@ public void AddAttempt(GuessResult guessResult)
}
}

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

/// <summary>
/// Gets the current attempt number (1-based).
/// </summary>
Expand Down
Loading