Spaces:
Paused
Paused
necdet.sayiner commited on
Commit ·
83f4c73
1
Parent(s): 5fe82f2
Add inventory system
Browse files- app.py +6 -1
- game/llm_engine.py +22 -5
- game/player.py +3 -0
- game/turn_engine.py +32 -15
- prompt/default_system.txt +17 -1
- systems/inventory_system.py +100 -0
app.py
CHANGED
|
@@ -133,7 +133,9 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 133 |
|
| 134 |
# 3. SEKME: ENVANTER
|
| 135 |
with gr.TabItem("🎒 Inventory"):
|
| 136 |
-
|
|
|
|
|
|
|
| 137 |
|
| 138 |
# 4. SEKME: JOURNAL
|
| 139 |
with gr.TabItem("📖 Journal"):
|
|
@@ -366,6 +368,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 366 |
chatbot, player_state,
|
| 367 |
light_dark_slider, order_chaos_slider,
|
| 368 |
scene_image, npc_image, item_image,
|
|
|
|
| 369 |
choice1, choice2, choice3, choice4, choice5
|
| 370 |
],
|
| 371 |
api_name=False
|
|
@@ -387,6 +390,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 387 |
chatbot, player_state,
|
| 388 |
light_dark_slider, order_chaos_slider,
|
| 389 |
scene_image, npc_image, item_image,
|
|
|
|
| 390 |
choice1, choice2, choice3, choice4, choice5
|
| 391 |
],
|
| 392 |
api_name=False
|
|
@@ -413,6 +417,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 413 |
chatbot, player_state,
|
| 414 |
light_dark_slider, order_chaos_slider,
|
| 415 |
scene_image, npc_image, item_image,
|
|
|
|
| 416 |
choice1, choice2, choice3, choice4, choice5
|
| 417 |
],
|
| 418 |
api_name=False
|
|
|
|
| 133 |
|
| 134 |
# 3. SEKME: ENVANTER
|
| 135 |
with gr.TabItem("🎒 Inventory"):
|
| 136 |
+
inventory_md = gr.Markdown(label="Inventory")
|
| 137 |
+
inventory_gallery = gr.Gallery(label="Items", columns=3, height="auto")
|
| 138 |
+
#inventory_display = gr.Markdown("*Your pockets are currently empty...*\n\n**Credits:** 0 🪙")
|
| 139 |
|
| 140 |
# 4. SEKME: JOURNAL
|
| 141 |
with gr.TabItem("📖 Journal"):
|
|
|
|
| 368 |
chatbot, player_state,
|
| 369 |
light_dark_slider, order_chaos_slider,
|
| 370 |
scene_image, npc_image, item_image,
|
| 371 |
+
inventory_md, inventory_gallery,
|
| 372 |
choice1, choice2, choice3, choice4, choice5
|
| 373 |
],
|
| 374 |
api_name=False
|
|
|
|
| 390 |
chatbot, player_state,
|
| 391 |
light_dark_slider, order_chaos_slider,
|
| 392 |
scene_image, npc_image, item_image,
|
| 393 |
+
inventory_md, inventory_gallery,
|
| 394 |
choice1, choice2, choice3, choice4, choice5
|
| 395 |
],
|
| 396 |
api_name=False
|
|
|
|
| 417 |
chatbot, player_state,
|
| 418 |
light_dark_slider, order_chaos_slider,
|
| 419 |
scene_image, npc_image, item_image,
|
| 420 |
+
inventory_md, inventory_gallery,
|
| 421 |
choice1, choice2, choice3, choice4, choice5
|
| 422 |
],
|
| 423 |
api_name=False
|
game/llm_engine.py
CHANGED
|
@@ -62,13 +62,26 @@ def generate_llm_stream(client, model_name, system_prompt, message, history, sta
|
|
| 62 |
<SCENE_DATA>
|
| 63 |
{{
|
| 64 |
"location_id": "a_unique_room_id",
|
| 65 |
-
"background_prompt": "Purely visual description
|
| 66 |
"encountered_npc": null,
|
| 67 |
-
"discovered_item": null
|
|
|
|
| 68 |
}}
|
| 69 |
</SCENE_DATA>
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
CRITICAL RULE 4: The ORDER is mandatory: story text → OPTIONS_DATA block → SCENE_DATA block. Never place SCENE_DATA before OPTIONS_DATA.
|
| 73 |
"""
|
| 74 |
|
|
@@ -91,7 +104,11 @@ def generate_llm_stream(client, model_name, system_prompt, message, history, sta
|
|
| 91 |
|
| 92 |
# LLM'in unutmasını engellemek için oyuncu mesajının sonuna zorunlu bir hatırlatıcı (injection) ekliyoruz.
|
| 93 |
# Bu metin UI'da oyuncuya yansımaz, sadece LLM'in aklını başına getirir.
|
| 94 |
-
tail_reminder =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
messages.append({"role": "user", "content": message + tail_reminder})
|
| 96 |
|
| 97 |
try:
|
|
|
|
| 62 |
<SCENE_DATA>
|
| 63 |
{{
|
| 64 |
"location_id": "a_unique_room_id",
|
| 65 |
+
"background_prompt": "Purely visual description of the environment",
|
| 66 |
"encountered_npc": null,
|
| 67 |
+
"discovered_item": null,
|
| 68 |
+
"credit_delta": 0
|
| 69 |
}}
|
| 70 |
</SCENE_DATA>
|
| 71 |
+
|
| 72 |
+
SCENE_DATA FIELD RULES:
|
| 73 |
+
- "encountered_npc": Replace null with {{"name": "...", "visual_prompt": "..."}} ONLY if a new named NPC appears in this scene.
|
| 74 |
+
- "discovered_item": Replace null with {{"name": "...", "visual_prompt": "..."}} ONLY if the player physically picks up or receives a tangible object (weapon, tool, artifact, etc.) in this turn. Do NOT set this for NPCs, locations, or abstract events.
|
| 75 |
+
- "credit_delta": An INTEGER representing how many credits the player gains (positive) or spends/loses (negative) in this turn due to trade, reward, theft, bribery, gambling, or any economic event. Set to 0 if no credits change hands. The player currently has {credits} credits — do not let them spend more than they own.
|
| 76 |
+
|
| 77 |
+
EXAMPLES of correct credit_delta usage:
|
| 78 |
+
Selling salvage to a merchant → "credit_delta": 75
|
| 79 |
+
Paying a docking fee → "credit_delta": -30
|
| 80 |
+
Winning a sabacc hand → "credit_delta": 200
|
| 81 |
+
No economic event → "credit_delta": 0
|
| 82 |
+
|
| 83 |
+
NOTE: ALL values inside SCENE_DATA (background_prompt, name, visual_prompt) MUST be written in ENGLISH ONLY.
|
| 84 |
+
CRITICAL RULE 3: The SCENE_DATA block is parsed by an image generation engine that only understands English.
|
| 85 |
CRITICAL RULE 4: The ORDER is mandatory: story text → OPTIONS_DATA block → SCENE_DATA block. Never place SCENE_DATA before OPTIONS_DATA.
|
| 86 |
"""
|
| 87 |
|
|
|
|
| 104 |
|
| 105 |
# LLM'in unutmasını engellemek için oyuncu mesajının sonuna zorunlu bir hatırlatıcı (injection) ekliyoruz.
|
| 106 |
# Bu metin UI'da oyuncuya yansımaz, sadece LLM'in aklını başına getirir.
|
| 107 |
+
tail_reminder = (
|
| 108 |
+
f"\n\n[CRITICAL SYSTEM REMINDER: You MUST conclude your response with BOTH data blocks in order: "
|
| 109 |
+
f"first <OPTIONS_DATA> (5 choices in {player_lang}), then <SCENE_DATA> (all values in English, "
|
| 110 |
+
f"including credit_delta as an integer). Do NOT skip either block. Do NOT place SCENE_DATA before OPTIONS_DATA.]"
|
| 111 |
+
)
|
| 112 |
messages.append({"role": "user", "content": message + tail_reminder})
|
| 113 |
|
| 114 |
try:
|
game/player.py
CHANGED
|
@@ -22,4 +22,7 @@ def create_player(name, gender, avatar, language):
|
|
| 22 |
"intelligence": 5,
|
| 23 |
"charisma": 5,
|
| 24 |
"willpower": 5,
|
|
|
|
|
|
|
|
|
|
| 25 |
}
|
|
|
|
| 22 |
"intelligence": 5,
|
| 23 |
"charisma": 5,
|
| 24 |
"willpower": 5,
|
| 25 |
+
# --- Inventory System ---
|
| 26 |
+
"credits": 150, # Başlangıç kredisi (Outer Rim standartlarında mütevazı bir miktar)
|
| 27 |
+
"inventory": [], # [{"name": str, "description": str, "image_path": str|None}, ...]
|
| 28 |
}
|
game/turn_engine.py
CHANGED
|
@@ -7,6 +7,7 @@ import re
|
|
| 7 |
from game.llm_engine import generate_llm_stream
|
| 8 |
from game.vlm_engine import extract_scene_data, generate_entity_image
|
| 9 |
from systems.alignment_system import evaluate_alignment
|
|
|
|
| 10 |
# ----------------------------
|
| 11 |
# GAME TURN
|
| 12 |
# ----------------------------
|
|
@@ -15,7 +16,7 @@ def extract_options_data(full_text, player_lang="English"):
|
|
| 15 |
# Kapanış etiketi olmasa da yakala; hem <OPTIONS_DATA> hem de <SCENE_DATA> arasını da dene
|
| 16 |
pattern = r"<OPTIONS_DATA>\s*(.*?)\s*(?:</OPTIONS_DATA>|<SCENE_DATA>|$)"
|
| 17 |
match = re.search(pattern, full_text, re.DOTALL | re.IGNORECASE)
|
| 18 |
-
|
| 19 |
# Hata durumunda devreye girecek varsayılan seçenekler (Fallback)
|
| 20 |
default_options = {
|
| 21 |
"English": ["Continue cautiously.", "Look around the area.", "Wait and observe.", "Try to communicate.", "Prepare for trouble."],
|
|
@@ -35,17 +36,21 @@ def extract_options_data(full_text, player_lang="English"):
|
|
| 35 |
# Butonlara sığması için metinleri biraz kırpabilirsin (örn: max 60 karakter)
|
| 36 |
return [opt[:60] + "..." if len(opt) > 60 else opt for opt in options]
|
| 37 |
else:
|
| 38 |
-
print(f"--- DEBUG ERROR [
|
| 39 |
except Exception as e:
|
| 40 |
-
print(f"--- DEBUG ERROR [
|
| 41 |
|
| 42 |
-
print(f"--- DEBUG ERROR [
|
| 43 |
return fallback
|
| 44 |
|
| 45 |
def play_turn(user_input, history, state_json, text_client, image_client, model_name, system_prompt, model_override=None):
|
| 46 |
"""
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
"""
|
| 50 |
if history is None:
|
| 51 |
history = []
|
|
@@ -72,12 +77,14 @@ def play_turn(user_input, history, state_json, text_client, image_client, model_
|
|
| 72 |
gr.update(), # scene_image (güncellenmiyor)
|
| 73 |
gr.update(), # npc_image (güncellenmiyor)
|
| 74 |
gr.update(), # item_image (güncellenmiyor)
|
|
|
|
|
|
|
| 75 |
# Butonları akış sırasında devre dışı bırakıp "..." yazıyoruz
|
| 76 |
gr.update(value="⏳ ...", interactive=False),
|
| 77 |
gr.update(value="⏳ ...", interactive=False),
|
| 78 |
gr.update(value="⏳ ...", interactive=False),
|
| 79 |
gr.update(value="⏳ ...", interactive=False),
|
| 80 |
-
gr.update(value="⏳ ...", interactive=False)
|
| 81 |
)
|
| 82 |
|
| 83 |
# 2. ALIGNMENT DEĞERLENDİRMESİ
|
|
@@ -91,7 +98,7 @@ def play_turn(user_input, history, state_json, text_client, image_client, model_
|
|
| 91 |
# 3. AŞAMA: JSON Verisini Çıkarma ve Görsel Üretimi
|
| 92 |
print(f"\n--- DEBUG: AI raw response ---\n{full_ai_response}\n------------------------------")
|
| 93 |
scene_data = extract_scene_data(full_ai_response)
|
| 94 |
-
|
| 95 |
# Görselleri varsayılan olarak boş başlatıyoruz, JSON başarıyla alındıysa görseli üretiyoruz
|
| 96 |
new_scene = None
|
| 97 |
new_npc = None
|
|
@@ -110,23 +117,33 @@ def play_turn(user_input, history, state_json, text_client, image_client, model_
|
|
| 110 |
item_data = scene_data.get("discovered_item")
|
| 111 |
if item_data and isinstance(item_data, dict):
|
| 112 |
new_item = generate_entity_image(image_client, "item", item_data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
player_lang = state.get("language", "English")
|
| 115 |
-
parsed_options = extract_options_data(full_ai_response, player_lang)
|
| 116 |
-
|
| 117 |
# 4. AŞAMA: Her Şeyi Döndür (Buton güncellemeleri dahil)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
yield (
|
| 119 |
history,
|
| 120 |
json.dumps(state),
|
| 121 |
state["light_dark"],
|
| 122 |
state["order_chaos"],
|
| 123 |
new_scene if new_scene else gr.update(),
|
| 124 |
-
new_npc
|
| 125 |
-
new_item
|
| 126 |
-
#
|
|
|
|
| 127 |
gr.update(value=parsed_options[0], interactive=True),
|
| 128 |
gr.update(value=parsed_options[1], interactive=True),
|
| 129 |
gr.update(value=parsed_options[2], interactive=True),
|
| 130 |
gr.update(value=parsed_options[3], interactive=True),
|
| 131 |
-
gr.update(value=parsed_options[4], interactive=True)
|
| 132 |
)
|
|
|
|
| 7 |
from game.llm_engine import generate_llm_stream
|
| 8 |
from game.vlm_engine import extract_scene_data, generate_entity_image
|
| 9 |
from systems.alignment_system import evaluate_alignment
|
| 10 |
+
from systems.inventory_system import add_item, update_credits, get_inventory_markdown, get_inventory_images
|
| 11 |
# ----------------------------
|
| 12 |
# GAME TURN
|
| 13 |
# ----------------------------
|
|
|
|
| 16 |
# Kapanış etiketi olmasa da yakala; hem <OPTIONS_DATA> hem de <SCENE_DATA> arasını da dene
|
| 17 |
pattern = r"<OPTIONS_DATA>\s*(.*?)\s*(?:</OPTIONS_DATA>|<SCENE_DATA>|$)"
|
| 18 |
match = re.search(pattern, full_text, re.DOTALL | re.IGNORECASE)
|
| 19 |
+
|
| 20 |
# Hata durumunda devreye girecek varsayılan seçenekler (Fallback)
|
| 21 |
default_options = {
|
| 22 |
"English": ["Continue cautiously.", "Look around the area.", "Wait and observe.", "Try to communicate.", "Prepare for trouble."],
|
|
|
|
| 36 |
# Butonlara sığması için metinleri biraz kırpabilirsin (örn: max 60 karakter)
|
| 37 |
return [opt[:60] + "..." if len(opt) > 60 else opt for opt in options]
|
| 38 |
else:
|
| 39 |
+
print(f"--- DEBUG ERROR [TURN ENGINE]: Options count mismatch: got {len(options) if isinstance(options, list) else 'non-list'}")
|
| 40 |
except Exception as e:
|
| 41 |
+
print(f"--- DEBUG ERROR [TURN ENGINE]: Options parse error: {e}\nRaw JSON string: {json_str[:200]}")
|
| 42 |
|
| 43 |
+
print(f"--- DEBUG ERROR [TURN ENGINE]: <OPTIONS_DATA> failed! Using fallback options.")
|
| 44 |
return fallback
|
| 45 |
|
| 46 |
def play_turn(user_input, history, state_json, text_client, image_client, model_name, system_prompt, model_override=None):
|
| 47 |
"""
|
| 48 |
+
Yields (in order):
|
| 49 |
+
history, state_json,
|
| 50 |
+
light_dark, order_chaos,
|
| 51 |
+
scene_image, npc_image, item_image,
|
| 52 |
+
inventory_markdown, inventory_gallery, ← YENİ
|
| 53 |
+
btn_0, btn_1, btn_2, btn_3, btn_4
|
| 54 |
"""
|
| 55 |
if history is None:
|
| 56 |
history = []
|
|
|
|
| 77 |
gr.update(), # scene_image (güncellenmiyor)
|
| 78 |
gr.update(), # npc_image (güncellenmiyor)
|
| 79 |
gr.update(), # item_image (güncellenmiyor)
|
| 80 |
+
gr.update(), # inventory_markdown (akış sırasında dokunmuyoruz)
|
| 81 |
+
gr.update(), # inventory_gallery
|
| 82 |
# Butonları akış sırasında devre dışı bırakıp "..." yazıyoruz
|
| 83 |
gr.update(value="⏳ ...", interactive=False),
|
| 84 |
gr.update(value="⏳ ...", interactive=False),
|
| 85 |
gr.update(value="⏳ ...", interactive=False),
|
| 86 |
gr.update(value="⏳ ...", interactive=False),
|
| 87 |
+
gr.update(value="⏳ ...", interactive=False),
|
| 88 |
)
|
| 89 |
|
| 90 |
# 2. ALIGNMENT DEĞERLENDİRMESİ
|
|
|
|
| 98 |
# 3. AŞAMA: JSON Verisini Çıkarma ve Görsel Üretimi
|
| 99 |
print(f"\n--- DEBUG: AI raw response ---\n{full_ai_response}\n------------------------------")
|
| 100 |
scene_data = extract_scene_data(full_ai_response)
|
| 101 |
+
|
| 102 |
# Görselleri varsayılan olarak boş başlatıyoruz, JSON başarıyla alındıysa görseli üretiyoruz
|
| 103 |
new_scene = None
|
| 104 |
new_npc = None
|
|
|
|
| 117 |
item_data = scene_data.get("discovered_item")
|
| 118 |
if item_data and isinstance(item_data, dict):
|
| 119 |
new_item = generate_entity_image(image_client, "item", item_data)
|
| 120 |
+
add_item(state, item_data, image_path=new_item)
|
| 121 |
+
|
| 122 |
+
# D. Kredi güncellemesi
|
| 123 |
+
credit_delta = scene_data.get("credit_delta", 0)
|
| 124 |
+
if isinstance(credit_delta, (int, float)) and credit_delta != 0:
|
| 125 |
+
update_credits(state, int(credit_delta))
|
| 126 |
|
|
|
|
|
|
|
|
|
|
| 127 |
# 4. AŞAMA: Her Şeyi Döndür (Buton güncellemeleri dahil)
|
| 128 |
+
player_lang = state.get("language", "English")
|
| 129 |
+
parsed_options = extract_options_data(full_ai_response, player_lang)
|
| 130 |
+
|
| 131 |
+
inventory_md = get_inventory_markdown(state)
|
| 132 |
+
inventory_images = get_inventory_images(state)
|
| 133 |
+
|
| 134 |
yield (
|
| 135 |
history,
|
| 136 |
json.dumps(state),
|
| 137 |
state["light_dark"],
|
| 138 |
state["order_chaos"],
|
| 139 |
new_scene if new_scene else gr.update(),
|
| 140 |
+
new_npc if new_npc else gr.update(),
|
| 141 |
+
new_item if new_item else gr.update(),
|
| 142 |
+
inventory_md, # inventory_markdown Markdown bileşenini güncelle
|
| 143 |
+
inventory_images, # inventory_gallery Gallery bileşenini güncelle
|
| 144 |
gr.update(value=parsed_options[0], interactive=True),
|
| 145 |
gr.update(value=parsed_options[1], interactive=True),
|
| 146 |
gr.update(value=parsed_options[2], interactive=True),
|
| 147 |
gr.update(value=parsed_options[3], interactive=True),
|
| 148 |
+
gr.update(value=parsed_options[4], interactive=True),
|
| 149 |
)
|
prompt/default_system.txt
CHANGED
|
@@ -25,4 +25,20 @@ The player is FREE to choose their destiny. They do not have to be a hero. They
|
|
| 25 |
- Deep relationships, including romantic encounters, are possible. These should develop naturally over time through shared experiences, not instantly.
|
| 26 |
|
| 27 |
[ALIGNMENT REACTIVITY]
|
| 28 |
-
- Keep the player's unseen Light/Dark and Order/Chaos alignment in mind. How the galaxy reacts to them, and the type of people drawn to them, should shift based on their brutal or compassionate actions.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
- Deep relationships, including romantic encounters, are possible. These should develop naturally over time through shared experiences, not instantly.
|
| 26 |
|
| 27 |
[ALIGNMENT REACTIVITY]
|
| 28 |
+
- Keep the player's unseen Light/Dark and Order/Chaos alignment in mind. How the galaxy reacts to them, and the type of people drawn to them, should shift based on their brutal or compassionate actions.
|
| 29 |
+
|
| 30 |
+
[ECONOMY & CREDITS]
|
| 31 |
+
- The player's current credit balance is provided in the Player State. Always respect it — never let the player spend more credits than they own.
|
| 32 |
+
- Credits (₡) are the standard currency of the galaxy. Prices reflect the gritty Outer Rim economy; nothing is cheap, nothing is free.
|
| 33 |
+
- REALISTIC PRICE RANGES (rough guide):
|
| 34 |
+
- Street food / cheap drink: ₡2–10
|
| 35 |
+
- Night in a flophouse: ₡15–40
|
| 36 |
+
- Passage on a freighter (short hop): ₡80–300
|
| 37 |
+
- Black-market blaster pistol: ₡150–400
|
| 38 |
+
- Bacta patch / field medkit: ₡50–120
|
| 39 |
+
- Docking fee (small vessel): ₡25–80
|
| 40 |
+
- Bribe to a low-ranking Imperial: ₡50–200
|
| 41 |
+
- Bounty payout (minor job): ₡200–800
|
| 42 |
+
- Rare artifact / lightsaber crystal: ₡500–2000+
|
| 43 |
+
- WHEN TO CHANGE CREDITS: Only set a non-zero credit_delta in SCENE_DATA when a concrete economic transaction actually occurs in the narrative (trade, reward, payment, theft, gambling win/loss, bribe, etc.). Do NOT invent spurious transactions just to fill the field — 0 is perfectly valid.
|
| 44 |
+
- Reflect economic changes naturally in your prose: mention the clink of credits changing hands, the weight of a payment, the sting of a loss.
|
systems/inventory_system.py
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ----------------------------
|
| 2 |
+
# INVENTORY & CREDITS SYSTEM
|
| 3 |
+
# ----------------------------
|
| 4 |
+
|
| 5 |
+
def add_item(state, item_data, image_path=None):
|
| 6 |
+
"""
|
| 7 |
+
Keşfedilen eşyayı player state'ine ekler.
|
| 8 |
+
|
| 9 |
+
Parametreler:
|
| 10 |
+
state : Mevcut oyuncu state dict'i (doğrudan güncellenir, referans üzerinden).
|
| 11 |
+
item_data : LLM'den gelen dict — en az {"name": "...", "visual_prompt": "..."} içermeli.
|
| 12 |
+
image_path: VLM tarafından üretilen görselin disk yolu (opsiyonel).
|
| 13 |
+
|
| 14 |
+
Dönüş: Güncellenmiş state.
|
| 15 |
+
"""
|
| 16 |
+
if "inventory" not in state:
|
| 17 |
+
state["inventory"] = []
|
| 18 |
+
|
| 19 |
+
item_name = item_data.get("name", "Unknown Item").strip()
|
| 20 |
+
|
| 21 |
+
# --- Duplicate koruması (aynı isimde eşya ekleme) ---
|
| 22 |
+
existing_names = {item["name"].lower() for item in state["inventory"]}
|
| 23 |
+
if item_name.lower() in existing_names:
|
| 24 |
+
print(f"--- DEBUG INFO [INVENTORY]: '{item_name}' already in inventory, skipping duplicate.")
|
| 25 |
+
return state
|
| 26 |
+
|
| 27 |
+
entry = {
|
| 28 |
+
"name": item_name,
|
| 29 |
+
"description": item_data.get("visual_prompt", ""),
|
| 30 |
+
"image_path": image_path, # None ise galeri bu item'ı atlar
|
| 31 |
+
}
|
| 32 |
+
state["inventory"].append(entry)
|
| 33 |
+
print(f"--- DEBUG INFO [INVENTORY]: '{item_name}' added. Total items: {len(state['inventory'])}")
|
| 34 |
+
return state
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def update_credits(state, delta):
|
| 38 |
+
"""
|
| 39 |
+
Oyuncunun kredilerini delta kadar artırır veya azaltır.
|
| 40 |
+
Bakiye asla negatife düşmez (min 0).
|
| 41 |
+
|
| 42 |
+
Parametreler:
|
| 43 |
+
state: Oyuncu state dict'i.
|
| 44 |
+
delta: int — pozitif (kazanç) ya da negatif (harcama).
|
| 45 |
+
|
| 46 |
+
Dönüş: Güncellenmiş state.
|
| 47 |
+
"""
|
| 48 |
+
if "credits" not in state:
|
| 49 |
+
state["credits"] = 0 # Güvenlik: state'de yoksa sıfırdan başlat
|
| 50 |
+
|
| 51 |
+
old_balance = state["credits"]
|
| 52 |
+
state["credits"] = max(0, old_balance + delta)
|
| 53 |
+
direction = "earned" if delta >= 0 else "spent"
|
| 54 |
+
print(
|
| 55 |
+
f"--- DEBUG INFO [INVENTORY]: Credits {direction}: {delta:+d} | "
|
| 56 |
+
f"{old_balance} → {state['credits']} credits"
|
| 57 |
+
)
|
| 58 |
+
return state
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# ----------------------------
|
| 62 |
+
# UI HELPERS
|
| 63 |
+
# ----------------------------
|
| 64 |
+
|
| 65 |
+
def get_inventory_markdown(state):
|
| 66 |
+
"""
|
| 67 |
+
Gradio Markdown bileşenine verilecek envanter metnini üretir.
|
| 68 |
+
Kredi bakiyesi + eşya listesi.
|
| 69 |
+
"""
|
| 70 |
+
inventory = state.get("inventory", [])
|
| 71 |
+
credits = state.get("credits", 0)
|
| 72 |
+
|
| 73 |
+
lines = [f"## 💰 {credits} Credits\n"]
|
| 74 |
+
|
| 75 |
+
if not inventory:
|
| 76 |
+
lines.append("*Your pack is empty. The galaxy has yet to offer you anything worth keeping.*")
|
| 77 |
+
else:
|
| 78 |
+
lines.append(f"### 🎒 Inventory — {len(inventory)} item(s)\n")
|
| 79 |
+
for i, item in enumerate(inventory, start=1):
|
| 80 |
+
lines.append(f"**{i}. {item['name']}**")
|
| 81 |
+
desc = item.get("description", "")
|
| 82 |
+
if desc:
|
| 83 |
+
short_desc = desc if len(desc) <= 90 else desc[:87] + "…"
|
| 84 |
+
lines.append(f"*{short_desc}*")
|
| 85 |
+
lines.append("") # boş satır (görsel aralık)
|
| 86 |
+
|
| 87 |
+
return "\n".join(lines)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def get_inventory_images(state):
|
| 91 |
+
"""
|
| 92 |
+
Gradio Gallery bileşenine verilecek (image_path, caption) tuple listesini döndürür.
|
| 93 |
+
image_path'i None olan eşyalar atlanır.
|
| 94 |
+
"""
|
| 95 |
+
inventory = state.get("inventory", [])
|
| 96 |
+
return [
|
| 97 |
+
(item["image_path"], item["name"])
|
| 98 |
+
for item in inventory
|
| 99 |
+
if item.get("image_path")
|
| 100 |
+
]
|