"Procedual" Generation Error

(Idk if this is actually procedual generation or not, or what its actually called. So im calling it this)

Godot Version

4.5

Question

What my code is essencially doing is placing puzzle pieces down with connections to the sides. Each side has one connection that can be connected onto, where not every side might have a connection. Each piece has a position and can be rotated by any form of 90 degrees. My code has allowed me to make a straight line one way on the z axis, but is not working for the x axis for whatever reason.

Code for main system:

Here is a few things before you try and debug this mess, and if you have any questions, let me know!

I do have some comments that explain what some things are doing

Anything that is commented out is either a print line I don’t want active right now, or something that I tried to impliment thats the next step of my project, before I realized this step does not even work yet.

Getting anything from the “bag” array (like in bag[n]) will be outputting 0.
Sorry my code is messy, im a novice and if you have any reccomendations to clean it up that wont make my brain explode from having to redo all of this would be appreciated!

const base_piece = preload("res://resources/scenes/piece.tscn")


var piece_number: int
var placed_tiles = []
var non_rotation_connection: bool
var placed_tile_rotation: int

const tile_index = {
	-1: "spawn",
	0: "straight",
	1: "corner",
}
const tile_definitions = {
	"spawn": {
		"connections": ["north", "east"],
		"mesh": preload("res://resources/scenes/pieces/spawnPiece.tscn"),
	},
	"straight": {
		"connections": ["north", "south"],
		"mesh": preload("res://resources/scenes/pieces/straightPiece.tscn"),
	},
	"corner": {
		"connections": ["north", "east"],
		"mesh": preload("res://resources/scenes/pieces/cornerPiece.tscn"),
	},
}

const direction_offset = {
	"north": Vector2i(-1,0),
	"east": Vector2i(0, -1),
	"south": Vector2i(1, 0),
	"west": Vector2i(0, 1)
}

const offsets_list = ["north", "east", "south", "west"]

#This is simply to take note of and copy+paste into code below (not functions that are used)
#func world_to_grid(inputX, inputZ, _grid_pos):
	#_grid_pos = Vector2i(roundi(inputX/4),roundi(inputZ/4))
#
#func grid_to_world(_output, grid_pos):
	#_output = Vector3(grid_pos.x, 1, grid_pos.y)


