The player can now have simple conversations with NPCs.
This commit is contained in:
parent
e0b88e7a45
commit
e81345e793
4 changed files with 115 additions and 10 deletions
65
gamebase.py
65
gamebase.py
|
@ -7,6 +7,10 @@ import gameevents as _ge
|
|||
import random as _ra
|
||||
import sys as _sys
|
||||
import pickle as _pi
|
||||
import ruamel.yaml as _yaml
|
||||
|
||||
class GameError(RuntimeError):
|
||||
pass
|
||||
|
||||
class GameBase(object):
|
||||
|
||||
|
@ -18,6 +22,7 @@ class GameBase(object):
|
|||
def __init__(self):
|
||||
self.outstream = _sys.stdout
|
||||
self.__useFuncs = {}
|
||||
self.__behaviors = {}
|
||||
self.__gameEvents = {}
|
||||
self.__IOCalls = {} # {str : function}
|
||||
self.customVals = {} # for setting flags and such
|
||||
|
@ -49,9 +54,11 @@ class GameBase(object):
|
|||
self.registerEvent('useon', self.handleUseOn)
|
||||
self.registerEvent('take', self.handleTake)
|
||||
self.registerEvent('drop', self.handleDrop)
|
||||
self.registerEvent('behave', self.handleBehave)
|
||||
self.registerUseFunc('examine', self.examine)
|
||||
self.registerUseFunc('key', self.key)
|
||||
self.registerUseFunc('container', self.container)
|
||||
self.registerBehavior('wander', self.wander)
|
||||
|
||||
# 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".
|
||||
The letter is not case-sensitive."""
|
||||
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
|
||||
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
|
||||
speed = 0.3333333
|
||||
|
@ -236,13 +243,46 @@ Object can be the name of the object, or its coordinates."""
|
|||
args.pop(0)
|
||||
thing, x, y = self.parseCoords(args, usePlayerCoords = False)
|
||||
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:
|
||||
print("There is nothing to see here.\n", file = self.outstream)
|
||||
else:
|
||||
print(self.justifyText(str(thing)), file = self.outstream)
|
||||
#_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):
|
||||
"""use [-r] [the] object [on [the] object2]
|
||||
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()))
|
||||
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):
|
||||
#print(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)
|
||||
del self.playerInv[e.item.name]
|
||||
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
|
||||
|
||||
|
@ -629,12 +679,21 @@ Object can be the name of the object, or its coordinates."""
|
|||
else:
|
||||
print("The key doesn't fit that lock.", file = self.outstream)
|
||||
return 0.125
|
||||
|
||||
# behaviors
|
||||
|
||||
def wander(self, actor):
|
||||
pass
|
||||
|
||||
# stuff for extended classes to use
|
||||
def registerUseFunc(self, name, func):
|
||||
"""Registers a function for use by things in the map, but not directly
|
||||
callable by the player."""
|
||||
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):
|
||||
"""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):
|
||||
"""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]
|
||||
|
|
11
gamemap.py
11
gamemap.py
|
@ -190,6 +190,7 @@ class NPC(Thing):
|
|||
self.inventory = inventory
|
||||
self.customValues = customValues
|
||||
self.graphic = graphic
|
||||
self.behaveEvent = None
|
||||
|
||||
def give(self, 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.register_class(Item)
|
||||
yaml.register_class(Useable)
|
||||
yaml.register_class(NPC)
|
||||
yaml.register_class(Door)
|
||||
yaml.register_class(MapExit)
|
||||
if infile != None:
|
||||
|
@ -553,14 +555,9 @@ Entering a map through stdin will be obsolete once testing is over."""
|
|||
info = yaml.load(f)
|
||||
except OSError as e:
|
||||
print("The file could not be read.")
|
||||
return None
|
||||
return None, nextThing
|
||||
else:
|
||||
|
||||
while tryToRead:
|
||||
try:
|
||||
data += input()
|
||||
except EOFError as e:
|
||||
tryToRead = False
|
||||
raise RuntimeError("No file was specified for loading.")
|
||||
|
||||
# Now what we do with the data
|
||||
mat = None
|
||||
|
|
42
gameshell.py
42
gameshell.py
|
@ -7,9 +7,12 @@ import sys as _sys
|
|||
import heapq
|
||||
#import gamemap
|
||||
import gameevents
|
||||
#from os import get_terminal_size
|
||||
import textwrap as _tw
|
||||
from shutil import get_terminal_size as _gts
|
||||
#import random
|
||||
|
||||
TERM_SIZE = _gts()[0]
|
||||
|
||||
class GameShell(Shell):
|
||||
|
||||
UP = 1
|
||||
|
@ -24,6 +27,7 @@ class GameShell(Shell):
|
|||
self.outstream = _sys.stdout
|
||||
self.gameBase = gameBase
|
||||
self.colorMode = 0
|
||||
self.ps2 = '?> '
|
||||
|
||||
# register functions
|
||||
|
||||
|
@ -33,6 +37,7 @@ class GameShell(Shell):
|
|||
self.registerCommand('move', self.gameBase.go)
|
||||
self.registerCommand('walk', self.gameBase.go)
|
||||
self.registerCommand('look', self.gameBase.look)
|
||||
self.registerCommand('talk', self.gameBase.talk)
|
||||
self.registerCommand('use', self.gameBase.use)
|
||||
self.registerCommand('loadMap', self.gameBase.loadMap)
|
||||
self.registerCommand('man', self.man)
|
||||
|
@ -50,6 +55,7 @@ class GameShell(Shell):
|
|||
self.registerCommand('colorTest', self.colorTest)
|
||||
self.registerAlias('run', ['go', '-r'])
|
||||
self.gameBase.registerIO('container', self.container)
|
||||
self.gameBase.registerIO('dialog', self.dialog)
|
||||
|
||||
# Helper functions
|
||||
|
||||
|
@ -111,6 +117,7 @@ If -l is given, a map legend will be printed under the map."""
|
|||
rows = []
|
||||
index = 0
|
||||
exits = {}
|
||||
characters = {}
|
||||
doors = {}
|
||||
useables = {}
|
||||
items = {}
|
||||
|
@ -135,6 +142,9 @@ If -l is given, a map legend will be printed under the map."""
|
|||
if thing.thingType == 'x': # exit
|
||||
rows[-1].append('X{0}'.format(thing.exitid))
|
||||
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
|
||||
doors[len(doors)+1] = (thing.name, thing.graphic[1])
|
||||
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"]
|
||||
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("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")
|
||||
for i in useables:
|
||||
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: ")
|
||||
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):
|
||||
self.gameBase.gameEventLoop()
|
||||
|
||||
|
|
|
@ -42,4 +42,11 @@ loadAlways:
|
|||
location: [21, 23]
|
||||
destination: testing/test4.yml
|
||||
name: downstairs
|
||||
- !NPC
|
||||
name: guy
|
||||
description: a guy
|
||||
location: [4, 20]
|
||||
behavior: wander
|
||||
customValues:
|
||||
dialogs: testing/testDialog.yml
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue