Skip to content

Commit

Permalink
Sublevels (hedyorg#412)
Browse files Browse the repository at this point in the history
* Adds 'step' to for loop in preparation for levels 7-1 and 7-2
* Adds grammars for levels 7-1 and 7-2. Started adding support for sublevels
* Adds 7-1/7-2 grammar parser to hedy.py
* Updates the texts for levels 7-1/2 and 8

Co-authored-by: Federico Pereiro <[email protected]>
Co-authored-by: Felienne <[email protected]>
  • Loading branch information
3 people authored Jun 1, 2021
1 parent b048fec commit 5f0541a
Show file tree
Hide file tree
Showing 18 changed files with 711 additions and 38 deletions.
20 changes: 16 additions & 4 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,15 @@ def parse():
return "body.code must be a string", 400
if 'level' not in body:
return "body.level must be a string", 400
if 'sublevel' in body and not type_check (body ['sublevel'], 'int'):
return "If present, body.sublevel must be an integer", 400
if 'adventure_name' in body and not type_check (body ['adventure_name'], 'str'):
return "if present, body.adventure_name must be a string", 400

code = body ['code']
level = int(body ['level'])
sublevel = body.get ('sublevel') or 0

# Language should come principally from the request body,
# but we'll fall back to browser default if it's missing for whatever
# reason.
Expand All @@ -255,7 +259,7 @@ def parse():
try:
hedy_errors = TRANSLATIONS.get_translations(lang, 'HedyErrorMessages')
with querylog.log_time('transpile'):
result = hedy.transpile(code, level)
result = hedy.transpile(code, level,sublevel)
response["Code"] = "# coding=utf8\nimport random\n" + result
except hedy.HedyException as E:
traceback.print_exc()
Expand Down Expand Up @@ -426,14 +430,22 @@ def adventure_page(adventure_name, level):
adventure_name=adventure_name)

