From b7d46e256730d3cc1b2aa12c6a450bb5de4f8226 Mon Sep 17 00:00:00 2001
From: Ryan Reed <git@ryanreed.net>
Date: Thu, 17 Aug 2023 19:01:51 -0400
Subject: [PATCH] Added detection of stalled evolutions

---
 world.gd   | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++----
 world.tscn | 16 +++++++++++++
 2 files changed, 78 insertions(+), 5 deletions(-)

diff --git a/world.gd b/world.gd
index e5eae74..588d07f 100644
--- a/world.gd
+++ b/world.gd
@@ -27,25 +27,37 @@ enum CellStates {
 @onready var generation_cell_size_y: LineEdit = $UI/WorldGeneration/VBoxContainer/CellSize/Input_y
 @onready var start_paused_button: CheckBox = $UI/WorldGeneration/VBoxContainer/FinalRow/StartPausedButton
 
+@onready var messages_label: Label = $UI/Messages/Label
+
 ## Other
 @onready var camera: Camera2D = $Camera2D
 @onready var generation_timer: Timer = $GenerationTimer
 
-@export var world_seed: int
-@export var cell_size: Vector2 = Vector2(16, 16)
-@export var cell_texture: Texture2D
-@export var world_size: Vector2 = Vector2(32, 32)
+@export var world_seed: int ## The seed utilized for generation of the world
+@export var cell_size: Vector2 = Vector2(16, 16) ## Size of each cell image
+@export var cell_texture: Texture2D ## Testure for each cell
+@export var world_size: Vector2 = Vector2(32, 32) ## The size of the world
 
 @export var zoom_increment: Vector2 = Vector2(0.1, 0.1)
 
 var cell_instance
+var evolution_is_stalled: bool = false
 var generation: int = 1
-var is_paused: int = false
+var is_paused: bool = false
 var total_living: int = 0
 
 var cell_ids: Array = [] # Store cell RIDs
 var cell_states: Array = []
 
+var previous_generation_hashes: Array = [] # Store the generation related to the hashes
+var previous_generation_count: int = 10 # Number of generations to check for evolution stalling
+
+# Number of collisions to consider evolution stalled
+#  The higher the number, the less likely to catch oscillators
+#  Consider the previous_generation_count as this may need to increase for larger counts
+#  4 Collisions out of 10 previous_generation_count seems reasonable although it may not be quite enough
+var collision_count_limit: int = 4
+
 
 func _ready() -> void:
 	update_generation_ui()
@@ -97,6 +109,7 @@ func start_conway() -> void:
 	generate_world()
 
 	generation = 1
+	evolution_is_stalled = false
 	debug_world_seed.text = "World Seeed: %s" % world_seed
 	debug_generation_counter.text = "Generation: %s" % generation
 	debug_living_cells_counter.text = "Living Cells: %s" % total_living
@@ -147,8 +160,32 @@ func count_living_neighbors(pos: Vector2) -> int:
 				count += 1
 	return count
 
+func end_world_generation() -> void:
+	# Determine which generation first stalled
+	var current_world_hash: int = cell_states.hash()
+	var colliding = previous_generation_hashes.filter(func(gen): return gen.hash == current_world_hash)
+	var stalled_generation = colliding[0].generation
+
+	messages_label.text = "Detected Stalled Evolution Starting at Generation %s" % stalled_generation
+	messages_label.visible = true
+	evolution_is_stalled = true
+
+## Get the number of previous generation results that collide with the current hash
+func get_colliding_hash_count(input_hash: int) -> int:
+	var count = 0
+	for gen in previous_generation_hashes:
+		count += int(gen.hash == input_hash)
+	return count
+
 ## Loop through world to generate cell states, hide/show cells depending on state
 func process_generation() -> void:
+	if evolution_is_stalled: return
+
+	update_previous_generation_hashes()
+
+	if previous_generation_hashes[previous_generation_hashes.size()-1].collisions >= collision_count_limit:
+		return end_world_generation()
+
 	generation += 1
 	total_living = 0
 
@@ -167,6 +204,23 @@ func process_generation() -> void:
 
 	cell_states = new_states
 
+## Update the previous run hash and generation ids
+##  We only store the last `previous_generation_count` number of results
+func update_previous_generation_hashes() -> void:
+	if previous_generation_hashes.size() < previous_generation_count:
+		previous_generation_hashes.resize(previous_generation_count)
+		previous_generation_hashes.fill({"generation": 0, "hash": 0, "collisions": 0})
+
+	# Remove the oldest generation information
+	previous_generation_hashes.remove_at(0)
+
+	var generation_hash := cell_states.hash()
+	previous_generation_hashes.append({
+		"generation": generation,
+		"hash": generation_hash,
+		"collisions": get_colliding_hash_count(generation_hash),
+	})
+
 ## Toggle Pause UI and Start/Stop Generation Timer
 func toggle_pause() -> void:
 	is_paused = !is_paused
@@ -217,6 +271,8 @@ func generate_world() -> void:
 			cell_states[x][y] = is_alive
 			cell_ids[x][y] = create_cell(Vector2(x, y), bool(is_alive))
 
+	update_previous_generation_hashes()
+
 
 func _on_generation_timer_timeout() -> void:
 	process_generation()
@@ -240,6 +296,7 @@ func _on_run_button_pressed() -> void:
 	debug_ui.visible = true
 	generation_ui.visible = false
 	background_ui.visible = false
+	messages_label.visible = false
 	seed(world_seed)
 
 	start_conway()
diff --git a/world.tscn b/world.tscn
index 848c3bf..00b61d6 100644
--- a/world.tscn
+++ b/world.tscn
@@ -196,6 +196,22 @@ layout_direction = 3
 layout_mode = 2
 text = "Quit"
 
+[node name="Messages" type="MarginContainer" parent="UI"]
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -20.0
+offset_top = -40.0
+offset_right = 20.0
+grow_horizontal = 2
+grow_vertical = 0
+
+[node name="Label" type="Label" parent="UI/Messages"]
+visible = false
+layout_mode = 2
+
 [connection signal="timeout" from="GenerationTimer" to="." method="_on_generation_timer_timeout"]
 [connection signal="pressed" from="UI/WorldGeneration/VBoxContainer/Seed/Generate" to="." method="_on_world_seed_generate_pressed"]
 [connection signal="toggled" from="UI/WorldGeneration/VBoxContainer/FinalRow/StartPausedButton" to="." method="_on_start_paused_button_toggled"]