Letting a player program.

BlitzMax Forums/BlitzMax Beginners Area/Letting a player program.

Recently I want to allow the player to program behaviors into objects. My question is how to allow the player to use math functions and the object's variables.

Lets say the object has an speed value. I currently have a dialog box that pops up and asks for a number to describe the speed. How would I allow the player to set the speed using mathmatical operators while addressing other field values within the object?

Maybe te player would like to set the speed based on another field variable coupled with some operators. Can this be done? Can they type in "cos(direction)*13" and operations such as this?

Sure, you just would have to `parse` what they type in and set up your own `mini script language` of keywords that are allowed, and then you'd search through the string and extract whatever bits interest you and process them. Ideally you'd probably turn the user input into a series of function calls or something so you don't have to parse it every time you want to execute it.

There are a few expression interpreters in the code archives. and as ImaginaryHuman suggest, generating an intermediate representation (like a node tree feks) would speed things up a little. or go a step further and create your own virtual machine.

Ah ok, I thought so. I suppose there is no way to directly evaluate a string of commands then? Without using word parsing?
There might be around 500 objects preforming actions at once. I think parsing may be too taxing for that...

I suppose there is no way to directly evaluate a string of commands then? Without using word parsing?

Im afraid not. and if there were, that method would still have to parse the string ;)

I would suggest parsing/compiling down to a simple stack machine with a single number type, and implementing functions as opcodes.

I'm really new to this concept. What do you mean my compiling down and single number type and opcodes?

I'm really new to this concept.

My mistake, i forgot this was in "Beginners Area".. hehe

If you havent done any parsing before i suggest reading up on it before jumping in.
http://en.wikipedia.org/wiki/Parsing
http://en.wikipedia.org/wiki/Recursive_descent_parser <-- easiest to implement in my oppinion

What it boils down to is that an expression like this:
x = a + b * 2

Is translated into either a node structure:
(ASSIGN
  (VARIABLE x)
  (ADD
    (VARIABLE a)
    (MUL
       (VARIABLE b)
       (NUMBER 2)
    )
  )
)

A node can defined like this.
Type TNode
  Field Left:TNode
  Field Right:TNode
  Field Value:String
EndType


Or directly to assembler (doesnt have to be real machine asm though, could be an imaginary machine like below)
GET a
GET b
PUSH 2
MUL
ADD
SET x

Compilers usually compile to a node tree, then work (doing optimizations etc) on that tree and then spit out some assembler code.
You dont have to use a virtual machine though, you could jsut as well execute the node structure, but it would be a little slower.

What i meant by single number type was just for simplicity of the virtual machine, eg no strings and other fancy structures like proper function calls.

an OpCode is roughly the same as an Instruction, a single operation a CPU or a virtual machine executes.

I adapted an expression interpreter i made a while back to spit out code for a very simple virtual machine, so you can see how it all fits together:

Framework BRL.StandardIO
Import BRL.LinkedList
Import BRL.Math

SuperStrict

Private

' COMPILER
Global source:String
Global pos:Int
Global code:Int[]
Global cp:Int
Global vars:TList = New TList
Global numvars:Int

Type TVariable
	Field Name:String
	Field Index:Int
	Field Value:Float
	
	Function Create:TVariable( name:String, value:Float, index:Int)
		Local v:TVariable = New TVariable
		v.Name = name
		v.Value = value
		v.Index = index
		Return v
	EndFunction
EndType

Function AddCode( op:Int)
	If code.Length = 0 Then
		code = New Int[32]
	ElseIf cp >= code.Length Then
		Local sz:Int = code.Length
		code = code[sz..sz*2]
	EndIf
	code[cp] = op
	cp :+ 1
EndFunction

Function AddParam( value:Float)
	If code.Length = 0 Then
		code = New Int[32]
	ElseIf cp >= code.Length Then
		Local sz:Int = code.Length
		code = code[sz..sz*2]
	EndIf
	Float Ptr(Varptr code[cp])[0] = value
	cp :+ 1
EndFunction

Function ReportError( s:String, printpos:Int = True)
	If printpos Then 
		Print "ERROR: pos="+pos+" : "+s
	Else
		Print "ERROR: "+s
	EndIf
EndFunction

Function EatWhitespace()
	While (pos < source.Length) And ((source[pos] = Asc(" ")) Or (source[pos] = Asc("~t")) Or (source[pos] = Asc("~n")) Or (source[pos] = Asc("~r")))
		pos :+ 1
	Wend
EndFunction

