Doroidotchi - The Tamagotchi-like Smart Contract

I've wanted to sit down and get a bit more familiar with ethereum and Solidity, but I tend to do best with an idea to pull off. Although it's a big job to be involved in the financial revolution, sometimes you just need something silly to hack out in an afternoon. With the recent 20th anniversary re-issue of the the Tamagotchi, the concept of an online dependant to look after seemed like exactly what I was looking for! So here is how we created a being that lives within the ethereum network.

I'll keep adding to this tutorial and polishing it here and there, eventually creating the full doroidotchi.com website within this article. If you have any feedback, please don't hesitate to comment on any of the various social media sites it will likely end up on.

Doroidotchi Requirements & Goals

  • No special token (no hate, just not needed here)
  • It's gotta look pretty cool (but that's Part 3)
  • It's gotta be easy to use
  • Time must progress without contract interaction ($$$)
  • One contract should support multiple Doroidotchis
  • Doroidotchi should be transferable
  • Gas fees should be kept to a minimum

Over the years, I had tried a couple tutorials, but none seemed to really work out. Documentation in the early era of blockchain tech can be a little lacking, or is fantastic but ends up being outdated due to the fast moving nature of the space. I expect this write-up to be similarly out of touch eventually, but hopefully it helps a couple people get started before then.

Just because this well respected blog is showing some code and deployment processes, doesn't mean it's doing it right. This is my first time creating on the ethereum network and this post should be considered completely void of adequate best practices. Read, Deploy, Run, and Destroy all at your own peril.

Let's setup a quick framework for our contract

pragma solidity ^0.4.17;

contract doroidotchiBasuketto {

  int constant hungerPerBlock = 1;
  int constant boredomPerBlock = 2;
  int constant energyPerBlock = 2;		

  int constant hungerPerFeed = 4000;
  int constant boredomPerEntertainment = 2000;
  int constant energyPerSleep = 8000;		

  struct DoroidotchiStruct {
    string doroidotchiName;

    int fed;
    uint fedBlock;
    int entertained;
    uint entertainedBlock;
    int rested;
    uint restedBlock;

    uint blockBorn;
    bool initiated;
  }
  mapping (address => DoroidotchiStruct) public doroidotchi;			

}

In the initial definition of our contract above, we start by defining some constants that will be used throughout the game and never change. We also create a struct[ure] that will hold each of the Doroidotchi details, such as how healthy they are at the moment, when their latest updates occurred, when they were created and what their name is.

The most foreign part here to me was the mapping function at the end. It starts to make a lot more sense if you just consider "mapping" as a fancy word for array in Solidity. In this case, the array made from our mapping will be accessible as doroidotchi[address], and each of the attributes defined in our struct will be accessible as doroidotchi[address].attribute.

Creating you own Doroidotchi in the Basuketto

Now we've got to give users the ability to create a new Doroidotchi in our contract, so we create a new createDoroidotchi function.

function createDoroidotchi(string doroidotchiName) external {

	address parent = msg.sender;

	require (!doroidotchi[parent].initiated);
	require (bytes(doroidotchiName).length > 0);

	doroidotchi[parent].initiated = true;
		    
	doroidotchi[parent].doroidotchiName = doroidotchiName;

	doroidotchi[parent].fed = 5000;
	doroidotchi[parent].fedBlock = block.number;
			
	doroidotchi[parent].entertained = 7000;
	doroidotchi[parent].entertainedBlock = block.number;
			
	doroidotchi[parent].rested = 90000;
	doroidotchi[parent].restedBlock	= block.number;

	doroidotchi[parent].blockBorn = block.number;
}

Since the function is to be used externally by other actors on the network (other than the contract itself), we define the function as external. The only external data we need from the user in this case is a name for their beautiful little Doroidotchi, so we also add a function parameter for the doroidotchiName.

HOLY CRAP SECURITY RISK: Notice how there is zero validation being done on this name field? Computation inside a contract is expensive, so we won't bother wasting money here to make sure that there is no droid named little Bobby Tables or Jessica Javascript. Any interface we write will need to be aware of this limitation in the contract and sanitize the name prior to using it.

Within the creation function, we are accessing a point in the array specifically where our senders address sits. If my ethereum address was 0xca35b7d915458ef540ade6068dfe2f44e8fa733c, then you can envision each of these array calls as defining the attributes of my droid at doroidotchi["0xca35b7d915458ef540ade6068dfe2f44e8fa733c"].attribute. The msg.sender value is built-in and will always return to the value of the address that sent the transaction executing our functions. Similarly, the block.number value will always return the current height of the ethereum chain.

Finally, the other slightly new concept here is the use of the require function. These are run and cease the execution of the contract immediately if they are not satisfied. Using the require function at the beginning of the createDoroidotchi function, we will check to make that the doroidotchi has not been created for this ethereum address, and then that that name given has at least one character. The .initiated variable is not magic, it is just a boolean value within the DoroidotchiStruct. Since boolean values initiate as false, it is a good way to check that they struct found there is a real Doroidotchi, or the default state of a new struct.

Monitoring our precious Doroidotchi

function getDoroidotchi(address parent) public view 
  returns (
    string doroidotchiName, 
    int fed, 
    int entertained, 
    int rested, 
    uint age, 
    bool isDead
  ) {
  require (doroidotchi[parent].initiated);

  return (
    doroidotchi[parent].doroidotchiName, 
    doroidotchi[parent].fed - calcHungerSince(parent), 
    doroidotchi[parent].entertained - calcBoredomSince(parent), 
    doroidotchi[parent].rested - calcEnergySince(parent), 
    block.number - doroidotchi[parent].blockBorn, 
    hasDoroidotchiDied(parent)
   );     
}

Which happens to call some ugly utility functions. These just do the math to see how much change to our attributes have passed since we last performed an action. These allow our Doroidotchi to be constantly changing without any direct new writes/transactions, saving all players money, or saving the costs of an oracle to progress the game.

function calcHungerSince(address parent) public view 
  returns (int amount) {
    return hungerPerBlock *
      int(block.number - doroidotchi[parent].fedBlock);
}

function calcBoredomSince(address parent) public view 
  returns (int amount) {
    return boredomPerBlock *
      int(block.number - doroidotchi[parent].entertainedBlock);
}
  
function calcEnergySince(address parent) public view 
  returns (int amount) {
    return energyPerBlock *
      int(block.number -  doroidotchi[parent].restedBlock);
}

We'll also need to know if our Doroidotchi has been properly cared for with the hasDoroidotchiDied() function.

function hasDoroidotchiDied(address parent) public view returns (bool isDead) {
  require (doroidotchi[parent].initiated);
    
  int fed = doroidotchi[parent].fed - calcHungerSince(parent);
    
  int entertained = doroidotchi[parent].entertained - calcBoredomSince(parent);
    
  int rested = doroidotchi[parent].rested - calcEnergySince(parent);
   
  if ((fed <= 0) ||  (entertained <= 0) || (rested <= 0)) {
    return true;
  } else {
    return false;
  }
}

Nothing too special about any of these utilities. They are all view, which means they can be run locally at no gas cost. They all return a specific single value, as defined in the returns portion.

Caring for our Doroidotchi

Now that we can create out Doroidotchi, and and monitor is health and happiness, we need to be able to care for it by Feeding it, putting it to Sleep, and Playing with it.

Like the createDoroidotchi function, these functions will also be executable only via transactions to the ethereum network. The results of these functions, will be written to the ethereum blockchain as the new state for specific users Doroidotchi.

function feed() external {
  require (doroidotchi[msg.sender].initiated);
  require(!hasDoroidotchiDied(msg.sender));
     
  doroidotchi[msg.sender].fed = hungerPerFeed + doroidotchi[msg.sender].fed - calcHungerSince(msg.sender);
  doroidotchi[msg.sender].fedBlock = block.number;
}
  
function play() external {
  require (doroidotchi[msg.sender].initiated);
  require(!hasDoroidotchiDied(msg.sender));

  doroidotchi[msg.sender].entertained = boredomPerEntertainment + doroidotchi[msg.sender].entertained - calcBoredomSince(msg.sender);
  doroidotchi[msg.sender].entertainedBlock = block.number;
}

function sleep() external {
  require (doroidotchi[msg.sender].initiated);
  require(!hasDoroidotchiDied(msg.sender));
    
  doroidotchi[msg.sender].rested = energyPerSleep + doroidotchi[msg.sender].rested - calcEnergySince(msg.sender);
  doroidotchi[msg.sender].restedBlock = block.number;
}

These functions each first run some checks using the require method, and then calculate both he increase from the action, along with all of the changes in state since the same action was last performed.

Some additional features

To finalize the contract, and give players a bit more control over their beloved Doroidotchi, we also added destroyDoroidotchi() and transferDoroidotchi()

function transferDoroidotchi(address newParent) public 
  returns (bool isTransfered) {
    require (doroidotchi[msg.sender].initiated);
    require (!doroidotchi[newParent].initiated);
    
    doroidotchi[newParent] = doroidotchi[msg.sender];
    delete doroidotchi[msg.sender];
    
    return true;  
}

