Skip to content

Commit

Permalink
implement guess processing logic (#5)
Browse files Browse the repository at this point in the history
## Why
Checking if the guesses correspond to the hidden word is essential for
the gameplay.
## How
By implementing the `tryGuess` function that accepts a word, increments
one attempt and then checks if the word correspond to the hidden word,
or parts of it, updating hitmaps according.

- adds a 4th state to the hitmap, corresponding to discarded letters
(don't exist at all);
- implements `findIndex` to facilitate alphabet state manipulation;
- implements `findAllOccurances` to ensure all occurences of a letter
have their intermediate state updated on the hidden word hitmap;
- declares the `ATTEMPTS` which will limit number of attempts;
- includes tests for the new functions;
  • Loading branch information
rccsousa authored Oct 30, 2024
1 parent 803c595 commit 05ed143
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/Interfaces.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ interface StructTypes {
*/
struct CharState {
string char;
uint256 state; // 0 = miss ; 1 = exists ; 2 = hit
uint256 state; // 0 = miss ; 1 = exists ; 2 = hit ; 3 = discarded
}
}
45 changes: 41 additions & 4 deletions src/StringUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,45 @@ library StringUtils {
return false;
}

// function that generates the hitmap for the target string and controls
// correct / wrong state;
// function that returns the index of a letter from a string hitmap
// this function stops at the first occurence and is only fit for alphabet hitmap updating
function findIndex(StructTypes.CharState[] memory target, string memory letter) internal pure returns (uint256) {
for (uint256 i = 0; i < target.length; i++) {
if (areEqual(target[i].char, letter)) {
return i;
}
}
return 0;
}

function findAllOccurences(string memory target, string memory letter) internal pure returns (uint256[] memory) {
// Check if the letter is not empty
require(bytes(letter).length == 1, "Letter must be a single character.");

bytes memory bStr = bytes(toLowerCase(target));
bytes memory bLetter = bytes(toLowerCase(letter));

// Create a dynamic array to hold the occurrences
uint256[] memory occurrences = new uint256[](bStr.length);
uint256 count = 0; // Counter for the number of occurrences

for (uint256 i = 0; i < bStr.length; i++) {
if (bStr[i] == bLetter[0]) {
occurrences[count] = i; // Store the index of occurrence
count++;
}
}

// Resize the array to the number of found occurrences
uint256[] memory result = new uint256[](count);
for (uint256 j = 0; j < count; j++) {
result[j] = occurrences[j];
}

return result;
}

// function that returns the index of a letter from a string hi // correct / wrong state;
function generateHitmap(string memory target) internal pure returns (StructTypes.CharState[] memory) {
bytes memory bStr = bytes(toLowerCase(target));
StructTypes.CharState[] memory res = new StructTypes.CharState[](bStr.length);
Expand All @@ -84,15 +121,15 @@ library StringUtils {
if (state > 2) {
revert("Invalid state.");
}
if (hitmap[index].state < 2) {
if (hitmap[index].state < 3) {
hitmap[index].state = state;
}
}

// function that checks if the hitmap is complete
function isHitmapComplete(StructTypes.CharState[] memory hitmap) internal pure returns (bool) {
for (uint256 i = 0; i < hitmap.length; i++) {
if (hitmap[i].state == 0) {
if (hitmap[i].state != 2) {
return false;
}
}
Expand Down
68 changes: 64 additions & 4 deletions src/Wordle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ pragma solidity ^0.8.28;

import {StringUtils} from "./StringUtils.sol";
import {StructTypes} from "./Interfaces.sol";
import {console} from "forge-std/console.sol";

contract Wordle {
using StringUtils for string;

// declare hidden word variable
StructTypes.CharState[] private HIDDEN_WORD;
StructTypes.CharState[] private ALPHABET;
string public HIDDEN_WORD;
StructTypes.CharState[] public HIDDEN_WORD_HITMAP;
StructTypes.CharState[] public ALPHABET;
uint256 public ATTEMPTS = 0;

// setup hidden word
// todo: implement obfuscation. would keccak256 be a good approach?
Expand All @@ -23,21 +26,78 @@ contract Wordle {
}

// generates the hitmap of the word and returns it
HIDDEN_WORD = StringUtils.generateHitmap(word);
HIDDEN_WORD = word;
HIDDEN_WORD_HITMAP = StringUtils.generateHitmap(word);
}

// verify if hidden word was setup correctly
// todo: remove once tests are finishied
function getHiddenWord() public view returns (StructTypes.CharState[] memory) {
return HIDDEN_WORD;
return HIDDEN_WORD_HITMAP;
}

function getAlphabet() public view returns (StructTypes.CharState[] memory) {
return ALPHABET;
}

function getAttempts() public view returns (uint256) {
return ATTEMPTS;
}

// setup alphabet hitmap
function setupAlphabet() public {
ALPHABET = StringUtils.generateHitmap("abcdefghijklmnopqrstuvwxyz");
}

/*
Processes the guess, comparing it to the hidden word and assessing
and updating the hitmap and alphabet accordingly.
*/
function tryGuess(string calldata guess) public returns (bool) {
ATTEMPTS++;
StructTypes.CharState[] memory guessHitmap = StringUtils.generateHitmap(guess);

// Check if the guess matches the hidden word immediately.
if (StringUtils.areEqual(guess, HIDDEN_WORD)) {
return true;
}

/*
Loop through each character of the guess hitmap. For each character,
update the hitmap and alphabet states based on the hit/miss/exist logic.
*/
for (uint256 i = 0; i < guessHitmap.length; i++) {
// Find the index of the letter in the alphabet for state updates.
uint256 alphaIdx = StringUtils.findIndex(ALPHABET, guessHitmap[i].char);

/*
If the character and its index match the hidden word,
update both hitmap and alphabet states to indicate a correct guess.
*/
if (StringUtils.areEqual(guessHitmap[i].char, HIDDEN_WORD_HITMAP[i].char)) {
HIDDEN_WORD_HITMAP[i].state = 2; // Final state for correct position
ALPHABET[alphaIdx].state = 2; // Final state in alphabet
continue;
}

/*
If the character does not match, check if it exists in the hidden word.
Update the states to indicate existence or discard.
*/
if (HIDDEN_WORD.contains(guessHitmap[i].char)) {
// [OPTIONAL?] Update hidden word hitmap to indicate the character exists in an incorrect position.
uint256[] memory occurrences = StringUtils.findAllOccurences(HIDDEN_WORD, guessHitmap[i].char);
for (uint256 j = 0; j < occurrences.length; j++) {
HIDDEN_WORD_HITMAP[occurrences[j]].state = 1; // Intermediate state for existence
}
ALPHABET[alphaIdx].state = 1; // Intermediate state in alphabet
} else {
// The character does not exist in the hidden word; mark it as discarded.
ALPHABET[alphaIdx].state = 3; // Discarded state in alphabet
}
}

// Check for the winning condition after processing all characters.
return StringUtils.isHitmapComplete(HIDDEN_WORD_HITMAP);
}
}
15 changes: 15 additions & 0 deletions test/StringUtils.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,19 @@ contract TestStringMethods is Test {
StringUtils.updateHitmap(hitmap, 1, 2);
assertTrue(StringUtils.isHitmapComplete(hitmap));
}

// test find index
function test_findIndex() public pure {
StructTypes.CharState[] memory alphabet = StringUtils.generateHitmap("abcdefghijklmnopqrstuvwxyz");
assertEq(StringUtils.findIndex(alphabet, "a"), 0);
assertEq(StringUtils.findIndex(alphabet, "b"), 1);
assertEq(StringUtils.findIndex(alphabet, "z"), 25);
}

// test find all occurences
function test_findAllOccurences() public pure {
assertEq(StringUtils.findAllOccurences("hello", "l").length, 2);
assertEq(StringUtils.findAllOccurences("hello", "z").length, 0);
assertEq(StringUtils.findAllOccurences("hello", "h").length, 1);
}
}
32 changes: 32 additions & 0 deletions test/Wordle.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ pragma solidity ^0.8.28;
import {Test} from "forge-std/Test.sol";
import {Wordle} from "../src/Wordle.sol";
import {StructTypes} from "../src/Interfaces.sol";
import {StringUtils} from "../src/StringUtils.sol";

contract WordleTest is Test {
using StringUtils for string;

Wordle wordle;

function setUp() public {
Expand Down Expand Up @@ -78,4 +81,33 @@ contract WordleTest is Test {
assertEq(alphabet[i].state, 0);
}
}

// test guess mechanic
function test_tryGuess() public {
wordle.setupAlphabet();

// setup hidden word
wordle.hideWord("BONGO");

// test wrong guess
assertFalse(wordle.tryGuess("olive"));

// test attempt increment
uint256 attempts = wordle.getAttempts();
assertEq(attempts, 1);

// test hitmap updates
StructTypes.CharState[] memory hitmap = wordle.getHiddenWord();
StructTypes.CharState[] memory alphabet = wordle.getAlphabet();
uint256 oIdx = StringUtils.findIndex(alphabet, "o");
uint256 vIdx = StringUtils.findIndex(alphabet, "v");
assertEq(hitmap[0].state, 0);
assertEq(hitmap[1].state, 1);
assertEq(hitmap[4].state, 1);
assertEq(alphabet[oIdx].state, 1);
assertEq(alphabet[vIdx].state, 3);

// test correct guess
assertTrue(wordle.tryGuess("BONGO"));
}
}

0 comments on commit 05ed143

Please sign in to comment.