Using an alphanumeric hash to generate a grid-based level?

Godot Version

4.3

Question

I’m still fairly new to Godot, but I think I have a decent handle of the bare basics. I’ve been working on a 2D, turn-based, grid-based puzzle game, in which the goal is to move the player character to the level exit while avoiding the enemy.

Levels are grid-based, and consist of the player, enemy, walls, exit, and empty squares. I think I’m probably going to use a dictionary to store the levels, and a couple for loops to instantiate everything.

What I want to know is, is it possible to have some sort of alphanumeric hash to represent the levels? Like, the idea would be that I can store the levels in the code as a hash, and make a function that will parse the hash and populate the dictionary, which then gets put into the level instantiate function when the level loads.

This could also open the ability for users to input a hash in-game to generate levels. As well as creating a level editing tool for users, or collaborators, to create levels and generate a level hash, which could then be coded in the game (in the case of collaborators), or shared by other users.

This idea seems more than a few degrees above my skill level to be able to figure out myself. So I thought I’d ask here to see if anyone has ideas of how to go about doing it.

Yes there is a global hash function that operates on most types including strings. The Noise resource can be used to get 2d noise based on this hash as it’s seed.

I did a quick look at the docs for the hash functions, and it seems like they only return a hash. Perhaps me using the term “hash” caused a little confusion, but I don’t know what else to call it. I think serialization might be closer to the concept I’m looking for, but I have no idea how to do that. Although, it’s more than possible that I misunderstood what you meant. If that’s the case, then I’ll ask for you to please dumb it down a bit more. I know enough to do the basics, but some of the intermediate and pretty much all advance topics are over my head. And this is definitely far over my head, but I wanna learn how to do it anyway.

Either way, I kind of want to re-explain my idea to try and clarify it, just in case. The first part of my idea is that I could have a menu in my game where the user can input a hash (for lack of a better term). And just for example say it’s 10-characters long, made up of numbers, lower-case, and capital letters (I don’t know if that would be too short or not), and the game would be able to convert that hash into a playable level. Behind the scenes, the game code would call a function that takes the inputted hash, parses it, and populates the level dictionary.

The second part of the idea involves another mode in-game, or a separate tool, that allows the user to create their own levels. And that tool could generate the serialized hash for the created custom level that can then be shared with other users. I could also have collaborators create levels using the tool for me to add to the game.

It could also let me just store levels in the code as these serialized hashes. Makes it a bit easier, I think.

These are all ideas, without any real idea of how to do them, at all. Grand ideas and no ability to see them through. Dreaming far above my current skill level, in other words.

Close to the correct term, I’d call that the seed, or user input seed for extra clarity. Since the user input is a String type and the Noise’s seed is a int, a conversion must take place. This is where the hash function comes in. It can convert any value into a int where the same input yields the same result.

A sample of this code may look like this

func _on_line_edit_enter_pressed(user_input_seed: String) -> void:
    var hash_value: int = hash(user_input_seed)
    var noise := FastNoiseLite.new()
    noise.seed = hash_value

    print("Seeded with value: ", hash_value)
    print("Getting position 10, 15: ", noise.get_noise_2d(10, 15))

What you want is strictly serialization, hash is a very fast one-way operation, you cannot take a hash_value and get the inputed string out of it. To hammer this home: a very simple and flawed hash function would take every letter in the string and add them up. “abc” would hash to 1+2+3 for a final value of 6, but af also results in 6, this is called a hash collision where multiple inputs result in the same hash_value.

Back to serialization, if you want a string-based representation of your level I recommend focusing on making a binary save format, using methods like var_to_bytes into a PackedByteArray. Using the PackeByteArray’s .compress() function, then finally converting this binary data to a somewhat user friendly string through Marshalls.raw_to_base64. And like any saving and loading system a chiral function for loading.

Here is a sample using PackedByteArray and raw_to_base64

var block1_data := PackedByteArray()
block1_data.append_array(var_to_bytes(self.transform))
var block1_compress := block1_data.compress()
var block1_base64: String = Marshalls.raw_to_base64(block1_compress)
print("Save data: ", block1_base64)

This sample printed as follows, the amount of A characters must mean the compressed data contains a lot of zeros.

Save data: AhIAACAAAoA/AOAEAOAMDwjAAAAAAAAAwEA=

