necdet.sayiner commited on
Commit
83f4c73
·
1 Parent(s): 5fe82f2

Add inventory system

Browse files
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
- inventory_display = gr.Markdown("*Your pockets are currently empty...*\n\n**Credits:** 0 🪙")
 
 
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
- NOTE: If there is a new NPC or Item in the scene, replace null with a JSON object containing "name" and "visual_prompt" keys.
71
- CRITICAL RULE 3: The SCENE_DATA block is parsed by an image generation engine that only understands English. ALL values inside the SCENE_DATA block (background_prompt, name, visual_prompt) MUST be written in ENGLISH ONLY, regardless of the game language.
 
 
 
 
 
 
 
 
 
 
 
 
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 = "\n\n[CRITICAL SYSTEM REMINDER: You MUST conclude your response with BOTH data blocks in order: first <OPTIONS_DATA> (5 choices in {player_lang}), then <SCENE_DATA> (in English). Do NOT skip either block. Do NOT place SCENE_DATA before OPTIONS_DATA.]"
 
 
 
 
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 [LLM ENGINE]: Options count mismatch: got {len(options) if isinstance(options, list) else 'non-list'}")
39
  except Exception as e:
40
- print(f"--- DEBUG ERROR [LLM ENGINE]: Options parse error: {e}\nRaw JSON string: {json_str[:200]}")
41
 
42
- print(f"--- DEBUG ERROR [LLM ENGINE]: <OPTIONS_DATA> failed! Using fallback options. {e} ---")
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
- model_name : Fallback model ID (app.py'den gelir, artık çoğunlukla kullanılmaz)
48
- model_override: UI dropdown'dan seçilen geçici model ID'si (opsiyonel)
 
 
 
 
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 if new_npc else gr.update(),
125
- new_item if new_item else gr.update(),
126
- # Butonları yeni metinlerle ve tıklanabilir (interactive) halde döndür
 
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
+ ]