Function EatIdent:String()
	Local start:Int = pos
	While (pos < source.Length) And (((source[pos] >= Asc("a")) And (source[pos] <= Asc("z"))) Or ..
		((source[pos] >= Asc("A")) And (source[pos] <= Asc("Z"))) Or ..
		((source[pos] >= Asc("0")) And (source[pos] <= Asc("9"))) Or (source[pos] = Asc("_")))
		pos :+ 1
	Wend
	Return source[start..pos]
EndFunction

Function EatNumber:Double()
	Local start:Int = pos
	Local gotsep:Int = False
	Local res:String	
	While (pos < source.Length) And ((source[pos] >= Asc("0")) And (source[pos] <= Asc("9")))
		pos :+ 1
		If pos >= source.Length Then Exit
		If source[pos] = Asc(".") Then
			If gotsep Then 
				ReportError( "error in number")
				Return False
			EndIf
			gotsep = True
			pos :+ 1
		EndIf
	Wend
	Return source[start..pos].ToDouble()
EndFunction

Function Primary:Int()
	EatWhitespace()
	If source[pos] = Asc("(") Then
		pos :+ 1
		AddExpression()
    		If source[pos] <> Asc(")") Then ReportError( "expected )")
    		pos :+ 1
	ElseIf (source[pos] >= Asc("0")) And (source[pos] <= Asc("9")) Then
		AddCode( OP_PUSH)
		AddParam( EatNumber())
	ElseIf source[pos] = Asc("-") Then
		pos :+ 1
		AddCode( OP_PUSH)
		AddParam( - EatNumber())
		If Not AddExpression() Return False
	ElseIf ((source[pos] >= Asc("a")) And (source[pos] <= Asc("z"))) Or ..
		((source[pos] >= Asc("A")) And (source[pos] <= Asc("Z"))) Or (source[pos] = Asc("_")) Then
		Local ident:String = EatIdent()
		If source[pos] = Asc("(") Then
			pos :+ 1
			EatWhitespace()
			If source[pos] = Asc(")") Then
				' no parameters
				pos :+ 1
				CompileFunction( ident)
			Else			
				AddExpression()
				If source[pos] = Asc(")") Then
					' 1 parameter
					pos :+ 1
					CompileFunction( ident)
				ElseIf source[pos] = Asc(",") Then
					' 2 parameters
					pos :+ 1
					AddExpression()					
		    			If source[pos] <> Asc(")") Then ReportError( "expected )")
	    				pos :+ 1			
					CompileFunction( ident)
				Else
					ReportError( "invalid function expression => " + ident)
					Return False
				EndIf
			EndIf
		Else
			Local id:Int = LookupVariable( ident)
			If id <> -1 Then 
				AddCode( OP_GET)
				AddCode( id)
			Else
				ReportError( "undefined variable " + ident)
				Return False
			EndIf			
		EndIf
	Else
		ReportError( "expected number or -number or (expression)")
		Return False
	EndIf
	EatWhitespace()
	Return True
EndFunction

Function MulExpression:Int()
	EatWhitespace()
	If Not Primary() Then Return False
	While (pos < source.Length) And ((source[pos] = Asc("*")) Or (source[pos] = Asc("/")))
		If source[pos] = Asc("*") Then
			pos :+ 1
			If Not Primary() Then Return False
			AddCode( OP_MUL)
		ElseIf source[pos] = Asc("/") Then
			pos :+ 1
			If Not Primary() Then Return False
			AddCode( OP_DIV)
		EndIf
	Wend
	EatWhitespace()
	Return True
EndFunction
	
Function AddExpression:Int()
	EatWhitespace()
	If Not MulExpression() Then Return False
	While (pos < source.Length) And ((source[pos] = Asc("+")) Or (source[pos] = Asc("-")))
		If source[pos] = Asc("+") Then
			pos :+ 1
			If Not MulExpression() Then Return False
			AddCode( OP_ADD)
		ElseIf source[pos] = Asc("-") Then
			pos :+ 1
			If Not MulExpression() Then Return False
			AddCode( OP_SUB)
		EndIf
	Wend
	EatWhitespace()
	Return True
EndFunction

Function CompileFunction:Int( ident:String)
	Select ident.ToLower()
		Case "sin"	AddCode( OP_SIN)
		Case "cos"	AddCode( OP_COS)
		Default
			ReportError( "undefined function " + ident)
			Return False
	EndSelect	
	Return True
EndFunction

Function LookupVariable:Int( name:String)
	name = name.ToLower()
	For Local v:TVariable = EachIn vars
		If v.Name = name Then Return v.Index
	Next
	Return -1
EndFunction

Public

Function AddVariable( name:String, value:Float = 0.0)
	Local v:TVariable = TVariable.Create( name.ToLower(), value, numvars)
	vars.AddLast( v)
	numvars :+ 1
EndFunction

Function Compile:Int( s:String)
	source = s
	Local id:Int
	Local idx:Int = source.Find( "=")
	If idx > 0 Then
		Local ident:String = EatIdent()
		id = LookupVariable( ident)
		If id = -1 Then 
			ReportError( "undefined variable " + ident)
			Return False
		EndIf
		EatWhitespace()
		pos :+ 1
		If Not AddExpression() Then Return False
		AddCode( OP_SET)
		AddCode( id)
		Return True
	ElseIf idx = 0 Then
		ReportError( "invalid assignment", False)
		Return False
	EndIf
	If Not AddExpression() Then Return False
	Return True
EndFunction

Function GetCompiledCodeData( c:Int[] Var, d:Float[] Var)
	c = code[..cp]	
	d = New Float[numvars]
	Local i:Int = 0
	For Local v:TVariable = EachIn vars
		d[i] = v.Value
		i :+ 1
	Next
EndFunction

Function ResetCompiler()
	source = Null
	pos = 0
	code = Null
	cp = 0
	vars.Clear()
	numvars = 0
EndFunction



' VIRTUAL MACHINE
Const OP_PUSH:Int = 0
Const OP_GET:Int = 1
Const OP_SET:Int = 2
Const OP_ADD:Int = 3
Const OP_SUB:Int = 4
Const OP_MUL:Int = 5
Const OP_DIV:Int = 6
Const OP_SIN:Int = 7
Const OP_COS:Int = 8

Type TMicroVM
	Field Data:Float[]
	Field Code:Int[]
	Field Stack:Int[]
	Field CP:Int
	Field SP:Int
	
	Method Execute:Float()
		CP = 0
		SP = Stack.Length
		While CP < Code.Length
			Select Code[CP]
				Case OP_PUSH
					CP :+ 1
					SP :- 1
					Stack[SP] = Code[CP]
				Case OP_GET
					CP :+ 1
					SP :- 1
					Float Ptr(Varptr Stack[SP])[0] = Data[ Code[CP]]
				Case OP_SET
					CP :+ 1
					Data[ Code[CP]] = Float Ptr(Varptr Stack[SP])[0]
					SP :+ 1
				Case OP_ADD
					Float Ptr(Varptr Stack[SP+1])[0] :+ Float Ptr(Varptr Stack[SP])[0]
					SP :+ 1
				Case OP_SUB
					Float Ptr(Varptr Stack[SP+1])[0] :- Float Ptr(Varptr Stack[SP])[0]
					SP :+ 1
				Case OP_MUL
					Float Ptr(Varptr Stack[SP+1])[0] :* Float Ptr(Varptr Stack[SP])[0]
					SP :+ 1
				Case OP_DIV
					Float Ptr(Varptr Stack[SP+1])[0] :/ Float Ptr(Varptr Stack[SP])[0]
					SP :+ 1			
				Case OP_SIN
					Float Ptr(Varptr Stack[SP])[0] = Sin( Float Ptr(Varptr Stack[SP])[0])
				Case OP_COS
					Float Ptr(Varptr Stack[SP])[0] = Sin( Float Ptr(Varptr Stack[SP])[0])					
			EndSelect
			CP :+ 1
		Wend
		If SP < Stack.Length Then 
			Return Float Ptr(Varptr Stack[SP])[0]
		Else
			Return Float Ptr(Varptr Stack[Stack.Length-1])[0]
		EndIf
	EndMethod
EndType



' TEST
Local vm:TMicroVM = New TMicroVM
ResetCompiler()
AddVariable( "a", 1)
AddVariable( "b", 2)
AddVariable( "x")
Compile( "x = a + b * 2")
vm.Stack = New Int[32]
GetCompiledCodeData( vm.Code, vm.Data)
Print vm.Execute()


I recommend you take a look at BriskVM, which is a scripting language you can easily use in BlitzMax.

Not only is it very easy to implement for you, it's also very easy to script in, because the syntax is 99.9% the same as BlitzMax.

You can find it here: http://www.koriolis-fx.com/

Highly recommended!

Thanks guys I see I'll have a busy evening. If I have trouble I will post again.