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"]