Apologies for the delayed response. I’m awful with procrastination.

Thanks for the responses. So, serialization is definitely what I want. And I kind of get the idea, but some of it is still a bit over my head. It at least points me in the right direction. Looking up tutorials and such might help fill in the gaps.

I think I still need to find out how to do the serialization on a dictionary. One where the keys are the x/y coordinates and the values are the objects at those coordinates. As well as reversing the whole process.

extends Node

Enum for tile types

enum TileType {
EMPTY = 0,
WALL = 1,
PLAYER = 2,
ENEMY = 3,
EXIT = 4
}

Character mapping for encoding/decoding

const CHAR_MAP = {
TileType.EMPTY: “0”,
TileType.WALL: “W”,
TileType.PLAYER: “P”,
TileType.ENEMY: “E”,
TileType.EXIT: “X”
}

Reverse mapping for decoding

var REVERSE_CHAR_MAP = {}

func _ready():
# Initialize reverse mapping
for key in CHAR_MAP:
REVERSE_CHAR_MAP[CHAR_MAP[key]] = key

func encode_level(level_data: Dictionary) → String:
var width = level_data.size
var height = level_data.size_y
var tiles = level_data.tiles

# First part of hash: width and height
var hash = "%02d%02d" % [width, height]

# Add separator
hash += "-"

# Encode tiles
for y in range(height):
    for x in range(width):
        var pos = Vector2(x, y)
        var tile_type = tiles.get(pos, TileType.EMPTY)
        hash += CHAR_MAP[tile_type]

return hash

func decode_level(level_hash: String) → Dictionary:
# Split hash into metadata and tile data
var parts = level_hash.split(“-”)
if parts.size() != 2:
push_error(“Invalid level hash format”)
return {}

# Parse width and height
var width = int(parts[0].substr(0, 2))
var height = int(parts[0].substr(2, 2))
var tile_data = parts[1]

# Validate tile data length
if tile_data.length() != width * height:
    push_error("Invalid tile data length")
    return {}

# Create level dictionary
var level = {
    "size": width,
    "size_y": height,
    "tiles": {}
}

# Decode tiles
var idx = 0
for y in range(height):
    for x in range(width):
        var char = tile_data[idx]
        if char in REVERSE_CHAR_MAP:
            var tile_type = REVERSE_CHAR_MAP[char]
            if tile_type != TileType.EMPTY:
                level.tiles[Vector2(x, y)] = tile_type
        idx += 1

return level

Example usage function

func create_example_level() → Dictionary:
return {
“size”: 5,
“size_y”: 5,
“tiles”: {
Vector2(0, 0): TileType.WALL,
Vector2(2, 2): TileType.PLAYER,
Vector2(4, 4): TileType.EXIT,
Vector2(3, 3): TileType.ENEMY
}
}

Test functions

func test_encoding():
var level = create_example_level()
var hash = encode_level(level)
print("Level hash: ", hash)

var decoded = decode_level(hash)
print("Decoded level: ", decoded)

Serializing a dictionary is mostly handled by the engine with var_to_bytes. The tricky part is serializing the objects, you will have to extract key information about each object down to base types. You already mention using position/coordinates (Vector2) as keys, could you store the type of tille as a enum, like in redflares sample?

Oh yeah, that was the plan. I’m really sorry if I’m not explaining things super clearly.

So, this is kind of an attempt to “port” an old game of mine to Godot. The original project only ever got to the “proof of concept” phase (because I’m awful at level design), but I wrote it as a college project in 2005. Written in C#, and using GDI+ for the graphics.

And in that, I used a two-dimensional array. The array indices were the x/y coordinates, and I used numbers to represent the objects. So like, 0 was empty, 1 was wall, and so on. Then the draw function would cycle through the array and draw the level based on the numbers in the array.

I’ve since learned that dictionaries are probably a better solution, especially in Godot. And so yeah, the key would be the coordinates, and I was gonna use an int or a string as the value to represent the object, then have a function that reads through the dictionary and matches the value with the object to set up the level. Thankfully, in Godot, I can use other features/functions to handle movement, collision, etc. rather than manipulating the dictionary (the array in the original code) directly. So I’ll only need it to set up the level.

But thank you both for the help. I really appreciate it.