From e3e46097174fbeb28ffe32c496fb8f584307b20d Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Thu, 23 Jul 2020 12:25:21 -0500
Subject: [PATCH 01/17] refactor: move actions into individual classes
It didn't make any sense for sorts with different action semantics to
share a global actions dictionary.
---
levels/bubble_sort.gd | 6 +++++-
levels/comparison_sort.gd | 13 ++-----------
levels/insertion_sort.gd | 6 +++++-
levels/merge_sort.gd | 4 ++++
levels/selection_sort.gd | 6 +++++-
project.godot | 20 --------------------
6 files changed, 21 insertions(+), 34 deletions(-)
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..5ca935e 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,
@@ -37,9 +29,8 @@ 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)
+ if event.is_pressed():
+ return next(event.as_text())
func next(action):
"""Check the action and advance state or emit signal as needed."""
diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd
index c217d32..867dff4 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,7 +34,7 @@ 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()
diff --git a/levels/merge_sort.gd b/levels/merge_sort.gd
index 62e6594..a8c42fa 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
diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd
index 075a53d..d1ac922 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:
diff --git a/project.godot b/project.godot
index 328acbe..3dde80e 100644
--- a/project.godot
+++ b/project.godot
@@ -125,26 +125,6 @@ ui_down={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
]
}
-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)
- ]
-}
-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]
From 9aae4e34a6a8f52fafce71d112cb30980b53ba94 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Sat, 25 Jul 2020 14:13:41 -0500
Subject: [PATCH 02/17] feat: add swap animation
---
models/array_model.gd | 3 +++
views/array_view.gd | 18 ++++++++++++++++++
2 files changed, 21 insertions(+)
diff --git a/models/array_model.gd b/models/array_model.gd
index b237231..bda96c7 100644
--- a/models/array_model.gd
+++ b/models/array_model.gd
@@ -1,6 +1,8 @@
class_name ArrayModel
extends Reference
+signal swapped(i, j) # where i <= j
+
var array = []
var size = 0
@@ -27,6 +29,7 @@ func swap(i, j):
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."""
diff --git a/views/array_view.gd b/views/array_view.gd
index 70e9ac7..1fc5f77 100644
--- a/views/array_view.gd
+++ b/views/array_view.gd
@@ -5,6 +5,11 @@ Visualization of an array as rectangles of varying heights.
class_name ArrayView
extends HBoxContainer
+const SWAP_DURATION = 0.1
+const TRANS_TYPE = Tween.TRANS_LINEAR
+const EASE_TYPE = Tween.EASE_IN_OUT
+
+var _tween = Tween.new()
var _level: ComparisonSort
var _rects = []
@@ -12,6 +17,7 @@ func _init(level):
"""Add colored rectangles."""
_level = level
_level.connect("mistake", self, "_on_Level_mistake")
+ _level.array.connect("swapped", self, "_on_ArrayModel_swapped")
add_child(_level) # NOTE: This is necessary for it to read input
for i in range(level.array.size):
var rect = ColorRect.new()
@@ -19,6 +25,7 @@ func _init(level):
rect.size_flags_vertical = Control.SIZE_SHRINK_END
_rects.append(rect)
add_child(rect)
+ add_child(_tween) # NOTE: This is necessary for it to animate
func _process(delta):
"""Update heights of rectangles based on array values."""
@@ -30,3 +37,14 @@ func _process(delta):
func _on_Level_mistake():
"""Flash the border red on mistakes."""
get_parent().flash()
+
+func _on_ArrayModel_swapped(i, j):
+ """Produce a swapping animation."""
+ _tween.interpolate_property(_rects[i], "rect_position",
+ null, _rects[j].rect_position, SWAP_DURATION, TRANS_TYPE, EASE_TYPE)
+ _tween.interpolate_property(_rects[j], "rect_position",
+ null, _rects[i].rect_position, SWAP_DURATION, TRANS_TYPE, EASE_TYPE)
+ var temp = _rects[i]
+ _rects[i] = _rects[j]
+ _rects[j] = temp
+ _tween.start()
From 96414dc87df278eca8d1565cb4314487e3a5aff2 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Tue, 28 Jul 2020 15:30:21 -0500
Subject: [PATCH 03/17] style: prefix private variables with underscore
---
models/array_model.gd | 30 ++++++++++++++++--------------
project.godot | 1 -
scripts/credits.gd | 3 ++-
3 files changed, 18 insertions(+), 16 deletions(-)
diff --git a/models/array_model.gd b/models/array_model.gd
index bda96c7..a83b7bf 100644
--- a/models/array_model.gd
+++ b/models/array_model.gd
@@ -3,39 +3,41 @@ extends Reference
signal swapped(i, j) # where i <= j
-var array = []
-var size = 0
+var _array = []
+var size = 0 setget , get_size
func _init(size=16):
"""Randomize the array."""
for i in range(1, size + 1):
- array.append(i)
- array.shuffle()
- self.size = array.size()
+ _array.append(i)
+ _array.shuffle()
func at(i):
"""Retrieve the value of the element at index i."""
- return array[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]:
+ 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
+
+func get_size():
+ return _array.size()
diff --git a/project.godot b/project.godot
index 3dde80e..47aadf6 100644
--- a/project.godot
+++ b/project.godot
@@ -77,7 +77,6 @@ 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"
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")
From a74cb50e15ce2c860d6d415b45f9147ff2f50bc9 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Wed, 29 Jul 2020 14:03:31 -0500
Subject: [PATCH 04/17] fix: prevent animation race condition
Save initial positions of array bars so that if the user presses a
button so fast that one animation begins before the previous was
finished, the positions wouldn't be messed up.
---
views/array_view.gd | 35 +++++++++++++++++++++--------------
1 file changed, 21 insertions(+), 14 deletions(-)
diff --git a/views/array_view.gd b/views/array_view.gd
index 1fc5f77..9272ff0 100644
--- a/views/array_view.gd
+++ b/views/array_view.gd
@@ -6,17 +6,17 @@ class_name ArrayView
extends HBoxContainer
const SWAP_DURATION = 0.1
-const TRANS_TYPE = Tween.TRANS_LINEAR
-const EASE_TYPE = Tween.EASE_IN_OUT
var _tween = Tween.new()
var _level: ComparisonSort
var _rects = []
+# Save initial positions to prevent animation race condition
+var _positions = []
+var _unit_height: int
func _init(level):
"""Add colored rectangles."""
_level = level
- _level.connect("mistake", self, "_on_Level_mistake")
_level.array.connect("swapped", self, "_on_ArrayModel_swapped")
add_child(_level) # NOTE: This is necessary for it to read input
for i in range(level.array.size):
@@ -26,24 +26,31 @@ func _init(level):
_rects.append(rect)
add_child(rect)
add_child(_tween) # NOTE: This is necessary for it to animate
+ _positions.resize(_level.array.size)
+
+func _ready():
+ _level.connect("mistake", get_parent(), "flash")
+
+func _draw():
+ # HACK: Workaround for resized signal not firing when window resized
+ _unit_height = rect_size.y / _level.array.size
+ for i in range(_rects.size()):
+ _rects[i].rect_scale.y = -1
+ _positions[i] = _rects[i].rect_position
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
+ for i in range(_rects.size()):
_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()
+ _rects[i].rect_size.y = _level.array.at(i) * _unit_height
func _on_ArrayModel_swapped(i, j):
"""Produce a swapping animation."""
- _tween.interpolate_property(_rects[i], "rect_position",
- null, _rects[j].rect_position, SWAP_DURATION, TRANS_TYPE, EASE_TYPE)
- _tween.interpolate_property(_rects[j], "rect_position",
- null, _rects[i].rect_position, SWAP_DURATION, TRANS_TYPE, EASE_TYPE)
+ var time = SWAP_DURATION * (1 + float(j - i) / _level.array.size)
+ _tween.interpolate_property(
+ _rects[i], "rect_position", null, _positions[j], time)
+ _tween.interpolate_property(
+ _rects[j], "rect_position", null, _positions[i], time)
var temp = _rects[i]
_rects[i] = _rects[j]
_rects[j] = temp
From 5ed98b5b64150e5a1d0329a917f197d35aaf5997 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Thu, 30 Jul 2020 12:17:42 -0500
Subject: [PATCH 05/17] fix: redraw ArrayView when (un)maximizing window
To accomplish this, ArrayView now uses manually positioned Polygon2Ds
instead of ColorRects because Controls turned out to be extremely
finnicky to work with. Also changed default window size to 1280x720 for
convenience on smaller screens and refactored scenes/levels.tscn.
---
assets/theme.theme | Bin 769 -> 764 bytes
project.godot | 4 ++--
scenes/levels.tscn | 55 ++++++++++++++++++++++----------------------
scenes/menu.tscn | 38 +++++++++++++++---------------
scripts/border.gd | 7 +++---
scripts/levels.gd | 8 +++----
views/array_view.gd | 43 ++++++++++++++++------------------
7 files changed, 75 insertions(+), 80 deletions(-)
diff --git a/assets/theme.theme b/assets/theme.theme
index 8d9e5b559ab92c16a7945091470407acf70ba052..a324e0e7423be0ca0fe0b4d5d862604115eb0f16 100644
GIT binary patch
literal 764
zcmV0LC3*JRq+G01UtYVMzo;
z5fihNyT5(?PriS@HzpiQD_z~+FAsHp6Ki7OU?}>qtz@I5Aktus2S)%*
z08IcCuZ-=Y{}nuHKi3SLshlV`E#>Mz#^b(an5EFYuJ-?dYYE5y2VAFwm@8DqYkR$9
z`v^
z5E2!-Y`7>O{)=Q1{Nu7(xwZ8;=hDtsMLR_}iH%a?wRze}m6uaon86j>mVsE`r+MRW
zU1v;6S%SD>Hbag_%B+!s@TdVOD=aG&D2RlBNciyBbZ|WQFTg$pq#_VH*8$Pg~c(fcm@=|06`|LnQV9d-^fG}xTwk-jO
zKy&p)nX}Q>k>bke>o@W+Pei*_PA5N9Qs#!s8L;B*1~2&S}zs1+er+j!L-%F~qYf%L4F%=GVZS0SiY3TJ1O
zKCz>MmViygiEH*`voRgklx{AJ`d_>`szN)ORGFsJG@3y>9JK0wSfW>^mJDy?w0AtS
uehUcqxEEe~2q}5$eSt`xpiw7BrOfC)iQtYD4$nu~F0=pC@>rfyQ$s_`(sE+}
literal 769
zcmV+c1OEI{Q$s@n000005C8xV2mk=-0ssIgwJ-f(4+s4g0LEQmJur^~0K+`b^QL*6
zpNDy#=j1|!jlcKvcmD|WKl%Rsj`|H&R1pN65xk^`$F^DRkjQAGQX>?P3`<#G(>zB2
zOaM&)6t9f!qW={vT3x@lE=kv
z`WAGanA2*g4VN|LA}jC`sD)+cy=>VTt%lqF8@Og3Q(_A~ZTEIGPHis_bSwk^5&Q=b
zGC?FNblGrGMEoDgB>2Z=wQ_6gan7ZkuZng8xrvQZ;72#0>C@U;06)22|gh=@C*mQ6}_&?x*1I-5K<(v`}DLAb)
z&ddKnWd(~*Qxgc6Jz@Ee!O)YmX2d&qI0uk#xX5^h7T2KS7$C@`b(6$=l}F=+7M9AK
z5Mwjps%*$&0}g#AIqTq)%m4vI?8HqEsJ0A
z?M0QBG@5qbB+L|@&}oH;RWM$4huSpVJz$=dmzn-O?kWV;
zs^R>M(`R&4&=RnzsBz7nY&NFDn$pb$QTG=+M^$u3lPc46nnp8dhl5rv4@>mQ)Dq!s
zo%W7v)^7pf9{0j|4
Date: Wed, 5 Aug 2020 16:22:24 -0500
Subject: [PATCH 06/17] feat: player can control preview size and speed
In addition, dynamically adjust the inter-block spacing depending on the
amount of elements for aesthetics.
---
project.godot | 21 ++++++++++++++++++++-
scenes/levels.tscn | 13 +++++++++++--
scripts/levels.gd | 19 ++++++++++++++++---
views/array_view.gd | 6 +++---
4 files changed, 50 insertions(+), 9 deletions(-)
diff --git a/project.godot b/project.godot
index 4adf353..199a6f9 100644
--- a/project.godot
+++ b/project.godot
@@ -64,7 +64,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"
@@ -124,6 +123,26 @@ ui_down={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
]
}
+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":46,"unicode":0,"echo":false,"script":null)
+ ]
+}
+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":44,"unicode":0,"echo":false,"script":null)
+ ]
+}
+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":61,"unicode":0,"echo":false,"script":null)
+ ]
+}
+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":45,"unicode":0,"echo":false,"script":null)
+ ]
+}
[rendering]
diff --git a/scenes/levels.tscn b/scenes/levels.tscn
index efd8353..43fc2a8 100644
--- a/scenes/levels.tscn
+++ b/scenes/levels.tscn
@@ -35,6 +35,15 @@ margin_top = 20.0
margin_right = 283.0
margin_bottom = 640.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 +/- and > to adjust the size and speed of the simulation, respectively."
+autowrap = true
+
[node name="Preview" type="VBoxContainer" parent="LevelSelect"]
margin_left = 311.0
margin_right = 1220.0
@@ -81,6 +90,6 @@ 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/scripts/levels.gd b/scripts/levels.gd
index 0fde1ad..674f6e8 100644
--- a/scripts/levels.gd
+++ b/scripts/levels.gd
@@ -6,7 +6,10 @@ const LEVELS = [
SelectionSort,
MergeSort,
]
-var _level: ComparisonSort
+const MIN_SIZE = 8
+const MAX_SIZE = 256
+
+var _level = LEVELS[0].new(ArrayModel.new())
func _ready():
for level in LEVELS:
@@ -28,8 +31,8 @@ func _ready():
if GlobalScene.get_param("level") == null:
top_button.grab_focus()
-func _on_Button_focus_changed():
- _level = _get_level(get_focus_owner().text).new(ArrayModel.new())
+func _on_Button_focus_changed(model=ArrayModel.new(_level.array.size)):
+ _level = _get_level(get_focus_owner().text).new(model)
_level.active = false
$Preview/InfoBorder/Info/About.text = _cleanup(_level.ABOUT)
$Preview/InfoBorder/Info/Controls.text = _cleanup(_level.CONTROLS)
@@ -40,6 +43,16 @@ func _on_Button_focus_changed():
child.queue_free()
$Preview/Display.add_child(ArrayView.new(_level))
+func _input(event):
+ if event.is_action_pressed("faster"):
+ $Timer.wait_time /= 2
+ elif event.is_action_pressed("slower"):
+ $Timer.wait_time *= 2
+ elif event.is_action_pressed("bigger"):
+ _on_Button_focus_changed(ArrayModel.new(min(MAX_SIZE, _level.array.size * 2)))
+ elif event.is_action_pressed("smaller"):
+ _on_Button_focus_changed(ArrayModel.new(max(MIN_SIZE, _level.array.size / 2)))
+
func _on_Button_pressed(name):
GlobalScene.change_scene("res://scenes/play.tscn", {"level": _get_level(name)})
diff --git a/views/array_view.gd b/views/array_view.gd
index be037b2..bd79e48 100644
--- a/views/array_view.gd
+++ b/views/array_view.gd
@@ -6,13 +6,13 @@ class_name ArrayView
extends HBoxContainer
const SWAP_DURATION = 0.1
-const SEPARATION = 8
var _tween = Tween.new()
var _level: ComparisonSort
var _rects = []
var _unit_width: int
var _unit_height: int
+onready var _separation = 128 / _level.array.size
func _init(level):
_level = level
@@ -29,8 +29,8 @@ func _ready():
rect.polygon = [
Vector2(0, 0),
Vector2(0, rect_size.y),
- Vector2(_unit_width - SEPARATION, rect_size.y),
- Vector2(_unit_width - SEPARATION, 0),
+ Vector2(_unit_width - _separation, rect_size.y),
+ Vector2(_unit_width - _separation, 0),
]
rect.position = Vector2(i * _unit_width, rect_size.y)
_rects.append(rect)
From a7c064ae79141df246f57558781a2208c5ed68b1 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Thu, 6 Aug 2020 12:58:01 -0500
Subject: [PATCH 07/17] fix: implement variable separation to fill display
Block sizes remain constant but separation can be one more pixel so the
ArrayView fills its entire parent without looking blurry. This is just a
quick fix; a thorough implementation later should vary block sizes while
keeping separation constant so that large simulations with zero
separation between blocks look acceptable. This would involve smarter
and more complex positioning code.
---
scripts/levels.gd | 2 +-
views/array_view.gd | 17 ++++++++++++++---
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/scripts/levels.gd b/scripts/levels.gd
index 674f6e8..77b3f12 100644
--- a/scripts/levels.gd
+++ b/scripts/levels.gd
@@ -7,7 +7,7 @@ const LEVELS = [
MergeSort,
]
const MIN_SIZE = 8
-const MAX_SIZE = 256
+const MAX_SIZE = 128
var _level = LEVELS[0].new(ArrayModel.new())
diff --git a/views/array_view.gd b/views/array_view.gd
index bd79e48..1b473b3 100644
--- a/views/array_view.gd
+++ b/views/array_view.gd
@@ -10,6 +10,7 @@ const SWAP_DURATION = 0.1
var _tween = Tween.new()
var _level: ComparisonSort
var _rects = []
+var _positions = []
var _unit_width: int
var _unit_height: int
onready var _separation = 128 / _level.array.size
@@ -23,16 +24,26 @@ func _ready():
yield(get_tree(), "idle_frame")
_unit_width = rect_size.x / _level.array.size
_unit_height = rect_size.y / _level.array.size
+ # 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")
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, rect_size.y),
Vector2(_unit_width - _separation, rect_size.y),
Vector2(_unit_width - _separation, 0),
]
- rect.position = Vector2(i * _unit_width, rect_size.y)
+ accumulated += error
+ rect.position = Vector2(x, rect_size.y)
+ _positions.append(x)
+ x += _unit_width
_rects.append(rect)
add_child(rect)
_level.array.connect("swapped", self, "_on_ArrayModel_swapped")
@@ -45,9 +56,9 @@ func _process(delta):
func _on_ArrayModel_swapped(i, j):
var time = SWAP_DURATION * (1 + float(j - i) / _level.array.size)
_tween.interpolate_property(
- _rects[i], "position:x", null, j * _unit_width, time)
+ _rects[i], "position:x", null, _positions[j], time)
_tween.interpolate_property(
- _rects[j], "position:x", null, i * _unit_width, time)
+ _rects[j], "position:x", null, _positions[i], time)
var temp = _rects[i]
_rects[i] = _rects[j]
_rects[j] = temp
From 0d3b1df73162687b401afd0ddb019ebcac1ddcb0 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Thu, 6 Aug 2020 14:46:36 -0500
Subject: [PATCH 08/17] feat: add sorting animation to merge sort
To do so, ArrayView had to be refactored to inherit Viewport so as to
hide rectangles outside of the display bounds.
Closes #2
---
levels/merge_sort.gd | 4 ++++
models/array_model.gd | 6 +++++-
project.godot | 2 +-
views/array_view.gd | 22 ++++++++++++++++++----
4 files changed, 28 insertions(+), 6 deletions(-)
diff --git a/levels/merge_sort.gd b/levels/merge_sort.gd
index a8c42fa..4e2d511 100644
--- a/levels/merge_sort.gd
+++ b/levels/merge_sort.gd
@@ -31,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/models/array_model.gd b/models/array_model.gd
index a83b7bf..3c3c697 100644
--- a/models/array_model.gd
+++ b/models/array_model.gd
@@ -1,7 +1,10 @@
class_name ArrayModel
extends Reference
-signal swapped(i, j) # where i <= j
+# For all parameterized signals, i <= j
+signal removed(i)
+signal swapped(i, j)
+signal sorted(i, j)
var _array = []
var size = 0 setget , get_size
@@ -38,6 +41,7 @@ func sort(i, j):
sorted.sort()
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/project.godot b/project.godot
index 199a6f9..56cf78f 100644
--- a/project.godot
+++ b/project.godot
@@ -14,7 +14,7 @@ _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"
diff --git a/views/array_view.gd b/views/array_view.gd
index 1b473b3..c88ed93 100644
--- a/views/array_view.gd
+++ b/views/array_view.gd
@@ -3,9 +3,9 @@ Visualization of an array as rectangles of varying heights.
"""
class_name ArrayView
-extends HBoxContainer
+extends ViewportContainer
-const SWAP_DURATION = 0.1
+const ANIM_DURATION = 0.1
var _tween = Tween.new()
var _level: ComparisonSort
@@ -13,15 +13,20 @@ var _rects = []
var _positions = []
var _unit_width: int
var _unit_height: int
+var _viewport = Viewport.new()
onready var _separation = 128 / _level.array.size
func _init(level):
_level = level
+ stretch = true
+ _viewport.usage = Viewport.USAGE_2D
add_child(_level) # NOTE: This is necessary for it to read input
add_child(_tween) # NOTE: This is necessary for it to animate
+ add_child(_viewport)
func _ready():
yield(get_tree(), "idle_frame")
+ _viewport.size = rect_size
_unit_width = rect_size.x / _level.array.size
_unit_height = rect_size.y / _level.array.size
# Keep track of accumulated pixel error from integer division
@@ -45,8 +50,9 @@ func _ready():
_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")
func _process(delta):
for i in range(_rects.size()):
@@ -54,7 +60,7 @@ func _process(delta):
_rects[i].scale.y = -float(_level.array.at(i)) / _level.array.size
func _on_ArrayModel_swapped(i, j):
- var time = SWAP_DURATION * (1 + float(j - i) / _level.array.size)
+ 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(
@@ -63,3 +69,11 @@ func _on_ArrayModel_swapped(i, j):
_rects[i] = _rects[j]
_rects[j] = temp
_tween.start()
+
+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()
From 8971957c7310746530fac8248dc9aeae5b7bcda6 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Fri, 7 Aug 2020 15:58:00 -0500
Subject: [PATCH 09/17] refactor: clean up levels.gd
---
scripts/levels.gd | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/scripts/levels.gd b/scripts/levels.gd
index 77b3f12..96d216b 100644
--- a/scripts/levels.gd
+++ b/scripts/levels.gd
@@ -16,8 +16,8 @@ func _ready():
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])
+ button.connect("focus_entered", self, "_on_Button_focus_entered")
+ button.connect("pressed", self, "_on_Button_pressed", [level])
$LevelsBorder/Levels.add_child(button)
# Autofocus last played level
if GlobalScene.get_param("level") == level:
@@ -31,13 +31,13 @@ func _ready():
if GlobalScene.get_param("level") == null:
top_button.grab_focus()
-func _on_Button_focus_changed(model=ArrayModel.new(_level.array.size)):
- _level = _get_level(get_focus_owner().text).new(model)
+func _on_Button_focus_entered(size=_level.array.size):
+ _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()
@@ -49,12 +49,12 @@ func _input(event):
elif event.is_action_pressed("slower"):
$Timer.wait_time *= 2
elif event.is_action_pressed("bigger"):
- _on_Button_focus_changed(ArrayModel.new(min(MAX_SIZE, _level.array.size * 2)))
+ _on_Button_focus_entered(min(MAX_SIZE, _level.array.size * 2))
elif event.is_action_pressed("smaller"):
- _on_Button_focus_changed(ArrayModel.new(max(MIN_SIZE, _level.array.size / 2)))
+ _on_Button_focus_entered(max(MIN_SIZE, _level.array.size / 2))
-func _on_Button_pressed(name):
- GlobalScene.change_scene("res://scenes/play.tscn", {"level": _get_level(name)})
+func _on_Button_pressed(level):
+ GlobalScene.change_scene("res://scenes/play.tscn", {"level": level})
func _get_level(name):
for level in LEVELS:
From 54422fabc8c564567b71b1d38401d36d2db4f01e Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Fri, 7 Aug 2020 16:50:21 -0500
Subject: [PATCH 10/17] feat: make level preview more user-friendly
The simulation speeds are now clamped and it no longer restarts the
preview immediately after finishing.
---
levels/bogo_sort.gd | 2 +-
levels/bubble_sort.gd | 2 +-
levels/comparison_sort.gd | 10 +++++++++-
levels/insertion_sort.gd | 2 +-
levels/merge_sort.gd | 2 +-
levels/selection_sort.gd | 2 +-
models/array_model.gd | 2 +-
scripts/levels.gd | 14 ++++++++++++--
8 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/levels/bogo_sort.gd b/levels/bogo_sort.gd
index 9655e91..99c749f 100644
--- a/levels/bogo_sort.gd
+++ b/levels/bogo_sort.gd
@@ -17,5 +17,5 @@ func next(action):
if array.is_sorted():
emit_signal("done")
-func get_effect(i):
+func _get_effect(i):
return EFFECTS.NONE
diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd
index 9aaaa5d..cb612b6 100644
--- a/levels/bubble_sort.gd
+++ b/levels/bubble_sort.gd
@@ -42,7 +42,7 @@ func next(action):
_end -= 1
_swapped = false
-func get_effect(i):
+func _get_effect(i):
if i == _index or i == _index + 1:
return EFFECTS.HIGHLIGHTED
if i >= _end:
diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd
index 5ca935e..b2097bf 100644
--- a/levels/comparison_sort.gd
+++ b/levels/comparison_sort.gd
@@ -27,7 +27,7 @@ func _init(array):
func _input(event):
"""Pass input events for checking and take appropriate action."""
- if not active:
+ if not active or array.is_sorted():
return
if event.is_pressed():
return next(event.as_text())
@@ -36,6 +36,14 @@ func next(action):
"""Check the action and advance state or emit signal as needed."""
push_error("NotImplementedError")
+func get_effect(i):
+ if array.is_sorted():
+ return EFFECTS.NONE
+ return _get_effect(i)
+
+func _get_effect(i):
+ 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 867dff4..0a9b576 100644
--- a/levels/insertion_sort.gd
+++ b/levels/insertion_sort.gd
@@ -38,7 +38,7 @@ func next(action):
return emit_signal("mistake")
_grow()
-func get_effect(i):
+func _get_effect(i):
if i == _index or i == _index - 1:
return EFFECTS.HIGHLIGHTED
if i < _end:
diff --git a/levels/merge_sort.gd b/levels/merge_sort.gd
index 4e2d511..4019eb4 100644
--- a/levels/merge_sort.gd
+++ b/levels/merge_sort.gd
@@ -62,7 +62,7 @@ func next(action):
_left = _get_begin()
_right = _get_middle()
-func get_effect(i):
+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:
diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd
index d1ac922..6521474 100644
--- a/levels/selection_sort.gd
+++ b/levels/selection_sort.gd
@@ -42,7 +42,7 @@ func next(action):
if _base == array.size - 1:
emit_signal("done")
-func get_effect(i):
+func _get_effect(i):
if i == _min or i == _index:
return EFFECTS.HIGHLIGHTED
if i < _base:
diff --git a/models/array_model.gd b/models/array_model.gd
index 3c3c697..d259b8c 100644
--- a/models/array_model.gd
+++ b/models/array_model.gd
@@ -21,7 +21,7 @@ func at(i):
func is_sorted():
"""Check if the array is in monotonically increasing order."""
- for i in range(size - 1):
+ for i in range(get_size() - 1):
if _array[i] > _array[i + 1]:
return false
return true
diff --git a/scripts/levels.gd b/scripts/levels.gd
index 96d216b..2ae3037 100644
--- a/scripts/levels.gd
+++ b/scripts/levels.gd
@@ -6,6 +6,8 @@ const LEVELS = [
SelectionSort,
MergeSort,
]
+const MIN_WAIT = 1.0 / 32 # Should be greater than maximum frame time
+const MAX_WAIT = 4
const MIN_SIZE = 8
const MAX_SIZE = 128
@@ -32,6 +34,14 @@ func _ready():
top_button.grab_focus()
func _on_Button_focus_entered(size=_level.array.size):
+ # 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)
@@ -45,9 +55,9 @@ func _on_Button_focus_entered(size=_level.array.size):
func _input(event):
if event.is_action_pressed("faster"):
- $Timer.wait_time /= 2
+ $Timer.wait_time = max(MIN_WAIT, $Timer.wait_time / 2)
elif event.is_action_pressed("slower"):
- $Timer.wait_time *= 2
+ $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"):
From cf0b7ff4cb5a73ca08af56b511302a0ca1848c0a Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Sat, 8 Aug 2020 11:24:44 -0500
Subject: [PATCH 11/17] refactor: move end.tscn's function into play.tscn
There was no need for a separate end scene with nothing but a score and
a button.
---
levels/comparison_sort.gd | 9 +++++++--
scenes/end.tscn | 37 -------------------------------------
scenes/menu.tscn | 4 ++--
scenes/play.tscn | 38 +++++++++++++++++++-------------------
scripts/end.gd | 9 ---------
scripts/play.gd | 26 ++++++++++++++++++++++++--
6 files changed, 52 insertions(+), 71 deletions(-)
delete mode 100644 scenes/end.tscn
delete mode 100644 scripts/end.gd
diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd
index b2097bf..bb604ba 100644
--- a/levels/comparison_sort.gd
+++ b/levels/comparison_sort.gd
@@ -14,6 +14,7 @@ const DISABLE_TIME = 1.0
var array: ArrayModel
var active = true
+var _done = false
var _timer = Timer.new()
@@ -24,10 +25,11 @@ 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 or array.is_sorted():
+ if _done or not active:
return
if event.is_pressed():
return next(event.as_text())
@@ -37,13 +39,16 @@ func next(action):
push_error("NotImplementedError")
func get_effect(i):
- if array.is_sorted():
+ if _done:
return EFFECTS.NONE
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/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/menu.tscn b/scenes/menu.tscn
index 2bcc271..0c692e5 100644
--- a/scenes/menu.tscn
+++ b/scenes/menu.tscn
@@ -58,14 +58,14 @@ custom_constants/separation = 500
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 = 550.0
margin_right = 620.0
margin_bottom = 19.0
-text = "credits"
+text = "CREDITS"
[node name="Timer" type="Timer" parent="."]
wait_time = 0.25
diff --git a/scenes/play.tscn b/scenes/play.tscn
index 681b29e..3d52201 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
+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"]
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/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/play.gd b/scripts/play.gd
index 08699f5..bc34f07 100644
--- a/scripts/play.gd
+++ b/scripts/play.gd
@@ -20,5 +20,27 @@ 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()})
+ 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 result = Label.new()
+ result.text = "%.3f" % get_score()
+ result.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ result.align = Label.ALIGN_RIGHT
+ _start_time = -1
+ $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(result)
+ restart.grab_focus()
+
+func _on_Button_pressed(scene):
+ GlobalScene.change_scene("res://scenes/" + scene + ".tscn",
+ {"level": GlobalScene.get_param("level")})
From 64625dfc3cf7c401c121bf290bd0307332929e96 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Sat, 8 Aug 2020 16:19:38 -0500
Subject: [PATCH 12/17] feat: add a single pointer display option
ArrayView now has the ability to display a smaller pointer to up to one
element.
---
levels/bogo_sort.gd | 2 +-
levels/bubble_sort.gd | 2 +-
levels/comparison_sort.gd | 10 ++++-----
levels/insertion_sort.gd | 4 ++--
levels/merge_sort.gd | 2 +-
levels/selection_sort.gd | 5 ++++-
models/array_model.gd | 6 ++++++
scripts/theme.gd | 9 ++++----
views/array_view.gd | 44 ++++++++++++++++++++++++++++-----------
9 files changed, 56 insertions(+), 28 deletions(-)
diff --git a/levels/bogo_sort.gd b/levels/bogo_sort.gd
index 99c749f..9655e91 100644
--- a/levels/bogo_sort.gd
+++ b/levels/bogo_sort.gd
@@ -17,5 +17,5 @@ func next(action):
if array.is_sorted():
emit_signal("done")
-func _get_effect(i):
+func get_effect(i):
return EFFECTS.NONE
diff --git a/levels/bubble_sort.gd b/levels/bubble_sort.gd
index cb612b6..9aaaa5d 100644
--- a/levels/bubble_sort.gd
+++ b/levels/bubble_sort.gd
@@ -42,7 +42,7 @@ func next(action):
_end -= 1
_swapped = false
-func _get_effect(i):
+func get_effect(i):
if i == _index or i == _index + 1:
return EFFECTS.HIGHLIGHTED
if i >= _end:
diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd
index bb604ba..a9ed43c 100644
--- a/levels/comparison_sort.gd
+++ b/levels/comparison_sort.gd
@@ -14,7 +14,7 @@ const DISABLE_TIME = 1.0
var array: ArrayModel
var active = true
-var _done = false
+var done = false
var _timer = Timer.new()
@@ -29,7 +29,7 @@ func _init(array):
func _input(event):
"""Pass input events for checking and take appropriate action."""
- if _done or not active:
+ if done or not active:
return
if event.is_pressed():
return next(event.as_text())
@@ -39,15 +39,13 @@ func next(action):
push_error("NotImplementedError")
func get_effect(i):
- if _done:
- return EFFECTS.NONE
- return _get_effect(i)
+ return get_effect(i)
func _get_effect(i):
push_error("NotImplementedError")
func _on_ComparisonSort_done():
- _done = true
+ done = true
func _on_ComparisonSort_mistake():
"""Disable the controls for one second."""
diff --git a/levels/insertion_sort.gd b/levels/insertion_sort.gd
index 0a9b576..c1eec73 100644
--- a/levels/insertion_sort.gd
+++ b/levels/insertion_sort.gd
@@ -38,10 +38,10 @@ func next(action):
return emit_signal("mistake")
_grow()
-func _get_effect(i):
+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 4019eb4..4e2d511 100644
--- a/levels/merge_sort.gd
+++ b/levels/merge_sort.gd
@@ -62,7 +62,7 @@ func next(action):
_left = _get_begin()
_right = _get_middle()
-func _get_effect(i):
+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:
diff --git a/levels/selection_sort.gd b/levels/selection_sort.gd
index 6521474..9e5f237 100644
--- a/levels/selection_sort.gd
+++ b/levels/selection_sort.gd
@@ -42,9 +42,12 @@ func next(action):
if _base == array.size - 1:
emit_signal("done")
-func _get_effect(i):
+func get_effect(i):
if i == _min or i == _index:
return EFFECTS.HIGHLIGHTED
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 d259b8c..c57ed2a 100644
--- a/models/array_model.gd
+++ b/models/array_model.gd
@@ -8,17 +8,23 @@ signal sorted(i, j)
var _array = []
var size = 0 setget , get_size
+var biggest = null
func _init(size=16):
"""Randomize the array."""
for i in range(1, size + 1):
_array.append(i)
_array.shuffle()
+ biggest = _array.max()
func at(i):
"""Retrieve the value of the element at index 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(get_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 c88ed93..a7e1993 100644
--- a/views/array_view.gd
+++ b/views/array_view.gd
@@ -11,9 +11,9 @@ var _tween = Tween.new()
var _level: ComparisonSort
var _rects = []
var _positions = []
-var _unit_width: int
-var _unit_height: int
var _viewport = Viewport.new()
+var _pointer = null
+var _pointer_size: int
onready var _separation = 128 / _level.array.size
func _init(level):
@@ -26,14 +26,15 @@ func _init(level):
func _ready():
yield(get_tree(), "idle_frame")
- _viewport.size = rect_size
- _unit_width = rect_size.x / _level.array.size
- _unit_height = rect_size.y / _level.array.size
+ 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 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:
@@ -41,23 +42,42 @@ func _ready():
accumulated -= 1
rect.polygon = [
Vector2(0, 0),
- Vector2(0, rect_size.y),
- Vector2(_unit_width - _separation, rect_size.y),
- Vector2(_unit_width - _separation, 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
+ x += unit_width
_rects.append(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):
+ 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()):
- _rects[i].color = _level.get_effect(i)
- _rects[i].scale.y = -float(_level.array.at(i)) / _level.array.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)
From bef33765072fe398d1ac42e2d56f06e62c9f1c36 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Sat, 8 Aug 2020 16:42:43 -0500
Subject: [PATCH 13/17] feat: add player-selectable array size
The player can now select the length of the level by changing it on the
level selection screen.
---
models/array_model.gd | 4 +++-
scripts/levels.gd | 10 +++++++---
scripts/menu.gd | 4 ++++
scripts/play.gd | 13 ++++++++++---
scripts/scene.gd | 4 ++--
5 files changed, 26 insertions(+), 9 deletions(-)
diff --git a/models/array_model.gd b/models/array_model.gd
index c57ed2a..bd038c8 100644
--- a/models/array_model.gd
+++ b/models/array_model.gd
@@ -6,11 +6,13 @@ signal removed(i)
signal swapped(i, j)
signal sorted(i, j)
+const DEFAULT_SIZE = 16
+
var _array = []
var size = 0 setget , get_size
var biggest = null
-func _init(size=16):
+func _init(size=DEFAULT_SIZE):
"""Randomize the array."""
for i in range(1, size + 1):
_array.append(i)
diff --git a/scripts/levels.gd b/scripts/levels.gd
index 2ae3037..2ff0df4 100644
--- a/scripts/levels.gd
+++ b/scripts/levels.gd
@@ -11,7 +11,8 @@ const MAX_WAIT = 4
const MIN_SIZE = 8
const MAX_SIZE = 128
-var _level = LEVELS[0].new(ArrayModel.new())
+var _level = LEVELS[0].new(ArrayModel.new(
+ GlobalScene.get_param("size", ArrayModel.DEFAULT_SIZE)))
func _ready():
for level in LEVELS:
@@ -54,7 +55,9 @@ func _on_Button_focus_entered(size=_level.array.size):
$Preview/Display.add_child(ArrayView.new(_level))
func _input(event):
- if event.is_action_pressed("faster"):
+ 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)
@@ -64,7 +67,8 @@ func _input(event):
_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})
+ GlobalScene.change_scene("res://scenes/play.tscn",
+ {"level": level, "size": _level.array.size})
func _get_level(name):
for level in LEVELS:
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 bc34f07..0f0c86a 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:
@@ -12,13 +14,18 @@ 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())
+ var level = _level.new(ArrayModel.new(
+ GlobalScene.get_param("size", ArrayModel.DEFAULT_SIZE)))
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)
+func _input(event):
+ if event.is_action_pressed("ui_cancel"):
+ _on_Button_pressed("levels")
+
func _on_Level_done():
var restart = Button.new()
restart.text = "RESTART LEVEL"
@@ -43,4 +50,4 @@ func _on_Level_done():
func _on_Button_pressed(scene):
GlobalScene.change_scene("res://scenes/" + scene + ".tscn",
- {"level": GlobalScene.get_param("level")})
+ {"level": _level, "size": GlobalScene.get_param("size")})
diff --git a/scripts/scene.gd b/scripts/scene.gd
index 0379e12..e855642 100644
--- a/scripts/scene.gd
+++ b/scripts/scene.gd
@@ -10,7 +10,7 @@ func change_scene(next_scene, params=null):
_params = params
get_tree().change_scene(next_scene)
-func get_param(name):
+func get_param(name, default=null):
if _params != null and _params.has(name):
return _params[name]
- return null
+ return default
From 605c62ac07889a0f73327cacd48c4287ba242248 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Mon, 10 Aug 2020 15:42:32 -0500
Subject: [PATCH 14/17] feat: add quicksort and binary tree model
The binary tree is used to bookkeep simulated recursion on subarray
bounds during quicksort. It may be removed later if I can successfully
rewrite the levels using coroutines, which I will hold off until Godot
4.0 for now because of improvements to yield/await.
---
levels/quick_sort.gd | 79 +++++++++++++++++++++++++++++++++++++
models/binary_tree_model.gd | 18 +++++++++
project.godot | 12 ++++++
scripts/levels.gd | 1 +
4 files changed, 110 insertions(+)
create mode 100644 levels/quick_sort.gd
create mode 100644 models/binary_tree_model.gd
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/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 56cf78f..bb69201 100644
--- a/project.godot
+++ b/project.godot
@@ -19,6 +19,11 @@ _global_script_classes=[ {
"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,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"
@@ -52,11 +62,13 @@ _global_script_classes=[ {
_global_script_class_icons={
"ArrayModel": "",
"ArrayView": "",
+"BinaryTreeModel": "",
"BogoSort": "",
"BubbleSort": "",
"ComparisonSort": "",
"InsertionSort": "",
"MergeSort": "",
+"QuickSort": "",
"SelectionSort": ""
}
diff --git a/scripts/levels.gd b/scripts/levels.gd
index 2ff0df4..7048c80 100644
--- a/scripts/levels.gd
+++ b/scripts/levels.gd
@@ -5,6 +5,7 @@ const LEVELS = [
InsertionSort,
SelectionSort,
MergeSort,
+ QuickSort,
]
const MIN_WAIT = 1.0 / 32 # Should be greater than maximum frame time
const MAX_WAIT = 4
From 4c9c7fe485e4f7755ebfcfc4d1dd42920fefc81a Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Tue, 11 Aug 2020 12:18:01 -0500
Subject: [PATCH 15/17] feat: add persistent high score
The save file is located in user://save.json.
---
scenes/levels.tscn | 11 +++++++++++
scripts/levels.gd | 30 +++++++++++++++++++++---------
scripts/play.gd | 9 +++++++++
scripts/scene.gd | 12 ++++++++++++
4 files changed, 53 insertions(+), 9 deletions(-)
diff --git a/scenes/levels.tscn b/scenes/levels.tscn
index 43fc2a8..52f0b2a 100644
--- a/scenes/levels.tscn
+++ b/scenes/levels.tscn
@@ -35,6 +35,17 @@ margin_top = 20.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
diff --git a/scripts/levels.gd b/scripts/levels.gd
index 7048c80..a5341d5 100644
--- a/scripts/levels.gd
+++ b/scripts/levels.gd
@@ -10,32 +10,44 @@ const LEVELS = [
const MIN_WAIT = 1.0 / 32 # Should be greater than maximum frame time
const MAX_WAIT = 4
const MIN_SIZE = 8
-const MAX_SIZE = 128
+const MAX_SIZE = 256
var _level = LEVELS[0].new(ArrayModel.new(
GlobalScene.get_param("size", ArrayModel.DEFAULT_SIZE)))
func _ready():
+ var buttons = $LevelsBorder/Levels/LevelsContainer/Buttons
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_entered")
button.connect("pressed", self, "_on_Button_pressed", [level])
- $LevelsBorder/Levels.add_child(button)
- # Autofocus last played level
- if GlobalScene.get_param("level") == level:
+ buttons.add_child(button)
+ var score = Label.new()
+ score.align = Label.ALIGN_RIGHT
+ $LevelsBorder/Levels/LevelsContainer/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_entered(size=_level.array.size):
+ # Update high scores
+ var buttons = $LevelsBorder/Levels/LevelsContainer/Buttons
+ var scores = $LevelsBorder/Levels/LevelsContainer/Scores
+ var save = GlobalScene.read_save()
+ for i in range(LEVELS.size()):
+ var name = buttons.get_child(i).text
+ if name in save and str(size) in save[name]:
+ scores.get_child(i).text = "%.3f" % save[name][str(size)]
+ else:
+ scores.get_child(i).text = "INF"
# Pause a bit to show completely sorted array
if _level.array.is_sorted():
$Timer.stop()
diff --git a/scripts/play.gd b/scripts/play.gd
index 0f0c86a..b4c3c86 100644
--- a/scripts/play.gd
+++ b/scripts/play.gd
@@ -47,6 +47,15 @@ func _on_Level_done():
$HUDBorder/HUD.add_child(back)
$HUDBorder/HUD.add_child(result)
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] = INF
+ save[name][size] = min(float(result.text), save[name][size])
+ GlobalScene.write_save(save)
func _on_Button_pressed(scene):
GlobalScene.change_scene("res://scenes/" + scene + ".tscn",
diff --git a/scripts/scene.gd b/scripts/scene.gd
index e855642..0aeacc6 100644
--- a/scripts/scene.gd
+++ b/scripts/scene.gd
@@ -14,3 +14,15 @@ func get_param(name, default=null):
if _params != null and _params.has(name):
return _params[name]
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()
From e2b0052ae6b5f02360cab7450f14f7f9345cbd58 Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Wed, 12 Aug 2020 15:30:56 -0500
Subject: [PATCH 16/17] feat: add tier system
For now it is based on the number of moves per second the player manages
to achieve. This should make unlucky shuffles a non-issue.
---
levels/comparison_sort.gd | 2 ++
project.godot | 14 ++++++++++----
scenes/levels.tscn | 2 +-
scenes/play.tscn | 4 ++--
scripts/levels.gd | 29 ++++++++++++++++++++++-------
scripts/play.gd | 29 ++++++++++++++++++-----------
scripts/scene.gd | 2 +-
scripts/score.gd | 21 +++++++++++++++++++++
8 files changed, 77 insertions(+), 26 deletions(-)
create mode 100644 scripts/score.gd
diff --git a/levels/comparison_sort.gd b/levels/comparison_sort.gd
index a9ed43c..3156df0 100644
--- a/levels/comparison_sort.gd
+++ b/levels/comparison_sort.gd
@@ -15,6 +15,7 @@ const DISABLE_TIME = 1.0
var array: ArrayModel
var active = true
var done = false
+var moves = 0
var _timer = Timer.new()
@@ -32,6 +33,7 @@ func _input(event):
if done or not active:
return
if event.is_pressed():
+ moves += 1
return next(event.as_text())
func next(action):
diff --git a/project.godot b/project.godot
index bb69201..c470a97 100644
--- a/project.godot
+++ b/project.godot
@@ -54,6 +54,11 @@ _global_script_classes=[ {
"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",
@@ -69,6 +74,7 @@ _global_script_class_icons={
"InsertionSort": "",
"MergeSort": "",
"QuickSort": "",
+"Score": "",
"SelectionSort": ""
}
@@ -137,22 +143,22 @@ ui_down={
}
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":46,"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)
]
}
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":44,"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)
]
}
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":61,"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)
]
}
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":45,"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/levels.tscn b/scenes/levels.tscn
index 52f0b2a..584fa7f 100644
--- a/scenes/levels.tscn
+++ b/scenes/levels.tscn
@@ -52,7 +52,7 @@ margin_top = 577.0
margin_right = 283.0
margin_bottom = 640.0
size_flags_vertical = 8
-text = "Use +/- and > to adjust the size and speed of the simulation, respectively."
+text = "Use the WASD keys to adjust the size and speed of the simulation."
autowrap = true
[node name="Preview" type="VBoxContainer" parent="LevelSelect"]
diff --git a/scenes/play.tscn b/scenes/play.tscn
index 3d52201..ba04115 100644
--- a/scenes/play.tscn
+++ b/scenes/play.tscn
@@ -48,14 +48,14 @@ size_flags_horizontal = 3
text = "0.000"
align = 2
-[node name="DisplayBorder" type="MarginContainer" parent="GameDisplay"]
+[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 = 287.0
margin_right = 1200.0
diff --git a/scripts/levels.gd b/scripts/levels.gd
index a5341d5..fea6ec5 100644
--- a/scripts/levels.gd
+++ b/scripts/levels.gd
@@ -12,11 +12,12 @@ const MAX_WAIT = 4
const MIN_SIZE = 8
const MAX_SIZE = 256
-var _level = LEVELS[0].new(ArrayModel.new(
+var _level = GlobalScene.get_param("level", LEVELS[0]).new(ArrayModel.new(
GlobalScene.get_param("size", ArrayModel.DEFAULT_SIZE)))
func _ready():
var buttons = $LevelsBorder/Levels/LevelsContainer/Buttons
+ var scores = $LevelsBorder/Levels/LevelsContainer/Scores
for level in LEVELS:
var button = Button.new()
button.text = level.NAME
@@ -24,9 +25,15 @@ func _ready():
button.connect("focus_entered", self, "_on_Button_focus_entered")
button.connect("pressed", self, "_on_Button_pressed", [level])
buttons.add_child(button)
- var score = Label.new()
- score.align = Label.ALIGN_RIGHT
- $LevelsBorder/Levels/LevelsContainer/Scores.add_child(score)
+ 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:
@@ -40,14 +47,22 @@ func _ready():
func _on_Button_focus_entered(size=_level.array.size):
# Update high scores
var buttons = $LevelsBorder/Levels/LevelsContainer/Buttons
- var scores = $LevelsBorder/Levels/LevelsContainer/Scores
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]:
- scores.get_child(i).text = "%.3f" % save[name][str(size)]
+ 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:
- scores.get_child(i).text = "INF"
+ 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()
diff --git a/scripts/play.gd b/scripts/play.gd
index b4c3c86..c70b21d 100644
--- a/scripts/play.gd
+++ b/scripts/play.gd
@@ -13,11 +13,11 @@ func _process(delta):
func _on_Timer_timeout():
_start_time = OS.get_ticks_msec()
- $DisplayBorder/Label.queue_free() # Delete ready text
+ $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")
- $DisplayBorder.add_child(ArrayView.new(level))
+ 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)
@@ -26,7 +26,9 @@ func _input(event):
if event.is_action_pressed("ui_cancel"):
_on_Button_pressed("levels")
-func _on_Level_done():
+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"])
@@ -35,17 +37,22 @@ func _on_Level_done():
var back = Button.new()
back.text = "BACK TO LEVEL SELECT"
back.connect("pressed", self, "_on_Button_pressed", ["levels"])
- var result = Label.new()
- result.text = "%.3f" % get_score()
- result.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- result.align = Label.ALIGN_RIGHT
+ 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(result)
+ $HUDBorder/HUD.add_child(time)
+ $HUDBorder/HUD.add_child(tier)
restart.grab_focus()
var save = GlobalScene.read_save()
var name = _level.NAME
@@ -53,8 +60,8 @@ func _on_Level_done():
if not name in save:
save[name] = {}
if not size in save[name]:
- save[name][size] = INF
- save[name][size] = min(float(result.text), save[name][size])
+ 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):
diff --git a/scripts/scene.gd b/scripts/scene.gd
index 0aeacc6..5ee37ba 100644
--- a/scripts/scene.gd
+++ b/scripts/scene.gd
@@ -11,7 +11,7 @@ func change_scene(next_scene, params=null):
get_tree().change_scene(next_scene)
func get_param(name, default=null):
- if _params != null and _params.has(name):
+ if _params != null and _params.get(name) != null:
return _params[name]
return default
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)]
From bd5b6c61ca1bf53524e2e1942f099f919726892f Mon Sep 17 00:00:00 2001
From: Daniel Ting
Date: Wed, 12 Aug 2020 17:43:36 -0500
Subject: [PATCH 17/17] chore: update README and add issue templates
---
.github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++
.github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++
CONTRIBUTING.md | 4 ++
README.md | 48 ++++++++++++++++------
assets/levels.png | Bin 237911 -> 175432 bytes
5 files changed, 97 insertions(+), 13 deletions(-)
create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md
create mode 100644 CONTRIBUTING.md
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.
+
+
+
+
+
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 b26967590d4d2bbdf526506ebb0e257469ddceef..46dddf178223227b6607c45fe21f2c4b76cdf3d5 100644
GIT binary patch
literal 175432
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 |