Add dungeon game code.

This commit is contained in:
Carlos Bazaga
2025-09-05 19:30:29 +02:00
parent 804ac62e78
commit c1fbb54a5a
16 changed files with 667 additions and 0 deletions

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,43 @@
"""AI Mastered Dungeon Extraction Game main entrypoint module."""
from logging import getLogger
from .config import SCENE_PROMPT, SCENE_STYLE, START_SCENE, STORYTELLER_PROMPT
from .gameplay import Gameplay_Config, get_gameplay_function
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, get_interface
from .storyteller import narrate
# Choose draw function.
DRAW_FUNCTION = draw_dalle_2
# 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,
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_',)
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…',
start_scene=START_SCENE)
_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,146 @@
"""AI Mastered Dungeon Extraction Game Configuration module."""
from logging import getLogger
# 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
# 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.
"""
_logger = getLogger(__name__)
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,55 @@
"""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
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.
_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, ''
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,50 @@
"""AI Mastered Dungeon Extraction Game Gradio interface module."""
from typing import NamedTuple
import gradio as gr
# 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
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, show_copy_button=True)
# 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)
user_input.submit(
fn=submit_function,
inputs=[user_input, history_state],
outputs=[scene_image, description_box, history_state, user_input])
# Submit button.
submit_btn = gr.Button(config.input_button)
submit_btn.click(
fn=submit_function,
inputs=[user_input, history_state],
outputs=[scene_image, description_box, history_state, user_input])
return ui

View File

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

View File

@@ -0,0 +1,67 @@
"""AI Mastered Dungeon Extraction Game Storyteller using OpenAI's GPT."""
from typing import List
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field
from ..config import STORYTELLER_LIMIT
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=STORYTELLER_LIMIT)
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
# 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",
)
],
)