lambda-v/lambdaV.py

583 lines
24 KiB
Python
Raw Normal View History

#!/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
2022-11-06 06:22:20 +01:00
import re, time
2022-11-06 02:23:38 +01:00
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]
2022-11-06 06:22:20 +01:00
command_delay = 0.3
2022-11-30 15:16:15 +01:00
empty_field = [
2022-11-06 02:23:38 +01:00
" _________________ ",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
"| |",
" ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ "
]
def draw_field():
2022-11-06 06:22:20 +01:00
print("\033[2J\033[H")
2022-11-06 02:23:38 +01:00
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()
2022-11-06 06:22:20 +01:00
time.sleep(command_delay)
return ""
def condition_facing_north(inverted):
2022-11-06 02:24:21 +01:00
if inverted:
return not cursor_current == cursor_north
return cursor_current == cursor_north
def condition_facing_south(inverted):
2022-11-06 02:24:21 +01:00
if inverted:
return not cursor_current == cursor_south
return cursor_current == cursor_south
def condition_facing_east(inverted):
2022-11-06 02:24:21 +01:00
if inverted:
return not cursor_current == cursor_east
return cursor_current == cursor_east
def condition_facing_west(inverted):
2022-11-06 02:24:21 +01:00
if inverted:
return not cursor_current == cursor_west
return cursor_current == cursor_west
2022-11-06 02:24:21 +01:00
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
2022-11-06 02:24:21 +01:00
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():
2022-11-06 03:30:58 +01:00
if condition_in_front_of_wall(False):
2022-11-06 06:22:20 +01:00
return "You stepped into a wall."
2022-11-06 03:30:58 +01:00
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():
2022-11-06 03:30:58 +01:00
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():
2022-11-06 03:30:58 +01:00
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 ""
2022-11-06 03:30:58 +01:00
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 ""
2022-11-06 03:30:58 +01:00
def command_repeat(number, parsed_code):
for i in range(number):
result = run_code(parsed_code)
if not result=="":
return result
return ""
2022-11-06 03:30:58 +01:00
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]
2022-11-26 15:19:02 +01:00
if not condition_code in allowed_conditions:
return [condition, inverted, "Condition is not allowed here: "+condition_code]
return [condition, inverted, ""]
2022-11-26 15:19:02 +01:00
# 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)
2022-11-26 15:19:02 +01:00
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)
2022-11-26 15:19:02 +01:00
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)
2022-11-26 15:19:02 +01:00
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)
2022-11-26 15:19:02 +01:00
if not next_command in allowed_commands:
return [[], [], parse_position, "Command is not allowed here: "+next_command, code]
elif next_command == "repeat":
2022-11-06 06:22:20 +01:00
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")
2022-11-26 15:19:02 +01:00
# 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")
2022-11-26 15:19:02 +01:00
# 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")
2022-11-26 15:19:02 +01:00
# 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!"
2022-11-06 06:22:20 +01:00
2022-11-30 15:16:15 +01:00
def setup_and_run_task(start_field, start_position, start_heading, text, allowed_commands, allowed_conditions):
2022-11-06 06:22:20 +01:00
global cursor_position
2022-11-30 15:16:15 +01:00
cursor_position = start_position
2022-11-06 06:22:20 +01:00
global cursor_current
2022-11-30 15:16:15 +01:00
cursor_current = start_heading
global field
field = start_field
2022-11-06 06:22:20 +01:00
draw_field()
2022-11-30 15:16:15 +01:00
print("\n"+text)
print("\nAllowed commands: "+", ".join(allowed_commands))
print("\nAvailable conditions: "+", ".join(allowed_conditions))
2022-12-23 12:56:57 +01:00
result = evaluate_parser_result(parse_code(input("\nCode: "), allowed_commands, allowed_conditions))
print(result)
time.sleep(3)
2022-12-23 12:56:57 +01:00
if result=="Success!":
return True
return False
2022-11-30 15:16:15 +01:00
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)
2022-12-23 12:56:57 +01:00
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")