# routing to index.html
@app.route('/hedy', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/hedy', methods=['GET'], defaults={'level': '1', 'step': 1})
@app.route('/hedy/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/hedy/<level>/<step>', methods=['GET'])
def index(level, step):
try:


# Sublevel requested
if re.match ('\d+-\d+', level):
pass
# If level has a dash, we keep it as a string
# Normal level requested
elif re.match ('\d', level):
g.level = level = int(level)
except:
else:
return 'No such Hedy level!', 404

g.lang = requested_lang()
g.prefix = '/hedy'

Expand Down
6 changes: 5 additions & 1 deletion coursedata/course/hedy/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ course:
- level: "4"
- level: "5"
- level: "6"
- level: "7"
-
level: "7"
subs:
- sub: "1"
- sub: "2"
- level: "8"
- level: "9"
- level: "10"
Expand Down
59 changes: 54 additions & 5 deletions coursedata/level-defaults/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,63 @@
else
repeat 5 times
print 'meh'
7-1:
start_code: "for counter is 1 to 5\n print counter"
intro_text:
"`print` works just the same but the `repeat` is now replaced by `for`!
You can now write `for counter is 1 to 5` and use `counter` in your program.
Try it out to see what happens!
Remember to use indentations after the `for` and `if` statements (That means starting a sentence with four spaces)"
commands:
- name: "for loop"
explanation: "We replace `repeat` with `for` and add a range"
example: "for counter is 1 to 5"
demo_code: "for counter is 1 to 5\n print counter"
- explanation: "Ask for the answer to a sum and check if it is correct. We can now print 2 lines."
example: "Example: answer is ask What is 5 plus 5?"
demo_code: |-
answer is ask What is 5 plus 5?
if answer is 10
print 'Well done!'
print 'Indeed, the answer was ' answer
else
print 'Oops!'
print 'The answer is 10'
7-2:
start_code: "for counter in range 1 to 5\n print counter"
intro_text:
"`print` works just the same but the `repeat` is now replaced by `for`!
You can now write `for counter in range 1 to 5` and use `counter` in your program. Try it out to see what happens!
Remember to use indentations after the `for` and `if` statements (That means starting a sentence with four spaces)"
commands:
- name: "for loop"
explanation: "We replace `repeat` with `for` and add a range"
example: "for counter in range 1 to 5"
demo_code: "for counter in range 1 to 5\n print counter"
- explanation: "Ask for the answer to a sum and check if it is correct. We can now print 2 lines."
example: "Example: answer is ask What is 5 plus 5?"
demo_code: |-
answer is ask What is 5 plus 5?
if answer is 10
print 'Well done!'
print 'Indeed, the answer was ' answer
else
print 'Oops!'
print 'The answer is 10'
8:
start_code: |-
for i in range 1 to 10
print i
print 'Ready or not, here I come!'
intro_text: "`print` works just the same but the `repeat` is now replaced by `for`! You use `for i in range 1 to 5`, instead of `repeat 5 times`. You can also use `i` in your program! Remember to use indentations after the `for` and `if` statements (That means starting a sentence with four spaces)"
for counter in range 2 to 10 step 2
print counter
print 'Steps of 2!'
intro_text: "We changed `for` again! Now you can use `step` in the for loop to skip through the counting faster. It is not necessary to use, so try loops with and without the step! Use `in range` to tell the for loop how far it needs to count."
commands:
- explanation: "we replace `repeat` with `for`"
- explanation: "We added a `step` to `for`"
example: "for counter in range 2 to 10 step 2"
demo_code: |
for counter in range 2 to 10 step 2
print counter
print 'Steps of 2!'
- explanation: "You can also use shorter names in the for loop to make your program smaller. Try using `i` as a counter for example."
example: "for i in range 1 to 10"
demo_code: |
for i in range 1 to 10
Expand Down
26 changes: 22 additions & 4 deletions courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import docs

import re
from utils import type_check

class LevelDefaults:
def __init__(self, language):
Expand All @@ -14,12 +16,15 @@ def __init__(self, language):
def levels(self):
return load_yaml(f'coursedata/level-defaults/{self.language}.yaml')

def get_defaults(self, level):
def get_defaults(self, level, sub=0):
"""Return the level defaults for a given level number.
Returns: Default
"""
return copy.deepcopy(self.levels.get(int(level), {}))
if sub:
return copy.deepcopy(self.levels.get(str(level) + "-" + str(sub), {}))
else:
return copy.deepcopy(self.levels.get(int(level), {}))


class NoSuchDefaults:
Expand Down Expand Up @@ -58,16 +63,18 @@ def max_step(self, level):
if level_ix >= len(self.course): return 0
return len(self.course[level_ix].get('assignments', []))

def get_assignment(self, level, number):
def get_assignment(self, level, number, sublevel=0):

"""Return the 1-based Assignment from this course."""
level_ix = int(level) - 1
if level_ix >= len(self.course): return None

assignments = self.course[level_ix].get('assignments')

assignment_values = {
"level": str(level),
}
assignment_values.update(**self.defaults.get_defaults(int(level)))
assignment_values.update(**self.defaults.get_defaults(int(level), sublevel))

# If we don't have any "assignments", return a default Assignment object
# based off the level and the level defaults. This is used in the Hedy main
Expand Down Expand Up @@ -95,6 +102,17 @@ def validate_course(self):
for level_i, level in enumerate(self.course):
expected_value = str(level_i + 1)
actual_value = level.get('level')

# Check for sub levels
subs = level.get('subs')
if (subs):
for sub_i, sub in enumerate(subs):
expected_sub = str(sub_i + 1)
actual_sub = sub.get('sub')
if expected_sub != actual_sub:
raise RuntimeError(f'Expected \'sub: "{expected_sub}"\' but got "{actual_sub}" in {self.course_name}-{self.language}')


if expected_value != actual_value:
raise RuntimeError(f'Expected \'level: "{expected_value}"\' but found "{actual_value}" in {self.course_name}-{self.language}')

Expand Down
2 changes: 1 addition & 1 deletion grammars/level10.lark
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ in_list_check: textwithoutspaces " in " var
?atom: NUMBER | var | NAME //TODO: means we cannot assign strings with spaces? would we want that?

//new for level 8
for_loop: "for " (NAME | var) " in " "range " (NUMBER | var) " to " (NUMBER | var) ":" _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"
for_loop: "for " (NAME | var) " in " "range " (NUMBER | var) " to " (NUMBER | var) (" step " (NUMBER | var))? ":" _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"

var: NAME -> var
list_access : var " at " (index | random) -> list_access //todo: could be merged with list_access_var?
Expand Down
2 changes: 1 addition & 1 deletion grammars/level11.lark
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ in_list_check: textwithoutspaces " in " var
?atom: NUMBER | var | NAME //TODO: means we cannot assign strings with spaces? would we want that?

//new for level 8
for_loop: "for " (NAME | var) " in " "range(" (NUMBER | var) (", "|",") (NUMBER | var) "):" _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"
for_loop: "for " (NAME | var) " in " "range(" (NUMBER | var) (", "|",") (NUMBER | var) ((", "|",") (NUMBER | var))? "):" _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"

var: NAME -> var
list_access : var " at " (index | random) -> list_access //todo: could be merged with list_access_var?
Expand Down
2 changes: 1 addition & 1 deletion grammars/level12.lark
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ in_list_check: textwithoutspaces " in " var
?atom: NUMBER | var | NAME //TODO: means we cannot assign strings with spaces? would we want that?

//new for level 8
for_loop: "for " (NAME | var) " in " "range(" (NUMBER | var)(", "|",") (NUMBER | var) "):" _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"
for_loop: "for " (NAME | var) " in " "range(" (NUMBER | var)(", "|",") (NUMBER | var) ((", "|",") (NUMBER | var))? "):" _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"

//new for level 12, assignment of list[i]
change_list_item : var "[" (index | var) "] is " (var | textwithoutspaces)
Expand Down
2 changes: 1 addition & 1 deletion grammars/level13.lark
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ in_list_check: textwithoutspaces " in " var
?atom: NUMBER | var | NAME //TODO: means we cannot assign strings with spaces? would we want that?

//new for level 8
for_loop: "for " (NAME | var) " in " "range(" (NUMBER | var)(", "|",") (NUMBER | var) "):" _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"
for_loop: "for " (NAME | var) " in " "range(" (NUMBER | var)(", "|",") (NUMBER | var) ((", "|",") (NUMBER | var))? "):" _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"

//new for level 12, assignment of list[i]
change_list_item : var "[" (index | var) "] is " (var | textwithoutspaces)
Expand Down
75 changes: 75 additions & 0 deletions grammars/level7-1.lark
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// trying to simply copy level 6 allowing for spaces
// the idea here is:
// allows for almost the same grammar as 6, easy for us
// gives better error messages out of the box
// we check indents in code, we can give better indent errors

start: program
program: _EOL* command (" ")* (_EOL+ command (" ")*)* _EOL* //lines may end on spaces and might be separated by many newlines
?command: print
| ifs elses?
| ask
| for_loop
| assign_list
| list_access_var
| assign //placing it here means print is will print 'is' and print is Felienne will print 'is Felienne'

//we had to drop invalid, I think because of the if-ifelse, but for now it is pretty ok!!

_EOL: "\r"?"\n"


print : "print " (quoted_text | list_access | var | sum) (" " (quoted_text | list_access | var | sum))*
ask : var " is ask " textwithspaces*
assign_list: var " is " textwithspaces ((", "|",") textwithspaces)+

//TODO: sum needs to be expression here too (like in 7 and up)

assign : var " is " sum | var " is " textwithoutspaces
invalid: textwithoutspaces " " textwithspaces


// new commands for level 4
elses : _EOL "else" _EOL (" "+ command) (_EOL " "+ command)*
ifs: "if " condition _EOL (" "+ command) (_EOL " "+ command)* //'if' cannot be used in Python, hence the name of the rule is 'ifs'


condition: (equality_check|in_list_check) (" and " condition)*
list_access_var : var " is " var " at " (index | random)
equality_check: textwithoutspaces " is " textwithoutspaces
in_list_check: textwithoutspaces " in " var

//new for level 6
?sum: product
| sum " "* "+" " "* product -> addition
| sum " "* "-" " "* product -> substraction

?product: atom
| product " "* "*" " "* atom -> multiplication
| product " "* "/" " "* atom -> division

?atom: NUMBER | var | NAME //TODO: means we cannot assign strings with spaces? would we want that?

//new for level 8
for_loop: "for " (NAME | var) " is " (NUMBER | var) " to " (NUMBER | var) _EOL (" "+ command) (_EOL " "+ command)*

var: NAME -> var
list_access : var " at " (index | random) -> list_access //todo: could be merged with list_access_var?
index : NUMBER
random : "random"

textwithspaces: /([^\n,]+)/ -> text //anything can be parsed except for a newline and a comma for list separators
textwithoutspaces: /([^\n, *+-\/]+)/ -> text //anything can be parsed except for spaces (plus: a newline and a comma for list separators)
//plus in level 6, calculation elements

quoted_text_no_escape: /'([^']*)'/ -> text //simply all between quotes should this be used at earlier levels?
quoted_text: /'((?:[^\\']|\\.)*)'/ -> text //text can be between single quotes, but quotes may be escaped with \



%import common.LETTER // imports from terminal library
%import common.DIGIT // imports from terminal library
%import common.WS_INLINE // imports from terminal library
%import common.NEWLINE // imports from terminal library
%import common.SIGNED_INT -> NUMBER
%import common.CNAME -> NAME
75 changes: 75 additions & 0 deletions grammars/level7-2.lark
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// trying to simply copy level 6 allowing for spaces
// the idea here is:
// allows for almost the same grammar as 6, easy for us
// gives better error messages out of the box
// we check indents in code, we can give better indent errors

start: program
program: _EOL* command (" ")* (_EOL+ command (" ")*)* _EOL* //lines may end on spaces and might be separated by many newlines
?command: print
| ifs elses?
| ask
| for_loop
| assign_list
| list_access_var
| assign //placing it here means print is will print 'is' and print is Felienne will print 'is Felienne'

//we had to drop invalid, I think because of the if-ifelse, but for now it is pretty ok!!

_EOL: "\r"?"\n"


print : "print " (quoted_text | list_access | var | sum) (" " (quoted_text | list_access | var | sum))*
ask : var " is ask " textwithspaces*
assign_list: var " is " textwithspaces ((", "|",") textwithspaces)+

//TODO: sum needs to be expression here too (like in 7 and up)

assign : var " is " sum | var " is " textwithoutspaces
invalid: textwithoutspaces " " textwithspaces


