It's much nicer to code with now

This commit is contained in:
2026-01-08 20:11:10 +11:00
parent 60f4fe7e91
commit ba5ad9d22a
9 changed files with 191 additions and 135 deletions

97
Set.gd
View File

@@ -1,4 +1,5 @@
class_name Set extends RefCounted
## Adapted from: https://gist.github.com/NoodleSushi/3eb9cc08eb1d1c369bef308262804f1e
## A custom implementation of a set data structure in GDScript.
##
## Usage Example:
@@ -48,47 +49,47 @@ func duplicate() -> Set:
# O(min(|self|, |set|))
## Returns a new [Set] containing elements that are present in the current set
## but not in the provided [param set].
func difference(set: Set) -> Set:
if set.hash() == self.hash():
func difference(param: Set) -> Set:
if param.hash() == self.hash():
return Set.new()
var out := self.duplicate()
if set.size() > self.size():
if param.size() > self.size():
for element in self.elements():
if set.has(element):
if param.has(element):
out.erase(element)
else:
for element in set.elements():
for element in param.elements():
out.erase(element)
return out
# O(min(|self|, |set|))
## Modifies the current [Set] to contain elements present in the current set\
## but not in the provided [param set].
func difference_update(set: Set) -> void:
if set.hash() == self.hash():
func difference_update(param: Set) -> void:
if param.hash() == self.hash():
self.clear()
return
if set.size() > self.size():
if param.size() > self.size():
for element in self.elements():
if set.has(element):
if param.has(element):
self.erase(element)
else:
for element in set.elements():
for element in param.elements():
self.erase(element)
# O(min(|self|, |set|))
## Returns a new [Set] containing elements common to both the current set and
## the provided [param set].
func intersection(set: Set) -> Set:
if set.hash() == self.hash():
func intersection(param: Set) -> Set:
if param.hash() == self.hash():
return duplicate()
var out := Set.new()
if set.size() > self.size():
if param.size() > self.size():
for element in self.elements():
if set.has(element):
if param.has(element):
out.add(element)
else:
for element in set.elements():
for element in param.elements():
if self.has(element):
out.add(element)
return out
@@ -96,16 +97,16 @@ func intersection(set: Set) -> Set:
# O(min(|self|, |set|))
## Modifies the current set to contain only elements common to both the
## current set and the provided [param set].
func intersection_update(set: Set) -> void:
if set.hash() == self.hash():
func intersection_update(param: Set) -> void:
if param.hash() == self.hash():
return
var out := Set.new()
if set.size() > self.size():
if param.size() > self.size():
for element in self.elements():
if set.has(element):
if param.has(element):
out.add(element)
else:
for element in set.elements():
for element in param.elements():
if self.has(element):
out.add(element)
self.clear()
@@ -114,13 +115,13 @@ func intersection_update(set: Set) -> void:
# O([1, min(|self|, |set|)])
## Returns [code]true[/code] if the sets have no elements in common;
## otherwise, returns [code]false[/code].
func isdisjoint(set: Set) -> bool:
if set.size() > self.size():
func isdisjoint(param: Set) -> bool:
if param.size() > self.size():
for element in self.elements():
if set.has(element):
if param.has(element):
return false
else:
for element in set.elements():
for element in param.elements():
if self.has(element):
return false
return true
@@ -128,23 +129,23 @@ func isdisjoint(set: Set) -> bool:
# O([1, |self|])
## Returns [code]true[/code] if every element of the current set is present
## in the provided [param set]; otherwise, returns [code]false[/code].
func issubset(set: Set) -> bool:
if set.size() < self.size():
func issubset(param: Set) -> bool:
if param.size() < self.size():
return false
else:
for element in self.elements():
if !set.has(element):
if !param.has(element):
return false
return true
# O([1, |set|])
## Returns [code]true[/code] if every element of the provided [param set]
## is present in the current set; otherwise, returns [code]false[/code].
func issuperset(set: Set) -> bool:
if self.size() < set.size():
func issuperset(param: Set) -> bool:
if self.size() < param.size():
return false
else:
for element in set.elements():
for element in param.elements():
if !self.has(element):
return false
return true
@@ -164,18 +165,18 @@ func erase(element: Variant) -> void:
# O(min(|self|, |set|))
## Returns a new [Set] containing elements that are present in either
## the current set or the provided [param set], but not in both.
func symmetric_difference(set: Set) -> Set:
if set.hash() == self.hash():
func symmetric_difference(param: Set) -> Set:
if param.hash() == self.hash():
return Set.new()
if set.size() > self.size():
var out := set.duplicate()
if param.size() > self.size():
var out := param.duplicate()
for element in self.elements():
if set.has(element):
if param.has(element):
out.remove(element)
return out
else:
var out := self.duplicate()
for element in set.elements():
for element in param.elements():
if self.has(element):
out.remove(element)
return out
@@ -183,9 +184,9 @@ func symmetric_difference(set: Set) -> Set:
# O(min(|self|, |set|))
## Modifies the current set to contain elements that are present in either
## the current set or the provided [param set], but not in both.
func symmetric_difference_update(set: Set) -> void:
if set.size() > self.size():
var temp := set.duplicate()
func symmetric_difference_update(param: Set) -> void:
if param.size() > self.size():
var temp := param.duplicate()
for element in self.elements():
if temp.has(element):
temp.erase(element)
@@ -193,7 +194,7 @@ func symmetric_difference_update(set: Set) -> void:
temp.add(element)
self._set = temp._set
else:
for element in set.elements():
for element in param.elements():
if self.has(element):
self.erase(element)
else:
@@ -202,30 +203,30 @@ func symmetric_difference_update(set: Set) -> void:
# O(min(|self|, |set|))
## Returns a new [Set] containing all elements from both the current
## set and the provided [param set].
func union(set: Set) -> Set:
if set.size() > self.size():
var out := set.duplicate()
func union(param: Set) -> Set:
if param.size() > self.size():
var out := param.duplicate()
for element in self.elements():
out.add(element)
return out
else:
var out := self.duplicate()
for element in set.elements():
for element in param.elements():
out.add(element)
return out
# O(min(|self|, |set|))
## Modifies the current set to contain all elements from both the current set
## and the provided [param set].
func update(set: Set) -> void:
if set.size() > self.size():
var temp := set.duplicate()
func update(param: Set) -> void:
if param.size() > self.size():
var temp := param.duplicate()
for element in self.elements():
temp.add(element)
self._dict = temp._dict
else:
var temp := self.duplicate()
for element in set.elements():
for element in param.elements():
temp.add(element)
self._dict = temp.dict

