Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✏️ Implementing Lists, at random from lists, add and remove for Micro:bit #5589

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a74ab64
Implemented "is" in Micro:bit
rmagedon97 Jun 3, 2024
6a2fa46
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
f32d60a
Adding the sleep command in Micro:bit
rmagedon97 Jun 3, 2024
ff08dec
Merge branch 'Microbit-Level_1' of https://github.com/hedyorg/hedy in…
rmagedon97 Jun 3, 2024
9db42bf
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
23b0c95
Adding a test for "is"
rmagedon97 Jun 3, 2024
4cab818
Merge branch 'Microbit-Level_1' of https://github.com/hedyorg/hedy in…
rmagedon97 Jun 3, 2024
4c2a803
fixed "is" test and added sleep test for Micro:bit
rmagedon97 Jun 3, 2024
5d6deb4
list and print at random from list
rmagedon97 Jun 3, 2024
2472e71
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
be577c6
Adding the functionality to or remove elements from a list.
rmagedon97 Jun 3, 2024
60ba07a
Merge branch 'Microbit-Level_3' of https://github.com/hedyorg/hedy in…
rmagedon97 Jun 3, 2024
e103d2d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 3, 2024
458308f
fix
rmagedon97 Jun 3, 2024
c6f92b7
Merge branch 'Microbit-Level_3' of https://github.com/hedyorg/hedy in…
rmagedon97 Jun 3, 2024
18739af
test update but still cant figure out the white space in front
rmagedon97 Jun 4, 2024
85e6a1d
Merge branch 'main' into Microbit-Level_3
rmagedon97 Jun 4, 2024
df242e1
Test is fixed
rmagedon97 Jun 4, 2024
5cbefcc
Merge branch 'Microbit-Level_3' of https://github.com/hedyorg/hedy in…
rmagedon97 Jun 4, 2024
eee0feb
Merge branch 'main' into Microbit-Level_3
rmagedon97 Jun 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand All @@ -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
Expand Down
238 changes: 171 additions & 67 deletions hedy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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']

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand All @@ -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:
Expand Down Expand Up @@ -1932,94 +1939,190 @@ 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)
return before_index + '-1' + ']' + after_index # account for 1-based indexing
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)
Expand Down Expand Up @@ -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):
Expand Down
12 changes: 12 additions & 0 deletions tests/test_level/test_level_02.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"')
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading