Implement NPC memory and interaction system with SQLite database; add NPC data structure and dynamic NPC handling; integrate LLM for NPC conversations and quest generation.

This commit is contained in:
2025-10-02 11:05:33 +02:00
parent 7e76353c6a
commit a34ba3e6f6
21 changed files with 12669 additions and 37790 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+74
View File
@@ -0,0 +1,74 @@
NPC_DATABASE = {
"barkeep_boris": {
"personality": "Jovial and gossipy, knows everyone's business",
"backstory": "Retired adventurer who settled down after losing party to dragon",
"quirks": ["Offers free drinks to good storytellers", "Hates elves"],
"relationships": {
"mad_wizard": "Finds his stories amusing, sometimes annoyed by his antics",
"blacksmith_greta": "Old friends, often shares rumors",
"mysterious_stranger": "Suspicious, keeps an eye on them",
"village_healer": "Respects her, sometimes flirts",
"young_thief": "Lets him steal food, pretends not to notice"
}
},
"mad_wizard": {
"personality": "Eccentric and forgetful, brilliant but scattered",
"backstory": "Expelled from wizard college for 'creative' spellcasting",
"quirks": ["Speaks to imaginary familiar", "Offers dangerous experimental potions"],
"relationships": {
"barkeep_boris": "Enjoys his company, shares magical gossip",
"blacksmith_greta": "Wants her to forge magical items, she refuses",
"mysterious_stranger": "Curious, tries to uncover their secrets",
"village_healer": "Respects her knowledge of herbs",
"young_thief": "Occasionally hires for odd errands"
}
},
"blacksmith_greta": {
"personality": "Gruff but fair, takes pride in her work",
"backstory": "Inherited the forge from her father, dreams of crafting legendary weapons",
"quirks": ["Talks to her hammer", "Never removes her apron"],
"relationships": {
"barkeep_boris": "Drinks together after work, trusts him",
"mad_wizard": "Annoyed by his requests, but intrigued",
"mysterious_stranger": "Doesn't trust them, keeps her distance",
"village_healer": "Respects her, sometimes repairs her tools",
"young_thief": "Chased him out of her shop more than once"
}
},
"mysterious_stranger": {
"personality": "Cryptic, speaks in riddles, always watching",
"backstory": "No one knows where they came from or what they want",
"quirks": ["Disappears when you look away", "Knows everyone's secrets"],
"relationships": {
"barkeep_boris": "Knows he is watching, uses him for information",
"mad_wizard": "Finds him amusing, but unpredictable",
"blacksmith_greta": "Avoids her, she asks too many questions",
"village_healer": "Respects her kindness, sometimes leaves gifts",
"young_thief": "Keeps an eye on him, sees potential"
}
},
"village_healer": {
"personality": "Kind, patient, and wise beyond her years",
"backstory": "Learned the healing arts from a traveling monk",
"quirks": ["Collects rare herbs", "Refuses payment for healing"],
"relationships": {
"barkeep_boris": "Enjoys his stories, sometimes worries about his health",
"mad_wizard": "Helps him with potions, tries to keep him out of trouble",
"blacksmith_greta": "Good friends, shares herbal remedies",
"mysterious_stranger": "Curious, senses a hidden pain",
"young_thief": "Treats his wounds, tries to guide him"
}
},
"young_thief": {
"personality": "Cheeky, quick-witted, always looking for trouble",
"backstory": "Grew up on the streets, steals to survive",
"quirks": ["Has a pet mouse", "Always hungry"],
"relationships": {
"barkeep_boris": "Grateful for his kindness, sometimes helps out",
"mad_wizard": "Finds him weird, but likes his tricks",
"blacksmith_greta": "Afraid of her, but admires her strength",
"mysterious_stranger": "Wants to impress, but is wary",
"village_healer": "Trusts her, sees her as a mother figure"
}
}
}
+100
View File
@@ -0,0 +1,100 @@
import json
import random
import requests
from .npc_data import NPC_DATABASE # <-- Add this import
def query_llm(prompt, model="neural-chat", url="http://100.103.117.14:11434/api/generate"):
# Add instruction for short answers
short_prompt = prompt + "\n\nKeep your answer short and concise (1-2 sentences)."
payload = {
"model": model,
"prompt": short_prompt,
"stream": False
}
try:
response = requests.post(url, json=payload, timeout=120)
response.raise_for_status()
data = response.json()
return data.get("response", "").strip()
except Exception as e:
print(f"LLM error: {e}")
return None
class DynamicNPC:
def __init__(self, name, data):
self.name = name
self.personality = data["personality"]
self.backstory = data["backstory"]
self.quirks = data["quirks"]
class NPCHandler:
def __init__(self, memory):
self.memory = memory
self.npcs = {name: DynamicNPC(name, data) for name, data in NPC_DATABASE.items()}
def chat_with_npc(self, npc_name, message, player_context):
npc = self.npcs.get(npc_name)
if not npc:
return "That NPC doesn't exist."
player_id = player_context['id']
# Fetch recent conversation history (last 3 exchanges)
history = self.memory.get_conversation(npc_name, player_id)
history_str = ""
if history:
history_str = "\nRecent conversation:\n"
for player_msg, npc_reply in history[-3:]:
history_str += f"Player: {player_msg}\n{npc.name}: {npc_reply}\n"
# Fetch affinity
affinity = self.memory.get_affinity(npc_name, player_id)
affinity_str = f"Affinity with player: {affinity}\n"
prompt = (
f"You are {npc.name}, an NPC in a fantasy world.\n"
f"Personality: {npc.personality}\n"
f"Backstory: {npc.backstory}\n"
f"Quirks: {', '.join(npc.quirks)}\n"
f"{history_str}"
f"{affinity_str}"
f"Player (level {player_context.get('level', 1)}): {message}\n"
f"Respond in character as {npc.name}."
)
llm_response = query_llm(prompt)
if not llm_response:
llm_response = f"{npc.name} seems lost in thought and doesn't reply."
self.memory.log_conversation(npc_name, player_id, message, llm_response)
self.memory.update_affinity(npc_name, player_id, 1)
return llm_response
def generate_quest(self, npc_name, player_level):
npc = self.npcs.get(npc_name)
if not npc:
return None
prompt = (
f"As {npc.name}, create a short quest for a level {player_level} adventurer.\n"
f"Personality: {npc.personality}\n"
f"Backstory: {npc.backstory}\n"
f"Format as JSON:\n"
"{{\n"
' "title": "Quest name",\n'
' "description": "What the player must do",\n'
' "reward": "coins/items/reputation",\n'
' "difficulty": "easy/medium/hard"\n'
"}}"
)
llm_response = query_llm(prompt)
try:
if not llm_response:
raise ValueError("No response from LLM")
quest = json.loads(llm_response)
return quest
except Exception:
quest = {
"title": f"{npc.name}'s Request",
"description": f"Help {npc.name} with a task suitable for level {player_level}.",
"reward": f"{random.randint(10, 100)} coins",
"difficulty": random.choice(["easy", "medium", "hard"])
}
return quest
+73
View File
@@ -0,0 +1,73 @@
import sqlite3
class NPCMemory:
def __init__(self):
self.conn = sqlite3.connect('npc_memory.db')
self.create_tables()
def create_tables(self):
self.conn.execute('''
CREATE TABLE IF NOT EXISTS npc_conversations (
npc_id TEXT,
player_id TEXT,
message TEXT,
response TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
self.conn.execute('''
CREATE TABLE IF NOT EXISTS npc_relationships (
npc_id TEXT,
player_id TEXT,
affinity INTEGER DEFAULT 0,
last_interaction DATETIME
)
''')
self.conn.commit()
def log_conversation(self, npc_id, player_id, message, response):
self.conn.execute(
'INSERT INTO npc_conversations (npc_id, player_id, message, response) VALUES (?, ?, ?, ?)',
(npc_id, player_id, message, response)
)
self.conn.commit()
def update_affinity(self, npc_id, player_id, delta):
cur = self.conn.cursor()
cur.execute('SELECT affinity FROM npc_relationships WHERE npc_id=? AND player_id=?', (npc_id, player_id))
row = cur.fetchone()
if row:
new_affinity = row[0] + delta
cur.execute('UPDATE npc_relationships SET affinity=?, last_interaction=CURRENT_TIMESTAMP WHERE npc_id=? AND player_id=?',
(new_affinity, npc_id, player_id))
else:
cur.execute('INSERT INTO npc_relationships (npc_id, player_id, affinity, last_interaction) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
(npc_id, player_id, delta))
self.conn.commit()
def get_conversation(self, npc_id, player_id, limit=3):
"""Return the last `limit` (default 3) (player_msg, npc_reply) tuples for this NPC/player."""
cursor = self.conn.execute(
'''
SELECT message, response FROM npc_conversations
WHERE npc_id = ? AND player_id = ?
ORDER BY timestamp DESC
LIMIT ?
''',
(npc_id, player_id, limit)
)
# Return in chronological order (oldest first)
rows = cursor.fetchall()
return rows[::-1]
def get_affinity(self, npc_id, player_id):
"""Return the affinity value for this NPC/player, or 0 if not set."""
cursor = self.conn.execute(
'''
SELECT affinity FROM npc_relationships
WHERE npc_id = ? AND player_id = ?
''',
(npc_id, player_id)
)
row = cursor.fetchone()
return row[0] if row else 0