View File

@@ -3,11 +3,6 @@ extends Node
#Master list of chunks
var _chunks: Array[Chunk] = []
#Create some test samples - these samples must ALWAYS be 9-elements long, 5th element is the result
#var _samples: Array[PackedInt32Array] = [
#[1,1,1, 1,1,1, 1,1,1],
#]
@onready
var wfc: Node = get_node("../Generator")
@@ -15,73 +10,14 @@ var wfc: Node = get_node("../Generator")
var map: TileMapLayer = get_node("../TileMapLayer")
func _ready() -> void:
var ruleset: PackedInt32Array = read_sample_ruleset("sample2.png")
var samples: Array[PackedInt32Array] = parse_samples_from_ruleset(ruleset)
var samples: Array[PackedInt32Array] = Rulesets.load_samples("sample3.png")
var c: Chunk = wfc.generate_chunk_at(0,0,_chunks,samples)
#TODO: build a visual layout for the results
#TODO: handle the chunk-edges
#TODO: handle the chunk-edges, non-zero chunk coords
#print(ruleset)
#print(samples)
print(c.data)
print(c.data) #debugging
draw_map_data(c)
## Read the png file, and parse it to a useable ruleset
func read_sample_ruleset(filename: String) -> PackedInt32Array:
var img: Image = Image.load_from_file(filename)
var png: PackedByteArray = img.get_data()
var hex: PackedInt32Array = []
@warning_ignore("integer_division")
var size: int = (png.size() / 3)
hex.resize(size)
#print(png)
for i in range(size): #the file is assumed to be in RGB format
hex[i] = (png[i * 3] << 16) | (png[i * 3 + 1] << 8) | (png[i * 3 + 2] << 0)
#print(i, "(", hex[i], "): ", png[i * 3], ",", png[i * 3 + 1], ",", png[i * 3 + 2])
return hex
func parse_samples_from_ruleset(ruleset: PackedInt32Array) -> Array[PackedInt32Array]:
#for now, assume the ruleset is 8x8
const RULESET_WIDTH: int = 8
const RULESET_HEIGHT: int = 8
#using a custom container type
var samples: Set = Set.new()
for x in range(1, RULESET_WIDTH-1):
for y in range(1, RULESET_HEIGHT-1):
var sample: PackedInt32Array = [
ruleset[(y -1) * RULESET_WIDTH + (x -1)],
ruleset[(y -1) * RULESET_WIDTH + (x )],
ruleset[(y -1) * RULESET_WIDTH + (x +1)],
ruleset[(y ) * RULESET_WIDTH + (x -1)],
ruleset[(y ) * RULESET_WIDTH + (x )],
ruleset[(y ) * RULESET_WIDTH + (x +1)],
ruleset[(y +1) * RULESET_WIDTH + (x -1)],
ruleset[(y +1) * RULESET_WIDTH + (x )],
ruleset[(y +1) * RULESET_WIDTH + (x +1)],
]
samples.add(sample)
var result: Array[PackedInt32Array] = []
result.assign(samples.elements())
return result
func draw_map_data(chunk: Chunk) -> void:
#hacky
var fix = func(x) -> int:
match x:
0x000000: return 0
0xFF0000: return 1
0x00FF00: return 2
0x0000FF: return 3
_: return -1
for x in range(Chunk.CHUNK_WIDTH):
for y in range(Chunk.CHUNK_HEIGHT):
map.set_cell(Vector2i(x, y), fix.call(chunk.data[y * Chunk.CHUNK_WIDTH + x]), Vector2i.ZERO)
map.set_cell(Vector2i(x, y), chunk.data[y * Chunk.CHUNK_WIDTH + x], Vector2i.ZERO)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
sample3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

