Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement Hashing for "Word of the Day" and Letters #179

Merged
merged 1 commit into from
Feb 27, 2025
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
32 changes: 24 additions & 8 deletions onchain/src/contracts/dewordle.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ pub mod DeWordle {
use dewordle::constants::LetterState;
use dewordle::interfaces::{IDeWordle, PlayerStat, DailyPlayerStat};

use dewordle::utils::{compare_word, is_correct_word};
use dewordle::utils::{compare_word, is_correct_hashed_word, hash_word, hash_letter};
use openzeppelin::access::accesscontrol::{AccessControlComponent};
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::introspection::src5::SRC5Component;

use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, Map, Vec, MutableVecTrait,
StoragePointerReadAccess, StoragePointerWriteAccess, Map, Vec, VecTrait, MutableVecTrait,
};

use starknet::{ContractAddress};
Expand Down Expand Up @@ -37,7 +37,7 @@ pub mod DeWordle {

#[storage]
struct Storage {
word_of_the_day: ByteArray, //TODO: hash word
word_of_the_day: felt252, //TODO: hash word
letters_in_word: Vec<felt252>, //TODO: hash letters
word_len: u8,
player_stat: Map<ContractAddress, PlayerStat>,
Expand Down Expand Up @@ -73,21 +73,36 @@ pub mod DeWordle {
fn set_daily_word(ref self: ContractState, word: ByteArray) {
self.accesscontrol.assert_only_role(ADMIN_ROLE);
let word_len = word.len();
let hash_word = hash_word(word.clone());
self.word_of_the_day.write(hash_word);
let mut i = 0;

while (i < word_len) {
self.letters_in_word.append().write(word[i].into());
let hashed_letter = hash_letter(word[i].into());
self.letters_in_word.append().write(hashed_letter);
i += 1;
};
self.word_of_the_day.write(word);

self.word_len.write(word_len.try_into().unwrap());
}

fn get_daily_word(self: @ContractState) -> ByteArray {
fn get_daily_word(self: @ContractState) -> felt252 {
self.accesscontrol.assert_only_role(ADMIN_ROLE);
self.word_of_the_day.read()
}

fn get_daily_letters(self: @ContractState) -> Array<felt252> {
self.accesscontrol.assert_only_role(ADMIN_ROLE);
let mut letter_arr = array![];
for i in 0
..self
.letters_in_word
.len() {
letter_arr.append(self.letters_in_word.at(i).read());
};
letter_arr
}

fn get_player_daily_stat(self: @ContractState, player: ContractAddress) -> DailyPlayerStat {
let daily_stat = self.daily_player_stat.read(player);

Expand Down Expand Up @@ -119,7 +134,8 @@ pub mod DeWordle {
let daily_stat = self.daily_player_stat.read(caller);
assert(!daily_stat.has_won, 'Player has already won');
assert(daily_stat.attempt_remaining > 0, 'Player has exhausted attempts');
if is_correct_word(self.get_daily_word(), guessed_word.clone()) {
let hash_guessed_word = hash_word(guessed_word.clone());
if is_correct_hashed_word(self.get_daily_word(), hash_guessed_word) {
let new_daily_stat = DailyPlayerStat {
player: caller,
attempt_remaining: daily_stat.attempt_remaining - 1,
Expand All @@ -136,7 +152,7 @@ pub mod DeWordle {
won_at_attempt: 0,
};
self.daily_player_stat.write(caller, new_daily_stat);
Option::Some(compare_word(self.get_daily_word(), guessed_word.clone()))
Option::Some(compare_word(self.get_daily_letters(), guessed_word.clone()))
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion onchain/src/interfaces.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use starknet::ContractAddress;
#[starknet::interface]
pub trait IDeWordle<TContractState> {
fn set_daily_word(ref self: TContractState, word: ByteArray);
fn get_daily_word(self: @TContractState) -> ByteArray;
fn get_daily_word(self: @TContractState) -> felt252;
fn get_daily_letters(self: @TContractState) -> Array<felt252>;

fn get_player_daily_stat(self: @TContractState, player: ContractAddress) -> DailyPlayerStat;
fn play(ref self: TContractState);
Expand Down
50 changes: 32 additions & 18 deletions onchain/src/utils.cairo
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
use core::pedersen::{pedersen};
use dewordle::constants::LetterState;

pub fn compare_word(word: ByteArray, guessed_word: ByteArray) -> Span<LetterState> {
pub fn compare_word(letters: Array<felt252>, guessed_word: ByteArray) -> Span<LetterState> {
let guessed_word_len = guessed_word.len();

assert(guessed_word_len == word.len(), 'Length does not match');
assert(guessed_word_len == letters.len(), 'Length does not match');

// Initialize tracking arrays
// Initialize tracking arrays
let mut i = 0;
let mut word_states = array![]; // Final letter states
let mut temp_states = array![]; // Temporary states to track exact matches
let mut letter_count_list = array![]; // To track letter frequency in the target word

// Count occurrences of each letter in the daily word
// Count occurrences of each letter in the daily word
while (i < guessed_word_len) {
let mut count: u32 = 0;
let mut j = 0;
while (j < guessed_word_len) {
if (word[i] == word[j]) {
if is_correct_hashed_word(*letters.at(i), *letters.at(j)) {
count += 1; // Count occurrences of the letter
}
j += 1;
Expand All @@ -27,41 +28,37 @@ pub fn compare_word(word: ByteArray, guessed_word: ByteArray) -> Span<LetterStat

i = 0;

// Identify exact matches and mark temporary state
// Identify exact matches and mark temporary state
while (i < guessed_word_len) {
if (guessed_word[i] == word[i]) {
temp_states.append(LetterState::CORRECT); // Letter is in the correct position
if (hash_letter(guessed_word[i].into()) == *letters.at(i)) {
temp_states.append(LetterState::CORRECT);
} else {
temp_states.append(LetterState::ABSENT); // Default to ABSENT for now
temp_states.append(LetterState::ABSENT);
}
i += 1;
};

i = 0;

// Identify misplaced letters
// Identify misplaced letters
while (i < guessed_word_len) {
let prev_word_states = word_states.clone();
// If the letter was marked ABSENT in the temporary states, check for misplaced
// occurrences
if (*temp_states.at(i) == LetterState::ABSENT) {
let mut j = 0;
while (j < guessed_word_len) {
if (guessed_word[i] == word[j]) {
if (hash_letter(guessed_word[i].into()) == *letters.at(j)) {
if (*temp_states.at(j) != LetterState::CORRECT) {
word_states.append(LetterState::PRESENT); // Mark as PRESENT (misplaced)
word_states.append(LetterState::PRESENT);
break;
}
}
j += 1;
};

// If no match was found, mark as ABSENT
if (prev_word_states.len() == word_states.len()) {
word_states.append(LetterState::ABSENT);
}
} else {
// If the letter was previously marked as CORRECT, preserve the state
word_states.append(LetterState::CORRECT);
}

Expand All @@ -72,6 +69,23 @@ pub fn compare_word(word: ByteArray, guessed_word: ByteArray) -> Span<LetterStat
word_states.span()
}

pub fn is_correct_word(correct_word: ByteArray, guessed_word: ByteArray) -> bool {
guessed_word == correct_word
pub fn is_correct_hashed_word(hashed_word: felt252, hashed_guess: felt252) -> bool {
hashed_word == hashed_guess
}

pub fn hash_word(word: ByteArray) -> felt252 {
let mut hash_accumulator = 0;
let word_len = word.len();

let mut i = 0;
while i < word_len {
hash_accumulator = pedersen(hash_accumulator, word[i].into());
i += 1;
};

hash_accumulator
}

pub fn hash_letter(letter: felt252) -> felt252 {
pedersen(letter, 0)
}
32 changes: 30 additions & 2 deletions onchain/tests/test_dewordle.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use dewordle::interfaces::{IDeWordleDispatcher, IDeWordleDispatcherTrait};
use dewordle::utils::{compare_word, is_correct_hashed_word, hash_word, hash_letter};
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address
Expand Down Expand Up @@ -31,7 +32,7 @@ fn test_set_daily_word() {
dewordle.set_daily_word(daily_word.clone());

// Verify that the daily word was set correctly
assert(dewordle.get_daily_word() == daily_word, 'Daily word not stored correctly');
assert(dewordle.get_daily_word() == hash_word(daily_word), 'Daily word not stored correctly');
}

#[test]
Expand Down Expand Up @@ -189,7 +190,7 @@ fn test_play_does_not_affect_other_storage() {
dewordle.play();

// Check that daily word is unchanged
assert(dewordle.get_daily_word() == "test", 'Daily word changed unexpectedly');
assert(dewordle.get_daily_word() == hash_word("test"), 'Daily word changed unexpectedly');

stop_cheat_caller_address(contract_address);
}
Expand Down Expand Up @@ -344,3 +345,30 @@ fn test_submit_guess_when_correct() {
new_daily_stat.won_at_attempt == 6 - daily_stat.attempt_remaining, 'Wrong won_at_attempt'
);
}

#[test]
fn test_get_daily_letters() {
let contract_address = deploy_contract();
let dewordle = IDeWordleDispatcher { contract_address };

start_cheat_caller_address(contract_address, OWNER());

// Define and set the daily word
let daily_word = "test";
dewordle.set_daily_word(daily_word.clone());

// Get the stored letters
let stored_letters = dewordle.get_daily_letters();
let word = array![
hash_letter('t'.into()),
hash_letter('e'.into()),
hash_letter('s'.into()),
hash_letter('t'.into())
];

for i in 0..word.len() {
assert(stored_letters[i] == word[i], 'Mismatched letter hash');
};

stop_cheat_caller_address(contract_address);
}
84 changes: 69 additions & 15 deletions onchain/tests/test_utils.cairo
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use dewordle::constants::LetterState;
use dewordle::utils::{compare_word, is_correct_word};
use dewordle::utils::{compare_word, is_correct_hashed_word, hash_word, hash_letter};

#[test]
fn test_compare_word_when_all_letters_are_correct() {
let daily_word = "test";
let daily_word = array![
hash_letter('t'.into()),
hash_letter('e'.into()),
hash_letter('s'.into()),
hash_letter('t'.into())
];

assert(
compare_word(
Expand All @@ -18,7 +23,12 @@ fn test_compare_word_when_all_letters_are_correct() {

#[test]
fn test_compare_word_when_some_letters_are_misplaced() {
let daily_word = "test";
let daily_word = array![
hash_letter('t'.into()),
hash_letter('e'.into()),
hash_letter('s'.into()),
hash_letter('t'.into())
];

assert(
compare_word(
Expand All @@ -33,7 +43,12 @@ fn test_compare_word_when_some_letters_are_misplaced() {

#[test]
fn test_compare_word_when_some_letters_are_absent() {
let daily_word = "test";
let daily_word = array![
hash_letter('t'.into()),
hash_letter('e'.into()),
hash_letter('s'.into()),
hash_letter('t'.into())
];

assert(
compare_word(
Expand All @@ -49,14 +64,26 @@ fn test_compare_word_when_some_letters_are_absent() {
#[test]
#[should_panic(expected: 'Length does not match')]
fn test_compare_word_panics() {
let daily_word = "slept";
let daily_word = array![
hash_letter('s'.into()),
hash_letter('l'.into()),
hash_letter('e'.into()),
hash_letter('p'.into()),
hash_letter('t'.into())
];

compare_word(daily_word, "sweeps");
}

#[test]
fn test_compare_word_when_some_letters_are_repeated() {
let daily_word = "slept";
let daily_word = array![
hash_letter('s'.into()),
hash_letter('l'.into()),
hash_letter('e'.into()),
hash_letter('p'.into()),
hash_letter('t'.into())
];

assert(
compare_word(
Expand All @@ -72,7 +99,12 @@ fn test_compare_word_when_some_letters_are_repeated() {
'Word not compared correctly'
);

let daily_word = "test";
let daily_word = array![
hash_letter('t'.into()),
hash_letter('e'.into()),
hash_letter('s'.into()),
hash_letter('t'.into())
];

assert(
compare_word(
Expand All @@ -86,16 +118,38 @@ fn test_compare_word_when_some_letters_are_repeated() {
}

#[test]
fn test_is_correct_word() {
let correct_word = "hello";
fn test_is_correct_hashed_word() {
let correct_word = hash_word("hello");

// Test case 1: Correct guess
let guessed_word = "hello";
let result = is_correct_word(correct_word.clone(), guessed_word.clone());
assert(result, 'Test case 1 failed');
let guessed_word = hash_word("hello");
assert(is_correct_hashed_word(correct_word, guessed_word), 'Test case 1 failed');

// Test case 2: Incorrect guess
let guessed_word = "world";
let result = is_correct_word(correct_word, guessed_word.clone());
assert(!result, 'Test case 2 failed');
let guessed_word = hash_word("world");
assert(!is_correct_hashed_word(correct_word, guessed_word), 'Test case 2 failed');
}

#[test]
fn test_hash_letter() {
let letter_a = 'a'.into();
let letter_b = 'b'.into();

let hash_a = hash_letter(letter_a.clone()); // Clone before passing
let hash_b = hash_letter(letter_b.clone());

assert!(hash_a != hash_b, "Different letters");
assert!(hash_a == hash_letter(letter_a), "Same letter");
}

#[test]
fn test_hash_word() {
let word1 = "hello";
let word2 = "world";

let hash1 = hash_word(word1.clone());
let hash2 = hash_word(word2.clone());

assert!(hash1 != hash2, "Different words");
assert!(hash1 == hash_word(word1), "Same word");
}