The player can now have simple conversations with NPCs.

This commit is contained in:
Patrick Marsee 2019-05-20 21:13:56 -04:00
parent e0b88e7a45
commit e81345e793
4 changed files with 115 additions and 10 deletions

View file

@ -7,6 +7,10 @@ import gameevents as _ge
import random as _ra import random as _ra
import sys as _sys import sys as _sys
import pickle as _pi import pickle as _pi
import ruamel.yaml as _yaml
class GameError(RuntimeError):
pass
class GameBase(object): class GameBase(object):
@ -18,6 +22,7 @@ class GameBase(object):
def __init__(self): def __init__(self):
self.outstream = _sys.stdout self.outstream = _sys.stdout
self.__useFuncs = {} self.__useFuncs = {}
self.__behaviors = {}
self.__gameEvents = {} self.__gameEvents = {}
self.__IOCalls = {} # {str : function} self.__IOCalls = {} # {str : function}
self.customVals = {} # for setting flags and such self.customVals = {} # for setting flags and such
@ -49,9 +54,11 @@ class GameBase(object):
self.registerEvent('useon', self.handleUseOn) self.registerEvent('useon', self.handleUseOn)
self.registerEvent('take', self.handleTake) self.registerEvent('take', self.handleTake)
self.registerEvent('drop', self.handleDrop) self.registerEvent('drop', self.handleDrop)
self.registerEvent('behave', self.handleBehave)
self.registerUseFunc('examine', self.examine) self.registerUseFunc('examine', self.examine)
self.registerUseFunc('key', self.key) self.registerUseFunc('key', self.key)
self.registerUseFunc('container', self.container) self.registerUseFunc('container', self.container)
self.registerBehavior('wander', self.wander)
# Helper functions # Helper functions
@ -178,7 +185,7 @@ Destination can be a coordinate pair or object name. For instance, if one
wanted to go to D6, one could say "go to D6", "go to d 6", or "go to 3 6". wanted to go to D6, one could say "go to D6", "go to d 6", or "go to 3 6".
The letter is not case-sensitive.""" The letter is not case-sensitive."""
if self.level == None: if self.level == None:
raise RuntimeError("Cannot move: No level has been loaded.") raise GameError("Cannot move: No level has been loaded.")
speed = 0.6666667 speed = 0.6666667
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
speed = 0.3333333 speed = 0.3333333
@ -236,13 +243,46 @@ Object can be the name of the object, or its coordinates."""
args.pop(0) args.pop(0)
thing, x, y = self.parseCoords(args, usePlayerCoords = False) thing, x, y = self.parseCoords(args, usePlayerCoords = False)
if not self.level.lineOfSight(self.playerx, self.playery, x, y): if not self.level.lineOfSight(self.playerx, self.playery, x, y):
print("{} cannot see that.".format(self.playerName)) print("{} cannot see that.".format(self.playerName), file = self.outstream)
elif thing == None: elif thing == None:
print("There is nothing to see here.\n", file = self.outstream) print("There is nothing to see here.\n", file = self.outstream)
else: else:
print(self.justifyText(str(thing)), file = self.outstream) print(self.justifyText(str(thing)), file = self.outstream)
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
def talk(self, args):
"""talk [to [the]] character
Talk to a character.
"to" and "the" do nothing, and are intended only to make certain commands
make more sense from a linguistic perspective (for instance, one could
say "talk to the guard" rather than just "talk guard").
Character can be the name of the character, or their coordinates."""
if len(args) == 0:
print(self.justifyText(self.level.description), file = self.outstream)
else:
if args[0] == 'to':
args.pop(0)
if args[0] == 'the':
args.pop(0)
thing, x, y = self.parseCoords(args, usePlayerCoords = False)
if not self.level.lineOfSight(self.playerx, self.playery, x, y):
print("{} cannot talk to {}.".format(self.playerName, thing.name), file = self.outstream)
elif thing == None:
print("There is nobody here.\n", file = self.outstream)
else:
if 'dialogs' in thing.customValues:
if isinstance(thing.customValues['dialogs'], list):
if 'dialogPtr' in thing.customValues and isinstance(thiong.customValues['dialogPtr'], int):
self.dialog(thing.customValues['dialogs'][thing.customValues['dialogPtr']], thing)
else:
self.dialog(thing.customValues['dialogs'][0], thing)
elif isinstance(thing.customValues['dialogs'], str):
self.dialog(thing.customValues['dialogs'], thing)
else:
raise GameError("Character '{}' has dialog of an unexpected type.".format(thing.name))
else:
print("{} has nothing to say to you.".format(thing.name), file = self.outstream)
def use(self, args): def use(self, args):
"""use [-r] [the] object [on [the] object2] """use [-r] [the] object [on [the] object2]
Use an object. If the player is not already close to it, they will go to it. Use an object. If the player is not already close to it, they will go to it.
@ -491,6 +531,13 @@ Object can be the name of the object, or its coordinates."""
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return return
def dialog(self, dialogName, conversant):
yaml = _yaml.YAML()
dialog = None
with open(dialogName, 'r') as f:
dialog = yaml.load(f)
self.getIO('dialog')(dialog)
def gameEventLoop(self): def gameEventLoop(self):
#print(self.skipLoop) #print(self.skipLoop)
if self.skipLoop: if self.skipLoop:
@ -576,6 +623,9 @@ Object can be the name of the object, or its coordinates."""
self.level.addThing(e.item, True) self.level.addThing(e.item, True)
del self.playerInv[e.item.name] del self.playerInv[e.item.name]
return True return True
def handleBehave(self, e):
self.__behaviors[e.actor.behavior](e.actor)
# default useFuncs: take a list of arguments, return the time the use took # default useFuncs: take a list of arguments, return the time the use took
@ -629,12 +679,21 @@ Object can be the name of the object, or its coordinates."""
else: else:
print("The key doesn't fit that lock.", file = self.outstream) print("The key doesn't fit that lock.", file = self.outstream)
return 0.125 return 0.125
# behaviors
def wander(self, actor):
pass
# stuff for extended classes to use # stuff for extended classes to use
def registerUseFunc(self, name, func): def registerUseFunc(self, name, func):
"""Registers a function for use by things in the map, but not directly """Registers a function for use by things in the map, but not directly
callable by the player.""" callable by the player."""
self.__useFuncs[name] = func self.__useFuncs[name] = func
def registerBehavior(self, name, func):
"""Registers a function for use as an NPC's behavior."""
self.__behaviors[name] = func
def registerEvent(self, name, func): def registerEvent(self, name, func):
"""Registers a function to handle an event in the event loop. """Registers a function to handle an event in the event loop.
@ -648,4 +707,6 @@ always give the player a turn, False otherwise."""
def getIO(self, name): def getIO(self, name):
"""This is so derived classes can access their IOCalls.""" """This is so derived classes can access their IOCalls."""
if name not in self.__IOCalls:
raise GameError("No IO call for {}.".format(name))
return self.__IOCalls[name] return self.__IOCalls[name]

View file

@ -190,6 +190,7 @@ class NPC(Thing):
self.inventory = inventory self.inventory = inventory
self.customValues = customValues self.customValues = customValues
self.graphic = graphic self.graphic = graphic
self.behaveEvent = None
def give(self, item): def give(self, item):
self.inventory.append(item) self.inventory.append(item)
@ -545,6 +546,7 @@ Entering a map through stdin will be obsolete once testing is over."""
yaml = ruamel.yaml.YAML() yaml = ruamel.yaml.YAML()
yaml.register_class(Item) yaml.register_class(Item)
yaml.register_class(Useable) yaml.register_class(Useable)
yaml.register_class(NPC)
yaml.register_class(Door) yaml.register_class(Door)
yaml.register_class(MapExit) yaml.register_class(MapExit)
if infile != None: if infile != None:
@ -553,14 +555,9 @@ Entering a map through stdin will be obsolete once testing is over."""
info = yaml.load(f) info = yaml.load(f)
except OSError as e: except OSError as e:
print("The file could not be read.") print("The file could not be read.")
return None return None, nextThing
else: else:
raise RuntimeError("No file was specified for loading.")
while tryToRead:
try:
data += input()
except EOFError as e:
tryToRead = False
# Now what we do with the data # Now what we do with the data
mat = None mat = None

View file

@ -7,9 +7,12 @@ import sys as _sys
import heapq import heapq
#import gamemap #import gamemap
import gameevents import gameevents
#from os import get_terminal_size import textwrap as _tw
from shutil import get_terminal_size as _gts
#import random #import random
TERM_SIZE = _gts()[0]
class GameShell(Shell): class GameShell(Shell):
UP = 1 UP = 1
@ -24,6 +27,7 @@ class GameShell(Shell):
self.outstream = _sys.stdout self.outstream = _sys.stdout
self.gameBase = gameBase self.gameBase = gameBase
self.colorMode = 0 self.colorMode = 0
self.ps2 = '?> '
# register functions # register functions
@ -33,6 +37,7 @@ class GameShell(Shell):
self.registerCommand('move', self.gameBase.go) self.registerCommand('move', self.gameBase.go)
self.registerCommand('walk', self.gameBase.go) self.registerCommand('walk', self.gameBase.go)
self.registerCommand('look', self.gameBase.look) self.registerCommand('look', self.gameBase.look)
self.registerCommand('talk', self.gameBase.talk)
self.registerCommand('use', self.gameBase.use) self.registerCommand('use', self.gameBase.use)
self.registerCommand('loadMap', self.gameBase.loadMap) self.registerCommand('loadMap', self.gameBase.loadMap)
self.registerCommand('man', self.man) self.registerCommand('man', self.man)
@ -50,6 +55,7 @@ class GameShell(Shell):
self.registerCommand('colorTest', self.colorTest) self.registerCommand('colorTest', self.colorTest)
self.registerAlias('run', ['go', '-r']) self.registerAlias('run', ['go', '-r'])
self.gameBase.registerIO('container', self.container) self.gameBase.registerIO('container', self.container)
self.gameBase.registerIO('dialog', self.dialog)
# Helper functions # Helper functions
@ -111,6 +117,7 @@ If -l is given, a map legend will be printed under the map."""
rows = [] rows = []
index = 0 index = 0
exits = {} exits = {}
characters = {}
doors = {} doors = {}
useables = {} useables = {}
items = {} items = {}
@ -135,6 +142,9 @@ If -l is given, a map legend will be printed under the map."""
if thing.thingType == 'x': # exit if thing.thingType == 'x': # exit
rows[-1].append('X{0}'.format(thing.exitid)) rows[-1].append('X{0}'.format(thing.exitid))
exits[thing.exitid] = (thing.name, thing.graphic[1]) exits[thing.exitid] = (thing.name, thing.graphic[1])
elif thing.thingType == 'c': # useable
characters[len(characters)+1] = (thing.name, thing.graphic[1])
rows[-1].append('C{0}'.format(len(characters)))
elif thing.thingType == 'd': # door elif thing.thingType == 'd': # door
doors[len(doors)+1] = (thing.name, thing.graphic[1]) doors[len(doors)+1] = (thing.name, thing.graphic[1])
rows[-1].append('D{0}'.format(len(doors))) rows[-1].append('D{0}'.format(len(doors)))
@ -173,6 +183,9 @@ If -l is given, a map legend will be printed under the map."""
"Xn - Exit to another area"] "Xn - Exit to another area"]
for i in exits: for i in exits:
legend.append(' {0}X{1}{2} - {3}'.format(self.color(exits[i][1][1:]), i, self.clearColor(), exits[i][0])) legend.append(' {0}X{1}{2} - {3}'.format(self.color(exits[i][1][1:]), i, self.clearColor(), exits[i][0]))
legend.append("Cn - Character")
for i in characters:
legend.append(' {0}U{1}{2} - {3}'.format(self.color(characters[i][1][1:]), i, self.clearColor(), characters[i][0]))
legend.append("Un - Useable object") legend.append("Un - Useable object")
for i in useables: for i in useables:
legend.append(' {0}U{1}{2} - {3}'.format(self.color(useables[i][1][1:]), i, self.clearColor(), useables[i][0])) legend.append(' {0}U{1}{2} - {3}'.format(self.color(useables[i][1][1:]), i, self.clearColor(), useables[i][0]))
@ -262,6 +275,33 @@ If -l is given, a map legend will be printed under the map."""
instr = input("Take, store, or exit: ") instr = input("Take, store, or exit: ")
return inv, cont, timeSpent return inv, cont, timeSpent
def dialog(self, dialogObj):
if 'opener' in dialogObj:
print(_tw.fill(dialogObj['opener'], width = TERM_SIZE))
while isinstance(dialogObj, dict):
if 'action' in dialogObj:
action = dialogObj['action']
if action == 'answer':
answer = 0
if 'answers' in dialogObj and isinstance(dialogObj['answers'], list):
for i in range(len(dialogObj['answers'])):
print(_tw.fill('{}: {}'.format(i+1, dialogObj['answers'][i]), width = TERM_SIZE))
answer = int(input(self.ps2)) - 1
if 'replies' in dialogObj and isinstance(dialogObj['replies'], list):
ret = self.dialog(dialogObj['replies'][answer])
if ret > 1:
return ret - 1
elif ret == 0:
return 0
# if ret == 1, then do this dialog again
elif len(action) >= 4 and action[:4] == 'back':
if len(action) == 4:
return 1
return int(action[4:])
elif action == 'exit':
return 0
def update(self): def update(self):
self.gameBase.gameEventLoop() self.gameBase.gameEventLoop()

View file

@ -42,4 +42,11 @@ loadAlways:
location: [21, 23] location: [21, 23]
destination: testing/test4.yml destination: testing/test4.yml
name: downstairs name: downstairs
- !NPC
name: guy
description: a guy
location: [4, 20]
behavior: wander
customValues:
dialogs: testing/testDialog.yml