Skip to content

Commit d28a13e

Browse files
committed
feat: implement daily gauntlet entry limit system
- Add configurable daily entry limit (default 10 per player) - Add ETH payment option to reset daily limit (default 0.001 ETH) - Automatic reset at midnight UTC via day number calculation - Gas-efficient mapping approach without history storage - Admin functions to adjust limit and reset cost - Comprehensive test coverage for all scenarios
1 parent ba0f6a9 commit d28a13e

2 files changed

Lines changed: 362 additions & 2 deletions

File tree

src/game/modes/GauntletGame.sol

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ error InvalidFutureBlocks(uint256 blocks);
5151
error CannotRecoverYet();
5252
error NoDefaultPlayersAvailable();
5353
error InvalidBlockhash();
54+
error DailyLimitExceeded(uint8 currentRuns, uint8 limit);
55+
error InsufficientResetFee();
5456

5557
//==============================================================//
5658
// HEAVY HELMS //
@@ -196,6 +198,14 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
196198
/// @notice If status is IN_GAUNTLET, maps player ID to the `gauntletId` they are participating in.
197199
mapping(uint32 => uint256) public playerCurrentGauntlet;
198200

201+
// --- Daily Limit System ---
202+
/// @notice Maximum gauntlet entries per player per day
203+
uint8 public dailyGauntletLimit = 10;
204+
/// @notice Cost in ETH to reset daily limit for a player
205+
uint256 public dailyResetCost = 0.001 ether;
206+
/// @notice Maps player ID to day number to run count (playerId => dayNumber => runCount)
207+
mapping(uint32 => mapping(uint256 => uint8)) private _playerDailyRuns;
208+
199209
//==============================================================//
200210
// EVENTS //
201211
//==============================================================//
@@ -244,6 +254,12 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
244254
event QueuePartiallyCleared(uint256 cleared, uint256 remaining);
245255
/// @notice Emitted when the game enabled state is updated.
246256
event GameEnabledUpdated(bool enabled);
257+
/// @notice Emitted when a player's daily gauntlet limit is reset via ETH payment
258+
event DailyLimitReset(uint32 indexed playerId, address indexed payer, uint256 dayNumber, uint256 amountPaid);
259+
/// @notice Emitted when the daily reset cost is updated
260+
event DailyResetCostUpdated(uint256 oldCost, uint256 newCost);
261+
/// @notice Emitted when the daily gauntlet limit is updated
262+
event DailyGauntletLimitUpdated(uint8 oldLimit, uint8 newLimit);
247263
// Inherited from BaseGame: event CombatResult(bytes32 indexed player1Data, bytes32 indexed player2Data, uint32 winnerId, bytes combatLog);
248264

249265
//==============================================================//
@@ -295,14 +311,21 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
295311

296312
/// @notice Allows a player owner to join the Gauntlet queue with a specific loadout.
297313
/// @param loadout The player's chosen skin and stance for the potential Gauntlet.
298-
/// @dev Validates player status, ownership, retirement status, and skin requirements.
314+
/// @dev Validates player status, ownership, retirement status, skin requirements, and daily limits.
299315
function queueForGauntlet(Fighter.PlayerLoadout calldata loadout) external whenGameEnabled nonReentrant {
300316
// Checks
301317
if (playerStatus[loadout.playerId] != PlayerStatus.NONE) revert AlreadyInQueue();
302318
address owner = playerContract.getPlayerOwner(loadout.playerId);
303319
if (msg.sender != owner) revert CallerNotPlayerOwner();
304320
if (playerContract.isPlayerRetired(loadout.playerId)) revert PlayerIsRetired();
305321

322+
// Check daily limit
323+
uint256 today = _getDayNumber();
324+
uint8 currentRuns = _playerDailyRuns[loadout.playerId][today];
325+
if (currentRuns >= dailyGauntletLimit) {
326+
revert DailyLimitExceeded(currentRuns, dailyGauntletLimit);
327+
}
328+
306329
// Validate player is in correct level bracket
307330
_validatePlayerLevel(loadout.playerId);
308331

@@ -328,6 +351,9 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
328351
playerIndexInQueue[playerId] = queueIndex.length; // 1-based index
329352
playerStatus[playerId] = PlayerStatus.QUEUED;
330353

354+
// Increment daily run counter (user pays gas for state change)
355+
_playerDailyRuns[playerId][today]++;
356+
331357
// Interactions (Event Emission)
332358
emit PlayerQueued(playerId, queueIndex.length);
333359
}
@@ -365,6 +391,31 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
365391
return registrationQueue[playerId];
366392
}
367393

