Skip to content
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
163 changes: 122 additions & 41 deletions contracts/contracts/MathBlocGame.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
*/
contract MathBlocGame is Ownable, ReentrancyGuard {

// ─── Structs ────────────────────────────────────────────────────────────
// --- Structs ---

struct Player {
string username;
Expand Down Expand Up @@ -40,7 +40,7 @@ contract MathBlocGame is Ownable, ReentrancyGuard {
uint256 streak;
}

// ─── State ───────────────────────────────────────────────────────────────
// --- State ---

mapping(address => Player) public players;
mapping(address => DailyActivity[]) public activityHistory;
Expand All @@ -57,7 +57,7 @@ contract MathBlocGame is Ownable, ReentrancyGuard {
uint256 public totalActiveDays; // global stat
uint256 public rewardPool; // CELO deposited by owner for rewards

// ─── Events ──────────────────────────────────────────────────────────────
// --- Events ---

event PlayerRegistered(address indexed player, string username, uint256 timestamp);
event ActivityRecorded(address indexed player, string topic, uint256 score, uint256 correct, uint256 attempts, uint256 day);
Expand All @@ -66,18 +66,18 @@ contract MathBlocGame is Ownable, ReentrancyGuard {
event CeloClaimed(address indexed player, uint256 amount);
event RewardPoolFunded(uint256 amount);

// ─── Modifiers ───────────────────────────────────────────────────────────
// --- Modifiers ---

modifier onlyRegistered() {
require(players[msg.sender].exists, "Not registered");
_;
}

// ─── Constructor ─────────────────────────────────────────────────────────
// --- Constructor ---

constructor() Ownable(msg.sender) {}

// ─── Registration ────────────────────────────────────────────────────────
// --- Registration ---

/**
* @notice Register a new player wallet with a username.
Expand All @@ -102,7 +102,7 @@ contract MathBlocGame is Ownable, ReentrancyGuard {
emit PlayerRegistered(msg.sender, username, block.timestamp);
}

// ─── Daily Activity ──────────────────────────────────────────────────────
// --- Daily Activity ---

/**
* @notice Record a game session result. Can be called once per day per player.
Expand Down Expand Up @@ -153,7 +153,7 @@ contract MathBlocGame is Ownable, ReentrancyGuard {
timestamp: block.timestamp
}));

// ── Coin rewards ──
// -- Coin rewards --
uint256 coinsToAward = DAILY_REWARD_COINS;
string memory reason = "daily activity";

Expand All @@ -176,7 +176,7 @@ contract MathBlocGame is Ownable, ReentrancyGuard {
emit CoinsEarned(msg.sender, coinsToAward, reason);
}

// ─── CELO Reward Claim ───────────────────────────────────────────────────
// --- CELO Reward Claim ---

/**
* @notice Claim CELO reward when coin balance reaches threshold.
Expand All @@ -196,7 +196,7 @@ contract MathBlocGame is Ownable, ReentrancyGuard {
emit CeloClaimed(msg.sender, CELO_REWARD_AMOUNT);
}

// ─── Owner: Fund Reward Pool ─────────────────────────────────────────────
// --- Owner: Fund Reward Pool ---

/**
* @notice Owner deposits CELO into the reward pool.
Expand All @@ -216,7 +216,7 @@ contract MathBlocGame is Ownable, ReentrancyGuard {
require(sent, "Withdraw failed");
}

// ─── Views ───────────────────────────────────────────────────────────────
// --- Views ---

function getPlayer(address addr) external view returns (Player memory) {
return players[addr];
Expand Down Expand Up @@ -244,48 +244,129 @@ contract MathBlocGame is Ownable, ReentrancyGuard {

/**
* @notice Returns top N players sorted by totalScore descending.
* Uses O(n * topN) partial selection instead of O(n²) bubble sort.
* Tie-breaking: higher streak first, then earlier registration (lower index).
* Uses a bounded insertion-sort (O(total * topN)) instead of a
* full O(n^2) bubble sort, and safely handles zero-player /
* zero-topN edge cases.
* Tie-breaking: equal totalScore is ordered by earlier registration
* (lower index in registeredPlayers comes first).
* @param topN Maximum entries to return. Clamped to registeredPlayers.length.
*/
function getLeaderboard(uint256 topN) external view returns (LeaderboardEntry[] memory) {
uint256 total = registeredPlayers.length;
if (total == 0 || topN == 0) return new LeaderboardEntry[](0);

// Edge cases: nothing to return
if (total == 0 || topN == 0) {
return new LeaderboardEntry[](0);
}
if (topN > total) topN = total;

// Copy addresses into memory
address[] memory addrs = new address[](total);
for (uint256 i = 0; i < total; i++) addrs[i] = registeredPlayers[i];

// Partial selection sort: find the top `topN` entries in O(n * topN)
for (uint256 i = 0; i < topN; i++) {
uint256 bestIdx = i;
uint256 bestScore = players[addrs[i]].totalScore;
uint256 bestStreak = players[addrs[i]].streak;

for (uint256 j = i + 1; j < total; j++) {
uint256 jScore = players[addrs[j]].totalScore;
uint256 jStreak = players[addrs[j]].streak;

// Descending by score, then by streak, then by registration order (lower index wins)
if (jScore > bestScore || (jScore == bestScore && jStreak > bestStreak)) {
bestIdx = j;
bestScore = jScore;
bestStreak = jStreak;
// Bounded top-K via insertion into a fixed-size array.
// We maintain a sorted array of length topN. For every registered
// player we find the correct insertion position and shift only the
// tail of the small array, giving O(total * topN) worst-case, which
// is O(total) when topN is a constant (e.g. 10 or 100).
address[] memory top = new address[](topN);
uint256[] memory topScores = new uint256[](topN);
uint256 filled = 0; // how many slots are occupied so far

for (uint256 i = 0; i < total; i++) {
address addr = registeredPlayers[i];
uint256 sc = players[addr].totalScore;

// Skip if the board is full and this score cannot make it in.
if (filled == topN && sc <= topScores[topN - 1]) continue;

// Find insertion position (first index whose score is < sc,
// or whose score == sc but was registered later, so earlier
// registrations stay ahead for tie-breaking).
uint256 pos = filled < topN ? filled : topN - 1;
for (uint256 k = 0; k < filled && k < topN; k++) {
if (sc > topScores[k]) {
pos = k;
break;
}
}

if (bestIdx != i) {
address tmp = addrs[i];
addrs[i] = addrs[bestIdx];
addrs[bestIdx] = tmp;
// Shift elements from pos to the right to make room.
uint256 end = filled < topN ? filled : topN - 1;
for (uint256 k = end; k > pos; k--) {
top[k] = top[k - 1];
topScores[k] = topScores[k - 1];
}

top[pos] = addr;
topScores[pos] = sc;

if (filled < topN) filled++;
}

// Build result
LeaderboardEntry[] memory board = new LeaderboardEntry[](filled);
for (uint256 i = 0; i < filled; i++) {
Player storage p = players[top[i]];
board[i] = LeaderboardEntry({
player: top[i],
username: p.username,
totalScore: p.totalScore,
streak: p.streak
});
}
return board;
}

/**
* @notice Paginated leaderboard -- returns entries ranked from offset
* to offset + limit - 1 (0-indexed). Useful for off-chain UIs
* that page through the full leaderboard.
* @param limit Maximum entries per page.
* @param offset Number of top entries to skip.
*/
function getLeaderboardPage(uint256 limit, uint256 offset)
external view returns (LeaderboardEntry[] memory)
{
uint256 total = registeredPlayers.length;
if (total == 0 || limit == 0 || offset >= total) {
return new LeaderboardEntry[](0);
}

// We need the top (offset + limit) entries, then slice [offset..].
uint256 need = offset + limit;
if (need > total) need = total;

// Reuse top-K insertion sort for need entries
address[] memory top = new address[](need);
uint256[] memory topScores = new uint256[](need);
uint256 filled = 0;

for (uint256 i = 0; i < total; i++) {
address addr = registeredPlayers[i];
uint256 sc = players[addr].totalScore;

if (filled == need && sc <= topScores[need - 1]) continue;

uint256 pos = filled < need ? filled : need - 1;
for (uint256 k = 0; k < filled && k < need; k++) {
if (sc > topScores[k]) { pos = k; break; }
}

uint256 end = filled < need ? filled : need - 1;
for (uint256 k = end; k > pos; k--) {
top[k] = top[k - 1];
topScores[k] = topScores[k - 1];
}
top[pos] = addr;
topScores[pos] = sc;
if (filled < need) filled++;
}

LeaderboardEntry[] memory board = new LeaderboardEntry[](topN);
for (uint256 i = 0; i < topN; i++) {
Player storage p = players[addrs[i]];
// Slice [offset .. filled)
uint256 start = offset < filled ? offset : filled;
uint256 count = filled > start ? filled - start : 0;
LeaderboardEntry[] memory board = new LeaderboardEntry[](count);
for (uint256 i = 0; i < count; i++) {
Player storage p = players[top[start + i]];
board[i] = LeaderboardEntry({
player: addrs[i],
player: top[start + i],
username: p.username,
totalScore: p.totalScore,
streak: p.streak
Expand Down
Loading
Loading