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.

In Part 2, coming soon, we will go over deployment and real-life interaction with the contract. In Part 3 will show how we can create an interface within the Parity client to make this all actually usable to a regular ethereum user.

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 {

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

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

  struct DoroidotchiStruct {
    string doroidotchiName;

    uint fed;
    uint fedBlock;
    uint entertained;
    uint entertainedBlock;
    uint 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, 
    uint fed, 
    uint entertained, 
    uint 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 (uint amount) {
    return hungerPerBlock *
      (block.number - 1 - doroidotchi[parent].fedBlock);
}

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

function calcEnergySince(address parent) public view  
  returns (uint amount) {
    return energyPerBlock *
      (block.number - 1 -  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);

  uint fed = doroidotchi[parent].fed - calcHungerSince(parent);

  uint entertained = doroidotchi[parent].entertained - calcBoredomSince(parent);

  uint 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.

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.

Proof!

Proof Two!