Object Oriented Programming (OOP) useful examples for Ren'Py items

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
User avatar
jeffster
Miko-Class Veteran
Posts: 877
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Object Oriented Programming (OOP) useful examples for Ren'Py items

#1 Post by jeffster »

V.1.2. (History & Changelog are in the next post).

Here are a few classes which can be used to easily manage any items in any kind of collection
(e.g. characters in a list of characters; personal attributes for a person...),
but originally it was for items in inventory, hence the main class name is Inventory.

This is a good example of OOP applied for Ren'Py script.

(I hope it will show newcomers how useful can be OOP, even some exotic methods like making an object callable, iterable, returning a "nonexistent" attribute etc.
For more details about OOP see e.g. a tutorial on magic methods).

Save the code below as "objects_ren.py" file into "game" directory of your Ren'Py project.

Then use it as it's shown in the second code block (an example of "scripts.rpy", that creates an inventory screen etc).

BTW you can play with objects_ren.py file in the interactive Python interpreter, launching it from command line:
python -i objects_ren.py

"Usage Cheat Sheet" see here:

objects_ren.py

Code: Select all

"""renpy
init -5 python:
"""

""" Inventory and items - v.1.2

Usage Cheat Sheet:

default inv = Inventory()           # Initialize the collection of items

inv("Apple", "is sweet.")           # Add a new item to the inventory
                                    # => inv.Apple
                                    # inv.Apple.name == "Apple"
                                    # inv.Apple.desc == "is sweet."

inv("Egg", "is fresh.", 3)          # Add 3 new items to the inventory
                                    # => inv.Egg.count == 3

inv("Piece of cheese", "is stale")  # => inv.Piece_of_cheese

inv("Ant", "is sour.", color="Red") # Add a new item with a custom attr
                                    # => inv.Ant.color == "Red"

inv.Bread("Bread loaf", "is good.") # Another syntax to add a new item
                                    # => inv.Bread.name == "Bread loaf"

                            # Address the item's attributes:
inv.Egg.name                # name,
inv.Egg.desc                # description,
inv.Egg.count               # amount.

inv.Egg.desc = "New phrase" # Update the description with a new string

inv.Egg.count += 1          # Increase the amount of an existing item
inv.Egg.count -= 1          # Decrease the amount of an existing item
inv.Egg.count = 4           # Set a different amount of an existing item

if inv.Egg:                 # Check if the item exists in the collection

for i in inv:               # Iterate items

print(inv.Egg)              # Print the contents of an item to console
"[inv.Egg]"                 # Output the contents of an item in dialog

print(inv)                  # Print the contents of the inventory
"[inv]"                     # Output in dialog (multi-line!)

len(inv)                    # The amount of different types of items
                            # in the inventory (zero means it's empty).
"""

class DoesNotExist:
    """ When an absent item is requested from the inventory, this class
        is used. Ex.:
        
            if inv.Alien        # False
            inv.Alien.count     # 0
            inv.Alien(args)     # create new item "Alien" with args
    """
    count = 0

    def __init__(self, obj, non_existent_attr):
        """ To create a new item, save `inventory` and `item`. """
        self.inv = obj
        self.nonexistent = non_existent_attr

    def __bool__(self):
        """ When checked for existence, report False. """
        return False

    def __setattr__(self, attr, value):
        """ Attempting to assign to non-existing item. Raise Error. """
        if attr in ("inv", "nonexistent"):
            object.__setattr__(self, attr, value)
            return

        raise AttributeError(f"Item `{self.nonexistent}` doesn't exist."
            f" Therefore it can't be assigned an attribute (`{attr}`).")

    def __call__(self, *args, **kwargs):
        """ Create a new item in Inventory. """
        if args or kwargs:
            self.inv(*args, **kwargs, own_var_name=self.nonexistent)


class Item:
    """ An item in the inventory.

        Normally it has `desc` (description).

        `count` can be 0, then the item "is not in the inventory".

        It has at least `name` and typically is accessed as inv.<name>,
        unless `own_var_name` attribuute exists: then it's accessed as

            inv.<own_var_name>

        (It's useful if the name has spaces or other symbols forbidden
        in Python identifiers, like 食パン etc.).

        To create a new item, use `inv()` or `inv.<name>()`:

            inv('NewItem', 'Description', 123) => inv.NewItem, 123 pcs

            inv.Name('This is The Name', 'Description') => inv.Name
    """
    def __init__(self, name, desc="", count=1, **kwargs):
        """
            Create an item in the inventory.

            If additional keyword arguments are present, store them
            with the item.
        """
        self.name = name
        self.desc = desc
        self.count = count
        for k, v in kwargs.items():
            setattr(self, k, v)


    def __bool__(self):
        """ To check if such item exists in the inventory.
        """
        return bool(self.count)


    def __str__(self):
        """ Return the item's info as a string.
        """
        s = ', '.join([f"{k}: {v}" for k, v in vars(self).items() \
                        if k not in ('own_var_name', 'name')])

        if 'own_var_name' in vars(self) and self.own_var_name != self.name:
            return f"{self.own_var_name} ({self.name}) = {s}"

        return f"{self.name} = {s}"


    def __getattr__(self, attr):
        """ Called when a requested attribute doesn't exist.

            self.own_var_name
                (if present) should be the item's name in the inventory.

            Otherwise `self.name` is the item's name in the inventory.
        """
        if attr == 'own_var_name':
            return None


    def __call__(self, *args, **kwargs):
        """ Trying to call an existing item (to create it?). => Error.
        """
        raise NameError(f"'Create item' `{self.own_var_name}` failed: "
                    "it already exists. Did you mean increase .count?")


