Merge pull request #644 from Carbaz/feature/ai-mastered-dungeon-game

AI Mastered dungeon extraction game.
This commit is contained in:
Ed Donner
2025-09-08 22:08:15 +01:00
committed by GitHub
26 changed files with 875 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
# The Neural Nexus
<!-- ![The horse](images/chair.jpg) -->
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 Storytellers) will set the limits.
Once submitted, the image will update to reflect the scene, accompanied by a description,
your inventory, your adventurers 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.
Youre free to type any action you want, the Storyteller will adapt.
Still, its instructed to keep the world coherent, so dont 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.

View File

@@ -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')

View File

@@ -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)

View File

@@ -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 models narrative context, the 'history',
# by design, to prevent potential leakage into the games 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}')

View File

@@ -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']

View File

@@ -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__)

View File

@@ -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']

View File

@@ -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)))

View File

@@ -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)))

View File

@@ -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

View File

@@ -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)))

View File

@@ -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))

View File

@@ -0,0 +1,6 @@
"""AI Mastered Dungeon Extraction Game interface package."""
from .interface import Interface_Config, get_interface
__all__ = ['Interface_Config', 'get_interface']

View File

@@ -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__)

View File

@@ -0,0 +1,6 @@
"""AI Mastered Dungeon Extraction Game Storyteller package."""
from .storyteller import narrate, set_description_limit
__all__ = ['narrate', 'set_description_limit']

View File

@@ -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

View File

@@ -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",
)
],
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB