From 1d68c8cebb7e34ecb99038770c3c2b66e7ce43c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:01:08 +0000 Subject: [PATCH 1/6] Initial plan From fac2bb5e4b96003a639406f188f0f3939f827b1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:16:01 +0000 Subject: [PATCH 2/6] Implement letter tracking feature - backend and UI complete Co-authored-by: webreidi <55603905+webreidi@users.noreply.github.com> --- .../DTOs/GameDtos.cs | 3 +- .../Mapping/DtoMapper.cs | 3 +- Codel-Cloud-Native.Tests/UnitTests.cs | 68 +++++++++++++++++++ .../Components/Pages/PlayCodele.razor | 29 ++++++++ Codel-Cloud-Native.Web/DTOs/GameDtos.cs | 3 +- CodeleLogic/Models/GameSession.cs | 35 ++++++++++ 6 files changed, 138 insertions(+), 3 deletions(-) diff --git a/Codel-Cloud-Native.ApiService/DTOs/GameDtos.cs b/Codel-Cloud-Native.ApiService/DTOs/GameDtos.cs index 1d8a8ac..9b558c5 100644 --- a/Codel-Cloud-Native.ApiService/DTOs/GameDtos.cs +++ b/Codel-Cloud-Native.ApiService/DTOs/GameDtos.cs @@ -28,7 +28,8 @@ public record GameSessionDto( int MaxAttempts, bool IsComplete, bool IsWin, - IReadOnlyList Guesses + IReadOnlyList Guesses, + IReadOnlyDictionary GuessedLetters // Key: letter, Value: status string ); /// diff --git a/Codel-Cloud-Native.ApiService/Mapping/DtoMapper.cs b/Codel-Cloud-Native.ApiService/Mapping/DtoMapper.cs index 80b9001..172949a 100644 --- a/Codel-Cloud-Native.ApiService/Mapping/DtoMapper.cs +++ b/Codel-Cloud-Native.ApiService/Mapping/DtoMapper.cs @@ -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() ); } diff --git a/Codel-Cloud-Native.Tests/UnitTests.cs b/Codel-Cloud-Native.Tests/UnitTests.cs index 4dfe14d..0a0a183 100644 --- a/Codel-Cloud-Native.Tests/UnitTests.cs +++ b/Codel-Cloud-Native.Tests/UnitTests.cs @@ -1,6 +1,8 @@ using System.Net; using System; using CodeleLogic; +using CodeleLogic.Models; +using CodeleLogic.Services; namespace Codel_Cloud_Native.Tests; @@ -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); + } } diff --git a/Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor b/Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor index 781c5b5..2f24a15 100644 --- a/Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor +++ b/Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor @@ -10,6 +10,35 @@
+ +@if (currentGame?.GuessedLetters != null && currentGame.GuessedLetters.Count > 0) +{ +
+
Letters Guessed:
+
+ @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" + }; + + } + else + { + + } + } +
+
+} +

Attempt #: @attempts

diff --git a/Codel-Cloud-Native.Web/DTOs/GameDtos.cs b/Codel-Cloud-Native.Web/DTOs/GameDtos.cs index 2bda2f8..010a581 100644 --- a/Codel-Cloud-Native.Web/DTOs/GameDtos.cs +++ b/Codel-Cloud-Native.Web/DTOs/GameDtos.cs @@ -28,7 +28,8 @@ public record GameSessionDto( int MaxAttempts, bool IsComplete, bool IsWin, - IReadOnlyList Guesses + IReadOnlyList Guesses, + IReadOnlyDictionary GuessedLetters // Key: letter, Value: status string ); /// diff --git a/CodeleLogic/Models/GameSession.cs b/CodeleLogic/Models/GameSession.cs index eb52ae6..b74a447 100644 --- a/CodeleLogic/Models/GameSession.cs +++ b/CodeleLogic/Models/GameSession.cs @@ -14,12 +14,19 @@ public class GameSession public bool IsComplete => IsWin || Attempts.Count >= MaxAttempts; public bool IsWin => Attempts.Any(a => a.IsWin); + /// + /// Dictionary tracking the status of letters that have been guessed + /// Key: letter character (uppercase), Value: best status achieved for that letter + /// + public Dictionary 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(); + GuessedLetters = new Dictionary(); } /// @@ -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; + } + } + } + + /// + /// Gets the priority of a letter status for tracking purposes + /// Higher priority statuses take precedence over lower ones + /// + private static int GetStatusPriority(LetterStatus status) + { + return status switch + { + LetterStatus.Correct => 3, + LetterStatus.IncorrectPosition => 2, + LetterStatus.Incorrect => 1, + _ => 0 + }; } } \ No newline at end of file From 242b88c2984882e5c1cc437aa6455b5dad863ce4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 17:13:00 +0000 Subject: [PATCH 3/6] Restore Program partial class for tests --- Codel-Cloud-Native.ApiService/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Codel-Cloud-Native.ApiService/Program.cs b/Codel-Cloud-Native.ApiService/Program.cs index 6f0ef4d..ff9a6b4 100644 --- a/Codel-Cloud-Native.ApiService/Program.cs +++ b/Codel-Cloud-Native.ApiService/Program.cs @@ -111,6 +111,7 @@ app.Run(); +public partial class Program { } From b086641721d674352860acf2703794fe5457017d Mon Sep 17 00:00:00 2001 From: "Wendy Breiding (She/Her)" Date: Tue, 30 Jun 2026 08:57:45 -0700 Subject: [PATCH 4/6] Update package references and enhance guess evaluation logic for duplicate letters --- .../Codel-Cloud-Native.ServiceDefaults.csproj | 2 +- .../DomainServicesTests.cs | 20 +++++++ Codel-Cloud-Native.Tests/UnitTests.cs | 20 +++++++ CodeleLogic/CodeleLogic.csproj | 2 +- CodeleLogic/Guess.cs | 47 +++++++++++++---- CodeleLogic/Services/GuessEvaluator.cs | 52 +++++++++++++------ 6 files changed, 114 insertions(+), 29 deletions(-) diff --git a/Codel-Cloud-Native.ServiceDefaults/Codel-Cloud-Native.ServiceDefaults.csproj b/Codel-Cloud-Native.ServiceDefaults/Codel-Cloud-Native.ServiceDefaults.csproj index 1ddb50d..90fffe7 100644 --- a/Codel-Cloud-Native.ServiceDefaults/Codel-Cloud-Native.ServiceDefaults.csproj +++ b/Codel-Cloud-Native.ServiceDefaults/Codel-Cloud-Native.ServiceDefaults.csproj @@ -13,7 +13,7 @@ - + diff --git a/Codel-Cloud-Native.Tests/DomainServicesTests.cs b/Codel-Cloud-Native.Tests/DomainServicesTests.cs index 48c5204..05d34aa 100644 --- a/Codel-Cloud-Native.Tests/DomainServicesTests.cs +++ b/Codel-Cloud-Native.Tests/DomainServicesTests.cs @@ -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() { diff --git a/Codel-Cloud-Native.Tests/UnitTests.cs b/Codel-Cloud-Native.Tests/UnitTests.cs index 5513e8c..8b5f5ac 100644 --- a/Codel-Cloud-Native.Tests/UnitTests.cs +++ b/Codel-Cloud-Native.Tests/UnitTests.cs @@ -110,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() { diff --git a/CodeleLogic/CodeleLogic.csproj b/CodeleLogic/CodeleLogic.csproj index 935837c..4516454 100644 --- a/CodeleLogic/CodeleLogic.csproj +++ b/CodeleLogic/CodeleLogic.csproj @@ -6,7 +6,7 @@ - + \ No newline at end of file diff --git a/CodeleLogic/Guess.cs b/CodeleLogic/Guess.cs index 233a838..db6cd7d 100644 --- a/CodeleLogic/Guess.cs +++ b/CodeleLogic/Guess.cs @@ -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(); + + // 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; + } + } + + // 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) { diff --git a/CodeleLogic/Services/GuessEvaluator.cs b/CodeleLogic/Services/GuessEvaluator.cs index 71de268..fd8f696 100644 --- a/CodeleLogic/Services/GuessEvaluator.cs +++ b/CodeleLogic/Services/GuessEvaluator.cs @@ -10,32 +10,52 @@ public class GuessEvaluator : IGuessEvaluator if (string.IsNullOrEmpty(guess) || string.IsNullOrEmpty(targetWord)) return Enumerable.Empty<(char, LetterStatus)>(); - var results = new List<(char, LetterStatus)>(); - - // Use the existing logic from Guess.cs with improvements var length = Math.Min(guess.Length, targetWord.Length); - + var statuses = new LetterStatus[length]; + var isExactMatch = new bool[length]; + var unmatchedTargetLetterCounts = new Dictionary(); + + // First pass: mark exact-position matches and count the remaining target letters. for (int i = 0; i < length; i++) { - char letter = guess[i]; - bool isDuplicateInAnswer = targetWord.Count(x => x == letter) > 1; + if (guess[i] == targetWord[i]) + { + isExactMatch[i] = true; + statuses[i] = LetterStatus.Correct; + } + else + { + var targetLetter = targetWord[i]; + unmatchedTargetLetterCounts[targetLetter] = unmatchedTargetLetterCounts.GetValueOrDefault(targetLetter) + 1; + } + } - // Check for duplicate letters - use existing logic - if ((results.Contains((letter, LetterStatus.Correct)) || results.Contains((letter, LetterStatus.IncorrectPosition))) && !isDuplicateInAnswer) + // Second pass: only assign IncorrectPosition if an unmatched target letter is still available. + for (int i = 0; i < length; i++) + { + if (isExactMatch[i]) { - results.Add((letter, LetterStatus.Incorrect)); + continue; } - else // regular Wordle logic + + var guessLetter = guess[i]; + if (unmatchedTargetLetterCounts.TryGetValue(guessLetter, out var count) && count > 0) + { + statuses[i] = LetterStatus.IncorrectPosition; + unmatchedTargetLetterCounts[guessLetter] = count - 1; + } + else { - if (guess[i] == targetWord[i]) - results.Add((letter, LetterStatus.Correct)); - else if (targetWord.Contains(letter)) - results.Add((letter, LetterStatus.IncorrectPosition)); - else - results.Add((letter, LetterStatus.Incorrect)); + statuses[i] = LetterStatus.Incorrect; } } + var results = new List<(char, LetterStatus)>(length); + for (int i = 0; i < length; i++) + { + results.Add((guess[i], statuses[i])); + } + // If guess is longer than answer, mark remaining letters as incorrect if (guess.Length > targetWord.Length) { From bc6408adbeea58545f7ddefca1f508f417b21f9e Mon Sep 17 00:00:00 2001 From: Wendy Breiding <55603905+webreidi@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:05:50 -0700 Subject: [PATCH 5/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor b/Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor index adefd4c..35b1505 100644 --- a/Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor +++ b/Codel-Cloud-Native.Web/Components/Pages/PlayCodele.razor @@ -14,7 +14,7 @@ {
Letters Guessed:
-
+
@foreach (char letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ") { @if (gameSession.GuessedLetters.ContainsKey(letter)) From ab86611f1b4777fd4d055e5f274748164de868f8 Mon Sep 17 00:00:00 2001 From: Wendy Breiding <55603905+webreidi@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:06:13 -0700 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CodeleLogic/Models/GameSession.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeleLogic/Models/GameSession.cs b/CodeleLogic/Models/GameSession.cs index b4263f3..c29decd 100644 --- a/CodeleLogic/Models/GameSession.cs +++ b/CodeleLogic/Models/GameSession.cs @@ -52,7 +52,7 @@ public void AddAttempt(GuessResult guessResult) // Update guessed letters tracking foreach (var letterResult in guessResult.Letters) { - char upperLetter = char.ToUpper(letterResult.Letter); + 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) ||