Hello again! This is post 2 on the development progress of my Saoirse (pronounced “Seer-Sha”) game.

Here’s a quick summary of what I’ve done since the last post:

1. I've changed how the registry stores objects. Previously, it would store a pair consisting of an identifier and the object itself. Now, the object part has been replaced with a function that returns that object.

2. I've been debugging some "invalid value" checks in the BlankInvalidOrValue and Identifier classes.

3. I've added some basic item registry objects to the main registry of the game.

4. I've added classes for items, entities, and blocks.

I’m still trying to fix those checks for invalid values in the two classes mentioned above.

I think I’ve reduced the problem a bit, but it’s hard to tell.

Currently, it looks like it gets stuck in a infinite loop where it tries to check if a value is invalid by creating an instance of an invalid value for comparison, which then gets checked, and so on.

Here’s the current code:

# This file is:
# src/python/saoirse_server.py

from dataclasses import dataclass

@dataclass(frozen=True)
class Logger():
    log_level_info="info"
    log_level_warn="warn"
    log_level_error="error"

    def log_msg(sender="Nobody", msg="This is a test message, please ignore it!", level=log_level_info, should_print=True):
        formatted_msg = f"Log: Sender = {sender}: Level = {level}: Message =\n{msg}\n"
        if should_print:
            print(formatted_msg)
        return formatted_msg


class VarHolder():
    def __init__(self):
        pass


class BlankInvalidOrValue():
    def __init__(self, value_in="__blank__", check_valid_in=True):
        self.set_value(value_in, check_valid_in)

    def __eq__(self, other_in):
        return self.is_equal(other_in)

    def is_equal(self, other_in):
        return isinstance(other_in, type(self)) and other_in.get_value() == self.get_value()

    def copy(self):
        return BlankInvalidOrValue(self.get_value(), False)

    def set_value_invalid(self):
        self.value = BlankInvalidOrValue.invalid_str()

    def get_value(self):
        return self.value

    def is_value_acceptable(self, value_in):
        return True

    def is_value_valid(self, value_in):
        if isinstance(value_in, str) and value_in == BlankInvalidOrValue.invalid_str():
            return False
        return self.is_value_acceptable(value_in)

    def set_value(self, new_value_in, check_valid=True):
        if self.is_value_valid(new_value_in) or not check_valid:
            self.value = new_value_in
        else:
            self.set_value_invalid()

    def blank_str():
        return "__blank__"

    def invalid_str():
        return "__invalid__"

    def blank():
        return BlankInvalidOrValue(BlankInvalidOrValue.blank_str(), False)

    def invalid():
        return BlankInvalidOrValue(BlankInvalidOrValue.invalid_str(), False)

    def is_blank(self):
        return self.is_equal(BlankInvalidOrValue.blank())

    def is_invalid(self):
        return self.is_equal(BlankInvalidOrValue.invalid())


