diff --git a/app.py b/app.py index cd5b09fd2af..b2f0f64fad4 100644 --- a/app.py +++ b/app.py @@ -778,7 +778,7 @@ def remove_file(response): return send_file("machine_files/" + filename + "." + extension, as_attachment=True) -MICROBIT_FEATURE = False +MICROBIT_FEATURE = True @app.route('/generate_microbit_files', methods=['POST']) @@ -803,7 +803,7 @@ def save_transpiled_code_for_microbit(transpiled_python_code): if not os.path.exists(folder): os.makedirs(folder) with open(filepath, 'w') as file: - custom_string = "from microbit import *\nwhile True:" + custom_string = "from microbit import *\nimport random\nwhile True:" file.write(custom_string + "\n") # Add space before every display.scroll call diff --git a/hedy.py b/hedy.py index 92f35aed00c..e26a91b6868 100644 --- a/hedy.py +++ b/hedy.py @@ -290,7 +290,7 @@ def add_level(commands, level, add=None, remove=None): add = [] if not remove: remove = [] - commands[level] = [c for c in commands[level-1] if c not in remove] + add + commands[level] = [c for c in commands[level - 1] if c not in remove] + add # Commands per Hedy level which are used to suggest the closest command when kids make a mistake @@ -313,7 +313,6 @@ def add_level(commands, level, add=None, remove=None): add_level(commands_per_level, level=17, add=['elif']) add_level(commands_per_level, level=18, add=['input'], remove=['ask']) - command_turn_literals = ['right', 'left'] english_colors = ['black', 'blue', 'brown', 'gray', 'green', 'orange', 'pink', 'purple', 'red', 'white', 'yellow'] @@ -1119,7 +1118,8 @@ def __default__(self, args, children, meta): # for the achievements we want to be able to also detect which operators were used by a kid operators = ['addition', 'subtraction', 'multiplication', 'division'] - if production_rule_name in commands_per_level[self.level] or production_rule_name in operators or production_rule_name == 'if_pressed_else': + if production_rule_name in commands_per_level[ + self.level] or production_rule_name in operators or production_rule_name == 'if_pressed_else': # if_pressed_else is not in the yamls, upsetting lookup code to get an alternative later # lookup should be fixed instead, making a special case for now if production_rule_name == 'else': # use of else also has an if @@ -1318,22 +1318,22 @@ def if_pressed_no_colon(self, meta, args): def if_pressed_elifs_no_colon(self, meta, args): # if_pressed_elifs starts with _EOL, so we need to add +1 to its line - raise exceptions.MissingColonException(command=Command.elif_, line_number=meta.line+1) + raise exceptions.MissingColonException(command=Command.elif_, line_number=meta.line + 1) def if_pressed_elses_no_colon(self, meta, args): # if_pressed_elses starts with _EOL, so we need to add +1 to its line - raise exceptions.MissingColonException(command=Command.else_, line_number=meta.line+1) + raise exceptions.MissingColonException(command=Command.else_, line_number=meta.line + 1) def ifs_no_colon(self, meta, args): raise exceptions.MissingColonException(command=Command.if_, line_number=meta.line) def elifs_no_colon(self, meta, args): # elifs starts with _EOL, so we need to add +1 to its line - raise exceptions.MissingColonException(command=Command.elif_, line_number=meta.line+1) + raise exceptions.MissingColonException(command=Command.elif_, line_number=meta.line + 1) def elses_no_colon(self, meta, args): # elses starts with _EOL, so we need to add +1 to its line - raise exceptions.MissingColonException(command=Command.else_, line_number=meta.line+1) + raise exceptions.MissingColonException(command=Command.else_, line_number=meta.line + 1) def for_list_no_colon(self, meta, args): raise exceptions.MissingColonException(command=Command.for_list, line_number=meta.line) @@ -1873,7 +1873,8 @@ def print(self, meta, args): exception = self.make_index_error_check_if_list(args) return exception + f"print(f'{argument_string}'){self.add_debug_breakpoint()}" else: - return f"""display.scroll('{argument_string}')""" + argument_string = ' '.join(args_new) + return f"display.scroll({argument_string})" def ask(self, meta, args): var = args[0] @@ -1889,13 +1890,19 @@ def make_print_ask_arg(self, arg, meta, var_to_escape=''): # therefore we should not process it anymore and thread it as a variable: # we set the line number to 100 so there is never an issue with variable access before # assignment (regular code will not work since random.choice(dieren) is never defined as var as such) - if "random.choice" in arg or "[" in arg: - return self.process_variable_for_fstring(arg, meta.line, var_to_escape) - - # this regex splits words from non-letter characters, such that name! becomes [name, !] - p = r"[·\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}]+|[^·\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}]+" - res = regex.findall(p, arg) - return ''.join([self.process_variable_for_fstring(x, meta.line, var_to_escape) for x in res]) + if not self.microbit: + if "random.choice" in arg or "[" in arg: + return self.process_variable_for_fstring(arg, meta.line, var_to_escape) + # this regex splits words from non-letter characters, such that name! becomes [name, !] + p = r"[·\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}]+|[^·\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}]+" + res = regex.findall(p, arg) + return ''.join([self.process_variable_for_fstring(x, meta.line, var_to_escape) for x in res]) + else: + if self.is_variable(arg, meta.line): + return arg + else: + # If the argument is not a variable, return it as a string literal with quotes + return f"'{arg}'" def forward(self, meta, args): if len(args) == 0: @@ -1932,65 +1939,113 @@ def play(self, meta, args): def assign(self, meta, args): variable_name = args[0] value = args[1] - - if self.is_random(value) or self.is_list(value): - exception = self.make_index_error_check_if_list([value]) - return exception + variable_name + " = " + value + self.add_debug_breakpoint() + if not self.microbit: + if self.is_random(value) or self.is_list(value): + exception = self.make_index_error_check_if_list([value]) + return exception + variable_name + " = " + value + self.add_debug_breakpoint() + else: + if self.is_variable(value, meta.line): # if the value is a variable, this is a reassign + value = self.process_variable(value, meta.line) + return variable_name + " = " + value + self.add_debug_breakpoint() + else: + # if the assigned value is not a variable and contains single quotes, escape them + value = process_characters_needing_escape(value) + return variable_name + " = '" + value + "'" + self.add_debug_breakpoint() else: - if self.is_variable(value, meta.line): # if the value is a variable, this is a reassign - value = self.process_variable(value, meta.line) - return variable_name + " = " + value + self.add_debug_breakpoint() + if self.is_random(value) or self.is_list(value): + exception = self.make_index_error_check_if_list([value]) + return " " + exception + str(variable_name) + " = " + value + self.add_debug_breakpoint() else: - # if the assigned value is not a variable and contains single quotes, escape them - value = process_characters_needing_escape(value) - return variable_name + " = '" + value + "'" + self.add_debug_breakpoint() + if self.is_variable(value, meta.line): # if the value is a variable, this is a reassign + value = self.process_variable(value, meta.line) + return " " + str(variable_name) + " = " + value + self.add_debug_breakpoint() + else: + # if the assigned value is not a variable and contains single quotes, escape them + value = process_characters_needing_escape(value) + return " " + str(variable_name) + " = '" + value + "'" + self.add_debug_breakpoint() def sleep(self, meta, args): - if not args: - return f"time.sleep(1){self.add_debug_breakpoint()}" + if not self.microbit: + if not args: + return f"time.sleep(1){self.add_debug_breakpoint()}" + else: + value = f'"{args[0]}"' if self.is_int(args[0]) else args[0] + if not self.is_int(args[0]): + self.add_variable_access_location(value, meta.line) + index_exception = self.make_index_error_check_if_list(args) + ex = make_value_error(Command.sleep, 'suggestion_number', self.language) + code = index_exception + \ + textwrap.dedent(f"time.sleep(int_with_error({value}, {ex})){self.add_debug_breakpoint()}") + return code else: - value = f'"{args[0]}"' if self.is_int(args[0]) else args[0] - if not self.is_int(args[0]): - self.add_variable_access_location(value, meta.line) - index_exception = self.make_index_error_check_if_list(args) - ex = make_value_error(Command.sleep, 'suggestion_number', self.language) - code = index_exception + \ - textwrap.dedent(f"time.sleep(int_with_error({value}, {ex})){self.add_debug_breakpoint()}") - return code + if not args: + return f"sleep(1000){self.add_debug_breakpoint()}" # Default 1 second sleep in milliseconds + else: + value = args[0] + if self.is_int(value): + # Direct conversion of seconds to milliseconds + milliseconds = int(value) * 1000 + else: + # If the value is a variable, ensure it's used correctly + milliseconds = f"{value} * 1000" + return f" sleep({milliseconds}){self.add_debug_breakpoint()}" @v_args(meta=True) @hedy_transpiler(level=3) @source_map_transformer(source_map) class ConvertToPython_3(ConvertToPython_2): + def assign_list(self, meta, args): - parameter = args[0] - values = [f"'{process_characters_needing_escape(a)}'" for a in args[1:]] - return f"{parameter} = [{', '.join(values)}]{self.add_debug_breakpoint()}" + if not self.microbit: + parameter = args[0] + values = [f"'{process_characters_needing_escape(a)}'" for a in args[1:]] + return f"{parameter} = [{', '.join(values)}]{self.add_debug_breakpoint()}" + else: + parameter = args[0] + values = [f"'{process_characters_needing_escape(a)}'" for a in args[1:]] + return f' {parameter} = [{", ".join(values)}]{self.add_debug_breakpoint()}' def list_access(self, meta, args): - args = [escape_var(a) for a in args] - listname = str(args[0]) - location = str(args[0]) + if not self.microbit: + args = [escape_var(a) for a in args] + listname = str(args[0]) + location = str(args[0]) - # check the arguments (except when they are random or numbers, that is not quoted nor a var but is allowed) - self.check_var_usage([a for a in args if a != 'random' and not a.isnumeric()], meta.line) + # check the arguments (except when they are random or numbers, that is not quoted nor a var but is allowed) + self.check_var_usage([a for a in args if a != 'random' and not a.isnumeric()], meta.line) - # store locations of both parts (can be list at var) - self.add_variable_access_location(listname, meta.line) - self.add_variable_access_location(location, meta.line) + # store locations of both parts (can be list at var) + self.add_variable_access_location(listname, meta.line) + self.add_variable_access_location(location, meta.line) - if args[1] == 'random': - return 'random.choice(' + listname + ')' + if args[1] == 'random': + return 'random.choice(' + listname + ')' + else: + return listname + '[int(' + args[1] + ')-1]' else: - return listname + '[int(' + args[1] + ')-1]' + args = [escape_var(a) for a in args] + listname = str(args[0]) + location = str(args[1]) + + # check the arguments (except when they are random or numbers, that is not quoted nor a var but is allowed) + self.check_var_usage([a for a in args if a != 'random' and not a.isnumeric()], meta.line) + + # store locations of both parts (can be list at var) + self.add_variable_access_location(listname, meta.line) + self.add_variable_access_location(location, meta.line) + + if args[1] == 'random': + return 'random.choice(' + listname + ')' + else: + return f"({listname}[int({args[1]})-1])" def process_argument(self, meta, arg): # only call process_variable if arg is a string, else keep as is (ie. # don't change 5 into '5', my_list[1] into 'my_list[1]') if arg.isnumeric() and isinstance(arg, int): # is int/float return arg - elif (self.is_list(arg)): # is list indexing + elif self.is_list(arg): # is list indexing list_name = arg.split('[')[0] self.add_variable_access_location(list_name, meta.line) before_index, after_index = arg.split(']', 1) @@ -1998,28 +2053,76 @@ def process_argument(self, meta, arg): else: return self.process_variable(arg, meta.line) + def print(self, meta, args): + args_new = [self.make_print_ask_arg(a, meta) for a in args] + if not self.microbit: + argument_string = ' '.join(args_new) + exception = self.make_index_error_check_if_list(args) + return exception + f"print(f'{argument_string}'){self.add_debug_breakpoint()}" + else: + parts = [] + current_part = [] + for arg in args_new: + if 'random.choice' in arg: + if current_part: + parts.append(" ".join(current_part)) + current_part = [] + parts.append(arg) + else: + current_part.append(arg) + if current_part: + parts.append(" ".join(current_part)) + + display_scrolls = [f"display.scroll({part})" if 'random.choice' in part else f"display.scroll({part})" for + part in parts] + return "\n".join(display_scrolls) + def add(self, meta, args): - value = self.process_argument(meta, args[0]) - list_var = args[1] + if not self.microbit: + value = self.process_argument(meta, args[0]) + list_var = args[1] - # both sides have been used now - self.add_variable_access_location(value, meta.line) - self.add_variable_access_location(list_var, meta.line) - return f"{list_var}.append({value}){self.add_debug_breakpoint()}" + # both sides have been used now + self.add_variable_access_location(value, meta.line) + self.add_variable_access_location(list_var, meta.line) + return f"{list_var}.append({value}){self.add_debug_breakpoint()}" + else: + value = self.process_argument(meta, args[0]) + list_var = args[1] + + # both sides have been used now + self.add_variable_access_location(value, meta.line) + self.add_variable_access_location(list_var, meta.line) + return f" {list_var}.append({value}){self.add_debug_breakpoint()}" def remove(self, meta, args): - value = self.process_argument(meta, args[0]) - list_var = args[1] + if not self.microbit: + value = self.process_argument(meta, args[0]) + list_var = args[1] - # both sides have been used now - self.add_variable_access_location(value, meta.line) - self.add_variable_access_location(list_var, meta.line) + # both sides have been used now + self.add_variable_access_location(value, meta.line) + self.add_variable_access_location(list_var, meta.line) - return textwrap.dedent(f"""\ - try: - {list_var}.remove({value}){self.add_debug_breakpoint()} - except: - pass""") + return textwrap.dedent(f"""\ + try: + {list_var}.remove({value}){self.add_debug_breakpoint()} + except: + pass""") + else: + value = self.process_argument(meta, args[0]) + list_var = args[1] + + # both sides have been used now + self.add_variable_access_location(value, meta.line) + self.add_variable_access_location(list_var, meta.line) + + code = textwrap.dedent(f"""\ + try: + {list_var}.remove({value}) + except: + pass""") + return textwrap.indent(code, " ") @v_args(meta=True) @@ -3075,7 +3178,8 @@ def get_parser(level, lang="en", keep_all_tokens=False, skip_faulty=False): ParseResult = namedtuple('ParseResult', ['code', 'source_map', 'has_turtle', - 'has_pressed', 'has_clear', 'has_music', 'has_sleep', 'commands', 'roles_of_variables']) + 'has_pressed', 'has_clear', 'has_music', 'has_sleep', 'commands', + 'roles_of_variables']) def transpile_inner_with_skipping_faulty(input_string, level, lang="en", unused_allowed=True): diff --git a/tests/test_level/test_level_02.py b/tests/test_level/test_level_02.py index 16f9130868c..50d083036c0 100644 --- a/tests/test_level/test_level_02.py +++ b/tests/test_level/test_level_02.py @@ -557,6 +557,12 @@ def test_sleep(self): self.multi_level_tester(code=code, expected=expected) + def test_sleep_micro_bit(self): + code = "sleep 1" + expected = " sleep(1000)" + + self.multi_level_tester(code=code, expected=expected, max_level=5, microbit=True) + def test_sleep_with_default_number(self): code = "sleep 1" expected = HedyTester.sleep_command_transpiled('"1"') @@ -654,6 +660,12 @@ def test_assign(self): self.multi_level_tester(code=code, expected=expected, max_level=11, unused_allowed=True) + def test_assign_micro_bit(self): + code = "naam is Felienne" + expected = " naam = 'Felienne'" + + self.multi_level_tester(code=code, expected=expected, max_level=5, unused_allowed=True, microbit=True) + def test_assign_catalan_var_name(self): code = textwrap.dedent("""\ print És hora una nit de Netflix diff --git a/tests/test_level/test_level_03.py b/tests/test_level/test_level_03.py index 30dd879a130..e7fa42b058c 100644 --- a/tests/test_level/test_level_03.py +++ b/tests/test_level/test_level_03.py @@ -640,6 +640,23 @@ def test_add_text_to_list(self): extra_check_function=self.result_in(['koe', 'kiep', 'muis']), ) + def test_add_text_to_list_microbit(self): + code = textwrap.dedent("""\ + dieren is koe, kiep + add muis to dieren + print dieren at random""") + + expected = (" dieren = ['koe', 'kiep']\n" + " dieren.append('muis')\n" + 'display.scroll(random.choice(dieren))') + + self.single_level_tester( + code=code, + expected=expected, + level=3, + microbit=True + ) + # add/remove tests (IMAN) # def test_add_text_to_list_numerical(self): @@ -991,7 +1008,7 @@ def test_3778_at_random(self): self.multi_level_tester(code=code, expected=expected, max_level=11) -# music tests + # music tests def test_play_random(self): code = textwrap.dedent("""\ notes is C4, E4, D4, F4, G4