And the big functions:
I imagine that the following is HORRIBLE in the pythonic-vision and
surely can be rewriten with a single map+reduce+filter + 200 lambdas
functions X-D, but I come from C and any advice on how to implement my
"simple scripting language" without using lex or yacc is welcome
#--------------------------------------------------------------------
def ExecParser_Parse( key, value, line, file ):
"""
Parses an exec or exec2 line, generating a list of "opcodes".
This function takes an exec= line from a OBJ file and parses it
generating a list of "executable opcodes" in a format executable
by ExecParser_Exec().
The rules for this "small" language are:
- Spaces and tabs are stripped.
- Comments (starting by REM) are ignored.
- A simple set of commands is available. Those commands modify
some game variables or objects. As an example: PLAYSOUND(snd),
plays the sound identified by the sound_tag "snd".
- Commands must be separated by ";" characters.
- Commands can receive parameters enclosed between ( and ) of
types INT or STR.
- Programmer can use "self" to refer to the current object in
functions that accept an object or enemy text id.
- Simple control flow is present with IF, ELIF, ELSE and ENDIF
statements.
- IF and ELIF statemens are followed by a BOOLEAN command, which
will return 0 or 1.
Example ('\' character wraps lines in the OBJ file):
KILLOBJECT(coin);\
REM Wait 5 seconds;\
IF FLAG(5);\
SLEEP(5);\
PLAYERSAY(test,5);\
SETMAP(10,10,top,5);\
IF FLAG(6);\
NOP();\
SLEEP(7);\
ELIF FLAG(7);\
NOP();\
SLEEP(9);\
ELSE;\
SLEEP(999);\
ENDIF;\
IF FLAG_VALUE(7,1);\
CHANGESCREEN(start,10,100);\
PLAYERFACING(0);\
ENDIF;\
ENDIF;\
SLEEP(12);\
SLEEP(11);
This function will parse the exec line and produce as output
opcodes in
this format:
[ type_of_exec, if_level, opcode, parameters ]
type_of_exec = 1 for exec= lines, and 2 for exec2= lines.
if_level is the current code "level" or scope. IF statements
increase
if_level and ENDIF statements decrease it.
opcode and parameters are the function_name and the params for this
command.
Example:
ExecType CodeLevel Opcode and params
----------------------------------------------------------
1 0 KILLOBJECT ['coin']
0 0 REM ['Wait 5 seconds']
1 0 IF FLAG [5]
1 1 SLEEP [5]
1 1 PLAYERSAY ['test', 5]
1 1 SETMAP [10, 10, 'top', 5]
1 1 IF FLAG [6]
1 2 NOP
1 2 SLEEP [7]
1 1 ENDIF
1 0 ENDIF
1 0 END
The function makes some small checkings, like:
count(IF)==count(ENDIFs),
check number of arguments, argument type checking, validate command
functions, and some others, but it does not cover all possible
syntax
errors or typing mistakes.
"""
reserved_words = {
"SETMAP" : ( 4, "int", "int", "str", "int" ),
"KILLOBJECT" : ( 1, "str" ),
"ENABLEOBJECT" : ( 1, "str" ),
"DISABLEOBJECT" : ( 1, "str" ),
"PLAYSOUND" : ( 1, "str" ),
"PLAYMUSIC" : ( 1, "str" ),
"SLEEPCYCLES" : ( 1, "int" ),
"SLEEP" : ( 1, "int" ),
"SHOWTEXT" : ( 1, "str" ),
"SHOWTEXTTIMED" : ( 2, "str", "int" ),
"SHOWSCREEN" : ( 2, "str", "int" ),
"CHANGESCREEN" : ( 3, "str", "int", "int" ),
"ADDINVENTORY" : ( 1, "str" ),
"REMOVEINVENTORY" : ( 1, "str" ),
"OBJECTFACING" : ( 2, "str", "int" ),
"PLAYERFACING" : ( 1, "int" ),
"PLAYERSAY" : ( 2, "str", "int" ),
"OBJECTSAY" : ( 3, "str", "str", "int" ),
"SETFLAG" : (2, "int", "int" ),
"INCFLAG" : (1, "int" ),
"DECFLAG" : (1, "int" ),
"DELEXEC" : (1, "int" ),
"REM" : ( 0, ),
"NOP" : ( 0, ),
"END" : ( 0, ),
"TRUE" : ( 0, ),
"FALSE" : ( 0, ),
"IF" : ( 0, ),
"ELIF" : ( 0, ),
"ELSE" : ( 0, ),
"ENDIF" : ( 0, ),
"FLAG" : ( 1, "int" ),
"NOT_FLAG" : ( 1, "int" ),
"FLAG_VALUE" : ( 2, "int", "int" ),
"PLAYER_HAS" : ( 1, "str" ),
"PLAYER_HAS_NOT" : ( 1, "str" ),
"SCREEN_IS" : ( 1, "str" ),
"SCREEN_IS_NOT" : ( 1, "str" )
}
#input is something like: "exec=COMMAND;COMMAND;COMMAND..."
if key.upper() == "EXEC": exec_type = 1
code = [] # Resulting code
if ";" in value: commands = value.split(";")
else: commands = list( value )
# Filter empty lines
commands = filter( lambda x: len(x) > 1, commands )
code_level = level_inc = 0 # Current scope level
found_if = found_elif = 0
# Parse all commands in the input script
for cmd_counter, cmd in enumerate(commands):
if cmd == '': continue
cmd_upper = cmd.upper()
cmderr = "(line %d:%d) of <%s>." % (line,cmd_counter+1,file)
pos = cmd.find(')')
if pos != -1 and pos != len(cmd)-1:
print "OBJ-exec: Command format error in %s %s" %
(cmd,cmderr)
continue
# Delete spaces, CR and LFs
if not cmd_upper.startswith("REM ") and \
not cmd_upper.startswith("IF ") and \
not cmd_upper.startswith("ELIF "):
for i in " \n\r\t": cmd = cmd.replace(i, "")
cmd_upper = cmd.upper()
# Check special commands (without parameters):
# REM and NOP commands, just add it to the list
if cmd_upper.startswith("REM "):
code.append( ( 0, code_level, "REM", [cmd[4:]] ) )
continue
elif cmd_upper.startswith("NOP"):
code.append( ( exec_type, code_level, "NOP", [] ) )
continue
elif cmd_upper == "END":
code.append( ( exec_type, code_level, "END", [] ) )
continue
# IF commands: increase code score and save the IF statement
# boolean function will be processed later
if cmd_upper.startswith("IF "):
level_inc = 1
found_if = 1
cmd = cmd[2:].strip()
# ELIF: save ELIF statement in previous level and code in next
# boolean function will be processed later
elif cmd_upper.startswith("ELIF "):
level_inc = 1
found_elif = 1
if code_level > 0: code_level -= 1
else: print "OBJ-exec: ELIF without IF %s" %
(cmderr)
cmd = cmd[4:].strip()
# ELSE: same as ELIF, but don't process any boolean function
elif cmd_upper.startswith("ELSE"):
if code_level > 0: code_level -= 1
else: print "OBJ-exec: ELSE without IF %s" %
(cmderr)
code.append( ( exec_type, code_level, "ELSE", [] ) )
code_level += 1
continue
# ENDIF: descend a "scope" level
elif cmd_upper.startswith("ENDIF"):
if code_level > 0:
code_level -= 1
code.append( ( exec_type, code_level, "ENDIF", [] ) )
else:
print "OBJ-exec: ENDIF without IF %s" % (cmderr)
continue
# Process IF, ELIF boolean checks or any other command:
# Commands with parameters, check for () and a function name:
if not '(' in cmd or not ')' in cmd or cmd.startswith("("):
print "OBJ-exec: Unknown command '%s' %s" % (cmd,cmderr)
continue
# Try to get the function name and uppercase it.
try:
rows = cmd.split('(')
function = rows[0].upper()
except:
print "OBJ-exec: command error '%s' %s" % (cmd,cmderr)
continue
# Check if the function name is a valid/existing one:
if function not in reserved_words:
print "OBJ-exec: unknown function call '%s' %s" %
(function,cmderr)
continue
params = []
# Try to get all the present parameters
try:
# Only 1 parameter present:
if not ',' in rows[1]:
params.append(rows[1].split(')')[0])
# More than 1 parameter present, get them:
else:
paramlist = rows[1].split(')')[0]
params.extend(paramlist.split(','))
except:
print "OBJ-exec: error getting params in '%s' %s" %
(function,cmderr)
continue
# Check if the number of parameters is correct:
rightnumparam = reserved_words[function][0]
if len(params) != rightnumparam:
print "OBJ-exec: Found %d parameter(s) in %s instead of %d
%s" \
% (len(params),function,rightnumparam,cmderr)
continue
# Check if all the params are of the right type (int or str)
# Try to convert ints to int, store str as str
for num_param in range(len(params)):
valid_type = reserved_words[function][num_param+1]
if valid_type == "int":
try:
params[num_param] = int(params[num_param])
except:
print "OBJ-exec: Parameter %s should be INT in %s %s" \
% (num_param,function,cmderr)
continue
if found_if == 1:
opcode = ( exec_type, code_level, "IF " + function,
(params) )
found_if = found_elif = 0
elif found_elif == 1:
opcode = ( exec_type, code_level, "ELIF " + function,
(params) )
found_if = found_elif = 0
else:
opcode = ( exec_type, code_level, function, (params) )
code.append(opcode)
# Check if next instructions must be in a different code scope
if level_inc != 0:
code_level += level_inc
level_inc = 0
# Check consistence IF / ENDIF
ifs = len(filter(lambda l: l[2].startswith("IF "), code))
endifs = len(filter(lambda l: l[2].startswith("ENDIF"), code))
if ifs > endifs:
print "OBJ-exec: missing %d ENDIFs in line %s of <%s>." % (ifs-
endifs,line,file)
elif ifs < endifs:
print "OBJ-exec: missing %d IFs in line %s of <%s>." % (endifs-
ifs,line,file)
# Check if we got as many opcodes as commands
num_opcodes = len(code)
num_commands = len(commands)
if num_opcodes != num_commands:
print "OBJ-exec: COMPILE ERROR; got %s opcodes from %s commands
in line %s of <%s>." \
% (num_opcodes,num_commands,line,file)
code.append( (exec_type, 0, "END", []) )
return code
Thanks for any advice