40
sample3.png.import Normal file
View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cvusc1g2mcgj5"
path="res://.godot/imported/sample3.png-40d6657997a818597ed152abe7d0ca8d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sample3.png"
dest_files=["res://.godot/imported/sample3.png-40d6657997a818597ed152abe7d0ca8d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -29,10 +29,10 @@ texture_region_size = Vector2i(32, 32)
[sub_resource type="TileSet" id="TileSet_gu47o"]
tile_size = Vector2i(32, 32)
sources/0 = SubResource("TileSetAtlasSource_5juve")
sources/1 = SubResource("TileSetAtlasSource_fy5k1")
sources/2 = SubResource("TileSetAtlasSource_5c1cw")
sources/3 = SubResource("TileSetAtlasSource_akxrc")
sources/4 = SubResource("TileSetAtlasSource_akxrc")
sources/3 = SubResource("TileSetAtlasSource_5c1cw")
sources/2 = SubResource("TileSetAtlasSource_fy5k1")
sources/1 = SubResource("TileSetAtlasSource_5juve")
[node name="Scene" type="Node2D"]

View File

@@ -12,20 +12,26 @@ func _reset_entropy() -> void:
entropy_total.clear()
entropy_total.resize(Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT)
entropy_total.fill(-1)
func _update_entropy(chunk: Chunk, samples: Array[PackedInt32Array]) -> void:
#iterate over indeterminate tiles to find their valid neighbours
for x in range(Chunk.CHUNK_WIDTH):
for y in range((Chunk.CHUNK_HEIGHT)):
if entropy_total[y * Chunk.CHUNK_WIDTH + x] < 0: #There's some excess multiplication in this code, but I want to get it right first
entropy_valid[y * Chunk.CHUNK_WIDTH + x] = _find_valid_samples_at(chunk, x, y, samples) #this overrides any pre-existing cached data
entropy_total[y * Chunk.CHUNK_WIDTH + x] = entropy_valid[y * Chunk.CHUNK_WIDTH + x].size() #the entropy is stored separately or faster processing
var index: int = y * Chunk.CHUNK_WIDTH + x #reduce number crunching
if chunk.data[index] > 0: #if the tile is already set, ignore
entropy_total[index] = 0
if entropy_total[index] < 0:
entropy_valid[index] = _find_valid_samples_at(chunk, x, y, samples) #this overrides any pre-existing cached data
entropy_total[index] = entropy_valid[index].size() #the entropy is stored separately for faster processing
func _clear_entropy_at(index: int) -> void:
func _zero_entropy_at(chunk: Chunk, index: int) -> void:
var clear_at := func (dx: int, dy: int) -> void:
var check = index + (dy * Chunk.CHUNK_WIDTH) + (dx)
if check >= 0 and check < Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT:
if entropy_total[check] > 0: entropy_total[check] = -1 #unset surrounding tiles
var idx = index + (dy * Chunk.CHUNK_WIDTH) + (dx)
if idx >= 0 and idx < Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT:
if entropy_total[idx] > 0 and chunk.data[idx] <= 0: entropy_total[idx] = -1 #unset surrounding tiles
clear_at.call(-1, -1)
clear_at.call( 0, -1)
clear_at.call( 1, -1)
@@ -40,16 +46,18 @@ func _clear_entropy_at(index: int) -> void:
func generate_chunk_at(_x: int, _y: int, chunk_array: Array[Chunk], samples: Array[PackedInt32Array]) -> Chunk:
var chunk: Chunk = Chunk.new(_x, _y)
_reset_entropy()
#chunk.data[0] = 1 #DEBUG: seed
_update_entropy(chunk, samples)
while true:
while true: #TODO: would a floodfill approach work better?
var index = _find_lowest_entropy_tile_index()
if index < 0: break #none found, finished
print(index, ":", entropy_total[index])
var s: PackedInt32Array = entropy_valid[index].pick_random()
chunk.data[index] = s[4]
_clear_entropy_at(index)
_zero_entropy_at(chunk, index)
_update_entropy(chunk, samples)
chunk_array.append(chunk)
@@ -59,18 +67,25 @@ func generate_chunk_at(_x: int, _y: int, chunk_array: Array[Chunk], samples: Arr
func _find_lowest_entropy_tile_index() -> int:
#find the lowest-entropy tile
var lowest: int = -1
var lowest_list: Array[int] #smooth out order bias
for i in range(Chunk.CHUNK_WIDTH * Chunk.CHUNK_HEIGHT):
if lowest < 0:
if entropy_total[i] <= 0: continue #no options
lowest = i
lowest_list = [i]
continue
if entropy_total[i] > 0 and entropy_total[i] < entropy_total[lowest]:
lowest = i
#actually update the lowest found
if entropy_total[i] > 0:
if entropy_total[i] < entropy_total[lowest]:
lowest = i
lowest_list = [i]
elif entropy_total[i] == entropy_total[lowest]:
lowest_list.append(i)
#finished
return lowest
return lowest_list.pick_random() if lowest_list.size() > 0 else lowest
func _find_valid_samples_at(chunk: Chunk, tile_x: int, tile_y: int, _samples: Array[PackedInt32Array]) -> Array[PackedInt32Array]:
var valid: Array[PackedInt32Array] = []

63
wfc/rulesets.gd Normal file
View File

@@ -0,0 +1,63 @@
class_name Rulesets extends Object
## These rulesets are loaded from a file and return samples for the generator
## Different rulesets can have different file dimensions
var _size: Vector2i = Vector2i.ZERO
static func load_samples(filename: String) -> Array[PackedInt32Array]:
var obj: Rulesets = Rulesets.new()
var ruleset: PackedInt32Array = obj._load_ruleset_file(filename)
var samples: Array[PackedInt32Array] = obj._parse_ruleset_to_samples(ruleset)
return samples
## Read the png file, and parse it to a useable ruleset
func _load_ruleset_file(filename: String) -> PackedInt32Array:
var img: Image = Image.load_from_file(filename)
img.decompress()
_size = img.get_size()
var png: PackedByteArray = img.get_data()
var hex: PackedInt32Array = []
@warning_ignore("integer_division")
var hex_size: int = (png.size() / 3)
hex.resize(hex_size)
for i in range(hex_size): #using RGB8 format
var rgb8: int = (png[i * 3] << 16) | (png[i * 3 + 1] << 8) | (png[i * 3 + 2] << 0)
match rgb8: #BUGFIX: remapped RGB to values the tilemap can handle
0x000000: hex[i] = 1
0xFF0000: hex[i] = 2
0x00FF00: hex[i] = 3
0x0000FF: hex[i] = 4
_: hex[i] = 1
return hex
## Convert the raw hexcodes to usable WFC samples
func _parse_ruleset_to_samples(ruleset: PackedInt32Array) -> Array[PackedInt32Array]:
#wrapped in a custom container type
var samples: Set = Set.new()
for x in range(1, _size.x-1):
for y in range(1, _size.y-1):
var sample: PackedInt32Array = [
ruleset[(y -1) * _size.x + (x -1)],
ruleset[(y -1) * _size.x + (x )],
ruleset[(y -1) * _size.x + (x +1)],
ruleset[(y ) * _size.x + (x -1)],
ruleset[(y ) * _size.x + (x )],
ruleset[(y ) * _size.x + (x +1)],
ruleset[(y +1) * _size.x + (x -1)],
ruleset[(y +1) * _size.x + (x )],
ruleset[(y +1) * _size.x + (x +1)],
]
samples.add(sample)
var result: Array[PackedInt32Array] = []
result.assign(samples.elements())
return result

1
wfc/rulesets.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://cb6akqmmarmxa