From ba5ad9d22a56da7533c866e6a80630d9fca7762c Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Thu, 8 Jan 2026 20:11:10 +1100 Subject: [PATCH] It's much nicer to code with now --- Set.gd | 97 ++++++++++++++++++++++---------------------- client.gd | 72 ++------------------------------ sample2.png | Bin 4236 -> 4460 bytes sample3.png | Bin 0 -> 618 bytes sample3.png.import | 40 ++++++++++++++++++ scene.tscn | 8 ++-- wfc/generator.gd | 45 +++++++++++++------- wfc/rulesets.gd | 63 ++++++++++++++++++++++++++++ wfc/rulesets.gd.uid | 1 + 9 files changed, 191 insertions(+), 135 deletions(-) create mode 100644 sample3.png create mode 100644 sample3.png.import create mode 100644 wfc/rulesets.gd create mode 100644 wfc/rulesets.gd.uid diff --git a/Set.gd b/Set.gd index fd9b5c2..ca6da7c 100644 --- a/Set.gd +++ b/Set.gd @@ -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 diff --git a/client.gd b/client.gd index 5ec51bb..71f3599 100644 --- a/client.gd +++ b/client.gd @@ -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) diff --git a/sample2.png b/sample2.png index ed2261f544874364a6a5ed78803895810933745c..62704bee64a82be427b9f026fb9771d43325622a 100644 GIT binary patch delta 792 zcmV+z1LypVA?zZMM}Hs;{Pz?c!LNWBhcv0w4LW`em?%^|$;a772R59a_X{2kObWVX zOC=?AXwkyeh2cQ$OsJhB!6}wHTn+Y;_$ec`6!&MHaB-sQYP%WPGaCEaC>s9uV-ycb z`w_c1!l;Ci@MusN)G-#OV);J8e&Pxn0gWVg@5|&j*X-LnHdQYQ^C=|gETZEB(jtLW z7u4CfsnT_0mN}9y5Z%%qZHF25>< zUJ*dp{YGVG8FP}9hHrh{Q#aLJoM-v>{aO8L!D2u_Bu+5Hw23!}XEtqv^FDEum1ULq zoOr^d0}?-SUGeyhbIE0aXGYCzW}Y}oES9=h>0(wkHR5UFn5yZNFXTK{Id5@))~a>Z zJ}19nq@b@XbDh=*QdqVW z;qJNp+tZ%k4>j;|jMwiVm3LGF$Jj$H90pnGd46iHaRzwMAyo(o7vJ6dEdT%jHc3Q5 zR4C75U>J0O5sCeu7?n(<+Q5P?@ShRGXcU`CwgJ0_|BP5{qLv@9IGZvXXde%Q5C8xL Wg9jI}*#}Ah0000R{gkS2|~3xhyE7(ous1Wqy4%cntJVviiDY2H8U2$K_bSM_3K_iU_lqe=SPmk~wa zG11Ul8nrZHiUy`Zk8goCTdrSNcbH*u${?|M?4Nw`?QPZHVO64?k-?c+VgMdT3wIZI zu+dQBTG*T`Ynu5&{_W{Rc>~w8SrxXA&Dj6|0flKpLr_UWLm+T+Z)Rz1WdHzpoPCkK zOT$nU#ZObkA{7TKh&W`ZPIj@A8v!$a3y=5Xyn7Ds-3JKGD$}f<1fc1*nMozZTz*yT zdqn^t2#8}?W|lE0Non}j*FAMp-Nks8f8U=qpcX6!1VrKlGfbO!gLr1sHaPDSM_5@_ ziO-2AO}ZfQBi9v=-#C|C7I+gXKNMXQ(jUy3G}`=&c`^=w+plyj`Mx&IIR;P_zYa>9e=F}%zl#I=xEU+U~n6_ zxbA4m9&ot>j64~#DZ5gTmQXA=0`F(^O?hDG7U*B|=GH#P=>w3ZS*34)gF|4VMA_>; z?+$g(?cbjE{C)s&=5nUbxT+rj01bGvfdYyGBsDW-I5;yjHZ5Z_VKXf>H#B4|Vly*1 zEi__cI59Y8I5}ovGm|d`sS7kTF*7zXGcYnZHIw57bO$s#G&njnHj`ckCkHe-G&njn zHnWTd5dpJ+2w(^nG%ue900006VoOIv00000008+zyML4R5Fu6rFgSMIqb~pe05?fQ zK~yNuV_={)V1zOL6RDVqR2x`Oc>fu37>%%>LK`rwVZ`tn$u`hl0}I|Dp@t2#k7?Qi Y0H@Cf9me5}TmS$707*qoM6N<$g4!QCJOBUy diff --git a/sample3.png b/sample3.png new file mode 100644 index 0000000000000000000000000000000000000000..e258df8c57c6ea06794b28592946f047a111a23c GIT binary patch literal 618 zcmV-w0+s!VP)EX>4Tx04R}tk-ba9P!z>aQ^g`J4t5Z6$WWc^q9Wo{t5Adrp;l<0iR+(n?Bmhmf z%}gpO=JKmz-zx$L(Km~T%q(M0lG5<4uY2mIx{LEH|Gqyfq82O$1VrKlGfbO!gLrz= zHaPDSM_E}`iO-40O*$a)Bi9v=-#C|C7IvQY>U>Kk4HicKs5$6mnI- z$gzMLG{~+W{0)B3)+$a;cuC`FmeLa_+EpV2erfx%m#f6bd)dmpC{K$d2ez5xyn zfw2;0ulw9P)IGOdsK~yNuy^=8w03ZkhTf_gqTxSs)S!9wn;DG|eV9MofAWRRX9&8Hog#-Yq@pWUJ z;+hV 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] = [] diff --git a/wfc/rulesets.gd b/wfc/rulesets.gd new file mode 100644 index 0000000..72e7a2d --- /dev/null +++ b/wfc/rulesets.gd @@ -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 diff --git a/wfc/rulesets.gd.uid b/wfc/rulesets.gd.uid new file mode 100644 index 0000000..3589207 --- /dev/null +++ b/wfc/rulesets.gd.uid @@ -0,0 +1 @@ +uid://cb6akqmmarmxa