How about a dice game on Steem-Engine?

in #steem-enginelast year (edited)

How about a dice game on Steem-Engine?

In the latest version of the Steem Smart Contracts, which is the software that powers the Steem-Engine, a deterministic random generator has been added, it will be used later on in the consensus layer but for now let's see what we could do with this new feature. I've seen several times in the steem-engine discord that people were asking about how to build a smart contract for a dice game so I took a few hours to build a basic example of what could be a dice contract on Steem-Engine.

As you're going to see it's pretty easy to build a smart contract on this platform, no need to be a c++ nor a solidity developer, just stay as you are, a STEEM developer!

Alright, let's dig in but first I'll throw the code so that you can look at it by yourself and try to understand what it does, most of the core functions of a smart contract are built-in the Steem Smart Contracts tool so that'll make our lives way much easier!

ok, here's the code:

const STEEM_PEGGED_SYMBOL = 'STEEMP';
const CONTRACT_NAME = 'dice';

actions.createSSC = async (payload) => {
    await api.db.createTable('params');

    // initialize a few parameters for the contract
    const params = {};
    params.houseEdge = "0.01";
    params.minBet = "0.1";
    params.maxBet = "100";
    await api.db.insert('params', params);
}

actions.roll = async (payload) => {
    // get the action parameters
    const { roll, amount } = payload;

    // check the action parameters
    if (api.assert(roll && Number.isInteger(roll) && roll >= 2 && roll <= 96, 'roll must be an integer and must be between 2 and 96')
        && api.assert(amount && typeof amount === 'string' && api.BigNumber(amount).dp() <= 3 && api.BigNumber(amount).gt(0), 'invalid amount')) {

        // get the contract parameters
        const params = await api.db.findOne('params', {});

        // check that the amount bet is in thr allowed range
        if (api.assert(api.BigNumber(amount).gte(params.minBet) && api.BigNumber(amount).lte(params.maxBet), 'amount must be between minBet and maxBet')) {
            // request lock of amount STEEMP tokens
            const res = await api.executeSmartContract('tokens', 'transferToContract', { symbol: STEEM_PEGGED_SYMBOL, quantity: amount, to: CONTRACT_NAME });

            // check if the tokens were locked
            if (res.errors === undefined &&
                res.events && res.events.find(el => el.contract === 'tokens' && el.event === 'transferToContract' && el.data.from === api.sender && el.data.to === CONTRACT_NAME && el.data.quantity === amount && el.data.symbol === STEEM_PEGGED_SYMBOL) !== undefined) {

                // get a deterministic random number 
                const random = api.random();

                // calculate the roll
                const randomRoll = Math.floor(random * 100) + 1;

                // check if the dice rolled under "roll"
                if (randomRoll < roll) {
                    let multiplier = api.BigNumber(1)
                        .minus(params.houseEdge)
                        .multipliedBy(100)
                        .dividedBy(roll);

                    // calculate the number of tokens won
                    const tokensWon = api.BigNumber(amount)
                        .multipliedBy(multiplier)
                        .toFixed(3, api.BigNumber.ROUND_DOWN);

                    // send the tokens out
                    await api.transferTokens(api.sender, STEEM_PEGGED_SYMBOL, tokensWon, 'user');

                    // emit an event
                    api.emit('results', { 'memo': 'you won. roll: ' + randomRoll + ', your bet: ' + roll });
                } else {
                    // emit an event
                    api.emit('results', { 'memo': 'you lost. roll: ' + randomRoll + ', your bet: ' + roll });
                }
            }
            // else, 
            // errors will be displayed in the logs of the transaction
        }
    }
}

Now, that you understood how it worked :) let's go with a short explanation of what this contract does and how it does it.

1) createSSC action

First, you may have seen that this contract has two actions, the first one is createSSC, it is used to initialize the contract, it is triggered only once when the contract is deployed, we'll use it to initialize a few parameters for our dice contract.

We first create a table that will hold the parameters for our contract, I chose to call that table 'params':
await api.db.createTable('params');

we initialize our parameters:
a house edge of 1%, just to make sure that we'll earn money in the long run :D
params.houseEdge = "0.01";

then we set the minimum and the maximum bet that can be placed:

params.minBet = "0.1";
params.maxBet = "100";

Now, we want to insert our parameters into that table:
await api.db.insert('params', params);

We're done with that first action, that was easy, no?

2) roll action

The other action is roll, yes, it is the action to call to roll the dice and see if you just won or lost your money!

As you can see, an action receives the "payload" parameter, it is the variable that contains what the user sent to the contract action, we want our user to be able to send the dice roll and the amount of "money" he wants to bet. The money in this example will be STEEMP, for those who don't know what is STEEMP, it is a STEEM pegged token that makes our life easier to handle STEEM on steem-engine. It's a 1:1 token, for each STEEMP token owned by an account, there is 1 STEEM held by the @steem-peg account.

Ok, so now, let's extract the user inputs from the payload:

const { roll, amount } = payload;

and perform a few verifications (never trust the inputs from a user :)), we'll user "api.assert" to log errors in the transaction's logs in case the inputs are incorrect:

make sure that the roll is an integer that's between 2 and 96:

api.assert(roll  &&  Number.isInteger(roll) &&  roll  >=  2  &&  roll  <=  96, 'roll must be an integer and must be between 2 and 96')

make sure that amount is a valid amount:

&&  api.assert(amount  &&  typeof  amount  ===  'string'  &&  api.BigNumber(amount).dp() <=  3  &&  api.BigNumber(amount).gt(0), 'invalid amount')

