diff --git a/community-contributions/dungeon_extraction_game/README.md b/community-contributions/dungeon_extraction_game/README.md new file mode 100644 index 0000000..55f76be --- /dev/null +++ b/community-contributions/dungeon_extraction_game/README.md @@ -0,0 +1,138 @@ +# The Neural Nexus + + + +TODO: + +* Set boundaries to user inputs. +* Add sounds to the scene +* Add voice acting for the Game master's descriptions. +* Add voice input. +* Use video for the final scene: escape or death. +* Generate a score based on total treasures, exp gained and deep reached. + +## Requirements + +AI services access configuration: + +* A `.env` file with the credentials required to access the different LLMs is required: + + * `OPENAI_API_KEY`: Required always as it's used by the *"storyteller"*. + * `XAI_API_KEY`: Required if Grok's illustrator is used. + *(Less prude, faster and portrait mode)* + * `GOOGLE_API_KEY` Required if Gemini's illustrator is used. + + Obviously the used services must have been topped up with a small amount to generate + the responses and the images.\ + *Refer to each service's current billing information.* + +There are 6 variant implementations for the illustrator component, some of them may have +additional dependencies: + +* `illustrator_dalle_2`: *(Set as default)* + + The Dall·E 2 implementation uses standard OpenAI client and should work out of the box. + Although Dall·E has proven to be a bit prude and rejects to draw some combat scenes. + +* `illustrator_dalle_3`: + + The Dall·E 3 implementation uses standard OpenAI client and should work out of the box. + Although Dall·E has proven to be a bit prude and rejects to draw some combat scenes. + This version gives noticeable better images than Dall·E 2 but at an increased cost + +* `illustrator_grok`: + + The Grok 2 Image implementation uses standard OpenAI client and should work out of the + box. + It's faster but does not support quality or size controls. + + Images are generated in a *portrait mode*, so it fits specially well on mobiles. + + Grok is much less prude with violence and may draw combat scenes, at least against + fantasy enemies, and blood. + +* `illustrator_gpt`: + + The GPT Image illustrator uses standard OpenAI client, should work out of the box but + it requires the user to be verified on OpenAI platform to have access to it. + +* `illustrator_gemini` + + The Gemini illustrator uses the new Google SDK, `genai`, which replaces the old one + used on the course, `generativeai`, this new one can be installed with: + + `python -m pip install google-genai` + + *Both `generativeai` and `genai` can be installed at the same time without problems* + +* `illustrator_grok_x` + + The Grok_X illustrator uses the xAI SDK, `xai-sdk`, this can be installed with: + + `python -m pip install xai-sdk` + +## Configuring the service and game + +All services and game values can be set at `config.py` file. + +Setting the `DRAW_FUNCTION` to `None` will disable the image generation and a fixed +image will be used. + +## Game launch + +The game can be launch from terminal, just navigate to game's root folder + +* `cd community-contributions\dungeon_extraction_game` + +and run the following command: + +* `python -m game`\ + *Notice the `-m` is required due to the project's structure and import strategy.* + +Game will take a few seconds to set up service and configure then logs will start to +show, among them the service address. + +It will attempt to launch your default browser directly to the game's page. + +The game can be stopped by hitting `ctrl + c` on the same terminal. + +## Playing the game + +Once on the browser the Starting screen will be shown: + +![The Chair](images/start_view.jpg) + +There you should input the kind of game you want to play on the lower box and submit. + +Your input can be as simple as a single word, like “spaceship”, or as detailed as you +like. + +![Set the adventure](images/start_input.jpg) + +From that point on, only your imagination (and the Storyteller’s) will set the limits. + +Once submitted, the image will update to reflect the scene, accompanied by a description, +your inventory, your adventurer’s status, and sometimes a few suggestions for what to do +next. + +![R'lyeh arrive](images/start_adventure.jpg) + +Although the game begins in English, if you switch to another language the Storyteller +understands, it will seamlessly continue in that language. + +You’re free to type any action you want, the Storyteller will adapt. +Still, it’s instructed to keep the world coherent, so don’t expect to go completely off +the rails. + +![Adventurer acts](images/first_input.jpg) + +The game continues this way + +![Adventurer dies](images/advance_adventure.jpg) + +Until you either escape with your treasures... +or meet your end. + +![Adventurer dies](images/tragic_end.jpg) + +The cling the bottom button to start over a new game. diff --git a/community-contributions/dungeon_extraction_game/game/__init__.py b/community-contributions/dungeon_extraction_game/game/__init__.py new file mode 100644 index 0000000..a70d9a4 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/__init__.py @@ -0,0 +1,18 @@ +"""AI Mastered Dungeon Extraction Game initialization module.""" + +from logging import basicConfig, getLogger + +from dotenv import load_dotenv + + +# Environment initialization. +load_dotenv(override=True) + +# Setup the global logger. +LOG_STYLE = '{' +LOG_LEVEL = 'INFO' +LOG_FORMAT = ('{asctime} {levelname:<8} {processName}({process}) ' + '{threadName} {name} {lineno} "{message}"') +basicConfig(level=LOG_LEVEL, style='{', format=LOG_FORMAT) + +getLogger(__name__).info('INITIALIZED GAME LOGGER') diff --git a/community-contributions/dungeon_extraction_game/game/__main__.py b/community-contributions/dungeon_extraction_game/game/__main__.py new file mode 100644 index 0000000..c166c6c --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/__main__.py @@ -0,0 +1,15 @@ +"""AI Mastered Dungeon Extraction Game main entrypoint module.""" + +from logging import getLogger + +from .config import GAME_CONFIG, UI_CONFIG +from .gameplay import get_gameplay_function +from .interface import get_interface + + +_logger = getLogger(__name__) + +if __name__ == '__main__': + _logger.info('STARTING GAME...') + gameplay_function = get_gameplay_function(GAME_CONFIG) + get_interface(gameplay_function, UI_CONFIG).launch(inbrowser=True, inline=False) diff --git a/community-contributions/dungeon_extraction_game/game/config.py b/community-contributions/dungeon_extraction_game/game/config.py new file mode 100644 index 0000000..8216c1f --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/config.py @@ -0,0 +1,189 @@ +"""AI Mastered Dungeon Extraction Game Configuration module.""" + +from logging import getLogger + +from dotenv import load_dotenv + +from .gameplay import Gameplay_Config +from .illustrator import draw_dalle_2, draw_dalle_3, draw_gemini, draw_gpt, draw_grok +from .illustrator import draw_grok_x +from .interface import Interface_Config +from .storyteller import narrate, set_description_limit + + +# Environment initialization. +load_dotenv(override=True) + + +# Choose draw function. +# Choose one from the imported ones up there or set to None to disable images. +DRAW_FUNCTION = draw_dalle_2 + +# Define a sample scene description for testing purposes. +SAMPLE_SCENE = '''A shadow-drenched chamber lies buried deep within the bowels of an +ancient castle, its silence broken only by the faint creak of age-old stone. +The walls, cloaked in thick cobwebs, seem to whisper forgotten stories, +while the air hangs heavy with the pungent scent of mildew and centuries of decay. +Dust dances in the dim light that filters through cracks in the ceiling, +casting eerie patterns across the cold floor. As your eyes adjust to the gloom, +you notice a narrow door to the north, slightly ajar, as if inviting or warning, and +in the far corner, half-swallowed by darkness, a figure stands motionless. +Its presence is felt before it's seen, watching, waiting''' + +# Define the starting scene text. +# This is intentionally excluded from the model’s narrative context, the 'history', +# by design, to prevent potential leakage into the game’s storyline. +START_SCENE = '''You stand before the Neural Nexus, a convergence of arcane circuitry +and deep cognition. It doesn't operate with buttons or commands. It responds to intent. + +Forged in forgotten labs and powered by living code, the Nexus is designed to interface +directly with your mind. Not to simulate reality, but to generate it. +The Nexus does not load worlds. It listens. + +If you choose to sit, the Nexus will initiate full neural synchronization. +Your thoughts will become terrain. Your instincts, adversaries. +Your imagination, the architect. + +Once the link is active, you must describe the nature of the challenge you wish to face. +A shifting maze? A sentient machine? A trial of memory and time? +Speak it aloud or think it clearly. The Nexus will listen. + +🜁 When you're ready, take your seat. The system awaits your signal...''' + +# Define an image prompt, mind that Grok or Dalle·2 models have a 1024 characters limit. +SCENE_PROMPT = '''Render a detailed image of the following scene: + +"""{scene_description}""" + +Stay strictly faithful to the description, no added elements, characters, doors, or text. +Do not depict the adventurer; show only what they see. + +Use the "{scene_style}" visual style. +''' + +# Define the scene drawing style, can be a simple word or a short sentence. +SCENE_STYLE = 'Photorealistic' + +# Set a Storyteller scene descriptions size limit to keep the draw prompt in range. +STORYTELLER_LIMIT = 730 +set_description_limit(STORYTELLER_LIMIT) # Need to patch pydantic class model. + +# Define the storyteller behaviour. Remember to specify a limited scene length. +STORYTELLER_PROMPT = f""" +You are a conversational dungeon crawler game master that describes scenes and findings +based on the player's declared actions. + +Your descriptions will always adhere to the OpenAI's safety system rules so they can be +drawn by Dall·E or other image models. + +The game start with the player, the adventurer, on a random room and the objetive is +escape the dungeon with the most treasures possible before dying. + +You will describe the environment, enemies, and items to the player. + +Your descriptions will always adhere to the OpenAI's safety system rules so they can be +drawn by Dall·E or other image models. + +You will ensure the game is engaging and fun, but at the same time risky by increasing +difficult the more the time the adventurer stays inside the dungeon, if the adventurer +takes too much risks he may even die, also bigger risks implies bigger rewards. + +You will control the time the adventurer is in, once enough time has passer he will die, +may it be a collapse, explosion, flooding, up to you. + +The more deep inside the adventurer is the most it will be represented on descriptions by +more suffocating environments, more dark, that kind of things, let the player feel the +risk on the ambience, make him fear. + +Same applies with time, the most time has passed the environment and situation will warn +him, or at least give clues that time is running and the end may be close soon, make him +stress. + +While leaving the dungeon, the more deep inside the adventurer is, the more steps must +take to get out, although some shortcuts may be available at your discretion. +Once the user exits the dungeon, at deepness zero, the game is over, give him a score +based on his actions, treasures and combat successes along the usual description. + +Don't be too much protective but not also a cruel master, just be fair. + +Your responses must always be a JSON with the following structure: + +{{ + "game_over" : "A boolean value indicating the game is over." + "scene_description" : "The detailed scene description. Max {STORYTELLER_LIMIT} chars" + "dungeon_deepness" : "How deep the adventurer has gone into the dungeon. initially 3" + "adventure_time" : "How much minutes has passed since the start of the adventure." + "adventurer_status" : {{ + "health": "Current health of the adventurer as an int, initially 100" + "max_health": "Maximum health of the adventurer as an int, initially 100" + "level": "Current adventurer's leve as an int, initially 1" + "experience": "Current adventurer experience as an int, initially 0"}} + "inventory_status" : "A list of inventory items, initially empty" +}} + +Remember to cap the "scene_description" to {STORYTELLER_LIMIT} characters maximum" + +You will respond to the adventurer's actions and choices. +You wont let the player to trick you by stating actions that do not fit the given scene. + * If he attempts to do so just politely tell him he can not do that there with the + description of the scene he is in. + +You will keep track of the adventurer's health. + * Health can go down due to combat, traps, accidents, etc. + * If Health reaches zero the adventurer dies and it's a "game over". + * Several items, places, and allowed actions may heal the adventurer. + * Some items, enchants, and such things may increase the adventurer's maximum health. + +You will keep track of the player's progress. +You will keep track of adventurer level and experience, + * He gains experience by finding items, solving puzzles, by combat with enemies, etc. + * Each (100 + 100 * current_level) experience the adventurer will gain a level. + * Gaining a level resets his experience to 0. + +You will keep track of the player's inventory. + * Only add items to inventory if user explicitly says he picks them or takes an + action that ends with the item on his possession. + * Inventory items will reflect quantity and will never display items with zero units. + * Example of inventory: ["Gold coins (135)", "Diamonds (2)", "Log sword (1)"] + * Be reasonable with the inventory capacity, don't bee to strict but things + like a big marble statue can't be taken, use common sense. + +You will use a turn-based system where the player and enemies take turns acting. + * Players will lose health when receiving hits on combat. + * The more damage they take the less damage they do, same applies to enemies. + * Reaching to zero health or lees implies the adventurer has die. +""" + +# Configure the game. +GAME_CONFIG = Gameplay_Config( + draw_func=DRAW_FUNCTION, + narrate_func=narrate, + scene_style=SCENE_STYLE, + scene_prompt=SCENE_PROMPT, + storyteller_prompt=STORYTELLER_PROMPT, + disable_img='images/disabled.jpg', + error_img='images/machine.jpg', + error_narrator='NEURAL SINAPSIS ERROR\n\n{ex}\n\nEND OF LINE\n\nRE-SUBMIT_', + error_illustrator='NEURAL PROJECTION ERROR\n\n{ex}\n\nEND OF LINE\n\nRE-SUBMIT_',) + +# Configure the interface. +UI_CONFIG = Interface_Config( + start_img='images/chair.jpg', + place_img='images/machine.jpg', + description_label='Cognitive Projection', + title_label='The Neural Nexus', + input_button='Imprint your will', + input_label='Cognitive Imprint', + input_command='Awaiting neural imprint…', + game_over_field='Game Over', + game_over_label='Disengage Neural Links', + start_scene=START_SCENE) + + +_logger = getLogger(__name__) + +# Log scene prompt length calculation. +if (max_image_prompt := len(SCENE_PROMPT) + len(SCENE_STYLE) + STORYTELLER_LIMIT) > 1024: + _logger.warning(f'ESTIMATED SCENE PROMPT MAX SIZE: {max_image_prompt}') +else: + _logger.info(f'ESTIMATED SCENE PROMPT MAX SIZE: {max_image_prompt}') diff --git a/community-contributions/dungeon_extraction_game/game/gameplay/__init__.py b/community-contributions/dungeon_extraction_game/game/gameplay/__init__.py new file mode 100644 index 0000000..c2d3d4c --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/gameplay/__init__.py @@ -0,0 +1,6 @@ +"""AI Mastered Dungeon Extraction Game gameplay package.""" + +from .gameplay import Gameplay_Config, get_gameplay_function + + +__all__ = ['Gameplay_Config', 'get_gameplay_function'] diff --git a/community-contributions/dungeon_extraction_game/game/gameplay/gameplay.py b/community-contributions/dungeon_extraction_game/game/gameplay/gameplay.py new file mode 100644 index 0000000..9a95362 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/gameplay/gameplay.py @@ -0,0 +1,61 @@ +"""AI Mastered Dungeon Extraction Game gameplay module.""" + +from logging import getLogger +from typing import Callable, NamedTuple + + +# Define gameplay's configuration class. +class Gameplay_Config(NamedTuple): + """Gradio interface configuration class.""" + draw_func: Callable + narrate_func: Callable + scene_style: str + scene_prompt: str + storyteller_prompt: str + disable_img: str + error_img: str + error_narrator: str + error_illustrator: str + + +# Define Game's functions. + +def get_gameplay_function(config: Gameplay_Config): + """Return a pre-configured turn gameplay function.""" + def gameplay_function(message, history): + """Generate Game Master's response and draw the scene image.""" + # Request narration. + _logger.info(f'NARRATING SCENE...') + try: + response = config.narrate_func(message, history, config.storyteller_prompt) + except Exception as ex: + scene = config.error_img + response = config.error_narrator.format(ex=ex) + _logger.error(f'ERROR NARRATING SCENE: {ex}\n{message}\n{history}') + return scene, response, history, message + # Update history. + history.append({"role": "user", "content": message}) + history.append({"role": "assistant", "content": response.model_dump_json()}) + # Draw scene. + if config.draw_func: + _logger.info(f'DRAWING SCENE...') + try: + scene_data = {'scene_description': response.scene_description, + 'scene_style': config.scene_style} + scene_prompt = config.scene_prompt.format(**scene_data) + _logger.info(f'PROMPT BODY IS: \n\n{scene_prompt}\n') + _logger.info(f'PROMPT LENGTH IS: {len(scene_prompt)}') + scene = config.draw_func(scene_prompt) + except Exception as ex: + scene = config.error_img + response = config.error_illustrator.format(ex=ex) + _logger.warning(f'ERROR DRAWING SCENE: {ex}') + return scene, response, history, '' + else: + _logger.info(f'DRAWING DISABLED...') + scene = config.disable_img + return scene, response, history, '' + return gameplay_function + + +_logger = getLogger(__name__) diff --git a/community-contributions/dungeon_extraction_game/game/illustrator/__init__.py b/community-contributions/dungeon_extraction_game/game/illustrator/__init__.py new file mode 100644 index 0000000..608db8e --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/illustrator/__init__.py @@ -0,0 +1,12 @@ +"""AI Mastered Dungeon Extraction Game scenes illustrator package.""" + +from .illustrator_dalle_2 import draw as draw_dalle_2 +from .illustrator_dalle_3 import draw as draw_dalle_3 +from .illustrator_gemini import draw as draw_gemini +from .illustrator_gpt import draw as draw_gpt +from .illustrator_grok import draw as draw_grok +from .illustrator_grok import draw_x as draw_grok_x + + +__all__ = ['draw_dalle_2', 'draw_dalle_3', 'draw_gemini', + 'draw_gpt', 'draw_grok', 'draw_grok_x'] diff --git a/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_dalle_2.py b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_dalle_2.py new file mode 100644 index 0000000..7269d7b --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_dalle_2.py @@ -0,0 +1,30 @@ +"""AI Mastered Dungeon Extraction Game scenes illustrator using OpenAI's DALL·E 3.""" + +import base64 +from io import BytesIO + +from dotenv import load_dotenv +from openai import OpenAI +from PIL import Image + + +# Environment initialization. +load_dotenv(override=True) + +# Define global defaults. +MODEL = 'dall-e-2' + +# Client instantiation. +CLIENT = OpenAI() + + +# Function definition. +def draw(prompt, size=(1024, 1024), client=CLIENT, model=MODEL, quality=None): + """Generate an image based on the prompt.""" + # Generate image. + response = client.images.generate( + model=model, prompt=prompt, n=1, + size=f'{size[0]}x{size[1]}', + response_format='b64_json') + # Process response. + return Image.open(BytesIO(base64.b64decode(response.data[0].b64_json))) diff --git a/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_dalle_3.py b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_dalle_3.py new file mode 100644 index 0000000..87f8051 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_dalle_3.py @@ -0,0 +1,32 @@ +"""AI Mastered Dungeon Extraction Game scenes illustrator using OpenAI's DALL·E 3.""" + +import base64 +from io import BytesIO + +from dotenv import load_dotenv +from openai import OpenAI +from PIL import Image + + +# Environment initialization. +load_dotenv(override=True) + +# Define global defaults. +MODEL = 'dall-e-3' +QUALITY = 'standard' # Set to 'hd' for more quality, but double the costs. + +# Client instantiation. +CLIENT = OpenAI() + + +# Function definition. +def draw(prompt, size=(1024, 1024), client=CLIENT, model=MODEL, quality=QUALITY): + """Generate an image based on the prompt.""" + # Generate image. + response = client.images.generate( + model=model, prompt=prompt, n=1, + size=f'{size[0]}x{size[1]}', + quality=quality, + response_format='b64_json') + # Process response. + return Image.open(BytesIO(base64.b64decode(response.data[0].b64_json))) diff --git a/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_gemini.py b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_gemini.py new file mode 100644 index 0000000..e586944 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_gemini.py @@ -0,0 +1,36 @@ +"""AI Mastered Dungeon Extraction Game scenes illustrator using Google's Gemini.""" + +from io import BytesIO + +from dotenv import load_dotenv +from google import genai # New Google's SDK 'genai' to replace 'generativeai'. +from PIL import Image + + +# Environment initialization. +load_dotenv(override=True) + +# Define globals. +MODEL = 'gemini-2.5-flash-image-preview' + +# Client instantiation. +CLIENT = genai.Client() + + +# Function definition. +def draw(prompt, size=(1024, 1024), client=CLIENT, model=MODEL): + """Generate an image based on the prompt.""" + # Generate image. + response = client.models.generate_content( + model=model, contents=[prompt]) + # Process response. + for part in response.candidates[0].content.parts: + if part.text is not None: + print(part.text) + elif part.inline_data is not None: + image_data = part.inline_data.data + # Open the generated image. + generated_image = Image.open(BytesIO(image_data)) + # Resize the image to the specified dimensions. + resized_image = generated_image.resize(size) + return resized_image diff --git a/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_gpt.py b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_gpt.py new file mode 100644 index 0000000..ae8b9f4 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_gpt.py @@ -0,0 +1,32 @@ +"""AI Mastered Dungeon Extraction Game scenes illustrator using OpenAI's GPT.""" + +import base64 +from io import BytesIO + +from dotenv import load_dotenv +from openai import OpenAI +from PIL import Image + + +# Environment initialization. +load_dotenv(override=True) + +# Define global defaults. +MODEL = 'gpt-image-1' +QUALITY = 'low' + +# Client instantiation. +CLIENT = OpenAI() + + +# Function definition. +def draw(prompt, size=(1024, 1024), client=CLIENT, model=MODEL, quality=QUALITY): + """Generate an image based on the prompt.""" + # Generate image. + response = client.images.generate( + model=model, prompt=prompt, n=1, + size=f'{size[0]}x{size[1]}', + output_format='png', + quality=quality) + # Process response. + return Image.open(BytesIO(base64.b64decode(response.data[0].b64_json))) diff --git a/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_grok.py b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_grok.py new file mode 100644 index 0000000..417af7a --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/illustrator/illustrator_grok.py @@ -0,0 +1,47 @@ +"""AI Mastered Dungeon Extraction Game scenes illustrator using xAI's Grok.""" + +import base64 +import os +from io import BytesIO + +from dotenv import load_dotenv +from openai import OpenAI +from PIL import Image +from xai_sdk import Client + + +# Environment initialization. +load_dotenv(override=True) + +# Define global defaults. +MODEL = 'grok-2-image' +QUALITY = None + +# Client instantiation. +XAI_API_KEY = os.getenv('XAI_API_KEY') +CLIENT = OpenAI(api_key=XAI_API_KEY, base_url="https://api.x.ai/v1") + + +# Function definition. +def draw(prompt, size=(1024, 1024), client=CLIENT, model=MODEL, quality=QUALITY): + """Generate an image based on the prompt.""" + # Generate image. + response = client.images.generate( + model=model, prompt=prompt, n=1, + response_format='b64_json') + # Process response. + return Image.open(BytesIO(base64.b64decode(response.data[0].b64_json))) + + +# xAI SDK Version: +CLIENT_X = Client(api_key=XAI_API_KEY) + + +def draw_x(prompt, size=(1024, 1024), client=CLIENT_X, model=MODEL, quality=QUALITY): + """Generate an image based on the prompt.""" + # Generate image. + response = client.image.sample( + model=model, prompt=prompt, + image_format='base64') + # Process response. + return Image.open(BytesIO(response.image)) diff --git a/community-contributions/dungeon_extraction_game/game/interface/__init__.py b/community-contributions/dungeon_extraction_game/game/interface/__init__.py new file mode 100644 index 0000000..b680128 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/interface/__init__.py @@ -0,0 +1,6 @@ +"""AI Mastered Dungeon Extraction Game interface package.""" + +from .interface import Interface_Config, get_interface + + +__all__ = ['Interface_Config', 'get_interface'] diff --git a/community-contributions/dungeon_extraction_game/game/interface/interface.py b/community-contributions/dungeon_extraction_game/game/interface/interface.py new file mode 100644 index 0000000..a327de4 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/interface/interface.py @@ -0,0 +1,94 @@ +"""AI Mastered Dungeon Extraction Game Gradio interface module.""" + +from typing import NamedTuple + +import gradio as gr +from logging import getLogger + + +# Define interface's configuration class. +class Interface_Config(NamedTuple): + """Gradio interface configuration class.""" + start_img: str + place_img: str + description_label: str + title_label: str + input_button: str + input_label: str + input_command: str + game_over_field: str + game_over_label: str + start_scene: str + + +# Define game's interface. +def get_interface(submit_function, config: Interface_Config): + """Create a game interface service.""" + with gr.Blocks(title=config.title_label) as ui: + # Title. + gr.Markdown(config.title_label) + # Hidden state for history. + history_state = gr.State([]) + # Scene's image. + scene_image = gr.Image( + label="Scene", value=config.start_img, placeholder=config.place_img, + type="pil", show_label=False) + # Scene's description. + description_box = gr.Textbox( + label=config.description_label, value=config.start_scene, + interactive=False, show_copy_button=True) + # Player's command. + user_input = gr.Textbox( + label=config.input_label, placeholder=config.input_command) + # Submit button. + submit_btn = gr.Button(config.input_button) + + # Define Game Over control. + + def _reset_game(): + """Return Initial values for game restart.""" + return (config.start_img, config.start_scene, [], '', + gr.update(interactive=True), + gr.update(value=config.input_button)) + + def _game_over(scene, response): + """Return Game Over values, blocking input field.""" + return (scene, response, [], config.game_over_field, + gr.update(interactive=False), + gr.update(value=config.game_over_label)) + + def game_over_wrap(message, history, button_label): + """Check Game over status Before and After Storyteller call.""" + # Check game over before. + print(button_label) + print(config.game_over_label) + if button_label == config.game_over_label: + _logger.warning('GAME OVER STATUS. RESTARTING...') + return _reset_game() + # Call Storyteller. + scene, response, history, input = submit_function(message, history) + _logger.warning(response) + # Check game over after. + if response.game_over: + _logger.info('GAME OVER AFTER MOVE. LOCKING.') + return _game_over(scene, response) + # Return Storyteller response. + return scene, response, history, input, gr.update(), gr.update() + + # Assign function to button click event. + submit_btn.click( + fn=game_over_wrap, + inputs=[user_input, history_state, submit_btn], + outputs=[scene_image, description_box, history_state, user_input, + user_input, submit_btn]) + # Assign function to input submit event. (Press enter) + user_input.submit( + fn=game_over_wrap, + inputs=[user_input, history_state, submit_btn], + outputs=[scene_image, description_box, history_state, user_input, + user_input, submit_btn]) + + return ui + + +_logger = getLogger(__name__) diff --git a/community-contributions/dungeon_extraction_game/game/storyteller/__init__.py b/community-contributions/dungeon_extraction_game/game/storyteller/__init__.py new file mode 100644 index 0000000..92d6739 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/storyteller/__init__.py @@ -0,0 +1,6 @@ +"""AI Mastered Dungeon Extraction Game Storyteller package.""" + +from .storyteller import narrate, set_description_limit + + +__all__ = ['narrate', 'set_description_limit'] diff --git a/community-contributions/dungeon_extraction_game/game/storyteller/storyteller.py b/community-contributions/dungeon_extraction_game/game/storyteller/storyteller.py new file mode 100644 index 0000000..4cae135 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/storyteller/storyteller.py @@ -0,0 +1,72 @@ +"""AI Mastered Dungeon Extraction Game Storyteller using OpenAI's GPT.""" + +from typing import List + +from annotated_types import MaxLen +from dotenv import load_dotenv +from openai import OpenAI +from pydantic import BaseModel, Field + +from .tools import handle_tool_call, tools + + +# Environment initialization. +load_dotenv(override=True) + +# Define globals. +MODEL = 'gpt-4o-mini' + +# Client instantiation. +CLIENT = OpenAI() + + +# Define Pydantic model classes for response format parsing. +class _character_sheet(BaseModel): + health: int + max_health: int + level: int + experience: int + + +class _response_format(BaseModel): + game_over: bool + scene_description: str = Field(..., max_length=700) + dungeon_deepness: int + adventure_time: int + adventurer_status: _character_sheet + inventory_status: List[str] + + def __str__(self): + """Represent response as a string.""" + response_view = ( + f'{self.scene_description}' + f'\n\nInventory: {self.inventory_status}' + f'\n\nAdventurer: {self.adventurer_status}' + f'\n\nTime: {self.adventure_time}' + f'\n\nDeepness: {self.dungeon_deepness}' + f'\n\nGame Over: {self.game_over}') + return response_view + + +def set_description_limit(limit): # HBD: We modify the class definition in runtime. + """Update "_response_format" class to set a new "scene_description" max length.""" + _response_format.model_fields['scene_description'].metadata[0] = MaxLen(limit) + + +# Function definition. +def narrate(message, history, system_message, client=CLIENT, model=MODEL): + """Chat with the game engine.""" + messages = ([{"role": "system", "content": system_message}] + history + + [{"role": "user", "content": message}]) + response = client.chat.completions.parse(model=model, messages=messages, tools=tools, + response_format=_response_format) + # Process tool calls. + if response.choices[0].finish_reason == "tool_calls": + message = response.choices[0].message + tool_response = handle_tool_call(message) + messages.append(message) + messages.append(tool_response) + response = client.chat.completions.parse(model=model, messages=messages, + response_format=_response_format) + # Return game's Master response. + return response.choices[0].message.parsed diff --git a/community-contributions/dungeon_extraction_game/game/storyteller/tools.py b/community-contributions/dungeon_extraction_game/game/storyteller/tools.py new file mode 100644 index 0000000..a995ec0 --- /dev/null +++ b/community-contributions/dungeon_extraction_game/game/storyteller/tools.py @@ -0,0 +1,81 @@ +"""AI Mastered Dungeon Extraction Game storyteller tools module WIP.""" + +from json import loads + +from openai.types.chat import ChatCompletionMessage +from openai.types.chat import ChatCompletionMessageFunctionToolCall +from openai.types.chat.chat_completion_message_function_tool_call import Function + + +# Tools declaration for future use. (E.g. Tools may handle user status and inventory) +tools = [] + +tools_map = {} # This will map each tool with it's tool function. + + +# A tool call function. +def handle_tool_call(message: ChatCompletionMessage): + """Tools call handler.""" + tool_call = message.tool_calls[0] + arguments = loads(tool_call.function.arguments) + print(f'\nFUNC CALL: {tool_call.function.name}({arguments})\n') + # Get tool function and call with arguments. + tool_func = tools_map.get(tool_call.function.name) + tool_response = tool_func(**arguments) + response = {"role": "tool", "content": tool_response, "tool_call_id": tool_call.id} + return response + + +draw_signature = { + "name": "draw_scene", + "description": "Generate an image of the scene based on the description", + "parameters": { + "type": "object", + "properties": { + "scene_description": { + "type": "string", + "description": "A detailed description of the scene to be drawn", + }, + "scene_style": { + "type": "string", + "description": "The art style for the image", + }, + }, + "required": ["scene_description"], + "additionalProperties": False, + }, +} + + +# Tool call response example. +ChatCompletionMessage( + content="""To begin, first I need to set a scene. + Imagine you are in a dark room of an old castle. + The walls are covered in cobwebs and there is a smell of mold in the air. + As you look around, you notice a slightly ajar door to the north + and a dark figure lurking in the corner. + + I am going to generate an image of this scene. One moment, please.""", + refusal=None, + role="assistant", + annotations=[], + audio=None, + function_call=None, + tool_calls=[ + ChatCompletionMessageFunctionToolCall( + id="call_oJqJeXMUPZUaC0GPfMeSd16E", + function=Function( + arguments='''{ + "scene_description":"A dark room in an ancient castle. + The walls are covered with cobwebs, and there\'s a musty smell in + the air. + A slightly ajar door to the north and a shadowy figure lurking in + the corner. + Dim lighting adds to the eerie atmosphere, with flickering shadows.", + "style":"fantasy" + }''', + name="draw_scene"), + type="function", + ) + ], +) diff --git a/community-contributions/dungeon_extraction_game/images/advance_adventure.jpg b/community-contributions/dungeon_extraction_game/images/advance_adventure.jpg new file mode 100644 index 0000000..c1821d1 Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/advance_adventure.jpg differ diff --git a/community-contributions/dungeon_extraction_game/images/chair.jpg b/community-contributions/dungeon_extraction_game/images/chair.jpg new file mode 100644 index 0000000..701daaa Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/chair.jpg differ diff --git a/community-contributions/dungeon_extraction_game/images/disabled.jpg b/community-contributions/dungeon_extraction_game/images/disabled.jpg new file mode 100644 index 0000000..2fac5f2 Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/disabled.jpg differ diff --git a/community-contributions/dungeon_extraction_game/images/first_input.jpg b/community-contributions/dungeon_extraction_game/images/first_input.jpg new file mode 100644 index 0000000..c71a447 Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/first_input.jpg differ diff --git a/community-contributions/dungeon_extraction_game/images/machine.jpg b/community-contributions/dungeon_extraction_game/images/machine.jpg new file mode 100644 index 0000000..1bbc849 Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/machine.jpg differ diff --git a/community-contributions/dungeon_extraction_game/images/start_adventure.jpg b/community-contributions/dungeon_extraction_game/images/start_adventure.jpg new file mode 100644 index 0000000..4d280a2 Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/start_adventure.jpg differ diff --git a/community-contributions/dungeon_extraction_game/images/start_input.jpg b/community-contributions/dungeon_extraction_game/images/start_input.jpg new file mode 100644 index 0000000..71c7438 Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/start_input.jpg differ diff --git a/community-contributions/dungeon_extraction_game/images/start_view.jpg b/community-contributions/dungeon_extraction_game/images/start_view.jpg new file mode 100644 index 0000000..d277d1d Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/start_view.jpg differ diff --git a/community-contributions/dungeon_extraction_game/images/tragic_end.jpg b/community-contributions/dungeon_extraction_game/images/tragic_end.jpg new file mode 100644 index 0000000..5484ad6 Binary files /dev/null and b/community-contributions/dungeon_extraction_game/images/tragic_end.jpg differ