// new commands for level 4
elses : _EOL "else" _EOL (" "+ command) (_EOL " "+ command)*
ifs: "if " condition _EOL (" "+ command) (_EOL " "+ command)* //'if' cannot be used in Python, hence the name of the rule is 'ifs'


condition: (equality_check|in_list_check) (" and " condition)*
list_access_var : var " is " var " at " (index | random)
equality_check: textwithoutspaces " is " textwithoutspaces
in_list_check: textwithoutspaces " in " var

//new for level 6
?sum: product
| sum " "* "+" " "* product -> addition
| sum " "* "-" " "* product -> substraction

?product: atom
| product " "* "*" " "* atom -> multiplication
| product " "* "/" " "* atom -> division

?atom: NUMBER | var | NAME //TODO: means we cannot assign strings with spaces? would we want that?

//new for level 8
for_loop: "for " (NAME | var) " in " "range " (NUMBER | var) " to " (NUMBER | var) _EOL (" "+ command) (_EOL " "+ command)*

var: NAME -> var
list_access : var " at " (index | random) -> list_access //todo: could be merged with list_access_var?
index : NUMBER
random : "random"

textwithspaces: /([^\n,]+)/ -> text //anything can be parsed except for a newline and a comma for list separators
textwithoutspaces: /([^\n, *+-\/]+)/ -> text //anything can be parsed except for spaces (plus: a newline and a comma for list separators)
//plus in level 6, calculation elements

quoted_text_no_escape: /'([^']*)'/ -> text //simply all between quotes should this be used at earlier levels?
quoted_text: /'((?:[^\\']|\\.)*)'/ -> text //text can be between single quotes, but quotes may be escaped with \



%import common.LETTER // imports from terminal library
%import common.DIGIT // imports from terminal library
%import common.WS_INLINE // imports from terminal library
%import common.NEWLINE // imports from terminal library
%import common.SIGNED_INT -> NUMBER
%import common.CNAME -> NAME
2 changes: 1 addition & 1 deletion grammars/level8.lark
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ in_list_check: textwithoutspaces " in " var
?atom: NUMBER | var | NAME //TODO: means we cannot assign strings with spaces? would we want that?

//new for level 8
for_loop: "for " (NAME | var) " in " "range " (NUMBER | var) " to " (NUMBER | var) _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"
for_loop: "for " (NAME | var) " in " "range " (NUMBER | var) " to " (NUMBER | var) (" step " (NUMBER | var))? _EOL (" "+ command) (_EOL " "+ command)* _EOL "end-block"

var: NAME -> var
list_access : var " at " (index | random) -> list_access //todo: could be merged with list_access_var?
Expand Down
Loading

0 comments on commit 5f0541a

Please sign in to comment.