394+
/// @notice Gets the current daily run count for a player
395+
/// @param playerId The ID of the player to check
396+
/// @return The number of gauntlet runs today for this player
397+
function getDailyRunCount(uint32 playerId) external view returns (uint8) {
398+
uint256 today = _getDayNumber();
399+
return _playerDailyRuns[playerId][today];
400+
}
401+
402+
/// @notice Resets the daily gauntlet limit for a player by paying ETH
403+
/// @param playerId The ID of the player to reset limit for
404+
/// @dev Player owner pays ETH to reset their daily gauntlet entry count to 0
405+
function resetDailyLimit(uint32 playerId) external payable nonReentrant {
406+
// Checks
407+
address owner = playerContract.getPlayerOwner(playerId);
408+
if (msg.sender != owner) revert CallerNotPlayerOwner();
409+
if (msg.value < dailyResetCost) revert InsufficientResetFee();
410+
411+
// Effects
412+
uint256 today = _getDayNumber();
413+
_playerDailyRuns[playerId][today] = 0;
414+
415+
// Interactions
416+
emit DailyLimitReset(playerId, msg.sender, today, msg.value);
417+
}
418+
368419
//==============================================================//
369420
// GAUNTLET LIFECYCLE (Triggered Externally) //
370421
//==============================================================//
@@ -1045,6 +1096,29 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
10451096
emit MinTimeBetweenGauntletsSet(newMinTime);
10461097
}
10471098

1099+
/// @notice Sets the cost for resetting daily gauntlet limits
1100+
/// @param newCost The new cost in ETH for daily limit resets
1101+
function setDailyResetCost(uint256 newCost) external onlyOwner {
1102+
uint256 oldCost = dailyResetCost;
1103+
dailyResetCost = newCost;
1104+
emit DailyResetCostUpdated(oldCost, newCost);
1105+
}
1106+
1107+
/// @notice Withdraws accumulated daily reset fees to the owner
1108+
/// @dev Only callable by contract owner
1109+
function withdrawFees() external onlyOwner {
1110+
SafeTransferLib.safeTransferETH(owner, address(this).balance);
1111+
}
1112+
1113+
/// @notice Sets the daily gauntlet entry limit per player
1114+
/// @param newLimit The new daily entry limit
1115+
function setDailyGauntletLimit(uint8 newLimit) external onlyOwner {
1116+
if (newLimit == 0) revert InvalidGauntletSize(newLimit);
1117+
uint8 oldLimit = dailyGauntletLimit;
1118+
dailyGauntletLimit = newLimit;
1119+
emit DailyGauntletLimitUpdated(oldLimit, newLimit);
1120+
}
1121+
10481122
//==============================================================//
10491123
// INTERNAL FUNCTIONS //
10501124
//==============================================================//
@@ -1086,6 +1160,12 @@ contract GauntletGame is BaseGame, ReentrancyGuard {
10861160
return uint256(keccak256(abi.encodePacked(baseHash, block.timestamp, block.number, gasleft(), tx.origin)));
10871161
}
10881162

1163+
/// @notice Calculates the current day number since Unix epoch
1164+
/// @return Day number (resets at midnight UTC)
1165+
function _getDayNumber() private view returns (uint256) {
1166+
return block.timestamp / 1 days;
1167+
}
1168+
10891169
// --- Helper Functions ---
10901170

10911171
/// @notice Loads combat data for a default player.

0 commit comments

Comments
 (0)