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..9655e91 100644 --- a/levels/bogo_sort.gd +++ b/levels/bogo_sort.gd @@ -1,14 +1,21 @@ -extends ComparisonSort class_name BogoSort +extends ComparisonSort -func _init(array).(array): - pass +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 check(action): - return true +func _init(array).(array): + pass -func next(): - array = ArrayModel.new(array.size) +func next(action): + array = ArrayModel.new(array.size) + 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 801cd82..e092db1 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -1,27 +1,46 @@ -extends ComparisonSort class_name BubbleSort +extends ComparisonSort -var swapped = false +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. +""" +const CONTROLS = """ +If the two highlighted elements are out of order, hit LEFT ARROW to swap +them. Otherwise, hit RIGHT ARROW to continue. +""" -func _init(array).(array): - pass +var _index = 0 # First of two elements being compared +var _end = array.size # Beginning of sorted subarray +var _swapped = false -func check(action): - if array.get(index) > array.get(index + 1): - return action == "swap" - else: - return action == "no_swap" +func _init(array).(array): + pass -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 +func next(action): + if array.at(_index) > array.at(_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 + # Prevent player from having to spam tap through the end + if _index + 1 == _end: + if not _swapped or _end == 2: # Stop if only one element left + emit_signal("done") + _index = 0 + _end -= 1 + _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 + if i >= _end: + return EFFECTS.DIMMED + return EFFECTS.NONE diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd index dbb0bf0..db4cbf4 100644 --- a/levels/comparison_sort.gd +++ b/levels/comparison_sort.gd @@ -1,43 +1,55 @@ -extends Node class_name ComparisonSort +extends Node signal done signal mistake -const ACTIONS = ["swap", "no_swap"] +const ACTIONS = { + "SWAP": "ui_left", + "NO_SWAP": "ui_right", + + "LEFT": "ui_left", + "RIGHT": "ui_right", +} + +const EFFECTS = { + "NONE": GlobalTheme.GREEN, + "HIGHLIGHTED": GlobalTheme.ORANGE, + "DIMMED": GlobalTheme.DARK_GREEN, +} + +const DISABLE_TIME = 1.0 var array: ArrayModel -var index = 0 -var timer = Timer.new() var active = true +var _timer = Timer.new() + 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") + """Initialize array and timer.""" + 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 +func _input(event): + """Pass input events for checking and take appropriate action.""" + if not active: + return + for action in ACTIONS.values(): + if event.is_action_pressed(action): + return next(action) -func next(): - pass +func next(action): + """Check the action and advance state or emit signal as needed.""" + push_error("NotImplementedError") func _on_ComparisonSort_mistake(): - active = false - timer.start(1) + """Disable the controls for one second.""" + active = false + _timer.start(DISABLE_TIME) func _on_Timer_timeout(): - 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") + """Reenable the controls.""" + active = true diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd new file mode 100644 index 0000000..c217d32 --- /dev/null +++ b/levels/insertion_sort.gd @@ -0,0 +1,48 @@ +class_name InsertionSort +extends ComparisonSort + +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. +""" +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 # Size of the sorted subarray +var _index = 1 # Position of element currently being inserted + +func _init(array).(array): + pass + +func next(action): + if array.at(_index - 1) > array.at(_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): + if i == _index or i == _index - 1: + return EFFECTS.HIGHLIGHTED + if i < _end: + return EFFECTS.DIMMED + return EFFECTS.NONE + +func _grow(): + _end += 1 + if _end == array.size: + emit_signal("done") + _index = _end diff --git a/levels/merge_sort.gd b/levels/merge_sort.gd new file mode 100644 index 0000000..62e6594 --- /dev/null +++ b/levels/merge_sort.gd @@ -0,0 +1,76 @@ +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 == _get_middle(): + if action != null and action != ACTIONS.RIGHT: + return emit_signal("mistake") + _right += 1 + elif _right == _get_end(): + if action != null and action != ACTIONS.LEFT: + return emit_signal("mistake") + _left += 1 + elif array.at(_left) <= array.at(_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 + # If both ends have been reached, merge and advance to next block + if _left == _get_middle() and _right == _get_end(): + array.sort(_get_begin(), _get_end()) + _sub_no += 1 + # If last block has been completed, go up a level + if _sub_no == array.size / (_sub_size): + _sub_size *= 2 + _sub_no = 0 + if _sub_size == array.size * 2: + emit_signal("done") + # Update pointers + _left = _get_begin() + _right = _get_middle() + +func get_effect(i): + 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 < _left or i >= _get_middle() and i < _right or i >= _get_end(): + 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/levels/selection_sort.gd b/levels/selection_sort.gd new file mode 100644 index 0000000..075a53d --- /dev/null +++ b/levels/selection_sort.gd @@ -0,0 +1,46 @@ +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 and +repeat. +""" + +var _base = 0 # Size of sorted subarray +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(_index) < array.at(_min): + if action != null and action != ACTIONS.SWAP: + return emit_signal("mistake") + _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 == _min or i == _index: + return EFFECTS.HIGHLIGHTED + if i < _base: + return EFFECTS.DIMMED + return EFFECTS.NONE diff --git a/models/array_model.gd b/models/array_model.gd index a27a86d..b237231 100644 --- a/models/array_model.gd +++ b/models/array_model.gd @@ -1,29 +1,38 @@ -""" -A plain old one-dimensional random access array. -""" - -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 + self.size = array.size() -func get(i): +func at(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 + +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/project.godot b/project.godot index b66bd5c..328acbe 100644 --- a/project.godot +++ b/project.godot @@ -33,13 +33,31 @@ _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" +}, { +"base": "ComparisonSort", +"class": "MergeSort", +"language": "GDScript", +"path": "res://levels/merge_sort.gd" +}, { +"base": "ComparisonSort", +"class": "SelectionSort", +"language": "GDScript", +"path": "res://levels/selection_sort.gd" } ] _global_script_class_icons={ "ArrayModel": "", "ArrayView": "", "BogoSort": "", "BubbleSort": "", -"ComparisonSort": "" +"ComparisonSort": "", +"InsertionSort": "", +"MergeSort": "", +"SelectionSort": "" } [application] @@ -52,15 +70,18 @@ config/icon="res://assets/icon.png" [autoload] -scene="*res://scripts/scene.gd" +GlobalScene="*res://scripts/scene.gd" +GlobalTheme="*res://scripts/theme.gd" [display] 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] @@ -104,16 +125,26 @@ 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={ "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) ] } -no_swap={ +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/scenes/levels.tscn b/scenes/levels.tscn index c67e806..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 @@ -68,25 +62,22 @@ 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 ) 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="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 rect_min_size = Vector2( 450, 0 ) size_flags_horizontal = 3 size_flags_vertical = 3 -text = "INSTRUCTIONS - -LEFT ARROW: CONTINUE -RIGHT ARROW: SWAP" +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/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 1d14369..d39a4c0 100644 --- a/scripts/end.gd +++ b/scripts/end.gd @@ -1,8 +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") + GlobalScene.change_scene("res://scenes/levels.tscn", + {"level": GlobalScene.get_param("level")}) diff --git a/scripts/levels.gd b/scripts/levels.gd index 834aa83..1ce23d6 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -1,44 +1,57 @@ extends HBoxContainer -var levels: Dictionary -var level +const LEVELS = [ + BubbleSort, + InsertionSort, + SelectionSort, + MergeSort, +] +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 - for level in levels: + """Dynamically load level data.""" + for level in LEVELS: var button = Button.new() - button.text = level + 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]) + button.connect("pressed", self, "_on_Button_pressed", [level.NAME]) $LevelsBorder/Levels.add_child(button) - # Automatically focus on first button - $LevelsBorder/Levels.get_child(0).grab_focus() + # Autofocus last played level + if GlobalScene.get_param("level") == level: + button.grab_focus() + 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: + top_button.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.active = false + """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) # 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(level): - scene.change_scene("res://scenes/play.tscn", - {"name": level, "level": get_level(level)}) +func _on_Button_pressed(name): + GlobalScene.change_scene("res://scenes/play.tscn", {"level": get_level(name)}) -func get_level(level): - match level: - "BUBBLE SORT": - return BubbleSort +func get_level(name): + for level in LEVELS: + if level.NAME == 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 fa303ec..a2caada 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() + _level.next(null) diff --git a/scripts/play.gd b/scripts/play.gd index 295745e..08699f5 100644 --- a/scripts/play.gd +++ b/scripts/play.gd @@ -1,26 +1,24 @@ extends VBoxContainer -var start_time = -1 +var _start_time = -1 func _ready(): - $HUDBorder/HUD/Level.text = scene.get_param("name") + $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() - # Delete ready text - $DisplayBorder/Label.queue_free() - # Load level - var array = ArrayModel.new(10) - var level = scene.get_param("level").new(array) + _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(): - scene.change_scene("res://scenes/end.tscn", {"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..fce2fc4 --- /dev/null +++ b/scripts/theme.gd @@ -0,0 +1,10 @@ +""" +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 4bb5707..70e9ac7 100644 --- a/views/array_view.gd +++ b/views/array_view.gd @@ -1,29 +1,32 @@ -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 -var rects = [] +var _level: ComparisonSort +var _rects = [] func _init(level): - level.connect("mistake", self, "_on_Level_mistake") - add_child(level) - self.level = level + """Add colored rectangles.""" + _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): - 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 + """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 func _on_Level_mistake(): + """Flash the border red on mistakes.""" get_parent().flash()