func place_tiles():
	#Make the original tile, the spawn tile
	piece_number += 1
	var spawn_tile = base_piece.instantiate() #Do not just move the placed_tile variable here, or it will try to reuse itself and just change each variable each passthrough, which won't work with the setup, as base_piece changes itself based off of func _ready
	spawn_tile.piece_name = "spawn"
	spawn_tile.connections = tile_definitions["spawn"]["connections"]
	spawn_tile.piece_mesh = tile_definitions["spawn"]["mesh"]
	spawn_tile.piece_number = piece_number
	add_child(spawn_tile)
	spawn_tile.piece_position = Vector2i(0,0)
	spawn_tile.name = "Tile" + str(spawn_tile.piece_position)
	spawn_tile.global_position = Vector3i(spawn_tile.piece_position.x*4, 1, spawn_tile.piece_position.y*4)
	placed_tiles.append(spawn_tile)
	#print(spawn_tile.name)
	
	
	#This is where the actual placing of the tiles happens
	for n in range(bag.size() - 1):

		#Makes a tile on the lowest tile number
		var branch_tile
		if n < placed_tiles.size():
			branch_tile = placed_tiles[n]
		else:
			push_error("Invalid access of index '"+str(n)+"' on a base object of type: 'Array'    (Basically: n >= placed_tile array size)")
			break
		
		#If your trying to place a tile off of nothing (going out of bounds of the array), then return an error. If an error is return it breaks the for loop so it does not cause a crash further down the road
		
		var actual_directions = []
		#print(branch_tile.connections)
		var open_connections = []
		for connection in branch_tile.connections:
			if !branch_tile.used_connections.has(connection):
				open_connections.append(connection)
		#Finds open connections
		for offset in open_connections:
			actual_directions.append(offsets_list[(offsets_list.find(offset)+branch_tile.piece_rotation) % 4])
			#Basically what this does it picks a direction from their direction list (that are open) and turns it into 0-3 (0 is north, east is 1, ect...), then adds 0-3 for the rotation (0 is none, 1 is turn right once, ect...) adds it together, modulo by 4 (so if you do 0%4 then you get 0/north, or 3%4 is 3/west) and you get the "actual" directions of your connections (the pieces connections with rotation accounted for)
		
		
		#Time to actually place this godamn tile
		piece_number += 1
		var placed_tile = base_piece.instantiate()
		
		placed_tile.piece_name = tile_index[bag[n]]
		placed_tile.connections = tile_definitions[tile_index[bag[n]]]["connections"]
		placed_tile.piece_mesh = tile_definitions[tile_index[bag[n]]]["mesh"]
		placed_tile.piece_number = piece_number
		
		
		var i = -1 #What i does it counting how many times the for loop has gone through so it allows the correct connection to be applied to branch_tile.used_connections
		#places a tile for each side of the branch tile
		print(str(actual_directions))
		for real_offset in actual_directions:
			print("Placing off: "+str(branch_tile.name)+", Piece Type: "+str(branch_tile.piece_name))
			i += 1
			placed_tile_rotation = 0
			var opposite_connection
			non_rotation_connection = false
			opposite_connection = find_opposite_connection(placed_tile, real_offset)
			
			#If there is an opposite connection then place the tile if not then rotate the tile until it works
			if non_rotation_connection == true:
				placed_tile.piece_position = branch_tile.piece_position + direction_offset[opposite_connection]
				print("Placed tile "+str(placed_tile)+" at position "+ str(placed_tile.piece_position))
				placed_tile.used_connections.append(offsets_list[(offsets_list.find(opposite_connection)-placed_tile_rotation) % 4])
				branch_tile.used_connections.append(open_connections[i])
				#print(str(placed_tile)+" (placed tile) has used connection(s) "+str(placed_tile.used_connections))
				#print(str(branch_tile)+" (branch tile) has used connection(s) "+str(branch_tile.used_connections))
			
			#if the placed tile does not have an opposite connection, then rotate the tile until you get an opposite connection
			else:
				push_error("No opposite connection was found for direction '"+str(open_connections[i])+"' for tile "+str(branch_tile))
		
		
		
		add_child(placed_tile)
		placed_tile.name = "Tile" + str(placed_tile.piece_position)
		placed_tiles.append(placed_tile)
		#print("piece position is: "+str(placed_tile.piece_position))



func find_opposite_connection(placed_tile, branch_tile_connection):
	for n in range(3):
		for placing_connection in placed_tile.connections:
			placing_connection = offsets_list[(offsets_list.find(placing_connection)+n) %4]
			print("possible opposite: "+str(placing_connection))
			if placing_connection == offsets_list[(offsets_list.find(branch_tile_connection)+2) % 4]:
				non_rotation_connection = true
				placed_tile_rotation = n
				placed_tile.piece_rotation = n
				print("Choosen Opposite: ", str(offsets_list[n]))
				#print(str(placing_connection)+" is the opposite of "+str(branch_tile_connection))
				#print("placing connection is: "+str(placing_connection))
				return placing_connection
				#Basically if the placed tile has a connection thats 2 rotations away (180°) then make the bool true, and it checks it for each connection of the branch tile. Once it finds one it ends the checking to ensure only one is found

Code for “Piece” (variable holder and debug printer):

extends Node3D

var piece_position: Vector2i
var piece_rotation: int
var piece_name: String
var connections: Array
var used_connections: Array
var piece_mesh: PackedScene
var piece_number: int



func _ready() -> void:
	if piece_mesh != null:
		add_child(piece_mesh.instantiate())
		rotation_degrees = Vector3i(0, piece_rotation*90, 0)
	else:
		push_error("No piece mesh found")
	global_position = Vector3i(piece_position.x*4, 1, piece_position.y*4)
	print(piece_name, str(position.x))

How did you determine that?

I don’t see bag declared or populated anywhere in the code you posted.

“Bag” is an array I’m using to contain the pieces I will be using later, it is above this section of the code. All of this code works and contains the bag variable and other elements to make a random number generator, none of which pertain to this part of the code other than what’s in bag (which I have set to be filled with 0).

I just found that I wouldn’t want to clog up the page with code that would not really be affecting the code I’m having a problem with, but if you want me to send it I can!

