Skip to content

Commit 67d7919

Browse files
authored
Merge pull request #7 from neph1/test_fixes
v0.7.4
2 parents 4cca924 + 33eaffb commit 67d7919

20 files changed

+235
-163
lines changed

stories/prancingllama/npcs/npcs.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from tale.player import Player
99
from tale.util import call_periodically, Context
1010
from tale import lang
11+
from typing import Optional
1112

1213
class InnKeeper(LivingNpc):
1314

@@ -54,6 +55,8 @@ def do_random_move(self, ctx: Context) -> None:
5455
def do_pick_up_dishes(self, ctx: Context) -> None:
5556
self.location.tell(f"{lang.capital(self.title)} wipes a table and picks up dishes.", evoke=False, max_length=True)
5657

58+
59+
5760
class Patron(LivingNpc):
5861

5962
def __init__(self, name: str, gender: str, *,
@@ -64,6 +67,13 @@ def __init__(self, name: str, gender: str, *,
6467
def init(self) -> None:
6568
self.aliases = {"patron"}
6669

70+
class Shanda(Patron):
71+
72+
def allow_give_item(self, item: Item, actor: Optional[Living]) -> None:
73+
if item.name == "rat_skull":
74+
self.sentiments(actor.title, 'impressed')
75+
self.do_say(f'{actor.title} gives Shanda a giant rat skull', actor=actor)
76+
6777
class Guard(LivingNpc):
6878

6979
def __init__(self, name: str, gender: str, *,
@@ -104,9 +114,9 @@ def init(self) -> None:
104114
@call_periodically(10, 25)
105115
def do_idle_action(self, ctx: Context) -> None:
106116
if random.random() < 0.5:
107-
self.tell_others("{Actor} hisses.", evoke=True, max_length=True)
117+
self.tell_others("{Actor} hisses.", evoke=False, max_length=True)
108118
else:
109-
self.tell_others("{Actor} sniffs around.", evoke=True, max_length=True)
119+
self.tell_others("{Actor} sniffs around.", evoke=False, max_length=True)
110120

111121

112122
norhardt = Patron("Norhardt", "m", age=56, descr="A grizzled old man, with parchment-like skin and sunken eyes. He\'s wearing ragged clothing and big leather boots. He\'s a formidable presence, commanding yet somber.", personality="An experienced explorer who is obsessed with finding the mythological Yeti which supposedly haunts these mountains. He won\'t stop talking about it.", short_descr="An old grizzled man sitting by the bar.")
@@ -115,11 +125,11 @@ def do_idle_action(self, ctx: Context) -> None:
115125
elid_gald = Patron("Elid Gald", "m", age=51, descr="A gentleman who's aged with grace. He has a slender appearance with regal facial features, and dark hair, but his (one) eye has a dangerous squint. The other is covered by a patch.", personality="Old Gold is a smooth-talking pick-pocket and charlatan. Although claiming to be retired, he never passes on an opportunity to relieve strangers of their valuables. He wishes to obtain the map Norhardt possesses.", short_descr="A slender gentleman with a patch over one of his eyes, leaning against the wall.")
116126
elid_gald.aliases = {"elid", "one eyed man", "gentleman"}
117127

118-
shanda = Patron("Shanda Heard", "f", age=31, descr="A fierce looking woman, with a face like chiseled from granite and a red bandana around her wild hair. She keeps an unblinking gaze on her surroundings.", personality="She's a heavy drinker and boaster, and for a drink she will spill tales of her conquests and battles and long lost love. She's feared by many but respected by all for her prowess in battle.", short_descr="A fierce looking woman sitting by a table, whose eyes seem to follow you.")
128+
shanda = Shanda("Shanda Heard", "f", age=31, descr="A fierce looking woman, with a face as if chiseled from granite and a red bandana around her wild hair. She keeps an unblinking gaze on her surroundings.", personality="She's a heavy drinker and boaster, and for a drink she will spill tales of her conquests and battles and long lost love. She's feared by many but respected by all for her prowess in battle.", short_descr="A fierce looking woman sitting by a table, whose eyes seem to follow you.")
119129
shanda.aliases = {"shanda", "fierce woman", "woman by table"}
120130

121-
count_karta = Patron("Count of Karta", "m", age=43, descr="A hood shadows his facial features, but a prominent jaw juts out. His mouth seems to be working constantly, as if muttering about something.", personality="Having fled from an attempt on his life, and being discredited, he has come here to plan his revenge. He seems to have gold and is looking for able bodies to help him.", short_descr="A mysterious man by the fire.")
122-
count_karta.aliases = {"count", "karta", "mysterious man"}
131+
count_karta = Patron("Count of Karta", "m", age=43, descr="A hood shadows his facial features, but a prominent jaw juts out from beneath it. His mouth seems to be working constantly, as if muttering about something.", personality="Having fled from an attempt on his life, and being discredited, he has come here to plan his revenge. He seems to have gold and is looking for able bodies to help him.", short_descr="A mysterious man by the fire.")
132+
count_karta.aliases = {"count", "karta", "mysterious man", "hooded man"}
123133

124134
urta = InnKeeper("Urta", "f", age=44, descr="A gruff, curvy woman with a long brown coat and bushy hair that reaches her waist. When not serving, she keeps polishing jugs with a dirty rag.", personality="She's the owner of The Prancing Llama, and of few words. But the words she speak are kind. She knows a little about all the patrons in her establishment.", short_descr="A curvy woman with long brown coat standing behind the bar.")
125135
urta.aliases = {"bartender", "inn keeper", "curvy woman"}

stories/prancingllama/zones/prancingllama.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ def init(self):
1616

1717
@call_periodically(40, 90)
1818
def spawn_rat(self, ctx: Context) -> None:
19+
rat_skull = Item("rat_skull", "giant rat skull", descr="It's a giant rat's bloody skull.")
1920
if not self.rat or self.rat.alive == False:
2021
self.rat = Rat("giant rat", random.choice("m"), descr="A vicious looking, giant, rat", race="rodent")
22+
self.rat.init_inventory([rat_skull])
2123
self.rat.move(self)
2224

2325
main_hall = Location("Main Hall", "An area full of tables with people eating, drinking and talking")

tale/accounts.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,12 @@ def _create_database(self) -> None:
102102
xp integer NOT NULL,
103103
hp integer NOT NULL,
104104
ac integer NOT NULL,
105+
wc integer NOT NULL,
105106
maxhp_dice varchar NULL,
106107
attack_dice varchar NULL,
107108
alignment integer NOT NULL,
109+
strength integer NOT NULL,
110+
dexterity integer NOT NULL,
108111
FOREIGN KEY(account) REFERENCES Account(id)
109112
);
110113
""")

tale/base.py

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from . import story
5454
from . import verbdefs
5555
from . import combat
56+
from . import player
5657
from .errors import ActionRefused, ParseError, LocationIntegrityError, TaleError, UnknownVerbException, NonSoulVerb
5758

5859
__all__ = ["MudObject", "Armour", 'Container', "Door", "Exit", "Item", "Living", "Stats", "Location", "Weapon", "Key", "Soul"]
@@ -891,7 +892,7 @@ def __init__(self) -> None:
891892
self.gender = 'n'
892893
self.level = 1
893894
self.xp = 0
894-
self.hp = 0
895+
self.hp = 5
895896
self.maxhp_dice = ""
896897
self.ac = 0
897898
self.wc = 0
@@ -903,7 +904,7 @@ def __init__(self) -> None:
903904
self.size = races.BodySize.HUMAN_SIZED
904905
self.race = "" # the name of the race of this creature
905906
self.strength = 3
906-
self.agility = 3
907+
self.dexterity = 3
907908

908909
def __repr__(self):
909910
return "<Stats: %s>" % vars(self)
@@ -926,6 +927,7 @@ def set_stats_from_race(self) -> None:
926927
self.language = r.language
927928
self.weight = r.mass
928929
self.size = r.size
930+
self.hp = r.hp
929931

930932

931933
class Living(MudObject):
@@ -1083,7 +1085,7 @@ def tell_later(self, message: str) -> None:
10831085
"""Tell something to this creature, but do it after all other messages."""
10841086
pending_tells.send(lambda: self.tell(message, evoke=True, max_length=False))
10851087

1086-
def tell_others(self, message: str, target: Optional['Living']=None, evoke=False, max_length : bool=True, alt_prompt='') -> None:
1088+
def tell_others(self, message: str, target: Optional['Living']=None, evoke: bool=False, max_length : bool=True, alt_prompt='') -> None:
10871089
"""
10881090
Send a message to the other livings in the location, but not to self.
10891091
There are a few formatting strings for easy shorthands:
@@ -1274,7 +1276,7 @@ def display_direction(directions: Sequence[str]) -> str:
12741276
if direction_txt:
12751277
message = f"{lang.capital(self.title)} leaves {direction_txt}."
12761278
else:
1277-
message = f"{lang.capital(self.title)} leaves."
1279+
message = f"{lang.capital(self.title)} leaves towards {target.title}."
12781280
original_location.tell(message, exclude_living=self, evoke=False, max_length=True)
12791281
# queue event
12801282
if is_player:
@@ -1284,7 +1286,7 @@ def display_direction(directions: Sequence[str]) -> str:
12841286
else:
12851287
target.insert(self, actor)
12861288
if not silent:
1287-
target.tell(f"{lang.capital(self.title)} arrives from {original_location}." , exclude_living=self, evoke=False, max_length=True)
1289+
target.tell(f"{lang.capital(self.title)} arrives from {original_location.title}." , exclude_living=self, evoke=False, max_length=True)
12881290
# queue event
12891291
if is_player:
12901292
pending_actions.send(lambda who=self, where=original_location: target.notify_player_arrived(who, where))
@@ -1330,25 +1332,39 @@ def locate_item(self, name: str, include_inventory: bool=True, include_location:
13301332

13311333
def start_attack(self, victim: 'Living') -> None:
13321334
"""Starts attacking the given living until death ensues on either side."""
1333-
# @todo I'm not yet sure if the combat/attack logic should go here (just on Living), or that it should be split with Player...
1334-
# @todo actual fight. Also implement 'assist' command to help someone that is already fighting.
1335-
# NOTE: combat commands should have a check so that you cannot spam them!
1336-
name = lang.capital(self.title)
1337-
1338-
result, dead = combat.resolve_attack(self, victim)
1335+
attacker_name = lang.capital(self.title)
1336+
victim_name = lang.capital(victim.title)
1337+
result, damage_to_attacker, damage_to_defender = combat.resolve_attack(self, victim)
13391338

1340-
room_msg = "%s attacks %s! %s" % (name, victim.title, result)
1341-
victim_msg = "%s attacks you. %s" % (name, result)
1342-
attacker_msg = "You attack %s! %s" % (victim.title, result)
1339+
room_msg = "%s attacks %s! %s" % (attacker_name, victim_name, result)
1340+
victim_msg = "%s attacks you. %s" % (attacker_name, result)
1341+
attacker_msg = "You attack %s! %s" % (victim_name, result)
13431342
victim.tell(victim_msg, evoke=True, max_length=False)
1344-
# TODO: try to get from config file instead
1345-
combat_prompt = mud_context.driver.llm_util.combat_prompt
1346-
victim.location.tell(room_msg, exclude_living=victim, specific_targets={self}, specific_target_msg=attacker_msg, evoke=True, max_length=False, alt_prompt=combat_prompt)
1347-
if dead:
1348-
remains = Container(f"remains of {dead.title}")
1349-
remains.init_inventory(dead.inventory)
1350-
dead.location.insert(remains, None)
1351-
dead.destroy(util.Context)
1343+
1344+
1345+
if isinstance(self, player.Player):
1346+
attacker_name += "as 'You'"
1347+
if isinstance(victim, player.Player):
1348+
victim_name += "as 'You'"
1349+
1350+
combat_prompt = mud_context.driver.llm_util.combat_prompt.format(attacker=attacker_name,
1351+
victim=victim_name,
1352+
attacker_msg=attacker_msg,
1353+
location=self.location.title,
1354+
location_description=self.location.short_description)
1355+
victim.location.tell(room_msg,
1356+
exclude_living=victim,
1357+
specific_targets={self},
1358+
specific_target_msg=attacker_msg,
1359+
evoke=True,
1360+
max_length=False,
1361+
alt_prompt=combat_prompt)
1362+
self.stats.hp -= damage_to_attacker
1363+
victim.stats.hp -= damage_to_defender
1364+
if self.stats.hp < 1:
1365+
combat.produce_remains(util.Context, self)
1366+
if victim.stats.hp < 1:
1367+
combat.produce_remains(util.Context, victim)
13521368

13531369
def allow_give_money(self, amount: float, actor: Optional['Living']) -> None:
13541370
"""Do we accept money? Raise ActionRefused if not."""
@@ -1680,8 +1696,8 @@ def open(self, actor: Living, item: Item=None) -> None:
16801696
raise ActionRefused("You try to open it, but it's locked.")
16811697
else:
16821698
self.opened = True
1683-
actor.tell("You open it.", evoke=True, max_length=True)
1684-
actor.tell_others("{Actor} opens the %s." % self.name, evoke=True, max_length=True)
1699+
actor.tell("You open it.", evoke=False, max_length=True)
1700+
actor.tell_others("{Actor} opens the %s." % self.name, evoke=False, max_length=True)
16851701
if self.linked_door:
16861702
self.linked_door.opened = True
16871703
self.target.tell("The %s is opened from the other side." % self.linked_door.name, evoke=False, max_length=True)
@@ -1691,8 +1707,8 @@ def close(self, actor: Living, item: Item=None) -> None:
16911707
if not self.opened:
16921708
raise ActionRefused("It's already closed.")
16931709
self.opened = False
1694-
actor.tell("You close it.", evoke=True, max_length=True)
1695-
actor.tell_others("{Actor} closes the %s." % self.name, evoke=True, max_length=True)
1710+
actor.tell("You close it.", evoke=False, max_length=True)
1711+
actor.tell_others("{Actor} closes the %s." % self.name, evoke=False, max_length=True)
16961712
if self.linked_door:
16971713
self.linked_door.opened = False
16981714
self.target.tell("The %s is closed from the other side." % self.linked_door.name, evoke=False, max_length=True)
@@ -1740,12 +1756,12 @@ def unlock(self, actor: Living, item: Item=None) -> None:
17401756
raise ActionRefused("You don't seem to have the means to unlock it.")
17411757
self.locked = False
17421758
self.opened = True
1743-
actor.tell("Your %s fits! You unlock the %s and open it." % (key.title, self.name), evoke=True, max_length=True)
1744-
actor.tell_others("{Actor} unlocks the %s with %s %s, and opens it." % (self.name, actor.possessive, key.title), evoke=True, max_length=True)
1759+
actor.tell("Your %s fits! You unlock the %s and open it." % (key.title, self.name), evoke=False, max_length=True)
1760+
actor.tell_others("{Actor} unlocks the %s with %s %s, and opens it." % (self.name, actor.possessive, key.title), evoke=False, max_length=True)
17451761
if self.linked_door:
17461762
self.linked_door.locked = False
17471763
self.linked_door.opened = True
1748-
self.target.tell("The %s is unlocked and opened from the other side." % self.linked_door.name, evoke=True, max_length=True)
1764+
self.target.tell("The %s is unlocked and opened from the other side." % self.linked_door.name, evoke=False, max_length=True)
17491765

17501766
def check_key(self, item: Item) -> bool:
17511767
"""Check if the item is a proper key for this door (based on key_code)"""

tale/combat.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,34 @@
55
"""
66

77
import random
8+
import tale.util as util
9+
import tale.base as base
10+
from tale.util import Context
811

912
def resolve_attack(attacker, victim):
10-
result = combat(attacker, victim)
11-
12-
text = 'After a fierce battle'
13-
if result < 0.5:
14-
return text + ', %s dies.' % (victim.title), victim
15-
elif result > 0.5:
16-
return text + ', %s dies.' % (attacker.title), attacker
17-
else:
18-
return text + 'both retreat.'
13+
damage_to_attacker, damage_to_defender = combat(attacker, victim)
14+
text = 'After a fierce exchange of attacks '
15+
if damage_to_attacker > 0:
16+
text = text + f', {attacker.title} is injured '
17+
if damage_to_defender > 0:
18+
text = text + f', {victim.title} is injured '
19+
if victim.stats.hp - damage_to_defender < 1:
20+
text = text + f', {victim.title} dies.'
21+
if attacker.stats.hp - damage_to_attacker < 1:
22+
text = text + f', {attacker.title} dies.'
23+
24+
return text, damage_to_attacker, damage_to_defender
1925

2026
def combat(actor1: 'Living', actor2: 'Living'):
21-
""" Simple combat, but rather complex logic. Credit to ChatGPT.
22-
< 0.5 actor2 'victim' dies
23-
> 0.5 actor1 'attacker' dies"""
27+
""" Simple combat, but rather complex logic. Credit to ChatGPT."""
2428

2529
def calculate_attack(actor: 'Living'):
2630
# The attack strength depends on the level and strength of the actor
2731
return actor.stats.level
2832

2933
def calculate_defense(actor: 'Living'):
30-
# The defense strength depends on the level and agility of the actor
31-
return actor.stats.level * actor.stats.agility
34+
# The defense strength depends on the level and dexterity of the actor
35+
return actor.stats.level * actor.stats.dexterity
3236

3337
def calculate_weapon_bonus(actor: 'Living'):
3438
# The weapon bonus is a random factor between 0 and 1. If the actor has no weapon, bonus is 1.
@@ -53,9 +57,13 @@ def calculate_damage(attacker: 'Living', defender: 'Living'):
5357
damage_to_actor1 *= random.uniform(0.9, 1.1)
5458
damage_to_actor2 *= random.uniform(0.9, 1.1)
5559

56-
# Calculate the normalized factor
57-
total_damage = damage_to_actor1 + damage_to_actor2
58-
if total_damage == 0:
59-
return 0.5
60-
return damage_to_actor1 / total_damage
61-
60+
return int(damage_to_actor1), int(damage_to_actor2)
61+
62+
63+
def produce_remains(context: Context, actor: 'Living'):
64+
# Produce the remains of the actor
65+
remains = base.Container(f"remains of {actor.title}")
66+
remains.init_inventory(actor.inventory)
67+
actor.location.insert(remains, None)
68+
actor.destroy(context)
69+
return remains

tale/driver.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from .errors import StoryCompleted
3333
from tale.load_character import CharacterLoader, CharacterV2
3434
from tale.llm_ext import LivingNpc
35+
from tale.llm_utils import LlmUtil
3536

3637

3738
topic_pending_actions = pubsub.topic("driver-pending-actions")
@@ -210,6 +211,7 @@ def __init__(self) -> None:
210211
self.game_clock = None # type: util.GameDateTime
211212
self.game_mode = None # type: GameMode
212213
self._stop_mainloop = True
214+
self.llm_util = LlmUtil()
213215
# playerconnections that wait for input; maps connection to tuple (dialog, validator, echo_input)
214216
self.waiting_for_input = {} # type: Dict[player.PlayerConnection, Tuple[Generator, Any, Any]]
215217
mud_context.driver = self
@@ -601,14 +603,14 @@ def _process_player_command(self, cmd: str, conn: player.PlayerConnection) -> No
601603
# the player command ended but signaled that an async dialog should be initiated
602604
topic_async_dialogs.send((conn, x.dialog))
603605

604-
def go_through_exit(self, player: player.Player, direction: str) -> None:
606+
def go_through_exit(self, player: player.Player, direction: str, evoke: bool=True) -> None:
605607
xt = player.location.exits[direction]
606608
xt.allow_passage(player)
607609
if xt.enter_msg:
608-
player.tell(xt.enter_msg, end=True, evoke=True, max_length=True)
610+
player.tell(xt.enter_msg, end=True, evoke=evoke, max_length=True)
609611
player.tell("\n")
610612
player.move(xt.target, direction_names=[xt.name] + list(xt.aliases))
611-
player.look()
613+
player.look(evoke=evoke)
612614

613615
def lookup_location(self, location_name: str) -> base.Location:
614616
location = self.zones
@@ -792,6 +794,7 @@ def load_character(self, player: player.Player, path: str):
792794
personality = character.personality,
793795
occupation = character.occupation)
794796
npc.following = player
797+
npc.stats.hp = character.hp
795798
player.location.insert(npc, None)
796799

797800

0 commit comments

Comments
 (0)