function destroyDoroidotchi() public {
  require (doroidotchi[msg.sender].initiated);
    
  delete doroidotchi[msg.sender];
}

These are also just mean to help people keep the ethereum chain clean once they are done playing with their toys. They can either pass it along to a friend, or melt it down into fresh bits.

Gas Costs

There is no payment to the contract owner to play the game, but you must still pay gas costs when creating your Doroidotchi, and when performing any actions such as Feeding, Resting and Playing. These fees go right to the miners as a reward for their service.

The most expensive portion of the game is creating the droid, this is because all sorts of new information is written to the ethereum blockchain, and as one would expect, storage across thousands of computers isn't cheap.

Creating a Doroidotchi will cost you 182492 gas, or about a $1 USD.

Performing game actions takes some gas as well, although it varies slightly depending on the action. The estimate is about 13942 gas, or $0.07 USD.

Not everything costs gas though, since getDoroidotchi(address) and hasDoroidotchiDied(address) are both view functions, these can be called directly from your ethereum client without a transaction being issued. The results are simply calculated locally and returned based on data stored within the ethereum network state.

Compiling

Now that we have written our contract, we need to deploy it out into the ether. This will require having some ether ready on hand in a wallet, compiling our contract, and then creating a transaction that publishes it into the Ethereum blockchain.

To compile the contract, and for debugging and testing, I have been using the remix.ethereum.org online IDE. This sweet little tool lets you use an ethereum testnet instance right in your browser.

Remix

Once your code is working as you intend, you'll need to extract two main pieces of information from remix, the contracts compiled bytecode, and the interface specification (also known as the ABI). You can find these details by clicking the Details button within the Compile tab.

Remix - Details

Copy each relevant section out and paste it into some new files for safe keeping. The bytecode will be the actual program code we upload to the chain, and the interface tells our client what functions and structures are available within the code to work with. I've included these at the bottom of this blog, but they should match if you compile the contract code yourself too.

Publishing

You can publish your contract to the ethereum blockchain using a number of different clients and wallets, but for this example I'll be using ethers.js, because it is now my favorite eth tool.

With ethers.js, we just need to write up a quick javascript file and then run it.

// The interface from Solidity
var abi = '[{"constant": true, "inputs": [{"name": "listPosition"...';

// The bytecode from Solidity (note the 0x prefix)
var bytecode = '0x6060604052600060015534156...3cb05e9b80029';

var deployTransaction = Contract.getDeployTransaction(bytecode, abi);
console.log(deployTransaction);

// Connect to the network
var provider = ethers.providers.getDefaultProvider();

// Create a wallet to deploy the contract with
var privateKey = '0x0123456...7890123';
var wallet = new ethers.Wallet(privateKey, provider);

// Send the transaction
var sendPromise = wallet.sendTransaction(deployTransaction);

// Get the transaction
sendPromise.then(function(transaction) {
    console.log(transaction);
});

Testing

Finally, we can try to create our own Doroidotchi and check to see if they are alive. Maybe we'll even feel like a good Doroidotchi caregiver and provide some food.

Creating my Doroidotchi

var doroidotchi = new ethers.Contract("0xCb558B4342721a7bbF01363e98A55d754fa9Ea1d", abi, wallet); 
 
var sendPromise = doroidotchi.createDoroidotchi("Cmdr");

sendPromise.then(function(transaction) {
   console.log(transaction);
});

Checking on its status

var doroidotchi = new ethers.Contract("0xCb558B4342721a7bbF01363e98A55d754fa9Ea1d", abi, wallet); 
 

var callPromise = doroidotchi.getDoroidotchi("0x46a2C329c069682D551a00280E6a454d1ea283F7");

callPromise.then(function(result){
  console.log(result.doroidotchiName);
  console.log(result.isDead);
  //Cmdr is alive!
});

Feeding Cmdr

 var doroidotchi = new ethers.Contract("0xCb558B4342721a7bbF01363e98A55d754fa9Ea1d", abi, wallet); 
 
 var sendPromise = doroidotchi.feed();

sendPromise.then(function(transaction) {
    console.log(transaction);
});

Conclusion

This tutorial barely scratches the surface of the ethereum ecosystem, and smart contract development, but it will hopefully give some hints and guidance to getting started within it. Parity has been helpful for some situations, but overall I've enjoyed working with ethers.js far more.

Resources

As well as being an infrequent author, Abstrct is the creator of the cult classic Coindroids blockchain battle game, as well as the SQL-based favourite Schemaverse.