Scarred Entertainment
7 min readNov 21, 2021

That old cornchippa 8 deserves to be thrown out. But I don’t know it’s growing on me a little. There’s an abandoned warehouse just a few miles out. I’m thinking of heading there to see if I can find some useful items.

1. Introduction

This tutorial will demonstrate a naive approach toward writing a DSL for pyteal contracts. We employ a process of tokenization by grouping the input text into various parts of the contract. We also apply these values dynamically through loops. This ensures that you can continue to write crafting contracts using a simple English like language.

It’s a little tedious to set up a dynamic contract that uses item requirements without having to hard code most of the interactions. You could do this if all the craftable items and the same number of requirements (as the number of items to check for availability won’t be dynamic).

We will be reading from the following sample data file. The syntax for an item is name'amount for example hammer'100. The syntax for a craftable item is the following: name:item1'amount,item2'amount,.... Our lexer will work by splitting the lines using the key characters. Namely ', , and :.

Let’s see .. to make a tezar 102 I would need 50 nails, 10 metal and a welding tool. I should hit up the grocer when I come back to pick up ingredients for tonight’s dinner.

data.txt

hammer'100000
graphite'100000
metal'100000
nail'10000000
welding tool'10000000
myntur 800: nail'50, graphite'10, hammer'2
tezar 102: welding tool'1, metal'10, nail'10
pot'100000
meat'100000
vegetable'100000
fruit'100000
salad and fruits: pot'1, vegetable'10, fruit'2
steak and chips: pot'1, vegetable'10, meat'2

We read all the lines of the above data file.

lex.py

import oslines = open('./data.txt', 'r').read().split('\n')

The following groups will store information about the contract. From a tab versus spaces perspective, we will use 2 spaces to signify a tab value.

prog_txt maps crafting functions to application call names.
init_txt initializes all the items and craftable items with their initial reserve value.
func_txt checks that the user has all the necessary requirements before crafting an item. It will hold this kind of function for all craftable items.
local_txt initializes all items’ reserve to 0 on the user’s local account. This function is invoked when the user opts into the contract.
kill_txt is the close out part of the program. It was intended to return everything the user has inside inventory back to the reserve. However, Algorand can only create contracts up to a certain size.

prog_txt = ""   # function calls
init_txt = "" # constructor
func_txt = "" # functions
get_txt = "" # get an item
local_txt = "" # join a contract
kill_txt = "" # closeout

Let’s start looping through the lines and constructing our data structures for items and craftable items.

for line in lines:

If the line contains a semi colon then this is a craftable item. We split this line to store the requirements. These are separated by commas.

if line.find(':') > 0:  # craftable item
key = line.split(':')

Now we split the requirements via the comma character.

if key[1].find(',') > 0:
_items = key[1].split(',')

We’ll store these values in a dictionary. A requirement has a name and amount. Since to create an item you may need 4 hammers and 2 nails for example.

items = []
for it in _items:
_its = it.split('\'')
_it = {
'name': _its[0].strip(),
'items': _its[1]
}
items.append(_it)

Here is the full data structure of the craftable item. It has a name and a list of requirements that follow the above schema Item{name:string,amount:integer}.

item = {
'name': key[0].strip(),
'reqs': items
}

There’s a guard at the entrance of the abandoned and he doesn’t seem too happy about seeing me. I don’t have enough bullets in this old thing. I hit him a few times but also lost a little hp. I’m outta bullets. I’ll have to try slide into him and taze him with my electroglove. It worked!

Now let’s start generating the contract. The function part of the contract will Assert to check if all the necessary requirements have been met.

fn = f"has_{item['name'].replace(' ', '_')}_reqs"
kill_txt += f" App.globalPut(Bytes('{item['name']}'), App.globalGet(Bytes('{item['name']}')) + App.localGet(Int(0), Bytes('{item['name']}'))),\n"

This text will be in the constructor where we set the value to zero. It sets the initial global reserve values for items and craftable items. Craftable items will start at 0.

init_txt += f"  App.globalPut(Bytes('{item['name']}'), Int(0)),\n"

This text will be used when the user optin to the contract. It sets all the items in the users local storage initial reserve value to zero.

local_txt += f"  App.localPut(Int(0), Bytes('{item['name']}'), Int(0)),\n"

I think I’ll walk and explain that I mean no harm. I’m here looking for some items to build weapons with. I’ll never be able to take out an entire warehouse right now. The guard only had a myntur 800 on him. I might just take that and dip. Too late.

This loop will set up the the function that crafts an item. First we check if the user has enough requirements. The we remove these items from the user’s local account before adding this newly crafted item.

func_txt += f" {fn} = Seq([\n"
ask_func_txt = "" # check requirements
ch_func_txt = "" # remove items from user's inventory.
for i in item['reqs']:
ask_func_txt += f" Assert(App.localGet(Int(0), Bytes('{i['name']}')) >= (Int({i['items']}) * amount)),\n"
ch_func_txt += f" App.localPut(Int(0), Bytes('{i['name']}'), App.localGet(Int(0), Bytes('{i['name']}')) - (Int({i['items']}) * amount)),\n"

Let’s bundle this up all to the function texts.

func_txt += ask_func_txt
func_txt += ch_func_txt

Here we add the newly crafted item. And return a truth value.