api.BigNumber gives us access to the BigNumber library, it's a very powerful library to handle floating numbers in javascript, api.BigNumber(amount).dp() will give us the number of decimal places
api.BigNumber(amount).gt(0) will make sure that the amount is greater than 0

Now that we are done with the first pass of verifications, let's retrieve our dice contract's parameters that we initialized earlier:

const params = await api.db.findOne('params', {});

and check that this is a valid bet by making sure that the amount is between minBet and maxBet:

api.assert(api.BigNumber(amount).gte(params.minBet) &&  api.BigNumber(amount).lte(params.maxBet), 'amount must be between minBet and maxBet')

Last but not least, we need to make sure that the user has the amount of STEEMP that he's trying to bet with, for that, we'll call the "tokens" contract and move the STEEMP to the smart contract's balance:

const res = await  api.executeSmartContract('tokens', 'transferToContract', { symbol:  STEEM_PEGGED_SYMBOL, quantity:  amount, to:  CONTRACT_NAME });

if there are erros in the "res" variable, that means that the transfer didn't go trough (no funds for example), the errors will be logged in the transaction's logs under the "errors" key.

Ok, now that we are sure that the tokens are held by the contract, we can start playing with our dice, let's first get a random number:

const random = api.random();

The random number is coming from a deterministic random number generator and is initialized this way (it's very similar to what's being used by SteemMonsters):

const rng = seedrandom(${prevRefSteemBlockId}${refSteemBlockId}${transactionId});

and roll our dice:

const randomRoll = Math.floor(random * 100) + 1;

if the dice rolled under the user's predicition, we calculate a winning multiplier based on the "houseEdge" and the "roll" that the user chose:

let  multiplier  =  api.BigNumber(1)

.minus(params.houseEdge)

.multipliedBy(100)

.dividedBy(roll);

We can then calculate the number of tokens that need to be send to the user:

const  tokensWon  =  api.BigNumber(amount)

.multipliedBy(multiplier)

.toFixed(3, api.BigNumber.ROUND_DOWN);

and send them to the user:

await  api.transferTokens(api.sender, STEEM_PEGGED_SYMBOL, tokensWon, 'user');

The last piece of code is used to emit an event on the transaction, these will be stored in the transaction's logs and can be used by the UI to display messages to the user:

api.emit('results', { 'memo':  'you won. roll: '  +  randomRoll  +  ', your bet: '  +  roll });

3) Enhancements

We could enhance this contract a little bit more by making sure for example that the contract has enough STEEMP available to proceed with the bet, we could also make the house edge dynamic based on referrals, etc...

That's all for today, don't hesitate to reach out to us via the steem-engine discord or via a Discord DM (Harpagon#4087)

If you're looking into building an app on Steem-Engine, let us know and we might help you to push that to the mainnet :)

Aslo, if someone is willing to create a basic UI for that contract, I'll be more than happy to push the contract on the testnet!

Sort:  

Isn’t magic dice a dice game? Lol am I missing something?

Posted using Partiko iOS

Love your work @harpagon and I am almost certain you're going to see some dice games for Steem Engine tokens appear as a result of this post. Quick question about the number generation, would you consider expanding this to support seeding and what kind of algorithm is being used currently to generate the numbers?

I don't think you'll need to add a another seed, basically the random number generator is initialized with the Steem block id (hash), the previous Steem block id (hash) and the transaction id (hash), so the Steem block id with the previous Steem block id represent the "server seed" which basically change every new block and the transaction id represents the client seed.

Here's how that looks like in the code (the library used is seedrandom):

const rng = seedrandom(${prevRefSteemBlockId}${refSteemBlockId}${transactionId});

then, everytime you call rng() it will give you a new random number based on what was used to initialize the library.

Legendary. You're completely right, no additional seeds are needed. Because anyone building PRNG on Steem would most likely end up using Steem blockchain hashes for the seed values anyway, so nice work. This is really exciting.

I would love to create a basic UI for this contract if you wanted to publish it. I do wonder, is it possible for the contract to support dynamic tokens? As in the client can specify what tokens it supports, meaning you could create a dice game that supported rolling dice with any Steem Engine token.

that's definitely something that can be done, it requires a few tweaks but nothing major. The contract owner will then need to "top-up" the contract with any tokens so that people can play with them!

Exactly what I was looking for @harpagon! This will encourage more devs to build on steem-engine! Cheers!

Posted using Partiko Android

Awesome work!

Thanks :) hope someone else will push it further!

Posted using Partiko Android

I’m so lost, html is way easier. (For me to wrap my head around)

I feel your pain 😢

Posted using Partiko iOS

You have been infected by the King of Disease!

Will you quarantine yourself?

Or will you spread the plague?

King Of Disease

Congratulations @harpagon! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You published more than 30 posts. Your next target is to reach 40 posts.

You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Do not miss the last post from @steemitboard:

Are you a DrugWars early adopter? Benvenuto in famiglia!
Vote for @Steemitboard as a witness to get one more award and increased upvotes!

Hi, @harpagon!

You just got a 0.05% upvote from SteemPlus!
To get higher upvotes, earn more SteemPlus Points (SPP). On your Steemit wallet, check your SPP balance and click on "How to earn SPP?" to find out all the ways to earn.
If you're not using SteemPlus yet, please check our last posts in here to see the many ways in which SteemPlus can improve your Steem experience on Steemit and Busy.

@harpagon You have received a 100% upvote from @botreporter because this post did not use any bidbots and you have not used bidbots in the last 30 days!

Upvoting this comment will help keep this service running.

This post has been included in the latest edition of SoS Daily News - a digest of all the latest news on the Steem blockchain.

Yet Not Another Dice Game?