class Identifier(BlankInvalidOrValue):
    def __init__(self, path_in="__blank__", delimiter_in="/", validate_path=True):
        super().__init__(path_in)
        self.set_delimiter(delimiter_in)
        self.set_path(path_in, validate_path)

    def is_value_valid(self, value_in):
        return isinstance(value_in, list) and all(isinstance(part, str) and part not in Identifier.invalid().get_path() for part in value_in)

    def set_path_invalid(self):
        self.value = Identifier.invalid().get_path()

    def set_path(self, new_path_in, update_self=True, validate=True):
        if isinstance(new_path_in, list):
            new_path = new_path_in
            valid = True
            if validate:
                valid = Identifier.validate_path(new_path)
            if valid:
                for i, part in enumerate(new_path):
                    if isinstance(part, str):
                        if self.get_delimiter() in part:
                            fixed = part.split(self.get_delimiter())
                            new_path.pop(i)
                            fixed.reverse()
                            for fixed_part in fixed:
                                new_path.insert(i, fixed_part)
                    else:
                        new_path = Identifier.invalid().get_path()
            else:
                new_path = Identifier.invalid().get_path()
        elif isinstance(new_path_in, str):
            new_path = new_path_in.split(self.get_delimiter())
        else:
            new_path = Identifier.invalid().get_path()

        if update_self:
            self.set_value(new_path)
            return self
        return Identifier(new_path)

    def set_delimiter(self, new_delimiter_in, update_self=True):
        if isinstance(new_delimiter_in, str):
            new_delimiter = new_delimiter_in
        else:
            new_delimiter = "/"

        if update_self:
            self.delimiter = new_delimiter
            return self
        return Identifier.blank().set_delimiter(new_delimiter, True)

    def get_path(self):
        return self.get_value()

    def get_delimiter(self):
        return self.delimiter

    def get_path_str(self):
        if self.validate_own_path():
            return self.get_delimiter().join(self.get_path())
        return Identifier.invalid_str()

    def is_equal(self, other_in):
        return isinstance(other_in, Identifier) and other_in.get_path() == self.get_path()

    def append(self, other_path_in, update_self=True):
        if isinstance(other_path_in, list):
            new_path = self.path.copy()
            new_path.extend(other_path_in)
            if update_self:
                self.set_path(new_path)
                return self
            return Identifier(new_path)

    def extend(self, other_in, update_self=True):
        if isinstance(other_in, Identifier):
            if not self.is_invalid() and not other_in.is_invalid():
                new_path = self.path.copy()
                new_path.extend(other_in.get_path())
                if update_self:
                    self.set_path(new_path)
                    return self
                return Identifier(new_path)
        if update_self:
            self.set_path_invalid()
            return self
        return Identifier.invalid()

    def copy(self):
        return Identifier(self.get_path(), self.get_delimiter())

    def blank():
        return Identifier(Identifier.blank_str())

    def invalid():
        return Identifier(Identifier.invalid_str(), False)

    def is_blank(self):
        return self.is_equal(Identifier.blank())

    def is_invalid(self):
        return self.is_equal(Identifier.invalid())

    def get_id_from_str_or_id(ide):
        if isinstance(ide, str):
            return Identifier(ide)
        elif isinstance(ide, Identifier):
            return ide
        else:
            return Identifier.invalid()


class IdentifierObjGetter(BlankInvalidOrValue):
    def __init__(self, id_in=Identifier.blank(), obj_getter_in=None):
        self.set_id(id_in)

        super().__init__(self.id)

        self.obj_getter = obj_getter_in

    def is_equal(self, other_in):
        return isinstance(other_in, IdentifierObjGetter) and self.get_id().is_equal(other_in.get_id()) and self.get_obj() == other_in.get_obj()

    def copy(self):
        return IdentifierObjGetter(self.get_obj(), self.get_id().copy())

    def set_id(self, id_in):
        self.id = Identifier.get_id_from_str_or_id(id_in)

    def get_id(self):
        if isinstance(self.id, Identifier):
            return self.id
        return Identifier.invalid()

    def get_obj(self):
        if callable(self.obj_getter):
            return self.obj_getter()
        return None

    def blank():
        return IdentifierObjGetter(None, Identifier.blank)

    def invalid():
        return IdentifierObjGetter(None, Identifier.invalid)

    def __str__(self):
        return f"{self.id.get_path_str()} : {self.obj_getter}"


class BaseRegistry():
    def __init__(self):
        self.entries = {}

    def get_entries(self):
        return self.entries

    def get_entry(self, id_in):
        ide = Identifier.get_id_from_str_or_id(id_in)
        if ide.is_invalid():
            return IdentifierObjGetter.invalid()

        if self.contains_id(ide):
            return self.entries[ide.get_path_str()]
        return IdentifierObjGetter.blank()

    def contains_id(self, id_in):
        ide = Identifier.get_id_from_str_or_id(id_in)
        if ide.is_invalid():
            return False
        return ide.get_path_str() in self.get_entries().keys()

    def register_id_obj(self, id_in, obj_getter_in):
        if isinstance(id_in, Identifier):
            self.register_id_obj_pair(IdentifierObjGetter(id_in, obj_getter_in))

    def register_id_obj_pair(self, id_obj_pair_in):
        if isinstance(id_obj_pair_in, IdentifierObjGetter):
            ide_invalid = True
            ide = id_obj_pair_in.get_id()
            if isinstance(ide, Identifier):
                if not ide.is_invalid():
                    ide_invalid = False
                    id_str = ide.get_path_str()
                    if id_str in self.get_entries().keys():
                        Logger.log_msg(sender="Registry", msg=f"Failed to register {id_obj_pair_in} as its id of {id_str} is alread registered!", level=Logger.log_level_warn)
                    else:
                        self.entries[id_str] = id_obj_pair_in
            if ide_invalid:
                Logger.log_msg(sender="Registry", msg=f"Failed to register {id_obj_pair_in} as its id of {ide} is invalid!", level=Logger.log_level_warn)
        else:
            Logger.log_msg(sender="Registry", msg=f"Failed to register {id_obj_pair_in} as it is invalid!", level=Logger.log_level_warn)


class BaseCategorizedRegistry(BaseRegistry):
    def __init__(self):
        super().__init__()

    def register_category(self, category_id_in):
        category_id = Identifier.get_id_from_str_or_id(category_id_in)
        if not category_id.is_invalid():
            self.register_id_obj(category_id, BaseRegistry())

    def get_categories(self):
        return super().get_entries()

    def get_entries(self):
        entries_out = {}
        for key in self.get_categories().keys():
            category = self.get_categories()[key]
            if isinstance(category, BaseRegistry):
                for entry in category.get_entries():
                    if isinstance(entry, IdentifierObjGetter):
                        categorized_entry = entry.copy()
                        entry_id = categorized_entry.get_id().copy()
                        entry_id_path = entry_id.get_path()
                        entry_id_path.insert(0, key)
                        categorized_entry.set_id(Identifier(entry_id_path, entry_id.get_delimiter()))
                        entries_out[categorized_entry.get_id().get_path_str()] = categorized_entry
        return entries_out

    def get_category(self, category_id_in):
        category_id = Identifier.get_id_from_str_or_id(category_id_in)
        if not category_id.is_invalid():
            if self.contains_id(category_id):
                return self.categories[category_id_in]
        return Identifier.invalid()

    def register_id_obj_getter_under_category(self, id_obj_pair_in, category_id_in):
        if isinstance(id_obj_pair_in, IdentifierObjGetter):
            if category_id_in not in self.get_categories().keys():
                self.register_category(category_id_in)
            self.register_id_obj_pair(IdentifierObjGetter(id_obj_pair_in, Identifier([category_id_in].extend(id_obj_pair_in.get_id().get_path()))))

    def register_id_obj_under_category(self, id_in, category_id_in, obj_getter_in):
        self.register_id_obj_getter_under_category(IdentifierObjGetter(id_in, obj_getter_in), category_id_in)

    def get_id_obj_pair_under_category(self, category_id_in, obj_id_in):
        category_id = Identifier.get_id_from_str_or_id(category_id_in)
        if category_id.is_invalid():
            return IdentifierObjGetter.invalid()

        obj_id = Identifier.get_id_from_str_or_id(obj_id_in)
        if obj_id.is_invalid():
            return IdentifierObjGetter.invalid()

        return self.get_entry(category_id.extend(obj_id_in, False))


class ThreeDimensionalPosition():
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

    def get_origin():
        return ThreeDimensionalPosition(0, 0, 0)

    def get_x(self):
        return self.x

    def get_y(self):
        return self.y

    def get_z(self):
        return self.z

    @dataclass(frozen=True)
    class Directions():
        up="up"
        down="down"
        north="north"
        south="south"
        east="east"
        west="west"

    def offset(self, direction, distance):
        if direction == ThreeDimensionalPosition.Directions.up:
            return ThreeDimensionalPosition(self.get_x(), self.get_y() + distance, self.get_z())

        if direction == ThreeDimensionalPosition.Directions.down:
            return ThreeDimensionalPosition(self.get_x(), self.get_y() - distance, self.get_z())

        if direction == ThreeDimensionalPosition.Directions.north:
            return ThreeDimensionalPosition(self.get_x() + distance, self.get_y(), self.get_z())

        if direction == ThreeDimensionalPosition.Directions.south:
            return ThreeDimensionalPosition(self.get_x() - distance, self.get_y(), self.get_z())

        if direction == ThreeDimensionalPosition.Directions.east:
            return ThreeDimensionalPosition(self.get_x(), self.get_y(), self.get_z() + distance)

        if direction == ThreeDimensionalPosition.Directions.west:
            return ThreeDimensionalPosition(self.get_x(), self.get_y(), self.get_z() - distance)

    def get_dict(self):
        return {"x": self.get_x(), "y": self.get_y(), "z": self.get_z()}

    def get_str(self):
        return str(self.get_dict())

    def from_dict(pos_dict):
        return ThreeDimensionalPosition(pos_dict.get("x"), pos_dict.get("y"), pos_dict.get("z"))

    def from_str(pos_str):
        return ThreeDimensionalPosition.from_dict(dict(pos_str))

    def __str__(self):
        return self.get_str()

    def __eq__(self, other):
        return self.get_x() == other.get_x() and self.get_y() == other.get_y() and self.get_z() == other.get_z()


class GameObject():
    def __init__(self, ide, dimension=None):
        self.ide = ide
        self.dimension = dimension

    def get_id(self):
        return self.ide

    def get_dimension(self):
        return self.dimension

    def tick(self):
        pass


class SpaceGameObject(GameObject):
    def __init__(self, ide, pos=ThreeDimensionalPosition.get_origin(), dimension=None):
        super().__init__(ide, dimension=dimension)
        self.pos = pos

    def set_pos(self, pos):
        self.pos = pos

    def get_pos(self):
        return self.pos

    def has_gravity(self):
        return True


class Tile(SpaceGameObject):
    def __init__(self, ide, pos=ThreeDimensionalPosition.get_origin(), dimension=None):
        super().__init__(ide, pos, dimension)


class Entity(SpaceGameObject):
    def __init__(self, ide, pos=ThreeDimensionalPosition.get_origin(), dimension=None):
        super().__init__(ide, pos, dimension)


class Item(GameObject):
    def __init__(self, ide, dimension=None):
        super().__init__(ide, dimension)


class ThreeDimensionalSpace():
    def __init__(self, dimension):
        self.tiles = []
        self.gravity_speed = 1
        self.dimension = dimension

    def get_tiles(self):
        return self.tiles

    def get_gravity_speed(self):
        return self.gravity_speed

    def get_dimension(self):
        return self.dimension

    def add_tile_at_pos(self, pos, tile):
        tile.set_pos(pos)
        tile.set_world(self.get_dimension())
        self.tiles.append(tile)

    def remove_tile_at_pos(self, pos, check_tile=None):
        for tile in self.get_tiles_at_pos(pos, check_tile):
            self.tiles.remove(tile)

    def set_tile_at_pos(self, pos, tile):
        self.remove_tile_at_pos(pos, tile)
        self.add_tile_at_pos(pos, tile)

    def get_tiles_at_pos(self, pos, check_tile=None):
        tiles = []
        for tile in self.get_tiles():
            if tile.get_pos() == pos and (check_tile == tile or check_tile is None):
                tiles.append(tile)
        return tiles

    def tick_tile_gravity(self, tile):
        if tile.has_gravity():
            tile.set_pos(tile.get_pos().offset(ThreeDimensionalPosition.Directions.down, self.get_gravity_speed()))

    def tick(self):
        for tile in self.get_tiles():
            tile.tick()
            self.tick_tile_gravity(tile)
# This file is:
# src/python/saoirse_server.py

from saoirse_base import VarHolder, BlankInvalidOrValue, Identifier, IdentifierObjGetter, BaseCategorizedRegistry, Logger, GameObject, Item, Tile, Entity
from dataclasses import dataclass

saoirse_id = "saoirse"

class SaoirseRegistry(BaseCategorizedRegistry):
    def __init__(self):
        super().__init__()

        self.category_tags = self.SaoirseRegistryCategories()

        self.items = VarHolder()
        self.items.hi = ""

        self.register_items()
        self.register_tiles()
        self.register_entities()

    def register_game_obj_under_category(self, ide, category_ide, game_obj_getter):
        if isinstance(game_obj_getter, GameObject):
            self.register_id_obj_under_category(ide, category_ide, game_obj_getter)
        return ide

    def register_items(self):
        self.register_category(self.category_tags.items)

        for ide_str in ["dirt", "stone", "gravel", "oak_log"]:
            ide = Identifier([saoirse_id, ide_str])
            self.register_item(ide, lambda: Item(ide))

    def register_item(self, ide, item_obj_getter=None):
        if item_obj_getter is None:
            item_obj_getter = lambda: Item(ide)
        self.register_id_obj_under_category(ide, self.category_tags.items, item_obj_getter)

    def register_tiles(self):
        self.register_category(self.category_tags.tiles)

    def register_tile(self, ide, tile_obj):
        self.register_id_obj_under_category(ide, self.category_tags.tiles, tile_obj)

    def register_entities(self):
        self.register_category(self.category_tags.entities)

    def register_entity(self, ide, entity_obj):
        self.register_id_obj_under_category(ide, self.category_tags.entities, entity_obj)

    @dataclass(frozen=True)
    class SaoirseRegistryCategories():
        items = "items"
        tiles = "tiles"
        entities = "entities"


registry = SaoirseRegistry()

Logger.log_msg(saoirse_id, registry.get_categories())
Logger.log_msg(saoirse_id, registry.get_entries())

class SaoriseDimension():
    def __init__(self):
        pass