diff --git a/backend_kobold_cpp.yaml b/backend_kobold_cpp.yaml
index 683b6adc..ca72b1f2 100644
--- a/backend_kobold_cpp.yaml
+++ b/backend_kobold_cpp.yaml
@@ -5,4 +5,5 @@ JSON_GRAMMAR_KEY: "grammar"
STREAM_ENDPOINT: "/api/extra/generate/stream"
DATA_ENDPOINT: "/api/extra/generate/check"
DEFAULT_BODY: '{"stop_sequence": "", "max_length":1500, "max_context_length":4096, "temperature":0.5, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
-GENERATION_BODY: '{"stop_sequence": "", "max_length":1500, "max_context_length":4096, "temperature":1.0, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
\ No newline at end of file
+GENERATION_BODY: '{"stop_sequence": "", "max_length":1500, "max_context_length":4096, "temperature":1.0, "top_k":120, "top_a":0.0, "top_p":0.85, "typical_p":1.0, "tfs":1.0, "rep_pen":1.2, "rep_pen_range":256, "sampler_order":[6,0,1,3,4,2,5], "seed":-1}'
+API_PASSWORD: "" # if koboldcpp is run with the --password flag, this must be set to the same password
\ No newline at end of file
diff --git a/llm_config.yaml b/llm_config.yaml
index 5b915c40..b1f3ac9d 100644
--- a/llm_config.yaml
+++ b/llm_config.yaml
@@ -19,7 +19,7 @@ ITEM_TYPES: ["Weapon", "Wearable", "Health", "Money", "Trash"]
PRE_PROMPT: 'You are a creative game keeper for an interactive fiction story telling session. You craft detailed worlds and interesting characters with unique and deep personalities for the player to interact with. Always follow the instructions given, never acknowledge the task or speak directly to the user or respond with anything besides the request.'
BASE_PROMPT: '{context}\n[USER_START] Rewrite [{input_text}] in your own words. The information inside the tags should be used to ensure it fits the story. Use about {max_words} words.'
DIALOGUE_PROMPT: '{context}\nThe following is a conversation between {character1} and {character2}; {character2}s sentiment towards {character1}: {sentiment}. Write a single response as {character2} in third person pov, using {character2} description and other information found inside the tags. If {character2} has a quest active, they will discuss it based on its status. Respond in JSON using this template: """{dialogue_template}""". [USER_START]Continue the following conversation as {character2}: {previous_conversation}'
-COMBAT_PROMPT: '{context}\nThe following is a combat scene between {attackers} and {defenders} in {location}. [USER_START] Describe the following combat result in about 150 words in vivid language, using the characters weapons and their health status: 1.0 is highest, 0.0 is lowest. Combat Result: {input_text}'
+COMBAT_PROMPT: '{context}\nThe following is a combat scene between {attackers} and {defenders} in {location}. [USER_START] Describe the following combat result in about 150 words in vivid language, using the characters weapons and describe their health status without mentioning numbers: 1.0 is highest, 0.0 is dead. {input_text}'
PRE_JSON_PROMPT: 'Below is an instruction that describes a task, paired with an input that provides further context. Write a response in valid JSON format that appropriately completes the request.'
CREATE_CHARACTER_PROMPT: '{context}\n[USER_START] Create a diverse character with rich personality that can be interacted with using the story context and keywords. {{quest_prompt}} Do not mention height. Keywords: {keywords}. Fill in the blanks in this JSON template and write nothing else: {character_template}'
CREATE_LOCATION_PROMPT: '{context}\nZone info: {zone_info}; Exit json example: {exit_template}; Npc or mob example: {npc_template}. Existing connected locations: {exit_locations}. [USER_START] Using the information supplied inside the tags, describe the following location: {location_name}. {items_prompt} {spawn_prompt} Add a brief description, and one to three additional exits leading to new locations. Fill in this JSON template and do not write anything else: {location_template}. Write the response in valid JSON.'
diff --git a/tale/base.py b/tale/base.py
index 49dc9fc3..bb0c2634 100644
--- a/tale/base.py
+++ b/tale/base.py
@@ -440,6 +440,7 @@ def __init__(self, name: str, title: str = "", *, descr: str = "", short_descr:
self.rent = 0.0 # price to keep in store / day
self.weight = 0.0 # some abstract unit
self.takeable = True # can this item be taken/picked up?
+ self.durability = 100 # how long can this item last?
super().__init__(name, title=title, descr=descr, short_descr=short_descr)
def init(self) -> None:
@@ -459,6 +460,7 @@ def to_dict(self) -> Dict[str, Any]:
"rent": self.rent,
"weight": self.weight,
"takeable": self.takeable,
+ "durability": self.durability,
"location" : self.location.name if self.location else ''
}
diff --git a/tale/cmds/wizard.py b/tale/cmds/wizard.py
index 4ea94a5e..2b427f57 100644
--- a/tale/cmds/wizard.py
+++ b/tale/cmds/wizard.py
@@ -904,9 +904,3 @@ def do_create_item(player: Player, parsed: base.ParseResult, ctx: util.Context)
player.tell(item.name + ' added.', evoke=False)
else:
raise ParseError("Item could not be added")
-
-@wizcmd("restart_story")
-def do_restart(player: Player, parsed: base.ParseResult, ctx: util.Context) -> None:
- """Restart the game."""
- player.tell("Restarting the game... Please reconnect")
- os.execv(sys.executable, ['python3'] + sys.argv)
\ No newline at end of file
diff --git a/tale/combat.py b/tale/combat.py
index f585a57e..5a7eee6d 100644
--- a/tale/combat.py
+++ b/tale/combat.py
@@ -52,6 +52,11 @@ def _calculate_armor_bonus(self, actor: 'base.Living', body_part: WearLocation =
return wearable.ac if wearable else 1
return actor.stats.ac + 1
+ def _subtract_armor_durability(self, actor: 'base.Living', body_part: WearLocation, amount: int):
+ wearable = actor.get_wearable(body_part)
+ if wearable:
+ wearable.durability -= amount
+
def resolve_body_part(self, defender: 'base.Living', size_factor: float, target_part: WearLocation = None) -> WearLocation:
""" Resolve the body part that was hit. """
body_parts = body_parts_for_bodytype(defender.stats.bodytype)
@@ -94,49 +99,55 @@ def resolve_attack(self) -> str:
for attacker in self.attackers:
random_defender = random.choice(self.defenders)
text_result, damage_to_defender = self._round(attacker, random_defender)
- texts.extend(text_result)
+ texts.append(text_result)
random_defender.stats.hp -= damage_to_defender
if random_defender.stats.hp < 1:
- texts.append(f'{random_defender.title} dies')
+ texts.append(f'{random_defender.title} dies from their injuries.')
for defender in self.defenders:
+ if defender.stats.hp < 1:
+ continue
random_attacker = random.choice(self.attackers)
text_result, damage_to_attacker = self._round(defender, random_attacker)
- texts.extend(text_result)
+ texts.append(text_result)
random_attacker.stats.hp -= damage_to_attacker
if random_attacker.stats.hp < 1:
- texts.append(f'{random_attacker.title} dies')
+ texts.append(f'{random_attacker.title} dies from their injuries.')
- return ', '.join(texts)
+ return '\n'.join(texts)
def _round(self, actor1: 'base.Living', actor2: 'base.Living') -> Tuple[List[str], int]:
attack_result = self._calculate_attack_success(actor1)
- texts = []
+ attack_text = f'{actor1.title} attacks {actor2.title} with their {actor1.wielding.name}'
if attack_result < 0:
if attack_result < -actor1.stats.weapon_skills.get(actor1.wielding.type) + 5:
- texts.append(f'{actor1.title} performs a critical hit on {actor2.title}')
+ attack_text += ' and hits critically'
block_result = 100
else:
- texts.append(f'{actor1.title} hits {actor2.title}')
+ attack_text += ' and hits'
block_result = self._calculate_block_success(actor1, actor2)
-
+
if block_result < 0:
- texts.append(f'but {actor2.title} blocks')
+ attack_text += f', but {actor2.title} blocks with their {actor2.wielding.name}'
+ actor2.wielding.durability -= random.randint(1, 10)
else:
- actor1_strength = self._calculate_weapon_bonus(actor1) * actor1.stats.size.order
+ actor1_attack = self._calculate_weapon_bonus(actor1) * actor1.stats.size.order
body_part = self.resolve_body_part(actor2, actor1.stats.size.order / actor2.stats.size.order, target_part=self.target_body_part)
- actor2_strength = self._calculate_armor_bonus(actor2, body_part) * actor2.stats.size.order
- damage_to_defender = int(max(0, actor1_strength - actor2_strength))
+ actor2_defense = self._calculate_armor_bonus(actor2, body_part) * actor2.stats.size.order
+ damage_to_defender = int(max(0, actor1_attack - actor2_defense))
if damage_to_defender > 0:
- texts.append(f', {actor2.title} is injured in the {body_part.name.lower()}')
+ attack_text += f', and {actor2.title} is injured in the {body_part.name.lower()}.'
+ elif actor1_attack < actor2_defense:
+ attack_text += f', but {actor2.title}\' armor protects them.'
+ self._subtract_armor_durability(actor2, body_part, actor1_attack)
else:
- texts.append(f', {actor2.title} is unharmed')
- return texts, damage_to_defender
+ attack_text += f', but {actor2.title} is unharmed.'
+ return attack_text, damage_to_defender
elif attack_result > 50:
- texts.append(f'{actor1.title} misses {actor2.title} completely')
+ attack_text + f', but misses completely.'
elif attack_result > 25:
- texts.append(f'{actor1.title} misses {actor2.title}')
+ attack_text + f', but misses.'
else:
- texts.append(f'{actor1.title} barely misses {actor2.title}')
- return texts, 0
+ attack_text + f', and barely misses.'
+ return attack_text, 0
diff --git a/tale/json_story.py b/tale/json_story.py
index 866a89b4..cffb02c3 100644
--- a/tale/json_story.py
+++ b/tale/json_story.py
@@ -1,4 +1,4 @@
-from tale import load_items
+from tale import load_items, wearable
from tale.items import generic
from tale.llm.dynamic_story import DynamicStory
from tale.player import Player
@@ -34,6 +34,8 @@ def init(self, driver) -> None:
self._catalogue._creatures = world['catalogue']['creatures']
if world['catalogue']['items']:
self._catalogue._items = world['catalogue']['items']
+ if world['catalogue'].get('wearables', None):
+ wearable.add_story_wearables(world['catalogue']['wearables'])
if world.get('world', None):
if world['world']['items']:
# Keep this so that saved items in worlds will transfer to locations. But don't save them.
diff --git a/tale/llm/io_adapters.py b/tale/llm/io_adapters.py
index 7cbb4a2a..41f569d7 100644
--- a/tale/llm/io_adapters.py
+++ b/tale/llm/io_adapters.py
@@ -22,7 +22,7 @@ def __init__(self, url: str, stream_endpoint: str, user_start_prompt: str = '',
self.prompt_end = prompt_end
@abstractmethod
- def stream_request(self, request_body: dict, io = None, wait: bool = False) -> str:
+ def stream_request(self, headers: dict, request_body: dict, io = None, wait: bool = False) -> str:
pass
@abstractmethod
@@ -44,8 +44,8 @@ def __init__(self, url: str, stream_endpoint: str, data_endpoint: str, user_star
self.data_endpoint = data_endpoint
self.place_context_in_memory = False
- def stream_request(self, request_body: dict, io: PlayerConnection = None, wait: bool = False) -> str:
- result = asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, request_body))
+ def stream_request(self, headers: dict, request_body: dict, io: PlayerConnection = None, wait: bool = False) -> str:
+ result = asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, headers, request_body))
try:
if result:
@@ -54,9 +54,10 @@ def stream_request(self, request_body: dict, io: PlayerConnection = None, wait:
print("Error parsing response from backend - ", exc)
return ''
- async def _do_stream_request(self, url: str, request_body: dict,) -> bool:
+ async def _do_stream_request(self, url: str, headers: dict, request_body: dict,) -> bool:
""" Send request to stream endpoint async to not block the main thread"""
async with aiohttp.ClientSession() as session:
+ session.headers.update(headers)
async with session.post(url, data=json.dumps(request_body)) as response:
if response.status == 200:
return True
@@ -103,14 +104,15 @@ def set_prompt(self, request_body: dict, prompt: str, context: str = '') -> dict
class LlamaCppAdapter(AbstractIoAdapter):
- def stream_request(self, request_body: dict, io: PlayerConnection = None, wait: bool = False) -> str:
- return asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, request_body, io = io))
+ def stream_request(self, headers: dict, request_body: dict, io: PlayerConnection = None, wait: bool = False) -> str:
+ return asyncio.run(self._do_stream_request(self.url + self.stream_endpoint, headers, request_body, io = io))
- async def _do_stream_request(self, url: str, request_body: dict, io: PlayerConnection) -> str:
+ async def _do_stream_request(self, url: str, headers: dict, request_body: dict, io: PlayerConnection) -> str:
""" Send request to stream endpoint async to not block the main thread"""
request_body['stream'] = True
text = ''
async with aiohttp.ClientSession() as session:
+ session.headers.update(headers)
async with session.post(url, data=json.dumps(request_body)) as response:
if response.status != 200:
print("Error occurred:", response.status)
diff --git a/tale/llm/llm_io.py b/tale/llm/llm_io.py
index a79476bb..593f54e6 100644
--- a/tale/llm/llm_io.py
+++ b/tale/llm/llm_io.py
@@ -13,6 +13,7 @@ def __init__(self, config: dict = None, backend_config: dict = None):
self.backend = config['BACKEND']
self.url = backend_config['URL']
self.endpoint = backend_config['ENDPOINT']
+ headers = {}
if self.backend != 'kobold_cpp':
headers = json.loads(backend_config['OPENAI_HEADERS'])
headers['Authorization'] = f"Bearer {backend_config['OPENAI_API_KEY']}"
@@ -20,8 +21,10 @@ def __init__(self, config: dict = None, backend_config: dict = None):
self.headers = headers
self.io_adapter = LlamaCppAdapter(self.url, backend_config['STREAM_ENDPOINT'], config.get('USER_START', ''), config.get('USER_END', ''), config.get('SYSTEM_START', ''), config.get('PROMPT_END', ''))
else:
+ if 'API_PASSWORD' in backend_config and backend_config['API_PASSWORD']:
+ headers['Authorization'] = f"Bearer {backend_config['API_PASSWORD']}"
+ self.headers = headers
self.io_adapter = KoboldCppAdapter(self.url, backend_config['STREAM_ENDPOINT'], backend_config['DATA_ENDPOINT'], config.get('USER_START', ''), config.get('USER_END', ''), config.get('SYSTEM_START', ''), config.get('PROMPT_END', ''))
- self.headers = {}
self.stream = backend_config['STREAM']
@@ -46,7 +49,7 @@ def asynchronous_request(self, request_body: dict, prompt: str, context: str = '
def stream_request(self, request_body: dict, prompt: str, context: str = '', io = None, wait: bool = False) -> str:
if self.io_adapter:
request_body = self.io_adapter.set_prompt(request_body, prompt, context)
- return self.io_adapter.stream_request(request_body, io, wait)
+ return self.io_adapter.stream_request(self.headers, request_body, io, wait)
# fall back if no io adapter
return self.synchronous_request(request_body=request_body, prompt=prompt, context=context)
diff --git a/tale/wearable.py b/tale/wearable.py
index 8203f9cb..a400a61c 100644
--- a/tale/wearable.py
+++ b/tale/wearable.py
@@ -35,14 +35,8 @@ def load_wearables_from_json(file_path):
wearables_fantasy = load_wearables_from_json('../items/wearables_fantasy.json')
wearables_modern = load_wearables_from_json('../items/wearables_modern.json')
+wearbles_story = []
-# Disclaimer: Not to limit the player, but to give the generator some hints
-female_clothing_modern = {'dress', 'dress_shirt', 'blouse', 'skirt', 'bra', 'panties', 'thong', 'stockings', 'top'}
-male_clothing_modern = {'suit', 'boxers', 'briefs', 'shirt'}
-neutral_clothing_modern = {'t-shirt', 'shirt', 'jeans', 'sneakers', 'belt', 'dress_shoes', 'hat', 'coveralls', 'sweater', 'socks', 'coat', 'jacket'}
-
-
-
dressable_body_types = [BodyType.HUMANOID, BodyType.SEMI_BIPEDAL, BodyType.WINGED_MAN]
def body_parts_for_bodytype(bodytype: BodyType) -> list:
@@ -53,13 +47,18 @@ def body_parts_for_bodytype(bodytype: BodyType) -> list:
return None
def random_wearable_for_body_part(bodypart: WearLocation, setting: str = 'fantasy', armor_only = False) -> dict:
+ wearables = []
if setting == 'fantasy':
wearables = wearables_fantasy
- else:
+ elif setting == 'modern' or setting == 'sci-fi' or setting == 'post-apocalyptic':
wearables = wearables_modern
+ wearables.extend(wearbles_story)
available_wearables = [item for item in wearables if item['location'] == bodypart and (not armor_only or item.get('ac', 0) > 0)]
if not available_wearables:
return None
wearable = random.choice(available_wearables)
wearable['short_descr'] = f"{random.choice(wearable_colors)} {wearable['name']}"
return wearable
+
+def add_story_wearables(wearables: list):
+ wearbles_story.extend(wearables)
\ No newline at end of file
diff --git a/tests/files/world_story/world.json b/tests/files/world_story/world.json
index e0e7ce6f..9392ddf4 100644
--- a/tests/files/world_story/world.json
+++ b/tests/files/world_story/world.json
@@ -202,6 +202,16 @@
"descr": "A wolf",
"type": "Mob"
}
+ ],
+ "wearables": [
+ {
+ "name": "fur jacket",
+ "short_descr": "A fur jacket",
+ "descr": "A warm fur jacket",
+ "location": "TORSO",
+ "type": "Wearable",
+ "ac": 1
+ }
]
}
}
diff --git a/tests/test_combat.py b/tests/test_combat.py
index 0d968ede..1589c248 100644
--- a/tests/test_combat.py
+++ b/tests/test_combat.py
@@ -32,9 +32,10 @@ def test_resolve_attack(self):
combat = Combat([attacker], [defender])
text = combat.resolve_attack()
- assert('attacker hits' in text or 'attacker performs a critical hit' in text)
+ assert('hits' in text)
assert('defender is injured' in text)
assert('defender dies' in text)
+ assert not 'defender attacks' in text
def test_block_ranged_fails(self):
@@ -235,8 +236,8 @@ def test_resolve_attack_group(self):
text = combat.resolve_attack()
self._assert_combat(attacker, defender, text)
- assert('attacker hits' in text or 'attacker performs a critical hit' in text)
- assert('attacker2 hits' in text or 'attacker2 performs a critical hit' in text)
+ assert('attacker attacks')
+ assert('attacker2 attacks')
def test_start_attack_no_combat_points(self):
attacker = Player(name='att', gender='m')
diff --git a/tests/test_json_story.py b/tests/test_json_story.py
index 61db1251..7837194d 100644
--- a/tests/test_json_story.py
+++ b/tests/test_json_story.py
@@ -5,13 +5,14 @@
from tale.coord import Coord
from tale.items import generic
import tale.parse_utils as parse_utils
-from tale import util
+from tale import util, wearable
from tale.base import Location
from tale.driver_if import IFDriver
from tale.json_story import JsonStory
from tale.mob_spawner import MobSpawner
class TestJsonStory():
+ wearable.wearbles_story = []
driver = IFDriver(screen_delay=99, gui=False, web=True, wizard_override=True)
driver.game_clock = util.GameDateTime(datetime.datetime(year=2023, month=1, day=1), 1)
story = JsonStory('tests/files/world_story/', parse_utils.load_story_config(parse_utils.load_json('tests/files/world_story/story_config.json')))
@@ -74,6 +75,8 @@ def test_load_story(self):
assert self.story.day_cycle
assert self.story.random_events
+
+ assert wearable.wearbles_story[0]['name'] == 'fur jacket'
def test_add_location(self):
diff --git a/tests/test_living_npc.py b/tests/test_living_npc.py
index ee656d25..7b594dc8 100644
--- a/tests/test_living_npc.py
+++ b/tests/test_living_npc.py
@@ -182,6 +182,7 @@ class TestLivingNpcActions():
'OPENAI_HEADERS': '',
'OPENAI_API_KEY': '',
'OPENAI_JSON_FORMAT': '',
+ 'API_PASSWORD': ''
}
driver = FakeDriver()
driver.story = DynamicStory()
diff --git a/tests/test_llm_io.py b/tests/test_llm_io.py
index 2227c3af..11b41f46 100644
--- a/tests/test_llm_io.py
+++ b/tests/test_llm_io.py
@@ -76,6 +76,22 @@ def test_set_prompt_llama_cpp(self):
assert('context' in result['messages'][1]['content'])
assert(result['messages'][0]['content'] != 'context')
+
+ def test_password_in_header(self):
+ config_file = self._load_config()
+ config_file['BACKEND'] = 'kobold_cpp'
+
+ backend_config = self._load_backend_config('kobold_cpp')
+ io_util = IoUtil(config=config_file, backend_config=backend_config)
+
+ assert not io_util.headers
+
+ backend_config = self._load_backend_config('kobold_cpp')
+ backend_config['API_PASSWORD'] = 'test_password'
+ io_util = IoUtil(config=config_file, backend_config=backend_config)
+
+ assert io_util.headers['Authorization'] == f"Bearer {backend_config['API_PASSWORD']}"
+
@responses.activate
def test_error_response(self):
config_file = self._load_config()
diff --git a/tests/test_story.py b/tests/test_story.py
index d3fbdddf..1f236d6e 100644
--- a/tests/test_story.py
+++ b/tests/test_story.py
@@ -24,6 +24,7 @@ class TestStoryContext:
'OPENAI_HEADERS': '',
'OPENAI_API_KEY': '',
'OPENAI_JSON_FORMAT': '',
+ 'API_PASSWORD': '',
}
def test_initialization(self):