gameshell/gameshell.py

597 lines
26 KiB
Python
Raw Normal View History

# gameshell.py
from shell import Shell
from gamebase import GameBase
import sys as _sys
import os as _os
#import re
import heapq
#import gamemap
import gameevents
import textwrap as _tw
from shutil import get_terminal_size as _gts
import gameutil as _gu
#import random
TERM_SIZE = _gts()[0]
class GameShell(Shell):
UP = 1
RIGHT = 2
DOWN = 4
LEFT = 8
WALLS = ('++', '++', ' +', '++', '++', '||', '++', '|+', '+ ', '++',
'==', '++', '++', '+|', '++', '++')
def __init__(self, gameBase, gameData = 'testing/testdata.yml'):
super(GameShell, self).__init__()
self.outstream = _sys.stdout
self.gameBase = gameBase
self.colorMode = 0
data = self.gameBase.loadGameData(gameData)
2019-06-08 13:42:00 -04:00
self.gameTitle = 'Game Shell' # should be changed for actual games
if 'title' in data:
self.gameTitle = data['title']
2019-06-08 13:42:00 -04:00
self.startLevel = 'testing/test1.yml' # should be changed for actual games
if 'startLevel' in data:
self.startLevel = data['startLevel']
2019-06-08 13:42:00 -04:00
self.openingText = '{}\nIn Development'
if 'openingText' in data:
self.openingText = data['openingText']
if 'playerName' in data:
self.gameBase.playerName = data['playerName']
if 'playerDescription' in data:
self.gameBase.playerDescription = data['playerDescription']
self.ps2 = '?> '
2019-06-08 13:42:00 -04:00
self.__inGame = False
# register functions
self.registerCommand('load', self.loadGame) # should always be available
2019-06-08 13:42:00 -04:00
self.registerCommand('flippetywick', self.devMode)
self.registerCommand('options', self.options)
self.registerCommand('colorTest', self.colorTest)
2019-06-08 13:42:00 -04:00
self.registerCommand('man', self.man)
self.registerCommand('help', self.man)
self.registerCommand('new', self.newGame)
self.gameBase.registerIO('container', self.container)
self.gameBase.registerIO('startDialog', self.startDialog)
self.gameBase.registerIO('openDialog', self.openDialog)
self.gameBase.registerIO('respondDialog', self.respondDialog)
self.gameBase.registerIO('endDialog', self.endDialog)
self.gameBase.registerIO('info', self.info)
2019-06-08 13:42:00 -04:00
self.gameBase.registerIO('playercmd', self.playercmd)
# Helper functions
def man(self, args):
super(GameShell, self).man(args)
def options(self, args):
i = 0
while i < len(args):
if args[i] == 'color':
i += 1
if len(args) > i:
if args[i] in ('0', '3', '4', '8', '24'):
self.colorMode = int(args[i])
i += 1
else:
print("Valid color depths are 0, 3, 4, 8, and 24.")
else:
print("Valid color depths are 0, 3, 4, 8, and 24.")
elif args[i] == 'automove':
i += 1
if len(args) > i:
if args[i] in ('on', 'off'):
self.gameBase.autoMove = args[i] == 'on'
i += 1
else:
print("Valid options are 'on' and 'off'.")
else:
print("Valid options are 'on' and 'off'.")
else:
print("Could not identify option {}".format(args[i]))
i += 1
def __colorTestRoutine(self, lower, upper):
colors = []
step = int((upper - lower) / 8)
#print(step)
for g in range(lower, upper, step):
colors.append(self.color(upper-1, g, lower, False))
#print(len(colors))
colors.append(self.color(upper-1, upper-1, lower, False))
for r in range(upper-step, lower-1, -step):
colors.append(self.color(r, upper-1, lower, False))
#print(len(colors))
for b in range(lower+step, upper, step):
colors.append(self.color(lower, upper-1, b, False))
#print(len(colors))
colors.append(self.color(lower, upper-1, upper-1, False))
for g in range(upper-step, lower-1, -step):
colors.append(self.color(lower, g, upper-1, False))
#print(len(colors))
for r in range(lower+step, upper, step):
colors.append(self.color(r, lower, upper-1, False))
colors.append(self.color(upper-1, lower, upper-1, False))
#print(len(colors))
for b in range(upper-step, lower, -step):
colors.append(self.color(upper-1, lower, b, False))
#print(len(colors))
colors.append('\x1b[0m')
print(' '.join(colors))
def colorTest(self, args):
for i in (3, 4, 8, 24):
print(i)
self.colorMode = i
self.__colorTestRoutine(0, 128) # dark
self.__colorTestRoutine(0, 256) # medium
self.__colorTestRoutine(128, 256) # light
2019-06-08 13:42:00 -04:00
def __getAdjFloors(self, level, x, y):
"""Get the floor colors of the 8 nearest tiles."""
adjFloors = []
for i in range(y-1, y+2):
if i < 0 or i >= level.dimensions[1]:
continue
for j in range(x-1, x+2):
if j < 0 or j >= level.dimensions[0]:
continue
if level.mapMatrix[i][j][0] == 'e':
adjFloors.append(level.mapMatrix[i][j][1])
return adjFloors
def __getUnderWallColor(self, level, x, y):
adjFloors = self.__getAdjFloors(level, x, y)
if len(adjFloors) > 0:
counts = {}
highestCount = 0
ret = -1
for i in adjFloors:
if i in counts:
counts[i] += 1
else:
counts[i] = 1
if counts[i] > highestCount:
ret = i
highestCount = counts[i]
elif counts[i] == highestCount and i < ret:
ret = i
return ret
else:
return -1
def showMap(self, args):
"""map [-l]
See a map of the local area. "ls" is an alias of map.
"l" and "legend" are aliases of -l.
If -l is given, a map legend will be printed under the map."""
xAxis = ' ' + ''.join([_gu.numberToLetter(i).ljust(2) for i in range(self.gameBase.level.dimensions[0])]) + '\n'
rows = []
index = 0
exits = {}
characters = {}
doors = {}
useables = {}
items = {}
level = self.gameBase.level
textColor = level.wallColors[0]
floorColor = level.floorColors[0]
priorities = {'p': 1, 'n': 2, 'i': 3, 'u': 4, 'd': 5, 'x': 6, 'a': 7}
for y in range(level.dimensions[1]):
rows.append(['{0}{1:2} {2}{3}'.format(self.clearColor(), y, self.color(textColor[1:]), self.color(floorColor[1:], fg = False))])
rows[-1].append(self.color(textColor[1:]))
for x in range(level.dimensions[0]):
pos = level.mapMatrix[y][x]
things = level.getThingsAtPos(index)
if len(things) > 0:
# Prioritize types: p, n, i, u, d, x, a
thing = things[0]
for i in things[1:]:
if priorities[i.thingType] < priorities[thing.thingType]:
thing = i
if thing.graphic.foreground != textColor:
textColor = thing.graphic.foreground
#print(textColor)
rows[-1].append(self.color(textColor[1:]))
if thing.thingType == 'p': # player
rows[-1].append('()')
elif thing.thingType == 'x': # exit
rows[-1].append('X{0}'.format(thing.exitid))
exits[thing.exitid] = (thing.name, thing.graphic.foreground)
elif thing.thingType == 'n': # NPC
characters[len(characters)+1] = (thing.name, thing.graphic.foreground)
rows[-1].append('C{0}'.format(len(characters)))
elif thing.thingType == 'd': # door
doors[len(doors)+1] = (thing.name, thing.graphic.foreground)
rows[-1].append('D{0}'.format(len(doors)))
elif thing.thingType == 'u': # useable
useables[len(useables)+1] = (thing.name, thing.graphic.foreground)
rows[-1].append('U{0}'.format(len(useables)))
elif thing.thingType == 'i': # item
items[len(items)+1] = (thing.name, thing.graphic.foreground)
rows[-1].append('I{0}'.format(len(items)))
else: # entrance
2019-05-30 15:44:29 -04:00
rows[-1].append(' ')
elif pos[0] == 'w':
if level.wallColors[level.mapMatrix[y][x][1]] != textColor:
textColor = level.wallColors[level.mapMatrix[y][x][1]]
rows[-1].append(self.color(textColor[1:]))
sides = 0
if y > 0 and level.mapMatrix[y-1][x][0] == 'w':
sides += GameShell.UP
if x < level.dimensions[0]-1 and level.mapMatrix[y][x+1][0] == 'w':
sides += GameShell.RIGHT
if y < level.dimensions[1]-1 and level.mapMatrix[y+1][x][0] == 'w':
sides += GameShell.DOWN
if x > 0 and level.mapMatrix[y][x-1][0] == 'w':
sides += GameShell.LEFT
2019-06-08 13:42:00 -04:00
underWallColor = self.__getUnderWallColor(level, x, y)
if level.floorColors[underWallColor] != floorColor and underWallColor != -1:
floorColor = level.floorColors[underWallColor]
rows[-1].append(self.color(floorColor[1:], fg = False))
rows[-1].append(GameShell.WALLS[sides])
else:
2019-06-08 13:42:00 -04:00
if level.floorColors[level.mapMatrix[y][x][1]] != floorColor:
floorColor = level.floorColors[level.mapMatrix[y][x][1]]
rows[-1].append(self.color(floorColor[1:], fg = False))
rows[-1].append(' ')
index += 1
rows[-1] = ''.join(rows[-1])
print(xAxis)
print('{0}\n'.format(self.clearColor(False)).join(rows) + self.clearColor())
self.setClearColor()
if len(args) > 0:
if args[0] == '-l' or args[0] == 'l' or args[0] == 'legend':
legend = ["\n---Legend---\n", "{0}(){1} - {2}".format(self.color('0000FF'), self.clearColor(), self.gameBase.playerName),
"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]))
legend.append("In - Item")
for i in items:
legend.append(' {0}I{1}{2} - {3}'.format(self.color(items[i][1][1:]), i, self.clearColor(), items[i][0]))
legend.append("Dn - Door")
for i in doors:
legend.append(' {0}D{1}{2} - {3}'.format(self.color(doors[i][1][1:]), i, self.clearColor(), doors[i][0]))
print('\n'.join(legend))
#heapq.heappush(self.gameBase.eventQueue, (self.gameBase.gameTime, gameevents.NoOpEvent()))
return
def status(self, args):
ret = []
if self.gameBase.level != None:
ret.append("Level:{0:.>74}".format(self.gameBase.level.name))
else:
ret.append("Level:{0:.>74}".format("None"))
ret.append("Gametime:{0:.>71.3}".format(self.gameBase.gameTime))
ret.append("Event queue:")
for i in sorted(self.gameBase.eventQueue):
ret.append("{0:.<8.3}:{1:.>71}".format(i[0], str(i[1])))
2019-05-30 15:44:29 -04:00
ret.append("custom values:")
for i in self.gameBase.customValues:
ret.append("{0:<22}: {1}".format(i, self.gameBase.customValues[i]))
ret.append("Player name:{0:.>68}".format(self.gameBase.player.name))
ret.append("Player position:{0:.>64}".format("{0}{1}".format(_gu.numberToLetter(self.gameBase.player.x), self.gameBase.player.y)))
ret.append("Prev. position:{0:.>65}".format("{0}{1}".format(_gu.numberToLetter(self.gameBase.player.prevx), self.gameBase.player.prevy)))
ret.append("Inventory:")
for i in self.gameBase.player.inventory:
ret.append("{0:<8}: {1}".format(i.thingID, i.name))
ret.append("Things:\nID Name X Y")
for i in self.gameBase.level.things:
j = self.gameBase.level.things[i]
ret.append("{0:<7} {1:<31} {2:<3} {3:<3}".format(i, j.name, j.x, j.y))
ret.append("Persistent:")
for i in self.gameBase.level.persistent:
ret.append(str(i))
print('\n'.join(ret))
#heapq.heappush(self.gameBase.eventQueue, (self.gameBase.gameTime, gameevents.NoOpEvent()))
return
def inv(self, args):
print('\n'.join([i for i in self.gameBase.player.thingNames]))
2019-06-08 13:42:00 -04:00
def newGame(self, args):
if self.__inGame:
response = input("Do you want to save before exiting (Y/n/x)? ")
if len(response) > 0:
if response[0] in 'Nn':
pass
2019-06-08 13:42:00 -04:00
elif response[0] in 'Xx': # cancel
return
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
2019-06-08 13:42:00 -04:00
else:
print('No save file given, cancelling exit.')
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
2019-06-08 13:42:00 -04:00
else:
print('No save file given, cancelling exit.')
try:
self.gameBase.loadMap([self.startLevel])
except RuntimeError as e:
print(e)
return
self.gameMode()
def loadGame(self, args):
# Check if there's a name in args. If not, just present a list of save files.
if len(args) == 0:
import pickle as _pi
for fileName in _os.listdir('saves'):
with open(f'saves/{fileName}', 'rb') as f:
player, levelname, persist, eventQueue, customValues, gameTime, nextThing = _pi.load(f)
print(f'{fileName[:-4]}\n - {levelname[5:-4]}')
return
if self.__inGame:
response = input("Do you want to save before exiting (Y/n/x)? ")
if len(response) > 0:
if response[0] in 'Nn':
pass
elif response[0] in 'Xx': # cancel
return
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
else:
print('No save file given, cancelling exit.')
return
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
else:
print('No save file given, cancelling exit.')
return
try:
self.gameBase.loadGame(args)
except RuntimeError as e:
print(e)
return
self.gameMode()
2019-06-08 13:42:00 -04:00
def gameMode(self):
"""Mode for in-game."""
if not self.__inGame:
self.registerCommand('map', self.showMap)
self.registerCommand('wait', self.gameBase.wait)
2019-06-08 13:42:00 -04:00
self.registerCommand('ls', self.showMap)
self.registerCommand('go', self.gameBase.go)
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('save', self.gameBase.saveGame)
self.registerCommand('take', self.gameBase.take)
self.registerCommand('get', self.gameBase.take)
self.registerCommand('drop', self.gameBase.drop)
self.registerCommand('inv', self.inv)
self.registerCommand('bag', self.inv)
self.registerCommand('items', self.inv)
self.registerAlias('run', ['go', '-r'])
self.__inGame = True
def menuMode(self, text = None):
"""Mode for main menus and the like."""
if self.__inGame:
self.registerCommand('map', self.showMap)
self.unregisterCommand('wait', self.gameBase.wait)
2019-06-08 13:42:00 -04:00
self.unRegisterCommand('ls', self.showMap)
self.unRegisterCommand('go', self.gameBase.go)
self.unRegisterCommand('move', self.gameBase.go)
self.unRegisterCommand('walk', self.gameBase.go)
self.unRegisterCommand('look', self.gameBase.look)
self.unRegisterCommand('talk', self.gameBase.talk)
self.unRegisterCommand('use', self.gameBase.use)
self.unRegisterCommand('save', self.gameBase.saveGame)
self.unRegisterCommand('take', self.gameBase.take)
self.unRegisterCommand('get', self.gameBase.take)
self.unRegisterCommand('drop', self.gameBase.drop)
self.unRegisterCommand('inv', self.inv)
self.unRegisterCommand('bag', self.inv)
self.unRegisterCommand('items', self.inv)
self.unRegisterAlias('run', ['go', '-r'])
self.__inGame = False
if text == None:
print(self.openingText.format(self.gameTitle))
else:
print(text.format(self.gameTitle))
def devMode(self, args):
print('Dev mode activated. Please dev responsibly.')
self.gameMode()
self.registerCommand('dev', self.runScript)
self.registerCommand('status', self.status)
self.registerCommand('loadMap', self.gameBase.loadMap)
def runScript(self, args):
"""simple wrapper for gameBase.runscript."""
2021-11-14 22:25:30 -05:00
args = ['"' + a + '"' for a in args]
self.gameBase.parseScript(' '.join(args) + '\n')
2019-06-08 13:42:00 -04:00
2019-05-30 15:44:29 -04:00
# IO calls
def container(self, player, cont: list):
"""container IO
Player is modified through side-effect."""
# Pretty print: get length of the longest inventory item's name
longestLen = 0
for i in player.thingNames:
if len(i) > longestLen:
longestLen = len(i)
if longestLen > 0:
inv = player.inventory # do this assignment because player.inventory is O(n)
print('{{0:<{0}}}{1}'.format(max(6, longestLen+2), "Container:").format("Inv:"))
i = 0
while i < len(inv) and i < len(cont):
print('{{0:<{0}}}{1}'.format(longestLen+2, cont[i].name).format(inv[i].name))
i += 1
while i < len(inv):
print(inv[i].name)
i += 1
while i < len(cont):
print(' '*(longestLen+2) + cont[i].name)
i += 1
else:
print('Container:')
for i in cont:
print(i.name)
# Now, actually interacting with the container
timeSpent = 0.5 # using a container always takes at least 1/2 second, even just opening and closing it again.
instr = input("Look, take, store, or exit: ")
while instr != "exit":
instr = instr.split()
if len(instr) != 0:
if instr[0] == "take":
# take something out of the container
if len(instr) > 1 and instr[1] == "the":
del instr[1]
thingName = ' '.join(instr[1:])
if len(thingName) == 0:
print(f"{self.gameBase.player.name} takes nothing.")
else:
for i in range(len(cont)):
if thingName == cont[i].name:
player.addThing(cont[i])
del cont[i]
timeSpent += 0.5
print(f"{thingName} taken.")
break
else:
# If it got here, it didn't find it.
print(f"No {thingName} in container.")
elif instr[0] == "store":
# store something in the container
if len(instr) > 1 and instr[1] == "the":
del instr[1]
thingName = ' '.join(instr[1:])
if len(thingName) == 0:
print(f"{self.gameBase.player.name} stores nothing.")
elif thingName in player.thingNames:
cont.append(player.removeThingByName(thingName))
print(f"{thingName} stored.")
timeSpent += 0.5
else:
print(f"No {thingName} in inventory.")
elif instr[0] == "look":
# look at something in the container
if len(instr) > 1 and instr[1] == "at":
del instr[1]
if len(instr) > 1 and instr[1] == "the":
del instr[1]
thingName = ' '.join(instr[1:])
if len(thingName) == 0:
print(f"{self.gameBase.player.name} looks at nothing.")
else:
# Check the container first.
for i in range(len(cont)):
if thingName == cont[i].name:
print(self.gameBase.justifyText(str(cont[i])))
break
else:
# If it wasn't there, try the player's inventory.
if thingName in player.thingNames:
print(self.gameBase.justifyText(str(player.getThingByName(thingName))))
else:
# If we get here, it just isn't there.
print(f"There is no {thingName} to look at.")
instr = input("Look, take, store, or exit: ")
return cont, timeSpent
def info(self, items):
"""IO for collections of information"""
charsRead = 0
for i in items:
print(' ', i)
instr = input("Choose an item to view, or exit: ")
while instr != 'exit':
if instr in items:
print(_tw.fill(items[instr], width = TERM_SIZE))
charsRead += len(items[instr])
else:
print('{} not here.'.format(instr))
2019-06-08 13:42:00 -04:00
input('<ENTER>')
for i in items:
print(' ', i)
instr = input("Choose an item to view, or exit: ")
return charsRead / 27 # based on average 250 words per minute, and word length of 5.5 + 1 for space.
def startDialog(self):
pass
def openDialog(self, opener):
for line in opener:
print(_tw.fill(line, width = TERM_SIZE))
input("...")
def respondDialog(self, options):
for lineNo in range(len(options)):
print(_tw.fill('{}: {}'.format(lineNo+1, options[lineNo]), width = TERM_SIZE))
answer = -1
while answer < 0 or answer >= len(options):
answerString = input(self.ps2)
if not answerString.isdigit():
# If the player inputs a non-integer, just prompt again.
continue
answer = int(answerString) - 1
return answer
def endDialog(self):
pass
2019-05-30 15:44:29 -04:00
def playercmd(self, args):
self.handleCommand(args)
def update(self):
self.gameBase.gameEventLoop()
2019-06-08 13:42:00 -04:00
def exitShell(self, args):
if self.__inGame:
response = input("Do you want to save before exiting (Y/n/x)? ")
if len(response) > 0:
if response[0] in 'Nn':
super(GameShell, self).exitShell(args)
elif response[0] in 'Xx': # cancel
return
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
2019-06-08 13:42:00 -04:00
super(GameShell, self).exitShell(args)
else:
print('No save file given, cancelling exit.')
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
2019-06-08 13:42:00 -04:00
super(GameShell, self).exitShell(args)
else:
print('No save file given, cancelling exit.')
else:
super(GameShell, self).exitShell(args)
if __name__ == '__main__':
sh = GameShell(GameBase())
sh.menuMode()
sh.run()
# |\_/|
# /0 0\
# \o/