Skip to content

Instantly share code, notes, and snippets.

@mlegls
Created September 15, 2021 21:28
Show Gist options
  • Save mlegls/e6d9b98de95798bf939c8b15ff6b65e6 to your computer and use it in GitHub Desktop.
Save mlegls/e6d9b98de95798bf939c8b15ff6b65e6 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.7+commit.e28d00a7.js&optimize=false&runs=200&gist=
pragma solidity >=0.7.0 <0.9.0;
/**
* @title Lootbox
* @dev Matrix lootbox contract
*/
contract Lootbox {
// NULL is for depleted items; fungible tokens not yet implemented
enum TokenType{ NULL, NONFUNGIBLE, SEMIFUNGIBLE, FUNGIBLE }
// pity indicates whether an item is in the pity pool
struct LootboxItem {
TokenType tokenType;
string name;
uint32 unitWeight;
bool pity;
}
// maps id to rate ranges
// width of range, inclusive. is the total weight (i.e. unit weight for NFTs, unit weight * qty for SFTs)
// open function generates a random int and checks which range it falls in
mapping(uint => uint32[2]) public normalRates;
mapping(uint => uint32[2]) public pityRates;
mapping(address => uint) public pityCounter;
// list of LootboxItems is the Lootbox pool
LootboxItem[] public pool;
uint pityNumber = 9; // guaranteed pity roll every this often
// random integer between 1 and rangeEnd, inclusive
function _fakeRandint(uint rangeEnd) private view returns (uint randint_) {
randint_ = (uint(keccak256(abi.encodePacked(block.timestamp, block.difficulty))) % rangeEnd) + 1;
}
function _rangeEnd() public view returns (uint rangeEnd_) {
if (pool.length == 0) return 0;
for (uint i=pool.length-1; i>=0; i--) {
if (i==0 && pool[0].tokenType == TokenType.NULL) return 0;
if (pool[i].tokenType == TokenType.NULL) continue;
rangeEnd_ = normalRates[i][1];
break;
}
}
function _pityRangeEnd() public view returns (uint rangeEnd_) {
if (pool.length == 0) return 0;
for (uint i=pool.length-1; i>=0; i--) {
if (i==0 && pool[0].tokenType == TokenType.NULL) return 0;
if (i==0 && !pool[0].pity) return 0;
if (pool[i].tokenType == TokenType.NULL) continue;
if (!pool[i].pity) continue;
rangeEnd_ = pityRates[i][1];
break;
}
}
function addNft(string memory _name, uint32 _weight, bool _pity) public {
uint32 rangeEnd = uint32(_rangeEnd()+1);
uint32 pityRangeEnd = uint32(_pityRangeEnd()+1);
LootboxItem memory item = LootboxItem({tokenType: TokenType.NONFUNGIBLE, name: _name, unitWeight: _weight, pity: _pity});
pool.push(item);
normalRates[pool.length-1] = [rangeEnd, rangeEnd+_weight-1];
if (_pity) {
pityRates[pool.length-1] = [pityRangeEnd, pityRangeEnd+_weight-1];
}
}
function addNft(string memory _name, uint32 _weight) public {
addNft(_name, _weight, false);
}
function addSft(string memory _name, uint32 _weight, uint32 _qty, bool _pity) public {
uint32 rangeEnd = uint32(_rangeEnd()+1);
uint32 pityRangeEnd = uint32(_pityRangeEnd()+1);
LootboxItem memory item = LootboxItem({tokenType: TokenType.SEMIFUNGIBLE, name: _name, unitWeight: _weight, pity: _pity});
pool.push(item);
normalRates[pool.length-1] = [rangeEnd, rangeEnd+(_weight*_qty)-1];
if (_pity) {
pityRates[pool.length-1] = [pityRangeEnd, pityRangeEnd+(_weight*_qty)-1];
}
}
function addSft(string memory _name, uint32 _weight, uint32 _qty) public {
addSft(_name, _weight, _qty, false);
}
function _removeItem(uint _index) private {
LootboxItem storage toRemove = pool[_index];
// change ranges of all other items first because unitWeight may be deleted
if (pool.length>0 && _index<pool.length-1) { // if removed item is not the last item
for (uint i=_index+1; i<pool.length; i++) {
if (pool[i].tokenType == TokenType.NULL) continue;
normalRates[i][0] -= toRemove.unitWeight;
normalRates[i][1] -= toRemove.unitWeight;
if (pool[i].pity) {
pityRates[i][0] -= toRemove.unitWeight;
pityRates[i][1] -= toRemove.unitWeight;
}
}
}
// remove relevant item
if (toRemove.tokenType == TokenType.SEMIFUNGIBLE) {
if (normalRates[_index][1] >= normalRates[_index][0] + toRemove.unitWeight) {
normalRates[_index][1] -= toRemove.unitWeight;
if (toRemove.pity) pityRates[_index][1] -= toRemove.unitWeight;
} else {
pool[_index].tokenType = TokenType.NULL;
}
} else if (toRemove.tokenType == TokenType.NONFUNGIBLE) {
pool[_index].tokenType = TokenType.NULL;
} else revert("Trying to remove null item");
}
function _open() private returns (LootboxItem memory item_) {
uint rangeEnd = _rangeEnd();
if (rangeEnd == 0) revert("Box empty");
uint32 randIndex = uint32(_fakeRandint(rangeEnd));
for (uint i=0; i<pool.length; i++) {
if (pool[i].tokenType == TokenType.NULL) continue;
if (randIndex <= normalRates[i][1]) {
item_ = pool[i];
_removeItem(i);
break;
}
}
}
function _pityOpen() private returns (LootboxItem memory item_) {
uint rangeEnd = _pityRangeEnd();
if (rangeEnd == 0) revert("Pity pool empty");
uint32 randIndex = uint32(_fakeRandint(rangeEnd));
for (uint i=0; i<pool.length; i++) {
if (pool[i].tokenType == TokenType.NULL) continue;
if (!pool[i].pity) continue;
if (randIndex <= pityRates[i][1]) {
item_ = pool[i];
_removeItem(i);
break;
}
}
}
function open() public returns (LootboxItem memory item_) {
item_ = _open();
}
function pityOpen() public returns (LootboxItem memory item_) {
item_ = _pityOpen();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment