lambda-v/lambdaV.py

584 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env python3
# Copyright 2022, Jan Danielzick (aka. BodgeMaster)
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published
# by the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# version 3 along with this program.
# If not, see https://www.gnu.org/licenses/gpl-3.0.en.html
import re, time
debug_mode = False
def debug_message(text):
if debug_mode:
print("DEBUG: ", end="")
print(text)
cursor_north = 'Λ'
cursor_south = 'V'
cursor_west = '<'
cursor_east = '>'
cursor_current = ' '
field = [
" _________________ ",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
" ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ "
]
wall = '#'
apple = 'ó'
goal = '$'
cursor_position = [0, 0]
command_delay = 0.3
empty_field = [
" _________________ ",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
" ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ "
]
def draw_field():
print("\033[2J\033[H")
for row in range(len(field)):
for column in range(len(field[row])):
if column==cursor_position[0] and row==cursor_position[1]:
print(cursor_current, end="")
else:
print(field[row][column], end="")
print("")
# returns "" or error message
# Parsed code is the result of running the parser,
# though at this point the input is assumed to be valid
# and no additional checks are performed on what the parser produced.
def run_code(parsed_code):
for i in range(len(parsed_code[0])):
result = parsed_code[0][i](*parsed_code[1][i])
if not result=="":
return result
draw_field()
time.sleep(command_delay)
return ""
def condition_facing_north(inverted):
if inverted:
return not cursor_current == cursor_north
return cursor_current == cursor_north
def condition_facing_south(inverted):
if inverted:
return not cursor_current == cursor_south
return cursor_current == cursor_south
def condition_facing_east(inverted):
if inverted:
return not cursor_current == cursor_east
return cursor_current == cursor_east
def condition_facing_west(inverted):
if inverted:
return not cursor_current == cursor_west
return cursor_current == cursor_west
def condition_in_front_of_wall(inverted):
if condition_facing_north(False):
result = field[cursor_position[1]-1][cursor_position[0]]==wall or field[cursor_position[1]-1][cursor_position[0]]=='_'
elif condition_facing_south(False):
result = field[cursor_position[1]+1][cursor_position[0]]==wall or field[cursor_position[1]+1][cursor_position[0]]=='¯'
elif condition_facing_east(False):
result = field[cursor_position[1]][cursor_position[0]+1]==wall or field[cursor_position[1]][cursor_position[0]+1]=='|'
elif condition_facing_west(False):
result = field[cursor_position[1]][cursor_position[0]-1]==wall or field[cursor_position[1]][cursor_position[0]-1]=='|'
else:
result = False
if inverted:
return not result
return result
def condition_goal_reached(inverted):
if inverted:
return not field[cursor_position[1]][cursor_position[0]]==goal
return field[cursor_position[1]][cursor_position[0]]==goal
def condition_on_apple(inverted):
if inverted:
return not field[cursor_position[1]][cursor_position[0]]==apple
return field[cursor_position[1]][cursor_position[0]]==apple
# return value: "" or error message
def command_step():
if condition_in_front_of_wall(False):
return "You stepped into a wall."
if condition_facing_north(False):
cursor_position[1] = cursor_position[1]-1
elif condition_facing_south(False):
cursor_position[1] = cursor_position[1]+1
elif condition_facing_east(False):
cursor_position[0] = cursor_position[0]+1
elif condition_facing_west(False):
cursor_position[0] = cursor_position[0]-1
else:
return "Cannot step because direction is unknown."
return ""
def command_left():
global cursor_current
if condition_facing_north(False):
cursor_current = cursor_west
elif condition_facing_south(False):
cursor_current = cursor_east
elif condition_facing_east(False):
cursor_current = cursor_north
elif condition_facing_west(False):
cursor_current = cursor_south
else:
return "Cannot turn left because direction is unknown."
return ""
def command_right():
global cursor_current
if condition_facing_north(False):
cursor_current = cursor_east
elif condition_facing_south(False):
cursor_current = cursor_west
elif condition_facing_east(False):
cursor_current = cursor_south
elif condition_facing_west(False):
cursor_current = cursor_north
else:
return "Cannot turn right because direction is unknown."
return ""
def command_take():
if condition_on_apple(True):
# Note: condition is inverted
return "Cannot take apple, there is no apple here."
field[cursor_position[1]] = field[cursor_position[1]][:cursor_position[0]]+" "+field[cursor_position[1]][cursor_position[0]+1:]
return ""
def command_repeat(number, parsed_code):
for i in range(number):
result = run_code(parsed_code)
if not result=="":
return result
return ""
def command_while(parsed_condition, parsed_code):
while parsed_condition[0](parsed_condition[1]):
result = run_code(parsed_code)
if not result=="":
return result
return ""
def command_if(parsed_condition, parsed_then_code, parsed_else_code):
if parsed_condition[0](parsed_condition[1]):
result = run_code(parsed_then_code)
if not result=="":
return result
elif not parsed_else_code==None:
result = run_code(parsed_else_code)
if not result=="":
return result
return ""
# returns -1 for generic error
# returns -2 if end cant be found
def get_length_of_condition(code):
if len(code)==0:
return -1
if code[0] == '<':
length = 0
while length<len(code) and code[length]!='>':
length = length+1
if length==len(code) and code[-1]!='>':
return -2
return length+1
else:
return -1
# returns -1 for generic error
# returns -2 if end cant be found
def get_length_of_contained_code(code):
if len(code)==0:
return -1
if code[0] == '(':
length = 1
nesting_level = 1
while length<len(code) and nesting_level>0:
if code[length] == '(':
nesting_level = nesting_level+1
elif code[length] == ')':
nesting_level = nesting_level-1
length = length+1
if length==len(code) and nesting_level > 0:
return -2
return length
else:
return -1
# clean up whitespace
def format_code(code):
regex_pattern = re.compile(r'\s+')
code = " ".join(re.sub(regex_pattern, ' ', code).lower().split())
code = code.replace(" (", "(")
code = code.replace("( ", "(")
code = code.replace(" )", ")")
code = code.replace(") ", ")")
code = code.replace(" <", "<")
code = code.replace("< ", "<")
code = code.replace(" >", ">")
code = code.replace("< ", "<")
code = code.replace("! ", "!")
# add spaces back after closing parenthesis
code = code.replace(")", ") ")
code = code.replace(") )", "))")
code = code.replace(") )", "))")
if len(code)>0 and code[-1]==' ':
return code[:-1]
return code
# returns [function, inverted?, error message]
def parse_condition(condition_code, allowed_conditions):
inverted = False
condition = None
if len(condition_code)==0:
debug_message(" No condition specified")
return [condition, inverted, "Syntax error: No condition specified"]
if condition_code[0]=='!':
debug_message(" Condition is inverted")
inverted = True
condition_code = condition_code[1:]
if len(condition_code)==0:
debug_message(" No condition specified")
return [condition, inverted, "Syntax error: No condition specified"]
if condition_code == "in front of wall":
condition = condition_in_front_of_wall
elif condition_code == "goal reached":
condition = condition_goal_reached
elif condition_code == "on apple":
condition = condition_on_apple
elif condition_code == "facing north":
condition = condition_facing_north
elif condition_code == "facing south":
condition = condition_facing_south
elif condition_code == "facing east":
condition = condition_facing_east
elif condition_code == "facing west":
condition = condition_facing_west
else:
debug_message(" Unknown condition: "+condition_code)
return [condition, inverted, "Unknown condition: "+condition_code]
if not condition_code in allowed_conditions:
return [condition, inverted, "Condition is not allowed here: "+condition_code]
return [condition, inverted, ""]
# returns [[functions], [arguments], -1, "", "formatted code"] or [[], [], error_position, "error message", "formatted code"]
def parse_code(code, allowed_commands, allowed_conditions, unformatted_code=True):
if unformatted_code:
code = format_code(code)
debug_message("Formatted code: "+code)
parse_position = 0
parsed_code = [[], [], -1, "", code]
while parse_position < len(code):
# find next command
next_space = code.find(' ', parse_position)
next_condition = code.find('<', parse_position)
if next_space == -1:
next_space = len(code)
if next_condition == -1:
next_condition = len(code)
# Are we working with a simple command?
if next_condition>next_space or next_condition==next_space:
next_command = code[parse_position:next_space]
debug_message("Next command is: "+next_command)
parsed_code[1].append(())
if next_command == "step":
parsed_code[0].append(command_step)
if not next_command in allowed_commands:
return [[], [], parse_position, "Command is not allowed here: "+next_command, code]
elif next_command == "left":
parsed_code[0].append(command_left)
if not next_command in allowed_commands:
return [[], [], parse_position, "Command is not allowed here: "+next_command, code]
elif next_command == "right":
parsed_code[0].append(command_right)
if not next_command in allowed_commands:
return [[], [], parse_position, "Command is not allowed here: "+next_command, code]
elif next_command == "take":
parsed_code[0].append(command_take)
if not next_command in allowed_commands:
return [[], [], parse_position, "Command is not allowed here: "+next_command, code]
elif next_command == "repeat":
return [[], [], next_space, "Syntax error: Number of repetitions missing", code]
elif next_command == "while":
return [[], [], next_space, "Syntax error: Condition missing", code]
elif next_command == "if":
return [[], [], next_space, "Syntax error: Condition missing", code]
elif next_command[0:4] == "else":
return [[], [], parse_position, "Syntax error: Else without if statement", code]
else:
#TODO: better error message (especially when special chars are detected inside next_command or a known command)
return [[], [], parse_position, "Unknown command: "+next_command, code]
parse_position = next_space+1
else:
debug_message("Found control structure...")
control_structure = code[parse_position:next_condition]
if control_structure == "repeat":
debug_message(" Type: repeat loop")
# This is checked here because otherwise it would throw a bogus error message for unknown control structures.
if not control_structure in allowed_commands:
return [[], [], parse_position, "Command is not allowed here: "+control_structure, code]
parse_position = next_condition
repetitions_length = get_length_of_condition(code[parse_position:])
if repetitions_length == -2:
debug_message(" Mismatched <>")
return [[], [], parse_position, "Syntax error: Cannot not find matching '>' for this '<'", code]
repetitions = code[parse_position:parse_position+repetitions_length]
debug_message(" Number of repetitions: "+repetitions)
repetitions_int = 0
try:
repetitions_int = int(repetitions[1:-1])
except ValueError:
return [[], [], parse_position, "Not a valid number: "+repetitions[1:-1], code]
parse_position = parse_position+len(repetitions)
contained_code_length = get_length_of_contained_code(code[parse_position:])
if contained_code_length == -1:
debug_message(" Missing (")
return [[], [], parse_position, "Syntax error: Cannot not find '(' for contained code", code]
if contained_code_length == -2:
debug_message(" Mismatched ()")
return [[], [], parse_position, "Syntax error: Cannot not find matching ')' for this '('", code]
contained_code = code[parse_position:parse_position+contained_code_length]
debug_message(" Contained code: "+contained_code)
parsed_contained_code = parse_code(contained_code[1:-1], allowed_commands, allowed_conditions, unformatted_code=False)
if not parsed_contained_code[2]==-1:
debug_message(" Error while parsing contained code")
return [[], [], parse_position+1+parsed_contained_code[2], parsed_contained_code[3], code]
parse_position = parse_position+contained_code_length+1
parsed_code[1].append((repetitions_int, parsed_contained_code))
parsed_code[0].append(command_repeat)
continue
elif control_structure == "if":
debug_message(" Type: if statement")
# This is checked here because otherwise it would throw a bogus error message for unknown control structures.
if not control_structure in allowed_commands:
return [[], [], parse_position, "Command is not allowed here: "+control_structure, code]
parse_position = next_condition
condition_length = get_length_of_condition(code[parse_position:])
if condition_length == -2:
debug_message(" Mismatched <>")
return [[], [], parse_position, "Syntax error: Cannot not find matching '>' for this '<'", code]
condition = code[parse_position:parse_position+condition_length]
debug_message(" Condition is: "+condition)
parsed_condition = parse_condition(condition[1:-1], allowed_conditions)
if not parsed_condition[2]=="":
return [[], [], parse_position, parsed_condition[2], code]
parse_position = parse_position+len(condition)
then_code_length = get_length_of_contained_code(code[parse_position:])
if then_code_length == -1:
debug_message(" Missing (")
return [[], [], parse_position, "Syntax error: Cannot not find '(' for then-code", code]
if then_code_length == -2:
debug_message(" Mismatched ()")
return [[], [], parse_position, "Syntax error: Cannot not find matching ')' for this '('", code]
then_code = code[parse_position:parse_position+then_code_length]
debug_message(" Then-code: "+then_code)
parsed_then_code = parse_code(then_code[1:-1], allowed_commands, allowed_conditions, unformatted_code=False)
if not parsed_then_code[2]==-1:
debug_message(" Error while parsing then-code")
return [[], [], parse_position+1+parsed_then_code[2], parsed_then_code[3], code]
parse_position = parse_position+then_code_length+1
parsed_else_code = None
if parse_position<len(code)+4 and code[parse_position:parse_position+4]=="else":
parse_position = parse_position+4
else_code_length = get_length_of_contained_code(code[parse_position:])
if else_code_length == -1:
debug_message(" Missing (")
return [[], [], parse_position, "Syntax error: Cannot not find '(' for else-code", code]
if else_code_length == -2:
debug_message(" Mismatched ()")
return [[], [], parse_position, "Syntax error: Cannot not find matching ')' for this '('", code]
else_code = code[parse_position:parse_position+else_code_length]
debug_message(" Else-code: "+else_code)
parsed_else_code = parse_code(else_code[1:-1], allowed_commands, allowed_conditions, unformatted_code=False)
parse_position = parse_position+else_code_length+1
parsed_code[1].append((parsed_condition, parsed_then_code, parsed_else_code))
parsed_code[0].append(command_if)
continue
elif control_structure == "while":
debug_message(" Type: while loop")
# This is checked here because otherwise it would throw a bogus error message for unknown control structures.
if not control_structure in allowed_commands:
return [[], [], parse_position, "Command is not allowed here: "+control_structure, code]
parse_position = next_condition
condition_length = get_length_of_condition(code[parse_position:])
if condition_length == -2:
debug_message(" Mismatched <>")
return [[], [], parse_position, "Syntax error: Cannot not find matching '>' for this '<'", code]
condition = code[parse_position:parse_position+condition_length]
debug_message(" Condition is: "+condition)
parsed_condition = parse_condition(condition[1:-1], allowed_conditions)
if not parsed_condition[2]=="":
return [[], [], parse_position, parsed_condition[2], code]
parse_position = parse_position+len(condition)
contained_code_length = get_length_of_contained_code(code[parse_position:])
if contained_code_length == -1:
debug_message(" Missing (")
return [[], [], parse_position, "Syntax error: Cannot not find '(' for contained code", code]
if contained_code_length == -2:
debug_message(" Mismatched ()")
return [[], [], parse_position, "Syntax error: Cannot not find matching ')' for this '('", code]
contained_code = code[parse_position:parse_position+contained_code_length]
debug_message(" Contained code: "+contained_code)
parsed_contained_code = parse_code(contained_code[1:-1], allowed_commands, allowed_conditions, unformatted_code=False)
if not parsed_contained_code[2]==-1:
debug_message(" Error while parsing contained code")
return [[], [], parse_position+1+parsed_contained_code[2], parsed_contained_code[3], code]
parse_position = parse_position+contained_code_length+1
parsed_code[1].append((parsed_condition, parsed_contained_code))
parsed_code[0].append(command_while)
else:
debug_message(" Type: unknown control structure: "+control_structure)
#TODO: better error message (especially when special chars are detected inside control_structure or a known command)
return [[], [], parse_position, "Syntax error: Unknown control structure: "+control_structure, code]
return parsed_code
# A wrapper for run_code that deals with potential errors produced by the parser.
# returns success or error message
def evaluate_parser_result(parsed_code):
if not parsed_code[2]==-1:
# parser error
return parsed_code[3] + "\n" + (" "*parsed_code[2]) + "\n" + parsed_code[4]
outcome = run_code(parsed_code)
if not outcome=="":
# runtime error
# TODO: pass back information about where the error occurred
return outcome
if condition_goal_reached(False):
return "Success!"
else:
return "Goal not reached!"
def setup_and_run_task(start_field, start_position, start_heading, text, allowed_commands, allowed_conditions):
global cursor_position
cursor_position = start_position
global cursor_current
cursor_current = start_heading
global field
field = start_field
draw_field()
print("\n"+text)
print("\nAllowed commands: "+", ".join(allowed_commands))
if len(allowed_conditions)>0:
print("\nAvailable conditions: "+", ".join(allowed_conditions))
result = evaluate_parser_result(parse_code(input("\nCode: "), allowed_commands, allowed_conditions))
print(result)
time.sleep(3)
if result=="Success!":
return True
return False
if __name__ == "__main__":
import sys, os, json
level = 0
single_file_mode = False
level_file_name = ""
def wrong_usage():
print("Usage:\n " + sys.argv[0] + " [l<number> | level_file_name.json]\n\n- Run without arguments to start at level 0.\n- Run with a single argument made of 'l' and a number to start at that level.\n- Run with a file name to start that specific level.")
sys.exit(1)
if len(sys.argv)>1:
if len(sys.argv)>2:
wrong_usage()
if sys.argv[1][0]=="l":
try:
level=int(sys.argv[1][1:])
except:
if os.path.isfile(sys.argv[1]):
level_file_name = sys.argv[1]
single_file_mode = True
else:
wrong_usage()
elif os.path.isfile(sys.argv[1]):
level_file_name = sys.argv[1]
single_file_mode = True
else:
wrong_usage()
if not single_file_mode:
level_file_name = os.path.join("levels", str(level)+".json")
while os.path.isfile(level_file_name):
level_file = open(level_file_name, "r")
level_raw = level_file.read()
level_file.close()
level_data = json.loads(level_raw)
cursor_start = ""
if level_data["start heading"]=="north":
cursor_start = cursor_north
elif level_data["start heading"]=="south":
cursor_start = cursor_south
elif level_data["start heading"]=="east":
cursor_start = cursor_east
elif level_data["start heading"]=="west":
cursor_start = cursor_west
else:
print("Unknown start heading!")
sys.exit(1)
while not setup_and_run_task(level_data["field"].copy(), level_data["start position"].copy(), cursor_start, level_data["text"], level_data["allowed commands"].copy(), level_data["allowed conditions"].copy()):
pass
if single_file_mode:
break
level+=1
level_file_name = os.path.join("levels", str(level)+".json")