diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f3d5c41 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..11fc491 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3776231 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +As I would like to submit this project to the Congressional App +Challenge, I unfortunately cannot accept contributions until Monday, +October 19th, 2020 at the earliest. Kindly hold any issues and pull +requests until then. diff --git a/README.md b/README.md index 9fa6671..49e3a78 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,35 @@ -# Human Computer Simulator -*Human Computer Simulator* is a game where you get to become your -favorite algorithm and or data structure. - -# Screenshots -![Level Select](assets/levels.png) - -# Download -This software is in an alpha stage of development and I do not plan on -releasing ready-to-run builds until a stable v1.0 release. However, it -is very easy to run it yourself. Just grab the free and open source -[Godot game engine](https://godotengine.org), import the `project.godot` -file, and hit the play button. +
+

+ Logo + +

Human Computer Simulator

+ +

+ A game where you get to become your favorite algorithm or data structure! +
+ Report Bug + ยท + Request Feature +

+

+ +## Table of Contents + +* [About the Project](#about-the-project) +* [Getting Started](#getting-started) + +## About The Project + +![Level select screen](assets/levels.png) + +You may have come across the famous [15 Sorting Algorithms in 6 Minutes](https://www.youtube.com/watch?v=kPRA0W1kECg) video by [Timo Bingoman](https://github.com/bingmann) at some point in your computer science career. There is currently no shortage of neat visualizations of all kinds of algorithms, but what if you could become the algorithm itself? + +In *Human Computer Simulator*, you control an algorithm operating on some data structure. Right now, the game is limited to sorting arrays. The end vision is to have a library of interactive, playable levels on anything from a search on a binary tree to Dijkstra's shortest path on a graph. + +It's written using the Godot game engine and licensed with [almost no restrictions](LICENSE.txt). Use it to make a lecture a bit more interesting, review for an assignment, or just kill time. Hope you enjoy. + +## Getting Started + +This software is in an alpha stage of development and I do not plan on releasing ready-to-run builds until a stable v1.0 release. However, it is very easy to run and hack the source code yourself. Just grab the lightweight free and open source [Godot game engine](https://godotengine.org/download), import the `project.godot` file, and hit the play button. + +A demo version (large download warning: ~20 MB) is available on this repository's [Github Pages](https://danielzting.github.io/human-computer-simulator). It requires a desktop browser with support for WebAssembly and WebGL; mobile is not currently supported. Since this is still in alpha, some things might be dumb, make no sense whatsoever, or just break outright. I welcome any feedback you may have. diff --git a/assets/levels.png b/assets/levels.png index b269675..46dddf1 100644 Binary files a/assets/levels.png and b/assets/levels.png differ diff --git a/assets/theme.theme b/assets/theme.theme index 8d9e5b5..a324e0e 100644 Binary files a/assets/theme.theme and b/assets/theme.theme differ diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd index e092db1..9aaaa5d 100644 --- a/levels/bubble_sort.gd +++ b/levels/bubble_sort.gd @@ -14,6 +14,10 @@ If the two highlighted elements are out of order, hit LEFT ARROW to swap them. Otherwise, hit RIGHT ARROW to continue. """ +const ACTIONS = { + "SWAP": "Left", + "CONTINUE": "Right", +} var _index = 0 # First of two elements being compared var _end = array.size # Beginning of sorted subarray var _swapped = false @@ -27,7 +31,7 @@ func next(action): return emit_signal("mistake") array.swap(_index, _index + 1) _swapped = true - elif action != null and action != ACTIONS.NO_SWAP: + elif action != null and action != ACTIONS.CONTINUE: return emit_signal("mistake") _index += 1 # Prevent player from having to spam tap through the end diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd index db4cbf4..3156df0 100644 --- a/levels/comparison_sort.gd +++ b/levels/comparison_sort.gd @@ -4,14 +4,6 @@ extends Node signal done signal mistake -const ACTIONS = { - "SWAP": "ui_left", - "NO_SWAP": "ui_right", - - "LEFT": "ui_left", - "RIGHT": "ui_right", -} - const EFFECTS = { "NONE": GlobalTheme.GREEN, "HIGHLIGHTED": GlobalTheme.ORANGE, @@ -22,6 +14,8 @@ const DISABLE_TIME = 1.0 var array: ArrayModel var active = true +var done = false +var moves = 0 var _timer = Timer.new() @@ -32,19 +26,29 @@ func _init(array): _timer.connect("timeout", self, "_on_Timer_timeout") add_child(_timer) self.connect("mistake", self, "_on_ComparisonSort_mistake") + self.connect("done", self, "_on_ComparisonSort_done") func _input(event): """Pass input events for checking and take appropriate action.""" - if not active: + if done or not active: return - for action in ACTIONS.values(): - if event.is_action_pressed(action): - return next(action) + if event.is_pressed(): + moves += 1 + return next(event.as_text()) func next(action): """Check the action and advance state or emit signal as needed.""" push_error("NotImplementedError") +func get_effect(i): + return get_effect(i) + +func _get_effect(i): + push_error("NotImplementedError") + +func _on_ComparisonSort_done(): + done = true + 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 c217d32..c1eec73 100644 --- a/levels/insertion_sort.gd +++ b/levels/insertion_sort.gd @@ -15,6 +15,10 @@ out of order. When this is no longer the case, hit RIGHT ARROW to advance. """ +const ACTIONS = { + "SWAP": "Left", + "CONTINUE": "Right", +} var _end = 1 # Size of the sorted subarray var _index = 1 # Position of element currently being inserted @@ -30,14 +34,14 @@ func next(action): if _index == 0: _grow() else: - if action != null and action != ACTIONS.NO_SWAP: + if action != null and action != ACTIONS.CONTINUE: return emit_signal("mistake") _grow() func get_effect(i): if i == _index or i == _index - 1: return EFFECTS.HIGHLIGHTED - if i < _end: + if i <= _end: return EFFECTS.DIMMED return EFFECTS.NONE diff --git a/levels/merge_sort.gd b/levels/merge_sort.gd index 62e6594..4e2d511 100644 --- a/levels/merge_sort.gd +++ b/levels/merge_sort.gd @@ -15,6 +15,10 @@ highlighted element is on. If you've reached the end of one side, press the other side's ARROW KEY. """ +const ACTIONS = { + "LEFT": "Left", + "RIGHT": "Right", +} 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 @@ -27,18 +31,22 @@ func next(action): if _left == _get_middle(): if action != null and action != ACTIONS.RIGHT: return emit_signal("mistake") + array.emit_signal("removed", _right) _right += 1 elif _right == _get_end(): if action != null and action != ACTIONS.LEFT: return emit_signal("mistake") + array.emit_signal("removed", _left) _left += 1 elif array.at(_left) <= array.at(_right): if action != null and action != ACTIONS.LEFT: return emit_signal("mistake") + array.emit_signal("removed", _left) _left += 1 else: if action != null and action != ACTIONS.RIGHT: return emit_signal("mistake") + array.emit_signal("removed", _right) _right += 1 # If both ends have been reached, merge and advance to next block if _left == _get_middle() and _right == _get_end(): diff --git a/levels/quick_sort.gd b/levels/quick_sort.gd new file mode 100644 index 0000000..3859d7f --- /dev/null +++ b/levels/quick_sort.gd @@ -0,0 +1,79 @@ +class_name QuickSort +extends ComparisonSort + +const NAME = "QUICKSORT" +const ABOUT = """ +Quicksort designates the last element as the pivot and puts everything +less than the pivot before it and everything greater after it. This +partitioning is done by iterating through the array while keeping track +of a pointer initially set to the first element. Every time an element +less than the pivot is encountered, it is swapped with the pointed +element and the pointer moves forward. At the end, the pointer and pivot +are swapped, and the process is repeated on the left and right halves. +""" +const CONTROLS = """ +If the highlighted element is less than the pivot or the pivot has been +reached, press LEFT ARROW to swap it with the pointer. Otherwise, press +RIGHT ARROW to move on. +""" + +const ACTIONS = { + "SWAP": "Left", + "CONTINUE": "Right", +} +var _index = 0 +var _pointer = 0 +# Bookkeep simulated recursion with a binary tree of subarray bounds +var _stack = BinaryTreeModel.new(Vector2(0, array.size - 1)) + +func _init(array).(array): + pass + +func next(action): + if _index == _pivot(): + if action != null and action != ACTIONS.SWAP: + return emit_signal("mistake") + array.swap(_index, _pointer) + if _pointer - _begin() > 1: + _stack.left = BinaryTreeModel.new(Vector2(_begin(), _pointer - 1)) + _stack = _stack.left + elif _pivot() - _pointer > 1: + _stack.right = BinaryTreeModel.new(Vector2(_pointer + 1, _pivot())) + _stack = _stack.right + else: + while (_stack.parent.right != null + or _stack.parent.left.value.y + 2 >= _stack.parent.value.y): + _stack = _stack.parent + if _stack.parent == null: + return emit_signal("done") + _stack.parent.right = BinaryTreeModel.new(Vector2( + _stack.parent.left.value.y + 2, _stack.parent.value.y)) + _stack = _stack.parent.right + _index = _begin() + _pointer = _index + elif array.at(_index) < array.at(_pivot()): + if action != null and action != ACTIONS.SWAP: + return emit_signal("mistake") + array.swap(_index, _pointer) + _index += 1 + _pointer += 1 + else: + if action != null and action != ACTIONS.CONTINUE: + return emit_signal("mistake") + _index += 1 + +func _begin(): + return _stack.value.x + +func _pivot(): + return _stack.value.y + +func get_effect(i): + if i < _begin() or i > _pivot(): + return EFFECTS.DIMMED + if i == _index or i == _pivot(): + return EFFECTS.HIGHLIGHTED + return EFFECTS.NONE + +func get_pointer(): + return _pointer diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd index 075a53d..9e5f237 100644 --- a/levels/selection_sort.gd +++ b/levels/selection_sort.gd @@ -15,6 +15,10 @@ smaller than the left highlighted element, then hit LEFT ARROW and repeat. """ +const ACTIONS = { + "SWAP": "Left", + "CONTINUE": "Right", +} var _base = 0 # Size of sorted subarray var _min = 0 # Index of smallest known element var _index = 1 # Element currently being compared @@ -27,7 +31,7 @@ func next(action): if action != null and action != ACTIONS.SWAP: return emit_signal("mistake") _min = _index - elif action != null and action != ACTIONS.NO_SWAP: + elif action != null and action != ACTIONS.CONTINUE: return emit_signal("mistake") _index += 1 if _index == array.size: @@ -44,3 +48,6 @@ func get_effect(i): if i < _base: return EFFECTS.DIMMED return EFFECTS.NONE + +func get_pointer(): + return _min diff --git a/models/array_model.gd b/models/array_model.gd index b237231..bd038c8 100644 --- a/models/array_model.gd +++ b/models/array_model.gd @@ -1,38 +1,55 @@ class_name ArrayModel extends Reference -var array = [] -var size = 0 +# For all parameterized signals, i <= j +signal removed(i) +signal swapped(i, j) +signal sorted(i, j) -func _init(size=16): +const DEFAULT_SIZE = 16 + +var _array = [] +var size = 0 setget , get_size +var biggest = null + +func _init(size=DEFAULT_SIZE): """Randomize the array.""" for i in range(1, size + 1): - array.append(i) - array.shuffle() - self.size = array.size() + _array.append(i) + _array.shuffle() + biggest = _array.max() func at(i): """Retrieve the value of the element at index i.""" - return array[i] + return _array[i] + +func frac(i): + """Get the quotient of the element at index i and the biggest.""" + return float(_array[i]) / biggest func is_sorted(): """Check if the array is in monotonically increasing order.""" - for i in range(size - 1): - if array[i] > array[i + 1]: + for i in range(get_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 + var temp = _array[i] + _array[i] = _array[j] + _array[j] = temp + emit_signal("swapped", min(i, j), max(i, j)) 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) + 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 + var back = _array.slice(j, size - 1) if j != size else [] + _array = front + sorted + back + emit_signal("sorted", i, j) + +func get_size(): + return _array.size() diff --git a/models/binary_tree_model.gd b/models/binary_tree_model.gd new file mode 100644 index 0000000..4eb5a5f --- /dev/null +++ b/models/binary_tree_model.gd @@ -0,0 +1,18 @@ +class_name BinaryTreeModel +extends Reference + +var left: BinaryTreeModel setget set_left +var right: BinaryTreeModel setget set_right +var parent: BinaryTreeModel +var value = null + +func _init(value): + self.value = value + +func set_left(child: BinaryTreeModel): + child.parent = self + left = child + +func set_right(child: BinaryTreeModel): + child.parent = self + right = child diff --git a/project.godot b/project.godot index 328acbe..c470a97 100644 --- a/project.godot +++ b/project.godot @@ -14,11 +14,16 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://models/array_model.gd" }, { -"base": "HBoxContainer", +"base": "ViewportContainer", "class": "ArrayView", "language": "GDScript", "path": "res://views/array_view.gd" }, { +"base": "Reference", +"class": "BinaryTreeModel", +"language": "GDScript", +"path": "res://models/binary_tree_model.gd" +}, { "base": "ComparisonSort", "class": "BogoSort", "language": "GDScript", @@ -45,6 +50,16 @@ _global_script_classes=[ { "path": "res://levels/merge_sort.gd" }, { "base": "ComparisonSort", +"class": "QuickSort", +"language": "GDScript", +"path": "res://levels/quick_sort.gd" +}, { +"base": "Reference", +"class": "Score", +"language": "GDScript", +"path": "res://scripts/score.gd" +}, { +"base": "ComparisonSort", "class": "SelectionSort", "language": "GDScript", "path": "res://levels/selection_sort.gd" @@ -52,11 +67,14 @@ _global_script_classes=[ { _global_script_class_icons={ "ArrayModel": "", "ArrayView": "", +"BinaryTreeModel": "", "BogoSort": "", "BubbleSort": "", "ComparisonSort": "", "InsertionSort": "", "MergeSort": "", +"QuickSort": "", +"Score": "", "SelectionSort": "" } @@ -64,7 +82,6 @@ _global_script_class_icons={ config/name="Human Computer Simulator" run/main_scene="res://scenes/menu.tscn" -run/low_processor_mode=true boot_splash/image="res://assets/splash.png" config/icon="res://assets/icon.png" @@ -75,9 +92,8 @@ GlobalTheme="*res://scripts/theme.gd" [display] -window/size/width=1920 -window/size/height=1080 -window/size/fullscreen=true +window/size/width=1280 +window/size/height=720 window/size/always_on_top=true window/dpi/allow_hidpi=true window/stretch/mode="2d" @@ -125,24 +141,24 @@ ui_down={ , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null) ] } -SWAP={ +faster={ "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":68,"unicode":0,"echo":false,"script":null) ] } -NO_SWAP={ +slower={ "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":65,"unicode":0,"echo":false,"script":null) ] } -LEFT={ +bigger={ "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":87,"unicode":0,"echo":false,"script":null) ] } -RIGHT={ +smaller={ "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":83,"unicode":0,"echo":false,"script":null) ] } diff --git a/scenes/end.tscn b/scenes/end.tscn deleted file mode 100644 index 39e2f70..0000000 --- a/scenes/end.tscn +++ /dev/null @@ -1,37 +0,0 @@ -[gd_scene load_steps=3 format=2] - -[ext_resource path="res://scripts/end.gd" type="Script" id=1] -[ext_resource path="res://assets/theme.theme" type="Theme" id=2] - -[node name="Viewport" type="MarginContainer"] -anchor_top = 0.5 -anchor_right = 1.0 -anchor_bottom = 0.5 -margin_top = -62.0 -margin_bottom = 62.0 -theme = ExtResource( 2 ) -__meta__ = { -"_edit_use_anchors_": false -} - -[node name="EndScreen" type="VBoxContainer" parent="."] -margin_left = 20.0 -margin_top = 20.0 -margin_right = 1900.0 -margin_bottom = 104.0 -script = ExtResource( 1 ) - -[node name="Score" type="Label" parent="EndScreen"] -margin_right = 1880.0 -margin_bottom = 38.0 -size_flags_vertical = 1 -text = "SCORE" -align = 1 -valign = 2 - -[node name="Button" type="Button" parent="EndScreen"] -margin_top = 46.0 -margin_right = 1880.0 -margin_bottom = 84.0 -text = "restart" -[connection signal="pressed" from="EndScreen/Button" to="EndScreen" method="_on_Button_pressed"] diff --git a/scenes/levels.tscn b/scenes/levels.tscn index f36be90..584fa7f 100644 --- a/scenes/levels.tscn +++ b/scenes/levels.tscn @@ -5,9 +5,8 @@ [ext_resource path="res://scripts/border.gd" type="Script" id=3] [node name="Viewport" type="MarginContainer"] -anchor_top = 0.00114754 anchor_right = 1.0 -anchor_bottom = 1.00115 +anchor_bottom = 1.0 theme = ExtResource( 2 ) custom_constants/margin_right = 30 custom_constants/margin_top = 30 @@ -20,66 +19,88 @@ __meta__ = { [node name="LevelSelect" type="HBoxContainer" parent="."] margin_left = 30.0 margin_top = 30.0 -margin_right = 1890.0 -margin_bottom = 1050.0 +margin_right = 1250.0 +margin_bottom = 690.0 script = ExtResource( 1 ) [node name="LevelsBorder" type="MarginContainer" parent="LevelSelect"] -margin_right = 480.0 -margin_bottom = 1020.0 -rect_min_size = Vector2( 480, 0 ) +margin_right = 303.0 +margin_bottom = 660.0 +size_flags_horizontal = 3 script = ExtResource( 3 ) [node name="Levels" type="VBoxContainer" parent="LevelSelect/LevelsBorder"] margin_left = 20.0 margin_top = 20.0 -margin_right = 460.0 -margin_bottom = 1000.0 +margin_right = 283.0 +margin_bottom = 640.0 + +[node name="LevelsContainer" type="HBoxContainer" parent="LevelSelect/LevelsBorder/Levels"] +margin_right = 263.0 + +[node name="Buttons" type="VBoxContainer" parent="LevelSelect/LevelsBorder/Levels/LevelsContainer"] +margin_right = 255.0 +size_flags_horizontal = 3 + +[node name="Scores" type="VBoxContainer" parent="LevelSelect/LevelsBorder/Levels/LevelsContainer"] +margin_left = 263.0 +margin_right = 263.0 + +[node name="Label" type="Label" parent="LevelSelect/LevelsBorder"] +margin_left = 20.0 +margin_top = 577.0 +margin_right = 283.0 +margin_bottom = 640.0 +size_flags_vertical = 8 +text = "Use the WASD keys to adjust the size and speed of the simulation." +autowrap = true [node name="Preview" type="VBoxContainer" parent="LevelSelect"] -margin_left = 488.0 -margin_right = 1860.0 -margin_bottom = 1020.0 +margin_left = 311.0 +margin_right = 1220.0 +margin_bottom = 660.0 size_flags_horizontal = 3 +size_flags_stretch_ratio = 3.0 [node name="Display" type="MarginContainer" parent="LevelSelect/Preview"] -margin_right = 1372.0 -margin_bottom = 640.0 -rect_min_size = Vector2( 0, 640 ) +margin_right = 909.0 +margin_bottom = 434.0 +size_flags_vertical = 3 +size_flags_stretch_ratio = 2.0 script = ExtResource( 3 ) [node name="InfoBorder" type="MarginContainer" parent="LevelSelect/Preview"] -margin_top = 648.0 -margin_right = 1372.0 -margin_bottom = 1020.0 +margin_top = 442.0 +margin_right = 909.0 +margin_bottom = 660.0 size_flags_vertical = 3 script = ExtResource( 3 ) [node name="Info" type="HBoxContainer" parent="LevelSelect/Preview/InfoBorder"] margin_left = 20.0 margin_top = 20.0 -margin_right = 1352.0 -margin_bottom = 352.0 +margin_right = 889.0 +margin_bottom = 198.0 custom_constants/separation = 50 [node name="About" type="Label" parent="LevelSelect/Preview/InfoBorder/Info"] -margin_right = 810.0 -margin_bottom = 332.0 -rect_min_size = Vector2( 810, 0 ) +margin_right = 546.0 +margin_bottom = 178.0 +size_flags_horizontal = 3 size_flags_vertical = 3 +size_flags_stretch_ratio = 2.0 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"] -margin_left = 860.0 -margin_right = 1332.0 -margin_bottom = 332.0 -rect_min_size = Vector2( 450, 0 ) +margin_left = 596.0 +margin_right = 869.0 +margin_bottom = 178.0 size_flags_horizontal = 3 size_flags_vertical = 3 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="."] +[node name="Timer" type="Timer" parent="LevelSelect"] autostart = true -[connection signal="timeout" from="Timer" to="LevelSelect" method="_on_Timer_timeout"] +[connection signal="timeout" from="LevelSelect/Timer" to="LevelSelect" method="_on_Timer_timeout"] diff --git a/scenes/menu.tscn b/scenes/menu.tscn index 55de3e5..0c692e5 100644 --- a/scenes/menu.tscn +++ b/scenes/menu.tscn @@ -21,20 +21,20 @@ __meta__ = { [node name="MainMenu" type="VBoxContainer" parent="."] margin_left = 40.0 margin_top = 30.0 -margin_right = 1879.0 -margin_bottom = 1049.0 +margin_right = 1239.0 +margin_bottom = 689.0 script = ExtResource( 1 ) [node name="Title" type="Label" parent="MainMenu"] -margin_right = 1839.0 -margin_bottom = 38.0 +margin_right = 1199.0 +margin_bottom = 19.0 text = "Human Computer Simulator" uppercase = true [node name="Display" type="MarginContainer" parent="MainMenu"] -margin_top = 46.0 -margin_right = 1839.0 -margin_bottom = 965.0 +margin_top = 27.0 +margin_right = 1199.0 +margin_bottom = 624.0 size_flags_vertical = 3 script = ExtResource( 3 ) __meta__ = { @@ -42,30 +42,30 @@ __meta__ = { } [node name="Spacing" type="Control" parent="MainMenu"] -margin_top = 973.0 -margin_right = 1839.0 -margin_bottom = 973.0 +margin_top = 632.0 +margin_right = 1199.0 +margin_bottom = 632.0 [node name="Buttons" type="HBoxContainer" parent="MainMenu"] -margin_left = 555.0 -margin_top = 981.0 -margin_right = 1283.0 -margin_bottom = 1019.0 +margin_left = 289.0 +margin_top = 640.0 +margin_right = 909.0 +margin_bottom = 659.0 size_flags_horizontal = 4 custom_constants/separation = 500 [node name="Start" type="Button" parent="MainMenu/Buttons"] -margin_right = 95.0 -margin_bottom = 38.0 +margin_right = 50.0 +margin_bottom = 19.0 size_flags_horizontal = 4 -text = "start" +text = "START" flat = true [node name="Credits" type="Button" parent="MainMenu/Buttons"] -margin_left = 595.0 -margin_right = 728.0 -margin_bottom = 38.0 -text = "credits" +margin_left = 550.0 +margin_right = 620.0 +margin_bottom = 19.0 +text = "CREDITS" [node name="Timer" type="Timer" parent="."] wait_time = 0.25 diff --git a/scenes/play.tscn b/scenes/play.tscn index 681b29e..ba04115 100644 --- a/scenes/play.tscn +++ b/scenes/play.tscn @@ -19,51 +19,51 @@ __meta__ = { [node name="GameDisplay" type="VBoxContainer" parent="."] margin_left = 30.0 margin_top = 30.0 -margin_right = 1890.0 -margin_bottom = 1050.0 +margin_right = 1250.0 +margin_bottom = 690.0 script = ExtResource( 1 ) [node name="HUDBorder" type="MarginContainer" parent="GameDisplay"] -margin_right = 1860.0 -margin_bottom = 78.0 +margin_right = 1220.0 +margin_bottom = 59.0 script = ExtResource( 3 ) [node name="HUD" type="HBoxContainer" parent="GameDisplay/HUDBorder"] margin_left = 20.0 margin_top = 20.0 -margin_right = 1840.0 -margin_bottom = 58.0 +margin_right = 1200.0 +margin_bottom = 39.0 [node name="Level" type="Label" parent="GameDisplay/HUDBorder/HUD"] -margin_right = 906.0 -margin_bottom = 38.0 +margin_right = 586.0 +margin_bottom = 19.0 size_flags_horizontal = 3 text = "LEVEL" [node name="Score" type="Label" parent="GameDisplay/HUDBorder/HUD"] -margin_left = 914.0 -margin_right = 1820.0 -margin_bottom = 38.0 +margin_left = 594.0 +margin_right = 1180.0 +margin_bottom = 19.0 size_flags_horizontal = 3 text = "0.000" align = 2 -[node name="DisplayBorder" type="MarginContainer" parent="GameDisplay"] -margin_top = 86.0 -margin_right = 1860.0 -margin_bottom = 1020.0 +[node name="Display" type="MarginContainer" parent="GameDisplay"] +margin_top = 67.0 +margin_right = 1220.0 +margin_bottom = 660.0 size_flags_vertical = 3 script = ExtResource( 3 ) -[node name="Label" type="Label" parent="GameDisplay/DisplayBorder"] +[node name="Label" type="Label" parent="GameDisplay/Display"] margin_left = 20.0 -margin_top = 448.0 -margin_right = 1840.0 -margin_bottom = 486.0 +margin_top = 287.0 +margin_right = 1200.0 +margin_bottom = 306.0 text = "ready..." align = 1 -[node name="Timer" type="Timer" parent="."] +[node name="Timer" type="Timer" parent="GameDisplay"] one_shot = true autostart = true -[connection signal="timeout" from="Timer" to="GameDisplay" method="_on_Timer_timeout"] +[connection signal="timeout" from="GameDisplay/Timer" to="GameDisplay" method="_on_Timer_timeout"] diff --git a/scripts/border.gd b/scripts/border.gd index e6d1899..f0cbfc0 100644 --- a/scripts/border.gd +++ b/scripts/border.gd @@ -13,18 +13,18 @@ var _timer = Timer.new() var _color_changes = 0 func _ready(): - """Time last return to green with reenabling of controls.""" + # 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(): - """Immediately flash red and then start timer.""" + # Immediately flash red and then start timer _on_Timer_timeout() _timer.start() func _on_Timer_timeout(): - """Switch between green and red.""" + # Switch between green and red if _color_changes == COLOR_CHANGES: _timer.stop() _color_changes = 0 @@ -35,5 +35,4 @@ func _on_Timer_timeout(): update() func _draw(): - """Draw the border.""" draw_rect(Rect2(Vector2(), rect_size), _color, false, WIDTH) diff --git a/scripts/credits.gd b/scripts/credits.gd index 08ed1ee..0da26e6 100644 --- a/scripts/credits.gd +++ b/scripts/credits.gd @@ -10,4 +10,5 @@ func _process(delta): rect_position.y -= 1 func _input(event): - GlobalScene.change_scene("res://scenes/menu.tscn") + if event.is_pressed(): + GlobalScene.change_scene("res://scenes/menu.tscn") diff --git a/scripts/end.gd b/scripts/end.gd deleted file mode 100644 index d39a4c0..0000000 --- a/scripts/end.gd +++ /dev/null @@ -1,9 +0,0 @@ -extends VBoxContainer - -func _ready(): - $Score.text = str(GlobalScene.get_param("score")) - $Button.grab_focus() - -func _on_Button_pressed(): - GlobalScene.change_scene("res://scenes/levels.tscn", - {"level": GlobalScene.get_param("level")}) diff --git a/scripts/levels.gd b/scripts/levels.gd index 1ce23d6..fea6ec5 100644 --- a/scripts/levels.gd +++ b/scripts/levels.gd @@ -5,47 +5,100 @@ const LEVELS = [ InsertionSort, SelectionSort, MergeSort, + QuickSort, ] -var _level: ComparisonSort +const MIN_WAIT = 1.0 / 32 # Should be greater than maximum frame time +const MAX_WAIT = 4 +const MIN_SIZE = 8 +const MAX_SIZE = 256 + +var _level = GlobalScene.get_param("level", LEVELS[0]).new(ArrayModel.new( + GlobalScene.get_param("size", ArrayModel.DEFAULT_SIZE))) func _ready(): - """Dynamically load level data.""" + var buttons = $LevelsBorder/Levels/LevelsContainer/Buttons + var scores = $LevelsBorder/Levels/LevelsContainer/Scores for level in LEVELS: var button = Button.new() 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.NAME]) - $LevelsBorder/Levels.add_child(button) - # Autofocus last played level - if GlobalScene.get_param("level") == level: + button.connect("focus_entered", self, "_on_Button_focus_entered") + button.connect("pressed", self, "_on_Button_pressed", [level]) + buttons.add_child(button) + var score = HBoxContainer.new() + var time = Label.new() + time.align = Label.ALIGN_RIGHT + time.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var tier = Label.new() +# tier.align = Label.ALIGN_RIGHT + score.add_child(time) + score.add_child(tier) + scores.add_child(score) + # Autofocus last played level + for button in buttons.get_children(): + if button.text == _level.NAME: button.grab_focus() - var top_button = $LevelsBorder/Levels.get_children()[0] - var bottom_button = $LevelsBorder/Levels.get_children()[-1] + var top_button = buttons.get_children()[0] + var bottom_button = buttons.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(): - """Initialize the preview section.""" - _level = get_level(get_focus_owner().text).new(ArrayModel.new()) +func _on_Button_focus_entered(size=_level.array.size): + # Update high scores + var buttons = $LevelsBorder/Levels/LevelsContainer/Buttons + var save = GlobalScene.read_save() + for i in range(LEVELS.size()): + var score = $LevelsBorder/Levels/LevelsContainer/Scores.get_child(i) + var name = buttons.get_child(i).text + if name in save and str(size) in save[name]: + var moves = save[name][str(size)][0] + var time = save[name][str(size)][1] + score.get_child(0).text = "%.3f" % time + score.get_child(1).text = Score.get_tier(moves, time) + score.get_child(1).add_color_override( + "font_color", Score.get_color(moves, time)) + else: + score.get_child(0).text = "" + score.get_child(1).text = "INF" + score.get_child(1).add_color_override( + "font_color", GlobalTheme.GREEN) + # Pause a bit to show completely sorted array + if _level.array.is_sorted(): + $Timer.stop() + yield(get_tree().create_timer(1), "timeout") + $Timer.start() + # Prevent race condition caused by switching levels during pause + if not _level.array.is_sorted(): + return + _level = _get_level(get_focus_owner().text).new(ArrayModel.new(size)) _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_entered") # Replace old display with new for child in $Preview/Display.get_children(): child.queue_free() $Preview/Display.add_child(ArrayView.new(_level)) -func _on_Button_pressed(name): - GlobalScene.change_scene("res://scenes/play.tscn", {"level": get_level(name)}) +func _input(event): + if event.is_action_pressed("ui_cancel"): + GlobalScene.change_scene("res://scenes/menu.tscn") + elif event.is_action_pressed("faster"): + $Timer.wait_time = max(MIN_WAIT, $Timer.wait_time / 2) + elif event.is_action_pressed("slower"): + $Timer.wait_time = min(MAX_WAIT, $Timer.wait_time * 2) + elif event.is_action_pressed("bigger"): + _on_Button_focus_entered(min(MAX_SIZE, _level.array.size * 2)) + elif event.is_action_pressed("smaller"): + _on_Button_focus_entered(max(MIN_SIZE, _level.array.size / 2)) + +func _on_Button_pressed(level): + GlobalScene.change_scene("res://scenes/play.tscn", + {"level": level, "size": _level.array.size}) -func get_level(name): +func _get_level(name): for level in LEVELS: if level.NAME == name: return level diff --git a/scripts/menu.gd b/scripts/menu.gd index a2caada..ebf0f8d 100644 --- a/scripts/menu.gd +++ b/scripts/menu.gd @@ -16,3 +16,7 @@ func _on_Credits_pressed(): func _on_Timer_timeout(): _level.next(null) + +func _input(event): + if event.is_action_pressed("ui_cancel"): + get_tree().quit() diff --git a/scripts/play.gd b/scripts/play.gd index 08699f5..c70b21d 100644 --- a/scripts/play.gd +++ b/scripts/play.gd @@ -1,9 +1,11 @@ extends VBoxContainer var _start_time = -1 +var _level = GlobalScene.get_param( + "level", preload("res://scripts/levels.gd").LEVELS[0]) func _ready(): - $HUDBorder/HUD/Level.text = GlobalScene.get_param("level").NAME + $HUDBorder/HUD/Level.text = _level.NAME func _process(delta): if _start_time >= 0: @@ -11,14 +13,57 @@ func _process(delta): func _on_Timer_timeout(): _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)) + $Display/Label.queue_free() # Delete ready text + var level = _level.new(ArrayModel.new( + GlobalScene.get_param("size", ArrayModel.DEFAULT_SIZE))) + level.connect("done", self, "_on_Level_done", [level]) + $Display.add_child(ArrayView.new(level)) func get_score(): return stepify((OS.get_ticks_msec() - _start_time) / 1000.0, 0.001) -func _on_Level_done(): - GlobalScene.change_scene("res://scenes/end.tscn", - {"level": GlobalScene.get_param("level"), "score": get_score()}) +func _input(event): + if event.is_action_pressed("ui_cancel"): + _on_Button_pressed("levels") + +func _on_Level_done(level): + var moves = level.moves + var score = get_score() + var restart = Button.new() + restart.text = "RESTART LEVEL" + restart.connect("pressed", self, "_on_Button_pressed", ["play"]) + var separator = Label.new() + separator.text = " / " + var back = Button.new() + back.text = "BACK TO LEVEL SELECT" + back.connect("pressed", self, "_on_Button_pressed", ["levels"]) + var time = Label.new() + time.text = "%.3f" % get_score() + time.align = Label.ALIGN_RIGHT + time.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _start_time = -1 + var tier = Label.new() + tier.text = Score.get_tier(moves, score) + tier.align = Label.ALIGN_RIGHT + tier.add_color_override("font_color", Score.get_color(moves, score)) + $HUDBorder/HUD/Level.queue_free() + $HUDBorder/HUD/Score.queue_free() + $HUDBorder/HUD.add_child(restart) + $HUDBorder/HUD.add_child(separator) + $HUDBorder/HUD.add_child(back) + $HUDBorder/HUD.add_child(time) + $HUDBorder/HUD.add_child(tier) + restart.grab_focus() + var save = GlobalScene.read_save() + var name = _level.NAME + var size = str(GlobalScene.get_param("size", ArrayModel.DEFAULT_SIZE)) + if not name in save: + save[name] = {} + if not size in save[name]: + save[name][size] = [-1, INF] + save[name][size] = [moves, min(float(time.text), save[name][size][1])] + GlobalScene.write_save(save) + +func _on_Button_pressed(scene): + GlobalScene.change_scene("res://scenes/" + scene + ".tscn", + {"level": _level, "size": GlobalScene.get_param("size")}) diff --git a/scripts/scene.gd b/scripts/scene.gd index 0379e12..5ee37ba 100644 --- a/scripts/scene.gd +++ b/scripts/scene.gd @@ -10,7 +10,19 @@ func change_scene(next_scene, params=null): _params = params get_tree().change_scene(next_scene) -func get_param(name): - if _params != null and _params.has(name): +func get_param(name, default=null): + if _params != null and _params.get(name) != null: return _params[name] - return null + return default + +func read_save(): + var file = File.new() + file.open("user://save.json", File.READ) + return {} if not file.is_open() else parse_json(file.get_as_text()) + file.close() + +func write_save(save): + var file = File.new() + file.open("user://save.json", File.WRITE) + file.store_line(to_json(save)) + file.close() diff --git a/scripts/score.gd b/scripts/score.gd new file mode 100644 index 0000000..8e786d9 --- /dev/null +++ b/scripts/score.gd @@ -0,0 +1,21 @@ +""" +Common helper library for scoring functions. +""" +class_name Score +extends Reference + +const TIERS = ["F", "D", "C", "B", "A", "S"] +const COLORS = [ + Color("f44336"), + Color("ff9800"), + Color("ffeb3b"), + Color("4caf50"), + Color("03a9f4"), + Color("e040fb"), +] + +static func get_tier(moves, seconds): + return TIERS[min(moves / seconds, TIERS.size() - 1)] + +static func get_color(moves, seconds): + return COLORS[min(moves / seconds, COLORS.size() - 1)] diff --git a/scripts/theme.gd b/scripts/theme.gd index fce2fc4..337ea07 100644 --- a/scripts/theme.gd +++ b/scripts/theme.gd @@ -4,7 +4,8 @@ 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) +const GREEN = Color("33ff33") +const DARK_GREEN = Color("7733ff33") +const ORANGE = Color("ffb000") +const RED = Color("f44336") +const BLUE = Color("2196f3") diff --git a/views/array_view.gd b/views/array_view.gd index 70e9ac7..a7e1993 100644 --- a/views/array_view.gd +++ b/views/array_view.gd @@ -3,30 +3,97 @@ Visualization of an array as rectangles of varying heights. """ class_name ArrayView -extends HBoxContainer +extends ViewportContainer +const ANIM_DURATION = 0.1 + +var _tween = Tween.new() var _level: ComparisonSort var _rects = [] +var _positions = [] +var _viewport = Viewport.new() +var _pointer = null +var _pointer_size: int +onready var _separation = 128 / _level.array.size func _init(level): - """Add colored rectangles.""" _level = level - _level.connect("mistake", self, "_on_Level_mistake") + stretch = true + _viewport.usage = Viewport.USAGE_2D 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 + add_child(_tween) # NOTE: This is necessary for it to animate + add_child(_viewport) + +func _ready(): + yield(get_tree(), "idle_frame") + var unit_width = rect_size.x / _level.array.size + _pointer_size = max((unit_width - _separation) / 4, 2) + # Keep track of accumulated pixel error from integer division + var error = float(rect_size.x) / _level.array.size - unit_width + var accumulated = 0 + var x = 0 + _level.connect("mistake", get_parent(), "flash") + var width = unit_width - _separation + var height = rect_size.y - _pointer_size * 2 + for i in range(_level.array.size): + var rect = Polygon2D.new() + if accumulated >= 1: + x += 1 + accumulated -= 1 + rect.polygon = [ + Vector2(0, 0), + Vector2(0, height), + Vector2(width, height), + Vector2(width, 0), + ] + accumulated += error + rect.position = Vector2(x, rect_size.y) + _positions.append(x) + x += unit_width _rects.append(rect) - add_child(rect) + _viewport.add_child(rect) + _level.array.connect("swapped", self, "_on_ArrayModel_swapped") + _level.array.connect("sorted", self, "_on_ArrayModel_sorted") + if _level.has_method("get_pointer"): + _pointer = Polygon2D.new() + _pointer.polygon = [ + Vector2(width / 2, _pointer_size), + Vector2(width / 2 - _pointer_size, 0), + Vector2(width / 2 + _pointer_size, 0), + ] + _pointer.color = GlobalTheme.BLUE + _viewport.add_child(_pointer) 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 + if _pointer != null: + var pointed = _level.get_pointer() + var height = rect_size.y - _pointer_size * 2 + _pointer.position = Vector2(_rects[pointed].position.x, + height - _level.array.frac(pointed) * height) + if _level.done: + _pointer.queue_free() + for i in range(_rects.size()): + if _level.done: + _rects[i].color = ComparisonSort.EFFECTS.NONE + else: + _rects[i].color = _level.get_effect(i) + _rects[i].scale.y = -_level.array.frac(i) + +func _on_ArrayModel_swapped(i, j): + var time = ANIM_DURATION * (1 + float(j - i) / _level.array.size) + _tween.interpolate_property( + _rects[i], "position:x", null, _positions[j], time) + _tween.interpolate_property( + _rects[j], "position:x", null, _positions[i], time) + var temp = _rects[i] + _rects[i] = _rects[j] + _rects[j] = temp + _tween.start() -func _on_Level_mistake(): - """Flash the border red on mistakes.""" - get_parent().flash() +func _on_ArrayModel_sorted(i, j): + for x in range(i, j): + _rects[x].position.y = 0 + for x in range(i, j): + _tween.interpolate_property( + _rects[x], "position:y", null, rect_size.y, ANIM_DURATION) + _tween.start()