func_txt += f"  App.localPut(Int(0), Bytes('{item['name']}'), App.localGet(Int(0), Bytes('{item['name']}')) + (Int(1) * amount)),\n"
func_txt += f" Return(Int(1))\n"
func_txt += f" ])\n\n"

This is how the user will be able to call the function. A better way would be to have the name as the second argument.

prog_txt += f"  [Txn.application_args[0] == Bytes('craft {item['name']}'), {fn}],\n"

I got a bit of a beating for making that guard pass out. But we’re all straight now. They’re pushing fugazee crystals into the gambling strip. I like that. You can just germinate one big crystals and chop it.

This is a primitive item that can not be crafted but can be picked up in the game world. Skip any empty line first. However, if its not empty we set the item according to our item schema.

else: # primitive item
# skip empty lines
if len(line) <= 0:
break
# Split the line and create the item data structure.
key = line.split('\'')
item = {
'name': key[0].strip(),
'count': key[1]
}

This logic details how the user can exit the contract. We will send back what the user had locally to the global reserve. (Note: I actually don’t include this text into the final generated file because Algorand Stateful Contracts have a limited size).

kill_txt += f"  App.globalPut(Bytes('{item['name']}'), App.globalGet(Bytes('{item['name']}')) + App.localGet(Int(0), Bytes('{item['name']}'))),\n"

Add this to the constructor so that it can get initialized with amount of items available for it.

init_txt += f"  App.globalPut(Bytes('{item['name']}'), Int({item['count']})),\n"

Enter a contract. Here we set the reserve amount to 0.

local_txt += f"  App.localPut(Int(0), Bytes('{item['name']}'), Int(0)),\n"

The guard I took out just woke up. Mr Stone Cold Steve Austin.
Steve — “What the hell you got on your hand man?”

This is the beginning of the full contract as we have most of the various groups tokenized into easy to access variables.

First we import pyteal

print("from pyteal import *\n")

Approval part of the program.

print("def approval():")

The constructor will save the sender’s address as admin. Then display our init_txt group.

print(" on_init = Seq([")
print(" App.globalPut(Bytes('admin'), Txn.sender()),")
print(init_txt)
print(" Return(Int(1))")
print(" ])\n")

The optin method will display the local_txt group that contains all the local item initial values.

print(" on_join = Seq([")
print(local_txt)
print(" Return(Int(1))")
print(" ])\n")

Leaving will just return true. I have actually made it this way since I ran out of space that you use in a contract (1024 bytes). If its more than this the Algorand chain will reject creating the application.

print(" on_leave = Seq([")
print(" Return(Int(1))")
print(" ])\n")

Check for admin and set the amount to what’s given as the first argument or leave it as 1.

print(" is_creator = Txn.sender() == App.globalGet(Bytes('admin'))\n")
print(" amount = Btoi(Txn.application_args[1]) or 1")

Here we display the group func_txt that has all the information about the crafting items.

print(func_txt)

Ah they’ve let me have a few of their rusty crafting equipment. I’ll help myself to a few of these. It was a good day today. I found a neat faction few miles out, some equipment to make modern guns and had the best dinner.

The get function is pretty generic as the user can choose the name and amount using application arguments. We assert that there’s enough of this item in the reserve and also check that the reserve is greater than zero (we do this because compilation will fail with regard to the possibility of the reserve becoming less than zero.

print(f" name = Txn.application_args[1]")
print(f" item = App.globalGet(name)")
print(f" amount = Btoi(Txn.application_args[2])")
get_txt += f" on_get = Seq([\n"
get_txt += f" Assert(item >= amount),\n"
get_txt += f" Assert(item >= Int(0)),\n"
get_txt += f" App.globalPut(name, item - amount),\n"
get_txt += f" App.localPut(Int(0), name, App.localGet(Int(0), name) + amount),\n"
get_txt += f" Return(Int(1)),\n"
get_txt += " ])\n\n"
print(get_txt)

We can now bundle the entire application into one contract. We also make use of the prog_txt group that maps our ‘check requirement’ functions to application call names.

print(" return Cond(")
print(" [Txn.application_id() == Int(0), on_init],")
print(" [Txn.on_completion() == OnComplete.DeleteApplication, Return(is_creator)],")
print(" [Txn.on_completion() == OnComplete.UpdateApplication, Return(is_creator)],")
print(" [Txn.on_completion() == OnComplete.OptIn, on_join],")
print(" [Txn.on_completion() == OnComplete.CloseOut, on_leave],")
prog_txt += f" [Txn.application_args[0] == Bytes('get'), on_get],\n"
print(prog_txt)
print(" )\n")

This main part of python program will compile the approval and clear part of the Algorand Stateful Contract.

print("def clear():")
print(" return Seq([Return(Int(1))])\n")
print("if __name__ == '__main__':")
print(" with open('game.teal', 'w') as f:")
print(" compiled = compileTeal(approval(), mode=Mode.Application, version=2)")
print(" f.write(compiled)")
print(" with open('clear.teal', 'w') as f:")
print(" compiled = compileTeal(clear(), mode=Mode.Application, version=2)")
print(" f.write(compiled)")

Scarred Entertainment
Scarred Entertainment

Written by Scarred Entertainment

Game Studio. Face your scares and heal your scars.

No responses yet