Last active
December 16, 2024 15:10
-
-
Save JarbasAl/f62237047bef883d34bf08c2739d6eb4 to your computer and use it in GitHub Desktop.
simple text game engine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """simple single scene game with no game objects""" | |
| from text_engine import (Callbacks, Conditions, GameScene, Keyword, KeywordIntent, GameIntents, IFGameEngine) | |
| class TheRoom(GameScene): | |
| def __init__(self): | |
| self.inventory = [] | |
| # Define the scene entities | |
| self.key = Keyword(name="key") | |
| self.door = Keyword(name="door") | |
| self.window = Keyword(name="window") | |
| self.table = Keyword(name="table") | |
| # Define the intents for interacting with the scene | |
| look_intent = KeywordIntent( | |
| name="look", | |
| required=[Keyword(name="look")], | |
| optional=[self.key, self.door, self.window, self.table], | |
| handler=self.on_look | |
| ) | |
| open_door_intent = KeywordIntent( | |
| name="open_door", | |
| required=[Keyword(name="open"), self.door], | |
| handler=self.on_open_door | |
| ) | |
| open_window_intent = KeywordIntent( | |
| name="open_window", | |
| required=[Keyword(name="open"), self.window], | |
| handler=self.on_open_window | |
| ) | |
| take_key_intent = KeywordIntent( | |
| name="take_key", | |
| required=[Keyword(name="take"), self.key], | |
| handler=self.on_take_key | |
| ) | |
| use_key_intent = KeywordIntent( | |
| name="use_key", | |
| required=[Keyword(name="use"), self.key, self.door], | |
| handler=self.on_use_key | |
| ) | |
| # this scene has no objects, if it did we should always have a | |
| # "back" intent in each object to refocus the scene | |
| # game.active_scene.active_object = -1 | |
| super().__init__("you are in a dimly lit room", | |
| intents=GameIntents([take_key_intent, | |
| look_intent, | |
| use_key_intent, | |
| open_door_intent])) | |
| # scene logic/intents | |
| def on_take_key(self, game: IFGameEngine, utterance: str): | |
| if "key" not in self.inventory: | |
| self.inventory.append("key") | |
| return "You pick up the small metal key." | |
| else: | |
| return "You already have the key." | |
| def on_open_door(self, game: IFGameEngine, utterance: str): | |
| if "key" in self.inventory: | |
| return "The door is locked. Maybe you need to use the key?" | |
| else: | |
| return "The door is locked. You need to find a way to open it." | |
| def on_open_window(self, game: IFGameEngine, utterance: str): | |
| return "The window is stuck shut. You won't get out this way." | |
| def on_use_key(self, game: IFGameEngine, utterance: str): | |
| if "key" in self.inventory: | |
| game.running.clear() # End the game | |
| return "You unlock the door with the key." | |
| else: | |
| return "You don't have the key." | |
| def on_look(self, game: IFGameEngine, utterance: str): | |
| if self.table.match(utterance): | |
| if "key" in self.inventory: | |
| return "You see a dusty table" | |
| return "You see a small key on the table." | |
| elif self.key.match(utterance): | |
| if "key" in self.inventory: | |
| return "The key is small and rusty" | |
| return "You see a small key on the table." | |
| elif self.door.match(utterance): | |
| return "The door is locked. It looks sturdy." | |
| elif self.window.match(utterance): | |
| return "The window is stuck shut. You won't get out this way." | |
| else: | |
| return "The room is small and empty, except for a table and a door." | |
| class EscapeRoom(IFGameEngine): | |
| def __init__(self): | |
| super().__init__( | |
| scenes=[TheRoom()], | |
| callbacks=Callbacks(on_end=self.on_end, | |
| on_start=self.on_start, | |
| on_win=lambda k: print("Congratulations! You escaped the room."), | |
| on_lose=lambda k: print("You ran out of oxygen and died!")), | |
| conditions=Conditions(is_loss=self.is_loss, is_win=self.is_win)) | |
| def on_start(self, game: IFGameEngine): | |
| game.print("You wake up in a locked room. There is a table with something on it.") | |
| game.print("Type commands to look around, pick up items, and interact with objects.") | |
| game.print("For example: 'look at key', 'take key', 'open door', or 'use key on door'.") | |
| def on_end(self, game: IFGameEngine): | |
| game.print("Game Over. Thanks for playing!") | |
| def is_win(self, game: IFGameEngine) -> bool: | |
| return not game.running.is_set() # If game stops running, it's a win | |
| def is_loss(self, game: IFGameEngine) -> bool: | |
| return game.current_turn > 3 | |
| if __name__ == "__main__": | |
| EscapeRoom().run() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """based on demo.py but with actual gameplay | |
| TODO: make dialogs/keywords come from files, locale folder similar to OVOS. | |
| this will allow easy translation while also abstracting the game engine away | |
| completely new games can be made by just changing resource files | |
| """ | |
| import random | |
| from text_engine import (Callbacks, Conditions, GameScene, Keyword, KeywordIntent, GameIntents, IFGameEngine) | |
| class TheCursedRoom(GameScene): | |
| def __init__(self): | |
| self.got_key = False | |
| self.inventory = [] | |
| self.max_sanity = 10 # Sanity level (difficulty setting can change this) | |
| self.sanity = self.max_sanity | |
| # Define the scene entities/keywords | |
| self.key = Keyword(name="key") | |
| self.door = Keyword(name="door") | |
| self.window = Keyword(name="window") | |
| self.table = Keyword(name="altar", samples=["altar", "table"]) | |
| self.symbols = Keyword(name="symbols") | |
| self.mirror = Keyword(name="mirror") | |
| self.book = Keyword(name="book") | |
| self.painting = Keyword(name="painting") | |
| self.floorboards = Keyword(name="floorboards") | |
| self.wall_slime = Keyword(name="slime") | |
| # samples are synonyms, may contain whitespaces/multiple words | |
| look = Keyword(name="look", samples=["look", "interact", "inspect", | |
| "see", "watch", "view", "observe", | |
| "examine"]) | |
| openk = Keyword(name="open", samples=["open", "use"]) | |
| move = Keyword(name="move", samples=["move", "push", "pull"]) | |
| room = Keyword(name="room", samples=["room", "around"]) | |
| touch = Keyword(name="touch", samples=["touch", "interact"]) | |
| read = Keyword(name="read") | |
| drop = Keyword(name="drop", samples=["drop", "dispose", "throw away"]) | |
| take = Keyword(name="take", samples=["take", "get", "grab"]) | |
| # Intents for interacting with the scene | |
| look_intent = KeywordIntent( | |
| name="look", | |
| required=[look], | |
| optional=[room, self.key, self.door, self.window, self.table, self.book, self.floorboards, | |
| self.wall_slime, self.painting], | |
| handler=self.on_look | |
| ) | |
| move_floorboards_intent = KeywordIntent( | |
| name="move_floorboards", | |
| required=[move, self.floorboards], | |
| handler=self.on_move_floorboards | |
| ) | |
| take_key_intent = KeywordIntent( | |
| name="take_key", | |
| required=[take, self.key], | |
| handler=self.on_take_key | |
| ) | |
| drop_key_intent = KeywordIntent( | |
| name="drop_key", | |
| required=[drop, self.key], | |
| handler=self.on_drop_key | |
| ) | |
| use_key_intent = KeywordIntent( | |
| name="use_key", | |
| required=[openk, self.key], | |
| handler=self.open_door | |
| ) | |
| open_door_intent = KeywordIntent( | |
| name="open_door", | |
| required=[openk, self.door], | |
| handler=self.open_door | |
| ) | |
| open_window_intent = KeywordIntent( | |
| name="open_window", | |
| required=[openk, self.window], | |
| handler=self.on_open_window | |
| ) | |
| read_book_intent = KeywordIntent( | |
| name="read_book", | |
| required=[read, self.book], | |
| handler=self.on_read_book | |
| ) | |
| touch_slime_intent = KeywordIntent( | |
| name="touch_slime", | |
| required=[touch, self.wall_slime], | |
| handler=self.on_slime | |
| ) | |
| help_intent = KeywordIntent( | |
| name="help", | |
| required=[Keyword(name="help")], | |
| handler=self.on_help | |
| ) | |
| default_response = ("You are in a cold, airless room filled with a dreadful silence. The walls shift subtly, and the air smells of decay and forgotten things." | |
| "\nType commands to interact: 'look', 'inspect altar', 'take key', 'examine symbols', 'look at mirror', 'read book', 'open door'") | |
| super().__init__(default_response, intents=GameIntents( | |
| [take_key_intent, look_intent, touch_slime_intent, help_intent, use_key_intent, open_door_intent, | |
| read_book_intent, open_window_intent, move_floorboards_intent, drop_key_intent])) | |
| def random_event(self): | |
| events = [ | |
| "You hear faint scratching noises from inside the walls.", | |
| "A cold wind brushes against your neck, but there are no windows open.", | |
| "You feel something watching you, though you see nothing.", | |
| "The floor creaks as if someone is walking behind you.", | |
| "The room seems to shift slightly, like a living thing adjusting its posture.", | |
| "You hear whispers, but you cannot make out the words. The silence that follows is worse.", | |
| "For a moment, the walls seem to close in, but then everything is normal again—if you can call this normal.", | |
| "A cold breeze brushes past you, though there is no wind in this place.", | |
| "The shadows in the corners seem darker now, as though something lurks within them." | |
| ] | |
| return random.choice(events) | |
| def decrease_sanity(self, decrement: int): | |
| self.sanity -= decrement | |
| if self.sanity <= 0: | |
| return random.choice([ | |
| "Your mind shatters, unable to comprehend the horrors of this place. You collapse into darkness.", | |
| "Your vision blurs, and the walls seem to close in. Your mind shatters like glass. You are lost to madness." | |
| ]) | |
| stages = [ | |
| "You feel a slight headache as unease grips your mind.", | |
| "Your thoughts are clouded, whispers echo in your skull.", | |
| "You struggle to focus as your sanity begins to fray.", | |
| "A horrible pressure builds in your skull; you can barely think.", | |
| "Your mind reels. Darkness creeps in at the edges of your vision." | |
| ] | |
| level = max(0, min(self.sanity // 2, len(stages) - 1)) | |
| return f"{stages[level]}\nYour sanity: {self.sanity}/{self.max_sanity}." | |
| def on_help(self, game: IFGameEngine, utterance: str): | |
| return ("You can interact with objects in the room using commands like:\n" | |
| "'look at [object]', 'take [object]', 'move [object]'.\n" | |
| "Type 'help' to see this message again.") | |
| def on_open_window(self, game: IFGameEngine, utterance: str): | |
| responses = [ | |
| "The window won't budge. It feels more like stone than glass.", | |
| "You press against the window, but it doesn't yield. Its surface is unnaturally cold and unyielding.", | |
| "No matter how hard you try, the window remains fixed in place—like it's part of the wall itself.", | |
| "The glass hums faintly, resisting your touch. There's something wrong about this window." | |
| ] | |
| return random.choice(responses) | |
| def on_take_key(self, game: IFGameEngine, utterance: str): | |
| self.got_key = True | |
| if "key" not in self.inventory: | |
| self.inventory.append("key") | |
| responses = [ | |
| f"As you pick up the key, it feels unnaturally cold. Whispers dance at the edge of your hearing. {self.decrease_sanity(1)}", | |
| f"The key's surface sends a jolt of chill through your fingers. You hear faint whispers, though you see no one. {self.decrease_sanity(1)}", | |
| f"You grasp the key reluctantly. It's as cold as death, and your head swims with ghostly murmurs. {self.decrease_sanity(1)}", | |
| f"The moment you touch the key, a fleeting shadow passes before your eyes. The air feels heavier now. {self.decrease_sanity(1)}" | |
| ] | |
| return random.choice(responses) | |
| else: | |
| responses = [ | |
| "You already possess the accursed key. Its touch still lingers on your skin.", | |
| "The key rests heavily in your pocket, its presence undeniable and chilling.", | |
| "You feel the key in your possession—it hasn't changed, but it still unnerves you.", | |
| "The key is already yours, though you wish it were not." | |
| ] | |
| return random.choice(responses) | |
| def on_drop_key(self, game: IFGameEngine, utterance: str): | |
| if "key" in self.inventory: | |
| self.inventory.remove("key") | |
| responses = [ | |
| "With trembling hands, you place the key on the altar. The very air seems to thicken, and a cold dread washes over you as you stand back. You cannot bring yourself to place it anywhere else; the altar seems to demand it, its presence heavy and oppressive. A deep, guttural fear rises within you, and you wonder if you’ve just sealed your fate by returning the key.", | |
| "You try to force yourself to leave the key somewhere else, but your hands tremble with fear, unable to stray far from the altar. The moment the key touches the surface, an eerie hum fills the room, and you can almost hear the altar breathing, as if alive. Your mind races, plagued by the thought that you've made a grave mistake.", | |
| "You know you should leave the key, but something in you refuses to let it out of your grasp. Finally, with great reluctance, you place it on the altar. A chilling shiver runs down your spine as the altar seems to ‘accept’ the key. Your heart pounds in your chest, and your mind is flooded with thoughts of what might happen next." | |
| ] | |
| self.sanity += 1 | |
| sanity_responses = [ | |
| "For a brief, fleeting moment, you feel a hint of clarity, as though your mind has momentarily escaped the crushing weight of the altar’s influence. But the sensation is fleeting, and the sense of dread only deepens, gnawing at you like a relentless tide.", | |
| "You feel a slight return of your composure, but it is overshadowed by the growing sense of terror. The altar’s call lingers, like a dark thread weaving through your thoughts, tying you to its horrific presence.", | |
| "A flicker of sanity returns, but it is as fragile as glass. You feel as though you have taken one step back, only to be dragged two steps further into the abyss. The altar’s power is relentless, and you fear the cost of even the smallest reprieve." | |
| ] | |
| response = f"{random.choice(responses)} {random.choice(sanity_responses)}" | |
| return response | |
| else: | |
| if self.got_key: | |
| # Case where the player previously had the key: | |
| previous_key_responses = [ | |
| "The key feels heavy in your memories, as though its presence is still lingering with you. You hesitate, heart pounding, unsure if returning it was a grave mistake. The altar calls to you, its dark influence growing stronger. You cannot help but wonder if you have done enough to sever its hold over you, or if this return will be the last step before something darker takes root.", | |
| "You remember holding the key in your hands, the weight of it growing heavier with each passing second. Now, standing before the altar again, you're unsure whether placing it back will free you or drag you further into its depths. The silence of the room presses in on you, and you fear the consequences of this action.", | |
| "Your hands are empty. The key is gone, lost to you, and the altar stands unmoving, as though mocking your confusion. You realize with a sinking heart that it never truly belonged to you in the first place.", | |
| "You feel around, but the key is nowhere to be found. The altar watches you with a stillness that only deepens your unease. Its presence seems to mock your helplessness, as if it knew you would never hold the key." | |
| ] | |
| return random.choice(previous_key_responses) | |
| else: | |
| # Case where the player never had the key: | |
| no_key_responses = [ | |
| "You hear a voice in your head, as though the altar is speaking to you, commanding you to return the key. But you search your hands and pockets—there is nothing. The confusion churns inside you, and you wonder if you're losing touch with reality. What key is the voice talking about? Did you ever even have it?", | |
| "The altar's dark whispers seem to echo in your mind, urging you to return the key. But you stand there, empty-handed. There is no key. The thought of it seems almost surreal. Did you ever have it? The more you think about it, the more uncertain you become, and a sense of vertigo grips you. What is real?", | |
| "A strange, disorienting thought crosses your mind. The voice inside you calls for the key, but you have nothing. There was no key, was there? You’re left in a state of confusion, questioning your memories and your sanity. The altar’s presence seems to mock your uncertainty, as if it knows the truth of your confusion." | |
| ] | |
| return f"{random.choice(no_key_responses)} {self.decrease_sanity(1)}" | |
| def open_door(self, game: IFGameEngine, utterance: str): | |
| if "key" in self.inventory: | |
| # The key is ultimately useless, no win condition | |
| responses = [ | |
| "You try to use the key, but it simply does not fit. The door resists your efforts, as though mocking you.", | |
| "The key turns in the lock, but the door doesn't budge. It's as if the key were made for nothing at all.", | |
| "You jam the key into the lock with frustration. It fits, but nothing happens. The door remains closed.", | |
| "You push the key into the door, but it does nothing. It’s a cruel trick, a dead end.", | |
| "You force the rusted key into the door, but it does nothing. It’s a cruel trick, a dead end." | |
| ] | |
| return random.choice(responses) + f" {self.decrease_sanity(1)}" | |
| else: | |
| responses = [ | |
| "The door remains shut. Its surface pulses as though alive. You don't have what you need to open it.", | |
| "You push against the door, but it holds firm. The key... you need the key.", | |
| "The door seems to breathe, mocking your efforts. You're missing something crucial.", | |
| "The door refuses to open. It shudders as though taunting you for your lack of preparation." | |
| ] | |
| return random.choice(responses) | |
| def on_look(self, game: IFGameEngine, utterance: str): | |
| if self.table.match(utterance): | |
| responses = [ | |
| "The stone altar bears dark stains and ancient carvings. Its presence unnerves you.", | |
| "The altar emanates a faint hum, like distant chanting. The air around it feels heavy.", | |
| "Strange symbols flicker on the altar, pulsing like a heartbeat. You feel a chill in your bones." | |
| ] | |
| if "key" in self.inventory: | |
| return random.choice(responses) | |
| return "Upon the altar lies a key, tarnished and vile. Symbols of an unknown language pulse faintly." | |
| elif self.symbols.match(utterance): | |
| result = random.choice([ | |
| "The symbols carved into the walls seem to shift and dance when you look directly at them. They whisper forgotten truths.", | |
| "You trace the shifting symbols with trembling fingers. Visions of cyclopean cities and endless voids overwhelm you. The walls scream silently, and your sanity fractures." | |
| ]) | |
| return f"{result} {self.decrease_sanity(2)}" | |
| elif self.key.match(utterance): | |
| responses = [ | |
| "The key is twisted and blackened, as if forged in unnatural fires.", | |
| "You feel the key vibrate faintly in your hand, as though eager to be used.", | |
| "The key looks brittle, yet it weighs heavy with significance." | |
| ] | |
| return random.choice(responses) | |
| elif self.door.match(utterance): | |
| responses = [ | |
| "The door pulses slightly, as though breathing. It fills you with an overwhelming sense of dread.", | |
| "The surface of the door shifts and twists, like flesh under duress.", | |
| "The door shimmers faintly. For a brief moment, you see countless eyes staring back at you." | |
| ] | |
| return random.choice(responses) | |
| elif self.window.match(utterance): | |
| responses = [ | |
| "The window reveals nothing but darkness. Shapes move in the void, watching you.", | |
| "A soft tapping sound comes from the window, though nothing is there.", | |
| "The glass feels unnaturally cold, as though it seals in the void beyond." | |
| ] | |
| return random.choice(responses) | |
| elif self.book.match(utterance): | |
| responses = [ | |
| "An ancient tome rests on a pedestal. Its pages hum faintly, calling to you.", | |
| "The book's cover writhes, as though alive. Words whisper faintly in your ears.", | |
| "A sigil on the book glows briefly when you approach, then fades into nothingness." | |
| ] | |
| return random.choice(responses) | |
| elif self.wall_slime.match(utterance): | |
| responses = [ | |
| "The slime on the walls glistens, pulsing faintly. It seems almost sentient.", | |
| "You notice the slime creeping slowly, defying gravity. The sight unsettles you.", | |
| "A rancid smell emanates from the slime, thick and nauseating." | |
| ] | |
| return random.choice(responses) | |
| elif self.mirror.match(utterance): | |
| result = random.choice([ | |
| "Your reflection smiles, but you did not smile. Your sanity shivers.", | |
| "The mirror shows a dark figure standing behind you, but when you turn, no one is there.", | |
| "The mirror's surface ripples like water. You catch a glimpse of something behind you, but when you turn, nothing is there.", | |
| "The mirror's reflection twists into a monstrous visage—your face, but horribly distorted and wrong. You stumble back, your mind reeling, as your sanity erodes" | |
| ]) | |
| return f"You peer into the mirror. {result} {self.decrease_sanity(1)}" | |
| elif self.painting.match(utterance): | |
| responses = [ | |
| f"The painting depicts a vast sea of writhing tentacles beneath a sickly yellow sky. One of the tentacles seems to move. {self.decrease_sanity(1)}", | |
| f"You gaze at the painting—a grotesque ocean of dark, twisting tentacles beneath a nauseating yellow sky. Something in the painting shifts, and you wonder if your eyes deceive you. {self.decrease_sanity(1)}", | |
| f"The painting is nightmarish—tentacles, endless and black, surge beneath a malignant sky. One tentacle twitches as though reaching for you. {self.decrease_sanity(1)}", | |
| f"The sea of tentacles in the painting seems endless, stretching beneath a sky poisoned with yellow. You swear one tentacle subtly moves, as if alive. {self.decrease_sanity(1)}" | |
| ] | |
| return random.choice(responses) | |
| elif self.floorboards.match(utterance): | |
| responses = [ | |
| f"The floorboards creak under your weight, an ominous sound. Something hollow echoes beneath, but you cannot tell what it is. {self.decrease_sanity(1)}", | |
| f"You press down on the floorboards, and they groan in protest. A hollow sound rises from below, like something is stirring. {self.decrease_sanity(1)}", | |
| f"As you press on the floorboards, they creak loudly. There's something hollow beneath—perhaps a hidden space, or something waiting. {self.decrease_sanity(1)}", | |
| f"The floorboards shift underfoot, emitting a hollow echo. Something stirs beneath the wood, and the air grows heavier. {self.decrease_sanity(1)}" | |
| ] | |
| return random.choice(responses) | |
| else: | |
| # NOTE: the painting might be mentioned or not | |
| responses = [ | |
| "The room is cold and bare, save for a painting and the eerie symbols scratched into the walls. Everything seems to be watching you.", | |
| "You scan the room. The walls are covered with strange symbols, their meaning lost to time. The air tastes stale, almost unnatural.", | |
| "The room is sparse and unsettling. Strange symbols mar the walls, and the floor creaks beneath your every step.", | |
| "The room feels oppressive, the walls adorned with a lonely painting and cryptic symbols, their meaning lost in the darkness." | |
| ] | |
| # dialog mentioning things to be looked at: slime, book, table, key, door, window, painting | |
| window_message = random.choice([ | |
| "A faint, unnerving shimmer emanates from a window, its glass barely containing the void beyond.", | |
| "The window appears almost liquid, rippling with an unnatural energy. You feel something watching you from beyond.", | |
| "Darkness stretches beyond the window, as if the glass holds back a universe of unspeakable horrors.", | |
| "The window is a mirror to nothingness, the glass cold and untouched, yet the air around it buzzes with static electricity." | |
| ]) | |
| slime_message = random.choice([ | |
| "Iridescent slime coats the walls, as though it is waiting for something to break its stillness.", | |
| "The slime writhes slowly, as if it's aware of your presence, its translucent form reflecting your every move.", | |
| "The slime seems to pulse, alive in its own way, rippling as though it senses your thoughts.", | |
| "Thick, viscous slime clings to the walls, its sheen flickering like a dying star, tempting you to touch it." | |
| ]) | |
| altar_message = random.choice([ | |
| "You see an altar, an ancient relic, that seems to throb with an unnatural rhythm, its surface marked by symbols forgotten by time.", | |
| "An ancient altar stands before you, its surface covered in dark stains and symbols that twist when you look away.", | |
| "The altar emits a low hum, its surface alive with strange, glowing runes. The air around it is oppressive.", | |
| "A weathered altar stands solemnly, etched with symbols that shift when you try to focus on them. A strange aura surrounds it." | |
| ]) | |
| door_message = random.choice([ | |
| "A door looms in the distance, twisted and unnatural, bearing an aura of dread that chills your soul.", | |
| "The door is an abomination, warped by time and something darker. You can feel it beckoning you closer.", | |
| "A door stands before you, dark wood twisted into grotesque shapes. You hear faint whispers coming from beyond.", | |
| "The door seems to pulse with life, its surface writhing as though something presses against it from the other side." | |
| ]) | |
| book_message = random.choice([ | |
| "There is a book. You sense an eerie presence near it. The air thickens around it, as if something awaits your approach.", | |
| "An old, weathered book rests before you, its pages flickering, as though trying to escape their bindings.", | |
| "The book radiates an aura of ancient knowledge, but something feels off—like it knows you're here and is waiting.", | |
| "A dusty tome lies open, the words shifting and changing as you approach, beckoning you to read them." | |
| ]) | |
| painting_message = random.choice([ | |
| "A painting hangs crookedly in one of the walls. It shifts when you aren’t looking.", | |
| "An unsettling painting graces the wall.", | |
| "A forgotten painting hangs askew, its grotesque subject matter obscured by the shadows. A cold, disquieting sensation lingers in the air around it." | |
| ]) | |
| items = [window_message, slime_message, altar_message, door_message, book_message] | |
| if random.choice([True, False]): | |
| items.append(painting_message) | |
| random.shuffle(items) | |
| items_message = "\n".join(items) | |
| return f"{random.choice(responses)}.\n{items_message}\n{self.random_event()}" | |
| def on_move_floorboards(self, game: IFGameEngine, utterance: str): | |
| # Sanity threshold to allow moving the floorboards | |
| sanity_threshold = self.max_sanity / 2 # half of sanity needs to be lost before moving floorboards is allowed | |
| if self.sanity > sanity_threshold: | |
| # If sanity is not low enough, the game refuses the action and gives an excuse | |
| refusal_responses = [ | |
| "You hesitate. Your hands tremble as you look at the floorboards. Something deep inside warns you not to disturb what lies beneath. It feels like you are not ready for what awaits you down there.", | |
| "The air feels heavier the closer you get to the floorboards. A voice whispers from the back of your mind, urging you to turn away. You can't shake the feeling that disturbing the floor might break you, or worse.", | |
| "A wave of unease washes over you. The floorboards seem to mock you, daring you to pry them loose. But your mind protests, pulling you back from the brink. You're not sure if you're ready to face what lies below.", | |
| "As you reach for the floorboards, a deep dread rises from within you. The very idea of moving them fills you with terror, and your body refuses to obey. Perhaps you're not prepared for what the void holds.", | |
| "You stand there, staring at the floorboards, unsure of what you’ll find beneath. Your sanity clings to you like a fragile thread, and you feel like tearing it apart by disturbing the floor might not be wise." | |
| ] | |
| return random.choice(refusal_responses) | |
| else: | |
| # If sanity is low enough, allow the action to proceed and end the game | |
| # This is how you win the game | |
| game.running.clear() | |
| success_responses = [ | |
| "You pry the floorboards loose with trembling hands and slip into the void below. What horrors await you now?", | |
| "With great effort, you remove the floorboards and descend into the darkness below. The air is thick with the smell of decay, and you can feel something watching you from the shadows.", | |
| "The floorboards creak and groan as you pull them aside, revealing the yawning chasm below. You step forward, your mind screaming at you to stop, but you descend anyway into the unknown.", | |
| "Your hands are slick with sweat as you remove the floorboards and descend into the abyss. The very air seems to pulse with a malevolent energy, and you feel your sanity slipping further away.", | |
| "The floorboards give way, and you step into the void below. A wave of dread fills you as the darkness swallows you whole. There's no turning back now." | |
| ] | |
| return random.choice(success_responses) | |
| def on_slime(self, game: IFGameEngine, utterance: str): | |
| responses = [ | |
| ( | |
| "The slime on the walls pulses under your touch. It feels alive. You recoil, but your hand tingles with a strange energy.", | |
| 2), | |
| ( | |
| "As you touch the slime, it ripples and reacts, sending a shiver down your spine. It's as though the slime has a will of its own.", | |
| 3), | |
| ( | |
| "The slime is unnervingly alive, pulsing and squelching beneath your fingers. Your hand tingles as if infected. You step back quickly.", | |
| 1), | |
| ( | |
| "You feel the slime move under your touch, its surface slick and alive. It leaves a strange aftertaste in your mouth, as if you've touched something wrong.", | |
| 2), | |
| ( | |
| "The slime writhes as you touch it, emitting an unnatural warmth. It feels almost sentient, like it's feeding off your touch.", | |
| 3), | |
| ( | |
| "The slime seems to stretch and crawl in response to your touch, darkening slightly as though it has absorbed something from you. You feel a wave of nausea.", | |
| 1) | |
| ] | |
| response, sanity_impact = random.choice(responses) | |
| return f"{response} {self.decrease_sanity(sanity_impact)}" | |
| def on_read_book(self, game: IFGameEngine, utterance: str): | |
| responses = [ | |
| f"You open the tome, and the words wriggle like worms across the pages. Eldritch chants seep into your brain, filling it with horrors you cannot comprehend. {self.decrease_sanity(3)}", | |
| f"As you flip the pages, the text twists and writhes, its language alien and terrifying. A cold sensation creeps through your mind as whispers start to fill the air. {self.decrease_sanity(3)}", | |
| f"The book’s pages flutter open on their own, and the words seem to slither across the paper. The air around you grows colder, and you can hear distant whispers. {self.decrease_sanity(3)}", | |
| f"You open the book and are greeted by impossible words that twist as if alive. The air grows thick, and your mind begins to reel with impossible thoughts. {self.decrease_sanity(3)}" | |
| ] | |
| return random.choice(responses) | |
| class EldritchEscape(IFGameEngine): | |
| def __init__(self): | |
| super().__init__( | |
| scenes=[TheCursedRoom()], | |
| callbacks=Callbacks(on_end=self.on_end, | |
| on_start=self.on_start, | |
| on_win=lambda k: print("You escaped, but the horrors will haunt you forever..."), | |
| on_lose=lambda k: print("Your mind shatters into oblivion. You are lost to the void.")), | |
| conditions=Conditions(is_loss=self.is_loss, is_win=self.is_win)) | |
| def on_start(self, game: IFGameEngine): | |
| game.print( | |
| "You feel a cold chill in the air, and the dim light flickers above you. " | |
| "\nThe room is silent, but something feels... off. There's a strange sense of dread in the air. " | |
| "\nYou can't quite remember how you got here. A door stands before you, and the shadows seem to stretch unnaturally. " | |
| "\nYou notice strange symbols etched into the wall, and the faint smell of something rotten lingers in the corner. " | |
| "\nYour mind feels clouded, as if a fog is settling over your thoughts.\n" | |
| ) | |
| def on_end(self, game: IFGameEngine): | |
| game.print("Game Over. The void consumes all.") | |
| def is_win(self, game: IFGameEngine) -> bool: | |
| return not game.running.is_set() # Game ends when door is unlocked | |
| def is_loss(self, game: IFGameEngine) -> bool: | |
| current_scene: TheCursedRoom = game.active_scene | |
| if hasattr(current_scene, 'sanity') and current_scene.sanity <= 0: | |
| return True # Sanity loss condition | |
| return False | |
| if __name__ == "__main__": | |
| EldritchEscape().run() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import threading | |
| from dataclasses import dataclass | |
| from typing import Callable, List, Union, Tuple, Optional | |
| from text_engine.intents import Keyword, KeywordIntent, IntentEngine | |
| @dataclass | |
| class Conditions: | |
| is_win: Callable[['IFGameEngine'], bool] | |
| is_loss: Callable[['IFGameEngine'], bool] | |
| @dataclass | |
| class Callbacks: | |
| on_start: Optional[Callable[['IFGameEngine'], None]] = None | |
| on_end: Optional[Callable[['IFGameEngine'], None]] = None | |
| on_win: Optional[Callable[['IFGameEngine'], None]] = None | |
| on_lose: Optional[Callable[['IFGameEngine'], None]] = None | |
| on_utterance: Optional[Callable[['IFGameEngine', str], None]] = None | |
| @dataclass | |
| class GameIntents: | |
| intents: List[KeywordIntent] | |
| parser: IntentEngine = IntentEngine() | |
| def __post_init__(self): | |
| for intent in self.intents: | |
| self.parser.register_intent(intent) | |
| def predict(self, utterance: str) -> Tuple[Optional[KeywordIntent], float]: | |
| intents = self.parser.calc_intents(utterance) | |
| if intents: | |
| return intents[0] | |
| return None, 0.0 | |
| @dataclass | |
| class GameScene: | |
| description: str | |
| game_objects: Optional[List['GameObject']] = None | |
| active_object: int = -1 # idx from game_objects | |
| intents: Optional[GameIntents] = None | |
| def __post_init__(self): | |
| self.game_objects = self.game_objects or [] | |
| def refocus(self): | |
| self.active_object: int = -1 | |
| def interact(self, game: 'IFGameEngine', utterance: str): | |
| if self.intents: | |
| intent, score = self.intents.predict(utterance) | |
| if score > 0.5: | |
| # change scenes here if needed via game.activate/add/remove_scene | |
| return intent.handler(game, utterance) | |
| if not len(self.game_objects) or self.active_object == -1: | |
| return self.description | |
| # change self.active_object here as needed | |
| return self.game_objects[self.active_object].interact(game=game, | |
| utterance=utterance) | |
| @dataclass | |
| class GameObject: | |
| name: Keyword | |
| intent_handlers: GameIntents | |
| default_dialog: str | |
| def bye(self, game: 'IFGameEngine'): | |
| # refocus scene instead of object | |
| game.active_scene.refocus() | |
| def interact(self, game: 'IFGameEngine', utterance: str) -> str: | |
| intent, score = self.intent_handlers.predict(utterance) | |
| # change game.active_scene.active_object here as needed | |
| if score < 0.5: | |
| return self.default_dialog | |
| return intent.handler(game, utterance) | |
| class IFGameEngine(threading.Thread): | |
| def __init__(self, | |
| scenes: List['GameScene'], | |
| conditions: Conditions, | |
| callbacks: Callbacks = Callbacks()): | |
| super().__init__() | |
| self.current_turn: int = 1 | |
| self.scenes = scenes | |
| self.callbacks = callbacks | |
| self.checks = conditions | |
| self.running = threading.Event() | |
| self._active_scene = 0 | |
| assert len(self.scenes) > 0 | |
| def print(self, text: str): | |
| # TODO - allow defining text color and such ? | |
| print(text) | |
| @property | |
| def active_scene(self) -> 'GameScene': | |
| return self.scenes[self._active_scene] | |
| def activate_scene(self, scene: Union[int, 'GameScene']): | |
| if isinstance(scene, int): | |
| self._active_scene = scene | |
| else: | |
| self._active_scene = self.scenes.index(scene) | |
| self.print(self.active_scene.description) | |
| def add_scene(self, scene: 'GameScene'): | |
| if scene not in self.scenes: | |
| self.scenes.append(scene) | |
| def remove_scene(self, scene: Union[int, 'GameScene']): | |
| if isinstance(scene, int): | |
| scene = self.scenes[scene] | |
| self.scenes.remove(scene) | |
| def run(self): | |
| self.running.set() | |
| if self.callbacks.on_start: | |
| self.callbacks.on_start(self) | |
| self.print(self.active_scene.description) | |
| while self.running.is_set(): | |
| utt = input("> ") | |
| if self.callbacks.on_utterance: | |
| self.callbacks.on_utterance(self, utt) | |
| ans = self.active_scene.interact(self, utt) | |
| if ans: | |
| self.print(ans) | |
| if self.checks.is_win(self): | |
| if self.callbacks.on_win: | |
| self.callbacks.on_win(self) | |
| break | |
| if self.checks.is_loss(self): | |
| if self.callbacks.on_lose: | |
| self.callbacks.on_lose(self) | |
| break | |
| self.advance() | |
| self.running.clear() | |
| if self.callbacks.on_end: | |
| self.callbacks.on_end(self) | |
| def advance(self): | |
| self.current_turn += 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from dataclasses import dataclass | |
| from typing import List, Dict, Tuple, Optional, Callable | |
| try: | |
| from quebra_frases import word_tokenize | |
| except ImportError: | |
| def word_tokenize(text: str, *args, **kwargs) -> List[str]: | |
| return text.split() | |
| # for typing | |
| IntentHandler = Callable[['IFGameEngine', str], str] | |
| @dataclass | |
| class Keyword: | |
| name: str | |
| samples: Optional[List[str]] = None | |
| # TODO - to_file/from_file as .json | |
| def __post_init__(self): | |
| self.samples = self.samples or [self.name] | |
| def match(self, utterance: str) -> bool: | |
| return any([s.lower() in utterance.lower() | |
| for s in self.samples]) | |
| @dataclass | |
| class KeywordIntent: | |
| name: str | |
| required: List[Keyword] | |
| optional: Optional[List[Keyword]] = None | |
| excludes: Optional[List[Keyword]] = None | |
| handler: Optional[IntentHandler] = None | |
| # TODO - to_file/from_file as .json | |
| def __post_init__(self): | |
| self.optional = self.optional or [] | |
| self.excludes = self.excludes or [] | |
| def score(self, utterance: str) -> float: | |
| if any([ent.match(utterance) for ent in self.excludes]): | |
| return 0.0 | |
| if not all([ent.match(utterance) for ent in self.required]): | |
| return 0.0 | |
| words = word_tokenize(utterance.lower()) | |
| n_required = 0 | |
| n_optional = 0 | |
| for ent in self.required: | |
| if ent.match(utterance): | |
| n_required += 1 | |
| for ent in self.optional: | |
| if ent.match(utterance): | |
| n_optional += 1 | |
| # score = (n_required_words + n_optional_words) / n_total_words | |
| return (len(self.required) + len(self.optional)) / len(words) | |
| class IntentEngine: | |
| """individual games may subclass this""" | |
| def __init__(self): | |
| self.intents: Dict[str, KeywordIntent] = {} | |
| # TODO - to_file/from_file as .json | |
| def calc_intents(self, utterance: str) -> List[Tuple[KeywordIntent, float]]: | |
| # calc intent + conf | |
| candidates = [] | |
| for name, intent in self.intents.items(): | |
| if any([ent.match(utterance) for ent in intent.excludes]): | |
| continue # no match! | |
| if all([ent.match(utterance) for ent in intent.required]): | |
| candidates.append(intent) # match! | |
| return sorted([(i, i.score(utterance)) for i in candidates], | |
| key=lambda k: k[1], reverse=True) | |
| def register_intent(self, intent: KeywordIntent): | |
| self.intents[intent.name] = intent | |
| def deregister_intent(self, name): | |
| if name in self.intents: | |
| self.intents.pop(name) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment