From a37cbb190a567cb67455e98abdcd4f105f030c6b Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Fri, 5 Jun 2020 12:08:56 -0500 Subject: [PATCH 01/13] refactor: move levels.json data into classes The codebase has been refactored so that each level contains its own title and description, removing the need for a separate levels.json file. --- assets/levels.json | 6 ------ assets/levels.png.import | 34 ++++++++++++++++++++++++++++++++++ levels/bogo_sort.gd | 12 ++++++++---- levels/bubble_sort.gd | 36 +++++++++++++++++++++--------------- levels/comparison_sort.gd | 38 +++++++++++++++++++------------------- scripts/levels.gd | 34 +++++++++++++++------------------- scripts/play.gd | 5 ++--- views/array_view.gd | 2 +- 8 files changed, 100 insertions(+), 67 deletions(-) delete mode 100644 assets/levels.json create mode 100644 assets/levels.png.import diff --git a/assets/levels.json b/assets/levels.json deleted file mode 100644 index 2cc1797..0000000 --- a/assets/levels.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "BUBBLE SORT": - { - "about": "Bubble sort iterates through the array and looks at each pair of elements, swapping them if they are out of order. When it has gone through the entire array without swapping a single pair, it has finished. Though simple to understand, bubble sort is hopelessly inefficient on all but the smallest of arrays." - } -} diff --git a/assets/levels.png.import b/assets/levels.png.import new file mode 100644 index 0000000..d4c8f5d --- /dev/null +++ b/assets/levels.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/levels.png-a6e910382d32e95b5c3f382bb559bde4.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/levels.png" +dest_files=[ "res://.import/levels.png-a6e910382d32e95b5c3f382bb559bde4.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/levels/bogo_sort.gd b/levels/bogo_sort.gd index 76e5692..517d994 100644 --- a/levels/bogo_sort.gd +++ b/levels/bogo_sort.gd @@ -1,14 +1,18 @@ extends ComparisonSort class_name BogoSort +const TITLE = "BOGOSORT" +const ABOUT = """Generates random permutations until the array is +sorted.""" + func _init(array).(array): - pass + pass func check(action): - return true + return true func next(): - array = ArrayModel.new(array.size) + array = ArrayModel.new(array.size) func emphasized(i): - return false + return false diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index 801cd82..929ff8e 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -1,27 +1,33 @@ extends ComparisonSort class_name BubbleSort +const TITLE = "BUBBLE SORT" +const ABOUT = """Bubble sort iterates through the array and looks at +each pair of elements, swapping them if they are out of order. When it +has gone through the entire array without swapping a single pair, it has +finished. Though simple to understand, bubble sort is hopelessly +inefficient on all but the smallest of arrays.""" var swapped = false func _init(array).(array): - pass + pass func check(action): - if array.get(index) > array.get(index + 1): - return action == "swap" - else: - return action == "no_swap" + if array.get(index) > array.get(index + 1): + return action == "swap" + else: + return action == "no_swap" func next(): - if array.get(index) > array.get(index + 1): - array.swap(index, index + 1) - swapped = true - index += 1 - if index == array.size - 1: - if not swapped: - emit_signal("done") - index = 0 - swapped = false + if array.get(index) > array.get(index + 1): + array.swap(index, index + 1) + swapped = true + index += 1 + if index == array.size - 1: + if not swapped: + emit_signal("done") + index = 0 + swapped = false func emphasized(i): - return i == index or i == index + 1 + return i == index or i == index + 1 diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd index dbb0bf0..1f9c19b 100644 --- a/levels/comparison_sort.gd +++ b/levels/comparison_sort.gd @@ -12,32 +12,32 @@ var timer = Timer.new() var active = true func _init(array): - self.array = array - timer.one_shot = true - timer.connect("timeout", self, "_on_Timer_timeout") - add_child(timer) - self.connect("mistake", self, "_on_ComparisonSort_mistake") + self.array = array + timer.one_shot = true + timer.connect("timeout", self, "_on_Timer_timeout") + add_child(timer) + self.connect("mistake", self, "_on_ComparisonSort_mistake") func check(action): - pass + pass func next(): - pass + pass func _on_ComparisonSort_mistake(): - active = false - timer.start(1) + active = false + timer.start(1) func _on_Timer_timeout(): - active = true + active = true func _input(event): - if not active: - return - - for action in ACTIONS: - if event.is_action_pressed(action): - if check(action): - next() - else: - emit_signal("mistake") + if not active: + return + + for action in ACTIONS: + if event.is_action_pressed(action): + if check(action): + next() + else: + emit_signal("mistake") diff --git a/scripts/levels.gd b/scripts/levels.gd index 834aa83..43be06c 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -1,29 +1,26 @@ extends HBoxContainer -var levels: Dictionary -var level +var levels = [ + BubbleSort, +] +var level: ComparisonSort func _ready(): - # Load level data - var descriptions = File.new() - descriptions.open("res://assets/levels.json", File.READ) - levels = parse_json(descriptions.get_as_text()) - # Dynamically add buttons + # Dynamically load level data for level in levels: var button = Button.new() - button.text = level + button.text = level.TITLE button.align = Button.ALIGN_LEFT button.connect("focus_entered", self, "_on_Button_focus_changed") - button.connect("pressed", self, "_on_Button_pressed", [level]) + button.connect("pressed", self, "_on_Button_pressed", [level.TITLE]) $LevelsBorder/Levels.add_child(button) # Automatically focus on first button $LevelsBorder/Levels.get_child(0).grab_focus() func _on_Button_focus_changed(): - var name = get_focus_owner().text - $Preview/InfoBorder/Info/Description.text = levels[name]["about"] - level = get_level(name).new(ArrayModel.new(10)) + level = get_level(get_focus_owner().text).new(ArrayModel.new(10)) level.active = false + $Preview/InfoBorder/Info/Description.text = level.ABOUT.replace("\n", " ") # Start over when simulation is finished level.connect("done", self, "_on_Button_focus_changed") # Replace old display with new @@ -31,14 +28,13 @@ func _on_Button_focus_changed(): child.queue_free() $Preview/Display.add_child(ArrayView.new(level)) -func _on_Button_pressed(level): - scene.change_scene("res://scenes/play.tscn", - {"name": level, "level": get_level(level)}) +func _on_Button_pressed(title): + scene.change_scene("res://scenes/play.tscn", {"level": get_level(title)}) -func get_level(level): - match level: - "BUBBLE SORT": - return BubbleSort +func get_level(title): + for level in levels: + if level.TITLE == title: + return level func _on_Timer_timeout(): level.next() diff --git a/scripts/play.gd b/scripts/play.gd index 295745e..f9c021c 100644 --- a/scripts/play.gd +++ b/scripts/play.gd @@ -3,7 +3,7 @@ extends VBoxContainer var start_time = -1 func _ready(): - $HUDBorder/HUD/Level.text = scene.get_param("name") + $HUDBorder/HUD/Level.text = scene.get_param("level").TITLE func _process(delta): if start_time >= 0: @@ -14,8 +14,7 @@ func _on_Timer_timeout(): # Delete ready text $DisplayBorder/Label.queue_free() # Load level - var array = ArrayModel.new(10) - var level = scene.get_param("level").new(array) + var level = scene.get_param("level").new(ArrayModel.new(10)) level.connect("done", self, "_on_Level_done") $DisplayBorder.add_child(ArrayView.new(level)) diff --git a/views/array_view.gd b/views/array_view.gd index 4bb5707..e1def82 100644 --- a/views/array_view.gd +++ b/views/array_view.gd @@ -4,7 +4,7 @@ class_name ArrayView const GREEN = Color(0.2, 1, 0.2) const ORANGE = Color(1, 0.69, 0) -var level +var level: ComparisonSort var rects = [] func _init(level): From 3c393fd885c6bbd3f0bfd7916090387800422fa6 Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Fri, 5 Jun 2020 13:10:32 -0500 Subject: [PATCH 02/13] feat: add insertion sort Added a complete insertion sort implementation including description. Made left arrow swap again because it makes more sense with insertion sort. Also included a nice quality-of-life feature where the game remembers the last played level and automatically highlights it for the next game. --- levels/bubble_sort.gd | 1 + levels/comparison_sort.gd | 1 - levels/insertion_sort.gd | 38 ++++++++++++++++++++++++++++++++++++++ project.godot | 12 +++++++++--- scenes/levels.tscn | 4 ++-- scripts/end.gd | 3 ++- scripts/levels.gd | 7 +++++-- scripts/play.gd | 3 ++- 8 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 levels/insertion_sort.gd diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index 929ff8e..7a3a577 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -7,6 +7,7 @@ each pair of elements, swapping them if they are out of order. When it has gone through the entire array without swapping a single pair, it has finished. Though simple to understand, bubble sort is hopelessly inefficient on all but the smallest of arrays.""" +var index = 0 var swapped = false func _init(array).(array): diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd index 1f9c19b..f771e58 100644 --- a/levels/comparison_sort.gd +++ b/levels/comparison_sort.gd @@ -7,7 +7,6 @@ signal mistake const ACTIONS = ["swap", "no_swap"] var array: ArrayModel -var index = 0 var timer = Timer.new() var active = true diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd new file mode 100644 index 0000000..f10f9e0 --- /dev/null +++ b/levels/insertion_sort.gd @@ -0,0 +1,38 @@ +extends ComparisonSort +class_name InsertionSort + +const TITLE = "INSERTION SORT" +const ABOUT = """Insertion sort goes through the array and inserts each +element into its correct position. It is most similar to how most people +would sort a deck of cards. It is also slow on large arrays but it is +one of the faster quadratic algorithms. It is often used to sort smaller +subarrays in hybrid sorting algorithms.""" +var end = 1 +var index = end + +func _init(array).(array): + pass + +func check(action): + if array.get(index - 1) > array.get(index): + return action == "swap" + else: + return action == "no_swap" + +func next(): + if array.get(index - 1) > array.get(index): + array.swap(index - 1, index) + index -= 1 + if index == 0: + _grow() + else: + _grow() + +func _grow(): + end += 1 + if end == array.size: + emit_signal("done") + index = end + +func emphasized(i): + return i == index or i == index - 1 diff --git a/project.godot b/project.godot index b66bd5c..b81f97e 100644 --- a/project.godot +++ b/project.godot @@ -33,13 +33,19 @@ _global_script_classes=[ { "class": "ComparisonSort", "language": "GDScript", "path": "res://levels/comparison_sort.gd" +}, { +"base": "ComparisonSort", +"class": "InsertionSort", +"language": "GDScript", +"path": "res://levels/insertion_sort.gd" } ] _global_script_class_icons={ "ArrayModel": "", "ArrayView": "", "BogoSort": "", "BubbleSort": "", -"ComparisonSort": "" +"ComparisonSort": "", +"InsertionSort": "" } [application] @@ -106,12 +112,12 @@ ui_down={ } swap={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null) ] } no_swap={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null) ] } diff --git a/scenes/levels.tscn b/scenes/levels.tscn index c67e806..36ed5b2 100644 --- a/scenes/levels.tscn +++ b/scenes/levels.tscn @@ -85,8 +85,8 @@ size_flags_horizontal = 3 size_flags_vertical = 3 text = "INSTRUCTIONS -LEFT ARROW: CONTINUE -RIGHT ARROW: SWAP" +LEFT ARROW: SWAP +RIGHT ARROW: CONTINUE" autowrap = true [node name="Timer" type="Timer" parent="."] diff --git a/scripts/end.gd b/scripts/end.gd index 1d14369..5dcc61a 100644 --- a/scripts/end.gd +++ b/scripts/end.gd @@ -5,4 +5,5 @@ func _ready(): $Button.grab_focus() func _on_Button_pressed(): - scene.change_scene("res://scenes/levels.tscn") + scene.change_scene("res://scenes/levels.tscn", + {"level": scene.get_param("level")}) diff --git a/scripts/levels.gd b/scripts/levels.gd index 43be06c..6367268 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -2,6 +2,7 @@ extends HBoxContainer var levels = [ BubbleSort, + InsertionSort, ] var level: ComparisonSort @@ -14,8 +15,10 @@ func _ready(): button.connect("focus_entered", self, "_on_Button_focus_changed") button.connect("pressed", self, "_on_Button_pressed", [level.TITLE]) $LevelsBorder/Levels.add_child(button) - # Automatically focus on first button - $LevelsBorder/Levels.get_child(0).grab_focus() + if scene.get_param("level") == level: + button.grab_focus() + if scene.get_param("level") == null: + $LevelsBorder/Levels.get_child(0).grab_focus() func _on_Button_focus_changed(): level = get_level(get_focus_owner().text).new(ArrayModel.new(10)) diff --git a/scripts/play.gd b/scripts/play.gd index f9c021c..9c85fc2 100644 --- a/scripts/play.gd +++ b/scripts/play.gd @@ -22,4 +22,5 @@ func get_score(): return stepify((OS.get_ticks_msec() - start_time) / 1000.0, 0.001) func _on_Level_done(): - scene.change_scene("res://scenes/end.tscn", {"score": get_score()}) + scene.change_scene("res://scenes/end.tscn", + {"level": scene.get_param("level"), "score": get_score()}) From 741dfe2c790a5060a4064c982ad97597fce38ba5 Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Tue, 9 Jun 2020 12:41:46 -0500 Subject: [PATCH 03/13] feat: add selection sort A modified version of selection sort is now available to play. To save development time, instead of just keeping track of the smallest element, it actually swaps every time it encounters a new low. This will likely have to be changed in a future release as it's pretty confusing. --- levels/selection_sort.gd | 33 +++++++++++++++++++++++++++++++++ project.godot | 8 +++++++- scripts/levels.gd | 1 + 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 levels/selection_sort.gd diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd new file mode 100644 index 0000000..538483e --- /dev/null +++ b/levels/selection_sort.gd @@ -0,0 +1,33 @@ +extends ComparisonSort +class_name SelectionSort + +const TITLE = "SELECTION SORT" +const ABOUT = """Selection sort incrementally builds a sorted array by +repeatedly looking for the smallest element and swapping it onto the +end of the sorted portion of the array, which initially starts with size +zero but grows after each round. It is faster than an unoptimized bubble +sort but slower than insertion sort.""" +var base = 0 +var index = base + 1 + +func _init(array).(array): + pass + +func check(action): + if array.get(base) > array.get(index): + return action == "swap" + else: + return action == "no_swap" + +func next(): + if array.get(base) > array.get(index): + array.swap(base, index) + index += 1 + if index == array.size: + base += 1 + index = base + 1 + if base == array.size - 1: + emit_signal("done") + +func emphasized(i): + return i == index or i == base diff --git a/project.godot b/project.godot index b81f97e..d06b71d 100644 --- a/project.godot +++ b/project.godot @@ -38,6 +38,11 @@ _global_script_classes=[ { "class": "InsertionSort", "language": "GDScript", "path": "res://levels/insertion_sort.gd" +}, { +"base": "ComparisonSort", +"class": "SelectionSort", +"language": "GDScript", +"path": "res://levels/selection_sort.gd" } ] _global_script_class_icons={ "ArrayModel": "", @@ -45,7 +50,8 @@ _global_script_class_icons={ "BogoSort": "", "BubbleSort": "", "ComparisonSort": "", -"InsertionSort": "" +"InsertionSort": "", +"SelectionSort": "" } [application] diff --git a/scripts/levels.gd b/scripts/levels.gd index 6367268..b94bf3c 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -3,6 +3,7 @@ extends HBoxContainer var levels = [ BubbleSort, InsertionSort, + SelectionSort, ] var level: ComparisonSort From 98b4a9097255cdd6ca618532494650ffc370bb6e Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Fri, 12 Jun 2020 16:36:14 -0500 Subject: [PATCH 04/13] refactor: comply with GDScript style guide I have rewritten my code to mostly comply with the Godot Doc's style guide, mainly prefixing private variables with an underscore and code order. One big exception is that I use 4 spaces instead of tabs for better viewing in Github. In addition, I have elected to not prefix public interface methods that are meant to be overriden with an underscore. Also added Python-style docstrings and reduced the number of magic constants. Per-level instructions have been added as well. --- levels/bogo_sort.gd | 14 ++++++++--- levels/bubble_sort.gd | 47 ++++++++++++++++++++--------------- levels/comparison_sort.gd | 52 ++++++++++++++++++++++++--------------- levels/insertion_sort.gd | 42 ++++++++++++++++++------------- levels/selection_sort.gd | 48 +++++++++++++++++++++--------------- models/array_model.gd | 13 +++++++--- project.godot | 7 +++--- scenes/levels.tscn | 4 +-- scripts/border.gd | 47 ++++++++++++++++++++--------------- scripts/credits.gd | 2 +- scripts/end.gd | 6 ++--- scripts/levels.gd | 29 +++++++++++++--------- scripts/menu.gd | 9 ++++--- scripts/play.gd | 12 ++++----- scripts/scene.gd | 4 +++ scripts/theme.gd | 9 +++++++ views/array_view.gd | 21 +++++++++------- 17 files changed, 221 insertions(+), 145 deletions(-) create mode 100644 scripts/theme.gd diff --git a/levels/bogo_sort.gd b/levels/bogo_sort.gd index 517d994..7efeb3b 100644 --- a/levels/bogo_sort.gd +++ b/levels/bogo_sort.gd @@ -1,9 +1,13 @@ -extends ComparisonSort class_name BogoSort +extends ComparisonSort -const TITLE = "BOGOSORT" -const ABOUT = """Generates random permutations until the array is -sorted.""" +const NAME = "BOGOSORT" +const ABOUT = """ +Generates random permutations until the array is sorted. +""" +const CONTROLS = """ +Keep on hitting RIGHT ARROW to CONTINUE and hope for the best! +""" func _init(array).(array): pass @@ -13,6 +17,8 @@ func check(action): func next(): array = ArrayModel.new(array.size) + if array.is_sorted(): + emit_signal("done") func emphasized(i): return false diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index 7a3a577..c74246b 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -1,34 +1,41 @@ -extends ComparisonSort class_name BubbleSort +extends ComparisonSort -const TITLE = "BUBBLE SORT" -const ABOUT = """Bubble sort iterates through the array and looks at -each pair of elements, swapping them if they are out of order. When it -has gone through the entire array without swapping a single pair, it has +const NAME = "BUBBLE SORT" +const ABOUT = """ +Bubble sort iterates through the array and looks at each pair of +elements, swapping them if they are out of order. When it has gone +through the entire array without swapping a single pair, it has finished. Though simple to understand, bubble sort is hopelessly -inefficient on all but the smallest of arrays.""" -var index = 0 -var swapped = false +inefficient on all but the smallest of arrays. +""" +const CONTROLS = """ +If the two highlighted elements are out of order, hit LEFT ARROW to SWAP +them. Otherwise, hit RIGHT ARROW to continue. +""" + +var _index = 0 +var _swapped = false func _init(array).(array): pass func check(action): - if array.get(index) > array.get(index + 1): - return action == "swap" + if array.get(_index) > array.get(_index + 1): + return action == ACTIONS.SWAP else: - return action == "no_swap" + return action == ACTIONS.NO_SWAP func next(): - if array.get(index) > array.get(index + 1): - array.swap(index, index + 1) - swapped = true - index += 1 - if index == array.size - 1: - if not swapped: + if array.get(_index) > array.get(_index + 1): + array.swap(_index, _index + 1) + _swapped = true + _index += 1 + if _index == array.size - 1: + if not _swapped: emit_signal("done") - index = 0 - swapped = false + _index = 0 + _swapped = false func emphasized(i): - return i == index or i == index + 1 + return i == _index or i == _index + 1 diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd index f771e58..cb43a70 100644 --- a/levels/comparison_sort.gd +++ b/levels/comparison_sort.gd @@ -1,42 +1,54 @@ -extends Node class_name ComparisonSort +extends Node signal done signal mistake -const ACTIONS = ["swap", "no_swap"] +enum ACTIONS {SWAP, NO_SWAP} + +const DISABLE_TIME = 1.0 var array: ArrayModel -var timer = Timer.new() var active = true +var _timer = Timer.new() + func _init(array): + """Initialize array and timer.""" self.array = array - timer.one_shot = true - timer.connect("timeout", self, "_on_Timer_timeout") - add_child(timer) + _timer.one_shot = true + _timer.connect("timeout", self, "_on_Timer_timeout") + add_child(_timer) self.connect("mistake", self, "_on_ComparisonSort_mistake") +func _input(event): + """Pass input events for checking and take appropriate action.""" + if not active: + return + for action in ACTIONS: + if event.is_action_pressed(action): + if check(ACTIONS[action]): + next() + else: + emit_signal("mistake") + func check(action): - pass + """Determine if the given action enum value is correct.""" + push_error("NotImplementedError") func next(): - pass + """Advance the state by one step and signal done if completed.""" + push_error("NotImplementedError") + +func emphasized(i): + """Return whether the given index should be highlighted.""" + push_error("NotImplementedError") func _on_ComparisonSort_mistake(): + """Disable the controls for one second.""" active = false - timer.start(1) + _timer.start(DISABLE_TIME) func _on_Timer_timeout(): + """Reenable the controls.""" active = true - -func _input(event): - if not active: - return - - for action in ACTIONS: - if event.is_action_pressed(action): - if check(action): - next() - else: - emit_signal("mistake") diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd index f10f9e0..afc8efc 100644 --- a/levels/insertion_sort.gd +++ b/levels/insertion_sort.gd @@ -1,38 +1,46 @@ -extends ComparisonSort class_name InsertionSort +extends ComparisonSort -const TITLE = "INSERTION SORT" -const ABOUT = """Insertion sort goes through the array and inserts each +const NAME = "INSERTION SORT" +const ABOUT = """ +Insertion sort goes through the array and inserts each element into its correct position. It is most similar to how most people would sort a deck of cards. It is also slow on large arrays but it is one of the faster quadratic algorithms. It is often used to sort smaller -subarrays in hybrid sorting algorithms.""" -var end = 1 -var index = end +subarrays in hybrid sorting algorithms. +""" +const CONTROLS = """ +Hit LEFT ARROW to SWAP the two highlighted elements as long as they are +out of order. When this is no longer the case, hit RIGHT ARROW to +advance. +""" + +var _end = 1 +var _index = _end func _init(array).(array): pass func check(action): - if array.get(index - 1) > array.get(index): - return action == "swap" + if array.get(_index - 1) > array.get(_index): + return action == ACTIONS.SWAP else: - return action == "no_swap" + return action == ACTIONS.NO_SWAP func next(): - if array.get(index - 1) > array.get(index): - array.swap(index - 1, index) - index -= 1 - if index == 0: + if array.get(_index - 1) > array.get(_index): + array.swap(_index - 1, _index) + _index -= 1 + if _index == 0: _grow() else: _grow() func _grow(): - end += 1 - if end == array.size: + _end += 1 + if _end == array.size: emit_signal("done") - index = end + _index = _end func emphasized(i): - return i == index or i == index - 1 + return i == _index or i == _index - 1 diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd index 538483e..4809f2a 100644 --- a/levels/selection_sort.gd +++ b/levels/selection_sort.gd @@ -1,33 +1,41 @@ -extends ComparisonSort class_name SelectionSort +extends ComparisonSort + +const NAME = "SELECTION SORT" +const ABOUT = """ +Selection sort incrementally builds a sorted array by repeatedly looking +for the smallest element and swapping it onto the end of the sorted +portion of the array, which initially starts with size zero but grows +after each round. It is faster than an unoptimized bubble sort but +slower than insertion sort. +""" +const CONTROLS = """ +Keep on hitting RIGHT ARROW until you encounter an element that is +smaller than the left highlighted element, then hit LEFT ARROW to swap +the new smallest into place and keep going. +""" -const TITLE = "SELECTION SORT" -const ABOUT = """Selection sort incrementally builds a sorted array by -repeatedly looking for the smallest element and swapping it onto the -end of the sorted portion of the array, which initially starts with size -zero but grows after each round. It is faster than an unoptimized bubble -sort but slower than insertion sort.""" -var base = 0 -var index = base + 1 +var _base = 0 +var _index = _base + 1 func _init(array).(array): pass func check(action): - if array.get(base) > array.get(index): - return action == "swap" + if array.get(_base) > array.get(_index): + return action == ACTIONS.SWAP else: - return action == "no_swap" + return action == ACTIONS.NO_SWAP func next(): - if array.get(base) > array.get(index): - array.swap(base, index) - index += 1 - if index == array.size: - base += 1 - index = base + 1 - if base == array.size - 1: + if array.get(_base) > array.get(_index): + array.swap(_base, _index) + _index += 1 + if _index == array.size: + _base += 1 + _index = _base + 1 + if _base == array.size - 1: emit_signal("done") func emphasized(i): - return i == index or i == base + return i == _index or i == _base diff --git a/models/array_model.gd b/models/array_model.gd index a27a86d..e3f7289 100644 --- a/models/array_model.gd +++ b/models/array_model.gd @@ -1,29 +1,34 @@ """ -A plain old one-dimensional random access array. +A plain old one-dimensional random access array wrapper around the +built-in class. """ -extends Reference class_name ArrayModel +extends Reference var array = [] -var size +var size = 0 -func _init(size): +func _init(size=16): + """Randomize the array.""" for i in range(1, size + 1): array.append(i) array.shuffle() self.size = size func get(i): + """Retrieve the value of the element at index i.""" return array[i] func is_sorted(): + """Check if the array is in monotonically increasing order.""" for i in range(size - 1): if array[i] > array[i + 1]: return false return true func swap(i, j): + """Swap the elements at indices i and j.""" var temp = array[i] array[i] = array[j] array[j] = temp diff --git a/project.godot b/project.godot index d06b71d..b9085b7 100644 --- a/project.godot +++ b/project.godot @@ -64,7 +64,8 @@ config/icon="res://assets/icon.png" [autoload] -scene="*res://scripts/scene.gd" +GlobalScene="*res://scripts/scene.gd" +GlobalTheme="*res://scripts/theme.gd" [display] @@ -116,12 +117,12 @@ ui_down={ , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null) ] } -swap={ +SWAP={ "deadzone": 0.5, "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null) ] } -no_swap={ +NO_SWAP={ "deadzone": 0.5, "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null) ] diff --git a/scenes/levels.tscn b/scenes/levels.tscn index 36ed5b2..ca08733 100644 --- a/scenes/levels.tscn +++ b/scenes/levels.tscn @@ -68,7 +68,7 @@ margin_right = 1352.0 margin_bottom = 352.0 custom_constants/separation = 50 -[node name="Description" type="Label" parent="LevelSelect/Preview/InfoBorder/Info"] +[node name="About" type="Label" parent="LevelSelect/Preview/InfoBorder/Info"] margin_right = 810.0 margin_bottom = 332.0 rect_min_size = Vector2( 810, 0 ) @@ -76,7 +76,7 @@ size_flags_vertical = 3 text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse luctus lorem felis, in imperdiet mauris faucibus in. Vestibulum interdum mi at arcu congue congue. Cras sodales mauris odio, eget iaculis dolor tempor quis. Suspendisse nec iaculis sapien, eu sollicitudin orci. Nulla volutpat pellentesque ex nec cursus." autowrap = true -[node name="Instructions" type="Label" parent="LevelSelect/Preview/InfoBorder/Info"] +[node name="Controls" type="Label" parent="LevelSelect/Preview/InfoBorder/Info"] margin_left = 860.0 margin_right = 1332.0 margin_bottom = 332.0 diff --git a/scripts/border.gd b/scripts/border.gd index 1a75c0d..e6d1899 100644 --- a/scripts/border.gd +++ b/scripts/border.gd @@ -1,32 +1,39 @@ +""" +A MarginContainer with a flashable colored border around it. +""" + extends MarginContainer -const GREEN = Color(0.2, 1, 0.2) -const RED = Color(1, 0, 0) const WIDTH = 5 -var color = GREEN -var timer = Timer.new() const FLASHES = 3 -var color_changes = 0 +const COLOR_CHANGES = FLASHES * 2 - 1 + +var _color = GlobalTheme.GREEN +var _timer = Timer.new() +var _color_changes = 0 func _ready(): - # Input should be reenabled right after the last red flash - timer.wait_time = 1.0 / (FLASHES * 2 - 1) - timer.connect("timeout", self, "_on_Timer_timeout") - add_child(timer) + """Time last return to green with reenabling of controls.""" + _timer.wait_time = ComparisonSort.DISABLE_TIME / COLOR_CHANGES + _timer.connect("timeout", self, "_on_Timer_timeout") + add_child(_timer) func flash(): - _on_Timer_timeout() - timer.start() + """Immediately flash red and then start timer.""" + _on_Timer_timeout() + _timer.start() func _on_Timer_timeout(): - if color_changes == FLASHES * 2 - 1: - timer.stop() - color_changes = 0 - color = GREEN - else: - color = RED if color_changes % 2 == 0 else GREEN - color_changes += 1 - update() + """Switch between green and red.""" + if _color_changes == COLOR_CHANGES: + _timer.stop() + _color_changes = 0 + _color = GlobalTheme.GREEN + else: + _color = GlobalTheme.RED if _color_changes % 2 == 0 else GlobalTheme.GREEN + _color_changes += 1 + update() func _draw(): - draw_rect(Rect2(Vector2(), rect_size), color, false, WIDTH) + """Draw the border.""" + draw_rect(Rect2(Vector2(), rect_size), _color, false, WIDTH) diff --git a/scripts/credits.gd b/scripts/credits.gd index e47684e..08ed1ee 100644 --- a/scripts/credits.gd +++ b/scripts/credits.gd @@ -10,4 +10,4 @@ func _process(delta): rect_position.y -= 1 func _input(event): - scene.change_scene("res://scenes/menu.tscn") + GlobalScene.change_scene("res://scenes/menu.tscn") diff --git a/scripts/end.gd b/scripts/end.gd index 5dcc61a..d39a4c0 100644 --- a/scripts/end.gd +++ b/scripts/end.gd @@ -1,9 +1,9 @@ extends VBoxContainer func _ready(): - $Score.text = str(scene.get_param("score")) + $Score.text = str(GlobalScene.get_param("score")) $Button.grab_focus() func _on_Button_pressed(): - scene.change_scene("res://scenes/levels.tscn", - {"level": scene.get_param("level")}) + GlobalScene.change_scene("res://scenes/levels.tscn", + {"level": GlobalScene.get_param("level")}) diff --git a/scripts/levels.gd b/scripts/levels.gd index b94bf3c..ecea53c 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -8,23 +8,27 @@ var levels = [ var level: ComparisonSort func _ready(): - # Dynamically load level data + """Dynamically load level data.""" for level in levels: var button = Button.new() - button.text = level.TITLE + button.text = level.NAME button.align = Button.ALIGN_LEFT button.connect("focus_entered", self, "_on_Button_focus_changed") - button.connect("pressed", self, "_on_Button_pressed", [level.TITLE]) + button.connect("pressed", self, "_on_Button_pressed", [level.NAME]) $LevelsBorder/Levels.add_child(button) - if scene.get_param("level") == level: + # Autofocus last played level + if GlobalScene.get_param("level") == level: button.grab_focus() - if scene.get_param("level") == null: + # If no last played level, autofocus first + if GlobalScene.get_param("level") == null: $LevelsBorder/Levels.get_child(0).grab_focus() func _on_Button_focus_changed(): - level = get_level(get_focus_owner().text).new(ArrayModel.new(10)) + """Initialize the preview section.""" + level = get_level(get_focus_owner().text).new(ArrayModel.new()) level.active = false - $Preview/InfoBorder/Info/Description.text = level.ABOUT.replace("\n", " ") + $Preview/InfoBorder/Info/About.text = _cleanup(level.ABOUT) + $Preview/InfoBorder/Info/Controls.text = _cleanup(level.CONTROLS) # Start over when simulation is finished level.connect("done", self, "_on_Button_focus_changed") # Replace old display with new @@ -32,13 +36,16 @@ func _on_Button_focus_changed(): child.queue_free() $Preview/Display.add_child(ArrayView.new(level)) -func _on_Button_pressed(title): - scene.change_scene("res://scenes/play.tscn", {"level": get_level(title)}) +func _on_Button_pressed(name): + GlobalScene.change_scene("res://scenes/play.tscn", {"level": get_level(name)}) -func get_level(title): +func get_level(name): for level in levels: - if level.TITLE == title: + if level.NAME == name: return level func _on_Timer_timeout(): level.next() + +func _cleanup(string): + return string.strip_edges().replace("\n", " ") diff --git a/scripts/menu.gd b/scripts/menu.gd index fa303ec..250c3a6 100644 --- a/scripts/menu.gd +++ b/scripts/menu.gd @@ -1,17 +1,18 @@ extends VBoxContainer -var level = BogoSort.new(ArrayModel.new(10)) +var level = BogoSort.new(ArrayModel.new()) func _ready(): $Buttons/Start.grab_focus() - $Display.add_child(ArrayView.new(level)) randomize() + level.active = false + $Display.add_child(ArrayView.new(level)) func _on_Start_pressed(): - scene.change_scene("res://scenes/levels.tscn") + GlobalScene.change_scene("res://scenes/levels.tscn") func _on_Credits_pressed(): - scene.change_scene("res://scenes/credits.tscn") + GlobalScene.change_scene("res://scenes/credits.tscn") func _on_Timer_timeout(): level.next() diff --git a/scripts/play.gd b/scripts/play.gd index 9c85fc2..72d2090 100644 --- a/scripts/play.gd +++ b/scripts/play.gd @@ -3,7 +3,7 @@ extends VBoxContainer var start_time = -1 func _ready(): - $HUDBorder/HUD/Level.text = scene.get_param("level").TITLE + $HUDBorder/HUD/Level.text = GlobalScene.get_param("level").NAME func _process(delta): if start_time >= 0: @@ -11,10 +11,8 @@ func _process(delta): func _on_Timer_timeout(): start_time = OS.get_ticks_msec() - # Delete ready text - $DisplayBorder/Label.queue_free() - # Load level - var level = scene.get_param("level").new(ArrayModel.new(10)) + $DisplayBorder/Label.queue_free() # Delete ready text + var level = GlobalScene.get_param("level").new(ArrayModel.new()) level.connect("done", self, "_on_Level_done") $DisplayBorder.add_child(ArrayView.new(level)) @@ -22,5 +20,5 @@ func get_score(): return stepify((OS.get_ticks_msec() - start_time) / 1000.0, 0.001) func _on_Level_done(): - scene.change_scene("res://scenes/end.tscn", - {"level": scene.get_param("level"), "score": get_score()}) + GlobalScene.change_scene("res://scenes/end.tscn", + {"level": GlobalScene.get_param("level"), "score": get_score()}) diff --git a/scripts/scene.gd b/scripts/scene.gd index 8b07d60..0379e12 100644 --- a/scripts/scene.gd +++ b/scripts/scene.gd @@ -1,3 +1,7 @@ +""" +Global helper class for passing parameters between changing scenes. +""" + extends Node var _params = null diff --git a/scripts/theme.gd b/scripts/theme.gd new file mode 100644 index 0000000..436a110 --- /dev/null +++ b/scripts/theme.gd @@ -0,0 +1,9 @@ +""" +Global constants relating to the GUI. +""" + +extends Node + +const GREEN = Color(0.2, 1, 0.2) +const ORANGE = Color(1, 0.69, 0) +const RED = Color(1, 0, 0) diff --git a/views/array_view.gd b/views/array_view.gd index e1def82..97640a5 100644 --- a/views/array_view.gd +++ b/views/array_view.gd @@ -1,16 +1,18 @@ -extends HBoxContainer -class_name ArrayView +""" +Visualization of an array as rectangles of varying heights. +""" -const GREEN = Color(0.2, 1, 0.2) -const ORANGE = Color(1, 0.69, 0) +class_name ArrayView +extends HBoxContainer var level: ComparisonSort var rects = [] func _init(level): + """Add colored rectangles.""" level.connect("mistake", self, "_on_Level_mistake") - add_child(level) self.level = level + add_child(level) # NOTE: This is necessary for it to read input for i in range(level.array.size): var rect = ColorRect.new() rect.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -19,11 +21,12 @@ func _init(level): add_child(rect) func _process(delta): + """Update heights of rectangles based on array values.""" for i in range(level.array.size): - rects[i].rect_scale.y = -1 # Override parent Control scale - rects[i].color = ORANGE if level.emphasized(i) else GREEN - var frac = float(level.array.get(i)) / level.array.size - rects[i].rect_size.y = rect_size.y * frac + rects[i].rect_scale.y = -1 # XXX: Override parent Control scale + rects[i].color = GlobalTheme.ORANGE if level.emphasized(i) else GlobalTheme.GREEN + rects[i].rect_size.y = rect_size.y * level.array.get(i) / level.array.size func _on_Level_mistake(): + """Flash the border red on mistakes.""" get_parent().flash() From 5cb9c76e62f8db6273026a1a1dfe875f056a90ca Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Thu, 18 Jun 2020 11:42:26 -0500 Subject: [PATCH 05/13] refactor: move effect logic into one function This makes it much easier to support multiple effects. --- levels/bogo_sort.gd | 4 ++-- levels/bubble_sort.gd | 8 +++++--- levels/comparison_sort.gd | 26 ++++++++++++++++++-------- levels/insertion_sort.gd | 14 ++++++++------ levels/selection_sort.gd | 10 ++++++---- project.godot | 16 ++++++++++++++++ scripts/levels.gd | 10 ++++++++-- scripts/theme.gd | 1 + views/array_view.gd | 2 +- 9 files changed, 65 insertions(+), 26 deletions(-) diff --git a/levels/bogo_sort.gd b/levels/bogo_sort.gd index 7efeb3b..901aba6 100644 --- a/levels/bogo_sort.gd +++ b/levels/bogo_sort.gd @@ -20,5 +20,5 @@ func next(): if array.is_sorted(): emit_signal("done") -func emphasized(i): - return false +func get_effect(i): + return EFFECTS.NONE diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index c74246b..f188807 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -10,7 +10,7 @@ finished. Though simple to understand, bubble sort is hopelessly inefficient on all but the smallest of arrays. """ const CONTROLS = """ -If the two highlighted elements are out of order, hit LEFT ARROW to SWAP +If the two highlighted elements are out of order, hit LEFT ARROW to swap them. Otherwise, hit RIGHT ARROW to continue. """ @@ -37,5 +37,7 @@ func next(): _index = 0 _swapped = false -func emphasized(i): - return i == _index or i == _index + 1 +func get_effect(i): + if i == _index or i == _index + 1: + return EFFECTS.HIGHLIGHTED + return EFFECTS.NONE diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd index cb43a70..421d23f 100644 --- a/levels/comparison_sort.gd +++ b/levels/comparison_sort.gd @@ -4,7 +4,20 @@ extends Node signal done signal mistake -enum ACTIONS {SWAP, NO_SWAP} +enum ACTIONS { + SWAP, + NO_SWAP, + + LEFT, + RIGHT, +} + +const EFFECTS = { + "NONE": GlobalTheme.GREEN, + "HIGHLIGHTED": GlobalTheme.ORANGE, + "DIMMED": GlobalTheme.DARK_GREEN, + "WUT": 0, +} const DISABLE_TIME = 1.0 @@ -25,12 +38,13 @@ func _input(event): """Pass input events for checking and take appropriate action.""" if not active: return + for action in ACTIONS: if event.is_action_pressed(action): if check(ACTIONS[action]): - next() - else: - emit_signal("mistake") + return next() + if event.is_pressed(): + emit_signal("mistake") func check(action): """Determine if the given action enum value is correct.""" @@ -40,10 +54,6 @@ func next(): """Advance the state by one step and signal done if completed.""" push_error("NotImplementedError") -func emphasized(i): - """Return whether the given index should be highlighted.""" - push_error("NotImplementedError") - func _on_ComparisonSort_mistake(): """Disable the controls for one second.""" active = false diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd index afc8efc..8a0d816 100644 --- a/levels/insertion_sort.gd +++ b/levels/insertion_sort.gd @@ -10,13 +10,13 @@ one of the faster quadratic algorithms. It is often used to sort smaller subarrays in hybrid sorting algorithms. """ const CONTROLS = """ -Hit LEFT ARROW to SWAP the two highlighted elements as long as they are +Hit LEFT ARROW to swap the two highlighted elements as long as they are out of order. When this is no longer the case, hit RIGHT ARROW to advance. """ -var _end = 1 -var _index = _end +var _end = 1 # Size of the sorted subarray +var _index = 1 # Position of element currently being inserted func _init(array).(array): pass @@ -36,11 +36,13 @@ func next(): else: _grow() +func get_effect(i): + if i == _index or i == _index - 1: + return EFFECTS.HIGHLIGHTED + return EFFECTS.NONE + func _grow(): _end += 1 if _end == array.size: emit_signal("done") _index = _end - -func emphasized(i): - return i == _index or i == _index - 1 diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd index 4809f2a..be01de3 100644 --- a/levels/selection_sort.gd +++ b/levels/selection_sort.gd @@ -15,8 +15,8 @@ smaller than the left highlighted element, then hit LEFT ARROW to swap the new smallest into place and keep going. """ -var _base = 0 -var _index = _base + 1 +var _base = 0 # Size of sorted subarray +var _index = 1 # Index of tentative new smallest func _init(array).(array): pass @@ -37,5 +37,7 @@ func next(): if _base == array.size - 1: emit_signal("done") -func emphasized(i): - return i == _index or i == _base +func get_effect(i): + if i == _index or i == _base: + return EFFECTS.HIGHLIGHTED + return EFFECTS.NONE diff --git a/project.godot b/project.godot index b9085b7..3dd21f9 100644 --- a/project.godot +++ b/project.godot @@ -40,6 +40,11 @@ _global_script_classes=[ { "path": "res://levels/insertion_sort.gd" }, { "base": "ComparisonSort", +"class": "MergeSort", +"language": "GDScript", +"path": "res://levels/merge_sort.gd" +}, { +"base": "ComparisonSort", "class": "SelectionSort", "language": "GDScript", "path": "res://levels/selection_sort.gd" @@ -51,6 +56,7 @@ _global_script_class_icons={ "BubbleSort": "", "ComparisonSort": "", "InsertionSort": "", +"MergeSort": "", "SelectionSort": "" } @@ -127,6 +133,16 @@ NO_SWAP={ "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null) ] } +LEFT={ +"deadzone": 0.5, +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777231,"unicode":0,"echo":false,"script":null) + ] +} +RIGHT={ +"deadzone": 0.5, +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777233,"unicode":0,"echo":false,"script":null) + ] +} [rendering] diff --git a/scripts/levels.gd b/scripts/levels.gd index ecea53c..edf8be1 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -4,6 +4,7 @@ var levels = [ BubbleSort, InsertionSort, SelectionSort, + MergeSort, ] var level: ComparisonSort @@ -19,9 +20,14 @@ func _ready(): # Autofocus last played level if GlobalScene.get_param("level") == level: button.grab_focus() - # If no last played level, autofocus first + var top_button = $LevelsBorder/Levels.get_children()[0] + var bottom_button = $LevelsBorder/Levels.get_children()[-1] + # Allow looping from ends of list + top_button.focus_neighbour_top = bottom_button.get_path() + bottom_button.focus_neighbour_bottom = top_button.get_path() + # If no last played level, autofocus first level if GlobalScene.get_param("level") == null: - $LevelsBorder/Levels.get_child(0).grab_focus() + top_button.grab_focus() func _on_Button_focus_changed(): """Initialize the preview section.""" diff --git a/scripts/theme.gd b/scripts/theme.gd index 436a110..fce2fc4 100644 --- a/scripts/theme.gd +++ b/scripts/theme.gd @@ -5,5 +5,6 @@ Global constants relating to the GUI. extends Node const GREEN = Color(0.2, 1, 0.2) +const DARK_GREEN = Color(0.2, 1, 0.2, 0.5) const ORANGE = Color(1, 0.69, 0) const RED = Color(1, 0, 0) diff --git a/views/array_view.gd b/views/array_view.gd index 97640a5..253f059 100644 --- a/views/array_view.gd +++ b/views/array_view.gd @@ -24,7 +24,7 @@ func _process(delta): """Update heights of rectangles based on array values.""" for i in range(level.array.size): rects[i].rect_scale.y = -1 # XXX: Override parent Control scale - rects[i].color = GlobalTheme.ORANGE if level.emphasized(i) else GlobalTheme.GREEN + rects[i].color = level.get_effect(i) rects[i].rect_size.y = rect_size.y * level.array.get(i) / level.array.size func _on_Level_mistake(): From 84d78ef3fc6a2e2035dcbad07b1a1f00029eaf78 Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Thu, 18 Jun 2020 12:01:55 -0500 Subject: [PATCH 06/13] feat: nerf bubble sort and add dimming effects The player no longer has to go through the sorted end in bubble sort, which is now dimmed. The sorted subarrays of insertion sort and selection sort are dimmed too. --- levels/bubble_sort.gd | 6 +++++- levels/insertion_sort.gd | 2 ++ levels/selection_sort.gd | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index f188807..fe0bb87 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -15,6 +15,7 @@ them. Otherwise, hit RIGHT ARROW to continue. """ var _index = 0 +var _end = array.size var _swapped = false func _init(array).(array): @@ -31,13 +32,16 @@ func next(): array.swap(_index, _index + 1) _swapped = true _index += 1 - if _index == array.size - 1: + if _index + 1 == _end: if not _swapped: emit_signal("done") _index = 0 + _end -= 1 _swapped = false func get_effect(i): + if i >= _end: + return EFFECTS.DIMMED if i == _index or i == _index + 1: return EFFECTS.HIGHLIGHTED return EFFECTS.NONE diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd index 8a0d816..c69c4cd 100644 --- a/levels/insertion_sort.gd +++ b/levels/insertion_sort.gd @@ -39,6 +39,8 @@ func next(): func get_effect(i): if i == _index or i == _index - 1: return EFFECTS.HIGHLIGHTED + if i < _end: + return EFFECTS.DIMMED return EFFECTS.NONE func _grow(): diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd index be01de3..9872fbf 100644 --- a/levels/selection_sort.gd +++ b/levels/selection_sort.gd @@ -40,4 +40,6 @@ func next(): func get_effect(i): if i == _index or i == _base: return EFFECTS.HIGHLIGHTED + if i <= _base: + return EFFECTS.DIMMED return EFFECTS.NONE From f65e1211a301c451bc5e1bcaaee7d5cfabd29244 Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Thu, 18 Jun 2020 13:06:38 -0500 Subject: [PATCH 07/13] refactor: combine check and advance logic The check(action) function in ComparisonSort has been deleted and all checking logic happens in next(action) to reduce repeated code. --- levels/bogo_sort.gd | 5 +---- levels/bubble_sort.gd | 12 +++++------- levels/comparison_sort.gd | 27 +++++++++------------------ levels/insertion_sort.gd | 12 +++++------- levels/selection_sort.gd | 12 +++++------- scenes/levels.tscn | 13 ++----------- scripts/levels.gd | 2 +- scripts/menu.gd | 2 +- 8 files changed, 29 insertions(+), 56 deletions(-) diff --git a/levels/bogo_sort.gd b/levels/bogo_sort.gd index 901aba6..9655e91 100644 --- a/levels/bogo_sort.gd +++ b/levels/bogo_sort.gd @@ -12,10 +12,7 @@ Keep on hitting RIGHT ARROW to CONTINUE and hope for the best! func _init(array).(array): pass -func check(action): - return true - -func next(): +func next(action): array = ArrayModel.new(array.size) if array.is_sorted(): emit_signal("done") diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index fe0bb87..5db4588 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -21,16 +21,14 @@ var _swapped = false func _init(array).(array): pass -func check(action): - if array.get(_index) > array.get(_index + 1): - return action == ACTIONS.SWAP - else: - return action == ACTIONS.NO_SWAP - -func next(): +func next(action): if array.get(_index) > array.get(_index + 1): + if action != null and action != ACTIONS.SWAP: + return emit_signal("mistake") array.swap(_index, _index + 1) _swapped = true + elif action != null and action != ACTIONS.NO_SWAP: + return emit_signal("mistake") _index += 1 if _index + 1 == _end: if not _swapped: diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd index 421d23f..db4cbf4 100644 --- a/levels/comparison_sort.gd +++ b/levels/comparison_sort.gd @@ -4,19 +4,18 @@ extends Node signal done signal mistake -enum ACTIONS { - SWAP, - NO_SWAP, +const ACTIONS = { + "SWAP": "ui_left", + "NO_SWAP": "ui_right", - LEFT, - RIGHT, + "LEFT": "ui_left", + "RIGHT": "ui_right", } const EFFECTS = { "NONE": GlobalTheme.GREEN, "HIGHLIGHTED": GlobalTheme.ORANGE, "DIMMED": GlobalTheme.DARK_GREEN, - "WUT": 0, } const DISABLE_TIME = 1.0 @@ -38,20 +37,12 @@ func _input(event): """Pass input events for checking and take appropriate action.""" if not active: return - - for action in ACTIONS: + for action in ACTIONS.values(): if event.is_action_pressed(action): - if check(ACTIONS[action]): - return next() - if event.is_pressed(): - emit_signal("mistake") - -func check(action): - """Determine if the given action enum value is correct.""" - push_error("NotImplementedError") + return next(action) -func next(): - """Advance the state by one step and signal done if completed.""" +func next(action): + """Check the action and advance state or emit signal as needed.""" push_error("NotImplementedError") func _on_ComparisonSort_mistake(): diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd index c69c4cd..27ec568 100644 --- a/levels/insertion_sort.gd +++ b/levels/insertion_sort.gd @@ -21,19 +21,17 @@ var _index = 1 # Position of element currently being inserted func _init(array).(array): pass -func check(action): - if array.get(_index - 1) > array.get(_index): - return action == ACTIONS.SWAP - else: - return action == ACTIONS.NO_SWAP - -func next(): +func next(action): if array.get(_index - 1) > array.get(_index): + if action != null and action != ACTIONS.SWAP: + return emit_signal("mistake") array.swap(_index - 1, _index) _index -= 1 if _index == 0: _grow() else: + if action != null and action != ACTIONS.NO_SWAP: + return emit_signal("mistake") _grow() func get_effect(i): diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd index 9872fbf..34b3f73 100644 --- a/levels/selection_sort.gd +++ b/levels/selection_sort.gd @@ -21,15 +21,13 @@ var _index = 1 # Index of tentative new smallest func _init(array).(array): pass -func check(action): - if array.get(_base) > array.get(_index): - return action == ACTIONS.SWAP - else: - return action == ACTIONS.NO_SWAP - -func next(): +func next(action): if array.get(_base) > array.get(_index): + if action != null and action != ACTIONS.SWAP: + return emit_signal("mistake") array.swap(_base, _index) + elif action != null and action != ACTIONS.NO_SWAP: + return emit_signal("mistake") _index += 1 if _index == array.size: _base += 1 diff --git a/scenes/levels.tscn b/scenes/levels.tscn index ca08733..f36be90 100644 --- a/scenes/levels.tscn +++ b/scenes/levels.tscn @@ -48,12 +48,6 @@ margin_bottom = 640.0 rect_min_size = Vector2( 0, 640 ) script = ExtResource( 3 ) -[node name="Placeholder" type="Control" parent="LevelSelect/Preview/Display"] -margin_left = 20.0 -margin_top = 20.0 -margin_right = 1352.0 -margin_bottom = 620.0 - [node name="InfoBorder" type="MarginContainer" parent="LevelSelect/Preview"] margin_top = 648.0 margin_right = 1372.0 @@ -73,7 +67,7 @@ margin_right = 810.0 margin_bottom = 332.0 rect_min_size = Vector2( 810, 0 ) size_flags_vertical = 3 -text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse luctus lorem felis, in imperdiet mauris faucibus in. Vestibulum interdum mi at arcu congue congue. Cras sodales mauris odio, eget iaculis dolor tempor quis. Suspendisse nec iaculis sapien, eu sollicitudin orci. Nulla volutpat pellentesque ex nec cursus." +text = "This is a short description of the algorithm. It should tell how it works in a simple yet complete way and explain its relevance in computer science. It should be accessible to the layman while not being oversimplifying." autowrap = true [node name="Controls" type="Label" parent="LevelSelect/Preview/InfoBorder/Info"] @@ -83,10 +77,7 @@ margin_bottom = 332.0 rect_min_size = Vector2( 450, 0 ) size_flags_horizontal = 3 size_flags_vertical = 3 -text = "INSTRUCTIONS - -LEFT ARROW: SWAP -RIGHT ARROW: CONTINUE" +text = "These are the controls for the level. They should be tailored to each level for maximum efficiency and simplicity." autowrap = true [node name="Timer" type="Timer" parent="."] diff --git a/scripts/levels.gd b/scripts/levels.gd index edf8be1..191573c 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -51,7 +51,7 @@ func get_level(name): return level func _on_Timer_timeout(): - level.next() + level.next(null) func _cleanup(string): return string.strip_edges().replace("\n", " ") diff --git a/scripts/menu.gd b/scripts/menu.gd index 250c3a6..a43447a 100644 --- a/scripts/menu.gd +++ b/scripts/menu.gd @@ -15,4 +15,4 @@ func _on_Credits_pressed(): GlobalScene.change_scene("res://scenes/credits.tscn") func _on_Timer_timeout(): - level.next() + level.next(null) From 377417cc734e0e174366e355510abb0eb0aabb69 Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Fri, 19 Jun 2020 14:15:39 -0500 Subject: [PATCH 08/13] feat: add merge sort Increased array size from 10 to 16 so divide and conquer algorithms split perfectly. However, this makes heights hard to distinguish during merge sort's endgame. --- levels/bubble_sort.gd | 4 +-- levels/merge_sort.gd | 80 +++++++++++++++++++++++++++++++++++++++++++ models/array_model.gd | 9 +++++ views/array_view.gd | 2 +- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 levels/merge_sort.gd diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index 5db4588..895b418 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -38,8 +38,8 @@ func next(action): _swapped = false func get_effect(i): - if i >= _end: - return EFFECTS.DIMMED if i == _index or i == _index + 1: return EFFECTS.HIGHLIGHTED + if i >= _end: + return EFFECTS.DIMMED return EFFECTS.NONE diff --git a/levels/merge_sort.gd b/levels/merge_sort.gd new file mode 100644 index 0000000..00363d9 --- /dev/null +++ b/levels/merge_sort.gd @@ -0,0 +1,80 @@ +class_name MergeSort +extends ComparisonSort + +const NAME = "MERGE SORT" +const ABOUT = """ +Merge sort is an efficient sorting algorithm that splits the array into +single-element chunks. Then it merges each pair of chunks until only one +sorted chunk is left by repeatedly choosing the smaller element at the +head of each chunk and moving the head back. However, it needs an entire +array's worth of auxiliary memory. +""" +const CONTROLS = """ +Press the ARROW KEY corresponding to the side that the smaller +highlighted element is on. If you've reached the end of one side, press +the other side's ARROW KEY. +""" + +var _left = 0 # Index of left subarray pointer +var _right = 1 # Index of right subarray pointer +var _sub_size = 2 # Combined size of left and right subarrays +var _sub_no = 0 # Currently being merged left-right pair number + +func _init(array).(array): + pass + +func next(action): + if _left == -1: + if action != null and action != ACTIONS.RIGHT: + return emit_signal("mistake") + _right += 1 + elif _right == -1: + if action != null and action != ACTIONS.LEFT: + return emit_signal("mistake") + _left += 1 + elif array.get(_left) <= array.get(_right): + if action != null and action != ACTIONS.LEFT: + return emit_signal("mistake") + _left += 1 + else: + if action != null and action != ACTIONS.RIGHT: + return emit_signal("mistake") + _right += 1 + # Test if end of subarrays have been reached + if _left == _get_middle(): + _left = -1 + if _right == _get_end(): + _right = -1 + # If both ends have been reached, merge and advance to next block + if _left == -1 and _right == -1: + array.sort(_get_begin(), _get_end()) + _sub_no += 1 + _left = _get_begin() + _right = _get_middle() + # If last block has been completed, go up a level + if _sub_no == array.size / (_sub_size): + _sub_size *= 2 + _sub_no = 0 + _left = _get_begin() + _right = _get_middle() + if _sub_size == array.size * 2: + emit_signal("done") + +func get_effect(i): + if i == _left or i == _right: + return EFFECTS.HIGHLIGHTED + if i < _sub_no * _sub_size or i >= _sub_no * _sub_size + _sub_size: + return EFFECTS.DIMMED + return EFFECTS.NONE + +func _get_begin(): + """Get the index of the left subarray's head.""" + return _sub_no * _sub_size + +func _get_middle(): + """Get the index of the right subarray's head.""" + return _sub_no * _sub_size + _sub_size / 2 + +func _get_end(): + """Get the index of one past the right subarray's tail.""" + return _sub_no * _sub_size + _sub_size diff --git a/models/array_model.gd b/models/array_model.gd index e3f7289..da0ca46 100644 --- a/models/array_model.gd +++ b/models/array_model.gd @@ -32,3 +32,12 @@ func swap(i, j): var temp = array[i] array[i] = array[j] array[j] = temp + +func sort(i, j): + """Sort the subarray starting at i and up to but not including j.""" + # Grr to the developer who made the upper bound inclusive + var front = array.slice(0, i - 1) if i != 0 else [] + var sorted = array.slice(i, j - 1) + sorted.sort() + var back = array.slice(j, size - 1) if j != size else [] + array = front + sorted + back diff --git a/views/array_view.gd b/views/array_view.gd index 253f059..23bf23b 100644 --- a/views/array_view.gd +++ b/views/array_view.gd @@ -23,7 +23,7 @@ func _init(level): func _process(delta): """Update heights of rectangles based on array values.""" for i in range(level.array.size): - rects[i].rect_scale.y = -1 # XXX: Override parent Control scale + rects[i].rect_scale.y = -1 # HACK: Override scale to bypass weird behavior rects[i].color = level.get_effect(i) rects[i].rect_size.y = rect_size.y * level.array.get(i) / level.array.size From b58fe2d7acb34c787dae17496082a4b14393f13a Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Thu, 25 Jun 2020 18:11:34 -0500 Subject: [PATCH 09/13] refactor: rename ArrayModel::get() to at() This avoids conflicts with the get() method in Object and better self-documents its purpose. --- levels/bubble_sort.gd | 2 +- levels/insertion_sort.gd | 2 +- levels/merge_sort.gd | 2 +- levels/selection_sort.gd | 2 +- models/array_model.gd | 7 +------ project.godot | 6 ++++++ scripts/levels.gd | 3 ++- views/array_view.gd | 2 +- 8 files changed, 14 insertions(+), 12 deletions(-) diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index 895b418..c9f9e76 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -22,7 +22,7 @@ func _init(array).(array): pass func next(action): - if array.get(_index) > array.get(_index + 1): + if array.at(_index) > array.at(_index + 1): if action != null and action != ACTIONS.SWAP: return emit_signal("mistake") array.swap(_index, _index + 1) diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd index 27ec568..c217d32 100644 --- a/levels/insertion_sort.gd +++ b/levels/insertion_sort.gd @@ -22,7 +22,7 @@ func _init(array).(array): pass func next(action): - if array.get(_index - 1) > array.get(_index): + if array.at(_index - 1) > array.at(_index): if action != null and action != ACTIONS.SWAP: return emit_signal("mistake") array.swap(_index - 1, _index) diff --git a/levels/merge_sort.gd b/levels/merge_sort.gd index 00363d9..b09846f 100644 --- a/levels/merge_sort.gd +++ b/levels/merge_sort.gd @@ -32,7 +32,7 @@ func next(action): if action != null and action != ACTIONS.LEFT: return emit_signal("mistake") _left += 1 - elif array.get(_left) <= array.get(_right): + elif array.at(_left) <= array.at(_right): if action != null and action != ACTIONS.LEFT: return emit_signal("mistake") _left += 1 diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd index 34b3f73..6a90eca 100644 --- a/levels/selection_sort.gd +++ b/levels/selection_sort.gd @@ -22,7 +22,7 @@ func _init(array).(array): pass func next(action): - if array.get(_base) > array.get(_index): + if array.at(_base) > array.at(_index): if action != null and action != ACTIONS.SWAP: return emit_signal("mistake") array.swap(_base, _index) diff --git a/models/array_model.gd b/models/array_model.gd index da0ca46..39585bc 100644 --- a/models/array_model.gd +++ b/models/array_model.gd @@ -1,8 +1,3 @@ -""" -A plain old one-dimensional random access array wrapper around the -built-in class. -""" - class_name ArrayModel extends Reference @@ -16,7 +11,7 @@ func _init(size=16): array.shuffle() self.size = size -func get(i): +func at(i): """Retrieve the value of the element at index i.""" return array[i] diff --git a/project.godot b/project.godot index 3dd21f9..496c291 100644 --- a/project.godot +++ b/project.godot @@ -45,6 +45,11 @@ _global_script_classes=[ { "path": "res://levels/merge_sort.gd" }, { "base": "ComparisonSort", +"class": "QuickSort", +"language": "GDScript", +"path": "res://levels/quick_sort.gd" +}, { +"base": "ComparisonSort", "class": "SelectionSort", "language": "GDScript", "path": "res://levels/selection_sort.gd" @@ -57,6 +62,7 @@ _global_script_class_icons={ "ComparisonSort": "", "InsertionSort": "", "MergeSort": "", +"QuickSort": "", "SelectionSort": "" } diff --git a/scripts/levels.gd b/scripts/levels.gd index 191573c..11bf322 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -5,6 +5,7 @@ var levels = [ InsertionSort, SelectionSort, MergeSort, + QuickSort, ] var level: ComparisonSort @@ -27,7 +28,7 @@ func _ready(): bottom_button.focus_neighbour_bottom = top_button.get_path() # If no last played level, autofocus first level if GlobalScene.get_param("level") == null: - top_button.grab_focus() + bottom_button.grab_focus() func _on_Button_focus_changed(): """Initialize the preview section.""" diff --git a/views/array_view.gd b/views/array_view.gd index 23bf23b..7097998 100644 --- a/views/array_view.gd +++ b/views/array_view.gd @@ -25,7 +25,7 @@ func _process(delta): for i in range(level.array.size): rects[i].rect_scale.y = -1 # HACK: Override scale to bypass weird behavior rects[i].color = level.get_effect(i) - rects[i].rect_size.y = rect_size.y * level.array.get(i) / level.array.size + rects[i].rect_size.y = rect_size.y * level.array.at(i) / level.array.size func _on_Level_mistake(): """Flash the border red on mistakes.""" From ff6a4d72c871833b03531e6a3900c3348534c9fc Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Mon, 29 Jun 2020 17:35:12 -0500 Subject: [PATCH 10/13] style: add underscore to private variables Also locked aspect ratio to 16:9 for a consistent experience across screens without unfair advantages/disadvantages. --- levels/bubble_sort.gd | 5 +++-- levels/selection_sort.gd | 4 ++-- project.godot | 2 ++ scripts/levels.gd | 24 ++++++++++++------------ scripts/menu.gd | 8 ++++---- scripts/play.gd | 8 ++++---- views/array_view.gd | 20 ++++++++++---------- 7 files changed, 37 insertions(+), 34 deletions(-) diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index c9f9e76..cbaa3de 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -14,8 +14,8 @@ If the two highlighted elements are out of order, hit LEFT ARROW to swap them. Otherwise, hit RIGHT ARROW to continue. """ -var _index = 0 -var _end = array.size +var _index = 0 # First of two elements being compared +var _end = array.size # Beginning of sorted subarray var _swapped = false func _init(array).(array): @@ -30,6 +30,7 @@ func next(action): elif action != null and action != ACTIONS.NO_SWAP: return emit_signal("mistake") _index += 1 + # Prevent player from having to spam tap through the end if _index + 1 == _end: if not _swapped: emit_signal("done") diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd index 6a90eca..cdf8045 100644 --- a/levels/selection_sort.gd +++ b/levels/selection_sort.gd @@ -11,8 +11,8 @@ slower than insertion sort. """ const CONTROLS = """ Keep on hitting RIGHT ARROW until you encounter an element that is -smaller than the left highlighted element, then hit LEFT ARROW to swap -the new smallest into place and keep going. +smaller than the left highlighted element, then hit LEFT ARROW and +repeat. """ var _base = 0 # Size of sorted subarray diff --git a/project.godot b/project.godot index 496c291..bdc995c 100644 --- a/project.godot +++ b/project.godot @@ -84,8 +84,10 @@ GlobalTheme="*res://scripts/theme.gd" window/size/width=1920 window/size/height=1080 window/size/fullscreen=true +window/size/always_on_top=true window/dpi/allow_hidpi=true window/stretch/mode="2d" +window/stretch/aspect="keep" [editor_plugins] diff --git a/scripts/levels.gd b/scripts/levels.gd index 11bf322..44436f4 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -1,17 +1,17 @@ extends HBoxContainer -var levels = [ +const LEVELS = [ BubbleSort, InsertionSort, SelectionSort, MergeSort, QuickSort, ] -var level: ComparisonSort +var _level: ComparisonSort func _ready(): """Dynamically load level data.""" - for level in levels: + for level in LEVELS: var button = Button.new() button.text = level.NAME button.align = Button.ALIGN_LEFT @@ -28,31 +28,31 @@ func _ready(): bottom_button.focus_neighbour_bottom = top_button.get_path() # If no last played level, autofocus first level if GlobalScene.get_param("level") == null: - bottom_button.grab_focus() + top_button.grab_focus() func _on_Button_focus_changed(): """Initialize the preview section.""" - level = get_level(get_focus_owner().text).new(ArrayModel.new()) - level.active = false - $Preview/InfoBorder/Info/About.text = _cleanup(level.ABOUT) - $Preview/InfoBorder/Info/Controls.text = _cleanup(level.CONTROLS) + _level = get_level(get_focus_owner().text).new(ArrayModel.new()) + _level.active = false + $Preview/InfoBorder/Info/About.text = _cleanup(_level.ABOUT) + $Preview/InfoBorder/Info/Controls.text = _cleanup(_level.CONTROLS) # Start over when simulation is finished - level.connect("done", self, "_on_Button_focus_changed") + _level.connect("done", self, "_on_Button_focus_changed") # Replace old display with new for child in $Preview/Display.get_children(): child.queue_free() - $Preview/Display.add_child(ArrayView.new(level)) + $Preview/Display.add_child(ArrayView.new(_level)) func _on_Button_pressed(name): GlobalScene.change_scene("res://scenes/play.tscn", {"level": get_level(name)}) func get_level(name): - for level in levels: + for level in LEVELS: if level.NAME == name: return level func _on_Timer_timeout(): - level.next(null) + _level.next(null) func _cleanup(string): return string.strip_edges().replace("\n", " ") diff --git a/scripts/menu.gd b/scripts/menu.gd index a43447a..a2caada 100644 --- a/scripts/menu.gd +++ b/scripts/menu.gd @@ -1,12 +1,12 @@ extends VBoxContainer -var level = BogoSort.new(ArrayModel.new()) +var _level = BogoSort.new(ArrayModel.new()) func _ready(): $Buttons/Start.grab_focus() randomize() - level.active = false - $Display.add_child(ArrayView.new(level)) + _level.active = false + $Display.add_child(ArrayView.new(_level)) func _on_Start_pressed(): GlobalScene.change_scene("res://scenes/levels.tscn") @@ -15,4 +15,4 @@ func _on_Credits_pressed(): GlobalScene.change_scene("res://scenes/credits.tscn") func _on_Timer_timeout(): - level.next(null) + _level.next(null) diff --git a/scripts/play.gd b/scripts/play.gd index 72d2090..08699f5 100644 --- a/scripts/play.gd +++ b/scripts/play.gd @@ -1,23 +1,23 @@ extends VBoxContainer -var start_time = -1 +var _start_time = -1 func _ready(): $HUDBorder/HUD/Level.text = GlobalScene.get_param("level").NAME func _process(delta): - if start_time >= 0: + if _start_time >= 0: $HUDBorder/HUD/Score.text = "%.3f" % get_score() func _on_Timer_timeout(): - start_time = OS.get_ticks_msec() + _start_time = OS.get_ticks_msec() $DisplayBorder/Label.queue_free() # Delete ready text var level = GlobalScene.get_param("level").new(ArrayModel.new()) level.connect("done", self, "_on_Level_done") $DisplayBorder.add_child(ArrayView.new(level)) func get_score(): - return stepify((OS.get_ticks_msec() - start_time) / 1000.0, 0.001) + return stepify((OS.get_ticks_msec() - _start_time) / 1000.0, 0.001) func _on_Level_done(): GlobalScene.change_scene("res://scenes/end.tscn", diff --git a/views/array_view.gd b/views/array_view.gd index 7097998..70e9ac7 100644 --- a/views/array_view.gd +++ b/views/array_view.gd @@ -5,27 +5,27 @@ Visualization of an array as rectangles of varying heights. class_name ArrayView extends HBoxContainer -var level: ComparisonSort -var rects = [] +var _level: ComparisonSort +var _rects = [] func _init(level): """Add colored rectangles.""" - level.connect("mistake", self, "_on_Level_mistake") - self.level = level - add_child(level) # NOTE: This is necessary for it to read input + _level = level + _level.connect("mistake", self, "_on_Level_mistake") + add_child(_level) # NOTE: This is necessary for it to read input for i in range(level.array.size): var rect = ColorRect.new() rect.size_flags_horizontal = Control.SIZE_EXPAND_FILL rect.size_flags_vertical = Control.SIZE_SHRINK_END - rects.append(rect) + _rects.append(rect) add_child(rect) func _process(delta): """Update heights of rectangles based on array values.""" - for i in range(level.array.size): - rects[i].rect_scale.y = -1 # HACK: Override scale to bypass weird behavior - rects[i].color = level.get_effect(i) - rects[i].rect_size.y = rect_size.y * level.array.at(i) / level.array.size + for i in range(_level.array.size): + _rects[i].rect_scale.y = -1 # HACK: Override scale to bypass weird behavior + _rects[i].color = _level.get_effect(i) + _rects[i].rect_size.y = rect_size.y * _level.array.at(i) / _level.array.size func _on_Level_mistake(): """Flash the border red on mistakes.""" From f82f0038f42d8b1c3e23fa4280bfbeb5fdf4800b Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Mon, 29 Jun 2020 17:50:21 -0500 Subject: [PATCH 11/13] fix: bubble sort no longer fails on reversed array A corner case in the end condition of bubble sort meant that if the array ever got to a state where the unsorted subarray is size 2 and out of order, i.e. _end = 2 AND array[0] > array[1] (starting with a reversed array will lead to this condition), the next iteration will have _index equal _end, missing the end condition entirely and eventually causing a crash when the player goes outside the array bounds. --- levels/bubble_sort.gd | 2 +- models/array_model.gd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index cbaa3de..e092db1 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -32,7 +32,7 @@ func next(action): _index += 1 # Prevent player from having to spam tap through the end if _index + 1 == _end: - if not _swapped: + if not _swapped or _end == 2: # Stop if only one element left emit_signal("done") _index = 0 _end -= 1 diff --git a/models/array_model.gd b/models/array_model.gd index 39585bc..b237231 100644 --- a/models/array_model.gd +++ b/models/array_model.gd @@ -9,7 +9,7 @@ func _init(size=16): for i in range(1, size + 1): array.append(i) array.shuffle() - self.size = size + self.size = array.size() func at(i): """Retrieve the value of the element at index i.""" From a75accba846486533f486ab9bb025df9311c2acb Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Tue, 30 Jun 2020 13:40:17 -0500 Subject: [PATCH 12/13] feat: change selection sort to be more realistic Selection sort now reflects a real-world implementation, which is probably less confusing for those already familiar with it without being more difficult for newcomers. --- levels/selection_sort.gd | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd index cdf8045..075a53d 100644 --- a/levels/selection_sort.gd +++ b/levels/selection_sort.gd @@ -16,28 +16,31 @@ repeat. """ var _base = 0 # Size of sorted subarray -var _index = 1 # Index of tentative new smallest +var _min = 0 # Index of smallest known element +var _index = 1 # Element currently being compared func _init(array).(array): pass func next(action): - if array.at(_base) > array.at(_index): + if array.at(_index) < array.at(_min): if action != null and action != ACTIONS.SWAP: return emit_signal("mistake") - array.swap(_base, _index) + _min = _index elif action != null and action != ACTIONS.NO_SWAP: return emit_signal("mistake") _index += 1 if _index == array.size: + array.swap(_base, _min) _base += 1 + _min = _base _index = _base + 1 if _base == array.size - 1: emit_signal("done") func get_effect(i): - if i == _index or i == _base: + if i == _min or i == _index: return EFFECTS.HIGHLIGHTED - if i <= _base: + if i < _base: return EFFECTS.DIMMED return EFFECTS.NONE From 8ebc0f7d8c57fb8381137a20269b4e48ada14694 Mon Sep 17 00:00:00 2001 From: Daniel Ting Date: Tue, 30 Jun 2020 14:57:33 -0500 Subject: [PATCH 13/13] feat: dim already compared elements in merge sort Previously, all elements in the left and right subarrays would be highlighted. Now, those elements that have already been passed over will be dimmed. --- levels/merge_sort.gd | 24 ++++++++++-------------- project.godot | 6 ------ scripts/levels.gd | 1 - 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/levels/merge_sort.gd b/levels/merge_sort.gd index b09846f..62e6594 100644 --- a/levels/merge_sort.gd +++ b/levels/merge_sort.gd @@ -24,11 +24,11 @@ func _init(array).(array): pass func next(action): - if _left == -1: + if _left == _get_middle(): if action != null and action != ACTIONS.RIGHT: return emit_signal("mistake") _right += 1 - elif _right == -1: + elif _right == _get_end(): if action != null and action != ACTIONS.LEFT: return emit_signal("mistake") _left += 1 @@ -40,30 +40,26 @@ func next(action): if action != null and action != ACTIONS.RIGHT: return emit_signal("mistake") _right += 1 - # Test if end of subarrays have been reached - if _left == _get_middle(): - _left = -1 - if _right == _get_end(): - _right = -1 # If both ends have been reached, merge and advance to next block - if _left == -1 and _right == -1: + if _left == _get_middle() and _right == _get_end(): array.sort(_get_begin(), _get_end()) _sub_no += 1 - _left = _get_begin() - _right = _get_middle() # If last block has been completed, go up a level if _sub_no == array.size / (_sub_size): _sub_size *= 2 _sub_no = 0 - _left = _get_begin() - _right = _get_middle() if _sub_size == array.size * 2: emit_signal("done") + # Update pointers + _left = _get_begin() + _right = _get_middle() func get_effect(i): - if i == _left or i == _right: + var is_left = _left != _get_middle() and i == _left + var is_right = _right != _get_end() and i == _right + if is_left or is_right: return EFFECTS.HIGHLIGHTED - if i < _sub_no * _sub_size or i >= _sub_no * _sub_size + _sub_size: + if i < _left or i >= _get_middle() and i < _right or i >= _get_end(): return EFFECTS.DIMMED return EFFECTS.NONE diff --git a/project.godot b/project.godot index bdc995c..328acbe 100644 --- a/project.godot +++ b/project.godot @@ -45,11 +45,6 @@ _global_script_classes=[ { "path": "res://levels/merge_sort.gd" }, { "base": "ComparisonSort", -"class": "QuickSort", -"language": "GDScript", -"path": "res://levels/quick_sort.gd" -}, { -"base": "ComparisonSort", "class": "SelectionSort", "language": "GDScript", "path": "res://levels/selection_sort.gd" @@ -62,7 +57,6 @@ _global_script_class_icons={ "ComparisonSort": "", "InsertionSort": "", "MergeSort": "", -"QuickSort": "", "SelectionSort": "" } diff --git a/scripts/levels.gd b/scripts/levels.gd index 44436f4..1ce23d6 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -5,7 +5,6 @@ const LEVELS = [ InsertionSort, SelectionSort, MergeSort, - QuickSort, ] var _level: ComparisonSort