Created
September 15, 2021 21:04
-
-
Save mlegls/98e6ae8e7182570a566dbc8d3845269f 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=
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
// list of LootboxItems is the Lootbox pool | |
LootboxItem[] public pool; | |
// 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 | |
for (uint i=_index+1; i<pool.length; i++) { | |
if (pool[i].tokenType == TokenType.NULL) continue; | |
normalRates[_index][0] -= toRemove.unitWeight; | |
normalRates[_index][1] -= toRemove.unitWeight; | |
if (pool[i].pity) { | |
pityRates[_index][0] -= toRemove.unitWeight; | |
pityRates[_index][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