Ren'Py sorting minigame

Home Edit


This post goes over how to create a sorting minigame in Ren’Py.

Ren'Py Sorting Game

The image is from the game KnitBone.

Class

Create the Python class:

init python:
    class Sorting:
        def __init__(self):
            self.moves = 0
            self.values = []

        def jump(self):
            renpy.jump("sorting_minigame")

        def start(self, moves: int, items: int, win: str, lose: str):
            self.moves = moves
            self.values = []
            self.win = win
            self.lose = lose

            while self.values == sorted(self.values):
                # TODO: change this logic if you don't want your items to be randomized
                self.values = renpy.random.sample(list(range(1, items + 1)), items)

            self.jump()

        def won(self):
            return self.values == sorted(self.values)

        def lost(self):
            return not self.moves

        def ondrag(self, drags, drop):
            drag = drags[0]
            [drag_index, drag_value] = self.parse_drag_name(drag.drag_name)

            if not drop:
                drag.snap(**self.get_snap(drag_index))
                return

            [drop_index, drop_value] = self.parse_drag_name(drop.drag_name)

            drag.snap(**self.get_snap(drop_index))
            drop.snap(**self.get_snap(drag_index))

            drag.drag_name = self.stringify_drag_name(drop_index, drag_value)
            drop.drag_name = self.stringify_drag_name(drag_index, drop_value)

            self.values[drag_index - 1] = drop_value
            self.values[drop_index - 1] = drag_value

            self.moves -= 1
            # TODO: replace or remove audio
            renpy.sound.queue("drop.ogg", relative_volume=1.0)
            self.jump()

        def get_snap(self, index: int):
            return {
                "x": (index - 1) / len(self.values),
                "y": 0.5,
                "delay": 0.2,
            }

        def stringify_drag_name(self, index: int, value: int):
            return f"{index},{value}"

        def parse_drag_name(self, name: str):
            [index, value] = name.split(",")
            return [int(index), int(value)]

And initialize it:

default sorting = Sorting()

Screen

Create the screen:

screen sorting_minigame():
    frame:
        background Solid((0, 0, 0, 100))
        text "{color=#ccc}Remaining Moves: [sorting.moves]"
        xpos 30
        ypos 30

    draggroup:
        for index, value in enumerate(sorting.values, start=1):
            drag:
                # TODO: add your images to `game/images/` folder and
                # use one-based numbering (e.g., `item 1.png` and `item 1 hover.png`)
                child f"item {value}.png"
                hover_child f"item {value} hover.png"
                selected_child f"item {value} hover.png"
                drag_name sorting.stringify_drag_name(index, value)
                draggable not(sorting.won() or sorting.lost())
                dragged sorting.ondrag
                droppable True
                xpos sorting.get_snap(index)["x"]
                ypos sorting.get_snap(index)["y"]
                # TODO: replace or remove audio
                hovered [Queue("sound", "hovered.ogg")]

Label

Create a label that calls the screen:

label sorting_minigame:
    if sorting.won():
        show screen sorting_minigame
        "You won!"
        hide screen sorting_minigame
        jump expression sorting.win
    elif sorting.lost():
        show screen sorting_minigame
        "You lost..."
        hide screen sorting_minigame
        jump expression sorting.lose

    call screen sorting_minigame

Usage

Start the slider minigame:

# game/script.rpy
label start:
    $ sorting.start(moves=4, items=6, win="my_win_label", lose="my_lose_label")

To disable user rollback (the Back button), add this to script.rpy:

define config.rollback_enabled = False

Script

See the full script sorting.rpy:

# game/sorting.rpy
default sorting = Sorting()

init python:
    class Sorting:
        def __init__(self):
            self.moves = 0
            self.values = []

        def jump(self):
            renpy.jump("sorting_minigame")

        def start(self, moves: int, items: int, win: str, lose: str):
            self.moves = moves
            self.values = []
            self.win = win
            self.lose = lose

            while self.values == sorted(self.values):
                # TODO: change this logic if you don't want your items to be randomized
                self.values = renpy.random.sample(list(range(1, items + 1)), items)

            self.jump()

        def won(self):
            return self.values == sorted(self.values)

        def lost(self):
            return not self.moves

        def ondrag(self, drags, drop):
            drag = drags[0]
            [drag_index, drag_value] = self.parse_drag_name(drag.drag_name)

            if not drop:
                drag.snap(**self.get_snap(drag_index))
                return

            [drop_index, drop_value] = self.parse_drag_name(drop.drag_name)

            drag.snap(**self.get_snap(drop_index))
            drop.snap(**self.get_snap(drag_index))

            drag.drag_name = self.stringify_drag_name(drop_index, drag_value)
            drop.drag_name = self.stringify_drag_name(drag_index, drop_value)

            self.values[drag_index - 1] = drop_value
            self.values[drop_index - 1] = drag_value

            self.moves -= 1
            # TODO: replace or remove audio
            renpy.sound.queue("drop.ogg", relative_volume=1.0)
            self.jump()

        def get_snap(self, index: int):
            return {
                "x": (index - 1) / len(self.values),
                "y": 0.5,
                "delay": 0.2,
            }

        def stringify_drag_name(self, index: int, value: int):
            return f"{index},{value}"

        def parse_drag_name(self, name: str):
            [index, value] = name.split(",")
            return [int(index), int(value)]

screen sorting_minigame():
    frame:
        background Solid((0, 0, 0, 100))
        text "{color=#ccc}Remaining Moves: [sorting.moves]"
        xpos 30
        ypos 30

    draggroup:
        for index, value in enumerate(sorting.values, start=1):
            drag:
                # TODO: add your images to `game/images/` folder and
                # use one-based numbering (e.g., `item 1.png` and `item 1 hover.png`)
                child f"item {value}.png"
                hover_child f"item {value} hover.png"
                selected_child f"item {value} hover.png"
                drag_name sorting.stringify_drag_name(index, value)
                draggable not(sorting.won() or sorting.lost())
                dragged sorting.ondrag
                droppable True
                xpos sorting.get_snap(index)["x"]
                ypos sorting.get_snap(index)["y"]
                # TODO: replace or remove audio
                hovered [Queue("sound", "hovered.ogg")]

label sorting_minigame:
    if sorting.won():
        show screen sorting_minigame
        "You won!"
        hide screen sorting_minigame
        jump expression sorting.win
    elif sorting.lost():
        show screen sorting_minigame
        "You lost..."
        hide screen sorting_minigame
        jump expression sorting.lose

    call screen sorting_minigame