Home - mt-sane/lsystem GitHub Wiki

LSystem

This is a l-system library (a mod) for minetest.

LSystem consists out of the three objects system, rules and states. These objects are used to build the l-system's generations.

How to use

Algae

Let's start with the classical algae.

local sys = LSystem.New("A")

sys.rules.A = LSystem.Rule.New("AB")
sys.rules.B = LSystem.Rule.New("A")

sys:Build()
sys:Build()
sys:Build()
sys:Build()

print(table.concat(sys.axiom)) --> ABAABABA

Note the table.concat(sys.axiom)). LSystem keeps it's axiom for efficiency reasons in a table rather than in a string.

Adding some state

Now let's give that system some state. Let's add a position and a direction.

local sys = LSystem.New(
	"A"
	, { diag = "action", } 
	, { pos = Lib.Cardinal.C.c, dir = Lib.Cardinal.C.n, }
)
sys.rules.A = LSystem.Rule.New("AB", LSystem.Rule.B.Move)
sys.rules.B = LSystem.Rule.New("A",  LSystem.Rule.B.Diag)

sys:Build()
sys:Build()
sys:Build()
sys:Build()

Resulting log:

ACTION[Main]: LSystem axiom='A'.
ACTION[Main]: LSystem Rule 'A', pos='(0,0,0)', dir='(0,0,1)'.
ACTION[Main]: LSystem axiom='AB'.
ACTION[Main]: LSystem Rule 'A', pos='(0,0,0)', dir='(0,0,1)'.
ACTION[Main]: LSystem Rule 'B', pos='(0,0,1)', dir='(0,0,1)'.
ACTION[Main]: LSystem axiom='ABA'.
ACTION[Main]: LSystem Rule 'A', pos='(0,0,0)', dir='(0,0,1)'.
ACTION[Main]: LSystem Rule 'B', pos='(0,0,1)', dir='(0,0,1)'.
ACTION[Main]: LSystem Rule 'A', pos='(0,0,1)', dir='(0,0,1)'.
ACTION[Main]: LSystem axiom='ABAAB'.
ACTION[Main]: LSystem Rule 'A', pos='(0,0,0)', dir='(0,0,1)'.
ACTION[Main]: LSystem Rule 'B', pos='(0,0,1)', dir='(0,0,1)'.
ACTION[Main]: LSystem Rule 'A', pos='(0,0,1)', dir='(0,0,1)'.
ACTION[Main]: LSystem Rule 'A', pos='(0,0,2)', dir='(0,0,1)'.
ACTION[Main]: LSystem Rule 'B', pos='(0,0,3)', dir='(0,0,1)'.

The LSystem.New gets two new parameters. The frist { diag = "action", } is the global state. The second is the local state.

The global state has a field diag set to action. That sets diagnostics to on (see Diag).

The local state has a pos and a dir field. pos is the current position. dir is the current direction. They are set to values from Lib.Cardinal.C which are the cardinal direction vectors.
Both fields are used by Move. That function simply adds dir to pos.

The rules now use their build function paramter to New.
Rule 'A' uses Move to advance pos every time the character 'A' is met in the current axiom.
Rule 'B' uses Diag to produce the logged information. Note that Move also produces a diagnostic output. That is because all default functions call Diag before actually doing their work. Also note that all rules that are created without an explicit build function will also call Diag. So LSystem.Rule.New("A", LSystem.Rule.B.Diag) and LSystem.Rule.New("A") produce exactly the same output.

Adding some randomness

Up to now our system is perfectly static. Whenever you make a certain number of calls to Build you will get the exact same results. Let's give ourselves a chance and change that.

local sys = LSystem.New(
	"A"
	, { diag = "action", } --diagLevels = { default = 0, }, } 
	, { pos = Lib.Cardinal.C.c, dir = Lib.Cardinal.C.n, }
	, 1
)
sys.rules.A = LSystem.Rule.New("AB", LSystem.Rule.B.Move)
sys.rules.B = LSystem.Rule.New("(AAAC)S")
sys.rules.C = LSystem.Rule.New("[(a)TA]")
sys.rules.S = LSystem.Rule.New("", LSystem.Rule.B.Select)
sys.rules.T = LSystem.Rule.New(".T", LSystem.Rule.B.Turn)

sys:Build()
sys:Build()
sys:Build()
sys:Build()
sys:Build()
sys:Build()

LSystem.New( is called with a third parameter, the seed. That value sets the random number seed for the system. If ommited the system will produce different results every time it is generated anew.

Rule 'B' is now (AAAC)S. It makes use of parameters. Rule 'B' here sets the paramter for 'S' to AAAC.

Rule 'C' = [(a)TA] introduces the internal rules push '[' and pop ']'. Using '[' will store the current local state on a stack. ']' restores that state. In this example the values pos and dir will be saved and restored.

So what 'C' does is

  • Store pos and dir on the stack
  • Call 'T' with parameter 'w', which will turn the direction 90° to the left.
  • Call 'A'
  • Restore pos and dir

Rule 'S' uses Select to select one characters from it's parameter as axiom.
By default parameters are consumed by the rules. So the parameter AAAC that is passed to 'S' by 'B' gets eaten and will not show up in the next generation. 'S' itself has a blank axiom (axiom is set to "") so the resulting axiom for the whole term '(AAAC)S' will be one character. That character is chosen randomly by Select.

Rule 'T' uses Turn to turn the direction. Normally the parameter a that 'C' passes to 'T' would be consumed and leave 'T' without a parameter for the next generation. But 'T''s axiom starts with an '.' character. This is another (and last) spciality. Rule axioms starting with '.' indicate that the Rule keeps it's parameters. The '.' character is replaced by the parameters and will not show up in the next generation.

So what does the whole thing do?

  • 'A' moves the current position forward.
  • 'B' will spawn a new variant of the system to the left with a 1 : 5 chance.
  • 'C' contains the spawn.
  • 'S' realizes the random selection.
  • 'T' realizes the turn.

So what is it good for?
Well not much ... maybe it could be used as an example for something somehow.

Doing someting with it

Up to now the system does nothing else than generate itself. Let's use it to put some nodes into the world.

Be aware that this will actuallay change your world without any respect for it's surroundings. Do not use it any place where you are not ready to take the grief.

local sys = LSystem.New(
	"A"
	, { diag = "action", } --diagLevels = { default = 0, }, } 
	, { pos = Lib.Cardinal.C.c, dir = Lib.Cardinal.C.n, }
	, 1
)
sys.rules.A = LSystem.Rule.New("AB", LSystem.Rule.B.Move)
sys.rules.B = LSystem.Rule.New("(AAAC)S")
sys.rules.C = LSystem.Rule.New("[(a)TA]")
sys.rules.S = LSystem.Rule.New("", LSystem.Rule.B.Select)
sys.rules.T = LSystem.Rule.New(".T", LSystem.Rule.B.Turn)

local deploy = {
	A = function(info)
		minetest.set_node(info.state.pos, { name="default:wood" } )
	end,
}

minetest.register_node(
	"lsystem_test:builder", 
	{
		description = "Use (left click, while wielded) to deploy the next generation.",
		tiles = {"default_clay.png^bubble.png"},
		on_use = function(itemstack, user, pointed_thing)
			
			local pos = user:getpos()
			local look = user:get_look_dir()
			local facedir = minetest.dir_to_facedir(look)
			local cardinalKey = Lib.Cardinal.F2CK[facedir]
			local cardinal = Lib.Cardinal.C[cardinalKey]
			vector.add(pos, cardinal)
			
			sys.state.pos = pos
			sys.state.dir = cardinal
			sys:Build(deploy)

		end,
	}
)

minetest.register_on_newplayer(function(player)
	player:get_inventory():add_item('main', 'lsystem_test:builder')
end)

There is essentially one new thing happening, if we ignore all the shebang that is necessary to get a node into the world. New is the deploy table, that is passed to the Build function. It contains functions that are called before the corresponding rule build functions. The deploy function gets the same info parameter as the build function, so be careful with the values in it, they are refernces.
If you do a harmless looking thing like:

local deploy = {
	A = function(info)
		local p = info.state.pos
		minetest.set_node(p, { name="default:wood" } )
		p.y = p.y + 1
		minetest.set_node(p, { name="default:wood" } )
	end,
}

You actually change the position for the whole system.

That’s all folks! Go and have fun.