#!/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 can’t be found def get_length_of_condition(code): if len(code)==0: return -1 if code[0] == '<': length = 0 while length0: 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") 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 | 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")