Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

Steller-Mole-Contract

The StarkMole Soroban contract handles four things: session management (start/end game rounds tied to a player's wallet), hit registration (mole clicks scored by type — standard, fast, or golden), daily leaderboards (on-chain ranked scores that reset every 24 hours via block timestamp), and reward distribution (a funded pool that top players claim directly from the contract). All state lives on-chain — no server can alter scores or intercept rewards.

// StarkMole Game Contract — Built on Stellar (Soroban) // File: contracts/starkmole_game.rs

#![no_std] use soroban_sdk::{ contract, contractimpl, contracttype, symbol_short, Address, Env, Map, Symbol, Vec, token, contracterror, };

// ─── Data Types ───────────────────────────────────────────────────────────────

#[contracttype] #[derive(Clone)] pub struct PlayerSession { pub player: Address, pub score: u64, pub start_time: u64, pub active: bool, pub day_id: u64, }

#[contracttype] #[derive(Clone)] pub struct LeaderboardEntry { pub player: Address, pub score: u64, pub rank: u32, }

#[contracttype] pub enum DataKey { Session(Address), DailyScore(Address, u64), DailyLeaderboard(u64), RewardPool(u64), RewardClaimed(Address, u64), Config, Admin, RewardToken, }

#[contracttype] #[derive(Clone)] pub struct Config { pub session_duration: u64, pub max_score: u64, pub top_n: u32, }

// ─── Errors ───────────────────────────────────────────────────────────────────

#[contracterror] #[derive(Copy, Clone)] pub enum GameError { SessionAlreadyActive = 1, NoActiveSession = 2, SessionExpired = 3, Unauthorized = 4, AlreadyClaimed = 5, NoRewardPool = 6, InvalidMole = 7, }

// ─── Contract ────────────────────────────────────────────────────────────────

#[contract] pub struct StarkMoleGame;

#[contractimpl] impl StarkMoleGame {

// ── Initialize ─────────────────────────────────────────────────────────

pub fn initialize(
    env: Env,
    admin: Address,
    reward_token: Address,
) {
    admin.require_auth();
    env.storage().instance().set(&DataKey::Admin, &admin);
    env.storage().instance().set(&DataKey::RewardToken, &reward_token);
    env.storage().instance().set(&DataKey::Config, &Config {
        session_duration: 60,   // 60 seconds per session
        max_score: 10_000,
        top_n: 100,
    });
}

// ── Game Session ───────────────────────────────────────────────────────

/// Start a new game session
pub fn start_session(env: Env, player: Address) -> Result<(), GameError> {
    player.require_auth();

    let key = DataKey::Session(player.clone());
    if let Some(session) = env.storage().temporary().get::<_, PlayerSession>(&key) {
        if session.active {
            return Err(GameError::SessionAlreadyActive);
        }
    }

    let now = env.ledger().timestamp();
    let day_id = now / 86_400;

    let session = PlayerSession {
        player: player.clone(),
        score: 0,
        start_time: now,
        active: true,
        day_id,
    };

    env.storage().temporary().set(&key, &session);
    env.events().publish(
        (symbol_short!("session"), symbol_short!("start")),
        (player, day_id),
    );

    Ok(())
}

/// Register a mole hit and award points
pub fn register_hit(
    env: Env,
    player: Address,
    mole_type: u32,
) -> Result<u64, GameError> {
    player.require_auth();

    let key = DataKey::Session(player.clone());
    let mut session: PlayerSession = env
        .storage()
        .temporary()
        .get(&key)
        .ok_or(GameError::NoActiveSession)?;

    if !session.active {
        return Err(GameError::NoActiveSession);
    }

    let config: Config = env.storage().instance().get(&DataKey::Config).unwrap();
    let now = env.ledger().timestamp();
    if now - session.start_time > config.session_duration {
        session.active = false;
        env.storage().temporary().set(&key, &session);
        return Err(GameError::SessionExpired);
    }

    // Points per mole type
    let points: u64 = match mole_type {
        1 => 10,   // Standard mole
        2 => 25,   // Fast mole
        3 => 50,   // Golden mole
        _ => return Err(GameError::InvalidMole),
    };

    let new_score = (session.score + points).min(config.max_score);
    session.score = new_score;
    env.storage().temporary().set(&key, &session);

    env.events().publish(
        (symbol_short!("mole"), symbol_short!("hit")),
        (player, mole_type, new_score),
    );

    Ok(new_score)
}

/// End session and commit score to leaderboard
pub fn end_session(env: Env, player: Address) -> Result<u64, GameError> {
    player.require_auth();

    let key = DataKey::Session(player.clone());
    let mut session: PlayerSession = env
        .storage()
        .temporary()
        .get(&key)
        .ok_or(GameError::NoActiveSession)?;

    if !session.active {
        return Err(GameError::NoActiveSession);
    }

    session.active = false;
    env.storage().temporary().set(&key, &session);

    let final_score = session.score;
    let day_id = session.day_id;

    // Update personal best for the day
    let score_key = DataKey::DailyScore(player.clone(), day_id);
    let existing: u64 = env.storage().persistent().get(&score_key).unwrap_or(0);
    if final_score > existing {
        env.storage().persistent().set(&score_key, &final_score);
        Self::_update_leaderboard(&env, player.clone(), final_score, day_id);
    }

    env.events().publish(
        (symbol_short!("session"), symbol_short!("end")),
        (player, final_score, day_id),
    );

    Ok(final_score)
}

// ── Leaderboard ────────────────────────────────────────────────────────

/// Get today's leaderboard (top N players)
pub fn get_leaderboard(env: Env, day_id: u64) -> Vec<LeaderboardEntry> {
    let key = DataKey::DailyLeaderboard(day_id);
    env.storage()
        .persistent()
        .get(&key)
        .unwrap_or(Vec::new(&env))
}

/// Get a player's best score for a specific day
pub fn get_player_score(env: Env, player: Address, day_id: u64) -> u64 {
    let key = DataKey::DailyScore(player, day_id);
    env.storage().persistent().get(&key).unwrap_or(0)
}

/// Get current day ID
pub fn get_day_id(env: Env) -> u64 {
    env.ledger().timestamp() / 86_400
}

// ── Rewards ────────────────────────────────────────────────────────────

/// Fund the daily reward pool (admin or sponsor)
pub fn fund_pool(env: Env, funder: Address, day_id: u64, amount: i128) {
    funder.require_auth();

    let token_address: Address = env
        .storage()
        .instance()
        .get(&DataKey::RewardToken)
        .unwrap();

    let contract_address = env.current_contract_address();
    token::Client::new(&env, &token_address)
        .transfer(&funder, &contract_address, &amount);

    let pool_key = DataKey::RewardPool(day_id);
    let current: i128 = env.storage().persistent().get(&pool_key).unwrap_or(0);
    env.storage().persistent().set(&pool_key, &(current + amount));

    env.events().publish(
        (symbol_short!("pool"), symbol_short!("fund")),
        (funder, day_id, amount),
    );
}

/// Claim reward for a completed day (top players only)
pub fn claim_reward(
    env: Env,
    player: Address,
    day_id: u64,
    amount: i128,
) -> Result<(), GameError> {
    player.require_auth();

    let claim_key = DataKey::RewardClaimed(player.clone(), day_id);
    if env.storage().persistent().get::<_, bool>(&claim_key).unwrap_or(false) {
        return Err(GameError::AlreadyClaimed);
    }

    let pool_key = DataKey::RewardPool(day_id);
    let pool: i128 = env.storage().persistent().get(&pool_key).unwrap_or(0);
    if pool <= 0 {
        return Err(GameError::NoRewardPool);
    }

    // Mark claimed
    env.storage().persistent().set(&claim_key, &true);

    // Transfer reward token to player
    let token_address: Address = env
        .storage()
        .instance()
        .get(&DataKey::RewardToken)
        .unwrap();

    let contract_address = env.current_contract_address();
    token::Client::new(&env, &token_address)
        .transfer(&contract_address, &player, &amount);

    env.events().publish(
        (symbol_short!("reward"), symbol_short!("claim")),
        (player, day_id, amount),
    );

    Ok(())
}

// ── Admin ──────────────────────────────────────────────────────────────

/// Update game config — admin only
pub fn update_config(
    env: Env,
    caller: Address,
    session_duration: u64,
    max_score: u64,
) -> Result<(), GameError> {
    caller.require_auth();
    let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
    if caller != admin {
        return Err(GameError::Unauthorized);
    }

    let config = Config { session_duration, max_score, top_n: 100 };
    env.storage().instance().set(&DataKey::Config, &config);
    Ok(())
}

// ── Internal ───────────────────────────────────────────────────────────

fn _update_leaderboard(env: &Env, player: Address, score: u64, day_id: u64) {
    let key = DataKey::DailyLeaderboard(day_id);
    let mut board: Vec<LeaderboardEntry> = env
        .storage()
        .persistent()
        .get(&key)
        .unwrap_or(Vec::new(env));

    // Remove existing entry for player if present
    let mut updated: Vec<LeaderboardEntry> = Vec::new(env);
    for entry in board.iter() {
        if entry.player != player {
            updated.push_back(entry);
        }
    }

    updated.push_back(LeaderboardEntry {
        player,
        score,
        rank: 0,
    });

    env.storage().persistent().set(&key, &updated);
}

}

Scarb.toml (Stellar/Soroban config): toml[package] name = "starkmole_game" version = "0.1.0" edition = "2023_11"

[lib]

[dependencies] soroban-sdk = { version = "21.0.0" }

[dev-dependencies] soroban-sdk = { version = "21.0.0", features = ["testutils"] }

[profile.release] opt-level = "z" overflow-checks = true

[profile.test] opt-level = "0"

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors