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 Dynamic Daily Reset for DailyPlayerStat #268

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
52 changes: 43 additions & 9 deletions onchain/src/contracts/dewordle.cairo
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
#[starknet::contract]
pub mod DeWordle {
use dewordle::constants::LetterState;
use dewordle::interfaces::{IDeWordle, PlayerStat, DailyPlayerStat};
use dewordle::interfaces::{DailyPlayerStat, IDeWordle, PlayerStat};

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

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

use starknet::{ContractAddress, get_block_timestamp};
Expand Down Expand Up @@ -64,6 +64,7 @@ pub mod DeWordle {
#[flat]
AccessControlEvent: AccessControlComponent::Event,
DayUpdated: DayUpdated,
PlayerStatsReset: PlayerStatsReset,
}

#[constructor]
Expand All @@ -80,6 +81,12 @@ pub mod DeWordle {
new_end_of_day: u64,
}

#[derive(Drop, starknet::Event)]
struct PlayerStatsReset {
player: ContractAddress,
timestamp: u64,
}

#[abi(embed_v0)]
impl DeWordleImpl of IDeWordle<ContractState> {
fn set_daily_word(ref self: ContractState, word: ByteArray) {
Expand All @@ -104,7 +111,6 @@ pub mod DeWordle {
}

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
Expand All @@ -121,7 +127,11 @@ pub mod DeWordle {
// A player without a stat will have 0 attempts remaining
if daily_stat.attempt_remaining == 0 {
DailyPlayerStat {
player: player, attempt_remaining: 6, has_won: false, won_at_attempt: 0,
player: player,
attempt_remaining: 6,
has_won: false,
won_at_attempt: 0,
last_attempt_timestamp: 0,
}
} else {
daily_stat
Expand All @@ -130,29 +140,52 @@ pub mod DeWordle {

fn play(ref self: ContractState) {
let caller: ContractAddress = starknet::get_caller_address();
let current_timestamp = get_block_timestamp();

let new_daily_stat = DailyPlayerStat {
player: caller, attempt_remaining: 6, has_won: false, won_at_attempt: 0,
player: caller,
attempt_remaining: 6,
has_won: false,
won_at_attempt: 0,
last_attempt_timestamp: current_timestamp,
};

self.daily_player_stat.write(caller, new_daily_stat);
}

fn submit_guess(
ref self: ContractState, guessed_word: ByteArray
ref self: ContractState, guessed_word: ByteArray,
) -> Option<Span<LetterState>> {
assert(guessed_word.len() == self.word_len.read().into(), 'Length does not match');
let caller = starknet::get_caller_address();
let daily_stat = self.daily_player_stat.read(caller);
let mut daily_stat = self.daily_player_stat.read(caller);
let current_timestamp = get_block_timestamp();

if current_timestamp >= self.end_of_day_timestamp.read() {
let new_end_of_day = get_next_midnight_timestamp();
self.end_of_day_timestamp.write(new_end_of_day);
self.emit(DayUpdated { new_end_of_day });

let new_daily_stat = DailyPlayerStat {
player: caller,
attempt_remaining: 6,
has_won: false,
won_at_attempt: 0,
last_attempt_timestamp: current_timestamp,
};
self.daily_player_stat.write(caller, new_daily_stat);
}
assert(!daily_stat.has_won, 'Player has already won');
assert(daily_stat.attempt_remaining > 0, 'Player has exhausted attempts');

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,
has_won: true,
won_at_attempt: 6 - daily_stat.attempt_remaining,
won_at_attempt: 7 - (daily_stat.attempt_remaining - 1),
last_attempt_timestamp: current_timestamp,
};
self.daily_player_stat.write(caller, new_daily_stat);
Option::None
Expand All @@ -162,6 +195,7 @@ pub mod DeWordle {
attempt_remaining: daily_stat.attempt_remaining - 1,
has_won: false,
won_at_attempt: 0,
last_attempt_timestamp: current_timestamp,
};
self.daily_player_stat.write(caller, new_daily_stat);
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 @@ -25,10 +25,11 @@ pub struct PlayerStat {
pub max_streak: u64 //TODO: Impl streaking logic
}

#[derive(Drop, Serde, starknet::Store)]
#[derive(Drop, Serde, starknet::Store, Debug)]
pub struct DailyPlayerStat {
pub player: ContractAddress,
pub attempt_remaining: u8,
pub has_won: bool,
pub won_at_attempt: u8,
pub last_attempt_timestamp: u64,
}
103 changes: 95 additions & 8 deletions onchain/tests/test_dewordle.cairo
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use dewordle::interfaces::{IDeWordleDispatcher, IDeWordleDispatcherTrait};
use dewordle::utils::{hash_word, hash_letter};
use dewordle::utils::{hash_letter, hash_word};
use snforge_std::{
declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address,
stop_cheat_caller_address, cheat_block_timestamp, CheatSpan,
CheatSpan, ContractClassTrait, DeclareResultTrait, cheat_block_timestamp, declare,
start_cheat_caller_address, stop_cheat_caller_address,
};
use starknet::ContractAddress;

Expand Down Expand Up @@ -302,7 +302,7 @@ fn test_submit_guess_when_incorrect() {
assert(new_daily_stat.player == OWNER(), 'Wrong player address');
assert(
new_daily_stat.attempt_remaining == daily_stat.attempt_remaining - 1,
'Wrongattempt_remaining'
'Wrongattempt_remaining',
);
assert(!new_daily_stat.has_won, 'has_won should be false');
assert(new_daily_stat.won_at_attempt == 0, 'won_at_attempt should be 0');
Expand Down Expand Up @@ -338,11 +338,12 @@ fn test_submit_guess_when_correct() {
assert(new_daily_stat.player == OWNER(), 'Wrong player address');
assert(
new_daily_stat.attempt_remaining == daily_stat.attempt_remaining - 1,
'Wrong attempt_remaining'
'Wrong attempt_remaining',
);
assert(new_daily_stat.has_won, 'has_won should be true');
assert(
new_daily_stat.won_at_attempt == 6 - daily_stat.attempt_remaining, 'Wrong won_at_attempt'
new_daily_stat.won_at_attempt == 7 - (daily_stat.attempt_remaining - 1),
'Wrong won_at_attempt',
);
}

Expand All @@ -363,7 +364,7 @@ fn test_get_daily_letters() {
hash_letter('t'.into()),
hash_letter('e'.into()),
hash_letter('s'.into()),
hash_letter('t'.into())
hash_letter('t'.into()),
];

for i in 0..word.len() {
Expand All @@ -384,7 +385,7 @@ fn test_update_end_of_day() {

// Fast forward time by one day
cheat_block_timestamp(
contract_address, starknet::get_block_timestamp() + 86400, CheatSpan::TargetCalls(1)
contract_address, starknet::get_block_timestamp() + 86400, CheatSpan::TargetCalls(1),
);
dewordle.update_end_of_day();

Expand All @@ -394,3 +395,89 @@ fn test_update_end_of_day() {

stop_cheat_caller_address(contract_address);
}

#[test]
fn test_submit_guess_with_time_reset() {
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 = "tests";
dewordle.set_daily_word(daily_word.clone());

// Play
dewordle.play();

// Make a guess
match dewordle.submit_guess("wrong") {
Option::None => panic!("ERROR"),
Option::Some(_) => (),
}

// Check attempts remaining
let daily_stat = dewordle.get_player_daily_stat(OWNER());

assert(daily_stat.attempt_remaining == 5, 'Should have 5 attempts left');

// Fast forward time by more than a day
let end_of_day_timestamp = dewordle.get_end_of_day_timestamp();
cheat_block_timestamp(contract_address, end_of_day_timestamp + 1, CheatSpan::TargetCalls(1));

// Make another guess - should reset attempts due to time passing
match dewordle.submit_guess("right") {
Option::None => panic!("ERROR"),
Option::Some(_) => (),
}

// Check that attempts reset to 5 (6-1 for the new guess)
let new_daily_stat = dewordle.get_player_daily_stat(OWNER());
assert(new_daily_stat.attempt_remaining == 4, 'Should have 4 attempts again');

stop_cheat_caller_address(contract_address);
}

#[test]
#[should_panic(expected: 'Caller is missing role')]
fn test_access_control_unauthorized_set_daily_word() {
let contract_address = deploy_contract();
let dewordle = IDeWordleDispatcher { contract_address };

// Try to set daily word as non-owner
let non_owner = starknet::contract_address_const::<0x123>();
start_cheat_caller_address(contract_address, non_owner);

// This should fail due to access control
dewordle.set_daily_word("test");
}


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

// Get the end of day timestamp
let timestamp = dewordle.get_end_of_day_timestamp();

// Verify it's greater than the current timestamp
assert(timestamp > starknet::get_block_timestamp(), 'Invalid end of day timestamp');
}

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

// Get initial timestamp
let initial_timestamp = dewordle.get_end_of_day_timestamp();

// Call update before the day ends
dewordle.update_end_of_day();

// Verify timestamp hasn't changed
let updated_timestamp = dewordle.get_end_of_day_timestamp();
assert(updated_timestamp == initial_timestamp, 'Timestamp should not change');
}