How is it not affecting the code when that’s literally the only thing you reported to not be working as expected. Post the entire code and clearly describe the results you’re getting versus the results you’re expecting to get.

extends Node3D

const TileDataMapConst = preload("res://resources/scripts/TileDataMap.gd")

#SEED MUST BE OVER 0 TO BE SET
@export var game_seed: int
@export var maxTiles: int

#Equation variables
const a = 0x5DEECE66D #multiplier, actually is 25,214,903,917
const c = 11 #increment, adds
const m = pow(2, 48) #modulus, uses modulus, actually is 281,474,976,710,656

#The random number itself
var random: float

#Hashing variables
const golden_ratio = 0x9e3779b9 #2654435769
var hashed: float
var shuffle_number: int = 1

#Creates a new number randomiser, coincedentally making a new seed
var rng := RandomNumberGenerator.new()



#Space for bag stuff
#All of this is used for the bag
var bag = []

var weight_dictionary := {}
var slot_dictionary := {}
var total_weight = 0

var tile_weights = []
@export var straightWeight: int
@export var cornerWeight: int
@export var weight2: int
@export var weight3: int
@export var weight4: int
@export var weight5: int
@export var weight6: int
@export var weight7: int
@export var weight8: int
@export var weight9: int
@export var weight10: int
@export var weight11: int
@export var weight12: int
@export var weight13: int
@export var weight14: int
@export var weight15: int
@export var DONT_TOUCH_WEIGHT: int = 1
#Keep DONT_TOUCH as the last variable


#Literal pieces lmao
var piece_dictionary = {}
var straight
var corner
var piece2 
var piece3 
var piece4 
var piece5 
var piece6
var piece7
var piece8 
var piece9
var piece10
var piece11
var piece12
var piece13
var piece14
var piece15
var DONT_TOUCH_PIECE
#Keep DONT_TOUCH as the last variable




#READY FUNCTION - INCLUDES CODE FROM EVERY SECTION

func _ready():
	#tile weights dictionary and keep DONT_TOUCH as the last variable
	weight_dictionary = {0: straightWeight, 1: cornerWeight, 2: weight2, 3: weight3, 4: weight4, 5: weight5, 6: weight6, 7: weight7, 8: weight8, 9: weight9, 10: weight10, 11: weight11, 12: weight12, 13: weight13, 14: weight14, 15: weight15, 16: DONT_TOUCH_WEIGHT}
	#piece dictionary and keep DONT_TOUCH as the last variable
	piece_dictionary = {0: straight, 1: corner, 2: piece2, 3: piece3, 4: piece4, 5: piece5, 6: piece6, 7: piece7, 8: piece8, 9: piece9, 10: piece10, 11: piece11, 12: piece12, 13: piece13, 14: piece14, 15: piece15, 16: DONT_TOUCH_WEIGHT}
	
	print("Weight Dictionary: ", weight_dictionary)
	
	if game_seed == 0:
		#makes the new seed randomised
		rng.randomize()
		game_seed = rng.get_seed()
	else:
		pass
		
		
	print("Starting Seed: ", game_seed)
		#adds up the total weight of all tiles
	for weight in weight_dictionary.values():
		total_weight += weight
	
	random_number(game_seed)
	createBag()
	place_tiles()







#RANDOM NUMBER GENERATORS SECTION

#Xₙ = | (a * Xₙ₋₁ + c) mod m |  (Absolute value), is the random number equation
func random_number(input_seed):
	random = abs(fmod((a *input_seed + c), m))
	#print("random: ", random)
	hashing()


func hashing():
	hashed = random + shuffle_number * golden_ratio
	@warning_ignore("narrowing_conversion")
	#print("hashed: ", hashed)






#BAG CREATION SECTION

#Creates a "bag" with random pieces (each number represents a piece)
func createBag():
	bag.clear() #Clears the "bag" to make sure its empty
	print(" . . . . . . . . . . ") #This is for styling
	print("Creating Bag")
	print(".")
	print(".")
	print(".")
	print("Max Tiles: ", maxTiles) #prints the max amount of tiles/numbers that will be in the "bag"
	
	var MaxWeight = 0
	for w in weight_dictionary.values(): #Gets the total "weight" of all the pieces
		MaxWeight += w
	print("Max Weight: ", MaxWeight)
	if (MaxWeight & (MaxWeight - 1)) == 0: #if max weight is a power of 2, subtract it by a lil bit so the entire thing does not explode
		MaxWeight = MaxWeight - 0.00001
	#Start getting tiles and putting it in "bag"
	for i in range(maxTiles, 0, -1):
		random_number(hashed)
		@warning_ignore("narrowing_conversion")
		var randomWeight: int = fmod(hashed, MaxWeight) #Gets a random number betwen 0-(max weight - 1), aka if its 0 its the top number
		#print("Rand Weight: ", randomWeight)
		
		var comulativeWeight: int = 0
		for k in weight_dictionary.keys(): #for every key in the dictionary
			comulativeWeight += weight_dictionary[k]
			#print(comulativeWeight)
			if randomWeight <= comulativeWeight:
				bag.append(k)
				#print("Choosen Tile: ", k)
				break


	print("Unshuffled Bag: ", bag)
	shuffle_bag()


#make the bag seem more random
func shuffle_bag():
	for t in range(bag.size() - 1, 0, -1): #t for tile
		random_number(hashed)
		var rand_index = fmod(hashed, t + 1)
		var temp_tile = bag[t] #puts the number thats in slot "t" into a temp var, so it cannot be lost
		bag[t] = bag[rand_index] #sets the number in slot "t" to the random number in "rand_index"'s slot
		bag[rand_index] = temp_tile #set "rand_index"'s slot to "t"'s slot's original number so "rand" is not cloned, and "t" is not lost
	print("Shuffled Bag: ", bag)



Output:
Godot Engine v4.5.1.stable.official.f62fdbde1 - https://godotengine.org
Metal 3.2 - Forward+ - Using Device #0: Apple - Apple M2 (Apple8)

Weight Dictionary: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0, 15: 0, 16: 1 }
Starting Seed: -7324905537206413209
. . . . . . . . . .
Creating Bag
.
.
.
Max Tiles: 0
Max Weight: 1
Unshuffled Bag:
Shuffled Bag:
spawn0.0
Weight Dictionary: { 0: 1, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0, 15: 0, 16: 1 }
Starting Seed: 1
. . . . . . . . . .
Creating Bag
.
.
.
Max Tiles: 100
Max Weight: 2
Unshuffled Bag: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Shuffled Bag: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
spawn0.0
[“north”, “east”]
Placing off: Tile(0, 0), Piece Type: spawn
possible opposite: north
possible opposite: south
Choosen Opposite: north
Placed tile Base_Piece:<Node3D#39409681955> at position (1, 0)
Placing off: Tile(0, 0), Piece Type: spawn
possible opposite: north
possible opposite: south
possible opposite: east
possible opposite: west
Choosen Opposite: east
Placed tile Base_Piece:<Node3D#39409681955> at position (0, 1)
straight0.0
[“east”]
Placing off: Tile(0, 1), Piece Type: straight
possible opposite: north
possible opposite: south
possible opposite: east
possible opposite: west
Choosen Opposite: east
Placed tile Base_Piece:<Node3D#39560676908> at position (0, 2)
straight0.0
[“east”]
Placing off: Tile(0, 2), Piece Type: straight
possible opposite: north
possible opposite: south
possible opposite: east
possible opposite: west
Choosen Opposite: east
Placed tile Base_Piece:<Node3D#39711671861> at position (0, 3)
straight0.0
[“east”]
Placing off: Tile(0, 3), Piece Type: straight
possible opposite: north
possible opposite: south
possible opposite: east
possible opposite: west
Choosen Opposite: east
Placed tile Base_Piece:<Node3D#39862666814> at position (0, 4)
straight0.0
[“east”]
Placing off: Tile(0, 4), Piece Type: straight
possible opposite: north
possible opposite: south
possible opposite: east
possible opposite: west
Choosen Opposite: east
Placed tile Base_Piece:<Node3D#40013661767> at position (0, 5)
straight0.0
[“east”]
Placing off: Tile(0, 5), Piece Type: straight
possible opposite: north
possible opposite: south
possible opposite: east
possible opposite: west
Choosen Opposite: east
(repeat another 95 times or something)

Anyways, I don’t know why the start happens twice, and sorry that the printing for the output has really confusing wording (I didn’t really know what to put there so I put some close enough sounding stuff)