|
|
@ -0,0 +1,308 @@ |
|
|
|
## Conway's Game of Life |
|
|
|
## Rule 1 - Any live cell with fewer than two live neighbours dies (underpopulation) |
|
|
|
## Rule 2 - Any live cell with two or three live neighbours lives |
|
|
|
## Rule 3 - Any live cell with more than three live neighbours dies (overpopulation) |
|
|
|
## Rule 4 - Any dead cell with exactly three live neighbours becomes a live cell (reproduction) |
|
|
|
extends Node2D |
|
|
|
|
|
|
|
|
|
|
|
enum CellStates { |
|
|
|
DEAD, |
|
|
|
ALIVE, |
|
|
|
} |
|
|
|
|
|
|
|
## UI |
|
|
|
@onready var background_ui: Control = $UI/Background |
|
|
|
|
|
|
|
@onready var debug_ui: MarginContainer = $UI/Debug |
|
|
|
@onready var debug_generation_counter: Label = $UI/Debug/VBoxContainer/GenerationCounter |
|
|
|
@onready var debug_living_cells_counter: Label = $UI/Debug/VBoxContainer/LivingCellsCounter |
|
|
|
@onready var debug_world_seed: Label = $UI/Debug/VBoxContainer/WorldSeed |
|
|
|
|
|
|
|
@onready var generation_ui: MarginContainer = $UI/WorldGeneration |
|
|
|
@onready var generation_seed: LineEdit = $UI/WorldGeneration/VBoxContainer/Seed/Input |
|
|
|
@onready var generation_speed: HSlider = $UI/WorldGeneration/VBoxContainer/GenerationSpeed/Input |
|
|
|
@onready var generation_world_size_x: LineEdit = $UI/WorldGeneration/VBoxContainer/WorldSize/Input_x |
|
|
|
@onready var generation_world_size_y: LineEdit = $UI/WorldGeneration/VBoxContainer/WorldSize/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 = $Camera |
|
|
|
@onready var generation_timer: Timer = $GenerationTimer |
|
|
|
|
|
|
|
@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 |
|
|
|
|
|
|
|
var cell_instance |
|
|
|
var evolution_is_stalled: bool = false |
|
|
|
var generation: int = 1 |
|
|
|
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 |
|
|
|
|
|
|
|
var timer_speeds: Array = [0.1, 0.08, 0.05, 0.03, 0.01] # The speeds for the generation_timer for selection from the UI |
|
|
|
|
|
|
|
|
|
|
|
func _ready() -> void: |
|
|
|
update_generation_ui() |
|
|
|
debug_ui.visible = false |
|
|
|
background_ui.visible = true |
|
|
|
generation_ui.visible = true |
|
|
|
|
|
|
|
func _input(event: InputEvent) -> void: |
|
|
|
if event.is_action_pressed("Pause"): |
|
|
|
toggle_pause() |
|
|
|
if is_paused and event.is_action_pressed("Next Generation"): |
|
|
|
process_generation() |
|
|
|
debug_generation_counter.text = "Generation: %s" % generation |
|
|
|
debug_living_cells_counter.text = "Living Cells: %s" % total_living |
|
|
|
if event.is_action_pressed("Menu"): |
|
|
|
toggle_pause() |
|
|
|
update_generation_ui() |
|
|
|
generation_ui.visible = is_paused |
|
|
|
|
|
|
|
|
|
|
|
# |
|
|
|
# UI |
|
|
|
# |
|
|
|
## Update the Generation UI with the current settings |
|
|
|
func update_generation_ui() -> void: |
|
|
|
if world_seed: |
|
|
|
generation_seed.text = str(world_seed) |
|
|
|
else: |
|
|
|
generation_seed.text = str(randi()) |
|
|
|
generation_world_size_x.text = str(world_size.x) |
|
|
|
generation_world_size_y.text = str(world_size.y) |
|
|
|
start_paused_button.button_pressed = false |
|
|
|
|
|
|
|
|
|
|
|
# |
|
|
|
# Conway Specific |
|
|
|
# |
|
|
|
func start_conway() -> void: |
|
|
|
clear_world() |
|
|
|
generate_world() |
|
|
|
update_previous_generation_hashes() |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
# Center the camera on the world |
|
|
|
camera.position = Vector2( |
|
|
|
world_size.x * cell_size.x / 2, |
|
|
|
world_size.y * cell_size.y / 2 |
|
|
|
) |
|
|
|
camera.zoom = Vector2.ONE |
|
|
|
|
|
|
|
if not is_paused: generation_timer.start() |
|
|
|
|
|
|
|
|
|
|
|
## Check a cell against the Conway rules and return True if cell should be alive |
|
|
|
## The logic in this could be cleaned up pretty easily. Only verbose for understanding. |
|
|
|
func cell_is_alive(pos: Vector2) -> bool: |
|
|
|
var neighbors := count_living_neighbors(pos) |
|
|
|
var currently_alive = cell_states[pos.x][pos.y] == CellStates.ALIVE |
|
|
|
|
|
|
|
# Rule 1 - Any live cell with fewer than two live neighbours dies (underpopulation) |
|
|
|
if currently_alive and neighbors < 2: |
|
|
|
return false |
|
|
|
# Rule 2 - Any live cell with two or three live neighbours lives |
|
|
|
elif currently_alive and (neighbors == 2 or neighbors == 3): |
|
|
|
return true |
|
|
|
# Rule 3 - Any live cell with more than three live neighbours dies (overpopulation) |
|
|
|
elif currently_alive and neighbors > 3: |
|
|
|
return false |
|
|
|
# Rule 4 - Any dead cell with exactly three live neighbours becomes a live cell (reproduction) |
|
|
|
elif not currently_alive and neighbors == 3: |
|
|
|
return true |
|
|
|
|
|
|
|
return false |
|
|
|
|
|
|
|
## Count all neighbors surrounding a cell |
|
|
|
func count_living_neighbors(pos: Vector2) -> int: |
|
|
|
var count := 0 |
|
|
|
|
|
|
|
@warning_ignore("incompatible_ternary") |
|
|
|
var x_min = 0 if pos.x - 1 < 0 else pos.x - 1 |
|
|
|
@warning_ignore("incompatible_ternary") |
|
|
|
var y_min = 0 if pos.y - 1 < 0 else pos.y - 1 |
|
|
|
var x_max = world_size.x if pos.x + 2 > world_size.x else pos.x + 2 |
|
|
|
var y_max = world_size.y if pos.y + 2 > world_size.y else pos.y + 2 |
|
|
|
|
|
|
|
for x in range(x_min, x_max): |
|
|
|
for y in range(y_min, y_max): |
|
|
|
if x == pos.x and y == pos.y: continue # Current cell - Don't count |
|
|
|
if cell_states[x][y] == CellStates.ALIVE: |
|
|
|
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 |
|
|
|
|
|
|
|
var rs := RenderingServer |
|
|
|
|
|
|
|
var new_states: Array= [] |
|
|
|
for x in range(world_size.x): |
|
|
|
new_states.append([]) |
|
|
|
new_states[x].resize(world_size.x) |
|
|
|
for y in range(world_size.y): |
|
|
|
var pos = Vector2(x, y) |
|
|
|
var is_alive = cell_is_alive(pos) |
|
|
|
total_living += int(is_alive) |
|
|
|
new_states[x][y] = int(is_alive) |
|
|
|
rs.canvas_item_set_visible(cell_ids[x][y], is_alive) |
|
|
|
|
|
|
|
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 |
|
|
|
background_ui.visible = is_paused |
|
|
|
|
|
|
|
if is_paused: generation_timer.stop() |
|
|
|
else: generation_timer.start() |
|
|
|
|
|
|
|
|
|
|
|
## Loop over cell_ids and free the resource in the RenderingServer |
|
|
|
## Clear cell_ids and cell_states |
|
|
|
func clear_world() -> void: |
|
|
|
for row in cell_ids: |
|
|
|
for id in row: |
|
|
|
RenderingServer.free_rid(id) |
|
|
|
cell_ids = [] |
|
|
|
cell_states = [] |
|
|
|
|
|
|
|
## Create the cell using the rendering server |
|
|
|
## This is only performed on initial world generation |
|
|
|
func create_cell(pos: Vector2, visible_cell: bool = true) -> RID: |
|
|
|
var rs = RenderingServer |
|
|
|
|
|
|
|
cell_instance = rs.canvas_item_create() |
|
|
|
rs.canvas_item_set_parent(cell_instance, get_canvas_item()) |
|
|
|
|
|
|
|
var rect: Rect2 = Rect2(-cell_size.x/2, -cell_size.y/2, cell_size.x, cell_size.y) # Centered with cell size |
|
|
|
rs.canvas_item_add_texture_rect(cell_instance, rect, cell_texture) |
|
|
|
|
|
|
|
var pos_fixed = Vector2(pos.x * cell_size.x, pos.y * cell_size.y) |
|
|
|
var trans = Transform2D(0, pos_fixed) |
|
|
|
rs.canvas_item_set_transform(cell_instance, trans) |
|
|
|
|
|
|
|
rs.canvas_item_set_visible(cell_instance, visible_cell) |
|
|
|
|
|
|
|
return cell_instance |
|
|
|
|
|
|
|
## Generate the world with cells in random states (alive or dead) |
|
|
|
func generate_world() -> void: |
|
|
|
for x in range(world_size.x): |
|
|
|
cell_ids.append([]) |
|
|
|
cell_ids[x].resize(world_size.x) |
|
|
|
cell_states.append([]) |
|
|
|
cell_states[x].resize(world_size.x) |
|
|
|
for y in range(world_size.y): |
|
|
|
var is_alive: int = randi_range(CellStates.DEAD, CellStates.ALIVE) |
|
|
|
total_living += is_alive |
|
|
|
cell_states[x][y] = is_alive |
|
|
|
cell_ids[x][y] = create_cell(Vector2(x, y), bool(is_alive)) |
|
|
|
|
|
|
|
|
|
|
|
func _on_generation_timer_timeout() -> void: |
|
|
|
process_generation() |
|
|
|
|
|
|
|
debug_generation_counter.text = "Generation: %s" % generation |
|
|
|
debug_living_cells_counter.text = "Living Cells: %s" % total_living |
|
|
|
|
|
|
|
generation_timer.start() |
|
|
|
|
|
|
|
func _on_generation_speed_drag_ended(value_changed: bool) -> void: |
|
|
|
var speed = generation_speed.value - 1 |
|
|
|
if value_changed: |
|
|
|
generation_timer.wait_time = timer_speeds[speed] |
|
|
|
|
|
|
|
func _on_quit_button_pressed() -> void: |
|
|
|
get_tree().quit() |
|
|
|
|
|
|
|
func _on_run_button_pressed() -> void: |
|
|
|
if generation_seed.text.strip_edges(): |
|
|
|
world_seed = int(generation_seed.text) |
|
|
|
else: |
|
|
|
world_seed = randi() |
|
|
|
world_size = Vector2(int(generation_world_size_x.text), int(generation_world_size_y.text)) |
|
|
|
|
|
|
|
debug_ui.visible = true |
|
|
|
generation_ui.visible = false |
|
|
|
background_ui.visible = false |
|
|
|
messages_label.visible = false |
|
|
|
seed(world_seed) |
|
|
|
|
|
|
|
start_conway() |
|
|
|
|
|
|
|
func _on_start_paused_button_toggled(button_pressed: bool) -> void: |
|
|
|
is_paused = button_pressed |
|
|
|
|
|
|
|
func _on_world_seed_generate_pressed() -> void: |
|
|
|
generation_seed.text = str(randi()) |
|
|
|
|
|
|
|
|
|
|
|
func _exit_tree() -> void: |
|
|
|
clear_world() |