class Inventory:

    def __str__(self):
        """ Report the contents of the inventory.
        """
        return '\n'.join([str(v) for v in vars(self).values() if v.count])


    def __call__(self, *args, **kwargs):
        """ If used without parameters `inv()`, report its contents.
            With parameters, add an item to the inventory.
        """
        if not (args or kwargs):
            return str(self)

        # Adding an item to the inventory
        # Get the attribute name (inv.<own_var_name>):
        if kwargs and 'own_var_name' in kwargs:
            n = kwargs['own_var_name']
            if not args and 'name' not in kwargs:
                kwargs['name'] = n
        else:
            if args:
                n = args[0]
            else:
                n = kwargs['name']

            # In case the name has spaces:
            if ' ' in n:
                n = n.replace(' ', '_')
                kwargs['own_var_name'] = n

        # In case the name still can't be an identifier:
        if not n.isidentifier():
            raise NameError(f"{n} is not a valid identifier.")

        # Create and insert the item
        setattr(self, n, Item(*args, **kwargs))


    def __getattr__(self, item):
        """ If an absent attribute is requested, return an instance of
            DoesNotExist().
        """
        return DoesNotExist(self, item)


    def __iter__(self):
        """ To iterate items. """
        for k, v in vars(self).items():
            if v.count:
                # Show only items with non-zero amount
                yield v


    def __len__(self):
        i = 0
        for item in self:
            i += 1
        return i

Here's a working example in Ren'Py (thanks to Kaivu for the inventory screen code!).

You can save this code as "script.rpy" or something, and run in Ren'Py to test:

script.rpy

Code: Select all

default inv = Inventory()
default selected_item = None

screen inventory_display_toggle():
    zorder 92
    frame:
        background "#000a"
        xalign 0.05
        yalign 0.095

        textbutton "Inventory":
            #hover_sound "click3.mp3"
            #activate_sound "click1.wav"
            action ToggleScreen("inventory_screen")


screen inventory_screen():
    modal True
    tag inventory

    frame:
        xfill True
        yfill True
        background "#531A"

        hbox:
            spacing 50  # Space between the two main sections
            align (0.5, 0.5)

            # Items list section
            frame:
                background "#000c"  # Background color for items list
                xminimum 550
                xmaximum 550
                ymaximum 680
                vbox:
                    spacing 15
                    text "Items" size 30 align (0.5, 0.5)
                    viewport:
                        draggable True
                        mousewheel True
                        vbox:
                            for item in inv:
                                textbutton item.name:
                                    action SetVariable("selected_item", item)

                                    xalign 0.0
                                    xmaximum 530  # Maximum width for item text
                                    yminimum 30  # Minimum height for each item

            # Description section
            frame:
                background "#000c"  # Background color for description area
                xminimum 550
                xmaximum 550
                ymaximum 680
                vbox:
                    spacing 15
                    text "Description" size 30 align (0.5, 0.5)

                    vbox:
                        xfill True
                        if selected_item:
                            text selected_item.desc xmaximum 530 line_spacing 1.2 xoffset 5
                            null height 20
                            text "Amount: [selected_item.count]" xoffset 5
                                    
                        else:
                            text "Select an item to see its description." xmaximum 530 line_spacing 1.2 xoffset 5


label start:
    python:
        quick_menu = False
        inv('Water', 'is tasty')
        inv('Bread', 'is mouldy', 3)
        inv.Sixpack('Six-pack', 'is good for health', 30)
        inv.cheese1('Large piece of cheese', 'is fresh')

    window hide
    scene black
    show screen inventory_display_toggle
    "[inv()]"
    "[inv.Sixpack].\nDo I have some? [not (not inv.Sixpack)]."
    """I have [inv.Water.count] bottles of [inv.Water.name]\n
    and [inv.Bread.count] pcs of [inv.Bread.name], and it [inv.Bread.desc]."""
    "Total kinds of items: [len(inv)]. The End"
Happy coding!
Last edited by jeffster on Thu Sep 26, 2024 10:20 am, edited 6 times in total.

User avatar
jeffster
Miko-Class Veteran
Posts: 877
Joined: Wed Feb 03, 2021 9:55 pm
Contact:

Re: Object Oriented Programming (OOP) useful examples for ren'Py items

#2 Post by jeffster »

History

m_from_space convinced me that for managing items, it's usually easier to use well constructed objects than my beloved dict's and list's,
viewtopic.php?p=569711#p569711
so I developed a bit of Python code to manage items and inventory with the simplest possible syntax.

Of course these classes can be used not only for inventory, but for any collection of any kind of items.

Then I noticed that it contains many various constructions and methods covering a large part of OOP territory:
  • Class attributes.
  • Converting object to string and to boolean.
  • Answering requests with non-existent attributes. (Using __getattr__).
  • Making the object callable. (Using __call__).
  • Making the object iterable. (Using __iter__).
So here we are. Changelog:

v.1.2:
- Removed name mangling as unnecessary and complicating.
+ Added `inv.<Item>()` syntax for item creation.
+ Trying to create an existing item raises an error.
+ Added Usage Cheat Sheet

v.1.1:

* Class Absent was renamed to DoesNotExist and improved, so that attempts to assign an attribute of a nonexistent item would cause a proper error message.
* Method __next__ was removed as it was used erroneously and actually not needed.
* A link to a Magic methods tutorial was added to the original post.

Post Reply

Who is online

Users browsing this forum: No registered users